From 3cba3bb74eeed9c2b1a537ffe9ccf921d0476a7a Mon Sep 17 00:00:00 2001 From: dongzi Date: Fri, 5 Jun 2026 16:18:40 +0800 Subject: [PATCH] commit --- .idea/AI-Check-Test.iml | 4 +- .idea/sqldialects.xml | 17 + .../main/java/ftb/test/controller/.gitkeep | 2 - .../ftb/test/controller/UserController.java | 44 - .../test/controller/UserCreateRequest.java | 26 - jnpf-ftb/jnpf-ftb-api/pom.xml | 62 + .../java/jnpf/account/PTenantAccountApi.java | 72 + .../fallback/PTenantAccountApiFallBack.java | 72 + .../PTenantAccountApiFallBackFactory.java | 22 + .../java/jnpf/attendance/AttendanceApi.java | 35 + .../jnpf/attendance/AttendanceConfirmApi.java | 27 + .../attendance/AttendanceDailyRuleApi.java | 63 + .../jnpf/attendance/AttendanceGroupApi.java | 59 + .../AttendanceLineSchedulingConfigApi.java | 25 + .../attendance/AttendanceSimulateDataApi.java | 24 + .../jnpf/attendance/AttendanceUserApi.java | 39 + .../java/jnpf/attendance/FtbClockInApi.java | 44 + .../jnpf/attendance/FtbStatisticsApi.java | 108 + .../dto/AttendanceCountAvgHoursDto.java | 32 + .../dto/AttendanceCountAvgHoursVo.java | 27 + .../attendance/dto/AttendanceUserGroupVo.java | 24 + .../dto/AttendanceUserListGroupVO.java | 34 + .../jnpf/attendance/dto/CompareTypeEnums.java | 30 + .../dto/DateDimensionsRangeDto.java | 31 + .../attendance/dto/DateDimensionsRangeVo.java | 24 + .../dto/DimensionsAttendanceCountDto.java | 26 + .../dto/DimensionsAttendanceDayCountDto.java | 33 + .../attendance/dto/GroupUpdateByUserDTO.java | 28 + .../dto/MonthStatsAbnormalConditionVo.java | 55 + .../dto/MonthStatsDailySituationVo.java | 23 + .../attendance/dto/MonthStatsDetailsDto.java | 27 + .../attendance/dto/MonthStatsDetailsVo.java | 63 + .../dto/MonthStatsFullSituationVo.java | 31 + .../dto/MonthStatsHoursRankingVo.java | 23 + .../dto/MonthStatsOvertimeSituationVo.java | 32 + .../attendance/dto/MonthStatsPerCapitaVo.java | 23 + .../attendance/dto/MonthStatsSituationVo.java | 27 + .../fallback/AttendanceApiFallback.java | 51 + .../AttendanceConfirmApiFallback.java | 37 + .../AttendanceDailyRuleApiFallback.java | 53 + .../fallback/AttendanceGroupApiFallback.java | 62 + ...ndanceLineSchedulingConfigApiFallback.java | 31 + .../AttendanceSimulateDataApiFallback.java | 30 + .../fallback/AttendanceUserApiFallback.java | 38 + .../fallback/FtbClockInApiFallback.java | 60 + .../fallback/FtbStatisticsApiFallback.java | 129 + .../java/jnpf/authority/FtbAuthorityApi.java | 226 + .../fallback/FtbAuthorityApiFallback.java | 188 + .../certificate/CertificateManageApi.java | 57 + .../certificate/CertificateWarningApi.java | 19 + .../CertificateManageFallbackApi.java | 44 + .../CertificateWarningFallbackApi.java | 16 + .../cultivate/FtbCultivateIdentifyApi.java | 33 + .../FtbCultivateLearnTaskListApi.java | 33 + .../cultivate/FtbCultivatePromotionApi.java | 42 + .../FtbCultivateStoreStatisticApi.java | 101 + .../cultivate/FtbCultivateTeachingApi.java | 51 + .../jnpf/cultivate/V2CultivateOldDealApi.java | 51 + .../FtbCultivateIdentifyApiFallback.java | 29 + .../FtbCultivateLearnTaskApiFallback.java | 31 + .../FtbCultivatePromotionFallback.java | 34 + ...FtbCultivateStoreStatisticApiFallback.java | 76 + .../FtbCultivateTeachingApiFallback.java | 42 + .../V2CultivateOldDealApiFallback.java | 39 + .../main/java/jnpf/doclibrary/StoreApi.java | 205 + .../doclibrary/fallback/StoreFallback.java | 178 + .../java/jnpf/exam/V2CultivateTimingApi.java | 43 + .../V2CultivateTimingApiFallback.java | 25 + .../java/jnpf/franchisee/FranchiseeApi.java | 71 + .../fallback/FranchiseeFallbackApi.java | 55 + .../main/java/jnpf/notice/FtbNoticeApi.java | 23 + .../notice/fallback/FtbNoticeFallBackApi.java | 18 + .../java/jnpf/personnels/FtbPersonneApi.java | 72 + .../FtbPersonnelsContactInfoManagerApi.java | 38 + .../personnels/FtbPersonnelsEmEntryApi.java | 19 + .../FtbPersonnelsEmployeeTypeRemoteApi.java | 30 + .../FtbPersonnelsEmploymentApplyApi.java | 28 + .../FtbPersonnelsInfoConfigApi.java | 44 + .../FtbPersonnelsMetaDataManagerApi.java | 59 + .../FtbPersonnelsRewardsPunishmentsApi.java | 50 + ...PersonnelsRewardsPunishmentsRemoteApi.java | 25 + .../FtbPersonnelsRosterManagerApi.java | 147 + .../FtbPersonnelsTurnoverManagementApi.java | 52 + ...rsonnelsContaceInfoManagerFallBackApi.java | 24 + ...rsonnelsEmployeeTypeRemoteFallBackApi.java | 18 + ...bPersonnelsEmploymentApplyFallBackApi.java | 26 + .../FtbPersonnelsInfoConfigBackApi.java | 29 + ...bPersonnelsMetaDataManagerFallBackApi.java | 47 + ...lsRewardsPunishmentsRemoteFallBackApi.java | 18 + ...FtbPersonnelsRosterManagerFallBackApi.java | 107 + ...rsonnelsTurnoverManagementFallBackApi.java | 47 + .../jnpf/util/auth/V2AuthPermissionApi.java | 22 + .../fallback/V2AuthPermissionApiFallback.java | 28 + jnpf-ftb/jnpf-ftb-biz/pom.xml | 333 + .../main/java/jnpf/JnpfFtbApplication.java | 24 + .../java/jnpf/aspect/ApiCallLogAspect.java | 98 + .../main/java/jnpf/aspect/FtbApiCallLog.java | 12 + .../jnpf/attendance/annotation/Machine.java | 18 + .../attendance/annotation/MachineAspect.java | 230 + .../attendance/antifreeze/UserAntifreeze.java | 307 + .../jnpf/attendance/bean/ChangeConfig.java | 51 + .../bean/FtbThreadPoolExecutor.java | 160 + .../controller/AppStatisticsController.java | 128 + .../controller/AttenceMachineController.java | 232 + .../controller/AttendanceAIController.java | 51 + .../AttendanceApproveController.java | 829 +++ .../AttendanceBaseSettingController.java | 121 + .../AttendanceBookConfigController.java | 305 + .../AttendanceBookOperationLogController.java | 119 + .../AttendanceBookRecordController.java | 244 + .../AttendanceChangeController.java | 112 + .../AttendanceClockInController.java | 385 ++ .../AttendanceClockInPicController.java | 42 + .../AttendanceCloudAlbumController.java | 61 + .../AttendanceConfirmController.java | 192 + .../AttendanceCustomizeTableController.java | 43 + .../AttendanceDailyRuleController.java | 399 ++ .../AttendanceFestivalRulesController.java | 103 + .../AttendanceFestivalSettingController.java | 122 + .../controller/AttendanceGroupController.java | 615 ++ .../AttendanceGroupUserController.java | 228 + .../AttendanceHolidaySettingController.java | 98 + .../AttendanceLeaveRulesController.java | 191 + ...endanceLineSchedulingConfigController.java | 107 + .../AttendanceLocationSettingController.java | 122 + .../AttendanceMachineManageController.java | 163 + .../AttendanceNoticeController.java | 64 + .../AttendancePermissionDictController.java | 71 + .../AttendanceQuickTemplateController.java | 100 + .../AttendanceShiftNameSettingController.java | 89 + .../AttendanceShiftSettingController.java | 99 + .../AttendanceSimulatedDataController.java | 76 + .../AttendanceSuperAdminController.java | 242 + .../AttendanceUserBalanceController.java | 71 + .../AttendanceUserSettingController.java | 88 + .../controller/FfiMachineController.java | 92 + .../controller/InitializationController.java | 54 + .../controller/IsPerfMachineController.java | 190 + .../attendance/controller/KeMiController.java | 88 + .../controller/OvertimeRuleController.java | 116 + .../PublicHolidayRulesController.java | 93 + .../controller/RV1109MachineController.java | 227 + .../ScheduleGroupRuleConfigController.java | 77 + .../SmartPreScheduleController.java | 75 + .../controller/UserConfigController.java | 46 + .../controller/UserFaceController.java | 233 + .../controller/UserPhoneController.java | 88 + .../controller/WebStatisticsController.java | 489 ++ .../controller/WorkstationController.java | 146 + .../attendance/entity/FixedHandleDTO.java | 32 + .../event/NotificationClockMQListener.java | 105 + .../event/OnboardingConsumerMQListener.java | 82 + .../OnboardingFailConsumerMQListener.java | 58 + .../event/OrganizeConsumerMQListener.java | 58 + .../event/OrganizeUserConsumerMQListener.java | 54 + .../event/SecondmentConsumerMQListener.java | 86 + ...econdmentWithdrawalConsumerMQListener.java | 94 + .../event/StatisticsBatchClearMQListener.java | 71 + .../StatisticsSingleHistoryMQListener.java | 104 + .../event/StatisticsSingleMQListener.java | 103 + .../excel/AttendanceBookMergeHandler.java | 129 + .../excel/CompositeWriteHandler.java | 52 + .../attendance/excel/HeadStyleHandler.java | 251 + .../listener/CustomStringStringConverter.java | 41 + .../listener/LineSchedulesDataListener.java | 161 + .../excel/listener/SchedulesDataListener.java | 177 + .../handler/JsonToListTypeHandler.java | 86 + .../attendance/mapper/ApiCallLogMapper.java | 13 + .../attendance/mapper/AttendanceAIMapper.java | 23 + .../AttendanceApprovalSettingMapper.java | 16 + .../mapper/AttendanceApproveMapper.java | 32 + .../mapper/AttendanceBalanceRecordMapper.java | 231 + .../AttendanceBalanceUseRecordMapper.java | 58 + .../mapper/AttendanceBaseSettingMapper.java | 22 + .../mapper/AttendanceBookConfigMapper.java | 16 + .../AttendanceBookOperationLogMapper.java | 15 + .../mapper/AttendanceBookRecordMapper.java | 18 + ...ttendanceCardReplacementApproveMapper.java | 6 + .../mapper/AttendanceClockInMapper.java | 114 + .../mapper/AttendanceClockInPicMapper.java | 13 + .../mapper/AttendanceClockInResultMapper.java | 189 + .../mapper/AttendanceCloudAlbumMapper.java | 12 + .../AttendanceConfirmDetailsMapper.java | 14 + .../mapper/AttendanceConfirmMapper.java | 39 + .../AttendanceConfirmSettingMapper.java | 14 + .../AttendanceCustomizeTableMapper.java | 16 + .../mapper/AttendanceDailyRuleMapper.java | 166 + .../mapper/AttendanceDayStatisticsMapper.java | 381 ++ .../mapper/AttendanceFestivalRulesMapper.java | 30 + .../AttendanceFestivalSettingMapper.java | 16 + .../AttendanceFieldPersonnelMapper.java | 26 + ...AttendanceFieldpersonnelApproveMapper.java | 6 + .../mapper/AttendanceFixedClassMapper.java | 30 + .../mapper/AttendanceGroupMapper.java | 166 + .../mapper/AttendanceGroupUserMapper.java | 97 + .../AttendanceHolidaySettingMapper.java | 16 + .../mapper/AttendanceLeaveApproveMapper.java | 319 + .../AttendanceLeaveGrantSettingMapper.java | 9 + .../mapper/AttendanceLeaveRulesMapper.java | 32 + .../mapper/AttendanceLeaveSettingsMapper.java | 7 + .../mapper/AttendanceLeaveTypeMapper.java | 10 + .../AttendanceLocationSettingMapper.java | 22 + .../mapper/AttendanceMachineLogMapper.java | 27 + .../mapper/AttendanceMachineManageMapper.java | 89 + .../mapper/AttendanceMachineSyncMapper.java | 13 + .../AttendanceManagerPermissionMapper.java | 117 + .../mapper/AttendanceNoticeMapper.java | 19 + .../AttendanceQuickTemplateItemMapper.java | 16 + .../mapper/AttendanceQuickTemplateMapper.java | 64 + .../mapper/AttendanceRepairMapper.java | 23 + .../AttendanceResultRollbackMapper.java | 13 + .../mapper/AttendanceSealSettingMapper.java | 7 + .../mapper/AttendanceSelfApproveMapper.java | 21 + .../AttendanceShiftNameSettingMapper.java | 101 + .../mapper/AttendanceShiftSettingMapper.java | 16 + .../AttendanceShiftSettingPeriodMapper.java | 16 + .../mapper/AttendanceUserBalanceMapper.java | 36 + .../AttendanceUserBalanceRecordMapper.java | 14 + .../mapper/AttendanceUserConfigMapper.java | 22 + .../mapper/AttendanceUserFaceMapper.java | 37 + .../AttendanceUserFingerprintMapper.java | 13 + .../mapper/AttendanceUserSettingMapper.java | 39 + .../mapper/ClockInResultMapper.java | 13 + .../mapper/ClockInResultV2Mapper.java | 13 + .../mapper/CommonSettingMapper.java | 13 + .../mapper/DailyRuleChangeMapper.java | 31 + .../mapper/EnableBalanceMapper.java | 37 + .../FtbAttendanceFaceChangeLogMapper.java | 23 + ...bAttendanceLineSchedulingConfigMapper.java | 20 + ...danceLineSchedulingPayrollHoursMapper.java | 18 + .../FtbScheduleGroupDrawingParamMapper.java | 22 + .../FtbScheduleGroupFixedParamMapper.java | 22 + .../mapper/InitializationMapper.java | 23 + .../mapper/OvertimeRuleDetailMapper.java | 13 + .../attendance/mapper/OvertimeRuleMapper.java | 24 + .../mapper/PermissionDictMapper.java | 16 + .../mapper/PublicHolidayRulesMapper.java | 96 + .../attendance/mapper/RuleScopeMapper.java | 45 + .../attendance/mapper/StatisticsMapper.java | 73 + .../attendance/mapper/StorageRestMapper.java | 24 + .../attendance/mapper/UserPhoneMapper.java | 24 + .../attendance/mapper/WorkstationMapper.java | 13 + .../mapper/WorkstationUserMapper.java | 13 + .../AttendanceGroupShiftMatchConfig.java | 38 + .../AttendanceGroupShiftMatchResult.java | 41 + .../AttendanceGroupShiftPeriodMatcher.java | 362 + .../schedule/AttendanceScheduleDayParse.java | 96 + .../schedule/AutoSchedulePipelineLog.java | 53 + .../schedule/CommonFixedShiftDiscovery.java | 219 + .../CommonFixedShiftDiscoveryConfig.java | 150 + .../CrossPostGeneralStaffAllocator.java | 293 + .../FinalScheduleRosterFromCoverBuilder.java | 183 + .../schedule/FinalScheduleRosterLine.java | 88 + .../schedule/FixedShiftCoverOutcome.java | 58 + .../FixedShiftGreedyCoverPlanner.java | 140 + .../FixedSoftConstraintRelaxationPlanner.java | 245 + .../schedule/FixedSoftRuleKind.java | 112 + .../HalfHourDemandBasedShiftPlanBuilder.java | 317 + .../schedule/HalfHourPostDemandMatrix.java | 100 + .../schedule/HalfHourSlotLabel.java | 77 + .../schedule/LineShiftCoverConfig.java | 70 + .../schedule/LineShiftGapPlanner.java | 79 + .../attendance/schedule/LineShiftPick.java | 53 + .../schedule/PostFixedShiftCandidate.java | 128 + .../schedule/ScheduleDemandCoverResult.java | 103 + .../schedule/ScheduleDemandCoverSteps678.java | 189 + .../SchedulePatternSimilarDaysAlgorithm.java | 199 + .../schedule/SchedulePeriodWorkTracker.java | 170 + .../ScheduleStaffAssignmentSteps910.java | 146 + .../ScheduleTemplateSimilarDaysAlgorithm.java | 615 ++ .../ScheduleTemplateSimilarDaysResult.java | 123 + .../schedule/SchedulingForTestCheckLog.java | 92 + .../schedule/SchedulingSimilarDaysMode.java | 48 + .../schedule/ShiftHalfHourNormalizer.java | 62 + .../schedule/ShiftPlanAssignmentResult.java | 458 ++ .../ShiftPlanAssignmentResultLogger.java | 101 + .../ShiftPlanAttendanceShiftIdResolver.java | 80 + .../attendance/schedule/ShiftPlanBlock.java | 169 + ...ShiftPlanBlocksFromDemandCoverBuilder.java | 305 + .../schedule/ShiftPlanFinalStaffAssigner.java | 935 +++ .../schedule/ShiftPlanPickSnapshot.java | 222 + .../schedule/ShiftPlanPostNeed.java | 48 + .../schedule/SimilarDayDemandSteps4And5.java | 233 + .../SimilarHistoricalDaySlotTableBuilder.java | 583 ++ .../schedule/SimilarHistoricalJobSlotRow.java | 92 + .../schedule/SlotPostRobustTargetConfig.java | 60 + .../SlotPostTargetHeadcountCalculator.java | 107 + .../schedule/StaffAssignmentContext.java | 84 + .../schedule/StaffAssignmentDayLedger.java | 79 + .../StaffAssignmentIntervalLedger.java | 56 + .../schedule/StaffRuleEvaluationPort.java | 65 + .../StaffRuleEvaluationPortPermissive.java | 42 + ...ortUsingScheduleRulesAndWorkSituation.java | 147 + .../schedule/WorkstationPostStaffBuckets.java | 117 + .../service/AppStatisticsService.java | 91 + .../service/AttenceMachineService.java | 110 + .../service/AttendanceAIService.java | 28 + .../AttendanceApprovalSettingService.java | 18 + .../service/AttendanceApproveService.java | 337 + .../service/AttendanceBaseSettingService.java | 125 + .../service/AttendanceBookConfigService.java | 119 + .../AttendanceBookOperationLogService.java | 31 + .../service/AttendanceBookRecordService.java | 106 + .../service/AttendanceChangeService.java | 58 + .../service/AttendanceClockInPicService.java | 31 + .../AttendanceClockInResultService.java | 29 + .../service/AttendanceClockInService.java | 306 + .../service/AttendanceCloudAlbumService.java | 30 + .../AttendanceConfirmDetailsService.java | 14 + .../service/AttendanceConfirmService.java | 116 + .../AttendanceConfirmSettingService.java | 31 + .../AttendanceCustomizeTableService.java | 35 + .../service/AttendanceDailyRuleService.java | 432 ++ .../AttendanceDayStatisticsService.java | 536 ++ .../AttendanceFestivalRulesService.java | 88 + .../AttendanceFestivalSettingService.java | 106 + .../service/AttendanceFixedClassService.java | 16 + .../service/AttendanceGroupService.java | 480 ++ .../AttendanceHolidaySettingService.java | 87 + .../service/AttendanceLeaveRulesService.java | 79 + .../service/AttendanceLeaveTypeService.java | 52 + ...AttendanceLineSchedulingConfigService.java | 27 + ...anceLineSchedulingPayrollHoursService.java | 98 + .../AttendanceLocationSettingService.java | 116 + .../AttendanceMachineManageService.java | 102 + .../service/AttendanceMachineSyncService.java | 13 + .../service/AttendanceNoticeService.java | 50 + .../AttendancePermissionDictService.java | 39 + .../AttendanceQuickTemplateItemService.java | 16 + .../AttendanceQuickTemplateService.java | 63 + .../service/AttendanceRepairService.java | 17 + .../service/AttendanceSealSettingService.java | 30 + .../AttendanceShiftNameSettingService.java | 82 + .../AttendanceShiftSettingPeriodService.java | 16 + .../AttendanceShiftSettingService.java | 112 + .../service/AttendanceSuperAdminService.java | 176 + .../AttendanceUserBalanceRecordService.java | 54 + .../service/AttendanceUserBalanceService.java | 86 + .../service/AttendanceUserFaceService.java | 47 + .../service/AttendanceUserService.java | 305 + .../service/AttendanceUserSettingService.java | 82 + .../service/ClockInResultService.java | 60 + .../service/CommonSettingService.java | 13 + .../service/DailyRuleChangeService.java | 27 + .../service/EnableBalanceService.java | 37 + .../service/InitializationService.java | 14 + .../service/IsPerfMachineService.java | 36 + .../attendance/service/MachineStrategy.java | 47 + .../attendance/service/NoticeService.java | 27 + .../service/OvertimeRuleDetailService.java | 13 + .../service/OvertimeRuleService.java | 107 + .../service/PublicHolidayRulesService.java | 83 + .../service/RV1109MachineService.java | 20 + .../attendance/service/RuleProcessor.java | 30 + .../attendance/service/RuleScopeService.java | 42 + .../ScheduleGroupRuleConfigService.java | 42 + .../service/SmartPreScheduleService.java | 31 + .../service/StatisticsUtilService.java | 149 + .../attendance/service/UserConfigService.java | 23 + .../attendance/service/UserFaceService.java | 123 + .../attendance/service/UserFaceTxService.java | 18 + .../attendance/service/UsualPhoneService.java | 58 + .../service/WorkstationService.java | 100 + .../attendance/service/filter/RuleFilter.java | 23 + .../service/filter/RuleFilterContext.java | 65 + .../service/filter/RuleFilterResult.java | 35 + .../service/filter/impl/CrossDayFilter.java | 59 + .../service/filter/impl/LeaveFilter.java | 55 + .../service/filter/impl/OvertimeFilter.java | 168 + .../service/filter/impl/SecondmentFilter.java | 48 + .../service/handle/chain/RuleFilterChain.java | 215 + .../notice/AttendanceApproveNotice.java | 214 + .../handle/notice/AttendanceChangeNotice.java | 128 + .../handle/notice/AttendanceNotice.java | 53 + .../notice/AttendanceNoticeHandler.java | 265 + .../handle/notice/BeforeClockInNotice.java | 87 + .../notice/ConsecUnscheduledNotice.java | 96 + .../notice/ConsecutiveAbsenceNotice.java | 71 + .../handle/notice/FastClockInNotice.java | 111 + .../handle/notice/GroupAdminUpdateNotice.java | 105 + .../handle/notice/GroupLockNotice.java | 107 + .../handle/notice/GroupUnLockNotice.java | 94 + .../LineNotSchedulingAttendanceNotice.java | 63 + .../LineShiftChangAttendanceNotice.java | 82 + .../handle/notice/NoClockInNotice.java | 134 + .../notice/RuleChangeAttendanceNotice.java | 132 + .../handle/notice/SettlementDayNotice.java | 96 + .../handle/notice/SettlementMonthNotice.java | 129 + .../notice/SettlementTeamMonthNotice.java | 118 + .../notice/ShiftChangAttendanceNotice.java | 173 + .../SystemTypeChangeAttendanceNotice.java | 89 + .../notice/UserChangAttendanceNotice.java | 177 + .../AttendanceRuleNotificationHandle.java | 328 + .../MultiTenantTimeSizeBatchProcessor.java | 206 + .../notification/TenantResourceManager.java | 54 + .../handle/rule/LeaveRuleProcessor.java | 932 +++ .../handle/rule/OrdinaryRuleProcessor.java | 737 +++ .../handle/rule/RestRuleProcessor.java | 159 + .../handle/rule/StepOutRuleProcessor.java | 177 + .../rule/WorkOvertimeRuleProcessor.java | 487 ++ .../impl/AppStatisticsServiceImpl.java | 1443 ++++ .../impl/AttenceMachineServiceImpl.java | 484 ++ .../service/impl/AttendanceAIServiceImpl.java | 47 + .../AttendanceApprovalSettingServiceImpl.java | 52 + .../impl/AttendanceApproveServiceImpl.java | 3343 ++++++++++ .../AttendanceBaseSettingServiceImpl.java | 453 ++ .../impl/AttendanceBookConfigServiceImpl.java | 1149 ++++ ...AttendanceBookOperationLogServiceImpl.java | 117 + .../impl/AttendanceBookRecordServiceImpl.java | 2918 ++++++++ .../impl/AttendanceChangeServiceImpl.java | 510 ++ .../impl/AttendanceClockInPicServiceImpl.java | 53 + .../AttendanceClockInResultServiceImpl.java | 132 + .../impl/AttendanceClockInServiceImpl.java | 5253 +++++++++++++++ .../impl/AttendanceCloudAlbumServiceImpl.java | 75 + .../AttendanceConfirmDetailsServiceImpl.java | 20 + .../impl/AttendanceConfirmServiceImpl.java | 550 ++ .../AttendanceConfirmSettingServiceImpl.java | 177 + .../AttendanceCustomizeTableServiceImpl.java | 155 + .../impl/AttendanceDailyRuleServiceImpl.java | 5880 +++++++++++++++++ .../AttendanceDayStatisticsServiceImpl.java | 2846 ++++++++ .../impl/AttendanceDayStatisticsUtilImpl.java | 415 ++ .../AttendanceFestivalRulesServiceImpl.java | 534 ++ .../AttendanceFestivalSettingServiceImpl.java | 410 ++ .../impl/AttendanceFixedClassServiceImpl.java | 20 + .../impl/AttendanceGroupServiceImpl.java | 2480 +++++++ .../AttendanceHolidaySettingServiceImpl.java | 246 + .../impl/AttendanceLeaveRulesServiceImpl.java | 687 ++ .../impl/AttendanceLeaveTypeServiceImpl.java | 93 + ...ndanceLineSchedulingConfigServiceImpl.java | 338 + ...LineSchedulingPayrollHoursServiceImpl.java | 167 + .../AttendanceLocationSettingServiceImpl.java | 410 ++ .../AttendanceMachineManageServiceImpl.java | 485 ++ .../AttendanceMachineSyncServiceImpl.java | 17 + .../impl/AttendanceNoticeServiceImpl.java | 150 + .../AttendancePermissionDictServiceImpl.java | 136 + ...ttendanceQuickTemplateItemServiceImpl.java | 20 + .../AttendanceQuickTemplateServiceImpl.java | 288 + .../impl/AttendanceRepairServiceImpl.java | 46 + .../AttendanceSealSettingServiceImpl.java | 175 + ...AttendanceShiftNameSettingServiceImpl.java | 589 ++ ...tendanceShiftSettingPeriodServiceImpl.java | 21 + .../AttendanceShiftSettingServiceImpl.java | 648 ++ .../impl/AttendanceSuperAdminServiceImpl.java | 917 +++ ...ttendanceUserBalanceRecordServiceImpl.java | 643 ++ .../AttendanceUserBalanceServiceImpl.java | 706 ++ .../impl/AttendanceUserFaceServiceImpl.java | 139 + .../impl/AttendanceUserServiceImpl.java | 1469 ++++ .../AttendanceUserSettingServiceImpl.java | 547 ++ .../service/impl/AutoScheduleService.java | 833 +++ .../impl/ClockInResultServiceImpl.java | 207 + .../impl/CommonSettingServiceImpl.java | 17 + .../impl/DailyRuleChangeServiceImpl.java | 65 + .../impl/EnableBalanceServiceImpl.java | 35 + .../impl/InitializationServiceImpl.java | 122 + .../impl/IsPerfMachineServiceImpl.java | 67 + .../impl/MachineKaiJiaYiStrategyImpl.java | 129 + .../service/impl/MachineKeMiStrategyImpl.java | 92 + .../impl/MachineMaoTongStrategyImpl.java | 166 + .../impl/MachineYuQueStrategyImpl.java | 91 + .../impl/OvertimeRuleDetailServiceImpl.java | 17 + .../service/impl/OvertimeRuleServiceImpl.java | 339 + .../impl/PublicHolidayRulesServiceImpl.java | 310 + .../impl/RV1109MachineServiceImpl.java | 42 + .../service/impl/RuleScopeServiceImpl.java | 42 + .../ScheduleGroupRuleConfigServiceImpl.java | 369 ++ .../impl/SmartPreScheduleServiceImpl.java | 147 + .../impl/StatisticsUtilServiceImpl.java | 653 ++ .../service/impl/UserConfigServiceImpl.java | 45 + .../service/impl/UserFaceServiceImpl.java | 643 ++ .../service/impl/UserFaceTxServiceImpl.java | 260 + .../service/impl/UsualPhoneServiceImpl.java | 178 + .../service/impl/WorkstationServiceImpl.java | 594 ++ .../ByEmployeeSchedulesV2Converter.java | 295 + .../PreScheduleByEmployeeFilter.java | 137 + .../PreScheduleIncompleteMsgBuilder.java | 281 + .../PreScheduleQueryValidator.java | 191 + .../preschedule/PreScheduleRedisSupport.java | 211 + .../preschedule/PreScheduleResultMapper.java | 448 ++ .../preschedule/PreScheduleSaveValidator.java | 122 + .../FtbPermissionFunctionMenuController.java | 125 + .../FtbPermissionGradesController.java | 35 + .../FtbPermissionMigrateController.java | 121 + .../FtbPermissionOrganizeController.java | 332 + .../FtbPermissionPositionController.java | 58 + ...rmissionRoleAuthorizePersonController.java | 193 + ...PermissionRoleAuthorizePostController.java | 84 + .../FtbPermissionRoleController.java | 238 + .../FtbPermissionUsersController.java | 91 + .../FtbPermissionFunctionMenuMapper.java | 22 + .../mapper/FtbPermissionMigrateMapper.java | 25 + ...tbPermissionRoleAuthorizePersonMapper.java | 99 + .../FtbPermissionRoleAuthorizePostMapper.java | 14 + .../mapper/FtbPermissionRoleMapper.java | 46 + .../mapper/FtbPermissionRoleMenuMapper.java | 25 + .../FtbPermissionRoleMenuRelationMapper.java | 14 + ...ermissionRolePersonUserRelationMapper.java | 14 + .../FtbPermissionFunctionMenuService.java | 39 + .../service/FtbPermissionGradesService.java | 23 + .../service/FtbPermissionMigrateService.java | 26 + .../service/FtbPermissionOrganizeService.java | 87 + .../service/FtbPermissionPositionService.java | 27 + ...bPermissionRoleAuthorizePersonService.java | 56 + ...FtbPermissionRoleAuthorizePostService.java | 33 + .../FtbPermissionRoleMenuRelationService.java | 16 + .../service/FtbPermissionRoleMenuService.java | 16 + .../service/FtbPermissionRoleService.java | 149 + .../service/FtbPermissionUsersService.java | 47 + .../FtbPermissionFunctionMenuServiceImpl.java | 294 + .../impl/FtbPermissionGradesServiceImpl.java | 38 + .../impl/FtbPermissionMigrateServiceImpl.java | 801 +++ .../FtbPermissionOrganizeServiceImpl.java | 259 + .../FtbPermissionPositionServiceImpl.java | 159 + ...missionRoleAuthorizePersonServiceImpl.java | 603 ++ ...ermissionRoleAuthorizePostServiceImpl.java | 174 + ...PermissionRoleMenuRelationServiceImpl.java | 23 + .../FtbPermissionRoleMenuServiceImpl.java | 23 + .../impl/FtbPermissionRoleServiceImpl.java | 731 ++ .../impl/FtbPermissionUsersServiceImpl.java | 148 + .../utils/PermissionsApplicableEnums.java | 37 + .../utils/PermissionsApplicableObject.java | 26 + .../authority/utils/PermissionsEnums.java | 39 + .../authority/utils/PermissionsUtils.java | 750 +++ .../config/FoodSafetyOcrConfig.java | 18 + .../consumer/CertificateConsumer.java | 338 + .../consumer/CertificateConsumerSource.java | 17 + .../CertificateInstanceController.java | 182 + .../CertificateManageApiController.java | 59 + .../controller/CertificateOcrController.java | 190 + .../CertificateStoreController.java | 75 + .../CertificateWarningController.java | 932 +++ .../app/AppCertificateManageController.java | 112 + .../app/AppCertificateReminderController.java | 83 + .../app/AppCertificateRiskController.java | 88 + .../jnpf/certificate/helper/NoticeHelper.java | 151 + .../helper/OrganizationHelper.java | 243 + .../mapper/CertificateAppReminderMapper.java | 14 + .../mapper/CertificateAppRiskMapper.java | 40 + .../CertificateBusinessLicenseExtMapper.java | 11 + .../CertificateHygieneLicenseExtMapper.java | 11 + .../mapper/CertificateInstanceItemMapper.java | 11 + .../mapper/CertificateInstanceMapper.java | 115 + .../model/CertificateAppReminderRecord.java | 57 + .../CertificateAppReminderService.java | 24 + .../service/CertificateAppRiskService.java | 47 + .../service/CertificateInstanceService.java | 181 + .../service/CertificateManageApiService.java | 50 + .../service/CertificateManageService.java | 50 + .../service/CertificateStoreService.java | 46 + .../CertificateAppReminderServiceImpl.java | 628 ++ .../impl/CertificateAppRiskServiceImpl.java | 514 ++ .../impl/CertificateInstanceServiceImpl.java | 1541 +++++ .../impl/CertificateManageApiServiceImpl.java | 574 ++ .../impl/CertificateManageServiceImpl.java | 481 ++ .../impl/CertificateStoreServiceImpl.java | 906 +++ .../util/CertificateStatusUtils.java | 17 + .../jnpf/certificate/util/StoreTypeUtils.java | 18 + .../certificate/util/SubjectNameUtils.java | 17 + .../certificate/util/WorkerStatusUtils.java | 14 + .../main/java/jnpf/config/AsyncConfig.java | 56 + .../java/jnpf/config/CustomValueFilter.java | 66 + .../jnpf/config/GlobalExceptionHandler.java | 135 + .../java/jnpf/config/MqttConfiguration.java | 36 + .../jnpf/config/StringToDateConverter.java | 107 + .../java/jnpf/config/TempDevOnlyConfig.java | 26 + .../src/main/java/jnpf/config/WebConfig.java | 88 + .../constant/AttendanceStatusConstant.java | 77 + ...atePromotionPostApplyForAppController.java | 72 + .../FtbCultivateCaseBaseAppController.java | 77 + ...FtbCultivateCaseBaseLikeAppController.java | 55 + .../FtbCultivateCourseAppController.java | 218 + .../controller/app/exam/ExamController.java | 401 ++ .../CultivateIdentifyAppController.java | 258 + ...FtbCultivateLearnTaskForAppController.java | 81 + ...bCultivateMyLearnTaskForAppController.java | 116 + ...FtbCultivateOfflineTrainAppController.java | 94 + .../FtbCultivatePositionForAppController.java | 88 + .../FtbCulProPostForAppController.java | 96 + ...CultivatePromotionNewForAppController.java | 154 + ...tbCultivateStatisticsForAppController.java | 111 + .../app/teaching/AppTeachingController.java | 182 + .../controller/web/MigrateController.java | 135 + .../FtbCultivateAiSupportController.java | 121 + ...CultivatePromotionPostApplyController.java | 105 + .../FtbCultivatePermissionUserController.java | 148 + .../FtbCultivateCaseBaseController.java | 56 + .../FtbCultivateCertificateController.java | 145 + ...tbCultivateCertificateImageController.java | 45 + ...ltivateCertificateStatisticController.java | 113 + ...FtbCultivateCertificateUserController.java | 276 + ...ltivateChapterTestStatisticController.java | 54 + .../FtbCultivateCourseChapterController.java | 93 + .../course/FtbCultivateCourseController.java | 180 + ...bCultivateCourseStatisticesController.java | 147 + .../FtbCultivateCourseTypeController.java | 88 + .../FtbCultivateCoursePackageController.java | 104 + .../web/exam/FtbCultivateExamController.java | 239 + .../FtbCultivateExamOldDataController.java | 73 + .../FtbCultivateExamReadOverController.java | 54 + .../exam/FtbCultivateExamUserController.java | 318 + .../FtbCourseGainedCommentController.java | 245 + .../web/gained/FtbCourseGainedController.java | 331 + .../gained/FtbCourseGainedLikeController.java | 116 + .../FtbCourseGainedReaderController.java | 79 + .../FtbCourseGainedShareController.java | 84 + .../CultivateCultivateIdentifyController.java | 367 + ...bCultivateIdentifyStatisticController.java | 126 + ...FtbCultivateLearnCategoriesController.java | 81 + ...tbCultivateLearnTaskContentController.java | 115 + .../FtbCultivateLearnTaskCountController.java | 75 + .../FtbCultivateLearnTaskInfoController.java | 115 + .../FtbCultivateLearnTaskListController.java | 219 + .../FtbCultivateOfflineTrainController.java | 173 + .../web/org/FtbCultivateOrgController.java | 241 + .../FtbCultivateTestPaperController.java | 189 + ...CultivatePositionAssessmentController.java | 85 + .../FtbCultivatePositionController.java | 475 ++ .../FtbCultivatePositionCopyController.java | 66 + .../FtbCultivatePositionMemberController.java | 84 + ...ultivatePositionStatisticesController.java | 187 + .../promotion/FtbCultivateMapsController.java | 71 + .../FtbCultivatePromotionController.java | 200 + ...FtbCultivatePromotionMemberController.java | 95 + .../FtbCultivatePromotionNewController.java | 146 + .../FtbCultivatePromotionPostController.java | 83 + ...tbCultivateAssessmentPointsController.java | 139 + .../FtbCultivateQuestionBankController.java | 163 + .../FtbCultivateQuestionController.java | 525 ++ .../web/rule/FtbCultivateRuleController.java | 55 + .../FtbCultivateStatisticsController.java | 163 + .../FtbCultivateStoreStatisticController.java | 195 + .../web/teaching/TeachingController.java | 113 + .../teaching/TeachingRecordController.java | 218 + .../web/teaching/TeachingSkillController.java | 126 + .../jnpf/cultivate/event/EventHandler.java | 34 + .../event/JnpfApplicationEventService.java | 20 + .../event/JsonToListTypeHandler.java | 54 + .../event/base/TriggerProcessor.java | 28 + .../course/CourseProcessLearnAbstract.java | 130 + .../CoursePositionTaskTriggerAbstract.java | 160 + .../ExamIdentifyTriggerAbstract.java | 34 + .../ExamIdentifyTriggerProcessor.java | 114 + .../PositionTaskTriggerAbstract.java | 36 + ...npfApplicationEventCertificateService.java | 598 ++ .../JnpfApplicationEventCourseService.java | 228 + ...ApplicationEventPositionCourseService.java | 72 + .../JnpfApplicationEventFileServiceImpl.java | 53 + .../CommonCourseProcessLearnProcessor.java | 31 + ...icationEventExamIdentificationTrigger.java | 40 + .../position/CourseTriggerProcessor.java | 141 + .../PositionProcessLearnProcessor.java | 94 + .../position/PositionTriggerProcessor.java | 153 + .../task/TaskProcessLearnProcessor.java | 94 + .../trigger/task/TaskTriggerProcessor.java | 130 + .../mapper/CultivateCourseMsgMapper.java | 29 + .../mapper/CultivateCourseMsgUserMapper.java | 12 + .../mapper/CultivateCoverCategoryMapper.java | 13 + .../mapper/CultivateCoverInfoMapper.java | 19 + .../mapper/CultivateExamDrawRuleMapper.java | 24 + .../cultivate/mapper/CultivateExamMapper.java | 59 + .../mapper/CultivateExamSettingMapper.java | 14 + .../cultivate/mapper/CultivateFileMapper.java | 13 + ...vateIdentifyApplyDetailsBackupsMapper.java | 19 + .../CultivateIdentifyApplyDetailsMapper.java | 36 + .../mapper/CultivateIdentifyApplyMapper.java | 171 + ...tivateIdentifyApplyTableBackupsMapper.java | 19 + .../mapper/CultivateIdentifyItemsMapper.java | 33 + .../mapper/CultivateIdentifyTableMapper.java | 103 + .../CultivatePositionCourseLogMapper.java | 23 + .../mapper/CultivateUserViewMapper.java | 13 + .../mapper/FtbCourseGainedCommentMapper.java | 54 + .../mapper/FtbCourseGainedLikeMapper.java | 29 + .../mapper/FtbCourseGainedMapper.java | 25 + .../mapper/FtbCourseGainedReaderMapper.java | 14 + .../mapper/FtbCourseGainedShareMapper.java | 16 + .../FtbCultivateAssessmentPointsMapper.java | 8 + .../FtbCultivateCaseBaseLikeMapper.java | 14 + .../mapper/FtbCultivateCaseBaseMapper.java | 45 + .../FtbCultivateCertificateImagesMapper.java | 28 + .../mapper/FtbCultivateCertificateMapper.java | 46 + .../FtbCultivateCertificateResultMapper.java | 14 + .../FtbCultivateCertificateUserMapper.java | 11 + .../mapper/FtbCultivateChapterTestMapper.java | 29 + .../FtbCultivateChapterTestOptionMapper.java | 7 + .../FtbCultivateChapterTestResultMapper.java | 23 + ...FtbCultivateCommonSettingGlobalMapper.java | 15 + .../FtbCultivateCourseChapterMapper.java | 22 + .../FtbCultivateCourseLearningLogMapper.java | 14 + .../mapper/FtbCultivateCourseMapper.java | 153 + .../FtbCultivateCoursePackageMapper.java | 28 + ...FtbCultivateCourseSettingGlobalMapper.java | 8 + .../FtbCultivateCourseSettingMapper.java | 13 + .../FtbCultivateCourseStatisticesMapper.java | 74 + .../FtbCultivateCourseTriggerLogMapper.java | 8 + .../mapper/FtbCultivateCourseTypeMapper.java | 22 + .../FtbCultivateExamFrequncyLogMapper.java | 33 + .../FtbCultivateExamHistoryPaperMapper.java | 14 + .../mapper/FtbCultivateExamMapper.java | 31 + .../mapper/FtbCultivateExamPaperMapper.java | 8 + .../FtbCultivateExamUserDetailMapper.java | 8 + .../mapper/FtbCultivateExamUserMapper.java | 349 + .../mapper/FtbCultivateFileMapper.java | 7 + .../FtbCultivateIdentifyCategoriesMapper.java | 15 + .../FtbCultivateIdentifyItemsPoolMapper.java | 26 + .../mapper/FtbCultivateLabelMapper.java | 19 + .../FtbCultivateLearnCategoriesMapper.java | 19 + ...FtbCultivateLearnTaskAssignmentMapper.java | 61 + ...tbCultivateLearnTaskCertificateMapper.java | 42 + .../FtbCultivateLearnTaskCourseMapper.java | 42 + .../FtbCultivateLearnTaskExamMapper.java | 66 + ...ultivateLearnTaskIdentificationMapper.java | 50 + ...tbCultivateLearnTaskInfoContentMapper.java | 21 + .../FtbCultivateLearnTaskInfoMapper.java | 80 + .../mapper/FtbCultivateLearnTaskMapper.java | 111 + .../FtbCultivateLearnTaskPhaseMapper.java | 23 + .../FtbCultivateLearnTaskPracticeMapper.java | 34 + ...bCultivateLearnTaskReminderRuleMapper.java | 23 + .../mapper/FtbCultivateMessageInfoMapper.java | 12 + .../FtbCultivateMyLearnTaskInfoMapper.java | 23 + .../FtbCultivateOfflineCourseMapper.java | 7 + .../FtbCultivateOfflineTrainMapper.java | 66 + .../mapper/FtbCultivateOfflineUserMapper.java | 8 + .../FtbCultivatePackageCourseMapper.java | 7 + ...FtbCultivatePositionCertificateMapper.java | 18 + ...tePositionCourceChapterLearningMapper.java | 23 + ...CultivatePositionCourceLearningMapper.java | 100 + ...tivatePositionCourseCertificateMapper.java | 52 + .../FtbCultivatePositionCourseExamMapper.java | 70 + ...CultivatePositionCourseIdentityMapper.java | 54 + .../FtbCultivatePositionCourseMapper.java | 150 + ...CultivatePositionCoursePracticeMapper.java | 59 + ...tbCultivatePositionExamIdentifyMapper.java | 8 + .../FtbCultivatePositionExamMapper.java | 8 + ...CultivatePositionIdentifyResultMapper.java | 8 + .../mapper/FtbCultivatePositionLogMapper.java | 9 + .../mapper/FtbCultivatePositionMapper.java | 70 + .../FtbCultivatePositionSettingMapper.java | 10 + ...FtbCultivatePositionStatisticesMapper.java | 224 + .../FtbCultivatePositionUserMapper.java | 8 + .../FtbCultivatePromotionLogMapper.java | 15 + .../mapper/FtbCultivatePromotionMapper.java | 76 + .../FtbCultivatePromotionMemberMapper.java | 62 + .../FtbCultivatePromotionMemberNewMapper.java | 54 + .../FtbCultivatePromotionNewMapper.java | 360 + ...FtbCultivatePromotionNewMessageMapper.java | 13 + .../FtbCultivatePromotionPostApplyMapper.java | 23 + .../FtbCultivatePromotionPostMapper.java | 40 + .../FtbCultivatePromotionPostNewMapper.java | 28 + .../FtbCultivatePromotionSettingMapper.java | 13 + .../FtbCultivatePromotionUserMapper.java | 43 + .../FtbCultivateQuestionAnalysisMapper.java | 8 + .../FtbCultivateQuestionBankCourseMapper.java | 8 + .../FtbCultivateQuestionBankMapper.java | 8 + .../mapper/FtbCultivateQuestionMapper.java | 80 + .../FtbCultivateQuestionOptionMapper.java | 8 + .../FtbCultivateQuestionPointsMapper.java | 7 + .../mapper/FtbCultivateRuleMapper.java | 14 + .../mapper/FtbCultivateStatisticsMapper.java | 71 + .../mapper/FtbCultivateTaskLogMapper.java | 13 + .../mapper/FtbCultivateTestPaperMapper.java | 7 + .../FtbCultivateTestPaperQuestionMapper.java | 8 + .../FtbCultivateTestPaperRuleMapper.java | 8 + .../mapper/TeachingApproveMapper.java | 18 + .../mapper/TeachingRecordMapper.java | 143 + .../cultivate/mapper/TeachingSkillMapper.java | 41 + .../mapper/TeachingStudentMapper.java | 34 + .../mock/CultivateMockController.java | 181 + .../cultivate/mock/CultivateMockUtils.java | 56 + .../service/CultivateCourseMsgService.java | 51 + .../CultivateCourseMsgUserService.java | 23 + .../CultivateCoverCategoryService.java | 49 + .../service/CultivateCoverInfoService.java | 34 + .../service/CultivateExamSettingService.java | 23 + .../service/CultivateFileService.java | 31 + .../service/CultivateIdentifyApiService.java | 49 + ...ateIdentifyApplyDetailsBackupsService.java | 16 + .../CultivateIdentifyApplyDetailsService.java | 16 + .../CultivateIdentifyApplyService.java | 287 + ...ivateIdentifyApplyTableBackupsService.java | 16 + .../CultivateIdentifyItemsService.java | 29 + .../CultivateIdentifyTableService.java | 86 + .../service/ExamFrequencyLogService.java | 15 + .../FtbCourseGainedCommentService.java | 51 + .../service/FtbCourseGainedLikeService.java | 62 + .../service/FtbCourseGainedReaderService.java | 48 + .../service/FtbCourseGainedService.java | 78 + .../service/FtbCourseGainedShareService.java | 55 + .../FtbCultivateAssessmentPointsService.java | 10 + .../service/FtbCultivateCaseBaseService.java | 71 + .../FtbCultivateCertificateImagesService.java | 34 + .../FtbCultivateCertificateService.java | 90 + .../FtbCultivateCertificateUserService.java | 64 + .../FtbCultivateChapterTestOptionService.java | 9 + .../FtbCultivateChapterTestResultService.java | 9 + .../FtbCultivateChapterTestService.java | 21 + .../service/FtbCultivateCourseAppService.java | 56 + .../FtbCultivateCourseChapterService.java | 81 + .../FtbCultivateCoursePackageService.java | 54 + .../service/FtbCultivateCourseService.java | 125 + .../FtbCultivateCourseSettingService.java | 22 + .../FtbCultivateCourseStatisticesService.java | 25 + .../FtbCultivateCourseTriggerLogService.java | 14 + .../FtbCultivateCourseTypeService.java | 52 + .../FtbCultivateExamHistoryPaperService.java | 47 + .../service/FtbCultivateExamService.java | 291 + .../FtbCultivateExamUserDetailService.java | 10 + .../service/FtbCultivateExamUserService.java | 550 ++ .../FtbCultivateLearnCategoriesService.java | 93 + ...tbCultivateLearnTaskAppContentService.java | 5 + ...tbCultivateLearnTaskAssignmentService.java | 30 + ...bCultivateLearnTaskCertificateService.java | 40 + .../FtbCultivateLearnTaskCourseService.java | 43 + .../FtbCultivateLearnTaskExamService.java | 41 + ...ltivateLearnTaskIdentificationService.java | 47 + ...bCultivateLearnTaskInfoContentService.java | 75 + ...FtbCultivateLearnTaskInfoCountService.java | 29 + .../FtbCultivateLearnTaskInfoService.java | 61 + .../FtbCultivateLearnTaskListService.java | 51 + ...CultivateLearnTaskReminderRuleService.java | 16 + .../service/FtbCultivateLearnTaskService.java | 55 + .../FtbCultivateMyLearnTaskInfoService.java | 76 + .../FtbCultivateOfflineCourseService.java | 10 + .../FtbCultivateOfflineTrainService.java | 102 + .../FtbCultivateOfflineUserService.java | 10 + .../FtbCultivatePackageCourseService.java | 9 + ...FtbCultivatePositionAssessmentService.java | 39 + ...tbCultivatePositionCertificateService.java | 9 + .../FtbCultivatePositionCopyService.java | 37 + ...ivatePositionCourseCertificateService.java | 47 + ...ultivatePositionCoursePracticeService.java | 55 + .../FtbCultivatePositionCourseService.java | 97 + ...FtbCultivatePositionExamCourseService.java | 29 + ...bCultivatePositionExamIdentifyService.java | 9 + .../FtbCultivatePositionExamService.java | 10 + .../FtbCultivatePositionForAppService.java | 55 + .../FtbCultivatePositionMemberService.java | 52 + .../service/FtbCultivatePositionService.java | 196 + ...tbCultivatePositionStatisticesService.java | 52 + .../FtbCultivatePositionUserService.java | 10 + .../FtbCultivatePromotionMemberService.java | 68 + .../FtbCultivatePromotionNewService.java | 233 + ...FtbCultivatePromotionPostApplyService.java | 58 + .../FtbCultivatePromotionPostService.java | 84 + .../service/FtbCultivatePromotionService.java | 107 + ...FtbCultivateQuestionBankCourseService.java | 10 + .../FtbCultivateQuestionBankService.java | 79 + .../FtbCultivateQuestionOptionService.java | 16 + .../FtbCultivateQuestionPointsService.java | 7 + .../service/FtbCultivateQuestionService.java | 130 + .../service/FtbCultivateRuleService.java | 16 + .../FtbCultivateStatisticsService.java | 54 + .../FtbCultivateStoreStatisticService.java | 64 + .../FtbCultivateTestPaperQuestionService.java | 10 + .../FtbCultivateTestPaperRuleService.java | 10 + .../service/FtbCultivateTestPaperService.java | 146 + .../PositionCultivateIdentifyService.java | 28 + .../TeachingRecordPracticeService.java | 141 + .../service/TeachingRecordService.java | 219 + .../service/TeachingSkillService.java | 108 + .../service/TeachingStudentService.java | 7 + .../cultivate/service/V2TeachingService.java | 38 + .../impl/CultivateCourseMsgServiceImpl.java | 122 + .../CultivateCourseMsgUserServiceImpl.java | 38 + .../CultivateCoverCategoryServiceImpl.java | 128 + .../impl/CultivateCoverInfoServiceImpl.java | 82 + .../impl/CultivateExamSettingServiceImpl.java | 37 + .../impl/CultivateFileServiceImpl.java | 51 + .../impl/CultivateIdentifyApiServiceImpl.java | 100 + ...dentifyApplyDetailsBackupsServiceImpl.java | 18 + ...tivateIdentifyApplyDetailsServiceImpl.java | 20 + .../CultivateIdentifyApplyServiceImpl.java | 2171 ++++++ ...eIdentifyApplyTableBackupsServiceImpl.java | 18 + .../CultivateIdentifyItemsServiceImpl.java | 34 + .../CultivateIdentifyTableServiceImpl.java | 546 ++ .../impl/ExamFrequencyLogServiceImpl.java | 23 + .../FtbCourseGainedCommentServiceImpl.java | 102 + .../impl/FtbCourseGainedLikeServiceImpl.java | 96 + .../FtbCourseGainedReaderServiceImpl.java | 78 + .../impl/FtbCourseGainedServiceImpl.java | 288 + .../impl/FtbCourseGainedShareServiceImpl.java | 110 + ...bCultivateAssessmentPointsServiceImpl.java | 12 + .../impl/FtbCultivateCaseBaseServiceImpl.java | 171 + ...CultivateCertificateImagesServiceImpl.java | 59 + .../FtbCultivateCertificateServiceImpl.java | 601 ++ ...tbCultivateCertificateUserServiceImpl.java | 200 + ...CultivateChapterTestOptionServiceImpl.java | 13 + ...CultivateChapterTestResultServiceImpl.java | 13 + .../FtbCultivateChapterTestServiceImpl.java | 103 + .../FtbCultivateCourseAppServiceImpl.java | 397 ++ .../FtbCultivateCourseChapterServiceImpl.java | 418 ++ .../FtbCultivateCoursePackageServiceImpl.java | 136 + .../impl/FtbCultivateCourseServiceImpl.java | 474 ++ .../FtbCultivateCourseSettingServiceImpl.java | 68 + ...CultivateCourseStatisticesServiceImpl.java | 339 + ...bCultivateCourseTriggerLogServiceImpl.java | 15 + .../FtbCultivateCourseTypeServiceImpl.java | 82 + ...bCultivateExamHistoryPaperServiceImpl.java | 114 + .../impl/FtbCultivateExamServiceImpl.java | 2221 +++++++ ...FtbCultivateExamUserDetailServiceImpl.java | 12 + .../impl/FtbCultivateExamUserServiceImpl.java | 3893 +++++++++++ .../service/impl/FtbCultivateFileService.java | 9 + .../impl/FtbCultivateFileServiceImpl.java | 16 + ...tbCultivateLearnCategoriesServiceImpl.java | 418 ++ ...ltivateLearnTaskAppContentServiceImpl.java | 9 + ...ltivateLearnTaskAssignmentServiceImpl.java | 76 + ...tivateLearnTaskCertificateServiceImpl.java | 44 + ...tbCultivateLearnTaskCourseServiceImpl.java | 85 + .../FtbCultivateLearnTaskExamServiceImpl.java | 56 + ...ateLearnTaskIdentificationServiceImpl.java | 55 + ...tivateLearnTaskInfoContentServiceImpl.java | 449 ++ ...ultivateLearnTaskInfoCountServiceImpl.java | 56 + .../FtbCultivateLearnTaskInfoServiceImpl.java | 433 ++ .../FtbCultivateLearnTaskListServiceImpl.java | 193 + ...ivateLearnTaskReminderRuleServiceImpl.java | 23 + .../FtbCultivateLearnTaskServiceImpl.java | 75 + ...tbCultivateMyLearnTaskInfoServiceImpl.java | 527 ++ .../FtbCultivateOfflineCourseServiceImpl.java | 12 + .../FtbCultivateOfflineTrainServiceImpl.java | 517 ++ .../FtbCultivateOfflineUserServiceImpl.java | 12 + .../FtbCultivatePackageCourseServiceImpl.java | 12 + ...ultivatePositionAssessmentServiceImpl.java | 445 ++ ...ltivatePositionCertificateServiceImpl.java | 13 + .../FtbCultivatePositionCopyServiceImpl.java | 244 + ...ePositionCourceChapterLearningService.java | 9 + ...itionCourceChapterLearningServiceImpl.java | 12 + ...ultivatePositionCourceLearningService.java | 9 + ...vatePositionCourceLearningServiceImpl.java | 11 + ...ePositionCourseCertificateServiceImpl.java | 83 + ...FtbCultivatePositionCourseExamService.java | 60 + ...ultivatePositionCourseExamServiceImpl.java | 106 + ...ultivatePositionCourseIdentityService.java | 48 + ...vatePositionCourseIdentityServiceImpl.java | 87 + ...vatePositionCoursePracticeServiceImpl.java | 94 + ...FtbCultivatePositionCourseServiceImpl.java | 188 + ...ultivatePositionExamCourseServiceImpl.java | 90 + ...tivatePositionExamIdentifyServiceImpl.java | 13 + .../FtbCultivatePositionExamServiceImpl.java | 12 + ...FtbCultivatePositionForAppServiceImpl.java | 311 + ...FtbCultivatePositionMemberServiceImpl.java | 293 + .../impl/FtbCultivatePositionServiceImpl.java | 954 +++ ...ltivatePositionStatisticesServiceImpl.java | 1074 +++ .../FtbCultivatePositionUserServiceImpl.java | 12 + ...tbCultivatePromotionMemberServiceImpl.java | 333 + .../FtbCultivatePromotionNewServiceImpl.java | 1420 ++++ ...ultivatePromotionPostApplyServiceImpl.java | 313 + .../FtbCultivatePromotionPostServiceImpl.java | 168 + .../FtbCultivatePromotionServiceImpl.java | 603 ++ ...ultivateQuestionBankCourseServiceImpl.java | 12 + .../FtbCultivateQuestionBankServiceImpl.java | 465 ++ ...FtbCultivateQuestionOptionServiceImpl.java | 30 + ...FtbCultivateQuestionPointsServiceImpl.java | 12 + .../impl/FtbCultivateQuestionServiceImpl.java | 823 +++ .../impl/FtbCultivateRuleServiceImpl.java | 23 + .../FtbCultivateStatisticsServiceImpl.java | 459 ++ ...tbCultivateStoreStatisticsServiceImpl.java | 1054 +++ ...CultivateTestPaperQuestionServiceImpl.java | 13 + .../FtbCultivateTestPaperRuleServiceImpl.java | 12 + .../FtbCultivateTestPaperServiceImpl.java | 1254 ++++ .../PositionCultivateIdentifyServiceImpl.java | 94 + .../TeachingRecordPracticeServiceImpl.java | 565 ++ .../impl/TeachingRecordServiceImpl.java | 1388 ++++ .../impl/TeachingSkillServiceImpl.java | 354 + .../impl/TeachingStudentServiceImpl.java | 11 + .../service/impl/V2TeachingServiceImpl.java | 146 + .../utils/AsyncExamQuestionUtils.java | 130 + .../utils/CultivateDateTimeUtils.java | 87 + .../utils/CultivateIdentifyIMUtils.java | 95 + .../jnpf/cultivate/utils/CultivateImUtil.java | 139 + .../utils/CultivateLearnTaskIMUtils.java | 99 + .../cultivate/utils/CultivateLearnUtils.java | 1155 ++++ .../cultivate/utils/CultivatePerUtils.java | 253 + .../utils/CultivateTaskLeanAsyncDealUtil.java | 44 + .../cultivate/utils/HistoryPaperUtils.java | 41 + .../utils/QuestionExcelExportUtil.java | 553 ++ .../jnpf/cultivate/utils/UserApiV2Util.java | 1960 ++++++ .../jnpf/cultivate/utils/UserExamUtil.java | 541 ++ ...LocalDisableRocketMqListenerProcessor.java | 119 + .../V2CultivateCourseAppController.java | 250 + .../app/exam/ExamSubController.java | 62 + .../V2CultivateUserExamAppController.java | 150 + .../V2CourseGainedCommentController.java | 55 + ...CultivateIdentifyManagerAppController.java | 98 + .../V2CultivateMyIdentifyAppController.java | 70 + .../V2CultivateOfflineTrainAppController.java | 36 + .../V2CultivatePositionAppController.java | 111 + ...extUserCultivatePositionAppController.java | 178 + .../V2CultivatePromotionAppController.java | 121 + .../task/V2CultivateTaskAppController.java | 193 + .../app/teaching/V2TeachingAppController.java | 61 + .../V2CultivateAiHelperController.java | 148 + .../certificate/V2CertificateController.java | 87 + .../web/common/V2CommonController.java | 145 + ...ultivateCommonSettingGlobalController.java | 46 + ...ultivateCourseSettingGlobalController.java | 46 + ...2CultivateCourseStatisticesController.java | 91 + .../V2CultivateCourseWebController.java | 100 + .../web/exam/V2CultivateExamController.java | 245 + .../V2CultivateExamStatisticsController.java | 90 + .../exam/V2CultivateExamUserController.java | 81 + .../V2CultivateIdentifyApplyController.java | 145 + ...CultivateIdentifyCategoriesController.java | 81 + .../V2CultivateIdentifyController.java | 236 + ...2CultivateIdentifyItemsPoolController.java | 380 ++ .../label/FtbCultivateLabelController.java | 92 + .../web/old/V2CultivateOldDealController.java | 676 ++ .../V2CultivatePositionWebController.java | 124 + ...CultivatePromotionStatisticController.java | 81 + .../V2CultivatePromotionWebController.java | 182 + .../V2CultivateQuestionController.java | 250 + .../task/V2CultivateTaskWebController.java | 289 + .../web/teaching/V2TeachingController.java | 30 + .../teaching/V2TeachingRecordController.java | 37 + .../teaching/V2TeachingSkillController.java | 93 + .../web/timing/V2TimingController.java | 121 + .../v2/mq/CultivateConsumerMQListener.java | 154 + .../v2/mq/PermissionConsumerMEListener.java | 366 + .../service/CultivateExamDrawRuleService.java | 23 + .../CultivatePositionCourseLogService.java | 14 + .../FtbCultivateCourseLearningLogService.java | 14 + ...tbCultivateCourseSettingGlobalService.java | 26 + ...FtbCultivateIdentifyCategoriesService.java | 57 + .../FtbCultivateIdentifyItemsPoolService.java | 70 + .../v2/service/FtbCultivateLabelService.java | 76 + .../FtbCultivateLearnTaskPhaseService.java | 9 + .../FtbCultivateLearnTaskPracticeService.java | 25 + .../FtbCultivatePositionLogService.java | 78 + .../FtbCultivatePositionSettingService.java | 26 + .../FtbCultivatePromotionLogService.java | 36 + ...FtbCultivatePromotionMemberNewService.java | 30 + ...tbCultivatePromotionNewMessageService.java | 10 + .../FtbCultivatePromotionPostNewService.java | 11 + .../FtbCultivatePromotionSettingService.java | 10 + .../FtbCultivatePromotionUserService.java | 60 + .../service/FtbCultivateTaskLogService.java | 135 + .../service/V2CourseGainedCommentService.java | 35 + .../service/V2CultivateBatchQueryService.java | 199 + .../V2CultivateCertificateService.java | 8 + ...V2CultivateCommonSettingGlobalService.java | 29 + .../service/V2CultivateCourseAppService.java | 69 + .../v2/service/V2CultivateCourseService.java | 82 + .../v2/service/V2CultivateExamService.java | 196 + .../V2CultivateExamStatisticsService.java | 22 + .../V2CultivateIdentifyApplyService.java | 90 + .../V2CultivateIdentifyTableService.java | 60 + .../V2CultivateOfflineTrainService.java | 14 + .../service/V2CultivatePositionService.java | 148 + .../service/V2CultivatePostStudyService.java | 79 + .../service/V2CultivatePromotionService.java | 166 + .../service/V2CultivateQuestionService.java | 25 + .../service/V2CultivateTaskCountService.java | 22 + .../v2/service/V2CultivateTaskService.java | 166 + .../service/V2CultivateTaskStudyService.java | 55 + .../v2/service/V2TeachingSkillService.java | 45 + .../CultivateExamDrawRuleServiceImpl.java | 26 + ...CultivatePositionCourseLogServiceImpl.java | 22 + ...ltivateCommonSettingGlobalServiceImpl.java | 59 + ...CultivateCourceLearningLogServiceImpl.java | 18 + ...ltivateCourseSettingGlobalServiceImpl.java | 29 + ...ultivateIdentifyCategoriesServiceImpl.java | 283 + ...CultivateIdentifyItemsPoolServiceImpl.java | 330 + .../impl/FtbCultivateLabelServiceImpl.java | 239 + ...FtbCultivateLearnTaskPhaseServiceImpl.java | 12 + ...CultivateLearnTaskPracticeServiceImpl.java | 30 + .../FtbCultivatePositionLogServiceImpl.java | 302 + ...tbCultivatePositionSettingServiceImpl.java | 52 + .../FtbCultivatePromotionLogServiceImpl.java | 73 + ...ultivatePromotionMemberNewServiceImpl.java | 41 + ...ltivatePromotionNewMessageServiceImpl.java | 15 + ...bCultivatePromotionPostNewServiceImpl.java | 19 + ...bCultivatePromotionSettingServiceImpl.java | 16 + .../FtbCultivatePromotionUserServiceImpl.java | 96 + .../impl/FtbCultivateTaskLogServiceImpl.java | 366 + .../V2CourseGainedCommentServiceImpl.java | 301 + .../V2CultivateBatchQueryServiceImpl.java | 1026 +++ .../V2CultivateCertificateServiceImpl.java | 138 + .../impl/V2CultivateCourseAppServiceImpl.java | 732 ++ .../impl/V2CultivateCourseServiceImpl.java | 705 ++ .../impl/V2CultivateExamServiceImpl.java | 1567 +++++ .../V2CultivateExamStatisticsServiceImpl.java | 329 + .../V2CultivateIdentifyApplyServiceImpl.java | 1115 ++++ .../V2CultivateIdentifyTableServiceImpl.java | 514 ++ .../V2CultivateOfflineTrainServiceImpl.java | 64 + .../impl/V2CultivatePositionServiceImpl.java | 2245 +++++++ .../impl/V2CultivatePostStudyServiceImpl.java | 1833 +++++ .../impl/V2CultivatePromotionServiceImpl.java | 1548 +++++ .../impl/V2CultivateQuestionServiceImpl.java | 252 + .../impl/V2CultivateTaskCountServiceImpl.java | 634 ++ .../impl/V2CultivateTaskServiceImpl.java | 2190 ++++++ .../impl/V2CultivateTaskStudyServiceImpl.java | 1087 +++ .../impl/V2TeachingSkillServiceImpl.java | 365 + .../v2/util/CultivateBatchQueryUtil.java | 82 + .../v2/util/CultivateCourseStudyUtil.java | 261 + .../v2/util/CultivateIdentityUtil.java | 578 ++ .../v2/util/CultivateMqSendUtil.java | 175 + .../cultivate/v2/util/ExamChangeDetector.java | 227 + .../cultivate/v2/util/ExamChangeResult.java | 42 + .../v2/util/V2QuestionExcelExportUtil.java | 550 ++ .../controller/CultureClockInController.java | 36 +- .../CulturePicSettingController.java | 93 + .../controller/CultureStatController.java | 60 + .../CultureTextSettingController.java | 149 + .../culture/mapper/CultureClockInMapper.java | 52 + .../mapper/CultureClockInStatMapper.java | 24 + .../mapper/CulturePicSettingMapper.java | 28 + .../culture/mapper/CulturePicTempMapper.java | 13 + .../mapper/CultureTextSettingMapper.java | 51 + .../culture/service/CultureAppService.java | 10 + .../service/CultureClockInService.java | 50 + .../service/CulturePicSettingService.java | 51 + .../culture/service/CultureStatService.java | 29 + .../service/CultureTextSettingService.java | 61 + .../service/impl/CultureAppServiceImpl.java | 14 + .../impl/CultureClockInServiceImpl.java | 334 + .../impl/CulturePicSettingServiceImpl.java | 110 + .../service/impl/CultureStatServiceImpl.java | 239 + .../impl/CultureTextSettingServiceImpl.java | 331 + .../java/jnpf/culture/util/DateRangeUtil.java | 55 + .../main/java/jnpf/culture/util/FontUtil.java | 50 + .../jnpf/culture/util/ImageComboUtil.java | 452 ++ .../controller/DataReportController.java | 32 + .../controller/DocLibraryController.java | 315 + .../controller/SpaceController.java | 284 + .../crontask/DocLibraryCronTask.java | 34 + .../doclibrary/mapper/CommonConfigMapper.java | 11 + .../mapper/InformationAuthorityMapper.java | 119 + .../mapper/InformationLogMapper.java | 16 + .../doclibrary/mapper/InformationMapper.java | 103 + .../mapper/InformationRubbishMapper.java | 14 + .../InfornationRecentlyViewedMapper.java | 5 + .../jnpf/doclibrary/mapper/SpaceMapper.java | 306 + .../doclibrary/service/DataReportService.java | 18 + .../doclibrary/service/DocLibraryService.java | 146 + .../jnpf/doclibrary/service/SpaceService.java | 146 + .../service/impl/DataReportServiceImpl.java | 34 + .../service/impl/DocLibraryServiceImpl.java | 524 ++ .../service/impl/SpaceServiceImpl.java | 775 +++ .../java/jnpf/exception/ApproveException.java | 16 + .../java/jnpf/exception/HandleException.java | 17 + .../java/jnpf/exception/QueryException.java | 17 + .../java/jnpf/exception/ResultException.java | 94 + .../consumer/FranchiseeConsumerSource.java | 17 + .../consumer/FranchiseeStoreNumConsumer.java | 239 + .../controller/FranchiseeApiController.java | 86 + .../app/FranchiseeAppController.java | 151 + .../controller/web/FranchiseeController.java | 149 + ...FranchiseeCustomFieldConfigController.java | 114 + .../mapper/FranchiseeCustomFieldMapper.java | 21 + .../mapper/FranchiseeExperienceMapper.java | 21 + .../mapper/FranchiseeJoinRegionMapper.java | 21 + .../franchisee/mapper/FranchiseeMapper.java | 29 + .../mapper/FranchiseeStoreMapper.java | 11 + .../FranchiseeCustomFieldConfigService.java | 71 + .../franchisee/service/FranchiseeService.java | 149 + ...ranchiseeCustomFieldConfigServiceImpl.java | 409 ++ .../service/impl/FranchiseeServiceImpl.java | 1131 ++++ .../jnpf/handler/CustomCellStyleHandler.java | 40 + .../jnpf/handler/CustomCellWriteHandler.java | 60 + .../jnpf/handler/CustomRowHeightHandler.java | 25 + .../controller/HelpAiLogController.java | 62 + .../mapper/FtbHelpAiChatLogMapper.java | 15 + .../service/FtbHelpAiChatLogService.java | 27 + .../impl/FtbHelpAiChatLogServiceImpl.java | 59 + .../memberLog/controller/LogController.java | 150 + .../controller/WorkLogController.java | 223 + .../memberLog/mapper/LogCommentMapper.java | 7 + .../mapper/LogMemberAssociationMapper.java | 67 + .../mapper/LogMemberFieldMapper.java | 34 + .../memberLog/mapper/LogMemberLogMapper.java | 44 + .../mapper/LogTemplateFieldMapper.java | 16 + .../memberLog/mapper/LogTemplateMapper.java | 14 + .../jnpf/memberLog/mapper/WorkLogMapper.java | 190 + .../jnpf/memberLog/service/LogService.java | 89 + .../memberLog/service/WorkLogService.java | 112 + .../service/impl/LogServiceImpl.java | 598 ++ .../service/impl/WorkLogServiceImpl.java | 722 ++ .../AppMyNoticeAnnouncementsController.java | 143 + .../app/AppNoticeAnnouncementsController.java | 240 + .../app/AppNoticeCategoriesController.java | 93 + .../app/AppNoticeManagerController.java | 64 + .../app/AppNoticeUserGroupsController.java | 138 + .../notice/controller/app/ImController.java | 45 + .../FtbMyNoticeAnnouncementsController.java | 117 + .../web/FtbNoticeAnnouncementsController.java | 275 + .../FtbNoticeAnnouncementsLogController.java | 44 + .../web/FtbNoticeCategoriesController.java | 95 + .../web/FtbNoticeManagerController.java | 109 + .../web/FtbNoticeUserGroupsController.java | 104 + .../FtbNoticeAnnouncementsLogMapper.java | 29 + .../mapper/FtbNoticeAnnouncementsMapper.java | 85 + .../FtbNoticeAnnouncementsReceiveMapper.java | 46 + .../mapper/FtbNoticeCategoriesMapper.java | 18 + .../notice/mapper/FtbNoticeFilesMapper.java | 18 + .../notice/mapper/FtbNoticeManagerMapper.java | 29 + .../FtbNoticeUserGroupMembersMapper.java | 43 + .../mapper/FtbNoticeUserGroupsMapper.java | 27 + .../FtbNoticeAnnouncementsLogService.java | 30 + .../FtbNoticeAnnouncementsReceiveService.java | 184 + .../FtbNoticeAnnouncementsService.java | 156 + .../service/FtbNoticeCategoriesService.java | 95 + .../notice/service/FtbNoticeFilesService.java | 38 + .../service/FtbNoticeManagerService.java | 47 + .../FtbNoticeUserGroupMembersService.java | 66 + .../service/FtbNoticeUserGroupsService.java | 82 + .../FtbNoticeAnnouncementsLogServiceImpl.java | 56 + ...NoticeAnnouncementsReceiveServiceImpl.java | 486 ++ .../FtbNoticeAnnouncementsServiceImpl.java | 1354 ++++ .../impl/FtbNoticeCategoriesServiceImpl.java | 467 ++ .../impl/FtbNoticeFilesServiceImpl.java | 138 + .../impl/FtbNoticeManagerServiceImpl.java | 154 + .../FtbNoticeUserGroupMembersServiceImpl.java | 208 + .../impl/FtbNoticeUserGroupsServiceImpl.java | 338 + .../notice/utils/NoticeAsyncDealUtil.java | 229 + .../java/jnpf/notice/utils/NoticeIMUtils.java | 104 + .../java/jnpf/notice/utils/NoticeUtils.java | 448 ++ .../parameter/hepler/MysqlSequenceHelper.java | 55 + .../jnpf/parameter/service/ParamService.java | 48 + .../service/impl/ParamServiceImpl.java | 106 + .../config/EnvironmentParamConfig.java | 16 + .../config/TengxunLicenseConfig.java | 17 + ...tbPersonnelsPostApplyForAppController.java | 218 + ...ersonnelsAuditRunTaskForAppController.java | 49 + ...lsAuditRunTaskHistoryForAppController.java | 40 + .../FtbPersonnelsEmEntryAppController.java | 109 + ...nelsStaffEmploymentApplyAppController.java | 271 + ...taffRegistrationFormDataAppController.java | 216 + .../FtbPersonnelsGoodsAppController.java | 97 + ...nelsRegularManagementForAppController.java | 122 + ...sonnelsStaffArchivesHistoryController.java | 65 + ...FtbPersonnelsStaffRosterAppController.java | 316 + .../FtbPersonnelsSecondmentAppController.java | 94 + ...elsStaffTransferPositionAppController.java | 174 + ...PersonnelsTransferManageAppController.java | 68 + ...elsTurnoverManagementForAppController.java | 127 + ...bPersonnelsUchisuikePondAppController.java | 41 + .../mcp/FtbPersonStaffMCPController.java | 98 + .../FtbPersonnelChangesController.java | 102 + .../FtbPersonnelsBrainDrainController.java | 129 + ...bPersonnelsOverviewAnalysisController.java | 533 ++ ...bPersonnelsTurnoverAnalysisController.java | 262 + .../FtbPersonnelsPostApplyController.java | 238 + ...PersonnelsAuditMasterConfigController.java | 59 + .../FtbPersonnelsAuditRunTaskController.java | 152 + ...rsonnelsAuditRunTaskHistoryController.java | 38 + ...FtbPersonnelsPermissionUserController.java | 180 + .../FtbPersonnelsBlacklistController.java | 107 + ...bPersonnelsBlacklistHistoryController.java | 60 + .../FtbPersonnelsBlacklistTypeController.java | 86 + .../CompatibilityIssueController.java | 100 + ...tbPersonnelsEmployeeTypeWebController.java | 78 + .../FtbPersonnelsEmEntryController.java | 374 ++ ...sonnelsStaffEmploymentApplyController.java | 605 ++ ...lsStaffRegistrationFormDataController.java | 141 + .../FtbPersonnelsPromiseConfigController.java | 129 + ...onnelsRegistrationFormFieldController.java | 185 + ...sonnelsRegistrationFormTypeController.java | 130 + .../goods/FtbPersonnelsGoodsController.java | 95 + .../FtbPersonnelsGoodsReceiveController.java | 96 + ...FtbPersonnelsStaffGrowthLogController.java | 37 + .../web/org/FtbPersonnelOrgController.java | 580 ++ ...rsonnelsRecruitmentChannelsController.java | 59 + ...PersonnelsRegularManagementController.java | 170 + ...nationConfigurationCategoryController.java | 85 + ...FtbResignationConfigurationController.java | 77 + ...ersonnelsRewardsPunishmentsController.java | 390 ++ ...RewardsPunishmentsApproveOAController.java | 73 + .../FtbPersonnelsContactInfoController.java | 106 + .../FtbPersonnelsMetaDataController.java | 231 + ...sonnelsStaffArchivesHistoryController.java | 98 + .../FtbPersonnelsStaffHomePageController.java | 512 ++ .../FtbPersonnelsStaffRosterController.java | 688 ++ ...PersonnelsStaffRosterImportController.java | 130 + .../roster/FtbPersonnelsTrialController.java | 51 + .../FtbThousandFacePersonController.java | 36 + .../FtbPersonnelsRuleConfigController.java | 90 + .../FtbPersonnlesInfoConfigController.java | 74 + .../salary/FtbPersonnelsSalaryController.java | 266 + ...sonnelsStaffSalaryChangeLogController.java | 39 + ...PersonnelsStaffRosterSchemeController.java | 72 + .../FtbPersonnelsSecondmentController.java | 210 + .../FtbPersonnelsShortchainController.java | 110 + ...onnelsStaffTransferPositionController.java | 215 + ...FtbPersonnelsTransferManageController.java | 159 + ...ersonnelsTurnoverManagementController.java | 326 + .../FtbPersonnelsUchisuikePondController.java | 144 + ...sonnelInformationSupplementController.java | 101 + .../BaseEasyExcelCommonListener.java | 21 + .../listeners/BaseEasyExcelReadListener.java | 45 + .../DynamicHeadAnalysisEventListener.java | 87 + .../DynamicHeadAnalysisNewEventListener.java | 88 + .../impl/FtbPersonnelsRewardsServiceImpl.java | 164 + .../impl/FtbPersonnelsServiceImpl.java | 179 + .../mapper/FtbPersonStaffMCPMapper.java | 34 + .../mapper/FtbPersonnelChangesMapper.java | 42 + ...bPersonnelsAuditCarbonRecipientMapper.java | 7 + .../FtbPersonnelsAuditMasterConfigMapper.java | 7 + ...tbPersonnelsAuditRunTaskHistoryMapper.java | 13 + .../FtbPersonnelsAuditRunTaskMapper.java | 19 + .../FtbPersonnelsAuditSubConfigMapper.java | 36 + .../FtbPersonnelsAuditTaskInfoMapper.java | 29 + .../mapper/FtbPersonnelsAuthoritysMapper.java | 7 + .../FtbPersonnelsBlacklistHistoryMapper.java | 29 + .../mapper/FtbPersonnelsBlacklistMapper.java | 28 + .../FtbPersonnelsBlacklistTypeMapper.java | 16 + .../mapper/FtbPersonnelsGoodsMapper.java | 21 + .../FtbPersonnelsGoodsReceiveMapper.java | 21 + ...FtbPersonnelsIdcardVerificationMapper.java | 7 + .../FtbPersonnelsOverviewAnalysisMapper.java | 145 + .../FtbPersonnelsPermissionUserMapper.java | 7 + .../FtbPersonnelsPermissionsMapper.java | 57 + .../mapper/FtbPersonnelsPostApplyMapper.java | 65 + .../FtbPersonnelsPromiseConfigMapper.java | 14 + ...tbPersonnelsRecruitmentChannelsMapper.java | 7 + ...PersonnelsRegistrationFormFieldMapper.java | 21 + ...nelsRegistrationFormFieldOptionMapper.java | 7 + ...bPersonnelsRegistrationFormTypeMapper.java | 7 + .../FtbPersonnelsRegularManagementMapper.java | 38 + ...esignationCategoryConfigurationMapper.java | 14 + ...sonnelsResignationConfigurationMapper.java | 7 + ...FtbPersonnelsRewardsPunishmentsMapper.java | 107 + .../mapper/FtbPersonnelsRuleConfigMapper.java | 14 + ...ersonnelsSalaryTemporaryStorageMapper.java | 15 + .../FtbPersonnelsSecondmentConfigMapper.java | 14 + ...bPersonnelsSecondmentManagementMapper.java | 66 + .../mapper/FtbPersonnelsShortchainMapper.java | 7 + ...bPersonnelsStaffArchivesHistoryMapper.java | 8 + ...bPersonnelsStaffEmploymentApplyMapper.java | 162 + .../FtbPersonnelsStaffGrowthLogMapper.java | 7 + .../FtbPersonnelsStaffHomePageMapper.java | 18 + ...onnelsStaffRegistrationFormDataMapper.java | 62 + .../FtbPersonnelsStaffRosterMapper.java | 42 + .../FtbPersonnelsStaffRosterSchemeMapper.java | 8 + ...bPersonnelsStaffSalaryChangeLogMapper.java | 7 + ...lsStaffTransferPositionHandoverMapper.java | 7 + ...PersonnelsStaffTransferPositionMapper.java | 92 + .../FtbPersonnelsTransferManageMapper.java | 57 + ...nelsTurnoverAccountRegistrationMapper.java | 9 + .../FtbPersonnelsTurnoverAnalysisMapper.java | 93 + .../FtbPersonnelsTurnoverHandoverMapper.java | 7 + ...FtbPersonnelsTurnoverManagementMapper.java | 89 + .../FtbPersonnelsUchisuikePondMapper.java | 7 + .../FtbPersonnelsUchisuikePondOrgMapper.java | 7 + .../mapper/FtbPersonnlesInfoConfigMapper.java | 14 + ...FtbPersonnlesInfoDiyRangeConfigMapper.java | 14 + .../FtbPersonnlesInfoRangeConfigMapper.java | 14 + .../mapper/FtbThousandFacePersonMapper.java | 17 + .../msg/PersonnelsConsumerSourceBinder.java | 61 + .../msg/PersonnelsConsumerSourceMsg.java | 75 + .../service/FtbPersonStaffMCPService.java | 50 + .../service/FtbPersonnelChangesService.java | 65 + ...PersonnelsAuditCarbonRecipientService.java | 9 + ...FtbPersonnelsAuditMasterConfigService.java | 28 + ...bPersonnelsAuditRunTaskHistoryService.java | 13 + .../FtbPersonnelsAuditRunTaskService.java | 157 + .../FtbPersonnelsAuditSubConfigService.java | 28 + .../FtbPersonnelsAuditTaskInfoService.java | 14 + .../FtbPersonnelsAuthoritysService.java | 9 + .../FtbPersonnelsBlacklistHistoryService.java | 28 + .../FtbPersonnelsBlacklistService.java | 64 + .../FtbPersonnelsBlacklistTypeService.java | 46 + .../FtbPersonnelsBrainDrainService.java | 167 + .../FtbPersonnelsContactInfoService.java | 15 + .../service/FtbPersonnelsEmEntryService.java | 58 + .../FtbPersonnelsEmployeeTypeService.java | 22 + .../FtbPersonnelsGoodsReceiveService.java | 33 + .../service/FtbPersonnelsGoodsService.java | 26 + .../service/FtbPersonnelsMetaDataService.java | 74 + .../FtbPersonnelsOverviewAnalysisService.java | 233 + .../FtbPersonnelsPermissionUserService.java | 9 + .../FtbPersonnelsPermissionsService.java | 111 + .../FtbPersonnelsPostApplyService.java | 46 + .../FtbPersonnelsPromiseConfigService.java | 16 + ...bPersonnelsRecruitmentChannelsService.java | 21 + ...elsRegistrationFormFieldOptionService.java | 17 + ...ersonnelsRegistrationFormFieldService.java | 92 + ...PersonnelsRegistrationFormTypeService.java | 73 + ...FtbPersonnelsRegularManagementService.java | 55 + ...signationCategoryConfigurationService.java | 16 + ...onnelsResignationConfigurationService.java | 35 + ...tbPersonnelsRewardsPunishmentsService.java | 148 + .../FtbPersonnelsRosterValidService.java | 44 + .../FtbPersonnelsRuleConfigService.java | 16 + .../service/FtbPersonnelsSalaryService.java | 100 + ...PersonnelsSecondmentManagementService.java | 113 + .../FtbPersonnelsShortchainService.java | 36 + ...PersonnelsStaffArchivesHistoryService.java | 24 + ...PersonnelsStaffEmploymentApplyService.java | 163 + .../FtbPersonnelsStaffGrowthLogService.java | 38 + .../FtbPersonnelsStaffHomePageService.java | 29 + ...nnelsStaffRegistrationFormDataService.java | 219 + ...FtbPersonnelsStaffRosterImportService.java | 65 + ...FtbPersonnelsStaffRosterSchemeService.java | 22 + .../FtbPersonnelsStaffRosterService.java | 373 ++ ...PersonnelsStaffSalaryChangeLogService.java | 27 + ...sStaffTransferPositionHandoverService.java | 9 + ...ersonnelsStaffTransferPositionService.java | 105 + .../FtbPersonnelsTransferManageService.java | 129 + .../service/FtbPersonnelsTrialService.java | 21 + .../FtbPersonnelsTurnoverAnalysisService.java | 196 + .../FtbPersonnelsTurnoverHandoverService.java | 9 + ...tbPersonnelsTurnoverManagementService.java | 151 + .../FtbPersonnelsUchisuikePondService.java | 72 + .../FtbPersonnlesInfoConfigService.java | 29 + .../FtbPersonnlesInfoRangeConfigService.java | 13 + ...FtbRewardsPunishmentsApproveOAService.java | 36 + .../service/FtbThousandFacePersonService.java | 9 + .../impl/FtbPersonStaffMCPServiceImpl.java | 157 + .../impl/FtbPersonnelChangesServiceImpl.java | 402 ++ ...onnelsAuditCarbonRecipientServiceImpl.java | 16 + ...ersonnelsAuditMasterConfigServiceImpl.java | 460 ++ ...sonnelsAuditRunTaskHistoryServiceImpl.java | 29 + .../FtbPersonnelsAuditRunTaskServiceImpl.java | 740 +++ ...tbPersonnelsAuditSubConfigServiceImpl.java | 67 + ...FtbPersonnelsAuditTaskInfoServiceImpl.java | 23 + .../FtbPersonnelsAuthoritysServiceImpl.java | 13 + ...PersonnelsBlacklistHistoryServiceImpl.java | 28 + .../FtbPersonnelsBlacklistServiceImpl.java | 149 + ...FtbPersonnelsBlacklistTypeServiceImpl.java | 82 + .../FtbPersonnelsBrainDrainServiceImpl.java | 775 +++ .../FtbPersonnelsContactInfoServiceImpl.java | 404 ++ .../impl/FtbPersonnelsEmEntryServiceImpl.java | 1785 +++++ .../FtbPersonnelsEmployeeTypeServiceImpl.java | 116 + .../FtbPersonnelsGoodsReceiveServiceImpl.java | 247 + .../impl/FtbPersonnelsGoodsServiceImpl.java | 143 + .../FtbPersonnelsMetaDataServiceImpl.java | 788 +++ ...PersonnelsOverviewAnalysisServiceImpl.java | 1660 +++++ ...tbPersonnelsPermissionUserServiceImpl.java | 13 + .../FtbPersonnelsPermissionsServiceImpl.java | 484 ++ .../FtbPersonnelsPostApplyServiceImpl.java | 1139 ++++ ...FtbPersonnelsPromiseConfigServiceImpl.java | 23 + ...sonnelsRecruitmentChannelsServiceImpl.java | 39 + ...egistrationFormFieldOptionServiceImpl.java | 33 + ...nnelsRegistrationFormFieldServiceImpl.java | 427 ++ ...onnelsRegistrationFormTypeServiceImpl.java | 191 + ...ersonnelsRegularManagementServiceImpl.java | 1137 ++++ ...ationCategoryConfigurationServiceImpl.java | 23 + ...lsResignationConfigurationServiceImpl.java | 99 + ...rsonnelsRewardsPunishmentsServiceImpl.java | 352 + .../FtbPersonnelsRosterValidServiceImpl.java | 387 ++ ...tbPersonnelsRosterValidServiceNewImpl.java | 555 ++ .../FtbPersonnelsRuleConfigServiceImpl.java | 23 + .../impl/FtbPersonnelsSalaryServiceImpl.java | 467 ++ ...onnelsSecondmentManagementServiceImpl.java | 673 ++ .../FtbPersonnelsShortchainServiceImpl.java | 64 + ...onnelsStaffArchivesHistoryServiceImpl.java | 46 + ...onnelsStaffEmploymentApplyServiceImpl.java | 1418 ++++ ...tbPersonnelsStaffGrowthLogServiceImpl.java | 111 + ...FtbPersonnelsStaffHomePageServiceImpl.java | 392 ++ ...sStaffRegistrationFormDataServiceImpl.java | 2389 +++++++ ...ersonnelsStaffRosterImportServiceImpl.java | 491 ++ ...ersonnelsStaffRosterSchemeServiceImpl.java | 97 + .../FtbPersonnelsStaffRosterServiceImpl.java | 4713 +++++++++++++ ...onnelsStaffSalaryChangeLogServiceImpl.java | 55 + ...ffTransferPositionHandoverServiceImpl.java | 13 + ...nnelsStaffTransferPositionServiceImpl.java | 1558 +++++ ...tbPersonnelsTransferManageServiceImpl.java | 638 ++ .../impl/FtbPersonnelsTrialServiceImpl.java | 107 + ...PersonnelsTurnoverAnalysisServiceImpl.java | 1205 ++++ ...PersonnelsTurnoverHandoverServiceImpl.java | 13 + ...rsonnelsTurnoverManagementServiceImpl.java | 1879 ++++++ ...FtbPersonnelsUchisuikePondServiceImpl.java | 284 + .../FtbPersonnlesInfoConfigServiceImpl.java | 131 + ...tbPersonnlesInfoDiyRangeConfigService.java | 19 + ...bPersonnlesInfoRangeConfigServiceImpl.java | 20 + ...ewardsPunishmentsApproveOAServiceImpl.java | 174 + .../FtbThousandFacePersonServiceImpl.java | 135 + .../personnels/utils/AuthServerConstant.java | 17 + .../personnels/utils/CacheExcelUtils.java | 332 + .../jnpf/personnels/utils/CompanyAgeUtil.java | 248 + .../personnels/utils/CovertDateUtils.java | 58 + .../personnels/utils/ExcelHeaderUtil.java | 45 + .../utils/FtbPersonnlesIMUtils.java | 156 + .../utils/NoSendContactIMUtils.java | 104 + .../personnels/utils/PersonSalaryUtils.java | 123 + .../PersonalizedTenantWhitelistUtils.java | 36 + .../utils/PersonnelAsyncImportUtils.java | 245 + .../utils/PersonnelAsyncOldData.java | 61 + .../utils/PersonnelAsyncServiceUtils.java | 242 + .../personnels/utils/PersonnelCardOcr.java | 118 + .../utils/PersonnelDataAnalysisUtil.java | 158 + .../PersonnelIdCardVerificationUtils.java | 143 + .../personnels/utils/PersonnelOrgUtils.java | 2533 +++++++ .../personnels/utils/PersonnelPerUtils.java | 418 ++ .../utils/PersonnelPreTrailIMUtils.java | 68 + .../personnels/utils/PersonnelStaffUtils.java | 92 + .../utils/RosterExportThreadPool.java | 93 + .../personnels/utils/RosterExportUtils.java | 720 ++ .../java/jnpf/personnels/utils/SmsConfig.java | 43 + .../jnpf/personnels/utils/SmsProperties.java | 37 + .../jnpf/personnels/utils/SmsSendUtil.java | 108 + .../jnpf/personnels/utils/SmsTemplate.java | 20 + .../utils/StaffRosterImportSaveUtils.java | 741 +++ .../java/jnpf/preperties/CustomChannels.java | 12 + .../consummer/PositionGradeConsumer.java | 340 + .../PositionGradeConsumerSource.java | 17 + .../controller/QualificationsController.java | 171 + ...QualificationsFieldCategoryController.java | 80 + .../QualificationsFieldController.java | 115 + .../QualificationsFieldCategoryMapper.java | 21 + .../mapper/QualificationsFieldItemMapper.java | 13 + ...QualificationsFieldItemRelationMapper.java | 13 + .../mapper/QualificationsFieldMapper.java | 22 + .../mapper/QualificationsMapper.java | 15 + .../QualificationsFieldCategoryService.java | 26 + ...ualificationsFieldItemRelationService.java | 13 + .../QualificationsFieldItemService.java | 15 + .../service/QualificationsFieldService.java | 33 + .../service/QualificationsService.java | 56 + ...ualificationsFieldCategoryServiceImpl.java | 132 + ...ficationsFieldItemRelationServiceImpl.java | 19 + .../QualificationsFieldItemServiceImpl.java | 27 + .../impl/QualificationsFieldServiceImpl.java | 183 + .../impl/QualificationsServiceImpl.java | 423 ++ .../store/controller/StoreController.java | 566 ++ .../jnpf/store/controller/TestController.java | 106 + .../java/jnpf/store/mapper/StoreMapper.java | 151 + .../jnpf/store/mapper/StoreRegionMapper.java | 14 + .../jnpf/store/mapper/StoreUserMapper.java | 37 + .../store/mapper/StoreUserRelationMapper.java | 31 + .../store/service/StoreRegionService.java | 26 + .../java/jnpf/store/service/StoreService.java | 199 + .../jnpf/store/service/StoreUserService.java | 26 + .../service/impl/StoreRegionServiceImpl.java | 118 + .../store/service/impl/StoreServiceImpl.java | 995 +++ .../service/impl/StoreUserServiceImpl.java | 38 + .../StoreCertificatePhotoController.java | 120 + .../controller/WarningNoticeController.java | 109 + .../helper/StoreCertificatePhotoHelper.java | 51 + .../mapper/BaseParamMapper.java | 81 + .../StoreCertificatePhotoItemMapper.java | 11 + .../mapper/StoreCertificatePhotoMapper.java | 11 + .../service/StoreCertificatePhotoService.java | 52 + .../service/WarningNoticeService.java | 43 + .../StoreCertificatePhotoServiceImpl.java | 518 ++ .../impl/WarningNoticeServiceImpl.java | 641 ++ .../controller/TempModuleController.java | 105 + .../tempmodule/mapper/AppBannerMapper.java | 13 + .../tempmodule/mapper/RollImageMapper.java | 14 + .../tempmodule/service/AppBannerService.java | 16 + .../tempmodule/service/RollImageService.java | 30 + .../service/impl/AppBannerServiceImpl.java | 34 + .../service/impl/RollImageServiceImpl.java | 71 + .../src/main/java/jnpf/util/Base64Util.java | 71 + .../main/java/jnpf/util/CustomTenantUtil.java | 104 + .../main/java/jnpf/util/DateConvertUtil.java | 149 + .../src/main/java/jnpf/util/DateRange.java | 86 + .../src/main/java/jnpf/util/DynDicUtil.java | 196 + .../main/java/jnpf/util/EasyExcelUtil.java | 180 + .../src/main/java/jnpf/util/GenUtil.java | 473 ++ .../src/main/java/jnpf/util/Html2Text.java | 57 + .../src/main/java/jnpf/util/HttpStatus.java | 93 + .../src/main/java/jnpf/util/HttpUtil.java | 173 + .../java/jnpf/util/MultithreadExecutors.java | 95 + .../src/main/java/jnpf/util/NumberUtils.java | 24 + .../main/java/jnpf/util/OptionalUtils.java | 280 + .../src/main/java/jnpf/util/PageUtil.java | 34 + .../src/main/java/jnpf/util/ParamUtil.java | 125 + .../src/main/java/jnpf/util/PingYinUtil.java | 63 + .../java/jnpf/util/QuestionAnalysisUtil.java | 736 +++ .../jnpf/util/QuestionBankIDGenerator.java | 29 + .../main/java/jnpf/util/RangeConfigUtil.java | 194 + .../java/jnpf/util/RedisDistributedLock.java | 77 + .../main/java/jnpf/util/SeetaFaceUtil.java | 137 + .../main/java/jnpf/util/SelfGrowthUtil.java | 108 + .../main/java/jnpf/util/ServiceException.java | 94 + .../src/main/java/jnpf/util/TenantUtil.java | 38 + .../main/java/jnpf/util/V2UserQueryUtil.java | 101 + .../main/java/jnpf/util/ValidatorUtils.java | 37 + .../AttendanceGroupUserStatusUtil.java | 244 + .../jnpf/util/attendance/DailyRuleUtil.java | 51 + .../util/attendance/DayStatisticsUtils.java | 3553 ++++++++++ .../jnpf/util/attendance/ExpiresTimeUtil.java | 136 + .../attendance/GetPdfPrintDataDetails.java | 74 + .../util/attendance/GetPdfPrintDataModel.java | 56 + .../util/attendance/ImageCompressUtil.java | 88 + .../attendance/MachineStrategyFactory.java | 26 + .../jnpf/util/attendance/MqttPushClient.java | 181 + .../java/jnpf/util/attendance/MqttSender.java | 73 + .../util/attendance/OcsWatermarkUtils.java | 35 + .../jnpf/util/attendance/PermissionUtil.java | 19 + .../jnpf/util/attendance/PushCallback.java | 76 + .../util/attendance/RuleExcelImportUtil.java | 25 + .../jnpf/util/attendance/RuleScopeUtil.java | 443 ++ .../util/attendance/SecondmentTypeUtil.java | 50 + .../attendance/UnionQualityExcelUtil.java | 176 + .../util/auth/V2AuthPermissionController.java | 23 + .../jnpf/util/auth/V2AuthPermissionUtils.java | 414 ++ .../java/jnpf/util/excel/EasyExcelUtils.java | 359 + .../jnpf/util/excel/ExcelSelectedResolve.java | 33 + .../java/jnpf/util/excel/ShortChainUtils.java | 103 + .../src/main/java/jnpf/util/im/ImConst.java | 27 + .../jnpf/util/im/ImMessageNoticeUtils.java | 108 + .../java/jnpf/util/mapper/MybatisUtil.java | 109 + .../java/jnpf/util/permssion/V2Utils.java | 263 + .../v2/personnels/V2PersonnelController.java | 17 + .../controller/ApplyClockInController.java | 278 + .../workflow/controller/ApplyController.java | 153 + .../controller/LeaveApproveController.java | 84 + .../RewardsPunishmentsApproveController.java | 202 + .../controller/SelfApproveController.java | 84 + .../WorkOvertimeApproveController.java | 91 + .../mapper/ApplyAttendanceChangeMapper.java | 23 + .../mapper/ApplyAttendanceOutsideMapper.java | 14 + .../mapper/ApplyAttendanceRepairMapper.java | 23 + .../ApplyAttendanceViolationMapper.java | 13 + .../mapper/AttendanceLeaveMapper.java | 14 + .../AttendanceWorkOvertimeApproveMapper.java | 13 + .../mapper/BusinessTripApproveMapper.java | 13 + .../workflow/mapper/GoOutApproveMapper.java | 13 + .../mapper/PunishmentsApprovalMapper.java | 21 + .../mapper/PunishmentsApprovalUserMapper.java | 15 + .../workflow/mapper/RewardApprovalMapper.java | 21 + .../mapper/RewardApprovalUserMapper.java | 15 + .../workflow/mapper/SelfApproveMapper.java | 8 + .../mapper/SelfApproveUserMapper.java | 43 + .../service/ApplyAttendanceChangeService.java | 37 + .../ApplyAttendanceOutsideService.java | 36 + .../service/ApplyAttendanceRepairService.java | 38 + .../ApplyAttendanceViolationService.java | 20 + .../workflow/service/ApplyPicService.java | 27 + .../service/BusinessTripApproveService.java | 37 + .../workflow/service/FlowTaskService.java | 22 + .../workflow/service/GoOutApproveService.java | 36 + .../workflow/service/LeaveApproveService.java | 42 + .../service/PunishmentsApprovalService.java | 23 + .../jnpf/workflow/service/RewardService.java | 26 + .../workflow/service/SelfApproveService.java | 41 + .../service/WorkOvertimeApproveService.java | 38 + .../ApplyAttendanceChangeServiceImpl.java | 246 + .../ApplyAttendanceOutsideServiceImpl.java | 144 + .../ApplyAttendanceRepairServiceImpl.java | 232 + .../ApplyAttendanceViolationServiceImpl.java | 71 + .../service/impl/ApplyPicServiceImpl.java | 68 + .../impl/BusinessTripApproveServiceImpl.java | 178 + .../service/impl/FlowTaskServiceImpl.java | 52 + .../service/impl/GoOutApproveServiceImpl.java | 167 + .../service/impl/LeaveApproveServiceImpl.java | 196 + .../impl/PunishmentsApprovalServiceImpl.java | 86 + .../service/impl/RewardServiceImpl.java | 88 + .../service/impl/SelfApproveServiceImpl.java | 191 + .../impl/WorkOvertimeApproveServiceImpl.java | 157 + .../src/main/resources/application.yml | 44 + .../src/main/resources/bootstrap.yml | 76 + .../main/resources/fonts/MiSans-Normal.ttf | Bin 0 -> 8201144 bytes .../main/resources/fonts/MiSans-Semibold.ttf | Bin 0 -> 8034980 bytes ...icate_instance_upgrade_template_status.sql | 29 + .../src/main/resources/img/icon-bottom.png | Bin 0 -> 47960 bytes .../attendance/mapper/AttendanceAIMapper.xml | 56 + .../mapper/AttendanceApproveMapper.xml | 24 + .../mapper/AttendanceBalanceRecordMapper.xml | 457 ++ .../AttendanceBalanceUseRecordMapper.xml | 80 + .../mapper/AttendanceBaseSettingMapper.xml | 10 + ...AttendanceCardReplacementApproveMapper.xml | 42 + .../mapper/AttendanceClockInMapper.xml | 228 + .../mapper/AttendanceClockInResultMapper.xml | 289 + .../mapper/AttendanceConfirmMapper.xml | 68 + .../mapper/AttendanceDailyRuleMapper.xml | 319 + .../mapper/AttendanceDayStatisticsMapper.xml | 1535 +++++ .../mapper/AttendanceFestivalRulesMapper.xml | 32 + .../mapper/AttendanceFieldPersonnelMapper.xml | 22 + .../AttendanceFieldpersonnelApproveMapper.xml | 48 + .../mapper/AttendanceFixedClassMapper.xml | 20 + .../mapper/AttendanceGroupMapper.xml | 235 + .../mapper/AttendanceGroupUserMapper.xml | 125 + .../mapper/AttendanceLeaveApproveMapper.xml | 481 ++ .../AttendanceLeaveGrantSettingMapper.xml | 4 + .../mapper/AttendanceLeaveRulesMapper.xml | 33 + .../mapper/AttendanceLeaveSettingsMapper.xml | 11 + .../mapper/AttendanceLeaveTypeMapper.xml | 4 + .../AttendanceLocationSettingMapper.xml | 11 + .../mapper/AttendanceMachineLogMapper.xml | 14 + .../mapper/AttendanceMachineManageMapper.xml | 88 + .../AttendanceManagerPermissionMapper.xml | 289 + .../mapper/AttendanceNoticeMapper.xml | 17 + .../mapper/AttendanceQuickTemplateMapper.xml | 65 + .../mapper/AttendanceRepairMapper.xml | 16 + .../mapper/AttendanceSelfApproveMapper.xml | 55 + .../AttendanceShiftNameSettingMapper.xml | 101 + .../mapper/AttendanceUserBalanceMapper.xml | 46 + .../AttendanceUserBalanceRecordMapper.xml | 4 + .../mapper/AttendanceUserConfigMapper.xml | 21 + .../mapper/AttendanceUserFaceMapper.xml | 40 + .../mapper/AttendanceUserSettingMapper.xml | 77 + .../attendance/mapper/ClockInResultMapper.xml | 5 + .../mapper/ClockInResultV2Mapper.xml | 5 + .../mapper/DailyRuleChangeMapper.xml | 24 + .../attendance/mapper/DailyRuleMapper.xml | 5 + .../attendance/mapper/EnableBalanceMapper.xml | 38 + .../FtbAttendanceFaceChangeLogMapper.xml | 27 + .../FtbScheduleGroupDrawingParamMapper.xml | 21 + .../FtbScheduleGroupFixedParamMapper.xml | 27 + .../mapper/InitializationMapper.xml | 27 + .../attendance/mapper/OvertimeRuleMapper.xml | 56 + .../mapper/PublicHolidayRulesMapper.xml | 124 + .../attendance/mapper/RuleScopeMapper.xml | 74 + .../attendance/mapper/StatisticsMapper.xml | 399 ++ .../attendance/mapper/StorageRestMapper.xml | 44 + .../attendance/mapper/UserPhoneMapper.xml | 18 + .../FtbPermissionFunctionMenuMapper.xml | 33 + .../mapper/FtbPermissionMigrateMapper.xml | 52 + ...FtbPermissionRoleAuthorizePersonMapper.xml | 150 + .../FtbPermissionRoleAuthorizePostMapper.xml | 25 + .../mapper/FtbPermissionRoleMapper.xml | 61 + .../mapper/FtbPermissionRoleMenuMapper.xml | 20 + .../FtbPermissionRoleMenuRelationMapper.xml | 15 + .../mapper/CertificateAppReminderMapper.xml | 5 + .../mapper/CertificateAppRiskMapper.xml | 148 + .../CertificateBusinessLicenseExtMapper.xml | 5 + .../CertificateHygieneLicenseExtMapper.xml | 5 + .../mapper/CertificateInstanceItemMapper.xml | 6 + .../mapper/CertificateInstanceMapper.xml | 284 + .../mapper/CultivateCourseMsgMapper.xml | 52 + .../mapper/CultivateCourseMsgUserMapper.xml | 7 + .../mapper/CultivateCoverInfoMapper.xml | 27 + .../mapper/CultivateExamDrawRuleMapper.xml | 16 + .../cultivate/mapper/CultivateExamMapper.xml | 103 + .../cultivate/mapper/CultivateFileMapper.xml | 5 + .../CultivateIdentifyApplyDetailsMapper.xml | 32 + .../mapper/CultivateIdentifyApplyMapper.xml | 658 ++ .../mapper/CultivateIdentifyItemsMapper.xml | 47 + .../mapper/CultivateIdentifyTableMapper.xml | 504 ++ .../CultivatePositionCourseLogMapper.xml | 48 + .../mapper/FtbCourseGainedCommentMapper.xml | 91 + .../mapper/FtbCourseGainedLikeMapper.xml | 14 + .../mapper/FtbCourseGainedMapper.xml | 11 + .../FtbCultivateAssessmentPointsMapper.xml | 23 + .../mapper/FtbCultivateCaseBaseLikeMapper.xml | 5 + .../mapper/FtbCultivateCaseBaseMapper.xml | 89 + .../FtbCultivateCertificateImagesMapper.xml | 18 + .../mapper/FtbCultivateCertificateMapper.xml | 133 + .../FtbCultivateCertificateUserMapper.xml | 5 + .../mapper/FtbCultivateChapterTestMapper.xml | 73 + .../FtbCultivateChapterTestOptionMapper.xml | 27 + .../FtbCultivateChapterTestResultMapper.xml | 25 + .../FtbCultivateCommonSettingGlobalMapper.xml | 22 + .../FtbCultivateCourseChapterMapper.xml | 19 + .../FtbCultivateCourseLearningLogMapper.xml | 16 + .../mapper/FtbCultivateCourseMapper.xml | 296 + .../FtbCultivateCoursePackageMapper.xml | 51 + .../FtbCultivateCourseSettingGlobalMapper.xml | 5 + .../FtbCultivateCourseSettingMapper.xml | 10 + .../FtbCultivateCourseStatisticesMapper.xml | 206 + .../mapper/FtbCultivateCourseTypeMapper.xml | 15 + .../FtbCultivateExamFrequncyLogMapper.xml | 37 + .../FtbCultivateExamHistoryPaperMapper.xml | 4 + .../mapper/FtbCultivateExamMapper.xml | 72 + .../FtbCultivateExamUserDetailMapper.xml | 28 + .../mapper/FtbCultivateExamUserMapper.xml | 1020 +++ .../mapper/FtbCultivateFileMapper.xml | 29 + .../FtbCultivateIdentifyCategoriesMapper.xml | 8 + .../FtbCultivateIdentifyItemsPoolMapper.xml | 39 + .../mapper/FtbCultivateLabelMapper.xml | 26 + .../FtbCultivateLearnCategoriesMapper.xml | 26 + .../FtbCultivateLearnTaskAssignmentMapper.xml | 85 + ...FtbCultivateLearnTaskCertificateMapper.xml | 75 + .../FtbCultivateLearnTaskCourseMapper.xml | 81 + .../FtbCultivateLearnTaskExamMapper.xml | 111 + ...CultivateLearnTaskIdentificationMapper.xml | 94 + ...FtbCultivateLearnTaskInfoContentMapper.xml | 18 + .../FtbCultivateLearnTaskInfoMapper.xml | 173 + .../mapper/FtbCultivateLearnTaskMapper.xml | 321 + .../FtbCultivateLearnTaskPhaseMapper.xml | 16 + .../FtbCultivateLearnTaskPracticeMapper.xml | 54 + ...tbCultivateLearnTaskReminderRuleMapper.xml | 11 + .../FtbCultivateMyLearnTaskInfoMapper.xml | 29 + .../FtbCultivateOfflineCourseMapper.xml | 5 + .../mapper/FtbCultivateOfflineTrainMapper.xml | 140 + .../mapper/FtbCultivateOfflineUserMapper.xml | 24 + .../FtbCultivatePackageCourseMapper.xml | 5 + .../FtbCultivatePositionCertificateMapper.xml | 13 + ...atePositionCourceChapterLearningMapper.xml | 18 + ...bCultivatePositionCourceLearningMapper.xml | 190 + ...ltivatePositionCourseCertificateMapper.xml | 94 + .../FtbCultivatePositionCourseExamMapper.xml | 183 + ...bCultivatePositionCourseIdentityMapper.xml | 127 + .../FtbCultivatePositionCourseMapper.xml | 320 + ...bCultivatePositionCoursePracticeMapper.xml | 127 + ...FtbCultivatePositionExamIdentifyMapper.xml | 5 + .../mapper/FtbCultivatePositionExamMapper.xml | 25 + ...bCultivatePositionIdentifyResultMapper.xml | 7 + .../mapper/FtbCultivatePositionLogMapper.xml | 5 + .../mapper/FtbCultivatePositionMapper.xml | 177 + .../FtbCultivatePositionSettingMapper.xml | 6 + .../FtbCultivatePositionStatisticesMapper.xml | 615 ++ .../mapper/FtbCultivatePositionUserMapper.xml | 29 + .../mapper/FtbCultivatePromotionLogMapper.xml | 5 + .../mapper/FtbCultivatePromotionMapper.xml | 348 + .../FtbCultivatePromotionMemberMapper.xml | 115 + .../FtbCultivatePromotionMemberNewMapper.xml | 68 + .../mapper/FtbCultivatePromotionNewMapper.xml | 619 ++ .../FtbCultivatePromotionPostApplyMapper.xml | 47 + .../FtbCultivatePromotionPostMapper.xml | 69 + .../FtbCultivatePromotionPostNewMapper.xml | 39 + .../FtbCultivatePromotionSettingMapper.xml | 5 + .../FtbCultivatePromotionUserMapper.xml | 56 + .../FtbCultivateQuestionBankCourseMapper.xml | 22 + .../mapper/FtbCultivateQuestionBankMapper.xml | 24 + .../mapper/FtbCultivateQuestionMapper.xml | 122 + .../FtbCultivateQuestionOptionMapper.xml | 29 + .../FtbCultivateQuestionPointsMapper.xml | 18 + .../mapper/FtbCultivateRuleMapper.xml | 5 + .../mapper/FtbCultivateStatisticsMapper.xml | 259 + .../mapper/FtbCultivateTestPaperMapper.xml | 31 + .../FtbCultivateTestPaperQuestionMapper.xml | 25 + .../FtbCultivateTestPaperRuleMapper.xml | 23 + .../mapper/TeachingApproveMapper.xml | 27 + .../cultivate/mapper/TeachingRecordMapper.xml | 475 ++ .../cultivate/mapper/TeachingSkillMapper.xml | 50 + .../mapper/TeachingStudentMapper.xml | 66 + .../culture/mapper/CultureClockInMapper.xml | 62 + .../mapper/CultureClockInStatMapper.xml | 9 + .../mapper/CulturePicSettingMapper.xml | 16 + .../culture/mapper/CulturePicTempMapper.xml | 5 + .../mapper/CultureTextSettingMapper.xml | 51 + .../jnpf/doclibrary/document/空白PPT.pptx | Bin 0 -> 33112 bytes .../jnpf/doclibrary/document/空白文档.docx | 0 .../jnpf/doclibrary/document/空白表格.xlsx | Bin 0 -> 6618 bytes .../doclibrary/mapper/CommonConfigMapper.xml | 19 + .../mapper/InformationAuthorityMapper.xml | 109 + .../mapper/InformationLogMapper.xml | 9 + .../doclibrary/mapper/InformationMapper.xml | 165 + .../mapper/InformationRubbishMapper.xml | 10 + .../InfornationRecentlyViewedMapper.xml | 5 + .../jnpf/doclibrary/mapper/SpaceMapper.xml | 393 ++ .../mapper/FranchiseeCustomFieldMapper.xml | 28 + .../mapper/FranchiseeExperienceMapper.xml | 36 + .../mapper/FranchiseeJoinRegionMapper.xml | 28 + .../franchisee/mapper/FranchiseeMapper.xml | 129 + .../memberLog/mapper/LogCommentMapper.xml | 7 + .../mapper/LogMemberAssociationMapper.xml | 58 + .../memberLog/mapper/LogMemberFieldMapper.xml | 29 + .../memberLog/mapper/LogMemberLogMapper.xml | 61 + .../mapper/LogTemplateFieldMapper.xml | 12 + .../memberLog/mapper/LogTemplateMapper.xml | 9 + .../jnpf/memberLog/mapper/WorkLogMapper.xml | 330 + .../FtbNoticeAnnouncementsLogMapper.xml | 53 + .../mapper/FtbNoticeAnnouncementsMapper.xml | 345 + .../FtbNoticeAnnouncementsReceiveMapper.xml | 91 + .../mapper/FtbNoticeCategoriesMapper.xml | 26 + .../notice/mapper/FtbNoticeFilesMapper.xml | 29 + .../notice/mapper/FtbNoticeManagerMapper.xml | 44 + .../FtbNoticeUserGroupMembersMapper.xml | 69 + .../mapper/FtbNoticeUserGroupsMapper.xml | 27 + .../mapper/FtbPersonStaffMCPMapper.xml | 46 + .../mapper/FtbPersonnelChangesMapper.xml | 75 + ...tbPersonnelsAuditCarbonRecipientMapper.xml | 5 + .../FtbPersonnelsAuditMasterConfigMapper.xml | 5 + ...FtbPersonnelsAuditRunTaskHistoryMapper.xml | 20 + .../FtbPersonnelsAuditRunTaskMapper.xml | 50 + .../FtbPersonnelsAuditSubConfigMapper.xml | 83 + .../FtbPersonnelsAuditTaskInfoMapper.xml | 96 + .../mapper/FtbPersonnelsAuthoritysMapper.xml | 5 + .../FtbPersonnelsBlacklistHistoryMapper.xml | 33 + .../mapper/FtbPersonnelsBlacklistMapper.xml | 35 + .../FtbPersonnelsBlacklistTypeMapper.xml | 5 + .../mapper/FtbPersonnelsGoodsMapper.xml | 27 + .../FtbPersonnelsGoodsReceiveMapper.xml | 32 + .../FtbPersonnelsIdcardVerificationMapper.xml | 5 + .../FtbPersonnelsOverviewAnalysisMapper.xml | 374 ++ .../FtbPersonnelsPermissionUserMapper.xml | 5 + .../mapper/FtbPersonnelsPermissionsMapper.xml | 92 + .../mapper/FtbPersonnelsPostApplyMapper.xml | 346 + .../FtbPersonnelsPromiseConfigMapper.xml | 25 + ...FtbPersonnelsRecruitmentChannelsMapper.xml | 5 + ...bPersonnelsRegistrationFormFieldMapper.xml | 60 + ...nnelsRegistrationFormFieldOptionMapper.xml | 5 + ...tbPersonnelsRegistrationFormTypeMapper.xml | 5 + .../FtbPersonnelsRegularManagementMapper.xml | 487 ++ ...ResignationCategoryConfigurationMapper.xml | 17 + ...rsonnelsResignationConfigurationMapper.xml | 25 + .../FtbPersonnelsRewardsPunishmentsMapper.xml | 303 + .../mapper/FtbPersonnelsRuleConfigMapper.xml | 15 + ...PersonnelsSalaryTemporaryStorageMapper.xml | 5 + .../FtbPersonnelsSecondmentConfigMapper.xml | 5 + ...tbPersonnelsSecondmentManagementMapper.xml | 348 + .../mapper/FtbPersonnelsShortchainMapper.xml | 5 + ...tbPersonnelsStaffArchivesHistoryMapper.xml | 31 + ...tbPersonnelsStaffEmploymentApplyMapper.xml | 978 +++ .../FtbPersonnelsStaffGrowthLogMapper.xml | 5 + .../FtbPersonnelsStaffHomePageMapper.xml | 39 + ...sonnelsStaffRegistrationFormDataMapper.xml | 89 + .../mapper/FtbPersonnelsStaffRosterMapper.xml | 515 ++ .../FtbPersonnelsStaffRosterSchemeMapper.xml | 6 + ...tbPersonnelsStaffSalaryChangeLogMapper.xml | 5 + ...elsStaffTransferPositionHandoverMapper.xml | 5 + ...bPersonnelsStaffTransferPositionMapper.xml | 603 ++ .../FtbPersonnelsTransferManageMapper.xml | 131 + ...nnelsTurnoverAccountRegistrationMapper.xml | 5 + .../FtbPersonnelsTurnoverAnalysisMapper.xml | 160 + .../FtbPersonnelsTurnoverHandoverMapper.xml | 5 + .../FtbPersonnelsTurnoverManagementMapper.xml | 489 ++ .../FtbPersonnelsUchisuikePondMapper.xml | 5 + .../FtbPersonnelsUchisuikePondOrgMapper.xml | 5 + .../mapper/FtbPersonnlesInfoConfigMapper.xml | 5 + .../FtbPersonnlesInfoDiyRangeConfigMapper.xml | 5 + .../FtbPersonnlesInfoRangeConfigMapper.xml | 5 + .../mapper/FtbThousandFacePersonMapper.xml | 37 + .../QualificationsFieldCategoryMapper.xml | 29 + .../mapper/QualificationsFieldMapper.xml | 30 + .../mapper/QualificationsMapper.xml | 6 + .../jnpf/store/mapper/StoreMapper.xml | 329 + .../jnpf/store/mapper/StoreUserMapper.xml | 64 + .../store/mapper/StoreUserRelationMapper.xml | 38 + .../mapper/BaseParamMapper.xml | 112 + .../StoreCertificatePhotoItemMapper.xml | 5 + .../mapper/StoreCertificatePhotoMapper.xml | 5 + .../tempmodule/mapper/AppBannerMapper.xml | 5 + .../tempmodule/mapper/RollImageMapper.xml | 5 + .../mapper/ApplyAttendanceChangeMapper.xml | 18 + .../mapper/ApplyAttendanceRepairMapper.xml | 18 + .../mapper/PunishmentsApprovalMapper.xml | 21 + .../workflow/mapper/RewardApprovalMapper.xml | 20 + .../workflow/mapper/SelfApproveUserMapper.xml | 44 + .../src/main/resources/logback-spring.xml | 339 + .../question/questionExportTemplate.xlsx | Bin 0 -> 10357 bytes .../resources/question/questionTemplate.xlsx | Bin 0 -> 9614 bytes .../src/main/resources/roster/rewards.xlsx | Bin 0 -> 12052 bytes .../src/main/resources/roster/roster.xlsx | Bin 0 -> 11114 bytes .../certificate/ftb_certificate_instance.sql | 98 + ...stance_upgrade_health_certificate_data.sql | 173 + .../base_dictionary_franchisee_nature.sql | 136 + .../base_dictionary_industry_param.sql | 123 + .../sql/franchisee/ftb_franchisee.sql | 134 + ...anchisee_batch_insert_5000_with_region.sql | 3263 +++++++++ ...de_business_license_from_base_organize.sql | 152 + ...ate_instance_upgrade_org_store_missing.sql | 140 + .../ftb_store_add_store_type_franchisee.sql | 12 + .../sql/storecertificatephoto/base_param.sql | 11 + .../ftb_store_certificate_photo.sql | 51 + .../resources/startup-optimization-plan.md | 133 + jnpf-ftb/jnpf-ftb-entity/pom.xml | 73 + .../main/java/jnpf/annotation/Sensitive.java | 36 + .../jnpf/annotation/check/CheckLength.java | 17 + .../jnpf/annotation/check/CheckListSize.java | 13 + .../java/jnpf/annotation/check/CheckNull.java | 13 + .../jnpf/constants/AttendanceConstant.java | 318 + .../AttendancePermissionConstant.java | 43 + .../jnpf/constants/MessageTopicConstants.java | 69 + .../constants/QualificationsConstant.java | 13 + .../java/jnpf/constants/RedisConstant.java | 38 + .../java/jnpf/constants/ShareMsgConstant.java | 9 + .../jnpf/easyexcel/DynamicHeaderHandler.java | 31 + .../java/jnpf/easyexcel/EasyEnumNames.java | 9 + .../easyexcel/EasyEnumNamesConverter.java | 28 + .../jnpf/easyexcel/FastJsonEnumConverter.java | 28 + .../easyexcel/StaffDateStringConverter.java | 88 + .../src/main/java/jnpf/entity/ApiCallLog.java | 62 + .../src/main/java/jnpf/entity/AppBanner.java | 41 + .../entity/AttendanceApprovalSetting.java | 61 + .../jnpf/entity/AttendanceCustomizeTable.java | 79 + .../java/jnpf/entity/AttendanceGroup.java | 151 + .../java/jnpf/entity/AttendanceGroupUser.java | 93 + .../entity/AttendanceManagerPermission.java | 82 + .../src/main/java/jnpf/entity/BaseEntity.java | 41 + .../java/jnpf/entity/BaseInsertEntity.java | 38 + .../main/java/jnpf/entity/CommonConfig.java | 40 + .../main/java/jnpf/entity/ContractEntity.java | 33 + .../main/java/jnpf/entity/Information.java | 66 + .../jnpf/entity/InformationAuthority.java | 66 + .../main/java/jnpf/entity/InformationLog.java | 47 + .../java/jnpf/entity/InformationRubbish.java | 26 + .../entity/InfornationRecentlyViewed.java | 42 + .../java/jnpf/entity/PatrolTaskEntity.java | 50 + .../main/java/jnpf/entity/PermissionDict.java | 94 + .../src/main/java/jnpf/entity/RollImage.java | 46 + .../java/jnpf/entity/SharingSquareVO.java | 9 + .../main/java/jnpf/entity/StoreEntity.java | 110 + .../main/java/jnpf/entity/StoreRegion.java | 86 + .../java/jnpf/entity/StoreUserEntity.java | 70 + .../java/jnpf/entity/StoreUserRelation.java | 42 + .../main/java/jnpf/entity/Workstation.java | 122 + .../java/jnpf/entity/WorkstationUser.java | 74 + .../jnpf/entity/attendance/ApplyParam.java | 82 + .../entity/attendance/AppointPermission.java | 18 + .../attendance/AttendanceAppSetting.java | 78 + .../attendance/AttendanceAppUserSetting.java | 44 + .../attendance/AttendanceApprovalAdminVo.java | 16 + .../AttendanceBalanceRecordEntity.java | 116 + .../AttendanceBalanceUseRecord.java | 73 + .../attendance/AttendanceBaseSetting.java | 153 + .../AttendanceBookConfigEntity.java | 123 + .../AttendanceBookOperationLogEntity.java | 86 + .../AttendanceBookRecordEntity.java | 132 + .../attendance/AttendanceClockInPic.java | 41 + .../attendance/AttendanceClockInResult.java | 82 + .../attendance/AttendanceCloudAlbum.java | 42 + .../attendance/AttendanceCommonSetting.java | 46 + .../entity/attendance/AttendanceConfirm.java | 90 + .../attendance/AttendanceConfirmDetails.java | 280 + .../attendance/AttendanceConfirmSetting.java | 81 + .../attendance/AttendanceDayStatistics.java | 387 ++ .../attendance/AttendanceEnableBalance.java | 63 + .../attendance/AttendanceFestivalRules.java | 93 + .../AttendanceFestivalSettingEntity.java | 189 + .../attendance/AttendanceFieldPersonnel.java | 40 + .../AttendanceFixedClassEntity.java | 77 + .../AttendanceHolidaySettingEntity.java | 176 + .../attendance/AttendanceLateInLateOut.java | 38 + .../AttendanceLeaveGrantSetting.java | 96 + .../attendance/AttendanceLeaveRules.java | 92 + .../attendance/AttendanceLeaveType.java | 36 + .../attendance/AttendanceLocationSetting.java | 102 + .../attendance/AttendanceMachineLog.java | 65 + .../attendance/AttendanceMachineManage.java | 51 + .../attendance/AttendanceMachineSync.java | 58 + .../attendance/AttendanceNoticeEntity.java | 77 + .../attendance/AttendanceOvertimeRule.java | 38 + .../AttendanceOvertimeRuleDetail.java | 60 + .../AttendancePublicHolidayRules.java | 93 + .../AttendanceQuickTemplateEntity.java | 93 + .../AttendanceQuickTemplateItemEntity.java | 50 + .../entity/attendance/AttendanceRepair.java | 77 + .../attendance/AttendanceResultRollback.java | 88 + .../attendance/AttendanceRuleScope.java | 55 + .../attendance/AttendanceSealSetting.java | 47 + .../attendance/AttendanceShiftNameEntity.java | 124 + .../AttendanceShiftSettingEntity.java | 119 + .../AttendanceShiftSettingPeriodEntity.java | 253 + .../attendance/AttendanceStorageRest.java | 41 + .../AttendanceUserBalanceDetail.java | 46 + .../entity/attendance/AttendanceUserFace.java | 47 + .../attendance/AttendanceUserFingerprint.java | 42 + .../attendance/AttendanceUserPhone.java | 38 + .../entity/attendance/DailyRuleChange.java | 47 + .../attendance/FtbAttendanceClockIn.java | 117 + .../attendance/FtbAttendanceDailyRule.java | 441 ++ .../FtbAttendanceFaceChangeLog.java | 49 + .../FtbAttendanceLineSchedulingConfig.java | 147 + ...bAttendanceLineSchedulingPayrollHours.java | 95 + .../FtbScheduleGroupDrawingParamEntity.java | 67 + .../FtbScheduleGroupFixedParamEntity.java | 84 + ...FtbScheduleGroupRevenueStaffingEntity.java | 58 + .../FtbScheduleGroupTimeslotEntity.java | 58 + ...tbScheduleGroupTimeslotStaffingEntity.java | 60 + .../jnpf/entity/attendance/LeaveParam.java | 32 + .../cultivate/CultivateIdentifyApply.java | 187 + .../CultivateIdentifyApplyDetails.java | 48 + .../CultivateIdentifyApplyDetailsBackups.java | 68 + .../CultivateIdentifyApplyTableBackups.java | 101 + .../cultivate/CultivateIdentifyItems.java | 83 + .../cultivate/CultivateIdentifyTable.java | 103 + .../CultivatePositionCourseLogEntity.java | 89 + .../FtbCultivateCourseLearningLogEntity.java | 60 + .../jnpf/entity/cultivate/TeachingRecord.java | 101 + .../jnpf/entity/cultivate/TeachingSkill.java | 59 + .../entity/cultivate/TeachingStudent.java | 92 + .../jnpf/entity/culture/CultureClockIn.java | 52 + .../entity/culture/CultureClockInStat.java | 52 + .../entity/culture/CulturePicSetting.java | 34 + .../jnpf/entity/culture/CulturePicTemp.java | 38 + .../entity/culture/CultureTextSetting.java | 38 + .../entity/qualifications/Qualifications.java | 68 + .../qualifications/QualificationsField.java | 69 + .../QualificationsFieldCategory.java | 56 + .../QualificationsFieldItem.java | 57 + .../QualificationsFieldItemRelation.java | 78 + .../training/TrainingAphorismEntity.java | 83 + .../TrainingAssessmentPointsEntity.java | 30 + .../training/TrainingCertificateEntity.java | 64 + .../TrainingCertificateUserEntity.java | 80 + .../TrainingCourseChapterContentEntity.java | 38 + .../training/TrainingCourseChapterEntity.java | 59 + .../TrainingCourseChapterUserEntity.java | 64 + .../entity/training/TrainingCourseEntity.java | 58 + .../TrainingCourseGainedCommentEntity.java | 62 + .../training/TrainingCourseGainedEntity.java | 55 + .../TrainingCourseGainedLikeEntity.java | 42 + .../TrainingCourseGainedReaderEntity.java | 36 + .../TrainingCourseGainedShareEntity.java | 41 + .../training/TrainingCourseUserEntity.java | 57 + .../TrainingExamDetailOptionEntity.java | 42 + .../entity/training/TrainingExamEntity.java | 110 + .../TrainingExamUserDetailEntity.java | 66 + .../training/TrainingExamUserEntity.java | 86 + .../TrainingGroupBaseSettingEntity.java | 96 + .../training/TrainingGroupClockEntity.java | 73 + .../TrainingGroupCourseCorrEntity.java | 84 + .../training/TrainingGroupDynamicEntity.java | 104 + .../entity/training/TrainingGroupEntity.java | 90 + .../training/TrainingGroupEventEntity.java | 78 + .../training/TrainingGroupScoreEntity.java | 79 + .../training/TrainingGroupUserCorrEntity.java | 94 + .../TrainingIdentifyTemplateEntity.java | 42 + .../TrainingIdentifyTemplateOptionEntity.java | 38 + .../TrainingIdentifyUserDetailEntity.java | 46 + .../training/TrainingIdentifyUserEntity.java | 104 + .../TrainingKnowledgeBaseCourseEntity.java | 35 + .../training/TrainingKnowledgeBaseEntity.java | 43 + .../TrainingKnowledgeBasePositionEntity.java | 38 + .../training/TrainingPositionEntity.java | 46 + .../TrainingPositionLearningDetailEntity.java | 51 + .../TrainingPositionLearningEntity.java | 46 + .../TrainingQuestionAnalysisEntity.java | 33 + .../TrainingQuestionClassifyEntity.java | 29 + .../training/TrainingQuestionEntity.java | 46 + .../TrainingQuestionOptionEntity.java | 50 + .../TrainingQuestionPointsEntity.java | 35 + .../training/TrainingReadingLogEntity.java | 79 + .../training/TrainingTestPaperEntity.java | 47 + .../TrainingTestPaperQuestionEntity.java | 42 + .../training/TrainingTestPaperRuleEntity.java | 51 + .../workflow/ApplyAttendanceChange.java | 103 + .../workflow/ApplyAttendanceOutside.java | 88 + .../workflow/ApplyAttendanceRepair.java | 92 + .../workflow/ApplyAttendanceViolation.java | 83 + .../AttendanceBusinessTripApprove.java | 99 + .../workflow/AttendanceGoOutApprove.java | 90 + .../workflow/AttendanceLeaveApprove.java | 203 + .../AttendanceWorkOvertimeApprove.java | 122 + .../entity/workflow/PunishmentsApproval.java | 121 + .../workflow/PunishmentsApprovalUser.java | 62 + .../jnpf/entity/workflow/RewardApproval.java | 116 + .../entity/workflow/RewardApprovalUser.java | 63 + .../jnpf/entity/workflow/SelfApprove.java | 151 + .../jnpf/enums/AppraisalScoreTypeEnum.java | 63 + .../main/java/jnpf/enums/ClockTypeEnum.java | 13 + .../enums/PermissionSourceCategoryEnum.java | 18 + .../java/jnpf/enums/ReadingStatusEnum.java | 14 + .../java/jnpf/enums/SensitiveTypeEnum.java | 26 + .../java/jnpf/enums/UniAppMpTypeEnum.java | 32 + .../jnpf/enums/attendance/ActionEnum.java | 46 + .../jnpf/enums/attendance/ApplyTypeEnum.java | 40 + .../ApprovalPermissionTypeEnum.java | 22 + .../attendance/ApprovalSettingTypeEnum.java | 48 + .../attendance/AttendanceNoticeEnum.java | 72 + .../attendance/AttendanceOutTypeEnum.java | 132 + .../attendance/AttendanceStatusEnum.java | 174 + .../enums/attendance/AttendanceTypeEnum.java | 55 + .../jnpf/enums/attendance/BalanceEnum.java | 86 + .../jnpf/enums/attendance/CautionEnum.java | 15 + .../enums/attendance/ClockInStatusEnum.java | 56 + .../enums/attendance/CloudAlbumTypeEnum.java | 25 + .../enums/attendance/GroupUserTypeEnum.java | 23 + .../enums/attendance/LeaveTimeTypeEnum.java | 32 + .../jnpf/enums/attendance/LeaveTypeEnum.java | 54 + .../jnpf/enums/attendance/LeaveUnitEnum.java | 60 + .../jnpf/enums/attendance/MachineEnum.java | 43 + .../enums/attendance/NoticeModuleEnum.java | 34 + .../jnpf/enums/attendance/OvertimeType.java | 38 + .../jnpf/enums/attendance/PeriodTypeEnum.java | 63 + .../enums/attendance/PermissionTypeEnum.java | 23 + .../jnpf/enums/attendance/PuncheTypeEnum.java | 46 + .../enums/attendance/ScheduleImportEnum.java | 47 + .../enums/attendance/SchedulesTypeEnum.java | 52 + .../jnpf/enums/attendance/ScopeBizType.java | 47 + .../enums/attendance/SecondmentTypeEnum.java | 18 + .../jnpf/enums/attendance/SettingGroup.java | 34 + .../enums/attendance/StatisticsEnumUtil.java | 546 ++ .../enums/attendance/TriggerSceneEnum.java | 26 + .../enums/attendance/UserSettingEnum.java | 148 + .../enums/attendance/UserSettingTypeEnum.java | 49 + .../enums/attendance/VoucherTypeEnum.java | 33 + .../attendance/v2/ClockOutHandleParam.java | 62 + .../enums/attendance/v2/FuncCodingEnum.java | 42 + .../v2/WorkBoundaryCoverageEnum.java | 57 + .../jnpf/enums/cultivate/ApplyResultEnum.java | 44 + .../jnpf/enums/cultivate/ApplySourceEnum.java | 46 + .../jnpf/enums/cultivate/ApplyStatusEnum.java | 44 + .../jnpf/enums/cultivate/ApplyTypeEnum.java | 43 + .../cultivate/BusinessScenarioTypeEnum.java | 23 + .../cultivate/CultivateIsSystemEnum.java | 68 + .../cultivate/ExamQuestionShuffleEnum.java | 46 + .../cultivate/ExamRetakeFrequencyEnum.java | 49 + .../cultivate/IdentifyItemExceptionEnum.java | 91 + .../cultivate/IdentifyScoreTypeEnum.java | 64 + .../jnpf/enums/cultivate/StudyStatsEnum.java | 47 + .../cultivate/TeachingRecordTypeEnum.java | 16 + .../cultivate/course/CourseTypeEnum.java | 45 + .../task/TaskAssignmentRuleEnum.java | 82 + .../enums/cultivate/task/TaskStateEnum.java | 43 + .../cultivate/v2/LearnTaskStatusEnum.java | 49 + .../cultivate/v2/mq/CultivateTypeEnum.java | 61 + .../enums/memberLog/MemberLogContentEnum.java | 34 + .../jnpf/enums/memberLog/MemberLogImEnum.java | 21 + .../enums/memberLog/MemberLogImJumpEnum.java | 24 + .../main/java/jnpf/enums/memoo/Authority.java | 13 + .../java/jnpf/enums/memoo/DeleteMark.java | 60 + .../main/java/jnpf/enums/memoo/PushState.java | 62 + .../java/jnpf/enums/memoo/QueryTypeEnum.java | 21 + .../java/jnpf/enums/memoo/SelectType.java | 13 + .../main/java/jnpf/enums/memoo/UserDel.java | 59 + .../FtbPersonnelsCheckStatusCodeEnum.java | 24 + .../PersonnelFormDataSystemRosterFields.java | 27 + .../jnpf/enums/personnel/PhoneStatusEnum.java | 31 + .../enums/personnel/SalaryApplyTypeEnum.java | 24 + .../java/jnpf/enums/training/CourseEnums.java | 270 + .../enums/training/GroupEventTypeEnum.java | 14 + .../enums/training/GroupScoreTypeEnum.java | 15 + .../java/jnpf/model/ChapterEndGainedVO.java | 28 + .../main/java/jnpf/model/ClockResultVO.java | 17 + .../main/java/jnpf/model/ContractForm.java | 25 + .../main/java/jnpf/model/ContractInfoVO.java | 26 + .../main/java/jnpf/model/ContractListVO.java | 15 + .../java/jnpf/model/GroupBaseSettingBody.java | 52 + .../main/java/jnpf/model/GroupClockForm.java | 15 + .../main/java/jnpf/model/GroupCommentVO.java | 41 + .../main/java/jnpf/model/GroupDynamicVO.java | 41 + .../java/jnpf/model/GroupEventAddDTO.java | 16 + .../main/java/jnpf/model/GroupGainedVO.java | 56 + .../main/java/jnpf/model/PartUserInfoVo.java | 46 + .../java/jnpf/model/app/IdentifyInfoForm.java | 37 + .../java/jnpf/model/app/IdentifyInfoVO.java | 66 + .../model/app/PositionLearningAppListVO.java | 48 + .../model/app/PositionLearningCourseVO.java | 58 + .../model/app/PositionLearningExamVO.java | 28 + .../app/PositionLearningIdentifyDetailVO.java | 58 + .../model/app/PositionLearningIdentifyVO.java | 20 + .../model/app/TrainingCourseChapterVO.java | 47 + .../attendance/dto/AddFieldPersonnelDto.java | 20 + .../attendance/dto/AppStatisticsListDto.java | 31 + .../attendance/dto/AppStatisticsMoreDto.java | 31 + .../dto/AppStatisticsMoreInfoDto.java | 31 + .../dto/AppStatisticsRecordDto.java | 31 + .../dto/AppStatisticsTeamListDto.java | 33 + .../attendance/dto/AppTeamStatisticsDto.java | 33 + .../dto/AppTeamStatisticsListDto.java | 40 + .../dto/AppTeamStatisticsTabDto.java | 33 + .../dto/AppTeamTabStatisticsDto.java | 31 + .../dto/AppUserSettingQueryDto.java | 29 + .../dto/AttendanceAppUserSettingDto.java | 34 + .../dto/AttendanceApprovalSettingDto.java | 15 + .../dto/AttendanceBaseSettingDto.java | 128 + .../dto/AttendanceBookConfigAddUserDto.java | 29 + .../dto/AttendanceBookConfigDto.java | 66 + .../dto/AttendanceBookConfigQueryDto.java | 71 + .../dto/AttendanceBookRecordDto.java | 81 + .../dto/AttendanceFestivalRulesDto.java | 84 + .../dto/AttendanceFestivalSettingDto.java | 117 + .../dto/AttendanceGroupChargeDto.java | 25 + .../attendance/dto/AttendanceGroupOrgDto.java | 20 + .../dto/AttendanceGroupStatusDto.java | 70 + .../dto/AttendanceHolidaySettingDto.java | 103 + .../dto/AttendanceLeaveGrantSettingDto.java | 76 + .../dto/AttendanceLeaveRulesDto.java | 79 + .../dto/AttendanceLeaveRulesQueryDto.java | 19 + .../dto/AttendanceLeaveSettingsQueryDto.java | 26 + .../dto/AttendanceLocationSettingDto.java | 73 + .../dto/AttendancePublicHolidayRulesDto.java | 84 + .../attendance/dto/AttendanceReqDto.java | 21 + .../attendance/dto/AttendanceSecondedDto.java | 60 + .../attendance/dto/AttendanceShiftDto.java | 41 + .../dto/AttendanceShiftPeriodDto.java | 115 + .../dto/AttendanceUserBalanceDetailDto.java | 30 + .../dto/AttendanceUserBalanceDto.java | 41 + .../AttendanceUserBalanceListQueryDto.java | 30 + .../model/attendance/dto/BalanceQueryDto.java | 114 + .../dto/BatchAttendanceBookRecordDto.java | 27 + .../attendance/dto/BatchSaveGroupAdmin.java | 20 + .../attendance/dto/BookRecordDayListDto.java | 31 + .../dto/BookRecordMonthListDto.java | 31 + .../dto/BookRecordMonthStatisticsDto.java | 75 + .../model/attendance/dto/CancelPhoneDto.java | 23 + .../jnpf/model/attendance/dto/ClockInDto.java | 83 + .../dto/CloudAlbumBatchSaveDto.java | 21 + .../attendance/dto/ConfirmPageListDto.java | 43 + .../dto/ConfirmSettingSubmitDto.java | 44 + .../attendance/dto/ConfirmStatisticsDto.java | 36 + .../dto/DayPayrollStatisticsPageListDto.java | 41 + .../attendance/dto/DayStatisticsDataDto.java | 40 + .../dto/DayStatisticsDataInitDto.java | 32 + .../DayStatisticsDataPageListQueryDto.java | 46 + .../attendance/dto/DayStatisticsDto.java | 35 + .../dto/DayStatisticsExportDto.java | 44 + .../dto/DayStatisticsPageListDto.java | 69 + .../attendance/dto/DurationForOaDto.java | 18 + .../model/attendance/dto/EnableUpdateDto.java | 12 + .../attendance/dto/ExportUserTemplateDto.java | 24 + .../attendance/dto/FaceChangeQueryDto.java | 28 + .../model/attendance/dto/FaceQueryDto.java | 32 + .../model/attendance/dto/FestivalDto.java | 28 + .../attendance/dto/FestivalRulesQueryDto.java | 21 + .../model/attendance/dto/FixedBaseDto.java | 21 + .../dto/FixedClassChangeStatusDto.java | 20 + .../model/attendance/dto/FixedClassDto.java | 26 + .../attendance/dto/FixedClassGroupDto.java | 27 + .../attendance/dto/FixedClassSaveDto.java | 37 + .../attendance/dto/GetConfirmMonthDto.java | 21 + .../attendance/dto/GetValidUsersDto.java | 45 + .../attendance/dto/GoOutApproveForOaDto.java | 46 + .../model/attendance/dto/GrantBalanceDto.java | 30 + .../model/attendance/dto/GroupFilterDto.java | 32 + .../jnpf/model/attendance/dto/GroupOaDto.java | 73 + .../model/attendance/dto/GroupQueryDto.java | 28 + .../attendance/dto/GroupUserQueryDto.java | 59 + .../model/attendance/dto/JoinUserDto.java | 23 + .../dto/LatticeStatisticsVoDto.java | 34 + .../attendance/dto/LeaveApproveForOaDto.java | 165 + .../model/attendance/dto/LeaveQueryDto.java | 90 + .../attendance/dto/LeaveQueryForOaDto.java | 50 + .../attendance/dto/LeaveRemarkSummaryDto.java | 37 + .../attendance/dto/LineDrawingPeriodDto.java | 50 + .../dto/LineDrawingSchedulesConfigDto.java | 34 + .../attendance/dto/LineDrawingUserDto.java | 33 + .../model/attendance/dto/MachineDealDto.java | 39 + .../jnpf/model/attendance/dto/MachineDto.java | 39 + .../model/attendance/dto/MachineQueryDto.java | 30 + .../attendance/dto/MachineUpdateDto.java | 30 + .../dto/MonthAutoSealSettingDto.java | 36 + .../MonthPayrollStatisticsPageListDto.java | 40 + .../attendance/dto/MonthSealPageListDto.java | 34 + .../attendance/dto/MonthSealSubmitDto.java | 27 + .../dto/MonthStatisticsDataQueryDto.java | 46 + .../dto/MonthStatisticsExportDto.java | 43 + .../dto/MonthStatisticsNoticeDto.java | 12 + .../dto/MonthStatisticsPageListDto.java | 60 + .../attendance/dto/MonthUnSealSubmitDto.java | 31 + .../dto/MouthStatisticsDataDto.java | 35 + .../model/attendance/dto/NoApprovalDto.java | 37 + .../attendance/dto/NoticeContentInfoDto.java | 20 + .../model/attendance/dto/NoticeSaveDto.java | 44 + .../attendance/dto/OperationLogQueryDto.java | 51 + .../jnpf/model/attendance/dto/OrgTeamDto.java | 33 + .../attendance/dto/OvertimeRuleDetailDto.java | 44 + .../model/attendance/dto/OvertimeRuleDto.java | 37 + .../attendance/dto/OvertimeRuleQueryDto.java | 19 + .../model/attendance/dto/PeriodConfig.java | 21 + .../dto/PersonnelApiInfoPageListDto.java | 34 + .../PersonnelApiInfoSinglePageListDto.java | 28 + .../attendance/dto/ProcessHistoryDataDto.java | 25 + .../dto/QueryAttendanceGroupDto.java | 10 + .../dto/QueryGroupStatisticsDto.java | 26 + .../dto/QueryStatisticsInfoDto.java | 27 + .../model/attendance/dto/QuickTemDto.java | 21 + .../attendance/dto/QuickTemplateDto.java | 29 + .../dto/SalaryAttendanceSupportDto.java | 32 + .../attendance/dto/SalaryFtbStaticsDto.java | 32 + .../dto/SaveAttendanceGroupDto.java | 27 + .../model/attendance/dto/SaveForStoreDto.java | 23 + .../model/attendance/dto/SaveGroupAdmin.java | 21 + .../attendance/dto/SavePermissionDto.java | 36 + .../attendance/dto/SaveSuperAdminDto.java | 16 + .../attendance/dto/SchedulesDaySetDto.java | 38 + .../attendance/dto/SchedulesImportDto.java | 47 + .../model/attendance/dto/SchedulesSetDto.java | 25 + .../attendance/dto/SchedulesSetItemDto.java | 21 + .../model/attendance/dto/SecondCheckDto.java | 27 + .../attendance/dto/SelfApprovePassForOa.java | 21 + .../model/attendance/dto/ShiftNameDto.java | 52 + .../attendance/dto/ShiftNameQueryDto.java | 24 + .../dto/StatisticsDataQueryDto.java | 36 + .../model/attendance/dto/StorageRestDto.java | 22 + .../dto/TeamMonthStatisticsNoticeDto.java | 12 + .../jnpf/model/attendance/dto/TestDto.java | 15 + .../attendance/dto/UnifiedSchedulesDto.java | 28 + .../model/attendance/dto/UpdateChangeDto.java | 27 + .../attendance/dto/UpdateShiftConfigDto.java | 19 + .../model/attendance/dto/UpdateStoreDto.java | 9 + .../attendance/dto/UserDaySchedulesDto.java | 50 + .../attendance/dto/UserDayShiftQueryDto.java | 39 + .../jnpf/model/attendance/dto/UserDto.java | 25 + .../model/attendance/dto/UserFaceDto.java | 25 + .../jnpf/model/attendance/dto/UserOrgDto.java | 22 + .../model/attendance/dto/UserSortModel.java | 16 + .../attendance/dto/UserWorkSituationDto.java | 28 + .../model/attendance/dto/UsualPhoneDto.java | 28 + .../attendance/dto/UsualPhoneQueryDto.java | 19 + .../attendance/dto/UsualPhoneSettingDto.java | 25 + .../dto/WebStatisticsDetailsDto.java | 36 + .../dto/WebStatisticsExportDto.java | 44 + .../attendance/dto/WebStatisticsListDto.java | 44 + .../attendance/dto/WorkflowImQueryDto.java | 23 + .../attendance/dto/WorkstationQueryDto.java | 40 + .../attendance/dto/WorkstationSaveDto.java | 60 + .../attendance/dto/WorkstationUserAddDto.java | 29 + .../dto/WorkstationUserRemoveDto.java | 27 + .../scheduling/FixedSchedulingRuleDto.java | 63 + .../dto/scheduling/LineSchedulingRuleDto.java | 47 + .../scheduling/PreScheduleDayRevenueDto.java | 31 + .../scheduling/PreScheduleTableQueryDto.java | 46 + .../ScheduleGroupRuleConfigDto.java | 36 + .../event/StatisticsBatchClearDto.java | 48 + .../attendance/event/StatisticsSingleDto.java | 48 + .../event/StatisticsSingleHistoryDto.java | 48 + .../attendance/model/AbsenceCardRecord.java | 85 + .../attendance/model/AbsenceDetailModel.java | 40 + .../model/attendance/model/AbsenceRecord.java | 68 + .../attendance/model/AdminUpdateModel.java | 29 + .../model/AdminUpdateNoticeModel.java | 35 + .../model/AttendanceChangeModel.java | 26 + .../model/AttendanceChangeNoticeModel.java | 38 + .../model/AttendanceGroupParam.java | 12 + .../model/AttendanceNoticeModel.java | 39 + .../model/attendance/model/BalanceRecord.java | 40 + .../attendance/model/BatchNumberResult.java | 32 + .../attendance/model/BeforeClockInModel.java | 24 + .../attendance/model/BusOrOutRecord.java | 34 + .../attendance/model/ClockClassRecord.java | 161 + .../attendance/model/ClockGroupRecord.java | 152 + .../model/attendance/model/ClockInResult.java | 83 + .../attendance/model/ClockInResultRecord.java | 85 + .../model/ClockInResultRecordVo.java | 67 + .../model/attendance/model/ClockInfo.java | 34 + .../model/attendance/model/ClockRecord.java | 54 + .../model/attendance/model/ClockUser.java | 39 + .../model/ConsecUnscheduledModel.java | 22 + .../model/ConsecUnscheduledNoticeModel.java | 15 + .../model/ConsecutiveAbsenceModel.java | 26 + .../model/ConsecutiveAbsenceNoticeModel.java | 43 + .../model/attendance/model/DayClockRange.java | 37 + .../jnpf/model/attendance/model/DayInfo.java | 25 + .../model/DayLeaveTypeJsonData.java | 31 + .../model/DayLeaveTypeJsonPayData.java | 31 + .../attendance/model/DayNoticeModel.java | 104 + .../attendance/model/DaySettlementModel.java | 107 + .../model/DetermineUserClockRecord.java | 25 + .../attendance/model/EarlyLeaveRecord.java | 86 + .../model/FastClockInNoticeModel.java | 34 + .../attendance/model/GroupInfoModel.java | 29 + .../attendance/model/GroupStatistics.java | 64 + .../model/HolidaysTypeJsonData1.java | 31 + .../model/attendance/model/JsonHandler.java | 33 + .../jnpf/model/attendance/model/JumpUrl.java | 65 + .../model/attendance/model/LateRecord.java | 86 + .../attendance/model/LeaveApprovalModel.java | 43 + .../model/LeaveBalanceInfoModel.java | 27 + .../model/attendance/model/LeaveData.java | 31 + .../model/attendance/model/LeaveRecord.java | 35 + .../attendance/model/LeaveSituationData.java | 67 + .../attendance/model/LeaveStatistics.java | 31 + .../model/LeaveStatisticsTotal.java | 45 + .../attendance/model/LeaveTypeJsonData.java | 45 + .../attendance/model/LeaveTypeJsonHead.java | 51 + .../model/LeaveTypeStaDetailsModel.java | 29 + .../attendance/model/LeaveTypeStaModel.java | 48 + .../attendance/model/LeaveTypeTimes.java | 21 + .../model/LineShiftChangeNoticeModel.java | 13 + .../model/attendance/model/LockModel.java | 23 + .../attendance/model/MakeUpCardRecord.java | 40 + .../attendance/model/MonthNoticeModel.java | 67 + .../model/MonthSettlementModel.java | 67 + .../attendance/model/NoClockInModel.java | 26 + .../model/NoClockInNoticeModel.java | 41 + .../model/attendance/model/NoticeConfirm.java | 27 + .../attendance/model/NoticeDetailModel.java | 26 + .../model/OrganizeUserConsumerDTO.java | 25 + .../model/attendance/model/OutworkRecord.java | 40 + .../model/OvertimeBalanceModel.java | 43 + .../model/OvertimeHolidaysInfoModel.java | 27 + .../attendance/model/OvertimeRecord.java | 58 + .../model/attendance/model/PersonDetail.java | 25 + .../model/attendance/model/ProcessResult.java | 41 + .../attendance/model/ProcessTimeResult.java | 23 + .../attendance/model/QuickClockInModel.java | 30 + .../model/RealityAttendanceOneDays.java | 20 + .../model/RemindClockInNoticeModel.java | 35 + .../attendance/model/RuleChangeModel.java | 21 + .../model/RuleChangeNoticeModel.java | 25 + .../model/SalarySupportDayModel.java | 23 + .../model/SalarySupportDayStaModel.java | 32 + .../model/ScheduleImportFailModel.java | 17 + .../attendance/model/ScreenUserIdList.java | 49 + .../attendance/model/SecondApproveModel.java | 10 + .../model/attendance/model/SecondRecord.java | 44 + .../model/SettlementNoticeModel.java | 35 + .../attendance/model/ShiftChangModel.java | 31 + .../model/ShiftChangeNoticeModel.java | 15 + .../model/ShouldAttendDaysData.java | 27 + .../model/attendance/model/SituationData.java | 165 + .../attendance/model/SurplusDaysModel.java | 23 + .../model/SystemNoticeStrategy.java | 35 + .../model/SystemTypeChangeModel.java | 25 + .../model/SystemTypeChangeNoticeModel.java | 15 + .../model/TeamMonthNoticeModel.java | 59 + .../model/TeamMonthSettlementModel.java | 57 + .../jnpf/model/attendance/model/TimeJson.java | 37 + .../model/TurnoverConsumerModel.java | 10 + .../model/attendance/model/UnLockModel.java | 19 + .../model/UserAssociationGroupData.java | 34 + .../attendance/model/UserChangModel.java | 39 + .../model/UserChangeNoticeModel.java | 24 + .../attendance/model/UserClockRecord.java | 51 + .../attendance/model/UserClockTimeSpan.java | 60 + .../attendance/model/UserDayRuleData.java | 66 + .../model/UserDaySituationData.java | 100 + .../model/UserInGroupLeaveInfoData.java | 39 + .../jnpf/model/attendance/model/UserInfo.java | 33 + .../attendance/model/UserLeaveApprove.java | 91 + .../attendance/model/UserLeaveInfoData.java | 17 + .../attendance/model/UserRuleRecord.java | 68 + .../attendance/model/UserSecondRecord.java | 23 + .../model/UserShouldAttendDays.java | 15 + .../attendance/model/UserStaDayRuleData.java | 32 + .../attendance/model/UserStatistics.java | 187 + .../attendance/model/WebExportModel.java | 145 + .../model/WebExportNewDetailsModel.java | 128 + .../attendance/model/WebExportNewModel.java | 134 + .../attendance/vo/AbnormalClockInVo.java | 50 + .../model/attendance/vo/AbsenceClockInVo.java | 52 + .../attendance/vo/AppStatisticsListVo.java | 47 + .../vo/AppStatisticsMoreInfoVo.java | 61 + .../attendance/vo/AppStatisticsMoreVo.java | 88 + .../attendance/vo/AppStatisticsRecordVo.java | 37 + .../vo/AppStatisticsTeamListVo.java | 38 + .../vo/AppStatisticsTeamTabListVo.java | 68 + .../attendance/vo/AppStatisticsTeamTabVo.java | 28 + .../vo/AppTeamStatisticsListVo.java | 39 + .../attendance/vo/AppTeamStatisticsVo.java | 71 + .../model/attendance/vo/ApplyResultVo.java | 24 + .../attendance/vo/ApprovalGroupQueryDto.java | 22 + .../model/attendance/vo/ApprovalQueryDto.java | 24 + .../vo/AttendanceBalanceRecordVo.java | 101 + .../vo/AttendanceBaseSettingVo.java | 136 + .../attendance/vo/AttendanceBookConfigVo.java | 107 + .../vo/AttendanceFestivalSettingVo.java | 114 + .../attendance/vo/AttendanceGroupAdminVo.java | 23 + .../vo/AttendanceGroupChargeVo.java | 21 + .../attendance/vo/AttendanceGroupInfoVo.java | 10 + .../attendance/vo/AttendanceGroupUserVo.java | 50 + .../attendance/vo/AttendanceGroupVo.java | 76 + .../vo/AttendanceHolidaySettingVo.java | 107 + .../vo/AttendanceLeaveApproveVo.java | 118 + .../vo/AttendanceLeaveSettingsVo.java | 75 + .../vo/AttendanceLocationSettingVo.java | 70 + .../vo/AttendanceMachineManageVo.java | 53 + .../vo/AttendanceManagerDetailVo.java | 15 + .../attendance/vo/AttendanceOrgOrGroupVo.java | 27 + .../attendance/vo/AttendancePermissionVo.java | 15 + .../vo/AttendanceQuickTemplateItemVo.java | 44 + .../vo/AttendanceQuickTemplateVo.java | 50 + .../vo/AttendanceResultOptionGroupVo.java | 32 + .../vo/AttendanceResultOptionVo.java | 33 + .../model/attendance/vo/AttendanceRuleVo.java | 142 + .../vo/AttendanceSelfApproveVo.java | 64 + .../vo/AttendanceShiftSettingPeriodVo.java | 244 + .../vo/AttendanceShiftSettingVo.java | 64 + .../model/attendance/vo/AttendanceUserVo.java | 19 + .../vo/AttendanceWorkOverTimeVo.java | 43 + .../attendance/vo/BatchOperationResultVo.java | 69 + .../attendance/vo/BookConfigCreatorVo.java | 39 + .../model/attendance/vo/BookPersonnelVo.java | 63 + .../vo/BookRecordDailyLatticeVo.java | 64 + .../attendance/vo/BookRecordDayListVo.java | 53 + .../model/attendance/vo/BookRecordDayVo.java | 57 + .../attendance/vo/BookRecordMonthDayVo.java | 48 + .../attendance/vo/BookRecordMonthListVo.java | 53 + .../vo/BookRecordMonthStatisticsVo.java | 117 + .../model/attendance/vo/BookScopeUserVo.java | 64 + .../model/attendance/vo/ChangeClockInVo.java | 44 + .../model/attendance/vo/ChangeInfoVo.java | 45 + .../jnpf/model/attendance/vo/ChangeLogVo.java | 41 + .../model/attendance/vo/ClassesDetailVo.java | 61 + .../model/attendance/vo/ClockInMethodVo.java | 27 + .../model/attendance/vo/ClockInPicVo.java | 22 + .../jnpf/model/attendance/vo/ClockInVo.java | 131 + .../jnpf/model/attendance/vo/ConditionVo.java | 35 + .../attendance/vo/CreateDayStatistics.java | 27 + .../attendance/vo/CurUserPermissionVo.java | 22 + .../model/attendance/vo/DailyApprovalVo.java | 57 + .../model/attendance/vo/DailyBaseInfoVo.java | 37 + .../attendance/vo/DailyClockInDetailVo.java | 26 + .../model/attendance/vo/DailyClockInVo.java | 101 + .../jnpf/model/attendance/vo/DailyInfoVo.java | 24 + .../attendance/vo/DailyRuleResultVo.java | 173 + .../jnpf/model/attendance/vo/DailyRuleVo.java | 36 + .../model/attendance/vo/DailyTotalDataVo.java | 18 + .../attendance/vo/DayReceivableRevenueVo.java | 26 + .../attendance/vo/DayShiftRevenueStatVo.java | 34 + .../jnpf/model/attendance/vo/FaceMiniVo.java | 28 + .../model/attendance/vo/GroupApprovalVo.java | 27 + .../vo/GroupAttendanceStaticsVo.java | 107 + .../jnpf/model/attendance/vo/GroupInfoVo.java | 58 + .../jnpf/model/attendance/vo/GroupNodeVo.java | 32 + .../model/attendance/vo/GroupRepairVo.java | 36 + .../jnpf/model/attendance/vo/GroupRuleVo.java | 60 + .../vo/GroupUserLineScheduleVo.java | 32 + .../model/attendance/vo/HolidayOptionVo.java | 65 + .../model/attendance/vo/HolidayTypeVo.java | 32 + .../jnpf/model/attendance/vo/KeyValueVo.java | 20 + .../attendance/vo/LeaveRemarkItemVo.java | 43 + .../attendance/vo/LeaveRemarkSummaryVo.java | 122 + .../model/attendance/vo/LeaveShiftVo.java | 39 + .../jnpf/model/attendance/vo/LeaveStaVo.java | 39 + .../attendance/vo/LeaveTypeStatisticVo.java | 26 + .../attendance/vo/LeaveTypeStatisticsVo.java | 35 + .../model/attendance/vo/LineSchedulesVo.java | 74 + .../model/attendance/vo/MachineScopeVo.java | 41 + .../jnpf/model/attendance/vo/MethodVo.java | 50 + .../jnpf/model/attendance/vo/MiniGroupVo.java | 36 + .../model/attendance/vo/MockVideoInfoVo.java | 23 + .../model/attendance/vo/MonthRecordVo.java | 30 + .../jnpf/model/attendance/vo/NextRuleVo.java | 22 + .../attendance/vo/NoticeConfirmListVo.java | 28 + .../model/attendance/vo/NoticeConfirmVo.java | 46 + .../attendance/vo/NoticeContentInfoVo.java | 35 + .../model/attendance/vo/OldDayStatistics.java | 19 + .../attendance/vo/OperationLogPageVo.java | 81 + .../attendance/vo/OvertimeVouchersVo.java | 29 + .../model/attendance/vo/PeriodNameVO.java | 60 + .../model/attendance/vo/PermissionDictVo.java | 24 + .../attendance/vo/QueryGroupAdminVo.java | 11 + .../model/attendance/vo/QuickPeriodsVo.java | 29 + .../model/attendance/vo/RemindClockInVo.java | 52 + .../model/attendance/vo/RepairRuleVo.java | 33 + .../model/attendance/vo/RuleAndClockVo.java | 67 + .../jnpf/model/attendance/vo/RuleDayVo.java | 22 + .../jnpf/model/attendance/vo/RuleUserVo.java | 22 + .../attendance/vo/SalaryFtbStaticsVo.java | 140 + .../attendance/vo/ScheduleRuleDetailVo.java | 41 + .../model/attendance/vo/SchedulesDayVo.java | 40 + .../model/attendance/vo/SchedulesItemVo.java | 89 + .../model/attendance/vo/SchedulesV2Vo.java | 77 + .../jnpf/model/attendance/vo/SchedulesVo.java | 64 + .../model/attendance/vo/SecondmentDateVo.java | 18 + .../attendance/vo/ShiftAndLeaveRules.java | 31 + .../model/attendance/vo/ShiftPeriodVo.java | 59 + .../attendance/vo/ShiftPostHeadcountVo.java | 26 + .../model/attendance/vo/ShiftPostStatVo.java | 48 + .../java/jnpf/model/attendance/vo/TestVo.java | 15 + .../vo/UnifiedSchedulesResultVo.java | 32 + .../vo/UserAttendanceStaticsVo.java | 23 + .../model/attendance/vo/UserBalanceVo.java | 57 + .../model/attendance/vo/UserClassesVo.java | 27 + .../attendance/vo/UserDayShiftInfoVo.java | 159 + .../jnpf/model/attendance/vo/UserDayVo.java | 31 + .../model/attendance/vo/UserFaceDetailVo.java | 52 + .../jnpf/model/attendance/vo/UserFaceVo.java | 35 + .../attendance/vo/UserInGroupInfoVo.java | 21 + .../model/attendance/vo/UserSettingVo.java | 96 + .../jnpf/model/attendance/vo/VacationVo.java | 57 + .../attendance/vo/WebStatisticsListVo.java | 153 + .../attendance/vo/WorkstationDetailVo.java | 76 + .../attendance/vo/WorkstationGroupUserVo.java | 38 + .../attendance/vo/WorkstationUserVo.java | 48 + .../model/attendance/vo/WorkstationVo.java | 65 + .../attendance/vo/WorkstationWithUsersVo.java | 35 + .../vo/attendance/ApproveBaseImVo.java | 99 + .../attendance/vo/attendance/ApproveImVo.java | 39 + .../vo/attendance/AttendanceApproveImVo.java | 31 + .../AttendanceCustomizeTableVo.java | 58 + .../attendance/AttendanceDimensionDayVo.java | 28 + .../vo/attendance/AttendanceDimensionVo.java | 25 + .../attendance/AttendanceFestivalRulesVo.java | 90 + .../vo/attendance/AttendanceInfoVo.java | 25 + .../AttendanceLeaveGrantSettingVo.java | 74 + .../vo/attendance/AttendanceLeaveRulesVo.java | 104 + .../AttendancePublicHolidayBalance.java | 57 + .../AttendancePublicHolidayRulesVo.java | 92 + .../attendance/AttendanceStorageRestVo.java | 23 + .../AttendanceToThousandsFacesVo.java | 30 + .../AttendanceUserBalanceDetailVo.java | 47 + .../AttendanceUserBalanceListDataVo.java | 58 + .../AttendanceUserBalanceListVo.java | 34 + .../attendance/AttendanceUserBalanceVo.java | 41 + .../vo/attendance/AttendanceUserTitleVo.java | 53 + .../vo/attendance/AverageTrendPageListVo.java | 43 + .../vo/attendance/BalanceTitelVo.java | 35 + .../vo/attendance/BalanceUseRecordVo.java | 33 + .../vo/attendance/BaseConfirmInfo.java | 29 + .../vo/attendance/BusinessTripVo.java | 36 + .../vo/attendance/ClockDataReqVo.java | 22 + .../vo/attendance/ClockInExportVo.java | 127 + .../vo/attendance/ClockRecordVo.java | 38 + .../vo/attendance/ConfirmDetails.java | 29 + .../vo/attendance/ConfirmDetailsInfo.java | 128 + .../vo/attendance/ConfirmDetailsInfoNew.java | 89 + .../vo/attendance/ConfirmDetailsVo.java | 47 + .../vo/attendance/ConfirmListQueryVo.java | 33 + .../vo/attendance/ConfirmPageListVo.java | 49 + .../vo/attendance/ConfirmSettingInfoVo.java | 33 + .../vo/attendance/ConfirmStatisticsVo.java | 33 + .../vo/attendance/CustomizeTableUpdateVo.java | 27 + .../attendance/DailySituationPageListVo.java | 109 + .../DayPayrollStatisticsPageListVo.java | 146 + .../DayPayrollStatisticsQueryVo.java | 74 + .../vo/attendance/DayStatisticsDataVo.java | 28 + .../DayStatisticsNoticeQueryVo.java | 106 + .../attendance/DayStatisticsPageListVo.java | 229 + .../vo/attendance/DayStatisticsQueryDbVo.java | 41 + .../vo/attendance/DayStatisticsQueryVo.java | 144 + .../attendance/DayStatisticsShiftsJsonVo.java | 61 + .../vo/attendance/DayStatisticsVo.java | 21 + .../ExceptionSituationPageListVo.java | 73 + .../vo/attendance/FieldPersonnelVo.java | 28 + .../vo/attendance/FixedClassShiftVo.java | 28 + .../FullAttendanceStatusPageListVo.java | 55 + .../attendance/vo/attendance/GoOutVo.java | 29 + .../vo/attendance/GroupCheckVo.java | 22 + .../attendance/vo/attendance/GroupLockVo.java | 25 + .../attendance/vo/attendance/GroupMiniVo.java | 20 + .../vo/attendance/GroupShiftTimeVo.java | 38 + .../vo/attendance/GroupUserMiniVo.java | 26 + .../attendance/vo/attendance/JoinGroupVo.java | 20 + .../vo/attendance/LatticeStatisticsVo.java | 91 + .../attendance/LeaveConsumptionDetailVo.java | 40 + .../vo/attendance/LeaveDeductDetailVo.java | 30 + .../LeaveDeductWagesJsonDetailVo.java | 30 + .../vo/attendance/LeaveDetailVo.java | 26 + .../vo/attendance/LeaveRuleBalanceVo.java | 29 + .../vo/attendance/LeaveRuleFormulaVo.java | 22 + .../attendance/vo/attendance/LogMiniVo.java | 22 + .../vo/attendance/MonthAutoSealSettingVo.java | 32 + .../MonthConfirmStatisticsListVo.java | 130 + .../MonthPayrollStatisticsPageListVo.java | 122 + .../MonthPayrollStatisticsQueryVo.java | 74 + .../vo/attendance/MonthSealPageListVo.java | 41 + .../MonthStatisticsNoticeQueryVo.java | 72 + .../MonthStatisticsPageListExportVo.java | 303 + .../attendance/MonthStatisticsPageListVo.java | 229 + .../vo/attendance/MonthStatisticsQueryVo.java | 191 + .../MonthStatsAbnormalConditionQueryVo.java | 33 + .../MonthStatsDailySituationQueryVo.java | 59 + .../attendance/MonthStatsDetailsQueryVo.java | 39 + .../MonthStatsFullSituationQueryVo.java | 27 + .../MonthStatsHoursRankingQueryVo.java | 23 + .../MonthStatsOvertimeSituationQueryVo.java | 24 + .../MonthStatsPerCapitaQueryVo.java | 24 + .../vo/attendance/OutOrBusApproveVo.java | 73 + .../vo/attendance/OvertimeInfoVo.java | 31 + .../vo/attendance/OvertimeRuleDetailVo.java | 49 + .../vo/attendance/OvertimeRulePageVo.java | 36 + .../vo/attendance/OvertimeRuleVo.java | 47 + .../attendance/OvertimeSalaryHoursJsonVo.java | 28 + .../vo/attendance/OvertimeSalaryHoursVo.java | 32 + .../OvertimeSituationPageListVo.java | 55 + .../attendance/PersonnelTrendPageListVo.java | 45 + .../PublicHolidayTransferListVo.java | 28 + .../attendance/vo/attendance/PunchesVo.java | 24 + .../vo/attendance/QueryGroupStatisticsVo.java | 16 + .../vo/attendance/QueryStatisticsInfoVo.java | 45 + .../QueryUserOvertimeDetailsDbVo.java | 32 + .../QueryUserOvertimeDetailsVo.java | 32 + .../vo/attendance/QueryUserOvertimeVo.java | 32 + .../vo/attendance/QuickCheckInVo.java | 20 + .../attendance/vo/attendance/QuickTemVo.java | 28 + .../attendance/vo/attendance/RemarkVo.java | 21 + .../SalaryAttendanceSupportQuery.java | 205 + .../attendance/SalaryAttendanceSupportVo.java | 267 + .../vo/attendance/SealUserInfoDataVo.java | 21 + .../vo/attendance/SecondedUserVo.java | 16 + .../vo/attendance/ShiftNameListVo.java | 34 + .../vo/attendance/ShiftNameViewVo.java | 59 + .../attendance/vo/attendance/ShiftNameVo.java | 60 + .../vo/attendance/ShiftRotationVo.java | 24 + .../attendance/ShiftsJsonClockInResultVo.java | 47 + .../vo/attendance/StatisticsDataQueryVo.java | 62 + .../TeamMonthStatisticsNoticeQueryVo.java | 59 + .../attendance/vo/attendance/TimeVo.java | 15 + .../vo/attendance/UserConfigVo.java | 21 + .../vo/attendance/UserDailySealVo.java | 28 + .../vo/attendance/UserPermissions.java | 18 + .../vo/attendance/UserRuleListVo.java | 85 + .../vo/attendance/UserTenantVo.java | 29 + .../vo/attendance/UserWorkSituationDbVo.java | 32 + .../vo/attendance/UserWorkSituationVo.java | 28 + .../vo/attendance/UsualPhonePageVo.java | 41 + .../WorkHoursRankingPageListVo.java | 49 + .../vo/attendance/YesterdayRuleVo.java | 53 + .../attendance/vo/event/OvertimeEvent.java | 26 + .../model/attendance/vo/flow/FlowTaskVo.java | 32 + .../model/attendance/vo/flow/HandlerVo.java | 9 + .../vo/permission/ActionPermissionVo.java | 28 + .../vo/permission/ApprovalSettingVo.java | 9 + .../vo/permission/AttendanceTeamSetVo.java | 29 + .../vo/permission/ConfigPermissionVo.java | 21 + .../permission/app/ManagerPermissionVo.java | 13 + .../vo/scheduling/FixedSchedulingRuleVo.java | 63 + .../vo/scheduling/LineSchedulingRuleVo.java | 47 + .../vo/scheduling/PreSchedulePostVo.java | 31 + .../vo/scheduling/PreScheduleShiftPostVo.java | 34 + .../scheduling/PreScheduleShiftStaffVo.java | 24 + .../vo/scheduling/PreScheduleShiftVo.java | 31 + .../vo/scheduling/PreScheduleStationVo.java | 26 + .../vo/scheduling/PreScheduleTableRowVo.java | 49 + .../vo/scheduling/PreScheduleTableVo.java | 30 + .../vo/scheduling/PreScheduleUserVo.java | 21 + .../scheduling/ScheduleGroupRuleConfigVo.java | 33 + .../authorize/FtbPermissionAuthorizeVO.java | 21 + .../menu/FtbPermissionFunctionMenuDTO.java | 64 + .../menu/FtbPermissionMenuDirectoryDTO.java | 24 + .../menu/FtbPermissionMigrareRoleAddDTO.java | 13 + .../dto/menu/FtbPermissionRoleAddDTO.java | 123 + .../dto/menu/FunctionMenuRemoteDTO.java | 16 + .../authority/dto/menu/FuntionMenuDTO.java | 23 + .../AuthGetTargetUserInfoBatchDTO.java | 17 + .../permission/OrganizeWithPositionsDTO.java | 17 + .../FtbPermissionRoleBatchDeleteDTO.java | 17 + .../person/FtbPermissionRolePersonAddDTO.java | 23 + .../FtbPermissionRolePersonRelationDTO.java | 21 + .../authority/dto/person/FunctionDTO.java | 40 + .../dto/post/FtbPermissionRolePostAddDTO.java | 21 + .../dto/role/FtbPermissionDataDTO.java | 52 + ...FtbPermissionPositionAuthorizationDTO.java | 72 + .../role/FtbPermissionPositionMenuDTO.java | 24 + .../dto/role/FtbPermissionRoleCopyDTO.java | 18 + .../dto/role/FtbPermissionRoleInfoDTO.java | 22 + .../dto/role/FtbPermissionRoleUpdateDTO.java | 41 + .../po/FtbPermissionFunctionMenu.java | 62 + .../model/authority/po/FtbPermissionRole.java | 44 + .../po/FtbPermissionRoleAuthorizePerson.java | 43 + .../po/FtbPermissionRoleAuthorizePost.java | 44 + .../authority/po/FtbPermissionRoleMenu.java | 86 + .../po/FtbPermissionRoleMenuRelation.java | 47 + .../FtbPermissionRolePersonUserRelation.java | 63 + .../vo/menu/FtbPermissionFunctionMenuVO.java | 52 + .../vo/menu/FtbPermissionMenuConfigTree.java | 48 + .../menu/FtbPermissionMenuConfigTreeVO.java | 43 + .../vo/menu/FtbPermissionMenuDirectoryVO.java | 42 + .../vo/menu/FtbPermissionMigrateRoleTree.java | 31 + .../menu/FtbPermissionMigrateRoleTreeVO.java | 35 + .../vo/menu/FtbPermissionModuleInfoVO.java | 22 + .../vo/menu/FtbPermissionModuleVO.java | 38 + .../vo/person/FtbAuthorizedPersonnelVO.java | 56 + .../FtbEmployeePermissionPersonnelVO.java | 65 + .../vo/person/FtbRoleListDropDownVO.java | 30 + .../vo/person/FtbUserPermissionVO.java | 18 + .../model/authority/vo/person/FunctionVO.java | 45 + .../vo/post/FtbAuthorizedPostVO.java | 52 + .../vo/role/FtbPermissionPositionMenuVO.java | 69 + .../vo/role/FtbPermissionRoleDetailsVO.java | 48 + .../FtbPermissionRoleIdentificationVO.java | 20 + .../vo/role/FtbPermissionRoleInfoVO.java | 46 + .../vo/role/FtbPermissionRolePersonVO.java | 44 + .../certificate/dto/EmployeeOrganizeDTO.java | 20 + .../dto/HealthCertificateStatusDTO.java | 13 + .../jnpf/model/certificate/dto/OptionDTO.java | 13 + .../CertificateBusinessLicenseExtEntity.java | 67 + .../CertificateHygieneLicenseExtEntity.java | 42 + .../po/CertificateInstanceEntity.java | 76 + .../po/CertificateInstanceItemEntity.java | 66 + .../po/CertificateWarningLogEntity.java | 89 + .../req/CertificateFoodSafetyOcrReq.java | 23 + .../req/CertificateHealthManageQueryReq.java | 77 + .../req/CertificateInstanceAddReq.java | 78 + .../req/CertificateInstanceItemReq.java | 50 + .../req/CertificateInstanceQueryReq.java | 70 + .../req/CertificateInstanceUpdateReq.java | 22 + .../req/CertificateStoreDashboardReq.java | 42 + .../req/CertificateStoreManageQueryReq.java | 54 + .../req/CertificateStoreSaveReq.java | 44 + .../req/CertificateSyncHealthReq.java | 26 + .../req/app/CertificateAppBatchRemindReq.java | 29 + ...ertificateAppBusinessLicenseUpdateReq.java | 89 + .../CertificateAppEmployeeRiskQueryReq.java | 19 + ...tificateAppHealthCertificateUpdateReq.java | 39 + ...CertificateAppHygieneLicenseUpdateReq.java | 46 + .../req/app/CertificateAppRiskChartReq.java | 25 + .../app/CertificateAppSingleRemindReq.java | 24 + .../CertificateAppStoreCustomUpdateReq.java | 45 + .../app/CertificateAppStoreRiskQueryReq.java | 30 + .../vo/CertificateFoodSafetyOcrVO.java | 42 + ...ertificateHealthDashboardStatusStatVO.java | 30 + .../vo/CertificateHealthManageVO.java | 92 + .../vo/CertificateInstanceItemVO.java | 61 + .../certificate/vo/CertificateInstanceVO.java | 81 + .../CertificateOrganizeBusinessLicenseVO.java | 114 + .../vo/CertificateStoreAndCertificatesVO.java | 35 + .../vo/CertificateStoreCustomStatusBarVO.java | 49 + .../vo/CertificateStoreCustomStatusPieVO.java | 32 + .../CertificateStoreCustomStatusTableVO.java | 95 + .../vo/CertificateStoreDashboardVO.java | 48 + .../vo/CertificateStoreManageVO.java | 87 + .../certificate/vo/CertificateStoreTabVO.java | 35 + .../vo/CertificateTypeOptionVO.java | 24 + ...CertificateAppBusinessLicenseDetailVO.java | 71 + .../CertificateAppCertificateDetailVO.java | 40 + .../vo/app/CertificateAppEmployeeRiskVO.java | 78 + .../CertificateAppHygieneLicenseDetailVO.java | 41 + .../vo/app/CertificateAppRiskChartVO.java | 23 + .../CertificateAppRiskReminderCountVO.java | 25 + .../vo/app/CertificateAppStatusCountVO.java | 14 + .../CertificateAppStoreCustomDetailVO.java | 33 + .../vo/app/CertificateAppStoreRiskItemVO.java | 52 + .../vo/app/CertificateAppStoreRiskVO.java | 57 + .../vo/app/HealthCertificateDetailVO.java | 90 + .../vo/app/HealthCertificateVO.java | 81 + .../main/java/jnpf/model/common/CheckVo.java | 21 + .../java/jnpf/model/common/DateRangeDto.java | 51 + .../java/jnpf/model/common/DateRangeDto1.java | 22 + .../main/java/jnpf/model/common/PageDto.java | 21 + .../jnpf/model/cultivate/CultivatePage.java | 244 + .../cultivate/bo/FtbCultivateCourseBO.java | 15 + .../bo/FtbCultivatePositionExamBO.java | 23 + .../model/cultivate/bo/TriggerEventBO.java | 32 + .../FtbCultivateCommonSettingGlobalDTO.java | 47 + .../FtbCultivateCourseSettingGlobalDTO.java | 73 + ...bCultivatePromotionPostApplyCreateDto.java | 140 + .../FtbCultivatePromotionPostApplyDto.java | 59 + ...FtbCultivateBaseOrgWisdomStatisticDTO.java | 44 + ...CultivateBasePersonWisdomStatisticDTO.java | 43 + .../FtbCultivateCaseBaseAuditDTO.java | 22 + .../dto/casebase/FtbCultivateCaseBaseDTO.java | 27 + .../app/FtbCultivateCaseBaseCreatDTO.java | 50 + .../dto/certificate/FtbCertificateForm.java | 56 + .../FtbCertificateOrgWisdomStatisticDTO.java | 23 + ...tbCertificatePersonWisdomStatisticDTO.java | 42 + .../certificate/FtbCertificateUserForm.java | 47 + .../dto/course/FtbCommonKeyAndValDto.java | 9 + .../course/FtbCultivateCourseChapterDTO.java | 132 + .../FtbCultivateCourseChapterUpdateDTO.java | 19 + .../dto/course/FtbCultivateCourseDTO.java | 117 + .../FtbCultivateCourseOrgStatisticsDTO.java | 33 + ...tbCultivateCoursePersonStatisticesDTO.java | 29 + .../course/FtbCultivateCourseQueryDTO.java | 53 + .../course/FtbCultivateCourseSettingDTO.java | 82 + .../dto/course/FtbCultivateCourseTypeDTO.java | 32 + .../FtbCultivateCourseTypeUpdateDTO.java | 26 + .../course/FtbCultivateCourseUpdateDTO.java | 73 + .../course/FtbCultivateSelectPositionDto.java | 17 + .../dto/course/FtbCultivateShelvesDTO.java | 29 + .../dto/course/app/FtbChapterStudyDTO.java | 52 + .../app/FtbCultivateCourseMsgForAppDTO.java | 38 + .../app/FtbGlobalCurriculumAppWrapDTO.java | 16 + ...ManuallyTriggerExamsQualificationsDTO.java | 20 + .../app/FtbUserLevelMessageReadDTO.java | 16 + .../FtbCultivateChapterTestDTO.java | 140 + .../FtbCultivateChapterTestOptionDTO.java | 60 + .../FtbCultivateCoursePackageAddDTO.java | 54 + .../FtbCultivateCoursePackageQueryDTO.java | 13 + .../FtbCultivateCoursePackageUpdateDTO.java | 60 + .../dto/gained/FtbAppAttentionPage.java | 30 + .../dto/gained/FtbCommentPagination.java | 27 + .../dto/gained/FtbGainedPagination.java | 29 + .../dto/gained/FtbGainedQueryDto.java | 29 + .../dto/gained/FtbLikePagination.java | 17 + .../dto/gained/FtbShareInterestEntity.java | 96 + .../identify/ApplyDataResultNotifyDto.java | 19 + .../identify/BatchIdentifyApplySaveDto.java | 46 + .../FtbIdentityOrgWisdomStatisticDTO.java | 17 + .../FtbIdentityPersonWisdomStatisticDTO.java | 46 + .../identify/IdentifyApplyDataPushDto.java | 60 + .../identify/IdentifyApplyDeleteAppDto.java | 15 + .../identify/IdentifyApplyItemsSubmitDto.java | 25 + .../dto/identify/IdentifyApplyListAppDto.java | 23 + .../dto/identify/IdentifyApplyListDto.java | 44 + .../identify/IdentifyApplyReIdentifyDto.java | 14 + .../dto/identify/IdentifyApplySaveDto.java | 68 + .../identify/IdentifyApplySetTimeAppDto.java | 23 + .../dto/identify/IdentifyApplySubmitDto.java | 41 + .../dto/identify/IdentifyTableInfoDto.java | 12 + .../dto/identify/IdentifyTableListDto.java | 12 + .../dto/identify/IdentifyTableSaveDto.java | 57 + .../dto/identify/IdentifyTableUpdateDto.java | 62 + .../dto/identify/IdentifyUserInfoDto.java | 38 + .../dto/identify/UserIdentifyPageDto.java | 16 + .../dto/learn/BatchCommonCountDto.java | 15 + .../learn/FtbCultivateLearnAllocationDTO.java | 25 + .../learn/FtbCultivateLearnCategoriesDto.java | 54 + ...bCultivateLearnTaskCertificateInfoDto.java | 35 + .../FtbCultivateLearnTaskCourseInfoDto.java | 35 + .../FtbCultivateLearnTaskExamInfoDto.java | 33 + ...ltivateLearnTaskIdentificationInfoDto.java | 37 + .../learn/FtbCultivateLearnTaskInfoDto.java | 268 + .../FtbCultivateLearnTaskInfoExprotDto.java | 19 + .../learn/FtbCultivateLearnTaskListDto.java | 29 + .../cultivate/dto/learn/NeedAlertUserDto.java | 25 + .../dto/learn/NeedPerDayAlertDto.java | 25 + .../offline/FtbCultivateOfflineFileDTO.java | 51 + .../FtbCultivateOfflineTrainChangeDTO.java | 42 + .../offline/FtbCultivateOfflineTrainDTO.java | 148 + ...ltivateOfflineTrainPeopleSigningInDTO.java | 22 + .../FtbCultivateOfflineTrainSignInDTO.java | 23 + .../FtbCultivateOfflineTrainUpdateDTO.java | 29 + .../FtbModifyOfflineTrainingMembersDTO.java | 28 + .../position/FtbCourseDropDownListDTO.java | 24 + .../FtbCultivatePersonStatisticesDTO.java | 16 + ...tbCultivatePositionCourseLevelPageDTO.java | 19 + .../FtbCultivatePositionCoursePageDTO.java | 25 + .../FtbCultivatePositionExamIdentifyDTO.java | 29 + ...ltivatePositionJobLearnCertificateDTO.java | 45 + .../FtbCultivatePositionJobLearnDTO.java | 27 + .../FtbCultivatePositionJobLearnExamDTO.java | 45 + ...FtbCultivatePositionLearingCourselDTO.java | 48 + ...FtbCultivatePositionLearnPracticalDTO.java | 47 + ...FtbCultivatePositionOrgStatisticesDTO.java | 18 + ...CultivatePositionPersonStatisticesDTO.java | 28 + .../FtbCultivatePositionShelvesDTO.java | 29 + .../dto/position/FtbJobLearningPaginDTO.java | 15 + .../position/FtbReselectionAppraisalDTO.java | 33 + .../dto/position/FtbRetakeExamDTO.java | 31 + .../position/FtbStudyCourseExamAddDTO.java | 54 + .../FtbStudyCourseIdentityAddDTO.java | 53 + .../app/FtbCultivatePositionForAppDTO.java | 55 + .../app/FtbCultivatePositionForAppNewDTO.java | 34 + ...FtbCultivateAssociatedExamsCoursesDTO.java | 35 + .../web/FtbCultivatePositionCopyDTO.java | 22 + ...ePositionJobLearnCourseCertificateDTO.java | 50 + ...ePositionJobLearnCourseStateSwitchDTO.java | 38 + ...tbCultivatePositionPassExaminationDTO.java | 22 + ...FtbJobLearnCourseRetakeCertificateDTO.java | 32 + .../FtbCultivateCreatMeMapInfoDTO.java | 98 + .../FtbCultivatePromotionCreatDto.java | 52 + .../FtbCultivatePromotionCreatNewDto.java | 67 + .../promotion/FtbCultivatePromotionDto.java | 35 + .../FtbCultivatePromotionForAppDto.java | 64 + .../FtbCultivatePromotionMemberDto.java | 61 + .../FtbMapsOrgWisdomStatisticDTO.java | 41 + .../FtbMapsPersonWisdomStatisticDTO.java | 46 + .../dto/rule/FtbCultivateRuleDto.java | 20 + .../CultivatePermissionModuleParam.java | 11 + .../CultivateStatisticsCommonParam.java | 12 + .../statistics/CultivateUserExamCountDTO.java | 30 + .../statistics/ExamStatisticsForOrgDTO.java | 40 + .../ExamStatisticsForPersonDTO.java | 70 + .../statistics/FtbCultivateStatisticsDTO.java | 51 + .../InnerCultivateStatisticsDTO.java | 27 + .../InnerExamStatisticsForOrgDTO.java | 18 + .../InnerExamStatisticsForPersonDTO.java | 23 + .../FtbCultivateStoreStatisticsReq.java | 13 + .../dto/storestatistics/StoreIdentityReq.java | 17 + .../dto/storestatistics/StoreMyTaskReq.java | 17 + .../storestatistics/StoreOfflineTrainReq.java | 17 + .../StoreStatisticsCommonReq.java | 23 + .../StoreStatisticsMyExamReq.java | 16 + .../StoreStatisticsWaitMyCheckExamReq.java | 16 + .../dto/StoreCultivateTaskDto.java | 62 + .../dto/StoreOfflineTrainDto.java | 22 + .../dto/teaching/EmployeePageListDto.java | 46 + .../dto/teaching/MyRecordPageListDto.java | 45 + .../dto/teaching/QueryTeachingRecordDto.java | 37 + .../dto/teaching/RecordBatchDeleteDto.java | 21 + .../dto/teaching/RecordQueryDto.java | 21 + .../cultivate/dto/teaching/RecordSaveDto.java | 79 + .../dto/teaching/RecordUpdateDto.java | 70 + .../dto/teaching/TeachingApproveDto.java | 38 + .../dto/teaching/TeachingBaseFilter.java | 39 + .../dto/teaching/TeachingSaveDto.java | 55 + .../dto/teaching/TeachingSkillAddDto.java | 32 + .../dto/teaching/TeachingSkillPageDto.java | 23 + .../dto/teaching/TeachingSkillSortDto.java | 25 + .../dto/teaching/TeachingSkillUpdateDto.java | 39 + .../cultivate/dto/teaching/ViewDataDto.java | 42 + .../model/cultivate/entiy/BaseEntity.java | 77 + .../cultivate/event/JnpfApplicationEvent.java | 22 + .../event/dto/CourseTriggerEventDTO.java | 30 + .../dto/certificate/CertificateEventDTO.java | 48 + .../course/CommonCourseProcessLearnDTO.java | 30 + .../event/dto/course/CourseEventDTO.java | 31 + .../event/dto/course/CourseProcessLearn.java | 66 + .../dto/course/PositionCourseEventDTO.java | 57 + .../ExamIdentifyTriggerEventDTO.java | 41 + .../event/dto/file/FileEventDTO.java | 89 + .../position/PositionCourseProcessDTO.java | 46 + .../event/dto/task/TaskCourseProcessDTO.java | 45 + .../po/FtbCultivateAssessmentPoints.java | 54 + .../po/FtbCultivateCommonSettingGlobal.java | 39 + .../po/FtbCultivateCourseSettingGlobal.java | 70 + .../model/cultivate/po/FtbCultivateFile.java | 71 + .../po/FtbCultivateIdentifyCategories.java | 44 + .../po/FtbCultivateIdentifyItemsPool.java | 68 + ...FtbCultivatePositionCourseCertificate.java | 47 + .../model/cultivate/po/FtbCultivateRule.java | 44 + .../apply/FtbCultivatePromotionPostApply.java | 133 + .../po/casebase/FtbCultivateCaseBase.java | 63 + .../po/casebase/FtbCultivateCaseBaseLike.java | 41 + .../CertificateUserPagination.java | 20 + .../po/certificate/FtbCertificateEntity.java | 83 + .../FtbCertificateImagesEntity.java | 54 + .../certificate/FtbCertificateUserEntity.java | 87 + .../FtbCultivateCoverCategoryEntity.java | 41 + .../common/FtbCultivateCoverInfoEntity.java | 47 + .../course/FtbCultivateCertificateResult.java | 88 + .../po/course/FtbCultivateChapterTest.java | 65 + .../course/FtbCultivateChapterTestOption.java | 58 + .../course/FtbCultivateChapterTestResult.java | 65 + .../po/course/FtbCultivateCourse.java | 128 + .../po/course/FtbCultivateCourseChapter.java | 118 + .../po/course/FtbCultivateCourseSetting.java | 71 + .../course/FtbCultivateCourseTriggerLog.java | 46 + .../po/course/FtbCultivateCourseType.java | 33 + .../po/course/app/CultivateCourseMsg.java | 48 + .../po/course/app/CultivateCourseMsgUser.java | 35 + .../FtbCultivateCoursePackage.java | 51 + .../FtbCultivatePackageCourse.java | 45 + .../cultivate/po/exam/FtbCultivateExam.java | 307 + .../po/exam/FtbCultivateExamFrequencyLog.java | 39 + .../po/exam/FtbCultivateExamHistoryPaper.java | 300 + .../po/exam/FtbCultivateExamPaper.java | 121 + .../exam/FtbCultivateExamSettingEntity.java | 88 + .../po/exam/FtbCultivateExamUser.java | 332 + .../po/exam/FtbCultivateExamUserDetail.java | 131 + .../po/exam/InnerAnalysisOrgInfoDto.java | 14 + .../po/gained/FtbAppAttentionListVO.java | 42 + .../gained/FtbCourseGainedCommentEntity.java | 60 + .../po/gained/FtbCourseGainedEntity.java | 58 + .../po/gained/FtbCourseGainedLikeEntity.java | 38 + .../gained/FtbCourseGainedReaderEntity.java | 33 + .../po/gained/FtbCourseGainedShareEntity.java | 38 + .../cultivate/po/label/FtbCultivateLabel.java | 43 + .../po/learn/FtbCultivateLearnCategories.java | 43 + .../po/learn/FtbCultivateLearnTask.java | 156 + .../FtbCultivateLearnTaskAssignment.java | 61 + .../FtbCultivateLearnTaskCertificate.java | 49 + .../po/learn/FtbCultivateLearnTaskCourse.java | 54 + .../po/learn/FtbCultivateLearnTaskExam.java | 57 + .../FtbCultivateLearnTaskIdentification.java | 52 + .../FtbCultivateLearnTaskReminderRule.java | 50 + .../po/mesgg/CultivateMessageInfo.java | 41 + .../po/offline/FtbCultivateOfflineCourse.java | 25 + .../po/offline/FtbCultivateOfflineTrain.java | 111 + .../po/offline/FtbCultivateOfflineUser.java | 52 + .../po/org/FtbCultivatePositionGradeVO.java | 18 + .../po/org/FtbPositionGradesInfoBoundVO.java | 28 + .../po/paper/FtbCultivateTestPaper.java | 124 + .../paper/FtbCultivateTestPaperQuestion.java | 74 + .../po/paper/FtbCultivateTestPaperRule.java | 253 + .../po/position/FtbCultivatePosition.java | 50 + .../FtbCultivatePositionCertificate.java | 59 + ...ultivatePositionCourceChapterLearning.java | 71 + .../FtbCultivatePositionCourceLearning.java | 51 + .../position/FtbCultivatePositionCourse.java | 54 + .../FtbCultivatePositionCourseExam.java | 50 + .../FtbCultivatePositionCourseIdentity.java | 49 + .../FtbCultivatePositionCoursePractice.java | 57 + .../po/position/FtbCultivatePositionExam.java | 37 + .../FtbCultivatePositionExamIdentify.java | 38 + .../FtbCultivatePositionIdentifyResult.java | 59 + .../po/position/FtbCultivatePositionLog.java | 53 + .../position/FtbCultivatePositionSetting.java | 40 + .../po/position/FtbCultivatePositionUser.java | 55 + .../po/promotion/FtbCultivatePromotion.java | 39 + .../promotion/FtbCultivatePromotionLog.java | 47 + .../FtbCultivatePromotionMember.java | 75 + .../FtbCultivatePromotionMemberNew.java | 76 + .../promotion/FtbCultivatePromotionNew.java | 74 + .../FtbCultivatePromotionNewMessage.java | 23 + .../promotion/FtbCultivatePromotionPost.java | 42 + .../FtbCultivatePromotionPostNew.java | 72 + .../FtbCultivatePromotionSetting.java | 47 + .../promotion/FtbCultivatePromotionUser.java | 47 + .../po/question/FtbCultivateQuestion.java | 77 + .../FtbCultivateQuestionAnalysis.java | 28 + .../po/question/FtbCultivateQuestionBank.java | 55 + .../FtbCultivateQuestionBankCourse.java | 58 + .../question/FtbCultivateQuestionOption.java | 100 + .../question/FtbCultivateQuestionPoints.java | 39 + .../po/task/FtbCultivateLearnTaskPhase.java | 62 + .../task/FtbCultivateLearnTaskPractice.java | 58 + .../po/task/FtbCultivateTaskLog.java | 113 + .../cultivate/po/teaching/CultivateFile.java | 53 + .../po/teaching/CultivateUserView.java | 43 + .../po/teaching/TeachingApprove.java | 48 + .../req/exam/AppQueryExamQuestionReq.java | 13 + .../req/exam/AssessmentPointsReq.java | 22 + .../cultivate/req/exam/ExamStatisticsReq.java | 30 + .../req/exam/QueryAssessmentPointsReq.java | 15 + .../req/exam/QueryCultivateExamReq.java | 20 + .../req/exam/QueryExamForPostReq.java | 26 + .../cultivate/req/exam/QueryExamListReq.java | 23 + .../req/exam/QueryExamRankListReq.java | 14 + .../cultivate/req/exam/QueryExamReq.java | 31 + .../cultivate/req/exam/QueryExamUserReq.java | 28 + .../req/exam/QueryExpireExamListReq.java | 23 + .../req/exam/QueryExpireListReq.java | 21 + .../req/exam/QueryMyCompleteExamListReq.java | 18 + .../req/exam/QueryMyExamListReq.java | 22 + .../req/exam/QueryWaitMyExamListReq.java | 18 + .../cultivate/req/exam/ReadOverExamReq.java | 33 + .../req/exam/ReviewerAppointDto.java | 44 + .../model/cultivate/req/exam/SaveExamReq.java | 198 + .../cultivate/req/exam/StartExamReq.java | 28 + .../req/exam/SubExamQuestionReq.java | 23 + .../model/cultivate/req/exam/SubExamReq.java | 27 + .../req/exam/WebHistoryQueryExamUserReq.java | 20 + .../req/exam/WebQueryExamReadOverReq.java | 14 + .../cultivate/req/exam/WebRestartExamReq.java | 35 + .../req/learn/AddLearnCategoryReq.java | 21 + .../req/learn/QueryLearnCategoryListReq.java | 22 + .../req/learn/QueryLearnTaskCountListReq.java | 28 + .../req/learn/QueryLearnTaskListReq.java | 32 + .../req/learn/UpdateLearnCategoryReq.java | 8 + .../cultivate/req/paper/PaperConfigReq.java | 65 + .../req/paper/PostAndPositionReq.java | 19 + .../req/paper/PreQuestionImportReq.java | 18 + .../cultivate/req/paper/QueryPaperReq.java | 32 + .../cultivate/req/paper/SavePaperReq.java | 70 + .../req/questionbank/AddQuestionBankReq.java | 18 + .../req/questionbank/AddQuestionReq.java | 51 + .../req/questionbank/EditQuestionBankReq.java | 24 + .../req/questionbank/EditQuestionReq.java | 53 + .../questionbank/QueryQuestionBankReq.java | 16 + .../req/questionbank/QueryQuestionReq.java | 27 + .../req/questionbank/QuestionOptionReq.java | 67 + .../questionbank/UnbindQuestionBankReq.java | 24 + .../model/cultivate/resp/AppExamListVo.java | 201 + .../model/cultivate/resp/AppExamUserVo.java | 25 + .../jnpf/model/cultivate/resp/AppExamVo.java | 98 + .../model/cultivate/resp/AppPromotionVO.java | 13 + .../model/cultivate/resp/AppQuestionVo.java | 43 + .../cultivate/resp/AssessmentPointsVo.java | 18 + .../model/cultivate/resp/CanDeleteMsg.java | 39 + .../model/cultivate/resp/CompleteExamVo.java | 22 + .../cultivate/resp/DeleteAlertExamVo.java | 42 + .../cultivate/resp/ExamAndPaperDetailVo.java | 107 + .../jnpf/model/cultivate/resp/ExamAppVo.java | 169 + .../model/cultivate/resp/ExamBaseUser.java | 22 + .../cultivate/resp/ExamCultivateCountVo.java | 29 + .../resp/ExamCultivateForPositionVo.java | 32 + .../model/cultivate/resp/ExamDetailVo.java | 185 + .../model/cultivate/resp/ExamListUserVo.java | 93 + .../jnpf/model/cultivate/resp/ExamListVo.java | 158 + .../model/cultivate/resp/ExamPaperVo.java | 40 + .../cultivate/resp/ExamQuestionBakVo.java | 37 + .../resp/ExamStatisticsForOrgExcelVo.java | 121 + .../resp/ExamStatisticsForOrgVo.java | 103 + .../resp/ExamStatisticsForPersonExcelVo.java | 130 + .../resp/ExamStatisticsForPersonVo.java | 111 + .../cultivate/resp/ExamStatisticsVo.java | 69 + .../jnpf/model/cultivate/resp/ExamUserVo.java | 114 + .../jnpf/model/cultivate/resp/ExamVo.java | 246 + .../resp/ExcelImportQuestionResultReq.java | 59 + .../cultivate/resp/ExcelUserQuestionVo.java | 87 + .../resp/GeneralPaperQuestionVo.java | 20 + .../resp/ImportQuestionResultVo.java | 27 + .../model/cultivate/resp/InnerExamDto.java | 23 + .../resp/InnerQueryExamResultDto.java | 15 + .../model/cultivate/resp/MyBlowExamNum.java | 24 + .../model/cultivate/resp/MyExamListVo.java | 82 + .../model/cultivate/resp/PaperDetailVo.java | 120 + .../model/cultivate/resp/PaperDrawRuleVo.java | 39 + .../model/cultivate/resp/PaperListVo.java | 85 + .../model/cultivate/resp/PaperQuestionVo.java | 26 + .../jnpf/model/cultivate/resp/PaperVo.java | 86 + .../model/cultivate/resp/PositionExamDto.java | 164 + .../model/cultivate/resp/PostAndPosition.java | 46 + .../cultivate/resp/PreQuestionImportVO.java | 38 + .../cultivate/resp/QueryUserExamListDto.java | 20 + .../model/cultivate/resp/QuestionBankVo.java | 62 + .../cultivate/resp/QuestionCanDeleteMsg.java | 44 + .../cultivate/resp/QuestionCountDto.java | 29 + .../cultivate/resp/QuestionOptionVo.java | 60 + .../jnpf/model/cultivate/resp/QuestionVo.java | 79 + .../cultivate/resp/RecordPaperAndExam.java | 39 + .../model/cultivate/resp/ReviewerRole.java | 27 + .../cultivate/resp/StatisticsResultDto.java | 37 + .../StatisticsResultFirstAndRepeatDto.java | 28 + .../jnpf/model/cultivate/resp/SubExamVo.java | 16 + .../resp/TaskRelationCertificateVo.java | 25 + .../cultivate/resp/TaskRelationExamVo.java | 25 + .../resp/TaskRelationIdentificationVo.java | 28 + .../model/cultivate/resp/TriggerExamDto.java | 41 + .../model/cultivate/resp/UserExamCount.java | 43 + .../cultivate/resp/UserExamDetailVo.java | 257 + .../resp/UserOrganizationItemVo.java | 37 + .../cultivate/resp/UserOrganizationVo.java | 24 + .../model/cultivate/resp/UserPaperVo.java | 72 + .../model/cultivate/resp/UserQuestionVo.java | 83 + .../model/cultivate/resp/UserRankingVo.java | 30 + .../cultivate/resp/WaitReadOverNumVo.java | 15 + .../resp/WebReadOverExamAndPaperDetailVo.java | 123 + .../v2/apply/req/V2IdentifyApplyItemReq.java | 36 + .../apply/req/V2IdentifyApplyListAppReq.java | 44 + .../v2/apply/req/V2IdentifyApplyListReq.java | 46 + .../v2/apply/req/V2IdentifyApplySaveReq.java | 52 + .../apply/req/V2IdentifyApplySubmitReq.java | 41 + .../req/V2MyIdentifyApplyListAppReq.java | 18 + .../apply/req/V2MyIdentifyApplyListReq.java | 68 + .../cultivate/v2/apply/vo/IdentifyDataVo.java | 29 + .../vo/V2IdentifyApplyAppBasicInfoVo.java | 132 + .../apply/vo/V2IdentifyApplyBasicInfoVo.java | 60 + .../v2/apply/vo/V2IdentifyApplyInfoVo.java | 85 + .../v2/apply/vo/V2IdentifyApplyItemVo.java | 138 + .../v2/apply/vo/V2IdentifyApplyListAppVo.java | 89 + .../v2/apply/vo/V2IdentifyApplyListVo.java | 88 + .../v2/apply/vo/V2IdentifyApplySaveDto.java | 68 + .../req/DeleteCertificateImageReq.java | 23 + .../certificate/req/V2SaveCertificateReq.java | 19 + .../v2/common/req/V2BatchByPrimaryIdReq.java | 20 + .../common/req/V2SaveCommonCategoryReq.java | 17 + .../v2/common/vo/V2BaseIdNameVo.java | 25 + .../cultivate/v2/common/vo/V2ImportReq.java | 18 + .../cultivate/v2/common/vo/V2PreImportVo.java | 48 + .../course/vo/AiHelperCourseStatisticsVo.java | 25 + .../v2/course/vo/UserLearningStatusVo.java | 131 + .../vo/app/AppCommonCourseSimpleVo.java | 47 + .../v2/course/vo/app/AppCourseSimpleVo.java | 62 + .../vo/app/AppCultivateCourseExamVo.java | 98 + .../vo/app/AppCultivateCourseIdentityVo.java | 90 + .../v2/course/vo/app/CommonCourseCountVo.java | 20 + .../v2/course/vo/app/LastStudyCourseVo.java | 57 + .../vo/app/NextUserAppIdentityTmpVo.java | 23 + .../vo/app/OtherPositionCourseCountVo.java | 20 + .../vo/app/PositionLearningCourseVo.java | 30 + .../v2/course/vo/app/V2ChapterAppDetails.java | 180 + .../v2/course/vo/app/V2ChapterStudyVo.java | 50 + .../v2/course/vo/app/V2ChapterVo.java | 53 + .../v2/course/vo/app/V2CourseAppDto.java | 25 + .../course/vo/app/V2CourseDetailsAppVO.java | 85 + .../course/vo/app/V2CourseOutlineAppVo.java | 145 + .../vo/app/V2CultivateChapterTestAddDTO.java | 15 + .../vo/app/V2CultivateChapterTestDto.java | 21 + .../app/V2CultivateChapterTestOptionDto.java | 12 + .../vo/app/V2CultivateCourseMsgForAppDTO.java | 19 + .../web/req/V2CultivateCourseChapterReq.java | 143 + .../web/req/V2CultivateCourseListReq.java | 26 + .../course/web/req/V2CultivateCourseReq.java | 146 + .../web/req/V2CultivateCourseSelectReq.java | 18 + .../web/req/V2NextUserAllMapListReq.java | 12 + .../req/V2NextUserCultivateCourseListReq.java | 20 + .../web/vo/V2CultivateCourseChapterVo.java | 122 + .../web/vo/V2CultivateCourseDetailsVo.java | 83 + .../web/vo/V2CultivateCoursePageVo.java | 55 + .../web/vo/V2CultivateCourseSelectVo.java | 43 + .../V2InnerCultivateChapterStatisticsDto.java | 30 + ...2InnerCultivateCourseOrgStatisticsDto.java | 49 + .../model/cultivate/v2/enums/ApproveEnum.java | 40 + .../cultivate/v2/enums/ExamStatusEnum.java | 43 + .../cultivate/v2/enums/FrequencyEnum.java | 46 + .../cultivate/v2/enums/MyExamStatusEnum.java | 49 + .../v2/enums/PositionBusinessSourceEnum.java | 47 + .../cultivate/v2/enums/QuestionTypeEnum.java | 52 + .../cultivate/v2/exam/po/CultivateExam.java | 111 + .../v2/exam/po/CultivateExamDrawRule.java | 61 + .../cultivate/v2/exam/req/ExamRankReq.java | 24 + .../v2/exam/req/MarkDetailQueryReq.java | 24 + .../v2/exam/req/MyExamWebQueryDto.java | 22 + .../v2/exam/req/QueryReadOverExamListReq.java | 24 + .../cultivate/v2/exam/req/QuestionReq.java | 22 + .../model/cultivate/v2/exam/req/SkillDto.java | 21 + .../cultivate/v2/exam/req/SkillImportReq.java | 21 + .../v2/exam/req/V2AppQueryExamListReq.java | 16 + .../v2/exam/req/V2ExamSettingReq.java | 68 + .../exam/req/V2ExamStatisticsForOrgReq.java | 39 + .../req/V2ExamStatisticsForPersonReq.java | 72 + .../v2/exam/req/V2QueryCoverReq.java | 16 + .../v2/exam/req/V2QueryExamRecordReq.java | 14 + .../cultivate/v2/exam/req/V2QueryExamReq.java | 22 + .../v2/exam/req/V2RandomDrawRuleReq.java | 44 + .../v2/exam/req/V2ReadOverExamReq.java | 42 + .../v2/exam/req/V2RestartExamReq.java | 32 + .../v2/exam/req/V2SaveCoverCategoryReq.java | 23 + .../cultivate/v2/exam/req/V2SaveCoverReq.java | 24 + .../cultivate/v2/exam/req/V2SaveExamReq.java | 79 + .../v2/exam/req/V2SubmitExamReq.java | 31 + .../v2/exam/vo/AiHelperExamStatisticsVo.java | 32 + .../cultivate/v2/exam/vo/BankAnalysisVo.java | 27 + .../cultivate/v2/exam/vo/BankPickVo.java | 42 + .../v2/exam/vo/ConnectDrawRuleVo.java | 33 + .../cultivate/v2/exam/vo/ErrorObjectVo.java | 18 + .../cultivate/v2/exam/vo/ExamDrawRuleVo.java | 45 + .../cultivate/v2/exam/vo/ExamResultVo.java | 54 + .../cultivate/v2/exam/vo/ImportObjectVo.java | 27 + .../cultivate/v2/exam/vo/MyExamDetailVo.java | 83 + .../v2/exam/vo/MyExamTabCountVo.java | 26 + .../model/cultivate/v2/exam/vo/MyExamVo.java | 43 + .../cultivate/v2/exam/vo/MyExamWebVo.java | 53 + .../cultivate/v2/exam/vo/PaperDetailVo.java | 56 + .../v2/exam/vo/QuestionTypeAnalysisVo.java | 28 + .../cultivate/v2/exam/vo/QuestionVo.java | 35 + .../cultivate/v2/exam/vo/ReadOverExamVo.java | 48 + .../v2/exam/vo/ReadOverTabCountVo.java | 22 + .../v2/exam/vo/SkillUploadInfoVo.java | 30 + .../model/cultivate/v2/exam/vo/SkillVo.java | 24 + .../cultivate/v2/exam/vo/UserExamRankVo.java | 22 + .../cultivate/v2/exam/vo/V2AppExamListVo.java | 79 + .../v2/exam/vo/V2AppExamRankingVo.java | 27 + .../v2/exam/vo/V2AppMyExamDetailVo.java | 112 + .../cultivate/v2/exam/vo/V2AppQuestionVo.java | 48 + .../v2/exam/vo/V2AppReadOverExamDetailVo.java | 133 + .../model/cultivate/v2/exam/vo/V2CoverVo.java | 55 + .../cultivate/v2/exam/vo/V2ExamDetailVo.java | 63 + .../cultivate/v2/exam/vo/V2ExamListVo.java | 42 + .../vo/V2ExamStatisticsForOrgExcelVo.java | 104 + .../v2/exam/vo/V2ExamStatisticsForOrgVo.java | 103 + .../vo/V2ExamStatisticsForPersonExcelVo.java | 120 + .../exam/vo/V2ExamStatisticsForPersonVo.java | 140 + .../cultivate/v2/exam/vo/V2ExamUserVo.java | 66 + .../model/cultivate/v2/exam/vo/V2ExamVo.java | 70 + .../v2/exam/vo/V2PostAndPosition.java | 36 + .../v2/exam/vo/V2QuestionOptionVo.java | 33 + .../cultivate/v2/exam/vo/V2TestPaperVo.java | 57 + .../cultivate/v2/exam/vo/V2TreeCoverVo.java | 42 + .../v2/exam/vo/V2UserExamDetailVo.java | 63 + .../v2/exam/vo/V2UserQuestionVo.java | 71 + .../gained/req/V2AppCommentPageListReq.java | 14 + .../gained/vo/V2AppCourseGainedCommentVO.java | 40 + .../vo/V2SimpleCourseGainedCommentVO.java | 18 + .../req/QueryIdentifyCategoryListReq.java | 11 + .../req/V2IdentifyItemsImportSaveReq.java | 57 + .../identify/req/V2IdentifyItemsSaveReq.java | 58 + .../req/V2IdentifyTableImportSaveReq.java | 83 + .../identify/req/V2IdentifyTableListReq.java | 23 + .../identify/req/V2IdentifyTableSaveReq.java | 90 + .../identify/req/V2QueryIdentifyItemReq.java | 17 + .../identify/req/V2SaveIdentifyItemReq.java | 42 + .../identify/req/V2SaveItemCategoryReq.java | 24 + .../v2/identify/vo/ExcelTestDemoVO.java | 77 + .../v2/identify/vo/V2CateIdentifyTableVo.java | 24 + .../identify/vo/V2IdentifyCategoryItemVo.java | 54 + .../identify/vo/V2IdentifyCategoryTreeVo.java | 41 + .../v2/identify/vo/V2IdentifyCategoryVo.java | 22 + .../identify/vo/V2IdentifyItemsExportVo.java | 22 + .../v2/identify/vo/V2IdentifyItemsInfoVo.java | 67 + .../identify/vo/V2IdentifyScoreConfigVo.java | 40 + .../v2/identify/vo/V2IdentifyTableInfoVo.java | 84 + .../v2/identify/vo/V2IdentifyTableListVo.java | 60 + .../vo/V2IdentifyTableScoreConfigVo.java | 26 + .../v2/identify/vo/V2PreIdentifyErrorVo.java | 31 + .../v2/identify/vo/V2PreIdentifyImportVo.java | 33 + .../item_pool/req/IdentifyItemsPoolReq.java | 22 + .../SaveCultivateIdentifyItemsPoolReq.java | 85 + .../vo/FtbCultivateIdentifyItemsPoolVo.java | 70 + .../vo/V2IdentifyItemExceptionVo.java | 64 + .../v2/item_pool/vo/V2IdentifyItemVo.java | 79 + .../v2/label/dto/FtbCultivateLabelReq.java | 20 + .../label/dto/FtbCultivateUpdateLabelReq.java | 17 + .../v2/label/vo/FtbCultivateLabelVo.java | 40 + .../model/cultivate/v2/mq/CultivateMqDTO.java | 91 + .../V2CultivateOfflineTrainUpdateDTO.java | 34 + ...CultivatePositionCourseCertificateReq.java | 20 + .../FtbCultivatePositionCourseExamReq.java | 21 + ...FtbCultivatePositionCourseIdentityReq.java | 18 + ...FtbCultivatePositionCoursePracticeReq.java | 24 + .../req/FtbCultivatePositionCourseReq.java | 67 + .../req/FtbCultivatePositionSaveReq.java | 89 + .../req/FtbCultivatePositionSettingReq.java | 28 + .../req/V2CultivateCommonCourseForAppReq.java | 19 + .../req/V2CultivateCoursePageReq.java | 23 + .../V2CultivatePositionCourseForAppReq.java | 26 + .../v2/position/req/V2InnerPositionDto.java | 22 + .../V2MyCultivateCommonCourseForAppReq.java | 15 + ...OtherCultivatePositionCourseForAppReq.java | 20 + ...pCultivatePositionCourseCertificateVo.java | 55 + .../vo/AppCultivatePositionCourseExamVo.java | 23 + .../AppCultivatePositionCourseIdentityVo.java | 25 + .../AppCultivatePositionCoursePracticeVo.java | 64 + .../vo/AppCultivatePositionCourseVo.java | 103 + .../vo/AppCultivatePositionDetailVo.java | 113 + .../v2/position/vo/AppPracticeCountVo.java | 28 + .../v2/position/vo/BusinessSourceVo.java | 16 + .../position/vo/CheckPostAndGradeResult.java | 24 + .../v2/position/vo/CourseSimpleVo.java | 35 + .../vo/CultivatePositionCourseVo.java | 33 + .../vo/CultivatePositionSimpleVo.java | 39 + .../vo/CultivateSimpleUserInfoVo.java | 73 + .../position/vo/CultivateUserPositionVo.java | 37 + ...bCultivatePositionCourseCertificateVo.java | 39 + ...tePositionCourseCertificateWithNameVo.java | 53 + .../vo/FtbCultivatePositionCourseExamVo.java | 63 + ...CultivatePositionCourseExamWithNameVo.java | 53 + .../FtbCultivatePositionCourseIdentityVo.java | 58 + ...ivatePositionCourseIdentityWithNameVo.java | 53 + .../FtbCultivatePositionCoursePracticeVo.java | 48 + ...ivatePositionCoursePracticeWithNameVo.java | 58 + .../vo/FtbCultivatePositionCourseVo.java | 91 + .../FtbCultivatePositionCourseWithNameVo.java | 77 + .../vo/FtbCultivatePositionSettingVo.java | 29 + .../v2/position/vo/PersonForLeaderVo.java | 12 + .../position/vo/PositionStatusResultVo.java | 16 + .../vo/V2AllCultivatePositionCourseExam.java | 30 + .../vo/V2CultivateJobLearnCourseVo.java | 52 + .../vo/V2CultivatePositionDetailForApp.java | 36 + .../v2/position/vo/V2StudyCountVo.java | 14 + .../position/vo/WebCultivatePositionView.java | 52 + .../position/vo/WebCultivatePositionVo.java | 86 + .../vo/WebPositionLearningListVo.java | 50 + .../dto/PersonStatisticsDataDto.java | 23 + .../req/FtbCultivatePromotionReq.java | 22 + .../req/V2CultivatePositionExcludeReq.java | 15 + .../req/V2CultivatePromotionCreateReq.java | 96 + .../V2CultivatePromotionPhaseStateForApp.java | 27 + ...ultivatePromotionPositionDetailForApp.java | 32 + .../req/V2CultivatePromotionPostReq.java | 37 + .../req/V2CultivatePromotionScopeReq.java | 25 + ...V2CultivatePromotionSelectPositionReq.java | 26 + .../req/V2PromotionOrgStatisticReq.java | 39 + .../req/V2PromotionPersonStatisticReq.java | 38 + .../req/V2WebCultivateStudyMemberListReq.java | 17 + ...AppCultivatePromotionPositionDetailVo.java | 20 + .../vo/MyCultivatePromotionListVo.java | 64 + .../vo/NextUserCultivatePromotionVo.java | 65 + .../v2/promotion/vo/PositionProgressVo.java | 24 + .../v2/promotion/vo/PromotionAndPostVo.java | 32 + .../v2/promotion/vo/PromotionLevelInfoVo.java | 137 + .../vo/PromotionPhasePositionVo.java | 24 + .../v2/promotion/vo/PromotionPhaseVo.java | 28 + .../promotion/vo/UserPromotionDetailVo.java | 35 + .../vo/V2CultivatePositionGradeVo.java | 18 + .../promotion/vo/V2CultivatePositionVo.java | 23 + .../vo/V2CultivatePostAndGradeVo.java | 36 + .../vo/V2CultivatePostAndGradeVoExt.java | 11 + .../vo/V2CultivatePromotionErrorVo.java | 16 + .../vo/V2CultivatePromotionLevel.java | 23 + .../vo/V2CultivatePromotionMemberVo.java | 55 + .../V2CultivatePromotionOrgStatisticVo.java | 69 + ...V2CultivatePromotionPersonStatisticVo.java | 94 + .../vo/V2CultivatePromotionPostNewVO.java | 64 + .../vo/V2CultivatePromotionScopeVo.java | 29 + .../promotion/vo/V2CultivatePromotionVo.java | 54 + .../vo/V2WebCultivateStudyMemberListVo.java | 31 + .../vo/WebCultivatePromotionListVo.java | 52 + .../v2/question/req/BatchAddQuestionReq.java | 17 + .../v2/question/req/V2AddQuestionReq.java | 68 + .../v2/question/req/V2EditQuestionReq.java | 53 + .../req/V2ExcelImportQuestionResultReq.java | 59 + .../req/V2ImportQuestionResultVo.java | 25 + .../question/req/V2PreQuestionImportVO.java | 38 + .../v2/statistics/V2BatchQueryResult.java | 37 + .../v2/statistics/V2CultivateLineNumDto.java | 15 + .../v2/statistics/V2UserCourseStudyVo.java | 27 + .../V2UserLearningStatusResult.java | 35 + .../CheckUserTaskAllPhasesCompleteReq.java | 21 + ...V2CultivateLearnTaskListForManagerReq.java | 20 + .../v2/task/req/V2CultivateTaskCountReq.java | 42 + .../v2/task/req/V2CultivateTaskSaveReq.java | 300 + .../task/req/V2MyCultivateTaskListForReq.java | 20 + .../vo/CheckUserTaskAllPhasesCompleteVo.java | 92 + .../FtbCultivateLearnTaskCertificateVo.java | 56 + .../vo/FtbCultivateLearnTaskCourseVo.java | 51 + .../task/vo/FtbCultivateLearnTaskExamVo.java | 68 + ...FtbCultivateLearnTaskIdentificationVo.java | 79 + .../vo/FtbCultivateLearnTaskPracticeVo.java | 52 + .../v2/task/vo/PhaseExamStatusVo.java | 19 + .../task/vo/PhaseIdentificationStatusVo.java | 19 + .../v2/task/vo/PhaseStatusResultVo.java | 36 + .../vo/V2CultivateLearnTaskCertificateVo.java | 35 + .../task/vo/V2CultivateLearnTaskCourseVo.java | 60 + .../task/vo/V2CultivateLearnTaskExamVo.java | 19 + ...2CultivateLearnTaskFinishStatisticsVo.java | 34 + .../V2CultivateLearnTaskIdentificationVo.java | 19 + .../task/vo/V2CultivateLearnTaskPhaseVo.java | 86 + .../vo/V2CultivateLearnTaskPracticeVo.java | 54 + .../vo/V2CultivateLearnTaskSimplePhaseVo.java | 66 + .../task/vo/V2CultivateTaskCountExportVo.java | 71 + .../v2/task/vo/V2CultivateTaskCountVo.java | 116 + .../v2/task/vo/V2CultivateTaskDetailsVo.java | 158 + .../vo/V2CultivateTaskFinishUserListVo.java | 66 + .../vo/V2CultivateTaskListForManagerVo.java | 76 + .../task/vo/V2MyCultivateLearnTaskListVo.java | 105 + .../V2MyCultivateLearnTaskSimpleInfoVo.java | 147 + .../v2/teaching/model/V2Attachment.java | 28 + .../teaching/req/V2QueryTeachingSkillReq.java | 18 + .../teaching/req/V2SaveTeachingSkillReq.java | 26 + .../v2/teaching/req/V2TeachingAuditReq.java | 31 + .../v2/teaching/req/V2TeachingSaveReq.java | 59 + .../v2/teaching/vo/StoreTeachingVo.java | 28 + .../v2/teaching/vo/StudentInfoVo.java | 19 + .../v2/teaching/vo/TeachingApproveVo.java | 36 + .../v2/teaching/vo/TeachingDetailVo.java | 61 + .../v2/teaching/vo/V2MyRecordPageListVo.java | 50 + .../v2/teaching/vo/V2SkillCategoryVo.java | 25 + .../v2/teaching/vo/V2SkillTreeVo.java | 50 + .../cultivate/v2/teaching/vo/V2SkillVo.java | 50 + .../v2/teaching/vo/V2StoreTeachingVo.java | 27 + .../teaching/vo/V2TeachingRecordDetailVo.java | 108 + .../v2/teaching/vo/V2TeachingRecordVo.java | 58 + .../vo/FtbCultivateCommonSettingGlobalVO.java | 44 + .../vo/FtbCultivateCourseSettingGlobalVO.java | 90 + .../FtbCultivatePromotionPostApplyVO.java | 176 + ...bCultivatePromotionPostApplyWithPerVO.java | 179 + .../vo/casebase/FtbCultivateCaseBaseVO.java | 98 + .../vo/certificate/FtbCertificateInfoVO.java | 47 + .../vo/certificate/FtbCertificateListVO.java | 84 + .../FtbCertificateOrgWisdomStatisticVO.java | 53 + ...FtbCertificatePersonWisdomStatisticVO.java | 88 + .../FtbCertificateQueryStatisticAllVO.java | 37 + .../FtbCertificateUserAppListVO.java | 26 + .../certificate/FtbCertificateUserInfoVO.java | 19 + .../certificate/FtbCertificateUserListVO.java | 87 + .../chapter/FtbCultivateChapterTestInfo.java | 28 + .../FtbCultivateChapterTestResultVO.java | 42 + .../FtbCultivateChapterTestStatisticVO.java | 81 + .../vo/common/InnerPowerPositionVO.java | 19 + .../vo/common/InnerPowerStoreVO.java | 20 + .../cultivate/vo/common/InnerPowerUserVO.java | 19 + .../vo/course/app/ChapterInformationVO.java | 36 + .../vo/course/app/FtbAppTaskCountVO.java | 19 + .../vo/course/app/FtbChapterAppDetails.java | 105 + .../course/app/FtbCheckJobGradeChangeVO.java | 12 + .../vo/course/app/FtbCourseDetailsAppVO.java | 45 + .../vo/course/app/FtbCourseOutlineAppVO.java | 30 + .../app/FtbCultivateCourseMsgForAppVO.java | 28 + .../course/app/FtbGlobalCurriculumAppVO.java | 52 + .../app/FtbGlobalCurriculumAppWrapVO.java | 17 + .../course/app/FtbTrainingCoursesAppVO.java | 44 + .../vo/course/web/CultivateCourseAiVo.java | 27 + .../course/web/FtbCourseDeleteJobLearnVO.java | 47 + .../web/FtbCourseDeleteQuestionBankVO.java | 32 + .../vo/course/web/FtbCourseDeleteVO.java | 24 + .../FtbCultivateCourseChapterDetailsVO.java | 148 + .../web/FtbCultivateCourseChapterVO.java | 37 + .../web/FtbCultivateCourseDetailsVO.java | 81 + .../web/FtbCultivateCourseNumberVO.java | 53 + .../FtbCultivateCourseOrgStatisticesVO.java | 96 + .../course/web/FtbCultivateCoursePageVO.java | 64 + ...FtbCultivateCoursePersonStatisticesVO.java | 118 + .../course/web/FtbCultivateCourseTypeVO.java | 27 + .../web/PromotionChannelLearnCourseVO.java | 22 + .../vo/course/web/UserCourseStudyVo.java | 27 + .../FtbCultivateCoursePackageDetailsVO.java | 30 + .../FtbCultivateCoursePackagePageVO.java | 63 + .../vo/gained/FtbChapterEndGainedVO.java | 29 + .../vo/gained/FtbCourseGainedCommentVO.java | 38 + .../vo/gained/FtbCourseGainedDetailVO.java | 59 + .../vo/gained/FtbCourseGainedForm.java | 22 + .../vo/gained/FtbCourseGainedInfoVO.java | 60 + .../vo/gained/FtbCourseGainedLikeVO.java | 33 + .../vo/gained/FtbCourseGainedListVO.java | 18 + .../vo/gained/FtbGroupCommentVO.java | 41 + .../cultivate/vo/gained/FtbGroupGainedVO.java | 56 + ...CultivateIdentityOrgWisdomStatisticVO.java | 85 + ...tivateIdentityPersonWisdomStatisticVO.java | 114 + .../IdentifyAppAuthorityAppraiserVO.java | 55 + .../identify/IdentifyApplyBasicInfoAppVo.java | 81 + .../vo/identify/IdentifyApplyBasicInfoVo.java | 54 + .../identify/IdentifyApplyDetailsAppVo.java | 28 + .../identify/IdentifyApplyDetailsInfoVo.java | 62 + .../vo/identify/IdentifyApplyInfoApiVo.java | 66 + .../vo/identify/IdentifyApplyInfoAppVo.java | 32 + .../vo/identify/IdentifyApplyInfoVo.java | 78 + .../vo/identify/IdentifyApplyListAppDbVo.java | 84 + .../vo/identify/IdentifyApplyListAppVo.java | 66 + .../vo/identify/IdentifyApplyListDbVo.java | 87 + .../vo/identify/IdentifyApplyListVo.java | 82 + .../IdentifyApplyStatisticsApiVo.java | 19 + .../vo/identify/IdentifyInfoAppVo.java | 74 + .../vo/identify/IdentifyItemsInfoAppVo.java | 23 + .../vo/identify/IdentifyItemsInfoVo.java | 26 + .../vo/identify/IdentifyItemsSaveVo.java | 23 + .../vo/identify/IdentifyItemsUpdateVo.java | 30 + .../identify/IdentifyItemsWithCategoryVo.java | 76 + .../identify/IdentifyStatisticsForNewVo.java | 22 + .../vo/identify/IdentifyStatisticsVo.java | 37 + .../vo/identify/IdentifyTableDeleteVo.java | 17 + .../vo/identify/IdentifyTableInfoApiVo.java | 19 + .../vo/identify/IdentifyTableInfoVo.java | 47 + .../vo/identify/IdentifyTableListVo.java | 37 + .../cultivate/vo/identify/IdentifyTopVo.java | 21 + .../identify/IdentifyUserOrgInfoPushVo.java | 21 + .../vo/identify/IdentifyUserOrgInfoVo.java | 35 + .../vo/identify/ManuallyInitiateVo.java | 16 + .../identify/PostAndCourseCorrelationVo.java | 32 + .../vo/identify/PostAndCourseVo.java | 21 + .../vo/identify/UserIdentifyPageDbVo.java | 65 + .../vo/identify/UserIdentifyPageVo.java | 74 + .../vo/identify/UserOrgInfoAll1Vo.java | 39 + .../vo/identify/UserOrgInfoAllVo.java | 43 + .../vo/identify/UserOrgInfoPushVo.java | 26 + .../cultivate/vo/identify/UserOrgInfoVo.java | 26 + ...tbCultivateLearnTaskCertificateInfoVO.java | 35 + .../FtbCultivateLearnTaskCourseInfoVO.java | 54 + .../FtbCultivateLearnTaskExamInfoVO.java | 81 + .../FtbCultivateLearnTaskFinishInfoVO.java | 83 + ...bCultivateLearnTaskFinishStatisticsVO.java | 27 + ...ultivateLearnTaskIdentificationInfoVO.java | 49 + .../FtbCultivateLearnTaskInfoCountListVO.java | 137 + .../FtbCultivateLearnTaskInfoForAppVO.java | 73 + .../FtbCultivateLearnTaskInfoListVO.java | 114 + .../vo/learn/FtbCultivateLearnTaskInfoVO.java | 125 + .../vo/learn/FtbCultivateLearnTaskListVO.java | 54 + .../learn/FtbCultivateMyLearnTaskListVO.java | 79 + ...portCultivateLearnTaskInfoCountListVO.java | 200 + ...FtbExportCultivateLearnTaskInfoListVO.java | 151 + .../FtbLearnQueryTaskDetailsEditingVO.java | 142 + .../cultivate/vo/learn/WebLearnTaskDto.java | 44 + ...ltivateLearnTaskCertificateUserInfoVO.java | 19 + ...FtbCultivateLearnTaskCourseUserInfoVO.java | 25 + .../FtbCultivateLearnTaskExamUserInfoVO.java | 28 + ...vateLearnTaskIdentificationUserInfoVO.java | 27 + ...FtbCultivateLearnTaskUserFinishInfoVO.java | 27 + .../vo/learn/info/TimeDifference.java | 35 + .../vo/offline/FtbCultivateOfflineFileVO.java | 63 + .../FtbCultivateOfflineTrainDetailsAppVO.java | 58 + .../FtbCultivateOfflineTrainDetailsVO.java | 134 + .../FtbCultivateOfflineTrainPageVO.java | 88 + ...ultivateOfflineTrainPeopleSigningInVO.java | 30 + .../offline/FtbOfflineTrainingAppPageVO.java | 79 + .../cultivate/vo/offline/OfflineCourseVO.java | 30 + .../cultivate/vo/offline/UserInfoVO.java | 41 + .../vo/position/CourseStudyCountVO.java | 12 + .../vo/position/FtbCultivateCourseListVO.java | 45 + .../vo/position/FtbCultivateExamUserVO.java | 49 + .../position/FtbCultivateIdentifyUserVO.java | 67 + ...FtbCultivatePersonStatisticesExportVO.java | 116 + .../FtbCultivatePersonStatisticesVO.java | 120 + ...ivatePositionAssessmentOrgStatisticVO.java | 80 + .../FtbCultivatePositionForNewTrainVO.java | 33 + .../FtbCultivatePositionJobLearnCourseVO.java | 48 + .../FtbCultivatePositionLearnLevelVO.java | 53 + .../position/FtbCultivatePositionLevelVO.java | 40 + .../FtbCultivatePositionOrgStatisticesVO.java | 70 + ...vatePositionPersonStatisticesExportVO.java | 122 + ...bCultivatePositionPersonStatisticesVO.java | 85 + .../FtbCultivatePositionUserInfoVo.java | 33 + .../vo/position/FtbCultivatePositionVO.java | 36 + .../vo/position/FtbCultivateStoreCountVO.java | 62 + .../position/FtbCultivateWorkerCountVO.java | 23 + .../FtbJobLearningConfigurationVO.java | 158 + .../FtbJobLearningExamAppraisalVO.java | 45 + .../position/FtbJobLearningPaginatedVO.java | 48 + .../FtbManagerTrainingStatisticsVO.java | 32 + .../FtbPersonTrainingStatisticsVO.java | 80 + .../FtbPositionCourseDropDownListVO.java | 56 + .../FtbStoreManagerTrainingStatisticsVO.java | 54 + .../vo/position/JobTitleStatistics.java | 28 + .../vo/position/OrganizeCourseDetails.java | 40 + .../vo/position/PersonStatisticesDto.java | 24 + .../PersonalDimensionCourseDetails.java | 57 + .../app/FtbCultivatePositionForAppVO.java | 48 + .../app/FtbCultivatePositionPostForAppVO.java | 31 + .../vo/position/app/FtbPopUpPromptVO.java | 42 + .../app/FtbSubordinateLearningCoursesVO.java | 26 + .../app/OnTheJobLearningCourseVO.java | 23 + .../web/FtbCopyRelatedPositionsVO.java | 29 + .../FtbCultivatePositionAssessmentListVO.java | 87 + .../web/FtbCultivatePositionAssessmentVO.java | 31 + .../web/FtbCultivatePositionCopyVO.java | 19 + .../web/FtbCultivatePositionExamPersonVO.java | 53 + .../web/FtbCultivatePositionExamVO.java | 25 + .../FtbCultivatePositionIdentifyPersonVO.java | 30 + .../web/FtbCultivatePositionIdentifyVO.java | 24 + .../promotion/FtbCultivateLearnMapInfoVO.java | 32 + .../promotion/FtbCultivateMapSimpleDto.java | 29 + .../FtbCultivateMapStudyUserDto.java | 19 + .../FtbCultivateMapsOrgWisdomStatisticVO.java | 83 + ...bCultivateMapsPersonWisdomStatisticVO.java | 84 + .../vo/promotion/FtbCultivateMeberVO.java | 49 + .../vo/promotion/FtbCultivateMemberInfo.java | 38 + .../promotion/FtbCultivateNextCourseVO.java | 32 + .../FtbCultivatePromotionDeleteInfoVO.java | 31 + .../FtbCultivatePromotionForAppVO.java | 22 + .../FtbCultivatePromotionLevelMapVO.java | 50 + .../FtbCultivatePromotionMeberPostInfo.java | 28 + .../FtbCultivatePromotionMemberVO.java | 77 + .../FtbCultivatePromotionNewMemberVO.java | 79 + .../promotion/FtbCultivatePromotionNewVO.java | 67 + .../FtbCultivatePromotionPostNewVO.java | 49 + ...ultivatePromotionPostSelectCourseInfo.java | 28 + .../FtbCultivatePromotionPostVO.java | 68 + .../FtbCultivatePromotionStudyDeleteInfo.java | 30 + .../vo/promotion/FtbCultivatePromotionVO.java | 83 + .../FtbCultivatePromotionWithPersonelVO.java | 76 + ...tbCultivatePromotionWithPersonnelInfo.java | 26 + .../FtbCultivateStudyMemberInfo.java | 40 + .../promotion/FtbCultivateStudyMemberVO.java | 52 + .../FtbCultivateUnderMemberInfo.java | 44 + .../FtbCultivatelearningProgressVO.java | 18 + .../cultivate/vo/rule/FtbCultivateRuleVO.java | 34 + .../CultivateStatisticsForAppCommonParam.java | 11 + .../statistics/FtbCultivateStatisticsVO.java | 87 + .../statistics/NumberOfTrainingSessions.java | 45 + .../vo/statistics/NumberofAppSessions.java | 58 + .../vo/teaching/EmployeePageListVo.java | 48 + .../vo/teaching/EmployeeViewDataVo.java | 39 + .../vo/teaching/MyPracticeSummaryVo.java | 23 + .../vo/teaching/MyRecordPageListVo.java | 45 + .../cultivate/vo/teaching/PositionUserVo.java | 13 + .../vo/teaching/RecordExportListVo.java | 58 + .../cultivate/vo/teaching/RecordInfoVo.java | 76 + .../vo/teaching/RecordPageListVo.java | 118 + .../vo/teaching/RecordViewDataModel.java | 22 + .../cultivate/vo/teaching/SkillCountVo.java | 29 + .../cultivate/vo/teaching/SkillInfoVo.java | 31 + .../cultivate/vo/teaching/SkillKeyVo.java | 11 + .../vo/teaching/StoreInfoListVo.java | 21 + .../cultivate/vo/teaching/StudentUserVo.java | 12 + .../vo/teaching/SummaryPageListVo.java | 41 + .../teaching/SuperiorTeachingSummaryVo.java | 17 + .../vo/teaching/TeachingCountVo.java | 16 + .../vo/teaching/TeachingDataListVo.java | 67 + .../vo/teaching/TeachingRecordDetailVo.java | 94 + .../vo/teaching/TeachingRecordVo.java | 92 + .../vo/teaching/TeachingSkillVo.java | 47 + .../vo/teaching/TeachingStoreCountVo.java | 16 + .../vo/teaching/TeachingStoreListVo.java | 16 + .../cultivate/vo/teaching/TeachingUserVo.java | 24 + .../vo/teaching/TodaySummaryDataVo.java | 21 + .../cultivate/vo/teaching/ViewDataVo.java | 25 + .../culture/dto/CulturePicSettingDto.java | 21 + .../culture/dto/CultureTextSettingDto.java | 25 + .../culture/dto/PicSettingUploadDto.java | 21 + .../model/culture/dto/SettingQueryDto.java | 19 + .../jnpf/model/culture/dto/StatQueryDto.java | 30 + .../jnpf/model/culture/dto/UploadDto.java | 24 + .../LocalDateToEpochSerializer.java | 33 + .../jnpf/model/culture/vo/Base64ImageVo.java | 22 + .../model/culture/vo/CultureClockInVo.java | 45 + .../model/culture/vo/CulturePicSettingVo.java | 24 + .../jnpf/model/culture/vo/CultureStatVo.java | 45 + .../culture/vo/CultureTextSettingVo.java | 44 + .../jnpf/model/culture/vo/RecordDataVo.java | 30 + .../jnpf/model/culture/vo/RecordListVo.java | 27 + .../jnpf/model/culture/vo/UploadInfoVo.java | 27 + .../jnpf/model/culture/vo/YearDataVo.java | 29 + .../model/doclibrary/dto/FileQueryDto.java | 44 + .../model/doclibrary/dto/InfoDetailDto.java | 47 + .../dto/InformationAuthorityDto.java | 24 + .../doclibrary/dto/InformationQueryDto.java | 22 + .../model/doclibrary/dto/QueryMyFilesReq.java | 26 + .../model/doclibrary/dto/RubbishQueryDto.java | 25 + .../jnpf/model/doclibrary/dto/SpaceDto.java | 57 + .../model/doclibrary/dto/UndeleteDto.java | 22 + .../jnpf/model/doclibrary/dto/UploadDto.java | 25 + .../jnpf/model/doclibrary/dto/ViewDto.java | 21 + .../model/doclibrary/vo/CheckRubbishVo.java | 25 + .../model/doclibrary/vo/DeleteInfoVo.java | 21 + .../doclibrary/vo/InformationAuthorityVo.java | 42 + .../doclibrary/vo/InformationDetailVo.java | 74 + .../model/doclibrary/vo/InformationVo.java | 84 + .../doclibrary/vo/MiniInformationTreeVo.java | 44 + .../doclibrary/vo/MiniInformationVo.java | 59 + .../model/doclibrary/vo/RecentlyViewedVo.java | 32 + .../model/doclibrary/vo/RubbishInfoVo.java | 53 + .../jnpf/model/doclibrary/vo/RubbishVo.java | 24 + .../jnpf/model/doclibrary/vo/SpaceInfoVo.java | 21 + .../jnpf/model/doclibrary/vo/SpaceVo.java | 21 + .../jnpf/model/doclibrary/vo/UseDetailVo.java | 54 + .../jnpf/model/doclibrary/vo/UserInfoVo.java | 45 + .../java/jnpf/model/enums/CourseEnums.java | 524 ++ .../model/enums/EmployeeMetaDataType.java | 43 + .../jnpf/model/enums/ExamConfigEnums.java | 283 + .../jnpf/model/enums/ExamUpdateStatus.java | 23 + .../model/enums/ExamUserStatisticsEnums.java | 23 + .../jnpf/model/enums/FContractSignStatus.java | 29 + .../java/jnpf/model/enums/FormFieldType.java | 39 + .../model/enums/FtbGroupEventTypeEnum.java | 14 + .../enums/FtbPersonnelsAuditTaskEnum.java | 49 + .../model/enums/FtbPersonnelsCofigEnum.java | 44 + .../java/jnpf/model/enums/GrowthLogEnum.java | 32 + .../model/enums/SalaryChangeTypeEnum.java | 34 + .../java/jnpf/model/enums/SelfrowingEnum.java | 74 + .../jnpf/model/enums/StaffWorkerStatus.java | 43 + .../model/enums/TaskLearnStatusEnums.java | 30 + .../jnpf/model/enums/TrialPeriodStatus.java | 35 + .../enums/FranchiseeCustomFieldTypeEnum.java | 69 + .../po/FranchiseeCustomFieldEntity.java | 55 + .../model/franchisee/po/FranchiseeEntity.java | 213 + .../po/FranchiseeExperienceEntity.java | 79 + .../po/FranchiseeJoinRegionEntity.java | 58 + .../franchisee/req/FranchiseeAddReq.java | 264 + .../FranchiseeCustomFieldConfigOptionReq.java | 24 + .../req/FranchiseeCustomFieldConfigReq.java | 68 + .../req/FranchiseeCustomFieldReq.java | 29 + .../req/FranchiseeCustomNameUpdateReq.java | 23 + .../req/FranchiseeExperienceReq.java | 56 + .../req/FranchiseeJoinRegionReq.java | 34 + .../franchisee/req/FranchiseeQueryReq.java | 138 + .../req/FranchiseeSimpleAddReq.java | 82 + .../franchisee/req/FranchiseeUpdateReq.java | 22 + .../FranchiseeCustomFieldConfigOptionVO.java | 20 + .../vo/FranchiseeCustomFieldConfigVO.java | 65 + .../vo/FranchiseeCustomFieldVO.java | 37 + .../franchisee/vo/FranchiseeExperienceVO.java | 69 + .../model/franchisee/vo/FranchiseeIdName.java | 27 + .../model/franchisee/vo/FranchiseePageVO.java | 94 + .../franchisee/vo/FranchiseeStoreVO.java | 107 + .../model/franchisee/vo/FranchiseeVO.java | 231 + .../model/helpailog/dto/HelpAiLogDto.java | 51 + .../model/helpailog/dto/HelpAiLogReq.java | 15 + .../model/helpailog/po/FtbHelpAiChatLog.java | 72 + .../java/jnpf/model/im/AdministratorsMsg.java | 22 + .../java/jnpf/model/im/UserAndLinkDTO.java | 21 + .../java/jnpf/model/im/UserSuccessDTO.java | 18 + .../model/memberLog/dto/AddForwardDto.java | 26 + .../jnpf/model/memberLog/dto/AddReplyDto.java | 30 + .../model/memberLog/dto/CommentQueryDto.java | 27 + .../memberLog/dto/ConditionLogQueryDto.java | 33 + .../model/memberLog/dto/LogAgentAlertDto.java | 22 + .../dto/LogMemberAssociationDto.java | 45 + .../memberLog/dto/LogMemberFieldDto.java | 49 + .../model/memberLog/dto/LogMemberLogDto.java | 76 + .../jnpf/model/memberLog/dto/LogQueryDto.java | 33 + .../model/memberLog/dto/LogWebQueryDto.java | 43 + .../model/memberLog/dto/StatQueryDto.java | 48 + .../jnpf/model/memberLog/po/LogComment.java | 44 + .../memberLog/po/LogMemberAssociation.java | 56 + .../model/memberLog/po/LogMemberField.java | 45 + .../jnpf/model/memberLog/po/LogMemberLog.java | 104 + .../jnpf/model/memberLog/po/LogTemplate.java | 67 + .../model/memberLog/po/LogTemplateField.java | 55 + .../memberLog/vo/CommentPaginationVo.java | 28 + .../model/memberLog/vo/LogAssociationVo.java | 37 + .../jnpf/model/memberLog/vo/LogCommentVo.java | 60 + .../jnpf/model/memberLog/vo/LogDataVo.java | 24 + .../jnpf/model/memberLog/vo/LogInfoVo.java | 24 + .../model/memberLog/vo/LogMemberFieldVo.java | 49 + .../model/memberLog/vo/LogMemberLogVo.java | 129 + .../jnpf/model/memberLog/vo/LogStatVo.java | 46 + .../memberLog/vo/LogTemplateFieldVo.java | 59 + .../model/memberLog/vo/LogTemplateVo.java | 31 + .../jnpf/model/memberLog/vo/MiniLogVo.java | 47 + .../model/memberLog/vo/MiniMemberLogVo.java | 36 + .../model/memberLog/vo/ReceiveRangeVo.java | 36 + .../jnpf/model/memberLog/vo/WebLogVo.java | 43 + .../notice/domain/FtbNoticeAnnouncements.java | 121 + .../domain/FtbNoticeAnnouncementsLog.java | 56 + .../domain/FtbNoticeAnnouncementsReceive.java | 81 + .../notice/domain/FtbNoticeCategories.java | 47 + .../model/notice/domain/FtbNoticeFiles.java | 67 + .../model/notice/domain/FtbNoticeManager.java | 37 + .../domain/FtbNoticeUserGroupMembers.java | 36 + .../notice/domain/FtbNoticeUserGroups.java | 37 + .../notice/dto/AnnouncementsCategoryDto.java | 22 + .../notice/dto/AppNoticeCategoriesDto.java | 19 + .../dto/FtbNoticeAnnouncementsDetailDto.java | 140 + .../notice/dto/FtbNoticeAnnouncementsDto.java | 133 + .../dto/FtbNoticeAnnouncementsLogDto.java | 50 + .../dto/FtbNoticeAnnouncementsReceiveDto.java | 35 + .../notice/dto/FtbNoticeCategoriesDto.java | 50 + .../model/notice/dto/FtbNoticeFilesDto.java | 42 + .../model/notice/dto/FtbNoticeManagerDto.java | 39 + .../dto/FtbNoticeUserGroupMembersDto.java | 13 + .../notice/dto/FtbNoticeUserGroupsDto.java | 33 + .../dto/MyNoticeAnnouncementsDetailDto.java | 137 + .../notice/dto/MyNoticeAnnouncementsDto.java | 102 + .../dto/MyNoticeAnnouncementsMsgDto.java | 77 + .../notice/dto/NoticeManagerAuthorityDto.java | 18 + .../jnpf/model/notice/dto/NoticeUserDto.java | 38 + .../notice/dto/UserOrgAndPositionDto.java | 48 + .../jnpf/model/notice/enums/NoticeEnums.java | 335 + .../AddNoticeAnnouncementsReq.java | 87 + .../AppQueryAnnouncementListReq.java | 45 + .../AppQueryMyAnnouncementListReq.java | 46 + .../InnerAppQueryAnnouncementListReq.java | 13 + .../InnerAppQueryMyAnnouncementListReq.java | 19 + .../InnerQueryMyWebAnnouncementListReq.java | 13 + .../req/announcement/NoticeFilesVo.java | 38 + .../QueryAnnouncementListReq.java | 40 + .../QueryMyWebAnnouncementListReq.java | 40 + .../req/announcement/QueryUserListReq.java | 30 + .../UpdateNoticeAnnouncementsReq.java | 10 + .../notice/req/category/AddCategoryReq.java | 21 + .../req/category/QueryCategoryListReq.java | 15 + .../category/QueryNextCategoryListReq.java | 9 + .../req/category/UpdateCategoryReq.java | 8 + .../notice/req/group/AddUserGroupReq.java | 25 + .../req/group/ListGroupUserMemberReq.java | 17 + .../notice/req/group/UpdateUserGroupReq.java | 9 + .../req/manager/AddNoticeManagerReq.java | 17 + .../req/manager/QueryNoticeManagerReq.java | 12 + .../notice/req/oplog/QueryLogListReq.java | 34 + .../personnels/bo/ExportRosterOneVo.java | 21 + .../bo/FtbAddStaffImportRedisBO.java | 24 + .../bo/FtbRewardsImportRedisBO.java | 25 + .../bo/FtbRosterImportConstants.java | 217 + .../bo/FtbRosterImportHeadRuleBO.java | 48 + .../personnels/bo/FtbRosterImportRedisBO.java | 21 + .../bo/FtbRosterImportTemplateBO.java | 23 + .../analysis/FtbPersonnlesAnalysisDTO.java | 44 + .../PersonnlesAnalysisDeserializer.java | 40 + .../apply/FtbPersonnelsApplyCreateDto.java | 260 + .../dto/audit/FtbPersonnelsAuditDto.java | 75 + .../audit/FtbPersonnelsAuditSubConfigVO.java | 49 + .../FtbAddNewPermissionsBatchDTO.java | 86 + .../authoritys/FtbAddNewPermissionsDTO.java | 106 + .../FtbAddNewPermissionsUpdateDTO.java | 14 + .../dto/authoritys/FtbPermissionInfoDTO.java | 46 + .../dto/authoritys/PermissionsCacheDTO.java | 20 + .../dto/base/FtbPersonnelsBaseForOA.java | 45 + .../dto/base/PersonnelsQueryDTO.java | 81 + .../personnels/dto/base/SourcePhone.java | 13 + .../dto/black/FtbPersonnelsBlackListDTO.java | 28 + .../FtbPersonnelsBlackListHistoryDTO.java | 28 + .../black/FtbPersonnelsBlackUpdateDTO.java | 31 + .../black/FtbPersonnelsBlacklistAddDTO.java | 71 + .../FtbPersonnelsBlacklistTypeAddDTO.java | 17 + .../FtbPersonnelsBlacklistTypeUpdateDTO.java | 22 + .../config/FtbPersionnelsSendCranbonCopy.java | 67 + .../config/FtbPersonnelsAuditConfigDTO.java | 72 + .../FtbPersonnelsAuditConfigInfoDTO.java | 109 + .../dto/config/MasterConfigUserBoudDto.java | 25 + .../personnels/dto/config/TestOrgDto.java | 18 + .../dto/contractinfo/ContactStatusInfo.java | 105 + .../personnels/dto/emp/ExcelImprotEmpDTO.java | 31 + .../personnels/dto/emp/FtbEmpAddNewDTO.java | 101 + .../personnels/dto/emp/FtbEmpConfirmDTO.java | 180 + .../personnels/dto/emp/FtbEmpEntryDTO.java | 60 + .../personnels/dto/emp/FtbEmpQueryDTO.java | 19 + .../employeetype/FtbEmployeeTypeAddDTO.java | 18 + .../employeetype/FtbEmployeeTypeEditDTO.java | 23 + .../dto/goods/FtbPersonnelsGoodsFormDTO.java | 68 + .../goods/FtbPersonnelsGoodsFormQueryDTO.java | 24 + .../FtbPersonnelsGoodsFormUpdateDTO.java | 74 + .../FtbPersonnelsGoodsReceiveAddDTO.java | 62 + .../FtbPersonnelsGoodsReceiveQueryDTO.java | 20 + .../FtbPersonnelsGoodsReceiveReturnDTO.java | 58 + .../FtbPersonnelsGoodsReceiveUpdateDTO.java | 28 + .../dto/oa/FtbPersonnelsEmployInfoForOA.java | 34 + .../model/personnels/dto/oa/RequestForOA.java | 40 + .../dto/range/FtbRangeConfigDIYDTO.java | 35 + .../dto/range/FtbRangeConfigDTO.java | 37 + .../dto/range/FtbRangeDiyQueryDTO.java | 21 + .../model/personnels/dto/range/RangeDTO.java | 27 + .../PersonnelsRecruitmentChannelsAddDTO.java | 58 + .../regular/FtbPersonnelsForAppQueryDTO.java | 71 + .../FtbPersonnelsRegularCreateDTO.java | 329 + .../regular/FtbPersonnelsSalaryAuditDto.java | 67 + ...tbResignationConfigurationCategoryDTO.java | 43 + .../FtbResignationConfigurationDTO.java | 54 + .../rewardspunishments/FtbAwardPassedDTO.java | 23 + .../FtbAwardSubmissionDTO.java | 120 + .../FtbEmployeeRewardRecordsQueryDTO.java | 35 + .../FtbPenaltySubmissionDTO.java | 121 + .../FtbPersonnelSalaryRewardDTO.java | 25 + ...nelsRewardsPunishmentExchangeOrderDTO.java | 35 + ...tbPersonnelsRewardsPunishmentQueryDTO.java | 30 + ...tbPersonnelsRewardsPunishmentStartDTO.java | 24 + ...FtbPersonnelsRewardsPunishmentsAddDTO.java | 51 + ...PersonnelsRewardsPunishmentsUpdateDTO.java | 44 + .../dto/roster/FtbPersonnelsTrialDTO.java | 16 + .../dto/roster/FtbRosterImportDTO.java | 15 + .../dto/roster/FtbjobTrialRejectedDTO.java | 61 + .../dto/roster/QueryCompanyAgeDto.java | 31 + .../meta/FtbPersonnlesJobTenureDTO.java | 20 + .../dto/roster/meta/FtbPersonnlesJoeInfo.java | 15 + .../roster/meta/PersonnelsBaseMetaData.java | 123 + .../meta/PersonnelsContactMetaData.java | 22 + .../meta/PersonnelsEducationMetaData.java | 16 + .../meta/PersonnelsMaterialMetaData.java | 21 + .../dto/roster/meta/PersonnelsMetaDTO.java | 62 + .../roster/meta/PersonnelsOrgMetaData.java | 53 + .../roster/meta/PersonnelsRoleMetaData.java | 19 + .../roster/meta/PersonnelsWorkerMetaData.java | 62 + .../dto/salary/BaseSalaryStructureDTO.java | 51 + .../FtbPersonnelRosterSalaryHistoryDTO.java | 16 + .../dto/salary/FtbPersonnelsSalaryInfo.java | 80 + ...sonnelsSalaryTemporaryStorageCreatDto.java | 60 + .../dto/salary/FtbSalaryMetaDataQueryDto.java | 28 + .../dto/salary/FtbXcCustomFieldDto.java | 17 + .../dto/scheme/FtbPersonAddSchemeDTO.java | 38 + .../dto/scheme/FtbPersonEditSchemeDTO.java | 31 + .../secondment/FtbHandleSecondmentDTO.java | 225 + .../dto/secondment/FtbSecondMentQueryDTO.java | 29 + .../secondment/msg/SecondmentMsgUserInfo.java | 71 + .../FtbPersonnelsEmployApplyDto.java | 132 + .../FtbPersonnelsEmployApplyListDto.java | 95 + .../employment/FtbPersonnelsMoveLogVO.java | 174 + .../FtbPersonnelsPromotionLogVO.java | 112 + .../employment/FtbPersonnelsRegularVO.java | 67 + .../FtbPersonnelsStaffEmploymentApplyDto.java | 243 + .../employment/FtbPersonnelsTransferVO.java | 134 + .../FtbPersonnelsTurnoverLogVO.java | 68 + .../dto/staff/field/EditFormFieldDto.java | 11 + .../dto/staff/field/ExportFormFieldDto.java | 54 + .../dto/staff/field/ExportFormTypeDto.java | 20 + .../dto/staff/field/FormFieldDto.java | 32 + .../dto/staff/field/FormTypeDto.java | 26 + ...FtbPersonnelsRegistrationFormFieldDto.java | 150 + ...sonnelsRegistrationFormFieldOptionDto.java | 27 + .../FtbPersonnelsRegistrationFormTypeDto.java | 65 + .../dto/staff/field/SubFormFieldDto.java | 32 + .../dto/staff/field/SubMulitFieldValDto.java | 30 + .../dto/staff/growth/AddGrowthLogDto.java | 73 + .../FtbPersonnelsStaffGrowthLogDto.java | 67 + .../staff/registerform/BankConvertDto.java | 25 + .../CheckRegisterFormFillDto.java | 11 + ...ersonnelsStaffRegistrationFormDataDto.java | 56 + .../staff/registerform/GroupFieldDataDto.java | 31 + .../registerform/HealthConverterDto.java | 19 + .../registerform/IdCardConverterDto.java | 39 + .../registerform/ProbationPeriodDto.java | 30 + .../staff/registerform/WorkAddressDto.java | 19 + .../dto/staff/roster/CheckRosterDeleteVo.java | 58 + .../dto/staff/roster/EditUserBaseInfoDto.java | 19 + .../roster/FtbPersonnelsStaffRosterDto.java | 458 ++ .../roster/PartUserInfoVoAndWorkerNo.java | 18 + .../dto/staff/roster/SaffRoleDto.java | 22 + .../dto/staff/roster/ShopManagerUserDto.java | 14 + .../dto/staff/roster/SimpleStoreUserDto.java | 32 + .../dto/staff/roster/StaffBaseInfoDto.java | 55 + .../dto/staff/roster/StaffDepartDto.java | 87 + .../dto/staff/roster/StaffHomeDto.java | 55 + .../dto/staff/roster/StaffPromotionDto.java | 132 + .../dto/staff/roster/StaffRegularDto.java | 86 + .../dto/staff/roster/StaffRosterInfoDto.java | 67 + .../roster/StaffRosterSimpleBaseInfoDto.java | 70 + .../dto/staff/roster/StaffTravlFailDto.java | 39 + .../dto/staff/roster/WorkerGroupDataDto.java | 132 + .../dto/staff/roster/WorkerStatisticsDto.java | 67 + .../salarylog/AddSalaryChangeLogDto.java | 33 + .../FtbPersonnelsStaffSalaryChangeLogDto.java | 48 + ...FtbPersonnelsStaffTransferPositionDto.java | 118 + ...nnelsStaffTransferPositionHandoverDto.java | 67 + .../staff/transfer/TransferGrowthLogDto.java | 123 + .../transfer/TransferPositionCountDto.java | 39 + .../staff/transfer/TransferPositionDto.java | 544 ++ .../dto/turnover/FtbDepUserDTO.java | 20 + ...tbPersonnelsJobTrialRejectedCreateDTO.java | 108 + .../FtbPersonnelsTurnoverCreateDTO.java | 299 + .../turnover/FtbPersonnelsTurnoverDTO.java | 27 + .../FtbPersonnelsTurnoverHandoverDTO.java | 58 + .../dto/turnover/SaveTenantUserForm.java | 23 + .../DeleteRecommendedPersonnelDTO.java | 20 + .../uchisuike/FtbGenerateShortChainDTO.java | 25 + .../FtbRecommendationPoolOrgDTO.java | 17 + .../FtbinternalRecommendationPoolListDTO.java | 30 + .../FtbRecommendationInvitationAppDTO.java | 148 + .../po/FtbPersonnelsAuditCarbonRecipient.java | 95 + .../po/FtbPersonnelsAuditMasterConfig.java | 80 + .../po/FtbPersonnelsAuditRunTask.java | 56 + .../po/FtbPersonnelsAuditRunTaskHistory.java | 53 + .../po/FtbPersonnelsAuditSubConfig.java | 141 + .../po/FtbPersonnelsAuditTaskInfo.java | 101 + .../po/FtbPersonnelsAuthoritys.java | 74 + .../personnels/po/FtbPersonnelsBlacklist.java | 80 + .../po/FtbPersonnelsBlacklistHistory.java | 100 + .../po/FtbPersonnelsBlacklistType.java | 39 + .../personnels/po/FtbPersonnelsGoods.java | 58 + .../po/FtbPersonnelsGoodsReceive.java | 87 + .../po/FtbPersonnelsIdcardVerification.java | 50 + .../po/FtbPersonnelsPermissionUser.java | 43 + .../po/FtbPersonnelsPermissions.java | 109 + .../personnels/po/FtbPersonnelsPostApply.java | 248 + .../po/FtbPersonnelsPromiseConfig.java | 57 + .../po/FtbPersonnelsRecruitmentChannels.java | 34 + .../FtbPersonnelsRegistrationFormField.java | 161 + ...PersonnelsRegistrationFormFieldOption.java | 48 + .../po/FtbPersonnelsRegistrationFormType.java | 59 + .../po/FtbPersonnelsRegularManagement.java | 335 + ...nnelsResignationCategoryConfiguration.java | 48 + ...FtbPersonnelsResignationConfiguration.java | 62 + .../po/FtbPersonnelsRewardsPunishments.java | 67 + .../po/FtbPersonnelsRuleConfig.java | 40 + .../FtbPersonnelsSalaryTemporaryStorage.java | 55 + .../po/FtbPersonnelsSecondmentConfig.java | 42 + .../po/FtbPersonnelsSecondmentManagement.java | 205 + .../po/FtbPersonnelsShortchain.java | 50 + .../po/FtbPersonnelsStaffArchivesHistory.java | 53 + .../po/FtbPersonnelsStaffEmploymentApply.java | 357 + .../po/FtbPersonnelsStaffGrowthLog.java | 95 + ...tbPersonnelsStaffRegistrationFormData.java | 55 + .../po/FtbPersonnelsStaffRoster.java | 303 + .../po/FtbPersonnelsStaffRosterScheme.java | 43 + .../po/FtbPersonnelsStaffSalaryChangeLog.java | 61 + .../FtbPersonnelsStaffTransferPosition.java | 213 + ...rsonnelsStaffTransferPositionHandover.java | 60 + .../po/FtbPersonnelsTransferManage.java | 240 + ...PersonnelsTurnoverAccountRegistration.java | 59 + .../po/FtbPersonnelsTurnoverHandover.java | 74 + .../po/FtbPersonnelsTurnoverManagement.java | 277 + .../po/FtbPersonnelsUchisuikePond.java | 134 + .../po/FtbPersonnelsUchisuikePondOrg.java | 39 + .../po/FtbPersonnlesInfoConfig.java | 42 + .../po/FtbPersonnlesInfoDiyRangeConfig.java | 80 + .../po/FtbPersonnlesInfoRangeConfig.java | 66 + .../AddStaffEmploymentApplyReq.java | 150 + .../AddStaffEmploymentApplyResultDto.java | 37 + .../AddStaffEmploymentApplySalaryDTO.java | 74 + .../AddStaffEmploymentApplySalaryItemDTO.java | 70 + .../req/employment/BatchByPrimaryIdReq.java | 23 + .../employment/EmploymentApplyCheckDto.java | 65 + .../MyWebEmploymentApplyCheckListReq.java | 18 + .../QueryAppStaffEmploymentApplyListReq.java | 43 + .../QueryStaffEmploymentApplyListReq.java | 83 + .../req/employment/SendPhoneMsgReq.java | 23 + .../QueryRegistrationFormFieldListReq.java | 28 + .../QueryRegistrationFormTypeListReq.java | 11 + .../SaveRegistrationFormFieldOptionReq.java | 22 + .../field/SaveRegistrationFormFieldReq.java | 66 + .../field/SaveRegistrationFormTypeReq.java | 31 + .../req/goods/FtbPersonnelsGoodsForm.java | 61 + .../personnels/req/registerform/OcrReq.java | 17 + .../QueryRegisterFormDataReq.java | 12 + .../SaveAppRegisterFormDataReq.java | 22 + .../req/registerform/SaveFormDataReq.java | 35 + .../req/registerform/SaveMyFormDataReq.java | 17 + .../registerform/SaveRegisterFormDataReq.java | 20 + .../req/roster/AppStaffRosterListReq.java | 86 + .../roster/CheckOrgAndPosAndRankExistVo.java | 35 + .../req/roster/ConfirmOnDutyReq.java | 196 + .../req/roster/FtbPersonnelsMetaDataReq.java | 25 + .../roster/FtbPersonnelsMetaFuctionReq.java | 20 + .../req/roster/StaffRosterListExportReq.java | 25 + .../req/roster/StaffRosterListReq.java | 157 + .../personnels/req/roster/StaffRosterReq.java | 19 + .../personnels/req/roster/UserAccountDto.java | 37 + .../req/transfer/AppQueryTransferListReq.java | 22 + .../req/transfer/FtbHandleTransferDTO.java | 212 + .../transfer/FtbHandleTransferOAAddDTO.java | 198 + .../req/transfer/FtbHandleTransferOaDTO.java | 28 + .../transfer/FtbHandleTransferQueryDTO.java | 42 + .../transfer/MyWebTransferCheckListReq.java | 16 + .../req/transfer/QueryTransferListReq.java | 56 + .../req/transfer/SaveTransferReq.java | 155 + .../transfer/TransferPositionHandoverReq.java | 25 + .../FtbPersonnelsLeaveReasonDetailVO.java | 50 + ...PersonnelsNewEmployeeTurnoverDetailVO.java | 50 + .../FtbPersonnelsNewHireRateDetailVO.java | 47 + .../FtbPersonnelsOnboardingDetailVO.java | 47 + .../FtbPersonnelsPeopleInfoRatioVO.java | 48 + .../FtbPersonnelsPeoplePostRatioVO.java | 41 + .../FtbPersonnelsPeopleTransferRatioVO.java | 37 + .../FtbPersonnelsPostAdjustmentDetailsVO.java | 70 + .../FtbPersonnelsRegularizationDetailVO.java | 48 + .../FtbPersonnelsShopAdjustmentDetailsVO.java | 69 + .../FtbPersonnelsTurnoverAgeDetailVO.java | 53 + .../FtbPersonnelsTurnoverDetailVO.java | 54 + ...rsonnelsTurnoverFirstMonthLeaveRateVO.java | 29 + ...ersonnelsTurnoverFirstWeekLeaveRateVO.java | 25 + ...ersonnelsTurnoverFirstYearLeaveRateVO.java | 25 + .../FtbPersonnelsTurnoverGenderDetailVO.java | 53 + .../FtbPersonnelsTurnoverInfoDetailVO.java | 50 + ...sTurnoverLeavePeopleAgeDistributionVO.java | 38 + ...rsonnelsTurnoverLeavePeopleAgeRatioVO.java | 39 + ...sonnelsTurnoverLeavePeoplePostRatioVO.java | 43 + .../FtbPersonnelsTurnoverLeaveRateVO.java | 44 + ...PersonnelsTurnoverLeaveReasonDetailVO.java | 53 + ...nelsTurnoverLeaveReasonDistributionVO.java | 22 + ...PersonnelsTurnoverNewEmployeeDetailVO.java | 53 + .../FtbPersonnelsTurnoverPeopleSexVO.java | 39 + ...tbPersonnelsTurnoverPostRatioDetailVO.java | 47 + ...FtbPersonnelsTurnoverRateComparisonVO.java | 24 + .../FtbPersonnelsTurnoverRateInfoVO.java | 21 + .../analysis/FtbPersonnelsTurnoverRateVO.java | 39 + ...tbPersonnelsTurnoverSeniorityDetailVO.java | 53 + ...bPersonnelsTurnoverWithAnalysisInfoVO.java | 34 + .../analysis/FtbPersonnlesBrainDrainVO.java | 29 + .../FtbPersonnlesBrainShopAdjustmentVO.java | 29 + .../analysis/PersonnelDataAnalysisListVO.java | 57 + .../PersonnelsOverviewAgeDetailsVO.java | 52 + .../PersonnelsOverviewCompanyDetailsVO.java | 49 + ...PersonnelsOverviewDepartmentDetailsVO.java | 43 + .../PersonnelsOverviewEducationDetailsVO.java | 49 + .../PersonnelsOverviewEmployeesAgeVO.java | 29 + .../PersonnelsOverviewEmployeesDetailsVO.java | 54 + .../PersonnelsOverviewEmployeesVO.java | 29 + .../PersonnelsOverviewInsuranceDetailsVO.java | 49 + .../PersonnelsOverviewJobDetailsVO.java | 43 + ...PersonnelsOverviewProportionDetailsVO.java | 48 + .../PersonnelsOverviewProportionVO.java | 42 + .../PersonnelsOverviewSeniorityDetailsVO.java | 52 + .../PersonnelsOverviewSituationDetailsVO.java | 48 + .../PersonnelsOverviewStoreDetailsVO.java | 43 + .../PersonnelsOverviewTypeDetailsVO.java | 49 + .../vo/app/FtbPersonnelsBubbleCountVO.java | 28 + .../vo/apply/FtbPersonnelsApplyVO.java | 327 + .../vo/apply/FtbPersonnelsApplyWithPerVO.java | 321 + .../vo/apply/FtbPersonnelsCourseVO.java | 13 + .../vo/authoritys/FtbPermissionInfoVO.java | 87 + .../vo/authoritys/FtbPermissionUserVO.java | 26 + .../FtbPersonnelsPermissionUserVO.java | 92 + .../authoritys/FtbPersonnelsPermissionVO.java | 44 + .../vo/authoritys/FtbPersonnelsScopeVO.java | 18 + .../vo/black/BlackHistoryStatus.java | 20 + .../personnels/vo/black/BlackTermEnum.java | 80 + .../FtbPersonnelsBlackListHistoryVO.java | 80 + .../vo/black/FtbPersonnelsBlackListVO.java | 110 + .../FtbPersonnelsBlacklistTypeListVO.java | 17 + .../vo/config/FtbPersonnelsAuditConfigVO.java | 57 + .../config/FtbPersonnelsAuditRunTaskInfo.java | 109 + .../personnels/vo/emp/FtbEmpAddNewVO.java | 84 + .../personnels/vo/emp/FtbEmpConfirmVO.java | 227 + .../personnels/vo/emp/FtbEmpEntryVO.java | 165 + .../personnels/vo/emp/FtbEmpResultVO.java | 32 + .../FtbPersonnelsEmployeeTypeVO.java | 27 + .../AddStaffEmploymentApplyExcelERRVO.java | 96 + .../AddStaffEmploymentApplyExcelVO.java | 107 + .../vo/employment/CheckPhoneStatusVo.java | 14 + .../employment/FtbPeronnelsStatisticVo.java | 28 + .../FtbPersonnelsStaffEmploymentApplyVO.java | 117 + .../vo/goods/FtbPersonnelsGoodsPageVO.java | 58 + .../FtbPersonnelsGoodsReceiveDetailsVO.java | 70 + .../FtbPersonnelsGoodsReceivePageVO.java | 66 + .../goods/PersonnelsGoodsDistributedLock.java | 9 + .../FtbPersonnelsAuditRunTaskHistoryVO.java | 61 + .../vo/mcp/OnboardingThisMonthStatsVO.java | 20 + .../personnels/vo/mcp/StaffBirthdayVO.java | 43 + .../vo/mcp/StaffNotSignContractVO.java | 43 + .../vo/mcp/SubmittedButNotOnboardedVO.java | 22 + .../vo/range/FtbRangeConfigDIYVO.java | 52 + .../personnels/vo/range/FtbRangeConfigVO.java | 47 + .../regular/FtbPersonnelsRegularInfoVO.java | 301 + .../FtbPersonnelsRegularManagementVO.java | 312 + ...FtbResignationConfigurationCategoryVO.java | 36 + .../FtbResignationConfigurationVO.java | 54 + .../FtbEmployeeRewardRecordsVO.java | 110 + .../FtbEmployeeRewardUserInfoVO.java | 21 + .../FtbEmploymentRecordsExcelERRVO.java | 83 + .../FtbEmploymentRewardExcelVO.java | 160 + .../FtbPersonnelSalaryRewardVO.java | 17 + ...PersonnelsRewardsPunishmentApprovalVO.java | 43 + ...FtbPersonnelsRewardsPunishmentQueryVO.java | 62 + .../FtbXcEmployeeRewardRecordsVO.java | 33 + .../vo/roster/FtbHomePageRewardVO.java | 29 + .../vo/roster/FtbPersonnelsChangeInfoVO.java | 28 + .../FtbPersonnelsStaffArchivesHistoryVo.java | 33 + .../vo/roster/FtbPersonnlesJobTenureVO.java | 24 + .../vo/roster/FtbRosterAttributesVO.java | 49 + .../vo/roster/FtbRosterCategoryVO.java | 71 + .../FtbRosterImportFormFieldsConfigVO.java | 42 + .../vo/roster/FtbRosterImportVO.java | 41 + .../roster/FtbRosterInsertAttributesVO.java | 23 + .../vo/roster/FtbRosterInsertNomalVO.java | 45 + .../personnels/vo/roster/FtbRosterPageVO.java | 21 + .../vo/roster/FtbThousandFacePersonVO.java | 33 + .../personnels/vo/roster/ImportResultVO.java | 33 + .../vo/roster/InnerImportResultVO.java | 13 + .../vo/roster/InnerImportRosterDto.java | 38 + .../vo/salary/FtbPersonnelRosterSalaryVO.java | 54 + ...FtbPersonnelsSalaryTemporaryStorageVo.java | 60 + .../vo/salary/FtbPersonnelsSalaryVO.java | 24 + .../vo/salary/FtbXcCustomFieldVo.java | 43 + .../vo/salary/UserInfoWithSalary.java | 63 + .../vo/scheme/FtbPersonSchemeDetailsVO.java | 24 + .../vo/scheme/FtbPersonSchemeListVO.java | 17 + .../FtbPersonnelsSecondRecordVO.java | 84 + .../FtbPersonnelsSecondmentInfoVO.java | 101 + .../FtbPersonnelsSecondmentTaskVO.java | 74 + .../secondment/FtbPersonnelsSecondmentVO.java | 106 + .../vo/task/FtbPersonnelsAuditInfoVO.java | 36 + .../vo/task/FtbPersonnelsAuditRunTaskVO.java | 64 + .../vo/task/FtbPersonnelsResult.java | 51 + .../transfer/FtbHandleTransferDetailsVO.java | 235 + .../FtbHandleTransferPageExportVO.java | 109 + .../vo/transfer/FtbHandleTransferPageVO.java | 118 + .../vo/turnover/FtbPersonnelsTurOrgInfo.java | 22 + .../turnover/FtbPersonnelsTurnoverInfoVO.java | 209 + .../FtbPersonnelsTurnoverManagementVO.java | 278 + .../FtbPersonnelsTurnoverStatisticVO.java | 151 + ...FtbinternalRecommendationPoolExportVO.java | 149 + .../FtbinternalRecommendationPoolVO.java | 150 + .../dto/PaginationQualificationsDTO.java | 28 + .../qualifications/dto/QualificationsDto.java | 27 + .../dto/SaveQualificationsDTO.java | 29 + .../dto/SaveQualificationsFieldDTO.java | 30 + .../dto/SaveQualificationsFieldItemDTO.java | 30 + .../model/qualifications/vo/GradeVOExp.java | 17 + .../vo/PositionAndGradesVOExp.java | 14 + .../vo/QualificationsCategoryVo.java | 18 + .../vo/QualificationsInfoVO.java | 22 + .../vo/QualificationsItemsVo.java | 26 + .../vo/QualificationsListVO.java | 41 + .../vo/QualificationsStatisticsVO.java | 23 + .../qualifications/vo/QualificationsVo.java | 13 + .../src/main/java/jnpf/model/store/Store.java | 124 + .../jnpf/model/store/StorePagination.java | 64 + .../jnpf/model/store/StorePositionInfoVo.java | 33 + .../java/jnpf/model/store/StoreRegionVo.java | 39 + .../main/java/jnpf/model/store/StoreUser.java | 60 + .../java/jnpf/model/store/StoreUserNumVo.java | 25 + .../model/store/dto/BatchSaveStoreDto.java | 17 + .../store/dto/StoreAbnormalIdsQueryDTO.java | 17 + .../store/dto/StorePageByIdsNoDsQueryDTO.java | 21 + .../store/dto/StorePageByIdsQueryDTO.java | 21 + .../jnpf/model/store/dto/StoreUserDto.java | 20 + .../jnpf/model/store/vo/StoreBaseListVO.java | 62 + .../jnpf/model/store/vo/StoreLocationVO.java | 46 + .../model/store/vo/StoreUserRelationVo.java | 38 + .../jnpf/model/store/vo/StoreUsersVo.java | 29 + .../jnpf/model/store/vo/UserStoreListVo.java | 26 + .../po/StoreCertificatePhotoEntity.java | 72 + .../po/StoreCertificatePhotoItemEntity.java | 60 + .../req/StoreCertificatePhotoAddReq.java | 107 + .../req/StoreCertificatePhotoItemReq.java | 35 + .../req/StoreCertificatePhotoUpdateReq.java | 22 + .../vo/StoreCertificatePhotoIdNameVO.java | 40 + .../vo/StoreCertificatePhotoItemVO.java | 49 + .../vo/StoreCertificatePhotoVO.java | 86 + .../jnpf/model/tempmodule/AppBannerVo.java | 37 + .../jnpf/model/tempmodule/RollImageVo.java | 28 + .../model/tempmodule/dto/RollImageDto.java | 30 + .../enums/CertificateTypeEnum.java | 71 + .../warningnotice/po/FtbParamEntity.java | 50 + .../req/WarningNoticeSaveReq.java | 47 + .../req/WarningNoticeTargetReq.java | 29 + .../req/WarningNoticeUserConfigReq.java | 33 + .../vo/WarningNoticeTargetVO.java | 24 + .../vo/WarningNoticeUserConfigVO.java | 26 + .../warningnotice/vo/WarningNoticeVO.java | 52 + .../dto/ApplyAttendanceChangeDto.java | 54 + .../dto/ApplyAttendanceOutsideDto.java | 63 + .../dto/ApplyAttendanceRepairDto.java | 38 + .../dto/ApplyAttendanceViolationDto.java | 49 + .../workflow/dto/AttendanceBaseForOa.java | 88 + .../dto/AttendanceBusinessTripApproveDto.java | 67 + .../AttendanceBusinessTripApproveOaDto.java | 46 + .../dto/AttendanceGoOutApproveDto.java | 56 + .../dto/AttendanceLeaveApproveDto.java | 139 + .../dto/AttendanceWorkOvertimeApproveDto.java | 111 + .../workflow/dto/PunishmentsApprovalDto.java | 87 + .../model/workflow/dto/RewardApprovalDto.java | 87 + .../model/workflow/dto/SelfApproveDto.java | 180 + .../jnpf/model/workflow/dto/UserSelfDto.java | 19 + .../workflow/vo/ApplyAttendanceChangeVo.java | 50 + .../workflow/vo/ApplyAttendanceOutsideVo.java | 44 + .../workflow/vo/ApplyAttendanceRepairVo.java | 42 + .../vo/AttendanceBusinessTripApproveVo.java | 67 + .../workflow/vo/AttendanceGoOutApproveVo.java | 58 + .../vo/AttendanceLeaveFlowApproveVo.java | 138 + .../vo/AttendanceWorkOvertimeApproveVo.java | 91 + .../jnpf/model/workflow/vo/ClockKindVo.java | 24 + .../workflow/vo/PunishmentsApprovalVo.java | 88 + .../model/workflow/vo/RewardApprovalVo.java | 88 + .../jnpf/model/workflow/vo/SelfApproveVo.java | 134 + .../src/main/java/jnpf/util/ConstantUtil.java | 565 ++ .../src/main/java/jnpf/util/DateDetail.java | 1645 +++++ .../src/main/java/jnpf/util/DateStrVo.java | 24 + .../java/jnpf/util/DesensitizedUtils.java | 71 + .../src/main/java/jnpf/util/ExportUtil.java | 27 + .../src/main/java/jnpf/util/FtbUtil.java | 367 + .../main/java/jnpf/util/SeetaConstant.java | 36 + .../java/jnpf/util/TemplateExcelUtils.java | 578 ++ .../java/jnpf/util/TemplateWorkSheet.java | 39 + .../adapter/SelfDataConversionAdapter.java | 32 + .../jnpf/util/excelv2/annotation/Excel.java | 289 + .../jnpf/util/excelv2/annotation/Excels.java | 13 + .../domain/ExcelBeanMethodParseVo.java | 27 + .../util/excelv2/util/CellValueConvert.java | 851 +++ .../util/excelv2/util/ExcelDateUtils.java | 127 + .../jnpf/util/excelv2/util/ExcelUtils.java | 2216 +++++++ .../jnpf/util/excelv2/util/ImageUtils.java | 64 + .../jnpf/util/excelv2/util/ReflectUtils.java | 255 + .../util/excelv2/util/SpringBeanUtils.java | 125 + .../src/main/java/jnpf/valid/ValidInsert.java | 10 + .../src/main/java/jnpf/valid/ValidUpdate.java | 10 + jnpf-ftb/pom.xml | 26 + 4393 files changed, 450030 insertions(+), 103 deletions(-) create mode 100644 .idea/sqldialects.xml delete mode 100644 ftb/src/main/java/ftb/test/controller/.gitkeep delete mode 100644 ftb/src/main/java/ftb/test/controller/UserController.java delete mode 100644 ftb/src/main/java/ftb/test/controller/UserCreateRequest.java create mode 100644 jnpf-ftb/jnpf-ftb-api/pom.xml create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/PTenantAccountApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBack.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBackFactory.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceConfirmApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceDailyRuleApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceGroupApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceLineSchedulingConfigApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceSimulateDataApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceUserApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbClockInApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbStatisticsApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursDto.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserGroupVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserListGroupVO.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/CompareTypeEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceCountDto.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceDayCountDto.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/GroupUpdateByUserDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsAbnormalConditionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDailySituationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsFullSituationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsHoursRankingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsOvertimeSituationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsPerCapitaVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsSituationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceConfirmApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceDailyRuleApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceGroupApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceLineSchedulingConfigApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceSimulateDataApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceUserApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbClockInApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbStatisticsApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/FtbAuthorityApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/fallback/FtbAuthorityApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateManageApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateWarningApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateManageFallbackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateWarningFallbackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateIdentifyApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateLearnTaskListApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivatePromotionApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateStoreStatisticApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateTeachingApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/V2CultivateOldDealApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateIdentifyApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateLearnTaskApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivatePromotionFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateStoreStatisticApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateTeachingApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/V2CultivateOldDealApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/StoreApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/fallback/StoreFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/V2CultivateTimingApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/fallback/V2CultivateTimingApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/FranchiseeApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/fallback/FranchiseeFallbackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/FtbNoticeApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/fallback/FtbNoticeFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonneApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsContactInfoManagerApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmEntryApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmployeeTypeRemoteApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmploymentApplyApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsInfoConfigApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsMetaDataManagerApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsRemoteApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRosterManagerApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsTurnoverManagementApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsContaceInfoManagerFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmployeeTypeRemoteFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmploymentApplyFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsInfoConfigBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsMetaDataManagerFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRewardsPunishmentsRemoteFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRosterManagerFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsTurnoverManagementFallBackApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/V2AuthPermissionApi.java create mode 100644 jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/fallback/V2AuthPermissionApiFallback.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/pom.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/JnpfFtbApplication.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/ApiCallLogAspect.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/FtbApiCallLog.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/Machine.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/MachineAspect.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/antifreeze/UserAntifreeze.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/ChangeConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/FtbThreadPoolExecutor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AppStatisticsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttenceMachineController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceAIController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceApproveController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBaseSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookOperationLogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookRecordController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceChangeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInPicController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCloudAlbumController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceConfirmController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCustomizeTableController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceDailyRuleController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalRulesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupUserController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceHolidaySettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLeaveRulesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLineSchedulingConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLocationSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceMachineManageController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceNoticeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendancePermissionDictController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceQuickTemplateController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftNameSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSimulatedDataController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSuperAdminController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserBalanceController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/FfiMachineController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/InitializationController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/IsPerfMachineController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/KeMiController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/OvertimeRuleController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/PublicHolidayRulesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/RV1109MachineController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/ScheduleGroupRuleConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/SmartPreScheduleController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserFaceController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserPhoneController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WebStatisticsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WorkstationController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/entity/FixedHandleDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/NotificationClockMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingFailConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeUserConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentWithdrawalConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsBatchClearMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleHistoryMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/AttendanceBookMergeHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/CompositeWriteHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/HeadStyleHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/CustomStringStringConverter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/LineSchedulesDataListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/SchedulesDataListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/handler/JsonToListTypeHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ApiCallLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceAIMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApprovalSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceRecordMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceUseRecordMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBaseSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookOperationLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookRecordMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCardReplacementApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInPicMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInResultMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCloudAlbumMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmDetailsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCustomizeTableMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDailyRuleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDayStatisticsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalRulesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldPersonnelMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldpersonnelApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFixedClassMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceHolidaySettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveGrantSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveRulesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveSettingsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveTypeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLocationSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineManageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineSyncMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceManagerPermissionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceNoticeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateItemMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceRepairMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceResultRollbackMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSealSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSelfApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftNameSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingPeriodMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceRecordMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFaceMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFingerprintMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultV2Mapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/CommonSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/DailyRuleChangeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/EnableBalanceMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceFaceChangeLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingPayrollHoursMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupDrawingParamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupFixedParamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/InitializationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleDetailMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PermissionDictMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PublicHolidayRulesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/RuleScopeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StatisticsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StorageRestMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/UserPhoneMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchResult.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftPeriodMatcher.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceScheduleDayParse.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AutoSchedulePipelineLog.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscovery.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscoveryConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CrossPostGeneralStaffAllocator.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterFromCoverBuilder.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterLine.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftCoverOutcome.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftGreedyCoverPlanner.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftConstraintRelaxationPlanner.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftRuleKind.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourDemandBasedShiftPlanBuilder.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourPostDemandMatrix.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourSlotLabel.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftCoverConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftGapPlanner.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftPick.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/PostFixedShiftCandidate.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverResult.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverSteps678.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePatternSimilarDaysAlgorithm.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePeriodWorkTracker.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleStaffAssignmentSteps910.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysAlgorithm.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysResult.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingForTestCheckLog.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingSimilarDaysMode.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftHalfHourNormalizer.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResult.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResultLogger.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAttendanceShiftIdResolver.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlock.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlocksFromDemandCoverBuilder.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanFinalStaffAssigner.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPickSnapshot.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPostNeed.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarDayDemandSteps4And5.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalDaySlotTableBuilder.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalJobSlotRow.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostRobustTargetConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostTargetHeadcountCalculator.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentContext.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentDayLedger.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentIntervalLedger.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPort.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortPermissive.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/WorkstationPostStaffBuckets.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AppStatisticsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttenceMachineService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceAIService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApprovalSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApproveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBaseSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookOperationLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookRecordService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceChangeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInPicService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInResultService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCloudAlbumService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmDetailsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCustomizeTableService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDailyRuleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDayStatisticsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalRulesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFixedClassService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceGroupService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceHolidaySettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveRulesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveTypeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingPayrollHoursService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLocationSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineManageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineSyncService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceNoticeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendancePermissionDictService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateItemService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceRepairService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSealSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftNameSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingPeriodService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSuperAdminService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceRecordService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserFaceService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ClockInResultService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/CommonSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/DailyRuleChangeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/EnableBalanceService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/InitializationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/IsPerfMachineService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/MachineStrategy.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/NoticeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleDetailService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/PublicHolidayRulesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RV1109MachineService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleScopeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ScheduleGroupRuleConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/SmartPreScheduleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/StatisticsUtilService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceTxService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UsualPhoneService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/WorkstationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterContext.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterResult.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/CrossDayFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/LeaveFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/OvertimeFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/SecondmentFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/chain/RuleFilterChain.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceApproveNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceChangeNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNoticeHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/BeforeClockInNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecUnscheduledNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecutiveAbsenceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/FastClockInNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupAdminUpdateNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupLockNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupUnLockNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineNotSchedulingAttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineShiftChangAttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/NoClockInNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/RuleChangeAttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementDayNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementMonthNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementTeamMonthNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ShiftChangAttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SystemTypeChangeAttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/UserChangAttendanceNotice.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/AttendanceRuleNotificationHandle.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/MultiTenantTimeSizeBatchProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/TenantResourceManager.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/LeaveRuleProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/OrdinaryRuleProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/RestRuleProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/StepOutRuleProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/WorkOvertimeRuleProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AppStatisticsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttenceMachineServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceAIServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApprovalSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApproveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBaseSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookOperationLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookRecordServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceChangeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInPicServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInResultServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCloudAlbumServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmDetailsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCustomizeTableServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDailyRuleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsUtilImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalRulesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFixedClassServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceGroupServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceHolidaySettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveRulesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveTypeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingPayrollHoursServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLocationSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineManageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineSyncServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceNoticeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendancePermissionDictServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateItemServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceRepairServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSealSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftNameSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingPeriodServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSuperAdminServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceRecordServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserFaceServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AutoScheduleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ClockInResultServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/CommonSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/DailyRuleChangeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/EnableBalanceServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/InitializationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/IsPerfMachineServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKaiJiaYiStrategyImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKeMiStrategyImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineMaoTongStrategyImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineYuQueStrategyImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleDetailServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/PublicHolidayRulesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RV1109MachineServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RuleScopeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ScheduleGroupRuleConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/SmartPreScheduleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/StatisticsUtilServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceTxServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UsualPhoneServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/WorkstationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/ByEmployeeSchedulesV2Converter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleByEmployeeFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleIncompleteMsgBuilder.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleQueryValidator.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleRedisSupport.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleResultMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleSaveValidator.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionFunctionMenuController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionGradesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionMigrateController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionOrganizeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionPositionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePersonController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePostController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionUsersController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionFunctionMenuMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionMigrateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePersonMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePostMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuRelationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRolePersonUserRelationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionFunctionMenuService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionGradesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionMigrateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionOrganizeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionPositionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePersonService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePostService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuRelationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionUsersService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionFunctionMenuServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionGradesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionMigrateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionOrganizeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionPositionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePersonServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePostServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuRelationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionUsersServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableObject.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/config/FoodSafetyOcrConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumer.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumerSource.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateInstanceController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateManageApiController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateOcrController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateStoreController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateWarningController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateManageController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateReminderController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateRiskController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/NoticeHelper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/OrganizationHelper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppReminderMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppRiskMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateBusinessLicenseExtMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateHygieneLicenseExtMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceItemMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/model/CertificateAppReminderRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppReminderService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppRiskService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateInstanceService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageApiService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateStoreService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppReminderServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppRiskServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateInstanceServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageApiServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateStoreServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/CertificateStatusUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/StoreTypeUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/SubjectNameUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/WorkerStatusUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/AsyncConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/CustomValueFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/GlobalExceptionHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/MqttConfiguration.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/StringToDateConverter.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/TempDevOnlyConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/WebConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/constant/AttendanceStatusConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/apply/FtbCultivatePromotionPostApplyForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseLikeAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/course/FtbCultivateCourseAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/exam/ExamController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/identify/CultivateIdentifyAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateLearnTaskForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateMyLearnTaskForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/offline/FtbCultivateOfflineTrainAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/position/FtbCultivatePositionForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCulProPostForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCultivatePromotionNewForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/statistic/FtbCultivateStatisticsForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/teaching/AppTeachingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/MigrateController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/aisupport/FtbCultivateAiSupportController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/apply/FtbCultivatePromotionPostApplyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/authoritys/FtbCultivatePermissionUserController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/casebase/FtbCultivateCaseBaseController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateImageController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateStatisticController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateUserController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/chapter/FtbCultivateChapterTestStatisticController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseChapterController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseStatisticesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseTypeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/coursepackage/FtbCultivateCoursePackageController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamOldDataController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamReadOverController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamUserController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedCommentController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedLikeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedReaderController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedShareController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/CultivateCultivateIdentifyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/FtbCultivateIdentifyStatisticController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnCategoriesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskContentController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskCountController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskInfoController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskListController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/offline/FtbCultivateOfflineTrainController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/org/FtbCultivateOrgController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/paper/FtbCultivateTestPaperController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionAssessmentController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionCopyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionMemberController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionStatisticesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivateMapsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionMemberController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionNewController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionPostController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateAssessmentPointsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionBankController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/rule/FtbCultivateRuleController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStatisticsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStoreStatisticController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingRecordController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingSkillController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/EventHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JnpfApplicationEventService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JsonToListTypeHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/TriggerProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/course/CourseProcessLearnAbstract.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/CoursePositionTaskTriggerAbstract.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerAbstract.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/PositionTaskTriggerAbstract.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/certificate/JnpfApplicationEventCertificateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventPositionCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/file/JnpfApplicationEventFileServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/course/CommonCourseProcessLearnProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/distribute/JnpfApplicationEventExamIdentificationTrigger.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/CourseTriggerProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionProcessLearnProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionTriggerProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskProcessLearnProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskTriggerProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverCategoryMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverInfoMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamDrawRuleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateFileMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsBackupsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyTableBackupsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyItemsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyTableMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivatePositionCourseLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateUserViewMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedCommentMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedLikeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedReaderMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedShareMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateAssessmentPointsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseLikeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateImagesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateResultMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestOptionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestResultMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCommonSettingGlobalMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseChapterMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseLearningLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCoursePackageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingGlobalMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseStatisticesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTriggerLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTypeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamFrequncyLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamHistoryPaperMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamPaperMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserDetailMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateFileMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyCategoriesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyItemsPoolMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLabelMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnCategoriesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskAssignmentMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCertificateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCourseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskExamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskIdentificationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoContentMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPhaseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPracticeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskReminderRuleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMessageInfoMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMyLearnTaskInfoMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineCourseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineTrainMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePackageCourseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCertificateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceChapterLearningMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceLearningMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseCertificateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseExamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseIdentityMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCoursePracticeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamIdentifyMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionIdentifyResultMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionStatisticesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberNewMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMessageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostApplyMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostNewMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionAnalysisMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankCourseMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionOptionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionPointsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateRuleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateStatisticsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTaskLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperQuestionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperRuleMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingRecordMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingSkillMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingStudentMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverCategoryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverInfoService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateExamSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateFileService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApiService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsBackupsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyTableBackupsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyItemsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyTableService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/ExamFrequencyLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedCommentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedLikeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedReaderService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedShareService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateAssessmentPointsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCaseBaseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateImagesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestOptionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestResultService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseAppService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseChapterService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCoursePackageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseStatisticesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTriggerLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTypeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamHistoryPaperService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserDetailService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnCategoriesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAppContentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAssignmentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCertificateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskExamService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskIdentificationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoContentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoCountService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskListService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskReminderRuleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateMyLearnTaskInfoService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineTrainService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePackageCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionAssessmentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCertificateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCopyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseCertificateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCoursePracticeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamIdentifyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionForAppService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionMemberService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionStatisticesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionMemberService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionNewService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostApplyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionOptionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionPointsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateRuleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStatisticsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStoreStatisticService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperQuestionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperRuleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/PositionCultivateIdentifyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordPracticeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingSkillService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingStudentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/V2TeachingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverCategoryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverInfoServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateExamSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateFileServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApiServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsBackupsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyTableBackupsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyItemsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyTableServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/ExamFrequencyLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedCommentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedLikeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedReaderServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedShareServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateAssessmentPointsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCaseBaseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateImagesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestOptionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestResultServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseAppServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseChapterServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCoursePackageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseStatisticesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTriggerLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTypeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamHistoryPaperServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserDetailServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnCategoriesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAppContentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAssignmentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCertificateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskExamServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskIdentificationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoContentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoCountServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskListServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskReminderRuleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateMyLearnTaskInfoServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineTrainServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePackageCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionAssessmentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCertificateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCopyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseCertificateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCoursePracticeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamIdentifyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionForAppServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionMemberServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionStatisticesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionMemberServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionNewServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostApplyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionOptionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionPointsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateRuleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStatisticsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStoreStatisticsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperQuestionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperRuleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/PositionCultivateIdentifyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordPracticeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingSkillServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingStudentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/V2TeachingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/AsyncExamQuestionUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateDateTimeUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateIdentifyIMUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateImUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnTaskIMUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivatePerUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateTaskLeanAsyncDealUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/HistoryPaperUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/QuestionExcelExportUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserApiV2Util.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserExamUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/config/LocalDisableRocketMqListenerProcessor.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/course/V2CultivateCourseAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/ExamSubController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/V2CultivateUserExamAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/gained/V2CourseGainedCommentController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateIdentifyManagerAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateMyIdentifyAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/offline/V2CultivateOfflineTrainAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2CultivatePositionAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2NextUserCultivatePositionAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/promotion/V2CultivatePromotionAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/task/V2CultivateTaskAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/teaching/V2TeachingAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/aihelper/V2CultivateAiHelperController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/certificate/V2CertificateController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CommonController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CultivateCommonSettingGlobalController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/FtbCultivateCourseSettingGlobalController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseStatisticesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseWebController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamStatisticsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamUserController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyApplyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyCategoriesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyItemsPoolController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/label/FtbCultivateLabelController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/old/V2CultivateOldDealController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/position/V2CultivatePositionWebController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionStatisticController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionWebController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/question/V2CultivateQuestionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/task/V2CultivateTaskWebController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingRecordController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingSkillController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/timing/V2TimingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/CultivateConsumerMQListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/PermissionConsumerMEListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivateExamDrawRuleService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivatePositionCourseLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseLearningLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseSettingGlobalService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyCategoriesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyItemsPoolService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLabelService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPhaseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPracticeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionMemberNewService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionNewMessageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionPostNewService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateTaskLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CourseGainedCommentService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateBatchQueryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCertificateService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCommonSettingGlobalService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseAppService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamStatisticsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyApplyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyTableService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateOfflineTrainService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePositionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePostStudyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePromotionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateQuestionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskCountService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskStudyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2TeachingSkillService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivateExamDrawRuleServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivatePositionCourseLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCommonSettingGlobalServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourceLearningLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourseSettingGlobalServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyCategoriesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyItemsPoolServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLabelServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPhaseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPracticeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionMemberNewServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionNewMessageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionPostNewServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateTaskLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CourseGainedCommentServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateBatchQueryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCertificateServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseAppServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamStatisticsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyApplyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyTableServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateOfflineTrainServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePositionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePostStudyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePromotionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateQuestionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskCountServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskStudyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2TeachingSkillServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateBatchQueryUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateCourseStudyUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateIdentityUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateMqSendUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeDetector.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeResult.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/V2QuestionExcelExportUtil.java rename {ftb/src/main/java/ftb/test => jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture}/controller/CultureClockInController.java (76%) create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CulturePicSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureStatController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureTextSettingController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInStatMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicTempMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureTextSettingMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureAppService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureClockInService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CulturePicSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureStatService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureTextSettingService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureAppServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureClockInServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CulturePicSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureStatServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureTextSettingServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/DateRangeUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/FontUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/ImageComboUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DataReportController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DocLibraryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/SpaceController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/crontask/DocLibraryCronTask.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/CommonConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationAuthorityMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationRubbishMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InfornationRecentlyViewedMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/SpaceMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DataReportService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DocLibraryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/SpaceService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DataReportServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DocLibraryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/SpaceServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ApproveException.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/HandleException.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/QueryException.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ResultException.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeConsumerSource.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeStoreNumConsumer.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/FranchiseeApiController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/app/FranchiseeAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeCustomFieldConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeCustomFieldMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeExperienceMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeJoinRegionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeStoreMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeCustomFieldConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeCustomFieldConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellStyleHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellWriteHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomRowHeightHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/controller/HelpAiLogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/mapper/FtbHelpAiChatLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/FtbHelpAiChatLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/impl/FtbHelpAiChatLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/LogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/WorkLogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogCommentMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberAssociationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberFieldMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateFieldMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/WorkLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/LogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/WorkLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/LogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/WorkLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppMyNoticeAnnouncementsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeAnnouncementsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeCategoriesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeManagerController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeUserGroupsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/ImController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbMyNoticeAnnouncementsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsLogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeCategoriesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeManagerController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeUserGroupsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsReceiveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeCategoriesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeFilesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeManagerMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupMembersMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsReceiveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeCategoriesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeFilesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeManagerService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupMembersService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsReceiveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeCategoriesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeFilesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeManagerServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupMembersServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeAsyncDealUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeIMUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/hepler/MysqlSequenceHelper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/ParamService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/impl/ParamServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/EnvironmentParamConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/TengxunLicenseConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/apply/FtbPersonnelsPostApplyForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskHistoryForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsEmEntryAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsStaffEmploymentApplyAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/formdata/FtbPersonnelsStaffRegistrationFormDataAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/goods/FtbPersonnelsGoodsAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/regular/FtbPersonnelsRegularManagementForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/AppPersonnelsStaffArchivesHistoryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/FtbPersonnelsStaffRosterAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/secondment/FtbPersonnelsSecondmentAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsStaffTransferPositionAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsTransferManageAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/turnover/FtbPersonnelsTurnoverManagementForAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/uchisuike/FtbPersonnelsUchisuikePondAppController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/mcp/FtbPersonStaffMCPController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelChangesController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsBrainDrainController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsOverviewAnalysisController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsTurnoverAnalysisController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/apply/FtbPersonnelsPostApplyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditMasterConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskHistoryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/authoritys/FtbPersonnelsPermissionUserController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistHistoryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistTypeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/compatibility/CompatibilityIssueController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employeetype/FtbPersonnelsEmployeeTypeWebController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsEmEntryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsStaffEmploymentApplyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formdata/FtbPersonnelsStaffRegistrationFormDataController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsPromiseConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsRegistrationFormFieldController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formtype/FtbPersonnelsRegistrationFormTypeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsReceiveController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/growth/FtbPersonnelsStaffGrowthLogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/org/FtbPersonnelOrgController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/recruitmentchannels/FtbPersonnelsRecruitmentChannelsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/regular/FtbPersonnelsRegularManagementController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationCategoryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbPersonnelsRewardsPunishmentsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbRewardsPunishmentsApproveOAController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsContactInfoController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsMetaDataController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffArchivesHistoryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffHomePageController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterImportController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsTrialController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbThousandFacePersonController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnelsRuleConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnlesInfoConfigController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsSalaryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsStaffSalaryChangeLogController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/scheme/FtbPersonnelsStaffRosterSchemeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/secondment/FtbPersonnelsSecondmentController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/shortchain/FtbPersonnelsShortchainController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsStaffTransferPositionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsTransferManageController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/turnover/FtbPersonnelsTurnoverManagementController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/FtbPersonnelsUchisuikePondController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/PersonnelInformationSupplementController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelCommonListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelReadListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisEventListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisNewEventListener.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsRewardsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonStaffMCPMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelChangesMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditCarbonRecipientMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditMasterConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskHistoryMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditSubConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditTaskInfoMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuthoritysMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistHistoryMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistTypeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsReceiveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsIdcardVerificationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsOverviewAnalysisMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPostApplyMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPromiseConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRecruitmentChannelsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldOptionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormTypeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegularManagementMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationCategoryConfigurationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationConfigurationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRewardsPunishmentsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRuleConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSalaryTemporaryStorageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentManagementMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsShortchainMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffArchivesHistoryMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffEmploymentApplyMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffGrowthLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffHomePageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRegistrationFormDataMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterSchemeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffSalaryChangeLogMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionHandoverMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTransferManageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAccountRegistrationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAnalysisMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverHandoverMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverManagementMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondOrgMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoDiyRangeConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoRangeConfigMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbThousandFacePersonMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceBinder.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceMsg.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonStaffMCPService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelChangesService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditCarbonRecipientService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditMasterConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskHistoryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditSubConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditTaskInfoService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuthoritysService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistHistoryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistTypeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBrainDrainService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsContactInfoService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmEntryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmployeeTypeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsReceiveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsMetaDataService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsOverviewAnalysisService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPostApplyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPromiseConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRecruitmentChannelsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldOptionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormTypeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegularManagementService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationCategoryConfigurationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationConfigurationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRewardsPunishmentsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRosterValidService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRuleConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSalaryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSecondmentManagementService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsShortchainService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffArchivesHistoryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffEmploymentApplyService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffGrowthLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffHomePageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRegistrationFormDataService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterImportService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterSchemeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffSalaryChangeLogService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionHandoverService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTransferManageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTrialService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverAnalysisService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverHandoverService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverManagementService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsUchisuikePondService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoRangeConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbRewardsPunishmentsApproveOAService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbThousandFacePersonService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonStaffMCPServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelChangesServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditCarbonRecipientServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditMasterConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskHistoryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditSubConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditTaskInfoServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuthoritysServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistHistoryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistTypeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBrainDrainServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsContactInfoServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmEntryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmployeeTypeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsReceiveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsMetaDataServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsOverviewAnalysisServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPostApplyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPromiseConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRecruitmentChannelsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldOptionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormTypeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegularManagementServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationCategoryConfigurationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationConfigurationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRewardsPunishmentsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceNewImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRuleConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSalaryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSecondmentManagementServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsShortchainServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffArchivesHistoryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffEmploymentApplyServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffGrowthLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffHomePageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRegistrationFormDataServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterImportServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterSchemeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffSalaryChangeLogServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionHandoverServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTransferManageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTrialServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverAnalysisServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverHandoverServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverManagementServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsUchisuikePondServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoDiyRangeConfigService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoRangeConfigServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbRewardsPunishmentsApproveOAServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbThousandFacePersonServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/AuthServerConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CacheExcelUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CompanyAgeUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CovertDateUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/ExcelHeaderUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/FtbPersonnlesIMUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/NoSendContactIMUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonSalaryUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonalizedTenantWhitelistUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncImportUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncOldData.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncServiceUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelCardOcr.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelDataAnalysisUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelIdCardVerificationUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelOrgUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPerUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPreTrailIMUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelStaffUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportThreadPool.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsProperties.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsSendUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsTemplate.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/StaffRosterImportSaveUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/preperties/CustomChannels.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumer.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumerSource.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldCategoryController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldCategoryMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemRelationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldCategoryService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemRelationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldCategoryServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemRelationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/StoreController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/TestController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreRegionMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserRelationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreRegionService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreUserService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreRegionServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreUserServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/StoreCertificatePhotoController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/WarningNoticeController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/helper/StoreCertificatePhotoHelper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/BaseParamMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoItemMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/StoreCertificatePhotoService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/WarningNoticeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/StoreCertificatePhotoServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/WarningNoticeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/controller/TempModuleController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/AppBannerMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/RollImageMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/AppBannerService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/RollImageService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/AppBannerServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/RollImageServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Base64Util.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/CustomTenantUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateConvertUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateRange.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DynDicUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/EasyExcelUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/GenUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Html2Text.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpStatus.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/MultithreadExecutors.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/NumberUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/OptionalUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PageUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ParamUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PingYinUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionAnalysisUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionBankIDGenerator.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RangeConfigUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RedisDistributedLock.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SeetaFaceUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SelfGrowthUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ServiceException.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/TenantUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/V2UserQueryUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ValidatorUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/AttendanceGroupUserStatusUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DailyRuleUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DayStatisticsUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ExpiresTimeUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataModel.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ImageCompressUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MachineStrategyFactory.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttPushClient.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttSender.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/OcsWatermarkUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PermissionUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PushCallback.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleExcelImportUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleScopeUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/SecondmentTypeUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/UnionQualityExcelUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/EasyExcelUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ExcelSelectedResolve.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ShortChainUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImConst.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImMessageNoticeUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/mapper/MybatisUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/permssion/V2Utils.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/v2/personnels/V2PersonnelController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyClockInController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/LeaveApproveController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/RewardsPunishmentsApproveController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/SelfApproveController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/WorkOvertimeApproveController.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceChangeMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceOutsideMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceRepairMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceViolationMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceLeaveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceWorkOvertimeApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/BusinessTripApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/GoOutApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveUserMapper.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceChangeService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceOutsideService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceRepairService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceViolationService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyPicService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/BusinessTripApproveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/FlowTaskService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/GoOutApproveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/LeaveApproveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/PunishmentsApprovalService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/RewardService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/SelfApproveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/WorkOvertimeApproveService.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceChangeServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceOutsideServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceRepairServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceViolationServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyPicServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/BusinessTripApproveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/FlowTaskServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/GoOutApproveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/LeaveApproveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/PunishmentsApprovalServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/RewardServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/SelfApproveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/WorkOvertimeApproveServiceImpl.java create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/application.yml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/bootstrap.yml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/fonts/MiSans-Normal.ttf create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/fonts/MiSans-Semibold.ttf create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/ftb_certificate_instance_upgrade_template_status.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/img/icon-bottom.png create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceAIMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceApproveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceBalanceRecordMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceBalanceUseRecordMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceBaseSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceCardReplacementApproveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceClockInMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceClockInResultMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceConfirmMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceDailyRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceDayStatisticsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceFestivalRulesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceFieldPersonnelMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceFieldpersonnelApproveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceFixedClassMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceGroupMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceGroupUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceLeaveApproveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceLeaveGrantSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceLeaveRulesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceLeaveSettingsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceLeaveTypeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceLocationSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceMachineLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceMachineManageMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceManagerPermissionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceNoticeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceQuickTemplateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceRepairMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceSelfApproveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceShiftNameSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceUserBalanceMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceUserBalanceRecordMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceUserConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceUserFaceMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/AttendanceUserSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/ClockInResultMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/ClockInResultV2Mapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/DailyRuleChangeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/DailyRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/EnableBalanceMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/FtbAttendanceFaceChangeLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/FtbScheduleGroupDrawingParamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/FtbScheduleGroupFixedParamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/InitializationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/OvertimeRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/PublicHolidayRulesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/RuleScopeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/StatisticsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/StorageRestMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/attendance/mapper/UserPhoneMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionFunctionMenuMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionMigrateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionRoleAuthorizePersonMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionRoleAuthorizePostMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionRoleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionRoleMenuMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/authority/mapper/FtbPermissionRoleMenuRelationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/certificate/mapper/CertificateAppReminderMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/certificate/mapper/CertificateAppRiskMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/certificate/mapper/CertificateBusinessLicenseExtMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/certificate/mapper/CertificateHygieneLicenseExtMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/certificate/mapper/CertificateInstanceItemMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/certificate/mapper/CertificateInstanceMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateCourseMsgMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateCourseMsgUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateCoverInfoMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateExamDrawRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateExamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateFileMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateIdentifyApplyMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateIdentifyItemsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivateIdentifyTableMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/CultivatePositionCourseLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCourseGainedCommentMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCourseGainedLikeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCourseGainedMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateAssessmentPointsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCaseBaseLikeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCaseBaseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCertificateImagesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCertificateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCertificateUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateChapterTestMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateChapterTestOptionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateChapterTestResultMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCommonSettingGlobalMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseChapterMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseLearningLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCoursePackageMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseSettingGlobalMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseStatisticesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateCourseTypeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateExamFrequncyLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateExamHistoryPaperMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateExamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateExamUserDetailMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateExamUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateFileMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateIdentifyCategoriesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateIdentifyItemsPoolMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLabelMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnCategoriesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskAssignmentMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskCertificateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskCourseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskExamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskIdentificationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoContentMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskPhaseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskPracticeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateLearnTaskReminderRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateMyLearnTaskInfoMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateOfflineCourseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateOfflineTrainMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateOfflineUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePackageCourseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCertificateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCourceChapterLearningMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCourceLearningMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCourseCertificateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCourseExamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCourseIdentityMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCourseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionCoursePracticeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionExamIdentifyMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionExamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionIdentifyResultMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionStatisticesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePositionUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionMemberMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionMemberNewMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionNewMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionPostApplyMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionPostMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionPostNewMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivatePromotionUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateQuestionBankCourseMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateQuestionBankMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateQuestionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateQuestionOptionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateQuestionPointsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateStatisticsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateTestPaperMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateTestPaperQuestionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/FtbCultivateTestPaperRuleMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/TeachingApproveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/TeachingRecordMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/TeachingSkillMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/cultivate/mapper/TeachingStudentMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/culture/mapper/CultureClockInMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/culture/mapper/CultureClockInStatMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/culture/mapper/CulturePicSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/culture/mapper/CulturePicTempMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/culture/mapper/CultureTextSettingMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/document/空白PPT.pptx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/document/空白文档.docx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/document/空白表格.xlsx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/CommonConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/InformationAuthorityMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/InformationLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/InformationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/InformationRubbishMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/InfornationRecentlyViewedMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/doclibrary/mapper/SpaceMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/franchisee/mapper/FranchiseeCustomFieldMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/franchisee/mapper/FranchiseeExperienceMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/franchisee/mapper/FranchiseeJoinRegionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/franchisee/mapper/FranchiseeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/LogCommentMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/LogMemberAssociationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/LogMemberFieldMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/LogMemberLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/LogTemplateFieldMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/LogTemplateMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/memberLog/mapper/WorkLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeAnnouncementsLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeAnnouncementsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeAnnouncementsReceiveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeCategoriesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeFilesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeManagerMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeUserGroupMembersMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/notice/mapper/FtbNoticeUserGroupsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonStaffMCPMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelChangesMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuditCarbonRecipientMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuditMasterConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskHistoryMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuditSubConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuditTaskInfoMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsAuthoritysMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsBlacklistHistoryMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsBlacklistMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsBlacklistTypeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsGoodsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsGoodsReceiveMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsIdcardVerificationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsOverviewAnalysisMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsPermissionUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsPermissionsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsPostApplyMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsPromiseConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRecruitmentChannelsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldOptionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormTypeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRegularManagementMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsResignationCategoryConfigurationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsResignationConfigurationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRewardsPunishmentsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsRuleConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsSalaryTemporaryStorageMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsSecondmentConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsSecondmentManagementMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsShortchainMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffArchivesHistoryMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffEmploymentApplyMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffGrowthLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffHomePageMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffRegistrationFormDataMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffRosterMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffRosterSchemeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffSalaryChangeLogMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionHandoverMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsTransferManageMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsTurnoverAccountRegistrationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsTurnoverAnalysisMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsTurnoverHandoverMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsTurnoverManagementMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondOrgMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnlesInfoConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnlesInfoDiyRangeConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbPersonnlesInfoRangeConfigMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/personnels/mapper/FtbThousandFacePersonMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/qualifications/mapper/QualificationsFieldCategoryMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/qualifications/mapper/QualificationsFieldMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/qualifications/mapper/QualificationsMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/store/mapper/StoreMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/store/mapper/StoreUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/store/mapper/StoreUserRelationMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/storecertificatephoto/mapper/BaseParamMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoItemMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/tempmodule/mapper/AppBannerMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/tempmodule/mapper/RollImageMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/workflow/mapper/ApplyAttendanceChangeMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/workflow/mapper/ApplyAttendanceRepairMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/workflow/mapper/PunishmentsApprovalMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/workflow/mapper/RewardApprovalMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/jnpf/workflow/mapper/SelfApproveUserMapper.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/logback-spring.xml create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/question/questionExportTemplate.xlsx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/question/questionTemplate.xlsx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/roster/rewards.xlsx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/roster/roster.xlsx create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/certificate/ftb_certificate_instance.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/certificate/ftb_certificate_instance_upgrade_health_certificate_data.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/franchisee/base_dictionary_franchisee_nature.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/franchisee/base_dictionary_industry_param.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/franchisee/ftb_franchisee.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/franchisee/ftb_franchisee_batch_insert_5000_with_region.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/ftb_certificate_instance_upgrade_business_license_from_base_organize.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/ftb_certificate_instance_upgrade_org_store_missing.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/ftb_store_add_store_type_franchisee.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/storecertificatephoto/base_param.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/sql/storecertificatephoto/ftb_store_certificate_photo.sql create mode 100644 jnpf-ftb/jnpf-ftb-biz/src/main/resources/startup-optimization-plan.md create mode 100644 jnpf-ftb/jnpf-ftb-entity/pom.xml create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/annotation/Sensitive.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/annotation/check/CheckLength.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/annotation/check/CheckListSize.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/annotation/check/CheckNull.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/constants/AttendanceConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/constants/AttendancePermissionConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/constants/MessageTopicConstants.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/constants/QualificationsConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/constants/RedisConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/constants/ShareMsgConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/easyexcel/DynamicHeaderHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/easyexcel/EasyEnumNames.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/easyexcel/EasyEnumNamesConverter.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/easyexcel/FastJsonEnumConverter.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/easyexcel/StaffDateStringConverter.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/ApiCallLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/AppBanner.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/AttendanceApprovalSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/AttendanceCustomizeTable.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/AttendanceGroup.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/AttendanceGroupUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/AttendanceManagerPermission.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/BaseEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/BaseInsertEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/CommonConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/ContractEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/Information.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/InformationAuthority.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/InformationLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/InformationRubbish.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/InfornationRecentlyViewed.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/PatrolTaskEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/PermissionDict.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/RollImage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/SharingSquareVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/StoreEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/StoreRegion.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/StoreUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/StoreUserRelation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/Workstation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/WorkstationUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/ApplyParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AppointPermission.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceAppSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceAppUserSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceApprovalAdminVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceBalanceRecordEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceBalanceUseRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceBaseSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceBookConfigEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceBookOperationLogEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceBookRecordEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceClockInPic.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceClockInResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceCloudAlbum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceCommonSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceConfirm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceConfirmDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceConfirmSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceDayStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceEnableBalance.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceFestivalRules.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceFestivalSettingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceFieldPersonnel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceFixedClassEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceHolidaySettingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceLateInLateOut.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceLeaveGrantSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceLeaveRules.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceLeaveType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceLocationSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceMachineLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceMachineManage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceMachineSync.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceNoticeEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceOvertimeRule.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceOvertimeRuleDetail.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendancePublicHolidayRules.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceQuickTemplateEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceQuickTemplateItemEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceRepair.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceResultRollback.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceRuleScope.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceSealSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceShiftNameEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceShiftSettingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceShiftSettingPeriodEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceStorageRest.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceUserBalanceDetail.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceUserFace.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceUserFingerprint.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/AttendanceUserPhone.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/DailyRuleChange.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbAttendanceClockIn.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbAttendanceDailyRule.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbAttendanceFaceChangeLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbAttendanceLineSchedulingConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbAttendanceLineSchedulingPayrollHours.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbScheduleGroupDrawingParamEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbScheduleGroupFixedParamEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbScheduleGroupRevenueStaffingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbScheduleGroupTimeslotEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/FtbScheduleGroupTimeslotStaffingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/attendance/LeaveParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivateIdentifyApply.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivateIdentifyApplyDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivateIdentifyApplyDetailsBackups.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivateIdentifyApplyTableBackups.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivateIdentifyItems.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivateIdentifyTable.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/CultivatePositionCourseLogEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/FtbCultivateCourseLearningLogEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/TeachingRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/TeachingSkill.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/cultivate/TeachingStudent.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/culture/CultureClockIn.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/culture/CultureClockInStat.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/culture/CulturePicSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/culture/CulturePicTemp.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/culture/CultureTextSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/qualifications/Qualifications.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/qualifications/QualificationsField.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/qualifications/QualificationsFieldCategory.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/qualifications/QualificationsFieldItem.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/qualifications/QualificationsFieldItemRelation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingAphorismEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingAssessmentPointsEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCertificateEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCertificateUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseChapterContentEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseChapterEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseChapterUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseGainedCommentEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseGainedEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseGainedLikeEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseGainedReaderEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseGainedShareEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingCourseUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingExamDetailOptionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingExamEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingExamUserDetailEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingExamUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupBaseSettingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupClockEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupCourseCorrEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupDynamicEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupEventEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupScoreEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingGroupUserCorrEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingIdentifyTemplateEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingIdentifyTemplateOptionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingIdentifyUserDetailEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingIdentifyUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingKnowledgeBaseCourseEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingKnowledgeBaseEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingKnowledgeBasePositionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingPositionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingPositionLearningDetailEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingPositionLearningEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingQuestionAnalysisEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingQuestionClassifyEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingQuestionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingQuestionOptionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingQuestionPointsEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingReadingLogEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingTestPaperEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingTestPaperQuestionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/training/TrainingTestPaperRuleEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/ApplyAttendanceChange.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/ApplyAttendanceOutside.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/ApplyAttendanceRepair.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/ApplyAttendanceViolation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/AttendanceBusinessTripApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/AttendanceGoOutApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/AttendanceLeaveApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/AttendanceWorkOvertimeApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/PunishmentsApproval.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/PunishmentsApprovalUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/RewardApproval.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/RewardApprovalUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/entity/workflow/SelfApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/AppraisalScoreTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/ClockTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/PermissionSourceCategoryEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/ReadingStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/SensitiveTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/UniAppMpTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ActionEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ApplyTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ApprovalPermissionTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ApprovalSettingTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/AttendanceNoticeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/AttendanceOutTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/AttendanceStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/AttendanceTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/BalanceEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/CautionEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ClockInStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/CloudAlbumTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/GroupUserTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/LeaveTimeTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/LeaveTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/LeaveUnitEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/MachineEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/NoticeModuleEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/OvertimeType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/PeriodTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/PermissionTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/PuncheTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ScheduleImportEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/SchedulesTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/ScopeBizType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/SecondmentTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/SettingGroup.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/StatisticsEnumUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/TriggerSceneEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/UserSettingEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/UserSettingTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/VoucherTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/v2/ClockOutHandleParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/v2/FuncCodingEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/attendance/v2/WorkBoundaryCoverageEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/ApplyResultEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/ApplySourceEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/ApplyStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/ApplyTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/BusinessScenarioTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/CultivateIsSystemEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/ExamQuestionShuffleEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/ExamRetakeFrequencyEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/IdentifyItemExceptionEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/IdentifyScoreTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/StudyStatsEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/TeachingRecordTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/course/CourseTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/task/TaskAssignmentRuleEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/task/TaskStateEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/v2/LearnTaskStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/cultivate/v2/mq/CultivateTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memberLog/MemberLogContentEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memberLog/MemberLogImEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memberLog/MemberLogImJumpEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memoo/Authority.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memoo/DeleteMark.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memoo/PushState.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memoo/QueryTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memoo/SelectType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/memoo/UserDel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/personnel/FtbPersonnelsCheckStatusCodeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/personnel/PersonnelFormDataSystemRosterFields.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/personnel/PhoneStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/personnel/SalaryApplyTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/training/CourseEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/training/GroupEventTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/enums/training/GroupScoreTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/ChapterEndGainedVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/ClockResultVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/ContractForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/ContractInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/ContractListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/GroupBaseSettingBody.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/GroupClockForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/GroupCommentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/GroupDynamicVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/GroupEventAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/GroupGainedVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/PartUserInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/IdentifyInfoForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/IdentifyInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/PositionLearningAppListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/PositionLearningCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/PositionLearningExamVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/PositionLearningIdentifyDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/PositionLearningIdentifyVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/app/TrainingCourseChapterVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AddFieldPersonnelDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppStatisticsListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppStatisticsMoreDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppStatisticsMoreInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppStatisticsRecordDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppStatisticsTeamListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppTeamStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppTeamStatisticsListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppTeamStatisticsTabDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppTeamTabStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AppUserSettingQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceAppUserSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceApprovalSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceBaseSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceBookConfigAddUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceBookConfigDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceBookConfigQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceBookRecordDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceFestivalRulesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceFestivalSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceGroupChargeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceGroupOrgDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceGroupStatusDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceHolidaySettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceLeaveGrantSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceLeaveRulesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceLeaveRulesQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceLeaveSettingsQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceLocationSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendancePublicHolidayRulesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceReqDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceSecondedDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceShiftDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceShiftPeriodDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceUserBalanceDetailDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceUserBalanceDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/AttendanceUserBalanceListQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/BalanceQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/BatchAttendanceBookRecordDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/BatchSaveGroupAdmin.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/BookRecordDayListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/BookRecordMonthListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/BookRecordMonthStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/CancelPhoneDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ClockInDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/CloudAlbumBatchSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ConfirmPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ConfirmSettingSubmitDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ConfirmStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayPayrollStatisticsPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayStatisticsDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayStatisticsDataInitDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayStatisticsDataPageListQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayStatisticsExportDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DayStatisticsPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/DurationForOaDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/EnableUpdateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ExportUserTemplateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FaceChangeQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FaceQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FestivalDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FestivalRulesQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FixedBaseDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FixedClassChangeStatusDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FixedClassDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FixedClassGroupDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/FixedClassSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GetConfirmMonthDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GetValidUsersDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GoOutApproveForOaDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GrantBalanceDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GroupFilterDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GroupOaDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GroupQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/GroupUserQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/JoinUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LatticeStatisticsVoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LeaveApproveForOaDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LeaveQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LeaveQueryForOaDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LeaveRemarkSummaryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LineDrawingPeriodDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LineDrawingSchedulesConfigDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/LineDrawingUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MachineDealDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MachineDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MachineQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MachineUpdateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthAutoSealSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthPayrollStatisticsPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthSealPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthSealSubmitDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthStatisticsDataQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthStatisticsExportDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthStatisticsNoticeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthStatisticsPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MonthUnSealSubmitDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/MouthStatisticsDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/NoApprovalDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/NoticeContentInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/NoticeSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/OperationLogQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/OrgTeamDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/OvertimeRuleDetailDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/OvertimeRuleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/OvertimeRuleQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/PeriodConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/PersonnelApiInfoPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/PersonnelApiInfoSinglePageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ProcessHistoryDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/QueryAttendanceGroupDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/QueryGroupStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/QueryStatisticsInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/QuickTemDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/QuickTemplateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SalaryAttendanceSupportDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SalaryFtbStaticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SaveAttendanceGroupDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SaveForStoreDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SaveGroupAdmin.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SavePermissionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SaveSuperAdminDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SchedulesDaySetDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SchedulesImportDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SchedulesSetDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SchedulesSetItemDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SecondCheckDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/SelfApprovePassForOa.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ShiftNameDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/ShiftNameQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/StatisticsDataQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/StorageRestDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/TeamMonthStatisticsNoticeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/TestDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UnifiedSchedulesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UpdateChangeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UpdateShiftConfigDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UpdateStoreDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserDaySchedulesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserDayShiftQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserFaceDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserOrgDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserSortModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UserWorkSituationDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UsualPhoneDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UsualPhoneQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/UsualPhoneSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WebStatisticsDetailsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WebStatisticsExportDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WebStatisticsListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WorkflowImQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WorkstationQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WorkstationSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WorkstationUserAddDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/WorkstationUserRemoveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/scheduling/FixedSchedulingRuleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/scheduling/LineSchedulingRuleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/scheduling/PreScheduleDayRevenueDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/scheduling/PreScheduleTableQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/dto/scheduling/ScheduleGroupRuleConfigDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/event/StatisticsBatchClearDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/event/StatisticsSingleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/event/StatisticsSingleHistoryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AbsenceCardRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AbsenceDetailModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AbsenceRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AdminUpdateModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AdminUpdateNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AttendanceChangeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AttendanceChangeNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AttendanceGroupParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/AttendanceNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/BalanceRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/BatchNumberResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/BeforeClockInModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/BusOrOutRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockClassRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockGroupRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockInResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockInResultRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockInResultRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ClockUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ConsecUnscheduledModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ConsecUnscheduledNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ConsecutiveAbsenceModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ConsecutiveAbsenceNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DayClockRange.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DayInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DayLeaveTypeJsonData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DayLeaveTypeJsonPayData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DayNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DaySettlementModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/DetermineUserClockRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/EarlyLeaveRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/FastClockInNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/GroupInfoModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/GroupStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/HolidaysTypeJsonData1.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/JsonHandler.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/JumpUrl.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LateRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveApprovalModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveBalanceInfoModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveSituationData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveStatisticsTotal.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveTypeJsonData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveTypeJsonHead.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveTypeStaDetailsModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveTypeStaModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LeaveTypeTimes.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LineShiftChangeNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/LockModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/MakeUpCardRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/MonthNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/MonthSettlementModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/NoClockInModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/NoClockInNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/NoticeConfirm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/NoticeDetailModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/OrganizeUserConsumerDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/OutworkRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/OvertimeBalanceModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/OvertimeHolidaysInfoModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/OvertimeRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/PersonDetail.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ProcessResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ProcessTimeResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/QuickClockInModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/RealityAttendanceOneDays.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/RemindClockInNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/RuleChangeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/RuleChangeNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SalarySupportDayModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SalarySupportDayStaModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ScheduleImportFailModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ScreenUserIdList.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SecondApproveModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SecondRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SettlementNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ShiftChangModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ShiftChangeNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/ShouldAttendDaysData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SituationData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SurplusDaysModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SystemNoticeStrategy.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SystemTypeChangeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/SystemTypeChangeNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/TeamMonthNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/TeamMonthSettlementModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/TimeJson.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/TurnoverConsumerModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UnLockModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserAssociationGroupData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserChangModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserChangeNoticeModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserClockRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserClockTimeSpan.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserDayRuleData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserDaySituationData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserInGroupLeaveInfoData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserLeaveApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserLeaveInfoData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserRuleRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserSecondRecord.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserShouldAttendDays.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserStaDayRuleData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/UserStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/WebExportModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/WebExportNewDetailsModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/model/WebExportNewModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AbnormalClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AbsenceClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsMoreInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsMoreVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsTeamListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsTeamTabListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppStatisticsTeamTabVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppTeamStatisticsListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AppTeamStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ApplyResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ApprovalGroupQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ApprovalQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceBalanceRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceBaseSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceBookConfigVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceFestivalSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceGroupAdminVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceGroupChargeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceGroupInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceGroupUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceGroupVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceHolidaySettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceLeaveApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceLeaveSettingsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceLocationSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceMachineManageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceManagerDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceOrgOrGroupVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendancePermissionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceQuickTemplateItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceQuickTemplateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceResultOptionGroupVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceResultOptionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceSelfApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceShiftSettingPeriodVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceShiftSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/AttendanceWorkOverTimeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BatchOperationResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookConfigCreatorVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookPersonnelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookRecordDailyLatticeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookRecordDayListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookRecordDayVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookRecordMonthDayVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookRecordMonthListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookRecordMonthStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/BookScopeUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ChangeClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ChangeInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ChangeLogVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ClassesDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ClockInMethodVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ClockInPicVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ConditionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/CreateDayStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/CurUserPermissionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyApprovalVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyBaseInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyClockInDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyRuleResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DailyTotalDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DayReceivableRevenueVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/DayShiftRevenueStatVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/FaceMiniVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupApprovalVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupAttendanceStaticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupNodeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupRepairVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/GroupUserLineScheduleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/HolidayOptionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/HolidayTypeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/KeyValueVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LeaveRemarkItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LeaveRemarkSummaryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LeaveShiftVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LeaveStaVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LeaveTypeStatisticVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LeaveTypeStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/LineSchedulesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/MachineScopeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/MethodVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/MiniGroupVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/MockVideoInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/MonthRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/NextRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/NoticeConfirmListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/NoticeConfirmVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/NoticeContentInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/OldDayStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/OperationLogPageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/OvertimeVouchersVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/PeriodNameVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/PermissionDictVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/QueryGroupAdminVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/QuickPeriodsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/RemindClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/RepairRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/RuleAndClockVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/RuleDayVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/RuleUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/SalaryFtbStaticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ScheduleRuleDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/SchedulesDayVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/SchedulesItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/SchedulesV2Vo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/SchedulesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/SecondmentDateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ShiftAndLeaveRules.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ShiftPeriodVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ShiftPostHeadcountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/ShiftPostStatVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/TestVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UnifiedSchedulesResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserAttendanceStaticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserBalanceVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserClassesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserDayShiftInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserDayVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserFaceDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserFaceVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserInGroupInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/UserSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/VacationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/WebStatisticsListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/WorkstationDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/WorkstationGroupUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/WorkstationUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/WorkstationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/WorkstationWithUsersVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ApproveBaseImVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ApproveImVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceApproveImVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceCustomizeTableVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceDimensionDayVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceDimensionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceFestivalRulesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceLeaveGrantSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceLeaveRulesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendancePublicHolidayBalance.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendancePublicHolidayRulesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceStorageRestVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceToThousandsFacesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceUserBalanceDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceUserBalanceListDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceUserBalanceListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceUserBalanceVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AttendanceUserTitleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/AverageTrendPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/BalanceTitelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/BalanceUseRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/BaseConfirmInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/BusinessTripVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ClockDataReqVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ClockInExportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ClockRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmDetailsInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmDetailsInfoNew.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmDetailsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmListQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmSettingInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ConfirmStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/CustomizeTableUpdateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DailySituationPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayPayrollStatisticsPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayPayrollStatisticsQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsNoticeQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsQueryDbVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsShiftsJsonVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/DayStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ExceptionSituationPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/FieldPersonnelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/FixedClassShiftVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/FullAttendanceStatusPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/GoOutVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/GroupCheckVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/GroupLockVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/GroupMiniVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/GroupShiftTimeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/GroupUserMiniVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/JoinGroupVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LatticeStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LeaveConsumptionDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LeaveDeductDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LeaveDeductWagesJsonDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LeaveDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LeaveRuleBalanceVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LeaveRuleFormulaVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/LogMiniVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthAutoSealSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthConfirmStatisticsListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthPayrollStatisticsPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthPayrollStatisticsQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthSealPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatisticsNoticeQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatisticsPageListExportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatisticsPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatisticsQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsAbnormalConditionQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsDailySituationQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsDetailsQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsFullSituationQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsHoursRankingQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsOvertimeSituationQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/MonthStatsPerCapitaQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OutOrBusApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeRuleDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeRulePageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeSalaryHoursJsonVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeSalaryHoursVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/OvertimeSituationPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/PersonnelTrendPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/PublicHolidayTransferListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/PunchesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QueryGroupStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QueryStatisticsInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QueryUserOvertimeDetailsDbVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QueryUserOvertimeDetailsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QueryUserOvertimeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QuickCheckInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/QuickTemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/RemarkVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/SalaryAttendanceSupportQuery.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/SalaryAttendanceSupportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/SealUserInfoDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/SecondedUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ShiftNameListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ShiftNameViewVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ShiftNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ShiftRotationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/ShiftsJsonClockInResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/StatisticsDataQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/TeamMonthStatisticsNoticeQueryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/TimeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserConfigVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserDailySealVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserPermissions.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserRuleListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserTenantVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserWorkSituationDbVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UserWorkSituationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/UsualPhonePageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/WorkHoursRankingPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/attendance/YesterdayRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/event/OvertimeEvent.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/flow/FlowTaskVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/flow/HandlerVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/permission/ActionPermissionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/permission/ApprovalSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/permission/AttendanceTeamSetVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/permission/ConfigPermissionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/permission/app/ManagerPermissionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/FixedSchedulingRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/LineSchedulingRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreSchedulePostVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleShiftPostVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleShiftStaffVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleShiftVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleStationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleTableRowVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleTableVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/PreScheduleUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/attendance/vo/scheduling/ScheduleGroupRuleConfigVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/authorize/FtbPermissionAuthorizeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/menu/FtbPermissionFunctionMenuDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/menu/FtbPermissionMenuDirectoryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/menu/FtbPermissionMigrareRoleAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/menu/FtbPermissionRoleAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/menu/FunctionMenuRemoteDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/menu/FuntionMenuDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/permission/AuthGetTargetUserInfoBatchDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/permission/OrganizeWithPositionsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/person/FtbPermissionRoleBatchDeleteDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/person/FtbPermissionRolePersonAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/person/FtbPermissionRolePersonRelationDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/person/FunctionDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/post/FtbPermissionRolePostAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/role/FtbPermissionDataDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/role/FtbPermissionPositionAuthorizationDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/role/FtbPermissionPositionMenuDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/role/FtbPermissionRoleCopyDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/role/FtbPermissionRoleInfoDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/dto/role/FtbPermissionRoleUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionFunctionMenu.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionRole.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionRoleAuthorizePerson.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionRoleAuthorizePost.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionRoleMenu.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionRoleMenuRelation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/po/FtbPermissionRolePersonUserRelation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionFunctionMenuVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionMenuConfigTree.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionMenuConfigTreeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionMenuDirectoryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionMigrateRoleTree.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionMigrateRoleTreeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionModuleInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/menu/FtbPermissionModuleVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/person/FtbAuthorizedPersonnelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/person/FtbEmployeePermissionPersonnelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/person/FtbRoleListDropDownVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/person/FtbUserPermissionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/person/FunctionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/post/FtbAuthorizedPostVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/role/FtbPermissionPositionMenuVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/role/FtbPermissionRoleDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/role/FtbPermissionRoleIdentificationVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/role/FtbPermissionRoleInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/authority/vo/role/FtbPermissionRolePersonVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/dto/EmployeeOrganizeDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/dto/HealthCertificateStatusDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/dto/OptionDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/po/CertificateBusinessLicenseExtEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/po/CertificateHygieneLicenseExtEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/po/CertificateInstanceEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/po/CertificateInstanceItemEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/po/CertificateWarningLogEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateFoodSafetyOcrReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateHealthManageQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateInstanceAddReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateInstanceItemReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateInstanceQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateInstanceUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateStoreDashboardReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateStoreManageQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateStoreSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/CertificateSyncHealthReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppBatchRemindReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppBusinessLicenseUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppEmployeeRiskQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppHealthCertificateUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppHygieneLicenseUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppRiskChartReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppSingleRemindReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppStoreCustomUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/req/app/CertificateAppStoreRiskQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateFoodSafetyOcrVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateHealthDashboardStatusStatVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateHealthManageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateInstanceItemVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateInstanceVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateOrganizeBusinessLicenseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreAndCertificatesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreCustomStatusBarVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreCustomStatusPieVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreCustomStatusTableVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreDashboardVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreManageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateStoreTabVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/CertificateTypeOptionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppBusinessLicenseDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppCertificateDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppEmployeeRiskVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppHygieneLicenseDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppRiskChartVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppRiskReminderCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppStatusCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppStoreCustomDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppStoreRiskItemVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/CertificateAppStoreRiskVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/HealthCertificateDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/certificate/vo/app/HealthCertificateVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/common/CheckVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/common/DateRangeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/common/DateRangeDto1.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/common/PageDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/CultivatePage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/bo/FtbCultivateCourseBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/bo/FtbCultivatePositionExamBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/bo/TriggerEventBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/FtbCultivateCommonSettingGlobalDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/FtbCultivateCourseSettingGlobalDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/apply/FtbCultivatePromotionPostApplyCreateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/apply/FtbCultivatePromotionPostApplyDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/base/FtbCultivateBaseOrgWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/base/FtbCultivateBasePersonWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/casebase/FtbCultivateCaseBaseAuditDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/casebase/FtbCultivateCaseBaseDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/casebase/app/FtbCultivateCaseBaseCreatDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/certificate/FtbCertificateForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/certificate/FtbCertificateOrgWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/certificate/FtbCertificatePersonWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/certificate/FtbCertificateUserForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCommonKeyAndValDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseChapterDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseChapterUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseOrgStatisticsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCoursePersonStatisticesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseSettingDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseTypeDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseTypeUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateCourseUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateSelectPositionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/FtbCultivateShelvesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/app/FtbChapterStudyDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/app/FtbCultivateCourseMsgForAppDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/app/FtbGlobalCurriculumAppWrapDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/app/FtbManuallyTriggerExamsQualificationsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/app/FtbUserLevelMessageReadDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/testoption/FtbCultivateChapterTestDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/course/testoption/FtbCultivateChapterTestOptionDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/coursepackage/FtbCultivateCoursePackageAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/coursepackage/FtbCultivateCoursePackageQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/coursepackage/FtbCultivateCoursePackageUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/gained/FtbAppAttentionPage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/gained/FtbCommentPagination.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/gained/FtbGainedPagination.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/gained/FtbGainedQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/gained/FtbLikePagination.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/gained/FtbShareInterestEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/ApplyDataResultNotifyDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/BatchIdentifyApplySaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/FtbIdentityOrgWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/FtbIdentityPersonWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplyDataPushDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplyDeleteAppDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplyItemsSubmitDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplyListAppDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplyListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplyReIdentifyDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplySaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplySetTimeAppDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyApplySubmitDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyTableInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyTableListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyTableSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyTableUpdateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/IdentifyUserInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/identify/UserIdentifyPageDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/BatchCommonCountDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnAllocationDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnCategoriesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskCertificateInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskCourseInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskExamInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskIdentificationInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskInfoExprotDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/FtbCultivateLearnTaskListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/NeedAlertUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/learn/NeedPerDayAlertDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbCultivateOfflineFileDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbCultivateOfflineTrainChangeDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbCultivateOfflineTrainDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbCultivateOfflineTrainPeopleSigningInDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbCultivateOfflineTrainSignInDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbCultivateOfflineTrainUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/offline/FtbModifyOfflineTrainingMembersDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCourseDropDownListDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePersonStatisticesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionCourseLevelPageDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionCoursePageDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionExamIdentifyDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionJobLearnCertificateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionJobLearnDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionJobLearnExamDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionLearingCourselDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionLearnPracticalDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionOrgStatisticesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionPersonStatisticesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbCultivatePositionShelvesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbJobLearningPaginDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbReselectionAppraisalDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbRetakeExamDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbStudyCourseExamAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/FtbStudyCourseIdentityAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/app/FtbCultivatePositionForAppDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/app/FtbCultivatePositionForAppNewDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/web/FtbCultivateAssociatedExamsCoursesDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/web/FtbCultivatePositionCopyDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/web/FtbCultivatePositionJobLearnCourseCertificateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/web/FtbCultivatePositionJobLearnCourseStateSwitchDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/web/FtbCultivatePositionPassExaminationDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/position/web/FtbJobLearnCourseRetakeCertificateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbCultivateCreatMeMapInfoDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbCultivatePromotionCreatDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbCultivatePromotionCreatNewDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbCultivatePromotionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbCultivatePromotionForAppDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbCultivatePromotionMemberDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbMapsOrgWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/promotion/FtbMapsPersonWisdomStatisticDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/rule/FtbCultivateRuleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/CultivatePermissionModuleParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/CultivateStatisticsCommonParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/CultivateUserExamCountDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/ExamStatisticsForOrgDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/ExamStatisticsForPersonDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/FtbCultivateStatisticsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/InnerCultivateStatisticsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/InnerExamStatisticsForOrgDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/statistics/InnerExamStatisticsForPersonDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/FtbCultivateStoreStatisticsReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/StoreIdentityReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/StoreMyTaskReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/StoreOfflineTrainReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/StoreStatisticsCommonReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/StoreStatisticsMyExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/StoreStatisticsWaitMyCheckExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/dto/StoreCultivateTaskDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/storestatistics/dto/StoreOfflineTrainDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/EmployeePageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/MyRecordPageListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/QueryTeachingRecordDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/RecordBatchDeleteDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/RecordQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/RecordSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/RecordUpdateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingApproveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingBaseFilter.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingSaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingSkillAddDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingSkillPageDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingSkillSortDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/TeachingSkillUpdateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/dto/teaching/ViewDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/entiy/BaseEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/JnpfApplicationEvent.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/CourseTriggerEventDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/certificate/CertificateEventDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/course/CommonCourseProcessLearnDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/course/CourseEventDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/course/CourseProcessLearn.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/course/PositionCourseEventDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/examidentify/ExamIdentifyTriggerEventDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/file/FileEventDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/position/PositionCourseProcessDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/event/dto/task/TaskCourseProcessDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateAssessmentPoints.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateCommonSettingGlobal.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateCourseSettingGlobal.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateFile.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateIdentifyCategories.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateIdentifyItemsPool.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivatePositionCourseCertificate.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/FtbCultivateRule.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/apply/FtbCultivatePromotionPostApply.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/casebase/FtbCultivateCaseBase.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/casebase/FtbCultivateCaseBaseLike.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/certificate/CertificateUserPagination.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/certificate/FtbCertificateEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/certificate/FtbCertificateImagesEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/certificate/FtbCertificateUserEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/common/FtbCultivateCoverCategoryEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/common/FtbCultivateCoverInfoEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateCertificateResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateChapterTest.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateChapterTestOption.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateChapterTestResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateCourse.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateCourseChapter.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateCourseSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateCourseTriggerLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/FtbCultivateCourseType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/app/CultivateCourseMsg.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/course/app/CultivateCourseMsgUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/coursepackage/FtbCultivateCoursePackage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/coursepackage/FtbCultivatePackageCourse.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExamFrequencyLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExamHistoryPaper.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExamPaper.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExamSettingEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExamUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/FtbCultivateExamUserDetail.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/exam/InnerAnalysisOrgInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/gained/FtbAppAttentionListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/gained/FtbCourseGainedCommentEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/gained/FtbCourseGainedEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/gained/FtbCourseGainedLikeEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/gained/FtbCourseGainedReaderEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/gained/FtbCourseGainedShareEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/label/FtbCultivateLabel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnCategories.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTask.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTaskAssignment.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTaskCertificate.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTaskCourse.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTaskExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTaskIdentification.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/learn/FtbCultivateLearnTaskReminderRule.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/mesgg/CultivateMessageInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/offline/FtbCultivateOfflineCourse.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/offline/FtbCultivateOfflineTrain.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/offline/FtbCultivateOfflineUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/org/FtbCultivatePositionGradeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/org/FtbPositionGradesInfoBoundVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/paper/FtbCultivateTestPaper.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/paper/FtbCultivateTestPaperQuestion.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/paper/FtbCultivateTestPaperRule.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePosition.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCertificate.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCourceChapterLearning.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCourceLearning.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCourse.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCourseExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCourseIdentity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionCoursePractice.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionExamIdentify.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionIdentifyResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/position/FtbCultivatePositionUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotion.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionMember.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionMemberNew.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionNew.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionNewMessage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionPost.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionPostNew.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionSetting.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/promotion/FtbCultivatePromotionUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/question/FtbCultivateQuestion.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/question/FtbCultivateQuestionAnalysis.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/question/FtbCultivateQuestionBank.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/question/FtbCultivateQuestionBankCourse.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/question/FtbCultivateQuestionOption.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/question/FtbCultivateQuestionPoints.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/task/FtbCultivateLearnTaskPhase.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/task/FtbCultivateLearnTaskPractice.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/task/FtbCultivateTaskLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/teaching/CultivateFile.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/teaching/CultivateUserView.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/po/teaching/TeachingApprove.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/AppQueryExamQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/AssessmentPointsReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/ExamStatisticsReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryAssessmentPointsReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryCultivateExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExamForPostReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExamRankListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExamUserReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExpireExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryExpireListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryMyCompleteExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryMyExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/QueryWaitMyExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/ReadOverExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/ReviewerAppointDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/SaveExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/StartExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/SubExamQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/SubExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/WebHistoryQueryExamUserReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/WebQueryExamReadOverReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/exam/WebRestartExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/learn/AddLearnCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/learn/QueryLearnCategoryListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/learn/QueryLearnTaskCountListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/learn/QueryLearnTaskListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/learn/UpdateLearnCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/paper/PaperConfigReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/paper/PostAndPositionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/paper/PreQuestionImportReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/paper/QueryPaperReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/paper/SavePaperReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/AddQuestionBankReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/AddQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/EditQuestionBankReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/EditQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/QueryQuestionBankReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/QueryQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/QuestionOptionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/req/questionbank/UnbindQuestionBankReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/AppExamListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/AppExamUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/AppExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/AppPromotionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/AppQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/AssessmentPointsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/CanDeleteMsg.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/CompleteExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/DeleteAlertExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamAndPaperDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamBaseUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamCultivateCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamCultivateForPositionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamListUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamPaperVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamQuestionBakVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamStatisticsForOrgExcelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamStatisticsForOrgVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamStatisticsForPersonExcelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamStatisticsForPersonVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExcelImportQuestionResultReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ExcelUserQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/GeneralPaperQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ImportQuestionResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/InnerExamDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/InnerQueryExamResultDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/MyBlowExamNum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/MyExamListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PaperDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PaperDrawRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PaperListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PaperQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PaperVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PositionExamDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PostAndPosition.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/PreQuestionImportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/QueryUserExamListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/QuestionBankVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/QuestionCanDeleteMsg.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/QuestionCountDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/QuestionOptionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/QuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/RecordPaperAndExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/ReviewerRole.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/StatisticsResultDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/StatisticsResultFirstAndRepeatDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/SubExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/TaskRelationCertificateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/TaskRelationExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/TaskRelationIdentificationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/TriggerExamDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserExamCount.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserOrganizationItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserOrganizationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserPaperVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/UserRankingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/WaitReadOverNumVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/resp/WebReadOverExamAndPaperDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2IdentifyApplyItemReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2IdentifyApplyListAppReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2IdentifyApplyListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2IdentifyApplySaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2IdentifyApplySubmitReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2MyIdentifyApplyListAppReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/req/V2MyIdentifyApplyListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/IdentifyDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplyAppBasicInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplyBasicInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplyInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplyItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplyListAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplyListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/apply/vo/V2IdentifyApplySaveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/certificate/req/DeleteCertificateImageReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/certificate/req/V2SaveCertificateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/common/req/V2BatchByPrimaryIdReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/common/req/V2SaveCommonCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/common/vo/V2BaseIdNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/common/vo/V2ImportReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/common/vo/V2PreImportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/AiHelperCourseStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/UserLearningStatusVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/AppCommonCourseSimpleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/AppCourseSimpleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/AppCultivateCourseExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/AppCultivateCourseIdentityVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/CommonCourseCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/LastStudyCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/NextUserAppIdentityTmpVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/OtherPositionCourseCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/PositionLearningCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2ChapterAppDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2ChapterStudyVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2ChapterVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CourseAppDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CourseDetailsAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CourseOutlineAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CultivateChapterTestAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CultivateChapterTestDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CultivateChapterTestOptionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/vo/app/V2CultivateCourseMsgForAppDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/req/V2CultivateCourseChapterReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/req/V2CultivateCourseListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/req/V2CultivateCourseReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/req/V2CultivateCourseSelectReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/req/V2NextUserAllMapListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/req/V2NextUserCultivateCourseListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/vo/V2CultivateCourseChapterVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/vo/V2CultivateCourseDetailsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/vo/V2CultivateCoursePageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/vo/V2CultivateCourseSelectVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/vo/V2InnerCultivateChapterStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/course/web/vo/V2InnerCultivateCourseOrgStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/enums/ApproveEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/enums/ExamStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/enums/FrequencyEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/enums/MyExamStatusEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/enums/PositionBusinessSourceEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/enums/QuestionTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/po/CultivateExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/po/CultivateExamDrawRule.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/ExamRankReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/MarkDetailQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/MyExamWebQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/QueryReadOverExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/QuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/SkillDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/SkillImportReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2AppQueryExamListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2ExamSettingReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2ExamStatisticsForOrgReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2ExamStatisticsForPersonReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2QueryCoverReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2QueryExamRecordReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2QueryExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2RandomDrawRuleReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2ReadOverExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2RestartExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2SaveCoverCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2SaveCoverReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2SaveExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/req/V2SubmitExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/AiHelperExamStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/BankAnalysisVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/BankPickVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ConnectDrawRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ErrorObjectVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ExamDrawRuleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ExamResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ImportObjectVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/MyExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/MyExamTabCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/MyExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/MyExamWebVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/PaperDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/QuestionTypeAnalysisVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/QuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ReadOverExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/ReadOverTabCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/SkillUploadInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/SkillVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/UserExamRankVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2AppExamListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2AppExamRankingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2AppMyExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2AppQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2AppReadOverExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2CoverVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamStatisticsForOrgExcelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamStatisticsForOrgVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamStatisticsForPersonExcelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamStatisticsForPersonVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2ExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2PostAndPosition.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2QuestionOptionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2TestPaperVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2TreeCoverVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2UserExamDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/exam/vo/V2UserQuestionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/gained/req/V2AppCommentPageListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/gained/vo/V2AppCourseGainedCommentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/gained/vo/V2SimpleCourseGainedCommentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/QueryIdentifyCategoryListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2IdentifyItemsImportSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2IdentifyItemsSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2IdentifyTableImportSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2IdentifyTableListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2IdentifyTableSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2QueryIdentifyItemReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2SaveIdentifyItemReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/req/V2SaveItemCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/ExcelTestDemoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2CateIdentifyTableVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyCategoryItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyCategoryTreeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyCategoryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyItemsExportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyItemsInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyScoreConfigVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyTableInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyTableListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2IdentifyTableScoreConfigVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2PreIdentifyErrorVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/identify/vo/V2PreIdentifyImportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/item_pool/req/IdentifyItemsPoolReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/item_pool/req/SaveCultivateIdentifyItemsPoolReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/item_pool/vo/FtbCultivateIdentifyItemsPoolVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/item_pool/vo/V2IdentifyItemExceptionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/item_pool/vo/V2IdentifyItemVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/label/dto/FtbCultivateLabelReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/label/dto/FtbCultivateUpdateLabelReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/label/vo/FtbCultivateLabelVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/mq/CultivateMqDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/offline/V2CultivateOfflineTrainUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionCourseCertificateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionCourseExamReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionCourseIdentityReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionCoursePracticeReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionCourseReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/FtbCultivatePositionSettingReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/V2CultivateCommonCourseForAppReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/V2CultivateCoursePageReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/V2CultivatePositionCourseForAppReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/V2InnerPositionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/V2MyCultivateCommonCourseForAppReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/req/V2OtherCultivatePositionCourseForAppReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppCultivatePositionCourseCertificateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppCultivatePositionCourseExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppCultivatePositionCourseIdentityVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppCultivatePositionCoursePracticeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppCultivatePositionCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppCultivatePositionDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/AppPracticeCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/BusinessSourceVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/CheckPostAndGradeResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/CourseSimpleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/CultivatePositionCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/CultivatePositionSimpleVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/CultivateSimpleUserInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/CultivateUserPositionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseCertificateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseCertificateWithNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseExamWithNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseIdentityVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseIdentityWithNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCoursePracticeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCoursePracticeWithNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionCourseWithNameVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/FtbCultivatePositionSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/PersonForLeaderVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/PositionStatusResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/V2AllCultivatePositionCourseExam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/V2CultivateJobLearnCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/V2CultivatePositionDetailForApp.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/V2StudyCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/WebCultivatePositionView.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/WebCultivatePositionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/position/vo/WebPositionLearningListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/dto/PersonStatisticsDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/FtbCultivatePromotionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePositionExcludeReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePromotionCreateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePromotionPhaseStateForApp.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePromotionPositionDetailForApp.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePromotionPostReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePromotionScopeReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2CultivatePromotionSelectPositionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2PromotionOrgStatisticReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2PromotionPersonStatisticReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/req/V2WebCultivateStudyMemberListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/AppCultivatePromotionPositionDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/MyCultivatePromotionListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/NextUserCultivatePromotionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/PositionProgressVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/PromotionAndPostVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/PromotionLevelInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/PromotionPhasePositionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/PromotionPhaseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/UserPromotionDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePositionGradeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePositionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePostAndGradeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePostAndGradeVoExt.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionErrorVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionLevel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionMemberVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionOrgStatisticVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionPersonStatisticVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionPostNewVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionScopeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2CultivatePromotionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/V2WebCultivateStudyMemberListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/promotion/vo/WebCultivatePromotionListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/question/req/BatchAddQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/question/req/V2AddQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/question/req/V2EditQuestionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/question/req/V2ExcelImportQuestionResultReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/question/req/V2ImportQuestionResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/question/req/V2PreQuestionImportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/statistics/V2BatchQueryResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/statistics/V2CultivateLineNumDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/statistics/V2UserCourseStudyVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/statistics/V2UserLearningStatusResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/req/CheckUserTaskAllPhasesCompleteReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/req/V2CultivateLearnTaskListForManagerReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/req/V2CultivateTaskCountReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/req/V2CultivateTaskSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/req/V2MyCultivateTaskListForReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/CheckUserTaskAllPhasesCompleteVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/FtbCultivateLearnTaskCertificateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/FtbCultivateLearnTaskCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/FtbCultivateLearnTaskExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/FtbCultivateLearnTaskIdentificationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/FtbCultivateLearnTaskPracticeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/PhaseExamStatusVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/PhaseIdentificationStatusVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/PhaseStatusResultVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskCertificateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskExamVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskFinishStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskIdentificationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskPhaseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskPracticeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateLearnTaskSimplePhaseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateTaskCountExportVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateTaskCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateTaskDetailsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateTaskFinishUserListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2CultivateTaskListForManagerVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2MyCultivateLearnTaskListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/task/vo/V2MyCultivateLearnTaskSimpleInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/model/V2Attachment.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/req/V2QueryTeachingSkillReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/req/V2SaveTeachingSkillReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/req/V2TeachingAuditReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/req/V2TeachingSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/StoreTeachingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/StudentInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/TeachingApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/TeachingDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2MyRecordPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2SkillCategoryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2SkillTreeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2SkillVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2StoreTeachingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2TeachingRecordDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/v2/teaching/vo/V2TeachingRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/FtbCultivateCommonSettingGlobalVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/FtbCultivateCourseSettingGlobalVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/apply/FtbCultivatePromotionPostApplyVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/apply/FtbCultivatePromotionPostApplyWithPerVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/casebase/FtbCultivateCaseBaseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateOrgWisdomStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificatePersonWisdomStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateQueryStatisticAllVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateUserAppListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateUserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/certificate/FtbCertificateUserListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/chapter/FtbCultivateChapterTestInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/chapter/FtbCultivateChapterTestResultVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/chapter/FtbCultivateChapterTestStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/common/InnerPowerPositionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/common/InnerPowerStoreVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/common/InnerPowerUserVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/ChapterInformationVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbAppTaskCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbChapterAppDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbCheckJobGradeChangeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbCourseDetailsAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbCourseOutlineAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbCultivateCourseMsgForAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbGlobalCurriculumAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbGlobalCurriculumAppWrapVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/app/FtbTrainingCoursesAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/CultivateCourseAiVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCourseDeleteJobLearnVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCourseDeleteQuestionBankVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCourseDeleteVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCourseChapterDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCourseChapterVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCourseDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCourseNumberVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCourseOrgStatisticesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCoursePageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCoursePersonStatisticesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/FtbCultivateCourseTypeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/PromotionChannelLearnCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/course/web/UserCourseStudyVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/coursepackage/FtbCultivateCoursePackageDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/coursepackage/FtbCultivateCoursePackagePageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbChapterEndGainedVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbCourseGainedCommentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbCourseGainedDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbCourseGainedForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbCourseGainedInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbCourseGainedLikeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbCourseGainedListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbGroupCommentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/gained/FtbGroupGainedVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/FtbCultivateIdentityOrgWisdomStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/FtbCultivateIdentityPersonWisdomStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyAppAuthorityAppraiserVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyBasicInfoAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyBasicInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyDetailsAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyDetailsInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyInfoApiVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyInfoAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyListAppDbVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyListAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyListDbVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyApplyStatisticsApiVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyInfoAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyItemsInfoAppVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyItemsInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyItemsSaveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyItemsUpdateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyItemsWithCategoryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyStatisticsForNewVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyStatisticsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyTableDeleteVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyTableInfoApiVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyTableInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyTableListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyTopVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyUserOrgInfoPushVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/IdentifyUserOrgInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/ManuallyInitiateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/PostAndCourseCorrelationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/PostAndCourseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/UserIdentifyPageDbVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/UserIdentifyPageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/UserOrgInfoAll1Vo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/UserOrgInfoAllVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/UserOrgInfoPushVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/identify/UserOrgInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskCertificateInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskCourseInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskExamInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskFinishInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskFinishStatisticsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskIdentificationInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskInfoCountListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskInfoForAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskInfoListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateLearnTaskListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbCultivateMyLearnTaskListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbExportCultivateLearnTaskInfoCountListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbExportCultivateLearnTaskInfoListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/FtbLearnQueryTaskDetailsEditingVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/WebLearnTaskDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/info/FtbCultivateLearnTaskCertificateUserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/info/FtbCultivateLearnTaskCourseUserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/info/FtbCultivateLearnTaskExamUserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/info/FtbCultivateLearnTaskIdentificationUserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/info/FtbCultivateLearnTaskUserFinishInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/learn/info/TimeDifference.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/FtbCultivateOfflineFileVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/FtbCultivateOfflineTrainDetailsAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/FtbCultivateOfflineTrainDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/FtbCultivateOfflineTrainPageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/FtbCultivateOfflineTrainPeopleSigningInVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/FtbOfflineTrainingAppPageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/OfflineCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/offline/UserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/CourseStudyCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivateCourseListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivateExamUserVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivateIdentifyUserVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePersonStatisticesExportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePersonStatisticesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionAssessmentOrgStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionForNewTrainVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionJobLearnCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionLearnLevelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionLevelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionOrgStatisticesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionPersonStatisticesExportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionPersonStatisticesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionUserInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivatePositionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivateStoreCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbCultivateWorkerCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbJobLearningConfigurationVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbJobLearningExamAppraisalVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbJobLearningPaginatedVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbManagerTrainingStatisticsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbPersonTrainingStatisticsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbPositionCourseDropDownListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/FtbStoreManagerTrainingStatisticsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/JobTitleStatistics.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/OrganizeCourseDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/PersonStatisticesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/PersonalDimensionCourseDetails.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/app/FtbCultivatePositionForAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/app/FtbCultivatePositionPostForAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/app/FtbPopUpPromptVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/app/FtbSubordinateLearningCoursesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/app/OnTheJobLearningCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCopyRelatedPositionsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionAssessmentListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionAssessmentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionCopyVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionExamPersonVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionExamVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionIdentifyPersonVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/position/web/FtbCultivatePositionIdentifyVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateLearnMapInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateMapSimpleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateMapStudyUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateMapsOrgWisdomStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateMapsPersonWisdomStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateMeberVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateMemberInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateNextCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionDeleteInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionForAppVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionLevelMapVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionMeberPostInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionMemberVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionNewMemberVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionNewVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionPostNewVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionPostSelectCourseInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionPostVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionStudyDeleteInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionWithPersonelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatePromotionWithPersonnelInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateStudyMemberInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateStudyMemberVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivateUnderMemberInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/promotion/FtbCultivatelearningProgressVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/rule/FtbCultivateRuleVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/statistics/CultivateStatisticsForAppCommonParam.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/statistics/FtbCultivateStatisticsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/statistics/NumberOfTrainingSessions.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/statistics/NumberofAppSessions.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/EmployeePageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/EmployeeViewDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/MyPracticeSummaryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/MyRecordPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/PositionUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/RecordExportListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/RecordInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/RecordPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/RecordViewDataModel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/SkillCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/SkillInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/SkillKeyVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/StoreInfoListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/StudentUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/SummaryPageListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/SuperiorTeachingSummaryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingDataListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingRecordDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingRecordVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingSkillVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingStoreCountVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingStoreListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TeachingUserVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/TodaySummaryDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/cultivate/vo/teaching/ViewDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/dto/CulturePicSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/dto/CultureTextSettingDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/dto/PicSettingUploadDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/dto/SettingQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/dto/StatQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/dto/UploadDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/serializer/LocalDateToEpochSerializer.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/Base64ImageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/CultureClockInVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/CulturePicSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/CultureStatVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/CultureTextSettingVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/RecordDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/RecordListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/UploadInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/culture/vo/YearDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/FileQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/InfoDetailDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/InformationAuthorityDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/InformationQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/QueryMyFilesReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/RubbishQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/SpaceDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/UndeleteDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/UploadDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/dto/ViewDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/CheckRubbishVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/DeleteInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/InformationAuthorityVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/InformationDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/InformationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/MiniInformationTreeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/MiniInformationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/RecentlyViewedVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/RubbishInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/RubbishVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/SpaceInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/SpaceVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/UseDetailVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/doclibrary/vo/UserInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/CourseEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/EmployeeMetaDataType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/ExamConfigEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/ExamUpdateStatus.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/ExamUserStatisticsEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/FContractSignStatus.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/FormFieldType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/FtbGroupEventTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/FtbPersonnelsAuditTaskEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/FtbPersonnelsCofigEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/GrowthLogEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/SalaryChangeTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/SelfrowingEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/StaffWorkerStatus.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/TaskLearnStatusEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/enums/TrialPeriodStatus.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/enums/FranchiseeCustomFieldTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/po/FranchiseeCustomFieldEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/po/FranchiseeEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/po/FranchiseeExperienceEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/po/FranchiseeJoinRegionEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeAddReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeCustomFieldConfigOptionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeCustomFieldConfigReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeCustomFieldReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeCustomNameUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeExperienceReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeJoinRegionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeQueryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeSimpleAddReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/req/FranchiseeUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeCustomFieldConfigOptionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeCustomFieldConfigVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeCustomFieldVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeExperienceVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeIdName.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseePageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeStoreVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/franchisee/vo/FranchiseeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/helpailog/dto/HelpAiLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/helpailog/dto/HelpAiLogReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/helpailog/po/FtbHelpAiChatLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/im/AdministratorsMsg.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/im/UserAndLinkDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/im/UserSuccessDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/AddForwardDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/AddReplyDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/CommentQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/ConditionLogQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/LogAgentAlertDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/LogMemberAssociationDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/LogMemberFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/LogMemberLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/LogQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/LogWebQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/dto/StatQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/po/LogComment.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/po/LogMemberAssociation.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/po/LogMemberField.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/po/LogMemberLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/po/LogTemplate.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/po/LogTemplateField.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/CommentPaginationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogAssociationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogCommentVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogDataVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogMemberFieldVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogMemberLogVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogStatVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogTemplateFieldVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/LogTemplateVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/MiniLogVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/MiniMemberLogVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/ReceiveRangeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/memberLog/vo/WebLogVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeAnnouncements.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeAnnouncementsLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeAnnouncementsReceive.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeCategories.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeFiles.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeManager.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeUserGroupMembers.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/domain/FtbNoticeUserGroups.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/AnnouncementsCategoryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/AppNoticeCategoriesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeAnnouncementsDetailDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeAnnouncementsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeAnnouncementsLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeAnnouncementsReceiveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeCategoriesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeFilesDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeManagerDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeUserGroupMembersDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/FtbNoticeUserGroupsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/MyNoticeAnnouncementsDetailDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/MyNoticeAnnouncementsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/MyNoticeAnnouncementsMsgDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/NoticeManagerAuthorityDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/NoticeUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/dto/UserOrgAndPositionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/enums/NoticeEnums.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/AddNoticeAnnouncementsReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/AppQueryAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/AppQueryMyAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/InnerAppQueryAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/InnerAppQueryMyAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/InnerQueryMyWebAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/NoticeFilesVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/QueryAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/QueryMyWebAnnouncementListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/QueryUserListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/announcement/UpdateNoticeAnnouncementsReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/category/AddCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/category/QueryCategoryListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/category/QueryNextCategoryListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/category/UpdateCategoryReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/group/AddUserGroupReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/group/ListGroupUserMemberReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/group/UpdateUserGroupReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/manager/AddNoticeManagerReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/manager/QueryNoticeManagerReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/notice/req/oplog/QueryLogListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/ExportRosterOneVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/FtbAddStaffImportRedisBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/FtbRewardsImportRedisBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/FtbRosterImportConstants.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/FtbRosterImportHeadRuleBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/FtbRosterImportRedisBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/bo/FtbRosterImportTemplateBO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/analysis/FtbPersonnlesAnalysisDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/analysis/PersonnlesAnalysisDeserializer.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/apply/FtbPersonnelsApplyCreateDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/audit/FtbPersonnelsAuditDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/audit/FtbPersonnelsAuditSubConfigVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/authoritys/FtbAddNewPermissionsBatchDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/authoritys/FtbAddNewPermissionsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/authoritys/FtbAddNewPermissionsUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/authoritys/FtbPermissionInfoDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/authoritys/PermissionsCacheDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/base/FtbPersonnelsBaseForOA.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/base/PersonnelsQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/base/SourcePhone.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/black/FtbPersonnelsBlackListDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/black/FtbPersonnelsBlackListHistoryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/black/FtbPersonnelsBlackUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/black/FtbPersonnelsBlacklistAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/black/FtbPersonnelsBlacklistTypeAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/black/FtbPersonnelsBlacklistTypeUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/config/FtbPersionnelsSendCranbonCopy.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/config/FtbPersonnelsAuditConfigDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/config/FtbPersonnelsAuditConfigInfoDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/config/MasterConfigUserBoudDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/config/TestOrgDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/contractinfo/ContactStatusInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/emp/ExcelImprotEmpDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/emp/FtbEmpAddNewDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/emp/FtbEmpConfirmDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/emp/FtbEmpEntryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/emp/FtbEmpQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/employeetype/FtbEmployeeTypeAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/employeetype/FtbEmployeeTypeEditDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsFormDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsFormQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsFormUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsReceiveAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsReceiveQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsReceiveReturnDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/goods/FtbPersonnelsGoodsReceiveUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/oa/FtbPersonnelsEmployInfoForOA.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/oa/RequestForOA.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/range/FtbRangeConfigDIYDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/range/FtbRangeConfigDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/range/FtbRangeDiyQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/range/RangeDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/recruitmentchannels/PersonnelsRecruitmentChannelsAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/regular/FtbPersonnelsForAppQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/regular/FtbPersonnelsRegularCreateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/regular/FtbPersonnelsSalaryAuditDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/resignation/FtbResignationConfigurationCategoryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/resignation/FtbResignationConfigurationDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbAwardPassedDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbAwardSubmissionDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbEmployeeRewardRecordsQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPenaltySubmissionDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPersonnelSalaryRewardDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPersonnelsRewardsPunishmentExchangeOrderDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPersonnelsRewardsPunishmentQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPersonnelsRewardsPunishmentStartDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPersonnelsRewardsPunishmentsAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/rewardspunishments/FtbPersonnelsRewardsPunishmentsUpdateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/FtbPersonnelsTrialDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/FtbRosterImportDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/FtbjobTrialRejectedDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/QueryCompanyAgeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/FtbPersonnlesJobTenureDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/FtbPersonnlesJoeInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsBaseMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsContactMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsEducationMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsMaterialMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsMetaDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsOrgMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsRoleMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/roster/meta/PersonnelsWorkerMetaData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/salary/BaseSalaryStructureDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/salary/FtbPersonnelRosterSalaryHistoryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/salary/FtbPersonnelsSalaryInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/salary/FtbPersonnelsSalaryTemporaryStorageCreatDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/salary/FtbSalaryMetaDataQueryDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/salary/FtbXcCustomFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/scheme/FtbPersonAddSchemeDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/scheme/FtbPersonEditSchemeDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/secondment/FtbHandleSecondmentDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/secondment/FtbSecondMentQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/secondment/msg/SecondmentMsgUserInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsEmployApplyDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsEmployApplyListDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsMoveLogVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsPromotionLogVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsRegularVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsStaffEmploymentApplyDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsTransferVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/employment/FtbPersonnelsTurnoverLogVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/EditFormFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/ExportFormFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/ExportFormTypeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/FormFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/FormTypeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/FtbPersonnelsRegistrationFormFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/FtbPersonnelsRegistrationFormFieldOptionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/FtbPersonnelsRegistrationFormTypeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/SubFormFieldDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/field/SubMulitFieldValDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/growth/AddGrowthLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/growth/FtbPersonnelsStaffGrowthLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/BankConvertDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/CheckRegisterFormFillDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/FtbPersonnelsStaffRegistrationFormDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/GroupFieldDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/HealthConverterDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/IdCardConverterDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/ProbationPeriodDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/registerform/WorkAddressDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/CheckRosterDeleteVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/EditUserBaseInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/FtbPersonnelsStaffRosterDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/PartUserInfoVoAndWorkerNo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/SaffRoleDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/ShopManagerUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/SimpleStoreUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffBaseInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffDepartDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffHomeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffPromotionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffRegularDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffRosterInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffRosterSimpleBaseInfoDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/StaffTravlFailDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/WorkerGroupDataDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/roster/WorkerStatisticsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/salarylog/AddSalaryChangeLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/salarylog/FtbPersonnelsStaffSalaryChangeLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/transfer/FtbPersonnelsStaffTransferPositionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/transfer/FtbPersonnelsStaffTransferPositionHandoverDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/transfer/TransferGrowthLogDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/transfer/TransferPositionCountDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/staff/transfer/TransferPositionDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/turnover/FtbDepUserDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/turnover/FtbPersonnelsJobTrialRejectedCreateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/turnover/FtbPersonnelsTurnoverCreateDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/turnover/FtbPersonnelsTurnoverDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/turnover/FtbPersonnelsTurnoverHandoverDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/turnover/SaveTenantUserForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/uchisuike/DeleteRecommendedPersonnelDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/uchisuike/FtbGenerateShortChainDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/uchisuike/FtbRecommendationPoolOrgDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/uchisuike/FtbinternalRecommendationPoolListDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/dto/uchisuike/app/FtbRecommendationInvitationAppDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuditCarbonRecipient.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuditMasterConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuditRunTask.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuditRunTaskHistory.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuditSubConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuditTaskInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsAuthoritys.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsBlacklist.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsBlacklistHistory.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsBlacklistType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsGoods.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsGoodsReceive.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsIdcardVerification.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsPermissionUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsPermissions.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsPostApply.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsPromiseConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRecruitmentChannels.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRegistrationFormField.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRegistrationFormFieldOption.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRegistrationFormType.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRegularManagement.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsResignationCategoryConfiguration.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsResignationConfiguration.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRewardsPunishments.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsRuleConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsSalaryTemporaryStorage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsSecondmentConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsSecondmentManagement.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsShortchain.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffArchivesHistory.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffEmploymentApply.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffGrowthLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffRegistrationFormData.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffRoster.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffRosterScheme.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffSalaryChangeLog.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffTransferPosition.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsStaffTransferPositionHandover.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsTransferManage.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsTurnoverAccountRegistration.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsTurnoverHandover.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsTurnoverManagement.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsUchisuikePond.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnelsUchisuikePondOrg.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnlesInfoConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnlesInfoDiyRangeConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/po/FtbPersonnlesInfoRangeConfig.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/AddStaffEmploymentApplyReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/AddStaffEmploymentApplyResultDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/AddStaffEmploymentApplySalaryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/AddStaffEmploymentApplySalaryItemDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/BatchByPrimaryIdReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/EmploymentApplyCheckDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/MyWebEmploymentApplyCheckListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/QueryAppStaffEmploymentApplyListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/QueryStaffEmploymentApplyListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/employment/SendPhoneMsgReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/field/QueryRegistrationFormFieldListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/field/QueryRegistrationFormTypeListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/field/SaveRegistrationFormFieldOptionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/field/SaveRegistrationFormFieldReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/field/SaveRegistrationFormTypeReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/goods/FtbPersonnelsGoodsForm.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/registerform/OcrReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/registerform/QueryRegisterFormDataReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/registerform/SaveAppRegisterFormDataReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/registerform/SaveFormDataReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/registerform/SaveMyFormDataReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/registerform/SaveRegisterFormDataReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/AppStaffRosterListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/CheckOrgAndPosAndRankExistVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/ConfirmOnDutyReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/FtbPersonnelsMetaDataReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/FtbPersonnelsMetaFuctionReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/StaffRosterListExportReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/StaffRosterListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/StaffRosterReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/roster/UserAccountDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/AppQueryTransferListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/FtbHandleTransferDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/FtbHandleTransferOAAddDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/FtbHandleTransferOaDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/FtbHandleTransferQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/MyWebTransferCheckListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/QueryTransferListReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/SaveTransferReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/req/transfer/TransferPositionHandoverReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsLeaveReasonDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsNewEmployeeTurnoverDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsNewHireRateDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsOnboardingDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsPeopleInfoRatioVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsPeoplePostRatioVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsPeopleTransferRatioVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsPostAdjustmentDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsRegularizationDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsShopAdjustmentDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverAgeDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverFirstMonthLeaveRateVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverFirstWeekLeaveRateVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverFirstYearLeaveRateVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverGenderDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverInfoDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverLeavePeopleAgeDistributionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverLeavePeopleAgeRatioVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverLeavePeoplePostRatioVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverLeaveRateVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverLeaveReasonDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverLeaveReasonDistributionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverNewEmployeeDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverPeopleSexVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverPostRatioDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverRateComparisonVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverRateInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverRateVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverSeniorityDetailVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnelsTurnoverWithAnalysisInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnlesBrainDrainVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/FtbPersonnlesBrainShopAdjustmentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelDataAnalysisListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewAgeDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewCompanyDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewDepartmentDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewEducationDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewEmployeesAgeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewEmployeesDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewEmployeesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewInsuranceDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewJobDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewProportionDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewProportionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewSeniorityDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewSituationDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewStoreDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/analysis/PersonnelsOverviewTypeDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/app/FtbPersonnelsBubbleCountVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/apply/FtbPersonnelsApplyVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/apply/FtbPersonnelsApplyWithPerVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/apply/FtbPersonnelsCourseVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/authoritys/FtbPermissionInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/authoritys/FtbPermissionUserVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/authoritys/FtbPersonnelsPermissionUserVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/authoritys/FtbPersonnelsPermissionVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/authoritys/FtbPersonnelsScopeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/black/BlackHistoryStatus.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/black/BlackTermEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/black/FtbPersonnelsBlackListHistoryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/black/FtbPersonnelsBlackListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/black/FtbPersonnelsBlacklistTypeListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/config/FtbPersonnelsAuditConfigVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/config/FtbPersonnelsAuditRunTaskInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/emp/FtbEmpAddNewVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/emp/FtbEmpConfirmVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/emp/FtbEmpEntryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/emp/FtbEmpResultVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/employeetype/FtbPersonnelsEmployeeTypeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/employment/AddStaffEmploymentApplyExcelERRVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/employment/AddStaffEmploymentApplyExcelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/employment/CheckPhoneStatusVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/employment/FtbPeronnelsStatisticVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/employment/FtbPersonnelsStaffEmploymentApplyVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/goods/FtbPersonnelsGoodsPageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/goods/FtbPersonnelsGoodsReceiveDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/goods/FtbPersonnelsGoodsReceivePageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/goods/PersonnelsGoodsDistributedLock.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/history/FtbPersonnelsAuditRunTaskHistoryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/mcp/OnboardingThisMonthStatsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/mcp/StaffBirthdayVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/mcp/StaffNotSignContractVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/mcp/SubmittedButNotOnboardedVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/range/FtbRangeConfigDIYVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/range/FtbRangeConfigVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/regular/FtbPersonnelsRegularInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/regular/FtbPersonnelsRegularManagementVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/resignation/FtbResignationConfigurationCategoryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/resignation/FtbResignationConfigurationVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbEmployeeRewardRecordsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbEmployeeRewardUserInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbEmploymentRecordsExcelERRVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbEmploymentRewardExcelVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbPersonnelSalaryRewardVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbPersonnelsRewardsPunishmentApprovalVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbPersonnelsRewardsPunishmentQueryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/rewardspunishments/FtbXcEmployeeRewardRecordsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbHomePageRewardVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbPersonnelsChangeInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbPersonnelsStaffArchivesHistoryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbPersonnlesJobTenureVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterAttributesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterCategoryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterImportFormFieldsConfigVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterImportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterInsertAttributesVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterInsertNomalVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbRosterPageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/FtbThousandFacePersonVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/ImportResultVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/InnerImportResultVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/roster/InnerImportRosterDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/salary/FtbPersonnelRosterSalaryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/salary/FtbPersonnelsSalaryTemporaryStorageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/salary/FtbPersonnelsSalaryVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/salary/FtbXcCustomFieldVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/salary/UserInfoWithSalary.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/scheme/FtbPersonSchemeDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/scheme/FtbPersonSchemeListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/secondment/FtbPersonnelsSecondRecordVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/secondment/FtbPersonnelsSecondmentInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/secondment/FtbPersonnelsSecondmentTaskVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/secondment/FtbPersonnelsSecondmentVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/task/FtbPersonnelsAuditInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/task/FtbPersonnelsAuditRunTaskVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/task/FtbPersonnelsResult.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/transfer/FtbHandleTransferDetailsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/transfer/FtbHandleTransferPageExportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/transfer/FtbHandleTransferPageVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/turnover/FtbPersonnelsTurOrgInfo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/turnover/FtbPersonnelsTurnoverInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/turnover/FtbPersonnelsTurnoverManagementVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/turnover/FtbPersonnelsTurnoverStatisticVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/uchisuike/FtbinternalRecommendationPoolExportVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/personnels/vo/uchisuike/FtbinternalRecommendationPoolVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/dto/PaginationQualificationsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/dto/QualificationsDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/dto/SaveQualificationsDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/dto/SaveQualificationsFieldDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/dto/SaveQualificationsFieldItemDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/GradeVOExp.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/PositionAndGradesVOExp.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/QualificationsCategoryVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/QualificationsInfoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/QualificationsItemsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/QualificationsListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/QualificationsStatisticsVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/qualifications/vo/QualificationsVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/Store.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/StorePagination.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/StorePositionInfoVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/StoreRegionVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/StoreUser.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/StoreUserNumVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/dto/BatchSaveStoreDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/dto/StoreAbnormalIdsQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/dto/StorePageByIdsNoDsQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/dto/StorePageByIdsQueryDTO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/dto/StoreUserDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/vo/StoreBaseListVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/vo/StoreLocationVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/vo/StoreUserRelationVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/vo/StoreUsersVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/store/vo/UserStoreListVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/po/StoreCertificatePhotoEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/po/StoreCertificatePhotoItemEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/req/StoreCertificatePhotoAddReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/req/StoreCertificatePhotoItemReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/req/StoreCertificatePhotoUpdateReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/vo/StoreCertificatePhotoIdNameVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/vo/StoreCertificatePhotoItemVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/storecertificatephoto/vo/StoreCertificatePhotoVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/tempmodule/AppBannerVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/tempmodule/RollImageVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/tempmodule/dto/RollImageDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/enums/CertificateTypeEnum.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/po/FtbParamEntity.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/req/WarningNoticeSaveReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/req/WarningNoticeTargetReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/req/WarningNoticeUserConfigReq.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/vo/WarningNoticeTargetVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/vo/WarningNoticeUserConfigVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/warningnotice/vo/WarningNoticeVO.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/ApplyAttendanceChangeDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/ApplyAttendanceOutsideDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/ApplyAttendanceRepairDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/ApplyAttendanceViolationDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/AttendanceBaseForOa.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/AttendanceBusinessTripApproveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/AttendanceBusinessTripApproveOaDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/AttendanceGoOutApproveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/AttendanceLeaveApproveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/AttendanceWorkOvertimeApproveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/PunishmentsApprovalDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/RewardApprovalDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/SelfApproveDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/dto/UserSelfDto.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/ApplyAttendanceChangeVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/ApplyAttendanceOutsideVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/ApplyAttendanceRepairVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/AttendanceBusinessTripApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/AttendanceGoOutApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/AttendanceLeaveFlowApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/AttendanceWorkOvertimeApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/ClockKindVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/PunishmentsApprovalVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/RewardApprovalVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/model/workflow/vo/SelfApproveVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/ConstantUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/DateDetail.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/DateStrVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/DesensitizedUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/ExportUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/FtbUtil.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/SeetaConstant.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/TemplateExcelUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/TemplateWorkSheet.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/adapter/SelfDataConversionAdapter.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/annotation/Excel.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/annotation/Excels.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/domain/ExcelBeanMethodParseVo.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/util/CellValueConvert.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/util/ExcelDateUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/util/ExcelUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/util/ImageUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/util/ReflectUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/util/excelv2/util/SpringBeanUtils.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/valid/ValidInsert.java create mode 100644 jnpf-ftb/jnpf-ftb-entity/src/main/java/jnpf/valid/ValidUpdate.java create mode 100644 jnpf-ftb/pom.xml diff --git a/.idea/AI-Check-Test.iml b/.idea/AI-Check-Test.iml index d6ebd48..91c8737 100644 --- a/.idea/AI-Check-Test.iml +++ b/.idea/AI-Check-Test.iml @@ -2,7 +2,9 @@ - + + + diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..8e68dfc --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ftb/src/main/java/ftb/test/controller/.gitkeep b/ftb/src/main/java/ftb/test/controller/.gitkeep deleted file mode 100644 index 697d6d8..0000000 --- a/ftb/src/main/java/ftb/test/controller/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -# 示例 Spring Controller,用于本地测试 AST 解析 -# 将此目录结构复制到你的 Java 项目中进行验证 diff --git a/ftb/src/main/java/ftb/test/controller/UserController.java b/ftb/src/main/java/ftb/test/controller/UserController.java deleted file mode 100644 index cd2cfff..0000000 --- a/ftb/src/main/java/ftb/test/controller/UserController.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.controller; - -import org.springframework.web.bind.annotation.*; - -/** - * 示例 UserController,用于测试 API 变更检测。 - */ -@RestController -@RequestMapping("/api/users") -public class UserController { - - /** - * 查询用户详情 — 含多种参数类型,便于测试增删改检测。 - */ - @GetMapping("/{id}") - public String getUser(@PathVariable("id") String id, @RequestParam(value = "test", required = false, defaultValue = "false") Boolean includeDisabled) { - return "ok"; - } - - /** - * 新增用户 - */ - @PostMapping("/createUser") - public String createUser(@RequestBody UserCreateRequest request) { - return "created"; - } - - /** - * 更新用户 - */ - @PutMapping("/{id}") - public String updateUser(@PathVariable("id") Long id, @RequestBody UserCreateRequest request) { - return "updated"; - } - - /** - * 删除用户 - */ - @DeleteMapping("/{id}") - public String deleteUser(@PathVariable("id") Long id) { - return "deleted"; - } - -} diff --git a/ftb/src/main/java/ftb/test/controller/UserCreateRequest.java b/ftb/src/main/java/ftb/test/controller/UserCreateRequest.java deleted file mode 100644 index 1c72f19..0000000 --- a/ftb/src/main/java/ftb/test/controller/UserCreateRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.controller; - -/** - * 示例请求体 DTO,测试 @RequestBody 字段展开。 - */ -public class UserCreateRequest { - - private String userName; - private Boolean userType; - - public String getUserName() { - return userName; - } - - public void setUserName(String userName) { - this.userName = userName; - } - - public Boolean getUserType() { - return userType; - } - - public void setUserType(Boolean userType) { - this.userType = userType; - } -} diff --git a/jnpf-ftb/jnpf-ftb-api/pom.xml b/jnpf-ftb/jnpf-ftb-api/pom.xml new file mode 100644 index 0000000..a54fc1d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + com.jnpf + jnpf-ftb + 3.4.7-RELEASE + + + jnpf-ftb-api + + + 8 + 8 + UTF-8 + + + + + com.jnpf + jnpf-ftb-entity + ${project.version} + compile + + + com.jnpf + jnpf-duty-entity + ${project.version} + compile + + + com.jnpf + jnpf-common-feign + + + com.jnpf + jnpf-permission-entity + 3.4.7-RELEASE + compile + + + com.jnpf + jnpf-tenant-entity + 3.4.7-RELEASE + compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + + + + \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/PTenantAccountApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/PTenantAccountApi.java new file mode 100644 index 0000000..7d8ca94 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/PTenantAccountApi.java @@ -0,0 +1,72 @@ +package jnpf.account; + +import jnpf.account.fallback.PTenantAccountApiFallBackFactory; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.model.TenantGenerateDefaultAvatarVO; +import jnpf.model.personnels.dto.turnover.SaveTenantUserForm; +import jnpf.model.personnels.req.roster.UserAccountDto; +import jnpf.model.user.GenerateHeadForm; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +@FeignClient(name = "jnpf-tenant", fallbackFactory = PTenantAccountApiFallBackFactory.class, path = "/tenantUser") +public interface PTenantAccountApi { + + + @PutMapping("/batchAddUserAccount") + ActionResult> batchAddUserAccount(@RequestBody List userAccountForms); + + @PutMapping("/batchDisabledUserAccount") + ActionResult> batchDisabledUserAccount(@RequestBody List userAccountForms); + + @GetMapping("/generateDefaultAvatar") + ActionResult generateDefaultAvatar(@RequestParam("name") String name) throws IOException; + + + /** + * 批量异步生成头像 调用方自己要触发保底 + * @param names 用户名 + * @return + */ + @PostMapping("/batch/generate/default/avatar") + List batchGenerateDefaultAvatar(@RequestBody List names); + + /** + * 用户状态修改 + * @param saveTenantUserForm + * @return + */ + @PutMapping("/updateJobStatus") + ActionResult updateJobStatus(@RequestBody SaveTenantUserForm saveTenantUserForm); + + + @PutMapping("/userInfoSynchronous") + ActionResult userInfoSynchronous(@RequestBody SaveTenantUserForm saveTenantUserForm); + + @DeleteMapping("/deleteUser/{id}") + ActionResult deleteUser(@PathVariable("id") String id); + + /** + * 删除用户(同步删除租户用户) + * + * @param userId 用户ID + * @return ActionResult + */ + @DeleteMapping("/deleteUserWithPlatform/{userId}") + Boolean deleteUserWithPlatform(@PathVariable("userId") String userId); + + /** + * 批量删除用户 + */ + @DeleteMapping("/deleteUsers") + ActionResult deleteUsers(@RequestBody List userIds) ; + + @PostMapping("/generateDefaultAvatarCustomFilename") + ActionResult generateDefaultAvatar(@RequestBody GenerateHeadForm generateHeadForm) throws IOException; + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBack.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBack.java new file mode 100644 index 0000000..f90819e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBack.java @@ -0,0 +1,72 @@ +package jnpf.account.fallback; + +import jnpf.account.PTenantAccountApi; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.model.TenantGenerateDefaultAvatarVO; +import jnpf.model.personnels.dto.turnover.SaveTenantUserForm; +import jnpf.model.personnels.req.roster.UserAccountDto; +import jnpf.model.user.GenerateHeadForm; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.List; + +@Slf4j +public class PTenantAccountApiFallBack implements PTenantAccountApi { + + @Override + public ActionResult> batchAddUserAccount(List userAccountForms) { + log.error("调用批量添加用户接口失败,降级"); + return null; + } + + @Override + public ActionResult> batchDisabledUserAccount(List userAccountForms) { + return null; + } + + @Override + public ActionResult generateDefaultAvatar(String name) throws IOException { + log.error("调用生成默认头像接口失败,降级"); + return null; + } + + @Override + public List batchGenerateDefaultAvatar(List names) { + log.error("调用生成默认头像接口失败,降级"); + return List.of(); + } + + @Override + public ActionResult updateJobStatus(SaveTenantUserForm saveTenantUserForm) { + return null; + } + + @Override + public ActionResult userInfoSynchronous(SaveTenantUserForm saveTenantUserForm) { + log.error("调用同步用户信息接口失败,降级"); + return null; + } + + @Override + public ActionResult deleteUser(String id) { + return null; + } + + @Override + public Boolean deleteUserWithPlatform(String userId) { + log.error("调用删除租户用户接口失败,降级"); + return false; + } + + @Override + public ActionResult deleteUsers(List userIds) { + return null; + } + + @Override + public ActionResult generateDefaultAvatar(GenerateHeadForm generateHeadForm) throws IOException { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBackFactory.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBackFactory.java new file mode 100644 index 0000000..b757f61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/account/fallback/PTenantAccountApiFallBackFactory.java @@ -0,0 +1,22 @@ +package jnpf.account.fallback; + +import feign.FeignException; +import feign.Request; +import jnpf.account.PTenantAccountApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class PTenantAccountApiFallBackFactory implements FallbackFactory { + + @Override + public PTenantAccountApi create(Throwable cause) { + log.error("服务降级了"); + cause.printStackTrace(); + log.error(cause.getMessage(), cause); + return new PTenantAccountApiFallBack(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceApi.java new file mode 100644 index 0000000..4db8033 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceApi.java @@ -0,0 +1,35 @@ +package jnpf.attendance; + +import jnpf.attendance.fallback.AttendanceApiFallback; +import jnpf.model.attendance.vo.attendance.AttendanceToThousandsFacesVo; +import jnpf.util.NoDataSourceBind; +import org.apache.ibatis.annotations.Param; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/27 + */ + +@FeignClient(name = "jnpf-ftb", fallbackFactory = AttendanceApiFallback.class, path = "/attendance") +public interface AttendanceApi { + @PostMapping(value = "/invalidationCoupons") + @NoDataSourceBind + Boolean invalidationCoupons(@RequestParam(value = "tenantId") String tenantId); + +// @PostMapping(value = "/overtimeVouchers") +// @NoDataSourceBind +// Boolean overtimeVouchers(@RequestParam(value = "tenantId") String tenantId); + + @PostMapping(value = "/storageRest") + Boolean storageRest(@RequestParam("tenantId") String tenantId); + + + @PostMapping(value = "/attendanceToThousandsFaces") + AttendanceToThousandsFacesVo attendanceToThousandsFaces(); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceConfirmApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceConfirmApi.java new file mode 100644 index 0000000..a35b1ac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceConfirmApi.java @@ -0,0 +1,27 @@ +package jnpf.attendance; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.fallback.AttendanceUserApiFallback; +import jnpf.base.ActionResult; +import jnpf.model.thousandsfaces.TodayWorkVo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; + + +@FeignClient(name = "jnpf-ftb", fallbackFactory = AttendanceUserApiFallback.class, path = "/attendance/confirm") +public interface AttendanceConfirmApi { + + @Operation(summary = "自动生成考勤确认数据") + @GetMapping(value = "/autoConfirm") + ActionResult autoCreateConfirm(); + + @Operation(summary = "逾期自动确认") + @GetMapping(value = "/confirmAutoSlippage") + ActionResult confirmAutoSlippage(); + + @Operation(summary = "今日工作-考勤确认列表(0-待确认 1-已确认 2-已逾期)") + @GetMapping(value = "/getTodayWorkConfirmList") + List getTodayWorkConfirmList(); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceDailyRuleApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceDailyRuleApi.java new file mode 100644 index 0000000..0493420 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceDailyRuleApi.java @@ -0,0 +1,63 @@ +package jnpf.attendance; + +import jnpf.attendance.fallback.AttendanceDailyRuleApiFallback; +import jnpf.base.ActionResult; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Date; +import java.util.List; + +/** + * 组队动态 + * + * @author JNPF开发平台组 + * @version V3.1.0 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2021-03-24 + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = AttendanceDailyRuleApiFallback.class, path = "/arranging/work") +public interface AttendanceDailyRuleApi { + + + /** + * 定时执行初始化下个月固定排班 + */ + @PostMapping("initFixedScheduleRule") + ActionResult initFixedScheduleRule(@RequestParam(value = "tenantId") String tenantId); + + + /** + * 定时执行初始化下个月固定排班 + */ + @PostMapping("autoGrantBalance") + ActionResult autoGrantBalance(@RequestParam(value = "tenantId") String tenantId); + + /** + * 用户是否排班 + * + * @param tenantId 租户ID + * @param userId 用户ID + * @return + */ + @NoDataSourceBind + @GetMapping("userIsScheduling") + ActionResult userIsScheduling(@RequestParam("tenantId") String tenantId, @RequestParam("userId") String userId); + + @PostMapping("userIsSchedulingOrdinary") + ActionResult> userIsSchedulingOrdinary(@RequestBody List organizeIds); + /** + * 判断用户指定时间范围内是否排班 + * + * @param userId 用户ID + * @param start 开始时间 + * @param end 结束时间 + * @return + */ + @GetMapping("hasRuleByUserIdAndTime") + ActionResult hasRuleByUserIdAndTime(@RequestParam("userId") String userId, @RequestParam("start") Date start, @RequestParam("end") Date end); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceGroupApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceGroupApi.java new file mode 100644 index 0000000..a10c075 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceGroupApi.java @@ -0,0 +1,59 @@ +package jnpf.attendance; + + + +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.AttendanceUserListGroupVO; +import jnpf.attendance.fallback.AttendanceGroupApiFallback; +import jnpf.base.ActionResult; +import jnpf.entity.AttendanceGroup; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * @Title: 考情组api + * @Author: peng.hao + * @create: 2024/4/23:16:09 + */ +@FeignClient(name = "jnpf-ftb",fallbackFactory = AttendanceGroupApiFallback.class,path = "/group") +public interface AttendanceGroupApi { + /** + * 获取考勤组名称 + * @param groupId 考勤组Id + * + */ + @GetMapping("/queryTheNameOfTheAttendanceGroup") + AttendanceGroup queryTheNameOfTheAttendanceGroup(@RequestParam(value = "groupId") String groupId); + @GetMapping("/attendanceUserGroup") + List getAttendanceUserGroup(@RequestParam("userIds") List userIds); + + @GetMapping("/getGroupListByOrgId") + List getGroupListByOrgId(@RequestParam("organizeId") String organizeId); + + /** + * 批量获取用户绑定的考勤组 ,包含全名称 xxx/xxx/xxx + * @param userIds 用户ids + * + */ + @PostMapping("/list/user_bound") + ActionResult> getAttendanceUserListGroupVO(@RequestBody List userIds); + + /** + * 详情---获取一个考勤组 + * @param organizeName 组织名称 + * @param groupName 考勤组名称 + * @return 一个考勤组 + */ + @GetMapping("/info/organize_group/name") + ActionResult getAttendanceGroupByName(@RequestParam("organizeName") String organizeName, @RequestParam("groupName") String groupName); + + + @GetMapping("/checkScheduling") + boolean checkScheduling(); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceLineSchedulingConfigApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceLineSchedulingConfigApi.java new file mode 100644 index 0000000..4a41972 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceLineSchedulingConfigApi.java @@ -0,0 +1,25 @@ +package jnpf.attendance; + +import jnpf.attendance.fallback.AttendanceApiFallback; +import jnpf.model.attendance.vo.attendance.AttendanceToThousandsFacesVo; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/27 + */ + +@FeignClient(name = "jnpf-ftb", fallbackFactory = AttendanceApiFallback.class, path = "/arranging/line/scheduling/config") +public interface AttendanceLineSchedulingConfigApi { + + @GetMapping(value = "/noticeLineScheduling") + Boolean noticeLineScheduling(@RequestParam("tenantId") String tenantId); + + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceSimulateDataApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceSimulateDataApi.java new file mode 100644 index 0000000..501a2d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceSimulateDataApi.java @@ -0,0 +1,24 @@ +package jnpf.attendance; + +import jnpf.attendance.fallback.AttendanceSimulateDataApiFallback; +import jnpf.attendance.fallback.FtbClockInApiFallback; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 打卡api + * + * @author yanwenfu + * @create 2023-12-04 + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = AttendanceSimulateDataApiFallback.class, path = "/attendance/simulateData") +public interface AttendanceSimulateDataApi { + /** + * 打卡 + * @return java.lang.Object + */ + @PostMapping(value = "/clockIn") + void clockIn(@RequestParam(value = "tenantId") String tenantId) throws Exception; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceUserApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceUserApi.java new file mode 100644 index 0000000..24e9f50 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/AttendanceUserApi.java @@ -0,0 +1,39 @@ +package jnpf.attendance; + +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.attendance.fallback.AttendanceUserApiFallback; +import jnpf.base.ActionResult; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/27 + */ + +@FeignClient(name = "jnpf-ftb", fallbackFactory = AttendanceUserApiFallback.class, path = "/user") +public interface AttendanceUserApi { + /** + * 花名册考勤组变更 + * + * @param groupUpdateByUserDTO + * @return + */ + @PostMapping("groupUpdateByPersonnel") + ActionResult groupUpdateByPersonnel(@RequestBody GroupUpdateByUserDTO groupUpdateByUserDTO); + + /** + * 借调考勤组变动通知 + * @param tenantId + * @return + */ + @NoDataSourceBind + @GetMapping(value = "/userGroupUpdateBySecondNotice") + ActionResult userGroupUpdateBySecondNotice(@RequestParam(value = "tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbClockInApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbClockInApi.java new file mode 100644 index 0000000..349aefd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbClockInApi.java @@ -0,0 +1,44 @@ +package jnpf.attendance; + +import jnpf.attendance.fallback.FtbClockInApiFallback; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 打卡api + * + * @author yanwenfu + * @create 2023-12-04 + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbClockInApiFallback.class, path = "/attendance/clockIn") +public interface FtbClockInApi { + + @GetMapping(value = "/daily-rule-change/execute") + Boolean dailyRuleChangeExecute(); + + @PostMapping(value = "/absenceRecord") + @NoDataSourceBind + Boolean generateFtbAbsenceRecord(@RequestParam(value = "tenantId") String tenantId); + + @PostMapping(value = "/absenceTask") + @NoDataSourceBind + Boolean generateAbsenceTask(@RequestParam(value = "tenantId") String tenantId); + + /** + * 生成打卡提醒记录 + * @return java.lang.Boolean + */ + @PostMapping(value = "/remindRecord") + @NoDataSourceBind + Boolean generateBeforeWorkRemind(@RequestParam(value = "tenantId") String tenantId); + + @PostMapping(value = "/repairNum") + @NoDataSourceBind + Boolean generateRepairNum(@RequestParam(value = "tenantId") String tenantId); + + @PostMapping(value = "/continuousCheck") + Boolean continuousCheck(@RequestParam(value = "tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbStatisticsApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbStatisticsApi.java new file mode 100644 index 0000000..a27cd57 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/FtbStatisticsApi.java @@ -0,0 +1,108 @@ +package jnpf.attendance; + + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.dto.*; +import jnpf.attendance.fallback.FtbStatisticsApiFallback; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.DayStatisticsDto; +import jnpf.model.attendance.dto.SalaryAttendanceSupportDto; +import jnpf.model.attendance.vo.attendance.AttendanceCustomizeTableVo; +import jnpf.model.attendance.vo.attendance.DayStatisticsPageListVo; +import jnpf.model.attendance.vo.attendance.DayStatisticsVo; +import jnpf.model.attendance.vo.attendance.SalaryAttendanceSupportVo; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbStatisticsApiFallback.class, path = "/attendance/statistics") +public interface FtbStatisticsApi { + + @Operation(summary = "用户日统计数据初始化") + @GetMapping(value = "/userDayStatisticsInit") + ActionResult userDayStatisticsInit(@RequestParam(value = "tenantId") String tenantId); + + @Operation(summary = "个人考勤日报通知") + @GetMapping(value = "/dayStatisticsNotice") + ActionResult dayStatisticsNotice(@RequestParam(value = "tenantId") String tenantId); + + @NoDataSourceBind + @Operation(summary = "个人统计月报通知") + @GetMapping(value = "/monthStatisticsNotice") + ActionResult monthStatisticsNotice(@RequestParam(value = "tenantId") String tenantId); + + @NoDataSourceBind + @Operation(summary = "团队统计月报通知") + @GetMapping(value = "/teamMonthStatisticsNotice") + ActionResult teamMonthStatisticsNotice(@RequestParam(value = "tenantId") String tenantId); + + @Operation(summary = "连续未排班通知") + @GetMapping(value = "/consentUnscheduledNotice") + void consentUnscheduledNotice(@RequestParam(value = "tenantId") String tenantId); + + @Operation(summary = "考勤封账-自动封账定时器") + @PutMapping(value = "/autoSealTimer") + ActionResult autoSealTimer(@RequestParam(value = "tenantId") String tenantId); + + @Operation(summary = "计算考勤组平均工时(实际出勤工时)") + @PostMapping(value = "/countAttendanceAvgHours") + List countAttendanceAvgHours(@Valid @RequestBody AttendanceCountAvgHoursDto dto); + + @Operation(summary = "获取多考勤组月度统计数据") + @PostMapping(value = "/getAttendanceAvgHoursDetails") + ActionResult getAttendanceAvgHoursDetails(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "获取多考勤组月度人均工时折线图") + @PostMapping(value = "/getAttendanceMonthPerCapita") + ActionResult> getAttendanceMonthPerCapita(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "获取多考勤组月度日常情况") + @PostMapping(value = "/getAttendanceDailySituation") + ActionResult> getAttendanceDailySituation(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "获取多考勤组月度考勤工时排行") + @PostMapping(value = "/getAttendanceHoursRanking") + ActionResult> getAttendanceHoursRanking(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "获取多考勤组月度全勤情况") + @PostMapping(value = "/getAttendanceFullSituation") + ActionResult> getAttendanceFullSituation(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "获取多考勤组月度异常情况") + @PostMapping(value = "/getAttendanceAbnormalCondition") + ActionResult getAttendanceAbnormalCondition(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "获取多考勤组月度加班情况") + @PostMapping(value = "/getAttendanceOvertimeSituation") + ActionResult> getAttendanceOvertimeSituation(@Valid @RequestBody MonthStatsDetailsDto dto); + + @Operation(summary = "薪酬考勤数据支持") + @PostMapping(value = "/salaryAttendanceSupport") + Map salaryAttendanceSupport(@Valid @RequestBody SalaryAttendanceSupportDto dto); + + @Operation(summary = "考勤统计数据日度列表表头(薪酬)") + @PostMapping(value = "/attendanceDayStaTable") + List attendanceDayStaTable(); + + @Operation(summary = "考勤统计数据日度列表") + @PostMapping(value = "/attendanceDayStaList") + List attendanceDayStaList(@Valid @RequestBody SalaryAttendanceSupportDto dto); + + @Operation(summary = "获取日出勤信息") + @NoDataSourceBind + @PostMapping(value = "/getAttendanceDayStaList") + List getAttendanceDayStaList(@Valid @RequestBody DayStatisticsDto dto); + + @Operation(summary = "获取各维度出勤人数") + @PostMapping(value = "/getDimensionsAttendanceCountMap") + Map> getDimensionsAttendanceCountMap(@Valid @RequestBody DimensionsAttendanceCountDto dto); + + @Operation(summary = "获取各维度出勤人数") + @PostMapping(value = "/getDimensionsAttendanceDayCountMap") + Map> getDimensionsAttendanceDayCountMap(@Valid @RequestBody DimensionsAttendanceDayCountDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursDto.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursDto.java new file mode 100644 index 0000000..0c08ee4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursDto.java @@ -0,0 +1,32 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AttendanceCountAvgHoursDto { + /** + * 组织ID集合 + */ + @NotEmpty(message = "组织ID集合不能为空") + private List orgIds; + /** + * 开始月份(yyyyMM) + */ + @NotBlank(message = "开始月份不能为空") + private String startMonth; + /** + * 结束月份(yyyyMM) + */ + @NotBlank(message = "结束月份不能为空") + private String endMonth; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursVo.java new file mode 100644 index 0000000..0210cef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceCountAvgHoursVo.java @@ -0,0 +1,27 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AttendanceCountAvgHoursVo { + /** + * 月份 + */ + @NotBlank(message = "月份不能为空") + private String month; + /** + * 平均值 + */ + @NotNull(message = "平均值不能为空") + private BigDecimal avgHours = BigDecimal.ZERO; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserGroupVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserGroupVo.java new file mode 100644 index 0000000..fb09a3a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserGroupVo.java @@ -0,0 +1,24 @@ +package jnpf.attendance.dto; + +import lombok.Data; + +/** + * @Author huanglinpan + * @Date 2024/4/23 17:31 + * @Version 1.0 (版本号) + */ +@Data +public class AttendanceUserGroupVo { + /** + * 考勤组Id + */ + private String groupId; + /** + * 考勤组名称 + */ + private String groupName; + /** + * 用户Id + */ + private String userId; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserListGroupVO.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserListGroupVO.java new file mode 100644 index 0000000..b7811ac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/AttendanceUserListGroupVO.java @@ -0,0 +1,34 @@ +package jnpf.attendance.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-05-17 + */ +@Data +public class AttendanceUserListGroupVO implements Serializable { + /** + * 考勤组id + */ + private String groupId; + /** + * 考勤组名称 + */ + private String groupName; + + /** + * 考勤组名称 + */ + private String groupFullName; + + /** + * 用户Id + */ + private String userId; + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/CompareTypeEnums.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/CompareTypeEnums.java new file mode 100644 index 0000000..00102ff --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/CompareTypeEnums.java @@ -0,0 +1,30 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 查询维度枚举 + * + * @author Flynn Chan + * @create 2025-02-13 + */ +@AllArgsConstructor +@Getter +public enum CompareTypeEnums { + CURRENT(1, "本期"), + YOY(2, "同比"), + MOM(3, "环比"); + + private final int value; + private final String text; + public static CompareTypeEnums getTypeEnums(int value) { + CompareTypeEnums[] values = CompareTypeEnums.values(); + for (CompareTypeEnums typeEnums : values) { + if (typeEnums.getValue() == value) { + return typeEnums; + } + } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeDto.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeDto.java new file mode 100644 index 0000000..97b3118 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeDto.java @@ -0,0 +1,31 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.Date; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DateDimensionsRangeDto { + /** + * 查询维度枚举 + */ + @NotNull(message = "查询维度不能为空") + private CompareTypeEnums typeEnums; + /** + * 开始时间 + */ + @NotNull(message = "开始时间不能为空") + private Date startDate; + /** + * 结束时间 + */ + @NotNull(message = "结束时间不能为空") + private Date endDate; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeVo.java new file mode 100644 index 0000000..982fbc9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DateDimensionsRangeVo.java @@ -0,0 +1,24 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DateDimensionsRangeVo { + /** + * 查询维度枚举 + */ + @NotNull(message = "查询维度不能为空") + private CompareTypeEnums typeEnums; + /** + * 出勤人数 + */ + private Integer count; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceCountDto.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceCountDto.java new file mode 100644 index 0000000..5662fce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceCountDto.java @@ -0,0 +1,26 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DimensionsAttendanceCountDto { + /** + * 系统门店ID集合 + */ + @NotEmpty(message = "系统门店ID集合不能为空") + private List storeIds; + /** + * 时间维度集合 + */ + @NotEmpty(message = "时间维度集合不能为空") + private List dimensionsRangeList; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceDayCountDto.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceDayCountDto.java new file mode 100644 index 0000000..b1e93ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/DimensionsAttendanceDayCountDto.java @@ -0,0 +1,33 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Date; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DimensionsAttendanceDayCountDto { + /** + * 系统门店ID集合 + */ + @NotEmpty(message = "系统门店ID集合不能为空") + private List storeIds; + /** + * 开始时间 + */ + @NotNull(message = "开始时间不能为空") + private Date startDate; + /** + * 结束时间 + */ + @NotNull(message = "结束时间不能为空") + private Date endDate; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/GroupUpdateByUserDTO.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/GroupUpdateByUserDTO.java new file mode 100644 index 0000000..7957b80 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/GroupUpdateByUserDTO.java @@ -0,0 +1,28 @@ +package jnpf.attendance.dto; + +import lombok.Data; + +import java.util.Date; +import java.util.List; + +@Data +public class GroupUpdateByUserDTO { + /** + * 1入职 2离职 3调岗 4晋升 5批量删除 + */ + private Integer type; + /** + * 目标考勤组id + */ + private String toGroupId; + /** + * 用户id集合 + */ + private List userIds; + + private String tenantId; + /** + * 实际入职日期,yyyy-MM-dd + */ + private Date joiningDate; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsAbnormalConditionVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsAbnormalConditionVo.java new file mode 100644 index 0000000..560d045 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsAbnormalConditionVo.java @@ -0,0 +1,55 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsAbnormalConditionVo { + /** + * 正常次数 + */ + private Integer normalCount = 0; + /** + * 正常次数占比 + */ + private BigDecimal normalPercent = BigDecimal.ZERO; + /** + * 迟到次数 + */ + private Integer lateCount = 0; + /** + * 迟到次数占比 + */ + private BigDecimal latePercent = BigDecimal.ZERO; + /** + * 早退次数 + */ + private Integer earlyCount = 0; + /** + * 早退次数占比 + */ + private BigDecimal earlyPercent = BigDecimal.ZERO; + /** + * 缺卡次数 + */ + private Integer absenceCardCount = 0; + /** + * 缺卡次数占比 + */ + private BigDecimal absenceCardPercent = BigDecimal.ZERO; + /** + * 旷工次数 + */ + private Integer absenceCount = 0; + /** + * 旷工次数占比 + */ + private BigDecimal absencePercent = BigDecimal.ZERO; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDailySituationVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDailySituationVo.java new file mode 100644 index 0000000..ce05932 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDailySituationVo.java @@ -0,0 +1,23 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsDailySituationVo { + /** + * 出勤类型 + */ + private String typeName; + /** + * 工时(天) + */ + private BigDecimal avgDays; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsDto.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsDto.java new file mode 100644 index 0000000..7d4e8bd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsDto.java @@ -0,0 +1,27 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsDetailsDto { + /** + * 考勤组ID集合 + */ + @NotEmpty(message = "考勤组ID集合不能为空") + private List groupIds; + /** + * 月份(yyyyMM) + */ + @NotBlank(message = "开始月份不能为空") + private String month; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsVo.java new file mode 100644 index 0000000..ed4ca58 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsDetailsVo.java @@ -0,0 +1,63 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsDetailsVo { + /** + * 人均工时 + */ + private BigDecimal avgHours = BigDecimal.ZERO; + /** + * 人均工时同比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal avgHoursOnYear; + /** + * 人均工时环比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal avgHoursOnMonth; + /** + * 人均加班工时 + */ + private BigDecimal avgOverTimeHours = BigDecimal.ZERO; + /** + * 人均请假天数 + */ + private BigDecimal avgLeaveDays = BigDecimal.ZERO; + /** + * 人均请假天数同比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal avgLeaveDaysOnYear; + /** + * 人均请假天数环比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal avgLeaveDaysMonth; + /** + * 人均旷工天数 + */ + private BigDecimal avgAbsentDays = BigDecimal.ZERO; + /** + * 迟到人数 + */ + private BigDecimal lateCount = BigDecimal.ZERO; + /** + * 迟到人数同比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal lateCountOnYear; + /** + * 迟到人数环比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal lateCountOnMonth; + /** + * 早退人数 + */ + private BigDecimal earlyCount = BigDecimal.ZERO; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsFullSituationVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsFullSituationVo.java new file mode 100644 index 0000000..5f21838 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsFullSituationVo.java @@ -0,0 +1,31 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsFullSituationVo { + /** + * 日期 + */ + private String day; + /** + * 考勤组人数 + */ + private Integer groupUserCount; + /** + * 全勤人数 + */ + private Integer fullCount; + /** + * 全勤占比 + */ + private BigDecimal fullRatio; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsHoursRankingVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsHoursRankingVo.java new file mode 100644 index 0000000..4db86c7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsHoursRankingVo.java @@ -0,0 +1,23 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsHoursRankingVo { + /** + * 用户名 + */ + private String userName; + /** + * 工时(小时) + */ + private BigDecimal hours; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsOvertimeSituationVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsOvertimeSituationVo.java new file mode 100644 index 0000000..45530d5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsOvertimeSituationVo.java @@ -0,0 +1,32 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsOvertimeSituationVo { + /** + * 日期(MM.dd) + */ + private String day; + /** + * 加班工时(小时) + */ + private BigDecimal overtimeHours; + /** + * 加班工时同比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal overtimeHoursOnYear; + /** + * 加班工时环比增长(正负表示增减,为空表示没有可对比性展示--) + */ + private BigDecimal overtimeHoursOnMonth; + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsPerCapitaVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsPerCapitaVo.java new file mode 100644 index 0000000..bc9f658 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsPerCapitaVo.java @@ -0,0 +1,23 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsPerCapitaVo { + /** + * 日期(MM.dd) + */ + private String day; + /** + * 人均工时(小时) + */ + private BigDecimal avgHours; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsSituationVo.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsSituationVo.java new file mode 100644 index 0000000..dbf87d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/dto/MonthStatsSituationVo.java @@ -0,0 +1,27 @@ +package jnpf.attendance.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonthStatsSituationVo { + /** + * 日期(MM.dd) + */ + private String day; + /** + * 总人数 + */ + private Integer totalCount; + /** + * 全勤人数 + */ + private Integer onTimeCount; +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceApiFallback.java new file mode 100644 index 0000000..79ce2b6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceApiFallback.java @@ -0,0 +1,51 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceApi; +import jnpf.model.attendance.vo.attendance.AttendanceToThousandsFacesVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/27 + */ + +@Slf4j +@Component +public class AttendanceApiFallback implements FallbackFactory { + + @Override + public AttendanceApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceApi() { + @Override + public Boolean invalidationCoupons(String tenantId) { + log.error("定时失效考勤劵失败..."); + return Boolean.FALSE; + } + +// @Override +// public Boolean overtimeVouchers(String tenantId) { +// log.error("定时发放加班劵失败..."); +// return null; +// } + + @Override + public Boolean storageRest(String tenantId) { + log.error("每月定时计算用户存休失败..."); + return null; + } + + @Override + public AttendanceToThousandsFacesVo attendanceToThousandsFaces() { + log.error("每月定时计算用户存休失败..."); + return null; + } + + + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceConfirmApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceConfirmApiFallback.java new file mode 100644 index 0000000..c8f02b5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceConfirmApiFallback.java @@ -0,0 +1,37 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceConfirmApi; +import jnpf.base.ActionResult; +import jnpf.model.thousandsfaces.TodayWorkVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +public class AttendanceConfirmApiFallback implements FallbackFactory { + + @Override + public AttendanceConfirmApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceConfirmApi() { + + @Override + public ActionResult autoCreateConfirm() { + return null; + } + + @Override + public ActionResult confirmAutoSlippage() { + return null; + } + + @Override + public List getTodayWorkConfirmList() { + return List.of(); + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceDailyRuleApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceDailyRuleApiFallback.java new file mode 100644 index 0000000..f105fcc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceDailyRuleApiFallback.java @@ -0,0 +1,53 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceDailyRuleApi; +import jnpf.base.ActionResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +/** + * 获取用户信息Api降级处理 + * + * @author JNPF开发平台组 + * @version V3.1.0 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2021-03-24 + */ +@Component +@Slf4j +public class AttendanceDailyRuleApiFallback implements FallbackFactory { + @Override + public AttendanceDailyRuleApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceDailyRuleApi() { + @Override + public ActionResult initFixedScheduleRule(String tenantId) { + return ActionResult.fail("请求失败"); + } + + @Override + public ActionResult autoGrantBalance(String tenantId) { + return ActionResult.fail("请求失败"); + } + + @Override + public ActionResult userIsScheduling(String tenantId, String userId) { + return ActionResult.fail("请求失败"); + } + + @Override + public ActionResult> userIsSchedulingOrdinary(List organizeId) { + return ActionResult.fail("请求失败"); + } + + @Override + public ActionResult hasRuleByUserIdAndTime(String userId, Date start, Date end) { + return ActionResult.fail("请求失败"); + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceGroupApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceGroupApiFallback.java new file mode 100644 index 0000000..9db845c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceGroupApiFallback.java @@ -0,0 +1,62 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceGroupApi; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.AttendanceUserListGroupVO; +import jnpf.base.ActionResult; +import jnpf.entity.AttendanceGroup; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Title: + * @Author: peng.hao + * @create: 2024/4/23:16:09 + */ +@Slf4j +@Component +public class AttendanceGroupApiFallback implements FallbackFactory { + @Override + public AttendanceGroupApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceGroupApi() { + @Override + public AttendanceGroup queryTheNameOfTheAttendanceGroup(String groupId) { + return new AttendanceGroup(); + } + + @Override + public List getAttendanceUserGroup(List userIds) { + log.error("AttendanceGroupApiFallback调用getAttendanceUserGroup失败"); + return new ArrayList<>(); + } + + @Override + public List getGroupListByOrgId(String organizeId) { + return null; + } + + @Override + public ActionResult> getAttendanceUserListGroupVO(List userIds) { + return null; + } + + @Override + public ActionResult getAttendanceGroupByName(String organizeName, String groupName) { + return null; + } + + @Override + public boolean checkScheduling() { + return false; + } + + + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceLineSchedulingConfigApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceLineSchedulingConfigApiFallback.java new file mode 100644 index 0000000..f2c69ef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceLineSchedulingConfigApiFallback.java @@ -0,0 +1,31 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceApi; +import jnpf.attendance.AttendanceLineSchedulingConfigApi; +import jnpf.model.attendance.vo.attendance.AttendanceToThousandsFacesVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/27 + */ + +@Slf4j +@Component +public class AttendanceLineSchedulingConfigApiFallback implements FallbackFactory { + + @Override + public AttendanceLineSchedulingConfigApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceLineSchedulingConfigApi() { + @Override + public Boolean noticeLineScheduling(String tenantId) { + return null; + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceSimulateDataApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceSimulateDataApiFallback.java new file mode 100644 index 0000000..356d1f9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceSimulateDataApiFallback.java @@ -0,0 +1,30 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceSimulateDataApi; +import jnpf.attendance.FtbClockInApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * 打卡api fallback + * + * @author yanwenfu + * @create 2023-12-04 + */ +@Slf4j +@Component +public class AttendanceSimulateDataApiFallback implements FallbackFactory { + + @Override + public AttendanceSimulateDataApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceSimulateDataApi() { + + @Override + public void clockIn(String tenantId) throws Exception { + log.error("调用模拟打卡失败..."); + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceUserApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceUserApiFallback.java new file mode 100644 index 0000000..2e3e42d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/AttendanceUserApiFallback.java @@ -0,0 +1,38 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.AttendanceUserApi; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.base.ActionResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/27 + */ + +@Slf4j +@Component +public class AttendanceUserApiFallback implements FallbackFactory { + + @Override + public AttendanceUserApi create(Throwable cause) { + cause.printStackTrace(); + return new AttendanceUserApi() { + @Override + public ActionResult groupUpdateByPersonnel(GroupUpdateByUserDTO groupUpdateByUserDTO) { + log.error("定时失效考勤劵失败..."); + return null; + } + + @Override + public ActionResult userGroupUpdateBySecondNotice(String tenantId) { + return null; + } + + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbClockInApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbClockInApiFallback.java new file mode 100644 index 0000000..5f18327 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbClockInApiFallback.java @@ -0,0 +1,60 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.FtbClockInApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * 打卡api fallback + * + * @author yanwenfu + * @create 2023-12-04 + */ +@Slf4j +@Component +public class FtbClockInApiFallback implements FallbackFactory { + + @Override + public FtbClockInApi create(Throwable cause) { + cause.printStackTrace(); + return new FtbClockInApi() { + + @Override + public Boolean dailyRuleChangeExecute() { + log.error("dailyRuleChangeExecute调用失败..."); + return Boolean.FALSE; + } + + @Override + public Boolean generateFtbAbsenceRecord(String tenantId) { + log.error("generateFtbAbsenceRecord调用失败..."); + return Boolean.FALSE; + } + + @Override + public Boolean generateAbsenceTask(String tenantId) { + log.error("generateAbsenceTask调用失败..."); + return Boolean.FALSE; + } + + @Override + public Boolean generateBeforeWorkRemind(String tenantId) { + log.error("generateBeforeWorkRemind调用失败..."); + return false; + } + + @Override + public Boolean generateRepairNum(String tenantId) { + log.error("生成补卡次数失败..."); + return Boolean.FALSE; + } + + @Override + public Boolean continuousCheck(String tenantId) { + log.error("判定连续动作失败..."); + return Boolean.FALSE; + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbStatisticsApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbStatisticsApiFallback.java new file mode 100644 index 0000000..8f31f47 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/attendance/fallback/FtbStatisticsApiFallback.java @@ -0,0 +1,129 @@ +package jnpf.attendance.fallback; + +import jnpf.attendance.FtbStatisticsApi; +import jnpf.attendance.dto.*; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.DayStatisticsDto; +import jnpf.model.attendance.dto.SalaryAttendanceSupportDto; +import jnpf.model.attendance.vo.attendance.AttendanceCustomizeTableVo; +import jnpf.model.attendance.vo.attendance.DayStatisticsPageListVo; +import jnpf.model.attendance.vo.attendance.DayStatisticsVo; +import jnpf.model.attendance.vo.attendance.SalaryAttendanceSupportVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class FtbStatisticsApiFallback implements FallbackFactory { + @Override + public FtbStatisticsApi create(Throwable cause) { + // 打印降级原因 + log.error("进入 FtbStatisticsApi 的 fallback,降级原因:", cause); + return new FtbStatisticsApi() { + @Override + public ActionResult userDayStatisticsInit(String tenantId) { + return ActionResult.success(Boolean.FALSE); + } + + @Override + public ActionResult dayStatisticsNotice(String tenantId) { + return ActionResult.success(Boolean.FALSE); + } + + @Override + public ActionResult monthStatisticsNotice(String tenantId) { + return ActionResult.success(Boolean.FALSE); + } + + @Override + public ActionResult teamMonthStatisticsNotice(String tenantId) { + return ActionResult.success(Boolean.FALSE); + } + + @Override + public void consentUnscheduledNotice(String tenantId) { + + } + + @Override + public ActionResult autoSealTimer(String tenantId) { + return ActionResult.success(Boolean.FALSE); + } + + @Override + public List countAttendanceAvgHours(AttendanceCountAvgHoursDto dto) { + return List.of(); + } + + @Override + public ActionResult getAttendanceAvgHoursDetails(MonthStatsDetailsDto dto) { + return ActionResult.success(null); + } + + @Override + public ActionResult> getAttendanceMonthPerCapita(MonthStatsDetailsDto dto) { + return ActionResult.success(List.of()); + } + + @Override + public ActionResult> getAttendanceDailySituation(MonthStatsDetailsDto dto) { + return ActionResult.success(List.of()); + } + + @Override + public ActionResult> getAttendanceHoursRanking(MonthStatsDetailsDto dto) { + return ActionResult.success(List.of()); + } + + @Override + public ActionResult> getAttendanceFullSituation(MonthStatsDetailsDto dto) { + return ActionResult.success(List.of()); + } + + @Override + public ActionResult getAttendanceAbnormalCondition(MonthStatsDetailsDto dto) { + return ActionResult.success(null); + } + + @Override + public ActionResult> getAttendanceOvertimeSituation(MonthStatsDetailsDto dto) { + return ActionResult.success(List.of()); + } + + @Override + public Map salaryAttendanceSupport(SalaryAttendanceSupportDto dto) { + return Map.of(); + } + + @Override + public List attendanceDayStaTable() { + return List.of(); + } + + @Override + public List attendanceDayStaList(SalaryAttendanceSupportDto dto) { + return List.of(); + } + + @Override + public List getAttendanceDayStaList(DayStatisticsDto dto) { + return List.of(); + } + + @Override + public Map> getDimensionsAttendanceCountMap(DimensionsAttendanceCountDto dto) { + return Map.of(); + } + + @Override + public Map> getDimensionsAttendanceDayCountMap(DimensionsAttendanceDayCountDto dto) { + return Map.of(); + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/FtbAuthorityApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/FtbAuthorityApi.java new file mode 100644 index 0000000..b56934f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/FtbAuthorityApi.java @@ -0,0 +1,226 @@ +package jnpf.authority; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.authority.fallback.FtbAuthorityApiFallback; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.menu.FunctionMenuRemoteDTO; +import jnpf.model.authority.dto.menu.FuntionMenuDTO; +import jnpf.model.authority.dto.permission.OrganizeWithPositionsDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleInfoDTO; +import jnpf.model.authority.vo.person.FtbRoleListDropDownVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.vo.common.InnerPowerPositionVO; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.login.MenuTreeVO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryPageUserMoreKeywordDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.organize.OrganizeAndPositionListVO; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; +import jnpf.permission.vo.v2.position.PositionListUserVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.NoDataSourceBind; +import lombok.SneakyThrows; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2025/3/27 + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbAuthorityApiFallback.class) +public interface FtbAuthorityApi { + /** + * 获取当前人员的数据权限范围 + * + * @return + */ + @GetMapping("/web/permission-role/query-user-scope-permission") + Map queryUserScopeOfPermission(@RequestParam("userId") String userId); + + @Operation(summary = "[门店基础信息列表auth api] 根据登录人权限得到权限范围内门店基础信息") + @GetMapping(value = "/permission/organize/list/store/login") + List authStoreBaseListInfo(); + + @Operation(summary = "[列表] 指定组织下得所有员工信息(FTB带权限)") + @GetMapping("/permission/users/list/targetOrganize/auth/api") + List listTargetOrganizeIdAuthApi(@RequestParam(value = "organizeId") String organizeId, @RequestParam(value = "userWorkStatusEnumsList", required = false) List userWorkStatusEnumsList); + + @Operation(summary = "[列表] 指定组织集合下得所有员工信息(FTB带权限)") + @PostMapping("/permission/users/list/targetOrganizeIds/auth/api") + List listTargetOrganizeIdsAuthApi(@RequestBody List organizeIds, @RequestParam(value = "userWorkStatusEnumsList", required = false) List userWorkStatusEnumsList); + + @Operation(summary = "[分页] 在职人员列表") + @PostMapping("/permission/users/page") + @NoDataSourceBind + ActionResult> pagePost(@RequestBody QueryPageUserDTO dto); + + @Operation(summary = "[分页] 在职人员列表POST更多关键字信息") + @PostMapping("/permission/page/moreKeyword") + @NoDataSourceBind + ActionResult> pagePostMoreKeyword(@RequestBody QueryPageUserMoreKeywordDTO dto); + + @Operation(summary = "[列表]人员权限过滤后的岗位用户列表") + @GetMapping("/permission/position/tree/users") + ActionResult> authListPositionTreeUser(); + + @Operation(summary = "[api列表]权限范围内的多个组织及其岗位列表(不查人)") + @PostMapping("/permission/position/api/list/auth/organize-with-positions") + List authOrganizeWithPositions(@RequestBody OrganizeWithPositionsDTO dto); + + @Operation(summary = "[列表]权限过滤,获取所有用户信息,含绑定关系,或指定用户. 不使用权限则返回全部") + @PostMapping("/permission/users/info/all/auth") + @NoDataSourceBind + ActionResult> authGetAllUserInfoBatch(); + + @Operation(summary = "[列表]权限范围内仅返回用户 id 列表,不查关系与 VO") + @PostMapping("/permission/users/ids/auth") + @NoDataSourceBind + ActionResult> authGetPermissionScopeUserIds(); + + @Operation(summary = "[树形]人员权限过滤后的组织(公司,部门,门店,班主),人员") + @GetMapping(value = "/permission/organize/tree/users") + ActionResult> listOrganizeTreeUsers(); + + @Operation(summary = "[列表]人员权限过滤后的组织列表") + @GetMapping(value = "/permission/organize/info/list/users") + @NoDataSourceBind + ActionResult> authOrganizesByUserBound(@RequestParam(value = "organizeCategoryEnums", required = false) List organizeCategoryEnums); + + /** + * @param organizeCategoryEnums 组织类型 + * @param workStatusEnums 工作状态 + * @param withEmployee 是否包含人员 + * @param filterBindOtherStore 过滤绑定第三方信息门店 + * @param filterBindPayStore 过滤收银平台门店 + * @return 组织树(with auth) + */ + @Operation(summary = "[api树形,滤掉没有的分支]人员权限过滤后的组织(公司,部门,门店,班主),默认不带人员") + @GetMapping(value = "/permission/organize/api/tree/users/filterNode") + List listOrganizeTreeFilterNode( + @RequestParam(value = "organizeCategoryEnums", required = false) List organizeCategoryEnums, + @RequestParam(value = "workStatusEnums", required = false) List workStatusEnums, + @RequestParam(value = "withEmployee", required = false) Boolean withEmployee, + @RequestParam(value = "filterBindOtherStore", required = false) Boolean filterBindOtherStore, + @RequestParam(value = "filterBindPayStore", required = false) Boolean filterBindPayStore + ); + + @Operation(summary = "角色列表下拉") + @GetMapping(value = "/web/permission-role-authorize-person/allRolesOfTheCurrentPerson") + @NoDataSourceBind + ActionResult> roleListDropDown(@RequestParam("userId") String userId, @RequestParam("tenantId") String tenantId); + + /** + * 根据模块编码获取当前人员的数据权限范围 + * + * @return + */ + @GetMapping("/web/permission-role/query-user-scope-permission-for-module-code") + InnerPowerUserVO queryUserScopeOfPermissionForModuleCode(); + + /** + * 远程client + * 包含新增修改 + */ + @SneakyThrows + @PostMapping("/remoteClient") + @NoDataSourceBind + void remoteClient(FunctionMenuRemoteDTO dto); + + + @GetMapping("/position/get-curr-login-user-has-permission-position-id") + InnerPowerPositionVO getCurrLoginUserHasPermissionPositionId(@RequestParam(name = "permissionModule", defaultValue = "") String permissionModule); + + /** + * 更具岗位id清除所有绑定的角色 + */ + @PostMapping("/web/permission-role-authorize-post/clearRoleByPostId") + void clearRoleByPostId(@RequestBody List postIds); + + /** + * @param userIds 用户列表 + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param tenantId 租户id + * @return + */ + @Operation(summary = "[列表]根据用户列表批量查询权限范围的门店") + @PostMapping(value = "/permission/organize/batchForUserIds/{status}/{moduleId}/{tenantId}") + @NoDataSourceBind + Map> batchAuthOrganizesForUserIdsAndTenantId(@RequestBody List userIds, @PathVariable("status") Integer status,@PathVariable("moduleId") String moduleId,@PathVariable("tenantId") String tenantId); +/** + * @param userIds 用户列表 + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param tenantId 租户id + * @return + */ + @Operation(summary = "[列表]根据用户列表批量查询权限范围的门店") + @PostMapping(value = "/permission/organize/batchAllForUserIds/{status}/{moduleId}/{tenantId}") + @NoDataSourceBind + Map> batchAuthOrganizesAllForUserIdsAndTenantId(@RequestBody List userIds, @PathVariable("status") Integer status,@PathVariable("moduleId") String moduleId,@PathVariable("tenantId") String tenantId); + + /** + * @param userIds 用户列表 + * @param status 状态 1:禁用 0:启用 -1-所有 + * @return + */ + @Operation(summary = "[列表]根据用户列表批量查询权限范围的门店") + @PostMapping(value = "/permission/organize/batchForUserIds/{status}") + Map> batchAuthOrganizesForUserIds(@RequestBody List userIds, @PathVariable("status") Integer status); + + + /** + * 角色列表 + * + * @return {@link ActionResult }<{@link PageListVO }<{@link FtbPermissionRoleInfoVO }>> + */ + @GetMapping("/web/permission-role/role-list") + ActionResult> permissionList(@SpringQueryMap CultivatePage cultivatePage, @SpringQueryMap FtbPermissionRoleInfoDTO roleInfoDTO); + + /** + * 当前登录用户所有权限用户id + * + * @return 用户id集合 + */ + @GetMapping("/web/permission-role-authorize-person/permission-user") + ActionResult> permissionUser(@RequestParam("userId") String userId, @RequestParam("permissionModule") String permissionModule, @RequestParam("category") String category); + + /** + * 员工已有的功能权限标识集合 + */ + @GetMapping("/web/permission-role/permission-identification-collection") + ActionResult permissionIdentificationCollection(); + + /** + * 查询那些人员具有当前按钮权限 + */ + @PostMapping("/web/permission-role-authorize-person/queryButtonPermission") + List queryButtonPermission(@RequestBody FuntionMenuDTO funtion); + + /** + * 根据用户Id获取用户权限的用户Id集合(超级管理员不要调用此方法) + * @return 用户id->所具有的权限用户id集合 + */ + @PostMapping("/web/permission-role-authorize-person/collection-of-user") + Map> getUserPermissionUserCollection(@RequestBody List userIds); + + @GetMapping(value = "/permission/organize/user-auth") + TargetAuthIdsVO getUserAuth(); + /** + * 当前登录用户菜单树(与 FTB 登录菜单一致;type=Web|App) + */ + @GetMapping("/web/permission-role/obtain-menus-on-token") + ActionResult> obtainMenusOnToken(@RequestParam(value = "type", defaultValue = "Web") String type); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/fallback/FtbAuthorityApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/fallback/FtbAuthorityApiFallback.java new file mode 100644 index 0000000..c4a313c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/authority/fallback/FtbAuthorityApiFallback.java @@ -0,0 +1,188 @@ +package jnpf.authority.fallback; + +import jnpf.authority.FtbAuthorityApi; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.menu.FunctionMenuRemoteDTO; +import jnpf.model.authority.dto.menu.FuntionMenuDTO; +import jnpf.model.authority.dto.permission.OrganizeWithPositionsDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleInfoDTO; +import jnpf.model.authority.vo.person.FtbRoleListDropDownVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.vo.common.InnerPowerPositionVO; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.login.MenuTreeVO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryPageUserMoreKeywordDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.organize.OrganizeAndPositionListVO; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; +import jnpf.permission.vo.v2.position.PositionListUserVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class FtbAuthorityApiFallback implements FallbackFactory { + + @Override + public FtbAuthorityApi create(Throwable cause) { + cause.printStackTrace(); + return new FtbAuthorityApi() { + @Override + public Map queryUserScopeOfPermission(String userId) { + log.error("queryUserScopeOfPermission 调用失败..."); + return null; + } + + @Override + public List authStoreBaseListInfo() { + return List.of(); + } + + @Override + public List listTargetOrganizeIdAuthApi(String organizeId, List userWorkStatusEnumsList) { + return List.of(); + } + + @Override + public List listTargetOrganizeIdsAuthApi(List organizeIds, List userWorkStatusEnumsList) { + return List.of(); + } + + @Override + public ActionResult> pagePost(QueryPageUserDTO dto) { + return null; + } + + @Override + public ActionResult> pagePostMoreKeyword(QueryPageUserMoreKeywordDTO dto) { + return null; + } + + @Override + public ActionResult> authListPositionTreeUser() { + return null; + } + + @Override + public List authOrganizeWithPositions(OrganizeWithPositionsDTO dto) { + return List.of(); + } + + @Override + public ActionResult> authGetAllUserInfoBatch() { + return null; + } + + @Override + public ActionResult> authGetPermissionScopeUserIds() { + return null; + } + + @Override + public ActionResult> listOrganizeTreeUsers() { + return null; + } + + @Override + public ActionResult> authOrganizesByUserBound(List organizeCategoryEnums) { + return null; + } + + @Override + public List listOrganizeTreeFilterNode(List organizeCategoryEnums, List workStatusEnums, Boolean withEmployee, Boolean filterBindOtherStore, Boolean filterBindPayStore) { + return List.of(); + } + + @Override + public ActionResult> roleListDropDown(String userId, String tenantId) { + return null; + } + + @Override + public InnerPowerUserVO queryUserScopeOfPermissionForModuleCode() { + return null; + } + + @Override + public void remoteClient(FunctionMenuRemoteDTO dto) { + + } + + @Override + public InnerPowerPositionVO getCurrLoginUserHasPermissionPositionId(String permissionModule) { + return null; + } + + @Override + public void clearRoleByPostId(List postIds) { + + } + + @Override + public Map> batchAuthOrganizesForUserIdsAndTenantId(List userIds, Integer status, String moduleId, String tenantId) { + return Map.of(); + } + @Override + public Map> batchAuthOrganizesAllForUserIdsAndTenantId(List userIds, Integer status, String moduleId, String tenantId) { + return Map.of(); + } + + @Override + public Map> batchAuthOrganizesForUserIds(List userIds, Integer status) { + return null; + } + + + @Override + public ActionResult> permissionList(CultivatePage cultivatePage, FtbPermissionRoleInfoDTO roleInfoDTO) { + return null; + } + + @Override + public ActionResult> permissionUser(String userId, String permissionModule, String category) { + return null; + } + + @Override + public ActionResult permissionIdentificationCollection() { + return null; + } + + @Override + public List queryButtonPermission(FuntionMenuDTO funtion) { + return null; + } + + @Override + public Map> getUserPermissionUserCollection(List userIds) { + return Collections.emptyMap(); + } + + @Override + public ActionResult> obtainMenusOnToken(String type) { + log.warn("FtbAuthorityApi fallback: obtainMenusOnToken type={}", type); + return ActionResult.success(Collections.emptyList()); + } + + @Override + public TargetAuthIdsVO getUserAuth() { + return null; + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateManageApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateManageApi.java new file mode 100644 index 0000000..c0e88d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateManageApi.java @@ -0,0 +1,57 @@ +package jnpf.certificate; + +import jnpf.base.ActionResult; +import jnpf.certificate.fallback.CertificateManageFallbackApi; +import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +@FeignClient(name = "jnpf-ftb", fallback = CertificateManageFallbackApi.class, path = "/web/certificate-manage-api") +public interface CertificateManageApi { + + /** + * 根据组织ID查询营业执照信息。 + * + * @param organizeId 组织ID + * @return 营业执照信息 + */ + @GetMapping("/query-business-license") + ActionResult queryBusinessLicense(@RequestParam("organizeId") String organizeId); + + /** + * 根据组织ID列表批量查询营业执照信息。 + * + * @param organizeIds 组织ID列表 + * @return 营业执照信息列表 + */ + @PostMapping("/query-business-license-batch") + ActionResult> queryBusinessLicenseBatch(@RequestBody Collection organizeIds); + + /** + * 保存组织营业执照信息。 + * + * @param req 营业执照保存参数 + * @return 操作结果 + */ + @PostMapping("/save-business-license") + ActionResult saveBusinessLicense(@Valid @RequestBody CertificateOrganizeBusinessLicenseVO req); + + /** + * 根据组织ID删除该主体下全部证照信息。 + * + * @param organizeId 组织ID + * @param loginUserId 登录用户ID + * @return 操作结果 + */ + @DeleteMapping("/delete-business-license") + ActionResult deleteBusinessLicense(@RequestParam("organizeId") String organizeId, + @RequestParam(value = "loginUserId", required = false) String loginUserId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateWarningApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateWarningApi.java new file mode 100644 index 0000000..660c5ba --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/CertificateWarningApi.java @@ -0,0 +1,19 @@ +package jnpf.certificate; + +import jnpf.base.ActionResult; +import jnpf.certificate.fallback.CertificateWarningFallbackApi; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "jnpf-ftb", fallback = CertificateWarningFallbackApi.class, path = "/web/certificate-warning-api") +public interface CertificateWarningApi { + + /** + * 检查并发送预警 + * @param tenantId + * @return + */ + @PostMapping("/checkAndSendCertificateWarning") + ActionResult checkAndSendCertificateWarning(@RequestParam("tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateManageFallbackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateManageFallbackApi.java new file mode 100644 index 0000000..21a8552 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateManageFallbackApi.java @@ -0,0 +1,44 @@ +package jnpf.certificate.fallback; + +import jnpf.base.ActionResult; +import jnpf.certificate.CertificateManageApi; +import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 组织营业执照接口降级实现。 + */ +@Slf4j +@Component +public class CertificateManageFallbackApi implements CertificateManageApi { + + @Override + public ActionResult queryBusinessLicense(String organizeId) { + log.error("queryBusinessLicense调用失败,organizeId={}", organizeId); + return ActionResult.success(null); + } + + @Override + public ActionResult> queryBusinessLicenseBatch(Collection organizeIds) { + log.error("queryBusinessLicenseBatch调用失败,organizeIds={}", organizeIds); + return ActionResult.success(Collections.emptyList()); + } + + @Override + public ActionResult saveBusinessLicense(CertificateOrganizeBusinessLicenseVO req) { + log.error("saveBusinessLicense调用失败,req={}", req); + return ActionResult.success(); + } + + @Override + public ActionResult deleteBusinessLicense(String organizeId, String loginUserId) { + log.error("deleteBusinessLicense failed, organizeId={}.loginUserId={}", organizeId,loginUserId); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateWarningFallbackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateWarningFallbackApi.java new file mode 100644 index 0000000..4891a9b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/certificate/fallback/CertificateWarningFallbackApi.java @@ -0,0 +1,16 @@ +package jnpf.certificate.fallback; + +import jnpf.base.ActionResult; +import jnpf.certificate.CertificateWarningApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CertificateWarningFallbackApi implements CertificateWarningApi { + @Override + public ActionResult checkAndSendCertificateWarning(String tenantId) { + log.error("checkAndSendCertificateWarning调用失败,tenantId={}", tenantId); + return ActionResult.success(Boolean.FALSE); + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateIdentifyApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateIdentifyApi.java new file mode 100644 index 0000000..684ab06 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateIdentifyApi.java @@ -0,0 +1,33 @@ +package jnpf.cultivate; + +import jnpf.cultivate.fallback.FtbCultivateIdentifyApiFallback; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 鉴定api + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbCultivateIdentifyApiFallback.class, path = "/cultivate") +public interface FtbCultivateIdentifyApi { + /** + * 处理鉴定逾期数据 + * + * @param tenantId + * @return + */ + @PostMapping(value = "/apply/setIdentifyBeOverdue") + @NoDataSourceBind + Boolean setIdentifyBeOverdue(@RequestParam(value = "tenantId") String tenantId); + + /** + * 计划鉴定时间提醒 + * + * @param tenantId + * @return + */ + @PostMapping(value = "/apply/setIdentifyRemind") + @NoDataSourceBind + Boolean setIdentifyRemind(@RequestParam(value = "tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateLearnTaskListApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateLearnTaskListApi.java new file mode 100644 index 0000000..110808d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateLearnTaskListApi.java @@ -0,0 +1,33 @@ +package jnpf.cultivate; + +import jnpf.cultivate.fallback.FtbCultivateLearnTaskApiFallback; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +/** + * 鉴定api + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbCultivateLearnTaskApiFallback.class, path = "/web/learnTaskList/") +public interface FtbCultivateLearnTaskListApi { + + /** + * 定时加入任务 + * + * @return + */ + @GetMapping("/timing/timingAddNewPersonToTaskNew/{tenantId}") + @NoDataSourceBind + void timingAddNewPersonToTaskNew(@PathVariable("tenantId") String tenantId); + + + /** + * 定时提醒任务(每天定点) + * + * @return + */ + @GetMapping("/timing/timingTaskLearningAlertNew/{tenantId}") + @NoDataSourceBind + void timingTaskLearningAlertNew(@PathVariable("tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivatePromotionApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivatePromotionApi.java new file mode 100644 index 0000000..dd08646 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivatePromotionApi.java @@ -0,0 +1,42 @@ +package jnpf.cultivate; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.cultivate.fallback.FtbCultivatePromotionFallback; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * @Title:FtbCultivatePromotionMemberApi + * @Author:peng.hao + * @create: 2024/1/1815:58 + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbCultivatePromotionFallback.class ) +public interface FtbCultivatePromotionApi { + + /** + * 拖动删除成员启用对应晋升通道 + * @param userIds 用户id + * @return + */ + @PutMapping("/cul_pro_member/deleteMembersToProChannel") + ActionResult deleteMembersToProChannel(@RequestBody List userIds); + + /** + * 获取是否有人岗位职等的晋升申请 + * @param postId 公司岗位ID + * @param grandId 职等ID + * @param userId 用户ID + * @return 成功结果 + */ + @GetMapping("/cul-post-apply/userIsHasApply") + @Operation(summary = "查看当前的人岗位职等是否存在晋升申请") + ActionResult isThereAnAppForThePosLevel(@RequestParam("postId") String postId, + @RequestParam("grandId") String grandId, + @RequestParam("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateStoreStatisticApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateStoreStatisticApi.java new file mode 100644 index 0000000..a9ecb1a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateStoreStatisticApi.java @@ -0,0 +1,101 @@ +package jnpf.cultivate; + +import jnpf.base.ActionResult; +import jnpf.cultivate.fallback.FtbCultivateStoreStatisticApiFallback; +import jnpf.model.cultivate.dto.storestatistics.*; +import jnpf.model.cultivate.vo.position.*; +import jnpf.model.thousandsfaces.TodayWorkVo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +/** + * 鉴定api + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbCultivateStoreStatisticApiFallback.class, path = "/web/store-index-statistics") +public interface FtbCultivateStoreStatisticApi { + /** + * 店长页面 培训统计数量 + */ + @PostMapping("/get-cultivate-count") + ActionResult getCultivateCount(@RequestBody FtbCultivateStoreStatisticsReq req); + + /** + * 查询我的考试列表 + * + * @param req + * @return + */ + @PostMapping("/queryMyExamList") + ActionResult> queryMyExamList(@RequestBody StoreStatisticsMyExamReq req); + + + /** + * 待我批阅(下属的) + * + * @param req + * @return + */ + @PostMapping("/queryWaitMyReadOver") + ActionResult> queryWaitMyReadOver(@RequestBody StoreStatisticsWaitMyCheckExamReq req); + + + /** + * 今日工作-待参与及参与中的线下培训<负责人及参与人均属于相关人员>列表 + * + * @param req + * @return + */ + @PostMapping("/queryOfflineTrainList") + ActionResult> queryOfflineTrainList(@RequestBody StoreOfflineTrainReq req); + + + /** + * 今日工作-我的任务(当日开始的培训任务)列表 + * + * @param req + * @return + */ + @PostMapping("/myTaskList") + ActionResult> storeMyTaskList(@RequestBody StoreOfflineTrainReq req); + + /** + * 今日工作-鉴定他人(当日的待鉴定)列表 + * + * @param req + * @return + */ + @PostMapping("/myIdentityList") + ActionResult> storeMyIdentityList(@RequestBody StoreIdentityReq req); + + /** + * 员工界面 查询未学习的通用课程数量 + * + * @param req + * @return + */ + @PostMapping("/get-worker-cultivate-count") + ActionResult getWorkerCultivateCount(@RequestBody FtbCultivateStoreStatisticsReq req); + + + /** + * 工作台-员工 + */ + @PostMapping("/person/training-statistics") + ActionResult personTrainingStatistics(@RequestBody FtbCultivateStoreStatisticsReq req); + + + /** + * 工作台-店长 + */ + @PostMapping("/store-manager/training-statistics") + ActionResult storeManagerTrainingStatistics(@RequestBody FtbCultivateStoreStatisticsReq req); + + /** + * 工作台-管理层 + */ + @PostMapping("/manager/training-statistics") + ActionResult managerTrainingStatistics(@RequestBody FtbCultivateStoreStatisticsReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateTeachingApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateTeachingApi.java new file mode 100644 index 0000000..ed9a88b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/FtbCultivateTeachingApi.java @@ -0,0 +1,51 @@ +package jnpf.cultivate; + +import jnpf.cultivate.fallback.FtbCultivateTeachingApiFallback; +import jnpf.model.cultivate.vo.teaching.MyPracticeSummaryVo; +import jnpf.model.cultivate.vo.teaching.SuperiorTeachingSummaryVo; +import jnpf.model.cultivate.vo.teaching.TeachingStoreCountVo; +import jnpf.model.cultivate.vo.teaching.TodaySummaryDataVo; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 带教api + */ +@FeignClient(name = "jnpf-ftb", fallbackFactory = FtbCultivateTeachingApiFallback.class, path = "/") +public interface FtbCultivateTeachingApi { + + /** + * 店长界面我的带教统计 + * @param storeId 门店id + * @return TeachingStoreCountVo + */ + @GetMapping("app/teachingRecord/storeTeachingCount") + TeachingStoreCountVo storeTeachingCount(@RequestParam("storeId") String storeId); + + /** + * App店长界面-今日练习汇总数据 + * + * @param storeId 查询参数 + * @return 练习汇总数据 + */ + @GetMapping("teachingRecord/app/getTodaySummary") + TodaySummaryDataVo getTodaySummary(@RequestParam("storeId") String storeId); + + /** + * 员工界面-上级带教统计 + * @param storeId 门店id + * @return TeachingStoreCountVo + */ + @GetMapping("app/teachingRecord/getSuperiorTeachingSummary") + SuperiorTeachingSummaryVo getSuperiorTeachingSummary(@RequestParam("storeId") String storeId); + + /** + * 员工界面--我的练习汇总数据 + * + * @param storeId 查询参数 + * @return 练习汇总数据 + */ + @GetMapping("teachingRecord/app/getMyPracticeSummary") + MyPracticeSummaryVo getMyPracticeSummary(@RequestParam("storeId") String storeId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/V2CultivateOldDealApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/V2CultivateOldDealApi.java new file mode 100644 index 0000000..4a57efc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/V2CultivateOldDealApi.java @@ -0,0 +1,51 @@ +package jnpf.cultivate; + +import jnpf.base.ActionResult; +import jnpf.cultivate.fallback.V2CultivateOldDealApiFallback; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "jnpf-ftb", fallbackFactory = V2CultivateOldDealApiFallback.class, path = "/v2/cultivate/old") +public interface V2CultivateOldDealApi { + + /** + * 课程心得评论旧数据处理 + * + * @param tenantId 租户ID + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/gained/comment") + ActionResult dealOldData(@RequestParam("tenantId") String tenantId); + + @NoDataSourceBind + @GetMapping("/identify/deal-old") + ActionResult identifyDealOldData(@RequestParam("tenantId") String tenantId); + + /** + * 迁移旧培训任务数据到新的任务日志表 + * + * @param tenantId 租户ID + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/task/old-data") + ActionResult migrateOldTaskData(@RequestParam("tenantId") String tenantId); + + /** + * 旧晋升通道成员数据迁移接口 + * 将 ftb_cultivate_promotion_member_new 表中的用户数据迁移到 + * ftb_cultivate_promotion_user 和 ftb_cultivate_promotion_setting 表中 + * 同时校验用户是否为正常状态(enabledMark = 1) + * + * @param tenantId 租户ID + * @param promotionId 晋升通道ID(可选,如果传入则只迁移指定通道的数据) + * @return 响应结果 + */ + @NoDataSourceBind + @GetMapping("/promotion/migrate-old-data") + ActionResult migratePromotionOldData(@RequestParam("tenantId") String tenantId, + @RequestParam(value = "promotionId", required = false) String promotionId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateIdentifyApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateIdentifyApiFallback.java new file mode 100644 index 0000000..3129b56 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateIdentifyApiFallback.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.fallback; + +import jnpf.cultivate.FtbCultivateIdentifyApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FtbCultivateIdentifyApiFallback implements FallbackFactory { + + @Override + public FtbCultivateIdentifyApi create(Throwable cause) { + cause.printStackTrace(); + return new FtbCultivateIdentifyApi() { + @Override + public Boolean setIdentifyBeOverdue(String tenantId) { + log.error("setIdentifyBeOverdue调用失败..."); + return false; + } + + @Override + public Boolean setIdentifyRemind(String tenantId) { + log.error("setIdentifyRemind调用失败..."); + return false; + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateLearnTaskApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateLearnTaskApiFallback.java new file mode 100644 index 0000000..1cc0c6f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateLearnTaskApiFallback.java @@ -0,0 +1,31 @@ +package jnpf.cultivate.fallback; + +import jnpf.cultivate.FtbCultivateLearnTaskListApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FtbCultivateLearnTaskApiFallback implements FallbackFactory { + + + @Override + public FtbCultivateLearnTaskListApi create(Throwable cause) { + log.error("进入 FtbCultivateLearnTaskApiFallback 的 fallback,降级原因:", cause); // 打印降级原因 + + return new FtbCultivateLearnTaskListApi() { + + + @Override + public void timingAddNewPersonToTaskNew(String tenantId) { + + } + + @Override + public void timingTaskLearningAlertNew(String tenantId) { + + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivatePromotionFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivatePromotionFallback.java new file mode 100644 index 0000000..a1ac30e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivatePromotionFallback.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.fallback; + +import jnpf.base.ActionResult; +import jnpf.cultivate.FtbCultivatePromotionApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +public class FtbCultivatePromotionFallback implements FallbackFactory { + + + @Override + public FtbCultivatePromotionApi create(Throwable cause) { + cause.printStackTrace(); + return new FtbCultivatePromotionApi() { + @Override + public ActionResult deleteMembersToProChannel(List userIds) { + log.error("deleteMembersToProChannel 调用失败..."); + return null; + } + + @Override + public ActionResult isThereAnAppForThePosLevel(String postId, String grandId, String userId) { + log.error("isThereAnAppForThePosLevel 调用失败..."); + return null; + } + }; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateStoreStatisticApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateStoreStatisticApiFallback.java new file mode 100644 index 0000000..22f92d4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateStoreStatisticApiFallback.java @@ -0,0 +1,76 @@ +package jnpf.cultivate.fallback; + +import jnpf.base.ActionResult; +import jnpf.cultivate.FtbCultivateStoreStatisticApi; +import jnpf.model.cultivate.dto.storestatistics.*; +import jnpf.model.cultivate.vo.position.*; +import jnpf.model.thousandsfaces.TodayWorkVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +public class FtbCultivateStoreStatisticApiFallback implements FallbackFactory { + + + @Override + public FtbCultivateStoreStatisticApi create(Throwable cause) { + log.error("进入 FtbCultivateStoreStatisticApi 的 fallback,降级原因:", cause); // 打印降级原因 + + return new FtbCultivateStoreStatisticApi() { + + @Override + public ActionResult getCultivateCount(FtbCultivateStoreStatisticsReq req) { + return null; + } + + @Override + public ActionResult> queryMyExamList(StoreStatisticsMyExamReq req) { + return null; + } + + @Override + public ActionResult> queryWaitMyReadOver(StoreStatisticsWaitMyCheckExamReq req) { + return null; + } + + @Override + public ActionResult> queryOfflineTrainList(StoreOfflineTrainReq req) { + return null; + } + + @Override + public ActionResult> storeMyTaskList(StoreOfflineTrainReq req) { + return null; + } + + @Override + public ActionResult> storeMyIdentityList(StoreIdentityReq req) { + return null; + } + + @Override + public ActionResult getWorkerCultivateCount(FtbCultivateStoreStatisticsReq req) { + return null; + } + + @Override + public ActionResult personTrainingStatistics(FtbCultivateStoreStatisticsReq req) { + return null; + } + + @Override + public ActionResult storeManagerTrainingStatistics(FtbCultivateStoreStatisticsReq req) { + return null; + } + + @Override + public ActionResult managerTrainingStatistics(FtbCultivateStoreStatisticsReq req) { + return null; + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateTeachingApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateTeachingApiFallback.java new file mode 100644 index 0000000..46dd366 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/FtbCultivateTeachingApiFallback.java @@ -0,0 +1,42 @@ +package jnpf.cultivate.fallback; + +import jnpf.cultivate.FtbCultivateTeachingApi; +import jnpf.model.cultivate.vo.teaching.MyPracticeSummaryVo; +import jnpf.model.cultivate.vo.teaching.SuperiorTeachingSummaryVo; +import jnpf.model.cultivate.vo.teaching.TeachingStoreCountVo; +import jnpf.model.cultivate.vo.teaching.TodaySummaryDataVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class FtbCultivateTeachingApiFallback implements FallbackFactory { + + @Override + public FtbCultivateTeachingApi create(Throwable cause) { + cause.printStackTrace(); + return new FtbCultivateTeachingApi() { + + @Override + public TeachingStoreCountVo storeTeachingCount(String storeId) { + return null; + } + + @Override + public TodaySummaryDataVo getTodaySummary(String storeId) { + return new TodaySummaryDataVo(); + } + + @Override + public SuperiorTeachingSummaryVo getSuperiorTeachingSummary(String storeId) { + return null; + } + + @Override + public MyPracticeSummaryVo getMyPracticeSummary(String storeId) { + return null; + } + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/V2CultivateOldDealApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/V2CultivateOldDealApiFallback.java new file mode 100644 index 0000000..1ae6d33 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/cultivate/fallback/V2CultivateOldDealApiFallback.java @@ -0,0 +1,39 @@ +package jnpf.cultivate.fallback; + +import jnpf.base.ActionResult; +import jnpf.cultivate.V2CultivateOldDealApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class V2CultivateOldDealApiFallback implements FallbackFactory { + + @Override + public V2CultivateOldDealApi create(Throwable cause) { + cause.printStackTrace(); + return new V2CultivateOldDealApi() { + @Override + public ActionResult dealOldData(String tenantId) { + return null; + } + + @Override + public ActionResult identifyDealOldData(String tenantId) { + return null; + } + + @Override + public ActionResult migrateOldTaskData(String tenantId) { + return null; + } + + @Override + public ActionResult migratePromotionOldData(String tenantId, String promotionId) { + return null; + } + + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/StoreApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/StoreApi.java new file mode 100644 index 0000000..331a776 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/StoreApi.java @@ -0,0 +1,205 @@ +package jnpf.doclibrary; + +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.doclibrary.fallback.StoreFallback; +import jnpf.entity.StoreEntity; +import jnpf.model.store.Store; +import jnpf.model.store.StorePositionInfoVo; +import jnpf.model.store.StoreUserNumVo; +import jnpf.model.store.dto.StoreAbnormalIdsQueryDTO; +import jnpf.model.store.dto.StorePageByIdsNoDsQueryDTO; +import jnpf.model.store.dto.StorePageByIdsQueryDTO; +import jnpf.model.store.vo.StoreBaseListVO; +import jnpf.model.store.vo.StoreLocationVO; +import jnpf.model.store.vo.UserStoreListVo; +import jnpf.model.vo.StoreExecutionVo; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 门店内部调用api + * + * @author yanwenfu + * @create 2023-07-12 + */ +@FeignClient(name = "jnpf-ftb", fallback = StoreFallback.class, path = "/Store") +public interface StoreApi { + + /** + * 查询门店下岗位的成员数量 + * @param storeId 门店id + * @param positionList 岗位id集合 + * @return java.util.List + */ + @GetMapping(value = "/storePositionInfo/list") + List getStorePositionInfoList(@RequestParam(value = "storeId") String storeId, @RequestParam(value = "positionList") List positionList); + + /** + * 查询门店下的成员数量 + * @param storeIds 门店ids + * @return java.util.List + */ + @GetMapping(value = "/storePositionInfo/getUserNum") + List getUserNum(@RequestParam(value = "storeIds") List storeIds); + + /** + * 获取用户门店列表信息 + * @return 返回值 + */ + @GetMapping(value = "/userStoreList") + ActionResult> getUserStoreList(); + + /** + * 获取用户门店列表信息(值班) + * + * @return 返回值 + */ + @GetMapping(value = "/userStoreListDuty") + ActionResult> getUserStoreListDuty(); + + /** + * 查询用户所属的门店 + * @param userId 用户id + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/listByUserId/{userId}") + List getListByUserId(@PathVariable(value = "userId") String userId); + + @GetMapping(value = "/getAllUserStores") + List getAllUserStores(); + + /** + * 根据门店ids查询门店信息 + * @param storeIds 门店ids + * @return java.util.List + */ + @GetMapping(value = "/listByIds") + List getListByIds(@RequestParam(value = "storeIds") List storeIds); + + @GetMapping("/getList") + ActionResult> getList(@RequestParam("selectKey") String selectKey, + @RequestParam("organizeid") String organizeId, + @RequestParam("longitude") String longitude, + @RequestParam("latitude") String latitude); + + /** + * 查询组织下的门店 + * @param organizeId 组织id + * @return java.util.List + */ + @GetMapping(value = "/list") + List getStoreList(@RequestParam(value = "organizeId", required = false) String organizeId); + + /** + * 查询组织下的门店(返回了上级组织) + * @param organizeId 组织id + * @return java.util.List + */ + @GetMapping(value = "/orgList") + List getOrgStoreList(@RequestParam(value = "organizeId", required = false) String organizeId, @RequestParam(value = "queryStoreIds", required = false) List queryStoreIds); + + /** + * 查询门店信息 + * @param id 门店id + * @return jnpf.model.store.Store + */ + @GetMapping("/info/{id}") + Store getStoreInfo(@PathVariable("id") String id); + + /** + * 查询门店信息 + * @param id 门店id + * @return jnpf.model.store.Store + */ + @GetMapping("/getStoreInfoNoData") + StoreEntity getStoreInfoNoData(@RequestParam("id") String id, @RequestParam("tenantId") String tenantId); + + /** + * 根据门店ids分页查询门店记录 + * @param storeIds 门店ids + * @param currentPage 当前页码 + * @param pageSize 每页条数 + * @return com.github.pagehelper.PageInfo + */ + @PostMapping(value = "/store/pageByIds") + PageInfo getStorePageByIds(@RequestBody StorePageByIdsQueryDTO query); + + /** + * 根据门店ids分页查询门店记录 + * @param storeIds 门店ids + * @param currentPage 当前页码 + * @param pageSize 每页条数 + * @return com.github.pagehelper.PageInfo + */ + @PostMapping(value = "/store/pageByIdsNoDataSource") + PageInfo getStorePageByIdsNoDataSource(@RequestBody StorePageByIdsNoDsQueryDTO query); + + /** + * 查询异常的门店 + * @param storeIds 门店ids + * @return java.util.List + */ + @PostMapping(value = "/store/abnormal/record") + List getAbnormalStoreIds(@RequestBody StoreAbnormalIdsQueryDTO query); + + /** + * 根据组织ids查询门店列表 + * @param organizeIdList 组织ids + * @return java.util.List + */ + @PostMapping(value = "/store/list/byOrganizeList") + List getStoreListByOrganizeList(@RequestBody List organizeIdList); + + /** + * 查询门店信息(未绑定数据库) + * @param tenantId 租户id + * @param storeIds 门店ids + * @return java.util.List + */ + @GetMapping(value = "/store/list/noDataSource") + List getListByIdsNoDataSource(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "storeIds") List storeIds); + + /** + * 校验是否是值班人 + * @param userId 用户Id + */ + @GetMapping(value = "/store/checkStoreUser") + boolean checkStoreUser(@RequestParam(value = "userId") String userId); + + @Operation(description = "[列表] 目标组织,及其子组织所有已启用的门店") + @GetMapping(value = "/store/list/organizeChildren") + ActionResult> getStoreListByOrganizeIdAndChildOrganize(@RequestParam(value = "organizeId") String organizeId, @RequestParam(required = false, value = "disabled") Boolean disabled); + + + /** + * 查询门店下岗位的成员数量 + * @param storeId 门店id + * @param positionList 岗位id集合 + * @return java.util.List + */ + @NoDataSourceBind + @GetMapping(value = "/storePositionInfo/list/nodata") + List getStorePositionInfoListNodata(@RequestParam(value = "storeId") String storeId, @RequestParam(value = "positionList") List positionList, @RequestParam(value = "tenantId") String tenantId); + + /** + * 查询用户所属的门店 + * @param userId 用户id + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/listByUserId/nodata/{userId}") + @NoDataSourceBind + List getListByUserIdNodata(@PathVariable(value = "userId") String userId, @RequestParam(value = "tenantId") String tenantId); + + /** + * 获取门店位置信息 + * @param storeId 门店id + * @return jnpf.model.store.vo.StoreLocationVO + */ + @GetMapping(value = "/getStoreLocation") + StoreLocationVO getStoreLocation(@RequestParam(value = "storeId") String storeId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/fallback/StoreFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/fallback/StoreFallback.java new file mode 100644 index 0000000..d7e5dcb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/doclibrary/fallback/StoreFallback.java @@ -0,0 +1,178 @@ +package jnpf.doclibrary.fallback; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.doclibrary.StoreApi; +import jnpf.entity.StoreEntity; +import jnpf.model.store.Store; +import jnpf.model.store.StorePositionInfoVo; +import jnpf.model.store.StoreUserNumVo; +import jnpf.model.store.dto.StoreAbnormalIdsQueryDTO; +import jnpf.model.store.dto.StorePageByIdsNoDsQueryDTO; +import jnpf.model.store.dto.StorePageByIdsQueryDTO; +import jnpf.model.store.vo.StoreBaseListVO; +import jnpf.model.store.vo.StoreLocationVO; +import jnpf.model.store.vo.UserStoreListVo; +import jnpf.model.vo.StoreExecutionVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 资料库内部调用fallback + * + * @author yanwenfu + * @create 2023-07-12 + */ +@Slf4j +@Component +public class StoreFallback implements StoreApi { + @Override + public List getStorePositionInfoList(String storeId, List positionList) { + + log.error("类名: StoreFallback, 方法名: getStorePositionInfoList, 错误信息: 调用失败..."); + return new ArrayList<>(); + } + + @Override + public List getUserNum(List storeIds) { + log.error("类名: StoreFallback, 方法名: getUserNum, 错误信息: 调用失败..."); + return new ArrayList<>(); + } + + @Override + public List getListByUserId(String userId) { + + log.error("类名: StoreFallback, 方法名: getStoreListByUser, 错误信息: 调用失败..."); + return new ArrayList<>(); + } + + @Override + public List getAllUserStores() { + log.error("类名: StoreFallback, 方法名: getAllUserStores, 错误信息: 调用失败..."); + return null; + } + + @Override + public List getListByIds(List storeIds) { + + log.error("类名: StoreFallback, 方法名: getListByIds, 错误信息: 调用失败..."); + return new ArrayList<>(); + } + + @Override + public ActionResult> getList(String selectKey, String organizeId, String longitude, String latitude) { + + log.error("类名: StoreFallback, 方法名: getList, 错误信息: 调用失败..."); + return null; + } + + @Override + public ActionResult> getUserStoreList() { + log.error("类名: StoreFallback, 方法名: getUserStoreList, 错误信息: 调用失败..."); + return null; + } + + @Override + public ActionResult> getUserStoreListDuty() { + return null; + } + + @Override + public List getStoreList(String organizeId) { + + log.error("类名: StoreFallback, 方法名: getStoreList, 错误信息: 调用失败..."); + return new ArrayList<>(); + } + + @Override + public List getOrgStoreList(String organizeId, List queryStoreIds) { + log.error("类名: StoreFallback, 方法名: getOrgStoreList, 错误信息: 调用失败..."); + return null; + } + + + @Override + public Store getStoreInfo(String id) { + + log.error("类名: StoreFallback, 方法名: getStoreInfo, 错误信息: 调用失败..."); + return null; + } + + /** + * 查询门店信息 + * + * @param id 门店id + * @param tenantId + * @return jnpf.model.store.Store + */ + @Override + public StoreEntity getStoreInfoNoData(String id, String tenantId) { + + log.error("FTB_类名: StoreFallback, 方法名: getStoreInfoDutyQuery, 错误信息: 调用失败..., 当前租户号[{}] - 门店号[{}]", tenantId, id); + log.error(" getStoreInfoDutyQuery 没有拿到门店信息,走了fallback"); + return null; + } + + @Override + public PageInfo getStorePageByIds(StorePageByIdsQueryDTO query) { + + return new PageInfo<>(); + } + + @Override + public boolean checkStoreUser(String userId) { + log.error("类名: StoreFallback, 方法名: checkStoreUser, 错误信息: 调用失败..."); + return false; + } + + @Override + public ActionResult> getStoreListByOrganizeIdAndChildOrganize(String organizeId, Boolean disabled) { + return null; + } + + @Override + public List getStorePositionInfoListNodata(String storeId, List positionList, String tenantId) { + return null; + } + + @Override + public List getListByUserIdNodata(String userId, String tenantId) { + return null; + } + + @Override + public StoreLocationVO getStoreLocation(String storeId) { + return null; + } + + @Override + public PageInfo getStorePageByIdsNoDataSource(StorePageByIdsNoDsQueryDTO query) { + + return new PageInfo<>(); + } + + @Override + public List getAbnormalStoreIds(StoreAbnormalIdsQueryDTO query) { + return new ArrayList<>(); + } + + @Override + public List getStoreListByOrganizeList(List organizeIdList) { + + return new ArrayList<>(); + } + + @Override + public List getListByIdsNoDataSource(String tenantId, List storeIds) { + + return new ArrayList<>(); + } + +// @Override +// public ActionResult info(String id) { +// return null; +// } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/V2CultivateTimingApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/V2CultivateTimingApi.java new file mode 100644 index 0000000..2d6306c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/V2CultivateTimingApi.java @@ -0,0 +1,43 @@ +package jnpf.exam; + +import jnpf.base.ActionResult; +import jnpf.exam.fallback.V2CultivateTimingApiFallback; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "jnpf-ftb", fallback = V2CultivateTimingApiFallback.class, path = "/v2/cultivate/timing") +public interface V2CultivateTimingApi { + + + /** + * 培训相关每分钟执行 + * + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/per-minute") + ActionResult perMinute(@RequestParam("tenantId") String tenantId); + + /** + * 每小时执行 + * + * @param tenantId 租户ID + * @return 响应 + */ + + @NoDataSourceBind + @GetMapping("/per-hour") + ActionResult perHour(@RequestParam("tenantId") String tenantId); + + /** + * 每半个小时执行 + * + * @param tenantId 租户ID + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/per-half-hour") + ActionResult perHalfHour(@RequestParam("tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/fallback/V2CultivateTimingApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/fallback/V2CultivateTimingApiFallback.java new file mode 100644 index 0000000..eb67585 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/exam/fallback/V2CultivateTimingApiFallback.java @@ -0,0 +1,25 @@ +package jnpf.exam.fallback; + +import jnpf.base.ActionResult; +import jnpf.exam.V2CultivateTimingApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class V2CultivateTimingApiFallback implements V2CultivateTimingApi { + @Override + public ActionResult perMinute(String tenantId) { + return null; + } + + @Override + public ActionResult perHour(String tenantId) { + return null; + } + + @Override + public ActionResult perHalfHour(String tenantId) { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/FranchiseeApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/FranchiseeApi.java new file mode 100644 index 0000000..3535d9e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/FranchiseeApi.java @@ -0,0 +1,71 @@ +package jnpf.franchisee; + + +import jnpf.franchisee.fallback.FranchiseeFallbackApi; +import jnpf.model.franchisee.vo.FranchiseeIdName; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.Collection; +import java.util.List; + +@FeignClient(name = "jnpf-ftb", fallback = FranchiseeFallbackApi.class, path = "/web/franchise-api") +public interface FranchiseeApi { + + /** + * 获取所有加盟商信息 + * @return + */ + @GetMapping("/getFranchiseeIdNameList") + List getFranchiseeIdNameList(); + + /** + * 根据ID列表获取加盟商信息 + * @param ids + * @return + */ + @GetMapping("/getFranchiseeIdNameListByIds") + List getFranchiseeIdNameListByIds(Collection ids); + + /** + * 根据ID获取加盟商信息 + * @param id + * @return + */ + @GetMapping("/getFranchiseeIdNameListById") + FranchiseeIdName getFranchiseeIdNameListById(String id); + + /** + * 根据名称获取加盟商信息 + * @param name + * @return + */ + @GetMapping("/getFranchiseeIdNameListByName") + List getFranchiseeIdNameListByName(String name); + + /** + * 根据名称列表获取加盟商信息 + * @param names + * @return + */ + @GetMapping("/getFranchiseeIdNameListByNames") + List getFranchiseeIdNameListByNames(Collection names); + + + /** + * 根据编码获取加盟商信息 + * @param code + * @return + */ + @GetMapping("/getFranchiseeIdNameListByCode") + FranchiseeIdName getFranchiseeIdNameListByCode(String code); + + /** + * 根据编码列表获取加盟商信息 + * @param codes + * @return + */ + @GetMapping("/getFranchiseeIdNameListByCodes") + List getFranchiseeIdNameListByCodes(Collection codes); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/fallback/FranchiseeFallbackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/fallback/FranchiseeFallbackApi.java new file mode 100644 index 0000000..b6c951d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/franchisee/fallback/FranchiseeFallbackApi.java @@ -0,0 +1,55 @@ +package jnpf.franchisee.fallback; + +import jnpf.franchisee.FranchiseeApi; +import jnpf.model.franchisee.vo.FranchiseeIdName; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; + +@Slf4j +@Component +public class FranchiseeFallbackApi implements FranchiseeApi { + @Override + public List getFranchiseeIdNameList() { + log.error("getFranchiseeIdNameList fallback"); + return null; + } + + @Override + public List getFranchiseeIdNameListByIds(Collection ids) { + log.error("getFranchiseeIdNameListByIds fallback.ids:{}",ids); + return null; + } + + @Override + public FranchiseeIdName getFranchiseeIdNameListById(String id) { + log.error("getFranchiseeIdNameListById fallback.id:{}",id); + return null; + } + + @Override + public List getFranchiseeIdNameListByName(String name) { + log.error("getFranchiseeIdNameListByName fallback.name:{}",name); + return null; + } + + @Override + public List getFranchiseeIdNameListByNames(Collection names) { + log.error("getFranchiseeIdNameListByNames fallback.names:{}",names); + return null; + } + + @Override + public FranchiseeIdName getFranchiseeIdNameListByCode(String code) { + log.error("getFranchiseeIdNameListByCode fallback.names:{}",code); + return null; + } + + @Override + public List getFranchiseeIdNameListByCodes(Collection codes) { + log.error("getFranchiseeIdNameListByCodes fallback.names:{}",codes); + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/FtbNoticeApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/FtbNoticeApi.java new file mode 100644 index 0000000..6a50d5e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/FtbNoticeApi.java @@ -0,0 +1,23 @@ +package jnpf.notice; + +import jnpf.base.ActionResult; +import jnpf.notice.fallback.FtbNoticeFallBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +@FeignClient(name = "jnpf-ftb", fallback = FtbNoticeFallBackApi.class, path = "/web/noticeAnnouncements") +public interface FtbNoticeApi { + + /** + * 每半个小时检测一下是否有发布的通知公告 + * + * @param tenantId 租户ID + * @return + */ + @NoDataSourceBind + @GetMapping("/checkPublishNotice") + ActionResult checkPublishNotice(@RequestParam("tenantId") String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/fallback/FtbNoticeFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/fallback/FtbNoticeFallBackApi.java new file mode 100644 index 0000000..b8f9ae6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/notice/fallback/FtbNoticeFallBackApi.java @@ -0,0 +1,18 @@ +package jnpf.notice.fallback; + +import jnpf.base.ActionResult; +import jnpf.notice.FtbNoticeApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + + +@Component +@Slf4j +public class FtbNoticeFallBackApi implements FtbNoticeApi { + + + @Override + public ActionResult checkPublishNotice(String tenantId) { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonneApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonneApi.java new file mode 100644 index 0000000..21581cd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonneApi.java @@ -0,0 +1,72 @@ +package jnpf.personnels; + +import jnpf.model.attendance.vo.DailyApprovalVo; +import jnpf.model.personnels.dto.emp.FtbEmpQueryDTO; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJobTenureDTO; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.vo.roster.FtbPersonnelsChangeInfoVO; +import jnpf.model.personnels.vo.roster.FtbPersonnlesJobTenureVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2025/10/3 + */ +@FeignClient(name = "jnpf-ftb") +public interface FtbPersonneApi { + + /** + *根据userId 查询人事异动信息 + */ + @GetMapping("/web-app/staff-home-page/get-personnel-change-info") + FtbPersonnelsChangeInfoVO getPersonnelChangeInfo(@RequestParam(name = "userId") String userId); + + + @PostMapping("/web-app/staff-home-page/get-personnel-change-info-batch") + Map getPersonnelChangeInfoBatch(@RequestBody List userIds); + /** + * 根据userI 和开始时间结束时间查询借调记录 + */ + @GetMapping("/web/secondment/get-secondment-record") + List getSecondmentRecord(@RequestParam("userId") String userId, + @RequestParam(value = "startLeaveTime",required = false) String startLeaveTime, + @RequestParam(value = "endLeaveTime",required = false)String endLeaveTime); + + /** + * 批量查询审批中和审批完成的借调记录 + */ + @PostMapping("/web/secondment/get-secondment-record-bath") + List getSecondmentRecordBath(@RequestBody FtbSecondMentQueryDTO dto); + + + /** + * 查询审批中和审批完成的借调记录 + */ + @PostMapping("/web/secondment/list-query-approval") + List queryListApproval(@RequestBody FtbSecondMentQueryDTO dto); + + /** + * 模糊搜索 电话 名称 + */ + @PostMapping("/web/personnels-emp-entry/search-phone-name") + List searchPhoneName(@RequestBody FtbEmpQueryDTO dto); + + /** + * 查询岗龄 + * @param req + * @return + */ + @PostMapping("/web-app/staff-home-page/query-positionTenure-age") + @NoDataSourceBind + List queryPositionTenureAge(@RequestBody FtbPersonnlesJobTenureDTO req); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsContactInfoManagerApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsContactInfoManagerApi.java new file mode 100644 index 0000000..04451e0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsContactInfoManagerApi.java @@ -0,0 +1,38 @@ +package jnpf.personnels; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; +import jnpf.personnels.fallback.FtbPersonnelsContaceInfoManagerFallBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + + +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsContaceInfoManagerFallBackApi.class, path = "/web/personnelsContactInfo") +public interface FtbPersonnelsContactInfoManagerApi { + + /** + * 同步合同信息 + * + * @param info + * @return + */ + @PostMapping("/syncContactInfo") + @NoDataSourceBind + ActionResult syncContactInfo(@RequestBody ContactStatusInfo info); + + /** + * 定时任务检测未签署合同消息 + * + * @param tenantId + * @return + */ + @GetMapping("/checkNotSendContactSign") + @NoDataSourceBind + ActionResult checkNotSendContactSign(@RequestParam("tenantId") String tenantId); + + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmEntryApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmEntryApi.java new file mode 100644 index 0000000..93e8004 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmEntryApi.java @@ -0,0 +1,19 @@ +package jnpf.personnels; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * 入职api + */ +@FeignClient(name = "jnpf-ftb", path = "/web/personnels-emp-entry") +public interface FtbPersonnelsEmEntryApi { + + /** + * 根据userId修改手机号信息 + */ + @PutMapping("/update-phone-by-userId") + void updatePhoneByUserId( @RequestParam("userId") String userId, + @RequestParam("phone") String phone); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmployeeTypeRemoteApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmployeeTypeRemoteApi.java new file mode 100644 index 0000000..b230b17 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmployeeTypeRemoteApi.java @@ -0,0 +1,30 @@ +package jnpf.personnels; + +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; +import jnpf.personnels.fallback.FtbPersonnelsEmployeeTypeRemoteFallBackApi; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; +import java.util.Map; + +/** + * 员工类型模块 + * + * @author wangchunxiang + * @date 2025/09/30 + */ +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsEmployeeTypeRemoteFallBackApi.class, path = "/web/employee-type") +public interface FtbPersonnelsEmployeeTypeRemoteApi { + + /** + * 根据用户ID集合查询员工类型ID、名称 + * + * @param userIds 用户ID集合 + * @return key为用户Id,value为员工类型 + */ + @PostMapping("/get-employee-type-by-user-ids") + Map getEmployeeTypeByUserIds(@RequestBody List userIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmploymentApplyApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmploymentApplyApi.java new file mode 100644 index 0000000..cc99d1c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsEmploymentApplyApi.java @@ -0,0 +1,28 @@ +package jnpf.personnels; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.oa.FtbPersonnelsEmployInfoForOA; +import jnpf.personnels.fallback.FtbPersonnelsEmploymentApplyFallBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + + +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsEmploymentApplyFallBackApi.class, path = "/web/personnels-staff-employment-apply") +public interface FtbPersonnelsEmploymentApplyApi { + + @GetMapping("/updateEmploymentApplyStatus") + @NoDataSourceBind + public ActionResult updateEmploymentApplyStatus(@RequestParam("tenantId") String tenantId); + + /** + * 入职办理列表查询 ForOA 回显 姓名 手机 + */ + @GetMapping("/inquire-about-the-entry-list") + ActionResult> inquireAboutTheEntryList(@RequestParam(required = false, name = "keyWords") String keyWords, + @RequestParam(required = false,name = "phone")String phone, + @RequestParam(required = false,name = "workerName")String workerName); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsInfoConfigApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsInfoConfigApi.java new file mode 100644 index 0000000..6dc4ce0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsInfoConfigApi.java @@ -0,0 +1,44 @@ +package jnpf.personnels; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.oa.FtbPersonnelsEmployInfoForOA; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; +import jnpf.personnels.fallback.FtbPersonnelsEmploymentApplyFallBackApi; +import jnpf.personnels.fallback.FtbPersonnelsInfoConfigBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.annotation.Resource; +import java.util.List; + + +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsInfoConfigBackApi.class, path = "/web/range-config") +public interface FtbPersonnelsInfoConfigApi { + + /** + * 查询是平均范围配置 还是自定义配置 + * @param type 1 年龄 2 薪资 3 工龄 + * @return 配置类型 1平均 2自定义 + */ + @GetMapping("/query-type") + ActionResult queryType(@RequestParam("type") Integer type); + + /** + * 查询平均区间配置范围 + * @param type 1 年龄 2 薪资 3 工龄 + */ + @GetMapping("/query-info") + ActionResult queryInfo(@RequestParam("type") Integer type); + + /** + * 查询自定义区间配置范围 + * + * @param type 1 年龄 2 薪资 3 工龄 + */ + @GetMapping("/query-info-diy") + ActionResult> queryDiyInfo(@RequestParam("type") Integer type); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsMetaDataManagerApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsMetaDataManagerApi.java new file mode 100644 index 0000000..6be61dc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsMetaDataManagerApi.java @@ -0,0 +1,59 @@ +package jnpf.personnels; + +import jnpf.model.personnels.dto.roster.meta.PersonnelsMetaDTO; +import jnpf.model.personnels.dto.salary.FtbXcCustomFieldDto; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaDataReq; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaFuctionReq; +import jnpf.model.personnels.vo.salary.FtbXcCustomFieldVo; +import jnpf.personnels.fallback.FtbPersonnelsMetaDataManagerFallBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; +import java.util.Map; + + +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsMetaDataManagerFallBackApi.class, path = "/web/personnels/metaData") +public interface FtbPersonnelsMetaDataManagerApi { + + @PostMapping("/getMetaData") + @NoDataSourceBind + public List getMetaData(@RequestBody FtbPersonnelsMetaDataReq info); + + /** + * 查询用户当月是否已经离职 + * + * @param userId 用户ID + * @param tenantId 租户ID + * @return true-已经离职 false-未离职 + */ + @GetMapping("/queryCurrMonthDepartStatus/{userId}/{tenantId}") + @NoDataSourceBind + public Boolean queryCurrMonthDepartStatus(@PathVariable("userId") String userId, @PathVariable("tenantId") String tenantId); + + + /** + * 查询用户当月是否已经离职 + * + * @param req 请求 + * @return true-已经离职 false-未离职 + */ + @PostMapping("/queryCurrMonthDepartStatusByDate") + @NoDataSourceBind + Boolean queryCurrMonthDepartStatusByDate(@RequestBody FtbPersonnelsMetaFuctionReq req); + + /** + * 批量查询员工是否离职 + */ + @PostMapping("/query-current-month-leave") + @NoDataSourceBind + Map queryCurrMonthLeave(@RequestBody FtbPersonnelsMetaFuctionReq req); + + /** + * 批量查询员工自定义字段 + */ + @PostMapping("/query-custom-filed") + List queryCustomFiled(@RequestBody FtbXcCustomFieldDto req); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsApi.java new file mode 100644 index 0000000..a43695b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsApi.java @@ -0,0 +1,50 @@ +package jnpf.personnels; + +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelSalaryRewardDTO; +import jnpf.model.personnels.dto.salary.FtbSalaryMetaDataQueryDto; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelSalaryRewardVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbXcEmployeeRewardRecordsVO; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * 查询指定用户,指定月份内,被奖励和处罚的合计 + */ +@FeignClient(name = "jnpf-ftb", path = "/ftb-personnels-rewards-punishments") +public interface FtbPersonnelsRewardsPunishmentsApi { + + /** + * 薪酬奖励合计 + */ + @PostMapping(value = "/salary-reward") + List salaryReward(@RequestBody FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO); + + /** + * 薪酬惩罚合计 + */ + @PostMapping(value = "/pay-penalty") + List totalSalaryPenalty(@RequestBody FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO); + + /** + * 薪酬获取奖励和惩罚api + */ + @PostMapping(value = "/salaryMetaDataQuery") + @NoDataSourceBind + BigDecimal salaryMetaDataQuery(@RequestBody FtbSalaryMetaDataQueryDto dto); + + /** + * 薪酬获取奖励和惩罚api(批量) + */ + @PostMapping(value = "/salary-meta-data-batch-query") + @NoDataSourceBind + List salaryBatchMetaDataQuery(@RequestBody FtbSalaryMetaDataQueryDto dto); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsRemoteApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsRemoteApi.java new file mode 100644 index 0000000..ca5260a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRewardsPunishmentsRemoteApi.java @@ -0,0 +1,25 @@ +package jnpf.personnels; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentApprovalVO; +import jnpf.personnels.fallback.FtbPersonnelsRewardsPunishmentsRemoteFallBackApi; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@FeignClient(name = "jnpf-ftb", path = "/ftb-personnels-rewards-punishments", fallback = FtbPersonnelsRewardsPunishmentsRemoteFallBackApi.class) +public interface FtbPersonnelsRewardsPunishmentsRemoteApi { + + + /** + * OA审批获取奖惩规则 + * + * @param type 类型,0奖励,1惩罚 + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-approval") + ActionResult> listApproval(@RequestParam("type") Integer type); + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRosterManagerApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRosterManagerApi.java new file mode 100644 index 0000000..b7d9907 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsRosterManagerApi.java @@ -0,0 +1,147 @@ +package jnpf.personnels; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.roster.QueryCompanyAgeDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.ShopManagerUserDto; +import jnpf.model.personnels.dto.staff.roster.StaffRosterInfoDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.req.roster.StaffRosterReq; +import jnpf.permission.model.user.UserListVO; +import jnpf.permission.vo.user.UserListMatchVO; +import jnpf.personnels.fallback.FtbPersonnelsRosterManagerFallBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsRosterManagerFallBackApi.class, path = "/web/personnels-staff-roster") +public interface FtbPersonnelsRosterManagerApi { + + @NoDataSourceBind + @GetMapping("/updateCompanyAge") + ActionResult updateCompanyAge(@RequestParam("tenantId") String tenantId); + + + /** + * 绑定手机号 + * + * @param userId 用户ID + * @param phone 手机号 + * @return + */ + @PostMapping("/bindPhone") + @NoDataSourceBind + public ActionResult bindPhone(@RequestParam("tenantCode") String tenantCode, @RequestParam("userId") String userId, @RequestParam("phone") String phone); + + + @NoDataSourceBind + @PostMapping("/queryCompanyAge/{tenantId}") + ActionResult> queryCompanyAge(@RequestBody List userIds, @PathVariable("tenantId") String tenantId); + + /** + * 查询未提交入职登记表的用户 + * + * @param tenantId + * @return + */ + @NoDataSourceBind + @GetMapping("/queryNoSubmitForm") + ActionResult> queryNoSubmitForm(@RequestParam("tenantId") String tenantId); + + + /** + * 查询健康证过期的用户 + * + * @param tenantId + * @param days 距离健康证 快过期的天数 + * @return + */ + @NoDataSourceBind + @GetMapping("/queryHealthExpire") + ActionResult> queryHealthExpire(@RequestParam("tenantId") String tenantId, @RequestParam(value = "days", required = false) Long days, @RequestParam(value = "months", required = false) Long months); + + + /** + * 花名册查询列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @GetMapping("/query-list") + ActionResult> pageLists(@Validated @SpringQueryMap StaffRosterListReq req); + + /** + * 花名册查询列表 _post + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @PostMapping("/query-list/postWithSalary") + ActionResult> postWithSalary(@Validated @RequestBody StaffRosterListReq req); + + /** + * 花名册查询列表不分页 + */ + @PostMapping("/query-list/post-with-salary-no-page") + List postWithSalaryNoPage(@Validated @RequestBody StaffRosterListReq req); + + /** + * 花名册列表查询-无权限 + * @param req + * @return ActionResult + */ + @PostMapping("/query-list/byUserIds") + ActionResult> getPersonnelByUserIds(@RequestBody StaffRosterListReq req); + + @PostMapping("/query-list/post") + ActionResult> pageListsPost(@Validated @RequestBody StaffRosterListReq req); + + /** + * 查询离职人员信息 + * @return + */ + @PostMapping("/queryDepartUser") + ActionResult> queryDepartUser(@RequestBody List userIds); + + @Operation(summary = "[匹配]-指定user们哪些存在,哪些不存在, 包含花名册离职的用户") + @PostMapping("/user/list/match/ids") + ActionResult getUserListByMatch(@RequestBody List userIds); + + /** + * 根据用户ID查询所属门店负责人信息 + * + * @param tenantId + * @param userIds 用户ID + * @return + */ + @NoDataSourceBind + @PostMapping("/queryShopManagerUser/{tenantId}") + ActionResult queryShopManagerUser(@PathVariable("tenantId") String tenantId, @RequestBody List userIds); + + @NoDataSourceBind + @GetMapping("/timingAlertTrialJob/{tenantId}") + @Deprecated(since = "人事2.1废弃试岗状态") + ActionResult> timingAlertTrialJob(@PathVariable("tenantId") String tenantId); + + /** + * 根据用户ID查询用户信息 + * @param req + * @return + */ + @PostMapping("/queryWithUserIds") + List queryWithUserIds( @RequestBody StaffRosterListReq req); + + /** + * 查询指定userIds用户信息 + */ + @PostMapping("/queryWithUserIds/post") + List queryWithUserIdsPost(@RequestBody StaffRosterReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsTurnoverManagementApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsTurnoverManagementApi.java new file mode 100644 index 0000000..46e2920 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/FtbPersonnelsTurnoverManagementApi.java @@ -0,0 +1,52 @@ +package jnpf.personnels; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.fallback.FtbPersonnelsTurnoverManagementFallBackApi; +import jnpf.util.NoDataSourceBind; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * @Title: FtbPersonnelsTurnoverManagementApi + * @Author: peng.hao + * @create: 2024/2/19 10:36 + */ +@FeignClient(name = "jnpf-ftb", fallback = FtbPersonnelsTurnoverManagementFallBackApi.class, path = "/web/personnels-turnover") +public interface FtbPersonnelsTurnoverManagementApi { + + @GetMapping("/closeUserAccountRegularlyAfterResignation") + @NoDataSourceBind + ActionResult closeUserAccountRegularlyAfterResignation(@RequestParam("tenantId") String tenantId); + + /** + * 离职用户已签署离职协议 + * @param userId 用户id + * @return + */ + @GetMapping("/user-has-sign-an-agreement") + Boolean userHasSignedASeparationAgreement(@RequestParam("userId") String userId,@RequestParam("flag") Integer flag); + + /** + * 获取所有离职人员信息 + */ + @GetMapping("/query-turnover-list") + List queryTurnoverList(); + /** + * 获取离职人员信息 + * 1.按多个userId + * 2.按多个组织id + * 3.按多个岗位id + */ + @PostMapping("/get-dep-user") + List getInformationAboutTheDepartingPerson(@RequestBody FtbDepUserDTO dto); + @PostMapping("/not-token-get-dep-user") + List getInformationAboutTheDepartingPersonNotToken(@RequestBody FtbDepUserDTO dto); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsContaceInfoManagerFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsContaceInfoManagerFallBackApi.java new file mode 100644 index 0000000..c9a64a5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsContaceInfoManagerFallBackApi.java @@ -0,0 +1,24 @@ +package jnpf.personnels.fallback; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; +import jnpf.personnels.FtbPersonnelsContactInfoManagerApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + + +@Component +@Slf4j +public class FtbPersonnelsContaceInfoManagerFallBackApi implements FtbPersonnelsContactInfoManagerApi { + + + @Override + public ActionResult syncContactInfo(ContactStatusInfo info) { + return null; + } + + @Override + public ActionResult checkNotSendContactSign(String tenantId) { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmployeeTypeRemoteFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmployeeTypeRemoteFallBackApi.java new file mode 100644 index 0000000..1274e61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmployeeTypeRemoteFallBackApi.java @@ -0,0 +1,18 @@ +package jnpf.personnels.fallback; + +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; +import jnpf.personnels.FtbPersonnelsEmployeeTypeRemoteApi; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +@Component +public class FtbPersonnelsEmployeeTypeRemoteFallBackApi implements FtbPersonnelsEmployeeTypeRemoteApi { + + @Override + public Map getEmployeeTypeByUserIds(List userIds) { + return null; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmploymentApplyFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmploymentApplyFallBackApi.java new file mode 100644 index 0000000..60e2fd5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsEmploymentApplyFallBackApi.java @@ -0,0 +1,26 @@ +package jnpf.personnels.fallback; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.oa.FtbPersonnelsEmployInfoForOA; +import jnpf.personnels.FtbPersonnelsEmploymentApplyApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Component +@Slf4j +public class FtbPersonnelsEmploymentApplyFallBackApi implements FtbPersonnelsEmploymentApplyApi { + + + @Override + public ActionResult updateEmploymentApplyStatus(String tenantId) { + return null; + } + + @Override + public ActionResult> inquireAboutTheEntryList(String keyWords, String phone, String workerName) { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsInfoConfigBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsInfoConfigBackApi.java new file mode 100644 index 0000000..5a184a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsInfoConfigBackApi.java @@ -0,0 +1,29 @@ +package jnpf.personnels.fallback; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; +import jnpf.personnels.FtbPersonnelsInfoConfigApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Slf4j +public class FtbPersonnelsInfoConfigBackApi implements FtbPersonnelsInfoConfigApi { + @Override + public ActionResult queryType(Integer type) { + return null; + } + + @Override + public ActionResult queryInfo(Integer type) { + return null; + } + + @Override + public ActionResult> queryDiyInfo(Integer type) { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsMetaDataManagerFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsMetaDataManagerFallBackApi.java new file mode 100644 index 0000000..602fb28 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsMetaDataManagerFallBackApi.java @@ -0,0 +1,47 @@ +package jnpf.personnels.fallback; + +import jnpf.model.personnels.dto.roster.meta.PersonnelsMetaDTO; +import jnpf.model.personnels.dto.salary.FtbXcCustomFieldDto; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaDataReq; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaFuctionReq; +import jnpf.model.personnels.vo.salary.FtbXcCustomFieldVo; +import jnpf.personnels.FtbPersonnelsMetaDataManagerApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.Map; + + +@Component +@Slf4j +public class FtbPersonnelsMetaDataManagerFallBackApi implements FtbPersonnelsMetaDataManagerApi { + + + @Override + public List getMetaData(FtbPersonnelsMetaDataReq info) { + return null; + } + + @Override + public Boolean queryCurrMonthDepartStatus(String userId, String tenantId) { + return null; + } + + @Override + public Boolean queryCurrMonthDepartStatusByDate(FtbPersonnelsMetaFuctionReq req) { + return null; + } + + @Override + public Map queryCurrMonthLeave(FtbPersonnelsMetaFuctionReq req) { + return Map.of(); + } + + @Override + public List queryCustomFiled(FtbXcCustomFieldDto req) { + return List.of(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRewardsPunishmentsRemoteFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRewardsPunishmentsRemoteFallBackApi.java new file mode 100644 index 0000000..dbd043b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRewardsPunishmentsRemoteFallBackApi.java @@ -0,0 +1,18 @@ +package jnpf.personnels.fallback; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentApprovalVO; +import jnpf.personnels.FtbPersonnelsRewardsPunishmentsRemoteApi; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class FtbPersonnelsRewardsPunishmentsRemoteFallBackApi implements FtbPersonnelsRewardsPunishmentsRemoteApi { + + @Override + public ActionResult> listApproval(Integer type) { + return ActionResult.fail("OA审批获取奖惩规则降级处理"); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRosterManagerFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRosterManagerFallBackApi.java new file mode 100644 index 0000000..644fccb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsRosterManagerFallBackApi.java @@ -0,0 +1,107 @@ +package jnpf.personnels.fallback; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.roster.QueryCompanyAgeDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.ShopManagerUserDto; +import jnpf.model.personnels.dto.staff.roster.StaffRosterInfoDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.req.roster.StaffRosterReq; +import jnpf.permission.model.user.UserListVO; +import jnpf.permission.vo.user.UserListMatchVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + + +@Component +@Slf4j +public class FtbPersonnelsRosterManagerFallBackApi implements FtbPersonnelsRosterManagerApi { + + + @Override + public ActionResult updateCompanyAge(String tenantId) { + return null; + } + + @Override + public ActionResult bindPhone(String tenantCode, String userId, String phone) { + return null; + } + + @Override + public ActionResult> queryCompanyAge(List userIds, String tenantId) { + return null; + } + + @Override + public ActionResult> queryNoSubmitForm(String tenantId) { + return null; + } + + @Override + public ActionResult> queryHealthExpire(String tenantId, Long days, Long months) { + return null; + } + + @Override + public ActionResult> pageLists(StaffRosterListReq req) { + return null; + } + + @Override + public ActionResult> pageListsPost(StaffRosterListReq req) { + return null; + } + + @Override + public ActionResult> postWithSalary(StaffRosterListReq req) { + return null; + } + + @Override + public List postWithSalaryNoPage(StaffRosterListReq req) { + return List.of(); + } + + @Override + public ActionResult> getPersonnelByUserIds(StaffRosterListReq req) { + return null; + } + + @Override + public ActionResult getUserListByMatch(List userIds) { + return null; + } + + + @Override + public ActionResult> queryDepartUser(@RequestBody List userIds) { + return null; + } + + @Override + public ActionResult queryShopManagerUser(String tenantId, List userIds) { + return null; + } + + @Override + public ActionResult> timingAlertTrialJob(String tenantId) { + return null; + } + + @Override + public List queryWithUserIds(StaffRosterListReq req) { + return null; + } + + @Override + public List queryWithUserIdsPost(StaffRosterReq req) { + return null; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsTurnoverManagementFallBackApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsTurnoverManagementFallBackApi.java new file mode 100644 index 0000000..b397b18 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/personnels/fallback/FtbPersonnelsTurnoverManagementFallBackApi.java @@ -0,0 +1,47 @@ +package jnpf.personnels.fallback; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsTurnoverManagementApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * @Title: FtbPersonnelsTurnoverManagementFallBackApi + * @Author: peng.hao + * @create: 2024/2/19 10:38 + */ +@Slf4j +@Component +public class FtbPersonnelsTurnoverManagementFallBackApi implements FtbPersonnelsTurnoverManagementApi { + + @Override + public ActionResult closeUserAccountRegularlyAfterResignation(String tenantId) { + return null; + } + + @Override + public Boolean userHasSignedASeparationAgreement(String userId, Integer flag) { + return null; + } + + @Override + public List queryTurnoverList() { + return null; + } + + @Override + public List getInformationAboutTheDepartingPerson(FtbDepUserDTO dto) { + return null; + } + + @Override + public List getInformationAboutTheDepartingPersonNotToken(FtbDepUserDTO dto) { + return List.of(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/V2AuthPermissionApi.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/V2AuthPermissionApi.java new file mode 100644 index 0000000..92a8cb4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/V2AuthPermissionApi.java @@ -0,0 +1,22 @@ +package jnpf.util.auth; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.util.auth.fallback.V2AuthPermissionApiFallback; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; + +/** + * 登录人数据权限-组织范围 API(与 FTB 内 {@code V2AuthPermissionUtils#getLoginUserAuthOrganizeIds()} 语义一致)。 + *

+ * 返回值约定:{@code null} 表示全部;空列表表示无权限;非空为组织/门店 id 列表(已排除班组)。 + * Feign 降级时抛出异常,不会返回空列表以免误判为无权限(当前暂无正式熔断占位策略)。 + */ +@FeignClient(name = "jnpf-ftb", contextId = "v2AuthPermissionApi", path = "/permission/auth", fallbackFactory = V2AuthPermissionApiFallback.class) +public interface V2AuthPermissionApi { + + @Operation(summary = "[API] 当前登录人在权限范围内的组织/门店 id 列表(未包裹 ActionResult)") + @GetMapping("/login-user-organize-ids") + List getLoginUserAuthOrganizeIds(); +} diff --git a/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/fallback/V2AuthPermissionApiFallback.java b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/fallback/V2AuthPermissionApiFallback.java new file mode 100644 index 0000000..12ca729 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-api/src/main/java/jnpf/util/auth/fallback/V2AuthPermissionApiFallback.java @@ -0,0 +1,28 @@ +package jnpf.util.auth.fallback; + +import jnpf.util.auth.V2AuthPermissionApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * 无有效熔断占位数据:调用失败时直接失败,避免将空列表误判为「无人事数据权限」。 + */ +@Slf4j +@Component +public class V2AuthPermissionApiFallback implements FallbackFactory { + + @Override + public V2AuthPermissionApi create(Throwable cause) { + if (cause != null) { + log.error("V2AuthPermissionApi 调用失败(将向上抛出,不返回空权限): {}", cause.getMessage(), cause); + } + return () -> { + String msg = "人事数据权限服务暂不可用(V2AuthPermissionApi/getLoginUserAuthOrganizeIds),请稍后重试"; + if (cause != null) { + throw new IllegalStateException(msg, cause); + } + throw new IllegalStateException(msg); + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/pom.xml b/jnpf-ftb/jnpf-ftb-biz/pom.xml new file mode 100644 index 0000000..2dd3787 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/pom.xml @@ -0,0 +1,333 @@ + + + 4.0.0 + + com.jnpf + jnpf-ftb + 3.4.7-RELEASE + + + jnpf-ftb-biz + + + 8 + 8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-websocket + + + io.netty + netty-all + 4.1.85.Final + + + com.github.tencentyun + tls-sig-api-v2 + 2.0 + + + org.apache.rocketmq + rocketmq-spring-boot-starter + 2.2.2 + + + com.alibaba.cloud + spring-cloud-starter-stream-rocketmq + + + cn.hutool + hutool-crypto + 5.8.0 + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.2 + + + com.jnpf + jnpf-ftb-entity + ${project.version} + compile + + + com.jnpf + jnpf-ftb-api + ${project.version} + compile + + + com.jnpf + jnpf-file-api + ${project.version} + + + com.jnpf + jnpf-websocket-api + ${project.version} + + + com.jnpf + jnpf-provider-file + ${project.version} + + + com.jnpf + jnpf-permission-api + ${project.version} + compile + + + com.jnpf + jnpf-permission-entity + ${project.version} + compile + + + com.jnpf + jnpf-common-core + + + com.jnpf + jnpf-common-office + + + commons-io + commons-io + + + com.jnpf + jnpf-common-file + + + com.jnpf + jnpf-common-connector + + + com.jnpf + jnpf-common-sms + + + + com.jnpf + jnpf-common-database + + + com.jnpf + jnpf-common-springaop + ${project.version} + + + com.jnpf + jnpf-visualdev-onlinedev-biz + ${project.version} + compile + + + com.jnpf + jnpf-visualdev-base-api + ${project.version} + compile + + + com.jnpf + jnpf-visualdev-onlinedev-api + ${project.version} + compile + + + com.jnpf + jnpf-workflow-engine-api + ${project.version} + compile + + + + com.jnpf + jnpf-system-entity + ${project.version} + compile + + + com.jnpf + jnpf-example-biz + ${project.version} + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + org.locationtech.jts + jts-core + 1.19.0 + + + org.json + json + 20180130 + + + com.jnpf + jnpf-attence-api + 3.4.7-RELEASE + compile + + + com.belerweb + pinyin4j + 2.5.1 + + + cn.6tail + lunar + 1.3.15 + compile + + + com.jnpf + jnpf-message-api + 3.4.7-RELEASE + compile + + + com.tencentcloudapi + tencentcloud-sdk-java + 3.1.972 + + + + com.tencentcloudapi + tencentcloud-sdk-java-ocr + 3.1.965 + + + com.taobao.arthas + arthas-spring-boot-starter + 3.7.2 + + + com.jnpf + jnpf-im-api + 3.4.7-RELEASE + compile + + + com.jnpf + jnpf-salary-api + 3.4.7-RELEASE + + + com.jnpf + jnpf-contract-api + ${project.version} + compile + + + com.jnpf + jnpf-common-seata + + + com.jnpf + jnpf-file-storage + 3.4.7-RELEASE + + + com.jnpf + jnpf-oauth-api + ${project.version} + compile + + + com.jnpf + jnpf-tenant-entity + 3.4.7-RELEASE + compile + + + com.jnpf + jnpf-memoo-share-api + 3.4.7-RELEASE + compile + + + org.jsoup + jsoup + 1.11.3 + compile + + + + com.project.model + seeta-sdk-platform + 1.2.1 + + + com.fantaibao + jnpf-common-permission + ${project.version} + compile + + + + com.alibaba + transmittable-thread-local + 2.12.3 + + + + com.fantaibao + fantaibao-data-analysis-api + 3.4.7-RELEASE + compile + + + com.jnpf + fantaibao-patrol-store-api + 3.4.7-RELEASE + compile + + + + + jnpf-ftb-${project.version} + + + org.springframework.boot + spring-boot-maven-plugin + + + jnpf.JnpfFtbApplication + ZIP + + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/JnpfFtbApplication.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/JnpfFtbApplication.java new file mode 100644 index 0000000..a52ef10 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/JnpfFtbApplication.java @@ -0,0 +1,24 @@ +package jnpf; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; + +/** + * 翻台宝服务启动类 + * + * @author yanwenfu + * @create 2023-07-11 + */ +@SpringBootApplication +@EnableFeignClients +@Slf4j +public class JnpfFtbApplication { + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + SpringApplication.run(JnpfFtbApplication.class, args); + System.out.println("翻台宝服务启动成功,耗时:" + (System.currentTimeMillis() - start) + "毫秒"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/ApiCallLogAspect.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/ApiCallLogAspect.java new file mode 100644 index 0000000..62d9f5c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/ApiCallLogAspect.java @@ -0,0 +1,98 @@ +package jnpf.aspect; + +import jnpf.attendance.mapper.ApiCallLogMapper; +import jnpf.entity.ApiCallLog; +import jnpf.util.DateUtil; +import jnpf.util.FtbUtil; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +/** + * 接口调用记录切面 + * + * @author yanwenfu + * @create 2026-01-28 + */ +@Aspect +@Component +@Slf4j +public class ApiCallLogAspect { + + @Resource + private ApiCallLogMapper apiCallLogMapper; + + @Around("@annotation(apiCallLog)") + public Object around(ProceedingJoinPoint joinPoint, FtbApiCallLog apiCallLog) throws Throwable { + + long startTime = System.currentTimeMillis(); + ApiCallLog logEntity = new ApiCallLog(); + HttpServletRequest request = null; + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + request = attributes.getRequest(); + } + // ========= 基础信息 ========= + logEntity.setId(FtbUtil.getId()); + logEntity.setApiName(apiCallLog.name()); + logEntity.setDeleteMark(0); + logEntity.setCreatorTime(DateUtil.getNowDate()); + logEntity.setCreatorUserId(UserProvider.getLoginUserId()); + logEntity.setTenantId(UserProvider.getUser().getTenantId()); + if (request != null) { + logEntity.setApiPath(request.getRequestURI()); + logEntity.setHttpMethod(request.getMethod()); + logEntity.setIp(getIp(request)); + } else { + logEntity.setApiPath("UNKNOWN"); + logEntity.setHttpMethod("UNKNOWN"); + logEntity.setIp("UNKNOWN"); + } + // ========= 请求参数 ========= + try { + logEntity.setRequestBody(JsonUtil.getObjectToString(joinPoint.getArgs())); + } catch (Exception e) { + logEntity.setRequestBody("请求参数序列化失败"); + } + Object result; + try { + result = joinPoint.proceed(); + logEntity.setSuccess(1); + try { + logEntity.setResponseBody(JsonUtil.getObjectToString(result)); + } catch (Exception e) { + logEntity.setResponseBody("返回结果序列化失败"); + } + return result; + } catch (Throwable ex) { + logEntity.setSuccess(0); + logEntity.setErrorMsg(ex.getMessage()); + throw ex; + } finally { + logEntity.setCostTime((int) (System.currentTimeMillis() - startTime)); + try { + apiCallLogMapper.insert(logEntity); + } catch (Exception e) { + log.error("接口调用日志入库失败", e); + } + } + } + + private String getIp(HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (StringUtils.isBlank(ip)) { + ip = request.getRemoteAddr(); + } + return ip; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/FtbApiCallLog.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/FtbApiCallLog.java new file mode 100644 index 0000000..4acdf6c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/aspect/FtbApiCallLog.java @@ -0,0 +1,12 @@ +package jnpf.aspect; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface FtbApiCallLog { + + /** 接口名称 */ + String name(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/Machine.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/Machine.java new file mode 100644 index 0000000..3ec27a8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/Machine.java @@ -0,0 +1,18 @@ +package jnpf.attendance.annotation; + +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Machine { + + /** 操作动作(打卡, 下发人员, 删除人员) */ + ActionEnum dealAction(); + + /** 厂商 */ + MachineEnum factory(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/MachineAspect.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/MachineAspect.java new file mode 100644 index 0000000..a4331b0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/annotation/MachineAspect.java @@ -0,0 +1,230 @@ +package jnpf.attendance.annotation; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.mapper.AttendanceMachineLogMapper; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.base.UserInfo; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceMachineLog; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.model.attendance.vo.GroupInfoVo; +import jnpf.model.attendance.vo.attendance.UserTenantVo; +import jnpf.util.ConstantUtil; +import jnpf.util.DateUtil; +import jnpf.util.FtbUtil; +import jnpf.util.JsonUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 考勤机切面 + * + * @author yanwenfu + * @create 2024-07-30 + */ +@Aspect +@Component +@RefreshScope +public class MachineAspect { + + @Value("${machine-log}") + private Integer machineLog; + + @Resource + private AttendanceMachineLogMapper attendanceMachineLogMapper; + + @Resource + private AttendanceUserService attendanceUserService; + + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + + private static final ThreadLocal threadLocal = new ThreadLocal<>(); + + @Pointcut("@annotation(jnpf.attendance.annotation.Machine)") + public void machineLog() { + } + + @Around(value = "machineLog()") + public Object generateLog(ProceedingJoinPoint point) throws Throwable { + + if (null == machineLog || machineLog.equals(ConstantUtil.NUM_FALSE)) { + return point.proceed(); + } + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + Machine annotation = method.getAnnotation(Machine.class); + Object[] args = point.getArgs(); + ActionEnum actionEnum = annotation.dealAction(); + MachineEnum machineEnum = annotation.factory(); + JSONArray array = new JSONArray(); + AttendanceMachineLog machineLog = new AttendanceMachineLog(); + if (null != args && args.length > 0) { + String str; + if (actionEnum.equals(ActionEnum.DA_KA)) { + str = "arg[0]"; + String paramStr = args[0].toString(); + if (StringUtils.isNotEmpty(paramStr)) { + paramStr = paramStr.replaceAll("checkPic=[^,]+,", ""); + paramStr = paramStr.replaceAll("\"photo\":\"[^\"]*\",", ""); + } + if (MachineEnum.KE_MI.equals(machineEnum)) { + paramStr = JsonUtil.getObjectToString(args[0]); + } + MutablePair pair = this.getUserInfo(paramStr, machineEnum); + if (StringUtils.isNotEmpty(pair.getLeft())) { + String[] split = pair.getLeft().split("@"); + String tenantId = split[0]; + String userId = split[1]; + // 查询当前考勤组信息 + UserInfo userInfo = new UserInfo(); + userInfo.setTenantId(tenantId); + userInfo.setUserId(userId); + GroupInfoVo group = null; + try { + List groupList = attendanceUserService.getAttendanceGroupUsersOfSecondment(DateUtil.getNowDate(), DateUtil.getNowDate(), List.of(userId), null, false); + if (null != groupList && !groupList.isEmpty()) { + String groupName = attendanceGroupMapper.getGroupName(groupList.get(0).getGroupId()); + group = new GroupInfoVo(groupList.get(0).getGroupId(), groupName); + } + } catch (Exception e) { + e.printStackTrace(); + } + machineLog.setUserId(userId); + machineLog.setUserName(pair.getRight()); + if (null != group) { + machineLog.setGroupId(group.getGroupId()); + machineLog.setGroupName(group.getGroupName()); + } + } + JSONObject json = new JSONObject(); + json.set(str, paramStr); + array.add(json); + } else { + for (int i = 0; i < args.length; i++) { + str = "arg[" + i + "]"; + JSONObject json = new JSONObject(); + try { + if (args[i] != null) { + if (i == 0) { + JSONObject arg = new JSONObject(args[i]); + json.set(str, arg.get("userName")); + arg.clear(); + } else { + json.set(str, args[i].toString()); + } + } else { + json.set(str, "null"); + } + } catch (Exception e) { + json.set(str, e.getMessage()); + } + array.add(json); + } + } + } + machineLog.setId(FtbUtil.getId()); + machineLog.setFactoryCode(machineEnum.getValue()); + machineLog.setAction(actionEnum.getDescription()); + machineLog.setParamJson(array.toString()); + machineLog.setCreatorTime(new Date()); + threadLocal.set(machineLog); + return point.proceed(); + } + + private MutablePair getUserInfo(String paramStr, MachineEnum machineEnum) { + + String userId = null; + String userName = null; + String regexUserId; + String regexUserName; + switch (machineEnum) { + case KAI_JIA_YI: + regexUserId = "\\\"userId\\\":\\\"([^\\\"]*)\\\""; + regexUserName = "name=(.*?)(,|\\})"; + String regexTenantId = "\\\"tenantId\\\":\\\"([^\\\"]*)\\\""; + Matcher kjyMatcher = Pattern.compile(regexUserId + "|" + regexUserName + "|" + regexTenantId).matcher(paramStr); + String tenantId = ""; + while (kjyMatcher.find()) { + if (kjyMatcher.group(1) != null) { + userId = kjyMatcher.group(1); + } + if (kjyMatcher.group(2) != null) { + userName = kjyMatcher.group(2); + } + if (kjyMatcher.group(4) != null) { + tenantId = kjyMatcher.group(4); + } + } + userId = tenantId + "@" + userId; + break; + case MAO_TONG: + regexUserId = "user_id=(.*?)(?=,)"; + regexUserName = "user_name=(.*?)(?=,)"; + Matcher matcher = Pattern.compile(regexUserId + "|" + regexUserName).matcher(paramStr); + while (matcher.find()) { + if (matcher.group(1) != null) { + userId = matcher.group(1); + } + if (matcher.group(2) != null) { + userName = matcher.group(2); + } + } + break; + case KE_MI: + UserTenantVo userTenant = JsonUtil.getJsonToBean(paramStr, UserTenantVo.class); + userId = userTenant.getTenantId() + "@" + userTenant.getUserId(); + userName = userTenant.getUserName(); + default: + break; + } + return MutablePair.of(userId, userName); + } + + @AfterReturning(pointcut = "machineLog()", returning = "result") + public void afterReturningAdvice(JoinPoint joinPoint, Object result) { + + if (null == machineLog || machineLog.equals(ConstantUtil.NUM_FALSE)) { + return; + } + // 这里可以访问目标方法的返回值 + AttendanceMachineLog machineLog = threadLocal.get(); + if (null != machineLog) { + machineLog.setMethodResult(null == result ? "无返回值" : result.toString()); + attendanceMachineLogMapper.insert(machineLog); + threadLocal.remove(); + } + } + + @AfterThrowing(pointcut = "machineLog()", throwing = "ex") + public void afterThrowingAdvice(JoinPoint joinPoint, Throwable ex) { + + if (null == machineLog || machineLog.equals(ConstantUtil.NUM_FALSE)) { + return; + } + // 这里可以访问目标方法抛出的异常 + AttendanceMachineLog machineLog = threadLocal.get(); + if (null != machineLog) { + machineLog.setMethodResult(ex.getMessage()); + attendanceMachineLogMapper.insert(machineLog); + threadLocal.remove(); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/antifreeze/UserAntifreeze.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/antifreeze/UserAntifreeze.java new file mode 100644 index 0000000..0c89e27 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/antifreeze/UserAntifreeze.java @@ -0,0 +1,307 @@ +package jnpf.attendance.antifreeze; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import jnpf.authority.FtbAuthorityApi; +import jnpf.base.ActionResult; +import jnpf.file.FileUploadApi; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.V2UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.util.StringUtil; +import jnpf.util.UploaderUtil; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class UserAntifreeze { + @Resource + private V2UserApi v2UserApi; + @Resource + private FileUploadApi fileUploadApi; + @Resource + private FtbAuthorityApi ftbAuthorityApi; + @Resource + private FtbPersonnelsRosterManagerApi managerApi; + + /** + * 请求用户服务接口 + * + * @param userIds 用户ID集合 + * @return 用户信息 + */ + public List getInfoByIds(List userIds, String tenantId) { + List infoByIds = CollUtil.newArrayList(); + try { + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, tenantId); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + infoByIds = getPartUserInfoVos(userList.getData()); + } + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return infoByIds; + } + + /** + * 请求用户服务接口 + * + * @param orgId 组织ID + * @param workGroupId 班组ID + * @return 用户信息 + */ + public List getInfoByIds(String orgId, String workGroupId) { + List userBoundVoList = ftbAuthorityApi.listTargetOrganizeIdAuthApi(orgId, + List.of(UserWorkStatusEnums.NONE)); + if (StringUtil.isEmpty(workGroupId)) { + return userBoundVoList; + } + return userBoundVoList.stream().filter(vo -> StringUtil.equals(vo.getStoreTeamId(), workGroupId)).collect(Collectors.toList()); + } + + /** + * 请求用户服务接口 + * + * @param orgIds 组织ID集合 + * @param workStatusEnums 用户工作状态 + * @return 用户信息 + */ + public List batchGetInfoByIds(List orgIds, List workStatusEnums) { + return ftbAuthorityApi.listTargetOrganizeIdsAuthApi(orgIds, + UserWorkStatusEnums.getDifferenceEnumsFromBase(workStatusEnums)); + } + + /** + * 请求用户服务接口 + * + * @param userIds 用户ID集合 + * @return 用户信息 + */ + public List getStaffRosterListInfoByIds(List userIds) { + List infoByIds = CollUtil.newArrayList(); + try { + StaffRosterListReq req = new StaffRosterListReq(); + req.setUserIds(userIds); + req.setIsQueryAuth("0"); + List userBoundVos = managerApi.postWithSalaryNoPage(req); + infoByIds = getStaffRosterVos(userBoundVos); + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return infoByIds; + } + + private List getStaffRosterVos(List userBoundVos) { + List infoByIds; + if (CollUtil.isEmpty(userBoundVos)) { + return CollUtil.newArrayList(); + } + infoByIds = userBoundVos.stream().map(x -> { + PartUserInfoVo bean = BeanUtil.toBean(x, PartUserInfoVo.class); + bean.setUserId(x.getUserId()); + bean.setRealName(StringUtil.isNotEmpty(x.getName()) ? x.getName() : ""); + bean.setMobilePhone(StringUtil.isNotEmpty(x.getPhone()) ? x.getPhone() : ""); + bean.setWorkerStatus(Objects.nonNull(x.getWorkerStatus()) ? x.getWorkerStatus() : ""); + bean.setHeadIcon(StringUtil.isNotEmpty(x.getHeadLogo()) ? fileUploadApi.getHeadIcon(UploaderUtil.uploaderImg(x.getHeadLogo())) : ""); + if(CollUtil.isNotEmpty(x.getOrgList())){ + WorkerGroupDataDto groupDataDto = x.getOrgList().stream().findFirst().orElse(null); + if(Objects.nonNull(groupDataDto)){ + bean.setOrganizeName(groupDataDto.getAffiliatedOrgName()); + bean.setOrganizeId(groupDataDto.getAffiliatedOrg()); + bean.setPositionId(groupDataDto.getAffiliatedPosition()); + bean.setPositionName(groupDataDto.getAffiliatedPositionName()); + } +// Optional.ofNullable(groupDataDto).ifPresent(groupDataDto1 -> { +// bean.setOrganizeName(groupDataDto1.getAffiliatedOrgName()); +// bean.setOrganizeId(groupDataDto1.getAffiliatedOrg()); +// bean.setPositionId(groupDataDto1.getAffiliatedPosition()); +// bean.setPositionName(groupDataDto1.getAffiliatedPositionName()); +// }); + } + return bean; + }).collect(Collectors.toList()); + return infoByIds; + } + + /** + * 请求用户服务接口 + * + * @param userIds 用户ID集合 + * @return 用户信息 + */ + public List getInfoByIdsManyAndCopyPost(List userIds, String tenantId) { + List infoByIds = CollUtil.newArrayList(); + try { + List userBoundVos = v2UserApi.userListAndCopy(userIds, null, tenantId); + infoByIds = getPartUserInfoVos(userBoundVos); + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return infoByIds; + } + + /** + * 请求用户服务接口 + * + * @param userIds 用户ID集合 + * @return 用户信息 + */ + public List getAllByIds(List userIds, String tenantId) { + List infoByIds = CollUtil.newArrayList(); + try { + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, tenantId); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + infoByIds = getPartUserInfoVos(userList.getData()); + } + if (CollUtil.isEmpty(infoByIds)) { + List userBoundVos = v2UserApi.userListAndCopy(userIds, true, tenantId); + infoByIds = getPartUserInfoVos(userBoundVos); + return infoByIds; + } + if (infoByIds.size() < userIds.size()) { + List collect = infoByIds.stream().map(PartUserInfoVo::getUserId).collect(Collectors.toList()); + userIds = userIds.stream().filter(userId -> !collect.contains(userId)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)) { + List userBoundVos = v2UserApi.userListAndCopy(userIds, true, tenantId); + List partUserInfoVos = getPartUserInfoVos(userBoundVos); + infoByIds.addAll(partUserInfoVos); + } + } + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return infoByIds; + } + + @NotNull + private List getPartUserInfoVos(List userList) { + List infoByIds; + if (CollUtil.isEmpty(userList)) { + return CollUtil.newArrayList(); + } + infoByIds = userList.stream().map(x -> { + PartUserInfoVo bean = BeanUtil.toBean(x, PartUserInfoVo.class); + bean.setUserId(x.getId()); + bean.setRealName(StringUtil.isNotEmpty(x.getUserName()) ? x.getUserName() : ""); + bean.setMobilePhone(StringUtil.isNotEmpty(x.getPhone()) ? x.getPhone() : ""); + bean.setWorkerStatus(Objects.nonNull(x.getWorkStatusEnums()) ? x.getWorkStatusEnums().getCode() : ""); + bean.setHeadIcon(StringUtil.isNotEmpty(x.getHeadIcon()) ? fileUploadApi.getHeadIcon(UploaderUtil.uploaderImg(x.getHeadIcon())) : ""); + return bean; + }).collect(Collectors.toList()); + return infoByIds; + } + + @NotNull + private List getUserInfoVos(List userList) { + if (CollUtil.isEmpty(userList)) { + return CollUtil.newArrayList(); + } + return userList.stream().map(x -> { + UserEntity bean = BeanUtil.toBean(x, UserEntity.class); + bean.setId(x.getId()); + bean.setRealName(x.getUserName()); + bean.setMobilePhone(x.getPhone()); + bean.setHeadIcon(fileUploadApi.getHeadIcon(UploaderUtil.uploaderImg(x.getHeadIcon()))); + return bean; + }).collect(Collectors.toList()); + } + + /** + * 请求用户服务接口 + * + * @param userId 用户ID + * @return 用户信息 + */ + public PartUserInfoVo getInfo(String userId, String tenantId) { + try { + List infoByIds = getAllByIds(CollUtil.newArrayList(userId), tenantId); + if (CollUtil.isEmpty(infoByIds)) { + return null; + } + return infoByIds.get(0); + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return null; + } + + /** + * 请求用户服务接口 + * + * @param userIds 用户ID集合 + * @return 用户信息 + */ + public List getInfoByIds(List userIds) { + List infoByIds = CollUtil.newArrayList(); + try { + List userBoundVos = v2UserApi.userListAndCopy(userIds, false, null); + infoByIds = getUserInfoVos(userBoundVos); + if (CollUtil.isEmpty(infoByIds)) { + return CollUtil.newArrayList(); + } + infoByIds.stream().filter(Objects::nonNull).filter(user -> StringUtil.isNotEmpty(user.getHeadIcon())).forEach(user -> user.setHeadIcon(fileUploadApi.getHeadIcon(UploaderUtil.uploaderImg(user.getHeadIcon())))); + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return infoByIds; + } + + + /** + * 请求用户服务接口 + * + * @param userId 用户ID + * @return 用户信息 + */ + public UserEntity getInfo(String userId) { + try { + ActionResult usersBound = v2UserApi.getUsersBound(userId, null); + if (200 == usersBound.getCode() && null != usersBound.getData()) { + UserEntity infoById = BeanUtil.copyProperties(usersBound.getData(), UserEntity.class); + infoById.setRealName(usersBound.getData().getUserName()); + return infoById; + } + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return null; + } + + /** + * 请求用户服务接口 + * + * @param keyword 关键字 + * @return 用户信息 + */ + public List getInfoByLikeName(String keyword) { + List infoByIds = CollUtil.newArrayList(); + if (StringUtil.isEmpty(keyword)) { + return infoByIds; + } + try { + List userBoundVos = v2UserApi.userListAndCopyLikeName(keyword, null, null); + infoByIds = getUserInfoVos(userBoundVos); + } catch (Exception e) { + log.error("用户接口请求失败:", e); + } + return infoByIds; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/ChangeConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/ChangeConfig.java new file mode 100644 index 0000000..e80dea9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/ChangeConfig.java @@ -0,0 +1,51 @@ +package jnpf.attendance.bean; + +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.util.ConstantUtil; +import lombok.Getter; +import lombok.Setter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 变更配置 + * + * @author yanwenfu + * @create 2024-11-11 + */ +@Configuration +@Getter +@Setter +public class ChangeConfig { + + private Map>> changeMap = new HashMap<>(); + + public ChangeConfig() { + changeMap.put(ConstantUtil.ON_WORK, getOnWorkMap()); + changeMap.put(ConstantUtil.OFF_WORK, getOffWorkMap()); + } + + private Map> getOnWorkMap() { + + Map> map = new HashMap<>(); + map.put(ClockInStatusEnum.NORMAL, List.of(ClockInStatusEnum.WORK_LATE, ClockInStatusEnum.NO_CLOCK)); + map.put(ClockInStatusEnum.WORK_LATE, List.of(ClockInStatusEnum.NORMAL, ClockInStatusEnum.WORK_LATE, ClockInStatusEnum.NO_CLOCK)); + map.put(ClockInStatusEnum.NO_CLOCK, List.of(ClockInStatusEnum.NORMAL, ClockInStatusEnum.WORK_LATE)); + map.put(ClockInStatusEnum.ABSENCE, List.of(ClockInStatusEnum.NORMAL, ClockInStatusEnum.WORK_LATE)); + return map; + } + + private Map> getOffWorkMap() { + + Map> map = new HashMap<>(); + map.put(ClockInStatusEnum.NORMAL, List.of(ClockInStatusEnum.HOME_EARLY, ClockInStatusEnum.NO_CLOCK)); + map.put(ClockInStatusEnum.HOME_EARLY, List.of(ClockInStatusEnum.NORMAL, ClockInStatusEnum.HOME_EARLY, ClockInStatusEnum.NO_CLOCK)); + map.put(ClockInStatusEnum.NO_CLOCK, List.of(ClockInStatusEnum.NORMAL, ClockInStatusEnum.HOME_EARLY)); + map.put(ClockInStatusEnum.ABSENCE, List.of(ClockInStatusEnum.NORMAL, ClockInStatusEnum.HOME_EARLY)); + return map; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/FtbThreadPoolExecutor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/FtbThreadPoolExecutor.java new file mode 100644 index 0000000..4738b43 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/bean/FtbThreadPoolExecutor.java @@ -0,0 +1,160 @@ +package jnpf.attendance.bean; + +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 线程池配置类 + * 根据任务类型(CPU密集、IO密集、混合)创建合适的线程池。 + */ +@Configuration +public class FtbThreadPoolExecutor { + // 队列边界 + private static final int QUEUE_CAPACITY = 200; + // 线程存活时间 s(秒) + private static final long KEEP_ALIVE_TIME = 15L; + + private static final String THREAD_NAME_PREFIX = "FTB服务-考勤模块"; + @Bean(name = "attendanceRuleThreadPool", destroyMethod = "shutdown") + public ThreadPoolExecutor attendanceRuleThreadPool() { + return createNamedThreadPool("attendance-rule", TaskType.CPU_INTENSIVE); + } + @Bean(name = "cpuIntensiveThreadPool", destroyMethod = "shutdown") + public ThreadPoolExecutor cpuIntensiveThreadPool() { + return createThreadPool(TaskType.CPU_INTENSIVE); + } + + @Bean(name = "ioIntensiveThreadPool", destroyMethod = "shutdown") + public ThreadPoolExecutor ioIntensiveThreadPool() { + return createThreadPool(TaskType.IO_INTENSIVE); + } + + @Bean(name = "mixedThreadPool", destroyMethod = "shutdown") + public ThreadPoolExecutor mixedThreadPool() { + return createThreadPool(TaskType.MIXED); + } + + @Bean(name = "cultivateThreadPool", destroyMethod = "shutdown") + public ThreadPoolExecutor cultivateThreadPool() { + return createNamedThreadPool("cultivate-thread-v2", TaskType.IO_INTENSIVE); + } + + private ThreadPoolExecutor createNamedThreadPool(String name, TaskType type) { + int actualProcessors = Math.min(Runtime.getRuntime().availableProcessors(), 42); + int runTime, cpuTime; + switch (type) { + case CPU_INTENSIVE: + runTime = 10; + cpuTime = 1; + break; + case IO_INTENSIVE: + runTime = 5; + cpuTime = 1; + break; + case MIXED: + default: + runTime = 50; + cpuTime = 1; + break; + } + int maxPoolSize = calculateMaxPoolSize(actualProcessors, runTime, cpuTime, type); + BlockingQueue queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY); + + return new ThreadPoolExecutor( + actualProcessors, + maxPoolSize, + KEEP_ALIVE_TIME, TimeUnit.SECONDS, + queue, + new ThreadFactory() { + private final ThreadFactory defaultFactory = Executors.defaultThreadFactory(); + private final AtomicInteger count = new AtomicInteger(1); + + @Override + public Thread newThread(@NotNull Runnable r) { + Thread thread = defaultFactory.newThread(r); + thread.setName("FTB服务-考勤模块-" + name + "-thread-" + count.getAndIncrement()); + return thread; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + private ThreadPoolExecutor createThreadPool(TaskType type) { + // 限制为实际核心数 + int actualProcessors = Math.min(Runtime.getRuntime().availableProcessors(), 42); + int runTime, cpuTime; + switch (type) { + case CPU_INTENSIVE: + runTime = 10; + cpuTime = 1; + break; + case IO_INTENSIVE: + runTime = 5; + cpuTime = 1; + break; + case MIXED: + default: + runTime = 50; + cpuTime = 1; + break; + } + int maxPoolSize = calculateMaxPoolSize(actualProcessors, runTime, cpuTime, type); + BlockingQueue queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY); + + return new ThreadPoolExecutor( + actualProcessors, + maxPoolSize, + KEEP_ALIVE_TIME, TimeUnit.SECONDS, + queue, + new ThreadFactory() { + private final ThreadFactory defaultFactory = Executors.defaultThreadFactory(); + private final AtomicInteger count = new AtomicInteger(1); + + @Override + public Thread newThread(@NotNull Runnable r) { + Thread thread = defaultFactory.newThread(r); + thread.setName(THREAD_NAME_PREFIX + "-" + type.name().toLowerCase() + "-thread-" + count.getAndIncrement()); + return thread; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + /** + * 根据公式:最佳线程数 = N * (1 + (WT / ST)) + * 其中 WT = runTime - cpuTime,即线程等待时间 + * + * @param availableProcessors CPU核心数 + * @param runTime 线程总运行时间(单位可自定,如毫秒) + * @param cpuTime 线程CPU计算时间 + * @return 最佳线程池大小 + */ + private int calculateMaxPoolSize(int availableProcessors, int runTime, int cpuTime, TaskType type) { + if (cpuTime <= 0) { + throw new IllegalArgumentException("参数不合法:cpuTime 应大于 0"); + } + if (type == TaskType.IO_INTENSIVE) { + // I/O 密集型:4 倍核心数,上限 200 + return Math.min(availableProcessors * 4, 200); + } else if (type == TaskType.CPU_INTENSIVE) { + // CPU 密集型:等于核心数 + return availableProcessors; + } else { + // 混合型 + long ratio = 1L + (long) runTime / cpuTime; + long calculated = (long) availableProcessors * ratio; + return (int) Math.min(calculated, 200); + } + } + + public enum TaskType { + CPU_INTENSIVE, + IO_INTENSIVE, + MIXED + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AppStatisticsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AppStatisticsController.java new file mode 100644 index 0000000..e7c847b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AppStatisticsController.java @@ -0,0 +1,128 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.service.AppStatisticsService; +import jnpf.base.ActionResult; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.text.ParseException; +import java.util.List; + +/** + * 考勤统计APP + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/app/statistics") +public class AppStatisticsController { + + @Resource + private AppStatisticsService appStatisticsService; + + @Operation(summary = "我的考勤-主页") + @GetMapping(value = "/home") + public ActionResult getHomeData(@Valid AppStatisticsListDto req) throws QueryException { + try { + log.info("我的考勤-主页,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + AppStatisticsListVo listVo = appStatisticsService.getAppHomeData(req); + long ent = System.currentTimeMillis(); + log.info("我的考勤-主页,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(listVo); + } catch (QueryException e) { + if (e.getMessage().contains("用户暂无考勤组")) { + return ActionResult.success(e.getMessage()); + } else { + throw new QueryException(e.getMessage()); + } + } + } + + @Operation(summary = "我的考勤-出勤情况") + @GetMapping("/record") + public ActionResult> getRecordData(@Valid AppStatisticsRecordDto req) throws QueryException { + try { + List list = appStatisticsService.getRecordData(req); + return ActionResult.success(list); + } catch (Exception e) { + if (e.getMessage().contains("暂无数据") || e.getMessage().contains("用户暂无考勤组") || e.getMessage().contains("用户暂无排班")) { + return ActionResult.success("暂无数据"); + } else { + throw new QueryException(e.getMessage()); + } + } + } + + @Operation(summary = "我的考勤-更多统计-默认") + @GetMapping("/moreDefault") + public ActionResult getMoreDefaultData(@Valid AppStatisticsMoreDto req) throws Exception { + log.info("我的考勤-更多统计-默认,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + AppStatisticsMoreVo moreVo = appStatisticsService.getMoreDefaultData(req); + long ent = System.currentTimeMillis(); + log.info("我的考勤-更多统计-默认,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(moreVo); + } + + @Operation(summary = "我的考勤-更多统计-展开") + @GetMapping("/moreExpand") + public ActionResult getMoreExpandData(@Valid AppStatisticsMoreInfoDto req) throws Exception { + log.info("我的考勤-更多统计-展开,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + AppStatisticsMoreInfoVo moreInfoVo = appStatisticsService.getMoreExpandData(req); + long ent = System.currentTimeMillis(); + log.info("我的考勤-更多统计-展开,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(moreInfoVo); + } + + @Operation(summary = "团队考勤-首页") + @PostMapping("/team/home") + public ActionResult getTeamHomeData(@Valid @RequestBody AppStatisticsTeamListDto req) throws QueryException { + log.info("团队考勤-首页,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + AppStatisticsTeamListVo teamListVo = appStatisticsService.getTeamHomeData(req); + long ent = System.currentTimeMillis(); + log.info("团队考勤-首页,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(teamListVo); + } + + @Operation(summary = "团队考勤-tab列表数据") + @PostMapping("/team/tabList") + public ActionResult> getTabListData(@Valid @RequestBody AppTeamStatisticsTabDto req) { + log.info("团队考勤-tab列表数据,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + List teamTabVoList = appStatisticsService.getTabListData(req); + long ent = System.currentTimeMillis(); + log.info("团队考勤-tab列表数据,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(teamTabVoList); + } + + @Operation(summary = "团队考勤-团队统计") + @PostMapping("/team/statistics") + public ActionResult getStatisticsData(@Valid @RequestBody AppTeamStatisticsDto req) throws ParseException { + log.info("团队考勤-团队统计,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + AppTeamStatisticsVo statisticsVo = appStatisticsService.getStatisticsData(req); + long ent = System.currentTimeMillis(); + log.info("团队考勤-团队统计,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(statisticsVo); + } + + @Operation(summary = "团队考勤-团队统计-详情列表") + @PostMapping("/team/statisticsList") + public ActionResult> getStatisticsListData(@Valid @RequestBody AppTeamStatisticsListDto req) throws QueryException, ParseException { + log.info("团队考勤-团队统计-详情列表,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + List statisticsListVoList = appStatisticsService.getStatisticsListData(req); + long ent = System.currentTimeMillis(); + log.info("团队考勤-团队统计-详情列表,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(statisticsListVoList); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttenceMachineController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttenceMachineController.java new file mode 100644 index 0000000..a11a221 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttenceMachineController.java @@ -0,0 +1,232 @@ +package jnpf.attendance.controller; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import jnpf.SocketApi; +import jnpf.attendance.annotation.Machine; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceUserFaceService; +import jnpf.base.ActionResult; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.model.attendance.dto.UserFaceDto; +import jnpf.model.attendance.vo.UserFaceVo; +import jnpf.util.CustomTenantUtil; +import jnpf.util.DateDetail; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 考勤机控制器 + * + * @author yanwenfu + * @create 2023-11-08 + */ +@Slf4j +@RestController +public class AttenceMachineController { + + @Resource + private AttenceMachineService attenceMachineService; + + @Resource + private AttendanceUserFaceService attendanceUserFaceService; + + @Autowired + private CustomTenantUtil customTenantUtil; + + @Autowired + private SocketApi socketApi; + + /** + * 查询用户人脸 + * @param userId 用户id + * @return java.lang.Object + */ + @GetMapping(value = "/userface/{userId}") + public Object getUserFace(@PathVariable(value = "userId") String userId) { + + UserFaceVo userFace = attendanceUserFaceService.getUserFace(userId); + return ActionResult.success(userFace); + } + + /** + * 新增人脸 + * @param userFaceDto 人脸数据 + * @return java.lang.Object + */ + @PostMapping(value = "/userface") + public Object addUserFace(@RequestBody @Valid UserFaceDto userFaceDto) { + + attendanceUserFaceService.addUserFace(userFaceDto); + return ActionResult.success(); + } + + /** + * 修改人脸 + * @param userFaceDto 人脸数据 + * @return java.lang.Object + */ + @PutMapping(value = "/userface") + public Object updateUserFace(@RequestBody @Valid UserFaceDto userFaceDto) { + + attendanceUserFaceService.updateUserFace(userFaceDto); + return ActionResult.success(); + } + + /** + * 删除人脸 + * @param userId 用户id + * @return java.lang.Object + */ + @DeleteMapping(value = "/userface/{userId}") + public Object deleteUserFace(@PathVariable(value = "userId") String userId) { + + attendanceUserFaceService.deleteUserFace(userId); + return ActionResult.success(); + } + + /** + * 上传打卡记录 + * @param params 打卡数据 + * @return java.util.Map + */ + @PostMapping(value = "/record/face") + @NoDataSourceBind + // @Machine(dealAction = ActionEnum.DA_KA, factory = MachineEnum.MAO_TONG) + public Map uploadRecordFace(@RequestBody Map params) { + + Map map = new HashMap<>(); + map.put("Result", 0); + map.put("Msg", ""); + return map; + } + + /** + * 识别后在线验证 + * @param params 打卡数据 + * @return java.util.Map + */ + @PostMapping(value = "/verify_user") + @NoDataSourceBind + @Machine(dealAction = ActionEnum.DA_KA, factory = MachineEnum.MAO_TONG) + public Map verifyUser(@RequestBody Map params) { + + log.info("MaoTong-Clock: {}", params); + Map map = new HashMap<>(); + Object sn = params.get("sn"); + if (null == sn) { + map.put("Result", 3); + map.put("Msg", "设备id不能为空"); + return map; + } + Object userIdObj = params.get("user_id"); + if (null == userIdObj || StringUtils.isEmpty(userIdObj.toString())) { + map.put("Result", 3); + map.put("Msg", sn + "打卡用户不能为空"); + return map; + } + log.error("开始打卡,设备号:{},用户:{}", sn, userIdObj); + String[] split = userIdObj.toString().split("@"); + String tenantId = split[0]; + String userId = split[1]; + customTenantUtil.checkOutTenant(tenantId, userId); + String b = attenceMachineService.clockIn(sn.toString(), userId, tenantId); + String message; + switch (b) { + case "0": + message = "已记录"; + break; + case "1": + message = "打卡成功"; + break; + case "2": + message = "迟到"; + break; + case "3": + message = "早退"; + break; + default: + message = b; + break; + } + map.put("Result", 0); + map.put("Msg", message); + JSONObject json = new JSONObject(); + json.set("voice_code", -2); + json.set("voice_text", message); + map.put("Content", json); + return map; + } + + /** + * 陌生人打卡 + * @return java.util.Map + */ + @PostMapping(value = "/stranger") + @NoDataSourceBind + public Map strangerClockIn(@RequestBody Map params) { + + String sn = params.get("sn").toString(); + log.error("陌生人打卡,设备号:{},打卡时间:{}", sn, DateDetail.getDateTime2Str(new Date())); + Map map = new HashMap<>(); + map.put("Result", 0); + map.put("Msg", ""); + return map; + } + + /** + * 设备录入自定义编号判断 + * @return java.util.Map + */ + @PostMapping(value = "/addFace") + @NoDataSourceBind + public Map addFace(@RequestBody Map params) { + + Map map = new HashMap<>(); + map.put("Result", 0); + map.put("Msg", ""); + return map; + } + + /** + * 更新用户信息 + * @return java.util.Map + */ + @PostMapping(value = "/user/inf_photo") + @NoDataSourceBind + public Map updateUserInfoPhoto(@RequestBody Map params) { + + log.error("更新用户信息..."); + return attenceMachineService.updateUserInfoPhoto(params); + } + + @GetMapping(value = "/sendUser") + public Object testSendUser(@RequestParam(value = "code") String code, @RequestParam(value = "userId") String userId, @RequestParam(value = "sn") String sn) { + + attenceMachineService.sendUserToMachine(code, userId, sn); + return ActionResult.success(); + } + + @GetMapping(value = "/changeImg") + public Object testChangeImg() { + String img = attenceMachineService.changeImg(); + return ActionResult.success(img); + } + + @GetMapping(value = "/testSendMsg") + public void testSendMsg() { + + socketApi.sendMsg2Client("kaijiayi", "123", "虚空消息..."); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceAIController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceAIController.java new file mode 100644 index 0000000..4aa55af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceAIController.java @@ -0,0 +1,51 @@ +package jnpf.attendance.controller; + +import jnpf.attendance.service.AttendanceAIService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.AttendanceReqDto; +import jnpf.model.attendance.vo.attendance.ClockDataReqVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 考勤AI接口支持 + * + * @author yanwenfu + * @create 2026-05-07 + */ +@RestController +@RequestMapping(value = "/attendance/ai") +public class AttendanceAIController { + + @Resource + private AttendanceAIService attendanceAIService; + + /** + * 根据日期查询考勤打卡记录 + * @param dto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/clock-record") + public ActionResult getClockRecordByDate(AttendanceReqDto dto) { + + ClockDataReqVo req = attendanceAIService.getClockRecordByDate(dto); + return ActionResult.success(req); + } + + /** + * 查询考勤组加班规则 + * @param groupId 考勤组id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/overtime-rule/{groupId}") + public ActionResult getOvertimeRule(@PathVariable(value = "groupId") String groupId) { + + OvertimeRuleVo vo = attendanceAIService.getOvertimeRule(groupId); + return ActionResult.success(vo); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceApproveController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceApproveController.java new file mode 100644 index 0000000..ed5caf2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceApproveController.java @@ -0,0 +1,829 @@ +package jnpf.attendance.controller; + + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.AttendanceApi; +import jnpf.attendance.service.AttendanceApproveService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.engine.FlowTaskApi; +import jnpf.entity.attendance.AttendanceApprovalAdminVo; +import jnpf.entity.workflow.AttendanceWorkOvertimeApprove; +import jnpf.entity.workflow.SelfApprove; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.exception.ApproveException; +import jnpf.exception.HandleException; +import jnpf.exception.LoginException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.AttendanceToThousandsFacesVo; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import jnpf.model.attendance.vo.attendance.LeaveConsumptionDetailVo; +import jnpf.model.attendance.vo.attendance.TimeVo; +import jnpf.model.attendance.vo.flow.HandlerVo; +import jnpf.model.doclibrary.vo.UseDetailVo; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveOaDto; +import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto; +import jnpf.model.workflow.dto.SelfApproveDto; +import jnpf.util.*; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.workflow.service.SelfApproveService; +import jnpf.workflow.service.WorkOvertimeApproveService; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.util.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * describe + * 考勤审批相关 + * + * @author HuangLinPan + * @date 2023/11/22 + */ + +@RestController +@RequestMapping(value = "/attendance") +@Slf4j +public class AttendanceApproveController implements AttendanceApi { + + @Resource + private AttendanceApproveService attendanceApproveService; + @Autowired + private WorkOvertimeApproveService workApproveService; + @Resource + private UserProvider userProvider; + + @Autowired + private ConfigValueUtil configValueUtil; + + @Resource + private AttendanceGroupService attendanceGroupService; + + /** + * 假期劵的使用记录 + * + * @param id 劵的id + * @param balanceQueryDto 参数 + * @return jnpf.base.ActionResult + * @author hlp + */ + @GetMapping("/useDetail/{id}") + public ActionResult> getUseDetail(@PathVariable("id") String id, BalanceQueryDto balanceQueryDto) { + PageInfo vo = attendanceApproveService.getUseDetail(id, balanceQueryDto); + return ActionResult.page(vo.getList(), FtbUtil.getPagination(vo)); + } + + + /** + * 定时失效劵 + * + * @param tenantId 租户id + * @return java.lang.Boolean + * @author hlp + */ + @Override + @PostMapping(value = "/invalidationCoupons") + public Boolean invalidationCoupons(@RequestParam String tenantId) { + return attendanceApproveService.invalidationCoupons(tenantId); + } + + + /** + * 每月定时计算用户存休 + * + * @param tenantId 租户Id + */ + @Override + @PostMapping(value = "/storageRest") + public Boolean storageRest(@RequestParam(value = "tenantId") String tenantId) { + return attendanceApproveService.storageRest(tenantId); + } + + + /** + * 加班--开始日期选择后触发接口返回当天及后一天的排班信息 + * + * @param balanceQueryDto 参数 + * @return jnpf.base.ActionResult> + * @author hlp + */ + @GetMapping("/workOverTime/getClasses") + public ActionResult> getClasses(BalanceQueryDto balanceQueryDto) { + List vo = attendanceApproveService.getClasses(balanceQueryDto); + return ActionResult.success(vo); + } + + /** + * 加班--加班明细 + * + * @param balanceQueryDto 参数 + * @return jnpf.base.ActionResult> + * @author hlp + */ + @GetMapping("/workOverTime/shifts") + public ActionResult getWorkOverTimeShifts(BalanceQueryDto balanceQueryDto) { + try { + LeaveShiftVo vo = attendanceApproveService.getWorkOverTimeShifts(balanceQueryDto); + return ActionResult.success(vo); + } catch (Exception e) { + return ActionResult.fail(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode(), e.getMessage()); + } + } + + + /** + * 获取请假申请时长及设计班次(请假时选择了开始和结束时间后请求该接口) + * + * @param leaveQueryDto 参数 + * @return jnpf.base.ActionResult + * @author hlp + */ + @GetMapping("/leaveDuration") + public ActionResult getLeaveDuration(LeaveQueryDto leaveQueryDto) throws HandleException { + if (null == leaveQueryDto.getStartTime() || leaveQueryDto.getStartTime().isEmpty()) { + return ActionResult.fail("开始时间不能为空"); + } + if (null == leaveQueryDto.getEndTime() || leaveQueryDto.getEndTime().isEmpty()) { + return ActionResult.fail("结束时间不能为空"); + } + LeaveShiftVo vo = attendanceApproveService.getLeaveDuration(leaveQueryDto); + return ActionResult.success(vo); + } + + /** + * 请假审批通过后的触发接口 + * + * @param tenantId 租户id + * @param applyId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + * @param userId 审批用户Id + */ + @GetMapping(value = "/approve/leave/pass") + @NoDataSourceBind + public void leaveApprove(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "applyId") String applyId, @RequestParam(value = "status") Integer status, @RequestParam("userId") String userId, @RequestParam("userName") String userName) throws ApproveException { + log.error("请假审批通过后的触发接口 tenantId : {},入参applyId:{}", tenantId, applyId); + checkOutTenant(tenantId); + attendanceApproveService.leaveApprove(applyId, status, tenantId, userId, userName); + } + + /** + * 加班审批通过后触发接口 + * + * @param tenantId 租户id + * @param applyId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @param userId 审批用户Id + * @author hlp + */ + @GetMapping(value = "/approve/work/pass") + @NoDataSourceBind + public void workApprove(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "applyId") String applyId, @RequestParam(value = "status") Integer status, @RequestParam("userId") String userId, @RequestParam("userName") String userName) throws ApproveException { + log.error("加班审批通过后的触发接口 tenantId : {},入参applyId:{}", tenantId, applyId); + checkOutTenant(tenantId); + attendanceApproveService.workApprove(applyId, status, tenantId, userId, userName); + } + + /** + * 借调审批通过后触发接口 + * + * @param tenantId 租户id + * @param applyId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @param userId 审批用户Id + * @author hlp + */ + @GetMapping(value = "/approve/self/pass") + @NoDataSourceBind + public void selfApprove(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "applyId") String applyId, @RequestParam(value = "departureTime") String departureTime, @RequestParam(value = "backTime") String backTime, @RequestParam(value = "status") Integer status, @RequestParam("userId") String userId, @RequestParam("userName") String userName) throws ApproveException, HandleException { + log.error("借调审批通过后的触发接口 tenantId : {},入参applyId:{}", tenantId, applyId); + checkOutTenant(tenantId); + attendanceApproveService.selfApprove(applyId, departureTime, backTime, status, userId, userName, tenantId); + } + + /** + * 审批校验接口 + * + * @param taskId 任务id + * @param type 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批 7.外出 8.出差 + * @return jnpf.base.ActionResult + * @author hlp + */ + @PostMapping("/approvalCheck") + public ActionResult getApprovalAdmin(@RequestParam("taskId") String taskId, @RequestParam("type") String type) { + return attendanceApproveService.getApprovalAdmin(taskId, type); + } + + /** + * 提交时审批校验接口 + * + * @param type 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批 7.外出 8.出差 + * @return jnpf.base.ActionResult + * @author hlp + */ + @PostMapping("/submitApprovalCheck") + public ActionResult submitValidation(@RequestParam("type") String type) { + String body = getBody(ServletUtil.getRequest()); + return attendanceApproveService.submitValidation(body, type); + } + + /** + * 获取考勤组审批管理员 + * + * @param taskId 考勤组id + * @param type 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.外出 7.出差 其他:借调 + * @return + */ + @PostMapping("/getApprovalAdmin") + public ActionResult getApprovalAdmin(HttpServletRequest request, String taskId, String type) throws Exception { + ServletInputStream inputStream = request.getInputStream(); + byte[] bytes = IOUtils.toByteArray(inputStream); + String body = new String(bytes, "UTF-8"); + + type = type.split("\\?")[0]; + Integer typeInt = Integer.parseInt(type); + HandlerVo handlerVo = new HandlerVo(); + if (ApprovalSettingTypeEnum.LEAVE.getCode().equals(typeInt)) { + /** 请假审批*/ +// System.out.println("请假审批..."); + AttendanceApprovalAdminVo approveUser = attendanceApproveService.getLeaveApproveUser(body); + List userList = approveUser.getUserList(); + if (CollectionUtil.isEmpty(userList)) { + log.error("没有找到该考勤组请假审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = toUserIdsStr(userList); + handlerVo.setHandleId(idsStr); +// System.out.println("请假审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + if (ApprovalSettingTypeEnum.OVERTIME.getCode().equals(typeInt)) { + /** 加班审批*/ +// System.out.println("加班审批..."); + AttendanceApprovalAdminVo approveUser = attendanceApproveService.getOvertimeApproveUser(body); +// System.out.println("approveUser:" + approveUser.toString()); + if (CollectionUtil.isEmpty(approveUser.getUserList())) { + log.error("没有找到该考勤组加班审批管理员"); + return ActionResult.success(handlerVo); + } + handlerVo.setHandleId(toUserIdsStr(approveUser.getUserList())); +// System.out.println("加班审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + if (ApprovalSettingTypeEnum.ROUTINE.getCode().equals(typeInt)) { + /** 补卡审批*/ +// System.out.println("补卡审批..."); + AttendanceApprovalAdminVo approveUser = attendanceApproveService.getReplacementCardApproveUser(body); + List userList = approveUser.getUserList(); + if (CollectionUtil.isEmpty(userList)) { + log.error("没有找到该考勤组补卡审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = toUserIdsStr(userList); + handlerVo.setHandleId(idsStr); +// System.out.println("补卡审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + if (ApprovalSettingTypeEnum.ACTION_RESULT.getCode().equals(typeInt)) { + /** 调整出勤结果*/ +// System.out.println("调整出勤结果审批..."); + AttendanceApprovalAdminVo approvalUser = attendanceApproveService.getAttendanceAlter(body); + List userList = approvalUser.getUserList(); + if (CollectionUtil.isEmpty(userList)) { + log.error("没有找到该考勤组调整出勤结果审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = toUserIdsStr(userList); + handlerVo.setHandleId(idsStr); +// System.out.println("调整出勤审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + if (ApprovalSettingTypeEnum.OUT.getCode().equals(typeInt)) { + /** 外勤审批*/ +// System.out.println("外勤审批..."); + AttendanceApprovalAdminVo approvalUser = attendanceApproveService.getFieldApproval(); + List userList = approvalUser.getUserList(); + if (CollectionUtil.isEmpty(userList)) { + log.error("没有找到该考勤组外勤审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = toUserIdsStr(userList); + handlerVo.setHandleId(idsStr); +// System.out.println("外勤审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + if (ApprovalSettingTypeEnum.GO_OUT.getCode().equals(typeInt)) { + /** 外出审批*/ + AttendanceApprovalAdminVo approvalUser = attendanceApproveService.getGoOutApproval(); + List userList = approvalUser.getUserList(); + if (CollectionUtil.isEmpty(userList)) { + log.error("没有找到该考勤组外出审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = toUserIdsStr(userList); + handlerVo.setHandleId(idsStr); +// System.out.println("外勤审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + if (ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode().equals(typeInt)) { + /** 出差审批*/ +// System.out.println("出差审批..."); + AttendanceApprovalAdminVo approvalUser = attendanceApproveService.getBusinessTripApproval(); + List userList = approvalUser.getUserList(); + if (CollectionUtil.isEmpty(userList)) { + log.error("没有找到该考勤组出差审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = toUserIdsStr(userList); + handlerVo.setHandleId(idsStr); +// System.out.println("出差审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + /** 借调审批*/ +// System.out.println("借调审批..."); + List managerIds = attendanceApproveService.secondedApproval(body); + if (CollectionUtil.isEmpty(managerIds)) { + log.error("没有找到该考勤组借调审批管理员"); + return ActionResult.success(handlerVo); + } + String idsStr = CollectionUtil.join(managerIds, ","); + handlerVo.setHandleId(idsStr); +// System.out.println("借调审批-userIds:" + handlerVo); + return ActionResult.success(handlerVo); + } + + /** + * 获取考勤组管理员具体信息 + * + * @param type 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.外出 7.出差 0:借调 + * @return ActionResult + */ + @GetMapping("/getGroupAdminInfo/{type}") + public ActionResult getGroupAdminInfo(@PathVariable("type") Integer type, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime, + @RequestParam(required = false) String clockInResultId, + @RequestParam(required = false) String selfGroupId + ) throws HandleException { + + AttendanceApprovalAdminVo approvalAdminVo = attendanceApproveService.getGroupAdminInfo(type, startTime, endTime, clockInResultId, selfGroupId); + return ActionResult.success(approvalAdminVo); + } + + + /** + * 返回用户id + * + * @param userVoList + * @return + */ + private String toUserIdsStr(List userVoList) { + List userIds = userVoList.stream().map(AttendanceUserVo::getUserId).collect(Collectors.toList()); + return CollectionUtil.join(userIds, ","); + } + + + public void checkOutTenant(String tenantId) { + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } + + /** + * 获取远程请求参数 + * + * @return + * @throws IOException + */ + private String getBody(HttpServletRequest request) { + ServletInputStream inputStream = null; + try { + inputStream = request.getInputStream(); + byte[] bytes = IOUtils.toByteArray(inputStream); + String body = new String(bytes, "UTF-8"); + return body; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 获取考勤组用户余额 + * + * @param groupId 考勤组 + * @param month 年月(2024-5-13) + * @return jnpf.base.ActionResult + * @author hlp + */ + @GetMapping("/userBalanceByGroupId") + public ActionResult> getBalanceDetailS(@RequestParam String groupId, @RequestParam String month) { + List vo = attendanceApproveService.getBalanceDetailS(groupId, month); + return ActionResult.success(vo); + } + + /** + * 外出审批通/拒绝时调用接口 + * + * @param tenantId 租户Id + * @param applyId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + * @param userId 审批用户Id + * @throws ApproveException + */ + @GetMapping(value = "/approve/goOut/pass") + @NoDataSourceBind + public void goOutApprove(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "applyId") String applyId, @RequestParam(value = "status") Integer status, @RequestParam("userId") String userId, @RequestParam("userName") String userName) throws ApproveException { + log.error("外出审批通过后的触发接口 tenantId : {}", tenantId); + log.error("外出审批通过后的触发接口 入参applyId : {}", applyId); + checkOutTenant(tenantId); + attendanceApproveService.goOutApprove(applyId, status, tenantId, userId, userName); + } + + /** + * 出差审批通/拒绝时调用接口 + * + * @param tenantId 租户Id + * @param applyId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + * @param userId 审批用户Id + * @throws ApproveException + */ + @GetMapping(value = "/approve/businessTrip/pass") + @NoDataSourceBind + public void businessTripApprove(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "applyId") String applyId, @RequestParam(value = "status") Integer status, @RequestParam("userId") String userId, @RequestParam("userName") String userName) throws ApproveException { + checkOutTenant(tenantId); + attendanceApproveService.businessTripApprove(applyId, status, tenantId, userId, userName); + } + + + /** + * 发消息通知下一节点审核人 + * + * @param workflowImQueryDto 审批参数信息 + */ + @GetMapping("/im") + @NoDataSourceBind + public ActionResult workIm(WorkflowImQueryDto workflowImQueryDto) { + log.error("发消息通知下一节点审核人入参: {}", workflowImQueryDto.toString()); + // 因OA流调用时会等待流程完成后才会提交事务,本接口会依赖于事务的提交结果,所以单独开一个线程执行 + new Thread(() -> { + checkOutTenant(workflowImQueryDto.getTenantId()); + attendanceApproveService.sendIm(workflowImQueryDto); + }).start(); + + return ActionResult.success(); + } + + /** + * 获取用户当前时间所在考勤组的请假类型 + */ +// @GetMapping("/leaveList") +// public ActionResult getLeaveList() throws HandleException { +// return attendanceApproveService.getLeaveList(); +// } + + /***************************************以下为考勤V1.7版本提供的新接口****************************************************/ + + /** + * 新版OA收拢保存考勤出差审批 + * + * @param approveOaDto 出差对象 + */ + @PostMapping("/businessTripForOa") + public ActionResult createBusinessTrip(@RequestBody @Valid AttendanceBusinessTripApproveOaDto approveOaDto) { + log.error("FoeOa开始出差了参数................: {}", approveOaDto); + // AttendanceBusinessTripApproveOaDto(id=null, userId=["450558716723340421"], + // departure=[{"detailAddress":"","areaLabels":"贵州省/贵阳市/市辖区","areaIds":["354094501027910","354094501027911","354094501027912"]}], + // destination=[{"detailAd"四川省/南充市/市辖区","areaIds":["354094498033736","354094499168330","354094499168331"]}], + // startTime=null, endTime=null, dayNum=null, transportationVehicles=4, reason=------------, fileJs, time=[1730390400000, 1730476800000]) + return attendanceApproveService.createBusinessTrip(approveOaDto); + } + + /** + * 新版OA收拢出差审批通过后触发接口 + * + * @param taskId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @author hlp + */ + @GetMapping(value = "/oaApprove/businessTrip/pass") + public ActionResult businessTripApprove(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "status") Integer status) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + try { + UserInfo userInfo = userProvider.get(); + log.error("出差taskId:{},status:{},userInfo:{}", taskId, status, userInfo); + attendanceApproveService.businessTripApprove(taskId, status, userInfo.getTenantId(), userInfo.getUserId(), userInfo.getUserName()); + } catch (ApproveException e) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + } + return result; + } + + /** + * 通过传入的开始结束时间及单位获取时长 + * + * @param dto 时长对象 + */ + @GetMapping("/getDurationForOa") + public ActionResult getDurationForOa(@RequestBody DurationForOaDto dto) { + log.error("FoeOa通过传入的开始结束时间及单位获取时长了参数................: {}", dto); + return attendanceApproveService.getDurationForOa(dto); + } + + /** + * 新版OA收拢保存考勤外出审批 小时外出逻辑 + * + * @param dto 对象 + */ + @PostMapping("/goOutForOa") + public ActionResult createGoOutForOa(@RequestBody GoOutApproveForOaDto dto) { + log.error("FoeOa开始外出了参数................: {}", dto); + return attendanceApproveService.createGoOutForOa(dto); + } + + /** + * 新版OA收拢外出审批通过后触发接口 小时外出逻辑 + * + * @param taskId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @author hlp + */ + @GetMapping(value = "/oaApprove/goOut/pass") + public ActionResult goOutApproveForOa(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "status") Integer status) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + try { + UserInfo userInfo = userProvider.get(); + log.error("外出taskId:{},status:{},userInfo:{}", taskId, status, userInfo); + attendanceApproveService.goOutApprove(taskId, status, userInfo.getTenantId(), userInfo.getUserId(), userInfo.getUserName()); + } catch (ApproveException e) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + } + return result; + } + + /** + * 新版OA收拢保存考勤请假审批 + * + * @param dto 对象 + */ + @PostMapping("/leaveForOa") + public ActionResult createLeaveForOa(@RequestBody LeaveApproveForOaDto dto) { + log.error("FoeOa开始请假了参数................: {}", dto); + return attendanceApproveService.createLeaveForOa(dto); + } + + /** + * 请假审批通过后的触发接口 + * + * @param taskId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + */ + @GetMapping(value = "/oaApprove/leave/pass") + public ActionResult leaveApproveForOa(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "status") Integer status) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + try { + UserInfo userInfo = userProvider.get(); + log.error("请假taskId:{},status:{},userInfo:{}", taskId, status, userInfo); + attendanceApproveService.leaveApprove(taskId, status, userInfo.getTenantId(), userInfo.getUserId(), userInfo.getUserName()); + } catch (Exception e) { + log.error("exception", e); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + } + log.error("请假 result:{}", result); + return result; + } + + + + /** + * 新建加班申请 + * + * @param attendanceWorkOvertimeApproveDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/createWorkOvertimeForOa") + public Object createWorkOvertimeForOa(@RequestBody @Valid AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto) { + // AttendanceWorkOvertimeApproveDto(super=AttendanceBaseForOa(super=jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto@dc44583a, taskId=623849038774196101, status=null, fFlowtaskid=null, fFlowid=null, uniqueId=null, tenantId=null, flowTitle=null, applyUser=["450558466533106821"], startTime=Wed Nov 06 14:12:00 CST 2024, endTime=Wed Nov 06 15:13:00 CST 2024, time=[1730873520000, 1730877180000], unit=null), id=null, userId=null, workDay=null, startTime=Wed Nov 06 14:12:00 CST 2024, endTime=Wed Nov 06 15:13:00 CST 2024, time=[1730873520000, 1730877180000], reason=null, flowtaskid=null, flowid=null, uniqueid=null, tenantId=null, picture=null, fileJson=null, flowTitle=null, applyUser=["450558466533106821"], applyDate=null, submitStatus=null, shiftInvolved=null, groupId=504574013792892037) + log.error("新建加班申请 参数 : {}", attendanceWorkOvertimeApproveDto); + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + String userId = userProvider.get().getUserId(); + attendanceWorkOvertimeApproveDto.setUserId(userId); + attendanceWorkOvertimeApproveDto.setTenantId(userProvider.get().getTenantId()); + attendanceWorkOvertimeApproveDto.setId(attendanceWorkOvertimeApproveDto.getTaskId()); + AttendanceWorkOvertimeApprove entity = JsonUtil.getJsonToBean(attendanceWorkOvertimeApproveDto, AttendanceWorkOvertimeApprove.class); + // 处理加班日期 + entity.setWorkDay(attendanceWorkOvertimeApproveDto.getStartTime()); + try { + attendanceApproveService.submitCheckWorkForOa(attendanceWorkOvertimeApproveDto); + entity.setId(attendanceWorkOvertimeApproveDto.getTaskId()); + workApproveService.saveForOa(entity); + } catch (Exception e) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + } + return result; + } + + + /** + * 加班审批通过后触发接口 + * + * @param taskId 审批的唯一id 对应表f_id + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @author hlp + */ + @GetMapping(value = "/oaApprove/work/pass") + public ActionResult workApproveForOa(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "status") Integer status) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + try { + UserInfo user = UserProvider.getUser(); + log.error("加班审批通过后的触发接口 tenantId : {},入参applyId:{}", user.getTenantId(), taskId); + attendanceApproveService.checkWork(taskId); + attendanceApproveService.workApprove(taskId, status, user.getTenantId(), user.getUserId(), user.getUserName()); + } catch (Exception e) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + } + return result; + } + + /** + * 获取用户所选时间段所在考勤组 + */ + @PostMapping("/getUserGroupByTime") + public ActionResult getUserGroupByTime(@RequestBody GroupOaDto groupOaDto) { + log.error("getUserGroupByTime groupOaDto:{}", groupOaDto.toString()); + if (groupOaDto.isFromOaCondition()){ + List attendanceGroupVos = attendanceGroupService.secondedApprovalGroupListNew(); + return ActionResult.success(attendanceGroupVos.stream().map(vo->{ + GroupMiniVo groupMiniVo = new GroupMiniVo(); + groupMiniVo.setGroupId(vo.getId()); + groupMiniVo.setGroupName(vo.getGroupName()); + return groupMiniVo; + }).collect(Collectors.toList())); + } + + if (null == groupOaDto.getUserId()) { + groupOaDto.setUserIdStr(userProvider.get().getUserId()); + } else { + groupOaDto.setUserIdStr(groupOaDto.getUserId()[0]); + } + log.error("getUserGroupByTime groupOaDto:{}", groupOaDto); + return attendanceApproveService.getUserGroupByTime(groupOaDto); + } + + + /** + * oa收拢获取请假申请时长及设计班次(请假时选择了开始和结束时间后请求该接口) + * + * @param leaveQueryForOaDto 参数 + * @return jnpf.base.ActionResult + * @author hlp + */ + @PostMapping("/leaveDurationForOa") + public ActionResult getLeaveDuration(@RequestBody LeaveQueryForOaDto leaveQueryForOaDto) throws HandleException { + log.error("leaveDurationForOa leaveQueryForOaDto:{}", leaveQueryForOaDto); + if (null == leaveQueryForOaDto.getTime() || leaveQueryForOaDto.getTime().length < 1) { + return ActionResult.fail("时间参数不能为空"); + } + LeaveQueryDto leaveQueryDto = new LeaveQueryDto(leaveQueryForOaDto.getUnit(), leaveQueryForOaDto.getStartTime().getTime() + "", leaveQueryForOaDto.getEndTime().getTime() + "", leaveQueryForOaDto.getStartTimeType(), leaveQueryForOaDto.getEndTimeType(), leaveQueryForOaDto.getApplyId(), leaveQueryForOaDto.getTypeId()); + log.error("leaveDurationForOa leaveQueryDto:{}", leaveQueryDto); + LeaveShiftVo vo = attendanceApproveService.getLeaveDuration(leaveQueryDto); + return ActionResult.success(vo); + } + + /** + * 获取用户当前时间所在考勤组 + */ + @GetMapping("/nowGroup") + public ActionResult getNowGroup() { + return attendanceApproveService.getNowGroup(); + } + + + /** + * 提供OA使用时长计算 + * + * @param time 时间数组 + * @return 时长 + */ + @GetMapping("/oaForTime") + public ActionResult getOaForTime(String[] time) { + Date startTime = new Date(Long.valueOf(time[0])); + Date endTime = new Date(Long.valueOf(time[1])); + // 校验结束时间不能小于开始时间 + if (endTime.before(startTime)) { + return ActionResult.fail("结束时间不能小于开始时间"); + } + + BigDecimal totalDuration = FtbUtil.getTimeDifference(startTime, endTime); + TimeVo timeVo = new TimeVo(); + timeVo.setTotalDuration(totalDuration); + return ActionResult.success(timeVo); + + } + + + @Override + @PostMapping(value = "/attendanceToThousandsFaces") + public AttendanceToThousandsFacesVo attendanceToThousandsFaces() { + return attendanceApproveService.attendanceToThousandsFaces(); + } + + /** + * 选择了请假类型和请假时间后请求接口,获取用户余额使用情况: + * + * @param leaveQueryForOaDto 参数 + * @return 用户剩余请假时长 + */ + + @PostMapping("/residueBalance") + public ActionResult getResidueBalance(@RequestBody LeaveQueryForOaDto leaveQueryForOaDto) { + log.error("leaveDurationForOa leaveQueryForOaDto:{}", leaveQueryForOaDto); + if (null == leaveQueryForOaDto.getTime() || leaveQueryForOaDto.getTime().length < 1) { + return ActionResult.fail("时间参数不能为空"); + } + LeaveQueryDto leaveQueryDto = new LeaveQueryDto(leaveQueryForOaDto.getUnit(), leaveQueryForOaDto.getStartTime().getTime() + "", leaveQueryForOaDto.getEndTime().getTime() + "", leaveQueryForOaDto.getStartTimeType(), leaveQueryForOaDto.getEndTimeType(), leaveQueryForOaDto.getApplyId(), leaveQueryForOaDto.getTypeId()); + if (null == leaveQueryDto.getStartTime() || leaveQueryDto.getStartTime().isEmpty()) { + return ActionResult.fail("开始时间不能为空"); + } + if (null == leaveQueryDto.getEndTime() || leaveQueryDto.getEndTime().isEmpty()) { + return ActionResult.fail("结束时间不能为空"); + } + if (null == leaveQueryDto.getTypeId() || leaveQueryDto.getTypeId().isEmpty()) { + return ActionResult.fail("请选择请假类型"); + } + LeaveConsumptionDetailVo residueBalance = null; + try { + residueBalance = attendanceApproveService.getResidueBalance(leaveQueryDto); + } catch (Exception e) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + return result; + } + // 3天调休假+0.5天存休(剩余5.5天) + StringBuilder sb = new StringBuilder(); + assert residueBalance != null; + if (residueBalance.getInputTypeBalance().compareTo(BigDecimal.ZERO) > 0) { + sb.append(residueBalance.getInputTypeBalanceStr()); + } + if (residueBalance.getInputTypeBalance().compareTo(BigDecimal.ZERO) > 0 && residueBalance.getRetirementLeave().compareTo(BigDecimal.ZERO) > 0) { + sb.append("+"); + } + if (residueBalance.getRetirementLeave().compareTo(BigDecimal.ZERO) > 0) { + sb.append(residueBalance.getRetirementLeaveStr()); + } + if (residueBalance.getBalance().compareTo(BigDecimal.ZERO) > 0) { + sb.append("(剩余").append(residueBalance.getBalance().setScale(2, RoundingMode.HALF_UP)).append("天)"); + } + return ActionResult.success(new HashMap<>(){{put("residueBalance",sb.toString());}}); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBaseSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBaseSettingController.java new file mode 100644 index 0000000..1a7859b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBaseSettingController.java @@ -0,0 +1,121 @@ +package jnpf.attendance.controller; + + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import jnpf.attendance.service.AttendanceBaseSettingService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceBaseSettingDto; +import jnpf.model.attendance.vo.AttendanceBaseSettingVo; +import jnpf.model.attendance.vo.attendance.QuickCheckInVo; +import jnpf.util.ConstantUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 考勤基础设置表 前端控制器 + * + * @author ahua + * @since 2023-11-29 + */ +@RestController +@RequestMapping("/base/setting") +public class AttendanceBaseSettingController { + @Autowired + private AttendanceBaseSettingService attendanceBaseSettingService; + + /** + * 保存考勤基础设置信息。 + * + * @param attendanceBaseSettingDto 考勤基础设置数据传输对象 + * @throws HandleException 处理异常 + * @return 处理结果视图对象 + */ + @PostMapping + public ActionResult save(@RequestBody AttendanceBaseSettingDto attendanceBaseSettingDto) throws HandleException { + attendanceBaseSettingService.save(attendanceBaseSettingDto); + return ActionResult.success(); + } + + /** + * 根据考勤组ID获取单个考勤基础设置。 + * + * @param groupId 考勤组ID + * @return 考勤基础设置视图对象 + */ + @GetMapping("{groupId}") + public ActionResult getOne(@PathVariable String groupId) { + return ActionResult.success(attendanceBaseSettingService.getOne(groupId)); + } + + /** + * 更改考勤基础设置的启用状态。 + * + * @param groupId 考勤组ID + * @param enable 启用状态(0禁用,1启用) + * @return 处理结果视图对象 + */ + @PutMapping + public ActionResult changeStatus(@RequestParam String groupId, @RequestParam Integer enable) { + attendanceBaseSettingService.changeStatus(groupId, enable, attendanceBaseSettingService.getOne(groupId)); + return ActionResult.success(); + } + + /** + * 查询用户外勤是否必须拍照 + * @param isFromOaCondition 是否来自OA条件 是需要返回所有可能选项 + * @return java.lang.Object + */ + @GetMapping(value = "/takePhoto") + public Object getTakePhotoSetting(@RequestParam(required = false) boolean isFromOaCondition) throws Exception { + if (isFromOaCondition) { + JSONArray array = new JSONArray(); + JSONObject json = new JSONObject(); + json.set("state", ConstantUtil.NUM_TRUE); + json.set("name", "是"); + array.put(json); + JSONObject json1 = new JSONObject(); + json1.set("state", ConstantUtil.NUM_FALSE); + json1.set("name", "否"); + array.put(json1); + return ActionResult.success(array); + } + UserInfo user = UserProvider.getUser(); + JSONArray shouldTakePhoto = attendanceBaseSettingService.getTakePhotoSetting(user); + return ActionResult.success(shouldTakePhoto); + } + + /** + * 查询用户外勤是否必须拍照 + * @return java.lang.Object + */ + @GetMapping(value = "/takePhoto2") + public Object getTakePhotoSetting2() throws Exception { + + UserInfo user = UserProvider.getUser(); + JSONArray shouldTakePhoto = attendanceBaseSettingService.getTakePhotoSetting(user); + return ActionResult.success(shouldTakePhoto.get(0)); + } + + + /** + * 获取用户当前时间所处考勤组的内勤打卡基础设置。(过度使用) + */ + @GetMapping("/getNowGroupAttendancePhoto") + public ActionResult getNowGroupAttendancePhoto() { + return ActionResult.success(attendanceBaseSettingService.getNowGroupAttendancePhoto()); + } + + /** + * 获取用户当前时间所处考勤组是否能快速打卡 + * 和用户当前所处考勤组的基础设置中的是否需要拍照,是否需要人脸识别以及App个人设置中的上下班快速打开相关 + */ + @GetMapping("/quickCheckIn") + public ActionResult quickCheckIn() { + return ActionResult.success(attendanceBaseSettingService.quickCheckIn()); + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookConfigController.java new file mode 100644 index 0000000..1e8cf24 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookConfigController.java @@ -0,0 +1,305 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.AttendanceBookConfigService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.AttendanceBookConfigVo; +import jnpf.model.attendance.vo.AttendanceResultOptionGroupVo; +import jnpf.model.attendance.vo.AttendanceResultOptionVo; +import jnpf.model.attendance.vo.BookConfigCreatorVo; +import jnpf.model.attendance.vo.BookPersonnelVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 考勤本配置Controller + * + * @author Generated + * @create 2026-04-15 + */ +@Slf4j +@RestController +@RequestMapping("/attendance/book-config") +@Tag(name = "考勤本配置", description = "考勤本配置管理") +public class AttendanceBookConfigController { + + @Resource + private AttendanceBookConfigService attendanceBookConfigService; + + /** + * web-分页查询考勤本配置列表 + * + * @param queryDto 查询条件 + * @return 分页结果 + */ + @Operation(summary = "web/app-分页查询考勤本配置列表(app用于当前登录人为负责人的考勤本列表)") + @PostMapping("/list") + public ActionResult> getList(@RequestBody AttendanceBookConfigQueryDto queryDto) { + try { + log.info("分页查询考勤本配置列表,入参=>{}", JSONObject.toJSON(queryDto)); + long st = System.currentTimeMillis(); + + PageInfo pageInfo = attendanceBookConfigService.getPageList(queryDto); + + long ent = System.currentTimeMillis(); + log.info("分页查询考勤本配置列表,耗时=>{} 毫秒", ent - st); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } catch (Exception e) { + log.error("分页查询考勤本配置列表异常", e); + return ActionResult.fail("查询失败:" + e.getMessage()); + } + } + + /** + * web-新增或更新考勤本配置 + *

根据DTO中的id判断:id为空则新增,id不为空则更新

+ * + * @param configDto 考勤本配置DTO + * @return 操作结果 + */ + @Operation(summary = "web-新增或更新考勤本配置") + @PostMapping("/save-or-update") + public ActionResult saveOrUpdate(@Valid @RequestBody AttendanceBookConfigDto configDto) { + try { + String action = StringUtils.isBlank(configDto.getId()) ? "新增" : "更新"; + log.info("{}考勤本配置,入参=>{}", action, JSONObject.toJSON(configDto)); + long st = System.currentTimeMillis(); + + attendanceBookConfigService.saveOrUpdateAttendanceBookConfig(configDto); + + long ent = System.currentTimeMillis(); + log.info("{}考勤本配置,耗时=>{} 毫秒", action, ent - st); + return ActionResult.success(action + "成功"); + } catch (HandleException e) { + log.error("保存考勤本配置业务异常", e); + return ActionResult.fail(e.getMessage()); + } catch (Exception e) { + log.error("保存考勤本配置异常", e); + return ActionResult.fail("保存失败:" + e.getMessage()); + } + } + + /** + * web-删除考勤本配置 + * + * @param id 考勤本ID + * @return 操作结果 + */ + @Operation(summary = "web-删除考勤本配置") + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable String id) { + try { + log.info("删除考勤本配置,id=>{}", id); + attendanceBookConfigService.deleteAttendanceBookConfig(id); + return ActionResult.success("删除成功"); + } catch (Exception e) { + log.error("删除考勤本配置异常", e); + return ActionResult.fail("删除失败:" + e.getMessage()); + } + } + + /** + * web-更新启用状态 + * + * @param id 考勤本ID + * @return 操作结果 + */ + @Operation(summary = "web-更新启用状态") + @PutMapping("/{id}/status") + public ActionResult updateStatus(@PathVariable String id) { + try { + log.info("更新考勤本启用状态,id=>{}", id); + attendanceBookConfigService.updateEnableStatus(id); + return ActionResult.success("状态更新成功"); + } catch (HandleException e) { + log.error("更新考勤本状态业务异常", e); + return ActionResult.fail(e.getMessage()); + } catch (Exception e) { + log.error("更新考勤本状态异常", e); + return ActionResult.fail("状态更新失败:" + e.getMessage()); + } + } + + /** + * web-获取考勤本配置详情 + * + * @param id 考勤本ID + * @return 考勤本配置详情 + */ + @Operation(summary = "web-获取考勤本配置详情") + @GetMapping("/{id}") + public ActionResult getDetail(@PathVariable String id) { + try { + log.info("获取考勤本配置详情,id=>{}", id); + AttendanceBookConfigVo vo = attendanceBookConfigService.getDetail(id); + return ActionResult.success(vo); + } catch (Exception e) { + log.error("获取考勤本配置详情异常", e); + return ActionResult.fail("获取详情失败:" + e.getMessage()); + } + } + + /** + * app-增量添加使用范围人员 + *

向指定考勤本配置的使用范围中增量添加用户列表(不会覆盖已有人员)

+ * + * @param addUserDto 添加用户DTO + * @return 操作结果 + */ + @Operation(summary = "app-增量添加使用范围人员") + @PostMapping("/add-scope-users") + public ActionResult addScopeUsers(@Valid @RequestBody AttendanceBookConfigAddUserDto addUserDto) { + try { + log.info("增量添加使用范围人员,考勤本ID=>{}, 用户数=>{}", + addUserDto.getBookConfigId(), addUserDto.getUserIdList().size()); + long st = System.currentTimeMillis(); + + attendanceBookConfigService.addScopeUsers(addUserDto); + + long ent = System.currentTimeMillis(); + log.info("增量添加使用范围人员,耗时=>{} 毫秒", ent - st); + return ActionResult.success("添加成功"); + } catch (HandleException e) { + log.error("增量添加使用范围人员业务异常", e); + return ActionResult.fail(e.getMessage()); + } catch (Exception e) { + log.error("增量添加使用范围人员异常", e); + return ActionResult.fail("添加失败:" + e.getMessage()); + } + } + + /** + * app-判断当前登录人是否为考勤本负责人 + * + * @param bookConfigId 考勤本配置ID + * @return true-是负责人,false-不是负责人 + */ + @Operation(summary = "app-判断当前登录人是否为考勤本负责人") + @GetMapping("/{bookConfigId}/is-manager") + public ActionResult isCurrentUserManager(@PathVariable String bookConfigId) { + try { + log.info("判断当前登录人是否为考勤本负责人,考勤本ID=>{}", bookConfigId); + boolean isManager = attendanceBookConfigService.isCurrentUserManager(bookConfigId); + return ActionResult.success(isManager); + } catch (HandleException e) { + log.error("判断负责人业务异常", e); + return ActionResult.fail(e.getMessage()); + } catch (Exception e) { + log.error("判断负责人异常", e); + return ActionResult.fail("判断失败:" + e.getMessage()); + } + } + + /** + * web-获取指定组织集合及用户集合在当前时间下的所有涉及用户(筛选考勤本负责人) + *

查询这些组织和用户在当前时间的有效用户集合,返回完整的用户信息列表

+ * + * @param dto 查询参数(组织ID列表、用户ID列表) + * @return 有效用户信息列表 + */ + @Operation(summary = "web-获取指定组织集合及用户集合在当前时间下的所有涉及用户(筛选考勤本负责人)") + @PostMapping("/get-valid-users") + public ActionResult> getValidUsers(@RequestBody GetValidUsersDto dto) { + try { + log.info("获取有效用户列表,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + + List userList = attendanceBookConfigService.getValidUsers(dto); + + long ent = System.currentTimeMillis(); + log.info("获取有效用户列表,耗时=>{} 毫秒,返回用户数: {}", ent - st, userList.size()); + return ActionResult.success(userList); + } catch (Exception e) { + log.error("获取有效用户列表异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app-获取考勤结果下拉列表 + *

根据考勤本配置返回可用的考勤状态选项,包括基础考勤状态和该考勤本配置的请假类型

+ * + * @param bookConfigId 考勤本配置ID + * @return 考勤结果下拉选项列表 + */ + @Operation(summary = "app-获取考勤结果下拉列表(分组)") + @GetMapping("/{bookConfigId}/result-options") + public ActionResult getAttendanceResultOptions(@PathVariable String bookConfigId) { + try { + log.info("获取考勤结果下拉列表(分组),考勤本ID=>{}", bookConfigId); + long st = System.currentTimeMillis(); + + AttendanceResultOptionGroupVo options = attendanceBookConfigService.getAttendanceResultOptions(bookConfigId); + + long ent = System.currentTimeMillis(); + log.info("获取考勤结果下拉列表(分组),耗时=>{} 毫秒", ent - st); + return ActionResult.success(options); + } catch (HandleException e) { + log.error("获取考勤结果下拉列表业务异常", e); + return ActionResult.fail(e.getMessage()); + } catch (Exception e) { + log.error("获取考勤结果下拉列表异常", e); + return ActionResult.fail("获取失败:" + e.getMessage()); + } + } + + /** + * 需求2:获取考勤本配置创建人列表 + * + * @return 创建人列表 + */ + @Operation(summary = "web-获取考勤本配置创建人列表") + @GetMapping("/creator-list") + public ActionResult> getCreatorList() { + try { + log.info("获取考勤本配置创建人列表"); + long st = System.currentTimeMillis(); + + List creatorList = attendanceBookConfigService.getCreatorList(); + + long ent = System.currentTimeMillis(); + log.info("获取考勤本配置创建人列表,耗时=>{} 毫秒,创建人数: {}", ent - st, creatorList.size()); + return ActionResult.success(creatorList); + } catch (Exception e) { + log.error("获取考勤本配置创建人列表异常", e); + return ActionResult.fail("获取失败:" + e.getMessage()); + } + } + + /** + * 需求3:获取考勤本配置人员列表 + * + * @param dto 考勤本配置ID + * @return 人员列表 + */ + @Operation(summary = "app-获取考勤本配置人员列表") + @PostMapping("/personnel-list") + public ActionResult> getPersonnelList(@RequestBody BookRecordMonthListDto dto) { + try { + log.info("获取考勤本配置人员列表,考勤本ID=>{}", dto.getBookId()); + long st = System.currentTimeMillis(); + + List personnelList = attendanceBookConfigService.getBookPersonnelList(dto.getBookId(),dto.getMonth()); + + long ent = System.currentTimeMillis(); + log.info("获取考勤本配置人员列表,耗时=>{} 毫秒,人员数: {}", ent - st, personnelList.size()); + return ActionResult.success(personnelList); + } catch (Exception e) { + log.error("获取考勤本配置人员列表异常", e); + return ActionResult.fail("获取失败:" + e.getMessage()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookOperationLogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookOperationLogController.java new file mode 100644 index 0000000..3558d7c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookOperationLogController.java @@ -0,0 +1,119 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.AttendanceBookOperationLogService; +import jnpf.base.ActionResult; +import jnpf.entity.attendance.AttendanceBookOperationLogEntity; +import jnpf.model.attendance.dto.OperationLogQueryDto; +import jnpf.model.attendance.vo.OperationLogPageVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * 考勤本操作日志Controller + * + * @author Generated + * @create 2026-04-15 + */ +@Slf4j +@RestController +@RequestMapping("/attendance/book-operation-log") +@Tag(name = "考勤本操作日志", description = "考勤本操作日志管理") +public class AttendanceBookOperationLogController { + + @Resource + private AttendanceBookOperationLogService attendanceBookOperationLogService; + + /** + * 新增操作日志 + * + * @param entity 操作日志实体 + * @return 操作结果 + */ + @Operation(summary = "新增操作日志") + @PostMapping + public ActionResult create(@Valid @RequestBody AttendanceBookOperationLogEntity entity) { + try { + log.info("新增操作日志,入参=>{}", JSONObject.toJSON(entity)); + attendanceBookOperationLogService.saveLog(entity); + return ActionResult.success("操作成功"); + } catch (Exception e) { + log.error("新增操作日志异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * web-分页查询操作日志 + * + * @param queryDto 查询参数 + * @return 分页结果 + */ + @Operation(summary = "web-分页查询操作日志") + @GetMapping("/page") + public ActionResult> queryPage(@Valid OperationLogQueryDto queryDto) { + try { + log.info("分页查询操作日志,入参=>{}", JSONObject.toJSON(queryDto)); + long st = System.currentTimeMillis(); + + Page result = attendanceBookOperationLogService.queryPage(queryDto); + + long ent = System.currentTimeMillis(); + log.info("分页查询操作日志,耗时=>{} 毫秒,返回记录数: {}", ent - st, result.getRecords().size()); + return ActionResult.success(result); + } catch (Exception e) { + log.error("分页查询操作日志异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 获取操作日志详情 + * + * @param id 日志ID + * @return 操作结果 + */ + @Operation(summary = "获取操作日志详情") + @GetMapping("/{id}") + public ActionResult getById(@PathVariable String id) { + try { + log.info("获取操作日志详情,id=>{}", id); + AttendanceBookOperationLogEntity entity = attendanceBookOperationLogService.getById(id); + if (entity == null) { + return ActionResult.fail("记录不存在"); + } + return ActionResult.success(entity); + } catch (Exception e) { + log.error("获取操作日志详情异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 删除操作日志 + * + * @param id 日志ID + * @return 操作结果 + */ + @Operation(summary = "删除操作日志") + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable String id) { + try { + log.info("删除操作日志,id=>{}", id); + boolean removed = attendanceBookOperationLogService.removeById(id); + if (!removed) { + return ActionResult.fail("删除失败,记录不存在"); + } + return ActionResult.success("删除成功"); + } catch (Exception e) { + log.error("删除操作日志异常", e); + return ActionResult.fail(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookRecordController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookRecordController.java new file mode 100644 index 0000000..d36d53e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceBookRecordController.java @@ -0,0 +1,244 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.AttendanceBookRecordService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; + +/** + * 考勤本记录表Controller + * + * @author Generated + * @create 2026-04-15 + */ +@Slf4j +@RestController +@RequestMapping("/attendance/book-record") +@Tag(name = "考勤本记录", description = "考勤本记录管理") +public class AttendanceBookRecordController { + + @Resource + private AttendanceBookRecordService attendanceBookRecordService; + + /** + * app-新增或修改考勤记录(Upsert) + * 根据考勤本ID、员工ID、考勤日期、时段类型唯一确定一条记录 + * 如果记录存在则更新,不存在则新增 + * + * @param dto 考勤记录DTO + * @return 操作结果(包含记录ID) + */ + @Operation(summary = "app-新增或修改考勤记录") + @PostMapping("/saveOrUpdate") + public ActionResult saveOrUpdate(@Valid @RequestBody AttendanceBookRecordDto dto) { + try { + log.info("新增或修改考勤记录,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + + String recordId = attendanceBookRecordService.saveOrUpdateRecord(dto); + + long ent = System.currentTimeMillis(); + log.info("新增或修改考勤记录,耗时=>{} 毫秒,记录ID: {}", ent - st, recordId); + return ActionResult.success(recordId); + } catch (Exception e) { + log.error("新增或修改考勤记录异常", e); + return ActionResult.fail("系统异常,请联系管理员"); + } + } + + /** + * web-获取考勤本月统计数据 + * + * @param req 统计请求参数 + * @return 本月统计结果(按员工维度,支持分页) + */ + @Operation(summary = "web/app-获取考勤本月统计") + @PostMapping("/monthStatistics") + public ActionResult> getMonthStatistics(@Valid @RequestBody BookRecordMonthStatisticsDto req) { + try { + log.info("获取考勤本月统计,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + + PageInfo pageInfo = attendanceBookRecordService.getMonthStatistics(req); + + long ent = System.currentTimeMillis(); + // 防止pageInfo或list为null + List resultList = pageInfo.getList() != null ? pageInfo.getList() : new ArrayList<>(); + log.info("获取考勤本月统计,耗时=>{} 毫秒,返回记录数: {}", ent - st, resultList.size()); + return ActionResult.page(resultList, FtbUtil.getPagination(pageInfo)); + } catch (Exception e) { + log.error("获取考勤本月统计异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * web-考勤本导入 + * + * @param importDto 导入参数(包含bookId、month、fileUrl) + * @return 操作结果 + */ + @Operation(summary = "web-考勤本导入") + @PostMapping("/import") + public ActionResult bookRecordImport(@RequestBody SchedulesImportDto importDto) { + try { + log.info("考勤本导入,入参=>{}", JSONObject.toJSON(importDto)); + attendanceBookRecordService.bookRecordImport(importDto); + return ActionResult.success("导入成功"); + } catch (Exception e) { + log.error("考勤本导入异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * web-考勤本导出 + * + * @param bookId 考勤本ID + * @param month 月份(格式:yyyy-MM) + */ + @Operation(summary = "web-考勤本导出") + @GetMapping("/export") + public ActionResult bookRecordExport(@RequestParam String bookId, @RequestParam String month) { + try { + log.info("考勤本导出,bookId=>{}, month=>{}", bookId, month); + attendanceBookRecordService.bookRecordExport(bookId, month); + } catch (Exception e) { + log.error("考勤本导出异常", e); + throw new RuntimeException("导出失败:" + e.getMessage()); + } + return ActionResult.success(); + } + + /** + * web-导出考勤本模板(直接流导出) + *

返回的Excel文件与导出接口具有相同的表头结构,但不包含任何实际数据记录

+ * + * @param bookId 考勤本ID + * @param month 月份(格式:yyyy-MM) + * @param response HTTP响应对象 + */ + @Operation(summary = "web-导出考勤本模板") + @GetMapping("/export-template") + public void exportTemplate(@RequestParam String bookId, @RequestParam String month, javax.servlet.http.HttpServletResponse response) { + try { + log.info("导出考勤本模板,bookId=>{}, month=>{}", bookId, month); + attendanceBookRecordService.exportTemplate(bookId, month, response); + } catch (Exception e) { + log.error("导出考勤本模板异常", e); + throw new RuntimeException("导出模板失败:" + e.getMessage()); + } + } + + /** + * app-获取日考勤本列表 + *

根据考勤本的使用范围获取人员列表,展示指定日期的考勤记录

+ * + * @param req 查询参数(考勤本ID、日期) + * @return 日考勤本列表(按员工分组) + */ + @Operation(summary = "app-获取日考勤本列表") + @PostMapping("/day-list") + public ActionResult> getDayList(@Valid @RequestBody BookRecordDayListDto req) { + try { + log.info("获取日考勤本列表,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + + List result = attendanceBookRecordService.getDayList(req); + + long ent = System.currentTimeMillis(); + log.info("获取日考勤本列表,耗时=>{} 毫秒,返回记录数: {}", ent - st, result.size()); + return ActionResult.success(result); + } catch (Exception e) { + log.error("获取日考勤本列表异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app-获取月考勤本列表 + *

根据考勤本的使用范围获取人员列表,展示该月所有日期的考勤记录

+ * + * @param req 查询参数(考勤本ID、月份) + * @return 月考勤本列表(按员工分组) + */ + @Operation(summary = "app-获取月考勤本列表") + @PostMapping("/month-list") + public ActionResult> getMonthList(@Valid @RequestBody BookRecordMonthListDto req) { + try { + log.info("获取月考勤本列表,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + + List result = attendanceBookRecordService.getMonthList(req); + + long ent = System.currentTimeMillis(); + log.info("获取月考勤本列表,耗时=>{} 毫秒,返回记录数: {}", ent - st, result.size()); + return ActionResult.success(result); + } catch (Exception e) { + log.error("获取月考勤本列表异常", e); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 需求10:批量新增或修改考勤记录 + * + * @param dto 批量考勤记录DTO + * @return 批量操作结果 + */ + @Operation(summary = "app-批量新增或修改考勤记录") + @PostMapping("/batch-save-or-update") + public ActionResult batchSaveOrUpdate(@Valid @RequestBody BatchAttendanceBookRecordDto dto) { + try { + log.info("批量新增或修改考勤记录,记录数=>{}", dto.getRecords().size()); + long st = System.currentTimeMillis(); + + BatchOperationResultVo result = attendanceBookRecordService.batchSaveOrUpdateRecord(dto); + + long ent = System.currentTimeMillis(); + log.info("批量新增或修改考勤记录,耗时=>{} 毫秒,成功: {}, 失败: {}", + ent - st, result.getSuccessCount(), result.getFailCount()); + return ActionResult.success(result); + } catch (Exception e) { + log.error("批量新增或修改考勤记录异常", e); + return ActionResult.fail("系统异常,请联系管理员"); + } + } + + /** + * 获取假勤汇总信息 + * + * @param dto 统计请求参数 + * @return 假勤汇总信息 + */ + @Operation(summary = "app-获取假勤汇总信息") + @PostMapping("/leave-attendance-summary") + public ActionResult getLeaveAttendanceSummary(@Valid @RequestBody LeaveRemarkSummaryDto dto) { + try { + log.info("获取假勤汇总信息,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + + LeaveRemarkSummaryVo result = attendanceBookRecordService.getLeaveAttendanceSummary(dto); + + long ent = System.currentTimeMillis(); + log.info("获取假勤汇总信息,耗时=>{} 毫秒,备注数: {}", ent - st, result.getTotalCount()); + return ActionResult.success(result); + } catch (Exception e) { + log.error("获取假勤汇总信息异常", e); + return ActionResult.fail(e.getMessage()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceChangeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceChangeController.java new file mode 100644 index 0000000..ac4fe42 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceChangeController.java @@ -0,0 +1,112 @@ +package jnpf.attendance.controller; + +import jnpf.attendance.service.AttendanceChangeService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.attendance.dto.NoApprovalDto; +import jnpf.model.attendance.vo.ChangeInfoVo; +import jnpf.util.UserProvider; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import javax.validation.Valid; + +/** + * 考勤变更控制器 + * + * @author yanwenfu + * @create 2024-11-08 + */ +@RestController +@RequestMapping(value = "/change") +public class AttendanceChangeController { + + @Resource + private AttendanceChangeService attendanceChangeService; + + /** + * 被变更记录的信息 + * @param clockInResultId 结果id + * @return java.lang.Object + */ + @GetMapping(value = "/info/{clockInResultId}") + public Object getChangeInfo(@PathVariable(value = "clockInResultId") String clockInResultId) throws Exception { + + ChangeInfoVo info = attendanceChangeService.getChangeInfo(clockInResultId); + return ActionResult.success(info); + } + + /** + * 根据考勤组id及用户id查询过去一年内的变更涉及打卡记录 + * @param groupId 考勤组id + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/getChangeInfoList") + public ActionResult> getChangeInfoList(@RequestParam String groupId,@RequestParam String userId) { + List infos = attendanceChangeService.getChangeInfoList(groupId, userId); + return ActionResult.success(infos); + } + + /** + * 出勤变更(无需审批) + * @param noApprovalDto 出勤变更参数 + * @return java.lang.Object + */ + @PostMapping(value = "/noApproval") + public Object attendanceChangeNoApproval(@RequestBody @Valid NoApprovalDto noApprovalDto) throws Exception { + + attendanceChangeService.attendanceChangeNoApproval(noApprovalDto); + + return ActionResult.success(); + } + + /** + * 出勤变更(需审批) + * @param taskId 审批id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + * @return java.lang.Object + */ + @GetMapping(value = "/approval") + public Object attendanceChangeApproval(@RequestParam(value = "taskId") String taskId, @RequestParam(value = "passed") String passed) { + + UserInfo user = UserProvider.getUser(); + ActionResult actionResult = new ActionResult<>(); + try { + attendanceChangeService.attendanceChangeApproval(taskId, passed, user); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 出勤变更撤回 + * @param clockInResultId 打卡结果id + * @return java.lang.Object + */ + @PutMapping(value = "/rollback/{clockInResultId}") + public Object rollbackChange(@PathVariable(value = "clockInResultId") String clockInResultId) throws Exception { + + attendanceChangeService.rollbackChange(clockInResultId); + return ActionResult.success(); + } + + /** + * 查询能否撤回变更 + * @param clockInResultId 打卡结果id + * @return java.lang.Object + */ + @GetMapping(value = "/rollback/status") + public Object getRollbackStatus(@RequestParam(value = "clockInResultId") String clockInResultId) { + + Integer status = attendanceChangeService.getRollbackStatus(clockInResultId); + return ActionResult.success(status); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInController.java new file mode 100644 index 0000000..8acc323 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInController.java @@ -0,0 +1,385 @@ +package jnpf.attendance.controller; + +import jnpf.aspect.FtbApiCallLog; +import jnpf.attendance.FtbClockInApi; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.DailyRuleChangeService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.ClockInDto; +import jnpf.model.attendance.dto.NoApprovalDto; +import jnpf.model.attendance.vo.*; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 打卡控制器 + * + * @author yanwenfu + * @create 2023-11-21 + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/clockIn") +public class AttendanceClockInController implements FtbClockInApi { + + @Resource + private AttendanceClockInService attendanceClockInService; + + @Resource + private UserProvider userProvider; + + @Resource + private CustomTenantUtil customTenantUtil; + + @Resource + private DailyRuleChangeService dailyRuleChangeService; + + /** + * 打卡 - 主页 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/mainInfo") + public ActionResult> getClockInMainInfo(@RequestParam(required = false) String day) throws Exception { + + try { + Date date = StringUtils.isEmpty(day) ? new Date() : DateDetail.getStr2Date10(day); + List list = attendanceClockInService.getClockInMainInfo(date, userProvider.get(), ConstantUtil.NUM_TRUE); + return ActionResult.success(list); + } catch (QueryException e) { + if (e.getMessage().equals("当前用户暂无考勤组")) { + return ActionResult.success(e.getMessage()); + } else { + throw new QueryException(e.getMessage()); + } + } + } + + /** + * 查询考勤组审批列表 + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/group-approval/list") + public ActionResult> getGroupApprovalList(@RequestBody ApprovalQueryDto queryDto) { + + List list = attendanceClockInService.getGroupApprovalList(queryDto); + return ActionResult.success(list); + } + + /** + * 打卡 + * @return java.lang.Object + */ + @FtbApiCallLog(name = "考勤打卡") + @PostMapping(value = "/record") + public Object clockIn(@RequestBody @Valid ClockInDto clockInDto) throws Exception { + + MutablePair pair = attendanceClockInService.clockIn(clockInDto); + return ActionResult.success(pair.getLeft()); + } + + /** + * 更新打卡记录 + * @param clockInId 打卡id + * @return java.lang.Object + */ + @PutMapping(value = "/record/{clockInId}") + public Object updateClockIn(@PathVariable(value = "clockInId") String clockInId, @RequestBody @Valid ClockInDto clockInDto) throws Exception { + + MutablePair pair = attendanceClockInService.updateClockIn(clockInId, clockInDto); + return ActionResult.success(pair.getLeft()); + } + + @PostMapping(value = "/testChangeRule") + public void changeAttendanceRule(@RequestBody List userDayList) { + + UserInfo userInfo = userProvider.get(); + attendanceClockInService.changeAttendanceRuleBatch(userDayList, userInfo); + } + + /** + * 外勤打卡 + * @param applyId 审批id + */ + @GetMapping(value = "/outside") + public Object outsideClockIn(@RequestParam(value = "taskId") String applyId, @RequestParam(value = "clockInId", required = false) String clockInId) throws Exception { + + UserInfo user = userProvider.get(); + ActionResult actionResult = new ActionResult<>(); + try { + attendanceClockInService.outsideClockIn(applyId, user.getUserId(), clockInId); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 外勤打卡审批(通过/不通过/撤回) + * @param applyId 审批id + * @param passed - 是否通过(0: 否, 1: 是, 2: 撤回)
+ */ + @GetMapping(value = "/outside/approval") + public Object approvalOutsideClockIn(@RequestParam(value = "taskId") String applyId, @RequestParam(value = "passed") String passed) throws Exception { + + UserInfo userInfo = userProvider.get(); + ActionResult actionResult = new ActionResult<>(); + try { + attendanceClockInService.approvalOutsideClockIn(applyId, passed, userInfo); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 异常打卡审批(通过/不通过/撤回) + * @param applyId 审批id + * @param passed - 是否通过(0: 否, 1: 是, 2: 撤回)
+ */ + @GetMapping(value = "/unusual-phone/approval") + public Object unusualPhoneClockIn(@RequestParam(value = "taskId") String applyId, @RequestParam(value = "passed") String passed) throws Exception { + + UserInfo userInfo = userProvider.get(); + ActionResult actionResult = new ActionResult<>(); + try { + attendanceClockInService.approvalUnusualPhoneClockIn(applyId, passed, userInfo); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 生成用户补卡次数记录 + * @param tenantId 租户id + * @param groupId 考勤组id + * @param userId 用id + * @param type 生成类型(1: 新增组成员, 2: 借调到新组) + * @return java.lang.Object + */ + @GetMapping(value = "/generateRepairNum/{tenantId}") + @NoDataSourceBind + public Object generateRepairNumForUser(@PathVariable(value = "tenantId") String tenantId, @RequestParam String groupId, @RequestParam String userId, @RequestParam Integer type) { + + customTenantUtil.checkOutTenant(tenantId); + boolean b = attendanceClockInService.generateRepairNumForUser(groupId, userId, type); + return ActionResult.success(b); + } + + /** + * 扫描是否有未执行的班次变更任务 + * @return java.lang.Object + */ + @GetMapping(value = "/daily-rule-change/execute") + public Boolean dailyRuleChangeExecute() { + + return dailyRuleChangeService.dailyRuleChangeExecute(); + } + + /** + * 补卡 + * @param applyId 审批id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + */ + @GetMapping(value = "/repair") + public ActionResult repairClockIn(@RequestParam(value = "taskId") String applyId, @RequestParam(value = "passed") String passed) { + + log.error("补卡入参 -> applyId: {}, passed: {}", applyId, passed); + UserInfo userInfo = userProvider.get(); + String approveUserId = userInfo.getUserId(); + ActionResult actionResult = new ActionResult<>(); + try { + attendanceClockInService.repairClockIn(applyId, passed, approveUserId, userInfo.getTenantId()); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 出勤变更 + * @param tenantId 租户id + * @param applyId 申请id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + */ + @GetMapping(value = "/change") + @NoDataSourceBind + public void attendanceChange(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "applyId") String applyId, + @RequestParam(value = "passed") String passed, @RequestParam(value = "approveUserId") String approveUserId) throws Exception { + + log.error("出勤变更 -> tenantId: {}, applyId: {}, passed: {}", tenantId, applyId, passed); + customTenantUtil.checkOutTenant(tenantId, approveUserId); + attendanceClockInService.attendanceChange(applyId, passed, approveUserId,tenantId); + } + + /** + * 出勤变更(不审批) + * @param noApprovalDto 出勤变更无需审批dto + * @return java.lang.Object + */ + @PutMapping(value = "/change/noApproval") + public Object attendanceChangeNoApproval(@RequestBody @Valid NoApprovalDto noApprovalDto) throws Exception { + + attendanceClockInService.attendanceChangeNoApproval(noApprovalDto.getClockInResultId(), noApprovalDto.getChangeType()); + return ActionResult.success(); + } + + /** + * 可选择的补卡列表 + * @param userId 用户id + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/repair/list/{userId}") + public ActionResult> getRepairList(@PathVariable(value = "userId") String userId) throws Exception { + + List list = attendanceClockInService.getRepairList(userId); + return ActionResult.success(list); + } + + /** + * 执行打卡旷工逻辑 + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/absenceRecord") + public Boolean generateFtbAbsenceRecord(@RequestParam(value = "tenantId") String tenantId) { + return attendanceClockInService.generateFtbAbsenceRecord(tenantId); + } + + /** + * 生成打卡旷工任务 + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/absenceTask") + public Boolean generateAbsenceTask(@RequestParam(value = "tenantId") String tenantId) { + return attendanceClockInService.generateAbsenceTask(ConstantUtil.NUM_TRUE, tenantId); + } + + /** + * 生成打卡提醒记录 + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/remindRecord") + public Boolean generateBeforeWorkRemind(@RequestParam(value = "tenantId") String tenantId) { + return attendanceClockInService.generateBeforeWorkRemind(tenantId); + } + + /** + * 生成补卡次数 + * @param tenantId 租户id + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/repairNum") + public Boolean generateRepairNum(@RequestParam(value = "tenantId") String tenantId) { + return attendanceClockInService.generateRepairNum(); + } + + /** + * 判定连续动作(排班/旷工) + * @param tenantId 租户id + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/continuousCheck") + public Boolean continuousCheck(@RequestParam(value = "tenantId") String tenantId) { + return attendanceClockInService.continuousCheck(tenantId); + } + + /** + * 临时调用 + * @return java.lang.Boolean + */ + @NoDataSourceBind + @PostMapping(value = "/repairNum/all") + public Boolean generateRepairNumAll(@RequestParam String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return attendanceClockInService.generateRepairNumAll(); + } + + /** + * 临时调用 + */ + @PostMapping(value = "/test/{tenantId}") + public void test(@PathVariable(value = "tenantId") String tenantId) { + + attendanceClockInService.generateFtbAbsenceRecord(tenantId); + } + + /** + * 查询每日出勤及打卡记录 + * @param userId 用户id + * @param queryDate 查询日期 + * @param currentGroupId 当前考勤组 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/dailyClockInRecord") + public ActionResult> getDailyClockInRecord(@RequestParam(value = "userId") String userId, + @RequestParam(value = "queryDate") String queryDate, + @RequestParam(value = "currentGroupId") String currentGroupId) { + + List list = attendanceClockInService.getDailyClockInRecord(userId, queryDate, currentGroupId); + return ActionResult.success(list); + } + + /** + * 查询每日出勤及打卡记录 - v2 + * @param userId 用户id + * @param queryDate 查询日期 + * @param currentGroupId 当前考勤组 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/dailyClockInRecord/v2") + public ActionResult getDailyClockInRecordV2(@RequestParam(value = "userId") String userId, + @RequestParam(value = "queryDate") String queryDate, + @RequestParam(value = "currentGroupId") String currentGroupId, + @RequestParam(value = "queryOldData", required = false) Integer queryOldData) throws QueryException { + + queryOldData = null == queryOldData ? 0 : queryOldData; + DailyInfoVo dailyInfoVo = attendanceClockInService.getDailyClockInRecordV2(userId, queryDate, currentGroupId, queryOldData); + return ActionResult.success(dailyInfoVo); + } + + /** + * 查询考勤组补卡规则 + * @param groupId 考勤组id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/clockInRepair/rule/{groupId}") + public ActionResult getClockInRepairRule(@PathVariable(value = "groupId") String groupId) { + + RepairRuleVo rule = attendanceClockInService.getClockInRepairCheck(groupId); + return ActionResult.success(rule); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInPicController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInPicController.java new file mode 100644 index 0000000..038efca --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceClockInPicController.java @@ -0,0 +1,42 @@ +package jnpf.attendance.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.service.AttendanceClockInPicService; +import jnpf.base.ActionResult; +import jnpf.entity.attendance.AttendanceClockInPic; +import jnpf.model.attendance.vo.ClockInPicVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 打卡图片控制器 + * + * @author shitou + * @create 2024-10-30 + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/clockIn/pic") +public class AttendanceClockInPicController { + + @Resource + private AttendanceClockInPicService attendanceClockInPicService; + + @Operation(summary = "查询外勤打卡图片列表") + @GetMapping(value = "/list/{clockInId}") + public ActionResult> getClockInPicList(@Valid @PathVariable("clockInId") String clockInId) { + log.info("查询外勤打卡图片列表,入参=>{}", clockInId); + long st = System.currentTimeMillis(); + List clockInPicList = attendanceClockInPicService.getClockInPicList(clockInId); + long ent = System.currentTimeMillis(); + log.info("查询外勤打卡图片列表,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(clockInPicList); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCloudAlbumController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCloudAlbumController.java new file mode 100644 index 0000000..843a726 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCloudAlbumController.java @@ -0,0 +1,61 @@ +package jnpf.attendance.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.service.AttendanceCloudAlbumService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.CloudAlbumBatchSaveDto; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 考勤云相册 + * + * @Description: 考勤云相册控制类 + * @Author: shiTou(他是小石头) + * @Date: 2024-10-30 10:26 + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/cloudAlbum") +public class AttendanceCloudAlbumController { + @Resource + private AttendanceCloudAlbumService attendanceCloudAlbumService; + + @SneakyThrows + @Operation(summary = "列表查询") + @GetMapping("/getDataList") + public ActionResult> getDataList() { + long st = System.currentTimeMillis(); + List dataList = attendanceCloudAlbumService.getDataList(); + long ent = System.currentTimeMillis(); + log.info("列表查询,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(dataList); + } + + @SneakyThrows + @Operation(summary = "批量保存图片") + @PostMapping("/batchSave") + public ActionResult batchSave(@Valid @RequestBody CloudAlbumBatchSaveDto dto) { + long st = System.currentTimeMillis(); + attendanceCloudAlbumService.batchSave(dto.getPicUrlList()); + long ent = System.currentTimeMillis(); + log.info("批量保存图片,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(); + } + + @SneakyThrows + @Operation(summary = "获取登录用户水印") + @GetMapping("/getUserWatermark") + public ActionResult getUserWatermark() { + long st = System.currentTimeMillis(); + String watermark = attendanceCloudAlbumService.getUserWatermark(); + long ent = System.currentTimeMillis(); + log.info("获取登录用户水印,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success("获取成功",watermark); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceConfirmController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceConfirmController.java new file mode 100644 index 0000000..42c3aa1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceConfirmController.java @@ -0,0 +1,192 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.AttendanceConfirmApi; +import jnpf.attendance.service.AttendanceConfirmService; +import jnpf.attendance.service.AttendanceConfirmSettingService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.ConfirmPageListDto; +import jnpf.model.attendance.dto.ConfirmSettingSubmitDto; +import jnpf.model.attendance.dto.ConfirmStatisticsDto; +import jnpf.model.attendance.vo.attendance.ConfirmDetailsVo; +import jnpf.model.attendance.vo.attendance.ConfirmPageListVo; +import jnpf.model.attendance.vo.attendance.ConfirmSettingInfoVo; +import jnpf.model.attendance.vo.attendance.ConfirmStatisticsVo; +import jnpf.model.thousandsfaces.TodayWorkVo; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 考勤确认 + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/confirm") +public class AttendanceConfirmController implements AttendanceConfirmApi { + @Resource + private AttendanceConfirmService attendanceConfirmService; + @Resource + private AttendanceConfirmSettingService confirmSettingService; + + @SneakyThrows + @Operation(summary = "获取考勤确认设置") + @GetMapping(value = "/getConfirmSetting") + public ActionResult getConfirmSetting() { + long st = System.currentTimeMillis(); + ConfirmSettingInfoVo result = confirmSettingService.getConfirmSetting(Boolean.TRUE); + long ent = System.currentTimeMillis(); + log.info("获取考勤确认设置,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @SneakyThrows + @Operation(summary = "考勤确认设置提交") + @PostMapping(value = "/confirmSettingSubmit") + public ActionResult confirmSettingSubmit(@Valid @RequestBody ConfirmSettingSubmitDto dto) { + log.info("考勤确认设置提交,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + Boolean result = confirmSettingService.confirmSettingSubmit(dto); + long ent = System.currentTimeMillis(); + log.info("考勤确认设置提交,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @SneakyThrows + @Operation(summary = "获取考勤月份") + @GetMapping(value = "/getConfirmMonth") + public ActionResult getConfirmMonth() { + long st = System.currentTimeMillis(); + String result = attendanceConfirmService.getConfirmMonth(); + long ent = System.currentTimeMillis(); + log.info("获取考勤月份,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success("获取成功", result); + } + + @SneakyThrows + @Operation(summary = "考勤确认聚合统计") + @PostMapping(value = "/getStatistics") + public ActionResult getStatistics(@Valid @RequestBody ConfirmStatisticsDto dto) { + log.info("考勤确认聚合统计,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + ConfirmStatisticsVo result = attendanceConfirmService.getStatistics(dto); + long ent = System.currentTimeMillis(); + log.info("考勤确认聚合统计,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @SneakyThrows + @Operation(summary = "考勤确认列表展示") + @PostMapping(value = "/getPageList") + public ActionResult> getPageList(@Valid @RequestBody ConfirmPageListDto dto) { + log.info("考勤确认列表展示,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + PageInfo page = attendanceConfirmService.getPageList(dto); + long ent = System.currentTimeMillis(); + log.info("考勤确认列表展示,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @SneakyThrows + @Operation(summary = "获取考勤确认详情") + @GetMapping(value = "/getConfirmDetails/{id}") + public ActionResult getConfirmDetails(@PathVariable("id") String id) { + log.info("获取考勤确认详情,入参=>{}", id); + long st = System.currentTimeMillis(); + ConfirmDetailsVo result = attendanceConfirmService.getConfirmDetails(id); + long ent = System.currentTimeMillis(); + log.info("获取考勤确认详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @Override + @SneakyThrows + @Operation(summary = "自动生成考勤确认数据") + @GetMapping(value = "/autoConfirm") + public ActionResult autoCreateConfirm() { + long st = System.currentTimeMillis(); + boolean result = attendanceConfirmService.autoCreateConfirm(); + long ent = System.currentTimeMillis(); + log.info("自动生成考勤确认数据,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @Override + @SneakyThrows + @Operation(summary = "逾期自动确认") + @GetMapping(value = "/confirmAutoSlippage") + public ActionResult confirmAutoSlippage() { + long st = System.currentTimeMillis(); + boolean result = attendanceConfirmService.confirmAutoSlippage(); + long ent = System.currentTimeMillis(); + log.info("逾期自动确认,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @Override + @Operation(summary = "今日工作-考勤确认列表(0-待确认 1-已确认 2-已逾期)") + @GetMapping(value = "/getTodayWorkConfirmList") + public List getTodayWorkConfirmList() { + long st = System.currentTimeMillis(); + List result = attendanceConfirmService.getTodayWorkConfirmList(); + long ent = System.currentTimeMillis(); + log.info("获取App考勤确认详情,耗时=>{}", ent - st + " 毫秒"); + return result; + } + + @SneakyThrows + @Operation(summary = "获取App考勤确认详情") + @GetMapping(value = "/getAppConfirmDetails") + public ActionResult getAppConfirmDetails(@RequestParam(value = "id", required = false) String id) { + long st = System.currentTimeMillis(); + ConfirmDetailsVo result = attendanceConfirmService.getAppConfirmDetails(id); + long ent = System.currentTimeMillis(); + log.info("获取App考勤确认详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @SneakyThrows + @Operation(summary = "App温馨提示关闭") + @GetMapping(value = "/tipsClos/{id}") + public ActionResult tipsClos(@PathVariable("id") String id) { + log.info("温馨提示关闭,入参=>{}", id); + long st = System.currentTimeMillis(); + Boolean result = attendanceConfirmService.tipsClos(id); + long ent = System.currentTimeMillis(); + log.info("温馨提示关闭,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @SneakyThrows + @Operation(summary = "App已查看") + @GetMapping(value = "/look/{id}") + public ActionResult look(@PathVariable("id") String id) { + log.info("App已查看,入参=>{}", id); + long st = System.currentTimeMillis(); + Boolean result = attendanceConfirmService.look(id); + long ent = System.currentTimeMillis(); + log.info("App已查看,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } + + @SneakyThrows + @Operation(summary = "App考勤确认提交") + @GetMapping(value = "/confirmDetailsSubmit/{id}") + public ActionResult confirmDetailsSubmit(@PathVariable("id") String id) { + log.info("App考勤确认提交,入参=>{}", id); + long st = System.currentTimeMillis(); + Boolean result = attendanceConfirmService.confirmDetailsSubmit(id); + long ent = System.currentTimeMillis(); + log.info("App考勤确认提交,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(result); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCustomizeTableController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCustomizeTableController.java new file mode 100644 index 0000000..79bc11f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceCustomizeTableController.java @@ -0,0 +1,43 @@ +package jnpf.attendance.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.service.AttendanceCustomizeTableService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.vo.attendance.AttendanceCustomizeTableVo; +import jnpf.model.attendance.vo.attendance.CustomizeTableUpdateVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * 考勤统计WEB + * + * @author ahua + * @since 2024-09-03 + */ +@RestController +@RequestMapping("/attendance/customizeTable") +public class AttendanceCustomizeTableController { + @Autowired + private AttendanceCustomizeTableService tableService; + + @GetMapping + @Operation(summary = "自定义报表设置-列表查询") + public ActionResult> findList(@RequestParam(required = false) String keyword, + @RequestParam(required = false) Integer status, + @RequestParam(required = false, defaultValue = "1") Integer type) { + return ActionResult.success(tableService.findList(keyword, status, type)); + } + + @PutMapping + @Operation(summary = "自定义报表设置-设置") + public ActionResult update(@Valid @RequestBody CustomizeTableUpdateVo tableVos) { + tableService.update(tableVos); + return ActionResult.success(); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceDailyRuleController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceDailyRuleController.java new file mode 100644 index 0000000..c27de3f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceDailyRuleController.java @@ -0,0 +1,399 @@ +package jnpf.attendance.controller; + + +import cn.hutool.core.collection.CollUtil; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.AttendanceDailyRuleApi; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.AttendanceLeaveRulesService; +import jnpf.base.ActionResult; +import jnpf.entity.attendance.ApplyParam; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.v2.ClockOutHandleParam; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.LineDrawingSchedulesConfigDto; +import jnpf.model.attendance.dto.SchedulesImportDto; +import jnpf.model.attendance.dto.SchedulesSetDto; +import jnpf.model.attendance.dto.UnifiedSchedulesDto; +import jnpf.model.attendance.dto.UserDayShiftQueryDto; +import jnpf.model.attendance.vo.DayReceivableRevenueVo; +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import jnpf.model.attendance.vo.ScheduleRuleDetailVo; +import jnpf.model.attendance.vo.SchedulesV2Vo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleDetailVo; +import jnpf.model.attendance.vo.LineSchedulesVo; +import jnpf.model.attendance.vo.UserDayShiftInfoVo; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * 排班规则 + * + * @author ahua + * @since 2023-11-22 + */ +@RestController +@RequestMapping("/arranging/work") +@Slf4j +public class AttendanceDailyRuleController implements AttendanceDailyRuleApi { + @Resource + private CustomTenantUtil customTenantUtil; + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceLeaveRulesService attendanceLeaveRulesService; + + /** + * 根据指定条件查询某个群体在特定月份的调度情况 + * + * @param groupId 群体ID,用于标识特定的群体 + * @param realName 真实姓名,用于筛选特定的个人 + * @param month 月份,格式为YYYY-MM,用于指定查询的时间范围 + * @param userIds 用户ID列表,用于限定查询的用户范围 + * @param isSchedules 是否仅查询已调度的记录,true表示仅查询已调度记录,false表示查询所有记录 + * @return 返回根据条件查询到的调度情况结果 + */ + @GetMapping(value = "/schedulesExport") + public ActionResult schedulesExport(@RequestParam String groupId, @RequestParam(required = false) String workGroupId, @RequestParam(required = false) String realName, @RequestParam String month, @RequestParam(required = false) List userIds, @RequestParam(required = false, defaultValue = "1") Integer isSchedules) { + attendanceDailyRuleService.schedulesExport(groupId, workGroupId, realName, month, userIds, isSchedules); + return ActionResult.success(); + } + + /** + * 划线排班导出 + * + * @param groupId 考勤组id + * @param workGroupId 班组ID + * @param month 月份 + * @param userIdList 用户ID列表 + * @return + */ + @GetMapping(value = "/lineSchedulesExport") + public ActionResult lineSchedulesExport(@RequestParam String groupId, @RequestParam(required = false) String workGroupId, @RequestParam String month, @RequestParam(required = false) List userIdList) { + attendanceDailyRuleService.lineSchedulesExport(groupId, workGroupId, month, userIdList); + return ActionResult.success(); + } + + /** + * 排班导入 + * + * @param schedulesImportDto 参数 + * @return + */ + @PostMapping(value = "/schedulesImport") + public ActionResult> schedulesImport(@RequestBody SchedulesImportDto schedulesImportDto) throws IOException { + return ActionResult.success(attendanceDailyRuleService.schedulesImport(schedulesImportDto)); + } + + /** + * 划线排班导入 + * + * @param schedulesImportDto 参数 + * @return + */ + @PostMapping(value = "/lineSchedulesImport") + public ActionResult lineSchedulesImport(@RequestBody SchedulesImportDto schedulesImportDto) throws IOException { + attendanceDailyRuleService.lineSchedulesImport(schedulesImportDto); + return ActionResult.success(); + } + + /** + * 晚走晚到 + * + * @return + */ + @GetMapping(value = "/clockOutHandle") + public ActionResult clockOutHandle(@RequestParam String userId, @RequestParam String tenantId, @RequestParam String clockOut, @RequestParam String clockInTime) { + ClockOutHandleParam param = new ClockOutHandleParam(); + param.setUserId(userId); + param.setTenantId(tenantId); + param.setAttendanceType(AttendanceTypeEnum.ORDINARY.getCode()); + param.setStart(DateUtil.stringToDate(clockOut)); + param.setEnd(DateUtil.stringToDate(clockInTime)); + param.setApplyId(""); + param.setRuleId(""); + param.setDay(new Date()); + OvertimeRuleDetailVo overtimeRuleDetailVo = new OvertimeRuleDetailVo(); + overtimeRuleDetailVo.setOvertimeType(1); + overtimeRuleDetailVo.setMinOvertimeMinute(30); + overtimeRuleDetailVo.setEnabled(1); + overtimeRuleDetailVo.setCalcMethod(3); + overtimeRuleDetailVo.setTimeUnit(1); + overtimeRuleDetailVo.setCompensateRatio(BigDecimal.ONE); + overtimeRuleDetailVo.setCompensateType(1); + param.setRuleDetail(overtimeRuleDetailVo); + attendanceDailyRuleService.workOvertimeNotApprove(param); + return ActionResult.success(); + } + + /** + * 获取排班列表V2版本。 + * + * @param groupId 考勤组id + * @param workGroupId 班组ID + * @param realName 真实姓名(可选) + * @param month 月份 + * @param userIds 用户ID列表(可选) + * @param isSchedules 是否排班标识(可选,默认值为1) + * @return 排班列表V2版本视图对象 + */ + @GetMapping("v2") + public ActionResult> getSchedulesListV2(@RequestParam String groupId, @RequestParam(required = false) String workGroupId, @RequestParam(required = false) String realName, @RequestParam String month, @RequestParam(required = false) List userIds, @RequestParam(required = false, defaultValue = "1") Integer isSchedules) { + return ActionResult.success(attendanceDailyRuleService.getSchedulesListV2(groupId, workGroupId, realName, month, userIds, isSchedules)); + } + + /** + * 获取排班列表 V2(按开始日期、结束日期,其它参数与 v2 一致) + */ + @GetMapping("v2/range") + public ActionResult> getSchedulesListV2ByDateRange(@RequestParam String groupId, @RequestParam(required = false) String workGroupId, @RequestParam(required = false) String realName, @RequestParam String startDate, @RequestParam String endDate, @RequestParam(required = false) List userIds, @RequestParam(required = false, defaultValue = "1") Integer isSchedules) { + return ActionResult.success(attendanceDailyRuleService.getSchedulesListV2ByDateRange(groupId, workGroupId, realName, startDate, endDate, userIds, isSchedules)); + } + + /** + * 预排班草稿转排班列表 V2(按员工 + 日期区间);草稿 Redis 不存在时返回空列表。 + */ + @GetMapping("v2/preSchedule/draft") + public ActionResult> getSchedulesListV2ByPreScheduleDraft( + @RequestParam String groupId, + @RequestParam String startDate, + @RequestParam String endDate, + @RequestParam String draftId) { + return ActionResult.success( + attendanceDailyRuleService.getSchedulesListV2ByPreScheduleDraft( + groupId, startDate, endDate, draftId)); + } + + /** + * 根据ID获取排班规则详情。 + * + * @param id 排班规则ID + * @return 排班规则详情视图对象 + * @throws HandleException 处理异常 + */ + @GetMapping("{id}") + public ActionResult getSchedulesList(@PathVariable String id) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.getDetail(id)); + } + + /** + * 设置排班。 + * + * @param schedulesSets 排班设置列表 + * @return 处理结果字符串 + * @throws HandleException 处理异常 + */ + @PostMapping + public ActionResult setSchedules(@RequestBody List schedulesSets) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.setSchedules(schedulesSets)); + } + + /** + * 自己排班调整排班 + * + * @param shiftId 班次id + * @return + */ + @GetMapping("/selfSchedules") + public ActionResult setSchedulesForSelfSchedules(@RequestParam("shiftId") String shiftId) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.setSchedulesForSelfSchedules(shiftId)); + } + + /** + * 定时执行初始化下个月固定排班。 + */ + @PostMapping("initFixedScheduleRule") + public ActionResult initFixedScheduleRule(@RequestParam("tenantId") String tenantId) { + attendanceDailyRuleService.initFixedScheduleRule(tenantId); + return ActionResult.success(); + } + + /** + * 处理日常规则申请。 + * + * @param applyParam 申请参数类 + * @return 处理结果字符串 + * @throws HandleException 处理异常 + */ + @PostMapping("applyDailyRuleHandle") + public ActionResult applyDailyRuleHandle(@RequestBody ApplyParam applyParam) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.applyDailyRuleHandle(applyParam)); + } + + /** + * 处理借调申请日规则。 + * + * @param userId 借调用户ID + * @param fromGroupId 原考勤组ID + * @param toGroupId 借调考勤组ID + * @param start 开始时间 + * @param end 结束时间 + * @param departureTime 离岗时间 + * @param backTime 回岗时间 + * @return 处理结果视图对象 + * @throws HandleException 处理异常 + */ + @PostMapping("secondmentDailyRuleHandle") + public ActionResult secondmentDailyRuleHandle(@RequestParam String userId, @RequestParam String fromGroupId, @RequestParam String toGroupId, @RequestParam String start, @RequestParam String end, @RequestParam String departureTime, @RequestParam String backTime) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.secondmentDailyRuleHandle(CollUtil.newArrayList(userId), fromGroupId, toGroupId, DateDetail.getStr2DateTime(start), DateDetail.getStr2DateTime(end), DateDetail.getStr2DateTime(departureTime), DateDetail.getStr2DateTime(backTime), "yawentest2")); + } + + /** + * 自动授予假期余额。 + * + * @param tenantId 租户ID + * @return 处理结果视图对象 + */ + @PostMapping("autoGrantBalance") + public ActionResult autoGrantBalance(@RequestParam("tenantId") String tenantId) { + // 2.0 节日不发劵了 +// attendanceFestivalSettingService.autoGrantBalance(); + // 需要修改 +// attendanceHolidaySettingService.autoGrantBalance(tenantId); + attendanceLeaveRulesService.autoGrantBalance(tenantId); + + return ActionResult.success(); + } + + /** + * 用户是否排班 + * + * @param tenantId 租户ID + * @param userId 用户ID + * @return + */ + @Override + @NoDataSourceBind + @GetMapping("userIsScheduling") + public ActionResult userIsScheduling(@RequestParam("tenantId") String tenantId, @RequestParam("userId") String userId) { + customTenantUtil.checkOutTenant(tenantId); + return ActionResult.success(attendanceDailyRuleService.userIsScheduling(userId)); + } + + /** + * 用户是否排班 + * + * @return + */ + @PostMapping("userIsSchedulingOrdinary") + @Override + public ActionResult> userIsSchedulingOrdinary(@RequestBody List organizeIds) { + return ActionResult.success(attendanceDailyRuleService.userIsSchedulingOrdinary(organizeIds)); + } + + /** + * + * @param userId + * @param start + * @param end + * @return + */ + @GetMapping("hasRuleByUserIdAndTime") + @Override + public ActionResult hasRuleByUserIdAndTime(@RequestParam String userId, @RequestParam Date start, @RequestParam Date end) { + return ActionResult.success(attendanceDailyRuleService.hasRuleByUserIdAndTime(userId, start, end)); + } + /** + * 设置划线排班 + * + * @param configDto 划线排班配置DTO + * @return 处理结果 + * @throws HandleException 处理异常 + */ + @PostMapping("lineDrawingSchedules") + public ActionResult setLineDrawingSchedules(@RequestBody LineDrawingSchedulesConfigDto configDto) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.setLineDrawingSchedules(configDto)); + } + + /** + * 统一排班接口(支持固定排班和划线排班) + * + * @param dto 统一排班DTO + * @return 处理结果 + * @throws HandleException 处理异常 + */ + @PostMapping("unifiedSchedules") + public ActionResult setUnifiedSchedules(@RequestBody UnifiedSchedulesDto dto) throws HandleException { + return ActionResult.success(attendanceDailyRuleService.setUnifiedSchedules(dto)); + } + + /** + * 指定用户日期是否存在划线排班 + * + * @param configDto 划线排班配置DTO + * @return 处理结果 + */ + @PostMapping("queryLineSchedulingExist") + public ActionResult queryLineSchedulingExist(@RequestBody LineDrawingSchedulesConfigDto configDto) { + return ActionResult.success(attendanceDailyRuleService.queryLineSchedulingExist(configDto)); + } + + /** + * 获取划线排班列表 + * + * @param groupId 考勤组id + * @param workGroupId 班组ID + * @param dayList 日期列表 + * @param finalUserIdList 用户ID列表 + * @return 划线排班列表 + */ + @GetMapping("getLineSchedulesList") + public ActionResult> getLineSchedulesList(@RequestParam String groupId, @RequestParam(required = false) String workGroupId, @RequestParam(required = false) String dayList, @RequestParam(required = false) List finalUserIdList) { + return ActionResult.success(attendanceDailyRuleService.getLineSchedulesList(groupId, workGroupId, Arrays.asList(StringUtil.split(dayList, ",")), finalUserIdList)); + } + + /** + * 查询指定用户指定日期的班次信息,包含有效请假及普班 + * + * @param queryDto 查询参数(用户ID、查询日期) + * @return 用户指定日期的班次信息 + */ + /** + * 按自然日查询考勤组所属门店营业额预估 + * + * @param groupId 考勤组ID + * @param startTime 开始日期(含),yyyy-MM-dd + * @param endTime 结束日期(含),yyyy-MM-dd + */ + @Operation(summary = "按自然日查询考勤组营业额预估") + @GetMapping("/group-receivable-revenue-by-day") + public ActionResult> listReceivableRevenueByDay( + @RequestParam String groupId, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startTime, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endTime) { + return ActionResult.success(attendanceDailyRuleService.listReceivableRevenueByDay(groupId, startTime, endTime)); + } + + @Operation(summary = "查询指定用户指定日期的班次信息") + @PostMapping("/user-day-shift-info") + public ActionResult getUserDayShiftInfo(@Valid @RequestBody UserDayShiftQueryDto queryDto) { + try { + log.info("查询用户指定日期班次信息,userId=>{}, queryDate=>{}", + queryDto.getUserId(), DateUtil.dateToString(queryDto.getQueryDate(), "yyyy-MM-dd")); + long st = System.currentTimeMillis(); + + UserDayShiftInfoVo result = attendanceDailyRuleService.getUserDayShiftInfo( + queryDto.getUserId(), + queryDto.getQueryDate()); + + long ent = System.currentTimeMillis(); + log.info("查询用户指定日期班次信息完成,耗时=>{} 毫秒", ent - st); + return ActionResult.success(result); + } catch (Exception e) { + log.error("查询用户指定日期班次信息异常", e); + return ActionResult.fail("查询失败:" + e.getMessage()); + } + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalRulesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalRulesController.java new file mode 100644 index 0000000..d1f30cd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalRulesController.java @@ -0,0 +1,103 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.AttendanceFestivalRulesService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.AttendanceFestivalRulesDto; +import jnpf.model.attendance.dto.FestivalRulesQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceFestivalRulesVo; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +/** + * 节日规则 + * + * @author 盼盼 + * @create 2025-09-18 + */ +@RestController +@RequestMapping(value = "/attendance/festival-rules") +@Slf4j +public class AttendanceFestivalRulesController { + + @Autowired + private AttendanceFestivalRulesService attendanceFestivalRulesService; + + + /** + * 获取考勤节日管理列表 + * @param queryDto 节日名称模糊查询非必传及年份筛选必传 + */ + @PostMapping("/list") + public ActionResult> list(@RequestBody FestivalRulesQueryDto queryDto) { + PageInfo vo = attendanceFestivalRulesService.getPageList(queryDto); + return ActionResult.page(vo.getList(), FtbUtil.getPagination(vo)); + } + + /** + * 新增节日 + * @param festivalRulesDto 节日 + */ + @PostMapping() + public ActionResult add(@RequestBody AttendanceFestivalRulesDto festivalRulesDto) throws Exception { + attendanceFestivalRulesService.add(festivalRulesDto); + return ActionResult.success(); + } + + /** + * 更新法定节假日信息。 + * + * @param year 年份 + * @return 处理结果视图对象 + */ + @PutMapping("statutoryUpdate") + public ActionResult statutoryUpdate(@RequestParam String year) { + attendanceFestivalRulesService.statutoryUpdate(year); + return ActionResult.success(); + } + /** + * 修改节日 + * @param festivalRulesDto 节日 + */ + @PutMapping("/{id}") + public ActionResult put(@PathVariable("id") String id, @RequestBody AttendanceFestivalRulesDto festivalRulesDto) throws Exception { + festivalRulesDto.setId( id); + attendanceFestivalRulesService.update(festivalRulesDto); + return ActionResult.success(); + } + + /** + * 获取节日详情 + * @param id 节日 ID + */ + @GetMapping("/{id}") + public ActionResult detail(@PathVariable("id") String id) { + return ActionResult.success(attendanceFestivalRulesService.detail(id)); + } + + + /** + * 删除节日 + * @param id 节日 ID + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + attendanceFestivalRulesService.delete(id); + return ActionResult.success(); + } + + /** + * 启用、停用节日 + * @param festivalRulesDto 节日 + */ + @PutMapping("/updateState") + public ActionResult updateState( @RequestBody AttendanceFestivalRulesDto festivalRulesDto) { + attendanceFestivalRulesService.updateState(festivalRulesDto); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalSettingController.java new file mode 100644 index 0000000..e0f223b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceFestivalSettingController.java @@ -0,0 +1,122 @@ +package jnpf.attendance.controller; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import jnpf.attendance.service.AttendanceFestivalSettingService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceFestivalSettingDto; +import jnpf.model.attendance.dto.EnableUpdateDto; +import jnpf.model.attendance.vo.AttendanceFestivalSettingVo; +import jnpf.model.attendance.vo.HolidayOptionVo; +import jnpf.util.PageUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + *

+ * 考勤配置-节日配置 前端控制器 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@RestController +@RequestMapping("/festival/setting") +public class AttendanceFestivalSettingController { + @Autowired + private AttendanceFestivalSettingService attendanceFestivalSettingService; + /** + * 保存节假日设置信息。 + * + * @param attendanceFestivalSettingDto 节假日设置数据传输对象 + * @throws HandleException 处理异常 + * @return 处理结果视图对象 + */ + @PostMapping + public ActionResult save(@RequestBody AttendanceFestivalSettingDto attendanceFestivalSettingDto) throws HandleException { + attendanceFestivalSettingService.save(attendanceFestivalSettingDto); + return ActionResult.success(); + } + + /** + * 分页查询节假日设置信息。 + * + * @param groupId 考勤组ID + * @param year 年份(可选) + * @param currentPage 当前页码 + * @param pageSize 每页大小 + * @return 分页的节假日设置视图对象列表 + * @throws HandleException 处理异常 + */ + @GetMapping + public ActionResult> page(@RequestParam String groupId, @RequestParam(required = false) String year, @RequestParam Integer currentPage, @RequestParam Integer pageSize) throws HandleException { + PageDTO page1 = attendanceFestivalSettingService.page(groupId, year, currentPage, pageSize); + return ActionResult.page(page1.getRecords(), PageUtil.page(page1)); + } + + /** + * 根据ID获取单个节假日设置信息。 + * + * @param id 节假日设置ID + * @return 节假日设置视图对象 + */ + @GetMapping("{id}") + public ActionResult getOne(@PathVariable String id) { + return ActionResult.success(attendanceFestivalSettingService.getOne(id)); + } + + /** + * 删除指定的节假日设置信息。 + * + * @param id 节假日设置ID + * @return 处理结果视图对象 + */ + @DeleteMapping("{id}") + public ActionResult del(@PathVariable String id) { + attendanceFestivalSettingService.del(id); + return ActionResult.success(); + } + + /** + * 更改节假日设置信息的启用状态。 + * + * @param enableUpdateDto 启用状态更新数据传输对象 + * @throws HandleException 处理异常 + * @return 处理结果视图对象 + */ + @PutMapping("updateStatus") + public ActionResult updateStatus(@RequestBody EnableUpdateDto enableUpdateDto) throws HandleException { + attendanceFestivalSettingService.updateStatus(enableUpdateDto.getId(), enableUpdateDto.getEnable()); + return ActionResult.success(); + } + + /** + * 更新法定节假日信息。 + * + * @param groupId 考勤组ID + * @param year 年份 + * @return 处理结果视图对象 + */ + @PutMapping("statutoryUpdate") + public ActionResult statutoryUpdate(@RequestParam String groupId, @RequestParam String year) { + attendanceFestivalSettingService.statutoryUpdate(groupId, year); + return ActionResult.success(); + } + + /** + * 获取节假日选项列表。 + * + * @param groupId 考勤组ID + * @return 节假日选项视图对象列表 + */ + @GetMapping("options") + public ActionResult> getHolidayOptions(@RequestParam String groupId) { + return ActionResult.success(attendanceFestivalSettingService.getHolidayOptions(groupId)); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupController.java new file mode 100644 index 0000000..dab8680 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupController.java @@ -0,0 +1,615 @@ +package jnpf.attendance.controller; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.AttendanceUserListGroupVO; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.base.ActionResult; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AppointPermission; +import jnpf.enums.attendance.GroupUserTypeEnum; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.GroupLockVo; +import jnpf.util.*; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 考勤组控制器 + * + * @author yier + * @create 2023-11-21 + */ +@RestController +@RequestMapping(value = "/group") +public class AttendanceGroupController implements AttendanceGroupApi { + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceGroupService attendanceGroupService; + + /** + * 查询考勤组管理列表 + * + * @param groupQueryDto 考勤组名称查询实体 + * @return List + */ + @GetMapping + public ActionResult> list(GroupQueryDto groupQueryDto) { + return ActionResult.success(attendanceGroupService.groupManagerList(groupQueryDto)); + } + + /** + * 新增考勤组 + * + * @param saveAttendanceGroupDto 考勤组保存实体 + * @return ActionResult + */ + @PostMapping + public ActionResult add(@RequestBody SaveAttendanceGroupDto saveAttendanceGroupDto) throws Exception { + ParamUtil.checkParam(saveAttendanceGroupDto); + int num = attendanceGroupService.save(saveAttendanceGroupDto); + if (num < 0) { + return ActionResult.fail("考勤组名称重复,请换一个名称!"); + } + return ActionResult.success(); + } + + /** + * 修改考勤组信息 + * + * @param saveAttendanceGroupDto 考勤组保存实体 + * @return Void + */ + @PutMapping + public ActionResult update(@RequestBody SaveAttendanceGroupDto saveAttendanceGroupDto) throws Exception { + ParamUtil.checkParam(saveAttendanceGroupDto); + int num = attendanceGroupService.save(saveAttendanceGroupDto); + if (num < 0) { + return ActionResult.fail("考勤组名称重复,请换一个名称!"); + } + return ActionResult.success(); + } + + /** + * 删除考勤组 + * + * @param saveAttendanceGroupDto 考勤组保存实体 + * @return Void + */ + @DeleteMapping("/deleteGroup") + public ActionResult delete(@RequestBody SaveAttendanceGroupDto saveAttendanceGroupDto) throws HandleException { + /*移动考勤组*/ + String parentId = saveAttendanceGroupDto.getParentId(); + if (StrUtil.isNotBlank(parentId)) { + AttendanceGroup moveGroup = attendanceGroupService.getById(parentId); + if (moveGroup.getLevelCode().contains(saveAttendanceGroupDto.getId())) { + throw new HandleException("不能将删除考勤组移动到本组获取子组!"); + } + } + + String id = saveAttendanceGroupDto.getId(); + AttendanceGroup group = attendanceGroupService.getById(id); + if (group.getDeleteMark() == 1) { + throw new HandleException("该考勤组已删除,请勿频繁操作!"); + } + + attendanceGroupService.delete(saveAttendanceGroupDto.getId(), saveAttendanceGroupDto.getParentId()); + return ActionResult.success(); + } + + /** + * 借调时展示我能查看到的考勤组列表 + * + * @return ActionResult> + */ + @GetMapping("/secondedQueryGroup") + public ActionResult> queryManagerGroupList(String keyword) { + List attendanceGroupVoList = attendanceGroupService.queryManagerGroupList(keyword); + return ActionResult.success(attendanceGroupVoList); + } + + /** + * 查询所有考勤组 + */ + @GetMapping("/getAllGroup") + public ActionResult> getAllGroup(String keyword) { + List groupVoList = attendanceGroupService.queryList(keyword); + return ActionResult.success(groupVoList); + } + + /** + * 根据考勤组名称模糊查询考勤组列表(带权限控制) + * 返回用户有管理权限的考勤组,支持名称模糊查询 + * + * @param groupName 考勤组名称(支持模糊查询) + * @return ActionResult> + */ + @GetMapping("/searchByName") + public ActionResult> searchGroupByName(@RequestParam String groupName) { + return ActionResult.success(attendanceGroupService.searchGroupByName(groupName)); + } + + /** + * 查询我所在的考勤组列表(In:app/考勤规则) + * + * @return ActionResult> + */ + @GetMapping("/queryMyGroupList") + public ActionResult> queryMyGroupList() { + List groupList = attendanceGroupService.queryMyGroups(); + return ActionResult.success(groupList); + } + + /** + * 查询考勤组下成员 + * + * @param id 考勤组id + * @return ActionResult> + */ + @GetMapping("/{id}/userList") + public ActionResult> queryUserList(@PathVariable String id, String name, Integer type, String month) { + Date start = DateDetail.getMonthBeginDate(month); + Date end = DateDetail.getMonthEndDate(month); + return ActionResult.success(attendanceUserService.queryUsersByGroupId(id, name, type, null, start,end)); + } + + /** + * 借调查询考勤组下成员 + * + * @param id 考勤组id + * @return ActionResult> + */ + @GetMapping("/userList") + public ActionResult> queryUserListOfSecondment(@RequestParam String id, @RequestParam(required = false) String name, @RequestParam(required = false) Integer type, @RequestParam(required = false) String time) { + Assert.isFalse(StringUtil.isBlank(time), "未选择借调时间"); + String[] split = StringUtil.split(time, ","); + Assert.isFalse(split.length < 2, "未选择借调时间"); + Date secondedStartTime = new Date(Long.parseLong(split[0])); + Date secondedEndTime = new Date(Long.parseLong(split[1])); + List listActionResult = attendanceUserService.queryUsersByGroupId(id, name, type,null, secondedStartTime, secondedEndTime); + if (listActionResult!= null) { + listActionResult.forEach(v -> { + v.setRealName(v.getRealName() + "(" + v.getPositionName() + ")"); + }); + } + return ActionResult.success(listActionResult); + } + + /** + * 考勤组下拉列表 + * + * @return ActionResult> + */ + @GetMapping("/dropList") + public ActionResult> dropList() { + List dropList = attendanceGroupService.queryDropList(); + return ActionResult.success(dropList); + } + + /** + * 借调开始 + * + * @param attendanceSecondedDto 借调参数数据 + * @return ActionResult + */ + @PostMapping("/secondedStart") + public ActionResult secondedStart(@RequestBody AttendanceSecondedDto attendanceSecondedDto) throws HandleException { + attendanceGroupService.secondedStart(attendanceSecondedDto); + return ActionResult.success(); + } + + /** + * 切换考勤组配置状态 + * + * @param dto 考勤组配置状态参数 + * @return ActionResult + */ + @PutMapping("/exchangeGroupStatus") + public ActionResult exchangeGroupStatus(@RequestBody AttendanceGroupStatusDto dto) { + attendanceGroupService.exchangeGroupStatus(dto); + return ActionResult.success(); + } + + /** + * 获取考勤组配置状态 + * + * @param groupId 考勤组id + * @return ActionResult + */ + @GetMapping("groupSettingStatus") + public ActionResult groupSettingStatus(@RequestParam String groupId) { + return ActionResult.success(attendanceGroupService.groupSettingStatus(groupId, Boolean.FALSE)); + } + + /** + * 获取默认考勤组 + * + * @return ActionResult + */ + @GetMapping("/getDefaultGroup") + public ActionResult getDefaultGroup() { + AttendanceGroupVo defaultGroup = attendanceGroupService.getDefaultGroup(); + return ActionResult.success(defaultGroup); + } + + /** + * 切换考勤组 + * + * @param groupId 考勤组Id + * @return ActionResult + */ + @GetMapping("/handoffGroup") + public ActionResult handoffGroup(String groupId) { + attendanceGroupService.handoffGroup(groupId); + return ActionResult.success(); + } + + /** + * 排班考勤组列表 + * + * @return ActionResult + */ + @GetMapping("/schedulingGroupList") + public ActionResult> schedulingGroupList() { + List groupVoList = attendanceGroupService.schedulingGroupList(); + return ActionResult.success(groupVoList); + } + + /** + * 排班考勤组列表过滤固定班 + * + * @return ActionResult + */ + @GetMapping("/schedulingGroupListByNotFixed") + public ActionResult> schedulingGroupListByNotFixed() { + List groupVoList = attendanceGroupService.schedulingGroupListByNotFixed(); + return ActionResult.success(groupVoList); + } + + /** + * 余额考勤组列表 + * + * @return ActionResult + */ + @GetMapping("/balanceManagementGroupList") + public ActionResult> balanceManagementGroupList() { + List groupVoList = attendanceGroupService.balanceManagement(); + return ActionResult.success(groupVoList); + } + + /** + * 查询我有借调权限的考勤组管理列表 + * + */ + @GetMapping("/querySecondedApprovalGroupList") + public ActionResult> querySecondedApprovalGroupList() { + List groupVoList = attendanceGroupService.secondedApprovalGroupList(); + return ActionResult.success(groupVoList); + } + + /** + * 切换考勤组查询我管理的考勤组 + * + */ + @GetMapping("/handoffGroupTreeList") + public ActionResult> handoffGroupTreeList() { + List groupVoList = attendanceGroupService.handoffGroupTreeList(); + return ActionResult.success(groupVoList); + } + + /** + * 通过组织Id找到组织下的考勤组 + * + * @param orgId 组织Id + */ + @GetMapping("/groupListByOrgId/{orgId}") + public ActionResult> groupListByOrgId(@PathVariable("orgId") String orgId) { + List groupVoList = attendanceGroupService.groupListByOrgId(orgId); + return ActionResult.success(groupVoList); + } + + /** + * 通过组织Id找到组织下的考勤组 + * + * @param orgId 组织Id + */ + @GetMapping("/groupListByOrgId") + public ActionResult> groupListByOrgIds(@RequestParam("orgId") String orgId) { + List groupVoList = attendanceGroupService.groupListByOrgId(orgId); + return ActionResult.success(groupVoList); + } + + /** + * 通过考勤组Id获取绑定的组织名称(拼接好的)及考勤组人数 + * + * @param groupId 考勤组Id + */ + @GetMapping("/getGroupBindingOrg/{groupId}") + public ActionResult getGroupBindingOrg(@PathVariable("groupId") String groupId,@RequestParam(value = "workGroupId",required = false) String workGroupId) { + return ActionResult.success(attendanceGroupService.getGroupBindingOrg(groupId, workGroupId)); + } + /** + * 划线排班-通过考勤组Id获取绑定的组织名称(拼接好的)及考勤组人数 + * + * @param groupId 考勤组Id + */ + @GetMapping("/line/getGroupBindingOrg/{groupId}") + public ActionResult getGroupBindingOrgForLine(@PathVariable("groupId") String groupId,@RequestParam(value = "workGroupId",required = false) String workGroupId) { + return ActionResult.success(attendanceGroupService.getGroupBindingOrgForLine(groupId, workGroupId)); + } + + + /** + * 绑定考勤组的组织 + * + * @param attendanceGroupOrgDto 考勤组和组织信息 + */ + @PutMapping("/updateGroupOrg") + public ActionResult updateGroupOrg(@RequestBody AttendanceGroupOrgDto attendanceGroupOrgDto) { + attendanceGroupService.updateGroupOrg(attendanceGroupOrgDto); + return ActionResult.success(); + } + + /** + * 获取考勤组名称 + * + * @param groupId 考勤组Id + */ + @Override + @GetMapping("/queryTheNameOfTheAttendanceGroup") + public AttendanceGroup queryTheNameOfTheAttendanceGroup(@RequestParam(value = "groupId") String groupId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(AttendanceGroup::getId, groupId); + queryWrapper.eq(AttendanceGroup::getDeleteMark, 0); + return attendanceGroupService.getOne(queryWrapper); + } + + /** + * 获取用户考勤组 + * + * @param userIds 用户ids + */ + @Override + @GetMapping("/attendanceUserGroup") + public List getAttendanceUserGroup(@RequestParam("userIds") List userIds) { + return attendanceGroupService.getAttendanceUserGroup(userIds); + } + + /** + * 批量获取用户绑定的考勤组 ,包含全名称 xxx/xxx/xxx + * + * @param userIds 用户ids + */ + @Override + @PostMapping("/list/user_bound") + public ActionResult> getAttendanceUserListGroupVO(@RequestBody List userIds) { + + return ActionResult.success(attendanceGroupService.getAttendanceUserListGroupVO(userIds)); + } + + /** + * 获取组织下的考勤组 + * + * @param organizeId 组织id + * @return List + */ + @GetMapping("/getGroupListByOrgId") + @Override + public List getGroupListByOrgId(@RequestParam("organizeId") String organizeId) { + QueryWrapper groupQuery = new QueryWrapper<>(); + groupQuery.lambda() + .eq(AttendanceGroup::getOrgId, organizeId); + return attendanceGroupService.getByOrgId(organizeId); + } + + /** + * 详情---获取一个考勤组 + * + * @param organizeName 组织名称 + * @param groupName 考勤组名称 + * @return 一个考勤组 + */ + @Override + @GetMapping("/info/organize_group/name") + public ActionResult getAttendanceGroupByName(@RequestParam("organizeName") String organizeName, @RequestParam("groupName") String groupName) { + + return ActionResult.success(attendanceGroupService.getAttendanceGroupByNameOrganizeAndGroup(organizeName, groupName)); + } + + /** + * 锁定考勤组 + * + * @param groupId 考勤组id + */ + @PutMapping(value = "/lock/{groupId}") + public Object lockGroup(@PathVariable(value = "groupId") String groupId) { + + attendanceGroupService.lockGroup(groupId); + return ActionResult.success(); + } + /** + * 考勤组2.0历史数据处理 + * + * @param tenantId 考勤组id + */ + @GetMapping(value = "/oldDataProcessing/{tenantId}") + @NoDataSourceBind + public Object oldDataProcessing(@PathVariable(value = "tenantId") String tenantId) { + attendanceGroupService.oldDataProcessing(tenantId); + return ActionResult.success(); + } + + /** + * 查询用户当前考勤组锁定信息 + * + * @param userId 用户id + * @return java.lang.Object + */ + @GetMapping(value = "/lockInfo/{userId}") + public Object getUserCurrentGroupLockInfo(@PathVariable(value = "userId") String userId) throws Exception { + + GroupLockVo groupLockVo = attendanceGroupService.getUserCurrentGroupLockInfo(userId); + return ActionResult.success(groupLockVo); + } + + /** + * 查询当前考勤组锁定信息 + * + * @param groupId 考勤组id + * @return java.lang.Object + */ + @GetMapping(value = "/groupLockInfo/{groupId}") + public Object getGroupLockInfo(@PathVariable(value = "groupId") String groupId) throws Exception { + + GroupLockVo groupLockVo = attendanceGroupService.getGroupLockInfo(groupId); + return ActionResult.success(groupLockVo); + } + + /** + * 查询用户所在考勤组是否有指定权限 + * @param appointPermission 参数 + */ + @GetMapping("/appointPermission") + public ActionResult appointPermission(AppointPermission appointPermission) { + return ActionResult.success(attendanceGroupService.appointPermission(appointPermission)); + } + + //*************************************考勤V1.9***************************************************/ + + /** + * V1.9 查询考勤组管理列表 + * @param groupQueryDto 考勤组名称查询实体 + * @return List + */ + @GetMapping("/list") + public ActionResult> groupList(GroupQueryDto groupQueryDto) { + return ActionResult.success(attendanceGroupService.groupMyManagerList(groupQueryDto)); + } + + + /** + * V1.9 排班考勤组列表过滤固定班 + * @return ActionResult + */ + @GetMapping("/schedulingGroupListByNotFixedNew") + public ActionResult> schedulingGroupListByNotFixedNew() { + List groupVoList = attendanceGroupService.schedulingGroupListByNotFixedNew(); + return ActionResult.success(groupVoList); + } + + /** + * V1.9 切换考勤组查询我管理的考勤组 + */ + @GetMapping("/handoffGroupTreeListNew") + public ActionResult> handoffGroupTreeListNew() { + List groupVoList = attendanceGroupService.handoffGroupTreeListNew(); + return ActionResult.success(groupVoList); + } + + /** + * v1.9获取默认考勤组 + * + * @return ActionResult + */ + @GetMapping("/getDefaultGroupNew") + public ActionResult getDefaultGroupNew() { + AttendanceGroupVo defaultGroup = attendanceGroupService.getDefaultGroupNew(); + return ActionResult.success(defaultGroup); + } + + /** + * v1.9排班考勤组列表 + */ + @GetMapping("/schedulingGroupListNew") + public ActionResult> schedulingGroupListNew() { + List groupVoList = attendanceGroupService.schedulingGroupListNew(); + return ActionResult.success(groupVoList); + } + + + /** + * v1.9余额考勤组列表 + */ + @GetMapping("/balanceManagementGroupListNew") + public ActionResult> balanceManagementGroupListNew() { + List groupVoList = attendanceGroupService.balanceManagementNew(); + return ActionResult.success(groupVoList); + } + + + /** + * V2.0 出勤变更查询考勤组列表 + */ + @GetMapping("/querySecondedApprovalGroupListNew") + public ActionResult> querySecondedApprovalGroupListNew() { + List groupVoList = attendanceGroupService.secondedApprovalGroupListNew(); + return ActionResult.success(groupVoList); + } + + /** + * 是否有考勤组排班权限排班 + */ + @Override + @GetMapping("/checkScheduling") + public boolean checkScheduling() { + return attendanceGroupService.checkScheduling(); + } + + /** + * 获取有指定模块的权限的考勤组管理列表 + * + * @param positionModuleName 权限模块名称 + * @return List + */ + @GetMapping("/getPermissionsGroupList") + public ActionResult> getPermissionsGroupList(@RequestParam("positionModuleName") String positionModuleName) { + return ActionResult.success(attendanceGroupService.getPermissionsGroupList(positionModuleName)); + } + + /** + * 获取端考勤组列表(左侧选择组织||考勤组) + */ + @GetMapping("/getOrgOrGroupList") + public ActionResult> getOrgOrGroupList(String keyword) { + return ActionResult.success(attendanceGroupService.getOrgOrGroupList(keyword)); + } + + /** + * 设置考勤组负责人 + * + * @param dto 考勤组负责人信息 + * @return Boolean + */ + @PutMapping(value = "/setGroupCharge") + public ActionResult setGroupCharge(@Valid @RequestBody AttendanceGroupChargeDto dto) { + return ActionResult.success(attendanceGroupService.setGroupCharge(dto)); + } + + /** + * 获取考勤组负责人 + * + * @param groupId 考勤组id + * @return List + */ + @GetMapping(value = "/getGroupChargeInfo/{groupId}") + public ActionResult getGroupChargeInfo(@PathVariable(value = "groupId") String groupId) { + return ActionResult.success(attendanceGroupService.getGroupChargeInfo(groupId)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupUserController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupUserController.java new file mode 100644 index 0000000..6bf9392 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceGroupUserController.java @@ -0,0 +1,228 @@ +package jnpf.attendance.controller; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.AttendanceUserApi; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.base.ActionResult; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.ExportUserTemplateDto; +import jnpf.model.attendance.dto.GroupUserQueryDto; +import jnpf.model.attendance.dto.JoinUserDto; +import jnpf.model.attendance.dto.UserSortModel; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.attendance.vo.attendance.JoinGroupVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.CustomTenantUtil; +import jnpf.util.EasyExcelUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.ParamUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 考勤组/成员管理 + * + * @author yier + * @Time 2023-11-23 + */ +@Slf4j +@RestController +@RequestMapping(value = "/user") +public class AttendanceGroupUserController implements AttendanceUserApi { + + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceGroupService attendanceGroupService; + @Resource + private AttendanceDayStatisticsService dayStaService; + @Resource + private CustomTenantUtil customTenantUtil; + + /** + * 成员管理-查询系统用户(附带考勤组名称) + * + * @param orgId 部门id + * @param name 用户姓名 字段模糊搜索 + * @return ActionResult + */ + @GetMapping("/getSysUsers") + public ActionResult> get(String orgId, String name) { + List voList = attendanceUserService.querySysUserList(orgId, name); + return ActionResult.success(voList); + } + + + /** + * 用户排序 + * + * @param userSortModel + */ + @PutMapping("/sort") + public ActionResult sort(@RequestBody UserSortModel userSortModel) { + Assert.isFalse(CollUtil.isEmpty(userSortModel.getUserSortList()), "未传入用户数据"); + attendanceUserService.sort(userSortModel); + return ActionResult.success(); + } + + /** + * 用户加入考勤组 + * + * @param joinUserDto + * @return ActionResult + * @throws Exception + */ + @PostMapping + public ActionResult addUsers(@RequestBody JoinUserDto joinUserDto) throws Exception { + ParamUtil.checkParam(joinUserDto); + attendanceUserService.joinUsers(joinUserDto); + return ActionResult.success(); + } + + /** + * 批量移除考勤组成员 + * + * @param joinUserDto + * @return + */ + @DeleteMapping + public ActionResult batchRemove(@RequestBody JoinUserDto joinUserDto) { + attendanceUserService.batchDeleteUsersForSendNotice(joinUserDto); + return ActionResult.success(); + } + + /** + * 花名册考勤组变更 + * + * @param groupUpdateByUserDTO + * @return + */ + @PostMapping("groupUpdateByPersonnel") + @NoDataSourceBind + @Deprecated + public ActionResult groupUpdateByPersonnel(@RequestBody GroupUpdateByUserDTO groupUpdateByUserDTO) { + customTenantUtil.checkOutTenant(groupUpdateByUserDTO.getTenantId()); + attendanceUserService.groupUpdateByPersonnel(groupUpdateByUserDTO); + return ActionResult.success(); + } + + /** + * 检查用户是否已经加入 + * + * @param userIds + * @return ActionResult> + */ + @GetMapping("/checkUserJoinGroup") + public ActionResult> checkUserJoinGroup(String userIds) throws HandleException { + List userNameList = attendanceUserService.checkUserGroup(userIds); + return ActionResult.success(userNameList); + } + + + /** + * 检查用户是否已经加入1.9 + * @param userIds 用户ids + * @return ActionResult> + */ + @PostMapping("/checkUserJoinGroupToObject") + public ActionResult> checkUserJoinGroupToObject(@RequestBody String userIds) throws HandleException { + List userNameList = attendanceUserService.checkUserJoinGroupToObject(userIds); + return ActionResult.success(userNameList); + } + + /** + * 导出用户模版 + */ + @GetMapping("/downloadTemplate") + @NoDataSourceBind + public void downloadTemplate(HttpServletResponse response, @RequestParam String tenantId) throws Exception { + customTenantUtil.checkOutTenant(tenantId); + EasyExcelUtil.export(response, "考勤组成员.xlsx", "考勤组成员", ExportUserTemplateDto.class, null); + } + + /** + * 筛选考勤组下成员,去重,入参考勤组ids 出参用户集合 借调用户也要查询出来 + * + * @param groupUserQueryDto 考勤组信息 + * @return + */ + @PostMapping("/getUsersByGroupIds") + public ActionResult> getUsersByGroupIds(@RequestBody @Valid GroupUserQueryDto groupUserQueryDto) { + List voList = dayStaService.getUserIdArr(groupUserQueryDto.getFilterList(), + groupUserQueryDto.getStartTime(), groupUserQueryDto.getEndTime(), groupUserQueryDto.getUserIds(), "0"); + List userVoList = CollectionUtil.isEmpty(voList) ? new ArrayList<>() : + voList.stream().map(vo -> { + AttendanceGroupUserVo userVo = new AttendanceGroupUserVo(); + userVo.setUserId(vo.getId()); + userVo.setRealName(vo.getUserName()); + userVo.setNickname(vo.getNickname()); + return userVo; + }).collect(Collectors.toList()); + return ActionResult.success(userVoList); + } + + @Operation(summary = "借调考勤组变动通知") + @GetMapping(value = "/userGroupUpdateBySecondNotice") + public ActionResult userGroupUpdateBySecondNotice(@RequestParam(value = "tenantId") String tenantId) { + try { + log.info("执行借调考勤组变动通知"); + return ActionResult.success(attendanceUserService.userGroupUpdateBySecondNotice(tenantId)); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.success(Boolean.FALSE); + } + } + + /** + * 指定考勤组成员列表 + * + * @param id 考勤组Id + * @param isPermissions 是否权限控制 + */ + @GetMapping("/getGroupUserList") + public ActionResult> getGroupUserList(@RequestParam(value = "id") String id, + @RequestParam(value = "isPermissions") Boolean isPermissions) { + List voList = attendanceUserService.getGroupUserList(id, isPermissions); + return ActionResult.success(voList); + } + + /** + * 获取现在考勤组成员列表包含借调不含权限 + * @param id 考勤组Id + */ + @GetMapping("/getGroupNowAllUserList") + public ActionResult> getGroupNowAllUserList(@RequestParam(value = "id") String id) { + List attendanceGroupUsersOfSecondment = attendanceUserService.getGroupNowAllUserList(id); + return ActionResult.success(attendanceGroupUsersOfSecondment); + } + + /** + * 根据用户ID查询当前时间所在考勤组(包含借调) + * + * @param userId 用户ID + * @return ActionResult> + */ + @GetMapping("/getUserCurrentGroup") + public ActionResult> getUserCurrentGroup(@RequestParam String userId) { + Assert.isFalse(StrUtil.isBlank(userId), "用户ID不能为空"); + List groups = attendanceGroupService.getUserCurrentGroups(userId); + return ActionResult.success(groups); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceHolidaySettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceHolidaySettingController.java new file mode 100644 index 0000000..10a6d32 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceHolidaySettingController.java @@ -0,0 +1,98 @@ +package jnpf.attendance.controller; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import jnpf.attendance.service.AttendanceHolidaySettingService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.AttendanceHolidaySettingDto; +import jnpf.model.attendance.dto.EnableUpdateDto; +import jnpf.model.attendance.vo.AttendanceHolidaySettingVo; +import jnpf.util.PageUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + *

+ * 考勤配置-假日设置 前端控制器 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@RestController +@RequestMapping("/holiday/setting") +public class AttendanceHolidaySettingController { + + @Autowired + private AttendanceHolidaySettingService attendanceHolidaySettingService; + + /** + * 分页查询节假日设置信息。 + * + * @param groupId 考勤组ID + * @param name 节假日名称(可选) + * @param paidSalaryEnable 是否计入薪酬标识(可选) + * @param currentPage 当前页码 + * @param pageSize 每页大小 + * @return 分页的节假日设置视图对象列表 + */ + @GetMapping + public ActionResult> page(@RequestParam String groupId, + @RequestParam(required = false) String name, + @RequestParam(required = false) Integer paidSalaryEnable, + @RequestParam Integer currentPage, + @RequestParam Integer pageSize){ + PageDTO page1 = attendanceHolidaySettingService.page(groupId, name, paidSalaryEnable, currentPage, pageSize); + return ActionResult.page(page1.getRecords(), PageUtil.page(page1)); + } + + /** + * 根据ID获取单个节假日设置信息。 + * + * @param id 节假日设置ID + * @return 节假日设置视图对象 + */ + @GetMapping("{id}") + public ActionResult getOne(@PathVariable String id){ + return ActionResult.success(attendanceHolidaySettingService.getOne(id)); + } + + /** + * 删除指定的节假日设置信息。 + * + * @param id 节假日设置ID + * @return 处理结果视图对象 + */ + @DeleteMapping("{id}") + public ActionResult del(@PathVariable String id){ + attendanceHolidaySettingService.del(id); + return ActionResult.success(); + } + + /** + * 保存节假日设置信息。 + * + * @param dto 节假日设置数据传输对象 + * @return 处理结果视图对象 + */ + @PostMapping + public ActionResult save(@RequestBody AttendanceHolidaySettingDto dto){ + attendanceHolidaySettingService.save(dto); + return ActionResult.success(); + } + + /** + * 更改节假日设置信息的启用状态。 + * + * @param enableUpdateDto 启用状态更新数据传输对象 + * @return 处理结果视图对象 + */ + @PutMapping("updateStatus") + public ActionResult changeStatus(@RequestBody EnableUpdateDto enableUpdateDto){ + attendanceHolidaySettingService.changeStatus(enableUpdateDto.getId(), enableUpdateDto.getEnable()); + return ActionResult.success(); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLeaveRulesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLeaveRulesController.java new file mode 100644 index 0000000..a863b6f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLeaveRulesController.java @@ -0,0 +1,191 @@ +package jnpf.attendance.controller; + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.AttendanceLeaveRulesService; +import jnpf.attendance.service.AttendanceLeaveTypeService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.enums.attendance.LeaveTypeEnum; +import jnpf.enums.attendance.LeaveUnitEnum; +import jnpf.exception.ApproveException; +import jnpf.model.attendance.dto.AttendanceLeaveRulesDto; +import jnpf.model.attendance.dto.AttendanceLeaveRulesQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * 假期规则 + * + * @author panpan + * @create 2025-09-18 + */ +@RestController +@RequestMapping(value = "/attendance/leave-rules") +@Slf4j +public class AttendanceLeaveRulesController { + + @Autowired + private AttendanceLeaveRulesService attendanceLeaveRulesService; + + @Resource + private AttendanceLeaveTypeService attendanceLeaveTypeService; + + /** + * 获取考勤假期管理列表 + * @param attendanceLeaveRulesQueryDto 查询参数 + */ + @GetMapping("/list") + public ActionResult> list(AttendanceLeaveRulesQueryDto attendanceLeaveRulesQueryDto) { + PageInfo vo = attendanceLeaveRulesService.list(attendanceLeaveRulesQueryDto); + return ActionResult.page(vo.getList(), FtbUtil.getPagination(vo)); + } + + /** + * 新增假期 + * @param attendanceLeaveRulesDto 假期 + */ + @PostMapping() + public ActionResult add(@RequestBody AttendanceLeaveRulesDto attendanceLeaveRulesDto) throws Exception { + attendanceLeaveRulesService.create(attendanceLeaveRulesDto); + return ActionResult.success(); + } + + + /** + * 修改假期 + * @param attendanceLeaveRulesDto 假期 + */ + @PutMapping("/{id}") + public ActionResult put(@PathVariable("id") String id, @RequestBody AttendanceLeaveRulesDto attendanceLeaveRulesDto) throws Exception { + attendanceLeaveRulesDto.setId(id); + attendanceLeaveRulesService.update(attendanceLeaveRulesDto); + return ActionResult.success(); + } + + /** + * 获取假期详情 + * @param id 假期 ID + */ + @GetMapping("/{id}") + public ActionResult detail(@PathVariable("id") String id) { + return ActionResult.success(attendanceLeaveRulesService.detail(id)); + } + + + /** + * 删除假期 + * @param id 假期 ID + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + attendanceLeaveRulesService.delete(id); + return ActionResult.success(); + } + + /** + * 启用、停用假期 + * @param attendanceLeaveRulesDto 假期 + */ + @PutMapping("/updateState") + public ActionResult updateState( @RequestBody AttendanceLeaveRulesDto attendanceLeaveRulesDto) { + attendanceLeaveRulesService.updateState(attendanceLeaveRulesDto); + return ActionResult.success(); + } + + /** + * 获取用户请假列表 + */ + @GetMapping("/getUserLeaveList") + public ActionResult> getUserLeaveList(@RequestParam(required = false) boolean isFromOaCondition) { + if (isFromOaCondition){ + List list = attendanceLeaveTypeService.list(new LambdaQueryWrapper() + .eq(AttendanceLeaveType::getDeleteMark, 0) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + return ActionResult.success(list.stream().map(vo->{ + AttendanceLeaveRulesVo attendanceLeaveRulesVo = new AttendanceLeaveRulesVo(); + attendanceLeaveRulesVo.setLeaveTypeId(vo.getId()); + attendanceLeaveRulesVo.setLeaveTypeName(vo.getName()); + return attendanceLeaveRulesVo; + }).collect(Collectors.toList())); + } + return ActionResult.success(attendanceLeaveTypeService.getUserLeaveList()); + } + + /** + * 获取用户请假详情 + */ + + @GetMapping("/getUserLeaveDetail") + public ActionResult> getUserLeaveDetail(AttendanceLeaveType attendanceLeaveType ,@RequestParam(required = false) boolean isFromOaCondition) throws ApproveException { + if (isFromOaCondition) { + List list = new ArrayList<>(); + for (LeaveTypeEnum value : LeaveTypeEnum.values()) { + AttendanceLeaveRulesVo attendanceLeaveRulesVo = new AttendanceLeaveRulesVo(); + attendanceLeaveRulesVo.setUnit(value.getCode()); + attendanceLeaveRulesVo.setUnitName(value.getMsg()); + list.add(attendanceLeaveRulesVo); + } + return ActionResult.success(list); + } + AttendanceLeaveRulesVo vo = attendanceLeaveTypeService.getUserLeaveDetail(attendanceLeaveType.getId(),null); + List list = new ArrayList<>(); + list.add(vo); + return ActionResult.success(list); + } + + /** + * 获取用户请假详情 + */ + @GetMapping("/getLeaveDetailForOa") + public ActionResult getLeaveDetailForOa(AttendanceLeaveType attendanceLeaveType) throws ApproveException { + return ActionResult.success(attendanceLeaveTypeService.getUserLeaveDetail(attendanceLeaveType.getId(),null)); + } + + /** + * 获取请假类型列表 + */ + @GetMapping("/type/list") + public ActionResult> leaveTypeList(@RequestParam(value = "keyword", required = false) String keyword) { + List vo = attendanceLeaveTypeService.list(new LambdaQueryWrapper() + .eq(AttendanceLeaveType::getDeleteMark, 0) + .like(StringUtil.isNotEmpty(keyword), AttendanceLeaveType::getName, keyword) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + return ActionResult.success(vo); + } + + /** + * 新增/修改假期类型 + * @param attendanceLeaveType 假期类型 + */ + @PostMapping("/type") + public ActionResult typeSaveOrUpdate(@RequestBody AttendanceLeaveType attendanceLeaveType) { + attendanceLeaveTypeService.typeSaveOrUpdate(attendanceLeaveType); + return ActionResult.success(); + } + + + /** + * 删除假期类型 + * @param id 假期类型Id + */ + @DeleteMapping("/type/{id}") + public ActionResult deleteType(@PathVariable("id") String id) { + attendanceLeaveTypeService.deleteType(id); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLineSchedulingConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLineSchedulingConfigController.java new file mode 100644 index 0000000..29f1fe1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLineSchedulingConfigController.java @@ -0,0 +1,107 @@ +package jnpf.attendance.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceLineSchedulingConfigService; +import jnpf.base.ActionResult; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingConfig; +import jnpf.permission.V2PositionApi; +import jnpf.permission.dto.v2.position.QueryBasePositionBatchDTO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.util.StringUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +/** + * 划线排班配置控制器 + * + * @author ahua + * @version 2.1 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2025-12-30 + */ +@RestController +@RequestMapping(value = "/arranging/line/scheduling/config") +public class AttendanceLineSchedulingConfigController { + + @Resource + private AttendanceLineSchedulingConfigService attendanceLineSchedulingConfigService; + + @Resource + private V2PositionApi v2PositionApi; + + @Resource + private AttendanceGroupService attendanceGroupService; + + /** + * 根据考勤组ID获取划线排班配置详情 + * + * @param groupId 考勤组ID + * @return 划线排班配置详情 + */ + @GetMapping("detail") + public ActionResult detail(@RequestParam(value = "groupId") String groupId) { + FtbAttendanceLineSchedulingConfig config = attendanceLineSchedulingConfigService.getByGroupId(groupId); + return ActionResult.success(config); + } + /** + * 划线排班未排班通知 + * + * @param tenantId 租户id + * @return 划线排班配置详情 + */ + @GetMapping("noticeLineScheduling") + public ActionResult noticeLineScheduling(@RequestParam(value = "tenantId") String tenantId) { + attendanceLineSchedulingConfigService.noticeLineScheduling(tenantId); + return ActionResult.success(); + } + /** + * 保存或更新划线排班配置 + * + * @param config 划线排班配置 + * @return 保存结果 + */ + @PostMapping("saveOrUpdate") + public ActionResult saveOrUpdate(@RequestBody FtbAttendanceLineSchedulingConfig config) { + boolean result = attendanceLineSchedulingConfigService.saveOrUpdateLineSchedulingConfig(config); + return ActionResult.success(result); + } + + /** + * 根据考勤组ID获取岗位基础信息列表 + * + * @param groupId 考勤组ID + * @return 岗位基础信息列表 + */ + @Operation(summary = "[列表]根据考勤组ID获取岗位基础信息列表") + @GetMapping("/position/base/list") + public ActionResult> listPositionBaseInfo(@RequestParam(value = "groupId") String groupId) { + // 验证groupId + if (StringUtil.isBlank(groupId)) { + return ActionResult.fail("考勤组ID不能为空"); + } + + // 查询AttendanceGroup + AttendanceGroup attendanceGroup = attendanceGroupService.queryByGroupId(groupId); + if (attendanceGroup == null) { + return ActionResult.fail("未找到指定的考勤组"); + } + + // 获取orgId + String orgId = attendanceGroup.getOrgId(); + if (StringUtil.isBlank(orgId)) { + return ActionResult.fail("考勤组未绑定组织"); + } + + // 创建DTO并设置参数 + QueryBasePositionBatchDTO dto = new QueryBasePositionBatchDTO(); + dto.setOrganizeIds(Collections.singletonList(orgId)); + + // 调用Feign客户端获取岗位基础信息 + return v2PositionApi.listPositionBaseInfo(dto); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLocationSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLocationSettingController.java new file mode 100644 index 0000000..d840037 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceLocationSettingController.java @@ -0,0 +1,122 @@ +package jnpf.attendance.controller; + + +import jnpf.attendance.service.AttendanceLocationSettingService; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceLocationSettingDto; +import jnpf.model.attendance.dto.SaveForStoreDto; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.AttendanceLocationSettingVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + *

+ * 考勤组-考勤点配置表 前端控制器 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@RestController +@RequestMapping("/location/setting") +public class AttendanceLocationSettingController { + @Autowired + private AttendanceLocationSettingService attendanceLocationSettingService; + + /** + * 根据考勤组ID和类型查询考勤地点设置列表。 + * + * @param groupId 考勤组ID + * @param type 类型(用于筛选设置) + * @return 考勤地点设置视图对象列表 + */ + @GetMapping + public ActionResult> findList(@RequestParam String groupId, @RequestParam Integer type) { + return ActionResult.success(attendanceLocationSettingService.findList(groupId, type)); + } + + /** + * 根据门店信息保存考勤点数据 + * @param saveForStore + */ + @PostMapping("saveForStore") + public ActionResult saveForStore(@RequestBody SaveForStoreDto saveForStore){ + attendanceLocationSettingService.saveForStore(saveForStore); + return ActionResult.success(); + } + /** + * 根据门店id集合删除考勤点信息 + * @param saveForStore + */ + @DeleteMapping("delForStore") + public ActionResult delForStore(@RequestBody SaveForStoreDto saveForStore){ + attendanceLocationSettingService.delForStore(saveForStore); + return ActionResult.success(); + } + /** + * 获取当前考勤组考勤点关联门店id + * @param groupId + * @return + */ + @GetMapping("getStoreIds") + public ActionResult> getStoreIds(@RequestParam String groupId){ + return ActionResult.success(attendanceLocationSettingService.getStoreIds(groupId)); + } + + /** + * 获取启用的考勤组信息。 + * + * @param groupId 考勤组ID + * @return 启用的考勤组视图对象列表 + */ + @GetMapping("getEnableGroups") + public ActionResult> getEnableGroups(@RequestParam String groupId) { + return ActionResult.success(attendanceLocationSettingService.getEnableGroup(groupId)); + } + + /** + * 保存考勤地点设置信息。 + * + * @param dto 考勤地点设置数据传输对象 + * @throws HandleException 处理异常 + * @return 处理结果视图对象 + */ + @PostMapping + public ActionResult save(@RequestBody AttendanceLocationSettingDto dto) throws HandleException { + attendanceLocationSettingService.save(dto); + return ActionResult.success(); + } + + /** + * 删除指定的考勤地点设置信息。 + * + * @param id 考勤地点设置ID + * @return 处理结果视图对象 + */ + @DeleteMapping("{id}") + public ActionResult del(@PathVariable String id) { + attendanceLocationSettingService.del(id); + return ActionResult.success(); + } + + /** + * 更改考勤地点设置的启用状态。 + * + * @param groupId 考勤组ID + * @param type 类型(用于区分设置类型) + * @param enable 启用状态(0禁用,1启用) + * @return 处理结果视图对象 + */ + @PutMapping("changeStatus") + public ActionResult changeStatus(@RequestParam String groupId, @RequestParam Integer type, @RequestParam Integer enable) { + attendanceLocationSettingService.changeStatus(groupId, type, enable); + return ActionResult.success(); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceMachineManageController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceMachineManageController.java new file mode 100644 index 0000000..ed017b8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceMachineManageController.java @@ -0,0 +1,163 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.AttendanceMachineManageService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.dto.PageDto; +import jnpf.model.attendance.dto.MachineDto; +import jnpf.model.attendance.dto.MachineQueryDto; +import jnpf.model.attendance.dto.MachineUpdateDto; +import jnpf.model.attendance.vo.AttendanceMachineManageVo; +import jnpf.model.attendance.vo.MachineScopeVo; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import jnpf.model.attendance.vo.attendance.GroupUserMiniVo; +import jnpf.model.attendance.vo.attendance.LogMiniVo; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 考勤机管理 + * + * @author yanwenfu + * @create 2024-09-10 + */ +@RestController +@RequestMapping(value = "/machineManage") +public class AttendanceMachineManageController { + + @Resource + private AttendanceMachineManageService attendanceMachineManageService; + + /** + * 考勤机管理 - 考勤组列表 + * @param machineId 考勤机id + * @return java.lang.Object + */ + @GetMapping(value = "/group/list") + public Object getGroupList(@RequestParam(required = false) String machineId) { + + List list = attendanceMachineManageService.getGroupList(machineId); + return ActionResult.success(list); + } + + /** + * 考勤机管理 - 考勤机列表 + * @param queryDto 查询条件 + * @return java.lang.Object + */ + @GetMapping(value = "/machine/list") + public Object getMachineList(MachineQueryDto queryDto) { + + PageListVO page = attendanceMachineManageService.getMachineList(queryDto); + return ActionResult.page(page.getList(), page.getPagination()); + } + + /** + * 考勤机管理 - 查询考勤机更新信息 + * @param id 考勤机id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/update-detail/{id}") + public ActionResult getUpdateDetail(@PathVariable(value = "id") String id) { + + MachineScopeVo machineScope = attendanceMachineManageService.getUpdateDetail(id); + return ActionResult.success(machineScope); + } + + /** + * 考勤机管理 - 添加考勤机 + * @param machineDto 考勤机信息 + * @return java.lang.Object + */ + @PostMapping(value = "/machine") + public Object addMachine(@RequestBody @Valid MachineDto machineDto) throws Exception { + + attendanceMachineManageService.addMachine(machineDto); + return ActionResult.success(); + } + + /** + * 考勤机管理 - 编辑考勤机 + * @param id 考勤机id + * @param machineUpdateDto 考勤机信息 + * @return java.lang.Object + */ + @PutMapping(value = "/machine/{id}") + public Object updateMachine(@PathVariable(value = "id") String id, @RequestBody @Valid MachineUpdateDto machineUpdateDto) throws Exception { + + attendanceMachineManageService.updateMachine(id, machineUpdateDto); + return ActionResult.success(); + } + + /** + * 考勤机管理 - 移除 + * @param id 考勤机id + * @return java.lang.Object + */ + @DeleteMapping(value = "/machine/{id}") + public Object deleteMachine(@PathVariable(value = "id") String id) throws Exception { + + attendanceMachineManageService.deleteMachine(id); + return ActionResult.success(); + } + + /** + * 考勤机管理 - 考勤组成员列表 + * @param groupId 考勤组id + * @return java.lang.Object + */ + @GetMapping(value = "/group/user/{groupId}") + public Object getGroupUserList(@PathVariable(value = "groupId") String groupId) { + + List list = attendanceMachineManageService.getGroupUserList(groupId); + return ActionResult.success(list); + } + + @GetMapping(value = "/group/user") + public Object getGroupUserList2(@RequestParam(value = "groupId") String groupId) { + + List list = attendanceMachineManageService.getGroupUserList(groupId); + return ActionResult.success(list); + } + + /** + * 同步考勤机成员 + * @param id 考勤机id + * @return java.lang.Object + */ + @PutMapping("/sync/{id}") + public Object syncMachineMemberData(@PathVariable(value = "id") String id) { + + List list = attendanceMachineManageService.syncMachineMemberData(id); + return ActionResult.success(list); + } + + /** + * 考勤机管理 - 人脸库 + * @param id 考勤机id + * @return java.lang.Object + */ + @GetMapping(value = "/member/list/{id}") + public Object getMachineMemberList(@PathVariable(value = "id") String id) { + + List list = attendanceMachineManageService.getMachineMemberList(id); + return ActionResult.success(list); + } + + /** + * 查询考勤机打卡记录(查往前3个月的数据) + * @param id 考勤机id + * @return java.lang.Object + */ + @GetMapping(value = "/record/list/{id}") + public ActionResult> getLogList(@PathVariable(value = "id") String id, PageDto pageQuery) { + + PageInfo page = attendanceMachineManageService.getLogList(id, pageQuery); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceNoticeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceNoticeController.java new file mode 100644 index 0000000..7e91598 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceNoticeController.java @@ -0,0 +1,64 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.service.AttendanceNoticeService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.NoticeContentInfoDto; +import jnpf.model.attendance.vo.NoticeConfirmListVo; +import jnpf.model.attendance.vo.NoticeContentInfoVo; +import jnpf.util.CustomTenantUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import javax.websocket.server.PathParam; + +/** + * 考勤消息通知 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-08-08 10:49:41 + */ +@Slf4j +@RestController +@RequestMapping("/attendance/notice") +public class AttendanceNoticeController { + @Resource + private CustomTenantUtil customTenantUtil; + @Autowired + private AttendanceNoticeService attendanceNoticeService; + + + @Operation(summary = "内容详情") + @GetMapping(value = "/getContentInfo") + public ActionResult getContentInfo(@Valid NoticeContentInfoDto req) { + try { + log.info("内容详情,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + NoticeContentInfoVo data = attendanceNoticeService.getContentInfo(req); + long ent = System.currentTimeMillis(); + log.info("内容详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(data); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + @Operation(summary = "考勤通知-确认") + @PostMapping(value = "/confirm/{id}") + public ActionResult noticeConfirm(@PathVariable("id") String id) { + attendanceNoticeService.noticeConfirm(id); + return ActionResult.success(); + } + + @Operation(summary = "考勤通知-确认列表") + @GetMapping(value = "confirmList/{id}") + public ActionResult getNoticeConfirmList(@PathVariable("id") String id) { + return ActionResult.success(attendanceNoticeService.getNoticeConfirmList(id)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendancePermissionDictController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendancePermissionDictController.java new file mode 100644 index 0000000..3391219 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendancePermissionDictController.java @@ -0,0 +1,71 @@ +package jnpf.attendance.controller; + +import jnpf.attendance.service.AttendancePermissionDictService; +import jnpf.base.ActionResult; +import jnpf.entity.PermissionDict; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.SavePermissionDto; +import jnpf.model.attendance.vo.PermissionDictVo; +import jnpf.util.ParamUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 权限字典管理 + */ +@RestController +@RequestMapping("/dict") +public class AttendancePermissionDictController { + + @Resource + private AttendancePermissionDictService attendancePermissionDictService; + + /** + * 保存权限字典信息 + * @param savePermissionDto + * @return ActionResult + * @throws Exception + */ + @PostMapping + public ActionResult save(@RequestBody SavePermissionDto savePermissionDto) throws Exception { + ParamUtil.checkParam(savePermissionDto); + attendancePermissionDictService.save(savePermissionDto); + return ActionResult.success(); + } + + /** + * 权限字典查询列表 + * @param moduleType + * @return ActionResult> + */ + @GetMapping + public ActionResult> query(Integer moduleType) { + List result = attendancePermissionDictService.queryPermissionDictList(moduleType); + return ActionResult.success(result); + } + + /** + * 根据id删除权限字典 + * @param id 权限字典id + * @return ActionResult + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable String id) { + attendancePermissionDictService.delete(id); + return ActionResult.success(); + } + + /** + * 权限详情 + * @param id + * @return ActionResult + */ + @GetMapping("/{id}") + public ActionResult detail(@PathVariable String id) { + PermissionDict permissionDict = attendancePermissionDictService.detail(id); + return ActionResult.success(permissionDict); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceQuickTemplateController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceQuickTemplateController.java new file mode 100644 index 0000000..36b3e3b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceQuickTemplateController.java @@ -0,0 +1,100 @@ +package jnpf.attendance.controller; + + +import jnpf.attendance.service.AttendanceQuickTemplateService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.FixedClassGroupDto; +import jnpf.model.attendance.dto.QuickTemDto; +import jnpf.model.attendance.dto.QuickTemplateDto; +import jnpf.model.attendance.vo.AttendanceQuickTemplateVo; +import jnpf.model.attendance.vo.attendance.QuickTemVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + *

+ * 考勤配置-快速模板 前端控制器 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +@RestController +@RequestMapping("/quick/template") +public class AttendanceQuickTemplateController { + + @Autowired + private AttendanceQuickTemplateService attendanceQuickTemplateService; + + /** + * 根据考勤组ID查询快速模板列表。 + * + * @param groupId 考勤组ID + * @return 快速模板视图对象列表 + */ + @GetMapping + public ActionResult> findList(@RequestParam String groupId) { + List list = attendanceQuickTemplateService.findList(groupId); + return ActionResult.success(list); + } + + /** + * 根据快速模板ID查询单个快速模板。 + * + * @param id 快速模板ID + * @return 快速模板视图对象 + */ + @GetMapping("{id}") + public ActionResult findOne(@PathVariable String id) { + return ActionResult.success(attendanceQuickTemplateService.findOne(id)); + } + + /** + * 保存快速模板信息。 + * + * @param quickTemplateDto 快速模板数据传输对象 + * @return 处理结果视图对象 + */ + @PostMapping + public ActionResult save(@RequestBody QuickTemplateDto quickTemplateDto) { + attendanceQuickTemplateService.save(quickTemplateDto); + return ActionResult.success(); + } + + /** + * 删除指定的快速模板信息。 + * + * @param id 快速模板ID + * @return 处理结果视图对象 + */ + @DeleteMapping("{id}") + public ActionResult del(@PathVariable String id) { + attendanceQuickTemplateService.del(id); + return ActionResult.success(); + } + + + /** + * 新版考勤快速排班模板 + * @param dto 快速排班信息 + */ + @PostMapping("/new") + public ActionResult saveNew(@RequestBody @Valid QuickTemDto dto) { + attendanceQuickTemplateService.saveOrUpdateNew(dto); + return ActionResult.success(); + } + + /** + * 新版考勤快速排班列表 + * @param groupId 考勤组Id + */ + @GetMapping("/new") + public ActionResult> findNewList(@RequestParam String groupId) { + List list = attendanceQuickTemplateService.findNewList(groupId); + return ActionResult.success(list); + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftNameSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftNameSettingController.java new file mode 100644 index 0000000..21dd540 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftNameSettingController.java @@ -0,0 +1,89 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.AttendanceShiftNameSettingService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.FixedClassChangeStatusDto; +import jnpf.model.attendance.dto.FixedClassGroupDto; +import jnpf.model.attendance.dto.ShiftNameDto; +import jnpf.model.attendance.dto.ShiftNameQueryDto; +import jnpf.model.attendance.vo.attendance.ShiftNameListVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + + +/** + * 考勤组配置-考勤班制 前端控制器 + * @Author huanglinpan + * @Date 2024/5/9 10:01 + * @Version 1.0 (版本号) + */ +@RestController +@RequestMapping("/shifts/shiftName") +public class AttendanceShiftNameSettingController { + + @Resource + private AttendanceShiftNameSettingService attendanceShiftNameSettingService; + + /** + * 新增/修改班次名称 + * @param dto 班次名称信息 + */ + @PostMapping + public ActionResult save(@RequestBody @Valid ShiftNameDto dto) throws HandleException { + return attendanceShiftNameSettingService.save(dto); + } + + /** + * 起停班次 + * @param statusDto 班次状态修改dto + */ + @PutMapping("/changeStatus") + public ActionResult changeStatus(@RequestBody FixedClassChangeStatusDto statusDto) { + return attendanceShiftNameSettingService.changeStatus(statusDto.getShiftNameId(), statusDto.getEnable(), statusDto.getUpdateShift()); + } + + /** + * 删除班次 + * @param shiftNameId 班次名称Id + */ + @DeleteMapping("/{shiftNameId}") + public ActionResult delete(@PathVariable("shiftNameId") String shiftNameId) { + return attendanceShiftNameSettingService.delete(shiftNameId); + } + + /** + * 获取班次详情 + * @param shiftNameId 班次名称Id + */ + @GetMapping("{shiftNameId}") + public ActionResult getDetail(@PathVariable("shiftNameId") String shiftNameId) { + return ActionResult.success(attendanceShiftNameSettingService.getDetail(shiftNameId)); + } + + /** + * 班次列表 + * @param queryDto 查询条件 + * @return + */ + @GetMapping("/list") + public ActionResult> getList(@Valid ShiftNameQueryDto queryDto) throws HandleException { + PageInfo list = attendanceShiftNameSettingService.getList(queryDto); + return ActionResult.page(list.getList(), FtbUtil.getPagination(list)); + } + + /** + * 保存考勤组固定排班 + * @param dto 固定排班对象 + */ + @PutMapping("/fixedClass") + public ActionResult saveOrUpdateFixedClass(@RequestBody @Valid FixedClassGroupDto dto) { + return attendanceShiftNameSettingService.saveOrUpdateFixedClass(dto); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftSettingController.java new file mode 100644 index 0000000..33ad6f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceShiftSettingController.java @@ -0,0 +1,99 @@ +package jnpf.attendance.controller; + + +import jnpf.attendance.service.AttendanceShiftSettingService; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceShiftDto; +import jnpf.model.attendance.vo.AttendanceShiftSettingVo; +import jnpf.model.attendance.vo.ShiftPeriodVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + *

+ * 考勤组配置-考勤配置 前端控制器 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +@RestController +@RequestMapping("/shifts/setting") +public class AttendanceShiftSettingController { + @Autowired + private AttendanceShiftSettingService attendanceShiftSettingService; + + /** + * 根据考勤组ID查询班次设置。 + * + * @param groupId 考勤组ID + * @return 班次设置视图对象 + */ + @GetMapping("{groupId}") + public ActionResult findByGroupId(@PathVariable("groupId") String groupId) { + return ActionResult.success(attendanceShiftSettingService.findByGroupId(groupId)); + } + + /** + * 保存班次设置信息。 + * + * @param dto 班次设置数据传输对象 + * @throws HandleException 处理异常 + * @return 处理结果视图对象 + */ + @PostMapping + public ActionResult save(@RequestBody AttendanceShiftDto dto) throws HandleException { + attendanceShiftSettingService.save(dto); + return ActionResult.success(); + } + + /** + * 删除指定的班次周期。 + * + * @param periodId 班次周期ID + * @return 处理结果视图对象 + */ + @DeleteMapping("/period/{periodId}") + public ActionResult delPeriod(@PathVariable("periodId") String periodId) { + attendanceShiftSettingService.delPeriod(periodId); + return ActionResult.success(); + } + + /** + * 更改班次设置的启用状态。 + * + * @param groupId 考勤组ID + * @param enable 启用状态(0禁用,1启用) + * @return 处理结果视图对象 + */ + @PutMapping("changeStatus") + public ActionResult changeStatus(@RequestParam String groupId, @RequestParam Integer enable) { + attendanceShiftSettingService.changeStatus(groupId, enable, null); + return ActionResult.success(); + } + + /** + * 根据考勤组ID获取班次时段列表。 + * + * @param groupId 考勤组ID + * @return 班次周期视图对象列表 + */ + @GetMapping("/period/list") + public ActionResult> periodList(@RequestParam("groupId") String groupId) { + return ActionResult.success(attendanceShiftSettingService.periodList(groupId)); + } + /** + * 自我排班的班次列表 + * + * @return + */ + @GetMapping("/self/period/list") + public ActionResult> periodListForSelfScheduling() { + return ActionResult.success(attendanceShiftSettingService.periodListForSelfScheduling()); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSimulatedDataController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSimulatedDataController.java new file mode 100644 index 0000000..4fb8eaa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSimulatedDataController.java @@ -0,0 +1,76 @@ +package jnpf.attendance.controller; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.AttendanceSimulateDataApi; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.UserFaceService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.util.NoDataSourceBind; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 考勤打卡控制器 + * + * @author yanwenfu + * @create 2023-11-21 + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/simulateData") +public class AttendanceSimulatedDataController implements AttendanceSimulateDataApi { + + @Resource + private AttenceMachineService attendanceClockInService; + @Autowired + private AttendanceUserService attendanceUserService; + @Autowired + private UserFaceService userFaceService; + + /** + * 打卡 + * @return java.lang.Object + */ + @GetMapping(value = "/clockIn") + @NoDataSourceBind + public void clockIn(@RequestParam(value = "tenantId") String tenantId) throws Exception { + Assert.notNull(tenantId, "租户ID不能为空"); + TenantDataSourceUtil.switchTenant(tenantId); + List attendanceGroupUsers = attendanceUserService.queryByUsersAndGroupFilterSecondment(new Date(), new Date(), null, null); + UserProvider.getUser().setTenantId(tenantId); + Map> userFaceList = userFaceService.getUserFaceList(attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()), tenantId); + attendanceGroupUsers.forEach(attendanceGroupUser -> { + try { + List attendanceMachineManages = userFaceList.get(attendanceGroupUser.getUserId()); + if(CollUtil.isEmpty(attendanceMachineManages)){ + return; + } + AttendanceMachineManage attendanceMachineManage = attendanceMachineManages.stream().findFirst().orElse(null); + if(Objects.isNull(attendanceMachineManage)){ + return; + } + attendanceClockInService.clockIn(attendanceMachineManage.getMac(), attendanceGroupUser.getUserId(), tenantId); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSuperAdminController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSuperAdminController.java new file mode 100644 index 0000000..841621c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceSuperAdminController.java @@ -0,0 +1,242 @@ +package jnpf.attendance.controller; + +import jnpf.attendance.service.AttendanceApprovalSettingService; +import jnpf.attendance.service.AttendanceSuperAdminService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.AttendanceManagerDetailVo; +import jnpf.model.attendance.vo.AttendanceUserVo; +import jnpf.model.attendance.vo.CurUserPermissionVo; +import jnpf.model.attendance.vo.permission.ActionPermissionVo; +import jnpf.model.attendance.vo.permission.ApprovalSettingVo; +import jnpf.model.attendance.vo.permission.AttendanceTeamSetVo; +import jnpf.model.attendance.vo.permission.app.ManagerPermissionVo; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; +import jnpf.util.ParamUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * APP全局设置 + * @author yier + * @Time 2023-11-23 + */ +@RestController +@RequestMapping("/permission") +public class AttendanceSuperAdminController { + + @Resource + private AttendanceSuperAdminService attendanceSuperAdminService; + @Resource + private AttendanceApprovalSettingService attendanceApprovalSettingService; + + + /** + * 添加考勤组超级管理员 + * @param saveSuperAdminDto 保存信息 + * @return Void + * @throws Exception 抛出异常 + */ + @PostMapping + public ActionResult add(@RequestBody SaveSuperAdminDto saveSuperAdminDto) throws Exception { + ParamUtil.checkParam(saveSuperAdminDto); + attendanceSuperAdminService.add(saveSuperAdminDto); + return ActionResult.success(); + } + + /** + * 删除超级管理员 + * @param userIds 用户id + * @return ActionResult + */ + @DeleteMapping + public ActionResult delete(@RequestParam List userIds) { + attendanceSuperAdminService.delete(userIds); + return ActionResult.success(); + } + + /** + * 获取考勤超级管理员 + * @return ActionResult> + */ + @GetMapping + public ActionResult> getSuperAdmin(String name) { + return ActionResult.success(attendanceSuperAdminService.querySuperAdmin(name)); + } + + /** + * 添加考勤组管理员 + * @param saveGroupAdmin + * @return ActionResult + */ + @PostMapping("/addGroupAdmin") + public ActionResult addGroupAdmin(@RequestBody SaveGroupAdmin saveGroupAdmin) throws Exception { + ParamUtil.checkParam(saveGroupAdmin); + attendanceSuperAdminService.addGroupAdmin(saveGroupAdmin); + return ActionResult.success(); + } + + /** + * 批量添加考勤组管理员 + * @param groupAdminList + * @return ActionResult + */ + @PostMapping("/batchAddGroupAdmin") + public ActionResult batchAddGroupAdmin(@RequestBody BatchSaveGroupAdmin groupAdminList) throws Exception { + ParamUtil.checkParam(groupAdminList); + attendanceSuperAdminService.batchAddGroupAdmin(groupAdminList); + return ActionResult.success(); + } + + + /** + * 修改考勤组管理员权限 + * @param saveGroupAdmin + * @return + * @throws Exception + */ + @PutMapping("/updateGroupAdmin") + public ActionResult updateGroupAdmin(@RequestBody SaveGroupAdmin saveGroupAdmin) throws Exception { + ParamUtil.checkParam(saveGroupAdmin); + attendanceSuperAdminService.updateGroupAdmin(saveGroupAdmin); + return ActionResult.success(); + } + + /** + * 删除考勤组管理员 + * @param saveGroupAdmin + * @return ActionResult + */ + @DeleteMapping("/deleteGroupAdmin") + public ActionResult deleteGroupAdmin(@RequestBody SaveGroupAdmin saveGroupAdmin) { + attendanceSuperAdminService.deleteGroupAdmin(saveGroupAdmin); + return ActionResult.success(); + } + + /** + * 查询考勤组管理员列表 + * @param groupId 考勤组id + * @return ActionResult> + */ + @GetMapping("/listGroupAdmin") + public ActionResult> listGroupAdmin(String groupId) { + Map result = attendanceSuperAdminService.listGroupAdmin(groupId); + return ActionResult.success(result); + } + + /** + * 获取当前登录用户权限 + */ + @GetMapping("/getCurPermission") + public ActionResult getCurPermission(String groupId) { + CurUserPermissionVo curUserPermissionVo = attendanceSuperAdminService.getByUserId(groupId); + return ActionResult.success(curUserPermissionVo); + } + + /** + * 修改审批设置 + * @param attendanceApprovalSettingDto + * @return ActionResult + */ + @PostMapping("/updateApprovalSetting") + public ActionResult updateApprovalSetting(@RequestBody AttendanceApprovalSettingDto attendanceApprovalSettingDto) { + attendanceApprovalSettingService.update(attendanceApprovalSettingDto); + return ActionResult.success(); + } + + /** + * 管理员权限详情 + * @param groupId + * @param userId + * @return ActionResult> + */ + @GetMapping("/adminDetail") + public ActionResult adminDetail(String groupId, String userId) { + AttendanceManagerDetailVo dictVos = attendanceSuperAdminService.adminDetail(groupId, userId); + return ActionResult.success(dictVos); + } + + /** + * 是否有查看权限 + * @return ActionResult + */ + @GetMapping("/isView") + public ActionResult isView(String groupId) { + Boolean viewPermission = attendanceSuperAdminService.isViewPermission(groupId); + return ActionResult.success(viewPermission); + } + + /** + * 获取操作权限 + * @return ActionResult + */ + @GetMapping("/actionPermission") + public ActionResult actionPermission(String groupId) { + ActionPermissionVo actionPermission = attendanceSuperAdminService.getActionPermission(groupId); + return ActionResult.success(actionPermission); + } + + /** + * 是否有全局设置权限 + * @return ActionResult + */ + @GetMapping("/isGlobal") + public ActionResult isGlobal() { + Boolean globalSetting = attendanceSuperAdminService.isGlobalSetting(); + return ActionResult.success(globalSetting); + } + + /** + * 是否是考勤组管理员 + * @return ActionResult + */ + @GetMapping("/isManager") + public ActionResult isManager() { + ManagerPermissionVo manager = attendanceSuperAdminService.isManager(); + return ActionResult.success(manager); + } + + /** + * 获取考勤组审批设置 + * @return ActionResult + */ + @GetMapping("/getGroupApprovalPermission") + public ActionResult getGroupApprovalPermission(@RequestParam("groupId") String groupId, + @RequestParam("type") Integer type) { + ApprovalSettingVo approvalSettingInfo = attendanceSuperAdminService.getApprovalSettingInfo(groupId, type); + return ActionResult.success(approvalSettingInfo); + } + + /** + * 根据用户id获取所属权限集合 + * @param userIds 用户id + * @return FtbPermissionPositionMenuInnerVO + */ + @PostMapping("/queryPermissionListByUserIds") + public ActionResult> queryPermissionListByUserIds(@RequestBody List userIds) { + List voList = attendanceSuperAdminService.queryPermissionListByUserIds(userIds); + return ActionResult.success(voList); + } + + /** + * 获取考勤组团队设置 + * @return ActionResult + */ + @GetMapping("/getTeamSet") + public ActionResult getTeamSet(@Valid GroupFilterDto dto) { + return ActionResult.success(attendanceSuperAdminService.getTeamSet(dto)); + } + + /** + * 考勤组月报通知开启/关闭 + * @return ActionResult + */ + @PutMapping("/setMonthNotice") + public ActionResult setMonthNotice(@Valid @RequestBody GroupFilterDto dto) { + return ActionResult.success(attendanceSuperAdminService.setMonthNotice(dto)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserBalanceController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserBalanceController.java new file mode 100644 index 0000000..00bd806 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserBalanceController.java @@ -0,0 +1,71 @@ +package jnpf.attendance.controller; + + +import jnpf.attendance.service.AttendanceUserBalanceService; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceUserBalanceDto; +import jnpf.model.attendance.dto.AttendanceUserBalanceListQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceUserBalanceListVo; +import jnpf.model.attendance.vo.attendance.AttendanceUserBalanceVo; +import jnpf.model.attendance.vo.attendance.AttendanceUserTitleVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + + +/** + * 用户余额 + * @author 盼盼 + * @create 2025-09-18 + */ +@RestController +@RequestMapping(value = "/attendance/user-balance") +@Slf4j +public class AttendanceUserBalanceController { + + @Autowired + private AttendanceUserBalanceService attendanceUserBalanceService; + + /** + * 获取考勤余额管理列表 + * @param queryDto 余额名称模糊查询非必传 + */ + @PostMapping("/list") + public ActionResult list(@RequestBody @Valid AttendanceUserBalanceListQueryDto queryDto) throws HandleException { + AttendanceUserBalanceListVo vo = attendanceUserBalanceService.list(queryDto); + return ActionResult.success(vo); + } + + /** + * 编辑、批量编辑余额 + * @param userBalanceDto 余额 + */ + @PutMapping() + public ActionResult updateUserBalance(@RequestBody AttendanceUserBalanceDto userBalanceDto) { + attendanceUserBalanceService.updateUserBalance(userBalanceDto); + return ActionResult.success(); + } + + /** + * 获取余额详情 + * @param userBalanceDto 用户Id及类型Id(类型Id没有默认查调休) + */ + @GetMapping("/detail") + public ActionResult getDetail(AttendanceUserBalanceDto userBalanceDto) { + return ActionResult.success(attendanceUserBalanceService.getDetail(userBalanceDto)); + } + + /** + * 获取余额详情页-顶部用户相关及动态假期表头 + * @param userBalanceDto 用户Id及类型Id(类型Id没有默认查调休) + */ + @GetMapping("/title") + public ActionResult getTitle(AttendanceUserBalanceDto userBalanceDto) { + return ActionResult.success(attendanceUserBalanceService.getTitle(userBalanceDto)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserSettingController.java new file mode 100644 index 0000000..a7375d3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/AttendanceUserSettingController.java @@ -0,0 +1,88 @@ +package jnpf.attendance.controller; + + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.service.AttendanceUserSettingService; +import jnpf.base.ActionResult; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AppUserSettingQueryDto; +import jnpf.model.attendance.dto.AttendanceAppUserSettingDto; +import jnpf.model.attendance.vo.UserSettingVo; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * app用户个人及考勤组设置 + * @Author huanglinpan + * @Date 2024/8/8 10:53 + * @Version 1.0 (版本号) + */ +@RestController +@RequestMapping(value = "/attendance/app") +@Slf4j +public class AttendanceUserSettingController { + + @Resource + private AttendanceUserSettingService attendanceUserSettingService; + @Resource + private UserProvider userProvider; + + /** + * 修改设置 + * @param attendanceAppUserSettingDto 设置信息 + */ + @PutMapping("/setting") + public ActionResult saveOrUpdate(@RequestBody AttendanceAppUserSettingDto attendanceAppUserSettingDto) { + attendanceUserSettingService.saveOrUpdate(attendanceAppUserSettingDto); + return ActionResult.success(); + } + + /** + * 批量保存设置 + * @param attendanceAppUserSettingDto 保存的对象 + */ + @PutMapping("/saveOrUpdateList") + public ActionResult saveOrUpdateList(@RequestBody List attendanceAppUserSettingDto) { + attendanceUserSettingService.saveOrUpdateList(attendanceAppUserSettingDto); + return ActionResult.success(); + } + + /** + * 查询设置列表 + * @param appUserSettingQueryDto 查询信息 + */ + @GetMapping("/setting") + public ActionResult> getList(AppUserSettingQueryDto appUserSettingQueryDto) { + return ActionResult.success(attendanceUserSettingService.getList(appUserSettingQueryDto)); + } + + /** + * 获取用户上下班极速打卡配置 + */ + @GetMapping("/userSetting") + public ActionResult> getUserSettingList() { + return ActionResult.success(attendanceUserSettingService.getSettingList(CollUtil.newArrayList(userProvider.get().getUserId()),1, Stream.of(UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode(), + UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode()) + .collect(Collectors.toList()))); + } + + /*****************************************考勤V1.9************************************************/ + + /** + * v1.9查询设置列表 + * + * 因考虑兼容,导致原本的优化接口变为赋值接口并优化逻辑 + * @param appUserSettingQueryDto 查询信息 + */ + @GetMapping("/settingNew") + public ActionResult> getListNew(AppUserSettingQueryDto appUserSettingQueryDto) { + return ActionResult.success(attendanceUserSettingService.getListNew(appUserSettingQueryDto)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/FfiMachineController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/FfiMachineController.java new file mode 100644 index 0000000..350310e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/FfiMachineController.java @@ -0,0 +1,92 @@ +package jnpf.attendance.controller; + +import jnpf.util.CustomTenantUtil; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 英泰斯达考勤机控制器 + * + * @author yanwenfu + * @create 2024-03-28 + */ +@RestController +@Slf4j +@RequestMapping(value = "/ffi") +public class FfiMachineController { + + @Autowired + private CustomTenantUtil customTenantUtil; + + /** + * 设备录入自定义编号判断 + * @return java.util.Map + */ + @PostMapping(consumes = "application/octet-stream") + @NoDataSourceBind + public Map addFace(HttpServletRequest request) { + + String requestCode = request.getHeader("request_code"); + String devId = request.getHeader("dev_id"); + String contentLength = request.getHeader("Content-Length"); + log.error("requestCode: {}, devId: {}, contentLength: {}", requestCode, devId, contentLength); + int contentLength2 = request.getContentLength(); + log.error("contentLength2: {}", contentLength2); + // 获取请求体的ServletInputStream + String body; + try { + // 读取二进制流(这里需要自己实现) + ServletInputStream inputStream = request.getInputStream(); + byte[] buffer = new byte[contentLength2]; + int bytesRead = 0; + while (bytesRead < contentLength2) { + int bytes = inputStream.read(buffer, bytesRead, contentLength2 - bytesRead); + if (bytes == -1) { + break; + } + bytesRead += bytes; + } + log.error("buffer bytes: {}", Arrays.toString(buffer)); + body = getJsonBlock(buffer); + } catch (IOException e) { + log.error(e.getMessage()); + return null; + } + log.error("body: {}", body); + Map map = new HashMap<>(); + map.put("Result", 0); + map.put("Msg", "进入方法..."); + return map; + } + + private String getJsonBlock(byte[] buffer) { + + String str = ""; + if (buffer.length < 4) { + return str; + } + int lenText = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (lenText > buffer.length - 4 || lenText == 0) { + return str; + } + str = new String(buffer, 4, lenText, StandardCharsets.UTF_8); + if (buffer[4 + lenText - 1] == 0) { + str = new String(buffer, 4, lenText - 1, StandardCharsets.UTF_8); + } + return str; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/InitializationController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/InitializationController.java new file mode 100644 index 0000000..7bbe30b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/InitializationController.java @@ -0,0 +1,54 @@ +package jnpf.attendance.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.InitializationService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.ApproveException; +import jnpf.exception.LoginException; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 初始化控制器 + * @Author huanglinpan + * @Date 2024/7/1 9:14 + * @Version 1.0 (版本号) + */ +@RestController +@RequestMapping("/config") +@Tag(name = "初始化", description = "initialization") +public class InitializationController { + + + @Autowired + private ConfigValueUtil configValueUtil; + + + @Resource + private InitializationService initializationService; + + /** + * 初始化存休 有效期考勤v1.3版本 + */ + @GetMapping(value = "/storageRest") + @Deprecated + public ActionResult storageRest() { + Integer i = initializationService.storageRest(); + + return ActionResult.success("初始化成功"+i+"条数据"); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/IsPerfMachineController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/IsPerfMachineController.java new file mode 100644 index 0000000..3c94fd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/IsPerfMachineController.java @@ -0,0 +1,190 @@ +package jnpf.attendance.controller; + +import cn.hutool.json.JSONObject; +import jnpf.attendance.annotation.Machine; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.IsPerfMachineService; +import jnpf.attendance.service.MachineStrategy; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.model.attendance.dto.SecondCheckDto; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.ConstantUtil; +import jnpf.util.CustomTenantUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.RedisUtil; +import jnpf.util.TenantUtil; +import jnpf.util.attendance.MachineStrategyFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 开架易控制器 + * + * @author yanwenfu + * @create 2024-04-01 + */ +@RestController +@Slf4j +public class IsPerfMachineController { + + @Resource + private IsPerfMachineService isPerfMachineService; + + @Resource + private AttenceMachineService attenceMachineService; + + @Resource + private MachineStrategyFactory machineStrategyFactory; + + @Autowired + private CustomTenantUtil tenantUtil; + + @Autowired + private RedisUtil redisUtil; + + @GetMapping(value = "/main") + public Object welcome() { + + // 初始化netty服务器, 设备登录在 应用设置--其他设置--通信设置 填入服务器 ip:port + isPerfMachineService.welcome(); + return ActionResult.success("netty绑定成功"); + } + + @PostMapping(value = "/callback") + @NoDataSourceBind + public Object callback(@RequestBody Map params) { + + log.error("callback init ..."); + /*String userNo = params.get("userId").toString(); + log.info("callback in : {}", params.toString()); + JSONObject json = new JSONObject(params.get("extra")); + String tenantId = json.get("tenantId").toString(); + String userId = json.get("userId").toString(); + tenantUtil.checkOutTenant(tenantId, userId); + String sn = params.get("mac").toString(); + String b = attenceMachineService.clockIn(sn, userId, tenantId); + // log.error(b); + json.clear(); + // 将结果保存到redis 0: 打卡失败, 1: 正常, 2: 迟到, 3: 早退 + redisUtil.insert(ConstantUtil.MACHINE_KEY + sn + "-" + userNo, "locked", 5L);*/ + Map map = new HashMap<>(); + map.put("code", 1); + map.put("flag", 1); + map.put("tips", ""); + return map; + } + + /** + * 开架易 - 二次校验 + * @param params 二次校验dto + * @return java.lang.Object + */ + @PostMapping(value = "/kips/secondCheck") + @NoDataSourceBind + @Machine(dealAction = ActionEnum.DA_KA, factory = MachineEnum.KAI_JIA_YI) + public Object getClockInResult(@RequestBody Map params) { + + int code = 0; + int flag = 0; + log.error("callback in : {}", params.toString()); + Map map = new HashMap<>(); + // 解析入参 + String userNo = params.get("vipID").toString(); + JSONObject json = new JSONObject(params.get("extra")); + String tenantId = json.get("tenantId").toString(); + String userId = json.get("userId").toString(); + String sn = params.get("mac").toString(); + // 防止重复提交, 查看redis是否还在限制打卡 + String key = ConstantUtil.MACHINE_KEY + sn + "-" + userNo; + if (redisUtil.exists(key)) { + map.put("code", code); + map.put("flag", flag); + map.put("tips", ""); + return map; + } + tenantUtil.checkOutTenant(tenantId, userId); + String b = attenceMachineService.clockIn(sn, userId, tenantId); + json.clear(); + // 防重复提交 + redisUtil.insert(key, "locked", 5L); + String tips = ""; + switch (b) { + case "0": + tips = "打卡失败"; + break; + case "1": + tips = "打卡成功"; + break; + case "2": + tips = "迟到"; + break; + case "3": + tips = "早退"; + break; + default: + tips = b; + break; + } + map.put("code", code); + map.put("flag", flag); + map.put("tips", tips); + return map; + } + + /** + * 版本升级 + * @param params 参数 + */ + @PostMapping(value = "/updateApp") + public void updateApp(@RequestBody Map params) { + + isPerfMachineService.updateApp(params); + } + + /** + * 查看netty所有考勤机在线用户 + * @return java.lang.Object + */ + @GetMapping(value = "/allOnlineClient") + public Object getAllOnlineClient() { + + List list = isPerfMachineService.getAllOnlineClient(); + return ActionResult.success(list); + } + + @GetMapping("/kyj/sendMqtt/{type}") + @NoDataSourceBind + public String kyjSendMqtt(@PathVariable(value = "type") Integer type) { + + tenantUtil.checkOutTenant("ftb_dev"); + MachineEnum machineEnum = MachineEnum.getMachineEnum(MachineEnum.KAI_JIA_YI.getValue()); + MachineStrategy machineStrategy = machineStrategyFactory.getMachineStrategy(machineEnum); + UserInfo userInfo = new UserInfo(); + userInfo.setTenantId("ftb_dev"); + PartUserInfoVo user = new PartUserInfoVo(); + user.setRealName("小鄢"); + user.setUserId("471608060058616325"); + user.setUserNo(123); + user.setGender(1); + String sn = "B48F62EEAC7D"; + if (type == 1) { + machineStrategy.addUserToMachine(userInfo, user, sn); + } else { + machineStrategy.deleteUserList(userInfo, Stream.of(user.getUserNo().toString()).collect(Collectors.toList()), sn); + } + return "发送结束"; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/KeMiController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/KeMiController.java new file mode 100644 index 0000000..4ff481a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/KeMiController.java @@ -0,0 +1,88 @@ +package jnpf.attendance.controller; + +import cn.hutool.json.JSONUtil; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.model.attendance.vo.attendance.UserTenantVo; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.model.user.UserNoInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.Base64Util; +import jnpf.util.CustomTenantUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +/** + * 科密考勤机 + * + * @author yanwenfu + * @create 2025-08-22 + */ +@Slf4j +@RestController +@RequestMapping(value = "/kemi") +public class KeMiController { + + @Resource + private CustomTenantUtil customTenantUtil; + @Resource + private AttenceMachineService attenceMachineService; + @Autowired + private Base64Util base64Util; + @Autowired + private V2UserApi v2UserApi; + + @NoDataSourceBind + @PostMapping(value = "/{tenantId}") + public ResponseEntity messageFromMachine(@PathVariable(value = "tenantId") String tenantId, @RequestBody Map body, HttpServletRequest request) { + + customTenantUtil.checkOutTenant(tenantId); + // log.error("科密推送 - {}", JSONUtil.toJsonStr(body)); + // 获取命令内容 + String requestCode = request.getHeader("request_code"); + String transId = request.getHeader("trans_id"); + String devId = request.getHeader("dev_id"); + String token = request.getHeader("token"); + String userId = body.get("userId").toString(); + if (requestCode.equals("realtime_glog")) { + // 实时打卡记录推送 + if (StringUtil.isNotEmpty(userId)) { + List list = v2UserApi.userListAndCopy(List.of(userId), null, tenantId); + String userName = ""; + if (null != list && !list.isEmpty()) { + userName = list.get(0).getUserName(); + } + UserTenantVo userTenant = new UserTenantVo(userId, userId, userName, tenantId, devId); + attenceMachineService.KeMiClockIn(userTenant, devId, tenantId); + } else { + log.error("科密: 陌生人打卡, {}", userId); + } + } else if (requestCode.equals("realtime_enroll_data")) { + // 实时登记数据传输 + Object photo = body.get("photo"); + if (null != photo) { + String photoUrl = base64Util.convertAndUpload(body.get("photo").toString()); + attenceMachineService.updateKeMiPhoto(userId, photoUrl, tenantId); + } else { + log.error("未获取到base64图片, 用户id: {}", userId); + } + } + return ResponseEntity.ok() + .headers(header -> { + header.add("response_code", "OK"); + header.add("trans_id", transId); + header.add("token", token); + }) + .contentType(MediaType.APPLICATION_JSON).body(null); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/OvertimeRuleController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/OvertimeRuleController.java new file mode 100644 index 0000000..9f0f827 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/OvertimeRuleController.java @@ -0,0 +1,116 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.OvertimeRuleService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.OvertimeRuleDto; +import jnpf.model.attendance.dto.OvertimeRuleQueryDto; +import jnpf.model.attendance.vo.attendance.OvertimeRulePageVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import jnpf.util.DateDetail; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * 加班规则 + * + * @author yanwenfu + * @create 2025-09-17 + */ +@RestController +@RequestMapping(value = "/overtime-rule") +public class OvertimeRuleController { + + @Resource + private OvertimeRuleService overtimeRuleService; + + /** + * 新增加班规则 + * @param overtimeRuleDto 加班规则dto + * @return java.lang.Object + */ + @PostMapping + public Object addOvertimeRule(@RequestBody @Valid OvertimeRuleDto overtimeRuleDto) throws Exception { + + overtimeRuleService.addOvertimeRule(overtimeRuleDto); + return ActionResult.success(); + } + + /** + * 编辑加班规则 + * @param id 加班规则id + * @param overtimeRuleDto 加班规则dto + * @return java.lang.Object + */ + @PutMapping(value = "/{id}") + public Object updateOvertimeRule(@PathVariable(value = "id") String id, @RequestBody @Valid OvertimeRuleDto overtimeRuleDto) throws Exception { + + overtimeRuleService.updateOvertimeRule(id, overtimeRuleDto); + return ActionResult.success(); + } + + /** + * 删除加班规则 + * @param id 加班规则id + * @return java.lang.Object + */ + @DeleteMapping(value = "/{id}") + public Object deleteOvertimeRule(@PathVariable(value = "id") String id) { + + overtimeRuleService.deleteOvertimeRule(id); + return ActionResult.success(); + } + + /** + * 禁用/启用加班规则 + * @param id 加班规则id + * @return java.lang.Object + */ + @PutMapping(value = "/enable/{id}") + public Object updateEnableStatus(@PathVariable(value = "id") String id) throws Exception { + + overtimeRuleService.updateEnableStatus(id); + return ActionResult.success(); + } + + /** + * 查询加班规则详情 + * @param id 加班规则id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/detail/{id}") + public ActionResult getDetail(@PathVariable(value = "id") String id) { + + OvertimeRuleVo vo = overtimeRuleService.getDetail(id); + return ActionResult.success(vo); + } + + /** + * 查询加班规则列表(分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/page") + public ActionResult> getPage(OvertimeRuleQueryDto queryDto) { + + PageInfo page = overtimeRuleService.getPage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 查询用户加班规则 + * @param userId 用户id + * @param queryDate 查询日期(yyyy-MM-dd) + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/user") + public ActionResult getUserOvertimeRule(@RequestParam String userId, @RequestParam String queryDate) { + + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, null, DateDetail.getStr2Date(queryDate)); + return ActionResult.success(effectDetail); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/PublicHolidayRulesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/PublicHolidayRulesController.java new file mode 100644 index 0000000..9b3983f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/PublicHolidayRulesController.java @@ -0,0 +1,93 @@ +package jnpf.attendance.controller; + + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.PublicHolidayRulesService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.AttendancePublicHolidayRulesDto; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayRulesVo; +import jnpf.model.common.PageDto; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +/** + * 考勤公休规则 + * @author panpan + * @version V2.0 + */ + +@RestController +@RequestMapping(value = "/attendance/public-holiday") +@Slf4j +public class PublicHolidayRulesController { + + @Autowired + private PublicHolidayRulesService publicHolidayRulesService; + + /** + * 获取考勤公休规则列表 + * @param iText 规则名称模糊查询 非必传 + */ + @GetMapping("/list") + public ActionResult> list(@RequestParam(value = "iText", required = false) String iText, PageDto pageDto) { + PageInfo vo = publicHolidayRulesService.list(iText,pageDto); + return ActionResult.page(vo.getList(), FtbUtil.getPagination(vo)); + } + + /** + * 新增公休规则 + * @param publicHolidayRulesDto 公休规则 + */ + @PostMapping() + public ActionResult add(@RequestBody AttendancePublicHolidayRulesDto publicHolidayRulesDto) throws Exception { + publicHolidayRulesService.add(publicHolidayRulesDto); + return ActionResult.success(); + } + + + /** + * 修改公休规则 + * @param publicHolidayRulesDto 公休规则 + */ + @PutMapping("/{id}") + public ActionResult put(@PathVariable("id") String id, @RequestBody AttendancePublicHolidayRulesDto publicHolidayRulesDto) throws Exception{ + publicHolidayRulesDto.setId(id); + publicHolidayRulesService.update(publicHolidayRulesDto); + return ActionResult.success(); + } + + /** + * 获取公休规则详情 + * @param id 公休规则 ID + */ + @GetMapping("/{id}") + public ActionResult detail(@PathVariable("id") String id) { + return ActionResult.success(publicHolidayRulesService.selectOne(id)); + } + + + /** + * 删除公休规则 + * @param id 公休规则 ID + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + publicHolidayRulesService.delete(id); + return ActionResult.success(); + } + + /** + * 启用、停用公休规则 + * @param publicHolidayRulesDto 公休规则 + */ + @PutMapping("/updateState") + public ActionResult updateState( @RequestBody AttendancePublicHolidayRulesDto publicHolidayRulesDto) { + publicHolidayRulesService.updateState(publicHolidayRulesDto); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/RV1109MachineController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/RV1109MachineController.java new file mode 100644 index 0000000..583351e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/RV1109MachineController.java @@ -0,0 +1,227 @@ +package jnpf.attendance.controller; + +import cn.hutool.core.lang.UUID; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceUserFaceService; +import jnpf.attendance.service.MachineStrategy; +import jnpf.attendance.service.RV1109MachineService; +import jnpf.base.UserInfo; +import jnpf.config.MqttConfiguration; +import jnpf.enums.attendance.MachineEnum; +import jnpf.model.attendance.vo.UserFaceVo; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.util.attendance.MachineStrategyFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * RV1109考勤机控制器 + * + * @author yanwenfu + * @create 2024-04-09 + */ +@RestController +@Slf4j +@RequestMapping +public class RV1109MachineController { + + @Autowired + private CustomTenantUtil tenantUtil; + + @Autowired + private RV1109MachineService rv1109MachineService; + + @Autowired + private AttendanceUserFaceService attendanceUserFaceService; + + @Autowired + private AttenceMachineService attenceMachineService; + + @Autowired + private MqttConfiguration mqttConfiguration; + + @Autowired + private RedisUtil redisUtil; + + @Resource + private MachineStrategyFactory machineStrategyFactory; + + /** + * 设备登录 + * @return java.lang.Object + */ + @PostMapping(value = "/device/login") + @NoDataSourceBind + public Object deviceLogin(@RequestBody Map params) { + + String devSno = params.get("dev_sno").toString(); + String token = redisUtil.getHashValues(ConstantUtil.ATTENDANCE_DEVICE, devSno); + if (StringUtils.isEmpty(token)) { + token = UUID.randomUUID().toString(); + redisUtil.insertHash(ConstantUtil.ATTENDANCE_DEVICE, devSno, token); + } + JSONObject json1 = new JSONObject(); + json1.set("code", 0); + json1.set("dev_sno", devSno); + json1.set("msg", "登陆成功"); + json1.set("success", true); + json1.set("token", token); + JSONObject json2 = new JSONObject(); + String broker = mqttConfiguration.getHost(); + String[] split = broker.split(":"); + String host = split[1].substring(2); + Integer port = Integer.parseInt(split[2]); + json2.set("host", host); + json2.set("keepalive", 60); + json2.set("login", mqttConfiguration.getUsername()); + json2.set("password", mqttConfiguration.getPassword()); + json2.set("port", port); + json2.set("qos", mqttConfiguration.getQos()); + json2.set("topic", devSno); + json1.set("mqinfo", json2); + return json1; + } + + /** + * 设备同步成员 + * @return java.lang.Object + */ + @PostMapping(value = "/device/sync_person") + @NoDataSourceBind + @SuppressWarnings("unchecked") + public Object deviceSyncPerson(@RequestBody Map params) { + + // 获取租户信息 + Map pathParamMap = (Map) params.get("path_params"); + JSONArray array = JSONUtil.parseArray(pathParamMap.get("person_list")); + String userIdStr = array.get(0).toString(); + String tenantId = userIdStr.split("@")[0]; + String userId = userIdStr.split("@")[1]; + tenantUtil.checkOutTenant(tenantId, userId); + JSONObject json = new JSONObject(); + json.set("code", 0); + json.set("msg", "OK"); + json.set("success", true); + json.set("person_list", getJsonArray(userId, tenantId)); + return json; + } + + /** + * 返回结果至设备 + * @return java.lang.Object + */ + @PostMapping(value = "/device/notify") + @NoDataSourceBind + public Object deviceNotify(@RequestBody Map params) { + + // 获取租户信息 + boolean b = (boolean) params.get("success"); + JSONObject json = new JSONObject(); + if (b) { + json.set("code", 0); + json.set("msg", "OK"); + json.set("success", true); + } else { + json.set("code", 0); + json.set("msg", "NOT OK"); + json.set("success", false); + } + return json; + } + + private JSONArray getJsonArray(String userId, String tenantId) { + + JSONArray array = new JSONArray(); + UserEntity user = rv1109MachineService.getUserInfoById(userId, tenantId); + if (null == user) { + log.error("用户不存在, 用户ID:{}", userId); + return array; + } + // 查询人脸 + UserFaceVo userFace = attendanceUserFaceService.getUserFace(user.getId()); + if (null == userFace) { + log.error("人脸数据不存在, 用户ID:{}", userId); + return array; + } + JSONObject json = new JSONObject(); + json.set("person_id", tenantId + "@" + user.getId()); + json.set("person_name", user.getRealName()); + json.set("person_type", "4"); + json.set("sex", null == user.getGender() ? 0 : user.getGender()); + JSONArray picArray = new JSONArray(); + picArray.add(userFace.getFaceData()); + json.set("templateImgUrl", picArray); + json.set("qr", ""); + json.set("id_card", ""); + json.set("birthday", null == user.getBirthday() ? "" : DateDetail.getDate2Str(user.getBirthday(), DateDetail.DF)); + array.add(json); + return array; + } + + /** + * 记录上传 + * @return java.lang.Object + */ + @PostMapping(value = "/record/upload/online") + @NoDataSourceBind + public Object recordUploadOnline(@RequestBody Map params) { + + // 获取租户信息 + String personIdStr = params.get("person_id").toString(); + String tenantId = personIdStr.split("@")[0]; + String userId = personIdStr.split("@")[1]; + tenantUtil.checkOutTenant(tenantId, userId); + // 获取设备号 + String sn = params.get("dev_sno").toString(); + // 获取对比结果 + int captureStatus = Integer.parseInt(params.get("capture_status").toString()); + if (ConstantUtil.NUM_TRUE == captureStatus) { + // 对比通过 + String b = attenceMachineService.clockIn(sn, userId, tenantId); + if (StringUtils.isNotEmpty(b)) { + log.error("打卡失败"); + } + } + JSONObject json = new JSONObject(); + json.set("code", 0); + json.set("msg", "OK"); + json.set("success", true); + return json; + } + + /** + * 测试方法 + * @return java.lang.String + */ + @GetMapping("/sendMqtt/{type}") + @NoDataSourceBind + public String sendMqtt(@PathVariable(value = "type") String type) { + + MachineEnum machineEnum = MachineEnum.getMachineEnum(MachineEnum.YU_QUE.getValue()); + MachineStrategy machineStrategy = machineStrategyFactory.getMachineStrategy(machineEnum); + UserInfo userInfo = new UserInfo(); + userInfo.setTenantId("ftb_dev"); + PartUserInfoVo user = new PartUserInfoVo(); + user.setUserId("471608060058616325"); + String sn = "0A:0C:E1:43:B0:37"; + if (type.equals("add")) { + machineStrategy.addUserToMachine(userInfo, user, sn); + } + if (type.equals("delete")) { + machineStrategy.deleteUserList(userInfo, Stream.of(user.getUserId()).collect(Collectors.toList()), sn); + } + return "发送结束"; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/ScheduleGroupRuleConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/ScheduleGroupRuleConfigController.java new file mode 100644 index 0000000..b71d639 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/ScheduleGroupRuleConfigController.java @@ -0,0 +1,77 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.ScheduleGroupRuleConfigService; +import jnpf.base.ActionResult; +import jnpf.model.attendance.dto.scheduling.ScheduleGroupRuleConfigDto; +import jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; + +/** + * 智能排班:考勤组维度「排班规则配置」查询与保存(仅固定排班核心参数 + 划线排班参数)。 + * 控制器只负责打印接口入参与耗时(JNPF 日志规范)。 + * + * @author xiaofeng + * @since 2026-05-13 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/attendance/schedule") +@Tag(name = "排班规则配置", description = "考勤组固定排班与划线排班参数 GET/POST") +public class ScheduleGroupRuleConfigController { + + @Resource + private ScheduleGroupRuleConfigService scheduleGroupRuleConfigService; + + /** + * 获取排班规则配置。无表记录时各子块返回与表 DEFAULT 一致的默认结构。 + * + * @param groupId 考勤组 ID + * @return 配置 + */ + @SneakyThrows + @Operation(summary = "获取排班规则配置") + @GetMapping("/getRuleConfig") + public ActionResult getRuleConfig( + @RequestParam("groupId") @NotBlank(message = "考勤组ID不能为空") String groupId) { + log.info("获取排班规则配置,入参=>{}", groupId); + long st = System.currentTimeMillis(); + ScheduleGroupRuleConfigVo data = scheduleGroupRuleConfigService.getRuleConfig(groupId); + log.info("获取排班规则配置,耗时=>{} 毫秒", System.currentTimeMillis() - st); + return ActionResult.success(data); + } + + /** + * 保存排班规则配置。请求体根与各子块为 DTO;GET/POST 成功响应根与各子块均为 VO(JSON 字段名不变)。 + * 服务端在事务内 upsert 固定排班与划线排班参数表。 + * + * @param dto 配置({@code groupId}、{@code fixedScheduling}、{@code lineScheduling} 必填) + * @return 保存后的配置 + */ + @SneakyThrows + @Operation(summary = "保存排班规则配置") + @PostMapping("/saveRuleConfig") + public ActionResult saveRuleConfig( + @RequestBody @Valid ScheduleGroupRuleConfigDto dto) { + log.info("保存排班规则配置,入参=>{}", JSONObject.toJSON(dto)); + long st = System.currentTimeMillis(); + ScheduleGroupRuleConfigVo data = scheduleGroupRuleConfigService.saveRuleConfig(dto.getGroupId(), dto); + log.info("保存排班规则配置,耗时=>{} 毫秒", System.currentTimeMillis() - st); + return ActionResult.success(data); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/SmartPreScheduleController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/SmartPreScheduleController.java new file mode 100644 index 0000000..51d1896 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/SmartPreScheduleController.java @@ -0,0 +1,75 @@ +package jnpf.attendance.controller; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.SmartPreScheduleService; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.scheduling.PreScheduleTableQueryDto; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; +import jnpf.util.StringUtil; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * AI 智能排班确认:预排班主表生成与提交保存接口层。 + * + * @author xiaofeng + * @create 2026-05-13 + */ +@Slf4j +@RestController +@RequestMapping("/attendance/smartSchedule") +@Tag(name = "智能排班预排班", description = "预排班主表生成与整包保存") +public class SmartPreScheduleController { + + @Resource + private SmartPreScheduleService smartPreScheduleService; + + /** + * 预排班生成主表:返回 rows,每行含 posts、stations 预览。 + * + * @param dto 日期范围、按日预估营业额、人效目标、考勤组 + * @return 主表数据 + */ + @SneakyThrows + @Operation(summary = "预排班功能") + @PostMapping("/preScheduling") + public ActionResult buildTable(@RequestBody @Valid PreScheduleTableQueryDto dto) { + log.error("预排班生成主表,入参=>{}", JSONObject.toJSON(dto)); + long startMs = System.currentTimeMillis(); + PreScheduleTableVo data = smartPreScheduleService.buildPreScheduleTable(dto); + log.error("预排班生成主表,耗时=>{}毫秒", System.currentTimeMillis() - startMs); + ActionResult result = ActionResult.success(data); + if (data != null && StringUtil.isNotBlank(data.getMsg())) { + result.setMsg(data.getMsg()); + } + return result; + } + + /** + * 预排班主表提交保存:按 rows 过滤 Redis 中 byEmployee 并写回,返回 redisKeySuffix。 + * + * @param vo groupId + rows + * @return redisKeySuffix(考勤组:日期区间,不含租户) + */ + @Operation(summary = "预排班主表提交保存") + @PostMapping("/saveSchedul") + public ActionResult saveTable(@RequestBody @Valid PreScheduleTableVo vo) + throws HandleException, QueryException { + log.error("预排班主表提交保存,入参=>{}", JSONObject.toJSON(vo)); + long startMs = System.currentTimeMillis(); + String redisKeySuffix = smartPreScheduleService.savePreScheduleTable(vo); + log.error("预排班主表提交保存,耗时=>{}毫秒", System.currentTimeMillis() - startMs); + return ActionResult.success(redisKeySuffix); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserConfigController.java new file mode 100644 index 0000000..d7a6020 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserConfigController.java @@ -0,0 +1,46 @@ +package jnpf.attendance.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.attendance.service.UserConfigService; +import jnpf.base.ActionResult; + + +import jnpf.model.attendance.vo.attendance.GroupShiftTimeVo; +import jnpf.model.attendance.vo.attendance.UserConfigVo; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/6/24 11:23 + * @Version 1.0 (版本号) + */ + +@RestController +@RequestMapping("/config") +@Tag(name = "APP/后台 - 用户配置", description = "group") +public class UserConfigController { + + @Resource + private UserConfigService userConfigService; + + /** + * 获取用户app考勤配置 + */ + @GetMapping("/userConfig") + public ActionResult getUserConfig() { + + return ActionResult.success(userConfigService.getUserConfig()); + } + + /** + * 修改用户APP考勤配置 + */ + @PutMapping("/userConfig") + public ActionResult updateUserConfig(@RequestBody UserConfigVo userConfigVo) { + userConfigService.updateUserConfig(userConfigVo); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserFaceController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserFaceController.java new file mode 100644 index 0000000..f458dfa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserFaceController.java @@ -0,0 +1,233 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.UserFaceService; +import jnpf.attendance.service.UserFaceTxService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.FaceChangeQueryDto; +import jnpf.model.attendance.dto.FaceQueryDto; +import jnpf.model.attendance.dto.UserDto; +import jnpf.model.attendance.dto.UserFaceDto; +import jnpf.model.attendance.vo.ChangeLogVo; +import jnpf.model.attendance.vo.FaceMiniVo; +import jnpf.model.attendance.vo.UserFaceDetailVo; +import jnpf.model.attendance.vo.UserFaceVo; +import jnpf.model.common.PageDto; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 人脸识别 + * + * @author yanwenfu + * @create 2025-04-08 + */ +@RestController +@Slf4j +@RequestMapping(value = "/attendance/user-face") +public class UserFaceController { + + @Resource + private UserFaceService userFaceService; + @Resource + private UserFaceTxService userFaceTxService; + @Resource + private UserProvider userProvider; + + /** + * 下发用户人脸至考勤机 + * @param userId 用户id列表 + * @return jnpf.base.ActionResult + */ + @PutMapping(value = "/sync") + public ActionResult syncUserFaceToMachine(@RequestParam String userId) throws Exception { + + Integer result = userFaceTxService.syncUserFaceToMachine(userId); + return ActionResult.success(result); + } + + /** + * 人脸对比 + * @param file 人脸图片 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/compare") + public ActionResult getPhotoCheck(@RequestPart MultipartFile file) throws Exception { + + if (!isImageFile(file)) { + return ActionResult.fail("请上传图片文件进行比对"); + } + Boolean result = userFaceService.getPhotoCheck(file, userProvider.get()); + return ActionResult.success(result); + } + + /** + * 人脸对比2 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/compare-sec") + public ActionResult getPhotoCheck2() throws Exception { + + List list = UpUtil.getFileAll(); + MultipartFile file = list.get(0); + if (!isImageFile(file)) { + return ActionResult.fail("请上传图片文件进行比对"); + } + Boolean result = userFaceService.getPhotoCheck(file, userProvider.get()); + return ActionResult.success(result); + } + + private boolean isImageFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + return false; + } + String contentType = file.getContentType(); + return contentType != null && ConstantUtil.IMAGE_CONTENT_TYPES.contains(contentType); + } + + /** + * 录入人脸 + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @PostMapping + public ActionResult addUserFace(@RequestPart("file") MultipartFile file, @RequestPart("userId") String userId) throws Exception { + + if (!isImageFile(file)) { + return ActionResult.fail("文件格式错误!"); + } + Integer result = userFaceTxService.uploadUserFace(file, new UserDto(userId), userProvider.get()); + return ActionResult.success(result); + } + + /** + * 录入人脸 - add + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/add/{userId}") + public ActionResult addUserFace2(@PathVariable("userId") String userId) throws Exception { + + List list = UpUtil.getFileAll(); + MultipartFile file = list.get(0); + if (!isImageFile(file)) { + return ActionResult.fail("文件格式错误!"); + } + Integer result = userFaceTxService.uploadUserFace(file, new UserDto(userId), userProvider.get()); + return ActionResult.success(result); + } + + /** + * 清空人脸 + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @DeleteMapping(value = "/{userId}") + public ActionResult deleteUserFace(@PathVariable(value = "userId") String userId) throws Exception { + + userFaceService.deleteUserFace(userId, userProvider.get()); + return ActionResult.success(); + } + + /** + * 查看人脸 + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/{userId}") + public ActionResult getUserFace(@PathVariable(value = "userId") String userId) { + + UserFaceVo userFace = userFaceService.getUserFace(userId); + return ActionResult.success(userFace); + } + + /** + * 查看人脸详情 + * @param id 人脸记录id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/detail/{id}") + public ActionResult getUserFaceDetail(@PathVariable(value = "id") String id) { + + UserFaceVo userFace = userFaceService.getUserFaceDetail(id); + return ActionResult.success(userFace); + } + + /** + * 用户人脸信息 + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/info/{userId}") + public ActionResult getUserFaceInfo(@PathVariable(value = "userId") String userId) { + + FaceMiniVo userFace = userFaceService.getUserFaceInfo(userId); + return ActionResult.success(userFace); + } + + /** + * 变动记录(分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/change-log/page") + public ActionResult> getChangeLog(FaceChangeQueryDto queryDto) { + + PageInfo page = userFaceService.getChangeLogPage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 人脸记录(分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @PostMapping(value = "/page") + public ActionResult> getUserFacePage(@RequestBody @Valid FaceQueryDto queryDto) throws QueryException { + + if (StringUtil.isEmpty(queryDto.getGroupId()) && StringUtil.isEmpty(queryDto.getTeamId())) { + throw new QueryException("请选择考勤组或班组"); + } + PageInfo page = userFaceService.getUserFacePage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 判断用户是否上传人脸 + * @param userId 用户id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/check/{userId}") + public ActionResult hasUserFace(@PathVariable(value = "userId") String userId) { + + Boolean b = userFaceService.hasUserFace(userId); + return ActionResult.success(b); + } + + /** + * V1.8.2初始化人脸缩略图 + * @param tenantId 租户id + */ + @NoDataSourceBind + @GetMapping(value = "/initializationFace") + public ActionResult initializationFace(@RequestParam("tenantId") String tenantId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + userFaceService.initializationFace(tenantId); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserPhoneController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserPhoneController.java new file mode 100644 index 0000000..a15a904 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/UserPhoneController.java @@ -0,0 +1,88 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.UsualPhoneService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.CancelPhoneDto; +import jnpf.model.attendance.dto.UsualPhoneQueryDto; +import jnpf.model.attendance.dto.UsualPhoneSettingDto; +import jnpf.model.attendance.vo.attendance.UsualPhonePageVo; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * 常用设备管理 + * + * @author yanwenfu + * @create 2025-09-17 + */ +@RestController +@RequestMapping(value = "/user-phone") +public class UserPhoneController { + + @Resource + private UsualPhoneService usualPhoneService; + + /** + * 批量取消绑定 + * @param cancelPhoneDto 取消绑定参数 + * @return java.lang.Object + */ + @PutMapping(value = "/cancel") + public Object cancelPhoneBatch(@RequestBody @Valid CancelPhoneDto cancelPhoneDto) { + + usualPhoneService.cancelPhoneBatch(cancelPhoneDto); + return ActionResult.success(); + } + + /** + * 更新常用手机设置 + * @param usualPhoneSettingDto 常用手机设置dto + * @return java.lang.Object + */ + @PutMapping(value = "/setting") + public Object updateUsualPhoneSetting(@RequestBody @Valid UsualPhoneSettingDto usualPhoneSettingDto) { + + usualPhoneService.updateUsualPhoneSetting(usualPhoneSettingDto); + return ActionResult.success(); + } + + /** + * 查询常用手机列表(分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/page") + public ActionResult> getUsualPhonePage(UsualPhoneQueryDto queryDto) { + + PageInfo page = usualPhoneService.getUsualPhonePage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 检查常用设备是否异常 + * @param phoneName 手机名称 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/abnormal") + public ActionResult checkUsualPhone(@RequestParam(value = "phoneName") String phoneName, @RequestParam(value = "phoneCode") String phoneCode) { + + Boolean flag = usualPhoneService.checkUsualPhone(phoneName, phoneCode); + return ActionResult.success(flag); + } + + /** + * 查询常用手机配置 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/usual-phone/setting") + public ActionResult getUsualPhoneSetting() { + + UsualPhoneSettingDto dto = usualPhoneService.getUsualPhoneSetting(); + return ActionResult.success(dto); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WebStatisticsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WebStatisticsController.java new file mode 100644 index 0000000..8847b26 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WebStatisticsController.java @@ -0,0 +1,489 @@ +package jnpf.attendance.controller; + +import cn.hutool.core.date.DateUtil; +import com.fantaibao.permission.annotation.FtbCheckPermission; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.attendance.FtbStatisticsApi; +import jnpf.attendance.dto.*; +import jnpf.attendance.service.AttendanceCustomizeTableService; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.attendance.service.AttendanceSealSettingService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.enums.attendance.TriggerSceneEnum; +import jnpf.exception.LoginException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.event.StatisticsBatchClearDto; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.personnels.vo.analysis.PersonnelDataAnalysisListVO; +import jnpf.personnels.utils.PersonnelDataAnalysisUtil; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 考勤统计WEB + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendance/statistics") +public class WebStatisticsController implements FtbStatisticsApi { + @Resource + private RocketMQTemplate rocketMqTemplate; + @Resource + private AttendanceDayStatisticsService dayStaService; + @Autowired + private AttendanceCustomizeTableService tableService; + @Resource + private AttendanceSealSettingService sealSettingService; + + @Override + @Operation(summary = "用户日统计数据初始化") + @GetMapping(value = "/userDayStatisticsInit") + public ActionResult userDayStatisticsInit(@RequestParam(value = "tenantId") String tenantId) { + return ActionResult.success(dayStaService.handleDataForJob(tenantId)); + } + + @Operation(summary = "日度统计-汇总") + @PostMapping("/dayStatistics") + public ActionResult> dayStatistics(@Valid @RequestBody DayStatisticsDataDto req) { + return ActionResult.success(dayStaService.getDayStatisticsData(req)); + } + + @Operation(summary = "日度统计-分页列表") + @PostMapping(value = "/dayPageList") + public ActionResult> dayPageList(@Valid @RequestBody DayStatisticsPageListDto req) { + PageInfo page = dayStaService.getDayPageList(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @Operation(summary = "日度统计-导出") + @PostMapping(value = "/dayDataExport") + public ActionResult dayDataExport(@Valid @RequestBody DayStatisticsExportDto req) { + dayStaService.dayDataExport(req); + return ActionResult.success(); + } + + @Operation(summary = "月度统计-汇总") + @PostMapping("/monthStatistics") + public ActionResult> monthStatistics(@Valid @RequestBody MouthStatisticsDataDto req) { + return ActionResult.success(dayStaService.getMonthStatisticsData(req)); + } + + @Operation(summary = "月度统计-分页列表") + @PostMapping(value = "/monthPageList") + public ActionResult> monthPageList(@Valid @RequestBody MonthStatisticsPageListDto req) throws Exception { + PageInfo page = dayStaService.getMonthPageList(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @Operation(summary = "月度统计-导出") + @PostMapping(value = "/monthDataExport") + public ActionResult monthDataExport(@Valid @RequestBody MonthStatisticsExportDto req) { + dayStaService.monthDataExport(req); + return ActionResult.success(); + } + + @Override + @Operation(summary = "个人考勤日报通知") + @GetMapping(value = "/dayStatisticsNotice") + public ActionResult dayStatisticsNotice(@RequestParam(value = "tenantId") String tenantId) { + return ActionResult.success(dayStaService.dayStatisticsNotice(tenantId)); + } + + @Override + @Operation(summary = "个人统计月报通知") + @GetMapping(value = "/monthStatisticsNotice") + public ActionResult monthStatisticsNotice(@RequestParam(value = "tenantId") String tenantId) { + return ActionResult.success(dayStaService.monthStatisticsNotice(tenantId)); + } + + @Override + @Operation(summary = "连续未排班通知") + @GetMapping(value = "/consentUnscheduledNotice") + public void consentUnscheduledNotice(@RequestParam(value = "tenantId") String tenantId) { + dayStaService.consentUnscheduledNotice(tenantId); + } + + @Override + @Operation(summary = "团队统计月报通知") + @GetMapping(value = "/teamMonthStatisticsNotice") + public ActionResult teamMonthStatisticsNotice(@RequestParam(value = "tenantId") String tenantId) { + return ActionResult.success(dayStaService.teamMonthStatisticsNotice(tenantId)); + } + + @Override + @Operation(summary = "计算考勤组平均工时(实际出勤工时)") + @PostMapping(value = "/countAttendanceAvgHours") + public List countAttendanceAvgHours(@Valid @RequestBody AttendanceCountAvgHoursDto dto) { + return dayStaService.countAttendanceAvgHours(dto); + } + + @Override + @Operation(summary = "获取多考勤组月度统计数据") + @PostMapping(value = "/getAttendanceAvgHoursDetails") + public ActionResult getAttendanceAvgHoursDetails(@Valid @RequestBody MonthStatsDetailsDto dto) { + MonthStatsDetailsVo result = dayStaService.getAttendanceAvgHoursDetails(dto); + return ActionResult.success(result); + } + + @Override + @Operation(summary = "获取多考勤组月度人均工时折线图") + @PostMapping(value = "/getAttendanceMonthPerCapita") + public ActionResult> getAttendanceMonthPerCapita(@Valid @RequestBody MonthStatsDetailsDto dto) { + return ActionResult.success(dayStaService.getAttendanceMonthPerCapita(dto)); + } + + @Override + @Operation(summary = "获取多考勤组月度日常情况") + @PostMapping(value = "/getAttendanceDailySituation") + public ActionResult> getAttendanceDailySituation(@Valid @RequestBody MonthStatsDetailsDto dto) { + return ActionResult.success(dayStaService.getAttendanceDailySituation(dto)); + } + + @Override + @Operation(summary = "获取多考勤组月度考勤工时排行") + @PostMapping(value = "/getAttendanceHoursRanking") + public ActionResult> getAttendanceHoursRanking(@Valid @RequestBody MonthStatsDetailsDto dto) { + return ActionResult.success(dayStaService.getAttendanceHoursRanking(dto)); + } + + @Override + @Operation(summary = "获取多考勤组月度全勤情况") + @PostMapping(value = "/getAttendanceFullSituation") + public ActionResult> getAttendanceFullSituation(@Valid @RequestBody MonthStatsDetailsDto dto) { + return ActionResult.success(dayStaService.getAttendanceFullSituation(dto)); + } + + @Override + @Operation(summary = "获取多考勤组月度异常情况") + @PostMapping(value = "/getAttendanceAbnormalCondition") + public ActionResult getAttendanceAbnormalCondition(@Valid @RequestBody MonthStatsDetailsDto dto) { + return ActionResult.success(dayStaService.getAttendanceAbnormalCondition(dto)); + } + + @Override + @Operation(summary = "获取多考勤组月度加班情况") + @PostMapping(value = "/getAttendanceOvertimeSituation") + public ActionResult> getAttendanceOvertimeSituation(@Valid @RequestBody MonthStatsDetailsDto dto) { + return ActionResult.success(dayStaService.getAttendanceOvertimeSituation(dto)); + } + + @Operation(summary = "考勤封账-自动封账设置详情") + @GetMapping(value = "/getAutoSealSettingInfo") + public ActionResult getAutoSealSettingInfo() { + return ActionResult.success(sealSettingService.getAutoSealSettingInfo()); + } + + @Operation(summary = "考勤封账-自动封账设置") + @PutMapping(value = "/autoSealSetting") + public ActionResult autoSealSetting(@Valid @RequestBody MonthAutoSealSettingDto dto) { + return ActionResult.success(sealSettingService.autoSealSetting(dto)); + } + + @Override + @Operation(summary = "考勤封账-自动封账定时器") + @PutMapping(value = "/autoSealTimer") + public ActionResult autoSealTimer(@RequestParam(value = "tenantId") String tenantId) { + return ActionResult.success(sealSettingService.autoSealTimer(tenantId)); + } + + @Operation(summary = "考勤封账-分页列表") + @PostMapping(value = "/sealPageList") + public ActionResult> sealPageList(@Valid @RequestBody MonthSealPageListDto dto) { + PageInfo page = dayStaService.sealPageList(dto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @Operation(summary = "考勤封账-批量|单个封账") + @PutMapping(value = "/sealSubmit") + public ActionResult sealSubmit(@Valid @RequestBody MonthSealSubmitDto dto) { + return ActionResult.success(dayStaService.sealSubmit(dto)); + } + + @Operation(summary = "考勤封账-解封") + @PutMapping(value = "/unSealSubmit") + public ActionResult unSealSubmit(@Valid @RequestBody MonthUnSealSubmitDto dto) { + return ActionResult.success(dayStaService.unSealSubmit(dto)); + } + + @Operation(summary = "批量查询用户是否封账") + @PostMapping(value = "/selectUserIsSeal") + public ActionResult> selectUserIsSeal(@Valid @RequestBody MonthSealSubmitDto dto) { + return ActionResult.success(dayStaService.selectUserIsSeal(dto.getUserIdList(), dto.getMonth())); + } + + @Override + @Operation(summary = "薪酬考勤数据支持(薪酬)") + @PostMapping(value = "/salaryAttendanceSupport") + public Map salaryAttendanceSupport(@Valid @RequestBody SalaryAttendanceSupportDto dto) { + return dayStaService.salaryAttendanceSupport(dto); + } + + @Override + @Operation(summary = "考勤统计数据日度列表表头(薪酬)") + @PostMapping(value = "/attendanceDayStaTable") + public List attendanceDayStaTable() { + return tableService.findList(null, null, 1); + } + + @Override + @Operation(summary = "考勤统计数据日度列表(薪酬)") + @PostMapping(value = "/attendanceDayStaList") + public List attendanceDayStaList(@Valid @RequestBody SalaryAttendanceSupportDto dto) { + return dayStaService.attendanceStaList(dto); + } + + @Override + @Operation(summary = "获取日出勤信息") + @NoDataSourceBind + @PostMapping(value = "/getAttendanceDayStaList") + public List getAttendanceDayStaList(@Valid @RequestBody DayStatisticsDto dto) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(dto.getTenantId()); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + return dayStaService.getAttendanceDayStaList(dto); + } + + @NoDataSourceBind + @Operation(summary = "模拟统计数据消息推送") + @GetMapping("/mockStatisticsPush") + public ActionResult mockStatisticsPush(@RequestParam("tenantId") String tenantId, + @RequestParam("groupId") String groupId, + @RequestParam("userId") String userId, + @RequestParam("day") String day) { + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .userId(userId) + .triggerSceneEnum(TriggerSceneEnum.MANUAL_TRIGGER) + .day(DateUtil.parse(day)) + .build(); + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + return ActionResult.success(); + } + + @NoDataSourceBind + @Operation(summary = "日统计触发") + @GetMapping("/dayStatisticsTriggered") + public ActionResult dayStatisticsTriggered(@RequestParam("tenantId") String tenantId, + @RequestParam("groupId") String groupId, + @RequestParam("userId") String userId, + @RequestParam("day") String day) throws LoginException { + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .userId(userId) + .day(DateUtil.parse(day)) + .triggerSceneEnum(TriggerSceneEnum.MANUAL_TRIGGER) + .build(); + dayStaService.statisticDataChange(courseEventDTO); + return ActionResult.success(); + } + + @NoDataSourceBind + @Operation(summary = "日统计数据清除") + @GetMapping("/dayStatisticsClear") + public ActionResult dayStatisticsClear(@RequestParam("tenantId") String tenantId, + @RequestParam("groupId") String groupId, + @RequestParam("userId") List userList, + @RequestParam("day") String day, + @RequestParam("startDay") String startDay) { + StatisticsBatchClearDto courseEventDTO = StatisticsBatchClearDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .userIdList(userList) + .day(DateUtil.parse(day)) + .startDay(DateUtil.parse(startDay)) + .build(); + dayStaService.batchStatisticDataClear(courseEventDTO); + return ActionResult.success(); + } + + @NoDataSourceBind + @Operation(summary = "重新生成日统计数据") + @GetMapping(value = "/regenerateDayData") + public ActionResult regenerateDayData(@RequestParam("tenantId") String tenantId, + @RequestParam("start") Date start) { + dayStaService.regenerateDayData(tenantId, start); + return ActionResult.success(); + } + + @NoDataSourceBind + @Operation(summary = "处理历史数据") + @GetMapping(value = "/processHistoricalData") + public ActionResult processHistoricalData(@RequestParam("tenantId") String tenantId, + @RequestParam("start") Date start, + @RequestParam("end") Date end) { + dayStaService.processHistoricalData(tenantId, start, end); + return ActionResult.success(); + } + + @Override + @Operation(summary = "获取各维度出勤人数") + @PostMapping(value = "/getDimensionsAttendanceCountMap") + public Map> getDimensionsAttendanceCountMap(@Valid @RequestBody DimensionsAttendanceCountDto dto) { + return dayStaService.getDimensionsAttendanceCountMap(dto); + } + + @Override + @Operation(summary = "获取各维度出勤人数(天维度)") + @PostMapping(value = "/getDimensionsAttendanceDayCountMap") + public Map> getDimensionsAttendanceDayCountMap(@Valid @RequestBody DimensionsAttendanceDayCountDto dto) { + return dayStaService.getDimensionsAttendanceDayCountMap(dto); + } + + @Operation(summary = "日度计薪统计-分页列表") + @PostMapping(value = "/dayPayrollPageList") + public ActionResult> dayPayrollPageList(@Valid @RequestBody DayPayrollStatisticsPageListDto req) { + PageInfo page = dayStaService.getDayPayrollPageList(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @Operation(summary = "月度计薪统计-分页列表") + @PostMapping(value = "/monthPayrollPageList") + public ActionResult> monthPayrollPageList(@Valid @RequestBody MonthPayrollStatisticsPageListDto req) throws Exception { + PageInfo page = dayStaService.getMonthPayrollPageList(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @Operation(summary = "考勤平均工时趋势-分页列表") + @PostMapping(value = "/averageWorkHoursTrend/pageList") + public ActionResult averageWorkHoursTrendPageList(@Valid @RequestBody PersonnelApiInfoPageListDto dto) { + PageListVO result = dayStaService.averageWorkHoursTrendPageList(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, AverageTrendPageListVo.class)); + } + + @Operation(summary = "考勤平均工时趋势-导出") + @PostMapping(value = "/averageWorkHoursTrend/export") + public void averageWorkHoursTrendExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoPageListDto pageDto) throws Exception { + dayStaService.averageWorkHoursTrendExport(response, pageDto); + } + + @Operation(summary = "人均工时趋势-分页列表") + @PostMapping(value = "/personWorkHoursTrend/pageList") + public ActionResult personWorkHoursTrendPageList(@Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) { + PageListVO result = dayStaService.personWorkHoursTrendPageList(pageDto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelTrendPageListVo.class)); + } + + @Operation(summary = "人均工时趋势-导出") + @PostMapping(value = "/personWorkHoursTrend/export") + public void personWorkHoursTrendExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + dayStaService.personWorkHoursTrendExport(response, pageDto); + } + + @Operation(summary = "考勤组日常情况-分页列表") + @PostMapping(value = "/dailySituation/pageList") + public ActionResult dailySituationPageList(@Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) { + PageListVO result = dayStaService.dailySituationPageList(pageDto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, DailySituationPageListVo.class)); + } + + @Operation(summary = "考勤组日常情况-导出") + @PostMapping(value = "/dailySituation/export") + public void dailySituationExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + dayStaService.dailySituationExport(response, pageDto); + } + + @Operation(summary = "考勤工时排行-分页列表") + @PostMapping(value = "/workHoursRanking/pageList") + public ActionResult workHoursRankingPageList(@Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) { + PageListVO result = dayStaService.workHoursRankingPageList(pageDto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, WorkHoursRankingPageListVo.class)); + } + + @Operation(summary = "考勤工时排行-导出") + @PostMapping(value = "/workHoursRanking/export") + public void workHoursRankingExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + dayStaService.workHoursRankingExport(response, pageDto); + } + + @Operation(summary = "考勤组全勤情况-分页列表") + @PostMapping(value = "/fullAttendanceStatus/pageList") + public ActionResult fullAttendanceStatusPageList(@Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) { + PageListVO result = dayStaService.fullAttendanceStatusPageList(pageDto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FullAttendanceStatusPageListVo.class)); + } + + @Operation(summary = "考勤组全勤情况-导出") + @PostMapping(value = "/fullAttendanceStatus/export") + public void fullAttendanceStatusExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + dayStaService.fullAttendanceStatusExport(response, pageDto); + } + + @Operation(summary = "考勤异常情况-分页列表") + @PostMapping(value = "/exceptionSituation/pageList") + public ActionResult exceptionSituationPageList(@Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) { + PageListVO result = dayStaService.exceptionSituationPageList(pageDto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, ExceptionSituationPageListVo.class)); + } + + @Operation(summary = "考勤异常情况-导出") + @PostMapping(value = "/exceptionSituation/export") + public void exceptionSituationExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + dayStaService.exceptionSituationExport(response, pageDto); + } + + @Operation(summary = "考勤组加班情况-分页列表") + @PostMapping(value = "/overtimeSituation/pageList") + public ActionResult overtimeSituationPageList(@Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) { + PageListVO result = dayStaService.overtimeSituationPageList(pageDto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, OvertimeSituationPageListVo.class)); + } + + @Operation(summary = "考勤组加班情况-导出") + @PostMapping(value = "/overtimeSituation/export") + public void overtimeSituationExport(HttpServletResponse response, @Valid @RequestBody PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + dayStaService.overtimeSituationExport(response, pageDto); + } + + @Operation(summary = "查询用户考勤统计信息") + @PostMapping(value = "/queryUserStatisticsInfo") + @FtbCheckPermission("attendance_2.0.statistics.monthlyStatistics") + public ActionResult queryUserStatisticsInfo(@Valid @RequestBody QueryStatisticsInfoDto dto) { + return ActionResult.success(dayStaService.queryUserStatisticsInfo(dto)); + } + + @Operation(summary = "查询考勤组的考勤情况") + @PostMapping(value = "/queryGroupStatistics") + @FtbCheckPermission("attendance_2.0.statistics.monthlyStatistics") + public ActionResult queryGroupStatistics(@Valid @RequestBody QueryGroupStatisticsDto dto) { + return ActionResult.success(dayStaService.queryGroupStatistics(dto)); + } + + @Operation(summary = "查询用户的加班情况") + @PostMapping(value = "/queryUserOvertime") + @FtbCheckPermission("attendance_2.0.statistics.monthlyStatistics") + public ActionResult queryUserOvertime(@Valid @RequestBody QueryStatisticsInfoDto dto) { + return ActionResult.success(dayStaService.queryUserOvertime(dto)); + } + + @Operation(summary = "查询用户的上班情况") + @PostMapping(value = "/queryUserWorkSituation") + public ActionResult> queryUserWorkSituation(@Valid @RequestBody UserWorkSituationDto dto) { + return ActionResult.success(dayStaService.queryUserWorkSituation(dto)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WorkstationController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WorkstationController.java new file mode 100644 index 0000000..92dd1ab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/controller/WorkstationController.java @@ -0,0 +1,146 @@ +package jnpf.attendance.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.attendance.service.WorkstationService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.dto.WorkstationQueryDto; +import jnpf.model.attendance.dto.WorkstationSaveDto; +import jnpf.model.attendance.dto.WorkstationUserAddDto; +import jnpf.model.attendance.dto.WorkstationUserRemoveDto; +import jnpf.model.attendance.vo.WorkstationDetailVo; +import jnpf.model.attendance.vo.WorkstationVo; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; +import jnpf.util.FtbUtil; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Date; +import java.util.List; + +/** + * 考勤工作站控制器 + * + * @author AI Generated + * @create 2026-05-11 + */ +@RestController +@RequestMapping("/attendance/workstation") +public class WorkstationController { + + @Resource + private WorkstationService workstationService; + + /** + * 查询工作站列表(分页) + * + * @param dto 查询条件(含分页参数 currentPage/pageSize) + * @return 工作站分页列表 + */ + @GetMapping("/list") + public ActionResult> list(WorkstationQueryDto dto) { + PageInfo page = workstationService.listWorkstations(dto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 新增工作站 + * + * @param dto 工作站保存DTO + * @return 操作结果 + */ + @PostMapping + public ActionResult add(@RequestBody @Valid WorkstationSaveDto dto) { + workstationService.saveWorkstation(dto); + return ActionResult.success("新增成功"); + } + + /** + * 编辑工作站 + * + * @param id 工作站ID + * @param dto 工作站保存DTO + * @return 操作结果 + */ + @PutMapping("/{id}") + public ActionResult update(@PathVariable String id, @RequestBody @Valid WorkstationSaveDto dto) { + workstationService.updateWorkstation(id, dto); + return ActionResult.success("编辑成功"); + } + + /** + * 删除工作站 + * + * @param id 工作站ID + * @return 操作结果 + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable String id) { + workstationService.deleteWorkstation(id); + return ActionResult.success("删除成功"); + } + + /** + * 添加人员 + * + * @param dto 添加人员DTO + * @return 操作结果 + */ + @PostMapping("/users/add") + public ActionResult addUsers(@RequestBody @Valid WorkstationUserAddDto dto) { + workstationService.addUsers(dto); + return ActionResult.success("添加成功"); + } + + /** + * 删除人员 + * + * @param dto 删除人员DTO + * @return 操作结果 + */ + @DeleteMapping("/users/remove") + public ActionResult removeUser(@RequestBody @Valid WorkstationUserRemoveDto dto) { + workstationService.removeUser(dto); + return ActionResult.success("删除成功"); + } + + /** + * 查询工作站详情 + * + * @param id 工作站ID + * @return 工作站详情 + */ + @GetMapping("/{id}/detail") + public ActionResult detail( + @PathVariable String id) { + return ActionResult.success(workstationService.getDetail(id)); + } + + /** + * 按考勤组查询工作站及其归属员工(含在组日、划线排班权限) + * + * @param groupId 考勤组ID + * @param startTime 查询开始日期(含),yyyy-MM-dd + * @param endTime 查询结束日期(含),yyyy-MM-dd;同时作为成员快照截止日 + */ + @GetMapping("/by-group/{groupId}") + public ActionResult> listByGroupId( + @PathVariable String groupId, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startTime, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endTime) { + return ActionResult.success(workstationService.listWorkstationsByGroupId(groupId, startTime, endTime)); + } + + /** + * 重置工作站人员:恢复为岗位默认归属人员,清除另加入的额外人员 + * + * @param id 工作站ID + */ + @PostMapping("/{id}/reset") + public ActionResult reset(@PathVariable String id) { + workstationService.resetWorkstationUsers(id); + return ActionResult.success("重置成功"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/entity/FixedHandleDTO.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/entity/FixedHandleDTO.java new file mode 100644 index 0000000..27e5b08 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/entity/FixedHandleDTO.java @@ -0,0 +1,32 @@ +package jnpf.attendance.entity; + +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.entity.attendance.AttendanceFestivalRules; +import jnpf.model.attendance.vo.AttendanceShiftSettingVo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FixedHandleDTO { + private List groupIds; + private Date start; + private Date end; + private List attendanceGroupVos; + private List users; + private Map enableBaseSetting; + private Map shiftSettingMap; + private Map> festivalMap; + private String tenantId; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/NotificationClockMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/NotificationClockMQListener.java new file mode 100644 index 0000000..8c54493 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/NotificationClockMQListener.java @@ -0,0 +1,105 @@ +package jnpf.attendance.event; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.base.UserInfo; +import jnpf.constants.MessageTopicConstants; +import jnpf.constants.RedisConstant; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.attendance.vo.UserDayVo; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.Lists; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static jnpf.constants.RedisConstant.ATTENDANCE_NOTIFICATION_CLOCK_USER; + +/** + * 考勤统计-单条消息监听,监听生成用户日统计消息 + * @author 石头 + */ +@Slf4j +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.ATTENDANCE_NOTIFICATION_CLOCK_TOPIC, + consumerGroup = MessageTopicConstants.ATTENDANCE_NOTIFICATION_CLOCK_CONSUMER_GROUP, + consumeMode = ConsumeMode.CONCURRENTLY, + maxReconsumeTimes = 5) +public class NotificationClockMQListener implements RocketMQListener>, RocketMQPushConsumerLifecycleListener { + + @Resource + private RedissonClient redissonClient; + @Autowired + private AttendanceClockInService attendanceClockInService; + + @Override + public void onMessage(List userDayList) { + if (CollUtil.isEmpty(userDayList)) { + return; + } + log.error("接受到排班通知打卡消息,{}", JSONObject.toJSONString(userDayList)); + UserDayVo userDayVo1 = userDayList.get(0); + Assert.isFalse(StringUtil.isEmpty(userDayVo1.getTenantId()), "排班通知打卡tenantId不能为空"); + List locks = Lists.newArrayList(); + try { + for (UserDayVo userDayVo : userDayList) { + String lockKey = String.format(ATTENDANCE_NOTIFICATION_CLOCK_USER, userDayVo.getUserId(), userDayVo.getDay()); + RLock lock = redissonClient.getLock(lockKey); + RLock lock1 = redissonClient.getLock(String.format(RedisConstant.ATTENDANCE_USER_SET_SCHEDULES, userDayVo.getTenantId(), userDayVo.getUserId(), userDayVo.getUserId())); + // 立即尝试获取锁(不等待),失败直接抛异常 + if (lock.isLocked() || lock1.isLocked() || !lock.tryLock(5, 60, TimeUnit.SECONDS)) { + throw new RuntimeException(userDayVo.getUserId() + "通知打卡获取锁失败"); + } + locks.add(lock); + } + TenantDataSourceUtil.switchTenant(userDayVo1.getTenantId()); + UserInfo user = new UserInfo(); + user.setUserId(userDayVo1.getUserId()); + user.setTenantId(userDayVo1.getTenantId()); + // 执行业务(异常直接向上抛出) + attendanceClockInService.changeAttendanceRuleBatch(userDayList, user); + + } catch (Exception ex) { + log.error("处理排班通知打卡消息失败,触发重试,消息 {} ", JSONObject.toJSONString(userDayList), ex); + throw new RuntimeException("处理排班通知打卡消息失败,触发重试", ex); + } finally { + // 安全释放:仅当前线程持有时释放,异常仅记录不抛出 + locks.forEach(lock -> { + if (lock != null && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + }); + } + } + + @Override + public void prepareStart(DefaultMQPushConsumer consumer) { + // 优化消费参数 每次拉取的消息数量 + consumer.setPullBatchSize(32); + // 设置消费间隔,避免过于频繁拉取 + consumer.setPullInterval(200); + // 设置消费超时时间(分钟) + consumer.setConsumeTimeout(10); + // 设置消费起始位置(从上次消费的位置继续) + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + // 调整消费线程池最小线程数 + consumer.setConsumeThreadMin(10); + // 调整消费线程池最大线程数 + consumer.setConsumeThreadMax(20); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingConsumerMQListener.java new file mode 100644 index 0000000..238c152 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingConsumerMQListener.java @@ -0,0 +1,82 @@ +package jnpf.attendance.event; + +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.message.model.permission.PermissionRelationOrganizeUserListDTO; +import jnpf.model.attendance.model.OrganizeUserConsumerDTO; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static jnpf.constants.RedisConstant.GROUP_JOIN_USERS_KEY; +import static jnpf.constants.RedisConstant.GROUP_ONBOARDING_KEY; + +/** + * 入职请求信息 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.PERSONNEL_ONBOARDING_TOPIC, + consumerGroup = MessageTopicConstants.ONBOARDING_GROUP, + replyTimeout = 600000, + maxReconsumeTimes = 3, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class OnboardingConsumerMQListener implements RocketMQListener { + @Autowired + private AttendanceUserService attendanceUserService; + @Autowired + private RedissonClient redissonClient; + + @Override + public void onMessage(String message) { + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接收入职/变动请求信息{}", message); + OrganizeUserConsumerDTO messageList = JSON.parseObject(message, OrganizeUserConsumerDTO.class); + if (StringUtil.equals(messageList.getBeforeOrgId(), messageList.getOrganizeId())) { + return; + } + RLock lock = null; + try { + String key = String.format(GROUP_JOIN_USERS_KEY, messageList.getUserId()); + lock = redissonClient.getLock(key); + if (!lock.tryLock(5, 10, TimeUnit.SECONDS)) { + log.error("接收组织人员添加通知,目前用户【{}】正在被变更!", messageList.getUserId()); + throw new RuntimeException(String.format("接收组织人员添加通知,目前用户【%s】正在被变更,触发重试", messageList.getUserId())); + } + PermissionRelationOrganizeUserListDTO permissionRelationOrganizeUserListDTO = new PermissionRelationOrganizeUserListDTO(); + permissionRelationOrganizeUserListDTO.setUserId(messageList.getUserId()); + permissionRelationOrganizeUserListDTO.setOldOrganizeId(messageList.getBeforeOrgId()); + permissionRelationOrganizeUserListDTO.setOrganizeId(messageList.getOrganizeId()); + permissionRelationOrganizeUserListDTO.setEntryDate(messageList.getEntryDate()); + permissionRelationOrganizeUserListDTO.setTenantId(messageList.getTenantId()); + TenantDataSourceUtil.switchTenant(messageList.getTenantId()); + if (StringUtil.isNotEmpty(messageList.getBeforeOrgId())) { + attendanceUserService.removeUsers(List.of(permissionRelationOrganizeUserListDTO), false); + } + attendanceUserService.addUsers(List.of(permissionRelationOrganizeUserListDTO), true); + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + throw new RuntimeException(e.getMessage()); + } finally{ + if (Objects.nonNull(lock) && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingFailConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingFailConsumerMQListener.java new file mode 100644 index 0000000..7715262 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OnboardingFailConsumerMQListener.java @@ -0,0 +1,58 @@ +package jnpf.attendance.event; + +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.message.model.permission.PermissionRelationOrganizeUserListDTO; +import jnpf.model.attendance.model.TurnoverConsumerModel; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; + +/** + * 终止入职信息 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.PERSONNEL_SECONDMENT_FAIL_TOPIC, + consumerGroup = MessageTopicConstants.ONBOARDING_FAIL_GROUP, + replyTimeout = 600000, + maxReconsumeTimes = 3, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class OnboardingFailConsumerMQListener implements RocketMQListener { + @Autowired + private AttendanceUserService attendanceUserService; + + @Override + public void onMessage(String message) { + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接受终止入职信息{}", message); + try { + TurnoverConsumerModel messageList = JSON.parseObject(message, TurnoverConsumerModel.class); + if (Objects.isNull(messageList)) { + return; + } + TenantDataSourceUtil.switchTenant(messageList.getTenantId()); + PermissionRelationOrganizeUserListDTO permissionRelationOrganizeUserListDTO = new PermissionRelationOrganizeUserListDTO(); + permissionRelationOrganizeUserListDTO.setOldOrganizeId(messageList.getCurrOrgId()); + permissionRelationOrganizeUserListDTO.setUserId(messageList.getUserId()); + //删 + attendanceUserService.removeUsers(List.of(permissionRelationOrganizeUserListDTO),false); + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + } + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeConsumerMQListener.java new file mode 100644 index 0000000..e500640 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeConsumerMQListener.java @@ -0,0 +1,58 @@ +package jnpf.attendance.event; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.message.enums.permission.v2.OperationTypeMessageEnums; +import jnpf.message.model.permission.v2.OrganizeUpdateMessageDTO; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 组织变更消费者 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.PERMISSION_TOPIC, + consumerGroup = MessageTopicConstants.JNPF_GROUP, + selectorExpression = MessageTopicConstants.TAG_ORGANIZE, + replyTimeout = 600000, + maxReconsumeTimes = 3, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class OrganizeConsumerMQListener implements RocketMQListener { + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + public void onMessage(String message) { + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接受组织变更信息{}", message); + try { + List messageList = JSON.parseArray(message, OrganizeUpdateMessageDTO.class); + if (CollUtil.isEmpty(messageList) || StringUtil.isEmpty(messageList.get(0).getId())) { + return; + } + TenantDataSourceUtil.switchTenant(messageList.get(0).getTenantId()); + List collect = messageList.stream().filter(vo -> Objects.equals(vo.getOperationTypeEnum(), OperationTypeMessageEnums.ADD) || Objects.equals(vo.getOperationTypeEnum(), OperationTypeMessageEnums.UPDATE)).collect(Collectors.toList()); + attendanceGroupService.batchSaveGroupByOrgIds(collect.get(0).getTenantId(), collect.stream().map(OrganizeUpdateMessageDTO::getId).collect(Collectors.toList())); + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + } + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeUserConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeUserConsumerMQListener.java new file mode 100644 index 0000000..90c85db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/OrganizeUserConsumerMQListener.java @@ -0,0 +1,54 @@ +package jnpf.attendance.event; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.constants.MessageTopicConstants; +import jnpf.message.model.permission.PermissionRelationOrganizeUserListDTO; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 组织下人员变更 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.PERMISSION_TOPIC, + consumerGroup = MessageTopicConstants.JNPF_USER_GROUP, + selectorExpression = MessageTopicConstants.ORGANIZE_RELATION_USER, + replyTimeout = 600000, + maxReconsumeTimes = 3, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class OrganizeUserConsumerMQListener implements RocketMQListener { + @Autowired + private AttendanceUserService attendanceUserService; + + @Override + public void onMessage(String message) { + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接受组织下人员(批量入职/批量删除/预入职/离职)变更信息{}", message); + try { + List messageList = JSON.parseArray(message, PermissionRelationOrganizeUserListDTO.class); + if (CollUtil.isEmpty(messageList) || StringUtil.isEmpty(messageList.get(0).getUserId())) { + log.error("接受组织下人员无效数据过滤{}", message); + return; + } + //增删改 + attendanceUserService.orgUpdateHandle(messageList); + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + } + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentConsumerMQListener.java new file mode 100644 index 0000000..b2fadc2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentConsumerMQListener.java @@ -0,0 +1,86 @@ +package jnpf.attendance.event; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.ApproveException; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceSecondedDto; +import jnpf.model.personnels.dto.secondment.msg.SecondmentMsgUserInfo; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 借调申请消息 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.PERSONNEL_SECONDMENT_TOPIC, + consumerGroup = MessageTopicConstants.SECONDMENT_GROUP, + replyTimeout = 600000, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class SecondmentConsumerMQListener implements RocketMQListener { + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + public void onMessage(String message) { + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接受借调申请信息{}", message); + try { + SecondmentMsgUserInfo messageList = JSONUtil.toBean(message, SecondmentMsgUserInfo.class); + if (Objects.isNull(messageList)) { + return; + } + if (StringUtil.equals(messageList.getOriginalOrganizationId(), messageList.getSecondedOrganizationId())) { + return; + } + AttendanceSecondedDto bean = new AttendanceSecondedDto(); + bean.setBackTime(messageList.getSecondmentEndTime()); + bean.setDepartureTime(messageList.getSecondmentStartTime()); + bean.setStartTime(messageList.getSecondmentStartTime()); + bean.setEndTime(messageList.getSecondmentEndTime()); + bean.setUserIds(List.of(messageList.getUserId())); + TenantDataSourceUtil.switchTenant(messageList.getTenantId()); + Map org2grouopMap = attendanceGroupService.batchSaveGroupByOrgIds(messageList.getTenantId(), CollUtil.newArrayList(messageList.getOriginalOrganizationId(), messageList.getSecondedOrganizationId())); + if (!org2grouopMap.containsKey(messageList.getOriginalOrganizationId()) || !org2grouopMap.containsKey(messageList.getSecondedOrganizationId())) { + throw new ApproveException("组织不存在"); + } + bean.setSelfGroupId(org2grouopMap.get(messageList.getOriginalOrganizationId())); + bean.setGroupId(org2grouopMap.get(messageList.getSecondedOrganizationId())); + try { + attendanceGroupService.secondedStart(bean); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + //修改用户排班 + try { + attendanceDailyRuleService.secondmentDailyRuleHandle(bean.getUserIds(), bean.getSelfGroupId(), bean.getGroupId(), bean.getStartTime(), bean.getEndTime(), bean.getDepartureTime(), bean.getBackTime(), messageList.getTenantId()); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + } + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentWithdrawalConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentWithdrawalConsumerMQListener.java new file mode 100644 index 0000000..19cffc0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/SecondmentWithdrawalConsumerMQListener.java @@ -0,0 +1,94 @@ +package jnpf.attendance.event; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.model.attendance.vo.SecondmentDateVo; +import jnpf.model.personnels.dto.secondment.msg.SecondmentMsgUserInfo; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.Lists; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 借调撤销消息 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.SECONDMENT_WITHDRAWAL_OUTPUT, + consumerGroup = MessageTopicConstants.SECONDMENT_WITHDRAWAL_GROUP, + replyTimeout = 600000, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class SecondmentWithdrawalConsumerMQListener implements RocketMQListener { + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceUserService attendanceUserService; + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + public void onMessage(String message) { + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接受借调撤回信息{}", message); + try { + SecondmentMsgUserInfo messageList = JSONUtil.toBean(message, SecondmentMsgUserInfo.class); + if (Objects.isNull(messageList)) { + return; + } + TenantDataSourceUtil.switchTenant(messageList.getTenantId()); + //存在排班的借调撤回不执行 + Assert.isFalse(attendanceDailyRuleService.hasRuleByUserIdAndTime(messageList.getUserId(), messageList.getSecondmentStartTime(), messageList.getSecondmentEndTime()), "存在排班的借调撤回不执行"); + List byOrgId = attendanceGroupService.getByOrgIds(List.of(messageList.getOriginalOrganizationId(), messageList.getSecondedOrganizationId())); + Map group2orgMap = byOrgId.stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getOrgId)); + List userList = attendanceUserService.list(new LambdaQueryWrapper() + .in(AttendanceGroupUser::getGroupId, byOrgId.stream().map(AttendanceGroup::getId).collect(Collectors.toList())) + .eq(AttendanceGroupUser::getUserId, messageList.getUserId()) + ); + //删除借调组数据 + for (AttendanceGroupUser next : userList) { + String orgId = group2orgMap.get(next.getGroupId()); + List list = StringUtil.isEmpty(next.getTimeJson()) ? Lists.newArrayList() : JSONUtil.toList(next.getTimeJson(), SecondmentDateVo.class); + if (list.stream().noneMatch(item -> item.getStartTime().compareTo(messageList.getSecondmentStartTime()) == 0)) { + continue; + } + //如果是借调组或者被借调组,则根据借调时间范围找到timeJSON中指定时段并清除 + list.removeIf(item -> item.getStartTime().compareTo(messageList.getSecondmentStartTime()) == 0); + if (CollUtil.isNotEmpty(list) || Objects.equals(next.getType(),1)) { + next.setTimeJson(JSONUtil.toJsonStr(list)); + attendanceUserService.updateById(next); + continue; + } + //当借调组的timeJson为空时,则删除该用户信息 + if (StringUtil.equals(orgId, messageList.getSecondedOrganizationId())) { + attendanceUserService.removeById(next); + } + } + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + } + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsBatchClearMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsBatchClearMQListener.java new file mode 100644 index 0000000..780a385 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsBatchClearMQListener.java @@ -0,0 +1,71 @@ +package jnpf.attendance.event; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.constants.MessageTopicConstants; +import jnpf.model.attendance.event.StatisticsBatchClearDto; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Objects; + +/** + * 批量清除日统计消息监听,监听批量清除日统计消息 + * @author 石头 + */ +@Slf4j +@Component +@RocketMQMessageListener(topic = MessageTopicConstants.ATTENDANCE_STATISTICS_BATCH_CLEAR_TOPIC, + consumerGroup = MessageTopicConstants.ATTENDANCE_STATISTICS_BATCH_CLEAR_CONSUMER_GROUP, + consumeMode = ConsumeMode.CONCURRENTLY, + maxReconsumeTimes = 10) +public class StatisticsBatchClearMQListener implements RocketMQListener, RocketMQPushConsumerLifecycleListener { + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + + @Override + public void onMessage(StatisticsBatchClearDto batchClearDto) { + if (Objects.isNull(batchClearDto)) { + log.warn("接收到空消息,忽略处理"); + return; + } + log.info("接受到一条批量清除日统计消息,{}", JSONObject.toJSONString(batchClearDto)); + if (CollUtil.isEmpty(batchClearDto.getUserIdList()) || StringUtil.isEmpty(batchClearDto.getTenantId()) || + StringUtil.isEmpty(batchClearDto.getGroupId()) || (Objects.isNull(batchClearDto.getDay()) && Objects.isNull(batchClearDto.getStartDay()))){ + log.error("消费批量清除日统计消息失败:消息格式无效,内容: {}", JSONObject.toJSONString(batchClearDto)); + return; + } + try { + attendanceDayStatisticsService.batchStatisticDataClear(batchClearDto); + } catch (Exception ex) { + log.error("消费批量清除日统计消息消费失败,触发重试,消息 {}, 错误:{} ", JSONObject.toJSONString(batchClearDto), ExceptionUtil.stacktraceToString(ex)); + throw new RuntimeException("消费批量清除日统计消息消费失败,触发重试", ex); + } + } + + @Override + public void prepareStart(DefaultMQPushConsumer consumer) { + // 优化消费参数 每次拉取的消息数量 + consumer.setPullBatchSize(32); + // 设置消费间隔,避免过于频繁拉取 + consumer.setPullInterval(200); + // 设置消费超时时间(分钟) + consumer.setConsumeTimeout(10); + // 设置消费起始位置(从上次消费的位置继续) + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + // 调整消费线程池最小线程数 + consumer.setConsumeThreadMin(10); + // 调整消费线程池最大线程数 + consumer.setConsumeThreadMax(20); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleHistoryMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleHistoryMQListener.java new file mode 100644 index 0000000..5402694 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleHistoryMQListener.java @@ -0,0 +1,104 @@ +package jnpf.attendance.event; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.model.TenantVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.model.attendance.event.StatisticsSingleHistoryDto; +import jnpf.util.DateUtil; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 考勤统计-单条消息监听,监听生成历史数据处理 + * @author 石头 + */ +@Slf4j +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_HISTORY_TOPIC, + consumerGroup = MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_HISTORY_CONSUMER_GROUP, + consumeMode = ConsumeMode.CONCURRENTLY, + maxReconsumeTimes = 5) +public class StatisticsSingleHistoryMQListener implements RocketMQListener, RocketMQPushConsumerLifecycleListener { + + @Resource + private RedissonClient redissonClient; + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + + @Override + public void onMessage(StatisticsSingleHistoryDto singleDto) { + if (Objects.isNull(singleDto)) { + log.warn("接收到空消息,忽略处理"); + return; + } + log.info("接受到一条历史数据处理消息,{}", JSONObject.toJSONString(singleDto)); + // 校验必要字段 + if (ObjectUtil.isNull(singleDto) || StringUtil.isEmpty(singleDto.getUserId()) || + StringUtil.isEmpty(singleDto.getTenantId()) || StringUtil.isEmpty(singleDto.getGroupId()) || + ObjectUtil.isNull(singleDto.getDay())) { + log.error("生成历史数据处理消费失败:消息格式无效,内容: {}", JSONObject.toJSONString(singleDto)); + return; + } + try { + String lockKey = String.format(StatisticsEnumUtil.AttendanceClockEnum.DAY_STATISTICS.getKey(), singleDto.getTenantId(), singleDto.getGroupId(), + singleDto.getUserId(), DateUtil.daFormat(singleDto.getDay())); + RLock lock = redissonClient.getLock(lockKey); + try { + // 立即尝试获取锁(不等待),失败直接抛异常 + if (!lock.tryLock(5, 20, TimeUnit.SECONDS)) { + throw new RuntimeException("获取锁失败"); + } + // 租户不为空就需要切库 + TenantVO tenantVO = TenantDataSourceUtil.switchTenant(singleDto.getTenantId()); + log.error("切换tenant对象{}", JSON.toJSONString(tenantVO)); + attendanceDayStatisticsService.statisticDataChangeHistory(singleDto); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("获取锁时线程被中断: " + lockKey, e); + } finally { + // 安全释放:仅当前线程持有时释放,异常仅记录不抛出 + if (lock.isHeldByCurrentThread() && lock.isHeldByThread(Thread.currentThread().getId())) { + lock.unlock(); + } + } + } catch (Exception ex) { + log.info("处理历史数据处理失败,触发重试,消息 {}, 错误:{} ", JSONObject.toJSONString(singleDto), ex.getMessage()); + throw new RuntimeException("处理历史数据处理失败,触发重试", ex); + } + } + + @Override + public void prepareStart(DefaultMQPushConsumer consumer) { + // 优化消费参数 每次拉取的消息数量 + consumer.setPullBatchSize(32); + // 设置消费间隔,避免过于频繁拉取 + consumer.setPullInterval(200); + // 设置消费超时时间(分钟) + consumer.setConsumeTimeout(10); + // 设置消费起始位置(从上次消费的位置继续) + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + // 调整消费线程池最小线程数 + consumer.setConsumeThreadMin(10); + // 调整消费线程池最大线程数 + consumer.setConsumeThreadMax(20); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleMQListener.java new file mode 100644 index 0000000..03714ad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/event/StatisticsSingleMQListener.java @@ -0,0 +1,103 @@ +package jnpf.attendance.event; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.constants.MessageTopicConstants; +import jnpf.database.model.TenantVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.util.DateUtil; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.spring.annotation.ConsumeMode; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 考勤统计-单条消息监听,监听生成用户日统计消息 + * @author 石头 + */ +@Slf4j +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, + consumerGroup = MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_CONSUMER_GROUP, + consumeMode = ConsumeMode.CONCURRENTLY, + maxReconsumeTimes = 5) +public class StatisticsSingleMQListener implements RocketMQListener, RocketMQPushConsumerLifecycleListener { + + @Resource + private RedissonClient redissonClient; + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + + @Override + public void onMessage(StatisticsSingleDto singleDto) { + if (Objects.isNull(singleDto)) { + log.warn("接收到空消息,忽略处理"); + return; + } + log.error("接受到一条生成用户日统计消息,{}", JSONObject.toJSONString(singleDto)); + // 校验必要字段 + if (ObjectUtil.isNull(singleDto) || StringUtil.isEmpty(singleDto.getUserId()) || + StringUtil.isEmpty(singleDto.getTenantId()) || StringUtil.isEmpty(singleDto.getGroupId()) || + ObjectUtil.isNull(singleDto.getDay())) { + log.error("生成用户日统计消息消费失败:消息格式无效,内容: {}", JSONObject.toJSONString(singleDto)); + return; + } + try { + String lockKey = String.format(StatisticsEnumUtil.AttendanceClockEnum.DAY_STATISTICS.getKey(), singleDto.getTenantId(), + singleDto.getGroupId(), singleDto.getUserId(), DateUtil.daFormat(singleDto.getDay())); + RLock lock = redissonClient.getLock(lockKey); + try { + if (!lock.tryLock(5, 20, TimeUnit.SECONDS)) { + throw new RuntimeException("获取锁失败"); + } + // 租户不为空就需要切库 + TenantVO tenantVO = TenantDataSourceUtil.switchTenant(singleDto.getTenantId()); + log.error("切换tenant对象{}", JSON.toJSONString(tenantVO)); + attendanceDayStatisticsService.statisticDataChange(singleDto); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("获取锁时线程被中断: " + lockKey, e); + } finally { + // 安全释放:仅当前线程持有时释放,异常仅记录不抛出 + if (lock.isHeldByCurrentThread() && lock.isHeldByThread(Thread.currentThread().getId())) { + lock.unlock(); + } + } + } catch (Exception ex) { + log.error("处理用户日统计消息失败,触发重试,消息 {}, 错误:{} ", JSONObject.toJSONString(singleDto), ex.getMessage()); + throw new RuntimeException("处理用户日统计消息失败,触发重试", ex); + } + } + + @Override + public void prepareStart(DefaultMQPushConsumer consumer) { + // 优化消费参数 每次拉取的消息数量 + consumer.setPullBatchSize(32); + // 设置消费间隔,避免过于频繁拉取 + consumer.setPullInterval(200); + // 设置消费超时时间(分钟) + consumer.setConsumeTimeout(10); + // 设置消费起始位置(从上次消费的位置继续) + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET); + // 调整消费线程池最小线程数 + consumer.setConsumeThreadMin(10); + // 调整消费线程池最大线程数 + consumer.setConsumeThreadMax(20); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/AttendanceBookMergeHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/AttendanceBookMergeHandler.java new file mode 100644 index 0000000..e5b6baf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/AttendanceBookMergeHandler.java @@ -0,0 +1,129 @@ +package jnpf.attendance.excel; + +import com.alibaba.excel.write.handler.CellWriteHandler; +import com.alibaba.excel.write.handler.context.CellWriteHandlerContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; + +import java.util.List; + +/** + * 考勤本导出单元格合并处理器 + * 使用CellWriteHandler在数据写入过程中执行合并,避免与表头合并冲突 + * + * @author Generated + * @date 2026-04-29 + */ +@Slf4j +public class AttendanceBookMergeHandler implements CellWriteHandler { + + /** + * 需要合并的单元格范围列表(数据行索引,不包含表头) + */ + private final List mergeCells; + + /** + * 表头行数(用于偏移数据行索引) + */ + private final int headerRowCount; + + /** + * 预期数据总行数(用于判断是否是最后一行) + */ + private final int expectedDataRowCount; + + /** + * 额外表头行数(标题行+说明行) + */ + private final int extraHeaderRows; + + /** + * 总列数(用于合并标题和说明行) + */ + private final int totalColumns; + + /** + * 是否已执行合并 + */ + private boolean merged = false; + + public AttendanceBookMergeHandler(List mergeCells, int headerRowCount, int expectedDataRowCount, int totalColumns) { + this.mergeCells = mergeCells; + this.headerRowCount = headerRowCount; + this.expectedDataRowCount = expectedDataRowCount; + this.extraHeaderRows = 0; // 使用3层表头,不需要额外行 + this.totalColumns = totalColumns; + } + + @Override + public void afterCellDispose(CellWriteHandlerContext context) { + // 只在未合并且是数据行(非表头)时才执行合并 + if (!merged && mergeCells != null && !mergeCells.isEmpty()) { + Integer rowIndex = context.getRowIndex(); + Boolean isHead = context.getHead(); + + // 跳过表头行 + if (isHead != null && isHead) { + return; + } + + // 在写入最后一行数据后执行合并 + if (rowIndex != null && rowIndex >= headerRowCount + expectedDataRowCount - 1) { + Sheet sheet = context.getWriteSheetHolder().getSheet(); + + try { + int mergedCount = 0; + + // 合并数据区域(使用3层表头,偏移量为headerRowCount) + for (CellRangeAddress address : mergeCells) { + int offset = headerRowCount; // 3层表头 + CellRangeAddress adjustedAddress = new CellRangeAddress( + address.getFirstRow() + offset, + address.getLastRow() + offset, + address.getFirstColumn(), + address.getLastColumn() + ); + + // 检查是否与现有合并区域冲突 + if (!hasConflict(sheet, adjustedAddress)) { + sheet.addMergedRegion(adjustedAddress); + mergedCount++; + } else { + log.warn("跳过冲突的合并区域: {}", adjustedAddress); + } + } + merged = true; + log.info("考勤本导出单元格合并完成,成功合并 {} 个区域,表头行数: {}, 数据行数: {}", + mergedCount, headerRowCount, expectedDataRowCount); + } catch (Exception e) { + log.error("考勤本导出单元格合并失败: {}", e.getMessage(), e); + } + } + } + } + + /** + * 检查合并区域是否与现有合并区域冲突 + */ + private boolean hasConflict(Sheet sheet, CellRangeAddress newAddress) { + int numMergedRegions = sheet.getNumMergedRegions(); + for (int i = 0; i < numMergedRegions; i++) { + CellRangeAddress existing = sheet.getMergedRegion(i); + if (isOverlap(newAddress, existing)) { + return true; + } + } + return false; + } + + /** + * 检查两个合并区域是否重叠 + */ + private boolean isOverlap(CellRangeAddress addr1, CellRangeAddress addr2) { + return !(addr1.getLastRow() < addr2.getFirstRow() || + addr1.getFirstRow() > addr2.getLastRow() || + addr1.getLastColumn() < addr2.getFirstColumn() || + addr1.getFirstColumn() > addr2.getLastColumn()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/CompositeWriteHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/CompositeWriteHandler.java new file mode 100644 index 0000000..80f88f8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/CompositeWriteHandler.java @@ -0,0 +1,52 @@ +package jnpf.attendance.excel; + +import com.alibaba.excel.write.handler.CellWriteHandler; +import com.alibaba.excel.write.handler.RowWriteHandler; +import com.alibaba.excel.write.handler.context.CellWriteHandlerContext; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteTableHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Row; + +/** + * 组合写入处理器 + * 用于同时应用多个Handler(样式Handler + 合并Handler) + * + * @author Generated + * @date 2026-04-29 + */ +@Slf4j +public class CompositeWriteHandler implements RowWriteHandler, CellWriteHandler { + + private final RowWriteHandler rowHandler; + private final CellWriteHandler cellHandler; + + public CompositeWriteHandler(RowWriteHandler rowHandler, CellWriteHandler cellHandler) { + this.rowHandler = rowHandler; + this.cellHandler = cellHandler; + } + + @Override + public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) { + // 执行样式Handler + if (rowHandler != null) { + try { + rowHandler.afterRowDispose(writeSheetHolder, writeTableHolder, row, relativeRowIndex, isHead); + } catch (Exception e) { + log.error("RowHandler执行失败: {}", e.getMessage(), e); + } + } + } + + @Override + public void afterCellDispose(CellWriteHandlerContext context) { + // 执行单元格Handler(包含合并逻辑) + if (cellHandler != null) { + try { + cellHandler.afterCellDispose(context); + } catch (Exception e) { + log.error("CellHandler执行失败: {}", e.getMessage(), e); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/HeadStyleHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/HeadStyleHandler.java new file mode 100644 index 0000000..6b31dfa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/HeadStyleHandler.java @@ -0,0 +1,251 @@ +package jnpf.attendance.excel; + +import com.alibaba.excel.write.handler.RowWriteHandler; +import com.alibaba.excel.write.handler.SheetWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteTableHolder; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; + + +public class HeadStyleHandler implements RowWriteHandler, SheetWriteHandler { + @Override + public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) { + Sheet sheet = writeSheetHolder.getSheet(); + XSSFCellStyle contextCellStyle = getContextCellStyle(sheet); + int rowNum = row.getRowNum(); + Workbook workbook = sheet.getWorkbook(); + Font font = workbook.createFont(); + font.setFontHeight((short) (20 * 18)); + font.setColor(IndexedColors.WHITE.getIndex()); + for (Cell cell : row) { + if (isHead) { + if (textSetRedFont(cell)) { + contextCellStyle.setAlignment(HorizontalAlignment.LEFT); + cell.setCellStyle(contextCellStyle); + continue; + } + // 3层表头结构: + // rowNum == 0: 标题行(蓝色背景,白色字体) + // rowNum == 1: 说明行(黑色字体,左对齐,无背景) + // rowNum == 2: 表头行(黑色字体,大小11,无背景) + if (rowNum == 0) { + // 标题行:蓝色背景,白色字体 + contextCellStyle.setFont(font); + contextCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + contextCellStyle.setFillForegroundColor(new XSSFColor(new java.awt.Color(48, 84, 150), null)); + } else if (rowNum == 1) { + // 说明行:黑色字体,左对齐,无背景 + Font noteFont = workbook.createFont(); + noteFont.setFontHeight((short) (20 * 11)); + noteFont.setColor(IndexedColors.BLACK.getIndex()); + contextCellStyle.setFont(noteFont); + contextCellStyle.setAlignment(HorizontalAlignment.LEFT); + contextCellStyle.setFillPattern(FillPatternType.NO_FILL); + contextCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); + } else if (rowNum == 2) { + // 表头行:黑色字体,大小11,无背景 + Font headerFont = workbook.createFont(); + headerFont.setFontHeight((short) (20 * 11)); + headerFont.setColor(IndexedColors.BLACK.getIndex()); + contextCellStyle.setFont(headerFont); + contextCellStyle.setFillPattern(FillPatternType.NO_FILL); + contextCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); + } + } + cell.setCellStyle(contextCellStyle); + } + + // 实现自动行高调整,使用手动计算方式避免兼容性问题 + calculateAndSetRowHeight(row); + } + + /** + * 手动计算并设置行高 + * @param row 行对象 + */ + private void calculateAndSetRowHeight(Row row) { + float defaultRowHeight = 30.0f; // 默认行高 + float maxCellHeight = defaultRowHeight; + + // 遍历行中的每个单元格,根据内容计算所需高度 + for (Cell cell : row) { + String cellValue = getCellValueAsString(cell); + if (cellValue != null) { + // 计算基于内容的行高 + float cellHeight = calculateCellHeight(cellValue); + if (cellHeight > maxCellHeight) { + maxCellHeight = cellHeight; + } + } + } + + // 设置行高,确保不低于默认行高 + row.setHeightInPoints(Math.max(maxCellHeight, defaultRowHeight)); + } + + /** + * 根据单元格内容和列宽计算所需行高 + * @param content 单元格内容 + * @return 计算的行高 + */ + private float calculateCellHeight(String content) { + if (content == null || content.isEmpty()) { + return 19.0f; + } + // 计算需要的行数 + String[] lines = content.split("\n"); + int lineCount = lines.length; + + + // 每行大约15点高度 + return lineCount * 19; + } + + public Boolean textSetRedFont(Cell cell) { + String cellValue = getCellValueAsString(cell); + if (cellValue != null && cellValue.contains("[RED]")) { + Workbook workbook = cell.getSheet().getWorkbook(); + // 创建红色字体 + Font redFont = workbook.createFont(); + redFont.setColor(IndexedColors.RED.getIndex()); + redFont.setFontHeight((short) (20 * 14)); + redFont.setBold(true); + // 创建正常字体 + Font normalFont = workbook.createFont(); + normalFont.setColor(IndexedColors.BLACK.getIndex()); + normalFont.setFontHeight((short) (20 * 12)); + // 移除 [RED] 标记并保留原始内容 + String cleanValue = cellValue.replace("[RED]", ""); + XSSFRichTextString richText = new XSSFRichTextString(cleanValue); + // 分割文本为行 + String[] lines = cleanValue.split("\n"); + int currentIndex = 0; + for (String line : lines) { + // 检查当前行是否包含 [RED] 标记 + if (cellValue.contains("[RED]"+ line)) { + richText.applyFont(currentIndex, currentIndex + line.length(), redFont); + } else { + richText.applyFont(currentIndex, currentIndex + line.length(), normalFont); + } + currentIndex += line.length() + 1; // +1 for \n + } + + cell.setCellValue(richText); + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + /** + * 安全地获取单元格的字符串值 + * + * @param cell 单元格对象 + * @return 单元格的字符串表示 + */ + private String getCellValueAsString(Cell cell) { + if (cell == null) { + return null; + } + + try { + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + // 数值转字符串 + return String.valueOf(cell.getNumericCellValue()); + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + // 尝试获取公式结果 + try { + return cell.getStringCellValue(); + } catch (Exception e) { + try { + return String.valueOf(cell.getNumericCellValue()); + } catch (Exception ex) { + return cell.getCellFormula(); + } + } + case BLANK: + default: + return ""; + } + } catch (Exception e) { + // 如果出现异常,返回空字符串而不是抛出异常 + return ""; + } + } + + private XSSFCellStyle getContextCellStyle(Sheet sheet) { + Workbook workbook = sheet.getWorkbook(); + Font font = workbook.createFont(); + font.setBold(false); + font.setFontName("宋体"); + font.setFontHeight((short) (20 * 11)); + XSSFCellStyle headCellStyle = (XSSFCellStyle) workbook.createCellStyle(); + //居中 + headCellStyle.setAlignment(HorizontalAlignment.CENTER); + headCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); + //设置表头高度 + //上下居中 + headCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headCellStyle.setFont(font); + // 设置自动换行; + headCellStyle.setWrapText(true); + return headCellStyle; + } + + @Override + public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + // 不需要实现 + } + + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + // 在所有数据写入完成后,设置表头列宽自适应 + Sheet sheet = writeSheetHolder.getSheet(); + autoSizeHeaderColumns(sheet, 3); // 前3行是表头 + } + + /** + * 设置表头列宽自适应 + */ + private void autoSizeHeaderColumns(Sheet sheet, int headerRowCount) { + if (sheet.getRow(0) == null) { + return; + } + + // 遍历所有列 + for (int col = 0; col < sheet.getRow(0).getLastCellNum(); col++) { + // 遍历表头行,找到最长的内容 + int maxWidth = 0; + for (int row = 0; row < headerRowCount; row++) { + Row headerRow = sheet.getRow(row); + if (headerRow != null) { + Cell cell = headerRow.getCell(col); + if (cell != null) { + String content = cell.getStringCellValue(); + if (content != null && !content.isEmpty()) { + // 中文字符算2个单位,英文算1个单位 + int width = content.replaceAll("[\u4e00-\u9fa5]", "**").length(); + maxWidth = Math.max(maxWidth, width); + } + } + } + } + // 设置列宽,额外增加2个字符的边距 + if (maxWidth > 0) { + sheet.setColumnWidth(col, (maxWidth + 2) * 256); + } + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/CustomStringStringConverter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/CustomStringStringConverter.java new file mode 100644 index 0000000..865b263 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/CustomStringStringConverter.java @@ -0,0 +1,41 @@ +package jnpf.attendance.excel.listener; + + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.converters.ReadConverterContext; +import com.alibaba.excel.converters.WriteConverterContext; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.data.WriteCellData; + +public class CustomStringStringConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + /** + * 这里读的时候会调用 + * + * @param context + * @return + */ + @Override + public String convertToJavaData(ReadConverterContext context) { + return context.getReadCellData().getStringValue(); + } + + /** + * 这里是写的时候会调用 不用管 + * + * @return + */ + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>(context.getValue()); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/LineSchedulesDataListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/LineSchedulesDataListener.java new file mode 100644 index 0000000..4482cfa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/LineSchedulesDataListener.java @@ -0,0 +1,161 @@ +package jnpf.attendance.excel.listener; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import jnpf.attendance.service.impl.AttendanceDailyRuleServiceImpl; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingConfig; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.ScheduleImportEnum; +import jnpf.model.attendance.model.ScheduleImportFailModel; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.DateDetail; +import jnpf.util.StringUtil; +import jnpf.util.attendance.DailyRuleUtil; +import jnpf.util.attendance.RuleExcelImportUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 +@Slf4j +public class LineSchedulesDataListener extends AnalysisEventListener> { + + /** + * 缓存的数据 + */ + private final List cachedDataList; + private final List failList; + private final FtbAttendanceLineSchedulingConfig lineSchedulingConfig; + private final Date month; + private final String groupId; + private final List heads; + + private final Map userMap; + private final Map selfGroupMap; + + private int headRow = 0; + + public LineSchedulesDataListener(String groupId, List cachedDataList, List failList, List heads, Date month, Map userMap, FtbAttendanceLineSchedulingConfig lineSchedulingConfig, Map selfGroupMap) { + this.cachedDataList = cachedDataList; + this.failList = failList; + this.heads = heads; + this.month = month; + this.groupId = groupId; + this.userMap = userMap; + this.lineSchedulingConfig = lineSchedulingConfig; + this.selfGroupMap = selfGroupMap; + } + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + // 处理表头数据 + // headMap的key是列索引,value是表头名称 + headRow++; + if (context.readRowHolder().getRowIndex() < 2) { + return; + } + Assert.isFalse(heads.size() != headMap.size() || + !CollectionUtils.isEqualCollection(heads, headMap.values()), ScheduleImportEnum.IMPORT_ERROR_FORMAT_WRONG.getMessage()); + Assert.isFalse(Objects.isNull(lineSchedulingConfig), ScheduleImportEnum.IMPORT_ERROR_FORMAT_WRONG.getMessage()); + } + + @Override + public void invoke(Map data, AnalysisContext context) { + Assert.isFalse(headRow != 3, ScheduleImportEnum.IMPORT_ERROR_FORMAT_WRONG.getMessage()); + // 处理每一行数据 + // data的key是列索引,value是单元格值 + //用户人员信息校验 + String userName = data.get(0); + String phone = data.get(1); + //划线排班人员校验 + if (RuleExcelImportUtil.addFail(StringUtil.isEmpty(userName) || StringUtil.isEmpty(phone), ScheduleImportEnum.IMPORT_ERROR_UNIDENTIFIED_MEMBER, userName, failList)) { + return; + } + PartUserInfoVo userEntity = userMap.get(phone); + if (RuleExcelImportUtil.addFail(Objects.isNull(userEntity), ScheduleImportEnum.IMPORT_ERROR_MEMBER_NOT_IN_GROUP, userName, failList)) { + return; + } + if (RuleExcelImportUtil.addFail(!userEntity.getRealName().equals(userName), ScheduleImportEnum.IMPORT_ERROR_UNIDENTIFIED_MEMBER, userName, failList)) { + return; + } + if (RuleExcelImportUtil.addFail(cachedDataList.stream().anyMatch(vo -> vo.getUserId().equals(userEntity.getUserId())), ScheduleImportEnum.IMPORT_ERROR_DUPLICATE_MEMBERS, userEntity.getRealName(), failList)) { + return; + } + Integer selfGroup = selfGroupMap.get(userEntity.getUserId()); + if (RuleExcelImportUtil.addFail(Objects.isNull(selfGroup), ScheduleImportEnum.IMPORT_ERROR_MEMBER_NOT_IN_GROUP, userName, failList)) { + return; + } + DateTime today = DateUtil.beginOfDay(new Date()); + //班次信息校验 + List cachedDayDataList = CollUtil.newArrayList(); + data.forEach((index, periodSrt) -> { + if (index < 2 || heads.size() < index - 1) { + return; + } + String head = heads.get(index); + String dayIndex = head.split("\n")[1]; + DateTime day = DateUtil.offsetDay(month, Integer.parseInt(dayIndex) - 1); + if (day.isBefore(today)) { + return; + } + Date startTime = DateDetail.getDateByPoint(day, lineSchedulingConfig.getStartTime(), lineSchedulingConfig.getStartType()); + Date endTime = DateUtil.offsetHour(DateDetail.getDateByPoint(day, lineSchedulingConfig.getEndTime(), lineSchedulingConfig.getEndType()), 1); + if (StringUtil.isEmpty(periodSrt)) { + FtbAttendanceDailyRule ftbAttendanceDailyRule = AttendanceDailyRuleServiceImpl.buildLineSchedulesRule(groupId, selfGroup, day, day, day, userEntity.getUserId(), lineSchedulingConfig); + ftbAttendanceDailyRule.setAttendanceType(AttendanceTypeEnum.CLEAR.getCode()); + cachedDataList.add(ftbAttendanceDailyRule); + return; + } + cachedDayDataList.clear(); + String[] periodArr = periodSrt.split("\\|"); + for (String period : periodArr) { + if (StringUtil.isEmpty(period)) { + continue; + } + String[] dateArr = period.trim().split("-"); + try { + Date start = DateDetail.getDateByPoint(day, dateArr[0], 1); + Date end = DateDetail.getDateByPoint(day, dateArr[1], 1); + if (start.after(end)) { + end = DateUtil.offsetDay(end, 1); + } + Date ceilStart = new Date(start.getTime()); + DailyRuleUtil.ceilToHalfHour(start); + Date ceilEnd = new Date(end.getTime()); + DailyRuleUtil.ceilToHalfHour(end); + if (ceilStart.compareTo(start) != 0 || ceilEnd.compareTo(end) != 0) { + RuleExcelImportUtil.addFail(true, ScheduleImportEnum.IMPORT_ERROR_PERIOD_INVALID, userEntity.getRealName(), failList); + return; + } + if (start.before(startTime) || end.after(endTime)) { + RuleExcelImportUtil.addFail(true, ScheduleImportEnum.IMPORT_ERROR_OVERDUE_TIME_INVALID, userEntity.getRealName(), failList); + return; + } + cachedDayDataList.add(AttendanceDailyRuleServiceImpl.buildLineSchedulesRule(groupId, selfGroup, day, start, end, userEntity.getUserId(), lineSchedulingConfig)); + } catch (Exception e) { + RuleExcelImportUtil.addFail(true, ScheduleImportEnum.IMPORT_ERROR_PERIOD_INVALID, userEntity.getRealName(), failList); + return; + } + } + cachedDataList.addAll(DailyRuleUtil.mergeRules(cachedDayDataList)); + }); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 所有数据解析完成后的操作 + log.error("所有数据解析完成"); + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/SchedulesDataListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/SchedulesDataListener.java new file mode 100644 index 0000000..9ed5680 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/excel/listener/SchedulesDataListener.java @@ -0,0 +1,177 @@ +package jnpf.attendance.excel.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.ScheduleImportEnum; +import jnpf.model.attendance.model.ScheduleImportFailModel; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import jnpf.util.attendance.RuleExcelImportUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.compress.utils.Lists; + +import java.util.*; +import java.util.stream.Collectors; + +// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 +@Slf4j +public class SchedulesDataListener extends AnalysisEventListener> { + + /** + * 缓存的数据 + */ + private final List cachedDataList; + private final List failList; + + private final Date month; + + private final List heads; + + private final Map shiftMap; + private final Map userMap; + + private static final String format = "yyyy-MM-dd"; + + private int headRow = 0; + + public SchedulesDataListener(List cachedDataList, List failList, List heads, Date month, Map shiftMap, Map userMap) { + this.cachedDataList = cachedDataList; + this.failList = failList; + this.heads = heads; + this.month = month; + this.shiftMap = shiftMap; + this.userMap = userMap; + } + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + // 处理表头数据 + // headMap的key是列索引,value是表头名称 + headRow++; + if (context.readRowHolder().getRowIndex() < 2) { + return; + } + Assert.isFalse(heads.size() != headMap.size() || + !CollectionUtils.isEqualCollection(heads, headMap.values()), ScheduleImportEnum.IMPORT_ERROR_FORMAT_WRONG.getMessage()); + } + + @Override + public void invoke(Map data, AnalysisContext context) { + Assert.isFalse(headRow != 3, ScheduleImportEnum.IMPORT_ERROR_FORMAT_WRONG.getMessage()); + // 处理每一行数据 + // data的key是列索引,value是单元格值 + //用户人员信息校验 + String userName = data.get(0); + String phone = data.get(1); + if (RuleExcelImportUtil.addFail(StringUtil.isEmpty(userName) || StringUtil.isEmpty(phone), ScheduleImportEnum.IMPORT_ERROR_UNIDENTIFIED_MEMBER, userName, failList)) { + return; + } + PartUserInfoVo userEntity = userMap.get(phone); + if (RuleExcelImportUtil.addFail(Objects.isNull(userEntity), ScheduleImportEnum.IMPORT_ERROR_MEMBER_NOT_IN_GROUP, userName, failList)) { + return; + } + if (RuleExcelImportUtil.addFail(!userEntity.getRealName().equals(userName), ScheduleImportEnum.IMPORT_ERROR_UNIDENTIFIED_MEMBER, userName, failList)) { + return; + } + if (RuleExcelImportUtil.addFail(cachedDataList.stream().anyMatch(vo -> vo.getUserId().equals(userEntity.getUserId())), ScheduleImportEnum.IMPORT_ERROR_DUPLICATE_MEMBERS, userEntity.getRealName(), failList)) { + return; + } + LinkedHashMap schedulesDaySetDto = new LinkedHashMap<>(); + cachedDataList.add(SchedulesV2Vo.builder() + .userId(userEntity.getUserId()) + .mobilePhone(userEntity.getMobilePhone()) + .realName(userEntity.getRealName()) + .val(schedulesDaySetDto) + .build()); + //班次信息校验 + data.forEach((index, shiftNameSrt) -> { + if (index < 3 || heads.size() < index - 1) { + return; + } + String head = heads.get(index); + String dayIndex = head.split("\n")[1]; + DateTime day = DateUtil.offsetDay(month, Integer.parseInt(dayIndex) - 1); + String dayStr = DateUtil.format(day, format); + if (StringUtil.isEmpty(shiftNameSrt)) { + //清空 + schedulesDaySetDto.put(dayStr, SchedulesDayVo.builder().itemVos(List.of(SchedulesItemVo.builder().type(0).build())).build()); + return; + } + String[] shiftNameArr = shiftNameSrt.split("\\|"); + int length = shiftNameArr.length; + ArrayList list = Lists.newArrayList(); + schedulesDaySetDto.put(dayStr, SchedulesDayVo.builder().itemVos(list).build()); + for (int i = 0; i < shiftNameArr.length; i++) { + //上午半天是否为休 + boolean isAMOff = list.stream().anyMatch(vo -> Objects.equals(vo.getType(), 1) || Objects.equals(vo.getType(), 3) || Objects.equals(vo.getType(), 4)); + String shiftName = shiftNameArr[i]; + //判断班次类型 + ShiftNameVo shiftPeriodVo = shiftMap.get(shiftName); + if (Objects.nonNull(shiftPeriodVo)) { + //是否为全天班 + if (length > 1 && Arrays.stream(shiftNameArr).noneMatch(vo -> StringUtil.equals(vo, AttendanceTypeEnum.REST.getMsg()))) { + RuleExcelImportUtil.addFail(true, ScheduleImportEnum.IMPORT_ERROR_SHIFT_NAME_INVALID, userEntity.getRealName(), failList); + return; + } + //是否为全天班设置了两次 + List collect = list.stream().filter(vo -> Objects.equals(vo.getShiftId(), shiftPeriodVo.getId())).peek(vo -> vo.setSchedulesType(0)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)) { + continue; + } + list.add(SchedulesItemVo.builder() + .schedulesType(length < 2 ? 0 : i + 1) + .type(2) + .sort(length < 2 ? 0 : i + 1) + .shiftId(shiftPeriodVo.getId()) + .name(shiftPeriodVo.getName()) + .shortName(shiftPeriodVo.getShortName()) + .colour(shiftPeriodVo.getColour()) + .build()); + continue; + } + //秀 + if (!isAMOff && StringUtil.equals(shiftName, AttendanceTypeEnum.REST.getMsg())) { + list.add(SchedulesItemVo.builder() + .schedulesType(length < 2 ? 0 : i + 1) + .type(1) + .build()); + continue; + } + if (!isAMOff && StringUtil.equals(shiftName, AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getMsg())) { + list.add(SchedulesItemVo.builder() + .schedulesType(length < 2 ? 0 : i + 1) + .type(3) + .build()); + continue; + } + if (!isAMOff && StringUtil.equals(shiftName, AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getMsg())) { + list.add(SchedulesItemVo.builder() + .schedulesType(length < 2 ? 0 : i + 1) + .type(4) + .build()); + continue; + } + RuleExcelImportUtil.addFail(true, ScheduleImportEnum.IMPORT_ERROR_SHIFT_NAME_INVALID, userEntity.getRealName(), failList); + return; + + } + + }); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 所有数据解析完成后的操作 + log.error("所有数据解析完成"); + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/handler/JsonToListTypeHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/handler/JsonToListTypeHandler.java new file mode 100644 index 0000000..ccce68c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/handler/JsonToListTypeHandler.java @@ -0,0 +1,86 @@ +package jnpf.attendance.handler; + +import cn.hutool.core.collection.CollUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +public class JsonToListTypeHandler extends BaseTypeHandler> { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, toJson(parameter)); + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + List list = parseJson(rs.getString(columnName)); + if(CollUtil.isEmpty(list)){ + return null; + } + // 删除掉为空的数据 + list.removeIf(Objects::isNull); + return list; + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + List list = parseJson(rs.getString(columnIndex)); + if(CollUtil.isEmpty(list)){ + return null; + } + // 删除掉为空的数据 + list.removeIf(Objects::isNull); + return list; + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + List list = parseJson(cs.getString(columnIndex)); + if(CollUtil.isEmpty(list)){ + return null; + } + // 删除掉为空的数据 + list.removeIf(Objects::isNull); + return list; + } + + private String toJson(List list) { + try { + return OBJECT_MAPPER.writeValueAsString(list); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private List parseJson(String json) { + try { + if (json == null || json.isEmpty()) { + return null; + } + List result = OBJECT_MAPPER.readValue(json, + OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, Object.class) + ); + return result.stream() + .filter(Objects::nonNull) + .flatMap(item -> { + if (item instanceof List) { + List itemList = (List) item; + return itemList.stream().filter(Objects::nonNull); + } else { + return java.util.stream.Stream.of(item); + } + }).collect(java.util.stream.Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("JSON解析失败: " + json, e); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ApiCallLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ApiCallLogMapper.java new file mode 100644 index 0000000..45bce54 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ApiCallLogMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.ApiCallLog; + +/** + * 接口调用记录Mapper + * + * @author yanwenfu + * @create 2026-01-28 + */ +public interface ApiCallLogMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceAIMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceAIMapper.java new file mode 100644 index 0000000..ddebe09 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceAIMapper.java @@ -0,0 +1,23 @@ +package jnpf.attendance.mapper; + +import jnpf.model.attendance.vo.attendance.ClockRecordVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 考勤ai Mapper + * + * @author yanwenfu + * @create 2026-05-07 + */ +public interface AttendanceAIMapper { + + /** + * 查询打卡记录列表 + * @param userId 用户id + * @param queryDate 查询日期 + * @return java.util.List + */ + List getClockRecordList(@Param("userId") String userId, @Param("queryDate") String queryDate); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApprovalSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApprovalSettingMapper.java new file mode 100644 index 0000000..c676cdf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApprovalSettingMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.AttendanceApprovalSetting; + +/** + *

+ * 考勤审批设置表 Mapper 接口 + *

+ * + * @author Auto-generator + * @since 2023-11-21 + */ +public interface AttendanceApprovalSettingMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApproveMapper.java new file mode 100644 index 0000000..b8bb771 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceApproveMapper.java @@ -0,0 +1,32 @@ +package jnpf.attendance.mapper; + +import jnpf.model.attendance.vo.OvertimeVouchersVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * describe + * 考勤审批相关 + * @author HuangLinPan + * @date 2023/11/22 + */ +public interface AttendanceApproveMapper { + + + /** + * 查前天的加班出勤规则找这些出勤规则对应的加班结果表数据 + * @param date 日期 + * @return java.util.List + * @author hlp + */ + List getOvertimeVouchersList(@Param("date") Date date); + + /** + * 校验是否重复发放加班劵 + * @param ruleId 排班Id + * @param code 类型 : 1.节日 2.假日 3.加班(调休) + */ + Integer getOvertimeByRuleId(@Param("ruleId") String ruleId, @Param("code") Integer code); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceRecordMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceRecordMapper.java new file mode 100644 index 0000000..72eef1f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceRecordMapper.java @@ -0,0 +1,231 @@ +package jnpf.attendance.mapper; + + +import jnpf.entity.attendance.AttendanceBalanceRecordEntity; +import jnpf.entity.attendance.AttendanceStorageRest; +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.AttendanceBalanceRecordVo; +import jnpf.model.attendance.vo.UserBalanceVo; +import jnpf.model.attendance.vo.VacationVo; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +public interface AttendanceBalanceRecordMapper { + + /** + * 余额管理列表 + * + * @param balanceQueryDto 查询参数 + * @return java.util.List + * @author hlp + */ +// @Deprecated +// List getBalanceList(@Param("balanceQueryDto") BalanceQueryDto balanceQueryDto, @Param("balanceSort") String balanceSort, @Param("paidBalanceSort") String paidBalanceSort, @Param("unpaidBalanceSort") String unpaidBalanceSort, @Param("adventBalance") String adventBalance); + + + + /** + * 新增假期节假日劵 + * + * @param id 主键id + * @param userId 用户 + * @param holidayName 节假日名称 + * @param dayNum 总额 + * @param expiresTime 过期时间 + * @param unit 单位 : 1 :小时 2: 天 + * @param grantWay 发放方式 0.自动发放 1.手动发放 + * @param createUserId 创建人 + * @param paid 是否带薪(1带薪 2非带薪) + * @param type 类型 : 1.节日 2.假日 3.加班 + * @param objectId 关联id和Type联合使用 节日时是节日id 假日是是假日id 加班时是加班id + * @author hlp + */ +// @Deprecated +// void grantBalance(@Param("id") String id, @Param("userId") String userId, @Param("holidayName") String holidayName, @Param("dayNum") BigDecimal dayNum, @Param("expiresTime") Date expiresTime, @Param("unit") Integer unit, @Param("grantWay") Integer grantWay, @Param("createUserId") String createUserId, @Param("paid") Integer paid, @Param("type") Integer type, @Param("objectId") String objectId); + + /** + * 余额记录列表 + * + * @param balanceQueryDto 参数 + * @return java.util.List + * @author hlp + */ +// @Deprecated +// List getUserBalance(@Param("balanceQueryDto") BalanceQueryDto balanceQueryDto); + + /** + * 成员余额记录列表 + * + * @param balanceQueryDto 参数 + * @return java.util.List + * @author hlp + */ +// @Deprecated +// List getUserBalanceDetail(@Param("balanceQueryDto") BalanceQueryDto balanceQueryDto); + + /** + * 修改指定劵的状态 + * + * @param id 劵的id + * @param state 状态 0 正常 1 过期 2已停用 + * @author hlp + */ +// @Deprecated +// void updateBalanceState(@Param("id") String id, @Param("state") Integer state, @Param("userId") String userId); + + /** + * 获取用户余额 + * + * @param userId 用户id + * @return jnpf.model.attendance.vo.UserBalanceVo + * @author hlp + */ + @Deprecated + UserBalanceVo getBalanceByUserId(@Param("userId") String userId); + + /** + * 过期劵 + * + * @author hlp + */ + void invalidationCoupons(@Param("ids") List ids); + + /** + * 获取节日信息 + * + * @param balanceId 节日id + * @return jnpf.model.attendance.vo.VacationVo + * @author hlp + */ + VacationVo getFestivalSetting(@Param("balanceId") String balanceId); + + /** + * 获取假日配置信息 + * + * @param balanceId 假日id + * @return jnpf.model.attendance.vo.VacationVo + * @author hlp + */ + VacationVo getHolidaySetting(@Param("balanceId") String balanceId); + + /** + * 获取用户的劵过期的排前面 + * + * @param userId 用户id + * @param paid 是否带薪(1带薪 2非带薪) + * @return java.util.List + * @author hlp + */ + @Deprecated + List getBalanceRecordByUserId(@Param("userId") String userId, @Param("paid") List paid); + + /** + * 修改劵的使用情况 + * + * @param id 主键id + * @param balance 余额 + * @param over 是否使用完 0未使用完 1使用完 + * @author hlp + */ + void updateBalance(@Param("id") String id, @Param("balance") BigDecimal balance, @Param("over") Integer over); + + /** + * 获取用户指定类型假的余额总数 + * + * @param userId 用户id + * @param paid 是否带薪(1带薪 2非带薪) + * @return java.math.BigDecimal + * @author hlp + */ +// BigDecimal getBalanceRecordCountByUserId(@Param("userId") String userId, @Param("paid") Integer paid); + + + /** + * 通过劵的id获取劵的详情 + * + * @param balanceIds 劵的id + * @return java.util.List + * @author hlp + */ + List getBalanceDetailList(@Param("balanceIds") List balanceIds); + + /** + * 验证出勤规则id有无发加班劵 + * + * @param ruleId 出勤规则id + * @return java.lang.Integer + * @author hlp + */ + Integer getBalanceByRuleId(@Param("ruleId") String ruleId); + + /** + * 批量获取用户余额 + * + * @param userIds 用户ids + */ + List getBalanceByUserIds(@Param("userIds") List userIds); + + /** + * 根据申请id集合批量获取用户余额 + * + * @param applyIds 申请id集合 + */ + List getBalanceByApplyIds(@Param("applyIds") List applyIds); + + /** + * 获取用户余额 + * + * @return + */ + List getStraightBalanceList(); + + + /** + * 删除当月存休 + */ + void deleteByYearMonth(@Param("lastMonthDate") String lastMonthDate); + + /** + * 查询用户加班存休天数以及剩余天数 + * + * @param userIds 用户id集合 + */ + List getOvertimeBalanceInfo(@Param("startDate") Date startDate, @Param("endDate") Date endDate, + @Param("userIds") List userIds); + + /** + * 查询用户存休剩余天数 + * + * @param userIds 用户id集合 + */ + List getSurplusDaysInfo(@Param("userIds") List userIds); + + /** + * 查询用户节假日加班天数 + * + * @param userIds 用户id集合 + */ + List getOvertimeHolidaysInfo(@Param("startDate") Date startDate, @Param("endDate") Date endDate, + @Param("userIds") List userIds); + /** + * 查询查询用户各个请假类型余额天数 + * + * @param userIds 用户id集合 + */ + List getLeaveBalanceInfo(@Param("startDate") Date startDate, @Param("endDate") Date endDate, + @Param("userIds") List userIds); + + /** + * 查询查询用户各个请假审批数据 + * + * @param userIds 用户id集合 + */ + List getLeaveApprovalInfo(@Param("leaveIds") List leaveIds, + @Param("userIds") List userIds); + + List selectInvalidationCoupons(); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceUseRecordMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceUseRecordMapper.java new file mode 100644 index 0000000..fbf2205 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBalanceUseRecordMapper.java @@ -0,0 +1,58 @@ +package jnpf.attendance.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.attendance.AttendanceBalanceUseRecord; +import jnpf.model.doclibrary.vo.UseDetailVo; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +public interface AttendanceBalanceUseRecordMapper extends BaseMapper { + + /** + * 获取用户劵的使用劵详情 + * @param id 劵的Id + */ + List getUserBalanceDetail(String id); + + /** + * 新增劵使用记录 + * @param id 主键id + * @param balanceId 余额劵id + * @param quota 使用额度 + * @param unit 单位 : 1 :小时 2: 天 + * @param userType 使用方式 0: 审批 ,1:排班 + * @param objectId 关联id和useType联合使用 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param lock 锁定: 1锁定 2已消费 + * @author hlp + */ + void addOne(@Param("id") String id, @Param("balanceId") String balanceId, @Param("quota") BigDecimal quota, @Param("unit") Integer unit, @Param("userType") Integer userType, @Param("objectId") String objectId, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("lock") Integer lock); + + /** + * 查出本次排班相关的所有消费记录 + * @param id 关联id和useType联合使用 + * @param userType 使用方式 0: 审批 ,1:排班 + * @return java.util.List + * @author hlp + */ + List getUserBalanceListByObjectId(@Param("id") String id, @Param("userType") Integer userType); + + /** + * 根据劵的使用记录id删除记录 + * @param userBalanceList 劵的使用记录id + * @author hlp + */ + void deleteByIds(@Param("userBalanceList") List userBalanceList); + + /** + * 批量新增劵使用记录 + * @param useRecordList 劵使用记录 + * @author hlp + */ + void addBatch(@Param("useRecordList") List useRecordList); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBaseSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBaseSettingMapper.java new file mode 100644 index 0000000..2045bd7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBaseSettingMapper.java @@ -0,0 +1,22 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceBaseSetting; + +/** + *

+ * 考勤基础设置表 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceBaseSettingMapper extends SuperMapper { + + /** + * 获取出勤换算 + * @param userId + * @return + */ + Integer getAttendanceRatio(String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookConfigMapper.java new file mode 100644 index 0000000..deefa5f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookConfigMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceBookConfigEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 考勤本配置Mapper + * + * @author Generated + * @create 2026-04-15 + */ +@Mapper +public interface AttendanceBookConfigMapper extends SuperMapper{ +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookOperationLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookOperationLogMapper.java new file mode 100644 index 0000000..9121bd4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookOperationLogMapper.java @@ -0,0 +1,15 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.attendance.AttendanceBookOperationLogEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 考勤本操作日志Mapper + * + * @author Generated + * @create 2026-04-15 + */ +@Mapper +public interface AttendanceBookOperationLogMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookRecordMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookRecordMapper.java new file mode 100644 index 0000000..b89b49f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceBookRecordMapper.java @@ -0,0 +1,18 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.attendance.AttendanceBookRecordEntity; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +/** + * 考勤本记录表Mapper + * + * @author Generated + * @create 2026-04-15 + */ +@Mapper +@Component +public interface AttendanceBookRecordMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCardReplacementApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCardReplacementApproveMapper.java new file mode 100644 index 0000000..3fe6c05 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCardReplacementApproveMapper.java @@ -0,0 +1,6 @@ +package jnpf.attendance.mapper; + + +public interface AttendanceCardReplacementApproveMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInMapper.java new file mode 100644 index 0000000..018fd5e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInMapper.java @@ -0,0 +1,114 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbAttendanceClockIn; +import jnpf.model.attendance.model.ClockClassRecord; +import jnpf.model.attendance.vo.*; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 打卡mapper + * + * @author yanwenfu + * @create 2023-11-21 + */ +public interface AttendanceClockInMapper extends SuperMapper { + + /** + * 查询用户出勤结果 + * @param list 用户id + * @return java.util.List + */ + List getClockInResultByRule(@Param("list") List list); + + /** + * 查询用户出勤结果 + * @param ruleIdList 出勤规则ids + * @return java.util.List + */ + List getClockInResultByRuleList(@Param("list") List ruleIdList); + + /** + * 查询时间段内的补卡次数 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param userId 用户id + * @param groupId 考勤组id + * @return int + */ + int getRepairCount(@Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("userId") String userId, @Param("groupId") String groupId); + + /** + * 查询考勤组本月异常打卡记录 + * @param userId 用户id + * @param groupId 考勤组id + * @param monthBeginDate 月初 + * @param monthEndDate 月末 + * @param absenceType 能否补缺勤卡(1: 是, 0: 否) + * @param queryTypeList 补卡类型(1: 迟到, 2: 早退, 3: 缺卡) + * @return java.util.List + */ + List getAbnormalClockInList(@Param("userId") String userId, @Param("groupId") String groupId, + @Param("monthBeginDate") Date monthBeginDate, @Param("monthEndDate") Date monthEndDate, + @Param("absenceType") Integer absenceType, @Param("list") List queryTypeList); + + /** + * 更新为缺勤 + * @param ruleId 出勤规则id + * @param userId 用户id + * @param handleUser 上次操作人 + * @return int + */ + int updateToAbsence(@Param("ruleId") String ruleId, @Param("userId") String userId, @Param("handleUser") String handleUser); + + /** + * 更新为不缺勤 + * @param ruleId 出勤规则id + * @param userId 用户id + * @param handleUser 上次操作人 + * @return int + */ + int updateToUnAbsence(@Param("ruleId") String ruleId, @Param("userId") String userId, @Param("handleUser") String handleUser); + + /*** + * 查询当日所有打卡记录 + * @param userId 用户id + * @param queryDate 日期 + * @return java.util.List + */ + List getListByRuleAndDate(@Param("userId") String userId, @Param("queryDate") String queryDate); + + /*** + * 查询当日所有打卡记录 + * @param userId 用户id + * @param queryDate 日期 + * @return java.util.List + */ + List getClockInByStatistics(@Param("userId") String userId, @Param("queryDate") Date queryDate); + + /** + * 批量查询所有打卡记录 + * @param userIds 用户id集合 + * @param start 开始时间 + * @param end 结束时间 + */ + List getClockInList(@Param("userIds") List userIds, @Param("start") String start, @Param("end") String end); + + /** + * 查询打卡记录[批量] + * @param userDayList 用户日期列表 + * @param approvalStatus 审批状态 + * @return java.util.List + */ + List selectListBatch(@Param("list") List userDayList, @Param("approvalStatus") Integer approvalStatus); + + /** + * 删除审批中的打卡记录 + * @param userDayList 用户日期列表 + * @param passApproval 审批状态 + */ + void deleteBatchByUserDay(@Param("list") List userDayList, @Param("passApproval") Integer passApproval); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInPicMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInPicMapper.java new file mode 100644 index 0000000..3584c61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInPicMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceClockInPic; + +/** + * 外勤打卡图片mapper + * + * @author yanwenfu + * @create 2023-11-21 + */ +public interface AttendanceClockInPicMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInResultMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInResultMapper.java new file mode 100644 index 0000000..238687e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceClockInResultMapper.java @@ -0,0 +1,189 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.model.attendance.vo.ChangeInfoVo; +import jnpf.model.attendance.vo.ConditionVo; +import jnpf.model.attendance.vo.RuleDayVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 打卡结果mapper + * + * @author yanwenfu + * @create 2023-11-27 + */ +public interface AttendanceClockInResultMapper extends SuperMapper { + + /** + * 根据出勤规则删除用户的打卡结果 + * @param userId 用户id + * @param updateUserId 操作人 + * @return int + */ + int deleteRecordByRule(@Param("userId") String userId, @Param("updateUserId") String updateUserId); + + /** + * 根据出勤规则删除用户的打卡结果[批量] + * @param userIds 用户id列表 + * @param updateUserId 操作人 + * @return int + */ + int deleteRecordByRuleBatch(@Param("list") List userIds, @Param("updateUserId") String updateUserId); + + /** + * 根据条件查询用户打卡记录 + * @param dateStr 日期 + * @return java.util.List + */ + List selectUserClockInHistory(String dateStr); + + + /** + * 根据考勤组id,用户id及时间范围查询打卡结果记录 + */ + List selectUserClockInResult(@Param("groupId") String groupId, @Param("userId") String userId, @Param("start") Date start, @Param("end") Date end); + /** + * 批量更新记录为删除 + * @param delList 删除列表 + * @return int + */ + int updateBatchToDel(@Param("list") List delList); + + /** + * 更新申请信息为空 + * @param clockInId 打卡id + * @param applyId 审批id + * @return int + */ + int updateReplyToNull(@Param("clockInId") String clockInId, @Param("applyId") String applyId); + + /** + * 判断是否缺勤 + * @param ruleId 出勤规则 + * @param userId 用户id + * @return int + */ + int countAbsence(@Param("ruleId") String ruleId, @Param("userId") String userId); + + /** + * 更新审批为空 + * @param id 结果id + * @param dealUser 处理人 + * @return int + */ + int updateResultReplyToNull(@Param("id") String id, @Param("dealUser") String dealUser); + + /** + * 查询已存在的打卡结果 + * @param list 打卡结果列表 + * @param containsType 包含打卡类型(上/下班) 1: 是, 0: 否 + * @return java.util.List + */ + List selectExistRecord(@Param("list") List list, @Param("containsType") Integer containsType); + + /** + * 根据出勤规则删除打卡结果 + * @param list 出勤规则列表 + * @param updateUserId 更新用户id + * @return int + */ + int updateToDelByRule(@Param("list") List list, @Param("updateUserId") String updateUserId); + + /** + * 查询跨天的打卡id + * @param ruleIds 出勤规则ids + * @param day 日期 + * @return java.util.List + */ + List getCrossDayClockInIdList(@Param("list") List ruleIds, @Param("day") Date day); + /** + * 查询跨天的打卡id[批量] + * @param ruleDayList 规则日期列表 + * @return java.util.List + */ + List getCrossDayClockInIdListBatch(@Param("list") List ruleDayList); + + /** + * 查询打卡结果是否在审批中 + * @param clockInResultId 打卡结果id + * @return int + */ + int getReplyingCount(String clockInResultId); + + /** + * 更新缺勤为正常 + * @param id 结果id + * @return int + */ + int updateAbsenceToNormal(String id); + /** + * 更新为缺勤 + * @param id 结果id + * @return int + */ + int updateToAbsence(String id); + + /** + * 根据用户和日期查询打卡结果 + * @param userId 用户id + * @param date 日期 + * @return java.util.List + */ + List getByUserAndDay(@Param("userId") String userId, @Param("date") Date date); + + /** + * 根据出勤规则删除打卡结果 + * @param ruleIdList 出勤规则列表 + * @return int + */ + int removeClockInResultByRule(@Param("list") List ruleIdList); + + /** + * 获取用户打卡天数 + */ + Integer getClockInDayNum(String userId); + + /** + * 获取用户迟到次数 + */ + Integer getLateClockInNum(@Param("dayRuleIds") List dayRuleIds); + + /** + * 查询补卡审批中次数 + * @param userId 用户id + * @param groupId 考勤组id + * @return int + */ + int selectRepairApplyCount(@Param("userId") String userId, @Param("groupId") String groupId, @Param("beginDate") String beginDate, @Param("endDate") String endDate); + + /** + * 根据打卡ids查询打卡结果 + * @param clockInIds 打卡ids + * @return java.util.List + */ + List selectBatchByClockInIds(@Param("list") List clockInIds); + + /** + * 变更打卡结果绑定的rule + * @param resultId 打卡结果id + * @param ruleId 出勤规则id + */ + void updateResultRelation(@Param("resultId") String resultId, @Param("ruleId") String ruleId); + + /** + * 查询需要更新为缺勤的打卡结果 + * @param conditionList 缺勤条件 + * @return java.util.List + */ + List selectRuleIdsForAbsence(@Param("list") List conditionList); + + /** + * 更新为缺勤[批量] + * @param updateResultIds 打卡结果ids + */ + void updateToAbsenceBatch(@Param("list") List updateResultIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCloudAlbumMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCloudAlbumMapper.java new file mode 100644 index 0000000..221135d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCloudAlbumMapper.java @@ -0,0 +1,12 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceCloudAlbum; + +/** + * @Description: 考勤云相册表 + * @Author: shiTou(他是小石头) + * @Date: 2024-10-30 10:26 + */ +public interface AttendanceCloudAlbumMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmDetailsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmDetailsMapper.java new file mode 100644 index 0000000..1e18d00 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmDetailsMapper.java @@ -0,0 +1,14 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceConfirmDetails; + +/** + * 考勤确认详情 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +public interface AttendanceConfirmDetailsMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmMapper.java new file mode 100644 index 0000000..4283d84 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmMapper.java @@ -0,0 +1,39 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceConfirm; +import jnpf.model.attendance.vo.attendance.ConfirmListQueryVo; +import jnpf.model.attendance.vo.attendance.ConfirmStatisticsVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 考勤确认 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +public interface AttendanceConfirmMapper extends SuperMapper { + /** + * 考勤确认聚合统计 + * + * @param userIds 用户ID集合 + * @param year 考勤年份 + * @param month 月份 + * @return 考勤确认聚合统计 + */ + ConfirmStatisticsVo getStatistics(@Param("userIds") List userIds, @Param("year") int year, @Param("month") int month,@Param("type") Integer type); + + /** + * 考勤确认列表查询 + * + * @param userIds 用户ID集合 + * @param year 考勤年份 + * @param month 月份 + * @param type 数据类型 + * @return 考勤确认列表 + */ + List getPageList(@Param("userIds") List userIds, @Param("year") int year, @Param("month") int month, @Param("type") Integer type); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmSettingMapper.java new file mode 100644 index 0000000..4458580 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceConfirmSettingMapper.java @@ -0,0 +1,14 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceConfirmSetting; + +/** + * 考勤确认设置 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +public interface AttendanceConfirmSettingMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCustomizeTableMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCustomizeTableMapper.java new file mode 100644 index 0000000..f4e7b77 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceCustomizeTableMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.AttendanceCustomizeTable; +import jnpf.base.mapper.SuperMapper; + +/** + *

+ * 考勤-自定义报表设置 Mapper 接口 + *

+ * + * @author ahua + * @since 2024-09-03 + */ +public interface AttendanceCustomizeTableMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDailyRuleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDailyRuleMapper.java new file mode 100644 index 0000000..58e1d39 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDailyRuleMapper.java @@ -0,0 +1,166 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.YesterdayRuleVo; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.Date; +import java.util.List; + +/** + *

+ * 考勤组-每日出勤规则 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +public interface AttendanceDailyRuleMapper extends SuperMapper { + + /** + * 查询昨天和今天的出勤规则 + * @param currentDate 当前日期 + * @param userId 用户id + * @return java.util.List + */ + List getRuleClockTimeContainsToday(@Param("currentDate") Date currentDate, @Param("userId") String userId); + + /** + * 查询指定日期的最后一个出勤规则 + * @param currentDate 当前日期 + * @param userId 用户id + * @return jnpf.entity.attendance.FtbAttendanceDailyRule + */ + FtbAttendanceDailyRule getLastDailyRule(@Param("currentDate") Date currentDate, @Param("userId") String userId); + + /** + * 查询ruleId的上一个或下一个(班/加班)出勤 + * @param ruleId 出勤id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param userId 用户id + * @param queryType 查询类型(previous/next) + * @return jnpf.entity.attendance.FtbAttendanceDailyRule + */ + FtbAttendanceDailyRule getPreviousOrNextRule(@Param("ruleId") String ruleId, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("userId") String userId, @Param("queryType") String queryType); + + /** + * 获取指定日期的排班 + * @param startDay 指定日期 2023-11-11 + * @return java.util.List + * @author hlp + */ + List getShiftByDay(@Param("startDay") String startDay, @Param("userId") String userId); + + /** + * 查询所有下班缺卡时间是今天的出勤规则(普班) + * @param today 今天 + * @param yesterday 昨天 + * @param userId 用户id + * @return java.util.List + */ + List getRuleListByDate(@Param("today") Date today, @Param("yesterday") Date yesterday, @Param("userId") String userId); + + /** + * 查询指定日期最后一个班次是加班的出勤记录 + * @param date 日期 + * @param userId 用户id + * @return java.util.List + */ + List getOverTimeRuleListByDate(@Param("date") Date date, @Param("userId") String userId); + + /** + * 获取用户时间段内排班信息 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.lang.Integer + * @author hlp + */ + Integer getUserShift(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 查询所有工作时间是queryDate的出勤规则 + * @param queryDate 日期 + * @return java.util.List + */ + List getListWorkTimeInToday(@Param("queryDate") Date queryDate); + + /** + * 查询上班/下班时间在时间范围内的出勤规则 + * @param beginDate 开始时间 + * @param endDate 结束时间 + * @return java.util.List + */ + List getListByWorkTime(@Param("beginDate") Date beginDate, @Param("endDate") Date endDate); + + /** + * 查询一个月的打卡记录(缺勤次数统计) + * @return java.util.List + */ + List selectMonthRecord(); + + /** + * 查询存在的出勤规则 + * @param ruleIdList 出勤规则id列表 + * @return java.util.List + */ + List getExistRuleId(@Param("list") List ruleIdList); + + /** + * 获取排班用户数 + * @param userIds 用户Ids + * @param groupIds 考勤组Ids + * @return int + */ + Integer getGroupUserNum(@Param("userIds") List userIds, @Param("groupIds") List groupIds); + /** + * 查询今日班次 + * @param userId 用户Id + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return java.util.List + */ + List getYesterdayRule(@Param("userId") String userId,@Param("startDate") Date startDate,@Param("endDate") Date endDate); + /** + * 获取排班用户数 + * @param groupUserIds 用户Ids + * @param groupIds 考勤组Ids + * @return java.util.List + */ + List getDayRule(@Param("groupUserIds") List groupUserIds, @Param("groupIds") List groupIds); + + /** + * 批量查询[当日+前一日]出勤规则 + * @param userDayList 用户日期列表 + * @return java.util.List + */ + List getDailyRuleListBatch(@Param("list") List userDayList); + + /** + * 获取用户已使用的公休数 + * @param yearMonth 年月 格式yyyy-MM + * @param userIds 用户Ids + * @return java.util.Map + */ + List getUserPublicHoliday(@Param("yearMonth") String yearMonth, @Param("userIds") List userIds); + + /** + * 查询每个出勤规则的下一个出勤规则 + * @param dayRuleIds 出勤规则ids + * @param dayStr 日期 + * @return java.util.List + */ + List getNextRulePartInfo(@Param("list") List dayRuleIds, @Param("dayStr") String dayStr); + + /** + * 获取指定日期的出勤规则 + * @param finalStartDate 开始日期 + * @param finalEndDate 结束日期 + * @return 排班列表 + */ + List getDayRuleByMonth(@Param("startDate") LocalDate finalStartDate, @Param("endDate") LocalDate finalEndDate); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDayStatisticsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDayStatisticsMapper.java new file mode 100644 index 0000000..be96d9c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceDayStatisticsMapper.java @@ -0,0 +1,381 @@ +package jnpf.attendance.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.attendance.dto.AttendanceCountAvgHoursVo; +import jnpf.attendance.dto.DateDimensionsRangeDto; +import jnpf.entity.attendance.AttendanceDayStatistics; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.common.DateRangeDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import javax.validation.constraints.NotEmpty; +import java.time.LocalDate; +import java.util.Date; +import java.util.List; + +/** + * 考勤日度统计表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-06-24 09:33:43 + */ +@Mapper +@Component +public interface AttendanceDayStatisticsMapper extends BaseMapper { + /** + * 查询日统计数据 + * + * @param queryDto 筛选条件 + * @return 日统计数据 + */ + StatisticsDataQueryVo getDayStatisticsDataQuery(@Param("req") StatisticsDataQueryDto queryDto); + + /** + * 查询日统计分页数据 + * + * @param req 筛选条件 + * @return 分页数据 + */ + List getDayPageList(@Param("req") DayStatisticsDataPageListQueryDto req); + + /** + * 查询计薪日统计分页数据 + * + * @param req 筛选条件 + * @return 分页数据 + */ + List getDayPayrollPageList(@Param("req") DayStatisticsDataPageListQueryDto req); + + /** + * 查询月统计数据 + * + * @param queryDto 日欺范围 + * @param isGroupBy 是否按照考勤组分组 + * @return 月统计数据 + */ + StatisticsDataQueryVo getMonthStatisticsDataQuery(@Param("req") StatisticsDataQueryDto queryDto, @Param("isGroupBy") Boolean isGroupBy); + + /** + * 查询月统计分页数据 + * + * @param req 筛选条件 + * @return 分页数据 + */ + List getMonthPageList(@Param("req") MonthStatisticsDataQueryDto req); + + /** + * 查询计薪月统计分页数据 + * + * @param req 筛选条件 + * @return 分页数据 + */ + List getMonthPayrollPageList(@Param("req") MonthStatisticsDataQueryDto req); + + /** + * 查询需要提醒的日统计数据 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 日统计数据 + */ + List getDayStatisticsNotice(@Param("startDate") String startDate, @Param("endDate") String endDate); + + /** + * 查询需要提醒的月统计数据 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 月统计数据 + */ + List getMonthStatisticsNotice(@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 查询需要提醒的团队统计数据 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 团队统计数据 + */ + List getStemMonthStatisticsNotice(@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取考勤组每月的平均工时 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 每月的平均工时 + */ + List getAvgHours(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度统计数据 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 月度统计数据 + */ + MonthStatsDetailsQueryVo getAttendanceAvgHoursDetails(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度人均工时折线图 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 人均工时折线图 + */ + List getAttendanceMonthPerCapita(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度日常情况 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 月度日常情况 + */ + MonthStatsDailySituationQueryVo getAttendanceDailySituation(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度考勤工时排行 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 考勤工时排行 + */ + List getAttendanceHoursRanking(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度全勤情况 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 全勤情况 + */ + List getAttendanceFullSituation(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度异常情况 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 异常情况 + */ + MonthStatsAbnormalConditionQueryVo getAttendanceAbnormalCondition(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取多考勤组月度加班情况 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 加班情况 + */ + List getAttendanceOvertimeSituation(@Param("groupIds") List groupIds, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 获取当月考勤确认数据 + * + * @param userIds 用户ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 考勤组月度加班情况 + */ + List getConfirmDetailsInfoByMonth(@Param("userIds") List userIds, @Param("startDate") Date startDate, @Param("endDate") Date endDate); + + /** + * 批量查询用户在指定月份是否封账 + * + * @param userIdList 用户ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 是否封账 + */ + List selectUserIsSeal(@Param("userIdList") List userIdList, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** + * 按天查询用户在日期范围内的封存日期列表(仅返回已封存的日期) + * 用于跨月查询场景下按天判断每个用户每一天是否被封存 + * + * @param userIdList 用户ID集合 + * @param startDate 开始时间(含) + * @param endDate 结束时间(含) + * @return 用户按天封存记录 + */ + List selectUserDailySeal(@Param("userIdList") List userIdList, + @Param("startDate") String startDate, + @Param("endDate") String endDate); + + /** + * 考勤封账-分页列表 + * + * @param queryDto 考勤封账分页列表参数 + * @param seal 封账状态 + * @return 考勤封账分页列表结果 + */ + List sealPageList(@Param("req") StatisticsDataQueryDto queryDto, @Param("seal") Integer seal); + + /** + * 薪酬考勤数据支持 + * + * @param dto 薪酬考勤数据支持参数 + * @return 薪酬考勤数据支持结果 + */ + List salaryAttendanceSupport(@Param("req") SalaryAttendanceSupportDto dto); + + /** + * 获取考勤组维度考勤数据 + * + * @param groupIds 考勤组ID集合 + * @param dimensions 维度范围 + * @return 考勤组维度考勤数据 + */ + List getDimensionsAttendanceCountList(@Param("groupIds") List groupIds, + @Param("dimensions") List dimensions); + + /** + * 获取考勤组维度考勤数据-日维度 + * + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 考勤组维度考勤数据 + */ + List getDimensionsAttendanceDayCountList(@Param("groupIds") List groupIds, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); + + /** + * 考勤平均工时趋势-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 考勤平均工时趋势-分页列表结果 + */ + Page averageWorkHoursTrendPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 考勤人均工时趋势-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 考勤人均工时趋势-分页列表结果 + */ + Page personWorkHoursTrendPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 考勤日常情况-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 日常情况-分页列表结果 + */ + Page dailySituationPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 考勤工时排名-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 考勤组工时排名-分页列表结果 + */ + Page workHoursRankingPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 考勤全勤情况-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 全勤情况-分页列表结果 + */ + Page fullAttendanceStatusPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 考勤异常情况-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 异常情况-分页列表结果 + */ + Page exceptionSituationPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 考勤加班情况-分页列表 + * @param page 分页参数 + * @param groupIds 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 加班情况-分页列表结果 + */ + Page overtimeSituationPageList(@Param("page") Page page, + @Param("groupIds") List groupIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + /** + * 查询用户考勤统计信息 + * + * @param userId 用户ID + * @param dateRangeDto 时间范围 + * @return 用户考勤统计信息 + */ + DayStatisticsQueryDbVo queryUserStatisticsInfo(@Param("userId") String userId, @Param("dateRangeDto") DateRangeDto dateRangeDto); + + /** + * 查询用户加班详情 + * + * @param userId 用户ID + * @param dateRangeDto 时间范围 + * @return 用户加班详情 + */ + List queryUserOvertime(@Param("userId") String userId, @Param("dateRangeDto") DateRangeDto dateRangeDto); + + /** + * 查询用户工作状况统计信息 + * + * @param userIds 用户ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 用户工作状况统计信息 + */ + List queryUserWorkSituation(@Param("userIds") List userIds, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalRulesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalRulesMapper.java new file mode 100644 index 0000000..4c42781 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalRulesMapper.java @@ -0,0 +1,30 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceFestivalRules; +import jnpf.model.attendance.dto.FestivalRulesQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceFestivalRulesVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Mapper +@Component +public interface AttendanceFestivalRulesMapper extends SuperMapper { + /** + * 列表 + * @param queryDto 查询参数 + * @return 列表 + */ + List getPageList(@Param("queryDto") FestivalRulesQueryDto queryDto); + + /** + * 添加 + * @param id 主键 + * @param state 状态 1是 0否 + * @param userId 用户ID + */ + void updateState(@Param("id") String id, @Param("state") Integer state, @Param("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalSettingMapper.java new file mode 100644 index 0000000..9291f8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFestivalSettingMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceFestivalSettingEntity; +import jnpf.base.mapper.SuperMapper; + +/** + *

+ * 考勤配置-节日配置 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceFestivalSettingMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldPersonnelMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldPersonnelMapper.java new file mode 100644 index 0000000..6803fbb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldPersonnelMapper.java @@ -0,0 +1,26 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceFieldPersonnel; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2025/2/17 16:03 + * @Version 1.0 (版本号) + */ +public interface AttendanceFieldPersonnelMapper { + + /** 根据考勤组Id删除绑定的外勤人员 */ + void deleteByGroupId(@Param("groupId") String groupId); + + /** 批量新增考勤组可外勤人员 */ + void insertList(@Param("fieldPersonnelList") List fieldPersonnelList); + + /** + * 根据考勤组Id获取外勤人员Id列表 + * @param groupId 考勤组Id + */ + List getUserIdsByGroupId(@Param("groupId") String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldpersonnelApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldpersonnelApproveMapper.java new file mode 100644 index 0000000..f750618 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFieldpersonnelApproveMapper.java @@ -0,0 +1,6 @@ +package jnpf.attendance.mapper; + + +public interface AttendanceFieldpersonnelApproveMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFixedClassMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFixedClassMapper.java new file mode 100644 index 0000000..2199f52 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceFixedClassMapper.java @@ -0,0 +1,30 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceFixedClassEntity; +import org.apache.ibatis.annotations.Param; + +/** + * @Author huanglinpan + * @Date 2024/5/9 15:14 + * @Version 1.0 (版本号) + */ +public interface AttendanceFixedClassMapper extends SuperMapper{ + + /** + * 校验该班次有无在固定排班使用 + * @param shiftNameId 班次Id + */ + Integer getUseBYShiftNameId(@Param("shiftNameId") String shiftNameId); + + /** + * 校验该班次有无在排班使用 + * @param shiftNameId 班次Id + */ + Integer getRuleUseBYShiftNameId(@Param("shiftNameId") String shiftNameId); + /** + * 校验该班次有无在快速排班使用 + * @param shiftNameId 班次Id + */ + Integer getQuickUseBYShiftNameId(@Param("shiftNameId") String shiftNameId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupMapper.java new file mode 100644 index 0000000..42f8acb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupMapper.java @@ -0,0 +1,166 @@ +package jnpf.attendance.mapper; + +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.AttendanceUserListGroupVO; +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.AttendanceGroup; +import jnpf.model.attendance.dto.AttendanceGroupOrgDto; +import jnpf.model.attendance.dto.GroupQueryDto; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.GroupNodeVo; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 考勤组mapper + * + * @author yanwenfu + * @create 2023-11-21 + */ +public interface AttendanceGroupMapper extends SuperMapper { + /** + * 查询用户管理的考勤组列表(考勤组名称模糊搜索) + */ + List queryManagerGroupList(@Param("userId") String userId, @Param("keyword") String keyword); + /** + * 查询用户管理的考勤组列表 + */ + List queryGroupList(@Param("userId") String userId); + + /** + * 查询考勤组名称 + * + * @param groupId 考勤组id + * @return java.lang.String + */ + String getGroupName(String groupId); + + /** + * 获取指定考勤组信息 + * + * @param groupId 考勤组id + * @return jnpf.entity.AttendanceGroup + * @author hlp + */ + AttendanceGroup getGroupDetail(String groupId); + + /** + * 批量获取考勤组信息 + * + * @param list 考勤组ids + * @return java.util.List + * @author hlp + */ + List getGroupDetailList(@Param("list") List list); + + /** + * 查询被删除的考勤组 + * + * @param groupIdList 考勤组列表 + * @return java.util.List + */ + List selectDelIds(@Param("list") List groupIdList); + + /** + * 获取所有考勤组 + * + * @param groupQueryDto 考勤组查询条件 + * @return java.util.List + */ + List getAllGroupList(@Param("groupQueryDto") GroupQueryDto groupQueryDto, @Param("userId") String userId, @Param("permissionType") Integer permissionType); + + /** + * 通过组织Id找到组织下的考勤组 + * + * @param orgId 组织Id + */ + List groupListByOrgId(@Param("orgId") String orgId); + + /** + * 通过考勤组Id获取绑定的组织 + * + * @param groupId 组织Id + */ + AttendanceGroupVo getGroupBindingOrg(@Param("groupId") String groupId); + + /** + * 绑定考勤组的组织 + * + * @param attendanceGroupOrgDto 考勤组和组织信息 + */ + void updateGroupOrg(@Param("attendanceGroupOrgDto") AttendanceGroupOrgDto attendanceGroupOrgDto); + + /** + * 获取用户考勤组 + * + * @param userIds 用户ids + */ + List getAttendanceUserGroup(@Param("userIds") List userIds); + + /** + * 查询考勤组锁定日期 + * @param groupId 考勤组id + * @return java.util.Date + */ + Date getGroupLockDate(String groupId); + + /** + * 查询父级考勤组id + * @param groupId 考勤组id + * @return java.lang.String + */ + String getParentGroupId(String groupId); + + /** + * 查询本级及子级考勤组 + * @param groupId 考勤组id + * @return java.util.List + */ + List getSelfAndChildrenGroup(String groupId); + + /** + * 获取多用户的考勤组信息,包含多级考勤组名称 + * @param userIds 用户ids + * @return 包含多级考勤组名称的考勤信息 + */ + List getAttendanceUserListGroupVO(@Param("userIds") List userIds); + + /** + * 查询所有考勤组(层级排序) + * @return java.util.List + */ + List getAllGroupByLevel(@Param("list") List groupIds); + + /** + * 查询用户是否拥有指定考勤组的指定权限 + * @param userId 用户 + * @param groupId 考勤组 + * @param permissionCode 指定权限 + */ + Integer appointPermissionByGroupId(@Param("userId") String userId, @Param("groupId") String groupId, @Param("permissionCode") String permissionCode, @Param("type") Integer type); + + /** + * 获取组织下的考勤组 + * @param groupQueryDto 考勤组查询条件 + * @return List + */ + List getGroupListByOrgId(@Param("groupQueryDto") GroupQueryDto groupQueryDto, @Param("orgIds") List orgIds); + + /** + * 根据考勤组ID列表查询考勤组信息 + * + * @param groupIds 考勤组ID列表 + * @return 考勤组信息列表 + */ + List getGroupListByIds(@Param("groupIds") List groupIds); + + /** + * 查询组织id + * @param groupId 考勤组id + * @return java.lang.String + */ + String getOrgId(String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupUserMapper.java new file mode 100644 index 0000000..5ad6ef8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceGroupUserMapper.java @@ -0,0 +1,97 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.AttendanceGroupUser; +import jnpf.model.attendance.dto.GroupUserQueryDto; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.attendance.vo.MiniGroupVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Set; + +/** + *

+ * 考勤组成员管理表 Mapper 接口 + *

+ * + * @author Auto-generator + * @since 2023-11-21 + */ +public interface AttendanceGroupUserMapper extends SuperMapper { + /** + * 获取考勤组用户生存周期记录 + * @param groupId 考勤组名称 + */ + List viewGroupUserList(@Param("groupId") String groupId, @Param("name") String name); + + /** + * 查询系统用户列表 + * @param orgId + * @param name + * @return List + */ + List querySysUsers(@Param("orgId") String orgId, @Param("name") String name); + + /** + * 获取用户所有产生的考勤组集合 + * @param userId 用户id + * @return java.util.List + * @author hlp + */ + List getUserGroupList(@Param("userId") String userId); + + /** + * 查询考勤组用户信息列表 + * @param groupIds 考勤组ids + * @param userId 用户id + * @return java.util.List + */ + List getGroupUserList(@Param("list") Set groupIds, @Param("userId") String userId, @Param("delStatus") Integer delStatus); + + /** + * 查询自己的考勤组id + * @param userId 用户id + * @return java.lang.String + */ + String getSelfGroup(String userId); + + /** + * 批量修改考勤组用户 + * @param groupUserIds 考勤用户关系主键id + * @param type 1.本组 2.借调 3.移除 + */ + void batchUpdateGroupUser(@Param("groupUserIds") List groupUserIds, @Param("type") Integer type); + +// List viewGroupUserListInGroupIds(@Param("groupId") List groupIds); + + /** + * 根据用户ids查询考勤组用户信息 + * @param userIds 用户ids + * @param type 1.本组 2.借调 3.离组 + * @return List + */ + List getGroupInUserIds(@Param("userIds") List userIds, @Param("type") Integer type); + + /** + * 查询考勤组成员(不含借调) + * @param groupIdList 考勤组ids + * @return java.util.List + */ + List getGroupUserListByIds(@Param("list") List groupIdList); + + /** + * 查询考勤组成员 + * @param groupIds 查询条件考勤组集合 + * @return + */ + List getUsersByGroupIds(@Param("groupIds") List groupIds); + + /** + * 获取考勤组成员id + * @param groupIds 查询条件考勤组集合 + * @return java.util.List + */ + List getGroupUserIds(@Param("groupIds") List groupIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceHolidaySettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceHolidaySettingMapper.java new file mode 100644 index 0000000..53559a5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceHolidaySettingMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceHolidaySettingEntity; +import jnpf.base.mapper.SuperMapper; + +/** + *

+ * 考勤配置-假日设置 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceHolidaySettingMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveApproveMapper.java new file mode 100644 index 0000000..577d98f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveApproveMapper.java @@ -0,0 +1,319 @@ +package jnpf.attendance.mapper; + + +import jnpf.model.attendance.model.LeaveSituationData; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.BusinessTripVo; +import jnpf.model.attendance.vo.attendance.GoOutVo; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +public interface AttendanceLeaveApproveMapper { + + /** + * 获取请假申请详情 + * @param id 审批的唯一id + * @return jnpf.model.attendance.vo.AttendanceLeaveApproveVo + * @author hlp + */ + AttendanceLeaveApproveVo getLeaveDetailById(@Param("id") String id); + + /** + * 获取请假申请详情 + * @param groupId 考勤组id + * @param userId 用户id + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return java.util.List + */ + List getUserLeaveList(@Param("groupId") String groupId, @Param("userId") String userId, @Param("startDate") Date startDate, @Param("endDate") Date endDate); + + /** + * 获取请假申请详情 + * @param applyIds 审批id集合 + * @return java.util.List + */ + List getUserLeaveListByApplyIds(@Param("applyIds") List applyIds); + + /** + * 修改请假审批信息 + * @param id 审批的唯一id + * @param dayNum 未抵扣时间(单位 : 天) + * @param applicationDurationSecond 请假时长秒 + * @param undeductedTimeSecond 未抵扣时长秒 + * @param leaveName 请假名称 + * @param applicationDuration 请假时长 + * @param applicationDurationDay 半天、天时的请假时长 + * @param notDeductedDay 半天、天时的未抵扣时长 + * @param start 命中的开始时间 + * @param end 命中的结束时间 + * @param leaveDurationJson 请假时长Json + * @param leaveConsumptionDetailJson 请假消耗详情Json 2.0 新增 + * @author hlp + */ + void updateLeaveApprove(@Param("id") String id, @Param("dayNum") BigDecimal dayNum, @Param("applicationDuration") BigDecimal applicationDuration, @Param("leaveName") String leaveName, @Param("applicationDurationSecond") Integer applicationDurationSecond, @Param("undeductedTimeSecond") Integer undeductedTimeSecond, @Param("userId") String userId, @Param("userName") String userName, @Param("applicationDurationDay") BigDecimal applicationDurationDay, @Param("notDeductedDay") BigDecimal notDeductedDay, @Param("start") Date start, @Param("end") Date end, @Param("leaveDurationJson") String leaveDurationJson, @Param("leaveConsumptionDetailJson") String leaveConsumptionDetailJson); + + /** + * 获取用户该类型的请假有多少未抵扣余额 + * @param userId 用户id + * @param leaveTypeId 请假类型id + * @return java.math.BigDecimal + * @author hlp + */ + BigDecimal getUserUnDeductedTime(@Param("userId") String userId, @Param("leaveTypeId") String leaveTypeId); + + /** + * 获取用户时间段内审核中和审核通过的请假审批列表 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.util.List + * @author hlp + */ + List getUserLeaveByTimeSlot(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 获取用户时间段内审核通过的请假审批列表 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.util.List + * @author hlp + */ + List getUserPassLeaveByTime(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + *批量获取用户时间段内审核通过的请假审批列表 + * @param userIds 用户id集合 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.util.List + * @author hlp + */ + List getBatchLeaveByUserIds(@Param("userIds") List userIds, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 通过id获取加班详情 + * @param id 审批的唯一id + * @author hlp + */ + AttendanceWorkOverTimeVo getWorkOverTime(@Param("id") String id); + + /** + * 修改加班审批的审批状态 + * @param id 加班主键id + * @author hlp + */ + void updateWorkOverTimeApprove(@Param("id") String id, @Param("status") Integer status, @Param("userId") String userId, @Param("userName") String userName, @Param("json") String json); + + /** + * 获取待审批的借调审批详情 + * @param id 审批的唯一id + * @author hlp + */ + AttendanceSelfApproveVo getSelfApprove(@Param("id") String id); + + /** + * 获取借调用户集合 + * @param id 借调审批主键id + * @return java.util.List + * @author hlp + */ + List getUserList(@Param("id") String id); + + /** + * 修改借调审批为通过 + * @param id 主键id + * @author hlp + */ + void updateSelfApprove(@Param("id") String id, @Param("departureTime") Date departureTime, @Param("backTime") Date backTime, @Param("status") Integer status, @Param("userId") String userId, @Param("userName") String userName); + + /** + * 将对应审批变为未通过 + * @param id F_UniqueId 审批的唯一id + * @author hlp + */ + void updateLeaveApproveStatus(@Param("id") String id, @Param("status") Integer status, @Param("userId") String userId, @Param("userName") String userName); + + /** + * 获取用户时间段内加班申请数量 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.lang.Integer + * @author hlp + */ + Integer getUserWorkByTimeSlot(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 获取用户时间段内加班申请数量 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.lang.Integer + * @author hlp + */ + Integer getUserPassWorkByTime(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 修改外出审批状态 + * @param applyId 主键Id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + */ + void updateGoOutApprove(@Param("applyId") String applyId, @Param("status") Integer status, @Param("userId") String userId, @Param("userName") String userName); + + /** + * 获取外出审批详情 + * @param applyId 主键Id + */ + GoOutVo getGoOut(@Param("applyId") String applyId); + + /** + * 获取出差审批详情 + * @param applyId 主键Id + */ + BusinessTripVo getBusinessTrip(@Param("applyId") String applyId); + + /** + * 修改出差审批状态 + * @param applyId 主键Id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + */ + void updateBusinessTripApprove(@Param("applyId") String applyId, @Param("status") Integer status, @Param("userId") String userId, @Param("userName") String userName); + + /** + * 获取指定用户指定日期的加班申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyWorkOverTimeList(@Param("userId") String userId, @Param("queryDate") String queryDate, @Param("currentGroupId") String currentGroupId, @Param("code") Integer code); + + /** + * 获取指定用户指定日期的申请中的加班申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyingWorkOverTimeList(@Param("userId") String userId, @Param("queryDate") String queryDate); + + + /** + * 获取指定用户指定日期的外出申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyGoOutList(@Param("userId") String userId, @Param("queryDate") String queryDate, @Param("currentGroupId") String currentGroupId, @Param("code") Integer code); + + /** + * 获取指定用户指定日期的申请中的外出申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyingGoOutList(@Param("userId") String userId, @Param("queryDate") String queryDate); + + /** + * 获取指定用户指定日期的请假申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyLeaveList(@Param("userId") String userId, @Param("queryDate") String queryDate, @Param("currentGroupId") String currentGroupId, @Param("code") Integer code); + + /** + * 获取指定用户指定日期的申请中的请假申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyingLeaveList(@Param("userId") String userId, @Param("queryDate") String queryDate); + + + /** + * 获取指定用户指定日期的借调申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplySelfList(@Param("userId") String userId, @Param("queryDate") String queryDate, @Param("currentGroupId") String currentGroupId, @Param("code") Integer code); + + /** + * 获取指定用户指定日期的出差申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyBusinessTripList(@Param("userId") String userId, @Param("queryDate") String queryDate, @Param("currentGroupId") String currentGroupId, @Param("code") Integer code); + + + /** + * 获取指定用户指定日期的申请中的出差申请列表 + * @param userId 用户 + * @param queryDate 日期 + */ + List getApplyingBusinessTripList(@Param("userId") String userId, @Param("queryDate") String queryDate); + + + /** + * 获取指定用户指定日期的外出申请数量 + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + Integer getUserGoOutByTimeSlot(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 获取指定用户指定日期的出差申请数量 + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + Integer getUserBusinessTripByTimeSlot(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 获取已审批的借调记录 + * @author hlp + */ + List getSelfApproveList(@Param("startTime") String startTime, @Param("endTime") String endTime); + + /** + * 获取指定用户指定日期的出差申请数量除开本次申请 + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + Integer getBusinessTripForOa(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("taskId") String taskId); + + /** + * 获取指定用户指定日期的外出申请数量除开本次申请 小时 + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + Integer getUserGoOutForOa(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("taskId") String taskId); + + + /** + * 获取用户时间段内加班申请数量 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.lang.Integer + * @author hlp + */ + Integer getUserWorkByTimeSlotForOa(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("taskId") String taskId); + + /** + * 获取指定用户指定日期的外出申请数量除开本次申请 天 + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + Integer getUserGoOutForDay(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("taskId") String taskId); + + /** + * 获取用户集合在指定日期范围的借调申请中的用户集合 + * @param userIds 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param taskId 任务Id + */ + List checkSelfApprove(@Param("userIds") List userIds, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("taskId") String taskId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveGrantSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveGrantSettingMapper.java new file mode 100644 index 0000000..5dd81f9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveGrantSettingMapper.java @@ -0,0 +1,9 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.attendance.AttendanceLeaveGrantSetting; + +public interface AttendanceLeaveGrantSettingMapper extends BaseMapper { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveRulesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveRulesMapper.java new file mode 100644 index 0000000..21418d5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveRulesMapper.java @@ -0,0 +1,32 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceLeaveRules; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface AttendanceLeaveRulesMapper extends SuperMapper { + + /** + * 列表 + * @param iText 名称 + * @return 列表 + */ + List list(@Param("typeId") String typeId, @Param("iText") String iText); + + /** + * 更新状态 + * @param id 主键值 + * @param state 状态 + * @param userId 用户Id + */ + void updateState(@Param("id") String id, @Param("state") int state, @Param("userId") String userId); + + /** + * 更新请假类型 + * @param leaveTypeId 请假类型Id + */ + void updateByLeaveTypeId(@Param("leaveTypeId") String leaveTypeId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveSettingsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveSettingsMapper.java new file mode 100644 index 0000000..39de141 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveSettingsMapper.java @@ -0,0 +1,7 @@ +package jnpf.attendance.mapper; + + + +public interface AttendanceLeaveSettingsMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveTypeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveTypeMapper.java new file mode 100644 index 0000000..7c56daa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLeaveTypeMapper.java @@ -0,0 +1,10 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceLeaveType; + +/** + * @author panpan + */ +public interface AttendanceLeaveTypeMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLocationSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLocationSettingMapper.java new file mode 100644 index 0000000..20180db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceLocationSettingMapper.java @@ -0,0 +1,22 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceLocationSetting; + +/** + *

+ * 考勤组-考勤点配置表 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceLocationSettingMapper extends SuperMapper { + + /** + * 查询考勤机名称 + * @param sn 考勤机编码 + * @return java.lang.String + */ + String getMachineName(String sn); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineLogMapper.java new file mode 100644 index 0000000..b5ed88b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineLogMapper.java @@ -0,0 +1,27 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceMachineLog; +import jnpf.model.attendance.vo.attendance.LogMiniVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 考勤机日志mapper + * + * @author yanwenfu + * @create 2024-07-30 + */ +public interface AttendanceMachineLogMapper extends SuperMapper { + + /** + * 查询日志列表 + * @param action 动作 + * @param date 日期 + * @param mac mac地址 + * @return List + */ + List getLogList(@Param("action") String action, @Param("date") Date date, @Param("mac") String mac); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineManageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineManageMapper.java new file mode 100644 index 0000000..3c96893 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineManageMapper.java @@ -0,0 +1,89 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceMachineManage; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 考勤机管理mapper + * + * @author yanwenfu + * @create 2024-09-10 + */ +public interface AttendanceMachineManageMapper extends SuperMapper { + + /** + * 查询考勤机绑定的考勤组 + * @param machineId 考勤机id + * @return java.lang.String + */ + String getGroupIdsByMachine(String machineId); + + /** + * 查询总条数 + * @return int + */ + int getTotal(); + + /** + * 查询名称数量 + * @param name 考勤机名称 + * @param id 考勤机id + * @return int + */ + int getMachineNameCount(@Param("name") String name, @Param("id") String id); + + /** + * 查询mac数量 + * @param mac 考勤机mac + * @param id 考勤机id + * @return int + */ + int getMachineMacCount(@Param("mac") String mac, @Param("id") String id); + + /** + * 查询考勤机名称 + * @param mac mac地址 + * @return java.lang.String + */ + String getMachineName(String mac); + + /** 获取数据库时间 */ + Date getDbTime(); + + /** + * 查询关联此考勤组的考勤机 + * @param groupId 考勤组id + * @return java.util.List + */ + List getRelationMachineByGroupId(String groupId); + + /** + * 更新考勤机的考勤组关联 + * @param id 考勤机id + * @param groupIds 考勤组关联 + * @return int + */ + int updateRelationGroup(@Param("id") String id, @Param("groupIds") String groupIds); + + /** + * 查询绑定了考勤机的考勤组 + * @return java.lang.String + */ + String selectGroupBindMachine(); + + /** + * 查询考勤机列表 + * @param machineKind 考勤机类型(enCode) + * @param keywords 关键词(mac, 设备名称) + * @param scopeType 适配范围(1: 成员, 2: 组织) + * @param scopeValueList 适配范围的值 + * @param bizType 业务类型 + * @return java.util.List + */ + List getMachineList(@Param("machineKind") String machineKind, @Param("keywords") String keywords, + @Param("scopeType") Integer scopeType, @Param("list") List scopeValueList, @Param("bizType") String bizType); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineSyncMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineSyncMapper.java new file mode 100644 index 0000000..4fbde73 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceMachineSyncMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceMachineSync; + +/** + * 考勤机同步mapper + * + * @author yanwenfu + * @create 2024-10-24 + */ +public interface AttendanceMachineSyncMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceManagerPermissionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceManagerPermissionMapper.java new file mode 100644 index 0000000..ddbd53a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceManagerPermissionMapper.java @@ -0,0 +1,117 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.AttendanceManagerPermission; +import jnpf.entity.PermissionDict; +import jnpf.model.attendance.vo.*; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * 考勤管理员权限设置 Mapper 接口 + *

+ * + * @author Auto-generator + * @since 2023-11-21 + */ +public interface AttendanceManagerPermissionMapper extends BaseMapper { + /** + * 获取指定考勤组用户的权限结合(含子权限) + */ + List queryGroupUser(@Param("groupId") String groupId); + /** + * 批量获取指定考勤组列表用户的权限结合(含子权限) + */ + List queryGroupUsers(@Param("groupIds") List groupIds); + /** + * 批量获取指定考勤组列表及以上用户的超级管理员权限 + */ + List queryUserForCurrAndUpChildAndSuper(@Param("groupIdList") List groupIdList); /** + * 批量获取指定权限指定考勤组列表及以上用户的超级管理员权限 + */ + List queryPermissionBySpecify(@Param("groupIdList") List groupIdList,@Param("parentCodes") List parentCodes,@Param("childCodes") List childCodes); + + /** + * 获取用户管理员权限类型 + */ + List getByUserId(@Param("userId") String userId, @Param("type") Integer type, @Param("moduleType") Integer moduleType, @Param("groupId") String groupId); + /** + * 获取考勤组管理员权限类型 + */ + List queryMyPermissions(@Param("type") Integer type, @Param("groupId") String groupId, @Param("userId") String userId); + + /** + * 批量保存考勤组管理员信息 + * @param groupAdminList + */ + void batchSaveGroupAdmin(@Param("curGroupAdminList") List groupAdminList); + /** + * 获取用户在考勤组列表里面的管理员权限集合 + */ + List queryInGroupIds(@Param("groupIdList") List groupIdList, @Param("userId") String userId); + /** + * 获取考勤组权限列表里面包含指定编码层级的考勤组列表 + */ + List queryGroupVoInGroupIds(@Param("levelCodeList") List levelCodeList); + /** + * 获取用户在指定考勤组的权限列表 + */ + CurUserPermissionVo queryByGroupId(@Param("groupId") String groupId, @Param("userId") String userId, @Param("permissionName") String permissionName); + /** + * 获取考勤组的指定类型管理员及权限集合 + */ + List queryManagerByGroupIds(@Param("groupIds") List groupIds, @Param("type") Integer type); + + /** + * 获取用户在指定考勤组的权限列表 + * @param userId 用户Id + * @param groupId 考勤组Id + */ + AttendanceGroupAdminVo queryGroupUserPermissionByUserId(@Param("userId") String userId, @Param("groupId") String groupId); + + /** + * 更新用户考勤组管理员权限 + * @param userId 用户Id + * @param groupId 考勤组Id + * @param groupId1 修改后的考勤组Id + */ + void updateUserGroupManagerPermission(@Param("userId") String userId, @Param("groupId") String groupId, @Param("groupId1") String groupId1); + + /** + * 更新用户考勤组管理员权限 + * @param userIds 用户Id集合 + * @param oldGroupId 原来的考勤组Id + * @param groupId 修改后的考勤组Id + */ + void updateBatchUserGroupManagerPermission(@Param("userIds") List userIds, @Param("oldGroupId") String oldGroupId, @Param("groupId") String groupId); + + /** + * 获取用户在指定考勤组的权限列表 + * @param userId 用户Id + * @param groupId 考勤组 + */ + List getGroupManagerPermission(@Param("userId") String userId, @Param("groupId") String groupId); + + + /** + * 获取用户集合在指定考勤组的权限信息 + * @param userIds 用户Ids + * @param groupId 考勤组 + */ + List getGroupManagerPermissionByUserIds(@Param("userIds") List userIds, @Param("groupId") String groupId); + + /** + * 批量添加 + * @param list 用户权限信息 + */ + void addBatch(@Param("list") List list); + + /** + * 根据用户Id和考勤组Id删除对应的权限记录 + * @param userId 用户Id + * @param groupId 考勤组Id + */ + void deleteByGroupUserId(@Param("userId") String userId, @Param("groupId") String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceNoticeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceNoticeMapper.java new file mode 100644 index 0000000..c799f5b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceNoticeMapper.java @@ -0,0 +1,19 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.attendance.AttendanceNoticeEntity; +import org.springframework.stereotype.Component; +import org.apache.ibatis.annotations.Mapper; + +/** + * 考勤消息通知 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-08-08 10:49:41 + */ +@Mapper +@Component +public interface AttendanceNoticeMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateItemMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateItemMapper.java new file mode 100644 index 0000000..7198c24 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateItemMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceQuickTemplateItemEntity; +import jnpf.base.mapper.SuperMapper; + +/** + *

+ * 快速模板-单天模板 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +public interface AttendanceQuickTemplateItemMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateMapper.java new file mode 100644 index 0000000..03e6ac4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceQuickTemplateMapper.java @@ -0,0 +1,64 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceQuickTemplateEntity; +import jnpf.base.mapper.SuperMapper; +import jnpf.model.attendance.dto.FixedClassSaveDto; +import jnpf.model.attendance.dto.QuickTemDto; +import jnpf.model.attendance.vo.attendance.QuickTemVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + *

+ * 考勤配置-快速模板 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +public interface AttendanceQuickTemplateMapper extends SuperMapper { + + /** + * 新增模板信息 + * @param dto 快速排班 + */ + void saveTemplate(@Param("dto") QuickTemDto dto, @Param("userId") String userId); + + /** + * 修改模板信息 + * @param dto 快速排班 + */ + void updateTemplate(@Param("dto") QuickTemDto dto, @Param("userId") String userId); + + /** + * 删除模板绑定的信息信息 + * @param id 快速排班模板Id + */ + void deleteClass(@Param("id") String id); + /** + * 删除模板绑定的信息信息 + * @param id 快速排班模板Id + */ + void deleteTemplateItem(@Param("id") String id, @Param("shiftNameId") String shiftNameId, @Param("nums") List nums); + + + /** + * 批量保存快速排班班次信息 + * @param list 信息集合 + */ + void saveClass(@Param("list") List list); + + /** + * 获取考勤组快速模板集合 + * @param groupId 考勤组Id + */ + List getTemByGroupId(@Param("groupId") String groupId); + + /** + * 获取考勤组快速模板下班次集合信息 + * @param temIds 模板Ids + */ + List getShiftByTemIds(@Param("temIds") List temIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceRepairMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceRepairMapper.java new file mode 100644 index 0000000..14503fa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceRepairMapper.java @@ -0,0 +1,23 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceRepair; +import org.apache.ibatis.annotations.Param; + +/** + * 补卡次数mapper + * + * @author yanwenfu + * @create 2024-07-03 + */ +public interface AttendanceRepairMapper extends SuperMapper { + + /** + * 根据时间判定使用的配置 + * @param applyDate 日期 + * @param userId 用户id + * @param groupId 考勤组id + * @return jnpf.entity.attendance.AttendanceRepair + */ + AttendanceRepair getAttendanceRepairByDate(@Param("applyDate") String applyDate, @Param("userId") String userId, @Param("groupId") String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceResultRollbackMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceResultRollbackMapper.java new file mode 100644 index 0000000..4a2cbe9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceResultRollbackMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceResultRollback; + +/** + * 打卡结果回滚 + * + * @author yanwenfu + * @create 2024-11-11 + */ +public interface AttendanceResultRollbackMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSealSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSealSettingMapper.java new file mode 100644 index 0000000..0e9f92e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSealSettingMapper.java @@ -0,0 +1,7 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceSealSetting; + +public interface AttendanceSealSettingMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSelfApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSelfApproveMapper.java new file mode 100644 index 0000000..fb8aeab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceSelfApproveMapper.java @@ -0,0 +1,21 @@ +package jnpf.attendance.mapper; + + +import jnpf.model.attendance.vo.DailyRuleResultVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +public interface AttendanceSelfApproveMapper { + + /** + * 时间段内存在多个考勤组查询对应的借调信息 + * @param userId 用户id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.util.List + * @author hlp + */ + List getSelfApprove(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftNameSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftNameSettingMapper.java new file mode 100644 index 0000000..3cfdaa9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftNameSettingMapper.java @@ -0,0 +1,101 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceShiftNameEntity; +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.model.attendance.dto.FixedClassDto; +import jnpf.model.attendance.dto.FixedClassSaveDto; +import jnpf.model.attendance.dto.ShiftNameDto; +import jnpf.model.attendance.dto.ShiftNameQueryDto; +import jnpf.model.attendance.vo.attendance.ShiftNameListVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/9 10:27 + * @Version 1.0 (版本号) + */ +public interface AttendanceShiftNameSettingMapper extends SuperMapper { + + /** + * 校验班次名称是否重复 + * @param dto 保存数据 + */ + Integer checkName(@Param("dto") ShiftNameDto dto); + + /** + * 新增班次名称信息 + * @param dto 保存数据 + */ + void save(@Param("dto") ShiftNameDto dto); + + /** + * 保存班次名称信息 + * @param dto 保存数据 + */ + void update(@Param("dto") ShiftNameDto dto); + + /** + * 获取班次名称信息详情 + * @param id 保存数据 + */ + ShiftNameVo getDetail(@Param("id") String id); + + /** + * 删除原来的关联班次信息 + * @param id 班次名称信息id + */ + void deletePeriodByShiftId(@Param("id") String id); + + /** + * 修改启停状态 + * @param shiftNameId 班次名称Id + * @param enable 状态(1启用 2停用) + * @param userId 用户Id + */ + void updateEnable(@Param("shiftNameId") String shiftNameId, @Param("enable") Integer enable, @Param("userId") String userId); + + /** + * 删除班次 + * @param shiftNameId 班次Id + * @param userId 用户 + */ + void delete(@Param("shiftNameId") String shiftNameId, @Param("userId") String userId); + + /** + * 获取固定排班班次详情 + * @param groupId 考勤组 + */ + List getFixedClass(@Param("groupId") String groupId); + + + /** + * 新增固定排班班次详情 + * @param fixedClassDtos 固定班信息 + */ + void saveFixedClass(@Param("fixedClassDtos") List fixedClassDtos); + + /** + * 删除固定排班信息 + * @param settingId 配置Id + */ + void deleteFixedClass(@Param("settingId") String settingId); + + /** + * 获取考勤组班次列表 + * @param queryDto 考勤组查询信息 + * @return + */ + List getListByGroupId(@Param("queryDto") ShiftNameQueryDto queryDto); + + /** + * 修改排班基础配置 + * @param groupId 考勤组Id + * @param systemType 班制类型(1固定班 2排班) + * @param userId 用户 + */ + void updateByGroupId(@Param("groupId") String groupId, @Param("systemType") Integer systemType, @Param("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingMapper.java new file mode 100644 index 0000000..c713b25 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceShiftSettingEntity; +import jnpf.base.mapper.SuperMapper; + +/** + *

+ * 考勤组配置-考勤配置 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +public interface AttendanceShiftSettingMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingPeriodMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingPeriodMapper.java new file mode 100644 index 0000000..d5b6995 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceShiftSettingPeriodMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.base.mapper.SuperMapper; + +/** + *

+ * 考勤组-考勤配置-时段 Mapper 接口 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +public interface AttendanceShiftSettingPeriodMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceMapper.java new file mode 100644 index 0000000..d8873c2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceMapper.java @@ -0,0 +1,36 @@ +package jnpf.attendance.mapper; + +import jnpf.model.attendance.vo.attendance.AttendanceUserBalanceDetailVo; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author panpan + */ +public interface AttendanceUserBalanceMapper { + + /** + * 获取详情 + * @param id 主键 + * @return 详情 + */ + List getDetail(@Param("id") String id,@Param("userId") String userId); + + + /** + * 批量添加 + * @param list 列表 + */ + void batchAddBalanceDetail(@Param("list") List list); + + /** + * 获取指定类型的余额 + * @param balanceId LeaveBalanceId 查询存休时 为null + * @param userId 用户Id + * @param isRetirementLeave 是否统计存休 LeaveBalanceId不为空且对应的假期规则能使用存休抵扣时才为 true + * @return 余额 + */ + BigDecimal getTotalBalance(@Param("balanceId") String balanceId,@Param("userId") String userId,@Param("isRetirementLeave") boolean isRetirementLeave); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceRecordMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceRecordMapper.java new file mode 100644 index 0000000..25db51a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserBalanceRecordMapper.java @@ -0,0 +1,14 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.attendance.AttendanceBalanceRecordEntity; + + +/** + * @author + * @description 用户余额获取记录表 Mapper接口 + * @version V2.0 + */ +public interface AttendanceUserBalanceRecordMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserConfigMapper.java new file mode 100644 index 0000000..f35270b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserConfigMapper.java @@ -0,0 +1,22 @@ +package jnpf.attendance.mapper; + +import jnpf.model.attendance.vo.attendance.UserConfigVo; +import org.apache.ibatis.annotations.Param; + +/** + * @Author huanglinpan + * @Date 2024/6/25 9:34 + * @Version 1.0 (版本号) + */ +public interface AttendanceUserConfigMapper { + + /** 获取用户配置 */ + UserConfigVo getUserConfig(@Param("userId") String userId); + + /** 更新用户配置 */ + void updateUSerConfig(@Param("configJson") String configJson, @Param("userId") String userId); + + /** 新增用户配置 */ + void addUserConfig(@Param("userId") String userId, @Param("userConfigVo") UserConfigVo userConfigVo); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFaceMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFaceMapper.java new file mode 100644 index 0000000..a7644b4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFaceMapper.java @@ -0,0 +1,37 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.model.attendance.dto.FaceQueryDto; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.attendance.vo.UserFaceDetailVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 考勤机人脸数据mapper + * + * @author yanwenfu + * @create 2023-11-30 + */ +public interface AttendanceUserFaceMapper extends SuperMapper { + + /** + * 人脸列表 + * @param queryDto 查询条件 + * @param groupUserList 考勤组成员 + * @return java.util.List + */ + List getUserFaceList(@Param("queryDto") FaceQueryDto queryDto, @Param("list") List groupUserList); + + /** + * 查询所有没有缩略图的人脸列表 + */ + List getNoThumbnail(); + + /** + * 更新人脸缩略图 + */ + void updateUserFaceThumbnail(@Param("userFaceDetailVos") UserFaceDetailVo userFaceDetailVos); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFingerprintMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFingerprintMapper.java new file mode 100644 index 0000000..da39ed6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserFingerprintMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceUserFingerprint; + +/** + * 用户指纹mapper + * + * @author yanwenfu + * @create 2024-03-04 + */ +public interface AttendanceUserFingerprintMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserSettingMapper.java new file mode 100644 index 0000000..5d798a3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/AttendanceUserSettingMapper.java @@ -0,0 +1,39 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceAppUserSetting; +import jnpf.model.attendance.dto.AppUserSettingQueryDto; +import jnpf.model.attendance.dto.AttendanceAppUserSettingDto; +import jnpf.model.attendance.vo.UserSettingVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/8/8 10:57 + * @Version 1.0 (版本号) + */ +public interface AttendanceUserSettingMapper extends SuperMapper { + + /** 新增配置 */ + void add(@Param("attendanceAppUserSettingDto") AttendanceAppUserSettingDto attendanceAppUserSettingDto); + + /** 查询是否重复 */ + String getDetail(@Param("attendanceAppUserSettingDto") AttendanceAppUserSettingDto attendanceAppUserSettingDto); + + /** 更新配置 */ + void update(@Param("attendanceAppUserSettingDto") AttendanceAppUserSettingDto attendanceAppUserSettingDto); + + /** 根据父级Id查询设置列表 */ + List getSettingList(@Param("pId") String pId, @Param("type") Integer type); + + /** 查询用户设置/考勤组设置 */ + List getUserSetting(@Param("appUserSettingQueryDto") AppUserSettingQueryDto appUserSettingQueryDto); + + /** 批量获取考勤组设置 */ + List getGroupSetting(@Param("groupIds") List groupIds, @Param("type") Integer type); + + /** 根据用户编码及用户批量获取设置 */ + List getUserSettingByCode(@Param("associationId") List associationId, @Param("type") Integer type, @Param("code") List code); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultMapper.java new file mode 100644 index 0000000..0645aab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceClockInResult; + +/** + * 打卡结果mapper + * + * @author yanwenfu + * @create 2023-11-22 + */ +public interface ClockInResultMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultV2Mapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultV2Mapper.java new file mode 100644 index 0000000..84d0d6e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/ClockInResultV2Mapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceClockInResult; + +/** + * 打卡mapper2.0 + * + * @author yanwenfu + * @create 2025-09-23 + */ +public interface ClockInResultV2Mapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/CommonSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/CommonSettingMapper.java new file mode 100644 index 0000000..0550066 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/CommonSettingMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceCommonSetting; + +/** + * 通用配置mapper + * + * @author yanwenfu + * @create 2025-09-17 + */ +public interface CommonSettingMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/DailyRuleChangeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/DailyRuleChangeMapper.java new file mode 100644 index 0000000..1171222 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/DailyRuleChangeMapper.java @@ -0,0 +1,31 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.DailyRuleChange; +import jnpf.model.attendance.vo.UserDayVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * 出勤规则变更 + * + * @author yanwenfu + * @create 2026-05-20 + */ +public interface DailyRuleChangeMapper extends SuperMapper { + + /** + * 批量保存变更 + * @param list 变更列表 + */ + void saveRecordBatch(@Param("list") List list); + + /** + * 删除小于当前时间的记录 + * @param list 变更列表 + * @param day 当前时间 + */ + void removeBatch(@Param("list") List list, @Param("day") Date day); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/EnableBalanceMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/EnableBalanceMapper.java new file mode 100644 index 0000000..7d65007 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/EnableBalanceMapper.java @@ -0,0 +1,37 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceEnableBalance; +import jnpf.model.attendance.vo.attendance.OvertimeSalaryHoursJsonVo; +import jnpf.model.attendance.vo.attendance.OvertimeSalaryHoursVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 加班余额[不存休,仅记录]mapper + * + * @author yanwenfu + * @create 2025-10-03 + */ +public interface EnableBalanceMapper extends SuperMapper { + /** + * 获取加班算薪加班小时数(不计算存休-结算薪酬) + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param userIds 用户ID + * @return 加班算薪加班小时数( + */ + List getOvertimeSalary(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("userIds") List userIds); + + /** + * 获取加班算薪加班小时数(不计算存休-结算薪酬) + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param userIds 用户ID + * @return 加班算薪加班小时数( + */ + List getOvertimeSalaryJson(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("userIds") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceFaceChangeLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceFaceChangeLogMapper.java new file mode 100644 index 0000000..60864ef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceFaceChangeLogMapper.java @@ -0,0 +1,23 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbAttendanceFaceChangeLog; +import jnpf.model.attendance.vo.ChangeLogVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 人脸变动 mapper + * + * @author yanwenfu + * @create 2025-04-14 + */ +public interface FtbAttendanceFaceChangeLogMapper extends SuperMapper { + + /** + * 查询变动记录列表 + * @return java.util.List + */ + List getChangeLogList(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("list") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingConfigMapper.java new file mode 100644 index 0000000..c0a0539 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingConfigMapper.java @@ -0,0 +1,20 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingConfig; +import org.apache.ibatis.annotations.Mapper; + +/** + * 划线排班配置Mapper + * + * @author ahua + * @version 2.1 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2025-12-30 + */ +@Mapper +public interface FtbAttendanceLineSchedulingConfigMapper extends BaseMapper, SuperMapper { + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingPayrollHoursMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingPayrollHoursMapper.java new file mode 100644 index 0000000..42c31ce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbAttendanceLineSchedulingPayrollHoursMapper.java @@ -0,0 +1,18 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingPayrollHours; +import org.apache.ibatis.annotations.Mapper; + +/** + * 划线排班计薪工时Mapper + * + * @author jnpf + * @since 2026-02-27 + */ +@Mapper +public interface FtbAttendanceLineSchedulingPayrollHoursMapper extends BaseMapper, SuperMapper { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupDrawingParamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupDrawingParamMapper.java new file mode 100644 index 0000000..e378d6f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupDrawingParamMapper.java @@ -0,0 +1,22 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbScheduleGroupDrawingParamEntity; +import jnpf.model.attendance.dto.scheduling.LineSchedulingRuleDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 考勤组划线排班参数 Mapper。 + */ +@Mapper +public interface FtbScheduleGroupDrawingParamMapper extends SuperMapper { + + /** + * 按考勤组查询划线排班规则(列别名与 {@link LineSchedulingRuleDto} 一致)。 + * + * @param groupId 考勤组主键 + * @return 无记录时为 null + */ + LineSchedulingRuleDto selectLineRuleDtoByGroupId(@Param("groupId") String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupFixedParamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupFixedParamMapper.java new file mode 100644 index 0000000..e173978 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/FtbScheduleGroupFixedParamMapper.java @@ -0,0 +1,22 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.FtbScheduleGroupFixedParamEntity; +import jnpf.model.attendance.dto.scheduling.FixedSchedulingRuleDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 考勤组固定排班核心参数 Mapper。 + */ +@Mapper +public interface FtbScheduleGroupFixedParamMapper extends SuperMapper { + + /** + * 按考勤组查询固定排班规则(列别名与 {@link FixedSchedulingRuleDto} 一致)。 + * + * @param groupId 考勤组主键 + * @return 无记录时为 null + */ + FixedSchedulingRuleDto selectFixedRuleDtoByGroupId(@Param("groupId") String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/InitializationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/InitializationMapper.java new file mode 100644 index 0000000..830e80d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/InitializationMapper.java @@ -0,0 +1,23 @@ +package jnpf.attendance.mapper; + +import jnpf.model.attendance.vo.AttendanceBalanceRecordVo; +import jnpf.model.attendance.vo.attendance.BalanceUseRecordVo; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/7/1 9:17 + * @Version 1.0 (版本号) + */ +public interface InitializationMapper { + /** 初始化存休表 */ + void truncatStorageRest(); + + /** 获取所有的用户劵逻辑 */ + List getBalanceRecord(); + + /** 获取所有的用户劵逻辑 */ + List getAllBalanceUseRecord(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleDetailMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleDetailMapper.java new file mode 100644 index 0000000..a342fa0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleDetailMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceOvertimeRuleDetail; + +/** + * 加班规则子表mapper + * + * @author yanwenfu + * @create 2025-09-17 + */ +public interface OvertimeRuleDetailMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleMapper.java new file mode 100644 index 0000000..15945db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/OvertimeRuleMapper.java @@ -0,0 +1,24 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceOvertimeRule; +import jnpf.model.attendance.vo.attendance.OvertimeRulePageVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 加班规则主表mapper + * + * @author yanwenfu + * @create 2025-09-17 + */ +public interface OvertimeRuleMapper extends SuperMapper { + + /** + * 查询加班规则列表 + * @param ruleName 规则名称 + * @return java.util.List + */ + List selectOvertimeRulePage(@Param("ruleName") String ruleName); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PermissionDictMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PermissionDictMapper.java new file mode 100644 index 0000000..e434579 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PermissionDictMapper.java @@ -0,0 +1,16 @@ +package jnpf.attendance.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.PermissionDict; + +/** + *

+ * 权限字典表 Mapper 接口 + *

+ * + * @author Auto-generator + * @since 2023-11-21 + */ +public interface PermissionDictMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PublicHolidayRulesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PublicHolidayRulesMapper.java new file mode 100644 index 0000000..80b0632 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/PublicHolidayRulesMapper.java @@ -0,0 +1,96 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendancePublicHolidayRules; +import jnpf.model.attendance.dto.AttendancePublicHolidayRulesDto; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayBalance; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayRulesVo; +import jnpf.model.attendance.vo.attendance.PublicHolidayTransferListVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +/** + * @author panpan + */ +public interface PublicHolidayRulesMapper extends SuperMapper { + /** + * 列表 + * @param iText 搜索条件 + * @return 列表 + */ + List list(@Param("iText") String iText); + + /** + * 添加 + * @param publicHolidayRulesDto 实体 + * @return 主键 + */ + void insert(@Param("publicHolidayRulesDto") AttendancePublicHolidayRulesDto publicHolidayRulesDto, @Param("userId") String userId); + + /** + * 修改 + * @param publicHolidayRulesDto 实体 + */ + void update(@Param("publicHolidayRulesDto") AttendancePublicHolidayRulesDto publicHolidayRulesDto, @Param("userId") String userId); + + + /** + * 修改状态 + * @param id 主键 + * @param state 状态 1是 0否 + * @param userId 用户id + */ + void updateState(@Param("id") String id, @Param("state") int state, @Param("userId") String userId); + + /** + * 公休余额查询 + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + * @return 公休余额列表 + */ + List getBalanceList(@Param("yearMonth") String yearMonth, @Param("userIds") List userIds); + + /** + * 公休余额批量添加 + * @param addList 公休余额列表 + * @param yearMonth 年月格式yyyy-MM + */ + void batchAddPublicHolidayBalance(@Param("addList") List addList, @Param("yearMonth") String yearMonth); + + /** + * 公休余额查询 + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + * @return 公休余额列表 + */ + List getPublicHolidayBalanceList(@Param("yearMonth") String yearMonth, @Param("userIds") List userIds); + + /** + * 公休余额删除 + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + */ + void deletePublicHolidayBalance(@Param("yearMonth") String yearMonth, @Param("userIds") List userIds); + + /** + * 批量查询用户公休转存休天数 + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + */ + List getPublicHolidayTransferList(@Param("yearMonth") String yearMonth, @Param("userIds") List userIds); + + /** + * 清除公休范围 + */ + void updateScopeOfAdaptation(); + + /** + * 查询用户指定月份公休已有记录 + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + * @return 公休余额列表 + */ + List selectPublicHolidayBalanceList(@Param("yearMonth") String yearMonth, @Param("userIds") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/RuleScopeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/RuleScopeMapper.java new file mode 100644 index 0000000..9aefebd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/RuleScopeMapper.java @@ -0,0 +1,45 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.dto.UserOrgDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 适配范围mapper + * + * @author yanwenfu + * @create 2025-09-18 + */ +public interface RuleScopeMapper extends SuperMapper { + + /** + * 按优先级查询用户生效中的规则 + * @param userId 用户id + * @param organizeId 组织id + * @param bizType 业务类型 + * @return java.util.List + */ + List selectUserEffectList(@Param("userId") String userId, @Param("organizeId") String organizeId, @Param("bizType") String bizType); + + /** + * 按优先级查询用户生效中的规则[批量] + * @param userOrgList 用户组织列表 + * @param bizType 业务类型 + * @param priority 使用优先级(1: 是, 0: 否) + * @return java.util.List + */ + List selectUserEffectListBatch(@Param("list") List userOrgList, @Param("bizType") String bizType, + @Param("priority") Integer priority, @Param("leaveTypeIds") List leaveTypeIds); + + /** + * 查询组织生效中的规则[批量] + * @param organizeId 组织id + * @param value 类型 + * @return java.util.List + */ + List selectOrgEffectListBatch(@Param("organizeId") String organizeId, @Param("value") String value); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StatisticsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StatisticsMapper.java new file mode 100644 index 0000000..c2dfa63 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StatisticsMapper.java @@ -0,0 +1,73 @@ +package jnpf.attendance.mapper; + +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.attendance.UserRuleListVo; +import jnpf.model.common.DateRangeDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 统计服务mapper + * + * @author shitou + * @date 2023/11/21 + */ +@Mapper +public interface StatisticsMapper { + /** + * 查询用户关联的考勤组信息 + * + * @param groupIds 考勤组ID集合 + * @param userIds 用户ID集合 + */ + List getUserAssociationGroupDataList(@Param("groupIds") List groupIds, + @Param("userIds") List userIds); + + /** + * 查询被借调记录 + * + * @param userIds 用户id集合 + * @param groupIds 考勤组d集合 + */ + List getUserSecondRecordList(@Param("userIds") List userIds, @Param("groupIds") List groupIds); + + /** + * 获取用户打卡结果数据 + * + * @param userIds 用户ID集合 + * @param dateRangeDto 时间范围 + * @param groupIds 考勤组ID集合 + * @param clockInStatus 打卡结果类型 + * @param absenceStatus 缺勤状态 + * @param repairedStatus 是否补卡 + * @param clockInKind 打卡类型 + */ + List getUserClockInResultList(@Param("attendanceTypeList") List attendanceTypeList, + @Param("userIds") List userIds, @Param("dateRangeDto") DateRangeDto dateRangeDto, + @Param("groupIds") List groupIds, @Param("clockInStatus") Integer clockInStatus, + @Param("absenceStatus") Integer absenceStatus, @Param("repairedStatus") Integer repairedStatus, + @Param("clockInKind") Integer clockInKind); + + /** + * 获取用户排班数据 + * + * @param userIds 用户ID集合 + * @param dateRangeDto 时间范围 + * @param groupIdList 考勤组ID集合 + * @param attendanceTypeList 排班类型集合 + */ + List getUserRuleRecordList(@Param("userIds") List userIds, @Param("dateRangeDto") DateRangeDto dateRangeDto, + @Param("groupIdList") List groupIdList, @Param("attendanceTypeList") List attendanceTypeList); + + + /** + * 获取用户指定考勤组时间范围内的排班数据 + * @param userIds 用户ID集合 + * @param dateRangeDto 时间范围 + * @param groupId 群ID + * @param code 排班类型集合 + */ + List getUserRuleList(@Param("userIds") List userIds, @Param("dateRangeDto") DateRangeDto dateRangeDto, @Param("groupId") String groupId, @Param("code") List code); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StorageRestMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StorageRestMapper.java new file mode 100644 index 0000000..cf9a1ef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/StorageRestMapper.java @@ -0,0 +1,24 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceStorageRest; +import jnpf.model.attendance.vo.attendance.AttendanceStorageRestVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/7/1 16:13 + * @Version 1.0 (版本号) + */ +public interface StorageRestMapper extends SuperMapper { + /** 批量保存数据 */ + void saveBatch(@Param("list") List list); + + /** 获取用户指定年月的存休 */ + List batchGetUserStorageRest(@Param("userIds") List userIds, @Param("yearMonth") String yearMonth); + + /** 实时获取当月存休 */ + List getRealTimeUserStorageRest(@Param("userIds") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/UserPhoneMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/UserPhoneMapper.java new file mode 100644 index 0000000..3fbe73f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/UserPhoneMapper.java @@ -0,0 +1,24 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.attendance.AttendanceUserPhone; +import jnpf.model.attendance.vo.attendance.UsualPhonePageVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 用户常用设备mapper + * + * @author yanwenfu + * @create 2025-09-17 + */ +public interface UserPhoneMapper extends SuperMapper { + + /** + * 查询常用手机列表 + * @param userIds 用户id集合 + * @return java.util.List + */ + List getUsualPhoneList(@Param("list") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationMapper.java new file mode 100644 index 0000000..76201c6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.Workstation; + +/** + * 考勤工作站mapper + * + * @author AI Generated + * @create 2026-05-11 + */ +public interface WorkstationMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationUserMapper.java new file mode 100644 index 0000000..f3bcec7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/mapper/WorkstationUserMapper.java @@ -0,0 +1,13 @@ +package jnpf.attendance.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.WorkstationUser; + +/** + * 考勤工作站人员关联mapper + * + * @author AI Generated + * @create 2026-05-11 + */ +public interface WorkstationUserMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchConfig.java new file mode 100644 index 0000000..46fa413 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchConfig.java @@ -0,0 +1,38 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; + +/** + * 历史发现的固定班次与考勤组 {@code ShiftPeriodVo} 匹配阈值(产品:覆盖度、有效率)。 + */ +public final class AttendanceGroupShiftMatchConfig implements Serializable { + + /** 历史时段覆盖度 = 重叠时长 / 历史班次时长 */ + private final double minHistoryCoverageRatio; + /** 候选班次有效率 = 重叠时长 / 候选考勤班次总时长(防超排) */ + private final double minCandidateEfficiencyRatio; + + public AttendanceGroupShiftMatchConfig( + double minHistoryCoverageRatio, double minCandidateEfficiencyRatio) { + if (minHistoryCoverageRatio < 0 || minHistoryCoverageRatio > 1) { + throw new IllegalArgumentException("minHistoryCoverageRatio must be in [0,1]"); + } + if (minCandidateEfficiencyRatio < 0 || minCandidateEfficiencyRatio > 1) { + throw new IllegalArgumentException("minCandidateEfficiencyRatio must be in [0,1]"); + } + this.minHistoryCoverageRatio = minHistoryCoverageRatio; + this.minCandidateEfficiencyRatio = minCandidateEfficiencyRatio; + } + + public static AttendanceGroupShiftMatchConfig defaults() { + return new AttendanceGroupShiftMatchConfig(0.7d, 0.6d); + } + + public double getMinHistoryCoverageRatio() { + return minHistoryCoverageRatio; + } + + public double getMinCandidateEfficiencyRatio() { + return minCandidateEfficiencyRatio; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchResult.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchResult.java new file mode 100644 index 0000000..8ee09f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftMatchResult.java @@ -0,0 +1,41 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; + +/** + * 一条历史固定班次与考勤组中某一 {@link jnpf.model.attendance.vo.ShiftPeriodVo} 的匹配结果。 + */ +public final class AttendanceGroupShiftMatchResult implements Serializable { + + private final String groupShiftPeriodId; + private final double coverageRatio; + private final double efficiencyRatio; + private final boolean exact; + + public AttendanceGroupShiftMatchResult( + String groupShiftPeriodId, + double coverageRatio, + double efficiencyRatio, + boolean exact) { + this.groupShiftPeriodId = groupShiftPeriodId == null ? "" : groupShiftPeriodId; + this.coverageRatio = coverageRatio; + this.efficiencyRatio = efficiencyRatio; + this.exact = exact; + } + + public String getGroupShiftPeriodId() { + return groupShiftPeriodId; + } + + public double getCoverageRatio() { + return coverageRatio; + } + + public double getEfficiencyRatio() { + return efficiencyRatio; + } + + public boolean isExact() { + return exact; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftPeriodMatcher.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftPeriodMatcher.java new file mode 100644 index 0000000..5c47a6d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceGroupShiftPeriodMatcher.java @@ -0,0 +1,362 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.PeriodNameVO; +import jnpf.model.attendance.vo.ShiftPeriodVo; +import jnpf.model.attendance.vo.attendance.ShiftNameViewVo; + +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 将「相似历史日统计出的固定班次」与考勤组 {@link ShiftPeriodVo}({@code getPeriodList})做匹配。 + * + *

匹配顺序: + *

    + *
  • 若历史侧带有考勤班次 id({@link jnpf.model.attendance.vo.ShiftPostStatVo#getShiftId()}/{@linkplain PostFixedShiftCandidate#getHistoricalAttendanceShiftId()}), + * 且在 {@code ShiftPeriodVo} 列表中存在同 id,则直接使用该班次,做下列时段阈值与择优; + *
  • + *
  • 否则:「精确」/阈值模糊匹配择优(见下)。 + *
  • + *
+ * + *

规则摘要(时段路径): + *

    + *
  • 先找「精确」:覆盖度与有效率均为 1(考勤总时长全部落在历史需求内、且历史需求被完全覆盖)。
  • + *
  • 否则在覆盖度、有效率阈值下选最优:先比有效率,再比覆盖度;仍并列则随机。
  • + *
  • 无满足项则该历史班次不参与后续固定班排班。
  • + *
+ */ +public final class AttendanceGroupShiftPeriodMatcher { + + private static final double EPS = 1e-6; + + private AttendanceGroupShiftPeriodMatcher() {} + + /** + * 历史班次 id 与考勤组班次 {@linkplain ShiftPeriodVo#getId()} 一致时直接使用,不进行覆盖度/有效率比对。 + * + * @return 命中则精确结果;{@code periods} / id 不可用则 empty + */ + public static Optional tryMatchConfiguredShiftById( + String historicalAttendanceShiftId, List periods) { + String hid = normalizedGroupShiftId(historicalAttendanceShiftId); + if (hid.isEmpty() || periods == null || periods.isEmpty()) { + return Optional.empty(); + } + for (ShiftPeriodVo vo : periods) { + if (vo == null || vo.getId() == null) { + continue; + } + if (hid.equals(normalizedGroupShiftId(vo.getId()))) { + String boundId = vo.getId().trim(); + return Optional.of(new AttendanceGroupShiftMatchResult(boundId, 1d, 1d, true)); + } + } + return Optional.empty(); + } + + private static String normalizedGroupShiftId(String s) { + return s == null ? "" : s.trim(); + } + + /** + * @param periods {@code null} 表示不做过滤(兼容旧调用);空列表表示无考勤班次配置,将丢弃全部历史固定班候选。 + */ + public static Map> filterCandidatesByAttendancePeriods( + Map> candidatesByPost, + List periods, + AttendanceGroupShiftMatchConfig cfg, + Random random) { + if (candidatesByPost == null || candidatesByPost.isEmpty()) { + return candidatesByPost == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new LinkedHashMap<>()); + } + if (periods == null) { + return Collections.unmodifiableMap(new LinkedHashMap<>(candidatesByPost)); + } + if (periods.isEmpty()) { + Map> empty = new LinkedHashMap<>(); + for (String k : candidatesByPost.keySet()) { + empty.put(k, Collections.emptyList()); + } + return Collections.unmodifiableMap(empty); + } + AttendanceGroupShiftMatchConfig c = cfg == null ? AttendanceGroupShiftMatchConfig.defaults() : cfg; + Random rnd = random == null ? ThreadLocalRandom.current() : random; + Map> out = new LinkedHashMap<>(); + for (Map.Entry> e : candidatesByPost.entrySet()) { + List kept = new ArrayList<>(); + for (PostFixedShiftCandidate cand : emptyIfNull(e.getValue())) { + if (pickBestMatch(cand, periods, c, rnd).isPresent()) { + kept.add(cand); + } + } + out.put(e.getKey(), Collections.unmodifiableList(kept)); + } + return Collections.unmodifiableMap(out); + } + + public static Optional pickBestMatch( + PostFixedShiftCandidate historical, + List periods, + AttendanceGroupShiftMatchConfig cfg, + Random random) { + if (historical == null) { + return Optional.empty(); + } + Optional direct = + tryMatchConfiguredShiftById(historical.getHistoricalAttendanceShiftId(), periods); + if (direct.isPresent()) { + return direct; + } + return pickBestMatchForTimeWindow( + historical.getStartMinuteInclusive(), + historical.getEndMinuteExclusive(), + periods, + cfg, + random, + null); + } + + /** + * 将计划时段与考勤组班次匹配:先可选按历史快照 id({@link #tryMatchConfiguredShiftById});否则时段路径下先精确, + * 否则在阈值下选最优。 + * + * @param startMinuteInclusive 计划班次左闭起点(分钟) + * @param endMinuteExclusive 计划班次右开终点(分钟) + * @see #pickBestMatchForTimeWindow(int, int, List, AttendanceGroupShiftMatchConfig, Random, String) + */ + public static Optional pickBestMatchForTimeWindow( + int startMinuteInclusive, + int endMinuteExclusive, + List periods, + AttendanceGroupShiftMatchConfig cfg, + Random random) { + return pickBestMatchForTimeWindow( + startMinuteInclusive, endMinuteExclusive, periods, cfg, random, null); + } + + /** + * @param historicalAttendanceShiftId 若非空且在 {@code periods} 中存在相同 id,则不与计划时段比对,直接使用该班次 + */ + public static Optional pickBestMatchForTimeWindow( + int startMinuteInclusive, + int endMinuteExclusive, + List periods, + AttendanceGroupShiftMatchConfig cfg, + Random random, + String historicalAttendanceShiftId) { + Optional direct = + tryMatchConfiguredShiftById(historicalAttendanceShiftId, periods); + if (direct.isPresent()) { + return direct; + } + if (periods == null || periods.isEmpty()) { + return Optional.empty(); + } + AttendanceGroupShiftMatchConfig c = cfg == null ? AttendanceGroupShiftMatchConfig.defaults() : cfg; + Random rnd = random == null ? ThreadLocalRandom.current() : random; + int hS = startMinuteInclusive; + int hE = endMinuteExclusive; + int histLen = hE - hS; + if (histLen <= 0) { + return Optional.empty(); + } + + List exact = new ArrayList<>(); + List fuzzy = new ArrayList<>(); + for (ShiftPeriodVo vo : periods) { + if (vo == null) { + continue; + } + ScoredShift s = score(vo, hS, hE, histLen); + if (s == null) { + continue; + } + if (s.exact) { + exact.add(s); + } else if (s.coverage + EPS >= c.getMinHistoryCoverageRatio() + && s.efficiency + EPS >= c.getMinCandidateEfficiencyRatio()) { + fuzzy.add(s); + } + } + + if (!exact.isEmpty()) { + return Optional.of(toResult(pickRandom(exact, rnd))); + } + if (fuzzy.isEmpty()) { + return Optional.empty(); + } + fuzzy.sort( + Comparator.comparingDouble((ScoredShift s) -> s.efficiency) + .thenComparingDouble(s -> s.coverage) + .thenComparing(s -> s.shiftId, Comparator.nullsFirst(String::compareTo)) + .reversed()); + double bestEff = fuzzy.get(0).efficiency; + double bestCov = fuzzy.get(0).coverage; + List tier = new ArrayList<>(); + for (ScoredShift s : fuzzy) { + if (Math.abs(s.efficiency - bestEff) <= EPS && Math.abs(s.coverage - bestCov) <= EPS) { + tier.add(s); + } + } + return Optional.of(toResult(pickRandom(tier, rnd))); + } + + private static AttendanceGroupShiftMatchResult toResult(ScoredShift s) { + return new AttendanceGroupShiftMatchResult(s.shiftId, s.coverage, s.efficiency, s.exact); + } + + private static ScoredShift pickRandom(List list, Random rnd) { + return list.get(rnd.nextInt(list.size())); + } + + private static List emptyIfNull(List list) { + return list == null ? Collections.emptyList() : list; + } + + private static final class ScoredShift { + final String shiftId; + final double coverage; + final double efficiency; + final boolean exact; + + ScoredShift(String shiftId, double coverage, double efficiency, boolean exact) { + this.shiftId = shiftId; + this.coverage = coverage; + this.efficiency = efficiency; + this.exact = exact; + } + } + + /** @return null 若无法解析出有效时段 */ + private static ScoredShift score(ShiftPeriodVo vo, int hS, int hE, int histLen) { + List merged = mergedNormalizedSegments(vo); + if (merged.isEmpty()) { + return null; + } + long overlap = 0; + for (int[] seg : merged) { + overlap += overlapLen(seg[0], seg[1], hS, hE); + } + long totalCand = 0; + for (int[] seg : merged) { + totalCand += (long) seg[1] - seg[0]; + } + if (totalCand <= 0) { + return null; + } + double coverage = overlap / (double) histLen; + double efficiency = overlap / (double) totalCand; + boolean exact = coverage + EPS >= 1d && efficiency + EPS >= 1d; + String id = vo.getId() == null ? "" : vo.getId(); + return new ScoredShift(id, coverage, efficiency, exact); + } + + private static long overlapLen(int a, int b, int hS, int hE) { + return Math.max(0, (long) Math.min(b, hE) - Math.max(a, hS)); + } + + private static List mergedNormalizedSegments(ShiftPeriodVo vo) { + ShiftNameViewVo sn = vo.getSchedulingPeriod(); + if (sn == null || sn.getPeriodNames() == null || sn.getPeriodNames().isEmpty()) { + return Collections.emptyList(); + } + List raw = new ArrayList<>(); + for (PeriodNameVO p : sn.getPeriodNames()) { + if (p == null) { + continue; + } + for (int[] seg : splitToSameDayHalfHourSegments(p)) { + if (seg[1] > seg[0]) { + raw.add(seg); + } + } + } + if (raw.isEmpty()) { + return Collections.emptyList(); + } + raw.sort(Comparator.comparingInt(a -> a[0])); + List merged = new ArrayList<>(); + int curS = raw.get(0)[0]; + int curE = raw.get(0)[1]; + for (int i = 1; i < raw.size(); i++) { + int s = raw.get(i)[0]; + int e = raw.get(i)[1]; + if (s <= curE) { + curE = Math.max(curE, e); + } else { + merged.add(new int[] {curS, curE}); + curS = s; + curE = e; + } + } + merged.add(new int[] {curS, curE}); + return merged; + } + + /** + * 将一条打卡时段拆到自然日 [0,1440) 上的半小时对齐区间;跨午夜拆两段(与历史排班片段对齐方式一致)。 + */ + private static List splitToSameDayHalfHourSegments(PeriodNameVO p) { + LocalTime inT = parseClockPoint(p.getInPoint()); + LocalTime outT = parseClockPoint(p.getOutPoint()); + if (inT == null || outT == null) { + return Collections.emptyList(); + } + int sm = HalfHourSlotLabel.localTimeToMinute(inT); + int em = HalfHourSlotLabel.localTimeToMinute(outT); + List out = new ArrayList<>(); + if (outT.isAfter(inT)) { + int[] norm = ShiftHalfHourNormalizer.normalizeMinuteRange(sm, em); + if (norm != null && norm[1] > norm[0]) { + out.add(norm); + } + return out; + } + int[] n1 = ShiftHalfHourNormalizer.normalizeMinuteRange(sm, 24 * 60); + if (n1 != null && n1[1] > n1[0]) { + out.add(n1); + } + int[] n2 = ShiftHalfHourNormalizer.normalizeMinuteRange(0, em); + if (n2 != null && n2[1] > n2[0]) { + out.add(n2); + } + return out; + } + + static LocalTime parseClockPoint(String s) { + if (s == null) { + return null; + } + String t = s.trim(); + if (t.isEmpty()) { + return null; + } + try { + if (t.length() >= 5 && t.charAt(2) == ':') { + return LocalTime.parse(t.substring(0, 5)); + } + int sp = t.lastIndexOf(' '); + if (sp >= 0 && sp + 6 <= t.length()) { + String tail = t.substring(sp + 1, sp + 6); + if (tail.charAt(2) == ':') { + return LocalTime.parse(tail); + } + } + return LocalTime.parse(t); + } catch (DateTimeParseException e) { + return null; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceScheduleDayParse.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceScheduleDayParse.java new file mode 100644 index 0000000..cae389c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AttendanceScheduleDayParse.java @@ -0,0 +1,96 @@ +package jnpf.attendance.schedule; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.format.SignStyle; +import java.time.temporal.ChronoField; +import java.util.Optional; + +/** + * 历史排班/营业额按日 VO 中 {@code day} 字段静默归一为 ISO {@code yyyy-MM-dd}, + * 与 {@link LocalDate#toString()} 一致以便与相似日的 {@code LocalDate} 对齐。 + * + *

支持:yyyy-MM-dd、{@code yyyy/M/d} 变体、{@code yyyyMMdd}(8 位数字)。 + */ +public final class AttendanceScheduleDayParse { + + private AttendanceScheduleDayParse() {} + + private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_LOCAL_DATE; + + /** 支持 2026-1-05、2026/01/05 等非补零写法 */ + private static final DateTimeFormatter SLASH_RELAXED = + new DateTimeFormatterBuilder() + .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('/') + .appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NOT_NEGATIVE) + .appendLiteral('/') + .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE) + .toFormatter(); + + private static final DateTimeFormatter COMPACT_BASIC = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + /** + * 若能解析日历日则返回 ISO 字符串;否则 empty(调用方仍可回退原始 key)。 + */ + public static Optional normalizeToIsoDayString(String raw) { + return tryParse(raw).map(LocalDate::toString); + } + + /** 静默解析为 {@link LocalDate}。 */ + public static Optional tryParse(String raw) { + if (raw == null) { + return Optional.empty(); + } + String s = raw.trim(); + if (s.isEmpty()) { + return Optional.empty(); + } + Optional iso = tryIso(s); + if (iso.isPresent()) { + return iso; + } + Optional slash = trySlash(s); + if (slash.isPresent()) { + return slash; + } + Optional compact = tryCompact(s); + if (compact.isPresent()) { + return compact; + } + return Optional.empty(); + } + + private static Optional tryIso(String s) { + try { + return Optional.of(LocalDate.parse(s, ISO)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + private static Optional trySlash(String s) { + try { + if (!s.contains("/")) { + return Optional.empty(); + } + return Optional.of(LocalDate.parse(s, SLASH_RELAXED)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } + + private static Optional tryCompact(String s) { + if (s.length() != 8 || !s.chars().allMatch(Character::isDigit)) { + return Optional.empty(); + } + try { + return Optional.of(LocalDate.parse(s, COMPACT_BASIC)); + } catch (DateTimeParseException e) { + return Optional.empty(); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AutoSchedulePipelineLog.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AutoSchedulePipelineLog.java new file mode 100644 index 0000000..58d0134 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/AutoSchedulePipelineLog.java @@ -0,0 +1,53 @@ +package jnpf.attendance.schedule; + +import org.slf4j.Logger; + +/** + * 自动排班主链路日志规范:与 {@link SchedulingForTestCheckLog} 一致,主链路里程碑用 {@link org.slf4j.Logger#error} 输出,便于线上仅开启 ERROR 时仍可检索。 + * + *

检索:日志统一带 {@link #MARKER},可 grep {@code AUTO_SCHEDULE}。 + */ +public final class AutoSchedulePipelineLog { + + /** 单行日志固定前缀(含方括号便于肉眼扫描)。 */ + public static final String MARKER = "[AUTO_SCHEDULE]"; + + /** 流程起点:考勤组与时间区间 */ + public static final String PHASE_START = "START"; + /** 历史营业额与日排班快照条数(90 天窗口内实际返回数量) */ + public static final String PHASE_HISTORY_SNAPSHOT = "HISTORY_SNAPSHOT"; + /** 第四步:相似模版日映射 */ + public static final String PHASE_SIMILAR_DAYS = "SIMILAR_DAYS"; + /** 历史半小时用工行(仅成功模版日聚合) */ + public static final String PHASE_HALF_HOUR_TABLE = "HALF_HOUR_TABLE"; + /** 第五段:半小时 × 岗位需求矩阵 */ + public static final String PHASE_DEMAND_MATRIX = "DEMAND_MATRIX"; + /** 第六~八步:固定班贪心 + 划线覆盖 */ + public static final String PHASE_COVER_678 = "COVER_678"; + /** 由覆盖结果合成的班次计划块(匹配考勤班次前) */ + public static final String PHASE_SHIFT_PLAN_DRAFT = "SHIFT_PLAN_DRAFT"; + /** 考勤组班次时段匹配后的计划(不匹配块已剔除) */ + public static final String PHASE_ATTEND_SHIFT_MATCH = "ATTEND_SHIFT_MATCH"; + /** 在岗人员池(工作站带人)规模 */ + public static final String PHASE_WORKSTATION_POOL = "WORKSTATION_POOL"; + /** 第九~十步人员规则门面类型 */ + public static final String PHASE_STAFF_RULES = "STAFF_RULES"; + /** 最终选人一行摘要(班次块数、人数、需求/已选);按班次/员工全文见 DEBUG。 */ + public static final String PHASE_PICK_SUMMARY = "PICK_SUMMARY"; + + private AutoSchedulePipelineLog() {} + + public static void info(Logger log, String phase, String detail) { + if (log == null || !log.isErrorEnabled()) { + return; + } + log.error("{} phase={} | {}", MARKER, phase, detail); + } + + public static void warn(Logger log, String phase, String detail) { + if (log == null || !log.isErrorEnabled()) { + return; + } + log.error("{} phase={} | {} | levelTag=WARN", MARKER, phase, detail); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscovery.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscovery.java new file mode 100644 index 0000000..7378d42 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscovery.java @@ -0,0 +1,219 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import jnpf.model.attendance.vo.ShiftPostHeadcountVo; +import jnpf.model.attendance.vo.ShiftPostStatVo; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +/** + * 第六步:从相似历史日的排班数据中按岗位识别常见固定班次(排除划线班),统计并筛选候选池。 + */ +public final class CommonFixedShiftDiscovery { + + private CommonFixedShiftDiscovery() {} + + /** + * @param similarDatesOrdered 第三步给出的相似历史日(顺序保留,用于占比分母) + * @param historyByDay key 为 {@link DayShiftRevenueStatVo#getDay()}(yyyy-MM-dd) + */ + public static Map> discoverCandidatesByPost( + List similarDatesOrdered, + Map historyByDay, + CommonFixedShiftDiscoveryConfig cfg) { + if (cfg == null) { + throw new IllegalArgumentException("cfg"); + } + List sims = similarDatesOrdered == null ? Collections.emptyList() : similarDatesOrdered; + int similarTotal = sims.size(); + Map> patternKeyToDayOcc = + new LinkedHashMap<>(); // post|sm|em -> day -> count that day + + for (LocalDate d : sims) { + if (d == null) { + continue; + } + DayShiftRevenueStatVo vo = historyByDay == null ? null : historyByDay.get(d.toString()); + if (vo == null || vo.getShifts() == null) { + continue; + } + for (ShiftPostStatVo shift : vo.getShifts()) { + if (shift == null || Boolean.TRUE.equals(shift.getLineSchedule())) { + continue; + } + List frags = splitToSameDayMinuteFragments(d, shift); + for (DayMinuteFragment f : frags) { + int[] norm = ShiftHalfHourNormalizer.normalizeMinuteRange(f.startMinuteInclusive, f.endMinuteExclusive); + if (norm == null) { + continue; + } + int sm = norm[0]; + int em = norm[1]; + int dur = em - sm; + if (dur < cfg.getLegalMinShiftMinutes() || dur > cfg.getLegalMaxShiftMinutes()) { + continue; + } + if (!cfg.isWindowWithinBusiness(sm, em)) { + continue; + } + for (ShiftPostHeadcountVo p : emptyIfNull(shift.getPosts())) { + if (p == null || p.getHeadcount() == null || p.getHeadcount() <= 0) { + continue; + } + String postId = p.getPostId() == null ? "" : p.getPostId(); + String shiftSid = + shift.getShiftId() == null || shift.getShiftId().isBlank() + ? "" + : shift.getShiftId().trim(); + String key = postId + "|" + sm + "|" + em + "|" + shiftSid; + patternKeyToDayOcc + .computeIfAbsent(key, k -> new LinkedHashMap<>()) + .merge(f.rowDate, 1, Integer::sum); + } + } + } + } + + List raw = new ArrayList<>(); + for (Map.Entry> e : patternKeyToDayOcc.entrySet()) { + String[] parsed = parsePatternKey(e.getKey()); + String postId = parsed[0]; + int sm = Integer.parseInt(parsed[1]); + int em = Integer.parseInt(parsed[2]); + String histRaw = parsed[3]; + String histShiftStored = histRaw == null || histRaw.isBlank() ? null : histRaw; + Map byDay = e.getValue(); + int dayCount = 0; + int occ = 0; + for (Integer c : byDay.values()) { + if (c != null && c > 0) { + dayCount++; + occ += c; + } + } + String label = ShiftHalfHourNormalizer.toLabel(sm, em); + raw.add( + new PostFixedShiftCandidate( + postId, + label, + sm, + em, + dayCount, + occ, + similarTotal, + histShiftStored)); + } + + List pool = new ArrayList<>(); + for (PostFixedShiftCandidate c : raw) { + if (passesInitialPool(c, cfg)) { + pool.add(c); + } + } + + List finalList = new ArrayList<>(); + for (PostFixedShiftCandidate c : pool) { + if (passesFinalTier(c, cfg)) { + finalList.add(c); + } + } + + finalList.sort( + Comparator.comparing(PostFixedShiftCandidate::getPostId) + .thenComparingInt(PostFixedShiftCandidate::getStartMinuteInclusive) + .thenComparingInt(PostFixedShiftCandidate::getEndMinuteExclusive)); + + Map> byPost = new TreeMap<>(); + for (PostFixedShiftCandidate c : finalList) { + byPost.computeIfAbsent(c.getPostId(), k -> new ArrayList<>()).add(c); + } + return Collections.unmodifiableMap(new LinkedHashMap<>(byPost)); + } + + private static boolean passesInitialPool(PostFixedShiftCandidate c, CommonFixedShiftDiscoveryConfig cfg) { + double ratio = c.getAppearanceDayRatio(); + return c.getAppearanceDayCount() >= cfg.getPoolMinAppearanceDays() + || c.getAppearanceCount() >= cfg.getPoolMinAppearanceCount() + || ratio + 1e-9 >= cfg.getPoolMinDayRatio(); + } + + private static boolean passesFinalTier(PostFixedShiftCandidate c, CommonFixedShiftDiscoveryConfig cfg) { + int n = c.getSimilarHistoryDayTotal(); + if (n >= cfg.getSampleLargeThresholdDays()) { + return c.getAppearanceDayRatio() + 1e-9 >= cfg.getFinalMinDayRatioWhenSampleLarge(); + } + if (n >= cfg.getSampleMediumLowDays() && n <= cfg.getSampleMediumHighDays()) { + return c.getAppearanceDayCount() >= cfg.getFinalMinDaysWhenSampleMedium(); + } + return true; + } + + private static String[] parsePatternKey(String key) { + String[] p = key.split("\\|", 4); + if (p.length < 3) { + return new String[] {"", "0", "0", ""}; + } + String sidPart = p.length >= 4 ? p[3] : ""; + return new String[] {p[0], p[1], p[2], sidPart}; + } + + private static List emptyIfNull(List posts) { + return posts == null ? Collections.emptyList() : posts; + } + + /** 将一条班次拆成自然日内的 [start,end) 分钟片段;跨午夜拆为两段。 */ + static List splitToSameDayMinuteFragments(LocalDate rowDate, ShiftPostStatVo shift) { + List out = new ArrayList<>(); + LocalTime s = parseHhMm(shift.getStartTime()); + LocalTime e = parseHhMm(shift.getEndTime()); + if (s == null || e == null) { + return out; + } + int sm = HalfHourSlotLabel.localTimeToMinute(s); + int em = HalfHourSlotLabel.localTimeToMinute(e); + if (e.isAfter(s)) { + out.add(new DayMinuteFragment(rowDate, sm, em)); + return out; + } + out.add(new DayMinuteFragment(rowDate, sm, 24 * 60)); + out.add(new DayMinuteFragment(rowDate.plusDays(1), 0, em)); + return out; + } + + private static LocalTime parseHhMm(String hhMm) { + if (hhMm == null) { + return null; + } + String t = hhMm.trim(); + if (t.isEmpty()) { + return null; + } + try { + return LocalTime.parse(t); + } catch (DateTimeParseException e) { + return null; + } + } + + static final class DayMinuteFragment { + final LocalDate rowDate; + final int startMinuteInclusive; + final int endMinuteExclusive; + + DayMinuteFragment(LocalDate rowDate, int startMinuteInclusive, int endMinuteExclusive) { + this.rowDate = Objects.requireNonNull(rowDate); + this.startMinuteInclusive = startMinuteInclusive; + this.endMinuteExclusive = endMinuteExclusive; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscoveryConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscoveryConfig.java new file mode 100644 index 0000000..33f43d3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CommonFixedShiftDiscoveryConfig.java @@ -0,0 +1,150 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalTime; +import java.util.Objects; + +/** + * 第六步:候选固定班次发现与过滤参数。 + */ +public final class CommonFixedShiftDiscoveryConfig implements Serializable { + + /** 进入候选池:出现天数 ≥ 该值 */ + private final int poolMinAppearanceDays; + /** 进入候选池:出现次数 ≥ 该值 */ + private final int poolMinAppearanceCount; + /** 进入候选池:出现天数占比 ≥ 该值(0~1) */ + private final double poolMinDayRatio; + + /** 合法班次最短时长(分钟),小于则剔除(如 09:00-09:30 共 30 分钟) */ + private final int legalMinShiftMinutes; + /** 合法班次最长时长(分钟) */ + private final int legalMaxShiftMinutes; + + /** 营业开始(含) */ + private final LocalTime businessOpen; + /** 营业结束(不含);若为 00:00 表示次日 0 点即 24:00 营业结束 */ + private final LocalTime businessCloseExclusive; + + /** 样本量 ≥ 该值时,最终筛选要求「出现天数占比 ≥ finalMinDayRatioWhenSampleLarge」 */ + private final int sampleLargeThresholdDays; + private final double finalMinDayRatioWhenSampleLarge; + + /** 样本量处于 [mediumLow, large) 时,最终筛选要求「出现天数 ≥ finalMinDaysWhenSampleMedium」 */ + private final int sampleMediumLowDays; + private final int sampleMediumHighDays; + private final int finalMinDaysWhenSampleMedium; + + public CommonFixedShiftDiscoveryConfig( + int poolMinAppearanceDays, + int poolMinAppearanceCount, + double poolMinDayRatio, + int legalMinShiftMinutes, + int legalMaxShiftMinutes, + LocalTime businessOpen, + LocalTime businessCloseExclusive, + int sampleLargeThresholdDays, + double finalMinDayRatioWhenSampleLarge, + int sampleMediumLowDays, + int sampleMediumHighDays, + int finalMinDaysWhenSampleMedium) { + if (poolMinAppearanceDays < 0 + || poolMinAppearanceCount < 0 + || poolMinDayRatio < 0 + || legalMinShiftMinutes < 0 + || legalMaxShiftMinutes < legalMinShiftMinutes) { + throw new IllegalArgumentException("invalid CommonFixedShiftDiscoveryConfig"); + } + this.poolMinAppearanceDays = poolMinAppearanceDays; + this.poolMinAppearanceCount = poolMinAppearanceCount; + this.poolMinDayRatio = poolMinDayRatio; + this.legalMinShiftMinutes = legalMinShiftMinutes; + this.legalMaxShiftMinutes = legalMaxShiftMinutes; + this.businessOpen = businessOpen == null ? LocalTime.MIDNIGHT : businessOpen; + this.businessCloseExclusive = + businessCloseExclusive == null ? LocalTime.MIDNIGHT : businessCloseExclusive; + this.sampleLargeThresholdDays = Math.max(1, sampleLargeThresholdDays); + this.finalMinDayRatioWhenSampleLarge = finalMinDayRatioWhenSampleLarge; + this.sampleMediumLowDays = Math.max(1, sampleMediumLowDays); + this.sampleMediumHighDays = Math.max(sampleMediumLowDays, sampleMediumHighDays); + this.finalMinDaysWhenSampleMedium = Math.max(1, finalMinDaysWhenSampleMedium); + } + + public static CommonFixedShiftDiscoveryConfig defaults() { + return new CommonFixedShiftDiscoveryConfig( + 2, + 3, + 0.30d, + 60, + 12 * 60, + LocalTime.MIDNIGHT, + LocalTime.MIDNIGHT, + 10, + 0.60d, + 5, + 9, + 2); + } + + public int getPoolMinAppearanceDays() { + return poolMinAppearanceDays; + } + + public int getPoolMinAppearanceCount() { + return poolMinAppearanceCount; + } + + public double getPoolMinDayRatio() { + return poolMinDayRatio; + } + + public int getLegalMinShiftMinutes() { + return legalMinShiftMinutes; + } + + public int getLegalMaxShiftMinutes() { + return legalMaxShiftMinutes; + } + + public LocalTime getBusinessOpen() { + return businessOpen; + } + + public LocalTime getBusinessCloseExclusive() { + return businessCloseExclusive; + } + + public int getSampleLargeThresholdDays() { + return sampleLargeThresholdDays; + } + + public double getFinalMinDayRatioWhenSampleLarge() { + return finalMinDayRatioWhenSampleLarge; + } + + public int getSampleMediumLowDays() { + return sampleMediumLowDays; + } + + public int getSampleMediumHighDays() { + return sampleMediumHighDays; + } + + public int getFinalMinDaysWhenSampleMedium() { + return finalMinDaysWhenSampleMedium; + } + + /** 营业窗口内 [open, close) 分钟;close 为 MIDNIGHT 表示全天 0~1440。 */ + public boolean isWindowWithinBusiness(int startMinuteInclusive, int endMinuteExclusive) { + int open = HalfHourSlotLabel.localTimeToMinute(businessOpen); + int close = HalfHourSlotLabel.localTimeToMinute(businessCloseExclusive); + if (businessCloseExclusive.equals(LocalTime.MIDNIGHT) && businessOpen.equals(LocalTime.MIDNIGHT)) { + return startMinuteInclusive >= 0 && endMinuteExclusive <= 24 * 60; + } + if (close > open) { + return startMinuteInclusive >= open && endMinuteExclusive <= close; + } + // 跨日营业简化为全天 + return startMinuteInclusive >= 0 && endMinuteExclusive <= 24 * 60; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CrossPostGeneralStaffAllocator.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CrossPostGeneralStaffAllocator.java new file mode 100644 index 0000000..ceeec7c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/CrossPostGeneralStaffAllocator.java @@ -0,0 +1,293 @@ +package jnpf.attendance.schedule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 四、专岗/通岗:本岗位专岗优先;不足时用通岗;通岗同时在多岗位候选池时优先补缺口更大的岗位(文档「马六」示例)。 + */ +public final class CrossPostGeneralStaffAllocator { + + private static final Logger LOG = LoggerFactory.getLogger(CrossPostGeneralStaffAllocator.class); + + private CrossPostGeneralStaffAllocator() {} + + /** + * @param demandByPost 各岗位本时段仍缺人数(≥0) + * @param specialistIdsByPost 专岗(本岗位绑定工作站) + * @param generalIdsByPost 通岗 + * @param random 用于均衡打散;可为 null + * @return 每岗位本轮分配到的员工 id(顺序无业务含义,每人至多出现一次) + */ + public static Map> allocateForOneTimeWindow( + Map demandByPost, + Map> specialistIdsByPost, + Map> generalIdsByPost, + Random random) { + if (demandByPost == null || demandByPost.isEmpty()) { + return Collections.emptyMap(); + } + Random rnd = random == null ? new Random() : random; + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "cross_post_allocate_input", + "algo=CrossPostGeneralStaffAllocator step=0_demand_and_pools | demandByPost=" + + demandByPost + + " | specialistPools=" + + stringifyIdSets(specialistIdsByPost) + + " | generalPools=" + + stringifyIdSets(generalIdsByPost)); + + Map remaining = new LinkedHashMap<>(); + for (Map.Entry e : demandByPost.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + remaining.put(e.getKey(), Math.max(0, e.getValue())); + } + + Map> assigned = new LinkedHashMap<>(); + for (String p : remaining.keySet()) { + assigned.put(p, new ArrayList<>()); + } + + Set used = new HashSet<>(); + Set specialistAnywhere = specialistIdsAnywhere(specialistIdsByPost); + + // 1) 专岗:「桥接」人员(本岗专岗且在其他岗通岗池)优先排在本岗,避免先被其他岗通岗占走(文档马六→服务员) + for (String post : remaining.keySet()) { + List specs = + shuffledBridgeFirst( + safeGet(specialistIdsByPost, post), + post, + generalIdsByPost, + rnd); + int need = remaining.getOrDefault(post, 0); + for (String s : specs) { + if (need <= 0 || used.contains(s)) { + continue; + } + assigned.get(post).add(s); + used.add(s); + need--; + } + remaining.put(post, need); + } + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "cross_post_after_phase1_specialists", + "algo=CrossPostGeneralStaffAllocator step=1_bridge_specialists_first | remainingDemand=" + + remaining + + " | partialAssigned=" + + assignedSnapshot(assigned)); + + // 2) 同时出现在 ≥2 个仍有缺口岗位的「人员池」(专岗 ∪ 通岗)→ 优先补缺口大者(马六类场景) + while (true) { + List postsWithNeed = new ArrayList<>(); + for (Map.Entry e : remaining.entrySet()) { + if (e.getValue() != null && e.getValue() > 0) { + postsWithNeed.add(e.getKey()); + } + } + if (postsWithNeed.isEmpty()) { + break; + } + Set sharedAcrossPosts = + findSharedAcrossPosts(specialistIdsByPost, generalIdsByPost, postsWithNeed, used); + if (sharedAcrossPosts.isEmpty()) { + break; + } + List sharedList = new ArrayList<>(sharedAcrossPosts); + Collections.shuffle(sharedList, rnd); + String g = sharedList.get(0); + String bestPost = + postsWithNeed.stream() + .max( + Comparator.comparingInt( + p -> remaining.getOrDefault(p, 0)) + .thenComparing(Comparator.naturalOrder())) + .orElse(null); + if (bestPost == null) { + break; + } + assigned.get(bestPost).add(g); + used.add(g); + remaining.put(bestPost, remaining.getOrDefault(bestPost, 0) - 1); + } + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "cross_post_after_phase2_shared_general", + "algo=CrossPostGeneralStaffAllocator step=2_multi_post_shared_pool_max_gap_first | remainingDemand=" + + remaining + + " | assignedSoFar=" + + assignedSnapshot(assigned)); + + // 3) 剩余通岗(只服务单岗位或共享已耗尽) + for (String post : new ArrayList<>(remaining.keySet())) { + int need = remaining.getOrDefault(post, 0); + if (need <= 0) { + continue; + } + List gens = shuffledCopy(safeGet(generalIdsByPost, post), rnd); + for (String g : gens) { + if (need <= 0 || used.contains(g)) { + continue; + } + // 专岗身份不跨岗「做通岗」:避免把马六从服务员专岗挪去收银通岗 + if (specialistAnywhere.contains(g) && !safeGet(specialistIdsByPost, post).contains(g)) { + continue; + } + assigned.get(post).add(g); + used.add(g); + need--; + } + remaining.put(post, need); + } + + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "cross_post_after_phase3_residual_generals", + "algo=CrossPostGeneralStaffAllocator step=3_residual_generals | remainingDemand=" + + remaining + + " | assigned=" + + assignedSnapshot(assigned)); + + Map> out = new LinkedHashMap<>(); + for (Map.Entry> e : assigned.entrySet()) { + out.put(e.getKey(), Collections.unmodifiableList(e.getValue())); + } + return Collections.unmodifiableMap(out); + } + + private static Map> assignedSnapshot(Map> assigned) { + Map> snap = new LinkedHashMap<>(); + if (assigned == null) { + return snap; + } + for (Map.Entry> e : assigned.entrySet()) { + snap.put(e.getKey(), e.getValue() == null ? Collections.emptyList() : new ArrayList<>(e.getValue())); + } + return snap; + } + + private static String stringifyIdSets(Map> byPost) { + if (byPost == null || byPost.isEmpty()) { + return "{}"; + } + return byPost.toString(); + } + + private static Set findSharedAcrossPosts( + Map> specialistIdsByPost, + Map> generalIdsByPost, + List postsWithNeed, + Set used) { + Map freq = new HashMap<>(); + for (String p : postsWithNeed) { + Set pool = new LinkedHashSet<>(safeGet(specialistIdsByPost, p)); + pool.addAll(safeGet(generalIdsByPost, p)); + for (String g : pool) { + if (used.contains(g)) { + continue; + } + freq.merge(g, 1, Integer::sum); + } + } + Set shared = new LinkedHashSet<>(); + for (Map.Entry e : freq.entrySet()) { + if (e.getValue() != null && e.getValue() >= 2) { + shared.add(e.getKey()); + } + } + return shared; + } + + private static Set safeGet(Map> map, String key) { + if (map == null || key == null) { + return Collections.emptySet(); + } + Set s = map.get(key); + return s == null ? Collections.emptySet() : s; + } + + private static List shuffledCopy(Set ids, Random rnd) { + List list = new ArrayList<>(ids); + Collections.shuffle(list, rnd); + return list; + } + + private static Set specialistIdsAnywhere(Map> specialistIdsByPost) { + Set out = new HashSet<>(); + if (specialistIdsByPost == null) { + return out; + } + for (Set s : specialistIdsByPost.values()) { + if (s != null) { + out.addAll(s); + } + } + return out; + } + + /** + * 在本岗专岗列表中,把「同时出现在其他岗通岗池」的人排在前面(同层内再打散)。 + */ + private static List shuffledBridgeFirst( + Set specialistIds, + String post, + Map> generalIdsByPost, + Random rnd) { + List bridges = new ArrayList<>(); + List others = new ArrayList<>(); + for (String id : specialistIds) { + if (id == null) { + continue; + } + if (isBridgeSpecialist(id, post, generalIdsByPost)) { + bridges.add(id); + } else { + others.add(id); + } + } + Collections.shuffle(bridges, rnd); + Collections.shuffle(others, rnd); + List out = new ArrayList<>(bridges.size() + others.size()); + out.addAll(bridges); + out.addAll(others); + return out; + } + + private static boolean isBridgeSpecialist( + String employeeId, String homePost, Map> generalIdsByPost) { + if (generalIdsByPost == null || homePost == null) { + return false; + } + for (Map.Entry> e : generalIdsByPost.entrySet()) { + String other = e.getKey(); + if (other == null || other.equals(homePost)) { + continue; + } + Set gen = e.getValue(); + if (gen != null && gen.contains(employeeId)) { + return true; + } + } + return false; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterFromCoverBuilder.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterFromCoverBuilder.java new file mode 100644 index 0000000..b52e996 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterFromCoverBuilder.java @@ -0,0 +1,183 @@ +package jnpf.attendance.schedule; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 根据第六~八步 {@link ScheduleDemandCoverResult}(固定班贪心 picks、划线段)与 + * {@link WorkstationPostStaffBuckets} 生成「日期 + 班次 + 人员」列表,便于测试/报表核对。 + * + *

分配规则(刻意简单、确定性):按当日时段起点排序后依次落人;专岗优先于通岗;同一用户同一日不允许时段重叠。 + * 若该岗池内无人可用则使用 {@code UNASSIGNED|<postId>}。 + */ +public final class FinalScheduleRosterFromCoverBuilder { + + private static final String UNASSIGNED_PREFIX = "UNASSIGNED|"; + + private FinalScheduleRosterFromCoverBuilder() {} + + public static List build( + Map coverByDay, + Map matrices, + WorkstationPostStaffBuckets buckets) { + if (coverByDay == null || coverByDay.isEmpty()) { + return Collections.emptyList(); + } + List days = new ArrayList<>(coverByDay.keySet()); + Collections.sort(days); + List out = new ArrayList<>(); + for (LocalDate day : days) { + ScheduleDemandCoverResult cov = coverByDay.get(day); + HalfHourPostDemandMatrix matrix = matrices == null ? null : matrices.get(day); + if (cov == null || matrix == null) { + continue; + } + out.addAll(buildForOneDay(day, cov, matrix, buckets)); + } + return Collections.unmodifiableList(out); + } + + private static List buildForOneDay( + LocalDate day, + ScheduleDemandCoverResult cov, + HalfHourPostDemandMatrix matrix, + WorkstationPostStaffBuckets buckets) { + List pending = new ArrayList<>(); + List posts = matrix.getPostIdsOrdered(); + List slots = matrix.getSlotRangeLabelsOrdered(); + for (String postId : posts) { + FixedShiftCoverOutcome fo = cov.getFixedShiftCoverByPost().get(postId); + if (fo != null) { + for (PostFixedShiftCandidate c : fo.getPicksInOrder()) { + if (c == null) { + continue; + } + pending.add( + new PendingShift( + postId, + FinalScheduleRosterLine.KIND_FIXED, + c.getNormalizedShiftLabel(), + c.getStartMinuteInclusive(), + c.getEndMinuteExclusive())); + } + } + List linePicks = cov.getLineShiftPicksByPost().get(postId); + if (linePicks != null) { + for (LineShiftPick lp : linePicks) { + if (lp == null) { + continue; + } + int[] range = linePickToMinuteRange(slots, lp); + String slotLabel = linePickSlotLabel(slots, lp); + pending.add( + new PendingShift( + postId, + FinalScheduleRosterLine.KIND_LINE, + slotLabel, + range[0], + range[1])); + } + } + } + pending.sort(PENDING_ORDER); + + Map> busyByUser = new HashMap<>(); + List lines = new ArrayList<>(pending.size()); + for (PendingShift p : pending) { + String uid = pickEmployee(p.postId, p.start, p.end, buckets, busyByUser); + lines.add( + new FinalScheduleRosterLine( + day, p.postId, p.kind, p.shiftLabel, p.start, p.end, uid)); + } + return lines; + } + + private static final Comparator PENDING_ORDER = + Comparator.comparingInt((PendingShift p) -> p.start) + .thenComparingInt((PendingShift p) -> -p.end) + .thenComparing(p -> p.kind.equals(FinalScheduleRosterLine.KIND_FIXED) ? 0 : 1) + .thenComparing(p -> p.postId); + + private static int[] linePickToMinuteRange(List slots, LineShiftPick lp) { + int i = lp.getStartSlotIndexInclusive(); + int j = lp.getEndSlotIndexExclusive(); + if (slots == null || slots.isEmpty() || i < 0 || j <= i || i >= slots.size()) { + return new int[] {0, 0}; + } + int last = Math.min(j - 1, slots.size() - 1); + int[] a = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(i)); + int[] b = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(last)); + return new int[] {a[0], b[1]}; + } + + private static String linePickSlotLabel(List slots, LineShiftPick lp) { + int i = lp.getStartSlotIndexInclusive(); + int j = lp.getEndSlotIndexExclusive(); + if (slots == null || slots.isEmpty() || i < 0 || j <= i || i >= slots.size()) { + return "LINE[" + i + "," + j + ")"; + } + int last = Math.min(j - 1, slots.size() - 1); + return slots.get(i) + "~" + slots.get(last); + } + + private static String pickEmployee( + String postId, + int startInclusive, + int endExclusive, + WorkstationPostStaffBuckets buckets, + Map> busyByUser) { + Objects.requireNonNull(busyByUser, "busyByUser"); + List pool = new ArrayList<>(); + if (buckets != null) { + pool.addAll(buckets.getSpecialistIds(postId)); + pool.addAll(buckets.getGeneralIds(postId)); + } + for (String uid : pool) { + if (uid == null || uid.isEmpty()) { + continue; + } + if (!overlapsExisting(busyByUser.get(uid), startInclusive, endExclusive)) { + busyByUser.computeIfAbsent(uid, k -> new ArrayList<>()).add(new int[] {startInclusive, endExclusive}); + return uid; + } + } + return UNASSIGNED_PREFIX + postId; + } + + private static boolean overlapsExisting(List intervals, int startInclusive, int endExclusive) { + if (intervals == null || intervals.isEmpty()) { + return false; + } + for (int[] iv : intervals) { + if (iv == null || iv.length < 2) { + continue; + } + if (startInclusive < iv[1] && iv[0] < endExclusive) { + return true; + } + } + return false; + } + + private static final class PendingShift { + final String postId; + final String kind; + final String shiftLabel; + final int start; + final int end; + + PendingShift(String postId, String kind, String shiftLabel, int start, int end) { + this.postId = postId == null ? "" : postId; + this.kind = kind; + this.shiftLabel = shiftLabel == null ? "" : shiftLabel; + this.start = start; + this.end = end; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterLine.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterLine.java new file mode 100644 index 0000000..56f80e5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FinalScheduleRosterLine.java @@ -0,0 +1,88 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; + +/** + * 由需求覆盖结果(固定班贪心 + 划线兜底)与人员池推导出的「可核对」排班行:日期、班次标签、人员。 + * + *

说明:此为在 {@link ScheduleDemandCoverResult} 之上的派生视图,用于报表/测试对账; + * 人员分配采用专岗优先、再通岗、同日时段不重叠的贪心规则,与生产第九/十步完整规则可能不同。 + */ +public final class FinalScheduleRosterLine implements Serializable { + + public static final String KIND_FIXED = "FIXED"; + public static final String KIND_LINE = "LINE"; + + private final LocalDate scheduleDay; + private final String postId; + private final String kind; + /** 展示用班次标签(固定班为归一化区间;划线为时段标签或分钟区间格式化)。 */ + private final String shiftLabel; + private final int startMinuteInclusive; + private final int endMinuteExclusive; + private final String employeeUserId; + + public FinalScheduleRosterLine( + LocalDate scheduleDay, + String postId, + String kind, + String shiftLabel, + int startMinuteInclusive, + int endMinuteExclusive, + String employeeUserId) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.postId = postId == null ? "" : postId; + this.kind = kind == null ? "" : kind; + this.shiftLabel = shiftLabel == null ? "" : shiftLabel; + this.startMinuteInclusive = startMinuteInclusive; + this.endMinuteExclusive = endMinuteExclusive; + this.employeeUserId = employeeUserId == null ? "" : employeeUserId; + } + + public LocalDate getScheduleDay() { + return scheduleDay; + } + + public String getPostId() { + return postId; + } + + public String getKind() { + return kind; + } + + public String getShiftLabel() { + return shiftLabel; + } + + public int getStartMinuteInclusive() { + return startMinuteInclusive; + } + + public int getEndMinuteExclusive() { + return endMinuteExclusive; + } + + public String getEmployeeUserId() { + return employeeUserId; + } + + /** 与 {@link HalfHourSlotLabel#formatMinuteRange} 一致的可读时段。 */ + public String getTimeRangeLabel() { + return HalfHourSlotLabel.formatMinuteRange(startMinuteInclusive, endMinuteExclusive); + } + + @Override + public String toString() { + return "FinalScheduleRosterLine{" + + "day=" + scheduleDay + + ", post=" + postId + + ", kind=" + kind + + ", shift=" + shiftLabel + + ", time=" + getTimeRangeLabel() + + ", user=" + employeeUserId + + '}'; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftCoverOutcome.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftCoverOutcome.java new file mode 100644 index 0000000..579825b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftCoverOutcome.java @@ -0,0 +1,58 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** 第七步:某岗位固定班次贪心覆盖结果。 */ +public final class FixedShiftCoverOutcome implements Serializable { + + private final String postId; + private final List picksInOrder; + private final List slotLabelsOrdered; + private final int[] coveredPerSlot; + private final int[] targetPerSlot; + + public FixedShiftCoverOutcome( + String postId, + List picksInOrder, + int[] coveredPerSlot, + int[] targetPerSlot, + List slotLabelsOrdered) { + this.postId = postId == null ? "" : postId; + this.picksInOrder = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(picksInOrder))); + this.slotLabelsOrdered = + Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(slotLabelsOrdered))); + this.coveredPerSlot = coveredPerSlot.clone(); + this.targetPerSlot = targetPerSlot.clone(); + } + + public String getPostId() { + return postId; + } + + public List getPicksInOrder() { + return picksInOrder; + } + + public List getSlotLabelsOrdered() { + return slotLabelsOrdered; + } + + public int[] getCoveredPerSlot() { + return coveredPerSlot.clone(); + } + + public int[] getTargetPerSlot() { + return targetPerSlot.clone(); + } + + public int remainingNeedAt(int slotIndex) { + if (slotIndex < 0 || slotIndex >= targetPerSlot.length) { + return 0; + } + return Math.max(0, targetPerSlot[slotIndex] - coveredPerSlot[slotIndex]); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftGreedyCoverPlanner.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftGreedyCoverPlanner.java new file mode 100644 index 0000000..d73fa6c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedShiftGreedyCoverPlanner.java @@ -0,0 +1,140 @@ +package jnpf.attendance.schedule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * 第七步:按岗位用候选固定班次贪心覆盖半小时需求矩阵(无权重打分,纯规则序)。 + */ +public final class FixedShiftGreedyCoverPlanner { + + private FixedShiftGreedyCoverPlanner() {} + + public static FixedShiftCoverOutcome planForPost( + HalfHourPostDemandMatrix matrix, + String postId, + List candidates, + int maxIterations) { + Objects.requireNonNull(matrix, "matrix"); + if (postId == null) { + postId = ""; + } + List slots = matrix.getSlotRangeLabelsOrdered(); + int n = slots.size(); + int[] target = new int[n]; + for (int i = 0; i < n; i++) { + target[i] = matrix.getTarget(slots.get(i), postId); + } + int[] covered = new int[n]; + List candList = candidates == null ? Collections.emptyList() : candidates; + + List wraps = new ArrayList<>(); + for (PostFixedShiftCandidate c : candList) { + if (c == null) { + continue; + } + List ix = + slotIndicesOverlapping(matrix, c.getStartMinuteInclusive(), c.getEndMinuteExclusive()); + if (!ix.isEmpty()) { + wraps.add(new CandidateSlotIndices(c, ix)); + } + } + + List picks = new ArrayList<>(); + int guard = 0; + while (guard++ < maxIterations) { + Comparator cmp = comparator(covered, target); + CandidateSlotIndices best = null; + for (CandidateSlotIndices w : wraps) { + if (gapHalfHours(covered, target, w.indices) <= 0) { + continue; + } + if (best == null || cmp.compare(w, best) < 0) { + best = w; + } + } + if (best == null) { + break; + } + for (int i : best.indices) { + covered[i]++; + } + picks.add(best.candidate); + } + + return new FixedShiftCoverOutcome(postId, picks, covered, target, slots); + } + + static int gapHalfHours(int[] covered, int[] target, List indices) { + int g = 0; + for (int i : indices) { + if (covered[i] < target[i]) { + g++; + } + } + return g; + } + + static int redundantHalfHours(int[] covered, int[] target, List indices) { + int r = 0; + for (int i : indices) { + if (covered[i] >= target[i]) { + r++; + } + } + return r; + } + + static List slotIndicesOverlapping( + HalfHourPostDemandMatrix matrix, int windowStartMinuteInclusive, int windowEndMinuteExclusive) { + List slots = matrix.getSlotRangeLabelsOrdered(); + List ix = new ArrayList<>(); + for (int i = 0; i < slots.size(); i++) { + int[] se = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(i)); + if (HalfHourSlotLabel.overlaps(se[0], se[1], windowStartMinuteInclusive, windowEndMinuteExclusive)) { + ix.add(i); + } + } + return ix; + } + + private static final class CandidateSlotIndices { + final PostFixedShiftCandidate candidate; + final List indices; + + CandidateSlotIndices(PostFixedShiftCandidate candidate, List indices) { + this.candidate = candidate; + this.indices = indices; + } + } + + static Comparator comparator(int[] covered, int[] target) { + return (a, b) -> { + int ga = gapHalfHours(covered, target, a.indices); + int gb = gapHalfHours(covered, target, b.indices); + if (ga != gb) { + return Integer.compare(gb, ga); + } + int ra = redundantHalfHours(covered, target, a.indices); + int rb = redundantHalfHours(covered, target, b.indices); + if (ra != rb) { + return Integer.compare(ra, rb); + } + int da = a.candidate.getAppearanceDayCount(); + int db = b.candidate.getAppearanceDayCount(); + if (da != db) { + return Integer.compare(db, da); + } + int la = a.candidate.getEndMinuteExclusive() - a.candidate.getStartMinuteInclusive(); + int lb = b.candidate.getEndMinuteExclusive() - b.candidate.getStartMinuteInclusive(); + if (la != lb) { + return Integer.compare(la, lb); + } + return Integer.compare( + a.candidate.getStartMinuteInclusive(), b.candidate.getStartMinuteInclusive()); + }; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftConstraintRelaxationPlanner.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftConstraintRelaxationPlanner.java new file mode 100644 index 0000000..7fc57cb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftConstraintRelaxationPlanner.java @@ -0,0 +1,245 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 第十步核心:固定排班规则下的「必须 / 尽量」分级与按序放松。 + *

+ * 被 {@link ScheduleStaffAssignmentSteps910#pickForPostDemand} 与 + * {@link ShiftPlanFinalStaffAssigner}(内联等价逻辑)调用。 + *

+ * 划线排班不使用本类(调用方传 {@code fixedRules=null} 时 {@link #applyFilters} 仅过第九步硬约束)。 + *

+ * 放松顺序(仅 priority=尽量 的已启用项):见 {@link FixedSoftRuleKind#relaxOrder()}。 + */ +public final class FixedSoftConstraintRelaxationPlanner { + + private static final Logger LOG = LoggerFactory.getLogger(FixedSoftConstraintRelaxationPlanner.class); + + private FixedSoftConstraintRelaxationPlanner() {} + + /** + * 在固定排班规则下筛选并最多返回 {@code targetCount} 人。 + *

+ * 步骤 + *

    + *
  1. 若存在固定规则且候选非空:{@link #filterHardAndMustPriorityRulesOnly} — + * 无人通过「硬约束 + 全部必须项」则直接返回空(本班次不排,放松尽量亦无效);
  2. + *
  3. 循环:{@link #applyFilters} 得到当前放松集合下的合格池; + * 若 {@code pool.size() >= targetCount} → {@link #shufflePick} 取目标人数;
  4. + *
  5. 人数仍不足 → {@link #nextRelaxableKind} 放开下一档「尽量」规则,加入 {@code relaxed} 后重试;
  6. + *
  7. 无可再放规则 → 对当前池 {@link #shufflePick} 取 min(targetCount, pool.size())(有多少排多少)。
  8. + *
+ * + * @param targetCount 目标人数上限 + * @param candidateEmployeeIds 候选员工 ID(顺序会被 shuffle,不保证专岗优先) + * @param fixedRules 固定排班规则 VO;null 表示仅应用第九步硬约束 + * @param port 规则判定端口 + * @param context 本段班次上下文(Assigner 场景下为「代表」上下文;每人单独判断见 Assigner 内联逻辑) + * @param random 抽样打乱用 + */ + public static List pickWithRelaxation( + int targetCount, + List candidateEmployeeIds, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context, + Random random) { + Objects.requireNonNull(port, "port"); + Objects.requireNonNull(context, "context"); + if (targetCount <= 0) { + return Collections.emptyList(); + } + Random rnd = random == null ? new Random() : random; + List ids = candidateEmployeeIds == null ? Collections.emptyList() : candidateEmployeeIds; + + // 前置:必须规则下是否至少有一人可选 + if (fixedRules != null && !ids.isEmpty()) { + List afterMust = filterHardAndMustPriorityRulesOnly(ids, fixedRules, port, context); + if (afterMust.isEmpty()) { + return Collections.emptyList(); + } + } + + Set relaxed = new LinkedHashSet<>(); + while (true) { + List pool = applyFilters(ids, fixedRules, port, context, relaxed); + if (pool.size() >= targetCount) { + return shufflePick(pool, targetCount, rnd); + } + FixedSoftRuleKind next = nextRelaxableKind(fixedRules, relaxed); + if (next == null) { + return shufflePick(pool, Math.min(targetCount, pool.size()), rnd); + } + relaxed.add(next); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "staff_assign_soft_constraint_relax_planner", + String.format( + "algo=FixedSoftConstraintRelaxationPlanner.pickWithRelaxation | scheduleDay=%s | postId=%s | groupId=%s | slotLabel=%s " + + "| targetCount=%d | poolAfterFilters=%d | newlyRelaxedSoftRule=%s | relaxedAccumulated=%s", + context.getScheduleDay(), + context.getPostId(), + context.getGroupId(), + context.getSlotRangeLabel(), + targetCount, + pool.size(), + next.name(), + String.valueOf(relaxed))); + } + } + + /** + * 仅保留通过「第九步硬约束 + 全部启用的必须级固定规则」的候选人。 + *

+ * 用于判断:若结果为空,则禁止通过放松「尽量」规则增加人选。 + */ + private static List filterHardAndMustPriorityRulesOnly( + List candidateEmployeeIds, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context) { + List out = new ArrayList<>(); + for (String id : candidateEmployeeIds) { + if (id == null || id.trim().isEmpty()) { + continue; + } + if (!port.passesOperationalHardConstraints(id, context)) { + continue; + } + if (!passesMustPriorityFixedRulesOnly(id, fixedRules, port, context)) { + continue; + } + out.add(id); + } + return out; + } + + /** 该员工是否满足全部「已启用且 priority=必须」的 {@link FixedSoftRuleKind}。 */ + private static boolean passesMustPriorityFixedRulesOnly( + String employeeId, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context) { + if (fixedRules == null) { + return true; + } + for (FixedSoftRuleKind kind : FixedSoftRuleKind.relaxOrder()) { + if (!kind.isEnabled(fixedRules)) { + continue; + } + if (!kind.isMustPriority(fixedRules)) { + continue; + } + if (kind.violates(employeeId, context, port)) { + return false; + } + } + return true; + } + + /** + * 从合格池中随机抽取至多 {@code n} 人(打乱后取前 n 个)。 + */ + private static List shufflePick(List pool, int n, Random rnd) { + if (pool.isEmpty() || n <= 0) { + return Collections.emptyList(); + } + List copy = new ArrayList<>(pool); + Collections.shuffle(copy, rnd); + return Collections.unmodifiableList(new ArrayList<>(copy.subList(0, Math.min(n, copy.size())))); + } + + /** + * 在当前放松集合 {@code relaxed} 下,过滤出通过硬约束 + 固定规则(必须 + 未放松的尽量)的候选人。 + */ + private static List applyFilters( + List candidateEmployeeIds, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context, + Set relaxed) { + List out = new ArrayList<>(); + for (String id : candidateEmployeeIds) { + if (id == null || id.trim().isEmpty()) { + continue; + } + if (!port.passesOperationalHardConstraints(id, context)) { + continue; + } + if (!passesFixedSchedulingRules(id, fixedRules, port, context, relaxed)) { + continue; + } + out.add(id); + } + return out; + } + + /** + * 固定排班规则综合判定:必须项始终检查;尽量项在未列入 {@code relaxed} 时检查。 + */ + private static boolean passesFixedSchedulingRules( + String employeeId, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context, + Set relaxed) { + if (fixedRules == null) { + return true; + } + for (FixedSoftRuleKind kind : FixedSoftRuleKind.relaxOrder()) { + if (!kind.isEnabled(fixedRules)) { + continue; + } + boolean must = kind.isMustPriority(fixedRules); + if (must) { + if (kind.violates(employeeId, context, port)) { + return false; + } + continue; + } + if (relaxed.contains(kind)) { + continue; + } + if (kind.violates(employeeId, context, port)) { + return false; + } + } + return true; + } + + /** + * 下一档可放松的「尽量」规则(已启用、非必须、尚未在 {@code relaxed} 中)。 + */ + private static FixedSoftRuleKind nextRelaxableKind( + FixedSchedulingRuleVo fixedRules, Set relaxed) { + if (fixedRules == null) { + return null; + } + for (FixedSoftRuleKind kind : FixedSoftRuleKind.relaxOrder()) { + if (!kind.isEnabled(fixedRules)) { + continue; + } + if (kind.isMustPriority(fixedRules)) { + continue; + } + if (relaxed.contains(kind)) { + continue; + } + return kind; + } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftRuleKind.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftRuleKind.java new file mode 100644 index 0000000..cc612fb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/FixedSoftRuleKind.java @@ -0,0 +1,112 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 固定排班第十步的五个可配置维度,及「尽量」规则下的放松顺序。 + *

+ * 与 {@link FixedSchedulingRuleVo} 各 {@code *Enabled} / {@code *Value} / {@code *Priority} 字段一一对应。 + *

+ * 约束级别:{@code priority=1} 必须(不可放松);{@code priority=0} 尽量(可按 {@link #relaxOrder()} 逐条放开)。 + */ +public enum FixedSoftRuleKind { + /** 单日工作小时数上限 */ + DAILY_WORK_HOURS, + /** 连续工作天数上限 */ + CONSECUTIVE_WORK_DAYS, + /** 每周工作天数上限 */ + WEEKLY_WORK_DAYS, + /** 每周工作小时数上限 */ + WEEKLY_WORK_HOURS, + /** 相邻两班之间最小休息间隔(小时) */ + MIN_REST_BETWEEN_SHIFTS; + + /** + * 产品文档规定的「尽量」放松顺序;{@link FixedSoftConstraintRelaxationPlanner} 与 + * {@link ShiftPlanFinalStaffAssigner} 均按此顺序逐条加入 {@code relaxed} 集合。 + */ + public static List relaxOrder() { + return Collections.unmodifiableList( + Arrays.asList( + DAILY_WORK_HOURS, + CONSECUTIVE_WORK_DAYS, + WEEKLY_WORK_DAYS, + WEEKLY_WORK_HOURS, + MIN_REST_BETWEEN_SHIFTS)); + } + + /** 该维度在 VO 中是否启用({@code *Enabled == true})。 */ + public boolean isEnabled(FixedSchedulingRuleVo vo) { + if (vo == null) { + return false; + } + switch (this) { + case DAILY_WORK_HOURS: + return Boolean.TRUE.equals(vo.getDailyWorkHoursEnabled()); + case CONSECUTIVE_WORK_DAYS: + return Boolean.TRUE.equals(vo.getConsecutiveWorkDaysEnabled()); + case WEEKLY_WORK_DAYS: + return Boolean.TRUE.equals(vo.getWeeklyWorkDaysEnabled()); + case WEEKLY_WORK_HOURS: + return Boolean.TRUE.equals(vo.getWeeklyWorkHoursEnabled()); + case MIN_REST_BETWEEN_SHIFTS: + return Boolean.TRUE.equals(vo.getMinRestBetweenShiftsEnabled()); + default: + return false; + } + } + + /** + * 是否为「必须」级别({@code *Priority == 1})。 + *

+ * 必须项:违反则不可选,且不能通过放松其它尽量项来绕过;若全员违反必须项则本班次不排。 + */ + public boolean isMustPriority(FixedSchedulingRuleVo vo) { + if (vo == null) { + return false; + } + Byte p = priorityByte(vo); + return p != null && p.intValue() == 1; + } + + private Byte priorityByte(FixedSchedulingRuleVo vo) { + switch (this) { + case DAILY_WORK_HOURS: + return vo.getDailyWorkHoursPriority(); + case CONSECUTIVE_WORK_DAYS: + return vo.getConsecutiveWorkDaysPriority(); + case WEEKLY_WORK_DAYS: + return vo.getWeeklyWorkDaysPriority(); + case WEEKLY_WORK_HOURS: + return vo.getWeeklyWorkHoursPriority(); + case MIN_REST_BETWEEN_SHIFTS: + return vo.getMinRestBetweenShiftsPriority(); + default: + return null; + } + } + + /** + * 委托 {@link StaffRuleEvaluationPort} 对应方法判断是否违反本维度。 + */ + public boolean violates(String employeeId, StaffAssignmentContext ctx, StaffRuleEvaluationPort port) { + switch (this) { + case DAILY_WORK_HOURS: + return port.violatesDailyWorkHoursLimit(employeeId, ctx); + case CONSECUTIVE_WORK_DAYS: + return port.violatesConsecutiveWorkDaysLimit(employeeId, ctx); + case WEEKLY_WORK_DAYS: + return port.violatesWeeklyWorkDaysLimit(employeeId, ctx); + case WEEKLY_WORK_HOURS: + return port.violatesWeeklyWorkHoursLimit(employeeId, ctx); + case MIN_REST_BETWEEN_SHIFTS: + return port.violatesMinRestBetweenShifts(employeeId, ctx); + default: + return false; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourDemandBasedShiftPlanBuilder.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourDemandBasedShiftPlanBuilder.java new file mode 100644 index 0000000..7bdb67e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourDemandBasedShiftPlanBuilder.java @@ -0,0 +1,317 @@ +package jnpf.attendance.schedule; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 将 {@link HalfHourPostDemandMatrix}(半小时×岗位目标人数)整理为「完整班次」块列表,供后续排班/展示。 + * + *

整理规则(按岗位独立处理后再汇总) + *

    + *
  • 若该岗位在当日有需求的时段内人数始终不变:生成一段固定排班,覆盖从首段有需求到末段有需求的整段连续时间, + * 人数为该常数。
  • + *
  • 若人数中途有变化:在「有需求」的索引区间内取最小需求人数 {@code minNeed}, + * 再取所有 {@code target==minNeed} 的最长连续半小时段(若有并列取最早的一段)作为固定排班,人数为 {@code minNeed};
  • + *
  • 其余时段的缺口用划线排班表达:固定段外侧用全额目标人数;固定段内侧用 {@code target-minNeed}。
  • + *
+ * + *

输出:{@code Map<排班日, List<班次块>>},每块含开始/结束分钟、是否固定、{@link ShiftPlanPostNeed} 列表(通常单岗位一条,合并后可能多岗位)。 + */ +public final class HalfHourDemandBasedShiftPlanBuilder { + + private static final Logger log = LoggerFactory.getLogger(HalfHourDemandBasedShiftPlanBuilder.class); + + private HalfHourDemandBasedShiftPlanBuilder() {} + + /** + * @param matrices key 为排班日,与 {@link SimilarDayDemandSteps4And5#buildPerScheduleDayDemandMatrices} 输出一致 + * @return 每个排班日按开始时间、再划线优先排序的班次块列表(已做同窗同类型合并) + */ + public static Map> build(Map matrices) { + if (matrices == null || matrices.isEmpty()) { + return Collections.emptyMap(); + } + List days = new ArrayList<>(); + for (LocalDate d : matrices.keySet()) { + if (d != null) { + days.add(d); + } + } + if (days.size() < matrices.size()) { + log.error( + "ignored {} null LocalDate key(s) in matrices map (total keys={})", + matrices.size() - days.size(), + matrices.size()); + } + Collections.sort(days); + Map> out = new LinkedHashMap<>(); + for (LocalDate day : days) { + HalfHourPostDemandMatrix m = matrices.get(day); + if (day == null || m == null) { + continue; + } + List raw = buildBlocksForOneDay(day, m); + List merged = mergeCoextensiveBlocks(raw); + merged.sort( + Comparator.comparingInt(ShiftPlanBlock::getStartMinuteInclusive) + .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive) + .thenComparing(b -> b.isFixedScheduling())); + out.put(day, Collections.unmodifiableList(merged)); + } + return Collections.unmodifiableMap(out); + } + + private static List buildBlocksForOneDay(LocalDate day, HalfHourPostDemandMatrix matrix) { + List all = new ArrayList<>(); + for (String postId : matrix.getPostIdsOrdered()) { + all.addAll(buildBlocksForPost(day, matrix, postId)); + } + return all; + } + + private static List buildBlocksForPost( + LocalDate day, HalfHourPostDemandMatrix matrix, String postId) { + List slots = matrix.getSlotRangeLabelsOrdered(); + int n = slots.size(); + if (n == 0) { + return Collections.emptyList(); + } + int[] t = new int[n]; + for (int i = 0; i < n; i++) { + t[i] = matrix.getTarget(slots.get(i), postId); + } + int first = -1; + int last = -1; + for (int i = 0; i < n; i++) { + if (t[i] > 0) { + first = i; + break; + } + } + for (int i = n - 1; i >= 0; i--) { + if (t[i] > 0) { + last = i; + break; + } + } + if (first < 0) { + return Collections.emptyList(); + } + + int minNeed = Integer.MAX_VALUE; + for (int i = first; i <= last; i++) { + if (t[i] > 0) { + minNeed = Math.min(minNeed, t[i]); + } + } + if (minNeed == Integer.MAX_VALUE) { + minNeed = 0; + } + if (minNeed <= 0) { + minNeed = minPositiveInRange(t, first, last); + } + if (minNeed <= 0) { + return Collections.emptyList(); + } + + boolean allConstant = true; + for (int i = first; i <= last; i++) { + if (t[i] > 0 && t[i] != minNeed) { + allConstant = false; + break; + } + } + + List out = new ArrayList<>(); + if (allConstant) { + int sm = slotStartMinute(slots, first); + int em = slotEndMinuteExclusive(slots, last); + if (em > sm) { + out.add( + new ShiftPlanBlock( + day, + sm, + em, + true, + Collections.singletonList(new ShiftPlanPostNeed(postId, minNeed)))); + } + return out; + } + + int[] longest = longestContiguousRunWhereEquals(t, first, last, minNeed); + int runStart = longest[0]; + int runEndExclusive = longest[1]; + if (runStart >= 0 && runEndExclusive > runStart) { + int sm = slotStartMinute(slots, runStart); + int em = slotEndMinuteExclusive(slots, runEndExclusive - 1); + if (em > sm && minNeed > 0) { + out.add( + new ShiftPlanBlock( + day, + sm, + em, + true, + Collections.singletonList(new ShiftPlanPostNeed(postId, minNeed)))); + } + } + + int[] lineNeed = new int[n]; + for (int i = first; i <= last; i++) { + if (i >= runStart && i < runEndExclusive) { + lineNeed[i] = Math.max(0, t[i] - minNeed); + } else { + lineNeed[i] = Math.max(0, t[i]); + } + } + + int i = first; + while (i <= last) { + if (lineNeed[i] <= 0) { + i++; + continue; + } + int v = lineNeed[i]; + int j = i; + while (j <= last && lineNeed[j] == v) { + j++; + } + int sm = slotStartMinute(slots, i); + int em = slotEndMinuteExclusive(slots, j - 1); + if (em > sm && v > 0) { + out.add( + new ShiftPlanBlock( + day, + sm, + em, + false, + Collections.singletonList(new ShiftPlanPostNeed(postId, v)))); + } + i = j; + } + return out; + } + + /** @return int[2] {runStartInclusive, runEndExclusive} */ + private static int[] longestContiguousRunWhereEquals(int[] t, int first, int last, int value) { + int bestStart = -1; + int bestLen = 0; + int a = first; + while (a <= last) { + if (t[a] != value) { + a++; + continue; + } + int b = a; + while (b <= last && t[b] == value) { + b++; + } + int len = b - a; + if (len > bestLen || (len == bestLen && (bestStart < 0 || a < bestStart))) { + bestLen = len; + bestStart = a; + } + a = b; + } + if (bestStart < 0) { + return new int[] {-1, -1}; + } + return new int[] {bestStart, bestStart + bestLen}; + } + + private static int minPositiveInRange(int[] t, int first, int last) { + int m = Integer.MAX_VALUE; + for (int i = first; i <= last; i++) { + if (t[i] > 0) { + m = Math.min(m, t[i]); + } + } + return m == Integer.MAX_VALUE ? 0 : m; + } + + private static int slotStartMinute(List slots, int slotIndex) { + int[] se = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(slotIndex)); + return se[0]; + } + + private static int slotEndMinuteExclusive(List slots, int slotIndexInclusive) { + int[] se = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(slotIndexInclusive)); + return se[1]; + } + + private static List mergeCoextensiveBlocks(List blocks) { + if (blocks == null || blocks.isEmpty()) { + return new ArrayList<>(); + } + List sorted = new ArrayList<>(); + for (ShiftPlanBlock b : blocks) { + if (b != null) { + sorted.add(b); + } + } + if (sorted.isEmpty()) { + return new ArrayList<>(); + } + sorted.sort( + Comparator.comparing((ShiftPlanBlock b) -> b.getScheduleDay()) + .thenComparingInt(ShiftPlanBlock::getStartMinuteInclusive) + .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive) + .thenComparing(ShiftPlanBlock::isFixedScheduling)); + List out = new ArrayList<>(); + ShiftPlanBlock cur = sorted.get(0); + for (int k = 1; k < sorted.size(); k++) { + ShiftPlanBlock next = sorted.get(k); + if (sameWindow(cur, next)) { + cur = mergePostNeeds(cur, next); + } else { + out.add(cur); + cur = next; + } + } + out.add(cur); + return out; + } + + private static boolean sameWindow(ShiftPlanBlock a, ShiftPlanBlock b) { + return a.getScheduleDay().equals(b.getScheduleDay()) + && a.getStartMinuteInclusive() == b.getStartMinuteInclusive() + && a.getEndMinuteExclusive() == b.getEndMinuteExclusive() + && a.isFixedScheduling() == b.isFixedScheduling(); + } + + private static ShiftPlanBlock mergePostNeeds(ShiftPlanBlock a, ShiftPlanBlock b) { + Map sum = new LinkedHashMap<>(); + for (ShiftPlanPostNeed n : a.getPostNeeds()) { + if (n == null || n.getPostId() == null || n.getPostId().trim().isEmpty()) { + continue; + } + sum.merge(n.getPostId(), n.getNeedCount(), Integer::sum); + } + for (ShiftPlanPostNeed n : b.getPostNeeds()) { + if (n == null || n.getPostId() == null || n.getPostId().trim().isEmpty()) { + continue; + } + sum.merge(n.getPostId(), n.getNeedCount(), Integer::sum); + } + List list = new ArrayList<>(); + for (Map.Entry e : sum.entrySet()) { + if (e.getValue() != null && e.getValue() > 0) { + list.add(new ShiftPlanPostNeed(e.getKey(), e.getValue())); + } + } + return new ShiftPlanBlock( + a.getScheduleDay(), + a.getStartMinuteInclusive(), + a.getEndMinuteExclusive(), + a.isFixedScheduling(), + list, + a.getShiftId()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourPostDemandMatrix.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourPostDemandMatrix.java new file mode 100644 index 0000000..087fe3e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourPostDemandMatrix.java @@ -0,0 +1,100 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 第五步:半小时时间段 × 岗位的目标人数矩阵(行=时间段,列=岗位)。 + */ +public final class HalfHourPostDemandMatrix implements Serializable { + + private final LocalDate scheduleDay; + private final List slotRangeLabelsOrdered; + private final List postIdsOrdered; + /** slot → (post → target) */ + private final Map> targetBySlotThenPost; + + public HalfHourPostDemandMatrix( + LocalDate scheduleDay, + List slotRangeLabelsOrdered, + List postIdsOrdered, + Map> targetBySlotThenPost) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.slotRangeLabelsOrdered = + Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(slotRangeLabelsOrdered))); + this.postIdsOrdered = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(postIdsOrdered))); + Map> copy = new LinkedHashMap<>(); + for (Map.Entry> e : targetBySlotThenPost.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + copy.put(e.getKey(), Collections.unmodifiableMap(new LinkedHashMap<>(e.getValue()))); + } + this.targetBySlotThenPost = Collections.unmodifiableMap(copy); + } + + public LocalDate getScheduleDay() { + return scheduleDay; + } + + /** 按时间顺序排列的时间段标签(与 {@link #getPostIdsOrdered()} 构成矩阵行列)。 */ + public List getSlotRangeLabelsOrdered() { + return slotRangeLabelsOrdered; + } + + /** 岗位 id 列顺序(表头)。 */ + public List getPostIdsOrdered() { + return postIdsOrdered; + } + + public int getTarget(String slotRangeLabel, String postId) { + if (slotRangeLabel == null || postId == null) { + return 0; + } + Map row = targetBySlotThenPost.get(slotRangeLabel); + if (row == null) { + return 0; + } + Integer v = row.get(postId); + return v == null ? 0 : v; + } + + public Map> getTargetBySlotThenPost() { + return targetBySlotThenPost; + } + + /** 便于日志:TSV 首行为 post 表头,后续每行 slot + 各岗位人数。 */ + public String toTsvTable() { + StringBuilder sb = new StringBuilder(); + sb.append("时间段"); + for (String p : postIdsOrdered) { + sb.append('\t').append(p); + } + sb.append('\n'); + for (String slot : slotRangeLabelsOrdered) { + sb.append(slot); + for (String p : postIdsOrdered) { + sb.append('\t').append(getTarget(slot, p)); + } + sb.append('\n'); + } + return sb.toString(); + } + + @Override + public String toString() { + return "HalfHourPostDemandMatrix{scheduleDay=" + + scheduleDay + + ", slots=" + + slotRangeLabelsOrdered.size() + + ", posts=" + + postIdsOrdered.size() + + '}'; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourSlotLabel.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourSlotLabel.java new file mode 100644 index 0000000..ea17a2b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/HalfHourSlotLabel.java @@ -0,0 +1,77 @@ +package jnpf.attendance.schedule; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * 半小时时间段标签解析(与 {@link SimilarHistoricalJobSlotRow#getSlotRangeLabel()} 约定一致)。 + */ +public final class HalfHourSlotLabel { + + private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm"); + + /** 展示用:小时不补零(如 {@code 9:00-14:00}),与 {@link #HM} 的语义、跨日尾段规则一致。 */ + private static final DateTimeFormatter HM_DISPLAY = DateTimeFormatter.ofPattern("H:mm"); + + private HalfHourSlotLabel() {} + + /** 左闭右开 [startInclusiveMinute, endExclusiveMinute),0~1440;尾段 {@code xx:xx-00:00} 视为跨到 24:00。 */ + public static int[] startEndExclusiveMinutes(String slotRangeLabel) { + if (slotRangeLabel == null) { + return new int[] {0, 0}; + } + String s = slotRangeLabel.trim(); + int dash = s.indexOf('-'); + if (dash < 0) { + return new int[] {0, 0}; + } + String a = s.substring(0, dash).trim(); + String b = s.substring(dash + 1).trim(); + try { + int sm = localTimeToMinute(LocalTime.parse(a, HM)); + int em = localTimeToMinute(LocalTime.parse(b, HM)); + if ("00:00".equals(b) && sm >= 23 * 60) { + em = 24 * 60; + } else if (em <= sm && em > 0) { + em += 24 * 60; + } + return new int[] {sm, em}; + } catch (DateTimeParseException e) { + return new int[] {0, 0}; + } + } + + public static int localTimeToMinute(LocalTime t) { + return t.getHour() * 60 + t.getMinute(); + } + + public static String formatMinuteRange(int startInclusiveMinute, int endExclusiveMinute) { + int sm = Math.max(0, Math.min(24 * 60, startInclusiveMinute)); + int em = Math.max(0, Math.min(24 * 60, endExclusiveMinute)); + LocalTime st = LocalTime.of(sm / 60, sm % 60); + if (em >= 24 * 60) { + return st.format(HM) + "-00:00"; + } + LocalTime en = LocalTime.of(em / 60, em % 60); + return st.format(HM) + "-" + en.format(HM); + } + + /** + * 与 {@link #formatMinuteRange} 相同的起止语义,展示为小时不补零的 {@code H:mm-H:mm}(如 {@code 9:00-14:00})。 + */ + public static String formatMinuteRangeDisplay(int startInclusiveMinute, int endExclusiveMinute) { + int sm = Math.max(0, Math.min(24 * 60, startInclusiveMinute)); + int em = Math.max(0, Math.min(24 * 60, endExclusiveMinute)); + LocalTime st = LocalTime.of(sm / 60, sm % 60); + if (em >= 24 * 60) { + return st.format(HM_DISPLAY) + "-" + LocalTime.MIDNIGHT.format(HM_DISPLAY); + } + LocalTime en = LocalTime.of(em / 60, em % 60); + return st.format(HM_DISPLAY) + "-" + en.format(HM_DISPLAY); + } + + public static boolean overlaps(int slotS, int slotE, int winS, int winE) { + return slotS < winE && winS < slotE; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftCoverConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftCoverConfig.java new file mode 100644 index 0000000..64b7f61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftCoverConfig.java @@ -0,0 +1,70 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.scheduling.LineSchedulingRuleVo; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 第八步:划线班次兜底参数(半小时为 1 格,2 小时=4 格)。 + */ +public final class LineShiftCoverConfig implements Serializable { + + private final int minHalfHourSlots; + private final int maxHalfHourSlots; + /** 单日每岗位最多输出的划线班段个数(算法侧上限)。 */ + private final int maxLineSegmentsPerPostPerDay; + + public LineShiftCoverConfig(int minHalfHourSlots, int maxHalfHourSlots, int maxLineSegmentsPerPostPerDay) { + if (minHalfHourSlots < 1 || maxHalfHourSlots < minHalfHourSlots || maxLineSegmentsPerPostPerDay < 1) { + throw new IllegalArgumentException("invalid LineShiftCoverConfig"); + } + this.minHalfHourSlots = minHalfHourSlots; + this.maxHalfHourSlots = maxHalfHourSlots; + this.maxLineSegmentsPerPostPerDay = maxLineSegmentsPerPostPerDay; + } + + public static LineShiftCoverConfig defaults() { + return new LineShiftCoverConfig(4, 8, 4); + } + + /** + * 从考勤组规则 VO 读取;未启用或值为 null 时使用默认 2h~4h、每岗每日最多 4 段。 + */ + public static LineShiftCoverConfig fromLineSchedulingRule(LineSchedulingRuleVo vo) { + int minSlots = 4; + int maxSlots = 8; + int maxSeg = 4; + if (vo != null) { + if (Boolean.TRUE.equals(vo.getMinSingleSegmentHoursEnabled()) + && vo.getMinSingleSegmentHoursValue() != null) { + minSlots = Math.max(1, hoursToHalfHourSlots(vo.getMinSingleSegmentHoursValue(), RoundingMode.CEILING)); + } + if (Boolean.TRUE.equals(vo.getMaxSingleSegmentHoursEnabled()) + && vo.getMaxSingleSegmentHoursValue() != null) { + maxSlots = Math.max(minSlots, vo.getMaxSingleSegmentHoursValue() * 2); + } + if (Boolean.TRUE.equals(vo.getMaxDailySegmentsEnabled()) && vo.getMaxDailySegmentsValue() != null) { + maxSeg = Math.max(1, vo.getMaxDailySegmentsValue()); + } + } + return new LineShiftCoverConfig(minSlots, maxSlots, maxSeg); + } + + private static int hoursToHalfHourSlots(BigDecimal hours, RoundingMode mode) { + return hours.multiply(BigDecimal.valueOf(2)).setScale(0, mode).intValue(); + } + + public int getMinHalfHourSlots() { + return minHalfHourSlots; + } + + public int getMaxHalfHourSlots() { + return maxHalfHourSlots; + } + + public int getMaxLineSegmentsPerPostPerDay() { + return maxLineSegmentsPerPostPerDay; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftGapPlanner.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftGapPlanner.java new file mode 100644 index 0000000..e833b99 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftGapPlanner.java @@ -0,0 +1,79 @@ +package jnpf.attendance.schedule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * 第八步:在连续「仍有缺口」的半小时段上,用划线班段兜底(受最短/最长/段数限制)。 + */ +public final class LineShiftGapPlanner { + + private LineShiftGapPlanner() {} + + /** + * @param remainingNeedPerSlot 与矩阵行同序;值 > 0 表示该半小时仍缺人数 + */ + public static List planForPost( + HalfHourPostDemandMatrix matrix, + String postId, + int[] remainingNeedPerSlot, + LineShiftCoverConfig cfg) { + Objects.requireNonNull(matrix, "matrix"); + Objects.requireNonNull(cfg, "cfg"); + if (postId == null) { + postId = ""; + } + List slots = matrix.getSlotRangeLabelsOrdered(); + int n = slots.size(); + int[] need = remainingNeedPerSlot == null ? new int[n] : remainingNeedPerSlot.clone(); + if (need.length != n) { + throw new IllegalArgumentException("remainingNeedPerSlot length must match matrix slots"); + } + + int min = cfg.getMinHalfHourSlots(); + int max = cfg.getMaxHalfHourSlots(); + int maxSeg = cfg.getMaxLineSegmentsPerPostPerDay(); + + List picks = new ArrayList<>(); + int segments = 0; + int i = 0; + while (i < n && segments < maxSeg) { + if (need[i] <= 0) { + i++; + continue; + } + int j = i; + while (j < n && need[j] > 0) { + j++; + } + int pos = i; + while (pos < j && segments < maxSeg) { + int rem = j - pos; + if (rem < min) { + break; + } + int len = Math.min(max, rem); + if (rem - len > 0 && rem - len < min) { + if (rem >= 2 * min) { + len = rem - min; + } else { + len = rem; + } + } + if (len < min) { + break; + } + picks.add(new LineShiftPick(postId, pos, pos + len)); + for (int k = pos; k < pos + len; k++) { + need[k] = Math.max(0, need[k] - 1); + } + pos += len; + segments++; + } + i = j; + } + return Collections.unmodifiableList(picks); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftPick.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftPick.java new file mode 100644 index 0000000..c8d8dd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/LineShiftPick.java @@ -0,0 +1,53 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.util.Objects; + +/** 第八步:一条划线班段(连续半小时格)。 */ +public final class LineShiftPick implements Serializable { + + private final String postId; + private final int startSlotIndexInclusive; + private final int endSlotIndexExclusive; + + public LineShiftPick(String postId, int startSlotIndexInclusive, int endSlotIndexExclusive) { + this.postId = postId == null ? "" : postId; + this.startSlotIndexInclusive = startSlotIndexInclusive; + this.endSlotIndexExclusive = endSlotIndexExclusive; + } + + public String getPostId() { + return postId; + } + + public int getStartSlotIndexInclusive() { + return startSlotIndexInclusive; + } + + public int getEndSlotIndexExclusive() { + return endSlotIndexExclusive; + } + + public int halfHourSlotCount() { + return Math.max(0, endSlotIndexExclusive - startSlotIndexInclusive); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LineShiftPick that = (LineShiftPick) o; + return startSlotIndexInclusive == that.startSlotIndexInclusive + && endSlotIndexExclusive == that.endSlotIndexExclusive + && Objects.equals(postId, that.postId); + } + + @Override + public int hashCode() { + return Objects.hash(postId, startSlotIndexInclusive, endSlotIndexExclusive); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/PostFixedShiftCandidate.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/PostFixedShiftCandidate.java new file mode 100644 index 0000000..c26e2be --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/PostFixedShiftCandidate.java @@ -0,0 +1,128 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 第六步:候选固定班次统计与展示字段。 + */ +public final class PostFixedShiftCandidate implements Serializable { + + private final String postId; + /** 半小时对齐后的展示标签,如 {@code 09:00-12:00} */ + private final String normalizedShiftLabel; + private final int startMinuteInclusive; + private final int endMinuteExclusive; + private final int appearanceDayCount; + private final int appearanceCount; + private final int similarHistoryDayTotal; + /** + * 历史班次在 {@link jnpf.model.attendance.vo.ShiftPostStatVo#getShiftId()} 上的快照(与考勤组 {@link jnpf.model.attendance.vo.ShiftPeriodVo#getId()} + * 同源时可走 ID 直配,跳过时段阈值匹配)。 + */ + private final String historicalAttendanceShiftId; + + public PostFixedShiftCandidate( + String postId, + String normalizedShiftLabel, + int startMinuteInclusive, + int endMinuteExclusive, + int appearanceDayCount, + int appearanceCount, + int similarHistoryDayTotal) { + this(postId, normalizedShiftLabel, startMinuteInclusive, endMinuteExclusive, appearanceDayCount, appearanceCount, + similarHistoryDayTotal, null); + } + + public PostFixedShiftCandidate( + String postId, + String normalizedShiftLabel, + int startMinuteInclusive, + int endMinuteExclusive, + int appearanceDayCount, + int appearanceCount, + int similarHistoryDayTotal, + String historicalAttendanceShiftId) { + this.postId = postId == null ? "" : postId; + this.normalizedShiftLabel = + normalizedShiftLabel == null ? "" : normalizedShiftLabel; + this.startMinuteInclusive = startMinuteInclusive; + this.endMinuteExclusive = endMinuteExclusive; + this.appearanceDayCount = Math.max(0, appearanceDayCount); + this.appearanceCount = Math.max(0, appearanceCount); + this.similarHistoryDayTotal = Math.max(0, similarHistoryDayTotal); + this.historicalAttendanceShiftId = + historicalAttendanceShiftId == null || historicalAttendanceShiftId.isBlank() + ? null + : historicalAttendanceShiftId.trim(); + } + + public String getPostId() { + return postId; + } + + public String getNormalizedShiftLabel() { + return normalizedShiftLabel; + } + + public int getStartMinuteInclusive() { + return startMinuteInclusive; + } + + public int getEndMinuteExclusive() { + return endMinuteExclusive; + } + + public int getAppearanceDayCount() { + return appearanceDayCount; + } + + public int getAppearanceCount() { + return appearanceCount; + } + + public int getSimilarHistoryDayTotal() { + return similarHistoryDayTotal; + } + + public String getHistoricalAttendanceShiftId() { + return historicalAttendanceShiftId; + } + + /** 出现天数 / 相似历史日总天数(0~1)。 */ + public double getAppearanceDayRatio() { + if (similarHistoryDayTotal <= 0) { + return 0d; + } + return appearanceDayCount / (double) similarHistoryDayTotal; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PostFixedShiftCandidate that = (PostFixedShiftCandidate) o; + return startMinuteInclusive == that.startMinuteInclusive + && endMinuteExclusive == that.endMinuteExclusive + && appearanceDayCount == that.appearanceDayCount + && appearanceCount == that.appearanceCount + && similarHistoryDayTotal == that.similarHistoryDayTotal + && Objects.equals(postId, that.postId) + && Objects.equals(normalizedShiftLabel, that.normalizedShiftLabel) + && Objects.equals(historicalAttendanceShiftId, that.historicalAttendanceShiftId); + } + + @Override + public int hashCode() { + return Objects.hash( + postId, + normalizedShiftLabel, + startMinuteInclusive, + endMinuteExclusive, + historicalAttendanceShiftId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverResult.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverResult.java new file mode 100644 index 0000000..4b9034a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverResult.java @@ -0,0 +1,103 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** 第六~八步:固定班候选、贪心覆盖、划线兜底后的汇总结果。 */ +public final class ScheduleDemandCoverResult implements Serializable { + + private final LocalDate scheduleDay; + private final Map> fixedCandidatesByPost; + private final Map fixedShiftCoverByPost; + private final Map> lineShiftPicksByPost; + /** 每岗位、每半小时行序下,最终仍缺人数(≥0) */ + private final Map remainingNeedByPostAfterLine; + + public ScheduleDemandCoverResult( + LocalDate scheduleDay, + Map> fixedCandidatesByPost, + Map fixedShiftCoverByPost, + Map> lineShiftPicksByPost, + Map remainingNeedByPostAfterLine) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.fixedCandidatesByPost = unmodifiableCopyNested(fixedCandidatesByPost); + this.fixedShiftCoverByPost = unmodifiableCopyMap(fixedShiftCoverByPost); + this.lineShiftPicksByPost = unmodifiableCopyNestedList(lineShiftPicksByPost); + this.remainingNeedByPostAfterLine = unmodifiableCopyIntArrays(remainingNeedByPostAfterLine); + } + + private static Map> unmodifiableCopyNested( + Map> in) { + Map> m = new LinkedHashMap<>(); + if (in != null) { + for (Map.Entry> e : in.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + m.put(e.getKey(), Collections.unmodifiableList(new java.util.ArrayList<>(e.getValue()))); + } + } + return Collections.unmodifiableMap(m); + } + + private static Map unmodifiableCopyMap( + Map in) { + Map m = new LinkedHashMap<>(); + if (in != null) { + m.putAll(in); + } + return Collections.unmodifiableMap(m); + } + + private static Map> unmodifiableCopyNestedList( + Map> in) { + Map> m = new LinkedHashMap<>(); + if (in != null) { + for (Map.Entry> e : in.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + m.put(e.getKey(), Collections.unmodifiableList(new java.util.ArrayList<>(e.getValue()))); + } + } + return Collections.unmodifiableMap(m); + } + + private static Map unmodifiableCopyIntArrays(Map in) { + Map m = new LinkedHashMap<>(); + if (in != null) { + for (Map.Entry e : in.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + m.put(e.getKey(), e.getValue().clone()); + } + } + return Collections.unmodifiableMap(m); + } + + public LocalDate getScheduleDay() { + return scheduleDay; + } + + public Map> getFixedCandidatesByPost() { + return fixedCandidatesByPost; + } + + public Map getFixedShiftCoverByPost() { + return fixedShiftCoverByPost; + } + + public Map> getLineShiftPicksByPost() { + return lineShiftPicksByPost; + } + + public Map getRemainingNeedByPostAfterLine() { + return remainingNeedByPostAfterLine; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverSteps678.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverSteps678.java new file mode 100644 index 0000000..b6f0f57 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleDemandCoverSteps678.java @@ -0,0 +1,189 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import jnpf.model.attendance.vo.ShiftPeriodVo; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; + +/** + * 第六、七、八步编排:常见固定班次发现 → 贪心覆盖半小时需求 → 连续缺口划线兜底。 + */ +public final class ScheduleDemandCoverSteps678 { + + /** 第七步贪心单次循环上限(防止异常数据死循环)。 */ + public static final int DEFAULT_GREEDY_MAX_ITERATIONS = 5000; + + private ScheduleDemandCoverSteps678() {} + + public static Map indexHistoryByDay(List list) { + Map map = new LinkedHashMap<>(); + if (list == null) { + return map; + } + for (DayShiftRevenueStatVo vo : list) { + if (vo != null && vo.getDay() != null && !vo.getDay().trim().isEmpty()) { + String trimmed = vo.getDay().trim(); + String key = AttendanceScheduleDayParse.normalizeToIsoDayString(trimmed).orElse(trimmed); + map.putIfAbsent(key, vo); + } + } + return map; + } + + public static ScheduleDemandCoverResult coverOneScheduleDay( + HalfHourPostDemandMatrix matrix, + List similarDatesOrdered, + Map historyByDay, + CommonFixedShiftDiscoveryConfig discoveryCfg, + LineShiftCoverConfig lineCfg) { + return coverOneScheduleDay( + matrix, + similarDatesOrdered, + historyByDay, + discoveryCfg, + lineCfg, + null, + null, + null, + DEFAULT_GREEDY_MAX_ITERATIONS); + } + + public static ScheduleDemandCoverResult coverOneScheduleDay( + HalfHourPostDemandMatrix matrix, + List similarDatesOrdered, + Map historyByDay, + CommonFixedShiftDiscoveryConfig discoveryCfg, + LineShiftCoverConfig lineCfg, + int greedyMaxIterations) { + return coverOneScheduleDay( + matrix, + similarDatesOrdered, + historyByDay, + discoveryCfg, + lineCfg, + null, + null, + null, + greedyMaxIterations); + } + + /** + * @param attendanceShiftPeriods 考勤组班次列表(与 {@code AttendanceShiftSettingService#periodList} 一致); + * {@code null} 时不做考勤班次过滤;非空则仅保留能与考勤组班次匹配上的历史固定班候选。 + */ + public static ScheduleDemandCoverResult coverOneScheduleDay( + HalfHourPostDemandMatrix matrix, + List similarDatesOrdered, + Map historyByDay, + CommonFixedShiftDiscoveryConfig discoveryCfg, + LineShiftCoverConfig lineCfg, + List attendanceShiftPeriods, + AttendanceGroupShiftMatchConfig shiftMatchCfg, + Random shiftMatchRandom, + int greedyMaxIterations) { + Objects.requireNonNull(matrix, "matrix"); + Objects.requireNonNull(discoveryCfg, "discoveryCfg"); + Objects.requireNonNull(lineCfg, "lineCfg"); + + Map> raw = + CommonFixedShiftDiscovery.discoverCandidatesByPost( + similarDatesOrdered, historyByDay, discoveryCfg); + Map> candidates = + AttendanceGroupShiftPeriodMatcher.filterCandidatesByAttendancePeriods( + raw, attendanceShiftPeriods, shiftMatchCfg, shiftMatchRandom); + + Map fixedOut = new LinkedHashMap<>(); + Map> lineOut = new LinkedHashMap<>(); + Map remainingFinal = new LinkedHashMap<>(); + + for (String post : matrix.getPostIdsOrdered()) { + List cList = candidates.getOrDefault(post, Collections.emptyList()); + FixedShiftCoverOutcome fo = + FixedShiftGreedyCoverPlanner.planForPost(matrix, post, cList, greedyMaxIterations); + fixedOut.put(post, fo); + + int n = fo.getSlotLabelsOrdered().size(); + int[] rem = new int[n]; + for (int i = 0; i < n; i++) { + rem[i] = fo.remainingNeedAt(i); + } + List linePicks = LineShiftGapPlanner.planForPost(matrix, post, rem, lineCfg); + lineOut.put(post, linePicks); + for (LineShiftPick p : linePicks) { + for (int k = p.getStartSlotIndexInclusive(); k < p.getEndSlotIndexExclusive(); k++) { + rem[k] = Math.max(0, rem[k] - 1); + } + } + remainingFinal.put(post, rem); + } + + return new ScheduleDemandCoverResult( + matrix.getScheduleDay(), candidates, fixedOut, lineOut, remainingFinal); + } + + /** + * 对区间内每个已有需求矩阵的排班日执行覆盖(仅处理 {@code matrices} 中存在的 key)。 + */ + public static Map coverAllScheduleDays( + Map matrices, + Map similarByScheduleDay, + Map historyByDay, + CommonFixedShiftDiscoveryConfig discoveryCfg, + LineShiftCoverConfig lineCfg) { + return coverAllScheduleDays( + matrices, + similarByScheduleDay, + historyByDay, + discoveryCfg, + lineCfg, + null, + null, + null); + } + + public static Map coverAllScheduleDays( + Map matrices, + Map similarByScheduleDay, + Map historyByDay, + CommonFixedShiftDiscoveryConfig discoveryCfg, + LineShiftCoverConfig lineCfg, + List attendanceShiftPeriods, + AttendanceGroupShiftMatchConfig shiftMatchCfg, + Random shiftMatchRandom) { + if (matrices == null || matrices.isEmpty()) { + return Collections.emptyMap(); + } + Map out = new LinkedHashMap<>(); + for (Map.Entry e : matrices.entrySet()) { + LocalDate day = e.getKey(); + HalfHourPostDemandMatrix m = e.getValue(); + if (day == null || m == null) { + continue; + } + ScheduleTemplateSimilarDaysResult sim = + similarByScheduleDay == null ? null : similarByScheduleDay.get(day); + if (sim == null || !sim.isSuccess()) { + continue; + } + out.put( + day, + coverOneScheduleDay( + m, + sim.getSimilarDates(), + historyByDay, + discoveryCfg, + lineCfg, + attendanceShiftPeriods, + shiftMatchCfg, + shiftMatchRandom, + DEFAULT_GREEDY_MAX_ITERATIONS)); + } + return Collections.unmodifiableMap(out); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePatternSimilarDaysAlgorithm.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePatternSimilarDaysAlgorithm.java new file mode 100644 index 0000000..d6dcdd1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePatternSimilarDaysAlgorithm.java @@ -0,0 +1,199 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 租户无数据分析营业额(或全部为 0 / null)时:不按营业额筛选,按有效历史排班日选相似样本。 + * + *

与产品文档(无营业额场景)一致: + *

    + *
  1. 最近 30 个自然日内,与目标日同星期几、且带有效班次列表的历史日 → 若至少 1 天则全部采用;若为 0 天则
  2. + *
  3. 目标日前连续 {@value ScheduleTemplateSimilarDaysResult#PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW} 个自然日中,须每日均有有效班次 + * → 采用该窗口内全部有班次日;若不满足则
  4. + *
  5. 失败,提示请先手动补排 {@code n} 天, + * {@code n = }{@link ScheduleTemplateSimilarDaysResult#PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW} + * −(该窗口内有有效班次的天数)(见 {@link ScheduleTemplateSimilarDaysResult#msgInsufficientHistoryForPatternSchedule(int)})。 + *
+ */ +public final class SchedulePatternSimilarDaysAlgorithm { + + private static final Logger LOG = LoggerFactory.getLogger(SchedulePatternSimilarDaysAlgorithm.class); + + /** 与产品「最近30天」口径一致(不含目标日) */ + public static final int SAME_WEEKDAY_LOOKBACK_DAYS = 30; + + /** 最近自然日窗口长度(应与 {@link ScheduleTemplateSimilarDaysResult#PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW} 一致) */ + public static final int LAST_NATURAL_DAY_WINDOW = ScheduleTemplateSimilarDaysResult.PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW; + + /** + * 条件(2)中要求最少「有效排班」天数:连续窗口内均需有班次 ⇒ 等与窗口长度相等。 + */ + public static final int MIN_EFFECTIVE_DAYS_IN_LAST_NATURAL_WINDOW = LAST_NATURAL_DAY_WINDOW; + + /** + * 与 {@link #findForScheduleDays(List, Map)} 相同入参语义:仅使用 + * {@code estimatedRevenueByDay} 的 key 集合作为目标排班日。 + * + * @param dayShiftRevenueStatVoList 通常为近 90 天历史;内部按 ISO 日历日聚合 + * @param estimatedRevenueByDay key 必填;营业额值在本算法中不参与筛选 + */ + public Map findForScheduleDays( + List dayShiftRevenueStatVoList, + Map estimatedRevenueByDay) { + if (estimatedRevenueByDay == null || estimatedRevenueByDay.isEmpty()) { + LOG.error("[排班模板-规律模式][多日] estimatedRevenueByDay 为空,跳过"); + return Collections.emptyMap(); + } + Map byIso = normalizedHistoryIndex(dayShiftRevenueStatVoList); + Map out = new LinkedHashMap<>(); + for (LocalDate scheduleDay : estimatedRevenueByDay.keySet()) { + if (scheduleDay == null) { + continue; + } + ScheduleTemplateSimilarDaysResult r = findSimilarPatternDays(byIso, scheduleDay); + out.put(scheduleDay, r); + LOG.error( + "[排班模板-规律模式][多日] scheduleDay={} success={} tier={} similarCount={} message={}", + scheduleDay, + r.isSuccess(), + r.getTier(), + r.getSimilarDates().size(), + r.getMessage()); + } + return Collections.unmodifiableMap(out); + } + + public ScheduleTemplateSimilarDaysResult findSimilarPatternDaysForOneScheduleDay( + List dayShiftRevenueStatVoList, LocalDate targetScheduleDay) { + Map byIso = normalizedHistoryIndex(dayShiftRevenueStatVoList); + return findSimilarPatternDays(byIso, targetScheduleDay); + } + + /** key: ISO yyyy-MM-dd(与相似日返回的 LocalDate.toString() 对齐) */ + public static Map normalizedHistoryIndex( + List dayShiftRevenueStatVoList) { + Map map = new LinkedHashMap<>(); + if (dayShiftRevenueStatVoList == null || dayShiftRevenueStatVoList.isEmpty()) { + return map; + } + for (DayShiftRevenueStatVo vo : dayShiftRevenueStatVoList) { + if (vo == null || vo.getDay() == null || vo.getDay().trim().isEmpty()) { + continue; + } + String trimmed = vo.getDay().trim(); + String key = + AttendanceScheduleDayParse.normalizeToIsoDayString(trimmed).orElse(trimmed); + if (key.isEmpty()) { + continue; + } + map.putIfAbsent(key, vo); + } + return map; + } + + private ScheduleTemplateSimilarDaysResult findSimilarPatternDays( + Map byIso, LocalDate targetScheduleDay) { + Objects.requireNonNull(targetScheduleDay, "targetScheduleDay"); + Objects.requireNonNull(byIso, "byIso"); + + DayOfWeek dow = targetScheduleDay.getDayOfWeek(); + // (1)最近 30 个自然日内、同星期的有效排班日(≥1 天即接受) + List sameWeekEffective = new ArrayList<>(); + for (int delta = 1; delta <= SAME_WEEKDAY_LOOKBACK_DAYS; delta++) { + LocalDate d = targetScheduleDay.minusDays(delta); + if (d.getDayOfWeek() != dow) { + continue; + } + DayShiftRevenueStatVo vo = byIso.get(d.toString()); + if (hasEffectiveShiftStructure(vo)) { + sameWeekEffective.add(d); + } + } + Collections.sort(sameWeekEffective); + if (!sameWeekEffective.isEmpty()) { + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_pattern", + String.format( + "algo=SchedulePatternSimilarDaysAlgorithm | targetScheduleDay=%s | branch=1_SAME_WEEKDAY_30_NATURAL_DAYS " + + "| lookBackDays=%d | targetDow=%s | effectiveSameWeekdayHistoryCount=%d | similarDates=%s", + targetScheduleDay, + SAME_WEEKDAY_LOOKBACK_DAYS, + dow, + sameWeekEffective.size(), + SchedulingForTestCheckLog.formatDatesBrief( + sameWeekEffective, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok( + sameWeekEffective, ScheduleTemplateSimilarDaysResult.SelectionTier.PATTERN_SAME_WEEKDAY_30D); + } + + // (2)连续「最近 LAST_NATURAL_DAY_WINDOW」个自然日均须有有效班次 + List last7Effective = new ArrayList<>(); + for (int delta = 1; delta <= LAST_NATURAL_DAY_WINDOW; delta++) { + LocalDate d = targetScheduleDay.minusDays(delta); + DayShiftRevenueStatVo vo = byIso.get(d.toString()); + if (hasEffectiveShiftStructure(vo)) { + last7Effective.add(d); + } + } + Collections.sort(last7Effective); + if (last7Effective.size() >= MIN_EFFECTIVE_DAYS_IN_LAST_NATURAL_WINDOW) { + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_pattern", + String.format( + "algo=SchedulePatternSimilarDaysAlgorithm | targetScheduleDay=%s | branch=2_LAST_%d_NATURAL_DAYS_ALL_EFFECTIVE " + + "| naturalWindowDays=%d | requiredEffectiveEq=%d | effectiveDaysWithShifts=%d | similarDates=%s", + targetScheduleDay, + LAST_NATURAL_DAY_WINDOW, + LAST_NATURAL_DAY_WINDOW, + MIN_EFFECTIVE_DAYS_IN_LAST_NATURAL_WINDOW, + last7Effective.size(), + SchedulingForTestCheckLog.formatDatesBrief( + last7Effective, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok( + last7Effective, ScheduleTemplateSimilarDaysResult.SelectionTier.PATTERN_LAST_7_NATURAL_GE7); + } + + String failMsg = + ScheduleTemplateSimilarDaysResult.msgInsufficientHistoryForPatternSchedule(last7Effective.size()); + + LOG.error( + "[排班模板-规律模式] target={} 同周30天内无班次,且近{}日有效排班仅{}天(需={}天才可)→ 拒绝智能排班", + targetScheduleDay, + LAST_NATURAL_DAY_WINDOW, + last7Effective.size(), + MIN_EFFECTIVE_DAYS_IN_LAST_NATURAL_WINDOW); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_pattern", + String.format( + "algo=SchedulePatternSimilarDaysAlgorithm | targetScheduleDay=%s | outcome=FAIL | branch=FALLBACK " + + "| sameWeekday30dCandidates=0 | lastNaturalWindowEffectiveDays=%d | requiredEffectiveEq=%d | bizMessage=%s", + targetScheduleDay, + last7Effective.size(), + MIN_EFFECTIVE_DAYS_IN_LAST_NATURAL_WINDOW, + failMsg)); + return ScheduleTemplateSimilarDaysResult.failure(failMsg); + } + + /** 班次列表非空即视为可作半小时表输入(与§2.4「有班次结构」一致)。 */ + private static boolean hasEffectiveShiftStructure(DayShiftRevenueStatVo vo) { + return vo != null && vo.getShifts() != null && !vo.getShifts().isEmpty(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePeriodWorkTracker.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePeriodWorkTracker.java new file mode 100644 index 0000000..c8d29a7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulePeriodWorkTracker.java @@ -0,0 +1,170 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.attendance.UserWorkSituationVo; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 排班区间内动态累计员工工时与工作状态,供第十步规则与 + * {@link ShiftPlanFinalStaffAssigner#buildContext} 使用。 + *

+ * 生命周期 + *

    + *
  1. {@link #from(Map)}:用考勤组查询的初始 {@link UserWorkSituationVo} 快照初始化;
  2. + *
  3. 每次 {@link ShiftPlanFinalStaffAssigner#assign} 成功选中一人后调用 {@link #recordAssignment};
  4. + *
  5. {@link #asSituationMap()} 返回的 map 与内部状态同一引用,随排班推进持续更新。
  6. + *
+ *

+ * 与 {@link StaffAssignmentDayLedger} 分工:Tracker 管工时/周累计/休息间隔;Ledger 管同日固定 vs 划线互斥。 + */ +public final class SchedulePeriodWorkTracker { + + /** 员工工作状态(连续天数、周工作天数、周工时),选人后递增 */ + private final Map situationByUserId; + /** 员工 → 排班日 → 当日已排工时(小时) */ + private final Map> hoursByUserThenDay = new HashMap<>(); + /** 员工 → 排班日 → 当日最后一段班的结束分钟(0~1440,用于同日/跨日休息间隔) */ + private final Map> lastEndMinuteByUserThenDay = new HashMap<>(); + /** 员工 → 最近一次被分配的自然日(用于连续工作天数) */ + private final Map lastAssignedDayByUser = new HashMap<>(); + + private SchedulePeriodWorkTracker(Map situationByUserId) { + this.situationByUserId = situationByUserId; + } + + /** + * 由排班开始前的人事统计快照构建 Tracker。 + * + * @param base userId → 初始 {@link UserWorkSituationVo}(如 {@code attendanceDayStatisticsService.queryUserWorkSituation}) + */ + public static SchedulePeriodWorkTracker from(Map base) { + Map copy = new LinkedHashMap<>(); + if (base != null) { + for (Map.Entry e : base.entrySet()) { + if (e.getKey() == null || e.getKey().trim().isEmpty()) { + continue; + } + UserWorkSituationVo src = e.getValue(); + copy.put( + e.getKey().trim(), + UserWorkSituationVo.builder() + .continuousDays(src == null ? null : src.getContinuousDays()) + .weekWorkDays(src == null ? null : src.getWeekWorkDays()) + .weekWorkHours( + src == null || src.getWeekWorkHours() == null + ? BigDecimal.ZERO + : src.getWeekWorkHours()) + .build()); + } + } + return new SchedulePeriodWorkTracker(copy); + } + + /** + * 供 {@link StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation} 读取的最新工作状态。 + *

+ * 返回内部 map 的不可变视图包装;排班过程中 {@link #recordAssignment} 会修改其中 VO 字段。 + */ + public Map asSituationMap() { + return Collections.unmodifiableMap(situationByUserId); + } + + /** + * 该员工在指定排班日已分配的工时合计(小时)。 + *

+ * 用于构造 {@link StaffAssignmentContext#getExistingSameDayScheduledWorkHours()}。 + */ + public double getSameDayScheduledHours(String employeeId, LocalDate day) { + if (employeeId == null || day == null) { + return 0d; + } + Map byDay = hoursByUserThenDay.get(employeeId.trim()); + if (byDay == null) { + return 0d; + } + return byDay.getOrDefault(day, 0d); + } + + /** + * 当前拟排班段开始时刻,与上一段已排班结束时刻的间隔(分钟)。 + *

    + *
  • 同日:{@code startMinuteInclusive - sameDayEnd}(仅当 start ≥ 上一段结束);
  • + *
  • 跨日:前一日最后结束到当日 0 点 + 当日开始分钟;
  • + *
  • 无上一段:{@code null}(最小休息规则不判违反)。
  • + *
+ */ + public Integer minutesRestSincePriorShiftEnd( + String employeeId, LocalDate scheduleDay, int startMinuteInclusive) { + if (employeeId == null || scheduleDay == null) { + return null; + } + String id = employeeId.trim(); + Map ends = lastEndMinuteByUserThenDay.get(id); + if (ends != null) { + Integer sameDayEnd = ends.get(scheduleDay); + if (sameDayEnd != null && startMinuteInclusive >= sameDayEnd) { + return startMinuteInclusive - sameDayEnd; + } + } + LocalDate prev = scheduleDay.minusDays(1); + if (ends != null) { + Integer prevEnd = ends.get(prev); + if (prevEnd != null) { + return (24 * 60 - prevEnd) + startMinuteInclusive; + } + } + return null; + } + + /** + * 记录一次成功排班分配,更新日工时、段结束时刻、连续天数、周工作天数、周工时。 + *

+ * 应在 {@link ShiftPlanFinalStaffAssigner} 将员工写入结果集之后调用, + * 以便后续班次能读到本段的累计效应。 + * + * @param startMinuteInclusive 班段开始(当日 0 点起算分钟) + * @param endMinuteExclusive 班段结束(开区间) + */ + public void recordAssignment( + String employeeId, LocalDate scheduleDay, int startMinuteInclusive, int endMinuteExclusive) { + Objects.requireNonNull(scheduleDay, "scheduleDay"); + if (employeeId == null || employeeId.trim().isEmpty()) { + return; + } + String id = employeeId.trim(); + double hours = Math.max(0d, (endMinuteExclusive - startMinuteInclusive) / 60.0); + hoursByUserThenDay + .computeIfAbsent(id, k -> new HashMap<>()) + .merge(scheduleDay, hours, Double::sum); + lastEndMinuteByUserThenDay + .computeIfAbsent(id, k -> new HashMap<>()) + .merge(scheduleDay, endMinuteExclusive, Math::max); + + UserWorkSituationVo vo = + situationByUserId.computeIfAbsent(id, k -> UserWorkSituationVo.builder().build()); + LocalDate last = lastAssignedDayByUser.get(id); + boolean firstOnDay = last == null || !last.equals(scheduleDay); + if (firstOnDay) { + int prevConsecutive = nz(vo.getContinuousDays()); + if (last == null || last.equals(scheduleDay.minusDays(1))) { + vo.setContinuousDays(prevConsecutive + 1); + } else { + vo.setContinuousDays(1); + } + vo.setWeekWorkDays(nz(vo.getWeekWorkDays()) + 1); + } + BigDecimal wh = vo.getWeekWorkHours() == null ? BigDecimal.ZERO : vo.getWeekWorkHours(); + vo.setWeekWorkHours(wh.add(BigDecimal.valueOf(hours))); + lastAssignedDayByUser.put(id, scheduleDay); + } + + private static int nz(Integer x) { + return x == null ? 0 : x; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleStaffAssignmentSteps910.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleStaffAssignmentSteps910.java new file mode 100644 index 0000000..95d24cd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleStaffAssignmentSteps910.java @@ -0,0 +1,146 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + +/** + * 智能排班第九、十步的「单上下文」编排入口(与 {@link ShiftPlanFinalStaffAssigner} 并行存在)。 + *

+ * 差异:本类对整批候选人使用同一个 {@link StaffAssignmentContext} 做规则判断; + * {@link ShiftPlanFinalStaffAssigner} 则为每位员工单独构造上下文(含个人当日已排工时与休息间隔),生产路径以 Assigner 为准。 + *

+ * 处理流程({@link #pickForPostDemand}) + *

    + *
  1. {@link #buildCandidatePool}:工作站专岗/通岗求交 + 随机打乱;
  2. + *
  3. {@link FixedSoftConstraintRelaxationPlanner#pickWithRelaxation}:第九步硬约束 + 第十步固定规则(含尽量放松);
  4. + *
  5. 按 {@link StaffAssignmentDayLedger} 过滤并标记:同日不可既固定又划线。
  6. + *
+ *

+ * 划线排班:{@code lineAssignment=true} 时不传 {@link FixedSchedulingRuleVo},不参与固定规则放松。 + */ +public final class ScheduleStaffAssignmentSteps910 { + + private ScheduleStaffAssignmentSteps910() {} + + /** + * 为某岗位、某需求人数选出员工。 + * + * @param targetCount 目标人数(岗位需求 headcount) + * @param rawCandidatesForPost 上游给出的该岗位候选员工 ID(如专岗+通岗有序列表) + * @param workstationBuckets 工作站岗位绑定;用于与候选求交 + * @param port 第九~十步规则判定端口 + * @param context 本段班次的分配上下文(本方法中全员共用同一 context) + * @param fixedSchedulingRules 固定排班规则;划线班时由调用方在内部置 null + * @param ledger 同日固定/划线互斥账本 + * @param lineAssignment true=划线班(跳过固定规则) + * @param fixedShiftGroupKey 固定班逻辑键({@link ShiftPlanFinalStaffAssigner#fixedShiftGroupKey} 同源); + * 划线时可 null + * @param random 候选池打乱用;null 则新建 Random + * @return 最多 {@code targetCount} 个员工 ID,顺序为规则过滤后的遍历顺序(非专岗优先序) + */ + public static List pickForPostDemand( + int targetCount, + List rawCandidatesForPost, + WorkstationPostStaffBuckets workstationBuckets, + StaffRuleEvaluationPort port, + StaffAssignmentContext context, + FixedSchedulingRuleVo fixedSchedulingRules, + StaffAssignmentDayLedger ledger, + boolean lineAssignment, + String fixedShiftGroupKey, + Random random) { + if (targetCount <= 0) { + return Collections.emptyList(); + } + Objects.requireNonNull(port, "port"); + Objects.requireNonNull(context, "context"); + Objects.requireNonNull(ledger, "ledger"); + Random rnd = random == null ? new Random() : random; + + // 步骤 1:构建本岗位候选池(工作站求交 + shuffle) + List pool = + buildCandidatePool( + context.getPostId(), rawCandidatesForPost, workstationBuckets, rnd); + + // 步骤 2:第十步 — 固定规则 + 尽量放松(划线班 fixedVo=null) + FixedSchedulingRuleVo fixedVo = lineAssignment ? null : fixedSchedulingRules; + List relaxed = + FixedSoftConstraintRelaxationPlanner.pickWithRelaxation( + targetCount, pool, fixedVo, port, context, rnd); + + // 步骤 3:账本互斥 — 在放松结果上按序取人并 mark,满 targetCount 即停 + List out = new ArrayList<>(); + for (String id : relaxed) { + if (id == null || id.trim().isEmpty()) { + continue; + } + if (lineAssignment) { + if (!ledger.canAssignLine(id, context.getScheduleDay())) { + continue; + } + } else { + if (!ledger.canAssignFixed(id, context.getScheduleDay(), fixedShiftGroupKey)) { + continue; + } + } + out.add(id); + if (lineAssignment) { + ledger.markLineShift(id, context.getScheduleDay()); + } else { + ledger.markFixed(id, context.getScheduleDay(), fixedShiftGroupKey); + } + if (out.size() >= targetCount) { + break; + } + } + return Collections.unmodifiableList(out); + } + + /** + * 构建本岗位候选员工池。 + *

+ * 无工作站绑定:清洗 {@code rawCandidatesForPost} 后 shuffle 返回。
+ * 有绑定:与工作站专岗+通岗 ID 求交;交集为空则返回空列表(不再退化为工作站全集),再 shuffle。 + *

+ * 与 {@link ShiftPlanFinalStaffAssigner#intersectWorkstationPool} 逻辑类似,但本方法始终打乱顺序以均衡人选。 + */ + private static List buildCandidatePool( + String postId, + List rawCandidatesForPost, + WorkstationPostStaffBuckets workstationBuckets, + Random rnd) { + List raw = rawCandidatesForPost == null ? Collections.emptyList() : rawCandidatesForPost; + if (workstationBuckets == null || postId == null || !workstationBuckets.hasWorkstationBinding(postId)) { + List copy = new ArrayList<>(); + for (String s : raw) { + if (s != null && !s.trim().isEmpty()) { + copy.add(s.trim()); + } + } + Collections.shuffle(copy, rnd); + return copy; + } + Set allowed = new LinkedHashSet<>(); + allowed.addAll(workstationBuckets.getSpecialistIds(postId)); + allowed.addAll(workstationBuckets.getGeneralIds(postId)); + List out = new ArrayList<>(); + for (String s : raw) { + if (s == null || s.trim().isEmpty()) { + continue; + } + String t = s.trim(); + if (allowed.contains(t)) { + out.add(t); + } + } + Collections.shuffle(out, rnd); + return out; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysAlgorithm.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysAlgorithm.java new file mode 100644 index 0000000..72f5bc5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysAlgorithm.java @@ -0,0 +1,615 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 根据近 90 天按日班次营业额统计与目标日预估营业额,筛选用于排班模板的历史参考日。 + * + *

规则顺序(与产品说明一致): + *

    + *
  1. 在最近 90 天窗口内(不含目标排班日本身),取与目标「同星期几」的历史日;
  2. + *
  3. 营业额优先落在预估 ±λ₁ 区间内(默认 λ₁=0.10);若样本数 ≥ minBandSampleDays(默认 10)则采用;
  4. + *
  5. 否则在同星期几上放宽到 ±λ₂(默认 0.15);同上样本阈值;
  6. + *
  7. 若仍不足,改为「同工作日/周末类型」且 ±λ₂;同上样本阈值;
  8. + *
  9. 若仍不足,在全窗口内按 |历史营业额−预估| 升序取前 n 个自然日(n∈[1,10],默认 10);
  10. + *
  11. 若仍无任何候选日,返回失败及「无参考数据,无法智能排班。」
  12. + *
+ * + *

营业额带(预估非 0、非 null):历史营业额 R ∈ [预估×(1−λ), 预估×(1+λ)];tier1 用 λ₁,tier2/tier3 用 λ₂(默认 0.10 / 0.15,可由构造参数或 {@code AutoScheduleService} 配置覆盖)。 + * 预估为 0 或 null 时,不按营业额过滤(该档内窗口内日期在营业额条件上全部通过)。 + */ +public final class ScheduleTemplateSimilarDaysAlgorithm { + + private static final Logger LOG = LoggerFactory.getLogger(ScheduleTemplateSimilarDaysAlgorithm.class); + + /** 回看自然日窗口长度(与常见「近 90 天」统计一致) */ + public static final int LOOKBACK_DAYS = 90; + + /** + * 档位 1~3(及同类型合并分支)要求的最少样本天数默认值; + * 运行时可由 {@code new ScheduleTemplateSimilarDaysAlgorithm(topCap, minSamples)} 或业务入口注入覆盖。 + */ + public static final int DEFAULT_MIN_BAND_SAMPLE_DAYS = 10; + + /** + * 与 {@link #DEFAULT_MIN_BAND_SAMPLE_DAYS} 同义,兼容旧代码与文档中的 {@code MIN_SAMPLE_DAYS} 称呼。 + */ + public static final int MIN_SAMPLE_DAYS = DEFAULT_MIN_BAND_SAMPLE_DAYS; + + /** tier1(同星期几·严带)默认半宽 λ,即 ±10%。 */ + public static final BigDecimal DEFAULT_BAND_STRICT = new BigDecimal("0.10"); + + /** tier2 / tier3(同星期几·宽带、同工作日周末类型)默认半宽 λ,即 ±15%。 */ + public static final BigDecimal DEFAULT_BAND_RELAXED = new BigDecimal("0.15"); + + private final int topNearestCap; + + /** 营业额带各档(tier1~3 及 tier2∪tier3 合并)采用前的最少样本天数,区间 [1, {@link #LOOKBACK_DAYS}] */ + private final int minBandSampleDays; + + /** tier1:`filterSameDowAndBand(..., λ₁)` */ + private final BigDecimal bandStrict; + + /** tier2、tier3:`filterSame...AndBand(..., λ₂)` */ + private final BigDecimal bandRelaxed; + + public ScheduleTemplateSimilarDaysAlgorithm() { + this(10, DEFAULT_MIN_BAND_SAMPLE_DAYS, DEFAULT_BAND_STRICT, DEFAULT_BAND_RELAXED); + } + + /** + * @param topNearestCap 第 6 步 Top n 上限,限制在 1~10 + */ + public ScheduleTemplateSimilarDaysAlgorithm(int topNearestCap) { + this(topNearestCap, DEFAULT_MIN_BAND_SAMPLE_DAYS, DEFAULT_BAND_STRICT, DEFAULT_BAND_RELAXED); + } + + /** + * @param topNearestCap 第 6 步 Top n 上限,限制在 1~10 + * @param minBandSampleDays 档位 1~3 要求的最少样本天数,clamp 到 [1, {@link #LOOKBACK_DAYS}] + */ + public ScheduleTemplateSimilarDaysAlgorithm(int topNearestCap, int minBandSampleDays) { + this(topNearestCap, minBandSampleDays, DEFAULT_BAND_STRICT, DEFAULT_BAND_RELAXED); + } + + /** + * @param bandStrict tier1 营业额半宽 λ₁,须在 (0,1),否则回退 {@link #DEFAULT_BAND_STRICT} + * @param bandRelaxed tier2/tier3 营业额半宽 λ₂,须在 (0,1),否则回退 {@link #DEFAULT_BAND_RELAXED} + */ + public ScheduleTemplateSimilarDaysAlgorithm( + int topNearestCap, int minBandSampleDays, BigDecimal bandStrict, BigDecimal bandRelaxed) { + int cap = topNearestCap; + if (cap < 1) { + cap = 1; + } + if (cap > 10) { + cap = 10; + } + this.topNearestCap = cap; + + int ms = minBandSampleDays; + if (ms < 1) { + ms = 1; + } + if (ms > LOOKBACK_DAYS) { + ms = LOOKBACK_DAYS; + } + this.minBandSampleDays = ms; + + this.bandStrict = coerceRevenueHalfWidthLambda(bandStrict, DEFAULT_BAND_STRICT, "bandStrict"); + this.bandRelaxed = coerceRevenueHalfWidthLambda(bandRelaxed, DEFAULT_BAND_RELAXED, "bandRelaxed"); + } + + private static BigDecimal coerceRevenueHalfWidthLambda(BigDecimal value, BigDecimal fallback, String label) { + BigDecimal v = value != null ? value : fallback; + if (v.compareTo(BigDecimal.ZERO) <= 0 || v.compareTo(BigDecimal.ONE) >= 0) { + LOG.error( + "[排班模板] 营业额带 {}={} 非法(须在开区间 (0,1)),使用默认 {}", + label, + Objects.toString(value, "null"), + fallback.toPlainString()); + return fallback; + } + return v; + } + + /** + * 对排班区间内每个目标日,分别计算参考历史日。 + * + * @param dayShiftRevenueStatVoList 近窗按日统计(通常含约 90 天) + * @param estimatedRevenueByDay 目标自然日 → 预估营业额(key 为排班日) + */ + public Map findForScheduleDays( + List dayShiftRevenueStatVoList, + Map estimatedRevenueByDay) { + if (estimatedRevenueByDay == null || estimatedRevenueByDay.isEmpty()) { + LOG.error("[排班模板][多日] estimatedRevenueByDay 为空,跳过"); + return Collections.emptyMap(); + } + LOG.error( + "[排班模板][多日] 开始:排班日数={},历史 VO 条数={}", + estimatedRevenueByDay.size(), + dayShiftRevenueStatVoList == null ? 0 : dayShiftRevenueStatVoList.size()); + Map out = new LinkedHashMap<>(); + for (Map.Entry e : estimatedRevenueByDay.entrySet()) { + if (e.getKey() == null) { + continue; + } + ScheduleTemplateSimilarDaysResult one = + findSimilarTemplateDays(dayShiftRevenueStatVoList, e.getKey(), e.getValue()); + out.put(e.getKey(), one); + LOG.error( + "[排班模板][多日] 单日完成:排班日={} 预估营业额={} success={} tier={} similarCount={}", + e.getKey(), + e.getValue(), + one.isSuccess(), + one.getTier(), + one.getSimilarDates().size()); + } + return out; + } + + /** + * 为单个目标排班日寻找模板参考历史日。 + */ + public ScheduleTemplateSimilarDaysResult findSimilarTemplateDays( + List dayShiftRevenueStatVoList, + LocalDate targetScheduleDay, + BigDecimal targetEstimatedRevenue) { + Objects.requireNonNull(targetScheduleDay, "targetScheduleDay"); + + LOG.error( + "[排班模板][单日] 输入参数:dayShiftRevenueStatVoList={} (size={}),targetScheduleDay={},targetEstimatedRevenue={}", + summarizeDayShiftRevenueStatVoList(dayShiftRevenueStatVoList), + dayShiftRevenueStatVoList == null ? 0 : dayShiftRevenueStatVoList.size(), + targetScheduleDay, + targetEstimatedRevenue == null ? "null" : targetEstimatedRevenue.toPlainString()); + + LOG.error( + "[排班模板][单日] ========== 开始:目标排班日={} 星期={} 预估营业额={} 回看天数={} 带状档最少样本={} λ1={} λ2={} TopN上限={}", + targetScheduleDay, + targetScheduleDay.getDayOfWeek(), + targetEstimatedRevenue, + LOOKBACK_DAYS, + minBandSampleDays, + bandStrict.toPlainString(), + bandRelaxed.toPlainString(), + topNearestCap); + + LocalDate windowStart = targetScheduleDay.minusDays(LOOKBACK_DAYS); + List inWindow = filterWindow(dayShiftRevenueStatVoList, windowStart, targetScheduleDay); + LOG.error( + "[排班模板][单日] 步骤0-窗口:左闭右开 [{}, {}) 内有效历史日数={}", + windowStart, + targetScheduleDay, + inWindow.size()); + if (inWindow.isEmpty()) { + LOG.error("[排班模板][单日] 目标日={} 窗口内无有效历史日 → 失败", targetScheduleDay); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | targetScheduleDay=%s | dow=%s | targetEstimatedRevenue=%s " + + "| historyWindowLeftClosedRightOpen=[%s,%s) | inWindowEffectiveDays=%d | minBandSampleDays=%d | topNearestCap=%d " + + "| bandStrict_lambda=%s | bandRelaxed_lambda=%s | outcome=FAIL | reason=WINDOW_EMPTY | bizMessage=%s", + targetScheduleDay, + targetScheduleDay.getDayOfWeek(), + targetEstimatedRevenue == null ? "null" : targetEstimatedRevenue.toPlainString(), + windowStart, + targetScheduleDay, + inWindow.size(), + minBandSampleDays, + topNearestCap, + bandStrict.toPlainString(), + bandRelaxed.toPlainString(), + ScheduleTemplateSimilarDaysResult.MSG_NO_REFERENCE_DATA)); + return ScheduleTemplateSimilarDaysResult.failure(ScheduleTemplateSimilarDaysResult.MSG_NO_REFERENCE_DATA); + } + + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | targetScheduleDay=%s | dow=%s | targetEstimatedRevenue=%s " + + "| historyWindowLeftClosedRightOpen=[%s,%s) | inWindowEffectiveDays=%d | minBandSampleDays=%d | topNearestCap=%d " + + "| bandStrict_lambda=%s | bandRelaxed_lambda=%s", + targetScheduleDay, + targetScheduleDay.getDayOfWeek(), + targetEstimatedRevenue == null ? "null" : targetEstimatedRevenue.toPlainString(), + windowStart, + targetScheduleDay, + inWindow.size(), + minBandSampleDays, + topNearestCap, + bandStrict.toPlainString(), + bandRelaxed.toPlainString())); + + DayOfWeek targetDow = targetScheduleDay.getDayOfWeek(); + boolean targetWeekend = isWeekend(targetDow); + int sameDowInPool = countSameDowInPool(inWindow, targetDow); + LOG.error( + "[排班模板][单日] 步骤0-统计:目标为{} 窗口内同星期几历史日数={}", + targetWeekend ? "周末" : "工作日", + sameDowInPool); + logRevenueBand("步骤1-同星期几·严带(λ1)", targetEstimatedRevenue, bandStrict); + + // tier1:同星期几 × λ₁ + List tier1 = filterSameDowAndBand(inWindow, targetDow, targetEstimatedRevenue, bandStrict); + LOG.error("[排班模板][单日] 步骤1-同星期几·严带:命中样本数={}(阈值>={})", tier1.size(), minBandSampleDays); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_tier_decision", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | step=STEP1_SAME_WEEKDAY_STRICT_BAND_lambda1 " + + "| targetScheduleDay=%s | tierSampleCount=%d | minBandSampleDays=%s | decision=%s", + targetScheduleDay, + tier1.size(), + minBandSampleDays, + tier1.size() >= minBandSampleDays ? "ACCEPT" : "SKIP_TO_STEP2")); + if (tier1.size() >= minBandSampleDays) { + List dates = datesOf(tier1); + logResultReturn(targetScheduleDay, ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WEEKDAY_STRICT_BAND, dates); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_outcome", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | outcome=OK | tier=%s | similarCount=%d | similarDates=%s", + ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WEEKDAY_STRICT_BAND, + dates.size(), + SchedulingForTestCheckLog.formatDatesBrief(dates, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok( + dates, ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WEEKDAY_STRICT_BAND); + } + + logRevenueBand("步骤2-同星期几·宽带(λ2)", targetEstimatedRevenue, bandRelaxed); + // tier2:同星期几 × λ₂ + List tier2 = filterSameDowAndBand(inWindow, targetDow, targetEstimatedRevenue, bandRelaxed); + LOG.error("[排班模板][单日] 步骤2-同星期几·宽带:命中样本数={}(阈值>={})", tier2.size(), minBandSampleDays); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_tier_decision", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | step=STEP2_SAME_WEEKDAY_RELAXED_BAND_lambda2 " + + "| targetScheduleDay=%s | tierSampleCount=%d | minBandSampleDays=%s | decision=%s", + targetScheduleDay, + tier2.size(), + minBandSampleDays, + tier2.size() >= minBandSampleDays ? "ACCEPT" : "SKIP_TO_STEP3")); + if (tier2.size() >= minBandSampleDays) { + List dates = datesOf(tier2); + logResultReturn(targetScheduleDay, ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WEEKDAY_RELAXED_BAND, dates); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_outcome", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | outcome=OK | tier=%s | similarCount=%d | similarDates=%s", + ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WEEKDAY_RELAXED_BAND, + dates.size(), + SchedulingForTestCheckLog.formatDatesBrief(dates, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok( + dates, ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WEEKDAY_RELAXED_BAND); + } + + LOG.error( + "[排班模板][单日] 步骤3-同工作日/周末类型·宽带(λ2):开始(λ2={})", + bandRelaxed.toPlainString()); + // tier3:同工作日/周末类型 × λ₂ + List tier3 = + filterSameWorkdayTypeAndBand(inWindow, targetWeekend, targetEstimatedRevenue, bandRelaxed); + int sameTypeInPool = countSameWorkdayTypeInPool(inWindow, targetWeekend); + LOG.error( + "[排班模板][单日] 步骤3-同类型·宽带:同类型历史日数={} 命中营业额带样本数={}(阈值>={})", + sameTypeInPool, + tier3.size(), + minBandSampleDays); + if (tier3.size() >= minBandSampleDays) { + List dates = datesOf(tier3); + logResultReturn( + targetScheduleDay, + ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WORKDAY_TYPE_RELAXED_BAND, + dates); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_outcome", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | outcome=OK | tier=%s | path=PURE_TIER3_BAND " + + "| tier3SampleCount=%d | combinedNotUsed=true | similarCount=%d | similarDates=%s", + ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WORKDAY_TYPE_RELAXED_BAND, + tier3.size(), + dates.size(), + SchedulingForTestCheckLog.formatDatesBrief(dates, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok( + dates, ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WORKDAY_TYPE_RELAXED_BAND); + } else if (tier3.size() > 0) { + Set combined = new LinkedHashSet<>(tier2); + combined.addAll(tier3); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_tier_decision", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | step=STEP3_SAME_WORKDAY_TYPE_RELAXED_BAND_fallback " + + "| targetScheduleDay=%s | tier3AloneSampleCount=%d | tier3BelowThreshold=yes " + + "| tier2_union_tier3_distinct_days=%d | minBandSampleDays=%s | decision=%s", + targetScheduleDay, + tier3.size(), + combined.size(), + minBandSampleDays, + combined.size() >= minBandSampleDays ? "ACCEPT_COMBINED_T2_PLUS_T3" : "SKIP_TO_TOPN")); + if (combined.size() >= minBandSampleDays) { + List dates = combined.stream().map(d -> d.date).collect(Collectors.toList()); + logResultReturn( + targetScheduleDay, + ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WORKDAY_TYPE_RELAXED_BAND, + dates); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_outcome", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | outcome=OK | tier=%s | path=COMBINED_TIER2_PLUS_TIER3 " + + "| similarCount=%d | similarDates=%s", + ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WORKDAY_TYPE_RELAXED_BAND, + dates.size(), + SchedulingForTestCheckLog.formatDatesBrief(dates, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok( + dates, ScheduleTemplateSimilarDaysResult.SelectionTier.SAME_WORKDAY_TYPE_RELAXED_BAND); + } + } else { + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_tier_decision", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | step=STEP3_SAME_WORKDAY_TYPE_RELAXED_BAND " + + "| targetScheduleDay=%s | tier3SampleCount=0 | decision=SKIP_TO_TOPN", + targetScheduleDay)); + } + + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_tier_decision", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | targetScheduleDay=%s | step=STEP4_TOP_NEAREST_ENTER " + + "| precedingBandTiers=ALL_SKIPPED_OR_INSUFFICIENT_SAMPLE", + targetScheduleDay)); + + // 6:营业额最接近 Top n + LOG.error("[排班模板][单日] 步骤4-Top最近营业额:全窗口候选数={} 取前 n={}", inWindow.size(), topNearestCap); + List top = topNearestByRevenue(inWindow, targetEstimatedRevenue, topNearestCap); + if (top.isEmpty()) { + LOG.error("[排班模板][单日] 目标日={} TopN 阶段无候选 → 失败", targetScheduleDay); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | targetScheduleDay=%s | step=STEP4_TOP_NEAREST_REVENUE " + + "| inWindowCandidates=%d | topNearestCap=%d | outcome=FAIL | bizMessage=%s", + targetScheduleDay, + inWindow.size(), + topNearestCap, + ScheduleTemplateSimilarDaysResult.MSG_NO_REFERENCE_DATA)); + return ScheduleTemplateSimilarDaysResult.failure(ScheduleTemplateSimilarDaysResult.MSG_NO_REFERENCE_DATA); + } + logResultReturn(targetScheduleDay, ScheduleTemplateSimilarDaysResult.SelectionTier.TOP_NEAREST_REVENUE, top); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "similar_days_revenue_outcome", + String.format( + "algo=ScheduleTemplateSimilarDaysAlgorithm | outcome=OK | tier=%s | path=TOP_NEAREST_AFTER_BAND_FAIL " + + "| similarCount=%d | similarDates=%s", + ScheduleTemplateSimilarDaysResult.SelectionTier.TOP_NEAREST_REVENUE, + top.size(), + SchedulingForTestCheckLog.formatDatesBrief(top, SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + return ScheduleTemplateSimilarDaysResult.ok(top, ScheduleTemplateSimilarDaysResult.SelectionTier.TOP_NEAREST_REVENUE); + } + + private static void logRevenueBand(String stepLabel, BigDecimal targetRevenue, BigDecimal lambda) { + if (targetRevenue == null || targetRevenue.signum() == 0) { + LOG.error("[排班模板][单日] {}:预估为 null/0 → 不按营业额过滤", stepLabel); + return; + } + BigDecimal low = targetRevenue.multiply(BigDecimal.ONE.subtract(lambda)); + BigDecimal high = targetRevenue.multiply(BigDecimal.ONE.add(lambda)); + LOG.error( + "[排班模板][单日] {}:R∈[预估×(1-λ), 预估×(1+λ)] 预估={} λ={} → [{}, {}]", + stepLabel, + targetRevenue.toPlainString(), + lambda.toPlainString(), + low.toPlainString(), + high.toPlainString()); + } + + private static void logResultReturn( + LocalDate target, ScheduleTemplateSimilarDaysResult.SelectionTier tier, List dates) { + LOG.error( + "[排班模板][单日] ========== 返回:目标日={} tier={} similarDates.size={} dates={}", + target, + tier, + dates.size(), + dates); + } + + /** + * 输入列表摘要(避免 90 天全量刷屏):前几条 day/revenue,条数多时用省略号。 + */ + private static String summarizeDayShiftRevenueStatVoList(List list) { + if (list == null) { + return "null"; + } + if (list.isEmpty()) { + return "[]"; + } + int n = list.size(); + int head = Math.min(5, n); + StringBuilder sb = new StringBuilder(128); + sb.append('['); + for (int i = 0; i < head; i++) { + if (i > 0) { + sb.append(", "); + } + DayShiftRevenueStatVo vo = list.get(i); + if (vo == null) { + sb.append("null"); + } else { + sb.append('{') + .append(vo.getDay()) + .append(',') + .append(vo.getRevenue() == null ? "null" : vo.getRevenue().toPlainString()) + .append('}'); + } + } + if (n > head) { + sb.append(", ...(共").append(n).append("条)"); + } + sb.append(']'); + return sb.toString(); + } + + private static int countSameDowInPool(List pool, DayOfWeek dow) { + int c = 0; + for (DayRow d : pool) { + if (d.date.getDayOfWeek() == dow) { + c++; + } + } + return c; + } + + private static int countSameWorkdayTypeInPool(List pool, boolean targetWeekend) { + int c = 0; + for (DayRow d : pool) { + if (isWeekend(d.date.getDayOfWeek()) == targetWeekend) { + c++; + } + } + return c; + } + + private static final class DayRow { + final LocalDate date; + final BigDecimal revenue; + + DayRow(LocalDate date, BigDecimal revenue) { + this.date = date; + this.revenue = revenue; + } + } + + private static List filterWindow( + List list, LocalDate windowStartInclusive, LocalDate targetExclusive) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + List out = new ArrayList<>(); + for (DayShiftRevenueStatVo vo : list) { + if (vo == null || vo.getDay() == null || vo.getDay().isEmpty()) { + continue; + } + LocalDate date = + AttendanceScheduleDayParse.tryParse(vo.getDay()).orElse(null); + if (date == null) { + continue; + } + if (!date.isBefore(windowStartInclusive) && date.isBefore(targetExclusive)) { + out.add(new DayRow(date, vo.getRevenue())); + } + } + return out; + } + + private static List filterSameDowAndBand( + List pool, DayOfWeek dow, BigDecimal targetRevenue, BigDecimal relativeBand) { + List out = new ArrayList<>(); + for (DayRow d : pool) { + if (d.date.getDayOfWeek() == dow && inRevenueBand(d.revenue, targetRevenue, relativeBand)) { + out.add(d); + } + } + return out; + } + + private static List filterSameWorkdayTypeAndBand( + List pool, boolean targetWeekend, BigDecimal targetRevenue, BigDecimal relativeBand) { + List out = new ArrayList<>(); + for (DayRow d : pool) { + if (isWeekend(d.date.getDayOfWeek()) == targetWeekend && inRevenueBand(d.revenue, targetRevenue, relativeBand)) { + out.add(d); + } + } + return out; + } + + /** + * 预估为 0 或 null 时:不按营业额过滤(该档内全部通过营业额条件)。 + */ + private static boolean inRevenueBand(BigDecimal historicalRevenue, BigDecimal targetRevenue, BigDecimal relativeHalfWidth) { + if (targetRevenue == null) { + throw new IllegalArgumentException("预估营业额不能为null"); + } + if (targetRevenue.signum() < 0) { + throw new IllegalArgumentException("预估营业额不能为负数: " + targetRevenue); + } + BigDecimal low = targetRevenue.multiply(BigDecimal.ONE.subtract(relativeHalfWidth)); + BigDecimal high = targetRevenue.multiply(BigDecimal.ONE.add(relativeHalfWidth)); + BigDecimal r = historicalRevenue == null ? BigDecimal.ZERO : historicalRevenue; + return r.compareTo(low) >= 0 && r.compareTo(high) <= 0; + } + + private static boolean isWeekend(DayOfWeek dow) { + return dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY; + } + + private static List datesOf(List rows) { + return rows.stream().map(d -> d.date).collect(Collectors.toList()); + } + + private static List topNearestByRevenue(List pool, BigDecimal targetRevenue, int n) { + BigDecimal center = targetRevenue == null ? BigDecimal.ZERO : targetRevenue; + List sorted = new ArrayList<>(pool); + sorted.sort(Comparator.comparing(d -> diffAbs(d.revenue, center))); + Set seen = new LinkedHashSet<>(); + List out = new ArrayList<>(); + for (DayRow d : sorted) { + if (seen.add(d.date)) { + out.add(d.date); + if (out.size() >= n) { + break; + } + } + } + return out; + } + + private static BigDecimal diffAbs(BigDecimal revenue, BigDecimal center) { + BigDecimal r = revenue == null ? BigDecimal.ZERO : revenue; + return r.subtract(center).abs().setScale(2, RoundingMode.HALF_UP); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysResult.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysResult.java new file mode 100644 index 0000000..a35cb12 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ScheduleTemplateSimilarDaysResult.java @@ -0,0 +1,123 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * 按营业额与星期规则筛选出的「排班模板」参考历史日结果。 + */ +public final class ScheduleTemplateSimilarDaysResult implements Serializable { + + public static final String MSG_NO_REFERENCE_DATA = "无参考数据,无法智能排班。"; + + /** + * 「规律模式」分支 (2):目标日前连续自然日窗口长度(与 {@link SchedulePatternSimilarDaysAlgorithm} 一致)。 + */ + public static final int PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW = 7; + + /** + * 规律模式兜底失败提示:{@code n = PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW - effectiveDaysWithShiftInLastWindow}, + * 即「请先手动补满」的天数口径。 + */ + public static String msgInsufficientHistoryForPatternSchedule(int effectiveDaysWithShiftInLastWindow) { + int clipped = Math.max(0, effectiveDaysWithShiftInLastWindow); + int capped = Math.min(PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW, clipped); + int needMoreNaturalDays = PATTERN_SCHEDULE_LAST_NATURAL_DAY_WINDOW - capped; + return String.format( + "提示:当前无历史排班信息,AI无参考数据。请先手动排班至少 %d 天后,再使用智能排班功能。", + needMoreNaturalDays); + } + + /** 识别 {@link #msgInsufficientHistoryForPatternSchedule(int)} 产出的文案(用于不完整说明聚合)。 */ + public static boolean isInsufficientHistoryForPatternScheduleMessage(String message) { + return message != null && message.startsWith("提示:当前无历史排班信息,AI无参考数据。"); + } + + public enum SelectionTier { + /** 同星期几 + 营业额 ±10%,样本 ≥10 */ + SAME_WEEKDAY_STRICT_BAND, + /** 同星期几 + 营业额 ±15% */ + SAME_WEEKDAY_RELAXED_BAND, + /** 同工作日/周末类型 + 营业额 ±15% */ + SAME_WORKDAY_TYPE_RELAXED_BAND, + /** 营业额最接近 Top n */ + TOP_NEAREST_REVENUE, + /** 无数据分析营业额:近 30 自然日内「同星期 + 有效班次列表」的全部参考日 */ + PATTERN_SAME_WEEKDAY_30D, + /** 无数据分析营业额:目标日前连续 7 个自然日均须有效班次,采用这 7 日内全部参考日 */ + PATTERN_LAST_7_NATURAL_GE7, + /** 未选到 */ + NONE + } + + private final boolean success; + private final List similarDates; + private final SelectionTier tier; + private final String message; + + public ScheduleTemplateSimilarDaysResult( + boolean success, List similarDates, SelectionTier tier, String message) { + this.success = success; + this.similarDates = + similarDates == null ? Collections.emptyList() : Collections.unmodifiableList(similarDates); + this.tier = tier == null ? SelectionTier.NONE : tier; + this.message = message == null ? "" : message; + } + + public static ScheduleTemplateSimilarDaysResult failure(String message) { + return new ScheduleTemplateSimilarDaysResult(false, Collections.emptyList(), SelectionTier.NONE, message); + } + + public static ScheduleTemplateSimilarDaysResult ok(List dates, SelectionTier tier) { + return new ScheduleTemplateSimilarDaysResult(true, dates, tier, ""); + } + + public boolean isSuccess() { + return success; + } + + public List getSimilarDates() { + return similarDates; + } + + public SelectionTier getTier() { + return tier; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "ScheduleTemplateSimilarDaysResult{" + + "success=" + success + + ", tier=" + tier + + ", count=" + similarDates.size() + + ", message='" + message + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ScheduleTemplateSimilarDaysResult that = (ScheduleTemplateSimilarDaysResult) o; + return success == that.success + && tier == that.tier + && Objects.equals(similarDates, that.similarDates) + && Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(success, similarDates, tier, message); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingForTestCheckLog.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingForTestCheckLog.java new file mode 100644 index 0000000..684f44f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingForTestCheckLog.java @@ -0,0 +1,92 @@ +package jnpf.attendance.schedule; + +import org.slf4j.Logger; + +import java.time.LocalDate; +import java.util.List; + +/** + * 智能排班算法「可测试观测」日志:统一前缀 {@value #MARKER},供测试/实施在日志中 grep 穿透黑盒决策。 + * + *

每条 INFO 日志均带 {@code testIssue},与测试侧「三大不可验证痛点」对齐: + * + *

    + *
  • {@link #TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES 问题1} — 相似历史日期筛选策略(步骤触发、样本集、兜底);
  • + *
  • {@link #TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX 问题2} — 半小时岗位需求矩阵(波动阈值、中位数/P75、追溯);
  • + *
  • {@link #TEST_ISSUE_3_STAFF_ASSIGNMENT 问题3} — 人员分配(硬/软约束、通岗补缺、固定班放松链等)。
  • + *
+ * + *

说明:线上若仅打印 ERROR,本类统一用 {@link org.slf4j.Logger#error} 输出,便于验收/测试穿透;体量仍大时请用 traceId/排班单次开关过滤。 + */ +public final class SchedulingForTestCheckLog { + + /** 测试检索标签(与现有 {@link AutoSchedulePipelineLog#MARKER} 区分开)。 */ + public static final String MARKER = "[for_test_check]"; + + /** + * 测试反馈「问题1」:相似历史日期筛选不可观测 — 日志值 {@value}(检索:{@code testIssue=1})。 + */ + public static final String TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES = "1"; + + /** + * 测试反馈「问题2」:半小时岗位需求矩阵生成不可观测 — 日志值 {@value}(检索:{@code testIssue=2})。 + */ + public static final String TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX = "2"; + + /** + * 测试反馈「问题3」:人员分配决策链不可观测 — 日志值 {@value}(检索:{@code testIssue=3})。 + */ + public static final String TEST_ISSUE_3_STAFF_ASSIGNMENT = "3"; + + /** 单行内相似日列表等元素过多时的默认截断条数(避免单次日志爆表)。 */ + public static final int DEFAULT_DATE_CAP = 48; + + private SchedulingForTestCheckLog() {} + + /** + * @param testIssue 对应测试问题编号,见 {@link #TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES} 等常量。 + * @param subPhase 同一场景下的子节点/子阶段,便于串联一次排班流水线。 + */ + public static void info(Logger log, String testIssue, String subPhase, String detail) { + if (log == null || !log.isErrorEnabled()) { + return; + } + log.error("{} testIssue={} subPhase={} | {}", MARKER, testIssue, subPhase, detail); + } + + /** + * 将日期列表格式化为短字符串,超出 {@code cap} 条时追加 {@code …(+N)}。 + */ + public static String formatDatesBrief(List dates, int cap) { + if (dates == null || dates.isEmpty()) { + return "[]"; + } + int n = dates.size(); + int show = Math.min(cap, n); + StringBuilder sb = new StringBuilder(n * 14); + sb.append('['); + for (int i = 0; i < show; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(dates.get(i)); + } + sb.append(']'); + if (n > cap) { + sb.append("…(+").append(n - cap).append(')'); + } + return sb.toString(); + } + + /** 单行化多行表格:便于采集端按行 ingest。 */ + public static String singleLineTsv(String tsvTable, int maxChars) { + if (tsvTable == null || tsvTable.isEmpty()) { + return ""; + } + String ln = tsvTable.replace('\n', '¤').replace('\t', '`'); + if (maxChars > 0 && ln.length() > maxChars) { + return ln.substring(0, maxChars) + "…(truncated_chars=" + maxChars + ')'; + } + return ln; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingSimilarDaysMode.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingSimilarDaysMode.java new file mode 100644 index 0000000..61a00ca --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SchedulingSimilarDaysMode.java @@ -0,0 +1,48 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 智能排班「相似历史日」走营业额模型还是「按历史排班规律」模型。 + * + *

当租户关闭数据分析或未上传历史营业额时,走 {@link SchedulePatternSimilarDaysAlgorithm}; + * 配置项优先于启发式推断,见 {@link jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo}。 + */ +public final class SchedulingSimilarDaysMode { + + private SchedulingSimilarDaysMode() {} + + /** + * @param intelligentSchedulingHistoricalRevenueAvailable {@code Boolean.FALSE} + * 强制按「无历史营业额 / 仅用排班规律」选相似日;{@code null/true} 时若历史 VO 营业额均空或 0 + * 则降级为规律模式。 + */ + public static boolean shouldUseSchedulingPatternSimilarDays( + Boolean intelligentSchedulingHistoricalRevenueAvailable, + List dayShiftHistory) { + if (Boolean.FALSE.equals(intelligentSchedulingHistoricalRevenueAvailable)) { + return true; + } + return allHistoricalRevenueNullOrZero(dayShiftHistory); + } + + /** 历史中无任何「非零营业额」的记录时视为无可用历史营业额样本。VO 为空也视为不可用。 */ + public static boolean allHistoricalRevenueNullOrZero(List list) { + if (list == null || list.isEmpty()) { + return true; + } + for (DayShiftRevenueStatVo vo : list) { + if (vo == null) { + continue; + } + BigDecimal r = vo.getRevenue(); + if (r != null && r.signum() != 0) { + return false; + } + } + return true; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftHalfHourNormalizer.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftHalfHourNormalizer.java new file mode 100644 index 0000000..6944af8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftHalfHourNormalizer.java @@ -0,0 +1,62 @@ +package jnpf.attendance.schedule; + +import java.time.LocalTime; + +/** + * 第六步:将班次起止时间统一到半小时粒度(与需求文档示例一致的「最近半小时」取整)。 + */ +public final class ShiftHalfHourNormalizer { + + private ShiftHalfHourNormalizer() {} + + /** 将分钟数(0~1440+)就近取整到半小时刻度(0,30,60,…)。 */ + public static int roundMinuteToNearestHalfHour(int minuteOfDay) { + long m = minuteOfDay; + int q = (int) Math.round(m / 30.0); + if (q < 0) { + q = 0; + } + if (q > 48) { + q = 48; + } + return q * 30; + } + + /** + * 对「左闭右开」区间 [start, end) 做半小时取整;若取整后无效(时长为 0),返回 {@code null}。 + *

+ * 仅限已保证同日且 {@code end.isAfter(start)} 的片段。历史夜班(同日字段上 + * {@code end}≤{@code start})不得在拆段之前传入:请先用与 + * {@link CommonFixedShiftDiscovery#splitToSameDayMinuteFragments(LocalDate, jnpf.model.attendance.vo.ShiftPostStatVo)} + * 等价的方式拆成「当日尾」「次日首」两段再归一。(产品口径:不要求独立的「次日」字段;拆分在链路内自动完成。) + */ + public static int[] normalizeSameDayStartEndExclusiveMinutes(LocalTime start, LocalTime end) { + if (start == null || end == null || !end.isAfter(start)) { + return null; + } + return normalizeMinuteRange( + HalfHourSlotLabel.localTimeToMinute(start), HalfHourSlotLabel.localTimeToMinute(end)); + } + + /** + * 对「左闭右开」分钟区间 [startMin, endMin) 做半小时取整;若无效返回 {@code null}。 + */ + public static int[] normalizeMinuteRange(int startMinuteInclusive, int endMinuteExclusive) { + int sm = roundMinuteToNearestHalfHour(startMinuteInclusive); + int em = roundMinuteToNearestHalfHour(endMinuteExclusive); + if (em <= sm) { + em = sm + 30; + } + if (em > 24 * 60) { + em = 24 * 60; + } + if (em <= sm) { + return null; + } + return new int[] {sm, em}; + } + + public static String toLabel(int startInclusiveMinute, int endExclusiveMinute) { + return HalfHourSlotLabel.formatMinuteRange(startInclusiveMinute, endExclusiveMinute); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResult.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResult.java new file mode 100644 index 0000000..4699836 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResult.java @@ -0,0 +1,458 @@ +package jnpf.attendance.schedule; + +import lombok.Getter; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 最终排班结果:按班次维度与按员工维度两种视图;另附不完整说明(如日级班段剔除、岗位未排满原因)。 + */ +public final class ShiftPlanAssignmentResult implements Serializable { + + /** 当日班段均未匹配考勤组班次时段并已移除(AutoSchedule matcher 粒度:按日汇总剔除块数)。 */ + public static final String INCOMPLETE_ATTEND_SHIFT_BLOCKS_DROPPED = "ATTEND_SHIFT_BLOCKS_DROPPED"; + + /** + * 相似日第三步判定成功,但召回到的全部历史日在半小时展开后均无可用班次岗位数据(班次结构缺失或无法解析)。 + * 文案对齐 intelligent-schedule-product-constraints_reply §2.4。 + */ + public static final String INCOMPLETE_SIMILAR_HISTORY_NO_SHIFT_DATA = "SIMILAR_HISTORY_NO_SHIFT_DATA"; + + /** + * 「无营业额 / 仅用排班规律」相似日中,近窗有效排班不足(产品:先完成若干天手动排班后再智能排班),见 + * {@link SchedulePatternSimilarDaysAlgorithm}。 + */ + public static final String INCOMPLETE_PATTERN_HISTORY_INSUFFICIENT = "PATTERN_HISTORY_INSUFFICIENT"; + + /** 过滤后当日该岗无专岗/通岗可排候选人。 */ + public static final String INCOMPLETE_NO_CANDIDATE = "NO_CANDIDATE"; + + /** 候选人与工作站岗位绑定名单无交集。 */ + public static final String INCOMPLETE_WORKSTATION_POOL_EMPTY = "WORKSTATION_POOL_EMPTY"; + + /** 历史需求中的岗位在主数据已无记录(产品与文档 intelligent-schedule §7.4),请手动处理。 */ + public static final String INCOMPLETE_HISTORICAL_POST_MISSING_MASTER = "HISTORICAL_POST_NOT_IN_MASTER"; + + /** 固定班「必须」规则下候选无人可用(本班段该岗未排任一人员时常为此类)。 */ + public static final String INCOMPLETE_FIXED_MUST_BLOCK = "FIXED_MUST_BLOCK"; + + /** 第九步硬约束、固定尽量项、同人块内占用、时段重叠等综合原因导致未凑满需求。 */ + public static final String INCOMPLETE_RULES_OR_CONFLICT = "RULES_OR_CONFLICT"; + + private final List byShift; + private final List byEmployee; + private final List incompleteScheduleReasons; + + public ShiftPlanAssignmentResult( + List byShift, List byEmployee) { + this(byShift, byEmployee, Collections.emptyList()); + } + + public ShiftPlanAssignmentResult( + List byShift, + List byEmployee, + List incompleteScheduleReasons) { + this.byShift = + byShift == null + ? Collections.emptyList() + : Collections.unmodifiableList(byShift); + this.byEmployee = + byEmployee == null + ? Collections.emptyList() + : Collections.unmodifiableList(byEmployee); + this.incompleteScheduleReasons = + incompleteScheduleReasons == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(incompleteScheduleReasons)); + } + + /** + * 将前置说明(如考勤班次匹配剔除)与用户分配阶段说明串联为新的结果视图(班次与人不变)。 + */ + public static ShiftPlanAssignmentResult prependIncompleteReasons( + List prefix, ShiftPlanAssignmentResult result) { + Objects.requireNonNull(result, "result"); + List p = prefix == null ? Collections.emptyList() : prefix; + if (p.isEmpty()) { + return result; + } + List merged = new ArrayList<>(p.size() + result.incompleteScheduleReasons.size()); + merged.addAll(p); + merged.addAll(result.incompleteScheduleReasons); + return new ShiftPlanAssignmentResult(result.byShift, result.byEmployee, merged); + } + + public List getByShift() { + return byShift; + } + + public List getByEmployee() { + return byEmployee; + } + + /** + * 业务侧不完整说明:每项对应某日(及可选的具体班段/岗位)的人类可读描述与机器可读 {@link #getReasonCode()}。 + */ + public List getIncompleteScheduleReasons() { + return incompleteScheduleReasons; + } + + /** 是否存在排班不完整类说明条目(考勤剔除、岗位未满等);有则应对用户或运维展示解释文本。 */ + public boolean hasIncompleteScheduleExplanations() { + return !incompleteScheduleReasons.isEmpty(); + } + + /** 已排班总人次(按岗位需求条目累计)。 */ + public int countAssignedStaff() { + int n = 0; + for (AssignedShiftBlockView shift : byShift) { + for (AssignedPostStaffView post : shift.getPostAssignments()) { + n += post.getStaff().size(); + } + } + return n; + } + + /** 岗位需求总人数。 */ + public int countTotalNeed() { + int n = 0; + for (AssignedShiftBlockView shift : byShift) { + for (AssignedPostStaffView post : shift.getPostAssignments()) { + n += post.getNeedCount(); + } + } + return n; + } + + /** 不完整说明可读文本(空列表返回固定提示)。 */ + public String formatIncompleteScheduleReasonsText() { + if (incompleteScheduleReasons.isEmpty()) { + return "【不完整说明】(无)"; + } + StringBuilder sb = new StringBuilder(256); + sb.append("【不完整说明】\n"); + for (IncompleteScheduleReason r : incompleteScheduleReasons) { + sb.append(r.getScheduleDay()) + .append(" [") + .append(r.getReasonCode()) + .append("] "); + if (r.getTimeRangeText() != null && !r.getTimeRangeText().isEmpty()) { + sb.append(r.getTimeRangeText()).append(' '); + } + if (r.getPostId() != null && !r.getPostId().isEmpty()) { + sb.append("岗位=").append(r.getPostId()).append(' '); + } + sb.append(r.getMessage()).append('\n'); + } + return sb.toString(); + } + + /** 按班次维度的可读文本(a)。 */ + public String formatByShiftText() { + if (byShift.isEmpty()) { + return "(无班次排班结果)"; + } + StringBuilder sb = new StringBuilder(512); + sb.append("【按班次】\n"); + for (AssignedShiftBlockView shift : byShift) { + String kind = shift.isFixedScheduling() ? "固定" : "划线"; + String shiftId = shift.getShiftId() == null || shift.getShiftId().isEmpty() ? "-" : shift.getShiftId(); + for (AssignedPostStaffView post : shift.getPostAssignments()) { + sb.append(shift.getScheduleDay()) + .append(' ') + .append(shift.getTimeRangeText()) + .append(" [") + .append(kind) + .append(" shiftId=") + .append(shiftId) + .append("] 岗位=") + .append(post.getPostId()); + String pn = post.getPositionName(); + if (pn != null && !pn.isEmpty()) { + sb.append('(').append(pn).append(')'); + } + sb.append(" 需求=") + .append(post.getNeedCount()) + .append(" 已排=") + .append(post.getStaff().size()) + .append(": "); + sb.append(formatStaffList(post.getStaff())); + sb.append('\n'); + } + } + return sb.toString(); + } + + /** 按员工维度的可读文本(b)。 */ + public String formatByEmployeeText() { + if (byEmployee.isEmpty()) { + return "(无员工排班结果)"; + } + StringBuilder sb = new StringBuilder(512); + sb.append("【按员工】\n"); + for (AssignedEmployeeView emp : byEmployee) { + sb.append(emp.getUserName()) + .append('(') + .append(emp.getUserId()) + .append("): "); + if (emp.getShifts().isEmpty()) { + sb.append("(无班次)\n"); + continue; + } + String shifts = + emp.getShifts().stream() + .map( + s -> { + String kind = s.isFixedScheduling() ? "固定" : "划线"; + String sid = + s.getShiftId() == null || s.getShiftId().isEmpty() + ? "-" + : s.getShiftId(); + return s.getScheduleDay() + + " " + + s.getTimeRangeText() + + " " + + s.getPostId() + + '/' + + kind + + "/shiftId=" + + sid; + }) + .collect(Collectors.joining("; ")); + sb.append(shifts).append('\n'); + } + return sb.toString(); + } + + /** 完整排班结果(摘要 + 按班次 + 按员工)。 */ + public String formatPipelineResultText() { + StringBuilder sb = new StringBuilder(1024); + sb.append("========== 智能排班 Pipeline 结果 ==========\n"); + sb.append("班次块数=") + .append(byShift.size()) + .append(" 员工数=") + .append(byEmployee.size()) + .append(" 已排人次=") + .append(countAssignedStaff()) + .append(" 需求人次=") + .append(countTotalNeed()) + .append('\n'); + sb.append(formatIncompleteScheduleReasonsText()).append('\n'); + sb.append(formatByShiftText()).append('\n'); + sb.append(formatByEmployeeText()); + sb.append("========== 排班结果结束 =========="); + return sb.toString(); + } + + private static String formatStaffList(List staff) { + if (staff == null || staff.isEmpty()) { + return "(空)"; + } + return staff.stream() + .map( + s -> { + String tag = s.isExtra() ? "通岗" : "专岗"; + String name = + s.getUserName() == null || s.getUserName().isEmpty() + ? s.getUserId() + : s.getUserName(); + return name + '(' + s.getUserId() + ")[" + tag + ']'; + }) + .collect(Collectors.joining("; ")); + } + + /** 按班次块 + 岗位:班次信息及已排员工列表。 */ + public static final class AssignedShiftBlockView implements Serializable { + private final LocalDate scheduleDay; + private final String shiftId; + private final String timeRangeText; + private final boolean fixedScheduling; + private final List postAssignments; + + public AssignedShiftBlockView( + LocalDate scheduleDay, + String shiftId, + String timeRangeText, + boolean fixedScheduling, + List postAssignments) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.shiftId = shiftId; + this.timeRangeText = timeRangeText == null ? "" : timeRangeText; + this.fixedScheduling = fixedScheduling; + this.postAssignments = + postAssignments == null + ? Collections.emptyList() + : Collections.unmodifiableList(postAssignments); + } + + public LocalDate getScheduleDay() { + return scheduleDay; + } + + public String getShiftId() { + return shiftId; + } + + public String getTimeRangeText() { + return timeRangeText; + } + + public boolean isFixedScheduling() { + return fixedScheduling; + } + + public List getPostAssignments() { + return postAssignments; + } + } + + public static final class AssignedPostStaffView implements Serializable { + private final String postId; + /** 与 {@link jnpf.model.attendance.vo.WorkstationWithUsersVo#getPositionName()} 对齐(按岗位首次匹配工作站)。 */ + private final String positionName; + private final int needCount; + private final List staff; + + /** + * 兼容旧调用:不传岗位名称时 {@link #getPositionName()} 为空串。 + */ + public AssignedPostStaffView(String postId, int needCount, List staff) { + this(postId, "", needCount, staff); + } + + public AssignedPostStaffView( + String postId, String positionName, int needCount, List staff) { + this.postId = postId == null ? "" : postId; + this.positionName = positionName == null ? "" : positionName.trim(); + this.needCount = Math.max(0, needCount); + this.staff = staff == null ? Collections.emptyList() : Collections.unmodifiableList(staff); + } + + public String getPostId() { + return postId; + } + + public String getPositionName() { + return positionName; + } + + public int getNeedCount() { + return needCount; + } + + public List getStaff() { + return staff; + } + } + + public static final class AssignedStaffRef implements Serializable { + private final String userId; + private final String userName; + private final boolean extra; + + public AssignedStaffRef(String userId, String userName, boolean extra) { + this.userId = userId == null ? "" : userId; + this.userName = userName == null ? "" : userName; + this.extra = extra; + } + + public String getUserId() { + return userId; + } + + public String getUserName() { + return userName; + } + + public boolean isExtra() { + return extra; + } + } + + /** 按员工:员工信息及其班次列表。 */ + @Getter + public static final class AssignedEmployeeView implements Serializable { + private final String userId; + private final String userName; + private final List shifts; + + public AssignedEmployeeView(String userId, String userName, List shifts) { + this.userId = userId == null ? "" : userId; + this.userName = userName == null ? "" : userName; + this.shifts = shifts == null ? Collections.emptyList() : Collections.unmodifiableList(shifts); + } + + } + + @Getter + public static final class AssignedShiftRef implements Serializable { + private final LocalDate scheduleDay; + private final String shiftId; + private final String timeRangeText; + private final boolean fixedScheduling; + private final String postId; + + public AssignedShiftRef( + LocalDate scheduleDay, + String shiftId, + String timeRangeText, + boolean fixedScheduling, + String postId) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.shiftId = shiftId; + this.timeRangeText = timeRangeText == null ? "" : timeRangeText; + this.fixedScheduling = fixedScheduling; + this.postId = postId == null ? "" : postId; + } + + } + + /** + * 单日或单班段维度的排班不完整说明;{@link #getTimeRangeText()}、{@link #getPostId()} 为空串时表示日级汇总。 + */ + @Getter + public static final class IncompleteScheduleReason implements Serializable { + private final LocalDate scheduleDay; + /** + * -- GETTER -- + * 班次块时段文案;日级条目可为空串。 + */ + private final String timeRangeText; + /** + * -- GETTER -- + * 岗位 id;日级或非岗位类说明可为空串。 + */ + private final String postId; + /** + * -- GETTER -- + * 与类上 + * 常量对齐。 + */ + private final String reasonCode; + /** + * -- GETTER -- + * 简短中文叙述,可直接展示给用户。 + */ + private final String message; + + public IncompleteScheduleReason( + LocalDate scheduleDay, + String timeRangeText, + String postId, + String reasonCode, + String message) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.timeRangeText = timeRangeText == null ? "" : timeRangeText; + this.postId = postId == null ? "" : postId.trim(); + this.reasonCode = reasonCode == null ? "" : reasonCode.trim(); + this.message = message == null ? "" : message.trim(); + } + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResultLogger.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResultLogger.java new file mode 100644 index 0000000..5bfef86 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAssignmentResultLogger.java @@ -0,0 +1,101 @@ +package jnpf.attendance.schedule; + +import org.slf4j.Logger; + +/** + * 将 {@link ShiftPlanAssignmentResult} / {@link ShiftPlanPickSnapshot} 写入日志。 + * + *

线上(仅 ERROR 可见时):里程碑与 shortage / incomplete 说明均经 {@link AutoSchedulePipelineLog} 以 ERROR 输出;明细全文亦用 ERROR(体量大时请依赖 logger 过滤)。 + */ +public final class ShiftPlanAssignmentResultLogger { + + private ShiftPlanAssignmentResultLogger() {} + + /** @see #logPickSnap(Logger, ShiftPlanPickSnapshot) */ + public static void logInfo(Logger log, ShiftPlanAssignmentResult result) { + logPickSnap(log, result); + } + + public static void logPickSnap(Logger log, ShiftPlanAssignmentResult result) { + logPickSnap(log, ShiftPlanPickSnapshot.from(result)); + } + + public static void logPickSnap(Logger log, ShiftPlanPickSnapshot pickSnap) { + if (log == null) { + return; + } + if (pickSnap == null) { + return; + } + int noteCount = pickSnap.getIncompleteScheduleReasons().size(); + if (pickSnap.isEmpty()) { + if (log.isErrorEnabled()) { + if (noteCount > 0) { + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_PICK_SUMMARY, + "EMPTY_ROSTER | shift blocks were all dropped or produced no assignment | incompleteNotes=" + + noteCount); + } else { + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_PICK_SUMMARY, + "EMPTY | no roster rows after staff assignment"); + } + } + } else { + int picked = pickSnap.countAssignedStaff(); + int need = pickSnap.countTotalNeed(); + if (log.isErrorEnabled()) { + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_PICK_SUMMARY, + String.format( + "shiftBlocks=%d employees=%d picked=%d need=%d complete=%s incompleteNotes=%d", + pickSnap.getByShift().size(), + pickSnap.getByEmployee().size(), + picked, + need, + need <= 0 || picked >= need, + noteCount)); + } + if (picked < need) { + AutoSchedulePipelineLog.warn( + log, + AutoSchedulePipelineLog.PHASE_PICK_SUMMARY, + "SHORTAGE deficit=" + + (need - picked) + + " (enable ERROR for AutoScheduleService / ShiftPlanAssignmentResultLogger to inspect)"); + } + if (noteCount > 0) { + AutoSchedulePipelineLog.warn( + log, + AutoSchedulePipelineLog.PHASE_PICK_SUMMARY, + "INCOMPLETE explanatory notes=" + + noteCount + + " (inspect ShiftPlanAssignmentResult.getIncompleteScheduleReasons or pickSnap ERROR detail)"); + } + } + if (!log.isErrorEnabled()) { + return; + } + log.error( + "{} pickSnap detail:\n{}", + AutoSchedulePipelineLog.MARKER, + pickSnap.formatPickSnapText()); + } + + /** + * 完整拼接文本(与 {@link #logPickSnap} 末尾明细一致);在仅开启 ERROR 时用于补打大块详情。 + */ + public static void logDebug(Logger log, ShiftPlanAssignmentResult result) { + if (log == null || !log.isErrorEnabled()) { + return; + } + ShiftPlanPickSnapshot pickSnap = ShiftPlanPickSnapshot.from(result); + log.error( + "{} pickSnap detail:\n{}", + AutoSchedulePipelineLog.MARKER, + pickSnap.formatPickSnapText()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAttendanceShiftIdResolver.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAttendanceShiftIdResolver.java new file mode 100644 index 0000000..c840598 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanAttendanceShiftIdResolver.java @@ -0,0 +1,80 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.ShiftPeriodVo; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 为 {@link ShiftPlanBlock} 绑定考勤组班次 id,或剔除无法匹配的班次块。 + * + *

规则与 {@link AttendanceGroupShiftPeriodMatcher} 一致,并在 {@linkplain ShiftPlanBlock#getHistoricalAttendanceShiftId()} 存在且与某考勤班次 {@linkplain ShiftPeriodVo#getId()} 相同时, + * 先于时段阈值绑定该班次 id。 + *

    + *
  • 「可用」:覆盖度、有效率均为 1(计划时段与考勤班次完全对齐)。
  • + *
  • 否则:历史覆盖度 = 重叠 / 计划时长 > 阈值,且候选有效率 = 重叠 / 考勤班次总时长 ≥ 阈值(默认 70%、60%)。
  • + *
  • 无满足项则从当日计划中移除该块。
  • + *
+ */ +public final class ShiftPlanAttendanceShiftIdResolver { + + private ShiftPlanAttendanceShiftIdResolver() {} + + /** + * @param shiftPlanByScheduleDay 按日完整班次计划(通常来自 {@link HalfHourDemandBasedShiftPlanBuilder}) + * @param shiftPeriodVoList 考勤组班次配置;{@code null} 表示不处理(原样返回);空列表则清空各日块 + * @return 仅保留能匹配到考勤班次的块,并写入 {@link ShiftPlanBlock#getShiftId()} + */ + public static Map> assignGroupShiftIdsOrRemoveUnmatched( + Map> shiftPlanByScheduleDay, + List shiftPeriodVoList, + AttendanceGroupShiftMatchConfig cfg, + Random random) { + if (shiftPlanByScheduleDay == null || shiftPlanByScheduleDay.isEmpty()) { + return shiftPlanByScheduleDay == null + ? Collections.emptyMap() + : Collections.unmodifiableMap(new LinkedHashMap<>()); + } + if (shiftPeriodVoList == null) { + return Collections.unmodifiableMap(new LinkedHashMap<>(shiftPlanByScheduleDay)); + } + if (shiftPeriodVoList.isEmpty()) { + Map> empty = new LinkedHashMap<>(); + for (LocalDate day : shiftPlanByScheduleDay.keySet()) { + empty.put(day, Collections.emptyList()); + } + return Collections.unmodifiableMap(empty); + } + AttendanceGroupShiftMatchConfig c = cfg == null ? AttendanceGroupShiftMatchConfig.defaults() : cfg; + Random rnd = random == null ? ThreadLocalRandom.current() : random; + Map> out = new LinkedHashMap<>(); + for (Map.Entry> e : shiftPlanByScheduleDay.entrySet()) { + List kept = new ArrayList<>(); + for (ShiftPlanBlock block : emptyIfNull(e.getValue())) { + Optional match = + AttendanceGroupShiftPeriodMatcher.pickBestMatchForTimeWindow( + block.getStartMinuteInclusive(), + block.getEndMinuteExclusive(), + shiftPeriodVoList, + c, + rnd, + block.getHistoricalAttendanceShiftId()); + match.ifPresent( + r -> kept.add(block.withShiftId(r.getGroupShiftPeriodId()))); + } + out.put(e.getKey(), Collections.unmodifiableList(kept)); + } + return Collections.unmodifiableMap(out); + } + + private static List emptyIfNull(List list) { + return list == null ? Collections.emptyList() : list; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlock.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlock.java new file mode 100644 index 0000000..6a8450b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlock.java @@ -0,0 +1,169 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * 由半小时需求矩阵整理出的「完整班次」一段:起止时间、是否固定排班、各岗位需求人数。 + * + *

时间语义与 {@link HalfHourSlotLabel} 一致:左闭右开 {@code [startMinuteInclusive, endMinuteExclusive)}, + * 且须满足 {@link #isValidTimeWindow(int, int)}(自然日内 0~1440 分钟坐标,{@code endExclusive} 可至 1440 表示到当日 24:00)。 + */ +public final class ShiftPlanBlock implements Serializable { + + /** 当日分钟上界:右端点可取 1440 表示 24:00(与 {@link HalfHourSlotLabel} 语义一致)。 */ + public static final int MAX_MINUTE_EXCLUSIVE = 24 * 60; + + private final LocalDate scheduleDay; + private final int startMinuteInclusive; + private final int endMinuteExclusive; + /** 展示用起止,如 {@code 9:00-14:00}(与 {@link #getTimeRangeLabel()} 的语义一致,小时不补零)。 */ + private final String timeRangeText; + private final boolean fixedScheduling; + /** 考勤组 {@link jnpf.model.attendance.vo.ShiftPeriodVo#getId()},经匹配后写入;未匹配前为 {@code null}。 */ + private final String shiftId; + /** + * 来自历史 {@link jnpf.model.attendance.vo.ShiftPostStatVo#getShiftId()}(仅草稿固定班链路携带);若在配置中与某 {@link ShiftPeriodVo} 同 id, + * 则 {@linkplain ShiftPlanAttendanceShiftIdResolver} 可先于时段模糊匹配绑定该班次。 + */ + private final String historicalAttendanceShiftId; + private final List postNeeds; + + public ShiftPlanBlock( + LocalDate scheduleDay, + int startMinuteInclusive, + int endMinuteExclusive, + boolean fixedScheduling, + List postNeeds) { + this(scheduleDay, startMinuteInclusive, endMinuteExclusive, fixedScheduling, postNeeds, null, null); + } + + public ShiftPlanBlock( + LocalDate scheduleDay, + int startMinuteInclusive, + int endMinuteExclusive, + boolean fixedScheduling, + List postNeeds, + String shiftId) { + this(scheduleDay, startMinuteInclusive, endMinuteExclusive, fixedScheduling, postNeeds, shiftId, null); + } + + public ShiftPlanBlock( + LocalDate scheduleDay, + int startMinuteInclusive, + int endMinuteExclusive, + boolean fixedScheduling, + List postNeeds, + String shiftId, + String historicalAttendanceShiftId) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + validateTimeWindow(startMinuteInclusive, endMinuteExclusive); + this.startMinuteInclusive = startMinuteInclusive; + this.endMinuteExclusive = endMinuteExclusive; + this.timeRangeText = + HalfHourSlotLabel.formatMinuteRangeDisplay(startMinuteInclusive, endMinuteExclusive); + this.fixedScheduling = fixedScheduling; + this.shiftId = shiftId == null || shiftId.isEmpty() ? null : shiftId; + this.historicalAttendanceShiftId = + historicalAttendanceShiftId == null || historicalAttendanceShiftId.isBlank() + ? null + : historicalAttendanceShiftId.trim(); + this.postNeeds = + postNeeds == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(postNeeds)); + } + + /** 写入考勤组班次 id,其余字段不变。 */ + public ShiftPlanBlock withShiftId(String shiftId) { + return new ShiftPlanBlock( + scheduleDay, + startMinuteInclusive, + endMinuteExclusive, + fixedScheduling, + postNeeds, + shiftId, + historicalAttendanceShiftId); + } + + /** + * 自然日分钟坐标下的合法班段(左闭右开):{@code 0 ≤ start < end ≤ 1440}。 + * + *

跨自然日的夜班须拆到两日或多段,不得用「end < start」在同一 {@link LocalDate} 上编码。 + */ + public static boolean isValidTimeWindow(int startMinuteInclusive, int endMinuteExclusive) { + return startMinuteInclusive >= 0 + && startMinuteInclusive < MAX_MINUTE_EXCLUSIVE + && endMinuteExclusive > startMinuteInclusive + && endMinuteExclusive <= MAX_MINUTE_EXCLUSIVE; + } + + private static void validateTimeWindow(int startMinuteInclusive, int endMinuteExclusive) { + if (!isValidTimeWindow(startMinuteInclusive, endMinuteExclusive)) { + throw new IllegalArgumentException( + "invalid shift window [start,end)=[" + + startMinuteInclusive + + "," + + endMinuteExclusive + + "); require 0 ≤ start < " + + MAX_MINUTE_EXCLUSIVE + + ", start < end ≤ " + + MAX_MINUTE_EXCLUSIVE); + } + } + + public LocalDate getScheduleDay() { + return scheduleDay; + } + + public int getStartMinuteInclusive() { + return startMinuteInclusive; + } + + public int getEndMinuteExclusive() { + return endMinuteExclusive; + } + + /** + * 班次开始—结束的字符串,如 {@code 9:00-14:00};与分钟字段同义,左闭右开区间的右端点为「结束时刻」展示值。 + */ + public String getTimeRangeText() { + return timeRangeText; + } + + public boolean isFixedScheduling() { + return fixedScheduling; + } + + public String getShiftId() { + return shiftId; + } + + public String getHistoricalAttendanceShiftId() { + return historicalAttendanceShiftId; + } + + public List getPostNeeds() { + return postNeeds; + } + + /** 与 {@link HalfHourSlotLabel#formatMinuteRange} 一致的可读起止。 */ + public String getTimeRangeLabel() { + return HalfHourSlotLabel.formatMinuteRange(startMinuteInclusive, endMinuteExclusive); + } + + @Override + public String toString() { + return "ShiftPlanBlock{" + + "day=" + scheduleDay + + ", time=" + timeRangeText + + (shiftId != null ? ", shiftId=" + shiftId : "") + + ", fixed=" + fixedScheduling + + ", needs=" + postNeeds + + '}'; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlocksFromDemandCoverBuilder.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlocksFromDemandCoverBuilder.java new file mode 100644 index 0000000..b82a3ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanBlocksFromDemandCoverBuilder.java @@ -0,0 +1,305 @@ +package jnpf.attendance.schedule; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 将 {@link ScheduleDemandCoverResult}(第六~八步:固定班贪心、划线、剩余缺口)转为 + * {@link ShiftPlanBlock} 列表,供 {@link ShiftPlanAttendanceShiftIdResolver} 与 + * {@link ShiftPlanFinalStaffAssigner} 使用。 + * + *

规则: + *

    + *
  • 固定班:同一岗位、同一分钟窗的多次贪心选取合并为一条 {@link ShiftPlanPostNeed}(人数=选本次数);
  • + *
  • 划线班:同一岗位、同一半小时索引窗的重复划线段合并计数;
  • + *
  • 剩余缺口:{@link ScheduleDemandCoverResult#getRemainingNeedByPostAfterLine()} 按「连续相等且 >0」 + * 的半小时序列收束为划线块(仍无法满足的缺口继续向下游可见);
  • + *
  • 同窗、同固定/划线类型、同时间窗的跨岗位块与 {@link HalfHourDemandBasedShiftPlanBuilder} 一样做合并。
  • + *
+ */ +public final class ShiftPlanBlocksFromDemandCoverBuilder { + + private ShiftPlanBlocksFromDemandCoverBuilder() {} + + /** + * @param coverByScheduleDay 与 {@code matrices} 中排班日 key 应对齐(通常来自 + * {@link ScheduleDemandCoverSteps678#coverAllScheduleDays}) + */ + public static Map> build( + Map coverByScheduleDay, + Map matrices) { + if (matrices == null || matrices.isEmpty()) { + return Collections.emptyMap(); + } + Map> out = new LinkedHashMap<>(); + for (Map.Entry e : matrices.entrySet()) { + LocalDate day = e.getKey(); + HalfHourPostDemandMatrix matrix = e.getValue(); + if (day == null || matrix == null) { + continue; + } + ScheduleDemandCoverResult cov = + coverByScheduleDay == null ? null : coverByScheduleDay.get(day); + if (cov == null) { + continue; + } + out.put(day, Collections.unmodifiableList(buildForOneDay(day, cov, matrix))); + } + return Collections.unmodifiableMap(out); + } + + private static List buildForOneDay( + LocalDate day, ScheduleDemandCoverResult cov, HalfHourPostDemandMatrix matrix) { + Objects.requireNonNull(day, "day"); + Objects.requireNonNull(cov, "cov"); + Objects.requireNonNull(matrix, "matrix"); + if (!day.equals(cov.getScheduleDay()) || !day.equals(matrix.getScheduleDay())) { + throw new IllegalArgumentException( + "schedule day mismatch: day=" + + day + + " covDay=" + + cov.getScheduleDay() + + " matrixDay=" + + matrix.getScheduleDay()); + } + + List slots = matrix.getSlotRangeLabelsOrdered(); + List raw = new ArrayList<>(); + + for (String postId : matrix.getPostIdsOrdered()) { + if (postId == null) { + continue; + } + + FixedShiftCoverOutcome fo = cov.getFixedShiftCoverByPost().get(postId); + if (fo != null) { + Map rep = new LinkedHashMap<>(); + Map counts = new LinkedHashMap<>(); + for (PostFixedShiftCandidate c : fo.getPicksInOrder()) { + if (c == null) { + continue; + } + String key = fixedPickKey(c); + rep.putIfAbsent(key, c); + counts.merge(key, 1, Integer::sum); + } + for (Map.Entry en : counts.entrySet()) { + PostFixedShiftCandidate c = rep.get(en.getKey()); + int n = en.getValue(); + if (c == null || n <= 0) { + continue; + } + raw.add( + new ShiftPlanBlock( + day, + c.getStartMinuteInclusive(), + c.getEndMinuteExclusive(), + true, + Collections.singletonList(new ShiftPlanPostNeed(postId, n)), + null, + c.getHistoricalAttendanceShiftId())); + } + } + + List linePicks = cov.getLineShiftPicksByPost().get(postId); + if (linePicks != null) { + Map lineCounts = new LinkedHashMap<>(); + Map lineRep = new LinkedHashMap<>(); + for (LineShiftPick lp : linePicks) { + if (lp == null) { + continue; + } + String key = + postId + + "|" + + lp.getStartSlotIndexInclusive() + + "|" + + lp.getEndSlotIndexExclusive(); + lineRep.putIfAbsent(key, lp); + lineCounts.merge(key, 1, Integer::sum); + } + for (Map.Entry en : lineCounts.entrySet()) { + LineShiftPick lp = lineRep.get(en.getKey()); + int n = en.getValue(); + if (lp == null || n <= 0) { + continue; + } + int[] range = linePickToMinuteRange(slots, lp); + if (range[1] > range[0]) { + raw.add( + new ShiftPlanBlock( + day, + range[0], + range[1], + false, + Collections.singletonList(new ShiftPlanPostNeed(postId, n)))); + } + } + } + + int[] rem = cov.getRemainingNeedByPostAfterLine().get(postId); + addRemainderLineBlocks(day, postId, rem, slots, raw); + } + + List merged = mergeCoextensiveBlocks(raw); + merged.sort( + Comparator.comparingInt(ShiftPlanBlock::getStartMinuteInclusive) + .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive) + .thenComparing(b -> !b.isFixedScheduling())); + return merged; + } + + private static String fixedPickKey(PostFixedShiftCandidate c) { + String hid = c.getHistoricalAttendanceShiftId(); + String hidSeg = hid == null ? "" : hid; + return c.getPostId() + + "|" + + c.getStartMinuteInclusive() + + "|" + + c.getEndMinuteExclusive() + + "|" + + hidSeg; + } + + private static int[] linePickToMinuteRange(List slots, LineShiftPick lp) { + int i = lp.getStartSlotIndexInclusive(); + int j = lp.getEndSlotIndexExclusive(); + if (slots == null || slots.isEmpty() || i < 0 || j <= i || i >= slots.size()) { + return new int[] {0, 0}; + } + int last = Math.min(j - 1, slots.size() - 1); + int[] a = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(i)); + int[] b = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(last)); + return new int[] {a[0], b[1]}; + } + + /** + * 将仍缺的半小时,按「连续、且缺口人数相同」收束为划线块。 + */ + private static void addRemainderLineBlocks( + LocalDate day, + String postId, + int[] rem, + List slots, + List out) { + if (rem == null || slots == null || rem.length == 0 || slots.size() != rem.length) { + return; + } + int i = 0; + while (i < rem.length) { + if (rem[i] <= 0) { + i++; + continue; + } + int v = rem[i]; + int j = i; + while (j < rem.length && rem[j] == v) { + j++; + } + int sm = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(i))[0]; + int em = HalfHourSlotLabel.startEndExclusiveMinutes(slots.get(j - 1))[1]; + if (em > sm && v > 0) { + out.add( + new ShiftPlanBlock( + day, + sm, + em, + false, + Collections.singletonList(new ShiftPlanPostNeed(postId, v)))); + } + i = j; + } + } + + private static List mergeCoextensiveBlocks(List blocks) { + if (blocks == null || blocks.isEmpty()) { + return new ArrayList<>(); + } + List sorted = new ArrayList<>(); + for (ShiftPlanBlock b : blocks) { + if (b != null) { + sorted.add(b); + } + } + if (sorted.isEmpty()) { + return new ArrayList<>(); + } + sorted.sort( + Comparator.comparing((ShiftPlanBlock b) -> b.getScheduleDay()) + .thenComparingInt(ShiftPlanBlock::getStartMinuteInclusive) + .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive) + .thenComparing(ShiftPlanBlock::isFixedScheduling)); + List out = new ArrayList<>(); + ShiftPlanBlock cur = sorted.get(0); + for (int k = 1; k < sorted.size(); k++) { + ShiftPlanBlock next = sorted.get(k); + if (sameWindow(cur, next)) { + cur = mergePostNeeds(cur, next); + } else { + out.add(cur); + cur = next; + } + } + out.add(cur); + return out; + } + + private static boolean sameWindow(ShiftPlanBlock a, ShiftPlanBlock b) { + return a.getScheduleDay().equals(b.getScheduleDay()) + && a.getStartMinuteInclusive() == b.getStartMinuteInclusive() + && a.getEndMinuteExclusive() == b.getEndMinuteExclusive() + && a.isFixedScheduling() == b.isFixedScheduling(); + } + + private static ShiftPlanBlock mergePostNeeds(ShiftPlanBlock a, ShiftPlanBlock b) { + Map sum = new LinkedHashMap<>(); + for (ShiftPlanPostNeed n : a.getPostNeeds()) { + if (n == null || n.getPostId() == null || n.getPostId().trim().isEmpty()) { + continue; + } + sum.merge(n.getPostId(), n.getNeedCount(), Integer::sum); + } + for (ShiftPlanPostNeed n : b.getPostNeeds()) { + if (n == null || n.getPostId() == null || n.getPostId().trim().isEmpty()) { + continue; + } + sum.merge(n.getPostId(), n.getNeedCount(), Integer::sum); + } + List list = new ArrayList<>(); + for (Map.Entry e : sum.entrySet()) { + if (e.getValue() != null && e.getValue() > 0) { + list.add(new ShiftPlanPostNeed(e.getKey(), e.getValue())); + } + } + String mergedHistorical = mergeHistoricalAttendanceShiftIds(a, b); + return new ShiftPlanBlock( + a.getScheduleDay(), + a.getStartMinuteInclusive(), + a.getEndMinuteExclusive(), + a.isFixedScheduling(), + list, + a.getShiftId(), + mergedHistorical); + } + + private static String mergeHistoricalAttendanceShiftIds(ShiftPlanBlock a, ShiftPlanBlock b) { + String ha = a.getHistoricalAttendanceShiftId(); + String hb = b.getHistoricalAttendanceShiftId(); + if (ha == null || ha.isBlank()) { + return hb == null || hb.isBlank() ? null : hb.trim(); + } + if (hb == null || hb.isBlank()) { + return ha.trim(); + } + if (ha.trim().equals(hb.trim())) { + return ha.trim(); + } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanFinalStaffAssigner.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanFinalStaffAssigner.java new file mode 100644 index 0000000..bb38dc9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanFinalStaffAssigner.java @@ -0,0 +1,935 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.WorkstationGroupUserVo; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; +import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo; +import jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 智能排班流水线第九、十步:在已有 {@link ShiftPlanBlock}(班次时段 + 各岗位需求人数)基础上, + * 为每个岗位需求选出具体员工,并产出「按班次」「按员工」两种视图的 {@link ShiftPlanAssignmentResult}。 + *

+ * 与 {@link AutoScheduleService#start} 衔接方式:上游完成第六~八步班次计划后,将 + * {@code shiftPlanByScheduleDay}、工作站人员、{@link ScheduleGroupRuleConfigVo}、{@link StaffRuleEvaluationPort}、 + * {@link SchedulePeriodWorkTracker} 传入 {@link #assign}。 + *

+ * 核心策略(按处理顺序) + *

    + *
  1. 按排班日升序、同日内按班次开始时间升序遍历班次块;
  2. + *
  3. 每块内先 {@link CrossPostGeneralStaffAllocator} 统筹多岗缺口(马六类跨岗),再按岗位落位;
  4. + *
  5. 同一块内一人只排一个岗位({@code usedInBlock});
  6. + *
  7. 候选人:专岗优先、通岗次之({@link #orderedCandidates}),余量选人前打乱(五-5);
  8. + *
  9. 第九步:{@link StaffRuleEvaluationPort#passesOperationalHardConstraints}(请假/借调等,当前实现多为占位);
  10. + *
  11. 第十步:固定班应用 {@link FixedSchedulingRuleVo}(必须项不可放松、尽量项按序放松); + * 划线班不传固定规则({@link ScheduleStaffAssignmentSteps910} 同约定);
  12. + *
  13. 同日互斥:{@link StaffAssignmentDayLedger} 禁止同人同日既固定又划线;
  14. + *
  15. {@link StaffAssignmentIntervalLedger} 禁止同人同日时段重叠(划线多段、同 logical key 多段均受约束);
  16. + *
  17. 选中后 {@link SchedulePeriodWorkTracker#recordAssignment} 累计工时,供后续班次规则判断。
  18. + *
+ */ +public final class ShiftPlanFinalStaffAssigner { + + private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + + private static final Logger log = LoggerFactory.getLogger(ShiftPlanFinalStaffAssigner.class); + + private ShiftPlanFinalStaffAssigner() {} + + /** + * 为排班区间内的全部班次块分配人员,并组装双视图结果。 + * + * @param shiftPlanByScheduleDay 排班日 → 当日 {@link ShiftPlanBlock} 列表(来自 + * {@link HalfHourDemandBasedShiftPlanBuilder} + 考勤班次 id 匹配) + * @param workstationWithUsersVoList 工作站及绑定员工(专岗/通岗、在组日、是否可划线) + * @param scheduleGroupRuleConfigVo 考勤组规则;仅使用其中 {@link ScheduleGroupRuleConfigVo#getFixedScheduling()}, + * 划线规则在第六~八步已体现在班次块形态上 + * @param staffRuleEvaluationPort 第九~十步规则判定(固定规则 + 员工工作状态 VO 等) + * @param workTracker 排班期内每人已排工时/班段,选人时构造 {@link StaffAssignmentContext}, + * 选中后回写本段 assignment + * @param groupId 考勤组 ID,写入 {@link StaffAssignmentContext} + * @param random 打乱/均衡用随机源;{@code null} 时内部新建 + * @return 按班次、按员工两种视图的最终排班结果 + */ + public static ShiftPlanAssignmentResult assign( + Map> shiftPlanByScheduleDay, + List workstationWithUsersVoList, + ScheduleGroupRuleConfigVo scheduleGroupRuleConfigVo, + StaffRuleEvaluationPort staffRuleEvaluationPort, + SchedulePeriodWorkTracker workTracker, + String groupId, + Random random) { + Objects.requireNonNull(workTracker, "workTracker"); + Objects.requireNonNull(staffRuleEvaluationPort, "staffRuleEvaluationPort"); + + // ---------- 步骤 0:入参规范化与索引结构 ---------- + Map> plan = + shiftPlanByScheduleDay == null ? Collections.emptyMap() : shiftPlanByScheduleDay; + // 按岗位汇总专岗/通岗员工 ID(positionId 与 ShiftPlanPostNeed.postId 对齐) + WorkstationPostStaffBuckets buckets = + WorkstationPostStaffBuckets.fromWorkstations(workstationWithUsersVoList); + // userId → 员工元数据(姓名、在组日、可划线等),用于候选过滤与结果展示 + Map userMeta = indexUserMeta(workstationWithUsersVoList); + Map positionNameByPostId = indexPositionNameByPostId(workstationWithUsersVoList); + // 固定排班规则 VO;划线块选人时不使用(见 pickForPostDemandWithPerEmployeeContext) + FixedSchedulingRuleVo fixedRules = + scheduleGroupRuleConfigVo == null + ? null + : scheduleGroupRuleConfigVo.getFixedScheduling(); + // 同日「固定 vs 划线」互斥账本,跨班次块共享 + StaffAssignmentDayLedger dayLedger = new StaffAssignmentDayLedger(); + // 同人同日已排班时段,防重叠(划线多段、多班块) + StaffAssignmentIntervalLedger intervalLedger = new StaffAssignmentIntervalLedger(); + Random rnd = random == null ? new Random() : random; + String gid = groupId == null ? "" : groupId; + + List incompleteNotes = new ArrayList<>(); + + // 按班次维度累积视图;按员工维度用 shiftsByEmployee 暂存后一次性转 AssignedEmployeeView + List byShift = new ArrayList<>(); + Map> shiftsByEmployee = + new LinkedHashMap<>(); + + // ---------- 步骤 1:按排班日升序遍历(忽略 null 排班日 key,避免 NPE) ---------- + List days = new ArrayList<>(); + for (LocalDate d : plan.keySet()) { + if (d != null) { + days.add(d); + } + } + if (days.size() < plan.size()) { + log.error( + "{} phase=ASSIGN | ignored {} null LocalDate key(s) in shiftPlanByScheduleDay (total keys={})", + AutoSchedulePipelineLog.MARKER, + plan.size() - days.size(), + plan.size()); + List orphan = plan.get(null); + if (orphan != null && !orphan.isEmpty()) { + log.error( + "{} phase=ASSIGN | dropping {} ShiftPlanBlock(s) under null schedule day key — assign by calendar day only", + AutoSchedulePipelineLog.MARKER, + orphan.size()); + } + } + days.sort(Comparator.naturalOrder()); + for (LocalDate day : days) { + List blocks = plan.get(day); + if (blocks == null || blocks.isEmpty()) { + continue; + } + // ---------- 步骤 2:同日内按班次开始时间排序(先早后晚,利于休息间隔/日工时累计) ---------- + List ordered = new ArrayList<>(blocks); + ordered.sort( + Comparator.comparingInt(ShiftPlanBlock::getStartMinuteInclusive) + .thenComparingInt(ShiftPlanBlock::getEndMinuteExclusive)); + + // ---------- 步骤 3:逐班次块处理 ---------- + for (ShiftPlanBlock block : ordered) { + if (block == null) { + log.error( + "{} phase=ASSIGN | skip null ShiftPlanBlock under scheduleDay={}", + AutoSchedulePipelineLog.MARKER, + day); + continue; + } + if (!ShiftPlanBlock.isValidTimeWindow( + block.getStartMinuteInclusive(), block.getEndMinuteExclusive())) { + log.error( + "{} phase=ASSIGN | skip ShiftPlanBlock with invalid window: day={} startMin={} endMin={} fixed={}", + AutoSchedulePipelineLog.MARKER, + day, + block.getStartMinuteInclusive(), + block.getEndMinuteExclusive(), + block.isFixedScheduling()); + continue; + } + List blockNeeds = mergePostNeedsByPostId(block.getPostNeeds()); + List postViews = new ArrayList<>(); + Set usedInBlock = new HashSet<>(); + + double shiftHours = + Math.max( + 0d, + (block.getEndMinuteExclusive() - block.getStartMinuteInclusive()) + / 60.0); + int blockStartInclusive = block.getStartMinuteInclusive(); + int blockEndExclusive = block.getEndMinuteExclusive(); + boolean lineAssignment = !block.isFixedScheduling(); + String fixedKey = fixedShiftGroupKey(block); + FixedSchedulingRuleVo fixedRulesForBlock = + lineAssignment ? null : fixedRules; + String dayStr = day.format(DAY_FMT); + + // ---------- 步骤 4a:块级多岗统筹(跨岗通岗优先补缺口大岗位) ---------- + Map demandByPost = new LinkedHashMap<>(); + Map> eligSpecByPost = new LinkedHashMap<>(); + Map> eligGenByPost = new LinkedHashMap<>(); + for (ShiftPlanPostNeed need : blockNeeds) { + if (need.getNeedCount() <= 0) { + continue; + } + String postId = need.getPostId(); + demandByPost.put(postId, need.getNeedCount()); + eligSpecByPost.put( + postId, + filterEligibleIds( + buckets.getSpecialistIds(postId), + userMeta, + dayStr, + block.isFixedScheduling())); + eligGenByPost.put( + postId, + filterEligibleIds( + buckets.getGeneralIds(postId), + userMeta, + dayStr, + block.isFixedScheduling())); + } + SchedulingForTestCheckLog.info( + log, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "staff_assign_block_inputs", + String.format( + "algo=ShiftPlanFinalStaffAssigner | step=BLOCK_OPEN | groupId=%s | scheduleDay=%s " + + "| attendanceShiftId=%s | minuteWindow=[%d,%d) | timeRangeText=%s | fixedScheduling=%s " + + "| mergedDemandByPost=%s | eligibleSpecialistCountByPost=%s " + + "| eligibleGeneralCountByPost=%s", + gid, + day, + block.getShiftId(), + blockStartInclusive, + blockEndExclusive, + block.getTimeRangeText(), + Boolean.toString(!lineAssignment), + demandByPost, + stringifyPoolSizes(eligSpecByPost), + stringifyPoolSizes(eligGenByPost))); + + Map> crossAlloc = + demandByPost.isEmpty() + ? Collections.emptyMap() + : CrossPostGeneralStaffAllocator.allocateForOneTimeWindow( + demandByPost, eligSpecByPost, eligGenByPost, rnd); + SchedulingForTestCheckLog.info( + log, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "staff_assign_cross_post_allocation", + String.format( + "algo=ShiftPlanFinalStaffAssigner | step=CROSS_POST_RESULT | groupId=%s | scheduleDay=%s " + + "| attendanceShiftId=%s | minuteWindow=[%d,%d) | allocatedByPost=%s", + gid, + day, + block.getShiftId(), + blockStartInclusive, + blockEndExclusive, + crossAlloc)); + + // ---------- 步骤 4b:按岗位落位(先跨岗建议,再规则选人;块内去重) ---------- + for (ShiftPlanPostNeed need : blockNeeds) { + if (need.getNeedCount() <= 0) { + continue; + } + String postId = need.getPostId(); + Function contextFor = + uid -> + buildContext( + workTracker, gid, day, postId, block, shiftHours, uid); + + List picked = new ArrayList<>(); + List preferred = + crossAlloc.getOrDefault(postId, Collections.emptyList()); + for (String uid : preferred) { + if (picked.size() >= need.getNeedCount()) { + break; + } + if (usedInBlock.contains(uid)) { + continue; + } + List one = + pickForPostDemandWithPerEmployeeContext( + 1, + Collections.singletonList(uid), + buckets, + postId, + staffRuleEvaluationPort, + contextFor, + fixedRulesForBlock, + dayLedger, + lineAssignment, + fixedKey, + usedInBlock, + rnd, + intervalLedger, + blockStartInclusive, + blockEndExclusive); + if (!one.isEmpty()) { + picked.add(one.get(0)); + usedInBlock.add(one.get(0)); + } + } + + int remaining = need.getNeedCount() - picked.size(); + if (remaining > 0) { + List candidates = + orderedCandidates( + postId, + buckets, + userMeta, + day, + block.isFixedScheduling()); + List more = + pickForPostDemandWithPerEmployeeContext( + remaining, + candidates, + buckets, + postId, + staffRuleEvaluationPort, + contextFor, + fixedRulesForBlock, + dayLedger, + lineAssignment, + fixedKey, + usedInBlock, + rnd, + intervalLedger, + blockStartInclusive, + blockEndExclusive); + picked.addAll(more); + usedInBlock.addAll(more); + } + + if (picked.size() < need.getNeedCount()) { + incompleteNotes.add( + explainStaffShortageGap( + day, + block, + postId, + need.getNeedCount(), + picked.size(), + buckets, + userMeta, + staffRuleEvaluationPort, + fixedRulesForBlock, + lineAssignment, + contextFor)); + } + + List staffRefs = new ArrayList<>(); + for (String uid : picked) { + workTracker.recordAssignment( + uid, + day, + block.getStartMinuteInclusive(), + block.getEndMinuteExclusive()); + WorkstationGroupUserVo meta = userMeta.get(uid); + staffRefs.add( + new ShiftPlanAssignmentResult.AssignedStaffRef( + uid, + meta == null ? "" : nullToEmpty(meta.getUserName()), + meta != null && Boolean.TRUE.equals(meta.getIsExtra()))); + shiftsByEmployee + .computeIfAbsent(uid, k -> new ArrayList<>()) + .add( + new ShiftPlanAssignmentResult.AssignedShiftRef( + day, + block.getShiftId(), + block.getTimeRangeText(), + block.isFixedScheduling(), + postId)); + } + postViews.add( + new ShiftPlanAssignmentResult.AssignedPostStaffView( + postId, + positionNameForPost(positionNameByPostId, postId), + need.getNeedCount(), + staffRefs)); + } + + byShift.add( + new ShiftPlanAssignmentResult.AssignedShiftBlockView( + day, + block.getShiftId(), + block.getTimeRangeText(), + block.isFixedScheduling(), + postViews)); + } + } + + // ---------- 步骤 5:由 shiftsByEmployee 构建按员工视图并排序 ---------- + List byEmployee = new ArrayList<>(); + for (Map.Entry> e : + shiftsByEmployee.entrySet()) { + WorkstationGroupUserVo meta = userMeta.get(e.getKey()); + byEmployee.add( + new ShiftPlanAssignmentResult.AssignedEmployeeView( + e.getKey(), + meta == null ? "" : nullToEmpty(meta.getUserName()), + e.getValue())); + } + byEmployee.sort(Comparator.comparing(ShiftPlanAssignmentResult.AssignedEmployeeView::getUserId)); + return new ShiftPlanAssignmentResult(byShift, byEmployee, incompleteNotes); + } + + /** + * 当某班段某岗「需求 > 已选」时归类原因,供 {@link ShiftPlanAssignmentResult#getIncompleteScheduleReasons()} 展示。 + */ + private static ShiftPlanAssignmentResult.IncompleteScheduleReason explainStaffShortageGap( + LocalDate day, + ShiftPlanBlock block, + String postId, + int needCount, + int assignedCount, + WorkstationPostStaffBuckets buckets, + Map userMeta, + StaffRuleEvaluationPort staffRuleEvaluationPort, + FixedSchedulingRuleVo fixedRulesForBlock, + boolean lineAssignment, + Function contextFor) { + int deficit = Math.max(0, needCount - assignedCount); + String timeRange = block.getTimeRangeText() == null ? "" : block.getTimeRangeText(); + List allOrdered = + orderedCandidates(postId, buckets, userMeta, day, block.isFixedScheduling()); + List pool = intersectWorkstationPool(postId, allOrdered, buckets); + String pidDisplay = postId == null ? "" : postId.trim(); + if (allOrdered.isEmpty()) { + return new ShiftPlanAssignmentResult.IncompleteScheduleReason( + day, + timeRange, + pidDisplay, + ShiftPlanAssignmentResult.INCOMPLETE_NO_CANDIDATE, + String.format("还差%d人:在组日与班种过滤后无专岗/通岗候选人", deficit)); + } + if (pool.isEmpty()) { + return new ShiftPlanAssignmentResult.IncompleteScheduleReason( + day, + timeRange, + pidDisplay, + ShiftPlanAssignmentResult.INCOMPLETE_WORKSTATION_POOL_EMPTY, + String.format("还差%d人:候选人不落在工作站该岗位绑定名单内", deficit)); + } + if (!lineAssignment + && fixedRulesForBlock != null + && assignedCount == 0 + && !anyPassesMustOnly( + pool, fixedRulesForBlock, staffRuleEvaluationPort, contextFor)) { + return new ShiftPlanAssignmentResult.IncompleteScheduleReason( + day, + timeRange, + pidDisplay, + ShiftPlanAssignmentResult.INCOMPLETE_FIXED_MUST_BLOCK, + String.format( + "还差%d人:固定班「必须」规则下没有人能排该班段的该岗位", + deficit)); + } + return new ShiftPlanAssignmentResult.IncompleteScheduleReason( + day, + timeRange, + pidDisplay, + ShiftPlanAssignmentResult.INCOMPLETE_RULES_OR_CONFLICT, + String.format( + "还差%d人:硬约束/固定尽量项、同人占用或时段重叠等,仅能排到%d/%d", + deficit, assignedCount, needCount)); + } + + /** + * 为单个岗位需求选出最多 {@code targetCount} 名员工。 + *

+ * 与 {@link ScheduleStaffAssignmentSteps910#pickForPostDemand} 逻辑等价,但为每位员工 + * 单独调用 {@code contextForEmployee} 构造 {@link StaffAssignmentContext}(含该员工当日已排工时、 + * 与当前班段开始时间的休息间隔),以便日工时、最小休息等规则按人准确判断。 + *

+ * 内层循环 + *

    + *
  1. {@link #intersectWorkstationPool}:候选人与工作站绑定求交;
  2. + *
  3. 固定班且全员不满足「必须」规则 → 直接空列表(本岗位本班次不排);
  4. + *
  5. 扫描候选池:第九步硬约束 → 第十步固定规则(含已放松的尽量项)→ 账本同日互斥 → 时段不重叠;
  6. + *
  7. 本轮无人入选则 {@link #nextRelaxable} 放开下一档「尽量」规则后重试。
  8. + *
+ */ + private static List pickForPostDemandWithPerEmployeeContext( + int targetCount, + List orderedCandidates, + WorkstationPostStaffBuckets workstationBuckets, + String postId, + StaffRuleEvaluationPort port, + Function contextForEmployee, + FixedSchedulingRuleVo fixedSchedulingRules, + StaffAssignmentDayLedger ledger, + boolean lineAssignment, + String fixedShiftGroupKey, + Set excludeEmployeeIds, + Random random, + StaffAssignmentIntervalLedger intervalLedger, + int blockStartInclusive, + int blockEndExclusive) { + Objects.requireNonNull(intervalLedger, "intervalLedger"); + if (targetCount <= 0) { + return Collections.emptyList(); + } + List pool = + intersectWorkstationPool(postId, orderedCandidates, workstationBuckets); + if (pool.isEmpty()) { + return Collections.emptyList(); + } + if (excludeEmployeeIds != null && !excludeEmployeeIds.isEmpty()) { + List filtered = new ArrayList<>(); + for (String id : pool) { + if (id != null && !excludeEmployeeIds.contains(id)) { + filtered.add(id); + } + } + pool = filtered; + if (pool.isEmpty()) { + return Collections.emptyList(); + } + } + if (pool.size() > 1 && random != null) { + Collections.shuffle(pool, random); + } + // 划线班不参与 FixedSchedulingRuleVo 的必须/尽量判断 + FixedSchedulingRuleVo fixedVo = lineAssignment ? null : fixedSchedulingRules; + // 固定班:若存在启用的「必须」规则且候选池中无人同时满足硬约束+全部必须项,则本需求放弃排人 + if (fixedVo != null && !pool.isEmpty() && !anyPassesMustOnly(pool, fixedVo, port, contextForEmployee)) { + return Collections.emptyList(); + } + List picked = new ArrayList<>(); + Set relaxed = new LinkedHashSet<>(); + while (picked.size() < targetCount) { + boolean progressed = false; + for (String id : pool) { + if (picked.size() >= targetCount || picked.contains(id)) { + continue; + } + StaffAssignmentContext ctx = contextForEmployee.apply(id); + if (!port.passesOperationalHardConstraints(id, ctx)) { + continue; + } + if (!passesFixedRules(id, fixedVo, port, ctx, relaxed)) { + continue; + } + LocalDate day = dayFrom(ctx); + if (!intervalLedger.canAssign(id, day, blockStartInclusive, blockEndExclusive)) { + continue; + } + if (lineAssignment) { + if (!ledger.canAssignLine(id, day)) { + continue; + } + ledger.markLineShift(id, day); + } else { + if (!ledger.canAssignFixed(id, day, fixedShiftGroupKey)) { + continue; + } + ledger.markFixed(id, day, fixedShiftGroupKey); + } + intervalLedger.mark(id, day, blockStartInclusive, blockEndExclusive); + picked.add(id); + progressed = true; + } + if (picked.size() >= targetCount) { + break; + } + if (!progressed) { + FixedSoftRuleKind next = nextRelaxable(fixedVo, relaxed); + if (next == null) { + break; + } + relaxed.add(next); + if (log.isErrorEnabled() && !pool.isEmpty()) { + StaffAssignmentContext ctxProbe = contextForEmployee.apply(pool.get(0)); + SchedulingForTestCheckLog.info( + log, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "staff_assign_soft_constraint_relax", + String.format( + "algo=ShiftPlanFinalStaffAssigner.pickForPostDemandWithPerEmployeeContext | " + + "scheduleDay=%s | postId=%s | groupId=%s | lineScheduling=%s " + + "| shiftWindowMinute=[%d,%d) | targetCount=%d | poolSize=%d | pickedSoFar=%d " + + "| newlyRelaxedSoftRule=%s | accumulatedRelaxedSoftRules=%s", + ctxProbe.getScheduleDay(), + ctxProbe.getPostId(), + ctxProbe.getGroupId(), + Boolean.toString(lineAssignment), + blockStartInclusive, + blockEndExclusive, + targetCount, + pool.size(), + picked.size(), + next.name(), + String.valueOf(relaxed))); + } + } + } + return Collections.unmodifiableList(picked); + } + + /** + * 将「全局有序候选」与工作站该岗位绑定名单求交。 + *

+ * 若岗位未配置工作站绑定({@link WorkstationPostStaffBuckets#hasWorkstationBinding} 为 false), + * 原样返回 {@code orderedCandidates};若有绑定则与专岗∪通岗求交,交集为空返回空列表(不再退化为工作站全集)。 + */ + private static List intersectWorkstationPool( + String postId, + List orderedCandidates, + WorkstationPostStaffBuckets workstationBuckets) { + if (orderedCandidates == null || orderedCandidates.isEmpty()) { + return Collections.emptyList(); + } + if (workstationBuckets == null || !workstationBuckets.hasWorkstationBinding(postId)) { + return new ArrayList<>(orderedCandidates); + } + Set allowed = new LinkedHashSet<>(); + allowed.addAll(workstationBuckets.getSpecialistIds(postId)); + allowed.addAll(workstationBuckets.getGeneralIds(postId)); + List out = new ArrayList<>(); + for (String id : orderedCandidates) { + if (id != null && allowed.contains(id.trim())) { + out.add(id.trim()); + } + } + return out; + } + + /** + * 同窗若重复出现同一岗位 id 的 {@link ShiftPlanPostNeed},合并需求人数,避免跨岗统筹阶段 {@code demandByPost} 被覆盖。 + */ + static List mergePostNeedsByPostId(List raw) { + if (raw == null || raw.isEmpty()) { + return Collections.emptyList(); + } + Map sum = new LinkedHashMap<>(); + for (ShiftPlanPostNeed n : raw) { + if (n == null) { + continue; + } + String pid = n.getPostId() == null ? "" : n.getPostId().trim(); + if (pid.isEmpty()) { + continue; + } + int c = n.getNeedCount(); + if (c <= 0) { + continue; + } + sum.merge(pid, c, Integer::sum); + } + List out = new ArrayList<>(); + for (Map.Entry e : sum.entrySet()) { + if (e.getValue() != null && e.getValue() > 0) { + out.add(new ShiftPlanPostNeed(e.getKey(), e.getValue())); + } + } + return out; + } + + /** + * 固定班选人前置检查:候选池中是否至少有一人通过「第九步硬约束 + 全部启用的必须级固定规则」。 + *

+ * 若返回 false,{@link #pickForPostDemandWithPerEmployeeContext} 直接返回空,表示本岗位本班次在必须规则下无法排班 + * (放松「尽量」规则也不能增加人选,与 {@link FixedSoftConstraintRelaxationPlanner} 约定一致)。 + */ + private static boolean anyPassesMustOnly( + List pool, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + Function contextForEmployee) { + for (String id : pool) { + if (!port.passesOperationalHardConstraints(id, contextForEmployee.apply(id))) { + continue; + } + if (passesMustRulesOnly(id, fixedRules, port, contextForEmployee.apply(id))) { + return true; + } + } + return false; + } + + /** + * 仅校验 {@link FixedSoftRuleKind} 中「已启用且 priority=必须(1)」的维度是否全部不违反。 + */ + private static boolean passesMustRulesOnly( + String employeeId, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context) { + for (FixedSoftRuleKind kind : FixedSoftRuleKind.relaxOrder()) { + if (!kind.isEnabled(fixedRules) || !kind.isMustPriority(fixedRules)) { + continue; + } + if (kind.violates(employeeId, context, port)) { + return false; + } + } + return true; + } + + /** 从上下文中取排班日;缺省为 {@link LocalDate#EPOCH}。 */ + private static LocalDate dayFrom(StaffAssignmentContext ctx) { + return ctx == null ? LocalDate.EPOCH : ctx.getScheduleDay(); + } + + /** + * 第十步:按 {@link FixedSoftRuleKind#relaxOrder()} 检查固定排班规则。 + *

    + *
  • 必须项:始终生效,违反则不可选;
  • + *
  • 尽量项:未列入 {@code relaxed} 时生效;已放松的维度跳过。
  • + *
+ * {@code fixedRules == null}(划线班)时恒为 true。 + */ + private static boolean passesFixedRules( + String employeeId, + FixedSchedulingRuleVo fixedRules, + StaffRuleEvaluationPort port, + StaffAssignmentContext context, + Set relaxed) { + if (fixedRules == null) { + return true; + } + for (FixedSoftRuleKind kind : FixedSoftRuleKind.relaxOrder()) { + if (!kind.isEnabled(fixedRules)) { + continue; + } + if (kind.isMustPriority(fixedRules)) { + if (kind.violates(employeeId, context, port)) { + return false; + } + continue; + } + if (relaxed.contains(kind)) { + continue; + } + if (kind.violates(employeeId, context, port)) { + return false; + } + } + return true; + } + + /** + * 在尚未放松的「尽量」规则中,取 {@link FixedSoftRuleKind#relaxOrder()} 的下一项。 + *

+ * 用于 {@link #pickForPostDemandWithPerEmployeeContext} 外层 while:本轮无人入选时放开一档尽量规则后重扫候选池。 + */ + private static FixedSoftRuleKind nextRelaxable( + FixedSchedulingRuleVo fixedRules, Set relaxed) { + if (fixedRules == null) { + return null; + } + for (FixedSoftRuleKind kind : FixedSoftRuleKind.relaxOrder()) { + if (!kind.isEnabled(fixedRules) || kind.isMustPriority(fixedRules) || relaxed.contains(kind)) { + continue; + } + return kind; + } + return null; + } + + /** + * 为指定员工构造本段班次的 {@link StaffAssignmentContext}。 + *

+ * 从 {@link SchedulePeriodWorkTracker} 读取: + *

    + *
  • {@code existingSameDayScheduledWorkHours}:该员工排班日当天已排工时(含此前已选中的块);
  • + *
  • {@code minutesRestSincePriorShiftEnd}:当前块开始时刻与上一段班结束时刻的间隔(分钟), + * 供「班次间最小休息」规则使用;无上一班时为 null(不判违反)。
  • + *
+ */ + private static StaffAssignmentContext buildContext( + SchedulePeriodWorkTracker tracker, + String groupId, + LocalDate day, + String postId, + ShiftPlanBlock block, + double shiftHours, + String sampleEmployeeForRest) { + double existing = 0d; + Integer restMin = null; + if (sampleEmployeeForRest != null) { + existing = tracker.getSameDayScheduledHours(sampleEmployeeForRest, day); + restMin = + tracker.minutesRestSincePriorShiftEnd( + sampleEmployeeForRest, day, block.getStartMinuteInclusive()); + } + return new StaffAssignmentContext( + day, + postId, + block.getTimeRangeText(), + groupId, + existing, + shiftHours, + restMin); + } + + /** + * 固定班「逻辑班次」键:用于 {@link StaffAssignmentDayLedger} 判断同日是否可兼多个固定段。 + *
    + *
  • 优先使用 {@link ShiftPlanBlock#getShiftId()};
  • + *
  • 无 shiftId 时退化为「排班日 + 时段文本」。
  • + *
+ * 同一键下允许多个 {@link ShiftPlanBlock} 段分配给同一人;不同键在同日互斥。 + */ + private static String fixedShiftGroupKey(ShiftPlanBlock block) { + if (block.getShiftId() != null && !block.getShiftId().isEmpty()) { + return block.getShiftId(); + } + return block.getScheduleDay() + "|" + block.getTimeRangeText(); + } + + /** + * 生成某岗位在某日的有序候选员工 ID 列表。 + *

+ * 顺序:先 {@link WorkstationPostStaffBuckets#getSpecialistIds}(专岗), + * 再 {@link WorkstationPostStaffBuckets#getGeneralIds}(通岗); + * 仅保留 {@link #eligible} 通过者,去重且保持插入顺序。 + */ + private static List orderedCandidates( + String postId, + WorkstationPostStaffBuckets buckets, + Map userMeta, + LocalDate day, + boolean fixedScheduling) { + String dayStr = day.format(DAY_FMT); + LinkedHashSet ordered = new LinkedHashSet<>(); + for (String uid : buckets.getSpecialistIds(postId)) { + if (eligible(uid, userMeta, dayStr, fixedScheduling)) { + ordered.add(uid); + } + } + for (String uid : buckets.getGeneralIds(postId)) { + if (eligible(uid, userMeta, dayStr, fixedScheduling)) { + ordered.add(uid); + } + } + return new ArrayList<>(ordered); + } + + /** + * 候选资格过滤(第九步之前的业务硬过滤)。 + *

    + *
  • 员工元数据存在;
  • + *
  • {@link WorkstationGroupUserVo#getInGroupDays()} 包含当前排班日;
  • + *
  • 划线班要求 {@link WorkstationGroupUserVo#getCanLineSchedule()} 为 true。
  • + *
+ */ + private static Set filterEligibleIds( + Set userIds, + Map userMeta, + String dayStr, + boolean fixedScheduling) { + Set out = new LinkedHashSet<>(); + if (userIds == null) { + return out; + } + for (String uid : userIds) { + if (eligible(uid, userMeta, dayStr, fixedScheduling)) { + out.add(uid); + } + } + return out; + } + + private static boolean eligible( + String userId, + Map userMeta, + String dayStr, + boolean fixedScheduling) { + WorkstationGroupUserVo u = userMeta.get(userId); + if (u == null) { + return false; + } + List inGroup = u.getInGroupDays(); + if (inGroup == null || inGroup.isEmpty() || !inGroup.contains(dayStr)) { + return false; + } + if (!fixedScheduling && !Boolean.TRUE.equals(u.getCanLineSchedule())) { + return false; + } + return true; + } + + /** + * 岗位 ID → {@link WorkstationWithUsersVo#getPositionName()}; + * 同一 {@code positionId} 多条工作站记录时保留首次非 null 名称。 + */ + private static Map indexPositionNameByPostId(List workstations) { + Map map = new LinkedHashMap<>(); + if (workstations == null) { + return map; + } + for (WorkstationWithUsersVo w : workstations) { + if (w == null || w.getPositionId() == null || w.getPositionId().trim().isEmpty()) { + continue; + } + String pid = w.getPositionId().trim(); + map.putIfAbsent(pid, nullToEmpty(w.getPositionName())); + } + return map; + } + + private static String positionNameForPost(Map positionNameByPostId, String postId) { + if (postId == null || postId.trim().isEmpty()) { + return ""; + } + String trimmed = postId.trim(); + String n = positionNameByPostId.get(trimmed); + return n != null ? n : ""; + } + + /** + * 由工作站列表构建 userId → {@link WorkstationGroupUserVo} 索引(同一 userId 仅保留首次出现)。 + */ + private static Map indexUserMeta( + List workstations) { + Map map = new LinkedHashMap<>(); + if (workstations == null) { + return map; + } + for (WorkstationWithUsersVo w : workstations) { + if (w == null || w.getUserList() == null) { + continue; + } + for (WorkstationGroupUserVo u : w.getUserList()) { + if (u != null && u.getUserId() != null && !u.getUserId().trim().isEmpty()) { + map.putIfAbsent(u.getUserId().trim(), u); + } + } + } + return map; + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } + + /** + * [for_test_check] 专供日志:各岗位池人数(不列出具体 userId,防止隐私/体积问题)。 + */ + private static String stringifyPoolSizes(Map> pools) { + if (pools == null || pools.isEmpty()) { + return "{}"; + } + Map counts = new LinkedHashMap<>(); + for (Map.Entry> e : pools.entrySet()) { + Set ids = e.getValue(); + counts.put(e.getKey(), ids == null ? 0 : ids.size()); + } + return counts.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPickSnapshot.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPickSnapshot.java new file mode 100644 index 0000000..1702a65 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPickSnapshot.java @@ -0,0 +1,222 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 最终排班「选人快照」:由 {@link ShiftPlanAssignmentResult} 派生,供日志按班次 / 按员工两个维度输出。 + */ +public final class ShiftPlanPickSnapshot implements Serializable { + + private final List byShift; + private final List byEmployee; + private final List incompleteScheduleReasons; + + private ShiftPlanPickSnapshot( + List byShift, + List byEmployee, + List incompleteScheduleReasons) { + this.byShift = + byShift == null + ? Collections.emptyList() + : Collections.unmodifiableList(byShift); + this.byEmployee = + byEmployee == null + ? Collections.emptyList() + : Collections.unmodifiableList(byEmployee); + this.incompleteScheduleReasons = + incompleteScheduleReasons == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(incompleteScheduleReasons)); + } + + public static ShiftPlanPickSnapshot from(ShiftPlanAssignmentResult result) { + if (result == null) { + return new ShiftPlanPickSnapshot( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList()); + } + return new ShiftPlanPickSnapshot( + result.getByShift(), + result.getByEmployee(), + result.getIncompleteScheduleReasons()); + } + + public List getIncompleteScheduleReasons() { + return incompleteScheduleReasons; + } + + public List getByShift() { + return byShift; + } + + public List getByEmployee() { + return byEmployee; + } + + public int countAssignedStaff() { + int n = 0; + for (ShiftPlanAssignmentResult.AssignedShiftBlockView shift : byShift) { + for (ShiftPlanAssignmentResult.AssignedPostStaffView post : shift.getPostAssignments()) { + n += post.getStaff().size(); + } + } + return n; + } + + public int countTotalNeed() { + int n = 0; + for (ShiftPlanAssignmentResult.AssignedShiftBlockView shift : byShift) { + for (ShiftPlanAssignmentResult.AssignedPostStaffView post : shift.getPostAssignments()) { + n += post.getNeedCount(); + } + } + return n; + } + + public boolean isEmpty() { + return byShift.isEmpty(); + } + + /** pickSnap | 按班次:每个班次块 × 岗位 → 已 pick 员工列表。 */ + public String formatByShiftText() { + if (byShift.isEmpty()) { + return "pickSnap | 按班次 | (无数据)"; + } + StringBuilder sb = new StringBuilder(512); + sb.append("========== pickSnap | 按班次 ==========\n"); + for (ShiftPlanAssignmentResult.AssignedShiftBlockView shift : byShift) { + String kind = shift.isFixedScheduling() ? "固定" : "划线"; + String shiftId = + shift.getShiftId() == null || shift.getShiftId().isEmpty() ? "-" : shift.getShiftId(); + for (ShiftPlanAssignmentResult.AssignedPostStaffView post : shift.getPostAssignments()) { + sb.append("scheduleDay=") + .append(shift.getScheduleDay()) + .append(" | timeRange=") + .append(shift.getTimeRangeText()) + .append(" | kind=") + .append(kind) + .append(" | shiftId=") + .append(shiftId) + .append(" | postId=") + .append(post.getPostId()) + .append(" | positionName=") + .append(post.getPositionName() == null ? "" : post.getPositionName()) + .append(" | need=") + .append(post.getNeedCount()) + .append(" | picked=") + .append(post.getStaff().size()) + .append(" | pickedUsers=") + .append(formatPickedUsers(post.getStaff())) + .append('\n'); + } + } + return sb.toString(); + } + + /** pickSnap | 按员工:每位员工 → 已 pick 的班次列表。 */ + public String formatByEmployeeText() { + if (byEmployee.isEmpty()) { + return "pickSnap | 按员工 | (无数据)"; + } + StringBuilder sb = new StringBuilder(512); + sb.append("========== pickSnap | 按员工 ==========\n"); + for (ShiftPlanAssignmentResult.AssignedEmployeeView emp : byEmployee) { + sb.append("userId=") + .append(emp.getUserId()) + .append(" | userName=") + .append(emp.getUserName()) + .append(" | shiftCount=") + .append(emp.getShifts().size()) + .append('\n'); + if (emp.getShifts().isEmpty()) { + sb.append(" (无班次)\n"); + continue; + } + for (ShiftPlanAssignmentResult.AssignedShiftRef s : emp.getShifts()) { + String kind = s.isFixedScheduling() ? "固定" : "划线"; + String shiftId = + s.getShiftId() == null || s.getShiftId().isEmpty() ? "-" : s.getShiftId(); + sb.append(" - scheduleDay=") + .append(s.getScheduleDay()) + .append(" | timeRange=") + .append(s.getTimeRangeText()) + .append(" | postId=") + .append(s.getPostId()) + .append(" | kind=") + .append(kind) + .append(" | shiftId=") + .append(shiftId) + .append('\n'); + } + } + return sb.toString(); + } + + /** 摘要 + 按班次 + 按员工。 */ + public String formatPickSnapText() { + StringBuilder sb = new StringBuilder(1024); + sb.append("========== pickSnap | 排班选人快照 ==========\n"); + sb.append("shiftBlockViews=") + .append(byShift.size()) + .append(" | employees=") + .append(byEmployee.size()) + .append(" | pickedHeadcount=") + .append(countAssignedStaff()) + .append(" | needHeadcount=") + .append(countTotalNeed()) + .append(" | incompleteExplanationCount=") + .append(incompleteScheduleReasons.size()) + .append('\n'); + if (!incompleteScheduleReasons.isEmpty()) { + sb.append(formatIncompleteReasonLinesSnippet()); + } + sb.append(formatByShiftText()).append('\n'); + sb.append(formatByEmployeeText()); + sb.append("========== pickSnap | 结束 =========="); + return sb.toString(); + } + + private static String formatPickedUsers(List staff) { + if (staff == null || staff.isEmpty()) { + return "(空)"; + } + return staff.stream() + .map( + s -> { + String tag = s.isExtra() ? "通岗" : "专岗"; + String name = + s.getUserName() == null || s.getUserName().isEmpty() + ? s.getUserId() + : s.getUserName(); + return name + '(' + s.getUserId() + ")[" + tag + ']'; + }) + .collect(Collectors.joining(", ")); + } + + private String formatIncompleteReasonLinesSnippet() { + StringBuilder sb = new StringBuilder(incompleteScheduleReasons.size() * 64); + sb.append("---------- pickSnap | 不完整说明 ----------\n"); + for (ShiftPlanAssignmentResult.IncompleteScheduleReason r : incompleteScheduleReasons) { + sb.append("scheduleDay=") + .append(r.getScheduleDay()) + .append(" | code=") + .append(r.getReasonCode()) + .append(" | ") + .append(r.getTimeRangeText() == null || r.getTimeRangeText().isEmpty() + ? "" + : "range=" + r.getTimeRangeText() + " | ") + .append(r.getPostId() == null || r.getPostId().isEmpty() + ? "" + : "postId=" + r.getPostId() + " | ") + .append("msg=") + .append(r.getMessage()) + .append('\n'); + } + return sb.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPostNeed.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPostNeed.java new file mode 100644 index 0000000..21a1571 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/ShiftPlanPostNeed.java @@ -0,0 +1,48 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 排班班次块内的单岗位需求:工种(岗位)id + 人数。 + */ +public final class ShiftPlanPostNeed implements Serializable { + + private final String postId; + private final int needCount; + + public ShiftPlanPostNeed(String postId, int needCount) { + this.postId = postId == null ? "" : postId; + this.needCount = Math.max(0, needCount); + } + + public String getPostId() { + return postId; + } + + public int getNeedCount() { + return needCount; + } + + @Override + public String toString() { + return postId + "=" + needCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ShiftPlanPostNeed that = (ShiftPlanPostNeed) o; + return needCount == that.needCount && postId.equals(that.postId); + } + + @Override + public int hashCode() { + return Objects.hash(postId, needCount); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarDayDemandSteps4And5.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarDayDemandSteps4And5.java new file mode 100644 index 0000000..cbcda3d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarDayDemandSteps4And5.java @@ -0,0 +1,233 @@ +package jnpf.attendance.schedule; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 第四、五步:按排班日聚合相似历史半小时行 → (时间段,岗位) 稳健目标人数 → 半小时岗位需求矩阵。 + */ +public final class SimilarDayDemandSteps4And5 { + + private static final Logger LOG = LoggerFactory.getLogger(SimilarDayDemandSteps4And5.class); + + /** 测试观测:单张矩阵内逐格解释(中位数/P75)最多输出条数,避免超大门店刷屏。 */ + private static final int FOR_TEST_CELL_DETAIL_CAP = 250; + + private SimilarDayDemandSteps4And5() {} + + /** + * 对每个排班日生成一张 {@link HalfHourPostDemandMatrix}(仅包含有行数据的排班日;失败日无行则跳过该日不出现在 map 中)。 + * + * @param rows 须带 {@link SimilarHistoricalJobSlotRow#getTemplateScheduleDay()} + * @param similarTemplateDaysByScheduleDay 与第三步同源,用于取相似历史日列表(补 0 顺序) + * @param config 中位数 / P75 与是否补 0 + */ + public static Map buildPerScheduleDayDemandMatrices( + List rows, + Map similarTemplateDaysByScheduleDay, + SlotPostRobustTargetConfig config) { + if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) { + return Collections.emptyMap(); + } + Objects.requireNonNull(config, "config"); + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX, + "demand_matrix_global_config", + String.format( + "algo=SimilarDayDemandSteps4And5.buildPerScheduleDayDemandMatrices | SlotPostRobustTargetConfig.maxMinusMinUseMedianThreshold=%d " + + "| imputeZeroForMissingSimilarDays=%s | ruleInterpretation=maxMinusMinGtThreshold_Then_P75_else_Median", + config.getMaxMinusMinUseMedianThreshold(), + config.isImputeZeroForMissingSimilarDays())); + List safeRows = rows == null ? Collections.emptyList() : rows; + + Map out = new LinkedHashMap<>(); + for (Map.Entry e : similarTemplateDaysByScheduleDay.entrySet()) { + LocalDate scheduleDay = e.getKey(); + ScheduleTemplateSimilarDaysResult sim = e.getValue(); + if (scheduleDay == null || sim == null || !sim.isSuccess()) { + if (LOG.isErrorEnabled() && scheduleDay != null) { + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX, + "demand_matrix_skip_day", + "algo=SimilarDayDemandSteps4And5 | scheduleDay=" + + scheduleDay + + " | skippedBecauseSimilarDaysNotSuccess=" + + (sim == null ? "null_sim" : Boolean.toString(sim.isSuccess())) + + " | tier=" + + (sim == null ? "NONE" : sim.getTier()) + + " | msg=" + + (sim == null ? "" : sim.getMessage())); + } + continue; + } + List forDay = new ArrayList<>(); + for (SimilarHistoricalJobSlotRow r : safeRows) { + if (r != null && scheduleDay.equals(r.getTemplateScheduleDay())) { + forDay.add(r); + } + } + if (forDay.isEmpty()) { + continue; + } + HalfHourPostDemandMatrix matrix = + buildMatrixForScheduleDay(scheduleDay, forDay, sim.getSimilarDates(), config); + out.put(scheduleDay, matrix); + } + return Collections.unmodifiableMap(out); + } + + static HalfHourPostDemandMatrix buildMatrixForScheduleDay( + LocalDate scheduleDay, + List rowsForScheduleDay, + List similarDatesOrdered, + SlotPostRobustTargetConfig config) { + // 历史自然日 → 时间段 → 岗位 → 当日合计(同日多班次先相加) + Map>> byDaySlotPost = new LinkedHashMap<>(); + Map slotLabelToOrderStart = new LinkedHashMap<>(); + for (SimilarHistoricalJobSlotRow r : rowsForScheduleDay) { + if (r == null) { + continue; + } + String slot = r.getSlotRangeLabel(); + String post = r.getPostId(); + byDaySlotPost + .computeIfAbsent(r.getRowDate(), d -> new LinkedHashMap<>()) + .computeIfAbsent(slot, s -> new LinkedHashMap<>()) + .merge(post, r.getHeadcount(), Integer::sum); + slotLabelToOrderStart.putIfAbsent(slot, r.getSlotStartInclusive()); + } + + Set allSlots = new LinkedHashSet<>(); + Set allPosts = new TreeSet<>(); + for (Map> slotMap : byDaySlotPost.values()) { + for (Map.Entry> se : slotMap.entrySet()) { + allSlots.add(se.getKey()); + if (se.getValue() != null) { + allPosts.addAll(se.getValue().keySet()); + } + } + } + + List slotsOrdered = new ArrayList<>(allSlots); + slotsOrdered.sort(Comparator.comparing(slotLabelToOrderStart::get)); + + List postsOrdered = new ArrayList<>(allPosts); + + Map> targetBySlot = new LinkedHashMap<>(); + List similarDates = + similarDatesOrdered == null ? Collections.emptyList() : similarDatesOrdered; + + int cellLogs = 0; + for (String slot : slotsOrdered) { + Map row = new LinkedHashMap<>(); + for (String post : postsOrdered) { + List samples = buildSamplesForSlotPost(byDaySlotPost, slot, post, similarDates, config); + int target = SlotPostTargetHeadcountCalculator.robustTargetFromSamples(samples, config); + row.put(post, target); + if (LOG.isErrorEnabled() && cellLogs < FOR_TEST_CELL_DETAIL_CAP) { + cellLogs++; + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX, + "demand_matrix_robust_cell", + "algo=SimilarDayDemandSteps4And5 | scheduleDay=" + + scheduleDay + + " | slot=" + + slot + + " | post=" + + post + + " | similarDatesCount=" + + similarDates.size() + + " | distinctHistoryCalendarDaysInRows=" + + byDaySlotPost.size() + + " | " + + SlotPostTargetHeadcountCalculator.describeRobustAggregation(samples, config)); + } + } + targetBySlot.put(slot, row); + } + if (LOG.isErrorEnabled() && postsOrdered.size() * slotsOrdered.size() > FOR_TEST_CELL_DETAIL_CAP) { + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX, + "demand_matrix_robust_cell", + "algo=SimilarDayDemandSteps4And5 | scheduleDay=" + + scheduleDay + + " | note=per_cell_detail_capped | cap=" + + FOR_TEST_CELL_DETAIL_CAP + + " | totalCells=" + + (postsOrdered.size() * slotsOrdered.size())); + } + + HalfHourPostDemandMatrix matrix = new HalfHourPostDemandMatrix(scheduleDay, slotsOrdered, postsOrdered, targetBySlot); + if (LOG.isErrorEnabled()) { + SchedulingForTestCheckLog.info( + LOG, + SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX, + "demand_matrix_tsv", + "algo=SimilarDayDemandSteps4And5 | scheduleDay=" + + scheduleDay + + " | slotCount=" + + slotsOrdered.size() + + " | postCount=" + + postsOrdered.size() + + " | tsvSingleLine=" + + SchedulingForTestCheckLog.singleLineTsv(matrix.toTsvTable(), 20000)); + } + return matrix; + } + + private static List buildSamplesForSlotPost( + Map>> byDaySlotPost, + String slot, + String post, + List similarDates, + SlotPostRobustTargetConfig config) { + List samples = new ArrayList<>(); + if (config.isImputeZeroForMissingSimilarDays() && !similarDates.isEmpty()) { + for (LocalDate d : similarDates) { + if (d == null) { + continue; + } + int v = 0; + Map> slotMap = byDaySlotPost.get(d); + if (slotMap != null) { + Map postMap = slotMap.get(slot); + if (postMap != null && postMap.get(post) != null) { + v = postMap.get(post); + } + } + samples.add(v); + } + } else { + TreeSet days = new TreeSet<>(byDaySlotPost.keySet()); + for (LocalDate d : days) { + Map> slotMap = byDaySlotPost.get(d); + if (slotMap == null) { + continue; + } + Map postMap = slotMap.get(slot); + if (postMap == null || !postMap.containsKey(post)) { + continue; + } + samples.add(postMap.get(post)); + } + } + return samples; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalDaySlotTableBuilder.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalDaySlotTableBuilder.java new file mode 100644 index 0000000..f0c1f6d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalDaySlotTableBuilder.java @@ -0,0 +1,583 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import jnpf.model.attendance.vo.ShiftPostHeadcountVo; +import jnpf.model.attendance.vo.ShiftPostStatVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 将「相似历史日」对应的 {@link DayShiftRevenueStatVo} 班次,拆成半小时粒度、按岗位人数展开(表结构:日期 | 时间段 | 岗位 | 人数)。 + *

+ * 入参/出参字段级详细日志见 {@link #buildHalfHourRowsFromSuccessfulSimilarDays};可通过系统属性限制条数或把逐条降到 DEBUG(见类内说明)。 + */ +public final class SimilarHistoricalDaySlotTableBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(SimilarHistoricalDaySlotTableBuilder.class); + + private static final String P = "[SimilarHistoricalDaySlotTableBuilder]"; + + /** + * 入参 {@code fullDayShiftHistory} 最多打印多少条「日维度」明细(DEBUG)。 + * 系统属性:{@code jnpf.schedule.similarSlotLogInputHistoryMax}; + * 默认 {@code 0}(仅汇总);{@code <0} 不限制;{@code >0} 最多该条数。 + */ + + /** + * 入参 map 最多打印多少条 entry 明细(DEBUG)。系统属性:{@code jnpf.schedule.similarSlotLogInputMapMax};默认 {@code 0}。 + */ + + /** + * 出参最多打印多少行字段明细(DEBUG)。系统属性:{@code jnpf.schedule.similarSlotLogOutputRowsMax};默认 {@code 0}。 + */ + + /** + * 每个相似历史日最多打印多少条 {@code expand_shift}(DEBUG)。 + * 系统属性:{@code jnpf.schedule.similarSlotLogExpandShiftMax};默认 {@code 0}(不逐班次);{@code <0} 不限制;{@code >0} 上限。 + */ + + private static final long SLOT_NANOS = 30L * 60 * 1_000_000_000L; + private static final long NANOS_PER_DAY = 24L * 60 * 60 * 1_000_000_000L; + + private SimilarHistoricalDaySlotTableBuilder() {} + + /** + * 仅处理 {@link ScheduleTemplateSimilarDaysResult#isSuccess()} 为 true 的排班日;对每个相似历史日, + * 从 {@code fullDayShiftHistory} 中按 {@code day} 匹配 VO,再拆班次为半小时 × 岗位人数行。 + * + * @param fullDayShiftHistory 与 {@link jnpf.attendance.service.AttendanceDailyRuleService#getGroupShiftHistory90Days} 同源列表 + * @param similarTemplateDaysByScheduleDay 排班日 → 模板参考日筛选结果 + */ + public static List buildHalfHourRowsFromSuccessfulSimilarDays( + List fullDayShiftHistory, + Map similarTemplateDaysByScheduleDay) { + logInputFullDetail(fullDayShiftHistory, similarTemplateDaysByScheduleDay); + + if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) { + LOG.error( + "[SimilarHistoricalDaySlotTableBuilder] STEP | early_exit | reason=similarTemplateDaysByScheduleDay is null or empty"); + logOutputFullDetail(Collections.emptyList()); + return Collections.emptyList(); + } + + Map byDay = indexByDay(fullDayShiftHistory); + + List out = new ArrayList<>(); + int skippedNullResult = 0; + int skippedNotSuccess = 0; + int similarDateNull = 0; + int missingHistoryVo = 0; + int missingShiftsList = 0; + int scheduleDaysProcessed = 0; + int similarDatesExpanded = 0; + int shiftsProcessed = 0; + int rowsFromShifts = 0; + + for (Map.Entry e : similarTemplateDaysByScheduleDay.entrySet()) { + LocalDate scheduleDay = e.getKey(); + ScheduleTemplateSimilarDaysResult sim = e.getValue(); + if (sim == null) { + skippedNullResult++; + LOG.error("{} STEP | skip_schedule_day | scheduleDay={} | reason=result_null", P, scheduleDay); + continue; + } + if (!sim.isSuccess()) { + skippedNotSuccess++; + LOG.error( + "{} STEP | skip_schedule_day | scheduleDay={} | success=false | tier={} | message={}", + P, + scheduleDay, + sim.getTier(), + sim.getMessage()); + continue; + } + scheduleDaysProcessed++; + LOG.error( + "{} STEP | process_schedule_day | scheduleDay={} | tier={} | similarDates={}", + P, + scheduleDay, + sim.getTier(), + sim.getSimilarDates()); + + for (LocalDate similarDate : sim.getSimilarDates()) { + if (similarDate == null) { + similarDateNull++; + LOG.error( + "{} STEP | skip_similar_date | scheduleDay={} | reason=similarDate_null", P, scheduleDay); + continue; + } + DayShiftRevenueStatVo vo = byDay.get(similarDate.toString()); + if (vo == null) { + missingHistoryVo++; + LOG.error( + "{} STEP | skip_similar_date | scheduleDay={} | similarDate={} | reason=no_day_vo_in_history_index", + P, + scheduleDay, + similarDate); + continue; + } + if (vo.getShifts() == null) { + missingShiftsList++; + LOG.error( + "{} STEP | skip_similar_date | scheduleDay={} | similarDate={} | reason=shifts_null", + P, + scheduleDay, + similarDate); + continue; + } + similarDatesExpanded++; + int rowsThisSimilarDate = 0; + int shiftLogCap = similarSlotLogExpandShiftMax(); + int shiftLogged = 0; + List shifts = vo.getShifts(); + LOG.error( + "{} STEP | expand_similar_date | scheduleDay={} | similarDate={} | shiftCount={}", + P, + scheduleDay, + similarDate, + shifts.size()); + + for (ShiftPostStatVo shift : shifts) { + shiftsProcessed++; + List piece = expandShiftToHalfHourRows(similarDate, shift, scheduleDay); + rowsFromShifts += piece.size(); + rowsThisSimilarDate += piece.size(); + out.addAll(piece); + if (shouldLogExpandShiftDetail(shiftLogCap, shiftLogged)) { + LOG.error( + "{} STEP | expand_shift | similarDate={} | shiftId={} | shiftName={} | startTime={} | endTime={} | lineSchedule={} | rowsAdded={}", + P, + similarDate, + shift == null ? null : shift.getShiftId(), + shift == null ? null : shift.getShiftName(), + shift == null ? null : shift.getStartTime(), + shift == null ? null : shift.getEndTime(), + shift == null ? null : shift.getLineSchedule(), + piece.size()); + shiftLogged++; + } + } + LOG.error( + "{} STEP | expand_similar_date_done | scheduleDay={} | similarDate={} | shifts={} | rowsAdded={}", + P, + scheduleDay, + similarDate, + shifts.size(), + rowsThisSimilarDate); + } + } + + LOG.error( + "{} done | historyDays={} | scheduleDaysProcessed={} | similarDatesExpanded={} | shiftsProcessed={} | outputRows={} | skipped(nullResult={}, notSuccess={}, similarDateNull={}, missingHistoryVo={}, missingShiftsList={})", + P, + byDay.size(), + scheduleDaysProcessed, + similarDatesExpanded, + shiftsProcessed, + out.size(), + skippedNullResult, + skippedNotSuccess, + similarDateNull, + missingHistoryVo, + missingShiftsList); + LOG.error( + "{} STEP | aggregate | rowsFromShifts={}", + P, + rowsFromShifts); + logOutputFullDetail(out); + + return out; + } + + /** + * {@link ScheduleTemplateSimilarDaysResult#isSuccess()} 为 true,但聚合后无任何 {@link SimilarHistoricalJobSlotRow} + * (召回到的全部参考日的班次结构不可用或全部被解析过滤)。仅这些排班日在后续需求矩阵中为「假性成功」,需按日对用户提示; + * 见 intelligent-schedule-product-constraints_reply §2.4。 + */ + public static List listScheduleDaysWithSuccessfulSimilarityButZeroHalfHourRows( + Map similarTemplateDaysByScheduleDay, + List halfHourRows) { + if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) { + return Collections.emptyList(); + } + HashSet templateDaysWithRows = new HashSet<>(); + if (halfHourRows != null) { + for (SimilarHistoricalJobSlotRow r : halfHourRows) { + if (r != null && r.getTemplateScheduleDay() != null) { + templateDaysWithRows.add(r.getTemplateScheduleDay()); + } + } + } + List missing = new ArrayList<>(); + for (Map.Entry e : similarTemplateDaysByScheduleDay.entrySet()) { + LocalDate scheduleDay = e.getKey(); + ScheduleTemplateSimilarDaysResult sim = e.getValue(); + if (scheduleDay == null || sim == null || !sim.isSuccess()) { + continue; + } + if (!templateDaysWithRows.contains(scheduleDay)) { + missing.add(scheduleDay); + } + } + Collections.sort(missing); + return missing; + } + + private static int intSysProp(String key, int defaultValue) { + String s = System.getProperty(key); + if (s == null || s.trim().isEmpty()) { + return defaultValue; + } + try { + return Integer.parseInt(s.trim()); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private static int similarSlotLogInputHistoryMax() { + return intSysProp("jnpf.schedule.similarSlotLogInputHistoryMax", 0); + } + + private static int similarSlotLogInputMapMax() { + return intSysProp("jnpf.schedule.similarSlotLogInputMapMax", 0); + } + + private static int similarSlotLogOutputRowsMax() { + return intSysProp("jnpf.schedule.similarSlotLogOutputRowsMax", 0); + } + + private static int similarSlotLogExpandShiftMax() { + return intSysProp("jnpf.schedule.similarSlotLogExpandShiftMax", 0); + } + + /** {@code maxCfg < 0} 不限制;{@code 0} 不打印;{@code >0} 最多该条数。 */ + private static boolean shouldLogExpandShiftDetail(int maxCfg, int alreadyLogged) { + if (maxCfg < 0) { + return true; + } + if (maxCfg == 0) { + return false; + } + return alreadyLogged < maxCfg; + } + + /** + * @param configuredMax 系统属性取值:{@code <0} 不限制;{@code 0} 不输出逐条;{@code >0} 上限 + */ + private static int detailLogCap(int configuredMax, int naturalCount) { + if (configuredMax < 0) { + return naturalCount; + } + if (configuredMax == 0) { + return 0; + } + return Math.min(naturalCount, configuredMax); + } + + private static void logInputFullDetail( + List fullDayShiftHistory, + Map similarTemplateDaysByScheduleDay) { + if (!LOG.isErrorEnabled()) { + return; + } + LOG.error("{} INPUT | begin | fullDayShiftHistory", P); + if (fullDayShiftHistory == null) { + LOG.error("{} INPUT | fullDayShiftHistory | value=null", P); + } else { + int n = fullDayShiftHistory.size(); + int maxCfg = similarSlotLogInputHistoryMax(); + int cap = detailLogCap(maxCfg, n); + LOG.error( + "{} INPUT | fullDayShiftHistory | listSize={} | detailLoggedItems={} | limitProperty=jnpf.schedule.similarSlotLogInputHistoryMax (<0=unlimited,0=summary_only,>0=cap)", + P, + n, + cap); + if (maxCfg > 0 && n > cap) { + LOG.error( + "{} INPUT | fullDayShiftHistory | truncated | notLoggedItemCount={} | raise jnpf.schedule.similarSlotLogInputHistoryMax or set to <0 for unlimited", + P, + n - cap); + } + for (int i = 0; i < cap; i++) { + DayShiftRevenueStatVo vo = fullDayShiftHistory.get(i); + if (vo == null) { + LOG.error("{} INPUT | fullDayShiftHistory[{}] | value=null", P, i); + continue; + } + BigDecimal rev = vo.getRevenue(); + LOG.error( + "{} INPUT | fullDayShiftHistory[{}] | day={} | revenue={}", + P, + i, + vo.getDay(), + rev == null ? "null" : rev.toPlainString()); + List shifts = vo.getShifts(); + if (shifts == null) { + LOG.error("{} INPUT | fullDayShiftHistory[{}] | shifts=null", P, i); + continue; + } + LOG.error("{} INPUT | fullDayShiftHistory[{}] | shifts.size={}", P, i, shifts.size()); + for (int j = 0; j < shifts.size(); j++) { + ShiftPostStatVo sh = shifts.get(j); + if (sh == null) { + LOG.error("{} INPUT | fullDayShiftHistory[{}].shifts[{}] | value=null", P, i, j); + continue; + } + LOG.error( + "{} INPUT | fullDayShiftHistory[{}].shifts[{}] | shiftId={} | shiftName={} | startTime={} | endTime={} | lineSchedule={}", + P, + i, + j, + sh.getShiftId(), + sh.getShiftName(), + sh.getStartTime(), + sh.getEndTime(), + sh.getLineSchedule()); + List posts = sh.getPosts(); + if (posts == null) { + LOG.error("{} INPUT | fullDayShiftHistory[{}].shifts[{}] | posts=null", P, i, j); + continue; + } + LOG.error("{} INPUT | fullDayShiftHistory[{}].shifts[{}] | posts.size={}", P, i, j, posts.size()); + for (int k = 0; k < posts.size(); k++) { + ShiftPostHeadcountVo p = posts.get(k); + if (p == null) { + LOG.error( + "{} INPUT | fullDayShiftHistory[{}].shifts[{}].posts[{}] | value=null", + P, i, j, k); + continue; + } + LOG.error( + "{} INPUT | fullDayShiftHistory[{}].shifts[{}].posts[{}] | postId={} | headcount={}", + P, + i, + j, + k, + p.getPostId(), + p.getHeadcount()); + } + } + } + } + + LOG.error("{} INPUT | begin | similarTemplateDaysByScheduleDay", P); + if (similarTemplateDaysByScheduleDay == null) { + LOG.error("{} INPUT | similarTemplateDaysByScheduleDay | value=null", P); + } else { + int m = similarTemplateDaysByScheduleDay.size(); + int maxMapCfg = similarSlotLogInputMapMax(); + int mapCap = detailLogCap(maxMapCfg, m); + LOG.error( + "{} INPUT | similarTemplateDaysByScheduleDay | mapSize={} | detailLoggedEntries={} | limitProperty=jnpf.schedule.similarSlotLogInputMapMax (<0=unlimited,0=summary_only,>0=cap)", + P, + m, + mapCap); + if (maxMapCfg > 0 && m > mapCap) { + LOG.error( + "{} INPUT | similarTemplateDaysByScheduleDay | truncated | notLoggedEntryCount={}", + P, + m - mapCap); + } + int ei = 0; + for (Map.Entry e : similarTemplateDaysByScheduleDay.entrySet()) { + if (ei >= mapCap) { + break; + } + LocalDate scheduleDay = e.getKey(); + ScheduleTemplateSimilarDaysResult r = e.getValue(); + LOG.error( + "{} INPUT | similarTemplateDaysByScheduleDay[{}] | mapKey.scheduleDay={}", + P, + ei, + scheduleDay); + if (r == null) { + LOG.error("{} INPUT | similarTemplateDaysByScheduleDay[{}] | value=null", P, ei); + } else { + LOG.error( + "{} INPUT | similarTemplateDaysByScheduleDay[{}] | success={} | tier={} | message={}", + P, + ei, + r.isSuccess(), + r.getTier(), + r.getMessage()); + List sims = r.getSimilarDates(); + LOG.error( + "{} INPUT | similarTemplateDaysByScheduleDay[{}] | similarDates.size={}", + P, + ei, + sims == null ? "null" : sims.size()); + if (sims != null) { + for (int si = 0; si < sims.size(); si++) { + LOG.error( + "{} INPUT | similarTemplateDaysByScheduleDay[{}] | similarDates[{}]={}", + P, + ei, + si, + sims.get(si)); + } + } + } + ei++; + } + } + LOG.error("{} INPUT | end", P); + } + + private static void logOutputFullDetail(List out) { + if (!LOG.isErrorEnabled()) { + return; + } + LOG.error("{} OUTPUT | begin", P); + if (out == null) { + LOG.error("{} OUTPUT | list=null", P); + LOG.error("{} OUTPUT | end", P); + return; + } + int n = out.size(); + int maxOutCfg = similarSlotLogOutputRowsMax(); + int cap = detailLogCap(maxOutCfg, n); + LOG.error( + "{} OUTPUT | rowCount={} | detailLoggedRows={} | limitProperty=jnpf.schedule.similarSlotLogOutputRowsMax (<0=unlimited,0=summary_only,>0=cap)", + P, + n, + cap); + if (maxOutCfg > 0 && n > cap) { + LOG.error("{} OUTPUT | truncated | notLoggedRowCount={}", P, n - cap); + } + for (int i = 0; i < cap; i++) { + SimilarHistoricalJobSlotRow r = out.get(i); + if (r == null) { + LOG.error("{} OUTPUT | rows[{}] | value=null", P, i); + continue; + } + LOG.error( + "{} OUTPUT | rows[{}] | rowDate={} | slotStartInclusive={} | slotEndExclusive={} | slotRangeLabel={} | postId={} | headcount={} | shiftId={} | templateScheduleDay={}", + P, + i, + r.getRowDate(), + r.getSlotStartInclusive(), + r.getSlotEndExclusive(), + r.getSlotRangeLabel(), + r.getPostId(), + r.getHeadcount(), + r.getShiftId(), + r.getTemplateScheduleDay()); + } + LOG.error("{} OUTPUT | end", P); + } + + private static Map indexByDay(List list) { + Map map = new LinkedHashMap<>(); + if (list == null) { + return map; + } + for (DayShiftRevenueStatVo vo : list) { + if (vo != null && vo.getDay() != null && !vo.getDay().trim().isEmpty()) { + String trimmed = vo.getDay().trim(); + String key = AttendanceScheduleDayParse.normalizeToIsoDayString(trimmed).orElse(trimmed); + map.putIfAbsent(key, vo); + } + } + return map; + } + + private static List expandShiftToHalfHourRows( + LocalDate rowDate, ShiftPostStatVo shift, LocalDate templateScheduleDay) { + if (shift == null) { + return Collections.emptyList(); + } + LocalTime start = parseHhMm(shift.getStartTime()); + LocalTime end = parseHhMm(shift.getEndTime()); + if (start == null || end == null) { + return Collections.emptyList(); + } + if (start.equals(end)) { + return Collections.emptyList(); + } + long sn = start.toNanoOfDay(); + long en = end.toNanoOfDay(); + List out = new ArrayList<>(); + if (en > sn) { + out.addAll(expandNanoRange(rowDate, sn, en, shift, templateScheduleDay)); + } else { + out.addAll(expandNanoRange(rowDate, sn, NANOS_PER_DAY, shift, templateScheduleDay)); + out.addAll(expandNanoRange(rowDate.plusDays(1), 0, en, shift, templateScheduleDay)); + } + return out; + } + + /** 左闭右开区间 [startNanoInclusive, endNanoExclusive),且均在同一自然日内({@code endNanoExclusive ≤ 24h})。 */ + private static List expandNanoRange( + LocalDate rowDate, + long startNanoInclusive, + long endNanoExclusive, + ShiftPostStatVo shift, + LocalDate templateScheduleDay) { + if (endNanoExclusive <= startNanoInclusive) { + return Collections.emptyList(); + } + List rows = new ArrayList<>(); + String sid = Objects.toString(shift.getShiftId(), ""); + for (long t = startNanoInclusive; t < endNanoExclusive; t += SLOT_NANOS) { + long rawEnd = Math.min(t + SLOT_NANOS, endNanoExclusive); + LocalTime st = LocalTime.ofNanoOfDay(t); + LocalTime enTime = + rawEnd >= NANOS_PER_DAY ? LocalTime.MIDNIGHT : LocalTime.ofNanoOfDay(rawEnd); + addPostRows(rows, rowDate, st, enTime, shift.getPosts(), sid, templateScheduleDay); + } + return rows; + } + + private static void addPostRows( + List out, + LocalDate rowDate, + LocalTime slotStart, + LocalTime slotEnd, + List posts, + String shiftId, + LocalDate templateScheduleDay) { + if (posts == null) { + return; + } + for (ShiftPostHeadcountVo p : posts) { + if (p == null) { + continue; + } + int hc = p.getHeadcount() == null ? 0 : p.getHeadcount(); + if (hc <= 0) { + continue; + } + out.add( + new SimilarHistoricalJobSlotRow( + rowDate, slotStart, slotEnd, p.getPostId(), hc, shiftId, templateScheduleDay)); + } + } + + private static LocalTime parseHhMm(String hhMm) { + if (hhMm == null) { + return null; + } + String s = hhMm.trim(); + if (s.isEmpty()) { + return null; + } + try { + return LocalTime.parse(s); + } catch (DateTimeParseException e) { + return null; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalJobSlotRow.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalJobSlotRow.java new file mode 100644 index 0000000..92a6f6e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SimilarHistoricalJobSlotRow.java @@ -0,0 +1,92 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Objects; + +/** + * 相似历史日模板拆成的「自然日 + 半小时槽 + 岗位 + 人数」一行,对应需求侧表结构。 + *

+ * 岗位展示默认使用 {@link #getPostId()};名称需业务侧用岗位字典解析。 + */ +public final class SimilarHistoricalJobSlotRow implements Serializable { + + private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm"); + + private final LocalDate rowDate; + private final LocalTime slotStartInclusive; + private final LocalTime slotEndExclusive; + private final String postId; + private final int headcount; + private final String shiftId; + /** 排班区间内的「模板日」:本行由该排班日对应的相似历史日展开得到,用于第四步按日聚合。 */ + private final LocalDate templateScheduleDay; + + public SimilarHistoricalJobSlotRow( + LocalDate rowDate, + LocalTime slotStartInclusive, + LocalTime slotEndExclusive, + String postId, + int headcount, + String shiftId, + LocalDate templateScheduleDay) { + this.rowDate = Objects.requireNonNull(rowDate, "rowDate"); + this.slotStartInclusive = Objects.requireNonNull(slotStartInclusive, "slotStartInclusive"); + this.slotEndExclusive = Objects.requireNonNull(slotEndExclusive, "slotEndExclusive"); + this.postId = postId == null ? "" : postId; + this.headcount = Math.max(0, headcount); + this.shiftId = shiftId == null ? "" : shiftId; + this.templateScheduleDay = Objects.requireNonNull(templateScheduleDay, "templateScheduleDay"); + } + + public LocalDate getRowDate() { + return rowDate; + } + + public LocalTime getSlotStartInclusive() { + return slotStartInclusive; + } + + public LocalTime getSlotEndExclusive() { + return slotEndExclusive; + } + + public String getPostId() { + return postId; + } + + public int getHeadcount() { + return headcount; + } + + public String getShiftId() { + return shiftId; + } + + public LocalDate getTemplateScheduleDay() { + return templateScheduleDay; + } + + /** 与文档表头「时间段」一致,如 {@code 09:00-09:30}(左闭右开半小时);尾段跨午夜时显示为 {@code 23:30-00:00}。 */ + public String getSlotRangeLabel() { + if (slotEndExclusive.equals(LocalTime.MIDNIGHT) && slotStartInclusive.isAfter(LocalTime.MIDNIGHT)) { + return slotStartInclusive.format(HM) + "-00:00"; + } + return slotStartInclusive.format(HM) + "-" + slotEndExclusive.format(HM); + } + + @Override + public String toString() { + return rowDate + + "\t" + + getSlotRangeLabel() + + "\t" + + postId + + "\t" + + headcount + + "\t" + + templateScheduleDay; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostRobustTargetConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostRobustTargetConfig.java new file mode 100644 index 0000000..60c1878 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostRobustTargetConfig.java @@ -0,0 +1,60 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 第四步「时间段 + 岗位」稳健目标人数:波动小用中位数,波动大用 P75。 + */ +public final class SlotPostRobustTargetConfig implements Serializable { + + /** + * 当 {@code max(samples) - min(samples) <= maxMinusMinUseMedianThreshold} 时取中位数,否则取P75(最近序位法)。 + * 默认 1:如 2,2,2,2,2(极差 0)用中位数;如 1,2,2,3,4(极差 3)用 P75。 + */ + private final int maxMinusMinUseMedianThreshold; + + /** + * 为 true 时:对每个相似历史日若该日无 (时间段,岗位) 样本则按 0 计入,再算中位数/P75; + * 为 false 时:仅使用在原始行中出现过的历史日样本(样本量可能少于相似日个数)。 + */ + private final boolean imputeZeroForMissingSimilarDays; + + public SlotPostRobustTargetConfig(int maxMinusMinUseMedianThreshold, boolean imputeZeroForMissingSimilarDays) { + if (maxMinusMinUseMedianThreshold < 0) { + throw new IllegalArgumentException("maxMinusMinUseMedianThreshold must be >= 0"); + } + this.maxMinusMinUseMedianThreshold = maxMinusMinUseMedianThreshold; + this.imputeZeroForMissingSimilarDays = imputeZeroForMissingSimilarDays; + } + + public static SlotPostRobustTargetConfig defaults() { + return new SlotPostRobustTargetConfig(1, false); + } + + public int getMaxMinusMinUseMedianThreshold() { + return maxMinusMinUseMedianThreshold; + } + + public boolean isImputeZeroForMissingSimilarDays() { + return imputeZeroForMissingSimilarDays; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SlotPostRobustTargetConfig that = (SlotPostRobustTargetConfig) o; + return maxMinusMinUseMedianThreshold == that.maxMinusMinUseMedianThreshold + && imputeZeroForMissingSimilarDays == that.imputeZeroForMissingSimilarDays; + } + + @Override + public int hashCode() { + return Objects.hash(maxMinusMinUseMedianThreshold, imputeZeroForMissingSimilarDays); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostTargetHeadcountCalculator.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostTargetHeadcountCalculator.java new file mode 100644 index 0000000..0db15a8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/SlotPostTargetHeadcountCalculator.java @@ -0,0 +1,107 @@ +package jnpf.attendance.schedule; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 第四步:由「相似历史日」在同一 (时间段, 岗位) 上的人数样本,计算稳健目标人数。 + */ +public final class SlotPostTargetHeadcountCalculator { + + private SlotPostTargetHeadcountCalculator() {} + + /** + * 规则:若 {@code max - min <= config.maxMinusMinUseMedianThreshold} 取中位数,否则取P75(最近序位: + * 第 {@code ceil(0.75 * n)} 小的数,{@code n} 为样本量)。 + */ + public static int robustTargetFromSamples(List samples, SlotPostRobustTargetConfig config) { + if (config == null) { + throw new IllegalArgumentException("config"); + } + if (samples == null || samples.isEmpty()) { + return 0; + } + List sorted = new ArrayList<>(samples); + Collections.sort(sorted); + int min = sorted.get(0); + int max = sorted.get(sorted.size() - 1); + if (max - min <= config.getMaxMinusMinUseMedianThreshold()) { + return median(sorted); + } + return p75NearestRank(sorted); + } + + /** + * 供测试观测:明文解释「极差 vs 阈值 → 中位数/P75」的判定与结果(不写业务语义外的推断)。 + */ + public static String describeRobustAggregation(List samples, SlotPostRobustTargetConfig config) { + if (config == null) { + return "config=null"; + } + if (samples == null || samples.isEmpty()) { + return "samples=[] sorted=[] spread=n/a threshold=" + + config.getMaxMinusMinUseMedianThreshold() + + " imputeZero=" + + config.isImputeZeroForMissingSimilarDays() + + " target=0 rule=EMPTY_SAMPLES"; + } + List sorted = new ArrayList<>(samples); + Collections.sort(sorted); + int min = sorted.get(0); + int max = sorted.get(sorted.size() - 1); + int spread = max - min; + int thresh = config.getMaxMinusMinUseMedianThreshold(); + boolean useMedian = spread <= thresh; + int target = robustTargetFromSamples(samples, config); + String rule = useMedian ? "MEDIAN_STABLE_SPREAD_LE_THRESHOLD" : "P75_VOLATILE_SPREAD_GT_THRESHOLD"; + return "samples=" + + samples + + " sorted=" + + sorted + + " sampleSize=" + + sorted.size() + + " min=" + + min + + " max=" + + max + + " spread=max-min=" + + spread + + " maxMinusMinUseMedianThreshold=" + + thresh + + " imputeZeroForMissingSimilarDays=" + + config.isImputeZeroForMissingSimilarDays() + + " target=" + + target + + " rule=" + + rule; + } + + /** 升序列表上的中位数,整数结果(偶数个时上取整平均)。 */ + public static int median(List sortedAscending) { + int n = sortedAscending.size(); + if (n == 0) { + return 0; + } + if ((n & 1) == 1) { + return sortedAscending.get(n / 2); + } + int a = sortedAscending.get(n / 2 - 1); + int b = sortedAscending.get(n / 2); + return (a + b + 1) / 2; + } + + /** + * 最近序位 P75:第 {@code ceil(0.75 * n)} 个最小值(1-based 序位)。 + * 例如 n=5 时为第 4 小 → 对 1,2,2,3,4 得 3。 + */ + public static int p75NearestRank(List sortedAscending) { + int n = sortedAscending.size(); + if (n == 0) { + return 0; + } + int rank1Based = (int) Math.ceil(0.75 * n); + int idx = Math.min(n - 1, Math.max(0, rank1Based - 1)); + return sortedAscending.get(idx); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentContext.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentContext.java new file mode 100644 index 0000000..3b19b2a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentContext.java @@ -0,0 +1,84 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.Objects; + +/** + * 单次「拟分配」的规则判定上下文,供第九、十步 {@link StaffRuleEvaluationPort} 使用。 + *

+ * 由 {@link ShiftPlanFinalStaffAssigner#buildContext} 按每位员工构造 + * (因日工时、休息间隔因人而异);{@link ScheduleStaffAssignmentSteps910} 场景下可能多人共用同一 context。 + */ +public final class StaffAssignmentContext implements Serializable { + + private final LocalDate scheduleDay; + private final String postId; + /** 与 {@link HalfHourPostDemandMatrix} 行标签一致;可为 null 表示按整日聚合判断 */ + private final String slotRangeLabel; + private final String groupId; + /** + * 该员工在本排班日、本段班之前已排工时(小时)。 + * 来源:{@link SchedulePeriodWorkTracker#getSameDayScheduledHours}。 + */ + private final double existingSameDayScheduledWorkHours; + /** + * 本段 {@link ShiftPlanBlock} 的工时(小时),与 existing 相加后用于日工时上限判断。 + */ + private final double proposedAssignmentWorkHours; + /** + * 本段开始与上一段结束之间的间隔(分钟)。 + * {@code null} 表示无上一段或无法计算 → {@link StaffRuleEvaluationPort#violatesMinRestBetweenShifts} 不判违反。 + */ + private final Integer minutesRestSincePriorShiftEnd; + + /** 简化构造:无已排工时、无休息间隔(仅用于联调或 Steps910 单上下文场景)。 */ + public StaffAssignmentContext(LocalDate scheduleDay, String postId, String slotRangeLabel, String groupId) { + this(scheduleDay, postId, slotRangeLabel, groupId, 0d, 0d, null); + } + + public StaffAssignmentContext( + LocalDate scheduleDay, + String postId, + String slotRangeLabel, + String groupId, + double existingSameDayScheduledWorkHours, + double proposedAssignmentWorkHours, + Integer minutesRestSincePriorShiftEnd) { + this.scheduleDay = Objects.requireNonNull(scheduleDay, "scheduleDay"); + this.postId = postId == null ? "" : postId; + this.slotRangeLabel = slotRangeLabel; + this.groupId = groupId == null ? "" : groupId; + this.existingSameDayScheduledWorkHours = Math.max(0d, existingSameDayScheduledWorkHours); + this.proposedAssignmentWorkHours = Math.max(0d, proposedAssignmentWorkHours); + this.minutesRestSincePriorShiftEnd = minutesRestSincePriorShiftEnd; + } + + public LocalDate getScheduleDay() { + return scheduleDay; + } + + public String getPostId() { + return postId; + } + + public String getSlotRangeLabel() { + return slotRangeLabel; + } + + public String getGroupId() { + return groupId; + } + + public double getExistingSameDayScheduledWorkHours() { + return existingSameDayScheduledWorkHours; + } + + public double getProposedAssignmentWorkHours() { + return proposedAssignmentWorkHours; + } + + public Integer getMinutesRestSincePriorShiftEnd() { + return minutesRestSincePriorShiftEnd; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentDayLedger.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentDayLedger.java new file mode 100644 index 0000000..0246637 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentDayLedger.java @@ -0,0 +1,79 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * 排班「五-3」逻辑:同一员工在同一自然日内的固定班 vs 划线班互斥账本。 + *

+ * 在 {@link ShiftPlanFinalStaffAssigner#assign} / {@link ScheduleStaffAssignmentSteps910#pickForPostDemand} + * 选人成功时调用 {@link #markFixed} 或 {@link #markLineShift};选人前调用 {@link #canAssignFixed} / + * {@link #canAssignLine} 过滤。 + *

+ * 规则摘要 + *

    + *
  • 同日已划一线 → 不可再排固定班;
  • + *
  • 同日已排固定班 → 不可再划一线;
  • + *
  • 同日已排固定班 A → 仅可继续排相同 {@code fixedShiftGroupKey} 的固定段(多时段同班);
  • + *
  • 不同 fixedShiftGroupKey 的固定班在同日互斥。
  • + *
+ * 划线班允许多段,仅需 {@code lineUsed=true},不区分段数。 + */ +public final class StaffAssignmentDayLedger implements Serializable { + + private final Map> byEmployeeThenDay = new HashMap<>(); + + private DayMarks marks(String employeeId, LocalDate day) { + return byEmployeeThenDay + .computeIfAbsent(employeeId == null ? "" : employeeId, k -> new HashMap<>()) + .computeIfAbsent(day, d -> new DayMarks()); + } + + /** + * 是否仍可分配固定班段。 + * + * @param fixedShiftGroupKey 逻辑班次键,见 {@link ShiftPlanFinalStaffAssigner#fixedShiftGroupKey} + */ + public boolean canAssignFixed(String employeeId, LocalDate day, String fixedShiftGroupKey) { + DayMarks m = marks(employeeId, day); + if (m.lineUsed) { + return false; + } + if (m.fixedUsed) { + return Objects.equals(m.fixedShiftGroupKey, fixedShiftGroupKey); + } + return true; + } + + /** 是否仍可分配划线班段(当日尚未占用固定班)。 */ + public boolean canAssignLine(String employeeId, LocalDate day) { + DayMarks m = marks(employeeId, day); + return !m.fixedUsed; + } + + /** 记录该员工当日已排固定班,并绑定逻辑班次键。 */ + public void markFixed(String employeeId, LocalDate day, String fixedShiftGroupKey) { + DayMarks m = marks(employeeId, day); + m.fixedUsed = true; + m.fixedShiftGroupKey = fixedShiftGroupKey; + } + + /** 记录该员工当日已参与划线班(可多段多次调用)。 */ + public void markLineShift(String employeeId, LocalDate day) { + DayMarks m = marks(employeeId, day); + m.lineUsed = true; + } + + /** 员工 × 自然日 的固定/划线占用标记。 */ + private static final class DayMarks implements Serializable { + /** 当日是否已排固定班 */ + boolean fixedUsed; + /** 当日固定班逻辑键(仅 fixedUsed 时有意义) */ + String fixedShiftGroupKey; + /** 当日是否已排划线班 */ + boolean lineUsed; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentIntervalLedger.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentIntervalLedger.java new file mode 100644 index 0000000..06fbc2d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffAssignmentIntervalLedger.java @@ -0,0 +1,56 @@ +package jnpf.attendance.schedule; + +import java.io.Serializable; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 同一员工在的自然日内已占位班段(左闭右开分钟坐标),用于禁止时间重叠。 + *

+ * 与 {@link StaffAssignmentDayLedger} 分工:账本管固定 vs 划线互斥与同 logical key;本类管「不能同时出现在两个重叠时段」。 + */ +public final class StaffAssignmentIntervalLedger implements Serializable { + + private final Map>> byEmployeeThenDay = new HashMap<>(); + + /** 左闭右开区间 [startInclusive, endExclusive) 是否与已占位区间重叠。 */ + public boolean canAssign( + String employeeId, LocalDate day, int startInclusiveMinute, int endExclusiveMinute) { + if (employeeId == null || employeeId.trim().isEmpty() || day == null) { + return false; + } + if (endExclusiveMinute <= startInclusiveMinute) { + return false; + } + String id = employeeId.trim(); + for (int[] iv : intervals(id, day)) { + if (intervalsOverlap(iv[0], iv[1], startInclusiveMinute, endExclusiveMinute)) { + return false; + } + } + return true; + } + + public void mark(String employeeId, LocalDate day, int startInclusiveMinute, int endExclusiveMinute) { + if (employeeId == null || employeeId.trim().isEmpty() || day == null) { + return; + } + if (endExclusiveMinute <= startInclusiveMinute) { + return; + } + intervals(employeeId.trim(), day).add(new int[] {startInclusiveMinute, endExclusiveMinute}); + } + + private List intervals(String employeeId, LocalDate day) { + return byEmployeeThenDay + .computeIfAbsent(employeeId, k -> new HashMap<>()) + .computeIfAbsent(day, d -> new ArrayList<>()); + } + + private static boolean intervalsOverlap(int a0, int a1, int b0, int b1) { + return a0 < b1 && b0 < a1; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPort.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPort.java new file mode 100644 index 0000000..f541894 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPort.java @@ -0,0 +1,65 @@ +package jnpf.attendance.schedule; + +/** + * 智能排班第九、十步规则判定的统一端口,由业务侧接入真实人事/考勤数据。 + *

+ * 第九步:{@link #passesOperationalHardConstraints} — 运营硬约束(请假、借调、资格、班次冲突等), + * 违反任一则不可排该时段/岗位。
+ * 第十步:五个 {@code violates*} 方法 — 与 {@link jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo} + * 配合,由 {@link FixedSoftConstraintRelaxationPlanner} / {@link ShiftPlanFinalStaffAssigner} 按「必须/尽量」调用。 + *

+ * 实现 + *

    + *
  • {@link StaffRuleEvaluationPortPermissive} — 全放行,骨架联调;
  • + *
  • {@link StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation} — 固定规则 + 员工工作状态 VO(生产默认)。
  • + *
+ */ +public interface StaffRuleEvaluationPort { + + /** + * 第九步:是否通过运营硬约束。 + *

+ * 预期聚合:未离职、未请假、未外出、未借调、时段可用、岗位资格、不与已有班次冲突等 + * (具体由实现类对接各业务服务)。当前 {@link StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation} + * 仅校验 employeeId 非空。 + * + * @return true 表示可进入第十步固定规则判断 + */ + boolean passesOperationalHardConstraints(String employeeId, StaffAssignmentContext context); + + /** + * 第十步:单日工作小时数是否违反上限。 + *

+ * 语义:{@code existingSameDayScheduledWorkHours + proposedAssignmentWorkHours > cap}; + * 是否启用、cap 值、必须/尽量由 {@link FixedSchedulingRuleVo} 与 {@link FixedSoftRuleKind#DAILY_WORK_HOURS} 决定。 + */ + boolean violatesDailyWorkHoursLimit(String employeeId, StaffAssignmentContext context); + + /** + * 第十步:连续工作天数是否违反上限。 + *

+ * 通常结合 {@link SchedulePeriodWorkTracker} 更新后的 {@code UserWorkSituationVo.continuousDays}。 + */ + boolean violatesConsecutiveWorkDaysLimit(String employeeId, StaffAssignmentContext context); + + /** + * 第十步:每周工作天数是否违反上限。 + *

+ * 通常结合 {@code UserWorkSituationVo.weekWorkDays}(每次新排入一个自然日时 +1)。 + */ + boolean violatesWeeklyWorkDaysLimit(String employeeId, StaffAssignmentContext context); + + /** + * 第十步:每周工作小时数是否违反上限。 + *

+ * 语义:{@code weekWorkHours + proposedAssignmentWorkHours > cap}。 + */ + boolean violatesWeeklyWorkHoursLimit(String employeeId, StaffAssignmentContext context); + + /** + * 第十步:与上一班次间隔是否短于最小休息要求。 + *

+ * {@link StaffAssignmentContext#getMinutesRestSincePriorShiftEnd()} 为 null 时实现应返回 false(不判违反)。 + */ + boolean violatesMinRestBetweenShifts(String employeeId, StaffAssignmentContext context); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortPermissive.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortPermissive.java new file mode 100644 index 0000000..178bcd6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortPermissive.java @@ -0,0 +1,42 @@ +package jnpf.attendance.schedule; + +/** + * {@link StaffRuleEvaluationPort} 的全放行实现:硬约束与五个固定维度均视为不违反。 + *

+ * 用于算法骨架联调、单元测试或尚未接入人事数据时;生产环境应使用 + * {@link StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation}。 + *

+ * 当 {@link AutoScheduleService} 无规则配置且无工作状态数据时也会回退到本实现。 + */ +public final class StaffRuleEvaluationPortPermissive implements StaffRuleEvaluationPort { + + @Override + public boolean passesOperationalHardConstraints(String employeeId, StaffAssignmentContext context) { + return true; + } + + @Override + public boolean violatesDailyWorkHoursLimit(String employeeId, StaffAssignmentContext context) { + return false; + } + + @Override + public boolean violatesConsecutiveWorkDaysLimit(String employeeId, StaffAssignmentContext context) { + return false; + } + + @Override + public boolean violatesWeeklyWorkDaysLimit(String employeeId, StaffAssignmentContext context) { + return false; + } + + @Override + public boolean violatesWeeklyWorkHoursLimit(String employeeId, StaffAssignmentContext context) { + return false; + } + + @Override + public boolean violatesMinRestBetweenShifts(String employeeId, StaffAssignmentContext context) { + return false; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation.java new file mode 100644 index 0000000..7f8110c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation.java @@ -0,0 +1,147 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.attendance.UserWorkSituationVo; +import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Map; + +/** + * {@link StaffRuleEvaluationPort} 的生产实现:结合 {@link FixedSchedulingRuleVo} 与 + * {@link UserWorkSituationVo}(通常来自 {@link SchedulePeriodWorkTracker#asSituationMap()})判定第十步各维度。 + *

+ * 由 {@link AutoScheduleService#buildStaffRuleEvaluationPort} 组装并注入 + * {@link ShiftPlanFinalStaffAssigner#assign}。 + *

+ * 第九步:当前仅校验 employeeId 非空;请假/借调/资格等待接入其它数据源后扩展 + * {@link #passesOperationalHardConstraints}。 + */ +public final class StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation implements StaffRuleEvaluationPort { + + private final FixedSchedulingRuleVo fixedSchedulingRuleVo; + private final Map situationByUserId; + + /** + * @param fixedSchedulingRuleVo 考勤组固定排班规则;可为 null(全部 violates* 返回 false) + * @param situationByUserId 员工工作状态;排班过程中应使用 Tracker 的可变 map + */ + public StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation( + FixedSchedulingRuleVo fixedSchedulingRuleVo, + Map situationByUserId) { + this.fixedSchedulingRuleVo = fixedSchedulingRuleVo; + this.situationByUserId = + situationByUserId == null ? Collections.emptyMap() : situationByUserId; + } + + @Override + public boolean passesOperationalHardConstraints(String employeeId, StaffAssignmentContext context) { + return employeeId != null && !employeeId.trim().isEmpty(); + } + + /** + * 规则未启用或 cap 无效 → 不违反。 + * 违反条件:当日已排 + 本段 proposed > {@code dailyWorkHoursValue}。 + */ + @Override + public boolean violatesDailyWorkHoursLimit(String employeeId, StaffAssignmentContext context) { + FixedSchedulingRuleVo vo = fixedSchedulingRuleVo; + if (vo == null || !Boolean.TRUE.equals(vo.getDailyWorkHoursEnabled())) { + return false; + } + Integer cap = vo.getDailyWorkHoursValue(); + if (cap == null || cap <= 0) { + return false; + } + double total = + context.getExistingSameDayScheduledWorkHours() + context.getProposedAssignmentWorkHours(); + return total - cap > 1e-6; + } + + /** + * 违反条件:{@code continuousDays >= consecutiveWorkDaysValue}(排班已连续天数已达上限)。 + */ + @Override + public boolean violatesConsecutiveWorkDaysLimit(String employeeId, StaffAssignmentContext context) { + FixedSchedulingRuleVo vo = fixedSchedulingRuleVo; + if (vo == null || !Boolean.TRUE.equals(vo.getConsecutiveWorkDaysEnabled())) { + return false; + } + Integer cap = vo.getConsecutiveWorkDaysValue(); + if (cap == null || cap <= 0) { + return false; + } + int cur = nz(situationOf(employeeId).getContinuousDays()); + return cur >= cap; + } + + /** + * 违反条件:{@code weekWorkDays >= weeklyWorkDaysValue}。 + */ + @Override + public boolean violatesWeeklyWorkDaysLimit(String employeeId, StaffAssignmentContext context) { + FixedSchedulingRuleVo vo = fixedSchedulingRuleVo; + if (vo == null || !Boolean.TRUE.equals(vo.getWeeklyWorkDaysEnabled())) { + return false; + } + Integer cap = vo.getWeeklyWorkDaysValue(); + if (cap == null || cap <= 0) { + return false; + } + int cur = nz(situationOf(employeeId).getWeekWorkDays()); + return cur >= cap; + } + + /** + * 违反条件:{@code weekWorkHours + proposed > weeklyWorkHoursValue}。 + */ + @Override + public boolean violatesWeeklyWorkHoursLimit(String employeeId, StaffAssignmentContext context) { + FixedSchedulingRuleVo vo = fixedSchedulingRuleVo; + if (vo == null || !Boolean.TRUE.equals(vo.getWeeklyWorkHoursEnabled())) { + return false; + } + Integer cap = vo.getWeeklyWorkHoursValue(); + if (cap == null || cap <= 0) { + return false; + } + BigDecimal wh = situationOf(employeeId).getWeekWorkHours(); + if (wh == null) { + wh = BigDecimal.ZERO; + } + BigDecimal proposed = BigDecimal.valueOf(context.getProposedAssignmentWorkHours()); + return wh.add(proposed).subtract(BigDecimal.valueOf(cap)).doubleValue() > 1e-6; + } + + /** + * 休息间隔未知(null)→ 不违反;已知且 < 规则小时数×60 → 违反。 + */ + @Override + public boolean violatesMinRestBetweenShifts(String employeeId, StaffAssignmentContext context) { + FixedSchedulingRuleVo vo = fixedSchedulingRuleVo; + if (vo == null || !Boolean.TRUE.equals(vo.getMinRestBetweenShiftsEnabled())) { + return false; + } + Integer hours = vo.getMinRestBetweenShiftsValue(); + if (hours == null || hours <= 0) { + return false; + } + Integer restMin = context.getMinutesRestSincePriorShiftEnd(); + if (restMin == null) { + return false; + } + return restMin < hours * 60L; + } + + private UserWorkSituationVo situationOf(String employeeId) { + if (employeeId == null) { + return UserWorkSituationVo.builder().build(); + } + UserWorkSituationVo v = situationByUserId.get(employeeId.trim()); + return v != null ? v : UserWorkSituationVo.builder().build(); + } + + private static int nz(Integer x) { + return x == null ? 0 : x; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/WorkstationPostStaffBuckets.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/WorkstationPostStaffBuckets.java new file mode 100644 index 0000000..aefa883 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/schedule/WorkstationPostStaffBuckets.java @@ -0,0 +1,117 @@ +package jnpf.attendance.schedule; + +import jnpf.model.attendance.vo.WorkstationGroupUserVo; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 工作站维度汇总的「岗位 → 可排人员」专岗/通岗桶,供第九~十步候选过滤与专岗优先排序。 + *

+ * 由 {@link #fromWorkstations(List)} 从 {@link WorkstationWithUsersVo} 列表构建; + * {@link ShiftPlanFinalStaffAssigner#assign} 入口第一步即创建本对象。 + *

+ * 岗位对齐:{@link WorkstationWithUsersVo#getPositionId()} 须与 {@link ShiftPlanPostNeed#getPostId()} 一致。 + *

+ * 人员分类:{@link WorkstationGroupUserVo#getIsExtra()} == false → 专岗; + * true → 通岗(可跨岗支援,在 {@link ShiftPlanFinalStaffAssigner#orderedCandidates} 中排在专岗之后)。 + */ +public final class WorkstationPostStaffBuckets { + + private final Map> specialistIdsByPost; + private final Map> generalIdsByPost; + + public WorkstationPostStaffBuckets( + Map> specialistIdsByPost, Map> generalIdsByPost) { + this.specialistIdsByPost = unmodifiableCopy(specialistIdsByPost); + this.generalIdsByPost = unmodifiableCopy(generalIdsByPost); + } + + private static Map> unmodifiableCopy(Map> in) { + Map> m = new LinkedHashMap<>(); + if (in != null) { + for (Map.Entry> e : in.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + m.put(e.getKey(), Collections.unmodifiableSet(new LinkedHashSet<>(e.getValue()))); + } + } + return Collections.unmodifiableMap(m); + } + + /** + * 遍历工作站列表,按岗位 ID 汇总专岗/通岗 userId 集合。 + *

+ * 同一 userId 出现在多工作站同岗位时,Set 自动去重。 + */ + public static WorkstationPostStaffBuckets fromWorkstations(List workstations) { + Map> spec = new LinkedHashMap<>(); + Map> gen = new LinkedHashMap<>(); + if (workstations == null) { + return new WorkstationPostStaffBuckets(spec, gen); + } + for (WorkstationWithUsersVo w : workstations) { + if (w == null || w.getPositionId() == null || w.getPositionId().trim().isEmpty()) { + continue; + } + String postId = w.getPositionId().trim(); + List users = w.getUserList(); + if (users == null) { + continue; + } + for (WorkstationGroupUserVo u : users) { + if (u == null || u.getUserId() == null || u.getUserId().trim().isEmpty()) { + continue; + } + String uid = u.getUserId().trim(); + if (Boolean.TRUE.equals(u.getIsExtra())) { + gen.computeIfAbsent(postId, k -> new LinkedHashSet<>()).add(uid); + } else { + spec.computeIfAbsent(postId, k -> new LinkedHashSet<>()).add(uid); + } + } + } + return new WorkstationPostStaffBuckets(spec, gen); + } + + /** 该岗位绑定的专岗员工 ID(无序 Set)。 */ + public Set getSpecialistIds(String postId) { + if (postId == null) { + return Collections.emptySet(); + } + Set s = specialistIdsByPost.get(postId); + return s == null ? Collections.emptySet() : s; + } + + /** 该岗位绑定的通岗员工 ID(无序 Set)。 */ + public Set getGeneralIds(String postId) { + if (postId == null) { + return Collections.emptySet(); + } + Set s = generalIdsByPost.get(postId); + return s == null ? Collections.emptySet() : s; + } + + /** + * 该岗位是否配置了至少一名工作站人员(专岗或通岗)。 + *

+ * true 时与上游候选人求交;交集为空则无法从该岗位工作站选人(不再退化为工作站全集)。 + */ + public boolean hasWorkstationBinding(String postId) { + return !getSpecialistIds(postId).isEmpty() || !getGeneralIds(postId).isEmpty(); + } + + public Map> getSpecialistIdsByPost() { + return specialistIdsByPost; + } + + public Map> getGeneralIdsByPost() { + return generalIdsByPost; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AppStatisticsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AppStatisticsService.java new file mode 100644 index 0000000..a2b9ef1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AppStatisticsService.java @@ -0,0 +1,91 @@ +package jnpf.attendance.service; + +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.model.ClockUser; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.ClockInExportVo; +import jnpf.model.common.DateRangeDto; + +import java.text.ParseException; +import java.util.List; + +/** + * 统计服务 + * + * @author shitou + * @date 2023/11/21 + */ +public interface AppStatisticsService { + /** + * 获取我的考勤-主页 + * + * @param req 筛选条件 + * @return AppStatisticsListVo + */ + AppStatisticsListVo getAppHomeData(AppStatisticsListDto req) throws QueryException; + + /** + * 获取我的考勤-出勤情况 + * + * @param req 筛选条件 + * @return List + */ + List getRecordData(AppStatisticsRecordDto req) throws Exception; + + /** + * 获取我的考勤-更多统计-默认 + * + * @param req 筛选条件 + * @return AppStatisticsMoreVo + */ + AppStatisticsMoreVo getMoreDefaultData(AppStatisticsMoreDto req) throws Exception; + + /** + * 获取我的考勤-更多统计-展开 + * + * @param req 筛选条件 + * @return AppStatisticsMoreInfoVo + */ + AppStatisticsMoreInfoVo getMoreExpandData(AppStatisticsMoreInfoDto req) throws Exception; + + /** + * 获取团队考勤-首页 + * + * @param req 筛选条件 + * @return AppStatisticsTeamListVo + */ + AppStatisticsTeamListVo getTeamHomeData(AppStatisticsTeamListDto req) throws QueryException; + + /** + * 获取团队考勤-tab列表数据 + * + * @param req 筛选条件 + * @return List + */ + List getTabListData(AppTeamStatisticsTabDto req); + + /** + * 获取团队考勤-团队统计 + * + * @param req 筛选条件 + * @return AppTeamStatisticsVo + */ + AppTeamStatisticsVo getStatisticsData(AppTeamStatisticsDto req) throws ParseException; + + /** + * 获取团队考勤-团队统计-详情列表 + * + * @param req 筛选条件 + * @return List + */ + List getStatisticsListData(AppTeamStatisticsListDto req) throws ParseException, QueryException; + + /** + * 考勤组指定日期考勤打卡及时段 + * @param usersByGroupVos 用户id集合 + * @param tenantId 租户id + * @param dateRangeDto 日期范围 + */ + List getDayClockInPageListExport(List usersByGroupVos, DateRangeDto dateRangeDto,String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttenceMachineService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttenceMachineService.java new file mode 100644 index 0000000..96cb5d0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttenceMachineService.java @@ -0,0 +1,110 @@ +package jnpf.attendance.service; + +import jnpf.attendance.annotation.Machine; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.model.attendance.vo.attendance.UserTenantVo; +import jnpf.permission.model.user.PartUserInfoVo; + +import java.util.List; +import java.util.Map; + +/** + * 考勤机服务 + * + * @author yanwenfu + * @create 2023-11-29 + */ +public interface AttenceMachineService extends SuperService { + + /** + * 发送用户到设备 + * @param userId 用户id + * @param sn 设备号 + * @param code 厂商编码 + */ + void sendUserToMachine(String code, String userId, String sn); + + /** + * 发送用户到设备 + * @param user 用户信息 + * @param sn 设备号 + * @param code 厂商编码 + */ + void sendUserToMachine(String code, PartUserInfoVo user, String sn); + + /** + * 更新考勤机用户信息 + * @param sn 设备号 + * @param userId 用户id + * @param userName 用户名称 + */ + void updateUserInfoByWebsocket(String sn, String userId, String userName); + + /** + * 删除人员 + * @param sn 设备号 + * @param userId 用户id + */ + void deleteUser(String sn, String userId); + + /** + * 批量删除人员 + * @param sn 设备号 + * @param userIds 用户ids + */ + void deleteUserList(String code, String sn, List userIds); + + /** + * 更新用户信息 + * @param params 参数 + * @return java.util.Map + */ + Map updateUserInfoPhoto(Map params); + + /** + * 修改图片 + * 此方法用于执行图片的修改操作它可能涉及从一个源获取图片数据, + * 应用一些转换或更新,并将修改后的图片保存回原始位置或新位置 + * 具体的实现细节在这个方法内部,包括如何获取、修改和保存图片, + * 依赖于进一步的代码实现 + * + * @return String 返回一个字符串,可能包含修改后的图片的路径、URL或状态信息 + */ + String changeImg(); + + /** + * 考勤机打卡 + * @param sn 设备号 + * @param userId 用户id + * @param tenantId 租户id + * java.lang.String + */ + String machineClockIn(String sn, String userId, String tenantId, String clockInId) throws Exception; + + /** + * 打卡/更新打卡 + * @param sn 设备号 + * @param userId 用户id + * @param tenantId 租户id + * @return boolean 是否成功 + */ + String clockIn(String sn, String userId, String tenantId); + + /** + * 更新用户照片 + * @param userId 用户id + * @param photoUrl 照片 + */ + void updateKeMiPhoto(String userId, String photoUrl, String tenantId); + + /** + * 科密考勤机打卡 + * @param userTenant 用户租户信息 + * @param devId 设备id + * @param tenantId 租户id + */ + void KeMiClockIn(UserTenantVo userTenant, String devId, String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceAIService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceAIService.java new file mode 100644 index 0000000..5c26a13 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceAIService.java @@ -0,0 +1,28 @@ +package jnpf.attendance.service; + +import jnpf.model.attendance.dto.AttendanceReqDto; +import jnpf.model.attendance.vo.attendance.ClockDataReqVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; + +/** + * 考勤ai服务 + * + * @author yanwenfu + * @create 2026-05-07 + */ +public interface AttendanceAIService { + + /** + * 根据日期查询考勤打卡记录 + * @param dto 查询条件 + * @return jnpf.model.attendance.vo.attendance.ClockDataReqVo + */ + ClockDataReqVo getClockRecordByDate(AttendanceReqDto dto); + + /** + * 查询考勤组加班规则 + * @param groupId 考勤组id + * @return jnpf.model.attendance.vo.attendance.OvertimeRuleVo + */ + OvertimeRuleVo getOvertimeRule(String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApprovalSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApprovalSettingService.java new file mode 100644 index 0000000..62c8864 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApprovalSettingService.java @@ -0,0 +1,18 @@ +package jnpf.attendance.service; + +import jnpf.model.attendance.dto.AttendanceApprovalSettingDto; + +public interface AttendanceApprovalSettingService { + + /** + * 修改考勤组审批配置 + * @param attendanceApprovalSettingDto 修改参数 + */ + void update(AttendanceApprovalSettingDto attendanceApprovalSettingDto); + + /** + * 添加考勤组审批设置模版 + * @param groupId 考勤组id + */ + void addTemplateSetting(String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApproveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApproveService.java new file mode 100644 index 0000000..d5479b0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceApproveService.java @@ -0,0 +1,337 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.entity.attendance.AttendanceApprovalAdminVo; +import jnpf.exception.ApproveException; +import jnpf.exception.HandleException; +import jnpf.exception.WorkFlowException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.AttendanceToThousandsFacesVo; +import jnpf.model.attendance.vo.attendance.LeaveConsumptionDetailVo; +import jnpf.model.doclibrary.vo.UseDetailVo; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveOaDto; +import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.util.Date; +import java.util.List; + +public interface AttendanceApproveService { + + + + + /** + * 劵使用记录 + * @param id 劵id + * @param balanceQueryDto 参数 + * @return com.github.pagehelper.PageInfo + * @author hlp + */ + PageInfo getUseDetail(String id, BalanceQueryDto balanceQueryDto); + + + + /** + * 定时失效劵 + * @param tenantId 租户id + * @return java.lang.Boolean + * @author hlp + */ + Boolean invalidationCoupons(String tenantId); + + /** + * 加班--开始日期选择后触发接口返回当天及后一天的排班信息 + * @param balanceQueryDto 参数 + * @return java.util.List + * @author hlp + */ + List getClasses(BalanceQueryDto balanceQueryDto); + + /** + * 获取加班期间的班次信息 + * 此方法旨在根据查询条件获取员工在加班时间段内的班次信息,以便进行相应的考勤和工时计算 + * 它通常用于处理加班申请、工时统计等场景,确保加班时间得到正确记录和处理 + * + * @param balanceQueryDto 查询条件对象,包含需要查询的员工信息、时间范围等条件 + * @return LeaveShiftVo 返回加班期间的班次信息对象,包括班次时间、员工ID等关键信息 + * @throws HandleException 当查询过程中发生错误时抛出此异常,调用者需要处理该异常 + */ + LeaveShiftVo getWorkOverTimeShifts(BalanceQueryDto balanceQueryDto) throws HandleException; + + + /** + * 获取请假申请时长及设计班次(请假时选择了开始和结束时间后请求该接口) + * @param leaveQueryDto 参数 + * @return jnpf.base.ActionResult + * @author hlp + */ + + LeaveShiftVo getLeaveDuration(LeaveQueryDto leaveQueryDto) throws HandleException; + + /** + * 请假审批通过后的触发接口 + * @param id 审批的唯一id + * @author hlp + */ + void leaveApprove(String id, Integer status, String tenantId, String userId, String userName) throws ApproveException; + + /** + * 审核第二个任务项 + * 该方法用于执行第二个任务项的检查操作,确保任务符合预期标准 + * + * @param taskId 任务项的唯一标识符,用于指定需要检查的任务 + * @throws HandleException 如果检查过程中遇到错误或异常情况,则抛出此异常 + */ + void checkSeconded(String taskId) throws HandleException; + + + + /** + * 检查工作任务项 + * 该方法用于对工作任务项进行检查,以确保其符合既定的工作标准或流程 + * + * @param taskId 工作任务项的唯一标识符,用于指定需要检查的任务 + * @throws HandleException 如果检查过程中发现异常或错误,则抛出此异常 + */ + void checkWork(String taskId) throws HandleException; + + /** + * 借调审批 + * 该方法用于获取借调审批的管理员信息 + * + * @param groupId 组ID,标识一个特定的组 + * @param type 审批类型,用于区分不同的审批情况 + * @return 返回一个包含审批管理员信息的对象 + */ + AttendanceApprovalAdminVo getApproveUser(String groupId, Integer type); + + /** + * 请假审批 + * 该方法用于获取请假审批的管理员信息 + * + * @param body 包含审批所需信息的字符串 + * @return 返回一个包含审批管理员信息的对象 + * @throws HandleException 当处理过程中遇到错误时抛出 + * @throws WorkFlowException 当工作流执行过程中遇到错误时抛出 + */ + AttendanceApprovalAdminVo getLeaveApproveUser(String body) throws HandleException, WorkFlowException; + + /** + * 加班审批 + * 该方法用于获取加班审批的管理员信息 + * + * @param body 包含审批所需信息的字符串 + * @return 返回一个包含审批管理员信息的对象 + * @throws Exception 当审批过程中遇到任何错误时抛出 + */ + AttendanceApprovalAdminVo getOvertimeApproveUser(String body) throws Exception; + + /** + * 补卡审批 + * 该方法用于获取补卡审批的管理员信息 + * + * @param body 包含审批所需信息的字符串 + * @return 返回一个包含审批管理员信息的对象 + * @throws Exception 当审批过程中遇到任何错误时抛出 + */ + AttendanceApprovalAdminVo getReplacementCardApproveUser(String body) throws Exception; + + /** + * 考勤结果变更 + * 该方法用于处理考勤结果的变更审批 + * + * @param body 包含变更审批所需信息的字符串 + * @return 返回一个包含审批管理员信息的对象 + * @throws Exception 当审批过程中遇到任何错误时抛出 + */ + AttendanceApprovalAdminVo getAttendanceAlter(String body) throws Exception; + + /** + * 外勤审批 + * 该方法用于获取外勤审批的管理员信息 + * + * @return 返回一个包含审批管理员信息的对象 + */ + AttendanceApprovalAdminVo getFieldApproval(); + + /** + * 借调审批 + * 该方法用于获取借调审批的管理员列表 + * + * @param body 包含审批所需信息的字符串 + * @return 返回一个包含审批管理员ID的列表 + * @throws Exception 当审批过程中遇到任何错误时抛出 + */ + List secondedApproval(String body) throws Exception; + + + /** + * 2.0已重构 选择了请假类型和请假时间后请求接口 + * @param leaveQueryDto 参数 + * @return jnpf.model.attendance.vo.UserBalanceVo + * @author hlp + */ + LeaveConsumptionDetailVo getResidueBalance(LeaveQueryDto leaveQueryDto) throws ParseException, HandleException, ApproveException; + + /** + * 加班审批通过后请求接口 + * @param id 审批的唯一id + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @author hlp + */ + void workApprove(String id, Integer status, String tenantId, String userId, String userName) throws ApproveException; + + /** + * 借调审批通过后的触发的接口 + * @param id 审批的唯一id + * @param departureTime 离岗实际 2023-11-10 10:10 + * @param backTime 回岗时间 2023-11-10 10:10 + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 + * @param tenantId 租户id + * @author hlp + */ + void selfApprove(String id, String departureTime, String backTime, Integer status, String userId, String userName, String tenantId) throws ApproveException, HandleException; + + + /** + * 审批校验接口 + * @param taskId 任务id + * @param type 类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批 + * @return jnpf.base.ActionResult + * @author hlp + */ + ActionResult getApprovalAdmin(String taskId, String type); + + /** + * 提交时审批校验接口 + * @param body 参数 + * @param type 类型 + */ + ActionResult submitValidation(String body, String type); + + /** + * 提交检查工作的请求给OA系统 + * 此方法用于将加班审批的信息提交到OA系统进行处理 + * + * @param workOverTime 加班审批的详细信息,包含申请人、加班时间、理由等 + * @throws HandleException 当提交过程中发生错误时抛出此异常 + */ + void submitCheckWorkForOa(AttendanceWorkOvertimeApproveDto workOverTime) throws HandleException; + + /** + * 获取考勤组管理员信息 + * @param type 类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 考勤组管理员信息 + */ + AttendanceApprovalAdminVo getGroupAdminInfo(Integer type, Date startTime, Date endTime, String clockInResultId, String selfGroupId) throws HandleException; + + /** + * 获取考勤组用户余额 + * @param groupId 考勤组 + * @param month 年月(2024-5-13) + * @return jnpf.base.ActionResult + * @author hlp + */ + List getBalanceDetailS(String groupId, String month); + + /** + * 修改外出审批状态 + * @param applyId 主键Id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + */ + void goOutApprove(String applyId, Integer status, String tenantId, String userId, String userName) throws ApproveException; + + /** + * 修改出差审批状态 + * @param applyId 主键Id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 + */ + void businessTripApprove(String applyId, Integer status, String tenantId, String userId, String userName) throws ApproveException; + + /** + * 获取指定优惠余额 + * @param userIds 用户id + * @return List + */ + List getUsersBalance(List userIds); + + + + /** + * 外出审批 + */ + AttendanceApprovalAdminVo getGoOutApproval(); + + /** + * 出差审批 + */ + AttendanceApprovalAdminVo getBusinessTripApproval(); + + /** + * 每月定时计算用户存休 + * @param tenantId 租户id + * @return java.lang.Boolean + */ + Boolean storageRest(String tenantId); + + /** + * 发消息通知下一节点审核人 发送Im消息 + */ + void sendIm(WorkflowImQueryDto workflowImQueryDto); + + + + + + /** + * 保存出差审批及校验 + */ + ActionResult createBusinessTrip(AttendanceBusinessTripApproveOaDto approveOaDto); + + /** + * 外出审批校验及保存 + */ + ActionResult createGoOutForOa(GoOutApproveForOaDto dto); + + /** + * 通过传入的开始结束时间及单位获取时长 + */ + ActionResult getDurationForOa(DurationForOaDto dto); + + /** + * 请假审批校验及保存 + */ + ActionResult createLeaveForOa(LeaveApproveForOaDto dto); + + /** + * 获取用户所选时间段所在考勤组 + */ + ActionResult getUserGroupByTime(GroupOaDto groupOaDto); + + /** + * 获取当前分组信息 + * 此方法用于获取当前系统或用户所在的分组信息,用于界面展示或其他业务逻辑处理 + * + * @return ActionResult 返回一个包含分组信息的操作结果对象 + */ + ActionResult getNowGroup(); + + /** + * 获取出勤到千人面信息 + * 此方法用于获取出勤记录中与千人面相关的数据,可能包括出勤时间、地点、人员等信息 + * + * @return AttendanceToThousandsFacesVo 返回一个出勤到千人面信息对象,包含详细的出勤和千人面数据 + */ + AttendanceToThousandsFacesVo attendanceToThousandsFaces(); + + /** + * v2.0.1 补丁版本请假审批未排班或休变为排班时触发扣劵的补偿逻辑 + */ + void leaveCompensation(String applyId, String userId,String day, BigDecimal balance) throws ApproveException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBaseSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBaseSettingService.java new file mode 100644 index 0000000..5a36f0b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBaseSettingService.java @@ -0,0 +1,125 @@ +package jnpf.attendance.service; + +import cn.hutool.json.JSONArray; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.AttendanceBaseSettingDto; +import jnpf.model.attendance.vo.AttendanceBaseSettingVo; +import jnpf.model.attendance.vo.attendance.QuickCheckInVo; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 考勤基础设置表 服务类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceBaseSettingService extends SuperService { + + /** + * 保存考勤基础设置信息。 + * + * @param attendanceBaseSettingDto 考勤基础设置数据传输对象 + * @throws HandleException 处理异常 + */ + void save(AttendanceBaseSettingDto attendanceBaseSettingDto) throws HandleException; + + void hisBaseSetting(Map group2orgMap, Map org2groupMap); + + /** + * 根据考勤组ID获取单个考勤基础设置。 + * + * @param groupId 考勤组ID + * @return 考勤基础设置视图对象 + */ + AttendanceBaseSettingVo getOne(String groupId); + + /** + * 更改考勤基础设置的启用状态。 + * + * @param groupId 考勤组ID + * @param enable 启用状态(0禁用,1启用) + * @param attendanceBaseSetting 考勤基础设置视图对象 + */ + void changeStatus(String groupId, Integer enable, AttendanceBaseSettingVo attendanceBaseSetting); + + /** + * 获取启用的考勤基础设置信息。 + * + * @param groupIds 考勤组ID列表 + * @return 启用的考勤基础设置映射 + */ + Map getEnableBaseSetting(List groupIds); + + /** + * 获取所有启用的考勤基础设置信息。 + * + * @param groupIds 考勤组ID列表 + * @return 所有启用的考勤基础设置映射 + */ + Map getEnableBaseSettingAll(List groupIds); + + /** + * 获取指定考勤组和考勤组视图列表的启用的考勤基础设置信息。 + * + * @param groupIds 考勤组ID列表 + * @param attendanceGroupVos 考勤组视图对象列表 + * @return 启用的考勤基础设置映射 + */ + Map getEnableBaseSetting(List groupIds, List attendanceGroupVos); + + /** + * 初始化考勤基础设置。 + * + * @param groupId 考勤组ID + */ + void initBaseSetting(String groupId); + + /** + * 查找考勤基础设置信息。 + * + * @param groupId 考勤组ID + * @return 考勤基础设置对象 + */ + AttendanceBaseSetting findBaseSetting(String groupId); + + /** + * 查询用户外勤是否必须拍照 + * @param userInfo 用户信息 + * @return cn.hutool.json.JSONArray + */ + JSONArray getTakePhotoSetting(UserInfo userInfo) throws QueryException; + + /** + * 获取用户当前时间所处考勤组的内勤打卡基础设置 + */ + Integer getNowGroupAttendancePhoto(); + + /** + * 获取用户当前时间所处考勤组是否能外勤打卡 + * @param setting 考勤组配置 + */ + boolean getFieldClockStatusSelf(AttendanceBaseSetting setting); + + + /** + * 获取用户当前时间所处考勤组是否能快速打卡 + * 和用户当前所处考勤组的基础设置中的是否需要拍照,是否需要人脸识别以及App个人设置中的上下班快速打开相关 + */ + QuickCheckInVo quickCheckIn(); + + /** + * 获取用户当前时间所处考勤组是否能外勤打卡 + * @param groupId 考勤组 + */ + boolean getNeedFace(String groupId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookConfigService.java new file mode 100644 index 0000000..0694f2e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookConfigService.java @@ -0,0 +1,119 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceBookConfigEntity; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceBookConfigAddUserDto; +import jnpf.model.attendance.dto.AttendanceBookConfigDto; +import jnpf.model.attendance.dto.AttendanceBookConfigQueryDto; +import jnpf.model.attendance.dto.GetValidUsersDto; +import jnpf.model.attendance.vo.AttendanceBookConfigVo; +import jnpf.model.attendance.vo.AttendanceResultOptionGroupVo; +import jnpf.model.attendance.vo.AttendanceResultOptionVo; +import jnpf.model.attendance.vo.BookConfigCreatorVo; +import jnpf.model.attendance.vo.BookPersonnelVo; +import jnpf.permission.model.user.PartUserInfoVo; + +import java.util.List; + +/** + * 考勤本配置服务接口 + * + * @author Generated + * @create 2026-04-15 + */ +public interface AttendanceBookConfigService extends SuperService { + + /** + * 新增或更新考勤本配置 + *

根据DTO中的id判断:id为空则新增,id不为空则更新

+ * + * @param configDto 考勤本配置DTO + * @throws HandleException 业务异常 + */ + void saveOrUpdateAttendanceBookConfig(AttendanceBookConfigDto configDto) throws HandleException; + + /** + * 删除考勤本配置 + * + * @param id 考勤本ID + */ + void deleteAttendanceBookConfig(String id); + + /** + * 更新启用状态 + * + * @param id 考勤本ID + * @throws HandleException 业务异常 + */ + void updateEnableStatus(String id) throws HandleException; + + /** + * 获取考勤本配置详情 + * + * @param id 考勤本ID + * @return 考勤本配置VO + */ + AttendanceBookConfigVo getDetail(String id); + + /** + * 分页查询考勤本配置列表 + * + * @param queryDto 查询条件 + * @return 分页结果 + */ + PageInfo getPageList(AttendanceBookConfigQueryDto queryDto); + + /** + * 增量添加使用范围人员 + *

向指定考勤本配置的使用范围中增量添加用户列表

+ * + * @param addUserDto 添加用户DTO + * @throws HandleException 业务异常 + */ + void addScopeUsers(AttendanceBookConfigAddUserDto addUserDto) throws HandleException; + + /** + * 判断当前登录人是否为考勤本负责人 + * + * @param bookConfigId 考勤本配置ID + * @return true-是负责人,false-不是负责人 + * @throws HandleException 业务异常 + */ + boolean isCurrentUserManager(String bookConfigId) throws HandleException; + + /** + * 获取指定组织集合及用户集合在当前时间下的所有涉及用户 + *

查询这些组织和用户在当前时间的有效用户集合,返回完整的用户信息列表

+ * + * @param dto 查询参数(组织ID列表、用户ID列表) + * @return 有效用户信息列表 + */ + List getValidUsers(GetValidUsersDto dto); + + /** + * app-获取考勤结果下拉列表(分组返回) + *

根据考勤本配置返回可用的考勤状态选项,包括基础考勤状态、外勤结果和该考勤本配置的请假类型

+ * + * @param bookConfigId 考勤本配置ID + * @return 考勤结果选项分组(考勤结果、外勤结果、假期类型) + * @throws HandleException 业务异常 + */ + AttendanceResultOptionGroupVo getAttendanceResultOptions(String bookConfigId) throws HandleException; + + /** + * 需求2:获取考勤本配置创建人列表 + * + * @return 创建人列表 + */ + List getCreatorList(); + + /** + * 需求3:获取考勤本配置人员列表 + * + * @param bookConfigId 考勤本配置ID + * @return 人员列表 + */ + List getBookPersonnelList(String bookConfigId,String month); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookOperationLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookOperationLogService.java new file mode 100644 index 0000000..d989c8b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookOperationLogService.java @@ -0,0 +1,31 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.attendance.AttendanceBookOperationLogEntity; +import jnpf.model.attendance.dto.OperationLogQueryDto; +import jnpf.model.attendance.vo.OperationLogPageVo; + +/** + * 考勤本操作日志Service + * + * @author Generated + * @create 2026-04-15 + */ +public interface AttendanceBookOperationLogService extends IService { + + /** + * 记录操作日志 + * + * @param entity 操作日志实体 + */ + void saveLog(AttendanceBookOperationLogEntity entity); + + /** + * 分页查询操作日志 + * + * @param queryDto 查询参数 + * @return 分页结果 + */ + Page queryPage(OperationLogQueryDto queryDto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookRecordService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookRecordService.java new file mode 100644 index 0000000..266a11f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceBookRecordService.java @@ -0,0 +1,106 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.entity.attendance.AttendanceBookRecordEntity; +import jnpf.model.attendance.dto.AttendanceBookRecordDto; +import jnpf.model.attendance.dto.BatchAttendanceBookRecordDto; +import jnpf.model.attendance.dto.BookRecordDayListDto; +import jnpf.model.attendance.dto.BookRecordMonthListDto; +import jnpf.model.attendance.dto.BookRecordMonthStatisticsDto; +import jnpf.model.attendance.dto.LeaveRemarkSummaryDto; +import jnpf.model.attendance.dto.SchedulesImportDto; +import jnpf.model.attendance.vo.BatchOperationResultVo; +import jnpf.model.attendance.vo.BookRecordDayListVo; +import jnpf.model.attendance.vo.BookRecordMonthListVo; +import jnpf.model.attendance.vo.BookRecordMonthStatisticsVo; +import jnpf.model.attendance.vo.LeaveRemarkSummaryVo; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * 考勤本记录表Service + * + * @author Generated + * @create 2026-04-15 + */ +public interface AttendanceBookRecordService extends IService { + + /** + * 新增或修改考勤记录(Upsert) + * 根据考勤本ID、员工ID、考勤日期、时段类型唯一确定一条记录 + * 如果记录存在则更新,不存在则新增 + * + * @param dto 考勤记录DTO + * @return 记录ID + */ + String saveOrUpdateRecord(AttendanceBookRecordDto dto); + + /** + * 获取考勤本月统计数据 + * + * @param req 统计请求参数 + * @return 本月统计结果(按员工维度,支持分页) + */ + PageInfo getMonthStatistics(BookRecordMonthStatisticsDto req); + + /** + * 考勤本导入 + * + * @param importDto 导入参数 + * @throws IOException IO异常 + */ + void bookRecordImport(SchedulesImportDto importDto) throws IOException; + + /** + * 考勤本导出 + * + * @param bookId 考勤本ID + * @param month 月份(格式:yyyy-MM) + */ + void bookRecordExport(String bookId, String month); + + /** + * 导出考勤本模板(只有表头,无数据) + * + * @param bookId 考勤本ID + * @param month 月份(格式:yyyy-MM) + */ + void exportTemplate(String bookId, String month, HttpServletResponse response); + + /** + * 获取日考勤本列表 + *

根据考勤本的使用范围获取人员列表,展示指定日期的考勤记录

+ * + * @param req 查询参数 + * @return 日考勤本列表(按员工分组) + */ + List getDayList(BookRecordDayListDto req); + + /** + * 获取月考勤本列表 + *

根据考勤本的使用范围获取人员列表,展示该月所有日期的考勤记录

+ * + * @param req 查询参数 + * @return 月考勤本列表(按员工分组) + */ + List getMonthList(BookRecordMonthListDto req); + + /** + * 需求10:批量新增或修改考勤记录 + * + * @param dto 批量考勤记录DTO + * @return 批量操作结果 + */ + BatchOperationResultVo batchSaveOrUpdateRecord(BatchAttendanceBookRecordDto dto); + + /** + * 需求6:获取假勤汇总信息(包含请假备注和考勤统计) + * + * @param dto 统计请求参数 + * @return 假勤汇总信息 + */ + LeaveRemarkSummaryVo getLeaveAttendanceSummary(LeaveRemarkSummaryDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceChangeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceChangeService.java new file mode 100644 index 0000000..09733b5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceChangeService.java @@ -0,0 +1,58 @@ +package jnpf.attendance.service; + +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceResultRollback; +import jnpf.model.attendance.dto.NoApprovalDto; +import jnpf.model.attendance.vo.ChangeInfoVo; + +import java.util.List; + +/** + * 考勤变更服务 + * + * @author yanwenfu + * @create 2024-11-08 + */ +public interface AttendanceChangeService { + + /** + * 被变更记录的信息 + * @param clockInResultId 结果id + * @return jnpf.model.attendance.vo.ChangeInfoVo + */ + ChangeInfoVo getChangeInfo(String clockInResultId) throws Exception; + /** + * 根据考勤组id及用户id查询过去一年内的变更涉及打卡记录 + * @param groupId 考勤组id + * @param userId 用户id + * @return java.util.List + */ + List getChangeInfoList(String groupId, String userId); + + /** + * 出勤变更(无需审批) + * @param noApprovalDto 出勤变更参数 + */ + void attendanceChangeNoApproval(NoApprovalDto noApprovalDto) throws Exception; + + /** + * 出勤变更(需审批) + * @param taskId 审批id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + * @param user 审批人 + */ + void attendanceChangeApproval(String taskId, String passed, UserInfo user) throws Exception; + + /** + * 出勤变更撤回 + * @param clockInResultId 打卡结果id + */ + void rollbackChange(String clockInResultId) throws Exception; + + /** + * 查询能否撤回变更 + * @param clockInResultId 打卡结果id + * @return java.lang.Integer + */ + Integer getRollbackStatus(String clockInResultId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInPicService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInPicService.java new file mode 100644 index 0000000..f9fbd55 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInPicService.java @@ -0,0 +1,31 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceClockInPic; +import jnpf.model.attendance.vo.ClockInPicVo; + +import java.util.List; +import java.util.Map; + +/** + * 外勤打卡图片服务 + * + * @author yanwenfu + * @create 2023-11-21 + */ +public interface AttendanceClockInPicService extends SuperService { + /** + * 查询打卡记录的拍照图片集合 + * + * @param ids 打卡记录ID + * @return 返回外勤打卡图片集合(键值对) + */ + Map> getClockInPicByIds(List ids, List appIds); + + /** + * 查询外勤打卡图片列表 + * @param clockInId 打卡ID + * @return 返回外勤打卡图片集合 + */ + List getClockInPicList(String clockInId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInResultService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInResultService.java new file mode 100644 index 0000000..1e54f3d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInResultService.java @@ -0,0 +1,29 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.model.attendance.vo.UserDayVo; + +import java.util.List; +import java.util.Map; + +/** + * 打卡结果服务 + * + * @author yanwenfu + * @create 2023-11-29 + */ +public interface AttendanceClockInResultService extends SuperService { + + /** + * 打卡重新匹配数据库处理 + * @param resultList 生成的打卡结果 + * @param userDayList 用户日期vo + * @param updateUserId 更新人 + */ + void dbMatchDeal(String requestId, List resultList, List userDayList, String updateUserId); + + void updateOldResultChangeList(Map map); + + void updateOldResultRepairList(Map map); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInService.java new file mode 100644 index 0000000..4194907 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceClockInService.java @@ -0,0 +1,306 @@ +package jnpf.attendance.service; + +import jnpf.base.UserInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.FtbAttendanceClockIn; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.ClockInDto; +import jnpf.model.attendance.model.DayClockRange; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.ClockInExportVo; +import jnpf.model.common.DateRangeDto; +import org.apache.commons.lang3.tuple.MutablePair; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * 打卡服务 + * + * @author yanwenfu + * @create 2023-11-21 + */ +public interface AttendanceClockInService extends SuperService { + + /** + * 打卡 - 主页 + * @param today 日期 + * @param userInfo 用户信息 + * @param isMainInfo 是否打卡主页(1: 是, 0: 否) + * @return java.util.List + */ + List getClockInMainInfo(Date today, UserInfo userInfo, Integer isMainInfo) throws Exception; + + /** + * 打卡 + * + * @param clockInDto 打卡信息 + * @return org.apache.commons.lang3.tuple.MutablePair
left:打卡结果状态, right:打卡结果id + */ + MutablePair clockIn(ClockInDto clockInDto) throws Exception; + + /** + * 更新打卡记录 + * @param clockInDto 更新内容 + * @param clockInId 打卡记录id + * @return org.apache.commons.lang3.tuple.MutablePair
left:打卡结果状态, right:打卡结果id + */ + MutablePair updateClockIn(String clockInId, ClockInDto clockInDto) throws Exception; + + /** + * 判断本次是否外出打卡 + * @param rule 考勤规则 + * @param clockInType 上/下班打卡 + * @return boolean + */ + boolean getOutsideCheck(AttendanceRuleVo rule, Integer clockInType); + + /** + * 变更出勤规则[批量] + * @param userDayList 用户日期列表 + * @param user 操作人 + */ + void changeAttendanceRuleBatch(List userDayList, UserInfo user); + + /** + * 查询出勤规则 + * @param ruleId 出勤规则id + * @return jnpf.model.attendance.vo.AttendanceRuleVo + */ + AttendanceRuleVo getAttendanceRule(String ruleId) throws HandleException; + + /** + * 查询出勤规则[批量] + * @param ruleIds 出勤规则ids + * @return java.util.Map + */ + Map getAttendanceRuleBatch(List ruleIds); + + /** + * 查询考勤组信息 + * @param today 日期 + * @param groupId 考勤组id + * @param selfGroupInt 是否自己的考勤组 + * @param userInfo 当前登陆用户 + * @return jnpf.model.attendance.vo.GroupInfoVo + */ + GroupInfoVo getGroupInfo(Date today, String groupId, int selfGroupInt, UserInfo userInfo) throws QueryException; + + /** + * 查询考勤组信息 + * @param today 日期 + * @param groupId 考勤组id + * @param selfGroupInt 是否自己的考勤组 + * @param userInfo 当前登陆用户 + * @param isMainInfo 是否主页进入 + * @return jnpf.model.attendance.vo.GroupInfoVo + */ + GroupInfoVo getGroupInfo(Date today, String groupId, int selfGroupInt, UserInfo userInfo, Integer isMainInfo) throws QueryException; + + /** + * 查询考勤组出勤规则 + * @param groupId 考勤组id + * @param userId 用户id + * @return jnpf.model.attendance.vo.GroupRuleVo + */ + GroupRuleVo getGroupRule(String groupId, String userId); + + /** + * 能否补卡 + * @param day 日期 + * @param clockInResult 出勤结果 + * @param approvalStatus 审批状态 + * @param groupRule 考勤组规则 + * @return org.apache.commons.lang3.tuple.MutablePair + */ + MutablePair couldRepairRecord(Date day, AttendanceClockInResult clockInResult, Integer approvalStatus, GroupRuleVo groupRule) throws Exception; + + /** + * 根据日期查询考勤组信息 + * @param today 日期 + * @param userInfo 当前登陆用户 + * @return jnpf.model.attendance.vo.GroupInfoVo + */ + GroupInfoVo getGroupInfoByDate(Date today, UserInfo userInfo) throws QueryException; + + /** + * 外勤打卡 + * @param approvalCode 审批code + * @param tenantId 租户id + * @param clockInId 打卡id + */ + void outsideClockIn(String approvalCode, String tenantId, String clockInId) throws Exception; + + /** + * 外勤打卡审批(通过/不通过/撤回) + * @param applyId 审批id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + * @param userInfo 当前登录人 + */ + void approvalOutsideClockIn(String applyId, String passed, UserInfo userInfo) throws Exception; + + /** + * 异常打卡审批(通过/不通过/撤回) + * @param applyId 审批id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + * @param userInfo 当前登录人 + */ + void approvalUnusualPhoneClockIn(String applyId, String passed, UserInfo userInfo) throws Exception; + + /** + * 补卡 + * @param applyId 审批id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + * @param tenantId 租户ID + */ + void repairClockIn(String applyId, String passed, String approveUserId, String tenantId) throws Exception; + + /** + * 执行缺卡逻辑 + * @param tenantId 租户id + * @return java.lang.Boolean + */ + Boolean generateFtbAbsenceRecord(String tenantId); + + /** + * 生成上班前打卡提醒 + * @param tenantId 租户id + * @return java.lang.Boolean + */ + Boolean generateBeforeWorkRemind(String tenantId); + + /** + * 异步处理缺卡 + * @param tenantId 租户id + * @param conditionList 需要执行的任务 + * @param now 当前时间 + */ + CompletableFuture asyncDeal(String tenantId, String hashKey, List conditionList, String now); + + /** + * 可选择的补卡列表 + * @param userId 用户id + * @return java.util.List + */ + List getRepairList(String userId) throws Exception; + + /** + * 查询每日出勤及打卡记录 + * @param userId 用户id + * @param queryDate 查询日期 + * @param currentGroupId 当前考勤组 + * @return java.util.List + */ + List getDailyClockInRecord(String userId, String queryDate, String currentGroupId); + + /** + * 出勤变更 + * @param applyId 申请id + * @param passed 是否通过(0: 否, 1: 是, 2: 撤回) + * @param approveUserId 审批人id + * @param tenantId 租户ID + */ + void attendanceChange(String applyId, String passed, String approveUserId, String tenantId) throws HandleException; + + /** + * 出勤变更(不审批) + * @param clockInResultId 打卡结果id + * @param changeType 变更类型(1: 变更为旷工, 2: 撤销旷工, 3: 变更为正常, 4: 补卡) + */ + void attendanceChangeNoApproval(String clockInResultId, Integer changeType) throws HandleException; + + /** + * 查询每日出勤及打卡记录 - v2 + * @param userId 用户id + * @param queryDate 查询日期 + * @param currentGroupId 当前考勤组id + * @param queryOldData 查看原始数据(1: 是, 0: 否) + * @return jnpf.model.attendance.vo.DailyInfoVo + */ + DailyInfoVo getDailyClockInRecordV2(String userId, String queryDate, String currentGroupId, Integer queryOldData) throws QueryException; + + /** + * 获取班次时间 + * @param rule 出勤规则 + * @param workStatus 上/下班 + * @return java.lang.String + */ + String getShiftTimeStr(FtbAttendanceDailyRule rule, int workStatus); + + /** + * 生成补卡次数 + * @return java.lang.Boolean + */ + Boolean generateRepairNum(); + + /** + * 生成用户补卡次数记录 + * @param groupId 考勤组id + * @param userId 用户id + * @param generateType 生成类型(1: 新增组成员, 2: 借调到新组) + * @return java.lang.Boolean + */ + Boolean generateRepairNumForUser(String groupId, String userId, Integer generateType); + + /** + * 判断考勤组是否可以补卡 + * @param groupId 考勤组id + * @return jnpf.model.attendance.vo.RepairRuleVo + */ + RepairRuleVo getClockInRepairCheck(String groupId); + + /** + * 生成全面维修编号 + * 本方法旨在生成一个全面的维修编号,该编号用于唯一标识一次维修事件或记录 + * 它可能涉及到复杂的逻辑,如数据库查询、序列生成或其他策略,以确保编号的唯一性和连续性 + * + * @return Boolean 表示维修编号是否成功生成true表示成功,false表示失败 + */ + Boolean generateRepairNumAll(); + + /** + * 查询每日出勤及打卡记录 + * @param userId 用户id + * @param clockRecord 查询日期范围 + */ + List getDailyClockInRecord(String userId, DayClockRange clockRecord); + + /** + * 判定连续动作(排班/旷工) + * @param tenantId 租户id + * @return java.lang.Boolean + */ + Boolean continuousCheck(String tenantId); + + /** + * 获取当前考勤组排班 + * @param today 日期 + * @param userInfo 日期 + */ + List getCurrentDailyRuleListOfWithdraw(Date today, UserInfo userInfo); + + /** + * 获取当前时间段内考勤组排班 + */ + List getCurrentDailyRuleListOfDay(DateRangeDto dateRangeDto, List usersByGroupVos); + + /** + * 生成打卡旷工任务 + * @param flag 是否删除redisKey重新生成(1: 是, 0: 否) + * @param tenantId 租户id + * @return java.lang.Boolean + */ + Boolean generateAbsenceTask(Integer flag, String tenantId); + + /** + * 查询考勤组审批列表 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getGroupApprovalList(ApprovalQueryDto queryDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCloudAlbumService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCloudAlbumService.java new file mode 100644 index 0000000..0d45646 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCloudAlbumService.java @@ -0,0 +1,30 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceCloudAlbum; + +import java.util.List; + +/** + * @Description: 考勤云相册 + * @Author: shiTou(他是小石头) + * @Date: 2024-10-30 10:26 + */ +public interface AttendanceCloudAlbumService extends SuperService { + /** + * 查询云相册列表 + * + * @return List + */ + List getDataList(); + + /** + * 保存云相册 + */ + void batchSave(List picUrlList); + + /** + * 获取登录用户水印 + */ + String getUserWatermark(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmDetailsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmDetailsService.java new file mode 100644 index 0000000..a4ef329 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmDetailsService.java @@ -0,0 +1,14 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceConfirmDetails; + +/** + * 考勤确认详情 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +public interface AttendanceConfirmDetailsService extends SuperService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmService.java new file mode 100644 index 0000000..e3315af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmService.java @@ -0,0 +1,116 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceConfirm; +import jnpf.model.attendance.dto.ConfirmPageListDto; +import jnpf.model.attendance.dto.ConfirmStatisticsDto; +import jnpf.model.attendance.vo.attendance.ConfirmDetailsVo; +import jnpf.model.attendance.vo.attendance.ConfirmPageListVo; +import jnpf.model.attendance.vo.attendance.ConfirmStatisticsVo; +import jnpf.model.thousandsfaces.TodayWorkVo; + +import java.util.Date; +import java.util.List; + +/** + * 考勤确认 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +public interface AttendanceConfirmService extends SuperService { + /** + * 获取考勤确认默认月份 + * + * @return 月份 + */ + String getConfirmMonth(); + + /** + * 考勤确认列表展示 + * + * @param dto 筛选条件 + * @return 考勤确认列表 + */ + PageInfo getPageList(ConfirmPageListDto dto); + + /** + * 考勤确认聚合统计 + * + * @param dto 筛选条件 + * @return 考勤确认列表 + */ + ConfirmStatisticsVo getStatistics(ConfirmStatisticsDto dto); + + /** + * 考勤确认详情 + * + * @param id 确认ID + * @return 考勤确认详情 + */ + ConfirmDetailsVo getConfirmDetails(String id); + + /** + * 获取App考勤确认详情 + * + * @return 考勤确认详情 + */ + ConfirmDetailsVo getAppConfirmDetails(String id); + + /** + * 自动生成考勤确认数据 + * + * @return 生成是否成功 + */ + boolean autoCreateConfirm(); + + /** + * 生成考勤确认数据 + * + * @param year 年份 + * @param month 月份 + * @param startTime 开始时间 + * @param slippageResult 逾期是否自动确认 + */ + void createConfirm(int year, int month, Date expectedTime, Date startTime, Integer slippageResult); + + /** + * 逾期自动确认 + * + * @return 逾期自动确认是否成功 + */ + boolean confirmAutoSlippage(); + + /** + * 温馨提示关闭 + * + * @param id 考勤确认id + * @return 关闭结果 + */ + Boolean tipsClos(String id); + + /** + * 温馨提示关闭 + * + * @param id 考勤确认id + * @return 关闭结果 + */ + Boolean confirmDetailsSubmit(String id); + + /** + * App已查看 + * + * @param id 考勤确认id + * @return 关闭结果 + */ + Boolean look(String id); + + /** + * 今日工作-考勤确认列表(0-待确认 1-已确认 2-已逾期) + * + * @return 考勤确认列表 + */ + List getTodayWorkConfirmList(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmSettingService.java new file mode 100644 index 0000000..7d82c64 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceConfirmSettingService.java @@ -0,0 +1,31 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceConfirmSetting; +import jnpf.model.attendance.dto.ConfirmSettingSubmitDto; +import jnpf.model.attendance.vo.attendance.ConfirmSettingInfoVo; + +/** + * 考勤确认设置 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +public interface AttendanceConfirmSettingService extends SuperService { + /** + * 获取考勤确认设置 + * @param isNew 是否获取最新配置 + * @return 勤确认设置 + */ + ConfirmSettingInfoVo getConfirmSetting(Boolean isNew); + + /** + * 考勤确认设置提交 + * + * @param dto 确认ID + * @return 考勤确认设置提交结果 + */ + Boolean confirmSettingSubmit(ConfirmSettingSubmitDto dto); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCustomizeTableService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCustomizeTableService.java new file mode 100644 index 0000000..1498db2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceCustomizeTableService.java @@ -0,0 +1,35 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.AttendanceCustomizeTable; +import jnpf.model.attendance.vo.attendance.AttendanceCustomizeTableVo; +import jnpf.model.attendance.vo.attendance.CustomizeTableUpdateVo; + +import java.util.List; + +/** + *

+ * 考勤-自定义报表设置 服务类 + *

+ * + * @author ahua + * @since 2024-09-03 + */ +public interface AttendanceCustomizeTableService extends SuperService { + /** + * 根据关键词、状态和类型查询自定义表格列表。 + * + * @param keyword 关键词,用于搜索过滤 + * @param status 状态,用于筛选设置 + * @param type 类型,用于筛选设置 + * @return 自定义表格视图对象列表 + */ + List findList(String keyword, Integer status, Integer type); + + /** + * 更新自定义表格信息。 + * + * @param tableVos 自定义表格数据传输对象列表 + */ + void update(CustomizeTableUpdateVo tableVos); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDailyRuleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDailyRuleService.java new file mode 100644 index 0000000..167a9ff --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDailyRuleService.java @@ -0,0 +1,432 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.*; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.v2.ClockOutHandleParam; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.LineDrawingSchedulesConfigDto; +import jnpf.model.attendance.dto.PeriodConfig; +import jnpf.model.attendance.dto.SchedulesImportDto; +import jnpf.model.attendance.dto.SchedulesSetDto; +import jnpf.model.attendance.dto.UnifiedSchedulesDto; +import jnpf.model.attendance.model.DayClockRange; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.OutOrBusApproveVo; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + *

+ * 考勤组-每日出勤规则 服务类 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +public interface AttendanceDailyRuleService extends SuperService { + + /** + * 导出指定月份的调度信息 + * + * @param groupId 调度组ID,用于标识一组调度信息 + * @param realName 用户真实姓名,用于过滤特定用户的调度信息 + * @param month 指定的月份,格式为"YYYY-MM",用于获取该月的调度信息 + * @param userIdList 用户ID列表,用于过滤需要导出调度信息的用户 + * @param isSchedules 是否包含调度信息的标志,用于指示是否需要导出调度详情 + */ + void schedulesExport(String groupId, String workGroupId, String realName, String month, List userIdList, Integer isSchedules); + + void hisDailyRule(Map group2orgMap, Map org2groupMap, Map> usersMap); + + void clockOutHandle(ClockOutHandleParam param); + + FtbAttendanceDailyRule workOvertimeNotApprove(ClockOutHandleParam param); + + /** + * 根据条件获取排班列表V2版本。 + * + * @param groupId 考勤组ID + * @param realName 真实姓名 + * @param month 月份 + * @param userIdList 用户ID列表 + * @param isSchedules 是否排班标识 + * @return 排班列表V2版本 + */ + List getSchedulesListV2(String groupId, String workGroupId, String realName, String month, List userIdList, Integer isSchedules); + + /** + * 根据条件获取排班列表 V2(按开始日期、结束日期,其它与 {@link #getSchedulesListV2} 一致) + * + * @param startDate 开始日期(yyyy-MM-dd) + * @param endDate 结束日期(yyyy-MM-dd) + */ + List getSchedulesListV2ByDateRange(String groupId, String workGroupId, String realName, String startDate, String endDate, List userIdList, Integer isSchedules); + + /** + * 按预排班 Redis 草稿({@link jnpf.attendance.schedule.ShiftPlanAssignmentResult#getByEmployee()} JSON,key 为租户 + 草稿 id) + * 转换为排班列表 V2 结构;草稿不存在或为空返回空列表。 + * + * @param groupId 考勤组 id + * @param startDate 开始日期 yyyy-MM-dd + * @param endDate 结束日期 yyyy-MM-dd + * @param draftId 草稿 id(Redis key 第二段) + */ + List getSchedulesListV2ByPreScheduleDraft( + String groupId, String startDate, String endDate, String draftId); + + /** + * 根据班次ID列表获取班次名称实体映射。 + * + * @param shiftIds 班次ID列表 + * @return 班次ID与班次名称实体的映射 + */ + Map getShiftByShiftIds(List shiftIds); + + /** + * 检查指定日期用户是否存在。 + * + * @param users 考勤组用户列表 + * @param date 日期 + * @return 用户存在标识(- 1离组 0未加入 1存在 2借调) + */ + Integer findUserIsExistsStatusByDay(List users, Date date); + + /** + * 检查指定日期用户是否存在。 + * + * @param users 考勤组用户列表 + * @param date 日期 + * @return 用户存在标识(1存在,0不存在) + */ + Integer findUserIsExistsByDay(List users, Date date); + + /** + * 检查指定日期范围内用户是否存在。 + * + * @param users 考勤组用户列表 + * @param start 开始日期 + * @param end 结束日期 + * @return 用户存在标识(1存在,0不存在) + */ + Integer findUserIsExistsByDay(List users, Date start, Date end); + + /** + * 检查指定日期范围内用户是否存在。 + * + * @param users 考勤组用户列表 + * @param start 开始日期 + * @param end 结束日期 + * @return 用户存在标识(1存在,0不存在) + */ + Map findUserIsExistsByUserList(List users, Date start, Date end); + + /** + * 判断当天内用户的状态, -1为已离 0为未入 1正常 2为全天被借调 3部分被借调 4借调 + * + * @param users 考勤组用户列表 + * @param date 日期 + * @return -1为已离 0为未入 1正常 2为全天被借调 3部分被借调 4借调 + */ + Integer isExistStatus(List users, Date date); + + /** + * 判断时间范围内用户的状态, -1为已离 0为未入 1正常 2为全天被借调 3部分被借调 4借调 + * @param users + * @param start + * @param end + * @return + */ + Integer isExistStatus(List users, Date start, Date end); + + /** + * 是否借调,不管借调还是被借调,只要命中借调时间,则返回true + * + * @param users 考勤组用户列表 + * @param start 开始日期 + * @param end 结束日期 + * @return 是否在借调状态 + */ + Boolean isInSecondment(List users, Date start, Date end); + + /** + * 获取排班规则详情。 + * + * @param id 排班规则ID + * @return 排班规则详情 + * @throws HandleException 处理异常 + */ + ScheduleRuleDetailVo getDetail(String id) throws HandleException; + + /** + * 修改固定周期班次设置。 + * + * @param groupIds 考勤组ID列表 + * @param periodList 周期班次设置列表 + * @param enableTime 生效时间 + */ + void fixedPeriodChange(List groupIds, AttendanceShiftSettingVo periodList, Date enableTime); + + /** + * 添加用户的固定班次规则处理。 + * + * @param groupId 考勤组ID + * @param userIds 用户ID列表 + */ + void addUserFixedHandle(String groupId, List userIds); + + /** + * 添加用户的固定班次规则处理,包含租户ID。 + * + * @param groupId 考勤组ID + * @param tenantId 租户ID + * @param userIds 用户ID列表 + */ + void addUserFixedHandle(String groupId, String tenantId, List userIds); + + /** + * 添加节假日日常规则。 + * + * @param start 开始日期 + * @param end 结束 + * @param attendanceGroupVos 考勤组列表 + * @param festivalSetting 节假日设置映射 + * @param hisFestivalSetting 历史节假日设置实体 + **/ + void addHolidayDailyRule(Date start, Date end, List attendanceGroupVos, AttendanceFestivalRules festivalSetting, AttendanceFestivalRules hisFestivalSetting, List newUserIds, List oldUserIds); + + /** + * 为自助调度设置时间表 + * + * @param shiftId 班次ID,用于标识特定的班次 + * @return 如果设置成功,返回确认信息;否则返回错误信息 + * @throws HandleException 如果设置过程中发生错误,则抛出此异常 + */ + String setSchedulesForSelfSchedules(String shiftId) throws HandleException; + + /** + * 更新班次配置时设置时间表 + * + * @param groupId 组ID,用于标识需要更新配置的组 + * @param mark 标记,用于指示更新的版本或状态 + * @param periodConfigs 时段配置列表,包含需要更新的班次配置信息 + * @return 如果设置成功,返回受影响的行数或状态码;否则返回错误信息 + * @throws HandleException 如果设置过程中发生错误,则抛出此异常 + */ + Integer setSchedulesForShiftConfigUpdate(String groupId, Integer mark, List periodConfigs, List periodEntities) throws HandleException; + + /** + * 设置排班 + * + * @param schedulesSets 排班设置列表 + * @return 排班结果字符串 + * @throws HandleException 处理异常 + **/ + String setSchedules(List schedulesSets) throws HandleException; + + /** + * 初始化固定排班规则。 + * + * @param tenantId 租户ID + */ + void initFixedScheduleRule(String tenantId); + + void clearGroupRule(String groupId, List userIds); + + /** + * 清除考勤组用户集合的考勤规则 + * + * @param groupId 考勤组ID + * @param userIds userId集合 + */ + void clearGroupRule(String groupId, List userIds, Date departTime, String tenantId); + + /** + * 申请验证的日规则处理 + * + * @param applyParam 申请参数类 + */ + List applyVerifyHandle(ApplyParam applyParam) throws HandleException; + + /** + * 申请的日规则处理 + * + * @param applyParam 申请参数类 + */ + String applyDailyRuleHandle(ApplyParam applyParam) throws HandleException; + + /** + * 借调申请日规则处理 + * + * @param userIds 借调用户id集合 + * @param fromGroupId 原考勤组id + * @param toGroupId 借调考勤组id + * @param start 开始时间 + * @param end 结束时间 + * @param departureTime 离岗时间 + * @param backTime 回岗时间 + * @param tenantId + */ + List secondmentDailyRuleHandle(List userIds, String fromGroupId, String toGroupId, Date start, Date end, Date departureTime, Date backTime, String tenantId) throws HandleException; + + /** + * 查询用户当日排班信息 + * + * @param groupId 群组ID,用于识别哪个群组的考勤规则需要查询 + * @param userId 用户ID,指定查询考勤规则的用户 + * @param day 日期,指定查询考勤规则的具体日期 + * @return 返回一个包含考勤规则的列表,这些规则适用于指定用户和日期 + */ + List getAttendanceDayRulesForStatistic(String groupId, String userId, Date day); + + /** + * 获取用户当日可打卡时间范围 + * 开始时间:如果前一天有夸日班次,开始时间就是夸日班次的下班缺卡时间,没有就是当日的00:00:00 + * 结束时间:当日最后一个班次的下班缺卡时间 + * + * @param userId 用户ID,指定查询考勤规则的用户 + * @param day 日期,指定查询考勤规则的具体日期 + * @return 返回一个包含开始时间和结束时间的对象 + */ + DayClockRange getDayClockRange(String userId, Date day); + + /** + * 获取当天最后一个班次的下班缺卡时间 + * + * @param userId 用户ID,指定查询考勤规则的用户 + * @param day 日期,指定查询考勤规则的具体日期 + * @return 返回一个Date对象,表示当天最后一个班次的下班缺卡时间 + */ + Date getDayEndRuleDeletionDate(String userId, Date day); + + List userIsSchedulingOrdinary(List organizeIds); + + boolean hasRuleByUserIdAndTime(String userId, Date start, Date end); + + /** + * 获取用户当天外出/出差批次号 + * + * @param userId 用户ID,指定查询考勤规则的用户 + * @param groupId 考勤组ID + * @param day 日期,指定查询考勤规则的具体日期 + * @param typeEnumList 出勤类型 + * @return 返回一个包含外出/出差批次号的列表 + */ + List getUserDayBusAndOutInfo(String userId, String groupId, Date day, List typeEnumList); + + /** + * 获取用户外出出差次数 + * + * @param userIds 用户ID,指定查询考勤规则的用户 + * @param groupIdList 考勤组ID集合 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param businessTrip 出勤类型 + * @return Integer + */ + Map getUserBusAndOutCount(List userIds, List groupIdList, Date startDate, Date endDate, AttendanceTypeEnum businessTrip); + + /** + * 用户是否排班 + * + * @param userId 用户ID + * @return boolean true: 是 false: 否 + */ + boolean userIsScheduling(String userId); + + List schedulesImport(SchedulesImportDto schedulesImportDto) throws IOException; + + + Map getUserPublicHoliday(String yearMonth, List userIds); + + /** + * 获取最早排班时间 + * + * @return Date + */ + Date getEarliestSchedulingDate(Date start); + + boolean hasLinearRulesByPeriod(String userId, Date start, Date end); + + /** + * 设置划线排班 + * + * @param configDto 划线排班配置DTO + * @return 处理结果 + * @throws HandleException 处理异常 + */ + String setLineDrawingSchedules(LineDrawingSchedulesConfigDto configDto) throws HandleException; + + boolean queryLineSchedulingExist(LineDrawingSchedulesConfigDto configDto); + + void lineSchedulesExport(String groupId, String workGroupId, String month, List userIdList); + + void lineSchedulesImport(SchedulesImportDto schedulesImportDto) throws IOException; + + List getLineSchedulesList(String groupId, String workGroupId, List dayList, List finalUserIdList); + /** + * 查询单个用户是否划线排班 + * @param userId + * @param start + * @param end + * @return + */ + boolean isLineScheduleByUserId(String userId, Date start, Date end); + + FtbAttendanceLineSchedulingConfig lineSchedulesConfigFilter(String groupId, List userIds); + + @Nullable FtbAttendanceLineSchedulingConfig getFtbAttendanceLineSchedulingConfig(List userIds, FtbAttendanceLineSchedulingConfig lineSchedulingConfig); + + /** + * 获取排班 + * @param finalStartDate 开始日期 + * @param finalEndDate 结束日期 + * @return 排班列表 + */ + List getDayRuleByMonth(LocalDate finalStartDate, LocalDate finalEndDate); + + /** + * 查询指定用户指定日期的班次信息,合并展示普班和请假时段 + * 请假信息作为班次信息的补充,当班次被请假覆盖时,根据覆盖情况展示时段 + * + * @param userId 用户ID + * @param queryDate 查询日期 + * @return 用户指定日期时段信息(包含普班和请假) + */ + UserDayShiftInfoVo getUserDayShiftInfo(String userId, Date queryDate); + + /** + * 统一排班接口(支持固定排班和划线排班) + * + * @param dto 统一排班DTO + * @return 处理结果消息 + * @throws HandleException 处理异常 + */ + String setUnifiedSchedules(UnifiedSchedulesDto dto) throws HandleException; + + /** + * 查询考勤组近 90 天历史排班:按自然日返回班次及岗位人数;划线排班一人一条,人数为 1 + * + * @param groupId 考勤组 id + * @return 每日营业额(预留)、班次及岗位人数列表 + */ + List getGroupShiftHistory90Days(String groupId); + + /** + * 按自然日查询考勤组所属门店营业额预估 + * + * @param groupId 考勤组 id + * @param startTime 开始日期(含) + * @param endTime 结束日期(含) + * @return 按日营业额预估列表 + */ + List listReceivableRevenueByDay(String groupId, Date startTime, Date endTime); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDayStatisticsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDayStatisticsService.java new file mode 100644 index 0000000..25b117b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceDayStatisticsService.java @@ -0,0 +1,536 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.dto.*; +import jnpf.base.vo.PageListVO; +import jnpf.entity.attendance.AttendanceDayStatistics; +import jnpf.exception.LoginException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.event.StatisticsBatchClearDto; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.event.StatisticsSingleHistoryDto; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.permission.vo.v2.user.UserBoundVO; +import org.apache.commons.lang3.tuple.MutablePair; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 考勤日度统计表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-06-24 09:33:43 + */ +public interface AttendanceDayStatisticsService extends IService { + + /** + * 获取有权限的用户列表 + * + * @param filterList 筛选条件 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param userIdsFilter 用户ID + * @param workStatus 用户状态 + * @return 用户列表 + */ + List getUserIdArr(List filterList, String startDate, String endDate, List userIdsFilter, String workStatus); + + /** + * 获取日度统计数据。 + * 根据请求参数(DayStatisticsDataDto)查询并返回日度统计数据的列表。 + * + * @param req 请求参数,包含查询条件等。 + * @return 返回日度统计数据的列表(List)。 + */ + List getDayStatisticsData(DayStatisticsDataDto req); + + /** + * 获取日度统计数据的分页列表。 + * 根据请求参数(DayStatisticsPageListDto)分页查询并返回日度统计数据的列表。 + * + * @param req 请求参数,包含分页信息和查询条件等。 + * @return 返回分页的日度统计数据列表(PageInfo)。 + */ + PageInfo getDayPageList(DayStatisticsPageListDto req); + + /** + * 执行日度统计数据的导出操作。 + * 根据请求参数(DayStatisticsExportDto)生成并导出日度统计数据的文件。 + * + * @param req 请求参数,包含导出所需的条件和选项。 + */ + void dayDataExport(DayStatisticsExportDto req); + + /** + * 获取月度统计数据。 + * 根据请求参数(MouthStatisticsDataDto)查询并返回月度统计数据的列表。 + * 注意:方法名中的"Mouth"可能是笔误,应为"Month"。 + * + * @param req 请求参数,包含查询条件等。 + * @return 返回月度统计数据的列表(List,这里可能是类型复用或错误,通常应为特定的月度数据类型)。 + */ + List getMonthStatisticsData(MouthStatisticsDataDto req); + + /** + * 获取月度统计数据的分页列表。 + * 根据请求参数(MonthStatisticsPageListDto)分页查询并返回月度统计数据的列表。 + * + * @param req 请求参数,包含分页信息和查询条件等。 + * @return 返回分页的月度统计数据列表(PageInfo)。 + * @throws Exception 抛出异常以处理可能出现的错误情况。 + */ + PageInfo getMonthPageList(MonthStatisticsPageListDto req) throws Exception; + + /** + * 执行月度统计数据的导出操作。 + * 根据请求参数(MonthStatisticsExportDto)生成并导出月度统计数据的文件。 + * + * @param req 请求参数,包含导出所需的条件和选项。 + */ + void monthDataExport(MonthStatisticsExportDto req); + + /** + * 获取排班格子统计数据。 + * 根据请求参数(LatticeStatisticsVoDto)查询并返回排班格子统计数据。 + * + * @param req 请求参数,包含查询条件等。 + * @return 返回排班格子统计数据(LatticeStatisticsVo)。 + */ + LatticeStatisticsVo getLatticeStatistics(LatticeStatisticsVoDto req); + + /** + * 用户加入时生成空白统计数据。 + * 当用户加入指定组时,为该用户生成初始的空白统计数据。 + * + * @param groupId 组ID,指定用户加入的组。 + * @param userIds 用户ID列表,包含加入组的用户。 + */ + void userJoinHandleData(String tenantId, String groupId, List userIds); + + /** + * 用户加入时生成空白统计数据。 + * 当用户加入指定组时,为该用户生成初始的空白统计数据。 + * + * @param date 加入时间 + * @param groupId 组ID,指定用户加入的组。 + * @param userIds 用户ID列表,包含加入组的用户。 + */ + void userJoinHandleData(String tenantId, Date date, String groupId, List userIds); + + /** + * 用户日统计数据初始化。 + * 初始化用户日统计数据的相关操作,如重置统计记录等。 + * + * @return 返回操作是否成功的布尔值。 + */ + Boolean handleDataForJob(String tenantId); + + /** + * 批量清除日统计数据。 + * 根据给定的组ID、用户ID和日期,清除用户的日统计数据。 + * + * @param courseEventDTO 批量清除用户日统计数据所需的参数。 + */ + void batchStatisticDataClear(StatisticsBatchClearDto courseEventDTO); + + /** + * 组织重构考勤数据变更 + * + * @param tenantId 租户id + * @param start 开始时间 + */ + void regenerateDayData(String tenantId, Date start); + + /** + * 处理历史数据 + * + * @param tenantId 租户id + * @param start 开始时间 + * @param end 结束时间 + */ + void processHistoricalData(String tenantId, Date start, Date end); + + /** + * 生成用户日统计数据。 + * + * @param singleDto 单个用户日统计数据变更参数 + */ + void statisticDataChange(StatisticsSingleDto singleDto) throws LoginException; + + /** + * 生成用户日统计数据。 + * + * @param singleDto 单个用户日统计数据变更参数 + */ + void statisticDataChangeHistory(StatisticsSingleHistoryDto singleDto) throws LoginException; + + /** + * 发送个人考勤日报通知。 + * 根据给定的租户ID,发送个人考勤日报通知。 + * + * @param tenantId 租户ID。 + * @return 返回操作是否成功的布尔值。 + */ + Boolean dayStatisticsNotice(String tenantId); + + /** + * 发送个人统计月报通知。 + * 根据给定的租户ID,发送个人统计月报通知。 + * + * @param tenantId 租户ID。 + * @return 返回操作是否成功的布尔值。 + */ + Boolean monthStatisticsNotice(String tenantId); + + /** + * 发送连续未排班通知。 + * 根据给定的租户ID,发送连续未排班通知。 + * + * @param tenantId 租户ID。 + */ + void consentUnscheduledNotice(String tenantId); + + /** + * 发送团队统计月报通知。 + * 根据给定的租户ID,发送团队统计月报通知。 + * + * @param tenantId 租户ID。 + * @return 返回操作是否成功的布尔值。 + */ + Boolean teamMonthStatisticsNotice(String tenantId); + + /** + * 计算考勤组平均工时(实际出勤工时) + * + * @param dto 筛选条件 + * @return 考勤平均工时 + */ + List countAttendanceAvgHours(AttendanceCountAvgHoursDto dto); + + + /** + * 获取多考勤组月度统计数据 + * + * @param dto 筛选条件 + * @return 月度统计数据 + */ + MonthStatsDetailsVo getAttendanceAvgHoursDetails(MonthStatsDetailsDto dto); + + /** + * 获取多考勤组月度人均工时折线图 + * + * @param dto 筛选条件 + * @return 考勤组月度人均工时折线图 + */ + List getAttendanceMonthPerCapita(MonthStatsDetailsDto dto); + + /** + * 获取多考勤组月度日常情况 + * + * @param dto 筛选条件 + * @return 考勤组月度日常情况 + */ + List getAttendanceDailySituation(MonthStatsDetailsDto dto); + + /** + * 获取多考勤组月度考勤工时排行 + * + * @param dto 筛选条件 + * @return 考勤组月度考勤工时排行 + */ + List getAttendanceHoursRanking(MonthStatsDetailsDto dto); + + /** + * 获取多考勤组月度全勤情况 + * + * @param dto 筛选条件 + * @return 考勤组月度出勤情况 + */ + List getAttendanceFullSituation(MonthStatsDetailsDto dto); + + /** + * 获取多考勤组月度异常情况 + * + * @param dto 筛选条件 + * @return 考勤组月度异常情况 + */ + MonthStatsAbnormalConditionVo getAttendanceAbnormalCondition(MonthStatsDetailsDto dto); + + /** + * 获取多考勤组月度加班情况 + * + * @param dto 筛选条件 + * @return 考勤组月度加班情况 + */ + List getAttendanceOvertimeSituation(MonthStatsDetailsDto dto); + + /** + * 获取当月考勤确认数据 + * + * @param userIds 用户ID列表 + * @param month 考勤月份 + * @return 考勤组月度加班情况 + */ + List getConfirmDetailsInfoByMonth(List userIds, String month); + + /** + * 考勤封账-分页列表 + * + * @param dto 考勤封账分页列表参数 + * @return 考勤封账分页列表结果 + */ + PageInfo sealPageList(MonthSealPageListDto dto); + + /** + * 考勤封账/批量|单个封账 + * + * @param dto 考勤封账参数 + * @return 考勤封账结果 + */ + Boolean sealSubmit(MonthSealSubmitDto dto); + + /** + * 考勤封账/解封 + * @param dto 用户ID + * @return 解封结果 + */ + Boolean unSealSubmit(MonthUnSealSubmitDto dto); + + /** + * 批量查询用户在指定月份是否封账 + * + * @param userIdList 用户ID集合 + * @param month 月份(yyyy-MM) + * @return 是否封账 + */ + Map selectUserIsSeal(List userIdList, String month); + + /** + * 批量查询用户在指定日期范围内是否存在封账记录 + * + * @param userIdList 用户ID集合 + * @param start 范围开始(含当日) + * @param end 范围结束(含当日) + * @return 是否封账 + */ + Map selectUserIsSealByDateRange(List userIdList, Date start, Date end); + + /** + * 按天查询用户在日期范围内的已封存日期集合(跨月场景使用) + * 返回每个用户已被封存的日期(yyyy-MM-dd)集合,用于在排班视图中按天判断锁定状态 + * + * @param userIdList 用户ID集合 + * @param start 范围开始(含当日) + * @param end 范围结束(含当日) + * @return userId -> 已封存日期字符串集合(yyyy-MM-dd) + */ + Map> selectUserDailySealByDateRange(List userIdList, Date start, Date end); + + /** + * 薪酬考勤数据支持(薪酬) + * + * @param dto 薪酬考勤数据支持参数 + * @return 薪酬考勤数据支持结果 + */ + Map salaryAttendanceSupport(SalaryAttendanceSupportDto dto); + + /** + * 考勤统计数据列表(薪酬) + * + * @param dto 考勤统计数据列表参数 + * @return 考勤统计数据列表结果 + */ + List attendanceStaList(SalaryAttendanceSupportDto dto); + + /** + * 获取考勤统计数据列表 + * + * @param dto 获取考勤统计数据列表参数 + * @return 考勤统计数据列表结果 + */ + List getAttendanceDayStaList(DayStatisticsDto dto); + + /** + * 获取考勤组维度统计数据 + * + * @param dto 筛选条件 + * @return 考勤组维度统计数据 + */ + Map> getDimensionsAttendanceCountMap(DimensionsAttendanceCountDto dto); + + /** + * 获取考勤组维度统计数据-日维度 + * + * @param dto 筛选条件 + * @return 考勤组维度统计数据 + */ + Map> getDimensionsAttendanceDayCountMap(DimensionsAttendanceDayCountDto dto); + + /** + * 日度计薪统计-分页列表 + * + * @param req 筛选条件 + * @return 考勤组维度统计数据 + */ + PageInfo getDayPayrollPageList(DayPayrollStatisticsPageListDto req); + + /** + * 月度计薪统计-分页列表 + * + * @param req 筛选条件 + * @return 考勤组维度统计数据 + */ + PageInfo getMonthPayrollPageList(MonthPayrollStatisticsPageListDto req) throws Exception; + + /** + * 考勤平均工时趋势-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO averageWorkHoursTrendPageList(PersonnelApiInfoPageListDto pageDto); + + /** + * 考勤平均工时趋势-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void averageWorkHoursTrendExport(HttpServletResponse response, PersonnelApiInfoPageListDto pageDto) throws Exception; + + /** + * 考勤人均工时趋势-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO personWorkHoursTrendPageList(PersonnelApiInfoSinglePageListDto pageDto); + + /** + * 考勤人均工时趋势-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void personWorkHoursTrendExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception; + + /** + * 考勤日常情况-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO dailySituationPageList(PersonnelApiInfoSinglePageListDto pageDto); + + /** + * 考勤日常情况-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void dailySituationExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception; + + /** + * 考勤工时排行-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO workHoursRankingPageList(PersonnelApiInfoSinglePageListDto pageDto); + + /** + * 考勤工时排行-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void workHoursRankingExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception; + + /** + * 考勤全勤情况-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO fullAttendanceStatusPageList(PersonnelApiInfoSinglePageListDto pageDto); + + /** + * 考勤全勤情况-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void fullAttendanceStatusExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception; + + /** + * 考勤异常情况-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO exceptionSituationPageList(PersonnelApiInfoSinglePageListDto pageDto); + + /** + * 考勤考勤异常情况-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void exceptionSituationExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception; + + /** + * 考勤加班情况-分页列表 + * + * @param pageDto 筛选条件 + * @return 分页列表 + */ + PageListVO overtimeSituationPageList(PersonnelApiInfoSinglePageListDto pageDto); + + /** + * 考勤加班情况-导出 + * + * @param response 响应 + * @param pageDto 筛选条件 + */ + void overtimeSituationExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception; + + /** + * 查询用户考勤统计信息 + * + * @param dto 筛选条件 + * @return 考勤统计信息 + */ + QueryStatisticsInfoVo queryUserStatisticsInfo(QueryStatisticsInfoDto dto); + + /** + * 查询考勤组的考勤情况 + * + * @param dto 筛选条件 + * @return 考勤组维度统计信息 + */ + QueryGroupStatisticsVo queryGroupStatistics(QueryGroupStatisticsDto dto); + + /** + * 查询用户的加班情况 + * + * @param dto 筛选条件 + * @return 用户加班信息 + */ + QueryUserOvertimeVo queryUserOvertime(QueryStatisticsInfoDto dto); + + /** + * 查询用户工作状况 + * + * @param dto 筛选条件 + * @return Map<用户ID, Map<日期,是否排班>> + */ + Map queryUserWorkSituation(UserWorkSituationDto dto); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalRulesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalRulesService.java new file mode 100644 index 0000000..335a01a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalRulesService.java @@ -0,0 +1,88 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.entity.attendance.AttendanceFestivalRules; +import jnpf.model.attendance.dto.AttendanceFestivalRulesDto; +import jnpf.model.attendance.dto.FestivalDto; +import jnpf.model.attendance.dto.FestivalRulesQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceFestivalRulesVo; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface AttendanceFestivalRulesService { + + /** + * 列表 + * @param queryDto 查询条件 + * @return 列表 + */ + PageInfo getPageList(FestivalRulesQueryDto queryDto); + + /** + * 新增 + * @param festivalRulesDto 参数 + */ + void add(AttendanceFestivalRulesDto festivalRulesDto) throws Exception; + + + void statutoryUpdate(String year); + + /** + * 修改 + * @param festivalRulesDto 参数 + */ + void update(AttendanceFestivalRulesDto festivalRulesDto) throws Exception; + + /** + * 详情 + * @param id 主键值 + */ + AttendanceFestivalRulesVo detail(String id); + + /** + * 删除 + * @param id 主键值 + */ + void delete(String id); + + /** + * 更新状态 + * @param festivalRulesDto festivalRulesDto + */ + void updateState(AttendanceFestivalRulesDto festivalRulesDto); + + /** + * 检查规则 + * @param date 日期 + * @param userId 用户ID + * @return boolean + */ + boolean checkFestivalRules(Date date, String userId); + + /** + * 批量[多用户]判定日期内是否节假日 + * @param date 日期 + * @param userIds 用户ID + * @return java.util.Map + */ + Map> checkFestivalRulesBatch(Date date, List userIds); + + /** + * 批量检查规则 + * @param date 日期 + * @param userIds 用户ID列表 + * @return boolean + */ + Map batchCheckFestivalRules(Date date, List userIds); + + /** + * 批量获取用户指定时间区间内的所有节假日 + * @param start 开始时间 + * @param end 结束时间 + * @param userIds 用户ID列表 + * @return java.util.Map> + */ + Map> batchGetFestivalRulesByUserIds(Date start ,Date end, List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalSettingService.java new file mode 100644 index 0000000..c071a54 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFestivalSettingService.java @@ -0,0 +1,106 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceFestivalSettingEntity; +import jnpf.base.service.SuperService; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceFestivalSettingDto; +import jnpf.model.attendance.vo.AttendanceFestivalSettingVo; +import jnpf.model.attendance.vo.HolidayOptionVo; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 考勤配置-节日配置 服务类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceFestivalSettingService extends SuperService { + + /** + * 保存节日设置信息。 + * + * @param attendanceFestivalSettingDto 节日设置数据传输对象 + * @throws HandleException 处理异常 + */ + void save(AttendanceFestivalSettingDto attendanceFestivalSettingDto) throws HandleException; + + /** + * 分页查询节日设置信息。 + * + * @param groupId 考勤组ID + * @param year 年份 + * @param page 页码 + * @param size 每页大小 + * @return 分页的节日设置视图对象 + * @throws HandleException 处理异常 + */ + PageDTO page(String groupId, String year, Integer page, Integer size) throws HandleException; + + /** + * 根据ID获取单个节日设置信息。 + * + * @param id 节日设置ID + * @return 节日设置视图对象 + */ + AttendanceFestivalSettingVo getOne(String id); + + /** + * 删除指定的节日设置信息。 + * + * @param id 节日设置ID + */ + void del(String id); + + /** + * 获取启用状态的节日设置信息。 + * + * @param groupIds 考勤组ID列表 + * @param attendanceGroupVos 考勤组视图对象列表 + * @return 启用的节日设置信息映射 + */ + Map> getEnableFestivalSetting(List groupIds, List attendanceGroupVos); + + /** + * 自动授予假期余额。 + */ +// void autoGrantBalance(); + + /** + * 更新法定节日信息。 + * + * @param groupId 考勤组ID + * @param year 年份 + */ + void statutoryUpdate(String groupId, String year); + + /** + * 更新节日设置信息的启用状态。 + * + * @param id 节日设置ID + * @param enable 启用状态(0禁用,1启用) + * @throws HandleException 处理异常 + */ + void updateStatus(String id, Integer enable) throws HandleException; + + /** + * 初始化节日设置。 + * + * @param groupId 考勤组ID + */ + void initFestivalSetting(String groupId); + + /** + * 获取节日选项列表。 + * + * @param groupId 考勤组ID + * @return 节日选项视图对象列表 + */ + List getHolidayOptions(String groupId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFixedClassService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFixedClassService.java new file mode 100644 index 0000000..df23e53 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceFixedClassService.java @@ -0,0 +1,16 @@ +package jnpf.attendance.service; + +import jnpf.entity.attendance.AttendanceFixedClassEntity; +import jnpf.base.service.SuperService; + +/** + *

+ * 固定班关联表 服务类 + *

+ * + * @author ahua + * @since 2024-05-10 + */ +public interface AttendanceFixedClassService extends SuperService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceGroupService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceGroupService.java new file mode 100644 index 0000000..d7de65b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceGroupService.java @@ -0,0 +1,480 @@ +package jnpf.attendance.service; + +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.AttendanceUserListGroupVO; +import jnpf.base.service.SuperService; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AppointPermission; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.AttendanceGroupChargeVo; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.AttendanceOrgOrGroupVo; +import jnpf.model.attendance.vo.attendance.GroupLockVo; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 打卡服务 + * + * @author yanwenfu + * @create 2023-11-21 + */ +public interface AttendanceGroupService extends SuperService { + + /** + * 新增考勤组 + */ + int save(SaveAttendanceGroupDto saveAttendanceGroupDto) throws Exception; + + /** + * 根据用户ID和申请时间范围获取考勤组ID列表 + * + * @param userId 用户ID + * @param applyStart 申请开始时间 + * @param applyEnd 申请结束时间 + * @return java.util.List 考勤组ID列表 + * @throws HandleException 处理异常 + */ + List getGroupIdByUserId(String userId, Date applyStart, Date applyEnd) throws HandleException; + + /** + * 根据用户列表和时间范围获取考勤组ID列表 + * + * @param users 用户列表 + * @param start 开始时间 + * @param end 结束时间 + * @return java.util.List 考勤组ID列表 + */ + List getGroupIdByUserId(List users, Date start, Date end); + + /** + * 删除考勤组 + * + * @param id 考勤组ID + * @param parentId 父考勤组ID + */ + void delete(String id, String parentId); + + /** + * 切换考勤组状态 + * + * @param dto 考勤组状态DTO + */ + void exchangeGroupStatus(AttendanceGroupStatusDto dto); + + /** + * 获取考勤组及其子组ID列表 + * + * @param groupId 考勤组ID + * @param settingType 设置类型 + * @return java.util.List 考勤组ID列表 + */ + List getSelfAndChildrenGroupIds(String groupId, Integer settingType); + + /** + * 查询本组及(继承)子组考勤组成员 + * + * @param groupId 考勤组id + * @return java.util.List 考勤组成员列表 + */ + List getSelfAndChildrenMembers(String groupId); + + /** + * 查询考勤组列表 + * + * @param groupName 考勤组名称 + * @return java.util.List 考勤组列表 + */ + List queryList(String groupName); + + /** + * 查询包括已删除的考勤组列表 + * + * @param groupIds 考勤组ID列表 + * @return java.util.List 考勤组列表 + */ + List queryListIncludeDeleteByIds(List groupIds); + + /** + * 根据ID列表查询考勤组列表 + * + * @param groupIds 考勤组ID列表 + * @return java.util.List 考勤组列表 + */ + List queryListByIds(List groupIds); + + /** + * 根据所有ID查询考勤组列表 + * + * @return java.util.List 考勤组列表 + */ + List queryAllListByIds(); + + /** + * 查询我管理的考勤组 + * + * @param keyword 搜索关键字 + * @return java.util.List 我管理的考勤组列表 + */ + List queryManagerGroupList(String keyword); + + /** + * 根据考勤组名称模糊查询考勤组列表(带权限控制) + * 返回用户有管理权限的考勤组,支持名称模糊查询 + * + * @param groupName 考勤组名称(支持模糊查询) + * @return java.util.List 考勤组列表 + */ + List searchGroupByName(String groupName); + + /** + * 查询下拉列表考勤组 + * + * @return java.util.List 下拉列表考勤组 + */ + List queryDropList(); + + /** + * 查询下拉列表考勤组 + * + * @return java.util.List 下拉列表考勤组 + */ + AttendanceGroup queryByGroupId(String groupId); + + /** + * 查询我的考勤组列表 + * + * @return java.util.List 我的考勤组列表 + */ + List queryMyGroups(); + + /** + * 考勤组管理 获取我管理的考勤组(树状结构展示) + * @return java.util.List 我管理的考勤组树状列表 + */ + List groupManagerList(GroupQueryDto groupQueryDto); + + /** + * 获取启用状态的考勤组ID列表 + * + * @param groupId 考勤组ID + * @return java.util.List 启用状态的考勤组ID列表 + */ + List getEnableGroupIds(String groupId); + + /** + * 借调开始 + * + * @param attendanceSecondedDto 借调信息DTO + * @throws HandleException 处理异常 + */ + void secondedStart(AttendanceSecondedDto attendanceSecondedDto) throws HandleException; + + /** + * 获取一个默认考勤组 + * + * @return AttendanceGroupVo 默认考勤组信息 + */ + AttendanceGroupVo getDefaultGroup(); + + /** + * 切换考勤组 + * + * @param groupId 考勤组id + */ + void handoffGroup(String groupId); + + /** + * 获取考勤组设置状态 + * + * @param groupId 考勤组ID + * @param aFalse 布尔值,表示是否为默认状态 + * @return AttendanceGroupStatusDto 考勤组状态DTO + */ + AttendanceGroupStatusDto groupSettingStatus(String groupId, Boolean aFalse); + + /** + * 获取我有排班权限的考勤组列表 + * + * @return java.util.List 我有排班权限的考勤组列表 + */ + List schedulingGroupList(); + + /** + * 获取我有排班权限的考勤组列表(非固定班次) + * + * @return java.util.List 我有排班权限的考勤组列表(非固定班次) + */ + List schedulingGroupListByNotFixed(); + + /** + * 获取我有余额管理权限的考勤组列表 + * + * @return java.util.List 我有余额管理权限的考勤组列表 + */ + List balanceManagement(); + + /** + * 获取我有借调审批权限的考勤组列表 + * + * @return java.util.List 我有借调审批权限的考勤组列表 + */ + List secondedApprovalGroupList(); + + /** + * 获取切换考勤组列表以树结构展示 + * + * @return java.util.List 切换考勤组树结构列表 + */ + List handoffGroupTreeList(); + + /** + * 通过组织Id找到组织下的考勤组 + * + * @param orgId 组织Id + * @return java.util.List 组织下的考勤组列表 + */ + List groupListByOrgId(String orgId); + + /** + * 通过考勤组Id获取绑定的组织名称(拼接好的) + * + * @param groupId 考勤组Id + * @return AttendanceGroupVo 考勤组绑定的组织名称信息 + */ + AttendanceGroupVo getGroupBindingOrg(String groupId, String workGroupId); + + AttendanceGroupVo getGroupBindingOrgForLine(String groupId, String workGroupId); + + /** + * 绑定考勤组的组织 + * + * @param attendanceGroupOrgDto 考勤组和组织信息 + */ + void updateGroupOrg(AttendanceGroupOrgDto attendanceGroupOrgDto); + + /** + * 查询用户所在考勤组 + * + * @param userIds 用户ids + * @return java.util.List 用户所在考勤组列表 + */ + List getAttendanceUserGroup(List userIds); + + /** + * 锁定考勤组 + * + * @param groupId 考勤组id + */ + void lockGroup(String groupId); + + + void oldDataProcessing(String tenantId); + + Map batchSaveGroupByOrgIds(String tenantId, List orgIds); + + /** + * 获取组织下所有考勤组 + * + * @param organizeId 组织ID + * @return java.util.List 组织下的所有考勤组列表 + */ + List getByOrgId(String organizeId); + + /** + * 获取组织下所有考勤组 + * + * @param organizeIds 组织ID + * @return java.util.List 组织下的所有考勤组列表 + */ + List getByOrgIds(List organizeIds); + + /** + * 查询用户当前考勤组锁定信息 + * + * @param userId 用户id + * @return jnpf.model.attendance.vo.attendance.GroupLockVo 用户当前考勤组锁定信息 + * @throws QueryException 查询异常 + */ + GroupLockVo getUserCurrentGroupLockInfo(String userId) throws QueryException; + + /** + * 查询当前考勤组锁定信息 + * + * @param groupId 考勤组id + * @return jnpf.model.attendance.vo.attendance.GroupLockVo 当前考勤组锁定信息 + */ + GroupLockVo getGroupLockInfo(String groupId); + + /** + * 根据用户ID列表获取考勤组信息 + * + * @param userIds 用户ID列表 + * @return java.util.List 用户考勤组信息列表 + */ + List getAttendanceUserListGroupVO(List userIds); + + /** + * 根据组织名称和考勤组名称获取考勤组信息 + * + * @param organizeName 组织名称 + * @param groupName 考勤组名称 + * @return AttendanceGroupVo 考勤组信息 + */ + AttendanceGroupVo getAttendanceGroupByNameOrganizeAndGroup(String organizeName, String groupName); + + /** + * 获取审批考勤组列表 + * @param name 考勤组名称模糊搜索 + */ + List viewApprovalGroupList(String name); + + /** + * 获取用户管理的考勤组信息 + * + * @return java.util.Map> 用户管理的考勤组信息映射表 + */ + Map> userManagerGroupInfo(); + + /** + * 获取组织下所有考勤组 + * + * @param orgIds 组织ID列表 + * @return List + */ + List getGroupListByOrgIds(List orgIds); + + /** + * 查询用户是否拥有指定考勤组的指定权限 + * @param permissionCode 参数 + */ + boolean appointPermission(AppointPermission permissionCode); + + /** + * 获取指定考勤组的设置(是否继承上级) + * @param groupId 考勤组Id + */ + AttendanceGroup getGroupSetting(String groupId); + + /** + * V1.9 + * 获取我管理的考勤组(树状结构展示) + * @param groupQueryDto 参数 + */ + List groupMyManagerList(GroupQueryDto groupQueryDto); + + /** + * V1.9 + * 获取我管理的考勤组(树状结构展示)排除固定班 + */ + List schedulingGroupListByNotFixedNew(); + + /** + * V1.9 + * 切换考勤组查询我管理的考勤组 + */ + List handoffGroupTreeListNew(); + + /** + * V1.9 获取默认考勤组 + */ + AttendanceGroupVo getDefaultGroupNew(); + + /** + * V1.9 获取我管理的考勤组 + */ + List schedulingGroupListNew(); + + /** + * V1.9 余额考勤组列表 + */ + List balanceManagementNew(); + + /** + * V1.9 查询我有借调权限的考勤组管理列表 + */ + List secondedApprovalGroupListNew(); + + /** + * V1.9 获取审批考勤组列表 + * @param name 考勤组名称模糊搜索 + */ + List viewApprovalGroupListNew(String name); + + /** + * 获取指定用户管理的考勤组 + * @param userId 用户Id + * @param b 是否排除固定排班 + * @return List 考勤组Id + */ + List getGroupIdsByManagePermission(String userId, boolean b); + + /** + * 检查调度是否有效 + * 此方法用于确定当前调度状态是否允许继续执行或需要停止 + * 它不接受任何参数 + * + * @return boolean 表示调度是否有效的布尔值如果返回true,表示调度可以继续进行; + * 如果返回false,表示调度需要停止或重新评估 + */ + boolean checkScheduling(); + + /** + * 获取权限组列表 + * 此方法用于获取指定职位模块下的权限组列表 + * + * @param positionModuleName 职位模块名称 + * @return String 返回权限组列表的字符串表示 + */ + List getPermissionsGroupList(String positionModuleName); + + /** + * 查询考勤组列表(带权限) + * @return java.util.List + */ + List queryGroupListNew(String loginUserId, GroupQueryDto groupQueryDto); + + /** + * 获取组织(考勤组)列表 + * @param keyword 关键字 + * @return 组织下的所有考勤组列表 + */ + List getOrgOrGroupList(String keyword); + + /** + * 设置考勤组负责人 + * + * @param dto 考勤组负责人信息 + * @return Boolean + */ + Boolean setGroupCharge(AttendanceGroupChargeDto dto); + + /** + * 获取考勤组负责人信息 + * + * @param groupId 考勤组ID + * @return 考勤组负责人信息 + */ + AttendanceGroupChargeVo getGroupChargeInfo(String groupId); + + /** + * 移除考勤组负责人 + * + * @param groupId 考勤组ID + * @param removeUserIds 移除的用户ID列表 + */ + void removeManager(String groupId, List removeUserIds); + + /** + * 根据用户ID查询当前时间所在考勤组(包含借调) + * + * @param userId 用户ID + * @return java.util.List 考勤组列表 + */ + List getUserCurrentGroups(String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceHolidaySettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceHolidaySettingService.java new file mode 100644 index 0000000..657aac2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceHolidaySettingService.java @@ -0,0 +1,87 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceHolidaySettingEntity; +import jnpf.base.service.SuperService; +import jnpf.model.attendance.dto.AttendanceHolidaySettingDto; +import jnpf.model.attendance.vo.AttendanceHolidaySettingVo; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 考勤配置-假日设置 服务类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceHolidaySettingService extends SuperService { + /** + * 分页查询假日设置信息。 + * + * @param groupId 考勤组ID + * @param name 节假日名称(用于搜索过滤) + * @param paidSalaryEnable 是否计入薪酬标识(用于搜索过滤) + * @param page 页码 + * @param size 每页大小 + * @return 分页的假日设置视图对象 + */ + PageDTO page(String groupId, String name, Integer paidSalaryEnable, Integer page, Integer size); + + /** + * 根据ID获取单个假日设置信息。 + * + * @param id 节假日设置ID + * @return 节假日设置视图对象 + */ + AttendanceHolidaySettingVo getOne(String id); + + /** + * 删除指定的假日设置信息。 + * + * @param id 节假日设置ID + */ + void del(String id); + + /** + * 初始化假日设置。 + * + * @param groupId 考勤组ID + */ + void initHolidaySetting(String groupId); + + /** + * 为指定租户自动授予假期余额。 + * + * @param tenantId 租户ID + */ +// void autoGrantBalance(String tenantId); + + /** + * 获取启用状态的假日设置信息。 + * + * @param groupIds 考勤组ID列表 + * @param attendanceGroupVos 考勤组视图对象列表 + * @return 启用的假日设置信息映射 + */ + Map> getEnableFestivalSetting(List groupIds, List attendanceGroupVos); + + /** + * 保存假日设置信息。 + * + * @param dto 假日设置数据传输对象 + */ + void save(AttendanceHolidaySettingDto dto); + + /** + * 更改假日设置信息的启用状态。 + * + * @param id 假日设置ID + * @param enable 启用状态(0禁用,1启用) + */ + void changeStatus(String id, Integer enable); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveRulesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveRulesService.java new file mode 100644 index 0000000..30fc65a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveRulesService.java @@ -0,0 +1,79 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.exception.ApproveException; +import jnpf.model.attendance.dto.AttendanceLeaveRulesDto; +import jnpf.model.attendance.dto.AttendanceLeaveRulesQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; +import jnpf.model.common.PageDto; + +import java.util.List; + +/** + * @author panpan + */ +public interface AttendanceLeaveRulesService { + /** + * 列表 + * @param attendanceLeaveRulesQueryDto 搜索条件 + * @return 列表 + */ + PageInfo list(AttendanceLeaveRulesQueryDto attendanceLeaveRulesQueryDto); + + /** + * 创建 + * @param attendanceLeaveRulesDto 创建参数 + */ + void create(AttendanceLeaveRulesDto attendanceLeaveRulesDto) throws Exception; + + /** + * 修改 + * @param attendanceLeaveRulesDto 修改参数 + */ + void update(AttendanceLeaveRulesDto attendanceLeaveRulesDto) throws Exception; + + /** + * 详情 + * @param id 主键值 + */ + AttendanceLeaveRulesVo detail(String id); + + /** + * 删除 + * @param id 主键值 + */ + void delete(String id); + + /** + * 修改状态 + * @param attendanceLeaveRulesDto 修改参数 + */ + void updateState(AttendanceLeaveRulesDto attendanceLeaveRulesDto); + + /** + * 获取用户请假列表 + * @return 列表 + */ + List getUserLeaveList(List leaveTypes); + + + /** + * 2.0自动授予请假余额 + * @param tenantId 租户ID + */ + void autoGrantBalance(String tenantId); + + /** + * 删除假期类型关联的规则 + * @param id 假期类型Id + */ + void deleteByTypeId(String id); + + /** + * 获取用户请假详情 + * @param vo 假期类型 + * @return 详情 + */ + AttendanceLeaveRulesVo getUserLeaveDetail(AttendanceLeaveType vo,String userId) throws ApproveException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveTypeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveTypeService.java new file mode 100644 index 0000000..b2b4207 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLeaveTypeService.java @@ -0,0 +1,52 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.exception.ApproveException; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; + +import java.util.List; + +/** + * @author panpan + */ +public interface AttendanceLeaveTypeService extends IService { + /** + * 创建或修改假期类型 + * @param attendanceLeaveType 创建参数 + */ + void typeSaveOrUpdate(AttendanceLeaveType attendanceLeaveType); + /** + * 删除假期类型 + * @param id 假期类型 ID + */ + void deleteType(String id); + + /** + * 获取用户请假列表 + * @return 列表 + */ + List getUserLeaveList(); + + /** + * 获取用户请假详情 + * @param id 假期类型 ID + * @return 详情 + */ + AttendanceLeaveRulesVo getUserLeaveDetail(String id,String userId) throws ApproveException; + + /** + * 根据假期类型ID列表查询请假类型(过滤已删除) + * @param holidayIdList 假期类型ID列表 + * @return 请假类型列表 + */ + List getAttendanceLeaveTypes(List holidayIdList); + + /** + * 根据假期类型ID列表查询请假类型(包含已删除,供考勤结果下拉等场景) + * + * @param holidayIdList 假期类型ID列表,为空时查询全部 + * @return 请假类型列表(含已删除) + */ + List getAttendanceLeaveTypesIncludeDeleted(List holidayIdList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingConfigService.java new file mode 100644 index 0000000..46a67a3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingConfigService.java @@ -0,0 +1,27 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingConfig; + +/** + * 划线排班配置Service + * + * @author ahua + * @version 2.1 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2025-12-30 + */ +public interface AttendanceLineSchedulingConfigService extends SuperService { + + /** + * 根据考勤组ID查询划线排班配置 + * + * @param groupId 考勤组ID + * @return 划线排班配置 + */ + FtbAttendanceLineSchedulingConfig getByGroupId(String groupId); + + void noticeLineScheduling(String tenantId); + + boolean saveOrUpdateLineSchedulingConfig(FtbAttendanceLineSchedulingConfig config); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingPayrollHoursService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingPayrollHoursService.java new file mode 100644 index 0000000..28ce0dc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLineSchedulingPayrollHoursService.java @@ -0,0 +1,98 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingPayrollHours; + +import java.util.Date; +import java.util.List; + +/** + * 划线排班计薪工时Service + * + * @author jnpf + * @since 2026-02-27 + */ +public interface AttendanceLineSchedulingPayrollHoursService extends SuperService { + + /** + * 根据用户ID、考勤组ID和日期范围查询计薪工时列表 + * + * @param userId 用户ID + * @param groupId 考勤组ID + * @param startDay 开始日期 + * @param endDay 结束日期 + * @return 计薪工时列表 + */ + List listByUserIdAndGroupId(String userId, String groupId, Date startDay, Date endDay); + + /** + * 根据用户ID列表和日期范围查询计薪工时列表 + * + * @param userIds 用户ID列表 + * @param groupId 考勤组ID + * @param startDay 开始日期 + * @param endDay 结束日期 + * @return 计薪工时列表 + */ + List listByUserIdsAndGroupId(List userIds, String groupId, Date startDay, Date endDay); + + /** + * 根据日期范围查询计薪工时列表 + * + * @param groupId 考勤组ID + * @param startDay 开始日期 + * @param endDay 结束日期 + * @return 计薪工时列表 + */ + List listByGroupId(String groupId, Date startDay, Date endDay); + + /** + * 批量保存或更新划线排班计薪工时 + * + * @param payrollHoursList 计薪工时列表 + * @return 是否成功 + */ + boolean saveOrUpdateBatch(List payrollHoursList); + + /** + * 根据用户ID、考勤组ID和日期删除计薪工时 + * + * @param userId 用户ID + * @param groupId 考勤组ID + * @param day 日期 + * @return 是否成功 + */ + boolean deleteByUserIdAndGroupId(String userId, String groupId, Date day); + + /** + * 根据考勤组ID和日期范围删除计薪工时 + * + * @param groupId 考勤组ID + * @param startDay 开始日期 + * @param endDay 结束日期 + * @return 是否成功 + */ + boolean deleteByGroupId(String groupId, Date startDay, Date endDay); + + FtbAttendanceLineSchedulingPayrollHours listByUserIdsAndDays(String userId, String groupId, Date day); + + /** + * 根据用户集合、考勤组ID和时间集合查询计薪工时 + * + * @param userIds 用户ID集合 + * @param groupId 考勤组ID + * @param days 日期集合 + * @return 计薪工时列表 + */ + List listByUserIdsAndDays(List userIds, String groupId, List days); + + List listByUserIdsAndDays(List userIds, List groupIds, List days); + + /** + * 保存划线排班计薪工时集合(先清除后添加) + * + * @param payrollHoursList 计薪工时集合 + * @return 是否成功 + */ + boolean savePayrollHoursList(List payrollHoursList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLocationSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLocationSettingService.java new file mode 100644 index 0000000..54abe0a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceLocationSettingService.java @@ -0,0 +1,116 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceLocationSetting; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceLocationSettingDto; +import jnpf.model.attendance.dto.SaveForStoreDto; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.AttendanceLocationSettingVo; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 考勤组-考勤点配置表 服务类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +public interface AttendanceLocationSettingService extends SuperService { + + /** + * 根据考勤组ID和类型查询考勤地点设置列表。 + * + * @param groupId 考勤组ID + * @param type 类型(用于筛选设置) + * @return 考勤地点设置视图对象列表 + */ + List findList(String groupId, Integer type); + + /** + * 根据门店信息保存考勤点数据 + * @param saveForStore 存储门店信息 + */ + void saveForStore(SaveForStoreDto saveForStore); + + /** + * 根据门店id集合删除考勤点信息 + * @param saveForStore 存储门店信息 + */ + void delForStore(SaveForStoreDto saveForStore); + + /** + * 获取当前考勤组考勤点关联门店id + * @param groupId 考勤组ID + * @return 门店id集合 + */ + List getStoreIds(String groupId); + + /** + * 保存考勤地点设置信息。 + * + * @param dto 考勤地点设置数据传输对象 + * @throws HandleException 处理异常 + */ + void save(AttendanceLocationSettingDto dto) throws HandleException; + + /** + * 将单个用户添加到设备上。 + * + * @param groupId 考勤组ID + * @param userId 用户ID + */ + void sendSingleUserToMachine(String groupId, String userId); + + /** + * 从设备上删除单个用户。 + * + * @param groupId 考勤组ID + * @param userId 用户ID + */ + void deleteUserToMachine(String groupId, String userId); + + /** + * 初始化考勤地点设置。 + * + * @param groupId 考勤组ID + */ + void initLocationSetting(String groupId); + + /** + * 获取启用的考勤地点设置信息。 + * + * @param groupIds 考勤组ID列表 + * @return 启用的考勤地点设置映射 + */ + Map> getEnableLocationSetting(List groupIds); + + /** + * 获取启用的考勤组信息。 + * + * @param groupId 考勤组ID + * @return 启用的考勤组视图对象列表 + */ + List getEnableGroup(String groupId); + + /** + * 删除指定的考勤地点设置信息。 + * + * @param id 考勤地点设置ID + */ + void del(String id); + + /** + * 更改考勤地点设置的启用状态。 + * + * @param groupId 考勤组ID + * @param type 类型(用于区分设置类型) + * @param enable 启用状态(0禁用,1启用) + */ + void changeStatus(String groupId, Integer type, Integer enable); + + void hisLocationSetting(Map group2orgMap, Map org2GroupMap); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineManageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineManageService.java new file mode 100644 index 0000000..07f385b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineManageService.java @@ -0,0 +1,102 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.dto.PageDto; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.MachineDto; +import jnpf.model.attendance.dto.MachineQueryDto; +import jnpf.model.attendance.dto.MachineUpdateDto; +import jnpf.model.attendance.vo.AttendanceMachineManageVo; +import jnpf.model.attendance.vo.MachineScopeVo; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import jnpf.model.attendance.vo.attendance.GroupUserMiniVo; +import jnpf.model.attendance.vo.attendance.LogMiniVo; + +import java.util.List; + +/** + * 考勤机管理服务 + * + * @author yanwenfu + * @create 2024-09-10 + */ +public interface AttendanceMachineManageService { + + /** + * 考勤机管理 - 考勤组列表 + * @param machineId 考勤机id + * @return java.util.List + */ + List getGroupList(String machineId); + + /** + * 考勤机管理 - 考勤机列表 + * @param queryDto 查询条件 + * @return jnpf.base.vo.PageListVO + */ + PageListVO getMachineList(MachineQueryDto queryDto); + + /** + * 移除考勤机关联的考勤组 + * + * @param groupIds 需要移除关联的考勤组 + * @param moveTo 移动到哪个考勤组 + * @param userIds 需要移动的成员 + */ + void removeMachineRelation(List userIds, String moveTo, List groupIds); + + /** + * 考勤机管理 - 添加考勤机 + * @param machineDto 考勤机信息 + */ + void addMachine(MachineDto machineDto) throws HandleException; + + /** + * 考勤机管理 - 编辑考勤机 + * @param id 考勤机id + * @param machineUpdateDto 考勤机信息 + */ + void updateMachine(String id, MachineUpdateDto machineUpdateDto) throws HandleException; + + /** + * 考勤机管理 - 移除 + * @param id 考勤机id + */ + void deleteMachine(String id) throws HandleException; + + /** + * 考勤机管理 - 考勤组成员列表/考勤变更人员查询 + * @param groupId 考勤组id + * @return java.util.List + */ + List getGroupUserList(String groupId); + + /** + * 考勤机管理 - 人脸库 + * @param id 考勤机id + * @return java.util.List + */ + List getMachineMemberList(String id); + + /** + * 查询考勤机打卡记录(查往前3个月的数据) + * @param id 考勤机id + * @param pageQuery 分页 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getLogList(String id, PageDto pageQuery); + + /** + * 同步考勤机成员 + * @param id 考勤机id + */ + List syncMachineMemberData(String id); + + /** + * 考勤机管理 - 查询考勤机更新信息 + * @param id 考勤机id + * @return jnpf.model.attendance.vo.MachineScopeVo + */ + MachineScopeVo getUpdateDetail(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineSyncService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineSyncService.java new file mode 100644 index 0000000..d32275a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceMachineSyncService.java @@ -0,0 +1,13 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceMachineSync; + +/** + * 考勤机同步服务 + * + * @author yanwenfu + * @create 2024-10-24 + */ +public interface AttendanceMachineSyncService extends SuperService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceNoticeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceNoticeService.java new file mode 100644 index 0000000..1009871 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceNoticeService.java @@ -0,0 +1,50 @@ +package jnpf.attendance.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.attendance.AttendanceNoticeEntity; +import jnpf.model.attendance.dto.NoticeContentInfoDto; +import jnpf.model.attendance.dto.NoticeSaveDto; +import jnpf.model.attendance.vo.NoticeConfirmListVo; +import jnpf.model.attendance.vo.NoticeContentInfoVo; + +/** + * 考勤消息通知 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-08-08 10:49:41 + */ +public interface AttendanceNoticeService extends IService { + /** + * 获取内容详情 + * + * @param req 获取小修内容详情的请求数据传输对象 + * @return 返回通知内容信息的值对象 + */ + NoticeContentInfoVo getContentInfo(NoticeContentInfoDto req); + + /** + * 通知记录存储 + * + * @param noticeSaveDto 通知保存的数据传输对象,包含需要保存的通知信息 + */ + void noticeSave(NoticeSaveDto noticeSaveDto); + + /** + * 消息确认 + * + * @param id 需要进行消息确认的通知ID + */ + void noticeConfirm(String id); + + /** + * 获取消息确认列表 + * + * @param id 用于获取确认列表的通知ID + * @return 返回通知确认列表的值对象 + */ + NoticeConfirmListVo getNoticeConfirmList(String id); + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendancePermissionDictService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendancePermissionDictService.java new file mode 100644 index 0000000..2517a8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendancePermissionDictService.java @@ -0,0 +1,39 @@ +package jnpf.attendance.service; + +import jnpf.entity.PermissionDict; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.SavePermissionDto; +import jnpf.model.attendance.vo.PermissionDictVo; + +import java.util.List; +import java.util.Map; + +public interface AttendancePermissionDictService { + + /** + * 保存字典信息 + * @param savePermissionDto 保存参数 + */ + void save(SavePermissionDto savePermissionDto) throws HandleException; + + /** + * 查询权限字典列表 + * @param moduleType 模块类型 1.考勤 + * @return List + */ + List queryPermissionDictList(Integer moduleType); + + /** + * 删除权限字典 + * @param id 指定Id + */ + void delete(String id); + + /** + * 权限字典详情 + * @param id 指定Id + * @return PermissionDict + */ + PermissionDict detail(String id); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateItemService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateItemService.java new file mode 100644 index 0000000..d7bcca4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateItemService.java @@ -0,0 +1,16 @@ +package jnpf.attendance.service; + +import jnpf.entity.attendance.AttendanceQuickTemplateItemEntity; +import jnpf.base.service.SuperService; + +/** + *

+ * 快速模板-单天模板 服务类 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +public interface AttendanceQuickTemplateItemService extends SuperService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateService.java new file mode 100644 index 0000000..4d43ff0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceQuickTemplateService.java @@ -0,0 +1,63 @@ +package jnpf.attendance.service; + +import jnpf.entity.attendance.AttendanceQuickTemplateEntity; +import jnpf.base.service.SuperService; +import jnpf.model.attendance.dto.QuickTemDto; +import jnpf.model.attendance.dto.QuickTemplateDto; +import jnpf.model.attendance.vo.AttendanceQuickTemplateVo; +import jnpf.model.attendance.vo.attendance.QuickTemVo; + +import java.util.List; + +/** + *

+ * 考勤配置-快速模板 服务类 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +public interface AttendanceQuickTemplateService extends SuperService { + + /** + * 根据考勤组ID查询快速模板列表。 + * + * @param groupId 考勤组ID + * @return 快速模板视图对象列表 + */ + List findList(String groupId); + + /** + * 根据快速模板ID查询单个快速模板。 + * + * @param templateId 快速模板ID + * @return 快速模板视图对象 + */ + AttendanceQuickTemplateVo findOne(String templateId); + + /** + * 保存快速模板信息。 + * + * @param dto 快速模板数据传输对象 + */ + void save(QuickTemplateDto dto); + + /** + * 删除指定的快速模板信息。 + * + * @param id 快速模板ID + */ + void del(String id); + + /** + * 新版考勤快速排班模板 + * @param dto 快速排班信息 + */ + void saveOrUpdateNew(QuickTemDto dto); + + /** + * 新版考勤快速排班列表 + * @param groupId 考勤组Id + */ + List findNewList(String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceRepairService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceRepairService.java new file mode 100644 index 0000000..3f19162 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceRepairService.java @@ -0,0 +1,17 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceRepair; + +/** + * 补卡次数服务 + * + * @author yanwenfu + * @create 2024-07-03 + */ +public interface AttendanceRepairService extends SuperService { + + AttendanceRepair getAttendanceRepair(String userId, String groupId); + + AttendanceRepair getAttendanceRepairByDate(String applyDate, String userId, String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSealSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSealSettingService.java new file mode 100644 index 0000000..cc38672 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSealSettingService.java @@ -0,0 +1,30 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceSealSetting; +import jnpf.model.attendance.dto.MonthAutoSealSettingDto; +import jnpf.model.attendance.vo.attendance.MonthAutoSealSettingVo; + +public interface AttendanceSealSettingService extends SuperService { + /** + * 自动封账设置详情 + * + * @return 自动封账设置详情 + */ + MonthAutoSealSettingVo getAutoSealSettingInfo(); + + /** + * 自动封账设置 + * + * @param dto 自动封账设置参数 + * @return 自动封账设置结果 + */ + Boolean autoSealSetting(MonthAutoSealSettingDto dto); + + /** + * 考勤封账-自动封账定时器 + * + * @return 自动封账结果 + */ + Boolean autoSealTimer(String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftNameSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftNameSettingService.java new file mode 100644 index 0000000..bf515be --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftNameSettingService.java @@ -0,0 +1,82 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceShiftNameEntity; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.FixedClassGroupDto; +import jnpf.model.attendance.dto.ShiftNameDto; +import jnpf.model.attendance.dto.ShiftNameQueryDto; +import jnpf.model.attendance.vo.AttendanceShiftSettingPeriodVo; +import jnpf.model.attendance.vo.SecondmentDateVo; +import jnpf.model.attendance.vo.attendance.FixedClassShiftVo; +import jnpf.model.attendance.vo.attendance.ShiftNameListVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @Author huanglinpan + * @Date 2024/5/9 10:06 + * @Version 1.0 (版本号) + */ +public interface AttendanceShiftNameSettingService extends SuperService { + /** + * 新增/修改班次名称 + * @param dto 班次名称信息 + */ + ActionResult save(ShiftNameDto dto); + + Boolean fixedPeriodSingleConflict(Date date, List list, AttendanceShiftSettingPeriodVo period); + + /** + * 起停班次 + * @param shiftNameId 班次名称Id + * @param enable 状态(1启用 2停用) + * @param updateShift 是否修改之前班次(1 : 保持之前班次 2:修改之前的班次) + */ + ActionResult changeStatus(String shiftNameId, Integer enable, Integer updateShift); + + /** + * 删除班次 + * @param shiftNameId 班次Id + * @return 删除结果 + */ + ActionResult delete(String shiftNameId); + + /** + * 获取班次详情 + * @param shiftNameId 班次Id + * @return 详情 + */ + ShiftNameVo getDetail(String shiftNameId); + + /** + * 获取班次详情 + * @param shiftIds 班次时段Id + * @return 详情 + */ + Map getDetailList(List shiftIds); + + /** + * 获取固定排班班次详情 + * @param groupId 考勤组 + */ + List getFixedClass(String groupId); + + /** + * 编辑考勤组固定排班 + * @param dto 固定排班对象 + */ + ActionResult saveOrUpdateFixedClass(FixedClassGroupDto dto); + + /** + * 考勤组班次列表 + * @param queryDto 考勤组查询信息 + * @return 列表 + */ + PageInfo getList(ShiftNameQueryDto queryDto) throws HandleException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingPeriodService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingPeriodService.java new file mode 100644 index 0000000..5d44aa2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingPeriodService.java @@ -0,0 +1,16 @@ +package jnpf.attendance.service; + +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.base.service.SuperService; + +/** + *

+ * 考勤组-考勤配置-时段 服务类 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +public interface AttendanceShiftSettingPeriodService extends SuperService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingService.java new file mode 100644 index 0000000..ab09b46 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceShiftSettingService.java @@ -0,0 +1,112 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceShiftSettingEntity; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceShiftDto; +import jnpf.model.attendance.vo.AttendanceShiftSettingVo; +import jnpf.model.attendance.vo.ShiftPeriodVo; + +import java.util.List; +import java.util.Map; + +/** + *

+ * 考勤组配置-考勤配置 服务类 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +public interface AttendanceShiftSettingService extends SuperService { + + /** + * 根据考勤组ID和考勤组视图列表获取启用的班次设置信息。 + * + * @param groupIds 考勤组ID列表 + * @param attendanceGroupVos 考勤组视图对象列表 + * @return 启用的班次设置信息映射 + */ + Map getEnableShiftSetting(List groupIds, List attendanceGroupVos); + + /** + * 初始化班次设置。 + * + * @param groupId 考勤组ID + */ + void initShiftSetting(String groupId); + + /** + * 根据考勤组ID获取所有班次设置。 + * + * @param groupId 考勤组ID + * @return 班次设置视图对象 + */ + AttendanceShiftSettingVo findAllByGroupId(String groupId); + + /** + * 根据考勤组ID获取单个班次设置。 + * + * @param groupId 考勤组ID + * @return 班次设置视图对象 + */ + AttendanceShiftSettingVo findByGroupId(String groupId); + + /** + * 保存班次设置信息。 + * + * @param dto 班次设置数据传输对象 + * @throws HandleException 处理异常 + */ + void save(AttendanceShiftDto dto) throws HandleException; + + /** + * 更改系统类型。 + * + * @param groupId 考勤组ID + * @param systemType 系统类型 + */ + void changeSystemType(String groupId, Integer systemType); + + /** + * 删除指定的班次周期。 + * + * @param periodId 班次周期ID + */ + void delPeriod(String periodId); + + /** + * 更改班次设置的启用状态。 + * + * @param groupId 考勤组ID + * @param enable 启用状态(0禁用,1启用) + * @param byGroupId 班次设置视图对象 + */ + void changeStatus(String groupId, Integer enable, AttendanceShiftSettingVo byGroupId); + + /** + * 根据考勤组ID获取班次周期列表。 + * + * @param groupId 考勤组ID + * @return 班次周期视图对象列表 + */ + List periodList(String groupId); + + /** + * 智能排班专用:便于区分 §6.1(未取得班次定义)与 §6.2(已取得定义但班次列表为空)。 + * + * @return {@code null} — 未取得该考勤组启用班次设置(与「占位空列表」不同);{@link Collections#emptyList()} — + * 已取得设置但未配置班次周期;否则为周期列表。 + */ + List periodListForIntelligentSchedule(String groupId); + + /** + * 自我排班的班次列表 + * + * @return 班次周期视图对象列表 + */ + List periodListForSelfScheduling(); + + void hisShiftSetting(Map group2orgMap, Map org2GroupMap); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSuperAdminService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSuperAdminService.java new file mode 100644 index 0000000..aedf5ce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceSuperAdminService.java @@ -0,0 +1,176 @@ +package jnpf.attendance.service; + +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.BatchSaveGroupAdmin; +import jnpf.model.attendance.dto.GroupFilterDto; +import jnpf.model.attendance.dto.SaveGroupAdmin; +import jnpf.model.attendance.dto.SaveSuperAdminDto; +import jnpf.model.attendance.vo.AttendanceManagerDetailVo; +import jnpf.model.attendance.vo.AttendanceUserVo; +import jnpf.model.attendance.vo.CurUserPermissionVo; +import jnpf.model.attendance.vo.permission.ActionPermissionVo; +import jnpf.model.attendance.vo.permission.ApprovalSettingVo; +import jnpf.model.attendance.vo.permission.AttendanceTeamSetVo; +import jnpf.model.attendance.vo.permission.app.ManagerPermissionVo; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; + +import java.util.List; +import java.util.Map; + +public interface AttendanceSuperAdminService { + + /** + * 添加考勤组考勤管理员 + * @param saveSuperAdminDto 保存信息 + */ + void add(SaveSuperAdminDto saveSuperAdminDto) throws HandleException; + + /** + * 删除超级管理员 + * @param userIds 用户id + */ + void delete(List userIds); + + /** + * 获取考勤组超级管理员列表 + * @return List + */ + List querySuperAdmin(String name); + + /** + * 保存考勤组管理员 + * @param saveGroupAdmin 保存信息 + */ + void addGroupAdmin(SaveGroupAdmin saveGroupAdmin) throws Exception; + + /** + * 批量添加考勤组管理员 + * @param batchSaveGroupAdmin 保存信息 + */ + void batchAddGroupAdmin(BatchSaveGroupAdmin batchSaveGroupAdmin); + + /** + * 修改考勤组管理员权限 + * @param saveGroupAdmin 保存信息 + */ + void updateGroupAdmin(SaveGroupAdmin saveGroupAdmin); + + /** + * 删除考勤组管理员 + */ + void deleteGroupAdmin(SaveGroupAdmin saveGroupAdmin); + + /** + * 查看考勤组下管理员列表 + * @param groupId 考勤组 + * @return List + */ + Map listGroupAdmin(String groupId); + + /** + * 获取管理员,包含当前组管理员、上级子组权限管理员、超级管理员,其中key为 -1时超级管理员集合 + * @param groupIds 考勤组id集合 + * @return Map> key为 groupId value为管理员userId集合 + */ + Map> queryUserForCurrAndUpChildAndSuper(List groupIds); + + /** + * 根据指定条件查询权限信息 + * 该方法通过接收群组ID、父权限代码和子权限代码列表作为参数, + * 查询并返回一个映射,其中键为群组ID,值为该群组下匹配指定权限代码的权限列表 + * + * @param groupIds 群组ID列表,用于指定需要查询权限的群组 + * @param parentCodes 父权限代码列表,用于指定需要查询的父权限范围 + * @param childCodes 子权限代码列表,用于指定需要查询的子权限范围 + * @return 返回一个映射,每个键值对表示一个群组及其对应的权限列表 + */ + Map> queryPermissionBySpecify(List groupIds, List parentCodes, List childCodes); + + /** + * 获取当前登录用户权限 + * @param groupId 考勤组id + * @return CurUserPermissionVo + */ + CurUserPermissionVo getByUserId(String groupId); + + /** + * 是否是考勤超级管理员 + * @param userId 用户id + * @return Boolean + */ + Boolean isSuperAdmin(String userId); + + /** + * 考勤组管理员详情 + * @param groupId 考勤组id + * @param userId 用户id + * @return AttendanceGroupVo + */ + AttendanceManagerDetailVo adminDetail(String groupId, String userId); + + /** + * 获取当前用户权限 + * @param userId 用户id + * @param levelCodeList 考勤组层级编码 + * @param permissionName 权限名称 + * @return CurUserPermissionVo + */ + CurUserPermissionVo queryPermissionByUserId(String userId, List levelCodeList, String permissionName); + + /** + * 是否有查看权限 + * @param groupId 考勤组id + * @return Boolean + */ + Boolean isViewPermission(String groupId); + + /** + * 获取操作权限 + * @param groupId 考勤组id + * @return ActionPermissionVo + */ + ActionPermissionVo getActionPermission(String groupId); + + /** + * 是否是考勤组超级管理员||系统超级管理员 + * @return Boolean + */ + Boolean isGlobalSetting(); + + /** + * 是否是考勤组管理员 + * @return ManagerPermissionVo + */ + ManagerPermissionVo isManager(); + + /** + * 获取考勤组审批设置 + * @param groupId 考勤组id + * @param type 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 + * @return ApprovalSettingVo + */ + ApprovalSettingVo getApprovalSettingInfo(String groupId, Integer type); + + /** + * 根据用户id集合查询所属的权限-查询所有权限 + * @param userIds 用户id集合 + * @return FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO + */ + List queryPermissionListByUserIds(List userIds); + + /** + * 获取考勤组设置 + * + * @param dto 考勤组id + * @return AttendanceTeamSetVo + */ + AttendanceTeamSetVo getTeamSet(GroupFilterDto dto); + + /** + * 设置月报通知 + * + * @param dto 考勤组id + * @return Boolean + */ + Boolean setMonthNotice(GroupFilterDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceRecordService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceRecordService.java new file mode 100644 index 0000000..03c9dbb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceRecordService.java @@ -0,0 +1,54 @@ +package jnpf.attendance.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.attendance.AttendanceBalanceRecordEntity; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayBalance; +import jnpf.model.attendance.vo.attendance.LeaveConsumptionDetailVo; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +/** + * @author panpan + * @description 用户余额获取记录表 Service接口 + * @version V2.0 + */ +public interface AttendanceUserBalanceRecordService extends IService { + + /** + * 回滚用户余额记录 + * @param balanceId 假日类型Id(ftb_attendance_leave_rules)(存休时为空) + * @param needRetirementLeave 需回滚的数据 + * @param leaveName 请假类型名称 + * @param isPublicHoliday 是否是公休触发--公休已单独处理日志这边不记录对应的日志 + */ + void rollbackUserBalanceRecord(List needRetirementLeave,String balanceId,String leaveName,boolean isPublicHoliday); + + /** + * 需要转存劵的用户余额记录 (存休不能触发) + * @param ruleIds 假日类型Id(ftb_attendance_leave_rules) + * @param userIds 用户id + */ + void needRetirementBalance(List ruleIds,List userIds); + + /** + * 请假模拟消费 + * @param userLeaveDetail 假日类型对象 + * @param userId 用户id + * @param balance 余额 + * @return 未抵扣的余额 + */ + LeaveConsumptionDetailVo leaveSimulationConsumption(String userId , BigDecimal balance, AttendanceLeaveRulesVo userLeaveDetail) ; + + /** + * 请假消费劵 + * @param userLeaveDetail 假日类型(ftb_attendance_leave_rules)(存休时为空) + * @param userId 用户id + * @param balance 余额 + * @return 未抵扣的余额 + */ + LeaveConsumptionDetailVo leaveConsumption(String userId , BigDecimal balance, AttendanceLeaveRulesVo userLeaveDetail,Date startTime, Date endTime,String applyId) ; + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceService.java new file mode 100644 index 0000000..58eaec2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserBalanceService.java @@ -0,0 +1,86 @@ +package jnpf.attendance.service; + + +import jnpf.entity.attendance.AttendanceLeaveRules; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceLeaveRulesDto; +import jnpf.model.attendance.dto.AttendanceUserBalanceDto; +import jnpf.model.attendance.dto.AttendanceUserBalanceListQueryDto; +import jnpf.model.attendance.vo.attendance.*; + +import java.math.BigDecimal; +import java.util.List; + +/** + * @author panpan + */ +public interface AttendanceUserBalanceService { + /** + * 列表 + * + * @param queryDto 查询条件 + * @return 列表 + */ + AttendanceUserBalanceListVo list(AttendanceUserBalanceListQueryDto queryDto) throws HandleException; + + + /** + * 编辑、批量编辑余额 + * + * @param userBalanceDto 余额 + */ + void updateUserBalance(AttendanceUserBalanceDto userBalanceDto); + + /** + * 获取余额详情 + * + * @param userBalanceDto 详情 + * @return 详情 + */ + AttendanceUserBalanceVo getDetail(AttendanceUserBalanceDto userBalanceDto); + + /** + * 获取标题 + * + * @param userBalanceDto 标题 + * @return 标题 + */ + AttendanceUserTitleVo getTitle(AttendanceUserBalanceDto userBalanceDto); + + + /** + * 修改用户公休余额(公休转存休使用)日志 + * + * @param needRetirementLeave 需转存休 + */ + void addUserBalances(List needRetirementLeave); + + /** + * 撤销用户公休余额(公休转存休使用) 修改余额及退卷记录 + * + * @param needRetirementLeave 需撤销休 + */ + void rollbackUserBalance(List needRetirementLeave); + + /** + * 加班触发 + * + * @param overtimeInfoList 加班信息 + */ + void overTime(List overtimeInfoList, String tenantId); + + /** + * 获取指定类型的余额 + * + * @param balanceId LeaveBalanceId 类型Id 查询存休时 为null + * @param userId 用户Id + * @param isRetirementLeave 是否统计存休 LeaveBalanceId不为空且对应的假期规则能使用存休抵扣时才为 true + * @return 余额 + */ + BigDecimal getTotalBalance(String balanceId, String userId, boolean isRetirementLeave); + + + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserFaceService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserFaceService.java new file mode 100644 index 0000000..0cd785e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserFaceService.java @@ -0,0 +1,47 @@ +package jnpf.attendance.service; + +import jnpf.model.attendance.dto.UserFaceDto; +import jnpf.model.attendance.dto.MachineDealDto; +import jnpf.model.attendance.vo.UserFaceVo; + +import java.util.List; + +/** + * 人脸服务 + * + * @author yanwenfu + * @create 2024-04-09 + */ +public interface AttendanceUserFaceService { + + /** + * 查询用户人脸 + * @param userId 用户id + * @return jnpf.model.attendance.vo.UserFaceVo + */ + UserFaceVo getUserFace(String userId); + + /** + * 新增用户人脸 + * @param userFaceDto 人脸dto + */ + void addUserFace(UserFaceDto userFaceDto); + + /** + * 编辑用户人脸 + * @param userFaceDto 人脸dto + */ + void updateUserFace(UserFaceDto userFaceDto); + + /** + * 删除用户人脸 + * @param userId 用户id + */ + void deleteUserFace(String userId); + + /** + * 考勤机处理 + * @param machineDealDto 考勤机处理参数 + */ + List machineDeal(MachineDealDto machineDealDto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserService.java new file mode 100644 index 0000000..efa9ad9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserService.java @@ -0,0 +1,305 @@ +package jnpf.attendance.service; + +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.base.service.SuperService; +import jnpf.entity.AttendanceGroupUser; +import jnpf.exception.HandleException; +import jnpf.message.model.permission.PermissionRelationOrganizeUserListDTO; +import jnpf.model.attendance.dto.DayStatisticsPageListDto; +import jnpf.model.attendance.dto.GroupUserQueryDto; +import jnpf.model.attendance.dto.JoinUserDto; +import jnpf.model.attendance.dto.UserSortModel; +import jnpf.model.attendance.model.OrganizeUserConsumerDTO; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.attendance.vo.GroupUserLineScheduleVo; +import jnpf.model.attendance.vo.SecondmentDateVo; +import jnpf.model.attendance.vo.attendance.ClockInExportVo; +import jnpf.model.attendance.vo.attendance.JoinGroupVo; +import jnpf.model.common.DateRangeDto; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import org.apache.commons.lang3.tuple.MutablePair; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface AttendanceUserService extends SuperService { + + /** + * 根据考勤组查询成员列表 + * + * @param groupId 考勤组ID + * @param name 用户名称 + * @param type 用户类型 + * @param userIds 用户ID列表 + * @return 用户列表 + */ + List queryUsersByGroupId(String groupId, String name, Integer type, List userIds, Date start,Date end); + + List getUserIds(List organizeList, List userIdList, Date start, Date end, Integer scopeOfAdaptation); + List getUserIdsAndGroupIds(List organizeList, List userIdList, Date start, Date end, Integer scopeOfAdaptation); + + /** + * 对用户排序模型进行排序 + * 该方法用于根据特定的排序规则对用户排序模型对象进行排序操作 + * 它改变用户排序模型对象的顺序,以便后续处理或显示时符合预期的排序逻辑 + * + * @param userSortModel 用户排序模型对象,包含需要被排序的数据和排序规则 + * 这个参数不应为null,否则排序操作将抛出异常 + */ + void sort(UserSortModel userSortModel); + + /** + * 根据用户和考勤组ID列表查询关联信息 + * + * @param groupIds 考勤组ID列表 + * @return 关联信息列表 + */ + List queryByUsersGroupIds(List groupIds); + + /** + * 根据所有用户和考勤组ID列表查询关联信息 + * + * @param groupIds 考勤组ID列表 + * @return 关联信息列表 + */ + List queryByAllUsersGroupIds(List groupIds); + + /** + * 查询所有用户和考勤组的关联信息 + * + * @return 关联信息列表 + */ + List queryAll(); + + /** + * 查询所有非外派人员的考勤组用户信息 + * 此方法用于获取系统中所有不属于外派人员的考勤组用户列表它没有接受任何参数, + * 表示将返回数据库中符合条件的所有用户信息 + * + * @return 非外派人员的考勤组用户列表如果列表为空,表示没有符合条件的用户 + */ + List queryAllForNotSecondment(); + + /** + * 根据用户ID列表查询用户和考勤组的关联信息 + * + * @param userIds 用户ID列表 + * @return 关联信息列表 + */ + List queryByUsersIds(List userIds); + + /** + * 根据用户ID列表查询所有用户的考勤组关联信息 + * + * @param userIds 用户ID列表 + * @return 关联信息列表 + */ + List queryAllByUsersIds(List userIds); + + /** + * 根据时间范围、用户ID列表和考勤组ID列表查询用户和考勤组的关联信息 + * + * @param start 开始时间 + * @param end 结束时间 + * @param userIds 用户ID列表 + * @param groupIds 考勤组ID列表 + * @return 关联信息列表 + */ + List queryByUsersAndGroup(Date start, Date end, List userIds, List groupIds); + + /** + * 根据时间范围、用户ID列表和考勤组ID列表查询用户和考勤组的关联信息,并过滤借调人员 + * + * @param start 开始时间 + * @param end 结束时间 + * @param userIds 用户ID列表 + * @param groupIds 考勤组ID列表 + * @return 关联信息列表 + */ + List queryByUsersAndGroupFilterSecondment(Date start, Date end, List userIds, List groupIds); + + List queryByUsersAndGroupFilterSecondment(Date start, Date end, List userIds, List groupIds, Boolean isContainsDeleteGroup); + + /** + * 根据用户和小组过滤条件查询特定时间段内的出勤记录 + * 此方法主要用于查询属于特定用户组的用户在指定时间范围内的出勤信息, + * 并可以根据需要选择是否包括已删除的小组中的用户 + * + * @param start 开始日期,查询范围的起始时间 + * @param end 结束日期,查询范围的结束时间 + * @param userIds 用户ID列表,用于指定需要查询的用户 + * @param groupIds 小组ID列表,用于指定用户所属的小组 + * @param isContainsDeleteGroup 是否包含已删除小组的标志, + * 如果为true,则结果中包含属于已删除小组的用户; + * 如果为false,则结果中不包含属于已删除小组的用户 + * @return 返回符合查询条件的出勤记录列表 + */ + List getAttendanceGroupUsersOfSecondment(Date start, Date end, List userIds, List groupIds, boolean isContainsDeleteGroup); + + List getAttendanceGroupUsers(Date start, Date end, List attendanceGroupUsers); + + /** + * 获取借调人员的考勤组信息 + * + * @param start 开始时间 + * @param end 结束时间 + * @param userIds 用户ID列表 + * @param groupIds 考勤组ID列表 + * @return 考勤组信息列表 + */ + List getAttendanceGroupUsersOfSecondment(Date start, Date end, List userIds, List groupIds); + + /** + * 查询系统用户列表 + * + * @param orgId 组织ID + * @param name 用户名称 + * @return 用户列表 + */ + List querySysUserList(String orgId, String name); + + void orgUpdateHandle(List userList); + + void addUsers(List userList, Boolean isOnboarding); + + void removeUsers(List userList, Boolean isTurnover); + + /** + * 用户加入考勤组 + * + * @param joinUserDto 加入用户DTO + * @throws HandleException 处理异常 + */ + void joinUsers(JoinUserDto joinUserDto) throws HandleException; + + /** + * 批量删除用户以发送通知 + * + * @param joinUserDto 加入用户DTO + */ + void batchDeleteUsersForSendNotice(JoinUserDto joinUserDto); + + /** + * 批量删除考勤组成员 + * + * @param joinUserDto 加入用户DTO + */ + void batchDeleteUsers(JoinUserDto joinUserDto); + + /** + * 检查用户是否已经加入考勤组 + * + * @param userId 用户ID + * @return 考勤组ID列表 + * @throws HandleException 处理异常 + */ + List checkUserGroup(String userId) throws HandleException; + + /** + * 批量获取用户加入考勤组的周期 + * + * @param dateRangeDto 日期范围DTO + * @param groupIds 考勤组ID列表 + * @param userIds 用户ID列表 + * @return 用户周期映射 + * @throws Exception 获取异常 + */ + Map>> batchGetUserCycleList(DateRangeDto dateRangeDto, List groupIds, List userIds) throws Exception; + + /** + * 借调考勤组变动通知 + * + * @param tenantId 租户ID + * @return 处理结果 + */ + Boolean userGroupUpdateBySecondNotice(String tenantId); + + + /** + * 花名册考勤组变更 + * @param groupUpdateByUserDTO 考勤组变更DTO + */ + void groupUpdateByPersonnel(GroupUpdateByUserDTO groupUpdateByUserDTO); + + /** + * 指定考勤组成员列表 + * + * @param id 考勤组Id + * @param isPermissions 是否带权限 + */ + List getGroupUserList(String id, Boolean isPermissions); + + /** + * 获取指定考勤组中当前处于借调的用户原本所属考勤组信息 + * @param groupId 考勤组Id + */ + List getSelfUsersBeLongToGroup(String groupId); + + /** + * 获取指定日期员工所在考勤组集合 + * @param groupUserQueryDto 查询参数DTO + */ + List getUsersDayByGroupIds(GroupUserQueryDto groupUserQueryDto, DayStatisticsPageListDto req); + + /** + * 带权限查询指定考勤组及班组人员信息 + * @param groupId 考勤组ID + * @param workGroupId 班组ID + * @return 用户信息集合 + */ + List getUserBoundVO(String groupId, String workGroupId); + + /** + * 批量获取带权限查询指定考勤组及班组人员信息 + * @param groupIds 考勤组ID集合 + * @param workStatusEnums 用户工作状态枚举集合 + * @return 用户信息集合 + */ + List batchGetUserBoundVO(List groupIds, List workStatusEnums); + + /** + * 获取用户当前所属考勤组 + */ + String getUserNowGroup(); + + /** + * 根据查询条件和分组信息获取签到导出数据列表 + * + * @param groupUserQueryDto 查询条件,包含需要查询的分组用户相关信息 + * @param usersByGroupVos 已按分组查询到的用户数据列表,用于进一步处理和生成签到导出数据 + * @return 返回一个ClockInExportVo对象的列表,包含根据查询条件和分组信息生成的签到导出数据 + */ + List getClockInExportVoList(GroupUserQueryDto groupUserQueryDto, List usersByGroupVos); + + /** + * 检查用户是否已经加入考勤组 + * @param userIds 用户ID列表 + */ + List checkUserJoinGroupToObject(String userIds); + + /** + * 根据群组ID列表获取用户ID列表 + * 本函数旨在通过一组群组ID来收集所有相关的用户ID这些用户ID代表了属于给定群组的所有用户 + * 它主要用于需要了解特定群组包含哪些用户的应用场景中 + * + * @param groupIds 群组ID列表,用于查询相关的用户ID + * @return 包含所有与给定群组相关的用户ID的列表 + */ + List getGroupUserIds(List groupIds); + + List getGroupNowAllUserList(String groupId); + + /** + * 按考勤组查询成员列表,并标识每位成员是否允许划线排班 + *

+ * 成员通过 {@link #getAttendanceGroupUsersOfSecondment(Date, Date, List, List)} 拉取, + * 每位成员是否允许划线排班由考勤组划线排班配置(员工类型/工作性质/岗位/指定成员)综合判定, + * 与排班界面 {@code canLineSchedule} 标识的口径保持一致。 + * + * @param groupId 考勤组ID + * @return 成员列表,包含 userId、userName、positionId、positionName、canLineSchedule + */ + List listLineScheduleUsersByGroupId(String groupId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserSettingService.java new file mode 100644 index 0000000..604c9df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/AttendanceUserSettingService.java @@ -0,0 +1,82 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceAppUserSetting; +import jnpf.model.attendance.dto.AppUserSettingQueryDto; +import jnpf.model.attendance.dto.AttendanceAppUserSettingDto; +import jnpf.model.attendance.vo.UserSettingVo; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/8/8 10:54 + * @Version 1.0 (版本号) + */ +public interface AttendanceUserSettingService extends SuperService { + /** + * 修改设置 + * @param attendanceAppUserSettingDto 修改设置信息 + */ + void saveOrUpdate(AttendanceAppUserSettingDto attendanceAppUserSettingDto); + + /** + * 查询设置列表 + * @param appUserSettingQueryDto 查询信息 + */ + List getList(AppUserSettingQueryDto appUserSettingQueryDto); + + /** + * 查询设置列表 + * @param associationId 关联Id(与类型一起使用,个人时表示用户Id,考勤组时表示考勤组Id) + * @param type 类型 1:个人设置,2:考勤组设置 + * @param code 编码 + */ + List getSettingList(String associationId, Integer type, String code); + + /** + * 查询设置列表 + * @param associationId 关联Id(与类型一起使用,个人时表示用户Id,考勤组时表示考勤组Id) + * @param type 类型 1:个人设置,2:考勤组设置 + * @param code 编码 + */ + List getSettingList(List associationId, Integer type, String code); + + /** + * 查询设置列表 + * @param associationId 关联Id(与类型一起使用,个人时表示用户Id,考勤组时表示考勤组Id) + * @param type 类型 1:个人设置,2:考勤组设置 + * @param code 编码 + */ + List getSettingList(List associationId, Integer type, List code); + + /** + * 校验web设置 + * @param code 编码 + */ + UserSettingVo checkWebSetting(String code); + + /** + * 刷新用户定时提醒缓存 + * @param userId 用户id + * @param value 值 + * @param tenantId 租户id + */ + void refreshRemindRedisCache(String code, String userId, Integer value, String tenantId); + + /** + * 批量保存设置 + * @param attendanceAppUserSettingDto 保存数据 + */ + void saveOrUpdateList(List attendanceAppUserSettingDto); + + /** + * 获取新的用户设置列表 + * 本方法用于根据查询条件获取用户设置的列表信息,以便在界面上展示或者进一步处理 + * + * @param appUserSettingQueryDto 查询条件对象,包含查询用户设置所需的参数 + * @return 返回一个UserSettingVo对象的列表,表示查询到的用户设置信息 + */ + List getListNew(AppUserSettingQueryDto appUserSettingQueryDto); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ClockInResultService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ClockInResultService.java new file mode 100644 index 0000000..9d06337 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ClockInResultService.java @@ -0,0 +1,60 @@ +package jnpf.attendance.service; + +import jnpf.base.UserInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.exception.QueryException; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 打卡服务2.0 + * + * @author yanwenfu + * @create 2025-09-23 + */ +public interface ClockInResultService extends SuperService { + + /** + * 查询今天可以打卡的出勤规则 + * @param today 日期 + * @param userInfo 用户信息 + * @return java.util.List + */ + List getTodayRuleList(Date today, UserInfo userInfo); + + /** + * 查询所有用户今天可以打卡的出勤规则 + * @param today 日期 + * @param userId 用户id(有则查单人, 无则查全部) + * @return java.util.List + */ + Map> getTodayRuleListByUser(Date today, String userId); + + /** + * 查询指定用户的出勤规则[批量] + * @param groupedMap k:userId_targetDate v:当日+前一日的出勤规则 + * @param userGroupMap 用户考勤组map + * @return java.util.Map> + */ + Map> getRuleListByUserDay(Map> groupedMap, Map> userGroupMap); + + /** + * 清除异常的出勤规则 + * @param ruleList 出勤规则列表 + * @param userId 用户id + * @return java.util.List + */ + List clearErrorRule(List ruleList, String userId, Date date, List effectGroupIds); + + /** + * 获取用户绑定的考勤机 + * @param userId 用户id + * @return java.util.List + */ + List getMachineList(String userId, String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/CommonSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/CommonSettingService.java new file mode 100644 index 0000000..a517252 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/CommonSettingService.java @@ -0,0 +1,13 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceCommonSetting; + +/** + * 公共配置服务 + * + * @author yanwenfu + * @create 2025-09-19 + */ +public interface CommonSettingService extends SuperService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/DailyRuleChangeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/DailyRuleChangeService.java new file mode 100644 index 0000000..aafa662 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/DailyRuleChangeService.java @@ -0,0 +1,27 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.DailyRuleChange; + +import java.util.List; + +/** + * 出勤规则变更服务 + * + * @author yanwenfu + * @create 2026-05-20 + */ +public interface DailyRuleChangeService extends SuperService { + + /** + * 出勤规则变更执行 + * @return boolean + */ + boolean dailyRuleChangeExecute(); + + /** + * 批量保存 + * @param list 出勤规则变更list + */ + void saveRecordBatch(List list); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/EnableBalanceService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/EnableBalanceService.java new file mode 100644 index 0000000..d046e55 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/EnableBalanceService.java @@ -0,0 +1,37 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceEnableBalance; +import jnpf.model.attendance.vo.attendance.OvertimeSalaryHoursJsonVo; +import jnpf.model.attendance.vo.attendance.OvertimeSalaryHoursVo; + +import java.util.List; +import java.util.Map; + +/** + * 加班余额记录[不存休,仅记录]服务 + * + * @author yanwenfu + * @create 2025-10-03 + */ +public interface EnableBalanceService extends SuperService { + /** + * 获取加班算薪加班小时数(不计算存休-结算薪酬) + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param userIds 用户ID + * @return 加班算薪加班小时数( + */ + Map getOvertimeSalary(String startDate, String endDate, List userIds); + + /** + * 获取加班算薪加班Json小时数(不计算存休-结算薪酬) + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param userIds 用户ID + * @return 加班算薪加班小时数( + */ + List getOvertimeSalaryJson(String startDate, String endDate, List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/InitializationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/InitializationService.java new file mode 100644 index 0000000..18edd23 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/InitializationService.java @@ -0,0 +1,14 @@ +package jnpf.attendance.service; + +/** + * @Author huanglinpan + * @Date 2024/7/1 9:16 + * @Version 1.0 (版本号) + */ +public interface InitializationService { + /** + * 初始化存休 有效期考勤v1.3版本 + */ + Integer storageRest(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/IsPerfMachineService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/IsPerfMachineService.java new file mode 100644 index 0000000..5bb5cf2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/IsPerfMachineService.java @@ -0,0 +1,36 @@ +package jnpf.attendance.service; + +import java.util.List; +import java.util.Map; + +/** + * KIPS服务 + * + * @author yanwenfu + * @create 2024-04-01 + */ +public interface IsPerfMachineService { + + /** + * 显示欢迎信息的方法 + * 此方法用于向用户展示欢迎信息 + */ + void welcome(); + + /** + * 更新应用配置的方法 + * 此方法接受一个参数映射,用于更新应用的配置信息 + * + * @param params 包含配置参数的映射,其中键是参数名,值是参数值 + */ + void updateApp(Map params); + + /** + * 获取所有在线客户端列表的方法 + * 此方法用于获取当前所有在线客户端的列表 + * + * @return 包含所有在线客户端标识的列表 + */ + List getAllOnlineClient(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/MachineStrategy.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/MachineStrategy.java new file mode 100644 index 0000000..72f9f58 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/MachineStrategy.java @@ -0,0 +1,47 @@ +package jnpf.attendance.service; + +import jnpf.base.UserInfo; +import jnpf.enums.attendance.MachineEnum; +import jnpf.permission.model.user.PartUserInfoVo; + +import java.util.List; + +/** + * 考勤机策略模式 + * + * @author yanwenfu + * @create 2024-04-03 + */ +public interface MachineStrategy { + + /** + * 获取当前上下文相关的机器枚举类型 + * 此方法用于提供当前执行环境或上下文有关的机器信息通过返回一个MachineEnum枚举类型, + * 可以标准化地表示不同的机器或环境类型,以便于在代码的其他部分根据具体的机器类型执行相应的逻辑 + * + * @return MachineEnum 一个枚举类型,表示当前上下文相关的机器类型 + */ + MachineEnum getMachine(); + + /** + * 下发设备到考勤机 + * @param userInfo 当前登录用户 + * @param user 下发用户 + * @param sn 设备码 + */ + void addUserToMachine(UserInfo userInfo, PartUserInfoVo user, String sn); + + /** + * 删除考勤机中的用户 + * @param userInfo 当前登录用户 + * @param userIds 删除用户ids + * @param sn 设备码 + */ + void deleteUserList(UserInfo userInfo, List userIds, String sn); + + /** + * 同步考勤机人脸库 + * @param sn 设备码 + */ + void syncMachineMember(String sn); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/NoticeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/NoticeService.java new file mode 100644 index 0000000..32fdee5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/NoticeService.java @@ -0,0 +1,27 @@ +package jnpf.attendance.service; + + +public interface NoticeService { + /** + * 检查执行流程的前置条件是否满足 + * + * @param dto 包含执行流程所需数据的传输对象 + * @return 如果前置条件满足,则返回true;否则返回false + */ + Boolean preconditions(Execute dto); + + /** + * 保存数据到存储系统或特定介质 + * + * @param dto 包含待保存数据的传输对象 + * @return 返回保存操作的结果标识,通常是一个表示成功或失败的字符串 + */ + String saveData(Execute dto); + + /** + * 推送数据到目标系统或用户 + * + * @param dto 包含推送数据和目标信息的传输对象 + */ + void push(PushData dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleDetailService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleDetailService.java new file mode 100644 index 0000000..b2f057b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleDetailService.java @@ -0,0 +1,13 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceOvertimeRuleDetail; + +/** + * 加班规则明细表服务 + * + * @author yanwenfu + * @create 2025-09-18 + */ +public interface OvertimeRuleDetailService extends SuperService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleService.java new file mode 100644 index 0000000..3a6f992 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/OvertimeRuleService.java @@ -0,0 +1,107 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceOvertimeRule; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.OvertimeRuleDto; +import jnpf.model.attendance.dto.OvertimeRuleQueryDto; +import jnpf.model.attendance.vo.attendance.OvertimeRuleDetailVo; +import jnpf.model.attendance.vo.attendance.OvertimeRulePageVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 加班规则服务 + * + * @author yanwenfu + * @create 2025-09-18 + */ +public interface OvertimeRuleService extends SuperService { + + /** + * 新增加班规则 + * @param overtimeRuleDto 加班规则dto + */ + void addOvertimeRule(OvertimeRuleDto overtimeRuleDto) throws HandleException; + + /** + * 编辑加班规则 + * @param id 加班规则id + * @param overtimeRuleDto 加班规则dto + */ + void updateOvertimeRule(String id, OvertimeRuleDto overtimeRuleDto) throws HandleException; + + /** + * 删除加班规则 + * @param id 加班规则id + */ + void deleteOvertimeRule(String id); + + /** + * 禁用/启用加班规则 + * @param id 加班规则id + */ + void updateEnableStatus(String id) throws HandleException; + + /** + * 查询加班规则详情 + * @param id 加班规则id + * @return jnpf.model.attendance.vo.attendance.OvertimeRuleVo + */ + OvertimeRuleVo getDetail(String id); + + /** + * 查询启用中的规则详情 + * @param ruleId 加班规则id + * @return jnpf.model.attendance.vo.attendance.OvertimeRuleVo + */ + OvertimeRuleVo getActiveDetail(String ruleId); + + /** + * 查询生效中的规则详情 + * 若attendanceType为null 则 effectRuleDetail为null + * 若节假日/公休日/工作日配置未启用, 则获取到的effectRuleDetail为null + * @param userId 用户id + * @param attendanceType 考勤类型 + * @param date 日期 + * @return jnpf.model.attendance.vo.attendance.OvertimeRuleVo + */ + OvertimeRuleVo getEffectDetail(String userId, Integer attendanceType, Date date); + + /** + * 查询考勤组加班规则 + * @param groupId 考勤组id + * @return jnpf.model.attendance.vo.attendance.OvertimeRuleVo + */ + OvertimeRuleVo getGroupOvertimeRule(String groupId); + + /** + * 查询[多用户]生效中的规则详情 + * 若attendanceType为null 则 effectRuleDetail为null + * 若节假日/公休日/工作日配置未启用, 则获取到的effectRuleDetail为null + * @param userIds 用户ids + * @param date 日期 + * @return java.util.Map + */ + Map getEffectDetailByUserList(List userIds, Date date); + + /** + * 根据出勤类型获取节假日/公休日/工作日配置 + * @param rule 规则 + * @param date 日期 + * @param attendanceType 出勤类型 + * @return jnpf.model.attendance.vo.attendance.OvertimeRuleDetailVo + */ + OvertimeRuleDetailVo getEffectWorkDetail(OvertimeRuleVo rule, Date date, Integer attendanceType); + + /** + * 查询加班规则列表(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getPage(OvertimeRuleQueryDto queryDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/PublicHolidayRulesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/PublicHolidayRulesService.java new file mode 100644 index 0000000..8e7634f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/PublicHolidayRulesService.java @@ -0,0 +1,83 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; + +import jnpf.model.attendance.dto.AttendancePublicHolidayRulesDto; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayBalance; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayRulesVo; +import jnpf.model.common.PageDto; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * @author panpan + */ +public interface PublicHolidayRulesService { + + /** + * 公休管理列表 + * @param iText 规则名称模糊查询 非必传 + * @return + */ + PageInfo list(String iText, PageDto pageDto); + + /** + * 新增公休管理 + * @param publicHolidayRulesDto 公休管理 + */ + void add(AttendancePublicHolidayRulesDto publicHolidayRulesDto) throws Exception; + + /** + * 修改公休管理 + * @param publicHolidayRulesDto 公休管理 + */ + void update(AttendancePublicHolidayRulesDto publicHolidayRulesDto) throws Exception; + + /** + * 删除公休管理 + * @param id 公休管理Id + */ + AttendancePublicHolidayRulesVo selectOne(String id); + + /** + * 删除公休管理 + * @param id 公休管理Id + */ + void delete(@Param("id") String id); + + /** + * 修改公休管理状态 + * @param publicHolidayRulesDto 公休管理 + */ + void updateState(AttendancePublicHolidayRulesDto publicHolidayRulesDto); + + /** + * 公休余额查询 固定班不会触发 + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + * @return 公休余额列表 + */ + List getBalanceList(String yearMonth,List userIds) ; + + /** + * 处理公休(封账时调用) 固定班不会触发 + * + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + * @param ratioMap 用户出勤换算比 + */ + void processPublicHoliday(String yearMonth, List userIds, Map ratioMap); + + + /** + * 处理公休(解封时调用) + * @param yearMonth 年月格式yyyy-MM + * @param userIds 用户ID列表 + */ + void rollbackPublicHoliday(String yearMonth,List userIds); + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RV1109MachineService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RV1109MachineService.java new file mode 100644 index 0000000..ea32435 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RV1109MachineService.java @@ -0,0 +1,20 @@ +package jnpf.attendance.service; + +import jnpf.permission.entity.UserEntity; + +/** + * RV1109考勤机服务 + * + * @author yanwenfu + * @create 2024-04-09 + */ +public interface RV1109MachineService { + + /** + * 根据用户id查询用户信息 + * @param userId 用户id + * @param tenantId 租户id + * @return jnpf.permission.entity.UserEntity + */ + UserEntity getUserInfoById(String userId, String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleProcessor.java new file mode 100644 index 0000000..8594b58 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleProcessor.java @@ -0,0 +1,30 @@ +package jnpf.attendance.service; + +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.model.attendance.vo.DailyRuleResultVo; + +import java.util.List; + +public abstract class RuleProcessor { + protected RuleProcessor nextRuleProcessor; + public RuleProcessor setNext(RuleProcessor nextRuleProcessor) { + this.nextRuleProcessor=nextRuleProcessor; + return nextRuleProcessor; + } + + /** + * 获取下一个规则 + * @return 下一个规则 + */ + public RuleProcessor getNext(){ + return nextRuleProcessor; + } + + /** + * 执行规则生成 + * @param hisDailyRules 历史规则 + * @param currDailyRule 当前新增规则 + * @param resultList 异常结果响应 + */ + public abstract void ruleArrangementHandle(List hisDailyRules, List currDailyRule, List resultList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleScopeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleScopeService.java new file mode 100644 index 0000000..0e5e344 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/RuleScopeService.java @@ -0,0 +1,42 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.dto.UserOrgDto; + +import java.util.List; + +/** + * 适配范围服务 + * + * @author yanwenfu + * @create 2025-09-18 + */ +public interface RuleScopeService extends SuperService { + + /** + * 查询用户生效中的规则 + * @param userId 用户id + * @param organizeId 组织id + * @param bizType 业务类型 + * @return java.util.List + */ + List selectUserEffectList(String userId, String organizeId, ScopeBizType bizType); + + /** + * 查询用户生效中的规则[批量] + * @param userOrgList 用户组织列表 + * @param bizType 业务类型 + * @param priority 使用优先级(1: 是, 0: 否) + * @return java.util.List + */ + List selectUserEffectListBatch(List userOrgList, ScopeBizType bizType, Integer priority,List leaveTypeIds); + + /** + * 查询组织生效中的假期规则[批量] + * @param oldOrganizeId 组织id + * @return java.util.List + */ + List selectOrgEffectListBatch(String oldOrganizeId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ScheduleGroupRuleConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ScheduleGroupRuleConfigService.java new file mode 100644 index 0000000..9f3f977 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/ScheduleGroupRuleConfigService.java @@ -0,0 +1,42 @@ +package jnpf.attendance.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.FtbScheduleGroupFixedParamEntity; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.scheduling.ScheduleGroupRuleConfigDto; +import jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo; + +/** + * 考勤组智能排班「排班规则配置」:固定排班核心参数与划线排班参数的查询与保存。 + * + *

以「固定排班核心参数表」({@link FtbScheduleGroupFixedParamEntity}) 为主实体(每考勤组 1:1), + * 划线参数由 {@code FtbScheduleGroupDrawingParamMapper} 协同维护。 + * + * @author xiaofeng + * @since 2026-05-13 + */ +public interface ScheduleGroupRuleConfigService extends SuperService { + + /** + * 获取排班规则配置。无表记录时子块返回与表 DEFAULT 一致的默认结构。 + * + * @param groupId 考勤组 ID + * @return 配置 + * @throws HandleException 参数非法 + * @throws QueryException 考勤组不存在或已删除 + */ + ScheduleGroupRuleConfigVo getRuleConfig(String groupId) throws HandleException, QueryException; + + /** + * 保存排班规则配置。事务内对当前考勤组两块参数表按 {@code groupId} upsert。 + * + * @param groupId 考勤组 ID(与请求体根 {@code groupId} 须一致) + * @param dto 保存请求体(嵌套为 DTO);与响应 VO 字段名与取值类型一致 + * @return 保存后的配置(等同再查一次) + * @throws HandleException 参数非法 / 业务校验失败 + * @throws QueryException 考勤组不存在或已删除 + */ + ScheduleGroupRuleConfigVo saveRuleConfig(String groupId, ScheduleGroupRuleConfigDto dto) + throws HandleException, QueryException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/SmartPreScheduleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/SmartPreScheduleService.java new file mode 100644 index 0000000..a9fbe7c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/SmartPreScheduleService.java @@ -0,0 +1,31 @@ +package jnpf.attendance.service; + +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.scheduling.PreScheduleTableQueryDto; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; + +/** + * AI 智能排班确认:预排班主表生成与整包保存。 + * + * @author xiaofeng + * @create 2026-05-13 + */ +public interface SmartPreScheduleService { + + /** + * 按日期范围与人效目标生成预排班主表(含行内预览)。 + * + * @param dto 查询条件 + * @return 主表数据 + */ + PreScheduleTableVo buildPreScheduleTable(PreScheduleTableQueryDto dto); + + /** + * 按提交主表过滤 Redis 中 byEmployee 并写回,返回 Redis key 可变片段供后续确认落库。 + * + * @param vo 含 groupId、rows,与生成接口 data 结构一致 + * @return redisKeySuffix:考勤组:yyyy-MM-dd_yyyy-MM-dd(不含租户与固定前缀) + */ + String savePreScheduleTable(PreScheduleTableVo vo) throws HandleException, QueryException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/StatisticsUtilService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/StatisticsUtilService.java new file mode 100644 index 0000000..1b85037 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/StatisticsUtilService.java @@ -0,0 +1,149 @@ +package jnpf.attendance.service; + +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.attendance.DayStatisticsQueryVo; +import jnpf.model.common.DateRangeDto; +import jnpf.permission.vo.v2.user.UserBoundVO; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 统计公共方法服务 + * + * @author shitou + * @date 2023/11/22 + */ +public interface StatisticsUtilService { + /** + * 获取用户考勤情况 + * + * @param userId 用户ID + * @param selectDateRangeDto 日期范围 + * @param dateArrayList 月日期集合 + * @param dayStatisticsQueryVoList 用户日统计数据 + * @return List + */ + List getUserClockDateArrayList(String userId, DateRangeDto selectDateRangeDto, + List dateArrayList, List dayStatisticsQueryVoList); + + /** + * 获取请假记录 + * + * @param userMap 用户信息 + * @param ratioMap 出勤换算比 + * @param userIds 用户集合 + * @param dateRangeDto 日期范围 + * @return 请假记录 Key是用户ID+考勤组ID + */ + Map> getLeaveMap(Map userMap, Map ratioMap, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取迟到记录集合 + * + * @param userMap 用户信息 + * @param sealMap 用户是否封账 + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 迟到记录 Key是用户ID+考勤组ID + */ + Map> getLateMap(Map userMap, Map sealMap, List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取早退记录集合 + * + * @param userMap 用户信息 + * @param sealMap 用户是否封账 + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 早退记录 Key是用户ID+考勤组ID + */ + Map> getEarlyMap(Map userMap, Map sealMap, List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取缺卡记录集合 + * + * @param userMap 用户信息 + * @param sealMap 用户是否封账 + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 缺卡记录 Key是用户ID+考勤组ID + */ + Map> getAbsenceCardMap(Map userMap, Map sealMap, List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取缺勤记录集合 + * + * @param groupIds 考勤组集合 + * @param sealMap 用户是否封账 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 缺勤记录 Key是用户ID+考勤组ID + */ + Map> getAbsenceMap(List groupIds, Map sealMap, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取补卡记录集合 + * + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 补卡记录 Key是用户ID+考勤组ID + */ + Map> getMakeUpCardMap(List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取外勤记录集合 + * + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 外勤记录 Key是用户ID+考勤组ID + */ + Map> getOutworkRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取加班记录集合 + * + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 加班记录 Key是用户ID+考勤组ID + */ + Map> getOvertimeRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取借调记录集合 + * + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 借调记录 Key是用户ID+考勤组ID + */ + Map> getSecondRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取出差记录集合 + * + * @param groupIds 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 出差记录 Key是用户ID+考勤组ID + */ + Map> getBusRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto); + + /** + * 获取外出记录集合 + * + * @param ratioMap 考勤组集合 + * @param userIds 用户集合 + * @param dateRangeDto 筛选时间范围 + * @return 外出记录 Key是用户ID+考勤组ID + */ + Map> getOutRecordList(Map ratioMap, List userIds, DateRangeDto dateRangeDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserConfigService.java new file mode 100644 index 0000000..73ba525 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserConfigService.java @@ -0,0 +1,23 @@ +package jnpf.attendance.service; + +import jnpf.model.attendance.vo.attendance.GroupShiftTimeVo; +import jnpf.model.attendance.vo.attendance.UserConfigVo; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/6/25 9:30 + * @Version 1.0 (版本号) + */ +public interface UserConfigService { + /** + * 获取用户app考勤配置 + */ + UserConfigVo getUserConfig(); + + /** + * 修改用户APP考勤配置 + */ + void updateUserConfig(UserConfigVo userConfigVo); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceService.java new file mode 100644 index 0000000..41e2b5f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceService.java @@ -0,0 +1,123 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.FaceChangeQueryDto; +import jnpf.model.attendance.dto.FaceQueryDto; +import jnpf.model.attendance.dto.UserDto; +import jnpf.model.attendance.dto.UserFaceDto; +import jnpf.model.attendance.vo.ChangeLogVo; +import jnpf.model.attendance.vo.FaceMiniVo; +import jnpf.model.attendance.vo.UserFaceDetailVo; +import jnpf.model.attendance.vo.UserFaceVo; +import jnpf.model.common.PageDto; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; + +/** + * 人脸识别服务 + * + * @author yanwenfu + * @create 2025-04-08 + */ +public interface UserFaceService { + + /** + * 人脸对比 + * @param file 人脸图片 + * @param userInfo 用户信息 + * @return java.lang.Boolean + */ + Boolean getPhotoCheck(MultipartFile file, UserInfo userInfo) throws HandleException; + + /** + * 保存人脸信息 + * @param userDto 用户信息 + * @param userInfo 登录人信息 + * @param faceUrl 人脸url + * @param thumbnailUrl 缩略图url + */ + void saveUserFace(UserDto userDto, UserInfo userInfo, String faceUrl, String thumbnailUrl); + + /** + * 清空人脸 + * @param userId 用户id + */ + void deleteUserFace(String userId, UserInfo userInfo) throws Exception; + + /** + * 查看人脸 + * @param userId 用户id + * @return jnpf.model.attendance.vo.UserFaceVo + */ + UserFaceVo getUserFace(String userId); + + /** + * 变动记录(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getChangeLogPage(FaceChangeQueryDto queryDto); + + /** + * 人脸记录(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getUserFacePage(FaceQueryDto queryDto); + + Map> getUserFaceList(List userIds, String tenantId); + + /** + * 判断用户是否上传人脸 + * @param userId 用户id + * @return java.lang.Boolean + */ + Boolean hasUserFace(String userId); + + /** + * 用户人脸信息 + * @param userId 用户id + * @return jnpf.model.attendance.vo.FaceMiniVo + */ + FaceMiniVo getUserFaceInfo(String userId); + + /** + * 查看人脸详情 + * @param id 人脸记录id + * @return jnpf.model.attendance.vo.UserFaceVo + */ + UserFaceVo getUserFaceDetail(String id); + + /** + * 初始化人脸 + **/ + void initializationFace( String tenantId); + + /** + * 更新用户人脸信息 + * @param userId 用户id + * @param updateUserId 更新人id + */ + AttendanceUserFace updateUserFaceInfo(String userId, String updateUserId); + + /** + * 下发到考勤机 + * @param userId 用户id + * @return java.lang.Integer + */ + Integer sendToMachine(String userId) throws Exception; + + /** + * 更新考勤机返回的状态 + * @param userId 用户id + * @param result 1: 成功, 0: 失败 + */ + void updateMachineSyncStatus(String userId, Integer result); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceTxService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceTxService.java new file mode 100644 index 0000000..1b2028e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UserFaceTxService.java @@ -0,0 +1,18 @@ +package jnpf.attendance.service; + +import jnpf.base.UserInfo; +import jnpf.model.attendance.dto.UserDto; +import org.springframework.web.multipart.MultipartFile; + +/** + * 人脸服务 + * + * @author yanwenfu + * @create 2025-12-11 + */ +public interface UserFaceTxService { + + Integer uploadUserFace(MultipartFile mFile, UserDto userDto, UserInfo userInfo) throws Exception; + + Integer syncUserFaceToMachine(String userId) throws Exception; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UsualPhoneService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UsualPhoneService.java new file mode 100644 index 0000000..004cf64 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/UsualPhoneService.java @@ -0,0 +1,58 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.attendance.AttendanceUserPhone; +import jnpf.model.attendance.dto.CancelPhoneDto; +import jnpf.model.attendance.dto.UsualPhoneDto; +import jnpf.model.attendance.dto.UsualPhoneQueryDto; +import jnpf.model.attendance.dto.UsualPhoneSettingDto; +import jnpf.model.attendance.vo.attendance.UsualPhonePageVo; + +/** + * 常用设备服务 + * + * @author yanwenfu + * @create 2025-09-18 + */ +public interface UsualPhoneService extends SuperService { + + /** + * 批量取消绑定 + * @param cancelPhoneDto 取消绑定参数 + */ + void cancelPhoneBatch(CancelPhoneDto cancelPhoneDto); + + /** + * 更新常用手机设置 + * @param usualPhoneSettingDto 常用手机设置dto + */ + void updateUsualPhoneSetting(UsualPhoneSettingDto usualPhoneSettingDto); + + /** + * 查询常用手机列表(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getUsualPhonePage(UsualPhoneQueryDto queryDto); + + /** + * 新增常用设备 + * @param usualPhoneDto 常用设备dto + */ + void addUsualPhone(UsualPhoneDto usualPhoneDto); + + /** + * 检查常用设备是否异常 + * @param phoneName 手机名称 + * @param phoneCode 手机编码 + * @return java.lang.Boolean + */ + Boolean checkUsualPhone(String phoneName, String phoneCode); + + /** + * 查询常用手机配置 + * @return jnpf.model.attendance.dto.UsualPhoneSettingDto + */ + UsualPhoneSettingDto getUsualPhoneSetting(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/WorkstationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/WorkstationService.java new file mode 100644 index 0000000..db2fce5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/WorkstationService.java @@ -0,0 +1,100 @@ +package jnpf.attendance.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.Workstation; +import jnpf.model.attendance.dto.WorkstationQueryDto; +import jnpf.model.attendance.dto.WorkstationSaveDto; +import jnpf.model.attendance.dto.WorkstationUserAddDto; +import jnpf.model.attendance.dto.WorkstationUserRemoveDto; +import jnpf.model.attendance.vo.WorkstationDetailVo; +import jnpf.model.attendance.vo.WorkstationVo; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; + +import java.util.Date; +import java.util.List; + +/** + * 考勤工作站服务 + * + * @author AI Generated + * @create 2026-05-11 + */ +public interface WorkstationService extends SuperService { + + /** + * 新增工作站 + * + * @param dto 工作站保存DTO + */ + void saveWorkstation(WorkstationSaveDto dto); + + /** + * 编辑工作站 + * + * @param id 工作站ID + * @param dto 工作站保存DTO + */ + void updateWorkstation(String id, WorkstationSaveDto dto); + + /** + * 删除工作站 + * + * @param id 工作站ID + */ + void deleteWorkstation(String id); + + /** + * 查询工作站列表(分页) + * + * @param dto 查询条件(含分页参数) + * @return 工作站分页列表 + */ + PageInfo listWorkstations(WorkstationQueryDto dto); + + /** + * 添加人员 + * + * @param dto 添加人员DTO + */ + void addUsers(WorkstationUserAddDto dto); + + /** + * 删除人员 + * + * @param dto 删除人员DTO + */ + void removeUser(WorkstationUserRemoveDto dto); + + /** + * 查询工作站详情 + * + * @param id 工作站ID + * @return 工作站详情 + */ + WorkstationDetailVo getDetail(String id); + + /** + * 按考勤组查询工作站及其归属员工列表 + *

+ * 工作站归属:考勤组的所属门店({@code orgId})下所有未删除的工作站; + * 工作站员工:考勤组成员通过 + * {@code AttendanceUserService#getAttendanceGroupUsersOfSecondment} 拉取(截止 {@code endTime}、含借调过滤), + * 主岗位与工作站岗位一致的自动归属({@code isExtra=false}), + * 工作站-人员关联表中显式额外加入的为另加入员工({@code isExtra=true})。 + * 每人附带 {@code inGroupDays}({@code [startTime, endTime]} 内在组日)及 {@code canLineSchedule}。 + * + * @param groupId 考勤组ID + * @param startTime 查询开始时间(含) + * @param endTime 查询结束时间(含);同时作为成员快照截止时刻 + * @return 工作站列表,每个元素包含工作站属性及其员工列表 + */ + List listWorkstationsByGroupId(String groupId, Date startTime, Date endTime); + + /** + * 重置指定工作站人员:仅保留主岗位与工作站岗位一致的默认归属,清除另加入的额外人员。 + * + * @param workstationId 工作站ID + */ + void resetWorkstationUsers(String workstationId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilter.java new file mode 100644 index 0000000..5add4fc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilter.java @@ -0,0 +1,23 @@ +package jnpf.attendance.service.filter; + +import jnpf.base.UserInfo; +import jnpf.entity.attendance.FtbAttendanceDailyRule; + +/** + * 出勤规则过滤器 + * + * @author yanwenfu + * @create 2025-09-25 + */ +public interface RuleFilter { + + /** + * @param rule 当前 rule + * @param context 上下文(可以放整个 ruleList,或者中间结果) + * @param userInfo 用户信息 + * @return jnpf.attendance.service.filter.RuleFilterResult 是否保留该 rule + */ + RuleFilterResult keepRule(FtbAttendanceDailyRule rule, RuleFilterContext context, UserInfo userInfo); + + int getOrder(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterContext.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterContext.java new file mode 100644 index 0000000..538bcdf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterContext.java @@ -0,0 +1,65 @@ +package jnpf.attendance.service.filter; + +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.model.attendance.vo.attendance.OvertimeRuleDetailVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import jnpf.util.DateDetail; +import lombok.Getter; +import lombok.Setter; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 出勤规则过滤器上下文 + * + * @author yanwenfu + * @create 2025-09-25 + */ +@Getter +@Setter +public class RuleFilterContext { + + /** 当前日期 */ + private final Date curDate; + // 原始列表 今天最后一个班次 -> 昨天 倒序 + private final List ruleList; + // 过滤出的列表 + private final List collectList = new ArrayList<>(); + // 加班关联的列表 + private final Map map = new HashMap<>(); + // 加班规则map + private final Map overtimeMap; + // 加班规则 + private OvertimeRuleVo overtimeRule; + // 是否已经查询过加班规则 + private Integer hasRuleQuery = 0; + + public RuleFilterContext(Date curDate, List ruleList, Map overtimeMap) { + ruleList.forEach(v -> { + if (v.getInUnbounded() == null) { + v.setInUnbounded(0); + } + if (v.getOutUnbounded() == null) { + v.setOutUnbounded(0); + } + }); + this.curDate = curDate; + this.ruleList = ruleList; + this.overtimeMap = overtimeMap; + } + + /** + * 获取最后一个rule + * @return jnpf.entity.attendance.FtbAttendanceDailyRule + */ + public FtbAttendanceDailyRule getYesterdayLastRule() { + + return ruleList.stream() + .filter(v -> !DateDetail.getDate2Str(v.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(new Date(), DateDetail.DF)) + && null != v.getInPoint()) + .max(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)) + .orElse(null); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterResult.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterResult.java new file mode 100644 index 0000000..fb0ee04 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/RuleFilterResult.java @@ -0,0 +1,35 @@ +package jnpf.attendance.service.filter; + +import lombok.Getter; + +/** + * 出勤规则过滤器结果 + * + * @author yanwenfu + * @create 2025-09-25 + */ +@Getter +public class RuleFilterResult { + + /** 是否保留 */ + private final boolean keep; + /** 是否终止责任链 */ + private final boolean terminate; + + public RuleFilterResult(boolean keep, boolean terminate) { + this.keep = keep; + this.terminate = terminate; + } + + public static RuleFilterResult keep() { + return new RuleFilterResult(true, false); + } + + public static RuleFilterResult discard() { + return new RuleFilterResult(false, false); + } + + public static RuleFilterResult terminate() { + return new RuleFilterResult(false, true); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/CrossDayFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/CrossDayFilter.java new file mode 100644 index 0000000..e06b722 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/CrossDayFilter.java @@ -0,0 +1,59 @@ +package jnpf.attendance.service.filter.impl; + +import jnpf.attendance.service.filter.RuleFilter; +import jnpf.attendance.service.filter.RuleFilterContext; +import jnpf.attendance.service.filter.RuleFilterResult; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import org.springframework.stereotype.Component; + +/** + * 跨日过滤器 + * + * @author yanwenfu + * @create 2025-09-25 + */ +@Component +public class CrossDayFilter implements RuleFilter { + + @Override + public RuleFilterResult keepRule(FtbAttendanceDailyRule rule, RuleFilterContext context, UserInfo userInfo) { + + if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + return RuleFilterResult.discard(); + } + // 昨日的休不展示 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) && !DateDetail.checkSameDay(context.getCurDate(), rule.getDay())) { + return RuleFilterResult.discard(); + } + // 今日的休 + if (DateDetail.checkSameDay(context.getCurDate(), rule.getDay())) { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) { + return RuleFilterResult.keep(); + } + } + // 上班打卡时间是今天的, 都留下 + if (DateDetail.checkSameDay(context.getCurDate(), rule.getInPoint())) { + return RuleFilterResult.keep(); + } + // 下班结束时间是今天的, 都留下 + if (null != rule.getOutLackPoint() && DateDetail.checkSameDay(context.getCurDate(), rule.getOutLackPoint())) { + if (!DateDetail.checkSameDay(context.getCurDate(), rule.getDay()) && rule.getAttendanceType().equals(ConstantUtil.RULE_TYPE_REST)) { + return RuleFilterResult.discard(); + } + return RuleFilterResult.keep(); + } + /*if (DateDetail.checkSameDay(rule.getDay(), context.getCurDate())) { + return RuleFilterResult.terminate(); + }*/ + return RuleFilterResult.discard(); + } + + @Override + public int getOrder() { + return 3; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/LeaveFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/LeaveFilter.java new file mode 100644 index 0000000..c73f64a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/LeaveFilter.java @@ -0,0 +1,55 @@ +package jnpf.attendance.service.filter.impl; + +import jnpf.attendance.service.filter.RuleFilter; +import jnpf.attendance.service.filter.RuleFilterContext; +import jnpf.attendance.service.filter.RuleFilterResult; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.util.DateDetail; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 请假过滤器 + * 判定需要保留的情况 + * @author yanwenfu + * @create 2025-09-25 + */ +@Component +public class LeaveFilter implements RuleFilter { + + @Override + public RuleFilterResult keepRule(FtbAttendanceDailyRule rule, RuleFilterContext context, UserInfo userInfo) { + + if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + // 今天的出勤规则如果只有请假, 就返回回去 + // 获取当天的所有非休息规则 + List sameDayRules = context.getRuleList().stream() + .filter(v -> DateDetail.checkSameDay(context.getCurDate(), v.getDay())) + .filter(v -> !v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) + .collect(Collectors.toList()); + // 如果所有非休息规则都是请假,则返回 keep + boolean allLeave = !sameDayRules.isEmpty() && + sameDayRules.stream().allMatch(v -> v.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())); + if (allLeave) { + return RuleFilterResult.keep(); + } + } + if (WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode() == rule.getInUnbounded() || WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode() == rule.getOutUnbounded()) { + if (DateDetail.checkSameDay(context.getCurDate(), rule.getOutLackPoint()) || DateDetail.checkSameDay(context.getCurDate(), rule.getInPoint())) { + // 当前出勤规则有上/下班请假覆盖 并且 是跨天(昨日跨今日) || 上班时间是今天 + return RuleFilterResult.keep(); + } + } + return RuleFilterResult.discard(); + } + + @Override + public int getOrder() { + return 0; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/OvertimeFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/OvertimeFilter.java new file mode 100644 index 0000000..66731c2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/OvertimeFilter.java @@ -0,0 +1,168 @@ +package jnpf.attendance.service.filter.impl; + +import jnpf.attendance.service.OvertimeRuleService; +import jnpf.attendance.service.filter.RuleFilter; +import jnpf.attendance.service.filter.RuleFilterContext; +import jnpf.attendance.service.filter.RuleFilterResult; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.model.attendance.vo.attendance.OvertimeRuleDetailVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import jnpf.util.JsonUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 加班过滤器 + * + * @author yanwenfu + * @create 2025-09-25 + */ +@Component +public class OvertimeFilter implements RuleFilter { + + @Resource + private OvertimeRuleService overtimeRuleService; + + @Override + public RuleFilterResult keepRule(FtbAttendanceDailyRule rule, RuleFilterContext context, UserInfo userInfo) { + + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + if (StringUtils.isEmpty(rule.getPeriodInfo())) { + return RuleFilterResult.discard(); + } + OvertimeRuleDetailVo ruleDetail; + try { + ruleDetail = JsonUtil.getJsonToBean(rule.getPeriodInfo(), OvertimeRuleDetailVo.class); + if (null == ruleDetail) { + throw new Exception("加班规则异常"); + } + } catch (Exception e) { + return RuleFilterResult.discard(); + } + // 查询加班规则 + rule.setOvertimeRuleDetail(ruleDetail); + // 出勤规则是加班 + if (DateDetail.checkSameDay(context.getCurDate(), rule.getInPoint())) { + // 今天的加班 + if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + addMapValue(rule, context); + } + return RuleFilterResult.keep(); + } + // 昨天的加班 查询下一个班次 + FtbAttendanceDailyRule nextRule = null; + if (rule.getRn() - 1 > 0) { + nextRule = context.getRuleList().get(rule.getRn() - 2); + } + if (null == nextRule || DateDetail.checkSameDay(context.getCurDate(), nextRule.getInPoint())) { + addMapValue(rule, context); + return RuleFilterResult.keep(); + } + } else { + OvertimeRuleDetailVo vo = generateOvertimeRuleDetail(context, rule, userInfo); + if (null == vo) { + return RuleFilterResult.discard(); + } + // 昨天 或 今天 + if (DateDetail.checkSameDay(context.getCurDate(), rule.getDay())) { + // 今天 + if (StringUtils.isNotEmpty(vo.getId()) && vo.getCalcMethod().equals(3)) { + if (AttendanceTypeEnum.ORDINARY.getCode().equals(rule.getAttendanceType())) { + if (!DateDetail.checkSameDay(rule.getInPoint(), context.getCurDate())) { + return RuleFilterResult.discard(); + } + } + rule.setOvertimeRuleDetail(vo); + rule.setOvertime(ConstantUtil.NUM_TRUE); + return RuleFilterResult.keep(); + } + } else { + if (!rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) { + return RuleFilterResult.discard(); + } + // 昨天 出勤规则不是加班 判定是不是昨天最后一个班次 + FtbAttendanceDailyRule lastRule = context.getYesterdayLastRule(); + if (null != lastRule && rule.getId().equals(lastRule.getId())) { + if (vo.getCalcMethod().equals(3)) { + rule.setOvertimeRuleDetail(vo); + rule.setOvertime(ConstantUtil.NUM_TRUE); + return RuleFilterResult.keep(); + } + } + } + } + return RuleFilterResult.discard(); + } + + private OvertimeRuleDetailVo generateOvertimeRuleDetail(RuleFilterContext context, FtbAttendanceDailyRule rule, UserInfo userInfo) { + + OvertimeRuleVo vo = context.getOvertimeRule(); + if (null == vo && context.getHasRuleQuery().equals(ConstantUtil.NUM_TRUE)) { + // 是否已经查询过用户加班规则 + return null; + } + if (null == context.getOvertimeMap()) { + return null; + } + FtbAttendanceDailyRule nextRule = null; + if (rule.getRn() - 1 > 0) { + nextRule = context.getRuleList().get(rule.getRn() - 2); + } + Integer attendanceType = getAttendanceType(rule, nextRule); + if (null == vo) { + // 查询用户加班配置 + OvertimeRuleVo overtimeRule = context.getOvertimeMap().get(userInfo.getUserId()); + context.setOvertimeRule(overtimeRule); + context.setHasRuleQuery(ConstantUtil.NUM_TRUE); + return null == overtimeRule ? null : overtimeRuleService.getEffectWorkDetail(context.getOvertimeRule(), rule.getDay(), attendanceType); + } + return overtimeRuleService.getEffectWorkDetail(context.getOvertimeRule(), rule.getDay(), attendanceType); + } + + private Integer getAttendanceType(FtbAttendanceDailyRule rule, FtbAttendanceDailyRule nextRule) { + Integer attendanceType; + if (rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) { + return AttendanceTypeEnum.ORDINARY.getCode(); + } + if (null == nextRule) { + attendanceType = rule.getAttendanceType(); + } else { + // rule 和 nextRule 中有一个休 则出勤类型为休, 否则为工作日 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || nextRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) { + attendanceType = AttendanceTypeEnum.REST.getCode(); + } else { + attendanceType = AttendanceTypeEnum.ORDINARY.getCode(); + } + } + return attendanceType; + } + + private void addMapValue(FtbAttendanceDailyRule rule, RuleFilterContext context) { + // 加班上班有连续 + if (rule.getRn() + 1 < context.getRuleList().size()) { + FtbAttendanceDailyRule dailyRule = context.getRuleList().get(rule.getRn()); + if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + context.getMap().put(rule.getId(), dailyRule); + } + } + // 加班下班有连续 + if (rule.getRn() - 1 > 0) { + FtbAttendanceDailyRule dailyRule = context.getRuleList().get(rule.getRn() - 2); + if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + context.getMap().put(rule.getId(), dailyRule); + } + } + } + + @Override + public int getOrder() { + return 2; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/SecondmentFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/SecondmentFilter.java new file mode 100644 index 0000000..1896f7c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/filter/impl/SecondmentFilter.java @@ -0,0 +1,48 @@ +package jnpf.attendance.service.filter.impl; + +import jnpf.attendance.service.filter.RuleFilter; +import jnpf.attendance.service.filter.RuleFilterContext; +import jnpf.attendance.service.filter.RuleFilterResult; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.util.DateDetail; +import org.springframework.stereotype.Component; + +/** + * 借调过滤器 + * + * @author yanwenfu + * @create 2025-09-25 + */ +@Component +public class SecondmentFilter implements RuleFilter { + + @Override + public RuleFilterResult keepRule(FtbAttendanceDailyRule rule, RuleFilterContext context, UserInfo userInfo) { + + if (null == rule.getOutLackPoint()) { + return RuleFilterResult.discard(); + } + boolean dateCheck = DateDetail.checkSameDay(context.getCurDate(), rule.getOutLackPoint()) || DateDetail.checkSameDay(context.getCurDate(), rule.getInPoint()); + // rule本身是借调 + if (null != rule.getSelfGroup() && rule.getSelfGroup().equals(2)) { + if (dateCheck) { + return RuleFilterResult.keep(); + } + } + // 上/下班借调覆盖 并且跨日, 或者上班时间在今日 + if (WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode() == rule.getInUnbounded() || WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode() == rule.getOutUnbounded()) { + if (dateCheck) { + // 当前出勤规则有上/下班借调覆盖 并且 是跨天(昨日跨今日) || 上班时间是今天 + return RuleFilterResult.keep(); + } + } + return RuleFilterResult.discard(); + } + + @Override + public int getOrder() { + return 1; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/chain/RuleFilterChain.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/chain/RuleFilterChain.java new file mode 100644 index 0000000..dd1f2df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/chain/RuleFilterChain.java @@ -0,0 +1,215 @@ +package jnpf.attendance.service.handle.chain; + +import jnpf.attendance.service.filter.RuleFilter; +import jnpf.attendance.service.filter.RuleFilterContext; +import jnpf.attendance.service.filter.RuleFilterResult; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 出勤规则过滤链执行器 + * + * @author yanwenfu + * @create 2025-09-25 + */ +@Component +public class RuleFilterChain { + + private final List filters; + + public RuleFilterChain(List filters) { + // 按照 getOrder 排序,保证执行顺序 + this.filters = filters.stream() + .sorted(Comparator.comparingInt(RuleFilter::getOrder)) + .collect(Collectors.toList()); + } + + /** + * 获取今日可打卡出勤规则+数据填写 + * @param curDate 当前日期 + * @param ruleList 出勤规则列表(当前+前一天) + * @param map k:用户id, v:加班规则 + * @param userInfo 用户信息 + * @return java.util.List + */ + public List execute(Date curDate, List ruleList, Map map, UserInfo userInfo) { + RuleFilterContext context = new RuleFilterContext(curDate, ruleList, map); + stop : for (FtbAttendanceDailyRule rule : ruleList) { + boolean keep = false; + for (RuleFilter filter : filters) { + RuleFilterResult result = filter.keepRule(rule, context, userInfo); + if (result.isTerminate()) { + // 终止整个责任链 + break stop; + } + if (result.isKeep()) { + // 不保留,但继续走 + keep = true; + break; + } + } + if (keep) { + context.getCollectList().add(rule); + } + } + // 填值 + if (context.getCollectList().isEmpty()) { + return new ArrayList<>(); + } + // 去除半天休 + List restRuleList = context.getCollectList().stream() + .filter(v -> DateDetail.checkSameDay(context.getCurDate(), v.getDay()) && v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) + .map(FtbAttendanceDailyRule::getId) + .collect(Collectors.toList()); + long count = context.getCollectList().stream().filter(v -> DateDetail.checkSameDay(context.getCurDate(), v.getDay())).count(); + if (!restRuleList.isEmpty() && count > 1) { + context.getCollectList().removeIf(v -> restRuleList.contains(v.getId())); + } + // 今日所有需要打卡的出勤规则 + List addList = new ArrayList<>(); + context.getCollectList().forEach(rule -> { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + // 外出或出差, 且无班次则设置班次类型为-1 + rule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode()); + rule.setInStepOutType(ConstantUtil.NUM_TRUE); + rule.setOutStepOutType(ConstantUtil.NUM_TRUE); + } + // 普通班次 且 允许无需审批的加班 缺卡时间延后 与加班下班相同 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) && rule.getOvertime().equals(ConstantUtil.NUM_TRUE)) { + outLackSet(rule, context); + } + // 请假 + if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode())) { + rule.setMsg("请假回岗"); + // rule.setInLackPoint(rule.getInPoint()); + } + if (rule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode())) { + rule.setMsg("请假离岗"); + // rule.setOutLackPoint(rule.getOutPoint()); + } + // 借调 + if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) { + rule.setInHideStatus(ConstantUtil.STR_TRUE); + // 在这个时间点生成无需打卡 -2 + rule.setOnWorkIgnore(true); + rule.setInLackPoint(rule.getInPoint()); + } + if (rule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) { + rule.setOutHideStatus(ConstantUtil.STR_TRUE); + // 在这个时间点生成无需打卡 -2 + rule.setOffWorkIgnore(true); + rule.setOutLackPoint(rule.getOutPoint()); + } + // 加班 + if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + rule.setInHideStatus(ConstantUtil.STR_TRUE); + // 在这个时间点生成无需打卡 -2 + rule.setOnWorkIgnore(true); + rule.setClockStartPoint(rule.getInPoint()); + rule.setInLackPoint(rule.getInPoint()); + setAnother(ConstantUtil.ON_WORK, context, rule, addList); + } + if (rule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + rule.setOutHideStatus(ConstantUtil.STR_TRUE); + // 在这个时间点生成无需打卡 -2 + rule.setOffWorkIgnore(true); + rule.setOutLackPoint(rule.getOutPoint()); + setAnother(ConstantUtil.OFF_WORK, context, rule, addList); + } + // 晚走晚到 + if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) { + // 在这个时间点生成无需打卡 -2 + rule.setOnWorkIgnore(true); + } + if (rule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) { + // 在这个时间点生成无需打卡 -2 + rule.setOffWorkIgnore(true); + } + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 按审批时长申请计算 无需打卡 + if (rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) { + rule.setOnWorkIgnore(true); + rule.setOffWorkIgnore(true); + } + // 查询上一个班次的下班时间 + inLackSet(rule, context); + // 查询下一个班次的上班时间 + outLackSet(rule, context); + } + }); + context.getCollectList().addAll(addList); + context.getCollectList().sort( + Comparator.comparing( + FtbAttendanceDailyRule::getInPoint, + Comparator.nullsLast(Comparator.naturalOrder()) + ) + ); + return context.getCollectList(); + } + + private void outLackSet(FtbAttendanceDailyRule rule, RuleFilterContext context) { + // 加班 或 普班无审批加班 + if (null == rule.getOutLackPoint() || (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) && rule.getOvertime().equals(ConstantUtil.NUM_TRUE))) { + if (rule.getRn() - 1 > 0) { + FtbAttendanceDailyRule nextRule = context.getRuleList().get(rule.getRn() - 2); + if (null != nextRule.getInPoint()) { + rule.setOutLackPoint(nextRule.getInPoint()); + } else { + if (!DateDetail.checkSameDay(rule.getDay(), context.getCurDate()) && DateDetail.checkSameDay(rule.getOutPoint(), context.getCurDate())) { + // 昨天的班下班时间在今天, 所以缺卡时间最多到今晚24点 + rule.setOutLackPoint(DateDetail.endOfDay(rule.getOutPoint())); + } else { + rule.setOutLackPoint(DateDetail.getNextDay(rule.getOutPoint())); + } + } + } else { + // 无下一个班次 暂时设置为24小时后, 明天会重新获取 + rule.setOutLackPoint(DateDetail.getNextDay(rule.getOutPoint())); + } + } + } + + private void inLackSet(FtbAttendanceDailyRule rule, RuleFilterContext context) { + if (null == rule.getInLackPoint()) { + if (rule.getRn() + 1 < context.getRuleList().size()) { + FtbAttendanceDailyRule dailyRule = context.getRuleList().get(rule.getRn()); + rule.setClockStartPoint(null == dailyRule.getOutPoint() ? DateDetail.beginOfDay(rule.getInPoint()) : dailyRule.getOutPoint()); + } else { + // 无上一个班次 设置为上班时间的0点 + rule.setClockStartPoint(DateDetail.beginOfDay(rule.getInPoint())); + } + rule.setInLackPoint(rule.getOutPoint()); + } + } + + /** + * 设置无需打卡时间 + */ + private void setAnother(Integer workStatus, RuleFilterContext context, FtbAttendanceDailyRule rule, List addList) { + + FtbAttendanceDailyRule dailyRule = context.getMap().get(rule.getId()); + if (null != dailyRule) { + FtbAttendanceDailyRule r = context.getCollectList().stream().filter(v -> v.getId().equals(dailyRule.getId())).findFirst().orElse(null); + if (null == r) { + if (workStatus.equals(ConstantUtil.ON_WORK)) { + dailyRule.setOutHideStatus(ConstantUtil.STR_TRUE); + dailyRule.setOffWorkIgnore(true); + } else { + dailyRule.setInHideStatus(ConstantUtil.STR_TRUE); + dailyRule.setOnWorkIgnore(true); + } + addList.add(dailyRule); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceApproveNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceApproveNotice.java new file mode 100644 index 0000000..7aab57c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceApproveNotice.java @@ -0,0 +1,214 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceUserSettingService; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.*; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.ShiftChangModel; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.vo.UserSettingVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.DateDetail; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +/** + * @Author huanglinpan + * @Date 2024/8/12 10:58 + * @Version 1.0 (版本号) + */ +@Component(AttendanceConstant.APPROVE) +@Slf4j +public class AttendanceApproveNotice implements AttendanceNotice{ + + @Resource + private AttendanceUserSettingService attendanceUserSettingService; + + @Override + public Boolean isPass(ApproveImVo approveImVo) { + if (!approveImVo.getAttendanceNoticeEnum().equals(AttendanceNoticeEnum.APPROVE) && !approveImVo.getAttendanceNoticeEnum().equals(AttendanceNoticeEnum.JOIN_GROUP)) { + return Boolean.FALSE; + } + return Boolean.TRUE; + } + + @Override + public List generatingData(ApproveImVo approveImVo) { + List list = new ArrayList<>(); + list.add(approveImVo.getApproveBaseImVo()); + return list; + } + + @Override + public List getRecipient(ApproveImVo approveImVo) { + List list = checkSetting(approveImVo.getUserIds(),UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode()); + if(null != approveImVo.getType() && Objects.equals(AttendanceNoticeEnum.JOIN_GROUP.getCode(), approveImVo.getType())) { + // 处理被借调人信息 + return checkSetting(list,UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_JOIN_LEAVE_REMINDER.getCode()); + } + return list; + } + + @NotNull + private List checkSetting(List userIds, String code) { + // 校验接收人是否打开配置 + List settingList = attendanceUserSettingService.getSettingList(userIds, UserSettingTypeEnum.USER.getCode(), code); + Map map = settingList.stream().collect(Collectors.toMap(UserSettingVo::getAssociationId, Function.identity())); + List list = new ArrayList<>(); + userIds.forEach(id -> { + UserSettingVo userSettingVo = map.get(id); + if (null == userSettingVo) { + list.add(id); + }else { + // 检查是否打开 + if (1 == userSettingVo.getStatus()) { + list.add(id); + } + } + }); + return list; + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(ApproveImVo approveImVo, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + // 特殊处理借调审批被借调用户看到的URL的参数 + log.error("noticeId:{},approveImVo.getApproveBaseImVo().getUrl():{}",noticeId,approveImVo.getApproveBaseImVo().getUrl()); + String url = APPROVE_SECONDED_URL.equals(approveImVo.getApproveBaseImVo().getUrl()) ? String.format(APPROVE_SECONDED_URL, noticeId) : approveImVo.getApproveBaseImVo().getUrl(); + log.error("url:{}",url); + objects.add(JumpUrl.builder().url(url).urlName(ATTENDANCE_NOTICE_DETAIL_BUTTON_NAME).color(COLOR_BLACK).build()); + return SystemNoticeStrategy.builder() + .title(approveImVo.getApproveBaseImVo().getTitle()) + .noticeModuleEnum(NoticeModuleEnum.KQ_SP) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + ApproveBaseImVo approveBaseImVo = data.get(0); + // 通过/拒绝的消息有操作者 + String startStr = null != approveBaseImVo.getCreateUserName() ? "操作者:" + approveBaseImVo.getCreateUserName() + "
": ""; + if (Objects.equals(ApprovalSettingTypeEnum.LEAVE.getCode(), approveBaseImVo.getType())){ + //开始时间:2024年5月16日 16:23 + //结束时间:2024年5月17日 09:00 + // 考勤V1.6 加入请假类型 小时类维持以上不变,天类: 开始时间:2024年5月16日 结束时间:2024年5月17日 半天类:2024年5月16日 上半天 结束时间:2024年5月17日 下半天 + Integer unit = approveBaseImVo.getUnit(); + if (LeaveUnitEnum.HOUR.getCode().equals(unit)){ + return startStr + + "开始时间:" + DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd HH:mm") + "
" + + "结束时间:" + DateUtil.format(approveBaseImVo.getEndTime(), "yyyy-MM-dd HH:mm"); + }else if (LeaveUnitEnum.DAY.getCode().equals(unit)){ + return startStr + + "开始时间:" + DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd") + "
" + + "结束时间:" + DateUtil.format(approveBaseImVo.getEndTime(), "yyyy-MM-dd"); + }else { + return startStr + + "开始时间:" + DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd ") + dayStr(approveBaseImVo.getStartTimeType()) + "
" + + "结束时间:" + DateUtil.format(approveBaseImVo.getEndTime(), "yyyy-MM-dd ")+ dayStr(approveBaseImVo.getEndTimeType()); + } + } + if (Objects.equals(ApprovalSettingTypeEnum.OVERTIME.getCode(), approveBaseImVo.getType())){ + //选择日期:2024年5月16日 + //开始时间:22:00 + //结束时间:次日08:00 + // 结束时间次日处理 + StringBuffer end = new StringBuffer(); + if (!DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd").equals(DateUtil.format(approveBaseImVo.getEndTime(), "yyyy-MM-dd"))){ + end.append("次日"); + } + end.append(DateUtil.format(approveBaseImVo.getEndTime(), "HH:mm")).append("
"); + return startStr + + "选择日期:" + DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd") + "
" + + "开始时间:" + DateUtil.format(approveBaseImVo.getStartTime(), "HH:mm") + "
" + + "结束时间:" + end; + } + if (Objects.equals(ApprovalSettingTypeEnum.GO_OUT.getCode(), approveBaseImVo.getType())){ + //开始时间:2024年5月16日 + //结束时间:2024年5月17日 + //时长:2天 + return startStr + + "开始时间:" + DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd") + "
" + + "结束时间:" + DateUtil.format(approveBaseImVo.getEndTime(), "yyyy-MM-dd") + "
" + + "时长:" + approveBaseImVo.getDuration(); + } + + if (Objects.equals(ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode(), approveBaseImVo.getType())){ + // 出发地:地点名称地点名称地点名称地点名称 + //目的地:地点名称地点名称地点名称地点名称 + return startStr + + "出发地:" + approveBaseImVo.getDeparture() + "
" + + "目的地:" + approveBaseImVo.getDestination(); + } + + if (Objects.equals(ApprovalSettingTypeEnum.SECONDED.getCode(), approveBaseImVo.getType())){ + //借调开始时间:2024年5月16日 16:23 + //借调结束时间:2024年5月17日 09:00 + //借调考勤组:小露测试考勤组 + //被借调考勤组:小露验收考勤组 + return startStr + + "借调开始时间:" + DateUtil.format(approveBaseImVo.getStartTime(), "yyyy-MM-dd HH:mm") + "
" + + "借调结束时间:" + DateUtil.format(approveBaseImVo.getEndTime(), "yyyy-MM-dd HH:mm") + "
" + + "借调考勤组:" + approveBaseImVo.getGroupName() + "
" + + "被借调考勤组:" + approveBaseImVo.getSecondedGroupName(); + } + if (Objects.equals(ApprovalSettingTypeEnum.OUT.getCode(), approveBaseImVo.getType())) { + + return startStr + + "发起时间:" + DateDetail.getDate2Str(approveBaseImVo.getCreateTime(), DateDetail.DF20) + "
" + + "考勤点:" + approveBaseImVo.getAddress(); + } + if (Objects.equals(ApprovalSettingTypeEnum.ROUTINE.getCode(), approveBaseImVo.getType())) { + + String str = startStr + "补卡时间:" + approveBaseImVo.getRuleTime() + "
"; + if (StringUtils.isEmpty(startStr)) { + str += "出勤结果:" + approveBaseImVo.getResult(); + } + return str; + } + if (Objects.equals(ApprovalSettingTypeEnum.ACTION_RESULT.getCode(), approveBaseImVo.getType())) { + + return startStr + + "变更人员:" + approveBaseImVo.getSecondedUsersName() + "
" + + "变更时间:" + approveBaseImVo.getRuleTime() + "
" + + "出勤结果:" + approveBaseImVo.getAfterResult() + "
" + + "处理结果:" + approveBaseImVo.getResult(); + } + return null; + } + + private static String dayStr(Integer type) { + if (1 == type){ + return "上半天"; + }else { + return "下半天"; + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceChangeNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceChangeNotice.java new file mode 100644 index 0000000..6a7b61d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceChangeNotice.java @@ -0,0 +1,128 @@ +package jnpf.attendance.service.handle.notice; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceDailyRuleMapper; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.AttendanceChangeModel; +import jnpf.model.attendance.model.AttendanceChangeNoticeModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import jnpf.util.StringUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 考勤变更(不审批) + * + * @author yanwenfu + * @create 2024-08-16 + */ +@Service(value = AttendanceConstant.CHECK_RESULT_CHANGE) +public class AttendanceChangeNotice implements AttendanceNotice { + + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private AttendanceDailyRuleMapper attendanceDailyRuleMapper; + @Resource + private UserAntifreeze userAntifreeze; + + @Override + public Boolean isPass(AttendanceChangeNoticeModel model) { + + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_RESULT_CHANGE_REMINDER); + } + + @Override + public List generatingData(AttendanceChangeNoticeModel model) { + + List list = userAntifreeze.getAllByIds(List.of(model.getHandleUserId()), model.getTenantId()); + PartUserInfoVo user = list.stream().findFirst().orElse(null); + StringBuilder handelUser = new StringBuilder(); + if (Objects.isNull(user)) { + handelUser.append("无"); + } else { + handelUser.append(user.getRealName()).append("(").append( + StringUtil.isEmpty(user.getOrganizeName()) ? "暂无" : user.getOrganizeName()).append("-").append( + StringUtil.isEmpty(user.getPositionName()) ? "暂无" : user.getPositionName()).append(")"); + } + FtbAttendanceDailyRule dailyRule = attendanceDailyRuleMapper.selectById(model.getRuleId()); + String changeTime; + if (null == dailyRule) { + changeTime = "--"; + } else { + changeTime = model.getClockInType().equals(ConstantUtil.ON_WORK) ? DateDetail.getDate2Str(dailyRule.getInPoint(), DateDetail.DF9) : DateDetail.getDate2Str(dailyRule.getOutPoint(), DateDetail.DF9); + } + AttendanceChangeModel attendanceChangeModel = AttendanceChangeModel.builder() + .handelUser(handelUser.toString()) + .changeTime(null == dailyRule ? "--" : changeTime) + .changeResult(getResult(model.getChangeType())) + .build(); + return List.of(attendanceChangeModel); + } + + private String getResult(Integer changeType) { + + // 变更类型(-1: 缺卡, 1: 正常, 2: 迟到, 3: 早退, 99: 撤回变更) + switch (changeType) { + case -1: + return "缺卡"; + case 1: + return "正常"; + case 2: + return "迟到"; + case 3: + return "早退"; + case 99: + return "撤回变更"; + default: + return "--"; + } + } + + @Override + public List getRecipient(AttendanceChangeNoticeModel model) { + + return List.of(model.getUserId()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(AttendanceChangeNoticeModel model, String noticeId) { + + List list = new ArrayList<>(); + list.add(new JumpUrl(String.format(AttendanceConstant.GROUP_LOCK_URL1, model.getHandleUserId()), AttendanceConstant.GROUP_LOCK_BUTTON_NAME1, AttendanceConstant.COLOR_BLACK,3,"contactInfo", JSON.toJSONString(new JSONObject(){{ + put("contactId", model.getHandleUserId()); + }}))); + + return SystemNoticeStrategy.builder() + .title(AttendanceConstant.TITLE_CLOCK_IN_CHANGE) + .noticeModuleEnum(NoticeModuleEnum.CQJG_BD) + .jumpUrls(list) + .build(); + } + + @Override + public String getContext(List dataList) { + + if (null == dataList || dataList.isEmpty()) { + return ""; + } + AttendanceChangeModel model = dataList.get(0); + return "操作人:" + model.getHandelUser() + "
" + + "变更记录:" + model.getChangeTime() + "
" + + "变更结果:" + model.getChangeResult(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNotice.java new file mode 100644 index 0000000..a882bae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNotice.java @@ -0,0 +1,53 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.base.UserInfo; +import jnpf.model.attendance.model.AttendanceNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.Objects; + +/** + * + * @param 作为详情展示及IM消息内容的实体 + */ +public interface AttendanceNotice { + + /** + * 通过当前通知发送的条件实现(前置条件) + * @param attendanceNoticeModel + * @return + */ + Boolean isPass(V attendanceNoticeModel); + + /** + * 生成记录数据,用于详情及im消息展示 + * @param attendanceNoticeModel 过程数据 + * @return + */ + List generatingData(V attendanceNoticeModel); + + /** + * 设置接收人 + * @param attendanceNoticeModel 过程数据 + */ + List getRecipient(V attendanceNoticeModel); + /** + * 设置系统通知策略 + * @param attendanceNoticeModel 过程数据 + * @return + */ + SystemNoticeStrategy setSystemNoticeStrategy(V attendanceNoticeModel, String noticeId); + + /** + * 设置im通知内容部分,如需要请使用/br换行 + * @param data 详情展示及IM消息内容的实体集合 + * @return + */ + String getContext(List data); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNoticeHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNoticeHandler.java new file mode 100644 index 0000000..6cb8e98 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/AttendanceNoticeHandler.java @@ -0,0 +1,265 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.ImRobotApi; +import jnpf.attendance.mapper.AttendanceManagerPermissionMapper; +import jnpf.attendance.service.AttendanceNoticeService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.AttendanceUserSettingService; +import jnpf.base.ActionResult; +import jnpf.entity.AttendanceGroupUser; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.enums.attendance.UserSettingTypeEnum; +import jnpf.from.*; +import jnpf.model.attendance.dto.NoticeSaveDto; +import jnpf.model.attendance.model.AttendanceNoticeModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.NoticeConfirm; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.vo.AttendanceGroupAdminVo; +import jnpf.model.attendance.vo.AttendancePermissionVo; +import jnpf.model.attendance.vo.UserSettingVo; +import jnpf.util.ConstantUtil; +import jnpf.util.RandomUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.formula.functions.T; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +@Component +@Slf4j +public class AttendanceNoticeHandler { + @Autowired + private Map attendanceNoticeList; + @Autowired + private AttendanceNoticeService attendanceNoticeService; + @Autowired + private ImRobotApi imRobotApi; + @Autowired + private AttendanceUserSettingService attendanceUserSettingService; + @Resource + private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper; + @Autowired + private AttendanceUserService attendanceUserService; + + /** + * 通知发送 + * @param attendanceNoticeModel 过程数据 + */ + public void send(AttendanceNoticeModel attendanceNoticeModel) { + if (Objects.isNull(attendanceNoticeModel.getAttendanceNoticeEnum())) { + log.error("通知类型未填充"); + return; + } + AttendanceNotice notice = attendanceNoticeList.get(attendanceNoticeModel.getAttendanceNoticeEnum().getServiceName()); + if (Objects.isNull(notice)) { + log.error("未获取到该通知[{}]具体service实现", attendanceNoticeModel.getAttendanceNoticeEnum().getDesc()); + return; + } + if (!notice.isPass(attendanceNoticeModel)) { + log.error("未满足通知[{}]条件,过滤", attendanceNoticeModel.getAttendanceNoticeEnum().getDesc()); + return; + } + List data = notice.generatingData(attendanceNoticeModel); + if(CollUtil.isEmpty(data)){ + return; + } + List recipientList = notice.getRecipient(attendanceNoticeModel); + if (CollUtil.isEmpty(recipientList)) { + log.error("未获取到该通知[{}]接收人", attendanceNoticeModel.getAttendanceNoticeEnum().getDesc()); + return; + } + String noticeId = RandomUtil.uuId(); + SystemNoticeStrategy build = notice.setSystemNoticeStrategy(attendanceNoticeModel, noticeId); + Integer isConfirm = Objects.nonNull(build.getIsConfirm()) && build.getIsConfirm() ? 1 : 0; + attendanceNoticeService.noticeSave(NoticeSaveDto.builder() + .dataJson(JSON.toJSONString(data)) + .id(noticeId) + .title(build.getTitle()) + .isConfirm(isConfirm) + .type(attendanceNoticeModel.getAttendanceNoticeEnum()) + .userId(UserProvider.getLoginUserId()) + .confirmList(Objects.equals(isConfirm, 0) ? null : JSON.toJSONString(initNoticeConfirm(recipientList))) + .build()); + sendNotice(attendanceNoticeModel.getTenantId(), notice.getContext(data), build, isConfirm, recipientList, noticeId, attendanceNoticeModel.getModuleId()); + + } + + private void sendNotice(String tenantId, String context, SystemNoticeStrategy systemNoticeStrategy, Integer isConfirm, List recipientList, String noticeId,String moduleId) { + if (CollUtil.isEmpty(recipientList)) { + log.error("考勤发送系统通知记录id[{}],接收人为空", noticeId); + return; + } + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setMessageAttributionRobotMpId(MP_ID); + form.setToUserIds(recipientList); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(ATTENDANCE_APP_LOGO);//固定图片 + robotNoticeDataForm.setAppName(systemNoticeStrategy.getNoticeModuleEnum().getMsg()); + robotNoticeDataForm.setTitle(systemNoticeStrategy.getTitle()); + robotNoticeDataForm.setContent(context); + robotNoticeDataForm.setContentSummary(systemNoticeStrategy.getTitle()); + LinkedList objects = CollUtil.newLinkedList(); + if (Objects.equals(isConfirm, 1)) { + objects.add(JumpUrlListModel.builder() + .displayMethodEnum(JumpUrlListModel.DisplayMethodEnum.FALSE) + .reqMethod(JumpUrlListModel.ReqMethodEnum.GET) + .type(1) + .url(String.format(ATTENDANCE_NOTICE_CONFIRM_LIST_VIEW_URL, noticeId)) + .buttonName(ATTENDANCE_NOTICE_CONFIRM_LIST_BUTTON_NAME) + .mpId(MP_ID) + .buttonColor("#1A1A1A") + .build()); + objects.add(JumpUrlListModel.builder() + .type(2) + .isConfirm(1) + .confirmButton(ConfirmButton.builder().status(1) + .buttName1("确认收到") + .color1("#3C6DF8") + .url1(String.format(ATTENDANCE_NOTICE_CONFIRM_URL, noticeId)) + .buttName2("已收到") + .color2("#D9D9DB") + .url2("") + .build()) + .displayMethodEnum(JumpUrlListModel.DisplayMethodEnum.FALSE) + .reqMethod(JumpUrlListModel.ReqMethodEnum.POST) + .buttonName(ATTENDANCE_NOTICE_CONFIRM) + .url("") + .build()); + + } else { + List jumpUrls = systemNoticeStrategy.getJumpUrls(); + if (CollUtil.isEmpty(jumpUrls)) { + log.error("考勤发送系统通知记录id[{}],未设置按钮事件", noticeId); + return; + } + int size = jumpUrls.size(); + jumpUrls.forEach(jumpUrl -> objects.add(JumpUrlListModel.builder() + .reqMethod(JumpUrlListModel.ReqMethodEnum.GET) + .displayMethodEnum(size > 1 ? JumpUrlListModel.DisplayMethodEnum.FALSE : JumpUrlListModel.DisplayMethodEnum.TRUE) + .type(Objects.isNull(jumpUrl.getType()) ? 1 : jumpUrl.getType()) + .url(jumpUrl.getUrl()) + .mpId(StringUtil.isNotBlank(jumpUrl.getMpId()) ? jumpUrl.getMpId() : MP_ID) + .buttonName(jumpUrl.getUrlName()) + .buttonColor(jumpUrl.getColor()) + .nativeUrl(Objects.isNull(jumpUrl.getType()) ? null : NativeUrl.builder() + .page(jumpUrl.getPage()) + .param(jumpUrl.getParam()) + .build()) + .build())); + } + robotNoticeDataForm.setJumpUrlList(objects); + form.setRobotNoticeDataForm(robotNoticeDataForm); +// log.error("打印消息内容form:{}", JSON.toJSONString(form)); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + } + + private List initNoticeConfirm(List recipientList) { + return recipientList.stream().map(userId -> NoticeConfirm.builder().userId(userId).confirmedTime(new Date()).isConfirmed(0).build()).collect(Collectors.toList()); + } + + public List getRecipientForSelfAndChild(String groupId, Boolean isAdmin) { + List selfAndChildrenGroupIds = CollUtil.newArrayList(groupId); + if (isAdmin) { + List attendanceGroupAdminVos = attendanceManagerPermissionMapper.queryGroupUsers(selfAndChildrenGroupIds); + List adminUserIds = attendanceGroupAdminVos.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); + if (CollUtil.isEmpty(adminUserIds)) { + return Collections.emptyList(); + } + return getUserIdsForFilterStatus(adminUserIds, UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_RULE_CHANGE_REMINDER); + } + Date date = new Date(); + List attendanceGroupUsers = attendanceUserService.queryByUsersAndGroupFilterSecondment(date, date, null, selfAndChildrenGroupIds); + List userIds = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + List userIdsForFilterStatus = getUserIdsForFilterStatus(userIds, UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_RULE_CHANGE_REMINDER); + return userIdsForFilterStatus; + } + + /** + * 根据配置多个code来过滤出开关都开启的用户集合 + * + * @param userIds + * @param settingCodes + * @return + */ + public List getUserIdsForFilterStatus(List userIds, UserSettingEnum... settingCodes) { + // 判断前置 开启考勤消息提醒 开启极速打卡成功提醒 + List userSettingList = new ArrayList<>(); + Arrays.stream(settingCodes).forEach(v -> userSettingList.add(v.getCode())); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + List settingList = attendanceUserSettingService.getSettingList(userIds, UserSettingTypeEnum.USER.getCode(), userSettingList); + Map> collect = settingList.stream().collect(Collectors.groupingBy(UserSettingVo::getAssociationId)); + userSettingList.clear(); + collect.forEach((userId, settings) -> { + if (settings.stream().allMatch(setting -> Objects.equals(setting.getStatus(), 1))) { + userSettingList.add(userId); + } + }); + return userSettingList; + } + + public boolean checkBeforeCondition(String userId, UserSettingEnum... settingCodes) { + + // 判断前置 开启考勤消息提醒 开启极速打卡成功提醒 + List userSettingList = new ArrayList<>(); + Arrays.stream(settingCodes).forEach(v -> userSettingList.add(v.getCode())); + if (StringUtil.isEmpty(userId)) { + return false; + } + List settingList = attendanceUserSettingService.getSettingList(List.of(userId), UserSettingTypeEnum.USER.getCode(), userSettingList); + if (null == settingList || settingList.isEmpty()) { + return false; + } + Map map = settingList.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + boolean result = true; + for (String code : userSettingList) { + result = checkRule(code, map); + if (!result) { + break; + } + } + return result; + } + + private boolean checkRule(String code, Map map) { + switch (code) { + case UserSettingEnum.MESSAGE_RECEIVE: + case UserSettingEnum.SPEED_CHECK_SUCCESS_REMINDER: + case UserSettingEnum.MISSING_CHECKIN_REMINDER: + case UserSettingEnum.MISSING_END_WORK_REMINDER: + case UserSettingEnum.CLASS_ABSENCE_REMINDER: + case UserSettingEnum.DAILY_REPORT: + case UserSettingEnum.PERSONAL_MONTHLY_REPORT: + case UserSettingEnum.ATTENDANCE_GROUP_LOCK_UNLOCK_NOTICE: + case UserSettingEnum.CLASS_CHANGE_REMINDER: + case UserSettingEnum.PRE_WORK_REMINDER: + case UserSettingEnum.END_WORK_REMINDER: + case UserSettingEnum.ATTENDANCE_RESULT_CHANGE_REMINDER: + UserSettingVo absenceSetting = map.get(code); + if (null == absenceSetting) { + return false; + } else { + return absenceSetting.getStatus().equals(ConstantUtil.NUM_TRUE); + } + default: + return true; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/BeforeClockInNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/BeforeClockInNotice.java new file mode 100644 index 0000000..18e199b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/BeforeClockInNotice.java @@ -0,0 +1,87 @@ +package jnpf.attendance.service.handle.notice; + +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.BeforeClockInModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.RemindClockInNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 打卡前提醒 + * + * @author yanwenfu + * @create 2024-08-15 + */ +@Service(value = AttendanceConstant.BEFORE_CLOCK_IN) +public class BeforeClockInNotice implements AttendanceNotice { + + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(RemindClockInNoticeModel model) { + + if (ConstantUtil.ON_WORK.equals(model.getClockInType())) { + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_PRE_WORK_REMINDER); + } else { + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_END_WORK_REMINDER); + } + } + + @Override + public List generatingData(RemindClockInNoticeModel model) { + + BeforeClockInModel beforeClockInModel = BeforeClockInModel.builder() + .workTimeStr(null == model.getWorkTime() ? "--" : DateDetail.getDate2Str(model.getWorkTime(), DateDetail.DF9)) + .remark(ConstantUtil.ON_WORK.equals(model.getClockInType()) ? AttendanceConstant.TITLE_BEFORE_ON_WORK : AttendanceConstant.TITLE_BEFORE_OFF_WORK) + .clockInType(model.getClockInType()) + .build(); + return List.of(beforeClockInModel); + } + + @Override + public List getRecipient(RemindClockInNoticeModel model) { + + return List.of(model.getUserId()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(RemindClockInNoticeModel model, String noticeId) { + + List list = new ArrayList<>(); + list.add(new JumpUrl(AttendanceConstant.BTN_RULE_URL + "?day=" + DateDetail.getDate2Str(new Date(), DateDetail.DF), AttendanceConstant.BTN_NAME_TO_DETAIL, AttendanceConstant.COLOR_BLACK)); + list.add(new JumpUrl(AttendanceConstant.BTN_FAST_UPDATE_URL, AttendanceConstant.BTN_NAME_GO, AttendanceConstant.COLOR_BLUE)); + + return SystemNoticeStrategy.builder() + .title(ConstantUtil.ON_WORK.equals(model.getClockInType()) ? AttendanceConstant.TITLE_BEFORE_ON_WORK : AttendanceConstant.TITLE_BEFORE_OFF_WORK) + .noticeModuleEnum(NoticeModuleEnum.KQ_DK) + .jumpUrls(list) + .build(); + } + + @Override + public String getContext(List dataList) { + + if (null == dataList || dataList.isEmpty()) { + return ""; + } + BeforeClockInModel model = dataList.get(0); + if (ConstantUtil.ON_WORK.equals(model.getClockInType())) { + return model.getWorkTimeStr() + " " + AttendanceConstant.TITLE_BEFORE_ON_WORK + "!"; + } else { + return model.getWorkTimeStr() + " " + AttendanceConstant.TITLE_BEFORE_OFF_WORK + "!"; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecUnscheduledNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecUnscheduledNotice.java new file mode 100644 index 0000000..664ce82 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecUnscheduledNotice.java @@ -0,0 +1,96 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceSuperAdminService; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.AttendanceGroup; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.model.attendance.model.*; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; +import static jnpf.constants.AttendancePermissionConstant.MANAGER; +import static jnpf.constants.AttendancePermissionConstant.SCHEDULING; + +@Component(AttendanceConstant.CONSEC_UNSCHEDULED) +@Slf4j +public class ConsecUnscheduledNotice implements AttendanceNotice { + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private AttendanceSuperAdminService attendanceSuperAdminService; + + @Override + public Boolean isPass(ConsecUnscheduledNoticeModel attendanceNoticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(ConsecUnscheduledNoticeModel attendanceNoticeModel) { + List shiftChangModels = CollUtil.newArrayList(); + if (CollUtil.isEmpty(attendanceNoticeModel.getUserIds())) { + return shiftChangModels; + } + List infoByIds = userAntifreeze.getInfoByIds(attendanceNoticeModel.getUserIds(), attendanceNoticeModel.getTenantId()); + if (CollUtil.isEmpty(infoByIds)) { + return shiftChangModels; + } + List collect = infoByIds.stream().map(PartUserInfoVo::getRealName).collect(Collectors.toList()); + AttendanceGroup byId = attendanceGroupService.getById(attendanceNoticeModel.getGroupId()); + if (Objects.isNull(byId)) { + return shiftChangModels; + } + attendanceNoticeModel.setAttendanceGroup(new AttendanceGroupParam(attendanceNoticeModel.getGroupId(),byId.getGroupName())); + shiftChangModels.add(new ConsecUnscheduledModel(byId.getGroupName(), collect)); + return shiftChangModels; + } + + @Override + public List getRecipient(ConsecUnscheduledNoticeModel attendanceNoticeModel) { + Map> stringListMap = attendanceSuperAdminService.queryPermissionBySpecify(CollUtil.newArrayList(attendanceNoticeModel.getGroupId()), List.of(SCHEDULING), List.of(MANAGER)); + List orDefault = stringListMap.getOrDefault("-1", CollUtil.newArrayList()); + List orDefault1 = stringListMap.getOrDefault(attendanceNoticeModel.getGroupId(), CollUtil.newArrayList()); + orDefault.addAll(orDefault1); + return orDefault.stream().distinct().filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(ConsecUnscheduledNoticeModel attendanceNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(BTN_SCHEDULE_URL, JSON.toJSONString(attendanceNoticeModel.getAttendanceGroup()))).urlName(BTN_SCHEDULE_GO).color(COLOR_BLACK).build()); + return SystemNoticeStrategy.builder() + .title(ATTENDANCE_NOTICE_CONSEC_UNSCHEDULED_TITLE) + .noticeModuleEnum(NoticeModuleEnum.KQ_PB) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + StringBuffer stringBuffer = new StringBuffer(); + ConsecUnscheduledModel shiftChangModel = data.stream().findFirst().orElse(null); + stringBuffer.append("考勤组:").append(shiftChangModel.getGroupName()).append("
"); + List userNames = shiftChangModel.getUserNames(); + stringBuffer.append("相关成员:").append(StringUtil.join(userNames.stream().limit(3).collect(Collectors.toList()), "、")).append(userNames.size() > 3 ? ("等共计" + userNames.size() + "名成员") : ""); + return stringBuffer.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecutiveAbsenceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecutiveAbsenceNotice.java new file mode 100644 index 0000000..69e3349 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ConsecutiveAbsenceNotice.java @@ -0,0 +1,71 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.*; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * 连续缺勤消息 + * + * @author yanwenfu + * @create 2024-08-12 + */ +@Service(value = AttendanceConstant.CONSECUTIVE_ABSENCE) +public class ConsecutiveAbsenceNotice implements AttendanceNotice { + + @Override + public Boolean isPass(ConsecutiveAbsenceNoticeModel model) { + return Boolean.TRUE; + } + + @Override + public List generatingData(ConsecutiveAbsenceNoticeModel model) { + + ConsecutiveAbsenceModel consecutiveAbsenceModel = ConsecutiveAbsenceModel.builder() + .groupName(model.getGroupName()) + .userName(model.getUserName()) + .absenceDate(model.getAbsenceDate()) + .absenceDetailModel(model.getAbsenceDetailModel()) + .build(); + return List.of(consecutiveAbsenceModel); + } + + @Override + public List getRecipient(ConsecutiveAbsenceNoticeModel attendanceNoticeModel) { + + return attendanceNoticeModel.getToUserList(); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(ConsecutiveAbsenceNoticeModel model, String noticeId) { + + List list = new ArrayList<>(); + list.add(new JumpUrl(String.format(AttendanceConstant.BTN_CONSECUTIVE_ABSENCE_URL, noticeId), AttendanceConstant.BTN_NAME_TO_DETAIL, AttendanceConstant.COLOR_BLACK)); + list.add(new JumpUrl(String.format(AttendanceConstant.ADMINISTRATOR_CHANGE_URL2, model.getGroupId()), AttendanceConstant.ADMINISTRATOR_CHANGE_BUTTON_NAME2, AttendanceConstant.COLOR_BLUE)); + + return SystemNoticeStrategy.builder() + .title(AttendanceConstant.TITLE_CONSECUTIVE_ABSENCE) + .noticeModuleEnum(NoticeModuleEnum.KQ_DK) + .jumpUrls(list) + .build(); + } + + @Override + public String getContext(List dataList) { + + if (null == dataList || dataList.isEmpty()) { + return ""; + } + ConsecutiveAbsenceModel model = dataList.get(0); + return "考勤组:" + model.getGroupName() + "
" + + "成员:" + model.getUserName() + "
" + + "考勤时间:" + model.getAbsenceDate(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/FastClockInNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/FastClockInNotice.java new file mode 100644 index 0000000..48b03e4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/FastClockInNotice.java @@ -0,0 +1,111 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.FastClockInNoticeModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.QuickClockInModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import jnpf.util.StringUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * 极速打卡消息 + * + * @author yanwenfu + * @create 2024-08-12 + */ +@Service(value = AttendanceConstant.FAST_CLOCK_IN) +public class FastClockInNotice implements AttendanceNotice { + + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(FastClockInNoticeModel model) { + + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_SPEED_CHECK_SUCCESS_REMINDER); + } + + @Override + public List generatingData(FastClockInNoticeModel model) { + + QuickClockInModel quickClockInModel = QuickClockInModel.builder() + .workTimeStr(DateDetail.getDate2Str(model.getWorkTime(), DateDetail.DF9)) + .remark(getTitle(model.getAttendanceNoticeEnum())) + .clockInTimeStr(DateDetail.getDate2Str(model.getClockIn().getClockInTime(), DateDetail.DF10)) + .clockInMethod(getClockInMethod(model.getClockIn().getDeviceType())) + .clockInAddress(StringUtil.isEmpty(model.getClockIn().getAddress()) ? "--" : model.getClockIn().getAddress()) + .build(); + return List.of(quickClockInModel); + } + + private String getClockInMethod(Integer deviceType) { + + switch (deviceType) { + case ConstantUtil.DEVICE_PLACE: + return "GPS地点打卡"; + case ConstantUtil.DEVICE_WIFI: + return "WIFI打卡"; + case ConstantUtil.DEVICE_MACHINE: + return "考勤机打卡"; + default: + return "未知"; + } + } + + @Override + public List getRecipient(FastClockInNoticeModel attendanceNoticeModel) { + + return CollUtil.newArrayList(attendanceNoticeModel.getUserId()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(FastClockInNoticeModel model, String noticeId) { + + List list = new ArrayList<>(); + list.add(new JumpUrl(AttendanceConstant.BTN_FAST_DETAIL_URL, AttendanceConstant.BTN_NAME_TO_DETAIL, AttendanceConstant.COLOR_BLACK)); + list.add(new JumpUrl(AttendanceConstant.BTN_FAST_UPDATE_URL, AttendanceConstant.BTN_NAME_FAST_UPDATE, AttendanceConstant.COLOR_BLUE)); + + return SystemNoticeStrategy.builder() + .title(getTitle(model.getAttendanceNoticeEnum())) + .noticeModuleEnum(NoticeModuleEnum.KQ_DK) + .jumpUrls(list) + .build(); + } + + private String getTitle(AttendanceNoticeEnum attendanceNoticeEnum) { + + String title = ""; + if (AttendanceNoticeEnum.CHECK_IN_QUICK.equals(attendanceNoticeEnum)) { + title = AttendanceConstant.ON_WORK; + } + if (AttendanceNoticeEnum.CHECK_OUT_QUICK.equals(attendanceNoticeEnum)) { + title = AttendanceConstant.OFF_WORK; + } + return String.format(AttendanceConstant.TITLE_FAST, title); + } + + @Override + public String getContext(List dataList) { + + if (null == dataList || dataList.isEmpty()) { + return ""; + } + QuickClockInModel model = dataList.get(0); + return model.getWorkTimeStr() + model.getRemark()+ "!" + "
" + + "打卡时间:" + model.getClockInTimeStr() + "
" + + "考勤方式:" + model.getClockInMethod() + "
" + + "考勤点:" + model.getClockInAddress(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupAdminUpdateNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupAdminUpdateNotice.java new file mode 100644 index 0000000..a493b3d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupAdminUpdateNotice.java @@ -0,0 +1,105 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.model.attendance.model.AdminUpdateModel; +import jnpf.model.attendance.model.AdminUpdateNoticeModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static jnpf.constants.AttendanceConstant.*; +import static jnpf.enums.attendance.UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE; + +/** + * 考勤组锁定通知 + */ +@Component(AttendanceConstant.ADMINISTRATOR_CHANGE) +public class GroupAdminUpdateNotice implements AttendanceNotice { + @Autowired + private UserAntifreeze userAntifreeze; + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(AdminUpdateNoticeModel noticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(AdminUpdateNoticeModel noticeModel) { + return CollUtil.newArrayList(AdminUpdateModel.builder() + .type(noticeModel.getType()) + .groupName(noticeModel.getGroupName()) + .currentAuthority(noticeModel.getCurrentAuthority()) + .sonAuthority(noticeModel.getSonAuthority()) + .build()); + } + + @Override + public List getRecipient(AdminUpdateNoticeModel noticeModel) { + return attendanceNoticeHandler.getUserIdsForFilterStatus(noticeModel.getUserIds(), ATTENDANCE_SETTING_MESSAGE_RECEIVE); + } + + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(AdminUpdateNoticeModel noticeModel, String noticeId) { + UserInfo user = UserProvider.getUser(); + ArrayList objects = CollUtil.newArrayList(); + if (noticeModel.getType().equals(1)) { + objects.add(JumpUrl.builder().url(String.format(ADMINISTRATOR_CHANGE_URL1, noticeId)) + .urlName(ADMINISTRATOR_CHANGE_BUTTON_NAME1).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(String.format(ADMINISTRATOR_CHANGE_URL2, noticeModel.getGroupId())) + .urlName(ADMINISTRATOR_CHANGE_BUTTON_NAME2).color(COLOR_BLUE).build()); + return SystemNoticeStrategy.builder() + .title(String.format(ADMINISTRATOR_CHANGE_TITLE1, noticeModel.getGroupName())) + .noticeModuleEnum(NoticeModuleEnum.KQZ_GLY_BD) + .jumpUrls(objects) + .build(); + } else { + objects.add(new JumpUrl(String.format(AttendanceConstant.GROUP_LOCK_URL1, user.getUserId()), AttendanceConstant.GROUP_LOCK_BUTTON_NAME1, AttendanceConstant.COLOR_BLACK,3,"contactInfo", JSON.toJSONString(new JSONObject(){{ + put("contactId", user.getUserId()); + }}))); + return SystemNoticeStrategy.builder() + .title(String.format(ADMINISTRATOR_CHANGE_TITLE2, noticeModel.getGroupName())) + .noticeModuleEnum(NoticeModuleEnum.KQZ_GLY_BD) + .jumpUrls(objects) + .build(); + } + } + + @Override + public String getContext(List data) { + StringBuffer stringBuffer = new StringBuffer(); + AdminUpdateModel lockModel = data.stream().findFirst().orElse(null); + if (lockModel.getType().equals(1)) { + stringBuffer.append("当前考勤组权限:").append(lockModel.getCurrentAuthority()); + } else { + UserInfo user = UserProvider.getUser(); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(user.getUserId()), user.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + if (Objects.isNull(partUserInfoVo)) { + stringBuffer.append("操作人:无
"); + } else { + stringBuffer.append("操作人:").append(partUserInfoVo.getRealName()).append("(").append(StringUtil.isEmpty(partUserInfoVo.getOrganizeName()) ? "暂无" : partUserInfoVo.getOrganizeName()).append("-").append(StringUtil.isEmpty(partUserInfoVo.getPositionName()) ? "暂无" : partUserInfoVo.getPositionName()).append(")
"); + } + stringBuffer.append("您已不能再对【").append(lockModel.getGroupName()).append("】进行相应操作(包含查看成员考勤信息,对考勤组规则进行配置,对成员进行排班等操作)。").append("
"); + } + return stringBuffer.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupLockNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupLockNotice.java new file mode 100644 index 0000000..634b7d7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupLockNotice.java @@ -0,0 +1,107 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.AttendanceGroupUser; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.AttendanceNoticeModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.LockModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +/** + * 考勤组锁定通知 + */ +@Component(AttendanceConstant.GROUP_LOCK) +public class GroupLockNotice implements AttendanceNotice { + @Autowired + private UserAntifreeze userAntifreeze; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(AttendanceNoticeModel noticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(AttendanceNoticeModel noticeModel) { + List data = new ArrayList<>(); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(noticeModel.getUserId()), noticeModel.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + StringBuffer stringBuffer = new StringBuffer(); + if (Objects.isNull(partUserInfoVo)) { + stringBuffer.append("无"); + } else { + stringBuffer.append(partUserInfoVo.getRealName()).append("(").append(StringUtil.isEmpty(partUserInfoVo.getOrganizeName()) ? "暂无" : partUserInfoVo.getOrganizeName()).append("-").append(StringUtil.isEmpty(partUserInfoVo.getPositionName()) ? "暂无" : partUserInfoVo.getPositionName()).append(")"); + } + data.add(LockModel.builder() + .date(new Date()) + .userName(stringBuffer.toString()) + .build()); + return data; + } + + @Override + public List getRecipient(AttendanceNoticeModel noticeModel) { + Date date = new Date(); + List userList = attendanceUserService.getAttendanceGroupUsersOfSecondment(date, date, null, List.of(noticeModel.getGroupId())); + return CollUtil.isEmpty(userList) ? null : attendanceNoticeHandler.getUserIdsForFilterStatus( + userList.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()), + UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_LOCK_UNLOCK_NOTICE); + } + + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(AttendanceNoticeModel noticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(GROUP_LOCK_URL1, noticeModel.getUserId())) + .urlName(GROUP_LOCK_BUTTON_NAME1).color(COLOR_BLACK) + .type(3) + .page("contactInfo") + .param(JSON.toJSONString(new JSONObject(){{ + put("contactId", noticeModel.getUserId()); + }})) + .build()); + objects.add(JumpUrl.builder().url(String.format(GROUP_LOCK_URL2, noticeId)) + .urlName(GROUP_LOCK_BUTTON_NAME2).color(COLOR_BLUE).build()); + return SystemNoticeStrategy.builder() + .title(TITLE_GROUP_LOCK) + .noticeModuleEnum(NoticeModuleEnum.KQZ_SD) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + StringBuffer stringBuffer = new StringBuffer(); + LockModel lockModel = data.stream().findFirst().orElse(null); + stringBuffer.append("操作人:").append(lockModel.getUserName()).append("
"); + stringBuffer.append("当前所在考勤组已被管理员锁定,无法再对").append(DateUtil.format(lockModel.getDate(), "yyyy年MM月dd日")) + .append("之前的出勤结果做变更!"); + return stringBuffer.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupUnLockNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupUnLockNotice.java new file mode 100644 index 0000000..9e95745 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/GroupUnLockNotice.java @@ -0,0 +1,94 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.AttendanceGroupUser; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.AttendanceNoticeModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.model.UnLockModel; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +/** + * 考勤组解锁通知 + */ +@Component(AttendanceConstant.GROUP_UNLOCK) +public class GroupUnLockNotice implements AttendanceNotice { + @Autowired + private UserAntifreeze userAntifreeze; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(AttendanceNoticeModel noticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(AttendanceNoticeModel noticeModel) { + List data = new ArrayList<>(); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(noticeModel.getUserId()), noticeModel.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + StringBuffer stringBuffer = new StringBuffer(); + if (Objects.isNull(partUserInfoVo)) { + stringBuffer.append("无"); + } else { + stringBuffer.append(partUserInfoVo.getRealName()).append("(").append(StringUtil.isEmpty(partUserInfoVo.getOrganizeName()) ? "暂无" : partUserInfoVo.getOrganizeName()).append("-").append(StringUtil.isEmpty(partUserInfoVo.getPositionName()) ? "暂无" : partUserInfoVo.getPositionName()).append(")"); + } + data.add(UnLockModel.builder() + .userName(stringBuffer.toString()) + .build()); + return data; + } + + @Override + public List getRecipient(AttendanceNoticeModel noticeModel) { + Date date = new Date(); + List userList = attendanceUserService.getAttendanceGroupUsersOfSecondment(date, date, null, List.of(noticeModel.getGroupId())); + return CollUtil.isEmpty(userList) ? null : attendanceNoticeHandler.getUserIdsForFilterStatus( + userList.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()), + UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_LOCK_UNLOCK_NOTICE); + } + + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(AttendanceNoticeModel noticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(GROUP_UNLOCK_URL, noticeId)) + .urlName(GROUP_LOCK_BUTTON_NAME2).color(COLOR_BLACK).build()); + return SystemNoticeStrategy.builder() + .title(TITLE_GROUP_UNLOCK) + .noticeModuleEnum(NoticeModuleEnum.KQZ_SD) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + StringBuffer stringBuffer = new StringBuffer(); + UnLockModel unLockModel = data.stream().findFirst().orElse(null); + stringBuffer.append("操作人:").append(unLockModel.getUserName()).append("
"); + stringBuffer.append("考勤组解锁后可以进行如下操作:"); + return stringBuffer.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineNotSchedulingAttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineNotSchedulingAttendanceNotice.java new file mode 100644 index 0000000..f6bfd91 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineNotSchedulingAttendanceNotice.java @@ -0,0 +1,63 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.dto.LineDrawingPeriodDto; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.LineShiftChangeNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +@Component(AttendanceConstant.LINE_SHIFT_NOT_SCHEDUING) +@Slf4j +public class LineNotSchedulingAttendanceNotice implements AttendanceNotice { + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(LineShiftChangeNoticeModel attendanceNoticeModel) { + return true; + } + + @Override + public List generatingData(LineShiftChangeNoticeModel attendanceNoticeModel) { + //查询时段数据 + return attendanceNoticeModel.getPeriods(); + } + + @Override + public List getRecipient(LineShiftChangeNoticeModel attendanceNoticeModel) { + return attendanceNoticeModel.getUserIds().stream().filter(userId -> attendanceNoticeHandler.checkBeforeCondition(userId, UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE)).collect(Collectors.toList()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(LineShiftChangeNoticeModel attendanceNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(BTN_LINE_SCHEDULE_URL, JSON.toJSONString(attendanceNoticeModel.getAttendanceGroup())) + "&moduleId=" + attendanceNoticeModel.getModuleId()).urlName(BTN_SCHEDULE_GO).color(COLOR_BLACK).build()); + return SystemNoticeStrategy.builder() + .title(ATTENDANCE_NOTICE_LINE_SCHEDULED_TITLE) + .noticeModuleEnum(NoticeModuleEnum.BC_BD) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + return "提醒通知:请您及时为划线排班人员排班!如已排班,请忽略"; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineShiftChangAttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineShiftChangAttendanceNotice.java new file mode 100644 index 0000000..833b5ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/LineShiftChangAttendanceNotice.java @@ -0,0 +1,82 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.dto.LineDrawingPeriodDto; +import jnpf.model.attendance.model.*; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +@Component(AttendanceConstant.LINE_SHIFT_CHANG) +@Slf4j +public class LineShiftChangAttendanceNotice implements AttendanceNotice { + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(LineShiftChangeNoticeModel attendanceNoticeModel) { + return true; + } + + @Override + public List generatingData(LineShiftChangeNoticeModel attendanceNoticeModel) { + //查询时段数据 + if (CollUtil.isEmpty(attendanceNoticeModel.getPeriods())) { + return List.of(LineDrawingPeriodDto.builder().day(attendanceNoticeModel.getDay()).build()); + } + return attendanceNoticeModel.getPeriods(); + } + + @Override + public List getRecipient(LineShiftChangeNoticeModel attendanceNoticeModel) { + return CollUtil.isEmpty(attendanceNoticeModel.getUserIds()) ? List.of() : attendanceNoticeModel.getUserIds().stream().filter(userId -> attendanceNoticeHandler.checkBeforeCondition(userId, UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE)).collect(Collectors.toList()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(LineShiftChangeNoticeModel attendanceNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + boolean hasSchedule = attendanceNoticeModel.getPeriods().stream().anyMatch(vo -> StringUtil.isNotEmpty(vo.getStart())); + if (hasSchedule) { + objects.add(JumpUrl.builder().url(BTN_FAST_UPDATE_URL).urlName(BTN_NAME_GO).color(COLOR_BLACK).build()); + } else { + objects.add(JumpUrl.builder().build()); + } + return SystemNoticeStrategy.builder() + .title(ATTENDANCE_NOTICE_LINE_SHIFT_CHANGE_TITLE) + .noticeModuleEnum(NoticeModuleEnum.BC_BD) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + StringBuilder stringBuffer = new StringBuilder(); + String dayStr = data.stream().map(LineDrawingPeriodDto::getDay).distinct().map(day -> DateUtil.format(day, "yyyy年MM月dd日") + "(" + DateUtil.dayOfWeekEnum(day).toChinese() + ")").collect(Collectors.joining(",")); + stringBuffer.append("提醒通知:您在").append(dayStr); + if (data.stream().allMatch(vo -> Objects.isNull(vo.getStart()))) { + stringBuffer.append("暂无排班。"); + return stringBuffer.toString(); + } + if (data.stream().allMatch(vo -> StringUtil.isEmpty(vo.getStart()))) { + stringBuffer.append("的排班已取消。"); + return stringBuffer.toString(); + } + List data1 = data.stream().collect(Collectors.groupingBy(LineDrawingPeriodDto::getDay)).values().stream().findFirst().orElse(List.of()); + stringBuffer.append("的排班调整为:").append(data1.stream().sorted(Comparator.comparing(LineDrawingPeriodDto::getStartType).thenComparing(LineDrawingPeriodDto::getStart)).map(vo -> vo.getStart() + "~" + vo.getEnd()).collect(Collectors.joining(","))).append("。"); + return stringBuffer.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/NoClockInNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/NoClockInNotice.java new file mode 100644 index 0000000..20e0a6e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/NoClockInNotice.java @@ -0,0 +1,134 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.DataStatisticsApi; +import jnpf.engine.model.flowstatistics.vo.TemplatePagesVo; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.NoClockInModel; +import jnpf.model.attendance.model.NoClockInNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 缺卡/旷工消息服务 + * + * @author yanwenfu + * @create 2024-08-13 + */ +@Service(value = AttendanceConstant.WORK_MISSING) +public class NoClockInNotice implements AttendanceNotice { + + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Autowired + private DataStatisticsApi dataStatisticsApi; + + @Override + public Boolean isPass(NoClockInNoticeModel model) { + + // 上班缺卡 下班缺卡 旷工 + if (ConstantUtil.NUM_TRUE == model.getAbsence()) { + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_CLASS_ABSENCE_REMINDER); + } else if (ConstantUtil.ON_WORK.equals(model.getClockInType())) { + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_MISSING_CHECKIN_REMINDER); + } else { + return attendanceNoticeHandler.checkBeforeCondition(model.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_MISSING_END_WORK_REMINDER); + } + } + + @Override + public List generatingData(NoClockInNoticeModel model) { + + String workTimeStr; + if (ConstantUtil.NUM_TRUE == model.getAbsence()) { + workTimeStr = DateDetail.getDate2Str(model.getOnWorkTime(), DateDetail.DF9)+ "至" + DateDetail.getDate2Str(model.getOffWorkTime(), DateDetail.DF10); + } else if (ConstantUtil.ON_WORK.equals(model.getClockInType())) { + workTimeStr = DateDetail.getDate2Str(model.getOnWorkTime(), DateDetail.DF9); + } else { + workTimeStr = DateDetail.getDate2Str(model.getOffWorkTime(), DateDetail.DF9); + } + NoClockInModel noClockInModel = NoClockInModel.builder() + .workTimeStr(workTimeStr) + .remark(getTitle(model.getAbsence(), model.getClockInType())) + .absence(model.getAbsence()) + .clockInType(model.getClockInType()) + .build(); + return List.of(noClockInModel); + } + + private String getTitle(Integer absence, Integer clockInType) { + + if (ConstantUtil.NUM_TRUE == absence) { + return AttendanceConstant.TITLE_ABSENCE; + } else if (ConstantUtil.ON_WORK.equals(clockInType)) { + return AttendanceConstant.TITLE_ON_WORK; + } else { + return AttendanceConstant.TITLE_OFF_WORK; + } + } + + @Override + public List getRecipient(NoClockInNoticeModel model) { + + return List.of(model.getUserId()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(NoClockInNoticeModel model, String noticeId) { + + // 查询url需要的参数(补卡审批) + List list = dataStatisticsApi.getFeignTemplatePagesByBusinessType(ConstantUtil.REPAIR_TYPE); + String url = ""; + if (null != list && !list.isEmpty()) { + TemplatePagesVo template = list.get(0); + JSONObject json = new JSONObject(); + // 前端需要回显补卡的记录 所需的json格式 + JSONObject data = new JSONObject(); + data.set("clockInResultId", model.getClockInResultId()); + json.set("interfaceData", data); + json.set("extraData", new JSONObject()); + json.set("paramsData", new JSONObject()); + JSONObject sourceData = new JSONObject(); + sourceData.set("text", model.getClockInType().equals(ConstantUtil.ON_WORK) ? DateDetail.getDateTime2Str(model.getOnWorkTime()) : DateDetail.getDateTime2Str(model.getOffWorkTime())); + json.set("sourceData", sourceData); + json.set("tableData", new JSONArray()); + url = String.format(AttendanceConstant.BTN_REPAIR_URL, template.getFlowId(), template.getTemplateId(), json); + } + return SystemNoticeStrategy.builder() + .title(getTitle(model.getAbsence(), model.getClockInType())) + .noticeModuleEnum(NoticeModuleEnum.KQ_DK) + .jumpUrls(List.of(new JumpUrl(url, AttendanceConstant.BTN_NAME_REPAIR, AttendanceConstant.COLOR_BLACK, "__UNI__65CF373"))) + .build(); + } + + @Override + public String getContext(List dataList) { + + if (null == dataList || dataList.isEmpty()) { + return ""; + } + NoClockInModel model = dataList.get(0); + + if (ConstantUtil.NUM_TRUE == model.getAbsence()) { + return model.getWorkTimeStr() + " 上下班均未打卡,计为旷工!"; + } else if (ConstantUtil.ON_WORK.equals(model.getClockInType())) { + return model.getWorkTimeStr() + " 上班已缺卡!"; + } else { + return model.getWorkTimeStr() + " 下班已缺卡!"; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/RuleChangeAttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/RuleChangeAttendanceNotice.java new file mode 100644 index 0000000..4123bd5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/RuleChangeAttendanceNotice.java @@ -0,0 +1,132 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceManagerPermissionMapper; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.RuleChangeModel; +import jnpf.model.attendance.model.RuleChangeNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.vo.AttendanceGroupAdminVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.ATTENDANCE_NOTICE_ADMIN_RULE_CHANGE_TITLE; +import static jnpf.constants.AttendanceConstant.ATTENDANCE_NOTICE_RULE_CHANGE_TITLE; + +@Component(AttendanceConstant.RULE_CHANGE_RATIO) +public class RuleChangeAttendanceNotice implements AttendanceNotice { + + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private UserAntifreeze userAntifreeze; + + @Override + public Boolean isPass(RuleChangeNoticeModel attendanceNoticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(RuleChangeNoticeModel attendanceNoticeModel) { + String tips = ""; + if (Objects.equals(attendanceNoticeModel.getAttendanceNoticeEnum(), AttendanceNoticeEnum.RULE_CHANGE_RATIO)) { + tips = String.format("出勤换算比由%s小时/天改为%s小时/天", attendanceNoticeModel.getOldValue(), attendanceNoticeModel.getNewValue()); + } + if (Objects.equals(attendanceNoticeModel.getAttendanceNoticeEnum(), AttendanceNoticeEnum.RULE_CHANGE_FIELD)) { + tips = String.format("外勤打卡已调整为%s外勤打卡", Objects.equals(attendanceNoticeModel.getNewValue(), 1) ? "允许" : "不允许"); + } + if (Objects.equals(attendanceNoticeModel.getAttendanceNoticeEnum(), AttendanceNoticeEnum.RULE_CHANGE_CARD_MAKEUP)) { + tips = String.format("补卡规则已调整为%s补卡", Objects.equals(attendanceNoticeModel.getNewValue(), 1) ? "允许" : "不允许"); + } + if (Objects.equals(attendanceNoticeModel.getAttendanceNoticeEnum(), AttendanceNoticeEnum.RULE_CHANGE_GPS_SWIFT)) { + tips = String.format("考勤方式已%s使用GPS地点打卡", Objects.equals(attendanceNoticeModel.getNewValue(), 1) ? "允许" : "禁止"); + if (Objects.equals(attendanceNoticeModel.getNewValue(), 1)) { + StringBuilder sb = new StringBuilder("
可使用考勤点:").append("
"); + if (CollUtil.isNotEmpty(attendanceNoticeModel.getAddressList())) { + sb.append(String.join("
", attendanceNoticeModel.getAddressList())); + } + tips += sb.toString(); + } + } + if (Objects.equals(attendanceNoticeModel.getAttendanceNoticeEnum(), AttendanceNoticeEnum.RULE_CHANGE_WIFI_SWIFT)) { + tips = String.format("考勤方式已%s使用WIFI打卡", Objects.equals(attendanceNoticeModel.getNewValue(), 1) ? "允许" : "不允许"); + if (Objects.equals(attendanceNoticeModel.getNewValue(), 1)) { + StringBuilder sb = new StringBuilder("
可使用考勤点:").append("
"); + if (CollUtil.isNotEmpty(attendanceNoticeModel.getAddressList())) { + sb.append(String.join("、", attendanceNoticeModel.getAddressList())); + } + tips += sb.toString(); + } + } + if (Objects.equals(attendanceNoticeModel.getAttendanceNoticeEnum(), AttendanceNoticeEnum.RULE_CHANGE_KQJ_SWIFT)) { + tips = String.format("考勤方式已%s使用考勤机打卡", Objects.equals(attendanceNoticeModel.getNewValue(), 1) ? "允许" : "不允许"); + if (Objects.equals(attendanceNoticeModel.getNewValue(), 1)) { + StringBuilder sb = new StringBuilder("
可使用考勤点:").append("
"); + if (CollUtil.isNotEmpty(attendanceNoticeModel.getAddressList())) { + sb.append(String.join("、", attendanceNoticeModel.getAddressList())); + } + tips += sb.toString(); + } + } + AttendanceGroup byId = attendanceGroupService.getById(attendanceNoticeModel.getGroupId()); + return CollUtil.newArrayList(RuleChangeModel.builder() + .groupName(byId.getGroupName()) + .tips(tips) + .build()); + } + + @Override + public List getRecipient(RuleChangeNoticeModel attendanceNoticeModel) { + return attendanceNoticeHandler.getRecipientForSelfAndChild(attendanceNoticeModel.getGroupId(), attendanceNoticeModel.getIsAdmin()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(RuleChangeNoticeModel attendanceNoticeModel, String noticeId) { + return SystemNoticeStrategy.builder() + .noticeModuleEnum(NoticeModuleEnum.KQZ_BD) + .title(attendanceNoticeModel.getIsAdmin() ? ATTENDANCE_NOTICE_ADMIN_RULE_CHANGE_TITLE : ATTENDANCE_NOTICE_RULE_CHANGE_TITLE) + .isConfirm(Boolean.TRUE) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + UserInfo user = UserProvider.getUser(); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(user.getUserId()), user.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + StringBuffer stringBuffer = new StringBuffer(); + RuleChangeModel ruleChangeModel = data.stream().findFirst().orElse(null); + stringBuffer.append("考勤组:").append(ruleChangeModel.getGroupName()).append("
"); + if (Objects.isNull(partUserInfoVo)) { + stringBuffer.append("操作人:无
"); + } else { + stringBuffer.append("操作人:").append(partUserInfoVo.getRealName()).append("(").append(StringUtil.isEmpty(partUserInfoVo.getOrganizeName()) ? "暂无" : partUserInfoVo.getOrganizeName()).append("-").append(StringUtil.isEmpty(partUserInfoVo.getPositionName()) ? "暂无" : partUserInfoVo.getPositionName()).append(")
"); + } + stringBuffer.append(ruleChangeModel.getTips()); + return stringBuffer.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementDayNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementDayNotice.java new file mode 100644 index 0000000..e82b64b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementDayNotice.java @@ -0,0 +1,96 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.DaySettlementModel; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.SettlementNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.util.DateUtil; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +/** + * 日统计考勤结算通知 + */ +@Component(AttendanceConstant.INDIVIDUAL_DAILY_STATISTICS) +public class SettlementDayNotice implements AttendanceNotice { + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(SettlementNoticeModel settlementNoticeModel) { + return attendanceNoticeHandler.checkBeforeCondition(settlementNoticeModel.getUserId(), + UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_DAILY_REPORT); + } + + @Override + public List generatingData(SettlementNoticeModel settlementNoticeModel) { + return settlementNoticeModel.getDayNoticeModelList().stream().map(item -> DaySettlementModel.builder() + .groupName(item.getGroupName()) + .shiftStr(String.join(" ", item.getShiftList())) + .shiftTimeStr(String.join(" ", item.getShiftTimeList())) + .clockTimeStr(String.join("|", item.getClockTimeList())) + .clockTime(item.getClockTime()) + .shouldAttendHours(item.getShouldAttendHours()) + .actualAttendHours(item.getActualAttendHours()) + .workingHours(item.getWorkingHours()) + .lateTimes(item.getLateTimes()) + .lateMinutes(item.getLateMinutes()) + .earlyLeaveTimes(item.getEarlyLeaveTimes()) + .earlyLeaveMinutes(item.getEarlyLeaveMinutes()) + .absenceCardTimes(item.getAbsenceCardTimes()) + .absenceCardHours(item.getAbsenceCardHours()) + .absenceTimes(item.getAbsenceTimes()) + .absenceHours(item.getAbsenceHours()) + .leaveHours(item.getLeaveHours()) + .leaveDays(item.getLeaveDays()) + .busDays(item.getBusDays()) + .outDays(item.getOutDays()) + .outHours(item.getOutHours()) + .overtimeHours(item.getOvertimeHours()) + .endDate(DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH:mm")) + .build()).collect(Collectors.toList()); + } + + @Override + public List getRecipient(SettlementNoticeModel settlementNoticeModel) { + return CollUtil.newArrayList(settlementNoticeModel.getUserId()); + } + + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(SettlementNoticeModel settlementNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(INDIVIDUAL_DAILY_STATISTICS_URL1, noticeId)) + .urlName(INDIVIDUAL_DAILY_STATISTICS_BUTTON_NAME1).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(INDIVIDUAL_DAILY_STATISTICS_URL2) + .urlName(INDIVIDUAL_DAILY_STATISTICS_BUTTON_NAME2).color(COLOR_BLUE).build()); + return SystemNoticeStrategy.builder() + .title(settlementNoticeModel.getTitle()) + .noticeModuleEnum(NoticeModuleEnum.GR_RB) + .jumpUrls(objects) + .build(); + + } + + @Override + public String getContext(List data) { + DaySettlementModel daySettlementModel = data.stream().findFirst().orElse(null); + return "考勤组" + daySettlementModel.getGroupName() + "
" + + "当日班次:" + daySettlementModel.getShiftStr() + "
" + + "班次时间:" + daySettlementModel.getShiftTimeStr() + "
" + + "打卡时间:" + daySettlementModel.getClockTimeStr() + "
"; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementMonthNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementMonthNotice.java new file mode 100644 index 0000000..95d1dca --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementMonthNotice.java @@ -0,0 +1,129 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.MonthSettlementModel; +import jnpf.model.attendance.model.SettlementNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +/** + * 日统计考勤结算通知 + */ +@Component(AttendanceConstant.INDIVIDUAL_MONTHLY_STATISTICS) +public class SettlementMonthNotice implements AttendanceNotice { + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(SettlementNoticeModel settlementNoticeModel) { + return attendanceNoticeHandler.checkBeforeCondition(settlementNoticeModel.getUserId(), + UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_PERSONAL_MONTHLY_REPORT); + } + + @Override + public List generatingData(SettlementNoticeModel settlementNoticeModel) { + return settlementNoticeModel.getMonthNoticeModelList().stream().map(item -> MonthSettlementModel.builder() + .cycleDate(item.getCycleDate()) + .groupName(item.getGroupName()) + .effectiveAttendDays(item.getEffectiveAttendDays()) + .leaveDays(item.getLeaveDays()) + .leaveHours(item.getLeaveHours()) + .lateTimes(item.getLateTimes()) + .earlyLeaveTimes(item.getEarlyLeaveTimes()) + .absenceCardTimes(item.getAbsenceCardTimes()) + .absenceTimes(item.getAbsenceTimes()) + .busTimes(item.getBusTimes()) + .outTimes(item.getOutTimes()) + .overtimeTimes(item.getOvertimeTimes()) + .endDate(item.getEndDate()) + .build()).collect(Collectors.toList()); + } + + @Override + public List getRecipient(SettlementNoticeModel settlementNoticeModel) { + return CollUtil.newArrayList(settlementNoticeModel.getUserId()); + } + + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(SettlementNoticeModel settlementNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(INDIVIDUAL_MONTHLY_STATISTICS_URL1, noticeId)) + .urlName(INDIVIDUAL_MONTHLY_STATISTICS_BUTTON_NAME1).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(INDIVIDUAL_MONTHLY_STATISTICS_URL2) + .urlName(INDIVIDUAL_MONTHLY_STATISTICS_BUTTON_NAME2).color(COLOR_BLUE).build()); + return SystemNoticeStrategy.builder() + .title(settlementNoticeModel.getTitle()) + .noticeModuleEnum(NoticeModuleEnum.GR_RB) + .jumpUrls(objects) + .build(); + + } + + @Override + public String getContext(List data) { + MonthSettlementModel monthSettlementModel = data.stream().findFirst().orElse(null); + StringBuilder stringBuffer = new StringBuilder(); + stringBuffer.append("时间:").append(monthSettlementModel.getCycleDate()).append("
"); + stringBuffer.append("考勤组:").append(monthSettlementModel.getGroupName()).append("
"); + stringBuffer.append("出勤情况:").append("
"); + getTipsStr(monthSettlementModel, stringBuffer); + return stringBuffer.toString(); + } + + private void getTipsStr(MonthSettlementModel monthSettlementModel, StringBuilder tips) { + if (monthSettlementModel.getEffectiveAttendDays().compareTo(BigDecimal.ZERO) > 0) { + tips.append("出勤天数:").append(monthSettlementModel.getEffectiveAttendDays()).append("天
"); + } else if (monthSettlementModel.getLeaveDays().compareTo(BigDecimal.ZERO) > 0) { + tips.append("请假天数:").append(getFieldValue(monthSettlementModel.getLeaveDays(), monthSettlementModel.getLeaveHours())).append("
"); + } else if (monthSettlementModel.getLateTimes() > 0) { + tips.append("迟到次数:").append(monthSettlementModel.getLateTimes()).append("次
"); + } else if (monthSettlementModel.getEarlyLeaveTimes() > 0) { + tips.append("早退次数:").append(monthSettlementModel.getEarlyLeaveTimes()).append("次
"); + } else if (monthSettlementModel.getAbsenceCardTimes() > 0) { + tips.append("缺卡次数:").append(monthSettlementModel.getAbsenceCardTimes()).append("次
"); + } else if (monthSettlementModel.getAbsenceTimes() > 0) { + tips.append("缺勤次数:").append(monthSettlementModel.getAbsenceTimes()).append("次
"); + } else if (monthSettlementModel.getBusTimes() > 0) { + tips.append("出差次数:").append(monthSettlementModel.getBusTimes()).append("次
"); + } else if (monthSettlementModel.getOutTimes() > 0) { + tips.append("外出次数:").append(monthSettlementModel.getOutTimes()).append("次
"); + } else if (monthSettlementModel.getOvertimeTimes() > 0) { + tips.append("加班次数:").append(monthSettlementModel.getOvertimeTimes()).append("次
"); + } + } + /** + * 生成组合字段值 + * + * @param durationDays 时长天数 + * @param durationHours 时长小时 + * @return 组合字段值 + */ + private static String getFieldValue(BigDecimal durationDays, BigDecimal durationHours) { + StringBuilder sb = new StringBuilder(); + if (durationDays.compareTo(BigDecimal.ZERO) == 0 && durationHours.compareTo(BigDecimal.ZERO) == 0) { + sb.append("0天"); + } else if (durationDays.compareTo(BigDecimal.ZERO) > 0 && durationHours.compareTo(BigDecimal.ZERO) > 0) { + sb.append(durationDays).append("天").append(durationHours).append("小时"); + } else if (durationDays.compareTo(BigDecimal.ZERO) == 0 && durationHours.compareTo(BigDecimal.ZERO) > 0) { + sb.append(durationHours).append("小时"); + } else if (durationDays.compareTo(BigDecimal.ZERO) > 0 && durationHours.compareTo(BigDecimal.ZERO) == 0) { + sb.append(durationDays).append("天"); + } + return sb.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementTeamMonthNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementTeamMonthNotice.java new file mode 100644 index 0000000..c946a7d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SettlementTeamMonthNotice.java @@ -0,0 +1,118 @@ +package jnpf.attendance.service.handle.notice; + + +import cn.hutool.core.collection.CollUtil; +import com.google.common.collect.Maps; +import jnpf.constants.AttendanceConstant; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.SettlementNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.model.TeamMonthSettlementModel; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +/** + * 日统计考勤结算通知 + */ +@Component(AttendanceConstant.TEAM_MONTHLY_STATISTICS) +public class SettlementTeamMonthNotice implements AttendanceNotice { + @Override + public Boolean isPass(SettlementNoticeModel settlementNoticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(SettlementNoticeModel settlementNoticeModel) { + return settlementNoticeModel.getTeamMonthNoticeModelList().stream().map(item -> TeamMonthSettlementModel.builder() + .groupName(item.getGroupName()) + .normalCount(item.getNormalCount()) + .leaveCount(item.getLeaveCount()) + .lateCount(item.getLateCount()) + .earlyLeaveCount(item.getEarlyLeaveCount()) + .absenceCardCount(item.getAbsenceCardCount()) + .absenceCount(item.getAbsenceCount()) + .busCount(item.getBusCount()) + .outCount(item.getOutCount()) + .overtimeCount(item.getOvertimeCount()) + .endDate(item.getEndDate()) + .build()).collect(Collectors.toList()); + } + + @Override + public List getRecipient(SettlementNoticeModel settlementNoticeModel) { + return CollUtil.newArrayList(settlementNoticeModel.getUserIds()); + } + + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(SettlementNoticeModel settlementNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(TEAM_MONTHLY_STATISTICS_URL1, noticeId)) + .urlName(TEAM_MONTHLY_STATISTICS_BUTTON_NAME1).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(TEAM_MONTHLY_STATISTICS_URL2) + .urlName(TEAM_MONTHLY_STATISTICS_BUTTON_NAME2).color(COLOR_BLUE).build()); + return SystemNoticeStrategy.builder() + .title(settlementNoticeModel.getTitle()) + .noticeModuleEnum(NoticeModuleEnum.GR_RB) + .jumpUrls(objects) + .build(); + + } + + @Override + public String getContext(List data) { + TeamMonthSettlementModel teamMonthSettlementModel = data.stream().findFirst().orElse(null); + StringBuilder stringBuffer = new StringBuilder(); + stringBuffer.append("考勤组:").append(teamMonthSettlementModel.getGroupName()).append("
"); + stringBuffer.append("出勤情况:").append("
");; + getTipsList(teamMonthSettlementModel, stringBuffer); + return stringBuffer.toString(); + } + + private void getTipsList(TeamMonthSettlementModel teamMonthSettlementModel, StringBuilder tips) { + LinkedHashMap map = Maps.newLinkedHashMap(); + if (teamMonthSettlementModel.getNormalCount() > 0) { + map.put("正常", teamMonthSettlementModel.getNormalCount()); + } + if (teamMonthSettlementModel.getLeaveCount() > 0) { + map.put("请假", teamMonthSettlementModel.getLeaveCount()); + } + if (teamMonthSettlementModel.getLateCount() > 0) { + map.put("迟到", teamMonthSettlementModel.getLateCount()); + } + if (teamMonthSettlementModel.getEarlyLeaveCount() > 0) { + map.put("早退", teamMonthSettlementModel.getEarlyLeaveCount()); + } + if (teamMonthSettlementModel.getAbsenceCardCount() > 0) { + map.put("缺卡", teamMonthSettlementModel.getAbsenceCardCount()); + } + if (teamMonthSettlementModel.getAbsenceCount() > 0) { + map.put("旷工", teamMonthSettlementModel.getAbsenceCount()); + } + if (teamMonthSettlementModel.getBusCount() > 0) { + map.put("出差", teamMonthSettlementModel.getBusCount()); + } + if (teamMonthSettlementModel.getOutCount() > 0) { + map.put("外出", teamMonthSettlementModel.getOutCount()); + } + if (teamMonthSettlementModel.getOvertimeCount() > 0) { + map.put("加班", teamMonthSettlementModel.getOvertimeCount()); + } + int i = 0; + for (Map.Entry stringIntegerEntry : map.entrySet()) { + if (i > 1) { + break; + } + tips.append(stringIntegerEntry.getKey()).append(":").append(stringIntegerEntry.getValue()).append("人
"); + i++; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ShiftChangAttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ShiftChangAttendanceNotice.java new file mode 100644 index 0000000..fb9e1bb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/ShiftChangAttendanceNotice.java @@ -0,0 +1,173 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceShiftNameSettingService; +import jnpf.attendance.service.AttendanceShiftSettingPeriodService; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.AttendanceShiftNameEntity; +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.ShiftChangModel; +import jnpf.model.attendance.model.ShiftChangeNoticeModel; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.vo.AttendanceShiftSettingPeriodVo; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; + +@Component(AttendanceConstant.SHIFT_CHANG) +@Slf4j +public class ShiftChangAttendanceNotice implements AttendanceNotice { + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private AttendanceShiftNameSettingService attendanceShiftNameSettingService; + @Autowired + private AttendanceShiftSettingPeriodService attendanceShiftSettingPeriodService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Override + public Boolean isPass(ShiftChangeNoticeModel attendanceNoticeModel) { + return attendanceNoticeHandler.checkBeforeCondition(attendanceNoticeModel.getUserId(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, + UserSettingEnum.ATTENDANCE_SETTING_CLASS_CHANGE_REMINDER); + } + + @Override + public List generatingData(ShiftChangeNoticeModel attendanceNoticeModel) { + List shiftChangModels = CollUtil.newArrayList(); + //查询时段数据 + List dailyRuleResultVos = attendanceNoticeModel.getDailyRuleResultVos(); + List collect = dailyRuleResultVos.stream().map(DailyRuleResultVo::getFromShiftId).filter(StringUtil::isNotEmpty).collect(Collectors.toList()); + List collect1 = dailyRuleResultVos.stream().map(DailyRuleResultVo::getToShiftId).filter(StringUtil::isNotEmpty).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect1)) { + collect.addAll(collect1); + } + List list1 = CollUtil.isEmpty(collect1) ? CollUtil.newArrayList() : attendanceShiftSettingPeriodService.list(new LambdaQueryWrapper() + .in(AttendanceShiftSettingPeriodEntity::getShiftId, collect1).eq(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.FALSE)); + List attendanceShiftSettingPeriodVos = BeanUtil.copyToList(list1, AttendanceShiftSettingPeriodVo.class); + Map> periodMap = attendanceShiftSettingPeriodVos.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodVo::getShiftId)); + List attendanceShiftNameEntities = CollUtil.isEmpty(collect) ? CollUtil.newArrayList() : attendanceShiftNameSettingService.listByIds(collect); + Map shiftMap = attendanceShiftNameEntities.stream().collect(Collectors.toMap(AttendanceShiftNameEntity::getId, entity -> entity)); + dailyRuleResultVos.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getDate)).forEach((day, dailyRuleVos) -> { + //旧班次名称 + String oldShiftName = ""; + //新班次名称 + String newShiftName = ""; + //新班次时间 + String newShift = ""; + + Map> collect2 = dailyRuleVos.stream() + .sorted(Comparator.comparing(vo -> Objects.isNull(vo.getToSchedulesType()) ? 1 : vo.getToSchedulesType())) + .collect(Collectors.groupingBy(vo -> Objects.isNull(vo.getToSchedulesType()) ? 1 : vo.getToSchedulesType())); + int size = collect2.size(); + int i = 0; + for (Map.Entry> stringListEntry : collect2.entrySet()) { + DailyRuleResultVo dailyRuleVo = stringListEntry.getValue().get(0); + AttendanceShiftNameEntity toShiftEntity = shiftMap.getOrDefault(dailyRuleVo.getToShiftId(), null); + newShiftName += (newShiftName.length() > 0 ? " | " : "") + (Objects.equals(dailyRuleVo.getToType(), AttendanceTypeEnum.ORDINARY.getCode()) ? Objects.isNull(toShiftEntity) ? "" : toShiftEntity.getShortName() : Objects.isNull(dailyRuleVo.getToType()) ? "" : AttendanceTypeEnum.getMsg(dailyRuleVo.getToType())); + List attendanceShiftSettingPeriodVos1 = periodMap.get(dailyRuleVo.getToShiftId()); + if (CollUtil.isEmpty(attendanceShiftSettingPeriodVos1) || !Objects.equals(dailyRuleVo.getToType(), AttendanceTypeEnum.ORDINARY.getCode())) { + i++; + continue; + } + if (size < 2) { + for (AttendanceShiftSettingPeriodVo attendanceShiftSettingPeriodVo : attendanceShiftSettingPeriodVos1) { + newShift += (newShift.length() > 0 ? " | " : "") + attendanceShiftSettingPeriodVo.getInPoint() + "至" + attendanceShiftSettingPeriodVo.getOutPoint(); + } + continue; + } + if (i == 0) { + AttendanceShiftSettingPeriodVo attendanceShiftSettingPeriodVo = attendanceShiftSettingPeriodVos1.stream().findFirst().orElse(null); + newShift = attendanceShiftSettingPeriodVo.getInPoint() + "至" + attendanceShiftSettingPeriodVo.getOutPoint(); + } else { + AttendanceShiftSettingPeriodVo attendanceShiftSettingPeriodVo = attendanceShiftSettingPeriodVos1.stream().reduce((p1, p2) -> p2).orElse(null); + newShift += (newShift.length() > 0 ? " | " : "") + attendanceShiftSettingPeriodVo.getInPoint() + "至" + attendanceShiftSettingPeriodVo.getOutPoint(); + } + i++; + } + Map> collect3 = dailyRuleVos.stream() + .sorted(Comparator.comparing(vo -> Objects.isNull(vo.getFromSchedulesType()) ? 1 : vo.getFromSchedulesType())) + .collect(Collectors.groupingBy(vo -> Objects.isNull(vo.getFromSchedulesType()) ? 1 : vo.getFromSchedulesType())); + for (Map.Entry> stringListEntry : collect3.entrySet()) { + DailyRuleResultVo dailyRuleVo = stringListEntry.getValue().stream().findFirst().orElse(null); + if (collect2.containsKey(stringListEntry.getKey())) { + List dailyRuleResultVos1 = collect2.get(stringListEntry.getKey()); + List collect4 = dailyRuleResultVos1.stream().filter(result -> StringUtil.equals(result.getToId(), dailyRuleVo.getFromId())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect4)) { + continue; + } + } + + AttendanceShiftNameEntity fromShiftEntity = shiftMap.getOrDefault(dailyRuleVo.getFromShiftId(), null); + oldShiftName += (oldShiftName.length() > 0 ? " | " : "") + (Objects.equals(dailyRuleVo.getFromType(), AttendanceTypeEnum.ORDINARY.getCode()) && Objects.nonNull(fromShiftEntity) ? fromShiftEntity.getShortName() : Objects.isNull(dailyRuleVo.getFromType()) ? "" : AttendanceTypeEnum.getMsg(dailyRuleVo.getFromType())); + } + shiftChangModels.add(ShiftChangModel.builder().day(day).oldShiftName(oldShiftName).newShiftName(newShiftName).newShift(newShift).build()); + }); + return shiftChangModels.stream().sorted(Comparator.comparing(ShiftChangModel::getDay)).collect(Collectors.toList()); + } + + @Override + public List getRecipient(ShiftChangeNoticeModel attendanceNoticeModel) { + return CollUtil.newArrayList(attendanceNoticeModel.getUserId()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(ShiftChangeNoticeModel attendanceNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + objects.add(JumpUrl.builder().url(String.format(ATTENDANCE_NOTICE_SHIFT_CHANGE_DETAIL_URL, noticeId)).urlName(ATTENDANCE_NOTICE_DETAIL_BUTTON_NAME).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(ATTENDANCE_NOTICE_CLICK_URL).urlName(ATTENDANCE_NOTICE_TO_CLICK).color(COLOR_BLUE).build()); + return SystemNoticeStrategy.builder() + .title(ATTENDANCE_NOTICE_SHIFT_CHANGE_TITLE) + .noticeModuleEnum(NoticeModuleEnum.BC_BD) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + UserInfo user = UserProvider.getUser(); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(user.getUserId()), user.getTenantId()); + if (CollUtil.isEmpty(allByIds)) { + log.error("请求用户服务失败或者未查询到当前用户数据:{}", user.getUserId()); + return null; + } + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + StringBuffer stringBuffer = new StringBuffer(); + if (Objects.isNull(partUserInfoVo)) { + stringBuffer.append("操作人:无
"); + } else { + stringBuffer.append("操作人:").append(partUserInfoVo.getRealName()).append("(").append(StringUtil.isEmpty(partUserInfoVo.getOrganizeName()) ? "暂无" : partUserInfoVo.getOrganizeName()).append("-").append(StringUtil.isEmpty(partUserInfoVo.getPositionName()) ? "暂无" : partUserInfoVo.getPositionName()).append(")
"); + } + if (data.size() > 1) { + stringBuffer.append("变更日期:").append(StringUtil.join(data.stream().map(shiftChangModel1 -> DateUtil.format(shiftChangModel1.getDay(), "yyyy.MM.dd")).collect(Collectors.toList()), "、")).append("
"); + return stringBuffer.toString(); + } + ShiftChangModel shiftChangModel = data.stream().findFirst().orElse(null); + stringBuffer.append("变更日期:").append(DateUtil.format(shiftChangModel.getDay(), "yyyy.MM.dd")).append("
"); + stringBuffer.append("新班次名称:").append(StringUtil.isEmpty(shiftChangModel.getNewShiftName()) ? "无" : shiftChangModel.getNewShiftName()).append("
"); + stringBuffer.append("新班次时间:").append(StringUtil.isEmpty(shiftChangModel.getNewShift()) ? "无" : shiftChangModel.getNewShift()); + return stringBuffer.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SystemTypeChangeAttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SystemTypeChangeAttendanceNotice.java new file mode 100644 index 0000000..b77402b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/SystemTypeChangeAttendanceNotice.java @@ -0,0 +1,89 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.AttendanceGroup; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.model.SystemTypeChangeModel; +import jnpf.model.attendance.model.SystemTypeChangeNoticeModel; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; + +import static jnpf.constants.AttendanceConstant.ATTENDANCE_NOTICE_ADMIN_RULE_CHANGE_TITLE; +import static jnpf.constants.AttendanceConstant.ATTENDANCE_NOTICE_RULE_CHANGE_TITLE; + +@Component(AttendanceConstant.RULE_CHANGE_SHIFT_SYSTEM) +public class SystemTypeChangeAttendanceNotice implements AttendanceNotice { + + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + + @Override + public Boolean isPass(SystemTypeChangeNoticeModel attendanceNoticeModel) { + if (!attendanceNoticeModel.getAttendanceNoticeEnum().equals(AttendanceNoticeEnum.RULE_CHANGE_SHIFT_SYSTEM)) { + return Boolean.FALSE; + } + return Boolean.TRUE; + } + + @Override + public List generatingData(SystemTypeChangeNoticeModel attendanceNoticeModel) { + AttendanceGroup byId = attendanceGroupService.getById(attendanceNoticeModel.getGroupId()); + return CollUtil.newArrayList(SystemTypeChangeModel.builder() + .groupName(byId.getGroupName()) + .oldSystemType(Objects.equals(attendanceNoticeModel.getSystemType(), 2) ? "固定班制" : "排班制") + .newSystemType(Objects.equals(attendanceNoticeModel.getSystemType(), 1) ? "固定班制" : "排班制") + .build()); + } + + @Override + public List getRecipient(SystemTypeChangeNoticeModel attendanceNoticeModel) { + return attendanceNoticeHandler.getRecipientForSelfAndChild(attendanceNoticeModel.getGroupId(), attendanceNoticeModel.getIsAdmin()); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(SystemTypeChangeNoticeModel attendanceNoticeModel, String noticeId) { + return SystemNoticeStrategy.builder() + .noticeModuleEnum(NoticeModuleEnum.KQZ_BD) + .title(attendanceNoticeModel.getIsAdmin() ? ATTENDANCE_NOTICE_ADMIN_RULE_CHANGE_TITLE : ATTENDANCE_NOTICE_RULE_CHANGE_TITLE) + .isConfirm(Boolean.TRUE) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + UserInfo user = UserProvider.getUser(); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(user.getUserId()), user.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + StringBuffer stringBuffer = new StringBuffer(); + SystemTypeChangeModel systemTypeChangeModel = data.stream().findFirst().orElse(null); + stringBuffer.append("考勤组:").append(systemTypeChangeModel.getGroupName()).append("
"); + if (Objects.isNull(partUserInfoVo)) { + stringBuffer.append("操作人:无
"); + } else { + stringBuffer.append("操作人:").append(partUserInfoVo.getRealName()).append("(").append(StringUtil.isEmpty(partUserInfoVo.getOrganizeName()) ? "暂无" : partUserInfoVo.getOrganizeName()).append("-").append(StringUtil.isEmpty(partUserInfoVo.getPositionName()) ? "暂无" : partUserInfoVo.getPositionName()).append(")
"); + } + stringBuffer.append("考勤组类型已由").append(systemTypeChangeModel.getOldSystemType()).append("改为").append(systemTypeChangeModel.getNewSystemType()); + return stringBuffer.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/UserChangAttendanceNotice.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/UserChangAttendanceNotice.java new file mode 100644 index 0000000..ed66691 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notice/UserChangAttendanceNotice.java @@ -0,0 +1,177 @@ +package jnpf.attendance.service.handle.notice; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceManagerPermissionMapper; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.AttendanceGroup; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.NoticeModuleEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.model.attendance.model.JumpUrl; +import jnpf.model.attendance.model.SystemNoticeStrategy; +import jnpf.model.attendance.model.UserChangModel; +import jnpf.model.attendance.model.UserChangeNoticeModel; +import jnpf.model.attendance.vo.AttendanceGroupAdminVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.DateUtil; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static jnpf.constants.AttendanceConstant.*; +import static jnpf.enums.attendance.AttendanceNoticeEnum.*; +import static jnpf.enums.attendance.UserSettingEnum.*; + +/** + * 考勤组人员变动通知 + */ +@Component(AttendanceConstant.JOIN_GROUP) +public class UserChangAttendanceNotice implements AttendanceNotice { + @Autowired + private UserAntifreeze userAntifreeze; + + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper; + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + public Boolean isPass(UserChangeNoticeModel attendanceNoticeModel) { + return Boolean.TRUE; + } + + @Override + public List generatingData(UserChangeNoticeModel attendanceNoticeModel) { + List shiftChangModels = CollUtil.newArrayList(); + + String tips = ""; + AttendanceGroup byId = null; + List realNames = null; + if (Objects.equals(AttendanceNoticeEnum.REMOVE_USER_REMOVE_GROUP, attendanceNoticeModel.getAttendanceNoticeEnum()) + || Objects.equals(AttendanceNoticeEnum.REMOVE_USER, attendanceNoticeModel.getAttendanceNoticeEnum())) { + tips = "请尽快联系管理员加入考勤组,以确保可以使用考勤打卡"; + } else { + //获取管理员 + List attendanceGroupAdminVos = attendanceManagerPermissionMapper.queryGroupUser(attendanceNoticeModel.getJoinGroupId()); + List adminUserIds = attendanceGroupAdminVos.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); + byId = attendanceGroupService.getById(attendanceNoticeModel.getJoinGroupId()); + Assert.isFalse(Objects.isNull(byId), "变更考勤组通知,未查询到考勤组信息[{}]", attendanceNoticeModel.getJoinGroupId()); + List allByIds = CollUtil.isEmpty(adminUserIds) ? CollUtil.newArrayList() : userAntifreeze.getAllByIds(adminUserIds, attendanceNoticeModel.getTenantId()); + realNames = allByIds.stream().map(PartUserInfoVo::getRealName).collect(Collectors.toList()); + } + + shiftChangModels.add(UserChangModel.builder() + .type(attendanceNoticeModel.getAttendanceNoticeEnum().getCode()) + .administratorList(CollUtil.isEmpty(realNames) ? "" : StringUtil.join(realNames, "、")) + .joinGroup(Objects.isNull(byId) ? "" : byId.getGroupName()) + .start(Objects.isNull(attendanceNoticeModel.getStart()) ? "" : DateUtil.dateToString(attendanceNoticeModel.getStart(), "yyyy年MM月dd日 HH:mm")) + .reason(getReason(attendanceNoticeModel.getAttendanceNoticeEnum(), Objects.isNull(byId) ? "" : byId.getGroupName())) + .tips(tips) + .build()); + return shiftChangModels; + } + + /** + * @param attendanceNoticeEnum + * @param groupName + * @return + */ + private String getReason(AttendanceNoticeEnum attendanceNoticeEnum, String groupName) { + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.JOIN_GROUP)) { + return ""; + } + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.GROUP_CHANGE_REMOVE_JOIN_GROUP)) { + return "原考勤组已解散,已加入新考勤组!"; + } + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.REMOVE_USER_REMOVE_GROUP)) { + return "原考勤组已解散"; + } + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.REMOVE_USER)) { + return "已从考勤组中移除"; + } + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.GROUP_CHANGE_PROMOTION)) { + return String.format("晋升已产生,已加入【%s】", groupName); + } + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.GROUP_CHANGE_TRANSFER_POSITION)) { + return String.format("调岗已产生,已加入【%s】", groupName); + } + if (Objects.equals(attendanceNoticeEnum, AttendanceNoticeEnum.GROUP_CHANGE_SECONDMENT)) { + return String.format("借调申请已通过,借调至【%s】", groupName); + } + return ""; + } + + @Override + public List getRecipient(UserChangeNoticeModel attendanceNoticeModel) { + UserSettingEnum userSettingEnum = AttendanceNoticeEnum.JOIN_GROUP.equals(attendanceNoticeModel.getAttendanceNoticeEnum()) || REMOVE_USER.equals(attendanceNoticeModel.getAttendanceNoticeEnum()) || AttendanceNoticeEnum.REMOVE_USER_REMOVE_GROUP.equals(attendanceNoticeModel.getAttendanceNoticeEnum()) ? ATTENDANCE_SETTING_ATTENDANCE_GROUP_JOIN_LEAVE_REMINDER : ATTENDANCE_SETTING_ATTENDANCE_GROUP_CHANGE_REMINDER; + return attendanceNoticeHandler.getUserIdsForFilterStatus(attendanceNoticeModel.getUserIds(), UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE, userSettingEnum); + } + + @Override + public SystemNoticeStrategy setSystemNoticeStrategy(UserChangeNoticeModel attendanceNoticeModel, String noticeId) { + ArrayList objects = CollUtil.newArrayList(); + String title = ATTENDANCE_NOTICE_GROUP_CHANGE_TITLE; + if (AttendanceNoticeEnum.JOIN_GROUP.equals(attendanceNoticeModel.getAttendanceNoticeEnum())) { + title = ATTENDANCE_NOTICE_JOIN_GROUP_CHANGE_TITLE; + objects.add(JumpUrl.builder().url(ATTENDANCE_NOTICE_RULE_URL).urlName(ATTENDANCE_NOTICE_DETAIL_BUTTON_NAME).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(ATTENDANCE_NOTICE_CLICK_URL).urlName(ATTENDANCE_NOTICE_TO_CLICK).color(COLOR_BLUE).build()); + } else if (REMOVE_USER.equals(attendanceNoticeModel.getAttendanceNoticeEnum())) { + title = ATTENDANCE_NOTICE_REMOVE_USER_TITLE; + objects.add(JumpUrl.builder().url(String.format(ATTENDANCE_NOTICE_GROUP_CHANGES_URL, noticeId)).urlName(ATTENDANCE_NOTICE_DETAIL_BUTTON_NAME).color(COLOR_BLACK).build()); + } else if (REMOVE_USER_REMOVE_GROUP.equals(attendanceNoticeModel.getAttendanceNoticeEnum())) { + title = ATTENDANCE_NOTICE_REMOVE_USER_TITLE; + objects.add(JumpUrl.builder().url(String.format(ATTENDANCE_NOTICE_GROUP_CHANGES_URL, noticeId)).urlName(ATTENDANCE_NOTICE_DETAIL_BUTTON_NAME).color(COLOR_BLACK).build()); + } else { + objects.add(JumpUrl.builder().url(ATTENDANCE_NOTICE_RULE_URL).urlName(ATTENDANCE_NOTICE_DETAIL_BUTTON_NAME).color(COLOR_BLACK).build()); + objects.add(JumpUrl.builder().url(ATTENDANCE_NOTICE_CLICK_URL).urlName(ATTENDANCE_NOTICE_TO_CLICK).color(COLOR_BLUE).build()); + } + return SystemNoticeStrategy.builder() + .title(title) + .noticeModuleEnum(NoticeModuleEnum.KQZ_BD) + .jumpUrls(objects) + .build(); + } + + @Override + public String getContext(List data) { + if (CollUtil.isEmpty(data)) { + return null; + } + StringBuffer stringBuffer = new StringBuffer(); + UserChangModel shiftChangModel = data.stream().findFirst().orElse(null); + if (StringUtil.isNotEmpty(shiftChangModel.getReason())) { + stringBuffer.append("变动原因:").append(shiftChangModel.getReason()).append("
"); + } + if (AttendanceNoticeEnum.JOIN_GROUP.getCode().equals(shiftChangModel.getType())) { + stringBuffer.append("加入考勤组:").append(shiftChangModel.getJoinGroup()).append("
"); + stringBuffer.append("考勤组管理员:").append(StringUtil.isNotEmpty(shiftChangModel.getAdministratorList()) ? shiftChangModel.getAdministratorList() : "暂无管理员").append("
"); + } + if (AttendanceNoticeEnum.GROUP_CHANGE_REMOVE_JOIN_GROUP.getCode().equals(shiftChangModel.getType())) { + stringBuffer.append("新考勤组:").append(shiftChangModel.getJoinGroup()).append("
"); + stringBuffer.append("考勤组管理员:").append(StringUtil.isNotEmpty(shiftChangModel.getAdministratorList()) ? shiftChangModel.getAdministratorList() : "暂无管理员").append("
"); + } + if (AttendanceNoticeEnum.GROUP_CHANGE_SECONDMENT.getCode().equals(shiftChangModel.getType())) { + stringBuffer.append("开始时间:").append(shiftChangModel.getStart()).append("
"); + } + if (AttendanceNoticeEnum.GROUP_CHANGE_TRANSFER_POSITION.getCode().equals(shiftChangModel.getType()) + || AttendanceNoticeEnum.GROUP_CHANGE_PROMOTION.getCode().equals(shiftChangModel.getType())) { + stringBuffer.append("考勤组管理员:").append(StringUtil.isNotEmpty(shiftChangModel.getAdministratorList()) ? shiftChangModel.getAdministratorList() : "暂无管理员").append("
"); + } + if (StringUtil.isNotEmpty(shiftChangModel.getTips())) { + stringBuffer.append(shiftChangModel.getTips()); + } + return stringBuffer.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/AttendanceRuleNotificationHandle.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/AttendanceRuleNotificationHandle.java new file mode 100644 index 0000000..983b138 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/AttendanceRuleNotificationHandle.java @@ -0,0 +1,328 @@ +package jnpf.attendance.service.handle.notification; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.DailyRuleChangeService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.UserInfo; +import jnpf.constants.MessageTopicConstants; +import jnpf.entity.attendance.DailyRuleChange; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.TriggerSceneEnum; +import jnpf.model.attendance.dto.LineDrawingPeriodDto; +import jnpf.model.attendance.dto.LineDrawingSchedulesConfigDto; +import jnpf.model.attendance.event.StatisticsBatchClearDto; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.model.LineShiftChangeNoticeModel; +import jnpf.model.attendance.model.ShiftChangeNoticeModel; +import jnpf.model.attendance.model.SystemTypeChangeNoticeModel; +import jnpf.model.attendance.vo.AttendanceShiftSettingVo; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.model.attendance.vo.UserDayVo; +import jnpf.util.DateDetail; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.attendance.DailyRuleUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class AttendanceRuleNotificationHandle { + @Resource + private RocketMQTemplate rocketMqTemplate; + @Autowired + private AttendanceClockInService attendanceClockInService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private DailyRuleChangeService dailyRuleChangeService; + + public void notificationHandle(List resultList, String tenantId, Boolean isSync, Boolean isNotificationClock) { + UserInfo user = UserProvider.getUser(); + if (StringUtil.isNotNull(tenantId)) { + user.setTenantId(tenantId); + } + if (isSync) { + notificationClockHandleSync(resultList, user, isNotificationClock); + return; + } + notificationMqHandle(resultList, user, isNotificationClock); + } + + public void notificationHandle(List resultList, Boolean isSync, Boolean isNotificationClock) { + notificationHandle(resultList, null, isSync, isNotificationClock); + } + + public void notificationHandle(List resultList, Boolean isSync) { + notificationHandle(resultList, isSync, Boolean.TRUE); + } + + public void notificationHandle(List resultList, String tenantId) { + notificationHandle(resultList, tenantId, Boolean.FALSE, Boolean.TRUE); + } + + + public void notificationClearHandle(String tenantId, String fromGroupId, List userIds, Date day) { + if (CollUtil.isEmpty(userIds)) { + return; + } + //调用批量清除日统计数据接口s + StatisticsBatchClearDto batchClearDto = StatisticsBatchClearDto.builder() + .tenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId) + .groupId(fromGroupId) + .userIdList(userIds) + .startDay(day) + .build(); + Message clearMessage = MessageBuilder.withPayload(batchClearDto).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_BATCH_CLEAR_TOPIC, clearMessage, 3000L, 2); + } + + + /** + * 规则变动发送通知 + * + * @param resultList 规则变动结果 + * @param tenantId 租户id + */ + @Async("threadPoolTaskExecutor") + public void sendSystemNotice(List resultList, String tenantId) { + resultList.stream() + .filter(dailyRuleResultVo -> Objects.nonNull(dailyRuleResultVo.getFromType()) + && (Objects.equals(dailyRuleResultVo.getFromType(), AttendanceTypeEnum.ORDINARY.getCode()) + || Objects.equals(dailyRuleResultVo.getFromType(), AttendanceTypeEnum.REST.getCode()) + || Objects.equals(dailyRuleResultVo.getFromType(), AttendanceTypeEnum.HOLIDAYS.getCode()) + || Objects.equals(dailyRuleResultVo.getFromType(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) + || Objects.equals(dailyRuleResultVo.getFromType(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())) + && (Objects.equals(dailyRuleResultVo.getToType(), AttendanceTypeEnum.ORDINARY.getCode()) + || Objects.equals(dailyRuleResultVo.getToType(), AttendanceTypeEnum.REST.getCode()) + || Objects.equals(dailyRuleResultVo.getToType(), AttendanceTypeEnum.HOLIDAYS.getCode()) + || Objects.equals(dailyRuleResultVo.getToType(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) + || Objects.equals(dailyRuleResultVo.getToType(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()) + || Objects.isNull(dailyRuleResultVo.getToType()))).collect(Collectors.groupingBy(DailyRuleResultVo::getUserId)) + .forEach((userId, results) -> { + ShiftChangeNoticeModel shiftChangeNoticeModel = new ShiftChangeNoticeModel(); + shiftChangeNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + shiftChangeNoticeModel.setUserId(userId); + shiftChangeNoticeModel.setDailyRuleResultVos(results); + shiftChangeNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.SHIFT_CHANG); + attendanceNoticeHandler.send(shiftChangeNoticeModel); + }); + } + + @Async("threadPoolTaskExecutor") + public void sendNoticeBatch(List selfAndChildrenGroupIds, Integer systemType) { + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNotice(itemGroupId, systemType)); + } + + private void sendNotice(String groupId, Integer systemType) { + SystemTypeChangeNoticeModel systemTypeChangeNoticeModel = new SystemTypeChangeNoticeModel(); + systemTypeChangeNoticeModel.setSystemType(systemType); + systemTypeChangeNoticeModel.setGroupId(groupId); + systemTypeChangeNoticeModel.setIsAdmin(Boolean.FALSE); + systemTypeChangeNoticeModel.setTenantId(UserProvider.getUser().getTenantId()); + systemTypeChangeNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.RULE_CHANGE_SHIFT_SYSTEM); + attendanceNoticeHandler.send(systemTypeChangeNoticeModel); + systemTypeChangeNoticeModel.setIsAdmin(Boolean.TRUE); + attendanceNoticeHandler.send(systemTypeChangeNoticeModel); + } + + public void sendSystemNoticeForLine(List resultList, LineDrawingSchedulesConfigDto configDto, String tenantId) { + sendSystemNoticeForLine(resultList, configDto, null, tenantId); + } + + /** + * 划线排班规则变动发送通知 + * + * @param configDto 规则 + * @param tenantId 租户id + */ + public void sendSystemNoticeForLine(List resultList, LineDrawingSchedulesConfigDto configDto, List days, String tenantId) { + LineShiftChangeNoticeModel shiftChangeNoticeModel = new LineShiftChangeNoticeModel(); + shiftChangeNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.LINE_SHIFT_CHANG); + shiftChangeNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + List daysList = CollUtil.isNotEmpty(days) ? days : Arrays.stream(StringUtil.split(configDto.getDays(), ",")).map(jnpf.util.DateUtil::stringToDates).collect(Collectors.toList()); + Map> collect = resultList.stream().collect(Collectors.groupingBy(vo -> vo.getUserId() + DateDetail.getDate2Str(vo.getDate(), DateDetail.DF))); + configDto.getUserList() + .forEach(user -> { + List periods = user.getPeriods(); + if (CollUtil.isEmpty(periods) && CollUtil.isEmpty(resultList)) { + return; + } + if (CollUtil.isNotEmpty(periods) && periods.stream().allMatch(vo -> StringUtil.isNotEmpty(vo.getId()))) { + return; + } + List periodList = CollUtil.newArrayList(); + List collect1 = periods.stream().map(LineDrawingPeriodDto::getDay).distinct().sorted().collect(Collectors.toList()); + if (CollUtil.isEmpty(collect1)) { + collect1 = daysList; + } + collect1.forEach(day -> { + String key = user.getUserId() + DateDetail.getDate2Str(day, DateDetail.DF); + if (!collect.containsKey(key)) { + return; + } + if (CollUtil.isEmpty(periods)) { + periodList.add(LineDrawingPeriodDto.builder() + .day(day) + .start("") + .build()); + return; + } + periodList.addAll(BeanUtil.copyToList(periods, LineDrawingPeriodDto.class).stream().peek(vo -> vo.setDay(day)).collect(Collectors.toList())); + }); + if (CollUtil.isEmpty(periodList)) { + return; + } + shiftChangeNoticeModel.setUserIds(List.of(user.getUserId())); + shiftChangeNoticeModel.setPeriods(periodList); + attendanceNoticeHandler.send(shiftChangeNoticeModel); + }); + } + + /** + * 划线排班规则变动发送通知 + * + * @param rules 规则 + * @param tenantId 租户id + */ + @Async("threadPoolTaskExecutor") + public void sendSystemNoticeForLine2(List resultList, List rules, String tenantId) { + Map> collect = resultList.stream().collect(Collectors.groupingBy(vo -> vo.getUserId() + DateDetail.getDate2Str(vo.getDate(), DateDetail.DF))); + LineShiftChangeNoticeModel shiftChangeNoticeModel = new LineShiftChangeNoticeModel(); + shiftChangeNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + shiftChangeNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.LINE_SHIFT_CHANG); + rules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))) + .forEach((key, userRules) -> { + FtbAttendanceDailyRule rule = userRules.stream().findFirst().orElse(null); + if (Objects.isNull(rule)) { + return; + } + if (!collect.containsKey(key)) { + return; + } + DateTime tomorrow = DateUtil.offsetDay(rule.getDay(), 1); + List orDefault = collect.getOrDefault(key, List.of()); + boolean isClear = orDefault.stream().allMatch(vo -> Objects.isNull(vo.getType())); + List periods = DailyRuleUtil.mergeRules(userRules).stream().map(dayRule -> LineDrawingPeriodDto.builder() + .day(dayRule.getDay()) + .startType(dayRule.getInPoint().after(tomorrow) ? 2 : 1) + .start(isClear ? "" : jnpf.util.DateUtil.dateToString(dayRule.getInPoint(), "HH:mm")) + .startType(dayRule.getOutPoint().after(tomorrow) ? 2 : 1) + .end(isClear ? "" : jnpf.util.DateUtil.dateToString(dayRule.getOutPoint(), "HH:mm")) + .build()).collect(Collectors.toList()); + shiftChangeNoticeModel.setUserIds(List.of(rule.getUserId())); + shiftChangeNoticeModel.setPeriods(periods); + shiftChangeNoticeModel.setDay(rule.getDay()); + attendanceNoticeHandler.send(shiftChangeNoticeModel); + }); + } + + + private void notificationClockHandleSync(List resultList, UserInfo user, boolean isNotificationClock) { + if (isNotificationClock) { + try { + // 出勤变更通知 + List userDayList = resultList.stream() + .map(vo -> new UserDayVo(user.getTenantId(), vo.getGroupId(), vo.getUserId(), vo.getDate(), DateDetail.getDate2Str(vo.getDate(), DateDetail.DF))) + .collect(Collectors.collectingAndThen( + // 去除重复记录, 冲突时保留第一条 + Collectors.toMap(u -> u.getUserId() + "_" + u.getDayStr(), Function.identity(), (existing, replacement) -> existing), + map -> new ArrayList<>(map.values()) + )); + dailyRuleChangeService.saveRecordBatch(userDayList.stream().map(userDayVo -> new DailyRuleChange(userDayVo.getGroupId(), userDayVo.getUserId(), userDayVo.getDay(), userDayVo.getTenantId())).collect(Collectors.toList())); + attendanceClockInService.changeAttendanceRuleBatch(userDayList, user); + } catch (Exception e) { + log.error("排班通知打卡失败", e); + } + } + //调用批量生成日统计数据接口 + Map> groupMap = resultList.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getGroupId)); + groupMap.forEach((groupId, dayList) -> { + Map> dayMap = dayList.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getDate)); + dayMap.forEach((date, userList) -> { + notificationClearHandle(user.getTenantId(), groupId, userList.stream().filter(vo -> Objects.isNull(vo.getToType())).map(DailyRuleResultVo::getUserId).distinct().collect(Collectors.toList()), date); + notificationStatistics(user.getTenantId(), groupId, date, userList.stream().map(DailyRuleResultVo::getUserId).distinct().collect(Collectors.toList())); + }); + }); + + } + + private void notificationMqHandle(List resultList, UserInfo user, boolean isNotificationClock) { + if (isNotificationClock) { + notificationClockMq(resultList, user); + } + //调用批量生成日统计数据接口 + Map> groupMap = resultList.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getGroupId)); + groupMap.forEach((groupId, dayList) -> { + Map> dayMap = dayList.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getDate)); + dayMap.forEach((date, userList) -> { + notificationClearHandle(user.getTenantId(), groupId, userList.stream().filter(vo -> Objects.isNull(vo.getToType())).map(DailyRuleResultVo::getUserId).distinct().collect(Collectors.toList()), date); + notificationStatistics(user.getTenantId(), groupId, date, userList.stream().map(DailyRuleResultVo::getUserId).distinct().collect(Collectors.toList())); + }); + }); + } + + public void notificationClockMq(List resultList, UserInfo user) { + //调用生成日统计数据接口 + // 出勤变更通知 + List userDayList = resultList.stream() + .map(vo -> new UserDayVo(user.getTenantId(), vo.getGroupId(), vo.getUserId(), vo.getDate(), DateDetail.getDate2Str(vo.getDate(), DateDetail.DF))) + .collect(Collectors.collectingAndThen( + // 去除重复记录, 冲突时保留第一条 + Collectors.toMap(u -> u.getUserId() + "_" + u.getDayStr(), Function.identity(), (existing, replacement) -> existing), + map -> new ArrayList<>(map.values()) + )); + Message> message = MessageBuilder.withPayload(userDayList).build(); + try { + dailyRuleChangeService.saveRecordBatch(userDayList.stream().map(userDayVo -> new DailyRuleChange(userDayVo.getGroupId(), userDayVo.getUserId(), userDayVo.getDay(), userDayVo.getTenantId())).collect(Collectors.toList())); + SendResult sendResult = rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_NOTIFICATION_CLOCK_TOPIC, message, 3000L, 3); + Assert.isFalse(!sendResult.getSendStatus().equals(SendStatus.SEND_OK), "排班通知打卡异常重试"); + } catch (Exception e) { + log.error("AttendanceRuleNotificationHandle#notificationClockMq 排班通知打卡异常:{}", JSON.toJSONString(message), e); + throw new RuntimeException("排班通知打卡异常", e); + } + } + + + public void notificationStatistics(String tenantId, String groupId, Date date, List userList) { + //调用生成日统计数据接口 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .day(date) + .triggerSceneEnum(TriggerSceneEnum.SCHEDULES) + .build(); + userList.forEach(item -> { + courseEventDTO.setUserId(item); + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + if (StrUtil.isBlank(tenantId) || StrUtil.isBlank(groupId)) { + log.error("AttendanceRuleNotificationHandle#notificationStatistics 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + return; + } + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + }); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/MultiTenantTimeSizeBatchProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/MultiTenantTimeSizeBatchProcessor.java new file mode 100644 index 0000000..a18a724 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/MultiTenantTimeSizeBatchProcessor.java @@ -0,0 +1,206 @@ +package jnpf.attendance.service.handle.notification; + +import jnpf.config.CustomTenantUtil; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; + +@Component +@Slf4j +public class MultiTenantTimeSizeBatchProcessor { + // 租户处理器映射 + private final Map> tenantHandlers = new ConcurrentHashMap<>(); + + + // 共享资源管理 + private final TenantResourceManager tenantResourceManager; + private final CustomTenantUtil customTenantUtil; + + // 定期清理不活跃租户的调度器 + private final ScheduledExecutorService cleanupScheduler = Executors.newScheduledThreadPool(1); + + public MultiTenantTimeSizeBatchProcessor( + TenantResourceManager tenantResourceManager, + CustomTenantUtil customTenantUtil) { + this.tenantResourceManager = tenantResourceManager; + this.customTenantUtil = customTenantUtil; + + // 启动定期清理任务 + cleanupScheduler.scheduleAtFixedRate( + this::cleanupInactiveHandlers, 30, 30, TimeUnit.MINUTES); + } + + /** + * 添加数据到批处理队列 + * @param tenantId 租户ID + * @param data 要处理的数据 + */ + public void addData(String tenantId, T data) { + // 获取或创建租户处理器 + TenantBatchHandler handler = tenantHandlers.computeIfAbsent( + tenantId, + id -> new TenantBatchHandler<>(id, tenantResourceManager, customTenantUtil) + ); + + // 添加数据到处理器 + handler.addData(data); + } + + /** + * 清理不活跃的租户处理器 + */ + private void cleanupInactiveHandlers() { + long currentTime = System.currentTimeMillis(); + long inactiveThreshold = 30 * 60 * 1000; // 30分钟不活跃 + + tenantHandlers.entrySet().removeIf(entry -> { + TenantBatchHandler handler = entry.getValue(); + if (currentTime - handler.getLastActiveTime() > inactiveThreshold) { + handler.cleanup(); + log.info("Cleaned up inactive tenant handler: {}", entry.getKey()); + return true; + } + return false; + }); + } + + @PreDestroy + public void shutdown() { + cleanupScheduler.shutdown(); + tenantHandlers.values().forEach(TenantBatchHandler::cleanup); + log.info("MultiTenantTimeSizeBatchProcessor shutdown completed"); + } + + /** + * 租户专用批处理器 - 实现时间和数量双重触发机制 + */ + private static class TenantBatchHandler { + // 数据缓冲区 + private final List buffer = new ArrayList<>(); + private final Object lock = new Object(); + + // 定时器管理 + private final ScheduledExecutorService scheduler; + private ScheduledFuture currentTimer; + + // 租户信息 + private final String tenantId; + private final TenantResourceManager tenantResourceManager; + private final CustomTenantUtil customTenantUtil; + @Getter + private volatile long lastActiveTime; + + // 批处理配置 - 根据租户规模动态调整 + private final int batchSize; + private final long timeInterval; + + public TenantBatchHandler(String tenantId, + TenantResourceManager tenantResourceManager, + CustomTenantUtil customTenantUtil) { + this.tenantId = tenantId; + this.tenantResourceManager = tenantResourceManager; + this.customTenantUtil = customTenantUtil; + this.lastActiveTime = System.currentTimeMillis(); + + // 为小租户优化配置 + this.batchSize = 50; // 小批次 + this.timeInterval = 3000; // 3秒超时 + + // 创建专用调度器 + this.scheduler = Executors.newSingleThreadScheduledExecutor( + r -> new Thread(r, "tenant-" + tenantId + "-batch-scheduler")); + } + + /** + * 添加数据到批处理队列 + */ + public void addData(T data) { + synchronized (lock) { + buffer.add(data); + lastActiveTime = System.currentTimeMillis(); + + // 双重触发机制 + if (buffer.size() >= batchSize) { + // 数量触发:达到批次大小立即处理 + log.debug("Tenant {} batch triggered by size: {}", tenantId, buffer.size()); + processBatch(); + } else if (currentTimer == null || currentTimer.isDone()) { + // 时间触发:启动定时器 + log.debug("Tenant {} batch timer started", tenantId); + currentTimer = scheduler.schedule( + this::processBatch, timeInterval, TimeUnit.MILLISECONDS); + } + } + } + + /** + * 处理批次数据 + */ + private void processBatch() { + synchronized (lock) { + if (buffer.isEmpty()) return; + + // 获取当前批次数据 + List batch = new ArrayList<>(buffer); + buffer.clear(); + + // 取消当前定时器 + if (currentTimer != null && !currentTimer.isDone()) { + currentTimer.cancel(false); + currentTimer = null; + } + + // 使用共享线程池异步处理 + CompletableFuture.runAsync(() -> { + processData(batch); + }, tenantResourceManager.getSharedThreadPool()); + } + } + + /** + * 实际数据处理逻辑 + */ + private void processData(List data) { + try { + lastActiveTime = System.currentTimeMillis(); + // 关键:切换到租户数据库 + customTenantUtil.checkOutTenant(tenantId); + // 执行实际的业务处理 + executeBatchOperation(data); + } catch (Exception e) { + log.error("Error processing batch for tenant " + tenantId, e); + // 可以添加重试机制或死信队列处理 + } + } + + /** + * 执行批量数据库操作 + */ + private void executeBatchOperation(List data) { + try { + // 批量插入或更新操作 + + + } catch (Exception e) { + log.error("Database operation failed for tenant " + tenantId, e); + throw new RuntimeException("Batch operation failed", e); + } + } + /** + * 清理资源 + */ + public void cleanup() { + if (currentTimer != null && !currentTimer.isDone()) { + currentTimer.cancel(true); + } + scheduler.shutdown(); + buffer.clear(); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/TenantResourceManager.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/TenantResourceManager.java new file mode 100644 index 0000000..d0142f7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/notification/TenantResourceManager.java @@ -0,0 +1,54 @@ +package jnpf.attendance.service.handle.notification; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +@Slf4j +public class TenantResourceManager { + // 共享线程池 - 避免为每个小租户创建独立线程池 + private final ThreadPoolExecutor sharedThreadPool; + + public TenantResourceManager() { + // 优化的线程池配置 + this.sharedThreadPool = new ThreadPoolExecutor( + 20, // 核心线程数 + 100, // 最大线程数 + 60L, // 空闲线程存活时间 + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(10000), // 队列大小 + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "tenant-shared-thread-" + threadNumber.getAndIncrement()); + t.setDaemon(false); + return t; + } + }, + new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 + ); + } + + public Executor getSharedThreadPool() { + return sharedThreadPool; + } + + @PreDestroy + public void shutdown() { + sharedThreadPool.shutdown(); + try { + if (!sharedThreadPool.awaitTermination(30, TimeUnit.SECONDS)) { + sharedThreadPool.shutdownNow(); + } + } catch (InterruptedException e) { + sharedThreadPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + log.info("TenantResourceManager shutdown completed"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/LeaveRuleProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/LeaveRuleProcessor.java new file mode 100644 index 0000000..212e1e2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/LeaveRuleProcessor.java @@ -0,0 +1,932 @@ +package jnpf.attendance.service.handle.rule; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.RuleProcessor; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.attendance.LeaveParam; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.LeaveUnitEnum; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 处理历史请假申请 + */ +@Component +@Slf4j +public class LeaveRuleProcessor extends RuleProcessor { + + @Override + public void ruleArrangementHandle(List hisDailyRules, List currDailyRule, List resultList) { + Map> collect = hisDailyRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getAttendanceType)); + List ftbAttendanceDailyRules = collect.getOrDefault(AttendanceTypeEnum.LEAVE.getCode(), List.of()).stream().filter(rule -> Objects.isNull(rule.getIsInsert()) || !rule.getIsInsert()).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + Iterator iterator = currDailyRule.iterator(); + while (iterator.hasNext()) { + FtbAttendanceDailyRule rule = iterator.next(); + //新增排休 + if (restHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增上班 + if (ordinaryHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增加班 + if (workOvertimeHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增请假 + if (leaveHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增出差、外出 + if (stepOutHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增清除 + clearHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList); + } + } + if (Objects.nonNull(nextRuleProcessor)) { + nextRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList); + } + } + + private Boolean clearHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增清除 + if (!Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + oldRule.setShiftId(""); + oldRule.setInPoint(oldRule.getApplyStart()); + oldRule.setOutPoint(oldRule.getApplyEnd()); + oldRule.setApplyViewEnable(2); + oldRule.setLeaveDay(BigDecimal.ZERO); + oldRule.setPayrollHours(BigDecimal.ZERO); + oldRule.setValidDuration(0); + + }); + return Boolean.TRUE; + } + + /** + * 新增外出、出差申请 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean stepOutHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增外出、出差申请 + if (!Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType()) && !Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //过滤隐藏规则 + if (Objects.equals(oldRule.getApplyViewEnable(), 0)) { + return; + } + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + //原小时外出没命中已有班次数据,隐藏外出申请 + if (rule.getOutPoint().before(oldRule.getInPoint()) + || rule.getInPoint().after(oldRule.getOutPoint()) + || rule.getInPoint().after(oldRule.getInPoint()) && rule.getOutPoint().before(oldRule.getOutPoint())) { + if (!rule.getIsInsert()) { + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + } + log.error("新增排班数据未命中小时外出申请或者外出数据完全被排班数据覆盖,过滤{}", JSON.toJSONString(rule)); + return; + } + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + rule.setApplyViewEnable(0); + oldRule.setApplyViewEnable(rule.getAttendanceType()); + //原小时外出数据覆盖现在新增排班数据上班时间 + if (!rule.getInPoint().after(oldRule.getInPoint())) { + oldRule.setInStepOutType(1); + oldRule.setInStepOutApplyId(rule.getApplyId()); + } + //原小时外出数据覆盖现在新增排班数据下班时间 + if (!rule.getOutPoint().before(oldRule.getOutPoint())) { + oldRule.setOutStepOutType(1); + oldRule.setOutStepOutApplyId(rule.getApplyId()); + } + log.error("新增外出、出差申请标记原请假时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增请假 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean leaveHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增请假 + if (!Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert() || Objects.equals(rule.getId(), oldRule.getId())) { + return; + } + if (!Objects.equals(LeaveUnitEnum.HOUR.getCode(), rule.getApplyUnit()) || !Objects.equals(LeaveUnitEnum.HOUR.getCode(), oldRule.getApplyUnit())) { + if (isLeaveDateConflict(rule, oldRule)) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.LEAVE)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())); + return; + } + return; + } + //如果当前申请的请假单位为天或者半天,原班次存在单位为小时的请假,则直接提示 + Assert.isFalse(isLeaveDateConflict(rule, oldRule) || isLeaveDateConflict(oldRule, rule), "当前请假时间与原请假时间冲突"); + //请假单位非小时,不走下述逻辑 + if (Objects.nonNull(rule.getApplyUnit()) && !Objects.equals(rule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode())) { + return; + } + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + //新增请假时间完全覆盖原来请假时段 + if (DateDetail.checkTimeBetween(oldRule.getApplyStart(), rule.getApplyStart(), rule.getApplyEnd()) + && DateDetail.checkTimeBetween(oldRule.getApplyEnd(), rule.getApplyStart(), rule.getApplyEnd())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.LEAVE)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())); + return; + } + + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkBetween(rule.getApplyEnd(), oldRule.getApplyStart(), oldRule.getApplyEnd())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.LEAVE)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())); + return; + } + //结束部分覆盖 + if (DateDetail.checkBetween(rule.getApplyStart(), oldRule.getApplyStart(), oldRule.getApplyEnd())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.LEAVE)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())); + return; + } + //新增请假时段处于原请假时段中间 + if (DateDetail.checkTimeBetweenL(rule.getApplyStart(), oldRule.getApplyStart(), oldRule.getApplyEnd()) + && DateDetail.checkTimeBetweenR(rule.getApplyEnd(), oldRule.getApplyStart(), oldRule.getApplyEnd())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.LEAVE)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())); + return; + } + //请假与请假时间边界连接 + //请假结束时间与原请假开始时间相同 + if (oldRule.getInPoint().compareTo(rule.getApplyEnd()) == 0) { + oldRule.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + rule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + } + //请假开始时间与原请假时段结束时间相同 + if (oldRule.getOutPoint().compareTo(rule.getApplyStart()) == 0) { + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + rule.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + } + if (!hisDailyRules.contains(rule) && !rule.getIsInsert()) { + hisDailyRules.add(rule); + log.error("新增请假时段与原请假时段没交集{}", JSON.toJSONString(rule)); + } + }); + return Boolean.TRUE; + } + + /** + * 判断修改后的规则是否与旧规则在请假时间上存在冲突 + * 此方法专门用于检查是否存在请假日期冲突,即旧规则设置为请假,而新规则不设置为请假的情况 + * 这种冲突检测对于确保考勤政策的一致性和准确性至关重要 + * + * @param rule 新的考勤日常规则对象,包含修改后的规则信息 + * @param oldRule 旧的考勤日常规则对象,包含修改前的规则信息 + * @return 如果存在请假日期冲突,则返回true;否则返回false + */ + private boolean isLeaveDateConflict(FtbAttendanceDailyRule rule, FtbAttendanceDailyRule oldRule) { + if (oldRule.getIsInsert()) { + return Boolean.FALSE; + } + if (!Objects.equals(rule.getApplyUnit(), oldRule.getApplyUnit())) { + log.error("新增请假与原请假时间冲突"); + return Boolean.TRUE; + } + //新增请假不是全天且不是半天,则过滤 + if (Objects.equals(rule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode()) || Objects.isNull(rule.getApplyUnit())) { + return Boolean.FALSE; + } + + // 检查旧规则是否请假单位为小时,同时新规则的请假单位不为小时,表示存在冲突 + //如果原来为全天请假则冲突 + if (Objects.equals(oldRule.getApplyUnit(), LeaveUnitEnum.DAY.getCode())) { + log.error("新增请假与原全天请假冲突"); + return Boolean.TRUE; + } + //如果原来为全天请假则冲突 + //如果原半天请假时间处于新增请假中间 + LeaveParam leaveParam = JsonUtil.getJsonToBean(rule.getLeaveParam(), LeaveParam.class); + LeaveParam oldLeaveParam = JsonUtil.getJsonToBean(oldRule.getLeaveParam(), LeaveParam.class); + if (leaveParam.getStart().compareTo(oldLeaveParam.getEnd()) == 0 && oldLeaveParam.getEndTimeType() < leaveParam.getStartTimeType()) { + return Boolean.FALSE; + } + if (leaveParam.getEnd().compareTo(oldLeaveParam.getStart()) == 0 && oldLeaveParam.getStartTimeType() > leaveParam.getEndTimeType()) { + return Boolean.FALSE; + } + log.error("新增半天请假与原半天请假冲突"); + return Boolean.TRUE; + } + + /** + * 新增上班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param ordinaryRule 当前规则 + */ + private Boolean ordinaryHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule ordinaryRule, List resultList) { + //新增上班 + if (!Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), ordinaryRule.getAttendanceType())) { + return Boolean.FALSE; + } + Set seen = new HashSet<>(); + hisDailyRules.removeAll(ftbAttendanceDailyRules); + List ftbAttendanceDailyRules1 = ftbAttendanceDailyRules.stream() + .filter(p -> seen.add(p.getApplyId())) + .collect(Collectors.toList()); + hisDailyRules.addAll(ftbAttendanceDailyRules1); + ftbAttendanceDailyRules1.stream().filter(rule1 -> !Objects.equals(rule1.getApplyUnit(), 1) && Objects.nonNull(rule1.getApplyUnit())).forEach(leaveRule -> { + daysLeaveHandle(hisDailyRules, leaveRule, ordinaryRule, resultList); + }); + ftbAttendanceDailyRules1.removeIf(dailyRule -> Objects.equals(dailyRule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && !Objects.equals(dailyRule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode())); + List collect = ftbAttendanceDailyRules1.stream().filter(rule1 -> Objects.equals(rule1.getApplyUnit(), 1) || Objects.isNull(rule1.getApplyUnit())).collect(Collectors.toList()); + Iterator leaveRuleIterator = collect.iterator(); + while (leaveRuleIterator.hasNext()) { + houseLeaveHandle(hisDailyRules, ordinaryRule, leaveRuleIterator, resultList); + } + return Boolean.TRUE; + } + + /**u + * 小时请假处理 + * + * @param hisDailyRules + * @param ordinaryRule + * @param leaveRuleIterator + * @param resultList + */ + private void houseLeaveHandle(List hisDailyRules, FtbAttendanceDailyRule ordinaryRule, Iterator leaveRuleIterator, List resultList) { + FtbAttendanceDailyRule leaveRule = leaveRuleIterator.next(); + if (Objects.nonNull(leaveRule.getApplyUnit()) && !Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode())) { + return; + } + if (Objects.equals(ordinaryRule.getApplyViewEnable(), 0)) { + leaveRule.setApplyViewEnable(2); + return; + } + ordinaryRule.setSelfGroup(leaveRule.getSelfGroup()); + leaveRule.setApplyViewEnable(1); + leaveRule.setFixedMark(ordinaryRule.getFixedMark()); + leaveRule.setPeriodId(ordinaryRule.getPeriodId()); + //新增上班时段处于原请假时段中间 + if (DateDetail.checkTimeBetween(ordinaryRule.getInPoint(), leaveRule.getApplyStart(), leaveRule.getApplyEnd()) + && DateDetail.checkTimeBetween(ordinaryRule.getOutPoint(), leaveRule.getApplyStart(), leaveRule.getApplyEnd())) { + leaveRule = getFtbAttendanceDailyRule(ordinaryRule, leaveRule); + leaveRule.setInPoint(ordinaryRule.getInPoint()); + leaveRule.setOutPoint(ordinaryRule.getOutPoint()); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + getBreakTimeByOrdinary(leaveRule, ordinaryRule); + houseLeaveDayProcess(leaveRule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + ordinaryRule.insertTrue(); + hisDailyRules.remove(ordinaryRule); + log.error("新增上班时段处于原请假时段中间{}", JSON.toJSONString(ordinaryRule)); + resultList.add(DailyRuleResultVo.successBuild(leaveRule, ordinaryRule)); + return; + } + //新增上班时间完全覆盖原来请假时段 + if (DateDetail.checkTimeBetween(leaveRule.getApplyStart(), ordinaryRule.getInPoint(), ordinaryRule.getOutPoint()) + && DateDetail.checkTimeBetween(leaveRule.getApplyEnd(), ordinaryRule.getInPoint(), ordinaryRule.getOutPoint())) { + leaveRule = getFtbAttendanceDailyRule(ordinaryRule, leaveRule); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + FtbAttendanceDailyRule rule1 = BeanUtil.toBean(ordinaryRule, FtbAttendanceDailyRule.class); + resultList.add(DailyRuleResultVo.successBuild(leaveRule, ordinaryRule)); + ordinaryRule.setOutLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getOutLackPoint(), leaveRule.getApplyStart())); + ordinaryRule.setEarlyPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getEarlyPoint(), leaveRule.getApplyStart())); + ordinaryRule.setOutPoint(leaveRule.getApplyStart()); + ordinaryRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), leaveRule.getApplyStart())); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), leaveRule.getApplyStart())); + getBreakTimeByOrdinary(leaveRule, ordinaryRule); + rule1.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), leaveRule.getApplyEnd())); + rule1.setClockStartPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getClockStartPoint(), leaveRule.getApplyEnd())); + rule1.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), leaveRule.getApplyEnd())); + rule1.setInPoint(leaveRule.getApplyEnd()); + rule1.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + rule1.setId(RandomUtil.uuId()); + getBreakTimeByOrdinary(leaveRule, rule1); + houseLeaveDayProcess(leaveRule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + if (!hisDailyRules.contains(ordinaryRule)) { + ordinaryRule.insertTrue(); + hisDailyRules.add(ordinaryRule); + } + if (DateUtil.dateDiff(ordinaryRule.getOutPoint(), ordinaryRule.getInPoint()) == 0) { + hisDailyRules.remove(ordinaryRule); + } + log.error("新增上班时间完全覆盖原来请假时段{}", JSON.toJSONString(ordinaryRule)); + if (DateUtil.dateDiff(rule1.getOutPoint(), rule1.getInPoint()) != 0) { + leaveRuleIterator.remove(); + if (!leaveRuleIterator.hasNext()) { + if (!hisDailyRules.contains(rule1)) { + rule1.insertTrue(); + hisDailyRules.add(rule1); + resultList.add(DailyRuleResultVo.successBuild(leaveRule, rule1)); + } + return; + } + houseLeaveHandle(hisDailyRules, rule1, leaveRuleIterator, resultList); + } + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetweenR(ordinaryRule.getOutPoint(), leaveRule.getApplyStart(), leaveRule.getApplyEnd())) { + leaveRule = getFtbAttendanceDailyRule(ordinaryRule, leaveRule); + leaveRule.setOutPoint(ordinaryRule.getOutPoint()); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + ordinaryRule.setOutLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getOutLackPoint(), leaveRule.getApplyStart())); + ordinaryRule.setEarlyPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getEarlyPoint(), leaveRule.getApplyEnd())); + ordinaryRule.setOutPoint(leaveRule.getApplyStart()); + ordinaryRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), ordinaryRule.getInPoint())); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), ordinaryRule.getInPoint())); + getBreakTimeByOrdinary(leaveRule, ordinaryRule); + houseLeaveDayProcess(leaveRule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + log.error("新增上班时段开始部分覆盖原请假时段中间{}", JSON.toJSONString(ordinaryRule)); + if (!hisDailyRules.contains(ordinaryRule)) { + ordinaryRule.insertTrue(); + hisDailyRules.add(ordinaryRule); + } + resultList.add(DailyRuleResultVo.successBuild(leaveRule, ordinaryRule)); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetweenL(ordinaryRule.getInPoint(), leaveRule.getApplyStart(), leaveRule.getApplyEnd())) { + leaveRule = getFtbAttendanceDailyRule(ordinaryRule, leaveRule); + leaveRule.setInPoint(ordinaryRule.getInPoint()); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), leaveRule.getApplyEnd())); + ordinaryRule.setClockStartPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getClockStartPoint(), leaveRule.getApplyEnd())); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), leaveRule.getApplyEnd())); + ordinaryRule.setInPoint(leaveRule.getApplyEnd()); + ordinaryRule.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + getBreakTimeByOrdinary(leaveRule, ordinaryRule); + houseLeaveDayProcess(leaveRule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + log.error("新增上班时段结束部分覆盖原请假时段中间{}", JSON.toJSONString(ordinaryRule)); + if (DateUtil.dateDiff(ordinaryRule.getOutPoint(), ordinaryRule.getInPoint()) != 0) { + leaveRuleIterator.remove(); + if (!leaveRuleIterator.hasNext()) { + if (!hisDailyRules.contains(ordinaryRule)) { + ordinaryRule.insertTrue(); + hisDailyRules.add(ordinaryRule); + resultList.add(DailyRuleResultVo.successBuild(leaveRule, ordinaryRule)); + } + return; + } + houseLeaveHandle(hisDailyRules, ordinaryRule, leaveRuleIterator, resultList); + } + return; + } + + if (!leaveRule.getIsInsert()) { + leaveRule.setApplyViewEnable(2); + } + if (!hisDailyRules.contains(ordinaryRule) && !ordinaryRule.getIsInsert()) { + //完全没交集 + hisDailyRules.add(ordinaryRule); + ordinaryRule.insertTrue(); + } + log.error("新增上班时间与原来请假时段完全没交集{}", JSON.toJSONString(ordinaryRule)); + } + + private FtbAttendanceDailyRule getFtbAttendanceDailyRule(FtbAttendanceDailyRule ordinaryRule, FtbAttendanceDailyRule rule) { + if (rule.getIsInsert()) { + rule = BeanUtil.toBean(rule, FtbAttendanceDailyRule.class); + rule.setId(RandomUtil.uuId()); + rule.setSelfGroup(ordinaryRule.getSelfGroup()); + rule.setLeaveDay(BigDecimal.ZERO); + rule.setPayrollHours(BigDecimal.ZERO); + } + rule.setApplyViewEnable(1); + rule.setFixedMark(ordinaryRule.getFixedMark()); + rule.setBreakEnable(ordinaryRule.getBreakEnable()); + rule.setSchedulesType(ordinaryRule.getSchedulesType()); + rule.setShiftId(ordinaryRule.getShiftId()); + rule.setPeriodId(ordinaryRule.getPeriodId()); + rule.setInPoint(rule.getApplyStart()); + rule.setOutPoint(rule.getApplyEnd()); + rule.insertTrue(); + return rule; + } + + private void houseLeaveDayProcess(FtbAttendanceDailyRule leaveRule, Integer validDuration, BigDecimal periodWorkDay, BigDecimal payrollHours) { + leaveRule.calValidDuration(); + leaveRule.setLeaveDay(Objects.isNull(periodWorkDay) ? null : BigDecimal.valueOf(leaveRule.getValidDuration()) + .divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP) + .multiply(periodWorkDay).setScale(2, RoundingMode.HALF_UP)); + housePayrollHoursProcess(leaveRule, payrollHours, validDuration); + } + private void housePayrollHoursProcess(FtbAttendanceDailyRule leaveRule, BigDecimal payrollHours, Integer validDuration) { + leaveRule.calValidDuration(); + leaveRule.setPayrollHours(Objects.isNull(payrollHours) ? null : BigDecimal.valueOf(leaveRule.getValidDuration()) + .divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP) + .multiply(payrollHours).setScale(2, RoundingMode.HALF_UP)); + } + /** + * 天、半天请假 + * + * @param hisDailyRules + * @param leaveRule + * @param ordinaryRule + * @param resultList + */ + private void daysLeaveHandle(List hisDailyRules, FtbAttendanceDailyRule leaveRule, FtbAttendanceDailyRule ordinaryRule, List resultList) { + //同一请假申请保留一条请假记录 + if (Objects.isNull(leaveRule.getApplyUnit()) || Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode())) { + return; + } + LeaveParam leaveParam = JsonUtil.getJsonToBean(leaveRule.getLeaveParam(), LeaveParam.class); + //是否为半天假 + halfDayLeave(hisDailyRules, ordinaryRule, resultList, leaveRule, leaveParam); + //全天请假 + dayLeave(hisDailyRules, ordinaryRule, resultList, leaveRule); + //休息时间处理 + //如果开始时间处于休息时间中间,开始时间变更为休息时间的开始时间 + if (Objects.nonNull(ordinaryRule.getBreakStartPoint()) && DateDetail.checkTimeBetween(leaveRule.getInPoint(), ordinaryRule.getOriginBreakStartPoint(), ordinaryRule.getOriginBreakEndPoint())) { + leaveRule.setInPoint(ordinaryRule.getOriginBreakStartPoint()); + leaveRule.setBreakStartPoint(ordinaryRule.getOriginBreakStartPoint()); + ordinaryRule.setOutPoint(ordinaryRule.getOriginBreakStartPoint()); + ordinaryRule.setBreakStartPoint(null); + ordinaryRule.setBreakEndPoint(null); + log.error("新增请假开始时间调整为休息开始时间{}", JSON.toJSONString(ordinaryRule.getOriginBreakStartPoint())); + } + //如果结束时间处理休息时间中间,结束时间变更为休息时间的结束时间 + if (Objects.nonNull(ordinaryRule.getBreakStartPoint()) && DateDetail.checkTimeBetween(leaveRule.getOutPoint(), ordinaryRule.getOriginBreakStartPoint(), ordinaryRule.getOriginBreakEndPoint())) { + leaveRule.setOutPoint(ordinaryRule.getOriginBreakEndPoint()); + leaveRule.setBreakEndPoint(ordinaryRule.getOriginBreakEndPoint()); + ordinaryRule.setInPoint(ordinaryRule.getOriginBreakEndPoint()); + ordinaryRule.setBreakStartPoint(null); + ordinaryRule.setBreakEndPoint(null); + log.error("新增请假结束时间调整为休息结束时间{}", JSON.toJSONString(ordinaryRule.getOriginBreakEndPoint())); + } + + } + + /** + * 处理全天请假的逻辑 + * + * @param hisDailyRules 历史每日规则列表 + * @param ordinaryRule 原有的每日规则 + * @param resultList 结果列表,用于存储处理结果 + * @param leaveRule 请假的每日规则 + */ + private void dayLeave(List hisDailyRules, FtbAttendanceDailyRule ordinaryRule, List resultList, FtbAttendanceDailyRule leaveRule) { + // 检查请假类型是否为全天 + if (!Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.DAY.getCode())) { + return; + } + // 如果是插入操作,则直接返回 + if (leaveRule.getIsInsert()) { + return; + } + //全天请假 + leaveRule = getFtbAttendanceDailyRule(ordinaryRule, leaveRule); + hisDailyRules.remove(ordinaryRule); + leaveRule.setInPoint(ordinaryRule.getInPoint()); + leaveRule.setOutPoint(ordinaryRule.getOutPoint()); + //如果为全天班,则请假时长为时段工时 + leaveRule.setLeaveDay(!Objects.equals(ordinaryRule.getSchedulesType(), 0) ? ordinaryRule.getPeriodWorkDay() : BigDecimal.ONE); + leaveRule.setPayrollHours(ordinaryRule.getPayrollHours()); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + log.error("新增全天请假时间覆盖原来班时段{}", JSON.toJSONString(leaveRule)); + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + } + + private void halfDayLeave(List hisDailyRules, FtbAttendanceDailyRule ordinaryRule, List resultList, FtbAttendanceDailyRule leaveRule, LeaveParam leaveParam) { + //是否为半天假 + if (!Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.HALF_DAY.getCode())) { + return; + } + //划线排班,隐藏半天请假 + if (Objects.equals(ordinaryRule.getFixedMark(), 2)) { + leaveRule.setApplyViewEnable(0); + return; + } + if (isNotBetweenByHalfDayLeave(ordinaryRule, leaveParam)) { + //完全没交集 + if (!hisDailyRules.contains(leaveRule)) { + leaveRule.setApplyViewEnable(2); + hisDailyRules.add(leaveRule); + } + if (!hisDailyRules.contains(ordinaryRule)) { + hisDailyRules.add(ordinaryRule); + } + return; + } + leaveRule = getFtbAttendanceDailyRule(ordinaryRule, leaveRule); + if (Objects.equals(ordinaryRule.getSchedulesType(), 0)) { + Date leaveDate = getLeaveDate(ordinaryRule); + boolean isMorningHalfDay = isHalfDayLeave(ordinaryRule, leaveParam, 1); + boolean isAfternoonHalfDay = isHalfDayLeave(ordinaryRule, leaveParam, 2); + //请假开始是否为上半天请假 + if (isMorningHalfDay && !isAfternoonHalfDay) { + if (leaveDate.before(ordinaryRule.getInPoint())) { + return; + } + //请假开始时间为开始日期当天排班开始时间 + leaveRule.setInPoint(ordinaryRule.getOriginInPoint()); + leaveRule.setOutPoint(leaveDate); + leaveRule.setSchedulesType(ordinaryRule.getSchedulesType()); + leaveRule.setLeaveDay(BigDecimal.valueOf(0.5)); + getBreakTimeByOrdinary(leaveRule,ordinaryRule); + housePayrollHoursProcess(leaveRule, ordinaryRule.getPayrollHours(), ordinaryRule.getOriginValidDuration()); + if (leaveDate.compareTo(ordinaryRule.getInPoint()) == 0) { + log.error("新增下半天请假时间覆盖原来全天班时段{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + hisDailyRules.remove(ordinaryRule); + return; + } + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), leaveDate)); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), leaveDate)); + ordinaryRule.setInPoint(leaveDate); + ordinaryRule.setSchedulesType(2); + ordinaryRule.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + housePayrollHoursProcess(ordinaryRule, ordinaryRule.getPayrollHours(), ordinaryRule.getOriginValidDuration()); + log.error("新增全天班时段时间覆盖原来上半天请假{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + if (!hisDailyRules.contains(ordinaryRule)) { + hisDailyRules.add(ordinaryRule); + } + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + //请假开始是否为下半天请假 + } else if (!isMorningHalfDay && isAfternoonHalfDay) { + if (leaveDate.after(ordinaryRule.getOutPoint())) { + return; + } + leaveRule.setInPoint(leaveDate); + leaveRule.setSchedulesType(ordinaryRule.getSchedulesType()); + leaveRule.setOutPoint(ordinaryRule.getOriginOutPoint()); + leaveRule.setLeaveDay(BigDecimal.valueOf(0.5)); + getBreakTimeByOrdinary(leaveRule,ordinaryRule); + housePayrollHoursProcess(leaveRule, ordinaryRule.getPayrollHours(), ordinaryRule.getOriginValidDuration()); + if (leaveDate.compareTo(ordinaryRule.getInPoint()) == 0) { + log.error("新增下半天请假时间覆盖原来全天班时段{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + hisDailyRules.remove(ordinaryRule); + return; + } + ordinaryRule.setEarlyPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getEarlyPoint(), leaveDate)); + ordinaryRule.setOutLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getOutLackPoint(), leaveDate)); + ordinaryRule.setOutPoint(leaveDate); + ordinaryRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + ordinaryRule.setSchedulesType(1); + housePayrollHoursProcess(ordinaryRule, ordinaryRule.getPayrollHours(), ordinaryRule.getOriginValidDuration()); + log.error("新增全天班时段覆盖原来下半天请假时间{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + if (!hisDailyRules.contains(ordinaryRule)) { + hisDailyRules.add(ordinaryRule); + } + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + } else { + //请假两个半天,隐藏全天班 + leaveRule.setInPoint(ordinaryRule.getInPoint()); + leaveRule.setOutPoint(ordinaryRule.getOutPoint()); + leaveRule.setSchedulesType(ordinaryRule.getSchedulesType()); + leaveRule.setLeaveDay(BigDecimal.ONE); + housePayrollHoursProcess(leaveRule, ordinaryRule.getPayrollHours(), ordinaryRule.getOriginValidDuration()); + hisDailyRules.removeIf(rule -> StringUtil.equals(rule.getId(), ordinaryRule.getId())); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + if (!hisDailyRules.contains(ordinaryRule)) { + ordinaryRule.setApplyViewEnable(0); + hisDailyRules.add(ordinaryRule); + } + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + log.error("新增全天班完全覆盖请假时间{}", JSON.toJSONString(ordinaryRule)); + } + return; + } + //如果为半天班,每个上下班只保留一条 + if (!Objects.equals(ordinaryRule.getSchedulesType(), leaveRule.getSchedulesType())) { + return; + } + //请假开始时间为开始日期当天排班开始时间 + leaveRule.setInPoint(ordinaryRule.getInPoint()); + leaveRule.setOutPoint(ordinaryRule.getOutPoint()); + leaveRule.setSchedulesType(ordinaryRule.getSchedulesType()); + leaveRule.setLeaveDay(BigDecimal.valueOf(0.5)); + leaveRule.setPayrollHours(ordinaryRule.getPayrollHours()); + getBreakTimeByOrdinary(leaveRule, ordinaryRule); + hisDailyRules.removeIf(rule -> StringUtil.equals(rule.getId(), ordinaryRule.getId())); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + ordinaryRule.insertTrue(); + log.error("新增半天班时段覆盖原来半天请假时间{}", JSON.toJSONString(leaveRule)); + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, leaveRule)); + + } + + // 提取辅助方法,减少重复逻辑 + private boolean isHalfDayLeave(FtbAttendanceDailyRule oldRule, LeaveParam leaveParam, int timeType) { + // 开始日期且时间类型匹配 + boolean isStartDateMatch = Objects.equals(leaveParam.getStartTimeType(), timeType) && + oldRule.getDay().compareTo(leaveParam.getStart()) == 0; + + // 中间日期 + boolean isMiddleDate = (Objects.equals(timeType, 2) || oldRule.getDay().after(leaveParam.getStart())) && + (Objects.equals(timeType, 1) || oldRule.getDay().before(leaveParam.getEnd())); + + // 结束日期且时间类型匹配 + boolean isEndDateMatch = Objects.equals(leaveParam.getEndTimeType(), timeType) && + oldRule.getDay().compareTo(leaveParam.getEnd()) == 0; + + return isStartDateMatch || isMiddleDate || isEndDateMatch; + } + + /** + * 是否不属于半天、全天假范围内 + * + * @param oldRule2 + * @param leaveParam + * @return + */ + private static boolean isNotBetweenByHalfDayLeave(FtbAttendanceDailyRule oldRule2, LeaveParam leaveParam) { + return !DateDetail.checkTimeBetween(oldRule2.getDay(), leaveParam.getStart(), leaveParam.getEnd()) + || !Objects.equals(oldRule2.getSchedulesType(), 0) && leaveParam.getStartTimeType() > oldRule2.getSchedulesType() && oldRule2.getDay().compareTo(leaveParam.getStart()) == 0 + || !Objects.equals(oldRule2.getSchedulesType(), 0) && leaveParam.getEndTimeType() < oldRule2.getSchedulesType() && oldRule2.getDay().compareTo(leaveParam.getEnd()) == 0; + } + + /** + * 计算半天请假全天班中间临界请假时间 + * + * @param ftbAttendanceDailyRule + * @return + */ + private Date getLeaveDate(FtbAttendanceDailyRule ftbAttendanceDailyRule) { + int ruleMinute = DateDetail.calculateMinuteDiff(ftbAttendanceDailyRule.getOriginInPoint(), ftbAttendanceDailyRule.getOriginOutPoint()); + return cn.hutool.core.date.DateUtil.offsetMinute(ftbAttendanceDailyRule.getOriginInPoint(), ruleMinute / 2); + } + + /** + * 计算请假天数 + * + * @param ordinaryRule + * @param leaveRule + */ + private void leaveDaysProcess(FtbAttendanceDailyRule ordinaryRule, FtbAttendanceDailyRule leaveRule) { + //查询是否为全天班 + boolean isFullDay = Objects.equals(ordinaryRule.getSchedulesType(), 0); + //是否为半天请假 + if (Objects.equals(leaveRule.getApplyUnit(), 3)) { + //如果为全天班且当天请假时长为多个半天则设置为1 + if (isFullDay && leaveRule.getLeaveDay().compareTo(BigDecimal.valueOf(0.5)) > 0) { + leaveRule.setLeaveDay(BigDecimal.ONE); + leaveRule.setPayrollHours(ordinaryRule.getPayrollHours()); + } else { + leaveRule.setLeaveDay(ordinaryRule.getPeriodWorkDay()); + leaveRule.setPayrollHours(ordinaryRule.getPayrollHours()); + } + } + //是否为全天请假 + if (Objects.equals(leaveRule.getApplyUnit(), 2)) { + leaveRule.setLeaveDay(BigDecimal.ONE); + if (!isFullDay) { + //如果为半天班且班次数量大于1则设置为0.5 + leaveRule.setLeaveDay(ordinaryRule.getPeriodWorkDay()); + leaveRule.setPayrollHours(ordinaryRule.getPayrollHours()); + } + } + } + + /** + * 新增排班的休息时间处理 + * + * @param leaveRule 原请假规则 + * @param ordinaryRule 新增正常排班规则 + */ + private void getBreakTimeByOrdinary(FtbAttendanceDailyRule leaveRule, FtbAttendanceDailyRule ordinaryRule) { + leaveRule.insertTrue(); + if (!Objects.equals(ordinaryRule.getBreakEnable(), 1) || Objects.isNull(ordinaryRule.getBreakStartPoint())) { + return; + } + leaveRule.setBreakStartPoint(leaveRule.getInPoint().after(ordinaryRule.getBreakEndPoint())|| leaveRule.getOutPoint().before(ordinaryRule.getBreakStartPoint()) ? null : leaveRule.getInPoint().after(ordinaryRule.getBreakStartPoint()) ? leaveRule.getInPoint() : ordinaryRule.getBreakStartPoint()); + leaveRule.setBreakEndPoint(leaveRule.getInPoint().after(ordinaryRule.getBreakEndPoint()) || leaveRule.getOutPoint().before(ordinaryRule.getBreakStartPoint()) ? null : leaveRule.getOutPoint().before(ordinaryRule.getBreakEndPoint()) ? leaveRule.getOutPoint() : ordinaryRule.getBreakEndPoint()); + //休息结束时间小于排班上班时间,或者休息开始时间大于排班下班时间 + if (ordinaryRule.getBreakEndPoint().compareTo(ordinaryRule.getInPoint()) <= 0 || ordinaryRule.getBreakStartPoint().compareTo(ordinaryRule.getOutPoint()) >= 0) { + ordinaryRule.setBreakStartPoint(null); + ordinaryRule.setBreakEndPoint(null); + ordinaryRule.setBreakEnable(0); + return; + } + //请假时间完全覆盖原来休息时段 + if (DateDetail.checkTimeBetween(ordinaryRule.getBreakStartPoint(), leaveRule.getApplyStart(), leaveRule.getApplyEnd()) + && DateDetail.checkTimeBetween(ordinaryRule.getBreakEndPoint(), leaveRule.getApplyStart(), leaveRule.getApplyEnd())) { + ordinaryRule.setBreakStartPoint(null); + ordinaryRule.setBreakEndPoint(null); + ordinaryRule.setBreakEnable(0); + return; + } + //休息时间完全覆盖请假时间 + if (DateDetail.checkTimeBetween(leaveRule.getApplyStart(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint()) + && DateDetail.checkTimeBetween(leaveRule.getApplyEnd(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint())) { + if (leaveRule.getApplyStart().compareTo(ordinaryRule.getBreakStartPoint()) >= 0) { + ordinaryRule.setBreakEndPoint(leaveRule.getApplyStart()); + leaveRule.setBreakStartPoint(leaveRule.getApplyStart()); + } + if (leaveRule.getApplyEnd().compareTo(ordinaryRule.getBreakEndPoint()) <= 0) { + leaveRule.setBreakEndPoint(leaveRule.getApplyEnd()); + ordinaryRule.setBreakEndPoint(leaveRule.getApplyEnd()); + } + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetween(leaveRule.getApplyEnd(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint())) { + leaveRule.setBreakEndPoint(leaveRule.getApplyEnd()); + ordinaryRule.setBreakStartPoint(leaveRule.getApplyEnd()); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetween(leaveRule.getApplyStart(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint())) { + ordinaryRule.setBreakEndPoint(leaveRule.getApplyStart()); + leaveRule.setBreakStartPoint(leaveRule.getApplyStart()); + return; + } + } + + /** + * 新增排休 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean restHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增排休 + if (!Objects.equals(AttendanceTypeEnum.REST.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + if (Objects.nonNull(rule.getOutPoint()) && (rule.getOutPoint().before(oldRule.getInPoint()) || oldRule.getInPoint().before(rule.getOutPoint()))) { + //完全没交集 + rule.insertTrue(); + hisDailyRules.add(rule); + log.error("新增排休与原来请假时段完全没交集{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + log.error(AttendanceConstant.getSchedulingResult(rule.getUserId(), rule.getDay(), AttendanceConstant.LEAVE, AttendanceTypeEnum.getMsg(rule.getAttendanceType()))); + resultList.add(DailyRuleResultVo.build(rule, AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType(), oldRule.getFixedMark())); + }); + return Boolean.TRUE; + } + + /** + * 新增加班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean workOvertimeHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增加班 + if (!Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + //完全覆盖 + //加班时间完全覆盖原来请假时段 + if (DateDetail.checkTimeBetween(oldRule.getInPoint(), rule.getInPoint(), rule.getOutPoint()) + && DateDetail.checkTimeBetween(oldRule.getOutPoint(), rule.getInPoint(), rule.getOutPoint())) { + oldRule.setInPoint(oldRule.getApplyStart()); + oldRule.setOutPoint(oldRule.getApplyEnd()); + oldRule.setApplyViewEnable(0); + if (!hisDailyRules.contains(rule)) { + rule.insertTrue(); + hisDailyRules.add(rule); + } + log.error("新增加班时间完全覆盖原来请假时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetweenR(rule.getOutPoint(), oldRule.getInPoint(), oldRule.getOutPoint())) { + oldRule.setInPoint(rule.getApplyEnd()); + oldRule.setApplyViewEnable(1); + if (!hisDailyRules.contains(rule)) { + rule.insertTrue(); + hisDailyRules.add(rule); + } + log.error("新增加班时间完全开始部分覆盖原来请假时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetweenL(rule.getInPoint(), oldRule.getInPoint(), oldRule.getOutPoint())) { + oldRule.setOutPoint(rule.getApplyStart()); + rule.setApplyViewEnable(1); + if (!hisDailyRules.contains(rule)) { + rule.insertTrue(); + hisDailyRules.add(rule); + } + log.error("新增加班时间完全结束部分覆盖原来请假时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //请假完全覆盖加班 + if (DateDetail.checkTimeBetween(rule.getInPoint(), oldRule.getInPoint(), oldRule.getOutPoint()) + && DateDetail.checkTimeBetween(rule.getOutPoint(), oldRule.getInPoint(), oldRule.getOutPoint())) { + FtbAttendanceDailyRule oldRule1 = BeanUtil.toBean(oldRule, FtbAttendanceDailyRule.class); + oldRule.setOutPoint(rule.getApplyStart()); + oldRule1.setInPoint(rule.getApplyEnd()); + oldRule1.setId(RandomUtil.uuId()); + oldRule1.setApplyViewEnable(1); + oldRule1.insertTrue(); + hisDailyRules.add(oldRule1); + if (!hisDailyRules.contains(rule)) { + rule.insertTrue(); + hisDailyRules.add(rule); + } + log.error("原来请假时段完全覆盖新增加班时间{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //完全没交集 + if (!hisDailyRules.contains(rule)) { + rule.insertTrue(); + hisDailyRules.add(rule); + } + log.error("新增加班时间与原来请假时段完全没交集{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/OrdinaryRuleProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/OrdinaryRuleProcessor.java new file mode 100644 index 0000000..71ffeb2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/OrdinaryRuleProcessor.java @@ -0,0 +1,737 @@ +package jnpf.attendance.service.handle.rule; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.RuleProcessor; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.attendance.LeaveParam; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.LeaveUnitEnum; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.util.DateDetail; +import jnpf.util.DateUtil; +import jnpf.util.JsonUtil; +import jnpf.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 处理历史普通排班 + */ +@Component +@Slf4j +public class OrdinaryRuleProcessor extends RuleProcessor { + @Override + public void ruleArrangementHandle(List hisDailyRules, List currDailyRule, List resultList) { + Map> collect = hisDailyRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getAttendanceType)); + List ftbAttendanceDailyRules = collect.get(AttendanceTypeEnum.ORDINARY.getCode()); + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + Iterator iterator = currDailyRule.iterator(); + while (iterator.hasNext()) { + FtbAttendanceDailyRule rule = iterator.next(); + //新增排休 + if (restHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增上班 + if (ordinaryHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增加班 + if (workOvertimeHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增请假 + if (leaveHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增外出、出差 + if (stepOutHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增清除 + clearHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList); + } + } + if (Objects.nonNull(nextRuleProcessor)) { + nextRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList); + } + } + + /** + * 新增外出、出差申请 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean stepOutHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增外出 + if (!Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType()) && !Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + //原小时外出没命中已有班次数据,隐藏外出申请 + if (rule.getOutPoint().before(oldRule.getInPoint()) + || rule.getInPoint().after(oldRule.getOutPoint()) + || rule.getInPoint().after(oldRule.getInPoint()) && rule.getOutPoint().before(oldRule.getOutPoint())) { + if (!rule.getIsInsert()) { + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + } + log.error("新增排班数据未命中小时外出申请或者外出数据完全被排班数据覆盖,过滤{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successNailBuild(rule)); + return; + } + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + rule.setApplyViewEnable(0); + oldRule.setApplyViewEnable(rule.getAttendanceType()); + //原小时外出数据覆盖现在新增排班数据上班时间 + if (!rule.getInPoint().after(oldRule.getInPoint())) { + oldRule.setInStepOutType(1); + oldRule.setInStepOutApplyId(rule.getApplyId()); + } + //原小时外出数据覆盖现在新增排班数据下班时间 + if (!rule.getOutPoint().before(oldRule.getOutPoint())) { + oldRule.setOutStepOutType(1); + oldRule.setOutStepOutApplyId(rule.getApplyId()); + } + log.error("新增外出、出差申请标记原普班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增清除 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean clearHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增清除 + if (!Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + hisDailyRules.removeAll(ftbAttendanceDailyRules); + ftbAttendanceDailyRules.forEach(oldRule -> resultList.add(DailyRuleResultVo.successBuild(oldRule))); + return Boolean.TRUE; + } + + /** + * 新增请假 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule1 当前规则 + */ + private Boolean leaveHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule1, List resultList) { + //新增请假 + if (!Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), rule1.getAttendanceType())) { + return Boolean.FALSE; + } + boolean isMultipleRules = ftbAttendanceDailyRules.size() > 1; + ftbAttendanceDailyRules.forEach(oldRule -> { + daysLeaveHandle(hisDailyRules, oldRule, rule1, resultList, isMultipleRules); + houseLeaveHandle(hisDailyRules, oldRule, rule1, resultList); + }); + return Boolean.TRUE; + } + + /** + * 小时请假 + * + * @param hisDailyRules + * @param ordinaryRule + * @param rule + * @param resultList + */ + private void houseLeaveHandle(List hisDailyRules, FtbAttendanceDailyRule ordinaryRule, FtbAttendanceDailyRule rule, List resultList) { + if (Objects.nonNull(rule.getApplyUnit()) && !Objects.equals(rule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode())) { + return; + } + //新加入数据不做处理 + if (ordinaryRule.getIsInsert()) { + return; + } + //请假处于排班时段中间a + if (DateDetail.checkTimeBetween(rule.getApplyStart(), ordinaryRule.getInPoint(), ordinaryRule.getOutPoint()) + && DateDetail.checkTimeBetween(rule.getApplyEnd(), ordinaryRule.getInPoint(), ordinaryRule.getOutPoint())) { + FtbAttendanceDailyRule oldRule1 = BeanUtil.toBean(ordinaryRule, FtbAttendanceDailyRule.class); + rule = getFtbAttendanceDailyRule(ordinaryRule, rule); + ordinaryRule.setEarlyPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getEarlyPoint(), rule.getApplyStart())); + ordinaryRule.setOutLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getOutLackPoint(), rule.getApplyStart())); + ordinaryRule.setOutPoint(rule.getApplyStart()); + ordinaryRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), ordinaryRule.getInPoint())); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), ordinaryRule.getInPoint())); + getBreakTimeByOrdinary(ordinaryRule, rule); + oldRule1.setClockStartPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getClockStartPoint(), rule.getApplyEnd())); + oldRule1.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), rule.getApplyEnd())); + oldRule1.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), rule.getApplyEnd())); + oldRule1.setInPoint(rule.getApplyEnd()); + oldRule1.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + oldRule1.setId(RandomUtil.uuId()); + getBreakTimeByOrdinary(oldRule1, rule); + houseLeaveDayProcess(rule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + oldRule1.insertTrue(); + if (DateUtil.dateDiff(oldRule1.getOutPoint(), oldRule1.getInPoint()) != 0) { + hisDailyRules.add(oldRule1); + } + if (DateUtil.dateDiff(ordinaryRule.getOutPoint(), ordinaryRule.getInPoint()) == 0) { + hisDailyRules.remove(ordinaryRule); + } + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增请假处于排班时段中间{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, rule)); + return; + } + //请假时间完全覆盖原来上班时段 + if (DateDetail.checkTimeBetween(ordinaryRule.getInPoint(), rule.getApplyStart(), rule.getApplyEnd()) + && DateDetail.checkTimeBetween(ordinaryRule.getOutPoint(), rule.getApplyStart(), rule.getApplyEnd())) { + rule = getFtbAttendanceDailyRule(ordinaryRule, rule); + hisDailyRules.remove(ordinaryRule); + rule.setInPoint(ordinaryRule.getInPoint()); + rule.setOutPoint(ordinaryRule.getOutPoint()); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + getBreakTimeByOrdinary(ordinaryRule, rule); + houseLeaveDayProcess(rule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + log.error("新增请假时间完全覆盖原来上班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, rule)); + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetweenR(rule.getApplyEnd(), ordinaryRule.getInPoint(), ordinaryRule.getOutPoint())) { + rule = getFtbAttendanceDailyRule(ordinaryRule, rule); + rule.setInPoint(ordinaryRule.getInPoint()); + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), rule.getApplyEnd())); + ordinaryRule.setClockStartPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getClockStartPoint(), rule.getApplyEnd())); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), rule.getApplyEnd())); + ordinaryRule.setInPoint(rule.getApplyEnd()); + + ordinaryRule.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + rule.insertTrue(); + } + getBreakTimeByOrdinary(ordinaryRule, rule); + houseLeaveDayProcess(rule, ordinaryRule); + log.error("新增请假时间开始部分覆盖原来上班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, rule)); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetweenL(rule.getApplyStart(), ordinaryRule.getInPoint(), ordinaryRule.getOutPoint())) { + rule = getFtbAttendanceDailyRule(ordinaryRule, rule); + rule.setOutPoint(ordinaryRule.getOutPoint()); + ordinaryRule.setEarlyPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getEarlyPoint(), rule.getApplyStart())); + ordinaryRule.setOutLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getOutPoint(), ordinaryRule.getOutLackPoint(), rule.getApplyStart())); + ordinaryRule.setOutPoint(rule.getApplyStart()); + ordinaryRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + ordinaryRule.calInLackPoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getInLackPoint(), ordinaryRule.getInPoint())); + ordinaryRule.calLatePoint(DateDetail.getNewDateByPeriod(ordinaryRule.getInPoint(), ordinaryRule.getLatePoint(), ordinaryRule.getInPoint())); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + rule.insertTrue(); + } + getBreakTimeByOrdinary(ordinaryRule, rule); + houseLeaveDayProcess(rule, ordinaryRule); + log.error("新增请假时间结束部分覆盖原来上班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(ordinaryRule, rule)); + return; + } + + //完全没交集 + if (!hisDailyRules.contains(rule) && !rule.getIsInsert()) { + rule.setShiftId(ordinaryRule.getShiftId()); + rule.setPeriodId(ordinaryRule.getPeriodId()); + rule.setApplyViewEnable(2); + hisDailyRules.add(rule); + log.error("新增请假与原排班时段中间没交集{}", JSON.toJSONString(rule)); + } + } + + private FtbAttendanceDailyRule getFtbAttendanceDailyRule(FtbAttendanceDailyRule ordinaryRule, FtbAttendanceDailyRule rule) { + if (rule.getIsInsert()) { + rule = BeanUtil.toBean(rule, FtbAttendanceDailyRule.class); + rule.setId(RandomUtil.uuId()); + rule.setSelfGroup(ordinaryRule.getSelfGroup()); + } + rule.setApplyViewEnable(1); + rule.setFixedMark(ordinaryRule.getFixedMark()); + rule.setBreakEnable(ordinaryRule.getBreakEnable()); + rule.setSchedulesType(ordinaryRule.getSchedulesType()); + rule.setShiftId(ordinaryRule.getShiftId()); + rule.setPeriodId(ordinaryRule.getPeriodId()); + rule.setInPoint(rule.getApplyStart()); + rule.setOutPoint(rule.getApplyEnd()); + rule.setOriginInPoint(ordinaryRule.getOriginInPoint()); + rule.setOriginOutPoint(ordinaryRule.getOriginOutPoint()); + rule.insertTrue(); + return rule; + } + + /** + * 天、半天请假 + * + * @param hisDailyRules + * @param oldRule + * @param leaveRule + * @param resultList + */ + private void daysLeaveHandle(List hisDailyRules, FtbAttendanceDailyRule oldRule, FtbAttendanceDailyRule leaveRule, List resultList, Boolean isMultipleRules) { + if (Objects.isNull(leaveRule.getApplyUnit()) || Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.HOUR.getCode())) { + return; + } + LeaveParam leaveParam = JsonUtil.getJsonToBean(leaveRule.getLeaveParam(), LeaveParam.class); + //是否为半天假 + //开始日期 + //是否为全天班 + halfDayLeave(hisDailyRules, oldRule, resultList, leaveRule, leaveParam); + //全天请假 + dayLeave(hisDailyRules, oldRule, resultList, leaveRule); + //休息时间处理 + //如果开始时间处于休息时间中间,开始时间变更为休息时间的开始时间 + if (Objects.nonNull(oldRule.getBreakStartPoint()) && DateDetail.checkTimeBetween(leaveRule.getInPoint(), oldRule.getOriginBreakStartPoint(), oldRule.getOriginBreakEndPoint())) { + leaveRule.setInPoint(oldRule.getOriginBreakStartPoint()); + leaveRule.setBreakStartPoint(oldRule.getOriginBreakStartPoint()); + oldRule.setOutPoint(oldRule.getOriginBreakStartPoint()); + oldRule.setBreakStartPoint(null); + oldRule.setBreakEndPoint(null); + log.error("新增请假开始时间调整为休息开始时间{}", JSON.toJSONString(oldRule.getOriginBreakStartPoint())); + } + //如果结束时间处理休息时间中间,结束时间变更为休息时间的结束时间 + if (Objects.nonNull(oldRule.getBreakStartPoint()) && DateDetail.checkTimeBetween(leaveRule.getOutPoint(), oldRule.getOriginBreakStartPoint(), oldRule.getOriginBreakEndPoint())) { + leaveRule.setOutPoint(oldRule.getOriginBreakEndPoint()); + leaveRule.setBreakEndPoint(oldRule.getOriginBreakEndPoint()); + oldRule.setInPoint(oldRule.getOriginBreakEndPoint()); + oldRule.setBreakStartPoint(null); + oldRule.setBreakEndPoint(null); + log.error("新增请假结束时间调整为休息结束时间{}", JSON.toJSONString(oldRule.getOriginBreakEndPoint())); + } + } + + private void dayLeave(List hisDailyRules, FtbAttendanceDailyRule oldRule, List resultList, FtbAttendanceDailyRule leaveRule) { + if (!Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.DAY.getCode())) { + return; + } + //全天请假 + leaveRule = getFtbAttendanceDailyRule(oldRule, leaveRule); + hisDailyRules.remove(oldRule); + leaveRule.setInPoint(oldRule.getInPoint()); + leaveRule.setOutPoint(oldRule.getOutPoint()); + //如果为全天班,则请假时长为时段工时 + leaveRule.setLeaveDay(!Objects.equals(oldRule.getSchedulesType(), 0) ? oldRule.getPeriodWorkDay() : BigDecimal.ONE); + leaveRule.setPayrollHours(oldRule.getPayrollHours()); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + log.error("新增全天请假时间覆盖原来班时段{}", JSON.toJSONString(leaveRule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + } + + private void halfDayLeave(List hisDailyRules, FtbAttendanceDailyRule oldRule, List resultList, FtbAttendanceDailyRule leaveRule, LeaveParam leaveParam) { + //是否为半天假 + if (!Objects.equals(leaveRule.getApplyUnit(), LeaveUnitEnum.HALF_DAY.getCode())) { + return; + } + //划线排班,隐藏半天请假 + if (Objects.equals(oldRule.getFixedMark(), 2)) { + log.error("划线排班,隐藏半天请假{}", JSON.toJSONString(oldRule)); + leaveRule.setApplyViewEnable(0); + return; + } + if (isNotBetweenByHalfDayLeave(oldRule, leaveParam, leaveRule)) { + //完全没交集 + if (!hisDailyRules.contains(leaveRule)) { + leaveRule.setApplyViewEnable(2); + leaveRule.setFixedMark(oldRule.getFixedMark()); + hisDailyRules.add(leaveRule); + } + return; + } + leaveRule = getFtbAttendanceDailyRule(oldRule, leaveRule); + if (Objects.equals(oldRule.getSchedulesType(), 0)) { + Date leaveDate = getLeaveDate(oldRule); + boolean isMorningHalfDay = isHalfDayLeave(oldRule, leaveParam, 1); + boolean isAfternoonHalfDay = isHalfDayLeave(oldRule, leaveParam, 2); + + //请假开始是否为上半天请假 + if (isMorningHalfDay && !isAfternoonHalfDay) { + if (leaveDate.before(oldRule.getInPoint())) { + return; + } + //请假开始时间为开始日期当天排班开始时间 + leaveRule.setInPoint(oldRule.getOriginInPoint()); + leaveRule.setOutPoint(leaveDate); + leaveRule.setSchedulesType(oldRule.getSchedulesType()); + leaveRule.setLeaveDay(BigDecimal.valueOf(0.5)); + leaveRule.setSchedulesType(1); + leaveRule.insertTrue(); + getBreakTimeByOrdinary(oldRule, leaveRule); + housePayrollHoursProcess(leaveRule, oldRule.getPayrollHours(),oldRule.getOriginValidDuration()); + if (leaveDate.compareTo(oldRule.getOutPoint()) == 0) { + log.error("新增下半天请假时间覆盖原来全天班时段{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + hisDailyRules.remove(oldRule); + return; + } + oldRule.calInLackPoint(DateDetail.getNewDateByPeriod(oldRule.getInPoint(), oldRule.getInLackPoint(), leaveDate)); + oldRule.calLatePoint(DateDetail.getNewDateByPeriod(oldRule.getInPoint(), oldRule.getLatePoint(), leaveDate)); + oldRule.setInPoint(leaveDate); + oldRule.setInUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + oldRule.setSchedulesType(2); + housePayrollHoursProcess(oldRule, oldRule.getPayrollHours(), oldRule.getOriginValidDuration()); + log.error("新增上半天请假时间覆盖原来全天班时段{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + } else if (!isMorningHalfDay && isAfternoonHalfDay) { + if (leaveDate.after(oldRule.getOutPoint())) { + return; + } + leaveRule.setInPoint(leaveDate); + leaveRule.setSchedulesType(oldRule.getSchedulesType()); + leaveRule.setOutPoint(oldRule.getOriginOutPoint()); + leaveRule.setLeaveDay(BigDecimal.valueOf(0.5)); + leaveRule.setSchedulesType(2); + leaveRule.insertTrue(); + getBreakTimeByOrdinary(oldRule, leaveRule); + housePayrollHoursProcess(leaveRule, oldRule.getPayrollHours(), oldRule.getOriginValidDuration()); + if (leaveDate.compareTo(oldRule.getInPoint()) == 0) { + log.error("新增下半天请假时间覆盖原来全天班时段{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + hisDailyRules.remove(oldRule); + return; + } + oldRule.setEarlyPoint(DateDetail.getNewDateByPeriod(oldRule.getOutPoint(), oldRule.getEarlyPoint(), leaveDate)); + oldRule.setOutLackPoint(DateDetail.getNewDateByPeriod(oldRule.getOutPoint(), oldRule.getOutLackPoint(), leaveDate)); + oldRule.setOutPoint(leaveDate); + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()); + oldRule.setSchedulesType(1); + housePayrollHoursProcess(oldRule, oldRule.getPayrollHours(), oldRule.getOriginValidDuration()); + log.error("新增下半天请假时间覆盖原来全天班时段{}", JSON.toJSONString(leaveRule)); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + } else { + leaveRule.setInPoint(oldRule.getInPoint()); + leaveRule.setOutPoint(oldRule.getOutPoint()); + leaveRule.setSchedulesType(oldRule.getSchedulesType()); + getBreakTimeByOrdinary(oldRule, leaveRule); + houseLeaveDayProcess(leaveRule, oldRule); + leaveRule.insertTrue(); + hisDailyRules.remove(oldRule); + if (!hisDailyRules.contains(leaveRule)) { + hisDailyRules.add(leaveRule); + } + getBreakTimeByOrdinary(oldRule, leaveRule); + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + log.error("新增请假时间完全覆盖全天班{}", JSON.toJSONString(oldRule)); + } + return; + } + //是否为半天班 + if (!leaveRule.getSchedulesType().equals(oldRule.getSchedulesType())) { + return; + } + //请假开始时间为开始日期当天排班开始时间 + leaveRule.setInPoint(oldRule.getInPoint()); + leaveRule.setOutPoint(oldRule.getOutPoint()); + leaveRule.setSchedulesType(oldRule.getSchedulesType()); + if (!hisDailyRules.contains(leaveRule)) { + leaveRule.insertTrue(); + hisDailyRules.add(leaveRule); + } + getBreakTimeByOrdinary(oldRule, leaveRule); + leaveRule.setLeaveDay(BigDecimal.valueOf(0.5)); + leaveRule.setPayrollHours(oldRule.getPayrollHours()); + hisDailyRules.remove(oldRule); + log.error("新增半天请假时间覆盖原来半天班时段{}", JSON.toJSONString(leaveRule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, leaveRule)); + + } + + // 提取辅助方法,减少重复逻辑 + private boolean isHalfDayLeave(FtbAttendanceDailyRule oldRule, LeaveParam leaveParam, int timeType) { + // 开始日期且时间类型匹配 + boolean isStartDateMatch = Objects.equals(leaveParam.getStartTimeType(), timeType) && + oldRule.getDay().compareTo(leaveParam.getStart()) == 0; + + // 中间日期 + boolean isMiddleDate = (Objects.equals(timeType, 2) || oldRule.getDay().after(leaveParam.getStart())) && + (Objects.equals(timeType, 1) || oldRule.getDay().before(leaveParam.getEnd())); + + // 结束日期且时间类型匹配 + boolean isEndDateMatch = Objects.equals(leaveParam.getEndTimeType(), timeType) && + oldRule.getDay().compareTo(leaveParam.getEnd()) == 0; + + return isStartDateMatch || isMiddleDate || isEndDateMatch; + } + + /** + * 半天请假是否班次未命中请假 + * + * @param oldRule2 + * @param leaveParam + * @param leaveRule + * @return + */ + private static boolean isNotBetweenByHalfDayLeave(FtbAttendanceDailyRule oldRule2, LeaveParam leaveParam, FtbAttendanceDailyRule leaveRule) { + return !DateDetail.checkTimeBetween(oldRule2.getDay(), leaveParam.getStart(), leaveParam.getEnd()) + || !Objects.equals(oldRule2.getSchedulesType(), 0) && leaveParam.getStartTimeType() > oldRule2.getSchedulesType() && oldRule2.getDay().compareTo(leaveParam.getStart()) == 0 + || !Objects.equals(oldRule2.getSchedulesType(), 0) && leaveParam.getEndTimeType() < oldRule2.getSchedulesType() && oldRule2.getDay().compareTo(leaveParam.getEnd()) == 0; + } + + /** + * 计算半天请假全天班中间临界请假时间 + * + * @param ftbAttendanceDailyRule + * @return + */ + private Date getLeaveDate(FtbAttendanceDailyRule ftbAttendanceDailyRule) { + int ruleMinute = DateDetail.calculateMinuteDiff(ftbAttendanceDailyRule.getOriginInPoint(), ftbAttendanceDailyRule.getOriginOutPoint()); + Date dateTime = cn.hutool.core.date.DateUtil.offsetMinute(ftbAttendanceDailyRule.getOriginInPoint(), ruleMinute / 2); + return dateTime; + } + + /** + * 新增请假的休息时间处理 + * + * @param leaveRule 原请假规则 + * @param ordinaryRule 新增正常排班规则 + */ + private void getBreakTimeByOrdinary(FtbAttendanceDailyRule ordinaryRule, FtbAttendanceDailyRule leaveRule) { + if (!Objects.equals(ordinaryRule.getBreakEnable(), 1) || Objects.isNull(ordinaryRule.getBreakStartPoint())) { + return; + } + leaveRule.setBreakStartPoint(leaveRule.getInPoint().after(ordinaryRule.getBreakEndPoint())|| leaveRule.getOutPoint().before(ordinaryRule.getBreakStartPoint()) ? null : leaveRule.getInPoint().after(ordinaryRule.getBreakStartPoint()) ? leaveRule.getInPoint() : ordinaryRule.getBreakStartPoint()); + leaveRule.setBreakEndPoint(leaveRule.getInPoint().after(ordinaryRule.getBreakEndPoint()) || leaveRule.getOutPoint().before(ordinaryRule.getBreakStartPoint()) ? null : leaveRule.getOutPoint().before(ordinaryRule.getBreakEndPoint()) ? leaveRule.getOutPoint() : ordinaryRule.getBreakEndPoint()); + //休息结束时间小于排班上班时间,或者休息开始时间大于排班下班时间 + if (ordinaryRule.getBreakEndPoint().compareTo(ordinaryRule.getInPoint()) <= 0 || ordinaryRule.getBreakStartPoint().compareTo(ordinaryRule.getOutPoint()) >= 0) { + ordinaryRule.setBreakStartPoint(null); + ordinaryRule.setBreakEndPoint(null); + ordinaryRule.setBreakEnable(0); + return; + } + //请假时间完全覆盖原来休息时段 + if (DateDetail.checkTimeBetween(ordinaryRule.getBreakStartPoint(), leaveRule.getInPoint(), leaveRule.getOutPoint()) + && DateDetail.checkTimeBetween(ordinaryRule.getBreakEndPoint(), leaveRule.getInPoint(), leaveRule.getOutPoint())) { + ordinaryRule.setBreakStartPoint(null); + ordinaryRule.setBreakEndPoint(null); + ordinaryRule.setBreakEnable(0); + return; + } + //休息时间完全覆盖请假时间 + if (DateDetail.checkTimeBetween(leaveRule.getInPoint(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint()) + && DateDetail.checkTimeBetween(leaveRule.getOutPoint(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint())) { + if (leaveRule.getInPoint().compareTo(ordinaryRule.getBreakStartPoint()) >= 0) { + ordinaryRule.setBreakEndPoint(leaveRule.getInPoint()); + leaveRule.setBreakStartPoint(leaveRule.getInPoint()); + } + if (leaveRule.getOutPoint().compareTo(ordinaryRule.getBreakEndPoint()) <= 0) { + leaveRule.setBreakEndPoint(leaveRule.getOutPoint()); + ordinaryRule.setBreakEndPoint(leaveRule.getOutPoint()); + } + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetween(leaveRule.getOutPoint(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint())) { + leaveRule.setBreakEndPoint(leaveRule.getOutPoint()); + ordinaryRule.setBreakStartPoint(leaveRule.getOutPoint()); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetween(leaveRule.getInPoint(), ordinaryRule.getBreakStartPoint(), ordinaryRule.getBreakEndPoint())) { + ordinaryRule.setBreakEndPoint(leaveRule.getInPoint()); + leaveRule.setBreakStartPoint(leaveRule.getInPoint()); + } + } + + private void houseLeaveDayProcess(FtbAttendanceDailyRule leaveRule, FtbAttendanceDailyRule ordinaryRule) { + houseLeaveDayProcess(leaveRule, ordinaryRule.getOriginValidDuration(), ordinaryRule.getPeriodWorkDay(), ordinaryRule.getPayrollHours()); + } + + private void houseLeaveDayProcess(FtbAttendanceDailyRule leaveRule, Integer validDuration, BigDecimal periodWorkDay, BigDecimal payrollHours) { + leaveRule.calValidDuration(); + leaveRule.setLeaveDay(Objects.isNull(periodWorkDay) ? null : BigDecimal.valueOf(leaveRule.getValidDuration()) + .divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP) + .multiply(periodWorkDay).setScale(2, RoundingMode.HALF_UP)); + housePayrollHoursProcess(leaveRule, payrollHours, validDuration); + } + + private void housePayrollHoursProcess(FtbAttendanceDailyRule leaveRule, BigDecimal payrollHours, Integer validDuration) { + leaveRule.calValidDuration(); + leaveRule.setPayrollHours(Objects.isNull(payrollHours) ? null : BigDecimal.valueOf(leaveRule.getValidDuration()) + .divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP) + .multiply(payrollHours).setScale(2, RoundingMode.HALF_UP)); + } + + /** + * 新增上班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean ordinaryHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增上班 + if (!Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), rule.getAttendanceType()) || rule.getIsInsert()) { + return Boolean.FALSE; + } + if (hisDailyRules.containsAll(ftbAttendanceDailyRules)) { + if (Objects.equals(rule.getFixedMark(), 1)) { + ftbAttendanceDailyRules = ftbAttendanceDailyRules.stream().filter(rule1 -> !Objects.equals(rule1.getFixedMark(), 3)).collect(Collectors.toList()); + } + hisDailyRules.removeAll(ftbAttendanceDailyRules); + } + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + if (!hisDailyRules.contains(rule)) { + if(ftbAttendanceDailyRules.stream().anyMatch(vo->Objects.equals(vo.getApplyViewEnable(),3))){ + rule.setApplyViewEnable(3); + } + hisDailyRules.add(rule); + } + } + ftbAttendanceDailyRules.forEach(oldRule -> { + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增排休 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean restHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增排休 + if (!Objects.equals(AttendanceTypeEnum.REST.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.HOLIDAYS.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + if (Objects.equals(rule.getFixedMark(), 1)) { + ftbAttendanceDailyRules = ftbAttendanceDailyRules.stream().filter(rule1 -> !Objects.equals(rule1.getFixedMark(), 3)).collect(Collectors.toList()); + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + hisDailyRules.remove(oldRule); + }); + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + } + log.error("新增排休覆盖原来上班时段{},目标结果为{}", JSON.toJSONString(rule), JSON.toJSONString(hisDailyRules)); + ftbAttendanceDailyRules.forEach(oldRule -> { + resultList.add(DailyRuleResultVo.successRestBuild(oldRule, rule, rule.getPeriodWorkDay())); + }); + return Boolean.TRUE; + } + + /** + * 新增加班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean workOvertimeHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增加班 + if (!Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setSelfGroup(oldRule.getSelfGroup()); + rule.setFixedMark(oldRule.getFixedMark()); + //完全覆盖 + if (DateDetail.checkTimeBetween(rule.getApplyStart(), oldRule.getInPoint(), oldRule.getOutPoint()) + && DateDetail.checkTimeBetween(rule.getApplyEnd(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.ORDINARY)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode())); + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetweenR(rule.getApplyEnd(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.ORDINARY)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode())); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetweenL(rule.getApplyStart(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.ORDINARY)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode())); + return; + } + //加班时间完全覆盖原来上班时段 + if (DateDetail.checkBetween(oldRule.getInPoint(), rule.getApplyStart(), rule.getApplyEnd()) + && DateDetail.checkBetween(oldRule.getOutPoint(), rule.getApplyStart(), rule.getApplyEnd())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.ORDINARY)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode())); + return; + } + //加班与普班时间边界连接 + //加班结束时间与上班时段开始时间相同 + if (oldRule.getInPoint().compareTo(rule.getApplyEnd()) == 0) { + oldRule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + } + //加班开始时间与上班时段结束时间相同 + if (oldRule.getOutPoint().compareTo(rule.getApplyStart()) == 0) { + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + } + rule.insertTrue(); + //完全没交集 + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增加班与原排班时段中间没交集{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successNailBuild(rule)); + }); + return Boolean.TRUE; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/RestRuleProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/RestRuleProcessor.java new file mode 100644 index 0000000..289af07 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/RestRuleProcessor.java @@ -0,0 +1,159 @@ +package jnpf.attendance.service.handle.rule; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.RuleProcessor; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 处理历史排休 + */ +@Component +@Slf4j +public class RestRuleProcessor extends RuleProcessor { + @Override + public void ruleArrangementHandle(List hisDailyRules, List currDailyRule, List resultList) { + + Map> collect = hisDailyRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getAttendanceType)); + List ftbAttendanceDailyRules = CollUtil.newArrayList(); + if (collect.containsKey(AttendanceTypeEnum.DEFAULT.getCode())) { + hisDailyRules.removeAll(collect.get(AttendanceTypeEnum.DEFAULT.getCode())); + } + if (collect.containsKey(AttendanceTypeEnum.REST.getCode())) { + ftbAttendanceDailyRules.addAll(collect.getOrDefault(AttendanceTypeEnum.REST.getCode(), List.of()).stream().filter(rule -> !rule.getIsInsert()).collect(Collectors.toList())); + } + if (collect.containsKey(AttendanceTypeEnum.HOLIDAYS.getCode())) { + ftbAttendanceDailyRules.addAll(collect.get(AttendanceTypeEnum.HOLIDAYS.getCode())); + } + if (collect.containsKey(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode())) { + ftbAttendanceDailyRules.addAll(collect.get(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode())); + } + if (collect.containsKey(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())) { + ftbAttendanceDailyRules.addAll(collect.get(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())); + } + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + Iterator iterator = currDailyRule.iterator(); + while (iterator.hasNext()) { + FtbAttendanceDailyRule rule = iterator.next(); + //新增清除 + if (clearHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + if (stepOutHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + if (Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())) { + if (!hisDailyRules.contains(rule)) { + rule.setApplyViewEnable(0); + rule.insertTrue(); + hisDailyRules.add(rule); + } + continue; + } + if (Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())) { + if (!hisDailyRules.contains(rule)) { + rule.setApplyViewEnable(1); + rule.insertTrue(); + hisDailyRules.add(rule); + resultList.add(DailyRuleResultVo.successBuild(rule)); + } + continue; + } + if (Objects.equals(AttendanceTypeEnum.REST.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), rule.getAttendanceType())) { + log.error("新增{}覆盖原来休{}",AttendanceTypeEnum.getEnum(rule.getAttendanceType()).getMsg(), JSON.toJSONString(rule)); + ftbAttendanceDailyRules.forEach(oldRule -> { + hisDailyRules.remove(oldRule); + resultList.add(DailyRuleResultVo.successRestBuild(oldRule, rule, rule.getPeriodWorkDay())); + }); + if (!hisDailyRules.contains(rule)) { + rule.setApplyViewEnable(1); + hisDailyRules.add(rule); + } + continue; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + resultList.add(DailyRuleResultVo.successRestBuild(oldRule, rule, oldRule.getPeriodWorkDay())); + }); + + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + if (!Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())) { + rule.setApplyViewEnable(1); + } + hisDailyRules.add(rule); + } + + } + } + if (Objects.nonNull(nextRuleProcessor)) { + nextRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList); + } + } + + /** + * 新增外出、出差申请 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean stepOutHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增请假 + if (!Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType()) && !Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + rule.setApplyViewEnable(0); + rule.insertTrue(); + oldRule.setInPoint(rule.getInPoint()); + oldRule.setOutPoint(rule.getOutPoint()); + oldRule.setInStepOutType(1); + oldRule.setOutStepOutType(1); + oldRule.setInStepOutApplyId(rule.getApplyId()); + oldRule.setOutStepOutApplyId(rule.getApplyId()); + oldRule.setApplyViewEnable(rule.getAttendanceType()); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增外出、出差申请标记原排休时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增清除 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean clearHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增清除 + if (!Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + resultList.add(DailyRuleResultVo.successBuild(oldRule)); + hisDailyRules.remove(oldRule); + }); + return Boolean.TRUE; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/StepOutRuleProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/StepOutRuleProcessor.java new file mode 100644 index 0000000..f0627fd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/StepOutRuleProcessor.java @@ -0,0 +1,177 @@ +package jnpf.attendance.service.handle.rule; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.RuleProcessor; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 处理历史出差、外出申请 + */ +@Component +@Slf4j +public class StepOutRuleProcessor extends RuleProcessor { + @Override + public void ruleArrangementHandle(List hisDailyRules, List currDailyRule, List resultList) { + + Map> collect = hisDailyRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getAttendanceType)); + List ftbAttendanceDailyRules = CollUtil.newArrayList(); + if (collect.containsKey(AttendanceTypeEnum.BUSINESS_TRIP.getCode())) { + ftbAttendanceDailyRules.addAll(collect.get(AttendanceTypeEnum.BUSINESS_TRIP.getCode())); + } + if (collect.containsKey(AttendanceTypeEnum.STEP_OUT.getCode())) { + ftbAttendanceDailyRules.addAll(collect.get(AttendanceTypeEnum.STEP_OUT.getCode())); + } + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + currDailyRule.forEach(rule -> { + //新增清除 + if (clearHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + return; + } + if (!hisDailyRules.contains(rule) && !rule.getIsInsert()) { + hisDailyRules.add(rule); + } + + }); + Iterator iterator = currDailyRule.iterator(); + while (iterator.hasNext()) { + FtbAttendanceDailyRule rule = iterator.next(); + //新增清除 + if (Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType())) { + continue; + } + daysStepOutHandle(hisDailyRules, resultList, ftbAttendanceDailyRules, rule); + housesStepOutHandle(hisDailyRules, resultList, ftbAttendanceDailyRules, rule); + } + } + if (Objects.nonNull(nextRuleProcessor)) { + nextRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList); + } + } + + /** + * 处理单位为天的外出、出差规则冲突 + * 该方法用于处理在已有的日常规则结果列表中,新加入的外出或出差规则与已有规则之间的冲突 + * 如果新规则与已有规则冲突且为外出或出差类型,则记录错误日志并添加失败结果到列表中 + * 如果不冲突,则更新新旧规则的申请查看权限,并添加成功结果到列表中 + * + * @param resultList 日常规则结果列表,用于存储处理结果 + * @param ftbAttendanceDailyRules 已有的日常规则列表,用于比较和更新 + * @param rule 新加入的日常规则,用于检查冲突和更新 + */ + private void daysStepOutHandle(List hisDailyRules, List resultList, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule) { + List collect = ftbAttendanceDailyRules.stream().filter(oldRule -> Objects.equals(oldRule.getApplyUnit(), 2) || Objects.isNull(oldRule.getApplyUnit())).collect(Collectors.toList()); + collect.forEach(oldRule -> { + if (StringUtil.equals(oldRule.getId(), rule.getId())) { + return; + } + if (Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType())) { + log.error(AttendanceConstant.getSchedulingResult(rule.getUserId(), rule.getDay(), AttendanceTypeEnum.getMsg(oldRule.getAttendanceType()), AttendanceTypeEnum.getMsg(rule.getAttendanceType()))); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, oldRule.getAttendanceType(), rule.getAttendanceType())); + log.error("新增外出、出差申请与原外出、出差申请冲突{}", JSON.toJSONString(rule)); + return; + } + rule.setApplyViewEnable(oldRule.getAttendanceType()); + rule.setInStepOutType(1); + rule.setOutStepOutType(1); + rule.setInStepOutApplyId(oldRule.getApplyId()); + rule.setOutStepOutApplyId(oldRule.getApplyId()); + oldRule.setApplyViewEnable(0); + log.error("原外出、出差申请标记新增排班时段{}", JSON.toJSONString(rule)); + if (!oldRule.getIsInsert() && !hisDailyRules.contains(rule) && !rule.getIsInsert()) { + hisDailyRules.add(rule); + rule.insertTrue(); + } + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + } + + /** + * 处理单位为小时的外出、出差规则冲突 + * 该方法用于处理在已有的日常规则结果列表中,新加入的外出或出差规则与已有规则之间的冲突 + * 如果新规则与已有规则冲突且为外出或出差类型,则记录错误日志并添加失败结果到列表中 + * 如果不冲突,则更新新旧规则的申请查看权限,并添加成功结果到列表中 + * + * @param resultList 日常规则结果列表,用于存储处理结果 + * @param ftbAttendanceDailyRules 已有的日常规则列表,用于比较和更新 + * @param rule 新加入的日常规则,用于检查冲突和更新 + */ + private void housesStepOutHandle(List hisDailyRules, List resultList, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule) { + List collect = ftbAttendanceDailyRules.stream().filter(oldRule -> Objects.equals(oldRule.getApplyUnit(), 1) && !oldRule.equals(rule)).collect(Collectors.toList()); + collect.forEach(stepOutRule -> { + if (Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode()) || Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode())) { + if (!stepOutRule.getOutPoint().before(rule.getInPoint()) && !stepOutRule.getInPoint().after(rule.getOutPoint())) { + resultList.add(DailyRuleResultVo.failBuild(stepOutRule, rule, stepOutRule.getAttendanceType(), rule.getAttendanceType())); + log.error("新增小时外出申请与原外出申请冲突{}", JSON.toJSONString(rule)); + return; + } + } + if(Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) && Objects.isNull(rule.getInPoint()) ){ + rule.setApplyViewEnable(0); + if (!hisDailyRules.contains(rule) && !rule.getIsInsert()) { + hisDailyRules.add(rule); + rule.insertTrue(); + } + return; + } + //原小时外出没命中已有班次数据,隐藏外出申请 + if (stepOutRule.getOutPoint().before(rule.getInPoint()) + || stepOutRule.getInPoint().after(rule.getOutPoint()) + || stepOutRule.getInPoint().after(rule.getInPoint()) && stepOutRule.getOutPoint().before(rule.getOutPoint())) { + if (!stepOutRule.getIsInsert() && !hisDailyRules.contains(rule) && !rule.getIsInsert()) { + hisDailyRules.add(rule); + rule.insertTrue(); + } + log.error("排班数据未命中小时外出申请或者外出数据完全被排班数据覆盖,过滤{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successNailBuild(rule)); + return; + } + stepOutRule.setApplyViewEnable(0); + rule.setApplyViewEnable(stepOutRule.getAttendanceType()); + //原小时外出数据覆盖现在新增排班数据上班时间 + if (!stepOutRule.getInPoint().after(rule.getInPoint())) { + rule.setInStepOutType(1); + rule.setInStepOutApplyId(stepOutRule.getApplyId()); + } + //原小时外出数据覆盖现在新增排班数据下班时间 + if (!stepOutRule.getOutPoint().before(rule.getOutPoint())) { + rule.setOutStepOutType(1); + rule.setOutStepOutApplyId(stepOutRule.getApplyId()); + } + if (!stepOutRule.getIsInsert() && !hisDailyRules.contains(rule) && !rule.getIsInsert()) { + hisDailyRules.add(rule); + rule.insertTrue(); + } + log.error("新增外出、出差申请标记排班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(stepOutRule, rule)); + }); + } + + /** + * 新增清除 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean clearHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增清除 + if (!Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> oldRule.setApplyViewEnable(oldRule.getAttendanceType())); + return Boolean.TRUE; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/WorkOvertimeRuleProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/WorkOvertimeRuleProcessor.java new file mode 100644 index 0000000..e43626d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/handle/rule/WorkOvertimeRuleProcessor.java @@ -0,0 +1,487 @@ +package jnpf.attendance.service.handle.rule; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import jnpf.attendance.service.RuleProcessor; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.LeaveUnitEnum; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.util.DateDetail; +import jnpf.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 处理历史加班申请 + */ +@Component +@Slf4j +public class WorkOvertimeRuleProcessor extends RuleProcessor { + @Override + public void ruleArrangementHandle(List hisDailyRules, List currDailyRule, List resultList) { + Map> collect = hisDailyRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getAttendanceType)); + List ftbAttendanceDailyRules = collect.getOrDefault(AttendanceTypeEnum.WORKOVERTIME.getCode(), List.of()).stream().filter(rule -> Objects.isNull(rule.getIsInsert()) || !rule.getIsInsert()).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + if (currDailyRule.stream().allMatch(rule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), rule.getAttendanceType()) + || Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType()))) { + ftbAttendanceDailyRules.forEach(oldRule -> { + oldRule.setOutUnbounded(0); + oldRule.setInUnbounded(0); + oldRule.setApplyViewEnable(1); + }); + } + + Iterator iterator = currDailyRule.iterator(); + Boolean isRecover = Boolean.TRUE; + + while (iterator.hasNext()) { + FtbAttendanceDailyRule rule = iterator.next(); + //新增清除 + if (clearHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增排休 + if (restHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增上班 + if (ordinaryHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList, isRecover)) { + isRecover = Boolean.FALSE; + continue; + } + //新增加班 + if (workOvertimeHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增请假 + if (leaveHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + //新增外出、出差 + if (stepOutHandle(hisDailyRules, ftbAttendanceDailyRules, rule, resultList)) { + continue; + } + } + } + if (Objects.nonNull(nextRuleProcessor)) { + nextRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList); + } + } + + /** + * 新增清除 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean clearHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增清除 + if (!Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + recoverHandle(hisDailyRules, ftbAttendanceDailyRules); + return Boolean.TRUE; + } + + /** + * 恢复原加班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + */ + private void recoverHandle(List hisDailyRules, List ftbAttendanceDailyRules) { + //恢复原加班 + hisDailyRules.removeAll(hisDailyRules.stream().filter(rule -> !rule.getIsInsert() && Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList())); + Map> collect = ftbAttendanceDailyRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)); + collect.forEach((k, rules) -> { + FtbAttendanceDailyRule ftbAttendanceDailyRule = rules.stream().sorted(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)).findFirst().orElse(null); + ftbAttendanceDailyRule.setInPoint(ftbAttendanceDailyRule.getApplyStart()); + ftbAttendanceDailyRule.setOutPoint(ftbAttendanceDailyRule.getApplyEnd()); + hisDailyRules.removeAll(rules); + hisDailyRules.add(ftbAttendanceDailyRule); + }); + } + + /** + * 新增外出、出差申请 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean stepOutHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增请假 + if (!Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType()) && !Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //过滤隐藏规则 + if (Objects.equals(oldRule.getApplyViewEnable(), 0)) { + return; + } + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setShiftId(oldRule.getShiftId()); + rule.setPeriodId(oldRule.getPeriodId()); + rule.setApplyViewEnable(0); + //原小时外出没命中已有班次数据,隐藏外出申请 + if (rule.getOutPoint().before(oldRule.getInPoint()) + || rule.getInPoint().after(oldRule.getOutPoint()) + || rule.getInPoint().after(oldRule.getInPoint()) && rule.getOutPoint().before(oldRule.getOutPoint())) { + if (!rule.getIsInsert()) { + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + } + log.error("新增排班数据未命中小时外出申请或者外出数据完全被排班数据覆盖,过滤{}", JSON.toJSONString(rule)); + return; + } + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + rule.setApplyViewEnable(0); + oldRule.setApplyViewEnable(rule.getAttendanceType()); + //原小时外出数据覆盖现在新增排班数据上班时间 + if (!rule.getInPoint().after(oldRule.getInPoint())) { + oldRule.setInStepOutType(1); + oldRule.setInStepOutApplyId(rule.getApplyId()); + } + //原小时外出数据覆盖现在新增排班数据下班时间 + if (!rule.getOutPoint().before(oldRule.getOutPoint())) { + oldRule.setOutStepOutType(1); + oldRule.setOutStepOutApplyId(rule.getApplyId()); + } + log.error("新增外出、出差申请标记原加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增请假 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean leaveHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增请假 + if (!Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + //全天假与加班冲突 + if (Objects.equals(rule.getApplyUnit(), LeaveUnitEnum.DAY.getCode())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + FtbAttendanceDailyRule oldRule = ftbAttendanceDailyRules.stream().findFirst().orElse(null); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return Boolean.TRUE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setSelfGroup(oldRule.getSelfGroup()); + //完全覆盖 + //原来加班时段完全覆盖请假时间 + if (DateDetail.checkBetween(rule.getApplyStart(), oldRule.getInPoint(), oldRule.getOutPoint()) + && DateDetail.checkBetween(rule.getApplyEnd(), oldRule.getInPoint(), oldRule.getOutPoint()) + || (rule.getApplyStart().compareTo(oldRule.getInPoint()) == 0 && rule.getApplyEnd().compareTo(oldRule.getOutPoint()) == 0)) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkBetween(rule.getApplyEnd(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + //结束部分覆盖 + if (DateDetail.checkBetween(rule.getApplyStart(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + //请假完全覆盖加班 + if (DateDetail.checkTimeBetweenL(oldRule.getApplyStart(), rule.getInPoint(), rule.getOutPoint()) + && DateDetail.checkTimeBetweenR(oldRule.getApplyEnd(), rule.getInPoint(), rule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + rule.insertTrue(); + //完全没交集 + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("请假时间完全与原来加班时段完全没交集{}", JSON.toJSONString(rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增加班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean workOvertimeHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增加班 + if (!Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + rule.setSelfGroup(oldRule.getSelfGroup()); + //新增加班时间完全覆盖原来加班时段 + if (DateDetail.checkTimeBetween(oldRule.getApplyStart(), rule.getInPoint(), rule.getOutPoint()) + && DateDetail.checkTimeBetween(oldRule.getApplyEnd(), rule.getInPoint(), rule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkBetween(rule.getApplyEnd(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + //结束部分覆盖 + if (DateDetail.checkBetween(rule.getApplyStart(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + //新增加班时段处于原加班时段中间 + if (DateDetail.checkTimeBetweenL(rule.getApplyStart(), oldRule.getInPoint(), oldRule.getOutPoint()) + && DateDetail.checkTimeBetweenR(rule.getApplyEnd(), oldRule.getInPoint(), oldRule.getOutPoint())) { + log.error(AttendanceConstant.getApplyResult(rule.getUserId(), AttendanceConstant.WORKOVERTIME)); + resultList.add(DailyRuleResultVo.failBuild(oldRule, rule, AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType())); + return; + } + //加班与普班时间边界连接 + //加班开始时间与上班时段结束时间相同 + if (oldRule.getInPoint().compareTo(rule.getOutPoint()) == 0) { + oldRule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + } + //加班结束时间与上班时段开始时间相同 + if (oldRule.getOutPoint().compareTo(rule.getInPoint()) == 0) { + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + } + //完全没交集 + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增加班时段与原加班时段没交集{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增上班 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean ordinaryHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList, Boolean isRecover) { + if (!Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + if (isRecover) { + recoverHandle(hisDailyRules, ftbAttendanceDailyRules); + } + //新增上班 + ftbAttendanceDailyRules = hisDailyRules.stream().filter(rule1 -> Objects.equals(rule1.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).collect(Collectors.toList()); + ftbAttendanceDailyRules.forEach(oldRule -> { + //新加入数据不做处理 + if (oldRule.getIsInsert()) { + return; + } + //原加班时段处于新增上班时段中间 + if (DateDetail.checkTimeBetween(oldRule.getInPoint(), rule.getInPoint(), rule.getOutPoint()) + && DateDetail.checkTimeBetween(oldRule.getOutPoint(), rule.getInPoint(), rule.getOutPoint())) { + oldRule.setApplyViewEnable(0); + oldRule.setShiftId(rule.getShiftId()); + oldRule.setPeriodId(rule.getPeriodId()); + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增上班时段完全覆盖原来加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkBetween(rule.getOutPoint(), oldRule.getInPoint(), oldRule.getOutPoint()) + && rule.getInPoint().compareTo(oldRule.getApplyStart()) <= 0) { + oldRule.setInPoint(rule.getOutPoint()); + oldRule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + oldRule.setShiftId(rule.getShiftId()); + oldRule.setPeriodId(rule.getPeriodId()); + oldRule.setApplyViewEnable(1); + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增上班时段开始部分覆盖原来加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //结束部分覆盖 + if (DateDetail.checkBetween(rule.getInPoint(), oldRule.getInPoint(), oldRule.getOutPoint()) + && rule.getOutPoint().compareTo(oldRule.getApplyEnd()) >= 0) { + oldRule.setOutPoint(rule.getInPoint()); + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + oldRule.setShiftId(rule.getShiftId()); + oldRule.setPeriodId(rule.getPeriodId()); + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增上班时段结束部分覆盖原来加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //原来加班时段完全覆盖新增上班时间 + if (DateDetail.checkTimeBetween(rule.getInPoint(), oldRule.getInPoint(), oldRule.getOutPoint()) + && DateDetail.checkTimeBetween(rule.getOutPoint(), oldRule.getInPoint(), oldRule.getOutPoint())) { + oldRule.setShiftId(rule.getShiftId()); + oldRule.setPeriodId(rule.getPeriodId()); + FtbAttendanceDailyRule oldRule1 = BeanUtil.toBean(oldRule, FtbAttendanceDailyRule.class); + oldRule.setOutPoint(rule.getInPoint()); + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + oldRule1.setInPoint(rule.getOutPoint()); + oldRule1.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + oldRule1.setId(RandomUtil.uuId()); + oldRule1.setApplyViewEnable(1); + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + hisDailyRules.add(oldRule1); + log.error("新增上班时间处于原来加班时段中间{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //加班与普班时间边界连接 + //加班开始时间与上班时段结束时间相同 + if (oldRule.getInPoint().compareTo(rule.getOutPoint()) == 0) { + oldRule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + } + //加班结束时间与上班时段开始时间相同 + if (oldRule.getOutPoint().compareTo(rule.getInPoint()) == 0) { + oldRule.setOutUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + rule.setInUnbounded(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode()); + } + //完全没交集 + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + log.error("新增上班时段与原加班时段没交集{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + return Boolean.TRUE; + } + + /** + * 新增排休 + * + * @param hisDailyRules 历史规则 + * @param ftbAttendanceDailyRules 新增规则集合 + * @param rule 当前规则 + */ + private Boolean restHandle(List hisDailyRules, List ftbAttendanceDailyRules, FtbAttendanceDailyRule rule, List resultList) { + //新增排休 + if (!Objects.equals(AttendanceTypeEnum.REST.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.HOLIDAYS.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), rule.getAttendanceType()) + && !Objects.equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), rule.getAttendanceType())) { + return Boolean.FALSE; + } + recoverHandle(hisDailyRules, ftbAttendanceDailyRules); + //休不存在打卡时间默认为当天最早、最后时间 + rule.setInPoint(Objects.isNull(rule.getInPoint()) ? DateUtil.beginOfDay(rule.getDay()) : rule.getInPoint()); + rule.setOutPoint(Objects.isNull(rule.getOutPoint()) ? DateUtil.endOfDay(rule.getDay()) : rule.getOutPoint()); + rule.insertTrue(); + if (!hisDailyRules.contains(rule)) { + hisDailyRules.add(rule); + } + ftbAttendanceDailyRules.forEach(oldRule -> { + //原加班时段处于休息时段中间 + if (DateDetail.checkTimeBetween(oldRule.getApplyStart(), rule.getInPoint(), rule.getOutPoint()) + && DateDetail.checkTimeBetween(oldRule.getApplyEnd(), rule.getInPoint(), rule.getOutPoint())) { + oldRule.setInPoint(oldRule.getApplyStart()); + oldRule.setOutPoint(oldRule.getApplyEnd()); + log.error("新增休完全覆盖原来加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetween(rule.getOutPoint(), oldRule.getApplyStart(), oldRule.getApplyEnd()) + && rule.getInPoint().compareTo(oldRule.getApplyStart()) <= 0) { + oldRule.setInPoint(oldRule.getApplyStart()); + oldRule.setApplyViewEnable(1); + log.error("新增休开始部分覆盖原来加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetween(rule.getInPoint(), oldRule.getApplyStart(), oldRule.getApplyEnd()) + && rule.getOutPoint().compareTo(oldRule.getApplyEnd()) >= 0) { + oldRule.setOutPoint(oldRule.getApplyEnd()); + log.error("新增休结束部分覆盖原来加班时段{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + //原来加班时段完全覆盖休时间 + if (DateDetail.checkTimeBetween(rule.getInPoint(), oldRule.getApplyStart(), oldRule.getApplyEnd()) + && DateDetail.checkTimeBetween(rule.getOutPoint(), oldRule.getApplyStart(), oldRule.getApplyEnd())) { + log.error("原来加班时段完全覆盖新增休时间{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + return; + } + oldRule.setShiftId(rule.getShiftId()); + oldRule.setPeriodId(rule.getPeriodId()); + //完全没交集 + log.error("新增休与原加班时段没交集{}", JSON.toJSONString(rule)); + resultList.add(DailyRuleResultVo.successBuild(oldRule, rule)); + }); + log.error("新增排休与原加班时段,过滤新增排休{}", JSON.toJSONString(rule)); + return Boolean.TRUE; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AppStatisticsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AppStatisticsServiceImpl.java new file mode 100644 index 0000000..bcea5f0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AppStatisticsServiceImpl.java @@ -0,0 +1,1443 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.util.StringUtil; +import com.google.common.collect.Maps; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceClockInMapper; +import jnpf.attendance.mapper.AttendanceDayStatisticsMapper; +import jnpf.attendance.mapper.AttendanceLeaveApproveMapper; +import jnpf.attendance.mapper.StatisticsMapper; +import jnpf.attendance.service.*; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.entity.attendance.AttendanceClockInPic; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.common.DateRangeDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.util.*; +import jnpf.util.attendance.DayStatisticsUtils; +import jnpf.util.auth.V2AuthPermissionUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.hutool.core.collection.CollUtil.newArrayList; +import static jnpf.enums.attendance.StatisticsEnumUtil.TabDataTypeEnum.JD; +import static jnpf.enums.attendance.StatisticsEnumUtil.TabDataTypeEnum.getTabDataTypeEnum; +import static jnpf.util.DateUtil.stringToDates; +import static jnpf.util.attendance.DayStatisticsUtils.getCollect; + +@Slf4j +@Service +public class AppStatisticsServiceImpl implements AppStatisticsService { + @Resource + private V2UserApi v2UserApi; + @Autowired + private UserAntifreeze userApi; + @Resource + private StatisticsMapper statisticsMapper; + @Resource + private StatisticsUtilService utilService; + @Resource + private AttendanceGroupService groupService; + @Resource + private AttendanceUserService userService; + @Resource + private AttendanceClockInMapper clockInMapper; + @Resource + private AttendanceClockInService clockInService; + @Resource + private V2AuthPermissionUtils authPermissionUtils; + @Resource + private AttendanceDailyRuleService dailyRuleService; + @Resource + private AttendanceClockInPicService clockInPicService; + @Resource + private AttendanceBaseSettingService baseSettingService; + @Resource + private AttendanceLeaveApproveMapper leaveApproveMapper; + @Resource + private AttendanceDayStatisticsMapper dayStatisticsMapper; + @Resource + private AttendanceDayStatisticsService dayStatisticsService; + + + @Override + public AppStatisticsListVo getAppHomeData(AppStatisticsListDto req) { + Date start = DateDetail.getMonthBeginDate(req.getMonth()); + Date end = DateDetail.getMonthEndDate(req.getMonth()); + List groupUserList = userService.getAttendanceGroupUsersOfSecondment(start, end, + newArrayList(req.getUserId()), null); + AppStatisticsListVo appStatisticsListVo = new AppStatisticsListVo(); + if (CollUtil.isEmpty(groupUserList)) { + return appStatisticsListVo; + } + //月份格式化返回月份开始结束日期 + DateRangeDto selectDateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + DateRangeDto showDateRangeDto = new DateRangeDto(req.getMonth(), Boolean.FALSE); + MonthStatisticsDataQueryDto monthStatisticsQueryDto = MonthStatisticsDataQueryDto.builder() + .startDate(selectDateRangeDto.getStartDate()) + .endDate(selectDateRangeDto.getEndDate()) + .userIds(Collections.singletonList(req.getUserId())) + .queryType(1) + .build(); + List pageList = dayStatisticsMapper.getMonthPageList(monthStatisticsQueryDto); + if (CollUtil.isEmpty(pageList)) { + return appStatisticsListVo; + } + //获取用户的应出勤天数 + appStatisticsListVo.setActualAttendDays(pageList.stream() + .map(MonthStatisticsQueryVo::getEffectiveAttendDays).reduce(BigDecimal::add).orElse(BigDecimal.ZERO)); + appStatisticsListVo.setLateTimes(pageList.stream().map(MonthStatisticsQueryVo::getLateTimes) + .reduce(Integer::sum).orElse(0)); + appStatisticsListVo.setEarlyLeaveTimes(pageList.stream() + .map(MonthStatisticsQueryVo::getEarlyLeaveTimes).reduce(Integer::sum).orElse(0)); + appStatisticsListVo.setMissingCardTimes(pageList.stream() + .map(MonthStatisticsQueryVo::getAbsenceCardTimes).reduce(Integer::sum).orElse(0)); + appStatisticsListVo.setAbsentTimes(pageList.stream() + .map(MonthStatisticsQueryVo::getAbsenceTimes).reduce(Integer::sum).orElse(0)); + //生成每月考勤展示数据 + List dateArrayList = new ArrayList<>(); + LocalDate startDate = LocalDate.parse(showDateRangeDto.getStartDate()); + LocalDate endDate = LocalDate.parse(showDateRangeDto.getEndDate()); + while (startDate.isBefore(endDate) || startDate.isEqual(endDate)) { + dateArrayList.add(Date.from(startDate.atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant())); + startDate = startDate.plusDays(1); + } + //获取日统计数据 + DayStatisticsDataPageListQueryDto dayStatisticsQueryDto = DayStatisticsDataPageListQueryDto.builder() + .startDate(selectDateRangeDto.getStartDate()) + .endDate(selectDateRangeDto.getEndDate()) + .userIds(Collections.singletonList(req.getUserId())) + .queryType(1) + .build(); + List dayStatisticsQueryVoList = dayStatisticsMapper.getDayPageList(dayStatisticsQueryDto); + //获取每日排班信息-app首页点点展示 + appStatisticsListVo.setClockInfoList(utilService.getUserClockDateArrayList(req.getUserId(), + selectDateRangeDto, dateArrayList, dayStatisticsQueryVoList)); + return appStatisticsListVo; + } + + /** + * 添加原始数据到列表 + * + * @param clockIns 打卡详情 + * @param allRuleIds 今日所有出勤规则 + * @param ruleIds 需要排除的记录 + */ + private void setOldData(List clockIns, List allRuleIds, List ruleIds, String userId, Date day) { + // 查询今日所有打卡 + List clockInList = clockInMapper.getClockInByStatistics(userId, day); + if (clockInList.isEmpty()) { + return; + } + List finalSorted = getClockInListSort(clockIns); + List collect = clockInList.stream().filter(v -> allRuleIds.contains(v.getRuleId())).collect(Collectors.toList()); + if (!collect.isEmpty()) { + List indexList = new ArrayList<>(); + collect.forEach(v -> indexList.add(clockInList.indexOf(v))); + Integer minDelIndex = null; + Integer maxDelIndex = null; + // 获取最大 和 最小的 index + int minIndex = indexList.get(0); + int maxIndex = indexList.get(indexList.size() - 1); + if (minIndex > 0) { + // 往上查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往上所有记录 + for (int i = indexList.get(0) - 1; i > 0; i--) { + ClockClassRecord vo = clockInList.get(i); + if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) { + minDelIndex = i; + break; + } + } + } + if (maxIndex < clockInList.size() - 1) { + // 往下查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往下所有记录 + for (int i = indexList.get(0) + 1; i < clockInList.size(); i++) { + ClockClassRecord vo = clockInList.get(i); + if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) { + maxDelIndex = i; + break; + } + } + } + // 排除 ruleId 在 ruleIds 中的记录 和 minDelIndex 往上的记录 和 maxDelIndex 往下的记录 + List removeList = new ArrayList<>(); + if (null != minDelIndex) { + removeList.addAll(clockInList.subList(0, minDelIndex)); + } + if (null != maxDelIndex) { + removeList.addAll(clockInList.subList(maxDelIndex, clockInList.size() - 1)); + } + clockInList.removeAll(removeList); + clockInList.removeIf(v -> ruleIds.contains(v.getRuleId())); + } + if (!clockInList.isEmpty()) { + // 转换数据 clockInTime 格式化, 打卡方式 转换成文字 + clockInList.forEach(v -> { + String str = DateDetail.getDate2Str(day, DateDetail.DF).equals(DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF)) ? "" : "次日"; + v.setClockInTimeStr(str + DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF10)); + v.setClockInMethod(DayStatisticsUtils.getClockInMethod(null == v.getClockInMethod() ? + ConstantUtil.DEVICE_UNKNOWN : Integer.parseInt(v.getClockInMethod()))); + }); + + // 将clockInList插入到今日出勤中[插入到两个班次时间之间] + if (finalSorted.isEmpty()) { + finalSorted.addAll(clockInList.stream().sorted(Comparator.comparing(ClockClassRecord::getClockInTime)).collect(Collectors.toList())); + } else { + Map> map = new HashMap<>(); + Date beginTime = null; + AtomicReference ruleId = new AtomicReference<>(null); + for (int i = 0; i < finalSorted.size(); i++) { + ClockClassRecord v1 = finalSorted.get(i); + ruleId.set(v1.getRuleId()); + if (Objects.isNull(v1.getWorkDate())) { + continue; + } + ClockClassRecord v2 = null; + if (i + 1 < finalSorted.size()) { + v2 = finalSorted.get(i + 1); + } + if (i == 0) { + // 判断小于v1的clockInList + List list = clockInList.stream() + .filter(clockIn -> clockIn.getClockInTime().before(v1.getWorkDate())) + .peek(v -> { + if (StringUtils.isEmpty(v.getRuleId())) { + v.setRuleId(ruleId.get()); + } + }) + .collect(Collectors.toList()); + map.put(null, list); + } + if (null != v2 && v2.getWorkDate() != null) { + Date afterTime; + if (null != beginTime) { + afterTime = beginTime; + beginTime = null; + ruleId.set(v2.getRuleId()); + } else { + afterTime = v1.getWorkDate(); + } + Date beforeTime; + if (null != v2.getClockInStatus() && v2.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) { + beforeTime = v1.getLackTime(); + beginTime = v1.getLackTime(); + } else { + beforeTime = v2.getWorkDate(); + } + // 如果v2不为空, 判断哪些clockInList在v1, v2班次之间 + List list = clockInList.stream() + .filter(clockIn -> clockIn.getClockInTime().after(afterTime) && clockIn.getClockInTime().before(beforeTime)) + .peek(v -> { + if (StringUtils.isEmpty(v.getRuleId())) { + v.setRuleId(ruleId.get()); + } + }) + .collect(Collectors.toList()); + map.putIfAbsent(v1, list); + } else { + // 如果v2为空, 判断哪些clockInList大于v1班次 + List list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime() + .after(v1.getWorkDate())).collect(Collectors.toList()); + map.putIfAbsent(v1, list); + } + } + map.forEach((key, valueList) -> { + List list = valueList.stream().sorted(Comparator.comparing(ClockClassRecord::getClockInTime)) + .collect(Collectors.toList()); + if (key == null) { + finalSorted.addAll(0, list); + } else { + int index = finalSorted.indexOf(key); + if (index + 1 > finalSorted.size() - 1) { + finalSorted.addAll(list); + } else { + finalSorted.addAll(index + 1, list); + } + } + }); + clockIns.clear(); + clockIns.addAll(finalSorted); + } + } + } + + private List getClockInListSort(List clockIns) { + + if (clockIns.isEmpty()) { + return new ArrayList<>(); + } + // 1. 按 ruleId 分组,得到 Map> + Map> grouped = + clockIns.stream() + .collect(Collectors.groupingBy(ClockClassRecord::getRuleId)); + // 2. 对每个分组,按照组内 workDate 升序排序(保证组内顺序:上班(早)在前) + grouped.replaceAll((ruleId, l) -> l.stream() + .filter(clockIn -> clockIn.getWorkDate() != null) + .sorted(Comparator.comparing(ClockClassRecord::getWorkDate)) + .collect(Collectors.toList())); + // 3. 构造一个 List of groups (Entry),并按每组的最小 workDate 排序 + List>> groupsSortedByMinDate = + grouped.entrySet().stream() + .sorted(Comparator.comparing(e -> e.getValue().get(0).getWorkDate())) + .collect(Collectors.toList()); + // 4. 将排序好的组合并回一个最终列表(组内保持升序) + List finalSorted = new ArrayList<>(); + for (Map.Entry> entry : groupsSortedByMinDate) { + finalSorted.addAll(entry.getValue()); + } + return finalSorted; + } + + /** + * 获取打卡记录 + * + * @param clockIn 打卡记录 + * @param rule 出勤规则 + * @return jnpf.model.attendance.model.ClockClassRecord + */ + private ClockClassRecord getDailyClockInVo(ClockInVo clockIn, AttendanceRuleVo rule, Map enableBaseSetting) { + ClockClassRecord dailyClockIn = BeanUtil.toBean(clockIn, ClockClassRecord.class); + dailyClockIn.setOutStepOutType(rule.getOutStepOutType()); + dailyClockIn.setInStepOutType(rule.getInStepOutType()); + dailyClockIn.setDay(rule.getDay()); + dailyClockIn.setIsEffectiveClock(1); + dailyClockIn.setCouldRepair(ConstantUtil.NUM_FALSE); + dailyClockIn.setRemark(clockIn.getRemark()); + dailyClockIn.setAssociationApplyId(clockIn.getAssociationApplyId()); + dailyClockIn.setApprovalStatus(StringUtils.isEmpty(clockIn.getApplyId()) ? 0 : 1); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(rule.getGroupId()); + // 能否补卡判断 + if (Objects.nonNull(attendanceBaseSetting) && !clockIn.getClockInStatus().equals(jnpf.enums.ClockInStatusEnum.WORK_NORMAL.getValue()) && + !clockIn.getClockInStatus().equals(jnpf.enums.ClockInStatusEnum.HOME_NORMAL.getValue())) { + AttendanceClockInResult clockInResult = new AttendanceClockInResult(); + clockInResult.setUserId(clockIn.getUserId()); + clockInResult.setRuleId(clockIn.getRuleId()); + clockInResult.setClockInStatus(clockIn.getClockInStatus()); + clockInResult.setAbsence(clockIn.getAbsence()); + try { + GroupRuleVo bean = BeanUtil.toBean(attendanceBaseSetting, GroupRuleVo.class); + bean.setSelfGroupId(rule.getGroupId()); + MutablePair datePair = clockInService.couldRepairRecord(dailyClockIn.getDay(), + clockInResult, dailyClockIn.getApprovalStatus(), bean); + if (DateDetail.checkTimeBetween(dailyClockIn.getDay(), datePair.getLeft(), datePair.getRight())) { + dailyClockIn.setCouldRepair(ConstantUtil.NUM_TRUE); + } + } catch (Exception e) { + log.error("获取统计默认数据时发生异常", e); + } + } + dailyClockIn.setWorkDate(clockIn.getClockInType() == 1 ? rule.getInPoint() : rule.getOutPoint()); + dailyClockIn.setLackTime(clockIn.getClockInType() == 1 ? rule.getInLackPoint() : rule.getOutLackPoint()); + return dailyClockIn; + } + + private List clockInAllRecord(String userId, Date day, List dailyRuleList) { + if (CollUtil.isEmpty(dailyRuleList)) { + return newArrayList(); + } + List allRuleIdList = dailyRuleList.stream().map(AttendanceRuleVo::getId).collect(Collectors.toList()); + List clockInResultList = clockInMapper.getClockInResultByRuleList(allRuleIdList); + List collect1 = dailyRuleList.stream().map(AttendanceRuleVo::getGroupId).distinct().collect(Collectors.toList()); + Map enableBaseSetting = baseSettingService.getEnableBaseSetting(collect1); + Map collect = dailyRuleList.stream().collect(Collectors.toMap(AttendanceRuleVo::getId, Function.identity())); + // 需要展示打卡记录的出勤规则 + dailyRuleList = dailyRuleList.stream().filter(v -> v.getInPoint() != null && v.getOutPoint() != null).collect(Collectors.toList()); + List ruleIdList = dailyRuleList.stream().map(AttendanceRuleVo::getId).collect(Collectors.toList()); + List clockIns = clockInResultList.stream().map(clockIn -> + getDailyClockInVo(clockIn, collect.getOrDefault(clockIn.getRuleId(), new AttendanceRuleVo()), + enableBaseSetting)).collect(Collectors.toList()); + setOldData(clockIns, allRuleIdList, ruleIdList, userId, day); + List userIds = clockIns.stream().map(ClockClassRecord::getAbsenceLeader).filter(StringUtil::isNotEmpty) + .distinct().collect(Collectors.toList()); + List userList = CollUtil.isNotEmpty(userIds) ? v2UserApi.userNameAndCopy(userIds) : new ArrayList<>(); + Map userMap = CollUtil.isNotEmpty(userList) ? userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) : new HashMap<>(); + clockIns.stream().filter(item -> item.getAbsenceLeader() != null).forEach(clockIn -> + clockIn.setAbsenceLeaderName(StringUtil.isNotEmpty(clockIn.getAbsenceLeader()) ? + userMap.get(clockIn.getAbsenceLeader()).getUserName() : null)); + return clockIns; + } + + @Override + public List getRecordData(AppStatisticsRecordDto req) throws Exception { + List statisticsRecordVoList = new ArrayList<>(); + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(req.getUserId()); + Date day = stringToDates(req.getDate()); + List groupInfoList = clockInService.getClockInMainInfo(stringToDates(req.getDate()), userInfo, ConstantUtil.NUM_TRUE); + if (CollectionUtil.isNotEmpty(groupInfoList)) { + // 排除掉不是今天的出勤规则 + groupInfoList.forEach(v -> { + v.getAttendanceRuleList().removeIf(v1 -> v1.getDay().compareTo(day) != 0 || v1.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())); + }); + groupInfoList.removeIf(v -> CollUtil.isEmpty(v.getAttendanceRuleList())); + if (CollectionUtil.isEmpty(groupInfoList)) { + return statisticsRecordVoList; + } + List attendanceRuleVos = groupInfoList.stream().map(GroupInfoVo::getAttendanceRuleList) + .flatMap(Collection::stream).collect(Collectors.toList()); + //查询打卡记录 + List dailyClockInVos = clockInAllRecord(req.getUserId(), day, attendanceRuleVos); + //查询打卡记录的拍照图片集合 + Map> clockInPicMap = CollUtil.isNotEmpty(dailyClockInVos) ? + clockInPicService.getClockInPicByIds(dailyClockInVos.stream().map(ClockClassRecord::getClockInId). + filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()), dailyClockInVos.stream() + .map(ClockClassRecord::getApplyId).filter(StringUtil::isNotEmpty).distinct() + .collect(Collectors.toList())) : new HashMap<>(); + AtomicReference ruleId = new AtomicReference<>(dailyClockInVos.stream().map(ClockClassRecord::getRuleId) + .filter(StringUtil::isNotEmpty).findFirst().orElse(null)); + // 查询用户是否封账 + Map sealMap = dayStatisticsService.selectUserIsSeal(List.of(req.getUserId()), req.getDate().substring(0, 7)); + Map> clockClassRecordList = Maps.newHashMap(); + dailyClockInVos.forEach(vo -> { + vo.setSeal(sealMap.getOrDefault(vo.getUserId(), false) ? 1 : 0); + if (StringUtil.isNotEmpty(vo.getRuleId())) { + ruleId.set(vo.getRuleId()); + } + if (!clockClassRecordList.containsKey(ruleId.get())) { + clockClassRecordList.put(ruleId.get(), newArrayList()); + } + clockClassRecordList.get(ruleId.get()).add(DayStatisticsUtils.clock2ClockClassRecord(vo, clockInPicMap)); + }); + for (GroupInfoVo groupInfoVo : groupInfoList) { + AppStatisticsRecordVo statisticsRecordVo = new AppStatisticsRecordVo(); + BeanUtils.copyProperties(groupInfoVo, statisticsRecordVo); + statisticsRecordVo.setDay(day); + List attendanceRuleList = new ArrayList<>(); + //排除出勤类型为-1和0的排班记录 + List filterAttendanceRuleList = groupInfoVo.getAttendanceRuleList().stream().filter(m -> + !Arrays.asList(AttendanceTypeEnum.CLEAR.getCode(), + AttendanceTypeEnum.DEFAULT.getCode()).contains(m.getAttendanceType())).collect(Collectors.toList()); + //获取更新打卡的历史数据 + filterAttendanceRuleList.forEach(rule -> { + ClockGroupRecord bean = BeanUtil.toBean(rule, ClockGroupRecord.class); + bean.setClockList(clockClassRecordList.getOrDefault(rule.getId(), newArrayList())); + attendanceRuleList.add(bean); + }); + statisticsRecordVo.setAttendanceRuleList(attendanceRuleList); + statisticsRecordVoList.add(statisticsRecordVo); + } + return statisticsRecordVoList; + } + Date start = DateDetail.getStr2Date10(req.getDate()); + Date end = cn.hutool.core.date.DateUtil.endOfDay(start); + List groupUserList = userService.getAttendanceGroupUsersOfSecondment(start, end, + newArrayList(req.getUserId()), null); + if (CollUtil.isEmpty(groupUserList)) { + throw new QueryException("暂无数据"); + } + String groupId = groupUserList.stream().filter(m -> m.getType().equals(ConstantUtil.NUM_TRUE)) + .collect(Collectors.toList()).get(0).getGroupId(); + AttendanceGroup group = groupService.getById(groupId); + if (Objects.isNull(group)) { + throw new QueryException("暂无数据"); + } + AppStatisticsRecordVo statisticsRecordVo = new AppStatisticsRecordVo(); + statisticsRecordVo.setDay(stringToDates(req.getDate())); + statisticsRecordVo.setGroupId(group.getId()); + statisticsRecordVo.setGroupName(group.getGroupName()); + statisticsRecordVo.setSelfGroup(1); + statisticsRecordVo.setUserId(req.getUserId()); + statisticsRecordVoList.add(statisticsRecordVo); + return statisticsRecordVoList; + } + + @Override + public AppStatisticsMoreVo getMoreDefaultData(AppStatisticsMoreDto req) throws Exception { + DateRangeDto selectDateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + MonthStatisticsDataQueryDto dataQueryDto = MonthStatisticsDataQueryDto.builder() + .startDate(selectDateRangeDto.getStartDate()) + .endDate(selectDateRangeDto.getEndDate()) + .userIds(Collections.singletonList(req.getUserId())) + .queryType(1) + .build(); + List monthStatisticsQueryVoList = dayStatisticsMapper.getMonthPageList(dataQueryDto); + if (CollUtil.isEmpty(monthStatisticsQueryVoList)) { + throw new QueryException("暂无数据"); + } + AppStatisticsMoreVo statisticsMore = new AppStatisticsMoreVo(); + BigDecimal actualAttendDays = BigDecimal.ZERO; + BigDecimal leaveDays = BigDecimal.ZERO; + BigDecimal leaveHours = BigDecimal.ZERO; + int lateTimes = 0; + BigDecimal lateAccumulatedMinutes = BigDecimal.ZERO; + int earlyLeaveTimes = 0; + BigDecimal earlyLeaveAccumulatedMinutes = BigDecimal.ZERO; + int absenceCardTimes = 0; + int absenceTimes = 0; + int makeUpCardTimes = 0; + int outworkClockTimes = 0; + int overtimeTimes = 0; + int busTimesTotal = 0; + for (MonthStatisticsQueryVo item : monthStatisticsQueryVoList) { + actualAttendDays = DayStatisticsUtils.safeAdd(actualAttendDays, item.getEffectiveAttendDays()); + leaveDays = DayStatisticsUtils.safeAdd(leaveDays, item.getLeaveDays()); + leaveHours = DayStatisticsUtils.safeAdd(leaveHours, item.getLeaveHours()); + lateTimes += DayStatisticsUtils.defaultIfNull(item.getLateTimes(), 0); + lateAccumulatedMinutes = DayStatisticsUtils.safeAdd(lateAccumulatedMinutes, item.getLateMinutes()); + earlyLeaveTimes += DayStatisticsUtils.defaultIfNull(item.getEarlyLeaveTimes(), 0); + earlyLeaveAccumulatedMinutes = DayStatisticsUtils.safeAdd(earlyLeaveAccumulatedMinutes, item.getEarlyLeaveMinutes()); + absenceCardTimes += DayStatisticsUtils.defaultIfNull(item.getAbsenceCardTimes(), 0); + absenceTimes += DayStatisticsUtils.defaultIfNull(item.getAbsenceTimes(), 0); + makeUpCardTimes += DayStatisticsUtils.defaultIfNull(item.getMakeUpCardTimes(), 0); + outworkClockTimes += DayStatisticsUtils.defaultIfNull(item.getOutworkTimes(), 0); + overtimeTimes += DayStatisticsUtils.defaultIfNull(item.getOvertimeTimes(), 0); + busTimesTotal += DayStatisticsUtils.countSplitElements(item.getBusTimes()); + } + List outTimesList = monthStatisticsQueryVoList.stream() + .map(item -> MonthStatisticsQueryVo.getOutTimesList(item.getOutTimes())) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + statisticsMore.setActualAttendDays(actualAttendDays); + statisticsMore.setLeaveDays(leaveDays); + statisticsMore.setLeaveHours(leaveHours); + statisticsMore.setLateTimes(lateTimes); + statisticsMore.setLateAccumulatedMinutes(lateAccumulatedMinutes); + statisticsMore.setEarlyLeaveTimes(earlyLeaveTimes); + statisticsMore.setEarlyLeaveAccumulatedMinutes(earlyLeaveAccumulatedMinutes); + statisticsMore.setAbsenceCardTimes(absenceCardTimes); + statisticsMore.setAbsenceTimes(absenceTimes); + statisticsMore.setMakeUpCardTimes(makeUpCardTimes); + statisticsMore.setOutworkClockTimes(outworkClockTimes); + statisticsMore.setOvertimeTimes(overtimeTimes); + statisticsMore.setOutTimes(Math.toIntExact(outTimesList.stream().map(BatchNumberResult::getBatchNumberId).distinct().count())); + statisticsMore.setBusTimes(busTimesTotal); + // 获取借调次数 + List userSecondRecordList = statisticsMapper.getUserSecondRecordList(List.of(req.getUserId()), null); + if (CollUtil.isNotEmpty(userSecondRecordList)) { + statisticsMore.setSecondTimes(DayStatisticsUtils.handleDateJsonObject(selectDateRangeDto, userSecondRecordList)); + } + return statisticsMore; + } + + @Override + public AppStatisticsMoreInfoVo getMoreExpandData(AppStatisticsMoreInfoDto req) throws Exception { + AppStatisticsMoreInfoVo moreInfoVo = new AppStatisticsMoreInfoVo(); + //月份格式化返回月份开始结束日期 + DateRangeDto rangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + StatisticsEnumUtil.DataTypeEnum dataTypeEnum = StatisticsEnumUtil.DataTypeEnum.getDataTypeEnum(req.getType()); + if (Objects.isNull(dataTypeEnum)) { + throw new QueryException("记录类型不正确"); + } + //获取用户信息 + List userList = v2UserApi.userNameAndCopy(List.of(req.getUserId())); + Map userMap = CollUtil.isNotEmpty(userList) ? userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) : new HashMap<>(); + //获取用户月度考勤统计数据 + MonthStatisticsDataQueryDto monthStatisticsQueryDto = MonthStatisticsDataQueryDto.builder() + .startDate(rangeDto.getStartDate()) + .endDate(rangeDto.getEndDate()) + .userIds(Collections.singletonList(req.getUserId())) + .queryType(1) + .build(); + List monthStatisticsQueryVoList = dayStatisticsMapper.getMonthPageList(monthStatisticsQueryDto); + if (CollUtil.isEmpty(monthStatisticsQueryVoList)) { + throw new QueryException("暂无数据"); + } + Integer enumTimes = dataTypeEnum.getTimes(monthStatisticsQueryVoList); + // 获取每个考勤组的出勤换算比 + Map ratioMap = monthStatisticsQueryVoList.stream().collect( + Collectors.toMap(MonthStatisticsQueryVo::getGroupId, MonthStatisticsQueryVo::getAttendanceRatio)); + // 查询用户是否封账 + Map sealMap = dayStatisticsService.selectUserIsSeal(List.of(req.getUserId()), req.getMonth()); + if (enumTimes > 0) { + switch (dataTypeEnum) { + case QJ: + Map> userLeaveMap = utilService.getLeaveMap(userMap, ratioMap, List.of(req.getUserId()), rangeDto); + moreInfoVo.setLeaveRecordList(userLeaveMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case CD: + Map> userLateMap = utilService.getLateMap(userMap, sealMap, null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setLateRecordList(userLateMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case ZT: + Map> earlyLeaveMap = utilService.getEarlyMap(userMap, sealMap, null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setEarlyLeaveRecordList(earlyLeaveMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case QK: + Map> absenceCardMap = utilService.getAbsenceCardMap(userMap, sealMap, null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setAbsenceCardRecordList(absenceCardMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case QQ: + Map> absenceMap = utilService.getAbsenceMap(null, sealMap, List.of(req.getUserId()), rangeDto); + moreInfoVo.setAbsenceRecordList(absenceMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case BK: + Map> makeUpCardMap = utilService.getMakeUpCardMap(null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setMakeUpCardRecordList(makeUpCardMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case WQ: + Map> outworkMap = utilService.getOutworkRecordList(null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setOutworkRecordList(outworkMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case JB: + Map> overtimeMap = utilService.getOvertimeRecordList(null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setOvertimeRecordList(overtimeMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case JD: + Map> secondMap = utilService.getSecondRecordList(null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setSecondRecordList(secondMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case CC: + Map> busMap = utilService.getBusRecordList(null, List.of(req.getUserId()), rangeDto); + moreInfoVo.setBusRecordList(busMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + case WC: + Map> outMap = utilService.getOutRecordList(ratioMap, List.of(req.getUserId()), rangeDto); + moreInfoVo.setOutRecordList(outMap.values().stream().flatMap(List::stream).collect(Collectors.toList())); + break; + } + } + return moreInfoVo; + } + + @Override + public AppStatisticsTeamListVo getTeamHomeData(AppStatisticsTeamListDto req) { + AppStatisticsTeamListVo teamListVo = new AppStatisticsTeamListVo(); + //月份格式化返回月份开始结束日期 + DateRangeDto selectDateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + //获取用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), selectDateRangeDto.getStartDate(), selectDateRangeDto.getEndDate(), null, String.valueOf(0)); + if (CollUtil.isEmpty(userBoundVoList)) { + return teamListVo; + } + List groupIds = req.getFilterList().stream() + .map(GroupFilterDto::getGroupId) + .distinct().collect(Collectors.toList()); + StatisticsDataQueryDto monthStatisticsQueryDto = StatisticsDataQueryDto.builder() + .groupIds(CollUtil.isNotEmpty(groupIds) ? groupIds : null) + .startDate(selectDateRangeDto.getStartDate()) + .endDate(selectDateRangeDto.getEndDate()) + .userIds(userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList())) + .build(); + StatisticsDataQueryVo statisticsDataQueryVo = dayStatisticsMapper.getMonthStatisticsDataQuery(monthStatisticsQueryDto, Boolean.FALSE); + if (Objects.isNull(statisticsDataQueryVo)) { + return teamListVo; + } + teamListVo.setLeaveNumber(statisticsDataQueryVo.getLeaveCount()); + teamListVo.setLateNumber(statisticsDataQueryVo.getLateCount()); + teamListVo.setEarlyLeaveNumber(statisticsDataQueryVo.getEarlyLeaveCount()); + teamListVo.setAbsenceCardNumber(statisticsDataQueryVo.getAbsenceCardCount()); + teamListVo.setAbsenceNumber(statisticsDataQueryVo.getAbsenceCount()); + return teamListVo; + } + + /** + * 获取有权限的用户列表 + * + * @param filterList 筛选条件 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param workStatus 用户状态 + * @return 用户列表 + */ + public List getUserIdArr(List filterList, String startDate, String endDate, List userIdsFilter, String workStatus) { + List groupIds = filterList.stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + List teamIds = filterList.stream().filter(item -> item.getTeam() == 1).map(GroupFilterDto::getTeamId).distinct().collect(Collectors.toList()); + Map> groupTeamMap = Map.of(); + Map> groupTeamStaMap = Map.of(); + if (CollUtil.isNotEmpty(teamIds)) { + // 按照考勤组分组,获取不同的班次id集合 + groupTeamMap = filterList.stream().filter(item -> item.getTeam() == 1 && jnpf.util.StringUtil.isNotEmpty(item.getTeamId())).collect(Collectors.groupingBy(GroupFilterDto::getGroupId, Collectors.mapping(GroupFilterDto::getTeamId, Collectors.toList()))); + groupTeamStaMap = filterList.stream().collect(Collectors.groupingBy(GroupFilterDto::getGroupId, Collectors.mapping(GroupFilterDto::getTeam, Collectors.toList()))); + } + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(workStatus); + Assert.notNull(workStatusEnum, "工作状态不正确"); + List workStatusEnums = DayStatisticsUtils.getWorkStatusEnumList(Objects.requireNonNull(workStatusEnum)); + List userBoundVoList = userService.batchGetUserBoundVO(groupIds, workStatusEnums); + if (CollUtil.isEmpty(userBoundVoList)) { + userBoundVoList = new ArrayList<>(); + } + // 判断是否有人员过滤,有的话需要取交集 + List userIds = CollUtil.isNotEmpty(userBoundVoList) ? userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()) : List.of(); + // 查询用户所属权限 + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + // 是否配置仅下属 + List underlingUserId = null; + if (Objects.nonNull(targetAuthIdsVO) && Objects.equals(targetAuthIdsVO.getTargetAuthEnums(), TargetAuthEnums.USER)) { + if (CollUtil.isEmpty(userBoundVoList)) { + return CollUtil.newArrayList(); + } + underlingUserId = userIds; + } + // 查询用户考勤组用户 + DateRangeDto dateRangeDto = new DateRangeDto(startDate, endDate); + Date start = jnpf.util.DateUtil.stringToDate(dateRangeDto.getStartDate()); + Date end = jnpf.util.DateUtil.stringToDate(dateRangeDto.getEndDate()); + List groupUsers = userService.queryByUsersAndGroup(start, end, underlingUserId, groupIds); + if (CollUtil.isEmpty(groupUsers)) { + return CollUtil.newArrayList(); + } + List queryUserIds = new ArrayList<>(); + Map> groupUserMap = CollUtil.isNotEmpty(groupUsers) ? groupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)) : new HashMap<>(); + for (Map.Entry> listEntry : groupUserMap.entrySet()) { + if (dailyRuleService.findUserIsExistsByDay(listEntry.getValue(), start, end) != 0) { + queryUserIds.add(listEntry.getKey()); + } + } + if (CollUtil.isEmpty(queryUserIds)) { + return CollUtil.newArrayList(); + } + // 获取未在权限用户里面的用户集合 + List noPermissionUserIds = new ArrayList<>(CollUtil.subtract(queryUserIds, userIds)); + if (CollUtil.isNotEmpty(noPermissionUserIds)) { + List userBoundVos = userApi.getStaffRosterListInfoByIds(noPermissionUserIds); + Map userMap = userBoundVos.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity(), (r1, r2) -> r1)); + if (CollUtil.isNotEmpty(userMap)) { + List finalUserBoundVoList = userBoundVoList; + userBoundVos.forEach(item -> { + PartUserInfoVo partUserInfoVo = userMap.get(item.getUserId()); + if (partUserInfoVo != null && finalUserBoundVoList.stream().noneMatch(userBoundVO -> userBoundVO.getId().equals(partUserInfoVo.getUserId()))) { + UserBoundVO userBoundVO = new UserBoundVO(); + BeanUtils.copyProperties(partUserInfoVo, userBoundVO); + userBoundVO.setId(partUserInfoVo.getUserId()); + userBoundVO.setUserName(partUserInfoVo.getRealName()); + userBoundVO.setWorkStatusEnums(UserWorkStatusEnums.getUserWorkStatusEnumsByValue(item.getWorkerStatus())); + finalUserBoundVoList.add(userBoundVO); + } + }); + } + } + if (CollUtil.isEmpty(userBoundVoList)) { + return CollUtil.newArrayList(); + } + Map userIdToUserBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, item -> item)); + List result = new ArrayList<>(); + if (CollUtil.isNotEmpty(teamIds)) { + Map> groupUserListMap = groupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)).entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream().map(AttendanceGroupUser::getUserId).map(userIdToUserBoundVoMap::get).filter(Objects::nonNull).collect(Collectors.toList()))); + Map> finalGroupTeamMap = groupTeamMap; + Map> finalGroupTeamStaMap = groupTeamStaMap; + groupUserListMap.forEach((groupId, groupUserList) -> { + boolean flag = finalGroupTeamMap.containsKey(groupId) && (finalGroupTeamStaMap.containsKey(groupId) && !finalGroupTeamStaMap.get(groupId).contains(0)); + result.addAll(flag ? groupUserList.stream().filter(item -> teamIds.contains(item.getStoreTeamId())).collect(Collectors.toList()) : groupUserList); + }); + userBoundVoList = result.stream().distinct().collect(Collectors.toList()); + } + if (!workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.QB)) { + userBoundVoList = userBoundVoList.stream().filter(item -> item.getWorkStatusEnums().getCode().equals(workStatusEnum.getCode())).collect(Collectors.toList()); + } + if (CollUtil.isNotEmpty(userIdsFilter)) { + userBoundVoList = userBoundVoList.stream().filter(item -> userIdsFilter.contains(item.getId())).collect(Collectors.toList()); + } + return userBoundVoList; + } + + @Override + public List getTabListData(AppTeamStatisticsTabDto req) { + DateRangeDto dateRangeDto = new DateRangeDto(req.getDate(), req.getDate()); + //根据枚举生成出勤类型的TabList + List teamTabVoList = DayStatisticsUtils.createTabDataDirectory().stream().sorted(Comparator.comparing(AppStatisticsTeamTabVo::getDataType)).collect(Collectors.toList()); + //获取用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), req.getDate(), req.getDate(), null, String.valueOf(0)); + if (CollUtil.isEmpty(userBoundVoList)) { + return teamTabVoList; + } + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + Map userBoundMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + //获取考勤组当天打卡数据 + AppStatisticsTeamTabListVo teamTabListVo = statisticsTeamTabListVo(userBoundMap, groupIds, dateRangeDto); + // 构建枚举到数据列表的映射关系 + Map> dataMap = new EnumMap<>(StatisticsEnumUtil.TabDataTypeEnum.class); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.QB, teamTabListVo.getAllDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.XX, teamTabListVo.getRestDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.CD, teamTabListVo.getLateDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.ZT, teamTabListVo.getEarlyLeaveDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.QJ, teamTabListVo.getLeaveDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.QK, teamTabListVo.getAbsenceCardDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.QQ, teamTabListVo.getAbsenceDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.JD, teamTabListVo.getSecondDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.BK, teamTabListVo.getMakeUpCardDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.WQ, teamTabListVo.getOutworkDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.JB, teamTabListVo.getOvertimeDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.CC, teamTabListVo.getBusDataList()); + dataMap.put(StatisticsEnumUtil.TabDataTypeEnum.WC, teamTabListVo.getGoOutDataList()); + for (AppStatisticsTeamTabVo teamTabVo : teamTabVoList) { + StatisticsEnumUtil.TabDataTypeEnum dataTypeEnum = getTabDataTypeEnum(teamTabVo.getDataType()); + if (Objects.isNull(dataTypeEnum)) { + continue; + } + teamTabVo.setDataList(dataMap.get(dataTypeEnum)); + } + return teamTabVoList; + } + + @Override + public AppTeamStatisticsVo getStatisticsData(AppTeamStatisticsDto req) { + AppTeamStatisticsVo statisticsVo = new AppTeamStatisticsVo(); + DateRangeDto selectDateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + //获取用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), selectDateRangeDto.getStartDate(), selectDateRangeDto.getEndDate(), null, String.valueOf(0)); + if (CollUtil.isEmpty(userBoundVoList)) { + return statisticsVo; + } + List groupIds = req.getFilterList().stream() + .map(GroupFilterDto::getGroupId) + .distinct().collect(Collectors.toList()); + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + MonthStatisticsDataQueryDto monthStatisticsQueryDto = MonthStatisticsDataQueryDto.builder() + .groupIds(CollUtil.isNotEmpty(groupIds) ? groupIds : null) + .startDate(selectDateRangeDto.getStartDate()) + .endDate(selectDateRangeDto.getEndDate()) + .userIds(userIds) + .queryType(1) + .build(); + List monthStatisticsQueryVoList = dayStatisticsMapper.getMonthPageList(monthStatisticsQueryDto); + if (CollUtil.isNotEmpty(monthStatisticsQueryVoList)) { + statisticsVo.setRestNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getRestTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setLeaveNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> StringUtil.isNotEmpty(item.getLeaveTimes())).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setLateNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getLateTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setEarlyLeaveNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getEarlyLeaveTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setAbsenceCardNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getAbsenceCardTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setAbsenceNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getAbsenceTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setMakeUpCardNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getMakeUpCardTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setOutworkNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getOutworkTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setOvertimeNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> item.getOvertimeTimes() > 0).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setOutNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> CollUtil.isNotEmpty(MonthStatisticsQueryVo.getOutTimesList(item.getOutTimes()))).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + statisticsVo.setBusNumber(Math.toIntExact(monthStatisticsQueryVoList.stream().filter(item -> StringUtil.isNotEmpty(item.getBusTimes())).map(MonthStatisticsQueryVo::getUserId).distinct().count())); + } + // 获取借调人数 + Map> secondMap = utilService.getSecondRecordList(groupIds, userIds, new DateRangeDto(selectDateRangeDto.getStartDate(), selectDateRangeDto.getEndDate())); + Integer uniqueUserCount = Math.toIntExact(secondMap.keySet().stream().map(key -> key.split("&")[0]).distinct().count()); + statisticsVo.setSecondNumber(uniqueUserCount); + return statisticsVo; + } + + @Override + public List getStatisticsListData(AppTeamStatisticsListDto req) throws QueryException { + StatisticsEnumUtil.TabDataTypeEnum dataTypeEnum = getTabDataTypeEnum(req.getDataType()); + if (Objects.isNull(dataTypeEnum)) { + throw new QueryException("查询记录类型不正确"); + } + DateRangeDto selectDateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + List statisticsListVoList = new ArrayList<>(); + //获取用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), selectDateRangeDto.getStartDate(), selectDateRangeDto.getEndDate(), null, String.valueOf(0)); + if (CollUtil.isEmpty(userBoundVoList)) { + return statisticsListVoList; + } + Map userBoundMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + if (dataTypeEnum.equals(JD)) { + //借调查询 + secondedMethod(selectDateRangeDto, groupIds, userBoundMap, statisticsListVoList); + return statisticsListVoList; + } + //查询月统计数据 + MonthStatisticsDataQueryDto monthStatisticsQueryDto = MonthStatisticsDataQueryDto.builder() + .groupIds(CollUtil.isNotEmpty(groupIds) ? groupIds : null) + .startDate(selectDateRangeDto.getStartDate()) + .endDate(selectDateRangeDto.getEndDate()) + .userIds(userIds) + .queryType(1) + .build(); + List monthPageList = dayStatisticsMapper.getMonthPageList(monthStatisticsQueryDto); + if (CollUtil.isEmpty(monthPageList)) { + return statisticsListVoList; + } + List filterList = dataTypeEnum.dataFilter(monthPageList); + if (CollUtil.isEmpty(filterList)) { + return statisticsListVoList; + } + Map> dataDetailsMap = new HashMap<>(); + Map ratioMap = monthPageList.stream().collect(Collectors.toMap(MonthStatisticsQueryVo::getGroupId, + MonthStatisticsQueryVo::getAttendanceRatio, (existing, replacement) -> replacement)); + // 查询用户是否封账 + Map sealMap = dayStatisticsService.selectUserIsSeal(userIds, req.getMonth()); + switch (dataTypeEnum) { + case QJ: + Map> userLeaveMap = utilService.getLeaveMap(userBoundMap, ratioMap, userIds, selectDateRangeDto); + dataDetailsMap = userLeaveMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case CD: + Map> userLateMap = utilService.getLateMap(userBoundMap, sealMap, groupIds, userIds, selectDateRangeDto); + dataDetailsMap = userLateMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case ZT: + Map> earlyLeaveMap = utilService.getEarlyMap(userBoundMap, sealMap, groupIds, userIds, selectDateRangeDto); + dataDetailsMap = earlyLeaveMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case QK: + Map> absenceCardMap = utilService.getAbsenceCardMap(userBoundMap, sealMap, groupIds, userIds, selectDateRangeDto); + dataDetailsMap = absenceCardMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case QQ: + Map> absenceMap = utilService.getAbsenceMap(groupIds, sealMap, userIds, selectDateRangeDto); + dataDetailsMap = absenceMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case BK: + Map> makeUpCardMap = utilService.getMakeUpCardMap(groupIds, userIds, selectDateRangeDto); + dataDetailsMap = makeUpCardMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case WQ: + Map> outworkMap = utilService.getOutworkRecordList(groupIds, userIds, selectDateRangeDto); + dataDetailsMap = outworkMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case JB: + Map> overtimeMap = utilService.getOvertimeRecordList(groupIds, userIds, selectDateRangeDto); + dataDetailsMap = overtimeMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case CC: + Map> busMap = utilService.getBusRecordList(groupIds, userIds, selectDateRangeDto); + dataDetailsMap = busMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + case WC: + Map> outMap = utilService.getOutRecordList(ratioMap, userIds, selectDateRangeDto); + dataDetailsMap = outMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + break; + } + statisticsListVoList = DayStatisticsUtils.dateEncapsulationV2(dataTypeEnum, filterList, dataDetailsMap, userBoundMap); + return statisticsListVoList; + } + + /** + * 借调统计 + * @param rangeDto 查询参数 + * @param groupIds 考勤组id + * @param userBoundMap 用户信息 + * @param statisticsListVoList 统计数据 + */ + private void secondedMethod(DateRangeDto rangeDto, List groupIds, Map userBoundMap, List statisticsListVoList) { + List userIds = newArrayList(userBoundMap.keySet()); + Map> secondMap = utilService.getSecondRecordList(groupIds, userIds, new DateRangeDto(rangeDto.getStartDate(), rangeDto.getEndDate())); + Map> dataDetailsMap = secondMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue()))); + if (CollUtil.isEmpty(secondMap)) { + return; + } + dataDetailsMap.forEach((userAndGroupKey, secondList) -> { + String userId = userAndGroupKey.split("&")[0]; + AppTeamStatisticsListVo statisticsListVo = new AppTeamStatisticsListVo(); + statisticsListVo.setUserId(userId); + UserBoundVO userInfoVo = userBoundMap.get(userId); + statisticsListVo.setUserHeadSculpture(StringUtil.isNotEmpty(userInfoVo.getHeadIcon()) ? + UploaderUtil.uploaderImg(userInfoVo.getHeadIcon()) : null); + statisticsListVo.setUserName(StringUtil.isNotEmpty(userInfoVo.getUserName()) ? userInfoVo.getUserName() : + StringUtil.isNotEmpty(userInfoVo.getNickname()) ? userInfoVo.getNickname() : null); + statisticsListVo.setIsSecond(0); + statisticsListVo.setNumber(secondList.size()); + statisticsListVo.setDetalList(secondList); + statisticsListVoList.add(statisticsListVo); + }); + } + + public void setClockNew(List usersByGroupVos, DateRangeDto dateRangeDto, Map keyMap) { + //所有的需要的出勤规则 通过日期,考勤组,时间 过滤的数据 + List allUserRule = clockInService.getCurrentDailyRuleListOfDay(dateRangeDto, usersByGroupVos); + List allRule = JsonUtil.getJsonToList(allUserRule, AttendanceRuleVo.class); + if (null == allRule || allRule.isEmpty()) { + // 没排班 + return; + } + allRule.forEach(v -> v.setDayStr(DateDetail.getDate2Str(v.getDay(), DateDetail.DF))); + Map ruleIdMap = allRule.stream().collect(Collectors.toMap(AttendanceRuleVo::getId, Function.identity())); + // 通过 用户-日期-考勤组 分组 + Map> userIdByRuleMap = allRule.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + "-" + rule.getDayStr() + "-" + rule.getGroupId())); + // 所有的用户 + List allUserIds = usersByGroupVos.stream().map(ClockInExportVo::getUserId).distinct().collect(Collectors.toList()); + // 批量查询所有打卡记录 + List clockInList = clockInMapper.getClockInList(allUserIds, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + clockInList.forEach(v -> v.setDayStr(DateDetail.getDate2Str(v.getDay(), DateDetail.DF))); + // 通过 用户-日期 分组 + Map> userIdByClickMap = clockInList.stream().collect(Collectors.groupingBy(clockClassRecord -> clockClassRecord.getUserId() + "-" + clockClassRecord.getDayStr())); + SimpleDateFormat hms = new SimpleDateFormat("HH:mm"); + SimpleDateFormat ymd = new SimpleDateFormat("yyyy-MM-dd"); + // 以打卡结果表为主 查出所有的打卡结果记录 + List clockResultResultList = clockInMapper.getClockInResultByRuleList(allRule.stream().map(AttendanceRuleVo::getId).distinct().collect(Collectors.toList())); + List records = newArrayList(); + clockResultResultList.forEach(v -> { + ClockClassRecord dailyClockIn = BeanUtil.toBean(v, ClockClassRecord.class); + dailyClockIn.setIsEffectiveClock(1); + dailyClockIn.setCouldRepair(ConstantUtil.NUM_FALSE); + AttendanceRuleVo attendanceRuleVo = ruleIdMap.get(v.getRuleId()); + if (null != attendanceRuleVo) { + dailyClockIn.setWorkDate(v.getClockInType() == 1 ? attendanceRuleVo.getInPoint() : attendanceRuleVo.getOutPoint()); + } + records.add(dailyClockIn); + }); + Map> ruleIdByMap = records.stream().collect(Collectors.groupingBy(ClockClassRecord::getRuleId)); + usersByGroupVos.forEach(v -> { + // 以下代码目的是为了找到当日的所有打卡记录 + // 获取所有和该用户当天且与本考勤组相关的出勤规则 + List ftbAttendanceDailyRules = userIdByRuleMap.get(v.getKeyString()); + if (null != ftbAttendanceDailyRules) { + // 当天的出勤规则集合 + List recordList = newArrayList(); + List todayRuleIds = ftbAttendanceDailyRules.stream().map(AttendanceRuleVo::getId).distinct().collect(Collectors.toList()); + todayRuleIds.forEach(value -> { + List clockClassRecords = ruleIdByMap.get(value); + if (CollUtil.isNotEmpty(clockClassRecords)) { + recordList.addAll(clockClassRecords); + } + }); + // 获取所有和该用户本日期相关的打卡记录 + List clockClassRecords = userIdByClickMap.get(v.getUserId() + "-" + v.getDate()); + if (null != recordList || null != clockClassRecords) { + List dayClock = DayStatisticsUtils.setOldDataNew(ftbAttendanceDailyRules.stream().map(AttendanceRuleVo::getId).collect(Collectors.toList()), stringToDates(v.getDate()), clockClassRecords, recordList); + // 处理打卡信息 + DayStatisticsUtils.setClockTime(v.getGroupId(), keyMap, v.getUserId(), v.getDate(), dayClock, ymd, hms); + } + } + }); + + } + + /** + * 考勤组指定日期考勤打卡及时段 + * + * @param usersByGroupVos 用户集合 + * @param tenantId 租户id + * @param dateRangeDto 日期范围 + */ + @Override + public List getDayClockInPageListExport(List usersByGroupVos, DateRangeDto + dateRangeDto, String tenantId) { + // 将传入的用户信息clockInExportVoList根据 userId分组 + List groupIds = usersByGroupVos.stream().map(ClockInExportVo::getGroupId).distinct().collect(Collectors.toList()); + Map> groupClockMap = usersByGroupVos.stream().collect(Collectors.groupingBy(ClockInExportVo::getGroupId)); + Map keyMap = usersByGroupVos.stream().collect(Collectors.toMap(ClockInExportVo::getKeyString, Function.identity())); + // 获取明天的日期 + for (String groupId : groupIds) { + List groupClockList = groupClockMap.get(groupId); + if (CollUtil.isEmpty(groupClockList)) { + continue; + } + List userIds = groupClockList.stream().map(ClockInExportVo::getUserId).distinct().collect(Collectors.toList()); + // 查询打卡规则 + List userRuleList = statisticsMapper.getUserRuleList(userIds, dateRangeDto, groupId, List.of( + AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.LEAVE.getCode(), + AttendanceTypeEnum.WORKOVERTIME.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), + AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), AttendanceTypeEnum.SECONDMENT.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), + AttendanceTypeEnum.STEP_OUT.getCode())); + // 根据人分组 + Map> userRuleMap = userRuleList.stream().collect(Collectors.groupingBy(UserRuleListVo::getUserId)); + for (Map.Entry> entry : userRuleMap.entrySet()) { + if (CollUtil.isEmpty(entry.getValue())) { + continue; + } + // 根据日期分组 + Map> dayRuleMap = entry.getValue().stream().collect(Collectors.groupingBy(UserRuleListVo::getDay)); + for (Map.Entry> e : dayRuleMap.entrySet()) { + if (CollUtil.isEmpty(e.getValue()) || !keyMap.containsKey(entry.getKey() + "-" + e.getKey() + "-" + groupId)) { + continue; + } + // 组装班次信息 + RemarkVo remarkVo = getShiftPeriod(e.getValue()); + ClockInExportVo clockInExportVo = keyMap.get(entry.getKey() + "-" + e.getKey() + "-" + groupId); + // 处理班次时间 + clockInExportVo.setShiftTimeStr(remarkVo.getShiftPeriod().toString()); + // 是全天假,外出,出差时 remarkVo.getClockTime() 有值 + clockInExportVo.setClockTime(0 != remarkVo.getClockTime().length() ? remarkVo.getClockTime().toString() : ""); + clockInExportVo.setRuleIds(remarkVo.getRuleIds()); + } + } + } + // 新的逻辑 + setClockNew(usersByGroupVos, dateRangeDto, keyMap); + return usersByGroupVos; + } + + private RemarkVo getShiftPeriod(List dayList) { + if (CollUtil.isEmpty(dayList)) { + return new RemarkVo(); + } + RemarkVo remarkVo = new RemarkVo(); + // 取出dayList中的RuleId 并去重,然后通过英文逗号拼接 + remarkVo.setRuleIds(dayList.stream() + .filter(item -> item.getSchedulesType() != null) + .sorted(Comparator.comparing(UserRuleListVo::getSchedulesType)) + .map(UserRuleListVo::getRuleId) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.joining(","))); + // 过滤出正常排班,且时段一样的数据,然后根据班次类型进行正序排序 + DayStatisticsUtils.checkOrdinary(dayList, remarkVo); + // 请假 + DayStatisticsUtils.checkLeave(dayList, remarkVo); + // 外出等情况 + checkGoOut(dayList, remarkVo); + // 出差 + DayStatisticsUtils.checkBusinessTrip(dayList, remarkVo); + return remarkVo; + } + + /** + * 处理外出 + * + * @param dayList 每日出勤规则 + * @param remarkVo 组装信息 + */ + private void checkGoOut(List dayList, RemarkVo remarkVo) { + //举例: + //1.班次时间为9-18,外出时间为8-20 最终班次时间栏为:9-18(外出时间:8-20) 打卡结果栏为:无需打卡-无需打卡 + //2.班次为9-12 | 14-18,外出时间为8-20,最终班次时间为9-12 | 14-18(外出时间8-20)打卡结果栏为:无需打卡-无需打卡|无需打卡-无需打卡 + //3.班次时间为9-18,外出时间9-10,16-18,最终班次时间栏为9-18(外出时间:9-10)(外出时间:16-18)打卡结果栏为:无需打卡-无需打卡 + //4.班次时间为9-18,外出选择当天(整天外出)最终班次时间栏为9-18(当天外出),打卡结果栏为:无需打卡-无需打卡 + //5.班次时间为9-12 | 14-18,外出选择当天(整天外出)最终班次时间栏为9-12 | 14-18(当天外出),打卡结果栏为:无需打卡-无需打卡|无需打卡-无需打卡 + List gouOut = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), dayRule.getApplyViewEnable())).collect(Collectors.toList()).stream() + .sorted(Comparator.comparing(UserRuleListVo::getInPoint)) + .collect(Collectors.toList()); + // 组装请假 + if (!gouOut.isEmpty()) { + SimpleDateFormat formatter = new SimpleDateFormat("HH:mm"); + // 如果当天全是外出,那么通过外出命中的时段组装当天上班时段 + if (remarkVo.getShiftPeriod().toString().isEmpty()) { + for (int i = 0; i < gouOut.size(); i++) { + UserRuleListVo vo = gouOut.get(i); + if (i == 0) { + remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint())); + } else { + remarkVo.getShiftPeriod().append(" | ").append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint())); + } + } + } + // 组装外出时段 09:00-18:00(上班外出) 09:00-18:00(下班外出) + List list = new ArrayList<>(); + // 班次日期 + String day = dayList.get(0).getDay(); + SimpleDateFormat ymd = new SimpleDateFormat("yyyy-MM-dd"); + gouOut.forEach(v -> { + // 通过单位判断组装小时外出还是整天外出 + if (v.getAttendanceType() == 1) { + if (null != v.getInStepOutApplyId() && !list.contains(v.getInStepOutApplyId())) { + list.add(v.getInStepOutApplyId()); + // 通过applyId查询外出审批详情 + GoOutVo goOut = leaveApproveMapper.getGoOut(v.getInStepOutApplyId()); + if (2 == goOut.getUnit()) { + remarkVo.getShiftPeriod().append("(当天外出)"); + } else { + // 小时外出跨天次日处理 + // 比对班次时间和申请时间匹配范围 + remarkVo.getShiftPeriod().append("("); + if (!day.equals(ymd.format(goOut.getStartTime()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(goOut.getStartTime())).append("-"); + if (!day.equals(ymd.format(goOut.getEndTime()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(goOut.getEndTime())).append("外出)"); + } + } + if (null != v.getOutStepOutApplyId() && !list.contains(v.getOutStepOutApplyId())) { + list.add(v.getOutStepOutApplyId()); + // 通过applyId查询外出审批详情 + GoOutVo goOut = leaveApproveMapper.getGoOut(v.getOutStepOutApplyId()); + if (2 == goOut.getUnit()) { + remarkVo.getShiftPeriod().append("(当天外出)"); + } else { + // 小时外出跨天次日处理 + remarkVo.getShiftPeriod().append("("); + if (!day.equals(ymd.format(goOut.getStartTime()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(goOut.getStartTime())).append("-"); + if (!day.equals(ymd.format(goOut.getEndTime()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(goOut.getEndTime())).append("外出)"); + } + } + } + }); + } + } + + /** + * 获取考勤组当天打卡数据 + * + * @param userBoundVoMap 用户集合 + * @param groupIdList 考勤组ID集合 + * @param dateRangeDto 日期范围 + * @return AppStatisticsTeamTabListVo + */ + private AppStatisticsTeamTabListVo statisticsTeamTabListVo(Map userBoundVoMap, + List groupIdList, DateRangeDto dateRangeDto) { + List allDataList = newArrayList(), restDataList = newArrayList(), lateDataList = newArrayList(), + earlyLeaveDataList = newArrayList(), leaveDataList = newArrayList(), absenceCardDataList = newArrayList(), + absenceDataList = newArrayList(), makeUpCardDataList = newArrayList(), outworkDataList = newArrayList(), + overtimeDataList = newArrayList(), secondDataList = newArrayList(), busDataList = newArrayList(), + goOutDataList = newArrayList(); + //获取用户排班数据 + List userRuleRecordList = statisticsMapper.getUserRuleRecordList(new ArrayList<>(userBoundVoMap.keySet()), + dateRangeDto, groupIdList, List.of( + AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.LEAVE.getCode(), + AttendanceTypeEnum.WORKOVERTIME.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), + AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), AttendanceTypeEnum.SECONDMENT.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), + AttendanceTypeEnum.STEP_OUT.getCode())); + userRuleRecordList.forEach(record -> record.getClockInResultList().removeIf(result -> Objects.equals(result.getDeleteMark(), 1) || StringUtil.isEmpty(result.getId()))); + Map> userRuleRecordMap = CollUtil.isNotEmpty(userRuleRecordList) ? userRuleRecordList.stream().collect(Collectors.groupingBy(UserRuleRecord::getUserId)) : new HashMap<>(); + //查询手动变更结果的用户信息 + List adminUserIdList = userRuleRecordList.stream() + .filter(record -> CollUtil.isNotEmpty(record.getClockInResultList())) + .flatMap(record -> record.getClockInResultList().stream()) + .map(ClockInResultRecordVo::getAbsenceLeader) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + List userList = CollUtil.isNotEmpty(adminUserIdList) ? v2UserApi.userNameAndCopy(adminUserIdList) : new ArrayList<>(); + Map adminUserMap = CollUtil.isNotEmpty(userList) ? userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) : new HashMap<>(); + // 用户+考勤组分组 + Map userBusCountMap = this.dailyRuleService.getUserBusAndOutCount( + new ArrayList<>(userBoundVoMap.keySet()), groupIdList, + DateUtil.stringToDate(dateRangeDto.getStartDate()), + DateUtil.stringToDate(dateRangeDto.getEndDate()), + AttendanceTypeEnum.BUSINESS_TRIP + ); + // 用户+考勤组分组 + Map userOutCountMap = this.dailyRuleService.getUserBusAndOutCount( + new ArrayList<>(userBoundVoMap.keySet()), groupIdList, + DateUtil.stringToDate(dateRangeDto.getStartDate()), + DateUtil.stringToDate(dateRangeDto.getEndDate()), + AttendanceTypeEnum.STEP_OUT + ); + for (Map.Entry userBoundVo : userBoundVoMap.entrySet()) { + List orDefault = userRuleRecordMap.getOrDefault(userBoundVo.getKey(), newArrayList()); + if (orDefault.isEmpty()) { + continue; + } + ClockUser clockUser = new ClockUser(); + UserBoundVO userInfoVo = userBoundVo.getValue(); + // 获取用户头像 + clockUser.setUserId(userInfoVo.getId()); + clockUser.setUserHeadSculpture(StrUtil.isNotEmpty(userInfoVo.getHeadIcon()) ? + UploaderUtil.uploaderImg(userInfoVo.getHeadIcon()) : null); + clockUser.setUserName(userInfoVo.getUserName()); + clockUser.setIdentifying(StrUtil.isNotEmpty(userInfoVo.getUserName()) ? + PingYinUtil.getPinYinHeadChar(userInfoVo.getUserName().substring(0, 1)).toUpperCase() : ""); + // 更改为返回多个对象,按照用户+考勤组分组返回 + Map, List>> groupByGroupMap = getGroupByUserList(adminUserMap, orDefault); + if (CollUtil.isEmpty(groupByGroupMap)) { + continue; + } + // 判断每个月用户在每个考勤组是否有数据 + Map> attendanceTypeMap = orDefault.stream() + .collect(Collectors.groupingBy(record -> record.getUserId() + "&" + record.getGroupId(), + Collectors.mapping(UserRuleRecord::getAttendanceType, Collectors.toList()))); + groupByGroupMap.forEach((userAndGroupKey, value) -> { + ClockUser clockUserCopy = new ClockUser(); + BeanUtils.copyProperties(clockUser, clockUserCopy); + String groupId = userAndGroupKey.split("&")[1]; + // 借调 + boolean anyMatch = orDefault.stream().anyMatch(item -> item.getSelfGroup() != 0); + if (anyMatch) { + clockUserCopy.setClockTimeSpanList(value.right); + secondDataList.add(clockUserCopy); + } + if (!attendanceTypeMap.containsKey(userAndGroupKey) || CollUtil.isEmpty(orDefault)) { + return; + } + clockUserCopy.setClockTimeSpanList(value.right); + allDataList.add(clockUserCopy); + // 休息日 + if (attendanceTypeMap.get(userAndGroupKey).contains(AttendanceTypeEnum.REST.getCode())) { + restDataList.add(clockUserCopy); + } + // 请假 + if (attendanceTypeMap.get(userAndGroupKey).contains(AttendanceTypeEnum.LEAVE.getCode())) { + leaveDataList.add(clockUserCopy); + } + // 加班 + if (attendanceTypeMap.get(userAndGroupKey).contains(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + overtimeDataList.add(clockUserCopy); + } + // 所有打卡状态 + List allStatusList = orDefault.stream() + .filter(item -> item.getGroupId().equals(groupId) && CollUtil.isNotEmpty(item.getClockInResultList()) && + !DayStatisticsUtils.isHeadAbsence(item.getClockInResultList())) + .flatMap(item -> item.getClockInResultList().stream()) + .map(ClockInResultRecordVo::getClockInStatus) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + // 所有旷工状态 + List allAbsenceList = orDefault.stream() + .filter(item -> item.getGroupId().equals(groupId) && CollUtil.isNotEmpty(item.getClockInResultList()) && + DayStatisticsUtils.isHeadAbsence(item.getClockInResultList())) + .flatMap(item -> item.getClockInResultList().stream()) + .map(ClockInResultRecordVo::getAbsence) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + // 所有补卡状态 + List allRepairedList = orDefault.stream() + .filter(item -> item.getGroupId().equals(groupId) && CollUtil.isNotEmpty(item.getClockInResultList())) + .flatMap(item -> item.getClockInResultList().stream()) + .map(ClockInResultRecordVo::getRepaired) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + // 所有外勤状态 + List allClockInKindList = orDefault.stream() + .filter(item -> item.getGroupId().equals(groupId) && CollUtil.isNotEmpty(item.getClockInResultList())) + .flatMap(item -> item.getClockInResultList().stream()) + .map(ClockInResultRecordVo::getClockInKind) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + // 迟到 + if (CollUtil.isNotEmpty(allStatusList) && allStatusList.contains(ClockInStatusEnum.WORK_LATE.getValue())) { + clockUserCopy.setClockTimeSpanList(value.right); + lateDataList.add(clockUserCopy); + } + // 早退 + if (CollUtil.isNotEmpty(allStatusList) && allStatusList.contains(ClockInStatusEnum.HOME_EARLY.getValue())) { + clockUserCopy.setClockTimeSpanList(value.right); + earlyLeaveDataList.add(clockUserCopy); + } + // 缺卡 + if (CollUtil.isNotEmpty(allStatusList) && allStatusList.contains(ClockInStatusEnum.NO_CLOCK.getValue())) { + clockUserCopy.setClockTimeSpanList(value.right); + absenceCardDataList.add(clockUserCopy); + } + // 旷工 + if (CollUtil.isNotEmpty(allAbsenceList) && allAbsenceList.contains(1)) { + clockUserCopy.setClockTimeSpanList(value.right); + absenceDataList.add(clockUserCopy); + } + // 补卡 + if (CollUtil.isNotEmpty(allRepairedList) && allRepairedList.contains(1)) { + clockUserCopy.setClockTimeSpanList(value.right); + makeUpCardDataList.add(clockUserCopy); + } + // 外勤 + if (CollUtil.isNotEmpty(allClockInKindList) && allClockInKindList.contains(ConstantUtil.KIND_OUTSIDE)) { + clockUserCopy.setClockTimeSpanList(value.right); + outworkDataList.add(clockUserCopy); + } + // 出差 + if (CollUtil.isNotEmpty(userBusCountMap) && userBusCountMap.getOrDefault(userAndGroupKey, 0) > 0) { + clockUserCopy.setClockTimeSpanList(value.right); + busDataList.add(clockUserCopy); + } + // 外出 + if (CollUtil.isNotEmpty(userOutCountMap) && userOutCountMap.getOrDefault(userAndGroupKey, 0) > 0) { + clockUserCopy.setClockTimeSpanList(value.right); + goOutDataList.add(clockUserCopy); + } + }); + } + AppStatisticsTeamTabListVo teamTabListVo = new AppStatisticsTeamTabListVo(); + teamTabListVo.setAllDataList(getCollect(allDataList)); + teamTabListVo.setRestDataList(getCollect(restDataList)); + teamTabListVo.setLateDataList(getCollect(lateDataList)); + teamTabListVo.setEarlyLeaveDataList(getCollect(earlyLeaveDataList)); + teamTabListVo.setLeaveDataList(getCollect(leaveDataList)); + teamTabListVo.setAbsenceCardDataList(getCollect(absenceCardDataList)); + teamTabListVo.setMakeUpCardDataList(getCollect(makeUpCardDataList)); + teamTabListVo.setAbsenceDataList(getCollect(absenceDataList)); + teamTabListVo.setSecondDataList(getCollect(secondDataList)); + teamTabListVo.setOutworkDataList(getCollect(outworkDataList)); + teamTabListVo.setOvertimeDataList(getCollect(overtimeDataList)); + teamTabListVo.setBusDataList(getCollect(busDataList)); + teamTabListVo.setGoOutDataList(getCollect(goOutDataList)); + return teamTabListVo; + } + + /** + * 封装用户排班信息 + * + * @param adminUserMap 管理员用户信息 + * @param userRuleRecordList 排班数据 + */ + private Map, List>> getGroupByUserList(Map adminUserMap, List userRuleRecordList) { + Map, List>> groupByUserMap = new HashMap<>(); + Map> userAndGroupMap = userRuleRecordList.stream().collect(Collectors.groupingBy(record -> record.getUserId() + "&" + record.getGroupId())); + userAndGroupMap.forEach((userAndGroupKey, ruleRecordList) -> { + List clockTimeSpanList = new ArrayList<>(); + List attendanceTypeList = ruleRecordList.stream().map(UserRuleRecord::getAttendanceType).distinct().collect(Collectors.toList()); + for (UserRuleRecord userRuleRecord : ruleRecordList) { + //判断当前班次是否旷工 + boolean isAbsence = userRuleRecord.getClockInResultList().stream().anyMatch(item -> + Objects.nonNull(item.getAbsence()) && item.getAbsence().equals(ConstantUtil.NUM_TRUE)); + UserClockTimeSpan userClockTimeSpan = new UserClockTimeSpan(); + if (!userRuleRecord.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) && + !userRuleRecord.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + userClockTimeSpan.setStartTime(userRuleRecord.getInPoint()); + userClockTimeSpan.setEndTime(userRuleRecord.getOutPoint()); + userClockTimeSpan.setSecond(userRuleRecord.getSelfGroup()); + List inRecordList = new ArrayList<>(); + if (CollUtil.isNotEmpty(userRuleRecord.getClockInResultList())) { + userRuleRecord.getClockInResultList().forEach(clockInResult -> { + UserClockRecord clockRecord = new UserClockRecord(); + BeanUtils.copyProperties(clockInResult, clockRecord); + //是否外勤打卡 + if (Objects.nonNull(clockInResult.getClockInKind()) && clockInResult.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)) { + clockRecord.setEffectiveTime(clockInResult.getClockTime()); + } else { + clockRecord.setEffectiveTime(clockInResult.getEffectiveTime()); + } + //是否补卡 + if (Objects.nonNull(clockInResult.getRepaired()) && clockInResult.getRepaired().equals(ConstantUtil.NUM_TRUE)) { + clockRecord.setClockInKind(3); + } + //增加是否审批中字段 + clockRecord.setClockInTime(clockInResult.getClockTime()); + clockRecord.setIsApprove(clockInResult.getApprovalStatus()); + clockRecord.setAbsence(isAbsence ? 1 : 0); + if (StringUtil.isNotEmpty(clockInResult.getAbsenceLeader()) && !userRuleRecord.getUserId().equals(clockInResult.getAbsenceLeader())) { + clockRecord.setAbsenceLeaderName(adminUserMap.containsKey(clockInResult.getAbsenceLeader()) ? adminUserMap.get(clockInResult.getAbsenceLeader()).getUserName() : "用户不存在"); + } + inRecordList.add(clockRecord); + }); + if (CollUtil.isNotEmpty(inRecordList)) { + userClockTimeSpan.setInRecordList(inRecordList.stream().sorted(Comparator.comparing(UserClockRecord::getClockInType)).collect(Collectors.toList())); + } + } + } + userClockTimeSpan.setAttendanceType(userRuleRecord.getAttendanceType()); + userClockTimeSpan.setApplyViewEnable(userRuleRecord.getApplyViewEnable()); + userClockTimeSpan.setInStepOutType(userRuleRecord.getInStepOutType()); + userClockTimeSpan.setOutStepOutType(userRuleRecord.getOutStepOutType()); + clockTimeSpanList.add(userClockTimeSpan); + } + List collect = clockTimeSpanList.stream().filter(userClockTimeSpan -> + !Objects.equals(userClockTimeSpan.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList()); + MutablePair, List> mutablePair = new MutablePair<>(attendanceTypeList, (CollUtil.isEmpty(collect) ? + clockTimeSpanList : collect).stream().sorted(Comparator.comparing(span -> Objects.isNull(span.getStartTime()) ? new Date() : + span.getStartTime())).collect(Collectors.toList())); + groupByUserMap.put(userAndGroupKey, mutablePair); + }); + return groupByUserMap; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttenceMachineServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttenceMachineServiceImpl.java new file mode 100644 index 0000000..fd659d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttenceMachineServiceImpl.java @@ -0,0 +1,484 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.xuyanwu.spring.file.storage.FileInfo; +import cn.xuyanwu.spring.file.storage.MockMultipartFile; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.SocketApi; +import jnpf.attendance.annotation.Machine; +import jnpf.attendance.mapper.AttendanceMachineManageMapper; +import jnpf.attendance.mapper.AttendanceUserFaceMapper; +import jnpf.attendance.mapper.AttendanceUserFingerprintMapper; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.MachineStrategy; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constant.FileTypeConstant; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.entity.attendance.AttendanceUserFingerprint; +import jnpf.entity.attendance.FtbAttendanceClockIn; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.file.FileApi; +import jnpf.file.FileUploadApi; +import jnpf.model.attendance.dto.ClockInDto; +import jnpf.model.attendance.vo.AttendanceRuleVo; +import jnpf.model.attendance.vo.GroupInfoVo; +import jnpf.model.attendance.vo.attendance.UserTenantVo; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.model.user.UserNoInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.MachineStrategyFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 考勤机服务实现 + * + * @author yanwenfu + * @create 2023-11-29 + */ +@Slf4j +@Service +public class AttenceMachineServiceImpl extends SuperServiceImpl implements AttenceMachineService { + + @Autowired + private V2UserApi v2UserApi; + + @Autowired + private UserApi userApi; + + @Autowired + private AttendanceUserFaceMapper attendanceUserFaceMapper; + + @Resource + private AttendanceUserFingerprintMapper attendanceUserFingerprintMapper; + + @Autowired + private AttendanceClockInService attendanceClockInService; + + @Autowired + private AttendanceMachineManageMapper attendanceMachineManageMapper; + + @Autowired + private CustomTenantUtil customTenantUtil; + + @Autowired + private UserProvider userProvider; + + @Autowired + private FileApi fileApi; + + @Autowired + private FileUploadApi fileUploadApi; + + @Autowired + private SocketApi socketApi; + + @Resource + private MachineStrategyFactory machineStrategyFactory; + + @Override + public void sendUserToMachine(String code, String userId, String sn) { + + UserInfo userInfo = userProvider.get(); + // 查询考勤组用户 + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), null); + log.error("查询用户信息:{}, 参数:{}, {}, {}", userList, code, userId, sn); + if (200 != userList.getCode() || CollectionUtil.isEmpty(userList.getData())) { + return; + } + UserBoundVO userBoundVO = userList.getData().get(0); + PartUserInfoVo user = BeanUtil.copyProperties(userBoundVO, PartUserInfoVo.class); + user.setRealName(userBoundVO.getUserName()); + user.setUserId(userBoundVO.getId()); + MachineEnum machineEnum = MachineEnum.getMachineEnum(code); + if (null == machineEnum) { + log.error("未找到考勤设备"); + return; + } + MachineStrategy machineStrategy = machineStrategyFactory.getMachineStrategy(machineEnum); + machineStrategy.addUserToMachine(userInfo, user, sn); + } + + @Override + public void sendUserToMachine(String code, PartUserInfoVo user, String sn) { + + UserInfo userInfo = userProvider.get(); + MachineEnum machineEnum = MachineEnum.getMachineEnum(code); + if (null == machineEnum) { + log.error("未找到考勤设备"); + return; + } + MachineStrategy machineStrategy = machineStrategyFactory.getMachineStrategy(machineEnum); + machineStrategy.addUserToMachine(userInfo, user, sn); + } + + @Override + public void updateUserInfoByWebsocket(String sn, String userId, String userName) { + + UserInfo userInfo = userProvider.get(); + try { + JSONObject json = new JSONObject(); + json.set("cmd", "to_device"); + json.set("from", "server"); + json.set("to", sn); + JSONObject json1 = new JSONObject(); + json1.set("cmd", "editUser"); + json1.set("user_id", userInfo.getTenantId() + "@" + userId); + json1.set("name", userName); + json.set("data", json1); + socketApi.sendMsg2Client(MachineEnum.KAI_JIA_YI.getValue(), sn, json.toString()); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + public void deleteUser(String sn, String userId) { + + UserInfo userInfo = userProvider.get(); + try { + JSONObject json = new JSONObject(); + json.set("cmd", "to_device"); + json.set("from", "server"); + json.set("to", sn); + JSONObject json1 = new JSONObject(); + json1.set("cmd", "delUser"); + json1.set("user_id", userInfo.getTenantId() + "@" + userId); + json.set("data", json1); + socketApi.sendMsg2Client(MachineEnum.KAI_JIA_YI.getValue(), sn, json.toString()); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + public void deleteUserList(String code, String sn, List userIds) { + + UserInfo userInfo = userProvider.get(); + + MachineEnum machineEnum = MachineEnum.getMachineEnum(code); + if (null == machineEnum) { + log.error("未找到考勤设备"); + return; + } + MachineStrategy machineStrategy = machineStrategyFactory.getMachineStrategy(machineEnum); + machineStrategy.deleteUserList(userInfo, userIds, sn); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Map updateUserInfoPhoto(Map params) { + + Map map = new HashMap<>(); + // 查询数据库是否存在用户数据, 存在, 更新, 不存在, 新增 + Object content = params.get("content"); + JSONArray jsonArray = new JSONArray(content); + List list = jsonArray.toList(JSONObject.class); + String user = list.get(0).get("user_id").toString(); + String[] split = user.split("@"); + if (split.length == 2) { + customTenantUtil.checkOutTenant(split[0]); + } else { + map.put("Result", -2); + map.put("Msg", "切换租户失败"); + return map; + } + for (JSONObject json : list) { + String userId = json.get("user_id").toString().split("@")[1]; + Object fp = json.get("fp"); + if (null != fp) { + log.error("fp: {}", fp); + // 删除原有的指纹 + LambdaUpdateWrapper fingerprintUpdateWrapper = new LambdaUpdateWrapper() + .set(AttendanceUserFingerprint::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(AttendanceUserFingerprint::getDeleteTime, new Date()) + .eq(AttendanceUserFingerprint::getUserId, userId) + .eq(AttendanceUserFingerprint::getDeleteMark, ConstantUtil.NUM_TRUE); + attendanceUserFingerprintMapper.update(null, fingerprintUpdateWrapper); + // 新增新的指纹 + JSONArray insertFpArray = JSONUtil.parseArray(fp); + List insertFpList = new ArrayList<>(); + insertFpArray.toList(JSONObject.class).forEach(v -> { + AttendanceUserFingerprint fingerprint = new AttendanceUserFingerprint(); + fingerprint.setId(FtbUtil.getId()); + fingerprint.setUserId(userId); + fingerprint.setDataId(v.get("id").toString()); + fingerprint.setData(v.get("data").toString()); + fingerprint.setDataName(v.get("name").toString()); + insertFpList.add(fingerprint); + }); + if (!insertFpList.isEmpty()) { + insertFpList.forEach(v -> attendanceUserFingerprintMapper.insert(v)); + } + } + Object vlPhoto = json.get("vl_photo"); + if (null != vlPhoto) { + String url = changeBase64ToImage(vlPhoto.toString()); + log.error("图片路径: {}", url); + updateUserPhoto(url, userId); + } + } + map.put("Result", 0); + map.put("Msg", "更新成功"); + return map; + } + + /** + * 更新考勤机回传的人脸图片 + * @param url 人脸图片 + * @param userId 用户id + */ + private void updateUserPhoto(String url, String userId) { + + LambdaQueryWrapper userFaceQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, userId) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(userFaceQueryWrapper); + if (null != userFace) { + // 更新人脸数据 + LambdaUpdateWrapper userFaceUpdateWrapper = new LambdaUpdateWrapper() + .eq(AttendanceUserFace::getUserId, userId); + userFace.setFaceData(url); + userFace.setFaceDataThumbnail(url); + attendanceUserFaceMapper.update(userFace, userFaceUpdateWrapper); + } else { + // 新增人脸数据 + AttendanceUserFace insertUserFace = new AttendanceUserFace(); + insertUserFace.setId(FtbUtil.getId()); + insertUserFace.setUserId(userId); + insertUserFace.setFaceData(url); + insertUserFace.setFaceDataThumbnail(url); + attendanceUserFaceMapper.insert(insertUserFace); + } + } + + @Override + public String changeImg() { + + // 查询用户照片 + LambdaQueryWrapper userFaceQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, "471608060058616325") + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(userFaceQueryWrapper); + return changeBase64ToImage(userFace.getFaceData()); + } + + @Override + public String machineClockIn(String sn, String userId, String tenantId, String clockInId) throws Exception { + + // 查询考勤机名称 + ClockInDto clockInDto = getClockInDto(sn, userId, tenantId); + MutablePair pair; + if (StringUtils.isEmpty(clockInId)) { + pair = attendanceClockInService.clockIn(clockInDto); + } else { + pair = attendanceClockInService.updateClockIn(clockInId, clockInDto); + } + return pair.getLeft().toString(); + } + + private ClockInDto getClockInDto(String sn, String userId, String tenantId) { + + String name = attendanceMachineManageMapper.getMachineName(sn); + name = StringUtils.isEmpty(name) ? "未知设备" : name; + return new ClockInDto(ConstantUtil.CLOCK_TYPE_NORMAL, sn, ConstantUtil.DEVICE_MACHINE, sn, name, userId, tenantId); + } + + @Override + public String clockIn(String sn, String userId, String tenantId) { + + String result = "0"; + Date today = new Date(); + try { + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + userInfo.setTenantId(tenantId); + List list = attendanceClockInService.getClockInMainInfo(new Date(), userInfo, ConstantUtil.NUM_TRUE); + if (list.isEmpty()) { + generateClockIn(today, sn, userId, tenantId); + return "未找到可打卡班次"; + } + AttendanceRuleVo rule = null; + for (GroupInfoVo groupInfoVo : list) { + rule = groupInfoVo.getAttendanceRuleList().stream().filter(v -> v.getCurrentPeriod().equals(ConstantUtil.NUM_TRUE)).findFirst().orElse(null); + if (null != rule) { + break; + } + } + if (null == rule) { + // 查询当前时间在出勤规则上班时间 -> 下班缺卡时间段(不能为空)内, 下班打卡记录不为空, 取缺卡时间最大的一个 + List ruleList = new ArrayList<>(); + for (GroupInfoVo groupInfoVo : list) { + AttendanceRuleVo ruleCheck = groupInfoVo.getAttendanceRuleList().stream().filter(v -> null != v.getOutLackPoint() && DateDetail.checkTimeBetween(new Date(), v.getInPoint(), v.getOutLackPoint())).findFirst().orElse(null); + if (null != ruleCheck && null != ruleCheck.getOffWorkInfoVo()) { + ruleList.add(ruleCheck); + } + } + if (ruleList.isEmpty()) { + generateClockIn(today, sn, userId, tenantId); + return "未找到可打卡出勤规则"; + } + rule = ruleList.stream().max(Comparator.comparing(AttendanceRuleVo::getOutLackPoint)).get(); + } + if (null == rule.getOnWorkInfoVo()) { + // 打上班卡 + result = machineClockIn(sn, userId, tenantId, null); + } + // 判定出勤规则类型 + boolean attendanceTypeCheck = checkAttendanceType(rule.getAttendanceType()); + if (null != rule.getOnWorkInfoVo() && null == rule.getOffWorkInfoVo()) { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 加班超过上班时间无法更新打卡, 所以设置默认不允许迟到 + rule.setLateEnable(ConstantUtil.NUM_FALSE); + } + // 更新上班卡 + if (attendanceTypeCheck && ConstantUtil.NUM_FALSE == rule.getLateEnable() && today.before(rule.getInPoint()) && !rule.getOnWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + if (!rule.getOnWorkInfoVo().getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) || + (rule.getOnWorkInfoVo().getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) && rule.getOnWorkInfoVo().getApplyType().equals(ConstantUtil.APPLY_OUTSIDE))) { + // 不允许迟到, 更新上班打卡 + result = machineClockIn(sn, userId, tenantId, rule.getOnWorkInfoVo().getClockInId()); + } + } else if (attendanceTypeCheck && ConstantUtil.NUM_TRUE == rule.getLateEnable() && today.before(rule.getLatePoint()) && !rule.getOnWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + if (!rule.getOnWorkInfoVo().getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) || + (rule.getOnWorkInfoVo().getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) && rule.getOnWorkInfoVo().getApplyType().equals(ConstantUtil.APPLY_OUTSIDE))) { + // 不允许迟到, 更新上班打卡 + result = machineClockIn(sn, userId, tenantId, rule.getOnWorkInfoVo().getClockInId()); + } + } else { + // 打下班卡 + result = machineClockIn(sn, userId, tenantId, null); + } + } + if (attendanceTypeCheck && null != rule.getOffWorkInfoVo() && !rule.getOffWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + if (!rule.getOffWorkInfoVo().getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) || + (rule.getOffWorkInfoVo().getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) && rule.getOffWorkInfoVo().getApplyType().equals(ConstantUtil.APPLY_OUTSIDE))) { + // 更新下班卡 + result = machineClockIn(sn, userId, tenantId, rule.getOffWorkInfoVo().getClockInId()); + } + } + } catch (Exception e) { + e.printStackTrace(); + log.error("打卡失败,{}", e.getMessage()); + result = ConstantUtil.STR_FALSE; + } + // 打卡不成功也会生成打卡记录, 但没有打卡结果 + if (!result.equals("1") && !result.equals("2") && !result.equals("3")) { + generateClockIn(today, sn, userId, tenantId); + } + return result; + } + + @Override + public void updateKeMiPhoto(String userId, String photoUrl, String tenantId) { + + List list = v2UserApi.userListAndCopy(List.of(userId), null, tenantId); + if (null == list || list.isEmpty()) { + log.error("未找到用户信息, 用户no: {}", userId); + return; + } + UserBoundVO userInfo = list.get(0); + updateUserPhoto(photoUrl, userInfo.getId()); + } + + @Override + @Machine(dealAction = ActionEnum.DA_KA, factory = MachineEnum.KE_MI) + public void KeMiClockIn(UserTenantVo userTenant, String devId, String tenantId) { + + clockIn(devId, userTenant.getUserId(), tenantId); + } + + private void generateClockIn(Date today, String sn, String userId, String tenantId) { + ClockInDto clockInDto = getClockInDto(sn, userId, tenantId); + FtbAttendanceClockIn clockIn = new FtbAttendanceClockIn(); + clockIn.setId(FtbUtil.getId()); + clockIn.setDay(today); + clockIn.setUserId(userId); + clockIn.setClockInTime(today); + clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + clockIn.setClockInKind(clockInDto.getClockInKind()); + clockIn.setAddress(clockInDto.getAddress()); + clockIn.setDeviceType(clockInDto.getDeviceType()); + clockIn.setDeviceId(clockInDto.getDeviceId()); + clockIn.setDeviceName(clockInDto.getDeviceName()); + // 添加到数据库 + attendanceClockInService.save(clockIn); + } + + private boolean checkAttendanceType(Integer attendanceType) { + return attendanceType.equals(AttendanceTypeEnum.ORDINARY.getCode()) || attendanceType.equals(AttendanceTypeEnum.WORKOVERTIME.getCode()); + } + + private String changeBase64ToImage(String faceData) { + + if (StringUtils.isEmpty(faceData)) { + return ""; + } + // 解码base64 + String decode = URLDecoder.decode(faceData, StandardCharsets.UTF_8); + String imgStr = decode.replaceFirst("data:image/jpeg;base64,", ""); + // 转换成图片 + String replace = imgStr.replaceAll("\r", ""); + String str = replace.replaceAll("\n", ""); + byte[] bytes = Base64.getDecoder().decode(str); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + String url; + try { + BufferedImage image = ImageIO.read(byteArrayInputStream); + byteArrayInputStream.close(); + String uuid = FtbUtil.getId(); + String path = fileApi.getPath(FileTypeConstant.TEMPORARY); + String tmpPath = System.getProperty("java.io.tmpdir") + path; + Path p = Paths.get(tmpPath); + if (!Files.exists(p)) { + Files.createDirectories(p); + } + String fileName = tmpPath + uuid + ".jpg"; + File outputFile = new File(fileName); + ImageIO.write(image, "jpg", outputFile); + MultipartFile multiFile = new MockMultipartFile(uuid, outputFile.getName(), MediaType.MULTIPART_FORM_DATA_VALUE, new FileInputStream(outputFile)); + FileInfo fileInfo = fileUploadApi.uploadFile(multiFile, path, multiFile.getName()); + url = fileInfo.getUrl().replace("/jnpf-resource-1304460613/", ""); + FileUtil.deleteFile(fileName); + } catch (IOException e) { + throw new RuntimeException(e); + } + return url; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceAIServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceAIServiceImpl.java new file mode 100644 index 0000000..bcad22d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceAIServiceImpl.java @@ -0,0 +1,47 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceAIMapper; +import jnpf.attendance.service.AttendanceAIService; +import jnpf.attendance.service.OvertimeRuleService; +import jnpf.model.attendance.dto.AttendanceReqDto; +import jnpf.model.attendance.vo.attendance.ClockDataReqVo; +import jnpf.model.attendance.vo.attendance.ClockRecordVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.util.List; + +/** + * 考勤ai服务实现 + * + * @author yanwenfu + * @create 2026-05-07 + */ +@Service +public class AttendanceAIServiceImpl implements AttendanceAIService { + + @Resource + private AttendanceAIMapper attendanceAIMapper; + @Resource + private OvertimeRuleService overtimeRuleService; + + @Override + public ClockDataReqVo getClockRecordByDate(AttendanceReqDto dto) { + + LocalDate date = LocalDate.parse(dto.getQueryDate()); + ClockDataReqVo req = new ClockDataReqVo(); + req.setDate(date); + // 查询考勤打卡记录 + List dataList = attendanceAIMapper.getClockRecordList(dto.getUserId(), dto.getQueryDate()); + req.setDataList(dataList); + return req; + } + + @Override + public OvertimeRuleVo getOvertimeRule(String groupId) { + + return overtimeRuleService.getGroupOvertimeRule(groupId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApprovalSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApprovalSettingServiceImpl.java new file mode 100644 index 0000000..acad4d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApprovalSettingServiceImpl.java @@ -0,0 +1,52 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceApprovalSettingMapper; +import jnpf.attendance.service.AttendanceApprovalSettingService; +import jnpf.entity.AttendanceApprovalSetting; +import jnpf.enums.attendance.ApprovalPermissionTypeEnum; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.model.attendance.dto.AttendanceApprovalSettingDto; +import jnpf.util.FtbUtil; +import jnpf.util.JsonUtil; +import jnpf.util.RandomUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service +public class AttendanceApprovalSettingServiceImpl implements AttendanceApprovalSettingService { + + @Resource + private AttendanceApprovalSettingMapper attendanceApprovalSettingMapper; + + @Override + public void update(AttendanceApprovalSettingDto attendanceApprovalSettingDto) { + AttendanceApprovalSetting attendanceApprovalSetting = JsonUtil.getJsonToBean(attendanceApprovalSettingDto, AttendanceApprovalSetting.class); + if (attendanceApprovalSetting.getId().isEmpty()) { + attendanceApprovalSetting.setId(FtbUtil.getId()); + attendanceApprovalSettingMapper.insert(attendanceApprovalSetting); + }else { + attendanceApprovalSettingMapper.updateById(attendanceApprovalSetting); + } + } + + @Override + public void addTemplateSetting(String groupId) { + save(groupId, ApprovalSettingTypeEnum.ROUTINE.getCode(), ApprovalPermissionTypeEnum.CURRENT_GROUP.getCode()); + save(groupId, ApprovalSettingTypeEnum.ACTION_RESULT.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + save(groupId, ApprovalSettingTypeEnum.OUT.getCode(), ApprovalPermissionTypeEnum.CURRENT_GROUP.getCode()); + save(groupId, ApprovalSettingTypeEnum.LEAVE.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + save(groupId, ApprovalSettingTypeEnum.OVERTIME.getCode(), ApprovalPermissionTypeEnum.CURRENT_GROUP.getCode()); + save(groupId, ApprovalSettingTypeEnum.GO_OUT.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + save(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + } + + private void save(String groupId, Integer type, Integer permission) { + AttendanceApprovalSetting attendanceApprovalSetting = new AttendanceApprovalSetting(); + attendanceApprovalSetting.setId(RandomUtil.uuId()); + attendanceApprovalSetting.setType(type); + attendanceApprovalSetting.setPermission(permission); + attendanceApprovalSetting.setGroupId(groupId); + attendanceApprovalSettingMapper.insert(attendanceApprovalSetting); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApproveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApproveServiceImpl.java new file mode 100644 index 0000000..df45334 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceApproveServiceImpl.java @@ -0,0 +1,3343 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.*; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.ActionResult; +import jnpf.constants.AttendanceConstant; +import jnpf.constants.AttendancePermissionConstant; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.engine.vo.UserOperatorVo; +import jnpf.entity.AttendanceApprovalSetting; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.AttendanceManagerPermission; +import jnpf.entity.attendance.*; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.enums.attendance.*; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.exception.ApproveException; +import jnpf.exception.HandleException; +import jnpf.exception.LoginException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.attendance.vo.flow.FlowTaskVo; +import jnpf.model.doclibrary.vo.UseDetailVo; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveOaDto; +import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonneApi; +import jnpf.util.*; +import jnpf.workflow.mapper.ApplyAttendanceChangeMapper; +import jnpf.workflow.mapper.ApplyAttendanceRepairMapper; +import jnpf.workflow.service.BusinessTripApproveService; +import jnpf.workflow.service.FlowTaskService; +import jnpf.workflow.service.GoOutApproveService; +import jnpf.workflow.service.LeaveApproveService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/11/22 + */ + +@Service +@Slf4j +public class AttendanceApproveServiceImpl implements AttendanceApproveService { + + + @Autowired + private UserProvider userProvider; + + @Resource + private AttendanceBalanceRecordMapper attendanceBalanceRecordMapper; + + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + + @Resource + private V2UserApi v2UserApi; + @Autowired + private V2OrganizeApi organizeApi; + + @Resource + private AttendanceBalanceUseRecordMapper attendanceBalanceUseRecordMapper; + + @Resource + private AttendanceClockInMapper attendanceClockInMapper; + + @Resource + private ApplyAttendanceRepairMapper applyAttendanceRepairMapper; + + @Resource + private ApplyAttendanceChangeMapper applyAttendanceChangeMapper; + + @Resource + private AttendanceClockInResultService attendanceClockInResultService; + + @Resource + private AttendanceClockInService attendanceClockInService; + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + + @Resource + private AttendanceDailyRuleMapper attendanceDailyRuleMapper; + + @Resource + private AttendanceApprovalSettingMapper attendanceApprovalSettingMapper; + @Resource + private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + + @Resource + private AttendanceGroupService attendanceGroupService; + + @Resource + private AttendanceLeaveApproveMapper attendanceLeaveApproveMapper; + + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + + @Resource + private AttendanceBaseSettingService attendanceBaseSettingService; + + @Resource + private AttendanceGroupUserMapper attendanceGroupUserMapper; + + @Resource + private AttendanceSuperAdminService attendanceSuperAdminService; + + + @Autowired + private AttendanceUserService attendanceGroupUserService; + + @Resource + private StorageRestMapper storageRestMapper; + + @Resource + private FlowTaskService flowTaskService; + + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Autowired + private UserAntifreeze userAntifreeze; + + @Autowired + private RedisUtil redisUtil; + + @Resource + private AttendanceUserService attendanceUserService; + + @Resource + private BusinessTripApproveService businessTripApproveService; + + @Resource + private GoOutApproveService goOutApproveService; + + @Resource + private LeaveApproveService leaveApproveService; + + @Resource + private AttendanceUserBalanceRecordService attendanceUserBalanceRecordService; + + @Resource + @Lazy + private AttendanceLeaveTypeService attendanceLeaveTypeService; + + @Resource + @Lazy + private FtbPersonneApi ftbPersonneApi; + + @Resource + @Lazy + private AttendanceUserBalanceMapper attendanceUserBalanceMapper; + + + + @Override + public PageInfo getUseDetail(String id, BalanceQueryDto balanceQueryDto) { + PageHelper.startPage(balanceQueryDto.getCurrentPage(), balanceQueryDto.getPageSize()); + PageInfo page = new PageInfo<>(attendanceBalanceUseRecordMapper.getUserBalanceDetail(id)); + if (!page.getList().isEmpty()) { + //使用日期指我用在哪一天,记录日期是实际扣除的时间。比如我申请11-17 -- 11-18请假抵扣 使用日期 11-17 -- 11-18,但是我是11月10号申请的且在当天通过的。记录日期 11-10 + //处理使用日期 + page.getList().forEach(v -> { + v.setUseDate(v.getStartTime().equals(v.getEndTime()) ? v.getStartTime() : v.getStartTime() + "--" + v.getEndTime()); + String unitStr = Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), v.getUnit()) ? BalanceEnum.BALANCE_UNIT_XH.getMsg() : BalanceEnum.BALANCE_UNIT_T.getMsg(); + v.setQuotaStr(v.getQuota().setScale(2, RoundingMode.HALF_UP) + unitStr); + }); + } + return page; + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean invalidationCoupons(String tenantId) { + if (StrUtil.isBlank(tenantId)) { + log.error("租户ID不能为空"); + throw new IllegalArgumentException("租户ID不能为空"); + } + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + log.error("切换租户失败, 租户id: {}", tenantId); + throw new RuntimeException("invalidationCoupons方法,切换租户数据源失败,无法执行券失效操作", e); + } + // 查询需要定时失效的劵 + List list = attendanceBalanceRecordMapper.selectInvalidationCoupons(); + if (CollUtil.isEmpty(list)) { + log.debug("租户 {} 没有需要失效的券", tenantId); + return Boolean.TRUE; + } + // 生成过期记录 + List balanceDetailVoList = buildBalanceDetails(list); + if (!balanceDetailVoList.isEmpty()) { + // 消费记录 仅记录生成劵的使用记录 + attendanceUserBalanceMapper.batchAddBalanceDetail(balanceDetailVoList); + } + //定时过期 + List ids = list.stream().map(AttendanceBalanceRecordEntity::getId).collect(Collectors.toList()); + if (!ids.isEmpty()) { + // 过期劵 + attendanceBalanceRecordMapper.invalidationCoupons(ids); + } + return Boolean.TRUE; + } + + + + private List buildBalanceDetails(List coupons) { + return coupons.stream() + .map(coupon -> { + AttendanceUserBalanceDetailVo detail = new AttendanceUserBalanceDetailVo(); + detail.setId(FtbUtil.getId()); + detail.setObjectId(coupon.getObjectId()); + detail.setUserId(coupon.getUserId()); + detail.setQuota(coupon.getBalance().negate()); // 负数表示扣减 + detail.setContent(buildExpireMessage(coupon)); + return detail; + }) + .collect(Collectors.toList()); + } + + private String buildExpireMessage(AttendanceBalanceRecordEntity coupon) { + String issueDate = DateDetail.getDate2Str(coupon.getCreatorTime(), DateDetail.DF); + BigDecimal remainingBalance = coupon.getBalance().setScale(2, RoundingMode.HALF_UP); + return String.format("系统已将%s发放未使用剩余的%.2f天余额,因到期自动清除", issueDate, remainingBalance); + } + + @Override + public List getClasses(BalanceQueryDto balanceQueryDto) { + List list = new ArrayList<>(); + //开始日期选择后触发接口返回当天及后一天的排班信息 + String userId = userProvider.get().getUserId(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date startTime = new Date(Long.parseLong(balanceQueryDto.getStartDay())); + String startTimeStr = sdf.format(startTime); + //获取指定日期的排班 + setUserClasses(startTimeStr, list, userId); + //获取下一天的排班 + try { + Date startDate = sdf.parse(startTimeStr); + Date nextDay = DateDetail.getNextDay(startDate); + String nextDayStr = sdf.format(nextDay); + setUserClasses(nextDayStr, list, userId); + } catch (ParseException e) { + throw new RuntimeException(e); + } + return list; + } + + @Override + public LeaveShiftVo getWorkOverTimeShifts(BalanceQueryDto balanceQueryDto) throws HandleException { + // 2024年10月23日 本版本确认该接口没有任何校验,直接返回用户涉及班次及时长 + if (StringUtils.isNotEmpty(balanceQueryDto.getTaskId())) { + AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(balanceQueryDto.getTaskId()); + if (1 == workOverTime.getStatus()) { + return JsonUtil.getJsonToBean(workOverTime.getShiftInvolved(), LeaveShiftVo.class); + } + } + String userId = StringUtil.isEmpty(balanceQueryDto.getUserId()) ? userProvider.get().getUserId() : balanceQueryDto.getUserId(); + LeaveShiftVo leaveShiftVo = new LeaveShiftVo(); + Date startTime = new Date(Long.parseLong(balanceQueryDto.getStartTime())); + Date endTime = new Date(Long.parseLong(balanceQueryDto.getEndTime())); + DateTime dateTime = cn.hutool.core.date.DateUtil.offsetDay(startTime, 1); + Assert.isFalse(dateTime.before(endTime), "所选时间超过24小时"); + log.info("请求涉及班次入参 userId: {},startTime : {},endTime : {} ", userId, startTime, endTime); + // 新加上下半天参数及请假类型参数 // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间 + ApplyParam applyParam = new ApplyParam(userId, startTime, endTime, AttendanceTypeEnum.WORKOVERTIME, null, null, null); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam); + log.info("dailyRuleResultVos : {}", dailyRuleResultVos); + leaveShiftVo.getDailyRuleVos().addAll(dailyRuleResultVos); + // 根据请假类型进行小时/天的时长统计 + // 计算加班申请时长小时 + leaveShiftVo.setTotalDuration(FtbUtil.getTimeDifference(startTime, endTime).toString()); + return leaveShiftVo; + } + + @Override + public LeaveShiftVo getLeaveDuration(LeaveQueryDto leaveQueryDto) throws HandleException { + // 2024年10月23日 本版本确认该接口没有任何校验,直接返回用户涉及班次及时长 + + String userId = StringUtil.isEmpty(leaveQueryDto.getUserId()) ? userProvider.get().getUserId() : leaveQueryDto.getUserId(); + LeaveShiftVo leaveShiftVo = new LeaveShiftVo(); + boolean flg = true; + if (StringUtils.isNotEmpty(leaveQueryDto.getTaskId())) { + leaveQueryDto.setApplyId(leaveQueryDto.getTaskId()); + } + Integer unit = leaveQueryDto.getUnit(); + if (StringUtils.isNotEmpty(leaveQueryDto.getApplyId())) { + log.info("leaveQueryDto.getApplyId()" + leaveQueryDto.getApplyId()); + AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(leaveQueryDto.getApplyId()); + if (null == attendanceLeaveApproveVo) { + log.error("未找到对应的请假审批"); + throw new HandleException("未找到对应的请假审批"); + } + // 有对应的审批 + if (1 == attendanceLeaveApproveVo.getStatus()) { + leaveShiftVo = JsonUtil.getJsonToBean(attendanceLeaveApproveVo.getShiftInvolved(), LeaveShiftVo.class); + return leaveShiftVo; + } + flg = false; + userId = attendanceLeaveApproveVo.getUserId(); + unit = attendanceLeaveApproveVo.getUnit(); + } + log.info("flg === {}", flg); + Date startTime = new Date(Long.parseLong(leaveQueryDto.getStartTime())); + Date endTime = new Date(Long.parseLong(leaveQueryDto.getEndTime())); + if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) { + // 和出勤换算比做对比,算出时长 + BigDecimal timeDifference = DateDetail.getTimeDifference(startTime, endTime); + // 小时假不能超过24小时 + if (timeDifference.compareTo(BigDecimal.valueOf(24)) > 0) { + log.error("小时假不能超过24小时"); + throw new HandleException("小时假不能超过24小时"); + } + } + log.info("请求涉及班次入参 userId: {},startTime : {},endTime : {} ", userId, startTime, endTime); + // 新加上下半天参数及请假类型参数 // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间 + ApplyParam applyParam = new ApplyParam(userId, startTime, endTime, AttendanceTypeEnum.LEAVE, leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeType(), leaveQueryDto.getUnit()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + log.info("dailyRuleResultVos : {}", dailyRuleResultVos); + List dailyCopy = dailyRuleResultVos; + leaveShiftVo.getDailyRuleVos().addAll(dailyRuleResultVos); + // 根据请假类型进行小时/天的时长统计 + //申请时长小时 + BigDecimal day = BigDecimal.ZERO; + dailyRuleResultVos = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList()); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (null != dailyRuleResultVo.getInPoint() && null != dailyRuleResultVo.getOutPoint()) { + day = day.add(dailyRuleResultVo.getLeaveDays()); + } + } + //先根据日期date分组 + DailyRuleResultVo min = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).min(Comparator.comparing(DailyRuleResultVo::getInPoint)).orElse(null); + if (null != min) { + leaveQueryDto.setStartTimeHit(min.getInPoint()); + } else { + leaveQueryDto.setStartTimeHit(startTime); + } + DailyRuleResultVo max = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).max(Comparator.comparing(DailyRuleResultVo::getOutPoint)).orElse(null); + if (null != max) { + leaveQueryDto.setEndTimeHit(max.getOutPoint()); + } else { + leaveQueryDto.setEndTimeHit(endTime); + } + // 命中时长为0 表示未命中班次且需要特殊处理半天及天的请假时间 + if (BigDecimal.ZERO.compareTo(day) == 0 && !Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(),leaveQueryDto.getUnit())) { + leaveQueryDto.setStartTimeHit(DateDetail.getDayBeginByDay(applyParam.getStart())); + // endtime 取日期的23:59:59 + leaveQueryDto.setEndTimeHit(DateDetail.getDayEndByDay(applyParam.getEnd())); + } + // 处理划线排班 + day = getUnderline(dailyCopy, userId, applyParam.getStart(), applyParam.getEnd(), day,leaveQueryDto.getUnit()); + // 如果在这儿day的值还是=0 则说明没有命中班次,则需要重新计算请假时长 + if (BigDecimal.ZERO.compareTo(day) == 0) { + // 请假时长公式: + day = getDay(unit, leaveQueryDto, userId, day,null); + } + // 保留两位 + leaveShiftVo.setTotalDayDuration(day.setScale(2, RoundingMode.HALF_UP).toString()); + return leaveShiftVo; + } + + /** + * 获取请假时长 + * + * @param unit + * @param leaveQueryDto + * @param userId + * @param day + * @param attendanceBaseSetting + **/ + @NotNull + private BigDecimal getDay(Integer unit, LeaveQueryDto leaveQueryDto, String userId, BigDecimal day,AttendanceBaseSetting attendanceBaseSetting) throws HandleException { + // 小时、半天、全天只区分命中和未命中,命中了时长=命中时长,未命中班次 请假时长 = 结束-开始(区分小时1天限制,半天取一个半天0.5,天一天取1) + if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) { + // 和出勤换算比做对比,算出时长 + BigDecimal timeDifference = DateDetail.getTimeDifference(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit()); + // 小时假不能超过24小时 + if (timeDifference.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + if(null == attendanceBaseSetting) { + attendanceBaseSetting = getAttendanceRatio(leaveQueryDto, userId); + attendanceBaseSetting.setAttendanceRatio(null == attendanceBaseSetting.getAttendanceRatio() ? new BigDecimal("8") : attendanceBaseSetting.getAttendanceRatio()); + } + + if (!DateDetail.getDate2Str(leaveQueryDto.getStartTimeHit(), DateDetail.DF).equals(DateDetail.getDate2Str(leaveQueryDto.getEndTimeHit(), DateDetail.DF))) { + // 小时假跨天,单独拆分每天的时间进行计算时长 + // 获取前一天的时长 + BigDecimal dayTime1 = DateDetail.getTimeDifference(leaveQueryDto.getStartTimeHit(), DateDetail.getDayEndByDay(leaveQueryDto.getStartTimeHit())); + // 获取后一天的时长 + BigDecimal dayTime2 = DateDetail.getTimeDifference(DateDetail.getDayBeginByDay(leaveQueryDto.getEndTimeHit()), leaveQueryDto.getEndTimeHit()); + BigDecimal hours1 = dayTime1.divide(attendanceBaseSetting.getAttendanceRatio(), 3, RoundingMode.HALF_UP); + BigDecimal hours2 = dayTime2.divide(attendanceBaseSetting.getAttendanceRatio(), 3, RoundingMode.HALF_UP); + day = (hours1.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : hours1).add(hours2.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : hours2).setScale(2, RoundingMode.HALF_UP); + } else { + BigDecimal hours = timeDifference.divide(attendanceBaseSetting.getAttendanceRatio(), 2, RoundingMode.HALF_UP); + day = hours.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : hours; + } + } else if (Objects.equals(BalanceEnum.BALANCE_UNIT_T.getCode(), unit)) { + // 天类型请假计算时长结束日期减开始日期 + day = DateDetail.calculateDayDiff(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit()).add(BigDecimal.ONE); + } else { + // 半天计算时长 + day = DateDetail.calculateHalfDayDiff(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeHit(), leaveQueryDto.getEndTimeType()); + } + return day; + } + + private AttendanceBaseSetting getAttendanceRatio(LeaveQueryDto leaveQueryDto, String userId) { + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), Collections.singletonList(userId), null); + String groupId = attendanceGroupUsers.get(0).getGroupId(); + Map settingMap = attendanceBaseSettingService.getEnableBaseSettingAll(Collections.singletonList(groupId)); + return settingMap.get(groupId); + } + + /** + * 获取划线排班涉及时长 + * @param userId 用户 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param dailyCopy 排班集合 + * @param day 天数 + * @param unit 单位 : 1 :小时 2: 天 3.半天 + */ + private BigDecimal getUnderline(List dailyCopy, String userId, Date startTime, Date endTime , BigDecimal day,Integer unit) throws HandleException { + dailyCopy = dailyCopy.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList()); + if (!dailyCopy.isEmpty()) { + if (Objects.equals(unit, BalanceEnum.BALANCE_UNIT_XH.getCode())){ + BigDecimal attendanceRatio = getAttendanceRatio(userId, startTime, endTime); + // 根据日期进行分组 + Map> dayDailys = dailyCopy.stream().collect(Collectors.groupingBy(DailyRuleResultVo::getDate)); + for (Map.Entry> entry : dayDailys.entrySet()) { + List dailyRuleResultVos1 = entry.getValue(); + BigDecimal zero = BigDecimal.ZERO; + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos1) { + zero = zero.add(dailyRuleResultVo.getDuration()); + } + // 一天最多扣一天的劵 + zero = zero.divide(attendanceRatio, 3, RoundingMode.HALF_UP).compareTo(BigDecimal.ONE) >= 0 ? BigDecimal.ONE : zero.divide(attendanceRatio, 3, RoundingMode.HALF_UP); + day = day.add(zero); + } + } else { + // 获取数据去重后的日期 + List list = dailyCopy.stream().map(DailyRuleResultVo::getDate).distinct().collect(Collectors.toList()); + day = day.add(new BigDecimal(list.size())); + } + } + return day.setScale(2, RoundingMode.HALF_UP); + } + + + @NotNull + private BigDecimal getAttendanceRatio(String userId, Date startTime, Date endTime) throws HandleException { + Set groupIds = getGroupIdsNew(userId, startTime, endTime); + if (groupIds.size() > 1) { + // 选择的时间段范围内有多个考勤组 请假失败 + log.error("选择的时间段范围内有其他借调信息"); + throw new HandleException("选择的时间段范围内有其他借调信息"); + } + if (groupIds.isEmpty()) { + log.error("未找到用户的考勤组"); + throw new HandleException("未找到用户的考勤组"); + } + List list = new ArrayList<>(groupIds); + Map enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(list); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(list.get(0)); + // 查询用户对应的考勤组配置的基础规则 + return null == attendanceBaseSetting ? new BigDecimal("8") : attendanceBaseSetting.getAttendanceRatio(); + } + + + /** + * 获取用户时间段内所在考勤组 + * + * @param userId 用户 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 考勤组ID + * @throws HandleException 获取失败 + */ + + private Set getGroupIdsNew(String userId, Date startTime, Date endTime) throws HandleException { + // 获取用户时间段范围内的所有考勤组 + GroupCheckVo groupCheckVo = new GroupCheckVo(); + List userGroupList = attendanceGroupUserMapper.getUserGroupList(userId); + if (null != userGroupList && !userGroupList.isEmpty()) { + List selfGroup = userGroupList.stream().filter(v -> 2 == v.getType()).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(selfGroup)) { + setMyGroup(userGroupList, groupCheckVo); + } + checkSelfGroupNew(groupCheckVo, selfGroup, startTime, endTime); + if (groupCheckVo.isMyType()) { + setMyGroup(userGroupList, groupCheckVo); + } + if (CollectionUtil.isEmpty(groupCheckVo.getGroupIds()) || groupCheckVo.getGroupIds().isEmpty()) { + throw new HandleException("未找到你所属考勤组!"); + } + } + return groupCheckVo.getGroupIds(); + } + + private void checkSelfGroupNew(GroupCheckVo groupCheckVo, List selfGroup, Date startTime, Date endTime) { + for (AttendanceGroupUserVo attendanceGroupUserVo : selfGroup) { + List secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class); + if (secondmentDateVos.isEmpty()) { + groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId()); + break; + } else { + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(dateVo.getStartTime(), startTime, endTime) + || DateDetail.checkTimeBetween(dateVo.getEndTime(), startTime, endTime)) { + groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId()); + if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime()) + && DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())) { + groupCheckVo.setMyType(false); + } + break; + } + } + } + } + } + + + /** + * 校验借调考勤组信息 + * + * @param groupIds 考勤组ids + * @param selfGroup 借调考勤组 + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void checkSelfGroup(Set groupIds, List selfGroup, Date startTime, Date endTime) { + for (AttendanceGroupUserVo attendanceGroupUserVo : selfGroup) { + List secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class); + if (secondmentDateVos.isEmpty()) { + groupIds.add(attendanceGroupUserVo.getGroupId()); + break; + } else { + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(dateVo.getStartTime(), startTime, endTime) + || DateDetail.checkTimeBetween(dateVo.getEndTime(), startTime, endTime)) { + groupIds.add(attendanceGroupUserVo.getGroupId()); + break; + } + } + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void leaveApprove(String id, Integer status, String tenantId, String userId, String userName) throws ApproveException { + AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(id); + if (null != attendanceLeaveApproveVo) { + consumption(id, attendanceLeaveApproveVo, status, tenantId, userId, userName); + } + } + + + // 2.1.1 已重构 加入解耦班次逻辑 + @Override + public LeaveConsumptionDetailVo getResidueBalance(LeaveQueryDto leaveQueryDto) throws HandleException, ApproveException { + log.error("leaveQueryDto:{}", leaveQueryDto); + // (leaveTypeUnit=null, startTime=1730131200000, endTime=1730131200000, startTimeType=1, endTimeType=1, leaveTypeId=1850802520956473344, balanceStatus=0, paid=1, noPaid=1, applyId=null, typeId=1850802520956473344, unit=3) + String userId = userProvider.get().getUserId(); + Integer unit = leaveQueryDto.getUnit(); + Date startTime = new Date(Long.parseLong(leaveQueryDto.getStartTime())); + Date endTime = new Date(Long.parseLong(leaveQueryDto.getEndTime())); + if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) { + // 和出勤换算比做对比,算出时长 + BigDecimal timeDifference = DateDetail.getTimeDifference(startTime, endTime); + // 小时假不能超过24小时 + if (timeDifference.compareTo(BigDecimal.valueOf(24)) > 0) { + log.error("小时假不能超过24小时"); + throw new HandleException("小时假不能超过24小时"); + } + } + + if (StringUtil.isNotEmpty(leaveQueryDto.getApplyId())) { + AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(leaveQueryDto.getApplyId()); + if (Objects.isNull(attendanceLeaveApproveVo)) { + log.error("未找到本次请假审批提交记录"); + throw new ApproveException("未找到本次请假审批提交记录"); + } + userId = attendanceLeaveApproveVo.getUserId(); + } + //通过请假类型id查询请假类型详情 + AttendanceLeaveRulesVo userLeaveDetail = attendanceLeaveTypeService.getUserLeaveDetail(leaveQueryDto.getTypeId(), userId); + if (null == userLeaveDetail) { + throw new HandleException("未找到本次请假审批选择的类型"); + } + log.info("leaveSetting: {}", userLeaveDetail); + // 划线排班不能请半天类型的假 + if (Objects.equals(BalanceEnum.BALANCE_UNIT_BT.getCode(), userLeaveDetail.getUnit())) { + boolean lineScheduleByUserId = attendanceDailyRuleService.hasLinearRulesByPeriod(userId, startTime, endTime); + if (lineScheduleByUserId) { + log.error("划线排班不能请半天类型的假"); + throw new HandleException("划线排班不能请半天类型的假"); + } + } + ApplyParam applyParam = new ApplyParam(userId, startTime, endTime, AttendanceTypeEnum.LEAVE, leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeType(), unit); + // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间 + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam); + List dailyCopy = dailyRuleResultVos; + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + // 计算申请时长 + BigDecimal day = BigDecimal.ZERO; + dailyRuleResultVos = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList()); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + day = day.add(dailyRuleResultVo.getLeaveDays()); + } + + //先根据日期date分组 + DailyRuleResultVo min = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).min(Comparator.comparing(DailyRuleResultVo::getInPoint)).orElse(null); + if (null != min) { + leaveQueryDto.setStartTimeHit(min.getInPoint()); + } else { + leaveQueryDto.setStartTimeHit(startTime); + } + DailyRuleResultVo max = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).max(Comparator.comparing(DailyRuleResultVo::getOutPoint)).orElse(null); + if (null != max) { + leaveQueryDto.setEndTimeHit(max.getOutPoint()); + } else { + leaveQueryDto.setEndTimeHit(endTime); + } + + // 命中时长为0 表示未命中班次且需要特殊处理半天及天的请假时间 + if (BigDecimal.ZERO.compareTo(day) == 0 && !Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(),userLeaveDetail.getUnit())) { + leaveQueryDto.setStartTimeHit(DateDetail.getDayBeginByDay(applyParam.getStart())); + // endtime 取日期的23:59:59 + leaveQueryDto.setEndTimeHit(DateDetail.getDayEndByDay(applyParam.getEnd())); + } + // 校验加班时间是否重叠 + checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlot(userId, leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit()), "选择的时间段范围内有其他加班(待审核/审核已通过)"); + // 校验考勤组是否锁定 + // 通过开始/结束时间获取申请时长及设计班次 + // 校验请假时间是否重叠 2024年10月30日更改逻辑为只校验当前审批与已审核通过的审批时间是否重叠 + // 考勤1.8.3版本新增审核中判断2025年08月28日 + checkLeaveTimeOverlap(userId, leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), leaveQueryDto.getApplyId(),leaveQueryDto.getStartTimeType(), leaveQueryDto.getEndTimeType(), unit); + // 新增借调校验 + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + checkGroupClockForOa(userId, leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 处理划线排班 + day = getUnderline(dailyCopy, userId, leaveQueryDto.getStartTimeHit(), leaveQueryDto.getEndTimeHit(), day,userLeaveDetail.getUnit()); + // 如果在这儿day的值还是=0 则说明没有命中班次,则需要重新计算请假时长 + if (BigDecimal.ZERO.compareTo(day) == 0) { + // 请假时长公式: + // 小时、半天、全天只区分命中和未命中,命中了时长=命中时长,未命中班次 请假时长 = 结束-开始(区分小时1天限制,半天取一个半天0.5,天一天取1) + day = getDay(userLeaveDetail.getUnit(), leaveQueryDto, userId, day,null); + } + if (new BigDecimal(userLeaveDetail.getDuration()).compareTo(day) < 0) { + throw new HandleException("请假时长超出可请假的最大时长"); + } + return attendanceUserBalanceRecordService.leaveSimulationConsumption(userId, day, userLeaveDetail); + } + + + /** + * 校验考勤组是否锁定 + * + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void checkClock(String userId, Date startTime, Date endTime) throws HandleException { + + Set groupIds = getGroupIdsNew(userId, startTime, endTime); + if (groupIds.size() > 1) { + log.error("找到多个考勤组"); + throw new HandleException("申请时间不可以跨两个考勤组"); + } else if (groupIds.size() == 1) { + String monthDate = DateDetail.getDate2Str(startTime, DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate); + if (map.get(userId)) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + } + } + + /** + * 外出和出差专用 + * 疑似作废 + * @param userId 用户 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @throws HandleException 错误 + */ + private void checkGroupClock(String userId, Date startTime, Date endTime) throws HandleException { + GroupCheckVo groupBusinessAndGoOut = getGroupBusinessAndGoOut(userId, startTime, endTime); + if (groupBusinessAndGoOut.getGroupIds().size() > 1) { + log.error("找到多个考勤组"); + // 申请时间与借调时间(XX月XX日)冲突!请重新选择 + String dayStr = groupBusinessAndGoOut.getRepeatDay().stream().sorted().collect(Collectors.joining(",")); + throw new HandleException("申请时间与借调时间" + dayStr + "冲突!请重新选择"); + } else if (groupBusinessAndGoOut.getGroupIds().size() == 1) { + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(startTime, DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate); + if (map.get(userId)) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + } + } + + /** + * 校验借调与考勤组锁定 考勤1.8.3版本新增审核中判断 + * 2.0 借调划归人事 该公共方法只校验考勤组和封账 + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param result ActionResult + */ + private void checkGroupClockForOa(String userId, Date startTime, Date endTime, ActionResult result) { + + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(startTime, endTime, Collections.singletonList(userId), null); + if (null == attendanceGroupUsers || attendanceGroupUsers.isEmpty()) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("所选时间范围内未找到考勤组"); + return; + } + if (attendanceGroupUsers.stream().map(AttendanceGroupUser::getGroupId).distinct().count() > 1) { + log.info("找到多个考勤组"); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("申请时间与借调时间冲突!请重新选择"); + } else { + // 校验用户是否在该时间范围被其它考勤组借调(审批中) 2.0 借调划归人事 + FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO(); + ftbSecondMentQueryDTO.setUserIds(Collections.singletonList(userId)); + ftbSecondMentQueryDTO.setStartTime(startTime); + ftbSecondMentQueryDTO.setEndTime(endTime); + List secondmentRecordBath = ftbPersonneApi.getSecondmentRecordBath(ftbSecondMentQueryDTO); + if (CollectionUtil.isNotEmpty(secondmentRecordBath)) { + List uIds = Optional.ofNullable(secondmentRecordBath) + .orElse(Collections.emptyList()) + .stream() + .filter(Objects::nonNull) // 过滤掉null元素 + .map(FtbPersonnelsSecondmentVO::getUserId) + .filter(Objects::nonNull) // 再次过滤掉null的userId + .collect(Collectors.toList()); + if (!uIds.isEmpty()) { + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("用户:" + userList.getData().get(0).getUserName() + "已处于借调审批中,请勿重复操作!"); + } + } + } + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(startTime, DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate); + if (map.get(userId)) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(monthDate + "已封账,过去的考勤记录无法修改!"); + } + } + } + + + /** + * 校验时间段内用户有无外出 考勤1.8.3版本新增审核中判断 + * + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void checkGoOutForOa(String userId, Date startTime, Date endTime, ActionResult result, String taskId) { + Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getUserGoOutForOa(userId, startTime, endTime, taskId); + Integer goOutForDay = attendanceLeaveApproveMapper.getUserGoOutForDay(userId, startTime, endTime, taskId); + if ((null != userWorkByTimeSlot && userWorkByTimeSlot > 0) || (null != goOutForDay && goOutForDay > 0)) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("申请时间段内已有申请中或申请通过的外出申请,请重新选择!"); + } + } + + /** + * 校验时间段内用户有无出差 考勤1.8.3版本新增审核中判断 + * + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void checkBusinessTripForOa(String userId, Date startTime, Date endTime, ActionResult result, String taskId) { + Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getBusinessTripForOa(userId, startTime, endTime, taskId); + if (null != userWorkByTimeSlot && userWorkByTimeSlot > 0) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("申请时间段内已有申请中或申请通过的出差申请,请重新选择!"); + } + } + + + /** + * 获取用户时间段范围内的所有考勤组 + * 外出出差专用校验 + * + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + * 1.时间段完全在开始时间之前 + * 2.时间段包含开始时间但不包含结束时间 + * 3.时间段完全包含开始时间结束时间 + * 4.时间段完全在开始时间与结束时间之间 + * 5.时间段完全在开始时间之后包含结束时间 + * 6.时间段完全在结束时间之后 + */ + private GroupCheckVo getGroupBusinessAndGoOut(String userId, Date startTime, Date endTime) throws HandleException { + GroupCheckVo groupCheckVo = new GroupCheckVo(); + SimpleDateFormat sdfday = new SimpleDateFormat("yyyy-MM-dd"); + // 开始时间结束时间是否完整的借调 + List userGroupList = attendanceGroupUserMapper.getUserGroupList(userId); + if (null != userGroupList && !userGroupList.isEmpty()) { + List selfGroup = userGroupList.stream().filter(v -> 2 == v.getType()).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(selfGroup)) { + setMyGroup(userGroupList, groupCheckVo); + } + checkSelfGroupNew(selfGroup, startTime, endTime, sdfday, groupCheckVo); + if (groupCheckVo.isMyType()) { + setMyGroup(userGroupList, groupCheckVo); + } + if (CollectionUtil.isEmpty(groupCheckVo.getGroupIds()) || groupCheckVo.getGroupIds().isEmpty()) { + throw new HandleException("未找到你所属考勤组!"); + } + } + return groupCheckVo; + + } + + /** + * 我的考勤组 + * + * @param userGroupList 考勤组集合 + * @param groupCheckVo 用户考勤组信息 + */ + private static void setMyGroup(List userGroupList, GroupCheckVo groupCheckVo) { + AttendanceGroupUserVo myGroup = userGroupList.stream().filter(v -> 1 == v.getType()).findFirst().orElse(null); + if (myGroup != null) { + groupCheckVo.setMyGroupId(myGroup.getGroupId()); + groupCheckVo.getGroupIds().add(myGroup.getGroupId()); + } + } + + /** + * 校验借调考勤组 + * + * @param selfGroup 借调考勤组 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param sdfday 年月日格式 + * @param groupCheckVo 考勤组 + */ + private void checkSelfGroupNew(List selfGroup, Date startTime, Date endTime, SimpleDateFormat sdfday, GroupCheckVo groupCheckVo) { + for (AttendanceGroupUserVo attendanceGroupUserVo : selfGroup) { + List secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class); + if (secondmentDateVos.isEmpty()) { + groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId()); + // 借调 拼接时间集合 + List allDays = DateDetail.getAllDays(startTime, endTime); + allDays.forEach(v -> { + groupCheckVo.getRepeatDay().add(sdfday.format(v)); + }); + break; + } else { + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(dateVo.getStartTime(), startTime, endTime) + || DateDetail.checkTimeBetween(dateVo.getEndTime(), startTime, endTime)) { + groupCheckVo.getGroupIds().add(attendanceGroupUserVo.getGroupId()); + // 借调 拼接时间集合 + List allDays = DateDetail.getDateIntersection(startTime, endTime, dateVo.getStartTime(), dateVo.getEndTime()); + if (!allDays.isEmpty()) { + groupCheckVo.getRepeatDay().addAll(allDays); + } + } + if (DateDetail.checkTimeBetween(startTime, dateVo.getStartTime(), dateVo.getEndTime()) + && DateDetail.checkTimeBetween(endTime, dateVo.getStartTime(), dateVo.getEndTime())) { + groupCheckVo.setMyType(false); + } + + } + } + } + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void workApprove(String id, Integer status, String tenantId, String userId, String userName) throws ApproveException { + AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(id); + if (null == workOverTime) { + log.error("未找到对应的加班审批"); + throw new ApproveException("未找到对应的加班审批"); + } + LeaveShiftVo workOverTimeShifts = new LeaveShiftVo(); + if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) { + try { + // 查询加班明细 及保存 + BalanceQueryDto balanceQueryDto = new BalanceQueryDto(workOverTime.getUserId(), workOverTime.getStartTime().getTime() + "", workOverTime.getEndTime().getTime() + ""); + workOverTimeShifts = getWorkOverTimeShifts(balanceQueryDto); + // 生成班次 + attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(workOverTime.getUserId(), id, workOverTime.getStartTime(), workOverTime.getEndTime(), BigDecimal.ZERO, AttendanceTypeEnum.WORKOVERTIME)); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + } + + //修改审批状态 + attendanceLeaveApproveMapper.updateWorkOverTimeApprove(id, status, userId, userName, JSONUtil.toJsonStr(workOverTimeShifts)); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void selfApprove(String id, String departureTime, String backTime, Integer status, String userId, String userName, String tenantId) throws ApproveException, HandleException { + + //获取待审批的借调审批详情 + AttendanceSelfApproveVo selfApprove = attendanceLeaveApproveMapper.getSelfApprove(id); + if (null != selfApprove) { + if (status != 1) { + attendanceLeaveApproveMapper.updateSelfApprove(selfApprove.getId(), null, null, status, userId, userName); + } else { + //校验参数不能为空 + Date departure = StringUtils.isEmpty(departureTime) ? selfApprove.getStartTime() : new Date(Long.parseLong(departureTime)); + Date back = StringUtils.isEmpty(backTime) ? selfApprove.getEndTime() : new Date(Long.parseLong(backTime)); + // 通过时校验借调离岗回岗时间 测试提供 + // 离岗时间, 在借调开始时间之前 + // 回岗时间,在借调结束时间之后 + if (departure.after(selfApprove.getStartTime())) { + throw new ApproveException("离岗时间需在借调开始时间之前"); + } + if (back.before(selfApprove.getEndTime())) { + throw new ApproveException("回岗时间需在借调结束时间之后"); + } + List userIds = attendanceLeaveApproveMapper.getUserList(selfApprove.getId()); + //查出审批对应的用户 + if (null != userIds && !userIds.isEmpty()) { + // 修改用户进入考勤组 + selfApprove.setUserIds(userIds); + AttendanceSecondedDto bean = BeanUtil.toBean(selfApprove, AttendanceSecondedDto.class); + bean.setBackTime(back); + bean.setDepartureTime(departure); + selfApprove.setBackTime(back); + selfApprove.setDepartureTime(departure); + try { + attendanceGroupService.secondedStart(bean); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + //修改用户排班 + try { + attendanceDailyRuleService.secondmentDailyRuleHandle(userIds, selfApprove.getSelfGroupId(), selfApprove.getGroupId(), selfApprove.getStartTime(), selfApprove.getEndTime(), departure, back, tenantId); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + } + + //修改审批状态 保存离岗回岗时间数据 + attendanceLeaveApproveMapper.updateSelfApprove(selfApprove.getId(), departure, back, status, userId, userName); + // 发送Im消息 注意借调开始时间是小于当前时间点 ,小于的时候出发考勤组变动提示,否则定时通知 + log.error("准备立即开始提示被借调用户借调信息入参StartTime:{},now:{}", selfApprove.getStartTime().getTime(), new Date().getTime()); + if (null != selfApprove.getStartTime() && (selfApprove.getStartTime().before(new Date()))) { + log.error("准备立即开始提示被借调用户借调信息"); + secondedUserSendIm(selfApprove, tenantId); + } + } + } else { + log.error("未找到对应的借调审批"); + throw new ApproveException("未找到对应的借调审批"); + } + + } + + + private void secondedUserSendIm(AttendanceSelfApproveVo approveVo, String tenantId) { + DateDetail dateDetail = new DateDetail(); + //与您相关新的【借调】信息 + //借调开始时间:2024年5月16日 16:23 + //借调结束时间:2024年5月17日 09:00 + //借调考勤组:小露测试考勤组 + //被借调考勤组:小露验收考勤组 + List userIds = attendanceLeaveApproveMapper.getUserList(approveVo.getId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(userIds), tenantId); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(tenantId); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + approveImVo.setUserIds(userIds); + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.SECONDED.getCode()); + approveBaseImVo.setStartTime(approveVo.getStartTime()); + approveBaseImVo.setEndTime(approveVo.getEndTime()); + approveBaseImVo.setGroupName(attendanceGroupService.getById(approveVo.getGroupId()).getGroupName()); + approveBaseImVo.setSecondedGroupName(attendanceGroupService.getById(approveVo.getSelfGroupId()).getGroupName()); + approveBaseImVo.setUrl(AttendanceConstant.APPROVE_SECONDED_URL); + approveBaseImVo.setTitle("与您相关新的【借调】信息"); + //时长:1.23天 + //被借调人员:小露、张三、李四、王五 + //离岗时间:2024年5月16日 16:00 + //回岗时间:2024年5月17日 10:00 + StringBuilder userNames = new StringBuilder(); + for (int i = 0; i < allByIds.size(); i++) { + if (i == 0) { + userNames.append(allByIds.get(i).getRealName()); + } else { + userNames.append("、").append(allByIds.get(i).getRealName()); + } + } + approveBaseImVo.setSecondedUsersName(userNames.toString()); + approveBaseImVo.setBackTime(approveVo.getBackTime()); + approveBaseImVo.setDepartureTime(approveVo.getDepartureTime()); + approveBaseImVo.setDuration(dateDetail.calculateDaysDifference(approveVo.getStartTime(), approveVo.getEndTime()) + "天"); + approveImVo.setApproveBaseImVo(approveBaseImVo); + attendanceNoticeHandler.send(approveImVo); + } + + + @Override + public ActionResult getApprovalAdmin(String taskId, String type) { + try { + switch (type) { + // 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批 7.外出 8.出差 + case "1": + ApplyAttendanceRepair applyAttendanceRepair = applyAttendanceRepairMapper.selectById(taskId); + submitCheckRepair(JSONUtil.toJsonStr(applyAttendanceRepair), "apply"); + break; + case "2": + checkChange(taskId); + break; + case "3": + checkOutside(taskId); + break; + case "4": + checkLeave(taskId); + break; + case "5": +// checkWork(taskId); + break; + case "6": + checkSeconded(taskId); + break; + case "7": + checkOut(taskId); + break; + case "8": + checkBusiness(taskId); + break; + default: + throw new HandleException("请传入合法的Type"); + } + } catch (Exception e) { + log.error("考勤审批校验失败:{}", e.getMessage()); + return ActionResult.fail(e.getMessage()); + } + return ActionResult.success(); + } + + + /** + * 校验时间段内用户有无外出 + * + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void checkGoOut(String userId, Date startTime, Date endTime) throws HandleException { + Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getUserGoOutByTimeSlot(userId, startTime, endTime); + if (null != userWorkByTimeSlot && userWorkByTimeSlot > 0) { + throw new HandleException("申请时间段内已有申请中或申请通过的外出申请,请重新选择!"); + } + } + + /** + * 校验时间段内用户有无出差 + * + * @param userId 用户Id + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void checkBusinessTrip(String userId, Date startTime, Date endTime) throws HandleException { + Integer userWorkByTimeSlot = attendanceLeaveApproveMapper.getUserBusinessTripByTimeSlot(userId, startTime, endTime); + if (null != userWorkByTimeSlot && userWorkByTimeSlot > 0) { + throw new HandleException("申请时间段内已有申请中或申请通过的出差申请,请重新选择!"); + } + } + + /** + * 出差校验 + * + * @param taskId 任务Id + * @throws HandleException 异常 + */ + private void checkBusiness(String taskId) throws HandleException { + // 校验是否和借调冲突 + BusinessTripVo vo = attendanceLeaveApproveMapper.getBusinessTrip(taskId); + // 校验是否处于请假,加班,借调 + checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + // 考勤1.5.2 去掉请假及加班校验 + checkGoOut(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.BUSINESS_TRIP)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + + /** + * 外出校验 + * + * @param taskId 任务Id + * @throws HandleException 异常 + */ + private void checkOut(String taskId) throws HandleException { + // 校验是否和借调冲突 + GoOutVo vo = attendanceLeaveApproveMapper.getGoOut(taskId); + // 校验是否处于请假,加班,借调 + checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + // 考勤1.5.2 去掉请假及加班校验 + checkBusinessTrip(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.STEP_OUT)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + + /** + * 外勤校验 + * + * @param taskId 任务Id + * @throws HandleException 异常 + */ + private void checkOutside(String taskId) throws HandleException { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(FtbAttendanceClockIn::getApprovalCode, taskId) + .eq(FtbAttendanceClockIn::getApprovalStatus, ConstantUtil.PASS_APPROVAL); + FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectOne(queryWrapper); + if (null == clockIn) { + throw new HandleException("未找到打卡记录"); + } + LambdaQueryWrapper resultQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getClockInId, clockIn.getId()); + AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper); + if (null == dbResult) { + throw new HandleException("未找到打卡结果"); + } + checkLock(dbResult.getId()); + } + + /** + * 出勤变更校验 + * + * @param taskId 任务Id + * @throws HandleException 异常 + */ + private void checkChange(String taskId) throws HandleException { + + ApplyAttendanceChange applyAttendanceChange = applyAttendanceChangeMapper.selectById(taskId); + checkLock(applyAttendanceChange.getClockInResultId()); + } + + /** + * 校验考勤组是否锁定 + * + * @param clockInResultId 打卡Id + * @throws HandleException 异常 + */ + private void checkLock(String clockInResultId) throws HandleException { + + // 判断考勤组是否被锁定 + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId); + // 查询考勤规则 + AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId()); + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate); + if (map.get(clockInResult.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || attendanceClockInService.getOutsideCheck(rule, clockInResult.getClockInType())) { + throw new HandleException("操作失败,班次结果已变更!"); + } + } + + @Override + public ActionResult submitValidation(String body, String type) { + try { + switch (type) { + // 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.借调审批 7.外出 8.出差 + case "1": + submitCheckRepair(body, "submit"); + break; + case "2": + submitCheckChange(body); + break; + case "3": + log.info("外勤审批验证: {}", body); + break; + case "4": + submitCheckLeave(body); + break; + case "5": + submitCheckWork(body); + break; + case "6": + submitCheckSeconded(body); + break; + case "7": + submitCheckOut(body); + break; + case "8": + submitCheckBusiness(body); + break; + default: + throw new HandleException("请传入合法的Type"); + } + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + return ActionResult.success(); + } + + /** + * 出差提交校验 + * + * @param body 提交对象 + * @throws HandleException 异常 + */ + private void submitCheckBusiness(String body) throws HandleException { + BusinessTripVo vo = JsonUtil.getJsonToBean(body, BusinessTripVo.class); + // 校验是否处于请假,加班,借调 + checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + // 考勤1.5.2 去掉请假及加班校验 +// checkLeaveBYPeriod(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); +// checkWorkBYPeriod(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + // 校验是否与外出重叠 + checkGoOut(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.BUSINESS_TRIP)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + + /** + * 外出提交校验 + * + * @param body 提交对象 + * @throws HandleException 异常 + */ + private void submitCheckOut(String body) throws HandleException { + GoOutVo vo = JsonUtil.getJsonToBean(body, GoOutVo.class); + // 校验是否处于请假,加班,借调 + checkGroupClock(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + // 考勤1.5.2 去掉请假及加班校验 + // 校验是否与出差重叠 + checkBusinessTrip(vo.getUserId(), vo.getStartTime(), vo.getEndTime()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(vo.getUserId(), vo.getStartTime(), vo.getEndTime(), AttendanceTypeEnum.STEP_OUT)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + + /** + * 出勤变更提交校验 + * + * @param body 提交对象 + * @throws HandleException 异常 + */ + private void submitCheckChange(String body) throws Exception { + JSONObject json = new JSONObject(body); + Object clockInResultIdObj = json.get("clockInResultId"); + if (null == clockInResultIdObj) { + throw new HandleException("clockInResultId不能为空"); + } + String clockInResultId = clockInResultIdObj.toString(); + // 判断考勤组是否被锁定 + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId); + // 查询考勤规则 + AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId()); + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate); + if (map.get(clockInResult.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + // 判断当前出勤结果是否在进行别的审批 + int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId); + if (count > 0) { + throw new QueryException("当前记录正在审批中,请审批结束后再重新执行操作"); + } + json.clear(); + } + + private void submitCheckRepair(String body, String handleType) throws Exception { + + JSONObject json = new JSONObject(body); + Object clockInResultIdObj = json.get("clockInResultId"); + if (null == clockInResultIdObj) { + throw new HandleException("clockInResultId不能为空"); + } + String clockInResultId = clockInResultIdObj.toString(); + // 判断当前出勤结果是否在进行别的审批 + if (handleType.equals("submit")) { + int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId); + if (count > 0) { + throw new QueryException("当前记录正在审批中,请审批结束后再重新执行操作"); + } + } + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId); + // 查询考勤规则 + AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId()); + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate); + if (map.get(clockInResult.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + // 查询考勤组规则 + GroupRuleVo groupRule = attendanceClockInService.getGroupRule(rule.getGroupId(), clockInResult.getUserId()); + // 判断有无补卡次数 + MutablePair datePair = attendanceClockInService.couldRepairRecord(rule.getDay(), clockInResult, ConstantUtil.PASS_TRUE, groupRule); + // 判断打卡记录是否在时间范围内 + if (!DateDetail.checkTimeBetween(rule.getDay(), datePair.getLeft(), datePair.getRight())) { + throw new QueryException("当前打卡记录不在补卡时间范围内"); + } + json.clear(); + } + + /** + * 借调时校验(考勤组是否被删除,用户是否还在该考勤组) + * + * @param groupId 借调考勤组id + * @param selfGroupId 被借调考勤组id + * @param userIds 被借调用户ids + */ + private void check(String groupId, String selfGroupId, List userIds) throws HandleException { + + /* 校验借调组是否已解散*/ + AttendanceGroup group = attendanceGroupMapper.selectById(groupId); + if (group.getDeleteMark() == 1) { + throw new HandleException("考勤组或借调组已不存在!"); + } + /* 校验被借调考勤组是否已解散*/ + AttendanceGroup selfGroup = attendanceGroupMapper.selectById(selfGroupId); + if (selfGroup.getDeleteMark() == 1) { + throw new HandleException("考勤组或借调组已不存在!"); + } + + /* 校验借调过程中借调人员是否离开借调考勤组*/ + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .eq(AttendanceGroupUser::getGroupId, selfGroupId) + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + + if (CollectionUtil.isEmpty(groupUserList)) { + throw new HandleException("被借调考勤组下无成员或已被全部移除该考勤组!"); + } + List dbUserIds = groupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + + List differenceUserIds = userIds.stream().filter(userId -> { + return !dbUserIds.contains(userId); + }).collect(Collectors.toList()); + + if (CollectionUtil.isNotEmpty(differenceUserIds)) { + if (CollectionUtil.size(differenceUserIds) > 5) { + differenceUserIds = differenceUserIds.subList(0, 4); + + ActionResult> userList = v2UserApi.getAllUserInfoBatch(differenceUserIds, null); + List userNameList = new ArrayList<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userNameList = userList.getData().stream().map(UserBoundVO::getUserName).collect(Collectors.toList()); + } + String join = CollectionUtil.join(userNameList, ",") + "..."; + throw new HandleException(join + "已离开被借调考勤组!"); + } + + ActionResult> userList = v2UserApi.getAllUserInfoBatch(differenceUserIds, null); + List userNameList = new ArrayList<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userNameList = userList.getData().stream().map(UserBoundVO::getUserName).collect(Collectors.toList()); + } + + String join = CollectionUtil.join(userNameList, ","); + throw new HandleException(join + "已离开被借调考勤组!"); + } + } + + /** + * 加班提交校验 + * + * @param body 提交对象 + * @throws HandleException 异常 + */ + private void submitCheckWork(String body) throws HandleException { + AttendanceWorkOverTimeVo workOverTime = JsonUtil.getJsonToBean(body, AttendanceWorkOverTimeVo.class); + if (Math.abs(workOverTime.getEndTime().getTime() - workOverTime.getStartTime().getTime()) > 24 * 60 * 60 * 1000) { + log.error("加班时间不许超过24小时"); + throw new HandleException("加班时间不许超过24小时"); + } + // 时间范围内考勤组是否锁定 + checkClock(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()); + // 校验加班时间是否重叠 + checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlot(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的时间段范围内有其他加班(待审核/审核已通过)"); + //校验 加班提交期间被排了班 + checkWorkTimeOverlap(attendanceDailyRuleMapper.getUserShift(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的加班时间段范围内有其他排班"); + checkMoreGroup(workOverTime); + // 校验请假时间是否重叠 + checkLeaveTimeOverlap(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), null,null,null,1); + + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), AttendanceTypeEnum.WORKOVERTIME)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + + /** + * 加班提交校验 + * + * @param workOverTime 提交对象 + * @throws HandleException 异常 + */ + @Override + public void submitCheckWorkForOa(AttendanceWorkOvertimeApproveDto workOverTime) throws HandleException { + if (Math.abs(workOverTime.getEndTime().getTime() - workOverTime.getStartTime().getTime()) > 24 * 60 * 60 * 1000) { + log.error("加班时间不许超过24小时"); + throw new HandleException("加班时间不许超过24小时"); + } + // 时间范围内考勤组是否锁定 + // 校验加班时间是否重叠 + checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlotForOa(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), workOverTime.getTaskId()), "选择的时间段范围内有其他申请中或已通过的加班"); + //校验 加班提交期间被排了班 + checkWorkTimeOverlap(attendanceDailyRuleMapper.getUserShift(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的加班时间段范围内有其他排班"); + checkMoreGroup(workOverTime); + // 校验请假时间是否重叠 +// checkLeaveTimeOverlap(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), null); + + // 新增借调校验 + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + checkGroupClockForOa(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new HandleException(result.getMsg()); + } + + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), AttendanceTypeEnum.WORKOVERTIME)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + + private void checkMoreGroup(AttendanceWorkOverTimeVo workOverTime) throws HandleException { + //校验考勤组是否存在 + Set groupIds = getGroupIdsNew(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()); + + if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) { + throw new HandleException("加班时间不可以跨两个考勤组"); + } + } + + private void checkMoreGroup(AttendanceWorkOvertimeApproveDto workOverTime) throws HandleException { + //校验考勤组是否存在 + Set groupIds = getGroupIdsNew(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()); + + if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) { + throw new HandleException("加班时间不可以跨两个考勤组"); + } + } + + private void checkWorkTimeOverlap(Integer attendanceLeaveApproveMapper, String s) throws HandleException { + if (null != attendanceLeaveApproveMapper && attendanceLeaveApproveMapper > 0) { + log.error(s); + throw new HandleException(s); + } + } + + /** + * 校验请假时间是否重叠 过期( 2024年10月30日更改逻辑为只校验当前审批与已审核通过的审批时间是否重叠) + * 考勤1.8.3版本新增审核中判断2025年08月28日 + * 2026年03月09日 考勤2.1.1 优化逻辑 + * @param userId 用户Id + * @param startTime 本次请假命中的开始时间 + * @param endTime 本次请假命中的结束时间 + * @param applyId 申请Id + * @param startTimeType 当请假类型为天时开始时间选择的班次信息 同ftb_attendance_shift_setting_period表F_Type类型:1 上半天 2 下半天 (半天制班次使用)当请假类型单位为半天时生效 + * @param endTimeType 当请假类型为天时开始时间选择的班次信息 同ftb_attendance_shift_setting_period表F_Type类型:1 上半天 2 下半天 (半天制班次使用)当请假类型单位为半天时生效 + * @param unit 单位 : 1 :小时 2: 天 3.半天 + */ + private void checkLeaveTimeOverlap(String userId, Date startTime, Date endTime, String applyId,Integer startTimeType,Integer endTimeType,Integer unit) throws HandleException { + // 校验是否与请假时间重叠 + List userLeaveByTimeSlot = attendanceLeaveApproveMapper.getUserLeaveByTimeSlot(userId, startTime, endTime); + if (null == userLeaveByTimeSlot || userLeaveByTimeSlot.isEmpty()) { + return; + } + userLeaveByTimeSlot = userLeaveByTimeSlot.stream().filter(x -> !x.getId().equals(applyId)).collect(Collectors.toList()); + if ( userLeaveByTimeSlot.isEmpty()) { + return; + } + // 同一个日期范围内只能提交同一种类型的请假 + if (userLeaveByTimeSlot.stream().anyMatch(x -> !x.getUnit().equals(unit))) { + log.error("选择的时间段范围内有其他请假"); + throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假"); + } + // 已有请假的类型和本次请假类型相同,全天假不能请; + if (Objects.equals(BalanceEnum.BALANCE_UNIT_T.getCode(), unit)) { + log.error("选择的时间段范围内有其他请假"); + throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假"); + } + // 已有请假的类型和本次请假类型相同,小时类型的假不能请重复 + if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) { + List list = userLeaveByTimeSlot.stream().filter(x -> x.getUnit().equals(BalanceEnum.BALANCE_UNIT_XH.getCode())).collect(Collectors.toList()); + // 校验小时范围有没有交叉 + if (list.stream().anyMatch(x -> { + return x.getStartTime().before(endTime) && x.getEndTime().after(startTime); + })) { + log.error("选择的时间段范围内有其他请假"); + throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假"); + } + } else { + List list = userLeaveByTimeSlot.stream().filter(x -> x.getUnit().equals(BalanceEnum.BALANCE_UNIT_BT.getCode())).collect(Collectors.toList()); + // 半天校验 + long startNowNum = Long.parseLong(DateDetail.getDate2Str(startTime, DateDetail.DF6) + startTimeType); + long endNowNum = Long.parseLong(DateDetail.getDate2Str(endTime, DateDetail.DF6) + endTimeType); + if (list.stream().anyMatch(x -> { + long startNum = Long.parseLong(DateDetail.getDate2Str(x.getStartTime(), DateDetail.DF6) + x.getStartTimeType()); + long endNum = Long.parseLong(DateDetail.getDate2Str(x.getEndTime(), DateDetail.DF6) + x.getEndTimeType()); + return startNum <= endNowNum && endNum >= startNowNum; + })) { + log.error("选择的时间段范围内有其他请假"); + throw new HandleException("选择的时间段范围内有其他申请中或已通过的请假"); + } + } + } + + /** + * 请假提交校验 + * + * @param body 提交对象 + * @throws HandleException 异常 + */ + private void submitCheckLeave(String body) throws HandleException, ApproveException, ParseException { + log.error("请假提交校验: {}", body); + LeaveQueryDto leaveQueryDto = JsonUtil.getJsonToBean(body, LeaveQueryDto.class); + leaveQueryDto.setApplyId(null); + leaveQueryDto.setLeaveTypeId(leaveQueryDto.getTypeId()); + // 2.0已重构 + getResidueBalance(leaveQueryDto); + } + + /** + * 获取考勤组对应权限的审批管理员 + * + * @param type 类型 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param clockInResultId 打卡Id + * @param selfGroupId 借调考勤组 + * @throws HandleException 异常 + */ + @Override + public AttendanceApprovalAdminVo getGroupAdminInfo(Integer type, Date startTime, Date endTime, String clockInResultId, String selfGroupId) throws HandleException { + String loginUserId = UserProvider.getLoginUserId(); + if (ApprovalSettingTypeEnum.ROUTINE.getCode().equals(type)) { + /* 补卡审批*/ + String curGroupId = groupIdByClockInResultId(clockInResultId); + return getApproveUser(curGroupId, type); + } + if (ApprovalSettingTypeEnum.ACTION_RESULT.getCode().equals(type)) { + /* 调整出勤结果*/ + String curGroupId = groupIdByClockInResultId(clockInResultId); + return getApproveUser(curGroupId, type); + } + if (ApprovalSettingTypeEnum.OUT.getCode().equals(type)) { + String curGroupId = getGroupIdByUserId(loginUserId); + return getApproveUser(curGroupId, type); + } + if (ApprovalSettingTypeEnum.LEAVE.getCode().equals(type)) { + /* 请假审批*/ + Set groupIds = getGroupIdsNew(loginUserId, startTime, endTime); + if (groupIds.size() > 1) { + throw new HandleException("请假审批不可以跨两个考勤组"); + } + if (CollectionUtil.isEmpty(groupIds)) { + throw new HandleException("未找到你所属考勤组!"); + } + String nextGroupId = groupIds.iterator().next(); + return getApproveUser(nextGroupId, type); + } + if (ApprovalSettingTypeEnum.OVERTIME.getCode().equals(type)) { + /* 加班审批*/ + Set groupIds = getGroupIdsNew(loginUserId, startTime, endTime); + if (groupIds.size() > 1) { + throw new HandleException("请假审批不可以跨两个考勤组"); + } + if (CollectionUtil.isEmpty(groupIds)) { + throw new HandleException("未找到你所属考勤组!"); + } + String nextGroupId = groupIds.iterator().next(); + return getApproveUser(nextGroupId, type); + } + if (ApprovalSettingTypeEnum.GO_OUT.getCode().equals(type)) { + /* 外出审批*/ + String groupId = getGroupIdByUserId(loginUserId); + if (null == groupId) { + throw new HandleException("没找到考勤组"); + } + return getApproveUser(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()); + } + if (ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode().equals(type)) { + /* 出差审批*/ + String groupId = getGroupIdByUserId(loginUserId); + if (null == groupId) { + throw new HandleException("没找到考勤组"); + } + return getApproveUser(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()); + } + /* 借调审批*/ + AttendanceApprovalAdminVo approveUser = new AttendanceApprovalAdminVo(); + List userIds = getSecondAdmins(selfGroupId); + List userVoList = toUserVoInfo(userIds); + approveUser.setIsApproval(true); + approveUser.setUserList(userVoList); + return approveUser; + } + + @Override + public List getBalanceDetailS(String groupId, String month) { + Date start = DateDetail.getMonthBeginDate(month); + Date end = DateDetail.getMonthEndDate(month); + List attendanceGroupUserVos = attendanceGroupUserService.queryByUsersAndGroup(start, end, null, CollUtil.newArrayList(groupId)); + if (null != attendanceGroupUserVos && !attendanceGroupUserVos.isEmpty()) { + attendanceGroupUserVos = attendanceGroupUserVos.stream().filter(v -> 1 != v.getDeleteMark()).collect(Collectors.toList()); + List userIds = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (userIds.isEmpty()) { + return new ArrayList<>(); + } + return getUsersBalance(userIds); + } + return new ArrayList<>(); + } + + @Override + public List getUsersBalance(List userIds) { + List list = new ArrayList<>(); + // userIds 去重 + userIds = userIds.stream().distinct().collect(Collectors.toList()); + List userBalanceVo = attendanceBalanceRecordMapper.getBalanceByUserIds(userIds); + + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, null); + Map map = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + + for (Map.Entry entry : map.entrySet()) { + //赋值用户名称 + UserBalanceVo userBalanceVo1 = new UserBalanceVo(); + userBalanceVo1.setUserId(entry.getKey()); + userBalanceVo1.setUserName(entry.getValue().getUserName()); + list.add(userBalanceVo1); + } + Map listMap = list.stream().collect(Collectors.toMap(UserBalanceVo::getUserId, Function.identity())); + userBalanceVo.forEach(v -> { + UserBalanceVo balanceVo = listMap.get(v.getUserId()); + if (null != balanceVo) { + // 赋值余额 + balanceVo.setBalance(v.getBalance()); + balanceVo.setPaidBalance(v.getPaidBalance()); + balanceVo.setUnpaidBalance(v.getUnpaidBalance()); + balanceVo.setAdventBalance(v.getAdventBalance()); + } + }); + return list; + } + + + @Override + public AttendanceApprovalAdminVo getGoOutApproval() { + String loginUserId = UserProvider.getLoginUserId(); + String groupId = getGroupIdByUserId(loginUserId); + return getApproveUser(groupId, ApprovalSettingTypeEnum.GO_OUT.getCode()); + } + + @Override + public AttendanceApprovalAdminVo getBusinessTripApproval() { + String loginUserId = UserProvider.getLoginUserId(); + String groupId = getGroupIdByUserId(loginUserId); + return getApproveUser(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()); + } + + @Override + public Boolean storageRest(String tenantId) { + // 每月定时生成用户存休记录 + // 获取上个月的年月 + SimpleDateFormat Y_M = new SimpleDateFormat("yyyy-MM"); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.MONTH, -1); + // 获取上个月的年月 + String lastMonthDate = Y_M.format(calendar.getTime()); + // 预防重复调用产生脏数据 + attendanceBalanceRecordMapper.deleteByYearMonth(lastMonthDate); + // 找到现在还有劵的用户 2.0 改为查询存休余额 + List list = attendanceBalanceRecordMapper.getStraightBalanceList(); + if (null != list && !list.isEmpty()) { + list.forEach(v -> { + v.setId(FtbUtil.getId()); + v.setYearMonth(lastMonthDate); + }); + // 批量保存存休信息 + storageRestMapper.saveBatch(list); + } + return true; + } + + @Override + public void sendIm(WorkflowImQueryDto workflowImQueryDto) { + // 查询下一节点审批人 + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(workflowImQueryDto.getTaskId(), workflowImQueryDto.getTaskNodeId(), workflowImQueryDto.getTenantId(), null); + if (null == data) { + return; + } + AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + jsonToBean.setId(workflowImQueryDto.getTaskId()); + try { + switch (workflowImQueryDto.getType()) { + // 审批类型 0.借调 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.外出 7.出差 + case "0": + secondedSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean); + break; + case "1": + // 审批发送给下一节点 + sendMessageToIm(data.getUserOperators(), jsonToBean, ConstantUtil.APPROVE_NOTICE_REPAIR); + break; + case "2": + sendMessageToIm(data.getUserOperators(), jsonToBean, ConstantUtil.APPROVE_NOTICE_CHANGE); + break; + case "3": + sendMessageToIm(data.getUserOperators(), jsonToBean, ConstantUtil.APPROVE_NOTICE_OUTSIDE); + break; + case "4": + leaveSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean); + break; + case "5": + workSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean); + break; + case "6": + goOutSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean); + break; + case "7": + businessSendIm(workflowImQueryDto, data.getUserOperators(), jsonToBean); + break; + default: + throw new HandleException("请传入合法的Type"); + } + } catch (Exception e) { + e.printStackTrace(); + ActionResult.fail(e.getMessage()); + return; + } + ActionResult.success(); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public ActionResult createBusinessTrip(AttendanceBusinessTripApproveOaDto approveOaDto) { + + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + // 处理收到的数组用户 + // 字符串专数组 + String userId = JSONUtil.toList(approveOaDto.getUserId(), String.class).get(0); + if (StringUtils.isEmpty(userId)) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("未找到以用户"); + return result; + } + approveOaDto.setUserId(userId); + // 计算时间差 出差只有天 + // 计算两个时间之间的日期差 + approveOaDto.setDayNum(new BigDecimal(FtbUtil.dateDiff(approveOaDto.getStartTime(), approveOaDto.getEndTime()) + 1)); + log.error("FoeOa处理后的出差参数................: {}", approveOaDto); + // 校验出差开始与结束时间与已通过的借调时间重叠 + checkGroupClockForOa(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + // 出差开始与结束时间与已通过的外出时间重叠 考勤1.8.3版本新增审核中判断 + checkGoOutForOa(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), result, approveOaDto.getTaskId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + // 出差开始与结束时间与已通过的出差时间重叠 考勤1.8.3版本新增审核中判断 + checkBusinessTripForOa(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), result, approveOaDto.getTaskId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + try { + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(approveOaDto.getUserId(), approveOaDto.getStartTime(), approveOaDto.getEndTime(), AttendanceTypeEnum.BUSINESS_TRIP)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(dailyRuleResultVo.getFailMsg()); + break; + } + } + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + } catch (HandleException e) { + throw new RuntimeException(e); + } + approveOaDto.setId(approveOaDto.getTaskId()); + businessTripApproveService.saveForOa(approveOaDto); + return result; + } + + @Override + public ActionResult createGoOutForOa(GoOutApproveForOaDto dto) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + // 处理时长 + String[] time = dto.getTime(); + Date start = new Date(Long.parseLong(time[0])); + Date end = new Date(Long.parseLong(time[1])); + if (dto.getUnit().equals(LeaveUnitEnum.DAY.getCode())) { + int i = FtbUtil.dateDiff(start, end) + 1; + dto.setDayNum(new BigDecimal(i)); + } else { + dto.setDayNum(FtbUtil.getTimeDifference(start, end)); + } + log.error("createGoOutForOa处理完后的参数................: {}", dto); + // 校验是否和借调冲突 +// GoOutVo vo = attendanceLeaveApproveMapper.getGoOut(taskId); + String userId = JSONUtil.toList(dto.getUserId(), String.class).get(0); + if (StringUtils.isEmpty(userId)) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getMsg()); + return result; + } + dto.setUserId(userId); + + // 校验出差开始与结束时间与已通过的借调时间重叠 + checkGroupClockForOa(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + // 出差开始与结束时间与已通过的外出时间重叠 + checkGoOutForOa(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), result, dto.getTaskId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + // 出差开始与结束时间与已通过的出差时间重叠 + checkBusinessTripForOa(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), result, dto.getTaskId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + List dailyRuleResultVos = null; + try { + dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(dto.getUserId(), dto.getStartTime(), dto.getEndTime(), AttendanceTypeEnum.STEP_OUT)); + } catch (HandleException e) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("内部异常" + e.getMessage()); + return result; + } + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(dailyRuleResultVo.getFailMsg()); + break; + } + } + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + return result; + } + dto.setId(dto.getTaskId()); + goOutApproveService.saveForOa(dto); + return result; + } + + @Override + public ActionResult getDurationForOa(DurationForOaDto dto) { + String[] time = dto.getTime(); + Date start = new Date(Long.parseLong(time[0])); + Date end = new Date(Long.parseLong(time[1])); + if (dto.getUnit().equals(LeaveUnitEnum.DAY.getCode())) { + int i = FtbUtil.dateDiff(start, end) + 1; + return ActionResult.success(String.valueOf(i)); + } + BigDecimal timeDifference = FtbUtil.getTimeDifference(start, end); + return ActionResult.success(timeDifference.toString()); + } + + @Override + public ActionResult createLeaveForOa(LeaveApproveForOaDto dto) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + LeaveQueryDto leaveQueryDto = new LeaveQueryDto(); + leaveQueryDto.setLeaveTypeId(dto.getTypeId()); + leaveQueryDto.setStartTime(String.valueOf(dto.getStartTime().getTime())); + leaveQueryDto.setEndTime(String.valueOf(dto.getEndTime().getTime())); + leaveQueryDto.setBalanceStatus(dto.getBalanceStatus()); + leaveQueryDto.setPaid(dto.getPaid()); + leaveQueryDto.setNoPaid(dto.getNoPaid()); + leaveQueryDto.setUnit(dto.getUnit()); + leaveQueryDto.setTypeId(dto.getTypeId()); + leaveQueryDto.setStartTimeType(dto.getStartTimeType()); + leaveQueryDto.setEndTimeType(dto.getEndTimeType()); + try { + log.error("createLeaveForOa:{}", leaveQueryDto); + // 校验请假数据 2.0已重构 + LeaveConsumptionDetailVo residueBalance = getResidueBalance(leaveQueryDto); + dto.setBalanceJsonNew(JSONUtil.toJsonStr(residueBalance)); + } catch (Exception e) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg(e.getMessage()); + return result; + } + dto.setId(dto.getTaskId()); + dto.setStartTimeHit(leaveQueryDto.getStartTimeHit()); + dto.setEndTimeHit(leaveQueryDto.getEndTimeHit()); + + leaveApproveService.saveForOa(dto); + return result; + } + + @Override + public ActionResult getUserGroupByTime(GroupOaDto groupOaDto) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + List list = new ArrayList<>(); + // 获取用户所选时间段所在考勤组 + List userList = new ArrayList<>(); + userList.add(groupOaDto.getUserIdStr()); + //查询当前时间所属考勤组 + //查询请假时间内所有考勤组 + List attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(groupOaDto.getStartTime(), groupOaDto.getEndTime() , userList, null); + if (attendanceGroupUsers.isEmpty()) { + return ActionResult.fail("当前用户暂无考勤组"); + } + //如果考勤组数量大于1,判断是否包含当前时间所属考勤组,包含则取当前所属考勤组,不包含直接报错 + Assert.isFalse(attendanceGroupUsers.stream().map(AttendanceGroupUser::getGroupId).distinct().count() > 1, "当前申请时段存在多个考勤组"); + if (StringUtils.isEmpty(attendanceGroupUsers.get(0).getGroupId())) { + return ActionResult.fail("该用户暂无考勤组"); + } + // 通过考勤组id获取考勤组信息 当只有一个考勤组的时候才返回数据 + AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(attendanceGroupUsers.get(0).getGroupId()); + GroupMiniVo groupMiniVo = new GroupMiniVo(); + groupMiniVo.setGroupId(groupDetail.getId()); + OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(null, groupDetail.getOrgId()).getData(); + Assert.isFalse(Objects.isNull(infoById), "组织不存在"); + groupMiniVo.setGroupName(infoById.getName()); + list.add(groupMiniVo); + return ActionResult.success(list); + } + + @Override + public ActionResult getNowGroup() { + List list = new ArrayList<>(); + // 查询用户现在所在考勤组 + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(new Date(), new Date(), Collections.singletonList(UserProvider.getLoginUserId()), null); + if (null == attendanceGroupUsers || attendanceGroupUsers.isEmpty()) { + return ActionResult.success(list); + } + AttendanceGroupUser groupUser = attendanceGroupUsers.get(0); + if (StringUtils.isEmpty(groupUser.getGroupId())) { + return ActionResult.fail("该用户暂无考勤组"); + } + // 通过考勤组id获取考勤组信息 当只有一个考勤组的时候才返回数据 + AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(groupUser.getGroupId()); + GroupMiniVo groupMiniVo = new GroupMiniVo(); + groupMiniVo.setGroupId(groupDetail.getId()); + OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(null, groupDetail.getOrgId()).getData(); + Assert.isFalse(Objects.isNull(infoById), "组织不存在"); + groupMiniVo.setGroupName(infoById.getName()); + list.add(groupMiniVo); + return ActionResult.success(list); + } + + @Override + public AttendanceToThousandsFacesVo attendanceToThousandsFaces() { + AttendanceToThousandsFacesVo attendanceToThousandsFacesVo = new AttendanceToThousandsFacesVo(); + int totalUser = 0; + int schedulingUser = 0; + String userId = userProvider.get().getUserId(); + // 统计打卡人数 + // 打卡结果表单日有一次正常的打卡记录,记为1天(迟到、早退、正常)跨天班次一次排班天算1天 + Integer clockInDayNum = attendanceClockInResultMapper.getClockInDayNum(userId); + attendanceToThousandsFacesVo.setClockInDayNum(null == clockInDayNum ? 0 : clockInDayNum); + // 统计排班人数 + // 今日所有具有管理权限的考勤组已排班人数(含今日排休人数,不含今日固定排班)/所有具有权限的考勤组总人数(含今日排休人数,不含今日固定排班) + // 获取进入具有本组排班及子考勤组的管理权限的考勤组 考勤组ids集合 + List noFixedGroupIds = attendanceGroupService.getGroupIdsByManagePermission(userId, true); + if (null != noFixedGroupIds && !noFixedGroupIds.isEmpty()) { + // 通过考勤组ids集合获取非固定排班总人数(包含当天借调过来的人数) + List noFixedGroupUserIds = attendanceGroupUserService.getGroupUserIds(noFixedGroupIds); + if (null != noFixedGroupUserIds && !noFixedGroupUserIds.isEmpty()) { + totalUser = noFixedGroupUserIds.size(); + // 考勤组ids集合获取已排班的人数 + Integer groupUserNum = attendanceDailyRuleMapper.getGroupUserNum(noFixedGroupUserIds, noFixedGroupIds); + schedulingUser = null == groupUserNum ? 0 : groupUserNum; + } + } + attendanceToThousandsFacesVo.setSchedulingProportion(schedulingUser + "/" + totalUser); + Date dayBegin = DateUtil.getDayBegin(); + Date date = DateUtil.dateAddDays(dayBegin, 1); + List yesterdayRule = attendanceDailyRuleMapper.getYesterdayRule(UserProvider.getLoginUserId(), dayBegin, date); + attendanceToThousandsFacesVo.setYesterdayRuleList(yesterdayRule); + return attendanceToThousandsFacesVo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void leaveCompensation(String applyId, String userId,String day, BigDecimal balance) throws ApproveException { + +// // 定义常量 +// final int MAX_RETRIES = 30; +// final long LOCK_WAIT_TIME = 10; // 秒 +// final long LOCK_LEASE_TIME = 60; // 秒 +// final long RETRY_INTERVAL = 1000; // 毫秒 +// String executingDayKey = "attendance:leave:compensation:executing:" + applyId; +// String lockKey = "attendance:leave:compensation:lock:" + applyId; +// RLock lock = redisson.getLock(lockKey); +// // 使用分布式锁确保同一申请的同一天只被处理一次 +// int retryCount = 0; +// while (retryCount < MAX_RETRIES) { +// try { +// // 尝试获取分布式锁 +// if (lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS)) { +// try { +// // 在获取锁后再次检查当前正在执行的day +// String currentExecutingDay = (String) redisTemplate.opsForValue().get(executingDayKey); +// if (currentExecutingDay != null && !currentExecutingDay.equals(day)) { +// // 如果正在执行的day和传入的day不同,等待之前的执行完成 +// log.info("当前申请正在处理day: {}, 传入day: {}, 需要等待上一个执行完成", currentExecutingDay, day); +// // 继续循环重试 +// } else if (currentExecutingDay != null && currentExecutingDay.equals(day)) { +// // 如果正在执行的day和传入的day相同,打印日志并退出 +// log.warn("当前申请正在处理中,请勿重复操作 - applyId: {}, userId: {}, day: {}, balance: {}", +// applyId, userId, day, balance); +// return; +// } else { +// // 没有正在执行的记录,或者当前执行的day与传入day相同但需要继续处理 +// // 设置当前执行的day标记 +// redisTemplate.opsForValue().set(executingDayKey, day, LOCK_LEASE_TIME, TimeUnit.SECONDS); +// break; // 可以继续执行业务逻辑 +// } +// } finally { +// // 释放锁 +// if (lock.isHeldByCurrentThread()) { +// lock.unlock(); +// } +// } +// } +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// throw new ApproveException("线程被中断,操作失败"); +// } +// +// try { +// Thread.sleep(RETRY_INTERVAL); // 等待后重试 +// retryCount++; +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// throw new ApproveException("线程被中断,操作失败"); +// } +// } +// +// if (retryCount >= MAX_RETRIES) { +// throw new ApproveException("等待上一个申请执行超时,请稍后重试"); +// } +// +// // 执行业务逻辑 +// try { +// AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(applyId); +// // 特殊情况-请假类型单位使用创建审批时的历史纪录,其他配置获取实时更新的请假类型信息 +// AttendanceLeaveRulesVo userLeaveDetail = attendanceLeaveTypeService.getUserLeaveDetail(attendanceLeaveApproveVo.getTypeId(), attendanceLeaveApproveVo.getUserId()); +// LeaveConsumptionDetailVo leaveConsumptionDetailVo = attendanceUserBalanceRecordService.leaveConsumption(attendanceLeaveApproveVo.getUserId(), balance, userLeaveDetail, attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), attendanceLeaveApproveVo.getId()); +// +// // 处理完成后清理执行标记 +// redisTemplate.delete(executingDayKey); +// } catch (Exception e) { +// // 发生异常时也要清理执行标记 +// redisTemplate.delete(executingDayKey); +// if (e instanceof ApproveException) { +// throw e; +// } else { +// throw new ApproveException("请假补偿处理失败: " + e.getMessage()); +// } +// } finally { +// // 确保锁被释放 +// if (lock.isHeldByCurrentThread()) { +// lock.unlock(); +// } +// } + + } + + private void sendMessageToIm(List operatorList, AttendanceApproveImVo approveIm, String key) { + + key += approveIm.getId(); + if (redisUtil.exists(key)) { + Object value = redisUtil.getString(key); + ApproveImVo approveImVo = JsonUtil.getJsonToBean(value, ApproveImVo.class); + sendIm(operatorList, approveIm, approveImVo.getApproveBaseImVo(), approveImVo); + } + } + + private void secondedSendIm(WorkflowImQueryDto workflowImQueryDto, List userOperators, AttendanceApproveImVo jsonToBean) { + //张三的【借调】审批 + //借调开始时间:2024年5月16日 16:23 + //借调结束时间:2024年5月17日 09:00 + //借调考勤组:小露测试考勤组 + //被借调考勤组:小露验收考勤组 + AttendanceSelfApproveVo selfApprove = attendanceLeaveApproveMapper.getSelfApprove(workflowImQueryDto.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(selfApprove.getCreatorUserId()), workflowImQueryDto.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(workflowImQueryDto.getTenantId()); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.SECONDED.getCode()); + approveBaseImVo.setStartTime(selfApprove.getStartTime()); + approveBaseImVo.setEndTime(selfApprove.getEndTime()); + approveBaseImVo.setGroupName(selfApprove.getGroupName()); + approveBaseImVo.setSecondedGroupName(selfApprove.getSelfGroupName()); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【借调】审批").toString()); + sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo); + } + + private void businessSendIm(WorkflowImQueryDto workflowImQueryDto, List userOperators, AttendanceApproveImVo jsonToBean) { + // 张三的【出差】审批 + // 出发地:地点名称地点名称地点名称地点名称 + // 目的地:地点名称地点名称地点名称地点名称 + BusinessTripVo vo = attendanceLeaveApproveMapper.getBusinessTrip(workflowImQueryDto.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(vo.getUserId()), workflowImQueryDto.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(workflowImQueryDto.getTenantId()); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()); + approveBaseImVo.setDestination(vo.getDestination()); + approveBaseImVo.setDeparture(vo.getDeparture()); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【出差】审批").toString()); + sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo); + } + + private void sendIm(List userOperators, AttendanceApproveImVo jsonToBean, ApproveBaseImVo approveBaseImVo, ApproveImVo approveImVo) { + userOperators.forEach(v -> { + jsonToBean.setTaskId(v.getTaskOperatorId()); + // URL 拼接 +// String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean)); + String encode = FtbUtil.encodeJson(jsonToBean); + String url = AttendanceConstant.APPROVE_URL + encode; + approveBaseImVo.setUrl(url); + approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId())); + approveImVo.setApproveBaseImVo(approveBaseImVo); + // 轮询用户发送消息 因每一个用户跳转的信息都不同 + attendanceNoticeHandler.send(approveImVo); + }); + } + + private void goOutSendIm(WorkflowImQueryDto workflowImQueryDto, List userOperators, AttendanceApproveImVo jsonToBean) { + //张三的【外出】审批 + //开始时间:2024年5月16日 + //结束时间:2024年5月17日 + //时长:2天 + GoOutVo vo = attendanceLeaveApproveMapper.getGoOut(workflowImQueryDto.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(vo.getUserId()), workflowImQueryDto.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(workflowImQueryDto.getTenantId()); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.GO_OUT.getCode()); + approveBaseImVo.setStartTime(vo.getStartTime()); + approveBaseImVo.setEndTime(vo.getEndTime()); + approveBaseImVo.setDuration(vo.getDayNum() + "天"); + + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【外出】审批").toString()); + + sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo); + } + + private void workSendIm(WorkflowImQueryDto workflowImQueryDto, List userOperators, AttendanceApproveImVo jsonToBean) { + //张三的【加班】审批 + //选择日期:2024年5月16日 + //开始时间:22:00 + //结束时间:次日08:00 + AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(workflowImQueryDto.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(workOverTime.getUserId()), workflowImQueryDto.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(workflowImQueryDto.getTenantId()); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.OVERTIME.getCode()); + approveBaseImVo.setSelectTime(workOverTime.getWorkDay()); + approveBaseImVo.setStartTime(workOverTime.getStartTime()); + approveBaseImVo.setEndTime(workOverTime.getEndTime()); + + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【加班】审批").toString()); + + sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo); + } + + + private void leaveSendIm(WorkflowImQueryDto workflowImQueryDto, List userOperators, AttendanceApproveImVo jsonToBean) throws HandleException { + AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(workflowImQueryDto.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(attendanceLeaveApproveVo.getUserId()), workflowImQueryDto.getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(workflowImQueryDto.getTenantId()); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.LEAVE.getCode()); + approveBaseImVo.setStartTime(attendanceLeaveApproveVo.getStartTime()); + approveBaseImVo.setEndTime(attendanceLeaveApproveVo.getEndTime()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(attendanceLeaveApproveVo.getUserId(), attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), AttendanceTypeEnum.LEAVE)); + // 实时获取申请时长 + BigDecimal hours = new BigDecimal("0"); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + hours = hours.add(dailyRuleResultVo.getDuration()); + } + approveBaseImVo.setDuration(hours + "小时"); + // 请假类型 + approveBaseImVo.setLeaveType(attendanceLeaveApproveVo.getType()); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【请假】审批").toString()); + + sendIm(userOperators, jsonToBean, approveBaseImVo, approveImVo); + } + + /** + * 初始化设置 + */ + private static List getList() { + List list = new ArrayList<>(); + AttendanceLeaveSettingsVo sickVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.SICK_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.SICK_LEAVE.getCode()); + list.add(sickVo); + AttendanceLeaveSettingsVo medicalVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.MEDICAL_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.MEDICAL_LEAVE.getCode()); + list.add(medicalVo); + AttendanceLeaveSettingsVo worInjuryVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.WORK_INJURY_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.WORK_INJURY_LEAVE.getCode()); + list.add(worInjuryVo); + AttendanceLeaveSettingsVo annualVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.ANNUAL_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.ANNUAL_LEAVE.getCode()); + list.add(annualVo); + AttendanceLeaveSettingsVo weddingVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.WEDDING_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.WEDDING_LEAVE.getCode()); + list.add(weddingVo); + AttendanceLeaveSettingsVo maternityVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.MATERNITY_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.MATERNITY_LEAVE.getCode()); + list.add(maternityVo); + AttendanceLeaveSettingsVo paternityVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.PATERNITY_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.PATERNITY_LEAVE.getCode()); + list.add(paternityVo); + AttendanceLeaveSettingsVo funeralVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.FUNERAL_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.FUNERAL_LEAVE.getCode()); + list.add(funeralVo); + AttendanceLeaveSettingsVo familyVisitVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.FAMILY_VISIT_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.FAMILY_VISIT_LEAVE.getCode()); + list.add(familyVisitVo); + AttendanceLeaveSettingsVo childCareVo = new AttendanceLeaveSettingsVo(FtbUtil.getId(), LeaveTypeEnum.CHILD_CARE_LEAVE.getMsg(), 2, 3, 0, -150, 0, LeaveTypeEnum.CHILD_CARE_LEAVE.getCode()); + list.add(childCareVo); + return list; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void goOutApprove(String applyId, Integer status, String tenantId, String userId, String userName) throws ApproveException { + GoOutVo goOutVo = attendanceLeaveApproveMapper.getGoOut(applyId); + if (null == goOutVo) { + log.error("未找到对应的外出审批"); + throw new ApproveException("未找到对应的外出审批"); + } + + if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + // 校验出差开始与结束时间与已通过的借调时间重叠 + checkGroupClockForOa(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 出差开始与结束时间与已通过的外出时间重叠 + checkGoOutForOa(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime(), result, goOutVo.getId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 出差开始与结束时间与已通过的出差时间重叠 + checkBusinessTripForOa(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime(), result, goOutVo.getId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 通过时生成 + try { + BigDecimal attendanceRatio = getAttendanceRatio(goOutVo.getUserId(), goOutVo.getStartTime(), goOutVo.getEndTime()); + BigDecimal subtract = goOutVo.getDayNum().multiply(attendanceRatio); + + attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(goOutVo.getUserId(), goOutVo.getId(), goOutVo.getStartTime(), goOutVo.getEndTime(), subtract, AttendanceTypeEnum.STEP_OUT, null, null, goOutVo.getUnit(), null)); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + } + //修改审批状态 + attendanceLeaveApproveMapper.updateGoOutApprove(applyId, status, userId, userName); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void businessTripApprove(String applyId, Integer status, String tenantId, String userId, String userName) throws ApproveException { + BusinessTripVo businessTripVo = attendanceLeaveApproveMapper.getBusinessTrip(applyId); + if (null == businessTripVo) { + log.error("未找到对应的出差审批"); + throw new ApproveException("未找到对应的出差审批"); + } + if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + // 校验出差开始与结束时间与已通过的借调时间重叠 + checkGroupClockForOa(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 出差开始与结束时间与已通过的外出时间重叠 + checkGoOutForOa(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), result, businessTripVo.getId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 出差开始与结束时间与已通过的出差时间重叠 + checkBusinessTripForOa(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), result, businessTripVo.getId()); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + // 通过时生成 + try { + BigDecimal attendanceRatio = getAttendanceRatio(businessTripVo.getUserId(), businessTripVo.getStartTime(), businessTripVo.getEndTime()); + BigDecimal subtract = businessTripVo.getDayNum().multiply(attendanceRatio); + attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(businessTripVo.getUserId(), businessTripVo.getId(), businessTripVo.getStartTime(), businessTripVo.getEndTime(), subtract, AttendanceTypeEnum.BUSINESS_TRIP)); + } catch (HandleException e) { + throw new ApproveException(e.getMessage()); + } + } + //修改审批状态 + attendanceLeaveApproveMapper.updateBusinessTripApprove(applyId, status, userId, userName); + } + + + private static void setUrlParam(Integer status, StringBuilder stringBuffer, AttendanceApproveImVo jsonToBean) { + if (Objects.equals(BalanceEnum.APPROVE_STATUS_PASS.getCode(), status)) { + stringBuffer.append("已通过"); + jsonToBean.setFormType(null); + jsonToBean.setOpType(0); + jsonToBean.setStatus(2); + } else { + stringBuffer.append("已驳回"); + jsonToBean.setFormType(1); + jsonToBean.setOpType(-1); + jsonToBean.setStatus(3); + } + } + + /** + * 获取借调审批管理员 + * + * @param selfGroupId 被借调考勤组id + * @return List 用户ids + */ + private List getSecondAdmins(String selfGroupId) { + return getSecondedAdminUserId(selfGroupId); + } + + /** + * 借调校验 + */ + @Override + public void checkSeconded(String taskId) throws HandleException { + AttendanceSelfApproveVo selfApprove = attendanceLeaveApproveMapper.getSelfApprove(taskId); + String selfGroupId = selfApprove.getSelfGroupId(); + List userIds = attendanceLeaveApproveMapper.getUserList(taskId); + check(selfApprove.getGroupId(), selfApprove.getSelfGroupId(), userIds); + if (CollectionUtil.isEmpty(userIds)) { + throw new HandleException("请选择借调用户"); + } + // 判断考勤组是否被锁定 + checkSelfGroupLock(selfApprove.getGroupId(), selfApprove.getStartTime()); + checkSelfGroupLock(selfApprove.getSelfGroupId(), selfApprove.getStartTime()); + // 校验借调时间范围内是否被锁定 +// attendanceGroupMapper.getClock(selfApprove.getGroupId(),selfApprove.getStartTime(),selfApprove.getEndTime()); + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .eq(AttendanceGroupUser::getGroupId, selfGroupId) + .in(AttendanceGroupUser::getUserId, userIds); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + if (CollectionUtil.isEmpty(groupUserList)) { + return; + } + Map groupUserMap = new HashMap<>(); + for (AttendanceGroupUser groupUser : groupUserList) { + groupUserMap.put(groupUser.getUserId(), groupUser); + } +// List infoByIds = userApi.getInfoByIds(userIds); + ActionResult> infoByIds = v2UserApi.getAllUserInfoBatch(userIds, null); + if (200 == infoByIds.getCode() && CollectionUtil.isNotEmpty(infoByIds.getData())) { + Map userMap = infoByIds.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v)); + /* 校验用户是否在该时间范围被其它考勤组借调*/ + for (String userId : userIds) { + AttendanceGroupUser attendanceGroupUser = groupUserMap.get(userId); + List dateVoList = JsonUtil.getJsonToList(attendanceGroupUser.getTimeJson(), SecondmentDateVo.class); + checkClock(userId, selfApprove.getStartTime(), selfApprove.getEndTime()); + // 2025年08月28日优化之前的校验方式 + checkSecondDateNew2(dateVoList, selfApprove.getStartTime(), selfApprove.getEndTime(), userId, userMap); + } + // 校验用户是否在该时间范围被其它考勤组借调(审批中) 2.0 借调划归人事 + FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO(); + ftbSecondMentQueryDTO.setUserIds(userIds); + ftbSecondMentQueryDTO.setStartTime(selfApprove.getStartTime()); + ftbSecondMentQueryDTO.setEndTime(selfApprove.getEndTime()); + List secondmentRecordBath = ftbPersonneApi.getSecondmentRecordBath(ftbSecondMentQueryDTO); + if (CollectionUtil.isNotEmpty(secondmentRecordBath)) { + List uIds = Optional.ofNullable(secondmentRecordBath) + .orElse(Collections.emptyList()) + .stream() + .filter(Objects::nonNull) // 过滤掉null元素 + .map(FtbPersonnelsSecondmentVO::getUserId) + .filter(Objects::nonNull) // 再次过滤掉null的userId + .collect(Collectors.toList()); + +// List uIds = attendanceLeaveApproveMapper.checkSelfApprove(userIds, selfApprove.getStartTime(), selfApprove.getEndTime(), taskId); + if (CollectionUtil.isNotEmpty(uIds)) { + // 获取用户名 + // 逗号拼接用户 名 + String userNames = userMap.values().stream().filter(v -> uIds.contains(v.getId())).map(UserBoundVO::getName).collect(Collectors.joining(",")); + throw new HandleException("用户:" + userNames + "已处于借调审批中,请勿重复操作!"); + } + } + } + } + + private void checkSelfGroupLock(String groupId, Date startTime) throws HandleException { + Date groupLockDate = attendanceGroupMapper.getGroupLockDate(groupId); + if (null != groupLockDate && startTime.before(groupLockDate)) { + throw new HandleException("考勤组已锁定,过去的考勤记录无法修改!"); + } + } + + /** + * 提交时借调校验 + */ + private void submitCheckSeconded(String body) throws HandleException { + FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class); + //借调时校验(考勤组是否被删除,用户是否还在该考勤组) + check(flowTaskVo.getGroupId(), flowTaskVo.getSelfGroupId(), flowTaskVo.getUserIds()); + // 判断考勤组是否被锁定 + checkSelfGroupLock(flowTaskVo.getGroupId(), flowTaskVo.getStartTime()); + checkSelfGroupLock(flowTaskVo.getSelfGroupId(), flowTaskVo.getStartTime()); + String selfGroupId = flowTaskVo.getSelfGroupId(); + List userIds = flowTaskVo.getUserIds(); + if (CollectionUtil.isEmpty(userIds)) { + throw new HandleException("请选择借调用户"); + } + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .eq(AttendanceGroupUser::getGroupId, selfGroupId) + .in(AttendanceGroupUser::getUserId, userIds); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + if (CollectionUtil.isEmpty(groupUserList)) { + return; + } + Map groupUserMap = new HashMap<>(); + for (AttendanceGroupUser groupUser : groupUserList) { + groupUserMap.put(groupUser.getUserId(), groupUser); + } + + /* 校验用户是否在该时间范围被其它考勤组借调*/ + for (String userId : userIds) { + AttendanceGroupUser attendanceGroupUser = groupUserMap.get(userId); + List dateVoList = JsonUtil.getJsonToList(attendanceGroupUser.getTimeJson(), SecondmentDateVo.class); + checkClock(userId, flowTaskVo.getStartTime(), flowTaskVo.getEndTime()); + checkSecondDate(dateVoList, flowTaskVo.getStartTime(), flowTaskVo.getEndTime(), userId); + + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(userId, flowTaskVo.getStartTime(), flowTaskVo.getEndTime(), AttendanceTypeEnum.SECONDMENT)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } + } + + + + @Override + public void checkWork(String taskId) throws HandleException { + AttendanceWorkOverTimeVo workOverTime = attendanceLeaveApproveMapper.getWorkOverTime(taskId); + if (Math.abs(workOverTime.getEndTime().getTime() - workOverTime.getStartTime().getTime()) > 24 * 60 * 60 * 1000) { + log.error("加班时间不许超过24小时"); + throw new HandleException("加班时间不许超过24小时"); + } + //校验加班提交期间被排了班 + checkWorkTimeOverlap(attendanceDailyRuleMapper.getUserShift(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()), "选择的加班时间段范围内有其他排班"); + //校验考勤组是否存在跨组(已通过的借调) + checkMoreGroup(workOverTime); + // 考勤组是否锁定 + checkClock(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(workOverTime.getUserId(), workOverTime.getStartTime(), workOverTime.getEndTime(), AttendanceTypeEnum.WORKOVERTIME)); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + ActionResult> infoByIds = v2UserApi.getAllUserInfoBatch(Collections.singletonList(workOverTime.getUserId()), null); + if (200 == infoByIds.getCode() && CollectionUtil.isNotEmpty(infoByIds.getData())) { + Map userMap = infoByIds.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v)); +// 校验用户是否在该时间范围被其它考勤组借调(审批中) 2.0 借调划归人事 +// List uIds = attendanceLeaveApproveMapper.checkSelfApprove(Collections.singletonList(workOverTime.getUserId()), workOverTime.getStartTime(), workOverTime.getEndTime(), taskId); + FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO(); + ftbSecondMentQueryDTO.setUserIds(Collections.singletonList(workOverTime.getUserId())); + ftbSecondMentQueryDTO.setStartTime(workOverTime.getStartTime()); + ftbSecondMentQueryDTO.setEndTime(workOverTime.getEndTime()); + List secondmentRecordBath = ftbPersonneApi.getSecondmentRecordBath(ftbSecondMentQueryDTO); + if (CollectionUtil.isNotEmpty(secondmentRecordBath)) { + List uIds = Optional.ofNullable(secondmentRecordBath) + .orElse(Collections.emptyList()) + .stream() + .filter(Objects::nonNull) // 过滤掉null元素 + .map(FtbPersonnelsSecondmentVO::getUserId) + .filter(Objects::nonNull) // 再次过滤掉null的userId + .collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(uIds)) { + // 获取用户名 + // 逗号拼接用户 名 + String userNames = userMap.values().stream().filter(v -> uIds.contains(v.getId())).map(UserBoundVO::getName).collect(Collectors.joining(",")); + throw new HandleException("用户:" + userNames + "已处于借调审批中,请勿重复操作!"); + } + } + } + } + + private void checkLeave(String taskId) throws HandleException, ParseException, ApproveException { + AttendanceLeaveApproveVo attendanceLeaveApproveVo = attendanceLeaveApproveMapper.getLeaveDetailById(taskId); + if (null != attendanceLeaveApproveVo) { + LeaveQueryDto leaveQueryDto = new LeaveQueryDto(); + leaveQueryDto.setLeaveTypeId(attendanceLeaveApproveVo.getTypeId()); + leaveQueryDto.setStartTime(String.valueOf(attendanceLeaveApproveVo.getStartTime().getTime())); + leaveQueryDto.setEndTime(String.valueOf(attendanceLeaveApproveVo.getEndTime().getTime())); + leaveQueryDto.setBalanceStatus(attendanceLeaveApproveVo.getBalanceStatus()); + leaveQueryDto.setPaid(attendanceLeaveApproveVo.getPaid()); + leaveQueryDto.setNoPaid(attendanceLeaveApproveVo.getNoPaid()); + leaveQueryDto.setApplyId(attendanceLeaveApproveVo.getId()); + leaveQueryDto.setUnit(attendanceLeaveApproveVo.getUnit()); + leaveQueryDto.setTypeId(attendanceLeaveApproveVo.getTypeId()); + leaveQueryDto.setStartTimeType(attendanceLeaveApproveVo.getStartTimeType()); + leaveQueryDto.setEndTimeType(attendanceLeaveApproveVo.getEndTimeType()); + // 2.0已重构 + getResidueBalance(leaveQueryDto); + } + + } + + + /** + * 设置使用劵规则 + * + * @param balanceStatus 是否使用余额抵扣 0 否 1是 + * @param paid 带薪是否选中 1是0否 + * @param noPaid 不带薪是否选中 1是0否 + */ + private List setPaidList(Integer balanceStatus, Integer paid, Integer noPaid) { + List list = new ArrayList<>(); + if (Objects.equals(BalanceEnum.BALANCE_TRUE.getCode(), balanceStatus)) { + if (Objects.equals(BalanceEnum.BALANCE_TRUE.getCode(), paid)) { + list.add(BalanceEnum.BALANCE_PAID_DH.getCode()); + } + if (Objects.equals(BalanceEnum.BALANCE_TRUE.getCode(), noPaid)) { + list.add(BalanceEnum.BALANCE_PAID_DK.getCode()); + } + } + return list; + } + + /** + * 时间校验 + * + * @param start 借调开始时间 + * @param end 借调结束时间 + * @param userId 用户id + */ + private void checkSecondDate(List timeJsons, Date start, Date end, String userId) throws HandleException { + if (CollectionUtil.isEmpty(timeJsons)) { + return; + } + for (SecondmentDateVo dateVo : timeJsons) { + + Date startTime = dateVo.getStartTime(); + Date endTime = dateVo.getEndTime(); + Boolean startCompareStart = startTime != null && (DateUtil.dateCompare(start, startTime) >= 0 && DateUtil.dateCompare(end, endTime) <= 0); +// || (DateUtil.dateCompare(departureTime, startTime) >= 0 && DateUtil.dateCompare(backTime, endTime) <= 0); + /* 时间比较 用户借调开始时间<=用户已被借调结束时间 || 用户回岗时间<用户已被借调结束时间*/ + Boolean endCompareStart = endTime != null && (DateUtil.dateCompare(start, endTime) <= 0 && DateUtil.dateCompare(end, endTime) >= 0); +// || (DateUtil.dateCompare(backTime, endTime) <= 0 && DateUtil.dateCompare(backTime, endTime) >= 0); + if (startCompareStart || endCompareStart) { +// UserEntity userEntity = userApi.getInfoById(userId); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { +// map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) ; + throw new HandleException(userList.getData().get(0).getUserName() + "已被借调"); + } + + } + } + } + + + /** + * 时间校验 2.0 版本 + * + * @param timeJsons + * @param start 借调开始时间 + * @param end 借调结束时间 + * @param userId 用户id + * @throws HandleException + */ + private void checkSecondDateNew2(List timeJsons, Date start, Date end, String userId, Map userMap) throws HandleException { + if (CollectionUtil.isEmpty(timeJsons)) { + return; + } + for (SecondmentDateVo dateVo : timeJsons) { + + Date startTime = dateVo.getStartTime(); + Date endTime = dateVo.getEndTime(); + Boolean startCompareStart = startTime != null && (DateUtil.dateCompare(start, startTime) >= 0 && DateUtil.dateCompare(end, endTime) <= 0); + /** 时间比较 用户借调开始时间<=用户已被借调结束时间 || 用户回岗时间<用户已被借调结束时间*/ + Boolean endCompareStart = endTime != null && (DateUtil.dateCompare(start, endTime) <= 0 && DateUtil.dateCompare(end, endTime) >= 0); + if (startCompareStart || endCompareStart) { + + throw new HandleException(userMap.get(userId).getName() + "已被借调"); + } + } + } + + /** + * 请假审批通过后触发接口 + * + * @param id 审批的唯一id + * @param attendanceLeaveApproveVo 请假信息 + * @param status 是否审核通过 0.待审核 2.未通过 1.通过 3.撤销 + * @author hlp + */ + private void consumption(String id, AttendanceLeaveApproveVo attendanceLeaveApproveVo, Integer status, String tenantId, String approveUserId, String approveUserName) throws ApproveException { + if (0 == attendanceLeaveApproveVo.getStatus()) { + Integer unit = attendanceLeaveApproveVo.getUnit(); + if (1 != status) { + // 修改请假审批记录 + attendanceLeaveApproveMapper.updateLeaveApproveStatus(id, status, approveUserId, approveUserName); + } else { + //获取请假类型信息 + // 特殊情况-请假类型单位使用创建审批时的历史纪录,其他配置获取实时更新的请假类型信息 + AttendanceLeaveRulesVo userLeaveDetail = attendanceLeaveTypeService.getUserLeaveDetail(attendanceLeaveApproveVo.getTypeId(), attendanceLeaveApproveVo.getUserId()); + if (null == userLeaveDetail) { + throw new ApproveException("未找到本次请假审批选择的类型"); + } + log.info("leaveSetting: {}", userLeaveDetail); + // 涉及班次信息 + LeaveShiftVo leaveDuration = new LeaveShiftVo(); + LeaveQueryDto leaveQueryDto = new LeaveQueryDto(); + leaveQueryDto.setUserId(attendanceLeaveApproveVo.getUserId()); + leaveQueryDto.setTypeId(attendanceLeaveApproveVo.getTypeId()); + leaveQueryDto.setStartTime(attendanceLeaveApproveVo.getStartTime().getTime() + ""); + leaveQueryDto.setEndTime(attendanceLeaveApproveVo.getEndTime().getTime() + ""); + leaveQueryDto.setEndTimeType(attendanceLeaveApproveVo.getEndTimeType()); + leaveQueryDto.setStartTimeType(attendanceLeaveApproveVo.getStartTimeType()); + leaveQueryDto.setUnit(attendanceLeaveApproveVo.getUnit()); + // 划线排班不能请半天类型的假 + if (Objects.equals(BalanceEnum.BALANCE_UNIT_BT.getCode(), unit)) { + boolean lineScheduleByUserId = attendanceDailyRuleService.hasLinearRulesByPeriod(attendanceLeaveApproveVo.getUserId(), attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime()); + if (lineScheduleByUserId) { + log.error("划线排班不能请半天类型的假"); + throw new ApproveException("划线排班不能请半天类型的假"); + } + } + if (Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(), unit)) { + // 和出勤换算比做对比,算出时长 + BigDecimal timeDifference = DateDetail.getTimeDifference(attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime()); + // 小时假不能超过24小时 + if (timeDifference.compareTo(BigDecimal.valueOf(24)) > 0) { + log.error("小时假不能超过24小时"); + throw new ApproveException("小时假不能超过24小时"); + } + } + // 通过请假时的请假类型判断走什么逻辑 + // 获取时段涉及的班次和时间 + List dailyRuleResultVos = null; + ApplyParam applyParam = new ApplyParam(attendanceLeaveApproveVo.getUserId(), attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), AttendanceTypeEnum.LEAVE, attendanceLeaveApproveVo.getStartTimeType(), attendanceLeaveApproveVo.getEndTimeType(), unit); + try { + // applyParam 对象中start 和 end 会被一下方法重新赋值 表示命中的开始和结束时间 + // 主要处理半天时判断时间使用 + dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(applyParam); + leaveDuration.getDailyRuleVos().addAll(dailyRuleResultVos); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + if (1 == dailyRuleResultVo.getType()) { + // 有异常直接报错 + log.error(dailyRuleResultVo.getFailMsg()); + throw new HandleException(dailyRuleResultVo.getFailMsg()); + } + } + } catch (HandleException e) { + log.error(e.getMessage(), e); + throw new ApproveException(e.getMessage()); + } + List dailyCopy = dailyRuleResultVos; + //申请时长 + BigDecimal day = BigDecimal.ZERO; + dailyRuleResultVos = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).collect(Collectors.toList()); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + day = day.add(dailyRuleResultVo.getLeaveDays()); + } + try { + // 处理划线排班 + day = getUnderline(dailyCopy, attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd(), day,unit); + } catch (HandleException e) { + log.error(e.getMessage(), e); + throw new ApproveException(e.getMessage()); + } + //先根据日期date分组 + DailyRuleResultVo min = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).min(Comparator.comparing(DailyRuleResultVo::getInPoint)).orElse(null); + if (null != min) { + leaveQueryDto.setStartTimeHit(min.getInPoint()); + } else { + leaveQueryDto.setStartTimeHit(attendanceLeaveApproveVo.getStartTime()); + } + DailyRuleResultVo max = dailyRuleResultVos.stream().filter(vo -> Objects.equals(vo.getToType(), 3) && !Objects.equals(vo.getFromFixedMark(), 2) && StringUtil.isBlank(vo.getApplyId())).max(Comparator.comparing(DailyRuleResultVo::getOutPoint)).orElse(null); + if (null != max) { + leaveQueryDto.setEndTimeHit(max.getOutPoint()); + } else { + leaveQueryDto.setEndTimeHit(attendanceLeaveApproveVo.getEndTime()); + } + AttendanceBaseSetting attendanceBaseSetting = getAttendanceRatio(leaveQueryDto, attendanceLeaveApproveVo.getUserId()); + leaveDuration.setAttendanceRatio(attendanceBaseSetting.getAttendanceRatio()); + if (day.compareTo(BigDecimal.ZERO) <= 0) { + // 命中时长为0 表示未命中班次且需要特殊处理半天及天的请假时间 + if (BigDecimal.ZERO.compareTo(day) == 0 && !Objects.equals(BalanceEnum.BALANCE_UNIT_XH.getCode(),unit)) { + leaveQueryDto.setStartTimeHit(DateDetail.getDayBeginByDay(applyParam.getStart())); + // endtime 取日期的23:59:59 + leaveQueryDto.setEndTimeHit(DateDetail.getDayEndByDay(applyParam.getEnd())); + } + // 请假时长公式: + // 小时、半天、全天只区分命中和未命中,命中了时长=命中时长,未命中班次 请假时长 = 结束-开始(区分小时1天限制,半天取一个半天0.5,天一天取1) + try { + day = getDay(unit, leaveQueryDto, attendanceLeaveApproveVo.getUserId(), day,attendanceBaseSetting); + } catch (HandleException e) { + log.error("getDay", e); + throw new ApproveException(e.getMessage()); + } + } + try { + // 校验加班时间是否重叠 + checkWorkTimeOverlap(attendanceLeaveApproveMapper.getUserWorkByTimeSlot(attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd()), "选择的时间段范围内有其他加班(待审核/审核已通过)"); + // 校验请假时间是否重叠 2024年10月30日更改逻辑为只校验当前审批与已审核通过的审批时间是否重叠 + checkLeaveTimeOverlap(attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd(), attendanceLeaveApproveVo.getId(),attendanceLeaveApproveVo.getStartTimeType(),attendanceLeaveApproveVo.getEndTimeType(),attendanceLeaveApproveVo.getUnit()); + // 新增借调校验 + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + checkGroupClockForOa(attendanceLeaveApproveVo.getUserId(), applyParam.getStart(), applyParam.getEnd(), result); + if (result.getCode().equals(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode())) { + throw new ApproveException(result.getMsg()); + } + } catch (HandleException e) { + log.error(e.getMessage(), e); + throw new ApproveException(e.getMessage()); + } + + // 备份半天、天类型请假时长存数据库使用 + BigDecimal applicationDurationDay = day; + int applicationDurationSecond = 0; + // 备份小时类型的请假天数 + BigDecimal applicationDuration = BigDecimal.ZERO; + int undeductedTimeSecond = 0; + // 单次最大时长校验 + if (new BigDecimal(userLeaveDetail.getDuration()).compareTo(day) < 0) { + throw new ApproveException("请假时长超出可请假的最大时长"); + } + // 2.0 消费假期劵 2.0.1 可能有0小时的请假 + LeaveConsumptionDetailVo leaveConsumptionDetailVo = attendanceUserBalanceRecordService.leaveConsumption(attendanceLeaveApproveVo.getUserId(), day, userLeaveDetail, attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), attendanceLeaveApproveVo.getId()); + leaveConsumptionDetailVo.setAttendanceRatio(attendanceBaseSetting.getAttendanceRatio()); + // 调用生成排班班次信息 + try { + String s = attendanceDailyRuleService.applyDailyRuleHandle(new ApplyParam(attendanceLeaveApproveVo.getUserId(), id, attendanceLeaveApproveVo.getStartTime(), attendanceLeaveApproveVo.getEndTime(), BigDecimal.ZERO, AttendanceTypeEnum.LEAVE, attendanceLeaveApproveVo.getStartTimeType(), attendanceLeaveApproveVo.getEndTimeType(), unit, BigDecimal.ZERO)); + if (StringUtils.isNotEmpty(s)) { + throw new ApproveException(s); + } + } catch (HandleException e) { + log.error(e.getMessage(), e); + throw new ApproveException(e.getMessage()); + } + // 修改请假审批记录 2024-1-22 1请假通过保留请假类型 + // 新增未抵扣时长天与请假时间天 + attendanceLeaveApproveMapper.updateLeaveApprove(id, BigDecimal.ZERO, applicationDuration, userLeaveDetail.getName(), applicationDurationSecond, undeductedTimeSecond, approveUserId, approveUserName, applicationDurationDay, leaveConsumptionDetailVo.getUnconsumedBalance(), applyParam.getStart(), applyParam.getEnd(), JSONUtil.toJsonStr(leaveDuration), JSONUtil.toJsonStr(leaveConsumptionDetailVo)); + + } + } + } + + + /** + * 获取考勤组审批的管理员 + * + * @param groupId 考勤组id + * @param type 审批类型 0 借调 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 6.外出 7.出差 + * @return Map + */ + @Override + public AttendanceApprovalAdminVo getApproveUser(String groupId, Integer type) { + AttendanceApprovalAdminVo approvalAdminVo = new AttendanceApprovalAdminVo(); + approvalAdminVo.setIsApproval(true); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceApprovalSetting::getGroupId, groupId) + .eq(AttendanceApprovalSetting::getType, type); + AttendanceApprovalSetting approvalSetting = attendanceApprovalSettingMapper.selectOne(queryWrapper); + if (approvalSetting == null && (Objects.equals(type, ApprovalSettingTypeEnum.GO_OUT.getCode()) || Objects.equals(type, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()))) { + if (Objects.equals(type, ApprovalSettingTypeEnum.GO_OUT.getCode())) { + approvalSetting = new AttendanceApprovalSetting(groupId, ApprovalSettingTypeEnum.GO_OUT.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + } + if (Objects.equals(type, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode())) { + approvalSetting = new AttendanceApprovalSetting(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + } + } + if (approvalSetting == null) { + return approvalAdminVo; + } + + if (approvalSetting.getPermission().equals(ApprovalPermissionTypeEnum.NOT.getCode())) { + /* 无需审批*/ + approvalAdminVo.setIsApproval(false); + approvalAdminVo.setUserList(new ArrayList<>()); + return approvalAdminVo; + } + if (approvalSetting.getPermission().equals(ApprovalPermissionTypeEnum.ATTENDANCE_SUPER_ADMIN.getCode())) { + /* 考勤超级管理员*/ + QueryWrapper permissionQueryWrapper = new QueryWrapper<>(); + permissionQueryWrapper.lambda() + .eq(AttendanceManagerPermission::getDeleteMark, 0) + .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode()); + List superAdminList = attendanceManagerPermissionMapper.selectList(permissionQueryWrapper); + if (CollectionUtil.isEmpty(superAdminList)) { + approvalAdminVo.setUserList(new ArrayList<>()); + return approvalAdminVo; + } + List userIdList = superAdminList.stream().map(AttendanceManagerPermission::getUserId).collect(Collectors.toList()); + List userVoList = toUserVoInfo(userIdList); + approvalAdminVo.setUserList(userVoList); + return approvalAdminVo; + } + if (approvalSetting.getPermission().equals(ApprovalPermissionTypeEnum.SUPERIOR.getCode())) { + /* 上级考勤组管理员*/ + AttendanceGroup group = attendanceGroupMapper.selectById(groupId); + List permissionVos = recursionGetLeaderPermission(group, type); + if (CollectionUtil.isEmpty(permissionVos)) { + approvalAdminVo.setUserList(new ArrayList<>()); + return approvalAdminVo; + } + + List userIdList = permissionVos.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); + List userVoList = toUserVoInfo(userIdList); + approvalAdminVo.setUserList(userVoList); + return approvalAdminVo; + } + /* 当前考勤组管理员*/ + List groupAdminVoList = attendanceManagerPermissionMapper.queryGroupUser(groupId); + if (CollectionUtil.isEmpty(groupAdminVoList)) { + List superiorAdminList = superiorAdmin(groupId, type); + if (CollectionUtil.isEmpty(superiorAdminList)) { + approvalAdminVo.setUserList(new ArrayList<>()); + return approvalAdminVo; + } + + List userIdList = superiorAdminList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); + List userVoList = toUserVoInfo(userIdList); + approvalAdminVo.setUserList(userVoList); + return approvalAdminVo; + } + /* 对应权限审批管理员*/ + List hasPermissionAdminList = groupAdminVoList.stream().filter(admin -> { + String byCode = ApprovalSettingTypeEnum.getByCode(type); +// return hasPermission(admin.getCurPermissionJson(), type); + return checkPermissionJson(admin.getCurPermissionJson(), byCode); + }).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(hasPermissionAdminList)) { + List superiorAdminList = superiorAdmin(groupId, type); + if (CollectionUtil.isEmpty(superiorAdminList)) { + approvalAdminVo.setUserList(new ArrayList<>()); + return approvalAdminVo; + } + List userIdList = superiorAdminList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); + List userVoList = toUserVoInfo(userIdList); + approvalAdminVo.setUserList(userVoList); + return approvalAdminVo; + } + List userIdList = hasPermissionAdminList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); +// List infoByIds = userApi.getInfoByIds(userIdList); +// List userList = JsonUtil.getJsonToList(infoByIds, AttendanceUserVo.class); + List userVoList = toUserVoInfo(userIdList); + approvalAdminVo.setUserList(userVoList); + return approvalAdminVo; + } + + private List toUserVoInfo(List userIdList) { + if (CollectionUtil.isEmpty(userIdList)) { + return new ArrayList<>(); + } + + + ActionResult> result = v2UserApi.getAllUserInfoBatch(userIdList, null); + List userList = new ArrayList<>(); + if (200 == result.getCode() && CollectionUtil.isNotEmpty(result.getData())) { + userList = JsonUtil.getJsonToList(result.getData(), AttendanceUserVo.class); + } + + for (AttendanceUserVo attendanceUserVo : userList) { + String uploaderImg = UploaderUtil.uploaderImg(attendanceUserVo.getHeadIcon()); + attendanceUserVo.setHeadIcon(uploaderImg); + } + return userList; + } + + /** + * 获取上级对应权限考勤管理员 + * + * @param groupId 考勤组id + * @param type 类型 + * @return List + */ + private List superiorAdmin(String groupId, Integer type) { + AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId); + return recursionGetLeaderPermission(attendanceGroup, type); + } + + /** + * 获取请假申请审批管理员 + * + * @param body 请假开始时间 + * @return AttendanceApprovalAdminVo + */ + @Override + public AttendanceApprovalAdminVo getLeaveApproveUser(String body) throws HandleException { + + FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class); + + String loginUserId = UserProvider.getLoginUserId(); + Set groupIds = this.getGroupIdsNew(loginUserId, flowTaskVo.getApplyDate(), flowTaskVo.getEndTime()); + if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) { + throw new HandleException("请假时间不可以跨两个考勤组"); + } + + if (CollectionUtil.isEmpty(groupIds)) { + throw new HandleException("你不属于任何考勤组"); + } + String groupId = groupIds.iterator().next(); + return getApproveUser(groupId, ApprovalSettingTypeEnum.LEAVE.getCode()); + } + + /** + * 加班审批 + */ + @Override + public AttendanceApprovalAdminVo getOvertimeApproveUser(String body) throws Exception { + String loginUserId = UserProvider.getLoginUserId(); + FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class); + System.out.println("加班开始时间:" + flowTaskVo.getStartTime()); + System.out.println("加班结束时间:" + flowTaskVo.getEndTime()); + Set groupIds = getGroupIdsNew(loginUserId, flowTaskVo.getStartTime(), flowTaskVo.getEndTime()); + if (CollectionUtil.isNotEmpty(groupIds) && groupIds.size() > 1) { + throw new HandleException("加班时间不可以跨两个考勤组"); + } + if (CollectionUtil.isEmpty(groupIds)) { + throw new HandleException("你不在任何考勤组!"); + } + String nextGroupId = groupIds.iterator().next(); + + if (nextGroupId == null) { + throw new HandleException("你不属于任何考勤组"); + } +// AttendanceApprovalAdminVo approveUser = getApproveUser(curGroupUser.getGroupId(), ApprovalSettingTypeEnum.OVERTIME.getCode()); + return getApproveUser(nextGroupId, ApprovalSettingTypeEnum.OVERTIME.getCode()); + } + + /** + * 获取补卡审批管理员 + * + * @return AttendanceApprovalAdminVo + */ + @Override + public AttendanceApprovalAdminVo getReplacementCardApproveUser(String body) throws Exception { +// FlowTaskEntity flowTask = flowTaskApi.findByTaskId(taskId); + FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class); + String groupId = groupIdByClockInResultId(flowTaskVo.getClockInResultId()); + return getApproveUser(groupId, ApprovalSettingTypeEnum.ROUTINE.getCode()); + } + + /** + * 考勤变更 + */ + @Override + public AttendanceApprovalAdminVo getAttendanceAlter(String body) throws Exception { +// FlowTaskEntity flowTask = flowTaskApi.findByTaskId(taskId); + FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class); + String groupId = groupIdByClockInResultId(flowTaskVo.getClockInResultId()); + return getApproveUser(groupId, ApprovalSettingTypeEnum.ACTION_RESULT.getCode()); + } + + /** + * 获取外勤审批管理员 + * + * @return AttendanceApprovalAdminVo + */ + @Override + public AttendanceApprovalAdminVo getFieldApproval() { + String loginUserId = UserProvider.getLoginUserId(); + String groupId = getGroupIdByUserId(loginUserId); + return getApproveUser(groupId, ApprovalSettingTypeEnum.OUT.getCode()); + } + + /** + * 借调审批 + */ + @Override + public List secondedApproval(String body) throws Exception { + FlowTaskVo flowTaskVo = JsonUtil.getJsonToBean(body, FlowTaskVo.class); + + String selfGroupId = flowTaskVo.getSelfGroupId(); + + return getSecondedAdminUserId(selfGroupId); +// List managerIds = recursionGetSecondedManager(selfGroupId, managerUserIds); + } + + + /** + * 获取用户所属考勤组id + * + * @param userId 用户id + * @return String + */ + private String getGroupIdByUserId(String userId) { + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper + .lambda() + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()) + .eq(AttendanceGroupUser::getUserId, userId).eq(AttendanceGroupUser::getDeleteMark, 0); + AttendanceGroupUser groupUser = attendanceGroupUserMapper.selectOne(groupUserQueryWrapper); + return groupUser != null ? groupUser.getGroupId() : null; + } + + /** + * 通过打卡结果获取考勤组id + * + * @return String + */ + private String groupIdByClockInResultId(String clockInResultId) { + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectById(clockInResultId); + String ruleId = clockInResult.getRuleId(); + FtbAttendanceDailyRule attendanceDailyRule = attendanceDailyRuleMapper.selectById(ruleId); + return attendanceDailyRule.getGroupId(); + } + + /** + * 递归获取上级考勤组管理员 + * + * @param group 考勤组id + * @return List + */ + private List recursionGetLeaderPermission(AttendanceGroup group, Integer type) { + List adminVoList = new ArrayList<>(); + AttendanceGroup parent = attendanceGroupMapper.selectById(group.getParentId()); + if (parent == null) { + // 新增逻辑找考勤组超管 + /* 考勤超级管理员*/ + QueryWrapper permissionQueryWrapper = new QueryWrapper<>(); + permissionQueryWrapper.lambda() + .eq(AttendanceManagerPermission::getDeleteMark, 0) + .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode()); + List superAdminList = attendanceManagerPermissionMapper.selectList(permissionQueryWrapper); + if (CollectionUtil.isEmpty(superAdminList)) { + return adminVoList; + } + List userIdList = superAdminList.stream().map(AttendanceManagerPermission::getUserId).collect(Collectors.toList()); + userIdList.forEach(v -> { + AttendanceGroupAdminVo attendanceGroupAdminVo = new AttendanceGroupAdminVo(); + attendanceGroupAdminVo.setUserId(v); + adminVoList.add(attendanceGroupAdminVo); + }); + return adminVoList; + } + List groupAdminVoList = attendanceManagerPermissionMapper.queryGroupUser(parent.getId()); + if (CollectionUtil.isEmpty(groupAdminVoList)) { + return recursionGetLeaderPermission(parent, type); + } + String byCode = ApprovalSettingTypeEnum.getByCode(type); + List approvalPermissions = groupAdminVoList.stream().filter(permission -> { + return checkPermissionJson(permission.getChildPermissionJson(), byCode); + }).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(approvalPermissions)) { + return recursionGetLeaderPermission(parent, type); + } + + adminVoList.addAll(approvalPermissions); + return adminVoList; + } + + + /** + * 设置班次时段信息 + * + * @param startDay 开始日期 2023-11-11 + * @param list 集合 + * @param userId 用户id + * @author hlp + */ + private void setUserClasses(String startDay, List list, String userId) { + //查询指定日期的排班 + List classesDetailVos = attendanceDailyRuleMapper.getShiftByDay(startDay, userId); + UserClassesVo userClassesVo = new UserClassesVo(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + try { + userClassesVo.setDay(sdf.parse(startDay)); + } catch (ParseException e) { + throw new RuntimeException(e); + } + SimpleDateFormat hm = new SimpleDateFormat("HH:mm"); + classesDetailVos.forEach(v -> { + // 处理次日 + String inPoint = sdf.format(v.getInPointDate()); + String outPoint = sdf.format(v.getOutPointDate()); + String breakStartPoint = null; + String breakEndPoint = null; + String breakStartPointHm = null; + String breakEndPointHm = null; + if (null != v.getBreakStartPointDate()) { + breakStartPoint = sdf.format(v.getBreakStartPointDate()); + breakStartPointHm = hm.format(v.getBreakStartPointDate()); + } + if (null != v.getBreakStartPointDate()) { + breakEndPoint = sdf.format(v.getBreakEndPointDate()); + breakEndPointHm = hm.format(v.getBreakEndPointDate()); + } + String inPointHm = hm.format(v.getInPointDate()); + String outPointHm = hm.format(v.getOutPointDate()); + v.setInPoint(inPointHm); + v.setOutPoint(inPoint.equals(outPoint) ? outPointHm : "次日" + outPointHm); + v.setBreakStartPoint(breakStartPointHm); + v.setBreakEndPoint(null == breakStartPoint ? null : (breakStartPoint.equals(breakEndPoint) ? breakEndPointHm : "次日" + breakEndPointHm)); + }); + userClassesVo.getClassesDetailVoList().addAll(classesDetailVos); + if (CollUtil.isNotEmpty(classesDetailVos)) { + list.add(userClassesVo); + } + } + + /** + * 权限校验 + */ + private static Boolean checkPermissionJson(String permissionJson, String permissionMsg) { + if (StrUtil.isBlank(permissionJson)) { + return false; + } + /* 获取权限之间的交集*/ + List toPermissions = ListUtil.toList(permissionJson.split(",")); + List toMsgs = ListUtil.toList(permissionMsg.split(",")); + Collection intersection = CollectionUtil.intersection(toPermissions, toMsgs); + return !intersection.isEmpty(); + } + + /** + * 获取借调权限的管理员用户id + * + * @param groupId 考勤组id + * @return List + */ + private List getSecondedAdminUserId(String groupId) { + List managerUserIds = new ArrayList<>(); + Map map = attendanceSuperAdminService.listGroupAdmin(groupId); + List managerList = (List) map.get("managerList"); + System.out.println("managerList:" + managerList); + if (CollectionUtil.isNotEmpty(managerList)) { + List groupAdminVos = managerList.stream().filter(manager -> { + return StrUtil.isNotBlank(manager.getCurPermissionJson()) && manager.getCurPermissionJson().contains(AttendancePermissionConstant.SECONDED_APPROVAL); + }).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(groupAdminVos)) { + managerUserIds.addAll(groupAdminVos.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList())); + } + } + + + /* 获取父考勤组拥有子借调权限的管理员*/ + AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId); + String[] parentGroupIdArr = attendanceGroup.getLevelCode().split("#"); + List groupIds = Arrays.stream(parentGroupIdArr).collect(Collectors.toList()); + + List queryGroupAdminVos = attendanceManagerPermissionMapper.queryManagerByGroupIds(groupIds, PermissionTypeEnum.CHILD.getCode()); + if (CollectionUtil.isNotEmpty(queryGroupAdminVos)) { + List adminList = queryGroupAdminVos.stream().filter(manager -> { + return manager.getPermissions().contains(AttendancePermissionConstant.SECONDED_APPROVAL); + }).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(adminList)) { + managerUserIds.addAll(adminList.stream().map(QueryGroupAdminVo::getUserId).collect(Collectors.toList())); + } + } + log.info("借调管理员id:{}", managerUserIds); + return managerUserIds; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBaseSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBaseSettingServiceImpl.java new file mode 100644 index 0000000..010ac75 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBaseSettingServiceImpl.java @@ -0,0 +1,453 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.alibaba.fastjson.TypeReference; +import com.google.common.collect.Maps; +import jnpf.attendance.mapper.AttendanceBaseSettingMapper; +import jnpf.attendance.mapper.AttendanceFieldPersonnelMapper; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.entity.attendance.AttendanceFieldPersonnel; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.AttendanceBaseSettingDto; +import jnpf.model.attendance.dto.AttendanceGroupStatusDto; +import jnpf.model.attendance.model.RuleChangeNoticeModel; +import jnpf.model.attendance.vo.AttendanceBaseSettingVo; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.attendance.vo.GroupInfoVo; +import jnpf.model.attendance.vo.UserSettingVo; +import jnpf.model.attendance.vo.attendance.QuickCheckInVo; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.data.DataSourceContextHolder; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jnpf.constants.RedisConstant.ATTENDANCE_BASE_SETTING_CACHE_KEY; + +/** + *

+ * 考勤基础设置表 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@Service +public class AttendanceBaseSettingServiceImpl extends SuperServiceImpl implements AttendanceBaseSettingService { + @Resource + private V2UserApi v2UserApi; + @Resource + private UserProvider userProvider; + @Resource + private AttendanceUserService attendanceUserService; + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private AttendanceClockInService attendanceClockInService; + @Resource + private AttendanceSuperAdminService attendanceSuperAdminService; + @Resource + private AttendanceUserSettingService attendanceUserSettingService; + @Resource + private AttendanceFieldPersonnelMapper attendanceFieldPersonnelMapper; + @Autowired + private StringRedisTemplate redisTemplate; + @Override + @Transactional + public void save(AttendanceBaseSettingDto attendanceBaseSettingDto) throws HandleException { + AttendanceBaseSetting attendanceBaseSettingEntity = BeanUtil.toBean(attendanceBaseSettingDto, AttendanceBaseSetting.class); + UserInfo user = UserProvider.getUser(); + List list = lambdaQuery().eq(AttendanceBaseSetting::getGroupId, attendanceBaseSettingEntity.getGroupId()) + .eq(AttendanceBaseSetting::getDeleteMark, Boolean.FALSE).list(); + AttendanceGroupStatusDto attendanceGroupStatusDto = attendanceGroupService.groupSettingStatus(attendanceBaseSettingDto.getGroupId(), Boolean.FALSE); + if (Objects.isNull(attendanceGroupStatusDto)) { + throw new HandleException("当前考勤组未查到"); + } + Integer enable = attendanceGroupStatusDto.getBaseSetting(); + if (CollUtil.isEmpty(list)) { + attendanceBaseSettingEntity.setCreatorTime(new Date()); + attendanceBaseSettingEntity.setCreatorUserId(user.getUserId()); + attendanceBaseSettingEntity.setTenantId(user.getTenantId()); + attendanceBaseSettingEntity.setId(RandomUtil.uuId()); + attendanceBaseSettingEntity.setEnable(enable); + attendanceBaseSettingEntity.setDeleteMark(0); + } else { + AttendanceBaseSetting attendanceBaseSetting = list.get(0); + attendanceBaseSettingEntity.setId(attendanceBaseSetting.getId()); + attendanceBaseSettingEntity.setLastModifyTime(new Date()); + attendanceBaseSettingEntity.setLastModifyUserId(UserProvider.getLoginUserId()); + attendanceBaseSettingEntity.setEnable(enable); + attendanceBaseSettingEntity.setDeleteMark(0); + // 非超级管理员没有不能修改出勤换算比 + UserInfo userInfo = userProvider.get(); + Boolean isAdmin = attendanceSuperAdminService.isSuperAdmin(userInfo.getUserId()); + if (!isAdmin && !userProvider.get().getIsAdministrator()) { + // 不是系统超级管理员及考勤组超级管理员 不能修改出勤换算比 和 是否需要拍照 1.8.2 新加人脸识别 + attendanceBaseSettingEntity.setAttendanceRatio(null); + attendanceBaseSettingEntity.setAttendancePhoto(null); + attendanceBaseSettingEntity.setFace(null); + } + sendNotice(attendanceBaseSettingEntity.getGroupId(), attendanceBaseSetting.getFieldClockStatus(), attendanceBaseSettingEntity.getFieldClockStatus(), AttendanceNoticeEnum.RULE_CHANGE_FIELD); + sendNotice(attendanceBaseSettingEntity.getGroupId(), attendanceBaseSetting.getPatchClockStatus(), attendanceBaseSettingEntity.getPatchClockStatus(), AttendanceNoticeEnum.RULE_CHANGE_CARD_MAKEUP); + } + saveOrUpdate(attendanceBaseSettingEntity); + redisTemplate.delete(String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY,UserProvider.getUser().getTenantId())); + // 指定成员 + if (2 == attendanceBaseSettingDto.getFilePersonnelType()) { + // 保存外勤人员列表 V1.8.1 版本新增 + attendanceFieldPersonnelMapper.deleteByGroupId(attendanceBaseSettingDto.getGroupId()); + if (StringUtil.isNotEmpty(attendanceBaseSettingDto.getFieldPersonnelIds())) { + List fieldPersonnelList = new ArrayList<>(); + attendanceBaseSettingDto.getFieldPersonnelIds().forEach(v -> { + AttendanceFieldPersonnel attendanceFieldPersonnel = new AttendanceFieldPersonnel(); + attendanceFieldPersonnel.setId(FtbUtil.getId()); + attendanceFieldPersonnel.setGroupId(attendanceBaseSettingDto.getGroupId()); + attendanceFieldPersonnel.setUserId(v); + fieldPersonnelList.add(attendanceFieldPersonnel); + }); + attendanceFieldPersonnelMapper.insertList(fieldPersonnelList); + } + } + } + @Override + public void hisBaseSetting(Map group2orgMap, Map org2groupMap){ + List list = lambdaQuery().eq(AttendanceBaseSetting::getDeleteMark, Boolean.FALSE).list(); + list.forEach(item->item.setGroupId(org2groupMap.getOrDefault(group2orgMap.getOrDefault(item.getGroupId(),item.getGroupId()),item.getGroupId()))); + list.stream().collect(Collectors.groupingBy(AttendanceBaseSetting::getGroupId)).forEach((key,value)->{ + value.stream().skip(1).forEach(vo->vo.setDeleteMark(1)); + }); + updateBatchById(list); + redisTemplate.delete(String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY,UserProvider.getUser().getTenantId())); + } + + private void sendNotice(String groupId, Object oldValue, Object newValue, AttendanceNoticeEnum attendanceNoticeEnum) { + if (Objects.equals(oldValue, newValue)) { + return; + } + List selfAndChildrenGroupIds = attendanceGroupService.getSelfAndChildrenGroupIds(groupId, 1); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, oldValue, newValue, attendanceNoticeEnum)); + } + + private void sendNoticeItem(String groupId, Object oldValue, Object newValue, AttendanceNoticeEnum attendanceNoticeEnum) { + RuleChangeNoticeModel ruleChangeNoticeModel = new RuleChangeNoticeModel(); + ruleChangeNoticeModel.setIsAdmin(Boolean.FALSE); + ruleChangeNoticeModel.setOldValue(oldValue); + ruleChangeNoticeModel.setNewValue(newValue); + ruleChangeNoticeModel.setGroupId(groupId); + ruleChangeNoticeModel.setTenantId(UserProvider.getUser().getTenantId()); + ruleChangeNoticeModel.setAttendanceNoticeEnum(attendanceNoticeEnum); + attendanceNoticeHandler.send(ruleChangeNoticeModel); + ruleChangeNoticeModel.setIsAdmin(Boolean.TRUE); + attendanceNoticeHandler.send(ruleChangeNoticeModel); + } + + @Override + public AttendanceBaseSettingVo getOne(String groupId) { + // 处理新增字段 内勤拍照 + Map enableBaseSetting = getEnableBaseSetting(CollUtil.newArrayList(groupId)); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId); + if (Objects.isNull(attendanceBaseSetting)) { + return null; + } + AttendanceBaseSettingVo bean = BeanUtil.toBean(attendanceBaseSetting, AttendanceBaseSettingVo.class); + // 非超级管理员不能查看出勤换算比 + UserInfo userInfo = userProvider.get(); + Boolean isAdmin = attendanceSuperAdminService.isSuperAdmin(userInfo.getUserId()); + if (isAdmin || userProvider.get().getIsAdministrator()) { + bean.setAttendanceRatioType(true); + } + // 处理新增字段 外勤人员问题,当继承上级设置时且允许外勤打开,外勤人员列表默认为全员 + // 获取选中考勤组的basesetting状态 关闭情况下 为继承上级 + AttendanceGroup attendanceGroup = attendanceGroupService.getGroupSetting(groupId); + // 本组全员(不含借调) + List groupUserList = attendanceUserService.getGroupUserList(groupId, Boolean.FALSE); + List list = new ArrayList<>(); + List collect = groupUserList.stream().map(AttendanceGroupUserVo::getUserId).collect(Collectors.toList()); + // 是否打开自定义 + if (null != attendanceGroup && 0 == attendanceGroup.getBaseSetting()){ + // 关闭继承上级 上级外勤打开 且设置为全部 子集展示为全部 父级设置为指定,子级默认为空 + if (1 == bean.getFieldClockStatus() && 1 == bean.getFilePersonnelType() ){ + setUser(list, collect); + bean.setFieldPersonnellist(list); + } + } else { + // 打开自定义 查自己外勤人员列表配置 + // 选择为全部 + // 查询本组绑定的外勤人员 + List userIds = attendanceFieldPersonnelMapper.getUserIdsByGroupId(groupId); + // 去除不在本组中的成员 + userIds.retainAll(collect); + setUser(list,userIds); + bean.setFieldPersonnellist(list); + } + return bean; + } + + /** + * 获取用户信息集合 + * @param list 用户信息 + * @param userIds 用户ids + */ + private void setUser(List list,List userIds) { + ActionResult> userList = v2UserApi.getUserPrimaryBoundBatch(userIds, null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + for (UserBoundVO userPrimaryPositionDetailVO : userList.getData()) { + AttendanceGroupUserVo groupUserVo = new AttendanceGroupUserVo(); + groupUserVo.setUserId(userPrimaryPositionDetailVO.getId()); + groupUserVo.setRealName(userPrimaryPositionDetailVO.getUserName()); + groupUserVo.setHeadIcon(UploaderUtil.uploaderImg(userPrimaryPositionDetailVO.getHeadIcon())); + groupUserVo.setPositionName(userPrimaryPositionDetailVO.getPositionName()); + list.add(groupUserVo); + } + } + } + + + @Override + public void changeStatus(String groupId, Integer enable, AttendanceBaseSettingVo attendanceBaseSetting) { + lambdaUpdate().eq(AttendanceBaseSetting::getGroupId, groupId) + .set(AttendanceBaseSetting::getEnable, enable) + .update(); + AttendanceBaseSetting attendanceBaseSettingEntity = getEnableBaseSetting(CollUtil.newArrayList(groupId)).get(groupId); + redisTemplate.delete(String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY,UserProvider.getUser().getTenantId())); + if(Objects.isNull(attendanceBaseSetting) || Objects.isNull(attendanceBaseSettingEntity)){ + return; + } + sendNotice(groupId, attendanceBaseSetting.getFieldClockStatus(), attendanceBaseSettingEntity.getFieldClockStatus(), AttendanceNoticeEnum.RULE_CHANGE_FIELD); + sendNotice(groupId, attendanceBaseSetting.getPatchClockStatus(), attendanceBaseSettingEntity.getPatchClockStatus(), AttendanceNoticeEnum.RULE_CHANGE_CARD_MAKEUP); + } + + @Override + public Map getEnableBaseSetting(List groupIds) { + Map collect = getStringAttendanceBaseSettingMap(); + return groupIds.stream().distinct().collect(Collectors.toMap(Function.identity(), collect::get)); + } + + @NotNull + private Map getStringAttendanceBaseSettingMap() { + String key = String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY, StringUtil.isEmpty(UserProvider.getUser().getTenantId()) ? DataSourceContextHolder.getDatasourceId() : UserProvider.getUser().getTenantId()); + if(redisTemplate.hasKey(key)){ + String object = redisTemplate.opsForValue().get(key); + return Objects.requireNonNull(com.alibaba.fastjson.JSONObject.parseObject(object, new TypeReference<>() { + })); + } + List attendanceGroupVos = attendanceGroupService.queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return Maps.newHashMap(); + } + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getBaseSetting() == 1)); + List list = lambdaQuery() + .eq(AttendanceBaseSetting::getDeleteMark, Boolean.FALSE) + .orderByDesc(AttendanceBaseSetting::getCreatorTime) + .list(); + Map baseSettingMap = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.toMap(AttendanceBaseSetting::getGroupId, baseSetting -> baseSetting, (b1, b2) -> b2)); + // ========== 优化4:结果映射 ========== + Map result = new HashMap<>(collect1.size()); + for (Map.Entry entry : collect1.entrySet()) { + result.put(entry.getKey(), findNextBaseSetting(baseSettingMap, groupIdToParent, entry.getKey())); + } + redisTemplate.opsForValue().set(key, com.alibaba.fastjson.JSONObject.toJSONString(result)); + return result; + } + + @Override + public Map getEnableBaseSettingAll(List groupIds) { + List attendanceGroupVos = attendanceGroupService.queryAllListByIds(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return Maps.newHashMap(); + } + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getBaseSetting() == 1)); + List list = lambdaQuery().orderByDesc(AttendanceBaseSetting::getCreatorTime).list(); + Map collect = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.toMap(AttendanceBaseSetting::getGroupId, baseSetting -> baseSetting, (b1, b2) -> b2)); + Map map = Maps.newHashMap(); + groupIds.forEach(groupId -> map.put(groupId, findNextBaseSetting(collect, groupIdToParent, groupId))); + return map; + } + + + @Override + public Map getEnableBaseSetting(List groupIds, List attendanceGroupVos) { + List list = lambdaQuery() + .eq(AttendanceBaseSetting::getDeleteMark, Boolean.FALSE) + .orderByDesc(AttendanceBaseSetting::getCreatorTime) + .list(); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getBaseSetting() == 1)); + Map collect = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.toMap(AttendanceBaseSetting::getGroupId, baseSetting -> baseSetting, (b1, b2) -> b2)); + Map map = Maps.newHashMap(); + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + groupIds.forEach(groupId -> map.put(groupId, findNextBaseSetting(collect, groupIdToParent, groupId))); + return map; + } + + @Override + public void initBaseSetting(String groupId) { + List list = lambdaQuery().eq(AttendanceBaseSetting::getGroupId, groupId) + .eq(AttendanceBaseSetting::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isNotEmpty(list)) { + return; + } + Map enableBaseSetting = getEnableBaseSetting(CollUtil.newArrayList(groupId)); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId); + if (Objects.nonNull(attendanceBaseSetting)) { + attendanceBaseSetting.setId(RandomUtil.uuId()); + attendanceBaseSetting.setGroupId(groupId); + attendanceBaseSetting.setCreatorTime(new Date()); + attendanceBaseSetting.setCreatorUserId(UserProvider.getLoginUserId()); + save(attendanceBaseSetting); + } + } + + @Override + public AttendanceBaseSetting findBaseSetting(String groupId) { + return lambdaQuery() + .eq(AttendanceBaseSetting::getDeleteMark, Boolean.FALSE) + .eq(AttendanceBaseSetting::getGroupId, groupId) + .orderByDesc(AttendanceBaseSetting::getCreatorTime) + .last("limit 1") + .one(); + } + + @Override + public JSONArray getTakePhotoSetting(UserInfo userInfo) throws QueryException { + + // 根据当前时间用户所在考勤组查询配置 + GroupInfoVo groupInfo = attendanceClockInService.getGroupInfoByDate(new Date(), userInfo); + JSONArray array = new JSONArray(); + JSONObject json = new JSONObject(); + Integer shouldTakePhoto = groupInfo.getGroupRule().getShouldTakePhoto(); + json.set("state", shouldTakePhoto); + json.set("name", shouldTakePhoto.equals(ConstantUtil.NUM_TRUE) ? "是" : "否"); + array.put(json); + return array; + } + + @Override + public Integer getNowGroupAttendancePhoto() { + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(new Date(), new Date(), List.of(userProvider.get().getUserId()), null); + if (CollUtil.isNotEmpty(attendanceGroupUsers)) { + // 注意可能会继承上级配置 + Map enableBaseSetting = getEnableBaseSetting(CollUtil.newArrayList(attendanceGroupUsers.get(0).getGroupId())); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(attendanceGroupUsers.get(0).getGroupId()); + if (Objects.isNull(attendanceBaseSetting)) { + return 0; + } + return 1 == attendanceBaseSetting.getFace() || 1 == attendanceBaseSetting.getAttendancePhoto() ? 1 : 0; + } + return 0; + } + + @Override + public boolean getFieldClockStatusSelf(AttendanceBaseSetting setting) { + if (null == setting) { + return false; + } + // 传入考勤组全员(不含借调) + List groupUserList = attendanceUserService.getGroupUserList(setting.getGroupId(), Boolean.FALSE); + List collect = groupUserList.stream().map(AttendanceGroupUserVo::getUserId).collect(Collectors.toList()); + if (ConstantUtil.NUM_FALSE == setting.getFieldClockStatus()) { + return false; + } else { + if (1 == setting.getFilePersonnelType()) { + return true; + } else { + // 查询本组绑定的外勤人员 + List userIds = attendanceFieldPersonnelMapper.getUserIdsByGroupId(setting.getGroupId()); + // 去除不在本组中的成员 + userIds.retainAll(collect); + return userIds.contains(userProvider.get().getUserId()); + } + } + } + + @Override + public QuickCheckInVo quickCheckIn() { + QuickCheckInVo quickCheckInVo = new QuickCheckInVo(); + // 用户上下班打卡信息 + List settingList = attendanceUserSettingService.getSettingList(CollUtil.newArrayList(userProvider.get().getUserId()), 1, Stream.of(UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode(), + UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode()) + .collect(Collectors.toList())); + Optional startOpt = settingList.stream() + .filter(v -> UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode().equals(v.getCode())) + .findFirst(); + quickCheckInVo.setQuickGoToWork(startOpt.map(UserSettingVo::getStatus).orElse(0)); + + Optional endOpt = settingList.stream() + .filter(v -> UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode().equals(v.getCode())) + .findFirst(); + quickCheckInVo.setQuickOffWork(endOpt.map(UserSettingVo::getStatus).orElse(0)); + + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(new Date(), new Date(), List.of(userProvider.get().getUserId()), null); + if (CollUtil.isNotEmpty(attendanceGroupUsers)) { + // 注意可能会继承上级配置 + Map enableBaseSetting = getEnableBaseSetting(CollUtil.newArrayList(attendanceGroupUsers.get(0).getGroupId())); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(attendanceGroupUsers.get(0).getGroupId()); + if (Objects.isNull(attendanceBaseSetting)) { + quickCheckInVo.setQuickGoToWork(0); + quickCheckInVo.setQuickOffWork(0); + } else { + quickCheckInVo.setQuickGoToWork(( 1 == attendanceBaseSetting.getFace() || 1 == attendanceBaseSetting.getAttendancePhoto()) ? 0 : quickCheckInVo.getQuickGoToWork()); + quickCheckInVo.setQuickOffWork(( 1 == attendanceBaseSetting.getFace() || 1 == attendanceBaseSetting.getAttendancePhoto()) ? 0 : quickCheckInVo.getQuickOffWork()); + } + } + return quickCheckInVo; + } + + @Override + public boolean getNeedFace(String groupId) { + Map enableBaseSetting = getEnableBaseSetting(CollUtil.newArrayList(groupId)); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId); + if (Objects.isNull(attendanceBaseSetting)) { + return false; + } + return 1 == attendanceBaseSetting.getFace(); + } + + /** + * 获取考勤组下个考勤组配置 + * + * @param collect 考勤组配置 + * @param groupIdToParent 考勤组层级关系 + * @param groupId 考勤组id + * @return 考勤组配置 + */ + private AttendanceBaseSetting findNextBaseSetting(Map collect, Map groupIdToParent, String groupId) { + if (StringUtil.isBlank(groupId)) { + return null; + } + if (!collect.containsKey(groupId)) { + return findNextBaseSetting(collect, groupIdToParent, groupIdToParent.get(groupId)); + } + return collect.get(groupId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookConfigServiceImpl.java new file mode 100644 index 0000000..244e1ab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookConfigServiceImpl.java @@ -0,0 +1,1149 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceBookConfigMapper; +import jnpf.attendance.mapper.AttendanceBookRecordMapper; +import jnpf.attendance.service.AttendanceBookConfigService; +import jnpf.attendance.service.AttendanceLeaveTypeService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.RuleScopeService; +import jnpf.base.ActionResult; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.AttendanceBookConfigEntity; +import jnpf.entity.attendance.AttendanceBookRecordEntity; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.AttendanceStatusEnum; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceBookConfigAddUserDto; +import jnpf.model.attendance.dto.AttendanceBookConfigDto; +import jnpf.model.attendance.dto.AttendanceBookConfigQueryDto; +import jnpf.model.attendance.dto.GetValidUsersDto; +import jnpf.model.attendance.vo.*; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.util.*; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 考勤本配置服务实现 + * + * 设计说明: + * 1. 考勤本配置独立管理,包含名称、使用范围、负责人等信息 + * 2. 负责人和可排假期都支持多选,用逗号拼接存储 + * 3. 优化:统一查询用户信息,避免多次调用API + * + * 已实现需求: + * - 需求1:考勤本名称去空格+重复性校验 + * - 需求2:创建人列表接口 + * - 需求3:人员列表接口 + * - 需求9:列表负责人过滤 + * + * @author Generated + * @create 2026-04-15 + */ +@Slf4j +@Service +public class AttendanceBookConfigServiceImpl extends SuperServiceImpl implements AttendanceBookConfigService { + + // ==================== 常量定义 ==================== + // 注:所有业务常量已迁移至 AttendanceConstant 类 + + @Autowired + private RuleScopeUtil ruleScopeUtil; + @Autowired + private RuleScopeService ruleScopeService; + @Autowired + private AttendanceUserService attendanceUserService; + @Autowired + private UserAntifreeze userAntifreeze; + @Resource + private AttendanceLeaveTypeService attendanceLeaveTypeService; + @Resource + private V2OrganizeApi v2OrganizeApi; + @Resource + private AttendanceBookRecordMapper attendanceBookRecordMapper; + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveOrUpdateAttendanceBookConfig(AttendanceBookConfigDto configDto) throws HandleException { + if (StringUtils.isBlank(configDto.getId())) { + addAttendanceBookConfig(configDto); + } else { + updateAttendanceBookConfig(configDto.getId(), configDto); + } + } + + private void addAttendanceBookConfig(AttendanceBookConfigDto configDto) throws HandleException { + // 需求1:考勤本名称去空格处理 + String bookName = configDto.getBookName(); + if (StringUtils.isNotBlank(bookName)) { + configDto.setBookName(bookName.trim()); + } + + // 需求1:考勤本名称重复性校验 + validateBookNameDuplicate(null, configDto.getBookName()); + + AttendanceBookConfigEntity config = getAttendanceBookConfig(configDto); + List ruleScopeList = ruleScopeUtil.getRuleScopeList( + config.getId(), + configDto.getScopeType(), + MutablePair.of(configDto.getOrganizeList(), configDto.getUserIdList()), + ScopeBizType.ATTENDANCE_BOOK + ); + this.save(config); + if (!ruleScopeList.isEmpty()) { + ruleScopeUtil.saveBatch(ruleScopeList); + } + } + + private void updateAttendanceBookConfig(String id, AttendanceBookConfigDto configDto) throws HandleException { + // 需求1:考勤本名称去空格处理 + String bookName = configDto.getBookName(); + if (StringUtils.isNotBlank(bookName)) { + configDto.setBookName(bookName.trim()); + } + + // 需求1:考勤本名称重复性校验(排除当前记录) + validateBookNameDuplicate(id, configDto.getBookName()); + + AttendanceBookConfigEntity config = this.getById(id); + if (null == config) { + throw new HandleException(AttendanceConstant.ERR_CONFIG_NOT_FOUND); + } + Integer oldScope = config.getScopeType(); + Integer newScope = configDto.getScopeType(); + + validateScopeChangeWhenRecordsExist(id, oldScope, newScope, configDto); + + config.setBookName(configDto.getBookName()); + config.setScopeType(configDto.getScopeType()); + config.setManagerIds(configDto.getManagerIds()); + config.setHolidayScopeType(configDto.getHolidayScopeType()); + config.setHolidayIds(configDto.getHolidayIds()); + config.setEnabledMark(configDto.getEnabledMark() != null ? configDto.getEnabledMark() : ConstantUtil.NUM_TRUE); + config.setLastModifyTime(DateUtil.getNowDate()); + config.setLastModifyUserId(UserProvider.getLoginUserId()); + this.updateById(config); + + ruleScopeUtil.updateRuleScopeList( + id, + oldScope, + newScope, + MutablePair.of(configDto.getOrganizeList(), configDto.getUserIdList()), + ScopeBizType.ATTENDANCE_BOOK + ); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteAttendanceBookConfig(String id) { + this.update(new LambdaUpdateWrapper() + .set(AttendanceBookConfigEntity::getDeleteMark, ConstantUtil.NUM_TRUE) + .set(AttendanceBookConfigEntity::getDeleteTime, DateUtil.getNowDate()) + .set(AttendanceBookConfigEntity::getDeleteUserId, UserProvider.getLoginUserId()) + .eq(AttendanceBookConfigEntity::getId, id)); + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, id) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.ATTENDANCE_BOOK.getValue())); + } + + @Override + public void updateEnableStatus(String id) throws HandleException { + AttendanceBookConfigEntity config = this.getById(id); + if (null == config) { + throw new HandleException(AttendanceConstant.ERR_CONFIG_NOT_EXIST); + } + + this.update(new LambdaUpdateWrapper() + .set(AttendanceBookConfigEntity::getEnabledMark, + config.getEnabledMark().equals(ConstantUtil.NUM_TRUE) ? ConstantUtil.NUM_FALSE : ConstantUtil.NUM_TRUE) + .set(AttendanceBookConfigEntity::getLastModifyTime, DateUtil.getNowDate()) + .set(AttendanceBookConfigEntity::getLastModifyUserId, UserProvider.getLoginUserId()) + .eq(AttendanceBookConfigEntity::getId, id)); + } + + @Override + public AttendanceBookConfigVo getDetail(String id) { + AttendanceBookConfigEntity config = this.getById(id); + return setDetailInfo(config); + } + + /** + * 设置详细信息(优化版:统一查询用户信息) + * + * 优化点: + * 1. 提前返回,减少嵌套 + * 2. 抽取查询逻辑,职责分离 + * 3. 使用 Optional 处理空值 + * 4. 常量提取,提高可读性 + * 5. 参考OvertimeRuleServiceImpl.setDetailInfo方法,添加使用范围查询 + */ + private AttendanceBookConfigVo setDetailInfo(AttendanceBookConfigEntity config) { + if (config == null) { + return null; + } + + // 1. 基础信息转换 + AttendanceBookConfigVo vo = JsonUtil.getJsonToBean(config, AttendanceBookConfigVo.class); + + // 2. 查询使用范围(参考OvertimeRuleServiceImpl.setDetailInfo方法) + MutablePair, List> scopeData = ruleScopeUtil.selectScopeList( + vo.getId(), + ScopeBizType.ATTENDANCE_BOOK + ); + if (null != scopeData) { + if (scopeData.getLeft().isEmpty() && scopeData.getRight().isEmpty()) { + // 无适配范围,说明是"全部" + vo.setScopeType(-1); + } else { + vo.getOrganizeList().addAll(scopeData.getLeft()); + vo.getUserIdList().addAll(scopeData.getRight()); + } + } + + // 3. 查询并构建用户信息映射 + Map userMap = buildUserMap(config); + + // 4. 设置展示字段 + vo.setScopeDisplayText(buildScopeDisplayText(config, userMap)); + vo.setManagerList(buildManagerList(config.getManagerIds(), userMap)); + vo.setCreatorUserName(getUserName(config.getCreatorUserId(), userMap)); + + // 5. 设置假期相关信息 + setHolidayInfo(vo, config); + + return vo; + } + + /** + * 构建用户信息映射(统一查询) + */ + private Map buildUserMap(AttendanceBookConfigEntity config) { + Set allUserIds = collectAllUserIds(config); + + if (allUserIds.isEmpty()) { + return Collections.emptyMap(); + } + + return queryUserMap(allUserIds); + } + + /** + * 批量查询并转换为VO列表(优化版:避免N+1问题) + */ + private List convertToVoListWithBatchQuery(List entityList) { + if (CollUtil.isEmpty(entityList)) { + return Collections.emptyList(); + } + + // 1. 收集所有需要查询的用户ID + Set allUserIds = new HashSet<>(); + // 收集所有需要查询的规则ID(用于查询使用范围) + List ruleIds = new ArrayList<>(); + + for (AttendanceBookConfigEntity config : entityList) { + ruleIds.add(config.getId()); + // 收集负责人ID + allUserIds.addAll(parseIdsToList(config.getManagerIds())); + // 收集创建人ID + if (StringUtils.isNotBlank(config.getCreatorUserId())) { + allUserIds.add(config.getCreatorUserId()); + } + } + + // 1.5. 批量查询使用范围,收集使用范围中的用户ID + Map, List>> scopeMap = ruleScopeUtil.selectScopeListBatch(ruleIds, ScopeBizType.ATTENDANCE_BOOK); + for (MutablePair, List> scope : scopeMap.values()) { + if (scope != null && scope.getRight() != null) { + allUserIds.addAll(scope.getRight()); + } + } + + // 2. 批量查询用户信息 + Map userMap = queryUserMap(allUserIds); + + // 3. 批量查询假期信息(只需要查询一次) + Map leaveMap = buildLeaveMap(null); + + // 4. 转换为VO列表 + List voList = new ArrayList<>(); + for (AttendanceBookConfigEntity config : entityList) { + AttendanceBookConfigVo vo = convertToVoWithCachedData(config, userMap, leaveMap, scopeMap); + voList.add(vo); + } + + return voList; + } + + /** + * 使用缓存的数据转换为VO(避免重复查询) + */ + private AttendanceBookConfigVo convertToVoWithCachedData(AttendanceBookConfigEntity config, + Map userMap, + Map leaveMap, + Map, List>> scopeMap) { + // 1. 基础信息转换 + AttendanceBookConfigVo vo = JsonUtil.getJsonToBean(config, AttendanceBookConfigVo.class); + + // 2. 设置使用范围展示文本(使用缓存的使用范围数据) + MutablePair, List> scopeData = scopeMap.get(config.getId()); + vo.setScopeDisplayText(buildScopeDisplayTextWithCache(config, userMap, scopeData)); + + // 3. 设置负责人列表 + vo.setManagerList(buildManagerList(config.getManagerIds(), userMap)); + + // 4. 设置创建人姓名(使用缓存的用户信息) + vo.setCreatorUserName(getUserName(config.getCreatorUserId(), userMap)); + + // 5. 设置假期相关信息(使用缓存的假期信息) + vo.setHolidayDisplayText(buildHolidayDisplayTextWithCache(config, leaveMap)); + vo.setHolidayTypes(buildHolidayTypesWithCache(config.getHolidayIds(), leaveMap)); + + return vo; + } + + /** + * 使用缓存的假期信息构建假期展示文本 + */ + private String buildHolidayDisplayTextWithCache(AttendanceBookConfigEntity config, + Map leaveMap) { + if (config.getHolidayScopeType() == null) { + return ""; + } + + if (Integer.valueOf(1).equals(config.getHolidayScopeType())) { + return "全部假期"; + } + + if (StringUtils.isBlank(config.getHolidayIds())) { + return ""; + } + + List holidayIdList = parseIdsToList(config.getHolidayIds()); + + return holidayIdList.stream() + .map(leaveMap::get) + .filter(Objects::nonNull) + .map(AttendanceLeaveType::getName) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(",")); + } + + /** + * 使用缓存的假期信息构建假期类型列表 + */ + private List buildHolidayTypesWithCache(String holidayIds, + Map leaveMap) { + List holidayIdList = parseIdsToList(holidayIds); + if (holidayIdList.isEmpty()) { + return Collections.emptyList(); + } + + return holidayIdList.stream() + .map(leaveMap::get) + .filter(Objects::nonNull) + .map(leave -> HolidayTypeVo.builder() + .id(leave.getId()) + .name(leave.getName()) + .build()) + .collect(Collectors.toList()); + } + + /** + * 收集所有需要查询的用户ID + */ + private Set collectAllUserIds(AttendanceBookConfigEntity config) { + + // 收集负责人ID + Set userIds = new HashSet<>(parseIdsToList(config.getManagerIds())); + + // 收集创建人ID + Optional.ofNullable(config.getCreatorUserId()) + .filter(StringUtils::isNotBlank) + .ifPresent(userIds::add); + + // 收集使用范围中的用户ID + if (isOrgUserScope(config)) { + List scopeUserIds = getScopeUserIds(config.getId()); + userIds.addAll(scopeUserIds); + } + + return userIds; + } + + /** + * 查询用户信息并构建Map + */ + private Map queryUserMap(Set userIds) { + try { + List result = userAntifreeze.getStaffRosterListInfoByIds( + new ArrayList<>(userIds) + ); + + if (CollUtil.isEmpty(result)) { + return Collections.emptyMap(); + } + + return result.stream() + .filter(user -> user != null && user.getUserId() != null) + .collect(Collectors.toMap( + PartUserInfoVo::getUserId, + user -> user, + (existing, replacement) -> existing + )); + } catch (Exception e) { + log.error("统一查询用户信息失败, userIds: {}", userIds, e); + return Collections.emptyMap(); + } + } + + /** + * 设置假期相关信息(仅查询一次有效假期类型,已删除的不展示) + */ + private void setHolidayInfo(AttendanceBookConfigVo vo, AttendanceBookConfigEntity config) { + if (config.getHolidayScopeType() == null) { + vo.setHolidayDisplayText(""); + vo.setHolidayTypes(Collections.emptyList()); + return; + } + if (Integer.valueOf(1).equals(config.getHolidayScopeType())) { + vo.setHolidayDisplayText("全部假期"); + vo.setHolidayTypes(Collections.emptyList()); + return; + } + if (StringUtils.isBlank(config.getHolidayIds())) { + vo.setHolidayDisplayText(""); + vo.setHolidayTypes(Collections.emptyList()); + return; + } + Map leaveMap = buildLeaveMap(parseIdsToList(config.getHolidayIds())); + vo.setHolidayDisplayText(buildHolidayDisplayTextWithCache(config, leaveMap)); + vo.setHolidayTypes(buildHolidayTypesWithCache(config.getHolidayIds(), leaveMap)); + vo.setHolidayIds(String.join(",", leaveMap.keySet())); + } + + /** + * 构建假期映射(仅包含未删除的假期类型) + * + * @param holidayIdList 指定 ID 列表;为空或 null 时查询全部有效假期 + */ + private Map buildLeaveMap(List holidayIdList) { + List leaveList = attendanceLeaveTypeService.getAttendanceLeaveTypes( + CollUtil.isEmpty(holidayIdList) ? null : holidayIdList); + if (leaveList == null || leaveList.isEmpty()) { + return Collections.emptyMap(); + } + + return leaveList.stream() + .filter(leave -> leave != null && leave.getId() != null) + .collect(Collectors.toMap( + AttendanceLeaveType::getId, + leave -> leave, + (existing, replacement) -> existing + )); + } + + /** + * 构建使用范围展示文本 + */ + private String buildScopeDisplayText(AttendanceBookConfigEntity config, Map userMap) { + if (ConstantUtil.SCOPE_ALL.equals(config.getScopeType())) { + return "全部"; + } + + List scopes = queryRuleScopes(config.getId()); + if (scopes.isEmpty()) { + return ""; + } + + return buildScopeDisplayTextFromScopes(scopes, userMap); + } + + /** + * 使用缓存的使用范围数据构建展示文本 + */ + private String buildScopeDisplayTextWithCache(AttendanceBookConfigEntity config, + Map userMap, + MutablePair, List> scopeData) { + if (ConstantUtil.SCOPE_ALL.equals(config.getScopeType())) { + return "全部"; + } + + if (scopeData == null) { + return ""; + } + + List orgIds = scopeData.getLeft(); + List userIds = scopeData.getRight(); + + if ((orgIds == null || orgIds.isEmpty()) && (userIds == null || userIds.isEmpty())) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + // 处理组织:直接通过组织API查询组织名称 + if (orgIds != null && !orgIds.isEmpty()) { + try { + // 批量查询组织信息 + ActionResult> result = v2OrganizeApi.organizesByOrganizeIds(orgIds); + + if (result != null && result.getData() != null && CollUtil.isNotEmpty(result.getData())) { + List orgList = result.getData(); + String orgNames = orgList.stream() + .filter(org -> org != null && StringUtils.isNotBlank(org.getName())) + .map(OrganizeGeneralDetailVO::getName) + .collect(Collectors.joining(",")); + + if (StringUtils.isNotBlank(orgNames)) { + sb.append("【使用的组织】:").append(orgNames); + } else { + sb.append("【使用的组织】:").append(String.join(",", orgIds)); + } + } else { + // 如果查询不到组织信息,显示组织ID + sb.append("【使用的组织】:").append(String.join(",", orgIds)); + } + } catch (Exception e) { + log.error("查询组织名称失败,orgIds: {}", orgIds, e); + sb.append("【使用的组织】:").append(String.join(",", orgIds)); + } + } + + // 处理用户 + if (userIds != null && !userIds.isEmpty()) { + if (sb.length() > 0) { + sb.append("; "); + } + + String userNames = userIds.stream() + .map(userId -> Optional.ofNullable(userMap.get(userId)) + .map(PartUserInfoVo::getRealName) + .orElse(userId)) + .collect(Collectors.joining(",")); + + sb.append("【使用的人员】:").append(userNames); + } + + return sb.toString(); + } + + /** + * 查询规则使用范围 + */ + private List queryRuleScopes(String ruleId) { + return ruleScopeService.list( + new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, ruleId) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.ATTENDANCE_BOOK.getValue()) + ); + } + + /** + * 从使用范围构建展示文本 + */ + private String buildScopeDisplayTextFromScopes(List scopes, Map userMap) { + List orgIds = scopes.stream() + .filter(s -> ConstantUtil.RULE_SCOPE_ORG.equals(s.getScopeType())) + .map(AttendanceRuleScope::getScopeValue) + .collect(Collectors.toList()); + + List userIds = scopes.stream() + .filter(s -> ConstantUtil.RULE_SCOPE_USER.equals(s.getScopeType())) + .map(AttendanceRuleScope::getScopeValue) + .collect(Collectors.toList()); + + if (orgIds.isEmpty() && userIds.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + if (!orgIds.isEmpty()) { + sb.append("【使用的组织】:").append(String.join(",", orgIds)); + } + + if (!userIds.isEmpty()) { + appendUserNames(sb, userIds, userMap); + } + + return sb.toString(); + } + + /** + * 追加用户名称到 StringBuilder + */ + private void appendUserNames(StringBuilder sb, List userIds, Map userMap) { + if (sb.length() > 0) { + sb.append("; "); + } + + String userNames = userIds.stream() + .map(userId -> Optional.ofNullable(userMap.get(userId)) + .map(PartUserInfoVo::getRealName) + .orElse(userId)) + .collect(Collectors.joining(",")); + + sb.append("【使用的人员】:").append(userNames); + } + + /** + * 构建负责人集合 + */ + private List buildManagerList(String managerIds, Map userMap) { + List managerIdList = parseIdsToList(managerIds); + if (managerIdList.isEmpty()) { + return Collections.emptyList(); + } + + return managerIdList.stream() + .map(managerId -> buildChargeVo(managerId, userMap)) + .collect(Collectors.toList()); + } + + /** + * 构建负责人 VO + */ + private AttendanceGroupChargeVo buildChargeVo(String managerId, Map userMap) { + String managerName = getUserName(managerId, userMap); + return AttendanceGroupChargeVo.builder() + .managerId(managerId) + .managerName(managerName) + .build(); + } + + /** + * 获取用户姓名 + */ + private String getUserName(String userId, Map userMap) { + String userName = Optional.ofNullable(userId) + .filter(StringUtils::isNotBlank) + .map(userMap::get) + .map(PartUserInfoVo::getRealName) + .orElse(""); + + if (StringUtils.isNotBlank(userId) && StringUtils.isBlank(userName)) { + log.warn("创建人ID: {} 在userMap中未找到对应的用户信息,userMap大小: {}", userId, userMap.size()); + } + + return userName; + } + + /** + * 解析ID列表(逗号分隔) + */ + private List parseIdsToList(String ids) { + if (StringUtils.isBlank(ids)) { + return Collections.emptyList(); + } + + return Arrays.stream(ids.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } + + /** + * 判断是否为组织/用户范围 + */ + private boolean isOrgUserScope(AttendanceBookConfigEntity config) { + return ConstantUtil.SCOPE_ORG_USER.equals(config.getScopeType()); + } + + /** + * 获取使用范围中的用户ID列表 + */ + private List getScopeUserIds(String ruleId) { + MutablePair, List> listListMutablePair = ruleScopeUtil.selectScopeList(ruleId, ScopeBizType.ATTENDANCE_BOOK); + return ruleScopeService.list( + new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, ruleId) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.ATTENDANCE_BOOK.getValue()) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_USER) + ).stream() + .map(AttendanceRuleScope::getScopeValue) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } + + private AttendanceBookConfigEntity getAttendanceBookConfig(AttendanceBookConfigDto configDto) { + AttendanceBookConfigEntity config = new AttendanceBookConfigEntity(); + config.setId(FtbUtil.getId()); + config.setBookName(configDto.getBookName()); + config.setScopeType(configDto.getScopeType()); + config.setManagerIds(configDto.getManagerIds()); + config.setHolidayScopeType(configDto.getHolidayScopeType()); + config.setHolidayIds(configDto.getHolidayIds()); + config.setEnabledMark(configDto.getEnabledMark() != null ? configDto.getEnabledMark() : ConstantUtil.NUM_TRUE); + config.setCreatorUserId(UserProvider.getLoginUserId()); + config.setCreatorTime(DateUtil.getNowDate()); + config.setLastModifyUserId(UserProvider.getLoginUserId()); + config.setLastModifyTime(DateUtil.getNowDate()); + config.setDeleteMark(ConstantUtil.NUM_FALSE); + config.setTenantId(UserProvider.getUser().getTenantId()); + return config; + } + + @Override + public PageInfo getPageList(AttendanceBookConfigQueryDto queryDto) { + log.info("分页查询考勤本配置列表,入参=> bookName: {}, enabledMark: {}, creatorUserIdList: {}, creatorStartTime: {}, creatorEndTime: {}, isManagerFilter: {}", + queryDto.getBookName(), queryDto.getEnabledMark(), queryDto.getCreatorUserIdList(), + queryDto.getCreatorStartTime(), queryDto.getCreatorEndTime(), queryDto.getIsManagerFilter()); + if (queryDto.getPageSize() > 0) { + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookConfigEntity::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(AttendanceBookConfigEntity::getTenantId, UserProvider.getUser().getTenantId()) + .like(StringUtils.isNotBlank(queryDto.getBookName()), + AttendanceBookConfigEntity::getBookName, queryDto.getBookName()) + .eq(queryDto.getEnabledMark() != null, + AttendanceBookConfigEntity::getEnabledMark, queryDto.getEnabledMark()); + + // 创建人集合过滤 + if (CollUtil.isNotEmpty(queryDto.getCreatorUserIdList())) { + queryWrapper.in(AttendanceBookConfigEntity::getCreatorUserId, queryDto.getCreatorUserIdList()); + } + + // 修复时间搜索问题:结束日期应该包含当天的全部时间(直到23:59:59) + if (queryDto.getCreatorStartTime() != null) { + queryWrapper.ge(AttendanceBookConfigEntity::getCreatorTime, queryDto.getCreatorStartTime()); + } + if (queryDto.getCreatorEndTime() != null) { + // 使用Hutool工具类获取当天结束时间(23:59:59) + Date endTime = cn.hutool.core.date.DateUtil.endOfDay(queryDto.getCreatorEndTime()); + queryWrapper.le(AttendanceBookConfigEntity::getCreatorTime, endTime); + } + + // 需求9:按负责人过滤 + if (queryDto.getIsManagerFilter()) { + queryWrapper.eq(AttendanceBookConfigEntity::getEnabledMark, 1); + if (!UserProvider.getUser().getIsAdministrator()) { + String currentUserId = UserProvider.getLoginUserId(); + queryWrapper.apply("F_ManagerIds LIKE {0}", "%" + currentUserId + "%"); + } + } + + queryWrapper.orderByDesc(AttendanceBookConfigEntity::getCreatorTime); + + List entityList = this.list(queryWrapper); + log.info("查询到考勤本配置数量: {}", entityList.size()); + PageInfo pageInfo = new PageInfo<>(entityList); + + // 优化:批量查询所有用户信息和假期信息,避免N+1问题 + List voList = convertToVoListWithBatchQuery(entityList); + + PageInfo result = new PageInfo<>(); + result.setList(voList); + result.setTotal(pageInfo.getTotal()); + result.setPages(pageInfo.getPages()); + result.setPageNum(pageInfo.getPageNum()); + result.setPageSize(pageInfo.getPageSize()); + + return result; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addScopeUsers(AttendanceBookConfigAddUserDto addUserDto) throws HandleException { + String bookConfigId = addUserDto.getBookConfigId(); + List userIdList = addUserDto.getUserIdList(); + + AttendanceBookConfigEntity config = this.getById(bookConfigId); + if (null == config) { + throw new HandleException(AttendanceConstant.ERR_CONFIG_NOT_FOUND); + } + + if (config.getScopeType().equals(ConstantUtil.SCOPE_ALL)) { + throw new HandleException("当前考勤本使用范围为全部,无需添加人员"); + } + + List existingScopes = ruleScopeService.list( + new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, bookConfigId) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.ATTENDANCE_BOOK.getValue()) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_USER) + ); + + Set existingUserIds = existingScopes.stream() + .map(AttendanceRuleScope::getScopeValue) + .collect(Collectors.toSet()); + + List newUserIds = userIdList.stream() + .filter(userId -> !existingUserIds.contains(userId)) + .collect(Collectors.toList()); + + if (newUserIds.isEmpty()) { + throw new HandleException("所有用户已在使用范围中,无需重复添加"); + } + + List newScopeList = newUserIds.stream() + .map(userId -> ruleScopeUtil.getRuleScope( + bookConfigId, + userId, + ConstantUtil.RULE_SCOPE_USER, + ScopeBizType.ATTENDANCE_BOOK + )) + .collect(Collectors.toList()); + + if (!newScopeList.isEmpty()) { + ruleScopeUtil.saveBatch(newScopeList); + } + } + + @Override + public boolean isCurrentUserManager(String bookConfigId) throws HandleException { + AttendanceBookConfigEntity config = this.getById(bookConfigId); + if (null == config) { + throw new HandleException(AttendanceConstant.ERR_CONFIG_NOT_FOUND); + } + + String currentUserId = UserProvider.getLoginUserId(); + + if (StringUtils.isBlank(config.getManagerIds())) { + return false; + } + + List managerIdList = Arrays.asList(config.getManagerIds().split(",")); + return managerIdList.stream() + .map(String::trim) + .anyMatch(managerId -> managerId.equals(currentUserId)); + } + + @Override + public List getValidUsers(GetValidUsersDto dto) { + Integer scopeOrgUser = ConstantUtil.SCOPE_ORG_USER; + if ((dto.getOrganizeList() == null || dto.getOrganizeList().isEmpty()) + && (dto.getUserIdList() == null || dto.getUserIdList().isEmpty())) { + log.warn("组织列表和用户列表都为空"); + scopeOrgUser = ConstantUtil.SCOPE_ALL; + } + // 2. 解析月份,获取该月的起止日期 + Date startDate = new Date(); + Date endDate = new Date(); + if (StringUtil.isNotEmpty(dto.getMonth())) { + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), DateTimeFormatter.ofPattern("yyyy-MM")); + startDate = Date.from(yearMonth.atDay(1).atStartOfDay().atZone(java.time.ZoneId.systemDefault()).toInstant()); + endDate = Date.from(yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(java.time.ZoneId.systemDefault()).toInstant()); + } + List userIds = attendanceUserService.getUserIds( + dto.getOrganizeList(), + dto.getUserIdList(), + startDate, + endDate, + scopeOrgUser + ); + + if (CollUtil.isEmpty(userIds)) { + return List.of(); + } + + try { + List result = userAntifreeze.getStaffRosterListInfoByIds( + userIds + ); + + if (result != null) { + return result.stream() + .map(userBoundVO -> { + PartUserInfoVo partUserInfoVo = new PartUserInfoVo(); + partUserInfoVo.setUserId(userBoundVO.getUserId()); + partUserInfoVo.setRealName(userBoundVO.getRealName()); + partUserInfoVo.setMobilePhone(userBoundVO.getMobilePhone()); + partUserInfoVo.setOrganizeName(userBoundVO.getOrganizeName()); + partUserInfoVo.setPositionName(userBoundVO.getPositionName()); + return partUserInfoVo; + }) + .collect(Collectors.toList()); + } + } catch (Exception e) { + log.error("查询用户详细信息失败", e); + } + + return List.of(); + } + + @Override + public AttendanceResultOptionGroupVo getAttendanceResultOptions(String bookConfigId) throws HandleException { + log.info("获取考勤结果下拉列表(分组),bookConfigId: {}", bookConfigId); + + AttendanceBookConfigEntity config = this.getById(bookConfigId); + if (config == null) { + throw new HandleException(AttendanceConstant.ERR_CONFIG_NOT_FOUND); + } + + // 1. 构建考勤结果选项(basic类型) + List attendanceResults = new ArrayList<>(); + for (AttendanceStatusEnum status : AttendanceStatusEnum.getBasicStatuses()) { + attendanceResults.add(AttendanceResultOptionVo.builder() + .value(String.valueOf(status.getCode())) + .label(status.getName()) + .type(status.getType()) + .build()); + } + + // 2. 构建外勤结果选项(out类型) + List outResults = new ArrayList<>(); + for (AttendanceStatusEnum status : AttendanceStatusEnum.getOutStatuses()) { + outResults.add(AttendanceResultOptionVo.builder() + .value(String.valueOf(status.getCode())) + .label(status.getName()) + .type(status.getType()) + .build()); + } + + // 3. 构建假期类型选项(leave类型,含已删除并返回删除标识) + List leaveTypes = new ArrayList<>(); + if (Objects.equals(config.getHolidayScopeType(), 1)) { + List allLeaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypesIncludeDeleted(null); + leaveTypes.addAll(toLeaveResultOptions(allLeaveTypes)); + } else if (StringUtils.isNotBlank(config.getHolidayIds())) { + List holidayIdList = Arrays.stream(config.getHolidayIds().split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + List voList = attendanceLeaveTypeService.getAttendanceLeaveTypesIncludeDeleted(holidayIdList); + leaveTypes.addAll(toLeaveResultOptions(voList)); + } + + AttendanceResultOptionGroupVo result = AttendanceResultOptionGroupVo.builder() + .attendanceResults(attendanceResults) + .outResults(outResults) + .leaveTypes(leaveTypes) + .build(); + + log.info("获取考勤结果下拉列表成功,bookConfigId: {}, 考勤结果数: {}, 外勤结果数: {}, 假期类型数: {}", + bookConfigId, attendanceResults.size(), outResults.size(), leaveTypes.size()); + return result; + } + + private List toLeaveResultOptions(List leaveTypeList) { + if (CollUtil.isEmpty(leaveTypeList)) { + return Collections.emptyList(); + } + List options = new ArrayList<>(leaveTypeList.size()); + for (AttendanceLeaveType leave : leaveTypeList) { + options.add(AttendanceResultOptionVo.builder() + .value(leave.getId()) + .label(leave.getName()) + .type("leave") + .deleted(Objects.equals(leave.getDeleteMark(), ConstantUtil.NUM_TRUE)) + .build()); + } + return options; + } + + @Override + public List getCreatorList() { + log.info("获取考勤本配置创建人列表"); + + // 1. 查询所有考勤本配置的创建人ID(去重) + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookConfigEntity::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(AttendanceBookConfigEntity::getTenantId, UserProvider.getUser().getTenantId()) + .select(AttendanceBookConfigEntity::getCreatorUserId); + + List configList = this.list(queryWrapper); + if (CollUtil.isEmpty(configList)) { + return Collections.emptyList(); + } + + // 2. 获取所有创建人ID(去重) + List creatorUserIds = configList.stream() + .map(AttendanceBookConfigEntity::getCreatorUserId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (creatorUserIds.isEmpty()) { + return Collections.emptyList(); + } + + // 3. 查询创建人详细信息 + List result = new ArrayList<>(); + try { + List userResult = userAntifreeze.getStaffRosterListInfoByIds( + creatorUserIds); + + if (userResult != null && !userResult.isEmpty()) { + for (PartUserInfoVo user : userResult) { + result.add(BookConfigCreatorVo.builder() + .userId(user.getUserId()) + .userName(user.getRealName()) + .organizeName(user.getOrganizeName()) + .build()); + } + } + } catch (Exception e) { + log.error("查询创建人详细信息失败", e); + } + + log.info("获取考勤本配置创建人列表成功,创建人数: {}", result.size()); + return result; + } + + @Override + public List getBookPersonnelList(String bookConfigId, String month) { + log.info("获取考勤本配置人员列表,bookConfigId: {}", bookConfigId); + + // 1. 查询考勤本配置 + AttendanceBookConfigEntity config = this.getById(bookConfigId); + if (config == null) { + log.warn("考勤本配置不存在,bookConfigId: {}", bookConfigId); + return Collections.emptyList(); + } + + // 2. 获取使用范围 + MutablePair, List> scopeList = ruleScopeUtil.selectScopeList( + bookConfigId, ScopeBizType.ATTENDANCE_BOOK); + if (scopeList == null) { + return Collections.emptyList(); + } + // 2. 解析月份,获取该月的起止日期 + Date startDate = new Date(); + Date endDate = new Date(); + if (StringUtil.isNotEmpty(month)) { + YearMonth yearMonth = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")); + startDate = Date.from(yearMonth.atDay(1).atStartOfDay().atZone(java.time.ZoneId.systemDefault()).toInstant()); + endDate = Date.from(yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(java.time.ZoneId.systemDefault()).toInstant()); + } + // 3. 获取有效用户ID列表 + List userIds = attendanceUserService.getUserIds( + scopeList.getLeft(), + scopeList.getRight(), + startDate, + endDate, + config.getScopeType() + ); + + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + + // 4. 查询用户详细信息 + List userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds); + if (CollUtil.isEmpty(userInfoList)) { + return Collections.emptyList(); + } + + // 5. 构建返回结果 + List result = new ArrayList<>(); + for (PartUserInfoVo userInfo : userInfoList) { + BookPersonnelVo personnelVo = BookPersonnelVo.builder() + .userId(userInfo.getUserId()) + .realName(userInfo.getRealName()) + .organizeName(userInfo.getOrganizeName()) + .positionName(userInfo.getPositionName()) + .mobilePhone(userInfo.getMobilePhone()) + .accountStatus(0) // 默认未封账 + .transferTag("") // 默认无离组借调标签 + .build(); + result.add(personnelVo); + } + + log.info("获取考勤本配置人员列表成功,bookConfigId: {}, 人员数: {}", bookConfigId, result.size()); + return result; + } + + /** + * 更新考勤本时:若已存在考勤记录,则不允许变更使用范围(范围类型、组织、成员) + */ + private void validateScopeChangeWhenRecordsExist(String bookId, Integer oldScope, Integer newScope, + AttendanceBookConfigDto configDto) throws HandleException { + MutablePair, List> oldScopeData = ruleScopeUtil.selectScopeList( + bookId, ScopeBizType.ATTENDANCE_BOOK); + if (!isScopeChanged(oldScope, newScope, oldScopeData, configDto)) { + return; + } + if (hasBookRecords(bookId)) { + throw new HandleException(AttendanceConstant.ERR_BOOK_SCOPE_CHANGE_HAS_RECORDS); + } + } + + /** + * 判断使用范围是否发生变更(范围类型、组织列表、成员列表) + */ + private boolean isScopeChanged(Integer oldScope, Integer newScope, + MutablePair, List> oldScopeData, + AttendanceBookConfigDto configDto) { + if (!Objects.equals(oldScope, newScope)) { + return true; + } + if (ConstantUtil.SCOPE_ALL.equals(newScope)) { + return false; + } + Set oldOrgs = toNormalizedIdSet(oldScopeData != null ? oldScopeData.getLeft() : null); + Set newOrgs = toNormalizedIdSet(configDto.getOrganizeList()); + Set oldUsers = toNormalizedIdSet(oldScopeData != null ? oldScopeData.getRight() : null); + Set newUsers = toNormalizedIdSet(configDto.getUserIdList()); + return !oldOrgs.equals(newOrgs) || !oldUsers.equals(newUsers); + } + + private Set toNormalizedIdSet(List ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptySet(); + } + return ids.stream() + .filter(StringUtils::isNotBlank) + .map(String::trim) + .collect(Collectors.toSet()); + } + + /** + * 考勤本下是否已有考勤记录 + */ + private boolean hasBookRecords(String bookId) { + Long count = attendanceBookRecordMapper.selectCount( + new LambdaQueryWrapper() + .eq(AttendanceBookRecordEntity::getBookId, bookId)); + return count != null && count > 0; + } + + /** + * 需求1:校验考勤本名称是否重复 + * + * @param excludeId 排除的ID(更新时使用,新增时传null) + * @param bookName 考勤本名称 + * @throws HandleException 如果名称重复则抛出异常 + */ + private void validateBookNameDuplicate(String excludeId, String bookName) throws HandleException { + if (StringUtils.isBlank(bookName)) { + return; + } + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookConfigEntity::getBookName, bookName) + .eq(AttendanceBookConfigEntity::getDeleteMark, ConstantUtil.NUM_FALSE); + + // 如果是更新操作,排除当前记录 + if (StringUtils.isNotBlank(excludeId)) { + queryWrapper.ne(AttendanceBookConfigEntity::getId, excludeId); + } + + long count = this.count(queryWrapper); + if (count > 0) { + throw new HandleException(AttendanceConstant.ERR_BOOK_NAME_EXISTS); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookOperationLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookOperationLogServiceImpl.java new file mode 100644 index 0000000..615a6a1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookOperationLogServiceImpl.java @@ -0,0 +1,117 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.attendance.mapper.AttendanceBookOperationLogMapper; +import jnpf.attendance.service.AttendanceBookOperationLogService; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceBookOperationLogEntity; +import jnpf.model.attendance.dto.OperationLogQueryDto; +import jnpf.model.attendance.vo.OperationLogPageVo; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 考勤本操作日志Service实现类 + * + * @author Generated + * @create 2026-04-15 + */ +@Slf4j +@Service +public class AttendanceBookOperationLogServiceImpl extends ServiceImpl implements AttendanceBookOperationLogService { + + @Override + public void saveLog(AttendanceBookOperationLogEntity entity) { + // 参数校验 + if (entity == null) { + log.warn("操作日志实体为空,跳过记录"); + return; + } + + // 自动填充主键 + if (entity.getId() == null || entity.getId().isEmpty()) { + entity.setId(RandomUtil.uuId()); + } + + // 自动填充操作时间 + if (entity.getOperationTime() == null) { + entity.setOperationTime(new Date()); + } + + // 获取当前用户信息 + UserInfo user = UserProvider.getUser(); + if (user != null) { + // 如果没有传入操作人信息,则使用当前登录用户 + if (entity.getOperatorId() == null || entity.getOperatorId().isEmpty()) { + entity.setOperatorId(user.getUserId()); + } + if (entity.getOperatorName() == null || entity.getOperatorName().isEmpty()) { + entity.setOperatorName(user.getUserName()); + } + } + + this.save(entity); + log.info("记录考勤本操作日志成功,日志ID: {}, 考勤本ID: {}, 员工ID: {}", + entity.getId(), entity.getBookId(), entity.getEmployeeId()); + } + + @Override + public Page queryPage(OperationLogQueryDto queryDto) { + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(queryDto.getBookId())) { + queryWrapper.eq(AttendanceBookOperationLogEntity::getBookId, queryDto.getBookId()); + } + if (StringUtils.isNotBlank(queryDto.getMonth())) { + queryWrapper.eq(AttendanceBookOperationLogEntity::getMonth, queryDto.getMonth()); + } + if (StringUtils.isNotBlank(queryDto.getEmployeeId())) { + queryWrapper.eq(AttendanceBookOperationLogEntity::getEmployeeId, queryDto.getEmployeeId()); + } + queryWrapper.orderByDesc(AttendanceBookOperationLogEntity::getOperationTime); + + // 执行分页查询 + Page pageResult = this.page( + new Page<>(queryDto.getPage(), queryDto.getSize()), queryWrapper); + + // 转换为 VO + List voList = pageResult.getRecords().stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + + // 构建返回结果 + Page result = new Page<>(pageResult.getCurrent(), pageResult.getSize(), pageResult.getTotal()); + result.setRecords(voList); + + return result; + } + + /** + * 实体转换为VO + * + * @param entity 实体 + * @return VO + */ + private OperationLogPageVo convertToVo(AttendanceBookOperationLogEntity entity) { + OperationLogPageVo vo = new OperationLogPageVo(); + vo.setId(entity.getId()); + vo.setBookId(entity.getBookId()); + vo.setMonth(entity.getMonth()); + vo.setRecordId(entity.getRecordId()); + vo.setEmployeeId(entity.getEmployeeId()); + vo.setOperationContent(entity.getOperationContent()); + vo.setOperatorId(entity.getOperatorId()); + vo.setOperatorName(entity.getOperatorName()); + vo.setOperationTime(entity.getOperationTime()); + return vo; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookRecordServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookRecordServiceImpl.java new file mode 100644 index 0000000..99a3458 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceBookRecordServiceImpl.java @@ -0,0 +1,2918 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.excel.HeadStyleHandler; +import jnpf.attendance.mapper.AttendanceBookRecordMapper; +import jnpf.attendance.service.*; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.controller.util.ExcelExportTemplate; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceBookConfigEntity; +import jnpf.entity.attendance.AttendanceBookOperationLogEntity; +import jnpf.entity.attendance.AttendanceBookRecordEntity; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.enums.attendance.AttendanceStatusEnum; +import jnpf.enums.attendance.PeriodTypeEnum; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.DateDetail; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import jnpf.util.attendance.AttendanceGroupUserStatusUtil; +import jnpf.util.attendance.RuleScopeUtil; +import jnpf.util.attendance.SecondmentTypeUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URL; +import java.text.Normalizer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 考勤本记录表Service实现类 + * + * 已实现需求: + * - 需求4:导出功能调整(上下半部分显示+班次列) + * - 需求5:导入功能增强(表头/手机号/考勤本名称/假期类型校验) + * - 需求6:请假备注信息列表统计 + * - 需求8:操作日志优化(HTML红色标记状态) + * - 需求10:批量变更功能 + * + * @author Generated + * @create 2026-04-15 + */ +@Slf4j +@Service +public class AttendanceBookRecordServiceImpl extends ServiceImpl implements AttendanceBookRecordService { + + // ==================== 常量定义 ==================== + // 注:所有业务常量已迁移至 AttendanceConstant 类 + + /** + * 导入模板大标题:与导出一致 {@code yyyy年M月 + 考勤本名称 + 考勤本} + */ + private static final Pattern IMPORT_BOOK_TITLE_PATTERN = Pattern.compile("^\\d{4}年\\d{1,2}月(.+)考勤本$"); + + /** + * 考勤状态:中文 → 数字 + */ + private static final Map STATUS_CN_TO_CODE; + + /** + * 考勤状态:数字 → 中文 + */ + private static final Map STATUS_CODE_TO_CN; + + static { + STATUS_CN_TO_CODE = new LinkedHashMap<>(); + for (AttendanceStatusEnum status : AttendanceStatusEnum.values()) { + STATUS_CN_TO_CODE.put(status.getName(), status.getCode()); + } + + STATUS_CODE_TO_CN = new LinkedHashMap<>(); + STATUS_CN_TO_CODE.forEach((cn, code) -> STATUS_CODE_TO_CN.put(code, cn)); + } + + // ==================== 依赖注入 ==================== + + @Resource + private UserAntifreeze userAntifreeze; + + @Resource + private ExcelExportTemplate excelExportTemplate; + + @Resource + private AttendanceBookOperationLogService attendanceBookOperationLogService; + + @Resource + private AttendanceBookConfigService attendanceBookConfigService; + + @Resource + private RuleScopeUtil ruleScopeUtil; + + @Autowired + private AttendanceUserService attendanceUserService; + + @Resource + private AttendanceLeaveTypeService attendanceLeaveTypeService; + + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + public String saveOrUpdateRecord(AttendanceBookRecordDto dto) { + // 构建实体对象 + AttendanceBookRecordEntity entity = new AttendanceBookRecordEntity(); + entity.setId(dto.getId()); + entity.setBookId(dto.getBookId()); + entity.setUserId(dto.getUserId()); + entity.setDay(dto.getDay()); + entity.setPeriodType(dto.getPeriodType()); + entity.setAttendanceStatus(dto.getAttendanceStatus()); + entity.setLeaveType(dto.getLeaveType()); + entity.setRemark(dto.getRemark()); + + // 自动填充月份字段 + if (StringUtils.isBlank(entity.getMonth()) && entity.getDay() != null) { + entity.setMonth(entity.getDay().toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy-MM"))); + } + + boolean clearRequest = isClearRequest(dto); + AttendanceBookRecordEntity existingRecord = resolveExistingRecord(dto); + + // 用户当日不在组:不做新增/修改/清除 + List userGroupUsers = loadUserGroupUsersForDay( + entity.getBookId(), entity.getUserId(), entity.getDay()); + if (!AttendanceGroupUserStatusUtil.isUserInGroupOnDay(userGroupUsers, entity.getDay())) { + log.info("用户当日不在组,跳过保存,userId: {}, day: {}, periodType: {}", + entity.getUserId(), entity.getDay(), entity.getPeriodType()); + return existingRecord != null ? existingRecord.getId() : null; + } + + // 用户当月已封存(锁定):不做新增/修改/清除 + if (isUserSealedForMonth(entity.getUserId(), entity.getMonth())) { + log.info("用户当月已封存,跳过保存,userId: {}, month: {}, periodType: {}", + entity.getUserId(), entity.getMonth(), entity.getPeriodType()); + return existingRecord != null ? existingRecord.getId() : null; + } + + // 获取当前用户信息 + UserInfo user = UserProvider.getUser(); + Date now = new Date(); + + String recordId; + if (clearRequest) { + if (existingRecord == null) { + log.info("清除考勤记录:无对应记录,跳过,userId: {}, day: {}, periodType: {}", + entity.getUserId(), entity.getDay(), entity.getPeriodType()); + return StringUtils.isNotBlank(dto.getId()) ? dto.getId() : null; + } + this.removeById(existingRecord.getId()); + recordId = existingRecord.getId(); + saveOperationLog(existingRecord, existingRecord, AttendanceConstant.LOG_OPERATION_CLEAN); + log.info("清除考勤记录成功,记录ID: {}, 员工ID: {}, 日期: {}, 时段: {}", + recordId, entity.getUserId(), entity.getDay(), entity.getPeriodType()); + } else if (Objects.nonNull(existingRecord)) { + // 记录操作日志(在修改前记录,保留原始状态用于"调整前"展示) + saveOperationLog(entity, existingRecord, AttendanceConstant.LOG_OPERATION_UPDATE); + + // 更新现有记录 + existingRecord.setAttendanceStatus(entity.getAttendanceStatus()); + existingRecord.setLeaveType(entity.getLeaveType()); + existingRecord.setRemark(entity.getRemark()); + existingRecord.setLastModifyUserId(user != null ? user.getUserId() : null); + existingRecord.setLastModifyTime(now); + this.updateById(existingRecord); + recordId = existingRecord.getId(); + log.info("更新考勤记录成功,记录ID: {}, 员工ID: {}, 日期: {}, 时段: {}, 状态: {}", + recordId, entity.getUserId(), entity.getDay(), entity.getPeriodType(), entity.getAttendanceStatus()); + } else { + // 新增记录 + entity.setId(RandomUtil.uuId()); + entity.setCreatorUserId(user != null ? user.getUserId() : null); + entity.setCreatorTime(now); + entity.setTenantId(user != null ? user.getTenantId() : null); + this.save(entity); + recordId = entity.getId(); + log.info("新增考勤记录成功,记录ID: {}, 员工ID: {}, 日期: {}, 时段: {}, 状态: {}", + recordId, entity.getUserId(), entity.getDay(), entity.getPeriodType(), entity.getAttendanceStatus()); + + // 记录操作日志 + saveOperationLog(entity, null, AttendanceConstant.LOG_OPERATION_ADD); + } + + return recordId; + } + + @Override + public PageInfo getMonthStatistics(BookRecordMonthStatisticsDto req) { + + // 2. 查询考勤本配置 + AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(req.getBookId()); + if (bookConfig == null) { + log.warn("考勤本配置不存在,bookId: {}", req.getBookId()); + return new PageInfo<>(); + } + String bookName = bookConfig.getBookName(); + + // 3. 解析月份,获取该月的所有日期列表 + YearMonth yearMonth = YearMonth.parse(req.getMonth(), DateTimeFormatter.ofPattern("yyyy-MM")); + List allDaysInMonth = generateMonthDays(yearMonth); + Date startDate = getMonthStartDate(yearMonth); + Date endDate = getMonthEndDate(yearMonth); + // 4. 获取考勤本使用范围的用户列表 + List attendanceGroupUsersOfSecondment = getBookUserIds(bookConfig, req.getUserIds(), startDate, endDate); + if (CollUtil.isEmpty(attendanceGroupUsersOfSecondment)) { + log.info("考勤本无有效用户,bookId: {}", req.getBookId()); + return new PageInfo<>(); + } + List attendanceGroups = attendanceGroupService.listByIds(attendanceGroupUsersOfSecondment.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList())); + Map attendanceGroupMap = attendanceGroups.stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getGroupName, (existing, replacement) -> replacement)); + Map> attendanceGroupUsersMap = attendanceGroupUsersOfSecondment.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + + List allUserIds = attendanceGroupUsersOfSecondment.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + // 7. 仅查询当前页用户的信息(避免全量查询) + Map userInfoMap = batchQueryUserInfo(allUserIds); + List strings = new ArrayList<>(userInfoMap.keySet()); + // 6. 先对用户ID列表进行分页(allUserIds顺序稳定,直接分页) + List pagedUserIds = paginateUserIds(strings, req.getCurrentPage(), req.getPageSize()); + if (CollUtil.isEmpty(pagedUserIds)) { + log.info("分页后用户列表为空,bookId: {}, page: {}", req.getBookId(), req.getCurrentPage()); + return new PageInfo<>(); + } + + + // 过滤掉查不到用户信息的ID(避免产生空的用户数据) + pagedUserIds = pagedUserIds.stream() + .filter(userInfoMap::containsKey) + .collect(Collectors.toList()); + + Map sealMap = attendanceDayStatisticsService.selectUserIsSeal(pagedUserIds, req.getMonth()); + + // 8. 查询当前页用户的考勤记录 + List records = queryMonthRecords(req.getBookId(), startDate, endDate, pagedUserIds); + Map> recordsByUser = records.stream() + .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId)); + // 5. 查询所有请假类型,构建ID到名称的映射(使用统一方法,自动过滤已删除的) + Map leaveTypeIdToNameMap = getAttendanceLeaveTypeMap(bookConfig, records); + // 9. 遍历当前页用户构建统计结果 + List pageResult = buildMonthStatisticsList( + req.getBookId(), bookName, pagedUserIds, recordsByUser, userInfoMap, allDaysInMonth, leaveTypeIdToNameMap, attendanceGroupUsersMap, attendanceGroupMap, sealMap, + startDate, endDate); + + log.info("获取考勤本月统计成功,考勤本ID: {}, 考勤本名称: {}, 月份: {}, 当前页员工数: {}, 总员工数: {}", + req.getBookId(), bookName, req.getMonth(), pageResult.size(), allUserIds.size()); + + // 10. 构建分页结果(总记录数为所有用户数) + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(pageResult); + pageInfo.setTotal(strings.size()); + pageInfo.setPages((int) Math.ceil((double) strings.size() / req.getPageSize())); + pageInfo.setPageNum(req.getCurrentPage()); + pageInfo.setPageSize(req.getPageSize()); + return pageInfo; + } + @NotNull + private Map getAttendanceLeaveTypeMap(AttendanceBookConfigEntity bookConfig, List recordList) { + List leaveTypeIds = recordList.stream().map(AttendanceBookRecordEntity::getLeaveType).distinct().collect(Collectors.toList()); + return getStringAttendanceLeaveTypeMap(bookConfig, leaveTypeIds); + } + @NotNull + private Map getStringAttendanceLeaveTypeMap(AttendanceBookConfigEntity bookConfig,List leaveTypeIds) { + leaveTypeIds.addAll(Objects.equals(bookConfig.getHolidayScopeType(), 1) + ? CollUtil.newArrayList() + : (StringUtils.isNotBlank(bookConfig.getHolidayIds()) ? Arrays.asList(bookConfig.getHolidayIds().split(",")) : CollUtil.newArrayList())); + List leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypesIncludeDeleted(leaveTypeIds); + return leaveTypes.stream() + .collect(Collectors.toMap(AttendanceLeaveType::getId, Function.identity(), (v1, v2) -> v1)); + } + + /** + * 获取考勤本使用范围的用户ID列表 + *

参考AttendanceBookConfigServiceImpl.getBookPersonnelList方法的逻辑

+ * + * @param bookConfig 考勤本配置 + * @param filterUserIds 筛选的用户ID列表(如果传入,则从范围用户中筛选) + * @param startDate 开始时间(用于获取该时间段内的有效用户) + * @param endDate 结束时间(用于获取该时间段内的有效用户) + * @return 用户ID列表 + */ + private List getBookUserIds(AttendanceBookConfigEntity bookConfig, List filterUserIds, Date startDate, Date endDate) { + // 1. 获取使用范围 + MutablePair, List> scopeList = ruleScopeUtil.selectScopeList( + bookConfig.getId(), ScopeBizType.ATTENDANCE_BOOK); + + // 2. 如果使用范围为"全部"(scopeList为null),则查询所有有效用户 + if (scopeList == null) { + if (!Objects.equals(bookConfig.getScopeType(), 0)) { + log.warn("考勤本使用范围配置异常,bookId: {}, scopeType: {}", bookConfig.getId(), bookConfig.getScopeType()); + return Collections.emptyList(); + } + // scopeType=0 表示全部,查询所有有效用户 + List userIdsAndGroupIds = attendanceUserService.getUserIdsAndGroupIds( + Collections.emptyList(), + Collections.emptyList(), + startDate, + endDate, + bookConfig.getScopeType() + ); + if (CollUtil.isEmpty(userIdsAndGroupIds)) { + log.info("考勤本有效用户列表为空,bookId: {}", bookConfig.getId()); + return Collections.emptyList(); + } + + // 如果传入了筛选用户ID,则进行交集筛选 + if (CollUtil.isNotEmpty(filterUserIds)) { + userIdsAndGroupIds = userIdsAndGroupIds.stream() + .filter(vo -> filterUserIds.contains(vo.getUserId())) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(userIdsAndGroupIds)) { + log.info("筛选后的用户列表为空,bookId: {}", bookConfig.getId()); + return Collections.emptyList(); + } + } + + return userIdsAndGroupIds; + } + + // 3. 指定组织/成员范围:需要同时处理组织和直接指定的用户 + List orgIds = scopeList.getLeft(); + List directUserIds = scopeList.getRight(); + + // 调用 getUserIds 获取组织下的所有人员 + 直接指定的用户 + List userIdsAndGroupIds = attendanceUserService.getUserIdsAndGroupIds( + orgIds, + directUserIds, + startDate, + endDate, + bookConfig.getScopeType() + ); + if (CollUtil.isEmpty(userIdsAndGroupIds)) { + log.info("考勤本有效用户列表为空,bookId: {}", bookConfig.getId()); + return Collections.emptyList(); + } + + // 4. 如果传入了筛选用户ID,则进行交集筛选 + if (CollUtil.isNotEmpty(filterUserIds)) { + userIdsAndGroupIds = userIdsAndGroupIds.stream() + .filter(vo -> filterUserIds.contains(vo.getUserId())) + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(userIdsAndGroupIds)) { + log.info("筛选后的用户列表为空,bookId: {}", bookConfig.getId()); + return Collections.emptyList(); + } + } + + log.info("获取考勤本用户列表成功,bookId: {}, 用户数: {}", bookConfig.getId(), userIdsAndGroupIds.size()); + return userIdsAndGroupIds; + } + + + /** + * 生成本月所有日期列表 + */ + private List generateMonthDays(YearMonth yearMonth) { + List allDays = new ArrayList<>(); + for (int day = 1; day <= yearMonth.lengthOfMonth(); day++) { + allDays.add(yearMonth.atDay(day).format(DateTimeFormatter.ISO_LOCAL_DATE)); + } + return allDays; + } + + /** + * 获取月份起始日期 + */ + private Date getMonthStartDate(YearMonth yearMonth) { + return Date.from(yearMonth.atDay(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * 获取月份结束日期 + */ + private Date getMonthEndDate(YearMonth yearMonth) { + return Date.from(yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * 对用户ID列表进行分页 + */ + private List paginateUserIds(List allUserIds, int pageNum, int pageSize) { + if (CollUtil.isEmpty(allUserIds)) { + return Collections.emptyList(); + } + + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, allUserIds.size()); + + if (fromIndex >= allUserIds.size()) { + return Collections.emptyList(); + } + + return allUserIds.subList(fromIndex, toIndex); + } + + /** + * 查询月份考勤记录 + */ + private List queryMonthRecords(String bookId, Date startDate, + Date endDate, List userIds) { + return lambdaQuery().eq(AttendanceBookRecordEntity::getBookId, bookId) + .between(AttendanceBookRecordEntity::getDay, startDate, endDate) + .in(CollUtil.isNotEmpty(userIds), AttendanceBookRecordEntity::getUserId, userIds) + .orderByAsc(AttendanceBookRecordEntity::getUserId) + .orderByAsc(AttendanceBookRecordEntity::getDay) + .orderByAsc(AttendanceBookRecordEntity::getPeriodType) + .list(); + } + + + /** + * 批量查询用户信息 + */ + private Map batchQueryUserInfo(List userIds) { + List userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds); + return userInfoList.stream() + .collect(Collectors.toMap(PartUserInfoVo::getUserId, user -> user, (u1, u2) -> u1)); + } + + /** + * 构建月度统计列表 + */ + private List buildMonthStatisticsList( + String bookId, + String bookName, + List allUserIds, + Map> recordsByUser, + Map userInfoMap, + List allDaysInMonth, + Map leaveTypeIdToNameMap, + Map> attendanceGroupUsersMap, + Map attendanceGroupMap, + Map sealMap, + Date startDate, + Date endDate) { + + List resultList = new ArrayList<>(); + int sortOrder = 1; + + for (String userId : allUserIds) { + List userRecords = recordsByUser.getOrDefault(userId, new ArrayList<>()); + PartUserInfoVo userInfo = userInfoMap.get(userId); + List attendanceGroupUsers = attendanceGroupUsersMap.getOrDefault(userId, new ArrayList<>()); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + continue; + } + // 构建该用户的统计VO + BookRecordMonthStatisticsVo statisticsVo = buildUserStatistics( + bookId, bookName, userId, userInfo, userRecords, allDaysInMonth, sortOrder++, leaveTypeIdToNameMap, attendanceGroupUsers, attendanceGroupMap, + sealMap.getOrDefault(userId, false), startDate, endDate); + resultList.add(statisticsVo); + } + + return resultList; + } + + /** + * 构建单个用户的月度统计 + */ + private BookRecordMonthStatisticsVo buildUserStatistics( + String bookId, + String bookName, + String userId, + PartUserInfoVo userInfo, + List userRecords, + List allDaysInMonth, + int sortOrder, + Map leaveTypeIdToNameMap, + List attendanceGroupUsers, + Map attendanceGroupMap, + boolean isSeal, + Date startDate, + Date endDate) { + + // 按日期分组记录 + Map> recordsByDay = userRecords.stream() + .collect(Collectors.groupingBy(record -> + record.getDay().toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ISO_LOCAL_DATE))); + + // 初始化统计数据 + MonthlyAttendanceStats stats = new MonthlyAttendanceStats(); + List dailyLatticeList = new ArrayList<>(allDaysInMonth.size()); + + // 遍历所有日期构建格子数据和统计 + for (String day : allDaysInMonth) { + List dayRecords = recordsByDay.getOrDefault(day, new ArrayList<>()); + + // 查找上午和下午记录 + AttendanceBookRecordEntity morningRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.MORNING.getCode()); + AttendanceBookRecordEntity afternoonRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.AFTERNOON.getCode()); + + // 构建格子数据 + dailyLatticeList.add(buildDailyLattice(day, morningRecord, afternoonRecord, attendanceGroupUsers)); + + // 累加统计(仅在有数据时) + if (morningRecord != null) { + stats.accumulate(morningRecord); + } + if (afternoonRecord != null) { + stats.accumulate(afternoonRecord); + } + } + int secondmentType = SecondmentTypeUtil.toSecondmentType(AttendanceGroupUserStatusUtil.isExistStatus(attendanceGroupUsers, startDate, endDate)); + AttendanceGroupUser attendanceGroupUser = attendanceGroupUsers.stream().reduce((groupUser1, groupUser2) -> Objects.equals(groupUser1.getDeleteMark(), 0) ? groupUser1 : Objects.equals(groupUser2.getDeleteMark(), 0) ? groupUser2 : groupUser1.getCreatorTime().after(groupUser2.getCreatorTime()) ? groupUser1 : groupUser2).orElse(null); + + // 用户级「是否离组」与每日格子展示无关:按考勤组成员 removeTime 判定(VO:0否 1是)。多条组关系时优先取仍在组(removeTime 为空)的记录,否则取离组时间最晚的一条。 + // 构建并返回统计VO + return BookRecordMonthStatisticsVo.builder() + .bookId(bookId) + .bookName(bookName) + .userId(userId) + .realName(getValueOrNull(userInfo, PartUserInfoVo::getRealName)) + .secondmentType(secondmentType) + .isOutGroup(Objects.isNull(attendanceGroupUser) ? 1 : attendanceGroupUser.getDeleteMark()) + .organizeName(attendanceGroupMap.getOrDefault(attendanceGroupUser.getGroupId(), getValueOrNull(userInfo, PartUserInfoVo::getOrganizeName))) + .positionName(getValueOrNull(userInfo, PartUserInfoVo::getPositionName)) + .mobilePhone(getValueOrNull(userInfo, PartUserInfoVo::getMobilePhone)) + .sortOrder(sortOrder) + .isSeal(isSeal) + .dailyLatticeList(dailyLatticeList) + .actualAttendDays(stats.actualAttendDays) + .publicHolidayDays(stats.publicHolidayDays) + .leaveDaysByType(stats.toLeaveTypeStatisticList(leaveTypeIdToNameMap)) + .lateDays(stats.lateDays) + .earlyLeaveDays(stats.earlyLeaveDays) + .missingCardDays(stats.missingCardDays) + .absenteeismDays(stats.absenteeismDays) + .businessTripDays(stats.businessTripDays) + .outOfficeDays(stats.outOfficeDays) + .fieldWorkDays(stats.fieldWorkDays) + .actualAttendCount(stats.actualAttendCount) + .publicHolidayCount(stats.publicHolidayCount) + .lateCount(stats.lateCount) + .earlyLeaveCount(stats.earlyLeaveCount) + .missingCardCount(stats.missingCardCount) + .absenteeismCount(stats.absenteeismCount) + .businessTripCount(stats.businessTripCount) + .outOfficeCount(stats.outOfficeCount) + .fieldWorkCount(stats.fieldWorkCount) + .remarkList(stats.remarkList) + .build(); + } + + /** + * 根据时段类型查找记录 + */ + private AttendanceBookRecordEntity findRecordByPeriod(List records, Integer periodType) { + if (CollUtil.isEmpty(records) || periodType == null) { + return null; + } + return records.stream() + .filter(r -> periodType.equals(r.getPeriodType())) + .max(Comparator + .comparing(AttendanceBookRecordEntity::getLastModifyTime, Comparator.nullsLast(Date::compareTo)) + .thenComparing(AttendanceBookRecordEntity::getCreatorTime, Comparator.nullsLast(Date::compareTo)) + .thenComparing(AttendanceBookRecordEntity::getId, Comparator.nullsLast(String::compareTo))) + .orElse(null); + } + + /** + * 构建每日格子数据 + */ + private BookRecordDailyLatticeVo buildDailyLattice(String day, + AttendanceBookRecordEntity morningRecord, + AttendanceBookRecordEntity afternoonRecord, + List attendanceGroupUsers) { + Date date = DateDetail.getStr2Date10(day); + Integer existStatus = AttendanceGroupUserStatusUtil.isExistStatus(attendanceGroupUsers, date); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + return BookRecordDailyLatticeVo.builder() + .day(day) + .isOutGroup(existStatus) + .morningStatus(resolveStatus(morningRecord)) + .morningLeaveType(getValueOrNull(morningRecord, AttendanceBookRecordEntity::getLeaveType)) + .morningChanged(morningRecord != null && morningRecord.getLastModifyTime() != null) + .afternoonStatus(resolveStatus(afternoonRecord)) + .afternoonLeaveType(getValueOrNull(afternoonRecord, AttendanceBookRecordEntity::getLeaveType)) + .afternoonChanged(afternoonRecord != null && afternoonRecord.getLastModifyTime() != null) + .build(); + } + + /** + * 解析每日格子展示用考勤状态: + *

当记录上同时存在 {@code leaveType} 与 {@code attendanceStatus}(例如「调休 + 外出」), + * 应以请假类型为准展示,避免前端按 {@code status} 渲染时把请假类型显示成外出/外勤等基础状态。 + * 与 {@link MonthlyAttendanceStats#accumulate} 的"leaveType 优先"口径保持一致。

+ */ + private Integer resolveStatus(AttendanceBookRecordEntity record) { + if (record == null) { + return null; + } + if (StringUtils.isNotBlank(record.getLeaveType())) { + return null; + } + return record.getAttendanceStatus(); + } + + /** + * 安全获取值(避免空指针) + */ + private R getValueOrNull(T obj, java.util.function.Function getter) { + return obj != null ? getter.apply(obj) : null; + } + + /** + * 月度考勤统计数据(封装统计逻辑) + */ + private static class MonthlyAttendanceStats { + private static final BigDecimal HALF_DAY = new BigDecimal("0.5"); + + // 天数统计(每条半天记录算0.5天) + BigDecimal actualAttendDays = BigDecimal.ZERO; + BigDecimal publicHolidayDays = BigDecimal.ZERO; + Map leaveDaysByTypeMap = new HashMap<>(); + BigDecimal lateDays = BigDecimal.ZERO; + BigDecimal earlyLeaveDays = BigDecimal.ZERO; + BigDecimal missingCardDays = BigDecimal.ZERO; + BigDecimal absenteeismDays = BigDecimal.ZERO; + BigDecimal businessTripDays = BigDecimal.ZERO; + BigDecimal outOfficeDays = BigDecimal.ZERO; + BigDecimal fieldWorkDays = BigDecimal.ZERO; + + // 次数统计(每条记录算1次) + BigDecimal actualAttendCount = BigDecimal.ZERO; + BigDecimal publicHolidayCount = BigDecimal.ZERO; + Map leaveCountByTypeMap = new HashMap<>(); + BigDecimal lateCount = BigDecimal.ZERO; + BigDecimal earlyLeaveCount = BigDecimal.ZERO; + BigDecimal missingCardCount = BigDecimal.ZERO; + BigDecimal absenteeismCount = BigDecimal.ZERO; + BigDecimal businessTripCount = BigDecimal.ZERO; + BigDecimal outOfficeCount = BigDecimal.ZERO; + BigDecimal fieldWorkCount = BigDecimal.ZERO; + + List remarkList = new ArrayList<>(); + + /** + * 累加单条记录的统计数据 + */ + void accumulate(AttendanceBookRecordEntity record) { + Integer status = record.getAttendanceStatus(); + if (Objects.isNull(status) && Objects.isNull(record.getLeaveType())) { + return; + } + + AttendanceStatusEnum statusEnum = AttendanceStatusEnum.getByCode(status); + + // 收集备注 + if (StringUtils.isNotBlank(record.getRemark())) { + remarkList.add(record.getRemark()); + } + String leaveType = record.getLeaveType(); + if (StringUtils.isNotBlank(leaveType)) { + leaveDaysByTypeMap.merge(leaveType, HALF_DAY, BigDecimal::add); + leaveCountByTypeMap.merge(leaveType, BigDecimal.ONE, BigDecimal::add); + log.debug("累加请假: leaveTypeId={}, 天数={}, 次数={}", leaveType, leaveDaysByTypeMap.get(leaveType), leaveCountByTypeMap.get(leaveType)); + return; + } + if (statusEnum == null) { + return; + } + + // 根据状态累加统计 + switch (statusEnum) { + case NORMAL: + actualAttendDays = actualAttendDays.add(HALF_DAY); + actualAttendCount = actualAttendCount.add(BigDecimal.ONE); + break; + case PUBLIC_HOLIDAY: + publicHolidayDays = publicHolidayDays.add(HALF_DAY); + publicHolidayCount = publicHolidayCount.add(BigDecimal.ONE); + break; + case LATE: + lateDays = lateDays.add(HALF_DAY); + lateCount = lateCount.add(BigDecimal.ONE); + break; + case EARLY_LEAVE: + earlyLeaveDays = earlyLeaveDays.add(HALF_DAY); + earlyLeaveCount = earlyLeaveCount.add(BigDecimal.ONE); + break; + case MISSING_CARD: + missingCardDays = missingCardDays.add(HALF_DAY); + missingCardCount = missingCardCount.add(BigDecimal.ONE); + break; + case ABSENT: + absenteeismDays = absenteeismDays.add(HALF_DAY); + absenteeismCount = absenteeismCount.add(BigDecimal.ONE); + break; + case BUSINESS_TRIP: + businessTripDays = businessTripDays.add(HALF_DAY); + businessTripCount = businessTripCount.add(BigDecimal.ONE); + break; + case OUT_OFFICE: + outOfficeDays = outOfficeDays.add(HALF_DAY); + outOfficeCount = outOfficeCount.add(BigDecimal.ONE); + break; + case OUTER: + fieldWorkDays = fieldWorkDays.add(HALF_DAY); + fieldWorkCount = fieldWorkCount.add(BigDecimal.ONE); + break; + default: + break; + } + } + + /** + * 转换为请假类型统计列表 + * 即使没有请假数据,也要返回所有配置的假期类型(天数为0) + */ + List toLeaveTypeStatisticList(Map leaveTypeIdToNameMap) { + log.info("转换请假类型统计: leaveDaysByTypeMap={}, leaveTypeIdToNameMap={}", + leaveDaysByTypeMap, leaveTypeIdToNameMap); + + if (leaveTypeIdToNameMap == null || leaveTypeIdToNameMap.isEmpty()) { + return Collections.emptyList(); + } + + // 遍历所有配置的请假类型,确保都出现在结果中 + return leaveTypeIdToNameMap.entrySet().stream() + .map(entry -> { + String leaveTypeId = entry.getKey(); + AttendanceLeaveType leaveTypeName = entry.getValue(); + // 从统计结果中获取天数,如果没有则为0 + BigDecimal days = leaveDaysByTypeMap.getOrDefault(leaveTypeId, BigDecimal.ZERO); + if (days.compareTo(BigDecimal.ZERO) <= 0 && Objects.equals(leaveTypeName.getDeleteMark(), 1)) { + return null; + } + BigDecimal count = leaveCountByTypeMap.getOrDefault(leaveTypeId, BigDecimal.ZERO); + + return LeaveTypeStatisticVo.builder() + .leaveType(leaveTypeName.getName()) + .days(days) + .count(count) + .build(); + }).filter(Objects::nonNull) + .collect(Collectors.toList()); + } + } + + @Override + public void bookRecordImport(SchedulesImportDto importDto) throws IOException { + log.info("开始导入考勤本数据,bookId: {}, month: {}", importDto.getBookId(), importDto.getMonth()); + + // 1. 校验考勤本配置 + AttendanceBookConfigEntity config = validateBookConfig(importDto.getBookId()); + + // 2. 解析月份信息 + YearMonth yearMonth = parseYearMonth(importDto.getMonth()); + int daysInMonth = yearMonth.lengthOfMonth(); + Date startDate = getMonthStartDate(yearMonth); + Date endDate = getMonthEndDate(yearMonth); + + // 3. 获取考勤本使用范围的用户ID与考勤组ID(一次查询,复用于手机号映射与按天在组判断) + List bookUsers = getBookUserIds(config, null, startDate, endDate); + List bookUserIds = bookUsers.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + // 4. 构建手机号→用户信息映射 + Map phoneUserMap = buildPhoneUserMap(bookUserIds); + + // 5. 构建用户ID→当月考勤组成员关系(用于按天判断用户是否在组) + Map> userGroupUsersMap = bookUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + + // 5.1 查询当月已封存用户(封存月份不做导入,直接过滤) + Map sealMap = CollUtil.isEmpty(bookUserIds) + ? Collections.emptyMap() + : attendanceDayStatisticsService.selectUserIsSeal(bookUserIds.stream().distinct().collect(Collectors.toList()), importDto.getMonth()); + + // 6. 查询所有请假类型,构建名称→ID映射(用于导入时识别请假类型) + List leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypes(null); + Map leaveTypeNameToIdMap = leaveTypes.stream() + .collect(Collectors.toMap(AttendanceLeaveType::getName, AttendanceLeaveType::getId, (v1, v2) -> v1)); + Map leaveTypeIdToNameMap = leaveTypes.stream() + .collect(Collectors.toMap(AttendanceLeaveType::getId, AttendanceLeaveType::getName, (v1, v2) -> v1)); + + // 7. 读取并解析Excel数据 + ImportParseResult parseResult = parseExcelFile(importDto.getFileUrl(), phoneUserMap, yearMonth, daysInMonth, importDto.getMonth(), leaveTypeNameToIdMap, config.getBookName()); + + // 8. 校验解析结果 + validateParseResult(parseResult); + + // 9. 过滤掉用户当日不在考勤组的记录(不导入这部分数据) + List filteredImportData = filterImportDataByGroupMembership( + parseResult.getImportData(), userGroupUsersMap); + + // 9.1 过滤掉当月已封存用户的记录(封存月份不可变更考勤本数据) + filteredImportData = filterImportDataByUserSeal(filteredImportData, sealMap); + + // 10. 批量处理导入数据 + batchProcessImportData(filteredImportData, importDto.getBookId(), leaveTypeIdToNameMap); + + log.info("考勤本数据导入完成,bookId: {}, month: {}", importDto.getBookId(), importDto.getMonth()); + } + + /** + * 校验考勤本配置 + */ + private AttendanceBookConfigEntity validateBookConfig(String bookId) { + AttendanceBookConfigEntity config = attendanceBookConfigService.getById(bookId); + Assert.notNull(config, AttendanceConstant.ERR_IMPORT_CONFIG_NOT_FOUND); + Assert.isFalse(Objects.equals(config.getEnabledMark(), 0), AttendanceConstant.ERR_IMPORT_CONFIG_DISABLED); + return config; + } + + /** + * 解析年月 + */ + private YearMonth parseYearMonth(String month) { + return YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")); + } + + /** + * 导入比对:去除 BOM、零宽字符、全角/兼容区等与导出/Excel 不一致导致的假「不匹配」 + */ + private static String normalizeImportBookComparableText(String s) { + if (s == null) { + return ""; + } + String t = s.trim(); + if (t.startsWith("\uFEFF")) { + t = t.substring(1).trim(); + } + t = Normalizer.normalize(t, Normalizer.Form.NFKC); + return t.replace("\u200B", "").replace("\u200C", "").replace("\u200D", "").trim(); + } + + /** + * Excel 大标题是否与当前考勤本名称一致(支持从「yyyy年M月+名称+考勤本」解析嵌入名称) + */ + private static boolean importExcelTitleMatchesConfiguredBook(String titleRaw, String configBookNameRaw) { + String expected = normalizeImportBookComparableText(configBookNameRaw); + String title = normalizeImportBookComparableText(titleRaw); + if (StringUtils.isBlank(title) || StringUtils.isBlank(expected)) { + return true; + } + if (title.contains(expected) || expected.contains(title)) { + return true; + } + Matcher m = IMPORT_BOOK_TITLE_PATTERN.matcher(title); + if (m.matches()) { + String embedded = normalizeImportBookComparableText(m.group(1)); + if (StringUtils.isNotBlank(embedded)) { + return embedded.equals(expected) || embedded.contains(expected) || expected.contains(embedded); + } + } + return false; + } + + /** + * 构建手机号到用户信息的映射 + * + * @param userIds 考勤本使用范围内的用户ID集合(已由调用方计算) + */ + private Map buildPhoneUserMap(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyMap(); + } + + // 查询用户信息并构建映射(过滤掉无手机号的用户) + List userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds); + return userInfoList.stream() + .filter(u -> StringUtils.isNotBlank(u.getMobilePhone())) + .collect(Collectors.toMap(PartUserInfoVo::getMobilePhone, u -> u, (u1, u2) -> u1)); + } + + /** + * 过滤掉用户在某天不在考勤组的记录(不导入这部分数据) + * + *

按 (userId, day) 缓存判断结果,避免「上半天/下半天」对同一日重复计算。 + * existStatus = 1 视为在组;3(部分被借调)、4(借调)按业务约定也按 1 处理。

+ * + * @param importData 解析得到的待导入记录 + * @param userGroupUsersMap 用户ID → 当月考勤组成员关系 + * @return 仅保留「用户当日在组」的记录 + */ + private List filterImportDataByGroupMembership( + List importData, + Map> userGroupUsersMap) { + if (CollUtil.isEmpty(importData)) { + return importData; + } + Map inGroupCache = new HashMap<>(); + List kept = new ArrayList<>(importData.size()); + int skippedCount = 0; + for (AttendanceBookRecordDto dto : importData) { + String userId = dto.getUserId(); + Date day = dto.getDay(); + if (userId == null || day == null) { + kept.add(dto); + continue; + } + String cacheKey = userId + "_" + day.getTime(); + Boolean inGroup = inGroupCache.computeIfAbsent(cacheKey, k -> + AttendanceGroupUserStatusUtil.isUserInGroupOnDay( + userGroupUsersMap.getOrDefault(userId, Collections.emptyList()), day)); + if (inGroup) { + kept.add(dto); + } else { + skippedCount++; + } + } + if (skippedCount > 0) { + log.info("考勤本导入:用户当日不在组,已跳过 {} 条记录(共解析 {} 条)", + skippedCount, importData.size()); + } + return kept; + } + + /** + * 加载用户在指定考勤本、指定日期的考勤组成员关系(用于在组判断) + */ + private List loadUserGroupUsersForDay(String bookId, String userId, Date day) { + if (StringUtils.isAnyBlank(bookId, userId) || day == null) { + return Collections.emptyList(); + } + AttendanceBookConfigEntity config = attendanceBookConfigService.getById(bookId); + if (config == null) { + return Collections.emptyList(); + } + Date start = DateUtil.beginOfDay(day); + Date end = DateUtil.endOfDay(day); + List scopeUsers = getBookUserIds(config, Collections.singletonList(userId), start, end); + if (CollUtil.isEmpty(scopeUsers)) { + return Collections.emptyList(); + } + return scopeUsers; + } + + /** + * 按批量记录涉及的用户与日期范围,构建用户ID → 考勤组成员关系映射 + */ + private Map> buildUserGroupUsersMapForRecords( + String bookId, List records) { + if (StringUtils.isBlank(bookId) || CollUtil.isEmpty(records)) { + return Collections.emptyMap(); + } + AttendanceBookConfigEntity config = attendanceBookConfigService.getById(bookId); + if (config == null) { + return Collections.emptyMap(); + } + Set userIds = records.stream() + .map(AttendanceBookRecordDto::getUserId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + if (userIds.isEmpty()) { + return Collections.emptyMap(); + } + Optional minDay = records.stream() + .map(AttendanceBookRecordDto::getDay) + .filter(Objects::nonNull) + .min(Date::compareTo); + Optional maxDay = records.stream() + .map(AttendanceBookRecordDto::getDay) + .filter(Objects::nonNull) + .max(Date::compareTo); + if (minDay.isEmpty() || maxDay.isEmpty()) { + return Collections.emptyMap(); + } + Date start = DateUtil.beginOfDay(minDay.get()); + Date end = DateUtil.endOfDay(maxDay.get()); + List bookUsers = getBookUserIds(config, new ArrayList<>(userIds), start, end); + if (CollUtil.isEmpty(bookUsers)) { + return Collections.emptyMap(); + } + return bookUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + } + + /** + * 从考勤记录 DTO 解析月份(yyyy-MM) + */ + private String resolveRecordMonth(AttendanceBookRecordDto dto) { + if (dto == null) { + return null; + } + return resolveMonthFromDay(dto.getDay()); + } + + /** + * 从日期解析月份(yyyy-MM) + */ + private String resolveMonthFromDay(Date day) { + if (day == null) { + return null; + } + return day.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy-MM")); + } + + /** + * 判断用户在该自然月是否已封存(锁定) + */ + private boolean isUserSealedForMonth(String userId, String month) { + if (StringUtils.isAnyBlank(userId, month)) { + return false; + } + Map sealMap = attendanceDayStatisticsService.selectUserIsSeal( + Collections.singletonList(userId), month); + return Boolean.TRUE.equals(sealMap.get(userId)); + } + + /** + * 按批量记录涉及的用户与月份,构建「用户+月份」封存缓存(key: userId|yyyy-MM) + */ + private Map buildUserMonthSealMap(List records) { + if (CollUtil.isEmpty(records)) { + return Collections.emptyMap(); + } + Map> monthUserIds = new LinkedHashMap<>(); + for (AttendanceBookRecordDto record : records) { + String month = resolveRecordMonth(record); + if (StringUtils.isBlank(month) || StringUtils.isBlank(record.getUserId())) { + continue; + } + monthUserIds.computeIfAbsent(month, k -> new LinkedHashSet<>()).add(record.getUserId()); + } + Map sealedCache = new HashMap<>(); + for (Map.Entry> entry : monthUserIds.entrySet()) { + Map sealMap = attendanceDayStatisticsService.selectUserIsSeal( + new ArrayList<>(entry.getValue()), entry.getKey()); + for (String userId : entry.getValue()) { + if (Boolean.TRUE.equals(sealMap.get(userId))) { + sealedCache.put(buildUserMonthSealKey(userId, entry.getKey()), Boolean.TRUE); + } + } + } + return sealedCache; + } + + private String buildUserMonthSealKey(String userId, String month) { + return userId + "|" + month; + } + + private boolean isUserSealedInCache(String userId, String month, Map userMonthSealMap) { + if (StringUtils.isAnyBlank(userId, month) || CollUtil.isEmpty(userMonthSealMap)) { + return false; + } + return Boolean.TRUE.equals(userMonthSealMap.get(buildUserMonthSealKey(userId, month))); + } + + /** + * 过滤掉当月已封存用户的导入记录(封存月份不做导入) + * + * @param importData 待导入记录 + * @param sealMap 用户ID → 是否封存({@link AttendanceDayStatisticsService#selectUserIsSeal}) + */ + private List filterImportDataByUserSeal( + List importData, + Map sealMap) { + if (CollUtil.isEmpty(importData) || CollUtil.isEmpty(sealMap)) { + return importData; + } + List kept = new ArrayList<>(importData.size()); + int skippedCount = 0; + for (AttendanceBookRecordDto dto : importData) { + String userId = dto.getUserId(); + if (StringUtils.isNotBlank(userId) && Boolean.TRUE.equals(sealMap.get(userId))) { + skippedCount++; + continue; + } + kept.add(dto); + } + if (skippedCount > 0) { + log.info("考勤本导入:用户当月已封存,已跳过 {} 条记录(共 {} 条)", + skippedCount, importData.size()); + } + return kept; + } + + /** + * 解析Excel文件 + */ + private ImportParseResult parseExcelFile(String fileUrl, Map phoneUserMap, + YearMonth yearMonth, int daysInMonth, String month, + Map leaveTypeNameToIdMap, String bookName) throws IOException { + ImportParseResult result = new ImportParseResult(); + List importData = new ArrayList<>(); + List failMessages = new ArrayList<>(); + // 使用 phone+period 组合键去重,支持导出的2行/人格式(上半天、下半天各一行) + Set seenPhonePeriods = new HashSet<>(); + // 超过 500 行数据时只提示一次 + final boolean[] rowLimitExceeded = {false}; + final List dateHeaders = new ArrayList<>(); + // 跨行保留「上半天」行的有效手机号,用于「下半天」行因合并单元格而读取为空时兜底 + final String[] lastValidMobilePhone = {null}; + + EasyExcel.read(new URL(fileUrl).openStream(), new AnalysisEventListener>() { + private List headers; + // 已处理的数据行数(不含表头)。headRowNumber(3) 时 invoke() 只对数据行回调,故等价于"第几条数据" + private int dataRowCount = 0; + // 表头日期列只在首次进入数据行时校验一次 + private boolean dateHeadersValidated = false; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + // 按列号排序,避免依赖 Map.values() 的迭代顺序(headRowNumber(3) 会多次回调,最后一次为列名行) + headers = headMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .collect(Collectors.toList()); + } + + @Override + public void invoke(Map data, AnalysisContext context) { + dataRowCount++; + try { + // 首条数据行进入时一次性校验表头日期列(headers 已在 invokeHeadMap 最后一次回调时填充为列名行) + if (!dateHeadersValidated) { + dateHeadersValidated = true; + int dateStartIndex = getDateStartIndex(headers); + for (int i = dateStartIndex; i < headers.size(); i++) { + String header = headers.get(i); + if (header != null && header.matches(".*\\d+.*")) { + dateHeaders.add(header); + } + } + if (!validateDateHeaders(dateHeaders, yearMonth, month)) { + failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_HEADER_DATE_MISMATCH, month)); + // 表头不一致直接终止本行解析,避免后续按错误日期列误判数据 + return; + } + } + + // 单表最多 500 行数据(不含表头) + if (dataRowCount > 500) { + if (!rowLimitExceeded[0]) { + failMessages.add(AttendanceConstant.ERR_IMPORT_EXCEED_MAX_ROWS); + rowLimitExceeded[0] = true; + } + return; + } + + parseRowData(data, dataRowCount, headers, phoneUserMap, yearMonth, daysInMonth, month, bookName, + seenPhonePeriods, importData, failMessages, leaveTypeNameToIdMap, lastValidMobilePhone); + } catch (Exception e) { + failMessages.add(AttendanceConstant.ERR_IMPORT_DATA_PARSE_FAILED + " - " + e.getMessage()); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + if (importData.isEmpty() && failMessages.isEmpty()) { + failMessages.add(AttendanceConstant.ERR_IMPORT_DATA_EMPTY); + } + log.info("Excel读取完成,数据行数:{}", dataRowCount); + } + }).sheet().headRowNumber(3).doRead(); + + result.setImportData(importData); + result.setFailMessages(failMessages); + return result; + } + + /** + * 校验表头日期是否与月份匹配 + */ + private boolean validateDateHeaders(List dateHeaders, YearMonth yearMonth, String month) { + if (CollUtil.isEmpty(dateHeaders)) { + return false; + } + int maxDay = yearMonth.lengthOfMonth(); + boolean any = false; + // 表头日期列须均为当月有效「日」:任一列解析出的日不在 1..当月天数 则视为与所选月份不一致 + for (String header : dateHeaders) { + if (StringUtils.isBlank(header)) { + continue; + } + Matcher dm = Pattern.compile("(\\d+)").matcher(header); + if (!dm.find()) { + continue; + } + try { + int day = Integer.parseInt(dm.group(1)); + if (day < 1 || day > maxDay) { + return false; + } + any = true; + } catch (NumberFormatException e) { + return false; + } + } + return any; + } + + /** + * 解析单行数据 + * + * @param dataRowNum 当前数据行序号(不含表头,从 1 开始) + * @param lastValidMobilePhone 跨行保留的「上半天」行手机号,用于「下半天」行因合并单元格读取为空时兜底(长度为 1 的数组) + */ + private void parseRowData(Map data, int dataRowNum, List headers, + Map phoneUserMap, YearMonth yearMonth, + int daysInMonth, String month, String configBookName, Set seenPhonePeriods, + List importData, List failMessages, + Map leaveTypeNameToIdMap, String[] lastValidMobilePhone) { + // 防御性跳过:模板里若误把"序号/手机号"列名残留到数据区,按表头行处理 + String mobilePhoneRaw = data.getOrDefault(2, null); + if ("手机号".equals(mobilePhoneRaw) || "序号".equals(mobilePhoneRaw)) { + return; + } + + int dateStartIndex = getDateStartIndex(headers); + boolean hasPeriodColumn = (dateStartIndex == 4); + String periodStr = hasPeriodColumn ? data.getOrDefault(3, null) : null; + // 「下半天」行的前三列(考勤本名称、序号、手机号)在导出模板里是合并单元格,EasyExcel 解析时仅首行(上半天)有值 + boolean isMergedAfternoonRow = hasPeriodColumn && AttendanceConstant.PERIOD_AFTERNOON.equals(periodStr); + + // 考勤本名称(首列):与所选考勤本 ID 对应的配置名称比对;「下」行合并单元格常导致首列为空,仅上半天行强制填名称 + String bookNameCell = normalizeImportBookComparableText(data.getOrDefault(0, null)); + boolean skipBookNameCellChecks = isMergedAfternoonRow && StringUtils.isBlank(bookNameCell); + if (!skipBookNameCellChecks) { + if (StringUtils.isBlank(bookNameCell)) { + failMessages.add(AttendanceConstant.ERR_IMPORT_BOOK_NAME_EMPTY); + return; + } + if (StringUtils.isNotBlank(configBookName) + && !StringUtils.equals(bookNameCell, configBookName)) { + failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_BOOK_NAME_ROW_MISMATCH, + StringUtils.trimToEmpty(configBookName))); + return; + } + } + + // 解析手机号(data 第 3 列,索引为 2) + // 「下半天」行手机号常因合并单元格而为空,沿用上一条「上半天」行的有效手机号 + String mobilePhone = mobilePhoneRaw; + if (StringUtils.isBlank(mobilePhone)) { + if (isMergedAfternoonRow && StringUtils.isNotBlank(lastValidMobilePhone[0])) { + mobilePhone = lastValidMobilePhone[0]; + } else { + failMessages.add(AttendanceConstant.ERR_IMPORT_PHONE_EMPTY); + return; + } + } else { + // 更新跨行状态,供紧跟其后的「下半天」行复用 + lastValidMobilePhone[0] = mobilePhone; + } + + Integer rowPeriodType = null; + + if (hasPeriodColumn) { + // 2行/人格式:从"班次"列读取时段类型 + if (AttendanceConstant.PERIOD_MORNING.equals(periodStr)) { + rowPeriodType = PeriodTypeEnum.MORNING.getCode(); + } else if (AttendanceConstant.PERIOD_AFTERNOON.equals(periodStr)) { + rowPeriodType = PeriodTypeEnum.AFTERNOON.getCode(); + } + // 使用 phone+period 组合去重,允许同一手机号出现两次(上/下各一次) + String phonePeriodKey = mobilePhone + "_" + (periodStr != null ? periodStr : ""); + if (!AttendanceConstant.PERIOD_AFTERNOON.equals(periodStr) && !seenPhonePeriods.add(phonePeriodKey)) { + failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_PHONE_DUPLICATE, mobilePhone)); + return; + } + } else { + // 1行/人格式:手机号不能重复 + if (!seenPhonePeriods.add(mobilePhone)) { + failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_PHONE_DUPLICATE, mobilePhone)); + return; + } + } + + // 查找用户信息 + PartUserInfoVo userInfo = phoneUserMap.get(mobilePhone); + if (userInfo == null) { + failMessages.add(String.format(AttendanceConstant.ERR_IMPORT_PHONE_NOT_FOUND, mobilePhone)); + return; + } + String userId = userInfo.getUserId(); + + // 解析日期数据列 + parseDateColumns(data, headers, dateStartIndex, userId, yearMonth, + daysInMonth, month, importData, failMessages, leaveTypeNameToIdMap, rowPeriodType); + } + + + /** + * 获取日期数据起始列索引 + */ + private int getDateStartIndex(List headers) { + int dateStartIndex = 3; + if (headers.size() > 3 && AttendanceConstant.HEADER_SHIFT.equals(headers.get(3))) { + dateStartIndex = 4; + } + return dateStartIndex; + } + + /** + * 解析日期列数据 + * + * @param rowPeriodType 行指定的时段类型,非null时仅生成该时段记录;null时生成上午和下午两条记录 + */ + private void parseDateColumns(Map data, List headers, + int dateStartIndex, String userId, YearMonth yearMonth, + int daysInMonth, String month, List importData, + List failMessages, Map leaveTypeNameToIdMap, + Integer rowPeriodType) { + for (int i = dateStartIndex; i < headers.size() && i < data.size(); i++) { + String statusStr = data.get(i); + if (StringUtils.isBlank(statusStr)) { + continue; + } + + // 校验考勤状态或请假类型(严格按优先级匹配) + Integer status = STATUS_CN_TO_CODE.get(statusStr); + String leaveTypeId = null; + if (status == null) { + // 优先级2:不是标准考勤状态,尝试匹配请假类型名称 + leaveTypeId = leaveTypeNameToIdMap.get(statusStr); + if (leaveTypeId == null) { + failMessages.add(String.format("%s:" + AttendanceConstant.ERR_IMPORT_STATUS_INVALID, + headers.get(i), statusStr)); + continue; + } + } + + // 解析日期 + String header = headers.get(i); + String dayNum = header.replaceAll("[^0-9]", ""); + if (StringUtils.isBlank(dayNum)) { + continue; + } + + try { + int day = Integer.parseInt(dayNum); + // 校验日期范围 + if (day < 1 || day > daysInMonth) { + failMessages.add(String.format("%s:" + AttendanceConstant.ERR_IMPORT_DATE_OUT_OF_RANGE, header, month)); + continue; + } + + // 构建记录 + LocalDate localDate = LocalDate.of(yearMonth.getYear(), yearMonth.getMonthValue(), day); + Date dayDate = Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + + if (rowPeriodType != null) { + // 2行/人格式:仅生成当前行指定时段的记录 + importData.add(buildRecord(userId, dayDate, month, rowPeriodType, status, leaveTypeId)); + } else { + // 1行/人格式:同时生成上午和下午记录 + importData.add(buildRecord(userId, dayDate, month, PeriodTypeEnum.MORNING.getCode(), status, leaveTypeId)); + importData.add(buildRecord(userId, dayDate, month, PeriodTypeEnum.AFTERNOON.getCode(), status, leaveTypeId)); + } + } catch (NumberFormatException e) { + failMessages.add(String.format("%s:" + AttendanceConstant.ERR_IMPORT_DATE_PARSE_FAILED, header)); + } + } + } + + /** + * 构建考勤记录实体 + */ + private AttendanceBookRecordDto buildRecord(String userId, Date dayDate, String month, + Integer periodType, Integer status, String leaveTypeId) { + return AttendanceBookRecordDto.builder() + .userId(userId) + .day(dayDate) + .periodType(periodType) + .attendanceStatus(status) + .leaveType(leaveTypeId) + .build(); + } + + /** + * 校验解析结果 + */ + private void validateParseResult(ImportParseResult parseResult) { + if (!parseResult.getFailMessages().isEmpty()) { + String firstError = parseResult.getFailMessages().get(0); + throw new RuntimeException(AttendanceConstant.ERR_IMPORT_FAILED_PREFIX + firstError); + } + } + + /** + * 批量处理导入数据 + */ + private void batchProcessImportData(List importData, String bookId, Map leaveTypeNameToIdMap) { + if (CollUtil.isEmpty(importData)) { + log.info("没有需要导入的数据"); + return; + } + + // 获取当前用户信息 + UserInfo user = UserProvider.getUser(); + Date now = new Date(); + + // 批量查询已存在的记录 + Map existingRecordMap = batchQueryExistingRecords( + importData.stream() + .map(record -> { + AttendanceBookRecordDto dto = new AttendanceBookRecordDto(); + dto.setBookId(bookId); + dto.setUserId(record.getUserId()); + dto.setDay(record.getDay()); + dto.setPeriodType(record.getPeriodType()); + return dto; + }) + .collect(Collectors.toList()), bookId + ); + + List toInsert = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + List toClear = new ArrayList<>(); + // 批量收集操作日志,最后统一插入 + List operationLogs = new ArrayList<>(); + for (AttendanceBookRecordDto record : importData) { + String uniqueKey = buildUniqueKey(bookId, record.getUserId(), + record.getDay(), record.getPeriodType()); + record.setBookId(bookId); + AttendanceBookRecordEntity existingRecord = existingRecordMap.get(uniqueKey); + + if (existingRecord != null && (Objects.nonNull(record.getAttendanceStatus()) || Objects.nonNull(record.getLeaveType()))) { + // 收集操作日志,不立即插入数据库 + operationLogs.add(buildOperationLogEntity(record, existingRecord, AttendanceConstant.LOG_OPERATION_UPDATE, leaveTypeNameToIdMap)); + // 更新现有记录 + existingRecord.setAttendanceStatus(record.getAttendanceStatus()); + existingRecord.setLeaveType(record.getLeaveType()); + existingRecord.setLastModifyUserId(user != null ? user.getUserId() : null); + existingRecord.setLastModifyTime(now); + toUpdate.add(existingRecord); + + continue; + } + //清空数据 + if (existingRecord != null) { + toClear.add(existingRecord); + // 收集操作日志,不立即插入数据库 + operationLogs.add(buildOperationLogEntity(null, existingRecord, AttendanceConstant.LOG_OPERATION_CLEAN, leaveTypeNameToIdMap)); + continue; + } + // 新增记录 + AttendanceBookRecordEntity newEntity = buildEntityFromDto(record); + newEntity.setId(RandomUtil.uuId()); + newEntity.setBookId(bookId); + newEntity.setMonth(DateUtil.format(record.getDay(), "yyyy-MM")); + newEntity.setCreatorUserId(user != null ? user.getUserId() : null); + newEntity.setCreatorTime(now); + newEntity.setTenantId(user != null ? user.getTenantId() : null); + toInsert.add(newEntity); + // 收集操作日志,不立即插入数据库 + operationLogs.add(buildOperationLogEntity(record, null, AttendanceConstant.LOG_OPERATION_ADD, leaveTypeNameToIdMap)); + } + + // 批量插入和更新 + if (!toInsert.isEmpty()) { + this.saveBatch(toInsert); + } + if (!toUpdate.isEmpty()) { + this.updateBatchById(toUpdate); + } + if (!toClear.isEmpty()) { + this.removeBatchByIds(toClear); + } + // 批量插入操作日志 + if (!operationLogs.isEmpty()) { + attendanceBookOperationLogService.saveBatch(operationLogs); + log.info("批量插入操作日志成功,数量: {}", operationLogs.size()); + } + log.info("考勤本导入成功,共处理{}条记录(新增: {}, 更新: {})", + importData.size(), toInsert.size(), toUpdate.size()); + } + + /** + * 导入解析结果 + */ + private static class ImportParseResult { + private List importData; + private List failMessages; + + public List getImportData() { + return importData; + } + + public void setImportData(List importData) { + this.importData = importData; + } + + public List getFailMessages() { + return failMessages; + } + + public void setFailMessages(List failMessages) { + this.failMessages = failMessages; + } + } + + @Override + /** + * 导出考勤本记录 + * + * 导出格式(参考划线排班): + * - 3层表头:标题、说明、表头名称 + * - 每个员工2行:上半天和下半天 + * - 前3列合并(考勤本名称、姓名、手机号) + * - 第4列显示"上"或"下" + * + * @param bookId 考勤本ID + * @param month 月份(yyyy-MM) + */ + public void bookRecordExport(String bookId, String month) { + log.info("开始导出考勤本数据,bookId: {}, month: {}", bookId, month); + // 1. 查询基础数据 + AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(bookId); + Assert.notNull(bookConfig, "当前考勤本不存在"); + Date start = DateDetail.getMonthBeginDate(month); + Date end = DateDetail.getMonthEndDate(month); + List dates = DateDetail.getDatesByPeriod(start, end); + List records = queryBookRecords(bookId, start, end); + + // 2. 获取考勤本使用范围内的所有用户(即使没有考勤记录也要导出) + List allUserIds = getAllUserIdsFromBook(bookConfig, start, end); + + // 3. 数据分组 + Map> recordsByUser = records.stream() + .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId)); + // 3.1 批量查询请假类型,构建ID到名称的映射 + Map leaveTypeIdToNameMap = getAttendanceLeaveTypeMap(bookConfig, records); + Map userInfoMap = getUserInfoMap(allUserIds); + List strings = new ArrayList<>(userInfoMap.keySet()); + String titleHead = DateUtil.format(start, "yyyy年MM月") + bookConfig.getBookName() + "考勤本"; + // 4. 构建导出配置 + List> heads = buildExportHeaders(titleHead, dates, leaveTypeIdToNameMap.values().stream().filter(vo -> Objects.equals(vo.getDeleteMark(), 0)).map(AttendanceLeaveType::getName).distinct().collect(Collectors.toList())); + List mergeCells = buildMergeCells(strings.size()); + + // 5. 创建Handler并导出 + jnpf.attendance.excel.AttendanceBookMergeHandler mergeHandler = + new jnpf.attendance.excel.AttendanceBookMergeHandler( + mergeCells, 3, strings.size() * 2, heads.size()); + + jnpf.attendance.excel.CompositeWriteHandler compositeHandler = + new jnpf.attendance.excel.CompositeWriteHandler(new HeadStyleHandler(), mergeHandler); + + excelExportTemplate.exportModule(UserProvider.getLoginUserId(), titleHead, BookRecordMonthStatisticsVo.class, + heads, null, null, 2000, () -> compositeHandler, + (page, size) -> buildExportData(recordsByUser, userInfoMap, bookConfig.getBookName(), dates, strings, leaveTypeIdToNameMap)); + + log.info("考勤本导出完成,员工数: {}", strings.size()); + } + + + /** + * 获取考勤本使用范围内的所有用户ID + * 包括:已有考勤记录的用户 + 考勤本配置的使用范围内的用户 + */ + private List getAllUserIdsFromBook(AttendanceBookConfigEntity bookConfig, Date start, Date end) { + + // 2. 添加考勤本使用范围内的用户 + try { + MutablePair, List> scopePair = ruleScopeUtil.selectScopeList( + bookConfig.getId(), ScopeBizType.ATTENDANCE_BOOK); + if (scopePair != null) { + + List orgIds = scopePair.getLeft(); + List directUserIds = scopePair.getRight(); + // 使用 attendanceUserService.getUserIds 直接获取组织下的用户 + List userIds = attendanceUserService.getUserIds( + orgIds, // 组织ID列表 + directUserIds, // 直接用户ID列表(无) + start, // 开始时间(不过滤) + end, // 结束时间(不过滤) + bookConfig.getScopeType() // 适用范围(不过滤) + ); + log.info("考勤本使用范围 - 组织ID数: {}, 直接用户ID数: {}", + orgIds != null ? orgIds.size() : 0, + directUserIds != null ? directUserIds.size() : 0); + return userIds.stream().distinct().collect(Collectors.toList()); + } + } catch (Exception e) { + log.error("获取考勤本使用范围用户失败,bookId: {}", bookConfig.getId(), e); + } + return Collections.emptyList(); + } + + /** + * 查询考勤记录 + */ + private List queryBookRecords(String bookId, Date start, Date end) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookRecordEntity::getBookId, bookId) + .between(AttendanceBookRecordEntity::getDay, start, end) + .orderByAsc(AttendanceBookRecordEntity::getUserId) + .orderByAsc(AttendanceBookRecordEntity::getDay) + .orderByAsc(AttendanceBookRecordEntity::getPeriodType); + return this.list(queryWrapper); + } + + /** + * 获取员工信息映射 + */ + private Map getUserInfoMap(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyMap(); + } + List userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds); + return userInfoList.stream() + .collect(Collectors.toMap(PartUserInfoVo::getUserId, user -> user, (u1, u2) -> u1)); + } + + /** + * 构建导出表头(3层结构:标题、说明、表头名称) + */ + private List> buildExportHeaders(String titleHead, List dates, List leaveTypeNames) { + + String shiftHead = "导入说明:\n" + + "1.考勤本名称、手机号为必填项;表头日期、标题年月须与所选导入月份一致\n" + + "2.考勤本结果为正常、公休、迟到、早退、缺卡、旷工、外出、外勤、出差、" + String.join("、", leaveTypeNames) + ",日期考勤结果选择为空时,将不会导入考勤本结果,员工没有此项考勤数据\n" + + "3.请按照数据模板的格式准备导入数据,模板中的表头名称不可更改,表头行不能删除\n" + + "4.导入已经导入过的员工信息将更新原有信息,请谨慎操作\n" + + "5.同一张表内同一手机号码不可重复(两行/人格式时,同一号码可分别出现「上」「下」两行)\n" + + "6.文件大小不超过500MB,最多支持导入500行数据(不含表头),超出请分批导入\n"; + + List> heads = new ArrayList<>(); + // 基本信息列 + heads.add(CollUtil.newArrayList(titleHead, shiftHead, "考勤本名称")); + heads.add(CollUtil.newArrayList(titleHead, shiftHead, "姓名")); + heads.add(CollUtil.newArrayList(titleHead, shiftHead, "手机号")); + heads.add(CollUtil.newArrayList(titleHead, shiftHead, "班次")); + // 日期列 + for (Date date : dates) { + String dateHeader = DateUtil.format(date, "d") + " (" + DateUtil.dayOfWeekEnum(date).toChinese("周") + ")"; + heads.add(CollUtil.newArrayList(titleHead, shiftHead, dateHeader)); + } + return heads; + } + + /** + * 构建合并单元格策略(前3列合并) + */ + private List buildMergeCells(int employeeCount) { + List mergeCells = new ArrayList<>(); + for (int i = 0; i < employeeCount; i++) { + int rowIndex = i * 2; // 每个员工2行 + mergeCells.add(new org.apache.poi.ss.util.CellRangeAddress(rowIndex, rowIndex + 1, 0, 0)); + mergeCells.add(new org.apache.poi.ss.util.CellRangeAddress(rowIndex, rowIndex + 1, 1, 1)); + mergeCells.add(new org.apache.poi.ss.util.CellRangeAddress(rowIndex, rowIndex + 1, 2, 2)); + } + return mergeCells; + } + + /** + * 构建导出数据(每个员工2行:上半天、下半天) + */ + private List> buildExportData( + Map> recordsByUser, + Map userInfoMap, + String bookName, + List dates, + List allUserIds, + Map leaveTypeIdToNameMap) { + + List> data = new ArrayList<>(); + + // 遍历所有用户(包括没有考勤记录的用户) + for (String userId : allUserIds) { + PartUserInfoVo userInfo = userInfoMap.get(userId); + String realName = userInfo != null ? userInfo.getRealName() : ""; + String mobilePhone = userInfo != null ? userInfo.getMobilePhone() : ""; + + // 获取该用户的考勤记录 + List userRecords = recordsByUser.getOrDefault(userId, Collections.emptyList()); + + // 按日期分组 + Map> recordsByDay = userRecords.stream() + .collect(Collectors.groupingBy(record -> DateUtil.format(record.getDay(), "yyyy-MM-dd"))); + + // 上半天 + Map morningRow = new HashMap<>(); + morningRow.put(0, bookName); + morningRow.put(1, realName); + morningRow.put(2, mobilePhone); + morningRow.put(3, AttendanceConstant.PERIOD_MORNING); + fillAttendanceStatus(morningRow, recordsByDay, dates, PeriodTypeEnum.MORNING.getCode(), leaveTypeIdToNameMap); + + // 下半天 + Map afternoonRow = new HashMap<>(); + afternoonRow.put(0, bookName); + afternoonRow.put(1, realName); + afternoonRow.put(2, mobilePhone); + afternoonRow.put(3, AttendanceConstant.PERIOD_AFTERNOON); + fillAttendanceStatus(afternoonRow, recordsByDay, dates, PeriodTypeEnum.AFTERNOON.getCode(), leaveTypeIdToNameMap); + + data.add(morningRow); + data.add(afternoonRow); + } + + return data; + } + + /** + * 填充考勤状态数据 + */ + private void fillAttendanceStatus( + Map row, + Map> recordsByDay, + List dates, + Integer periodType, + Map leaveTypeIdToNameMap) { + + for (int i = 0; i < dates.size(); i++) { + String dateStr = DateUtil.format(dates.get(i), "yyyy-MM-dd"); + List dayRecords = recordsByDay.get(dateStr); + + String status = ""; + if (CollUtil.isNotEmpty(dayRecords)) { + AttendanceBookRecordEntity record = dayRecords.stream() + .filter(r -> r.getPeriodType().equals(periodType)) + .findFirst() + .orElse(null); + if (record != null) { + // 如果有请假类型,从映射中获取名称 + if (StringUtils.isNotBlank(record.getLeaveType()) && leaveTypeIdToNameMap.containsKey(record.getLeaveType())) { + status = leaveTypeIdToNameMap.get(record.getLeaveType()).getName(); + } else { + // 其他状态:使用状态名称 + status = STATUS_CODE_TO_CN.getOrDefault(record.getAttendanceStatus(), ""); + } + } + } + row.put(i + 4, status); + } + } + + /** + * 保存操作日志 + * + * @param entity 考勤记录实体 + * @param existingRecord 已有记录(更新时不为null) + * @param operationType 操作类型 + */ + private void saveOperationLog(AttendanceBookRecordEntity entity, AttendanceBookRecordEntity existingRecord, String operationType) { + try { + // 参数校验 + if (entity == null) { + log.warn("考勤记录实体为空,跳过记录操作日志"); + return; + } + + // 构建操作内容描述(按照图片格式) + // 单条记录时,直接查询请假类型名称 + String operationContent = buildOperationContent(entity, existingRecord, operationType); + + // 获取当前用户信息 + UserInfo user = UserProvider.getUser(); + + // 创建操作日志实体 + AttendanceBookOperationLogEntity logEntity = AttendanceBookOperationLogEntity.builder() + .bookId(entity.getBookId()) + .month(entity.getMonth()) + .recordId(existingRecord != null ? existingRecord.getId() : entity.getId()) + .employeeId(entity.getUserId()) + .operationContent(operationContent) + .operatorId(user != null ? user.getUserId() : null) + .operatorName(user != null ? user.getUserName() : null) + .operationTime(new Date()) + .build(); + + attendanceBookOperationLogService.saveLog(logEntity); + log.debug("记录考勤本操作日志成功,记录ID: {}, 员工ID: {}", entity.getId(), entity.getUserId()); + } catch (Exception e) { + // 记录操作日志失败不影响主业务,仅记录警告日志 + log.warn("记录操作日志失败,记录ID: {}, 员工ID: {}, 原因: {}", + entity.getId(), entity.getUserId(), e.getMessage(), e); + } + } + + /** + * 构建操作内容描述(单条记录,直接查询请假类型) + * + * @param entity 考勤记录实体 + * @param existingRecord 已有记录(更新时不为null) + * @param operationType 操作类型 + * @return 操作内容描述 + */ + private String buildOperationContent(AttendanceBookRecordEntity entity, AttendanceBookRecordEntity existingRecord, String operationType) { + // 调整前/后可能各带不同请假类型,须同时解析,避免「修改前」仅按新记录的 leaveType 建映射导致旧假别被当成考勤状态展示 + LinkedHashSet leaveTypeIds = new LinkedHashSet<>(); + if (StringUtils.isNotBlank(entity.getLeaveType())) { + leaveTypeIds.add(entity.getLeaveType().trim()); + } + if (existingRecord != null && StringUtils.isNotBlank(existingRecord.getLeaveType())) { + leaveTypeIds.add(existingRecord.getLeaveType().trim()); + } + Map leaveTypeIdToNameMap = buildLeaveTypeIdToNameMap(leaveTypeIds); + return buildOperationContent(entity, existingRecord, operationType, leaveTypeIdToNameMap); + } + + /** + * 构建操作内容描述(使用映射) + * + * @param entity 考勤记录实体 + * @param existingRecord 已有记录(更新时不为null) + * @param operationType 操作类型 + * @param leaveTypeIdToNameMap 请假类型ID到名称的映射 + * @return 操作内容描述 + */ + private String buildOperationContent(AttendanceBookRecordEntity entity, AttendanceBookRecordEntity existingRecord, String operationType, Map leaveTypeIdToNameMap) { + // 获取员工姓名 + String employeeName = getEmployeeName(entity.getUserId()); + + // 格式化日期信息 + String formattedDate = formatDateWithWeekday(entity.getDay()); + + // 时段类型:上午/下午 + String periodName = getPeriodName(entity.getPeriodType()); + + StringBuilder content = new StringBuilder(); + + if (AttendanceConstant.LOG_OPERATION_UPDATE.equals(operationType) && existingRecord != null) { + // 需求8:更新操作,使用红色标记考勤状态 + content.append("对员工\"") + .append(employeeName) + .append("\"在") + .append(formattedDate) + .append(periodName) + .append("的班次结果由") + .append(getStatusDisplayWithRedMark(existingRecord.getAttendanceStatus(), existingRecord.getLeaveType(), leaveTypeIdToNameMap)) + .append("调整为") + .append(getStatusDisplayWithRedMark(entity.getAttendanceStatus(), entity.getLeaveType(), leaveTypeIdToNameMap)); + } else if (AttendanceConstant.LOG_OPERATION_CLEAN.equals(operationType) && existingRecord != null) { + content.append("清除员工\"") + .append(employeeName) + .append("\"") + .append(formattedDate) + .append(periodName) + .append("的班次结果") + .append(getStatusDisplayWithRedMark(existingRecord.getAttendanceStatus(), existingRecord.getLeaveType(), leaveTypeIdToNameMap)); + } else { + // 需求8:新增操作,使用红色标记考勤状态 + content.append("设置员工\"") + .append(employeeName) + .append("\"") + .append(formattedDate) + .append(periodName) + .append("的班次结果为") + .append(getStatusDisplayWithRedMark(entity.getAttendanceStatus(), entity.getLeaveType(), leaveTypeIdToNameMap)); + } + + // 添加请假类型和备注信息(使用映射) + appendLeaveInfoWithMap(content, entity, leaveTypeIdToNameMap); + + return content.toString(); + } + + /** + * 获取员工姓名 + * + * @param userId 用户ID + * @return 员工姓名,如果查询失败则返回用户ID + */ + private String getEmployeeName(String userId) { + if (StringUtils.isBlank(userId)) { + return "未知员工"; + } + try { + // 尝试从用户信息中获取真实姓名 + List userInfoList = userAntifreeze.getStaffRosterListInfoByIds(Collections.singletonList(userId)); + if (CollUtil.isNotEmpty(userInfoList)) { + return userInfoList.get(0).getRealName(); + } + } catch (Exception e) { + log.warn("获取员工姓名失败,userId: {}, 原因: {}", userId, e.getMessage()); + } + // 降级处理:返回用户ID + return userId; + } + + /** + * 格式化日期为:yyyy-MM-dd(星期X) + * + * @param date 日期 + * @return 格式化后的日期字符串 + */ + private String formatDateWithWeekday(Date date) { + if (date == null) { + return "未知日期"; + } + String formattedDate = DateUtil.format(date, "yyyy-MM-dd"); + String dayOfWeek = DateUtil.dayOfWeekEnum(date).toChinese("星期"); + return formattedDate + "(" + dayOfWeek + ")"; + } + + /** + * 获取时段名称 + * + * @param periodType 时段类型 + * @return 时段名称(上半天/下半天) + */ + private String getPeriodName(Integer periodType) { + if (periodType == null) { + return "未知时段"; + } + return PeriodTypeEnum.MORNING.getCode().equals(periodType) ? "上半天" : "下半天"; + } + + /** + * 添加请假类型和备注信息(使用映射) + * + * @param content 内容构建器 + * @param entity 考勤记录实体 + * @param leaveTypeIdToNameMap 请假类型ID到名称的映射 + */ + private void appendLeaveInfoWithMap(StringBuilder content, AttendanceBookRecordEntity entity, Map leaveTypeIdToNameMap) { + if (entity == null) { + return; + } + + // 当为请假类型时,添加备注内容 + if (StringUtils.isNotBlank(entity.getLeaveType())) { + // 从映射中获取请假类型名称 + String leaveTypeName = leaveTypeIdToNameMap != null ? + leaveTypeIdToNameMap.getOrDefault(entity.getLeaveType(), entity.getLeaveType()) : + entity.getLeaveType(); + content.append(",请假类型:").append(leaveTypeName); + if (StringUtils.isNotBlank(entity.getRemark())) { + content.append(",备注:").append(entity.getRemark()); + } + } + } + + /** + * 根据请假类型 ID 集合构建「ID → 名称」映射(用于操作日志等)。 + * 批量接口须同时纳入修改前记录上的 leaveType,否则日志里「由 xxx 调整为」的上一段会丢失假别名称。 + */ + private Map buildLeaveTypeIdToNameMap(Collection leaveTypeIds) { + if (CollUtil.isEmpty(leaveTypeIds)) { + return Collections.emptyMap(); + } + List idList = leaveTypeIds.stream() + .filter(StringUtils::isNotBlank) + .map(String::trim) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(idList)) { + return Collections.emptyMap(); + } + Map map = new LinkedHashMap<>(); + List leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypes(idList); + if (CollUtil.isNotEmpty(leaveTypes)) { + for (AttendanceLeaveType t : leaveTypes) { + map.put(t.getId(), t.getName()); + } + } + for (String id : idList) { + if (map.containsKey(id)) { + continue; + } + AttendanceLeaveType byId = attendanceLeaveTypeService.getById(id); + if (byId != null && StringUtils.isNotBlank(byId.getName())) { + map.put(id, byId.getName()); + } else { + map.put(id, id); + } + } + return map; + } + + /** + * 获取考勤状态中文名称 + * + * @param status 状态码 + * @return 中文名称 + */ + private String getStatusName(Integer status) { + if (status == null) { + return "未知"; + } + return STATUS_CODE_TO_CN.getOrDefault(status, "未知"); + } + + /** + * 批量查询请假类型名称,构建ID到名称的映射 + *

规则与 {@link #getMonthStatistics(BookRecordMonthStatisticsDto)} 一致: + * 可排假期为「全部假期」({@code holidayScopeType == 1}) 或未配置具体 ID 时查询全部未删除的假期类型, + * 避免导出时映射为空导致单元格仅显示假期类型原始 ID(如 {@code 1})。

+ */ + private Map batchQueryLeaveTypeNames(AttendanceBookConfigEntity bookConfig) { + if (bookConfig == null) { + return Collections.emptyMap(); + } + List leaveTypeIds = Objects.equals(bookConfig.getHolidayScopeType(), 1) + ? null + : (StringUtils.isNotBlank(bookConfig.getHolidayIds()) + ? Arrays.stream(bookConfig.getHolidayIds().split(",")) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()) + : null); + + List leaveTypes = attendanceLeaveTypeService.getAttendanceLeaveTypes(leaveTypeIds); + if (CollUtil.isEmpty(leaveTypes)) { + return Collections.emptyMap(); + } + return leaveTypes.stream() + .collect(Collectors.toMap(AttendanceLeaveType::getId, AttendanceLeaveType::getName, (v1, v2) -> v1)); + } + + /** + * 获取考勤状态展示名称:有请假类型时返回假期名称,否则返回标准状态中文名。 + */ + private String getStatusNameWithLeaveType(AttendanceBookRecordEntity record, Map leaveTypeIdToNameMap) { + if (record == null) { + return null; + } + // 如果有请假类型,从映射中获取名称 + if (StringUtils.isNotBlank(record.getLeaveType()) && leaveTypeIdToNameMap != null && leaveTypeIdToNameMap.containsKey(record.getLeaveType())) { + return leaveTypeIdToNameMap.get(record.getLeaveType()).getName(); + } + // 否则使用状态名称 + return getStatusName(record.getAttendanceStatus()); + } + + /** + * 获取考勤状态显示名称(带红色标记),优先使用假期类型名称 + * 当记录有 leaveType 时展示假别名称(映射缺失时回退到考勤状态中文),不得再用 status 覆盖假别展示(否则修改前请假易被显示成「正常」) + */ + private String getStatusDisplayWithRedMark(Integer status, String leaveType, Map leaveTypeIdToNameMap) { + String displayName; + if (StringUtils.isNotBlank(leaveType)) { + if (leaveTypeIdToNameMap != null) { + displayName = leaveTypeIdToNameMap.getOrDefault(leaveType, getStatusName(status)); + } else { + displayName = getStatusName(status); + } + } else if (Objects.nonNull(status)) { + displayName = getStatusName(status); + } else { + displayName = "空"; + } + return AttendanceConstant.RED_MARK_START + "【" + displayName + "】" + AttendanceConstant.RED_MARK_END; + } + + + @Override + public void exportTemplate(String bookId, String month, HttpServletResponse response) { + log.info("开始导出考勤本模板,bookId: {}, month: {}", bookId, month); + + try { + // 1. 查询考勤本配置 + AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(bookId); + Assert.notNull(bookConfig, "当前考勤本不存在"); + + Date start = DateDetail.getMonthBeginDate(month); + Date end = DateDetail.getMonthEndDate(month); + List dates = DateDetail.getDatesByPeriod(start, end); + + // 2. 获取考勤本使用范围内的所有用户(与导出口径一致),用于在模板中预填「考勤本名称 / 姓名 / 手机号」三列 + List allUserIds = getAllUserIdsFromBook(bookConfig, start, end); + Map userInfoMap = getUserInfoMap(allUserIds); + List userIdList = new ArrayList<>(userInfoMap.keySet()); + + // 3. 复用考勤本导出的表头结构 + String titleHead = DateUtil.format(start, "yyyy年MM月") + bookConfig.getBookName() + "考勤本"; + List leaveTypeNames = batchQueryLeaveTypeNames(bookConfig).values() + .stream().distinct().collect(Collectors.toList()); + List> headList = buildExportHeaders(titleHead, dates, leaveTypeNames); + + // 4. 构建模板数据:复用 buildExportData,传入空考勤记录 Map,使日期列保持空白; + // 仅前 4 列被预填(考勤本名称 / 姓名 / 手机号 / 班次) + List> templateData = buildExportData( + Collections.emptyMap(), + userInfoMap, + bookConfig.getBookName(), + dates, + userIdList, + Collections.emptyMap()); + + // 5. 前 3 列「上半天/下半天」合并单元格处理器(与 bookRecordExport 完全一致) + List mergeCells = buildMergeCells(userIdList.size()); + jnpf.attendance.excel.AttendanceBookMergeHandler mergeHandler = + new jnpf.attendance.excel.AttendanceBookMergeHandler( + mergeCells, 3, userIdList.size() * 2, headList.size()); + jnpf.attendance.excel.CompositeWriteHandler compositeHandler = + new jnpf.attendance.excel.CompositeWriteHandler(new HeadStyleHandler(), mergeHandler); + + // 6. 设置响应头,直接流导出 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + String fileName = java.net.URLEncoder.encode(titleHead, "UTF-8").replaceAll("\\+", "%20"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + + // 7. 使用 EasyExcel 直接写入响应流(含合并处理器与预填的前 4 列) + com.alibaba.excel.EasyExcel.write(response.getOutputStream()) + .head(headList) + .registerWriteHandler(compositeHandler) + .sheet("考勤本模板") + .doWrite(templateData); + + log.info("考勤本模板导出完成,员工数: {}", userIdList.size()); + } catch (Exception e) { + log.error("考勤本模板导出失败", e); + throw new RuntimeException("导出模板失败:" + e.getMessage()); + } + } + + @Override + public List getDayList(BookRecordDayListDto req) { + log.info("获取日考勤本列表,bookId: {}, day: {}", req.getBookId(), req.getDay()); + + // 参数校验 + Assert.notBlank(req.getBookId(), "考勤本ID不能为空"); + Assert.notBlank(req.getDay(), "日期不能为空"); + + // 1. 查询考勤本配置 + AttendanceBookConfigEntity config = attendanceBookConfigService.getById(req.getBookId()); + if (config == null) { + log.warn("考勤本配置不存在,bookId: {}", req.getBookId()); + return CollUtil.newArrayList(); + } + + // 2. 解析日期,获取该日的起止时间 + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate localDate = LocalDate.parse(req.getDay(), dateFormatter); + Date startDate = Date.from(localDate.atStartOfDay().atZone(java.time.ZoneId.systemDefault()).toInstant()); + Date endDate = Date.from(localDate.atTime(23, 59, 59).atZone(java.time.ZoneId.systemDefault()).toInstant()); + MutablePair, List> userAndGroupIds = getUserIdsFromScope(req.getBookId(), config.getScopeType(), startDate, endDate); + // 3. 从考勤本使用范围中获取人员列表 + List userInfoList = userAndGroupIds.getLeft(); + List groupUsers = userAndGroupIds.getRight(); + // 4. 调用userAntifreeze获取员工详细信息 + if (CollUtil.isEmpty(userInfoList)) { + return CollUtil.newArrayList(); + } + + // 构建用户信息映射 + List userIds = userInfoList.stream().map(BookScopeUserVo::getUserId).distinct().collect(Collectors.toList()); + + // 查询用户考勤组成员信息(用于判断离组状态) + Map> groupUserMap = groupUsers.stream() + .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + + // 5. 查询该考勤本该日的所有考勤记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookRecordEntity::getBookId, req.getBookId()) + .in(AttendanceBookRecordEntity::getUserId, userIds) + .between(AttendanceBookRecordEntity::getDay, startDate, endDate) + .orderByAsc(AttendanceBookRecordEntity::getUserId) + .orderByAsc(AttendanceBookRecordEntity::getDay) + .orderByAsc(AttendanceBookRecordEntity::getPeriodType); + + List records = this.list(queryWrapper); + + // 6. 按员工分组 + Map> recordsByUser = records.stream() + .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId)); + Map stringStringMap = getAttendanceLeaveTypeMap(config, records); + + // 批量查询用户锁定状态 + Map sealMap = attendanceDayStatisticsService.selectUserIsSeal(userIds, DateUtil.format(startDate, "yyyy-MM")); + + // 7. 构建返回结果 + List result = new ArrayList<>(); + int sortOrder = 1; + + for (BookScopeUserVo userInfo : userInfoList) { + String userId = userInfo.getUserId(); + List userRecords = recordsByUser.getOrDefault(userId, CollUtil.newArrayList()); + + // 计算用户当日是否离组 + List userGroupUsers = groupUserMap.getOrDefault(userId, CollUtil.newArrayList()); + Integer existStatus = AttendanceGroupUserStatusUtil.isExistStatus(userGroupUsers, startDate); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + if (!Objects.equals(existStatus, 1)) { + continue; + } + // 构建每日记录列表 + List recordList = new ArrayList<>(); + + // 按日期分组 + Map> recordsByDay = userRecords.stream() + .collect(Collectors.groupingBy(record -> DateUtil.format(record.getDay(), "yyyy-MM-dd"))); + // 按日期排序 + List sortedDays = recordsByDay.keySet().stream() + .sorted() + .collect(Collectors.toList()); + + for (String day : sortedDays) { + List dayRecords = recordsByDay.get(day); + // 为每条记录构建VO + for (AttendanceBookRecordEntity record : dayRecords) { + BookRecordDayVo dayVo = BookRecordDayVo.builder() + .id(record.getId()) + .day(day) + .periodType(record.getPeriodType()) + .periodTypeName(PeriodTypeEnum.MORNING.getCode().equals(record.getPeriodType()) ? "上班卡" : "下班卡") + .attendanceStatus(resolveStatus(record)) + .attendanceStatusName(getStatusNameWithLeaveType(record, stringStringMap)) + .leaveType(record.getLeaveType()) + .leaveTypeName(StringUtils.isBlank(record.getLeaveType()) || !stringStringMap.containsKey(record.getLeaveType()) ? null + : stringStringMap.get(record.getLeaveType()).getName()) + .remark(record.getRemark()) + .changed(record.getLastModifyTime() != null) + .isOutGroup(existStatus) + .build(); + recordList.add(dayVo); + } + } + + // 检查是否已有上午和下午的记录,如果没有则补充空实体 + boolean hasMorning = recordList.stream().anyMatch(r -> PeriodTypeEnum.MORNING.getCode().equals(r.getPeriodType())); + boolean hasAfternoon = recordList.stream().anyMatch(r -> PeriodTypeEnum.AFTERNOON.getCode().equals(r.getPeriodType())); + + // 如果没有上午记录,补充空实体 + if (!hasMorning) { + recordList.add(BookRecordDayVo.builder() + .day(req.getDay()) + .periodType(PeriodTypeEnum.MORNING.getCode()) + .periodTypeName(PeriodTypeEnum.MORNING.getName()) + .isOutGroup(existStatus) + .build()); + } + + // 如果没有下午记录,补充空实体 + if (!hasAfternoon) { + recordList.add(BookRecordDayVo.builder() + .day(req.getDay()) + .periodType(PeriodTypeEnum.AFTERNOON.getCode()) + .periodTypeName(PeriodTypeEnum.AFTERNOON.getName()) + .isOutGroup(existStatus) + .build()); + } + int secondmentType = SecondmentTypeUtil.resolveSecondmentType(userGroupUsers, startDate, endDate); + // 构建员工分组VO + BookRecordDayListVo dayListVo = BookRecordDayListVo.builder() + .userId(userId) + .realName(userInfo.getUserName()) + .positionName(userInfo.getUserPost()) + .organizeName(userInfo.getOrganization()) + .mobilePhone(userInfo.getMobilePhone()) + .secondmentType(secondmentType) + .isOutGroup(userInfo.getIsOutGroup()) + .isSeal(sealMap.getOrDefault(userId, false)) + .sortOrder(sortOrder++) + .recordList(recordList.stream().sorted(Comparator.comparing(BookRecordDayVo::getPeriodType)).collect(Collectors.toList())) + .build(); + + result.add(dayListVo); + } + + log.info("获取日考勤本列表成功,bookId: {}, day: {}, 员工数: {}", + req.getBookId(), req.getDay(), result.size()); + return result; + } + + /** + * 从考勤本使用范围中获取用户信息列表 + * 返回包含用户基础信息及借调信息的完整用户信息 + * + * @param bookId 考勤本ID + * @param scopeType 使用范围类型(0:全部 1:指定组织/成员) + * @param start 开始时间 + * @param end 结束时间 + * @return 用户信息列表(包含借调信息) + */ + private MutablePair, List> getUserIdsFromScope(String bookId, Integer scopeType, Date start, Date end) { + // 查询使用范围 + MutablePair, List> scopeList = ruleScopeUtil.selectScopeList(bookId, ScopeBizType.ATTENDANCE_BOOK); + + List orgIds = scopeList != null ? scopeList.getLeft() : new ArrayList<>(); + List directUserIds = scopeList != null ? scopeList.getRight() : new ArrayList<>(); + + // 获取用户ID列表(scopeType=0时,attendanceUserService.getUserIds会返回所有人员) + List userAndGroupIds = attendanceUserService.getUserIdsAndGroupIds(orgIds, directUserIds, start, end, scopeType); + if (CollUtil.isEmpty(userAndGroupIds)) { + return new MutablePair<>(List.of(), List.of()); + } + List userIds = userAndGroupIds.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + // 查询用户基础信息 + List userInfoList = userAntifreeze.getStaffRosterListInfoByIds(userIds); + if (CollUtil.isEmpty(userInfoList)) { + return new MutablePair<>(List.of(), List.of()); + } + Map> userGroupUsersMap = userAndGroupIds.stream() + .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + + // 组装返回数据 + List result = new ArrayList<>(); + for (PartUserInfoVo userInfo : userInfoList) { + List userGroupUsers = userGroupUsersMap.getOrDefault(userInfo.getUserId(), Collections.emptyList()); + AttendanceGroupUser attendanceGroupUser = userGroupUsers.stream().reduce((groupUser1, groupUser2) -> Objects.equals(groupUser1.getDeleteMark(), 0) ? groupUser1 : Objects.equals(groupUser2.getDeleteMark(), 0) ? groupUser2 : groupUser1.getCreatorTime().after(groupUser2.getCreatorTime()) ? groupUser1 : groupUser2).orElse(null); + int secondmentType = SecondmentTypeUtil.toSecondmentType(AttendanceGroupUserStatusUtil.isExistStatus(userGroupUsers, start, end)); + BookScopeUserVo vo = BookScopeUserVo.builder() + .userId(userInfo.getUserId()) + .userName(userInfo.getRealName()) + .userPost(userInfo.getPositionName()) + .organization(userInfo.getOrganizeName()) + .mobilePhone(userInfo.getMobilePhone()) + .secondmentType(secondmentType) + .isOutGroup(Objects.isNull(attendanceGroupUser) ? 1 : attendanceGroupUser.getDeleteMark()) + .build(); + result.add(vo); + } + return new MutablePair<>(result, userAndGroupIds); + } + + @Override + public List getMonthList(BookRecordMonthListDto req) { + log.info("获取月考勤本列表,bookId: {}, month: {}", req.getBookId(), req.getMonth()); + + // 1. 查询考勤本配置 + AttendanceBookConfigEntity config = attendanceBookConfigService.getById(req.getBookId()); + if (config == null) { + log.warn("考勤本配置不存在,bookId: {}", req.getBookId()); + return CollUtil.newArrayList(); + } + + // 2. 解析月份,获取该月的起止日期 + YearMonth yearMonth = YearMonth.parse(req.getMonth(), DateTimeFormatter.ofPattern("yyyy-MM")); + Date startDate = Date.from(yearMonth.atDay(1).atStartOfDay().atZone(java.time.ZoneId.systemDefault()).toInstant()); + Date endDate = Date.from(yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(java.time.ZoneId.systemDefault()).toInstant()); + MutablePair, List> userAndGroupIds = getUserIdsFromScope(req.getBookId(), config.getScopeType(), startDate, endDate); + // 3. 从考勤本使用范围中获取人员列表 + List userInfoList = userAndGroupIds.getLeft(); + if (CollUtil.isEmpty(userInfoList)) { + return CollUtil.newArrayList(); + } + // 4. 构建用户ID列表 + List userIds = userInfoList.stream() + .map(BookScopeUserVo::getUserId) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(userAndGroupIds.getRight())) { + return CollUtil.newArrayList(); + } + // 查询用户考勤组成员信息(用于判断每日离组状态) + Map> groupUserMap = userAndGroupIds.getRight().stream() + .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + + // 5. 查询该考勤本该月的所有考勤记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookRecordEntity::getBookId, req.getBookId()) + .in(AttendanceBookRecordEntity::getUserId, userIds) + .between(AttendanceBookRecordEntity::getDay, startDate, endDate) + .orderByAsc(AttendanceBookRecordEntity::getUserId) + .orderByAsc(AttendanceBookRecordEntity::getDay) + .orderByAsc(AttendanceBookRecordEntity::getPeriodType); + + List records = this.list(queryWrapper); + + // 6. 按员工分组 + Map> recordsByUser = records.stream() + .collect(Collectors.groupingBy(AttendanceBookRecordEntity::getUserId)); + + // 7. 批量查询请假类型,构建ID到名称的映射(配置 + 记录中出现的 ID) + Map stringStringMap = getAttendanceLeaveTypeMap(config, records); + // 批量查询用户锁定状态 + Map sealMap = attendanceDayStatisticsService.selectUserIsSeal(userIds, req.getMonth()); + + // 8. 生成该月所有日期列表 + List allDaysInMonth = new ArrayList<>(); + LocalDate currentDate = yearMonth.atDay(1); + LocalDate lastDay = yearMonth.atEndOfMonth(); + while (!currentDate.isAfter(lastDay)) { + allDaysInMonth.add(currentDate); + currentDate = currentDate.plusDays(1); + } + + // 8. 构建返回结果 + List result = new ArrayList<>(); + int sortOrder = 1; + + for (BookScopeUserVo userInfo : userInfoList) { + String userId = userInfo.getUserId(); + List userRecords = recordsByUser.getOrDefault(userId, CollUtil.newArrayList()); + + // 获取用户的考勤组成员数据(用于每日离组判断) + List userGroupUsers = groupUserMap.getOrDefault(userId, CollUtil.newArrayList()); + + // 按日期分组 + Map> recordsByDay = userRecords.stream() + .collect(Collectors.groupingBy(record -> DateUtil.format(record.getDay(), "yyyy-MM-dd"))); + + // 构建每日记录列表 + List dayList = new ArrayList<>(); + + for (LocalDate day : allDaysInMonth) { + String dayStr = day.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + String weekDay = getWeekDay(day.getDayOfWeek().getValue()); + + // 计算用户当日是否离组 + Date dayDate = Date.from(day.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); + Integer existStatus = AttendanceGroupUserStatusUtil.isExistStatus(userGroupUsers, dayDate); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + // 获取该日的考勤记录 + List dayRecords = recordsByDay.getOrDefault(dayStr, CollUtil.newArrayList()); + + // 上班卡 / 下班卡各取一条:同日同时段若有多条(脏数据),取最后修改时间最新的一条,避免状态码随机错乱(如 2007/2008 串台) + AttendanceBookRecordEntity morningRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.MORNING.getCode()); + AttendanceBookRecordEntity eveningRecord = findRecordByPeriod(dayRecords, PeriodTypeEnum.AFTERNOON.getCode()); + + // 构建日记录VO + BookRecordMonthDayVo dayVo = BookRecordMonthDayVo.builder() + .day(dayStr) + .weekDay(weekDay) + .morningRecordId(morningRecord != null ? morningRecord.getId() : null) + .morningStatus(resolveStatus(morningRecord)) + .morningStatusName(getStatusNameWithLeaveType(morningRecord, stringStringMap)) + .eveningRecordId(eveningRecord != null ? eveningRecord.getId() : null) + .eveningStatus(resolveStatus(eveningRecord)) + .eveningStatusName(getStatusNameWithLeaveType(eveningRecord, stringStringMap)) + .isOutGroup(existStatus) + .build(); + dayList.add(dayVo); + } + int secondmentType = SecondmentTypeUtil.resolveSecondmentType(userGroupUsers, startDate, endDate); + // 构建员工分组VO + BookRecordMonthListVo monthListVo = BookRecordMonthListVo.builder() + .userId(userId) + .realName(userInfo.getUserName()) + .positionName(userInfo.getUserPost()) + .organizeName(userInfo.getOrganization()) + .mobilePhone(userInfo.getMobilePhone()) + .secondmentType(secondmentType) + .isOutGroup(userInfo.getIsOutGroup()) + .isSeal(sealMap.getOrDefault(userId, false)) + .sortOrder(sortOrder++) + .dayList(dayList) + .build(); + + result.add(monthListVo); + } + + log.info("获取月考勤本列表成功,bookId: {}, month: {}, 员工数: {}", + req.getBookId(), req.getMonth(), result.size()); + return result; + } + + /** + * 获取星期名称 + * + * @param dayOfWeek 星期几(1-7,1表示周一) + * @return 星期名称 + */ + private String getWeekDay(int dayOfWeek) { + switch (dayOfWeek) { + case 1: + return "周一"; + case 2: + return "周二"; + case 3: + return "周三"; + case 4: + return "周四"; + case 5: + return "周五"; + case 6: + return "周六"; + case 7: + return "周日"; + default: + return ""; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public BatchOperationResultVo batchSaveOrUpdateRecord(BatchAttendanceBookRecordDto dto) { + log.info("批量新增或修改考勤记录,记录数: {}", dto.getRecords().size()); + + int successCount = 0; + int failCount = 0; + List successIds = new ArrayList<>(); + List failDetails = new ArrayList<>(); + + // 优化:批量查询已存在的记录,避免循环中逐条查询 + List records = dto.getRecords(); + AttendanceBookRecordDto attendanceBookRecordDto = records.get(0); + String bookId = attendanceBookRecordDto.getBookId(); + Map existingRecordMap = batchQueryExistingRecords(records, bookId); + Map> userGroupUsersMap = buildUserGroupUsersMapForRecords(bookId, records); + Map userMonthSealMap = buildUserMonthSealMap(records); + + // 操作日志需同时识别每条 DTO 与对应「修改前」记录上的 leaveType,避免旧假别名缺失后被展示成考勤状态(如正常) + LinkedHashSet allLeaveTypeIds = new LinkedHashSet<>(); + for (AttendanceBookRecordDto r : records) { + if (StringUtils.isNotBlank(r.getLeaveType())) { + allLeaveTypeIds.add(r.getLeaveType().trim()); + } + String uniqueKey = buildUniqueKey(r.getBookId(), r.getUserId(), r.getDay(), r.getPeriodType()); + AttendanceBookRecordEntity existing = existingRecordMap.get(uniqueKey); + if (existing != null && StringUtils.isNotBlank(existing.getLeaveType())) { + allLeaveTypeIds.add(existing.getLeaveType().trim()); + } + } + Map leaveTypeIdToNameMap = buildLeaveTypeIdToNameMap(allLeaveTypeIds); + + // 获取当前用户信息 + UserInfo user = UserProvider.getUser(); + Date now = new Date(); + + // 分类:需要新增和需要更新的记录 + List toInsert = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + List toClear = new ArrayList<>(); + // 批量收集操作日志,最后统一插入 + List operationLogs = new ArrayList<>(); + + for (int i = 0; i < records.size(); i++) { + AttendanceBookRecordDto recordDto = records.get(i); + try { + // 构建唯一键:bookId+userId+day+periodType + String uniqueKey = buildUniqueKey(recordDto.getBookId(), recordDto.getUserId(), + recordDto.getDay(), recordDto.getPeriodType()); + + AttendanceBookRecordEntity existingRecord = existingRecordMap.get(uniqueKey); + + // 用户当日不在组:跳过,不做新增/修改/清除 + List userGroupUsers = userGroupUsersMap.getOrDefault( + recordDto.getUserId(), Collections.emptyList()); + if (!AttendanceGroupUserStatusUtil.isUserInGroupOnDay(userGroupUsers, recordDto.getDay())) { + log.info("批量保存:用户当日不在组,已跳过,userId: {}, day: {}, periodType: {}", + recordDto.getUserId(), recordDto.getDay(), recordDto.getPeriodType()); + continue; + } + + String recordMonth = resolveRecordMonth(recordDto); + if (isUserSealedInCache(recordDto.getUserId(), recordMonth, userMonthSealMap)) { + log.info("批量保存:用户当月已封存,已跳过,userId: {}, month: {}, periodType: {}", + recordDto.getUserId(), recordMonth, recordDto.getPeriodType()); + continue; + } + + boolean clearRequest = isClearRequest(recordDto); + if (clearRequest) { + if (existingRecord == null) { + log.info("批量清除:无对应记录,跳过,userId: {}, day: {}, periodType: {}", + recordDto.getUserId(), recordDto.getDay(), recordDto.getPeriodType()); + if (StringUtils.isNotBlank(recordDto.getId())) { + successIds.add(recordDto.getId()); + successCount++; + } + continue; + } + toClear.add(existingRecord); + operationLogs.add(buildOperationLogEntity(recordDto, existingRecord, + AttendanceConstant.LOG_OPERATION_CLEAN, leaveTypeIdToNameMap)); + successIds.add(existingRecord.getId()); + successCount++; + continue; + } + if (existingRecord != null) { + // 更新现有记录 + existingRecord.setAttendanceStatus(recordDto.getAttendanceStatus()); + existingRecord.setLeaveType(recordDto.getLeaveType()); + existingRecord.setRemark(recordDto.getRemark()); + existingRecord.setLastModifyUserId(user != null ? user.getUserId() : null); + existingRecord.setLastModifyTime(now); + toUpdate.add(existingRecord); + + successIds.add(existingRecord.getId()); + operationLogs.add(buildOperationLogEntity(recordDto, existingRecord, + AttendanceConstant.LOG_OPERATION_UPDATE, leaveTypeIdToNameMap)); + successCount++; + continue; + } + // 新增记录 + AttendanceBookRecordEntity newEntity = buildEntityFromDto(recordDto); + newEntity.setId(RandomUtil.uuId()); + newEntity.setCreatorUserId(user != null ? user.getUserId() : null); + newEntity.setCreatorTime(now); + newEntity.setTenantId(user != null ? user.getTenantId() : null); + // 自动填充月份字段 + if (StringUtils.isBlank(newEntity.getMonth()) && newEntity.getDay() != null) { + newEntity.setMonth(newEntity.getDay().toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy-MM"))); + } + + toInsert.add(newEntity); + // 收集成功ID + successIds.add(newEntity.getId()); + // 收集操作日志,不立即插入数据库 + operationLogs.add(buildOperationLogEntity(recordDto, null, AttendanceConstant.LOG_OPERATION_ADD, leaveTypeIdToNameMap)); + successCount++; + } catch (Exception e) { + failCount++; + failDetails.add(BatchOperationResultVo.FailDetailVo.builder() + .row(i + 1) + .message(e.getMessage()) + .build()); + log.error("批量操作第{}条记录失败", i + 1, e); + } + } + + // 批量插入和更新 + if (!toInsert.isEmpty()) { + this.saveBatch(toInsert); + log.info("批量新增考勤记录成功,数量: {}", toInsert.size()); + } + + if (!toUpdate.isEmpty()) { + this.updateBatchById(toUpdate); + log.info("批量更新考勤记录成功,数量: {}", toUpdate.size()); + } + if (!toClear.isEmpty()) { + this.removeBatchByIds(toClear); + log.info("批量清除考勤记录成功,数量: {}", toClear.size()); + } + // 批量插入操作日志 + if (!operationLogs.isEmpty()) { + attendanceBookOperationLogService.saveBatch(operationLogs); + log.info("批量插入操作日志成功,数量: {}", operationLogs.size()); + } + + BatchOperationResultVo result = BatchOperationResultVo.builder() + .successCount(successCount) + .successIds(successIds) + .failCount(failCount) + .failDetails(failDetails) + .build(); + + log.info("批量新增或修改考勤记录完成,成功: {}, 失败: {}", successCount, failCount); + return result; + } + + + /** + * 构建操作日志实体(不立即插入数据库) + */ + private AttendanceBookOperationLogEntity buildOperationLogEntity(AttendanceBookRecordDto recordDto, + AttendanceBookRecordEntity existingRecord, + String operationType, + Map leaveTypeIdToNameMap) { + try { + if (recordDto == null && existingRecord == null) { + log.warn("考勤记录DTO与已有记录均为空,跳过构建操作日志"); + return null; + } + + // 构建操作内容描述(清除时 DTO 可能为空,以库中记录为准) + AttendanceBookRecordEntity tempEntity = recordDto != null ? buildEntityFromDto(recordDto) : existingRecord; + String operationContent = buildOperationContent(tempEntity, existingRecord, operationType, leaveTypeIdToNameMap); + + // 获取当前用户信息 + UserInfo user = UserProvider.getUser(); + + // 创建操作日志实体 + String bookId = recordDto != null ? recordDto.getBookId() : existingRecord.getBookId(); + Date day = recordDto != null ? recordDto.getDay() : existingRecord.getDay(); + String recordId = existingRecord != null ? existingRecord.getId() + : (recordDto != null ? recordDto.getId() : null); + String userId = recordDto != null ? recordDto.getUserId() : existingRecord.getUserId(); + + return AttendanceBookOperationLogEntity.builder() + .bookId(bookId) + .month(day != null ? day.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(DateTimeFormatter.ofPattern("yyyy-MM")) : null) + .recordId(recordId) + .employeeId(userId) + .operationContent(operationContent) + .operatorId(user != null ? user.getUserId() : null) + .operatorName(user != null ? user.getUserName() : null) + .operationTime(new Date()) + .build(); + } catch (Exception e) { + // 构建操作日志失败不影响主业务,仅记录警告日志 + log.warn("构建操作日志失败,记录ID: {}, 员工ID: {}, 原因: {}", + recordDto.getId(), recordDto.getUserId(), e.getMessage(), e); + return null; + } + } + + + /** + * 批量查询已存在的记录 + * + * @param records 考勤记录DTO列表 + * @return 唯一键 -> 实体的映射 + */ + private Map batchQueryExistingRecords(List records, String bookId) { + if (CollUtil.isEmpty(records)) { + return Collections.emptyMap(); + } + + Map resultMap = new HashMap<>(); + // 收集该bookId下的所有条件 + Set userIds = new HashSet<>(); + Set days = new HashSet<>(); + Set periodTypes = new HashSet<>(); + + for (AttendanceBookRecordDto record : records) { + userIds.add(record.getUserId()); + days.add(record.getDay()); + periodTypes.add(record.getPeriodType()); + } + + // 批量查询 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookRecordEntity::getBookId, bookId) + .in(AttendanceBookRecordEntity::getUserId, userIds) + .in(AttendanceBookRecordEntity::getDay, days) + .in(AttendanceBookRecordEntity::getPeriodType, periodTypes); + + List existingRecords = this.list(queryWrapper); + + // 构建映射 + for (AttendanceBookRecordEntity record : existingRecords) { + String uniqueKey = buildUniqueKey(record.getBookId(), record.getUserId(), + record.getDay(), record.getPeriodType()); + resultMap.put(uniqueKey, record); + } + return resultMap; + } + + /** + * 构建唯一键 + */ + private String buildUniqueKey(String bookId, String userId, Date day, Integer periodType) { + return String.format("%s_%s_%s_%s", bookId, userId, + day != null ? day.getTime() : "null", periodType); + } + + /** + * 从DTO构建Entity + */ + private AttendanceBookRecordEntity buildEntityFromDto(AttendanceBookRecordDto dto) { + AttendanceBookRecordEntity entity = new AttendanceBookRecordEntity(); + entity.setId(dto.getId()); + entity.setBookId(dto.getBookId()); + entity.setUserId(dto.getUserId()); + entity.setDay(dto.getDay()); + entity.setPeriodType(dto.getPeriodType()); + entity.setAttendanceStatus(dto.getAttendanceStatus()); + entity.setLeaveType(dto.getLeaveType()); + entity.setRemark(dto.getRemark()); + return entity; + } + + /** + * 是否为清除请求:考勤状态与请假类型均为 null + */ + private boolean isClearRequest(AttendanceBookRecordDto dto) { + return dto != null && dto.getAttendanceStatus() == null && dto.getLeaveType() == null; + } + + /** + * 解析已存在的考勤记录:优先按 id,再按 bookId+userId+day+periodType + */ + private AttendanceBookRecordEntity resolveExistingRecord(AttendanceBookRecordDto dto) { + if (dto == null) { + return null; + } + if (StringUtils.isNotBlank(dto.getId())) { + AttendanceBookRecordEntity byId = this.getById(dto.getId()); + if (byId != null) { + return byId; + } + } + if (StringUtils.isBlank(dto.getBookId()) || StringUtils.isBlank(dto.getUserId()) + || dto.getDay() == null || dto.getPeriodType() == null) { + return null; + } + Date dayStart = DateUtil.beginOfDay(dto.getDay()); + Date dayEnd = DateUtil.endOfDay(dto.getDay()); + return this.getOne(new LambdaQueryWrapper() + .eq(AttendanceBookRecordEntity::getBookId, dto.getBookId()) + .eq(AttendanceBookRecordEntity::getUserId, dto.getUserId()) + .eq(AttendanceBookRecordEntity::getPeriodType, dto.getPeriodType()) + .between(AttendanceBookRecordEntity::getDay, dayStart, dayEnd) + .last("LIMIT 1")); + } + + @Override + public LeaveRemarkSummaryVo getLeaveAttendanceSummary(LeaveRemarkSummaryDto dto) { + log.info("获取假勤汇总信息,bookId: {}, month: {}, userId: {}", + dto.getBookId(), dto.getMonth(), dto.getUserId()); + + // 1. 参数校验 + Assert.notBlank(dto.getBookId(), "考勤本ID不能为空"); + Assert.notBlank(dto.getMonth(), "月份不能为空"); + + // 2. 月份范围:与导出等接口一致使用 DateDetail,避免与 F_Month / 服务器时区组合时出现边界漏查 + Date startDate = DateDetail.getMonthBeginDate(dto.getMonth()); + Date endDate = DateDetail.getMonthEndDate(dto.getMonth()); + + // 3. 查询考勤记录:优先按 F_Month 命中(与导入/单条保存写入的月份一致);兼容历史数据 F_Month 为空时按 F_Day 区间回退 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(AttendanceBookRecordEntity::getBookId, dto.getBookId()) + .and(w -> w.eq(AttendanceBookRecordEntity::getMonth, dto.getMonth()) + .or(w2 -> w2.isNull(AttendanceBookRecordEntity::getMonth) + .between(AttendanceBookRecordEntity::getDay, startDate, endDate))); + + // 如果传入了用户ID,则只查询该用户 + if (StringUtils.isNotBlank(dto.getUserId())) { + queryWrapper.eq(AttendanceBookRecordEntity::getUserId, dto.getUserId()); + } + + queryWrapper.orderByAsc(AttendanceBookRecordEntity::getDay) + .orderByAsc(AttendanceBookRecordEntity::getPeriodType); + + List records = this.list(queryWrapper); + if (records == null) { + records = Collections.emptyList(); + } + + AttendanceBookConfigEntity bookConfig = attendanceBookConfigService.getById(dto.getBookId()); + // 与月度统计一致:按考勤本假期范围取全量类型映射,并合并记录中实际出现过的类型,保证假勤汇总里「请假天数统计」列出全部假期类型(无数据为 0) + // 5. 查询所有请假类型,构建ID到名称的映射(使用统一方法,自动过滤已删除的) + Map leaveTypeIdToNameMap = getAttendanceLeaveTypeMap(bookConfig, records); + + MonthlyAttendanceStats stats = new MonthlyAttendanceStats(); + List leaveRemarks = new ArrayList<>(); + + for (AttendanceBookRecordEntity record : records) { + stats.accumulate(record); + + // 收集请假备注信息(只要有leaveType或remark就收集) + if (StringUtils.isNotBlank(record.getLeaveType()) || StringUtils.isNotBlank(record.getRemark())) { + + // 格式化日期:yyyy-MM-dd(星期X)【时段】 + String formattedDate = formatDateWithWeekday(record.getDay()); + String periodName = getPeriodName(record.getPeriodType()); + String dateInfo = formattedDate + "【" + periodName + "】"; + + // 请假类型(转换为名称) + AttendanceLeaveType leaveTypeName = StringUtils.isNotBlank(record.getLeaveType()) + ? leaveTypeIdToNameMap.getOrDefault(record.getLeaveType(), null) + : null; + + // 备注内容 + String remarkContent = StringUtils.isNotBlank(record.getRemark()) + ? record.getRemark() + : ""; + + // 构建请假备注信息:[请假类型]备注信息 + StringBuilder remarkBuilder = new StringBuilder(); + if (Objects.nonNull(leaveTypeName) && StringUtils.isNotBlank(leaveTypeName.getName())) { + remarkBuilder.append("【") + .append(leaveTypeName.getName()) + .append("】"); + } + if (StringUtils.isNotBlank(remarkContent)) { + remarkBuilder.append(remarkContent); + } + + String remark = remarkBuilder.toString(); + + // 构建请假备注项 + LeaveRemarkItemVo remarkItem = LeaveRemarkItemVo.builder() + .remark(remark) + .dateInfo(dateInfo) + .recordId(record.getId()) + .build(); + + leaveRemarks.add(remarkItem); + } + } + + LeaveRemarkSummaryVo result = LeaveRemarkSummaryVo.builder() + .leaveRemarks(leaveRemarks) + .totalCount(leaveRemarks.size()) + .actualAttendDays(stats.actualAttendDays) + .publicHolidayDays(stats.publicHolidayDays) + .lateDays(stats.lateDays) + .earlyLeaveDays(stats.earlyLeaveDays) + .missingCardDays(stats.missingCardDays) + .absenteeismDays(stats.absenteeismDays) + .businessTripDays(stats.businessTripDays) + .outOfficeDays(stats.outOfficeDays) + .fieldWorkDays(stats.fieldWorkDays) + .actualAttendCount(stats.actualAttendCount) + .publicHolidayCount(stats.publicHolidayCount) + .lateCount(stats.lateCount) + .earlyLeaveCount(stats.earlyLeaveCount) + .missingCardCount(stats.missingCardCount) + .absenteeismCount(stats.absenteeismCount) + .businessTripCount(stats.businessTripCount) + .outOfficeCount(stats.outOfficeCount) + .fieldWorkCount(stats.fieldWorkCount) + .leaveDaysByType(stats.toLeaveTypeStatisticList(leaveTypeIdToNameMap)) + .build(); + + log.info("获取假勤汇总信息成功,总数: {}", result.getTotalCount()); + return result; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceChangeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceChangeServiceImpl.java new file mode 100644 index 0000000..9ae5c3b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceChangeServiceImpl.java @@ -0,0 +1,510 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.mapper.AttendanceResultRollbackMapper; +import jnpf.attendance.service.AttendanceChangeService; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.UserInfo; +import jnpf.constants.MessageTopicConstants; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.AttendanceResultRollback; +import jnpf.entity.attendance.FtbAttendanceClockIn; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.TriggerSceneEnum; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.NoApprovalDto; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.model.AttendanceChangeNoticeModel; +import jnpf.model.attendance.vo.AttendanceRuleVo; +import jnpf.model.attendance.vo.ChangeInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.ApplyAttendanceChangeMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 考勤变更服务实现 + * + * @author yanwenfu + * @create 2024-11-08 + */ +@Slf4j +@Service +public class AttendanceChangeServiceImpl implements AttendanceChangeService { + + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + @Resource + private AttendanceClockInService attendanceClockInService; + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Resource + private AttendanceResultRollbackMapper attendanceResultRollbackMapper; + @Resource + private ApplyAttendanceChangeMapper applyAttendanceChangeMapper; + @Resource + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private RocketMQTemplate rocketMqTemplate; + + @Override + public ChangeInfoVo getChangeInfo(String clockInResultId) throws Exception { + + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectById(clockInResultId); + if (null == clockInResult) { + throw new QueryException("打卡结果不存在"); + } + AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId()); + FtbAttendanceDailyRule dailyRule = new FtbAttendanceDailyRule(); + dailyRule.setDay(rule.getDay()); + dailyRule.setInPoint(rule.getInPoint()); + dailyRule.setOutPoint(rule.getOutPoint()); + String shiftTimeStr; + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + shiftTimeStr = DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF4); + } else { + shiftTimeStr = DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF4); + } + String clockInTime = ""; + if (null != clockInResult.getClockInId()) { + LambdaQueryWrapper clockInQuery = new LambdaQueryWrapper() + .eq(FtbAttendanceClockIn::getId, clockInResult.getClockInId()); + FtbAttendanceClockIn clockIn = attendanceClockInService.getOne(clockInQuery); + if (null != clockIn) { + clockInTime = DateDetail.getDate2Str(clockIn.getClockInTime(), DateDetail.DF9); + } + } + Integer status = clockInResult.getClockInStatus(); + if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + status = ClockInStatusEnum.ABSENCE.getValue(); + } + return new ChangeInfoVo(clockInResultId, shiftTimeStr, clockInResult.getClockInType(), clockInTime, status); + } + + @Override + public List getChangeInfoList(String groupId, String userId) { + Date end = new Date(); + DateTime start = DateUtil.beginOfDay(DateUtil.offset(end, DateField.YEAR, -1)); + return attendanceClockInResultMapper.selectUserClockInResult(groupId, userId, start, end); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void attendanceChangeNoApproval(NoApprovalDto noApprovalDto) throws Exception { + + UserInfo userInfo = UserProvider.getUser(); + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectById(noApprovalDto.getClockInResultId()); + int count = attendanceClockInResultMapper.getReplyingCount(noApprovalDto.getClockInResultId()); + if (count > 0) { + throw new HandleException("当前记录正在审批中,请审批结束后再重新执行操作"); + } + changePassed(clockInResult, noApprovalDto, userInfo, userInfo.getUserId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void attendanceChangeApproval(String taskId, String passed, UserInfo user) throws Exception { + + log.error("出勤变更审批 -> taskId: {}, passed: {}, user: {}", taskId, passed, user.getUserId()); + // 根据审批id查询审批内容 + ApplyAttendanceChange apply = applyAttendanceChangeMapper.selectById(taskId); + if (null == apply) { + throw new Exception("未找到审批记录"); + } + log.info("审批内容:{}", apply); + // 查询打卡结果记录 + LambdaQueryWrapper resultQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getId, apply.getClockInResultId()) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper); + if (null == clockInResult) { + throw new Exception("当天班次已变更,请联系申请人重新发起!"); + } + FtbAttendanceDailyRule rule = attendanceDailyRuleService.getById(clockInResult.getRuleId()); + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) + || attendanceClockInService.getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockInResult.getClockInType())) { + throw new Exception("当天班次已变更,请联系申请人重新发起!"); + } + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate); + if (map.get(clockInResult.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + Integer status = null; + switch (passed) { + case "0": + status = ConstantUtil.STATUS_REFUSE; + case "2": + log.info("拒绝或撤销..."); + if (null == status) { + status = ConstantUtil.STATUS_ROLLBACK; + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getApplyId, null) + .set(AttendanceClockInResult::getApplyType, null) + .eq(AttendanceClockInResult::getId, apply.getClockInResultId()) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + attendanceClockInResultMapper.update(null, updateWrapper); + break; + case "1": + status = ConstantUtil.STATUS_PASS; + changePassed(clockInResult, new NoApprovalDto(apply.getClockInResultId(), apply.getChangeType(), apply.getChangeMinute()), user, apply.getCreatorUserId()); + break; + default: + break; + } + // 更新审批记录 + LambdaUpdateWrapper changeUpdate = new LambdaUpdateWrapper() + .set(ApplyAttendanceChange::getApproveUserId, user.getUserId()) + .set(ApplyAttendanceChange::getApproveUserName, user.getUserName()) + .set(ApplyAttendanceChange::getApproveTime, new Date()) + .set(ApplyAttendanceChange::getStatus, status) + .eq(ApplyAttendanceChange::getId, taskId); + applyAttendanceChangeMapper.update(null, changeUpdate); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void rollbackChange(String clockInResultId) throws Exception { + + AttendanceResultRollback rollback = attendanceResultRollbackMapper.selectById(clockInResultId); + if (null == rollback) { + throw new Exception("无可用的出勤变更记录可撤回"); + } + // 判断当前生效结果是否绑定打卡id, 有则删除绑定的打卡 + AttendanceClockInResult result = attendanceClockInResultMapper.selectById(clockInResultId); + if (null != result && StringUtils.isNotEmpty(result.getClockInId())) { + attendanceClockInService.removeById(result.getClockInId()); + } + // 如果撤回的是旷工记录, 需要判断成对的打卡是否还是缺卡 + Integer absence = rollback.getAbsence(); + // 成对的那个卡 + AttendanceClockInResult record = getAnotherRecord(rollback.getRuleId(), rollback.getClockInType()); + if (rollback.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + // 撤回之后是缺卡 + if (null == record) { + absence = ConstantUtil.NUM_FALSE; + } else if (record.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + absence = ConstantUtil.NUM_TRUE; + attendanceClockInResultMapper.updateToAbsence(record.getId()); + } else { + absence = ConstantUtil.NUM_FALSE; + } + } else { + // 撤回之后不是缺卡 + if (null != record && record.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + attendanceClockInResultMapper.updateAbsenceToNormal(record.getId()); + } + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getRuleId, rollback.getRuleId()) + .set(AttendanceClockInResult::getUserId, rollback.getUserId()) + .set(AttendanceClockInResult::getClockInId, rollback.getClockInId()) + .set(AttendanceClockInResult::getEffectiveTime, rollback.getEffectiveTime()) + .set(AttendanceClockInResult::getClockInType, rollback.getClockInType()) + .set(AttendanceClockInResult::getClockInStatus, rollback.getClockInStatus()) + .set(AttendanceClockInResult::getClockInKind, rollback.getClockInKind()) + .set(AttendanceClockInResult::getRepaired, rollback.getRepaired()) + .set(AttendanceClockInResult::getApplyType, rollback.getApplyType()) + .set(AttendanceClockInResult::getApplyId, rollback.getApplyId()) + .set(AttendanceClockInResult::getAbsence, absence) + .set(AttendanceClockInResult::getAbsenceLeader, rollback.getAbsenceLeader()) + .set(AttendanceClockInResult::getLastAbsenceLeader, rollback.getLastAbsenceLeader()) + .set(AttendanceClockInResult::getRestMinute, rollback.getRestMinute()) + .set(AttendanceClockInResult::getAbnormalMinute, rollback.getAbnormalMinute()) + .set(AttendanceClockInResult::getCreatorTime, rollback.getCreatorTime()) + .set(AttendanceClockInResult::getCreatorUserId, rollback.getCreatorUserId()) + .set(AttendanceClockInResult::getLastModifyTime, rollback.getLastModifyTime()) + .set(AttendanceClockInResult::getLastModifyUserId, rollback.getLastModifyUserId()) + .set(AttendanceClockInResult::getDeleteTime, rollback.getDeleteTime()) + .set(AttendanceClockInResult::getDeleteUserId, rollback.getDeleteUserId()) + .set(AttendanceClockInResult::getDeleteMark, rollback.getDeleteMark()) + .set(AttendanceClockInResult::getTenantId, rollback.getTenantId()) + .eq(AttendanceClockInResult::getId, rollback.getId()); + attendanceClockInResultMapper.update(null, updateWrapper); + attendanceResultRollbackMapper.deleteById(clockInResultId); + } + + @Override + public Integer getRollbackStatus(String clockInResultId) { + + AttendanceResultRollback rollback = attendanceResultRollbackMapper.selectById(clockInResultId); + return null == rollback ? ConstantUtil.NUM_FALSE : ConstantUtil.NUM_TRUE; + } + + /** + * 变更结果通过 + * @param clockInResult 原记录 + * @param noApprovalDto 变更参数 + * @param userInfo 变更人 + */ + private void changePassed(AttendanceClockInResult clockInResult, NoApprovalDto noApprovalDto, UserInfo userInfo, String dealUser) throws Exception { + + if (null == noApprovalDto.getChangeMinute()) { + noApprovalDto.setChangeMinute(0); + } + AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId()); + if (rule == null) { + log.info("未找到出勤规则"); + return; + } + // 判断考勤组是否被封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(rule.getUserId()), monthDate); + if (map.get(rule.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法变更!"); + } + ClockInStatusEnum statusEnum = ClockInStatusEnum.getClockInStatusEnum(noApprovalDto.getChangeType()); + if (null == statusEnum) { + throw new Exception("变更类型异常"); + } + // 变更撤回维护 + if (!statusEnum.equals(ClockInStatusEnum.ROLLBACK)) { + attendanceResultRollbackMapper.deleteById(clockInResult.getId()); + AttendanceResultRollback resultRollback = JsonUtil.getJsonToBean(clockInResult, AttendanceResultRollback.class); + resultRollback.setApplyId(null); + resultRollback.setApplyType(null); + attendanceResultRollbackMapper.insert(resultRollback); + } + // 生成新的打卡记录 + DateDetail dateDetail = new DateDetail(); + FtbAttendanceClockIn clockIn = generateClockIn(rule, clockInResult, noApprovalDto.getChangeMinute(), dateDetail); + boolean anotherChange = false; + switch (statusEnum) { + case NORMAL: + // 迟到, 早退, 缺卡, 旷工 -> 正常 + // 旷工变更为正常, 成对的另一个旷工变更为缺卡 + if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + anotherChange = true; + clockInResult.setAbsence(ConstantUtil.NUM_FALSE); + } + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + clockInResult.setEffectiveTime(rule.getInPoint()); + } else { + clockInResult.setEffectiveTime(rule.getOutPoint()); + // 设置打卡休息时间 + if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + clockInResult.setRestMinute(restMinute); + } + } + break; + case WORK_LATE: + // 上班: 正常, 迟到, 缺卡, 旷工 -> 迟到 + // 旷工变更为迟到, 成对的另一个旷工变更为缺卡 + if (null != rule.getFixedMark() && rule.getFixedMark().equals(2)) { + throw new HandleException("划线排班打卡结果不能变更为迟到"); + } + if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + anotherChange = true; + clockInResult.setAbsence(ConstantUtil.NUM_FALSE); + } + clockInResult.setClockInStatus(ClockInStatusEnum.WORK_LATE.getValue()); + clockInResult.setAbnormalMinute(noApprovalDto.getChangeMinute() * 60); + clockInResult.setEffectiveTime(dateDetail.addMinute(rule.getInPoint(), noApprovalDto.getChangeMinute())); + // 设置下班的休息时间 + setRestMinute(rule, ConstantUtil.ON_WORK, clockInResult); + break; + case HOME_EARLY: + // 下班: 正常, 早退, 缺卡, 旷工 -> 早退 + // 旷工变更为迟到, 成对的另一个旷工变更为缺卡 + if (null != rule.getFixedMark() && rule.getFixedMark().equals(2)) { + throw new HandleException("划线排班打卡结果不能变更为早退"); + } + if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + anotherChange = true; + clockInResult.setAbsence(ConstantUtil.NUM_FALSE); + } + clockInResult.setClockInStatus(ClockInStatusEnum.HOME_EARLY.getValue()); + clockInResult.setAbnormalMinute(noApprovalDto.getChangeMinute() * 60); + clockInResult.setEffectiveTime(dateDetail.addMinute(rule.getOutPoint(), -noApprovalDto.getChangeMinute())); + // 设置下班的休息时间 + setRestMinute(rule, ConstantUtil.OFF_WORK, clockInResult); + break; + case NO_CLOCK: + // 正常, 迟到, 早退 -> 缺卡 + AttendanceClockInResult workRecord = getAnotherRecord(clockInResult.getRuleId(), clockInResult.getClockInType()); + if (null != workRecord && workRecord.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + workRecord.setAbsence(ConstantUtil.NUM_TRUE); + LambdaUpdateWrapper update = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getAbsence, ConstantUtil.NUM_TRUE) + .eq(AttendanceClockInResult::getId, workRecord.getId()); + attendanceClockInResultMapper.update(null, update); + clockInResult.setAbsence(ConstantUtil.NUM_TRUE); + } + // 成对的打卡是缺卡, 则两个都变更为旷工 + clockInResult.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue()); + clockInResult.setAbnormalMinute(0); + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + clockInResult.setEffectiveTime(rule.getInPoint()); + } else { + clockInResult.setEffectiveTime(rule.getOutPoint()); + clockInResult.setRestMinute(0); + } + break; + case ROLLBACK: + // 撤销变更 + rollbackChange(clockInResult.getId()); + // 发布日统计数据生成事件 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(userInfo.getTenantId()) + .groupId(rule.getGroupId()) + .userId(clockInResult.getUserId()) + .triggerSceneEnum(TriggerSceneEnum.SCHEDULES) + .day(rule.getDay()) + .build(); + if(StrUtil.isBlank(userInfo.getTenantId()) || StrUtil.isBlank(rule.getGroupId())){ + log.error("AttendanceChangeServiceImpl#changePassed 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + return; + default: + break; + } + if (anotherChange) { + // 成对的卡取消旷工标识 + AttendanceClockInResult workRecord = getAnotherRecord(clockInResult.getRuleId(), clockInResult.getClockInType()); + if (null != workRecord) { + attendanceClockInResultMapper.updateAbsenceToNormal(workRecord.getId()); + } + } + clockInResult.setApplyType(null); + clockInResult.setApplyId(null); + clockInResult.setLastAbsenceLeader(clockInResult.getAbsenceLeader()); + clockInResult.setClockInId(clockIn.getId()); + clockInResult.setRepaired(ConstantUtil.NUM_FALSE); + clockInResult.setLastModifyTime(new Date()); + clockInResult.setLastModifyUserId(userInfo.getUserId()); + attendanceClockInService.save(clockIn); + attendanceClockInResultMapper.updateById(clockInResult); + attendanceClockInResultMapper.updateResultReplyToNull(clockInResult.getId(), dealUser); + sendResultChangedToIm(clockInResult, userInfo.getTenantId(), userInfo.getUserId(), noApprovalDto.getChangeType()); + // 发布日统计数据生成事件 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(userInfo.getTenantId()) + .groupId(rule.getGroupId()) + .userId(clockInResult.getUserId()) + .day(rule.getDay()) + .triggerSceneEnum(TriggerSceneEnum.SCHEDULES) + .build(); + if(StrUtil.isBlank(userInfo.getTenantId()) || StrUtil.isBlank(rule.getGroupId())){ + log.error("AttendanceChangeServiceImpl#changePassed 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + } + + private void setRestMinute(AttendanceRuleVo rule, Integer workType, AttendanceClockInResult clockInResult) { + // 设置打卡休息时间 + if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + // 查班次对应的上班记录 + AttendanceClockInResult workResult = getAnotherRecord(rule.getId(), workType); + if (workType.equals(ConstantUtil.ON_WORK)) { + // 变更上班, 处理下班休息时间 + if (null == workResult || workResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + return; + } + Date breakStart = clockInResult.getEffectiveTime().after(rule.getBreakStartPoint()) ? clockInResult.getEffectiveTime() : rule.getBreakStartPoint(); + Date breakEnd = workResult.getEffectiveTime().before(rule.getBreakEndPoint()) ? workResult.getEffectiveTime() : rule.getBreakEndPoint(); + int restMinute = DateDetail.calculateMinuteDiff(breakStart, breakEnd); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getRestMinute, restMinute) + .eq(AttendanceClockInResult::getId, workResult.getId()); + attendanceClockInResultMapper.update(null, updateWrapper); + } else { + // 变更下班, 处理下班休息时间 + if (null != workResult) { + Date breakStart; + if (null == workResult.getEffectiveTime()) { + breakStart = rule.getBreakStartPoint(); + } else { + breakStart = workResult.getEffectiveTime().after(rule.getBreakStartPoint()) ? workResult.getEffectiveTime() : rule.getBreakStartPoint(); + } + Date breakEnd = clockInResult.getEffectiveTime().before(rule.getBreakEndPoint()) ? clockInResult.getEffectiveTime() : rule.getBreakEndPoint(); + int restMinute = DateDetail.calculateMinuteDiff(breakStart, breakEnd); + clockInResult.setRestMinute(restMinute); + } + } + } + } + + private void sendResultChangedToIm(AttendanceClockInResult clockInResult, String tenantId, String handleUserId, Integer changeType) { + + AttendanceChangeNoticeModel model = new AttendanceChangeNoticeModel(); + model.setAttendanceNoticeModel(clockInResult.getUserId(), tenantId, AttendanceNoticeEnum.CHECK_RESULT_CHANGE); + model.setHandleUserId(handleUserId); + model.setChangeType(changeType); + model.setRuleId(clockInResult.getRuleId()); + model.setClockInType(clockInResult.getClockInType()); + attendanceNoticeHandler.send(model); + } + + private AttendanceClockInResult getAnotherRecord(String ruleId, Integer clockInType) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getRuleId, ruleId) + .eq(AttendanceClockInResult::getClockInType, clockInType.equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + return attendanceClockInResultMapper.selectOne(queryWrapper); + } + + private FtbAttendanceClockIn generateClockIn(AttendanceRuleVo rule, AttendanceClockInResult clockInResult, Integer changeMinute, DateDetail dateDetail) { + + FtbAttendanceClockIn clockIn = new FtbAttendanceClockIn(); + clockIn.setId(FtbUtil.getId()); + clockIn.setDay(rule.getDay()); + clockIn.setUserId(clockInResult.getUserId()); + if (changeMinute.equals(0)) { + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + clockIn.setClockInTime(rule.getInPoint()); + } else { + clockIn.setClockInTime(rule.getOutPoint()); + } + } else { + if (null == rule.getFixedMark() || !rule.getFixedMark().equals(2)) { + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + clockIn.setClockInTime(dateDetail.addMinute(rule.getLatePoint(), changeMinute)); + } else { + clockIn.setClockInTime(dateDetail.addMinute(rule.getOutPoint(), -changeMinute)); + } + } + } + clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + clockIn.setClockInKind(ConstantUtil.KIND_NORMAL); + return clockIn; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInPicServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInPicServiceImpl.java new file mode 100644 index 0000000..b458541 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInPicServiceImpl.java @@ -0,0 +1,53 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.mapper.AttendanceClockInPicMapper; +import jnpf.attendance.service.AttendanceClockInPicService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceClockInPic; +import jnpf.model.attendance.vo.ClockInPicVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 外勤打卡图片服务 + * + * @author yanwenfu + * @create 2023-11-21 + */ +@Slf4j +@Service +public class AttendanceClockInPicServiceImpl extends SuperServiceImpl implements AttendanceClockInPicService { + @Override + public Map> getClockInPicByIds(List ids, List appIds) { + if (CollUtil.isEmpty(ids)) { + return new HashMap<>(); + } + List clockInPics = this.lambdaQuery() + .and(x -> x.in(AttendanceClockInPic::getClockInId, ids) + .or() + .in(CollUtil.isNotEmpty(appIds), AttendanceClockInPic::getApprovalCode, appIds)) + .orderByDesc(AttendanceClockInPic::getCreatorTime) + .eq(AttendanceClockInPic::getDeleteMark, Boolean.FALSE) + .list(); + return CollUtil.isNotEmpty(clockInPics) ? clockInPics.stream().collect(Collectors.groupingBy(AttendanceClockInPic::getClockInId)) : new HashMap<>(); + } + + @Override + public List getClockInPicList(String clockInId) { + List clockInPics = this.lambdaQuery() + .eq(AttendanceClockInPic::getClockInId, clockInId) + .orderByDesc(AttendanceClockInPic::getCreatorTime) + .eq(AttendanceClockInPic::getDeleteMark, Boolean.FALSE) + .list(); + return CollUtil.isNotEmpty(clockInPics) ? clockInPics.stream().map(item -> ClockInPicVo.builder() + .clockInId(item.getClockInId()) + .picUrl(item.getPicUrl()) + .build()).collect(Collectors.toList()) : CollUtil.newArrayList(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInResultServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInResultServiceImpl.java new file mode 100644 index 0000000..ec1fd19 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInResultServiceImpl.java @@ -0,0 +1,132 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.mapper.AttendanceClockInMapper; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.service.AttendanceClockInResultService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.model.attendance.vo.ApplyResultVo; +import jnpf.model.attendance.vo.UserDayVo; +import jnpf.util.ConstantUtil; +import jnpf.workflow.mapper.ApplyAttendanceChangeMapper; +import jnpf.workflow.mapper.ApplyAttendanceRepairMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 打卡结果服务实现 + * + * @author yanwenfu + * @create 2023-11-29 + */ +@Slf4j +@Service +public class AttendanceClockInResultServiceImpl extends SuperServiceImpl implements AttendanceClockInResultService { + + @Resource + private AttendanceClockInMapper attendanceClockInMapper; + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + @Resource + private ApplyAttendanceChangeMapper applyAttendanceChangeMapper; + @Resource + private ApplyAttendanceRepairMapper applyAttendanceRepairMapper; + + @Transactional(rollbackFor = Exception.class) + @Override + public void dbMatchDeal(String requestId, List resultList, List userDayList, String updateUserId) { + // 申请中的打卡记录直接删除 + attendanceClockInMapper.deleteBatchByUserDay(userDayList, ConstantUtil.PASS_APPROVAL); + log.error("用户打卡重新匹配 - {} - 申请中的打卡记录直接删除", requestId); + // 根据userId删除原本的打卡结果 + List userIdList = userDayList.stream().map(UserDayVo::getUserId).collect(Collectors.toList()); + attendanceClockInResultMapper.deleteRecordByRuleBatch(userIdList, updateUserId); + log.error("用户打卡重新匹配 - {} - 根据userId删除原本的打卡结果", requestId); + // 新增新生成的打卡结果 筛选出缺勤记录和普通记录 + if (!resultList.isEmpty()) { + // 查询有审批记录(变更、补卡)的打卡结果, 更新为新的结果id + Map oldResultMap = resultList.stream() + .filter(v -> StringUtils.isNotEmpty(v.getOldResultId())) + .collect(Collectors.toMap(AttendanceClockInResult::getOldResultId, Function.identity())); + updateOldResultChangeList(oldResultMap); + log.error("用户打卡重新匹配 - {} - updateOldResultChangeList: {}", requestId, oldResultMap); + updateOldResultRepairList(oldResultMap); + log.error("用户打卡重新匹配 - {} - updateOldResultRepairList", requestId); + // 判断已存在记录, 将存在的记录删除 + List existList = new ArrayList<>(); + int batchSize = 500; + int total = resultList.size(); + // 分段查询 + for (int i = 0; i < total; i += batchSize) { + int end = Math.min(i + batchSize, total); + List subList = resultList.subList(i, end); + // 每次查 500 条以内 + List partList = attendanceClockInResultMapper.selectExistRecord(subList, ConstantUtil.NUM_FALSE); + existList.addAll(partList); + } + if (!existList.isEmpty()) { + List existIds = existList.stream().map(AttendanceClockInResult::getId).collect(Collectors.toList()); + this.removeBatchByIds(existIds); + } + log.error("用户打卡重新匹配 - {} - selectExistRecord", requestId); + this.saveBatch(resultList); + log.error("用户打卡重新匹配 - {} - 落库结束", requestId); + } + } + + @Override + public void updateOldResultChangeList(Map map) { + + if (!map.isEmpty()) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(ApplyAttendanceChange::getClockInResultId, map.keySet()); + List applyChangeList = applyAttendanceChangeMapper.selectList(queryWrapper); + if (!applyChangeList.isEmpty()) { + List list = new ArrayList<>(); + applyChangeList.forEach(v -> { + AttendanceClockInResult clockInResult = map.get(v.getClockInResultId()); + if (null != clockInResult) { + list.add(new ApplyResultVo(v.getId(), clockInResult.getId())); + } + }); + if (list.isEmpty()) { + return; + } + applyAttendanceChangeMapper.updateBatchOldResult(list); + } + } + } + + @Override + public void updateOldResultRepairList(Map map) { + + if (!map.isEmpty()) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(ApplyAttendanceRepair::getClockInResultId, map.keySet()); + List applyRepairList = applyAttendanceRepairMapper.selectList(queryWrapper); + List list = new ArrayList<>(); + applyRepairList.forEach(v -> { + AttendanceClockInResult clockInResult = map.get(v.getClockInResultId()); + if (null != clockInResult) { + list.add(new ApplyResultVo(v.getId(), clockInResult.getId())); + } + }); + if (list.isEmpty()) { + return; + } + applyAttendanceRepairMapper.updateBatchOldResult(list); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInServiceImpl.java new file mode 100644 index 0000000..7b2960f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceClockInServiceImpl.java @@ -0,0 +1,5253 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.google.common.collect.Lists; +import jnpf.attendance.mapper.*; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.base.vo.PageListVO; +import jnpf.constants.AttendanceConstant; +import jnpf.constants.MessageTopicConstants; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.*; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.entity.workflow.ApplyAttendanceOutside; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.entity.workflow.ApplyAttendanceViolation; +import jnpf.enums.attendance.*; +import jnpf.enums.attendance.v2.ClockOutHandleParam; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.ClockInDto; +import jnpf.model.attendance.dto.LatticeStatisticsVoDto; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.attendance.vo.event.OvertimeEvent; +import jnpf.model.common.DateRangeDto; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.BaseUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonneApi; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.util.*; +import jnpf.util.context.ThreadContext; +import jnpf.workflow.mapper.ApplyAttendanceChangeMapper; +import jnpf.workflow.mapper.ApplyAttendanceOutsideMapper; +import jnpf.workflow.mapper.ApplyAttendanceRepairMapper; +import jnpf.workflow.mapper.ApplyAttendanceViolationMapper; +import jnpf.workflow.service.FlowTaskService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.jetbrains.annotations.Nullable; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Lazy; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jnpf.constants.AttendanceConstant.HOLIDAYS_COLOR; +import static jnpf.constants.RedisConstant.ATTENDANCE_USER_SET_SCHEDULES; + +/** + * 打卡服务实现 + * + * @author yanwenfu + * @create 2023-11-21 + */ +@Slf4j +@Service +public class AttendanceClockInServiceImpl extends SuperServiceImpl implements AttendanceClockInService { + + private final AttendanceClockInResultService attendanceClockInResultService; + private final AttendanceDailyRuleService attendanceDailyRuleService; + private final AttendanceSuperAdminService attendanceSuperAdminService; + private final AttendanceRepairService attendanceRepairService; + private final AttendanceLocationSettingService attendanceLocationSettingService; + private final AttendanceBaseSettingService attendanceBaseSettingService; + private final AttendanceDayStatisticsService attendanceDayStatisticsService; + private final AttendanceUserSettingService attendanceUserSettingService; + private final AttendanceClockInPicService attendanceClockInPicService; + private final AttendanceUserService attendanceUserService; + private final AttendanceNoticeHandler attendanceNoticeHandler; + private final ClockInResultService clockInResultService; + private final OvertimeRuleService overtimeRuleService; + private final AttendanceUserBalanceService attendanceUserBalanceService; + private final FtbPersonneApi ftbPersonneApi; + @Resource + private AttendanceClockInMapper attendanceClockInMapper; + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + @Resource + private AttendanceDailyRuleMapper attendanceDailyRuleMapper; + @Resource + private AttendanceGroupUserMapper attendanceGroupUserMapper; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Resource + private ApplyAttendanceChangeMapper applyAttendanceChangeMapper; + @Resource + private ApplyAttendanceRepairMapper applyAttendanceRepairMapper; + @Resource + private ApplyAttendanceOutsideMapper applyAttendanceOutsideMapper; + @Resource + private ApplyAttendanceViolationMapper applyAttendanceViolationMapper; + @Resource + private AttendanceLeaveApproveMapper attendanceLeaveApproveMapper; + @Resource + private FlowTaskService flowTaskService; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private FtbPersonnelsRosterManagerApi rosterManagerApi; + @Resource + private UserProvider userProvider; + @Resource + private CustomTenantUtil customTenantUtil; + @Autowired + private RedisUtil redisUtil; + @Autowired + private RedissonClient redissonClient; + @Autowired + private ApplicationContext context; + @Resource + private RocketMQTemplate rocketMqTemplate; + @Resource + private DailyRuleChangeMapper dailyRuleChangeMapper; + + @Autowired + public AttendanceClockInServiceImpl(@Lazy AttendanceClockInResultService attendanceClockInResultService, @Lazy AttendanceDailyRuleService attendanceDailyRuleService, + @Lazy AttendanceSuperAdminService attendanceSuperAdminService, @Lazy AttendanceRepairService attendanceRepairService, + @Lazy AttendanceLocationSettingService attendanceLocationSettingService, @Lazy ClockInResultService clockInResultService, + @Lazy AttendanceBaseSettingService attendanceBaseSettingService, @Lazy AttendanceDayStatisticsService attendanceDayStatisticsService, + @Lazy AttendanceUserSettingService attendanceUserSettingService, @Lazy AttendanceClockInPicService attendanceClockInPicService, + @Lazy AttendanceUserService attendanceUserService, @Lazy AttendanceNoticeHandler attendanceNoticeHandler, + @Lazy OvertimeRuleService overtimeRuleService, @Lazy AttendanceUserBalanceService attendanceUserBalanceService, + @Lazy FtbPersonneApi ftbPersonneApi) { + this.attendanceClockInResultService = attendanceClockInResultService; + this.attendanceDailyRuleService = attendanceDailyRuleService; + this.attendanceSuperAdminService = attendanceSuperAdminService; + this.attendanceRepairService = attendanceRepairService; + this.attendanceLocationSettingService = attendanceLocationSettingService; + this.clockInResultService = clockInResultService; + this.attendanceBaseSettingService = attendanceBaseSettingService; + this.attendanceDayStatisticsService = attendanceDayStatisticsService; + this.attendanceUserSettingService = attendanceUserSettingService; + this.attendanceClockInPicService = attendanceClockInPicService; + this.attendanceUserService = attendanceUserService; + this.attendanceNoticeHandler = attendanceNoticeHandler; + this.overtimeRuleService = overtimeRuleService; + this.attendanceUserBalanceService = attendanceUserBalanceService; + this.ftbPersonneApi = ftbPersonneApi; + } + + @Override + public List getClockInMainInfo(Date today, UserInfo userInfo, Integer isMainInfo) throws Exception { + + // log.error("当前时间 - begin ,{}", System.currentTimeMillis()); + List groupList = new ArrayList<>(); + List dailyRuleList = clockInResultService.getTodayRuleList(today, userInfo); + // 如果今日没有出勤规则, 返回一条attendanceType为-1的记录, 且id为null + List collect = dailyRuleList.stream().filter(v -> DateDetail.checkSameDay(today, v.getDay())).collect(Collectors.toList()); + if (collect.isEmpty()) { + emptyDailyRuleDealNew(today, userInfo, dailyRuleList); + } + // log.error("当前时间 - rule-get ,{}", System.currentTimeMillis()); + boolean hasCurrentRule = false; + Map userMap = new HashMap<>(); + List ruleIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + List clockInList = attendanceClockInMapper.getClockInResultByRule(getRuleUserList(ruleIds, userInfo.getUserId())); + // 已查询过考勤组信息的考勤组 + String lastGroup = "-1"; + Set processedGroups = new HashSet<>(); + for (FtbAttendanceDailyRule dailyRule : dailyRuleList) { + if (processedGroups.add(dailyRule.getGroupId())) { + // 查询用户考勤组信息 + try { + lastGroup = dailyRule.getGroupId(); + GroupInfoVo groupInfo = getGroupInfo(dailyRule.getDay(), dailyRule.getGroupId(), dailyRule.getSelfGroup(), userInfo, isMainInfo); + groupList.add(groupInfo); + } catch (Exception e) { + // 考勤组异常 + continue; + } + } else { + if (!lastGroup.equals(dailyRule.getGroupId())) { + GroupInfoVo copyInfo = groupList.stream() + .filter(v -> v.getGroupId().equals(dailyRule.getGroupId())) + .findFirst().orElse(null); + assert null != copyInfo; + GroupInfoVo copyGroup = JsonUtil.getJsonToBean(copyInfo, GroupInfoVo.class); + copyGroup.getAttendanceRuleList().clear(); + copyGroup.getClockInList().clear(); + groupList.add(copyGroup); + } + } + if (groupList.isEmpty()) { + continue; + } + AttendanceRuleVo rule = BeanUtil.toBean(dailyRule, AttendanceRuleVo.class); + // 设置用户出勤打卡记录 + setWorkRecord(clockInList, rule, groupList.get(groupList.size() - 1), userMap); + // 判断是否当前时间段 + if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) { + // 如果排班是休/外出/出差, 直接true + if (!hasCurrentRule) { + rule.setCurrentPeriod(ConstantUtil.NUM_TRUE); + } + groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule); + break; + } + if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + // 请假 直接false + groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule); + continue; + } + if (dailyRule.getInUnbounded() != 0 && !dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()) + && !dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode()) + && dailyRule.getOutUnbounded() != 0 && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()) + && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) { + // 上下班同时隐藏 + groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule); + continue; + } + if (dailyRule.getOutUnbounded() != 0 && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LEAVE_COVERED.getCode()) && null != rule.getOnWorkInfoVo()) { + if (!dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode()) && !dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) { + // 上班隐藏无影响, 下班隐藏如果已经打了上班卡则不用等待下班时间 + groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule); + continue; + } + } + if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) { + // 普班 在开始打卡时间 - 结束打卡时间内 + Date maxDate = DateDetail.goToMaxSeconds(rule.getOutLackPoint()); + if (!hasCurrentRule && DateDetail.checkTimeBetween(today, rule.getClockStartPoint(), maxDate)) { + if (rule.getOffWorkInfoVo() == null) { + rule.setCurrentPeriod(ConstantUtil.NUM_TRUE); + hasCurrentRule = true; + } + } + // 设置缺卡时间点 + // 上班缺卡 是否允许迟到, 不允许, 上班时间一到就缺卡 + /*if (rule.getLateEnable().equals(ConstantUtil.NUM_FALSE)) { + rule.setInLackPoint(rule.getInPoint()); + }*/ + groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule); + continue; + } + if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 在上一个班的下班时间, 下一个班的上班时间 范围内, 且无下班打卡, 设置currentPeriod为true + if (DateDetail.checkTimeBetween(today, dailyRule.getClockStartPoint(), dailyRule.getOutLackPoint())) { + if (rule.getOffWorkInfoVo() == null) { + rule.setCurrentPeriod(ConstantUtil.NUM_TRUE); + hasCurrentRule = true; + } + } + groupList.get(groupList.size() - 1).getAttendanceRuleList().add(rule); + } + } + // log.error("当前时间 - rule-deal ,{}", System.currentTimeMillis()); + if (groupList.isEmpty()) { + if (isMainInfo.equals(ConstantUtil.NUM_TRUE)) { + throw new QueryException("用户暂无考勤组"); + } else { + // 查询用户有无考勤组 + String selfGroup = attendanceGroupUserMapper.getSelfGroup(userInfo.getUserId()); + if (StringUtils.isEmpty(selfGroup)) { + throw new QueryException("用户暂无排班"); + } + } + } + setCouldUpdate(groupList); + // log.error("当前时间 - could-update,{}", System.currentTimeMillis()); + return groupList; + } + + private List getRuleUserList(List ruleIds, String userId) { + + List list = new ArrayList<>(); + ruleIds.forEach(v -> list.add(new RuleUserVo(v, userId))); + return list; + } + + private void setApprovalList(List groupList, Date today, UserInfo userInfo) { + + String queryDate = DateDetail.getDate2Str(today, DateDetail.DF8); + if (!groupList.isEmpty()) { + groupList.forEach(group -> { + List clockInList = new ArrayList<>(); + group.getAttendanceRuleList().forEach(rule -> { + if (null != rule.getOnWorkInfoVo()) { + clockInList.add(new DailyClockInVo(rule.getOnWorkInfoVo().getId(), rule.getOnWorkInfoVo().getClockInKind(), rule.getOnWorkInfoVo().getClockInId())); + } + if (null != rule.getOffWorkInfoVo()) { + clockInList.add(new DailyClockInVo(rule.getOffWorkInfoVo().getId(), rule.getOffWorkInfoVo().getClockInKind(), rule.getOffWorkInfoVo().getClockInId())); + } + }); + List list = getApprovalList(clockInList, queryDate, group.getUserId(), group.getGroupId(), userInfo.getTenantId()); + list.removeIf(v -> !"已通过".equals(v.getLastResult())); + group.getApprovalList().addAll(list); + }); + } + } + + private void setCouldUpdate(List groupList) { + + if (!groupList.isEmpty()) { + List ruleList = groupList.stream() + .map(GroupInfoVo::getAttendanceRuleList) + .flatMap(List::stream) + .filter(v -> v.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) + || v.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) + .collect(Collectors.toList()); + for (int i = 0; i < ruleList.size(); i++) { + AttendanceRuleVo rule = ruleList.get(i); + if (rule.getOnWorkInfoVo() != null) { + rule.getOnWorkInfoVo().setUpdateCheck(rule.getOffWorkInfoVo() == null ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE); + } + if (rule.getOffWorkInfoVo() != null) { + if (i < ruleList.size() - 1) { + ClockInVo next = ruleList.get(i + 1).getOnWorkInfoVo(); + rule.getOffWorkInfoVo().setUpdateCheck(next == null ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE); + } else { + rule.getOffWorkInfoVo().setUpdateCheck(ConstantUtil.NUM_TRUE); + } + } + } + } + } + + private void emptyDailyRuleDealNew(Date today, UserInfo userInfo, List dailyRuleList) throws QueryException { + + // 查询今天属于哪个考勤组 + GroupInfoVo group = getGroupInfoByDate(today, userInfo); + // 判断考勤组人员关系是否正常 + AttendanceGroupUser groupUser = getGroupUser(userInfo.getUserId(), group.getGroupId()); + if (null == groupUser) { + throw new QueryException("已不是考勤组成员"); + } + FtbAttendanceDailyRule dailyRule = generateRestDailyRule(group, null); + dailyRuleList.add(dailyRule); + } + + private void emptyDailyRuleDeal(Date today, UserInfo userInfo, List groupList) throws QueryException { + + // 查询今天属于哪个考勤组 + GroupInfoVo group = getGroupInfoByDate(today, userInfo); + // 判断考勤组人员关系是否正常 + AttendanceGroupUser groupUser = getGroupUser(userInfo.getUserId(), group.getGroupId()); + if (null == groupUser) { + throw new QueryException("已不是考勤组成员"); + } + // 出勤规则是空当休息处理 + AttendanceRuleVo restRule = getRestDailyRule(group); + boolean flag = false; + for (GroupInfoVo v : groupList) { + AttendanceRuleVo vo = v.getAttendanceRuleList().stream().filter(rule -> rule.getCurrentPeriod().equals(ConstantUtil.NUM_TRUE)).findFirst().orElse(null); + if (null != vo) { + flag = true; + break; + } + } + if (!flag) { + restRule.setCurrentPeriod(ConstantUtil.NUM_TRUE); + } + GroupInfoVo existGroup = groupList.stream().filter(v -> group.getGroupId().equals(v.getGroupId())).findFirst().orElse(null); + if (null == existGroup) { + group.getAttendanceRuleList().add(restRule); + groupList.add(group); + } else { + existGroup.getAttendanceRuleList().add(restRule); + } + } + + private AttendanceRuleVo getRestDailyRule(GroupInfoVo group) throws QueryException { + + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_USER_SET_SCHEDULES, UserProvider.getUser().getTenantId(), group.getGroupId(), group.getUserId())); + if (lock.isLocked()) { + throw new QueryException("当前考勤组排班操作正在执行中,请稍后再试"); + } + FtbAttendanceDailyRule dailyRule = generateRestDailyRule(group, FtbUtil.getId()); + attendanceDailyRuleMapper.insert(dailyRule); + return JsonUtil.getJsonToBean(dailyRule, AttendanceRuleVo.class); + } + + /** + * 生成-1的出勤规则(今日无出勤规则) + * @param group 考勤组 + * @param id id可以不传(打卡时再insert到数据库) + * @return jnpf.entity.attendance.FtbAttendanceDailyRule + */ + private FtbAttendanceDailyRule generateRestDailyRule(GroupInfoVo group, String id) { + FtbAttendanceDailyRule dailyRule = new FtbAttendanceDailyRule(); + dailyRule.setId(id); + dailyRule.setGroupId(group.getGroupId()); + dailyRule.setSelfGroup(group.getSelfGroup()); + dailyRule.setDay(group.getDay()); + dailyRule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode()); + dailyRule.setUserId(group.getUserId()); + dailyRule.setValidDuration(0); + dailyRule.setApplyViewEnable(ConstantUtil.NUM_TRUE); + return dailyRule; + } + + private MutablePair getOverTimeWorkRange(AttendanceRuleVo rule, String userId) { + + DateDetail dateDetail = new DateDetail(rule.getInPoint()); + dateDetail.getYesterday(); + Date previousDate = dateDetail.getCurrentDate(); + FtbAttendanceDailyRule previousRule = attendanceDailyRuleMapper.getPreviousOrNextRule(rule.getId(), previousDate, rule.getInPoint(), userId, "previous"); + if (null != previousRule) { + previousDate = previousRule.getOutPoint(); + } + dateDetail.getTomorrow(); + dateDetail.getTomorrow(); + Date nextDate = dateDetail.getCurrentDate(); + FtbAttendanceDailyRule nextRule = attendanceDailyRuleMapper.getPreviousOrNextRule(rule.getId(), rule.getInPoint(), nextDate, userId, "next"); + if (null != nextRule) { + nextDate = nextRule.getInPoint(); + } + return MutablePair.of(previousDate, nextDate); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public MutablePair clockIn(ClockInDto clockInDto) throws Exception { + UserInfo userInfo = userProvider.get(); + if (null == userInfo || StringUtils.isEmpty(userInfo.getUserId())) { + if (StringUtils.isEmpty(clockInDto.getUserId()) || StringUtils.isEmpty(clockInDto.getTenantId())) { + return MutablePair.of(0, null); + } + userInfo = new UserInfo(); + userInfo.setUserId(clockInDto.getUserId()); + userInfo.setTenantId(clockInDto.getTenantId()); + } + // 当前考勤组及考勤规则 + MutablePair pair = getGroupInfoAndRule(new Date(), userInfo); + // 判断是上班打卡还是下班打卡 + Integer clockInType = getClockInType(pair.getRight()); + // 验证打卡方式 + addClockInMethodDeviceInfo(pair.getLeft().getClockInMethodVo(), userInfo.getUserId(), userInfo.getTenantId()); + checkDeviceType(clockInDto.getClockInKind(), clockInDto.getDeviceType(), clockInDto.getDeviceId(), pair.getRight(), pair.getLeft().getClockInMethodVo(), clockInType); + // 生成打卡记录 + FtbAttendanceClockIn clockIn = setClockInRecord(pair, clockInDto, null, userInfo); + //生成、修改外勤打卡图片 + List clockInPicAddOrUpdateList = setPicList(clockIn.getId(), clockInDto); + // 生成打卡结果 + AttendanceClockInResult clockInResult = generateClockInResultRecord(pair.getRight(), clockIn, null, clockInType, userInfo); + // 判断数据库是否已经存在当前出勤规则和打卡类型的卡, 有则新增失败 + LambdaQueryWrapper resultQuery = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getRuleId, clockInResult.getRuleId()) + .eq(AttendanceClockInResult::getClockInType, clockInResult.getClockInType()) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQuery); + if (null != dbResult) { + String remark = clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? "上班卡" : "下班卡"; + throw new HandleException("您已经打过" + remark + "了"); + } + // 保存数据库 + attendanceClockInMapper.insert(clockIn); + if (CollUtil.isNotEmpty(clockInPicAddOrUpdateList)) { + attendanceClockInPicService.saveOrUpdateBatch(clockInPicAddOrUpdateList); + } + attendanceClockInResultMapper.insert(clockInResult); + // 发送消息 + if (null != clockInDto.getFast() && clockInDto.getFast().equals(ConstantUtil.NUM_TRUE)) { + AttendanceNoticeEnum noticeEnum = clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? AttendanceNoticeEnum.CHECK_IN_QUICK : AttendanceNoticeEnum.CHECK_OUT_QUICK; + FastClockInNoticeModel model = new FastClockInNoticeModel(); + model.setWorkTime(clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? pair.getRight().getInPoint() : pair.getRight().getOutPoint()); + model.setClockIn(clockIn); + model.setAttendanceNoticeModel(clockInResult.getUserId(), pair.getRight().getGroupId(), userInfo.getTenantId(), new Date(), noticeEnum); + attendanceNoticeHandler.send(model); + } + // 发布日统计数据生成事件 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(userInfo.getTenantId()) + .groupId(pair.getLeft().getGroupId()) + .userId(clockInResult.getUserId()) + .day(clockIn.getDay()) + .triggerSceneEnum(TriggerSceneEnum.CHECK_IN) + .build(); + if(StrUtil.isBlank(userInfo.getTenantId()) || StrUtil.isBlank(pair.getLeft().getGroupId())){ + log.error("AttendanceClockInServiceImpl#clockIn 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + return MutablePair.of(clockInResult.getClockInStatus(), clockInResult.getId()); + } + + private void addClockInMethodDeviceInfo(ClockInMethodVo clockInMethodVo, String userId, String tenantId) { + + List machineList = clockInResultService.getMachineList(userId, tenantId); + if (!machineList.isEmpty()) { + List collect = machineList.stream().map(v -> new MethodVo(v.getName(), v.getMac(), v.getType())).collect(Collectors.toList()); + clockInMethodVo.getDeviceList().addAll(collect); + } + } + + private List setPicList(String clockInId, ClockInDto clockInDto) { + List clockInPicAddOrUpdateList = CollUtil.newArrayList(); + if (StringUtils.isNotEmpty(clockInDto.getApprovalCode())) { + //审批进来的 + clockInPicAddOrUpdateList = attendanceClockInPicService.lambdaQuery().eq(AttendanceClockInPic::getApprovalCode, clockInDto.getApprovalCode()).list(); + if (CollUtil.isNotEmpty(clockInPicAddOrUpdateList)) { + clockInPicAddOrUpdateList.forEach(item -> item.setClockInId(clockInId)); + } + } else { + //直接打卡进来的 + if (CollUtil.isNotEmpty(clockInDto.getPicUrlList())) { + clockInPicAddOrUpdateList.addAll(clockInDto.getPicUrlList().stream().map(item -> AttendanceClockInPic.builder() + .clockInId(clockInId) + .picUrl(item) + .build()).collect(Collectors.toList())); + } + } + return clockInPicAddOrUpdateList; + } + + private void checkDeviceType(Integer kind, Integer deviceType, String address, AttendanceRuleVo rule, ClockInMethodVo clockInMethod, Integer clockInType) throws HandleException { + + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode()) + || AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(rule, clockInType)) { + return; + } + switch (deviceType) { + case ConstantUtil.DEVICE_PLACE: + if (clockInMethod.getLocationList().isEmpty() && !kind.equals(ConstantUtil.KIND_OUTSIDE)) { + throw new HandleException("无可用的打卡地点,可前往考勤组设置"); + } + break; + case ConstantUtil.DEVICE_MACHINE: + if (clockInMethod.getDeviceList().isEmpty()) { + throw new HandleException("无可用的考勤机,可前往考勤组设置"); + } else { + MethodVo method = clockInMethod.getDeviceList().stream().filter(v -> v.getAddress().equals(address)).findFirst().orElse(null); + if (null == method) { + throw new HandleException("非法的考勤机"); + } + } + break; + case ConstantUtil.DEVICE_WIFI: + if (clockInMethod.getWifiList().isEmpty()) { + throw new HandleException("无可用的WIFI,可前往考勤组设置"); + } + break; + default: + break; + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public MutablePair updateClockIn(String clockInId, ClockInDto clockInDto) throws Exception { + + UserInfo userInfo = userProvider.get(); + if (null == userInfo || StringUtils.isEmpty(userInfo.getUserId())) { + if (StringUtils.isEmpty(clockInDto.getUserId()) || StringUtils.isEmpty(clockInDto.getTenantId())) { + return MutablePair.of(0, null); + } + userInfo = new UserInfo(); + userInfo.setUserId(clockInDto.getUserId()); + userInfo.setTenantId(clockInDto.getTenantId()); + } + // 查询打卡记录 + AttendanceClockInResult dbResult = getClockInResultById(clockInId); + if (null == dbResult) { + throw new QueryException("未找到打卡记录"); + } + // 查询打卡记录的出勤规则 + AttendanceRuleVo rule = getAttendanceRule(dbResult.getRuleId()); + // 查询考勤组规则 + GroupInfoVo groupInfo = getGroupInfo(rule.getDay(), rule.getGroupId(), rule.getSelfGroup(), userInfo); + // 查询考勤组规则 + Map userMap = new HashMap<>(); + List clockInList = attendanceClockInMapper.getClockInResultByRule(getRuleUserList(List.of(rule.getId()), userInfo.getUserId())); + setWorkRecord(clockInList, rule, groupInfo, userMap); + MutablePair pair = MutablePair.of(groupInfo, rule); + // 判断是上班打卡还是下班打卡 + Integer clockInType = dbResult.getClockInType(); + // 生成打卡记录 + FtbAttendanceClockIn clockIn = setClockInRecord(pair, clockInDto, clockInId, userInfo); + // 生成打卡结果 + AttendanceClockInResult clockInResult = generateClockInResultRecord(pair.getRight(), clockIn, null, clockInType, userInfo); + // V1.8.1 新增更新打卡图片功能 + //生成、修改外勤打卡图片 + List clockInPicAddOrUpdateList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(clockInDto.getPicUrlList())) { + clockInPicAddOrUpdateList.addAll(clockInDto.getPicUrlList().stream().map(item -> AttendanceClockInPic.builder() + .clockInId(clockIn.getId()) + .picUrl(item) + .build()).collect(Collectors.toList())); + } + if (StringUtils.isNotEmpty(dbResult.getApplyId()) || StringUtils.isNotEmpty(clockInResult.getApplyId())) { + // 审批中, 更新上一次打卡结果为已删除 + dbResult.setDeleteMark(ConstantUtil.NUM_TRUE); + dbResult.setDeleteTime(new Date()); + dbResult.setUserId(userInfo.getUserId()); + attendanceClockInResultMapper.updateById(dbResult); + attendanceClockInResultMapper.insert(clockInResult); + // 审批中, 标识图片删除 + attendanceClockInPicService.update(new LambdaUpdateWrapper() + .eq(AttendanceClockInPic::getClockInId, clockInId) + .set(AttendanceClockInPic::getDeleteMark, ConstantUtil.NUM_TRUE)); + } else { + // 直接在上一次打卡结果上更新 + clockInResult.setId(dbResult.getId()); + clockInResult.setDeleteMark(ConstantUtil.NUM_FALSE); + attendanceClockInResultMapper.updateById(clockInResult); + // 删除原来的图片 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("F_ClockInId", clockInId); + attendanceClockInPicService.remove(queryWrapper); + } + // 保存数据库 + attendanceClockInMapper.insert(clockIn); + // 打卡图片不为空 + if (CollUtil.isNotEmpty(clockInPicAddOrUpdateList)) { + attendanceClockInPicService.saveOrUpdateBatch(clockInPicAddOrUpdateList); + } + if (StringUtils.isNotEmpty(clockInDto.getApprovalCode())) { + attendanceClockInPicService.update(new LambdaUpdateWrapper() + .set(AttendanceClockInPic::getClockInId, clockIn.getId()) + .eq(AttendanceClockInPic::getApprovalCode, clockInDto.getApprovalCode()) + .eq(AttendanceClockInPic::getDeleteMark, ConstantUtil.NUM_FALSE)); + } + // 发布日统计数据生成事件 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(userInfo.getTenantId()) + .groupId(rule.getGroupId()) + .userId(clockInResult.getUserId()) + .day(clockIn.getDay()) + .triggerSceneEnum(TriggerSceneEnum.CHECK_IN) + .build(); + if(StrUtil.isBlank(userInfo.getTenantId()) || StrUtil.isBlank(rule.getGroupId())){ + log.error("AttendanceClockInServiceImpl#updateClockIn 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + return MutablePair.of(clockInResult.getClockInStatus(), clockInResult.getId()); + } + + private AttendanceClockInResult getClockInResultById(String clockInId) { + + LambdaQueryWrapper resultQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getClockInId, clockInId) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE) + .orderByDesc(AttendanceClockInResult::getCreatorTime) + .last("limit 1"); + return attendanceClockInResultMapper.selectOne(resultQueryWrapper); + } + + @Override + public AttendanceRuleVo getAttendanceRule(String ruleId) throws HandleException { + + LambdaQueryWrapper dailyRuleQueryWrapper = new LambdaQueryWrapper() + .eq(FtbAttendanceDailyRule::getId, ruleId); + FtbAttendanceDailyRule dailyRule = attendanceDailyRuleMapper.selectOne(dailyRuleQueryWrapper); + if (null == dailyRule) { + throw new HandleException("未查询到当前出勤规则"); + } + if (null == dailyRule.getInUnbounded()) { + dailyRule.setInUnbounded(ConstantUtil.NUM_FALSE); + } + if (null == dailyRule.getOutUnbounded()) { + dailyRule.setOutUnbounded(ConstantUtil.NUM_FALSE); + } + // 加班获取加班规则 如果加班规则为空, 说明出勤规则有问题 + if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + if (StringUtils.isNotEmpty(dailyRule.getPeriodInfo())) { + OvertimeRuleDetailVo overtimeRuleDetail = JsonUtil.getJsonToBean(dailyRule.getPeriodInfo(), OvertimeRuleDetailVo.class); + dailyRule.setOvertimeRuleDetail(overtimeRuleDetail); + } + } else { + // 无需审批加班需要自己查询加班规则 查询用户加班配置 + OvertimeRuleVo overtimeRule = overtimeRuleService.getEffectDetail(dailyRule.getUserId(), null, dailyRule.getDay()); + if (null != overtimeRule) { + // 判定用户使用哪个配置 节假日/工作日/公休日 + FtbAttendanceDailyRule nextRule = attendanceDailyRuleMapper.selectOne(new LambdaQueryWrapper() + .eq(FtbAttendanceDailyRule::getDay, dailyRule.getDay()) + .gt(FtbAttendanceDailyRule::getInPoint, dailyRule.getInPoint()) + .orderByAsc(FtbAttendanceDailyRule::getInPoint) + .last("limit 1")); + Integer attendanceType; + if (null == nextRule) { + attendanceType = getAttendanceType(dailyRule, null, null); + } else { + attendanceType = getAttendanceType(dailyRule, nextRule.getId(), nextRule.getAttendanceType()); + } + OvertimeRuleDetailVo effectWorkDetail = overtimeRuleService.getEffectWorkDetail(overtimeRule, dailyRule.getDay(), attendanceType); + if (null != effectWorkDetail) { + dailyRule.setOvertimeRuleDetail(effectWorkDetail); + if (effectWorkDetail.getCalcMethod().equals(3)) { + dailyRule.setOvertime(ConstantUtil.NUM_TRUE); + } + } + } + } + setOtherField(dailyRule); + return JsonUtil.getJsonToBean(dailyRule, AttendanceRuleVo.class); + } + + @Override + public Map getAttendanceRuleBatch(List ruleIds) { + + Map returnMap = new HashMap<>(); + // 使用 Guava 的 Lists.partition 分批 + List> partitions = Lists.partition(ruleIds, 1000); + for (List batch : partitions) { + // 批量查询 dailyRule + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbAttendanceDailyRule::getId, batch); + List dailyRuleList = attendanceDailyRuleMapper.selectList(wrapper); + if (dailyRuleList == null || dailyRuleList.isEmpty()) { + continue; + } + // 按 day 分组 + Map> dayMap = dailyRuleList.stream() + .collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay)); + for (Map.Entry> entry : dayMap.entrySet()) { + Date day = entry.getKey(); + List dayRuleList = entry.getValue(); + // 获取该天用户列表 + List userIds = dayRuleList.stream() + .map(FtbAttendanceDailyRule::getUserId) + .distinct() + .collect(Collectors.toList()); + // 获取加班规则 + Map overtimeMap = overtimeRuleService.getEffectDetailByUserList(userIds, day); + // 查询每个规则的下一个规则 + List dayRuleIds = dayRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + List nextRuleList = attendanceDailyRuleMapper.getNextRulePartInfo(dayRuleIds, DateDetail.getDate2Str(day, DateDetail.DF)); + Map nextRuleMap = nextRuleList.stream().collect(Collectors.toMap(NextRuleVo::getRuleId, Function.identity())); + for (FtbAttendanceDailyRule dailyRule : dayRuleList) { + // 判空设置默认值 + if (dailyRule.getInUnbounded() == null) dailyRule.setInUnbounded(ConstantUtil.NUM_FALSE); + if (dailyRule.getOutUnbounded() == null) dailyRule.setOutUnbounded(ConstantUtil.NUM_FALSE); + // 加班规则处理 + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(dailyRule.getAttendanceType())) { + if (StringUtils.isNotEmpty(dailyRule.getPeriodInfo())) { + OvertimeRuleDetailVo overtimeRuleDetail = JsonUtil.getJsonToBean(dailyRule.getPeriodInfo(), OvertimeRuleDetailVo.class); + dailyRule.setOvertimeRuleDetail(overtimeRuleDetail); + } + } else { + OvertimeRuleVo overtimeRule = overtimeMap.get(dailyRule.getUserId()); + if (overtimeRule != null) { + NextRuleVo nextRule = nextRuleMap.get(dailyRule.getId()); + if (nextRule != null) { + Integer attendanceType = getAttendanceType(dailyRule, nextRule.getNextRuleId(), nextRule.getNextAttendanceType()); + OvertimeRuleDetailVo effectWorkDetail = overtimeRuleService.getEffectWorkDetail(overtimeRule, dailyRule.getDay(), attendanceType); + if (effectWorkDetail != null) { + dailyRule.setOvertimeRuleDetail(effectWorkDetail); + if (effectWorkDetail.getCalcMethod().equals(3)) { + dailyRule.setOvertime(ConstantUtil.NUM_TRUE); + } + } + } + } + } + // 设置其他字段 + setOtherField(dailyRule); + // 转成 VO + AttendanceRuleVo bean = JsonUtil.getJsonToBean(dailyRule, AttendanceRuleVo.class); + returnMap.put(bean.getId(), bean); + } + } + } + return returnMap; + } + + private void setOtherField(FtbAttendanceDailyRule dailyRule) { + // 借调 + if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) { + dailyRule.setInHideStatus(ConstantUtil.STR_TRUE); + dailyRule.setOnWorkIgnore(true); + dailyRule.setInLackPoint(dailyRule.getInPoint()); + } + if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) { + dailyRule.setOutHideStatus(ConstantUtil.STR_TRUE); + dailyRule.setOffWorkIgnore(true); + dailyRule.setOutLackPoint(dailyRule.getOutPoint()); + } + // 加班 + if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + dailyRule.setInHideStatus(ConstantUtil.STR_TRUE); + dailyRule.setOnWorkIgnore(true); + dailyRule.setClockStartPoint(dailyRule.getInPoint()); + dailyRule.setInLackPoint(dailyRule.getInPoint()); + } + if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.OVERTIME_COVERED.getCode())) { + dailyRule.setOutHideStatus(ConstantUtil.STR_TRUE); + dailyRule.setOffWorkIgnore(true); + dailyRule.setOutLackPoint(dailyRule.getOutPoint()); + } + // 晚走晚到 + if (dailyRule.getInUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) { + // 在这个时间点生成无需打卡 -2 + dailyRule.setOnWorkIgnore(true); + } + if (dailyRule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE.getCode())) { + // 在这个时间点生成无需打卡 -2 + dailyRule.setOffWorkIgnore(true); + } + if (dailyRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 按审批时长申请计算 无需打卡 + if (dailyRule.getOvertimeRuleDetail().getCalcMethod().equals(1)) { + dailyRule.setOnWorkIgnore(true); + dailyRule.setOffWorkIgnore(true); + } + } + } + + private Integer getAttendanceType(FtbAttendanceDailyRule rule, String nextRuleId, Integer nextRuleAttendanceType) { + Integer attendanceType; + if (rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) { + return AttendanceTypeEnum.ORDINARY.getCode(); + } + if (null == nextRuleId) { + attendanceType = rule.getAttendanceType(); + } else { + // rule 和 nextRule 中有一个休 则出勤类型为休, 否则为工作日 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || nextRuleAttendanceType.equals(AttendanceTypeEnum.REST.getCode())) { + attendanceType = AttendanceTypeEnum.REST.getCode(); + } else { + attendanceType = AttendanceTypeEnum.ORDINARY.getCode(); + } + } + return attendanceType; + } + + @Override + public void changeAttendanceRuleBatch(List userDayList, UserInfo updateUser) { + + String requestId = FtbUtil.getId(); + log.error("用户打卡重新匹配 - {} - init...", requestId); + String updateUserId = updateUser.getUserId(); + String tenantId = updateUser.getTenantId(); + if (null == userDayList || userDayList.isEmpty()) { + return; + } + // 备份list + Date now = new Date(); + List backupList = new ArrayList<>(userDayList); + Map> userGroupMap = userDayList.stream() + .collect(Collectors.groupingBy( + item -> item.getUserId() + "_" + item.getDayStr(), + Collectors.mapping(UserDayVo::getGroupId, Collectors.toList()) + )); + // 只有今天以及今天之前的班次变动才需要匹配打卡 + userDayList.removeIf(v -> v.getDay().after(new Date())); + if (userDayList.isEmpty()) { + return; + } + log.error("用户打卡重新匹配 - {} - 匹配日期: {}", requestId, JsonUtil.getObjectToString(userDayList)); + // 查询用户当日 + 昨日的出勤规则 + List dailyRuleList = attendanceDailyRuleMapper.getDailyRuleListBatch(userDayList); + if (CollUtil.isEmpty(dailyRuleList)) { + log.error("用户打卡重新匹配 - {} - 未查询到出勤规则", requestId); + return; + } + log.error("用户打卡重新匹配 - {} - 查询排班结果: {}", requestId, + JsonUtil.getObjectToString(dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).distinct().collect(Collectors.toList()))); + // 按目标日期 + 用户分组 + Map> groupedMap = dailyRuleList.stream() + .collect(Collectors.groupingBy(rule -> rule.getUserId() + "_" + rule.getTargetDate())); + Map> ruleListByUserDay = clockInResultService.getRuleListByUserDay(groupedMap, userGroupMap); + // 当前的打卡记录列表 + List clockInList = new ArrayList<>(); + int batchSize = 150; + for (int i = 0; i < userDayList.size(); i += batchSize) { + List batch = userDayList.subList(i, Math.min(i + batchSize, userDayList.size())); + if (batch.isEmpty()) { + continue; + } + List list = attendanceClockInMapper.selectListBatch(batch, ConstantUtil.PASS_TRUE); + if (list != null && !list.isEmpty()) { + clockInList.addAll(list); + } + } + log.error("用户打卡重新匹配 - {} - 查询到的所有打卡记录: {}", requestId, clockInList.stream().map(FtbAttendanceClockIn::getId).collect(Collectors.toList())); + Map dbClockInMap; + if (!clockInList.isEmpty()) { + List clockInIds = clockInList.stream().map(FtbAttendanceClockIn::getId).collect(Collectors.toList()); + List clockInResultList = attendanceClockInResultMapper.selectBatchByClockInIds(clockInIds); + dbClockInMap = clockInResultList.stream().collect(Collectors.toMap(AttendanceClockInResult::getClockInId, Function.identity())); + } else { + dbClockInMap = new HashMap<>(); + } + log.error("用户打卡重新匹配 - {} - 过滤后可用的打卡: {}", requestId, clockInList.stream().map(FtbAttendanceClockIn::getId).collect(Collectors.toList())); + // 删除这些出勤规则关联的打卡记录 + ConcurrentMap> map = dailyRuleList.stream().collect(Collectors.groupingByConcurrent(FtbAttendanceDailyRule::getUserId)); + ConcurrentMap> clockInMap = clockInList.stream() + .collect(Collectors.groupingByConcurrent(clockIn -> clockIn.getUserId() + "_" + DateDetail.getDate2Str(clockIn.getDay(), DateDetail.DF))); + // 生成打卡结果 + List resultList = new ArrayList<>(); + Set distinctSet = new HashSet<>(); + List overtimeInfoList = new ArrayList<>(); + ruleListByUserDay.forEach((k, ruleList) -> { + UserInfo userInfo = new UserInfo(); + String[] split = k.split("_"); + String userId = split[0]; + String targetDate = split[1]; + userInfo.setUserId(userId); + userInfo.setTenantId(tenantId); + List collect = ruleList == null ? + new ArrayList<>() : ruleList.stream().filter(rule -> DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(targetDate)) + .collect(Collectors.toList()); + List clockInCollect = clockInMap.getOrDefault(k, new ArrayList<>()); + // 生成打卡结果列表 + List matchList = getMatchList(collect, userInfo, clockInCollect, dbClockInMap, overtimeInfoList); + matchList.forEach(match -> { + String mStr = match.getRuleId() + "_" + match.getClockInType(); + if (distinctSet.add(mStr)) { + resultList.add(match); + } + }); + }); + log.error("用户打卡重新匹配 - {} - 开始匹配新的打卡结果", requestId); + attendanceClockInResultService.dbMatchDeal(requestId, resultList, userDayList, updateUserId); + log.error("用户打卡重新匹配 - {} - DB处理完成", requestId); + if (!overtimeInfoList.isEmpty()) { + attendanceUserBalanceService.overTime(overtimeInfoList, tenantId); + log.error("用户打卡重新匹配 - {} - 加班通知结束", requestId); + } + // 发送生日日统计通知 + String groupId = CollUtil.isNotEmpty(dailyRuleList) && Objects.nonNull(dailyRuleList.get(0)) ? dailyRuleList.get(0).getGroupId() : null; + Map> daysByPeriod = userDayList.stream().collect(Collectors.groupingBy(UserDayVo::getDay)); + daysByPeriod.forEach((day, list) -> { + //调用生成日统计数据接口 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .day(day) + .build(); + List userIds = list.stream().map(UserDayVo::getUserId).distinct().collect(Collectors.toList()); + userIds.forEach(item -> { + courseEventDTO.setUserId(item); + if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(groupId)){ + log.error("AttendanceClockInServiceImpl#changeAttendanceRuleBatch 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + return; + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + }); + }); + // 用于删除变更记录表的记录 + List todayList = new ArrayList<>(); + List otherList = new ArrayList<>(); + backupList.forEach(v -> { + if (DateDetail.checkSameDay(v.getDay(), now)) { + todayList.add(v); + } else { + otherList.add(v); + } + }); + if (!otherList.isEmpty()) { + dailyRuleChangeMapper.removeBatch(otherList, now); + } + log.error("用户打卡重新匹配 - 新结果: {}", JsonUtil.getObjectToString(resultList)); + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + // 缺卡规则变更 + UserInfo user = UserProvider.getUser(); + customTenantUtil.checkOutTenant(user.getTenantId()); + userDayList.forEach(v -> { + if (DateDetail.checkSameDay(v.getDay(), new Date())) { + List ruleList = map.get(v.getUserId()); + personalAbsenceTimeChange(requestId, v.getUserId(), tenantId, ruleList); + } + }); + dailyRuleChangeMapper.removeBatch(todayList, now); + }, updateUser)); + } + + private List getMatchList(List dailyRuleList, UserInfo userInfo, List clockInList, + Map dbClockInMap, List overtimeInfoList) { + + List resultList = new ArrayList<>(); + List ruleList = JsonUtil.getJsonToList(dailyRuleList, AttendanceRuleVo.class); + for (AttendanceRuleVo rule : ruleList) { + try { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + // 假 无需打卡 + continue; + } + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) { + if (ruleList.size() > 1) { + continue; + } + // 休 匹配第一条和最后一条 + if (clockInList.size() >= 1) { + AttendanceClockInResult result = generateClockInResultRecord(rule, clockInList.get(0), + dbClockInMap.get(clockInList.get(0).getId()), ConstantUtil.ON_WORK, userInfo); + resultList.add(result); + } + if (clockInList.size() >= 2) { + AttendanceClockInResult result = generateClockInResultRecord(rule, clockInList.get(clockInList.size() - 1), + dbClockInMap.get(clockInList.get(clockInList.size() - 1).getId()), ConstantUtil.OFF_WORK, userInfo); + resultList.add(result); + } + break; + } + if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) { + // 班 优先选择正常, 次选迟到、早退, 无 则缺卡 + // 选择打卡记录后 移除本身和打卡时间小于此打卡记录的所有记录 + if (!clockInList.isEmpty()) { + // 选择上班记录 靠近上班时间的打卡记录, 优选正常, 次选迟到 + Date endDate = rule.getLateEnable().equals(ConstantUtil.NUM_TRUE) ? rule.getLatePoint() : rule.getInPoint(); + endDate = setSecondTo59(endDate); + Map chooseMap = getChooseClockRecord(rule.getClockStartPoint(), endDate, clockInList); + if (!chooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, chooseMap, ConstantUtil.MAX, rule, ConstantUtil.ON_WORK, userInfo); + } else { + // 是否允许迟到, 不允许直接缺卡 + if (rule.getLateEnable().equals(ConstantUtil.NUM_TRUE)) { + // 允许迟到, 筛选迟到记录 + Map lateChooseMap = getChooseClockRecord(rule.getLatePoint(), setSecondTo59(rule.getInLackPoint()), clockInList); + if (!lateChooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, lateChooseMap, ConstantUtil.MIN, rule, ConstantUtil.ON_WORK, userInfo); + } else { + // 无记录则判断当前时间是否超过缺卡时间, 超过则生成缺卡记录 + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getInLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } else { + // 不允许迟到, 生成缺卡 + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getInLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + } else { + // 上班缺卡 + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getInLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + if (!clockInList.isEmpty()) { + // 选择下班记录 靠近下班时间的打卡记录, 优选正常, 次选早退 + Date beginDate = rule.getOutPoint(); + Map chooseMap = getChooseClockRecord(beginDate, setSecondTo59(rule.getOutLackPoint()), clockInList); + if (!chooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, chooseMap, ConstantUtil.MIN, rule, ConstantUtil.OFF_WORK, userInfo); + } else { + // 是否允许早退, 不允许直接缺卡 + if (rule.getEarlyEnable().equals(ConstantUtil.NUM_TRUE)) { + Date endPoint = rule.getOutPoint(); + Map earlyChooseMap = getChooseClockRecord(rule.getEarlyPoint(), endPoint, clockInList); + if (!earlyChooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, earlyChooseMap, ConstantUtil.MAX, rule, ConstantUtil.OFF_WORK, userInfo); + } else { + // 无记录则判断当前时间是否超过缺卡时间, 超过则生成缺卡记录 + if (new Date().after(rule.getOutLackPoint())) { + AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null); + AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + } else { + // 不允许早退, 生成缺卡 + AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null); + AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + } else { + // 下班缺卡 + AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null); + AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + Date today = new Date(); + if (null != rule.getOutLackPoint() && !DateDetail.checkSameDay(rule.getOutLackPoint(), today) && rule.getOutLackPoint().before(today)) { + // 变更的过去的加班 如果加班类型是按申请时长加班, 则转存休 + if (null != rule.getOvertimeRuleDetail() && rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) { + overtimeInfoList.add(new OvertimeInfoVo(rule.getId(), rule.getDay(), rule.getGroupId(), rule.getOvertimeRuleDetail())); + } + } + // 加班 查询开始时间 结束时间 + if (!clockInList.isEmpty()) { + Map onWorkChooseMap = getChooseClockRecord(rule.getClockStartPoint(), rule.getInPoint(), clockInList); + if (!onWorkChooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, onWorkChooseMap, ConstantUtil.MAX, rule, ConstantUtil.ON_WORK, userInfo); + } else { + Map lateChooseMap = getChooseClockRecord(rule.getInPoint(), rule.getOutPoint(), clockInList); + if (!lateChooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, lateChooseMap, ConstantUtil.MIN, rule, ConstantUtil.ON_WORK, userInfo); + } else { + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getOutPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + } else { + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, ConstantUtil.ON_WORK, rule.getOutPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + if (!clockInList.isEmpty()) { + Map offWorkChooseMap = getChooseClockRecord(rule.getOutPoint(), rule.getOutLackPoint(), clockInList); + if (!offWorkChooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, offWorkChooseMap, ConstantUtil.MIN, rule, ConstantUtil.OFF_WORK, userInfo); + } else { + Map earlyChooseMap = getChooseClockRecord(rule.getInPoint(), rule.getOutPoint(), clockInList); + if (!earlyChooseMap.isEmpty()) { + dealChooseRecord(resultList, clockInList, dbClockInMap, earlyChooseMap, ConstantUtil.MAX, rule, ConstantUtil.OFF_WORK, userInfo); + } else { + AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null); + AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + } else { + AttendanceClockInResult r1 = resultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).findFirst().orElse(null); + AttendanceClockInResult result = generateNoClockRecordRematch(rule, r1, ConstantUtil.OFF_WORK, rule.getOutLackPoint(), userInfo); + if (null != result) { + resultList.add(result); + } else { + break; + } + } + } + } catch (Exception e) { + log.error(e.getMessage()); + } + } + return resultList; + } + + public Date setSecondTo59(Date date) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 更新指定用户的缺卡时间点 + * + * @param userId 用户id + * @param dailyRuleList 用户的出勤规则 + */ + private void personalAbsenceTimeChange(String requestId, String userId, String tenantId, List dailyRuleList) { + log.error("定时器缺卡任务更新 - {} - init: {}, {}", requestId, userId, LocalDateTime.now()); + Date today = new Date(); + // 查询缺卡缓存 + String todayStr = DateDetail.getDate2Str(today, DateDetail.DF); + String hashKey = tenantId + ":" + todayStr; + // 旷工时间更新 + absenceTimeChange(userId, today, hashKey); + log.error("定时器缺卡任务更新 - {} - 旷工时间更新: {}, {}", requestId, userId, LocalDateTime.now()); + // 打卡提醒时间更新 + remindTimeChange(dailyRuleList, hashKey, today); + log.error("定时器缺卡任务更新 - {} - 执行结束: {}, {}", requestId, userId, LocalDateTime.now()); + } + + private void remindTimeChange(List dailyRuleList, String hashKey, Date today) { + + DateDetail dateDetail = new DateDetail(today); + String redisKey = ConstantUtil.FTB_REMIND_KEY + hashKey; + Map> remindMap = new HashMap<>(); + Set userIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getUserId).collect(Collectors.toSet()); + // 查询班次用户的提醒配置 + List settingList = attendanceUserSettingService.getSettingList(new ArrayList<>(userIds), UserSettingTypeEnum.USER.getCode(), + Stream.of(UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode(), + UserSettingEnum.ATTENDANCE_SETTING_PRE_WORK_REMINDER.getCode(), + UserSettingEnum.ATTENDANCE_SETTING_END_WORK_REMINDER.getCode()) + .collect(Collectors.toList())); + ConcurrentMap> settingMap = settingList.stream().collect(Collectors.groupingByConcurrent(UserSettingVo::getAssociationId)); + for (FtbAttendanceDailyRule rule : dailyRuleList) { + List userSettingList = settingMap.get(rule.getUserId()); + if (null == userSettingList || userSettingList.isEmpty()) { + continue; + } + Map codeMap = userSettingList.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + UserSettingVo receiveSetting = codeMap.get(UserSettingEnum.MESSAGE_RECEIVE); + if (null == receiveSetting || ConstantUtil.NUM_TRUE != receiveSetting.getStatus()) { + continue; + } + // 判断上班时间是否在今天 + if (null != rule.getInPoint() && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF))) { + UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.PRE_WORK_REMINDER); + if (null != preWorkSetting && null != preWorkSetting.getValue()) { + // 计算提醒时间 + dateDetail.addMinute(rule.getInPoint(), -preWorkSetting.getValue()); + if (new Date().before(dateDetail.getCurrentDate()) && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) { + List voList = remindMap.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>()); + RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2)); + voList.add(vo); + } + } + } + // 判断下班时间是否在今天 + if (null != rule.getOutPoint() && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF))) { + UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.END_WORK_REMINDER); + if (null != preWorkSetting && null != preWorkSetting.getValue()) { + // 计算提醒时间 + dateDetail.addMinute(rule.getOutPoint(), preWorkSetting.getValue()); + if (new Date().before(dateDetail.getCurrentDate()) && DateDetail.getDate2Str(today, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) { + List voList = remindMap.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>()); + RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2)); + voList.add(vo); + } + } + } + } + if (!remindMap.isEmpty()) { + remindMap.forEach((k, v) -> redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v))); + } + } + + private void absenceTimeChange(String userId, Date date, String hashKey) { + + String redisKey = ConstantUtil.FTB_CLOCK_KEY + hashKey; + Map> userMap = initAbsenceInfo2(date, userId); + if (!userMap.isEmpty()) { + userMap.forEach((k, v) -> { + if (redisUtil.hasKey(redisKey, k)) { + v.forEach(value -> value.setV(value.getV() + 1)); + } + redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v)); + }); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void outsideClockIn(String approvalCode, String tenantId, String clockInId) throws Exception { + + // 查询审批记录 + ApplyAttendanceOutside applyAttendanceOutside = applyAttendanceOutsideMapper.selectById(approvalCode); + if (null == applyAttendanceOutside) { + throw new QueryException("未找到审批记录"); + } + ClockInDto clockInDto = new ClockInDto(); + clockInDto.setClockInKind(ConstantUtil.KIND_OUTSIDE); + clockInDto.setAddress(applyAttendanceOutside.getAddress()); + clockInDto.setLng(applyAttendanceOutside.getLng()); + clockInDto.setLat(applyAttendanceOutside.getLat()); + clockInDto.setDeviceType(ConstantUtil.DEVICE_PLACE); + clockInDto.setApprovalCode(approvalCode); + clockInDto.setApplyType(ApplyTypeEnum.OUTSIDE); + clockInDto.setUserId(applyAttendanceOutside.getApplyUserId()); + clockInDto.setTenantId(tenantId); + clockInDto.setRemark(applyAttendanceOutside.getRemark()); + if (StringUtils.isEmpty(clockInId)) { + // 打卡 + clockIn(clockInDto); + } else { + // 更新打卡 + updateClockIn(clockInId, clockInDto); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void approvalOutsideClockIn(String applyId, String passed, UserInfo userInfo) throws Exception { + + log.error("外勤审批 -> applyId: {}, passed: {}, userInfo: {}", applyId, passed, userInfo.getUserId()); + // 查询打卡记录 + FtbAttendanceClockIn clockIn = getClockInRecord(applyId); + if (null == clockIn) { + log.error("未找到打卡记录"); + throw new Exception("当天班次已变更,请联系申请人重新发起!"); + } + LambdaQueryWrapper resultQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getClockInId, clockIn.getId()); + AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper); + if (null == dbResult) { + log.error("未找到打卡结果"); + throw new Exception("当天班次已变更,请联系申请人重新发起!"); + } + // 当前生效中的打卡记录 + AttendanceClockInResult currentResult = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), dbResult.getClockInType(), ConstantUtil.NUM_FALSE); + Integer status = null; + // String title = "您发起的【外勤】审批%s"; + switch (passed) { + case "0": + // 拒绝, 更新打卡记录为已拒绝, 退回之前的打卡结果 + // title = String.format(title, "已驳回"); + status = ConstantUtil.STATUS_REFUSE; + clockIn.setRelationId(null); + clockIn.setApprovalStatus(ConstantUtil.PASS_FALSE); + // 退回之前的打卡结果 + rollbackLastRecord(currentResult, dbResult); + attendanceClockInMapper.updateById(clockIn); + break; + case "1": + // 通过, 更新打卡记录为已通过, 关联id置为空 + // title = String.format(title, "已通过"); + status = ConstantUtil.STATUS_PASS; + clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + // 查询当前生效中的打卡结果, 如果是缺卡, 回退到之前的外勤结果 + if (currentResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + AttendanceClockInResult clockInResult = rollbackLastRecord(currentResult, dbResult); + // 判断是否为旷工, 是旷工则将成对的那条记录的旷工状态去除 + if (currentResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + Integer clockInType = dbResult.getClockInType().equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK; + AttendanceClockInResult anotherRecord = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), clockInType, ConstantUtil.NUM_FALSE); + if (null != anotherRecord) { + attendanceClockInResultMapper.updateAbsenceToNormal(anotherRecord.getId()); + } + } + updateClockInResult(clockInResult); + } else { + updateClockInResult(currentResult); + } + attendanceClockInMapper.updateById(clockIn); + attendanceClockInResultMapper.updateReplyToNull(clockIn.getId(), applyId); + break; + case "2": + // 撤回, 删除审批中的打卡记录, 退回之前的打卡结果 + status = ConstantUtil.STATUS_ROLLBACK; + rollbackLastRecord(currentResult, dbResult); + attendanceClockInMapper.deleteById(clockIn.getId()); + break; + default: + break; + } + // 更新审批记录 + LambdaUpdateWrapper outsideUpdate = new LambdaUpdateWrapper() + .set(ApplyAttendanceOutside::getApproveUserId, userInfo.getUserId()) + .set(ApplyAttendanceOutside::getApproveTime, new Date()) + .set(ApplyAttendanceOutside::getStatus, status) + .eq(ApplyAttendanceOutside::getId, applyId); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userInfo.getUserId()).collect(Collectors.toList()), null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + outsideUpdate.set(ApplyAttendanceOutside::getApproveUserName, userList.getData().get(0).getUserName()); + } + applyAttendanceOutsideMapper.update(null, outsideUpdate); + //sendToIm(applyId, clockIn.getUserId(), title, approveUser.getUserName(), ConstantUtil.APPROVE_NOTICE_OUTSIDE, passed); + statisticsMq(userInfo.getTenantId(), currentResult.getRuleId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void approvalUnusualPhoneClockIn(String applyId, String passed, UserInfo userInfo) throws Exception { + log.error("异常审批 -> applyId: {}, passed: {}, userInfo: {}", applyId, passed, userInfo.getUserId()); + // 查询打卡记录 + FtbAttendanceClockIn clockIn = getClockInRecord(applyId); + if (null == clockIn) { + throw new Exception("当天班次已变更,请联系申请人重新发起!"); + } + LambdaQueryWrapper resultQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getClockInId, clockIn.getId()); + AttendanceClockInResult dbResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper); + if (null == dbResult) { + throw new Exception("当天班次已变更,请联系申请人重新发起!"); + } + // 当前生效中的打卡记录 + AttendanceClockInResult currentResult = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), dbResult.getClockInType(), ConstantUtil.NUM_FALSE); + Integer status = null; + switch (passed) { + case "0": + // 拒绝, 更新打卡记录为已拒绝, 退回之前的打卡结果 + status = ConstantUtil.STATUS_REFUSE; + clockIn.setRelationId(null); + clockIn.setApprovalStatus(ConstantUtil.PASS_FALSE); + // 退回之前的打卡结果 + rollbackLastRecord(currentResult, dbResult); + attendanceClockInMapper.updateById(clockIn); + break; + case "1": + // 通过, 更新打卡记录为已通过, 关联id置为空 + status = ConstantUtil.STATUS_PASS; + clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + // 查询当前生效中的打卡结果, 如果是缺卡, 回退到之前的异常结果 + if (currentResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + AttendanceClockInResult clockInResult = rollbackLastRecord(currentResult, dbResult); + // 判断是否为旷工, 是旷工则将成对的那条记录的旷工状态去除 + if (currentResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + Integer clockInType = dbResult.getClockInType().equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK; + AttendanceClockInResult anotherRecord = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), clockInType, ConstantUtil.NUM_FALSE); + if (null != anotherRecord) { + attendanceClockInResultMapper.updateAbsenceToNormal(anotherRecord.getId()); + } + } + updateClockInResult(clockInResult); + } else { + updateClockInResult(currentResult); + } + attendanceClockInMapper.updateById(clockIn); + attendanceClockInResultMapper.updateReplyToNull(clockIn.getId(), applyId); + break; + case "2": + // 撤回, 删除审批中的打卡记录, 退回之前的打卡结果 + // title = String.format(title, "已撤回"); + status = ConstantUtil.STATUS_ROLLBACK; + rollbackLastRecord(currentResult, dbResult); + attendanceClockInMapper.deleteById(clockIn.getId()); + break; + default: + break; + } + // 更新审批记录 + LambdaUpdateWrapper violationUpdate = new LambdaUpdateWrapper() + .set(ApplyAttendanceViolation::getApproveUserId, userInfo.getUserId()) + .set(ApplyAttendanceViolation::getApproveTime, new Date()) + .set(ApplyAttendanceViolation::getStatus, status) + .eq(ApplyAttendanceViolation::getId, applyId); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userInfo.getUserId()).collect(Collectors.toList()), null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + violationUpdate.set(ApplyAttendanceViolation::getApproveUserName, userList.getData().get(0).getUserName()); + } + applyAttendanceViolationMapper.update(null, violationUpdate); + + statisticsMq(userInfo.getTenantId(), currentResult.getRuleId()); + } + /** + * 打卡通知统计 + * + * @param tenantId 租户id + * @param ruleId 规则id + */ + private void statisticsMq(String tenantId, String ruleId) { + try { + FtbAttendanceDailyRule byId = attendanceDailyRuleService.getById(ruleId); + if (null == byId) { + return; + } + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(byId.getGroupId()) + .userId(byId.getUserId()) + .day(byId.getDay()) + .build(); + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 2000L, 2); + }catch (Exception e){ + log.error("打卡通知统计失败",e); + } + } + + /** + * 更新打卡结果(由于班次变更为外出、出差) + * + * @param currentResult 打卡结果 + */ + private void updateClockInResult(AttendanceClockInResult currentResult) { + // 查询出勤规则, 判断出勤规则是否已变更为外出、外勤 + FtbAttendanceDailyRule rule = attendanceDailyRuleMapper.selectById(currentResult.getRuleId()); + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), currentResult.getClockInType())) { + // 有效打卡时间为满勤(有效打卡时间为上班时间或下班时间), -休息时间 + if (currentResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + currentResult.setEffectiveTime(rule.getInPoint()); + } else { + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute; + if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) { + restMinute = 0; + } else { + restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + } + currentResult.setRestMinute(restMinute); + } + currentResult.setEffectiveTime(rule.getOutPoint()); + } + currentResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + currentResult.setAbnormalMinute(0); + attendanceClockInResultMapper.updateById(currentResult); + } + } + + private AttendanceClockInResult rollbackLastRecord(AttendanceClockInResult currentResult, AttendanceClockInResult dbResult) { + // 查询上一次打卡记录 + AttendanceClockInResult lastResult = getClockInResultByRule(dbResult.getRuleId(), dbResult.getUserId(), dbResult.getClockInType(), ConstantUtil.NUM_TRUE); + if (null != lastResult) { + lastResult.setDeleteMark(ConstantUtil.NUM_FALSE); + attendanceClockInResultMapper.updateById(lastResult); + if (StringUtils.isNotEmpty(lastResult.getClockInId())) { + // 有打卡图片的话, 恢复打卡图片 + attendanceClockInPicService.update(new LambdaUpdateWrapper() + .eq(AttendanceClockInPic::getClockInId, lastResult.getClockInId()) + .set(AttendanceClockInPic::getDeleteMark, ConstantUtil.NUM_FALSE)); + } + } + attendanceClockInResultMapper.deleteById(currentResult.getId()); + // 删除当前的图片 + if (StringUtils.isNotEmpty(currentResult.getClockInId())) { + attendanceClockInPicService.remove(new LambdaQueryWrapper() + .eq(AttendanceClockInPic::getClockInId, currentResult.getClockInId())); + } + return lastResult; + } + + private AttendanceClockInResult getClockInResultByRule(String ruleId, String userId, Integer clockInType, Integer delStatus) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getUserId, userId) + .eq(AttendanceClockInResult::getRuleId, ruleId) + .eq(AttendanceClockInResult::getClockInType, clockInType) + .eq(AttendanceClockInResult::getDeleteMark, delStatus) + .orderByDesc(AttendanceClockInResult::getCreatorTime) + .last("limit 1"); + return attendanceClockInResultMapper.selectOne(queryWrapper); + } + + private FtbAttendanceClockIn getClockInRecord(String approvalCode) { + + // 查询审批关联的打卡记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(FtbAttendanceClockIn::getApprovalCode, approvalCode) + .eq(FtbAttendanceClockIn::getApprovalStatus, ConstantUtil.PASS_APPROVAL); + return attendanceClockInMapper.selectOne(queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void repairClockIn(String applyId, String passed, String approveUserId, String tenantId) throws Exception { + + log.error("补卡 -> applyId: {}, passed: {}", applyId, passed); + // 查询审批记录 + ApplyAttendanceRepair applyAttendanceRepair = applyAttendanceRepairMapper.selectById(applyId); + if (null == applyAttendanceRepair) { + throw new QueryException("未找到审批记录"); + } + // 根据打卡结果id查询打卡结果 + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(applyAttendanceRepair.getClockInResultId()); + if (null == clockInResult) { + throw new QueryException("当天班次已变更,请联系申请人重新发起!"); + } + int pass = Integer.parseInt(passed.substring(0, 1)); + Integer status = null; + // 查询考勤规则 + AttendanceRuleVo rule = null; + // String title = "您发起的【补卡】审批%s"; + switch (pass) { + case 0: + status = ConstantUtil.STATUS_REFUSE; + // title = String.format(title, "已驳回"); + case 2: + if (null == status) { + status = ConstantUtil.STATUS_ROLLBACK; + // title = String.format(title, "已撤回"); + } + emptyApplyInfo(clockInResult.getId(), null, null); + break; + case 1: + // title = String.format(title, "已通过"); + status = ConstantUtil.STATUS_PASS; + // 查询考勤规则 + rule = getAttendanceRule(clockInResult.getRuleId()); + // 查询打卡记录 + LambdaQueryWrapper clockInQueryWrapper = new LambdaQueryWrapper() + .eq(FtbAttendanceClockIn::getId, clockInResult.getClockInId()); + FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectOne(clockInQueryWrapper); + if (null == clockIn) { + clockIn = new FtbAttendanceClockIn(); + clockIn.setDay(rule.getDay()); + clockIn.setUserId(clockInResult.getUserId()); + clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + clockIn.setClockInKind(ConstantUtil.KIND_NORMAL); + // clockIn.setDeviceType(ConstantUtil.DEVICE_PLACE); + } + // 判断是否封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate); + if (map.get(clockInResult.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + // 查询考勤组规则 + GroupRuleVo groupRule = getGroupRule(rule.getGroupId(), clockInResult.getUserId()); + // 判断有无补卡次数 + MutablePair datePair = couldRepairRecord(rule.getDay(), clockInResult, ConstantUtil.PASS_TRUE, groupRule); + // 判断打卡记录是否在时间范围内 + if (!DateDetail.checkTimeBetween(rule.getDay(), datePair.getLeft(), datePair.getRight())) { + throw new QueryException("当前打卡记录不在补卡时间范围内"); + } + // 生成新的打卡记录 + clockIn.setId(FtbUtil.getId()); + clockIn.setCreatorTime(new Date()); + // 重新绑定考勤规则 + clockInResult.setClockInId(clockIn.getId()); + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + clockIn.setClockInTime(rule.getInPoint()); + clockInResult.setEffectiveTime(rule.getInPoint()); + } else { + clockIn.setClockInTime(rule.getOutPoint()); + clockInResult.setEffectiveTime(rule.getOutPoint()); + // 设置打卡休息时间 + if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + clockInResult.setRestMinute(restMinute); + } + } + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setRepaired(ConstantUtil.NUM_TRUE); + if (ConstantUtil.NUM_TRUE == clockInResult.getAbsence()) { + clockInResult.setAbsence(ConstantUtil.NUM_FALSE); + // 管理员变更的记录可以被补卡,补卡后管理员变更的标识取消,成对的那条打卡记录变更人不受影响 + if (StringUtils.isNotEmpty(clockInResult.getAbsenceLeader())) { + clockInResult.setLastAbsenceLeader(clockInResult.getAbsenceLeader()); + clockInResult.setAbsenceLeader(null); + } + // 查询另一个打卡结果, 并设置为不旷工 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getRuleId, clockInResult.getRuleId()) + .eq(AttendanceClockInResult::getClockInType, clockInResult.getClockInType().equals(ConstantUtil.ON_WORK) ? ConstantUtil.OFF_WORK : ConstantUtil.ON_WORK) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceClockInResult workRecord = attendanceClockInResultMapper.selectOne(queryWrapper); + if (null != workRecord) { + attendanceClockInResultMapper.updateAbsenceToNormal(workRecord.getId()); + } + } + clockInResult.setAbsenceLeader(null); + clockInResult.setAbnormalMinute(0); + // 数据库操作 + // 补卡次数维护 + String applyDate = DateDetail.getDate2Str(applyAttendanceRepair.getApplyDate(), DateDetail.DF); + AttendanceRepair repair = attendanceRepairService.getAttendanceRepairByDate(applyDate, clockInResult.getUserId(), groupRule.getSelfGroupId()); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceRepair::getRepairNum, repair.getRepairNum() - 1) + .set(AttendanceRepair::getRepairRecord, StringUtils.isEmpty(repair.getRepairRecord()) ? clockInResult.getId() : repair.getRepairRecord() + "," + clockInResult.getId()) + .eq(AttendanceRepair::getId, repair.getId()); + attendanceRepairService.update(null, updateWrapper); + attendanceClockInMapper.insert(clockIn); + attendanceClockInResultMapper.updateById(clockInResult); + attendanceClockInResultMapper.updateResultReplyToNull(clockInResult.getId(), null); + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 如果补的加班卡 + attendanceUserBalanceService.overTime(List.of(new OvertimeInfoVo(rule.getId(), rule.getDay(), rule.getGroupId(), rule.getOvertimeRuleDetail())), tenantId); + } + break; + default: + break; + } + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(approveUserId).collect(Collectors.toList()), null); + LambdaUpdateWrapper repairUpdate = new LambdaUpdateWrapper() + .set(ApplyAttendanceRepair::getApproveUserId, approveUserId) + .set(ApplyAttendanceRepair::getApproveTime, new Date()) + .set(ApplyAttendanceRepair::getStatus, status) + .eq(ApplyAttendanceRepair::getId, applyId); + + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + repairUpdate.set(ApplyAttendanceRepair::getApproveUserName, userList.getData().get(0).getUserName()); + } + applyAttendanceRepairMapper.update(null, repairUpdate); + //审批通过 + if(pass == 1){ + // 发布日统计数据生成事件 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(rule.getGroupId()) + .userId(clockInResult.getUserId()) + .day(rule.getDay()) + .triggerSceneEnum(TriggerSceneEnum.CHECK_IN) + .build(); + if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(rule.getGroupId())){ + log.error("AttendanceClockInServiceImpl#repairClockIn 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + } + } + + private void sendToIm(String applyId, String sendTo, String title, String userName, String head, String passed) { + + String key = head + applyId; + Object obj = redisUtil.getString(key); + if (null != obj) { + JSONObject redisJson = new JSONObject(obj); + ApproveImVo approveImVo = redisJson.toBean(ApproveImVo.class); + approveImVo.getUserIds().clear(); + approveImVo.getUserIds().add(sendTo); + approveImVo.getApproveBaseImVo().setTitle(title); + approveImVo.getApproveBaseImVo().setCreateUserName(userName); + int pass = Integer.parseInt(passed.substring(0, 1)); + String urlParam = approveImVo.getApproveBaseImVo().getUrl().substring(AttendanceConstant.APPROVE_URL.length()); + JSONObject json = new JSONObject(urlParam); + if (ConstantUtil.STATUS_PASS == pass) { + json.set("formType", null); + json.set("opType", 0); + json.set("status", 2); + } else { + json.set("formType", 1); + json.set("opType", -1); + json.set("status", 3); + } + urlParam = json.toString(); + approveImVo.getApproveBaseImVo().setUrl(AttendanceConstant.APPROVE_URL + urlParam); + attendanceNoticeHandler.send(approveImVo); + redisUtil.remove(key); + } + } + + private void emptyApplyInfo(String id, String ruleId, String dealUser) { + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getApplyId, null) + .set(AttendanceClockInResult::getApplyType, null) + .set(AttendanceClockInResult::getAbsenceLeader, dealUser) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + if (StringUtils.isNotEmpty(ruleId)) { + updateWrapper.eq(AttendanceClockInResult::getRuleId, ruleId); + } else { + updateWrapper.eq(AttendanceClockInResult::getId, id); + } + attendanceClockInResultService.update(updateWrapper); + } + + @Override + public Boolean generateBeforeWorkRemind(String tenantId) { + + DateDetail dateDetail = new DateDetail(); + String now = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2); + String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + String redisKey = ConstantUtil.FTB_REMIND_KEY + tenantId + ":" + todayStr; + // 缓存不存在的话就生成 + if (!redisUtil.exists(redisKey)) { + // redis不存在缺勤信息,昨天(时间段最后可能会跨天) + 今天排班可能缺卡的时间点 + Map> remindMap = initRemindInfo(dateDetail); + remindMap.forEach((k, v) -> redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v))); + redisUtil.expire(redisKey, ConstantUtil.EXPIRE_TIME_REDIS); + } + Map> remindMap = new HashMap<>(); + List needDealList = new ArrayList<>(); + // 分布式锁的key + String lockKey = "LOCK:FTB_REMIND:" + redisKey; + RLock lock = redissonClient.getLock(lockKey); + try { + if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { + try { + // 获取锁成功,执行逻辑 + setObjectMapValue(redisKey, remindMap, RemindClockInVo.class); + if (remindMap.isEmpty()) { + return Boolean.TRUE; + } + // 判断当前时间需要提醒的记录 + remindMap.values().forEach(values -> { + values.forEach(value -> { + if (now.equals(value.getRemindTime())) { + needDealList.add(value); + } else { + // 在当前时间之前 且 未被处理过的记录 且 小于上班/下班时间 + boolean flag = DateDetail.getStr2DateTime(value.getRemindTime()).before(DateDetail.getStr2DateTime(now)); + boolean remindFlag = DateDetail.getStr2DateTime(value.getRemindTime()).before(DateDetail.getStr2DateTime(value.getWorkTime())); + if (flag && value.getDeal().equals(ConstantUtil.NUM_FALSE) && remindFlag) { + needDealList.add(value); + } + } + }); + }); + if (!needDealList.isEmpty()) { + // 发送消息 + needDealList.forEach(needDeal -> { + RemindClockInNoticeModel model = new RemindClockInNoticeModel(); + AttendanceNoticeEnum noticeEnum = needDeal.getClockInType().equals(ConstantUtil.ON_WORK) ? AttendanceNoticeEnum.CHECK_IN : AttendanceNoticeEnum.CHECK_OUT; + model.setAttendanceNoticeModel(needDeal.getUserId(), null, tenantId, null, noticeEnum); + model.setWorkTime(DateDetail.getStr2DateTime(needDeal.getWorkTime())); + model.setClockInType(needDeal.getClockInType()); + attendanceNoticeHandler.send(model); + needDeal.setDeal(ConstantUtil.NUM_TRUE); + }); + // 更新redis缓存 + updateConditionListToRedis( + redisKey, + needDealList, + RemindClockInVo::getOrDefaultV, + vo -> vo.getUserId() + ":" + vo.getRuleId(), + vo -> vo.getRuleId() + "_" + vo.getClockInType(), + RemindClockInVo.class + ); + } + return Boolean.TRUE; + } finally { + // 结束时间 + lock.unlock(); + } + } else { + log.error("定时提醒 {}- 获取分布式锁失败,可能已有其他实例在处理相同任务: {}", tenantId, lockKey); + return Boolean.FALSE; + } + } catch (InterruptedException e) { + // 恢复中断状态 + Thread.currentThread().interrupt(); + log.error("获取锁过程中被中断", e); + } + return true; + } + + private Map> initRemindInfo(DateDetail dateDetail) { + + dateDetail.changeDay(new Date()); + Date today = dateDetail.getCurrentDate(); + dateDetail.getTomorrow(); + Date tomorrow = dateDetail.getCurrentDate(); + // 查询昨天和今天的排班信息 + Map> map = new HashMap<>(); + // 查询所有上/下班时间在今天的班次 + List list = attendanceDailyRuleMapper.getListWorkTimeInToday(today); + // 查询明天00:00到00:30的班次(减掉提前提醒分钟数后提醒时间可能在今天) + String begin = DateDetail.getDate2Str(tomorrow, DateDetail.DF16); + Date beginDate = DateDetail.getStr2DateTime(begin); + Date endDate = dateDetail.addMinute(beginDate, DateDetail.MAX_REMIND); + List tomorrowList = attendanceDailyRuleMapper.getListByWorkTime(beginDate, endDate); + // 今天的班次需要今天提醒的 + if (!list.isEmpty()) { + initRemindDetail(map, list, dateDetail, today, ConstantUtil.NUM_TRUE); + } + // 需要在今天提醒的明天的班次 + if (!tomorrowList.isEmpty()) { + initRemindDetail(map, tomorrowList, dateDetail, tomorrow, ConstantUtil.NUM_FALSE); + } + return map; + } + + private void initRemindDetail(Map> map, List list, DateDetail dateDetail, Date currentDay, int isToday) { + Set userIds = list.stream().map(FtbAttendanceDailyRule::getUserId).collect(Collectors.toSet()); + // 查询班次用户的提醒配置 + List settingList = attendanceUserSettingService.getSettingList(new ArrayList<>(userIds), UserSettingTypeEnum.USER.getCode(), + Stream.of(UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode(), + UserSettingEnum.ATTENDANCE_SETTING_PRE_WORK_REMINDER.getCode(), + UserSettingEnum.ATTENDANCE_SETTING_END_WORK_REMINDER.getCode()) + .collect(Collectors.toList())); + ConcurrentMap> settingMap = settingList.stream().collect(Collectors.groupingByConcurrent(UserSettingVo::getAssociationId)); + // 根据提醒配置计算提醒时间 + for (FtbAttendanceDailyRule rule : list) { + List userSettingList = settingMap.get(rule.getUserId()); + if (null == userSettingList || userSettingList.isEmpty()) { + continue; + } + Map codeMap = userSettingList.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + UserSettingVo receiveSetting = codeMap.get(UserSettingEnum.MESSAGE_RECEIVE); + if (null == receiveSetting || ConstantUtil.NUM_TRUE != receiveSetting.getStatus()) { + continue; + } + // 判断上班时间是否在今天 + if (null != rule.getInPoint() && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF))) { + UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.PRE_WORK_REMINDER); + if (null != preWorkSetting && null != preWorkSetting.getValue()) { + List voList = map.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>()); + // 计算提醒时间 + dateDetail.addMinute(rule.getInPoint(), -preWorkSetting.getValue()); + // 排除计算出来的提醒时间是昨天的 + if (ConstantUtil.NUM_TRUE == isToday && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) { + RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2)); + voList.add(vo); + } + if (ConstantUtil.NUM_FALSE == isToday && !DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) { + RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2)); + voList.add(vo); + } + } + } + // 判断下班时间是否在今天 + if (null != rule.getOutPoint() && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF))) { + UserSettingVo preWorkSetting = codeMap.get(UserSettingEnum.END_WORK_REMINDER); + if (null != preWorkSetting && null != preWorkSetting.getValue()) { + List voList = map.computeIfAbsent(rule.getUserId() + ":" + rule.getId(), k -> new ArrayList<>()); + // 计算提醒时间 + dateDetail.addMinute(rule.getOutPoint(), -preWorkSetting.getValue()); + // 排除计算出来的提醒时间是昨天的 + if (ConstantUtil.NUM_TRUE == isToday && DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) { + RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2)); + voList.add(vo); + } + if (ConstantUtil.NUM_FALSE == isToday && !DateDetail.getDate2Str(currentDay, DateDetail.DF).equals(DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF))) { + RemindClockInVo vo = new RemindClockInVo(rule.getUserId(), rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF2), DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2)); + voList.add(vo); + } + } + } + } + } + + @Override + public Boolean generateAbsenceTask(Integer flag, String tenantId) { + + DateDetail dateDetail = new DateDetail(); + String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + // 每个租户一个独立的缺卡任务缓存 + String redisKey = ConstantUtil.FTB_CLOCK_KEY + tenantId + ":" + todayStr; + boolean needRebuild = !redisUtil.exists(redisKey) || flag == ConstantUtil.NUM_TRUE; + // 是否需要重新生成 + if (needRebuild) { + redisUtil.remove(redisKey); + log.error("定时缺卡 {}- 开始生成缺卡任务, 日期: {}", tenantId, todayStr); + Map> absenceMap = initAbsenceInfo2(dateDetail.getCurrentDate(), null); + absenceMap.forEach((k, v) -> redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(v))); + redisUtil.expire(redisKey, ConstantUtil.EXPIRE_TIME_REDIS); + } + return Boolean.TRUE; + } + + @Override + public List getGroupApprovalList(ApprovalQueryDto queryDto) { + + List returnList = new ArrayList<>(); + queryDto.getGroupQueryList().forEach(group -> { + List list = getApprovalList(group.getClockInList(), queryDto.getQueryDate(), + queryDto.getUserId(), group.getGroupId(), userProvider.get().getTenantId()); + list.removeIf(v -> !"已通过".equals(v.getLastResult())); + returnList.add(new GroupApprovalVo(group.getGroupId(), list)); + }); + return returnList; + } + + @Override + public Boolean generateFtbAbsenceRecord(String tenantId) { + + DateDetail dateDetail = new DateDetail(); + String now = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF2); + String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + // 每个租户一个独立的缺卡任务缓存 + String redisKey = ConstantUtil.FTB_CLOCK_KEY + tenantId + ":" + todayStr; + // 分布式锁的key + String lockKey = "LOCK:FTB_ABSENCE:" + redisKey; + RLock lock = redissonClient.getLock(lockKey); + // 判定哪些condition需要执行 + List conditionList = new ArrayList<>(); + Map> absenceMap = new HashMap<>(); + if (!redisUtil.exists(redisKey)) { + generateAbsenceTask(ConstantUtil.NUM_FALSE, tenantId); + } + try { + if (lock.tryLock(5, TimeUnit.SECONDS)) { + try { + // 获取锁成功,执行逻辑 + // 获取缺卡任务 + setObjectMapValue(redisKey, absenceMap, AbsenceClockInVo.class); + if (absenceMap.isEmpty()) { + return Boolean.TRUE; + } + absenceMap.forEach((k, v) -> { + for (AbsenceClockInVo value : v) { + if (value.getDeal().equals(ConstantUtil.NUM_FALSE)) { + // 匹配到缺卡 相等 或 小于 || 在当前时间之前 且 未被处理过的记录 + boolean flag = DateDetail.getStr2DateTime(value.getAbsenceTime()).before(DateDetail.getStr2DateTime(now)); + if (now.equals(value.getAbsenceTime()) || flag) { + // 判断有没有打卡(包括标识为删除的), 没有, 新增缺卡, 有, 判断是否为审批中的外勤, 是, 标识为删除, 新增缺卡 + AbsenceClockInVo bean = JsonUtil.getJsonToBean(value, AbsenceClockInVo.class); + bean.setDeal(ConstantUtil.NUM_TRUE); + conditionList.add(bean); + } + } + } + }); + if (!conditionList.isEmpty()) { + // 更新redis缓存 更新了部分任务为执行中 + updateConditionListToRedis( + redisKey, + conditionList, + AbsenceClockInVo::getOrDefaultV, + vo -> vo.getUserId() + ":" + vo.getRuleId(), + vo -> vo.getRuleId() + "_" + vo.getClockInType(), + AbsenceClockInVo.class + ); + } + } finally { + // 结束时间 + lock.unlock(); + } + // log.error("定时缺卡 {}- 当前缓存的redis数据:{}", tenantId, JSONUtil.toJsonStr(absenceMap)); + AttendanceClockInService bean = context.getBean(AttendanceClockInService.class); + bean.asyncDeal(tenantId, redisKey, conditionList, now); + return Boolean.TRUE; + } else { + log.error("定时缺卡 {}- 获取分布式锁失败,可能已有其他实例在处理相同任务: {}", tenantId, lockKey); + return Boolean.FALSE; + } + } catch (InterruptedException e) { + // 恢复中断状态 + Thread.currentThread().interrupt(); + log.error("获取锁过程中被中断", e); + } + return Boolean.TRUE; + } + + private List updateConditionListToRedis( + String redisKey, + List conditionList, + Function mapFunc, + Function groupKeyFunc, + Function uniqueKeyFunc, + Class clazz) { + + List exceptRuleIds = new ArrayList<>(); + Map> map = changeConditionToMap(conditionList, groupKeyFunc); + map.forEach((k, v) -> { + String hashValues = redisUtil.getHashValues(redisKey, k); + List oldList; + Integer oldVersion = v.stream().map(mapFunc).findFirst().orElse(null); + Integer newVersion = null; + if (StringUtil.isEmpty(hashValues)) { + oldList = new ArrayList<>(); + } else { + oldList = JSONUtil.toList(hashValues, clazz); + newVersion = oldList.stream().map(mapFunc).findFirst().orElse(null); + } + if (oldVersion != null && newVersion != null && !oldVersion.equals(newVersion)) { + exceptRuleIds.add(k.split(":")[1]); + } else { + // ===== 合并逻辑(唯一键)===== + Map mergeMap = new LinkedHashMap<>(); + // 1. 旧数据 + for (T vo : oldList) { + mergeMap.put(uniqueKeyFunc.apply(vo), vo); + } + // 2. 新数据覆盖旧数据 + for (T vo : v) { + mergeMap.put(uniqueKeyFunc.apply(vo), vo); + } + List mergedList = new ArrayList<>(mergeMap.values()); + // ===== 合并结束 ===== + redisUtil.insertHash(redisKey, k, JsonUtil.getObjectToString(mergedList)); + } + }); + return exceptRuleIds; + } + + private Map> changeConditionToMap( + List conditionList, + Function groupKeyFunc) { + + return conditionList.stream() + .collect(Collectors.groupingBy( + groupKeyFunc, + HashMap::new, + Collectors.toList() + )); + } + + private void setObjectMapValue(String redisKey, Map> objectMap, Class clazz) { + + Map map = redisUtil.getMap(redisKey); + if (map == null || map.isEmpty()) { + return; + } + + map.forEach((k, v) -> { + List list = JSONUtil.toList(v.toString(), clazz); + objectMap.put(k.toString(), list); + }); + } + + @Async + @Transactional(rollbackFor = Exception.class) + @Override + public CompletableFuture asyncDeal(String tenantId, String redisKey, List conditionList, String now) { + log.info("init... :{}", LocalDate.now()); + List addList = new ArrayList<>(); + List sqlConditionList = new ArrayList<>(); + List delList = new ArrayList<>(); + // 储存加班下班的通知 + List overtimeInfoList = new ArrayList<>(); + // 排班已变更导致本次执行不入库的RuleIds + List exceptRuleIds = new ArrayList<>(); + Map ruleMap; + if (conditionList.isEmpty()) { + ruleMap = new HashMap<>(); + } else { + List ruleIds = conditionList.stream().map(AbsenceClockInVo::getRuleId).distinct().collect(Collectors.toList()); + ruleMap = getAttendanceRuleBatch(ruleIds); + log.info("获取到所有出勤规则... :{}", LocalDateTime.now()); + List changeList = new ArrayList<>(); + // 查询用户打卡记录 + try { + List resultList = attendanceClockInResultMapper.selectUserClockInHistory(now); + log.info("获取到所有打卡记录... :{}", LocalDateTime.now()); + Map> resultMap = resultList.stream().filter(v -> null != v.getApprovalStatus()).collect(Collectors.groupingBy( + r -> r.getUserId() + ":" + r.getRuleId() + ":" + r.getClockInType() + )); + log.info("开始循环... :{}", LocalDateTime.now()); + for (AbsenceClockInVo condition : conditionList) { + List collectList = resultMap.getOrDefault( + condition.getUserId() + ":" + condition.getRuleId() + ":" + condition.getClockInType(), + Collections.emptyList() + ); + AttendanceRuleVo rule = ruleMap.get(condition.getRuleId()); + if (null == rule) { + log.error("定时缺卡 {}- 查询用户{}出勤规则失败: {}", tenantId, condition.getUserId(), condition.getRuleId()); + // 更新为已完成 + condition.setDeal(ConstantUtil.NUM_FINISH); + changeList.add(condition); + continue; + } + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(condition.getUserId()); + // 是否需要生成缺卡 + if (needAddNoClockInRecord(collectList, delList)) { + addNoClockInRecord(rule, condition.getClockInType(), userInfo, addList, sqlConditionList); + } + // 如果是加班下班, 添加到通知中 如果 overtimeRuleDetail 为空, 说明加班出勤规则有问题 + if (condition.getClockInType().equals(ConstantUtil.OFF_WORK) && rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + if (null == rule.getOvertimeRuleDetail()) { + log.error("加班出勤规则获取不到加班规则, ruleId:{}", rule.getId()); + } else { + OvertimeInfoVo overtimeInfo = new OvertimeInfoVo(rule.getId(), rule.getDay(), rule.getGroupId(), rule.getOvertimeRuleDetail()); + overtimeInfoList.add(overtimeInfo); + } + } + // 设置为已处理 + condition.setDeal(ConstantUtil.NUM_FINISH); + changeList.add(condition); + } + log.info("循环结束... :{}", LocalDateTime.now()); + } catch (Exception e) { + log.info("定时缺卡 {}- 执行报错, message: {}", tenantId, e.getMessage()); + } + if (!changeList.isEmpty()) { + List ids = updateConditionListToRedis( + redisKey, + changeList, + AbsenceClockInVo::getOrDefaultV, + vo -> vo.getUserId() + ":" + vo.getRuleId(), + vo -> vo.getRuleId() + "_" + vo.getClockInType(), + AbsenceClockInVo.class + ); + exceptRuleIds.addAll(ids); + log.info("更新redis... :{}", LocalDateTime.now()); + } + } + // 由于redis缺卡任务变更导致出勤规则被排除(ruleId未改变, 缺卡时间改变) + if (!exceptRuleIds.isEmpty()) { + delList.removeIf(exceptRuleIds::contains); + addList.removeIf(v -> exceptRuleIds.contains(v.getRuleId())); + sqlConditionList.removeIf(v -> exceptRuleIds.contains(v.getRuleId())); + } + if (!delList.isEmpty()) { + attendanceClockInResultMapper.updateBatchToDel(delList); + log.info("数据库删除操作... :{}", LocalDateTime.now()); + } + if (!addList.isEmpty()) { + if (!sqlConditionList.isEmpty()) { + // 查询需要更新为缺勤的打卡结果 + List updateResultList = attendanceClockInResultMapper.selectRuleIdsForAbsence(sqlConditionList); + if (!updateResultList.isEmpty()) { + List updateResultIds = updateResultList.stream().map(ConditionVo::getResultId).distinct().collect(Collectors.toList()); + attendanceClockInResultMapper.updateToAbsenceBatch(updateResultIds); + Set updateRuleIds = updateResultList.stream().map(ConditionVo::getRuleId).collect(Collectors.toSet()); + addList.forEach(v -> { + if (updateRuleIds.contains(v.getRuleId())) { + v.setAbsence(ConstantUtil.NUM_TRUE); + } + }); + } + } + // 查询数据库已存在的数据, 并排除 + List existList = attendanceClockInResultMapper.selectExistRecord(addList, ConstantUtil.NUM_TRUE); + List existIds = new ArrayList<>(); + if (!existList.isEmpty()) { + existList.forEach(v -> { + addList.stream().filter(add -> add.getRuleId().equals(v.getRuleId()) && add.getUserId().equals(v.getUserId()) + && add.getClockInType().equals(v.getClockInType())).findFirst().ifPresent(exist -> existIds.add(exist.getId())); + }); + } + if (!existIds.isEmpty()) { + addList.removeIf(v -> existIds.contains(v.getId())); + } + // 用于防止上下班同时生成缺卡时, 数据库查不到另外一条记录, 导致只缺卡不旷工(加班不旷工) + Map> resultByRuleMap = addList.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getRuleId)); + resultByRuleMap.forEach((ruleId, list) -> { + if (list.size() != 2) { + return; + } + for (AttendanceClockInResult v : list) { + if (!AttendanceTypeEnum.WORKOVERTIME.getCode().equals(v.getAttendanceType())) { + v.setAbsence(ConstantUtil.NUM_TRUE); + } + } + }); + attendanceClockInResultService.saveBatch(addList); + // 根据出勤规则判断哪些是缺卡 + List noClockList = addList.stream().filter(v -> v.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())).collect(Collectors.toList()); + if (!noClockList.isEmpty()) { + // 缺卡/旷工 发送消息 + noClockList.forEach(add -> { + AttendanceRuleVo rule = ruleMap.get(add.getRuleId()); + if (null != rule) { + NoClockInNoticeModel model = new NoClockInNoticeModel(); + AttendanceNoticeEnum noticeEnum = getNoticeEnum(add.getClockInType(), add.getAbsence()); + model.setAttendanceNoticeModel(add.getUserId(), rule.getGroupId(), tenantId, add.getDay(), noticeEnum); + model.setAbsence(add.getAbsence()); + model.setClockInType(add.getClockInType()); + model.setOnWorkTime(rule.getInPoint()); + model.setOffWorkTime(rule.getOutPoint()); + model.setClockInResultId(add.getId()); + attendanceNoticeHandler.send(model); + } + }); + } + // 加班通知 + if (!overtimeInfoList.isEmpty()) { + log.error("发送加班通知消息:{}", overtimeInfoList); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_OVERTIME_TOPIC, new OvertimeEvent(tenantId, overtimeInfoList)); + } + //调用批量生成日统计数据接口 + Map> groupMap = addList.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getGroupId)); + groupMap.forEach((groupId, inResultList) -> { + Map> dayMap = inResultList.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getDay)); + dayMap.forEach((day, list) -> { + //调用生成日统计数据接口 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .day(day) + .triggerSceneEnum(TriggerSceneEnum.CHECK_IN) + .build(); + List userIds = list.stream().map(AttendanceClockInResult::getUserId).distinct().collect(Collectors.toList()); + userIds.forEach(item ->{ + courseEventDTO.setUserId(item); + if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(groupId)){ + log.error("AttendanceClockInServiceImpl#asyncDeal 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + return; + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + }); + }); + }); + } + log.info("执行结束... :{}", LocalDateTime.now()); + return CompletableFuture.completedFuture(null); + } + + private boolean needAddNoClockInRecord(List collectList, List delList) { + + // 没有任何打卡记录 + if (collectList.isEmpty()) { + return true; + } + // 判断有没有审批中的打卡记录, 标记为删除 + AttendanceClockInResult approvalResult = collectList.stream() + .filter(v -> + v.getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL) + && v.getDeleteMark().equals(ConstantUtil.NUM_FALSE) + ) + .findFirst() + .orElse(null); + // 有审批中的记录 同一时间只有一个记录,在审批中,被删除之后就没有打卡记录了,所以需要缺卡 + if (approvalResult != null) { + delList.add(approvalResult.getId()); + return true; + } + // 有打卡记录,但全部是删除状态 + long deletedCount = collectList.stream() + .filter(v -> v.getDeleteMark().equals(ConstantUtil.NUM_TRUE)) + .count(); + return deletedCount == collectList.size(); + } + + private AttendanceNoticeEnum getNoticeEnum(Integer clockInType, Integer absence) { + + if (null != absence && ConstantUtil.NUM_TRUE == absence) { + return AttendanceNoticeEnum.ABSENCE; + } else if (ConstantUtil.ON_WORK.equals(clockInType)) { + return AttendanceNoticeEnum.IN_MISSING; + } else { + return AttendanceNoticeEnum.OUT_MISSING; + } + } + + private void addNoClockInRecord(AttendanceRuleVo rule, Integer clockInType, UserInfo userInfo, List addList, List conditionList) { + + MutablePair pair = generateNoClockAndCondition(rule, clockInType, userInfo); + pair.getLeft().setGroupId(rule.getGroupId()); + pair.getLeft().setDay(rule.getDay()); + addList.add(pair.getLeft()); + if (null != pair.getRight()) { + conditionList.add(pair.getRight()); + } + } + + @Override + public List getRepairList(String userId) throws Exception { + + List returnList = new ArrayList<>(); + DateDetail dateDetail = new DateDetail(); + int year = dateDetail.getYear(); + int month = dateDetail.getMonth(); + int day = dateDetail.getDay(); + Date currentDate = dateDetail.getCurrentDate(); + Date beginDate = dateDetail.addNum(dateDetail.getCurrentDate(), -60, Calendar.DATE); + // 查询用户本月所在的考勤组 + List groupUserList = attendanceUserService.getAttendanceGroupUsersOfSecondment(beginDate, currentDate, List.of(userId), null); + if (null == groupUserList || groupUserList.isEmpty()) { + return returnList; + } + List groupIds = groupUserList.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList()); + // 用于封账判定 + Map monthMap = new HashMap<>(); + // 考勤组打卡列表 + for (String groupId : groupIds) { + // 查询考勤组规则 + GroupRuleVo groupRule = getGroupRule(groupId, userId); + if (null == groupRule) { + throw new HandleException("请先配置考勤组规则"); + } + GroupRepairVo groupRepair = new GroupRepairVo(); + groupRepair.setGroupId(groupId); + groupRepair.setGroupName(attendanceGroupMapper.getGroupName(groupId)); + if (StringUtils.isEmpty(groupRule.getPatchType())) { + groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组暂无补卡类型. "); + } + if (null == groupRule.getPatchCycleDay()) { + groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组未设置补卡周期天数. "); + } + // 查询每个考勤组是否可以补卡, 已补卡次数 + if (null == groupRule.getPatchClockStatus() || groupRule.getPatchClockStatus().equals(ConstantUtil.NUM_FALSE)) { + groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组不允许补卡. "); + } + // 查询补卡刷新日 + if (null == groupRule.getPatchRefreshDay()) { + groupRepair.setMessage(groupRepair.getMessage() + "当前考勤组未设置补卡次数刷新日. "); + } + // 查询补卡刷新次数 + AttendanceRepair repair = attendanceRepairService.getAttendanceRepair(userId, groupId); + if (null == repair) { + // 当月补卡次数未生成 + log.error("当月补卡次数未生成 -> 月份: {}, user: {}, groupId: {}", dateDetail.getMonth(), userId, groupRule.getSelfGroupId()); + LocalDate now = LocalDate.now(); + Date expireDate = dateDetail.changeMonthAndDate(currentDate, 1, groupRule.getPatchRefreshDay()); + repair = new AttendanceRepair(groupRule.getSelfGroupId(), userId, year, month, day, + groupRule.getPatchClockNumber(), expireDate); + attendanceRepairService.save(repair); + } else if (repair.getRepairNum() == 0) { + groupRepair.setMessage(groupRepair.getMessage() + "您的补卡次数已用完. "); + } else { + String startDate = String.format("%d-%02d-%02d", repair.getGenerateYear(), repair.getGenerateMonth(), repair.getGenerateDay()); + String endDate = DateDetail.getDate2Str(repair.getExpireDate(), DateDetail.DF); + int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(userId, groupId, startDate, endDate); + if (repair.getRepairNum() - applyCount <= 0) { + groupRepair.setMessage(groupRepair.getMessage() + "您的补卡次数已用完. "); + } + repair.setRepairNum(repair.getRepairNum() - applyCount); + } + // 还有剩余次数, 返回考勤组时间段内可以补卡的异常打卡记录 + dateDetail.changeDay(currentDate); + Date queryBeginDate = dateDetail.addNum(dateDetail.getCurrentDate(), -groupRule.getPatchCycleDay(), Calendar.DATE); + MutablePair> queryPair = getQueryPair(groupRule.getPatchType()); + List clockInList = attendanceClockInMapper.getAbnormalClockInList(userId, groupId, queryBeginDate, currentDate, queryPair.getLeft(), queryPair.getRight()); + if (!clockInList.isEmpty()) { + if (StringUtils.isNotEmpty(groupRepair.getMessage())) { + returnList.add(groupRepair); + continue; + } + assert repair != null; + groupRepair.setUsedTimes(repair.getTotalNum() - repair.getRepairNum()); + groupRepair.setResidueTimes(repair.getRepairNum()); + // 有变更人的记录需要查出变更人名字 + List leaderList = clockInList.stream().filter(v -> StringUtils.isNotEmpty(v.getAbsenceLeader())).map(AbnormalClockInVo::getAbsenceLeader).collect(Collectors.toList()); + Map userMap = new HashMap<>(); + if (!leaderList.isEmpty()) { + ActionResult> userList = v2UserApi.getAllUserInfoBatch(leaderList, null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userMap = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + } + for (AbnormalClockInVo clockIn : clockInList) { + if (StringUtils.isNotEmpty(clockIn.getAbsenceLeader())) { + UserBoundVO user = userMap.get(clockIn.getAbsenceLeader()); + if (null != user) { + clockIn.setAbsenceLeaderName(user.getUserName()); + } + } + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(clockIn.getDay(), DateDetail.DF15); + Boolean b = monthMap.get(monthDate); + if (null == b) { + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(userId), monthDate); + monthMap.put(monthDate, map.get(userId)); + } + if (monthMap.get(monthDate)) { + // true 已封账 不可操作 + clockIn.setEnable(ConstantUtil.NUM_FALSE); + } else { + clockIn.setEnable(ConstantUtil.NUM_TRUE); + } + } + // 考勤组锁定的打卡记录不返回 + clockInList = clockInList.stream().filter(v -> v.getEnable().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList()); + groupRepair.setClockInList(clockInList); + } + returnList.add(groupRepair); + } + return returnList; + } + + private MutablePair> getQueryPair(String patchType) { + + List queryList = new ArrayList<>(); + int b = 0; + if (patchType.contains("1")) { + queryList.add(ClockInStatusEnum.WORK_LATE.getValue()); + } + if (patchType.contains("2")) { + queryList.add(ClockInStatusEnum.HOME_EARLY.getValue()); + } + if (patchType.contains("3")) { + queryList.add(ClockInStatusEnum.NO_CLOCK.getValue()); + } + if (patchType.contains("4")) { + b = 1; + } + if (queryList.isEmpty()) { + queryList.add(ClockInStatusEnum.UNUSED.getValue()); + } + return MutablePair.of(b, queryList); + } + + @Override + public DailyInfoVo getDailyClockInRecordV2(String userId, String queryDate, String currentGroupId, Integer queryOldData) throws QueryException { + + DailyInfoVo dailyInfoVo = new DailyInfoVo(); + Date date = DateDetail.getStr2Date(queryDate); + // 查询基本信息 + StaffRosterListReq req = new StaffRosterListReq(); + req.setPageSize(-1); + req.setIsQueryAuth(ConstantUtil.STR_FALSE); + req.setUserIds(List.of(userId)); + ActionResult> page = rosterManagerApi.postWithSalary(req); + if (null == page || !page.getCode().equals(200) || null == page.getData() || CollUtil.isEmpty(page.getData().getList())) { + throw new QueryException("查询当前用户信息失败..."); + } + FtbPersonnelsStaffRosterDto dto = page.getData().getList().get(0); + DailyBaseInfoVo baseInfo = setDailyBaseInfoNew(date, dto); + // 查询考勤组信息 + List miniGroupList = attendanceGroupUserMapper.getGroupUserList(Stream.of(currentGroupId).collect(Collectors.toSet()), userId, ConstantUtil.DEL); + if (null == miniGroupList || miniGroupList.isEmpty()) { + throw new QueryException("查询考勤组信息失败..."); + } + // 查询考勤组信息 + String groupName = attendanceGroupMapper.getGroupName(currentGroupId); + baseInfo.setGroupId(currentGroupId); + baseInfo.setGroupName(groupName); + // 查询班次信息 + List currentDateShift = new ArrayList<>(); + dailyInfoVo.setBaseInfo(baseInfo); + // 查询考勤详情 + // 打卡列表 + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + // 查询当日出勤规则 + List dailyRuleList = getCurrentDailyRuleListOfWithdraw(DateDetail.getStr2Date(queryDate), userInfo); + List shiftIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getShiftId).collect(Collectors.toList()); + Map shiftMap; + if (!shiftIds.isEmpty()) { + shiftMap = attendanceDailyRuleService.getShiftByShiftIds(shiftIds); + } else { + shiftMap = new HashMap<>(); + } + boolean isContainsOrdinary = dailyRuleList.stream().anyMatch(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType())); + dailyRuleList.forEach(rule -> { + AttendanceShiftNameEntity shift = shiftMap.get(rule.getShiftId()); + SchedulesItemVo item = setItemInfo(shift, rule, isContainsOrdinary); + if (Objects.nonNull(item) && rule.getGroupId().equals(currentGroupId)) { + currentDateShift.add(item); + } + }); + List allRuleIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + // 需要展示打卡记录的出勤规则 + dailyRuleList = dailyRuleList.stream().filter(v -> v.getInPoint() != null && v.getOutPoint() != null) + .sorted(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)) + .collect(Collectors.toList()); + List ruleIdList = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + DailyClockInDetailVo clockInDetail = new DailyClockInDetailVo(); + if (!dailyRuleList.isEmpty()) { + // 查询出勤规则关联的打卡记录 + List clockInResultList = attendanceClockInMapper.getClockInResultByRuleList(ruleIdList); + // 根据请假规则判断顺序 上班、下班、加班上班、加班下班、请假离岗、请假回岗、借调离岗、借调回岗 + for (FtbAttendanceDailyRule rule : dailyRuleList) { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) + || rule.getAttendanceType().equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()) + || rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) + || rule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) + || rule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + continue; + } + if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + // 请假 生成请假记录 index++ + DailyClockInVo v1 = getDailyClockInVo(null, rule, 0, null); + setLastResult(v1, rule); + DailyClockInVo v2 = getDailyClockInVo(null, rule, 1, null); + setLastResult(v2, rule); + clockInDetail.getClockInList().add(v1); + clockInDetail.getClockInList().add(v2); + continue; + } + List collect = clockInResultList.stream().filter(v -> rule.getId().equals(v.getRuleId())) + .sorted(Comparator.comparing(ClockInVo::getClockInType)).collect(Collectors.toList()); + String sortStr = null; + for (int i = 0; i < 2; i++) { + ClockInVo clockIn; + try { + if (i == 0) { + clockIn = collect.stream().filter(v -> ConstantUtil.ON_WORK.equals(v.getClockInType())).findFirst().orElse(null); + if (rule.getInUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) { + sortStr = "借调回岗"; + } + } else { + clockIn = collect.stream().filter(v -> ConstantUtil.OFF_WORK.equals(v.getClockInType())).findFirst().orElse(null); + if (rule.getOutUnbounded().equals(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode())) { + sortStr = "借调离岗"; + } + } + } catch (Exception e) { + clockIn = null; + } + DailyClockInVo dailyClockIn = getDailyClockInVo(clockIn, rule, i, miniGroupList.get(0).getNeedApproval()); + if (null != sortStr) { + dailyClockIn.setSortStr(sortStr); + sortStr = null; + } + clockInDetail.getClockInList().add(dailyClockIn); + } + } + } + // 添加请假记录 + if (!clockInDetail.getClockInList().isEmpty()) { + ConcurrentMap> shiftClockMap = clockInDetail.getClockInList().stream().filter(v -> StringUtils.isNotEmpty(v.getShiftId())).collect(Collectors.groupingByConcurrent(DailyClockInVo::getShiftId)); + // 处理请假逻辑 + shiftClockMap.forEach((k, v) -> { + ConcurrentMap> shiftTimeMap = v.stream().collect(Collectors.groupingByConcurrent(DailyClockInVo::getShiftTimeStr)); + shiftTimeMap.forEach((time, list) -> { + if (list.size() > 1) { + DailyClockInVo vo1 = list.get(0); + DailyClockInVo vo2 = list.get(1); + String v1LastResult = StringUtils.isNotEmpty(vo1.getLastResult()) ? vo1.getLastResult() : ""; + String v2LastResult = StringUtils.isNotEmpty(vo2.getLastResult()) ? vo2.getLastResult() : ""; + if ((ConstantUtil.LEAVE_STR.equals(v1LastResult) && !ConstantUtil.LEAVE_STR.equals(v2LastResult) && ConstantUtil.ON_WORK_STR.equals(vo2.getSortStr())) + || (ConstantUtil.ON_WORK_STR.equals(vo1.getSortStr()) && !ConstantUtil.LEAVE_STR.equals(v1LastResult) && ConstantUtil.LEAVE_STR.equals(v2LastResult))) { + // 请假 + 上班 = 请假回岗 + setLeaveValue(vo1, ConstantUtil.BACK_WORK_STR); + setLeaveValue(vo2, ConstantUtil.BACK_WORK_STR); + } + if ((ConstantUtil.LEAVE_STR.equals(v1LastResult) && !ConstantUtil.LEAVE_STR.equals(v2LastResult) && ConstantUtil.OFF_WORK_STR.equals(vo2.getSortStr())) + || (ConstantUtil.OFF_WORK_STR.equals(vo1.getSortStr()) && !ConstantUtil.LEAVE_STR.equals(v1LastResult) && ConstantUtil.LEAVE_STR.equals(v2LastResult))) { + // 请假 + 下班 = 请假离岗 + setLeaveValue(vo1, ConstantUtil.LEAVE_WORK_STR); + setLeaveValue(vo2, ConstantUtil.LEAVE_WORK_STR); + } + } + }); + }); + clockInDetail.getClockInList().removeIf(DailyClockInVo::isNeedDel); + clockInDetail.getClockInList().forEach(v -> { + // 如果最终结果为外出或出差, 则设置最终结果为外出或出差 + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(v.getApplyViewEnable())) { + v.setLastResult(AttendanceTypeEnum.BUSINESS_TRIP.getMsg()); + } + // 外出判定变更为 判断上/下班是否有外出标识 + if (getOutsideCheck(new AttendanceRuleVo(v.getApplyViewEnable(), v.getOutStepOutType(), v.getInStepOutType()), v.getClockInType())) { + v.setLastResult(AttendanceTypeEnum.STEP_OUT.getMsg()); + } + }); + } + // 移除非当前考勤组的打卡信息 + clockInDetail.getClockInList().removeIf(v -> !currentGroupId.equals(v.getGroupId())); + if (queryOldData.equals(ConstantUtil.NUM_TRUE)) { + // 查看原始数据 + setOldData(clockInDetail, allRuleIds, ruleIdList, userId, queryDate); + } + currentDateShift.stream().filter(vo -> StringUtil.isNotEmpty(vo.getName())).collect(Collectors.groupingBy(SchedulesItemVo::getName)).forEach((name, items) -> { + if (StringUtil.isEmpty(name) || CollUtil.isEmpty(items) || items.size() < 2) { + return; + } + items.stream().skip(1).forEach(lastItemVo -> { + lastItemVo.setName(""); + lastItemVo.setShortName(""); + lastItemVo.setIsShow(0); + }); + }); + // 设置班次信息 + dailyInfoVo.getBaseInfo().getCurrentDateShift().addAll(currentDateShift); + // 查询审批记录 + List approvalList = getApprovalList(clockInDetail.getClockInList(), queryDate, userId, currentGroupId, userProvider.get().getTenantId()); + clockInDetail.getApprovalList().addAll(approvalList); + // 统计数据 + LatticeStatisticsVo latticeStatistics = attendanceDayStatisticsService.getLatticeStatistics(new LatticeStatisticsVoDto(currentGroupId, userId, date)); + clockInDetail.setTotalData(latticeStatistics); + List clockIds = clockInDetail.getClockInList().stream().map(DailyClockInVo::getClockInId).filter(StringUtils::isNoneBlank).distinct().collect(Collectors.toList()); + Map> clockInPicMap = CollUtil.isEmpty(clockIds) ? new HashMap<>() : attendanceClockInPicService.getClockInPicByIds(clockIds, null); + if (CollUtil.isNotEmpty(clockInDetail.getClockInList())) { + clockInDetail.getClockInList().forEach(v -> { + if (StringUtil.isNotEmpty(v.getClockInId()) && clockInPicMap.containsKey(v.getClockInId())) { + v.setFieldPersonnelPic(!clockInPicMap.get(v.getClockInId()).isEmpty() ? 1 : 0); + if (v.getFieldPersonnelPic().equals(ConstantUtil.NUM_TRUE)) { + List picList = clockInPicMap.getOrDefault(v.getClockInId(), List.of()) + .stream() + .map(AttendanceClockInPic::getPicUrl) + .collect(Collectors.toList()); + v.setPicList(picList); + } + } + }); + } + dailyInfoVo.setClockInDetail(clockInDetail); + // 处理打卡结果展示审批备注问题 + if (!CollUtil.isEmpty(clockInDetail.getApprovalList())) { + // 通过审批类型分组 + Map idMap = clockInDetail.getApprovalList().stream().collect(Collectors.toMap(DailyApprovalVo::getTaskId, v -> v)); + if (!CollUtil.isEmpty(clockInDetail.getClockInList())) { + clockInDetail.getClockInList().forEach(v -> { + if (null != v.getLastResult() && !v.getLastResult().isEmpty()) { + // 判断 请假、加班、外出、出差 + if ("请假".equals(v.getLastResult()) || "加班".equals(v.getLastResult())) { + DailyApprovalVo dailyApprovalVo = idMap.get(v.getAssociationApplyId()); + if (null != dailyApprovalVo) { + v.setRemark(dailyApprovalVo.getReason()); + } + } + if ("外出".equals(v.getLastResult())) { + DailyApprovalVo dailyApprovalVo = idMap.get(v.getStepOutApplyId()); + if (null != dailyApprovalVo) { + v.setRemark(dailyApprovalVo.getReason()); + } + } + } + if ("出差".equals(v.getLastResult())) { + clockInDetail.getApprovalList().stream().filter(item -> item.getApprovalName().contains("出差申请")).findFirst().ifPresent(dailyApprovalVo -> v.setRemark(dailyApprovalVo.getReason())); + } + }); + } + } + + // 上一天/下一天 + MutablePair pair = setLastAndNext(date, userId, currentGroupId); + dailyInfoVo.setHasLast(pair.getLeft()); + dailyInfoVo.setHasNext(pair.getRight()); + return dailyInfoVo; + } + + private DailyBaseInfoVo setDailyBaseInfoNew(Date date, FtbPersonnelsStaffRosterDto userBound) { + + DailyBaseInfoVo dailyBaseInfo = new DailyBaseInfoVo(); + dailyBaseInfo.setUserId(userBound.getUserId()); + dailyBaseInfo.setDay(date); + dailyBaseInfo.setOrganizeNames(userBound.getCurrOrgName()); + dailyBaseInfo.setPositionName(userBound.getCurrPositionName()); + dailyBaseInfo.setUserName(userBound.getName()); + UserWorkStatusEnums workStatusEnum = StringUtil.isEmpty(userBound.getWorkerStatus()) ? UserWorkStatusEnums.NONE + : UserWorkStatusEnums.getUserWorkStatusEnumsByValue(userBound.getWorkerStatus()); + dailyBaseInfo.setWorkStatus(workStatusEnum.getDescription()); + return dailyBaseInfo; + } + + /** + * 添加原始数据到列表 + * @param clockInDetail 打卡详情 + * @param allRuleIds 今日所有出勤规则 + * @param ruleIds 需要排除的记录 + */ + private void setOldData(DailyClockInDetailVo clockInDetail, List allRuleIds, List ruleIds, String userId, String queryDate) { + + // 查询今日所有打卡 + List clockInList = attendanceClockInMapper.getListByRuleAndDate(userId, queryDate); + if (clockInList.isEmpty()) { + return; + } + // 在使用中的打卡记录(即在clock-in-result中有效) + List c = clockInDetail.getClockInList().stream().map(DailyClockInVo::getClockInId).filter(clockInId -> clockInId != null).collect(Collectors.toList()); + clockInList.removeIf(v -> c.contains(v.getClockInId())); + List collect = clockInList.stream().filter(v -> allRuleIds.contains(v.getRuleId())).collect(Collectors.toList()); + if (!collect.isEmpty()) { + List indexList = new ArrayList<>(); + collect.forEach(v -> indexList.add(clockInList.indexOf(v))); + Integer minDelIndex = null; + Integer maxDelIndex = null; + // 获取最大 和 最小的 index + int minIndex = indexList.get(0); + int maxIndex = indexList.get(indexList.size() - 1); + if (minIndex > 0) { + // 往上查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往上所有记录 + for (int i = indexList.get(0) - 1; i > 0; i--) { + DailyClockInVo vo = clockInList.get(i); + if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) { + minDelIndex = i; + break; + } + } + } + if (maxIndex < clockInList.size() - 1) { + // 往下查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往下所有记录 + for (int i = indexList.get(0) + 1; i < clockInList.size(); i++) { + DailyClockInVo vo = clockInList.get(i); + if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) { + maxDelIndex = i; + break; + } + } + } + // 排除 ruleId 在 ruleIds 中的记录 和 minDelIndex 往上的记录 和 maxDelIndex 往下的记录 + List removeList = new ArrayList<>(); + if (null != minDelIndex) { + removeList.addAll(clockInList.subList(0, minDelIndex)); + } + if (null != maxDelIndex) { + removeList.addAll(clockInList.subList(maxDelIndex, clockInList.size() - 1)); + } + clockInList.removeAll(removeList); + clockInList.removeIf(v -> ruleIds.contains(v.getRuleId()) && v.getDeleteMark() == 0); + } + if (!clockInList.isEmpty()) { + // 转换数据 clockInTime 格式化, 打卡方式 转换成文字 + clockInList.forEach(v -> { + String str = queryDate.equals(DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF)) ? "" : "次日"; + v.setClockInTimeStr(str + DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF10)); + v.setClockInMethod(getClockInMethod(null == v.getClockInMethod() ? ConstantUtil.DEVICE_UNKNOWN : Integer.parseInt(v.getClockInMethod()))); + }); + // 将clockInList插入到今日出勤中[插入到两个班次时间之间] + if (clockInDetail.getClockInList().isEmpty()) { + clockInDetail.getClockInList().addAll(clockInList.stream().sorted(Comparator.comparing(DailyClockInVo::getClockInTime)).collect(Collectors.toList())); + } else { + Map> map = new HashMap<>(); + for (int i = 0; i < clockInDetail.getClockInList().size(); i++) { + DailyClockInVo v1 = clockInDetail.getClockInList().get(i); + DailyClockInVo v2 = null; + if (i + 1 < clockInDetail.getClockInList().size()) { + v2 = clockInDetail.getClockInList().get(i + 1); + } + if (i == 0) { + // 判断小于v1的clockInList + List list = clockInList.stream() + .filter(clockIn -> clockIn.getClockInTime().before(v1.getWorkDate())) + .collect(Collectors.toList()); + map.put(null, list); + } + if (null != v2) { + // 如果v2不为空, 判断哪些clockInList在v1, v2班次之间, 如果有无需打卡 before的判定时间需要改为缺卡时间 + Date beforeTime; + if (null != v2.getClockInStatus() && v2.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) { + beforeTime = v1.getLackTime(); + } else { + beforeTime = v2.getWorkDate(); + } + List list = clockInList.stream() + .filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate()) && clockIn.getClockInTime().before(beforeTime)) + .collect(Collectors.toList()); + map.putIfAbsent(v1, list); + } else { + // 如果v2为空, 判断哪些clockInList大于v1班次 + List list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate())).collect(Collectors.toList()); + map.putIfAbsent(v1, list); + + } + } + map.keySet().forEach(key -> { + List list = map.get(key).stream().sorted(Comparator.comparing(DailyClockInVo::getClockInTime)).collect(Collectors.toList()); + if (key == null) { + clockInDetail.getClockInList().addAll(0, list); + } else { + int index = clockInDetail.getClockInList().indexOf(key); + if (index + 1 > clockInDetail.getClockInList().size() - 1) { + clockInDetail.getClockInList().addAll(list); + } else { + clockInDetail.getClockInList().addAll(index + 1, list); + } + } + }); + } + } + } + + private void setLeaveValue(DailyClockInVo v, String sortValue) { + String lastResult = StringUtils.isNotEmpty(v.getLastResult()) ? v.getLastResult() : ""; + if (lastResult.equals(ConstantUtil.LEAVE_STR)) { + v.setNeedDel(true); + } else { + v.setSortStr(sortValue); + } + } + + private List getApprovalList(List clockInList, String queryDate, String userId, String currentGroupId, String tenantId) { + + Date date = DateDetail.getStr2Date(queryDate); + List approvalList = new ArrayList<>(); + if (!clockInList.isEmpty()) { + List idList = clockInList.stream().map(DailyClockInVo::getClockInResultId).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + // 考勤变更审批 + approvalList.addAll(getApplyChangeList(idList, date)); + // 补卡审批 + approvalList.addAll(getApplyRepairList(idList, date)); + // 外勤审批 + List clockInIds = clockInList.stream().filter(v -> null != v.getClockInKind() && v.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE) && StringUtils.isNotEmpty(v.getClockInId())) + .map(DailyClockInVo::getClockInId).collect(Collectors.toList()); + approvalList.addAll(getApplyOutsideList(clockInIds, date)); + // 异常打卡审批 + approvalList.addAll(getApplyViolationList(idList)); + } + // 加班审批 + approvalList.addAll(getApplyWorkOverTimeList(userId, queryDate, currentGroupId)); + // 外出审批 + approvalList.addAll(getApplyGoOutList(userId, queryDate, currentGroupId)); + // 出差审批 + approvalList.addAll(getApplyBusinessTripList(userId, queryDate, currentGroupId)); + // 请假审批 + approvalList.addAll(getApplyLeaveList(userId, queryDate, currentGroupId)); + // 借调审批 2.0 人事提供 + approvalList.addAll(getApplySelfList(userId, queryDate, currentGroupId)); + // 排序 + if (!approvalList.isEmpty()) { + approvalList.forEach(approval -> { + if (!"审批中".equals(approval.getLastResult())) { + ApproverByTaskIdAndNodeIdVo info = flowTaskService.getApproveInfo(approval.getTaskId(), null, tenantId, 0); + if (null != info) { + approval.setFlowId(info.getFlowId()); + approval.setEnCode(info.getEnCode()); + approval.setId(info.getId()); + } + } + }); + approvalList = approvalList.stream().sorted(Comparator.comparing(DailyApprovalVo::getSubmitTime)).collect(Collectors.toList()); + } + return approvalList; + } + + private void setLastResult(DailyClockInVo v, FtbAttendanceDailyRule rule) { + + /*String lastResult = ConstantUtil.LEAVE_STR; + if (rule.getApplyViewEnable().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode())) { + lastResult = AttendanceTypeEnum.BUSINESS_TRIP.getMsg(); + } + if (rule.getApplyViewEnable().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + lastResult = AttendanceTypeEnum.STEP_OUT.getMsg(); + }*/ + v.setLastResult(ConstantUtil.LEAVE_STR); + v.setClockInTimeStr(ClockInStatusEnum.NEED_NOT.getDescription()); + } + + /** + * 获取打卡记录 + * + * @param clockIn 打卡记录 + * @param rule 出勤规则 + * @param workStatus 0: 上班, 1: 下班 + * @param needApproval 是否需要审批 + * @return jnpf.model.attendance.vo.DailyClockInVo + */ + private DailyClockInVo getDailyClockInVo(ClockInVo clockIn, FtbAttendanceDailyRule rule, int workStatus, Integer needApproval) { + + DailyClockInVo dailyClockIn; + if (null == clockIn) { + // 还未打卡 + dailyClockIn = new DailyClockInVo(); + dailyClockIn.setClockInType(workStatus == 0 ? ConstantUtil.ON_WORK : ConstantUtil.OFF_WORK); + dailyClockIn.setSortStr(workStatus == 0 ? PuncheTypeEnum.ON_DUTY.getTitle() : PuncheTypeEnum.OFF_DUTY.getTitle()); + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + dailyClockIn.setSortStr("加班" + dailyClockIn.getSortStr()); + } + String shiftTime = getShiftTimeStr(rule, workStatus); + dailyClockIn.setShiftTimeStr(shiftTime); + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) + || getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), dailyClockIn.getClockInType())) { + dailyClockIn.setWorkTime(ClockInStatusEnum.NEED_NOT.getDescription()); + } + } else { + dailyClockIn = setDailyClockInData(clockIn, rule, needApproval); + } + dailyClockIn.setGroupId(rule.getGroupId()); + dailyClockIn.setShiftId(rule.getShiftId()); + dailyClockIn.setApplyViewEnable(rule.getApplyViewEnable()); + dailyClockIn.setOutStepOutType(rule.getOutStepOutType()); + dailyClockIn.setInStepOutType(rule.getInStepOutType()); + dailyClockIn.setWorkDate(workStatus == 0 ? rule.getInPoint() : rule.getOutPoint()); + dailyClockIn.setLackTime(workStatus == 0 ? rule.getInLackPoint() : rule.getOutLackPoint()); + dailyClockIn.setRuleId(rule.getId()); + dailyClockIn.setAssociationApplyId(rule.getApplyId()); + dailyClockIn.setStepOutApplyId(workStatus == 0 ? rule.getInStepOutApplyId() : rule.getOutStepOutApplyId()); + return dailyClockIn; + } + + @Override + public String getShiftTimeStr(FtbAttendanceDailyRule rule, int workStatus) { + + String onStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 "; + String offStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 "; + return workStatus == 0 ? onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) : offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10); + } + + @Override + public Boolean generateRepairNum() { + + // 查询需要生成补卡次数的考勤组 + LambdaQueryWrapper groupQuery = new LambdaQueryWrapper() + .eq(AttendanceGroup::getDeleteMark, ConstantUtil.NUM_FALSE); + List allGroupList = attendanceGroupMapper.selectList(groupQuery); + if (allGroupList.isEmpty()) { + return true; + } + List groupIds = allGroupList.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); + Map settingMap = attendanceBaseSettingService.getEnableBaseSetting(groupIds); + // 查询今天是否为本月最后一天(是: 查询配置天数>=今天的所有配置, 否: 查询配置天数=今天的所有配置) + DateDetail dateDetail = new DateDetail(); + Date today = dateDetail.getCurrentDate(); + int currentDay = dateDetail.getDay(); + int lastDay = dateDetail.getLastDateOfMonth(dateDetail.getYear(), dateDetail.getMonth()); + // 满足条件的基础配置 + Map map = new HashMap<>(); + if (currentDay == lastDay) { + settingMap.keySet().forEach(key -> { + AttendanceBaseSetting v = settingMap.get(key); + if (null != v.getPatchRefreshDay() && v.getPatchRefreshDay() >= currentDay) { + map.put(key, v); + } + }); + } else { + settingMap.keySet().forEach(key -> { + AttendanceBaseSetting v = settingMap.get(key); + if (null != v && null != v.getPatchRefreshDay() && v.getPatchRefreshDay() == currentDay) { + map.put(key, v); + } + }); + } + generateRepairStart(map, today); + return true; + } + + @Override + public Boolean generateRepairNumAll() { + + // 查询需要生成补卡次数的考勤组 + LambdaQueryWrapper groupQuery = new LambdaQueryWrapper() + .eq(AttendanceGroup::getDeleteMark, ConstantUtil.NUM_FALSE); + List allGroupList = attendanceGroupMapper.selectList(groupQuery); + if (allGroupList.isEmpty()) { + return true; + } + List groupIds = allGroupList.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); + Map settingMap = attendanceBaseSettingService.getEnableBaseSetting(groupIds); + generateRepairStart(settingMap, new Date()); + return true; + } + + @Override + public List getDailyClockInRecord(String userId, DayClockRange clockRecord) { + LambdaQueryChainWrapper queryChainWrapper = this.lambdaQuery().eq(FtbAttendanceClockIn::getUserId, userId); + if (clockRecord.getStartTimeIsOpenClose()) { + queryChainWrapper.ge(FtbAttendanceClockIn::getClockInTime, clockRecord.getStartTime()); + } else { + queryChainWrapper.gt(FtbAttendanceClockIn::getClockInTime, clockRecord.getStartTime()); + } + if (clockRecord.getEndTimeIsOpenClose()) { + queryChainWrapper.le(FtbAttendanceClockIn::getClockInTime, clockRecord.getEndTime()); + } else { + queryChainWrapper.lt(FtbAttendanceClockIn::getClockInTime, clockRecord.getEndTime()); + } + queryChainWrapper.in(FtbAttendanceClockIn::getApprovalStatus, ConstantUtil.PASS_TRUE, ConstantUtil.PASS_APPROVAL); + List clockInList = queryChainWrapper.orderByAsc(FtbAttendanceClockIn::getClockInTime).list(); + return CollUtil.isNotEmpty(clockInList) ? clockInList : Lists.newArrayList(); + } + + @Override + public Boolean continuousCheck(String tenantId) { + + // 查询用户连续旷工配置 + UserSettingVo userSettingVo = attendanceUserSettingService.checkWebSetting(UserSettingEnum.ATTENDANCE_GROUP_NO_CHECKIN_REMIND_ADMIN); + if (null != userSettingVo && userSettingVo.getStatus().equals(ConstantUtil.NUM_TRUE)) { + // 新建一个列表装需要发送的消息 + List returnList = new ArrayList<>(); + // 开启连续旷工配置提醒 判定企业用户连续旷工情况 + List list = attendanceDailyRuleMapper.selectMonthRecord(); + if (!list.isEmpty()) { + ConcurrentMap> groupMap = list.stream().collect(Collectors.groupingByConcurrent(MonthRecordVo::getGroupId)); + groupMap.forEach((groupId, groupRecordList) -> { + ConcurrentMap> userMap = groupRecordList.stream().collect(Collectors.groupingByConcurrent(MonthRecordVo::getUserId)); + userMap.forEach((userId, userRecordList) -> { + // 判断连续旷工天数, 大于配置的天数则生成要发送的消息 + LinkedHashMap> dayMap = userRecordList.stream().collect(Collectors.groupingBy(MonthRecordVo::getDay, LinkedHashMap::new, Collectors.toList())); + // 所有每天的出勤记录 + int consecutiveDays = (int) dayMap.entrySet().stream() + .takeWhile(entry -> entry.getValue().stream().allMatch(v -> v.getAbsenceCount() >= 2)) + .count(); + if (consecutiveDays >= userSettingVo.getValue()) { + // 生成消息 + Object[] array = dayMap.keySet().toArray(); + String dateStr = getContinuousDate(array, consecutiveDays); + ConsecutiveAbsenceNoticeModel model = new ConsecutiveAbsenceNoticeModel(); + model.setAttendanceNoticeModel(userId, groupId, tenantId, new Date(), AttendanceNoticeEnum.CONSECUTIVE_ABSENCE); + model.setAbsenceDate(dateStr); + // 设置详情 + AbsenceDetailModel absenceDetailModel = new AbsenceDetailModel(AttendanceConstant.TITLE_CONSECUTIVE_ABSENCE, + DateDetail.getDate2Str(new Date(), DateDetail.DF10), "", "", dateStr); + List detailList = new ArrayList<>(); + for (int i = 0; i < consecutiveDays; i++) { + List recordList = dayMap.get(array[i].toString()); + recordList.forEach(record -> detailList.add(new NoticeDetailModel(record.getDay().substring(5), record.getRule(), record.getRuleTime()))); + } + absenceDetailModel.setDetailList(detailList); + model.setAbsenceDetailModel(absenceDetailModel); + returnList.add(model); + } + }); + }); + } + // 若要发送的消息不为空, 则发送消息 + if (!returnList.isEmpty()) { + List userIds = returnList.stream().map(ConsecutiveAbsenceNoticeModel::getUserId).collect(Collectors.toList()); +// List userList = userApi.getInfoByIdsNoData(userIds, tenantId); +// Map userMap = userList.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, tenantId); + Map userMap = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userMap = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + + List groupIds = returnList.stream().map(ConsecutiveAbsenceNoticeModel::getGroupId).collect(Collectors.toList()); + List groupList = attendanceGroupMapper.getGroupDetailList(groupIds); + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, Function.identity())); + Map> groupAdminMap = attendanceSuperAdminService.queryUserForCurrAndUpChildAndSuper(groupIds); + // 查询用户名和考勤组名 + Map finalUserMap = userMap; + returnList.forEach(v -> { +// PartUserInfoVo user = userMap.get(v.getUserId()); + UserBoundVO user = finalUserMap.get(v.getUserId()); + v.setUserName(null == user ? "--" : user.getUserName()); + AttendanceGroup group = groupMap.get(v.getGroupId()); + v.setGroupName(null == group ? "--" : group.getGroupName()); + v.getAbsenceDetailModel().setUserName(v.getUserName()); + v.getAbsenceDetailModel().setGroupName(v.getGroupName()); + v.setToUserList(groupAdminMap.get(v.getGroupId())); + // 补全数据 发送消息给考勤组管理员 + attendanceNoticeHandler.send(v); + }); + } + } + return true; + } + + private String getContinuousDate(Object[] array, int consecutiveDays) { + + String max = array[0].toString().substring(5); + String min = array[consecutiveDays - 1].toString().substring(5); + return min + " 至 " + max; + } + + private void generateRepairStart(Map settingMap, Date today) { + + if (settingMap.isEmpty()) { + return; + } + // 查询配置关联的考勤组及考勤组成员 + LambdaQueryWrapper userQuery = new LambdaQueryWrapper() + .ne(AttendanceGroupUser::getType, GroupUserTypeEnum.REMOVE.getCode()) + .eq(AttendanceGroupUser::getDeleteMark, ConstantUtil.NUM_FALSE) + .in(AttendanceGroupUser::getGroupId, settingMap.keySet()); + // 本组成员 + 借调成员(需要再判断timeJson是否符合) + List userList = attendanceGroupUserMapper.selectList(userQuery); + if (userList.isEmpty()) { + return; + } + List addList = new ArrayList<>(); + ConcurrentMap> groupUserMap = userList.stream().collect(Collectors.groupingByConcurrent(AttendanceGroupUser::getGroupId)); + for (String groupId : settingMap.keySet()) { + AttendanceBaseSetting setting = settingMap.get(groupId); + List groupUserList = groupUserMap.get(groupId); + if (null == groupUserList || groupUserList.isEmpty()) { + continue; + } + if (null == setting || null == setting.getPatchRefreshDay() || null == setting.getPatchClockNumber()) { + continue; + } + // 拆分本组成员和借调成员 + List currentGroupUserList = groupUserList.stream().filter(v -> v.getType().equals(GroupUserTypeEnum.CUR.getCode())).collect(Collectors.toList()); + List borrowGroupUserList = groupUserList.stream().filter(v -> v.getType().equals(GroupUserTypeEnum.BORROW.getCode())).collect(Collectors.toList()); + if (!currentGroupUserList.isEmpty()) { + for (AttendanceGroupUser user : currentGroupUserList) { + addList.add(getRepairSetting(groupId, setting, user)); + } + } + if (!borrowGroupUserList.isEmpty()) { + // 判断是否还在借调时间段内, 是则生成 + firstLoop: + for (AttendanceGroupUser borrowUser : borrowGroupUserList) { + if (StringUtil.isEmpty(borrowUser.getTimeJson())) { + continue; + } + JSONArray jsonArray = new JSONArray(borrowUser.getTimeJson()); + List jsonList = jsonArray.toList(JSONObject.class); + for (JSONObject json : jsonList) { + String startTimeStr = json.get("startTime").toString(); + String endTimeStr = json.get("endTime").toString(); + boolean b = DateDetail.checkTimeBetween(today, DateDetail.getStr2DateTime(startTimeStr), DateDetail.getStr2DateTime(endTimeStr)); + if (b) { + addList.add(getRepairSetting(groupId, setting, borrowUser)); + break firstLoop; + } + } + } + } + } + if (addList.isEmpty()) { + return; + } + attendanceRepairService.saveBatch(addList); + } + + @Override + public Boolean generateRepairNumForUser(String groupId, String userId, Integer generateType) { + + // 查询当前考勤组是否已经存在当前用户的补卡次数记录 + AttendanceRepair repair = attendanceRepairService.getAttendanceRepair(userId, groupId); + if (null != repair) { + return true; + } + // 查询有没有同组成员的配置 + repair = attendanceRepairService.getAttendanceRepair(null, groupId); + if (null != repair) { + // 使用同组的配置生成当前用户的配置 + repair.setId(FtbUtil.getId()); + repair.setUserId(userId); + repair.setRepairNum(repair.getTotalNum()); + repair.setRepairRecord(null); + } else { + // 查询当前考勤组的基础配置, 生成记录 + Map settingMap = attendanceBaseSettingService.getEnableBaseSetting(Stream.of(groupId).collect(Collectors.toList())); + AttendanceBaseSetting setting = settingMap.get(groupId); + if (null == setting) { + return false; + } + AttendanceGroupUser user = new AttendanceGroupUser(); + user.setUserId(userId); + repair = getRepairSetting(groupId, setting, user); + } + if (generateType.equals(2)) { + // 借调到新组, 根据借调时间判断是否需要生成 + LambdaQueryWrapper userQuery = new LambdaQueryWrapper() + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.BORROW.getCode()) + .eq(AttendanceGroupUser::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(AttendanceGroupUser::getGroupId, groupId) + .eq(AttendanceGroupUser::getUserId, userId); + AttendanceGroupUser groupUser = attendanceGroupUserMapper.selectOne(userQuery); + if (null == groupUser || StringUtils.isEmpty(groupUser.getTimeJson())) { + return false; + } + JSONArray jsonArray = new JSONArray(groupUser.getTimeJson()); + List jsonList = jsonArray.toList(JSONObject.class); + boolean b = false; + for (JSONObject json : jsonList) { + String endTimeStr = json.get("endTime").toString(); + if (DateDetail.getStr2DateTime(endTimeStr).before(repair.getExpireDate())) { + b = true; + break; + } + } + if (!b) { + return true; + } + } + attendanceRepairService.save(repair); + return true; + } + + @Override + public RepairRuleVo getClockInRepairCheck(String groupId) { + + Map settingMap = attendanceBaseSettingService.getEnableBaseSetting(Stream.of(groupId).collect(Collectors.toList())); + AttendanceBaseSetting setting = settingMap.get(groupId); + if (null == setting) { + log.info("查询考勤组补卡规则失败, {}", groupId); + return null; + } + return JsonUtil.getJsonToBean(setting, RepairRuleVo.class); + } + + private AttendanceRepair getRepairSetting(String groupId, AttendanceBaseSetting setting, AttendanceGroupUser user) { + + DateDetail dateDetail = new DateDetail(); + AttendanceRepair attendanceRepair = new AttendanceRepair(); + attendanceRepair.setId(FtbUtil.getId()); + attendanceRepair.setGroupId(groupId); + attendanceRepair.setUserId(user.getUserId()); + attendanceRepair.setGenerateYear(dateDetail.getYear()); + attendanceRepair.setGenerateMonth(dateDetail.getMonth()); + attendanceRepair.setGenerateDay(dateDetail.getDay()); + attendanceRepair.setTotalNum(setting.getPatchClockNumber()); + attendanceRepair.setRepairNum(setting.getPatchClockNumber()); + // 计算过期日期 + dateDetail.addNum(dateDetail.getCurrentDate(), 1, Calendar.MONTH); + dateDetail.changeDate(dateDetail.getCurrentDate(), setting.getPatchRefreshDay()); + attendanceRepair.setExpireDate(dateDetail.getCurrentDate()); + return attendanceRepair; + } + + private MutablePair setLastAndNext(Date date, String userId, String currentGroupId) { + + // 无法查看的情况 + int hasLast = ConstantUtil.NUM_TRUE; + int hasNext = ConstantUtil.NUM_TRUE; + // 上一天/下一天 为 上一个月/下一个月 + DateDetail dateDetail = new DateDetail(date); + dateDetail.getYesterday(); + Date lastDay = dateDetail.getCurrentDate(); + if (!DateDetail.checkSameMonth(date, lastDay)) { + hasLast = ConstantUtil.NUM_FALSE; + } + dateDetail.getTomorrow(); + dateDetail.getTomorrow(); + Date nextDay = dateDetail.getCurrentDate(); + if (!DateDetail.checkSameMonth(date, nextDay)) { + hasNext = ConstantUtil.NUM_FALSE; + } + // 离组后未来的日期 & 借调之后不在借调范围内的日期(借调考勤组) + LambdaQueryWrapper groupUserQuery = new LambdaQueryWrapper() + .eq(AttendanceGroupUser::getUserId, userId) + .eq(AttendanceGroupUser::getGroupId, currentGroupId); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQuery); + if (attendanceDailyRuleService.findUserIsExistsByDay(groupUserList, lastDay).equals(ConstantUtil.NUM_FALSE)) { + hasLast = ConstantUtil.NUM_FALSE; + } + if (attendanceDailyRuleService.findUserIsExistsByDay(groupUserList, nextDay).equals(ConstantUtil.NUM_FALSE)) { + hasNext = ConstantUtil.NUM_FALSE; + } + return MutablePair.of(hasLast, hasNext); + } + + private SchedulesItemVo setItemInfo(AttendanceShiftNameEntity shift, FtbAttendanceDailyRule rule, boolean hasOrdinary) { + Integer type = getType(rule.getAttendanceType()); + if (Objects.isNull(type) || (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType()) && hasOrdinary)) { + return null; + } + SchedulesItemVo item = new SchedulesItemVo(); + item.setSchedulesType(rule.getSchedulesType()); + item.setType(type); + item.setColour(HOLIDAYS_COLOR); + if(Objects.equals(rule.getFixedMark(),2)){ + item.setName("划线排班"); + item.setShortName("划线排班"); + return item; + } + if (Objects.nonNull(shift)) { + if (Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) + || Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())) { + item.setColour(shift.getColour()); + item.setShortName(shift.getShortName()); + item.setName(shift.getName()); + } + } + if (Objects.nonNull(rule.getPeriodInfo())) { + ShiftNameVo shiftNameVo = JSONUtil.toBean(rule.getPeriodInfo(), ShiftNameVo.class); + if (Objects.nonNull(shiftNameVo)) { + item.setShortName(shiftNameVo.getShortName()); + item.setColour(shiftNameVo.getColour()); + item.setName(shiftNameVo.getName()); + } + } + return item; + } + + private Integer getType(Integer attendanceType) { + if (AttendanceTypeEnum.ORDINARY.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + if (AttendanceTypeEnum.REST.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.REST.getCode(); + } + if (AttendanceTypeEnum.LEAVE.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.LEAVE.getCode(); + } + return null; + } + + private Collection getApplyViolationList(List idList) { + + List list = new ArrayList<>(); + if (CollUtil.isEmpty(idList)) { + return list; + } + LambdaQueryWrapper violationQuery = new LambdaQueryWrapper() + .in(ApplyAttendanceViolation::getClockInResultId, idList); + List vioList = applyAttendanceViolationMapper.selectList(violationQuery); + if (vioList.isEmpty()) { + return list; + } + vioList.forEach(vio -> { + String applyTime = ""; + if (null != vio.getApplyDate()) { + applyTime = DateDetail.getDate2Str(vio.getApplyDate(), DateDetail.DF9); + } + list.add(new DailyApprovalVo(vio.getId(), vio.getId(), "违规打卡", applyTime, vio.getCreatorTime(), + vio.getApproveTime(), vio.getApproveUserId(), vio.getApproveUserName(), getLastResult(vio.getStatus()))); + }); + return list; + } + + private Collection getApplyChangeList(List idList, Date date) { + + List list = new ArrayList<>(); + if (CollUtil.isEmpty(idList)) { + return list; + } + LambdaQueryWrapper changeQuery = new LambdaQueryWrapper() + .in(ApplyAttendanceChange::getClockInResultId, idList); + List changeList = applyAttendanceChangeMapper.selectList(changeQuery); + if (changeList.isEmpty()) { + return list; + } + changeList.forEach(change -> { + String applyTime = ""; + if (null != change.getOnWorkTime()) { + applyTime = DateDetail.getDate2Str(change.getOnWorkTime(), DateDetail.DF9); + } + if (null != change.getOffWorkTime()) { + if (StringUtils.isEmpty(applyTime)) { + applyTime = DateDetail.getDate2Str(change.getOffWorkTime(), DateDetail.DF9); + } else { + applyTime = applyTime + " 至 " + DateDetail.getDate2Str(change.getOffWorkTime(), DateDetail.DF10); + } + } + list.add(new DailyApprovalVo(change.getId(), change.getFlowId(), "出勤变更", applyTime, change.getCreatorTime(), + change.getApproveTime(), change.getApproveUserId(), change.getApproveUserName(), getLastResult(change.getStatus()))); + }); + return list; + } + + private Collection getApplyRepairList(List idList, Date date) { + + List list = new ArrayList<>(); + if (CollUtil.isEmpty(idList)) { + return list; + } + LambdaQueryWrapper repairQuery = new LambdaQueryWrapper() + .in(ApplyAttendanceRepair::getClockInResultId, idList); + List repairList = applyAttendanceRepairMapper.selectList(repairQuery); + if (repairList.isEmpty()) { + return list; + } + repairList.forEach(repair -> list.add(new DailyApprovalVo(repair.getId(), repair.getFlowId(), "补卡申请", repair.getRepairDateStr() + " " + repair.getRepairTimeStr(), repair.getCreatorTime(), + repair.getApproveTime(), repair.getApproveUserId(), repair.getApproveUserName(), getLastResult(repair.getStatus())))); + return list; + } + + private Collection getApplyOutsideList(List idList, Date date) { + + List list = new ArrayList<>(); + if (idList.isEmpty()) { + return list; + } + // 查询外勤打卡的任务id + LambdaQueryWrapper clockInQuery = new LambdaQueryWrapper() + .eq(FtbAttendanceClockIn::getClockInKind, ConstantUtil.KIND_OUTSIDE) + .eq(FtbAttendanceClockIn::getDeleteMark, ConstantUtil.NUM_FALSE) + .isNotNull(FtbAttendanceClockIn::getApprovalCode) + .in(FtbAttendanceClockIn::getId, idList); + List clockInList = attendanceClockInMapper.selectList(clockInQuery); + if (clockInList.isEmpty()) { + return list; + } + List taskIds = clockInList.stream().map(FtbAttendanceClockIn::getApprovalCode).collect(Collectors.toList()); + + LambdaQueryWrapper outsideQuery = new LambdaQueryWrapper() + .in(ApplyAttendanceOutside::getId, taskIds); + List outsideList = applyAttendanceOutsideMapper.selectList(outsideQuery); + if (outsideList.isEmpty()) { + return list; + } + outsideList.forEach(outside -> list.add(new DailyApprovalVo(outside.getId(), outside.getFlowId(), "外勤申请", DateDetail.getDate2Str(outside.getApplyDate(), DateDetail.DF9), outside.getCreatorTime(), + outside.getApproveTime(), outside.getApproveUserId(), outside.getApproveUserName(), getLastResult(outside.getStatus())))); + return list; + } + + + private Collection getApplyWorkOverTimeList(String userId, String queryDate, String currentGroupId) { + List list1 = attendanceLeaveApproveMapper.getApplyWorkOverTimeList(userId, queryDate, currentGroupId, AttendanceTypeEnum.WORKOVERTIME.getCode()); + List list = new ArrayList<>(list1); + // 审批中的查询 + List list2 = attendanceLeaveApproveMapper.getApplyingWorkOverTimeList(userId, queryDate); + list.addAll(list2); + return setResult(list); + } + + @Nullable + private List setResult(List list) { + if (null != list && !list.isEmpty()) { + // 处理审批状态 + list.forEach(v -> { + v.setLastResult(getLastResult(Integer.parseInt(v.getLastResult()))); + }); + } + return list; + } + + private Collection getApplyGoOutList(String userId, String queryDate, String currentGroupId) { + List list1 = attendanceLeaveApproveMapper.getApplyGoOutList(userId, queryDate, currentGroupId, AttendanceTypeEnum.STEP_OUT.getCode()); + List list = new ArrayList<>(list1); + // 审批中的查询 + List list2 = attendanceLeaveApproveMapper.getApplyingGoOutList(userId, queryDate); + list.addAll(list2); + return setResult(list); + } + + private Collection getApplyLeaveList(String userId, String queryDate, String currentGroupId) { + List list1 = attendanceLeaveApproveMapper.getApplyLeaveList(userId, queryDate, currentGroupId, AttendanceTypeEnum.LEAVE.getCode()); + List list = new ArrayList<>(list1); + // 审批中的查询 + List list2 = attendanceLeaveApproveMapper.getApplyingLeaveList(userId, queryDate); + list.addAll(list2); + return setResult(list); + } + + private Collection getApplySelfList(String userId, String queryDate, String currentGroupId) { + // 查询出考勤组对应的组织 + AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(currentGroupId); + if (null == groupDetail) { + return Collections.emptyList(); + } + FtbSecondMentQueryDTO ftbSecondMentQueryDTO = new FtbSecondMentQueryDTO(); + ftbSecondMentQueryDTO.setOrgId(groupDetail.getOrgId()); + ftbSecondMentQueryDTO.setUserId(userId); + ftbSecondMentQueryDTO.setStartTime(DateDetail.getStr2Date(queryDate)); + ftbSecondMentQueryDTO.setEndTime(DateDetail.getStr2Date(queryDate)); + List list = ftbPersonneApi.queryListApproval(ftbSecondMentQueryDTO); + // 处理借调状态对齐 人事方:1审批中,2,审核通过,3,审核不通过 考勤方:0.待审核 1.通过 2.未通过 3.撤回 + list.forEach(v -> { + if ("1".equals(v.getLastResult())) { + v.setLastResult("0"); + } else if ("2".equals(v.getLastResult())) { + v.setLastResult("1"); + } else if ("3".equals(v.getLastResult())) { + v.setLastResult("2"); + } + }); + return setResult(list); + } + + private Collection getApplyBusinessTripList(String userId, String queryDate, String currentGroupId) { + + List list1 = attendanceLeaveApproveMapper.getApplyBusinessTripList(userId, queryDate, currentGroupId, AttendanceTypeEnum.BUSINESS_TRIP.getCode()); + List list = new ArrayList<>(list1); + // 审批中的查询 + List list2 = attendanceLeaveApproveMapper.getApplyingBusinessTripList(userId, queryDate); + list.addAll(list2); + return setResult(list); + } + + private String getLastResult(Integer status) { + + switch (status) { + case 0: + return "审批中"; + case 1: + return "已通过"; + case 2: + return "已驳回"; + case 3: + return "已撤回"; + default: + return ""; + } + } + + private DailyClockInVo setDailyClockInData(ClockInVo clockIn, FtbAttendanceDailyRule rule, Integer needApproval) { + + DailyClockInVo dailyClockIn = new DailyClockInVo(); + dailyClockIn.setRuleType(ConstantUtil.RULE_TYPE_WORK); + dailyClockIn.setClockInResultId(clockIn.getId()); + dailyClockIn.setRepaired(clockIn.getRepaired()); + dailyClockIn.setAttendanceType(rule.getAttendanceType()); + // 判断有无次日 + String onStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 "; + String offStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 "; + dailyClockIn.setSortStr(clockIn.getClockInType().equals(ConstantUtil.ON_WORK) ? PuncheTypeEnum.ON_DUTY.getTitle() : PuncheTypeEnum.OFF_DUTY.getTitle()); + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + dailyClockIn.setSortStr("加班" + dailyClockIn.getSortStr()); + } + dailyClockIn.setShiftTimeStr(clockIn.getClockInType().equals(ConstantUtil.ON_WORK) ? onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) : offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10)); + dailyClockIn.setWorkTime(dailyClockIn.getShiftTimeStr()); + dailyClockIn.setTotalShiftTimeStr(onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) + "-" + offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10)); + String clockInTimeStr = getCheckNextDay(clockIn.getClockInTime(), rule.getDay()); + dailyClockIn.setClockInTimeStr(clockIn.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) ? ClockInStatusEnum.NEED_NOT.getDescription() : clockInTimeStr); + dailyClockIn.setClockInMethod(getClockInMethod(null == clockIn.getDeviceType() ? ConstantUtil.DEVICE_UNKNOWN : clockIn.getDeviceType())); + dailyClockIn.setClockInPlace(clockIn.getAddress()); + dailyClockIn.setClockInKind(clockIn.getClockInKind()); + dailyClockIn.setClockInStatus(clockIn.getClockInStatus()); + if (clockIn.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + dailyClockIn.setClockInStatus(ClockInStatusEnum.ABSENCE.getValue()); + } + boolean flag = StringUtils.isNotEmpty(clockIn.getAbsenceLeader()) && StringUtils.isEmpty(clockIn.getApplyId()); + dailyClockIn.setChanged(flag ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE); + dailyClockIn.setClockInId(clockIn.getClockInId()); + dailyClockIn.setRuleId(clockIn.getRuleId()); + dailyClockIn.setClockInTime(clockIn.getClockInTime()); + dailyClockIn.setEffectiveTime(clockIn.getEffectiveTime()); + dailyClockIn.setApprovalStatus(clockIn.getApprovalStatus()); + dailyClockIn.setClockInType(clockIn.getClockInType()); + // 新增备注 + dailyClockIn.setRemark(clockIn.getRemark()); + dailyClockIn.setAssociationApplyId(clockIn.getAssociationApplyId()); + dailyClockIn.setNeedApproval(needApproval); + // 设置最终结果 + String lastResult = getClockInLastResult(dailyClockIn.getAttendanceType(), dailyClockIn.getRepaired(), dailyClockIn.getChanged(), + dailyClockIn.getClockInStatus(), dailyClockIn.getClockInKind(), dailyClockIn.getClockInType(), rule); + dailyClockIn.setLastResult(lastResult); + // 判断能否变更 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) { + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || ConstantUtil.KIND_OUTSIDE == dailyClockIn.getClockInKind()) { + dailyClockIn.setCouldChange(ConstantUtil.NUM_FALSE); + } else if (!getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockIn.getClockInType())) { + dailyClockIn.setCouldChange(ConstantUtil.NUM_TRUE); + } + } + return dailyClockIn; + } + + private String getCheckNextDay(Date clockInTime, Date day) { + + String str = clockInTime == null ? "" : DateDetail.getDate2Str(day, DateDetail.DF).equals(DateDetail.getDate2Str(clockInTime, DateDetail.DF)) ? "" : "次日 "; + return clockInTime == null ? "" : str + DateDetail.getDate2Str(clockInTime, DateDetail.DF10); + } + + /** + * 获取最终结果 + * + * @param attendanceType 出勤类型(4: 加班) + * @param repaired 是否补卡(1: 是, 0: 否) + * @param changed 结果是否变更(1: 是, 0: 否) + * @param clockInStatus 打卡状态(-1: 缺卡, 1: 正常, 2: 迟到, 3: 早退) + * @param clockInKind 打卡种类(1: 普通打卡, 2: 外勤打卡) + * @param rule 审核数据展示标识(1.普通班 9.标识出差 10.标识外出) + * @return java.lang.String + */ + private String getClockInLastResult(Integer attendanceType, Integer repaired, Integer changed, Integer clockInStatus, Integer clockInKind, Integer clockInType, FtbAttendanceDailyRule rule) { + + String lastResult = ""; + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(attendanceType)) { + lastResult = "加班"; + } + if (clockInKind.equals(ConstantUtil.KIND_OUTSIDE)) { + lastResult = "外勤"; + } + switch (clockInStatus) { + case -1: + lastResult = "缺卡"; + break; + case 1: + if (StringUtils.isEmpty(lastResult)) { + lastResult = "正常"; + } + break; + case 2: + lastResult = "迟到"; + break; + case 3: + lastResult = "早退"; + break; + case 4: + lastResult = "旷工"; + break; + default: + break; + } + if (null != repaired && ConstantUtil.NUM_TRUE == repaired) { + lastResult += "(补卡)"; + } + if (null != changed && ConstantUtil.NUM_TRUE == changed) { + lastResult += "(变更)"; + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())) { + lastResult = AttendanceTypeEnum.BUSINESS_TRIP.getMsg(); + } + if (getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockInType)) { + lastResult = AttendanceTypeEnum.STEP_OUT.getMsg(); + } + return lastResult; + } + + private String getClockInMethod(Integer deviceType) { + + if (null == deviceType) { + return ""; + } + switch (deviceType) { + case ConstantUtil.DEVICE_PLACE: + return "地点打卡"; + case ConstantUtil.DEVICE_WIFI: + return "Wi-Fi打卡"; + case ConstantUtil.DEVICE_MACHINE: + return "考勤机打卡"; + default: + return ""; + } + } + + private DailyBaseInfoVo setDailyBaseInfo(Date date, BaseUserInfoVo baseUserInfo) { + + DailyBaseInfoVo dailyBaseInfo = new DailyBaseInfoVo(); + dailyBaseInfo.setUserId(baseUserInfo.getUserId()); + dailyBaseInfo.setDay(date); + dailyBaseInfo.setOrganizeNames(baseUserInfo.getOrganizeNames()); + dailyBaseInfo.setPositionName(baseUserInfo.getPositionName()); + dailyBaseInfo.setUserName(baseUserInfo.getUserName()); + dailyBaseInfo.setWorkStatus(baseUserInfo.getWorkStatusName()); + return dailyBaseInfo; + } + + @Override + public List getDailyClockInRecord(String userId, String queryDate, String currentGroupId) { + + List list = new ArrayList<>(); + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + // 查询当日出勤规则 + List dailyRuleList = getCurrentDailyRuleListOfWithdraw(DateDetail.getStr2Date(queryDate), userInfo); + if (dailyRuleList.isEmpty()) { + return new ArrayList<>(); + } + // 查询出勤规则关联的打卡记录 + List ruleIdList = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + List clockInResultList = attendanceClockInMapper.getClockInResultByRuleList(ruleIdList); + Set groupIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toSet()); + // 查询考勤组信息 + List miniGroupList = attendanceGroupUserMapper.getGroupUserList(groupIds, userId, ConstantUtil.DEL); + // 查询用户自己的考勤组 + String selfGroupId = attendanceGroupUserMapper.getSelfGroup(userId); + // 判断 我的/原/借出/借入考勤组 + if (currentGroupId.equals(selfGroupId)) { + // 查询 我的 和 借出的考勤组 + miniGroupList.forEach(group -> { + if (currentGroupId.equals(group.getGroupId())) { + group.setGroupKind(ConstantUtil.GROUP_KIND_MINE); + } else { + group.setGroupKind(ConstantUtil.GROUP_KIND_LEND); + } + }); + } else { + miniGroupList.forEach(group -> { + if (currentGroupId.equals(group.getGroupId())) { + if (group.getType().equals(3)) { + // 当前考勤组 关联的 用户 是移除状态 -> 原考勤组 + group.setGroupKind(ConstantUtil.GROUP_KIND_OLD); + } else { + // 当前考勤组 == 借调考勤组 -> 借入考勤组 + group.setGroupKind(ConstantUtil.GROUP_KIND_BORROW); + } + } + }); + } + miniGroupList.removeIf(group -> group.getGroupKind() == null); + if (miniGroupList.isEmpty()) { + new ArrayList<>(); + } + // 生成打卡结果信息 + miniGroupList.forEach(group -> { + List ruleList = dailyRuleList.stream().filter(v -> v.getGroupId().equals(group.getGroupId())) + .sorted(Comparator.comparing(FtbAttendanceDailyRule::getInPoint, Comparator.nullsLast(Comparator.naturalOrder()))).collect(Collectors.toList()); + if (!ruleList.isEmpty()) { + List ruleAndClockList = new ArrayList<>(); + ruleList.forEach(rule -> { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + // 请假 + String onRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 "; + String offRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 "; + RuleAndClockVo leaveWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_LEAVE, rule.getId(), null, + onRestStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10) + " - " + offRestStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10)); + ruleAndClockList.add(leaveWorkVo); + } else if (!rule.getAttendanceType().equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) + && !rule.getAttendanceType().equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()) + && !rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())) { + // 根据规则获取打卡信息 + List collect = clockInResultList.stream().filter(v -> v.getRuleId().equals(rule.getId())).collect(Collectors.toList()); + // 上班时间 + String onStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF)) ? "" : "次日 "; + RuleAndClockVo onWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_WORK, rule.getId(), ConstantUtil.ON_WORK, onStr + DateDetail.getDate2Str(rule.getInPoint(), DateDetail.DF10)); + collect.stream().filter(v -> v.getClockInType().equals(ConstantUtil.ON_WORK)).findFirst().ifPresent(onWorkVo::setClockInInfo); + ruleAndClockList.add(onWorkVo); + // 休息时间 + if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) && null != rule.getBreakStartPoint() && null != rule.getBreakEndPoint()) { + String onRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getBreakStartPoint(), DateDetail.DF)) ? "" : "次日 "; + String offRestStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getBreakEndPoint(), DateDetail.DF)) ? "" : "次日 "; + RuleAndClockVo restWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_REST, rule.getId(), null, + onRestStr + DateDetail.getDate2Str(rule.getBreakStartPoint(), DateDetail.DF10) + " - " + offRestStr + DateDetail.getDate2Str(rule.getBreakEndPoint(), DateDetail.DF10)); + ruleAndClockList.add(restWorkVo); + } + // 下班时间 + String offStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF).equals(DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF)) ? "" : "次日 "; + RuleAndClockVo offWorkVo = new RuleAndClockVo(ConstantUtil.RULE_TYPE_WORK, rule.getId(), ConstantUtil.OFF_WORK, offStr + DateDetail.getDate2Str(rule.getOutPoint(), DateDetail.DF10)); + collect.stream().filter(v -> v.getClockInType().equals(ConstantUtil.OFF_WORK)).findFirst().ifPresent(offWorkVo::setClockInInfo); + ruleAndClockList.add(offWorkVo); + // 判断是否旷工 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + if (null != onWorkVo.getAbsence() && null != offWorkVo.getAbsence() && onWorkVo.getAbsence().equals(ConstantUtil.NUM_TRUE) && offWorkVo.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + onWorkVo.setClockInStatus(ClockInStatusEnum.ABSENCE.getValue()); + offWorkVo.setClockInStatus(ClockInStatusEnum.ABSENCE.getValue()); + } + } + } + }); + group.getList().addAll(ruleAndClockList); + list.add(group); + } + }); + return list; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void attendanceChange(String applyId, String passed, String approveUserId, String tenantId) throws HandleException { + + log.info("考勤变更 -> applyId: {}, passed: {}, approveUserId: {}", applyId, passed, approveUserId); + // 根据审批id查询审批内容 + ApplyAttendanceChange applyAttendanceChange = applyAttendanceChangeMapper.selectById(applyId); + if (null == applyAttendanceChange) { + log.info("未找到审批记录"); + return; + } + log.info("审批内容:{}", applyAttendanceChange); + // 查询打卡结果记录 + LambdaQueryWrapper resultQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getId, applyAttendanceChange.getClockInResultId()) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectOne(resultQueryWrapper); + if (null == clockInResult) { + log.info("未找到打卡结果"); + return; + } + FtbAttendanceDailyRule rule = attendanceDailyRuleMapper.selectById(clockInResult.getRuleId()); + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) + || getOutsideCheck(new AttendanceRuleVo(rule.getApplyViewEnable(), rule.getOutStepOutType(), rule.getInStepOutType()), clockInResult.getClockInType())) { + log.info("操作失败,班次结果已变更"); + } + int pass = Integer.parseInt(passed.substring(0, 1)); + Integer status = null; + // String title = "您发起的【出勤变更】审批%s"; + switch (pass) { + case 0: + status = ConstantUtil.STATUS_REFUSE; + // title = String.format(title, "已驳回"); + case 2: + log.info("拒绝或撤销..."); + if (null == status) { + status = ConstantUtil.STATUS_ROLLBACK; + // title = String.format(title, "已撤回"); + } + String ruleId = null; + if (applyAttendanceChange.getChangeType().equals(1) || applyAttendanceChange.getChangeType().equals(2)) { + ruleId = clockInResult.getRuleId(); + } + emptyApplyInfo(clockInResult.getId(), ruleId, clockInResult.getLastAbsenceLeader()); + break; + case 1: + // title = String.format(title, "已通过"); + status = ConstantUtil.STATUS_PASS; + changePassed(clockInResult, applyAttendanceChange.getChangeType(), applyAttendanceChange.getCreatorUserId(), tenantId); + break; + default: + break; + } + // 更新审批记录 + ActionResult usersBound = v2UserApi.getUsersBound(approveUserId, null); + LambdaUpdateWrapper changeUpdate = new LambdaUpdateWrapper() + .set(ApplyAttendanceChange::getApproveUserId, approveUserId) + .set(ApplyAttendanceChange::getApproveTime, new Date()) + .set(ApplyAttendanceChange::getStatus, status) + .eq(ApplyAttendanceChange::getId, applyId); + if (200 == usersBound.getCode() && null != usersBound.getData()) { + changeUpdate.set(ApplyAttendanceChange::getApproveUserName, usersBound.getData().getUserName()); + } + applyAttendanceChangeMapper.update(null, changeUpdate); + //sendToIm(applyId, applyAttendanceChange.getCreatorUserId(), title, approveUser.getUserName(), ConstantUtil.APPROVE_NOTICE_CHANGE, passed); + //sendResultChangedToIm(clockInResult, tenantId, applyAttendanceChange.getCreatorUserId(), applyAttendanceChange.getChangeType()); + } + + private void changePassed(AttendanceClockInResult clockInResult, Integer changeType, String handleUser, String tenantId) throws HandleException { + log.info("变更通过中..."); + AttendanceRuleVo rule = getAttendanceRule(clockInResult.getRuleId()); + if (rule == null) { + log.info("未找到出勤规则"); + return; + } + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(clockInResult.getUserId()); + // 判断考勤组是否被封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(rule.getUserId()), monthDate); + if (map.get(rule.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法变更!"); + } + switch (changeType) { + case 1: + log.info("变更为旷工..."); + // 变更为旷工 根据ruleId找到对应的上下班打卡, 同时更新为旷工 + // 判断有没有下班打卡记录, 有, 同时变更, 无, 新增缺卡记录标识为旷工 + AttendanceClockInResult offWork = getClockInResultByWorkType(clockInResult.getRuleId(), clockInResult.getUserId(), ConstantUtil.OFF_WORK); + if (null == offWork) { + AttendanceClockInResult noClockRecord = generateNoClockRecord(rule, ConstantUtil.OFF_WORK, null, userInfo); + noClockRecord.setAbsenceLeader(handleUser); + noClockRecord.setLastAbsenceLeader(handleUser); + attendanceClockInResultMapper.insert(noClockRecord); + } else { + emptyApplyInfo(offWork.getId(), null, handleUser); + } + attendanceClockInMapper.updateToAbsence(clockInResult.getRuleId(), clockInResult.getUserId(), handleUser); + emptyApplyInfo(clockInResult.getId(), clockInResult.getRuleId(), handleUser); + break; + case 2: + log.info("撤销旷工..."); + // 撤销旷工 根据ruleId找到对应的上下班打卡, 同时取消旷工 + // 查询下班打卡记录, 普班/加班, 缺卡时间, 缺卡还没结束 + attendanceClockInMapper.updateToUnAbsence(clockInResult.getRuleId(), clockInResult.getUserId(), handleUser); + emptyApplyInfo(clockInResult.getId(), clockInResult.getRuleId(), handleUser); + AttendanceClockInResult offWorkRecord = getClockInResultByWorkType(clockInResult.getRuleId(), clockInResult.getUserId(), ConstantUtil.OFF_WORK); + // emptyApplyInfo(offWorkRecord.getId(), handleUser); + Date absenceTime; + if (offWorkRecord.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + if (rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 查询加班缺卡时间点 + MutablePair datePair = getOverTimeWorkRange(rule, userInfo.getUserId()); + absenceTime = datePair.getRight(); + } else { + // 普班缺卡时间点 + absenceTime = rule.getOutLackPoint(); + } + if (null != absenceTime && absenceTime.after(new Date())) { + // 删除缺卡记录 + attendanceClockInResultMapper.deleteById(offWorkRecord.getId()); + } + } + break; + case 3: + log.info("变更为正常..."); + if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + throw new HandleException("旷工的卡无法变更为正常"); + } + // 变更为正常 将对应的打卡结果更改为正常, 异常时间归零 + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + clockInResult.setEffectiveTime(rule.getInPoint()); + } else { + clockInResult.setEffectiveTime(rule.getOutPoint()); + } + clockInResult.setApplyType(null); + clockInResult.setApplyId(null); + clockInResult.setLastAbsenceLeader(handleUser); + attendanceClockInResultMapper.updateById(clockInResult); + attendanceClockInResultMapper.updateResultReplyToNull(clockInResult.getId(), handleUser); + break; + case 4: + log.info("变更为补卡..."); + // 补卡 新增打卡记录并绑定, 不标识为补卡 + FtbAttendanceClockIn repairClockIn = new FtbAttendanceClockIn(); + repairClockIn.setId(FtbUtil.getId()); + repairClockIn.setUserId(clockInResult.getUserId()); + repairClockIn.setDay(rule.getDay()); + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + repairClockIn.setClockInTime(rule.getInPoint()); + } else { + repairClockIn.setClockInTime(rule.getOutPoint()); + } + repairClockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + repairClockIn.setClockInKind(ConstantUtil.KIND_NORMAL); + // repairClockIn.setDeviceType(ConstantUtil.DEVICE_PLACE); + repairClockIn.setDeleteMark(ConstantUtil.NUM_FALSE); + // 生成打卡结果 + AttendanceClockInResult insertResult = generateClockInResultRecord(rule, repairClockIn, null, clockInResult.getClockInType(), userInfo); + insertResult.setApplyId(null); + insertResult.setApplyType(null); + insertResult.setAbsenceLeader(handleUser); + insertResult.setLastAbsenceLeader(handleUser); + this.save(repairClockIn); + attendanceClockInResultMapper.deleteById(clockInResult); + insertResult.setId(clockInResult.getId()); + attendanceClockInResultMapper.insert(insertResult); + break; + default: + break; + } + // 发布日统计数据生成事件 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(rule.getGroupId()) + .userId(clockInResult.getUserId()) + .day(rule.getDay()) + .triggerSceneEnum(TriggerSceneEnum.CHECK_IN) + .build(); + if(StrUtil.isBlank(tenantId) || StrUtil.isBlank(rule.getGroupId())){ + log.error("AttendanceClockInServiceImpl#changePassed 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + } + + @Override + public void attendanceChangeNoApproval(String clockInResultId, Integer changeType) throws HandleException { + + UserInfo userInfo = userProvider.get(); + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectById(clockInResultId); + int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId); + if (count > 0) { + throw new HandleException("当前记录正在审批中,请审批结束后再重新执行操作"); + } + changePassed(clockInResult, changeType, userInfo.getUserId(), userInfo.getTenantId()); + // 发送消息 + //sendResultChangedToIm(clockInResult, userInfo.getTenantId(), userInfo.getUserId(), changeType); + } + + private AttendanceClockInResult getClockInResultByWorkType(String ruleId, String userId, Integer workType) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getRuleId, ruleId) + .eq(AttendanceClockInResult::getUserId, userId) + .eq(AttendanceClockInResult::getClockInType, workType) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE); + return attendanceClockInResultMapper.selectOne(queryWrapper); + } + + /** + * 初始化缺卡信息v2.0 + * @param day 日期 + * @return java.util.Map> + */ + private Map> initAbsenceInfo2(Date day, String uId) { + + Map> userRuleMap = clockInResultService.getTodayRuleListByUser(day, uId); + if (userRuleMap.isEmpty()) { + // 今日没有需要缺卡的 + return new HashMap<>(); + } + Map> map = new HashMap<>(); + userRuleMap.forEach((userId, list) -> { + if (null != list && !list.isEmpty()) { + Map> ruleCollect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getId)); + ruleCollect.forEach((ruleId, v) -> { + List absenceList = map.computeIfAbsent(userId + ":" + ruleId, k -> new ArrayList<>()); + // 昨天 -> 今天 + v.sort(Comparator.comparingInt(FtbAttendanceDailyRule::getRn).reversed()); + v.forEach(rule -> { + if (null != rule.getInLackPoint()) { + if (DateDetail.checkSameDay(rule.getInLackPoint(), day)) { + AbsenceClockInVo onWork = new AbsenceClockInVo(userId, rule.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(rule.getInLackPoint(), DateDetail.DF2)); + absenceList.add(onWork); + } + } + if (null != rule.getOutLackPoint()) { + if (DateDetail.checkSameDay(rule.getOutLackPoint(), day)) { + AbsenceClockInVo offWork = new AbsenceClockInVo(userId, rule.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(rule.getOutLackPoint(), DateDetail.DF2)); + absenceList.add(offWork); + } + } + }); + }); + } + }); + return map; + } + + private Map> initAbsenceInfo(DateDetail dateDetail) { + + dateDetail.changeDay(new Date()); + Date today = dateDetail.getCurrentDate(); + dateDetail.getYesterday(); + Date yesterday = dateDetail.getCurrentDate(); + // 查询昨天和今天的排班信息 + Map> map = new HashMap<>(); + // 查询昨天所有下班缺卡时间是今天的出勤规则(普班) + log.error("查询昨天所有下班缺卡时间是今天的出勤规则, 参数: {}, {}", today, yesterday); + List list = attendanceDailyRuleMapper.getRuleListByDate(today, yesterday, null); + if (!list.isEmpty()) { + list.forEach(v -> { + List voList = map.computeIfAbsent(v.getUserId(), k -> new ArrayList<>()); + // 请假可以导致昨天的班次上班时间是今天 + if (null != v.getInLackPoint() && DateDetail.getDate2Str(v.getInLackPoint(), DateDetail.DF).equals(DateDetail.getDate2Str(today, DateDetail.DF))) { + AbsenceClockInVo vo = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(v.getInLackPoint(), DateDetail.DF2)); + voList.add(vo); + } + AbsenceClockInVo vo = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(v.getOutLackPoint(), DateDetail.DF2)); + voList.add(vo); + }); + log.error("数据不为空, 条数为: {}", list.size()); + } + // 查询昨天所有最后一个班次是加班的出勤规则(加班) + List overTimeList = attendanceDailyRuleMapper.getOverTimeRuleListByDate(yesterday, null); + if (!overTimeList.isEmpty()) { + AtomicInteger count = new AtomicInteger(0); + overTimeList.forEach(v -> { + List voList = map.computeIfAbsent(v.getUserId(), k -> new ArrayList<>()); + // 加班下班缺卡时间 + Date nextDate = getOverTimeOffWorkAbsenceTime(dateDetail, v.getId(), v.getInPoint(), v.getUserId()); + String dateStr = DateDetail.getDate2Str(today, DateDetail.DF16); + if (nextDate.after(DateDetail.getStr2DateTime(dateStr))) { + // 缺卡时间在今天 + AbsenceClockInVo vo = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(nextDate, DateDetail.DF2)); + voList.add(vo); + count.getAndIncrement(); + } + }); + log.error("定时缺卡 {}- 出勤规则为昨日加班, 需要在今天缺卡的条数: {}", UserProvider.getUser().getTenantId(), count.get()); + } + // 查询今天所有出勤规则(加班判断出勤规则是否是今天, 否, 不生成旷工时间点到redis) + List currentDailyRuleList = getCurrentDailyRuleList(today, null); + calculateAbsenceTime("生成缺卡任务", today, currentDailyRuleList, map, dateDetail); + return map; + } + + private void calculateAbsenceTime(String entrance, Date today, List currentDailyRuleList, Map> map, DateDetail dateDetail) { + if (!currentDailyRuleList.isEmpty()) { + // 筛选出 普班/加班 + currentDailyRuleList = currentDailyRuleList.stream() + .filter(v -> v.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) + .collect(Collectors.toList()); + // 今天晚上24点的日期 + String dateStr = DateDetail.getDate2Str(today, DateDetail.DF16); + dateDetail.changeDay(DateDetail.getStr2DateTime(dateStr)); + dateDetail.getTomorrow(); + Date tomorrow = dateDetail.getCurrentDate(); + log.error("定时缺卡 {}- {}- 计算旷工时间, 参数: {}, {}", UserProvider.getUser().getTenantId(), entrance, dateStr, tomorrow); + AtomicInteger count = new AtomicInteger(0); + currentDailyRuleList.forEach(v -> { + List voList = map.computeIfAbsent(v.getUserId(), k -> new ArrayList<>()); + if (v.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) { + // 普班 + AbsenceClockInVo vo1 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(v.getInLackPoint(), DateDetail.DF2)); + voList.add(vo1); + count.getAndIncrement(); + if (v.getOutLackPoint().before(tomorrow)) { + AbsenceClockInVo vo2 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(v.getOutLackPoint(), DateDetail.DF2)); + voList.add(vo2); + count.getAndIncrement(); + } + } else { + // 加班 + if (v.getOutPoint().before(tomorrow)) { + // 下班时间在今天, 上班缺卡就在今天 + AbsenceClockInVo vo3 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.ON_WORK, DateDetail.getDate2Str(v.getOutPoint(), DateDetail.DF2)); + voList.add(vo3); + count.getAndIncrement(); + Date nextDate = getOverTimeOffWorkAbsenceTime(dateDetail, v.getId(), v.getInPoint(), v.getUserId()); + if (nextDate.before(tomorrow)) { + // 下班缺卡时间在明天之前 + AbsenceClockInVo vo4 = new AbsenceClockInVo(v.getUserId(), v.getId(), ConstantUtil.OFF_WORK, DateDetail.getDate2Str(nextDate, DateDetail.DF2)); + voList.add(vo4); + count.getAndIncrement(); + } + } + } + }); + log.error("定时缺卡 {}- {}- 今日需要缺卡的条数: {}", UserProvider.getUser().getTenantId(), entrance, count.get()); + } + } + + /** + * 获取加班下班缺卡时间 + * + * @param dateDetail 工具 + * @param ruleId 规则id + * @param inPoint 上班时间 + * @param userId 用户id + * @return java.util.Date + */ + private Date getOverTimeOffWorkAbsenceTime(DateDetail dateDetail, String ruleId, Date inPoint, String userId) { + + dateDetail.changeDay(inPoint); + dateDetail.getTomorrow(); + Date nextDate = dateDetail.getCurrentDate(); + FtbAttendanceDailyRule nextRule = attendanceDailyRuleMapper.getPreviousOrNextRule(ruleId, inPoint, nextDate, userId, "next"); + if (null != nextRule) { + nextDate = nextRule.getInPoint(); + } + return nextDate; + } + + private void dealChooseRecord(List resultList, List clockInList, Map dbResultMap, Map chooseMap, + String type, AttendanceRuleVo rule, Integer workType, UserInfo userInfo) throws HandleException { + if (onWorkNotNeed(rule, workType) && !AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) && !getOutsideCheck(rule, workType)) { + // 上班无需打卡, 生成-2 + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, workType, rule.getInLackPoint(), userInfo); + resultList.add(result); + return; + } + if (offWorkNotNeed(rule, workType) && !AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) && !getOutsideCheck(rule, workType)) { + // 下班无需打卡, 生成-2 + AttendanceClockInResult result = generateNoClockRecordRematch(rule, null, workType, rule.getOutLackPoint(), userInfo); + resultList.add(result); + return; + } + assert !chooseMap.isEmpty(); + int index; + if (type.equals(ConstantUtil.MAX)) { + index = chooseMap.keySet().stream().max(Integer::compareTo).get(); + } else { + index = chooseMap.keySet().stream().min(Integer::compareTo).get(); + } + FtbAttendanceClockIn clockIn = chooseMap.get(index); + AttendanceClockInResult result = generateClockInResultRecord(rule, clockIn, dbResultMap.get(clockIn.getId()), workType, userInfo); + resultList.add(result); + clockInList.remove(index); + clockInList.removeIf(v -> v.getClockInTime().before(clockIn.getClockInTime())); + } + + /** + * 生成旷工记录 + * + * @param rule 出勤规则 + * @param workType 打卡类型(1: 上班, 2: 下班) + * @param userInfo 用户信息 + * @return jnpf.entity.attendance.AttendanceClockInResult + */ + private AttendanceClockInResult generateNoClockRecord(AttendanceRuleVo rule, Integer workType, Date keyPoint, UserInfo userInfo) { + + if (null == keyPoint) { + return generateNoClock(rule, workType, userInfo); + } else { + if (new Date().after(keyPoint)) { + return generateNoClock(rule, workType, userInfo); + } + } + return null; + } + + private AttendanceClockInResult generateNoClock(AttendanceRuleVo rule, Integer workType, UserInfo userInfo) { + + AttendanceClockInResult result = new AttendanceClockInResult(); + result.setId(FtbUtil.getId()); + result.setRuleId(rule.getId()); + result.setUserId(userInfo.getUserId()); + result.setClockInType(workType); + result.setClockInKind(ConstantUtil.KIND_NORMAL); + result.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue()); + result.setAbsence(ConstantUtil.NUM_FALSE); + result.setAbnormalMinute(0); + if (onWorkNotNeed(rule, workType) || offWorkNotNeed(rule, workType) + || Objects.equals(rule.getApplyViewEnable(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || getOutsideCheck(rule, workType)) { + result.setClockInStatus(ClockInStatusEnum.NEED_NOT.getValue()); + if (workType.equals(ConstantUtil.ON_WORK)) { + result.setEffectiveTime(rule.getInPoint()); + } else { + result.setEffectiveTime(rule.getOutPoint()); + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute; + if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) { + restMinute = 0; + } else { + restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + } + result.setRestMinute(restMinute); + } + } + } else { + Integer wt = workType.equals(ConstantUtil.OFF_WORK) ? ConstantUtil.ON_WORK : ConstantUtil.OFF_WORK; + // 判断成对的班次是否也是缺卡 + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getRuleId, rule.getId()) + .eq(AttendanceClockInResult::getClockInType, wt) + .eq(AttendanceClockInResult::getClockInStatus, ClockInStatusEnum.NO_CLOCK.getValue()) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE) + .last("limit 1"); + AttendanceClockInResult clockInResult = attendanceClockInResultMapper.selectOne(query); + result.setAttendanceType(rule.getAttendanceType()); + if (null != clockInResult && !rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 加班不旷工 + LambdaUpdateWrapper update = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getAbsence, ConstantUtil.NUM_TRUE) + .eq(AttendanceClockInResult::getId, clockInResult.getId()); + attendanceClockInResultMapper.update(null, update); + result.setAbsence(ConstantUtil.NUM_TRUE); + } + } + return result; + } + + private MutablePair generateNoClockAndCondition(AttendanceRuleVo rule, Integer workType, UserInfo userInfo) { + + ConditionVo condition = null; + AttendanceClockInResult result = new AttendanceClockInResult(); + result.setId(FtbUtil.getId()); + result.setRuleId(rule.getId()); + result.setUserId(userInfo.getUserId()); + result.setClockInType(workType); + result.setClockInKind(ConstantUtil.KIND_NORMAL); + result.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue()); + result.setAbsence(ConstantUtil.NUM_FALSE); + result.setAbnormalMinute(0); + if (onWorkNotNeed(rule, workType) || offWorkNotNeed(rule, workType) + || Objects.equals(rule.getApplyViewEnable(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || getOutsideCheck(rule, workType)) { + result.setClockInStatus(ClockInStatusEnum.NEED_NOT.getValue()); + if (workType.equals(ConstantUtil.ON_WORK)) { + result.setEffectiveTime(rule.getInPoint()); + } else { + result.setEffectiveTime(rule.getOutPoint()); + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute; + if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) { + restMinute = 0; + } else { + restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + } + result.setRestMinute(restMinute); + } + } + } else { + Integer wt = workType.equals(ConstantUtil.OFF_WORK) ? ConstantUtil.ON_WORK : ConstantUtil.OFF_WORK; + condition = new ConditionVo(rule.getId(), wt); + } + return MutablePair.of(result, condition); + } + + /** + * 上班无需打卡 + */ + private boolean onWorkNotNeed(AttendanceRuleVo rule, Integer workType) { + + return workType.equals(ConstantUtil.ON_WORK) && rule.isOnWorkIgnore(); + } + + /** + * 下班无需打卡 + */ + private boolean offWorkNotNeed(AttendanceRuleVo rule, Integer workType) { + + return workType.equals(ConstantUtil.OFF_WORK) && rule.isOffWorkIgnore(); + } + + /** + * 生成缺卡记录[重新匹配打卡记录] + * @param rule 出勤规则 + * @param onWorkResult 上班打卡(workTye为下班时传) + * @param workType 上班/下班 + * @param keyPoint 缺卡时间点 + * @param userInfo 用户信息 + * @return jnpf.entity.attendance.AttendanceClockInResult + */ + private AttendanceClockInResult generateNoClockRecordRematch(AttendanceRuleVo rule, AttendanceClockInResult onWorkResult, Integer workType, Date keyPoint, UserInfo userInfo) { + + if (null == keyPoint) { + return generateNoClockRematch(rule, onWorkResult, workType, userInfo); + } else { + if (new Date().after(keyPoint)) { + return generateNoClockRematch(rule, onWorkResult, workType, userInfo); + } + } + return null; + } + + private AttendanceClockInResult generateNoClockRematch(AttendanceRuleVo rule, AttendanceClockInResult onWorkResult, Integer workType, UserInfo userInfo) { + + AttendanceClockInResult result = new AttendanceClockInResult(); + result.setId(FtbUtil.getId()); + result.setRuleId(rule.getId()); + result.setUserId(userInfo.getUserId()); + result.setClockInType(workType); + result.setClockInKind(ConstantUtil.KIND_NORMAL); + result.setClockInStatus(ClockInStatusEnum.NO_CLOCK.getValue()); + result.setAbsence(ConstantUtil.NUM_FALSE); + result.setAbnormalMinute(0); + if (onWorkNotNeed(rule, workType) || offWorkNotNeed(rule, workType) + || Objects.equals(rule.getApplyViewEnable(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || getOutsideCheck(rule, workType)) { + result.setClockInStatus(ClockInStatusEnum.NEED_NOT.getValue()); + if (workType.equals(ConstantUtil.ON_WORK)) { + result.setEffectiveTime(rule.getInPoint()); + } else { + result.setEffectiveTime(rule.getOutPoint()); + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute; + if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) { + restMinute = 0; + } else { + restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + } + result.setRestMinute(restMinute); + } + } + } else { + if (workType.equals(ConstantUtil.OFF_WORK)) { + if (null != onWorkResult && !rule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 判断上班是否也是缺卡(加班不计旷工) + if (onWorkResult.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + // 设置为旷工 + onWorkResult.setAbsence(ConstantUtil.NUM_TRUE); + result.setAbsence(ConstantUtil.NUM_TRUE); + } + } + } + } + return result; + } + + /** + * 筛选打卡记录 + * + * @param startPoint 开始时间 + * @param endPoint 结束时间 + * @return java.util.Map + */ + private Map getChooseClockRecord(Date startPoint, Date endPoint, List clockInList) { + + Map map = new HashMap<>(); + for (int i = 0; i < clockInList.size(); i++) { + if (DateDetail.checkTimeBetween(clockInList.get(i).getClockInTime(), startPoint, endPoint)) { + map.put(i, clockInList.get(i)); + } else { + if (clockInList.get(i).getClockInTime().after(endPoint)) { + break; + } + } + } + return map; + } + + private AttendanceClockInResult generateClockInResultRecord(AttendanceRuleVo rule, FtbAttendanceClockIn clockIn, AttendanceClockInResult dbResult, Integer clockInType, UserInfo userInfo) throws HandleException { + + if (null == dbResult) { + // 根据打卡id查询打卡结果(变更多次会产生id绑定了多个结果, 时间从大到小获取第一个) + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceClockInResult::getClockInId, clockIn.getId()) + .orderByDesc(AttendanceClockInResult::getCreatorTime) + .last("limit 1"); + dbResult = attendanceClockInResultMapper.selectOne(lambdaQueryWrapper); + } + + AttendanceClockInResult clockInResult = new AttendanceClockInResult(); + clockInResult.setId(FtbUtil.getId()); + clockInResult.setRuleId(getRuleId(rule)); + clockInResult.setUserId(userInfo.getUserId()); + clockInResult.setRepaired(null == dbResult ? ConstantUtil.NUM_FALSE : dbResult.getRepaired()); + clockInResult.setClockInId(clockIn.getId()); + clockInResult.setClockInType(clockInType); + clockInResult.setClockInKind(clockIn.getClockInKind()); + clockInResult.setAbsence(ConstantUtil.NUM_FALSE); + clockInResult.setOldResultId(null == dbResult ? null : dbResult.getId()); + AttendanceTypeEnum attendanceType = AttendanceTypeEnum.getEnum(rule.getAttendanceType()); + if (null == attendanceType) { + throw new HandleException("未知类型"); + } + if (clockIn.getApprovalStatus().equals(ConstantUtil.PASS_APPROVAL)) { + if (null == clockIn.getApplyType()) { + throw new HandleException("打卡异常,请刷新页面后重试"); + } + clockInResult.setApplyType(clockIn.getApplyType().getValue()); + clockInResult.setApplyId(clockIn.getApprovalCode()); + } + if (clockInType.equals(ConstantUtil.OFF_WORK) && rule.getOvertime().equals(ConstantUtil.NUM_TRUE)) { + // 判定打卡时间是否超过配置的加班最低时间, 超过则申请打加班卡, 并生成当前出勤规则的下班无需打卡, 和加班的上班无需打卡 + int minute; + Date beginDate; + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) { + beginDate = rule.getOnWorkInfoVo().getClockInTime(); + } else { + beginDate = rule.getOutPoint(); + } + minute = DateDetail.calculateMinuteDiff(beginDate, clockIn.getClockInTime()); + if (minute >= rule.getOvertimeRuleDetail().getMinOvertimeMinute()) { + // 加班规则 + FtbAttendanceDailyRule overtimeRule = attendanceDailyRuleService.workOvertimeNotApprove(generateHandleParam(rule.getOvertimeRuleDetail(), userInfo, rule, beginDate, clockIn.getClockInTime())); + AttendanceRuleVo rVo = JsonUtil.getJsonToBean(overtimeRule, AttendanceRuleVo.class); + if (rule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.DEFAULT.getCode())) { + // 删除上班卡与休/未排班的关联, 关联到加班打卡上 + attendanceClockInResultMapper.updateResultRelation(rule.getOnWorkInfoVo().getId(), overtimeRule.getId()); + } else { + // 下班无需打卡 + rule.setOffWorkIgnore(true); + AttendanceClockInResult result = generateNoClock(rule, clockInType, userInfo); + // 加班上班无需打卡 + rVo.setOnWorkIgnore(true); + AttendanceClockInResult notNeedOnWork = generateNoClock(rVo, ConstantUtil.ON_WORK, userInfo); + attendanceClockInResultMapper.insert(result); + attendanceClockInResultMapper.insert(notNeedOnWork); + attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rVo, rVo.getInPoint(), clockIn.getClockInTime())); + } + // 加班下班卡 + clockInResult.setRuleId(rVo.getId()); + clockInResult.setEffectiveTime(clockIn.getClockInTime()); + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + // 生成redis缺卡缓存 用于加班存休 + String todayStr = DateDetail.getDate2Str(overtimeRule.getDay(), DateDetail.DF); + String hashKey = userInfo.getTenantId() + ":" + todayStr; + absenceTimeChange(overtimeRule.getUserId(), overtimeRule.getDay(), hashKey); + return clockInResult; + } + } + // 如果标识为外出和出差 且 出勤规则不是请假、休、未排班 + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(rule, clockInType)) { + if (AttendanceTypeEnum.ORDINARY.equals(attendanceType) || AttendanceTypeEnum.WORKOVERTIME.equals(attendanceType)) { + // 有效打卡时间为满勤(有效打卡时间为上班时间或下班时间), -休息时间 + if (clockInType.equals(ConstantUtil.ON_WORK)) { + clockInResult.setEffectiveTime(rule.getInPoint()); + } else { + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + int restMinute; + if (null == rule.getBreakStartPoint() || null == rule.getBreakEndPoint()) { + restMinute = 0; + } else { + restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + } + clockInResult.setRestMinute(restMinute); + } + clockInResult.setEffectiveTime(rule.getOutPoint()); + } + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + return clockInResult; + } + } + switch (attendanceType) { + case ORDINARY: + if (clockIn.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)) { + dealWorkOutside(clockInResult, clockInType, clockIn, rule, userInfo); + } else { + DateDetail dateDetail = new DateDetail(); + if (clockInType.equals(ConstantUtil.ON_WORK)) { + // 上班 是否开启休息 当前是否在休息时间 是否允许迟到 + Date onWorkTime = clockIn.getClockInTime(); + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + if (null != rule.getBreakStartPoint() && null != rule.getBreakEndPoint()) { + onWorkTime = DateDetail.checkTimeBetween(clockIn.getClockInTime(), rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakStartPoint() : clockIn.getClockInTime(); + } + } + Date effectiveTime; + int abnormalMinute = 0; + ClockInStatusEnum statusEnum = ClockInStatusEnum.NORMAL; + // 是否允许迟到, 是 判断迟到了多久, 否 迟到提示无法打卡 + if (rule.getLateEnable().equals(ConstantUtil.NUM_FALSE)) { + // 不允许迟到 + Date lateDate = dateDetail.setSecond(rule.getInPoint(), 0); + if (onWorkTime.after(lateDate)) { + throw new HandleException("暂不允许迟到打卡"); + } + effectiveTime = rule.getInPoint(); + } else { + // 允许迟到 迟到时间 +59 秒[暂弃] + Date lateDate = dateDetail.setSecond(rule.getLatePoint(), 0); + Date inLackPoint = DateDetail.goToMaxSeconds(rule.getInLackPoint()); + if (clockIn.getClockInTime().after(inLackPoint)) { + throw new HandleException("上班打卡时间已结束"); + } + if (clockIn.getClockInTime().after(lateDate)) { + statusEnum = ClockInStatusEnum.WORK_LATE; + } + if (onWorkTime.after(lateDate)) { + abnormalMinute = DateDetail.calculateSecondDiff(rule.getLatePoint(), onWorkTime); + effectiveTime = dateDetail.addNum(rule.getInPoint(), abnormalMinute, Calendar.SECOND); + // 计算迟到分钟数 如果打卡时间在休息时间后, 迟到分钟数 - 休息分钟数 + /*if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) && onWorkTime.after(rule.getBreakEndPoint())) { + int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + abnormalMinute -= restMinute; + }*/ + } else { + effectiveTime = rule.getInPoint(); + } + } + clockInResult.setEffectiveTime(effectiveTime); + clockInResult.setClockInStatus(statusEnum.getValue()); + clockInResult.setAbnormalMinute(abnormalMinute); + } else { + // 下班 是否开启休息 当前是否在休息时间 是否允许早退 (下班需要计算休息时间是多少分钟) + Date offWorkTime = clockIn.getClockInTime(); + // 下班开始时间 + Date offWorkBeginTime; + if (rule.getEarlyEnable().equals(ConstantUtil.NUM_FALSE)) { + offWorkBeginTime = rule.getOutPoint(); + } else { + offWorkBeginTime = rule.getEarlyPoint(); + } + // 打卡时间不在 上班缺卡时间点 - 下班缺卡时间点之间, 无法打卡 + Date outLackPoint = DateDetail.goToMaxSeconds(rule.getOutLackPoint()); + if (!DateDetail.checkTimeBetween(offWorkTime, offWorkBeginTime, outLackPoint)) { + throw new HandleException("未到下班打卡时间"); + } + int restMinute; + if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + if (null != rule.getBreakStartPoint() && null != rule.getBreakEndPoint()) { + offWorkTime = DateDetail.checkTimeBetween(clockIn.getClockInTime(), rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakEndPoint() : clockIn.getClockInTime(); + restMinute = getRestMinute(rule, clockIn.getClockInTime()); + } else { + offWorkTime = clockIn.getClockInTime(); + restMinute = 0; + } + clockInResult.setRestMinute(restMinute); + } + Date effectiveTime; + int abnormalMinute = 0; + ClockInStatusEnum statusEnum = ClockInStatusEnum.NORMAL; + // 是否允许早退 + if (rule.getEarlyEnable().equals(ConstantUtil.NUM_FALSE)) { + // 不允许早退 + if (offWorkTime.before(rule.getOutPoint())) { + throw new HandleException("暂不允许早退打卡"); + } + effectiveTime = rule.getOutPoint(); + } else { + // 允许早退 + if (DateDetail.checkTimeBetween(clockIn.getClockInTime(), rule.getEarlyPoint(), rule.getOutPoint())) { + statusEnum = ClockInStatusEnum.HOME_EARLY; + effectiveTime = offWorkTime; + abnormalMinute = DateDetail.calculateSecondDiff(DateDetail.getStr2DateTime(DateDetail.getDate2Str(clockIn.getClockInTime(), DateDetail.DF2)), rule.getOutPoint()); + // 计算早退分钟数 如果打卡时间在休息时间前, 早退分钟数 - 休息分钟数 + /*if (rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) && offWorkTime.before(rule.getBreakStartPoint())) { + abnormalMinute -= restMinute; + }*/ + } else { + // 正常下班 + effectiveTime = rule.getOutPoint(); + } + } + clockInResult.setEffectiveTime(effectiveTime); + clockInResult.setClockInStatus(statusEnum.getValue()); + clockInResult.setAbnormalMinute(abnormalMinute); + // 晚走晚到通知 + attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rule, rule.getOutPoint(), clockIn.getClockInTime())); + } + } + break; + case LEAVE: + throw new HandleException("请假无需打卡"); + case WORKOVERTIME: + dealWorkOvertime(clockInResult, clockInType, clockIn, rule, userInfo); + break; + case DEFAULT: + case REST: + case BUSINESS_TRIP: + case STEP_OUT: + clockInResult.setEffectiveTime(clockIn.getClockInTime()); + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + break; + default: + throw new HandleException("未知打卡种类"); + } + return clockInResult; + } + + private String getRuleId(AttendanceRuleVo rule) { + + if (StringUtils.isNotEmpty(rule.getId())) { + return rule.getId(); + } + // 未排班(-1) 需要赋值id并insert到数据库 + rule.setId(FtbUtil.getId()); + FtbAttendanceDailyRule dailyRule = JsonUtil.getJsonToBean(rule, FtbAttendanceDailyRule.class); + attendanceDailyRuleMapper.insert(dailyRule); + return dailyRule.getId(); + } + + private ClockOutHandleParam generateHandleParam(OvertimeRuleDetailVo ruleDetail, UserInfo userInfo, AttendanceRuleVo rule, Date startTime, Date endTime) { + + return ClockOutHandleParam.builder() + .groupId(rule.getGroupId()) + .ruleDetail(ruleDetail) + .userId(userInfo.getUserId()) + .tenantId(userInfo.getTenantId()) + .ruleId(rule.getId()) + .attendanceType(rule.getAttendanceType()) + .day(rule.getDay()) + .applyId(rule.getApplyId()) + .start(startTime) + .end(endTime) + .build(); + } + + @Override + public boolean getOutsideCheck(AttendanceRuleVo rule, Integer clockInType) { + + if (!rule.getApplyViewEnable().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + return false; + } + if (ConstantUtil.ON_WORK.equals(clockInType)) { + return rule.getInStepOutType() != null && !rule.getInStepOutType().equals(ConstantUtil.NUM_FALSE); + } + if (ConstantUtil.OFF_WORK.equals(clockInType)) { + return rule.getOutStepOutType() != null && !rule.getOutStepOutType().equals(ConstantUtil.NUM_FALSE); + } + return false; + } + + private int getRestMinute(AttendanceRuleVo rule, Date clockInTime) { + + // 上班时间在休息结束时间后, 休息时间为0 + int restMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + if (null != rule.getOnWorkInfoVo() && !rule.getOnWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue()) + && !rule.getOnWorkInfoVo().getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) + && rule.getOnWorkInfoVo().getClockInTime().after(rule.getBreakEndPoint())) { + // 上班时间在休息结束时间之后 + restMinute = 0; + } + if (clockInTime.before(rule.getBreakStartPoint())) { + // 下班时间在休息开始时间之前 + restMinute = 0; + } + return restMinute; + } + + /** + * 外勤处理 + * + * @param clockInResult 打卡结果 + * @param clockInType 上班/下班 + * @param clockIn 打卡记录 + * @param rule 出勤规则 + */ + private void dealWorkOutside(AttendanceClockInResult clockInResult, Integer clockInType, FtbAttendanceClockIn clockIn, AttendanceRuleVo rule, UserInfo userInfo) { + Date effective; + if (clockInType.equals(ConstantUtil.ON_WORK)) { + effective = clockIn.getClockInTime().after(rule.getInPoint()) ? clockIn.getClockInTime() : rule.getInPoint(); + if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE)) { + effective = DateDetail.checkTimeBetween(effective, rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakStartPoint() : effective; + } + } else { + effective = clockIn.getClockInTime().before(rule.getOutPoint()) ? clockIn.getClockInTime() : rule.getOutPoint(); + if (null != rule.getBreakEnable() && rule.getBreakEnable().equals(ConstantUtil.NUM_TRUE) + && (null != rule.getBreakStartPoint() || null != rule.getBreakEndPoint())) { + effective = DateDetail.checkTimeBetween(effective, rule.getBreakStartPoint(), rule.getBreakEndPoint()) ? rule.getBreakEndPoint() : effective; + int restMinute = getRestMinute(rule, clockIn.getClockInTime()); + clockInResult.setRestMinute(restMinute); + } + // 晚走晚到通知 + attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rule, rule.getOutPoint(), clockIn.getClockInTime())); + } + clockInResult.setEffectiveTime(effective); + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + } + + /** + * 加班处理 + * + * @param clockInResult 打卡结果 + * @param clockInType 上班/下班 + * @param clockIn 打卡记录 + * @param rule 出勤规则 + */ + private void dealWorkOvertime(AttendanceClockInResult clockInResult, Integer clockInType, FtbAttendanceClockIn clockIn, AttendanceRuleVo rule, UserInfo userInfo) { + Date effective; + if (clockInType.equals(ConstantUtil.ON_WORK)) { + if (rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) { + effective = rule.getInPoint(); + } else { + effective = clockIn.getClockInTime().after(rule.getInPoint()) ? clockIn.getClockInTime() : rule.getInPoint(); + } + } else { + if (rule.getOvertimeRuleDetail().getCalcMethod().equals(1)) { + effective = rule.getOutPoint(); + } else if (rule.getOvertimeRuleDetail().getCalcMethod().equals(2)) { + effective = clockIn.getClockInTime().before(rule.getOutPoint()) ? clockIn.getClockInTime() : rule.getOutPoint(); + } else { + // 无需审批 加班更新打卡 tips: 无需审批的加班下班卡不在此处, 前面的逻辑就处理后返回了, 正常情况只有加班更新打卡才会走到这个地方 + attendanceDailyRuleService.workOvertimeNotApprove(generateHandleParam(rule.getOvertimeRuleDetail(), userInfo, rule, rule.getInPoint(), clockIn.getClockInTime())); + effective = clockIn.getClockInTime(); + } + } + clockInResult.setEffectiveTime(effective); + clockInResult.setClockInStatus(ClockInStatusEnum.NORMAL.getValue()); + clockInResult.setAbnormalMinute(0); + attendanceDailyRuleService.clockOutHandle(generateHandleParam(null, userInfo, rule, rule.getInPoint(), clockIn.getClockInTime())); + } + + private FtbAttendanceClockIn setClockInRecord(MutablePair pair, ClockInDto clockInDto, String relationId, UserInfo userInfo) throws HandleException { + + FtbAttendanceClockIn clockIn = new FtbAttendanceClockIn(); + clockIn.setId(FtbUtil.getId()); + clockIn.setDay(pair.getRight().getDay()); + clockIn.setUserId(userInfo.getUserId()); + clockIn.setClockInTime(new Date()); + clockIn.setApprovalStatus(ConstantUtil.PASS_TRUE); + clockIn.setClockInKind(clockInDto.getClockInKind()); + clockIn.setApprovalCode(clockInDto.getApprovalCode()); + clockIn.setApplyType(clockInDto.getApplyType()); + if (clockInDto.getClockInKind() == ConstantUtil.KIND_OUTSIDE) { + // 外勤打卡 + if (pair.getLeft().getGroupRule().getFieldClockStatus().equals(ConstantUtil.NUM_FALSE)) { + throw new HandleException("暂未开启外勤打卡"); + } else { + if (pair.getLeft().getGroupRule().getApproveEnable().equals(ConstantUtil.NUM_TRUE)) { + clockIn.setApprovalStatus(ConstantUtil.PASS_APPROVAL); + clockIn.setRelationId(relationId); + } + } + } + if (ApplyTypeEnum.VIOLATION.equals(clockIn.getApplyType())) { + // 异常打卡 + clockIn.setApprovalStatus(ConstantUtil.PASS_APPROVAL); + clockIn.setRelationId(relationId); + } + clockIn.setAddress(clockInDto.getAddress()); + clockIn.setLng(clockInDto.getLng()); + clockIn.setLat(clockInDto.getLat()); + clockIn.setDeviceType(clockInDto.getDeviceType()); + clockIn.setDeviceId(clockInDto.getDeviceId()); + clockIn.setDeviceName(clockInDto.getDeviceName()); + clockIn.setRemark(clockInDto.getRemark()); + return clockIn; + } + + private MutablePair getGroupInfoAndRule(Date date, UserInfo userInfo) throws Exception { + + List groupList = getClockInMainInfo(date, userInfo, ConstantUtil.NUM_FALSE); + if (null == groupList || groupList.isEmpty()) { + throw new QueryException("您尚未加入考勤组"); + } + GroupInfoVo groupInfo = null; + AttendanceRuleVo currentRule = null; + for (GroupInfoVo vo : groupList) { + AttendanceRuleVo rule = vo.getAttendanceRuleList().stream().filter(v -> ConstantUtil.NUM_TRUE == v.getCurrentPeriod()).findFirst().orElse(null); + if (null != rule) { + groupInfo = vo; + currentRule = rule; + break; + } + } + if (null == groupInfo) { + throw new QueryException("当天班次已变更,请重试!"); + } + return MutablePair.of(groupInfo, currentRule); + } + + private Integer getClockInType(AttendanceRuleVo currentRule) throws QueryException { + + Integer clockInType; + if (null == currentRule.getOnWorkInfoVo()) { + clockInType = ConstantUtil.ON_WORK; + } else if (null == currentRule.getOffWorkInfoVo()) { + clockInType = ConstantUtil.OFF_WORK; + } else { + throw new QueryException("当前无需打卡"); + } + return clockInType; + } + + /** + * 设置用户出勤打卡记录 + * + * @param rule 出勤规则 + */ + private void setWorkRecord(List clockInList, AttendanceRuleVo rule, GroupInfoVo groupInfo, Map userMap) { + + GroupRuleVo groupRule = groupInfo.getGroupRule(); + if (null == rule.getId()) { + return; + } + // 设置上班打卡记录 + ClockInVo onWorkRecord = clockInList.stream().filter(v -> v.getRuleId().equals(rule.getId()) && v.getClockInType().equals(ConstantUtil.ON_WORK)) + .findFirst().orElse(null); + // 设置下班打卡记录 + ClockInVo offWorkRecord = clockInList.stream().filter(v -> v.getRuleId().equals(rule.getId()) && v.getClockInType().equals(ConstantUtil.OFF_WORK)) + .findFirst().orElse(null); + // 设置归属日期, 设置能否补卡 + setCouldRepairRecord(rule.getDay(), rule, onWorkRecord, groupRule); + setCouldRepairRecord(rule.getDay(), rule, offWorkRecord, groupRule); + if (null != onWorkRecord) { + groupInfo.getClockInList().add(new DailyClockInVo(onWorkRecord.getId(), onWorkRecord.getClockInKind(), onWorkRecord.getClockInId())); + if (StringUtils.isNotEmpty(onWorkRecord.getAbsenceLeader())) { + onWorkRecord.setAbsenceLeaderName(getAbsenceLeaderName(userMap, onWorkRecord.getAbsenceLeader())); + } + if (!StringUtils.isEmpty(onWorkRecord.getApplyId())) { + onWorkRecord.setApprovalStatus(ConstantUtil.PASS_APPROVAL); + } + } + if (null != offWorkRecord) { + groupInfo.getClockInList().add(new DailyClockInVo(offWorkRecord.getId(), offWorkRecord.getClockInKind(), offWorkRecord.getClockInId())); + if (StringUtils.isNotEmpty(offWorkRecord.getAbsenceLeader())) { + offWorkRecord.setAbsenceLeaderName(getAbsenceLeaderName(userMap, offWorkRecord.getAbsenceLeader())); + } + if (!StringUtils.isEmpty(offWorkRecord.getApplyId())) { + offWorkRecord.setApprovalStatus(ConstantUtil.PASS_APPROVAL); + } + } + rule.setOnWorkInfoVo(onWorkRecord); + rule.setOffWorkInfoVo(offWorkRecord); + } + + private String getAbsenceLeaderName(Map userMap, String absenceLeader) { + + // 查询变更人名称 + UserBoundInfoVO userBound = userMap.get(absenceLeader); + if (null == userBound) { + ActionResult usersBoundResult = v2UserApi.getUsersBound(absenceLeader, null); + if (200 == usersBoundResult.getCode() && null != usersBoundResult.getData()) { + userBound = usersBoundResult.getData(); + } else { + userBound = new UserBoundInfoVO(); + } + userMap.put(absenceLeader, userBound); + } + return userBound.getUserName(); + } + + private void setCouldRepairRecord(Date day, AttendanceRuleVo rule, ClockInVo workRecord, GroupRuleVo groupRule) { + if (null == workRecord) { + return; + } + // 设置归属日期 + workRecord.setDay(day); + // 设置能否补卡 按月初1号 - 月末最后一天 统计 + try { + AttendanceClockInResult clockInResult = new AttendanceClockInResult(); + clockInResult.setUserId(workRecord.getUserId()); + clockInResult.setRuleId(workRecord.getRuleId()); + clockInResult.setClockInStatus(workRecord.getClockInStatus()); + clockInResult.setAbsence(workRecord.getAbsence()); + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable()) || getOutsideCheck(rule, workRecord.getClockInType())) { + // 外出和出差无需补卡 + workRecord.setCouldRepair(ConstantUtil.NUM_FALSE); + } else { + MutablePair datePair = couldRepairRecord(day, clockInResult, workRecord.getApprovalStatus(), groupRule); + if (DateDetail.checkTimeBetween(workRecord.getDay(), datePair.getLeft(), datePair.getRight())) { + workRecord.setCouldRepair(ConstantUtil.NUM_TRUE); + } else { + workRecord.setCouldRepair(ConstantUtil.NUM_FALSE); + } + } + } catch (Exception e) { + workRecord.setCouldRepair(ConstantUtil.NUM_FALSE); + } + } + + public MutablePair couldRepairRecord(Date day, AttendanceClockInResult result, Integer approvalStatus, GroupRuleVo groupRule) throws Exception { + + if (null == groupRule.getPatchClockStatus() || groupRule.getPatchClockStatus().equals(ConstantUtil.NUM_FALSE)) { + throw new Exception("暂未开启允许补卡设置"); + } + if (checkAbsence(result.getRuleId(), result.getUserId())) { + throw new Exception("旷工无法补卡"); + } + if (result.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue())) { + throw new Exception("正常打卡无需补卡"); + } + approvalStatus = null == approvalStatus ? 0 : approvalStatus; + if (!approvalStatus.equals(ConstantUtil.PASS_TRUE)) { + throw new Exception("审批未通过的卡无法补卡"); + } + // 查询申请补卡记录 + AttendanceRepair repair; + int applyCount = 0; + if (StringUtils.isNotEmpty(result.getApplyId())) { + ApplyAttendanceRepair applyAttendanceRepair = applyAttendanceRepairMapper.selectById(result.getApplyId()); + String applyDate = DateDetail.getDate2Str(applyAttendanceRepair.getApplyDate(), DateDetail.DF); + repair = attendanceRepairService.getAttendanceRepairByDate(applyDate, result.getUserId(), groupRule.getSelfGroupId()); + } else { + // 查询补卡刷新次数 + repair = attendanceRepairService.getAttendanceRepair(result.getUserId(), groupRule.getSelfGroupId()); + if (null != repair) { + // 统计当月已补卡次数 + String beginDate = String.format("%d-%02d-%02d", repair.getGenerateYear(), repair.getGenerateMonth(), repair.getGenerateDay()); + String endDate = DateDetail.getDate2Str(repair.getExpireDate(), DateDetail.DF); + // 审批id不为空 并且 approvalStatus=1 说明是通过补卡审批进来的, 不需要将审批中的记录加到已审批次数上 + applyCount = attendanceClockInResultMapper.selectRepairApplyCount(result.getUserId(), groupRule.getSelfGroupId(), beginDate, endDate); + } else { + // 当月补卡次数未生成 + DateDetail dateDetail = new DateDetail(new Date()); + int year = dateDetail.getYear(); + int month = dateDetail.getMonth(); + int curDay = dateDetail.getDay(); + log.error("当月补卡次数未生成 -> 月份: {}, user: {}, groupId: {}", dateDetail.getMonth(), result.getUserId(), groupRule.getSelfGroupId()); + Date expireDate = dateDetail.changeMonthAndDate(dateDetail.getCurrentDate(), 1, groupRule.getPatchRefreshDay()); + repair = new AttendanceRepair(groupRule.getSelfGroupId(), result.getUserId(), year, month, curDay, + groupRule.getPatchClockNumber(), expireDate); + attendanceRepairService.save(repair); + } + } + if (null == repair) { + throw new HandleException("未配置补卡次数刷新日期,user: " + result.getUserId() + ", groupId: " + groupRule.getSelfGroupId()); + } + if (repair.getRepairNum() - applyCount <= 0) { + throw new QueryException("当前暂无补卡次数"); + } + MutablePair> queryPair = getQueryPair(groupRule.getPatchType()); + boolean flag = result.getAbsence().equals(ConstantUtil.NUM_TRUE) && queryPair.getLeft().equals(ConstantUtil.NUM_TRUE); + if (queryPair.getRight().contains(result.getClockInStatus())) { + flag = !ClockInStatusEnum.NO_CLOCK.getValue().equals(result.getClockInStatus()) || !result.getAbsence().equals(ConstantUtil.NUM_TRUE) || !queryPair.getLeft().equals(ConstantUtil.NUM_FALSE); + } + if (!flag) { + throw new Exception("当前考勤状态暂不允许补卡"); + } + DateDetail dateDetail = new DateDetail(); + Date queryEndDate = DateDetail.endOfDay(dateDetail.getCurrentDate()); + Date queryBeginDate = DateDetail.beginOfDay(dateDetail.addNum(dateDetail.getCurrentDate(), -groupRule.getPatchCycleDay(), Calendar.DATE)); + return MutablePair.of(queryBeginDate, queryEndDate); + } + + /** + * 判断是否旷工 + * + * @param ruleId 出勤规则 + * @param userId 用户id + * @return boolean + */ + private boolean checkAbsence(String ruleId, String userId) { + + int num = attendanceClockInResultMapper.countAbsence(ruleId, userId); + return num == 2; + } + + public GroupInfoVo getGroupInfoByDate(Date today, UserInfo userInfo) throws QueryException { + + customTenantUtil.checkOutTenant(userInfo.getTenantId()); + // 查询考勤组用户信息 + List groupUserList = getMyGroupList(userInfo.getUserId()); + // 自己的考勤组 + AttendanceGroupUser selfGroup = groupUserList.stream().filter(v -> v.getType().equals(1)).findFirst().orElse(null); + if (selfGroup == null) { + throw new QueryException("当前用户暂无考勤组"); + } + // 借调的考勤组 + List borrowGroupList = groupUserList.stream().filter(v -> v.getType().equals(2)).collect(Collectors.toList()); + // 判断有无借调, 无借调, 取当前考勤组, 有借调, 根据timeJson判断用户当前是否处于借调时间内, 在, 取借调考勤组 + String groupId = selfGroup.getGroupId(); + int selfGroupInt = 1; + if (!borrowGroupList.isEmpty()) { + firstLoop: + for (AttendanceGroupUser group : borrowGroupList) { + if (StringUtil.isEmpty(group.getTimeJson())) { + continue; + } + JSONArray jsonArray = new JSONArray(group.getTimeJson()); + List list = jsonArray.toList(JSONObject.class); + for (JSONObject json : list) { + String startTimeStr = json.get("startTime").toString(); + String endTimeStr = json.get("endTime").toString(); + boolean b = DateDetail.checkTimeBetween(today, DateDetail.getStr2DateTime(startTimeStr), DateDetail.getStr2DateTime(endTimeStr)); + if (b) { + groupId = group.getGroupId(); + selfGroupInt = 0; + break firstLoop; + } + } + } + } + return getGroupInfo(today, groupId, selfGroupInt, userInfo); + } + + /** + * 用户一个月内所在的考勤组 + * + * @param userId 用户id + * @param monthBegin 月初 + * @param monthEnd 月末 + * @return java.util.List + */ + private List getMyGroupByMonth(String userId, Date monthBegin, Date monthEnd) throws QueryException { + + List groupIds = new ArrayList<>(); + // 查询考勤组用户信息 + List groupUserList = getMyGroupList(userId); + // 自己的考勤组 + AttendanceGroupUser selfGroup = groupUserList.stream().filter(v -> v.getType().equals(1)).findFirst().orElse(null); + assert selfGroup != null; + groupIds.add(selfGroup.getGroupId()); + // 借调的考勤组 + List borrowGroupList = groupUserList.stream().filter(v -> v.getType().equals(2)).collect(Collectors.toList()); + if (!borrowGroupList.isEmpty()) { + for (AttendanceGroupUser group : borrowGroupList) { + if (StringUtil.isEmpty(group.getTimeJson())) { + continue; + } + JSONArray jsonArray = new JSONArray(group.getTimeJson()); + List list = jsonArray.toList(JSONObject.class); + for (JSONObject json : list) { + String startTimeStr = json.get("startTime").toString(); + String endTimeStr = json.get("endTime").toString(); + if (monthBegin.before(Objects.requireNonNull(DateDetail.getStr2DateTime(endTimeStr))) && Objects.requireNonNull(DateDetail.getStr2DateTime(startTimeStr)).before(monthEnd)) { + groupIds.add(group.getGroupId()); + break; + } + } + } + } + return groupIds; + } + + private List getMyGroupList(String userId) throws QueryException { + + LambdaQueryWrapper groupUserQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceGroupUser::getUserId, userId) + .eq(AttendanceGroupUser::getDeleteMark, 0); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + if (groupUserList.isEmpty()) { + throw new QueryException("当前用户暂无考勤组"); + } + return groupUserList; + } + + @Override + public GroupInfoVo getGroupInfo(Date today, String groupId, int selfGroupInt, UserInfo userInfo) throws QueryException { + return getGroupInfo(today, groupId, selfGroupInt, userInfo, ConstantUtil.NUM_TRUE); + } + + @Override + public GroupInfoVo getGroupInfo(Date today, String groupId, int selfGroupInt, UserInfo userInfo, Integer isMainInfo) throws QueryException { + + // 查询考勤组信息 + LambdaQueryWrapper groupQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceGroup::getId, groupId) + .eq(AttendanceGroup::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceGroup group = attendanceGroupMapper.selectOne(groupQueryWrapper); + if (null == group) { + throw new QueryException("当前用户暂无考勤组"); + } + // 考勤组用户绑定关系 + AttendanceGroupUser groupUser = getGroupUser(userInfo.getUserId(), groupId, isMainInfo); + if (null == groupUser) { + throw new QueryException("已不是考勤组成员"); + } + GroupInfoVo groupInfo = new GroupInfoVo(today, group.getId(), group.getGroupName(), group.getLockedDate(), selfGroupInt, userInfo.getUserId()); + Map> locationMap = attendanceLocationSettingService.getEnableLocationSetting(Stream.of(group.getId()).collect(Collectors.toList())); + List locationList = new ArrayList<>(); + if (!locationMap.isEmpty()) { + locationList = locationMap.get(group.getId()); + } + if (null != locationList && !locationList.isEmpty()) { + ConcurrentMap> map = locationList.stream().filter(v -> v.getEnable().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.groupingByConcurrent(AttendanceLocationSetting::getType)); + if (!map.isEmpty()) { + map.keySet().forEach(key -> { + List methodList = map.get(key); + setClockInMethod(groupInfo, key, methodList); + }); + } + } + // 查询考勤组规则 + GroupRuleVo groupRule = getGroupRule(groupId, userInfo.getUserId()); + groupInfo.setGroupRule(groupRule); + return groupInfo; + } + + private AttendanceGroupUser getGroupUser(String userId, String groupId, Integer isMainInfo) { + LambdaQueryWrapper groupUserQuery = new LambdaQueryWrapper() + .eq(AttendanceGroupUser::getUserId, userId) + .eq(AttendanceGroupUser::getGroupId, groupId) + .eq(!Objects.equals(isMainInfo, ConstantUtil.NUM_WITHDRAW), AttendanceGroupUser::getDeleteMark, ConstantUtil.NUM_FALSE) + .last("limit 1"); + return attendanceGroupUserMapper.selectOne(groupUserQuery); + } + + private AttendanceGroupUser getGroupUser(String userId, String groupId) { + return getGroupUser(userId, groupId, ConstantUtil.NUM_TRUE); + } + + @Override + public GroupRuleVo getGroupRule(String groupId, String userId) { + + Map settingMap = attendanceBaseSettingService.getEnableBaseSetting(List.of(groupId)); + if (null != settingMap && !settingMap.isEmpty()) { + AttendanceBaseSetting setting = settingMap.get(groupId); + if (null != setting) { + // 查询考勤组锁定时间 + // Date lockDate = attendanceGroupMapper.getGroupLockDate(groupId); + GroupRuleVo bean = JsonUtil.getJsonToBean(setting, GroupRuleVo.class); + // 处理该用户在考勤中是否是允许外勤人员 + try { + bean.setFieldClockStatus(attendanceBaseSettingService.getFieldClockStatusSelf(setting) ? 1 : 0); + } catch (Exception e) { + bean.setFieldClockStatus(ConstantUtil.NUM_FALSE); + } + bean.setFace(ConstantUtil.NUM_TRUE == setting.getFace()); + bean.setSelfGroupId(groupId); + return bean; + } + } + return null; + } + + private void setClockInMethod(GroupInfoVo groupInfo, Integer key, List methodList) { + if (null == methodList) { + methodList = new ArrayList<>(); + } + List methodLocationList = JsonUtil.getJsonToList(methodList, MethodVo.class); + switch (key) { + case ConstantUtil.DEVICE_PLACE: + groupInfo.getClockInMethodVo().getLocationList().addAll(methodLocationList); + break; + case ConstantUtil.DEVICE_MACHINE: + groupInfo.getClockInMethodVo().getDeviceList().addAll(methodLocationList); + break; + case ConstantUtil.DEVICE_WIFI: + groupInfo.getClockInMethodVo().getWifiList().addAll(methodLocationList); + break; + default: + break; + } + } + + private List getDailyRuleList(Date today, UserInfo userInfo) { + + List list = new ArrayList<>(); + DateDetail dateDetail = new DateDetail(today); + dateDetail.getYesterday(); + // 查询昨天的最后一个出勤规则 + FtbAttendanceDailyRule yesterdayRule = attendanceDailyRuleMapper.getLastDailyRule(dateDetail.getCurrentDate(), userInfo.getUserId()); + if (null != yesterdayRule) { + if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + if (DateDetail.checkSameDay(today, yesterdayRule.getOutLackPoint())) { + // 外出或出差, 且无班次则设置班次类型为-1 + yesterdayRule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode()); + yesterdayRule.setInStepOutType(ConstantUtil.NUM_TRUE); + yesterdayRule.setOutStepOutType(ConstantUtil.NUM_TRUE); + list.add(yesterdayRule); + } + } + // 休, 无 跳过 + if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + // 加班, 添加到今日出勤中 + list.add(yesterdayRule); + } + if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())) { + // 普班, 判断下班结束时间是否为今天, 是, 添加到今日出勤中 + if (DateDetail.checkSameDay(today, yesterdayRule.getOutLackPoint())) { + list.add(yesterdayRule); + } + } + if (yesterdayRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())) { + // 假, 判断请假结束时间(下班时间)是否是今天, 是, 添加到今日出勤中 + if (DateDetail.checkSameDay(today, yesterdayRule.getOutPoint())) { + list.add(yesterdayRule); + } + } + } + List currentDailyRuleList = getCurrentDailyRuleList(today, userInfo); + currentDailyRuleList.forEach(v -> { + if (v.getAttendanceType().equals(AttendanceTypeEnum.HOLIDAYS.getCode()) || v.getAttendanceType().equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) + || v.getAttendanceType().equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())) { + v.setAttendanceType(AttendanceTypeEnum.REST.getCode()); + } + }); + long count = currentDailyRuleList.stream().filter(v -> v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).count(); + if (count > 0 && currentDailyRuleList.size() > 1) { + // 半天休问题处理 + currentDailyRuleList.removeIf(v -> v.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())); + } + // 今日出勤规则 + list.addAll(currentDailyRuleList); + return list; + } + + private List getCurrentDailyRuleList(Date today, UserInfo userInfo) { + + // 今日出勤规则 + LambdaQueryWrapper dailyRuleQueryWrapper = new LambdaQueryWrapper<>(); + if (null != userInfo) { + dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getUserId, userInfo.getUserId()); + } + dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getDay, DateDetail.getDate2Str(today, DateDetail.DF)) + .in(FtbAttendanceDailyRule::getApplyViewEnable, List.of(1, 3, 9, 10)) + .eq(FtbAttendanceDailyRule::getDeleteMark, ConstantUtil.NUM_FALSE) + .orderByAsc(FtbAttendanceDailyRule::getInPoint); + List dbList = attendanceDailyRuleMapper.selectList(dailyRuleQueryWrapper); + if (!dbList.isEmpty()) { + // 排除考勤组被删除的出勤规则 + List groupIdList = dbList.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList()); + List delIds = attendanceGroupMapper.selectDelIds(groupIdList); + if (!delIds.isEmpty()) { + dbList.removeIf(v -> delIds.contains(v.getGroupId())); + } + dbList.forEach(rule -> { + // 如果今日外出或出差, 且无排班, 设置班次类型为-1 + if (rule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || rule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode())) { + rule.setInStepOutType(ConstantUtil.NUM_TRUE); + rule.setOutStepOutType(ConstantUtil.NUM_TRUE); + rule.setAttendanceType(AttendanceTypeEnum.DEFAULT.getCode()); + } + }); + } + return new ArrayList<>(dbList); + } + + @Override + public List getCurrentDailyRuleListOfWithdraw(Date today, UserInfo userInfo) { + + // 今日出勤规则 + LambdaQueryWrapper dailyRuleQueryWrapper = new LambdaQueryWrapper<>(); + if (null != userInfo) { + dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getUserId, userInfo.getUserId()); + } + dailyRuleQueryWrapper.eq(FtbAttendanceDailyRule::getDay, DateDetail.getDate2Str(today, DateDetail.DF)) + .in(FtbAttendanceDailyRule::getApplyViewEnable, ConstantUtil.NUM_TRUE, ConstantUtil.NUM_WITHDRAW, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()) + .eq(FtbAttendanceDailyRule::getDeleteMark, ConstantUtil.NUM_FALSE) + .orderByAsc(FtbAttendanceDailyRule::getSchedulesType) + .orderByAsc(FtbAttendanceDailyRule::getInPoint); + List dbList = attendanceDailyRuleMapper.selectList(dailyRuleQueryWrapper); + if (!dbList.isEmpty()) { + // 排除考勤组被删除的出勤规则 + /*List groupIdList = dbList.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList()); + List delIds = attendanceGroupMapper.selectDelIds(groupIdList); + if (!delIds.isEmpty()) { + dbList.removeIf(v -> delIds.contains(v.getGroupId())); + }*/ + dbList.forEach(rule -> { + if (null == rule.getInUnbounded()) { + rule.setInUnbounded(0); + } + if (null == rule.getOutUnbounded()) { + rule.setOutUnbounded(0); + } + }); + } + return new ArrayList<>(dbList); + } + + @Override + public List getCurrentDailyRuleListOfDay(DateRangeDto dateRangeDto, List usersByGroupVos) { + // 时间段内出勤规则 + LambdaQueryWrapper dailyRuleQueryWrapper = new LambdaQueryWrapper<>(); + dailyRuleQueryWrapper.between(FtbAttendanceDailyRule::getDay, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()) + .in(FtbAttendanceDailyRule::getApplyViewEnable, ConstantUtil.NUM_TRUE, ConstantUtil.NUM_WITHDRAW, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()) + .in(FtbAttendanceDailyRule::getUserId, usersByGroupVos.stream().map(ClockInExportVo::getUserId).distinct().collect(Collectors.toList())) + .in(FtbAttendanceDailyRule::getGroupId, usersByGroupVos.stream().map(ClockInExportVo::getGroupId).distinct().collect(Collectors.toList())) + .eq(FtbAttendanceDailyRule::getDeleteMark, ConstantUtil.NUM_FALSE) + .orderByAsc(FtbAttendanceDailyRule::getInPoint); + return attendanceDailyRuleMapper.selectList(dailyRuleQueryWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCloudAlbumServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCloudAlbumServiceImpl.java new file mode 100644 index 0000000..a5334a9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCloudAlbumServiceImpl.java @@ -0,0 +1,75 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.mapper.AttendanceCloudAlbumMapper; +import jnpf.attendance.service.AttendanceCloudAlbumService; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceCloudAlbum; +import jnpf.enums.attendance.CloudAlbumTypeEnum; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.attendance.OcsWatermarkUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @Description: 考勤云相册表 + * @Author: shiTou(他是小石头) + * @Date: 2024-10-30 10:26 + */ +@Slf4j +@Service +public class AttendanceCloudAlbumServiceImpl extends SuperServiceImpl implements AttendanceCloudAlbumService { + @Override + public List getDataList() { + List cloudAlbumList = this.lambdaQuery().select(AttendanceCloudAlbum::getPicUrl) + .eq(AttendanceCloudAlbum::getUserId, UserProvider.getUser().getUserId()) + .ge(AttendanceCloudAlbum::getDeleteTime, new Date()) + .orderByDesc(AttendanceCloudAlbum::getCreatorTime) + .eq(AttendanceCloudAlbum::getDeleteMark, Boolean.FALSE) + .list(); + return CollUtil.isNotEmpty(cloudAlbumList) ? cloudAlbumList.stream().map(AttendanceCloudAlbum::getPicUrl).collect(Collectors.toList()) : CollUtil.newArrayList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchSave(List picUrlList) { + List cloudAlbumList = CollUtil.newArrayList(); + if (CollUtil.isEmpty(picUrlList)) { + return; + } + Date newDate = new Date(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(newDate); + calendar.add(Calendar.DAY_OF_MONTH, 7); + Date dateSevenDaysAfter = calendar.getTime(); + cloudAlbumList.addAll(picUrlList.stream().map(item -> { + AttendanceCloudAlbum cloudAlbum = new AttendanceCloudAlbum(); + cloudAlbum.setUserId(UserProvider.getUser().getUserId()); + cloudAlbum.setType(CloudAlbumTypeEnum.FIELD_PERSONNEL.getCode()); + cloudAlbum.setPicUrl(item); + cloudAlbum.setCreatorTime(newDate); + cloudAlbum.setDeleteTime(dateSevenDaysAfter); + return cloudAlbum; + }).collect(Collectors.toList())); + if (CollUtil.isNotEmpty(cloudAlbumList)) { + this.saveBatch(cloudAlbumList); + } + } + + @Override + public String getUserWatermark() { + UserInfo userInfo = UserProvider.getUser(); + return OcsWatermarkUtils.watermarkByStr(Objects.isNull(userInfo) ? + null : StringUtil.isNotEmpty(userInfo.getUserName()) ? userInfo.getUserName() : + StringUtil.isNotEmpty(userInfo.getNickName()) ? userInfo.getNickName() : null); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmDetailsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmDetailsServiceImpl.java new file mode 100644 index 0000000..3d57115 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmDetailsServiceImpl.java @@ -0,0 +1,20 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceConfirmDetailsMapper; +import jnpf.attendance.service.AttendanceConfirmDetailsService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceConfirmDetails; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 考勤确认详情 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +@Slf4j +@Service +public class AttendanceConfirmDetailsServiceImpl extends SuperServiceImpl implements AttendanceConfirmDetailsService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmServiceImpl.java new file mode 100644 index 0000000..b0cd956 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmServiceImpl.java @@ -0,0 +1,550 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.excel.util.DateUtils; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceConfirmMapper; +import jnpf.attendance.service.*; +import jnpf.base.service.SuperServiceImpl; +import jnpf.emnus.MiniAppEnum; +import jnpf.emnus.WorkStatusEnum; +import jnpf.emnus.WorkTypeEnum; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceConfirm; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.model.attendance.dto.ConfirmPageListDto; +import jnpf.model.attendance.dto.ConfirmStatisticsDto; +import jnpf.model.attendance.dto.GroupFilterDto; +import jnpf.model.attendance.dto.StatisticsDataQueryDto; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.common.DateRangeDto; +import jnpf.model.thousandsfaces.TodayWorkVo; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.RandomUtil; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.attendance.DayStatisticsUtils; +import jnpf.util.context.ThreadContext; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.Resource; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +/** + * 考勤确认 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +@Slf4j +@Service +public class AttendanceConfirmServiceImpl extends SuperServiceImpl implements AttendanceConfirmService { + @Resource + private UserAntifreeze userAntifreeze; + @Resource + private AttendanceUserService userService; + @Resource(name = "ioIntensiveThreadPool") + private ThreadPoolExecutor threadPoolExecutor; + @Resource + private TransactionTemplate transactionTemplate; + @Resource + private AttendanceConfirmService confirmService; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceDayStatisticsService dayStatisticsService; + @Resource + private AttendanceCustomizeTableService customizeTableService; + @Resource + private AttendanceConfirmSettingService confirmSettingService; + + @Override + public String getConfirmMonth() { + AttendanceConfirm confirm = this.lambdaQuery().orderByDesc(AttendanceConfirm::getYear) + .orderByDesc(AttendanceConfirm::getMonth) + .last("limit 1") + .one(); + if (Objects.isNull(confirm)) { + return DateUtil.format(new Date(), DatePattern.NORM_MONTH_PATTERN); + } + YearMonth yearMonth = YearMonth.of(confirm.getYear(), confirm.getMonth()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + return yearMonth.format(formatter); + } + + /** + * 获取有权限的用户列表 + * + * @param filterList 筛选条件 + * @param workStatus 用户状态 + * @return 用户列表 + */ + private List getUserIdArr(List filterList, String workStatus) { + List groupIds = filterList.stream() + .map(GroupFilterDto::getGroupId) + .distinct() + .collect(Collectors.toList()); + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(workStatus); + Assert.notNull(workStatusEnum, "工作状态不正确"); + List workStatusEnums = DayStatisticsUtils.getWorkStatusEnumList(Objects.requireNonNull(workStatusEnum)); + List userBoundVoList = attendanceUserService.batchGetUserBoundVO(groupIds, workStatusEnums); + return CollUtil.isNotEmpty(userBoundVoList) ? userBoundVoList : CollUtil.newArrayList(); + } + + @Override + public ConfirmStatisticsVo getStatistics(ConfirmStatisticsDto dto) { + DateRangeDto dateRangeDto = new DateRangeDto(dto.getMonth(), Boolean.TRUE); + StatisticsDataQueryDto queryDto = new StatisticsDataQueryDto(); + BeanUtils.copyProperties(dto, queryDto); + queryDto.setStartDate(dateRangeDto.getStartDate()); + queryDto.setEndDate(dateRangeDto.getEndDate()); + Date newDate = new Date(); + List groupIds = dto.getFilterList().stream() + .map(GroupFilterDto::getGroupId) + .distinct() + .collect(Collectors.toList()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(dto.getFilterList(), dto.getWorkStatus()); + if (StringUtil.isNotEmpty(dto.getKeyword())) { + userBoundVoList = userBoundVoList.stream().filter(item -> + (StringUtil.isNotEmpty(item.getPhone()) && item.getPhone().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getName()) && item.getName().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getOrganizeName()) && item.getOrganizeName().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getPositionName()) && item.getPositionName().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getGradeName()) && item.getGradeName().contains(dto.getKeyword()))) + .collect(Collectors.toList()); + } + if (CollUtil.isEmpty(userBoundVoList)) { + return new ConfirmStatisticsVo(); + } + List groupUserList = userService.getAttendanceGroupUsersOfSecondment(newDate, newDate, + userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()), groupIds); + List userIdList = CollUtil.isNotEmpty(groupUserList) ? groupUserList.stream() + .map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(userIdList)) { + return new ConfirmStatisticsVo(); + } + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + return this.baseMapper.getStatistics(userIdList, yearMonth.getYear(), yearMonth.getMonthValue(), null); + } + + @Override + public PageInfo getPageList(ConfirmPageListDto dto) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) dto.getPageSize()); + pageInfo.setPageNum((int) dto.getCurrentPage()); + Date newDate = new Date(); + List groupIds = dto.getFilterList().stream() + .map(GroupFilterDto::getGroupId) + .distinct() + .collect(Collectors.toList()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(dto.getFilterList(), dto.getWorkStatus()); + if (CollUtil.isEmpty(userBoundVoList)) { + return pageInfo; + } + Map userBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + if (StringUtil.isNotEmpty(dto.getKeyword())) { + userBoundVoList = userBoundVoList.stream().filter(item -> + (StringUtil.isNotEmpty(item.getPhone()) && item.getPhone().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getName()) && item.getName().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getOrganizeName()) && item.getOrganizeName().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getPositionName()) && item.getPositionName().contains(dto.getKeyword())) || + (StringUtil.isNotEmpty(item.getGradeName()) && item.getGradeName().contains(dto.getKeyword()))) + .collect(Collectors.toList()); + } + List groupUserList = userService.getAttendanceGroupUsersOfSecondment(newDate, newDate, + userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()), groupIds); + List userIdList = CollUtil.isNotEmpty(groupUserList) ? groupUserList.stream() + .map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(userIdList)) { + log.error("未查询到考勤组用户数据"); + return pageInfo; + } + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + PageHelper.startPage(Math.toIntExact(dto.getCurrentPage()), Math.toIntExact(dto.getPageSize())); + List pageListVoList = this.baseMapper.getPageList(userIdList, yearMonth.getYear(), yearMonth.getMonthValue(), dto.getType()); + PageInfo pageInfo1 = new PageInfo<>(statisticsAssemble(pageListVoList, userBoundVoMap)); + pageInfo1.setTotal(new PageInfo<>(pageListVoList).getTotal()); + pageInfo1.setPageSize((int) dto.getPageSize()); + return pageInfo1; + } + + /** + * 封装考勤确认统计数据 + * + * @param statisticsQueryVoList 考勤确认数据 + * @param staffRosterMap 员工花名册数据 + * @return 考勤确认统计数据 + */ + private List statisticsAssemble(List statisticsQueryVoList, Map staffRosterMap) { + return statisticsQueryVoList.stream().map(item -> { + ConfirmPageListVo statistics = ConfirmPageListVo.builder().build(); + BeanUtils.copyProperties(item, statistics); + if (staffRosterMap.containsKey(item.getUserId())) { + UserBoundVO staffRosterDto = staffRosterMap.get(item.getUserId()); + statistics.setDeptName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + statistics.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + statistics.setUserName(staffRosterDto.getName()); + statistics.setPhone(staffRosterDto.getPhone()); + statistics.setWorkStatus(staffRosterDto.getWorkStatusEnums().getCode()); + } + return statistics; + }).collect(Collectors.toList()); + } + + @Override + public ConfirmDetailsVo getConfirmDetails(String id) { + AttendanceConfirm attendanceConfirm = this.getById(id); + ServiceException.notNull(attendanceConfirm, "未查询到考勤确认数据"); + ConfirmDetailsVo confirmDetailsVo = ConfirmDetailsVo.builder() + .id(attendanceConfirm.getId()) + .status(attendanceConfirm.getStatus()) + .slippageResult(attendanceConfirm.getSlippageResult()) + .tipsStatus(attendanceConfirm.getTipsStatus()) + .confirmStatus(new Date().compareTo(attendanceConfirm.getStartTime()) >= 0) + .deadline(DateUtils.format(attendanceConfirm.getEndTime(), "MM月dd日")) + .build(); + //获取考勤确认数据 + getConfirmData(attendanceConfirm, confirmDetailsVo); + return confirmDetailsVo; + } + + @Override + public ConfirmDetailsVo getAppConfirmDetails(String id) { + LambdaQueryChainWrapper lambdaQueryChainWrapper = this.lambdaQuery() + .orderByDesc(AttendanceConfirm::getCreateTime) + .eq(AttendanceConfirm::getUserId, UserProvider.getUser().getUserId()); + if (StringUtil.isNotEmpty(id)) { + lambdaQueryChainWrapper.eq(AttendanceConfirm::getId, id); + } + lambdaQueryChainWrapper.last("limit 1"); + AttendanceConfirm attendanceConfirm = lambdaQueryChainWrapper.one(); + ServiceException.notNull(attendanceConfirm, "未查询到考勤确认数据"); + ConfirmDetailsVo confirmDetailsVo = ConfirmDetailsVo.builder() + .id(attendanceConfirm.getId()) + .status(attendanceConfirm.getStatus()) + .slippageResult(attendanceConfirm.getSlippageResult()) + .tipsStatus(attendanceConfirm.getTipsStatus()) + .confirmStatus(new Date().compareTo(attendanceConfirm.getStartTime()) >= 0) + .deadline(DateUtils.format(attendanceConfirm.getEndTime(), "MM月dd日")) + .build(); + //获取考勤确认数据 + getConfirmData(attendanceConfirm, confirmDetailsVo); + return confirmDetailsVo; + } + + @Override + @Transactional + public boolean autoCreateConfirm() { + ConfirmSettingInfoVo confirmSetting = confirmSettingService.getConfirmSetting(Boolean.TRUE); + Integer allowConfirm = confirmSetting.getAllowConfirm(); + Date date = DateUtil.beginOfDay(new Date()); + int day = DateUtil.dayOfMonth(date); + if (!Objects.equals(allowConfirm, day)) { + return false; + } + int month = DateUtil.month(date) + 1; + int year = DateUtil.year(date); + Date expectedTime = DateUtil.offsetDay(date, confirmSetting.getConfirmDuration()); + date = DateUtil.offsetMonth(date, -1); + if (Objects.equals(confirmSetting.getConfirmMonth(), 2)) { + month = DateUtil.month(date) + 1; + year = DateUtil.year(date); + } + //异步生成考勤确认数据 + int finalYear = year; + int finalMonth = month; + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) { + try { + confirmService.createConfirm(finalYear, finalMonth, expectedTime, DateUtil.beginOfDay(new Date()), confirmSetting.getSlippageResult()); + } catch (Exception e) { + log.error("异步生成考勤确认数据失败,事务回滚", e); + status.setRollbackOnly(); + throw e; + } + } + }); + }), threadPoolExecutor) + .exceptionally(ex -> { + log.error("生成考勤确认数据异常", ex); + return null; + }); + return Boolean.TRUE; + } + + @Override + @Transactional + public void createConfirm(int year, int month, Date expectedTime, Date startTime, Integer slippageResult) { + lambdaUpdate().eq(AttendanceConfirm::getMonth, month) + .eq(AttendanceConfirm::getYear, year) + .remove(); + Date now = new Date(); + YearMonth yearMonth = YearMonth.of(year, month); + Date startDate = Date.from(yearMonth.atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant()); + Date endDate = Date.from(yearMonth.atEndOfMonth().atStartOfDay(ZoneId.systemDefault()).toInstant()); + List attendanceGroupUsers = userService.getAttendanceGroupUsersOfSecondment(startDate,endDate,null,null); Map userGroupMap = attendanceGroupUsers.stream().collect(Collectors.toMap(AttendanceGroupUser::getUserId, a -> a, (k1, k2) -> k1)); + List collect = new ArrayList<>(); + userGroupMap.forEach((key, value) -> { + AttendanceConfirm confirm = AttendanceConfirm.builder() + .id(RandomUtil.uuId()) + .userId(value.getUserId()) + .year(year) + .month(month) + .status(0) + .slippageResult(0) + .startTime(startTime) + .endTime(expectedTime) + .createTime(now) + .build(); + //判断是否逾期 + if (now.compareTo(expectedTime) >= 0) { + confirm.setSlippageResult(slippageResult); + if (slippageResult.equals(1)) { + confirm.setStatus(2); + confirm.setUpdateTime(new Date()); + } + } + collect.add(confirm); + }); + if (CollUtil.isNotEmpty(collect)) { + List userIds = collect.stream().map(AttendanceConfirm::getUserId).collect(Collectors.toList()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + // 获取自定义表单数据 + List customizeTableVos = getAttendanceCustomizeTableVos(); + processJsonData(yearMonth.format(formatter), userIds, customizeTableVos, collect); + saveBatch(collect); + } + } + + /** + * 处理自定义表单数据 + * + * @param month 月份 + * @param userIds 用户Id + * @param customizeTableVos 自定义表单 + * @param collect 考勤确认数据 + */ + private void processJsonData(String month, List userIds, List customizeTableVos, List collect) { + // 实时查询自定义请假类型 + List infoByMonth = this.dayStatisticsService.getConfirmDetailsInfoByMonth(userIds, month); + Map confirmDetailsInfoMap = infoByMonth.stream() + .collect(Collectors.toMap(MonthConfirmStatisticsListVo::getUserId, + item -> BeanUtil.toBean(item, ConfirmDetailsInfoNew.class), (k1, k2) -> k1)); + collect.forEach(confirm -> { + ConfirmDetailsInfoNew confirmDetailsInfo = confirmDetailsInfoMap.getOrDefault(confirm.getUserId(), new ConfirmDetailsInfoNew()); + confirmDetailsInfo.setMonth(confirm.getYear() + "年" + confirm.getMonth() + "月"); + Map confirmDetailsMap = DayStatisticsUtils.convertToMap(confirmDetailsInfo); + List detailsList = new ArrayList<>(); + customizeTableVos.forEach(item -> { + ConfirmDetails details = ConfirmDetails.builder() + .fieldName(item.getFieldName()) + .customLeave(item.getCustomLeave()) + .value(confirmDetailsMap.getOrDefault(item.getField() + "&" + item.getFieldName(), null)) + .sort(item.getSort()) + .build(); + detailsList.add(details); + }); + confirm.setDetailDataJson(JSON.toJSONString(detailsList)); + }); + } + + @Override + @Transactional + public boolean confirmAutoSlippage() { + Date date = DateUtil.beginOfDay(new Date()); + List list = lambdaQuery() + .eq(AttendanceConfirm::getSlippageResult, 0) + .eq(AttendanceConfirm::getEndTime, date) + .list(); + // 实时查询自定义请假类型 + List customizeTableVos = getAttendanceCustomizeTableVos(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + Map> batcheMap = list.stream().collect(Collectors.groupingBy(a -> YearMonth.of(a.getYear(), a.getMonth()).format(formatter))); + batcheMap.forEach((month, dataList) -> { + List userIds = dataList.stream().map(AttendanceConfirm::getUserId).collect(Collectors.toList()); + // 处理json数据 + processJsonData(month, userIds, customizeTableVos, dataList); + dataList.forEach(confirm -> { + confirm.setStatus(2); + confirm.setSlippageResult(1); + confirm.setUpdateTime(new Date()); + }); + this.updateBatchById(dataList); + }); + return Boolean.TRUE; + } + + @Override + public Boolean tipsClos(String id) { + AttendanceConfirm attendanceConfirm = this.getById(id); + ServiceException.notNull(attendanceConfirm, "未查询到考勤确认数据"); + attendanceConfirm.setTipsStatus(1); + return this.updateById(attendanceConfirm); + + } + + @Override + @Transactional + public Boolean confirmDetailsSubmit(String id) { + AttendanceConfirm attendanceConfirm = this.getById(id); + ServiceException.notNull(attendanceConfirm, "未查询到考勤确认数据"); + //判断是否已逾期 + ServiceException.isTrue(attendanceConfirm.getSlippageResult().equals(0), "该考勤确认已逾期"); + attendanceConfirm.setStatus(2); + attendanceConfirm.setUpdateTime(new Date()); + // 实时查询自定义请假类型 + List customizeTableVos = getAttendanceCustomizeTableVos(); + YearMonth yearMonth = YearMonth.of(attendanceConfirm.getYear(), attendanceConfirm.getMonth()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + List attendanceConfirmList = List.of(attendanceConfirm); + // 处理json数据 + processJsonData(yearMonth.format(formatter), List.of(attendanceConfirm.getUserId()), customizeTableVos, attendanceConfirmList); + return this.updateBatchById(attendanceConfirmList); + } + + /** + * 获取自定义表单 + * + * @return 自定义表单 + */ + private List getAttendanceCustomizeTableVos() { + List customizeTableVos = customizeTableService.findList(null, 1, 2); + List removeIds = List.of("userName", "groupName", "deptName", "postName", "workStatus", "cycleList"); + customizeTableVos.removeIf(item -> removeIds.contains(item.getField())); + customizeTableVos.sort(Comparator.comparingInt(AttendanceCustomizeTableVo::getSort)); + return customizeTableVos; + } + + @Override + public Boolean look(String id) { + AttendanceConfirm attendanceConfirm = this.getById(id); + ServiceException.notNull(attendanceConfirm, "未查询到考勤确认数据"); + attendanceConfirm.setStatus(1); + this.updateById(attendanceConfirm); + return Boolean.TRUE; + } + + @Override + public List getTodayWorkConfirmList() { + List result = new ArrayList<>(); + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.eq(AttendanceConfirm::getUserId, UserProvider.getUser().getUserId()); + lambdaQueryWrapper.isNotNull(AttendanceConfirm::getMonth); + lambdaQueryWrapper.and(i -> i + .le(AttendanceConfirm::getCreateTime, DateUtil.endOfDay(new Date())) + .or() + .le(AttendanceConfirm::getUpdateTime, DateUtil.endOfDay(new Date())) + ); + List attendanceConfirmList = this.list(lambdaQueryWrapper); + //获取代办数据 + List todoList = CollUtil.isNotEmpty(attendanceConfirmList) ? attendanceConfirmList.stream().filter(item -> + item.getStatus() != 2 && item.getSlippageResult().equals(0)).collect(Collectors.toList()) : new ArrayList<>(); + //获取已逾期数据(只获取今天范围内的) + List overdueList = CollUtil.isNotEmpty(attendanceConfirmList) ? attendanceConfirmList.stream().filter(item -> null != item.getUpdateTime() && + item.getSlippageResult().equals(2) && DateUtil.isIn(item.getUpdateTime(), DateUtil.beginOfDay(new Date()), DateUtil.endOfDay(new Date()))).collect(Collectors.toList()) : new ArrayList<>(); + //获取已确认数据(只获取今天范围内的) + List completeList = CollUtil.isNotEmpty(attendanceConfirmList) ? attendanceConfirmList.stream().filter(item -> null != item.getUpdateTime() && item.getStatus().equals(2) && + DateUtil.isIn(item.getUpdateTime(), DateUtil.beginOfDay(new Date()), DateUtil.endOfDay(new Date()))).collect(Collectors.toList()) : new ArrayList<>(); + processDailyPlans(todoList, WorkStatusEnum.TO_DO, result); + processDailyPlans(overdueList, WorkStatusEnum.OVERDUE, result); + processDailyPlans(completeList, WorkStatusEnum.FINISHED, result); + return result; + } + + private void processDailyPlans(List planList, WorkStatusEnum workStatus, List result) { + if (CollUtil.isEmpty(planList)) { + return; + } + List todayWorkVos = planList.stream().map(confirm -> { + TodayWorkVo vo = new TodayWorkVo(); + vo.setWorkId(confirm.getId()); + vo.setWorkStatus(workStatus); + vo.setTitle(confirm.getMonth() + "月考勤确认"); + vo.setFinishDate(confirm.getEndTime()); + vo.setCreatorTime(confirm.getCreateTime()); + vo.setWorkType(WorkTypeEnum.ATTENDANCE); + vo.setMiniApp(MiniAppEnum.ATTENDANCE_CONFIRM); + vo.setExtraJson(JSONObject.toJSONString(confirm)); + return vo; + }).collect(Collectors.toList()); + result.addAll(todayWorkVos); + } + + /** + * 获取考勤确认数据 + * + * @param attendanceConfirm 用户考勤确认数据 + * @param confirmDetailsVo 考勤确认数据 + */ + private void getConfirmData(AttendanceConfirm attendanceConfirm, ConfirmDetailsVo confirmDetailsVo) { + Date newDate = new Date(); + List groupUserList = userService.getAttendanceGroupUsersOfSecondment(newDate, newDate, Collections.singletonList(attendanceConfirm.getUserId()), null); + if (CollUtil.isEmpty(groupUserList)) { + log.error("未查询到用户所属考勤组数据"); + } + // 查询用户信息 + List staffRosterDtoList = userAntifreeze.getInfoByIdsManyAndCopyPost(Collections.singletonList(attendanceConfirm.getUserId()), UserProvider.getUser().getTenantId()); + if (CollUtil.isEmpty(staffRosterDtoList)) { + log.error("未查询到用户花名册数据:{}", attendanceConfirm.getUserId()); + } + Map staffRosterMap = staffRosterDtoList.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, a -> a, (k1, k2) -> k1)); + if (staffRosterMap.containsKey(attendanceConfirm.getUserId())) { + BaseConfirmInfo baseInfo = BaseConfirmInfo.builder().build(); + PartUserInfoVo staffRosterDto = staffRosterMap.get(attendanceConfirm.getUserId()); + baseInfo.setDeptName(staffRosterDto.getOrganizeName()); + baseInfo.setPostName(staffRosterDto.getPositionName()); + baseInfo.setUserName(staffRosterDto.getRealName()); + baseInfo.setWorkStatus(staffRosterDto.getWorkerStatus()); + confirmDetailsVo.setBaseInfo(baseInfo); + } + // 获取考勤确认详情数据 + if (!attendanceConfirm.getStatus().equals(StatisticsEnumUtil.AttendanceConfirmStatusEnum.YQR.getCode())) { + // 实时查询自定义请假类型 + List customizeTableVos = getAttendanceCustomizeTableVos(); + YearMonth yearMonth = YearMonth.of(attendanceConfirm.getYear(), attendanceConfirm.getMonth()); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + List attendanceConfirmList = List.of(attendanceConfirm); + // 处理json数据 + processJsonData(yearMonth.format(formatter), List.of(attendanceConfirm.getUserId()), customizeTableVos, attendanceConfirmList); + } + List detailsList = JSON.parseArray(attendanceConfirm.getDetailDataJson(), ConfirmDetails.class); + if (CollUtil.isNotEmpty(detailsList)) { + detailsList.sort(Comparator.comparingInt(ConfirmDetails::getSort)); + confirmDetailsVo.setConfirmDetailsList(detailsList); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmSettingServiceImpl.java new file mode 100644 index 0000000..7ba3f01 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceConfirmSettingServiceImpl.java @@ -0,0 +1,177 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.seata.common.util.StringUtils; +import jnpf.attendance.mapper.AttendanceConfirmSettingMapper; +import jnpf.attendance.service.AttendanceConfirmService; +import jnpf.attendance.service.AttendanceConfirmSettingService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceConfirmSetting; +import jnpf.model.attendance.dto.ConfirmSettingSubmitDto; +import jnpf.model.attendance.vo.attendance.ConfirmSettingInfoVo; +import jnpf.util.FtbUtil; +import jnpf.util.ServiceException; +import jnpf.util.UserProvider; +import jnpf.util.context.ThreadContext; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 考勤确认设置 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-11-07 09:33:43 + */ +@Slf4j +@Service +public class AttendanceConfirmSettingServiceImpl extends SuperServiceImpl implements AttendanceConfirmSettingService { + @Resource(name = "ioIntensiveThreadPool") + private ThreadPoolExecutor threadPoolExecutor; + @Autowired + private TransactionTemplate transactionTemplate; + @Resource + private AttendanceConfirmService attendanceConfirmService; + + @Override + public ConfirmSettingInfoVo getConfirmSetting(Boolean isNew) { + List confirmSettingList = this.lambdaQuery().list(); + ConfirmSettingInfoVo build = ConfirmSettingInfoVo.builder() + .confirmMonth(2) + .allowConfirm(1) + .confirmDuration(7) + .slippageResult(1) + .build(); + if (CollUtil.isEmpty(confirmSettingList)) { + return build; + } + AttendanceConfirmSetting confirmSetting = confirmSettingList.size() == 2 ? + confirmSettingList.stream().filter(setting -> isNew && setting.getNextData().equals(1)).findFirst().orElse(null) : + confirmSettingList.stream().findFirst().orElse(null); + if (Objects.isNull(confirmSetting)) { + return build; + } + return ConfirmSettingInfoVo.builder() + .id(confirmSetting.getId()) + .confirmMonth(confirmSetting.getConfirmMonth()) + .allowConfirm(confirmSetting.getAllowConfirm()) + .confirmDuration(confirmSetting.getConfirmDuration()) + .slippageResult(confirmSetting.getSlippageResult()) + .build(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean confirmSettingSubmit(ConfirmSettingSubmitDto dto) { + //新增、修改或者删除考勤确认设置 + saveOrUpdateOrDelete(dto); + //下一次应用 + if (dto.getOptMethod().equals(1)) { + return Boolean.TRUE; + } + Assert.isFalse(dto.getSlippageResult().equals(2), "不能设置逾期不可确认"); + //处理考勤确认数据 + YearMonth yearMonth = YearMonth.of(LocalDate.now().getYear(), LocalDate.now().getMonth().getValue()); + Date startDate = Date.from(yearMonth.atDay(dto.getAllowConfirm()).atStartOfDay(ZoneId.systemDefault()).toInstant()); + int currentYear = yearMonth.getYear(), currentMonth = yearMonth.getMonth().getValue(); + if (Objects.equals(dto.getConfirmMonth(), 2)) { + yearMonth = yearMonth.minusMonths(1); + currentYear = yearMonth.getYear(); + currentMonth = yearMonth.getMonth().getValue(); + } + Date expectedTime = DateUtil.offsetDay(startDate, dto.getConfirmDuration()); + //异步生成考勤确认数据 + int finalCurrentMonth = currentMonth; + int finalCurrentYear = currentYear; + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) { + try { + attendanceConfirmService.createConfirm(finalCurrentYear, finalCurrentMonth, expectedTime, startDate, dto.getSlippageResult()); + } catch (Exception e) { + log.error("异步生成考勤确认数据失败,事务回滚", e); + status.setRollbackOnly(); + throw e; + } + } + }); + }), threadPoolExecutor) + .exceptionally(ex -> { + log.error("生成考勤确认数据异常", ex); + return null; + }); + return Boolean.TRUE; + } + + /** + * 新增、修改或者删除考勤确认设置 + * + * @param dto 实体参数类 + */ + private void saveOrUpdateOrDelete(ConfirmSettingSubmitDto dto) { + if (StringUtils.isNotEmpty(dto.getId())) { + AttendanceConfirmSetting confirmSetting = this.getById(dto.getId()); + ServiceException.notNull(confirmSetting, "未查询到考勤确认设置"); + if (confirmSetting.getNextData().equals(1) && dto.getOptMethod().equals(2)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(AttendanceConfirmSetting::getNextData, Boolean.FALSE); + this.baseMapper.delete(queryWrapper); + confirmSetting.setNextData(0); + BeanUtils.copyProperties(dto, confirmSetting); + confirmSetting.setLastModifyTime(new Date()); + confirmSetting.setLastModifyUserId(UserProvider.getUser().getUserId()); + this.updateById(confirmSetting); + } else if (confirmSetting.getNextData().equals(0) && dto.getOptMethod().equals(1)) { + AttendanceConfirmSetting nextConfirmSetting = AttendanceConfirmSetting.builder() + .id(FtbUtil.getId()) + .confirmMonth(dto.getConfirmMonth()) + .allowConfirm(dto.getAllowConfirm()) + .confirmDuration(dto.getConfirmDuration()) + .slippageResult(dto.getSlippageResult()) + .nextData(1) + .creatorTime(new Date()) + .creatorUserId(UserProvider.getUser().getUserId()) + .build(); + this.save(nextConfirmSetting); + } else { + BeanUtils.copyProperties(dto, confirmSetting); + confirmSetting.setLastModifyTime(new Date()); + confirmSetting.setLastModifyUserId(UserProvider.getUser().getUserId()); + this.updateById(confirmSetting); + } + } else { + this.save(AttendanceConfirmSetting.builder() + .id(FtbUtil.getId()) + .confirmMonth(dto.getConfirmMonth()) + .allowConfirm(dto.getAllowConfirm()) + .confirmDuration(dto.getConfirmDuration()) + .slippageResult(dto.getSlippageResult()) + .nextData(0) + .creatorTime(new Date()) + .creatorUserId(UserProvider.getUser().getUserId()) + .build()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCustomizeTableServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCustomizeTableServiceImpl.java new file mode 100644 index 0000000..f361ae2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceCustomizeTableServiceImpl.java @@ -0,0 +1,155 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.mapper.AttendanceCustomizeTableMapper; +import jnpf.attendance.service.AttendanceCustomizeTableService; +import jnpf.attendance.service.AttendanceLeaveTypeService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceCustomizeTable; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.model.attendance.model.LeaveTypeJsonHead; +import jnpf.model.attendance.vo.attendance.AttendanceCustomizeTableVo; +import jnpf.model.attendance.vo.attendance.CustomizeTableUpdateVo; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static com.alibaba.fastjson.JSON.parseArray; + +/** + *

+ * 考勤-自定义报表设置 服务实现类 + *

+ * + * @author ahua + * @since 2024-09-03 + */ +@Service +public class AttendanceCustomizeTableServiceImpl extends SuperServiceImpl implements AttendanceCustomizeTableService { + @Resource + private AttendanceLeaveTypeService attendanceLeaveTypeService; + + @Override + @Transactional + public List findList(String keyword, Integer status, Integer type) { + List list = lambdaQuery().like(StringUtil.isNotEmpty(keyword), AttendanceCustomizeTable::getFieldName, keyword).eq(Objects.nonNull(status), AttendanceCustomizeTable::getStatus, status).eq(AttendanceCustomizeTable::getType, type).orderByAsc(AttendanceCustomizeTable::getSort).list(); + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + // 查询自定义假期表头 + AttendanceCustomizeTable customLeaveJson = list.stream().filter(customizeTable -> "customLeaveJson".equals(customizeTable.getField())).findFirst().orElse(null); + if (Objects.nonNull(customLeaveJson)) { + list.removeIf(customizeTable -> "customLeaveJson".equals(customizeTable.getField())); + } + List tableVos = BeanUtil.copyToList(list, AttendanceCustomizeTableVo.class); + // 判断是否存在自定义请假Json数据 + Map typeJsonDataMap = new HashMap<>(); + if (customLeaveJson != null && StringUtil.isNotEmpty(customLeaveJson.getCustomLeaveJson())) { + List typeJsonDataList = parseArray(customLeaveJson.getCustomLeaveJson(), LeaveTypeJsonHead.class); + Optional.ofNullable(typeJsonDataList).ifPresent(typeJsonDataList1 -> typeJsonDataList1.forEach(typeJsonData -> typeJsonDataMap.put(typeJsonData.getField(), typeJsonData))); + } + // 实时查询自定义请见类型 + List leaveRulesLiat = attendanceLeaveTypeService.list(new LambdaQueryWrapper() + .ne(AttendanceLeaveType::getBuiltIn, 1) + .isNotNull(AttendanceLeaveType::getDeleteMark) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + if (CollUtil.isEmpty(leaveRulesLiat)) { + return tableVos; + } + // 封装所有请假类型数据 + List typeJsonDataList = CollUtil.newArrayList(); + // 获取最大排序 + int maxSort = tableVos.stream().mapToInt(AttendanceCustomizeTableVo::getSort).max().orElse(0); + int maxLeaveSort = typeJsonDataMap.values().stream().mapToInt(LeaveTypeJsonHead::getSort).max().orElse(0); + AtomicInteger rank = new AtomicInteger(Math.max(maxSort, maxLeaveSort)); + AtomicBoolean isAdd = new AtomicBoolean(false); + leaveRulesLiat.forEach(leaveRules -> { + LeaveTypeJsonHead typeJsonData; + if (typeJsonDataMap.containsKey(leaveRules.getId())) { + typeJsonData = typeJsonDataMap.get(leaveRules.getId()); + if (!leaveRules.getName().equals(typeJsonData.getFieldName()) || !leaveRules.getDeleteMark().equals(typeJsonData.getShow())) { + isAdd.set(true); + } + typeJsonData.setShow(leaveRules.getDeleteMark()); + typeJsonData.setDeleteTime(leaveRules.getDeleteTime()); + typeJsonData.setFieldName(leaveRules.getName()); + typeJsonData.setHeadName(leaveRules.getName()); + } else { + isAdd.set(true); + typeJsonData = LeaveTypeJsonHead.builder() + .field(leaveRules.getId()) + .customLeave(1) + .fieldName(leaveRules.getName()) + .headName(leaveRules.getName()) + .width(120) + .status(1) + .show(leaveRules.getDeleteMark()) + .deleteTime(leaveRules.getDeleteTime()) + .sort(rank.getAndIncrement()) + .build(); + } + typeJsonDataList.add(typeJsonData); + }); + // 是否有新增假类型,或者变更请假名称 + if (isAdd.get() && Objects.nonNull(customLeaveJson)) { + customLeaveJson.setCustomLeaveJson(JSON.toJSONString(typeJsonDataList)); + list.add(customLeaveJson); + updateBatchById(list); + } + if (Objects.nonNull(customLeaveJson)) { + // 重新生成序号 + AtomicInteger index = new AtomicInteger(typeJsonDataList.stream().mapToInt(LeaveTypeJsonHead::getSort).min().orElse(0)); + typeJsonDataList.sort(Comparator.comparingInt(LeaveTypeJsonHead::getShow)); + typeJsonDataList.forEach(typeJsonData -> typeJsonData.setSort(index.getAndIncrement())); + customLeaveJson.setCustomLeaveJson(JSON.toJSONString(typeJsonDataList)); + updateById(customLeaveJson); + } + if (CollUtil.isNotEmpty(typeJsonDataList)) { + tableVos.addAll(typeJsonDataList.stream().map(typeJsonData -> AttendanceCustomizeTableVo.builder() + .id(typeJsonData.getField()) + .field(typeJsonData.getField()) + .customLeave(1) + .sort(typeJsonData.getSort()) + .fieldName(typeJsonData.getFieldName()) + .headName(typeJsonData.getHeadName()) + .status(typeJsonData.getStatus()) + .show(typeJsonData.getShow()) + .width(typeJsonData.getWidth()) + .deleteTime(typeJsonData.getDeleteTime()) + .build()).collect(Collectors.toList())); + } + tableVos.sort(Comparator.comparingInt(AttendanceCustomizeTableVo::getSort)); + return tableVos; + } + + @Override + @Transactional + public void update(CustomizeTableUpdateVo tableVos) { + Assert.notEmpty(tableVos.getTableVos(), "当前变更数据为空"); + // 获取自定义假字段 + List customLeaveTableVos = tableVos.getTableVos().stream().filter(tableVo -> tableVo.getCustomLeave().equals(1)).collect(Collectors.toList()); + // 转化为自定义请假类型 + List typeJsonDataList = CollUtil.isNotEmpty(customLeaveTableVos) ? JsonUtil.getJsonToList(customLeaveTableVos, LeaveTypeJsonHead.class) : CollUtil.newArrayList(); + // 移除掉自定义假字段 + tableVos.getTableVos().removeIf(tableVo -> tableVo.getCustomLeave().equals(1)); + //转化为数据库实体 + List updateDataList = tableVos.getTableVos().stream().map(tableVo -> BeanUtil.toBean(tableVo, AttendanceCustomizeTable.class)).collect(Collectors.toList()); + // 获取自定义假期Json字段 + AttendanceCustomizeTable customLeaveJsonField = lambdaQuery().eq(AttendanceCustomizeTable::getType, tableVos.getType()).eq(AttendanceCustomizeTable::getField, "customLeaveJson").last("limit 1").one(); + // 变更自定义假Json数据 + customLeaveJsonField.setCustomLeaveJson(JsonUtil.getObjectToString(typeJsonDataList)); + updateDataList.add(customLeaveJsonField); + updateBatchById(updateDataList); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDailyRuleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDailyRuleServiceImpl.java new file mode 100644 index 0000000..fa9e7d7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDailyRuleServiceImpl.java @@ -0,0 +1,5880 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.lang.Assert; +import cn.hutool.json.JSONUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import jnpf.AnalysisExternalInterfaceApi; +import jnpf.EstimateTurnoverTaskApi; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.entity.FixedHandleDTO; +import jnpf.attendance.excel.HeadStyleHandler; +import jnpf.attendance.excel.listener.CustomStringStringConverter; +import jnpf.attendance.excel.listener.LineSchedulesDataListener; +import jnpf.attendance.excel.listener.SchedulesDataListener; +import jnpf.attendance.mapper.AttendanceClockInMapper; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.mapper.AttendanceDailyRuleMapper; +import jnpf.attendance.service.*; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedEmployeeView; +import jnpf.attendance.service.impl.preschedule.ByEmployeeSchedulesV2Converter; +import jnpf.attendance.service.impl.preschedule.PreScheduleRedisSupport; +import jnpf.attendance.service.handle.notification.AttendanceRuleNotificationHandle; +import jnpf.attendance.service.handle.rule.*; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.constants.RedisConstant; +import jnpf.controller.util.ExcelExportTemplate; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.*; +import jnpf.entity.workflow.AttendanceBusinessTripApprove; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.enums.attendance.*; +import jnpf.enums.attendance.v2.ClockOutHandleParam; +import jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.model.DayClockRange; +import jnpf.model.attendance.model.DayInfo; +import jnpf.model.attendance.model.ScheduleImportFailModel; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.AttendanceGroupUserStatusUtil; +import jnpf.util.attendance.DailyRuleUtil; +import jnpf.util.attendance.SecondmentTypeUtil; +import jnpf.util.attendance.RuleExcelImportUtil; +import jnpf.util.auth.V2AuthPermissionUtils; +import jnpf.util.context.ThreadContext; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.vo.EstimateTurnoverStoredRowApiVo; +import jnpf.vo.StoreDayReceivableRevenueVo; +import jnpf.workflow.service.BusinessTripApproveService; +import jnpf.workflow.service.GoOutApproveService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.URL; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jnpf.constants.AttendanceConstant.HOLIDAYS_COLOR; +import static jnpf.constants.RedisConstant.ATTENDANCE_SET_SCHEDULES; +import static jnpf.enums.attendance.v2.WorkBoundaryCoverageEnum.LATE_LEAVE_LATE_ARRIVE; + +/** + *

+ * 考勤组-每日出勤规则 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +@Service +@Slf4j +public class AttendanceDailyRuleServiceImpl extends SuperServiceImpl implements AttendanceDailyRuleService { + + private final static String[] weekDays = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"}; + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private AttendanceUserService attendanceGroupUserService; + @Autowired + private AttendanceShiftSettingService attendanceShiftSettingService; + @Autowired + private AttendanceShiftNameSettingService attendanceShiftNameSettingService; + @Autowired + private AttendanceFestivalRulesService attendanceFestivalRulesService; + @Autowired + private AttendanceBaseSettingService attendanceBaseSettingService; + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + @Resource + private AttendanceClockInMapper attendanceClockInMapper; + @Autowired + private WorkOvertimeRuleProcessor workOvertimeRuleProcessor; + @Autowired + private StepOutRuleProcessor stepOutRuleProcessor; + @Autowired + private LeaveRuleProcessor leaveRuleProcessor; + @Autowired + private OrdinaryRuleProcessor ordinaryRuleProcessor; + @Autowired + private RestRuleProcessor restRuleProcessor; + @Autowired + private UserAntifreeze userApi; + @Autowired + private AnalysisExternalInterfaceApi analysisExternalInterfaceApi; + @Autowired + private EstimateTurnoverTaskApi estimateTurnoverTaskApi; + @Autowired + private AttendanceRuleNotificationHandle attendanceRuleNotificationHandle; + @Autowired + private RedissonClient redissonClient; + @Autowired + private GoOutApproveService goOutApproveService; + @Autowired + private BusinessTripApproveService businessTripApproveService; + @Resource + private ExcelExportTemplate excelExportTemplate; + @Resource + private ThreadPoolExecutor cpuIntensiveThreadPool; + @Resource + private CustomTenantUtil customTenantUtil; + @Autowired + private StringRedisTemplate redisUtil; + @Resource + private OvertimeRuleService overtimeRuleService; + @Autowired + private PublicHolidayRulesService publicHolidayRulesService; + @Resource + private AttendanceDailyRuleMapper attendanceDailyRuleMapper; + @Autowired + private V2OrganizeApi organizeApi; + @Autowired + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Resource + private V2AuthPermissionUtils authPermissionUtils; + @Autowired + private AttendanceLineSchedulingConfigService lineSchedulingConfigService; + @Autowired + private AttendanceLineSchedulingPayrollHoursService lineSchedulingPayrollHoursService; + @Resource + private PreScheduleRedisSupport preScheduleRedisSupport; + + + /** + * 获取目标日期涉及的借调时段 + * + * @param secondmentDateVos + * @param inPoint + * @param outPoint + * @return + */ + /** + * 获取目标日期涉及的借调时段 + * + * @param secondmentDateVos + * @param inPoint + * @param outPoint + * @return + */ + private static List getSecondmentTypeList(List secondmentDateVos, Date inPoint, Date outPoint) { + if (CollUtil.isEmpty(secondmentDateVos)) { + return null; + } + return secondmentDateVos.stream().filter(dateVo -> DateDetail.checkTimeBetween(inPoint, dateVo.getStartTime(), dateVo.getEndTime()) || DateDetail.checkTimeBetween(outPoint, dateVo.getStartTime(), dateVo.getEndTime()) || DateDetail.checkTimeBetween(dateVo.getStartTime(), inPoint, outPoint) || DateDetail.checkTimeBetween(dateVo.getEndTime(), inPoint, outPoint)).sorted(Comparator.comparing(SecondmentDateVo::getStartTime)).collect(Collectors.toList()); + } + + private OvertimeRuleDetailVo getOvertimeRuleDetailVo(List rules, Date start, Date end, String applyId) { + if (CollUtil.isEmpty(rules)) { + return null; + } + String userId = rules.get(0).getUserId(); + rules = rules.stream().filter(vo -> Objects.equals(vo.getApplyViewEnable(), 1) || Objects.equals(vo.getApplyViewEnable(), 3) || Objects.equals(vo.getApplyViewEnable(), 9) || Objects.equals(vo.getApplyViewEnable(), 10)) + //去除本身这条 + .filter(vo -> !Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || !StringUtil.equals(vo.getApplyId(), applyId)).collect(Collectors.toList()); + if (CollUtil.isEmpty(rules)) { + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + } + //规则是否包含公休 attendanceType = 2 + FtbAttendanceDailyRule restRule = rules.stream().filter(v -> v.getAttendanceType() == 2).findFirst().orElse(null); + //如果不包含公休,则获取工作日的加班规则 + if (Objects.isNull(restRule)) { + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + } + //如果包含公休,且公休为全天班,则获取公休的加班规则 + if (Objects.equals(restRule.getSchedulesType(), 0)) { + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.REST.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + } + //过滤加班类型为公休日的加班 + List collect = rules.stream().filter(vo -> { + if (!Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())) { + return true; + } + OvertimeRuleDetailVo overtimeRuleDetailVo = JSONObject.parseObject(vo.getPeriodInfo(), OvertimeRuleDetailVo.class); + return !Objects.equals(overtimeRuleDetailVo.getOvertimeType(), 2); + }).collect(Collectors.toList()); + //拿取rules中的去除休的最早普班、请假、加班 + FtbAttendanceDailyRule minRule = collect.stream().filter(v -> v.getAttendanceType() != 2).min(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)).orElse(null); + //拿取rules中的去除休的最晚普班、请假、加班 + FtbAttendanceDailyRule maxRule = collect.stream().filter(v -> v.getAttendanceType() != 2).max(Comparator.comparing(FtbAttendanceDailyRule::getOutPoint)).orElse(null); + //如果公休为上半天班 + if (Objects.equals(restRule.getSchedulesType(), 1)) { + //开始时间小于最早班次的开始时间,则获取公休的加班规则 + if (start.compareTo(minRule.getInPoint()) < 0) { + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.REST.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + } + //开始时间大于最早班次的开始时间,则获取工作日的加班规则 + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + } + //如果公休为下半天班 + //开始时间小于最晚班次的结束时间,则获取工作日的加班规则 + if (start.compareTo(maxRule.getInPoint()) < 0) { + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.ORDINARY.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + } + //开始时间大于最晚班次的结束时间,则获取公休的加班规则 + OvertimeRuleVo effectDetail = overtimeRuleService.getEffectDetail(userId, AttendanceTypeEnum.REST.getCode(), start); + if (Objects.isNull(effectDetail)) { + return null; + } + return effectDetail.getEffectRuleDetail(); + + } + + /** + * 排班导出 + * + * @param groupId + * @param realName + * @param month + * @return + */ + @Override + public void schedulesExport(String groupId, String workGroupId, String realName, String month, List userIdList, Integer isSchedules) { + List schedulesList = getSchedulesListV2(groupId, workGroupId, realName, month, userIdList, isSchedules); + Date date = DateUtil.stringToDates(month + "-01"); + List monthDayInfos = getMonthDayInfos(date); + AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findByGroupId(groupId); + boolean isFixed = Objects.equals(byGroupId.getSystemType(), 1); + List shiftPeriodVos = attendanceShiftSettingService.periodList(groupId); + AttendanceGroup byId = attendanceGroupService.getById(groupId); + List heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList()); + OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(StringUtil.isNotEmpty(workGroupId) ? OrganizeCategoryEnums.TEAM : null, StringUtil.isNotEmpty(workGroupId) ? workGroupId : byId.getOrgId()).getData(); + String titleHead = DateUtil.dateToString(date, "yyyy年MM月【") + infoById.getName() + "】排班表"; + List collect = shiftPeriodVos.stream().map(vo -> vo.getName() + ":" + vo.getSchedulingPeriod().getPeriodNames().stream().map(PeriodNameVO::getPeriodName).reduce((r1, r2) -> r1 + " | " + r2).orElse("")).filter(StringUtil::isNotEmpty).collect(Collectors.toList()); + List> partition = Lists.partition(collect, 8); + String shiftHead = "[RED]成员班次请输入班次全称!" + (char) 10 + "班次信息: 公休 " + partition.stream().map(list -> StringUtil.join(list, " ")).reduce((r1, r2) -> r1 + (char) 10 + r2).orElse(""); + heads.add(0, "已休|应休"); + heads.add(0, "手机号码"); + heads.add(0, "姓名"); + excelExportTemplate.exportModule(UserProvider.getLoginUserId(), titleHead, MonthStatisticsPageListExportVo.class, heads.stream().map(head -> CollUtil.newArrayList(titleHead, shiftHead, head)).collect(Collectors.toList()), null, null, 2000, HeadStyleHandler::new, (page, size) -> { + List> data = CollUtil.newArrayList(); + schedulesList.forEach(schedulesVo -> { + Map objectObjectHashMap = Maps.newHashMap(); + Map val = schedulesVo.getVal(); + objectObjectHashMap.put(0, schedulesVo.getRealName()); + objectObjectHashMap.put(1, schedulesVo.getMobilePhone()); + objectObjectHashMap.put(2, schedulesVo.getUsed() + "|" + (isFixed ? "-" : schedulesVo.getTotal())); + for (int i = 0; i < monthDayInfos.size(); i++) { + DayInfo dayInfo = monthDayInfos.get(i); + SchedulesDayVo orDefault = val.get(dayInfo.getDay()); + String context = CollUtil.isEmpty(orDefault.getItemVos()) ? "" : orDefault.getItemVos().stream().filter(item -> Objects.nonNull(item.getType())).map(item -> !Objects.equals(SchedulesTypeEnum.REST.getCode(), item.getType()) && !Objects.equals(SchedulesTypeEnum.HOLIDAYS.getCode(), item.getType()) && !Objects.equals(SchedulesTypeEnum.PAID_HOLIDAYS.getCode(), item.getType()) ? item.getName() : SchedulesTypeEnum.getMsg(item.getType())).filter(StringUtil::isNotEmpty).reduce((r1, r2) -> r1 + "|" + r2).orElse(""); + objectObjectHashMap.put(i + 3, context); + } + data.add(objectObjectHashMap); + }); + return data; + }); + } + + @Override + public List schedulesImport(SchedulesImportDto schedulesImportDto) throws IOException { + Date date = DateUtil.stringToDates(schedulesImportDto.getMonth() + "-01"); + List monthDayInfos = getMonthDayInfos(date); + AttendanceShiftSettingVo shiftPeriodVos = attendanceShiftSettingService.findByGroupId(schedulesImportDto.getGroupId()); + Map shiftMap = shiftPeriodVos.getSchedulingPeriods().stream().collect(Collectors.toMap(ShiftNameVo::getName, Function.identity(), (r1, r2) -> r1)); + //获取当前考勤组成员 + List attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(date, cn.hutool.core.date.DateUtil.endOfMonth(date), List.of(), CollUtil.newArrayList(schedulesImportDto.getGroupId()), Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return CollUtil.newArrayList(); + } + Map> groupUserMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + List infoByIds = userApi.getStaffRosterListInfoByIds(attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList())); + Map userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getMobilePhone, Function.identity(), (r1, r2) -> r1)); + List heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList()); + heads.add(0, "已休|应休"); + heads.add(0, "手机号码"); + heads.add(0, "姓名"); + List importData = CollUtil.newArrayList(); + List failList = CollUtil.newArrayList(); + EasyExcel.read(new URL(schedulesImportDto.getFileUrl()).openStream(), new SchedulesDataListener(importData, failList, heads, date, shiftMap, userMap)).registerConverter(new CustomStringStringConverter()).sheet().headRowNumber(3).doRead(); + importData.forEach(v -> { + if (groupUserMap.containsKey(v.getUserId())) { + List userList = groupUserMap.getOrDefault(v.getUserId(), List.of()); + v.getVal().forEach((day, value) -> { + DateTime parse = cn.hutool.core.date.DateUtil.parse(day, "yyyy-MM-dd"); + Integer existStatus = isExistStatus(userList, parse); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + Integer finalExistStatus = existStatus; + value.getItemVos().forEach(vo -> { + vo.setIsExist(finalExistStatus); + if (Objects.equals(finalExistStatus, 0) || Objects.equals(finalExistStatus, -1)) { + vo.setName(null); + vo.setShortName(null); + vo.setType(null); + } + }); + }); + } + }); + + List schedulesList = getSchedulesListV2(schedulesImportDto.getGroupId(), schedulesImportDto.getWorkGroupId(), null, schedulesImportDto.getMonth(), null, 1); + Map> collect = importData.stream().collect(Collectors.toMap(SchedulesV2Vo::getUserId, SchedulesV2Vo::getVal)); + schedulesList.forEach(v -> { + if (collect.containsKey(v.getUserId())) { + Map stringSchedulesDayVoMap = collect.get(v.getUserId()); + BigDecimal balance = v.getTotal(); + BigDecimal used = BigDecimal.ZERO; + for (Map.Entry entry : v.getVal().entrySet()) { + SchedulesDayVo value = entry.getValue(); + if (stringSchedulesDayVoMap.containsKey(entry.getKey())) { + value.setItemVos(stringSchedulesDayVoMap.get(entry.getKey()).getItemVos()); + } + List itemVos = value.getItemVos(); + if (CollUtil.isEmpty(itemVos)) { + continue; + } + if (itemVos.stream().noneMatch(vo -> Objects.equals(vo.getType(), 1))) { + continue; + } + String shiftName = itemVos.stream().filter(vo -> StringUtil.isNotEmpty(vo.getName())).map(SchedulesItemVo::getName).distinct().findFirst().orElse(""); + ShiftNameVo shiftPeriodVo = StringUtil.isEmpty(shiftName) ? null : shiftMap.get(shiftName); + List periods = Objects.isNull(shiftPeriodVo) ? null : shiftPeriodVo.getPeriods(); + //扣除公休余额逻辑 + List collect1 = itemVos.stream().filter(vo -> Objects.equals(vo.getType(), 1)).collect(Collectors.toList()); + for (SchedulesItemVo vo : collect1) { + BigDecimal calPeriodWorkDay = calPeriodWorkDay(vo.getSchedulesType(), periods); + balance = balance.subtract(calPeriodWorkDay); + used = used.add(calPeriodWorkDay); + if (RuleExcelImportUtil.addFail(balance.compareTo(BigDecimal.ZERO) < 0, ScheduleImportEnum.IMPORT_ERROR_PUBLIC_HOLIDAY_NOT_ENOUGH, v.getRealName(), failList)) { + return; + } + + } + } + v.setUsed(used); + } + }); + String fail = failList.stream().sorted(Comparator.comparingInt(model -> model.getError().getIndex())).map(model -> String.format(model.getError().getMessage(), StringUtil.join(model.getUserNames().stream().distinct().collect(Collectors.toList()), "】、【"))).findFirst().orElse(null); + Assert.isFalse(StringUtil.isNotEmpty(fail), fail); + return schedulesList; + } + + @Override + public Map getUserPublicHoliday(String yearMonth, List userIds) { + List publicHoliday = attendanceDailyRuleMapper.getUserPublicHoliday(yearMonth, userIds); + return CollUtil.isNotEmpty(publicHoliday) ? publicHoliday.stream().collect(Collectors.toMap(KeyValueVo::getUserId, KeyValueVo::getValue)) : new HashMap<>(); + } + + @Override + public Date getEarliestSchedulingDate(Date start) { + FtbAttendanceDailyRule one = lambdaQuery() + .select(FtbAttendanceDailyRule::getDay) + .ge(Objects.nonNull(start), FtbAttendanceDailyRule::getDay, start) + .orderByAsc(FtbAttendanceDailyRule::getDay) + .last("limit 1") + .one(); + return Objects.nonNull(one) ? one.getDay() : new Date(); + } + + @Override + public void hisDailyRule(Map group2orgMap, Map org2groupMap, Map> usersMap) { + //排班日规则历史数据处理 + List list = lambdaQuery().list(); + list.stream().collect(Collectors.groupingBy(dailyRule -> dailyRule.getGroupId() + dailyRule.getUserId() + dailyRule.getDay())).forEach((k, list1) -> { + boolean isOrdinary = list1.stream().anyMatch(v -> Objects.equals(v.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) || Objects.equals(v.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())); + list1.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.HOLIDAYS.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())).forEach(vo -> { + vo.setAttendanceType(AttendanceTypeEnum.REST.getCode()); + if (isOrdinary) { + vo.setPeriodWorkDay(BigDecimal.valueOf(0.5)); + vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(vo.getPayrollHours()).setScale(4, RoundingMode.HALF_UP)); + return; + } + vo.setPeriodWorkDay(BigDecimal.ONE); + vo.setPayrollHours(vo.getPayrollHours()); + }); + List ordinaryRules = list1.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList()); + ordinaryRules.forEach(vo -> { + if (Objects.equals(vo.getSchedulesType(), 1) || Objects.equals(vo.getSchedulesType(), 2)) { + vo.setPeriodWorkDay(BigDecimal.valueOf(0.5)); + vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(vo.getPayrollHours()).setScale(4, RoundingMode.HALF_UP)); + return; + } + vo.setPeriodWorkDay(BigDecimal.ONE); + vo.setPayrollHours(vo.getPayrollHours()); + }); + Map collect = ordinaryRules.stream().collect(Collectors.toMap(FtbAttendanceDailyRule::getSchedulesType, Function.identity(), (r1, r2) -> r1)); + List collect1 = list1.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && !Objects.equals(vo.getApplyViewEnable(), 0) && !Objects.equals(vo.getApplyViewEnable(), 2)).collect(Collectors.toList()); + Map> leaveForSchedulesType = collect1.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getSchedulesType)); + collect1.forEach(vo -> { + //如果普班为空,则是请假覆盖完普班 + BigDecimal leaveDay = Objects.equals(vo.getSchedulesType(), 1) || Objects.equals(vo.getSchedulesType(), 2) ? BigDecimal.valueOf(0.5) : BigDecimal.ONE; + if (CollUtil.isEmpty(collect)) { + //如果单个请假覆盖完全天班 + if (collect1.size() < 2) { + vo.setLeaveDay(BigDecimal.ONE); + vo.setPayrollHours(BigDecimal.valueOf(vo.getValidDuration())); + return; + } + //如果多个请假覆盖完全天班 + List leaveRules = leaveForSchedulesType.get(vo.getSchedulesType()); + //如果半天只有一条数据 + if (leaveRules.size() < 2) { + vo.setLeaveDay(BigDecimal.valueOf(0.5)); + vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(vo.getPayrollHours()).setScale(4, RoundingMode.HALF_UP)); + return; + } + //如果为全天班,则为0.5天,否则为1天 + Integer totalValidDuration = leaveRules.stream().map(FtbAttendanceDailyRule::getValidDuration).reduce(Integer::sum).orElse(0); + vo.setLeaveDay(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(totalValidDuration), 2, RoundingMode.HALF_UP).multiply(leaveDay).setScale(2, RoundingMode.HALF_UP)); + return; + } + //如果普班未覆盖完,则通过请假有效时长占普班原时长来计算, + FtbAttendanceDailyRule ordinaryRule = collect.get(vo.getSchedulesType()); + if (Objects.isNull(ordinaryRule)) { + //如果多个请假覆盖完半天班 + List leaveRules = leaveForSchedulesType.get(vo.getSchedulesType()); + //如果半天只有一条数据 + if (leaveRules.size() < 2) { + vo.setLeaveDay(BigDecimal.valueOf(0.5)); + vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(ordinaryRule.getPayrollHours()).setScale(4, RoundingMode.HALF_UP)); + return; + } + //如果为全天班,则为0.5天,否则为1天 + Integer totalValidDuration = leaveRules.stream().map(FtbAttendanceDailyRule::getValidDuration).reduce(Integer::sum).orElse(0); + vo.setLeaveDay(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(totalValidDuration), 2, RoundingMode.HALF_UP).multiply(leaveDay).setScale(2, RoundingMode.HALF_UP)); + vo.setPayrollHours(BigDecimal.valueOf(0.5).multiply(ordinaryRule.getPayrollHours()).setScale(4, RoundingMode.HALF_UP)); + return; + } + //计算ordinaryRule的originInPoint与originOutPoint的差值 + int originMinute = DateDetail.calculateMinuteDiff(ordinaryRule.getOriginInPoint(), ordinaryRule.getOriginOutPoint()); + // 计算originBreakStartPoint与originBreakEndPoint的差值来算出,如果任意一个为null,则默认为0 + originMinute -= (Objects.isNull(ordinaryRule.getBreakStartPoint()) ? 0 : DateDetail.calculateMinuteDiff(ordinaryRule.getOriginBreakStartPoint(), ordinaryRule.getOriginBreakEndPoint())); + vo.setLeaveDay(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(originMinute), 2, RoundingMode.HALF_UP).multiply(leaveDay).setScale(2, RoundingMode.HALF_UP)); + vo.setPayrollHours(BigDecimal.valueOf(vo.getValidDuration()).divide(BigDecimal.valueOf(originMinute), 4, RoundingMode.HALF_UP).multiply(ordinaryRule.getPayrollHours()).setScale(4, RoundingMode.HALF_UP)); + }); + }); + List collect = list.stream().filter(dailyRule -> !StringUtil.equals(org2groupMap.get(group2orgMap.getOrDefault(dailyRule.getGroupId(), "")), dailyRule.getGroupId())).collect(Collectors.toList()); + collect.forEach(dailyRule -> dailyRule.setGroupId(org2groupMap.get(group2orgMap.getOrDefault(dailyRule.getGroupId(), "")))); + collect.forEach(dailyRule -> { + List userList = usersMap.getOrDefault(dailyRule.getUserId(), List.of()); + if (userList.stream().anyMatch(vo -> Objects.equals(vo.getGroupId(), dailyRule.getGroupId()))) { + return; + } + AttendanceGroupUser attendanceGroupUser = userList.stream().filter(vo -> vo.getCreatorTime().compareTo(dailyRule.getDay()) <= 0 && (Objects.isNull(vo.getRemoveTime()) || vo.getRemoveTime().compareTo(dailyRule.getDay()) >= 0)).findFirst().orElse(null); + if (Objects.isNull(attendanceGroupUser)) { + return; + } + dailyRule.setGroupId(attendanceGroupUser.getGroupId()); + }); + updateBatchById(list); + } + + private List getMonthDayInfos(Date date) { + + DateTime beginOfMonth = cn.hutool.core.date.DateUtil.beginOfMonth(date); + DateTime endOfMonth = cn.hutool.core.date.DateUtil.endOfMonth(date); + List days = cn.hutool.core.date.DateUtil.rangeToList(beginOfMonth, endOfMonth, DateField.DAY_OF_MONTH); + //遍历获取每天属于周几 + return days.stream().map(day -> DayInfo.builder().day(DateUtil.dateToString(day, "yyyy-MM-dd")).weekStr(weekDays[cn.hutool.core.date.DateUtil.dayOfWeek(day) - 1]).dayNum(cn.hutool.core.date.DateUtil.dayOfMonth(day)).build()).collect(Collectors.toList()); + } + + /** + * 晚走晚到与加班无需审批 + * + * @param param + */ + @Override + public void clockOutHandle(ClockOutHandleParam param) { + if (param.getEnd().compareTo(cn.hutool.core.date.DateUtil.offsetMinute(param.getStart(), 1)) <= 0) { + return; + } + CompletableFuture.runAsync(() -> { + customTenantUtil.checkOutTenant(param.getTenantId()); + lateOutLateIn(param.getUserId(), param.getTenantId(), param.getStart(), param.getEnd()); + }); + } + + private void lateOutLateIn(String userId, String tenantId, Date start, Date end) { + DateTime endTime = cn.hutool.core.date.DateUtil.endOfDay(cn.hutool.core.date.DateUtil.offsetDay(end, 1)); + //检测是否已存在晚走晚到 + String key = String.format(RedisConstant.ATTENDANCE_LATE_IN_LATE_OUT, tenantId, userId); + if (redisUtil.hasKey(key)) { + String hashValues = redisUtil.opsForValue().get(key); + AttendanceLateInLateOut attendanceLateInLateOut = JSON.parseObject(hashValues, AttendanceLateInLateOut.class); + //是否为同一日期晚走晚到 + if (attendanceLateInLateOut.getEndTime().compareTo(endTime) == 0) { + return; + } + } + List attendanceGroupUsersOfSecondment = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(end, end, List.of(userId), null, Boolean.FALSE); + if (CollUtil.isEmpty(attendanceGroupUsersOfSecondment)) { + return; + } + AttendanceGroupUser attendanceGroupUser = attendanceGroupUsersOfSecondment.stream().findFirst().orElse(null); + //查询晚走晚到规则 + AttendanceBaseSettingVo baseSettingVo = attendanceBaseSettingService.getOne(attendanceGroupUser.getGroupId()); + if (Objects.equals(baseSettingVo.getLateEarly(), 0)) { + return; + } + //时间未达标数据过滤 + if (cn.hutool.core.date.DateUtil.offsetMinute(start, baseSettingVo.getLateMinis()).after(end)) { + return; + } + + //缓存起来,防止第二天重新排班 + redisUtil.opsForValue().set(key, JSON.toJSONString(AttendanceLateInLateOut.builder().startTime(end).endTime(endTime).lateMinis(baseSettingVo.getEarlyMinis()).build())); + redisUtil.expire(key, 2, TimeUnit.DAYS); + //查询daily rule,clockInTime后的最早一个班次 + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getUserId, userId).and(x -> x.or(x1 -> x1.gt(FtbAttendanceDailyRule::getInPoint, end).isNotNull(FtbAttendanceDailyRule::getInPoint)).or(x1 -> x1.between(FtbAttendanceDailyRule::getDay, end, endTime).isNull(FtbAttendanceDailyRule::getInPoint))).orderByAsc(FtbAttendanceDailyRule::getInPoint).last("limit 10").list(); + setRuleForLateOutLateIn(userId, tenantId, list); + } + + private void setRuleForLateOutLateIn(String userId, String tenantId, List list) { + //划线排班不做晚走晚到 + if (list.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) { + return; + } + String key = String.format(RedisConstant.ATTENDANCE_LATE_IN_LATE_OUT, tenantId, userId); + if (!redisUtil.hasKey(key)) { + return; + } + list = list.stream().filter(x -> Objects.equals(x.getUserId(), userId)).collect(Collectors.toList()); + list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay)).forEach((day, dayRules) -> { + Date amMaxDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getInPoint()) && Objects.equals(dayRule.getSchedulesType(), 1)).map(FtbAttendanceDailyRule::getOutPoint).min(Comparator.comparing(Date::getTime)).orElse(null); + Date pmMinDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getInPoint()) && Objects.equals(dayRule.getSchedulesType(), 2)).map(FtbAttendanceDailyRule::getInPoint).min(Comparator.comparing(Date::getTime)).orElse(null); + dayRules.forEach(dayRule -> { + if (Objects.nonNull(dayRule.getInPoint())) { + return; + } + //如果是首次数据,则设置inPoint为day + if (Objects.equals(dayRule.getSchedulesType(), 1)) { + dayRule.setInPoint(DateUtil.dateAddMinutes(pmMinDate, -1)); + return; + } + if (Objects.equals(dayRule.getSchedulesType(), 2)) { + dayRule.setInPoint(DateUtil.dateAddMinutes(amMaxDate, 1)); + return; + } + dayRule.setInPoint(dayRule.getDay()); + }); + }); + + String hashValues = redisUtil.opsForValue().get(key); + AttendanceLateInLateOut attendanceLateInLateOut = JSON.parseObject(hashValues, AttendanceLateInLateOut.class); + //第一个班次 + //如果第二天未查询到班 + if (CollUtil.isEmpty(list)) { + return; + } + CollUtil.sort(list, Comparator.comparing(FtbAttendanceDailyRule::getInPoint)); + FtbAttendanceDailyRule rule1 = list.stream().findFirst().orElse(null); + //最近班次为加班/请假/超过第二天的24点不做处理 + DateTime dateTime = Objects.isNull(rule1.getLatePoint()) ? null : cn.hutool.core.date.DateUtil.offsetMinute(rule1.getLatePoint(), attendanceLateInLateOut.getLateMinis()); + if (!Objects.equals(rule1.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) || rule1.getInPoint().before(attendanceLateInLateOut.getStartTime()) || rule1.getInPoint().after(attendanceLateInLateOut.getEndTime())) { + return; + } + list.forEach(vo -> { + if (Objects.nonNull(vo.getOriginalLatePoint())) { + vo.setInLackPoint(DateUtil.min(DateDetail.getNewDateByPeriod(vo.getLatePoint(), vo.getInLackPoint(), vo.getOriginalLatePoint()), DateUtil.dateAddMinutes(vo.getOutPoint(), -1))); + vo.setLatePoint(vo.getOriginalLatePoint()); + } + }); + list = list.stream().filter(rule -> DateDetail.checkTimeBetween(rule.getInPoint(), attendanceLateInLateOut.getStartTime(), attendanceLateInLateOut.getEndTime())).collect(Collectors.toList()); + if (list.size() > 1) { + FtbAttendanceDailyRule rule2 = list.get(1); + //第二班次在晚到范围内 + if (rule2.getInPoint().compareTo(dateTime) <= 0) { + return; + } + } + if (Objects.isNull(rule1.getOriginalLatePoint())) { + rule1.setOriginalLatePoint(rule1.getLatePoint()); + } + rule1.setInLackPoint(DateUtil.min(DateDetail.getNewDateByPeriod(rule1.getLatePoint(), rule1.getInLackPoint(), dateTime), DateUtil.dateAddMinutes(rule1.getOutPoint(), -1))); + rule1.setLatePoint(DateUtil.min(dateTime, DateUtil.dateAddMinutes(rule1.getInLackPoint(), -1))); + rule1.setIsLateOutLateIn(1); + if (rule1.getOutPoint().compareTo(dateTime) <= 0) { + rule1.setInUnbounded(Objects.equals(rule1.getInUnbounded(), 0) ? LATE_LEAVE_LATE_ARRIVE.getCode() : rule1.getInUnbounded()); + rule1.setOutUnbounded(Objects.equals(rule1.getOutUnbounded(), 0) ? LATE_LEAVE_LATE_ARRIVE.getCode() : rule1.getOutUnbounded()); + } + updateById(rule1); + } + + /** + * 加班不审批 + */ + @Override + @Transactional + public FtbAttendanceDailyRule workOvertimeNotApprove(ClockOutHandleParam param) { + if (StringUtil.isEmpty(param.getApplyId()) && Objects.equals(param.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())) { + lambdaUpdate().set(FtbAttendanceDailyRule::getOutPoint, param.getEnd()).set(FtbAttendanceDailyRule::getApplyEnd, param.getEnd()).eq(FtbAttendanceDailyRule::getId, param.getRuleId()).update(); + return null; + } + OvertimeRuleDetailVo overtimeRuleDetailVo = param.getRuleDetail(); + //查询打卡班次为公休、普班 + if (Objects.isNull(overtimeRuleDetailVo)) { + return null; + } + if (!Objects.equals(overtimeRuleDetailVo.getCalcMethod(), 3)) { + return null; + } + if (param.getEnd().compareTo(cn.hutool.core.date.DateUtil.offsetMinute(param.getStart(), overtimeRuleDetailVo.getMinOvertimeMinute())) <= 0) { + return null; + } + // 获取当前考勤组的排班锁 + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), param.getUserId())); + // 确保锁未被其他操作持有,避免并发执行 + try { + Assert.isFalse(lock.isLocked() || !lock.tryLock(5, TimeUnit.SECONDS), "存在其他排班或申请操作正在执行,请稍后再试"); + param.setApplyId("notApprove" + System.currentTimeMillis()); + List allRules = CollUtil.newArrayList(); + List allRuleIds = CollUtil.newArrayList(); + List dailyRuleResultVos = applyDailyRuleHandle(ApplyParam.builder().groupId(param.getGroupId()).userId(param.getUserId()).applyId(param.getApplyId()).validDuration(BigDecimal.ZERO).start(param.getStart()).end(param.getEnd()).attendanceType(AttendanceTypeEnum.WORKOVERTIME).build(), allRules, allRuleIds); + List collect = dailyRuleResultVos.stream().filter(result -> result.getType() == 1).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)) { + allRules.stream().filter(vo -> StringUtil.equals(vo.getApplyId(), param.getApplyId())).forEach(vo -> { + vo.setPeriodInfo(JSON.toJSONString(overtimeRuleDetailVo)); + }); + saveOrUpdateBatch(allRules, allRuleIds); + attendanceRuleNotificationHandle.notificationHandle(dailyRuleResultVos, param.getTenantId(), false, false); + } + return allRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).max(Comparator.comparing(FtbAttendanceDailyRule::getInPoint)).orElse(null); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private Map buildPublicHolidayBalanceMap(List userIds, Date periodStart, Date periodEnd) { + Map balanceMap = new HashMap<>(); + if (CollUtil.isEmpty(userIds)) { + return balanceMap; + } + LocalDate s = periodStart.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate e = periodEnd.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + YearMonth ymStart = YearMonth.from(s); + YearMonth ymEnd = YearMonth.from(e); + YearMonth ym = ymStart; + while (!ym.isAfter(ymEnd)) { + String ymStr = ym.toString(); + List list = publicHolidayRulesService.getBalanceList(ymStr, userIds); + if (CollUtil.isNotEmpty(list)) { + for (AttendancePublicHolidayBalance b : list) { + BigDecimal t = b.getTotal(); + if (t == null) { + t = BigDecimal.ZERO; + } + balanceMap.merge(b.getUserId(), t, BigDecimal::add); + } + } + ym = ym.plusMonths(1); + } + return balanceMap; + } + + /** + * 获取排班数据(按自然月) + */ + @Override + public List getSchedulesListV2(String groupId, String workGroupId, String realName, String month, List finalUserIdList, Integer isSchedules) { + List userIds = Optional.ofNullable(finalUserIdList).orElseGet(ArrayList::new); + Date start = DateDetail.getMonthBeginDate(month); + Date end = DateDetail.getMonthEndDate(month); + return getSchedulesListV2Core(groupId, workGroupId, realName, start, end, month, userIds, isSchedules); + } + + /** + * 获取排班数据(按开始日期、结束日期) + */ + @Override + public List getSchedulesListV2ByDateRange(String groupId, String workGroupId, String realName, String startDate, String endDate, List finalUserIdList, Integer isSchedules) { + List userIds = Optional.ofNullable(finalUserIdList).orElseGet(ArrayList::new); + Date start = cn.hutool.core.date.DateUtil.beginOfDay(DateUtil.stringToDates(startDate)); + Date end = cn.hutool.core.date.DateUtil.endOfDay(DateUtil.stringToDates(endDate)); + return getSchedulesListV2Core(groupId, workGroupId, realName, start, end, null, userIds, isSchedules, true); + } + + @Override + public List getSchedulesListV2ByPreScheduleDraft( + String groupId, String startDate, String endDate, String draftId) { + List byEmployee = + preScheduleRedisSupport.getByEmployeeByDraftId(draftId); + if (CollUtil.isEmpty(byEmployee)) { + return CollUtil.newArrayList(); + } + Date start = cn.hutool.core.date.DateUtil.beginOfDay(DateUtil.stringToDates(startDate)); + Date end = cn.hutool.core.date.DateUtil.endOfDay(DateUtil.stringToDates(endDate)); + List attendanceGroupUserVos = + attendanceGroupUserService.getAttendanceGroupUsersOfSecondment( + start, end, null, CollUtil.newArrayList(groupId), Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return CollUtil.newArrayList(); + } + Map> groupUserMap = + attendanceGroupUserVos.stream() + .filter(u -> StringUtil.isNotEmpty(u.getUserId())) + .collect( + Collectors.groupingBy( + u -> u.getUserId().trim())); + List userIds = + attendanceGroupUserVos.stream() + .map(AttendanceGroupUser::getUserId) + .filter(StringUtil::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + List infoByIds = + CollUtil.isNotEmpty(userIds) ? userApi.getStaffRosterListInfoByIds(userIds) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(infoByIds)) { + return CollUtil.newArrayList(); + } + Map realNameByUserId = + infoByIds.stream() + .filter(vo -> StringUtil.isNotEmpty(vo.getUserId())) + .collect( + Collectors.toMap( + PartUserInfoVo::getUserId, + vo -> + vo.getRealName() == null + ? "" + : vo.getRealName(), + (a, b) -> a, + LinkedHashMap::new)); + List userIdsInOrder = + infoByIds.stream() + .map(PartUserInfoVo::getUserId) + .filter(StringUtil::isNotEmpty) + .collect(Collectors.toList()); + Set shiftIdSet = ByEmployeeSchedulesV2Converter.collectShiftIds(byEmployee); + Map shiftMap = + CollUtil.isEmpty(shiftIdSet) + ? Collections.emptyMap() + : getShiftByShiftIds(new ArrayList<>(shiftIdSet)); + List datesByPeriod = DateDetail.getDatesByPeriod(start, end); + List schedulesV2Vos = + ByEmployeeSchedulesV2Converter.toSchedulesV2List( + byEmployee, + userIdsInOrder, + realNameByUserId, + shiftMap, + datesByPeriod, + (userId, day) -> + buildSchedulesDayVoForPreScheduleDraft( + groupUserMap.getOrDefault( + userId, Collections.emptyList()), + day)); + fillPreScheduleDraftSchedulesV2UserFields(start, end, groupUserMap, schedulesV2Vos); + fillCanLineScheduleByGroupConfig(groupId, schedulesV2Vos, infoByIds); + return schedulesV2Vos.stream() + .sorted( + Comparator.comparing( + vo -> + Objects.equals(vo.getDelMark(), 1) + ? 20000 + : Objects.isNull(vo.getSort()) ? 0 : vo.getSort())) + .collect(Collectors.toList()); + } + + /** + * 预排班草稿:填充 {@link SchedulesV2Vo} 人员头字段(delMark、selfGroup),与 + * {@link #getSchedulesListV2Core} 一致;canLineSchedule 由 {@link #fillCanLineScheduleByGroupConfig} 填充。 + */ + private void fillPreScheduleDraftSchedulesV2UserFields( + Date start, + Date end, + Map> groupUserMap, + List schedulesV2Vos) { + if (CollUtil.isEmpty(schedulesV2Vos)) { + return; + } + for (SchedulesV2Vo vo : schedulesV2Vos) { + if (StringUtil.isEmpty(vo.getUserId())) { + continue; + } + List groupUserVos = + groupUserMap.getOrDefault(vo.getUserId().trim(), Collections.emptyList()); + if (CollUtil.isEmpty(groupUserVos)) { + vo.setSelfGroup(0); + continue; + } + AttendanceGroupUser attendanceGroupUserVo = + groupUserVos.stream() + .max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)) + .orElse(null); + if (attendanceGroupUserVo == null) { + continue; + } + vo.setDelMark(attendanceGroupUserVo.getDeleteMark()); + Integer existStatus1 = isExistStatus(groupUserVos, start, end); + existStatus1 = + Objects.equals(existStatus1, 4) + ? 2 + : Objects.equals(existStatus1, 2) || Objects.equals(existStatus1, 3) ? 1 : 0; + vo.setSelfGroup(existStatus1); + } + } + + private SchedulesDayVo buildSchedulesDayVoForPreScheduleDraft( + List groupUserVos, Date day) { + Integer existStatus = isExistStatus(groupUserVos, day); + existStatus = ByEmployeeSchedulesV2Converter.normalizeExistStatus(existStatus); + return SchedulesDayVo.builder() + .isExist(existStatus) + .isLineSchedule(Boolean.FALSE) + .itemVos(new ArrayList<>()) + .build(); + } + + /** + * @param staffRosterList 已加载的花名册,非空时复用,避免重复调用 {@code userApi.getStaffRosterListInfoByIds} + */ + private void fillCanLineScheduleByGroupConfig(String groupId, List schedulesV2Vos, List staffRosterList) { + if (CollUtil.isEmpty(schedulesV2Vos)) { + return; + } + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(groupId); + if (Objects.isNull(lineSchedulingConfig)) { + schedulesV2Vos.forEach(vo -> vo.setCanLineSchedule(Boolean.FALSE)); + return; + } + List allUserIds = schedulesV2Vos.stream() + .map(SchedulesV2Vo::getUserId) + .filter(StringUtil::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + Set eligibleUserIds; + if (CollUtil.isNotEmpty(staffRosterList)) { + List filteredUserIds = CollUtil.newArrayList(allUserIds); + filterUserIdsByLineSchedulingConfig(filteredUserIds, lineSchedulingConfig, staffRosterList); + eligibleUserIds = new HashSet<>(filteredUserIds); + } else { + getFtbAttendanceLineSchedulingConfig(allUserIds, lineSchedulingConfig); + eligibleUserIds = new HashSet<>(allUserIds); + } + for (SchedulesV2Vo vo : schedulesV2Vos) { + vo.setCanLineSchedule(StringUtil.isNotEmpty(vo.getUserId()) && eligibleUserIds.contains(vo.getUserId())); + } + } + + /** + * 获取排班数据内部实现;month 非空时与按自然月查询一致,为空时按 start、end 自然日区间查询非请假规则 + */ + private List getSchedulesListV2Core(String groupId, String workGroupId, String realName, Date start, Date end, String month, List finalUserIdList, Integer isSchedules) { + return getSchedulesListV2Core(groupId, workGroupId, realName, start, end, month, finalUserIdList, isSchedules, false); + } + + private List getSchedulesListV2Core(String groupId, String workGroupId, String realName, Date start, Date end, String month, List finalUserIdList, Integer isSchedules, boolean fillCanLineSchedule) { + //班组查询 + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + List userBoundVO = null; + if (Objects.nonNull(targetAuthIdsVO) && Objects.equals(targetAuthIdsVO.getTargetAuthEnums(), TargetAuthEnums.USER) || StringUtil.isNotEmpty(workGroupId)) { + userBoundVO = attendanceGroupUserService.getUserBoundVO(groupId, workGroupId); + if (CollUtil.isEmpty(userBoundVO)) { + return List.of(); + } + } + List byRealName = StringUtil.isBlank(realName) ? null : userApi.getInfoByLikeName(realName); + if (StringUtil.isNotEmpty(realName) && CollUtil.isEmpty(byRealName)) { + return CollUtil.newArrayList(); + } + if (CollUtil.isNotEmpty(byRealName)) { + finalUserIdList.addAll(byRealName.stream().map(UserEntity::getId).collect(Collectors.toList())); + } + List userIdList; + if (CollUtil.isEmpty(userBoundVO)) { + userIdList = finalUserIdList; + } else { + userIdList = userBoundVO.stream().map(UserBoundVO::getId).filter(vo -> CollUtil.isEmpty(finalUserIdList) || finalUserIdList.contains(vo)).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIdList)) { + return List.of(); + } + } + + //获取当前考勤组成员 + List attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, Objects.isNull(userIdList) ? null : userIdList.stream().distinct().collect(Collectors.toList()), CollUtil.newArrayList(groupId), Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return CollUtil.newArrayList(); + } + Map> groupUserMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + //获取用户名 + List collect2 = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + Map stringBooleanMap = StringUtil.isNotEmpty(month) + ? attendanceDayStatisticsService.selectUserIsSeal(collect2, month) + : attendanceDayStatisticsService.selectUserIsSealByDateRange(collect2, start, end); + // 按天查询封存状态,跨月场景下用于精确判定每个用户每一天是否被封存 + Map> userDailySealMap = attendanceDayStatisticsService.selectUserDailySealByDateRange(collect2, start, end); + List infoByIds = CollUtil.isNotEmpty(collect2) ? userApi.getStaffRosterListInfoByIds(collect2) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(infoByIds)) { + return CollUtil.newArrayList(); + } + List list = lambdaQuery() + .eq(FtbAttendanceDailyRule::getGroupId, groupId) + .in(CollUtil.isNotEmpty(userIdList), FtbAttendanceDailyRule::getUserId, userIdList) + .notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode()) + .notIn(FtbAttendanceDailyRule::getApplyViewEnable, 0) + .and(x -> { + if (StringUtil.isNotEmpty(month)) { + x.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode()) + .le(FtbAttendanceDailyRule::getApplyStart, end) + .ge(FtbAttendanceDailyRule::getApplyEnd, start) + .or() + .likeRight(FtbAttendanceDailyRule::getDay, month); + } else { + Date ds = cn.hutool.core.date.DateUtil.beginOfDay(start); + Date de = cn.hutool.core.date.DateUtil.endOfDay(end); + x.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode()) + .le(FtbAttendanceDailyRule::getApplyStart, end) + .ge(FtbAttendanceDailyRule::getApplyEnd, start) + .or(w -> w.ge(FtbAttendanceDailyRule::getDay, ds).le(FtbAttendanceDailyRule::getDay, de)); + } + }) + .orderByAsc(FtbAttendanceDailyRule::getDay, FtbAttendanceDailyRule::getInPoint) + .list(); + if (Objects.isNull(list)) { + list = CollUtil.newArrayList(); + } + Map> userLeaveDayMap = list.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toMap(FtbAttendanceDailyRule::getUserId, vo -> DateDetail.getDatesByPeriod(vo.getApplyStart(), vo.getApplyEnd()), (k1, k2) -> { + k1.addAll(k2); + return k1.stream().distinct().collect(Collectors.toList()); + })); + Date now = new Date(); + list.removeIf(vo -> vo.getDay() != null && vo.getDay().before(cn.hutool.core.date.DateUtil.beginOfDay(start))); + list.removeIf(vo -> vo.getDay() != null && vo.getDay().after(cn.hutool.core.date.DateUtil.endOfDay(end))); + Map> collect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)); + Map userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + //查询打卡结果 + List ruleIds = list.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + List clockInResults = getClockInResult(ruleIds); + Map> clockInResult = clockInResults.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getRuleId)); + //查询打卡记录获取打卡时间 + List schedulesVos = CollUtil.newArrayList(); + //查询班次数据 + List collect5 = list.stream().map(FtbAttendanceDailyRule::getShiftId).collect(Collectors.toList()); + Map shiftMap = getShiftByShiftIds(collect5); + List datesByPeriod = DateDetail.getDatesByPeriod(start, end); + //公休余额查询 + Map balanceMap = StringUtil.isNotEmpty(month) + ? publicHolidayRulesService.getBalanceList(month, collect2).stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, AttendancePublicHolidayBalance::getTotal, (r1, r2) -> r2)) + : Maps.newHashMap(); + List attendanceTypeOfApply = CollUtil.newArrayList(); + AtomicReference isValidClock = new AtomicReference<>(Boolean.FALSE); + AtomicReference usedAll = new AtomicReference<>(BigDecimal.ZERO); + groupUserMap.forEach((ruleUserId, groupUserVos) -> { + Map val = Maps.newTreeMap(); + isValidClock.set(false); + usedAll.set(BigDecimal.ZERO); + AttendanceGroupUser attendanceGroupUserVo = groupUserVos.stream().max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).orElse(null); + List rules = collect.getOrDefault(ruleUserId, CollUtil.newArrayList()); + Map> collect1 = rules.stream() + //过滤原离组考勤组当日数据 + .filter(rule -> !Objects.equals(rule.getApplyViewEnable(), 3) || StringUtil.equals(rule.getGroupId(), groupId)).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay)); + datesByPeriod.forEach(date -> { + collect1.putIfAbsent(date, CollUtil.newArrayList()); + }); + Map> collect3 = collect1.entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) // 按 key 排序 + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new // 使用 LinkedHashMap 保持排序后的顺序 + )); + List userLeaveDayList = userLeaveDayMap.getOrDefault(ruleUserId, CollUtil.newArrayList()); + collect3.forEach((day, dayRules) -> { + attendanceTypeOfApply.clear(); + if (userLeaveDayList.stream().anyMatch(leaveDay -> day.compareTo(leaveDay) == 0)) { + attendanceTypeOfApply.add(SchedulesTypeEnum.LEAVE.getCode()); + } + String date = DateUtil.daFormat(day); + // 当日是否封存:按用户+日期精确判断,避免跨月查询时整体聚合导致误锁定 + boolean dayIsSeal = userDailySealMap.getOrDefault(ruleUserId, Collections.emptySet()).contains(date); + if (CollUtil.isEmpty(dayRules)) { + Integer existStatus = isExistStatus(groupUserVos, day); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3) || Objects.equals(existStatus, 2)) { + attendanceTypeOfApply.add(SchedulesTypeEnum.SECONDMENT_TO.getCode()); + } + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + val.put(date, SchedulesDayVo.builder().isExist(existStatus).attendanceTypeOfApply(attendanceTypeOfApply.stream().distinct().collect(Collectors.toList())).isLineSchedule(Boolean.FALSE).totalDuration(0).isSeal(dayIsSeal).build()); + return; + } + Integer existStatus = isExistStatus(groupUserVos, day); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3) || Objects.equals(existStatus, 2)) { + attendanceTypeOfApply.add(SchedulesTypeEnum.SECONDMENT_TO.getCode()); + } + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + boolean isLineScheduleDay = dayRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2)); + if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 2) || Objects.equals(existStatus, 0)) { + val.put(date, SchedulesDayVo.builder().isExist(existStatus).attendanceTypeOfApply(attendanceTypeOfApply.stream().distinct().collect(Collectors.toList())).isLineSchedule(isLineScheduleDay).totalDuration(0).isSeal(dayIsSeal).build()); + return; + } + if (Objects.equals(existStatus, 1)) { + BigDecimal used = dayRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) && Objects.nonNull(vo.getPeriodWorkDay())).map(FtbAttendanceDailyRule::getPeriodWorkDay).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + usedAll.set(usedAll.get().add(used)); + } + boolean isContainsOrdinary = dayRules.stream().anyMatch(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType())); + Date amMaxDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getOutPoint()) && Objects.equals(dayRule.getSchedulesType(), 1)).map(FtbAttendanceDailyRule::getOutPoint).min(Comparator.comparing(Date::getTime)).orElse(null); + Date pmMinDate = dayRules.stream().filter(dayRule -> Objects.nonNull(dayRule.getInPoint()) && Objects.equals(dayRule.getSchedulesType(), 2)).map(FtbAttendanceDailyRule::getInPoint).min(Comparator.comparing(Date::getTime)).orElse(null); + List schedulesItemVos = dayRules.stream().map(dayRule -> { + if (Objects.equals(attendanceGroupUserVo.getType(), 2) && StringUtil.equals(attendanceGroupUserVo.getGroupId(), groupId) && !StringUtil.equals(dayRule.getGroupId(), groupId) && Objects.equals(dayRule.getSelfGroup(), 2)) { + return null; + } + List attendanceClockInResults = clockInResult.get(dayRule.getId()); + ShiftNameVo shiftNameVo = JSONUtil.toBean(dayRule.getPeriodInfo(), ShiftNameVo.class); + AttendanceShiftNameEntity attendanceShiftNameEntity = shiftMap.getOrDefault(dayRule.getShiftId(), new AttendanceShiftNameEntity()); + Integer type = getType2(dayRule, attendanceTypeOfApply, attendanceClockInResults); + if (Objects.isNull(type) || AttendanceTypeEnum.LEAVE.getCode().equals(dayRule.getAttendanceType()) && (isContainsOrdinary || Objects.equals(dayRule.getApplyViewEnable(), 2))) { + return null; + } + if (Objects.equals(dayRule.getFixedMark(), 2)) { + return SchedulesItemVo.builder().isExist(1).id(dayRule.getId()).inPoint(dayRule.getInPoint()).start(getDate2Str(dayRule.getOriginInPoint())).isStartTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginInPoint())).end(getDate2Str(dayRule.getOriginOutPoint())).isEndTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginOutPoint())).validDuration(dayRule.getOriginValidDuration()).type(SchedulesTypeEnum.NORMAL.getCode()).name("划线排班").sort(dayRule.getSort()).selfGroup(dayRule.getSelfGroup()).build(); + } + if (CollUtil.isNotEmpty(attendanceClockInResults) || Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), dayRule.getAttendanceType())) { + isValidClock.set(Boolean.TRUE); + } + SchedulesItemVo build1 = SchedulesItemVo.builder().isExist(1).id(dayRule.getId()).schedulesType(dayRule.getSchedulesType()).sort(dayRule.getSort()).shiftId(dayRule.getShiftId()).selfGroup(attendanceGroupUserVo.getType()).type(type).isShow(1).start(getDate2Str(dayRule.getOriginInPoint())).isStartTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginInPoint())).end(getDate2Str(dayRule.getOriginOutPoint())).isEndTomorrow(isTomorrow(dayRule.getDay(), dayRule.getOriginOutPoint())).validDuration(dayRule.getValidDuration()).name(!Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && !Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) ? "" : StringUtil.isNotEmpty(shiftNameVo.getName()) ? shiftNameVo.getName() : attendanceShiftNameEntity.getName()).shortName(!Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && !Objects.equals(dayRule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) ? "" : StringUtil.isNotEmpty(shiftNameVo.getShortName()) ? shiftNameVo.getShortName() : attendanceShiftNameEntity.getShortName()).colour(Objects.equals(SchedulesTypeEnum.HOLIDAYS.getCode(), type) || Objects.equals(SchedulesTypeEnum.PAID_HOLIDAYS.getCode(), type) ? HOLIDAYS_COLOR : StringUtil.isNotEmpty(shiftNameVo.getColour()) ? shiftNameVo.getColour() : attendanceShiftNameEntity.getColour()).build(); + setItemSortInput(dayRule, build1, pmMinDate, amMaxDate); + return build1; + }).filter(Objects::nonNull).filter(rule -> !Objects.equals(rule.getType(), 0)) + //inpoint为空的数据按照schedulesType排序 + .sorted(Comparator.comparing(SchedulesItemVo::getInPoint)).collect(Collectors.toList()); + schedulesItemVos.stream().filter(item -> Objects.equals(SchedulesTypeEnum.REST.getCode(), item.getType())).skip(1).forEach(vo -> vo.setType(null)); + int size = schedulesItemVos.size(); + List collect6 = schedulesItemVos.stream().filter(vo -> !Objects.equals(SchedulesTypeEnum.LEAVE.getCode(), vo.getType())).collect(Collectors.toList()); + //存在请假且班次数量大于2,隐藏请假 + schedulesItemVos.stream().skip(collect6.isEmpty() ? 1 : 0).forEach(item -> { + if (size > 1 && Objects.equals(SchedulesTypeEnum.LEAVE.getCode(), item.getType())) { + item.setType(null); + } + }); + int isShow = Objects.nonNull(attendanceGroupUserVo.getRemoveTime()) && attendanceGroupUserVo.getRemoveTime().before(day) ? -1 : 0; + schedulesItemVos = schedulesItemVos.stream().filter(Objects::nonNull).filter(item -> Objects.nonNull(item.getType())).collect(Collectors.toList()); + //隐藏同班次多时段数据 + schedulesItemVos.stream().filter(vo -> StringUtil.isNotEmpty(vo.getName())).collect(Collectors.groupingBy(SchedulesItemVo::getName)).forEach((name, items) -> { + if (StringUtil.isEmpty(name) || CollUtil.isEmpty(items) || items.size() < 2) { + return; + } + items.stream().skip(1).forEach(lastItemVo -> { + lastItemVo.setName(""); + lastItemVo.setShortName(""); + lastItemVo.setIsShow(isShow); + }); + items.stream().collect(Collectors.groupingBy(SchedulesItemVo::getStart)).forEach((start1, items1) -> { + items1.stream().skip(1).forEach(lastItemVo -> { + lastItemVo.setStart(""); + lastItemVo.setEnd(""); + }); + }); + + }); + int totalDuration = schedulesItemVos.stream().map(SchedulesItemVo::getValidDuration).filter(Objects::nonNull).reduce(0, Integer::sum); + val.put(date, SchedulesDayVo.builder().isExist(existStatus).attendanceTypeOfApply(attendanceTypeOfApply.stream().distinct().collect(Collectors.toList())).itemVos(schedulesItemVos).isLineSchedule(isLineScheduleDay).totalDuration(totalDuration).isSeal(dayIsSeal).build()); + }); + AttendanceGroupUser attendanceGroupUser = groupUserVos.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(attendanceGroupUser)) { + return; + } + PartUserInfoVo partUserInfoVo = userMap.get(ruleUserId); + if (Objects.isNull(partUserInfoVo)) { + return; + } + int existStatus1 = SecondmentTypeUtil.toSecondmentType(isExistStatus(groupUserVos, start, end)); + SchedulesV2Vo build = SchedulesV2Vo.builder().userId(ruleUserId).IsSeal(stringBooleanMap.getOrDefault(ruleUserId, Boolean.FALSE)).sort(attendanceGroupUser.getSort()).total(balanceMap.getOrDefault(ruleUserId, BigDecimal.ZERO)).used(usedAll.get()).mobilePhone(partUserInfoVo.getMobilePhone()).realName(partUserInfoVo.getRealName()).delMark(attendanceGroupUserVo.getDeleteMark()).selfGroup(existStatus1).build(); + if (build != null && Objects.equals(build.getDelMark(), 1) && !isValidClock.get() && attendanceGroupUserVo.getRemoveTime().before(now)) { + return; + } + build.setVal(val); + schedulesVos.add(build); + }); + List result = schedulesVos.stream().sorted(Comparator.comparing(vo -> Objects.equals(vo.getDelMark(), 1) ? 20000 : Objects.isNull(vo.getSort()) ? 0 : vo.getSort())).collect(Collectors.toList()); + if (fillCanLineSchedule) { + fillCanLineScheduleByGroupConfig(groupId, result, infoByIds); + } + return result; + } + + private void setItemSortInput(FtbAttendanceDailyRule dayRule, SchedulesItemVo build1, Date pmMinDate, Date amMaxDate) { + if (Objects.nonNull(dayRule.getInPoint())) { + build1.setInPoint(dayRule.getInPoint()); + return; + } + //如果是首次数据,则设置inPoint为day + if (Objects.equals(dayRule.getSchedulesType(), 1)) { + build1.setInPoint(DateUtil.dateAddMinutes(pmMinDate, -1)); + return; + } + if (Objects.equals(dayRule.getSchedulesType(), 2)) { + build1.setInPoint(DateUtil.dateAddMinutes(amMaxDate, 1)); + return; + } + build1.setInPoint(dayRule.getDay()); + } + + @Override + public Map getShiftByShiftIds(List shiftIds) { + List attendanceShiftNameEntities = CollUtil.isEmpty(shiftIds) ? CollUtil.newArrayList() : attendanceShiftNameSettingService.listByIds(shiftIds); + return attendanceShiftNameEntities.stream().collect(Collectors.toMap(AttendanceShiftNameEntity::getId, entity -> entity)); + } + + /** + * 查询当日该用户是否存在于当前考勤组(-1离组 0未加入 1存在 2借调) + * + * @param users 该用户存在于当前考勤组的记录集合 + * @param date 当前日期 + * @return + */ + @Override + public Integer findUserIsExistsStatusByDay(List users, Date date) { + return AttendanceGroupUserStatusUtil.findUserIsExistsStatusByDay(users, date); + } + + @Override + public Integer findUserIsExistsByDay(List users, Date date) { + return AttendanceGroupUserStatusUtil.findUserIsExistsByDay(users, date); + } + + @Override + public Integer findUserIsExistsByDay(List users, Date start, Date end) { + return AttendanceGroupUserStatusUtil.findUserIsExistsByDay(users, start, end); + } + + @Override + public Map findUserIsExistsByUserList(List users, Date start, Date end) { + return AttendanceGroupUserStatusUtil.findUserIsExistsByUserList(users, start, end); + } + + @Override + public Integer isExistStatus(List users, Date start) { + return AttendanceGroupUserStatusUtil.isExistStatus(users, start); + } + + @Override + public Integer isExistStatus(List users, Date start, Date end) { + return AttendanceGroupUserStatusUtil.isExistStatus(users, start, end); + } + + @Override + public Boolean isInSecondment(List users, Date start, Date end) { + return AttendanceGroupUserStatusUtil.isInSecondment(users, start, end); + } + + @Override + public ScheduleRuleDetailVo getDetail(String id) throws HandleException { + FtbAttendanceDailyRule byId = getById(id); + if (Objects.isNull(byId)) { + throw new HandleException("未查询到当前排班的排班信息"); + } + List attendanceGroups = attendanceGroupService.queryListByIds(CollUtil.newArrayList(byId.getGroupId())); + if (CollUtil.isEmpty(attendanceGroups)) { + throw new HandleException("未查询到当前排班的考勤组信息"); + } + AttendanceGroup attendanceGroup = attendanceGroups.get(0); + if (Objects.isNull(attendanceGroup)) { + throw new HandleException("未查询到当前排班的考勤组信息"); + } + UserEntity infoById = userApi.getInfo(byId.getCreatorUserId()); + return ScheduleRuleDetailVo.builder().groupId(byId.getGroupId()).groupName(attendanceGroup.getDetailName()).attendanceType(byId.getAttendanceType()).createId(byId.getUserId()).createName(infoById.getRealName()).createTime(DateDetail.getDateTime2Str(byId.getCreatorTime())).day(DateDetail.getDate2Str(byId.getDay(), DateDetail.DF)).build(); + } + + @Override + @Transactional + public void fixedPeriodChange(List groupIds, AttendanceShiftSettingVo periodList, Date enableTime) { + List attendanceGroups = attendanceGroupService.queryDropList(); + Date end = cn.hutool.core.date.DateUtil.endOfMonth(enableTime); + List attendanceGroupUserAllVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(enableTime, end, null, groupIds); + Map shiftSettingMap = Maps.newHashMap(); + groupIds.forEach(groupId -> shiftSettingMap.put(groupId, periodList)); + int day = cn.hutool.core.date.DateUtil.dayOfMonth(new Date()); + if (day >= 20) { + end = cn.hutool.core.date.DateUtil.endOfMonth(DateUtil.dateAddMonths(enableTime, 1)); + } + FixedHandleDTO build = FixedHandleDTO.builder().groupIds(groupIds).attendanceGroupVos(attendanceGroups).users(attendanceGroupUserAllVos).shiftSettingMap(shiftSettingMap).tenantId(UserProvider.getUser().getTenantId()).start(enableTime).end(end).build(); + fixedHandle(build); + //处理节日自动排休 + addHolidayDailyRule(build); + } + + @Override + public void addUserFixedHandle(String groupId, List userIds) { + addUserFixedHandle(groupId, null, userIds); + } + + @Override + @Transactional + public void addUserFixedHandle(String groupId, String tenantId, List userIds) { + List groupIds = CollUtil.newArrayList(groupId); + List attendanceGroups = attendanceGroupService.queryDropList(); + Map shiftSettingMap = attendanceShiftSettingService.getEnableShiftSetting(groupIds, attendanceGroups); + AttendanceShiftSettingVo attendanceShiftSettingVo = shiftSettingMap.get(groupId); + if (Objects.isNull(attendanceShiftSettingVo)) { + + log.error("未查询到当前考勤组可用班制配置{}", groupId); + statisticsNotificationNail(groupId, userIds, tenantId); + return; + } + if (Objects.equals(attendanceShiftSettingVo.getSystemType(), 2)) { + log.error("非当前考勤组非固定班,不生成固定班规则{}", groupId); + statisticsNotificationNail(groupId, userIds, tenantId); + return; + } + Date end = DateUtil.getEndDayOfMonth(); + Date start = attendanceShiftSettingVo.getEnableTime(); + int day = cn.hutool.core.date.DateUtil.dayOfMonth(new Date()); + if (day >= 20) { + end = cn.hutool.core.date.DateUtil.endOfMonth(DateUtil.dateAddMonths(new Date(), 1)).toJdkDate(); + } + List attendanceGroupUserAllVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, userIds, groupIds); + //处理节日自动排休 + FixedHandleDTO build = FixedHandleDTO.builder().groupIds(groupIds).attendanceGroupVos(attendanceGroups).users(attendanceGroupUserAllVos).shiftSettingMap(shiftSettingMap).tenantId(tenantId).start(start).end(end).build(); + fixedHandle(build); + addHolidayDailyRule(build); + } + + private void statisticsNotificationNail(String groupId, List userIds, String tenantId) { + attendanceRuleNotificationHandle.notificationStatistics(tenantId, groupId, new Date(), userIds); + } + + private Integer isTomorrow(Date day, Date date) { + if (Objects.isNull(date)) { + return 1; + } + Date date1 = DateUtil.dateAddDays(day, 1); + if (date1.before(date)) { + return 2; + } + return 1; + } + + /** + * 自动排休处理 + * + * @param fixedHandleDTO fixedHandleDTO + */ + private void addHolidayDailyRule(FixedHandleDTO fixedHandleDTO) { + Map enableBaseSetting = CollUtil.isNotEmpty(fixedHandleDTO.getEnableBaseSetting()) ? fixedHandleDTO.getEnableBaseSetting() : attendanceBaseSettingService.getEnableBaseSetting(fixedHandleDTO.getGroupIds(), fixedHandleDTO.getAttendanceGroupVos()); + List attendanceGroupUserAllVos = attendanceGroupUserService.queryByUsersGroupIds(fixedHandleDTO.getGroupIds()); + //查询节日规则 + Map> festivalMap = CollUtil.isNotEmpty(fixedHandleDTO.getFestivalMap()) ? fixedHandleDTO.getFestivalMap() : attendanceFestivalRulesService.batchGetFestivalRulesByUserIds(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd(), attendanceGroupUserAllVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList())); + Map> userMap = attendanceGroupUserAllVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + Map shiftMap = CollUtil.isNotEmpty(fixedHandleDTO.getShiftSettingMap()) ? fixedHandleDTO.getShiftSettingMap() : attendanceShiftSettingService.getEnableShiftSetting(fixedHandleDTO.getGroupIds(), fixedHandleDTO.getAttendanceGroupVos()); + List userIds = attendanceGroupUserAllVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + List hisAllRules = getDailyRulesByPeriod(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd(), userIds, fixedHandleDTO.getGroupIds()); + Map> hisRulesMap = hisAllRules.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId())); + if (CollUtil.isEmpty(shiftMap)) { + log.error("自动排休未查询到所有考勤组班制配置"); + return; + } + addHolidayDailyRule(fixedHandleDTO.getGroupIds(), fixedHandleDTO.getTenantId(), enableBaseSetting, CollUtil.newArrayList(), festivalMap, userMap, shiftMap, hisRulesMap, CollUtil.newArrayList(), CollUtil.newArrayList()); + } + + /** + * 自动排休处理 + * + * @param start 开始时间 + * @param end 结束时间 + * @param attendanceGroupVos 考勤组集合 + * @param festivalSetting 调整后节日配置 + * @param hisFestivalSetting 调整前节日配置 + */ + /** + * 处理节日规则更新的日常考勤规则 + * + * @param start 开始时间 + * @param end 结束时间 + * @param attendanceGroupVos 考勤组集合 + * @param festivalSetting 调整后节日配置 + * @param hisFestivalSetting 调整前节日配置 + * @param newUserIds 新的用户ID列表 + * @param oldUserIds 旧的用户ID列表 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void addHolidayDailyRule(Date start, Date end, List attendanceGroupVos, AttendanceFestivalRules festivalSetting, AttendanceFestivalRules hisFestivalSetting, List newUserIds, List oldUserIds) { + // 1. 获取所有考勤组的班制配置 + Map shiftMap = attendanceShiftSettingService.getEnableShiftSetting(attendanceGroupVos.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList()), attendanceGroupVos); + + // 2. 检查是否获取到班制配置 + if (CollUtil.isEmpty(shiftMap)) { + log.error("自动排休未查询到所有考勤组班制配置"); + return; + } + + // 3. 过滤出固定班的班制配置(systemType=1) + shiftMap.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()) || Objects.equals(entry.getValue().getSystemType(), 2)); + + // 4. 检查是否有固定班的班制配置 + if (CollUtil.isEmpty(shiftMap)) { + log.error("自动排休未查询到所有固定班考勤组配置"); + return; + } + + // 5. 获取新用户的考勤组信息(包括借调情况) + List attendanceGroupUserAllVos = CollUtil.isEmpty(newUserIds) ? Lists.newArrayList() : attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, newUserIds, new ArrayList<>(shiftMap.keySet())); + + // 6. 获取新用户所在的考勤组ID列表 + List groupIds = attendanceGroupUserAllVos.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList()); + + // 7. 构建用户ID到节日配置的映射 + Map> festivalMap = attendanceGroupUserAllVos.stream().collect(Collectors.toMap(AttendanceGroupUser::getUserId, vo -> Objects.isNull(festivalSetting) ? List.of() : List.of(festivalSetting), (r1, r2) -> r1)); + + // 8. 过滤出固定班的用户 + attendanceGroupUserAllVos.removeIf(vo -> !shiftMap.containsKey(vo.getGroupId()) || Objects.equals(shiftMap.get(vo.getGroupId()).getSystemType(), 2)); + + // 9. 获取旧用户的考勤组信息(包括借调情况) + List oldGroupUserAll = CollUtil.isEmpty(oldUserIds) ? Lists.newArrayList() : attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, oldUserIds, new ArrayList<>(shiftMap.keySet())); + + // 10. 过滤出固定班的旧用户 + oldGroupUserAll.removeIf(vo -> !shiftMap.containsKey(vo.getGroupId()) || Objects.equals(shiftMap.get(vo.getGroupId()).getSystemType(), 2)); + + // 11. 获取旧用户所在的考勤组ID列表 + List oldGroupIds = oldGroupUserAll.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList()); + + // 12. 构建旧用户的考勤组映射 + Map> oldUserMap = oldGroupUserAll.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + + // 13. 合并新用户和旧用户的考勤组ID + List allGroupIds = new ArrayList<>(groupIds); + allGroupIds.addAll(oldGroupIds); + allGroupIds = allGroupIds.stream().distinct().collect(Collectors.toList()); + + // 14. 获取基础配置(包含新用户和旧用户的考勤组) + Map enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(allGroupIds, attendanceGroupVos); + + // 15. 准备变量 + List allRules = CollUtil.newArrayList(); + List allRuleIds = CollUtil.newArrayList(); + List resultList = CollUtil.newArrayList(); + + // 16. 合并新用户和旧用户的ID + List allUserIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(newUserIds)) { + allUserIds.addAll(newUserIds); + } + if (CollUtil.isNotEmpty(oldUserIds)) { + allUserIds.addAll(oldUserIds); + } + allUserIds = allUserIds.stream().distinct().collect(Collectors.toList()); + + // 17. 获取历史规则(查询所有用户的,修复逻辑缺陷) + List hisAllRules = getDailyRulesByPeriod(start, end, allUserIds, allGroupIds); + + // 18. 构建历史规则映射 + Map> hisRulesMap = hisAllRules.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId())); + + // 19. 清除历史规则 + clearHisRuleByUpdateHoliday(oldGroupIds, hisFestivalSetting, enableBaseSetting, resultList, oldUserMap, shiftMap, hisRulesMap, allRules, allRuleIds); + + // 20. 构建新用户的考勤组映射 + Map> userMap = attendanceGroupUserAllVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + + // 21. 添加新的节日规则 + addHolidayDailyRule(groupIds, UserProvider.getUser().getTenantId(), enableBaseSetting, resultList, festivalMap, userMap, shiftMap, hisRulesMap, allRules, allRuleIds); + } + + /** + * 清除修改的节日历史规则 + * + * @param groupIds 考勤组id + * @param hisFestivalSetting 历史节日配置 + * @param enableBaseSetting 当前组使用的基础配置 + * @param resultList 返回结果 + * @param userMap 考勤组用户集合 + * @param shiftMap 班制配置集合 + * @param hisRulesMap 历史规则集合 + */ + private void clearHisRuleByUpdateHoliday(List groupIds, AttendanceFestivalRules hisFestivalSetting, Map enableBaseSetting, List resultList, Map> userMap, Map shiftMap, Map> hisRulesMap, List allRules, List allRuleIds) { + //清除历史该节日规则 + if (Objects.isNull(hisFestivalSetting)) { + return; + } + if (StringUtil.isEmpty(hisFestivalSetting.getFestivalDate())) { + return; + } + List rules = CollUtil.newArrayList(); + groupIds.forEach(groupId -> { + AttendanceShiftSettingVo nextShiftSetting = shiftMap.get(groupId); + if (Objects.isNull(nextShiftSetting) || !Objects.equals(nextShiftSetting.getSystemType(), 1)) { + log.error("自动排休当请求为固定班规则生成,过滤非固定班配置{}", groupId); + return; + } + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId); + BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio(); + List dateList = Arrays.stream(hisFestivalSetting.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()); + List compensateInDate = StringUtil.isEmpty(hisFestivalSetting.getCompensateInDate()) ? CollUtil.newArrayList() : Arrays.stream(hisFestivalSetting.getCompensateInDate().split(",")).map(date -> DateDetail.getStr2Date10(date)).collect(Collectors.toList()); + dateList.addAll(compensateInDate); + List fixedClassShiftVos = nextShiftSetting.getFixedPeriods(); + Map collect = fixedClassShiftVos.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, fixedClassShift -> fixedClassShift)); + Map> applyCrossRules = Maps.newHashMap(); + List attendanceGroupUserAllVos = userMap.getOrDefault(groupId, List.of()); + attendanceGroupUserAllVos.forEach(user -> { + applyCrossRules.clear(); + dateList.forEach(date -> { + DateTime end = cn.hutool.core.date.DateUtil.endOfDay(date); + String userGroupDay = DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId(); + List hisRules = hisRulesMap.getOrDefault(userGroupDay, CollUtil.newArrayList()); + //将夸日的申请规则添加到当前日的规则中方便计算 + applyCrossRulesAdd(hisRules, applyCrossRules, end, date, allRules); + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + rules.clear(); + int currentWeekNumber = DateDetail.getCurrentWeekNumber(date); + FixedClassShiftVo fixedClassShiftVo = collect.get(currentWeekNumber); + if (Objects.isNull(fixedClassShiftVo)) { + return; + } + List shiftNameVos = fixedClassShiftVo.getShiftNameVos(); + if (CollUtil.isEmpty(shiftNameVos)) { + return; + } + ShiftNameVo ordShift = shiftNameVos.stream().filter(vo -> Objects.equals(vo.getType(), 2)).findFirst().orElse(null); + shiftNameVos.forEach(shiftName -> { + if (Objects.isNull(shiftName.getType())) { + return; + } + if (Objects.equals(shiftName.getType(), 1)) { + buildDailyRule(user, date, rules, shiftName.getFixedSort(), Objects.nonNull(ordShift) ? ordShift.getPeriods() : null, nextShiftSetting, AttendanceTypeEnum.REST, attendanceRatio, 4); + } else { + buildDailyRule(user, date, rules, shiftName.getFixedSort(), shiftName.getPeriods(), nextShiftSetting, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 4); + } + }); + + dailyRuleArrangementHandle(resultList, hisRules, rules); + secondmentDailyRulesHandle(resultList, user, hisRules); + //将夸日的申请规则添加到当前日的规则中方便计算 + applyCrossRulesAddNotCurrent(hisRules, applyCrossRules, end, date); + hisRulesMap.put(userGroupDay, hisRules); + allRuleIds.addAll(ruleIds); + }); + }); + }); + } + + private void addHolidayDailyRule(List groupIds, String tenantId, Map enableBaseSetting, List resultList, Map> festivalMap, Map> userMap, Map shiftMap, Map> hisRulesMap, List allRules, List allRuleIds) { + Map> applyCrossRules = Maps.newHashMap(); + List rules = CollUtil.newArrayList(); + for (String groupId : groupIds) { + AttendanceShiftSettingVo nextShiftSetting = shiftMap.get(groupId); + if (Objects.isNull(nextShiftSetting)) { + log.error("自动排休未查询到当前考勤组[{}]班制配置", groupId); + return; + } + if (!Objects.equals(nextShiftSetting.getSystemType(), 1)) { + log.error("自动排休当请求为固定班规则生成时,过滤非固定班配置{}", groupId); + return; + } + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId); + BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio(); + List attendanceGroupUserVos = userMap.getOrDefault(groupId, CollUtil.newArrayList()); + attendanceGroupUserVos.forEach(user -> { + try { + List festivals = festivalMap.get(user.getUserId()); + if (CollUtil.isEmpty(festivals)) { + log.error("自动排休未查询到当前人员[{}]节日配置", user.getUserId()); + return; + } + festivals.forEach(festival -> { + applyCrossRules.clear(); + if (StringUtil.isNotEmpty(festival.getFestivalDate())) { + List dateList = Arrays.stream(festival.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()); + dateList.forEach(date -> { + Date start = cn.hutool.core.date.DateUtil.beginOfDay(date); + Date end = cn.hutool.core.date.DateUtil.endOfDay(date); + if (!Objects.equals(user.getUserGroupType(), 2) && (Objects.isNull(user.getCreatorTime()) ? Boolean.FALSE : end.compareTo(user.getCreatorTime()) < 0 || (Objects.isNull(user.getRemoveTime()) ? Boolean.FALSE : user.getRemoveTime().compareTo(start) < 0))) { + return; + } + String key = DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId(); + List hisRules = hisRulesMap.getOrDefault(key, CollUtil.newArrayList()); + hisRulesMap.remove(key); + //将夸日的申请规则添加到当前日的规则中方便计算 + applyCrossRulesAdd(hisRules, applyCrossRules, end, date, allRules); + rules.clear(); + buildDailyRule(user, date, rules, 0, null, null, AttendanceTypeEnum.REST, attendanceRatio, 3); + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + dailyRuleArrangementHandle(resultList, hisRules, rules); + secondmentDailyRulesHandle(resultList, user, hisRules); + fillInNailRuleForOnlyStepOut(hisRules); + hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> { + OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId()); + ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo)); + }); + //将夸日的申请规则添加到当前日的规则中方便计算 + applyCrossRulesAddNotCurrent(hisRules, applyCrossRules, end, date); + allRules.addAll(hisRules); + allRuleIds.addAll(ruleIds); + }); + } + }); + } catch (Exception e) { + log.error("自动排休发生异常{}{}", user, e); + } + }); + + } + saveOrUpdateBatch(allRules.stream().distinct().collect(Collectors.toList()), allRuleIds); + attendanceRuleNotificationHandle.sendSystemNotice(resultList, tenantId); + attendanceRuleNotificationHandle.notificationHandle(resultList, tenantId); + } + + /** + * 查询打卡结果数据 + * + * @param ruleIds + * @return + */ + private List getClockInResult(List ruleIds) { + if (CollUtil.isEmpty(ruleIds)) { + return CollUtil.newArrayList(); + } + return attendanceClockInResultMapper.selectList(new LambdaQueryWrapper().in(AttendanceClockInResult::getRuleId, ruleIds).isNull(AttendanceClockInResult::getApplyId).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE)); + + } + + /** + * 自己排班调整排班 + * + * @param shiftId 班次id + * @return + */ + @Transactional + @Override + public String setSchedulesForSelfSchedules(String shiftId) throws HandleException { + //查询当前用户当前时间所在考勤组 + String loginUserId = UserProvider.getLoginUserId(); + Date date = new Date(); + List attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(date, date, List.of(loginUserId), null); + Assert.isFalse(CollUtil.isEmpty(attendanceGroupUsers), "未查询到当前时间所在考勤组"); + AttendanceGroupUser groupUser = attendanceGroupUsers.stream().findFirst().orElse(null); + //查询班次涉及考勤组 + AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findByGroupId(groupUser.getGroupId()); + Assert.isFalse(byGroupId.getSchedulingPeriods().stream().allMatch(vo -> !StringUtil.equals(vo.getId(), shiftId)), "所选班次非当前所在考勤组"); + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(date)).eq(FtbAttendanceDailyRule::getGroupId, groupUser.getGroupId()).eq(FtbAttendanceDailyRule::getUserId, loginUserId).in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 9, 10).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()).list(); + Assert.isFalse(CollUtil.isNotEmpty(list), "当前已存在排班,请重新进入打卡页面"); + SchedulesSetDto schedulesSetDto = new SchedulesSetDto(); + schedulesSetDto.setUserId(loginUserId); + schedulesSetDto.setGroupId(groupUser.getGroupId()); + Map val = Maps.newHashMap(); + val.put(DateDetail.getDate2Str(date, DateDetail.DF), SchedulesDayVo.builder().itemVos(List.of(new SchedulesItemVo() {{ + setShiftId(shiftId); + setSchedulesType(0); + setType(2); + }})).build()); + List schedulesDaySetDtos = List.of(new SchedulesItemVo() {{ + setId("1"); + setShiftId(shiftId); + setSchedulesType(0); + setType(2); + }}); + val.put(DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date, 1), DateDetail.DF), SchedulesDayVo.builder().itemVos(schedulesDaySetDtos).build()); + val.put(DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date, -1), DateDetail.DF), SchedulesDayVo.builder().itemVos(schedulesDaySetDtos).build()); + schedulesSetDto.setVal(val); + return setSchedulesHandle(CollUtil.newArrayList(schedulesSetDto), null, Boolean.TRUE, Boolean.FALSE, null); + } + + /** + * 根据班次配置更新排班日程信息 + * + * @param groupId 组织/集团ID + * @param mark 时间标记(1:当日,其他值:次日) + * @param periodConfigs 时段配置列表 + * @return 排班设置操作结果 + * @throws HandleException 业务处理异常 + */ + @Transactional + @Override + public Integer setSchedulesForShiftConfigUpdate(String groupId, Integer mark, List periodConfigs, List periodEntities) throws HandleException { + // 获取当日零点时间基准 + Date date = cn.hutool.core.date.DateUtil.beginOfDay(new Date()); + // 提取所有不重复的班次ID集合 + List shiftIds = periodConfigs.stream().map(PeriodConfig::getShiftId).distinct().collect(Collectors.toList()); + if (shiftIds.isEmpty()) { + return null; + } + + // 构建初始查询条件:根据时间标记确定查询起始日 + Date queryDate = Objects.equals(1, mark) ? date : cn.hutool.core.date.DateUtil.offsetDay(date, 1); + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, groupId).in(FtbAttendanceDailyRule::getShiftId, shiftIds).eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).ge(FtbAttendanceDailyRule::getDay, queryDate).list(); + + // 提取用户ID集合和日期集合进行二次精确查询 + List userIds = list.stream().map(FtbAttendanceDailyRule::getUserId).distinct().collect(Collectors.toList()); + List days = list.stream().map(FtbAttendanceDailyRule::getDay).distinct().collect(Collectors.toList()); + + if (userIds.isEmpty() || days.isEmpty()) { + return null; + } + list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, groupId).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getAttendanceType, List.of(AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode())).in(FtbAttendanceDailyRule::getDay, days).list(); + + // 按用户ID分组规则数据 + Map> collect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)); + + // 构建排班配置传输对象 + UpdateShiftConfigDto updateShiftConfigDto = new UpdateShiftConfigDto(); + updateShiftConfigDto.setPeriods(periodConfigs); + List schedulesSets = CollUtil.newArrayList(); + + // 遍历用户构建排班设置结构 + collect.forEach((userId, rules) -> { + SchedulesSetDto schedulesSetDto = new SchedulesSetDto(); + schedulesSetDto.setUserId(userId); + schedulesSetDto.setGroupId(groupId); + Map val = Maps.newTreeMap(); + // 按日期分组处理每日排班规则 + rules.stream().sorted(Comparator.comparing(FtbAttendanceDailyRule::getDay)).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay, TreeMap::new, Collectors.toList())).forEach((day, rules1) -> { + List schedulesDaySetDtos = CollUtil.newArrayList(); + rules1.stream().collect(Collectors.toMap(vo -> Objects.isNull(vo.getSchedulesType()) ? 0 : vo.getSchedulesType(), Function.identity(), (r1, r2) -> r1, TreeMap::new)).forEach((schedulesType, rule) -> { + SchedulesItemVo dto = new SchedulesItemVo(); + dto.setShiftId(rule.getShiftId()); + dto.setSchedulesType(schedulesType); + dto.setType(getAttendanceType2type(rule.getAttendanceType())); + schedulesDaySetDtos.add(dto); + }); + val.put(DateDetail.getDate2Str(day, DateDetail.DF), SchedulesDayVo.builder().itemVos(schedulesDaySetDtos).build()); + }); + List strings = CollUtil.newArrayList(val.keySet()); + strings.forEach(day -> { + DateTime date1 = cn.hutool.core.date.DateUtil.parse(day, DateDetail.DF); + String yesterday = DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date1, -1), DateDetail.DF); + String nextDay = DateDetail.getDate2Str(cn.hutool.core.date.DateUtil.offsetDay(date1, 1), DateDetail.DF); + if (!val.containsKey(yesterday)) { + val.put(yesterday, SchedulesDayVo.builder().itemVos(List.of(new SchedulesItemVo() {{ + setId("1"); + setShiftId("1"); + setSchedulesType(0); + setType(2); + }})).isExist(1).build()); + } + if (!val.containsKey(nextDay)) { + val.put(nextDay, SchedulesDayVo.builder().itemVos(List.of(new SchedulesItemVo() {{ + setId("1"); + setShiftId("1"); + setSchedulesType(0); + setType(2); + }})).isExist(1).build()); + } + }); + schedulesSetDto.setVal(val); + schedulesSets.add(schedulesSetDto); + }); + setSchedulesHandle(schedulesSets, updateShiftConfigDto, Boolean.FALSE, Boolean.TRUE, periodEntities); + // 调用排班设置核心处理方法 + return updateShiftConfigDto.getCode(); + } + + /** + * 调整排班 + * + * @param schedulesSets + * @return + */ + @Override + @Transactional + public String setSchedules(List schedulesSets) throws HandleException { + return setSchedulesHandle(schedulesSets, null, Boolean.FALSE, Boolean.FALSE, null); + } + + /** + * 统一排班接口(支持固定排班和划线排班) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String setUnifiedSchedules(UnifiedSchedulesDto dto) throws HandleException { + // 1. 参数验证 + Assert.isFalse(Objects.isNull(dto), "未查询到排班配置数据!"); + Assert.isFalse(StringUtil.isEmpty(dto.getGroupId()), "考勤组ID不能为空!"); + Assert.isFalse(CollUtil.isEmpty(dto.getUserDaySchedules()), "未找到需要排班的用户数据!"); + // 3. 调用高性能内部方法 + return setUnifiedSchedulesInternal(dto); + } + + private Boolean check24H(List rules) { + // 一个时段的时候不可能超过24小时不做校验 校验上班时间不超过24小时 规则:第二个时段的下班时间有没有超过第一个上班时间的24小时 并校验 下班时间不可超过上班时间的24小时! + // 9:00 -- 18:00 19:00 -- 次日 9:59 + Date inPoint = rules.stream().map(FtbAttendanceDailyRule::getInPoint).filter(Objects::nonNull).reduce((r1, r2) -> r1.before(r2) ? r1 : r2).orElse(null); + Date outPoint = rules.stream().map(FtbAttendanceDailyRule::getOutPoint).filter(Objects::nonNull).reduce((r1, r2) -> r1.after(r2) ? r1 : r2).orElse(null); + if (Objects.isNull(inPoint) || Objects.isNull(outPoint)) { + return Boolean.FALSE; + } + if (outPoint.getTime() - inPoint.getTime() > 86400000L) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + /** + * 调整排班 + * + * @param schedulesSets + * @return + */ + private String setSchedulesHandle(List schedulesSets, UpdateShiftConfigDto updateShiftConfigDto, Boolean isSync, Boolean isConfigUpdate, List list1) throws HandleException { + if (CollUtil.isEmpty(schedulesSets)) { + return ""; + } + SchedulesSetDto schedulesSetDto = schedulesSets.stream().findFirst().orElse(null); + // 确保schedulesSetDto不为空,否则抛出异常 + Assert.isFalse(Objects.isNull(schedulesSetDto), "未查询到排班数据!"); + // 获取当前考勤组的排班锁 + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), schedulesSetDto.getGroupId())); + // 确保锁未被其他操作持有,避免并发执行 + Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中,请稍后再试"); + // 获取用户级别的排班锁列表 + List rLocks = setSchedulesLockByUsers(schedulesSets); + // 尝试获取排班锁 + try { + Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中,请稍后再试"); + // 初始化排班日期列表和班次ID列表 + List dayList = CollUtil.newArrayList(); + List shiftIds = CollUtil.newArrayList(); + // 遍历排班设置,收集所有排班日期和班次ID + schedulesSets.forEach(dto -> { + dayList.addAll(dto.getVal().keySet()); + dto.getVal().forEach((k, v) -> { + shiftIds.addAll(v.getItemVos().stream().map(SchedulesItemVo::getShiftId).collect(Collectors.toList())); + }); + }); + + // 将排班日期去重并排序 + List collect = dayList.stream().distinct().map(this::getStr2Date).sorted(Comparator.comparing(v -> v)).collect(Collectors.toList()); + // 确保有排班日期,否则抛出异常 + Assert.isFalse(CollUtil.isEmpty(collect), "无排班数据提交!"); + // 获取排班日期范围的起始和结束日期 + Date start = collect.stream().findFirst().orElse(null); + Date end = collect.stream().max(Comparator.comparing(v -> v)).orElse(null); + // 收集所有用户的ID和考勤组ID + // 收集所有存在空ID排班项的用户ID + List userIds = schedulesSets.stream().filter(this::hasBlankIdItem).map(SchedulesSetDto::getUserId).distinct().collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return ""; + } + Map collect4 = schedulesSets.stream().collect(Collectors.toMap(SchedulesSetDto::getUserId, Function.identity(), (r1, r2) -> r2)); + List groupIds = schedulesSets.stream().map(SchedulesSetDto::getGroupId).distinct().collect(Collectors.toList()); + // 查询所有用户的考勤组信息 + List users = attendanceGroupUserService.queryAllByUsersIds(userIds); + // 确保查询到用户的考勤组信息,否则抛出异常 + Assert.isFalse(CollUtil.isEmpty(users), "未查询到当前选择用户所属考勤组!"); + // 按用户和考勤组分组 + Map> userMap = users.stream().collect(Collectors.groupingBy(user -> user.getGroupId() + user.getUserId())); + // 查询所有考勤组信息 + List attendanceGroupVos = attendanceGroupService.queryDropList(); + // 确保查询到考勤组信息,否则抛出异常 + Assert.isFalse(CollUtil.isEmpty(attendanceGroupVos), "未查询到考勤组信息!"); + // 查询指定日期范围内所有用户的排班规则 + List hisAllRules = getDailyRulesByPeriod(start, cn.hutool.core.date.DateUtil.endOfDay(end), userIds, groupIds); + Map> acrossTheMoonUserLeaveRuleMap = hisAllRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)); + // 按日期、考勤组和用户分组 + Map> collect1 = hisAllRules.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId())); + // 查询所有考勤组的有效班制设置 + Map allShiftMap = attendanceShiftSettingService.getEnableShiftSetting(groupIds, attendanceGroupVos); + // 确保查询到班制设置,否则抛出异常 + Assert.isFalse(CollUtil.isEmpty(allShiftMap), "未查询到所有考勤组班制配置!"); + // 获取当前考勤组的班制设置 + AttendanceShiftSettingVo attendanceShiftSettingVo = allShiftMap.get(groupIds.get(0)); + // 使用传入的list1或执行数据库查询 + List attendanceShiftSettingPeriodEntities = Objects.isNull(attendanceShiftSettingVo) ? CollUtil.newArrayList() : attendanceShiftSettingVo.getSchedulingPeriods().stream().filter(vo -> shiftIds.contains(vo.getId())).flatMap(shiftNameVo -> shiftNameVo.getPeriods().stream()).collect(Collectors.toList()); + // 按班次ID分组 + Map> periodMap = attendanceShiftSettingPeriodEntities.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodVo::getShiftId)); + // 确保当前考勤组的班制设置存在,否则抛出异常 + Assert.isFalse(Objects.isNull(attendanceShiftSettingVo), "未查询到当前考勤组班制配置!"); + // 初始化排班结果列表 + List resultList = new CopyOnWriteArrayList<>(); + // 查询所有考勤组的有效基础设置 + Map enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(groupIds); + // 初始化排班规则列表和规则ID列表 + List allRules = new CopyOnWriteArrayList<>(); + List allRuleIds = new CopyOnWriteArrayList<>(); + // 查询所有适用的排班规则 + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, schedulesSetDto.getGroupId()).between(FtbAttendanceDailyRule::getDay, start, cn.hutool.core.date.DateUtil.endOfDay(end)).eq(FtbAttendanceDailyRule::getApplyViewEnable, 3).list(); + // 按用户和日期分组 + Map> collect3 = list.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))); + // 查询所有用户的余额信息 + List usersBalance = publicHolidayRulesService.getBalanceList(cn.hutool.core.date.DateUtil.format(start, "yyyy-MM"), userIds); + Map stringBooleanMap = attendanceDayStatisticsService.selectUserIsSeal(userIds, DateUtil.dateToString(start, "yyyy-MM")); + // 将用户余额信息转换为Map,便于后续查询 + Map totalMap = usersBalance.stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, AttendancePublicHolidayBalance::getTotal, (r1, r2) -> r2)); + AtomicReference balance = new AtomicReference<>(BigDecimal.ZERO); + List> futures = CollUtil.newArrayList(); + // 遍历每个排班设置,进行排班逻辑处理 + for (String userId : userIds) { + SchedulesSetDto dto = collect4.get(userId); + futures.add(CompletableFuture.runAsync(ThreadContext.wrap(() -> { + // 初始化排班冲突规则列表 + // 初始化一个列表,用于存储时段冲突的考勤规则 + List periodConflictRules = CollUtil.newArrayList(); + List rules = CollUtil.newArrayList(); + String groupId = dto.getGroupId(); + // 获取用户余额信息 + BigDecimal total = totalMap.getOrDefault(userId, BigDecimal.ZERO); + // 初始化用户的公休余额 + balance.set(total); + // 获取考勤组的基础设置 + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(groupId); + // 获取考勤比例,默认为8小时 + BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio(); + // 获取用户考勤组信息列表 + List userList = userMap.getOrDefault(groupId + userId, CollUtil.newArrayList()); + // 获取调度任务的值,类型为Map,键为字符串,值为SchedulesDaySetDto对象的列表 + Map val = dto.getVal(); + List acrossTheMoonLeaveRuleList = acrossTheMoonUserLeaveRuleMap.getOrDefault(userId, CollUtil.newArrayList()); + // 遍历Map中的每天排班数据 + for (Map.Entry stringListEntry : val.entrySet()) { + // 获取日期字符串 + String day = stringListEntry.getKey(); + // 获取该日期对应的考勤规则对象列表 + List items = stringListEntry.getValue().getItemVos(); + // 将日期字符串转换为Date对象 + Date date = getStr2Date(day); + Date yesterday = cn.hutool.core.date.DateUtil.offsetDay(date, -1); + // 获取当天结束时间的Date对象 + Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date); + // 清空rules列表,减少重复创建集合 + rules.clear(); + // 根据日期和用户信息获取历史考勤规则 + List hisRules = collect1.getOrDefault(DateDetail.getDateTime2Str(date) + groupId + userId, CollUtil.newArrayList()); + //将夸日的申请规则添加到当前日的规则中方便计算 + // 获取所有非空的班次ID列表 + List collect2 = items.stream().map(SchedulesItemVo::getShiftId).filter(StringUtil::isNotEmpty).collect(Collectors.toList()); + // 根据班次ID获取对应的考勤时段设置 + List periods = CollUtil.isNotEmpty(collect2) ? periodMap.get(collect2.get(0)) : CollUtil.newArrayList(); + // 获取每天排班规则数量 + int size = items.size(); + // 获取每天排班规则中的第一个规则 + Integer fixedMark = attendanceShiftSettingVo.getSystemType(); + // 根据用户信息和日期获取最新的用户组信息 + AttendanceGroupUser user = userList.stream().filter(user1 -> user1.getUserGroupType() == 2 ? Boolean.TRUE : endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.before(cn.hutool.core.date.DateUtil.endOfDay(user1.getRemoveTime())))).reduce((r1, r2) -> r2).orElse(null); + // 如果用户组信息为空,则跳过当前循环 + if (Objects.isNull(user) || Objects.nonNull(user.getRemoveTime()) && date.after(user.getRemoveTime()) || date.before(cn.hutool.core.date.DateUtil.beginOfDay(user.getCreatorTime()))) { + continue; + } + batchBuildDailyRule(items, periodMap, balance, stringBooleanMap, userId, hisRules, size, user, date, rules, attendanceShiftSettingVo, groupId, periods, attendanceRatio); + // 如果rules列表为空,则将历史考勤规则添加到periodConflictRules列表中 + if (CollUtil.isEmpty(rules)) { + // 判断时段是否冲突 + periodConflictRules.addAll(hisRules); + boolean isPeriodConflict = !fixedPeriodConflict(periodConflictRules); + if (!isConfigUpdate) { + Assert.isFalse(isPeriodConflict, "选择的时段有冲突,请重新选择"); + continue; + } + //修改班次配置给定前端返回值 + if (isPeriodConflict) { + if (allRules.stream().anyMatch(rule -> date.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) { + allRules.removeAll(allRules.stream().filter(rule -> date.compareTo(rule.getDay()) == 0).collect(Collectors.toList())); + } + if (allRules.stream().anyMatch(rule -> yesterday.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) { + allRules.removeAll(allRules.stream().filter(rule -> yesterday.compareTo(rule.getDay()) == 0).collect(Collectors.toList())); + } + updateShiftConfigDto.setCode(CautionEnum.Caution_201.getCode()); + if (Objects.equals(fixedMark, 1)) { + updateShiftConfigDto.setCode(CautionEnum.Caution_202.getCode()); + } + periodConflictRules.clear(); + periodConflictRules.addAll(hisRules); + } + continue; + } + + // 获取历史考勤规则的ID列表 + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + hisRules.addAll(BeanUtil.copyToList(acrossTheMoonLeaveRuleList.stream().filter(rule -> rule.getApplyStart().compareTo(endByDay) <= 0 && rule.getApplyEnd().compareTo(date) >= 0).collect(Collectors.toList()), FtbAttendanceDailyRule.class).stream().peek(vo -> { + vo.setDay(date); + vo.setId(RandomUtil.uuId()); + }).collect(Collectors.toList())); + // 处理日常规则安排 + dailyRuleArrangementHandle(resultList, hisRules, rules); + // 处理借调日常规则 + secondmentDailyRulesHandle(resultList, user, hisRules); + // 填充仅外出的规则 + fillInNailRuleForOnlyStepOut(hisRules); + hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> { + OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId()); + ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo)); + }); + // 将历史考勤规则添加到periodConflictRules列表中 + periodConflictRules.addAll(hisRules); + + // 将历史考勤规则添加到allRules列表中 + allRules.addAll(hisRules); + // 将历史考勤规则的ID添加到allRuleIds列表中 + allRuleIds.addAll(ruleIds); + // 清理撤回规则 + clearWithdrawRule(collect3.getOrDefault(user.getUserId() + day, CollUtil.newArrayList()), hisRules); + // 判断时段是否冲突 + boolean isPeriodConflict = !fixedPeriodConflict(periodConflictRules); + Boolean is24H = check24H(rules); + if (!isConfigUpdate) { + Assert.isFalse(isPeriodConflict, "选择的时段有冲突,请重新选择"); + Assert.isFalse(is24H, "选择的时段已超过24小时,请重新选择"); + continue; + } + //修改班次配置给定前端返回值 + if (isPeriodConflict) { + if (allRules.stream().anyMatch(rule -> date.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) { + allRules.removeAll(allRules.stream().filter(rule -> date.compareTo(rule.getDay()) == 0).collect(Collectors.toList())); + } + if (allRules.stream().anyMatch(rule -> yesterday.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) { + allRules.removeAll(allRules.stream().filter(rule -> yesterday.compareTo(rule.getDay()) == 0).collect(Collectors.toList())); + } + updateShiftConfigDto.setCode(CautionEnum.Caution_201.getCode()); + if (Objects.equals(fixedMark, 1)) { + updateShiftConfigDto.setCode(CautionEnum.Caution_202.getCode()); + } + periodConflictRules.clear(); + periodConflictRules.addAll(hisRules); + } + //修改班次配置给定前端返回值 + if (is24H) { + //根据时段id清空班次 + if (allRules.stream().anyMatch(rule -> date.compareTo(rule.getDay()) == 0 && updateShiftConfigDto.getPeriods().stream().anyMatch(config -> StringUtil.equals(config.getShiftId(), rule.getShiftId()) && Objects.equals(rule.getSchedulesType(), config.getType())))) { + allRules.removeAll(allRules.stream().filter(rule -> date.compareTo(rule.getDay()) == 0).collect(Collectors.toList())); + } + updateShiftConfigDto.setCode(CautionEnum.Caution_203.getCode()); + } + + } + setRuleForLateOutLateIn(userId, UserProvider.getUser().getTenantId(), allRules); + }), cpuIntensiveThreadPool)); + } + // 等待所有任务完成 + CompletableFuture allFutures = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + // 阻塞主线程直到所有异步任务完成 + allFutures.join(); // 这里会抛出 CompletionException 包装的原始异常 + // 保存或更新考勤规则 + saveOrUpdateBatch(allRules, allRuleIds); + String result = resultSchedulingHandle(resultList).stream().map(DailyRuleResultVo::getFailMsg).filter(StringUtil::isNotEmpty).reduce((msg1, msg2) -> msg1 + "\\br" + msg2).orElse(""); + // 发送系统通知 + attendanceRuleNotificationHandle.sendSystemNotice(resultList, null); + // 处理考勤规则通知 + attendanceRuleNotificationHandle.notificationHandle(resultList, isSync); + // 处理结果列表,返回失败消息 + return result; + } catch (InterruptedException e) { + throw new RuntimeException(e.getMessage()); + } finally { + // 解锁 + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + rLocks.forEach(rLock -> { + if (rLock.isHeldByCurrentThread()) { + rLock.unlock(); + } + }); + } + } + + private void batchBuildDailyRule(List items, Map> periodMap, AtomicReference balance, Map stringBooleanMap, String userId, List hisRules, int size, AttendanceGroupUser user, Date date, List rules, AttendanceShiftSettingVo attendanceShiftSettingVo, String groupId, List periods, BigDecimal attendanceRatio) { + // 遍历items列表,处理每个排班 + items.forEach(item -> { + List attendanceShiftSettingPeriodVos1 = periodMap.get(item.getShiftId()); + if (Objects.equals(item.getType(), 1)) { + balance.set(balance.get().subtract(Objects.equals(item.getSchedulesType(), 0) || Objects.isNull(item.getSchedulesType()) || Objects.isNull(attendanceShiftSettingPeriodVos1) || attendanceShiftSettingPeriodVos1.size() < 2 ? BigDecimal.ONE : attendanceShiftSettingPeriodVos1.get(item.getSchedulesType() - 1).getTimeSlotDay())); + } + // 如果调度任务的ID非空或者不存在,则跳过当前循环 + if (StringUtil.isNotEmpty(item.getId()) || Objects.equals(item.getIsExist(), 0) || Objects.isNull(item.getType())) { + return; + } + Assert.isFalse(stringBooleanMap.getOrDefault(userId, Boolean.FALSE), "本月存在部分用户被封账!"); + // 根据班次ID获取考勤时段设置 + // 根据调度任务的类型处理考勤规则 + if (Objects.equals(item.getType(), 2)) { + boolean hasWork = hisRules.stream().anyMatch(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())); + boolean hasLeave = hisRules.stream().anyMatch(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())); + if (hasWork && hasLeave) { + return; + } + if (size < 2 && CollUtil.isNotEmpty(attendanceShiftSettingPeriodVos1)) { + attendanceShiftSettingPeriodVos1.forEach(period -> buildDailyRule(user, date, rules, item.getSchedulesType(), CollUtil.newArrayList(period), attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 0)); + return; + } + buildDailyRule(groupId, userId, date, rules, item.getSchedulesType(), attendanceShiftSettingPeriodVos1, attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 0); + return; + } + buildDailyRule(groupId, userId, date, rules, item.getSchedulesType(), periods, attendanceShiftSettingVo, getType2AttendanceType(item.getType()), attendanceRatio, 0, balance.get()); + + }); + } + + private void applyCrossRulesAdd(List hisRules, Map> applyCrossRules, Date endByDay, Date date, List allRules) { + //如果有从上一日夸日到今天的申请数据,则直接添加到今天 + hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)).forEach((applyId, applyRules) -> { + FtbAttendanceDailyRule ftbAttendanceDailyRule = applyRules.get(0); + if (isApplyCrossDay(ftbAttendanceDailyRule.getApplyStart(), ftbAttendanceDailyRule.getApplyEnd())) { + List collect2 = applyCrossRules.containsKey(applyId) ? applyCrossRules.get(applyId) : applyRules.stream().filter(vo -> vo.getInPoint().before(endByDay) && vo.getOutPoint().after(date)).collect(Collectors.toList()); + hisRules.addAll(collect2); + allRules.removeAll(collect2); + } + }); + } + + private void applyCrossRulesAddNotCurrent(List hisRules, Map> applyCrossRules, Date endByDay, Date date) { + //如果有从上一日夸日到当前天的申请数据,匹配完后迁移到后一天 + hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)).forEach((applyId, applyRules) -> { + FtbAttendanceDailyRule ftbAttendanceDailyRule = applyRules.get(0); + applyCrossRules.remove(applyId); + if (isApplyCrossDay(ftbAttendanceDailyRule.getApplyStart(), ftbAttendanceDailyRule.getApplyEnd()) && endByDay.before(ftbAttendanceDailyRule.getApplyEnd())) { + applyCrossRules.put(applyId, applyRules.stream().filter(vo -> vo.getInPoint().before(endByDay) && vo.getOutPoint().after(date)).collect(Collectors.toList())); + } + }); + } + + + private void applyCrossRulesAdd(List hisRules, List applyCrossRules, Date endByDay, Date date) { + //如果有从上一日夸日到当前天的申请数据,匹配完后迁移到后一天 + hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)).forEach((applyId, applyRules) -> { + applyRules.forEach(ftbAttendanceDailyRule -> { + if (isApplyCrossDay(ftbAttendanceDailyRule.getApplyStart(), ftbAttendanceDailyRule.getApplyEnd()) && endByDay.before(ftbAttendanceDailyRule.getApplyEnd())) { + applyCrossRules.add(ftbAttendanceDailyRule); + } + }); + }); + applyCrossRules.removeIf(vo -> vo.getApplyStart().after(endByDay) || vo.getApplyEnd().before(date)); + } + + /** + * 填充外出在存在无覆盖排班时,外出未标记 + * + * @param hisRules + */ + private void fillInNailRuleForOnlyStepOut(List hisRules) { + List collect = hisRules.stream().filter(hisRule -> (Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode())) && Objects.equals(hisRule.getApplyViewEnable(), hisRule.getAttendanceType())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)) { + List collect2 = hisRules.stream().filter(hisRule -> !collect.contains(hisRule) && !Objects.equals(hisRule.getApplyViewEnable(), AttendanceTypeEnum.STEP_OUT.getCode())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect2)) { + FtbAttendanceDailyRule ftbAttendanceDailyRule = collect.stream().findFirst().orElse(null); + collect.forEach(rule -> rule.setApplyViewEnable(0)); + hisRules.add(buildRuleForNail(ftbAttendanceDailyRule.getGroupId(), ftbAttendanceDailyRule.getUserId(), ftbAttendanceDailyRule.getDay(), ftbAttendanceDailyRule.getAttendanceType(), AttendanceTypeEnum.CLEAR)); + } + } + } + + private List setSchedulesLockByUsers(List schedulesSets) { + List locks = new ArrayList<>(); + String day = DateUtil.dateToString(new Date(), "yyyy-MM-dd"); + for (SchedulesSetDto set : schedulesSets) { + Map val = set.getVal(); + if (Objects.isNull(val) || !val.containsKey(day)) { + continue; + } + List schedulesDaySetDtos = val.get(day).getItemVos(); + if (CollUtil.isEmpty(schedulesDaySetDtos)) { + continue; + } + List collect = schedulesDaySetDtos.stream().filter(dto -> Objects.isNull(dto.getId())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)) { + RLock lock = redissonClient.getLock(String.format(RedisConstant.ATTENDANCE_USER_SET_SCHEDULES, UserProvider.getUser().getTenantId(), set.getGroupId(), set.getUserId())); + try { + Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前用户正在排班中,请稍后再试"); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + locks.add(lock); + + } + } + return locks; + } + + private Boolean fixedPeriodConflict(List periodEntities) { + if (CollUtil.isEmpty(periodEntities)) { + return Boolean.TRUE; + } + periodEntities = periodEntities.stream().filter(rule -> Objects.equals(rule.getApplyViewEnable(), 1) && !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.REST.getCode()) && !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).collect(Collectors.toMap(FtbAttendanceDailyRule::getId, rule -> rule, (r1, r2) -> Objects.isNull(r1.getInPoint()) ? r2 : r1)).values().stream().collect(Collectors.toList()); + List list = CollUtil.newArrayList(); + for (FtbAttendanceDailyRule period : periodEntities) { + if (!fixedPeriodSingleConflict(list, period)) { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + } + + private Boolean fixedPeriodSingleConflict(List list, FtbAttendanceDailyRule rule) { + Date inPoint = rule.getInPoint(); + Date outPoint = rule.getOutPoint(); + if (Objects.isNull(inPoint)) { + return Boolean.TRUE; + } + if (CollUtil.isEmpty(list)) { + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + for (SecondmentDateVo vo : list) { + if (DateDetail.checkTimeBetween(inPoint, vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(outPoint, vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(vo.getStartTime(), inPoint, outPoint) || DateDetail.checkTimeBetween(vo.getEndTime(), inPoint, outPoint)) { + if (vo.getStartTime().compareTo(outPoint) != 0 && vo.getEndTime().compareTo(inPoint) != 0) { + return Boolean.FALSE; + } + } + } + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + + private void saveOrUpdateBatch(List allRules, List allRuleIds) { + if (CollUtil.isNotEmpty(allRuleIds)) { + List collect = allRules.stream().filter(rule -> allRuleIds.contains(rule.getId())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)) { + updateBatchById(collect); + } + allRules = new ArrayList<>(allRules.stream().filter(rule -> !allRuleIds.contains(rule.getId())).distinct().collect(Collectors.toMap(FtbAttendanceDailyRule::getId, rule -> rule, (r1, r2) -> Objects.isNull(r1.getInPoint()) ? r2 : r1)).values()); + allRuleIds.removeAll(collect.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList())); + allRuleIds.removeAll(allRules.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList())); + removeBatchByIds(allRuleIds); + saveBatch(allRules); + crossDayRuleByWorkOvertime(collect); + return; + } + if (CollUtil.isNotEmpty(allRules)) { + saveBatch(allRules.stream().distinct().collect(Collectors.toList())); + } + } + + /** + * 跨日的加班规则处理 + * + * @param collect + */ + private void crossDayRuleByWorkOvertime(List collect) { + //过滤出加班且被跨日排班的规则 + Map ruleMap = collect.stream().filter(rule -> Objects.equals(rule.getApplyViewEnable(), 0) && Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode()) && !rule.getIsInsert()).collect(Collectors.toMap(FtbAttendanceDailyRule::getApplyId, rule -> rule, (v1, v2) -> v1)); + if (CollUtil.isEmpty(ruleMap)) { + return; + } + //查询所有隐藏的跨日加班申请涉及的所有加班申请规则 + List list = lambdaQuery().in(FtbAttendanceDailyRule::getApplyId, ruleMap.keySet()).eq(FtbAttendanceDailyRule::getApplyViewEnable, 1).eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.WORKOVERTIME.getCode()).orderByAsc(FtbAttendanceDailyRule::getInPoint).list(); + if (CollUtil.isEmpty(list)) { + return; + } + Map> collect1 = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getApplyId)); + List objects = CollUtil.newArrayList(); + collect1.forEach((applyId, rules) -> { + FtbAttendanceDailyRule currRule = ruleMap.get(applyId); + if (Objects.isNull(currRule)) { + return; + } + //找到当前隐藏申请之前的最后数据 + FtbAttendanceDailyRule ftbAttendanceDailyRule1 = rules.stream().filter(rule -> rule.getInPoint().before(currRule.getDay())).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(ftbAttendanceDailyRule1)) { + //修改最后下班打卡时间为隐藏规则的下班打卡时间 + ftbAttendanceDailyRule1.setOutPoint(currRule.getOutPoint()); + objects.add(ftbAttendanceDailyRule1); + } + }); + updateBatchById(objects); + } + + /** + * 定时执行初始化下个月固定排班 + */ + @Override + @Transactional + public void initFixedScheduleRule(String tenantId) { + Date start = DateUtil.dateAddMonths(DateUtil.getBeginDayOfMonth(), 1); + Date end = DateUtil.dateAddSeconds(DateUtil.dateAddMonths(start, 1), -1); + Date date = new Date(); + int day = cn.hutool.core.date.DateUtil.dayOfMonth(date); + if (day < 20) { + start = date; + end = cn.hutool.core.date.DateUtil.endOfMonth(date).toJdkDate(); + } else if (day > 20) { + start = date; + } + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).between(FtbAttendanceDailyRule::getDay, start, end).list(); + List enableGroupIds = CollUtil.isNotEmpty(list) ? list.stream().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList()) : CollUtil.newArrayList(); + //读取考勤组数据 + List attendanceGroupVos = attendanceGroupService.queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return; + } + + List groupIds = attendanceGroupVos.stream().filter(vo -> !enableGroupIds.contains(vo.getId())).map(AttendanceGroup::getId).collect(Collectors.toList()); + //获取考勤组人员数据 + List users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, null, groupIds); + if (CollUtil.isEmpty(users)) { + log.error("未查询到所有考勤组人员信息"); + return; + } + FixedHandleDTO build = FixedHandleDTO.builder().groupIds(groupIds).attendanceGroupVos(attendanceGroupVos).users(users).tenantId(tenantId).start(start).end(end).build(); + //规则处理 + fixedHandle(build); + //处理节日自动排休 + addHolidayDailyRule(build); + } + + @Override + public void clearGroupRule(String groupId, List userIds) { + clearGroupRule(groupId, userIds, new Date(), null); + } + + @Override + public void clearGroupRule(String groupId, List userIds, Date departTime, String tenantId) { + if (StringUtil.isEmpty(groupId) && CollUtil.isEmpty(userIds)) { + return; + } + DateTime end = cn.hutool.core.date.DateUtil.endOfDay(departTime); + List list = lambdaQuery() + .and(x -> x.ge(FtbAttendanceDailyRule::getInPoint, end). + or(x1 -> x1.gt(FtbAttendanceDailyRule::getDay, end) + .in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()))) + .notIn(StringUtil.isEmpty(userIds), FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()) + .eq(StringUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId) + .in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds) + .list(); + List ruleIds = list.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(ruleIds)) { + removeBatchByIds(ruleIds); + } + attendanceRuleNotificationHandle.notificationClearHandle(tenantId, groupId, userIds, end); + lambdaUpdate().eq(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(departTime)).notIn(StringUtil.isEmpty(userIds), FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()).eq(StringUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds).set(FtbAttendanceDailyRule::getApplyViewEnable, 3).update(); + + } + + private void clearWithdrawRule(List list, List hisRules) { + if (CollUtil.isEmpty(list)) { + return; + } + List resultList = CollUtil.newArrayList(); + List ruleIds = CollUtil.newArrayList(); + list.forEach(rule -> { + boolean isCover = hisRules.stream().anyMatch(hisRule -> Objects.equals(rule.getAttendanceType(), -1) || Objects.equals(rule.getAttendanceType(), 2) || Objects.isNull(hisRule.getInPoint()) || hisRule.getInPoint().compareTo(rule.getOutPoint()) < 0 && hisRule.getOutPoint().compareTo(rule.getInPoint()) > 0); + if (isCover) { + ruleIds.add(rule.getId()); + } + resultList.add(DailyRuleResultVo.successBuild(rule)); + }); + if (CollUtil.isNotEmpty(ruleIds)) { + removeBatchByIds(ruleIds); + } + } + + /** + * 申请验证的日规则处理 + * + * @param applyParam 申请参数类 + */ + @Override + public List applyVerifyHandle(ApplyParam applyParam) throws HandleException { + List allRules = CollUtil.newArrayList(); + List allRuleIds = CollUtil.newArrayList(); + if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP)) { + applyParam.setLeaveUnit(2); + } + List dailyRuleResultVos = applyDailyRuleHandle(applyParam, allRules, allRuleIds); + + allRules.stream().filter(vo -> (StringUtil.equals(vo.getApplyId(), applyParam.getApplyId()) || StringUtil.isEmpty(vo.getApplyId()) && StringUtil.isEmpty(applyParam.getApplyId())) && Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(vo -> { + OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(allRules, vo.getInPoint(), vo.getOutPoint(), applyParam.getApplyId()); + Assert.notNull(overtimeRuleDetailVo, "未匹配到加班规则"); + }); + return dailyRuleResultVos.stream().filter(vo -> !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.LEAVE) || StringUtil.isNotEmpty(vo.getFromId())).peek(vo -> vo.setDuration(Objects.requireNonNullElse(vo.getDuration(), BigDecimal.ZERO).divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP))).sorted(Comparator.comparing(DailyRuleResultVo::getInPoint)).collect(Collectors.toList()); + } + + /** + * 申请的日规则处理 + * + * @param applyParam 申请参数类 + */ + @Override + @Transactional + public String applyDailyRuleHandle(ApplyParam applyParam) throws HandleException { + List allRules = CollUtil.newArrayList(); + List allRuleIds = CollUtil.newArrayList(); + // 获取当前考勤组的排班锁 + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), applyParam.getUserId())); + // 确保锁未被其他操作持有,避免并发执行 + try { + Assert.isFalse(lock.isLocked() || !lock.tryLock(3, TimeUnit.SECONDS), "存在其他排班或申请操作正在执行,请稍后再试"); + } catch (Exception e) { + throw new HandleException("存在其他排班或申请操作正在执行,请稍后再试"); + } + try { + if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP)) { + applyParam.setLeaveUnit(2); + } + List dailyRuleResultVos = applyDailyRuleHandle(applyParam, allRules, allRuleIds); + List collect = dailyRuleResultVos.stream().filter(result -> result.getType() == 1).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)) { + allRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> { + OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(allRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId()); + ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo)); + }); + saveOrUpdateBatch(allRules, allRuleIds); + List datesByPeriod = DateDetail.getDatesByPeriod(applyParam.getStart(), cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd())); + attendanceRuleNotificationHandle.notificationHandle(datesByPeriod.stream().map(day -> { + DailyRuleResultVo dailyRuleResultVo = new DailyRuleResultVo(); + dailyRuleResultVo.setType(0); + dailyRuleResultVo.setGroupId(applyParam.getGroupId()); + dailyRuleResultVo.setDate(day); + dailyRuleResultVo.setUserId(applyParam.getUserId()); + return dailyRuleResultVo; + }).collect(Collectors.toList()), DataSourceContextHolder.getDatasourceId(), Boolean.FALSE, Boolean.TRUE); + } + return resultHandle(dailyRuleResultVos).stream().map(DailyRuleResultVo::getFailMsg).filter(StringUtil::isNotEmpty).reduce((msg1, msg2) -> msg1).orElse(""); + } catch (Exception e) { + throw new HandleException(e.getMessage()); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + /** + * 申请的日规则处理 + * + * @param applyParam + * @param allRules + * @param allRuleIds + * @return + * @throws HandleException + */ + private List applyDailyRuleHandle(ApplyParam applyParam, List allRules, List allRuleIds) throws HandleException { + List userIds = CollUtil.newArrayList(applyParam.getUserId()); + List users; + Date start = Objects.isNull(applyParam.getStart()) ? DateUtil.getDayBegin() : applyParam.getStart(); + Date end = Objects.isNull(applyParam.getEnd()) ? DateUtil.getDayEnd() : applyParam.getEnd(); + LeaveParam leaveParam = BeanUtil.toBean(applyParam, LeaveParam.class); + if (isLeaveUnitOfDayOrHalfDay(applyParam)) { + //查询请假时间内所有考勤组 + users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(applyParam.getStart(), Objects.equals(applyParam.getLeaveUnit(), LeaveUnitEnum.HOUR.getCode()) || Objects.isNull(applyParam.getLeaveUnit()) ? applyParam.getEnd() : cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()), userIds, StringUtil.isEmpty(applyParam.getGroupId()) ? null : List.of(applyParam.getGroupId())); + //如果考勤组数量大于1,判断是否包含当前时间所属考勤组,包含则取当前所属考勤组,不包含直接报错 + if (users.size() > 1) { + //查询当前时间所属考勤组 + Date date = new Date(); + users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(date, date, userIds, null); + Assert.isFalse(users.stream().anyMatch(vo -> Objects.equals(vo.getType(), GroupUserTypeEnum.BORROW.getCode())), "申请时间与借调时间冲突!请重新选择"); + } + end = cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()); + } else { + users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(applyParam.getStart(), applyParam.getEnd(), userIds, StringUtil.isEmpty(applyParam.getGroupId()) ? null : List.of(applyParam.getGroupId())); + } + Assert.isFalse(CollUtil.isEmpty(users), "未查询到所属考勤组"); + Assert.isFalse(users.stream().map(AttendanceGroupUser::getGroupId).distinct().count() > 1, "当前申请时段存在多个考勤组"); + List groupIds = users.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList()); + applyParam.setGroupId(groupIds.get(0)); + List resultList = CollUtil.newArrayList(); + Map> userMap = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + //查询基础配置 + Map enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(groupIds); + //查询原排班 + List hisAllRules = isFullDayQuery(applyParam) ? getDailyRulesByPeriodForStepOut(applyParam.getStart(), applyParam.getEnd(), userIds, groupIds) : getDailyRulesByPeriod(cn.hutool.core.date.DateUtil.beginOfDay(start), cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()), userIds, groupIds); + //计算半天、全天请假申请开始时间及结束时间 + FtbAttendanceDailyRule min = hisAllRules.stream().min(Comparator.comparing(FtbAttendanceDailyRule::getDay)).orElse(null); + //非外出、出差无考勤只保存第一天申请数据 + List dates = DateDetail.getDatesByPeriod(Objects.isNull(min) ? start : min.getDay().after(start) ? start : min.getDay(), !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME) && !Objects.equals(applyParam.getLeaveUnit(), 1) ? cn.hutool.core.date.DateUtil.endOfDay(end) : end); + Map> collect1 = hisAllRules.stream().collect(Collectors.groupingBy((rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId()))); + //遍历组员 + List rules = CollUtil.newArrayList(); + List applyCrossRules = CollUtil.newArrayList(); + Date applyBeginOfDay = cn.hutool.core.date.DateUtil.beginOfDay(applyParam.getStart()); + Date applyEndOfDay = cn.hutool.core.date.DateUtil.endOfDay(applyParam.getEnd()); + groupIds.forEach(id -> { + List usersGroup = userMap.get(id); + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(id); + BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.valueOf(8) : attendanceBaseSetting.getAttendanceRatio(); + Map> collect = usersGroup.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + collect.forEach((groupUserId, userList) -> { + applyCrossRules.clear(); + //遍历每天 + for (Date date : dates) { + Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date); + AttendanceGroupUser user = userList.stream().filter(user1 -> user1.getUserGroupType() == 2 ? Boolean.TRUE : endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.compareTo(user1.getRemoveTime()) < 0)).max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).orElse(null); + if (Objects.isNull(user)) { + continue; + } + List hisRules = collect1.getOrDefault(DateDetail.getDateTime2Str(date) + id + user.getUserId(), CollUtil.newArrayList()); + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + //不在申请时间内数据不做下面处理 + if (!DateDetail.checkTimeBetween(date, applyBeginOfDay, applyEndOfDay)) { + allRules.addAll(hisRules); + allRuleIds.addAll(ruleIds); + continue; + } + rules.clear(); + buildDailyRuleForApply(applyParam, date, user, rules, attendanceRatio, leaveParam, applyCrossRules); + dailyRuleArrangementHandle(resultList, hisRules, rules); + secondmentDailyRulesHandle(resultList, user, hisRules); + fillInNailRuleForOnlyStepOut(hisRules); + //清除原跨日规则,判断申请日期在今天后,继续将匹配后的规则继续传递到后一天 + applyCrossRules.clear(); + if (isApplyCrossDay(applyParam) && endByDay.before(applyParam.getEnd()) && !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP) && !Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT)) { + applyCrossRules.addAll(hisRules.stream().filter(vo -> Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) || Objects.equals(vo.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).filter(vo -> StringUtil.equals(vo.getApplyId(), applyParam.getApplyId()) || (StringUtil.isEmpty(vo.getApplyId()) && StringUtil.isEmpty(applyParam.getApplyId()))).collect(Collectors.toList())); + } + allRules.addAll(hisRules); + allRuleIds.addAll(ruleIds); + } + setRuleForLateOutLateIn(groupUserId, UserProvider.getUser().getTenantId(), allRules); + }); + }); + hisAllRules.clear(); + return resultHandle(resultList); + } + + private void buildDailyRuleForApply(ApplyParam applyParam, Date date, AttendanceGroupUser user, List rules, BigDecimal attendanceRatio, LeaveParam leaveParam, List applyCrossRules) { + //出差,外出跨日拆分为多日 + if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP) || Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT) && !Objects.equals(applyParam.getLeaveUnit(), 1)) { + Date startTime = cn.hutool.core.date.DateUtil.beginOfDay(date); + Date endTime = DateUtil.dateAddSeconds(DateUtil.dateAddDays(startTime, 1), -1); + buildDailyRule(user, date, rules, 0, null, null, applyParam.getAttendanceType(), applyParam.getApplyId(), startTime, endTime, attendanceRatio, 0, leaveParam); + } else { + //是否需要次日不展示 + boolean isTomorrow = CollUtil.isNotEmpty(applyCrossRules) && isApplyCrossDay(applyParam); + //请假申请,次日使用新day + if (isTomorrow && Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.LEAVE)) { + isTomorrow = false; + } + if (isTomorrow && Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME)) { + rules.addAll(applyCrossRules); + return; + } + buildDailyRule(user, isTomorrow ? cn.hutool.core.date.DateUtil.beginOfDay(applyParam.getStart()) : date, rules, 0, null, null, applyParam.getAttendanceType(), applyParam.getApplyId(), applyParam.getStart(), applyParam.getEnd(), applyParam.getValidDuration(), 0, leaveParam); + } + } + + private boolean isApplyCrossDay(Date start, Date end) { + return cn.hutool.core.date.DateUtil.beginOfDay(start).compareTo(cn.hutool.core.date.DateUtil.beginOfDay(end)) != 0; + } + + private boolean isApplyCrossDay(ApplyParam applyParam) { + return isApplyCrossDay(applyParam.getStart(), applyParam.getEnd()); + } + + /** + * 半天假全天班中间分界时间点 + * + * @param ftbAttendanceDailyRule + * @return + */ + private Date getLeaveDate(FtbAttendanceDailyRule ftbAttendanceDailyRule) { + int ruleMinute = DateDetail.calculateMinuteDiff(ftbAttendanceDailyRule.getInPoint(), ftbAttendanceDailyRule.getOutPoint()); + return cn.hutool.core.date.DateUtil.offsetMinute(ftbAttendanceDailyRule.getInPoint(), ruleMinute / 2); + } + + /** + * 判断是否是全天查询 + * + * @param applyParam 申请参数,包含出勤类型等信息 + * @return 如果是全天查询则返回true,否则返回false + */ + private boolean isFullDayQuery(ApplyParam applyParam) { + //外出申请非全天外出 + if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT) && Objects.equals(applyParam.getLeaveUnit(), 1)) { + return Boolean.FALSE; + } + // 如果出勤类型是商务出差或外出,则认为是全天查询 + if (Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP) || Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.STEP_OUT)) { + return Boolean.TRUE; + } + // 如果是按天或半天请假单位,则认为是全天查询 + if (isLeaveUnitOfDayOrHalfDay(applyParam)) { + return Boolean.TRUE; + } + // 默认不是全天查询 + return Boolean.FALSE; + } + + /** + * 判断申请的请假类型是否为全天或半天 + * + * @param applyParam 请假申请参数,包含考勤类型和请假单位等信息 + * @return 如果请假类型为全天或半天则返回true,否则返回false + */ + private boolean isLeaveUnitOfDayOrHalfDay(ApplyParam applyParam) { + // 检查考勤类型是否为请假,并且请假单位是否为全天或半天 + return Objects.equals(applyParam.getAttendanceType(), AttendanceTypeEnum.LEAVE) && (Objects.equals(applyParam.getLeaveUnit(), LeaveUnitEnum.DAY.getCode()) || Objects.equals(applyParam.getLeaveUnit(), LeaveUnitEnum.HALF_DAY.getCode())); + } + + /** + * 借调申请日规则处理 + * + * @param userIds 借调用户id集合 + * @param fromGroupId 原考勤组id + * @param toGroupId 借调考勤组id + * @param start 开始时间 + * @param end 结束时间 + * @param departureTime 离岗时间 + * @param backTime 回岗时间 + * @param tenantId + */ + @Override + public List secondmentDailyRuleHandle(List userIds, String fromGroupId, String toGroupId, Date start, Date end, Date departureTime, Date backTime, String tenantId) throws HandleException { + List groupIds = CollUtil.newArrayList(fromGroupId, toGroupId); + List attendanceGroupVos = attendanceGroupService.queryListByIds(groupIds); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息,{}", JSON.toJSONString(groupIds)); + throw new HandleException("未查询到考勤组信息"); + } + List users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(null, null, userIds, CollUtil.newArrayList(fromGroupId)); + users.forEach(user -> { + List secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? CollUtil.newArrayList() : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class); + SecondmentDateVo secondmentDateVo = new SecondmentDateVo(); + secondmentDateVo.setStartTime(departureTime); + secondmentDateVo.setEndTime(backTime); + secondmentDateVos.add(secondmentDateVo); + user.setTimeJson(JSON.toJSONString(secondmentDateVos)); + }); + for (String userId : userIds) { + AttendanceGroupUser attendanceGroupUser = new AttendanceGroupUser(); + attendanceGroupUser.setGroupId(toGroupId); + attendanceGroupUser.setUserId(userId); + attendanceGroupUser.setType(2); + attendanceGroupUser.setUserGroupType(2); + List secondmentDateVos = CollUtil.newArrayList(); + SecondmentDateVo secondmentDateVo = new SecondmentDateVo(); + secondmentDateVo.setStartTime(start); + secondmentDateVo.setEndTime(end); + secondmentDateVos.add(secondmentDateVo); + attendanceGroupUser.setTimeJson(JSON.toJSONString(secondmentDateVos)); + users.add(attendanceGroupUser); + } + if (CollUtil.isEmpty(users)) { + log.error("未查询到所有考勤组人员信息,{}", JSON.toJSONString(userIds)); + throw new HandleException("未查询到所有考勤组人员信息"); + } + List resultList = CollUtil.newArrayList(); + //根据考勤组id查询考勤组班制配置 + Map enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, attendanceGroupService.queryDropList()); + if (CollUtil.isEmpty(enableShiftSetting)) { + log.error("未查询到所有考勤组班制配置"); + return resultList; + } + List dates = DateDetail.getDatesByPeriod(start, end); + Map> collect1 = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + //获取每日出勤规则 + List dailyRulesByPeriod = getDailyRulesByPeriod(departureTime, backTime, userIds, groupIds); + Map> dailyRuleMap = CollUtil.isEmpty(dailyRulesByPeriod) ? Maps.newHashMap() : dailyRulesByPeriod.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId())); + Map enableBaseSetting = attendanceBaseSettingService.getEnableBaseSetting(groupIds, attendanceGroupVos); + List allRules = CollUtil.newArrayList(); + List allRuleIds = CollUtil.newArrayList(); + attendanceGroupVos.forEach(vo -> { + AttendanceShiftSettingVo attendanceShiftSettingVo = enableShiftSetting.get(vo.getId()); + if (Objects.isNull(attendanceShiftSettingVo)) { + log.error("未查询到考勤组[{}]班制时段数据", vo.getId()); + return; + } + List attendanceGroupUsers = collect1.get(vo.getId()); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + log.error("未查询到当前考勤组人员信息,{}", vo.getId()); + return; + } + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(vo.getId()); + Map> userMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + //遍历组员 + userMap.forEach((userId, userList) -> { + //遍历每天 + for (Date date : dates) { + Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date); + AttendanceGroupUser user = userList.stream().filter(user1 -> user1.getUserGroupType() == 2 ? Boolean.TRUE : Objects.isNull(user1.getCreatorTime()) ? Boolean.TRUE : endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.compareTo(user1.getRemoveTime()) < 0)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(user)) { + continue; + } + List hisRules = dailyRuleMap.getOrDefault(DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId(), CollUtil.newArrayList()); + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + //新增借调借调组固定班日规则生成 + afterSecondmentRulesHandle(attendanceShiftSettingVo, date, user, hisRules, attendanceBaseSetting); + secondmentDailyRulesHandle(resultList, user, hisRules); + hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> { + OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId()); + ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo)); + }); + allRules.addAll(hisRules); + allRuleIds.addAll(ruleIds); + } + setRuleForLateOutLateIn(userId, tenantId, allRules); + }); + }); + saveOrUpdateBatch(allRules, allRuleIds); + attendanceRuleNotificationHandle.notificationHandle(resultList, tenantId, Boolean.TRUE, Boolean.TRUE); + return resultHandle(resultList); + } + + @Override + public List getAttendanceDayRulesForStatistic(String groupId, String userId, Date day) { + return lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(StringUtil.isNotEmpty(userId), FtbAttendanceDailyRule::getUserId, userId).eq(StringUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode()).eq(FtbAttendanceDailyRule::getDay, day).orderByAsc(FtbAttendanceDailyRule::getApplyStart).list(); + } + + + @Override + public DayClockRange getDayClockRange(String userId, Date day) { + //查询开始时间 + DayClockRange clockRange = DayClockRange.builder().endTimeIsOpenClose(Boolean.TRUE).startTimeIsOpenClose(Boolean.TRUE).build(); + //查询昨天最后一个班次 + Date yesterday = DateUtil.dateAddDays(day, -1); + FtbAttendanceDailyRule yesterdayDailyRule = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDay, yesterday).gt(FtbAttendanceDailyRule::getOutPoint, DateUtil.getDayEndTime(yesterday)).orderByDesc(FtbAttendanceDailyRule::getOutPoint).last("limit 1").one(); + if (Objects.nonNull(yesterdayDailyRule)) { + //查询当天班次的打卡结果 + AttendanceClockInResult attendanceClockInResult = attendanceClockInResultMapper.selectOne(new LambdaQueryWrapper().eq(AttendanceClockInResult::getRuleId, yesterdayDailyRule.getId()).eq(AttendanceClockInResult::getClockInType, ConstantUtil.OFF_WORK).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE).last("limit 1")); + if (Objects.nonNull(attendanceClockInResult) && StringUtil.isNotEmpty(attendanceClockInResult.getClockInId())) { + FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectById(attendanceClockInResult.getClockInId()); + clockRange.setStartTime(Objects.nonNull(clockIn) ? clockIn.getClockInTime() : attendanceClockInResult.getEffectiveTime()); + clockRange.setEndTimeIsOpenClose(Boolean.FALSE); + } else { + clockRange.setStartTime(DateUtil.getDayStartTime(day)); + } + } else { + clockRange.setStartTime(DateUtil.getDayStartTime(day)); + } + //查询结束时间 + FtbAttendanceDailyRule dayDailyRule = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDay, day).orderByDesc(FtbAttendanceDailyRule::getOutPoint).last("limit 1").one(); + if (Objects.nonNull(dayDailyRule)) { + //查询当天班次的打卡结果 + AttendanceClockInResult attendanceClockInResult = attendanceClockInResultMapper.selectOne(new LambdaQueryWrapper().eq(AttendanceClockInResult::getRuleId, dayDailyRule.getId()).eq(AttendanceClockInResult::getClockInType, ConstantUtil.OFF_WORK).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE).last("limit 1")); + if (Objects.nonNull(attendanceClockInResult) && StringUtil.isNotEmpty(attendanceClockInResult.getClockInId())) { + FtbAttendanceClockIn clockIn = attendanceClockInMapper.selectById(attendanceClockInResult.getClockInId()); + clockRange.setEndTime(Objects.nonNull(clockIn) ? clockIn.getClockInTime() : attendanceClockInResult.getEffectiveTime()); + clockRange.setEndTimeIsOpenClose(Boolean.FALSE); + } else { + clockRange.setEndTime(DateUtil.getDayEndTime(day)); + } + } else { + clockRange.setEndTime(DateUtil.getDayEndTime(day)); + } + return clockRange; + } + + @Override + public Date getDayEndRuleDeletionDate(String userId, Date day) { + //查询当天最后一个班次 + FtbAttendanceDailyRule dailyEndRule = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3, AttendanceTypeEnum.BUSINESS_TRIP.getCode(), AttendanceTypeEnum.STEP_OUT.getCode()).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()).eq(FtbAttendanceDailyRule::getDay, day).orderByDesc(FtbAttendanceDailyRule::getOutPoint).last("limit 1").one(); + //判断最后一个班次是不是加班 + return Objects.nonNull(dailyEndRule) ? dailyEndRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()) ? dailyEndRule.getOutPoint() : dailyEndRule.getOutLackPoint() : null; + } + + @Override + public boolean userIsScheduling(String userId) { + return lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).notIn(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode()).count() > 0; + } + + @Override + public List userIsSchedulingOrdinary(List organizeIds) { + List byOrgId = attendanceGroupService.getByOrgIds(organizeIds); + if (CollUtil.isEmpty(byOrgId)) { + return List.of(); + } + List collect = byOrgId.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); + List list = lambdaQuery().select(FtbAttendanceDailyRule::getUserId).in(FtbAttendanceDailyRule::getApplyViewEnable, 1, 3).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(new Date())).in(FtbAttendanceDailyRule::getGroupId, collect).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()).list(); + return list.stream().map(FtbAttendanceDailyRule::getUserId).collect(Collectors.toList()); + } + + @Override + public boolean hasRuleByUserIdAndTime(String userId, Date start, Date end) { + return lambdaQuery().eq(FtbAttendanceDailyRule::getUserId, userId).isNotNull(FtbAttendanceDailyRule::getInPoint).le(FtbAttendanceDailyRule::getInPoint, end).ge(FtbAttendanceDailyRule::getOutPoint, start).count() > 0; + } + + @Override + public List getUserDayBusAndOutInfo(String userId, String groupId, Date day, List typeEnumList) { + List typeEnumIdList = typeEnumList.stream().map(AttendanceTypeEnum::getCode).collect(Collectors.toList()); + List dailyEndRuleList = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, CollUtil.addAll(typeEnumIdList, 0)).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).eq(FtbAttendanceDailyRule::getUserId, userId).in(FtbAttendanceDailyRule::getAttendanceType, typeEnumIdList).eq(FtbAttendanceDailyRule::getDay, day).list(); + List outOrBusApproveVoList = CollUtil.newArrayList(); + if (CollUtil.isEmpty(dailyEndRuleList)) { + return outOrBusApproveVoList; + } + if (dailyEndRuleList.stream().anyMatch(dailyEndRule -> dailyEndRule.getAttendanceType().equals(AttendanceTypeEnum.STEP_OUT.getCode()))) { + List outApproveList = this.goOutApproveService.getBatchByIds(List.of(groupId), dailyEndRuleList.stream().map(FtbAttendanceDailyRule::getApplyId).distinct().collect(Collectors.toList())); + for (AttendanceGoOutApprove approve : outApproveList) { + OutOrBusApproveVo outOrBusApproveVo = OutOrBusApproveVo.builder().id(approve.getId()).type(1).startTime(approve.getStartTime()).endTime(approve.getEndTime()).unit(approve.getUnit()).build(); + if (approve.getUnit().equals(LeaveUnitEnum.DAY.getCode()) && approve.getDayNum().compareTo(BigDecimal.ONE) > 0) { + outOrBusApproveVo.setDayNum(BigDecimal.ONE); + } else { + outOrBusApproveVo.setDayNum(approve.getDayNum()); + } + outOrBusApproveVoList.add(outOrBusApproveVo); + } + } + if (dailyEndRuleList.stream().anyMatch(dailyEndRule -> dailyEndRule.getAttendanceType().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()))) { + List busApproveList = this.businessTripApproveService.getBatchByIds(List.of(groupId), dailyEndRuleList.stream().map(FtbAttendanceDailyRule::getApplyId).distinct().collect(Collectors.toList())); + for (AttendanceBusinessTripApprove entry : busApproveList) { + OutOrBusApproveVo outOrBusApproveVo = OutOrBusApproveVo.builder().id(entry.getId()).type(2).startTime(entry.getStartTime()).endTime(entry.getEndTime()).build(); + if (entry.getDayNum().compareTo(BigDecimal.ONE) > 0) { + outOrBusApproveVo.setDayNum(BigDecimal.ONE); + } else { + outOrBusApproveVo.setDayNum(entry.getDayNum()); + } + outOrBusApproveVoList.add(outOrBusApproveVo); + } + } + return outOrBusApproveVoList; + } + + @Override + public Map getUserBusAndOutCount(List userIds, List groupIdList, Date startDate, Date endDate, AttendanceTypeEnum businessTrip) { + List dailyEndRuleList = lambdaQuery().in(FtbAttendanceDailyRule::getApplyViewEnable, List.of(businessTrip.getCode(), 0)).eq(FtbAttendanceDailyRule::getDeleteMark, Boolean.FALSE).in(FtbAttendanceDailyRule::getUserId, userIds).in(StringUtil.isNotEmpty(groupIdList), FtbAttendanceDailyRule::getGroupId, groupIdList).eq(FtbAttendanceDailyRule::getAttendanceType, businessTrip.getCode()).ge(FtbAttendanceDailyRule::getDay, startDate).le(FtbAttendanceDailyRule::getDay, endDate).list(); + Map> busApproveMap = CollUtil.isNotEmpty(dailyEndRuleList) ? dailyEndRuleList.stream().collect(Collectors.groupingBy(record -> record.getUserId() + "&" + record.getGroupId())) : Maps.newHashMap(); + Map resultMap = Maps.newHashMap(); + busApproveMap.forEach((key, value) -> resultMap.put(key, Math.toIntExact(value.stream().map(FtbAttendanceDailyRule::getApplyId).distinct().count()))); + return resultMap; + } + + /** + * 申请规则的异常结果处理 + * + * @param resultList + * @return + */ + private List resultHandle(List resultList) { + if (CollUtil.isEmpty(resultList)) { + return resultList; + } + List userIds = resultList.stream().map(DailyRuleResultVo::getUserId).collect(Collectors.toList()); + List infoByIds = userApi.getInfoByIds(userIds); + Map userMap = CollUtil.isEmpty(infoByIds) ? Maps.newHashMap() : infoByIds.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getRealName)); + resultList.forEach(result -> { + if (result.getType() == 1) { + result.setFailMsg(AttendanceConstant.getApplyResult(userMap.get(result.getUserId()), AttendanceTypeEnum.getMsg(result.getFromType()))); + } + }); + return resultList; + } + + /** + * 排班规则的异常结果处理 + * + * @param resultList + * @return + */ + private List resultSchedulingHandle(List resultList) { + List userIds = resultList.stream().filter(result -> Objects.equals(result.getType(), 1)).map(DailyRuleResultVo::getUserId).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return resultList; + } + List infoByIds = userApi.getStaffRosterListInfoByIds(userIds); + Map userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, PartUserInfoVo::getRealName)); + resultList.forEach(result -> { + if (result.getType() == 1) { + result.setFailMsg(AttendanceConstant.getSchedulingResult(userMap.get(result.getUserId()), result.getDate(), AttendanceTypeEnum.getMsg(result.getFromType()), AttendanceTypeEnum.getMsg(result.getToType()))); + } + }); + return resultList; + } + + /** + * 固定班规则处理 + * + * @param fixedHandleDTO + */ + private void fixedHandle(FixedHandleDTO fixedHandleDTO) { + List resultList = CollUtil.newArrayList(); + //根据考勤组id查询考勤组班制配置 + List groupIds = fixedHandleDTO.getUsers().stream().map(AttendanceGroupUser::getGroupId).collect(Collectors.toList()); + if (CollUtil.isEmpty(fixedHandleDTO.getShiftSettingMap())) { + Map enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, fixedHandleDTO.getAttendanceGroupVos()); + if (CollUtil.isEmpty(enableShiftSetting)) { + log.error("未查询到所有考勤组班制配置"); + return; + } + fixedHandleDTO.setShiftSettingMap(enableShiftSetting); + } + List dates = DateDetail.getDatesByPeriod(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd()); + Map> collect1 = fixedHandleDTO.getUsers().stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + List collect = fixedHandleDTO.getUsers().stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + //获取每日出勤规则 + List dailyRulesByPeriod = getDailyRulesByPeriod(fixedHandleDTO.getStart(), fixedHandleDTO.getEnd(), collect, groupIds); + //过滤昨日排班数据,次日下班 + Date date1 = new Date(); + dailyRulesByPeriod = dailyRulesByPeriod.stream().filter(rule -> !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) || rule.getInPoint().after(fixedHandleDTO.getStart())).collect(Collectors.toList()); + List collect4 = dailyRulesByPeriod.stream().filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && rule.getInPoint().before(fixedHandleDTO.getStart()) && rule.getOutPoint().after(date1)).collect(Collectors.toList()); + Assert.isFalse(CollUtil.isNotEmpty(collect4), "该班次上班时间与前一天下班时间有冲突!"); + Map> dailyRuleMap = CollUtil.isEmpty(dailyRulesByPeriod) ? Maps.newHashMap() : dailyRulesByPeriod.stream().collect(Collectors.groupingBy(rule -> DateDetail.getDateTime2Str(rule.getDay()) + rule.getGroupId() + rule.getUserId())); + Map enableBaseSetting = CollUtil.isNotEmpty(fixedHandleDTO.getEnableBaseSetting()) ? fixedHandleDTO.getEnableBaseSetting() : attendanceBaseSettingService.getEnableBaseSetting(groupIds, fixedHandleDTO.getAttendanceGroupVos()); + if (CollUtil.isEmpty(fixedHandleDTO.getEnableBaseSetting())) { + fixedHandleDTO.setEnableBaseSetting(enableBaseSetting); + } + List collect5 = fixedHandleDTO.getAttendanceGroupVos().stream().filter(group -> groupIds.contains(group.getId())).collect(Collectors.toList()); + List allRules = CollUtil.newArrayList(); + List allRuleIds = CollUtil.newArrayList(); + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getApplyViewEnable, 3).list(); + Map> collect3 = list.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))); + Map> applyCrossRules = Maps.newHashMap(); + List periodConflictRules = CollUtil.newArrayList(); + collect5.forEach(vo -> { + AttendanceShiftSettingVo attendanceShiftSettingVo = fixedHandleDTO.getShiftSettingMap().get(vo.getId()); + if (Objects.isNull(attendanceShiftSettingVo)) { + log.error("未查询到考勤组[{}]班制时段数据", vo.getId()); + return; + } + if (!Objects.equals(1, attendanceShiftSettingVo.getSystemType())) { + log.error("当请求为固定班规则生成时,过滤非固定班配置{}", vo.getId()); + return; + } + List attendanceGroupUsers = collect1.get(vo.getId()); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + log.error("未查询到当前考勤组人员信息,{}", vo.getId()); + return; + } + AttendanceBaseSetting attendanceBaseSetting = enableBaseSetting.get(vo.getId()); + BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.ZERO : attendanceBaseSetting.getAttendanceRatio(); + List fixedClassShiftVos = attendanceShiftSettingVo.getFixedPeriods(); + if (CollUtil.isEmpty(fixedClassShiftVos)) { + return; + } + Map collect2 = fixedClassShiftVos.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, fixedClassShift -> fixedClassShift)); + Map> userMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + //遍历组员 + userMap.forEach((userId, userList) -> { + periodConflictRules.clear(); + applyCrossRules.clear(); + //遍历每天 + for (Date date : dates) { + Date endByDay = cn.hutool.core.date.DateUtil.endOfDay(date); + AttendanceGroupUser user = userList.stream().filter(user1 -> Objects.equals(user1.getUserGroupType(), 2) || (endByDay.compareTo(user1.getCreatorTime()) >= 0 && (Objects.isNull(user1.getRemoveTime()) ? Boolean.TRUE : date.compareTo(user1.getRemoveTime()) < 0))).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(user)) { + continue; + } + List hisRules = dailyRuleMap.getOrDefault(DateDetail.getDateTime2Str(date) + user.getGroupId() + user.getUserId(), CollUtil.newArrayList()); + //历史规则如果存在划线排班,则过滤 + if (hisRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) { + return; + } + //将夸日的申请规则添加到当前日的规则中方便计算 + applyCrossRulesAdd(hisRules, applyCrossRules, endByDay, date, allRules); + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + List rules = CollUtil.newArrayList(); + int currentWeekNumber = DateDetail.getCurrentWeekNumber(date); + FixedClassShiftVo fixedClassShiftVo = collect2.get(currentWeekNumber); + //未设置固定班次过滤 + if (Objects.isNull(fixedClassShiftVo)) { + fixedClassShiftVo = FixedClassShiftVo.builder().shiftNameVos(CollUtil.newArrayList(new ShiftNameVo())).build(); + } + List shiftNameVos = fixedClassShiftVo.getShiftNameVos(); + ShiftNameVo ordShift = shiftNameVos.stream().filter(shiftNameVo -> Objects.equals(shiftNameVo.getAttendanceType(), 2)).findFirst().orElse(null); + int size = shiftNameVos.size(); + shiftNameVos.forEach(shiftName -> { + if (Objects.equals(shiftName.getType(), 1)) { + buildDailyRule(user, date, rules, 0, Objects.nonNull(ordShift) ? ordShift.getPeriods() : null, attendanceShiftSettingVo, AttendanceTypeEnum.REST, attendanceRatio, 1); + return; + } + if (Objects.equals(shiftName.getType(), 2)) { + if (size < 2) { + shiftName.getPeriods().forEach(period -> buildDailyRule(user, date, rules, 0, CollUtil.newArrayList(period), attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 1)); + return; + } + buildDailyRule(user, date, rules, 0, shiftName.getPeriods(), attendanceShiftSettingVo, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 1); + return; + } + buildDailyRule(user, date, rules, 0, shiftName.getPeriods(), attendanceShiftSettingVo, AttendanceTypeEnum.CLEAR, BigDecimal.ZERO, 1); + }); + if (CollUtil.isEmpty(rules)) { + periodConflictRules.addAll(hisRules); + continue; + } + dailyRuleArrangementHandle(resultList, hisRules, rules); + secondmentDailyRulesHandle(resultList, user, hisRules); + periodConflictRules.addAll(hisRules); + hisRules.stream().filter(ru -> Objects.equals(ru.getAttendanceType(), AttendanceTypeEnum.WORKOVERTIME.getCode())).forEach(ru -> { + OvertimeRuleDetailVo overtimeRuleDetailVo = getOvertimeRuleDetailVo(hisRules, ru.getInPoint(), ru.getOutPoint(), ru.getApplyId()); + ru.setPeriodInfo(Objects.isNull(overtimeRuleDetailVo) ? null : JSON.toJSONString(overtimeRuleDetailVo)); + }); + //将夸日的申请规则添加到当前日的规则中方便计算 + applyCrossRulesAddNotCurrent(hisRules, applyCrossRules, endByDay, date); + allRules.addAll(hisRules); + allRuleIds.addAll(ruleIds); + clearWithdrawRule(collect3.getOrDefault(user.getUserId() + DateDetail.getDate2Str(date, DateDetail.DF), CollUtil.newArrayList()), hisRules); + //判断时段是否冲突 + Assert.isTrue(fixedPeriodConflict(periodConflictRules), "所选班次时段存在冲突!"); + } + setRuleForLateOutLateIn(userId, fixedHandleDTO.getTenantId(), allRules); + }); + }); + saveOrUpdateBatch(allRules, allRuleIds); + attendanceRuleNotificationHandle.sendSystemNotice(resultList, fixedHandleDTO.getTenantId()); + attendanceRuleNotificationHandle.notificationHandle(resultList, fixedHandleDTO.getTenantId()); + } + + /** + * 借调的日规则处理 + * + * @param resultList 处理结果集合 + * @param user 考勤组用户信息 + * @param ftbAttendanceDailyRules 历史考勤规则 + */ + private void secondmentDailyRulesHandle(List resultList, AttendanceGroupUser user, List ftbAttendanceDailyRules) { + List secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? null : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class); + //处理借调类型 + if (CollUtil.isEmpty(secondmentDateVos)) { + ftbAttendanceDailyRules.forEach(rule -> { + rule.setSelfGroup(SecondmentTypeEnum.NOT.getCode()); + }); + return; + } + //历史借调 + hisSecondmentDailyRulesHandle(resultList, user, ftbAttendanceDailyRules, secondmentDateVos); + } + + /** + * 借调的规则生成处理 + * + * @param resultList + * @param user + * @param ftbAttendanceDailyRules + * @param secondmentDateVos + */ + private void hisSecondmentDailyRulesHandle(List resultList, AttendanceGroupUser user, List ftbAttendanceDailyRules, List secondmentDateVos) { + Iterator iterator = ftbAttendanceDailyRules.iterator(); + Map collect1 = ftbAttendanceDailyRules.stream().filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()) && Objects.equals(rule.getApplyUnit(), LeaveUnitEnum.DAY.getCode())).collect(Collectors.toMap(FtbAttendanceDailyRule::getSchedulesType, Function.identity(), (e1, e2) -> e1)); + List rules = CollUtil.newArrayList(); + while (iterator.hasNext()) { + FtbAttendanceDailyRule hisRule = iterator.next(); + Date start = Objects.nonNull(hisRule.getInPoint()) ? hisRule.getInPoint() : hisRule.getDay(); + Date end = Objects.nonNull(hisRule.getOutPoint()) ? hisRule.getOutPoint() : cn.hutool.core.date.DateUtil.endOfDay(hisRule.getDay()); + if (!Objects.equals(user.getUserGroupType(), 2) && (Objects.isNull(user.getCreatorTime()) ? Boolean.FALSE : end.compareTo(user.getCreatorTime()) < 0 || (Objects.isNull(user.getRemoveTime()) ? Boolean.FALSE : user.getRemoveTime().compareTo(start) < 0))) { + continue; + } + if (Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode())) { + continue; + } + secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule.getDay(), cn.hutool.core.date.DateUtil.endOfDay(hisRule.getDay()), hisRule, iterator, rules, collect1); + } + ftbAttendanceDailyRules.addAll(rules); + //当外出、出差标识的排次数据都被借调覆盖时,恢复外出、出差申请记录展示标识 + List collect = ftbAttendanceDailyRules.stream().filter(hisRule -> !Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) && !Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode()) || !Objects.equals(hisRule.getApplyViewEnable(), 0)).collect(Collectors.toList()); + List collect2 = ftbAttendanceDailyRules.stream().filter(hisRule -> Objects.equals(hisRule.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode()) && Objects.equals(hisRule.getApplyViewEnable(), AttendanceTypeEnum.STEP_OUT.getCode())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect2)) { + List collect3 = ftbAttendanceDailyRules.stream().filter(hisRule -> !collect2.contains(hisRule) && !Objects.equals(hisRule.getApplyViewEnable(), AttendanceTypeEnum.STEP_OUT.getCode())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect3)) { + FtbAttendanceDailyRule ftbAttendanceDailyRule = collect2.stream().findFirst().orElse(null); + collect2.forEach(rule -> rule.setApplyViewEnable(0)); + ftbAttendanceDailyRules.add(buildRuleForNail(ftbAttendanceDailyRule.getGroupId(), ftbAttendanceDailyRule.getUserId(), ftbAttendanceDailyRule.getDay(), ftbAttendanceDailyRule.getAttendanceType(), AttendanceTypeEnum.CLEAR)); + } + } + if (CollUtil.isEmpty(collect) && CollUtil.isNotEmpty(ftbAttendanceDailyRules)) { + FtbAttendanceDailyRule ftbAttendanceDailyRule = ftbAttendanceDailyRules.stream().findFirst().orElse(null); + ftbAttendanceDailyRules.add(buildRuleForNail(ftbAttendanceDailyRule.getGroupId(), ftbAttendanceDailyRule.getUserId(), ftbAttendanceDailyRule.getDay(), ftbAttendanceDailyRule.getAttendanceType(), AttendanceTypeEnum.CLEAR)); + } + } + + private void secondmentRuleHandle(List resultList, AttendanceGroupUser user, List secondmentDateVos, Date start, Date end, FtbAttendanceDailyRule hisRule, Iterator iterator, List rules, Map collect1) { + SecondmentDateVo secondmentDateVo; + secondmentDateVos = BeanUtil.copyToList(secondmentDateVos, SecondmentDateVo.class); + while (Objects.nonNull(secondmentDateVo = AttendanceGroupUserStatusUtil.getSecondmentType(secondmentDateVos, start, end)) && !hisRule.getIsDelete()) { + secondmentDateVos.remove(secondmentDateVo); + //被借调组 + if (SecondmentTypeEnum.FROM.getCode().equals(user.getUserGroupType())) { + hisRule.setSelfGroup(SecondmentTypeEnum.FROM.getCode()); + //调休 + if (Objects.isNull(hisRule.getInPoint()) && Objects.equals(AttendanceTypeEnum.REST.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.DEFAULT.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), hisRule.getAttendanceType())) { + DateTime endOfDay = cn.hutool.core.date.DateUtil.endOfDay(hisRule.getDay()); + if (endOfDay.compareTo(secondmentDateVo.getEndTime()) <= 0 && secondmentDateVo.getStartTime().compareTo(hisRule.getDay()) <= 0) { + log.error("被借调组,全天调休规则被借调完全覆盖,直接删除{}", JSON.toJSONString(hisRule)); + iterator.remove(); + hisRule.delete(); + return; + } + log.error("被借调组,全天调休规则直接保存{}", JSON.toJSONString(hisRule)); + continue; + } + if (secondmentDateVo.getStartTime().compareTo(hisRule.getOutPoint()) == 0 || secondmentDateVo.getEndTime().compareTo(hisRule.getInPoint()) == 0) { + if (secondmentDateVo.getStartTime().compareTo(hisRule.getOutPoint()) == 0) { + hisRule.setOutUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode()); + } + if (secondmentDateVo.getEndTime().compareTo(hisRule.getInPoint()) == 0) { + hisRule.setInUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode()); + } + continue; + } + //借调处在班次当中 + if (DateDetail.checkTimeBetween(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && DateDetail.checkTimeBetween(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && secondmentDateVo.getStartTime().compareTo(hisRule.getInPoint()) != 0 && secondmentDateVo.getEndTime().compareTo(hisRule.getOutPoint()) != 0) { + FtbAttendanceDailyRule hisRule2 = BeanUtil.toBean(hisRule, FtbAttendanceDailyRule.class); + hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getStartTime())); + hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getStartTime())); + hisRule.setOutPoint(secondmentDateVo.getStartTime()); + hisRule.setOutUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode()); + hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), hisRule.getInPoint())); + hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), hisRule.getInPoint())); + getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule); + calcPeriodWorkDayForSecondment(hisRule); + hisRule.setOriginOutPoint(secondmentDateVo.getStartTime()); + hisRule.calOriginValidDuration(); + hisRule2.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule2.getInPoint(), hisRule2.getInLackPoint(), secondmentDateVo.getEndTime())); + hisRule2.calLatePoint(DateDetail.getNewDateByPeriod(hisRule2.getInPoint(), hisRule2.getLatePoint(), secondmentDateVo.getEndTime())); + hisRule2.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule2.getInPoint(), hisRule2.getClockStartPoint(), secondmentDateVo.getEndTime())); + hisRule2.setInPoint(secondmentDateVo.getEndTime()); + hisRule2.setInUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode()); + hisRule2.setId(RandomUtil.uuId()); + getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule2); + calcPeriodWorkDayForSecondment(hisRule2); + hisRule2.setOriginInPoint(secondmentDateVo.getEndTime()); + hisRule2.calOriginValidDuration(); + secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule2.getInPoint(), hisRule2.getOutPoint(), hisRule2, iterator, rules, collect1); + rules.add(hisRule2); + log.error("被借调组,借调在在时段中,调整配置{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + continue; + } + //借调开始或者结束是否在班次当中 + //借调开始在班次中 + if (DateDetail.checkTimeBetween(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && secondmentDateVo.getStartTime().compareTo(hisRule.getOutPoint()) != 0 && secondmentDateVo.getStartTime().compareTo(hisRule.getInPoint()) != 0) { + //非调休 + hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getStartTime())); + hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getStartTime())); + hisRule.setOutPoint(secondmentDateVo.getStartTime()); + hisRule.setOutUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode()); + hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), hisRule.getInPoint())); + hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), hisRule.getInPoint())); + getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule); + calcPeriodWorkDayForSecondment(hisRule); + hisRule.setOriginOutPoint(secondmentDateVo.getStartTime()); + hisRule.calOriginValidDuration(); + secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule.getInPoint(), hisRule.getOutPoint(), hisRule, iterator, rules, collect1); + log.error("被借调组,借调开始在时段中,调整下班点配置{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + //如果为半天假全天班,且借调时间完全覆盖下半天假,剩下请假时段请假天数为0.5 + if (Objects.equals(hisRule.getApplyUnit(), 3) && Objects.equals(hisRule.getSchedulesType(), 0)) { + Date leaveDate = getLeaveDate(hisRule); + if (secondmentDateVo.getStartTime().compareTo(leaveDate) <= 0) { + hisRule.setLeaveDay(hisRule.getPeriodWorkDay()); + hisRule.setPayrollHours(hisRule.getPayrollHours()); + } + } + continue; + } + //借调结束在班次中 + if (DateDetail.checkTimeBetween(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && secondmentDateVo.getEndTime().compareTo(hisRule.getInPoint()) != 0 && secondmentDateVo.getEndTime().compareTo(hisRule.getOutPoint()) != 0) { + //非调休 + hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), secondmentDateVo.getEndTime())); + hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), secondmentDateVo.getEndTime())); + hisRule.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getClockStartPoint(), secondmentDateVo.getEndTime())); + hisRule.setInPoint(secondmentDateVo.getEndTime()); + hisRule.setInUnbounded(WorkBoundaryCoverageEnum.SECONDMENT_COVERED.getCode()); + getBreakTimeByOrdinary(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule); + calcPeriodWorkDayForSecondment(hisRule); + hisRule.setOriginInPoint(secondmentDateVo.getEndTime()); + hisRule.calOriginValidDuration(); + secondmentRuleHandle(resultList, user, secondmentDateVos, hisRule.getInPoint(), hisRule.getOutPoint(), hisRule, iterator, rules, collect1); + log.error("被借调组,借调结束在时段中,调整上班点配置{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + //如果为半天假全天班,且借调时间完全覆盖下半天假,剩下请假时段请假天数为0.5 + if (Objects.equals(hisRule.getApplyUnit(), 3) && Objects.equals(hisRule.getSchedulesType(), 0)) { + Date leaveDate = getLeaveDate(hisRule); + if (secondmentDateVo.getEndTime().compareTo(leaveDate) >= 0) { + hisRule.setLeaveDay(hisRule.getPeriodWorkDay()); + } + } + continue; + } + if (DateDetail.checkTimeBetween(hisRule.getInPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime()) && DateDetail.checkTimeBetween(hisRule.getOutPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime())) { + hisRule.setApplyViewEnable(0); + iterator.remove(); + hisRule.delete(); + log.error("被借调组,借调覆盖当前班次隐藏当前规则{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + //如果全天假半天班,剩余班次请假天数调整为 1 + if (Objects.equals(hisRule.getApplyUnit(), 2) && !Objects.equals(hisRule.getSchedulesType(), 0)) { + FtbAttendanceDailyRule ftbAttendanceDailyRule = collect1.get(Objects.equals(hisRule.getSchedulesType(), 1) ? 2 : 1); + if (Objects.nonNull(ftbAttendanceDailyRule)) { + ftbAttendanceDailyRule.setLeaveDay(BigDecimal.ONE); + ftbAttendanceDailyRule.setPayrollHours(hisRule.getPayrollHours()); + } + } + return; + } + hisRule.setSelfGroup(SecondmentTypeEnum.NOT.getCode()); + continue; + } + if (SecondmentTypeEnum.TO.getCode().equals(user.getUserGroupType())) { + hisRule.setSelfGroup(SecondmentTypeEnum.TO.getCode()); + //借调开始或者结束是否在班次当中 + //调休 + if (Objects.isNull(hisRule.getInPoint()) && Objects.equals(AttendanceTypeEnum.REST.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.DEFAULT.getCode(), hisRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), hisRule.getAttendanceType())) { + log.error("借调组,全天调休规则直接保存{}", JSON.toJSONString(hisRule)); + continue; + } + //借调处在班次当中 + if (DateDetail.checkTimeBetween(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint()) && DateDetail.checkTimeBetween(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint())) { + hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getEndTime())); + hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getEndTime())); + hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), secondmentDateVo.getStartTime())); + hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), secondmentDateVo.getStartTime())); + hisRule.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getClockStartPoint(), secondmentDateVo.getStartTime())); + hisRule.setInPoint(secondmentDateVo.getStartTime()); + hisRule.setOriginInPoint(secondmentDateVo.getStartTime()); + hisRule.setOutPoint(secondmentDateVo.getEndTime()); + getBreakTimeByOrdinaryOfSecondment(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule); + calcPeriodWorkDayForSecondment(hisRule); + hisRule.setOriginOutPoint(secondmentDateVo.getEndTime()); + hisRule.calOriginValidDuration(); + log.error("借调组,借调在时段中,调整配置{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + continue; + } + //借调开始在班次中 + if (DateDetail.checkTimeBetweenL(secondmentDateVo.getStartTime(), hisRule.getInPoint(), hisRule.getOutPoint())) { + //非调休 + hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), secondmentDateVo.getStartTime())); + hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), secondmentDateVo.getStartTime())); + hisRule.setClockStartPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getClockStartPoint(), secondmentDateVo.getStartTime())); + hisRule.setInPoint(secondmentDateVo.getStartTime()); + getBreakTimeByOrdinaryOfSecondment(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule); + calcPeriodWorkDayForSecondment(hisRule); + hisRule.setOriginInPoint(secondmentDateVo.getStartTime()); + hisRule.calOriginValidDuration(); + log.error("借调组,借调开始在时段中,调整上班点配置{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + continue; + } + //借调结束在班次中 + if (DateDetail.checkTimeBetweenR(secondmentDateVo.getEndTime(), hisRule.getInPoint(), hisRule.getOutPoint())) { + //调休 + //非调休 + hisRule.setEarlyPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getEarlyPoint(), secondmentDateVo.getEndTime())); + hisRule.setOutLackPoint(DateDetail.getNewDateByPeriod(hisRule.getOutPoint(), hisRule.getOutLackPoint(), secondmentDateVo.getEndTime())); + hisRule.setOutPoint(secondmentDateVo.getEndTime()); + hisRule.calInLackPoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getInLackPoint(), hisRule.getInPoint())); + hisRule.calLatePoint(DateDetail.getNewDateByPeriod(hisRule.getInPoint(), hisRule.getLatePoint(), hisRule.getInPoint())); + getBreakTimeByOrdinaryOfSecondment(secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime(), hisRule); + calcPeriodWorkDayForSecondment(hisRule); + hisRule.setOriginOutPoint(secondmentDateVo.getEndTime()); + hisRule.calOriginValidDuration(); + if (!rules.contains(hisRule)) { + rules.add(hisRule); + } + log.error("借调组,借调结束在时段中,调整下班点配置{}", JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + continue; + } + if (DateDetail.checkTimeBetween(hisRule.getInPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime()) && DateDetail.checkTimeBetween(hisRule.getOutPoint(), secondmentDateVo.getStartTime(), secondmentDateVo.getEndTime())) { + log.error("借调组,借调覆盖当前班次直接保存{}", JSON.toJSONString(hisRule)); + continue; + } + //是否没有下个借调请求 + if (!Objects.nonNull(AttendanceGroupUserStatusUtil.getSecondmentType(secondmentDateVos, start, end))) { + iterator.remove(); + } + log.error("借调组,{}班次未命中借调时间范围,过滤{}", AttendanceTypeEnum.getMsg(hisRule.getAttendanceType()), JSON.toJSONString(hisRule)); + resultList.add(DailyRuleResultVo.secondmentBuild(hisRule)); + } + } + } + + private void calcPeriodWorkDayForSecondment(FtbAttendanceDailyRule rule) { + if (Objects.isNull(rule.getPeriodWorkDay()) || Objects.isNull(rule.getOriginInPoint()) || Objects.isNull(rule.getOriginOutPoint())) { + return; + } + int validDuration = rule.getOriginValidDuration(); + rule.setPeriodWorkDay(BigDecimal.valueOf(rule.getValidDuration()).divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP).multiply(rule.getPeriodWorkDay()).setScale(2, RoundingMode.HALF_UP)); + if (Objects.nonNull(rule.getPayrollHours())) { + rule.setPayrollHours(BigDecimal.valueOf(rule.getValidDuration()).divide(BigDecimal.valueOf(validDuration), 4, RoundingMode.HALF_UP).multiply(rule.getPayrollHours()).setScale(2, RoundingMode.HALF_UP)); + } + } + + /** + * 新增排班的休息时间处理 + * + * @param appleStart 原请假规则 + * @param rule 新增正常排班规则 + */ + private void getBreakTimeByOrdinary(Date appleStart, Date appleEnd, FtbAttendanceDailyRule rule) { + if (!Objects.equals(rule.getBreakEnable(), 1)) { + return; + } + if (Objects.isNull(rule.getBreakStartPoint()) || Objects.isNull(rule.getBreakEndPoint())) { + return; + } + //休息结束时间小于排班上班时间,或者休息开始时间大于排班下班时间 + if (rule.getBreakEndPoint().compareTo(rule.getInPoint()) <= 0 || rule.getBreakStartPoint().compareTo(rule.getOutPoint()) >= 0) { + rule.setBreakStartPoint(null); + rule.setBreakEndPoint(null); + rule.setBreakEnable(0); + return; + } + //借调时间完全覆盖原来休息时段 + if (DateDetail.checkTimeBetween(rule.getBreakStartPoint(), appleStart, appleEnd) && DateDetail.checkTimeBetween(rule.getBreakEndPoint(), appleStart, appleEnd)) { + rule.setBreakStartPoint(null); + rule.setBreakEndPoint(null); + rule.setBreakEnable(0); + return; + } + //休息时间完全覆盖借调时间 + if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint()) && DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) { + if (appleStart.compareTo(rule.getBreakStartPoint()) >= 0) { + rule.setBreakEndPoint(appleStart); + rule.setOriginBreakEndPoint(appleStart); + } + if (appleEnd.compareTo(rule.getBreakEndPoint()) <= 0) { + rule.setBreakStartPoint(appleEnd); + rule.setOriginBreakStartPoint(appleEnd); + } + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) { + rule.setBreakStartPoint(appleEnd); + rule.setOriginBreakStartPoint(appleEnd); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint())) { + rule.setBreakEndPoint(appleStart); + rule.setOriginBreakEndPoint(appleStart); + + } + } + + /** + * 新增排班的休息时间处理 + * + * @param appleStart 原请假规则 + * @param rule 新增正常排班规则 + */ + private void getBreakTimeByOrdinaryOfSecondment(Date appleStart, Date appleEnd, FtbAttendanceDailyRule rule) { + if (!Objects.equals(rule.getBreakEnable(), 1)) { + return; + } + if (Objects.isNull(rule.getBreakStartPoint()) || Objects.isNull(rule.getBreakEndPoint())) { + return; + } + //休息结束时间小于排班上班时间,或者休息开始时间大于排班下班时间 + if (rule.getBreakEndPoint().compareTo(rule.getInPoint()) <= 0 || rule.getBreakStartPoint().compareTo(rule.getOutPoint()) >= 0) { + rule.setBreakStartPoint(null); + rule.setBreakEndPoint(null); + rule.setBreakEnable(0); + return; + } + //借调时间完全覆盖原来休息时段 + if (DateDetail.checkTimeBetween(rule.getBreakStartPoint(), appleStart, appleEnd) && DateDetail.checkTimeBetween(rule.getBreakEndPoint(), appleStart, appleEnd)) { + return; + } + //休息时间完全覆盖借调时间 + if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint()) && DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) { + if (appleStart.compareTo(rule.getBreakStartPoint()) >= 0) { + rule.setBreakStartPoint(appleStart); + } + if (appleEnd.compareTo(rule.getBreakEndPoint()) <= 0) { + rule.setBreakEndPoint(appleEnd); + } + return; + } + //部分覆盖 + //开始部分覆盖 + if (DateDetail.checkTimeBetween(appleEnd, rule.getBreakStartPoint(), rule.getBreakEndPoint())) { + rule.setBreakEndPoint(appleEnd); + return; + } + //结束部分覆盖 + if (DateDetail.checkTimeBetween(appleStart, rule.getBreakStartPoint(), rule.getBreakEndPoint())) { + rule.setBreakStartPoint(appleStart); + } + } + + /** + * 新增借调借调组的规则处理 + * + * @param nextShiftSetting + * @param date + * @param user + * @param ftbAttendanceDailyRules + */ + private void afterSecondmentRulesHandle(AttendanceShiftSettingVo nextShiftSetting, Date date, AttendanceGroupUser user, List ftbAttendanceDailyRules, AttendanceBaseSetting attendanceBaseSetting) { + if (CollUtil.isEmpty(ftbAttendanceDailyRules)) { + //是否为固定班 + if (!Objects.equals(1, nextShiftSetting.getSystemType())) { + log.error("新增借调无历史规则且非固定班,不生成固定班规则{}", user.getUserId()); + return; + } + if (!SecondmentTypeEnum.TO.getCode().equals(user.getUserGroupType())) { + log.error("新增借调当前非借调组,不生成固定班规则{}", user.getUserId()); + return; + } + BigDecimal attendanceRatio = Objects.isNull(attendanceBaseSetting) ? BigDecimal.ZERO : attendanceBaseSetting.getAttendanceRatio(); + List fixedClassShiftVos = nextShiftSetting.getFixedPeriods(); + Map collect = fixedClassShiftVos.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, fixedClassShift -> fixedClassShift)); + int currentWeekNumber = DateDetail.getCurrentWeekNumber(date); + FixedClassShiftVo fixedClassShiftVo = collect.get(currentWeekNumber); + List shiftNameVos = fixedClassShiftVo.getShiftNameVos(); + ShiftNameVo ordShift = shiftNameVos.stream().filter(shiftNameVo -> Objects.equals(shiftNameVo.getAttendanceType(), 2)).findFirst().orElse(null); + shiftNameVos.forEach(shiftName -> { + if (Objects.equals(shiftName.getType(), 1)) { + buildDailyRule(user, date, ftbAttendanceDailyRules, shiftName.getFixedSort(), Objects.nonNull(ordShift) ? ordShift.getPeriods() : null, nextShiftSetting, AttendanceTypeEnum.REST, attendanceRatio, 1); + } else { + if (StringUtil.isEmpty(shiftName.getId())) { + return; + } + buildDailyRule(user, date, ftbAttendanceDailyRules, shiftName.getFixedSort(), shiftName.getPeriods(), nextShiftSetting, AttendanceTypeEnum.ORDINARY, BigDecimal.ZERO, 1); + } + }); + ftbAttendanceDailyRules.forEach(rule -> rule.setSelfGroup(2)); + log.error("新增借调当前借调组用户[{}],生成固定班规则为[{}]", user.getUserId(), JsonUtil.getObjectToString(ftbAttendanceDailyRules)); + } + } + + /** + * 页面类型转排班表类型 + * + * @param type 页面传入类型 (0清除 1休 2时段 3假期(抵扣卷) 4假期(兑换卷)) + * @return + */ + private AttendanceTypeEnum getType2AttendanceType(Integer type) { + if (0 == type) { + return AttendanceTypeEnum.CLEAR; + } + if (1 == type) { + return AttendanceTypeEnum.REST; + } + if (3 == type) { + return AttendanceTypeEnum.DEDUCTION_HOLIDAYS; + } + if (4 == type) { + return AttendanceTypeEnum.EXCHANGE_HOLIDAYS; + } + return AttendanceTypeEnum.ORDINARY; + } + + + /** + * 页面类型转排班表类型 + * + * @param attendanceType 页面传入类型 (0清除 1休 2时段 3假期(抵扣卷) 4假期(兑换卷)) + * @return + */ + private Integer getAttendanceType2type(Integer attendanceType) { + if (Objects.equals(AttendanceTypeEnum.CLEAR.getCode(), attendanceType)) { + return 0; + } + if (Objects.equals(AttendanceTypeEnum.REST.getCode(), attendanceType)) { + return 1; + } + if (Objects.equals(AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), attendanceType)) { + return 3; + } + if (Objects.equals(AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode(), attendanceType)) { + return 4; + } + return 2; + } + + /** + * 获取排班展示类型 + * + * @param rule + * @return + */ + private Integer getType(FtbAttendanceDailyRule rule) { + //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班 +// 13借调 14被借调 +// * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况) + if (AttendanceTypeEnum.ORDINARY.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.REST.getCode(); + } + if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.PAID_HOLIDAYS.getCode(); + + } + return SchedulesTypeEnum.NONE.getCode(); + } + + /** + * 获取排班展示类型 + * + * @param rule + * @param groupUser + * @param day + * @return + */ + private Integer getType(FtbAttendanceDailyRule rule, AttendanceGroupUser groupUser, Date day, List attendanceClockInResults, Map clockInMap, Integer isSchedules) { + //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班 +// 13借调 14被借调 +// * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况) + if (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.LEAVE.getCode(); + } + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.OVERTIME.getCode(); + } + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.STEP_OUT.getCode(); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.BUSINESS_TRIP.getCode(); + } + Date date = new Date(); + if (AttendanceTypeEnum.DEFAULT.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.NONE.getCode(); + } + if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.REST.getCode(); + } + if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.PAID_HOLIDAYS.getCode(); + } + if (DateDetail.checkTimeBetween(date, rule.getClockStartPoint(), rule.getOutPoint())) { + if (CollUtil.isEmpty(attendanceClockInResults)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + //上班打卡 + AttendanceClockInResult attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(attendanceClockInResult)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + } + //查询最后异常打卡结果 + //是否有旷工 + List clockInResults = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getAbsence(), 1) && (Objects.equals(result.getClockInType(), 1) || Objects.equals(result.getClockInType(), 2))).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(clockInResults) && clockInResults.size() > 1) { + return SchedulesTypeEnum.ABSENCE.getCode(); + } + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> !Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + Integer clockInStatus = attendanceClockInResult.getClockInStatus(); + //缺卡 + if (Objects.equals(clockInStatus, -1)) { + return SchedulesTypeEnum.MISSING.getCode(); + } + //迟到 + if (Objects.equals(clockInStatus, 2)) { + return SchedulesTypeEnum.LATE.getCode(); + } + //早退 + if (Objects.equals(clockInStatus, 3)) { + return SchedulesTypeEnum.EARLY.getCode(); + } + } + //外勤打卡 + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInKind(), 2)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).findFirst().orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + return SchedulesTypeEnum.FIELD_WORK.getCode(); + } + return SchedulesTypeEnum.IN_CLOCK.getCode(); + } + //处理历史数据 + if (rule.getOutPoint().before(date)) { + if (CollUtil.isEmpty(attendanceClockInResults)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + //查询最后异常打卡结果 + //是否有旷工 + List clockInResults = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getAbsence(), 1) && (Objects.equals(result.getClockInType(), 1) || Objects.equals(result.getClockInType(), 2))).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(clockInResults) && clockInResults.size() > 1) { + return SchedulesTypeEnum.ABSENCE.getCode(); + } + //异常打卡 + AttendanceClockInResult attendanceClockInResult = attendanceClockInResults.stream().filter(result -> !Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + Integer clockInStatus = attendanceClockInResult.getClockInStatus(); + //缺卡 + if (Objects.equals(clockInStatus, -1)) { + return SchedulesTypeEnum.MISSING.getCode(); + } + //迟到 + if (Objects.equals(clockInStatus, 2)) { + return SchedulesTypeEnum.LATE.getCode(); + } + //早退 + if (Objects.equals(clockInStatus, 3)) { + return SchedulesTypeEnum.EARLY.getCode(); + } + } + //外勤打卡 + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInKind(), 2)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).findFirst().orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + return SchedulesTypeEnum.FIELD_WORK.getCode(); + } + //上班打卡 + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(attendanceClockInResult)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + } + //下班打卡 + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 2)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(attendanceClockInResult)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) { + rule.setOutPoint(ftbAttendanceClockIn.getClockInTime()); + } + return SchedulesTypeEnum.NORMAL.getCode(); + } + + List secondmentDateVos = StringUtil.isEmpty(groupUser.getTimeJson()) ? null : JsonUtil.getJsonToList(groupUser.getTimeJson(), SecondmentDateVo.class); + if (CollUtil.isNotEmpty(secondmentDateVos)) { + for (SecondmentDateVo secondmentDate : secondmentDateVos) { + if (DateDetail.checkTimeBetween(day, secondmentDate.getStartTime(), secondmentDate.getEndTime())) { + return SchedulesTypeEnum.NOT_IN_GROUP.getCode(); + } + } + } + if (AttendanceTypeEnum.ORDINARY.getCode().equals(rule.getAttendanceType())) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + return SchedulesTypeEnum.NONE.getCode(); + } + + private Integer getType2(FtbAttendanceDailyRule rule, List attendanceTypeOfApply, List attendanceClockInResults) { + //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班 +// 13借调 14被借调 +// * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况) + Integer schedulesType = SchedulesTypeEnum.NORMAL.getCode(); + if (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.LEAVE.getCode()); + } + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.OVERTIME.getCode()); + return null; + } + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode()); + schedulesType = SchedulesTypeEnum.STEP_OUT.getCode(); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode()); + schedulesType = SchedulesTypeEnum.BUSINESS_TRIP.getCode(); + } + if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) { + schedulesType = SchedulesTypeEnum.REST.getCode(); + } + if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + schedulesType = SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + schedulesType = SchedulesTypeEnum.HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + schedulesType = SchedulesTypeEnum.PAID_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getApplyViewEnable())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode()); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode()); + } + if (AttendanceTypeEnum.DEFAULT.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.CLEAR.getCode().equals(rule.getAttendanceType())) { + schedulesType = SchedulesTypeEnum.NONE.getCode(); + return schedulesType; + } + if (CollUtil.isEmpty(attendanceClockInResults)) { + return schedulesType; + } + attendanceClockInResults.forEach(attendanceClockInResult -> { + if (Objects.nonNull(attendanceClockInResult) && Objects.equals(attendanceClockInResult.getRepaired(), 1)) { + //补卡 + attendanceTypeOfApply.add(SchedulesTypeEnum.REISSUE.getCode()); + } + if (StringUtil.isNotEmpty(attendanceClockInResult.getAbsenceLeader())) { + //出勤变更 + attendanceTypeOfApply.add(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode()); + } + //外勤打卡 + if (Objects.equals(attendanceClockInResult.getClockInKind(), 2)) { + attendanceTypeOfApply.add(SchedulesTypeEnum.FIELD_WORK.getCode()); + } + }); + return schedulesType; + } + + /** + * 获取排班展示类型 + * + * @param rule + * @param groupUser + * @param day + * @param attendanceTypeOfApply + * @return + */ + private Integer getType(FtbAttendanceDailyRule rule, SchedulesItemVo build, AttendanceGroupUser groupUser, Date day, List attendanceClockInResults, Map clockInMap, Integer isSchedules, List attendanceTypeOfApply) { + //1休息日 2正常出勤 3假期 4带薪假期 5未来的时间不在本组 6迟到 7早退 8缺卡 9旷工 10外勤 11请假 12加班 +// 13借调 14被借调 +// * 15当日有早退且借调情况 16当日有迟到且借调情况 17当日有缺卡且借调情况 18当日有旷工且借调情况 19当日有打外勤且借调情况) + Integer schedulesType = SchedulesTypeEnum.NORMAL.getCode(); + if (AttendanceTypeEnum.LEAVE.getCode().equals(rule.getAttendanceType())) { + build.setStartType(SchedulesTypeEnum.LEAVE.getCode()); + build.setEndType(SchedulesTypeEnum.LEAVE.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.LEAVE.getCode()); + schedulesType = SchedulesTypeEnum.LEAVE.getCode(); + } + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) { + attendanceTypeOfApply.add(SchedulesTypeEnum.OVERTIME.getCode()); + } + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType())) { + rule.setInPoint(null); + rule.setOutPoint(null); + build.setStartType(SchedulesTypeEnum.STEP_OUT.getCode()); + build.setEndType(SchedulesTypeEnum.STEP_OUT.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode()); + schedulesType = SchedulesTypeEnum.STEP_OUT.getCode(); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) { + rule.setInPoint(null); + rule.setOutPoint(null); + build.setStartType(SchedulesTypeEnum.BUSINESS_TRIP.getCode()); + build.setEndType(SchedulesTypeEnum.BUSINESS_TRIP.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode()); + schedulesType = SchedulesTypeEnum.BUSINESS_TRIP.getCode(); + } + if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType())) { + build.setStartType(SchedulesTypeEnum.REST.getCode()); + build.setEndType(SchedulesTypeEnum.REST.getCode()); + schedulesType = SchedulesTypeEnum.REST.getCode(); + } + if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + build.setStartType(SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode()); + build.setEndType(SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode()); + schedulesType = SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + build.setStartType(SchedulesTypeEnum.HOLIDAYS.getCode()); + build.setEndType(SchedulesTypeEnum.HOLIDAYS.getCode()); + schedulesType = SchedulesTypeEnum.HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + build.setStartType(SchedulesTypeEnum.PAID_HOLIDAYS.getCode()); + build.setEndType(SchedulesTypeEnum.PAID_HOLIDAYS.getCode()); + schedulesType = SchedulesTypeEnum.PAID_HOLIDAYS.getCode(); + } + Integer inStepOutType = null; + Integer outStepOutType = null; + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getApplyViewEnable())) { + inStepOutType = Objects.equals(rule.getInStepOutType(), 0) ? null : SchedulesTypeEnum.STEP_OUT.getCode(); + outStepOutType = Objects.equals(rule.getOutStepOutType(), 0) ? null : SchedulesTypeEnum.STEP_OUT.getCode(); + attendanceTypeOfApply.add(SchedulesTypeEnum.STEP_OUT.getCode()); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getApplyViewEnable())) { + inStepOutType = SchedulesTypeEnum.BUSINESS_TRIP.getCode(); + outStepOutType = SchedulesTypeEnum.BUSINESS_TRIP.getCode(); + attendanceTypeOfApply.add(SchedulesTypeEnum.BUSINESS_TRIP.getCode()); + } + if (AttendanceTypeEnum.DEFAULT.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.CLEAR.getCode().equals(rule.getAttendanceType())) { + build.setStartType(SchedulesTypeEnum.NONE.getCode()); + build.setEndType(SchedulesTypeEnum.NONE.getCode()); + schedulesType = SchedulesTypeEnum.NONE.getCode(); + return schedulesType; + } + if (Objects.nonNull(inStepOutType)) { + build.setStartType(SchedulesTypeEnum.NONE.getCode()); + build.setEndType(SchedulesTypeEnum.NONE.getCode()); + if (Objects.isNull(rule.getInPoint())) { + build.setStartType(inStepOutType); + build.setEndType(inStepOutType); + } + } + //处理打卡 + if (CollUtil.isEmpty(attendanceClockInResults)) { + return schedulesType; + } + //是否有操作人 + boolean absenceLeader = attendanceClockInResults.stream().anyMatch(result -> StringUtil.isNotEmpty(result.getAbsenceLeader())); + if (absenceLeader) { + attendanceTypeOfApply.add(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode()); + } + if (AttendanceTypeEnum.REST.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.HOLIDAYS.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(rule.getAttendanceType())) { + return schedulesType; + } + //异常打卡 + List attendanceClockInResultList = attendanceClockInResults.stream().filter(result -> !Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(attendanceClockInResultList)) { + for (AttendanceClockInResult attendanceClockInResult : attendanceClockInResultList) { + Integer clockInStatus = attendanceClockInResult.getClockInStatus(); + //旷工 + if (Objects.equals(attendanceClockInResult.getAbsence(), 1) && StringUtil.isEmpty(attendanceClockInResult.getAbsenceLeader()) && (Objects.equals(attendanceClockInResult.getClockInType(), 1) || Objects.equals(attendanceClockInResult.getClockInType(), 2)) && StringUtil.isEmpty(attendanceClockInResult.getAbsenceLeader())) { + build.setStartType(SchedulesTypeEnum.ABSENCE.getCode()); + build.setEndType(SchedulesTypeEnum.ABSENCE.getCode()); + schedulesType = SchedulesTypeEnum.ABSENCE.getCode(); + continue; + } + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + //早退 + if (Objects.equals(clockInStatus, 3)) { + rule.setOutPoint(ftbAttendanceClockIn.getClockInTime()); + build.setEndType(SchedulesTypeEnum.EARLY.getCode()); + schedulesType = SchedulesTypeEnum.EARLY.getCode(); + } + //迟到 + if (Objects.equals(clockInStatus, 2)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + build.setStartType(SchedulesTypeEnum.LATE.getCode()); + schedulesType = SchedulesTypeEnum.LATE.getCode(); + } + //缺卡 + if (Objects.equals(clockInStatus, -1)) { + if (Objects.equals(attendanceClockInResult.getClockInType(), 1)) { + build.setStartType(SchedulesTypeEnum.MISSING.getCode()); + } else { + build.setEndType(SchedulesTypeEnum.MISSING.getCode()); + } + schedulesType = SchedulesTypeEnum.MISSING.getCode(); + } + } + } + //上班打卡 + AttendanceClockInResult attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + //补卡 + if (Objects.equals(attendanceClockInResult.getRepaired(), 1)) { + build.setStartType(SchedulesTypeEnum.REISSUE.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.REISSUE.getCode()); + } else { + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + build.setStartType(SchedulesTypeEnum.NORMAL.getCode()); + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) { + build.setStartType(Objects.nonNull(inStepOutType) ? inStepOutType : SchedulesTypeEnum.OVERTIME.getCode()); + schedulesType = Objects.nonNull(inStepOutType) ? inStepOutType : SchedulesTypeEnum.OVERTIME.getCode(); + } + } + } + } + + //下班打卡 + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 2) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + //补卡 + if (Objects.equals(attendanceClockInResult.getRepaired(), 1)) { + build.setEndType(SchedulesTypeEnum.REISSUE.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.REISSUE.getCode()); + } else { + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn) && Objects.equals(isSchedules, 0)) { + build.setEndType(SchedulesTypeEnum.NORMAL.getCode()); + rule.setOutPoint(ftbAttendanceClockIn.getClockInTime()); + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(rule.getAttendanceType())) { + build.setEndType(Objects.nonNull(outStepOutType) ? outStepOutType : SchedulesTypeEnum.OVERTIME.getCode()); + schedulesType = Objects.nonNull(outStepOutType) ? outStepOutType : SchedulesTypeEnum.OVERTIME.getCode(); + } + + } + } + } + + //外勤打卡 + attendanceClockInResultList = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInKind(), 2) && StringUtil.isEmpty(result.getApplyId())).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(attendanceClockInResultList)) { + for (AttendanceClockInResult clockInResult : attendanceClockInResultList) { + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(clockInResult.getClockInId()); + if (Objects.equals(clockInResult.getClockInType(), 1)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + build.setStartType(SchedulesTypeEnum.FIELD_WORK.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.FIELD_WORK.getCode()); + } else { + rule.setOutPoint(ftbAttendanceClockIn.getClockInTime()); + build.setEndType(SchedulesTypeEnum.FIELD_WORK.getCode()); + attendanceTypeOfApply.add(SchedulesTypeEnum.FIELD_WORK.getCode()); + } + } + schedulesType = SchedulesTypeEnum.FIELD_WORK.getCode(); + } + + //考勤变更 + attendanceClockInResultList = attendanceClockInResults.stream().filter(result -> StringUtil.isNotEmpty(result.getAbsenceLeader())).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(attendanceClockInResultList)) { + for (AttendanceClockInResult attendanceClockInResult1 : attendanceClockInResultList) { + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult1.getClockInId()); + Integer clockInStatus = attendanceClockInResult1.getClockInStatus(); + if (Objects.equals(attendanceClockInResult1.getAbsence(), 1)) { + schedulesType = SchedulesTypeEnum.ATTENDANCE_CHANGE_ABSENCE.getCode(); + build.setStartType(SchedulesTypeEnum.ATTENDANCE_CHANGE_ABSENCE.getCode()); + build.setEndType(SchedulesTypeEnum.ATTENDANCE_CHANGE_ABSENCE.getCode()); + } + if (Objects.equals(clockInStatus, 1)) { + if (Objects.equals(attendanceClockInResult1.getClockInType(), 1)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + build.setStartType(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode()); + } else { + rule.setOutPoint(ftbAttendanceClockIn.getClockInTime()); + build.setEndType(SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode()); + } + schedulesType = SchedulesTypeEnum.ATTENDANCE_CHANGE_NORMAL.getCode(); + } + } + } +/** + * applyViewEnable为外出、出差设置build的startType和EndType(上和下班类型)字段在分别存在打卡记录是为正常出勤,不存在时根据rule的applyViewEnable设置为外出或者出差, + * 存在上下班打卡时设置build的InPoint和OutPoint(上下班时间)为上下班打卡时间,出存在时设置为原InPoint和OutPoint值 + */ + if (Objects.nonNull(inStepOutType)) { + build.setStartType(SchedulesTypeEnum.NONE.getCode()); + build.setEndType(SchedulesTypeEnum.NONE.getCode()); + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 1) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn)) { + rule.setInPoint(ftbAttendanceClockIn.getClockInTime()); + build.setStartType(inStepOutType); + } + } + schedulesType = inStepOutType; + } + if (Objects.nonNull(outStepOutType)) { + attendanceClockInResult = attendanceClockInResults.stream().filter(result -> Objects.equals(result.getClockInType(), 2) && Objects.equals(result.getClockInStatus(), 1)).sorted(Comparator.comparing(AttendanceClockInResult::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.nonNull(attendanceClockInResult)) { + FtbAttendanceClockIn ftbAttendanceClockIn = clockInMap.get(attendanceClockInResult.getClockInId()); + if (Objects.nonNull(ftbAttendanceClockIn)) { + rule.setOutPoint(ftbAttendanceClockIn.getClockInTime()); + build.setEndType(outStepOutType); + } + } + + schedulesType = outStepOutType; + } + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(rule.getAttendanceType()) || AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(rule.getAttendanceType())) { + build.setStartType(inStepOutType); + build.setEndType(outStepOutType); + rule.setInPoint(null); + rule.setOutPoint(null); + } + List secondmentDateVos = StringUtil.isEmpty(groupUser.getTimeJson()) ? null : JsonUtil.getJsonToList(groupUser.getTimeJson(), SecondmentDateVo.class); + if (CollUtil.isNotEmpty(secondmentDateVos)) { + for (SecondmentDateVo secondmentDate : secondmentDateVos) { + if (DateDetail.checkTimeBetween(day, secondmentDate.getStartTime(), secondmentDate.getEndTime())) { + return SchedulesTypeEnum.NOT_IN_GROUP.getCode(); + } + } + } + return schedulesType; + } + + private String getDate2Str(Date date) { + return DateDetail.getDate2Str(date, DateDetail.DF10); + } + + private Date getStr2Date(String date) { + return DateDetail.getStr2Date10(date); + } + + /** + * 执行借调规则生成 + * + * @param resultList 异常结果响应 + * @param hisDailyRules 历史规则 + * @param currDailyRule 当前新增规则 + */ + private void dailyRuleArrangementHandle(List resultList, List hisDailyRules, List currDailyRule) { + hisDailyRules.removeIf(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.DEFAULT.getCode())); + if (CollUtil.isEmpty(hisDailyRules)) { + currDailyRule = currDailyRule.stream().filter(rule -> !Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.CLEAR.getCode())).collect(Collectors.toList()); + hisDailyRules.addAll(currDailyRule); + currDailyRule.forEach(rule -> { + rule.insertTrue(); + resultList.add(DailyRuleResultVo.successNailBuild(rule)); + }); + return; + } + workOvertimeRuleProcessor.setNext(ordinaryRuleProcessor).setNext(restRuleProcessor).setNext(leaveRuleProcessor).setNext(stepOutRuleProcessor); + workOvertimeRuleProcessor.ruleArrangementHandle(hisDailyRules, currDailyRule, resultList); + if (CollUtil.isEmpty(resultList)) { + resultList.addAll(currDailyRule.stream().map(DailyRuleResultVo::successNailBuild).collect(Collectors.toList())); + } + } + + + private void buildDailyRule(AttendanceGroupUser user, Date date, List rules, Integer schedulesType, List periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, BigDecimal validDuration, Integer fixedMark) { + buildDailyRule(user, date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, null, null, null, validDuration, fixedMark, null); + } + + private void buildDailyRule(String groupId, String userId, Date date, List rules, Integer schedulesType, List periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, BigDecimal validDuration, Integer fixedMark) { + buildDailyRule(groupId, userId, date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, null, null, null, validDuration, fixedMark, null, null); + } + + private void buildDailyRule(String groupId, String userId, Date date, List rules, Integer schedulesType, List periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, BigDecimal validDuration, Integer fixedMark, BigDecimal paidBalance) { + buildDailyRule(groupId, userId, date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, null, null, null, validDuration, fixedMark, paidBalance, null); + } + + private FtbAttendanceDailyRule buildDailyRule(AttendanceGroupUser user, Date date, List rules, Integer schedulesType, List periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, String applyId, Date applyStart, Date applyEnd, BigDecimal validDuration, Integer fixedMark, LeaveParam leaveParam) { + return buildDailyRule(user.getGroupId(), user.getUserId(), date, rules, schedulesType, periods, attendanceShiftSettingVo, attendanceTypeEnum, applyId, applyStart, applyEnd, validDuration, fixedMark, null, leaveParam); + } + + private FtbAttendanceDailyRule buildDailyRule(String groupId, String userId, Date date, List rules, Integer schedulesType, List periods, AttendanceShiftSettingVo attendanceShiftSettingVo, AttendanceTypeEnum attendanceTypeEnum, String applyId, Date applyStart, Date applyEnd, BigDecimal validDuration, Integer fixedMark, BigDecimal balance, LeaveParam leaveParam) { + FtbAttendanceDailyRule rule = new FtbAttendanceDailyRule(); + rule.setGroupId(groupId); + rule.setSchedulesType(schedulesType); + rule.setUserId(userId); + rule.setDeleteMark(0); + rule.setSelfGroup(0); + rule.setId(RandomUtil.uuId()); + rule.setDay(date); + rule.setFixedMark(Objects.isNull(fixedMark) ? 0 : fixedMark); + rule.setApplyViewEnable(1); + if (Objects.nonNull(leaveParam)) { + rule.setLeaveParam(JSON.toJSONString(leaveParam)); + rule.setApplyUnit(leaveParam.getLeaveUnit()); + if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.LEAVE.getCode())) { + if (leaveParam.getStart().compareTo(date) == 0) { + if (Objects.equals(leaveParam.getStartTimeType(), 2)) { + rule.setLeaveDay(BigDecimal.valueOf(0.5)); + } + } + if (leaveParam.getEnd().compareTo(date) == 0) { + if (Objects.equals(leaveParam.getEndTimeType(), 1)) { + rule.setLeaveDay(BigDecimal.valueOf(0.5)); + } + } + } + } + if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.LEAVE.getCode())) { + rule.setApplyViewEnable(2); + } + if (StringUtil.isNotEmpty(applyId)) { + rule.setApplyId(applyId); + } + if (Objects.nonNull(applyStart)) { + rule.setApplyStart(applyStart); + rule.setInPoint(applyStart); + } + if (Objects.nonNull(applyEnd)) { + rule.setApplyEnd(applyEnd); + rule.setOutPoint(applyEnd); + } + rule.setAttendanceType(attendanceTypeEnum.getCode()); + rule.setValidDuration(Objects.isNull(validDuration) ? 0 : validDuration.multiply(BigDecimal.valueOf(60)).setScale(0, RoundingMode.HALF_UP).intValue()); + if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.STEP_OUT.getCode())) { + rule.setApplyViewEnable(attendanceTypeEnum.getCode()); + rule.setValidDuration(0); + } + if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()) || Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.REST.getCode())) { + rule.setValidDuration(0); + } + buildHalfRestDailyRule(rule, periods); + if (Objects.equals(attendanceTypeEnum.getCode(), AttendanceTypeEnum.ORDINARY.getCode())) { + if (CollUtil.isEmpty(periods)) { + return rule; + } + List schedulingPeriods = attendanceShiftSettingVo.getSchedulingPeriods(); + if (!Objects.equals(schedulesType, 0) && periods.size() < 2) { + AttendanceShiftSettingPeriodVo period = periods.get(0); + buildDay2HalfDailyRule(rule, period); + if (CollUtil.isNotEmpty(schedulingPeriods)) { + ShiftNameVo shiftNameVo = schedulingPeriods.stream().filter(r -> Objects.equals(r.getId(), period.getShiftId())).findFirst().orElse(null); + if (Objects.nonNull(shiftNameVo)) { + rule.setPeriodInfo(JSON.toJSONString(shiftNameVo)); + } + } + rules.add(rule); + return rule; + } + AttendanceShiftSettingPeriodVo period = Objects.equals(schedulesType, 0) || Objects.equals(schedulesType, 1) ? periods.stream().findFirst().orElse(null) : periods.stream().reduce((r1, r2) -> r2).orElse(null); + rule.setSort(period.getSort()); + rule.setShiftId(period.getShiftId()); + rule.setPeriodId(period.getId()); + if (CollUtil.isNotEmpty(schedulingPeriods)) { + ShiftNameVo shiftNameVo = schedulingPeriods.stream().filter(r -> Objects.equals(r.getId(), period.getShiftId())).findFirst().orElse(null); + if (Objects.nonNull(shiftNameVo)) { + rule.setPeriodInfo(JSON.toJSONString(shiftNameVo)); + } + } + rule.setLateEnable(period.getLateEnable()); + rule.setEarlyEnable(period.getEarlyEnable()); + rule.setBreakEnable(period.getBreakEnable()); + rule.setInUnbounded(0); + rule.setOutUnbounded(0); + rule.setNextDayEnable(period.getNextDayEnable()); + rule.setSchedulesType(Objects.isNull(period.getType()) ? 0 : period.getType()); + rule.setClockStartPoint(DateDetail.getDateByPoint(date, period.getClockStartPoint(), period.getClockStartPointType())); + rule.setInPoint(DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType())); + rule.setOutPoint(DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType())); + rule.setOriginInPoint(rule.getInPoint()); + rule.setOriginOutPoint(rule.getOutPoint()); + rule.calLatePoint(DateDetail.getDateByPoint(date, period.getLatePoint(), period.getLateType())); + rule.setOriginalLatePoint(rule.getLatePoint()); + rule.setIsLateOutLateIn(0); + rule.calInLackPoint(DateDetail.getDateByPoint(date, period.getInLackPoint(), period.getInLackPointType())); + rule.setBreakStartPoint(DateDetail.getDateByPoint(date, period.getBreakStartPoint(), period.getBreakStartType())); + rule.setBreakEndPoint(DateDetail.getDateByPoint(date, period.getBreakEndPoint(), period.getBreakEndType())); + rule.setOriginBreakStartPoint(rule.getBreakStartPoint()); + rule.setOriginBreakEndPoint(rule.getBreakEndPoint()); + rule.setEarlyPoint(DateDetail.getDateByPoint(date, period.getEarlyPoint(), period.getEarlyType())); + rule.setOutLackPoint(DateDetail.getDateByPoint(date, period.getOutLackPoint(), period.getOutLackType())); + rule.setTenantId(period.getTenantId()); + rule.setPeriodWorkDay(period.getTimeSlotDay()); + rule.setPayrollHours(period.getPayrollHours()); + rule.setValidDuration(DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint())); + if (rule.getBreakEnable() == 1 && Objects.nonNull(rule.getBreakStartPoint()) && Objects.nonNull(rule.getBreakEndPoint())) { + rule.setValidDuration(rule.getValidDuration() - DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint())); + } + rule.setOriginValidDuration(rule.getValidDuration()); + + } + rules.add(rule); + return rule; + } + + /** + * 构建公休REST的DailyRule实体 + */ + private void buildHalfRestDailyRule(FtbAttendanceDailyRule restRule, List periods) { + if (Objects.equals(restRule.getAttendanceType(), AttendanceTypeEnum.REST.getCode())) { + if (CollUtil.isEmpty(periods)) { + restRule.setPeriodWorkDay(BigDecimal.ONE); + restRule.setPayrollHours(restRule.getPayrollHours()); + return; + } + if (periods.size() > 1) { + AttendanceShiftSettingPeriodVo periodVo = periods.get(Objects.equals(restRule.getSchedulesType(), 0) ? 0 : restRule.getSchedulesType() - 1); + restRule.setPeriodWorkDay(periodVo.getTimeSlotDay()); + restRule.setPayrollHours(periodVo.getPayrollHours()); + restRule.setValidDuration(periodVo.calValidDuration()); + restRule.setOriginValidDuration(periodVo.getValidDuration()); + restRule.setLateEnable(periodVo.getLateEnable()); + restRule.setEarlyEnable(periodVo.getEarlyEnable()); + restRule.setBreakEnable(periodVo.getBreakEnable()); + restRule.setInUnbounded(0); + restRule.setOutUnbounded(0); + restRule.setNextDayEnable(periodVo.getNextDayEnable()); + restRule.setSchedulesType(Objects.isNull(periodVo.getType()) ? 0 : periodVo.getType()); + restRule.setClockStartPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getClockStartPoint(), periodVo.getClockStartPointType())); + restRule.setInPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getInPoint(), periodVo.getInType())); + restRule.setOutPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getOutPoint(), periodVo.getOutType())); + restRule.setOriginInPoint(restRule.getInPoint()); + restRule.setOriginOutPoint(restRule.getOutPoint()); + restRule.calLatePoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getLatePoint(), periodVo.getLateType())); + restRule.setOriginalLatePoint(restRule.getLatePoint()); + restRule.setIsLateOutLateIn(0); + restRule.calInLackPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getInLackPoint(), periodVo.getInLackPointType())); + restRule.setBreakStartPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getBreakStartPoint(), periodVo.getBreakStartType())); + restRule.setBreakEndPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getBreakEndPoint(), periodVo.getBreakEndType())); + restRule.setOriginBreakStartPoint(restRule.getBreakStartPoint()); + restRule.setOriginBreakEndPoint(restRule.getBreakEndPoint()); + restRule.setEarlyPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getEarlyPoint(), periodVo.getEarlyType())); + restRule.setOutLackPoint(DateDetail.getDateByPoint(restRule.getDay(), periodVo.getOutLackPoint(), periodVo.getOutLackType())); + restRule.setTenantId(periodVo.getTenantId()); + return; + } + buildDay2HalfDailyRule(restRule, periods.get(0)); + } + } + + private void buildDay2HalfDailyRule(FtbAttendanceDailyRule rule, AttendanceShiftSettingPeriodVo period) { + rule.setSort(period.getSort()); + rule.setShiftId(period.getShiftId()); + rule.setPeriodId(period.getId()); + rule.setLateEnable(period.getLateEnable()); + rule.setEarlyEnable(period.getEarlyEnable()); + rule.setBreakEnable(period.getBreakEnable()); + rule.setNextDayEnable(period.getNextDayEnable()); + rule.setClockStartPoint(DateDetail.getDateByPoint(rule.getDay(), period.getClockStartPoint(), period.getClockStartPointType())); + rule.setInPoint(DateDetail.getDateByPoint(rule.getDay(), period.getInPoint(), period.getInType())); + rule.setOutPoint(DateDetail.getDateByPoint(rule.getDay(), period.getOutPoint(), period.getOutType())); + rule.calInLackPoint(DateDetail.getDateByPoint(rule.getDay(), period.getInLackPoint(), period.getInLackPointType())); + rule.calLatePoint(DateDetail.getDateByPoint(rule.getDay(), period.getLatePoint(), period.getLateType())); + rule.setEarlyPoint(DateDetail.getDateByPoint(rule.getDay(), period.getEarlyPoint(), period.getEarlyType())); + rule.setOutLackPoint(DateDetail.getDateByPoint(rule.getDay(), period.getOutLackPoint(), period.getOutLackType())); + + boolean isRest = Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.REST.getCode()); + rule.setOriginInPoint(rule.getInPoint()); + rule.setOriginOutPoint(rule.getOutPoint()); + rule.setBreakStartPoint(DateDetail.getDateByPoint(rule.getDay(), period.getBreakStartPoint(), period.getBreakStartType())); + rule.setBreakEndPoint(DateDetail.getDateByPoint(rule.getDay(), period.getBreakEndPoint(), period.getBreakEndType())); + //上半天休 + int periodMinuteAll = DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint()); + DateTime point = cn.hutool.core.date.DateUtil.offsetMinute(rule.getInPoint(), periodMinuteAll / 2); + if (Objects.equals(rule.getSchedulesType(), 1)) { + if (Objects.nonNull(rule.getBreakStartPoint())) { + Date outPoint = isRest ? rule.getBreakEndPoint() : rule.getBreakStartPoint(); + rule.setEarlyPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getEarlyPoint(), outPoint)); + rule.setOutLackPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getOutLackPoint(), outPoint)); + int breakMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + int newMinute = DateDetail.calculateMinuteDiff(rule.getInPoint(), outPoint) - (isRest ? breakMinute : 0); + int periodMinute = periodMinuteAll - breakMinute; + rule.setOutPoint(outPoint); + rule.setOriginOutPoint(outPoint); + if (!isRest) { + rule.setBreakStartPoint(null); + rule.setBreakEndPoint(null); + } + rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 1, RoundingMode.HALF_UP))); + rule.setOriginValidDuration(rule.getValidDuration()); + return; + } + rule.setEarlyPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getEarlyPoint(), point)); + rule.setOutLackPoint(DateDetail.getNewDateByPeriod(rule.getOutPoint(), rule.getOutLackPoint(), point)); + rule.setOutPoint(point); + rule.setOriginOutPoint(point); + int newMinute = DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint()); + rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP))); + rule.setPayrollHours(period.getPayrollHours().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP))); + rule.setOriginValidDuration(rule.getValidDuration()); + return; + } + if (Objects.equals(rule.getSchedulesType(), 2)) { + if (Objects.nonNull(rule.getBreakStartPoint())) { + Date inPoint = isRest ? rule.getBreakStartPoint() : rule.getBreakEndPoint(); + rule.calInLackPoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getInLackPoint(), inPoint)); + rule.calLatePoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getLatePoint(), inPoint)); + int breakMinute = DateDetail.calculateMinuteDiff(rule.getBreakStartPoint(), rule.getBreakEndPoint()); + int newMinute = DateDetail.calculateMinuteDiff(inPoint, rule.getOutPoint()) - (isRest ? breakMinute : 0); + int periodMinute = periodMinuteAll - breakMinute; + rule.setInPoint(inPoint); + rule.setOriginInPoint(inPoint); + if (!isRest) { + rule.setBreakStartPoint(null); + rule.setBreakEndPoint(null); + } + rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 1, RoundingMode.HALF_UP))); + rule.setPayrollHours(period.getPayrollHours().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 1, RoundingMode.HALF_UP))); + rule.setOriginValidDuration(rule.getValidDuration()); + return; + } + rule.calInLackPoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getInLackPoint(), point)); + rule.calLatePoint(DateDetail.getNewDateByPeriod(rule.getInPoint(), rule.getLatePoint(), point)); + rule.setOriginalLatePoint(rule.getLatePoint()); + rule.setIsLateOutLateIn(0); + rule.setInPoint(point); + rule.setOriginInPoint(point); + int newMinute = DateDetail.calculateMinuteDiff(rule.getInPoint(), rule.getOutPoint()); + rule.setPeriodWorkDay(period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP))); + rule.setPayrollHours(period.getPayrollHours().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 1, RoundingMode.HALF_UP))); + rule.setOriginValidDuration(rule.getValidDuration()); + } + } + + private BigDecimal calPeriodWorkDay(Integer schedulesType, List periods) { + if (CollUtil.isEmpty(periods) || Objects.equals(schedulesType, 0)) { + return BigDecimal.ONE; + } + if (periods.size() > 1) { + return periods.get(schedulesType - 1).getTimeSlotDay(); + } + AttendanceShiftSettingPeriodVo period = periods.get(0); + Date date = new Date(); + Date periodInPoint = DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType()); + Date periodOutPoint = DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType()); + Date periodBreakStart = DateDetail.getDateByPoint(date, period.getBreakStartPoint(), period.getBreakStartType()); + Date periodBreakEnd = DateDetail.getDateByPoint(date, period.getBreakEndPoint(), period.getBreakEndType()); + int periodMinuteAll = DateDetail.calculateMinuteDiff(periodInPoint, periodOutPoint); + int breakMinute = Objects.isNull(periodBreakStart) ? 0 : DateDetail.calculateMinuteDiff(periodBreakStart, periodBreakEnd); + DateTime point = cn.hutool.core.date.DateUtil.offsetMinute(periodInPoint, periodMinuteAll / 2); + if (Objects.equals(schedulesType, 1)) { + if (Objects.nonNull(periodBreakEnd)) { + int newMinute = DateDetail.calculateMinuteDiff(periodInPoint, periodBreakEnd) - breakMinute; + int periodMinute = periodMinuteAll - breakMinute; + return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 2, RoundingMode.HALF_UP)); + } + int newMinute = DateDetail.calculateMinuteDiff(periodInPoint, point); + return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 2, RoundingMode.HALF_UP)); + } + if (Objects.equals(schedulesType, 2)) { + if (Objects.nonNull(periodBreakStart)) { + int newMinute = DateDetail.calculateMinuteDiff(periodBreakStart, periodOutPoint) - breakMinute; + int periodMinute = periodMinuteAll - breakMinute; + return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinute), 2, RoundingMode.HALF_UP)); + } + int newMinute = DateDetail.calculateMinuteDiff(point, periodOutPoint); + return period.getTimeSlotDay().multiply(BigDecimal.valueOf(newMinute).divide(BigDecimal.valueOf(periodMinuteAll), 2, RoundingMode.HALF_UP)); + } + return period.getTimeSlotDay(); + } + + /** + * 构建针对填充数据的考勤规则 + * + * @param groupId 部门ID,标识考勤规则适用的部门 + * @param userId 用户ID,标识考勤规则适用的用户 + * @param date 日期,标识考勤规则适用的日期 + * @param applyViewEnable 考勤类型,如周休日、节假日等 + * @param attendanceTypeEnum 考勤类型枚举,用于详细指定考勤规则的类型 + * @return 返回构建的考勤规则对象 + */ + public FtbAttendanceDailyRule buildRuleForNail(String groupId, String userId, Date date, Integer applyViewEnable, AttendanceTypeEnum attendanceTypeEnum) { + // 创建一个新的考勤规则对象 + FtbAttendanceDailyRule rule = new FtbAttendanceDailyRule(); + // 设置部门ID + rule.setGroupId(groupId); + // 设置考勤类型 + rule.setSchedulesType(0); + // 设置用户ID + rule.setUserId(userId); + // 设置未删除标记 + rule.setDeleteMark(0); + // 设置非自建群组 + rule.setSelfGroup(0); + // 生成并设置唯一ID + rule.setId(RandomUtil.uuId()); + // 设置适用日期 + rule.setDay(date); + // 设置考勤类型代码 + rule.setAttendanceType(attendanceTypeEnum.getCode()); + rule.setApplyViewEnable(applyViewEnable); + // 设置有效时长为0,表示全天有效 + rule.setValidDuration(0); + // 返回构建的考勤规则对象 + return rule; + } + + /** + * 根据时段及用户集合、考勤组集合查询规则列表 + * + * @param start 开始时间 + * @param end 结束时间 + * @param userIds 用户id集合 + * @param groupId 考勤id集合 + * @return + */ + private List getDailyRulesByPeriod(Date start, Date end, List userIds, List groupId) { + List list = lambdaQuery().in(CollUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).and(x -> x.or(x1 -> x1.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).between(FtbAttendanceDailyRule::getDay, start, end)).or(x1 -> x1.ge(FtbAttendanceDailyRule::getOutPoint, start).le(FtbAttendanceDailyRule::getInPoint, end)).or(x1 -> x1.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode()).lt(FtbAttendanceDailyRule::getDay, start).le(FtbAttendanceDailyRule::getApplyStart, end).ge(FtbAttendanceDailyRule::getApplyEnd, start)).or(x1 -> x1.between(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(start), end).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()))).in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds).list(); + return BeanUtil.copyToList(list, FtbAttendanceDailyRule.class); + } + + /** + * 是否存在划线排班 + *@param userId 用户id + * @param start 开始时间 + * @param end 结束时间 + * @return + */ + @Override + public boolean hasLinearRulesByPeriod(String userId, Date start, Date end) { + Long count = lambdaQuery().eq(FtbAttendanceDailyRule::getFixedMark, 2).and(x -> x.or(x1 -> x1.eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()).between(FtbAttendanceDailyRule::getDay, start, end)).or(x1 -> x1.ge(FtbAttendanceDailyRule::getOutPoint, start).le(FtbAttendanceDailyRule::getInPoint, end)).or(x1 -> x1.between(FtbAttendanceDailyRule::getDay, cn.hutool.core.date.DateUtil.beginOfDay(start), end).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.DEFAULT.getCode(), AttendanceTypeEnum.CLEAR.getCode(), AttendanceTypeEnum.REST.getCode(), AttendanceTypeEnum.HOLIDAYS.getCode(), AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode(), AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode()))).eq(FtbAttendanceDailyRule::getUserId, userId).count(); + return count > 0; + } + + /** + * 根据时段及用户集合、考勤组集合查询规则列表,查询时间为日 + * + * @param start 开始时间 + * @param end 结束时间 + * @param userIds 用户id集合 + * @param groupId 考勤id集合 + * @return + */ + private List getDailyRulesByPeriodForStepOut(Date start, Date end, List userIds, List groupId) { + List list = lambdaQuery().between(FtbAttendanceDailyRule::getDay, start, end).in(CollUtil.isNotEmpty(userIds), FtbAttendanceDailyRule::getUserId, userIds).in(CollUtil.isNotEmpty(groupId), FtbAttendanceDailyRule::getGroupId, groupId).list(); + return BeanUtil.copyToList(list, FtbAttendanceDailyRule.class); + } + + + /** + * 划线排班(支持同用户不同日期不同时段) + * + * @param configDto 排班配置 + * @return 错误信息,空表示成功 + */ + private String setLineDrawingSchedulesWithMultiPeriods(LineDrawingSchedulesConfigDto configDto) { + // 1. 参数验证 + Assert.isFalse(Objects.isNull(configDto), "未查询到排班配置数据!"); + Assert.isFalse(StringUtil.isEmpty(configDto.getGroupId()), "考勤组ID不能为空!"); + Assert.isFalse(CollUtil.isEmpty(configDto.getUserList()), "未找到需要划线排班的用户"); + + // 2. 收集所有日期和用户ID(去重) + Set allDatesSet = new HashSet<>(); + List userIds = new ArrayList<>(); + + for (LineDrawingUserDto userDto : configDto.getUserList()) { + userIds.add(userDto.getUserId()); + if (CollUtil.isNotEmpty(userDto.getPeriods())) { + for (LineDrawingPeriodDto period : userDto.getPeriods()) { + if (Objects.nonNull(period.getDay())) { + allDatesSet.add(period.getDay()); + } + } + } + } + + Assert.isFalse(allDatesSet.isEmpty(), "未找到排班日期"); + + Date start = allDatesSet.stream().min(Date::compareTo).orElse(null); + Date end = cn.hutool.core.date.DateUtil.endOfDay(allDatesSet.stream().max(Date::compareTo).orElse(null)); + + // 扩展日期范围(前后各一天,用于冲突检测) + List days = new ArrayList<>(allDatesSet); + days.add(cn.hutool.core.date.DateUtil.offsetDay(start, -1)); + days.add(cn.hutool.core.date.DateUtil.offsetDay(end, 1)); + + // 3. 获取分布式锁 + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), configDto.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中,请稍后再试"); + + try { + Assert.isFalse(!lock.tryLock(10, 60, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中,请稍后再试"); + + // 4. 查询基础数据 + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(configDto.getGroupId()); + Assert.isFalse(Objects.isNull(lineSchedulingConfig), "未查询到考勤组的划线排班配置!"); + + List users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, userIds, List.of(configDto.getGroupId())); + Assert.isFalse(CollUtil.isEmpty(users), "当前人员所选日期不在考勤组!"); + Map> usersMap = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + + // 5. 查询历史规则 + List hisRulesAll = lambdaQuery() + .eq(FtbAttendanceDailyRule::getGroupId, configDto.getGroupId()) + .in(FtbAttendanceDailyRule::getUserId, userIds) + .in(FtbAttendanceDailyRule::getDay, days) + .list(); + + Map> ruleMap = hisRulesAll.stream() + .collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))); + Map> ruleUserMap = hisRulesAll.stream() + .collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)); + + // 6. 处理每个用户的排班 + List resultList = new CopyOnWriteArrayList<>(); + List allRules = new CopyOnWriteArrayList<>(); + List allRuleIds = new CopyOnWriteArrayList<>(); + List isExists = new CopyOnWriteArrayList<>(); + List payrollHoursList = new CopyOnWriteArrayList<>(); + + for (LineDrawingUserDto userDto : configDto.getUserList()) { + processUserLineDrawingSchedules(userDto, configDto.getGroupId(), lineSchedulingConfig, + usersMap, ruleMap, ruleUserMap, days, + resultList, allRules, allRuleIds, isExists, payrollHoursList); + } + + Assert.isFalse(CollUtil.isNotEmpty(isExists) && isExists.stream().noneMatch(boo -> boo), "当前时间不能复制"); + + // 7. 保存数据 + saveOrUpdateBatch(allRules, allRuleIds); + savePayrollHoursAndHandleNotification(configDto, userIds, days, lineSchedulingConfig, + resultList, payrollHoursList); + + return resultSchedulingHandle(resultList).stream() + .map(DailyRuleResultVo::getFailMsg) + .filter(StringUtil::isNotEmpty) + .reduce((msg1, msg2) -> msg1 + "\br" + msg2) + .orElse(""); + + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + /** + * 处理单个用户的划线排班 + */ + private void processUserLineDrawingSchedules(LineDrawingUserDto userDto, String groupId, + FtbAttendanceLineSchedulingConfig lineSchedulingConfig, + Map> usersMap, + Map> ruleMap, + Map> ruleUserMap, + List days, + List resultList, + List allRules, + List allRuleIds, + List isExists, + List payrollHoursList) { + String userId = userDto.getUserId(); + List allPeriods = userDto.getPeriods(); + + // 按日期分组时段 + Map> dayPeriodsMap = new HashMap<>(); + Map dayPayrollHoursMap = new HashMap<>(); + + if (CollUtil.isNotEmpty(allPeriods)) { + for (LineDrawingPeriodDto period : allPeriods) { + String dayStr = DateDetail.getDate2Str(period.getDay(), DateDetail.DF); + dayPeriodsMap.computeIfAbsent(dayStr, k -> new ArrayList<>()).add(period); + + if (Objects.nonNull(userDto.getPayrollHours())) { + dayPayrollHoursMap.put(dayStr, userDto.getPayrollHours()); + } + } + } + + // 用于快速查找排班日期 + Set schedulingDays = dayPeriodsMap.keySet(); + + // 检查时段是否未变化(优化:避免重复保存) + if (shouldSkipUpdate(allPeriods, ruleUserMap, userId)) { + // 只更新计薪工时 + for (Map.Entry entry : dayPayrollHoursMap.entrySet()) { + Date day = DateUtil.stringToDates(entry.getKey()); + payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder() + .day(day).groupId(groupId).userId(userId) + .payrollHours(entry.getValue()).build()); + } + return; + } + + // 处理排班 + List userList = usersMap.getOrDefault(userId, CollUtil.newArrayList()); + List periodConflictRules = CollUtil.newArrayList(); + + for (Date day : days) { + List attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsers( + day, cn.hutool.core.date.DateUtil.endOfDay(day), userList); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + continue; + } + + AttendanceGroupUser user = attendanceGroupUsers.stream().reduce((r1, r2) -> r2).orElse(null); + String date2Str = DateDetail.getDate2Str(day, DateDetail.DF); + List hisRules = ruleMap.getOrDefault(userId + date2Str, CollUtil.newArrayList()); + + // 非排班日期,只检查冲突 + if (!schedulingDays.contains(date2Str)) { + periodConflictRules.addAll(hisRules); + continue; + } + + // 处理排班日期的规则 + processDrawingDay(userDto, groupId, userId, day, date2Str, dayPeriodsMap, dayPayrollHoursMap, + attendanceGroupUsers, user, hisRules, lineSchedulingConfig, + allRuleIds, isExists, resultList, allRules, periodConflictRules, payrollHoursList); + } + + Assert.isTrue(fixedPeriodConflict(periodConflictRules), "设置班次时段与前一日/后一日班次时段存在冲突!"); + } + + /** + * 检查是否应该跳过更新(时段未变化) + */ + private boolean shouldSkipUpdate(List allPeriods, + Map> ruleUserMap, String userId) { + if (CollUtil.isEmpty(allPeriods) || !allPeriods.stream().allMatch(vo -> StringUtil.isNotEmpty(vo.getId()))) { + return false; + } + + List list = ruleUserMap.getOrDefault(userId, CollUtil.newArrayList()); + List ids = allPeriods.stream().map(LineDrawingPeriodDto::getId).collect(Collectors.toList()); + List ruleIds = list.stream() + .filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())) + .map(FtbAttendanceDailyRule::getId) + .collect(Collectors.toList()); + + return CollectionUtils.isEqualCollection(ids, ruleIds); + } + + /** + * 处理单个排班日期的规则 + */ + private void processDrawingDay(LineDrawingUserDto userDto, String groupId, String userId, + Date day, String date2Str, + Map> dayPeriodsMap, + Map dayPayrollHoursMap, + List attendanceGroupUsers, + AttendanceGroupUser user, + List hisRules, + FtbAttendanceLineSchedulingConfig lineSchedulingConfig, + List allRuleIds, List isExists, + List resultList, + List allRules, + List periodConflictRules, + List payrollHoursList) { + List dayPeriods = dayPeriodsMap.getOrDefault(date2Str, CollUtil.newArrayList()); + BigDecimal dayPayrollHours = dayPayrollHoursMap.get(date2Str); + + // 保存计薪工时 + if (Objects.nonNull(dayPayrollHours)) { + payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder() + .day(day).groupId(groupId).userId(userId) + .payrollHours(dayPayrollHours).build()); + } + + // 提取历史规则ID + List ruleIds = hisRules.stream() + .map(FtbAttendanceDailyRule::getId) + .filter(StringUtil::isNotBlank) + .collect(Collectors.toList()); + allRuleIds.addAll(ruleIds); + + List newRules = new ArrayList<>(); + + if (CollUtil.isNotEmpty(dayPeriods)) { + // 遍历该日期的每个时段 + for (LineDrawingPeriodDto period : dayPeriods) { + Date startByPoint = DateDetail.getDateByPoint(day, period.getStart(), period.getStartType()); + Date endByPoint = DateDetail.getDateByPoint(day, period.getEnd(), period.getEndType()); + Integer existStatus = isExistStatus(attendanceGroupUsers, startByPoint, endByPoint); + + if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 0) || Objects.equals(existStatus, 2)) { + isExists.add(Boolean.FALSE); + continue; + } + isExists.add(Boolean.TRUE); + + newRules.add(buildLineSchedulesRule(groupId, user.getType(), day, startByPoint, endByPoint, userId, lineSchedulingConfig)); + } + } else if (hisRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) { + // 清除规则 + FtbAttendanceDailyRule clearRule = buildLineSchedulesRule(groupId, user.getType(), day, day, day, userId, lineSchedulingConfig); + clearRule.setAttendanceType(AttendanceTypeEnum.CLEAR.getCode()); + newRules.add(clearRule); + isExists.add(Boolean.TRUE); + } + + // 处理规则安排 + dailyRuleArrangementHandle(resultList, hisRules, newRules); + attendanceGroupUsers.forEach(user1 -> secondmentDailyRulesHandle(resultList, user1, hisRules)); + fillInNailRuleForOnlyStepOut(hisRules); + + allRules.addAll(hisRules); + periodConflictRules.addAll(hisRules); + } + + /** + * 保存计薪工时并处理通知 + */ + private void savePayrollHoursAndHandleNotification(LineDrawingSchedulesConfigDto configDto, + List userIds, List days, + FtbAttendanceLineSchedulingConfig lineSchedulingConfig, + List resultList, + List payrollHoursList) { + // 查询原有计薪工时 + List oldPayrollHoursList = lineSchedulingPayrollHoursService.lambdaQuery() + .in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds) + .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, configDto.getGroupId()) + .in(FtbAttendanceLineSchedulingPayrollHours::getDay, days) + .eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0) + .list(); + + Map oldPayrollHoursMap = oldPayrollHoursList.stream() + .collect(Collectors.toMap( + ph -> ph.getUserId() + DateDetail.getDateTime2Str(ph.getDay()) + ph.getPayrollHours(), + FtbAttendanceLineSchedulingPayrollHours::getPayrollHours + )); + + // 保存新计薪工时 + lineSchedulingPayrollHoursService.savePayrollHoursList(payrollHoursList); + + // 发送通知 + if (Objects.equals(lineSchedulingConfig.getEmployeeNotify(), 1)) { + attendanceRuleNotificationHandle.sendSystemNoticeForLine(resultList, configDto, days, null); + } + + // 比对计薪工时变动 + Map resultMap = resultList.stream() + .collect(Collectors.toMap( + vo -> vo.getUserId() + DateDetail.getDateTime2Str(vo.getDate()), + Function.identity(), + (r1, r2) -> r1 + )); + + payrollHoursList.stream() + .filter(vo -> Objects.nonNull(vo.getPayrollHours())) + .filter(vo -> { + String key = vo.getUserId() + DateDetail.getDateTime2Str(vo.getDay()); + return !oldPayrollHoursMap.containsKey(key + vo.getPayrollHours()) && !resultMap.containsKey(key); + }) + .forEach(vo -> { + DailyRuleResultVo dailyRuleResultVo = new DailyRuleResultVo(); + dailyRuleResultVo.setType(0); + dailyRuleResultVo.setGroupId(vo.getGroupId()); + dailyRuleResultVo.setUserId(vo.getUserId()); + dailyRuleResultVo.setDate(vo.getDay()); + resultList.add(dailyRuleResultVo); + }); + + // 处理考勤规则通知 + attendanceRuleNotificationHandle.notificationHandle(resultList, false); + } + + public String setLineDrawingSchedules(LineDrawingSchedulesConfigDto configDto) { + // 1. 参数验证 + Assert.isFalse(Objects.isNull(configDto), "未查询到排班配置数据!"); + Assert.isFalse(StringUtil.isEmpty(configDto.getGroupId()), "考勤组ID不能为空!"); + Assert.isFalse(StringUtil.isEmpty(configDto.getDays()), "结束日期不能为空!"); + List schedulesDays = Arrays.stream(StringUtil.split(configDto.getDays(), ",")).map(DateUtil::stringToDates).collect(Collectors.toList()); + + Date start = schedulesDays.stream().min(Date::compareTo).orElse(null); + Date end = cn.hutool.core.date.DateUtil.endOfDay(schedulesDays.stream().max(Date::compareTo).orElse(null)); + DateTime theDayBefore = cn.hutool.core.date.DateUtil.offsetDay(start, -1); + DateTime theDayAfter = cn.hutool.core.date.DateUtil.offsetDay(end, 1); + List days = new ArrayList<>(schedulesDays); + days.add(0, theDayBefore); + days.add(theDayAfter); + // 2. 获取分布式锁,防止并发操作 + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), configDto.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中,请稍后再试"); + + try { + // 3. 尝试获取锁 + Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中,请稍后再试"); + // 4. 根据考勤组ID查询划线排班配置 + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(configDto.getGroupId()); + Assert.isFalse(Objects.isNull(lineSchedulingConfig), "未查询到考勤组的划线排班配置!"); + // 6. 获取所有用户ID + List userIds = configDto.getUserList().stream().map(LineDrawingUserDto::getUserId).collect(Collectors.toList()); + Assert.isFalse(CollUtil.isEmpty(userIds), "未找到需要划线排班的用户"); + // 7. 查询用户考勤组信息 + List users = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, userIds, List.of(configDto.getGroupId())); + Assert.isFalse(CollUtil.isEmpty(users), "当前人员所选日期不在考勤组!"); + Map> usersMap = users.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + // 9. 初始化结果列表 + List resultList = new CopyOnWriteArrayList<>(); + List allRules = new CopyOnWriteArrayList<>(); + List allRuleIds = new CopyOnWriteArrayList<>(); + // 11.3.2 查询历史考勤规则 + List hisRulesAll = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, configDto.getGroupId()).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getDay, days).list(); + Map> ruleMap = hisRulesAll.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))); + Map> ruleUserMap = hisRulesAll.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)); + List isExists = new CopyOnWriteArrayList<>(); + List payrollHoursList = new CopyOnWriteArrayList<>(); + // 11. 遍历每个用户 + configDto.getUserList().forEach(userDto -> { + // 11.3.4 构建新的考勤规则 + List newRules = new ArrayList<>(); + String userId = userDto.getUserId(); + List periods = userDto.getPeriods(); + if (CollUtil.isNotEmpty(periods) && periods.stream().allMatch(vo -> StringUtil.isNotEmpty(vo.getId()))) { + //如果划线排班时段都存在id,则判断已有的只包含普班排班是否的id是否和划线排班时段的全部id一致 + List list = ruleUserMap.getOrDefault(userId, CollUtil.newArrayList()); + List ids = periods.stream().map(LineDrawingPeriodDto::getId).collect(Collectors.toList()); + List ruleIds = list.stream().filter(rule -> Objects.equals(rule.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode())).map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + if (CollectionUtils.isEqualCollection(ids, ruleIds)) { + schedulesDays.forEach(day -> payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder().day(day).groupId(configDto.getGroupId()).userId(userDto.getUserId()).payrollHours(userDto.getPayrollHours()).build())); + return; + } + } + // 11.3.14 处理借调日常规则 + List userList = usersMap.getOrDefault(userId, CollUtil.newArrayList()); + // 初始化一个列表,用于存储时段冲突的考勤规则 + List periodConflictRules = CollUtil.newArrayList(); + for (Date day : days) { + List attendanceGroupUsers = attendanceGroupUserService.getAttendanceGroupUsers(day, cn.hutool.core.date.DateUtil.endOfDay(day), userList); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + continue; + } + AttendanceGroupUser user = attendanceGroupUsers.stream().reduce((r1, r2) -> r2).orElse(null); + String date2Str = DateDetail.getDate2Str(day, DateDetail.DF); + List hisRules = ruleMap.getOrDefault(userId + date2Str, CollUtil.newArrayList()); + if (!configDto.getDays().contains(date2Str)) { + periodConflictRules.addAll(hisRules); + continue; + } + payrollHoursList.add(FtbAttendanceLineSchedulingPayrollHours.builder().day(day).groupId(configDto.getGroupId()).userId(userDto.getUserId()).payrollHours(userDto.getPayrollHours()).build()); + // 11.3.3 提取历史规则ID + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + allRuleIds.addAll(ruleIds); + newRules.clear(); + if (CollUtil.isNotEmpty(periods)) { + // 11.2 遍历每个时段 + // 11.3 遍历每个日期,构建考勤规则 + for (LineDrawingPeriodDto period : periods) { + Date startByPoint = DateDetail.getDateByPoint(day, period.getStart(), period.getStartType()); + Date endByPoint = DateDetail.getDateByPoint(day, period.getEnd(), period.getEndType()); + Integer existStatus = isExistStatus(attendanceGroupUsers, startByPoint, endByPoint); + if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 0) || Objects.equals(existStatus, 2)) { + isExists.add(Boolean.FALSE); + continue; + } + isExists.add(Boolean.TRUE); + // 11.3.12 添加到新规则列表 + newRules.add(buildLineSchedulesRule(configDto.getGroupId(), user.getType(), day, startByPoint, endByPoint, userId, lineSchedulingConfig)); + } + } else if (hisRules.stream().anyMatch(rule -> Objects.equals(rule.getFixedMark(), 2))) { + //清除规则 + FtbAttendanceDailyRule ftbAttendanceDailyRule = buildLineSchedulesRule(configDto.getGroupId(), user.getType(), day, day, day, userId, lineSchedulingConfig); + ftbAttendanceDailyRule.setAttendanceType(AttendanceTypeEnum.CLEAR.getCode()); + newRules.add(ftbAttendanceDailyRule); + isExists.add(Boolean.TRUE); + } + // 11.3.13 处理规则安排 + dailyRuleArrangementHandle(resultList, hisRules, newRules); + attendanceGroupUsers.forEach(user1 -> secondmentDailyRulesHandle(resultList, user1, hisRules)); + // 11.3.15 填充仅外出的规则 + fillInNailRuleForOnlyStepOut(hisRules); + // 11.3.16 添加到所有规则列表 + allRules.addAll(hisRules); + periodConflictRules.addAll(hisRules); + } + //判断时段是否冲突 + Assert.isTrue(fixedPeriodConflict(periodConflictRules), "设置班次时段与前一日/后一日班次时段存在冲突!"); + }); + Assert.isFalse(CollUtil.isNotEmpty(isExists) && isExists.stream().noneMatch(boo -> boo), "当前时间不能复制"); + // 12. 保存或更新考勤规则 + saveOrUpdateBatch(allRules, allRuleIds); + List oldPayrollHoursList = lineSchedulingPayrollHoursService.lambdaQuery().in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds).eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, configDto.getGroupId()).in(FtbAttendanceLineSchedulingPayrollHours::getDay, days).eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0).list(); + //根据用户id与日期聚合 + Map oldPayrollHoursMap = oldPayrollHoursList.stream().collect(Collectors.toMap(payrollHours -> payrollHours.getUserId() + DateDetail.getDateTime2Str(payrollHours.getDay()) + payrollHours.getPayrollHours(), FtbAttendanceLineSchedulingPayrollHours::getPayrollHours)); + //保存划线排班计薪工时设置 + lineSchedulingPayrollHoursService.savePayrollHoursList(payrollHoursList); + if (Objects.equals(lineSchedulingConfig.getEmployeeNotify(), 1)) { + // 13. 发送系统通知 + attendanceRuleNotificationHandle.sendSystemNoticeForLine(resultList, configDto, null); + } + //比对出计薪工时有变动的数据,oldPayrollHoursList为原计薪工时数据,payrollHoursList为新计薪工时数据 + Map collect = resultList.stream().collect(Collectors.toMap(vo -> vo.getUserId() + DateDetail.getDateTime2Str(vo.getDate()), Function.identity(), (r1, r2) -> r1)); + payrollHoursList.stream().filter(vo -> Objects.nonNull(vo.getPayrollHours())).filter(vo -> { + String key = vo.getUserId() + DateDetail.getDateTime2Str(vo.getDay()); + return !oldPayrollHoursMap.containsKey(key + vo.getPayrollHours()) && !collect.containsKey(key); + }).forEach(vo -> { + DailyRuleResultVo dailyRuleResultVo = new DailyRuleResultVo(); + dailyRuleResultVo.setType(0); + dailyRuleResultVo.setGroupId(vo.getGroupId()); + dailyRuleResultVo.setUserId(vo.getUserId()); + dailyRuleResultVo.setDate(vo.getDay()); + resultList.add(dailyRuleResultVo); + }); + String result = resultSchedulingHandle(resultList).stream().map(DailyRuleResultVo::getFailMsg).filter(StringUtil::isNotEmpty).reduce((msg1, msg2) -> msg1 + "\br" + msg2).orElse(""); + // 14. 处理考勤规则通知 + attendanceRuleNotificationHandle.notificationHandle(resultList, false); + // 15. 返回处理结果 + return result; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + // 16. 释放分布式锁 + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + /** + * 查询指定日期集合指定考勤组指定人员是否存在划线排班 + */ + @Override + public boolean queryLineSchedulingExist(LineDrawingSchedulesConfigDto configDto) { + // 1. 参数验证 + Assert.isFalse(Objects.isNull(configDto), "未查询到排班配置数据!"); + Assert.isFalse(StringUtil.isEmpty(configDto.getGroupId()), "考勤组ID不能为空!"); + Assert.isFalse(StringUtil.isEmpty(configDto.getDays()), "结束日期不能为空!"); + List userIds = configDto.getUserList().stream().map(LineDrawingUserDto::getUserId).distinct().collect(Collectors.toList()); + List days = Arrays.stream(StringUtil.split(configDto.getDays(), ",")).map(DateUtil::stringToDates).collect(Collectors.toList()); + return lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, configDto.getGroupId()).eq(FtbAttendanceDailyRule::getFixedMark, 2).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getDay, days).count() > 0; + } + + /** + * 构建划线排班规则 + * + * @param groupId 考勤组id + * @param day 日期 + * @param start 时段开始 + * @param end 时段结束 + * @param userId 用户ID + * @param lineSchedulingConfig 排班配置 + * @return 排班规则 + */ + @NotNull + public static FtbAttendanceDailyRule buildLineSchedulesRule(String groupId, Integer selfGroup, Date day, Date start, Date end, String userId, FtbAttendanceLineSchedulingConfig lineSchedulingConfig) { + FtbAttendanceDailyRule rule = new FtbAttendanceDailyRule(); + rule.setId(RandomUtil.uuId()); + rule.setGroupId(groupId); + rule.setSelfGroup(selfGroup); + rule.setPeriodId(rule.getId()); + rule.setShiftId(rule.getId()); + rule.setUserId(userId); + rule.setDay(day); + + // 11.3.6 设置考勤类型为普通出勤 + rule.setAttendanceType(AttendanceTypeEnum.ORDINARY.getCode()); + + // 11.3.7 设置排班类型 + rule.setSchedulesType(0); + + // 11.3.8 解析时段的上班和下班时间 + // 将BigDecimal小时数转换为分钟数进行日期偏移 + BigDecimal startCheckInLimit = lineSchedulingConfig.getStartCheckInLimit() != null ? lineSchedulingConfig.getStartCheckInLimit() : BigDecimal.ZERO; + BigDecimal endCheckInLimit = lineSchedulingConfig.getEndCheckInLimit() != null ? lineSchedulingConfig.getEndCheckInLimit() : BigDecimal.ZERO; + + // 计算分钟数:小时数 * 60,转换为int值 + int startCheckInMinutes = startCheckInLimit.multiply(BigDecimal.valueOf(60)).intValue(); + int endCheckInMinutes = endCheckInLimit.multiply(BigDecimal.valueOf(60)).intValue(); + + // 使用offsetMinute方法进行日期偏移 + Date clockStartPoint = cn.hutool.core.date.DateUtil.offsetMinute(start, -startCheckInMinutes); + Date outLackPoint = cn.hutool.core.date.DateUtil.offsetMinute(end, endCheckInMinutes); + + // 11.3.9 设置上班开始时间和下班开始时间 + rule.setInPoint(start); + rule.setInLackPoint(start); + rule.setLateEnable(0); + rule.setEarlyEnable(0); + rule.setOriginInPoint(start); + rule.setOutPoint(end); + rule.setOriginOutPoint(end); + rule.setClockStartPoint(clockStartPoint); + rule.setOutLackPoint(outLackPoint); + // 11.3.10 其他时间点设为空 + rule.setLatePoint(null); + rule.setEarlyPoint(null); + rule.setBreakStartPoint(null); + rule.setBreakEndPoint(null); + rule.setOriginBreakStartPoint(null); + rule.setOriginBreakEndPoint(null); + rule.setFixedMark(2); + // 11.3.11 设置其他必要属性 + rule.setApplyViewEnable(1); + rule.setDeleteMark(0); + rule.setTenantId(UserProvider.getUser().getTenantId()); + rule.setCreatorUserId(UserProvider.getLoginUserId()); + rule.setCreatorTime(new Date()); + rule.setLastModifyUserId(UserProvider.getLoginUserId()); + rule.setLastModifyTime(new Date()); + rule.setOriginValidDuration(rule.getValidDuration()); + return rule; + } + + /** + * 排班导出 + * + * @param groupId 考勤组id + * @param workGroupId 班组id + * @param month 月份 + * @return + */ + @Override + public void lineSchedulesExport(String groupId, String workGroupId, String month, List userIdList) { + // 划线排班班次查询 + Date date = DateUtil.stringToDates(month + "-01"); + List monthDayInfos = getMonthDayInfos(date); + List schedulesList = getLineSchedulesList(groupId, workGroupId, monthDayInfos.stream().map(DayInfo::getDay).collect(Collectors.toList()), userIdList); + AttendanceGroup byId = attendanceGroupService.getById(groupId); + List heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList()); + OrganizeGeneralDetailVO infoById = organizeApi.organizeInfoById(StringUtil.isNotEmpty(workGroupId) ? OrganizeCategoryEnums.TEAM : null, StringUtil.isNotEmpty(workGroupId) ? workGroupId : byId.getOrgId()).getData(); + String titleHead = DateUtil.dateToString(date, "yyyy年MM月【") + infoById.getName() + "】划线排班表"; + String shiftHead = "导入说明:\n" + "1、手机号码为必填;\n" + "2、按照日期填写班次时间,以24小时制,填写格式:在1号填写班次时间为09:00-12:00,此为一个班次,如果多个班次则以“|”隔开,如1号整天有2个班次,则填写为09:00-12:00|13:00-18:00,同理如果有三个班次则继续增加,最多运行增加24个班次,如果班次时间跨日,则为18:00-02:00,跨日的班次在导入成功后需在划线排班页面展示,班次刻度为半小时制,非(00和30)半小时的将不能导入;\n" + "3、导入的月份会根据导入时划线排班页面的月份进行判断,如果当前月份只有30天,则只能会读取30天的数据,如果有跨日的班次,则填写班次时间后会在下月生成一条跨月次日的班次;\n" + "4、未填写的日期则表示对应日期不排班;\n" + "5、导入的人员需复合划线排班范围的员工类型、用工性质、员工岗位、排班成员信息才能导入,如果不符合条件,则不能导入;\n" + "6、导入后的数据为预发布数据,需保持修改后生效。"; + heads.add(0, "手机号码"); + heads.add(0, "姓名"); + excelExportTemplate.exportModule(UserProvider.getLoginUserId(), titleHead, MonthStatisticsPageListExportVo.class, heads.stream().map(head -> CollUtil.newArrayList(titleHead, shiftHead, head)).collect(Collectors.toList()), null, null, 2000, HeadStyleHandler::new, (page, size) -> { + List> data = CollUtil.newArrayList(); + schedulesList.forEach(schedulesVo -> { + Map objectObjectHashMap = Maps.newHashMap(); + List periodList = schedulesVo.getLineDrawingPeriods(); + Map> val = periodList.stream().collect(Collectors.groupingBy(lineDrawingPeriodDto -> DateUtil.dateToString(lineDrawingPeriodDto.getDay(), "yyyy-MM-dd"))); + objectObjectHashMap.put(0, schedulesVo.getRealName()); + objectObjectHashMap.put(1, schedulesVo.getMobilePhone()); + for (int i = 0; i < monthDayInfos.size(); i++) { + DayInfo dayInfo = monthDayInfos.get(i); + List periods = val.get(dayInfo.getDay()); + String context = CollUtil.isEmpty(periods) ? "" : periods.stream().map(item -> item.getStart() + "-" + item.getEnd()).filter(StringUtil::isNotEmpty).reduce((r1, r2) -> r1 + "|" + r2).orElse(""); + objectObjectHashMap.put(i + 2, context); + } + data.add(objectObjectHashMap); + }); + return data; + }); + } + + @Override + @Transactional + public void lineSchedulesImport(SchedulesImportDto schedulesImportDto) throws IOException { + Date start = DateDetail.getMonthBeginDate(schedulesImportDto.getMonth()); + Date end = DateDetail.getMonthEndDate(schedulesImportDto.getMonth()); + List dates = DateDetail.getDatesByPeriod(start, end); + List monthDayInfos = getMonthDayInfos(start); + // 2. 获取分布式锁,防止并发操作 + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_SET_SCHEDULES, UserProvider.getUser().getTenantId(), schedulesImportDto.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组排班操作正在执行中,请稍后再试"); + try { + // 3. 尝试获取锁 + Assert.isFalse(!lock.tryLock(5, 100, TimeUnit.SECONDS), "当前考勤组排班操作正在执行中,请稍后再试"); + //获取当前考勤组成员 + List attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, List.of(), CollUtil.newArrayList(schedulesImportDto.getGroupId()), Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return; + } + List allUserIds = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulesConfigFilter(schedulesImportDto.getGroupId(), allUserIds); + List infoByIds = userApi.getStaffRosterListInfoByIds(allUserIds); + Map userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getMobilePhone, Function.identity(), (r1, r2) -> r1)); + List heads = monthDayInfos.stream().map(dayInfo -> dayInfo.getWeekStr() + (char) 10 + dayInfo.getDayNum()).collect(Collectors.toList()); + heads.add(0, "手机号码"); + heads.add(0, "姓名"); + List importData = CollUtil.newArrayList(); + List failList = CollUtil.newArrayList(); + Map selfGroupMap = attendanceGroupUserVos.stream().collect(Collectors.toMap(AttendanceGroupUser::getUserId, AttendanceGroupUser::getType, (r1, r2) -> r2)); + EasyExcel.read(new URL(schedulesImportDto.getFileUrl()).openStream(), new LineSchedulesDataListener(schedulesImportDto.getGroupId(), importData, failList, heads, start, userMap, lineSchedulingConfig, selfGroupMap)).registerConverter(new CustomStringStringConverter()).sheet().headRowNumber(3).doRead(); + String fail = failList.stream().sorted(Comparator.comparingInt(model -> model.getError().getIndex())).map(model -> String.format(model.getError().getMessage(), StringUtil.join(model.getUserNames().stream().distinct().collect(Collectors.toList()), "】、【"))).findFirst().orElse(null); + Assert.isFalse(StringUtil.isNotEmpty(fail), fail); + List userIds = importData.stream().map(FtbAttendanceDailyRule::getUserId).distinct().collect(Collectors.toList()); + dates.add(0, cn.hutool.core.date.DateUtil.offsetDay(start, -1)); + dates.add(cn.hutool.core.date.DateUtil.offsetDay(end, 1)); + List hisRulesAll = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, schedulesImportDto.getGroupId()).in(FtbAttendanceDailyRule::getUserId, userIds).in(FtbAttendanceDailyRule::getDay, dates).list(); + // 11.3.13 处理规则安排 + List resultList = CollUtil.newArrayList(); + List allRules = new CopyOnWriteArrayList<>(); + List periodConflictRules = CollUtil.newArrayList(); + List noticeRules = new CopyOnWriteArrayList<>(); + List allRuleIds = new CopyOnWriteArrayList<>(); + Map> ruleMap = hisRulesAll.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))); + //划线排班 班次保存 + Map> usersMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + importData.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)).forEach((userId, list) -> { + list.stream().collect(Collectors.groupingBy(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF))).forEach((key, rules) -> { + // 11.3.3 提取历史规则ID + List hisRules = ruleMap.getOrDefault(key, Lists.newArrayList()); + List ruleIds = hisRules.stream().map(FtbAttendanceDailyRule::getId).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + allRuleIds.addAll(ruleIds); + dailyRuleArrangementHandle(resultList, hisRules, rules); + FtbAttendanceDailyRule rule = rules.get(0); + List users = attendanceGroupUserService.getAttendanceGroupUsers(rule.getDay(), cn.hutool.core.date.DateUtil.endOfDay(rule.getDay()), usersMap.getOrDefault(rule.getUserId(), Lists.newArrayList())); + // 11.3.14 处理借调日常规则 + users.forEach(user -> secondmentDailyRulesHandle(resultList, user, hisRules)); + // 11.3.15 填充仅外出的规则 + fillInNailRuleForOnlyStepOut(hisRules); + // 11.3.16 添加到所有规则列表 + allRules.addAll(hisRules); + noticeRules.addAll(rules); + periodConflictRules.addAll(hisRules); + }); + List collect = list.stream().map(FtbAttendanceDailyRule::getDay).distinct().collect(Collectors.toList()); + dates.stream().filter(day -> collect.stream().noneMatch(date -> date.compareTo(day) == 0)).forEach(day -> { + periodConflictRules.addAll(ruleMap.getOrDefault(userId + DateDetail.getDate2Str(day, DateDetail.DF), Lists.newArrayList())); + }); + }); + + periodConflictRules.addAll(hisRulesAll.stream().filter(vo -> vo.getDay().before(start) || vo.getDay().after(end)).collect(Collectors.toList())); + //判断时段是否冲突 + periodConflictRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)).forEach((key, rules) -> { + Assert.isTrue(fixedPeriodConflict(rules), "班次时段存在冲突"); + }); + // 12. 保存或更新考勤规则 + saveOrUpdateBatch(allRules, allRuleIds); + if (Objects.equals(lineSchedulingConfig.getEmployeeNotify(), 1)) { + // 13. 发送系统通知 + attendanceRuleNotificationHandle.sendSystemNoticeForLine2(resultList, noticeRules, null); + } + // 14. 处理考勤规则通知 + attendanceRuleNotificationHandle.notificationHandle(resultList, false); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + // 16. 释放分布式锁 + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + /** + * 获取排班数据 + * + * @param groupId 考勤组id + * @param workGroupId 班组id + * @param dayList + * @return + */ + @Override + public List getLineSchedulesList(String groupId, String workGroupId, List dayList, List finalUserIdList) { + //班组查询 + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + List userBoundVO = null; + if (Objects.nonNull(targetAuthIdsVO) && Objects.equals(targetAuthIdsVO.getTargetAuthEnums(), TargetAuthEnums.USER) || StringUtil.isNotEmpty(workGroupId)) { + userBoundVO = attendanceGroupUserService.getUserBoundVO(groupId, workGroupId); + if (CollUtil.isEmpty(userBoundVO)) { + return List.of(); + } + } + List userIdList; + if (CollUtil.isEmpty(userBoundVO)) { + userIdList = finalUserIdList; + } else { + userIdList = userBoundVO.stream().map(UserBoundVO::getId).filter(vo -> CollUtil.isEmpty(finalUserIdList) || finalUserIdList.contains(vo)).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIdList)) { + return List.of(); + } + } + List days = dayList.stream().map(DateUtil::stringToDates).collect(Collectors.toList()); + Date start = days.stream().min(Date::compareTo).orElse(null); + Date end = cn.hutool.core.date.DateUtil.endOfDay(days.stream().max(Date::compareTo).orElse(null)); + //获取当前考勤组成员 + List attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(start, end, Objects.isNull(userIdList) ? null : userIdList.stream().distinct().collect(Collectors.toList()), CollUtil.newArrayList(groupId), Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return CollUtil.newArrayList(); + } + Map> groupUserMap = attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + //获取用户名 + List collect2 = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulesConfigFilter(groupId, collect2); + Map stringBooleanMap = attendanceDayStatisticsService.selectUserIsSeal(collect2, DateUtil.getDateString(start, "yyyy-MM")); + List infoByIds = CollUtil.isNotEmpty(collect2) ? userApi.getStaffRosterListInfoByIds(collect2) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(infoByIds)) { + return CollUtil.newArrayList(); + } + List ftbAttendanceLineSchedulingPayrollHours = lineSchedulingPayrollHoursService.listByUserIdsAndDays(collect2, groupId, days); + Map userPayrollHoursMap = ftbAttendanceLineSchedulingPayrollHours.stream().collect(Collectors.toMap(rule -> rule.getUserId() + DateDetail.getDate2Str(rule.getDay(), DateDetail.DF), FtbAttendanceLineSchedulingPayrollHours::getPayrollHours)); + List list = lambdaQuery().eq(FtbAttendanceDailyRule::getGroupId, groupId).eq(FtbAttendanceDailyRule::getFixedMark, 2).in(CollUtil.isNotEmpty(userIdList), FtbAttendanceDailyRule::getUserId, userIdList).in(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.LEAVE.getCode(), AttendanceTypeEnum.ORDINARY.getCode()).notIn(FtbAttendanceDailyRule::getApplyViewEnable, 0, 2).in(FtbAttendanceDailyRule::getDay, days).orderByAsc(FtbAttendanceDailyRule::getDay, FtbAttendanceDailyRule::getInPoint).list(); + Map> collect = list.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getUserId)); + Map userMap = infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + //查询打卡记录获取打卡时间 + List schedulesVos = CollUtil.newArrayList(); + List attendanceTypeOfApply = CollUtil.newArrayList(); + AtomicReference isValidClock = new AtomicReference<>(Boolean.FALSE); + + groupUserMap.forEach((ruleUserId, groupUserVos) -> { + ArrayList val = Lists.newArrayList(); + isValidClock.set(false); + AttendanceGroupUser attendanceGroupUserVo = groupUserVos.stream().max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).orElse(null); + List rules = collect.getOrDefault(ruleUserId, CollUtil.newArrayList()); + Map> collect1 = rules.stream() + //过滤原离组考勤组当日数据 + .filter(rule -> !Objects.equals(rule.getApplyViewEnable(), 3) || StringUtil.equals(rule.getGroupId(), groupId)).collect(Collectors.groupingBy(FtbAttendanceDailyRule::getDay)); + + collect1.forEach((day, dayRules) -> { + Date dayStart = DateDetail.getDateByPoint(day, lineSchedulingConfig.getStartTime(), lineSchedulingConfig.getStartType()); + Date dayEnd = DateUtil.dateAddHours(DateDetail.getDateByPoint(day, lineSchedulingConfig.getEndTime(), lineSchedulingConfig.getEndType()), 1); + attendanceTypeOfApply.clear(); + Integer existStatus = isExistStatus(groupUserVos, day); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3) || Objects.equals(existStatus, 2)) { + attendanceTypeOfApply.add(SchedulesTypeEnum.SECONDMENT_TO.getCode()); + } + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 2) || Objects.equals(existStatus, 0)) { + return; + } + Date date = DateUtil.dateAddDays(day, 1); + List schedulesItemVos = DailyRuleUtil.mergeRules(dayRules).stream().map(dayRule -> { + if (Objects.equals(attendanceGroupUserVo.getType(), 2) && StringUtil.equals(attendanceGroupUserVo.getGroupId(), groupId) && !StringUtil.equals(dayRule.getGroupId(), groupId) && Objects.equals(dayRule.getSelfGroup(), 2)) { + return null; + } + if (dayRule.getInPoint().after(dayEnd) || dayRule.getOutPoint().before(dayStart) || Objects.equals(dayRule.getApplyViewEnable(), 2)) { + return null; + } + return LineDrawingPeriodDto.builder().id(dayRule.getId()).day(day).startType(DateUtil.max(dayRule.getInPoint(), dayStart).before(date) ? 1 : 2).start(DateUtil.dateToString(DateUtil.max(dayRule.getInPoint(), dayStart), "HH:mm")).endType(DateUtil.min(dayRule.getOutPoint(), dayEnd).before(date) ? 1 : 2).end(DateUtil.dateToString(DateUtil.min(dayRule.getOutPoint(), dayEnd), "HH:mm")).duration(BigDecimal.valueOf(cn.hutool.core.date.DateUtil.between(dayRule.getInPoint(), dayRule.getOutPoint(), DateUnit.MINUTE)).divide(BigDecimal.valueOf(60), 1, RoundingMode.HALF_UP)).build(); + }).filter(Objects::nonNull) + //inpoint为空的数据按照schedulesType排序 + .sorted(Comparator.comparing(LineDrawingPeriodDto::getStart)).collect(Collectors.toList()); + + val.addAll(schedulesItemVos); + }); + AttendanceGroupUser attendanceGroupUser = groupUserVos.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + if (Objects.isNull(attendanceGroupUser)) { + return; + } + PartUserInfoVo partUserInfoVo = userMap.get(ruleUserId); + if (Objects.isNull(partUserInfoVo)) { + return; + } + int existStatus1 = SecondmentTypeUtil.toSecondmentType(isExistStatus(groupUserVos, start, end)); + BigDecimal total = val.stream().map(LineDrawingPeriodDto::getDuration).reduce(BigDecimal.ZERO, BigDecimal::add); + LineSchedulesVo build = LineSchedulesVo.builder().userId(ruleUserId).IsSeal(stringBooleanMap.getOrDefault(ruleUserId, Boolean.FALSE)).sort(attendanceGroupUser.getSort()).mobilePhone(partUserInfoVo.getMobilePhone()).payrollHours(userPayrollHoursMap.getOrDefault(ruleUserId + DateDetail.getDate2Str(start, DateDetail.DF), null)).realName(partUserInfoVo.getRealName()).delMark(attendanceGroupUserVo.getDeleteMark()).total(total).selfGroup(existStatus1).existPeriods(getTimeSlotVo(lineSchedulingConfig, groupUserVos, start)).build(); + if (build != null && Objects.equals(build.getDelMark(), 1) && !isValidClock.get()) { + return; + } + build.setLineDrawingPeriods(val); + schedulesVos.add(build); + }); + return schedulesVos.stream().sorted(Comparator.comparing(vo -> Objects.equals(vo.getDelMark(), 1) ? 20000 : Objects.equals(vo.getSelfGroup(), 1) ? 19998 : Objects.equals(vo.getSelfGroup(), 2) ? 19999 : Objects.isNull(vo.getSort()) ? 0 : vo.getSort())).collect(Collectors.toList()); + } + + + /** + * 获取单个用户单天的存在时间段集合 + * @param lineSchedulingConfig + * @param groupUserVos + * @param day + * @return + */ + private List getTimeSlotVo(FtbAttendanceLineSchedulingConfig lineSchedulingConfig, List groupUserVos, Date day) { + if (Objects.isNull(lineSchedulingConfig)) { + return List.of(); + } + Date start = DailyRuleUtil.ceilToHalfHour(DateDetail.getDateByPoint(day, lineSchedulingConfig.getStartTime(), lineSchedulingConfig.getStartType())); + Date end = DailyRuleUtil.ceilToHalfHour(DateUtil.dateAddHours(DateDetail.getDateByPoint(day, lineSchedulingConfig.getEndTime(), lineSchedulingConfig.getEndType()), 1)); + Date date = DateUtil.dateAddDays(day, 1); + //借调组取借调时间,需要与开始时间与结束时间比较 + //借调组timeJson转借调数据 + //本组取创建时间及离组时间 + return groupUserVos.stream().flatMap(user -> { + List secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? List.of() : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class); + List secondmentDateVoList = getSecondmentTypeList(secondmentDateVos, start, end); + if (!Objects.equals(user.getType(), 0) && CollUtil.isNotEmpty(secondmentDateVoList)) { + //借调组取借调时间,需要与开始时间与结束时间比较 + if (Objects.equals(user.getType(), 2)) { + //借调组timeJson转借调数据 + return secondmentDateVos.stream().filter(secondmentDateVo -> start.before(secondmentDateVo.getEndTime()) && end.after(secondmentDateVo.getStartTime())).map(secondmentDateVo -> { + Date secondmentStart = DateUtil.max(DailyRuleUtil.ceilToHalfHour(secondmentDateVo.getStartTime()), start); + Date secondmentEnd = DateUtil.min(DailyRuleUtil.ceilToHalfHour(secondmentDateVo.getEndTime()), end); + return LineDrawingPeriodDto.builder().day(secondmentStart).startType(secondmentStart.before(date) ? 1 : 2).start(DateUtil.dateToString(secondmentStart, "HH:mm")).endType(secondmentEnd.before(date) ? 1 : 2).end(DateUtil.dateToString(secondmentEnd, "HH:mm")).build(); + }); + } + return screenshotTimePeriod(secondmentDateVoList, start, end, date, day); + } + Date creatorTime = DateUtil.max(DailyRuleUtil.ceilToHalfHour(user.getCreatorTime()), start); + Date removeTime = DateUtil.min(DailyRuleUtil.ceilToHalfHour(user.getRemoveTime()), end); + //本组取创建时间及离组时间 + return Stream.of(LineDrawingPeriodDto.builder().day(day).startType(creatorTime.before(date) ? 1 : 2).start(DateUtil.dateToString(creatorTime, "HH:mm")).endType(removeTime.before(date) ? 1 : 2).end(DateUtil.dateToString(removeTime, "HH:mm")).build()); + }).filter(Objects::nonNull).sorted(Comparator.comparing(LineDrawingPeriodDto::getStartType).thenComparing(LineDrawingPeriodDto::getStart)).collect(Collectors.toList()); + } + + private Stream screenshotTimePeriod(List secondmentDateVoList, Date start, Date end, Date date, Date day) { + ArrayList secondmentDateList = Lists.newArrayList(); + SecondmentDateVo previousSecondmentDateVo = null; + for (SecondmentDateVo secondmentDateVo : secondmentDateVoList) { + if (Objects.nonNull(secondmentDateVo)) { + //如果开始时间大于借调开始时间,且结束时间小于借调结束时间,则返回null + if (start.after(secondmentDateVo.getStartTime()) && end.before(secondmentDateVo.getEndTime())) { + break; + } + //如果开始时间小于借调开始时间,且结束时间小于借调结束时间,则返回开始时间至借调开始时间 + if (start.before(secondmentDateVo.getStartTime()) && end.before(secondmentDateVo.getEndTime())) { + secondmentDateVo.setEndTime(secondmentDateVo.getStartTime()); + secondmentDateVo.setStartTime(start); + secondmentDateList.add(secondmentDateVo); + break; + } + boolean hasPrevious = Objects.nonNull(previousSecondmentDateVo); + if (hasPrevious) { + start = previousSecondmentDateVo.getStartTime(); + end = previousSecondmentDateVo.getEndTime(); + } + //如果开始时间小于借调开始时间,且结束时间大于借调结束时间,则需要拆分为两段,开始时间至借调开始时间,结束时间至借调结束时间 + if (start.before(secondmentDateVo.getStartTime()) && end.after(secondmentDateVo.getEndTime())) { + SecondmentDateVo secondmentDateVo2 = BeanUtil.toBean(secondmentDateVo, SecondmentDateVo.class); + if (hasPrevious) { + previousSecondmentDateVo.setEndTime(secondmentDateVo.getStartTime()); + } else { + secondmentDateVo.setEndTime(secondmentDateVo.getStartTime()); + secondmentDateVo.setStartTime(start); + secondmentDateList.add(secondmentDateVo); + } + secondmentDateVo2.setStartTime(secondmentDateVo2.getEndTime()); + secondmentDateVo2.setEndTime(end); + secondmentDateList.add(secondmentDateVo2); + previousSecondmentDateVo = secondmentDateVo2; + continue; + } + + //如果开始时间大于借调开始时间,且结束时间大于借调结束时间,则返回借调结束时间至结束时间 + if (start.after(secondmentDateVo.getStartTime()) && end.after(secondmentDateVo.getEndTime())) { + if (hasPrevious) { + previousSecondmentDateVo.setStartTime(secondmentDateVo.getEndTime()); + continue; + } + secondmentDateVo.setStartTime(secondmentDateVo.getEndTime()); + secondmentDateVo.setEndTime(end); + secondmentDateList.add(secondmentDateVo); + previousSecondmentDateVo = secondmentDateVo; + + } + } + } + return secondmentDateList.stream().map(secondmentDateVo -> LineDrawingPeriodDto.builder().day(day).startType(secondmentDateVo.getStartTime().before(date) ? 1 : 2).start(DateUtil.dateToString(secondmentDateVo.getStartTime(), "HH:mm")).endType(secondmentDateVo.getEndTime().before(date) ? 1 : 2).end(DateUtil.dateToString(secondmentDateVo.getEndTime(), "HH:mm")).build()); + } + + /** + * 查询单个用户是否划线排班 + * @param userId + * @param start + * @param end + * @return + */ + @Override + public boolean isLineScheduleByUserId(String userId, Date start, Date end) { + List userIds = CollUtil.newArrayList(userId); + List attendanceGroupUsers = attendanceGroupUserService.queryByUsersAndGroupFilterSecondment(start, end, userIds, null); + AttendanceGroupUser attendanceGroupUser = attendanceGroupUsers.stream().findFirst().orElse(null); + Assert.notNull(attendanceGroupUser, "该用户暂无考勤组"); + lineSchedulesConfigFilter(attendanceGroupUser.getGroupId(), userIds); + return CollUtil.isNotEmpty(userIds); + } + + @Override + public FtbAttendanceLineSchedulingConfig lineSchedulesConfigFilter(String groupId, List userIds) { + //划线排班配置过滤 + if (CollUtil.isNotEmpty(userIds)) { + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(groupId); + if (Objects.isNull(lineSchedulingConfig)) { + userIds.clear(); + return null; + } + return getFtbAttendanceLineSchedulingConfig(userIds, lineSchedulingConfig); + } + return null; + } + + @Nullable + @Override + public FtbAttendanceLineSchedulingConfig getFtbAttendanceLineSchedulingConfig(List userIds, FtbAttendanceLineSchedulingConfig lineSchedulingConfig) { + List list = userApi.getStaffRosterListInfoByIds(userIds); + filterUserIdsByLineSchedulingConfig(userIds, lineSchedulingConfig, list); + return lineSchedulingConfig; + } + + /** + * 按划线排班配置过滤 userIds(就地修改),与 {@link #getFtbAttendanceLineSchedulingConfig} 规则一致。 + */ + private void filterUserIdsByLineSchedulingConfig(List userIds, FtbAttendanceLineSchedulingConfig lineSchedulingConfig, List staffRosterList) { + List workNatureArr = StringUtil.isEmpty(lineSchedulingConfig.getWorkNature()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getWorkNature(), ",")); + List employeeTypeArr = StringUtil.isEmpty(lineSchedulingConfig.getEmployeeType()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getEmployeeType(), ",")); + List positionArr = StringUtil.isEmpty(lineSchedulingConfig.getPosition()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getPosition(), ",")); + List memberArr = StringUtil.isEmpty(lineSchedulingConfig.getMembers()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getMembers(), ",")); + if (CollUtil.isEmpty(workNatureArr) && CollUtil.isEmpty(employeeTypeArr) && CollUtil.isEmpty(positionArr) && !Objects.equals(lineSchedulingConfig.getMembersType(), 0) && CollUtil.isEmpty(memberArr)) { + userIds.clear(); + return; + } + boolean hasOnboarding = workNatureArr.contains("301"); + List userIdsForConfigFilter = staffRosterList.stream().filter(dto -> (CollUtil.isNotEmpty(workNatureArr) && workNatureArr.contains(dto.getWorkerStatus())) && ((hasOnboarding && StringUtil.isEmpty(dto.getWorkerType())) || employeeTypeArr.contains("-1") || CollUtil.isNotEmpty(employeeTypeArr) && employeeTypeArr.contains(dto.getWorkerType())) || (CollUtil.isNotEmpty(positionArr) && positionArr.contains(dto.getPositionId())) || Objects.equals(lineSchedulingConfig.getMembersType(), 0) || (CollUtil.isNotEmpty(memberArr) && memberArr.contains(dto.getUserId()))).map(PartUserInfoVo::getUserId).collect(Collectors.toList()); + userIds.removeIf(userId -> !userIdsForConfigFilter.contains(userId)); + } + + @Override + public List getDayRuleByMonth(LocalDate finalStartDate, LocalDate finalEndDate) { + return this.baseMapper.getDayRuleByMonth(finalStartDate, finalEndDate); + } + + /** + * 判断排班设置中是否存在ID为空的排班项 + * + * @param schedulesSetDto 排班设置DTO + * @return 如果存在ID为空的排班项则返回true,否则返回false + */ + private boolean hasBlankIdItem(SchedulesSetDto schedulesSetDto) { + if (schedulesSetDto == null || schedulesSetDto.getVal() == null) { + return false; + } + return schedulesSetDto.getVal().values().stream().filter(dayVo -> dayVo != null && dayVo.getItemVos() != null).anyMatch(dayVo -> dayVo.getItemVos().stream().anyMatch(item -> StringUtil.isBlank(item.getId()))); + } + + @Override + public UserDayShiftInfoVo getUserDayShiftInfo(String userId, Date queryDate) { + UserDayShiftInfoVo result = UserDayShiftInfoVo.builder().userId(userId).queryDate(DateUtil.dateToString(queryDate, "yyyy-MM-dd")).shiftPeriods(new ArrayList<>()).build(); + + try { + // 获取用户信息 + PartUserInfoVo userInfo = userApi.getStaffRosterListInfoByIds(Collections.singletonList(userId)).stream().findFirst().orElse(null); + if (userInfo == null) { + return result; + } + result.setUserName(userInfo.getAccount()); + + // 获取当天排班规则 + List dailyRules = getAttendanceDayRulesForStatistic(null, userId, queryDate); + if (CollUtil.isEmpty(dailyRules)) { + return result; + } + + // 构建考勤组名称映射 + Set groupIds = dailyRules.stream().map(FtbAttendanceDailyRule::getGroupId).filter(Objects::nonNull).collect(Collectors.toSet()); + + Map groupNameMap = CollUtil.isEmpty(groupIds) ? Collections.emptyMap() : attendanceGroupService.listByIds(new ArrayList<>(groupIds)).stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getGroupName, (v1, v2) -> v1)); + + // 设置主考勤组信息 + groupIds.stream().findFirst().ifPresent(firstGroupId -> { + result.setGroupId(firstGroupId); + result.setGroupName(groupNameMap.get(firstGroupId)); + }); + + // 分离普班和请假规则 + List ordinaryRules = dailyRules.stream().filter(rule -> rule.getAttendanceType() != null).filter(rule -> rule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList()); + + List leaveRules = dailyRules.stream().filter(rule -> rule.getAttendanceType() != null).filter(rule -> rule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList()); + + // 合并普班和请假信息 + result.setShiftPeriods(mergeShiftAndLeave(ordinaryRules, leaveRules, groupNameMap)); + + } catch (Exception e) { + log.error("查询用户指定日期班次信息异常,userId=>{}", userId, e); + } + + return result; + } + + /** + * 合并普班和请假信息,生成时段列表 + */ + private List mergeShiftAndLeave(List ordinaryRules, List leaveRules, Map groupNameMap) { + + if (CollUtil.isEmpty(ordinaryRules)) { + return leaveRules.stream().map(leave -> buildLeavePeriod(leave, 0, null, groupNameMap)).collect(Collectors.toList()); + } + + List allPeriods = ordinaryRules.stream().flatMap(ordinary -> mergeSingleShift(ordinary, leaveRules, groupNameMap).stream()).collect(Collectors.toList()); + + return allPeriods.stream().sorted(Comparator.comparing(UserDayShiftInfoVo.ShiftPeriodVo::getStartTime)).collect(Collectors.toList()); + } + + /** + * 合并单个普班与请假信息 + */ + private List mergeSingleShift(FtbAttendanceDailyRule ordinaryRule, List leaveRules, Map groupNameMap) { + + Date ordinaryStart = ordinaryRule.getOriginInPoint(); + Date ordinaryEnd = ordinaryRule.getOriginOutPoint(); + + if (ordinaryStart == null || ordinaryEnd == null) { + return Collections.emptyList(); + } + + // 查找覆盖的请假 + List coveringLeaves = leaveRules.stream().filter(l -> l.getApplyStart() != null && l.getApplyEnd() != null).filter(l -> !l.getApplyEnd().before(ordinaryStart) && !l.getApplyStart().after(ordinaryEnd)).collect(Collectors.toList()); + + if (CollUtil.isEmpty(coveringLeaves)) { + Date startTime = ordinaryRule.getInPoint(); + Date endTime = ordinaryRule.getOutPoint(); + if (startTime != null && endTime != null) { + return Collections.singletonList(buildPeriodWithTime(ordinaryRule, 0, null, startTime, endTime, groupNameMap)); + } + return Collections.emptyList(); + } + + // 计算未覆盖时段 + 请假时段 + List periods = calculateUncoveredPeriods(ordinaryRule, ordinaryStart, ordinaryEnd, coveringLeaves, groupNameMap); + + coveringLeaves.stream().map(leave -> buildLeavePeriod(leave, 0, null, groupNameMap)).forEach(periods::add); + + return periods; + } + + /** + * 计算未被请假覆盖的时间段,直接生成时段VO + */ + private List calculateUncoveredPeriods(FtbAttendanceDailyRule ordinaryRule, Date ordinaryStart, Date ordinaryEnd, List coveringLeaves, Map groupNameMap) { + + List periods = new ArrayList<>(); + + if (CollUtil.isEmpty(coveringLeaves)) { + Date startTime = ordinaryRule.getInPoint(); + Date endTime = ordinaryRule.getOutPoint(); + if (startTime != null && endTime != null) { + periods.add(buildPeriodWithTime(ordinaryRule, 0, null, startTime, endTime, groupNameMap)); + } + return periods; + } + + // 按请假开始时间排序 + coveringLeaves.sort(Comparator.comparing(FtbAttendanceDailyRule::getApplyStart)); + + Date currentStart = ordinaryStart; + String relatedLeaveId = null; + + for (FtbAttendanceDailyRule leaveRule : coveringLeaves) { + Date leaveStart = leaveRule.getInPoint(); + Date leaveEnd = leaveRule.getOutPoint(); + + // 如果请假开始时间晚于当前开始时间,说明有未覆盖的时间段 + if (leaveStart.after(currentStart)) { + Date uncoveredEnd = leaveStart.before(ordinaryEnd) ? leaveStart : ordinaryEnd; + if (uncoveredEnd.after(currentStart)) { + periods.add(buildPeriodWithTime(ordinaryRule, 1, relatedLeaveId, currentStart, uncoveredEnd, groupNameMap)); + } + } + + // 更新当前开始时间为请假结束时间 + if (leaveEnd.after(currentStart)) { + currentStart = leaveEnd; + relatedLeaveId = leaveRule.getId(); + } + + // 如果已经超出普班结束时间,退出 + if (currentStart.after(ordinaryEnd)) { + break; + } + } + + // 添加最后一段未覆盖的时间 + if (currentStart.before(ordinaryEnd)) { + periods.add(buildPeriodWithTime(ordinaryRule, 1, relatedLeaveId, currentStart, ordinaryEnd, groupNameMap)); + } + + // 如果没有任何未覆盖的时间段,说明完全被覆盖 + if (periods.isEmpty()) { + periods.add(buildPeriodWithTime(ordinaryRule, 2, coveringLeaves.get(0).getId(), ordinaryStart, ordinaryEnd, groupNameMap)); + } + + return periods; + } + + /** + * 构建带指定时间的时段VO + */ + private UserDayShiftInfoVo.ShiftPeriodVo buildPeriodWithTime(FtbAttendanceDailyRule rule, Integer coverageStatus, String relatedLeaveId, Date startTime, Date endTime, Map groupNameMap) { + + int durationMinutes = startTime != null && endTime != null ? (int) ((endTime.getTime() - startTime.getTime()) / 60000) : 0; + + String groupId = rule.getGroupId(); + String groupName = groupNameMap != null && groupId != null ? groupNameMap.get(groupId) : null; + + return UserDayShiftInfoVo.ShiftPeriodVo.builder().periodId(rule.getId()).groupId(groupId).groupName(groupName).periodType(AttendanceTypeEnum.ORDINARY.getCode()).periodTypeName(AttendanceTypeEnum.ORDINARY.getMsg()).shiftName(rule.getShiftId()).startTime(startTime != null ? DateUtil.dateToString(startTime, "HH:mm") : null).endTime(endTime != null ? DateUtil.dateToString(endTime, "HH:mm") : null).latePoint(rule.getLatePoint() != null ? DateUtil.dateToString(rule.getLatePoint(), "HH:mm") : null).durationMinutes(durationMinutes).isCrossDay(rule.getNextDayEnable() != null ? rule.getNextDayEnable() : 0).coverageStatus(coverageStatus).relatedLeaveId(relatedLeaveId).build(); + } + + /** + * 构建请假时段VO + */ + private UserDayShiftInfoVo.ShiftPeriodVo buildLeavePeriod(FtbAttendanceDailyRule rule, Integer coverageStatus, String relatedLeaveId, Map groupNameMap) { + + Date startTime = rule.getApplyStart(); + Date endTime = rule.getApplyEnd(); + Double duration = rule.getLeaveDay() != null ? rule.getLeaveDay().doubleValue() : null; + int durationMinutes = duration != null ? (int) (duration * 60) : 0; + + String groupId = rule.getGroupId(); + String groupName = groupNameMap != null && groupId != null ? groupNameMap.get(groupId) : null; + + return UserDayShiftInfoVo.ShiftPeriodVo.builder().periodId(rule.getId()).groupId(groupId).groupName(groupName).periodType(rule.getAttendanceType()).periodTypeName(AttendanceTypeEnum.getMsg(rule.getAttendanceType())).leaveTypeName(AttendanceTypeEnum.getMsg(rule.getAttendanceType())).startTime(startTime != null ? DateUtil.dateToString(startTime, "yyyy-MM-dd HH:mm") : null).endTime(endTime != null ? DateUtil.dateToString(endTime, "yyyy-MM-dd HH:mm") : null).durationMinutes(durationMinutes).isCrossDay(0).coverageStatus(coverageStatus).relatedLeaveId(relatedLeaveId).build(); + } + + /** + * 统一排班内部实现 + */ + private String setUnifiedSchedulesInternal(UnifiedSchedulesDto dto) throws HandleException { + // 按排班类型分组 + Map> fixedUserDayMap = new HashMap<>(); + Map> lineUserDayMap = new HashMap<>(); + + for (UserDaySchedulesDto item : dto.getUserDaySchedules()) { + if (CollUtil.isNotEmpty(item.getFixedItems())) { + // 固定排班:按用户+日期组织 + fixedUserDayMap.computeIfAbsent(item.getUserId(), k -> new TreeMap<>()) + .put(item.getDay(), SchedulesDayVo.builder().itemVos(item.getFixedItems()).build()); + } + if (CollUtil.isNotEmpty(item.getLinePeriods())) { + // 划线排班:按用户分组 + lineUserDayMap.computeIfAbsent(item.getUserId(), k -> new ArrayList<>()).add(item); + } + } + + List errors = new ArrayList<>(); + + // 处理固定排班 + if (!fixedUserDayMap.isEmpty()) { + List fixedList = new ArrayList<>(); + for (Map.Entry> entry : fixedUserDayMap.entrySet()) { + SchedulesSetDto dto1 = new SchedulesSetDto(); + dto1.setGroupId(dto.getGroupId()); + dto1.setUserId(entry.getKey()); + dto1.setVal(entry.getValue()); + fixedList.add(dto1); + } + String fixedResult = setSchedules(fixedList); + if (StringUtil.isNotEmpty(fixedResult)) { + errors.add(fixedResult); + } + } + + // 处理划线排班 + if (!lineUserDayMap.isEmpty()) { + // 按用户构建划线排班数据 + List lineUserList = new ArrayList<>(); + + for (Map.Entry> entry : lineUserDayMap.entrySet()) { + String userId = entry.getKey(); + List userItems = entry.getValue(); + + LineDrawingUserDto lineUserDto = new LineDrawingUserDto(); + lineUserDto.setUserId(userId); + + List allPeriods = new ArrayList<>(); + BigDecimal lastPayrollHours = null; + + for (UserDaySchedulesDto item : userItems) { + + if (CollUtil.isNotEmpty(item.getLinePeriods())) { + for (LineDrawingPeriodDto period : item.getLinePeriods()) { + LineDrawingPeriodDto newPeriod = new LineDrawingPeriodDto(); + newPeriod.setId(period.getId()); + newPeriod.setDay(getStr2Date(item.getDay())); + newPeriod.setStart(period.getStart()); + newPeriod.setStartType(period.getStartType()); + newPeriod.setEnd(period.getEnd()); + newPeriod.setEndType(period.getEndType()); + newPeriod.setDuration(period.getDuration()); + allPeriods.add(newPeriod); + } + } + if (Objects.nonNull(item.getPayrollHours())) { + lastPayrollHours = item.getPayrollHours(); + } + } + + lineUserDto.setPeriods(allPeriods); + lineUserDto.setPayrollHours(lastPayrollHours); + lineUserList.add(lineUserDto); + } + + // 一次性调用划线排班(使用支持多时段的新方法) + LineDrawingSchedulesConfigDto lineDto = new LineDrawingSchedulesConfigDto(); + lineDto.setGroupId(dto.getGroupId()); + lineDto.setUserList(lineUserList); + + String lineResult = setLineDrawingSchedulesWithMultiPeriods(lineDto); + if (StringUtil.isNotEmpty(lineResult)) { + errors.add(lineResult); + } + } + + return errors.isEmpty() ? "" : String.join("\br", errors); + } + + private static final String LINE_SCHEDULE_SHIFT_NAME = "划线排班"; + private static final int SHIFT_HISTORY_DAY_COUNT = 90; + + @Override + public List getGroupShiftHistory90Days(String groupId) { + Date end = cn.hutool.core.date.DateUtil.endOfDay(cn.hutool.core.date.DateUtil.offsetDay(new Date(), -1)); + Date start = cn.hutool.core.date.DateUtil.beginOfDay( + cn.hutool.core.date.DateUtil.offsetDay(new Date(), -(SHIFT_HISTORY_DAY_COUNT))); + List datesByPeriod = DateDetail.getDatesByPeriod(start, end); + Map revenueByDay = loadReceivableRevenueAmountByDay(groupId); + + List attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment( + start, end, null, CollUtil.newArrayList(groupId), Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return buildEmptyDayShiftHistory(datesByPeriod, revenueByDay); + } + + Map> groupUserMap = attendanceGroupUserVos.stream() + .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + List userIds = new ArrayList<>(groupUserMap.keySet()); + + List infoByIds = userApi.getStaffRosterListInfoByIds(userIds); + Map userMap = CollUtil.isEmpty(infoByIds) ? Maps.newHashMap() + : infoByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity(), (a, b) -> a)); + List list = lambdaQuery() + .eq(FtbAttendanceDailyRule::getGroupId, groupId) + .in(FtbAttendanceDailyRule::getUserId, userIds) + .eq(FtbAttendanceDailyRule::getAttendanceType, AttendanceTypeEnum.ORDINARY.getCode()) + .notIn(FtbAttendanceDailyRule::getApplyViewEnable, 0, 2) + .ge(FtbAttendanceDailyRule::getDay, start) + .le(FtbAttendanceDailyRule::getDay, end) + .list(); + list = list.stream() + .filter(rule -> !Objects.equals(rule.getApplyViewEnable(), 3) || StringUtil.equals(rule.getGroupId(), groupId)) + .collect(Collectors.toList()); + + Map>> rulesByUserAndDay = indexRulesByUserAndDay(list); + Map shiftMap = getShiftByShiftIds( + list.stream().map(FtbAttendanceDailyRule::getShiftId).filter(StringUtil::isNotEmpty).distinct() + .collect(Collectors.toList())); + Map> inGroupDaysByUserId = buildInGroupDaysByUserId(groupUserMap, datesByPeriod); + Map latestGroupUserByUserId = buildLatestGroupUserByUserId(groupUserMap); + + List result = CollUtil.newArrayList(); + for (Date day : datesByPeriod) { + String dayStr = DateDetail.getDate2Str(day, DateDetail.DF); + List shifts = buildDayShiftPostStats(dayStr, groupUserMap, inGroupDaysByUserId, + rulesByUserAndDay, userMap, shiftMap, latestGroupUserByUserId, groupId); + result.add(DayShiftRevenueStatVo.builder() + .day(dayStr) + .revenue(revenueByDay.get(dayStr)) + .shifts(shifts) + .build()); + } + return result; + } + + /** + * 预计算 90 天内在组自然日(与 {@link #buildDayShiftPostStats} 原 {@code isExistStatus} 过滤口径一致)。 + */ + private Map> buildInGroupDaysByUserId(Map> groupUserMap, + List datesByPeriod) { + Map> result = new HashMap<>(groupUserMap.size()); + groupUserMap.forEach((userId, groupUserVos) -> { + Set inGroupDays = new HashSet<>(); + for (Date day : datesByPeriod) { + Integer existStatus = isExistStatus(groupUserVos, day); + if (Objects.equals(existStatus, -1) || Objects.equals(existStatus, 2) || Objects.equals(existStatus, 0)) { + continue; + } + inGroupDays.add(DateDetail.getDate2Str(day, DateDetail.DF)); + } + result.put(userId, inGroupDays); + }); + return result; + } + + private Map buildLatestGroupUserByUserId( + Map> groupUserMap) { + Map result = new HashMap<>(groupUserMap.size()); + groupUserMap.forEach((userId, groupUserVos) -> groupUserVos.stream() + .max(Comparator.comparing(AttendanceGroupUser::getCreatorTime)) + .ifPresent(u -> result.put(userId, u))); + return result; + } + + private Map>> indexRulesByUserAndDay(List list) { + Map>> rulesByUserAndDay = new HashMap<>(); + for (FtbAttendanceDailyRule rule : list) { + if (rule == null || rule.getDay() == null || StringUtil.isEmpty(rule.getUserId())) { + continue; + } + String dayStr = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF); + rulesByUserAndDay + .computeIfAbsent(rule.getUserId(), k -> new HashMap<>()) + .computeIfAbsent(dayStr, k -> new ArrayList<>()) + .add(rule); + } + return rulesByUserAndDay; + } + + private List buildEmptyDayShiftHistory(List datesByPeriod, + Map revenueByDay) { + return datesByPeriod.stream() + .map(day -> { + String dayStr = DateDetail.getDate2Str(day, DateDetail.DF); + return DayShiftRevenueStatVo.builder() + .day(dayStr) + .revenue(revenueByDay.get(dayStr)) + .shifts(CollUtil.newArrayList()) + .build(); + }) + .collect(Collectors.toList()); + } + + @Override + public List listReceivableRevenueByDay(String groupId, Date startTime, Date endTime) { + Date rangeStart = cn.hutool.core.date.DateUtil.beginOfDay(startTime); + Date rangeEnd = cn.hutool.core.date.DateUtil.endOfDay(endTime); + Map revenueByDay = loadEstimatedTurnoverAmountByDay(groupId, rangeStart, rangeEnd); + return DateDetail.getDatesByPeriod(rangeStart, rangeEnd).stream() + .map(day -> { + String dayStr = DateDetail.getDate2Str(day, DateDetail.DF); + return DayReceivableRevenueVo.builder() + .day(dayStr) + .revenue(revenueByDay.get(dayStr)) + .build(); + }) + .collect(Collectors.toList()); + } + + /** + * 按自然日汇总营业额预估。 + *

+ * {@code listEstimatedTurnoverByRange} 的 {@code storeId} 须为门店绑定的收银平台 ID({@code cashierStoreId}), + * 与 {@code ftb_analysis_estimated_turnover.F_StoreId} 及营业额预估任务 MQ 落库口径一致。 + */ + private Map loadEstimatedTurnoverAmountByDay(String groupId, Date rangeStart, Date rangeEnd) { + AttendanceGroup group = attendanceGroupService.getById(groupId); + if (group == null || StringUtil.isEmpty(group.getOrgId())) { + return Collections.emptyMap(); + } + String cashierStoreId = resolveCashierStoreIdByOrgId(group.getOrgId()); + if (StringUtil.isEmpty(cashierStoreId)) { + log.warn("loadEstimatedTurnoverAmountByDay 门店未绑定收银平台; groupId={}, orgId={}", groupId, group.getOrgId()); + return Collections.emptyMap(); + } + LocalDate startLd = rangeStart.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate endLd = rangeEnd.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + List rows; + try { + rows = estimateTurnoverTaskApi.listEstimatedTurnoverByRange(cashierStoreId, startLd, endLd); + } catch (Exception ex) { + log.warn("loadEstimatedTurnoverAmountByDay listEstimatedTurnoverByRange failed; groupId={}, cashierStoreId={}, msg={}", + groupId, cashierStoreId, ex.getMessage()); + return Collections.emptyMap(); + } + if (CollUtil.isEmpty(rows)) { + return Collections.emptyMap(); + } + Map map = new HashMap<>(); + for (EstimateTurnoverStoredRowApiVo row : rows) { + if (row == null || row.getDate() == null) { + continue; + } + BigDecimal amt = row.getEstimatedTurnover(); + if (amt == null) { + continue; + } + map.merge(row.getDate().toString(), amt, BigDecimal::add); + } + return map; + } + + /** + * 按自然日汇总门店应收营业额(数据分析服务 {@link AnalysisExternalInterfaceApi#getStore90DayData})。 + */ + private Map loadReceivableRevenueAmountByDay(String groupId) { + AttendanceGroup group = attendanceGroupService.getById(groupId); + if (group == null || StringUtil.isEmpty(group.getOrgId())) { + return Collections.emptyMap(); + } + String cashierStoreId = resolveCashierStoreIdByOrgId(group.getOrgId()); + if (StringUtil.isEmpty(cashierStoreId)) { + log.warn("loadReceivableRevenueAmountByDay 门店未绑定收银平台; groupId={}, orgId={}", groupId, group.getOrgId()); + return Collections.emptyMap(); + } + List rows; + try { + rows = analysisExternalInterfaceApi.getStore90DayData( + cashierStoreId); + } catch (Exception ex) { + log.error("loadReceivableRevenueAmountByDay getStoreDayData failed; groupId={}, cashierStoreId={}, msg={}", + groupId, cashierStoreId, ex.getMessage()); + return Collections.emptyMap(); + } + if (CollUtil.isEmpty(rows)) { + return Collections.emptyMap(); + } + Map map = new HashMap<>(); + for (StoreDayReceivableRevenueVo row : rows) { + if (row == null || Objects.isNull(row.getDate())) { + continue; + } + String dayStr = row.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + BigDecimal amt = row.getReceivables(); + if (amt == null) { + continue; + } + map.merge(dayStr, amt, BigDecimal::add); + } + return map; + } + + /** + * 由组织/门店 ID 解析收银平台门店 ID(营业额预估落库与 Feign 查询均使用该 ID)。 + */ + private String resolveCashierStoreIdByOrgId(String orgId) { + if (StringUtil.isEmpty(orgId)) { + return null; + } + OrganizeGeneralDetailVO organize = organizeApi.organizeInfoById(null, orgId).getData(); + return organize == null ? null : organize.getCashierStoreId(); + } + + private List buildDayShiftPostStats(String dayStr, + Map> groupUserMap, + Map> inGroupDaysByUserId, + Map>> rulesByUserAndDay, + Map userMap, + Map shiftMap, + Map latestGroupUserByUserId, + String groupId) { + Map nonLineTemplateMap = new LinkedHashMap<>(); + Map>> nonLinePostUsers = new LinkedHashMap<>(); + List lineShifts = CollUtil.newArrayList(); + + groupUserMap.forEach((userId, groupUserVos) -> { + if (!inGroupDaysByUserId.getOrDefault(userId, Collections.emptySet()).contains(dayStr)) { + return; + } + AttendanceGroupUser attendanceGroupUserVo = latestGroupUserByUserId.get(userId); + if (Objects.isNull(attendanceGroupUserVo)) { + return; + } + String postId = resolvePostId(userMap.get(userId)); + List dayRules = rulesByUserAndDay + .getOrDefault(userId, Collections.emptyMap()) + .getOrDefault(dayStr, Collections.emptyList()); + + for (FtbAttendanceDailyRule dayRule : dayRules) { + if (shouldSkipHistoryDayRule(dayRule, attendanceGroupUserVo, groupId)) { + continue; + } + if (Objects.equals(dayRule.getFixedMark(), 2)) { + lineShifts.add(buildLineShiftPostStat(dayRule, postId)); + continue; + } + if (StringUtil.isEmpty(dayRule.getShiftId())) { + continue; + } + nonLineTemplateMap.putIfAbsent(dayRule.getShiftId(), buildNonLineShiftPostTemplate(dayRule, shiftMap)); + nonLinePostUsers + .computeIfAbsent(dayRule.getShiftId(), k -> new LinkedHashMap<>()) + .computeIfAbsent(postId, k -> new LinkedHashSet<>()) + .add(userId); + } + }); + + List shifts = CollUtil.newArrayList(); + for (Map.Entry entry : nonLineTemplateMap.entrySet()) { + ShiftPostStatVo template = entry.getValue(); + Map> postUserMap = nonLinePostUsers.getOrDefault(entry.getKey(), Collections.emptyMap()); + template.setPosts(buildPostHeadcountList(postUserMap)); + shifts.add(template); + } + shifts.addAll(lineShifts); + shifts.sort(Comparator.comparing(ShiftPostStatVo::getStartTime, Comparator.nullsLast(String::compareTo))); + return shifts; + } + + private boolean shouldSkipHistoryDayRule(FtbAttendanceDailyRule dayRule, AttendanceGroupUser attendanceGroupUserVo, String groupId) { + return Objects.equals(attendanceGroupUserVo.getType(), 2) + && StringUtil.equals(attendanceGroupUserVo.getGroupId(), groupId) + && !StringUtil.equals(dayRule.getGroupId(), groupId) + && Objects.equals(dayRule.getSelfGroup(), 2); + } + + private String resolvePostId(PartUserInfoVo userInfo) { + if (Objects.isNull(userInfo) || StringUtil.isEmpty(userInfo.getPositionId())) { + return ""; + } + return userInfo.getPositionId(); + } + + private ShiftPostStatVo buildLineShiftPostStat(FtbAttendanceDailyRule dayRule, String postId) { + return ShiftPostStatVo.builder() + .shiftId(dayRule.getId()) + .shiftName(LINE_SCHEDULE_SHIFT_NAME) + .startTime(getDate2Str(dayRule.getOriginInPoint())) + .endTime(getDate2Str(dayRule.getOriginOutPoint())) + .lineSchedule(Boolean.TRUE) + .posts(CollUtil.newArrayList(ShiftPostHeadcountVo.builder().postId(postId).headcount(1).build())) + .build(); + } + + private ShiftPostStatVo buildNonLineShiftPostTemplate(FtbAttendanceDailyRule dayRule, Map shiftMap) { + ShiftNameVo shiftNameVo = StringUtil.isNotEmpty(dayRule.getPeriodInfo()) + ? JSONUtil.toBean(dayRule.getPeriodInfo(), ShiftNameVo.class) : new ShiftNameVo(); + AttendanceShiftNameEntity shiftEntity = shiftMap.getOrDefault(dayRule.getShiftId(), new AttendanceShiftNameEntity()); + String shiftName = StringUtil.isNotEmpty(shiftNameVo.getName()) ? shiftNameVo.getName() : shiftEntity.getName(); + return ShiftPostStatVo.builder() + .shiftId(dayRule.getShiftId()) + .shiftName(shiftName) + .startTime(getDate2Str(dayRule.getOriginInPoint())) + .endTime(getDate2Str(dayRule.getOriginOutPoint())) + .lineSchedule(Boolean.FALSE) + .build(); + } + + private List buildPostHeadcountList(Map> postUserMap) { + return postUserMap.entrySet().stream() + .map(e -> ShiftPostHeadcountVo.builder().postId(e.getKey()).headcount(e.getValue().size()).build()) + .sorted(Comparator.comparing(ShiftPostHeadcountVo::getPostId, Comparator.nullsLast(String::compareTo))) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsServiceImpl.java new file mode 100644 index 0000000..83bad13 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsServiceImpl.java @@ -0,0 +1,2846 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.dto.*; +import jnpf.attendance.mapper.AttendanceBalanceRecordMapper; +import jnpf.attendance.mapper.AttendanceDayStatisticsMapper; +import jnpf.attendance.mapper.PublicHolidayRulesMapper; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constants.MessageTopicConstants; +import jnpf.controller.util.ExcelExportTemplate; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.*; +import jnpf.enums.attendance.*; +import jnpf.exception.LoginException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.event.StatisticsBatchClearDto; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.event.StatisticsSingleHistoryDto; +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.AttendanceUserVo; +import jnpf.model.attendance.vo.CreateDayStatistics; +import jnpf.model.attendance.vo.SchedulesItemVo; +import jnpf.model.attendance.vo.UserSettingVo; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.common.DateRangeDto; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.DayStatisticsUtils; +import jnpf.util.auth.V2AuthPermissionUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.Lists; +import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.tuple.ImmutableTriple; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAdjusters; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.alibaba.fastjson.JSON.parseArray; +import static jnpf.constants.AttendanceConstant.HOLIDAYS_COLOR; +import static jnpf.enums.attendance.UserSettingEnum.*; +import static jnpf.util.attendance.DayStatisticsUtils.initSalaryUserData; + +/** + * 考勤日度统计表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-06-24 09:33:43 + */ +@Service +@Slf4j +public class AttendanceDayStatisticsServiceImpl extends ServiceImpl implements AttendanceDayStatisticsService { + @Resource + private V2UserApi v2UserApi; + @Autowired + private UserAntifreeze userApi; + @Resource + private RocketMQTemplate rocketMqTemplate; + @Resource + private AttendanceUserService userService; + @Resource + private AttendanceGroupService groupService; + @Resource + private AttendanceNoticeHandler noticeHandler; + @Resource(name = "ioIntensiveThreadPool") + private ThreadPoolExecutor threadPoolExecutor; + @Resource + private PublicHolidayRulesService rulesService; + @Resource + private AttendanceClockInService clockInService; + @Resource + private ExcelExportTemplate excelExportTemplate; + @Resource + private V2AuthPermissionUtils authPermissionUtils; + @Resource + private EnableBalanceService enableBalanceService; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceDailyRuleService dailyRuleService; + @Resource + private AttendanceLeaveTypeService leaveTypeService; + @Resource + private PublicHolidayRulesService holidayRulesService; + @Resource + private AttendanceDailyRuleService dailyRuleService1; + @Resource + private AttendanceSuperAdminService superAdminService; + @Resource + private AttendanceBaseSettingService baseSettingService; + @Resource + private AttendanceUserSettingService userSettingService; + @Resource + private AttendanceDayStatisticsUtilImpl dayStatisticsUtil; + @Resource + private PublicHolidayRulesMapper publicHolidayRulesMapper; + @Resource + private AttendanceBalanceRecordMapper balanceRecordMapper; + @Resource + private AttendanceFestivalRulesService festivalRulesService; + @Resource + private AttendanceCustomizeTableService customizeTableService; + + private static ImmutableTriple getShiftInfo(DayStatisticsShiftsJsonVo shiftInfo, AttendanceShiftNameEntity shiftNameEntity) { + String shiftName, shiftShortName, shiftShortColour; + // 判断当天是否为划线排班 + if (Objects.nonNull(shiftInfo.getFixedMark()) && shiftInfo.getFixedMark() == 2) { + shiftName = "划线排班"; + shiftShortName = "划线排班"; + shiftShortColour = HOLIDAYS_COLOR; + } else { + shiftName = Objects.nonNull(shiftNameEntity) && StringUtil.isNotEmpty(shiftNameEntity.getName()) ? shiftNameEntity.getName() : ""; + shiftShortName = Objects.nonNull(shiftNameEntity) && StringUtil.isNotEmpty(shiftNameEntity.getShortName()) ? shiftNameEntity.getShortName() : ""; + shiftShortColour = Objects.nonNull(shiftNameEntity) && StringUtil.isNotEmpty(shiftNameEntity.getColour()) ? shiftNameEntity.getColour() : ""; + } + return ImmutableTriple.of(shiftName, shiftShortName, shiftShortColour); + } + + private static List getShiftsTimeList(DayStatisticsNoticeQueryVo item, List shiftsJsonVoList, List shiftTimeList) { + List shiftsJsonVoFilterList = shiftsJsonVoList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(shiftsJsonVoFilterList)) { + shiftsJsonVoFilterList = shiftsJsonVoFilterList.stream().sorted(Comparator.comparing(DayStatisticsShiftsJsonVo::getInPoint)).collect(Collectors.toList()); + } + shiftsJsonVoFilterList.forEach(shiftsJsonVo -> { + StringBuilder shiftTimeStr = new StringBuilder(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("至"); + if (!DayStatisticsUtils.isSameDay(item.getDate(), shiftsJsonVo.getOutPoint())) { + shiftTimeStr.append("(次日)").append(DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")); + } else { + shiftTimeStr.append(DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")); + } + shiftTimeList.add(shiftTimeStr.toString()); + }); + return shiftsJsonVoFilterList; + } + + @Override + @SneakyThrows + public List getDayStatisticsData(DayStatisticsDataDto req) { + StatisticsDataQueryDto queryDto = new StatisticsDataQueryDto(); + BeanUtils.copyProperties(req, queryDto); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), req.getStartDate(), req.getEndDate(), req.getUserIds(), req.getWorkStatus()); + if (CollUtil.isEmpty(userBoundVoList)) { + return DayStatisticsUtils.getStatisticsDataPackage(null); + } + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + queryDto.setUserIds(userIds); + queryDto.setGroupIds(req.getFilterList().stream().map(GroupFilterDto::getGroupId).collect(Collectors.toList())); + return DayStatisticsUtils.getStatisticsDataPackage(this.baseMapper.getDayStatisticsDataQuery(queryDto)); + } + + /** + * 获取有权限的用户列表 + * + * @param filterList 筛选条件 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param userIdsFilter 用户ID + * @param workStatus 用户状态 + * @return 用户列表 + */ + @Override + public List getUserIdArr(List filterList, String startDate, String endDate, List userIdsFilter, String workStatus) { + List groupIds = filterList.stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + List teamIds = filterList.stream().filter(item -> item.getTeam() == 1).map(GroupFilterDto::getTeamId).distinct().collect(Collectors.toList()); + Map> groupTeamMap = Map.of(); + Map> groupTeamStaMap = Map.of(); + if (CollUtil.isNotEmpty(teamIds)) { + // 按照考勤组分组,获取不同的班次id集合 + groupTeamMap = filterList.stream().filter(item -> item.getTeam() == 1 && StringUtil.isNotEmpty(item.getTeamId())).collect(Collectors.groupingBy(GroupFilterDto::getGroupId, Collectors.mapping(GroupFilterDto::getTeamId, Collectors.toList()))); + groupTeamStaMap = filterList.stream().collect(Collectors.groupingBy(GroupFilterDto::getGroupId, Collectors.mapping(GroupFilterDto::getTeam, Collectors.toList()))); + } + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(workStatus); + Assert.notNull(workStatusEnum, "工作状态不正确"); + List workStatusEnums = DayStatisticsUtils.getWorkStatusEnumList(Objects.requireNonNull(workStatusEnum)); + List userBoundVoList = attendanceUserService.batchGetUserBoundVO(groupIds, workStatusEnums); + if (CollUtil.isEmpty(userBoundVoList)) { + userBoundVoList = new ArrayList<>(); + } + // 判断是否有人员过滤,有的话需要取交集 + List userIds = CollUtil.isNotEmpty(userBoundVoList) ? userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()) : List.of(); + // 查询用户所属权限 + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + // 是否配置仅下属 + List underlingUserId = null; + if (Objects.nonNull(targetAuthIdsVO) && Objects.equals(targetAuthIdsVO.getTargetAuthEnums(), TargetAuthEnums.USER)) { + if (CollUtil.isEmpty(userBoundVoList)) { + return CollUtil.newArrayList(); + } + underlingUserId = userIds; + } + // 查询用户考勤组用户 + DateRangeDto dateRangeDto = new DateRangeDto(startDate, endDate); + Date start = jnpf.util.DateUtil.stringToDate(dateRangeDto.getStartDate()); + Date end = jnpf.util.DateUtil.stringToDate(dateRangeDto.getEndDate()); + List groupUsers = attendanceUserService.queryByUsersAndGroup(start, end, underlingUserId, groupIds); + if (CollUtil.isEmpty(groupUsers)) { + return CollUtil.newArrayList(); + } + List queryUserIds = new ArrayList<>(); + Map> groupUserMap = CollUtil.isNotEmpty(groupUsers) ? groupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)) : new HashMap<>(); + for (Map.Entry> listEntry : groupUserMap.entrySet()) { + if (dailyRuleService.findUserIsExistsByDay(listEntry.getValue(), start, end) != 0) { + queryUserIds.add(listEntry.getKey()); + } + } + if (CollUtil.isEmpty(queryUserIds)) { + return CollUtil.newArrayList(); + } + // 获取未在权限用户里面的用户集合 + List noPermissionUserIds = new ArrayList<>(CollUtil.subtract(queryUserIds, userIds)); + if (CollUtil.isNotEmpty(noPermissionUserIds)) { + List userBoundVos = userApi.getStaffRosterListInfoByIds(noPermissionUserIds); + Map userMap = userBoundVos.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity(), (r1, r2) -> r1)); + if (CollUtil.isNotEmpty(userMap)) { + List finalUserBoundVoList = userBoundVoList; + userBoundVos.forEach(item -> { + PartUserInfoVo partUserInfoVo = userMap.get(item.getUserId()); + if (partUserInfoVo != null && finalUserBoundVoList.stream().noneMatch(userBoundVO -> userBoundVO.getId().equals(partUserInfoVo.getUserId()))) { + UserBoundVO userBoundVO = new UserBoundVO(); + BeanUtils.copyProperties(partUserInfoVo, userBoundVO); + userBoundVO.setId(partUserInfoVo.getUserId()); + userBoundVO.setUserName(partUserInfoVo.getRealName()); + userBoundVO.setWorkStatusEnums(UserWorkStatusEnums.getUserWorkStatusEnumsByValue(item.getWorkerStatus())); + finalUserBoundVoList.add(userBoundVO); + } + }); + } + } + if (CollUtil.isEmpty(userBoundVoList)) { + return CollUtil.newArrayList(); + } + Map userIdToUserBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, item -> item)); + List result = new ArrayList<>(); + if (CollUtil.isNotEmpty(teamIds)) { + Map> groupUserListMap = groupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)).entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().stream().map(AttendanceGroupUser::getUserId).map(userIdToUserBoundVoMap::get).filter(Objects::nonNull).collect(Collectors.toList()))); + Map> finalGroupTeamMap = groupTeamMap; + Map> finalGroupTeamStaMap = groupTeamStaMap; + groupUserListMap.forEach((groupId, groupUserList) -> { + boolean flag = finalGroupTeamMap.containsKey(groupId) && (finalGroupTeamStaMap.containsKey(groupId) && !finalGroupTeamStaMap.get(groupId).contains(0)); + result.addAll(flag ? groupUserList.stream().filter(item -> teamIds.contains(item.getStoreTeamId())).collect(Collectors.toList()) : groupUserList); + }); + userBoundVoList = result.stream().distinct().collect(Collectors.toList()); + } + if (!workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.QB)) { + userBoundVoList = userBoundVoList.stream().filter(item -> item.getWorkStatusEnums().getCode().equals(workStatusEnum.getCode())).collect(Collectors.toList()); + } + if (CollUtil.isNotEmpty(userIdsFilter)) { + userBoundVoList = userBoundVoList.stream().filter(item -> userIdsFilter.contains(item.getId())).collect(Collectors.toList()); + } + return userBoundVoList; + } + + @Override + @SneakyThrows + public PageInfo getDayPageList(DayStatisticsPageListDto req) { + DayStatisticsDataPageListQueryDto queryDto = new DayStatisticsDataPageListQueryDto(); + BeanUtils.copyProperties(req, queryDto); + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) req.getPageSize()); + pageInfo.setPageNum((int) req.getCurrentPage()); + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), req.getStartDate(), req.getEndDate(), req.getUserIds(), req.getWorkStatus()); + Map userBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + if (CollUtil.isEmpty(userBoundVoList)) { + return pageInfo; + } + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + //获取考勤组信息 + List groupList = this.groupService.queryListIncludeDeleteByIds(groupIds); + if (CollUtil.isEmpty(groupList)) { + log.info("未查询到考勤组数据:{}", groupIds); + return pageInfo; + } + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + queryDto.setUserIds(userIds); + req.setUserIds(userIds); + req.setGroupMap(groupMap); + req.setUserBoundMap(userBoundVoMap); + queryDto.setGroupIds(groupIds); + if (Objects.nonNull(req.getTypeId())) { + StatisticsEnumUtil.TabStaBlockEnum tabDataTypeEnum = StatisticsEnumUtil.TabStaBlockEnum.getTabDataTypeEnum(req.getTypeId()); + Assert.notNull(tabDataTypeEnum, "统计类型不正确"); + if (req.getTypeId().equals(StatisticsEnumUtil.TabStaBlockEnum.QB.getCode())) { + queryDto.setQueryType(1); + } else if (req.getTypeId().equals(StatisticsEnumUtil.TabStaBlockEnum.ZC.getCode())) { + queryDto.setQueryType(2); + } else { + queryDto.setQueryType(3); + queryDto.setFieldName(Objects.requireNonNull(tabDataTypeEnum).getFieldName()); + } + } + PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + List pageListVoList = this.baseMapper.getDayPageList(queryDto); + if (CollUtil.isEmpty(pageListVoList)) { + return pageInfo; + } + pageInfo = new PageInfo<>(dayStatisticsAssemble(pageListVoList, groupMap, userBoundVoMap)); + pageInfo.setTotal(new PageInfo<>(pageListVoList).getTotal()); + pageInfo.setPageSize(Math.toIntExact(req.getPageSize())); + pageInfo.setPageNum(Math.toIntExact(req.getCurrentPage())); + return pageInfo; + } + + /** + * 日统计数据封装 + * + * @param statisticsQueryVoList 日统计数据 + * @param groupMap 考勤数据 + * @param staffRosterMap 用户花名册数据 + * @return List + */ + private List dayStatisticsAssemble(List statisticsQueryVoList, Map groupMap, Map staffRosterMap) { + Map> shiftsJsonMap = new HashMap<>(); + Set shiftIdSet = new HashSet<>(); + for (DayStatisticsQueryVo item : statisticsQueryVoList) { + List shiftsJsonVoList = parseArray(item.getShiftsJson(), DayStatisticsShiftsJsonVo.class); + if (CollUtil.isEmpty(shiftsJsonVoList)) { + shiftsJsonVoList = new ArrayList<>(); + } + shiftsJsonMap.put(item.getId(), shiftsJsonVoList); + shiftsJsonVoList.stream().map(DayStatisticsShiftsJsonVo::getShiftId).filter(Objects::nonNull).forEach(shiftIdSet::add); + } + List shiftIdList = new ArrayList<>(shiftIdSet); + Map shiftNameMap = CollUtil.isEmpty(shiftIdList) ? new HashMap<>() : dailyRuleService1.getShiftByShiftIds(shiftIdList); + + return statisticsQueryVoList.stream().map(item -> { + DayStatisticsPageListVo dayStatistics = DayStatisticsPageListVo.builder().build(); + BeanUtils.copyProperties(item, dayStatistics); + // 组装用户信息 + UserBoundVO staffRosterDto = staffRosterMap.get(item.getUserId()); + if (staffRosterDto != null) { + dayStatistics.setDeptName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + dayStatistics.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + dayStatistics.setUserName(StringUtil.isNotEmpty(staffRosterDto.getName()) ? staffRosterDto.getName() : ""); + dayStatistics.setWorkStatus(Objects.nonNull(staffRosterDto.getWorkStatusEnums()) ? staffRosterDto.getWorkStatusEnums().getCode() : null); + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(dayStatistics.getWorkStatus()); + dayStatistics.setWorkStatusStr(Objects.nonNull(workStatusEnum) ? workStatusEnum.getMsg() : ""); + } + // 计算各类请假数据 + dayStatistics.setCustomLeaveList(DayStatisticsUtils.getDayCustomLeaveList(item.getCustomLeaveList())); + // 组装班组名称 + AttendanceGroup group = groupMap.get(item.getGroupId()); + dayStatistics.setGroupName(group != null ? group.getGroupName() : null); + dayStatistics.setDate(DateUtil.formatDate(item.getDate())); + // 处理班次、班次时间、打卡时间 + List shiftsJsonVoList = shiftsJsonMap.getOrDefault(item.getId(), new ArrayList<>()); + List schedulesItemVos = getSchedulesItemVos(shiftNameMap, shiftsJsonVoList); + dayStatistics.setShiftList(schedulesItemVos); + String shiftStr = CollUtil.isNotEmpty(schedulesItemVos) ? StringUtil.join(schedulesItemVos.stream().map(SchedulesItemVo::getShortName).filter(StringUtil::isNotBlank).distinct().collect(Collectors.toList()), "|") : ""; + dayStatistics.setShiftStr(shiftStr); + // 处理班次时间 + boolean containsOrdinary = shiftsJsonVoList.stream().anyMatch(json -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), json.getAttendanceType())); + List shiftsJsonVoFilterList = shiftsJsonVoList.stream().filter(m -> m.getAttendanceType() != null && (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()) || (!containsOrdinary && m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())))).sorted(Comparator.comparing(DayStatisticsShiftsJsonVo::getInPoint)).collect(Collectors.toList()); + List shiftTimeList = new ArrayList<>(); + StringBuilder shiftTimeStrList = new StringBuilder(); + for (DayStatisticsShiftsJsonVo shiftsJsonVo : shiftsJsonVoFilterList) { + String inTime = DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm"); + String outTime = DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm"); + StringBuilder shiftTimeStr = new StringBuilder(inTime).append("~"); + shiftTimeStrList.append(inTime).append("~"); + if (!DayStatisticsUtils.isSameDay(item.getDate(), shiftsJsonVo.getOutPoint())) { + String timeRange = "(次日)".concat(outTime); + shiftTimeStr.append(timeRange); + shiftTimeStrList.append(timeRange).append("|"); + } else { + shiftTimeStr.append(outTime); + shiftTimeStrList.append(outTime).append("|"); + } + shiftTimeList.add(shiftTimeStr.toString()); + } + dayStatistics.setShiftTimeList(shiftTimeList); + dayStatistics.setShiftTimeStr(shiftTimeStrList.length() > 0 ? shiftTimeStrList.substring(0, shiftTimeStrList.length() - 1) : ""); + // 处理打卡时间 + List clockTimeList = new ArrayList<>(); + StringBuilder clockTimeStr = new StringBuilder(); + for (DayStatisticsShiftsJsonVo shiftsJsonVo : shiftsJsonVoFilterList) { + DayStatisticsUtils.enclosureData(item, shiftsJsonVo, clockTimeList, clockTimeStr); + } + // 处理实际出勤工时 + if (dayStatistics.getActualAttendHours() != null && dayStatistics.getActualAttendHours().compareTo(BigDecimal.ZERO) < 0) { + dayStatistics.setActualAttendHours(BigDecimal.ZERO); + } + dayStatistics.setClockTimeList(clockTimeList); + dayStatistics.setClockTimeStr(clockTimeStr.length() > 0 ? clockTimeStr.substring(0, clockTimeStr.length() - 1) : ""); + return dayStatistics; + }).collect(Collectors.toList()); + } + + /** + * 班次数据封装 + * + * @param shiftNameMap 用户班次信息 + * @param shiftsJsonVoList 用户班次数据 + * @return List + */ + private List getSchedulesItemVos(Map shiftNameMap, List shiftsJsonVoList) { + if (CollUtil.isEmpty(shiftsJsonVoList)) { + return new ArrayList<>(); + } + // 判断当天是否为划线排班 + boolean isUnderlineScheduling = shiftsJsonVoList.stream().anyMatch(item -> Objects.nonNull(item.getFixedMark()) && item.getFixedMark().equals(2)); + List collect = new ArrayList<>(); + for (DayStatisticsShiftsJsonVo shift : shiftsJsonVoList) { + String shiftId = shift.getShiftId(); + Integer type = DayStatisticsUtils.getType(shift.getAttendanceType()); + if (Objects.isNull(type)) { + continue; + } + // 划线排班只需要显示一个班次 + if (isUnderlineScheduling && !collect.isEmpty()) { + continue; + } + AttendanceShiftNameEntity shiftName = shiftNameMap.get(shiftId); + if (Objects.equals(shift.getAttendanceType(), AttendanceTypeEnum.BUSINESS_TRIP.getCode()) || Objects.equals(shift.getAttendanceType(), AttendanceTypeEnum.STEP_OUT.getCode())) { + shiftName = null; + } + ImmutableTriple shiftInfo = getShiftInfo(shift, shiftName); + // 是否展示班次信息 + boolean isSpecialType = !Objects.equals(shift.getAttendanceType(), AttendanceTypeEnum.ORDINARY.getCode()) && !Objects.equals(shift.getAttendanceType(), AttendanceTypeEnum.LEAVE.getCode()); + // 是否计薪假或者不计薪假 + boolean isHolidayType = Objects.equals(SchedulesTypeEnum.HOLIDAYS.getCode(), type) || Objects.equals(SchedulesTypeEnum.PAID_HOLIDAYS.getCode(), type); + String nameValue = isSpecialType ? "" : shiftInfo.left; + String shortNameValue = isSpecialType ? AttendanceTypeEnum.getMsg(shift.getAttendanceType()) : shiftInfo.middle; + String colourValue = isHolidayType ? HOLIDAYS_COLOR : shiftInfo.right; + SchedulesItemVo schedulesItemVo = SchedulesItemVo.builder().type(type).name(nameValue).shortName(shortNameValue).colour(colourValue).schedulesType(shift.getSchedulesType()).build(); + collect.add(schedulesItemVo); + } + // 按名称分组处理重复的班次信息 + Map> groupedByName = collect.stream().filter(vo -> vo != null && StringUtil.isNotEmpty(vo.getName())).collect(Collectors.groupingBy(SchedulesItemVo::getName)); + groupedByName.forEach((name, items) -> { + if (StringUtil.isEmpty(name) || items.size() < 2) { + return; + } + for (int i = 1; i < items.size(); i++) { + SchedulesItemVo item = items.get(i); + item.setName(""); + item.setShortName(""); + item.setIsShow(0); + } + }); + return collect.stream().filter(Objects::nonNull).sorted(Comparator.comparing(SchedulesItemVo::getSchedulesType, Comparator.nullsFirst(Comparator.naturalOrder()))).collect(Collectors.toList()); + } + + private PageInfo getDayClockInPageListExport(DayStatisticsPageListDto req) { + // 过滤了指定用户以及指定工作状态筛选 + GroupUserQueryDto build = GroupUserQueryDto.builder().filterList(req.getFilterList()).startTime(req.getStartDate()).endTime(req.getEndDate()).tenantId(StringUtil.isNotEmpty(req.getTenantId()) ? req.getTenantId() : UserProvider.getUser().getTenantId()).userIds(req.getUserIds()).workStatusEnum(req.getWorkStatusEnum()).staffRosterMap(req.getUserBoundMap()).build(); + List exportVos = userService.getUsersDayByGroupIds(build, req); + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) req.getPageSize()); + pageInfo.setPageNum((int) req.getCurrentPage()); + pageInfo.setTotal(req.getTotal()); + pageInfo.setList(exportVos); + if (CollUtil.isEmpty(pageInfo.getList())) { + return pageInfo; + } + List userIdList = pageInfo.getList().stream().map(ClockInExportVo::getUserId).distinct().collect(Collectors.toList()); + if (CollUtil.isEmpty(userIdList)) { + return pageInfo; + } + // 将pageInfo1.getList()的数据先根据人分组排序在根据日期排序 + // 查询指定时间的打卡数据 + List clockInExportVoList = userService.getClockInExportVoList(build, pageInfo.getList()); + pageInfo.setList(clockInExportVoList); + // 得到用户 日期 考勤组 集合 + pageInfo.getList().forEach(v -> { + // 考勤组名称 + AttendanceGroup attendanceGroup = req.getGroupMap().get(v.getGroupId()); + v.setGroupName(null != attendanceGroup ? attendanceGroup.getGroupName() : ""); + }); + pageInfo.setPageSize((int) req.getPageSize()); + return pageInfo; + + } + + @Override + @SneakyThrows + public void dayDataExport(DayStatisticsExportDto req) { + String tenantId = UserProvider.getUser().getTenantId(); + DayStatisticsPageListDto dayStatisticsPageListDto = BeanUtil.toBean(req, DayStatisticsPageListDto.class); + List list = customizeTableService.findList(null, 1, 1); + // 排除掉已删除的请假类型 + if (CollUtil.isNotEmpty(list)) { + Date start = DateDetail.getStr2Date(req.getStartDate()); + Date end = DateDetail.getStr2Date(req.getEndDate()); + list.removeIf(v -> Objects.nonNull(v.getDeleteTime()) && (v.getDeleteTime().compareTo(start) < 0 || v.getDeleteTime().compareTo(end) > 0)); + } + Assert.notEmpty(list, "导出失败,未查询到表头数据"); + ExcelExportTemplate.MoreSheetDto moreSheetDto = new ExcelExportTemplate.MoreSheetDto(); + moreSheetDto.setUserId(UserProvider.getLoginUserId()); + moreSheetDto.setFilename(req.getStartDate() + "-" + req.getEndDate() + "考勤数据导出"); + // 获取动态表头相关信息 + List customizeTableVos = getAttendanceCustomizeTableVos(req.getStartDate(), req.getEndDate(), 1); + // 获取固定表头名称 + List> headerNameList = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().map(vo -> CollUtil.newArrayList(vo.getHeadName())).collect(Collectors.toList()) : new ArrayList<>(); + // 获取固定表头字段 + List headerFieldList = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().map(AttendanceCustomizeTableVo::getField).collect(Collectors.toList()) : new ArrayList<>(); + // 获取固定表头字段宽度 + List headerFieldWidthsList = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().map(AttendanceCustomizeTableVo::getWidth).collect(Collectors.toList()) : new ArrayList<>(); + // 导出字段的顺序 + LinkedHashMap dynamicHeadersMap = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().sorted(Comparator.comparing(AttendanceCustomizeTableVo::getSort)).collect(Collectors.toMap(AttendanceCustomizeTableVo::getField, AttendanceCustomizeTableVo::getSort, (e1, e2) -> e1, LinkedHashMap::new)) : new LinkedHashMap<>(); + dayStatisticsPageListDto.setCurrentPage(1); + dayStatisticsPageListDto.setPageSize(20000); + dayStatisticsPageListDto.setTenantId(tenantId); + List dayPageList = this.getDayPageList(dayStatisticsPageListDto).getList(); + List> data = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(dayPageList)) { + dayPageList.forEach(dayStaVo -> { + data.add(DayStatisticsUtils.getDayStaDataList(dayStaVo, dynamicHeadersMap)); + }); + } + ExcelExportTemplate.MoreSheetDetailDto moreSheetDetailDto = new ExcelExportTemplate.MoreSheetDetailDto("考勤数据导出", "统计数据", null, headerNameList, headerFieldList, headerFieldWidthsList, 2000, (page, size) -> data, null); + moreSheetDto.getMoreSheetDetailDto().add(moreSheetDetailDto); + String key = "day" + UserProvider.getLoginUserId() + System.currentTimeMillis(); + List head1 = DayStatisticsUtils.getHead(); + ExcelExportTemplate.MoreSheetDetailDto moreSheetDetailDto1 = new ExcelExportTemplate.MoreSheetDetailDto("打卡数据导出", "打卡数据", ClockInExportVo.class, head1.stream().map(vo -> CollUtil.newArrayList(req.getStartDate() + "至" + req.getEndDate() + "成员打卡记录", vo)).collect(Collectors.toList()), null, DayStatisticsUtils.getWidths(), 2000, (page, size) -> { + dayStatisticsPageListDto.setCurrentPage(page); + dayStatisticsPageListDto.setPageSize(size); + dayStatisticsPageListDto.setTenantId(tenantId); + dayStatisticsPageListDto.setKey(key); + return getDayClockInPageListExport(dayStatisticsPageListDto); + }, null); + moreSheetDto.getMoreSheetDetailDto().add(moreSheetDetailDto1); + excelExportTemplate.exportMoreExcelModule(moreSheetDto); + } + + /** + * 获取可显示的请假类型 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param type 1-日统计 2-月统计 + * @return 请假类型 + */ + private List getAttendanceCustomizeTableVos(String startDate, String endDate, Integer type) { + // 实时查询自定义请假类型 + List customizeTableVos = customizeTableService.findList(null, 1, type); + // 排除掉已删除的请假类型 + if (CollUtil.isNotEmpty(customizeTableVos)) { + Date start = DateDetail.getStr2Date(startDate); + Date end = DateDetail.getStr2Date(endDate); + customizeTableVos.removeIf(v -> Objects.nonNull(v.getDeleteTime()) && (v.getDeleteTime().compareTo(start) < 0 || v.getDeleteTime().compareTo(end) > 0)); + } + // 过滤掉不展示的数据 + customizeTableVos = customizeTableVos.stream().sorted(Comparator.comparing(AttendanceCustomizeTableVo::getSort)).collect(Collectors.toList()); + return customizeTableVos; + } + + @Override + @SneakyThrows + public List getMonthStatisticsData(MouthStatisticsDataDto req) { + DateRangeDto dateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + StatisticsDataQueryDto queryDto = new StatisticsDataQueryDto(); + BeanUtils.copyProperties(req, queryDto); + queryDto.setStartDate(dateRangeDto.getStartDate()); + queryDto.setEndDate(dateRangeDto.getEndDate()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), req.getUserIds(), req.getWorkStatus()); + if (CollUtil.isEmpty(userBoundVoList)) { + return DayStatisticsUtils.getStatisticsDataPackage(null); + } + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + queryDto.setUserIds(userIds); + queryDto.setGroupIds(groupIds); + return DayStatisticsUtils.getStatisticsDataPackage(this.baseMapper.getMonthStatisticsDataQuery(queryDto, Boolean.TRUE)); + } + + @Override + public PageInfo getMonthPageList(MonthStatisticsPageListDto req) throws Exception { + MonthStatisticsDataQueryDto queryDto = new MonthStatisticsDataQueryDto(); + BeanUtils.copyProperties(req, queryDto); + DateRangeDto dateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + queryDto.setStartDate(dateRangeDto.getStartDate()); + queryDto.setEndDate(dateRangeDto.getEndDate()); + //获取考勤组信息 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) req.getPageSize()); + pageInfo.setPageNum((int) req.getCurrentPage()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), req.getUserIds(), req.getWorkStatus()); + Map userBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + if (CollUtil.isEmpty(userBoundVoList)) { + return pageInfo; + } + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + List groupList = this.groupService.queryListIncludeDeleteByIds(groupIds); + if (CollUtil.isEmpty(groupList)) { + log.info("未查询到考勤组数据:{}", groupIds); + return pageInfo; + } + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + queryDto.setUserIds(userIds); + req.setUserIds(userIds); + req.setGroupMap(groupMap); + req.setUserBoundMap(userBoundVoMap); + queryDto.setGroupIds(groupIds); + if (Objects.nonNull(req.getTypeId())) { + StatisticsEnumUtil.TabStaBlockMonthEnum tabDataTypeEnum = StatisticsEnumUtil.TabStaBlockMonthEnum.getTabDataTypeEnum(req.getTypeId()); + Assert.notNull(tabDataTypeEnum, "统计类型不正确"); + if (req.getTypeId().equals(StatisticsEnumUtil.TabStaBlockMonthEnum.QB.getCode())) { + queryDto.setQueryType(1); + } else if (req.getTypeId().equals(StatisticsEnumUtil.TabStaBlockMonthEnum.ZC.getCode())) { + queryDto.setQueryType(2); + queryDto.setFieldName(Objects.requireNonNull(tabDataTypeEnum).getFieldName()); + } else { + queryDto.setQueryType(3); + queryDto.setFieldName(Objects.requireNonNull(tabDataTypeEnum).getFieldName()); + } + } + PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + List pageListVoList = this.baseMapper.getMonthPageList(queryDto); + if (CollUtil.isEmpty(pageListVoList)) { + return pageInfo; + } + pageInfo = new PageInfo<>(monthStatisticsAssemble(req.getMonth(), queryDto.getStartDate(), queryDto.getEndDate(), pageListVoList, groupMap, userBoundVoMap)); + pageInfo.setTotal(new PageInfo<>(pageListVoList).getTotal()); + pageInfo.setPageSize(Math.toIntExact(req.getPageSize())); + pageInfo.setPageNum(Math.toIntExact(req.getCurrentPage())); + return pageInfo; + } + + /** + * 月统计数据封装 + * + * @param month 月份 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param statisticsQueryVoList 日统计汇总数据 + * @param groupMap 考勤组信息 + * @param staffRosterMap 用户花名册数据 + * @return List + * @throws Exception 异常问题 + */ + private List monthStatisticsAssemble(String month, String startDate, String endDate, List statisticsQueryVoList, Map groupMap, Map staffRosterMap) throws Exception { + List userIds = statisticsQueryVoList.stream().map(MonthStatisticsQueryVo::getUserId).distinct().collect(Collectors.toList()); + List groupIds = statisticsQueryVoList.stream().map(MonthStatisticsQueryVo::getGroupId).distinct().collect(Collectors.toList()); + //批量获取用户加入考勤组周期 + DateRangeDto rangeDto = new DateRangeDto(); + rangeDto.setStartDate(startDate); + rangeDto.setEndDate(endDate); + Map>> userCycleMap = userService.batchGetUserCycleList(rangeDto, groupIds, userIds); + //批量获取用户应休天数 + List holidayBalances = rulesService.getBalanceList(month, userIds); + Map holidayBalanceMap = CollUtil.isNotEmpty(holidayBalances) ? holidayBalances.stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, AttendancePublicHolidayBalance::getTotal)) : new HashMap<>(); + //批量获取用户已休天数 + Map usePublicHoliday = dailyRuleService.getUserPublicHoliday(month, userIds); + // 实时查询自定义请假类型 + return statisticsQueryVoList.stream().map(item -> { + MonthStatisticsPageListVo monthStatistics = MonthStatisticsPageListVo.builder().build(); + BeanUtils.copyProperties(item, monthStatistics); + monthStatistics.setMonth(month); + monthStatistics.setGroupName(groupMap.get(item.getGroupId()).getGroupName()); + // 设置应休|已休 + monthStatistics.setRetired(usePublicHoliday.getOrDefault(item.getUserId(), BigDecimal.ZERO).stripTrailingZeros().toPlainString() + "|" + holidayBalanceMap.getOrDefault(item.getUserId(), BigDecimal.ZERO).stripTrailingZeros().toPlainString()); + // 计算各类请假数据 + monthStatistics.setCustomLeaveList(DayStatisticsUtils.getMonthCustomLeaveList(item.getCustomLeaveList())); + //设置用户基本信息 + if (staffRosterMap.containsKey(item.getUserId())) { + UserBoundVO staffRosterDto = staffRosterMap.get(item.getUserId()); + monthStatistics.setDeptName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + monthStatistics.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + monthStatistics.setUserName(StringUtil.isNotEmpty(staffRosterDto.getName()) ? staffRosterDto.getName() : ""); + monthStatistics.setWorkStatus(Objects.nonNull(staffRosterDto.getWorkStatusEnums()) ? staffRosterDto.getWorkStatusEnums().getCode() : null); + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(monthStatistics.getWorkStatus()); + monthStatistics.setWorkStatusStr(Objects.nonNull(workStatusEnum) ? workStatusEnum.getMsg() : ""); + } + //设置考勤组周期 + if (userCycleMap.containsKey(item.getGroupId()) && userCycleMap.get(item.getGroupId()).containsKey(item.getUserId())) { + monthStatistics.setCycleList(userCycleMap.get(item.getGroupId()).get(item.getUserId())); + StringBuilder cycleStr = new StringBuilder(); + userCycleMap.get(item.getGroupId()).get(item.getUserId()).forEach(cycle -> cycleStr.append(cycle).append(",")); + monthStatistics.setCycleStr(cycleStr.length() > 0 ? cycleStr.substring(0, cycleStr.length() - 1) : ""); + } + return monthStatistics; + }).collect(Collectors.toList()); + } + + /** + * 计薪月统计数据封装 + * + * @param month 月份 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param statisticsQueryVoList 日统计汇总数据 + * @param groupMap 考勤组信息 + * @param staffRosterMap 用户花名册数据 + * @return List + * @throws Exception 异常问题 + */ + private List monthPayrollStatisticsAssemble(String month, String startDate, String endDate, List statisticsQueryVoList, Map groupMap, Map staffRosterMap) throws Exception { + List userIds = statisticsQueryVoList.stream().map(MonthPayrollStatisticsQueryVo::getUserId).distinct().collect(Collectors.toList()); + List groupIds = statisticsQueryVoList.stream().map(MonthPayrollStatisticsQueryVo::getGroupId).distinct().collect(Collectors.toList()); + //批量获取用户加入考勤组周期 + DateRangeDto rangeDto = new DateRangeDto(); + rangeDto.setStartDate(startDate); + rangeDto.setEndDate(endDate); + Map>> userCycleMap = userService.batchGetUserCycleList(rangeDto, groupIds, userIds); + // 实时查询自定义请假类型 + return statisticsQueryVoList.stream().map(item -> { + MonthPayrollStatisticsPageListVo monthStatistics = MonthPayrollStatisticsPageListVo.builder().build(); + BeanUtils.copyProperties(item, monthStatistics); + monthStatistics.setMonth(month); + monthStatistics.setGroupName(groupMap.get(item.getGroupId()).getGroupName()); + // 计算各类请假数据 + monthStatistics.setCustomLeaveList(DayStatisticsUtils.getMonthCustomLeaveList(item.getCustomLeaveList())); + //设置用户基本信息 + if (staffRosterMap.containsKey(item.getUserId())) { + UserBoundVO staffRosterDto = staffRosterMap.get(item.getUserId()); + monthStatistics.setDeptName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + monthStatistics.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + monthStatistics.setUserName(StringUtil.isNotEmpty(staffRosterDto.getName()) ? staffRosterDto.getName() : ""); + monthStatistics.setWorkStatus(Objects.nonNull(staffRosterDto.getWorkStatusEnums()) ? staffRosterDto.getWorkStatusEnums().getCode() : null); + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(monthStatistics.getWorkStatus()); + monthStatistics.setWorkStatusStr(Objects.nonNull(workStatusEnum) ? workStatusEnum.getMsg() : ""); + } + //设置考勤组周期 + if (userCycleMap.containsKey(item.getGroupId()) && userCycleMap.get(item.getGroupId()).containsKey(item.getUserId())) { + monthStatistics.setCycleList(userCycleMap.get(item.getGroupId()).get(item.getUserId())); + StringBuilder cycleStr = new StringBuilder(); + userCycleMap.get(item.getGroupId()).get(item.getUserId()).forEach(cycle -> cycleStr.append(cycle).append(",")); + monthStatistics.setCycleStr(cycleStr.length() > 0 ? cycleStr.substring(0, cycleStr.length() - 1) : ""); + } + return monthStatistics; + }).collect(Collectors.toList()); + } + + /** + * 考勤确认月统计数据封装 + * + * @param month 月份 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param statisticsQueryVoList 日统计汇总数据 + * @return List + * @throws Exception 异常问题 + */ + private List monthConfirmStatisticsAssemble(String month, String startDate, String endDate, List statisticsQueryVoList) throws Exception { + List userIds = statisticsQueryVoList.stream().map(MonthStatisticsQueryVo::getUserId).distinct().collect(Collectors.toList()); + List groupIds = statisticsQueryVoList.stream().map(MonthStatisticsQueryVo::getGroupId).distinct().collect(Collectors.toList()); + if (CollUtil.isEmpty(statisticsQueryVoList) || CollUtil.isEmpty(userIds) || CollUtil.isEmpty(groupIds)) { + return new ArrayList<>(); + } + //批量获取用户加入考勤组周期 + DateRangeDto rangeDto = new DateRangeDto(); + rangeDto.setStartDate(startDate); + rangeDto.setEndDate(endDate); + Map>> userCycleMap = userService.batchGetUserCycleList(rangeDto, groupIds, userIds); + //批量获取用户应休天数 + List holidayBalances = rulesService.getBalanceList(month, userIds); + Map holidayBalanceMap = CollUtil.isNotEmpty(holidayBalances) ? holidayBalances.stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, AttendancePublicHolidayBalance::getTotal)) : new HashMap<>(); + //批量获取用户已休天数 + Map usePublicHoliday = dailyRuleService.getUserPublicHoliday(month, userIds); + return statisticsQueryVoList.stream().map(item -> { + MonthConfirmStatisticsListVo confirmStatisticsListVo = MonthConfirmStatisticsListVo.builder().build(); + BeanUtils.copyProperties(item, confirmStatisticsListVo); + confirmStatisticsListVo.setMonth(month); + // 设置应休|已休 + confirmStatisticsListVo.setRetired(usePublicHoliday.getOrDefault(item.getUserId(), BigDecimal.ZERO).stripTrailingZeros().toPlainString() + "|" + holidayBalanceMap.getOrDefault(item.getUserId(), BigDecimal.ZERO).stripTrailingZeros().toPlainString()); + // 计算各类请假数据 + confirmStatisticsListVo.setCustomLeaveList(DayStatisticsUtils.getMonthCustomLeaveList(item.getCustomLeaveList())); + //设置考勤组周期 + if (userCycleMap.containsKey(item.getGroupId()) && userCycleMap.get(item.getGroupId()).containsKey(item.getUserId())) { + confirmStatisticsListVo.setCycleList(userCycleMap.get(item.getGroupId()).get(item.getUserId())); + } + return confirmStatisticsListVo; + }).collect(Collectors.toList()); + } + + @Override + @SneakyThrows + public void monthDataExport(MonthStatisticsExportDto req) { + String tenantId = UserProvider.getUser().getTenantId(); + // 筛选后的用户 + req.setTenantId(tenantId); + DateRangeDto dateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + MonthStatisticsPageListDto monthStatisticsPageListDto = BeanUtil.toBean(req, MonthStatisticsPageListDto.class); + List list = customizeTableService.findList(null, 1, 2); + Assert.notEmpty(list, "导出失败,未查询到表头数据"); + ExcelExportTemplate.MoreSheetDto moreSheetDto = new ExcelExportTemplate.MoreSheetDto(); + moreSheetDto.setUserId(UserProvider.getLoginUserId()); + moreSheetDto.setFilename(req.getMonth() + "考勤数据导出"); + // 获取动态表头相关信息 + List customizeTableVos = getAttendanceCustomizeTableVos(dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), 2); + // 获取固定表头名称 + List> headerNameList = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().map(vo -> CollUtil.newArrayList(vo.getHeadName())).collect(Collectors.toList()) : new ArrayList<>(); + // 获取固定表头字段 + List headerFieldList = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().map(AttendanceCustomizeTableVo::getField).collect(Collectors.toList()) : new ArrayList<>(); + // 获取固定表头字段宽度 + List headerFieldWidthsList = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().map(AttendanceCustomizeTableVo::getWidth).collect(Collectors.toList()) : new ArrayList<>(); + // 导出字段的顺序 + LinkedHashMap dynamicHeadersMap = CollUtil.isNotEmpty(customizeTableVos) ? customizeTableVos.stream().sorted(Comparator.comparing(AttendanceCustomizeTableVo::getSort)).collect(Collectors.toMap(AttendanceCustomizeTableVo::getField, AttendanceCustomizeTableVo::getSort, (e1, e2) -> e1, LinkedHashMap::new)) : new LinkedHashMap<>(); + monthStatisticsPageListDto.setCurrentPage(1); + monthStatisticsPageListDto.setPageSize(20000); + List monthPageList = getMonthPageList(monthStatisticsPageListDto).getList(); + List> data = CollUtil.newArrayList(); + ExcelExportTemplate.MoreSheetDetailDto moreSheetDetailDto = new ExcelExportTemplate.MoreSheetDetailDto("考勤数据导出", "统计数据", MonthStatisticsPageListVo.class, headerNameList, headerFieldList, headerFieldWidthsList, 2000, (page, size) -> { + if (CollUtil.isNotEmpty(monthPageList)) { + monthPageList.forEach(monthStaVo -> { + data.add(DayStatisticsUtils.getMonthStaDataList(monthStaVo, dynamicHeadersMap)); + }); + } + return data; + }, null); + moreSheetDto.getMoreSheetDetailDto().add(moreSheetDetailDto); + DayStatisticsPageListDto dayStatisticsPageListDto = BeanUtil.toBean(req, DayStatisticsPageListDto.class); + BeanUtils.copyProperties(monthStatisticsPageListDto, dayStatisticsPageListDto); + dayStatisticsPageListDto.setStartDate(req.getMonth() + "-01"); + // 获取当月最后一天 + DateDetail dateDetail = new DateDetail(); + String lastDayOfMonth = dateDetail.getCutoffDate(req.getMonth()); + String key = "month" + UserProvider.getLoginUserId() + System.currentTimeMillis(); + dayStatisticsPageListDto.setEndDate(lastDayOfMonth); + List head1 = DayStatisticsUtils.getHead(); + ExcelExportTemplate.MoreSheetDetailDto moreSheetDetailDto1 = new ExcelExportTemplate.MoreSheetDetailDto("打卡数据导出", "打卡数据", ClockInExportVo.class, head1.stream().map(vo -> CollUtil.newArrayList(req.getMonth() + "成员打卡记录", vo)).collect(Collectors.toList()), null, DayStatisticsUtils.getWidths(), 2000, (page, size) -> { + dayStatisticsPageListDto.setCurrentPage(page); + dayStatisticsPageListDto.setPageSize(size); + dayStatisticsPageListDto.setKey(key); + return getDayClockInPageListExport(dayStatisticsPageListDto); + }, null); + moreSheetDto.getMoreSheetDetailDto().add(moreSheetDetailDto1); + excelExportTemplate.exportMoreExcelModule(moreSheetDto); + + + } + + @Override + public LatticeStatisticsVo getLatticeStatistics(LatticeStatisticsVoDto req) { + AttendanceDayStatistics dayStatistics = this.lambdaQuery().eq(AttendanceDayStatistics::getUserId, req.getUserId()).eq(AttendanceDayStatistics::getDate, req.getDay()).eq(AttendanceDayStatistics::getGroupId, req.getGroupId()).last("limit 1").one(); + LatticeStatisticsVo latticeStatisticsVo = LatticeStatisticsVo.builder().build(); + if (Objects.nonNull(dayStatistics)) { + BeanUtils.copyProperties(dayStatistics, latticeStatisticsVo); + latticeStatisticsVo.setActualAttendHours(latticeStatisticsVo.getEffectiveAttendHours()); + } + return latticeStatisticsVo; + } + + /** + * 用户加入时生成空白统计数据 + */ + @Override + @SneakyThrows + public void userJoinHandleData(String tenantId, String groupId, List userIds) { + if (StringUtil.isEmpty(groupId)) { + log.info("用户加入时生成空白统计数据,传入的考勤组id为空"); + return; + } + if (CollUtil.isEmpty(userIds)) { + log.info("用户加入时生成空白统计数据,传入的用户id为空"); + return; + } + DateTime dateTime = DateUtil.beginOfDay(new Date()); + CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS).execute(() -> { + try { + TenantDataSourceUtil.switchTenant(tenantId); + dayStatisticsUtil.handleNailData(groupId, userIds, dateTime); + } catch (Exception e) { + log.error("用户加入时生成空白统计数据异常", e); + } + }); + } + + @Override + @SneakyThrows + public void userJoinHandleData(String tenantId, Date date, String groupId, List userIds) { + if (StringUtil.isEmpty(groupId)) { + return; + } + if (CollUtil.isEmpty(userIds)) { + return; + } + CompletableFuture.delayedExecutor(3, TimeUnit.SECONDS).execute(() -> { + try { + TenantDataSourceUtil.switchTenant(tenantId); + Date today = new Date(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(today); // 修复变量名错误 + Date currentDate; + while (true) { + currentDate = calendar.getTime(); + if (currentDate.after(today) || currentDate.equals(today)) { + break; + } + dayStatisticsUtil.handleNailData(groupId, userIds, DateUtil.beginOfDay(currentDate)); + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + } catch (Exception e) { + log.error("用户加入时生成空白统计数据异常", e); + } + }); + } + + @Override + public Boolean handleDataForJob(String tenantId) { + Date day = DateUtil.beginOfDay(new Date()); + //已存在数据的考勤组人员不生成空白统计数据 + List list = lambdaQuery().eq(AttendanceDayStatistics::getDate, day).list(); + Map> userIdByGroupIdMap = list.stream().collect(Collectors.groupingBy(AttendanceDayStatistics::getGroupId, Collectors.mapping(AttendanceDayStatistics::getUserId, Collectors.toList()))); + List attendanceGroupUsers = attendanceUserService.queryAll(); + Map> collect = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + collect.forEach((groupId, userIds) -> { + List createUserId = Lists.newArrayList(); + Map> userCollect = userIds.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + userCollect.forEach((userId, userList) -> { + if (dailyRuleService1.isExistStatus(userList, day) > 0) { + createUserId.add(userId); + } + }); + createUserId.removeAll(userIdByGroupIdMap.getOrDefault(groupId, CollUtil.newArrayList())); + if (CollUtil.isNotEmpty(createUserId)) { + dayStatisticsUtil.handleNailData(groupId, createUserId, day); + } + }); + return Boolean.TRUE; + } + + @SneakyThrows + @Override + public void batchStatisticDataClear(StatisticsBatchClearDto courseEventDTO) { + TenantDataSourceUtil.switchTenant(courseEventDTO.getTenantId()); + if (Objects.nonNull(courseEventDTO.getStartDay())) { + lambdaUpdate().eq(AttendanceDayStatistics::getGroupId, courseEventDTO.getGroupId()) + .in(AttendanceDayStatistics::getUserId, courseEventDTO.getUserIdList()) + .ge(AttendanceDayStatistics::getDate, DateUtil.date(courseEventDTO.getStartDay())) + .remove(); + return; + } + lambdaUpdate().eq(AttendanceDayStatistics::getGroupId, courseEventDTO.getGroupId()) + .in(AttendanceDayStatistics::getUserId, courseEventDTO.getUserIdList()) + .eq(AttendanceDayStatistics::getDate, DateUtil.date(courseEventDTO.getDay())) + .remove(); + } + + // 该方法只会执行一次,用于处理历史数据 + @Override + @Transactional + public void regenerateDayData(String tenantId, Date start) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + // 从开始排班的时间开始 + Date startDay = dailyRuleService.getEarliestSchedulingDate(start); + LocalDate startDate = startDay.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate currentDate = LocalDate.now(); + // 限制最多10个并发线程 + final Semaphore semaphore = new Semaphore(5); + List> allFutures = new ArrayList<>(); + // 按月处理数据 + while (!startDate.isAfter(currentDate)) { + // 计算当前月的最后一天 + LocalDate endDate = startDate.with(TemporalAdjusters.lastDayOfMonth()); + // 如果当前月的最后一天超过今天,则结束日期为今天 + if (!endDate.isBefore(currentDate)) { + endDate = currentDate; + } + // 创建处理当前月份数据的任务 + LocalDate finalStartDate = startDate; + LocalDate finalEndDate = endDate; + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + semaphore.acquire(); + try { + TenantDataSourceUtil.switchTenant(tenantId); + // 调用处理方法处理当前月份的数据 + List createDataList = dailyRuleService.getDayRuleByMonth(finalStartDate, finalEndDate); + // 事务提交后执行后续操作 + if (CollUtil.isNotEmpty(createDataList)) { + this.handlePostTransactionOperations(tenantId, createDataList); + } + } finally { + semaphore.release(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("线程因获取信号量被中断", e); + } catch (Exception e) { + log.error("处理历史数据异常,时间范围: {} 至 {}", finalStartDate, finalEndDate, e); + } + }, threadPoolExecutor); + allFutures.add(future); + // 移动到下一个月 + startDate = startDate.plusMonths(1).withDayOfMonth(1); + } + // 等待所有任务完成 + if (!allFutures.isEmpty()) { + CompletableFuture allDone = CompletableFuture.allOf(allFutures.toArray(new CompletableFuture[0])); + try { + allDone.join(); + } catch (Exception e) { + log.error("等待所有任务完成时发生异常", e); + } + } + } catch (Exception e) { + log.error("重新生成日统计数据异常", e); + } + } + + // 该方法只会执行一次,用于处理历史数据 + @Override + public void processHistoricalData(String tenantId, Date start, Date end) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + // 从开始排班的时间开始 + Date startDay = dailyRuleService.getEarliestSchedulingDate(start); + LocalDate startDate = startDay.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate endDateParam = end.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + // 限制最多5个并发线程 + final Semaphore semaphore = new Semaphore(5); + List> allFutures = new ArrayList<>(); + // 按月处理数据 + while (startDate.isBefore(endDateParam) || startDate.isEqual(endDateParam)) { + // 计算当前月的最后一天 + LocalDate endDate = startDate.with(TemporalAdjusters.lastDayOfMonth()); + // 如果当前月的最后一天超过指定结束日期,则结束日期为指定日期 + if (endDate.isAfter(endDateParam)) { + endDate = endDateParam; + } + // 创建处理当前月份数据的任务 + LocalDate finalStartDate = startDate; + LocalDate finalEndDate = endDate; + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + semaphore.acquire(); + try { + TenantDataSourceUtil.switchTenant(tenantId); + // 生成指定范围内的所有日期集合 + List dateRange = new ArrayList<>(); + LocalDate currentDate = finalStartDate; + while (currentDate.isBefore(finalEndDate) || currentDate.isEqual(finalEndDate)) { + dateRange.add(currentDate); + currentDate = currentDate.plusDays(1); + } + // 调用处理方法处理当前月份的数据 + List createDataList = dailyRuleService.getDayRuleByMonth(finalStartDate, finalEndDate); + // **关键修改:即使没有排班数据,也要调用统计接口** + this.handlePostTransactionOperations1(tenantId, createDataList, dateRange); + } finally { + semaphore.release(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("线程因获取信号量被中断", e); + } catch (Exception e) { + log.error("处理历史数据异常,时间范围: {} 至 {}", finalStartDate, finalEndDate, e); + } + }, threadPoolExecutor); + allFutures.add(future); + // 移动到下一个月 + startDate = startDate.plusMonths(1).withDayOfMonth(1); + } + // 等待所有任务完成 + if (!allFutures.isEmpty()) { + CompletableFuture allDone = CompletableFuture.allOf(allFutures.toArray(new CompletableFuture[0])); + try { + allDone.join(); + } catch (Exception e) { + log.error("等待所有任务完成时发生异常", e); + } + } + } catch (Exception e) { + log.error("重新生成日统计数据异常", e); + } + } + + private void handlePostTransactionOperations(String tenantId, List createDayStatisticsList) { + if (CollUtil.isEmpty(createDayStatisticsList)) { + return; + } + try { + Map>> careateDayMap = createDayStatisticsList.stream().collect(Collectors.groupingBy(CreateDayStatistics::getGroupId, Collectors.groupingBy(CreateDayStatistics::getDay))); + careateDayMap.forEach((groupId, dayListMap) -> { + dayListMap.forEach((date, createList) -> { + //调用生成日统计数据接口 + StatisticsSingleDto courseEventDTO = StatisticsSingleDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .day(date) + .triggerSceneEnum(TriggerSceneEnum.MANUAL_TRIGGER) + .build(); + List userIds = createList.stream().map(CreateDayStatistics::getUserId).distinct().collect(Collectors.toList()); + userIds.forEach(item -> { + courseEventDTO.setUserId(item); + if (StrUtil.isBlank(tenantId) || StrUtil.isBlank(groupId)) { + log.error("AttendanceDayStatisticsServiceImpl#handlePostTransactionOperations 触发日统计租户ID或者考勤组ID为空:{}", JSON.toJSONString(courseEventDTO)); + } + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_TOPIC, message, 3000L, 2); + }); + }); + }); + } catch (Exception e) { + log.error("事务提交后处理操作异常", e); + } + } + + private void handlePostTransactionOperations1(String tenantId, List createDayStatisticsList, List dateRange) { + if (CollUtil.isEmpty(createDayStatisticsList)) { + return; + } + try { + Map> groupMap = createDayStatisticsList.stream() + .collect(Collectors.groupingBy(CreateDayStatistics::getGroupId)); + groupMap.forEach((groupId, createList) -> { + // 按dateRange日期顺序遍历 + for (LocalDate date : dateRange) { + Date currentDate = Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant()); + // 过滤出当前日期的数据 + List currentDayList = createList.stream() + .filter(item -> DateUtils.isSameDay(item.getDay(), currentDate)) + .collect(Collectors.toList()); + // 调用生成日统计数据接口 + StatisticsSingleHistoryDto courseEventDTO = StatisticsSingleHistoryDto.builder() + .tenantId(tenantId) + .groupId(groupId) + .day(currentDate) + .triggerSceneEnum(TriggerSceneEnum.MANUAL_TRIGGER) + .build(); + // 获取当前日期的用户ID列表 + List userIds = currentDayList.stream() + .map(CreateDayStatistics::getUserId) + .distinct() + .collect(Collectors.toList()); + // 发送消息 + userIds.forEach(item -> { + courseEventDTO.setUserId(item); + Message message = MessageBuilder.withPayload(courseEventDTO).build(); + rocketMqTemplate.syncSend(MessageTopicConstants.ATTENDANCE_STATISTICS_SINGLE_HISTORY_TOPIC, message, 3000L, 1); + }); + } + }); + } catch (Exception e) { + log.error("事务提交后处理操作异常", e); + } + } + + @Override + @DSTransactional + public void statisticDataChange(StatisticsSingleDto singleDto) throws LoginException { + // 执行业务(异常直接向上抛出) + this.dayStatisticsUtil.createDayStatisticsData(singleDto); + } + + @Override + @DSTransactional + public void statisticDataChangeHistory(StatisticsSingleHistoryDto singleDto) throws LoginException { + // 执行业务(异常直接向上抛出) + this.dayStatisticsUtil.createDayStatisticsDataHistory(singleDto); + } + + + @Override + @SneakyThrows + public Boolean dayStatisticsNotice(String tenantId) { + Integer globalNoticeSetting = getGlobalNoticeSetting(ATTENDANCE_GROUP_DAILY_REPORT_SEND); + String startTime = null; + String endTime = null; + if (Objects.equals(globalNoticeSetting, 1)) { + Date date = jnpf.util.DateUtil.localDateTimeToDate(LocalDateTime.now().minusHours(1)); + startTime = DateDetail.getDate2Str(date, DateDetail.DF2); + endTime = DateDetail.getDate2Str(date, DateDetail.DF19); + } else if (Objects.equals(globalNoticeSetting, 2)) { + Date date = new Date(); + startTime = DateDetail.getDate2Str(date, DateDetail.DF2); + endTime = DateDetail.getDate2Str(date, DateDetail.DF19); + } else if (Objects.equals(globalNoticeSetting, 3)) { + Date date = jnpf.util.DateUtil.localDateTimeToDate(LocalDateTime.now().minusDays(1)); + DateTime dateTime = DateUtil.beginOfDay(date); + DateTime dateTime1 = DateUtil.beginOfMinute(date); + if (dateTime.compareTo(dateTime1) != 0) { + return Boolean.TRUE; + } + startTime = DateDetail.getDateTime2Str(dateTime); + DateTime dateTime2 = DateUtil.offsetDay(dateTime, 1); + endTime = DateDetail.getDateTime2Str(dateTime2); + } + List dayStatisticsList = this.baseMapper.getDayStatisticsNotice(startTime, endTime); + if (CollUtil.isEmpty(dayStatisticsList)) { + log.info("当前时间:{} 个人日度统计提醒:未查询到统计数据", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + Map dayNoticeModelMap = getDayNoticeModelMap(dayStatisticsList); + //获取考勤组信息 + List groupIdList = dayStatisticsList.stream().map(DayStatisticsNoticeQueryVo::getGroupId).distinct().collect(Collectors.toList()); + List groupList = this.groupService.queryListByIds(groupIdList); + if (CollUtil.isEmpty(groupList)) { + log.info("当前时间:{} 个人日度统计提醒:未查询到考勤组信息", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + Map groupInfoMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + Map> userDayStatisticsMap = dayStatisticsList.stream().collect(Collectors.groupingBy(DayStatisticsNoticeQueryVo::getUserId)); + for (Map.Entry> userEntry : userDayStatisticsMap.entrySet()) { + Date date = userEntry.getValue().get(0).getDate(); + Map> groupDayStatisticsMap = userEntry.getValue().stream().collect(Collectors.groupingBy(DayStatisticsNoticeQueryVo::getGroupId)); + List dayNoticeModelList = Lists.newArrayList(); + for (Map.Entry> groupEntry : groupDayStatisticsMap.entrySet()) { + if (!groupInfoMap.containsKey(groupEntry.getKey())) { + continue; + } + for (DayStatisticsNoticeQueryVo dayStatistics : groupEntry.getValue()) { + if (!dayNoticeModelMap.containsKey(dayStatistics.getId())) { + continue; + } + DayNoticeModel dayNoticeModel = dayNoticeModelMap.get(dayStatistics.getId()); + dayNoticeModel.setGroupName(groupInfoMap.get(groupEntry.getKey()).getGroupName()); + dayNoticeModelList.add(dayNoticeModel); + } + } + SettlementNoticeModel settlementNoticeModel = new SettlementNoticeModel(); + settlementNoticeModel.setTitle(jnpf.util.DateUtil.dateToString(date, "yyyy年MM月dd日") + "考勤日报"); + settlementNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + settlementNoticeModel.setUserId(userEntry.getKey()); + settlementNoticeModel.setDayNoticeModelList(dayNoticeModelList); + settlementNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.INDIVIDUAL_DAILY_STATISTICS); + noticeHandler.send(settlementNoticeModel); + } + return Boolean.TRUE; + } + + /** + * 获取用户通知配置信息 + * + * @param code 配置编码 + * @return Integer + */ + @Nullable + private Integer getGlobalNoticeSetting(String code) { + UserSettingVo userSettingVo = userSettingService.checkWebSetting(code); + if (Objects.equals(userSettingVo.getStatus(), 0)) { + return null; + } + return userSettingVo.getValue(); + } + + @Override + @SneakyThrows + public Boolean monthStatisticsNotice(String tenantId) { + Integer globalNoticeSetting = getGlobalNoticeSetting(ATTENDANCE_GROUP_MONTHLY_REPORT_SEND); + boolean isAbsent = Boolean.FALSE; + DateTime now = DateUtil.beginOfMinute(new Date()); + // 获取当前日期 + LocalDate today = LocalDate.now(); + // 获取上个月的第一天 + LocalDate firstDayOfLastMonth = today.minusMonths(1).withDayOfMonth(1); + // 获取上个月的最后一天 + LocalDate lastDayOfLastMonth = today.minusMonths(1).with(TemporalAdjusters.lastDayOfMonth()); + Date date = DateUtil.date(lastDayOfLastMonth.atStartOfDay(ZoneId.systemDefault()).toInstant()); + if (Objects.equals(globalNoticeSetting, 1)) { + DateTime dateTime = DateUtil.beginOfMonth(now); + dateTime = dateTime.setField(DateField.HOUR_OF_DAY, 12); + if (now.compareTo(dateTime) != 0) { + return Boolean.TRUE; + } + } else if (Objects.equals(globalNoticeSetting, 2)) { + DateTime dateTime = DateUtil.beginOfMonth(now); + dateTime = dateTime.setField(DateField.HOUR_OF_DAY, 0); + if (now.compareTo(dateTime) != 0) { + return Boolean.TRUE; + } + } else if (Objects.equals(globalNoticeSetting, 3)) { + isAbsent = Boolean.TRUE; + String nowDay = DateDetail.getDate2Str(now, DateDetail.DF); + DateTime dateTime = DateUtil.endOfMonth(now); + String endDay = DateDetail.getDate2Str(dateTime, DateDetail.DF); + dateTime = DateUtil.beginOfMonth(now); + String beginDay = DateDetail.getDate2Str(dateTime, DateDetail.DF); + if (!StringUtil.equals(nowDay, endDay) && !StringUtil.equals(beginDay, nowDay)) { + return Boolean.TRUE; + } + if (StringUtil.equals(nowDay, endDay)) { + firstDayOfLastMonth = today.withDayOfMonth(1); + lastDayOfLastMonth = today.with(TemporalAdjusters.lastDayOfMonth()); + } + } + + List monthStatisticsList = this.baseMapper.getMonthStatisticsNotice(firstDayOfLastMonth, lastDayOfLastMonth); + if (CollUtil.isEmpty(monthStatisticsList)) { + log.info("当前时间:{} 个人月度统计提醒:未查询到统计数据", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + String month = lastDayOfLastMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")); + Map monthNoticeModelMap = getMonthNoticeModelMap(month, monthStatisticsList); + //获取考勤组信息 + List groupIdList = monthStatisticsList.stream().map(MonthStatisticsNoticeQueryVo::getGroupId).distinct().collect(Collectors.toList()); + List groupList = this.groupService.queryListByIds(groupIdList); + if (CollUtil.isEmpty(groupList)) { + log.info("当前时间:{} 个人月度统计提醒:未查询到考勤组信息", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + Map groupInfoMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + Map> userMonthStatisticsMap = monthStatisticsList.stream().collect(Collectors.groupingBy(MonthStatisticsNoticeQueryVo::getUserId)); + for (Map.Entry> userEntry : userMonthStatisticsMap.entrySet()) { + Map> groupDayStatisticsMap = userEntry.getValue().stream().collect(Collectors.groupingBy(MonthStatisticsNoticeQueryVo::getGroupId)); + List monthNoticeModelList = Lists.newArrayList(); + for (Map.Entry> groupEntry : groupDayStatisticsMap.entrySet()) { + if (!groupInfoMap.containsKey(groupEntry.getKey())) { + continue; + } + List value = groupEntry.getValue(); + for (MonthStatisticsNoticeQueryVo monthStatistics : value) { + if (!monthNoticeModelMap.containsKey(monthStatistics.getId())) { + continue; + } + boolean isEndOfDay = date.compareTo(DateUtil.endOfDay(monthStatistics.getRemindTime())) == 0; + if (isAbsent && (now.compareTo(monthStatistics.getRemindTime()) != 0 && !isEndOfDay && now.compareTo(DateUtil.beginOfDay(now)) != 0)) { + continue; + } + MonthNoticeModel monthNoticeModel = monthNoticeModelMap.get(monthStatistics.getId()); + monthNoticeModel.setGroupName(groupInfoMap.get(groupEntry.getKey()).getGroupName()); + monthNoticeModel.setEndDate(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + monthNoticeModelList.add(monthNoticeModel); + } + } + if (CollUtil.isEmpty(monthNoticeModelList)) { + continue; + } + SettlementNoticeModel settlementNoticeModel = new SettlementNoticeModel(); + settlementNoticeModel.setTitle(lastDayOfLastMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月")) + "个人统计月报"); + settlementNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + settlementNoticeModel.setUserId(userEntry.getKey()); + settlementNoticeModel.setMonthNoticeModelList(monthNoticeModelList); + settlementNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.INDIVIDUAL_MONTHLY_STATISTICS); + noticeHandler.send(settlementNoticeModel); + } + return Boolean.TRUE; + } + + @Override + @SneakyThrows + public void consentUnscheduledNotice(String tenantId) { + Integer globalNoticeSetting = getGlobalNoticeSetting(UN_SCHEDULED_REMIND_ADMIN); + DateTime now = DateUtil.beginOfDay(new Date()); + List collect = lambdaQuery().select(AttendanceDayStatistics::getDate).lt(AttendanceDayStatistics::getDate, now).eq(AttendanceDayStatistics::getIsScheduling, Boolean.FALSE).groupBy(AttendanceDayStatistics::getDate).orderByDesc(AttendanceDayStatistics::getDate).last("limit 30").list().stream().map(AttendanceDayStatistics::getDate).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)) { + return; + } + List list = lambdaQuery().select(AttendanceDayStatistics::getUserId, AttendanceDayStatistics::getGroupId, AttendanceDayStatistics::getDate).eq(AttendanceDayStatistics::getIsScheduling, Boolean.FALSE).in(AttendanceDayStatistics::getDate, collect).list(); + if (CollUtil.isEmpty(list)) { + return; + } + int offsetDays = Optional.ofNullable(globalNoticeSetting).orElse(0); + List datesByPeriod = DateDetail.getDatesByPeriod(DateUtil.offsetDay(new Date(), -offsetDays), new Date()); + List collect3 = datesByPeriod.stream().map(DateDetail::getDateTime2Str).collect(Collectors.toList()); + Map> collectGroup = list.stream().collect(Collectors.groupingBy(AttendanceDayStatistics::getGroupId)); + List userIdList = list.stream().map(AttendanceDayStatistics::getUserId).distinct().collect(Collectors.toList()); + List groupIdList = list.stream().map(AttendanceDayStatistics::getGroupId).distinct().collect(Collectors.toList()); + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date startOfMinuteRange = calendar.getTime(); + calendar.set(Calendar.SECOND, 59); + Date endOfMinuteRange = calendar.getTime(); + List attendanceGroupUserVos = attendanceUserService.getAttendanceGroupUsersOfSecondment(startOfMinuteRange, endOfMinuteRange, userIdList, groupIdList); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return; + } + //判断当前推送时间节点用户是否离组或者离职(离职人员不推送) + Map userMap = dailyRuleService1.findUserIsExistsByUserList(attendanceGroupUserVos, startOfMinuteRange, endOfMinuteRange); + List removeUserList = Optional.ofNullable(userMap).orElse(Collections.emptyMap()).entrySet().stream().filter(entry -> entry.getValue().equals(0)).map(Map.Entry::getKey).collect(Collectors.toList()); + List userIds = CollUtil.newArrayList(); + ConsecUnscheduledNoticeModel consecUnscheduledNoticeModel = new ConsecUnscheduledNoticeModel(); + collectGroup.forEach((groupId, userStatistics) -> { + Map> collect1 = userStatistics.stream().collect(Collectors.groupingBy(AttendanceDayStatistics::getUserId)); + userIds.clear(); + collect1.forEach((userId, sta) -> { + List collect2 = sta.stream().sorted(Comparator.comparing(AttendanceDayStatistics::getDate).reversed()).map(AttendanceDayStatistics::getDate).filter(date -> collect3.contains(DateDetail.getDateTime2Str(date))).collect(Collectors.toList()); + if (collect2.size() < Optional.ofNullable(globalNoticeSetting).orElse(0)) { + return; + } + userIds.add(userId); + }); + if (CollUtil.isEmpty(userIds)) { + return; + } + //排除掉已离组的人员 + if (CollUtil.isNotEmpty(removeUserList) && CollUtil.containsAny(userIds, removeUserList)) { + userIds.removeIf(removeUserList::contains); + } + consecUnscheduledNoticeModel.setUserIds(userIds); + consecUnscheduledNoticeModel.setGroupId(groupId); + consecUnscheduledNoticeModel.setTenantId(tenantId); + consecUnscheduledNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.CONSEC_UNSCHEDULED); + noticeHandler.send(consecUnscheduledNoticeModel); + }); + } + + @Override + @SneakyThrows + public Boolean teamMonthStatisticsNotice(String tenantId) { + Integer globalNoticeSetting = getGlobalNoticeSetting(ATTENDANCE_TEAM_MONTHLY_REPORT_SEND); + boolean isAbsent = Boolean.FALSE; + DateTime now = DateUtil.beginOfMinute(new Date()); + // 获取当前日期 + LocalDate today = LocalDate.now(); + LocalDateTime currentTime = LocalDateTime.now(); + // 获取上个月的第一天 + LocalDate firstDayOfLastMonth = today.minusMonths(1).withDayOfMonth(1); + // 获取上个月的最后一天 + LocalDate lastDayOfLastMonth = today.minusMonths(1).with(TemporalAdjusters.lastDayOfMonth()); + Date date = DateUtil.date(lastDayOfLastMonth.atStartOfDay(ZoneId.systemDefault()).toInstant()); + if (Objects.equals(globalNoticeSetting, 1)) { + DateTime dateTime = DateUtil.beginOfMonth(now); + dateTime = dateTime.setField(DateField.HOUR_OF_DAY, 12); + if (now.compareTo(dateTime) != 0) { + return Boolean.TRUE; + } + } else if (Objects.equals(globalNoticeSetting, 2)) { + DateTime dateTime = DateUtil.beginOfMonth(now); + dateTime = dateTime.setField(DateField.HOUR_OF_DAY, 0); + if (now.compareTo(dateTime) != 0) { + return Boolean.TRUE; + } + } else if (Objects.equals(globalNoticeSetting, 3)) { + isAbsent = Boolean.TRUE; + String nowDay = DateDetail.getDate2Str(now, DateDetail.DF); + DateTime dateTime = DateUtil.endOfMonth(now); + String endDay = DateDetail.getDate2Str(dateTime, DateDetail.DF); + dateTime = DateUtil.beginOfMonth(now); + String beginDay = DateDetail.getDate2Str(dateTime, DateDetail.DF); + if (!StringUtil.equals(nowDay, endDay) && !StringUtil.equals(beginDay, nowDay)) { + return Boolean.TRUE; + } + if (StringUtil.equals(nowDay, endDay)) { + firstDayOfLastMonth = today.withDayOfMonth(1); + lastDayOfLastMonth = today.with(TemporalAdjusters.lastDayOfMonth()); + } + } + List allTeamMonthStatisticsList = this.baseMapper.getStemMonthStatisticsNotice(firstDayOfLastMonth, lastDayOfLastMonth); + if (CollUtil.isEmpty(allTeamMonthStatisticsList)) { + log.info("当前时间:{} 团队考勤统计提醒:未查询到统计数据", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + Map teamMonthNoticeModelMap = DayStatisticsUtils.getTeamMonthNoticeModelMap(allTeamMonthStatisticsList); + List teamMonthStatisticsList = getObtainPermissionGroupList(allTeamMonthStatisticsList); + if (CollUtil.isEmpty(teamMonthStatisticsList)) { + log.info("当前时间:{} 团队考勤统计提醒:所有考勤组都未开启团队考勤统计提醒", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + //获取考勤组信息 + List groupIdList = teamMonthStatisticsList.stream().map(TeamMonthStatisticsNoticeQueryVo::getGroupId).distinct().collect(Collectors.toList()); + List groupList = this.groupService.queryListByIds(groupIdList); + if (CollUtil.isEmpty(groupList)) { + log.info("当前时间:{} 团队考勤统计提醒:未查询到考勤组信息", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + return Boolean.TRUE; + } + Map groupInfoMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + //发送给超级管理员 + List superAdminList = superAdminService.querySuperAdmin(null); + List superAdminUserList = CollUtil.isNotEmpty(superAdminList) ? superAdminList.stream().map(AttendanceUserVo::getId).collect(Collectors.toList()) : Lists.newArrayList(); + SettlementNoticeModel settlementNoticeModel = new SettlementNoticeModel(); + settlementNoticeModel.setTitle(lastDayOfLastMonth.format(DateTimeFormatter.ofPattern("yyyy年MM月")) + "所管理考勤组团队统计月报"); + settlementNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + settlementNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.TEAM_MONTHLY_STATISTICS); + if (CollUtil.isNotEmpty(superAdminList)) { + List teamMonthNoticeModelList = Lists.newArrayList(); + for (TeamMonthStatisticsNoticeQueryVo teamMonthStatistics : teamMonthStatisticsList) { + if (!groupInfoMap.containsKey(teamMonthStatistics.getGroupId())) { + continue; + } + boolean isEndOfDay = date.compareTo(DateUtil.endOfDay(teamMonthStatistics.getRemindTime())) == 0; + if (isAbsent && (now.compareTo(teamMonthStatistics.getRemindTime()) != 0 && !isEndOfDay && now.compareTo(DateUtil.beginOfDay(now)) != 0)) { + continue; + } + if (!teamMonthNoticeModelMap.containsKey(teamMonthStatistics.getGroupId()) || !groupInfoMap.containsKey(teamMonthStatistics.getGroupId())) { + continue; + } + TeamMonthNoticeModel teamMonthNoticeModel = teamMonthNoticeModelMap.get(teamMonthStatistics.getGroupId()); + teamMonthNoticeModel.setGroupName(groupInfoMap.get(teamMonthStatistics.getGroupId()).getGroupName()); + teamMonthNoticeModel.setEndDate(currentTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + teamMonthNoticeModelList.add(teamMonthNoticeModel); + } + if (CollUtil.isEmpty(teamMonthNoticeModelList)) { + return Boolean.TRUE; + } + settlementNoticeModel.setUserIds(superAdminUserList); + settlementNoticeModel.setTeamMonthNoticeModelList(teamMonthNoticeModelList); + noticeHandler.send(settlementNoticeModel); + } + //推送给管理员 + Map> userGroupMap = this.groupService.userManagerGroupInfo(); + for (Map.Entry> userEntry : userGroupMap.entrySet()) { + List teamMonthNoticeModelList = Lists.newArrayList(); + List teamMonthFilter = teamMonthStatisticsList.stream().filter(item -> userEntry.getValue().contains(item.getGroupId())).collect(Collectors.toList()); + if (CollUtil.isEmpty(teamMonthFilter)) { + continue; + } + for (TeamMonthStatisticsNoticeQueryVo teamMonthStatistics : teamMonthFilter) { + if (!groupInfoMap.containsKey(teamMonthStatistics.getGroupId())) { + continue; + } + TeamMonthNoticeModel teamMonthNoticeModel = teamMonthNoticeModelMap.get(teamMonthStatistics.getGroupId()); + teamMonthNoticeModel.setGroupName(groupInfoMap.get(teamMonthStatistics.getGroupId()).getGroupName()); + teamMonthNoticeModel.setEndDate(currentTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日HH:mm"))); + teamMonthNoticeModelList.add(teamMonthNoticeModel); + } + settlementNoticeModel.setUserIds(List.of(userEntry.getKey())); + settlementNoticeModel.setTeamMonthNoticeModelList(teamMonthNoticeModelList); + noticeHandler.send(settlementNoticeModel); + } + return Boolean.TRUE; + } + + @Override + public List countAttendanceAvgHours(AttendanceCountAvgHoursDto dto) { + List monthList = DayStatisticsUtils.generateMonthList(dto.getStartMonth(), dto.getEndMonth()); + //获取组织下所有考勤组 + List groupList = this.groupService.getGroupListByOrgIds(dto.getOrgIds().stream().distinct().collect(Collectors.toList())); + List countAvgHoursVoList = CollUtil.newArrayList(); + if (CollUtil.isEmpty(groupList) && CollUtil.isNotEmpty(monthList)) { + countAvgHoursVoList.addAll(monthList.stream().map(item -> AttendanceCountAvgHoursVo.builder().month(item).avgHours(BigDecimal.ZERO).build()).collect(Collectors.toList())); + return countAvgHoursVoList; + } + List groupIds = groupList.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList()); + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM", Locale.ENGLISH); + YearMonth yearStartMonth = YearMonth.parse(dto.getStartMonth(), monthFormatter); + LocalDate startDate = yearStartMonth.atDay(1); + YearMonth yearEndMonth = YearMonth.parse(dto.getEndMonth(), monthFormatter); + LocalDate endDate = yearEndMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearEndMonth.atEndOfMonth(); + //获取考勤组每月的平均工时 + countAvgHoursVoList = this.baseMapper.getAvgHours(groupIds, startDate, endDate); + Map monthlyHoursVoMap = countAvgHoursVoList.stream().collect(Collectors.toMap(AttendanceCountAvgHoursVo::getMonth, a -> a, (k1, k2) -> k1)); + return monthList.stream().map(item -> AttendanceCountAvgHoursVo.builder().month(item).avgHours(CollUtil.isEmpty(monthlyHoursVoMap) || !monthlyHoursVoMap.containsKey(item) ? BigDecimal.ZERO : monthlyHoursVoMap.get(item).getAvgHours()).build()).collect(Collectors.toList()); + } + + @Override + public MonthStatsDetailsVo getAttendanceAvgHoursDetails(MonthStatsDetailsDto dto) { + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + MonthStatsDetailsQueryVo monthStatsDetailsQueryVo = this.baseMapper.getAttendanceAvgHoursDetails(dto.getGroupIds(), startDate, endDate); + if (Objects.isNull(monthStatsDetailsQueryVo)) { + return null; + } + MonthStatsDetailsVo monthStatsDetailsVo = MonthStatsDetailsVo.builder().avgHours(monthStatsDetailsQueryVo.getAvgHours()).avgOverTimeHours(monthStatsDetailsQueryVo.getAvgOverTimeHours()).avgLeaveDays(monthStatsDetailsQueryVo.getAvgLeaveDays()).avgAbsentDays(monthStatsDetailsQueryVo.getAvgAbsentDays()).lateCount(monthStatsDetailsQueryVo.getLateCount()).earlyCount(monthStatsDetailsQueryVo.getEarlyCount()).build(); + // 获取同比数据 + YearMonth onYear = yearMonth.minusYears(1); + LocalDate onYearStartDate = onYear.atDay(1); + LocalDate onYearEndDate = onYear.atEndOfMonth(); + MonthStatsDetailsQueryVo onYearQueryVo = this.baseMapper.getAttendanceAvgHoursDetails(dto.getGroupIds(), onYearStartDate, onYearEndDate); + monthStatsDetailsVo.setAvgHoursOnYear(onYearQueryVo.getAvgHours().compareTo(BigDecimal.ZERO) > 0 ? monthStatsDetailsVo.getAvgHours().subtract(onYearQueryVo.getAvgHours()).multiply(new BigDecimal(100)).divide(onYearQueryVo.getAvgHours(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + monthStatsDetailsVo.setAvgLeaveDaysOnYear(onYearQueryVo.getAvgLeaveDays().compareTo(BigDecimal.ZERO) > 0 ? monthStatsDetailsVo.getAvgLeaveDays().subtract(onYearQueryVo.getAvgLeaveDays()).multiply(new BigDecimal(100)).divide(onYearQueryVo.getAvgLeaveDays(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + monthStatsDetailsVo.setLateCountOnYear(onYearQueryVo.getLateCount().compareTo(BigDecimal.ZERO) > 0 ? monthStatsDetailsVo.getLateCount().subtract(onYearQueryVo.getLateCount()).multiply(new BigDecimal(100)).divide(onYearQueryVo.getLateCount(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + // 获取环比数据 + YearMonth onMonth = yearMonth.minusMonths(1); + LocalDate onMonthStartDate = onMonth.atDay(1); + LocalDate onMonthEndDate = onMonth.atEndOfMonth(); + MonthStatsDetailsQueryVo onMonthQueryVo = this.baseMapper.getAttendanceAvgHoursDetails(dto.getGroupIds(), onMonthStartDate, onMonthEndDate); + monthStatsDetailsVo.setAvgHoursOnMonth(onMonthQueryVo.getAvgHours().compareTo(BigDecimal.ZERO) > 0 ? monthStatsDetailsVo.getAvgHours().subtract(onMonthQueryVo.getAvgHours()).multiply(new BigDecimal(100)).divide(onMonthQueryVo.getAvgHours(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + monthStatsDetailsVo.setAvgLeaveDaysMonth(onMonthQueryVo.getAvgLeaveDays().compareTo(BigDecimal.ZERO) > 0 ? monthStatsDetailsVo.getAvgLeaveDays().subtract(onMonthQueryVo.getAvgLeaveDays()).multiply(new BigDecimal(100)).divide(onMonthQueryVo.getAvgLeaveDays(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + monthStatsDetailsVo.setLateCountOnMonth(onMonthQueryVo.getLateCount().compareTo(BigDecimal.ZERO) > 0 ? monthStatsDetailsVo.getLateCount().subtract(onMonthQueryVo.getLateCount()).multiply(new BigDecimal(100)).divide(onMonthQueryVo.getLateCount(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + return monthStatsDetailsVo; + } + + @Override + public List getAttendanceMonthPerCapita(MonthStatsDetailsDto dto) { + //获取月份日期列表 + List dayList = DayStatisticsUtils.getDatesForMonth(dto.getMonth(), "yyyyMM"); + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("MM.dd", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + List perCapitaQueryVoList = this.baseMapper.getAttendanceMonthPerCapita(dto.getGroupIds(), startDate, endDate); + Map perCapitaMap = perCapitaQueryVoList.stream().collect(Collectors.toMap(MonthStatsPerCapitaQueryVo::getDay, item -> item)); + if (CollUtil.isEmpty(perCapitaQueryVoList)) { + return dayList.stream().map(item -> MonthStatsPerCapitaVo.builder().day(item.format(dayFormatter)).avgHours(BigDecimal.ZERO).build()).collect(Collectors.toList()); + } + return dayList.stream().map(item -> MonthStatsPerCapitaVo.builder().day(item.format(dayFormatter)).avgHours(perCapitaMap.containsKey(item) ? perCapitaMap.get(item).getAvgHours() : BigDecimal.ZERO).build()).collect(Collectors.toList()); + } + + @Override + public List getAttendanceDailySituation(MonthStatsDetailsDto dto) { + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + MonthStatsDailySituationQueryVo dailySituation = this.baseMapper.getAttendanceDailySituation(dto.getGroupIds(), startDate, endDate); + return DayStatisticsUtils.createDailySituationDirectory(dailySituation); + } + + @Override + public List getAttendanceHoursRanking(MonthStatsDetailsDto dto) { + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + List hoursRankingQueryVoList = this.baseMapper.getAttendanceHoursRanking(dto.getGroupIds(), startDate, endDate); + if (CollUtil.isEmpty(hoursRankingQueryVoList)) { + return List.of(); + } + //查询用户信息 + List userIdList = hoursRankingQueryVoList.stream().map(MonthStatsHoursRankingQueryVo::getUserId).collect(Collectors.toList()); + ActionResult> getUserResult = v2UserApi.getAllUserInfoBatch(userIdList, UserProvider.getUser().getTenantId()); + if (Objects.isNull(getUserResult) || CollUtil.isEmpty(getUserResult.getData())) { + log.info("未查询到用户花名册数据:{}", userIdList); + } + List staffRosterDtoList = getUserResult.getData(); + Map staffRosterMap = staffRosterDtoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + return hoursRankingQueryVoList.stream().map(item -> MonthStatsHoursRankingVo.builder().userName(staffRosterMap.containsKey(item.getUserId()) ? staffRosterMap.get(item.getUserId()).getName() : "未查到用户信息").hours(item.getHours()).build()).collect(Collectors.toList()); + } + + @Override + public List getAttendanceFullSituation(MonthStatsDetailsDto dto) { + //获取月份日期列表 + List dayList = DayStatisticsUtils.getDatesForMonth(dto.getMonth(), "yyyyMM"); + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("MM.dd", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + List fullSituationQueryVoList = this.baseMapper.getAttendanceFullSituation(dto.getGroupIds(), startDate, endDate); + Map perCapitaMap = fullSituationQueryVoList.stream().collect(Collectors.toMap(MonthStatsFullSituationQueryVo::getDay, item -> item)); + if (CollUtil.isEmpty(fullSituationQueryVoList)) { + return dayList.stream().map(item -> MonthStatsFullSituationVo.builder().day(item.format(dayFormatter)).groupUserCount(0).fullCount(0).fullRatio(BigDecimal.ZERO).build()).collect(Collectors.toList()); + } + return dayList.stream().map(item -> MonthStatsFullSituationVo.builder().day(item.format(dayFormatter)).groupUserCount(perCapitaMap.containsKey(item) ? perCapitaMap.get(item).getGroupUserCount() : 0).fullCount(perCapitaMap.containsKey(item) ? perCapitaMap.get(item).getFullCount() : 0).fullRatio(perCapitaMap.containsKey(item) ? new BigDecimal(perCapitaMap.get(item).getFullCount()).multiply(new BigDecimal(100)).divide(new BigDecimal(perCapitaMap.get(item).getGroupUserCount()), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO).build()).collect(Collectors.toList()); + } + + @Override + public MonthStatsAbnormalConditionVo getAttendanceAbnormalCondition(MonthStatsDetailsDto dto) { + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + MonthStatsAbnormalConditionQueryVo abnormalConditionQueryVo = this.baseMapper.getAttendanceAbnormalCondition(dto.getGroupIds(), startDate, endDate); + BigDecimal totalCount = new BigDecimal(abnormalConditionQueryVo.getNormalCount() + abnormalConditionQueryVo.getLateCount() + abnormalConditionQueryVo.getEarlyCount() + abnormalConditionQueryVo.getAbsenceCardCount() + abnormalConditionQueryVo.getAbsenceCount()); + return MonthStatsAbnormalConditionVo.builder().normalCount(abnormalConditionQueryVo.getNormalCount()).normalPercent(abnormalConditionQueryVo.getNormalCount() > 0 ? new BigDecimal(abnormalConditionQueryVo.getNormalCount()).multiply(new BigDecimal(100)).divide(totalCount, 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO).lateCount(abnormalConditionQueryVo.getLateCount()).latePercent(abnormalConditionQueryVo.getLateCount() > 0 ? new BigDecimal(abnormalConditionQueryVo.getLateCount()).multiply(new BigDecimal(100)).divide(totalCount, 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO).earlyCount(abnormalConditionQueryVo.getEarlyCount()).earlyPercent(abnormalConditionQueryVo.getEarlyCount() > 0 ? new BigDecimal(abnormalConditionQueryVo.getEarlyCount()).multiply(new BigDecimal(100)).divide(totalCount, 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO).absenceCardCount(abnormalConditionQueryVo.getAbsenceCardCount()).absenceCardPercent(abnormalConditionQueryVo.getAbsenceCardCount() > 0 ? new BigDecimal(abnormalConditionQueryVo.getAbsenceCardCount()).multiply(new BigDecimal(100)).divide(totalCount, 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO).absenceCount(abnormalConditionQueryVo.getAbsenceCount()).absencePercent(abnormalConditionQueryVo.getAbsenceCount() > 0 ? new BigDecimal(abnormalConditionQueryVo.getAbsenceCount()).multiply(new BigDecimal(100)).divide(totalCount, 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO).build(); + } + + @Override + public List getAttendanceOvertimeSituation(MonthStatsDetailsDto dto) { + //获取月份日期列表 + List dayList = DayStatisticsUtils.getDatesForMonth(dto.getMonth(), "yyyyMM"); + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM", Locale.ENGLISH); + DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("MM.dd", Locale.ENGLISH); + YearMonth yearMonth = YearMonth.parse(dto.getMonth(), monthFormatter); + LocalDate startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearMonth.atEndOfMonth(); + //获取当前月份加班情况 + List overtimeSituationQueryVoList = this.baseMapper.getAttendanceOvertimeSituation(dto.getGroupIds(), startDate, endDate); + if (CollUtil.isEmpty(overtimeSituationQueryVoList)) { + return dayList.stream().map(item -> MonthStatsOvertimeSituationVo.builder().day(item.format(dayFormatter)).overtimeHours(BigDecimal.ZERO).overtimeHoursOnYear(BigDecimal.ZERO).overtimeHoursOnMonth(BigDecimal.ZERO).build()).collect(Collectors.toList()); + } + Map overtimeMap = overtimeSituationQueryVoList.stream().collect(Collectors.toMap(MonthStatsOvertimeSituationQueryVo::getDay, item -> item)); + //获取同比月份加班情况 + YearMonth onYear = yearMonth.minusYears(1); + LocalDate onYearStartDate = onYear.atDay(1); + LocalDate onYearEndDate = onYear.atEndOfMonth(); + List overtimeOnYearList = this.baseMapper.getAttendanceOvertimeSituation(dto.getGroupIds(), onYearStartDate, onYearEndDate); + Map overtimeOnYearMap = CollUtil.isNotEmpty(overtimeOnYearList) ? overtimeOnYearList.stream().collect(Collectors.toMap(MonthStatsOvertimeSituationQueryVo::getDay, item -> item)) : new HashMap<>(); + //获取环比月份加班情况 + YearMonth onMonth = yearMonth.minusMonths(1); + LocalDate onMonthStartDate = onMonth.atDay(1); + LocalDate onMonthEndDate = onMonth.atEndOfMonth(); + List overtimeOnMonthList = this.baseMapper.getAttendanceOvertimeSituation(dto.getGroupIds(), onMonthStartDate, onMonthEndDate); + Map overtimeOnMonthMap = CollUtil.isNotEmpty(overtimeOnMonthList) ? overtimeOnMonthList.stream().collect(Collectors.toMap(MonthStatsOvertimeSituationQueryVo::getDay, item -> item)) : new HashMap<>(); + return dayList.stream().map(item -> { + MonthStatsOvertimeSituationVo statsOvertimeSituationVo = MonthStatsOvertimeSituationVo.builder().day(item.format(dayFormatter)).overtimeHours(overtimeMap.containsKey(item) ? overtimeMap.get(item).getOvertimeHours() : BigDecimal.ZERO).overtimeHoursOnYear(BigDecimal.ZERO).overtimeHoursOnMonth(BigDecimal.ZERO).build(); + if (overtimeMap.containsKey(item)) { + LocalDate onYearItem = item.minusYears(1); + if (CollUtil.isEmpty(overtimeOnYearMap) || !overtimeOnYearMap.containsKey(onYearItem)) { + statsOvertimeSituationVo.setOvertimeHoursOnYear(BigDecimal.ZERO); + } else { + statsOvertimeSituationVo.setOvertimeHoursOnYear(overtimeOnYearMap.get(onYearItem).getOvertimeHours().compareTo(BigDecimal.ZERO) > 0 ? overtimeMap.get(item).getOvertimeHours().subtract(overtimeOnYearMap.get(onYearItem).getOvertimeHours()).multiply(new BigDecimal(100)).divide(overtimeOnYearMap.get(onYearItem).getOvertimeHours(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + } + LocalDate onMonthItem = item.minusMonths(1); + if (CollUtil.isEmpty(overtimeOnMonthMap) || !overtimeOnMonthMap.containsKey(onMonthItem)) { + statsOvertimeSituationVo.setOvertimeHoursOnMonth(BigDecimal.ZERO); + } else { + statsOvertimeSituationVo.setOvertimeHoursOnMonth(overtimeOnMonthMap.get(onMonthItem).getOvertimeHours().compareTo(BigDecimal.ZERO) > 0 ? overtimeMap.get(item).getOvertimeHours().subtract(overtimeOnMonthMap.get(onMonthItem).getOvertimeHours()).multiply(new BigDecimal(100)).divide(overtimeOnMonthMap.get(onMonthItem).getOvertimeHours(), 4, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO); + } + } + return statsOvertimeSituationVo; + }).collect(Collectors.toList()); + } + + @SneakyThrows + @Override + public List getConfirmDetailsInfoByMonth(List userIds, String month) { + DateRangeDto dateRangeDto = new DateRangeDto(month, Boolean.TRUE); + Date startDate = DateUtil.parseDate(dateRangeDto.getStartDate()); + Date endDate = DateUtil.parseDate(dateRangeDto.getEndDate()); + List pageListVoList = this.baseMapper.getConfirmDetailsInfoByMonth(userIds, startDate, endDate); + return monthConfirmStatisticsAssemble(month, dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), pageListVoList); + } + + @Override + @SneakyThrows + public PageInfo sealPageList(MonthSealPageListDto dto) { + DateRangeDto dateRangeDto = new DateRangeDto(dto.getMonth(), Boolean.TRUE); + //校验是否选择当月 + Assert.isFalse(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM")).equals(dto.getMonth()), "不能选择当月!"); + StatisticsDataQueryDto queryDto = new StatisticsDataQueryDto(); + BeanUtils.copyProperties(dto, queryDto); + List groupIds = dto.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + queryDto.setGroupIds(groupIds); + queryDto.setStartDate(dateRangeDto.getStartDate()); + queryDto.setEndDate(dateRangeDto.getEndDate()); + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) dto.getPageSize()); + pageInfo.setPageNum((int) dto.getCurrentPage()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(dto.getFilterList(), dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), queryDto.getUserIds(), String.valueOf(0)); + if (CollUtil.isEmpty(userBoundVoList)) { + return pageInfo; + } + if (StringUtil.isNotEmpty(dto.getKeyword())) { + userBoundVoList = userBoundVoList.stream().filter(item -> (StringUtil.isNotEmpty(item.getPhone()) && item.getPhone().contains(dto.getKeyword())) || (StringUtil.isNotEmpty(item.getName()) && item.getName().contains(dto.getKeyword())) || (StringUtil.isNotEmpty(item.getOrganizeName()) && item.getOrganizeName().contains(dto.getKeyword())) || (StringUtil.isNotEmpty(item.getPositionName()) && item.getPositionName().contains(dto.getKeyword())) || (StringUtil.isNotEmpty(item.getGradeName()) && item.getGradeName().contains(dto.getKeyword()))).collect(Collectors.toList()); + } + List userIdList = CollUtil.isNotEmpty(userBoundVoList) ? userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(userIdList)) { + log.error("未查询到考勤组用户数据"); + return pageInfo; + } + Map userBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + queryDto.setUserIds(userIdList); + PageHelper.startPage(Math.toIntExact(dto.getCurrentPage()), Math.toIntExact(dto.getPageSize())); + List pageListVoList = this.baseMapper.sealPageList(queryDto, dto.getSeal()); + if (CollUtil.isEmpty(pageListVoList) || CollUtil.isEmpty(userBoundVoMap)) { + return pageInfo; + } + pageInfo = new PageInfo<>(monthSealPageAssemble(dateRangeDto, pageListVoList, userBoundVoMap)); + pageInfo.setTotal(new PageInfo<>(pageListVoList).getTotal()); + pageInfo.setPageSize((int) dto.getPageSize()); + return pageInfo; + } + + /** + * 获取 sealPageListVoList + * @param dateRangeDto 周期范围 + * @param pageListVoList 用户封账数据 + * @param userBoundVoMap 用户信息 + * @return sealPageListVoList + */ + private List monthSealPageAssemble(DateRangeDto dateRangeDto, List pageListVoList, Map userBoundVoMap) { + return pageListVoList.stream().peek(item -> { + //设置用户基本信息 + if (userBoundVoMap.containsKey(item.getUserId())) { + UserBoundVO staffRosterDto = userBoundVoMap.get(item.getUserId()); + item.setOrgName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + item.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + item.setUserName(staffRosterDto.getName()); + item.setWorkStatus(staffRosterDto.getWorkStatusEnums().getCode()); + item.setCycle(dateRangeDto.getStartDate() + "~" + dateRangeDto.getEndDate()); + } + }).collect(Collectors.toList()); + } + + @Override + @SneakyThrows + @Transactional + public Boolean sealSubmit(MonthSealSubmitDto dto) { + DateRangeDto dateRangeDto = new DateRangeDto(dto.getMonth(), Boolean.TRUE); + //校验是否选择当月 + Assert.isFalse(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM")).equals(dto.getMonth()), "不能选择当月!"); + //数据量非常大,分批执行数据修改 + List idList = this.listObjs(Wrappers.lambdaQuery(AttendanceDayStatistics.class).in(AttendanceDayStatistics::getUserId, dto.getUserIdList()).between(AttendanceDayStatistics::getDate, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()).select(AttendanceDayStatistics::getId), obj -> (String) obj); + // 如果idList为空,则直接返回 + if (CollUtil.isEmpty(idList)) { + return Boolean.TRUE; + } + List> batches = DayStatisticsUtils.partition(idList, 1000); + for (List batch : batches) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(AttendanceDayStatistics::getId, batch); + updateWrapper.set(AttendanceDayStatistics::getSeal, 1); + this.update(updateWrapper); + } + // 获取用户出勤换算比 + List attendanceUserGroupList = groupService.getAttendanceUserGroup(dto.getUserIdList()); + List groupIds = CollUtil.isNotEmpty(attendanceUserGroupList) ? attendanceUserGroupList.stream().map(AttendanceUserGroupVo::getGroupId).distinct().collect(Collectors.toList()) : CollUtil.newArrayList(); + Map groupBaseSettingMap = baseSettingService.getEnableBaseSettingAll(groupIds); + Map ratioMap = new HashMap<>(); + if (CollUtil.isNotEmpty(attendanceUserGroupList)) { + attendanceUserGroupList.forEach(item -> { + ratioMap.put(item.getUserId(), groupBaseSettingMap.get(item.getGroupId()).getAttendanceRatio()); + }); + } + holidayRulesService.processPublicHoliday(dto.getMonth(), dto.getUserIdList(), ratioMap); + return Boolean.TRUE; + } + + @Override + @SneakyThrows + @Transactional + public Boolean unSealSubmit(MonthUnSealSubmitDto dto) { + //校验是否选择当月 + Assert.isFalse(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM")).equals(dto.getMonth()), "不能选择当月!"); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(AttendanceDayStatistics::getUserId, dto.getUserId()); + updateWrapper.set(AttendanceDayStatistics::getSeal, dto.getSeal()); + this.update(updateWrapper); + holidayRulesService.rollbackPublicHoliday(dto.getMonth(), List.of(dto.getUserId())); + return Boolean.TRUE; + } + + @Override + public Map selectUserIsSeal(List userIdList, String month) { + DateRangeDto dateRangeDto = new DateRangeDto(month, Boolean.TRUE); + List sealUserInfoDataList = this.baseMapper.selectUserIsSeal(userIdList, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + Map sealUserInfoDataMap = CollUtil.isNotEmpty(sealUserInfoDataList) ? sealUserInfoDataList.stream().collect(Collectors.toMap(SealUserInfoDataVo::getUserId, item -> item)) : new HashMap<>(); + return userIdList.stream().collect(Collectors.toMap(item -> item, item -> sealUserInfoDataMap.containsKey(item) && sealUserInfoDataMap.get(item).getSeal() > 0)); + } + + @Override + public Map selectUserIsSealByDateRange(List userIdList, Date start, Date end) { + if (CollUtil.isEmpty(userIdList)) { + return new HashMap<>(); + } + String startStr = jnpf.util.DateUtil.dateToString(start, "yyyy-MM-dd"); + String endStr = jnpf.util.DateUtil.dateToString(end, "yyyy-MM-dd"); + DateRangeDto dateRangeDto = new DateRangeDto(startStr, endStr); + List sealUserInfoDataList = this.baseMapper.selectUserIsSeal(userIdList, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + Map sealUserInfoDataMap = CollUtil.isNotEmpty(sealUserInfoDataList) ? sealUserInfoDataList.stream().collect(Collectors.toMap(SealUserInfoDataVo::getUserId, item -> item)) : new HashMap<>(); + return userIdList.stream().collect(Collectors.toMap(item -> item, item -> sealUserInfoDataMap.containsKey(item) && sealUserInfoDataMap.get(item).getSeal() > 0)); + } + + @Override + public Map> selectUserDailySealByDateRange(List userIdList, Date start, Date end) { + if (CollUtil.isEmpty(userIdList) || start == null || end == null) { + return new HashMap<>(); + } + String startStr = jnpf.util.DateUtil.dateToString(start, "yyyy-MM-dd"); + String endStr = jnpf.util.DateUtil.dateToString(end, "yyyy-MM-dd"); + DateRangeDto dateRangeDto = new DateRangeDto(startStr, endStr); + List list = this.baseMapper.selectUserDailySeal(userIdList, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } + return list.stream() + .filter(vo -> vo != null && vo.getUserId() != null && vo.getDate() != null) + .collect(Collectors.groupingBy( + UserDailySealVo::getUserId, + Collectors.mapping(UserDailySealVo::getDate, Collectors.toSet()) + )); + } + + @Override + public Map salaryAttendanceSupport(SalaryAttendanceSupportDto dto) { + List supportVoList = this.baseMapper.salaryAttendanceSupport(dto); + Map salaryUserDataMap = initSalaryUserData(dto, new HashMap<>()); + DateRangeDto dateRangeDto = new DateRangeDto(dto.getStartDate(), dto.getEndDate()); + Date startDate = jnpf.util.DateUtil.stringToDate(dateRangeDto.getStartDate()); + Date endDate = jnpf.util.DateUtil.stringToDate(dateRangeDto.getEndDate()); + // 获取用户出勤换算比 + List attendanceUserGroupList = userService.getAttendanceGroupUsersOfSecondment(startDate, endDate, dto.getUserIds(), null); + List groupIds = CollUtil.isNotEmpty(supportVoList) ? supportVoList.stream().map(SalaryAttendanceSupportQuery::getGroupId).distinct().collect(Collectors.toList()) : CollUtil.newArrayList(); + if (CollUtil.isEmpty(attendanceUserGroupList) && CollUtil.isEmpty(groupIds)) { + return salaryUserDataMap; + } + if (CollUtil.isNotEmpty(attendanceUserGroupList)) { + groupIds.addAll(attendanceUserGroupList.stream().map(AttendanceGroupUser::getGroupId).distinct().collect(Collectors.toList())); + } + // 查询考勤组信息 + List groupList = this.groupService.queryListIncludeDeleteByIds(groupIds); + if (CollUtil.isEmpty(groupList)) { + return salaryUserDataMap; + } + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, item -> item)); + Map> userStaMap = supportVoList.stream().collect(Collectors.groupingBy(SalaryAttendanceSupportQuery::getUserId)); + // 用户的所有节假日 + Map> autoSchedulingDaysMap = festivalRulesService.batchGetFestivalRulesByUserIds(startDate, endDate, dto.getUserIds()); + // 自动排班天数 + Map autoSchedulingDaysStaMap = DayStatisticsUtils.getUserAutoSchedulingDaysStaMap(startDate, endDate, autoSchedulingDaysMap); + // 法定节假日天数 + Map> legalHolidaysStaMap = DayStatisticsUtils.getLegalHolidaysStaMap(startDate, endDate, autoSchedulingDaysMap); + // 用户的节假日天数 + Map> holidayDaysStaMapMap = DayStatisticsUtils.getUserHolidayDaysStaMapMap(startDate, endDate, autoSchedulingDaysMap); + //获取公休转存休天数 + List publicHolidayList = publicHolidayRulesMapper.getPublicHolidayTransferList(DayStatisticsUtils.getMonthFromDate(startDate), dto.getUserIds()); + Map publicHolidayTransferMap = CollUtil.isNotEmpty(publicHolidayList) ? publicHolidayList.stream().collect(Collectors.toMap(PublicHolidayTransferListVo::getUserId, item -> item)) : new HashMap<>(); + // 获取加班算薪加班小时数(不计算存休-结算薪酬) + Map overtimeSalaryMap = enableBalanceService.getOvertimeSalary(dto.getStartDate(), dto.getEndDate(), dto.getUserIds()); + // 加班存休天数以及剩余天数 + List overtimeBalanceInfo = balanceRecordMapper.getOvertimeBalanceInfo(startDate, endDate, dto.getUserIds()); + Map overtimeBalanceMap = CollUtil.isNotEmpty(overtimeBalanceInfo) ? overtimeBalanceInfo.stream().collect(Collectors.toMap(OvertimeBalanceModel::getUserId, item -> item)) : new HashMap<>(); + // 存休剩余天数 + List surplusDaysInfo = balanceRecordMapper.getSurplusDaysInfo(dto.getUserIds()); + Map surplusDaysMap = CollUtil.isNotEmpty(surplusDaysInfo) ? surplusDaysInfo.stream().collect(Collectors.toMap(SurplusDaysModel::getUserId, SurplusDaysModel::getSurplusDays)) : new HashMap<>(); + Map groupBaseSettingMap = baseSettingService.getEnableBaseSettingAll(groupIds); + Map ratioMap = new HashMap<>(); + if (CollUtil.isNotEmpty(attendanceUserGroupList)) { + attendanceUserGroupList.forEach(item -> { + ratioMap.put(item.getUserId(), groupBaseSettingMap.containsKey(item.getGroupId()) && Objects.nonNull(groupBaseSettingMap.get(item.getGroupId())) ? groupBaseSettingMap.get(item.getGroupId()).getAttendanceRatio() : BigDecimal.ZERO); + }); + } + // 获取所有请假类型 + List leaveRulesLiat = leaveTypeService.list(); + Map leaveTypeMap = leaveRulesLiat.stream().collect(Collectors.toMap(AttendanceLeaveType::getId, item -> item)); + // 自定义假期余额列表 + List holidayBalanceInfo = balanceRecordMapper.getLeaveBalanceInfo(startDate, endDate, dto.getUserIds()); + // 自定义请假审批记录 + List leaveIds = supportVoList.stream().map(SalaryAttendanceSupportQuery::getLeaveBatchNumber).filter(Objects::nonNull).flatMap(batchNumber -> DayStatisticsUtils.parseBatchNumbers(batchNumber).stream()).distinct().collect(Collectors.toList()); + List leaveApprovalInfo = CollUtil.isNotEmpty(leaveIds) ? balanceRecordMapper.getLeaveApprovalInfo(leaveIds, dto.getUserIds()) : new ArrayList<>(); + // 请假天数数据 + Map> leaveTypeMapMap = DayStatisticsUtils.buildLeaveMap(leaveApprovalInfo, leaveTypeMap, supportVoList); + // 请假余额数据 + Map> buildLeaveBalanceMap = DayStatisticsUtils.buildLeaveBalanceMap(holidayBalanceInfo, leaveTypeMap, ratioMap); + // 请假抵扣天数数据 + Map> leaveDeductMap = DayStatisticsUtils.buildLeaveDeductMap(leaveApprovalInfo, leaveTypeMap, startDate, endDate); + // 请假扣薪数据 + Map> leaveDeductWagesMap = DayStatisticsUtils.buildLeaveDeductWagesMap(leaveApprovalInfo, leaveTypeMap, startDate, endDate); + // 节日类型天数 + Map> holidayLeaveTypeMap = DayStatisticsUtils.buildHolidayTypeMap(startDate, endDate, autoSchedulingDaysMap, ratioMap); + // 节假日加班天数 + List overtimeHolidaysInfoList = balanceRecordMapper.getOvertimeHolidaysInfo(startDate, endDate, dto.getUserIds()); + // 加班算薪json + List overtimeSalaryJson = enableBalanceService.getOvertimeSalaryJson(dto.getStartDate(), dto.getEndDate(), dto.getUserIds()); + Map> overtimeHolidaysMap = DayStatisticsUtils.buildOvertimeHolidaysMap(overtimeHolidaysInfoList, overtimeSalaryJson, ratioMap); + for (Map.Entry entry : salaryUserDataMap.entrySet()) { + SalaryAttendanceSupportVo supportVo = entry.getValue(); + List dataList = userStaMap.containsKey(entry.getKey()) ? userStaMap.get(entry.getKey()) : new ArrayList<>(); + List groupInfoList = dataList.stream().map(item -> { + GroupInfoModel groupInfoModel = new GroupInfoModel(); + groupInfoModel.setGroupName(groupMap.containsKey(item.getGroupId()) ? groupMap.get(item.getGroupId()).getGroupName() : ""); + groupInfoModel.setAttendanceRatio(item.getAttendanceRatio()); + return groupInfoModel; + }).collect(Collectors.toList()); + supportVo.setGroupInfoList(groupInfoList); + supportVo.setShouldAttendDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getShouldAttendDays)); + supportVo.setShouldAttendHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getShouldAttendPayrollHours)); + supportVo.setActualAttendDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getEffectiveAttendDays)); + supportVo.setActualAttendHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getEffectiveAttendPayrollHours)); + if (holidayDaysStaMapMap.containsKey(entry.getKey())) { + List effectiveAttendDayList = DayStatisticsUtils.getCustomDataList(dataList.stream().map(SalaryAttendanceSupportQuery::getEffectiveAttendDayList).filter(CollUtil::isNotEmpty).collect(Collectors.toList()), SalarySupportDayStaModel.class); + Set dates = holidayDaysStaMapMap.get(entry.getKey()); + supportVo.setHolidaysSchedulingDays(effectiveAttendDayList.stream().filter(item -> dates.contains(item.getDay())).map(SalarySupportDayStaModel::getDays).collect(Collectors.toList()).stream().reduce(BigDecimal.ZERO, BigDecimal::add)); + supportVo.setHolidaysSchedulingHours(effectiveAttendDayList.stream().filter(item -> dates.contains(item.getDay())).map(SalarySupportDayStaModel::getHours).collect(Collectors.toList()).stream().reduce(BigDecimal.ZERO, BigDecimal::add)); + } + supportVo.setHolidaysAutoPaid(autoSchedulingDaysStaMap.getOrDefault(entry.getKey(), 0)); + supportVo.setHolidaysMap(legalHolidaysStaMap.getOrDefault(entry.getKey(), new HashMap<>())); + supportVo.setPublicHolidays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getPublicHolidaysDays)); + supportVo.setPublicHolidaysHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getPublicHolidaysPayrollHours)); + supportVo.setPublicHolidaysOvertimeDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getPublicHolidaysOvertimeDays)); + supportVo.setPublicHolidaysOvertimeHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getPublicHolidaysOvertimeHours)); + // 实公休天数 = 公休排班天数 - 公休加班天数(小时数也是一样的) + supportVo.setActualPublicHolidays(DayStatisticsUtils.calculateSubtract(supportVo.getPublicHolidays(), supportVo.getPublicHolidaysOvertimeDays())); + supportVo.setActualPublicHolidaysHours(DayStatisticsUtils.calculateSubtract(supportVo.getPublicHolidaysHours(), supportVo.getPublicHolidaysOvertimeHours())); + PublicHolidayTransferListVo transferListVo = publicHolidayTransferMap.getOrDefault(entry.getKey(), new PublicHolidayTransferListVo(entry.getKey(), BigDecimal.ZERO, BigDecimal.ZERO)); + supportVo.setPublicHolidaysTransfer(transferListVo.getSurplusDays().stripTrailingZeros()); + supportVo.setPublicHolidaysTransferHours(transferListVo.getSurplusDays().multiply(transferListVo.getAttendanceRatio()).stripTrailingZeros()); + OvertimeBalanceModel balanceModel = overtimeBalanceMap.getOrDefault(entry.getKey(), new OvertimeBalanceModel(entry.getKey(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)); + supportVo.setPublicHolidaysSurplus(surplusDaysMap.getOrDefault(entry.getKey(), BigDecimal.ZERO).stripTrailingZeros()); + // 出勤换算比 + BigDecimal bigDecimal = ratioMap.getOrDefault(entry.getKey(), BigDecimal.ZERO); + supportVo.setPublicHolidaysSurplusHours(surplusDaysMap.getOrDefault(entry.getKey(), BigDecimal.ZERO).multiply(bigDecimal).stripTrailingZeros()); + supportVo.setLateTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getLateTimes)); + supportVo.setLateMinutes(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getLateMinutes)); + List lateDayMinutesList = DayStatisticsUtils.getCustomDataList(dataList.stream().map(SalaryAttendanceSupportQuery::getLateDayMinutesList).filter(CollUtil::isNotEmpty).collect(Collectors.toList()), SalarySupportDayModel.class); + Map dateLateDayMinutesMap = lateDayMinutesList.stream().collect(Collectors.groupingBy(SalarySupportDayModel::getDay, Collectors.mapping(SalarySupportDayModel::getMinutes, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); + Map lateDayMinutesMap = CollUtil.isNotEmpty(dateLateDayMinutesMap) ? dateLateDayMinutesMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) : new HashMap<>(); + supportVo.setLateDayMinutesMap(lateDayMinutesMap); + supportVo.setEarlyLeaveTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getEarlyLeaveTimes)); + supportVo.setEarlyLeaveMinutes(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getEarlyLeaveMinutes)); + List earlyLeaveDayMinutesList = DayStatisticsUtils.getCustomDataList(dataList.stream().map(SalaryAttendanceSupportQuery::getEarlyLeaveDayMinutesList).filter(CollUtil::isNotEmpty).collect(Collectors.toList()), SalarySupportDayModel.class); + Map dateEarlyLeaveDayMinutesMap = earlyLeaveDayMinutesList.stream().collect(Collectors.groupingBy(SalarySupportDayModel::getDay, Collectors.mapping(SalarySupportDayModel::getMinutes, Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)))); + Map earlyLeaveDayMinutesMap = CollUtil.isNotEmpty(dateEarlyLeaveDayMinutesMap) ? dateEarlyLeaveDayMinutesMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) : new HashMap<>(); + supportVo.setEarlyLeaveDayMinutesMap(earlyLeaveDayMinutesMap); + supportVo.setAbsenceCardTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getAbsenceCardTimes)); + supportVo.setAbsenceTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getAbsenceTimes)); + supportVo.setAbsenceDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getAbsenceDays)); + supportVo.setAbsenceHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getAbsencePayrollHours)); + supportVo.setMakeUpCardTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getMakeUpCardTimes)); + supportVo.setOutworkTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getOutworkTimes)); + supportVo.setOutworkDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getOutworkDays)); + supportVo.setOutworkHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getOutworkPayrollHours)); + supportVo.setOutDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getOutDays)); + supportVo.setOutHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getOutHours)); + supportVo.setBusDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getBusDays)); + supportVo.setBusHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getBusHours)); + supportVo.setOvertimeTimes(DayStatisticsUtils.calculateSumToInt(dataList, SalaryAttendanceSupportQuery::getOvertimeTimes)); + supportVo.setOvertimeDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getOvertimeDays)); + supportVo.setOvertimeHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getOvertimeHours)); + OvertimeSalaryHoursVo overtimeSalaryHoursVo = overtimeSalaryMap.getOrDefault(entry.getKey(), new OvertimeSalaryHoursVo(entry.getKey(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)); + supportVo.setWeekdayOvertimeDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getWeekdayOvertimeDays)); + supportVo.setWeekdayOvertimeHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getWeekdayOvertimeHours)); + supportVo.setWeekdayOvertimeSalaryHours(overtimeSalaryHoursVo.getWeekdayOvertimeSalaryDays().multiply(bigDecimal).stripTrailingZeros()); + supportVo.setWeekdayOvertimeTransferHours(balanceModel.getWeekdayOvertimeDays().multiply(bigDecimal).stripTrailingZeros()); + supportVo.setHolidaysOvertimeDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getHolidaysOvertimeDays)); + supportVo.setHolidaysOvertimeHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getHolidaysOvertimeHours)); + supportVo.setHolidaysOvertimeSalaryHours(overtimeSalaryHoursVo.getHolidaysOvertimeSalaryDays().multiply(bigDecimal).stripTrailingZeros()); + supportVo.setHolidaysOvertimeTransferHours(balanceModel.getHolidaysOvertimeDays().multiply(bigDecimal).stripTrailingZeros()); + supportVo.setPublicHolidaysOvertimeSalaryHours(overtimeSalaryHoursVo.getPublicHolidaysOvertimeSalaryDays().multiply(bigDecimal).stripTrailingZeros()); + supportVo.setPublicHolidaysOvertimeTransferHours(balanceModel.getPublicHolidaysOvertimeDays().multiply(bigDecimal).stripTrailingZeros()); + supportVo.setLeaveDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getLeaveDays)); + supportVo.setLeaveHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getLeavePayrollHours)); + supportVo.setCompensationDays(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getCompensationDays)); + supportVo.setCompensationHours(DayStatisticsUtils.calculateSum(dataList, SalaryAttendanceSupportQuery::getCompensationPayrollHours)); + supportVo.setCustomLeaveMap(leaveTypeMapMap.getOrDefault(entry.getKey(), new HashMap<>())); + supportVo.setCustomLeaveSurplusMap(buildLeaveBalanceMap.getOrDefault(entry.getKey(), new HashMap<>())); + supportVo.setCustomLeaveDeductionMap(leaveDeductMap.getOrDefault(entry.getKey(), new HashMap<>())); + supportVo.setCustomLeavePayMap(leaveDeductWagesMap.getOrDefault(entry.getKey(), new HashMap<>())); + supportVo.setHolidaysTypeMap(holidayLeaveTypeMap.getOrDefault(entry.getKey(), new HashMap<>())); + supportVo.setHolidaysTypeOvertimeMap(overtimeHolidaysMap.getOrDefault(entry.getKey(), new HashMap<>())); + } + return salaryUserDataMap; + } + + @Override + public List attendanceStaList(SalaryAttendanceSupportDto dto) { + List staListVoList = new ArrayList<>(); + DayStatisticsDataPageListQueryDto queryDto = new DayStatisticsDataPageListQueryDto(); + queryDto.setUserIds(dto.getUserIds()); + queryDto.setQueryType(1); + queryDto.setStartDate(dto.getStartDate()); + queryDto.setEndDate(dto.getEndDate()); + // 查询用户信息 + ActionResult> getUserResult = v2UserApi.getAllUserInfoBatch(dto.getUserIds(), UserProvider.getUser().getTenantId()); + if (Objects.isNull(getUserResult) || CollUtil.isEmpty(getUserResult.getData())) { + log.info("未查询到用户数据:{}", dto.getUserIds()); + return staListVoList; + } + List staffRosterDtoList = getUserResult.getData(); + Map userBoundVoMap = staffRosterDtoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + List pageListVoList = this.baseMapper.getDayPageList(queryDto); + if (CollUtil.isEmpty(pageListVoList)) { + return staListVoList; + } + List groupIds = pageListVoList.stream().map(DayStatisticsQueryVo::getGroupId).distinct().collect(Collectors.toList()); + List groupList = this.groupService.queryListIncludeDeleteByIds(groupIds); + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + return dayStatisticsAssemble(pageListVoList, groupMap, userBoundVoMap); + } + + @Override + public List getAttendanceDayStaList(DayStatisticsDto dto) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(AttendanceGroup::getOrgId, dto.getStoreIds()); + queryWrapper.eq(AttendanceGroup::getDeleteMark, 0); + List groupList = this.groupService.list(queryWrapper); + if (CollUtil.isEmpty(groupList)) { + return dto.getStoreIds().stream().map(storeId -> DayStatisticsVo.builder().storeId(storeId).attendCount(0).build()).collect(Collectors.toList()); + } + Map groupIdMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getOrgId, AttendanceGroup::getId)); + DayStatisticsDataPageListQueryDto queryDto = new DayStatisticsDataPageListQueryDto(); + queryDto.setGroupIds(groupList.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList())); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + queryDto.setStartDate(dto.getDay().format(dateTimeFormatter)); + queryDto.setEndDate(dto.getDay().format(dateTimeFormatter)); + queryDto.setQueryType(1); + List pageListVoList = this.baseMapper.getDayPageList(queryDto); + if (CollUtil.isNotEmpty(pageListVoList)) { + pageListVoList.removeIf(item -> item.getEffectiveAttendHours().compareTo(BigDecimal.ZERO) <= 0); + } + Map> storeDayStaMap = CollUtil.isNotEmpty(pageListVoList) ? pageListVoList.stream().collect(Collectors.groupingBy(DayStatisticsQueryVo::getGroupId)) : new HashMap<>(); + return dto.getStoreIds().stream().map(item -> DayStatisticsVo.builder().storeId(item).attendCount(Math.toIntExact(storeDayStaMap.getOrDefault(groupIdMap.get(item), Lists.newArrayList()).stream().map(DayStatisticsQueryVo::getUserId).distinct().count())).build()).collect(Collectors.toList()); + } + + @Override + public Map> getDimensionsAttendanceCountMap(DimensionsAttendanceCountDto dto) { + List dimensionsRangeVoList = Stream.of(CompareTypeEnums.values()).map(typeEnums -> new DateDimensionsRangeVo(typeEnums, 0)).collect(Collectors.toList()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(AttendanceGroup::getOrgId, dto.getStoreIds()); + queryWrapper.eq(AttendanceGroup::getDeleteMark, 0); + List groupList = this.groupService.list(queryWrapper); + if (CollUtil.isEmpty(groupList)) { + return dto.getStoreIds().stream().collect(Collectors.toMap(a -> a, a -> dimensionsRangeVoList)); + } + Map groupIdMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getOrgId, AttendanceGroup::getId)); + List dimensionVoList = this.baseMapper.getDimensionsAttendanceCountList(new ArrayList<>(groupIdMap.values()), dto.getDimensionsRangeList()); + Map> groupMapList = dimensionVoList.stream().collect(Collectors.groupingBy(AttendanceDimensionVo::getGroupId)); + return dto.getStoreIds().stream().collect(Collectors.toMap(a -> a, id -> { + List resultVoList = Stream.of(CompareTypeEnums.values()).map(typeEnums -> new DateDimensionsRangeVo(typeEnums, 0)).collect(Collectors.toList()); + if (!groupMapList.containsKey(groupIdMap.get(id))) { + return resultVoList; + } + List dimensionVos = groupMapList.get(groupIdMap.get(id)); + resultVoList.forEach(item -> { + if (dimensionVos.stream().noneMatch(dimensionVo -> dimensionVo.getDimensionType().equals(item.getTypeEnums().getValue()))) { + return; + } + AttendanceDimensionVo dimensionVo1 = dimensionVos.stream().filter(dimensionVo -> dimensionVo.getDimensionType().equals(item.getTypeEnums().getValue())).findFirst().orElse(null); + item.setCount(Optional.ofNullable(dimensionVo1).map(AttendanceDimensionVo::getPersonCount).orElse(0)); + }); + return resultVoList; + }, (k1, k2) -> k1)); + } + + @Override + public Map> getDimensionsAttendanceDayCountMap(DimensionsAttendanceDayCountDto dto) { + Map dimensionsRangeVoList = new HashMap<>(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(AttendanceGroup::getOrgId, dto.getStoreIds()); + queryWrapper.eq(AttendanceGroup::getDeleteMark, 0); + List groupList = this.groupService.list(queryWrapper); + if (CollUtil.isEmpty(groupList)) { + return dto.getStoreIds().stream().collect(Collectors.toMap(a -> a, a -> dimensionsRangeVoList)); + } + Map groupIdMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getOrgId, AttendanceGroup::getId)); + List dimensionVoDayList = this.baseMapper.getDimensionsAttendanceDayCountList(new ArrayList<>(groupIdMap.values()), dto.getStartDate(), dto.getEndDate()); + Map> groupMapList = dimensionVoDayList.stream().collect(Collectors.groupingBy(AttendanceDimensionDayVo::getGroupId)); + return dto.getStoreIds().stream().collect(Collectors.toMap(a -> a, id -> { + Map resultVoList = new HashMap<>(); + if (!groupMapList.containsKey(groupIdMap.get(id))) { + return resultVoList; + } + List dimensionVos = groupMapList.get(groupIdMap.get(id)); + resultVoList = dimensionVos.stream().collect(Collectors.toMap(AttendanceDimensionDayVo::getDay, AttendanceDimensionDayVo::getPersonCount)); + return resultVoList; + }, (k1, k2) -> k1)); + } + + @Override + public PageInfo getDayPayrollPageList(DayPayrollStatisticsPageListDto req) { + DayStatisticsDataPageListQueryDto queryDto = new DayStatisticsDataPageListQueryDto(); + BeanUtils.copyProperties(req, queryDto); + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) req.getPageSize()); + pageInfo.setPageNum((int) req.getCurrentPage()); + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), req.getStartDate(), req.getEndDate(), req.getUserIds(), req.getWorkStatus()); + Map userBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + if (CollUtil.isEmpty(userBoundVoList)) { + return pageInfo; + } + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + //获取考勤组信息 + List groupList = this.groupService.queryListIncludeDeleteByIds(groupIds); + if (CollUtil.isEmpty(groupList)) { + log.info("未查询到考勤组数据:{}", groupIds); + return pageInfo; + } + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + queryDto.setUserIds(userIds); + queryDto.setGroupIds(groupIds); + PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + List pageListVoList = this.baseMapper.getDayPayrollPageList(queryDto); + if (CollUtil.isEmpty(pageListVoList)) { + return pageInfo; + } + pageInfo = new PageInfo<>(dayPayrollStatisticsAssemble(pageListVoList, groupMap, userBoundVoMap)); + pageInfo.setTotal(new PageInfo<>(pageListVoList).getTotal()); + pageInfo.setPageSize(Math.toIntExact(req.getPageSize())); + pageInfo.setPageNum(Math.toIntExact(req.getCurrentPage())); + return pageInfo; + } + + /** + * 计薪日统计数据封装 + * + * @param statisticsQueryVoList 日统计数据 + * @param groupMap 考勤数据 + * @param staffRosterMap 用户花名册数据 + * @return List + */ + private List dayPayrollStatisticsAssemble(List statisticsQueryVoList, Map groupMap, Map staffRosterMap) { + Map> shiftsJsonMap = new HashMap<>(); + List shiftIdList = new ArrayList<>(); + for (DayPayrollStatisticsQueryVo item : statisticsQueryVoList) { + List shiftsJsonVoList = parseArray(item.getShiftsJson(), DayStatisticsShiftsJsonVo.class); + if (CollUtil.isNotEmpty(shiftsJsonVoList)) { + shiftIdList.addAll(shiftsJsonVoList.stream().map(DayStatisticsShiftsJsonVo::getShiftId).collect(Collectors.toList())); + } + shiftsJsonMap.put(item.getId(), CollUtil.isNotEmpty(shiftsJsonVoList) ? shiftsJsonVoList : new ArrayList<>()); + } + if (CollUtil.isNotEmpty(shiftIdList)) { + shiftIdList = shiftIdList.stream().distinct().collect(Collectors.toList()); + } + Map shiftNameEntityMap = CollUtil.isEmpty(shiftIdList) ? new HashMap<>() : dailyRuleService1.getShiftByShiftIds(shiftIdList); + return statisticsQueryVoList.stream().map(item -> { + DayPayrollStatisticsPageListVo dayStatistics = DayPayrollStatisticsPageListVo.builder().build(); + BeanUtils.copyProperties(item, dayStatistics); + if (staffRosterMap.containsKey(item.getUserId())) { + UserBoundVO staffRosterDto = staffRosterMap.get(item.getUserId()); + dayStatistics.setDeptName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + dayStatistics.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + dayStatistics.setUserName(StringUtil.isNotEmpty(staffRosterDto.getName()) ? staffRosterDto.getName() : ""); + dayStatistics.setWorkStatus(Objects.nonNull(staffRosterDto.getWorkStatusEnums()) ? staffRosterDto.getWorkStatusEnums().getCode() : null); + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(dayStatistics.getWorkStatus()); + dayStatistics.setWorkStatusStr(Objects.nonNull(workStatusEnum) ? workStatusEnum.getMsg() : ""); + } + // 计算各类请假数据 + dayStatistics.setCustomLeaveList(DayStatisticsUtils.getDayCustomLeaveList(item.getCustomLeaveList())); + dayStatistics.setGroupName(groupMap.containsKey(item.getGroupId()) ? groupMap.get(item.getGroupId()).getGroupName() : null); + dayStatistics.setDate(DateUtil.formatDate(item.getDate())); + //处理班次、班次时间、打卡时间 + List shiftsJsonVoList = shiftsJsonMap.containsKey(item.getId()) ? shiftsJsonMap.get(item.getId()) : Lists.newArrayList(); + List collect = getSchedulesItemVos(shiftNameEntityMap, shiftsJsonVoList); + dayStatistics.setShiftList(collect); + dayStatistics.setShiftStr(CollUtil.isNotEmpty(collect) ? StringUtil.join(collect.stream().map(SchedulesItemVo::getShortName).filter(StringUtil::isNotBlank).distinct().collect(Collectors.toList()), "|") : ""); + //处理班次时间 + List shiftTimeList = new ArrayList<>(); + boolean containsOrdinary = shiftsJsonVoList.stream().anyMatch(json -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), json.getAttendanceType())); + List shiftsJsonVoFilterList = shiftsJsonVoList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()) || (!containsOrdinary && AttendanceTypeEnum.LEAVE.getCode().equals(m.getAttendanceType()))).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(shiftsJsonVoFilterList)) { + shiftsJsonVoFilterList = shiftsJsonVoFilterList.stream().sorted(Comparator.comparing(DayStatisticsShiftsJsonVo::getInPoint)).collect(Collectors.toList()); + } + StringBuilder shiftTimeStrList = new StringBuilder(); + shiftsJsonVoFilterList.forEach(shiftsJsonVo -> { + StringBuilder shiftTimeStr = new StringBuilder(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("~"); + shiftTimeStrList.append(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("~"); + if (!DayStatisticsUtils.isSameDay(item.getDate(), shiftsJsonVo.getOutPoint())) { + shiftTimeStr.append("(次日)").append(DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")); + shiftTimeStrList.append("(次日)").append(DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")).append("|"); + } else { + shiftTimeStr.append(DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")); + shiftTimeStrList.append(DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")).append("|"); + } + shiftTimeList.add(shiftTimeStr.toString()); + }); + dayStatistics.setShiftTimeList(shiftTimeList); + dayStatistics.setShiftTimeStr(shiftTimeStrList.length() > 0 ? shiftTimeStrList.substring(0, shiftTimeStrList.length() - 1) : ""); + //处理打卡时间 + List clockTimeList = new ArrayList<>(); + StringBuilder clockTimeStr = new StringBuilder(); + shiftsJsonVoFilterList.forEach(shiftsJsonVo -> { + DayStatisticsUtils.enclosurePayrollData(item, shiftsJsonVo, clockTimeList, clockTimeStr); + }); + dayStatistics.setActualAttendPayrollHours(dayStatistics.getActualAttendPayrollHours().compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : dayStatistics.getActualAttendPayrollHours()); + dayStatistics.setClockTimeList(clockTimeList); + dayStatistics.setClockTimeStr(clockTimeStr.length() > 0 ? clockTimeStr.substring(0, clockTimeStr.length() - 1) : ""); + return dayStatistics; + }).collect(Collectors.toList()); + } + + @Override + public PageInfo getMonthPayrollPageList(MonthPayrollStatisticsPageListDto req) throws Exception { + MonthStatisticsDataQueryDto queryDto = new MonthStatisticsDataQueryDto(); + BeanUtils.copyProperties(req, queryDto); + DateRangeDto dateRangeDto = new DateRangeDto(req.getMonth(), Boolean.TRUE); + queryDto.setStartDate(dateRangeDto.getStartDate()); + queryDto.setEndDate(dateRangeDto.getEndDate()); + //获取考勤组信息 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize((int) req.getPageSize()); + pageInfo.setPageNum((int) req.getCurrentPage()); + //获取有权限的用户列表 + List userBoundVoList = getUserIdArr(req.getFilterList(), dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), req.getUserIds(), req.getWorkStatus()); + Map userBoundVoMap = userBoundVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + if (CollUtil.isEmpty(userBoundVoList)) { + return pageInfo; + } + List userIds = userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + List groupIds = req.getFilterList().stream().map(GroupFilterDto::getGroupId).distinct().collect(Collectors.toList()); + List groupList = this.groupService.queryListIncludeDeleteByIds(groupIds); + if (CollUtil.isEmpty(groupList)) { + log.info("未查询到考勤组数据:{}", groupIds); + return pageInfo; + } + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)); + queryDto.setUserIds(userIds); + req.setUserIds(userIds); + queryDto.setGroupIds(groupIds); + PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + List pageListVoList = this.baseMapper.getMonthPayrollPageList(queryDto); + if (CollUtil.isEmpty(pageListVoList)) { + return pageInfo; + } + pageInfo = new PageInfo<>(monthPayrollStatisticsAssemble(req.getMonth(), queryDto.getStartDate(), queryDto.getEndDate(), pageListVoList, groupMap, userBoundVoMap)); + pageInfo.setTotal(new PageInfo<>(pageListVoList).getTotal()); + pageInfo.setPageSize(Math.toIntExact(req.getPageSize())); + pageInfo.setPageNum(Math.toIntExact(req.getCurrentPage())); + return pageInfo; + } + + /** + * 获取考勤组ID集合 + * + * @param ids 组织id/考勤组ID + * @param aTrue 是否考勤组ID + * @return 考勤组id + */ + private Map getGroupIds(List ids, Boolean aTrue) { + List groupList; + if (aTrue) { + groupList = this.groupService.queryListIncludeDeleteByIds(ids); + } else { + groupList = this.groupService.getGroupListByOrgIds(ids.stream().distinct().collect(Collectors.toList())); + } + return CollUtil.isNotEmpty(groupList) ? groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, a -> a, (k1, k2) -> k1)) : Map.of(); + } + + @Override + public PageListVO averageWorkHoursTrendPageList(PersonnelApiInfoPageListDto pageDto) { + //获取组织下所有考勤组 + Map groupMap = getGroupIds(pageDto.getOrgIds(), Boolean.FALSE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getStartMonth(), pageDto.getEndMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.averageWorkHoursTrendPageList(page, new ArrayList<>(groupMap.keySet()), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + listVoPage.getRecords().forEach(item -> { + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void averageWorkHoursTrendExport(HttpServletResponse response, PersonnelApiInfoPageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = averageWorkHoursTrendPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤平均工时趋势.xlsx", "考勤平均工时趋势", AverageTrendPageListVo.class, dataList); + } + + @Override + public PageListVO personWorkHoursTrendPageList(PersonnelApiInfoSinglePageListDto pageDto) { + //获取考勤组 + Map groupMap = getGroupIds(pageDto.getGroupIds(), Boolean.TRUE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getMonth(), pageDto.getMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.personWorkHoursTrendPageList(page, pageDto.getGroupIds(), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + listVoPage.getRecords().forEach(item -> { + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void personWorkHoursTrendExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = personWorkHoursTrendPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤人均工时趋势.xlsx", "考勤人均工时趋势", PersonnelTrendPageListVo.class, dataList); + + } + + @Override + public PageListVO dailySituationPageList(PersonnelApiInfoSinglePageListDto pageDto) { + //获取考勤组 + Map groupMap = getGroupIds(pageDto.getGroupIds(), Boolean.TRUE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getMonth(), pageDto.getMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.dailySituationPageList(page, new ArrayList<>(groupMap.keySet()), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + List userList = v2UserApi.userNameAndCopy(listVoPage.getRecords().stream().map(DailySituationPageListVo::getUserName).distinct().collect(Collectors.toList())); + Map userMap = CollUtil.isNotEmpty(userList) ? userList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)) : Map.of(); + listVoPage.getRecords().forEach(item -> { + UserBoundVO userBoundVO = userMap.getOrDefault(item.getUserName(), new UserBoundVO()); + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + item.setUserName(StrUtil.isBlank(userBoundVO.getUserName()) ? "---" : userBoundVO.getUserName()); + item.setPhoneNumber(StrUtil.isBlank(userBoundVO.getPhone()) ? "---" : userBoundVO.getPhone()); + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void dailySituationExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = dailySituationPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤日常情况.xlsx", "考勤日常情况", DailySituationPageListVo.class, dataList); + } + + @Override + public PageListVO workHoursRankingPageList(PersonnelApiInfoSinglePageListDto pageDto) { + //获取考勤组 + Map groupMap = getGroupIds(pageDto.getGroupIds(), Boolean.TRUE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getMonth(), pageDto.getMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.workHoursRankingPageList(page, new ArrayList<>(groupMap.keySet()), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + List userList = v2UserApi.userNameAndCopy(listVoPage.getRecords().stream().map(WorkHoursRankingPageListVo::getUserName).distinct().collect(Collectors.toList())); + Map userMap = CollUtil.isNotEmpty(userList) ? userList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)) : Map.of(); + listVoPage.getRecords().forEach(item -> { + UserBoundVO userBoundVO = userMap.getOrDefault(item.getUserName(), new UserBoundVO()); + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + item.setUserName(StrUtil.isBlank(userBoundVO.getUserName()) ? "---" : userBoundVO.getUserName()); + item.setPhoneNumber(StrUtil.isBlank(userBoundVO.getPhone()) ? "---" : userBoundVO.getPhone()); + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void workHoursRankingExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = workHoursRankingPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤工时排行.xlsx", "考勤工时排行", WorkHoursRankingPageListVo.class, dataList); + } + + @Override + public PageListVO fullAttendanceStatusPageList(PersonnelApiInfoSinglePageListDto pageDto) { + //获取考勤组 + Map groupMap = getGroupIds(pageDto.getGroupIds(), Boolean.TRUE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getMonth(), pageDto.getMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.fullAttendanceStatusPageList(page, new ArrayList<>(groupMap.keySet()), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + listVoPage.getRecords().forEach(item -> { + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + // 设置全勤占比 + if (item.getFullAttendCount() > 0) { + item.setFullAttendPercent(BigDecimal.valueOf(item.getFullAttendCount()).divide(BigDecimal.valueOf(item.getGroupCount()), 4, + RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP)); + } else { + item.setFullAttendPercent(BigDecimal.ZERO); + } + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void fullAttendanceStatusExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = fullAttendanceStatusPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤全勤情况.xlsx", "考勤全勤情况", FullAttendanceStatusPageListVo.class, dataList); + } + + @Override + public PageListVO exceptionSituationPageList(PersonnelApiInfoSinglePageListDto pageDto) { + //获取考勤组 + Map groupMap = getGroupIds(pageDto.getGroupIds(), Boolean.TRUE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getMonth(), pageDto.getMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.exceptionSituationPageList(page, new ArrayList<>(groupMap.keySet()), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + List userList = v2UserApi.userNameAndCopy(listVoPage.getRecords().stream().map(ExceptionSituationPageListVo::getUserName).distinct().collect(Collectors.toList())); + Map userMap = CollUtil.isNotEmpty(userList) ? userList.stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)) : Map.of(); + listVoPage.getRecords().forEach(item -> { + UserBoundVO userBoundVO = userMap.getOrDefault(item.getUserName(), new UserBoundVO()); + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + item.setUserName(StrUtil.isBlank(userBoundVO.getUserName()) ? "---" : userBoundVO.getUserName()); + item.setPhoneNumber(StrUtil.isBlank(userBoundVO.getPhone()) ? "---" : userBoundVO.getPhone()); + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void exceptionSituationExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = exceptionSituationPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤异常情况.xlsx", "考勤异常情况", ExceptionSituationPageListVo.class, dataList); + } + + @Override + public PageListVO overtimeSituationPageList(PersonnelApiInfoSinglePageListDto pageDto) { + //获取考勤组 + Map groupMap = getGroupIds(pageDto.getGroupIds(), Boolean.TRUE); + if (CollUtil.isEmpty(groupMap)) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + MutablePair dateRange = DayStatisticsUtils.getDateBetweenByMonth(pageDto.getMonth(), pageDto.getMonth()); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + Page listVoPage = this.baseMapper.overtimeSituationPageList(page, new ArrayList<>(groupMap.keySet()), dateRange.getLeft(), dateRange.getRight()); + if (CollUtil.isEmpty(listVoPage.getRecords())) { + return DayStatisticsUtils.returnEmptyList(pageDto); + } + listVoPage.getRecords().forEach(item -> { + item.setGroupName(groupMap.getOrDefault(item.getGroupName(), new AttendanceGroup()).getGroupName()); + }); + return DayStatisticsUtils.returnList(listVoPage, pageDto); + } + + @Override + public void overtimeSituationExport(HttpServletResponse response, PersonnelApiInfoSinglePageListDto pageDto) throws Exception { + pageDto.setPageSize(100000L); + PageListVO pageListVoList = overtimeSituationPageList(pageDto); + List dataList = CollUtil.isNotEmpty(pageListVoList.getList()) ? pageListVoList.getList() : new ArrayList<>(); + EasyExcelUtil.export(response, "考勤加班情况.xlsx", "考勤加班情况", OvertimeSituationPageListVo.class, dataList); + } + + @Override + public QueryStatisticsInfoVo queryUserStatisticsInfo(QueryStatisticsInfoDto dto) { + QueryStatisticsInfoVo infoVo = new QueryStatisticsInfoVo(); + DateRangeDto dateRangeDto = new DateRangeDto(dto.getMonth(), Boolean.TRUE); + DayStatisticsQueryDbVo dbVo = this.baseMapper.queryUserStatisticsInfo(dto.getUserId(), dateRangeDto); + if (Objects.isNull(dbVo)) { + return infoVo; + } + BeanUtils.copyProperties(dbVo, infoVo); + // 设置请假次数 + infoVo.setLeaveTimes(StrUtil.isNotBlank(dbVo.getLeaveTimes()) ? dbVo.getLeaveTimes().split(",").length : 0); + if (infoVo.getLeaveTimes() > 0) { + // 解析请假数据,得到各个请假类型的次数 + List leaveList = DayStatisticsUtils.getMonthCustomLeaveList(dbVo.getCustomLeaveList()); + infoVo.setLeaveTypeList(leaveList.stream().map(item -> + LeaveTypeTimes.builder() + .typeName(item.getFieldName()) + .times(item.getApplyList().size()) + .build() + ).collect(Collectors.toList())); + } + return infoVo; + } + + @Override + public QueryGroupStatisticsVo queryGroupStatistics(QueryGroupStatisticsDto dto) { + QueryGroupStatisticsVo queryGroupStatisticsVo = new QueryGroupStatisticsVo(); + DateRangeDto dateRangeDto = new DateRangeDto(dto.getMonth(), Boolean.TRUE); + List groupList = this.groupService.queryListIncludeDeleteByIds(List.of(dto.getGroupId())); + if (CollUtil.isEmpty(groupList)) { + log.info("未查询到考勤组数据:{}", dto.getGroupId()); + throw new RuntimeException("不存在该考勤组"); + } + queryGroupStatisticsVo.setGroupName(groupList.get(0).getGroupName()); + List filterList = groupList.stream().map(item -> GroupFilterDto.builder().groupId(item.getId()).team(0).build()).collect(Collectors.toList()); + List userBoundVoList = getUserIdArr(filterList, dateRangeDto.getStartDate(), dateRangeDto.getEndDate(), null, "0"); + if (CollUtil.isEmpty(userBoundVoList)) { + log.info("未查询到用户数据:{}", dto.getGroupId()); + throw new RuntimeException("未查询到用户数据或者不具备权限"); + } + StatisticsDataQueryDto queryDto = StatisticsDataQueryDto.builder() + .groupIds(List.of(dto.getGroupId())) + .startDate(dateRangeDto.getStartDate()) + .endDate(dateRangeDto.getEndDate()) + .userIds(userBoundVoList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList())) + .build(); + StatisticsDataQueryVo dataQuery = this.baseMapper.getMonthStatisticsDataQuery(queryDto, Boolean.FALSE); + if (Objects.isNull(dataQuery)) { + return queryGroupStatisticsVo; + } + BeanUtils.copyProperties(dataQuery, queryGroupStatisticsVo); + return queryGroupStatisticsVo; + } + + @Override + public QueryUserOvertimeVo queryUserOvertime(QueryStatisticsInfoDto dto) { + QueryUserOvertimeVo overtimeVo = new QueryUserOvertimeVo(); + DateRangeDto dateRangeDto = new DateRangeDto(dto.getMonth(), Boolean.TRUE); + List userBoundVos = userApi.getStaffRosterListInfoByIds(List.of(dto.getUserId())); + if (CollUtil.isEmpty(userBoundVos)) { + log.info("未查询到用户数据:{}", dto.getUserId()); + throw new RuntimeException("不存在该用户"); + } + overtimeVo.setUserName(userBoundVos.get(0).getRealName()); + List overtimeDetailsList = this.baseMapper.queryUserOvertime(dto.getUserId(), dateRangeDto); + if (CollUtil.isEmpty(overtimeDetailsList)) { + return overtimeVo; + } + // 加班小时数 + overtimeVo.setTotalHours(overtimeDetailsList.stream().map(QueryUserOvertimeDetailsDbVo::getHours).reduce(BigDecimal::add).orElse(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP)); + List shiftsJsonVoList = new ArrayList<>(); + for (QueryUserOvertimeDetailsDbVo item : overtimeDetailsList) { + List shiftsList = parseArray(item.getShiftsJson(), DayStatisticsShiftsJsonVo.class); + if (CollUtil.isEmpty(shiftsJsonVoList)) { + shiftsJsonVoList = new ArrayList<>(); + } + QueryUserOvertimeDetailsVo detailsVo = new QueryUserOvertimeDetailsVo(); + detailsVo.setDay(item.getDay()); + detailsVo.setHours(item.getHours()); + // 获取加班时间 + detailsVo.setOvertimeTime(getOvertimeTime(item.getDay(), shiftsList)); + shiftsJsonVoList.addAll(shiftsList); + } + // 加班次数 + overtimeVo.setTotalCount(shiftsJsonVoList.size()); + return null; + } + + + /** + * 获取加班时间 + * + * @param day 加班日期 + * @param shiftsList 排班班次 + * @return 加班时间 + */ + private String getOvertimeTime(Date day, List shiftsList) { + StringBuilder shiftTimeStrList = new StringBuilder(); + for (DayStatisticsShiftsJsonVo shiftsJsonVo : shiftsList) { + String inTime = DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm"); + String outTime = DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm"); + shiftTimeStrList.append(inTime).append("~"); + if (!DayStatisticsUtils.isSameDay(day, shiftsJsonVo.getOutPoint())) { + String timeRange = "(次日)".concat(outTime); + shiftTimeStrList.append(timeRange).append("|"); + } else { + shiftTimeStrList.append(outTime).append("|"); + } + } + return shiftTimeStrList.length() > 0 ? shiftTimeStrList.substring(0, shiftTimeStrList.length() - 1) : ""; + } + + /** + * 获取开启提醒的考勤组 + * + * @param teamMonthStatisticsList 考勤组统计信息 + * @return List + */ + private List getObtainPermissionGroupList(List teamMonthStatisticsList) { + List groupIdList = teamMonthStatisticsList.stream().map(TeamMonthStatisticsNoticeQueryVo::getGroupId).distinct().collect(Collectors.toList()); + List groupSettingList = userSettingService.getSettingList(groupIdList, UserSettingTypeEnum.GROUP.getCode(), Lists.newArrayList()); + List groupIds = groupSettingList.stream().map(UserSettingVo::getAssociationId).collect(Collectors.toList()); + return CollUtil.isEmpty(groupIds) ? Lists.newArrayList() : teamMonthStatisticsList.stream().filter(m -> groupIds.contains(m.getGroupId())).collect(Collectors.toList()); + } + + /** + * 封装日统计消息提醒数据 + * + * @param statisticsQueryVoList 用户日统计数据 + * @return 日统计消息提醒数据 + */ + public Map getDayNoticeModelMap(List statisticsQueryVoList) { + Map> dayStatisticsShiftsJsonMap = new HashMap<>(); + List shiftIdList = new ArrayList<>(); + for (DayStatisticsNoticeQueryVo item : statisticsQueryVoList) { + List shiftsJsonVoList = parseArray(item.getShiftsJson(), DayStatisticsShiftsJsonVo.class); + if (CollUtil.isNotEmpty(shiftsJsonVoList)) { + shiftIdList.addAll(shiftsJsonVoList.stream().map(DayStatisticsShiftsJsonVo::getShiftId).collect(Collectors.toList())); + } + dayStatisticsShiftsJsonMap.put(item.getUserId(), CollUtil.isNotEmpty(shiftsJsonVoList) ? shiftsJsonVoList : new ArrayList<>()); + } + if (CollUtil.isNotEmpty(shiftIdList)) { + shiftIdList = shiftIdList.stream().distinct().collect(Collectors.toList()); + } + Map shiftNameEntityMap = CollUtil.isEmpty(shiftIdList) ? new HashMap<>() : dailyRuleService1.getShiftByShiftIds(shiftIdList); + Map shiftsJsonStrVoMap = new HashMap<>(); + statisticsQueryVoList.forEach(item -> { + //处理班次、班次时间、打卡时间 + DayNoticeModel dayNoticeModel = new DayNoticeModel(); + List shiftsJsonVoList = dayStatisticsShiftsJsonMap.containsKey(item.getUserId()) ? dayStatisticsShiftsJsonMap.get(item.getUserId()) : Lists.newArrayList(); + List collect = getSchedulesItemVos(shiftNameEntityMap, shiftsJsonVoList); + dayNoticeModel.setShiftList(CollUtil.isNotEmpty(collect) ? collect.stream().map(SchedulesItemVo::getShortName).distinct().collect(Collectors.toList()) : Lists.newArrayList()); + //处理班次时间 + List shiftTimeList = new ArrayList<>(); + List shiftsJsonVoFilterList = getShiftsTimeList(item, shiftsJsonVoList, shiftTimeList); + dayNoticeModel.setShiftTimeList(shiftTimeList); + //处理打卡时间 + List clockTimeList = new ArrayList<>(); + shiftsJsonVoFilterList.forEach(shiftsJsonVo -> { + if (CollUtil.isNotEmpty(shiftsJsonVo.getClockInResultList())) { + if (DayStatisticsUtils.isShiftsJsonAbsence1(shiftsJsonVo.getClockInResultList())) { + clockTimeList.add("旷工"); + } else { + StringBuilder sb = new StringBuilder(); + List onClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList()); + ShiftsJsonClockInResultVo on = CollUtil.isNotEmpty(onClockInResultList) ? onClockInResultList.get(0) : null; + List offClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList()); + ShiftsJsonClockInResultVo off = CollUtil.isNotEmpty(offClockInResultList) ? offClockInResultList.get(0) : null; + sb.append(DayStatisticsUtils.getClockInResultStr(shiftsJsonVo.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()), item, on, off)); + clockTimeList.add(sb.toString()); + } + } + }); + dayNoticeModel.setClockTimeList(clockTimeList); + DayClockRange dayClockRange = dailyRuleService1.getDayClockRange(item.getUserId(), item.getDate()); + dayNoticeModel.setClockTime(clockInService.getDailyClockInRecord(item.getUserId(), dayClockRange).size()); + dayNoticeModel.setShouldAttendHours(item.getShouldAttendHours()); + dayNoticeModel.setActualAttendHours(item.getActualAttendHours()); + dayNoticeModel.setWorkingHours(item.getEffectiveAttendHours()); + dayNoticeModel.setLateTimes(item.getLateTimes()); + dayNoticeModel.setLateMinutes(item.getLateMinutes()); + dayNoticeModel.setEarlyLeaveTimes(item.getEarlyLeaveTimes()); + dayNoticeModel.setEarlyLeaveMinutes(item.getEarlyLeaveMinutes()); + dayNoticeModel.setAbsenceCardTimes(item.getAbsenceCardTimes()); + dayNoticeModel.setAbsenceCardHours(item.getAbsenceCardHours()); + dayNoticeModel.setAbsenceTimes(item.getAbsenceTimes()); + dayNoticeModel.setAbsenceHours(item.getAbsenceHours()); + dayNoticeModel.setLeaveHours(item.getLeaveHours()); + dayNoticeModel.setLeaveDays(item.getLeaveDays()); + dayNoticeModel.setOvertimeHours(item.getOvertimeHours()); + dayNoticeModel.setBusDays(StringUtil.isNotEmpty(item.getBusBatchNumber()) ? 1 : 0); + dayNoticeModel.setOutDays(item.getOutDays()); + dayNoticeModel.setOutHours(item.getOutHours()); + shiftsJsonStrVoMap.put(item.getId(), dayNoticeModel); + }); + return shiftsJsonStrVoMap; + } + + /** + * 封装用户月统计提醒数据 + * + * @param month 月份 + * @param monthStatisticsList 月统计数据 + * @return 月统计提醒数据 + * @throws Exception 异常问题 + */ + private Map getMonthNoticeModelMap(String month, List monthStatisticsList) throws Exception { + List userIds = monthStatisticsList.stream().map(MonthStatisticsNoticeQueryVo::getUserId).distinct().collect(Collectors.toList()); + List groupIds = monthStatisticsList.stream().map(MonthStatisticsNoticeQueryVo::getGroupId).distinct().collect(Collectors.toList()); + //批量获取用户加入考勤组周期 + Map>> userCycleMap = CollUtil.isNotEmpty(monthStatisticsList) ? userService.batchGetUserCycleList(new DateRangeDto(month, Boolean.TRUE), groupIds, userIds) : new HashMap<>(); + Map monthNoticeModelMap = new HashMap<>(); + for (MonthStatisticsNoticeQueryVo item : monthStatisticsList) { + MonthNoticeModel monthNoticeModel = MonthNoticeModel.builder().build(); + BeanUtils.copyProperties(item, monthNoticeModel); + //设置考勤组周期 + if (userCycleMap.containsKey(item.getGroupId()) && userCycleMap.get(item.getGroupId()).containsKey(item.getUserId())) { + StringBuilder cycleStr = new StringBuilder(); + userCycleMap.get(item.getGroupId()).get(item.getUserId()).forEach(cycle -> cycleStr.append(cycle).append("|")); + monthNoticeModel.setCycleDate(cycleStr.length() > 0 ? cycleStr.substring(0, cycleStr.length() - 1) : ""); + monthNoticeModel.setBusTimes(StrUtil.isNotBlank(item.getBusTimes()) ? item.getBusTimes().split(",").length : 0); + //设置外出次数 + monthNoticeModel.setOutTimes(StrUtil.isNotBlank(item.getOutTimes()) ? item.getOutTimes().split(",").length : 0); + } + monthNoticeModelMap.put(item.getId(), monthNoticeModel); + } + return monthNoticeModelMap; + } + + @Override + public Map queryUserWorkSituation(UserWorkSituationDto dto) { + // 初始化结果Map,为所有用户ID设置默认值 + Map resultMap = new HashMap<>(); + if (CollUtil.isEmpty(dto.getUserIds())) { + return resultMap; + } + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusDays(dto.getConfigDays() + 1); + // 获取连续排班周信息 + MutablePair weekRange = getWeekRangeByDate(LocalDate.now()); + List dataList = this.baseMapper.queryUserWorkSituation(dto.getUserIds(), weekRange.getLeft().isAfter(startDate) ? startDate : weekRange.getLeft(), weekRange.getRight()); + Map> userStaMap = CollUtil.isNotEmpty(dataList) ? dataList.stream().collect(Collectors.groupingBy(UserWorkSituationDbVo::getUserId)) : new HashMap<>(); + dto.getUserIds().forEach(item -> { + List userStaList = userStaMap.getOrDefault(item, new ArrayList<>()); + Map userDateMap = userStaList.stream() + .collect(Collectors.toMap(vo -> vo.getDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), + vo -> vo, (existing, replacement) -> existing)); + int continuousDays = calculateContinuousDays(endDate, userDateMap); + // 排除掉比开始时间都还早的数据 + userDateMap = userDateMap.entrySet().stream() + .filter(entry -> !entry.getKey().isBefore(weekRange.getLeft())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + MutablePair weekWorkSta = getWeekStaData(weekRange.getRight(), userDateMap); + UserWorkSituationVo userWorkSituationVo = UserWorkSituationVo.builder() + .continuousDays(continuousDays) + .weekWorkDays(weekWorkSta.getLeft()) + .weekWorkHours(weekWorkSta.getRight()) + .build(); + resultMap.put(item, userWorkSituationVo); + }); + return resultMap; + } + + /** + * 计算连续排班天数 + * + * @param today 今天 + * @param userDateMap 用户排班数据 + * @return 连续排班天数 + */ + private int calculateContinuousDays(LocalDate today, Map userDateMap) { + int continuousDays = 0; + LocalDate checkDate = today; + while (true) { + UserWorkSituationDbVo dayData = userDateMap.get(checkDate); + if (dayData != null && dayData.getIsScheduling() != null && dayData.getIsScheduling() == 1) { + continuousDays++; + checkDate = checkDate.minusDays(1); + } else { + break; + } + } + + return continuousDays; + } + + /** + * 获取本周内连续排班天数以及小时数 + * + * @param today 今天 + * @param userDateMap 用户排班数据 + * @return 周数据 + */ + private MutablePair getWeekStaData(LocalDate today, Map userDateMap) { + int continuousDays = 0; + BigDecimal continuousHours = BigDecimal.ZERO; + LocalDate checkDate = today; + while (true) { + UserWorkSituationDbVo dayData = userDateMap.get(checkDate); + if (dayData != null && dayData.getIsScheduling() != null && dayData.getIsScheduling() == 1) { + continuousDays++; + if (dayData.getAttendanceHours() != null) { + continuousHours = continuousHours.add(dayData.getAttendanceHours()); + } + checkDate = checkDate.minusDays(1); + } else { + break; + } + } + return MutablePair.of(continuousDays, continuousHours); + } + + private MutablePair getWeekRangeByDate(LocalDate date) { + LocalDate weekStart = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDate weekEnd = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); + return MutablePair.of(weekStart, weekEnd.isBefore(date) ? weekEnd : date); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsUtilImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsUtilImpl.java new file mode 100644 index 0000000..a8423d7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceDayStatisticsUtilImpl.java @@ -0,0 +1,415 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.mapper.AttendanceClockInMapper; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.mapper.AttendanceLeaveApproveMapper; +import jnpf.attendance.service.*; +import jnpf.database.model.TenantVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.*; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.exception.LoginException; +import jnpf.model.attendance.event.StatisticsSingleDto; +import jnpf.model.attendance.event.StatisticsSingleHistoryDto; +import jnpf.model.attendance.model.ClockInResult; +import jnpf.model.attendance.model.LeaveSituationData; +import jnpf.model.attendance.model.LeaveTypeStaModel; +import jnpf.model.attendance.model.UserDaySituationData; +import jnpf.model.attendance.vo.ClockInVo; +import jnpf.model.attendance.vo.attendance.DayStatisticsShiftsJsonVo; +import jnpf.model.attendance.vo.attendance.OutOrBusApproveVo; +import jnpf.model.attendance.vo.attendance.ShiftsJsonClockInResultVo; +import jnpf.util.RandomUtil; +import jnpf.util.StringUtil; +import jnpf.util.attendance.DayStatisticsUtils; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.Lists; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +import static cn.hutool.core.collection.CollUtil.newArrayList; + +@Slf4j +@Service +public class AttendanceDayStatisticsUtilImpl { + @Resource + private AttendanceUserService groupUsers; + @Resource + private AttendanceClockInMapper clockInMapper; + @Resource + private AttendanceDailyRuleService ruleService; + @Resource + private AttendanceBaseSettingService baseSettingService; + @Resource + private AttendanceLeaveApproveMapper leaveApproveMapper; + @Resource + private AttendanceDayStatisticsService statisticsService; + @Resource + private AttendanceClockInResultMapper clockInResultsMapper; + @Resource + private AttendanceLineSchedulingPayrollHoursService hoursService; + + /** + * 获取考勤组出勤换算比 + * + * @param groupId 考勤组ID + * @return 出勤换算比 + */ + private AttendanceBaseSetting getAttendanceRatio(String groupId) { + Map groupBaseSetting = baseSettingService.getEnableBaseSettingAll(CollUtil.newArrayList(groupId)); + return groupBaseSetting.get(groupId); + } + + /** + * 生成用户日统计数据 + * + */ + @DSTransactional + public void createDayStatisticsData(StatisticsSingleDto singleDto) throws LoginException { + Date start = DateUtil.beginOfDay(singleDto.getDay()); + Date end = DateUtil.endOfDay(singleDto.getDay()); + AttendanceBaseSetting attendanceRatio1 = getAttendanceRatio(singleDto.getGroupId()); + BigDecimal attendanceRatio = Objects.isNull(attendanceRatio1) ? BigDecimal.valueOf(8) : attendanceRatio1.getAttendanceRatio(); + List groupUsers = this.groupUsers.getAttendanceGroupUsersOfSecondment(start, end, newArrayList(singleDto.getUserId()), CollUtil.newArrayList(singleDto.getGroupId())); + if (ruleService.isExistStatus(groupUsers, singleDto.getDay()) <= 0) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(AttendanceDayStatistics::getGroupId, singleDto.getGroupId()); + queryWrapper.eq(AttendanceDayStatistics::getUserId, singleDto.getUserId()); + queryWrapper.eq(AttendanceDayStatistics::getDate, singleDto.getDay()); + this.statisticsService.remove(queryWrapper); + return; + } + AttendanceGroupUser groupUser = groupUsers.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + // 查询排班数据 + List dailyRuleList = ruleService.getAttendanceDayRulesForStatistic(singleDto.getGroupId(), singleDto.getUserId(), singleDto.getDay()); + // 查询当天的请假申请数据 + List leaveForApplyIds = leaveApproveMapper.getUserLeaveList(singleDto.getGroupId(), singleDto.getUserId(), start, end); + // 判断当天是否为划线排班 + boolean underlineScheduling = dailyRuleList.stream().anyMatch(item -> item.getFixedMark().equals(2)); + // 查询当天划线排班的计薪工时占比 MutablePair<计薪工时,占比> + MutablePair payrollProportion = MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO); + if (underlineScheduling && CollUtil.isNotEmpty(dailyRuleList)) { + int payrollMinute = dailyRuleList.stream().map(FtbAttendanceDailyRule::getValidDuration).mapToInt(Integer::intValue).sum(); + FtbAttendanceLineSchedulingPayrollHours dayPayrollHours = hoursService.listByUserIdsAndDays(singleDto.getUserId(), singleDto.getGroupId(), singleDto.getDay()); + BigDecimal payrollHours; + if (Objects.isNull(dayPayrollHours) || Objects.isNull(dayPayrollHours.getPayrollHours())) { + payrollHours = new BigDecimal(payrollMinute).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); + } else { + payrollHours = dayPayrollHours.getPayrollHours(); + } + if (payrollHours.compareTo(BigDecimal.ZERO) > 0 && payrollMinute > 0) { + payrollProportion = MutablePair.of(payrollHours, payrollHours.divide(BigDecimal.valueOf(payrollMinute), 6, RoundingMode.HALF_UP)); + } + } + if (CollUtil.isEmpty(dailyRuleList) && CollUtil.isEmpty(leaveForApplyIds)) { + handleNailData(singleDto.getGroupId(), CollUtil.newArrayList(singleDto.getUserId()), singleDto.getDay()); + return; + } + // 查询打卡结果 + List ruleIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + List clockInResults = CollUtil.isEmpty(ruleIds) ? CollUtil.newArrayList() : clockInResultsMapper.selectList(new LambdaQueryWrapper().in(AttendanceClockInResult::getRuleId, ruleIds).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE)); + Map> clockInResultMap = clockInResults.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getRuleId)); + // 查询有效打卡记录 + List clockInIds = clockInResults.stream().map(AttendanceClockInResult::getClockInId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); + List clockInResultList = CollUtil.isEmpty(clockInIds) ? CollUtil.newArrayList() : clockInMapper.getClockInResultByRuleList(ruleIds); + Map clockInMap = CollUtil.isNotEmpty(clockInResultList) ? clockInResultList.stream().filter(m -> Objects.nonNull(m.getClockInTime())).collect(Collectors.toMap(ClockInVo::getClockInId, a -> a, (k1, k2) -> k1)) : new HashMap<>(); + // 按照请假数据分组(申请通过就确定了,调整排班不影响结果) + Map> leaveTypeMap = CollUtil.isNotEmpty(leaveForApplyIds) ? leaveForApplyIds.stream().collect(Collectors.groupingBy(LeaveSituationData::getLeaveId)) : new HashMap<>(); + Map leaveTypeNameMap = CollUtil.isNotEmpty(leaveForApplyIds) ? leaveForApplyIds.stream().collect(Collectors.toMap(LeaveSituationData::getLeaveId, LeaveSituationData::getLeaveName, (k1, k2) -> k1)) : new HashMap<>(); + // 按照请假类型分组获取用户请假数据(申请通过就确定了,调整排班不影响结果) + Map leaveTypeStaMap = DayStatisticsUtils.getUserLeaveTypeSum(leaveTypeMap, start, end); + // 获取外出、出差时长(申请通过就确定了,调整排班不影响结果) + List busOrOutApproveList = this.ruleService.getUserDayBusAndOutInfo(singleDto.getUserId(), singleDto.getGroupId(), singleDto.getDay(), List.of(AttendanceTypeEnum.BUSINESS_TRIP, AttendanceTypeEnum.STEP_OUT)); + Map> shiftsMap = new HashMap<>(); + List ruleAndClockList = dailyRuleList.stream().map(rule -> { + UserDaySituationData situationData = BeanUtil.toBean(rule, UserDaySituationData.class); + situationData.setRuleId(rule.getId()); + situationData.setIsScheduling(1); + List inResultList = clockInResultMap.containsKey(rule.getId()) ? clockInResultMap.get(rule.getId()) : CollUtil.newArrayList(); + if (Objects.nonNull(inResultList)) { + List collect1 = inResultList.stream().map(clockInResult -> { + ClockInResult clockInResult1 = ClockInResult.builder().build(); + BeanUtils.copyProperties(clockInResult, clockInResult1); + clockInResult1.setClockInTime(clockInMap.containsKey(clockInResult.getClockInId()) ? clockInMap.get(clockInResult.getClockInId()).getClockInTime() : null); + return clockInResult1; + }).collect(Collectors.toList()); + situationData.setClockInResultList(collect1); + shiftsMap.put(rule.getId(), collect1.stream().map(item -> { + ShiftsJsonClockInResultVo clockInResultVo = ShiftsJsonClockInResultVo.builder().build(); + BeanUtils.copyProperties(item, clockInResultVo); + return clockInResultVo; + }).collect(Collectors.toList())); + } + return situationData; + }).collect(Collectors.toList()); + AttendanceDayStatistics dayStaVo = new AttendanceDayStatistics(); + dayStaVo.setAttendanceRatio(attendanceRatio); + dayStaVo.setDate(singleDto.getDay()); + dayStaVo.setUserId(singleDto.getUserId()); + dayStaVo.setGroupId(singleDto.getGroupId()); + dayStaVo.setIsScheduling(0); + dayStaVo.setCreatorTime(new Date()); + // 处理各项统计数据 + computeDayStatisticsData(dayStaVo, attendanceRatio, payrollProportion, ruleAndClockList, busOrOutApproveList, leaveTypeStaMap); + dayStaVo.setJoinTime(Objects.nonNull(groupUser) ? groupUser.getCreatorTime() : null); + dayStaVo.setSelfGroup(ruleService.isExistStatus(groupUsers, singleDto.getDay()) > 1 ? 1 : 0); + List shiftsJsonVos = Lists.newArrayList(); + List dailyRulesFilter = dailyRuleList.stream().filter(rule -> Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.REST.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.HOLIDAYS.getCode(), rule.getAttendanceType())).collect(Collectors.toList()); + dailyRulesFilter.forEach(item -> { + DayStatisticsShiftsJsonVo shiftsJsonVo = DayStatisticsShiftsJsonVo.builder().build(); + BeanUtils.copyProperties(item, shiftsJsonVo); + shiftsJsonVo.setRuleId(item.getId()); + if (shiftsMap.containsKey(item.getId())) { + shiftsJsonVo.setClockInResultList(shiftsMap.containsKey(item.getId()) ? shiftsMap.get(item.getId()) : CollUtil.newArrayList()); + } + shiftsJsonVos.add(shiftsJsonVo); + }); + dayStaVo.setShiftsJson(JSON.toJSONString(shiftsJsonVos)); + dayStaVo.setIsScheduling(CollUtil.isEmpty(dailyRulesFilter) ? 0 : 1); + // 是否需要打卡 + boolean anyMatch = dailyRulesFilter.stream().anyMatch(item -> item.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || item.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())); + dayStaVo.setIsClocked(anyMatch ? 1 : 0); + dayStaVo.setCustomLeaveJson(DayStatisticsUtils.getCustomLeaveJson(leaveTypeNameMap, leaveTypeStaMap)); + //获取提醒时间 + Date date = ruleService.getDayEndRuleDeletionDate(singleDto.getUserId(), singleDto.getDay()); + if (Objects.nonNull(date)) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.MINUTE, 2); + dayStaVo.setRemindTime(calendar.getTime()); + } + // 明确指定更新条件,避免默认行为 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(AttendanceDayStatistics::getGroupId, singleDto.getGroupId()) + .eq(AttendanceDayStatistics::getUserId, singleDto.getUserId()) + .eq(AttendanceDayStatistics::getDate, singleDto.getDay()); + // 仅在插入时需要设置ID + if (statisticsService.count(updateWrapper) == 0) { + dayStaVo.setId(RandomUtil.uuId()); + } + statisticsService.saveOrUpdate(dayStaVo, updateWrapper); + } + + @DSTransactional + public void createDayStatisticsDataHistory(StatisticsSingleHistoryDto singleDto) throws LoginException { + Date start = DateUtil.beginOfDay(singleDto.getDay()); + Date end = DateUtil.endOfDay(singleDto.getDay()); + AttendanceBaseSetting attendanceRatio1 = getAttendanceRatio(singleDto.getGroupId()); + BigDecimal attendanceRatio = Objects.isNull(attendanceRatio1) ? BigDecimal.valueOf(8) : attendanceRatio1.getAttendanceRatio(); + List groupUsers = this.groupUsers.getAttendanceGroupUsersOfSecondment(start, end, newArrayList(singleDto.getUserId()), CollUtil.newArrayList(singleDto.getGroupId())); + if (ruleService.isExistStatus(groupUsers, singleDto.getDay()) <= 0) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(AttendanceDayStatistics::getGroupId, singleDto.getGroupId()); + queryWrapper.eq(AttendanceDayStatistics::getUserId, singleDto.getUserId()); + queryWrapper.eq(AttendanceDayStatistics::getDate, singleDto.getDay()); + this.statisticsService.remove(queryWrapper); + return; + } + AttendanceGroupUser groupUser = groupUsers.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + // 查询排班数据 + List dailyRuleList = ruleService.getAttendanceDayRulesForStatistic(singleDto.getGroupId(), singleDto.getUserId(), singleDto.getDay()); + // 查询当天的请假申请数据 + List leaveForApplyIds = leaveApproveMapper.getUserLeaveList(singleDto.getGroupId(), singleDto.getUserId(), start, end); + // 判断当天是否为划线排班 + boolean underlineScheduling = dailyRuleList.stream().anyMatch(item -> item.getFixedMark().equals(2)); + // 查询当天划线排班的计薪工时占比 MutablePair<计薪工时,占比> + MutablePair payrollProportion = MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO); + if (underlineScheduling && CollUtil.isNotEmpty(dailyRuleList)) { + int payrollMinute = dailyRuleList.stream().map(FtbAttendanceDailyRule::getValidDuration).mapToInt(Integer::intValue).sum(); + FtbAttendanceLineSchedulingPayrollHours dayPayrollHours = hoursService.listByUserIdsAndDays(singleDto.getUserId(), singleDto.getGroupId(), singleDto.getDay()); + BigDecimal payrollHours; + if (Objects.isNull(dayPayrollHours) || Objects.isNull(dayPayrollHours.getPayrollHours())) { + payrollHours = new BigDecimal(payrollMinute).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); + } else { + payrollHours = dayPayrollHours.getPayrollHours(); + } + if (payrollHours.compareTo(BigDecimal.ZERO) > 0 && payrollMinute > 0) { + payrollProportion = MutablePair.of(payrollHours, payrollHours.divide(BigDecimal.valueOf(payrollMinute), 6, RoundingMode.HALF_UP)); + } + } + if (CollUtil.isEmpty(dailyRuleList) && CollUtil.isEmpty(leaveForApplyIds)) { + handleNailData(singleDto.getGroupId(), CollUtil.newArrayList(singleDto.getUserId()), singleDto.getDay()); + return; + } + // 查询打卡结果 + List ruleIds = dailyRuleList.stream().map(FtbAttendanceDailyRule::getId).collect(Collectors.toList()); + List clockInResults = CollUtil.isEmpty(ruleIds) ? CollUtil.newArrayList() : clockInResultsMapper.selectList(new LambdaQueryWrapper().in(AttendanceClockInResult::getRuleId, ruleIds).eq(AttendanceClockInResult::getDeleteMark, Boolean.FALSE)); + Map> clockInResultMap = clockInResults.stream().collect(Collectors.groupingBy(AttendanceClockInResult::getRuleId)); + // 查询有效打卡记录 + List clockInIds = clockInResults.stream().map(AttendanceClockInResult::getClockInId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); + List clockInResultList = CollUtil.isEmpty(clockInIds) ? CollUtil.newArrayList() : clockInMapper.getClockInResultByRuleList(ruleIds); + Map clockInMap = CollUtil.isNotEmpty(clockInResultList) ? clockInResultList.stream().filter(m -> Objects.nonNull(m.getClockInTime())).collect(Collectors.toMap(ClockInVo::getClockInId, a -> a, (k1, k2) -> k1)) : new HashMap<>(); + Map leaveTypeNameMap = CollUtil.isNotEmpty(leaveForApplyIds) ? leaveForApplyIds.stream().collect(Collectors.toMap(LeaveSituationData::getLeaveId, LeaveSituationData::getLeaveName, (k1, k2) -> k1)) : new HashMap<>(); + // 按照请假类型分组获取用户请假数据 + Map leaveTypeMap = DayStatisticsUtils.getUserLeaveTypeSum(attendanceRatio, leaveForApplyIds, dailyRuleList); + // 获取外出、出差时长(申请通过就确定了,调整排班不影响结果) + List busOrOutApproveList = this.ruleService.getUserDayBusAndOutInfo(singleDto.getUserId(), singleDto.getGroupId(), singleDto.getDay(), List.of(AttendanceTypeEnum.BUSINESS_TRIP, AttendanceTypeEnum.STEP_OUT)); + Map> shiftsMap = new HashMap<>(); + List ruleAndClockList = dailyRuleList.stream().map(rule -> { + UserDaySituationData situationData = BeanUtil.toBean(rule, UserDaySituationData.class); + situationData.setRuleId(rule.getId()); + situationData.setIsScheduling(1); + List inResultList = clockInResultMap.containsKey(rule.getId()) ? clockInResultMap.get(rule.getId()) : CollUtil.newArrayList(); + if (Objects.nonNull(inResultList)) { + List collect1 = inResultList.stream().map(clockInResult -> { + ClockInResult clockInResult1 = ClockInResult.builder().build(); + BeanUtils.copyProperties(clockInResult, clockInResult1); + clockInResult1.setClockInTime(clockInMap.containsKey(clockInResult.getClockInId()) ? clockInMap.get(clockInResult.getClockInId()).getClockInTime() : null); + return clockInResult1; + }).collect(Collectors.toList()); + situationData.setClockInResultList(collect1); + shiftsMap.put(rule.getId(), collect1.stream().map(item -> { + ShiftsJsonClockInResultVo clockInResultVo = ShiftsJsonClockInResultVo.builder().build(); + BeanUtils.copyProperties(item, clockInResultVo); + return clockInResultVo; + }).collect(Collectors.toList())); + } + return situationData; + }).collect(Collectors.toList()); + AttendanceDayStatistics dayStaVo = new AttendanceDayStatistics(); + dayStaVo.setAttendanceRatio(attendanceRatio); + dayStaVo.setDate(singleDto.getDay()); + dayStaVo.setUserId(singleDto.getUserId()); + dayStaVo.setGroupId(singleDto.getGroupId()); + dayStaVo.setIsScheduling(0); + dayStaVo.setCreatorTime(new Date()); + // 处理各项统计数据 + computeDayStatisticsData(dayStaVo, attendanceRatio, payrollProportion, ruleAndClockList, busOrOutApproveList, leaveTypeMap); + dayStaVo.setJoinTime(Objects.nonNull(groupUser) ? groupUser.getCreatorTime() : null); + dayStaVo.setSelfGroup(ruleService.isExistStatus(groupUsers, singleDto.getDay()) > 1 ? 1 : 0); + List shiftsJsonVos = Lists.newArrayList(); + List dailyRulesFilter = dailyRuleList.stream().filter(rule -> Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.STEP_OUT.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.REST.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), rule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.HOLIDAYS.getCode(), rule.getAttendanceType())).collect(Collectors.toList()); + dailyRulesFilter.forEach(item -> { + DayStatisticsShiftsJsonVo shiftsJsonVo = DayStatisticsShiftsJsonVo.builder().build(); + BeanUtils.copyProperties(item, shiftsJsonVo); + shiftsJsonVo.setRuleId(item.getId()); + if (shiftsMap.containsKey(item.getId())) { + shiftsJsonVo.setClockInResultList(shiftsMap.containsKey(item.getId()) ? shiftsMap.get(item.getId()) : CollUtil.newArrayList()); + } + shiftsJsonVos.add(shiftsJsonVo); + }); + dayStaVo.setShiftsJson(JSON.toJSONString(shiftsJsonVos)); + dayStaVo.setIsScheduling(CollUtil.isEmpty(dailyRulesFilter) ? 0 : 1); + // 是否需要打卡 + boolean anyMatch = dailyRulesFilter.stream().anyMatch(item -> item.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || item.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())); + dayStaVo.setIsClocked(anyMatch ? 1 : 0); + dayStaVo.setCustomLeaveJson(DayStatisticsUtils.getCustomLeaveJson(leaveTypeNameMap, leaveTypeMap)); + //获取提醒时间 + Date date = ruleService.getDayEndRuleDeletionDate(singleDto.getUserId(), singleDto.getDay()); + if (Objects.nonNull(date)) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.add(Calendar.MINUTE, 2); + dayStaVo.setRemindTime(calendar.getTime()); + } + // 明确指定更新条件,避免默认行为 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(AttendanceDayStatistics::getGroupId, singleDto.getGroupId()) + .eq(AttendanceDayStatistics::getUserId, singleDto.getUserId()) + .eq(AttendanceDayStatistics::getDate, singleDto.getDay()); + // 仅在插入时需要设置ID + if (statisticsService.count(updateWrapper) == 0) { + dayStaVo.setId(RandomUtil.uuId()); + } + statisticsService.saveOrUpdate(dayStaVo, updateWrapper); + } + + /** + * 生成单个考勤组指定人员空白统计数据 + */ + @Transactional(rollbackFor = Exception.class) + public void handleNailData(String groupId, List userIds, Date day) { + Date date = new Date(); + AttendanceBaseSetting baseSetting = getAttendanceRatio(groupId); + BigDecimal attendanceRatio = Objects.isNull(baseSetting) ? BigDecimal.valueOf(8) : baseSetting.getAttendanceRatio(); + List statisticList = new ArrayList<>(); + Date start = DateUtil.beginOfDay(date); + Date end = DateUtil.endOfDay(date); + List attendanceGroupUsers = groupUsers.getAttendanceGroupUsersOfSecondment(start, end, userIds, CollUtil.newArrayList(groupId)); + Map> userJoinDateMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + for (String userId : userIds) { + if (!userJoinDateMap.containsKey(userId)) { + continue; + } + List attendanceGroupUserList = userJoinDateMap.get(userId); + AttendanceGroupUser attendanceGroupUser = attendanceGroupUserList.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).reduce((r1, r2) -> r2).orElse(null); + AttendanceDayStatistics dayStatistics = AttendanceDayStatistics.builder().id(RandomUtil.uuId()).attendanceRatio(attendanceRatio).date(day).userId(userId).groupId(groupId).selfGroup(ruleService.isExistStatus(attendanceGroupUserList, day) > 1 ? 1 : 0).joinTime(Objects.nonNull(attendanceGroupUser) ? attendanceGroupUser.getCreatorTime() : null).remindTime(DateUtil.endOfDay(day)).creatorTime(date).build(); + // 获取外出出差记录 + List busOrOutApproveVoList = this.ruleService.getUserDayBusAndOutInfo(userId, groupId, day, List.of(AttendanceTypeEnum.BUSINESS_TRIP, AttendanceTypeEnum.STEP_OUT)); + // 获取外出出差时长 + DayStatisticsUtils.processBusOrOutData(busOrOutApproveVoList, dayStatistics, attendanceRatio); + statisticList.add(dayStatistics); + } + //按照用户+考勤组+日期进行去重 + if (CollUtil.isNotEmpty(statisticList)) { + // 先按关键字段分组 + Map> grouped = statisticList.stream().collect(Collectors.groupingBy(stat -> String.format("%s_%s_%s", stat.getUserId(), stat.getGroupId(), stat.getDate()))); + // 从每个组中取第一个(或根据业务逻辑合并) + statisticList = grouped.values().stream().map(list1 -> list1.get(0)).collect(Collectors.toList()); + } + //按照用户+考勤组+日期进行去重 + if (CollUtil.isNotEmpty(statisticList)) { + // 先按关键字段分组 + Map> grouped = statisticList.stream().collect(Collectors.groupingBy(stat -> String.format("%s_%s_%s", stat.getUserId(), stat.getGroupId(), stat.getDate()))); + // 从每个组中取第一个(或根据业务逻辑合并) + statisticList = grouped.values().stream().map(list1 -> list1.get(0)).collect(Collectors.toList()); + } + // 逐条处理数据,利用唯一索引避免重复 + for (AttendanceDayStatistics dayStatistics : statisticList) { + // 明确指定更新条件,避免默认行为 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(AttendanceDayStatistics::getGroupId, dayStatistics.getGroupId()) + .eq(AttendanceDayStatistics::getUserId, dayStatistics.getUserId()) + .eq(AttendanceDayStatistics::getDate, dayStatistics.getDate()); + // 仅在插入时需要设置ID + if (statisticsService.count(updateWrapper) == 0) { + dayStatistics.setId(RandomUtil.uuId()); + } + statisticsService.saveOrUpdate(dayStatistics, updateWrapper); + } + } + + /** + * 计算当日统计数据 + * + * @param statisticsListVo 日统计数据 + * @param attendanceRatio 出勤换算比 + * @param payrollProportion 当天划线排班计薪工时占比MutablePair<计薪工时小时数, 计薪工时小时数/分钟> + * @param dataList 用户排班打卡数据集合 + * @param busOrOutApproveList 外出出差数据集合 + * @param leaveTypeMap 请假数据 + */ + private void computeDayStatisticsData(AttendanceDayStatistics statisticsListVo, BigDecimal attendanceRatio, MutablePair payrollProportion, List dataList, List busOrOutApproveList, Map leaveTypeMap) { + // 获取全天班、半天班、划线排班的班次比以及计薪工时Map<班次类型, MutablePair<班次比, 计薪工时占比>> + Map>> schedulesTypeMap = DayStatisticsUtils.processClassTypeData(dataList, attendanceRatio, payrollProportion); + // 处理请假、调休数据 + DayStatisticsUtils.processLeaveOrCompensationData(statisticsListVo, leaveTypeMap); + // 处理外出、出差时长 + DayStatisticsUtils.processBusOrOutData(busOrOutApproveList, statisticsListVo, attendanceRatio); + // 处理应出勤、实际出勤 + DayStatisticsUtils.processShouldAttendData(dataList, schedulesTypeMap, statisticsListVo); + // 处理有效出勤处理 + DayStatisticsUtils.processEffectiveAttendData(dataList, schedulesTypeMap, statisticsListVo); + // 处理各种打卡类型的统计项 + DayStatisticsUtils.processStatisticalItems(attendanceRatio, dataList, schedulesTypeMap, statisticsListVo); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalRulesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalRulesServiceImpl.java new file mode 100644 index 0000000..8672d3f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalRulesServiceImpl.java @@ -0,0 +1,534 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.nlf.calendar.Holiday; +import jnpf.attendance.mapper.AttendanceFestivalRulesMapper; +import jnpf.attendance.service.*; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceFestivalRules; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.util.HttpUtil; +import jnpf.model.attendance.dto.AttendanceFestivalRulesDto; +import jnpf.model.attendance.dto.FestivalDto; +import jnpf.model.attendance.dto.FestivalRulesQueryDto; +import jnpf.model.attendance.vo.attendance.AttendanceFestivalRulesVo; +import jnpf.permission.V2UserApi; +import jnpf.util.*; +import jnpf.util.attendance.RuleScopeUtil; +import jnpf.util.context.ThreadContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +/** + * @author panpan + */ +@Service +@Slf4j +public class AttendanceFestivalRulesServiceImpl extends SuperServiceImpl implements AttendanceFestivalRulesService { + + @Resource + private AttendanceFestivalRulesMapper attendanceFestivalRulesMapper; + + @Resource + private RuleScopeUtil ruleScopeUtil; + + @Resource + private UserProvider userProvider; + + @Resource + private RuleScopeService ruleScopeService; + + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + @Resource + private AttendanceGroupService attendanceGroupService; + + @Resource + private V2UserApi v2UserApi; + @Resource + private ThreadPoolExecutor attendanceRuleThreadPool; + @Autowired + private AttendanceUserService attendanceUserService; + + @Override + public PageInfo getPageList(FestivalRulesQueryDto queryDto) { + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo festivalRulesVoPageInfo = new PageInfo<>(attendanceFestivalRulesMapper.getPageList(queryDto)); + // 当适配范围为指定组织或成员时查询适配范围 + if (null != festivalRulesVoPageInfo.getList() && !festivalRulesVoPageInfo.getList().isEmpty()) { + // 过滤出适配范围为指定组织或成员的规则id集合 + List scopeOfAdaptationIds = festivalRulesVoPageInfo.getList().stream().filter(v -> v.getScopeOfAdaptation().equals(1)).map(AttendanceFestivalRulesVo::getId).collect(Collectors.toList()); + if (!scopeOfAdaptationIds.isEmpty()) { + // 拼接适配范围 + Map, List>> stringMutablePairMap = ruleScopeUtil.selectScopeListBatch(scopeOfAdaptationIds, ScopeBizType.FESTIVAL_RULES); + // 遍历vo列表,根据规则id设置适配范围 + for (AttendanceFestivalRulesVo v : festivalRulesVoPageInfo.getList()) { + MutablePair, List> pair = stringMutablePairMap.get(v.getId()); + if (null != pair) { + v.setOrganizeNum(pair.getLeft().size()); + v.setUserIdNum(pair.getRight().size()); + } + } + } + } + return festivalRulesVoPageInfo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(AttendanceFestivalRulesDto festivalRulesDto) throws Exception { + String userId = userProvider.get().getUserId(); + List list = lambdaQuery().eq(AttendanceFestivalRules::getName, festivalRulesDto.getName()) + .eq(AttendanceFestivalRules::getYear, festivalRulesDto.getYear()) + .eq(AttendanceFestivalRules::getDeleteMark, 0) + .list(); + Assert.isTrue(CollUtil.isEmpty(list), "该名称已存在"); + Date date = new Date(); + AttendanceFestivalRules entity = BeanUtil.toBean(festivalRulesDto, AttendanceFestivalRules.class); + entity.setCreatorUserId(userId); + entity.setCreatorTime(date); + entity.setLastModifyUserId(userId); + entity.setLastModifyTime(date); + attendanceFestivalRulesMapper.insert(entity); + List ruleScopeList = ruleScopeUtil.getRuleScopeList(entity.getId(), entity.getScopeOfAdaptation(), MutablePair.of(festivalRulesDto.getOrganizeList(), festivalRulesDto.getUserIdList()), ScopeBizType.FESTIVAL_RULES); + ruleScopeUtil.saveBatch(ruleScopeList); + } + + private List getUserIds(List organizeList, List userIdList, AttendanceFestivalRules entity) { + List dateList = StringUtil.isNotEmpty(entity.getFestivalDate()) ? Arrays.stream(entity.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()) : new ArrayList<>(); + Date start = dateList.stream().min(Comparator.naturalOrder()).orElse(null); + if (null == start) { + return new ArrayList<>(); + } + Date end = dateList.stream().max(Comparator.naturalOrder()).orElse(null); + return attendanceUserService.getUserIds(organizeList, userIdList, start, end, entity.getScopeOfAdaptation()); + } + + // ahua + private void autoRest(AttendanceFestivalRules newEntity, AttendanceFestivalRules oldEntity, List newUserIds, List oldUserIds){ + Date start = new Date(); + Date end = jnpf.util.DateUtil.getEndDayOfMonth(); + if (Objects.nonNull(oldEntity) && StringUtil.isNotEmpty(oldEntity.getFestivalDate())) { + List dateList = Arrays.stream(oldEntity.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()); + start = Collections.min(dateList); + end = Collections.max(dateList); + } + if (Objects.nonNull(newEntity) && StringUtil.isNotEmpty(newEntity.getFestivalDate())) { + if (Objects.equals(newEntity.getState(), 0)) { + return; + } + if (Objects.equals(newEntity.getAutoScheduling(), 0)) { + return; + } + List dateList = Arrays.stream(newEntity.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()); + List collect1 = dateList.stream().filter(date -> jnpf.util.DateUtil.getDayEnd().after(date)).collect(Collectors.toList()); + Assert.isFalse(CollUtil.isNotEmpty(collect1), "请选择小于今天日期的节日日期"); + Date min = Collections.min(dateList); + start = start.after(min) ? start : min; + Date max = Collections.max(dateList); + end = end.before(max) ? max : end; + } + Date finalStart = start; + Date finalEnd = end; + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + attendanceDailyRuleService.addHolidayDailyRule(finalStart, finalEnd, attendanceGroupService.queryDropList(), newEntity, oldEntity, newUserIds, oldUserIds); + }), attendanceRuleThreadPool); + } + + @Transactional + @Override + public void statutoryUpdate(String year) { + List list1 = lambdaQuery() + .likeRight(AttendanceFestivalRules::getEndDate, year) + .eq(AttendanceFestivalRules::getDeleteMark, Boolean.FALSE) + .list(); + Map collect1 = list1.stream().collect(Collectors.toMap(AttendanceFestivalRules::getName, entity -> entity, (r1, r2) -> r1)); + List holidays = getHolidaysFromApi(year); + holidays.addAll(getHolidaysFromApi(DateDetail.getPreviousYear(year))); + Map> collect = holidays.stream().filter(festival -> festival.getTarget().contains(year)).collect(Collectors.groupingBy(Holiday::getName)); + List entities = CollUtil.newArrayList(); + UserInfo user = UserProvider.getUser(); + + collect.forEach((name, list) -> { + List workHolidays = list.stream().filter(Holiday::isWork).collect(Collectors.toList()); + List notWorkHolidays = list.stream().filter(holiday -> !holiday.isWork()).collect(Collectors.toList()); + Holiday firstHoliday = notWorkHolidays.stream().sorted(Comparator.comparing(Holiday::getDay)).reduce((h1, h2) -> h1).orElse(null); + Holiday lastHoliday = notWorkHolidays.stream().sorted(Comparator.comparing(Holiday::getDay)).reduce((h1, h2) -> h2).orElse(null); + AttendanceFestivalRules attendanceFestivalRules = new AttendanceFestivalRules(); + attendanceFestivalRules.setId(RandomUtil.uuId()); + attendanceFestivalRules.setYear(year); + attendanceFestivalRules.setName(name); + attendanceFestivalRules.setCreatorTime(new Date()); + attendanceFestivalRules.setCreatorUserId(user.getUserId()); + attendanceFestivalRules.setTenantId(user.getTenantId()); + attendanceFestivalRules.setAutoScheduling(1); + attendanceFestivalRules.setUpdateState(0); + attendanceFestivalRules.setState(0); + attendanceFestivalRules.setScopeOfAdaptation(0); + AttendanceFestivalRules orDefault = collect1.getOrDefault(name, attendanceFestivalRules); + orDefault.setStartDate(jnpf.util.DateUtil.stringToDates(firstHoliday.getDay())); + orDefault.setEndDate(jnpf.util.DateUtil.stringToDates(lastHoliday.getDay())); + String compensateInDate = workHolidays.stream().map(Holiday::getDay).reduce((r1, r2) -> r1 + "," + r2).orElse(""); + orDefault.setCompensateInDate(compensateInDate); + entities.add(orDefault); + }); + // 处理每年同日更新的配置 + List oldRules = lambdaQuery() + .likeRight(AttendanceFestivalRules::getEndDate, DateUtil.format(DateUtil.offsetMonth(DateUtil.parse(year + "-01-01 00:00:00", "yyyy-01-01 00:00:00"), -12), "yyyy")) + .eq(AttendanceFestivalRules::getUpdateState, 1) + .eq(AttendanceFestivalRules::getDeleteMark, Boolean.FALSE) + .list(); + oldRules.forEach(entity -> { + entity.setId(RandomUtil.uuId()); + entity.setDeleteTime(new Date()); + entity.setYear(year); + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(user.getUserId()); + entity.setStartDate(DateUtil.offsetMonth(entity.getStartDate(), 12)); + entity.setEndDate(DateUtil.offsetMonth(entity.getEndDate(), 12)); + entity.setYear(DateUtil.format(new Date(), "yyyy")); + entity.setScopeOfAdaptation(0); + entity.setState(0); + entity.setCompensateInDate(StringUtil.isEmpty(entity.getCompensateInDate()) ? null : Arrays.stream(entity.getCompensateInDate().split(",")).map(date -> jnpf.util.DateUtil.daFormat(DateUtil.offsetMonth(DateUtil.parse(date, "yyyy-MM-dd"), 12))).collect(Collectors.joining(","))); + entity.setFestivalDate(StringUtil.isEmpty(entity.getFestivalDate()) ? "" : jnpf.util.DateUtil.daFormat(DateUtil.offsetMonth(jnpf.util.DateUtil.stringToDates(entity.getFestivalDate()), 12))); + }); + oldRules.removeIf(rule -> list1.stream().anyMatch(vo -> StringUtil.equals(vo.getName(), rule.getName()))); + entities.addAll(oldRules); + saveOrUpdateBatch(entities.stream().sorted(Comparator.comparing(AttendanceFestivalRules::getStartDate)).collect(Collectors.toList())); + ruleScopeService.saveBatch(entities.stream().map(entity -> + ruleScopeUtil.getRuleScope(entity.getId(), null, ConstantUtil.RULE_SCOPE_ALL, ScopeBizType.FESTIVAL_RULES) + ).collect(Collectors.toList())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(AttendanceFestivalRulesDto festivalRulesDto) throws Exception { + List list = lambdaQuery().eq(AttendanceFestivalRules::getName, festivalRulesDto.getName()) + .eq(AttendanceFestivalRules::getYear, festivalRulesDto.getYear()) + .eq(AttendanceFestivalRules::getDeleteMark, 0) + .list(); + Assert.isFalse(CollUtil.isNotEmpty(list) && list.stream().noneMatch(entity -> entity.getId().equals(festivalRulesDto.getId())), "该名称已存在"); + AttendanceFestivalRules oldEntity = attendanceFestivalRulesMapper.selectById(festivalRulesDto.getId()); + String userId = userProvider.get().getUserId(); + Date date = new Date(); + AttendanceFestivalRules entity = BeanUtil.toBean(festivalRulesDto, AttendanceFestivalRules.class); + entity.setLastModifyUserId(userId); + entity.setLastModifyTime(date); + entity.setTenantId(UserProvider.getUser().getTenantId()); + entity.setDeleteMark(0); + entity.setState(oldEntity.getState()); + attendanceFestivalRulesMapper.updateById(entity); + MutablePair, List> listListMutablePair = ruleScopeUtil.selectScopeList(festivalRulesDto.getId(), ScopeBizType.FESTIVAL_RULES); + ruleScopeUtil.updateRuleScopeList(entity.getId(), oldEntity.getScopeOfAdaptation(), entity.getScopeOfAdaptation(), MutablePair.of(festivalRulesDto.getOrganizeList(), festivalRulesDto.getUserIdList()), ScopeBizType.FESTIVAL_RULES); + List oldUserIds = getUserIds(Objects.isNull(listListMutablePair) ? null : listListMutablePair.getLeft(), Objects.isNull(listListMutablePair) ? null : listListMutablePair.getRight(), oldEntity); + List userIds = getUserIds(festivalRulesDto.getOrganizeList(), festivalRulesDto.getUserIdList(), entity); + autoRest(entity, oldEntity, userIds, oldUserIds); + } + + @Override + public AttendanceFestivalRulesVo detail(String id) { + AttendanceFestivalRulesVo bean = BeanUtil.toBean(attendanceFestivalRulesMapper.selectById(id), AttendanceFestivalRulesVo.class); + // 当适配范围为指定组织或成员时查询适配范围 + if (bean.getScopeOfAdaptation().equals(1)) { + MutablePair, List> pair = ruleScopeUtil.selectScopeList(id, ScopeBizType.FESTIVAL_RULES); + if (null != pair) { + bean.setOrganizeList(pair.getLeft()); + bean.setUserIdList(pair.getRight()); + } + } + return bean; + } + + @Override + public void delete(String id) { + AttendanceFestivalRules byId = getById(id); + MutablePair, List> listListMutablePair = ruleScopeUtil.selectScopeList(id, ScopeBizType.FESTIVAL_RULES); + List oldUserIds = getUserIds(Objects.isNull(listListMutablePair) ? null : listListMutablePair.getLeft(), Objects.isNull(listListMutablePair) ? null : listListMutablePair.getRight(), byId); + attendanceFestivalRulesMapper.deleteById(id); + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, id) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.FESTIVAL_RULES)); + autoRest(null, byId, List.of(), oldUserIds); + } + + @Override + public void updateState(AttendanceFestivalRulesDto festivalRulesDto) { + AttendanceFestivalRules byId = getById(festivalRulesDto.getId()); + attendanceFestivalRulesMapper.updateState(festivalRulesDto.getId(), festivalRulesDto.getState(), userProvider.get().getId()); + MutablePair, List> listListMutablePair = ruleScopeUtil.selectScopeList(festivalRulesDto.getId(), ScopeBizType.FESTIVAL_RULES); + List oldUserIds = getUserIds(Objects.isNull(listListMutablePair) ? null : listListMutablePair.getLeft(), Objects.isNull(listListMutablePair) ? null : listListMutablePair.getRight(), byId); + + // 查询自动排休逻辑对应的人员列表 + if (Objects.equals(festivalRulesDto.getState(), 1)) { + byId.setState(1); + autoRest(byId, null, oldUserIds, List.of()); + } else { + autoRest(null, byId, List.of(), oldUserIds); + } + + } + + @Override + public boolean checkFestivalRules(Date date, String userId) { + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(Collections.singletonList(userId), ScopeBizType.FESTIVAL_RULES, ConstantUtil.NUM_FALSE); + if (CollUtil.isEmpty(attendanceRuleScopes)) { + return false; + } + List rules = getRules(date, attendanceRuleScopes); + return CollUtil.isNotEmpty(rules); + } + + @Override + public Map> checkFestivalRulesBatch(Date date, List userIds) { + + DateDetail dateDetail = new DateDetail(date); + Date today = dateDetail.getCurrentDate(); + String endDate = DateDetail.getDate2Str(today, DateDetail.DF); + dateDetail.getYesterday(); + Date yesterday = dateDetail.getCurrentDate(); + String startDate = DateDetail.getDate2Str(yesterday, DateDetail.DF); + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(userIds, ScopeBizType.FESTIVAL_RULES, ConstantUtil.NUM_FALSE); + ConcurrentMap> map = attendanceRuleScopes.stream().collect(Collectors.groupingByConcurrent(AttendanceRuleScope::getUserId)); + // 一次查出所有涉及的节假日规则 + List allRules; + if (map.isEmpty()) { + allRules = List.of(); + } else { + List rules = attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList()); + allRules = attendanceFestivalRulesMapper.selectList( + new QueryWrapper() + .lambda() + .eq(AttendanceFestivalRules::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(AttendanceFestivalRules::getState, ConstantUtil.NUM_TRUE) + .le(AttendanceFestivalRules::getStartDate, endDate) + .ge(AttendanceFestivalRules::getEndDate, startDate) + .in(AttendanceFestivalRules::getId, rules) + ); + } + ConcurrentMap> ruleMap = allRules.stream().collect(Collectors.groupingByConcurrent(AttendanceFestivalRules::getId)); + // 返回内容 + Map> returnMap = new HashMap<>(); + userIds.forEach(user -> { + List scopeList = map.getOrDefault(user, List.of()); + Map valueMap = new HashMap<>(); + if (scopeList.isEmpty()) { + valueMap.put(startDate, new FestivalDto(Boolean.FALSE, "")); + valueMap.put(endDate, new FestivalDto(Boolean.FALSE, "")); + returnMap.put(user, valueMap); + } else { + List userRules = scopeList.stream() + .map(AttendanceRuleScope::getRuleId) + .distinct() + .collect(Collectors.toList()); + // 判断[昨天]是否匹配节假日 + valueMap.put(startDate, checkFestivalByDay(yesterday, ruleMap, userRules)); + // 判断[今天]是否匹配节假日 + valueMap.put(endDate, checkFestivalByDay(today, ruleMap, userRules)); + returnMap.put(user, valueMap); + } + }); + return returnMap; + } + + private FestivalDto checkFestivalByDay(Date day, ConcurrentMap> ruleMap, List userRules) { + FestivalDto festivalDto = new FestivalDto(false, ""); + DateTime day1 = DateUtil.beginOfDay(day); + // 节日集合 + List festivalRulesList = new ArrayList<>(); + // 过滤节日对象,有值时拼接节日名称 + userRules.forEach(id -> { + List list = ruleMap.get(id); + if (list == null || list.isEmpty()) return; + List collect = list.stream().filter(r -> DateDetail.checkTimeBetween(day1, r.getStartDate(), r.getEndDate())).collect(Collectors.toList()); + if (collect.isEmpty()) return; + festivalRulesList.addAll(collect); + }); + // 去重festivalRulesList 用@拼接节日名称 + festivalDto.setFestival(!festivalRulesList.isEmpty()); + festivalDto.setFestivalStr(festivalRulesList.stream().map(AttendanceFestivalRules::getName).collect(Collectors.joining("@"))); + return festivalDto; + } + + /** + * 获取未删除且启用的规则 + * @param date 日期 + * @param attendanceRuleScopes 规则范围 + * @return 规则列表 + */ + private List getRules(Date date, List attendanceRuleScopes) { + return attendanceFestivalRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceFestivalRules::getDeleteMark, 0) + .eq(AttendanceFestivalRules::getState, 1) + .and(true, x -> x.le(AttendanceFestivalRules::getEndDate, date)) + .and(true, x -> x.ge(AttendanceFestivalRules::getStartDate, date)) + .in(CollUtil.isNotEmpty(attendanceRuleScopes), AttendanceFestivalRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + ); + } + + /** + * 获取未删除且启用的规则 + * @param attendanceRuleScopes 规则范围 + * @return 规则列表 + */ + private List getRules(Date start, Date end, List attendanceRuleScopes) { + return attendanceFestivalRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceFestivalRules::getDeleteMark, 0) + .eq(AttendanceFestivalRules::getState, 1) + .and(x -> x.ge(AttendanceFestivalRules::getEndDate, start)) + .and(x -> x.le(AttendanceFestivalRules::getStartDate, end)) + .in(CollUtil.isNotEmpty(attendanceRuleScopes), AttendanceFestivalRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + ); + } + + @Override + public Map batchCheckFestivalRules(Date date, List userIds) { + Map map = new HashMap<>(); + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(userIds, ScopeBizType.FESTIVAL_RULES, ConstantUtil.NUM_FALSE); + if (CollUtil.isEmpty(attendanceRuleScopes)) { + userIds.forEach(x -> map.put(x, false)); + return map; + } + List rules = getRules(date, attendanceRuleScopes); + // 命中且启用的节日规则Id集合 + List festivalRules = rules.stream().map(AttendanceFestivalRules::getId).distinct().collect(Collectors.toList()); + // 获取用户对应的启动中的规则 + Map> scopeMap = attendanceRuleScopes.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getUserId)); + userIds.forEach(userId -> { + List scopes = scopeMap.get(userId); + if (CollUtil.isEmpty(scopes)) { + // 未命中规则 + map.put(userId, false); + } else { + List list = scopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList()); + list.retainAll(festivalRules); + map.put(userId, !list.isEmpty()); + } + }); + return map; + } + + @Override + public Map> batchGetFestivalRulesByUserIds(Date start, Date end, List userIds) { + Map> map = new HashMap<>(); + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(userIds, ScopeBizType.FESTIVAL_RULES, ConstantUtil.NUM_FALSE); + if (CollUtil.isEmpty(attendanceRuleScopes)) { + return map; + } + List rules = getRules(start, end, attendanceRuleScopes); + // 获取用户对应的启动中的规则 + Map> scopeMap = attendanceRuleScopes.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getUserId)); + userIds.forEach(userId -> { + List scopes = scopeMap.get(userId); + if (CollUtil.isEmpty(scopes)) { + // 未命中规则 + return; + } + map.put(userId, rules.stream().filter(rule -> scopes.stream().anyMatch(scope -> StringUtil.equals(scope.getRuleId(), rule.getId()))).collect(Collectors.toList())); + }); + return map; + } + + /** + * 查询节假日信息并返回Holiday对象列表 + * @param year 年份 + * @return Holiday对象列表 + */ + public List getHolidaysFromApi(String year) { + // 验证输入参数 + if (StringUtil.isEmpty(year)) { + log.warn("Year parameter is empty, returning empty holiday list"); + return Collections.emptyList(); + } + List holidayList = new ArrayList<>(); + + try { + // 构建API URL + String apiUrl = "https://publicapi.xiaoai.me/holiday/year?date=" + year; + + // 调用API获取响应 + String response = HttpUtil.sendGet(apiUrl); + + if (StringUtil.isEmpty(response)) { + log.error("API returned empty response for year: {}", year); + return Collections.emptyList(); + } + + // 解析JSON响应 + JSONObject jsonObject = JSONObject.parseObject(response); + + // 检查响应状态 + if (jsonObject != null && jsonObject.getInteger("code") == 0) { + JSONArray dataArray = jsonObject.getJSONArray("data"); + + if (dataArray != null && !dataArray.isEmpty()) { + // 遍历节假日数据 + for (int i = 0; i < dataArray.size(); i++) { + JSONObject holidayJson = dataArray.getJSONObject(i); + + if (holidayJson != null) { + // 只包含节假日(daytype=1)和调休日(daytype=3) + Integer daytype = holidayJson.getInteger("daytype"); + if (daytype == 1 || daytype == 3) { + // 创建Holiday对象 + Holiday holiday = new Holiday(); + + // 设置属性 + holiday.setDay(holidayJson.getString("date")); + + // 提取节假日名称,移除"调休"后缀,确保同个节假日的名称一致 + String holidayName = holidayJson.getString("holiday"); + if (holidayName != null && holidayName.contains("调休")) { + holidayName = holidayName.replace("调休", "").trim(); + } + holiday.setName(holidayName); + + holiday.setWork(daytype == 3); // daytype=3表示调休日(需上班) + holiday.setTarget(year); // 设置目标年份 + + // 添加到列表 + holidayList.add(holiday); + } + } + } + + } else { + log.warn("No holiday data found for year: {}", year); + } + } else { + int code = jsonObject != null ? jsonObject.getInteger("code") : -1; + String message = jsonObject != null ? jsonObject.getString("msg") : "Unknown error"; + log.error("API returned error for year {}: code={}, message={}", year, code, message); + } + } catch (Exception e) { + log.error("Error fetching holidays from API for year {}: {}", year, e.getMessage(), e); + } + + return holidayList; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalSettingServiceImpl.java new file mode 100644 index 0000000..8c33261 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFestivalSettingServiceImpl.java @@ -0,0 +1,410 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import com.google.common.collect.Maps; +import com.nlf.calendar.Holiday; +import com.nlf.calendar.util.HolidayUtil; +import jnpf.attendance.mapper.AttendanceFestivalSettingMapper; +import jnpf.attendance.service.AttendanceFestivalSettingService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceHolidaySettingService; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceFestivalSettingEntity; +import jnpf.entity.attendance.AttendanceHolidaySettingEntity; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceFestivalSettingDto; +import jnpf.model.attendance.vo.AttendanceFestivalSettingVo; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.HolidayOptionVo; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 考勤配置-节日配置 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@Service +@Slf4j +public class AttendanceFestivalSettingServiceImpl extends SuperServiceImpl implements AttendanceFestivalSettingService { + + @Autowired + private AttendanceHolidaySettingService attendanceHolidaySettingService; + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + @Transactional + public void save(AttendanceFestivalSettingDto attendanceFestivalSettingDto) throws HandleException { + synchronized (attendanceFestivalSettingDto.getGroupId()) { + AttendanceFestivalSettingEntity entity = BeanUtil.toBean(attendanceFestivalSettingDto, AttendanceFestivalSettingEntity.class); + UserInfo user = UserProvider.getUser(); + if (StringUtil.isNull(entity.getId())) { + List list = lambdaQuery() + .eq(AttendanceFestivalSettingEntity::getName, entity.getName()) + .eq(AttendanceFestivalSettingEntity::getGroupId, attendanceFestivalSettingDto.getGroupId()) + .eq(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.FALSE) + .list(); + Assert.isFalse(CollUtil.isNotEmpty(list),"节日名称不能重复"); + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(user.getUserId()); + entity.setTenantId(user.getTenantId()); + } else { + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(user.getUserId()); + } + AttendanceFestivalSettingEntity byId = getById(attendanceFestivalSettingDto.getId()); + saveOrUpdate(entity); + entity.setEnable(Objects.isNull(byId) ? 0 : byId.getEnable()); + autoRest(entity, byId); + } + } + + private void autoRest(AttendanceFestivalSettingEntity entity, AttendanceFestivalSettingEntity hisFestivalSetting) throws HandleException { + if (Objects.equals(entity.getEnable(), 0)) { + return; + } + if (!Objects.equals(entity.getDistributeType(), 0)) { + return; + } + String groupId = entity.getGroupId(); + Date start = new Date(); + Date end = DateUtil.getEndDayOfMonth(); + List attendanceGroups = attendanceGroupService.queryDropList(); + AttendanceGroup attendanceGroup = attendanceGroups.stream().filter(group -> StringUtil.equals(groupId, group.getId())).reduce((g1, g2) -> g1).orElse(null); + if (Objects.isNull(attendanceGroup)) { + log.error("未查询到当前考勤组"); + throw new HandleException("未查询到当前考勤组"); + } + if (Objects.equals(attendanceGroup.getFestivalSetting(), 0)) { + return; + } + List dateList = Arrays.stream(entity.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()); + List collect1 = dateList.stream().filter(date -> DateUtil.getDayEnd().after(date)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect1)) { + throw new HandleException("请选择大于今天的节日日期"); + } + HashMap> objectObjectHashMap = Maps.newHashMap(); + List enableGroup = getEnableGroup(groupId); + List collect = enableGroup.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + collect.add(groupId); + for (String group : collect) { + objectObjectHashMap.put(group, CollUtil.newArrayList(entity)); + } +// attendanceDailyRuleService.addHolidayDailyRule(groupId, start, end, attendanceGroups, objectObjectHashMap, hisFestivalSetting); + } + + @Override + public PageDTO page(String groupId, String year, Integer page, Integer size) throws HandleException { + if (StringUtil.isEmpty(year)) { + throw new HandleException("请选择年份"); + } + List attendanceGroups = attendanceGroupService.queryDropList(); + Map collect = attendanceGroups.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group)); + Map groupIdToParent = attendanceGroups.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + PageDTO attendanceFestivalSettingVoPageDTO = upPage(groupId, groupIdToParent, collect, year, page, size); + if (Objects.isNull(attendanceFestivalSettingVoPageDTO)) { + return new PageDTO<>(page, size); + } + return attendanceFestivalSettingVoPageDTO; + } + + private PageDTO upPage(String groupId, Map groupIdToParent, Map collect, String year, Integer page, Integer size) { + if (StringUtil.isEmpty(groupId)) { + return null; + } + AttendanceGroup attendanceGroup = collect.get(groupId); + if (Objects.isNull(attendanceGroup)) { + return null; + } + if (Objects.equals(attendanceGroup.getFestivalSetting(), 1)) { + PageDTO page1 = lambdaQuery() + .likeRight(AttendanceFestivalSettingEntity::getEndDate, year) + .eq(AttendanceFestivalSettingEntity::getGroupId, groupId) + .eq(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.FALSE) + .orderByAsc(AttendanceFestivalSettingEntity::getStatutoryEnable, AttendanceFestivalSettingEntity::getDay) + .page(new PageDTO<>(page, size)); + return (PageDTO) page1.convert(x -> BeanUtil.toBean(x, AttendanceFestivalSettingVo.class)); + } + return upPage(groupIdToParent.get(groupId), groupIdToParent, collect, year, page, size); + } + + @Override + public AttendanceFestivalSettingVo getOne(String id) { + AttendanceFestivalSettingEntity one = lambdaQuery().eq(AttendanceFestivalSettingEntity::getId, id) + .eq(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.FALSE) + .one(); + return BeanUtil.toBean(one, AttendanceFestivalSettingVo.class); + } + + @Override + public void del(String id) { + lambdaUpdate().set(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.TRUE) + .set(AttendanceFestivalSettingEntity::getDeleteUserId, UserProvider.getLoginUserId()) + .set(AttendanceFestivalSettingEntity::getDeleteTime, new Date()) + .eq(AttendanceFestivalSettingEntity::getId, id) + .update(); + } + + @Override + public Map> getEnableFestivalSetting(List groupIds, List attendanceGroupVos) { + return getEnableFestivalSetting(groupIds,attendanceGroupVos, Boolean.TRUE); + } + + private Map> getEnableFestivalSetting(List groupIds, List attendanceGroupVos, Boolean isFilterDisable) { + List list = lambdaQuery().eq(isFilterDisable, AttendanceFestivalSettingEntity::getEnable, Boolean.TRUE) + .eq(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.FALSE) + .list(); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getFestivalSetting() == 1)); + Map> collect = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.groupingBy(AttendanceFestivalSettingEntity::getGroupId)); + Map> map = Maps.newHashMap(); + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + groupIds.forEach(groupId -> map.put(groupId, findNextBaseSetting(collect, groupIdToParent, groupId))); + return map; + } + +// @Override +// @Deprecated(since = "在2.0中已过期", forRemoval = true) +// public void autoGrantBalance() { +// log.error("开始节日自动发放劵"); +// List attendanceGroups = attendanceGroupService.queryDropList(); +// List groupIds = attendanceGroups.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); +// Map> enableFestivalSetting = getEnableFestivalSetting(groupIds, attendanceGroups); +// List attendanceGroupUsers = attendanceUserService.queryByUsersGroupIds(groupIds); +// Map> collect = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); +// enableFestivalSetting.forEach((groupId, list) -> { +// List attendanceGroupUsers1 = collect.get(groupId); +// if (CollUtil.isEmpty(attendanceGroupUsers1)) { +// return; +// } +// if (CollUtil.isEmpty(list)) { +// return; +// } +// list.forEach(setting -> { +// if (!Objects.equals(setting.getDistributeType(), 1)) { +// return; +// } +// attendanceGroupUsers1.forEach(user -> { +// Date date = DateUtil.getDayBegin(); +// Date dateByMonthAndDay; +// if (setting.getGiveType() == 1) { +// dateByMonthAndDay = DateDetail.getDateByMonthAndDay(date, date.getMonth() + 1, setting.getGiveDay()); +// } else { +// dateByMonthAndDay = DateDetail.getDateByMonthAndDay(date, setting.getGiveMonth(), setting.getGiveDay()); +// } +// if (date.compareTo(dateByMonthAndDay) != 0) { +// return; +// } +// if (setting.getExpiresType() == 1) { +// Integer expiresDay = setting.getExpiresDayNumber(); +// date = DateUtil.dateAddDays(date, expiresDay); +// } else if (Objects.equals(setting.getExpiresType(), 2)) { +// date = DateDetail.getDateByMonthAndDay(date, setting.getExpiresMonth(), setting.getExpiresDay()); +// } else { +// date = null; +// } +// BigDecimal dayNum = new BigDecimal(setting.getDay()); +// log.error("考勤组[{}]节日[{}]自动发放劵[{}]", user.getGroupId(), setting.getName(), dayNum); +// attendanceBalanceRecordMapper.grantBalance(RandomUtil.uuId(), user.getUserId(), setting.getName(), dayNum, date, 2, 0, null, setting.getPaidSalaryEnable(), 1, setting.getId()); +// }); +// }); +// }); +// log.error("节日自动发放劵结束"); +// } + + private List getEnableGroup(String groupId) { + List attendanceGroupVos = attendanceGroupService.queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return CollUtil.newArrayList(); + } + AttendanceGroup attendanceGroup = attendanceGroupVos.stream().filter(group -> StringUtil.equals(group.getId(), groupId)).reduce((g1, g2) -> g1).orElse(null); + if (Objects.equals(attendanceGroup.getFestivalSetting(), 0)) { + log.error("当前考勤组未开启节日配置"); + return CollUtil.newArrayList(); + } + Map> groupIdToParent = attendanceGroupVos.stream().collect(Collectors.groupingBy(AttendanceGroup::getParentId)); + List groupVos = CollUtil.newArrayList(); + List orDefault = groupIdToParent.getOrDefault(groupId, CollUtil.newArrayList()); + if (CollUtil.isEmpty(orDefault)) { + return CollUtil.newArrayList(); + } + getEnableSingleGroup(orDefault, groupVos, groupIdToParent); + return groupVos; + } + + private void getEnableSingleGroup(List groups, List groupVos, Map> groupIdToParent) { + groups.forEach(group -> { + if (Objects.equals(group.getFestivalSetting(), 1)) { + return; + } + groupVos.add(BeanUtil.toBean(group, AttendanceGroupVo.class)); + List attendanceGroups = groupIdToParent.get(group.getId()); + if (CollUtil.isEmpty(attendanceGroups)) { + return; + } + getEnableSingleGroup(attendanceGroups, groupVos, groupIdToParent); + }); + } + + /** + * 获取考勤组下个考勤组配置 + * + * @param collect + * @param groupIdToParent + * @param groupId + * @return + */ + private List findNextBaseSetting(Map> collect, Map groupIdToParent, String groupId) { + if (StringUtil.isBlank(groupId)) { + return CollUtil.newArrayList(); + } + if (!collect.containsKey(groupId)) { + return findNextBaseSetting(collect, groupIdToParent, groupIdToParent.get(groupId)); + } + return collect.get(groupId); + } + + @Override + public void statutoryUpdate(String groupId, String year) { + List list1 = lambdaQuery().eq(AttendanceFestivalSettingEntity::getGroupId, groupId) + .likeRight(AttendanceFestivalSettingEntity::getEndDate, year) + .eq(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.FALSE) + .eq(AttendanceFestivalSettingEntity::getStatutoryEnable, 1) + .list(); + Map collect1 = list1.stream().collect(Collectors.toMap(AttendanceFestivalSettingEntity::getName, entity -> entity)); + List holidays = HolidayUtil.getHolidays(year); + holidays.addAll(HolidayUtil.getHolidays(DateDetail.getPreviousYear(year))); + Map> collect = holidays.stream().filter(festival -> festival.getTarget().contains(year)).collect(Collectors.groupingBy(Holiday::getName)); + List entities = CollUtil.newArrayList(); + UserInfo user = UserProvider.getUser(); + collect.forEach((name, list) -> { + List workHolidays = list.stream().filter(Holiday::isWork).collect(Collectors.toList()); + List notWorkHolidays = list.stream().filter(holiday -> !holiday.isWork()).collect(Collectors.toList()); + Holiday firstHoliday = notWorkHolidays.stream().sorted(Comparator.comparing(Holiday::getDay)).reduce((h1, h2) -> h1).orElse(null); + Holiday lastHoliday = notWorkHolidays.stream().sorted(Comparator.comparing(Holiday::getDay)).reduce((h1, h2) -> h2).orElse(null); + AttendanceFestivalSettingEntity orDefault = collect1.getOrDefault(name, AttendanceFestivalSettingEntity.builder().groupId(groupId).deleteMark(0).statutoryEnable(1).name(name).creatorTime(new Date()).creatorUserId(user.getUserId()).tenantId(user.getTenantId()).build()); + orDefault.setStartDate(DateUtil.stringToDates(firstHoliday.getDay())); + orDefault.setEndDate(DateUtil.stringToDates(lastHoliday.getDay())); + String compensateInDate = workHolidays.stream().map(Holiday::getDay).reduce((r1, r2) -> r1 + "," + r2).orElse(""); + orDefault.setCompensateInDate(compensateInDate); + entities.add(orDefault); + }); + // 这里需要处理每年同日更新的配置 + + saveOrUpdateBatch(entities.stream().sorted(Comparator.comparing(AttendanceFestivalSettingEntity::getStartDate)).collect(Collectors.toList())); + } + + @Override + @Transactional + public void updateStatus(String id, Integer enable) throws HandleException { + if (Objects.equals(enable, 1)) { + AttendanceFestivalSettingEntity byId = getById(id); + if(StringUtil.isEmpty(byId.getFestivalDate())){ + throw new HandleException("请选择节日日期"); + } + + if(Objects.isNull(byId.getExpiresType())){ + throw new HandleException("请设置过期天数"); + } + + if(Objects.equals(byId.getExpiresType(),1) && Objects.isNull(byId.getExpiresDayNumber())){ + throw new HandleException("请设置过期天数"); + } + if(Objects.equals(byId.getExpiresType(),2) && Objects.isNull(byId.getExpiresDay())){ + throw new HandleException("请设置过期日期"); + } + List dateList = Arrays.stream(byId.getFestivalDate().split(",")).map(DateDetail::getStr2Date10).collect(Collectors.toList()); + List collect1 = dateList.stream().filter(date -> DateUtil.getDayEnd().after(date)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect1)) { + throw new HandleException("请选择大于今天的节日日期"); + } + } + + lambdaUpdate().set(AttendanceFestivalSettingEntity::getEnable, enable) + .eq(AttendanceFestivalSettingEntity::getId, id) + .update(); + autoRest(getById(id), null); + } + + @Override + public void initFestivalSetting(String groupId) { + List list = lambdaQuery().eq(AttendanceFestivalSettingEntity::getGroupId, groupId) + .eq(AttendanceFestivalSettingEntity::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isNotEmpty(list)) { + return; + } + List attendanceGroups = attendanceGroupService.queryDropList(); + Map> enableBaseSetting = getEnableFestivalSetting(CollUtil.newArrayList(groupId), attendanceGroups, Boolean.FALSE); + List attendanceBaseSettings = enableBaseSetting.get(groupId); + if (CollUtil.isNotEmpty(attendanceBaseSettings)) { + attendanceBaseSettings.forEach(attendanceBaseSetting -> { + attendanceBaseSetting.setId(RandomUtil.uuId()); + attendanceBaseSetting.setGroupId(groupId); + attendanceBaseSetting.setCreatorTime(new Date()); + attendanceBaseSetting.setCreatorUserId(UserProvider.getLoginUserId()); + }); + saveBatch(attendanceBaseSettings); + } + } + + @Override + public List getHolidayOptions(String groupId) { + List holidays = CollUtil.newArrayList(); + List attendanceGroups = attendanceGroupService.queryDropList(); + Map> festivalMap = getEnableFestivalSetting(CollUtil.newArrayList(groupId), attendanceGroups); + List entities = festivalMap.getOrDefault(groupId, CollUtil.newArrayList()); + entities.forEach(festival -> { + if(Objects.equals(festival.getDistributeType(),1) || Objects.equals(festival.getDistributeType(),0)){ + return; + } + holidays.add(HolidayOptionVo.builder() + .id(festival.getId()) + .dayType(1) + .name(festival.getName()) + .day(festival.getDay()) + .expiresDayNumber(festival.getExpiresDayNumber()) + .expiresMonth(festival.getExpiresMonth()) + .expiresDay(festival.getExpiresDay()) + .expiresType(festival.getExpiresType()) + .type(1) + .build()); + }); + Map> enableFestivalSetting = attendanceHolidaySettingService.getEnableFestivalSetting(CollUtil.newArrayList(groupId), attendanceGroups); + List entities1 = enableFestivalSetting.getOrDefault(groupId, CollUtil.newArrayList()); + entities1.forEach(holiday -> { + if(Objects.equals(holiday.getGiveType(),1)){ + return; + } + holidays.add(HolidayOptionVo.builder() + .id(holiday.getId()) + .name(holiday.getName()) + .dayType(holiday.getDayType()) + .yoeMultiple(holiday.getYoeMultiple()) + .yosMultiple(holiday.getYosMultiple()) + .day(holiday.getDayNumber()) + .expiresDayNumber(holiday.getExpiresDayNumber()) + .expiresMonth(holiday.getExpiresMonth()) + .expiresDay(holiday.getExpiresDay()) + .expiresType(holiday.getExpiresType()) + .type(2) + .build()); + }); + return holidays; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFixedClassServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFixedClassServiceImpl.java new file mode 100644 index 0000000..bdd0d69 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceFixedClassServiceImpl.java @@ -0,0 +1,20 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceFixedClassMapper; +import jnpf.attendance.service.AttendanceFixedClassService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceFixedClassEntity; +import org.springframework.stereotype.Service; + +/** + *

+ * 固定班关联表 服务实现类 + *

+ * + * @author ahua + * @since 2024-05-10 + */ +@Service +public class AttendanceFixedClassServiceImpl extends SuperServiceImpl implements AttendanceFixedClassService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceGroupServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceGroupServiceImpl.java new file mode 100644 index 0000000..5520f77 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceGroupServiceImpl.java @@ -0,0 +1,2480 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.AttendanceUserListGroupVO; +import jnpf.attendance.mapper.*; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.authority.FtbAuthorityApi; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.base.vo.PageListVO; +import jnpf.constants.AttendancePermissionConstant; +import jnpf.constants.RedisConstant; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.*; +import jnpf.entity.attendance.AppointPermission; +import jnpf.entity.attendance.AttendanceLocationSetting; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingConfig; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.GroupUserTypeEnum; +import jnpf.enums.attendance.PermissionTypeEnum; +import jnpf.enums.attendance.v2.FuncCodingEnum; +import jnpf.exception.HandleException; +import jnpf.exception.LoginException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.model.AttendanceNoticeModel; +import jnpf.model.attendance.model.RuleChangeNoticeModel; +import jnpf.model.attendance.model.UserChangeNoticeModel; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.GroupLockVo; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.organzie.QueryOrganizeListTargetTypesDTO; +import jnpf.permission.eum.v2.NodeTypeEnum; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.util.*; +import jnpf.util.mapper.MybatisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jnpf.constants.RedisConstant.ATTENDANCE_BASE_SETTING_CACHE_KEY; + +/** + * 打卡服务实现 + * + * @author yanwenfu + * @create 2023-11-21 + */ +@Service +@Slf4j +public class AttendanceGroupServiceImpl extends SuperServiceImpl implements AttendanceGroupService { + + private final static String GROUP_TOP_PARENT_ID = "000000"; + /** + * 考勤组id threadlocal + */ + private static final ThreadLocal> GROUP_ID_LOCAL = ThreadLocal.withInitial(ArrayList::new); + @Resource + private AttendanceMachineManageMapper attendanceMachineManageMapper; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Resource + private UserProvider userProvider; + @Resource + private AttendanceGroupUserMapper attendanceGroupUserMapper; + @Resource + private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper; + @Resource + private AttendanceApprovalSettingService attendanceApprovalSettingService; + @Resource + private AttendanceApprovalSettingMapper attendanceApprovalSettingMapper; + @Resource + private AttendanceSuperAdminService attendanceSuperAdminService; + @Autowired + private AttendanceBaseSettingService attendanceBaseSettingService; + @Autowired + private AttendanceShiftSettingService attendanceShiftSettingService; + @Autowired + private AttendanceFestivalSettingService attendanceFestivalSettingService; + @Autowired + private AttendanceHolidaySettingService attendanceHolidaySettingService; + @Autowired + private AttendanceLocationSettingService attendanceLocationSettingService; + @Resource + private RedisTemplate redisTemplate; + @Resource + private AttenceMachineService attenceMachineService; + @Autowired + private AttendanceMachineManageService attendanceMachineManageService; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + @Resource + private AttendanceClockInService attendanceClockInService; + @Autowired + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private PermissionDictMapper permissionDictMapper; + @Resource + private V2UserApi v2UserApi; + @Resource + private V2OrganizeApi v2OrganizeApi; + @Resource + private FtbAuthorityApi ftbAuthorityApi; + @Autowired + private FtbPermissionOrganizeService ftbPermissionOrganizeService; + @Autowired + @Lazy + private PermissionsUtils permissionsUtils; + @Autowired + private AttendanceLineSchedulingConfigService lineSchedulingConfigService; + @Autowired + private FtbPersonnelsRosterManagerApi ftbPersonnelsRosterManagerApi; + + @Transactional(rollbackFor = Exception.class) + @Override + public int save(SaveAttendanceGroupDto saveAttendanceGroupDto) throws HandleException { + AttendanceGroup byGroupName = MybatisUtil.findByFiled(attendanceGroupMapper, AttendanceGroup::getGroupName, saveAttendanceGroupDto.getGroupName(), true); + /* 根据名称查找考勤组*/ + AttendanceGroup attendanceGroup = JsonUtil.getJsonToBean(saveAttendanceGroupDto, AttendanceGroup.class); + /*id为空则新增考勤组*/ + if (saveAttendanceGroupDto.getId() == null) { + if (byGroupName != null) { +// throw new HandleException("该考勤组已存在!"); + return -1; + } + String uuId = RandomUtil.uuId(); + attendanceGroup.setId(uuId); + /* 基础字段*/ + attendanceGroup.setCreatorUserId(userProvider.get().getUserId()); + attendanceGroup.setCreatorTime(new Date()); + attendanceGroup.setParentId(StrUtil.isBlank(saveAttendanceGroupDto.getParentId()) ? GROUP_TOP_PARENT_ID : saveAttendanceGroupDto.getParentId()); + List group = new ArrayList<>(); + group.add(attendanceGroup); + /* 找到父级节点,维护考勤组层级编码*/ + recursionParent(attendanceGroup.getParentId(), group); + String levelCode = CollectionUtil.join(group.stream().map(AttendanceGroup::getId).collect(Collectors.toList()), "#"); + attendanceGroup.setLevelCode(levelCode); + List reverse = CollectionUtil.reverse(group); + attendanceGroup.setDetailName(CollectionUtil.join(reverse.stream().map(AttendanceGroup::getGroupName).collect(Collectors.toList()), "/")); + attendanceGroupMapper.insert(attendanceGroup); + + /* 新增考勤组审批设置模版*/ + attendanceApprovalSettingService.addTemplateSetting(uuId); + return 0; + } + /*id不为空则修改考勤组信息*/ + if (byGroupName != null && !byGroupName.getId().equals(saveAttendanceGroupDto.getId())) { +// throw new HandleException("该考勤组已存在!"); + return -1; + } + /* 基础字段*/ + attendanceGroup.setLastModifyTime(new Date()); + attendanceGroup.setLastModifyUserId(userProvider.get().getUserId()); + attendanceGroupMapper.updateById(attendanceGroup); + return 0; + } + + @Override + public List getGroupIdByUserId(String userId, Date applyStart, Date applyEnd) throws HandleException { + List userIds = CollUtil.newArrayList(userId); + List users = attendanceUserService.queryByUsersIds(userIds); + if (CollUtil.isEmpty(users)) { + log.error("未查询到所有考勤组人员信息,{}", JSON.toJSONString(userIds)); + throw new HandleException("未查询到所有考勤组人员信息"); + } + Date start = Objects.isNull(applyStart) ? DateUtil.getDayBegin() : applyStart; + Date end = Objects.isNull(applyEnd) ? DateUtil.getDayEnd() : applyEnd; + return getGroupIdByUserId(users, start, end); + } + + @Override + public List getGroupIdByUserId(List users, Date start, Date end) { + return users.stream().filter(user -> { + List secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? null : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class); + if (user.getUserGroupType() == 2) { + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.FALSE; + } + for (SecondmentDateVo dateVo : secondmentDateVos) { + return DateDetail.checkTimeBetween(start, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(end, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(dateVo.getStartTime(), start, end) + || DateDetail.checkTimeBetween(dateVo.getEndTime(), start, end); + } + } else { + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.TRUE; + } + for (SecondmentDateVo dateVo : secondmentDateVos) { + return !(DateDetail.checkTimeBetween(start, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(end, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(dateVo.getStartTime(), start, end) + || DateDetail.checkTimeBetween(dateVo.getEndTime(), start, end)); + } + } + return Boolean.FALSE; + }).map(AttendanceGroupUser::getGroupId).collect(Collectors.toList()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delete(String id, String parentId) { + /* 直接删除所有子考勤组*/ + /* 需要删除的子考勤组id集合*/ + QueryWrapper queryChildGroupWrapper = new QueryWrapper<>(); + queryChildGroupWrapper.lambda() + .like(AttendanceGroup::getLevelCode, id); + List attendanceGroups = attendanceGroupMapper.selectList(queryChildGroupWrapper); + List deleteGroupIds = attendanceGroups.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); + + /* 查询被删除考勤组下所有成员*/ + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .in(AttendanceGroupUser::getGroupId, deleteGroupIds) + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + // 查询被删除考勤组和移动新成员的考勤组对应的考勤机配置 + attendanceMachineManageService.removeMachineRelation(groupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()), parentId, deleteGroupIds); + if (CollUtil.isEmpty(groupUserList)) { + deleteGroup(deleteGroupIds); + return; + } + + /* 删除本组以及子考勤组所关联成员及借调数据*/ + List collect = groupUserList.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + LambdaUpdateWrapper in = new UpdateWrapper().lambda() + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .in(AttendanceGroupUser::getUserId, collect); + AttendanceGroupUser deleteGroupUser = new AttendanceGroupUser(); + deleteGroupUser.setDeleteMark(1); + String nullJson = JsonUtil.getJsonToBean(new ArrayList<>(), String.class); + deleteGroupUser.setTimeJson(nullJson); + deleteGroupUser.setRemoveTime(new Date()); + attendanceGroupUserMapper.update(deleteGroupUser, in); + + /* 删除审批配置配置*/ + UpdateWrapper deleteWrapper = new UpdateWrapper<>(); + deleteWrapper.lambda() + .in(AttendanceApprovalSetting::getGroupId, deleteGroupIds); + attendanceApprovalSettingMapper.delete(deleteWrapper); + /* 移除考勤机*/ + Map> userGroupByGroupRuleMap = groupUserList.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + userGroupByGroupRuleMap.forEach((k, v) -> { + log.error("删除考勤组,清除规则的考勤组【{}】用户集合为{}", k, JSON.toJSONString(v)); + List userIds = v.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + /* 移除考勤规则*/ + attendanceDailyRuleService.clearGroupRule(k, userIds); + }); + deleteGroup(deleteGroupIds); + /* 成员移动到所选考勤组考勤组*/ + List userIds = groupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (StrUtil.isNotBlank(parentId) && CollectionUtil.isNotEmpty(groupUserList)) { + List addGroupUserList = new ArrayList<>(); + for (String userId : userIds) { + if (CollectionUtil.isNotEmpty(addGroupUserList)) { + /* 避免用户操作太快,幂等问题*/ + List addUserIds = addGroupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (addUserIds.contains(userId)) { + continue; + } + } + AttendanceGroupUser groupUser = new AttendanceGroupUser(); + groupUser.setId(RandomUtil.uuId()); + groupUser.setGroupId(parentId); + groupUser.setUserId(userId); + groupUser.setType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setUserGroupType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setCreatorTime(new Date()); + groupUser.setCreatorUserId(UserProvider.getLoginUserId()); + addGroupUserList.add(groupUser); + } + attendanceUserService.saveBatch(addGroupUserList); + attendanceDayStatisticsService.userJoinHandleData(UserProvider.getUser().getTenantId(), parentId, userIds); + attendanceDailyRuleService.addUserFixedHandle(parentId, userIds); + // V1.8.1批量移动考勤组权限 + attendanceManagerPermissionMapper.updateBatchUserGroupManagerPermission(userIds, id, parentId); + sendNoticeByUserChange(parentId, userIds, UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.GROUP_CHANGE_REMOVE_JOIN_GROUP); + userIds.forEach(userId -> attendanceClockInService.generateRepairNumForUser(parentId, userId, 1)); + return; + } + sendNoticeByUserChange(null, userIds, UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.REMOVE_USER_REMOVE_GROUP); + } + + /** + * 删除考勤组以及子考勤组 + * + * @param deleteGroupIds 要删除的考勤组ID列表 + */ + private void deleteGroup(List deleteGroupIds) { + // 使用更新条件包装器来准备删除条件 + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + // 通过lambda表达式设置条件,筛选出需要删除的考勤组ID + updateWrapper.lambda() + .in(AttendanceGroup::getId, deleteGroupIds); + // 准备更新的数据对象,设置标志位和其他相关字段 + AttendanceGroup update = new AttendanceGroup(); + update.setDeleteMark(1); // 设置删除标志位为1,表示已删除 + update.setDeleteUserId(userProvider.get().getUserId()); // 设置执行删除操作的用户ID + update.setLastModifyTime(new Date()); // 设置最后修改时间为当前时间 + // 执行更新操作,将符合条件的考勤组标记为已删除 + attendanceGroupMapper.update(update, updateWrapper); + } + + + private void sendNoticeByUserChange(String joinGroupId, List userIds, String tenantId, AttendanceNoticeEnum attendanceNoticeEnum) { + UserChangeNoticeModel userChangeNoticeModel = new UserChangeNoticeModel(); + userChangeNoticeModel.setJoinGroupId(joinGroupId); + userChangeNoticeModel.setUserIds(userIds); + userChangeNoticeModel.setTenantId(tenantId); + userChangeNoticeModel.setAttendanceNoticeEnum(attendanceNoticeEnum); + attendanceNoticeHandler.send(userChangeNoticeModel); + } + + @Override + public void exchangeGroupStatus(AttendanceGroupStatusDto dto) { + AttendanceGroup attendanceGroup = new AttendanceGroup(); + attendanceGroup.setId(dto.getGroupId()); + if (Objects.nonNull(dto.getLineSchedulingSetting())) { + attendanceGroup.setLineSchedulingSetting(dto.getLineSchedulingSetting()); + attendanceGroupMapper.updateById(attendanceGroup); + return; + } + attendanceGroup.setHolidaySetting(dto.getHolidaySetting()); + attendanceGroup.setFestivalSetting(dto.getFestivalSetting()); + attendanceGroup.setLeaveSetting(dto.getLeaveSetting()); + attendanceGroup.setGpsEnable(dto.getGpsEnable()); + attendanceGroup.setWifiEnable(dto.getWifiEnable()); + attendanceGroup.setMachineEnable(dto.getMachineEnable()); + AttendanceGroupStatusDto attendanceGroupNew = groupSettingStatus(dto.getGroupId(), Boolean.TRUE); + if (Objects.nonNull(dto.getBaseSetting())) { + if (Objects.equals(1, dto.getBaseSetting())) { + attendanceBaseSettingService.initBaseSetting(dto.getGroupId()); + } + attendanceGroup.setBaseSetting(dto.getBaseSetting()); + AttendanceBaseSettingVo one = attendanceBaseSettingService.getOne(dto.getGroupId()); + attendanceBaseSettingService.changeStatus(dto.getGroupId(), dto.getBaseSetting(), one); + attendanceGroupMapper.updateById(attendanceGroup); + return; + } + if (Objects.nonNull(dto.getLeaveSetting())) { +// if (Objects.equals(1, dto.getLeaveSetting())) { +// try { +// attendanceApprovalService.initializationLeaveSetting(dto.getGroupId()); +// } catch (HandleException e) { +// e.printStackTrace(); +// log.error("初始化考勤组[{}]请假配置异常", dto.getGroupId()); +// } +// } + attendanceGroupMapper.updateById(attendanceGroup); + } + if (Objects.nonNull(dto.getAttendanceClassSetting())) { + if (Objects.equals(1, dto.getAttendanceClassSetting())) { + attendanceShiftSettingService.initShiftSetting(dto.getGroupId()); + } + attendanceGroup.setAttendanceClassSetting(dto.getAttendanceClassSetting()); + AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findByGroupId(dto.getGroupId()); + attendanceGroupMapper.updateById(attendanceGroup); + attendanceShiftSettingService.changeStatus(dto.getGroupId(), dto.getAttendanceClassSetting(), byGroupId); + return; + } + if (Objects.nonNull(dto.getFestivalSetting())) { + if (Objects.equals(1, dto.getFestivalSetting())) { + attendanceFestivalSettingService.initFestivalSetting(dto.getGroupId()); + } + attendanceGroupMapper.updateById(attendanceGroup); + return; + } + if (Objects.nonNull(dto.getHolidaySetting())) { + if (Objects.equals(1, dto.getHolidaySetting())) { + attendanceHolidaySettingService.initHolidaySetting(dto.getGroupId()); + } + attendanceGroupMapper.updateById(attendanceGroup); + return; + } + String parentGroupId = attendanceGroupMapper.getParentGroupId(dto.getGroupId()); + if (Objects.equals(1, dto.getAttendancePointsSetting())) { + // 开启配置 + attendanceLocationSettingService.initLocationSetting(dto.getGroupId()); + attendanceGroup.setAttendancePointsSetting(dto.getAttendancePointsSetting()); + attendanceGroupMapper.updateById(attendanceGroup); + // 查询父级考勤机的配置 + List locationSettings = getGroupLocationSettings(parentGroupId); + machineDeal(locationSettings, dto.getGroupId(), ConstantUtil.CAL_REDUCE); + } else if (Objects.equals(0, dto.getAttendancePointsSetting())) { + attendanceGroup.setAttendancePointsSetting(dto.getAttendancePointsSetting()); + attendanceGroupMapper.updateById(attendanceGroup); + // 关闭配置, 不移除自定义配置考勤机中的成员, 新增本级及继承子级的考勤组成员到父级考勤机中 + List locationSettings = getGroupLocationSettings(parentGroupId); + machineDeal(locationSettings, dto.getGroupId(), ConstantUtil.CAL_ADD); + } + if (Objects.nonNull(dto.getAttendancePointsSetting())) { + AttendanceGroup attendanceGroupOld = getEnableLocationSetting(parentGroupId); + if (attendanceGroupOld != null && !attendanceGroupNew.getGpsEnable().equals(attendanceGroupOld.getGpsEnable())) { + List addressesList = getAddressesList(dto.getAttendancePointsSetting().equals(0) ? attendanceGroupOld.getId() : attendanceGroupNew.getGroupId(), 1); + List selfAndChildrenGroupIds = getSelfAndChildrenGroupIds(dto.getGroupId(), 2); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, attendanceGroupNew.getGpsEnable(), dto.getAttendancePointsSetting().equals(0) ? attendanceGroupOld.getGpsEnable() : attendanceGroupNew.getGpsEnable(), addressesList, AttendanceNoticeEnum.RULE_CHANGE_GPS_SWIFT)); + } + if (attendanceGroupOld != null && !attendanceGroupNew.getWifiEnable().equals(attendanceGroupOld.getWifiEnable())) { + List addressesList = getAddressesList(dto.getAttendancePointsSetting().equals(0) ? attendanceGroupOld.getId() : attendanceGroupNew.getGroupId(), 2); + List selfAndChildrenGroupIds = getSelfAndChildrenGroupIds(dto.getGroupId(), 2); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, attendanceGroupNew.getWifiEnable(), dto.getAttendancePointsSetting().equals(0) ? attendanceGroupOld.getWifiEnable() : attendanceGroupNew.getWifiEnable(), addressesList, AttendanceNoticeEnum.RULE_CHANGE_WIFI_SWIFT)); + } + if (attendanceGroupOld != null && !attendanceGroupNew.getMachineEnable().equals(attendanceGroupOld.getMachineEnable())) { + List addressesList = getAddressesList(dto.getAttendancePointsSetting().equals(0) ? attendanceGroupOld.getId() : attendanceGroupNew.getGroupId(), 3); + List selfAndChildrenGroupIds = getSelfAndChildrenGroupIds(dto.getGroupId(), 2); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, attendanceGroupNew.getMachineEnable(), dto.getAttendancePointsSetting().equals(0) ? attendanceGroupOld.getMachineEnable() : attendanceGroupNew.getMachineEnable(), addressesList, AttendanceNoticeEnum.RULE_CHANGE_KQJ_SWIFT)); + } + } + if (Objects.nonNull(dto.getGpsEnable())) { + attendanceLocationSettingService.changeStatus(dto.getGroupId(), 1, dto.getGpsEnable()); + attendanceGroupMapper.updateById(attendanceGroup); + List selfAndChildrenGroupIds = getSelfAndChildrenGroupIds(dto.getGroupId(), 2); + List addressesList = getAddressesList(dto.getGroupId(), 1); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, null, dto.getGpsEnable(), addressesList, AttendanceNoticeEnum.RULE_CHANGE_GPS_SWIFT)); + } + if (Objects.nonNull(dto.getWifiEnable())) { + attendanceLocationSettingService.changeStatus(dto.getGroupId(), 2, dto.getWifiEnable()); + attendanceGroupMapper.updateById(attendanceGroup); + List addressesList = getAddressesList(dto.getGroupId(), 2); + List selfAndChildrenGroupIds = getSelfAndChildrenGroupIds(dto.getGroupId(), 2); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, null, dto.getWifiEnable(), addressesList, AttendanceNoticeEnum.RULE_CHANGE_WIFI_SWIFT)); + } + if (Objects.nonNull(dto.getMachineEnable())) { + attendanceLocationSettingService.changeStatus(dto.getGroupId(), 3, dto.getMachineEnable()); + attendanceGroupMapper.updateById(attendanceGroup); + List addressesList = getAddressesList(dto.getGroupId(), 3); + List selfAndChildrenGroupIds = getSelfAndChildrenGroupIds(dto.getGroupId(), 2); + selfAndChildrenGroupIds.forEach(itemGroupId -> sendNoticeItem(itemGroupId, null, dto.getMachineEnable(), addressesList, AttendanceNoticeEnum.RULE_CHANGE_KQJ_SWIFT)); + } + } + + private List getAddressesList(String groupId, Integer type) { + List addressesList = new ArrayList<>(); + List list = attendanceLocationSettingService.findList(groupId, type); + if (CollUtil.isNotEmpty(list)) { + list.forEach(vo -> { + StringBuilder sb = new StringBuilder(); + if (type.equals(1)) { + sb.append(vo.getName()).append("(").append(vo.getClockRange()).append("米)").append(":").append(vo.getAddress()); + } else { + sb.append(vo.getName()); + } + addressesList.add(sb.toString()); + }); + } + return addressesList; + } + + + private void sendNoticeItem(String groupId, Object oldValue, Object newValue, List addressList, AttendanceNoticeEnum attendanceNoticeEnum) { + if (Objects.equals(oldValue, newValue)) { + return; + } + RuleChangeNoticeModel ruleChangeNoticeModel = new RuleChangeNoticeModel(); + ruleChangeNoticeModel.setIsAdmin(Boolean.FALSE); + ruleChangeNoticeModel.setOldValue(oldValue); + ruleChangeNoticeModel.setNewValue(newValue); + ruleChangeNoticeModel.setAddressList(addressList); + ruleChangeNoticeModel.setGroupId(groupId); + ruleChangeNoticeModel.setTenantId(UserProvider.getUser().getTenantId()); + ruleChangeNoticeModel.setAttendanceNoticeEnum(attendanceNoticeEnum); + attendanceNoticeHandler.send(ruleChangeNoticeModel); + ruleChangeNoticeModel.setIsAdmin(Boolean.TRUE); + attendanceNoticeHandler.send(ruleChangeNoticeModel); + } + + private void machineDeal(List locationSettings, String groupId, String method) { + + if (!locationSettings.isEmpty()) { + // 查询本级和继承自己的考勤组成员 + List userIdList = getSelfAndChildrenMembers(groupId); + if (!userIdList.isEmpty()) { + if (method.equals(ConstantUtil.CAL_REDUCE)) { + locationSettings.forEach(locationSetting -> attenceMachineService.deleteUserList(locationSetting.getFactoryCode(), locationSetting.getAddress(), userIdList)); + } else { + locationSettings.forEach(locationSetting -> { + userIdList.forEach(userId -> { + attenceMachineService.sendUserToMachine(locationSetting.getFactoryCode(), userId, locationSetting.getAddress()); + }); + }); + } + } + } + } + + private List getGroupLocationSettings(String groupId) { + + Map> locationSettingMap = attendanceLocationSettingService.getEnableLocationSetting(Stream.of(groupId).collect(Collectors.toList())); + return locationSettingMap.get(groupId).stream().filter(v -> v.getType().equals(ConstantUtil.DEVICE_MACHINE)).collect(Collectors.toList()); + } + + @Override + public List getSelfAndChildrenGroupIds(String groupId, Integer settingType) { + List list = attendanceGroupMapper.getSelfAndChildrenGroup(groupId); + List levelCodes = CollUtil.newArrayList(); + if (Objects.equals(settingType, 1)) { + levelCodes.addAll(list.stream().filter(vo -> Objects.equals(vo.getBaseSetting(), 1) && !vo.getLevelCode().startsWith(groupId)).map(GroupNodeVo::getLevelCode).collect(Collectors.toList())); + } else if (Objects.equals(settingType, 2)) { + levelCodes.addAll(list.stream().filter(vo -> Objects.equals(vo.getAttendancePointsSetting(), 1) && !vo.getLevelCode().startsWith(groupId)).map(GroupNodeVo::getLevelCode).collect(Collectors.toList())); + } else if (Objects.equals(settingType, 3)) { + levelCodes.addAll(list.stream().filter(vo -> Objects.equals(vo.getAttendanceClassSetting(), 1) && !vo.getLevelCode().startsWith(groupId)).map(GroupNodeVo::getLevelCode).collect(Collectors.toList())); + } + list = list.stream().filter(vo -> levelCodes.stream().noneMatch(levelCode -> vo.getLevelCode().endsWith(levelCode))).collect(Collectors.toList()); + return list.stream().map(GroupNodeVo::getId).collect(Collectors.toList()); + } + + @Override + public List getSelfAndChildrenMembers(String groupId) { + + // 查询本组及子组考勤组 + List list = attendanceGroupMapper.getSelfAndChildrenGroup(groupId); + GroupNodeVo groupNode = list.stream().filter(v -> v.getId().equals(groupId)).findFirst().orElse(null); + assert groupNode != null; + // 生成树 + List tree = getGroupTree(list, groupNode); + // 遍历树获取考勤组id + List groupIdList = new ArrayList<>(); + getGroupIdList(tree, groupIdList); + // 查询考勤组成员 + if (groupIdList.isEmpty()) { + return new ArrayList<>(); + } + return attendanceGroupUserMapper.getGroupUserListByIds(groupIdList); + } + + private List getGroupTree(List list, GroupNodeVo groupNode) { + + List tree = new ArrayList<>(); + for (GroupNodeVo item : list) { + if (item.getPid().equals(groupNode.getPid())) { + tree.add(item); + } + // 寻找子节点 + for (GroupNodeVo treeNode : list) { + if (treeNode.getPid().equals(item.getId())) { + if (treeNode.getAttendancePointsSetting().equals(ConstantUtil.NUM_FALSE)) { + item.getChildren().add(treeNode); + } + } + } + } + return tree; + } + + private void getGroupIdList(List list, List groupIdList) { + + if (!list.isEmpty()) { + for (GroupNodeVo groupNode : list) { + groupIdList.add(groupNode.getId()); + getGroupIdList(groupNode.getChildren(), groupIdList); + } + } + } + + /** + * 查询考勤组管理列表 + * + * @param groupName 考勤组名称 + * @return List + */ + @Override + public List queryList(String groupName) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroup::getDeleteMark, 0) + .like(StringUtil.isNotEmpty(groupName), + AttendanceGroup::getGroupName, + groupName); + List attendanceGroupList = attendanceGroupMapper.selectList(queryWrapper); + return JsonUtil.getJsonToList(attendanceGroupList, AttendanceGroupVo.class); + } + + /** + * 查询考勤组管理列表 + * + * @param groupName 考勤组名称 + * @return List + */ + public List queryList(String groupName, List orgIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroup::getDeleteMark, 0) + .in(StringUtil.isNotEmpty(orgIds), + AttendanceGroup::getOrgId, orgIds) + .like(StringUtil.isNotEmpty(groupName), + AttendanceGroup::getGroupName, + groupName); + return attendanceGroupMapper.selectList(queryWrapper); + } + + @Override + public List queryListIncludeDeleteByIds(List groupIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .in(StringUtil.isNotEmpty(groupIds), + AttendanceGroup::getId, groupIds); + return attendanceGroupMapper.selectList(queryWrapper); + } + + @Override + public List queryListByIds(List groupIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroup::getDeleteMark, 0) + .in(StringUtil.isNotEmpty(groupIds), + AttendanceGroup::getId, groupIds); + + return attendanceGroupMapper.selectList(queryWrapper); + } + + @Override + public List queryAllListByIds() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda(); + return attendanceGroupMapper.selectList(queryWrapper); + } + + /** + * 查询我管理的考勤组 + * + * @return List + */ + @Override + public List queryManagerGroupList(String keyword) { + String userId = userProvider.get().getUserId(); + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + if (attendanceSuperAdminService.isSuperAdmin(userId) + || sysAdmin + ) { + /* 如果是考勤超级管理员、则直接返回所有考勤组*/ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroup::getDeleteMark, 0); + return queryList(keyword); + } + /* 返回我管理的考勤组*/ + List attendanceGroupList = attendanceGroupMapper.queryManagerGroupList(userId, keyword); + return JsonUtil.getJsonToList(attendanceGroupList, AttendanceGroupVo.class); +// if (CollectionUtil.isEmpty(attendanceGroupList)) { +// return new ArrayList<>(); +// } +// List levelCodeList = attendanceGroupList.stream().map(AttendanceGroup::getLevelCode).collect(Collectors.toList()); +// +// List groupIdList = new ArrayList<>(); +// for (String levelCode : levelCodeList) { +// List parentIdsByLevelCode = getParentIdsByLevelCode(levelCode, true); +// groupIdList.addAll(parentIdsByLevelCode); +// } +// List allGroupVoList = attendanceManagerPermissionMapper.queryGroupVoInGroupIds(groupIdList); + } + + /** + * 根据考勤组名称模糊查询考勤组列表(带权限控制) + * + * @param groupName 考勤组名称(支持模糊查询) + * @return List + */ + @Override + public List searchGroupByName(String groupName) { + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + if (sysAdmin) { + /* 超级管理员:返回所有匹配的考勤组 */ + return queryList(groupName); + } + + /* 普通用户:只返回有管理权限且名称匹配的考勤组 */ + List listActionResult = ftbAuthorityApi.authStoreBaseListInfo(); + List attendanceGroupList = queryList(groupName, listActionResult.stream().map(StoreBaseListInfo::getId).collect(Collectors.toList())); + return JsonUtil.getJsonToList(attendanceGroupList, AttendanceGroupVo.class); + } + + /** + * 查询考勤组下拉列表 + */ + @Override + public List queryDropList() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroup::getDeleteMark, 0); + return attendanceGroupMapper.selectList(queryWrapper); + } + + @Override + public AttendanceGroup queryByGroupId(String groupId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(AttendanceGroup::getDeleteMark, 0); + queryWrapper.eq(AttendanceGroup::getId, groupId); + queryWrapper.last("limit 1"); + return this.getOne(queryWrapper); + } + + /** + * 查询我所在的考勤组 + * + * @return List + */ + @Override + public List queryMyGroups() { + String loginUserId = UserProvider.getLoginUserId(); + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .eq(AttendanceGroupUser::getUserId, loginUserId); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + if (CollectionUtil.isEmpty(groupUserList)) { + return new ArrayList<>(); + } + QueryWrapper groupQueryWrapper = new QueryWrapper<>(); + groupQueryWrapper.lambda() + .in(AttendanceGroup::getId, groupUserList.stream().map(AttendanceGroupUser::getGroupId).collect(Collectors.toList())) + .eq(AttendanceGroup::getDeleteMark, 0); + return attendanceGroupMapper.selectList(groupQueryWrapper); + } + + /** + * 考勤组管理 获取我管理的考勤组(树状结构展示) + * + * @return List + */ + @Override + public List groupManagerList(GroupQueryDto groupQueryDto) { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupList(loginUserId, groupQueryDto); +// List groupVoList = queryGroupListNew(loginUserId, groupQueryDto); + /* 按照首字母排序*/ + sortByName(groupVoList); +// List treeGroupList = treeGroupVo(groupVoList); + + return treeMyManagerGroupVo(groupVoList); + } + + @Override + public List getEnableGroupIds(String groupId) { + List enableGroup = getEnableGroup(groupId); + List collect = enableGroup.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + collect.add(groupId); + return collect; + } + + private List getEnableGroup(String groupId) { + List attendanceGroupVos = queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return CollUtil.newArrayList(); + } + AttendanceGroup attendanceGroup = attendanceGroupVos.stream().filter(group -> StringUtil.equals(group.getId(), groupId)).reduce((g1, g2) -> g1).orElse(null); + if (attendanceGroup != null && Objects.equals(attendanceGroup.getAttendanceClassSetting(), 0)) { + log.error("当前考勤组未开启班制配置"); + return CollUtil.newArrayList(); + } + Map> groupIdToParent = attendanceGroupVos.stream().collect(Collectors.groupingBy(AttendanceGroup::getParentId)); + List groupVos = CollUtil.newArrayList(); + List orDefault = groupIdToParent.getOrDefault(groupId, CollUtil.newArrayList()); + if (CollUtil.isEmpty(orDefault)) { + return CollUtil.newArrayList(); + } + getEnableSingleGroup(orDefault, groupVos, groupIdToParent); + return groupVos; + } + + private void getEnableSingleGroup(List groups, List groupVos, Map> groupIdToParent) { + groups.forEach(group -> { + if (Objects.equals(group.getAttendanceClassSetting(), 1)) { + return; + } + groupVos.add(BeanUtil.toBean(group, AttendanceGroupVo.class)); + List attendanceGroups = groupIdToParent.get(group.getId()); + if (CollUtil.isEmpty(attendanceGroups)) { + return; + } + getEnableSingleGroup(attendanceGroups, groupVos, groupIdToParent); + }); + } + + /** + * 查询所有权限并赋予权限 + * + * @param loginUserId 登录用户Id + * @return List + */ + public List queryGroupListSetPermission(String loginUserId, GroupQueryDto groupQueryDto) { + if (Objects.isNull(groupQueryDto)) { + groupQueryDto = new GroupQueryDto(); + } + return queryGroupList(loginUserId, groupQueryDto); + } + + /** + * 查询所有权限并赋予权限 + * + * @return List + */ + public List queryGroupList(String loginUserId, GroupQueryDto groupQueryDto) { + Boolean isAdmin = attendanceSuperAdminService.isSuperAdmin(loginUserId); + /* 查询所有考勤组 V1.8.1 改为只查询本组权限 */ + List groupVoList = attendanceGroupMapper.getAllGroupList(groupQueryDto, null, null); + if (isAdmin || userProvider.get().getIsAdministrator()) { + + /* 如果是超级管理员则全部赋予重命名和删除权限*/ + groupVoList.forEach(groupVo -> { + groupVo.setIsAdmin(true); + groupVo.setAddChild(true); + groupVo.setRename(true); + groupVo.setDelete(true); + groupVo.setIsManager(true); + groupVo.setScheduling(true); + groupVo.setSecondedApproval(true); + groupVo.setBalanceManagement(true); + groupVo.setSecondedUser(true); + //2024-04-22新增绑定组织 + groupVo.setBindingOrg(true); + if (groupVo.getId().equals(GROUP_TOP_PARENT_ID)) { + groupVo.setDelete(false); + } + }); + return groupVoList; + } + if (CollectionUtil.isEmpty(groupVoList)) { + return new ArrayList<>(); + } + + /* 找到我管理的考勤组所有的levelCode*/ + List levelCodes = groupVoList.stream().map(AttendanceGroupVo::getLevelCode).collect(Collectors.toList()); + List groupIdList = new ArrayList<>(); + levelCodes.forEach(levelCode -> { + List parentIdsByLevelCode = getParentIdsByLevelCode(levelCode, true); + groupIdList.addAll(parentIdsByLevelCode); + }); + + if (CollectionUtil.isEmpty(groupIdList)) { + return treeGroupVo(groupVoList); + } + + Map map = new HashMap<>(); + if (CollectionUtil.isNotEmpty(groupIdList)) { + /* 查询有子级考勤组管理权限的考勤组信息列表*/ + List permissionVos = attendanceManagerPermissionMapper.queryInGroupIds(groupIdList, loginUserId); + permissionVos.forEach(vo -> { + map.put(vo.getGroupId(), vo.getAllPermissions()); + }); + } + + /* 获取我所属当前组权限*/ + List myGroupList = attendanceGroupMapper.getAllGroupList(groupQueryDto, loginUserId, PermissionTypeEnum.CUR.getCode()); + Map myGroupPermissionMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(myGroupList)) { + for (AttendanceGroupVo groupVo : myGroupList) { + myGroupPermissionMap.put(groupVo.getId(), groupVo.getAllPermissions()); + } + } + + /* 当前考勤组权限*/ + for (AttendanceGroupVo attendanceGroupVo : groupVoList) { + String myPermissions = myGroupPermissionMap.get(attendanceGroupVo.getId()); + if (StrUtil.isBlank(myPermissions)) { + attendanceGroupVo.setIsManager(false); + continue; + } + attendanceGroupVo.setIsManager(true); + attendanceGroupVo.setAddChild(myPermissions.contains(AttendancePermissionConstant.ADD_CHILD_GROUP)); + attendanceGroupVo.setRename(myPermissions.contains(AttendancePermissionConstant.RENAME)); + attendanceGroupVo.setDelete(myPermissions.contains(AttendancePermissionConstant.DELETE)); + attendanceGroupVo.setScheduling(myPermissions.contains(AttendancePermissionConstant.SCHEDULING)); + attendanceGroupVo.setBalanceManagement(myPermissions.contains(AttendancePermissionConstant.BALANCE_MANAGER)); + //2024-04-22新增绑定组织 + attendanceGroupVo.setBindingOrg(myPermissions.contains(AttendancePermissionConstant.BINDING_ORG)); + attendanceGroupVo.setSecondedApproval(myPermissions.contains(AttendancePermissionConstant.SECONDED_APPROVAL)); + attendanceGroupVo.setSecondedUser(myPermissions.contains(AttendancePermissionConstant.SECONDED_USER)); + } + + // 找到自己当前时间处于借调中的考勤组 + String userNowGroup = attendanceUserService.getUserNowGroup(); + if (null != userNowGroup) { + // 查询用户所属的考勤组 + QueryWrapper queryWrapper1 = new QueryWrapper<>(); + queryWrapper1.lambda() + .eq(AttendanceGroupUser::getUserGroupType, 1) + .isNull(AttendanceGroupUser::getRemoveTime) + .eq(AttendanceGroupUser::getUserId, userProvider.get().getUserId()); + AttendanceGroupUser groupUser = attendanceGroupUserMapper.selectOne(queryWrapper1); + // 判断是否是该考勤组管理员 + if (!userNowGroup.equals(groupUser.getGroupId())) { + // 当不是借调到组的管理员时 将借调到的考勤组赋值原组权限 + AttendanceGroupVo secondGroupVo = groupVoList.stream().filter(groupVo -> groupVo.getId().equals(userNowGroup)).findFirst().orElse(null); + AttendanceGroupVo yuanGroup = groupVoList.stream().filter(groupVo -> groupVo.getId().equals(groupUser.getGroupId())).findFirst().orElse(null); + if (yuanGroup != null) { + Objects.requireNonNull(secondGroupVo).setIsManager(yuanGroup.getIsManager()); + } + Objects.requireNonNull(secondGroupVo).setAddChild(Objects.requireNonNull(yuanGroup).getAddChild()); + secondGroupVo.setRename(yuanGroup.getRename()); + secondGroupVo.setDelete(yuanGroup.getDelete()); + secondGroupVo.setScheduling(yuanGroup.getScheduling()); + secondGroupVo.setBalanceManagement(yuanGroup.getBalanceManagement()); + //2024-04-22新增绑定组织 + secondGroupVo.setBindingOrg(yuanGroup.getBindingOrg()); + secondGroupVo.setSecondedApproval(yuanGroup.getSecondedApproval()); + secondGroupVo.setSecondedUser(yuanGroup.getSecondedUser()); + // 原组是否具有子集考勤组权限 + String childPermissions = map.get(yuanGroup.getId()); + if (!StringUtil.isEmpty(childPermissions)) { + map.put(userNowGroup, childPermissions); + } + } + } + + /* 子考勤组权限*/ + groupVoList.forEach(vo -> { + String allPermissions = myGroupPermissionMap.get(vo.getId()); + if (StrUtil.isBlank(allPermissions)) { + String levelCode = vo.getLevelCode(); + List parentGroupIds = getParentIdsByLevelCode(levelCode, false); + if (CollectionUtil.isNotEmpty(parentGroupIds)) { + for (String parent : parentGroupIds) { + String permissions = map.get(parent); + if (StrUtil.isNotBlank(permissions)) { + vo.setIsManager(true); + vo.setAddChild(permissions.contains(AttendancePermissionConstant.MANAGER)); + vo.setRename(permissions.contains(AttendancePermissionConstant.MANAGER)); + vo.setScheduling(permissions.contains(AttendancePermissionConstant.MANAGER)); + vo.setDelete(permissions.contains(AttendancePermissionConstant.DELETE)); + vo.setSecondedApproval(permissions.contains(AttendancePermissionConstant.SECONDED_APPROVAL)); + //2024-04-22新增绑定组织 + vo.setBindingOrg(permissions.contains(AttendancePermissionConstant.MANAGER)); + vo.setSecondedUser(permissions.contains(AttendancePermissionConstant.MANAGER)); + break; + } + } + } + } + }); + return groupVoList; + } + + + public List queryGroupListSetPermissionNew(String loginUserId, GroupQueryDto groupQueryDto) { + if (Objects.isNull(groupQueryDto)) { + groupQueryDto = new GroupQueryDto(); + } + return queryGroupListNew(loginUserId, groupQueryDto); + } + + /** + * v1.9 更换逻辑 + * + * @param loginUserId 用户 + * @param groupQueryDto 参数 + */ + @Override + public List queryGroupListNew(String loginUserId, GroupQueryDto groupQueryDto) { + + // 查看具有的权限列表 + FtbPermissionRoleIdentificationVO ftbPermissionRoleIdentificationVO = permissionsUtils.permissionIdentificationCollection(); + if (null == ftbPermissionRoleIdentificationVO) { + return new ArrayList<>(); + } + if (ftbPermissionRoleIdentificationVO.getIsSuperAdmin() == 1) { + // 超级管理员 + List allGroupList = attendanceGroupMapper.getAllGroupList(groupQueryDto, null, null); + allGroupList.forEach(v -> { + v.setScheduling(true); + v.setIsGray(false); + }); + return allGroupList; + } + if (null == ftbPermissionRoleIdentificationVO.getPermissionIdentifications() || ftbPermissionRoleIdentificationVO.getPermissionIdentifications().isEmpty()) { + // 没有页面权限 + return new ArrayList<>(); + } + if (ftbPermissionRoleIdentificationVO.getPermissionIdentifications().contains(FuncCodingEnum.NEWTYPESETTING.getValue()) || + ftbPermissionRoleIdentificationVO.getPermissionIdentifications().contains(FuncCodingEnum.ATTENDANCESCHEDUL.getValue()) || + ftbPermissionRoleIdentificationVO.getPermissionIdentifications().contains(FuncCodingEnum.SETUPSCHEDUL.getValue()) || + ftbPermissionRoleIdentificationVO.getPermissionIdentifications().contains(FuncCodingEnum.WORKFORCEMANAGE.getValue())) { + // 有排班权限 + // 获取用户指定模块的权限 + List organizeGeneralDetailVOS = ftbPermissionOrganizeService.authOrganizesByUserBound(null, null, false, false); + /* 查询所有考勤组 V1.8.1 改为只查询本组权限 */ + if (CollectionUtil.isEmpty(organizeGeneralDetailVOS)) { + // 当没有的时候,这个代表着没有权限 + return new ArrayList<>(); + } + List orgIds = organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); +// log.error("queryGroupListNew-- 用户具有的考勤组织权限 orgIds:{}", orgIds); + // 有值返回 通过返回的组织Id,查看绑定的考勤组列表 + List attendanceGroupVos = attendanceGroupMapper.getGroupListByOrgId(groupQueryDto, orgIds); + if (CollectionUtil.isEmpty(attendanceGroupVos)) { + attendanceGroupVos = new ArrayList<>(); + } + // 判断当前用户在当前时间是否处于借调 + // 找到自己当前时间处于借调中的考勤组 2.0 借调划归人事 +// String userNowGroup = attendanceUserService.getUserNowGroup(); +// if (StrUtil.isNotBlank(userNowGroup)) { +// AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(userNowGroup); +// if (null != groupDetail) { +// AttendanceGroupVo attendanceGroupVo = new AttendanceGroupVo(); +// BeanUtils.copyProperties(groupDetail, attendanceGroupVo); +// attendanceGroupVos.add(attendanceGroupVo); +// } +// } +// if (CollectionUtil.isEmpty(attendanceGroupVos)) { +// return new ArrayList<>(); +// } + // 通过考勤组的层级编码找到对应的所有考勤组 + List levelCodes = attendanceGroupVos.stream().map(AttendanceGroupVo::getLevelCode).collect(Collectors.toList()); + List groupIdList = new ArrayList<>(); + levelCodes.forEach(levelCode -> { + List parentIdsByLevelCode = getParentIdsByLevelCode(levelCode, true); + groupIdList.addAll(parentIdsByLevelCode); + }); + // 通过层级编码找到所有的考勤组 + List list = groupIdList.stream().distinct().collect(Collectors.toList()); + if (!CollectionUtil.isEmpty(list)) { + List groupVos = attendanceGroupMapper.getGroupListByIds(list); + List groupIds = attendanceGroupVos.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + // 将包含groupIds的考勤组 isgary 设置为false + groupVos.forEach(vo -> { + if (groupIds.contains(vo.getId())) { + vo.setScheduling(true); + vo.setIsGray(false); + } + }); + return groupVos; + } + } + return new ArrayList<>(); + } + + @Override + public List getOrgOrGroupList(String keyword) { + List nodeVOList = ftbAuthorityApi.listOrganizeTreeFilterNode(List.of(OrganizeCategoryEnums.COMPANY, + OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.TEAM), + null, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE); + // 获取所有考勤组 + List groupList = this.queryDropList(); + Map orgGroupMap = new HashMap<>(); + List groupIds = CollUtil.isNotEmpty(groupList) ? groupList.stream().map(AttendanceGroup::getId).collect(Collectors.toList()) : new ArrayList<>(); + Map shiftSettingMap = attendanceShiftSettingService.getEnableShiftSetting(groupIds, groupList); + if (CollUtil.isNotEmpty(groupList)) { + orgGroupMap = groupList.stream() + .filter(group -> StrUtil.isNotEmpty(group.getOrgId())) + .collect(Collectors.toMap(AttendanceGroup::getOrgId, a -> a, (k1, k2) -> k1)); + } + // 将树形结构转换为扁平化列表 + List result = new ArrayList<>(); + if (CollUtil.isNotEmpty(nodeVOList)) { + flattenTreeWithGroups(nodeVOList, result, orgGroupMap, shiftSettingMap); + } + // 如果有关键字筛选 + if (StrUtil.isNotBlank(keyword)) { + result = result.stream() + .filter(item -> StrUtil.containsIgnoreCase(item.getName(), keyword)) + .collect(Collectors.toList()); + } + return result; + } + + /** + * 递归遍历树形结构并转换为扁平化列表,同时包含考勤组信息 + * + * @param nodeList 树节点列表 + * @param result 结果列表 + * @param orgGroupMap 组织ID到考勤组列表的映射 + * @param shiftSettingMap 班次设置列表 + */ + private void flattenTreeWithGroups(List nodeList, + List result, + Map orgGroupMap, + Map shiftSettingMap) { + if (CollUtil.isEmpty(nodeList)) { + return; + } + for (OrganizeManagerFilterNodeVO node : nodeList) { + AttendanceGroup group = node.getNodeTypeEnum().equals(NodeTypeEnum.TEAM) ? + orgGroupMap.get(node.getPid()) : orgGroupMap.get(node.getId()); + AttendanceOrgOrGroupVo orgVo = new AttendanceOrgOrGroupVo(); + BeanUtils.copyProperties(node, orgVo); + if (Objects.nonNull(group)) { + orgVo.setGroupId(group.getId()); + orgVo.setIndependentRule(group.getBaseSetting()); + if (CollUtil.isNotEmpty(shiftSettingMap) && shiftSettingMap.containsKey(group.getId())) { + orgVo.setSystemType(Objects.nonNull(shiftSettingMap.get(group.getId())) ? shiftSettingMap.get(group.getId()).getSystemType() : null); + } + } + result.add(orgVo); + // 递归处理子节点 + if (CollUtil.isNotEmpty(node.getChildren())) { + flattenTreeWithGroups(node.getChildren(), orgVo.getChildrenList(), orgGroupMap, shiftSettingMap); + } + orgVo.getChildren().clear(); + } + } + + @Override + @Transactional + public Boolean setGroupCharge(AttendanceGroupChargeDto dto) { + AttendanceGroup group = this.getById(dto.getGroupId()); + Assert.notNull(group, "考勤组不存在"); + group.setManagerId(dto.getManagerId()); + return this.updateById(group); + } + + @Override + public AttendanceGroupChargeVo getGroupChargeInfo(String groupId) { + AttendanceGroup group = this.getById(groupId); + Assert.notNull(group, "考勤组不存在"); + AttendanceGroupChargeVo groupChargeVo = AttendanceGroupChargeVo.builder() + .managerId(StrUtil.isBlank(group.getManagerId()) ? null : group.getManagerId()) + .build(); + if (StrUtil.isNotBlank(group.getManagerId())) { + ActionResult> actionResult = v2UserApi.getAllUserInfoBatch( + List.of(group.getManagerId()), UserProvider.getUser().getTenantId()); + if (actionResult.getCode() == 200 && CollUtil.isNotEmpty(actionResult.getData())) { + actionResult.getData().stream().findFirst().ifPresent(user -> + groupChargeVo.setManagerName(user.getUserName())); + } + } + return groupChargeVo; + } + + @Override + public void removeManager(String groupId, List removeUserIds) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(AttendanceGroup::getId, groupId); + updateWrapper.isNotNull(AttendanceGroup::getManagerId); + updateWrapper.in(AttendanceGroup::getManagerId, removeUserIds); + updateWrapper.set(AttendanceGroup::getManagerId, null); + this.update(updateWrapper); + } + + + /** + * 根据levelCode获取考勤组父id + * + * @return List + */ + private List getParentIdsByLevelCode(String levelCode, Boolean hasCur) { + if (StrUtil.isBlank(levelCode)) { + return new ArrayList<>(); + } + String[] split = levelCode.split("#"); + List tempList = new ArrayList<>(); + CollectionUtil.addAll(tempList, split); + if (!hasCur) { + tempList.remove(0); + } + return new ArrayList<>(tempList); + } + + /** + * 借调开始 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void secondedStart(AttendanceSecondedDto attendanceSecondedDto) throws HandleException { + + List userIds = attendanceSecondedDto.getUserIds(); + if (CollectionUtil.isEmpty(userIds)) { + throw new HandleException("请选择借调人员"); + } + + /* 校验借调组是否已解散*/ + AttendanceGroup group = attendanceGroupMapper.selectById(attendanceSecondedDto.getGroupId()); + if (group.getDeleteMark() == 1) { + throw new HandleException("考勤组或借调组已不存在!"); + } + /* 校验被借调考勤组是否已解散*/ + AttendanceGroup selfGroup = attendanceGroupMapper.selectById(attendanceSecondedDto.getSelfGroupId()); + if (selfGroup.getDeleteMark() == 1) { + throw new HandleException("考勤组或借调组已不存在!"); + } + + /* 校验借调过程中借调人员是否离开借调考勤组*/ + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .eq(AttendanceGroupUser::getGroupId, attendanceSecondedDto.getSelfGroupId()) + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()); + List groupUserList = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + + if (CollectionUtil.isEmpty(groupUserList)) { + throw new HandleException("被借调考勤组下无成员或已被全部移除该考勤组!"); + } + List dbUserIds = groupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + + List differenceUserIds = userIds.stream().filter(userId -> { + return !dbUserIds.contains(userId); + }).collect(Collectors.toList()); + + if (CollectionUtil.isNotEmpty(differenceUserIds)) { + String join = CollectionUtil.join(differenceUserIds, ","); + throw new HandleException(join + "已离开被借调考勤组!"); + } + + /* 被借调考勤组信息*/ + groupUserInfoSave(attendanceSecondedDto, GroupUserTypeEnum.CUR.getCode()); + /* 借调考勤组信息*/ + groupUserInfoSave(attendanceSecondedDto, GroupUserTypeEnum.BORROW.getCode()); + userIds.forEach(userId -> attendanceClockInService.generateRepairNumForUser(attendanceSecondedDto.getGroupId(), userId, 2)); + + + } + + /** + * 获取默认考勤组 + * + * @return AttendanceGroup + */ + @Override + public AttendanceGroupVo getDefaultGroup() { + String loginUserId = UserProvider.getLoginUserId(); + Object defaultGroup = redisTemplate.opsForValue().get(RedisConstant.CACHE_GROUP + loginUserId); + + AttendanceGroupVo group = JsonUtil.getJsonToBean(defaultGroup, AttendanceGroupVo.class); + List groupVoList = queryGroupListSetPermission(loginUserId, null); + if (CollectionUtil.isEmpty(groupVoList)) { + return null; + } + List managerGroupList = groupVoList.stream().filter(vo -> !Objects.equals(vo.getDeleteMark(), 1)).filter(AttendanceGroupVo::getIsManager).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(managerGroupList)) { + return null; + } + List managerGroupIds = managerGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + if (group != null && MybatisUtil.findByFiled(attendanceGroupMapper, AttendanceGroup::getId, group.getId(), true) != null && managerGroupIds.contains(group.getId())) { + return group; + } + + sortByName(groupVoList); + AttendanceGroupVo attendanceGroupVo = managerGroupList.get(0); + redisTemplate.opsForValue().set(RedisConstant.CACHE_GROUP + loginUserId, attendanceGroupVo); + return attendanceGroupVo; + } + + /** + * 切换考勤组 + * 498140478206886917 + */ + @Override + public void handoffGroup(String groupId) { + String loginUserId = UserProvider.getLoginUserId(); + AttendanceGroup group = MybatisUtil.findByFiled(attendanceGroupMapper, AttendanceGroup::getId, groupId, true); + redisTemplate.opsForValue().set(RedisConstant.CACHE_GROUP + loginUserId, group); + } + + @Override + public AttendanceGroupStatusDto groupSettingStatus(String groupId, Boolean aFalse) { + AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId); + if (Objects.equals(attendanceGroup.getAttendancePointsSetting(), 0)) { + AttendanceGroup attendanceGroup2 = getEnableLocationSetting(groupId); + if (Objects.nonNull(attendanceGroup2)) { + if (!aFalse) { + attendanceGroup.setGpsEnable(attendanceGroup2.getGpsEnable()); + attendanceGroup.setWifiEnable(attendanceGroup2.getWifiEnable()); + attendanceGroup.setMachineEnable(attendanceGroup2.getMachineEnable()); + } + } else { + attendanceGroup.setGpsEnable(0); + attendanceGroup.setWifiEnable(0); + attendanceGroup.setMachineEnable(0); + } + } + AttendanceGroupStatusDto result = BeanUtil.toBean(attendanceGroup, AttendanceGroupStatusDto.class); + result.setIsRootGroup(attendanceGroup.getId().equals(GROUP_TOP_PARENT_ID)); + result.setGroupId(attendanceGroup.getId()); + return result; + } + + private AttendanceGroup getEnableLocationSetting(String groupId) { + List attendanceGroupVos = queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return null; + } + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + Map collect = attendanceGroupVos.stream().filter(group -> group.getAttendancePointsSetting() == 1).collect(Collectors.toMap(AttendanceGroup::getId, group -> group)); + return findNextBaseSetting(collect, groupIdToParent, groupId); + } + + /** + * 获取考勤组下个考勤组配置 + */ + private AttendanceGroup findNextBaseSetting(Map collect, Map groupIdToParent, String groupId) { + if (StringUtil.isBlank(groupId)) { + return null; + } + if (!collect.containsKey(groupId)) { + return findNextBaseSetting(collect, groupIdToParent, groupIdToParent.get(groupId)); + } + return collect.get(groupId); + } + + private void groupUserInfoSave(AttendanceSecondedDto attendanceSecondedDto, Integer groupType) throws HandleException { + for (String userId : attendanceSecondedDto.getUserIds()) { + /* 验证用户是否有被其它组借调*/ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getUserId, userId) + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()).eq(AttendanceGroupUser::getDeleteMark, 0) + .isNotNull(AttendanceGroupUser::getTimeJson); + AttendanceGroupUser groupUser = attendanceGroupUserMapper.selectOne(queryWrapper); + if (groupUser != null) { + /* 验证日期*/ + String timeJson = groupUser.getTimeJson(); + List timeJsonList = JsonUtil.getJsonToList(timeJson, String.class); + if (CollectionUtil.isNotEmpty(timeJsonList) && GroupUserTypeEnum.CUR.getCode().equals(groupType)) { + /* 日期校验*/ + this.checkSecondDate(timeJsonList, attendanceSecondedDto.getStartTime(), attendanceSecondedDto.getEndTime(), attendanceSecondedDto.getDepartureTime(), attendanceSecondedDto.getBackTime(), userId); + } + } + + /* 先查询该组下的该用户*/ + queryWrapper.clear(); + queryWrapper + .lambda() + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()) + .eq(AttendanceGroupUser::getUserId, userId) + .eq(AttendanceGroupUser::getDeleteMark, 0); + AttendanceGroupUser dbGroupUser = attendanceGroupUserMapper.selectOne(queryWrapper); + if (GroupUserTypeEnum.CUR.getCode().equals(groupType)) { + String dbGroupUserTimeJson = dbGroupUser.getTimeJson(); + dbGroupUser.setGroupId(attendanceSecondedDto.getSelfGroupId()); + SecondmentDateVo secondmentDateVo = SecondmentDateVo.builder() + .approvalId(attendanceSecondedDto.getId()) + .startTime(attendanceSecondedDto.getDepartureTime()) + .endTime(attendanceSecondedDto.getBackTime()).build(); + + List dateVoList = JSON.parseArray(dbGroupUserTimeJson, SecondmentDateVo.class); + if (CollectionUtil.isEmpty(dateVoList)) { + dateVoList = new ArrayList<>(); + } + dateVoList.add(secondmentDateVo); + String timeJson = JsonUtil.getObjectToString(dataToTimeJson(dateVoList)); + dbGroupUser.setTimeJson(timeJson); + dbGroupUser.setGroupId(attendanceSecondedDto.getSelfGroupId()); + dbGroupUser.setRemoveTime(null); + dbGroupUser.setDeleteMark(0); + attendanceGroupUserMapper.updateById(dbGroupUser); +// return; + } else { + /* 被借调考勤组*/ + queryWrapper.clear(); + queryWrapper + .lambda() +// .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.BORROW.getCode()) + .ne(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()) + .eq(AttendanceGroupUser::getGroupId, attendanceSecondedDto.getGroupId()) + .eq(AttendanceGroupUser::getUserId, userId).eq(AttendanceGroupUser::getDeleteMark, 0); + AttendanceGroupUser borrowGroupUser = attendanceGroupUserMapper.selectOne(queryWrapper); + if (borrowGroupUser == null) { + /* 未找到考勤组成员*/ + borrowGroupUser = new AttendanceGroupUser(); + borrowGroupUser.setId(RandomUtil.uuId()); + borrowGroupUser.setGroupId(attendanceSecondedDto.getGroupId()); + borrowGroupUser.setUserId(userId); + borrowGroupUser.setType(groupType); + borrowGroupUser.setUserGroupType(groupType); + borrowGroupUser.setCreatorTime(new Date()); + String loginUserId = UserProvider.getLoginUserId(); + borrowGroupUser.setCreatorUserId(StringUtil.isEmpty(loginUserId) ? "admin" : loginUserId); + borrowGroupUser.setDeleteMark(0); + List dateVoList = new ArrayList<>(); + SecondmentDateVo secondmentDateVo = SecondmentDateVo.builder() + .approvalId(attendanceSecondedDto.getId()) + .startTime(attendanceSecondedDto.getStartTime()) + .endTime(attendanceSecondedDto.getEndTime()).build(); + dateVoList.add(secondmentDateVo); + String timeJson = JsonUtil.getObjectToString(dataToTimeJson(dateVoList)); + borrowGroupUser.setTimeJson(timeJson); + attendanceGroupUserMapper.insert(borrowGroupUser); + continue; +// return; + } + + List dateVoList = JSON.parseArray(borrowGroupUser.getTimeJson(), SecondmentDateVo.class); + if (CollectionUtil.isEmpty(dateVoList)) { + dateVoList = new ArrayList<>(); + } + SecondmentDateVo secondmentDateVo = SecondmentDateVo.builder() + .approvalId(attendanceSecondedDto.getId()) + .startTime(attendanceSecondedDto.getStartTime()) + .endTime(attendanceSecondedDto.getEndTime()).build(); + + dateVoList.add(secondmentDateVo); + String timeJson = JsonUtil.getObjectToString(dataToTimeJson(dateVoList)); + borrowGroupUser.setTimeJson(timeJson); + borrowGroupUser.setType(groupType); + borrowGroupUser.setUserGroupType(groupType); + borrowGroupUser.setRemoveTime(null); + borrowGroupUser.setDeleteMark(0); + attendanceGroupUserMapper.updateById(borrowGroupUser); + } + } + } + + /** + * 日期转json + */ + private List> dataToTimeJson(List secondmentDateVos) { + List> objects = CollUtil.newArrayList(); + String format = "yyyy-MM-dd HH:mm:ss"; + for (SecondmentDateVo secondmentDateVo : secondmentDateVos) { + Date startTime = secondmentDateVo.getStartTime(); + Date endTime = secondmentDateVo.getEndTime(); + Map map = new HashMap<>(); + map.put("approvalId", secondmentDateVo.getApprovalId()); + map.put("startTime", cn.hutool.core.date.DateUtil.format(startTime, format)); + map.put("endTime", cn.hutool.core.date.DateUtil.format(endTime, format)); + objects.add(map); + } + return objects; + } + + /** + * 查询是否有排班权限的考勤组列表 + * + * @return List + */ + @Override + public List schedulingGroupList() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermission(loginUserId, null); + sortByName(groupVoList); + /* 筛选出有排班权限的考勤组*/ + List schedulingGroupList = groupVoList.stream().filter(groupVo -> { + return groupVo.getId().equals(GROUP_TOP_PARENT_ID) || groupVo.getScheduling(); + }).collect(Collectors.toList()); + sortByName(schedulingGroupList); + return treeMyManagerGroupVo(schedulingGroupList); + } + + @Override + public List schedulingGroupListByNotFixed() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermission(loginUserId, null); + + sortByName(groupVoList); + /* 筛选出有排班权限的考勤组*/ + List schedulingGroupList = groupVoList.stream().filter(groupVo -> { + return groupVo.getId().equals(GROUP_TOP_PARENT_ID) || groupVo.getScheduling(); + }).collect(Collectors.toList()); + List groupIds = schedulingGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + Map enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, queryDropList()); + schedulingGroupList = schedulingGroupList.stream().filter(groupVo -> { + AttendanceShiftSettingVo attendanceShiftSettingVo = enableShiftSetting.get(groupVo.getId()); + if (Objects.isNull(attendanceShiftSettingVo)) { + return Boolean.FALSE; + } + return Objects.equals(attendanceShiftSettingVo.getSystemType(), 2); + }).collect(Collectors.toList()); + sortByName(schedulingGroupList); + return treeMyManagerGroupVo(schedulingGroupList); + } + + /** + * 获取有余额管理权限的考勤组列表 + * + * @return List + */ + @Override + public List balanceManagement() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermission(loginUserId, null); + sortByName(groupVoList); + /* 筛选出有余额管理权限的考勤组*/ + List balanceManagementGroupList = groupVoList.stream().filter(groupVo -> { + return groupVo.getId().equals(GROUP_TOP_PARENT_ID) || groupVo.getBalanceManagement(); + }).collect(Collectors.toList()); + sortByName(balanceManagementGroupList); + return treeMyManagerGroupVo(balanceManagementGroupList); + } + + /** + * 查询我有借调权限的考勤组列表 + */ + @Override + public List secondedApprovalGroupList() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermission(loginUserId, null); + sortByName(groupVoList); + /* 筛选出有余额管理权限的考勤组*/ + return groupVoList.stream().filter(AttendanceGroupVo::getSecondedUser).collect(Collectors.toList()); + } + + @Override + public List viewApprovalGroupList(String name) { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermission(loginUserId, null); + groupVoList = groupVoList.stream().filter(AttendanceGroupVo::getIsManager).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(groupVoList)) { + return null; + } + Pattern pattern = Pattern.compile(".*" + name + ".*", Pattern.CASE_INSENSITIVE); + groupVoList = groupVoList.stream() + .filter(x -> pattern.matcher(x.getGroupName()).matches()) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(groupVoList)) { + return null; + } + sortByName(groupVoList); + return groupVoList; + } + + + @Override + public List viewApprovalGroupListNew(String name) { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermissionNew(loginUserId, null); + groupVoList = groupVoList.stream().filter(vo -> !vo.getIsGray()).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(groupVoList)) { + return null; + } + Pattern pattern = Pattern.compile(".*" + name + ".*", Pattern.CASE_INSENSITIVE); + groupVoList = groupVoList.stream() + .filter(x -> pattern.matcher(x.getGroupName()).matches()) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(groupVoList)) { + return null; + } + sortByName(groupVoList); + return groupVoList; + } + + /** + * 获取用户以及用户所管理的考勤组 + * + * @return Map> + */ + @Override + public Map> userManagerGroupInfo() { + //查询所有考勤组当前组管理员 + QueryWrapper managerQuery = new QueryWrapper<>(); + managerQuery.lambda() + .eq(AttendanceManagerPermission::getType, 1); + List curManagerList = attendanceManagerPermissionMapper.selectList(managerQuery); + if (CollectionUtil.isEmpty(curManagerList)) { + return new HashMap<>(); + } + + QueryWrapper permissionQuery = new QueryWrapper<>(); + permissionQuery.lambda() + .eq(PermissionDict::getModuleType, 2) + .eq(PermissionDict::getName, "查看"); + + PermissionDict viewPermission = permissionDictMapper.selectOne(permissionQuery); + + //子考勤组权限 + managerQuery.clear(); + managerQuery.lambda() + .eq(AttendanceManagerPermission::getType, 2) + .eq(AttendanceManagerPermission::getPermissionId, viewPermission.getId()); + List childManagerList = attendanceManagerPermissionMapper.selectList(managerQuery); + Map> childGroupMap = childManagerList.stream().collect(Collectors.groupingBy(AttendanceManagerPermission::getUserId)); + + //查询考勤组 + QueryWrapper groupQuery = new QueryWrapper<>(); + groupQuery.lambda() + .eq(AttendanceGroup::getDeleteMark, 0); + List groupList = attendanceGroupMapper.selectList(groupQuery); + Map groupMap = groupList.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group)); + + Map> resultMap = new HashMap<>(); + Map> curManagerMap = curManagerList.stream().collect(Collectors.groupingBy(AttendanceManagerPermission::getUserId)); + curManagerMap.forEach((userId, attendanceManagerPermissionList) -> { + Set groupIdSet = attendanceManagerPermissionList.stream().map(AttendanceManagerPermission::getGroupId).collect(Collectors.toSet()); + for (AttendanceManagerPermission permission : attendanceManagerPermissionList) { + //在查看是否有子权限 + List childManagerInfo = childGroupMap.get(permission.getUserId()); + if (CollectionUtil.isEmpty(childManagerInfo)) { + resultMap.put(permission.getUserId(), ListUtil.toList(groupIdSet)); + continue; + } + List childGroupList = groupList.stream().filter(group -> { + return group.getLevelCode().contains(permission.getGroupId()); + }).collect(Collectors.toList()); + Set childGroupIds = childGroupList.stream().map(AttendanceGroup::getId).collect(Collectors.toSet()); + groupIdSet.addAll(childGroupIds); + resultMap.put(permission.getUserId(), ListUtil.toList(groupIdSet)); + } + + }); + + return resultMap; + } + + @Override + public List getGroupListByOrgIds(List orgIds) { + return this.lambdaQuery().in(AttendanceGroup::getOrgId, orgIds) + .isNotNull(AttendanceGroup::getOrgId) + .eq(AttendanceGroup::getDeleteMark, 0) + .orderByDesc(AttendanceGroup::getCreatorTime) + .list(); + } + + @Override + public boolean appointPermission(AppointPermission permissionCode) { + UserInfo userInfo = userProvider.get(); + Boolean isAdmin = attendanceSuperAdminService.isSuperAdmin(userInfo.getUserId()); + if (isAdmin || userInfo.getIsAdministrator()) { + // 系统超管、考勤组超管默认有所有权限 + return true; + } + // 查询用户在当前考勤组是否是否有指定权限 + Integer count = attendanceGroupMapper.appointPermissionByGroupId(userInfo.getUserId(), permissionCode.getGroupId(), permissionCode.getPermissionCode(), PermissionTypeEnum.CUR.getCode()); + if (count > 0) { + return true; + } + // 没有,校验当前考勤组的父级是否拥有子组管理权限 + AttendanceGroup group = attendanceGroupMapper.selectById(permissionCode.getGroupId()); + String[] parentGroupIdArr = group.getLevelCode().split("#"); + List groupIds = Arrays.stream(parentGroupIdArr).collect(Collectors.toList()); + boolean b = false; + for (String groupId : groupIds) { + Integer num = attendanceGroupMapper.appointPermissionByGroupId(userInfo.getUserId(), groupId, "manager", PermissionTypeEnum.CHILD.getCode()); + if (num > 0) { + b = true; + break; + } + } + return b; + } + + @Override + public AttendanceGroup getGroupSetting(String groupId) { + return attendanceGroupMapper.selectById(groupId); + } + + @Override + public List groupMyManagerList(GroupQueryDto groupQueryDto) { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListNew(loginUserId, groupQueryDto); + // 查询所有绑定了考勤机的考勤组 + String groupBindIds = attendanceMachineManageMapper.selectGroupBindMachine(); + Set set = new HashSet<>(); + if (null != groupBindIds && !groupBindIds.isEmpty()) { + String[] split = groupBindIds.split(","); + Collections.addAll(set, split); + } + groupVoList.forEach(v -> { + if (set.contains(v.getId())) { + v.setBindingMachine(true); + } + }); + // 按照首字母排序 + sortByName(groupVoList); + return treeMyManagerGroupVo(groupVoList); + } + + @Override + public List schedulingGroupListByNotFixedNew() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermissionNew(loginUserId, null); + + sortByName(groupVoList); + /* 筛选出有排班权限的考勤组*/ + List schedulingGroupList = groupVoList.stream().filter(groupVo -> { + return groupVo.getId().equals(GROUP_TOP_PARENT_ID) || !groupVo.getIsGray(); + }).collect(Collectors.toList()); + List groupIds = schedulingGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + Map enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, queryDropList()); + schedulingGroupList = schedulingGroupList.stream().filter(groupVo -> { + AttendanceShiftSettingVo attendanceShiftSettingVo = enableShiftSetting.get(groupVo.getId()); + if (Objects.isNull(attendanceShiftSettingVo)) { + return Boolean.FALSE; + } + return Objects.equals(attendanceShiftSettingVo.getSystemType(), 2); + }).collect(Collectors.toList()); + sortByName(schedulingGroupList); + return treeMyManagerGroupVo(schedulingGroupList); + } + + @Override + public List handoffGroupTreeListNew() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermissionNew(loginUserId, new GroupQueryDto()); + List managerGroupList = groupVoList.stream().filter(group -> { + return !group.getIsGray() || group.getId().equals(GROUP_TOP_PARENT_ID); + }).collect(Collectors.toList()); + return treeMyManagerGroupVo(managerGroupList); + } + + @Override + public AttendanceGroupVo getDefaultGroupNew() { + String loginUserId = UserProvider.getLoginUserId(); + Object defaultGroup = redisTemplate.opsForValue().get(RedisConstant.CACHE_GROUP + loginUserId); + + AttendanceGroupVo group = JsonUtil.getJsonToBean(defaultGroup, AttendanceGroupVo.class); + List groupVoList = queryGroupListSetPermissionNew(loginUserId, null); + if (CollectionUtil.isEmpty(groupVoList)) { + return null; + } + List managerGroupList = groupVoList.stream().filter(vo -> !Objects.equals(vo.getDeleteMark(), 1)).filter(vo -> !vo.getIsGray()).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(managerGroupList)) { + return null; + } + List managerGroupIds = managerGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + if (group != null && MybatisUtil.findByFiled(attendanceGroupMapper, AttendanceGroup::getId, group.getId(), true) != null && managerGroupIds.contains(group.getId())) { + return group; + } + + sortByName(groupVoList); + AttendanceGroupVo attendanceGroupVo = managerGroupList.get(0); + redisTemplate.opsForValue().set(RedisConstant.CACHE_GROUP + loginUserId, attendanceGroupVo); + return attendanceGroupVo; + } + + @Override + public List schedulingGroupListNew() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermissionNew(loginUserId, null); +// sortByName(groupVoList); + /* 筛选出有排班权限的考勤组*/ + List schedulingGroupList = groupVoList.stream().filter(groupVo -> { + return groupVo.getId().equals(GROUP_TOP_PARENT_ID) || !groupVo.getIsGray(); + }).collect(Collectors.toList()); + sortByName(schedulingGroupList); + return treeMyManagerGroupVo(schedulingGroupList); + } + + @Override + public List balanceManagementNew() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermissionNew(loginUserId, null); +// sortByName(groupVoList); + /* 筛选出有余额管理权限的考勤组*/ + List balanceManagementGroupList = groupVoList.stream().filter(groupVo -> { + return groupVo.getId().equals(GROUP_TOP_PARENT_ID) || !groupVo.getIsGray(); + }).collect(Collectors.toList()); + sortByName(balanceManagementGroupList); + return treeMyManagerGroupVo(balanceManagementGroupList); + } + + @Override + public List secondedApprovalGroupListNew() { +// String loginUserId = UserProvider.getLoginUserId(); + // 2.0 修改为不看权限 + List groupVoList = attendanceGroupMapper.getAllGroupList(new GroupQueryDto(), null, null); + groupVoList.forEach(v -> { + v.setScheduling(true); + v.setIsGray(false); + }); +// List groupVoList = queryGroupListSetPermissionNew(loginUserId, null); + sortByName(groupVoList); + /* 筛选出有余额管理权限的考勤组*/ + return groupVoList.stream().filter(groupVo -> { + return !groupVo.getIsGray(); + }).collect(Collectors.toList()); + } + + + @Override + public List getGroupIdsByManagePermission(String userId, boolean b) { +// List groupVoList = queryGroupList(userId, new GroupQueryDto()); + // v1.9 切换权限判断逻辑 + List groupVoList = queryGroupListNew(userId, new GroupQueryDto()); + /* 筛选出有排班权限的考勤组*/ + List schedulingGroupList = groupVoList.stream().filter(AttendanceGroupVo::getScheduling).collect(Collectors.toList()); + List groupIds = schedulingGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); +// if (b) { +// // 去掉固定班考勤组 todo 2.0 没有固定排班说法,后续还要改 +// Map enableShiftSetting = attendanceShiftSettingService.getEnableShiftSetting(groupIds, queryDropList()); +// schedulingGroupList = schedulingGroupList.stream().filter(groupVo -> { +// AttendanceShiftSettingVo attendanceShiftSettingVo = enableShiftSetting.get(groupVo.getId()); +// if (Objects.isNull(attendanceShiftSettingVo)) { +// return Boolean.FALSE; +// } +// return Objects.equals(attendanceShiftSettingVo.getSystemType(), 2); +// }).collect(Collectors.toList()); +// groupIds = schedulingGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); +// } + return groupIds; + } + + @Override + public boolean checkScheduling() { + List groupVoList = queryGroupList(userProvider.get().getUserId(), new GroupQueryDto()); + /* 筛选出有排班权限的考勤组*/ + List schedulingGroupList = groupVoList.stream().filter(AttendanceGroupVo::getScheduling).collect(Collectors.toList()); + return !schedulingGroupList.isEmpty(); + } + + @Override + public List getPermissionsGroupList(String positionModuleName) { + List organizeGeneralDetailVOS = ftbPermissionOrganizeService.authOrganizesByUserBound(List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.DEPARTMENT), + null, false, false); + if (CollUtil.isEmpty(organizeGeneralDetailVOS) || CollUtil.isEmpty(organizeGeneralDetailVOS.stream() + .map(OrganizeGeneralDetailVO::getId).distinct().collect(Collectors.toList()))) { + return List.of(); + } + List organizeIds = organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).distinct().collect(Collectors.toList()); + //获取组织下所有考勤组 + GroupQueryDto groupQueryDto = new GroupQueryDto(); + groupQueryDto.setType(0); + groupQueryDto.setOrgIds(organizeIds); + return queryGroupList(UserProvider.getLoginUserId(), groupQueryDto); + } + + + /** + * 获取切换考勤组列表(树结构) + * + * @return List + */ + @Override + public List handoffGroupTreeList() { + String loginUserId = UserProvider.getLoginUserId(); + List groupVoList = queryGroupListSetPermission(loginUserId, new GroupQueryDto()); + List managerGroupList = groupVoList.stream().filter(group -> { + return group.getIsManager() || group.getId().equals(GROUP_TOP_PARENT_ID); + }).collect(Collectors.toList()); + return treeMyManagerGroupVo(managerGroupList); + } + + @Override + public List groupListByOrgId(String orgId) { + List groupVos = attendanceGroupMapper.groupListByOrgId(orgId); + List groupIds = CollUtil.newArrayList(); + groupVos.forEach(groupVo -> { + if (StringUtil.isEmpty(groupVo.getLevelCode())) { + return; + } + groupIds.addAll(Arrays.stream(groupVo.getLevelCode().split("#")).collect(Collectors.toList())); + }); + if (CollUtil.isEmpty(groupIds)) { + return groupVos; + } + List list = lambdaQuery().in(AttendanceGroup::getId, groupIds).eq(AttendanceGroup::getDeleteMark, Boolean.FALSE).list(); + if (CollUtil.isEmpty(list)) { + return groupVos; + } + groupVos.forEach(groupVo -> { + if (StringUtil.isEmpty(groupVo.getLevelCode())) { + return; + } + List ids = Arrays.stream(groupVo.getLevelCode().split("#")).collect(Collectors.toList()); + String detailName = list.stream().filter(group -> ids.contains(group.getId())) + .sorted(Comparator.comparing(AttendanceGroup::getCreatorTime)) + .map(AttendanceGroup::getGroupName) + .reduce((r1, r2) -> r1 + "/" + r2) + .orElse(""); + groupVo.setDetailName(detailName); + }); + return groupVos; + } + + @Override + public AttendanceGroupVo getGroupBindingOrg(String groupId, String workGroupId) { + AttendanceGroupVo attendanceGroupVo = attendanceGroupMapper.getGroupBindingOrg(groupId); + if (null == attendanceGroupVo) { + return attendanceGroupVo; + } + // 调用Flynn接口 入参 组织Id集合 返回 List model-> id,当前组织名称,拼接好的包含父级组织的名称 + if (null != attendanceGroupVo.getOrgId()) { + List list = new ArrayList<>(); + list.add(attendanceGroupVo.getOrgId()); + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(list); + if (200 == listActionResult.getCode() && null != listActionResult.getData()) { + listActionResult.getData().stream().filter(t -> t.getId().equals(attendanceGroupVo.getOrgId())).findFirst().ifPresent(organizeGeneralDetailVO -> attendanceGroupVo.setBindingOrgName(organizeGeneralDetailVO.getOrganizeTreeName())); + } + } + attendanceGroupVo.setDetailName(recursionParent(attendanceGroupVo.getLevelCode())); + //查询人员数 + List userBoundVO = attendanceUserService.getUserBoundVO(groupId, workGroupId); + attendanceGroupVo.setUserCount(Objects.isNull(userBoundVO) ? 0 : userBoundVO.stream().filter(vo -> !StringUtil.equals(vo.getUserName(), "管理员")).count()); + return attendanceGroupVo; + } + + @Override + public AttendanceGroupVo getGroupBindingOrgForLine(String groupId, String workGroupId) { + AttendanceGroupVo attendanceGroupVo = attendanceGroupMapper.getGroupBindingOrg(groupId); + if (null == attendanceGroupVo) { + return attendanceGroupVo; + } + attendanceGroupVo.setDetailName(recursionParent(attendanceGroupVo.getLevelCode())); + attendanceGroupVo.setUserCount(0); + // 调用Flynn接口 入参 组织Id集合 返回 List model-> id,当前组织名称,拼接好的包含父级组织的名称 + if (null != attendanceGroupVo.getOrgId()) { + List list = new ArrayList<>(); + list.add(attendanceGroupVo.getOrgId()); + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(list); + if (200 == listActionResult.getCode() && null != listActionResult.getData()) { + listActionResult.getData().stream().filter(t -> t.getId().equals(attendanceGroupVo.getOrgId())).findFirst().ifPresent(organizeGeneralDetailVO -> attendanceGroupVo.setBindingOrgName(organizeGeneralDetailVO.getOrganizeTreeName())); + } + } + //获取当前考勤组成员 + //查询人员数 + List userBoundVO = attendanceUserService.getUserBoundVO(groupId, workGroupId); + if (CollUtil.isEmpty(userBoundVO)) { + return attendanceGroupVo; + } + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = lineSchedulingConfigService.getByGroupId(groupId); + if (Objects.isNull(lineSchedulingConfig)) { + return attendanceGroupVo; + } + List allUserIds = userBoundVO.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + //查询人员数 + StaffRosterListReq staffRosterListReq = new StaffRosterListReq(); + staffRosterListReq.setPageSize(-1); + staffRosterListReq.setIsQueryAuth("0"); + staffRosterListReq.setUserIds(allUserIds); + ActionResult> pageListVOActionResult = ftbPersonnelsRosterManagerApi.postWithSalary(staffRosterListReq); + PageListVO data = pageListVOActionResult.getData(); + List list = data.getList(); + if (CollUtil.isEmpty(list)) { + return attendanceGroupVo; + } + List workNatureArr = StringUtil.isEmpty(lineSchedulingConfig.getWorkNature()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getWorkNature(), ",")); + List employeeTypeArr = StringUtil.isEmpty(lineSchedulingConfig.getEmployeeType()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getEmployeeType(), ",")); + List positionArr = StringUtil.isEmpty(lineSchedulingConfig.getPosition()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getPosition(), ",")); + List memberArr = StringUtil.isEmpty(lineSchedulingConfig.getMembers()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getMembers(), ",")); + if (CollUtil.isEmpty(workNatureArr) && CollUtil.isEmpty(employeeTypeArr) && CollUtil.isEmpty(positionArr) && !Objects.equals(lineSchedulingConfig.getMembersType(), 0) && CollUtil.isEmpty(memberArr)) { + return attendanceGroupVo; + } + boolean hasOnboarding = workNatureArr.contains("301"); + List userIdsForConfigFilter = list.stream() + .filter(dto -> (CollUtil.isNotEmpty(workNatureArr) && workNatureArr.contains(dto.getWorkerStatus())) + && ((hasOnboarding && StringUtil.isEmpty(dto.getWorkerType())) || employeeTypeArr.contains("-1") || CollUtil.isNotEmpty(employeeTypeArr) && employeeTypeArr.contains(dto.getWorkerType())) + || (CollUtil.isNotEmpty(positionArr) && positionArr.contains(dto.getCurrPosition())) + || Objects.equals(lineSchedulingConfig.getMembersType(), 0) + || (CollUtil.isNotEmpty(memberArr) && memberArr.contains(dto.getUserId()))).map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + attendanceGroupVo.setUserCount(userIdsForConfigFilter.size()); + return attendanceGroupVo; + } + + @Override + public void updateGroupOrg(AttendanceGroupOrgDto attendanceGroupOrgDto) { + attendanceGroupMapper.updateGroupOrg(attendanceGroupOrgDto); + } + + @Override + public List getAttendanceUserGroup(List userIds) { + return attendanceGroupMapper.getAttendanceUserGroup(userIds); + } + + @Override + public void lockGroup(String groupId) { + // 查询考勤组锁定状态 + AttendanceGroup group = attendanceGroupMapper.selectById(groupId); + if (null != group) { + Date date; + AttendanceNoticeEnum noticeEnum; + if (null == group.getLockedDate()) { + // 锁定 + date = new Date(); + String dateStr = DateDetail.getDate2Str(date, DateDetail.DF); + date = DateDetail.getStr2Date(dateStr); + noticeEnum = AttendanceNoticeEnum.GROUP_LOCK; + } else { + date = null; + noticeEnum = AttendanceNoticeEnum.GROUP_UNLOCK; + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceGroup::getLockedDate, date) + .eq(AttendanceGroup::getId, groupId); + attendanceGroupMapper.update(null, updateWrapper); + //发送解锁锁定通知 + UserInfo user = UserProvider.getUser(); + AttendanceNoticeModel attendanceNoticeModel = new AttendanceNoticeModel(); + attendanceNoticeModel.setTenantId(user.getTenantId()); + attendanceNoticeModel.setUserId(user.getUserId()); + attendanceNoticeModel.setAttendanceNoticeEnum(noticeEnum); + attendanceNoticeModel.setGroupId(groupId); + attendanceNoticeHandler.send(attendanceNoticeModel); + } + } + + @Override + public GroupLockVo getUserCurrentGroupLockInfo(String userId) throws QueryException { + + // 查询用户当前所在的考勤组 + UserInfo userInfo = userProvider.get(); + UserInfo user = new UserInfo(); + user.setUserId(userId); + user.setTenantId(userInfo.getTenantId()); + GroupInfoVo groupInfo = attendanceClockInService.getGroupInfoByDate(new Date(), user); + return new GroupLockVo(groupInfo.getGroupId(), groupInfo.getLockedDate()); + } + + @Override + public GroupLockVo getGroupLockInfo(String groupId) { + + Date groupLockDate = attendanceGroupMapper.getGroupLockDate(groupId); + return new GroupLockVo(groupId, groupLockDate); + } + + /** + * 时间校验 + * + * @param start 借调开始时间 + * @param end 借调结束时间 + * @param departureTime 离岗时间 + * @param backTime 回岗时间 + * @param userId 用户id + * "startTime": "2023-12-10 10:00:00", + * "endTime": "2023-12-12 10:00:00", + * "departureTime": "2023-12-10 10:00:00", + * "backTime": "2023-12-12 10:00:00" + */ + private void checkSecondDate(List timeJsons, Date start, Date end, Date departureTime, Date backTime, String userId) throws HandleException { + for (String timeJson : timeJsons) { + + SecondmentDateVo secondmentDateVo = JsonUtil.getJsonToBean(timeJson, SecondmentDateVo.class); + Date startTime = secondmentDateVo.getStartTime(); + Date endTime = secondmentDateVo.getEndTime(); + /* 时间比较 (借调开始时间>用户已被借调开始时间 && 借调结束时间<=用户已被借调结束时间) || 离岗时间-用户离岗时间*/ + Boolean startCompareStart = startTime != null && ((DateUtil.dateCompare(start, startTime) >= 0 && DateUtil.dateCompare(end, endTime) <= 0) || (DateUtil.dateCompare(departureTime, startTime) >= 0 && DateUtil.dateCompare(backTime, endTime) <= 0)); + /* 时间比较 用户借调开始时间<=用户已被借调结束时间 || 用户回岗时间<用户已被借调结束时间*/ + Boolean endCompareStart = endTime != null && ((DateUtil.dateCompare(start, endTime) <= 0 && DateUtil.dateCompare(end, endTime) >= 0) || (DateUtil.dateCompare(backTime, endTime) <= 0 && DateUtil.dateCompare(backTime, endTime) >= 0)); + if (startCompareStart || endCompareStart) { +// UserEntity userEntity = userApi.getInfoById(userId); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), null); + if (200 != userList.getCode() || CollectionUtil.isEmpty(userList.getData())) { + return; + } + throw new HandleException(userList.getData().get(0).getUserName() + "已被借调"); + } + } + } + + /** + * 按照名称首字母排序 + */ + private void sortByName(List attendanceGroupVoList) { + CollectionUtil.sort(attendanceGroupVoList, new Comparator() { + @Override + public int compare(AttendanceGroupVo o1, AttendanceGroupVo o2) { + if (o1 == null && o2 == null) { + return -1; + } + String name1 = null; + if (o1 != null) { + name1 = PinYinUtil.getPingYin(o1.getGroupName()); + } + String name2 = PinYinUtil.getPingYin(o2.getGroupName()); + Character a = Objects.requireNonNull(name1).charAt(0); + Character b = name2.charAt(0); +// return o1.getGroupName().compareTo(o2.getGroupName()); + return a.compareTo(b); + } + }); + } + + + /** + * 考勤组管理列表组成递归树结构 + * + * @return List + */ + private List treeGroupVo(List groupVoList) { + List topGroupList = groupVoList.stream().filter(group -> { + return group.getParentId().equals("0"); + }).collect(Collectors.toList()); + + return topGroupList.stream().peek(group -> group.setChild(recursiveQuery(group, groupVoList))).collect(Collectors.toList()); + } + + /** + * 获取我管理的考勤组管理列表组成递归树结构 + * + * @return List + */ + private List treeMyManagerGroupVo(List allGroupList) { + if (CollectionUtil.isEmpty(allGroupList)) { + return allGroupList; + } + /* 排序 筛选顶层节点*/ + allGroupList.sort(new Comparator() { + @Override + public int compare(AttendanceGroupVo o1, AttendanceGroupVo o2) { + Integer length1 = o1.getLevelCode().split("#").length; + Integer length2 = o2.getLevelCode().split("#").length; + return length1.compareTo(length2); + } + }); + + /* 筛选顶层节点*/ + List topGroupVoList = allGroupList.stream().filter(group -> { + String levelCode = group.getLevelCode(); + String[] split = levelCode.split("#"); + boolean flag = false; + if (group.getParentId().equals("0")) { + /* 如果本来就是根节点,则直接返回*/ + return true; + } + String parentLevelCode = levelCode.replace(split[0], ""); + List groupIds = allGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + + for (String groupId : groupIds) { + flag = !parentLevelCode.contains(groupId); + if (!flag) { + break; + } + } + return flag; + }).collect(Collectors.toList()); + List resultGroupTree = topGroupVoList.stream().peek(group -> group.setChild(recursiveQueryManagerGroup(group, allGroupList))).collect(Collectors.toList()); + GROUP_ID_LOCAL.remove(); +// groupIds.clear(); + return resultGroupTree; + } + + + /** + * 迭代查询 + * + * @return List + */ + private List recursiveQuery(AttendanceGroupVo root, List groupVoList) { + if (CollectionUtil.isEmpty(groupVoList)) { + root.setChild(null); + } + List children = new ArrayList<>(); + groupVoList.forEach(item -> { + if (Objects.equals(item.getParentId(), root.getId())) { + item.setChild(recursiveQuery(item, groupVoList)); + children.add(item); + } + }); + return children; + } + + private List recursiveQueryManagerGroup(AttendanceGroupVo root, List attendanceGroupVoList) { + if (CollectionUtil.isEmpty(attendanceGroupVoList)) { + root.setChild(null); + } + List children = new ArrayList<>(); + attendanceGroupVoList.forEach(item -> { + String levelCode = item.getLevelCode(); + String parentLevelCode = root.getLevelCode(); + if (levelCode.contains(parentLevelCode) + && levelCode.length() > parentLevelCode.length() + && !GROUP_ID_LOCAL.get().contains(item.getId())) +// && !groupIds.contains(item.getId())) + { + + item.setChild(recursiveQueryManagerGroup(item, attendanceGroupVoList)); + children.add(item); + GROUP_ID_LOCAL.get().add(item.getId()); +// groupIds.add(item.getId()); + } + }); + return children; + } + + + /** + * 递归获取考勤组父级id + */ + private void recursionParent(String parentId, List group) { + AttendanceGroup parent = attendanceGroupMapper.selectById(parentId); + if (parent == null) { + return; + } + + group.add(parent); + recursionParent(parent.getParentId(), group); + } + + /** + * 考勤组及考勤组人员老数据处理 + */ + @Override + public void oldDataProcessing(String tenantId) { + log.error("更新租户[{}],考勤组及考勤组人员老数据处理开始", tenantId); + //根据组织查询考勤组信息,如果未查询到组织对应考勤组,则根据组织名称创建考勤组 + //查询组织信息列表 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + List orgIds = CollUtil.newArrayList(); + List list2 = lambdaQuery().orderByAsc(AttendanceGroup::getCreatorTime).list(); + Map oldGroup2orgMap = list2.stream().filter(vo -> StringUtil.isNotEmpty(vo.getOrgId()) && (Objects.equals(vo.getDeleteMark(), 0) || StringUtil.equals(vo.getDeleteUserId(), "admin"))).collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getOrgId, (r1, r2) -> r1)); + Map org2GroupMap = batchSaveGroupByOrgIds(tenantId, orgIds); + log.error("更新租户[{}],更新考勤组信息完成", tenantId); + List list = lambdaQuery().eq(AttendanceGroup::getDeleteMark, Boolean.FALSE).orderByAsc(AttendanceGroup::getCreatorTime).list(); + Map group2orgMap = list.stream().filter(vo -> StringUtil.isNotEmpty(vo.getOrgId())).collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getOrgId, (r1, r2) -> r1)); + //原有考勤组 查询考勤组人员信息,如果人员信息不一致,多退少补 + List attendanceGroupUsers = attendanceUserService.queryAll(); + ActionResult> listActionResult1 = v2UserApi.listTargetOrganizesOrHaveChild(orgIds, false, List.of(UserWorkStatusEnums.RESIGNED), tenantId); + List userList = listActionResult1.getData(); + Map collect1 = userList.stream().filter(vo->Objects.nonNull(vo.getEntryDate())).collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getEntryDate, (r1, r2) -> r1)); + //过滤存在于原有考勤组的组织的人员信息,并根据组织分组 + Map> collect3 = userList.stream().collect(Collectors.groupingBy(UserBoundVO::getOrganizeId)); + collect3.forEach((orgId, users) -> { + String groupId = org2GroupMap.get(orgId); + //获取在其他非借调考勤组的本组织成员,移动到当前考勤组下 + List groupUsers = attendanceGroupUsers.stream().filter(vo -> Objects.equals(vo.getType(), 1) && users.stream().anyMatch(user -> user.getId().contains(vo.getUserId()))).collect(Collectors.toList()); + groupUsers.forEach(vo -> { + vo.setGroupId(groupId); + }); + //获取非组织绑定考勤组下成员 + List userIds = users.stream().map(UserBoundVO::getId).filter(userId -> groupUsers.stream().noneMatch(groupUser -> groupUser.getUserId().equals(userId))).collect(Collectors.toList()); + //获取在其他借调考勤组的本组织成员,移动到目标组织考勤组 + attendanceGroupUsers.stream().filter(vo -> Objects.equals(vo.getType(), 2) && userIds.contains(vo.getUserId())).forEach(vo -> { + vo.setGroupId(org2GroupMap.get(group2orgMap.getOrDefault(vo.getGroupId(), ""))); + }); + //不在考勤组的本组织成员,则创建考勤组成员信息 + List collect4 = userIds.stream() + .filter(userId -> groupUsers.stream().noneMatch(groupUser -> groupUser.getUserId().equals(userId))) + .map(userId -> { + AttendanceGroupUser attendanceGroupUser = new AttendanceGroupUser(); + attendanceGroupUser.setId(RandomUtil.uuId()); + attendanceGroupUser.setGroupId(groupId); + attendanceGroupUser.setUserId(userId); + attendanceGroupUser.setType(1); + attendanceGroupUser.setCreatorTime(collect1.getOrDefault(userId, new Date())); + attendanceGroupUser.setCreatorUserId("admin"); + attendanceGroupUser.setTenantId(tenantId); + attendanceGroupUser.setDeleteMark(0); + attendanceGroupUser.setSort(10000); + attendanceGroupUser.setUserGroupType(1); + return attendanceGroupUser; + }).collect(Collectors.toList()); + attendanceGroupUsers.addAll(collect4); + }); + attendanceUserService.saveOrUpdateBatch(attendanceGroupUsers); + List collect = collect3.entrySet().stream().flatMap(entry -> { + String groupId = org2GroupMap.get(entry.getKey()); + if (StringUtil.isEmpty(groupId)) { + return Stream.empty(); + } + //清除非本考勤组成员 + return attendanceGroupUsers.stream().filter(vo -> StringUtil.equals(groupId, vo.getGroupId()) && Objects.equals(vo.getType(), 1) && entry.getValue().stream().noneMatch(user -> user.getId().contains(vo.getUserId()))); + }).collect(Collectors.toList()); + attendanceUserService.removeBatchByIds(collect); + //调整groupUser离职数据 + List list1 = attendanceUserService.list(new LambdaQueryWrapper().eq(AttendanceGroupUser::getDeleteMark, 1)); + list1.forEach(vo -> vo.setGroupId(org2GroupMap.get(oldGroup2orgMap.getOrDefault(vo.getGroupId(), "")))); + attendanceUserService.updateBatchById(list1); + attendanceGroupUsers.removeAll(collect); + Map> usersMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + log.error("更新租户[{}],更新考勤组人员信息完成", tenantId); + attendanceDailyRuleService.hisDailyRule(oldGroup2orgMap, org2GroupMap, usersMap); + log.error("更新租户[{}],更新考勤规则信息完成", tenantId); + attendanceBaseSettingService.hisBaseSetting(oldGroup2orgMap, org2GroupMap); + log.error("更新租户[{}],更新考勤组基础信息完成", tenantId); + attendanceLocationSettingService.hisLocationSetting(oldGroup2orgMap, org2GroupMap); + log.error("更新租户[{}],更新考勤组考勤地点信息完成", tenantId); + attendanceShiftSettingService.hisShiftSetting(oldGroup2orgMap, org2GroupMap); + log.error("更新租户[{}],更新考勤组班次规则信息完成", tenantId); + // 组织重构考勤数据变更 +// attendanceDayStatisticsService.regenerateDayData(tenantId); + } + + @Override + @Transactional + public Map batchSaveGroupByOrgIds(String tenantId, List orgIds) { + QueryOrganizeListTargetTypesDTO queryOrganizeListTargetTypesDTO = new QueryOrganizeListTargetTypesDTO(List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.STORE)); + queryOrganizeListTargetTypesDTO.setOrganizeIds(orgIds); + queryOrganizeListTargetTypesDTO.setTenantId(tenantId); + ActionResult> listActionResult = v2OrganizeApi.listOrganizeByTargetTypes(queryOrganizeListTargetTypesDTO); + List data = listActionResult.getData(); + Map org2Parent = data.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, OrganizeGeneralDetailVO::getParentId, (r1, r2) -> r1)); + orgIds.clear(); + orgIds.addAll(data.stream().flatMap(vo -> Stream.of(vo.getId(), vo.getParentId())).distinct().collect(Collectors.toList())); + List list = lambdaQuery().eq(AttendanceGroup::getDeleteMark, Boolean.FALSE).in(CollUtil.isNotEmpty(orgIds), AttendanceGroup::getOrgId, orgIds).orderByAsc(AttendanceGroup::getCreatorTime).list(); + Map org2group = list.stream().collect(Collectors.toMap(AttendanceGroup::getOrgId, AttendanceGroup::getId, (r1, r2) -> r1)); + //查询每个组织下人员 + Map orgNameMap = data.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, OrganizeGeneralDetailVO::getName)); + List collect1 = data.stream() + .filter(vo -> !org2group.containsKey(vo.getId())) + .sorted(Comparator.comparing(OrganizeGeneralDetailVO::getTires)) + .map(vo -> { + AttendanceGroup attendanceGroup = new AttendanceGroup(); + attendanceGroup.setId(RandomUtil.uuId()); + attendanceGroup.setAttendanceClassSetting(0); + attendanceGroup.setAttendancePointsSetting(0); + attendanceGroup.setBaseSetting(0); + attendanceGroup.setDeleteMark(0); + attendanceGroup.setDetailName(vo.getName()); + attendanceGroup.setFestivalSetting(0); + attendanceGroup.setGpsEnable(0); + attendanceGroup.setHolidaySetting(0); + attendanceGroup.setLeaveSetting(0); + attendanceGroup.setGroupName(vo.getName()); + attendanceGroup.setOrgId(vo.getId()); + attendanceGroup.setParentId(org2group.getOrDefault(vo.getParentId(), "000000")); + attendanceGroup.setWifiEnable(0); + List organizeIdTree = Arrays.stream(vo.getOrganizeIdTree().split(",")).collect(Collectors.toList()); + List levelCodes = organizeIdTree.stream().map(parentId -> org2group.getOrDefault(parentId, "000000")).sorted((Comparator.reverseOrder())).collect(Collectors.toList()); + List detailName = organizeIdTree.stream().map(parentId -> orgNameMap.getOrDefault(parentId, "")).sorted((Comparator.reverseOrder())).collect(Collectors.toList()); + attendanceGroup.setLevelCode(StringUtil.join(levelCodes, "#")); + attendanceGroup.setDetailName(StringUtil.join(detailName, "/")); + attendanceGroup.setTenantId(tenantId); + attendanceGroup.setCreatorTime(new Date()); + attendanceGroup.setCreatorUserId("admin"); + org2group.put(vo.getId(), attendanceGroup.getId()); + return attendanceGroup; + }).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect1)) { + saveBatch(collect1); + redisTemplate.delete(String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY, tenantId)); + } + if (CollUtil.isNotEmpty(list)) { + list.forEach(group -> { + String groupId = org2group.get(group.getOrgId()); + if (StringUtil.equals(groupId, group.getId())) { + group.setGroupName(orgNameMap.getOrDefault(group.getOrgId(), group.getGroupName())); + group.setParentId(org2group.getOrDefault(org2Parent.getOrDefault(group.getOrgId(), "000000"), group.getParentId())); + return; + } + group.setDeleteMark(1); + group.setDeleteUserId("admin"); + } + ); + updateBatchById(list); + redisTemplate.delete(String.format(ATTENDANCE_BASE_SETTING_CACHE_KEY, tenantId)); + } + return org2group; + } + + /** + * 递归获取考勤组父级id + */ + private String recursionParent(String levelCode) { + if (StringUtil.isEmpty(levelCode)) { + return ""; + } + List groupIds = Arrays.stream(levelCode.split("#")).collect(Collectors.toList()); + List list = lambdaQuery().in(AttendanceGroup::getId, groupIds).eq(AttendanceGroup::getDeleteMark, Boolean.FALSE).list(); + if (CollUtil.isEmpty(list)) { + return ""; + } + return list.stream().sorted(Comparator.comparing(AttendanceGroup::getCreatorTime)).map(AttendanceGroup::getGroupName).reduce((r1, r2) -> r1 + "/" + r2).orElse(""); + } + + /** + * 获取组织下考勤组 + * + * @return List + */ + @Override + public List getByOrgId(String organizeId) { + QueryWrapper groupQuery = new QueryWrapper<>(); + groupQuery.lambda() + .eq(AttendanceGroup::getOrgId, organizeId) + .eq(AttendanceGroup::getDeleteMark, 0); + return attendanceGroupMapper.selectList(groupQuery); + } + + /** + * 获取组织下考勤组 + * + * @param organizeIds + * @return List + */ + @Override + public List getByOrgIds(List organizeIds) { + QueryWrapper groupQuery = new QueryWrapper<>(); + groupQuery.lambda() + .in(AttendanceGroup::getOrgId, organizeIds) + .eq(AttendanceGroup::getDeleteMark, 0); + List groups = attendanceGroupMapper.selectList(groupQuery); + return groups; + } + + @Override + public List getAttendanceUserListGroupVO(List userIds) { + List allAttendanceGroup = this.list(); + List userListGroupVOS = attendanceGroupMapper.getAttendanceUserListGroupVO(userIds); + if (CollUtil.isEmpty(allAttendanceGroup) || CollUtil.isEmpty(userListGroupVOS)) { + return new ArrayList<>(); + } + userListGroupVOS.forEach(userGroup -> { + StringBuffer fullName = new StringBuffer(); + getFullGroupName(allAttendanceGroup, userGroup.getGroupId(), userGroup.getGroupId(), fullName); + userGroup.setGroupFullName(fullName.toString()); + }); + + return userListGroupVOS; + } + + @Override + public AttendanceGroupVo getAttendanceGroupByNameOrganizeAndGroup(String organizeName, String groupName) { +// OrganizeEntity organize = organizeApi.getByFullName(organizeName); + OrganizeGeneralDetailVO organizeGeneralDetailVO = v2OrganizeApi.organizeInfoByName(OrganizeCategoryEnums.DEPARTMENT, organizeName); + if (organizeGeneralDetailVO == null) { + return null; + } + List attendanceGroupVos = this.groupListByOrgId(organizeGeneralDetailVO.getId()); + if (CollUtil.isEmpty(attendanceGroupVos)) { + return null; + } + return attendanceGroupVos.stream().filter(data -> data.getGroupName().equals(groupName)).findFirst().orElse(null); + } + + /** + * 获取考勤组全名称 xxx/xxx/xxx + * + * @param allAttendanceGroup 所有考勤组, 方便迭代,提高检索效率,避免多次sql + * @param groupId 目标考勤组id + * @param thisGroup 当前位置考勤组id + * @param fullName 考勤组全名 + */ + private void getFullGroupName(List allAttendanceGroup, String groupId, String thisGroup, StringBuffer fullName) { + AttendanceGroup thisAttendanceGroup = allAttendanceGroup.stream().filter(group -> group.getId().equals(thisGroup)).findFirst().orElse(null); + if (thisAttendanceGroup != null) { + //是否是顶级考勤组 + if (thisAttendanceGroup.getParentId().equals("0")) { + fullName.insert(0, thisAttendanceGroup.getGroupName().concat("/")); + } else { + if (thisAttendanceGroup.getId().equals(groupId)) { + fullName.append(thisAttendanceGroup.getGroupName()); + getFullGroupName(allAttendanceGroup, groupId, thisAttendanceGroup.getParentId(), fullName); + } else { + fullName.insert(0, thisAttendanceGroup.getGroupName().concat("/")); + getFullGroupName(allAttendanceGroup, groupId, thisAttendanceGroup.getParentId(), fullName); + } + } + } + + } + + @Override + public List getUserCurrentGroups(String userId) { + // 获取当前时间 + Date now = new Date(); + + // 查询当前时间用户所在的考勤组关联信息(包含借调) + List groupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment( + now, now, Collections.singletonList(userId), null); + + if (CollUtil.isEmpty(groupUsers)) { + return Collections.emptyList(); + } + + // 提取考勤组ID列表 + List groupIds = groupUsers.stream() + .map(AttendanceGroupUser::getGroupId) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(groupIds)) { + return Collections.emptyList(); + } + + // 查询考勤组详细信息 + return this.listByIds(groupIds); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceHolidaySettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceHolidaySettingServiceImpl.java new file mode 100644 index 0000000..dbf2615 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceHolidaySettingServiceImpl.java @@ -0,0 +1,246 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import com.google.common.collect.Maps; +import jnpf.attendance.mapper.AttendanceHolidaySettingMapper; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceHolidaySettingService; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceHolidaySettingEntity; +import jnpf.model.attendance.dto.AttendanceHolidaySettingDto; +import jnpf.model.attendance.vo.AttendanceHolidaySettingVo; +import jnpf.util.RandomUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + *

+ * 考勤配置-假日设置 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@Service +@Slf4j +public class AttendanceHolidaySettingServiceImpl extends SuperServiceImpl implements AttendanceHolidaySettingService { + + @Autowired + private AttendanceGroupService attendanceGroupService; + + @Override + public PageDTO page(String groupId, String name, Integer paidSalaryEnable, Integer page, Integer size) { + List attendanceGroups = attendanceGroupService.queryDropList(); + Map collect = attendanceGroups.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group)); + Map groupIdToParent = attendanceGroups.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + PageDTO attendanceFestivalSettingVoPageDTO = upPage(groupId, groupIdToParent, collect, name, paidSalaryEnable, page, size); + if (Objects.isNull(attendanceFestivalSettingVoPageDTO)) { + return new PageDTO<>(page, size); + } + return attendanceFestivalSettingVoPageDTO; + } + + private PageDTO upPage(String groupId, Map groupIdToParent, Map collect, String name, Integer paidSalaryEnable, Integer page, Integer size) { + if (StringUtil.isEmpty(groupId)) { + return null; + } + AttendanceGroup attendanceGroup = collect.get(groupId); + if (Objects.isNull(attendanceGroup)) { + return null; + } + if (Objects.equals(attendanceGroup.getHolidaySetting(), 1)) { + PageDTO page1 = lambdaQuery().eq(AttendanceHolidaySettingEntity::getGroupId, groupId) + .like(StringUtil.isNotEmpty(name), AttendanceHolidaySettingEntity::getName, name) + .eq(AttendanceHolidaySettingEntity::getDeleteMark, Boolean.FALSE) + .eq(Objects.nonNull(paidSalaryEnable), AttendanceHolidaySettingEntity::getPaidSalaryEnable, paidSalaryEnable) + .orderByAsc(AttendanceHolidaySettingEntity::getCreatorTime) + .page(new PageDTO(page, size)); + return (PageDTO) page1.convert(x -> BeanUtil.toBean(x, AttendanceHolidaySettingVo.class)); + } + return upPage(groupIdToParent.get(groupId), groupIdToParent, collect, name, paidSalaryEnable, page, size); + } + + @Override + public AttendanceHolidaySettingVo getOne(String id) { + return BeanUtil.toBean(getById(id), AttendanceHolidaySettingVo.class); + } + + @Override + public void del(String id) { + lambdaUpdate().set(AttendanceHolidaySettingEntity::getDeleteMark, Boolean.TRUE) + .set(AttendanceHolidaySettingEntity::getDeleteUserId, UserProvider.getLoginUserId()) + .set(AttendanceHolidaySettingEntity::getDeleteTime, new Date()) + .eq(AttendanceHolidaySettingEntity::getId, id) + .update(); + } + + @Override + public void initHolidaySetting(String groupId) { + List list = lambdaQuery().eq(AttendanceHolidaySettingEntity::getGroupId, groupId) + .eq(AttendanceHolidaySettingEntity::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isNotEmpty(list)) { + return; + } + List attendanceGroups = attendanceGroupService.queryDropList(); + Map> enableBaseSetting = getAllFestivalSetting(CollUtil.newArrayList(groupId), attendanceGroups); + List attendanceBaseSettings = enableBaseSetting.get(groupId); + if (CollUtil.isNotEmpty(attendanceBaseSettings)) { + attendanceBaseSettings.forEach(attendanceBaseSetting -> { + attendanceBaseSetting.setId(RandomUtil.uuId()); + attendanceBaseSetting.setGroupId(groupId); + attendanceBaseSetting.setCreatorTime(new Date()); + attendanceBaseSetting.setCreatorUserId(UserProvider.getLoginUserId()); + }); + saveBatch(attendanceBaseSettings); + } + } + +// @Override +// @Deprecated(since = "在2.0中已过期", forRemoval = true) +// public void autoGrantBalance(String tenantId) { +// log.error("开始假日自动发放劵"); +// List attendanceGroups = attendanceGroupService.queryDropList(); +// List groupIds = attendanceGroups.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); +// Map> enableFestivalSetting = getEnableFestivalSetting(groupIds, attendanceGroups); +// List attendanceGroupUsers = attendanceUserService.queryByUsersGroupIds(groupIds); +// if (attendanceGroupUsers.isEmpty()){ +// return; +// } +// Map> collect = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); +// List userIds = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); +// //查询工龄及司龄 +// List queryCompanyAgeDtos = ftbPersonnelsStaffRosterService.queryCompanyAge(userIds); +// Map companyDtoMap = queryCompanyAgeDtos.stream().collect(Collectors.toMap(QueryCompanyAgeDto::getUserId, vo -> vo)); +// enableFestivalSetting.forEach((groupId, list) -> { +// List attendanceGroupUsers1 = collect.get(groupId); +// if (CollUtil.isEmpty(attendanceGroupUsers1)) { +// return; +// } +// if (CollUtil.isEmpty(list)) { +// return; +// } +// list.forEach(setting -> { +// if (!Objects.equals(setting.getGiveType(), 1)) { +// return; +// } +// attendanceGroupUsers1.forEach(user -> { +// QueryCompanyAgeDto companyAge = companyDtoMap.get(user.getUserId()); +// Date date = DateUtil.getDayBegin(); +// Date dateByMonthAndDay; +// if (setting.getGiveDateType() == 1) { +// dateByMonthAndDay = DateDetail.getDateByMonthAndDay(date, date.getMonth() + 1, setting.getGiveDateDay()); +// } else if (setting.getGiveDateType() == 2) { +// dateByMonthAndDay = DateDetail.getDateByMonthAndDay(date, setting.getGiveDateMonth(), setting.getGiveDateDay()); +// } else if (setting.getGiveDateType() == 3) { +// //生日日期 +// if (StringUtil.isEmpty(companyAge.getBirthday())) { +// return; +// } +// Date birthday = DateUtil.stringToDates(companyAge.getBirthday()); +// dateByMonthAndDay = DateDetail.getDateByMonthAndDay(date, birthday.getMonth() + 1, cn.hutool.core.date.DateUtil.dayOfMonth(birthday)); +// } else { +// //入职日期 +// if (StringUtil.isEmpty(companyAge.getActualStartDate())) { +// return; +// } +// Date entryDate = DateUtil.stringToDates(companyAge.getActualStartDate()); +// dateByMonthAndDay = DateDetail.getDateByMonthAndDay(date, entryDate.getMonth() + 1, cn.hutool.core.date.DateUtil.dayOfMonth(entryDate)); +// } +// if (date.compareTo(dateByMonthAndDay) != 0) { +// return; +// } +// if (setting.getExpiresType() == 1) { +// Integer expiresDay = setting.getExpiresDayNumber(); +// date = DateUtil.dateAddDays(date, expiresDay); +// } else if (Objects.equals(setting.getExpiresType(), 2)) { +// date = DateDetail.getDateByMonthAndDay(date, setting.getExpiresMonth(), setting.getExpiresDay()); +// } else { +// date = null; +// } +// BigDecimal dayNum = new BigDecimal(setting.getDayNumber()); +// if (Objects.equals(setting.getDayType(), 2)) { +// Long entryYear = Objects.isNull(companyAge) || companyAge.getConpanyAge() == 0 ? 0 : companyAge.getConpanyAge()/ 365; +// Long workYear = Objects.isNull(companyAge) ? 0 : companyAge.getWorkerAge(); +// dayNum = new BigDecimal(setting.getDayNumber() + workYear * setting.getYoeMultiple() + entryYear * setting.getYosMultiple()); +// } +// log.error("考勤组[{}]人员[{}]假日[{}]自动发放劵[{}]", user.getGroupId(), user.getUserId(), setting.getName(), dayNum); +// attendanceBalanceRecordMapper.grantBalance(RandomUtil.uuId(), user.getUserId(), setting.getName(), dayNum, date, 2, 0, null, setting.getPaidSalaryEnable(), 2, setting.getId()); +// }); +// }); +// }); +// log.error("假日自动发放劵结束"); +// } + @Override + public Map> getEnableFestivalSetting(List groupIds, List attendanceGroupVos) { + return getFestivalSetting(groupIds,attendanceGroupVos,Boolean.TRUE); + } + + private Map> getAllFestivalSetting(List groupIds, List attendanceGroupVos) { + return getFestivalSetting(groupIds,attendanceGroupVos,Boolean.FALSE); + } + + private Map> getFestivalSetting(List groupIds, List attendanceGroupVos, Boolean isFilterDisable) { + List list = lambdaQuery().eq(isFilterDisable,AttendanceHolidaySettingEntity::getEnable, Boolean.TRUE) + .eq(AttendanceHolidaySettingEntity::getDeleteMark, Boolean.FALSE) + .list(); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getHolidaySetting() == 1)); + Map> collect = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.groupingBy(AttendanceHolidaySettingEntity::getGroupId)); + Map> map = Maps.newHashMap(); + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + groupIds.forEach(groupId -> map.put(groupId, findNextBaseSetting(collect, groupIdToParent, groupId))); + return map; + } + + private List findNextBaseSetting(Map> collect, Map groupIdToParent, String groupId) { + if (StringUtil.isBlank(groupId)) { + return CollUtil.newArrayList(); + } + if (!collect.containsKey(groupId)) { + return findNextBaseSetting(collect, groupIdToParent, groupIdToParent.get(groupId)); + } + return collect.get(groupId); + } + + @Override + public void save(AttendanceHolidaySettingDto dto) { + AttendanceHolidaySettingEntity attendanceHolidaySettingEntity = BeanUtil.toBean(dto, AttendanceHolidaySettingEntity.class); + UserInfo user = UserProvider.getUser(); + if (StringUtil.isEmpty(attendanceHolidaySettingEntity.getId())) { + List list = lambdaQuery() + .eq(AttendanceHolidaySettingEntity::getName, dto.getName()) + .eq(AttendanceHolidaySettingEntity::getGroupId,dto.getGroupId()) + .eq(AttendanceHolidaySettingEntity::getDeleteMark, Boolean.FALSE) + .list(); + Assert.isFalse(CollUtil.isNotEmpty(list),"假日名称不能重复"); + attendanceHolidaySettingEntity.setCreatorUserId(user.getUserId()); + attendanceHolidaySettingEntity.setCreatorTime(new Date()); + attendanceHolidaySettingEntity.setTenantId(user.getTenantId()); + } else { + attendanceHolidaySettingEntity.setLastModifyTime(new Date()); + attendanceHolidaySettingEntity.setLastModifyUserId(user.getUserId()); + } + saveOrUpdate(attendanceHolidaySettingEntity); + } + + @Override + public void changeStatus(String id, Integer enable) { + lambdaUpdate().eq(AttendanceHolidaySettingEntity::getId, id) + .set(AttendanceHolidaySettingEntity::getEnable, enable) + .update(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveRulesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveRulesServiceImpl.java new file mode 100644 index 0000000..7e24adb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveRulesServiceImpl.java @@ -0,0 +1,687 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.mapper.AttendanceLeaveGrantSettingMapper; +import jnpf.attendance.mapper.AttendanceLeaveRulesMapper; +import jnpf.attendance.mapper.AttendanceUserBalanceMapper; +import jnpf.attendance.service.AttendanceLeaveRulesService; +import jnpf.attendance.service.AttendanceUserBalanceRecordService; +import jnpf.attendance.service.RuleScopeService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.*; +import jnpf.enums.attendance.LeaveUnitEnum; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.ApproveException; +import jnpf.model.attendance.dto.AttendanceLeaveRulesDto; +import jnpf.model.attendance.dto.AttendanceLeaveRulesQueryDto; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.ConstantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.attendance.ExpiresTimeUtil; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.MutablePair; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author panpan + */ +@Service +@Slf4j +public class AttendanceLeaveRulesServiceImpl implements AttendanceLeaveRulesService { + + @Resource + private AttendanceLeaveRulesMapper attendanceLeaveRulesMapper; + + @Resource + private AttendanceLeaveGrantSettingMapper attendanceLeaveGrantSettingMapper; + + @Resource + @Lazy + private RuleScopeUtil ruleScopeUtil; + + @Resource + private UserProvider userProvider; + + @Resource + @Lazy + private RuleScopeService ruleScopeService; + + @Resource + @Lazy + private AttendanceUserBalanceRecordService attendanceUserBalanceRecordService; + + @Resource + @Lazy + private V2UserApi v2UserApi; + + @Resource + @Lazy + private ExpiresTimeUtil expiresTimeUtil; + + @Resource + @Lazy + private AttendanceUserBalanceMapper attendanceUserBalanceMapper; + + + @Override + public PageInfo list(AttendanceLeaveRulesQueryDto attendanceLeaveRulesQueryDto) { + PageHelper.startPage(attendanceLeaveRulesQueryDto.getCurrentPage(), attendanceLeaveRulesQueryDto.getPageSize()); + PageInfo leaveRulesVoPageInfo = new PageInfo<>(attendanceLeaveRulesMapper.list(attendanceLeaveRulesQueryDto.getLeaveTypeId(), attendanceLeaveRulesQueryDto.getIText())); + // 当适配范围为指定组织或成员时查询适配范围 + if (null != leaveRulesVoPageInfo.getList() && !leaveRulesVoPageInfo.getList().isEmpty()) { + // 过滤出适配范围为指定组织或成员的规则id集合 + List scopeOfAdaptationIds = leaveRulesVoPageInfo.getList().stream().filter(v -> Objects.equals(v.getScopeOfAdaptation(), 1)).map(AttendanceLeaveRulesVo::getId).collect(Collectors.toList()); + if (!scopeOfAdaptationIds.isEmpty()) { + // 拼接适配范围 + Map, List>> stringMutablePairMap = ruleScopeUtil.selectScopeListBatch(scopeOfAdaptationIds, ScopeBizType.LEAVE_RULES); + // 遍历vo列表,根据规则id设置适配范围 + for (AttendanceLeaveRulesVo v : leaveRulesVoPageInfo.getList()) { + MutablePair, List> pair = stringMutablePairMap.get(v.getId()); + if (null != pair) { + v.setOrganizeNum(pair.getLeft().size()); + v.setUserIdNum(pair.getRight().size()); + } + } + } + leaveRulesVoPageInfo.getList().stream().filter(v -> Objects.equals(v.getScopeOfAdaptation(), -1)).forEach(vo -> vo.setScopeOfAdaptation(null)); + } + return leaveRulesVoPageInfo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(AttendanceLeaveRulesDto attendanceLeaveRulesDto) throws Exception { + // 通过传入的类型修改同类型下的假期范围 新增的范围为全部清空其他 + if (0 == attendanceLeaveRulesDto.getScopeOfAdaptation()) { + attendanceLeaveRulesMapper.updateByLeaveTypeId(attendanceLeaveRulesDto.getLeaveTypeId()); + } + String userId = userProvider.get().getUserId(); + Date date = new Date(); + AttendanceLeaveRules entity = BeanUtil.toBean(attendanceLeaveRulesDto, AttendanceLeaveRules.class); + entity.setCreatorUserId(userId); + entity.setCreatorTime(date); + entity.setLastModifyUserId(userId); + entity.setLastModifyTime(date); + entity.setType(0); + attendanceLeaveRulesMapper.insert(entity); + if (null != attendanceLeaveRulesDto.getLeaveGrantSetting()) { + // 保存余额发放规则详情 + AttendanceLeaveGrantSetting bean = BeanUtil.toBean(attendanceLeaveRulesDto.getLeaveGrantSetting(), AttendanceLeaveGrantSetting.class); + bean.setLeaveRulesId(entity.getId()); + bean.setLeaveTypeId(entity.getLeaveTypeId()); + attendanceLeaveGrantSettingMapper.insert(bean); + } + // 保存适配范围 + List ruleScopeList = ruleScopeUtil.getRuleScopeList(entity.getId(), entity.getScopeOfAdaptation(), MutablePair.of(attendanceLeaveRulesDto.getOrganizeList(), attendanceLeaveRulesDto.getUserIdList()), ScopeBizType.LEAVE_RULES, entity.getLeaveTypeId()); + ruleScopeUtil.saveBatch(ruleScopeList); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(AttendanceLeaveRulesDto attendanceLeaveRulesDto) throws Exception { + AttendanceLeaveRules oldEntity = attendanceLeaveRulesMapper.selectById(attendanceLeaveRulesDto.getId()); + // 通过传入的类型修改同类型下的假期范围 新增的范围为全部清空其他 + if (0 == attendanceLeaveRulesDto.getScopeOfAdaptation()) { + attendanceLeaveRulesMapper.updateByLeaveTypeId(attendanceLeaveRulesDto.getLeaveTypeId()); + } + UserInfo userInfo = userProvider.get(); + String userId = userInfo.getUserId(); + Date date = new Date(); + AttendanceLeaveRules entity = BeanUtil.toBean(attendanceLeaveRulesDto, AttendanceLeaveRules.class); + entity.setLastModifyUserId(userId); + entity.setLastModifyTime(date); + entity.setDeleteMark(0); + attendanceLeaveRulesMapper.updateById(entity); + // 删除旧余额发放规则详情(为更新做准备) + AttendanceLeaveGrantSetting attendanceLeaveGrantSetting = new AttendanceLeaveGrantSetting(); + attendanceLeaveGrantSetting.setDeleteMark(1); + attendanceLeaveGrantSettingMapper.update(attendanceLeaveGrantSetting, new UpdateWrapper().eq("F_LeaveRulesId", entity.getId())); + if (null != attendanceLeaveRulesDto.getLeaveGrantSetting()) { + // 保存余额发放规则详情 + AttendanceLeaveGrantSetting bean = BeanUtil.toBean(attendanceLeaveRulesDto.getLeaveGrantSetting(), AttendanceLeaveGrantSetting.class); + bean.setLeaveRulesId(entity.getId()); + bean.setLeaveTypeId(oldEntity.getLeaveTypeId()); + attendanceLeaveGrantSettingMapper.insert(bean); + } + ruleScopeUtil.updateRuleScopeList(entity.getId(), oldEntity.getScopeOfAdaptation(), entity.getScopeOfAdaptation(), MutablePair.of(attendanceLeaveRulesDto.getOrganizeList(), attendanceLeaveRulesDto.getUserIdList()), ScopeBizType.LEAVE_RULES, oldEntity.getLeaveTypeId()); + } + + private void clearRecord(String ruleId, List userIds, String ruleName, List list, String userId, boolean isClear) { + // 获取今天的日期 格式:yyyy-mm-dd + String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + List records = attendanceUserBalanceRecordService.list(new QueryWrapper() + .lambda() + .eq(AttendanceBalanceRecordEntity::getObjectId, ruleId) + .in(AttendanceBalanceRecordEntity::getUserId, userIds) + .eq(AttendanceBalanceRecordEntity::getType, 2) + .eq(AttendanceBalanceRecordEntity::getDeleteMark, 0) + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + ); + // 先根据userId再根据objectId过滤出objectId假类型数据并分组计算余额 + Map> userBalanceMap = records.stream().collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getUserId)); + userBalanceMap.forEach((uId, value) -> { + // 计算余额 + BigDecimal total = value.stream().map(AttendanceBalanceRecordEntity::getBalance).reduce(BigDecimal.ZERO, BigDecimal::add); + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .userId(uId) + .objectId(ruleId) + .content(getContent(ruleName, today, total, isClear)) + .quota(total) + .build(); + list.add(detailVo); + value.forEach(record -> { + record.setIsOver(1); + record.setState(2); + record.setLastModifyUserId(userId); + record.setLastModifyTime(new Date()); + }); + }); + attendanceUserBalanceRecordService.saveBatch(records); + } + + @NotNull + private static String getContent(String ruleName, String today, BigDecimal total, boolean isClear) { + if (isClear) { + // 系统已将yyyy-mm-dd发放方式更改为“不发放”后剩余余额的X.X天/小时清除 + return "系统已将" + today + "发放方式更改为“不发放”后剩余余额的" + total.setScale(1, RoundingMode.HALF_UP) + "天清除"; + } else { + return "系统已将" + today + "未使用的余额" + total.setScale(1, RoundingMode.HALF_UP) + "天,因" + ruleName + "适配范围中被移除,停用余额"; + } + } + + private List getUserIds(Integer scopeOfAdaptation, List userIds, String tenantId, MutablePair, List> pair) { + if (scopeOfAdaptation.equals(0)) { + userIds = v2UserApi.getAllUserInfoBatch(null, tenantId).getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } else { + if (null != pair) { + if (null != pair.getRight() && !pair.getRight().isEmpty()) { + userIds.addAll(pair.getRight()); + } + if (null != pair.getLeft() && !pair.getLeft().isEmpty()) { + ActionResult> result = v2UserApi.listTargetOrganizesOrHaveChild(pair.getLeft(), false, null, tenantId); + userIds.addAll(result.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList())); + } + } + } + return userIds; + } + + @Override + public AttendanceLeaveRulesVo detail(String id) { + AttendanceLeaveRulesVo bean = BeanUtil.toBean(attendanceLeaveRulesMapper.selectById(id), AttendanceLeaveRulesVo.class); + // 余额发放规则详情 + AttendanceLeaveGrantSetting attendanceLeaveGrantSetting = attendanceLeaveGrantSettingMapper.selectOne(new LambdaQueryWrapper() + .eq(AttendanceLeaveGrantSetting::getLeaveRulesId, id) + .eq(AttendanceLeaveGrantSetting::getDeleteMark, 0)); + if (null != attendanceLeaveGrantSetting) { + bean.setLeaveGrantSetting(BeanUtil.toBean(attendanceLeaveGrantSetting, AttendanceLeaveGrantSettingVo.class)); + } + if (Objects.equals(bean.getScopeOfAdaptation(), -1)) { + bean.setScopeOfAdaptation(null); + } + // 当适配范围为指定组织或成员时查询适配范围 + if (Objects.equals(bean.getScopeOfAdaptation(), 1)) { + MutablePair, List> pair = ruleScopeUtil.selectScopeList(id, ScopeBizType.LEAVE_RULES); + if (null != pair) { + bean.setOrganizeList(pair.getLeft()); + bean.setOrganizeNum(pair.getLeft().size()); + bean.setUserIdList(pair.getRight()); + bean.setUserIdNum(pair.getRight().size()); + } + } + return bean; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(String id) { + // 删除适配范围 + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, id) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.LEAVE_RULES)); + // 删除余额发放规则详情 +// 删除旧余额发放规则详情(为更新做准备) + AttendanceLeaveGrantSetting attendanceLeaveGrantSetting = new AttendanceLeaveGrantSetting(); + attendanceLeaveGrantSetting.setDeleteMark(1); + attendanceLeaveGrantSettingMapper.update(attendanceLeaveGrantSetting, new UpdateWrapper().eq("F_LeaveRulesId", id)); + // 删除假期 + AttendanceLeaveRules attendanceLeaveRules = new AttendanceLeaveRules(); + attendanceLeaveRules.setId(id); + attendanceLeaveRules.setDeleteMark(1); + attendanceLeaveRulesMapper.updateById(attendanceLeaveRules); + } + + @Override + public void updateState(AttendanceLeaveRulesDto attendanceLeaveRulesDto) { + attendanceLeaveRulesMapper.updateState(attendanceLeaveRulesDto.getId(), attendanceLeaveRulesDto.getState(), userProvider.get().getId()); + } + + @Override + public List getUserLeaveList(List leaveTypes) { + List rulesVoList = new ArrayList<>(); + String userId = userProvider.get().getUserId(); + List leaveTypeIds = leaveTypes.stream().map(AttendanceLeaveType::getId).collect(Collectors.toList()); + // 查询请假类型 未命中的不返回 + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(Collections.singletonList(userId), ScopeBizType.LEAVE_RULES, ConstantUtil.NUM_TRUE, leaveTypeIds); + if (null == attendanceRuleScopes || attendanceRuleScopes.isEmpty()) { + return rulesVoList; + } + List rules = attendanceLeaveRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveRules::getDeleteMark, 0) + .eq(AttendanceLeaveRules::getState, 1) + .ne(AttendanceLeaveRules::getScopeOfAdaptation, -1) + .in(AttendanceLeaveRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + .orderByAsc(AttendanceLeaveRules::getCreatorTime) + ); + // 过滤出发放余额类型的规则Id + // 过滤出命中规则对应的假期类型 理论上命中的请假类型只有一个 + Map rulesMap = rules.stream().collect(Collectors.toMap(AttendanceLeaveRules::getLeaveTypeId, a -> a, (k1, k2) -> k1)); + leaveTypes.forEach(leaveType -> { + // 确保和类型查出的顺序一致 + AttendanceLeaveRules attendanceLeaveRules = rulesMap.get(leaveType.getId()); + if (null != attendanceLeaveRules) { + AttendanceLeaveRulesVo bean = BeanUtil.toBean(attendanceLeaveRules, AttendanceLeaveRulesVo.class); + bean.setLeaveTypeName(leaveType.getName()); + rulesVoList.add(bean); + } + }); + List records = attendanceUserBalanceRecordService.list(new QueryWrapper() + .lambda() + .and(x -> x.in(StringUtil.isNotEmpty(leaveTypeIds), AttendanceBalanceRecordEntity::getObjectId, leaveTypeIds) + .or() + .isNull(AttendanceBalanceRecordEntity::getObjectId)) + .eq(AttendanceBalanceRecordEntity::getUserId, userId) + .eq(AttendanceBalanceRecordEntity::getDeleteMark, 0) + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + ); + // 过滤出objectId为空的存休数据并计算余额 + BigDecimal balance = records.stream() + .filter(x -> x.getType().equals(3)) + .map(AttendanceBalanceRecordEntity::getBalance) + .reduce(BigDecimal.ZERO, BigDecimal::add); + // 在list中过滤出存休并将余额赋值到对应的假期中 + rulesVoList.stream().filter(x -> x.getBuiltIn().equals(1)).forEach(x -> x.setBalance(balance)); + // 其他的过滤出objectId不为空的假类型数据并分组计算余额 + Map map = records.stream() + .filter(x -> x.getType().equals(2) && StringUtil.isNotEmpty(x.getObjectId())) + .collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getObjectId, Collectors.reducing(BigDecimal.ZERO, AttendanceBalanceRecordEntity::getBalance, BigDecimal::add))); + // 遍历vo列表,将余额设置到对应的假期中 + rulesVoList.forEach(x -> { + x.setBalance(map.getOrDefault(x.getLeaveTypeId(), BigDecimal.ZERO)); + }); + return rulesVoList; + } + + @Override + public void autoGrantBalance(String tenantId) { + // 查出所有启用中的假期且发放余额,且发放方式为自动发放的规则列表, + List rules = attendanceLeaveRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveRules::getDeleteMark, 0) + .eq(AttendanceLeaveRules::getState, 1) + .ne(AttendanceLeaveRules::getScopeOfAdaptation, -1) + .eq(AttendanceLeaveRules::getDistributeBalance, 1) + ); + if(CollUtil.isEmpty(rules)){ + return; + } + // 查出对应的发放规则详情 + List settings = attendanceLeaveGrantSettingMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveGrantSetting::getDeleteMark, 0) + .eq(AttendanceLeaveGrantSetting::getGrantType, 0) + .in(AttendanceLeaveGrantSetting::getLeaveRulesId, rules.stream().map(AttendanceLeaveRules::getId).collect(Collectors.toList())) + ); + // 获取今天的年月日 + LocalDate today = LocalDate.now(); + // 今天的年月日 格式yyyy-MM-dd + String todayStr = today.toString(); + int year = today.getYear(); + int month = today.getMonthValue(); + int day = today.getDayOfMonth(); + String monthday = month + "-" + day; + // 当前月份 最后一天的日 + int monthLastDay = LocalDate.of(year, month, day).with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); + // 需要发劵的用户余额记录 + List balanceRecordList = new ArrayList<>(); + // 消费记录 + List balanceDetailVoList = new ArrayList<>(); + // 需要查找的规则集合 + List allRuleIds = new ArrayList<>(); + // 生日规则集合 + List birthRuleIds = new ArrayList<>(); + // 入职规则集合 + List onboardingRuleIds = new ArrayList<>(); + // 需要处理的规则集合 + HashMap leaveRuleBalanceVoMap = new HashMap<>(); + for (AttendanceLeaveGrantSetting setting : settings) { + checkGrantTimeType(setting, day, monthLastDay, allRuleIds, monthday, birthRuleIds, onboardingRuleIds, leaveRuleBalanceVoMap, month); + } + if(CollUtil.isEmpty(allRuleIds)){ + return; + } + // 通过规则Id找下面的人 + List scopes = ruleScopeService.list(new QueryWrapper() + .lambda() + .eq(AttendanceRuleScope::getBizType, ScopeBizType.LEAVE_RULES.getValue()) + .in(AttendanceRuleScope::getRuleId, allRuleIds) + ); + // 查询全部用户 + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(List.of(), tenantId); + if (allUserInfoBatch.getCode() != 200 || allUserInfoBatch.getData() == null) { + return; + } + List allUser = allUserInfoBatch.getData(); + // 根据加强类型Id过滤出map + Map> expandMap = scopes.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getExpand)); + expandMap.forEach((expand, expandList) -> { + // 类型list 数据示列 :规则1 类型1 范围全部 ;规则2 类型1 范围 指定组织 ;规则3 类型1 范围 指定用户1 ;规则4 类型1 范围 指定用户2 + // 优先级 : 用户 > 组织 > 全部 + // 指定人的ids + List userIdsByExpand = expandList.stream().filter(x -> x.getScopeType() == 1).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + List orgIdsByExpand = expandList.stream().filter(x -> x.getScopeType() == 2).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + // 指定组织下的用户ids排除指定人的ids + List orgUserIdsByExpand = allUser.stream().filter(x -> !userIdsByExpand.contains(x.getId()) && orgIdsByExpand.contains(x.getOrganizeId())).map(UserBoundVO::getId).collect(Collectors.toList()); + boolean isAll = expandList.stream().anyMatch(x -> x.getScopeType() == 3); + // 全部对应的剩余用户 + List allUserIdsByExpand = new ArrayList<>(); + if (isAll) { + allUserIdsByExpand = allUser.stream().filter(x -> !userIdsByExpand.contains(x.getId()) && !orgUserIdsByExpand.contains(x.getId())).map(UserBoundVO::getId).collect(Collectors.toList()); + } + Map> scopeMap = expandList.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getRuleId)); + List finalAllUserIdsByExpand = allUserIdsByExpand; + scopeMap.forEach((ruleId, list) -> { + // 本规则命中的用户信息 + List userList = new ArrayList<>(); + // 规则对应的公式仅司龄发放有效 + List formulaList = new ArrayList<>(); + if (birthRuleIds.contains(ruleId)) { + // 处理每年成员生日发放 + // 拿到规则下对应的人 + // 是否有全部范围的数据 + boolean b = list.stream().anyMatch(x -> x.getScopeType() == 3); + if (b) { + // 比对月日相同的用户 + userList = allUser.stream().filter(x -> finalAllUserIdsByExpand.contains(x.getId()) && expiresTimeUtil.checkMonthDay(x.getBirthday(), today)).collect(Collectors.toList()); + } else { + List userIds = list.stream().filter(x -> x.getScopeType() == 1).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + List orgIds = list.stream().filter(x -> x.getScopeType() == 2).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + // 获取符合的组织用户 + userList = allUser.stream().filter(x -> orgIds.contains(x.getOrganizeId()) && !userIdsByExpand.contains(x.getId()) && expiresTimeUtil.checkMonthDay(x.getBirthday(), today)).collect(Collectors.toList()); + // 过滤该规则指定的用户 + userList.addAll(allUser.stream().filter(x -> userIds.contains(x.getId()) && expiresTimeUtil.checkMonthDay(x.getBirthday(), today)).collect(Collectors.toList())); + } + } else if (onboardingRuleIds.contains(ruleId)) { + // 处理每年成员入职日期发放 + // 是否有全部范围的数据 + boolean b = list.stream().anyMatch(x -> x.getScopeType() == 3); + if (b) { + // 比对月日相同的用户 +// userList = allUser.stream().filter(x -> expiresTimeUtil.checkMonthDay(x.getEntryDate(), today)).collect(Collectors.toList()); + userList = allUser.stream().filter(x -> finalAllUserIdsByExpand.contains(x.getId()) && expiresTimeUtil.checkMonthDay(x.getEntryDate(), today)).collect(Collectors.toList()); + } else { + List userIds = list.stream().filter(x -> x.getScopeType() == 1).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + List orgIds = list.stream().filter(x -> x.getScopeType() == 2).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + // 获取符合的用户 + // 获取符合的组织用户 + userList = allUser.stream().filter(x -> orgIds.contains(x.getOrganizeId()) && !userIdsByExpand.contains(x.getId()) && expiresTimeUtil.checkMonthDay(x.getEntryDate(), today)).collect(Collectors.toList()); + // 过滤该规则指定的用户 + userList.addAll(allUser.stream().filter(x -> userIds.contains(x.getId()) && expiresTimeUtil.checkMonthDay(x.getEntryDate(), today)).collect(Collectors.toList())); + } + } else { + // 每月指定日期发放、每月指定日期发放 + // 是否有全部范围的数据 + boolean b = list.stream().anyMatch(x -> x.getScopeType() == 3); + if (b) { + // 比对月日相同的用户 + userList = allUser.stream().filter(x -> finalAllUserIdsByExpand.contains(x.getId())).collect(Collectors.toList()); + } else { + List userIds = list.stream().filter(x -> x.getScopeType() == 1).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + List orgIds = list.stream().filter(x -> x.getScopeType() == 2).map(AttendanceRuleScope::getScopeValue).collect(Collectors.toList()); + // 获取符合的用户 + // 获取符合的组织用户 + userList = allUser.stream().filter(x -> orgIds.contains(x.getOrganizeId()) && !userIdsByExpand.contains(x.getId())).collect(Collectors.toList()); + // 过滤该规则指定的用户 + userList.addAll(allUser.stream().filter(x -> userIds.contains(x.getId())).collect(Collectors.toList())); + } + } + // 组装劵信息及券使用记录 + LeaveRuleBalanceVo ruleBalanceVo = leaveRuleBalanceVoMap.get(ruleId); + setParm(expand, ruleBalanceVo, formulaList, userList, balanceRecordList, todayStr, balanceDetailVoList); + }); + }); + if (!balanceRecordList.isEmpty()) { + // 发放劵 + attendanceUserBalanceRecordService.saveBatch(balanceRecordList); + } + if (!balanceDetailVoList.isEmpty()) { + // 消费记录 仅记录生成劵的使用记录 + attendanceUserBalanceMapper.batchAddBalanceDetail(balanceDetailVoList); + } + } + + @Override + public void deleteByTypeId(String id) { + + // 删除余额发放规则详情 + AttendanceLeaveGrantSetting attendanceLeaveGrantSetting = new AttendanceLeaveGrantSetting(); + attendanceLeaveGrantSetting.setLeaveTypeId(id); + attendanceLeaveGrantSetting.setDeleteMark(1); + attendanceLeaveGrantSettingMapper.updateById(attendanceLeaveGrantSetting); + // 删除假期 + AttendanceLeaveRules attendanceLeaveRules = new AttendanceLeaveRules(); + attendanceLeaveRules.setLeaveTypeId(id); + attendanceLeaveRules.setDeleteMark(1); + attendanceLeaveRulesMapper.updateById(attendanceLeaveRules); + // 删除适配范围 + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getExpand, id) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.LEAVE_RULES)); + } + + @Override + public AttendanceLeaveRulesVo getUserLeaveDetail(AttendanceLeaveType vo,String userId) throws ApproveException { + if (null == userId) { + userId = userProvider.get().getUserId(); + } + // 查询请假类型 未命中的不返回 + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(Collections.singletonList(userId), ScopeBizType.LEAVE_RULES, ConstantUtil.NUM_TRUE, Collections.singletonList(vo.getId())); + if (attendanceRuleScopes.isEmpty()) { + throw new ApproveException("未找到命中的类型"); + } + List rules = attendanceLeaveRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveRules::getDeleteMark, 0) + .eq(AttendanceLeaveRules::getState, 1) + .ne(AttendanceLeaveRules::getScopeOfAdaptation, -1) + .in(AttendanceLeaveRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + .orderByAsc(AttendanceLeaveRules::getCreatorTime) + ); + if (rules.isEmpty()) { + throw new ApproveException("未找到命中的规则"); + } + // 一个人一个请假类型,只能命中一个规则 + AttendanceLeaveRulesVo bean = BeanUtil.toBean(rules.get(0), AttendanceLeaveRulesVo.class); + bean.setUnitName(LeaveUnitEnum.getMsgByCode(bean.getUnit())); + bean.setLeaveTypeName(vo.getName()); + return bean; + } + + private void setParm(String ruleId, LeaveRuleBalanceVo ruleBalanceVo, List formulaList, List userList, List balanceRecordList, String todayStr, List balanceDetailVoList) { + if (ruleBalanceVo.getGrantDurationType() == 2) { + // 计算余额发放方式为司龄的余额 + formulaList = getFormulaList(ruleBalanceVo.getFormulaJson(), formulaList); + // 计算司龄 + List finalFormulaList = formulaList; + userList.forEach(user -> { + AttendanceBalanceRecordEntity balanceRecord = new AttendanceBalanceRecordEntity(); + // 计算司龄月份数 + Integer seniorityMonth = expiresTimeUtil.calculateSeniorityMonth(user.getEntryDate()); + // 进行范围命中并返回对应的日期 + for (LeaveRuleFormulaVo vo : finalFormulaList) { + if (seniorityMonth >= vo.getMin() && seniorityMonth < vo.getMax()) { + balanceRecord.setBalance(new BigDecimal(vo.getDay().toString())); + balanceRecord.setTotal(new BigDecimal(vo.getMonth().toString())); + } + } + balanceRecord.setUserId(user.getId()); + balanceRecord.setType(2); + balanceRecord.setObjectId(ruleId);// 类型Id + balanceRecord.setUnit(2); + balanceRecord.setExpireTime(ruleBalanceVo.getExpireTime()); + balanceRecordList.add(balanceRecord); + // 组织劵日志 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(ruleId) + .userId(user.getId()) + // 系统在yyyy-mm-dd,按规则自动发放余额X.X天/小时 + .content("系统在" + todayStr + ",按规则自动发放余额" + balanceRecord.getBalance().setScale(1, RoundingMode.HALF_UP) + "天") + .quota(balanceRecord.getBalance()) + .build(); + balanceDetailVoList.add(detailVo); + }); + } else { + userList.forEach(user -> { + AttendanceBalanceRecordEntity balanceRecord = new AttendanceBalanceRecordEntity(); + balanceRecord.setBalance(ruleBalanceVo.getBalance()); + balanceRecord.setTotal(ruleBalanceVo.getBalance()); + balanceRecord.setUserId(user.getId()); + balanceRecord.setType(2); + balanceRecord.setObjectId(ruleId); + balanceRecord.setUnit(2); + balanceRecord.setExpireTime(ruleBalanceVo.getExpireTime()); + balanceRecordList.add(balanceRecord); + // 组织劵日志 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(ruleId) + .userId(user.getId()) + // 系统在yyyy-mm-dd,按规则自动发放余额X.X天/小时 + .content("系统在" + todayStr + ",按规则自动发放余额" + ruleBalanceVo.getBalance().setScale(1, RoundingMode.HALF_UP) + "天") + .quota(ruleBalanceVo.getBalance()) + .build(); + balanceDetailVoList.add(detailVo); + }); + } + } + + /** + * 设置规则余额信息 + * + * @param setting 请假发放设置 + * @param day 当前日期 + * @param monthLastDay 当前月份最后一天 + * @param allRuleIds 所有规则集合 + * @param monthday 当前日期 MM-dd + * @param birthRuleIds 生日规则集合 + * @param onboardingRuleIds 入职规则集合 + * @param leaveRuleBalanceVoMap 请假余额映射 + */ + private void checkGrantTimeType(AttendanceLeaveGrantSetting setting, int day, int monthLastDay, List allRuleIds, String monthday, List birthRuleIds, List onboardingRuleIds, HashMap leaveRuleBalanceVoMap, int month) { + LeaveRuleBalanceVo leaveRuleBalanceVo = new LeaveRuleBalanceVo(); + // 1.每月发放2.每年指定日期发放3.每年成员生日发放4.每年成员入职日期发放 + // 处理每月发放 + if (setting.getGrantTimeType() == 1) { + // 检查是否是当前月份对应的日期,如果当前日期为月底最后一天,设置的日期数大于今天也能命中 + if (day == setting.getMonthDay() || (monthLastDay == day && setting.getMonthDay() >= day)) { + allRuleIds.add(setting.getLeaveRulesId()); + setRuleBalanceVo(setting, leaveRuleBalanceVo); + } + } else if (setting.getGrantTimeType() == 2) { + // 处理每年指定日期发放 2-29 + String[] split = setting.getSpecifyDay().split("-"); + int inMonth = Integer.parseInt(split[0]); + int inDay = Integer.parseInt(split[1]); + if (monthday.equals(setting.getSpecifyDay()) || (inMonth == month && monthLastDay == day && inDay >= day)) { + allRuleIds.add(setting.getLeaveRulesId()); + setRuleBalanceVo(setting, leaveRuleBalanceVo); + } + } else if (setting.getGrantTimeType() == 3) { + birthRuleIds.add(setting.getLeaveRulesId()); + allRuleIds.add(setting.getLeaveRulesId()); + // 处理每年成员生日发放 // 计算发放时长 + setRuleBalanceVo(setting, leaveRuleBalanceVo); + } else if (setting.getGrantTimeType() == 4) { + onboardingRuleIds.add(setting.getLeaveRulesId()); + allRuleIds.add(setting.getLeaveRulesId()); + // 处理每年成员入职日期发放 // 计算发放时长 + setRuleBalanceVo(setting, leaveRuleBalanceVo); + } + leaveRuleBalanceVoMap.put(setting.getLeaveRulesId(), leaveRuleBalanceVo); + } + + private static List getFormulaList(String formulaJson, List formulaList) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + formulaList = objectMapper.readValue(formulaJson, new TypeReference>() { + }); + formulaList.sort(Comparator.comparingInt(LeaveRuleFormulaVo::getMonth)); + Integer min = 0; + for (LeaveRuleFormulaVo v : formulaList) { + // 计算司龄 min max + if ("司龄<".equals(v.getTitle())) { + v.setMin(0); + v.setMax(v.getMonth()); + min = v.getMonth(); + } else if ("司龄≥".equals(v.getTitle())) { + v.setMin(min); + // 设置最大边界值 + v.setMax(999999999); + } else { + v.setMin(min); + v.setMax(v.getMonth()); + min = v.getMonth(); + } + } + } catch (JsonProcessingException e) { + log.error("假期司龄规则JSON解析失败: " + formulaJson, e); + } + return formulaList; + } + + private void setRuleBalanceVo(AttendanceLeaveGrantSetting setting, LeaveRuleBalanceVo leaveRuleBalanceVo) { + leaveRuleBalanceVo.setRuleId(setting.getLeaveRulesId()); + leaveRuleBalanceVo.setGrantDurationType(setting.getGrantDurationType()); + // 计算发放时长 + if (setting.getGrantDurationType() == 1) { + leaveRuleBalanceVo.setBalance(new BigDecimal(setting.getSpecifyDayNum().toString())); + } else if (setting.getGrantDurationType() == 2) { + leaveRuleBalanceVo.setFormulaJson(setting.getFormulaJson()); + } + // 计算过期时间 + leaveRuleBalanceVo.setExpireTime(expiresTimeUtil.getExpiresTime(0 == setting.getExpiredType() ? 0 : 1, setting.getExpiredDayNum(), null)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveTypeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveTypeServiceImpl.java new file mode 100644 index 0000000..2e4c434 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLeaveTypeServiceImpl.java @@ -0,0 +1,93 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.attendance.mapper.AttendanceLeaveTypeMapper; +import jnpf.attendance.service.AttendanceLeaveRulesService; +import jnpf.attendance.service.AttendanceLeaveTypeService; +import jnpf.entity.attendance.AttendanceLeaveType; +import jnpf.exception.ApproveException; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; +import jnpf.util.StringUtil; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * @author panpan + */ +@Service +public class AttendanceLeaveTypeServiceImpl extends ServiceImpl implements AttendanceLeaveTypeService { + + @Resource + private AttendanceLeaveTypeMapper attendanceLeaveTypeMapper; + + @Resource + @Lazy + private AttendanceLeaveRulesService attendanceLeaveRulesService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void typeSaveOrUpdate(AttendanceLeaveType attendanceLeaveType) { + // 校验名称是否重复,考虑修改时排查传入的id对应的数据 + List attendanceLeaveTypes = attendanceLeaveTypeMapper.selectList(new LambdaQueryWrapper() + .eq(AttendanceLeaveType::getName, attendanceLeaveType.getName()) + .eq(StringUtil.isNotEmpty(attendanceLeaveType.getId()), AttendanceLeaveType::getId, attendanceLeaveType.getId()) + .eq(AttendanceLeaveType::getDeleteMark, 0)); + if (!attendanceLeaveTypes.isEmpty()) { + throw new RuntimeException("名称重复"); + } + attendanceLeaveType.setDeleteMark(0); + this.saveOrUpdate(attendanceLeaveType); + } + + @Override + public void deleteType(String id) { + AttendanceLeaveType attendanceLeaveType = new AttendanceLeaveType(); + attendanceLeaveType.setId(id); + attendanceLeaveType.setDeleteMark(1); + attendanceLeaveType.setDeleteTime(new Date()); + // 删除关联的假期类型 + attendanceLeaveTypeMapper.updateById(attendanceLeaveType); + attendanceLeaveRulesService.deleteByTypeId(id); + + } + + @Override + public List getUserLeaveList() { + List vo = attendanceLeaveTypeMapper.selectList(new QueryWrapper().lambda() + .eq(AttendanceLeaveType::getDeleteMark, 0) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + return attendanceLeaveRulesService.getUserLeaveList(vo); + + } + + @Override + public AttendanceLeaveRulesVo getUserLeaveDetail(String id,String userId) throws ApproveException { + AttendanceLeaveType vo = attendanceLeaveTypeMapper.selectOne(new QueryWrapper().lambda() + .eq(AttendanceLeaveType::getId, id) + ); + return attendanceLeaveRulesService.getUserLeaveDetail(vo,userId); + } + + @Override + public List getAttendanceLeaveTypes(List holidayIdList) { + return attendanceLeaveTypeMapper.selectList(new QueryWrapper().lambda() + .eq(AttendanceLeaveType::getDeleteMark, 0) + .in(CollUtil.isNotEmpty(holidayIdList), AttendanceLeaveType::getId, holidayIdList) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + } + + @Override + public List getAttendanceLeaveTypesIncludeDeleted(List holidayIdList) { + return attendanceLeaveTypeMapper.selectList(new QueryWrapper().lambda() + .in(CollUtil.isNotEmpty(holidayIdList), AttendanceLeaveType::getId, holidayIdList) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingConfigServiceImpl.java new file mode 100644 index 0000000..d91ff8d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingConfigServiceImpl.java @@ -0,0 +1,338 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jnpf.attendance.mapper.FtbAttendanceLineSchedulingConfigMapper; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceLineSchedulingConfigService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.authority.FtbAuthorityApi; +import jnpf.base.ActionResult; +import jnpf.base.ModuleApi; +import jnpf.base.SystemApi; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.entity.SystemEntity; +import jnpf.base.service.SuperServiceImpl; +import jnpf.base.vo.PageListVO; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingConfig; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.dto.LineDrawingPeriodDto; +import jnpf.model.attendance.model.AttendanceGroupParam; +import jnpf.model.attendance.model.LineShiftChangeNoticeModel; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.position.QueryPositionUserListDTO; +import jnpf.permission.vo.v2.position.PositionListUserVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.util.StringUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.apache.commons.compress.utils.Lists; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static jnpf.constants.RedisConstant.ATTENDANCE_LINE_SCHEDULING_NOTICE; + +/** + * 划线排班配置Service实现 + * + * @author ahua + * @version 2.1 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2025-12-30 + */ +@Service +public class AttendanceLineSchedulingConfigServiceImpl extends SuperServiceImpl implements AttendanceLineSchedulingConfigService { + + @Resource + private FtbAttendanceLineSchedulingConfigMapper ftbAttendanceLineSchedulingConfigMapper; + @Autowired + private AttendanceUserService attendanceGroupUserService; + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private FtbPersonnelsRosterManagerApi ftbPersonnelsRosterManagerApi; + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private FtbAuthorityApi ftbAuthorityApi; + @Autowired + private ModuleApi moduleApi; + @Autowired + private SystemApi systemApi; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private V2PositionApi v2PositionApi; + @Autowired + private StringRedisTemplate redisTemplate; + /** + * 根据考勤组ID查询划线排班配置 + * + * @param groupId 考勤组ID + * @return 划线排班配置 + */ + @Override + public FtbAttendanceLineSchedulingConfig getByGroupId(String groupId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbAttendanceLineSchedulingConfig::getGroupId, groupId); + FtbAttendanceLineSchedulingConfig ftbAttendanceLineSchedulingConfig = ftbAttendanceLineSchedulingConfigMapper.selectOne(queryWrapper); + if (Objects.isNull(ftbAttendanceLineSchedulingConfig)) { + return getFtbAttendanceLineSchedulingConfig(groupId); + } + List memberList = StringUtil.isEmpty(ftbAttendanceLineSchedulingConfig.getMembers()) ? Lists.newArrayList() : Arrays.asList(ftbAttendanceLineSchedulingConfig.getMembers().split(",")); + List notifyPersonList = StringUtil.isEmpty(ftbAttendanceLineSchedulingConfig.getNotifyPerson()) ? Lists.newArrayList() : Arrays.asList(ftbAttendanceLineSchedulingConfig.getNotifyPerson().split(",")); + ActionResult> userList = v2UserApi.getUserPrimaryBoundBatch(CollUtil.unionAll(memberList, notifyPersonList), null); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + List collect = userList.getData().stream().map(userPrimaryPositionDetailVO -> { + AttendanceGroupUserVo groupUserVo = new AttendanceGroupUserVo(); + groupUserVo.setUserId(userPrimaryPositionDetailVO.getId()); + groupUserVo.setRealName(userPrimaryPositionDetailVO.getUserName()); + groupUserVo.setHeadIcon(UploaderUtil.uploaderImg(userPrimaryPositionDetailVO.getHeadIcon())); + groupUserVo.setPositionName(userPrimaryPositionDetailVO.getPositionName()); + return groupUserVo; + }).collect(Collectors.toList()); + ftbAttendanceLineSchedulingConfig.setMemberList(collect.stream().filter(vo -> memberList.contains(vo.getUserId())).collect(Collectors.toList())); + ftbAttendanceLineSchedulingConfig.setNotifyPersonList(collect.stream().filter(vo -> notifyPersonList.contains(vo.getUserId())).collect(Collectors.toList())); + } + return ftbAttendanceLineSchedulingConfig; + } + + private FtbAttendanceLineSchedulingConfig getFtbAttendanceLineSchedulingConfig(String groupId) { + FtbAttendanceLineSchedulingConfig lineSchedulingConfig = new FtbAttendanceLineSchedulingConfig(); + lineSchedulingConfig.setGroupId(groupId); + lineSchedulingConfig.setMembersType(1); + lineSchedulingConfig.setStartCheckInLimit(BigDecimal.valueOf(2)); + lineSchedulingConfig.setEndCheckInLimit(BigDecimal.valueOf(5)); + lineSchedulingConfig.setStartType(1); + lineSchedulingConfig.setStartTime("00:00"); + lineSchedulingConfig.setEndType(1); + lineSchedulingConfig.setEndTime("23:00"); + lineSchedulingConfig.setNotifyTimeType(1); + lineSchedulingConfig.setMemberList(List.of()); + lineSchedulingConfig.setNotifyPersonList(List.of()); + return lineSchedulingConfig; + } + + @Override + public void noticeLineScheduling(String tenantId) { + if (StringUtil.isEmpty(tenantId)) return; + String key = String.format(ATTENDANCE_LINE_SCHEDULING_NOTICE, tenantId); + List list1; + if(redisTemplate.hasKey(key)) { + String value = redisTemplate.opsForValue().get(key); + list1 = JSON.parseArray(value, FtbAttendanceLineSchedulingConfig.class); + }else { + //划线排班配置 + list1 = lambdaQuery().list(); + redisTemplate.opsForValue().set(key, JSON.toJSONString(list1)); + } + Date date = new Date(); + String minute = DateUtil.format(date, "HH:mm"); + if (CollUtil.isEmpty(list1) || list1.stream().filter(config->Objects.nonNull(config.getNotifyTime())).noneMatch(config -> config.getNotifyTime().equals(minute))) { + return; + } + + List list = attendanceGroupService.list(new LambdaQueryWrapper().eq(AttendanceGroup::getLineSchedulingSetting, true).eq(AttendanceGroup::getDeleteMark, 0)); + if (CollUtil.isEmpty(list)) { + log.error("未查询到启用划线排班的考勤组"); + return; + } + List groupIds = list.stream().map(AttendanceGroup::getId).collect(Collectors.toList()); + Map group2ManagerMap = list.stream().filter(vo -> StringUtil.isNotEmpty(vo.getManagerId())).collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getManagerId)); + Map group2NameMap = list.stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getGroupName)); + Map org2GroupMap = list.stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getOrgId)); + //获取当前考勤组成员 + Date day = DateUtil.beginOfDay(date); + List attendanceGroupUserVos = attendanceGroupUserService.getAttendanceGroupUsersOfSecondment(day, day, null, groupIds, Boolean.TRUE); + if (CollUtil.isEmpty(attendanceGroupUserVos)) { + return; + } + List allUserIds = attendanceGroupUserVos.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + //获取组织下员工信息 + StaffRosterListReq staffRosterListReq = new StaffRosterListReq(); + staffRosterListReq.setPageSize(-1); + staffRosterListReq.setIsQueryAuth("0"); + staffRosterListReq.setUserIds(allUserIds); + ActionResult> pageListVOActionResult = ftbPersonnelsRosterManagerApi.postWithSalary(staffRosterListReq); + PageListVO data = pageListVOActionResult.getData(); + List userBaseDtoList = data.getList(); + Map> userBaseMap = userBaseDtoList.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRosterDto::getCurrOrg)); + Map lineConfigMap = list1.stream().collect(Collectors.toMap(FtbAttendanceLineSchedulingConfig::getGroupId, Function.identity())); + List notifyPositionList = list1.stream().flatMap(config -> StringUtil.isEmpty(config.getNotifyPosition()) ? Stream.of() : Stream.of(StringUtil.split(config.getNotifyPosition(), ","))).collect(Collectors.toList()); + QueryPositionUserListDTO queryPositionUserListDTO = new QueryPositionUserListDTO(); + queryPositionUserListDTO.setTenantId(tenantId); + queryPositionUserListDTO.setPositionIds(notifyPositionList); + ActionResult> listActionResult = v2PositionApi.listPositionTreeUser(queryPositionUserListDTO); + List data1 = listActionResult.getData(); + Map> positionUsersMap = data1.stream().collect(Collectors.toMap(PositionListUserVO::getId, userVo->userVo.getUserList().stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()))); + //查询这些考勤组当天的排班信息 + List allRules = attendanceDailyRuleService.list(new LambdaQueryWrapper() + .in(FtbAttendanceDailyRule::getGroupId, groupIds) + .eq(FtbAttendanceDailyRule::getDay, day) + .in(FtbAttendanceDailyRule::getUserId, allUserIds)); + Map> ruleMap = allRules.stream().collect(Collectors.groupingBy(FtbAttendanceDailyRule::getGroupId)); + List noticeUserIds = CollUtil.newArrayList(); + List noticeAdminUserIds = CollUtil.newArrayList(); + Map groupParamMap = new HashMap<>(); + attendanceGroupUserVos.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)).forEach((groupId, userList) -> { + String orgId = org2GroupMap.get(groupId); + if (StringUtil.isEmpty(orgId)) { + return; + } + List rules = ruleMap.getOrDefault(groupId, Lists.newArrayList()); + List userIdOfRule = rules.stream().map(FtbAttendanceDailyRule::getUserId).distinct().collect(Collectors.toList()); + List userIds = userList.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + FtbAttendanceLineSchedulingConfig ftbAttendanceLineSchedulingConfig = lineConfigMap.get(groupId); + if (Objects.isNull(ftbAttendanceLineSchedulingConfig)) { + return; + } + if (!StringUtil.equals(ftbAttendanceLineSchedulingConfig.getNotifyTime(), minute)) { + return; + } + List userBaseList = userBaseMap.get(orgId); + if (CollUtil.isEmpty(userBaseList)) { + return; + } + lineSchedulesConfigFilter(userIds, ftbAttendanceLineSchedulingConfig, userBaseList); + if (CollUtil.isEmpty(userIds)) { + return; + } + userIds.removeAll(userIdOfRule); + noticeUserIds.addAll(userIds); + //负责人未排班通知 + List notifyPersonArr = StringUtil.isEmpty(ftbAttendanceLineSchedulingConfig.getNotifyPerson()) ? CollUtil.newArrayList() : Arrays.stream(StringUtil.split(ftbAttendanceLineSchedulingConfig.getNotifyPerson(), ",")).collect(Collectors.toList()); + List notifyPositionArr = StringUtil.isEmpty(ftbAttendanceLineSchedulingConfig.getNotifyPosition()) ? CollUtil.newArrayList() : Arrays.stream(StringUtil.split(ftbAttendanceLineSchedulingConfig.getNotifyPosition(), ",")).collect(Collectors.toList()); + List notifyPositionUserIds = notifyPositionArr.stream().flatMap(positionId -> positionUsersMap.getOrDefault(positionId, CollUtil.newArrayList()).stream()).collect(Collectors.toList()); + notifyPersonArr.addAll(notifyPositionUserIds); + if (CollUtil.isEmpty(notifyPersonArr) && group2ManagerMap.containsKey(groupId)) { + notifyPersonArr.add(group2ManagerMap.get(groupId)); + } + noticeAdminUserIds.addAll(notifyPersonArr); + AttendanceGroupParam attendanceGroupParam = new AttendanceGroupParam(groupId, group2NameMap.get(groupId)); + notifyPersonArr.forEach(userId -> { + if (!groupParamMap.containsKey(userId)) { + groupParamMap.put(userId, attendanceGroupParam); + } + }); + + }); + //被排班人未排班通知 + LineShiftChangeNoticeModel shiftChangeNoticeModel = new LineShiftChangeNoticeModel(); + shiftChangeNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + shiftChangeNoticeModel.setUserIds(noticeUserIds); + shiftChangeNoticeModel.setDay(day); + shiftChangeNoticeModel.setPeriods(List.of(LineDrawingPeriodDto.builder().day(day).build())); + shiftChangeNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.LINE_SHIFT_CHANG); + attendanceNoticeHandler.send(shiftChangeNoticeModel); + //查询权限 + //查询moduleId + SystemEntity systemVO = systemApi.getSystemVO(); + String systemId = systemVO.getId(); + List modules = moduleApi.getListWithoutSystemId(systemId, "APP", "考勤管理", tenantId); + ModuleEntity moduleEntity = modules.stream().findFirst().orElse(null); + if (Objects.isNull(moduleEntity)) { + return; + } + Map> userId2GroupIds = attendanceGroupUserVos.stream().filter(vo -> noticeAdminUserIds.contains(vo.getUserId())).collect(Collectors.groupingBy(AttendanceGroupUser::getUserId, Collectors.mapping(vo -> org2GroupMap.get(vo.getGroupId()), Collectors.toList()))); + Map> stringListMap = ftbAuthorityApi.batchAuthOrganizesAllForUserIdsAndTenantId(noticeAdminUserIds, 0, moduleEntity.getId(), tenantId); + //清除没有划线排班考勤组权限的负责人 + for (Map.Entry> entry : userId2GroupIds.entrySet()) { + List orgIds = entry.getValue(); + List strings = stringListMap.get(entry.getKey()); + if (CollUtil.isEmpty(strings)) { + continue; + } + List collect = strings.stream().filter(orgIds::contains).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)) { + continue; + } + //负责人未排班通知 + shiftChangeNoticeModel.setTenantId(StringUtil.isEmpty(tenantId) ? UserProvider.getUser().getTenantId() : tenantId); + shiftChangeNoticeModel.setUserIds(List.of(entry.getKey())); + shiftChangeNoticeModel.setDay(day); + shiftChangeNoticeModel.setPeriods(List.of(LineDrawingPeriodDto.builder().day(day).build())); + shiftChangeNoticeModel.setModuleId(moduleEntity.getId()); + shiftChangeNoticeModel.setAttendanceGroup(groupParamMap.get(entry.getKey())); + shiftChangeNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.LINE_SHIFT_NOT_SCHEDUING); + attendanceNoticeHandler.send(shiftChangeNoticeModel); + } + + } + + @Override + public boolean saveOrUpdateLineSchedulingConfig(FtbAttendanceLineSchedulingConfig config) { + FtbAttendanceLineSchedulingConfig oldConfig = lambdaQuery().eq(FtbAttendanceLineSchedulingConfig::getGroupId, config.getGroupId()).last("limit 1").one(); + if(Objects.nonNull(oldConfig)){ + config.setId(oldConfig.getId()); + } + if (saveOrUpdate(config)) { + redisTemplate.delete(String.format(ATTENDANCE_LINE_SCHEDULING_NOTICE, UserProvider.getUser().getTenantId())); + Date date = new Date(); + List attendanceGroupUsers = attendanceGroupUserService.queryByUsersAndGroupFilterSecondment(date, date, List.of(), CollUtil.newArrayList(config.getGroupId())); + List userIds = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + List oldUserIds = CollUtil.newArrayList(); + if(Objects.nonNull(oldConfig)) { + oldUserIds.addAll(userIds); + attendanceDailyRuleService.getFtbAttendanceLineSchedulingConfig(oldUserIds, oldConfig); + } + attendanceDailyRuleService.getFtbAttendanceLineSchedulingConfig(userIds, config); + oldUserIds.removeAll(userIds); + if(CollUtil.isEmpty(oldUserIds)) { + return true; + } + attendanceDailyRuleService.clearGroupRule(config.getGroupId(), oldUserIds, date,null); + return true; + } + return false; + } + + private void lineSchedulesConfigFilter(List userIds, FtbAttendanceLineSchedulingConfig lineSchedulingConfig, List userList) { + //划线排班配置过滤 + if (CollUtil.isNotEmpty(userIds)) { + return; + } + if (Objects.isNull(lineSchedulingConfig)) { + userIds.clear(); + return; + } + List workNatureArr = StringUtil.isEmpty(lineSchedulingConfig.getWorkNature()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getWorkNature(), ",")); + List employeeTypeArr = StringUtil.isEmpty(lineSchedulingConfig.getEmployeeType()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getEmployeeType(), ",")); + List positionArr = StringUtil.isEmpty(lineSchedulingConfig.getPosition()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getPosition(), ",")); + List memberArr = StringUtil.isEmpty(lineSchedulingConfig.getMembers()) ? List.of() : Arrays.asList(StringUtil.split(lineSchedulingConfig.getMembers(), ",")); + if (CollUtil.isEmpty(workNatureArr) && CollUtil.isEmpty(employeeTypeArr) && CollUtil.isEmpty(positionArr) && !Objects.equals(lineSchedulingConfig.getMembersType(), 0) && CollUtil.isEmpty(memberArr)) { + return; + } + boolean hasOnboarding = workNatureArr.contains("301"); + List userIdsForConfigFilter = userList.stream() + .filter(dto -> (CollUtil.isNotEmpty(workNatureArr) && workNatureArr.contains(dto.getWorkerStatus())) + && ((hasOnboarding && StringUtil.isEmpty(dto.getWorkerType())) || employeeTypeArr.contains("-1") || CollUtil.isNotEmpty(employeeTypeArr) && employeeTypeArr.contains(dto.getWorkerType())) + || (CollUtil.isNotEmpty(positionArr) && positionArr.contains(dto.getCurrPosition())) + || Objects.equals(lineSchedulingConfig.getMembersType(), 0) + || (CollUtil.isNotEmpty(memberArr) && memberArr.contains(dto.getUserId()))).map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + userIds.removeIf(userId -> !userIdsForConfigFilter.contains(userId)); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingPayrollHoursServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingPayrollHoursServiceImpl.java new file mode 100644 index 0000000..8fbc056 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLineSchedulingPayrollHoursServiceImpl.java @@ -0,0 +1,167 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.attendance.mapper.FtbAttendanceLineSchedulingPayrollHoursMapper; +import jnpf.attendance.service.AttendanceLineSchedulingPayrollHoursService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.FtbAttendanceLineSchedulingPayrollHours; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 划线排班计薪工时Service实现 + * + * @author jnpf + * @since 2026-02-27 + */ +@Service +public class AttendanceLineSchedulingPayrollHoursServiceImpl extends SuperServiceImpl implements AttendanceLineSchedulingPayrollHoursService { + + @Override + public List listByUserIdAndGroupId(String userId, String groupId, Date startDay, Date endDay) { + if (CollUtil.isEmpty(CollUtil.newArrayList(userId)) || Objects.isNull(groupId) || Objects.isNull(startDay) || Objects.isNull(endDay)) { + return CollUtil.newArrayList(); + } + return lambdaQuery() + .eq(FtbAttendanceLineSchedulingPayrollHours::getUserId, userId) + .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupId) + .ge(FtbAttendanceLineSchedulingPayrollHours::getDay, startDay) + .le(FtbAttendanceLineSchedulingPayrollHours::getDay, endDay) + .eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0) + .list(); + } + + @Override + public List listByUserIdsAndGroupId(List userIds, String groupId, Date startDay, Date endDay) { + if (CollUtil.isEmpty(userIds) || Objects.isNull(groupId) || Objects.isNull(startDay) || Objects.isNull(endDay)) { + return CollUtil.newArrayList(); + } + return lambdaQuery() + .in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds) + .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupId) + .ge(FtbAttendanceLineSchedulingPayrollHours::getDay, startDay) + .le(FtbAttendanceLineSchedulingPayrollHours::getDay, endDay) + .eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0) + .list(); + } + + @Override + public List listByGroupId(String groupId, Date startDay, Date endDay) { + if (Objects.isNull(groupId) || Objects.isNull(startDay) || Objects.isNull(endDay)) { + return CollUtil.newArrayList(); + } + return lambdaQuery() + .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupId) + .ge(FtbAttendanceLineSchedulingPayrollHours::getDay, startDay) + .le(FtbAttendanceLineSchedulingPayrollHours::getDay, endDay) + .eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0) + .list(); + } + + @Override + public boolean saveOrUpdateBatch(List payrollHoursList) { + if (CollUtil.isEmpty(payrollHoursList)) { + return false; + } + return super.saveOrUpdateBatch(payrollHoursList); + } + + @Override + public boolean deleteByUserIdAndGroupId(String userId, String groupId, Date day) { + if (Objects.isNull(userId) || Objects.isNull(groupId) || Objects.isNull(day)) { + return false; + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbAttendanceLineSchedulingPayrollHours::getUserId, userId) + .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupId) + .eq(FtbAttendanceLineSchedulingPayrollHours::getDay, day) + .set(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 1); + return update(updateWrapper); + } + + @Override + public boolean deleteByGroupId(String groupId, Date startDay, Date endDay) { + if (Objects.isNull(groupId) || Objects.isNull(startDay) || Objects.isNull(endDay)) { + return false; + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupId) + .ge(FtbAttendanceLineSchedulingPayrollHours::getDay, startDay) + .le(FtbAttendanceLineSchedulingPayrollHours::getDay, endDay) + .set(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 1); + return update(updateWrapper); + } + @Override + public FtbAttendanceLineSchedulingPayrollHours listByUserIdsAndDays(String userId, String groupId, Date day) { + return listByUserIdsAndDays(List.of(userId),List.of(groupId),List.of(day)).stream().findFirst().orElse(null); + } + @Override + public List listByUserIdsAndDays(List userIds, String groupId, List days) { + return listByUserIdsAndDays(userIds,List.of(groupId),days); + } + @Override + public List listByUserIdsAndDays(List userIds, List groupIds, List days) { + if (CollUtil.isEmpty(userIds) || CollUtil.isEmpty(groupIds) || CollUtil.isEmpty(days)) { + return CollUtil.newArrayList(); + } + return lambdaQuery() + .in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds) + .in(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupIds) + .in(FtbAttendanceLineSchedulingPayrollHours::getDay, days) + .eq(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 0) + .list(); + } + + @Override + public boolean savePayrollHoursList(List payrollHoursList) { + if (CollUtil.isEmpty(payrollHoursList)) { + return false; + } + + // 从集合中提取groupId、userIds和days + String groupId = payrollHoursList.get(0).getGroupId(); + if (Objects.isNull(groupId)) { + return false; + } + + // 验证所有记录的groupId是否相同 + boolean allSameGroupId = payrollHoursList.stream().allMatch(item -> Objects.equals(item.getGroupId(), groupId)); + if (!allSameGroupId) { + return false; + } + + // 提取唯一的用户ID集合 + List userIds = payrollHoursList.stream() + .map(FtbAttendanceLineSchedulingPayrollHours::getUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + // 提取唯一的日期集合 + List days = payrollHoursList.stream() + .map(FtbAttendanceLineSchedulingPayrollHours::getDay) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(userIds) || CollUtil.isEmpty(days)) { + return false; + } + + // 先清除指定考勤组、用户集合和日期集合的计薪工时数据 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.in(FtbAttendanceLineSchedulingPayrollHours::getUserId, userIds) + .eq(FtbAttendanceLineSchedulingPayrollHours::getGroupId, groupId) + .in(FtbAttendanceLineSchedulingPayrollHours::getDay, days) + .set(FtbAttendanceLineSchedulingPayrollHours::getDeleteMark, 1); + update(updateWrapper); + + // 保存新的计薪工时数据 + return saveBatch(payrollHoursList.stream().filter(vo->Objects.nonNull(vo.getPayrollHours())).collect(Collectors.toList())); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLocationSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLocationSettingServiceImpl.java new file mode 100644 index 0000000..5fcf670 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceLocationSettingServiceImpl.java @@ -0,0 +1,410 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import jnpf.attendance.mapper.AttendanceLocationSettingMapper; +import jnpf.attendance.mapper.AttendanceMachineManageMapper; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceLocationSettingService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.entity.attendance.AttendanceLocationSetting; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceLocationSettingDto; +import jnpf.model.attendance.dto.SaveForStoreDto; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.AttendanceLocationSettingVo; +import jnpf.permission.StoreApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.dto.store.QueryStoreListDTO; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.util.ConstantUtil; +import jnpf.util.RandomUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + *

+ * 考勤组-考勤点配置表 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-29 + */ +@Service +public class AttendanceLocationSettingServiceImpl extends SuperServiceImpl implements AttendanceLocationSettingService { + + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private AttendanceMachineManageMapper attendanceMachineManageMapper; + @Autowired + private AttenceMachineService attenceMachineService; + @Autowired + private StoreApi storeApi; + @Override + public List findList(String groupId, Integer type) { + Map> enableLocationSetting = getEnableLocationSetting(CollUtil.newArrayList(groupId)); + List orDefault = enableLocationSetting.getOrDefault(groupId, CollUtil.newArrayList()); + if (CollUtil.isEmpty(orDefault)) { + return CollUtil.newArrayList(); + } + return orDefault.stream() + .filter(location -> Objects.equals(location.getType(), type)) + .map(entity -> BeanUtil.toBean(entity, AttendanceLocationSettingVo.class)) + .collect(Collectors.toList()); + + } + + @Override + public void saveForStore(SaveForStoreDto saveForStore) { + Assert.isFalse(CollUtil.isEmpty(saveForStore.getStoreIds()), "未查询到所选门店信息"); + List storeList = storeApi.listInfo(new QueryStoreListDTO() {{ + setIds(saveForStore.getStoreIds()); + }}).getData(); + Assert.isFalse(CollUtil.isEmpty(storeList), "未查询到所选门店信息"); + Date date = new Date(); + String loginUserId = UserProvider.getLoginUserId(); + List collect = storeList.stream().map(store -> { + AttendanceLocationSetting attendanceLocationSetting = new AttendanceLocationSetting(); + attendanceLocationSetting.setId(RandomUtil.uuId()); + attendanceLocationSetting.setEnable(1); + attendanceLocationSetting.setType(1); + attendanceLocationSetting.setGroupId(saveForStore.getGroupId()); + attendanceLocationSetting.setName(store.getStoreName()); + attendanceLocationSetting.setAddress(store.getAddress()); + attendanceLocationSetting.setLat(store.getLatitude()); + attendanceLocationSetting.setLng(store.getLongitude()); + attendanceLocationSetting.setClockRange(saveForStore.getClockRange()); + attendanceLocationSetting.setDeleteMark(0); + attendanceLocationSetting.setCreatorTime(date); + attendanceLocationSetting.setCreatorUserId(loginUserId); + attendanceLocationSetting.setStoreId(store.getId()); + return attendanceLocationSetting; + }).collect(Collectors.toList()); + saveBatch(collect); + } + @Override + public void delForStore(SaveForStoreDto saveForStore) { + Assert.isFalse(CollUtil.isEmpty(saveForStore.getStoreIds()), "未查询到所选门店信息"); + lambdaUpdate().set(AttendanceLocationSetting::getDeleteMark, 1) + .set(AttendanceLocationSetting::getDeleteTime, new Date()) + .set(AttendanceLocationSetting::getDeleteUserId, UserProvider.getLoginUserId()) + .eq(AttendanceLocationSetting::getGroupId, saveForStore.getGroupId()) + .in(AttendanceLocationSetting::getStoreId, saveForStore.getStoreIds()) + .update(); + } + + @Override + public List getStoreIds(String groupId) { + List list = lambdaQuery().select(AttendanceLocationSetting::getStoreId).eq(AttendanceLocationSetting::getGroupId, groupId) + .eq(AttendanceLocationSetting::getType, 1) + .isNotNull(AttendanceLocationSetting::getStoreId) + .eq(AttendanceLocationSetting::getDeleteMark, 0) + .list(); + return list.stream().map(AttendanceLocationSetting::getStoreId).collect(Collectors.toList()); + } + + @Override + public void save(AttendanceLocationSettingDto dto) throws HandleException { + AttendanceLocationSetting entity = BeanUtil.toBean(dto, AttendanceLocationSetting.class); + UserInfo user = UserProvider.getUser(); + List attendanceGroups = attendanceGroupService.queryListByIds(CollUtil.newArrayList(dto.getGroupId())); + if (CollUtil.isEmpty(attendanceGroups)) { + throw new HandleException("当前考勤组未查到"); + } + AttendanceGroup attendanceGroup = attendanceGroups.get(0); + Integer enable; + if (dto.getType() == 1) { + enable = attendanceGroup.getGpsEnable(); + } else if (dto.getType() == 2) { + enable = attendanceGroup.getWifiEnable(); + } else { + enable = attendanceGroup.getMachineEnable(); + } + if (StringUtil.isEmpty(entity.getId())) { + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(user.getUserId()); + entity.setTenantId(user.getTenantId()); + entity.setEnable(enable); + entity.setDeleteMark(0); + } else { + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(user.getUserId()); + entity.setEnable(enable); + entity.setDeleteMark(0); + } + saveOrUpdate(entity); + if (Objects.equals(dto.getType(), 3) && StringUtil.isEmpty(dto.getId())) { + sendUserToMachine(dto.getFactoryCode(), dto.getAddress(), dto.getGroupId()); + } + } + + @Override + public void sendSingleUserToMachine(String groupId, String userId) { + List list = lambdaQuery().eq(AttendanceLocationSetting::getGroupId, groupId) + .eq(AttendanceLocationSetting::getType, 3) + .eq(AttendanceLocationSetting::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(location -> { + sendUserToMachine(location.getFactoryCode(), location.getAddress(), CollUtil.newArrayList(userId)); + }); + } + + /** + * 批量删除设备人员 + * + * @param groupId 考勤组id + * @param userId 用户id + */ + @Override + public void deleteUserToMachine(String groupId, String userId) { + List list = lambdaQuery().eq(AttendanceLocationSetting::getGroupId, groupId) + .eq(AttendanceLocationSetting::getType, 3) + .eq(AttendanceLocationSetting::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isEmpty(list)) { + return; + } + list.forEach(location -> attenceMachineService.deleteUserList(location.getFactoryCode(), location.getAddress(), CollUtil.newArrayList(userId))); + } + + private void sendUserToMachine(String factoryCode, String sn, String groupId) { + // List attendanceGroupUsers = attendanceUserService.queryByUsersGroupIds(CollUtil.newArrayList(groupId)); + // List collect = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + List userIds = attendanceGroupService.getSelfAndChildrenMembers(groupId); + sendUserToMachine(factoryCode, sn, userIds); + } + + private void sendUserToMachine(String factoryCode, String sn, List userIds) { + if (userIds.isEmpty()) { + return; + } + userIds.forEach(userId -> { + attenceMachineService.sendUserToMachine(factoryCode, userId, sn); + }); + } + + /** + * 删除人员 + * + * @param factoryCode 厂商编码 + * @param sn 设备号 + * @param groupId 考勤组id + */ + private void deleteUserToMachineByGroupId(String factoryCode, String sn, String groupId) { + /*List attendanceGroupUsers = attendanceUserService.queryByUsersGroupIds(CollUtil.newArrayList(groupId)); + List userIds = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList());*/ + List userIds = attendanceGroupService.getSelfAndChildrenMembers(groupId); + if (!userIds.isEmpty()) { + attenceMachineService.deleteUserList(factoryCode, sn, userIds); + } + } + + @Override + @Transactional + public void initLocationSetting(String groupId) { + List list = lambdaQuery().eq(AttendanceLocationSetting::getGroupId, groupId) + .eq(AttendanceLocationSetting::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isNotEmpty(list)) { + return; + } + + Map> enableBaseSetting = getEnableLocationSetting(CollUtil.newArrayList(groupId)); + List attendanceLocationSettings = enableBaseSetting.get(groupId); + AttendanceGroup byId = attendanceGroupService.getById(groupId); + if (CollUtil.isNotEmpty(attendanceLocationSettings)) { + AttendanceLocationSetting attendanceLocationSetting1 = attendanceLocationSettings.stream().findFirst().orElse(null); + AttendanceGroup byId1 = null; + if (attendanceLocationSetting1 != null) { + byId1 = attendanceGroupService.getById(attendanceLocationSetting1.getGroupId()); + } + byId.setGpsEnable(byId1.getGpsEnable()); + byId.setWifiEnable(byId1.getWifiEnable()); + byId.setMachineEnable(byId1.getMachineEnable()); + attendanceGroupService.updateById(byId); + attendanceLocationSettings.forEach(attendanceLocationSetting -> { + attendanceLocationSetting.setId(RandomUtil.uuId()); + attendanceLocationSetting.setGroupId(groupId); + attendanceLocationSetting.setCreatorTime(new Date()); + attendanceLocationSetting.setCreatorUserId(UserProvider.getLoginUserId()); + }); + } + //初始化组织配置地址至考勤组 + ActionResult info = storeApi.info(byId.getOrgId()); + if (Objects.isNull(info) || Objects.isNull(info.getData())){ + if (CollUtil.isEmpty(attendanceLocationSettings)){ + return; + } + saveBatch(attendanceLocationSettings); + return; + } + if(Objects.isNull(attendanceLocationSettings)){ + attendanceLocationSettings = Lists.newArrayList(); + } + StoreBaseListInfo data = info.getData(); + AttendanceLocationSetting attendanceLocationSetting = new AttendanceLocationSetting(); + attendanceLocationSetting.setGroupId(groupId); + attendanceLocationSetting.setName(data.getStoreName()); + attendanceLocationSetting.setAddress(data.getAddress()); + attendanceLocationSetting.setLat(data.getLatitude()); + attendanceLocationSetting.setLng(data.getLongitude()); + attendanceLocationSetting.setType(1); + attendanceLocationSetting.setClockRange(100); + attendanceLocationSetting.setEnable(1); + attendanceLocationSetting.setId(RandomUtil.uuId()); + attendanceLocationSetting.setCreatorTime(new Date()); + attendanceLocationSetting.setCreatorUserId(UserProvider.getLoginUserId()); + attendanceLocationSetting.setDeleteMark(0); + attendanceLocationSetting.setStoreId(byId.getOrgId()); + attendanceLocationSettings.add(attendanceLocationSetting); + saveBatch(attendanceLocationSettings); + } + + @Override + public Map> getEnableLocationSetting(List groupIds) { + List attendanceGroupVos = attendanceGroupService.queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return Maps.newHashMap(); + } + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId())); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getAttendancePointsSetting() == 1)); + List list = lambdaQuery() + .eq(AttendanceLocationSetting::getDeleteMark, Boolean.FALSE) + .ne(AttendanceLocationSetting::getType, ConstantUtil.DEVICE_MACHINE) + .list(); + Map> collect = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.groupingBy(AttendanceLocationSetting::getGroupId)); + collect1.forEach((groupId, enable) -> { + if (enable && !collect.containsKey(groupId)) { + collect.put(groupId, CollUtil.newArrayList()); + } + }); + // 根据groupIds查询考勤机管理的考勤机 + for (String groupId : groupIds) { + List machineList = attendanceMachineManageMapper.getRelationMachineByGroupId(groupId); + List mList = changeToLocation(groupId, machineList); + if (!machineList.isEmpty()) { + collect.compute(groupId, (k, v) -> { + if (v == null) { + return mList; + } else { + v.addAll(mList); + return v; + } + }); + } + } + Map> map = Maps.newHashMap(); + groupIds.forEach(groupId -> map.put(groupId, findNextBaseSetting(collect, groupIdToParent, groupId))); + return map; + } + + private List changeToLocation(String groupId, List machineList) { + + List list = new ArrayList<>(); + machineList.forEach(machine -> list.add(new AttendanceLocationSetting(machine.getId(), groupId, machine.getName(), machine.getMac(), machine.getType()))); + return list; + } + + @Override + public List getEnableGroup(String groupId) { + List attendanceGroupVos = attendanceGroupService.queryDropList(); + if (CollUtil.isEmpty(attendanceGroupVos)) { + log.error("未查询到考勤组信息"); + return CollUtil.newArrayList(); + } + AttendanceGroup attendanceGroup = attendanceGroupVos.stream().filter(group -> StringUtil.equals(group.getId(), groupId)).reduce((g1, g2) -> g1).orElse(null); + if (attendanceGroup != null && Objects.equals(attendanceGroup.getAttendancePointsSetting(), 0)) { + log.error("当前考勤组未开启考勤点配置"); + return CollUtil.newArrayList(); + } + Map> groupIdToParent = attendanceGroupVos.stream().collect(Collectors.groupingBy(AttendanceGroup::getParentId)); + List groupVos = CollUtil.newArrayList(); + List orDefault = groupIdToParent.getOrDefault(groupId, CollUtil.newArrayList()); + if (CollUtil.isEmpty(orDefault)) { + return CollUtil.newArrayList(); + } + getEnableSingleGroup(orDefault, groupVos, groupIdToParent); + return groupVos; + } + + private void getEnableSingleGroup(List groups, List groupVos, Map> groupIdToParent) { + groups.forEach(group -> { + if (Objects.equals(group.getAttendancePointsSetting(), 1)) { + return; + } + groupVos.add(BeanUtil.toBean(group, AttendanceGroupVo.class)); + List attendanceGroups = groupIdToParent.get(group.getId()); + if (CollUtil.isEmpty(attendanceGroups)) { + return; + } + getEnableSingleGroup(attendanceGroups, groupVos, groupIdToParent); + }); + } + + private List findNextBaseSetting(Map> collect, Map groupIdToParent, String groupId) { + if (StringUtil.isBlank(groupId)) { + return CollUtil.newArrayList(); + } + if (!collect.containsKey(groupId)) { + return findNextBaseSetting(collect, groupIdToParent, groupIdToParent.get(groupId)); + } + return collect.get(groupId); + } + + @Override + public void del(String id) { + lambdaUpdate().set(AttendanceLocationSetting::getDeleteMark, Boolean.TRUE) + .set(AttendanceLocationSetting::getDeleteUserId, UserProvider.getLoginUserId()) + .set(AttendanceLocationSetting::getDeleteTime, new Date()) + .eq(AttendanceLocationSetting::getId, id) + .update(); + AttendanceLocationSetting byId = getById(id); + if (Objects.equals(byId.getType(), 3)) { + deleteUserToMachineByGroupId(byId.getFactoryCode(), byId.getAddress(), byId.getGroupId()); + } + } + + @Override + public void changeStatus(String groupId, Integer type, Integer enable) { + lambdaUpdate().set(AttendanceLocationSetting::getEnable, enable) + .eq(AttendanceLocationSetting::getGroupId, groupId) + .eq(AttendanceLocationSetting::getType, type) + .update(); + } + + @Override + public void hisLocationSetting(Map group2orgMap, Map org2groupMap) { + List list = lambdaQuery().eq(AttendanceLocationSetting::getDeleteMark, Boolean.FALSE).list(); + list.forEach(item->item.setGroupId(org2groupMap.getOrDefault(group2orgMap.getOrDefault(item.getGroupId(),item.getGroupId()),item.getGroupId()))); + AtomicInteger i = new AtomicInteger(0); + list.stream().collect(Collectors.groupingBy(vo -> vo.getGroupId() + vo.getType() + vo.getName())).forEach((vo,settings)->{ + i.set(0); + settings.stream().skip(1).forEach(setting->setting.setName(setting.getName()+i.incrementAndGet())); + }); + updateBatchById(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineManageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineManageServiceImpl.java new file mode 100644 index 0000000..01421b2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineManageServiceImpl.java @@ -0,0 +1,485 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.mapper.*; +import jnpf.attendance.service.*; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.dto.PageDto; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.AttendanceMachineSync; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.AttendanceMachineManageVo; +import jnpf.model.attendance.vo.MachineScopeVo; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import jnpf.model.attendance.vo.attendance.GroupUserMiniVo; +import jnpf.model.attendance.vo.attendance.LogMiniVo; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 考勤机管理服务实现 + * + * @author yanwenfu + * @create 2024-09-10 + */ +@Slf4j +@Service +public class AttendanceMachineManageServiceImpl implements AttendanceMachineManageService { + + @Resource + private AttendanceMachineManageMapper attendanceMachineManageMapper; + @Resource + private AttendanceMachineLogMapper attendanceMachineLogMapper; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Resource + private AttendanceGroupService attendanceGroupService; + @Resource + private AttendanceGroupUserMapper attendanceGroupUserMapper; + @Resource + private AttendanceUserFaceMapper attendanceUserFaceMapper; + @Resource + private AttenceMachineService attenceMachineService; + @Resource + private AttendanceMachineSyncService attendanceMachineSyncService; + @Resource + private RuleScopeUtil ruleScopeUtil; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private RedisUtil redisUtil; + + public static final String formatOrg = "【%s个组织】"; + public static final String formatUser = "【%s位成员】"; + @Autowired + private AttendanceUserService attendanceUserService; + + @Override + public List getGroupList(String machineId) { + + List authGroupList = attendanceGroupService.queryGroupListNew(null, new GroupQueryDto()); + if (null == authGroupList || authGroupList.isEmpty()) { + return List.of(); + } + List authGroupIds = authGroupList.stream().filter(v -> v.getIsGray().equals(Boolean.FALSE)).map(AttendanceGroupVo::getId).collect(Collectors.toList()); + List list; + if (StringUtils.isEmpty(machineId)) { + // 考勤机id为空 查询所有考勤组 + list = attendanceGroupMapper.getAllGroupByLevel(null); + } else { + // 不为空 查询考勤机绑定的考勤组 + String groupIds = attendanceMachineManageMapper.getGroupIdsByMachine(machineId); + if (StringUtils.isEmpty(groupIds)) { + return new ArrayList<>(); + } + String[] split = groupIds.split(","); + list = attendanceGroupMapper.getAllGroupByLevel(Arrays.asList(split)); + } + list = list.stream().filter(v -> authGroupIds.contains(v.getGroupId())).collect(Collectors.toList()); + return list; + } + + @Override + public PageListVO getMachineList(MachineQueryDto queryDto) { + + if (queryDto.getCurrentPage() == 0) { + queryDto.setCurrentPage(1); + } + Integer scopeType = null; + List scopeValueList = new ArrayList<>(); + if (StringUtils.isNotEmpty(queryDto.getOrganizeId())) { + // 判定是组织还是班组, 组织过滤适配范围的组织, 班组过滤组织范围的人 + if (queryDto.getOrganizeCategory().equals(OrganizeCategoryEnums.TEAM)) { + scopeType = ConstantUtil.RULE_SCOPE_USER; + List userList = getOrgUserList(List.of(queryDto.getOrganizeId())); + List collect = userList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + scopeValueList.addAll(collect); + } else { + scopeType = ConstantUtil.RULE_SCOPE_ORG; + scopeValueList.add(queryDto.getOrganizeId()); + } + } + // 无数据时使用 + PageListVO returnPage = new PageListVO<>(); + returnPage.setList(new ArrayList<>()); + PaginationVO pagination = new PaginationVO(); + pagination.setTotal(0); + pagination.setPageSize((long) queryDto.getPageSize()); + pagination.setCurrentPage(0L); + returnPage.setPagination(pagination); + // 查询 + List dbList = attendanceMachineManageMapper.getMachineList(queryDto.getMachineKind(), queryDto.getKeywords(), + scopeType, scopeValueList, ScopeBizType.ATTENDANCE_MACHINE.getValue()); + if (dbList.isEmpty()) { + return returnPage; + } + List list = JsonUtil.getJsonToList(dbList, AttendanceMachineManageVo.class); + list.forEach(v -> { + String strOrg = v.getOrgCount() > 0 ? String.format(formatOrg, v.getOrgCount()) : ""; + String strUser = v.getUserCount() > 0 ? String.format(formatUser, v.getUserCount()) : ""; + String result = Stream.of(strOrg, strUser) + .filter(s -> !s.isEmpty()) + .collect(Collectors.joining("、")); + v.setScope(result); + setOnline(v); + }); + int total = attendanceMachineManageMapper.getTotal(); + pagination.setTotal(total); + pagination.setCurrentPage((long) queryDto.getCurrentPage()); + pagination.setPageSize((long) queryDto.getPageSize()); + long startPage = (pagination.getCurrentPage() - 1) * pagination.getPageSize(); + // 数据筛选 + if (null != queryDto.getOnline()) { + list = list.stream().filter(v -> v.getOnline().equals(queryDto.getOnline())).skip(startPage).limit(pagination.getPageSize()).collect(Collectors.toList()); + } else { + list = list.stream().skip(startPage).limit(pagination.getPageSize()).collect(Collectors.toList()); + } + returnPage.setList(list); + returnPage.setPagination(pagination); + return returnPage; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeMachineRelation(List addUserIds, String moveTo, List delGroupIds) { + + int size = 0; + for (String groupId : delGroupIds) { + // 查询所有关联此考勤组的考勤机 + List machineList = attendanceMachineManageMapper.getRelationMachineByGroupId(groupId); + if (machineList.isEmpty()) { + return; + } + // 考勤机移除关联 + machineList.forEach(machine -> { + String[] split = machine.getGroupIds().split(","); + StringBuilder sb = new StringBuilder(); + for (String str : split) { + if (str.equals(groupId)) { + continue; + } + sb.append(str).append(","); + } + attendanceMachineManageMapper.updateRelationGroup(machine.getId(), sb.length() > 0 ? sb.substring(0, sb.length() - 1) : ""); + }); + // 查询考勤组用户 + List userIds = attendanceGroupUserMapper.getGroupUserListByIds(List.of(groupId)); + if (!userIds.isEmpty()) { + // 相关考勤机移除这些用户 + machineList.forEach(machine -> attenceMachineService.deleteUserList(machine.getType(), machine.getMac(), userIds)); + } + size += machineList.size(); + } + // 需要移动到别的考勤组的成员 + if (StringUtils.isNotEmpty(moveTo) && !addUserIds.isEmpty()) { + // 查询与考勤组相关的考勤机, 将成员下发到考勤机 + List machineList = attendanceMachineManageMapper.getRelationMachineByGroupId(moveTo); + if (!machineList.isEmpty()) { + // 查询用户信息 +// Map userMap = userApi.getInfoMapByIds(addUserIds); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(addUserIds, null); + Map userMap = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userMap = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) ; + } + // 下发成员到考勤机 + Map finalUserMap = userMap; + addUserIds.forEach(userId -> { + UserBoundVO userBoundVO = finalUserMap.get(userId); + PartUserInfoVo user = BeanUtil.copyProperties(userBoundVO, PartUserInfoVo.class); +// PartUserInfoVo user = userMap.get(userId); + if (null != user) { + user.setRealName(userBoundVO.getUserName()); + machineList.forEach(machine -> attenceMachineService.sendUserToMachine(machine.getType(), user, machine.getMac())); + } + }); + } + } + } + + private void setOnline(AttendanceMachineManageVo v) { + + MachineEnum machineEnum = MachineEnum.getMachineEnum(v.getType()); + if (null == machineEnum) { + v.setOnline(ConstantUtil.NUM_FALSE); + } else { + switch (machineEnum) { + case KAI_JIA_YI: + String mac = redisUtil.getHashValues(String.format(ConstantUtil.ONLINE_MAC, machineEnum.getValue()), v.getMac()); + v.setOnline(StringUtils.isEmpty(mac) ? ConstantUtil.NUM_FALSE : ConstantUtil.NUM_TRUE); + break; + case MAO_TONG: + String key = String.format(ConstantUtil.ONLINE_MAC, machineEnum.getValue()); + String values = redisUtil.getHashValues(key, UserProvider.getUser().getTenantId()); + if (StringUtils.isEmpty(values)) { + v.setOnline(ConstantUtil.NUM_FALSE); + } else { + v.setOnline(values.contains(v.getMac()) ? ConstantUtil.NUM_TRUE : ConstantUtil.NUM_FALSE); + } + break; + case KE_MI: + Object str = redisUtil.getString(ConstantUtil.KE_MI + v.getMac()); + v.setOnline(null == str ? ConstantUtil.NUM_FALSE : ConstantUtil.NUM_TRUE); + break; + default: + v.setOnline(ConstantUtil.NUM_FALSE); + break; + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addMachine(MachineDto machineDto) throws HandleException { + + // 判断考勤机名称不能重复 + int nameCount = attendanceMachineManageMapper.getMachineNameCount(machineDto.getName(), null); + if (nameCount > 0) { + throw new HandleException("该考勤机已存在,请换个名称!"); + } + // 判断考勤机Mac地址不能重复 + int macCount = attendanceMachineManageMapper.getMachineMacCount(machineDto.getName(), null); + if (macCount > 0) { + throw new HandleException("该考勤机ID已存在,请检查考勤机ID是否输入错误"); + } + AttendanceMachineManage machineManage = JsonUtil.getJsonToBean(machineDto, AttendanceMachineManage.class); + machineManage.setId(FtbUtil.getId()); + attendanceMachineManageMapper.insert(machineManage); + // 添加考勤机租户关系到redis + redisUtil.insertHash(ConstantUtil.DEVICE_TENANT, machineDto.getMac(), UserProvider.getUser().getTenantId()); + // 新增适配范围 + List ruleScopeList = ruleScopeUtil.getRuleScopeList(machineManage.getId(), ConstantUtil.SCOPE_ORG_USER, + MutablePair.of(machineDto.getOrganizeList(), machineDto.getUserIdList()), ScopeBizType.ATTENDANCE_MACHINE); + ruleScopeUtil.saveBatch(ruleScopeList); + // 查询用户信息并下发到考勤机 + getInfoAndSendToMachine(machineDto.getType(), machineDto.getMac(), machineDto.getOrganizeList(), machineDto.getUserIdList()); + } + + private void getInfoAndSendToMachine(String type, String mac, List organizeList, List userIdList) { + // 组织成员查询 + List userList = getOrgUserList(organizeList); + // 成员列表信息 + if (null != userIdList && !userIdList.isEmpty()) { + ActionResult> userResult = v2UserApi.getAllUserInfoBatch(userIdList, UserProvider.getUser().getTenantId()); + if (null != userResult && 200 == userResult.getCode()) { + userList.addAll(userResult.getData()); + } + } + // 下发成员到考勤机 + Set userSet = new HashSet<>(); + userList.forEach(userBoundVO -> { + if (userSet.add(userBoundVO.getId())) { + PartUserInfoVo user = new PartUserInfoVo(); + user.setUserId(userBoundVO.getId()); + user.setRealName(userBoundVO.getUserName()); + user.setUserNo(userBoundVO.getUserNo()); + user.setHeadIcon(userBoundVO.getHeadIcon()); + user.setUserId(userBoundVO.getId()); + user.setRealName(userBoundVO.getUserName()); + user.setGender(userBoundVO.getGender()); + attenceMachineService.sendUserToMachine(type, user, mac); + } + }); + } + + private List getOrgUserList(List organizeList) { + + List userList = new ArrayList<>(); + if (null != organizeList && !organizeList.isEmpty()) { + ActionResult> orgResult = v2UserApi.listTargetOrganizesOrHaveChild(organizeList, + Boolean.FALSE, List.of(UserWorkStatusEnums.RESIGNED), UserProvider.getUser().getTenantId()); + if (null != orgResult && 200 == orgResult.getCode()) { + userList.addAll(orgResult.getData()); + } + } + return userList; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateMachine(String id, MachineUpdateDto machineUpdateDto) throws HandleException { + + AttendanceMachineManage machineManage = attendanceMachineManageMapper.selectById(id); + if (null == machineManage) { + throw new HandleException("未找到该考勤机"); + } + // 判断考勤机名称不能重复 + int nameCount = attendanceMachineManageMapper.getMachineNameCount(machineUpdateDto.getName(), id); + if (nameCount > 0) { + throw new HandleException("该考勤机已存在,请换个名称!"); + } + // 适配范围变更 + UpdateChangeDto updateChange = ruleScopeUtil.updateRuleScopeList(machineManage.getId(), ConstantUtil.SCOPE_ORG_USER, ConstantUtil.SCOPE_ORG_USER, + MutablePair.of(machineUpdateDto.getOrganizeList(), machineUpdateDto.getUserIdList()), ScopeBizType.ATTENDANCE_MACHINE); + // 需要删除的 + List delUserList = getOrgUserList(updateChange.getDelOrgList()); + List delUserIds = delUserList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + delUserIds.addAll(updateChange.getDelUserList()); + if (!delUserIds.isEmpty()) { + // 从考勤机中移除 + attenceMachineService.deleteUserList(machineManage.getType(), machineManage.getMac(), delUserIds); + } + // 需要新增的 + // 查询用户信息并下发到考勤机 + getInfoAndSendToMachine(machineManage.getType(), machineManage.getMac(), updateChange.getAddOrgList(), updateChange.getAddUserList()); + // 更新考勤机信息 + machineManage.setName(machineUpdateDto.getName()); + machineManage.setLastModifyTime(new Date()); + machineManage.setLastModifyUserId(UserProvider.getUser().getUserId()); + attendanceMachineManageMapper.updateById(machineManage); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteMachine(String id) throws HandleException { + + AttendanceMachineManage machineManage = attendanceMachineManageMapper.selectById(id); + if (null == machineManage) { + throw new HandleException("未找到该考勤机"); + } + // 适配范围变更 + UpdateChangeDto updateChange = ruleScopeUtil.updateRuleScopeList(machineManage.getId(), ConstantUtil.SCOPE_ORG_USER, ConstantUtil.SCOPE_ORG_USER, + MutablePair.of(List.of(), List.of()), ScopeBizType.ATTENDANCE_MACHINE); + // 需要删除的 + List delUserList = getOrgUserList(updateChange.getDelOrgList()); + List delUserIds = delUserList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + delUserIds.addAll(updateChange.getDelUserList()); + if (!delUserIds.isEmpty()) { + // 从考勤机中移除 + attenceMachineService.deleteUserList(machineManage.getType(), machineManage.getMac(), delUserIds); + } + // 数据库操作 + machineManage.setDeleteMark(ConstantUtil.NUM_TRUE); + machineManage.setDeleteTime(new Date()); + machineManage.setDeleteUserId(UserProvider.getUser().getUserId()); + attendanceMachineManageMapper.updateById(machineManage); + // 移除redis考勤机租户关系 + redisUtil.removeHash(ConstantUtil.DEVICE_TENANT, machineManage.getMac()); + } + + @Override + public List getGroupUserList(String groupId) { + + List returnList = new ArrayList<>(); + DateDetail dateDetail = new DateDetail(); + Date end = dateDetail.getCurrentDate(); + Date start = dateDetail.addNum(dateDetail.getCurrentDate(), -2, Calendar.MONTH); + List groupUserList = attendanceUserService.getAttendanceGroupUsersOfSecondment(start, end, List.of(), List.of(groupId)); + List userIds = groupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (!userIds.isEmpty()) { + // 查询人脸信息 + Map faceMap = getFaceMap(userIds); + // 查询用户信息 + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, null); + Map userMap = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userMap = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) ; + } + Map finalUserMap = userMap; + userIds.forEach(userId -> { + AttendanceUserFace userFace = faceMap.get(userId); + UserBoundVO userBoundVO = finalUserMap.get(userId); + GroupUserMiniVo mini = new GroupUserMiniVo(userId, userBoundVO == null ? "--" : userBoundVO.getUserName(), userFace == null ? "" : userFace.getFaceData()); + returnList.add(mini); + }); + } + return returnList; + } + + private Map getFaceMap(List userIds) { + + LambdaQueryWrapper faceQuery = new LambdaQueryWrapper() + .in(AttendanceUserFace::getUserId, userIds) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + List faceList = attendanceUserFaceMapper.selectList(faceQuery); + return faceList.stream().collect(Collectors.toMap(AttendanceUserFace::getUserId, Function.identity())); + } + + @Override + public List getMachineMemberList(String id) { + + return null; + } + + @Override + public PageInfo getLogList(String id, PageDto pageQuery) { + + // 查询考勤机信息 + AttendanceMachineManage machineManage = attendanceMachineManageMapper.selectById(id); + if (null == machineManage) { + return new PageInfo<>(); + } + // 查询3个月的打卡记录 + DateDetail dateDetail = new DateDetail(); + Date date = dateDetail.addNum(null, -3, Calendar.MONTH); + PageHelper.startPage(pageQuery.getCurrentPage(), pageQuery.getPageSize()); + return new PageInfo<>(attendanceMachineLogMapper.getLogList(ActionEnum.DA_KA.getDescription(), date, machineManage.getMac())); + } + + @Override + public List syncMachineMemberData(String id) { + + // 查询考勤机信息 + AttendanceMachineManage machineManage = attendanceMachineManageMapper.selectById(id); + if (null == machineManage || StringUtils.isEmpty(machineManage.getType())) { + return new ArrayList<>(); + } + List returnList = new ArrayList<>(); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(AttendanceMachineSync::getMac, machineManage.getMac()); + List list = attendanceMachineSyncService.list(query); + if (!list.isEmpty()) { + List userIds = list.stream().map(AttendanceMachineSync::getUserId).collect(Collectors.toList()); + // 查询人脸数据 + Map faceMap = getFaceMap(userIds); + list.forEach(v -> { + AttendanceUserFace userFace = faceMap.get(v.getUserId()); + GroupUserMiniVo user = new GroupUserMiniVo(v.getUserId(), v.getUserName(), null == userFace ? "" : userFace.getFaceData()); + returnList.add(user); + }); + } + return returnList; + } + + @Override + public MachineScopeVo getUpdateDetail(String id) { + + AttendanceMachineManage machine = attendanceMachineManageMapper.selectById(id); + MachineScopeVo machineScope = JsonUtil.getJsonToBean(machine, MachineScopeVo.class); + MutablePair, List> pair = ruleScopeUtil.selectScopeList(id, ScopeBizType.ATTENDANCE_MACHINE); + machineScope.setOrganizeList(pair.getLeft()); + machineScope.setUserIdList(pair.getRight()); + return machineScope; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineSyncServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineSyncServiceImpl.java new file mode 100644 index 0000000..a1396f7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceMachineSyncServiceImpl.java @@ -0,0 +1,17 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceMachineSyncMapper; +import jnpf.attendance.service.AttendanceMachineSyncService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceMachineSync; +import org.springframework.stereotype.Service; + +/** + * 考勤机同步服务实现 + * + * @author yanwenfu + * @create 2024-10-24 + */ +@Service +public class AttendanceMachineSyncServiceImpl extends SuperServiceImpl implements AttendanceMachineSyncService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceNoticeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceNoticeServiceImpl.java new file mode 100644 index 0000000..310e44f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceNoticeServiceImpl.java @@ -0,0 +1,150 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceNoticeMapper; +import jnpf.attendance.service.AttendanceNoticeService; +import jnpf.base.UserInfo; +import jnpf.constants.AttendanceConstant; +import jnpf.entity.attendance.AttendanceNoticeEntity; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.dto.NoticeContentInfoDto; +import jnpf.model.attendance.dto.NoticeSaveDto; +import jnpf.model.attendance.model.NoticeConfirm; +import jnpf.model.attendance.vo.NoticeConfirmListVo; +import jnpf.model.attendance.vo.NoticeConfirmVo; +import jnpf.model.attendance.vo.NoticeContentInfoVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.alibaba.fastjson.JSON.parseArray; + +/** + * 考勤消息通知 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2024-08-08 10:49:41 + */ +@Service +public class AttendanceNoticeServiceImpl extends ServiceImpl implements AttendanceNoticeService { + + @Autowired + private RedissonClient redissonClient; + @Autowired + private UserAntifreeze userAntifreeze; + + @Override + public NoticeContentInfoVo getContentInfo(NoticeContentInfoDto req) { + AttendanceNoticeEntity attendanceNotice = this.getById(req.getId()); + Assert.notNull(attendanceNotice, "未查询到消息通知"); + AttendanceNoticeEnum noticeEnum = AttendanceNoticeEnum.getAttendanceNoticeEnum(attendanceNotice.getType()); + Assert.notNull(noticeEnum, "未查询到相应的消息通知类型"); + List shiftsJsonVoList = parseArray(attendanceNotice.getDataJson(), JSONObject.class); + NoticeContentInfoVo contentInfoVo = new NoticeContentInfoVo(); + if (Objects.nonNull(attendanceNotice.getCreatorUserId())) { + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(attendanceNotice.getCreatorUserId()), UserProvider.getUser().getTenantId()); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + if (Objects.nonNull(partUserInfoVo)) { + String operator = partUserInfoVo.getRealName() + "(" + partUserInfoVo.getOrganizeName() + "-" + partUserInfoVo.getPositionName() + ")"; + contentInfoVo.setOperator(operator); + } + } + contentInfoVo.setTitle(attendanceNotice.getTitle()); + contentInfoVo.setTime(attendanceNotice.getCreatorTime()); + contentInfoVo.setDataList(shiftsJsonVoList); + return contentInfoVo; + } + + @Override + public void noticeSave(NoticeSaveDto noticeSaveDto) { + this.save(AttendanceNoticeEntity.builder() + .id(noticeSaveDto.getId()) + .type(noticeSaveDto.getType().getCode()) + .title(StringUtils.isNotEmpty(noticeSaveDto.getTitle()) ? noticeSaveDto.getTitle() : null) + .dataJson(StringUtils.isNotEmpty(noticeSaveDto.getDataJson()) ? noticeSaveDto.getDataJson() : null) + .isConfirm(Objects.nonNull(noticeSaveDto.getIsConfirm()) ? noticeSaveDto.getIsConfirm() : null) + .confirmList(StringUtils.isNotEmpty(noticeSaveDto.getConfirmList()) ? noticeSaveDto.getConfirmList() : null) + .creatorTime(new Date()) + .creatorUserId(StringUtils.isNotEmpty(noticeSaveDto.getUserId()) ? noticeSaveDto.getUserId() : null) + .build()); + } + + @Override + public void noticeConfirm(String id) { + UserInfo user = UserProvider.getUser(); + String key = String.format(AttendanceConstant.FTB_ATTENDANCE_NOTICE_CONFIRM_KEY, user.getTenantId(), id); + RLock lock = redissonClient.getLock(key); + try { + if (lock.tryLock(10,20, TimeUnit.SECONDS)) { + AttendanceNoticeEntity byId = getById(id); + if (Objects.equals(byId.getIsConfirm(), 0) || StringUtils.isEmpty(byId.getConfirmList())) { + log.error("当前通知未开启确认功能"); + return; + } + List noticeConfirms = JSONObject.parseArray(byId.getConfirmList(), NoticeConfirm.class); + if (CollUtil.isEmpty(noticeConfirms)) { + log.error("当前通知未获取到确认列表"); + return; + } + noticeConfirms.stream().filter(noticeConfirm -> StringUtils.equals(noticeConfirm.getUserId(), user.getUserId())).forEach(noticeConfirm -> { + noticeConfirm.setIsConfirmed(1); + noticeConfirm.setConfirmedTime(new Date()); + }); + byId.setConfirmList(JSON.toJSONString(noticeConfirms)); + updateById(byId); + + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + @Override + public NoticeConfirmListVo getNoticeConfirmList(String id) { + AttendanceNoticeEntity byId = getById(id); + Assert.isFalse(Objects.equals(byId.getIsConfirm(), 0) || StringUtils.isEmpty(byId.getConfirmList()), "当前通知未开启确认功能"); + List noticeConfirms = JSONObject.parseArray(byId.getConfirmList(), NoticeConfirm.class); + if (CollUtil.isEmpty(noticeConfirms)) { + return NoticeConfirmListVo.builder().build(); + } + List userIds = noticeConfirms.stream().map(NoticeConfirm::getUserId).collect(Collectors.toList()); + List allByIds = userAntifreeze.getAllByIds(userIds, UserProvider.getUser().getTenantId()); + Map collect = allByIds.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + List collect1 = noticeConfirms.stream().map(noticeConfirm -> { + PartUserInfoVo partUserInfoVo = collect.get(noticeConfirm.getUserId()); + return NoticeConfirmVo.builder() + .userId(noticeConfirm.getUserId()) + .userName(partUserInfoVo.getRealName()) + .head(UploaderUtil.uploaderImg(partUserInfoVo.getHeadIcon())) + .org(partUserInfoVo.getOrganizeName()) + .position(partUserInfoVo.getPositionName()) + .isConfirmed(noticeConfirm.getIsConfirmed()) + .confirmedTime(noticeConfirm.getConfirmedTime()) + .build(); + }).collect(Collectors.toList()); + List collect2 = collect1.stream().filter(noticeConfirmVo -> Objects.equals(noticeConfirmVo.getIsConfirmed(), 1)).collect(Collectors.toList()); + List collect3 = collect1.stream().filter(noticeConfirmVo -> Objects.equals(noticeConfirmVo.getIsConfirmed(), 0)).collect(Collectors.toList()); + return NoticeConfirmListVo.builder().confirmedList(collect2).notConfirmList(collect3).build(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendancePermissionDictServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendancePermissionDictServiceImpl.java new file mode 100644 index 0000000..574a7d0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendancePermissionDictServiceImpl.java @@ -0,0 +1,136 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import jnpf.attendance.mapper.PermissionDictMapper; +import jnpf.attendance.service.AttendancePermissionDictService; +import jnpf.entity.PermissionDict; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.SavePermissionDto; +import jnpf.model.attendance.vo.PermissionDictVo; +import jnpf.util.JsonUtil; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import jnpf.util.mapper.MybatisUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 考勤权限字典 + */ +@Service +public class AttendancePermissionDictServiceImpl implements AttendancePermissionDictService { + + @Resource + private PermissionDictMapper permissionDictMapper; + + @Override + public void save(SavePermissionDto savePermissionDto) throws HandleException { + PermissionDict byName = findByName(savePermissionDto.getName(), savePermissionDto.getModuleType()); + PermissionDict permissionDict = JsonUtil.getJsonToBean(savePermissionDto, PermissionDict.class); + if (savePermissionDto.getId() == null) { + //新增 + if (byName != null) { + throw new HandleException("该数据已存在"); + } + permissionDict.setId(RandomUtil.uuId()); + permissionDict.setCreatorTime(new Date()); + permissionDict.setCreatorUserId(UserProvider.getLoginUserId()); + permissionDictMapper.insert(permissionDict); + return; + } + if (byName != null && !byName.getId().equals(permissionDict.getId())) { + throw new HandleException("该数据已存在"); + } + permissionDict.setLastModifyTime(new Date()); + permissionDict.setLastModifyUserId(UserProvider.getLoginUserId()); + permissionDictMapper.updateById(permissionDict); + } + + @Override + public List queryPermissionDictList(Integer moduleType) { +// List permissionDictList = MybatisUtil.findListByFiled(permissionDictMapper, PermissionDict::getModuleType, moduleType, true); + QueryWrapper curQueryWrapper = new QueryWrapper<>(); + curQueryWrapper.lambda() + .eq(PermissionDict::getModuleType, 1); + List curPermissionList = permissionDictMapper.selectList(curQueryWrapper); + List curVoList = treeGroupVo(curPermissionList); + QueryWrapper childQueryWrapper = new QueryWrapper<>(); + childQueryWrapper.lambda() + .eq(PermissionDict::getModuleType, 2); + List childPermissionList = permissionDictMapper.selectList(childQueryWrapper); + List childVoList = treeGroupVo(childPermissionList); + curVoList.addAll(childVoList); + return curVoList; + } + + @Override + public void delete(String id) { + PermissionDict permissionDict = permissionDictMapper.selectById(id); + List deleteIds = new ArrayList<>(); + deleteIds.add(permissionDict.getId()); + recursionDeleteChild(permissionDict.getId(), deleteIds); + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda() + .in(PermissionDict::getId, deleteIds); + PermissionDict delete = new PermissionDict(); + delete.setDeleteMark(1); + permissionDictMapper.update(delete, updateWrapper); + } + + @Override + public PermissionDict detail(String id) { + return permissionDictMapper.selectById(id); + } + + private void recursionDeleteChild(String id, List deleteIds) { + List permissionDictList = MybatisUtil.findListByFiled(permissionDictMapper, PermissionDict::getParent, id, true); + if (CollectionUtil.isEmpty(permissionDictList)) { + return; + } + List deleteIdList = permissionDictList.stream().map(PermissionDict::getId).collect(Collectors.toList()); + deleteIds.addAll(deleteIdList); + for (PermissionDict permissionDict : permissionDictList) { + recursionDeleteChild(permissionDict.getId(), deleteIds); + } + } + + private List treeGroupVo(List permissionDictList) { + List allGroupList = JsonUtil.getJsonToList(permissionDictList, PermissionDictVo.class); + List groupVoList = allGroupList.stream().filter(group -> { + return group.getParent().equals("0"); + }).collect(Collectors.toList()); + return groupVoList.stream().peek(group -> group.setChild(recursiveQuery(group, allGroupList))).collect(Collectors.toList()); + } + + private List recursiveQuery(PermissionDictVo root, List permissionDictVoList) { + if (CollectionUtil.isEmpty(permissionDictVoList)) { + root.setChild(null); + } + List children = new ArrayList<>(); + permissionDictVoList.forEach(item -> { + if (Objects.equals(item.getParent(), root.getId())) { + item.setChild(recursiveQuery(item, permissionDictVoList)); + children.add(item); + } + }); + return children; + } + + public PermissionDict findByName(String name, Integer moduleType) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper. + eq("F_DeleteMark", 0) + .lambda() + .eq(PermissionDict::getName, name) + .eq(PermissionDict::getType, moduleType); + return permissionDictMapper.selectOne(queryWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateItemServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateItemServiceImpl.java new file mode 100644 index 0000000..1708511 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateItemServiceImpl.java @@ -0,0 +1,20 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceQuickTemplateItemMapper; +import jnpf.attendance.service.AttendanceQuickTemplateItemService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceQuickTemplateItemEntity; +import org.springframework.stereotype.Service; + +/** + *

+ * 快速模板-单天模板 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +@Service +public class AttendanceQuickTemplateItemServiceImpl extends SuperServiceImpl implements AttendanceQuickTemplateItemService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateServiceImpl.java new file mode 100644 index 0000000..90e355a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceQuickTemplateServiceImpl.java @@ -0,0 +1,288 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.attendance.mapper.AttendanceQuickTemplateMapper; +import jnpf.attendance.service.*; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceQuickTemplateEntity; +import jnpf.entity.attendance.AttendanceQuickTemplateItemEntity; +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.FixedClassShiftVo; +import jnpf.model.attendance.vo.attendance.QuickTemVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import jnpf.util.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + *

+ * 考勤配置-快速模板 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-28 + */ +@Service +public class AttendanceQuickTemplateServiceImpl extends SuperServiceImpl implements AttendanceQuickTemplateService { + @Autowired + private AttendanceQuickTemplateItemService attendanceQuickTemplateItemService; + @Autowired + private AttendanceShiftSettingPeriodService attendanceShiftSettingPeriodService; + @Autowired + private AttendanceQuickTemplateMapper attendanceQuickTemplateMapper; + @Autowired + private AttendanceShiftSettingService attendanceShiftSettingService; + @Autowired + private AttendanceShiftNameSettingService shiftNameSettingService; + + + @Override + public List findList(String groupId) { + List list = lambdaQuery().eq(AttendanceQuickTemplateEntity::getGroupId, groupId) + .eq(AttendanceQuickTemplateEntity::getDeleteMark, Boolean.FALSE) + .list(); + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + List ids = list.stream().map(AttendanceQuickTemplateEntity::getId).collect(Collectors.toList()); + List list1 = attendanceQuickTemplateItemService.list(new LambdaQueryWrapper() + .in(AttendanceQuickTemplateItemEntity::getTemplateId, ids)); + Map> itemMap = list1.stream().collect(Collectors.groupingBy(AttendanceQuickTemplateItemEntity::getTemplateId)); + + return list.stream().map(temp -> { + List periodIds = CollUtil.newArrayList(); + List attendanceQuickTemplateItemEntities = itemMap.get(temp.getId()); + List collect = attendanceQuickTemplateItemEntities.stream().map(item -> { + AttendanceQuickTemplateItemVo attendanceQuickTemplateItemVo = new AttendanceQuickTemplateItemVo(); + attendanceQuickTemplateItemVo.setId(item.getId()); + attendanceQuickTemplateItemVo.setSort(item.getSort()); + attendanceQuickTemplateItemVo.setTemplateId(item.getTemplateId()); + List quickPeriodsVos = JsonUtil.getJsonToList(item.getPeriods(), QuickPeriodsVo.class); + periodIds.addAll(quickPeriodsVos.stream().map(QuickPeriodsVo::getPeriodId).collect(Collectors.toList())); + attendanceQuickTemplateItemVo.setPeriods(quickPeriodsVos); + return attendanceQuickTemplateItemVo; + }).collect(Collectors.toList()); + List attendanceShiftSettingPeriodEntities = attendanceShiftSettingPeriodService.listByIds(periodIds.stream().distinct().collect(Collectors.toList())); + String periodStr = attendanceShiftSettingPeriodEntities.stream().map(period -> period.getInPoint() + "-" + (Objects.equals(period.getOutType(), 2) ? "次日" : "") + period.getOutPoint()).distinct().reduce((r1, r2) -> r1 + "," + r2).orElse(""); + AttendanceQuickTemplateVo attendanceQuickTemplateVo = BeanUtil.toBean(temp, AttendanceQuickTemplateVo.class); + attendanceQuickTemplateVo.setItems(collect); + attendanceQuickTemplateVo.setPeriodicity(collect.size()); + attendanceQuickTemplateVo.setPeriodStr(periodStr); + return attendanceQuickTemplateVo; + }).collect(Collectors.toList()); + } + + @Override + public AttendanceQuickTemplateVo findOne(String templateId) { + AttendanceQuickTemplateEntity one = lambdaQuery().eq(AttendanceQuickTemplateEntity::getId, templateId) + .eq(AttendanceQuickTemplateEntity::getDeleteMark, Boolean.FALSE) + .one(); + List list1 = attendanceQuickTemplateItemService.list(new LambdaQueryWrapper() + .eq(AttendanceQuickTemplateItemEntity::getTemplateId, templateId)); + List itemVos = list1.stream().map(item -> { + AttendanceQuickTemplateItemVo attendanceQuickTemplateItemVo = new AttendanceQuickTemplateItemVo(); + attendanceQuickTemplateItemVo.setId(item.getId()); + attendanceQuickTemplateItemVo.setSort(item.getSort()); + attendanceQuickTemplateItemVo.setTemplateId(item.getTemplateId()); + List quickPeriodsVos = JsonUtil.getJsonToList(item.getPeriods(), QuickPeriodsVo.class); + attendanceQuickTemplateItemVo.setPeriods(quickPeriodsVos); + return attendanceQuickTemplateItemVo; + }).collect(Collectors.toList()); + AttendanceQuickTemplateVo attendanceQuickTemplateVo = BeanUtil.toBean(one, AttendanceQuickTemplateVo.class); + attendanceQuickTemplateVo.setItems(itemVos); + attendanceQuickTemplateVo.setPeriodicity(itemVos.size()); + return attendanceQuickTemplateVo; + } + + @Override + public void save(QuickTemplateDto dto) { + List list = lambdaQuery().eq(AttendanceQuickTemplateEntity::getName, dto.getName()) + .eq(AttendanceQuickTemplateEntity::getGroupId, dto.getGroupId()) + .eq(AttendanceQuickTemplateEntity::getDeleteMark, Boolean.FALSE) + .list(); + Assert.isFalse(CollUtil.isNotEmpty(list), "模板名称不能重复"); + List periodIds = CollUtil.newArrayList(); + List items1 = dto.getItems(); + items1.forEach(item -> { + if (Objects.nonNull(item)) { + periodIds.addAll(item.getPeriodIds()); + } + }); + List attendanceShiftSettingPeriodEntities = CollUtil.isEmpty(periodIds) ? CollUtil.newArrayList() : attendanceShiftSettingPeriodService.listByIdsIgnoreLogic(periodIds); + Map periodMap = attendanceShiftSettingPeriodEntities.stream().collect(Collectors.toMap(AttendanceShiftSettingPeriodEntity::getId, period -> period)); + AttendanceQuickTemplateEntity build = AttendanceQuickTemplateEntity.builder().id(dto.getId()).name(dto.getName()).build(); + UserInfo user = UserProvider.getUser(); + if (StringUtil.isNotEmpty(build.getId())) { + build.setLastModifyTime(new Date()); + build.setLastModifyUserId(user.getUserId()); + } else { + build.setId(RandomUtil.uuId()); + build.setCreatorTime(new Date()); + build.setCreatorUserId(user.getUserId()); + build.setTenantId(user.getTenantId()); + build.setGroupId(dto.getGroupId()); + } + int sort = 0; + List itemEntities = CollUtil.newArrayList(); + List items = dto.getItems(); + for (SchedulesSetItemDto item : items) { + List collect = CollUtil.newArrayList(); + if (item.getType() == 2) { + List collect1 = item.getPeriodIds().stream().map(periodId -> { + AttendanceShiftSettingPeriodEntity periodEntity = periodMap.get(periodId); + return QuickPeriodsVo.builder() + .periodId(periodId) + .sort(periodEntity.getSort()) + .name(periodEntity.getInPoint() + "-" + (periodEntity.getOutType() == 1 ? periodEntity.getOutPoint() : "次日" + periodEntity.getOutPoint())) + .type(item.getType()).build(); + }).collect(Collectors.toList()); + collect.addAll(collect1); + } else { + collect.add(QuickPeriodsVo.builder().type(item.getType()).build()); + } + itemEntities.add(AttendanceQuickTemplateItemEntity.builder().templateId(build.getId()).sort(sort++).periods(JSON.toJSONString(collect)).build()); + } + attendanceQuickTemplateItemService.remove(new LambdaUpdateWrapper().eq(AttendanceQuickTemplateItemEntity::getTemplateId, build.getId())); + attendanceQuickTemplateItemService.saveBatch(itemEntities); + saveOrUpdate(build); + } + + @Override + public void del(String id) { + lambdaUpdate().set(AttendanceQuickTemplateEntity::getDeleteMark, Boolean.TRUE) + .set(AttendanceQuickTemplateEntity::getDeleteUserId, UserProvider.getLoginUserId()) + .set(AttendanceQuickTemplateEntity::getDeleteTime, new Date()) + .eq(AttendanceQuickTemplateEntity::getId, id) + .update(); + // 删除之前绑定的班次信息 + attendanceQuickTemplateMapper.deleteClass(id); + } + + @Transactional + @Override + public void saveOrUpdateNew(QuickTemDto dto) { + List list1 = lambdaQuery().eq(AttendanceQuickTemplateEntity::getName, dto.getName()) + .eq(AttendanceQuickTemplateEntity::getGroupId, dto.getGroupId()) + .eq(AttendanceQuickTemplateEntity::getDeleteMark, Boolean.FALSE) + .list(); + Assert.isFalse(CollUtil.isNotEmpty(list1) && list1.stream().noneMatch(vo -> Objects.equals(vo.getId(), dto.getId())), "模板名称不能重复"); + String userId = UserProvider.getUser().getUserId(); + AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findByGroupId(dto.getGroupId()); + List schedulingPeriods = byGroupId.getSchedulingPeriods(); + Map collect = schedulingPeriods.stream().collect(Collectors.toMap(ShiftNameVo::getId, Function.identity())); + Date date = new Date(); + List dateList = CollUtil.newArrayList(); + if (null != dto.getId() && !dto.getId().isEmpty()) { + // 修改快速模板 + attendanceQuickTemplateMapper.updateTemplate(dto, userId); + // 删除之前绑定的班次信息 + attendanceQuickTemplateMapper.deleteClass(dto.getId()); + } else { + dto.setId(FtbUtil.getId()); + //新增快速模板 + attendanceQuickTemplateMapper.saveTemplate(dto, userId); + } + if (null != dto.getFixedClassDtos() && !dto.getFixedClassDtos().isEmpty()) { + List list = new ArrayList<>(); + int index = 0; + for (FixedClassDto v : dto.getFixedClassDtos()) { + // 生成偏移日期:按索引递增天数偏移基准日期 + DateTime dateTime = cn.hutool.core.date.DateUtil.offsetDay(date, ++index); + if (CollUtil.isEmpty(v.getFixedBases())) { + FixedClassSaveDto fixedClassSaveDto = new FixedClassSaveDto(FtbUtil.getId(), v.getNum(), v.getShiftType(), dto.getId(), 0, null, 1); + list.add(fixedClassSaveDto); + } else { + v.getFixedBases().forEach(value -> { + // 模板Id赋值到组Id做替换 + FixedClassSaveDto fixedClassSaveDto = new FixedClassSaveDto(FtbUtil.getId(), v.getNum(), v.getShiftType(), dto.getId(), value.getType(), value.getShiftNameId(), value.getSort()); + list.add(fixedClassSaveDto); + }); + } + for (FixedBaseDto fixedClassSaveDto : v.getFixedBases()) { + if (StringUtil.isNotEmpty(fixedClassSaveDto.getShiftNameId()) && collect.containsKey(fixedClassSaveDto.getShiftNameId())) { + ShiftNameVo shiftNameVo = collect.get(fixedClassSaveDto.getShiftNameId()); + if (Objects.equals(v.getShiftType(), 0)) { + for (AttendanceShiftSettingPeriodVo periodVo : shiftNameVo.getPeriods()) { + Assert.isTrue(shiftNameSettingService.fixedPeriodSingleConflict(dateTime, dateList, periodVo), "班次时间段冲突"); + } + } else { + Assert.isTrue(shiftNameSettingService.fixedPeriodSingleConflict(dateTime, dateList, shiftNameVo.getPeriods().get(shiftNameVo.getPeriods().size() < 2 ? 0 : fixedClassSaveDto.getSort() - 1)), "班次时间段冲突"); + } + } + } + } + if (!list.isEmpty()) { + // 保存本次传入的内容 + attendanceQuickTemplateMapper.saveClass(list); + } + } + } + + @Override + public List findNewList(String groupId) { + List quickTemVos = attendanceQuickTemplateMapper.getTemByGroupId(groupId); + if (null != quickTemVos && !quickTemVos.isEmpty()) { + // 考勤组下所有模板 + List temIds = quickTemVos.stream().map(QuickTemVo::getId).collect(Collectors.toList()); + // 模板下拼接下面的班次信息 + List shiftNameVos = attendanceQuickTemplateMapper.getShiftByTemIds(temIds); + if (null != shiftNameVos && !shiftNameVos.isEmpty()) { + List workMap = shiftNameVos.stream().filter(t -> 2 == t.getType()).collect(Collectors.toList()); + // 组装时间段 + List shiftIds = workMap.stream().distinct().map(ShiftNameVo::getId).collect(Collectors.toList()); + if (!shiftIds.isEmpty()) { + List list = attendanceShiftSettingPeriodService.list(new LambdaQueryWrapper() + .in(AttendanceShiftSettingPeriodEntity::getShiftId, shiftIds) + .eq(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.FALSE)); + Map> shiftMap = list.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodEntity::getShiftId)); + workMap.forEach(v -> { + // 赋值时段信息 + List attendanceShiftSettingPeriodEntities = shiftMap.get(v.getId()); + if (null != attendanceShiftSettingPeriodEntities && !attendanceShiftSettingPeriodEntities.isEmpty()) { + // 排序 + List shiftList = attendanceShiftSettingPeriodEntities.stream().sorted(Comparator.comparing(AttendanceShiftSettingPeriodEntity::getType)).collect(Collectors.toList()); + List periodEntities = BeanUtil.copyToList(shiftList, AttendanceShiftSettingPeriodVo.class); + v.setPeriods(periodEntities); + } + }); + } + // 组装数据 + Map> temMap = shiftNameVos.stream().collect(Collectors.groupingBy(ShiftNameVo::getTemplateId)); + quickTemVos.forEach(v -> { + // 指定模板下班次信息 + List shiftList = temMap.get(v.getId()); + if (null != shiftList && !shiftList.isEmpty()) { + List fixedList = new ArrayList<>(); + Map> numMap = shiftList.stream().collect(Collectors.groupingBy(ShiftNameVo::getNum)); + int cycle = 0; + for (Map.Entry> integerListEntry : numMap.entrySet()) { + FixedClassShiftVo fixedClassShiftVo = new FixedClassShiftVo(); + fixedClassShiftVo.setNum(integerListEntry.getKey()); + fixedClassShiftVo.setFixedType(integerListEntry.getValue().get(0).getFixedType()); + fixedClassShiftVo.setShiftNameVos(integerListEntry.getValue()); + fixedList.add(fixedClassShiftVo); + cycle = cycle + 1; + } + v.setCycle(cycle); + v.setList(fixedList.stream().sorted(Comparator.comparing(FixedClassShiftVo::getNum)).collect(Collectors.toList())); + } + }); + } + } + return quickTemVos; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceRepairServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceRepairServiceImpl.java new file mode 100644 index 0000000..a9d19fe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceRepairServiceImpl.java @@ -0,0 +1,46 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.mapper.AttendanceRepairMapper; +import jnpf.attendance.service.AttendanceRepairService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceRepair; +import jnpf.util.DateDetail; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; + +/** + * 补卡次数服务实现 + * + * @author yanwenfu + * @create 2024-07-03 + */ +@Service +public class AttendanceRepairServiceImpl extends SuperServiceImpl implements AttendanceRepairService { + + @Resource + private AttendanceRepairMapper attendanceRepairMapper; + + @Override + public AttendanceRepair getAttendanceRepair(String userId, String groupId) { + + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(AttendanceRepair::getGroupId, groupId); + if (StringUtils.isNotEmpty(userId)) { + query.eq(AttendanceRepair::getUserId, userId); + } + query.gt(AttendanceRepair::getExpireDate, DateDetail.getDate2Str(new Date(), DateDetail.DF)) + .orderByDesc(AttendanceRepair::getCreatorTime) + .last("limit 1"); + return this.getOne(query); + } + + @Override + public AttendanceRepair getAttendanceRepairByDate(String applyDate, String userId, String groupId) { + + return attendanceRepairMapper.getAttendanceRepairByDate(applyDate, userId, groupId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSealSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSealSettingServiceImpl.java new file mode 100644 index 0000000..1d2e920 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSealSettingServiceImpl.java @@ -0,0 +1,175 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.mapper.AttendanceSealSettingMapper; +import jnpf.attendance.service.*; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.RedisConstant; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.entity.attendance.AttendanceDayStatistics; +import jnpf.entity.attendance.AttendanceSealSetting; +import jnpf.model.attendance.dto.MonthAutoSealSettingDto; +import jnpf.model.attendance.vo.attendance.MonthAutoSealSettingVo; +import jnpf.util.UserProvider; +import jnpf.util.attendance.DayStatisticsUtils; +import jnpf.util.context.ThreadContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class AttendanceSealSettingServiceImpl extends SuperServiceImpl implements AttendanceSealSettingService { + @Autowired + private StringRedisTemplate redisUtil; + @Resource + private AttendanceGroupService groupService; + @Resource(name = "ioIntensiveThreadPool") + private ThreadPoolExecutor threadPoolExecutor; + @Resource + private PublicHolidayRulesService holidayRulesService; + @Resource + private AttendanceBaseSettingService baseSettingService; + @Resource + private AttendanceDayStatisticsService dayStatisticsService; + + @Override + public MonthAutoSealSettingVo getAutoSealSettingInfo() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(AttendanceSealSetting::getDeleteMark, 0); + queryWrapper.last("limit 1"); + AttendanceSealSetting sealSetting = this.getOne(queryWrapper); + return Objects.isNull(sealSetting) ? null : MonthAutoSealSettingVo.builder() + .id(sealSetting.getId()) + .autoSeal(sealSetting.getAutoSeal()) + .sealDay(sealSetting.getSealDay()) + .sealTime(sealSetting.getSealTime()) + .build(); + } + + @Override + public Boolean autoSealSetting(MonthAutoSealSettingDto dto) { + AttendanceSealSetting sealSetting = this.getById(dto.getId()); + Assert.notNull(sealSetting, "数据不存在"); + sealSetting.setAutoSeal(dto.getAutoSeal()); + sealSetting.setSealDay(dto.getSealDay()); + sealSetting.setSealTime(dto.getSealTime()); + this.updateById(sealSetting); + //检测是否已存在晚走晚到 + String sealSettingKey = String.format(RedisConstant.ATTENDANCE_SEAL_SETTING, UserProvider.getUser().getTenantId()); + redisUtil.opsForValue().set(sealSettingKey, JSON.toJSONString(dto)); + return Boolean.TRUE; + } + + @Override + @Transactional + public Boolean autoSealTimer(String tenantId) { + // 从缓存获取自动封账设置 + String sealSettingKey = String.format(RedisConstant.ATTENDANCE_SEAL_SETTING, tenantId); + MonthAutoSealSettingVo sealSettingVo; + if (!Boolean.TRUE.equals(redisUtil.hasKey(sealSettingKey))) { + sealSettingVo = this.getAutoSealSettingInfo(); + redisUtil.opsForValue().set(sealSettingKey, JSON.toJSONString(sealSettingVo)); + return Boolean.TRUE; + }else{ + sealSettingVo = JSON.parseObject(redisUtil.opsForValue().get(sealSettingKey), MonthAutoSealSettingVo.class); + } + // 校验是否开启自动封账 + if (Objects.isNull(sealSettingVo) || sealSettingVo.getAutoSeal().equals(0)) { + return Boolean.TRUE; + } + Integer sealDay = sealSettingVo.getSealDay(); + Date dateNew = new Date(); + // 判断当前时间点是否到了封账时间点 + if (!DayStatisticsUtils.isCurrentTimeEqualsSealTime(sealSettingVo.getSealTime())) { + return Boolean.TRUE; + } + Date date = DateUtil.beginOfDay(dateNew); + int day = DateUtil.dayOfMonth(date); + if (!Objects.equals(sealDay, day)) { + return Boolean.TRUE; + } + date = DateUtil.offsetMonth(date, -1); + int month = DateUtil.month(date) + 1; + int year = DateUtil.year(date); + //生成封账数据 + createSeal(year, month); + return Boolean.TRUE; + } + + /** + * 生成封账数据 + * + * @param year 年份 + * @param month 月份 + */ + private void createSeal(int year, int month) { + // 创建 YearMonth 对象 + YearMonth yearMonth = YearMonth.of(year, month); + // 获取该月的第一天 + LocalDate startDate = yearMonth.atDay(1); + // 获取该月的最后一天 + LocalDate endDate = yearMonth.atEndOfMonth(); + //数据量非常大,分批执行数据修改 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(AttendanceDayStatistics::getId, AttendanceDayStatistics::getUserId); + queryWrapper.between(AttendanceDayStatistics::getDate, startDate, endDate); + List dayStatisticsList = dayStatisticsService.list(queryWrapper); + // 如果idList为空,则直接返回 + if (CollUtil.isEmpty(dayStatisticsList)) { + return; + } + List idList = dayStatisticsList.stream().map(AttendanceDayStatistics::getId).collect(Collectors.toList()); + List userIdList = dayStatisticsList.stream().map(AttendanceDayStatistics::getUserId).distinct().collect(Collectors.toList()); + //数据量过大分批执行修改操作 + List> batches = DayStatisticsUtils.partition(idList, 1000); + for (List batch : batches) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(AttendanceDayStatistics::getId, batch); + updateWrapper.set(AttendanceDayStatistics::getSeal, 1); + dayStatisticsService.update(updateWrapper); + } + // 获取用户出勤换算比 + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + try{ + List attendanceUserGroupList = groupService.getAttendanceUserGroup(userIdList); + List groupIds = CollUtil.isNotEmpty(attendanceUserGroupList) ? + attendanceUserGroupList.stream().map(AttendanceUserGroupVo::getGroupId).distinct() + .collect(Collectors.toList()) : CollUtil.newArrayList(); + Map groupBaseSettingMap = baseSettingService.getEnableBaseSettingAll(groupIds); + Map ratioMap = new HashMap<>(); + if (CollUtil.isNotEmpty(attendanceUserGroupList)) { + attendanceUserGroupList.forEach(item -> { + if (CollUtil.isEmpty(groupBaseSettingMap) || !groupBaseSettingMap.containsKey(item.getGroupId()) || + Objects.isNull(groupBaseSettingMap.get(item.getGroupId())) || Objects.isNull(groupBaseSettingMap.get(item.getGroupId()).getAttendanceRatio())) { + return; + } + ratioMap.put(item.getUserId(), groupBaseSettingMap.get(item.getGroupId()).getAttendanceRatio()); + }); + } + holidayRulesService.processPublicHoliday(yearMonth.toString(), userIdList, ratioMap); + }catch (Exception e) { + log.error("获取用户出勤换算比失败", e); + } + }), threadPoolExecutor); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftNameSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftNameSettingServiceImpl.java new file mode 100644 index 0000000..db83ccf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftNameSettingServiceImpl.java @@ -0,0 +1,589 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import jnpf.attendance.mapper.AttendanceFixedClassMapper; +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.mapper.AttendanceQuickTemplateMapper; +import jnpf.attendance.mapper.AttendanceShiftNameSettingMapper; +import jnpf.attendance.service.*; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.attendance.AttendanceFixedClassEntity; +import jnpf.entity.attendance.AttendanceShiftNameEntity; +import jnpf.entity.attendance.AttendanceShiftSettingEntity; +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.enums.attendance.CautionEnum; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.vo.AttendanceShiftSettingPeriodVo; +import jnpf.model.attendance.vo.AttendanceShiftSettingVo; +import jnpf.model.attendance.vo.SecondmentDateVo; +import jnpf.model.attendance.vo.attendance.FixedClassShiftVo; +import jnpf.model.attendance.vo.attendance.QuickTemVo; +import jnpf.model.attendance.vo.attendance.ShiftNameListVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import jnpf.util.*; +import jnpf.util.context.ThreadContext; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static jnpf.constants.RedisConstant.*; + +/** + * @Author huanglinpan + * @Date 2024/5/9 10:06 + * @Version 1.0 (版本号) + */ +@Service +public class AttendanceShiftNameSettingServiceImpl extends SuperServiceImpl implements AttendanceShiftNameSettingService { + + + @Resource + private ThreadPoolExecutor attendanceRuleThreadPool; + @Resource + private ThreadPoolExecutor cpuIntensiveThreadPool; + @Resource + private AttendanceShiftNameSettingMapper attendanceShiftNameSettingMapper; + @Autowired + private AttendanceShiftSettingPeriodService attendanceShiftSettingPeriodService; + @Resource + private AttendanceFixedClassMapper attendanceFixedClassMapper; + @Autowired + private AttendanceShiftSettingService attendanceShiftSettingService; + @Autowired + private AttendanceGroupService attendeesGroupService; + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Autowired + private RedissonClient redissonClient; + @Autowired + private AttendanceFixedClassService attendanceFixedClassService; + @Autowired + private AttendanceQuickTemplateService attendanceQuickTemplateService; + @Resource + private AttendanceQuickTemplateMapper attendanceQuickTemplateMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public ActionResult save(ShiftNameDto dto) { + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_UPDATE_SHIFT_CONFIG, UserProvider.getUser().getTenantId(), dto.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组班次信息正在变更中,请稍后再试"); + try { + Assert.isFalse(!lock.tryLock(5, 5, TimeUnit.SECONDS), "当前考勤组班次信息正在变更中,请稍后再试"); + UserInfo user = UserProvider.getUser(); + dto.setUserId(user.getUserId()); + AttendanceShiftSettingVo allByGroupId = attendanceShiftSettingService.findAllByGroupId(dto.getGroupId()); + if (Objects.isNull(allByGroupId)) { + attendanceShiftSettingService.save(AttendanceShiftSettingEntity.builder().groupId(dto.getGroupId()).systemType(2).enable(1).id(RandomUtil.uuId()).enableTime(new Date()).creatorTime(new Date()).creatorUserId(user.getUserId()).build()); + } + // 校验名称不能重复 + Integer num = attendanceShiftNameSettingMapper.checkName(dto); + Assert.isFalse(num > 0, "班次名称不能重复"); + // 设置班制:1 全天制 2半天制 + dto.setShiftType((null == dto.getPeriods() || dto.getPeriods().size() < 2) ? 1 : 2); + if (null == dto.getId() || dto.getId().isEmpty()) { + // 新增班次名称信息 + dto.setId(FtbUtil.getId()); + attendanceShiftNameSettingMapper.save(dto); + } else { + // 保存班次名称信息 + attendanceShiftNameSettingMapper.update(dto); + // 先删除原来的关联班次时段信息 + attendanceShiftNameSettingMapper.deletePeriodByShiftId(dto.getId()); + } + if (CollUtil.isEmpty(dto.getPeriods())) { + return ActionResult.success(); + } + // 保存时段信息 + List periodEntities = BeanUtil.copyToList(dto.getPeriods(), AttendanceShiftSettingPeriodEntity.class); + // 时段交叉校验 9:00 -- 18:00 10:00 -- 19:00 + Assert.isTrue(fixedPeriodConflict(periodEntities), "班次时段存在交叉"); + periodEntities.forEach(v -> { + v.setShiftId(dto.getId()); + v.setLateEnable(1); + }); + if (periodEntities.size() > 1) { + AttendanceShiftSettingPeriodEntity oneEntity = periodEntities.get(0); + AttendanceShiftSettingPeriodEntity twoEntity = periodEntities.get(1); + // 上班开始时间在前面的排第一 + Date date = new Date(); + Date oneTime = DateDetail.getDateByPoint(date, oneEntity.getInPoint(), oneEntity.getInType()); + Date twoTime = DateDetail.getDateByPoint(date, twoEntity.getInPoint(), twoEntity.getInType()); + if (oneTime != null) { + oneEntity.setType(oneTime.before(twoTime) ? 1 : 2); + } + if (twoTime != null) { + twoEntity.setType(twoTime.before(oneTime) ? 1 : 2); + } + Assert.isFalse(check24H(periodEntities), "下班时间不可超过上班时间的24小时"); + } + // 保存 + attendanceShiftSettingPeriodService.saveOrUpdateBatch(periodEntities); + Boolean isConflict = fixedConfigPeriodConflict(dto.getGroupId(), dto.getId()); + // 固定班次排班冲突校验 + List enableGroupIds = attendeesGroupService.getEnableGroupIds(dto.getGroupId()); + enableGroupIds.remove(dto.getGroupId()); + try { + Integer result = attendanceDailyRuleService.setSchedulesForShiftConfigUpdate(dto.getGroupId(), dto.getMark(), periodEntities.stream().map(period -> PeriodConfig.builder().shiftId(period.getShiftId()).type(Objects.isNull(period.getType()) ? 0 : period.getType()).build()).collect(Collectors.toList()), periodEntities); + if (Objects.nonNull(allByGroupId) && Objects.equals(allByGroupId.getSystemType(), 1) && !isConflict) { + result = CautionEnum.Caution_202.getCode(); + } + adjustmentOfSubordinateScheduling(enableGroupIds, dto, periodEntities); + return ActionResult.success(result); + } catch (HandleException e) { + throw new RuntimeException("变更排班规则失败!"); + } + + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (lock.isLocked()) { + lock.unlock(); + } + } + } + + private void adjustmentOfSubordinateScheduling(List enableGroupIds, ShiftNameDto dto, List periodEntities) { + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + + for (String groupId : enableGroupIds) { + try { + attendanceDailyRuleService.setSchedulesForShiftConfigUpdate(groupId, dto.getMark(), periodEntities.stream().map(period -> PeriodConfig.builder().shiftId(period.getShiftId()).type(Objects.isNull(period.getType()) ? 0 : period.getType()).build()).collect(Collectors.toList()), periodEntities); + } catch (HandleException e) { + throw new RuntimeException(e); + } + } + // 快捷班次排班冲突校验 + quickTemplatePeriodConflict(dto.getGroupId(), dto.getId()); + for (String groupId : enableGroupIds) { + quickTemplatePeriodConflict(groupId, dto.getId()); + } + }), attendanceRuleThreadPool); + } + + private Boolean check24H(List periodEntities) { + // 一个时段的时候不可能超过24小时不做校验 校验上班时间不超过24小时 规则:第二个时段的下班时间有没有超过第一个上班时间的24小时 并校验 下班时间不可超过上班时间的24小时! + // 9:00 -- 18:00 19:00 -- 次日 9:59 + Date date = new Date(); + AttendanceShiftSettingPeriodEntity upEntity = periodEntities.stream().filter(entity -> 1 == entity.getType()).findFirst().orElse(null); + AttendanceShiftSettingPeriodEntity downEntity = periodEntities.stream().filter(entity -> 2 == entity.getType()).findFirst().orElse(null); + Date inPoint = DateDetail.getDateByPoint(date, Objects.requireNonNull(upEntity).getInPoint(), upEntity.getOutType()); + Date outPoint = DateDetail.getDateByPoint(date, Objects.requireNonNull(downEntity).getOutPoint(), downEntity.getOutType()); + if (Objects.requireNonNull(outPoint).getTime() - Objects.requireNonNull(inPoint).getTime() > 86400000L) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + + private void quickTemplatePeriodConflict(String groupId, String shiftId) { + List quickTemVos = attendanceQuickTemplateService.findNewList(groupId); + if (CollUtil.isEmpty(quickTemVos)) { + return; + } + List list = CollUtil.newArrayList(); + for (QuickTemVo quickTemVo : quickTemVos) { + Date date = new Date(); + int index = 0; + if (CollUtil.isEmpty(quickTemVo.getList())) { + continue; + } + for (FixedClassShiftVo fixedClassShiftVo : quickTemVo.getList()) { + List shiftNameVos = fixedClassShiftVo.getShiftNameVos(); + DateTime dateTime = cn.hutool.core.date.DateUtil.offsetDay(date, ++index); + boolean isConflictItem = shiftNameVos.stream().anyMatch(shiftNameVo -> Objects.isNull(shiftNameVo) || Objects.isNull(shiftNameVo.getPeriods()) ? Boolean.FALSE : shiftNameVo.getPeriods().stream().anyMatch(period -> !fixedPeriodSingleConflict(dateTime, list, period))); + if (isConflictItem) { + attendanceQuickTemplateMapper.deleteTemplateItem(quickTemVo.getId(), shiftId, List.of(index, index - 1)); + } + } + } + } + + /** + * 冲突清除固定班配置数据 + *

根据考勤组配置检查固定班次排班周期冲突,自动清理无冲突的排班记录

+ * + * @param groupId 考勤组ID(用于关联考勤班次配置) + * @return Boolean 冲突状态检查结果: + * TRUE-存在冲突或配置为空(默认状态) + * FALSE-检测到无冲突配置并执行了清理操作 + */ + private Boolean fixedConfigPeriodConflict(String groupId, String shiftId) { + // 获取考勤班次配置基础数据 + AttendanceShiftSettingVo setting = attendanceShiftSettingService.findByGroupId(groupId); + if (setting == null || CollUtil.isEmpty(setting.getFixedPeriods()) || StringUtil.isEmpty(shiftId)) { + return Boolean.TRUE; + } + List fixedPeriods = setting.getFixedPeriods(); + + // 初始化排班日期比对容器 + List list = CollUtil.newArrayList(); + Date date = new Date(); + int index = 0; + boolean isConflict = Boolean.TRUE; + + /* 遍历处理每个固定周期配置 */ + for (FixedClassShiftVo period : fixedPeriods) { + // 生成偏移日期:按索引递增天数偏移基准日期 + DateTime dateTime = cn.hutool.core.date.DateUtil.offsetDay(date, ++index); + + /* 冲突检测逻辑: + 1. 获取当前周期的所有班次配置 + 2. 检查是否存在与历史排班冲突的班次 + 3. 有冲突班次时执行清理操作 */ + List shiftNameVos = period.getShiftNameVos(); + boolean isConflictItem = shiftNameVos.stream().anyMatch( + shiftNameVo -> Objects.isNull(shiftNameVo) || Objects.isNull(shiftNameVo.getPeriods()) ? Boolean.FALSE : shiftNameVo.getPeriods().stream().anyMatch(period1 -> !fixedPeriodSingleConflict(dateTime, list, period1))); + + // 有冲突时清理对应排班记录 + if (isConflictItem) { + attendanceFixedClassService.update(new LambdaUpdateWrapper() + .set(AttendanceFixedClassEntity::getShiftNameId, null) + .set(AttendanceFixedClassEntity::getType, 0) + .in(AttendanceFixedClassEntity::getNum, index, index - 1) + .eq(AttendanceFixedClassEntity::getShiftNameId, shiftId) + .eq(AttendanceFixedClassEntity::getGroupId, groupId)); + isConflict = Boolean.FALSE; + } + } + return isConflict; + } + + + private Boolean fixedPeriodConflict(List periodEntities) { + + List list = CollUtil.newArrayList(); + for (AttendanceShiftSettingPeriodEntity period : periodEntities) { + if (!fixedPeriodSingleConflict(list, period)) { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + } + + @Override + public Boolean fixedPeriodSingleConflict(Date date, List list, AttendanceShiftSettingPeriodVo period) { + Date inPoint = DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType()); + Date outPoint = DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType()); + if (CollUtil.isEmpty(list)) { + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + for (SecondmentDateVo vo : list) { + if (DateDetail.checkTimeBetween(inPoint, vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(Objects.requireNonNull(outPoint), vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(vo.getStartTime(), inPoint, outPoint) || DateDetail.checkTimeBetween(vo.getEndTime(), inPoint, outPoint)) { + return Boolean.FALSE; + } + } + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + + private Boolean fixedPeriodSingleConflict(List list, AttendanceShiftSettingPeriodEntity period) { + Date date = new Date(); + Date inPoint = DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType()); + Date outPoint = DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType()); + if (CollUtil.isEmpty(list)) { + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + for (SecondmentDateVo vo : list) { + if (DateDetail.checkTimeBetween(inPoint, vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(Objects.requireNonNull(outPoint), vo.getStartTime(), vo.getEndTime()) || DateDetail.checkTimeBetween(vo.getStartTime(), inPoint, outPoint) || DateDetail.checkTimeBetween(vo.getEndTime(), inPoint, outPoint)) { + return Boolean.FALSE; + } + } + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + + /** + * 启停班次 + * + * @param shiftNameId 班次名称Id + * @param enable 状态(1启用 2停用) + * @param updateShift 是否修改之前班次(1 : 保持之前班次 2:修改之前的班次) + */ + @Override + public ActionResult changeStatus(String shiftNameId, Integer enable, Integer updateShift) { + AttendanceShiftNameEntity byId = getById(shiftNameId); + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_CHANGE_SHIFT_STATUS, UserProvider.getUser().getTenantId(), byId.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组启停班次操作正在执行中,请稍后再试"); + try { + if(lock.tryLock(10, 60, TimeUnit.SECONDS)){ + + } + String userId = UserProvider.getUser().getUserId(); + // 修改班次的启停标识 + attendanceShiftNameSettingMapper.updateEnable(shiftNameId, enable, userId); + //固定班变更 + if (1 == enable && 2 == updateShift) { + fixedPeriodChange(byId.getGroupId()); + } + return ActionResult.success(); + } catch (InterruptedException e) { + return ActionResult.fail("当前考勤组启停班次操作正在执行中,请稍后再试"); + } finally { + if(lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private void fixedPeriodChange(String groupId) { + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + // 变更现有排班规则 + attendanceDailyRuleService.fixedPeriodChange(attendeesGroupService.getEnableGroupIds(groupId), attendanceShiftSettingService.findByGroupId(groupId), DateUtil.getDayBegin()); + }), cpuIntensiveThreadPool); + } + + @Override + public ActionResult delete(String shiftNameId) { + String userId = UserProvider.getUser().getUserId(); + // 校验当前班次是否正在使用 快速模板 固定排班 用户排班 + Integer fixedNum = attendanceFixedClassMapper.getUseBYShiftNameId(shiftNameId); + if (fixedNum > 0) { + return ActionResult.fail("该班次正在被固定排班使用不可删除!"); + } + Integer ruleNum = attendanceFixedClassMapper.getRuleUseBYShiftNameId(shiftNameId); + if (ruleNum > 0) { + return ActionResult.fail("该班次正在被排班使用不可删除!"); + } + Integer quickNum = attendanceFixedClassMapper.getQuickUseBYShiftNameId(shiftNameId); + if (quickNum > 0) { + return ActionResult.fail("该班次正在被快速排班使用不可删除!"); + } + attendanceShiftNameSettingMapper.delete(shiftNameId, userId); + return ActionResult.success(); + } + + @Override + public ShiftNameVo getDetail(String shiftNameId) { + ShiftNameVo shiftNameVo = attendanceShiftNameSettingMapper.getDetail(shiftNameId); + // 组装时间段 + List list = attendanceShiftSettingPeriodService.list(new LambdaQueryWrapper().in(AttendanceShiftSettingPeriodEntity::getShiftId, shiftNameId).eq(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.FALSE)); + List periodEntities = BeanUtil.copyToList(list, AttendanceShiftSettingPeriodVo.class); + shiftNameVo.setPeriods(periodEntities); + return shiftNameVo; + } + + @Override + public Map getDetailList(List shiftIds) { + Map map = new HashMap<>(); + List shiftNameVos = this.getBaseMapper().selectBatchIds(shiftIds); + if (CollUtil.isEmpty(shiftNameVos)) { + return map; + } + List shiftNameVoList = BeanUtil.copyToList(shiftNameVos, ShiftNameVo.class); + Map shiftNameVoMap = shiftNameVoList.stream().collect(Collectors.toMap(ShiftNameVo::getId, t -> t)); + return shiftIds.stream().map(shiftNameVoMap::get).filter(Objects::nonNull).collect(Collectors.toMap(ShiftNameVo::getId, t -> t)); + } + + @Override + public List getFixedClass(String groupId) { + List fixedList = new ArrayList<>(); + List shiftNameVos = attendanceShiftNameSettingMapper.getFixedClass(groupId); + if (null != shiftNameVos && !shiftNameVos.isEmpty()) { + List workMap = shiftNameVos.stream().filter(t -> 2 == t.getType()).collect(Collectors.toList()); + // 组装时间段 + List shiftIds = workMap.stream().distinct().map(ShiftNameVo::getId).collect(Collectors.toList()); + List list = attendanceShiftSettingPeriodService.list(new LambdaQueryWrapper().in(AttendanceShiftSettingPeriodEntity::getShiftId, shiftIds).eq(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.FALSE)); + Map> shiftMap = list.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodEntity::getShiftId)); + workMap.forEach(v -> { + List attendanceShiftSettingPeriodEntities = shiftMap.get(v.getId()); + if (null != attendanceShiftSettingPeriodEntities && !attendanceShiftSettingPeriodEntities.isEmpty()) { + // 排序 + List shiftList = attendanceShiftSettingPeriodEntities.stream().sorted(Comparator.comparing(AttendanceShiftSettingPeriodEntity::getType)).collect(Collectors.toList()); + List periodEntities = BeanUtil.copyToList(shiftList, AttendanceShiftSettingPeriodVo.class); + v.setPeriods(periodEntities); + } + }); + // 组装数据 + Map> numMap = shiftNameVos.stream().collect(Collectors.groupingBy(ShiftNameVo::getNum)); + for (Map.Entry> integerListEntry : numMap.entrySet()) { + FixedClassShiftVo fixedClassShiftVo = new FixedClassShiftVo(); + fixedClassShiftVo.setNum(integerListEntry.getKey()); + fixedClassShiftVo.setFixedType(integerListEntry.getValue().get(0).getFixedType()); + fixedClassShiftVo.setShiftNameVos(integerListEntry.getValue()); + fixedList.add(fixedClassShiftVo); + } + fixedList = fixedList.stream().sorted(Comparator.comparing(FixedClassShiftVo::getNum)).collect(Collectors.toList()); + } + return fixedList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ActionResult saveOrUpdateFixedClass(FixedClassGroupDto dto) { + RLock lock = redissonClient.getLock(String.format(ATTENDANCE_UPDATE_FIXED_CLASS, UserProvider.getUser().getTenantId(), dto.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组编辑固定班操作正在执行中,请稍后再试"); + Date date = new Date(); + List dateList = CollUtil.newArrayList(); + try { + Assert.isFalse(!lock.tryLock(5, 60, TimeUnit.SECONDS), "当前考勤组编辑固定班操作正在执行中,请稍后再试"); + String userId = UserProvider.getUser().getUserId(); + AttendanceShiftSettingVo byGroupId = attendanceShiftSettingService.findAllByGroupId(dto.getGroupId()); + List schedulingPeriods = Objects.isNull(byGroupId) ? List.of() : byGroupId.getSchedulingPeriods(); + Map collect = schedulingPeriods.stream().collect(Collectors.toMap(ShiftNameVo::getId, Function.identity())); + // 修改setting + attendanceShiftSettingService.changeSystemType(dto.getGroupId(), dto.getSystemType()); + attendanceShiftNameSettingMapper.updateByGroupId(dto.getGroupId(), dto.getSystemType(), userId); + + // 删除之前绑定的班次信息 + attendanceShiftNameSettingMapper.deleteFixedClass(dto.getGroupId()); + if (null != dto.getFixedClassDtos() && !dto.getFixedClassDtos().isEmpty()) { + List list = new ArrayList<>(); + int index = 0; + for (FixedClassDto v : dto.getFixedClassDtos()) { + // 生成偏移日期:按索引递增天数偏移基准日期 + DateTime dateTime = cn.hutool.core.date.DateUtil.offsetDay(date, ++index); + if (CollUtil.isEmpty(v.getFixedBases())) { + FixedClassSaveDto fixedClassSaveDto = new FixedClassSaveDto(FtbUtil.getId(), v.getNum(), v.getShiftType(), dto.getGroupId(), 0, null, 1); + list.add(fixedClassSaveDto); + } else { + v.getFixedBases().forEach(value -> { + FixedClassSaveDto fixedClassSaveDto = new FixedClassSaveDto(FtbUtil.getId(), v.getNum(), v.getShiftType(), dto.getGroupId(), value.getType(), value.getShiftNameId(), value.getSort()); + list.add(fixedClassSaveDto); + }); + } + for (FixedBaseDto fixedClassSaveDto : v.getFixedBases()) { + if (StringUtil.isNotEmpty(fixedClassSaveDto.getShiftNameId()) && collect.containsKey(fixedClassSaveDto.getShiftNameId())) { + ShiftNameVo shiftNameVo = collect.get(fixedClassSaveDto.getShiftNameId()); + if (Objects.equals(v.getShiftType(), 0)) { + for (AttendanceShiftSettingPeriodVo periodVo : shiftNameVo.getPeriods()) { + Assert.isTrue(fixedPeriodSingleConflict(dateTime, dateList, periodVo), "班次时间段冲突"); + } + } else { + Assert.isTrue(fixedPeriodSingleConflict(dateTime, dateList, shiftNameVo.getPeriods().get(fixedClassSaveDto.getSort() - 1)), "班次时间段冲突"); + } + } + } + + } + if (!list.isEmpty()) { + // 保存本次传入的内容 + attendanceShiftNameSettingMapper.saveFixedClass(list); + } + } + // 变更现有排班规则 + fixedChange(attendanceShiftSettingService.findAllByGroupId(dto.getGroupId()), byGroupId); + if (Objects.equals(dto.getSystemType(), 2)) { + return ActionResult.success(); + } + return ActionResult.success(); + } catch (Exception e) { + return ActionResult.fail(e.getMessage()); + } finally { + if(lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private void fixedChange(AttendanceShiftSettingVo shiftSettingVo, AttendanceShiftSettingVo oldShiftSetting) { + //过滤班制为排班数据 + if (Objects.isNull(shiftSettingVo) || Objects.equals(shiftSettingVo.getSystemType(), 2)) { + return; + } + //判断固定排班星期几数据是否调整 + List fixedPeriods = shiftSettingVo.getFixedPeriods(); + List oldFixedPeriods = oldShiftSetting.getFixedPeriods(); + Map> fixedPeriodsMap = CollUtil.isEmpty(fixedPeriods) ? Maps.newHashMap() : fixedPeriods.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, FixedClassShiftVo::getShiftNameVos)); + Map> oldFixedPeriodsMap = CollUtil.isEmpty(oldFixedPeriods) ? Maps.newHashMap() : oldFixedPeriods.stream().collect(Collectors.toMap(FixedClassShiftVo::getNum, FixedClassShiftVo::getShiftNameVos)); + //判断星期几的排班id是否有调整 + boolean isUpdate = fixedPeriodsMap.entrySet().stream().anyMatch(entry -> { + Integer num = entry.getKey(); + List shiftNames = entry.getValue(); + List shiftIds = shiftNames.stream().map(ShiftNameVo::getId).collect(Collectors.toList()); + List shiftTypes = shiftNames.stream().map(ShiftNameVo::getType).collect(Collectors.toList()); + List oldShiftNames = oldFixedPeriodsMap.getOrDefault(num, CollUtil.newArrayList()); + List oldShiftIds = oldShiftNames.stream().map(ShiftNameVo::getId).collect(Collectors.toList()); + List oldShiftTypes = oldShiftNames.stream().map(ShiftNameVo::getType).collect(Collectors.toList()); + return !shiftIds.equals(oldShiftIds) || !shiftTypes.equals(oldShiftTypes); + }); + if (isUpdate) { + fixedPeriodChange(shiftSettingVo.getGroupId()); + } + } + + @Override + public PageInfo getList(ShiftNameQueryDto queryDto) throws HandleException { + // 通过考勤组id 获取考勤组详情信息 + AttendanceGroup groupDetail = attendanceGroupMapper.getGroupDetail(queryDto.getGroupId()); + // 通过查看本组的班次开关有没有打开 + if (null == groupDetail) { + log.error("未找到该考勤组"); + throw new HandleException("未找到该考勤组"); + } + // 打开展示本组配置的班次配置信息 班次设置 1.开启 0.关闭 + if (1 != groupDetail.getAttendanceClassSetting()) { + queryDto.setGroupId(null); + // 没有打开 去查询考勤组 F_LevelCode层级中的 上级是否打开 查到最近的一个上级查询对应的配置 + List list = new ArrayList<>(Arrays.asList(groupDetail.getLevelCode().split("#"))); + List groupDetails = attendanceGroupMapper.getGroupDetailList(list); + Map groupMap = groupDetails.stream().collect(Collectors.toMap(AttendanceGroup::getId, Function.identity())); + String groupId = null; + for (String gId : list) { + AttendanceGroup attendanceGroup = groupMap.get(gId); + if (null != attendanceGroup && attendanceGroup.getAttendanceClassSetting() == 1) { + groupId = attendanceGroup.getId(); + break; + } + } + if (null != groupId) { + queryDto.setGroupId(groupId); + } + } + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo shiftNameListVoList = new PageInfo<>(); + if (null == queryDto.getGroupId()) { + return shiftNameListVoList; + } + shiftNameListVoList = new PageInfo<>(attendanceShiftNameSettingMapper.getListByGroupId(queryDto)); + if (null != shiftNameListVoList.getList() && !shiftNameListVoList.getList().isEmpty()) { + // 组装时间段 + List shiftIds = shiftNameListVoList.getList().stream().map(ShiftNameListVo::getId).collect(Collectors.toList()); + List list = attendanceShiftSettingPeriodService.list(new LambdaQueryWrapper().in(AttendanceShiftSettingPeriodEntity::getShiftId, shiftIds).eq(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.FALSE)); + Map> shiftMap = list.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodEntity::getShiftId)); + shiftNameListVoList.getList().forEach(v -> { + List attendanceShiftSettingPeriodEntities = shiftMap.getOrDefault(v.getId(), CollUtil.newArrayList()); + if (CollUtil.isNotEmpty(attendanceShiftSettingPeriodEntities)) { + // 排序 + List shiftList = attendanceShiftSettingPeriodEntities.stream().sorted(Comparator.comparing(AttendanceShiftSettingPeriodEntity::getType)).collect(Collectors.toList()); + //拼接时段 08:00-14:00 16:00-次日07:58 + StringBuilder timeStr = new StringBuilder(); + for (int i = 0; i < shiftList.size(); i++) { + AttendanceShiftSettingPeriodEntity entity = shiftList.get(i); + if (i > 0) { + timeStr.append(","); + } + timeStr.append(Objects.equals(entity.getInType(), 2) ? "次日" + entity.getInPoint() : entity.getInPoint()).append("-"); + timeStr.append(Objects.equals(entity.getInType(), 2) ? "次日" + entity.getOutPoint() : entity.getOutPoint()); + } + v.setTimeStr(timeStr.toString()); + } + }); + } + return shiftNameListVoList; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingPeriodServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingPeriodServiceImpl.java new file mode 100644 index 0000000..4e1228f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingPeriodServiceImpl.java @@ -0,0 +1,21 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceShiftSettingPeriodMapper; +import jnpf.attendance.service.AttendanceShiftSettingPeriodService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import org.springframework.stereotype.Service; + +/** + *

+ * 考勤组-考勤配置-时段 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +@Service +public class AttendanceShiftSettingPeriodServiceImpl extends SuperServiceImpl implements AttendanceShiftSettingPeriodService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingServiceImpl.java new file mode 100644 index 0000000..f7f728f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceShiftSettingServiceImpl.java @@ -0,0 +1,648 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.google.common.collect.Maps; +import jnpf.attendance.mapper.AttendanceShiftSettingMapper; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notification.AttendanceRuleNotificationHandle; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.RedisConstant; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceFixedClassEntity; +import jnpf.entity.attendance.AttendanceShiftNameEntity; +import jnpf.entity.attendance.AttendanceShiftSettingEntity; +import jnpf.entity.attendance.AttendanceShiftSettingPeriodEntity; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.AttendanceShiftDto; +import cn.hutool.core.util.StrUtil; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.FixedClassShiftVo; +import jnpf.model.attendance.vo.attendance.ShiftNameViewVo; +import jnpf.model.attendance.vo.attendance.ShiftNameVo; +import jnpf.util.*; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + *

+ * 考勤组配置-考勤配置 服务实现类 + *

+ * + * @author ahua + * @since 2023-11-22 + */ +@Service +public class AttendanceShiftSettingServiceImpl extends SuperServiceImpl implements AttendanceShiftSettingService { + + @Autowired + private AttendanceShiftSettingPeriodService attendanceShiftSettingPeriodService; + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private AttendanceUserService attendanceUserService; + @Autowired + private AttendanceFixedClassService attendanceFixedClassService; + @Autowired + private AttendanceShiftNameSettingService attendanceShiftNameSettingService; + @Autowired + private RedissonClient redissonClient; + @Autowired + private AttendanceRuleNotificationHandle attendanceRuleNotificationHandle; + + @Override + public Map getEnableShiftSetting(List groupIds, List attendanceGroupVos) { + return getEnableShiftSetting(groupIds, attendanceGroupVos, Boolean.TRUE); + } + + private Map getAllShiftSetting(List groupIds, List attendanceGroupVos) { + return getEnableShiftSetting(groupIds, attendanceGroupVos, Boolean.FALSE); + } + + private Map getEnableShiftSetting(List groupIds, List attendanceGroupVos, Boolean isFilterDisabled) { + List list = findAllByDate(attendanceGroupVos.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList()), isFilterDisabled); + Map collect1 = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, group -> group.getAttendanceClassSetting() == 1)); + Map collect = list.stream().filter(base -> collect1.getOrDefault(base.getGroupId(), Boolean.FALSE)).collect(Collectors.toMap(AttendanceShiftSettingVo::getGroupId, baseSetting -> baseSetting, (s1, s2) -> s2)); + Map map = Maps.newHashMap(); + Map groupIdToParent = attendanceGroupVos.stream().collect(Collectors.toMap(AttendanceGroup::getId, groupVo -> StringUtil.isBlank(groupVo.getParentId()) ? "-1" : groupVo.getParentId(), (g1, g2) -> g2)); + groupIds.forEach(groupId -> map.put(groupId, findNextBaseSetting(collect, groupIdToParent, groupId))); + return map; + } + + @Override + public void initShiftSetting(String groupId) { + List list = attendanceShiftNameSettingService.list(new LambdaQueryWrapper() + .eq(AttendanceShiftNameEntity::getDeleteMark, Boolean.FALSE) + .eq(AttendanceShiftNameEntity::getGroupId, groupId)); + if (CollUtil.isNotEmpty(list)) { + return; + } + UserInfo user = UserProvider.getUser(); + List attendanceGroups = attendanceGroupService.queryDropList(); + Map enableBaseSetting = getEnableShiftSetting(CollUtil.newArrayList(groupId), attendanceGroups); + AttendanceShiftSettingVo attendanceLocationSetting = enableBaseSetting.get(groupId); + if (Objects.nonNull(attendanceLocationSetting)) { + AttendanceShiftSettingEntity attendanceShiftSettingEntity = BeanUtil.toBean(attendanceLocationSetting, AttendanceShiftSettingEntity.class); + attendanceShiftSettingEntity.setId(RandomUtil.uuId()); + attendanceShiftSettingEntity.setGroupId(groupId); + attendanceShiftSettingEntity.setCreatorTime(new Date()); + attendanceShiftSettingEntity.setCreatorUserId(user.getUserId()); + attendanceShiftSettingEntity.setTenantId(user.getTenantId()); + List attendanceFixedClassEntities = CollUtil.newArrayList(); + List attendanceShiftNameEntities = CollUtil.newArrayList(); + List periodEntitys = CollUtil.newArrayList(); + List shiftNameVos = attendanceLocationSetting.getSchedulingPeriods(); + Map idMap = Maps.newHashMap(); + shiftNameVos.forEach(shiftNameVo -> { + String id = RandomUtil.uuId(); + idMap.put(shiftNameVo.getId(), id); + shiftNameVo.setId(id); + shiftNameVo.setGroupId(groupId); + shiftNameVo.setSettingId(attendanceShiftSettingEntity.getId()); + List periods = shiftNameVo.getPeriods(); + List attendanceShiftSettingPeriodEntities = BeanUtil.copyToList(periods, AttendanceShiftSettingPeriodEntity.class); + attendanceShiftSettingPeriodEntities.forEach(period -> { + period.setId(RandomUtil.uuId()); + period.setGroupId(groupId); + period.setShiftId(id); + period.setSettingId(attendanceShiftSettingEntity.getId()); + period.setCreatorTime(new Date()); + period.setCreatorUserId(user.getUserId()); + period.setTenantId(user.getTenantId()); + }); + periodEntitys.addAll(attendanceShiftSettingPeriodEntities); + }); + attendanceShiftNameEntities.addAll(BeanUtil.copyToList(shiftNameVos, AttendanceShiftNameEntity.class)); + save(attendanceShiftSettingEntity); + attendanceShiftNameSettingService.saveBatch(attendanceShiftNameEntities); + attendanceShiftSettingPeriodService.saveBatch(periodEntitys); + if (attendanceLocationSetting.getSystemType() == 1) { + List shiftVos = attendanceLocationSetting.getFixedPeriods(); + shiftVos.forEach(vo -> { + List shiftNameVos2 = vo.getShiftNameVos(); + shiftNameVos2.forEach(shiftNameVo -> { + AttendanceFixedClassEntity build = AttendanceFixedClassEntity.builder() + .id(RandomUtil.uuId()) + .num(vo.getNum()) + .settingId(attendanceShiftSettingEntity.getId()) + .shiftNameId(idMap.get(shiftNameVo.getId())) + .shiftType(vo.getFixedType()) + .build(); + attendanceFixedClassEntities.add(build); + }); + }); + } + } + + } + + private AttendanceShiftSettingVo findNextBaseSetting(Map collect, Map groupIdToParent, String groupId) { + if (StringUtil.isBlank(groupId)) { + return null; + } + if (!collect.containsKey(groupId)) { + return findNextBaseSetting(collect, groupIdToParent, groupIdToParent.get(groupId)); + } + return collect.get(groupId); + } + + @Override + public AttendanceShiftSettingVo findAllByGroupId(String groupId) { + return findAllByDate(List.of(groupId), true).stream().findFirst().orElse(null); + } + + private List findAllByDate(List groupIds, Boolean isFilterDisabled) { + List list = lambdaQuery() + .in(CollUtil.isNotEmpty(groupIds), AttendanceShiftSettingEntity::getGroupId, groupIds) + .eq(AttendanceShiftSettingEntity::getEnable, isFilterDisabled) + .orderByAsc(AttendanceShiftSettingEntity::getEnableTime) + .list(); + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + //查询固定班 + List fixedClass = attendanceFixedClassService.list(new LambdaQueryWrapper() + .in(AttendanceFixedClassEntity::getGroupId, groupIds)); + Map> fixedClassMap = CollUtil.isEmpty(fixedClass) ? Maps.newHashMap() : fixedClass.stream().collect(Collectors.groupingBy(AttendanceFixedClassEntity::getGroupId)); + //查询所有班次 + List list2 = attendanceShiftNameSettingService.list(new LambdaQueryWrapper() + .eq(AttendanceShiftNameEntity::getDeleteMark, Boolean.FALSE) + .in(AttendanceShiftNameEntity::getGroupId, groupIds)); + if (CollUtil.isEmpty(list2)) { + return CollUtil.newArrayList(); + } + Map> shiftMap = CollUtil.isEmpty(list2) ? Maps.newHashMap() : list2.stream().collect(Collectors.groupingBy(AttendanceShiftNameEntity::getGroupId)); + List collect1 = list2.stream().map(AttendanceShiftNameEntity::getId).distinct().collect(Collectors.toList()); + //查询涉及的所有时段 + List list1 = attendanceShiftSettingPeriodService.list(new LambdaQueryWrapper() + .in(AttendanceShiftSettingPeriodEntity::getShiftId, collect1) + .eq(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.FALSE)); + List attendanceShiftSettingPeriods = CollUtil.isNotEmpty(list1) ? BeanUtil.copyToList(list1, AttendanceShiftSettingPeriodVo.class) : CollUtil.newArrayList(); + attendanceShiftSettingPeriods.forEach(period -> period.setValidDuration(period.calValidDuration())); + Map> periodMap = attendanceShiftSettingPeriods.stream().collect(Collectors.groupingBy(AttendanceShiftSettingPeriodVo::getShiftId)); + List attendanceShiftSettings = BeanUtil.copyToList(list, AttendanceShiftSettingVo.class); + List fixedClassShiftVos = CollUtil.newArrayList(); + List shiftNameVos = CollUtil.newArrayList(); + for (AttendanceShiftSettingVo setting : attendanceShiftSettings) { + //查询当前班组所有班次 + List shifts = shiftMap.getOrDefault(setting.getGroupId(), CollUtil.newArrayList()); + Map shiftNameMap = shifts.stream().collect(Collectors.toMap(AttendanceShiftNameEntity::getId, shiftName -> shiftName)); + //划分固定班及排班 + if (Objects.equals(setting.getSystemType(), 1)) { + fixedClassShiftVos.clear(); + //过滤固定班 + List fixedClassList = fixedClassMap.getOrDefault(setting.getGroupId(), CollUtil.newArrayList()); + Map> collect = fixedClassList.stream().collect(Collectors.groupingBy(AttendanceFixedClassEntity::getNum)); + collect.forEach((num, fixedList) -> { + shiftNameVos.clear(); + fixedList.forEach(fixed -> { + if (Objects.isNull(fixed) || Objects.isNull(fixed.getType())) { + return; + } + if (fixed.getType() == 1 || fixed.getType() == 0) { + ShiftNameVo shiftName = new ShiftNameVo(); + shiftName.setType(fixed.getType()); + shiftNameVos.add(shiftName); + return; + } + if (StringUtil.isEmpty(fixed.getShiftNameId())) { + return; + } + AttendanceShiftNameEntity attendanceShiftNameEntity = shiftNameMap.get(fixed.getShiftNameId()); + if (Objects.nonNull(attendanceShiftNameEntity)) { + ShiftNameVo shiftNameVo = BeanUtil.toBean(attendanceShiftNameEntity, ShiftNameVo.class); + shiftNameVo.setType(fixed.getType()); + shiftNameVo.setPeriods(periodMap.get(shiftNameVo.getId())); + shiftNameVos.add(shiftNameVo); + } + }); + fixedClassShiftVos.add(FixedClassShiftVo.builder() + .num(num) + .fixedType(fixedList.size() > 1 ? 1 : 0) + .shiftNameVos(BeanUtil.copyToList(shiftNameVos, ShiftNameVo.class)) + .build()); + }); + setting.setFixedPeriods(BeanUtil.copyToList(fixedClassShiftVos, FixedClassShiftVo.class)); + } + List shiftNameVo = BeanUtil.copyToList(shifts, ShiftNameVo.class); + Iterator iterator = shiftNameVo.iterator(); + while (iterator.hasNext()) { + ShiftNameVo shiftName = iterator.next(); + if (Objects.isNull(shiftName)) { + continue; + } + shiftName.setPeriods(periodMap.getOrDefault(shiftName.getId(), CollUtil.newArrayList())); + } + setting.setSchedulingPeriods(shiftNameVo); + } + return attendanceShiftSettings; + } + + @Override + public AttendanceShiftSettingVo findByGroupId(String groupId) { + List attendanceGroups = attendanceGroupService.queryDropList(); + Map enableShiftSetting = getEnableShiftSetting(CollUtil.newArrayList(groupId), attendanceGroups, Boolean.TRUE); + AttendanceShiftSettingVo attendanceShiftSettingVo = enableShiftSetting.get(groupId); + if (Objects.isNull(attendanceShiftSettingVo)) { + attendanceShiftSettingVo = new AttendanceShiftSettingVo(); + } + if (!StringUtil.equals(attendanceShiftSettingVo.getGroupId(), groupId)) { + attendanceShiftSettingVo.setGroupId(groupId); + attendanceShiftSettingVo.setEnable(0); + } + return attendanceShiftSettingVo; + } + + @Override + @Transactional + public void save(AttendanceShiftDto dto) throws HandleException { + Date time = null; + if (Objects.equals(dto.getEnableType(), 1)) { + dto.setEnableTime(DateUtil.getDayBegin()); + } + if (Objects.equals(dto.getEnableType(), 2)) { + AttendanceShiftSettingPeriodEntity one = attendanceShiftSettingPeriodService.getOne(new LambdaQueryWrapper() + .eq(AttendanceShiftSettingPeriodEntity::getGroupId, dto.getGroupId()) + .orderByDesc(AttendanceShiftSettingPeriodEntity::getOutLackPoint) + .last(" limit 1")); + Date outLackPoint = Objects.isNull(one) ? DateUtil.dateAddDays(DateUtil.getDayBegin(), 1) : DateDetail.getDateByPoint(new Date(), one.getOutLackPoint(), one.getOutLackType()); + Date dayBegin = DateUtil.dateAddDays(DateUtil.getDayBegin(), 1); + if (outLackPoint != null) { + dto.setEnableTime(outLackPoint.after(dayBegin) ? outLackPoint : dayBegin); + } + dto.setId(RandomUtil.uuId()); + time = new Date(); + } + AttendanceShiftSettingEntity attendanceShiftSettingEntity = BeanUtil.toBean(dto, AttendanceShiftSettingEntity.class); + UserInfo user = UserProvider.getUser(); + List list = lambdaQuery() + .eq(AttendanceShiftSettingEntity::getGroupId, dto.getGroupId()) + .ge(Objects.nonNull(time), AttendanceShiftSettingEntity::getEnableTime, time) + .eq(AttendanceShiftSettingEntity::getDeleteMark, Boolean.FALSE) + .orderByDesc(AttendanceShiftSettingEntity::getEnableTime) + .list(); + if (CollUtil.isEmpty(list)) { + attendanceShiftSettingEntity.setId(RandomUtil.uuId()); + attendanceShiftSettingEntity.setCreatorTime(new Date()); + attendanceShiftSettingEntity.setCreatorUserId(user.getUserId()); + attendanceShiftSettingEntity.setTenantId(user.getTenantId()); + attendanceShiftSettingEntity.setEnable(1); + attendanceShiftSettingEntity.setGroupId(dto.getGroupId()); + } else { + AttendanceShiftSettingEntity attendanceShiftSettingEntity1 = list.get(0); + if (Objects.equals(dto.getEnableType(), 1)) { + if (list.size() > 1) { + removeById(list.get(1)); + } + } + attendanceShiftSettingEntity.setId(attendanceShiftSettingEntity1.getId()); + attendanceShiftSettingEntity.setGroupId(dto.getGroupId()); + attendanceShiftSettingEntity.setLastModifyTime(new Date()); + attendanceShiftSettingEntity.setEnable(1); + attendanceShiftSettingEntity.setLastModifyUserId(user.getUserId()); + } + List periodEntities = BeanUtil.copyToList(dto.getPeriods(), AttendanceShiftSettingPeriodEntity.class); + if (CollUtil.isEmpty(periodEntities)) { + throw new HandleException("未设置班制时段数据"); + } + //判断固定班时段是否冲突 + if (!fixedPeriodConflict(dto.getSystemType(), periodEntities)) { + throw new HandleException("当前固定班时段存在冲突"); + } + //获取最后排序 + AttendanceShiftSettingPeriodEntity one = attendanceShiftSettingPeriodService.getOne(new LambdaQueryWrapper() + .eq(AttendanceShiftSettingPeriodEntity::getGroupId, dto.getGroupId()) + .orderByDesc(AttendanceShiftSettingPeriodEntity::getCreatorTime) + .last(" limit 1")); + AtomicInteger sort = new AtomicInteger(Objects.isNull(one) ? 0 : one.getSort()); + periodEntities.forEach(entity -> { + if (StringUtil.isEmpty(entity.getId())) { + if (12 < sort.get()) { + sort.set(0); + } + entity.setSort(sort.addAndGet(1)); + } + entity.setSystemType(attendanceShiftSettingEntity.getSystemType()); + entity.setSettingId(attendanceShiftSettingEntity.getId()); + if (StringUtil.isEmpty(entity.getId())) { + entity.setGroupId(dto.getGroupId()); + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(user.getUserId()); + entity.setTenantId(user.getTenantId()); + entity.setDeleteMark(0); + } else { + entity.setGroupId(dto.getGroupId()); + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(user.getUserId()); + entity.setDeleteMark(0); + } + }); + List collect = periodEntities.stream().map(AttendanceShiftSettingPeriodEntity::getId).collect(Collectors.toList()); + List shiftSettingPeriods = attendanceShiftSettingPeriodService.listByIds(collect); + //切换班制类型,清除原规则 + changeSystemType(attendanceShiftSettingEntity.getGroupId(), attendanceShiftSettingEntity.getSystemType()); + String settingId = CollUtil.isEmpty(list) ? null : list.get(0).getId(); + attendanceShiftSettingPeriodService.update(new LambdaUpdateWrapper() + .eq(AttendanceShiftSettingPeriodEntity::getGroupId, dto.getGroupId()) + .eq(StringUtil.isNotEmpty(settingId), AttendanceShiftSettingPeriodEntity::getSettingId, settingId) + .eq(AttendanceShiftSettingPeriodEntity::getSystemType, dto.getSystemType()) + .set(AttendanceShiftSettingPeriodEntity::getDeleteMark, 1)); + attendanceShiftSettingPeriodService.saveOrUpdateBatch(periodEntities); + if (Objects.equals(dto.getSystemType(), 2)) { + return; + } + //固定班时段调整,变更现有排班规则 + AttendanceShiftSettingVo attendanceShiftSettingVo = new AttendanceShiftSettingVo(); + attendanceShiftSettingVo.setSystemType(1); +// attendanceShiftSettingVo.setFixedPeriods(BeanUtil.copyToList(periodEntities, AttendanceShiftSettingPeriodVo.class)); + attendanceShiftSettingVo.setCycle(dto.getCycle()); + fixedPeriodChange(dto.getGroupId(), attendanceShiftSettingVo, shiftSettingPeriods, dto.getEnableTime()); + } + + private Boolean fixedPeriodConflict(Integer systemType, List periodEntities) { + if (Objects.equals(systemType, 2)) { + return Boolean.TRUE; + } + List list = CollUtil.newArrayList(); + for (AttendanceShiftSettingPeriodEntity period : periodEntities) { + if (!fixedPeriodSingleConflict(list, period)) { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + } + + private Boolean fixedPeriodSingleConflict(List list, AttendanceShiftSettingPeriodEntity + period) { + Date date = new Date(); + Date inPoint = DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType()); + Date outPoint = DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType()); + if (CollUtil.isEmpty(list)) { + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + for (SecondmentDateVo vo : list) { + if (outPoint != null && (DateDetail.checkTimeBetween(inPoint, vo.getStartTime(), vo.getEndTime()) + || DateDetail.checkTimeBetween(outPoint, vo.getStartTime(), vo.getEndTime()) + || DateDetail.checkTimeBetween(vo.getStartTime(), inPoint, outPoint) + || DateDetail.checkTimeBetween(vo.getEndTime(), inPoint, outPoint))) { + return Boolean.FALSE; + } + } + list.add(SecondmentDateVo.builder().startTime(inPoint).endTime(outPoint).build()); + return Boolean.TRUE; + } + + private void fixedPeriodChange(String groupId, AttendanceShiftSettingVo shiftSettingVo, List oldPeriods, Date enableTime) { +// Map collect = oldPeriods.stream().collect(Collectors.toMap(AttendanceShiftSettingPeriodEntity::getId, period -> period)); +// List periodEntities = shiftSettingVo.getFixedPeriods(); +// List collect1 = cycleUpdate ? periodEntities : periodEntities.stream().filter(period -> { +// AttendanceShiftSettingPeriodEntity periodEntity = collect.get(period.getId()); +// if (Objects.isNull(periodEntity)) { +// return Boolean.TRUE; +// } +// if (period.equalsPeriod(periodEntity)) { +// return Boolean.FALSE; +// } +// return Boolean.TRUE; +// }).collect(Collectors.toList()); +// if (CollUtil.isEmpty(collect1)) { +// return; +// } +// List enableGroup = getEnableGroup(groupId); +// List collect2 = enableGroup.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); +// collect2.add(groupId); +// attendanceDailyRuleService.fixedPeriodChange(collect2, shiftSettingVo, enableTime); + } + + @Override + public void changeSystemType(String groupId, Integer systemType) { + if (StringUtil.isEmpty(groupId)) { + return; + } + AttendanceShiftSettingEntity byId = lambdaQuery() + .eq(AttendanceShiftSettingEntity::getGroupId, groupId) + .orderByDesc(AttendanceShiftSettingEntity::getCreatorTime) + .last("limit 1") + .one(); + if (Objects.isNull(byId)) { + byId = new AttendanceShiftSettingEntity(); + byId.setId(RandomUtil.uuId()); + byId.setGroupId(groupId); + byId.setSystemType(systemType); + byId.setEnable(1); + byId.setCreatorTime(new Date()); + byId.setCreatorUserId(UserProvider.getLoginUserId()); + } + if (!Objects.equals(systemType, byId.getSystemType())) { + sendNoticeBatch(groupId, systemType); + } + byId.setSystemType(systemType); + byId.setEnable(1); + saveOrUpdate(byId); + } + + private void sendNoticeBatch(String groupId, Integer systemType) { + List selfAndChildrenGroupIds = attendanceGroupService.getSelfAndChildrenGroupIds(groupId, 3); + attendanceRuleNotificationHandle.sendNoticeBatch(selfAndChildrenGroupIds, systemType); + } + + private void sendNoticeByStatus(String groupId, Integer oldSystemType) { + AttendanceShiftSettingVo byGroupId = findByGroupId(groupId); + if (Objects.equals(oldSystemType, byGroupId.getSystemType())) { + return; + } + List selfAndChildrenGroupIds = attendanceGroupService.getSelfAndChildrenGroupIds(groupId, 3); + attendanceRuleNotificationHandle.sendNoticeBatch(selfAndChildrenGroupIds, byGroupId.getSystemType()); + } + + + @Override + public void delPeriod(String periodId) { + attendanceShiftSettingPeriodService.update(new LambdaUpdateWrapper() + .set(AttendanceShiftSettingPeriodEntity::getDeleteMark, Boolean.TRUE) + .set(AttendanceShiftSettingPeriodEntity::getDeleteUserId, UserProvider.getLoginUserId()) + .set(AttendanceShiftSettingPeriodEntity::getDeleteTime, new Date()) + .eq(AttendanceShiftSettingPeriodEntity::getId, periodId)); + } + + @Override + public void changeStatus(String groupId, Integer enable, AttendanceShiftSettingVo shiftSettingVo) { + RLock lock = redissonClient.getLock(String.format(RedisConstant.ATTENDANCE_CHANGE_GROUP_STATUS, UserProvider.getUser().getTenantId(), groupId)); + + try { + Assert.isFalse(lock.isLocked() || !lock.tryLock(5,60, TimeUnit.SECONDS), "当前考勤组切换班次操作正在执行中,请稍后再试"); + lambdaUpdate().eq(AttendanceShiftSettingEntity::getGroupId, groupId) + .set(AttendanceShiftSettingEntity::getEnable, enable).update(); + attendanceDailyRuleService.fixedPeriodChange(attendanceGroupService.getEnableGroupIds(groupId), findByGroupId(groupId), DateUtil.getDayBegin()); + sendNoticeByStatus(groupId, shiftSettingVo.getSystemType()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if(lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + @Override + public List periodList(String groupId) { + List resolved = periodListForIntelligentSchedule(groupId); + return resolved == null ? CollUtil.newArrayList() : resolved; + } + + @Override + public List periodListForIntelligentSchedule(String groupId) { + if (StrUtil.isBlank(groupId)) { + return CollUtil.newArrayList(); + } + List attendanceGroups = attendanceGroupService.queryDropList(); + Map enableShiftSetting = + getEnableShiftSetting(CollUtil.newArrayList(groupId), attendanceGroups, Boolean.TRUE); + AttendanceShiftSettingVo byGroupId = enableShiftSetting.get(groupId); + if (Objects.isNull(byGroupId)) { + return null; + } + if (CollUtil.isEmpty(byGroupId.getSchedulingPeriods())) { + return CollUtil.newArrayList(); + } + return mapSchedulingPeriodsToShiftPeriodVo(byGroupId); + } + + private List mapSchedulingPeriodsToShiftPeriodVo(AttendanceShiftSettingVo byGroupId) { + return byGroupId.getSchedulingPeriods().stream() + .map( + period -> { + ShiftNameViewVo shiftNameViewVo = BeanUtil.toBean(period, ShiftNameViewVo.class); + shiftNameViewVo.setPeriodNames( + period.getPeriods().stream() + .map( + p -> + PeriodNameVO.builder() + .periodName(getName(p)) + .breakEnable(p.getBreakEnable()) + .inType(p.getInType()) + .inPoint(p.getInPoint()) + .outType(p.getOutType()) + .outPoint(p.getOutPoint()) + .breakStartType(p.getBreakStartType()) + .breakEndType(p.getBreakEndType()) + .breakStartPoint(p.getBreakStartPoint()) + .breakEndPoint(p.getBreakEndPoint()) + .timeSlotDay(p.getTimeSlotDay()) + .validDuration(p.getValidDuration()) + .build()) + .collect(Collectors.toList())); + return ShiftPeriodVo.builder() + .id(period.getId()) + .name(period.getName()) + .shortName(period.getShortName()) + .sort(period.getFixedSort()) + .colour(period.getColour()) + .shiftType(period.getShiftType()) + .schedulingPeriod(shiftNameViewVo) + .build(); + }) + .collect(Collectors.toList()); + } + + @Override + public List periodListForSelfScheduling() { + String loginUserId = UserProvider.getLoginUserId(); + Date date = new Date(); + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(date, date, List.of(loginUserId), null); + Assert.isFalse(CollUtil.isEmpty(attendanceGroupUsers), "未查询到当前时间所在考勤组"); + AttendanceGroupUser groupUser = attendanceGroupUsers.stream().findFirst().orElse(null); + String groupId = null; + if (groupUser != null) { + groupId = groupUser.getGroupId(); + } + boolean isSecondment = Objects.equals(Objects.requireNonNull(groupUser).getType(), 2); + List dateVoList = isSecondment ? JsonUtil.getJsonToList(groupUser.getTimeJson(), SecondmentDateVo.class) : CollUtil.newArrayList(); + SecondmentDateVo secondmentDateVo = dateVoList.stream().filter(vo -> !date.before(vo.getStartTime()) && !date.after(vo.getEndTime())).findFirst().orElse(null); + //当前排班为个人 + List attendanceGroups = attendanceGroupService.queryDropList(); + Map enableShiftSetting = getEnableShiftSetting(CollUtil.newArrayList(groupId), attendanceGroups, Boolean.TRUE); + AttendanceShiftSettingVo byGroupId = enableShiftSetting.get(groupId); + if (Objects.isNull(byGroupId)) { + return CollUtil.newArrayList(); + } + if (CollUtil.isEmpty(byGroupId.getSchedulingPeriods())) { + return CollUtil.newArrayList(); + } + return byGroupId.getSchedulingPeriods().stream().map(shiftNameVo -> { + List periods = shiftNameVo.getPeriods(); + if (isSecondment) { + boolean allMatch = periods.stream().allMatch(period -> { + Date inPoint = DateDetail.getDateByPoint(date, period.getInPoint(), period.getInType()); + Date outPoint = DateDetail.getDateByPoint(date, period.getOutPoint(), period.getOutType()); + if (secondmentDateVo != null) { + return !secondmentDateVo.getStartTime().before(inPoint) && !secondmentDateVo.getEndTime().after(outPoint); + } + return false; + }); + if (!allMatch) { + return null; + } + } + return ShiftPeriodVo.builder() + .id(shiftNameVo.getId()) + .name(shiftNameVo.getName()) + .shortName(shiftNameVo.getShortName()) + .periodNames(periods.stream().map(this::getName2).reduce((r1, r2) -> r1 + " " + r2).orElse("")) + .sort(shiftNameVo.getFixedSort()) + .colour(shiftNameVo.getColour()) + .shiftType(shiftNameVo.getShiftType()) + .build(); + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + public void hisShiftSetting(Map group2orgMap, Map org2GroupMap) { + List list = lambdaQuery().in(AttendanceShiftSettingEntity::getDeleteMark, 0).list(); + list.forEach(item -> item.setGroupId(org2GroupMap.getOrDefault(group2orgMap.getOrDefault(item.getGroupId(), item.getGroupId()), item.getGroupId()))); + list.stream().collect(Collectors.groupingBy(AttendanceShiftSettingEntity::getGroupId)).forEach((key, value) -> { + value.stream().skip(1).forEach(vo -> vo.setDeleteMark(1)); + }); + updateBatchById(list); + Map group2setting = list.stream().filter(vo -> Objects.equals(vo.getDeleteMark(), 0)).collect(Collectors.toMap(AttendanceShiftSettingEntity::getGroupId, AttendanceShiftSettingEntity::getId)); + List list1 = attendanceShiftNameSettingService.list(new LambdaQueryWrapper().in(AttendanceShiftNameEntity::getDeleteMark, 0)); + list1.forEach(item -> { + item.setGroupId(org2GroupMap.getOrDefault(group2orgMap.getOrDefault(item.getGroupId(), item.getGroupId()), item.getGroupId())); + item.setSettingId(group2setting.getOrDefault(item.getGroupId(), item.getSettingId())); + }); + AtomicInteger i = new AtomicInteger(0); + list1.stream().collect(Collectors.groupingBy(vo -> vo.getGroupId() + vo.getName())).forEach((vo,settings)->{ + i.set(0); + settings.stream().skip(1).forEach(setting->setting.setName(setting.getName()+(i.incrementAndGet()))); + }); + attendanceShiftNameSettingService.updateBatchById(list1); + } + + private String getName(AttendanceShiftSettingPeriodVo period) { + return (Objects.equals(period.getInType(), 2) ? "次日" + period.getInPoint() : period.getInPoint()) + "-" + + (period.getOutType() == 1 ? period.getOutPoint() : "次日" + period.getOutPoint()); + } + + + private String getName2(AttendanceShiftSettingPeriodVo period) { + return (Objects.equals(period.getInType(), 2) ? "次日" + period.getInPoint() : period.getInPoint()) + "至" + + (period.getOutType() == 1 ? period.getOutPoint() : "次日" + period.getOutPoint()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSuperAdminServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSuperAdminServiceImpl.java new file mode 100644 index 0000000..29dda56 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceSuperAdminServiceImpl.java @@ -0,0 +1,917 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.mapper.AttendanceApprovalSettingMapper; +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.mapper.AttendanceManagerPermissionMapper; +import jnpf.attendance.mapper.PermissionDictMapper; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.ActionResult; +import jnpf.constants.AttendancePermissionConstant; +import jnpf.entity.*; +import jnpf.enums.attendance.ApprovalPermissionTypeEnum; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.PermissionTypeEnum; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.BatchSaveGroupAdmin; +import jnpf.model.attendance.dto.GroupFilterDto; +import jnpf.model.attendance.dto.SaveGroupAdmin; +import jnpf.model.attendance.dto.SaveSuperAdminDto; +import jnpf.model.attendance.model.AdminUpdateNoticeModel; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.UserPermissions; +import jnpf.model.attendance.vo.permission.ActionPermissionVo; +import jnpf.model.attendance.vo.permission.ApprovalSettingVo; +import jnpf.model.attendance.vo.permission.AttendanceTeamSetVo; +import jnpf.model.attendance.vo.permission.ConfigPermissionVo; +import jnpf.model.attendance.vo.permission.app.ManagerPermissionVo; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.PermissionUtil; +import jnpf.util.mapper.MybatisUtil; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; + +@Service +public class AttendanceSuperAdminServiceImpl implements AttendanceSuperAdminService { + private final List managerEmbodyPermissions = List.of("成员管理", "余额管理", "权限设置", "排班", "绑定组织", "锁定"); + @Resource + private V2UserApi v2UserApi; + @Resource + private UserProvider userProvider; + @Resource + private PermissionDictMapper permissionDictMapper; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Resource + private AttendanceGroupService attendanceGroupService; + @Resource + private AttendanceShiftSettingService shiftSettingService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private AttendanceUserSettingService attendanceUserSettingService; + @Resource + private AttendanceApprovalSettingMapper attendanceApprovalSettingMapper; + @Resource + private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper; + @Resource + private AttendanceUserService attendanceUserService; + + @NotNull + private static List getLevelIds(List levelCodeList) { + List groupIdList = new ArrayList<>(); + levelCodeList.forEach(levelCode -> { + String[] split = levelCode.split("#"); + CollectionUtil.addAll(groupIdList, split); + }); + + groupIdList.remove(0); + return groupIdList; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void add(SaveSuperAdminDto saveSuperAdminDto) throws HandleException { + /* 删除所有考勤组超级管理员*/ + UpdateWrapper deleteWrapper = new UpdateWrapper<>(); + deleteWrapper.lambda() + .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode()); + attendanceManagerPermissionMapper.delete(deleteWrapper); + for (String userId : saveSuperAdminDto.getUserIds()) { + AttendanceManagerPermission managerPermission = new AttendanceManagerPermission(); + managerPermission.setType(0); + managerPermission.setId(RandomUtil.uuId()); + managerPermission.setUserId(userId); + managerPermission.setCreatorTime(new Date()); + managerPermission.setCreatorUserId(UserProvider.getLoginUserId()); + attendanceManagerPermissionMapper.insert(managerPermission); + } + } + + @Override + public void delete(List userIds) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(AttendanceManagerPermission::getUserId, userIds) + .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode()); + attendanceManagerPermissionMapper.delete(updateWrapper); + } + + /** + * 获取考勤组超级管理员列表 + * + * @return List + */ + @Override + public List querySuperAdmin(String name) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode()) + .eq(AttendanceManagerPermission::getDeleteMark, 0); + + List superAdminList = attendanceManagerPermissionMapper.selectList(queryWrapper); + if (CollectionUtil.isEmpty(superAdminList)) { + return new ArrayList<>(); + } + List userList = v2UserApi.userListAndCopy(superAdminList.stream().map(AttendanceManagerPermission::getUserId).collect(Collectors.toList()), false, null); + return JsonUtil.getJsonToList(userList, AttendanceUserVo.class); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addGroupAdmin(SaveGroupAdmin saveGroupAdmin) throws Exception { + ParamUtil.checkParam(saveGroupAdmin); + List permissionIds = saveGroupAdmin.getPermissionIds(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .in(PermissionDict::getId, permissionIds); + List permissionList = permissionDictMapper.selectList(queryWrapper); + Map map = new HashMap<>(); + permissionList.forEach(permission -> { + map.put(permission.getId(), permission.getModuleType()); + }); + permissionIds.forEach(id -> { + save(id, saveGroupAdmin.getGroupId(), saveGroupAdmin.getUserId(), map.get(id)); + }); + //执行消息提醒 + AttendanceGroupVo groupVo = attendanceGroupMapper.getGroupBindingOrg(saveGroupAdmin.getGroupId()); + if (Objects.isNull(groupVo)) { + throw new HandleException("未查询到考勤组信息"); + } + AdminUpdateNoticeModel updateNoticeModel = new AdminUpdateNoticeModel(); + updateNoticeModel.setUserIds(List.of(saveGroupAdmin.getUserId())); + updateNoticeModel.setType(1); + updateNoticeModel.setGroupName(groupVo.getGroupName()); + List currentAuthorityList = permissionList.stream().filter(item -> item.getModuleType().equals(1)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(currentAuthorityList)) { + updateNoticeModel.setCurrentAuthority(currentAuthorityList.stream().map(PermissionDict::getName).collect(Collectors.joining(","))); + } + List sonAuthorityList = permissionList.stream().filter(item -> item.getModuleType().equals(2)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(sonAuthorityList)) { + updateNoticeModel.setSonAuthority(sonAuthorityList.stream().map(PermissionDict::getName).collect(Collectors.joining(","))); + } + updateNoticeModel.setTenantId(userProvider.get().getTenantId()); + updateNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.ADMINISTRATOR_CHANGE); + updateNoticeModel.setGroupId(saveGroupAdmin.getGroupId()); + attendanceNoticeHandler.send(updateNoticeModel); + } + + @Override + public void batchAddGroupAdmin(BatchSaveGroupAdmin batchSaveGroupAdmin) { + /* 先查询权限*/ + List permissionIds = batchSaveGroupAdmin.getPermissionIds(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .in(PermissionDict::getId, permissionIds); + List permissionList = permissionDictMapper.selectList(queryWrapper); + Map typeMap = new HashMap<>(); + for (PermissionDict permissionDict : permissionList) { + typeMap.put(permissionDict.getId(), permissionDict.getModuleType()); + } + + /* 先通过考勤组id和用户id批量删除该组的管理员*/ + String groupId = batchSaveGroupAdmin.getGroupId(); + List userIds = batchSaveGroupAdmin.getUserIds(); + UpdateWrapper deleteWrapper = new UpdateWrapper<>(); + deleteWrapper.lambda() + .eq(AttendanceManagerPermission::getGroupId, groupId) + .in(AttendanceManagerPermission::getUserId, userIds); + attendanceManagerPermissionMapper.delete(deleteWrapper); + /* 批量入库*/ + batchSaveGroupAdmin.getUserIds().forEach(userId -> { + List attendanceManagerPermissionList = new ArrayList<>(); + permissionIds.forEach(pId -> { + AttendanceManagerPermission insert = new AttendanceManagerPermission(); + insert.setId(RandomUtil.uuId()); + insert.setGroupId(batchSaveGroupAdmin.getGroupId()); + insert.setUserId(userId); + insert.setPermissionId(pId); + insert.setType(typeMap.get(pId)); + insert.setCreatorUserId(UserProvider.getLoginUserId()); + insert.setCreatorTime(new Date()); + attendanceManagerPermissionList.add(insert); + }); + attendanceManagerPermissionMapper.batchSaveGroupAdmin(attendanceManagerPermissionList); + }); + //执行消息提醒 + AttendanceGroupVo groupVo = attendanceGroupMapper.getGroupBindingOrg(batchSaveGroupAdmin.getGroupId()); + if (Objects.isNull(groupVo)) { + return; + } + AdminUpdateNoticeModel updateNoticeModel = new AdminUpdateNoticeModel(); + updateNoticeModel.setUserIds(batchSaveGroupAdmin.getUserIds()); + updateNoticeModel.setType(1); + updateNoticeModel.setGroupName(groupVo.getGroupName()); + List currentAuthorityList = permissionList.stream().filter(item -> item.getModuleType().equals(1)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(currentAuthorityList)) { + updateNoticeModel.setCurrentAuthority(currentAuthorityList.stream().map(PermissionDict::getName).collect(Collectors.joining(","))); + } + List sonAuthorityList = permissionList.stream().filter(item -> item.getModuleType().equals(2)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(sonAuthorityList)) { + updateNoticeModel.setSonAuthority(sonAuthorityList.stream().map(PermissionDict::getName).collect(Collectors.joining(","))); + } + updateNoticeModel.setTenantId(userProvider.get().getTenantId()); + updateNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.ADMINISTRATOR_CHANGE); + updateNoticeModel.setGroupId(batchSaveGroupAdmin.getGroupId()); + attendanceNoticeHandler.send(updateNoticeModel); + } + + @Override + public void updateGroupAdmin(SaveGroupAdmin saveGroupAdmin) { + /* 先删除该管理员在该考勤组下的所有权限*/ + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda() + .eq(AttendanceManagerPermission::getGroupId, saveGroupAdmin.getGroupId()) + .eq(AttendanceManagerPermission::getUserId, saveGroupAdmin.getUserId()); + attendanceManagerPermissionMapper.delete(updateWrapper); + saveGroupAdmin.getPermissionIds().forEach(id -> { + PermissionDict permissionDict = permissionDictMapper.selectById(id); + save(id, saveGroupAdmin.getGroupId(), saveGroupAdmin.getUserId(), permissionDict.getModuleType()); + }); + } + + /** + * 删除考勤组管理员 + */ + @Override + public void deleteGroupAdmin(SaveGroupAdmin saveGroupAdmin) { + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda() + .eq(AttendanceManagerPermission::getGroupId, saveGroupAdmin.getGroupId()) + .eq(AttendanceManagerPermission::getUserId, saveGroupAdmin.getUserId()); + attendanceManagerPermissionMapper.delete(updateWrapper); + //执行消息提醒 + AttendanceGroupVo groupVo = attendanceGroupMapper.getGroupBindingOrg(saveGroupAdmin.getGroupId()); + if (Objects.isNull(groupVo)) { + return; + } + AdminUpdateNoticeModel updateNoticeModel = new AdminUpdateNoticeModel(); + updateNoticeModel.setUserIds(List.of(saveGroupAdmin.getUserId())); + updateNoticeModel.setType(2); + updateNoticeModel.setGroupName(groupVo.getGroupName()); + updateNoticeModel.setTenantId(userProvider.get().getTenantId()); + updateNoticeModel.setGroupId(saveGroupAdmin.getGroupId()); + updateNoticeModel.setAttendanceNoticeEnum(AttendanceNoticeEnum.ADMINISTRATOR_CHANGE); + attendanceNoticeHandler.send(updateNoticeModel); + } + + /** + * 查看考勤组下管理员列表 + * + * @param groupId 考勤组 + * @return List + */ + @Override + public Map listGroupAdmin(String groupId) { + Map dataMap = new HashMap<>(); + + /* 获取审批设置信息*/ + List approvalSettings = MybatisUtil.findListByFiled(attendanceApprovalSettingMapper, AttendanceApprovalSetting::getGroupId, groupId, false); + // 处理新增的外出的初始化 + List typeList = approvalSettings.stream().map(AttendanceApprovalSetting::getType).collect(Collectors.toList()); + if (!typeList.contains(ApprovalSettingTypeEnum.GO_OUT.getCode())) { + AttendanceApprovalSetting attendanceApprovalSetting = new AttendanceApprovalSetting(groupId, ApprovalSettingTypeEnum.GO_OUT.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + approvalSettings.add(attendanceApprovalSetting); + } + // 出差审批的初始化 + if (!typeList.contains(ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode())) { + AttendanceApprovalSetting attendanceApprovalSetting = new AttendanceApprovalSetting(groupId, ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode(), ApprovalPermissionTypeEnum.SUPERIOR.getCode()); + approvalSettings.add(attendanceApprovalSetting); + } + approvalSettings = approvalSettings.stream().sorted(comparing(AttendanceApprovalSetting::getType)).collect(Collectors.toList()); + dataMap.put("approvalSettings", approvalSettings); + + List voList = attendanceManagerPermissionMapper.queryGroupUser(groupId); + if (CollUtil.isEmpty(voList)) { + voList = new ArrayList<>(); + } + // 自己组的管理员 + List userIds; + userIds = voList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()); + //过滤出当前时间在本组的借调人员 + List selfUsersBeLongToGroup = attendanceUserService.getSelfUsersBeLongToGroup(groupId); + // 借调用户集合 + List selfCollect; + if (null != selfUsersBeLongToGroup && !selfUsersBeLongToGroup.isEmpty()) { + selfCollect = selfUsersBeLongToGroup.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + List finalVoList = voList; + selfUsersBeLongToGroup.forEach(v -> { + // 当用户是当前借调组的管理员,同时也是用户所属考勤组的管理员, 那么只展示当前借调考勤组的权限信息 + if (!userIds.contains(v.getUserId())) { + AttendanceGroupAdminVo vo = attendanceManagerPermissionMapper.queryGroupUserPermissionByUserId(v.getUserId(), v.getGroupId()); + if (null != vo && StringUtil.isNotEmpty(vo.getUserId())) { + vo.setSelfGroupId(v.getGroupId()); + // 是管理员添加到里面 + finalVoList.add(vo); + } + } + }); + } else { + selfCollect = null; + } + if (CollectionUtil.isEmpty(voList)) { + return dataMap; + } + // 处理可能同时是两个组的管理员情况 + ActionResult> userVoList = v2UserApi.getAllUserInfoBatch(voList.stream().map(AttendanceGroupAdminVo::getUserId).collect(Collectors.toList()), null); + + if (200 != userVoList.getCode() || CollectionUtil.isEmpty(userVoList.getData())) { + return dataMap; + } + Map> listMap = userVoList.getData().stream().collect(Collectors.groupingBy(UserBoundVO::getId)); + voList.forEach(vo -> { + List list = listMap.get(vo.getUserId()); + if (CollectionUtil.isNotEmpty(list)) { + if (!userIds.contains(vo.getUserId()) && null != selfCollect && selfCollect.contains(vo.getUserId())) { + // 查出借调过来的管理员 ,且在名字后拼接(临时借调) + vo.setRealName(CollectionUtil.get(list, 0).getUserName() + "(临时借调)"); + } else { + vo.setRealName(CollectionUtil.get(list, 0).getUserName()); + } + + } + }); + + dataMap.put("managerList", voList); + + return dataMap; + } + + /** + * 获取管理员,包含当前组管理员、上级子组权限管理员、超级管理员,超级管理员groupId为-1 + * + * @param groupIds 管理员id集合 + * @return Map> key为 groupId value为管理员userId集合 + */ + @Override + public Map> queryUserForCurrAndUpChildAndSuper(List groupIds) { + List attendancePermissionVos = attendanceManagerPermissionMapper.queryUserForCurrAndUpChildAndSuper(groupIds); + return attendancePermissionVos.stream().collect(Collectors.groupingBy(AttendancePermissionVo::getGroupId, Collectors.mapping(AttendancePermissionVo::getUserId, Collectors.toList()))); + } + + /** + * 根据指定权限获取管理员,包含当前组管理员、上级子组权限管理员、超级管理员,超级管理员groupId为-1 + * + * @param groupIds 管理员id集合 + * @return Map> key为 groupId value为管理员userId集合 + */ + @Override + public Map> queryPermissionBySpecify(List groupIds, List parentCodes, List childCodes) { + List attendancePermissionVos = attendanceManagerPermissionMapper.queryPermissionBySpecify(groupIds, parentCodes, childCodes); + return attendancePermissionVos.stream().collect(Collectors.groupingBy(AttendancePermissionVo::getGroupId, Collectors.mapping(AttendancePermissionVo::getUserId, Collectors.toList()))); + } + + /** + * 获取当前登录用户权限 + * + * @param groupId 考勤组id + * @return CurUserPermissionVo + */ + @Override + public CurUserPermissionVo getByUserId(String groupId) { + + + String loginUserId = UserProvider.getLoginUserId(); + CurUserPermissionVo curUserPermissionVo = new CurUserPermissionVo(); + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + curUserPermissionVo.setIsAdmin(false); + /* 考勤组超级管理员 && 系统超级管理员*/ + if (!isSuperAdmin(loginUserId) && !sysAdmin) { + + List curPermissions = attendanceManagerPermissionMapper.getByUserId(UserProvider.getLoginUserId(), PermissionTypeEnum.CUR.getCode(), PermissionTypeEnum.CUR.getCode(), groupId); +// List childPermissions = attendanceManagerPermissionMapper.getByUserId(UserProvider.getLoginUserId(), PermissionTypeEnum.CHILD.getCode(), PermissionTypeEnum.CHILD.getCode(), groupId); + curUserPermissionVo.setCurPermissions(curPermissions); +// curUserPermissionVo.setChildPermissions(childPermissions); + return curUserPermissionVo; + } + /* 考勤超级管理员*/ + curUserPermissionVo.setIsAdmin(true); + Set permissionTemplate = PermissionUtil.getPermissionTemplate( + AttendancePermissionConstant.BALANCE_MANAGER, + AttendancePermissionConstant.USER_MANAGER, + AttendancePermissionConstant.ATTENDANCE_CONFIGURATION, + AttendancePermissionConstant.PERMISSION_CONFIGURATION + ); + curUserPermissionVo.setGroupPermissions(permissionTemplate); + /* 查询当前组权限*/ + QueryWrapper curGroupPermissionQuery = new QueryWrapper<>(); + curGroupPermissionQuery.lambda() + .eq(PermissionDict::getModuleType, PermissionTypeEnum.CUR.getCode()) + .eq(PermissionDict::getDeleteMark, 0); + List curPermissions = permissionDictMapper.selectList(curGroupPermissionQuery); + curUserPermissionVo.setCurPermissions(curPermissions); + /* 子考勤组权限*/ +// QueryWrapper childGroupPermissionQuery = new QueryWrapper<>(); +// childGroupPermissionQuery.lambda() +// .eq(PermissionDict::getModuleType, PermissionTypeEnum.CHILD.getCode()) +// .eq(PermissionDict::getDeleteMark, 0); +// List childPermissions = permissionDictMapper.selectList(childGroupPermissionQuery); +// curUserPermissionVo.setChildPermissions(childPermissions); + return curUserPermissionVo; + } + + @Override + public Boolean isSuperAdmin(String userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceManagerPermission::getType, PermissionTypeEnum.SUPER_ADMIN.getCode()) + .eq(AttendanceManagerPermission::getUserId, userId) + .eq(AttendanceManagerPermission::getDeleteMark, 0); + AttendanceManagerPermission admin = attendanceManagerPermissionMapper.selectOne(queryWrapper); + return admin != null; + } + + @Override + public AttendanceManagerDetailVo adminDetail(String groupId, String userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceManagerPermission::getGroupId, groupId) + .eq(AttendanceManagerPermission::getUserId, userId); + List permissionList = attendanceManagerPermissionMapper.selectList(queryWrapper); + if (CollectionUtil.isEmpty(permissionList)) { + return new AttendanceManagerDetailVo(); + } + List permissionIds = permissionList.stream().map(AttendanceManagerPermission::getPermissionId).collect(Collectors.toList()); + AttendanceManagerDetailVo attendanceManagerDetailVo = new AttendanceManagerDetailVo(); + attendanceManagerDetailVo.setUserId(userId); +// UserEntity userEntity = userApi.getInfoById(userId); + ActionResult usersBound = v2UserApi.getUsersBound(userId, null); + if (200 == usersBound.getCode() && null != usersBound.getData()) { + attendanceManagerDetailVo.setRealName(usersBound.getData().getUserName()); + } + attendanceManagerDetailVo.setPermissionIds(permissionIds); + return attendanceManagerDetailVo; + } + + @Override + public CurUserPermissionVo queryPermissionByUserId(String userId, List levelCodeList, String permissionName) { + List groupIdList = getLevelIds(levelCodeList); + /* 根据考勤组id集合查询管理员权限*/ + if (CollectionUtil.isEmpty(groupIdList)) { + return new CurUserPermissionVo(); + } + /* 循环查找我管理的父考勤组子集权限 */ + for (String groupId : groupIdList) { + CurUserPermissionVo permissionVo = attendanceManagerPermissionMapper.queryByGroupId(groupId, userId, permissionName); + if (permissionVo != null) { + return permissionVo; + } + } + + return new CurUserPermissionVo(); + } + + @Override + public Boolean isViewPermission(String groupId) { + String loginUserId = UserProvider.getLoginUserId(); + Boolean superAdmin = isSuperAdmin(loginUserId); + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + if (superAdmin || sysAdmin) { + return true; + } + // v1.8.1 获取当前考勤组的管理员列表 +// Boolean x = getManager(groupId, loginUserId); + if (Boolean.TRUE.equals(getManagerView(groupId, loginUserId, false))) { + return true; + } + // 不是当前考勤组的管理员,那么验证是否是上级的管理员且具备查看权限 + AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId); + List levelCodeList = new ArrayList<>(); + levelCodeList.add(attendanceGroup.getLevelCode()); + List levelIds = getLevelIds(levelCodeList); + if (CollectionUtil.isEmpty(levelIds)) { + return false; + } + boolean x = false; + for (String levelId : levelIds) { + if (Boolean.TRUE.equals(getManagerView(levelId, loginUserId, true))) { + x = true; + break; + } + } + return x; + // 当前考勤组的权限列表 +// List permissionDictList = getPermissions(groupId); +// if (CollectionUtil.isNotEmpty(permissionDictList)) { +// /** 判断是否有查看权限*/ +// List pList = permissionDictList.stream().map(PermissionDict::getName).collect(Collectors.toList()); +// return pList.contains(AttendancePermissionConstant.VIEW); +// } +// /** 如果没有继续向上查找*/ +// +// CurUserPermissionVo curUserPermissionVo = queryPermissionByUserId(loginUserId, levelCodeList, null); +// if (curUserPermissionVo == null || StrUtil.isBlank(curUserPermissionVo.getAllPermissions())) { +// return false; +// } +// +// return curUserPermissionVo.getAllPermissions().contains(AttendancePermissionConstant.VIEW); + } + + @NotNull + private Boolean getManagerView(String groupId, String loginUserId, boolean child) { + Map stringObjectMap = listGroupAdmin(groupId); + List managerList = JsonUtil.getJsonToList(stringObjectMap.get("managerList"), AttendanceGroupAdminVo.class); + if (CollectionUtil.isEmpty(managerList)) { + return false; + } + AttendanceGroupAdminVo attendanceGroupAdminVo = managerList.stream().filter(item -> item.getUserId().equals(loginUserId)).findFirst().orElse(null); + if (child) { + return null != attendanceGroupAdminVo && attendanceGroupAdminVo.getChildPermissionJson().contains(AttendancePermissionConstant.VIEW); + } else { + return null != attendanceGroupAdminVo && attendanceGroupAdminVo.getCurPermissionJson().contains(AttendancePermissionConstant.VIEW); + } + } + + /** + * 获取考勤组权限 + * + * @param groupId 考勤组 + * @param loginUserId 用户 + */ + private String getManagerDetail(String groupId, String loginUserId) { + Map stringObjectMap = listGroupAdmin(groupId); + List managerList = JsonUtil.getJsonToList(stringObjectMap.get("managerList"), AttendanceGroupAdminVo.class); + if (CollectionUtil.isEmpty(managerList)) { + return null; + } + AttendanceGroupAdminVo attendanceGroupAdminVo = managerList.stream().filter(item -> item.getUserId().equals(loginUserId)).findFirst().orElse(null); + if (null == attendanceGroupAdminVo) { + return null; + } + return attendanceGroupAdminVo.getChildPermissionJson(); + } + + @Override + public ActionPermissionVo getActionPermission(String groupId) { + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + String loginUserId = UserProvider.getLoginUserId(); + Boolean superAdmin = isSuperAdmin(loginUserId); + if (superAdmin || sysAdmin) { + /* 系统超级管路||考勤超级管理员*/ + ActionPermissionVo actionPermissionVo = new ActionPermissionVo(); + actionPermissionVo.setPermissionConfig(true); + actionPermissionVo.setBalanceManager(true); + actionPermissionVo.setUserManager(true); + actionPermissionVo.setScheduling(true); + actionPermissionVo.setAttendanceResult(true); + actionPermissionVo.setBindingOrg(true); + actionPermissionVo.setLock(true); + /* 自定义考勤配置权限*/ + ConfigPermissionVo configPermissionVo = new ConfigPermissionVo(); + configPermissionVo.setBase(true); + configPermissionVo.setAttendancePoints(true); + configPermissionVo.setAttendanceClass(true); + configPermissionVo.setLeaveType(true); + configPermissionVo.setFestival(true); + configPermissionVo.setHoliday(true); + actionPermissionVo.setConfigPermissionVo(configPermissionVo); + return actionPermissionVo; + } + + + UserPermissions permissionsNew = getPermissionsNew(groupId); + if (permissionsNew.isParent()) { + return getActionPermission(permissionsNew.getPermissions(), PermissionTypeEnum.CUR.getCode()); + } else { + return getActionPermission(permissionsNew.getPermissions(), PermissionTypeEnum.CHILD.getCode()); + } + +// List permissions = getPermissions(groupId); +// if (CollectionUtil.isEmpty(permissions)) { +// /** 如果没有权限则继续找是否是上级考勤组管理员有子考勤组的操作权限*/ +// AttendanceGroup group = attendanceGroupMapper.selectById(groupId); +// List levelCodeList = new ArrayList<>(); +// levelCodeList.add(group.getLevelCode()); +// CurUserPermissionVo curUserPermissionVo = this.queryPermissionByUserId(loginUserId, levelCodeList, null); +//// List parentGroupPermissions = curUserPermissionVos.stream().map(CurUserPermissionVo::getAllPermissions).collect(Collectors.toList()); +// ActionPermissionVo actionPermission = getActionPermission(curUserPermissionVo.getAllPermissions(), PermissionTypeEnum.CHILD.getCode()); +// return actionPermission; +// } +// ActionPermissionVo actionPermission = getActionPermission(CollectionUtil.join(permissions, ","), PermissionTypeEnum.CUR.getCode()); +// return actionPermission; + + } + + /** + * 是否有全局设置的权限 + * + * @return Boolean + */ + @Override + public Boolean isGlobalSetting() { + String loginUserId = UserProvider.getLoginUserId(); + Boolean superAdmin = isSuperAdmin(loginUserId); + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + return superAdmin || sysAdmin; + } + + /** + * 是否是考勤组管理员 + * + * @return Boolean + */ + @Override + public ManagerPermissionVo isManager() { + String loginUserId = UserProvider.getLoginUserId(); + Boolean superAdmin = isSuperAdmin(loginUserId); + Boolean sysAdmin = userProvider.get().getIsAdministrator(); + ManagerPermissionVo permissionVo = new ManagerPermissionVo(); + if (sysAdmin || superAdmin) { + permissionVo.setIsManager(true); + permissionVo.setScheduling(true); + permissionVo.setUserManager(true); + return permissionVo; + } + + /* 获取当前考勤组权限*/ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceManagerPermission::getDeleteMark, 0) + .eq(AttendanceManagerPermission::getUserId, loginUserId); + List attendanceManagerPermissionList = attendanceManagerPermissionMapper.selectList(queryWrapper); + + permissionVo.setIsManager(CollectionUtil.isNotEmpty(attendanceManagerPermissionList)); + /* 是否有排班权限*/ + if (CollectionUtil.isEmpty(attendanceManagerPermissionList)) { + permissionVo.setScheduling(false); + permissionVo.setUserManager(false); + return permissionVo; + } + List permissionIds = attendanceManagerPermissionList.stream().map(AttendanceManagerPermission::getPermissionId).collect(Collectors.toList()); + QueryWrapper permissionDictQueryWrapper = new QueryWrapper<>(); + permissionDictQueryWrapper.lambda() + .in(PermissionDict::getId, permissionIds) + .eq(PermissionDict::getDeleteMark, 0); + List permissionDictList = permissionDictMapper.selectList(permissionDictQueryWrapper); + if (CollectionUtil.isNotEmpty(permissionDictList)) { + /* 如果你是管理员并且有排班权限*/ + List permissionNames = permissionDictList.stream().map(PermissionDict::getName).collect(Collectors.toList()); + permissionVo.setScheduling(permissionNames.contains(AttendancePermissionConstant.SCHEDULING) + || permissionNames.contains(AttendancePermissionConstant.MANAGER)); + + permissionVo.setUserManager(permissionNames.contains(AttendancePermissionConstant.USER_MANAGER) + || permissionNames.contains(AttendancePermissionConstant.MANAGER)); + + return permissionVo; + } + return permissionVo; + } + + /** + * 获取考勤组审批设置 + * + * @param groupId 考勤组id + * @param type 审批类型 1.常规补卡审批 2.调整出勤结果审批 3.外勤审批 4.请假审批 5.加班审批 + * @return ApprovalSettingVo + */ + @Override + public ApprovalSettingVo getApprovalSettingInfo(String groupId, Integer type) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceApprovalSetting::getGroupId, groupId) + .eq(AttendanceApprovalSetting::getType, type); + AttendanceApprovalSetting attendanceApprovalSetting = attendanceApprovalSettingMapper.selectOne(queryWrapper); + return JsonUtil.getJsonToBean(attendanceApprovalSetting, ApprovalSettingVo.class); + } + + /** + * 根据用户id集合查询所属的权限-查询所有权限 + * @param userIds 用户id集合 + * @return FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO + */ + @Override + public List queryPermissionListByUserIds(List userIds) { + //查看所有权限 + QueryWrapper permissionDictQueryWrapper = new QueryWrapper<>(); + List permissionDictList = permissionDictMapper.selectList(permissionDictQueryWrapper); + //过滤当前考勤组和子考勤组权限 + permissionDictList = permissionDictList.stream().filter(permission -> !"0".equals(permission.getParent())).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(permissionDictList)) { + return new ArrayList<>(); + } + + //顶级权限 + List permissionIdList = permissionDictList.stream().map(PermissionDict::getId).collect(Collectors.toList()); + List rootPermissionList = permissionDictList.stream().filter(permission -> { + return !permissionIdList.contains(permission.getParent()); + }).collect(Collectors.toList()); + + Map> permissionGroupMap = rootPermissionList.stream().collect(Collectors.groupingBy(PermissionDict::getName)); + + + //管理权限 + PermissionDict managerPermission = permissionDictList.stream().filter(permission -> permission.getName().equals(AttendancePermissionConstant.MANAGER)).findFirst().get(); + + // 所属权限查询 + QueryWrapper managerPermissionQueryWrapper = new QueryWrapper<>(); + managerPermissionQueryWrapper.lambda() + .in(AttendanceManagerPermission::getUserId, userIds) + .ne(AttendanceManagerPermission::getType, 0); + List attendanceManagerPermissionList = attendanceManagerPermissionMapper.selectList(managerPermissionQueryWrapper); + if (CollectionUtil.isEmpty(attendanceManagerPermissionList)) { + return new ArrayList<>(); + } + List managerPermissionIdList = attendanceManagerPermissionList.stream().map(AttendanceManagerPermission::getPermissionId).collect(Collectors.toList()); + List voList = new ArrayList<>(); + + Set permissionKeys = permissionGroupMap.keySet(); + for (String permissionKey : permissionKeys) { + FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO vo = new FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO(); + List permissionDicts = permissionGroupMap.get(permissionKey); + PermissionDict permissionDict = permissionDicts.get(0); + vo.setMenuName(permissionDict.getName()); + List childPermissionList = permissionDictList.stream().filter(permission -> { + return permission.getParent().equals(permissionDict.getId()); + }).collect(Collectors.toList()); + boolean hasChild = CollectionUtil.isNotEmpty(childPermissionList); + if (hasChild) { + //获取该配置的子权限 + List chilList = permissionDictList.stream().filter(permission -> { + return permission.getParent().equals(permissionDict.getId()); + }).collect(Collectors.toList()); + List childVoList = new ArrayList<>(); + for (PermissionDict dict : chilList) { + FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO childVo = new FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO(); + childVo.setMenuName(dict.getName()); + childVo.setParentId(permissionDict.getId()); + childVo.setChecked(managerPermissionIdList.contains(dict.getId()) || managerPermissionIdList.contains(managerPermission.getId())); + childVoList.add(childVo); + } + vo.setChildrenList(childVoList); + long count = childVoList.stream().filter(FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO::getChecked).count(); + vo.setChecked(count > 0); + voList.add(vo); + continue; + } + List hasPermissionIds = permissionDicts.stream().map(PermissionDict::getId).collect(Collectors.toList()); + Collection intersection = CollectionUtil.intersection(managerPermissionIdList, hasPermissionIds); + vo.setChecked(CollectionUtil.isNotEmpty(intersection)); + if (managerEmbodyPermissions.contains(permissionDict.getName()) && managerPermissionIdList.contains(managerPermission.getId())) { + vo.setChecked(true); + } + voList.add(vo); + } + return voList; + } + + @Override + public AttendanceTeamSetVo getTeamSet(GroupFilterDto dto) { + AttendanceGroup attendanceGroup = attendanceGroupService.getById(dto.getGroupId()); + Assert.notNull(attendanceGroup, "考勤组不存在"); + AttendanceTeamSetVo teamSet = AttendanceTeamSetVo.builder().build(); + //获取考勤组负责人 + teamSet.setManagerId(attendanceGroup.getManagerId()); + UserEntity managerUserInfo = v2UserApi.getEntityByUserId(attendanceGroup.getManagerId()); + teamSet.setManagerName(Objects.nonNull(managerUserInfo) ? managerUserInfo.getRealName() : ""); + //获取考勤组班制类型 + AttendanceShiftSettingVo shiftSettingVo = shiftSettingService.findByGroupId(dto.getGroupId()); + if (Objects.nonNull(shiftSettingVo)) { + teamSet.setSystemType(shiftSettingVo.getSystemType()); + } + //获取考勤组月报通知 + teamSet.setMonthNotice(attendanceGroup.getMonthlyReportNotice()); + return teamSet; + } + + @Override + @Transactional + public Boolean setMonthNotice(GroupFilterDto dto) { + AttendanceGroup attendanceGroup = attendanceGroupService.getById(dto.getGroupId()); + Assert.notNull(attendanceGroup, "考勤组不存在"); + attendanceGroup.setMonthlyReportNotice(attendanceGroup.getMonthlyReportNotice().equals(1) ? 0 : 1); + return attendanceGroupService.updateById(attendanceGroup); + } + + private ActionPermissionVo getActionPermission(String permissionNames, Integer type) { + if (StrUtil.isBlank(permissionNames)) { + return new ActionPermissionVo(); + } + ActionPermissionVo actionPermissionVo = new ActionPermissionVo(); + if (PermissionTypeEnum.CUR.getCode().equals(type)) { + /* 当前考勤组*/ + actionPermissionVo.setBalanceManager(permissionNames.contains(AttendancePermissionConstant.BALANCE_MANAGER)); + actionPermissionVo.setUserManager(permissionNames.contains(AttendancePermissionConstant.USER_MANAGER)); + actionPermissionVo.setScheduling(permissionNames.contains(AttendancePermissionConstant.SCHEDULING)); + actionPermissionVo.setAttendanceResult(permissionNames.contains(AttendancePermissionConstant.ATTENDANCE_RESULT)); + actionPermissionVo.setPermissionConfig(permissionNames.contains(AttendancePermissionConstant.PERMISSION_CONFIGURATION)); + actionPermissionVo.setLock(permissionNames.contains(AttendancePermissionConstant.PERMISSION_LOCKED)); + actionPermissionVo.setBindingOrg(permissionNames.contains(AttendancePermissionConstant.BINDING_ORG)); + + /* 自定义考勤组配置*/ + ConfigPermissionVo configPermissionVo = new ConfigPermissionVo(); + configPermissionVo.setBase(permissionNames.contains(AttendancePermissionConstant.BASE)); + configPermissionVo.setAttendancePoints(permissionNames.contains(AttendancePermissionConstant.ATTENDANCE_POINTS)); + configPermissionVo.setAttendanceClass(permissionNames.contains(AttendancePermissionConstant.ATTENDANCE_CLASS)); + configPermissionVo.setLeaveType(permissionNames.contains(AttendancePermissionConstant.LEAVE_TYPE)); + configPermissionVo.setFestival(permissionNames.contains(AttendancePermissionConstant.FESTIVAL)); + configPermissionVo.setHoliday(permissionNames.contains(AttendancePermissionConstant.HOLIDAY)); + actionPermissionVo.setConfigPermissionVo(configPermissionVo); + + return actionPermissionVo; + } + /* 子考勤组*/ + if (permissionNames.contains(AttendancePermissionConstant.MANAGER)) { +// actionPermissionVo.setAttendanceResult(true); + actionPermissionVo.setPermissionConfig(true); + actionPermissionVo.setUserManager(true); + actionPermissionVo.setBalanceManager(true); + actionPermissionVo.setScheduling(true); + actionPermissionVo.setBindingOrg(true); + actionPermissionVo.setLock(true); + ConfigPermissionVo configPermissionVo = new ConfigPermissionVo(); + configPermissionVo.setBase(true); + configPermissionVo.setAttendancePoints(true); + configPermissionVo.setAttendanceClass(true); + configPermissionVo.setLeaveType(true); + configPermissionVo.setFestival(true); + configPermissionVo.setHoliday(true); + actionPermissionVo.setConfigPermissionVo(configPermissionVo); + } + + actionPermissionVo.setAttendanceResult(permissionNames.contains(AttendancePermissionConstant.CHILD_ADJUST_ATTENDANCE_RESULTS)); + return actionPermissionVo; + } + + private UserPermissions getPermissionsNew(String groupId) { + UserPermissions userPermissions = new UserPermissions(); + Map stringObjectMap = listGroupAdmin(groupId); + List managerList = JsonUtil.getJsonToList(stringObjectMap.get("managerList"), AttendanceGroupAdminVo.class); + if (!CollectionUtil.isEmpty(managerList)) { + AttendanceGroupAdminVo attendanceGroupAdminVo = managerList.stream().filter(item -> item.getUserId().equals(UserProvider.getLoginUserId())).findFirst().orElse(null); + if (null != attendanceGroupAdminVo) { + userPermissions.setParent(true); + userPermissions.setPermissions(attendanceGroupAdminVo.getCurPermissionJson()); + // 是当前考勤组管理员 返回本组权限 + return userPermissions; + } + } + // 不是本组管理员,查看是否是上级的管理员且具有子集管理权限 + AttendanceGroup attendanceGroup = attendanceGroupMapper.selectById(groupId); + List levelCodeList = new ArrayList<>(); + levelCodeList.add(attendanceGroup.getLevelCode()); + List levelIds = getLevelIds(levelCodeList); + if (!CollectionUtil.isEmpty(levelIds)) { + for (String levelId : levelIds) { + // 子集权限集合 + String managerDetail = getManagerDetail(levelId, UserProvider.getLoginUserId()); + if (null != managerDetail) { + userPermissions.setPermissions(managerDetail); + return userPermissions; + } + } + } + return userPermissions; + } + + /** + * 考勤组管理员权限入库 + * + * @param permissionId 权限id + * @param groupId 考勤组id + * @param userId 用户id + * @param type 权限类型 1.当前考勤组 2.子考勤组 + */ + private void save(String permissionId, String groupId, String userId, Integer type) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceManagerPermission::getDeleteMark, 0) + .eq(AttendanceManagerPermission::getGroupId, groupId) + .eq(AttendanceManagerPermission::getPermissionId, permissionId) + .eq(AttendanceManagerPermission::getType, type) + .eq(AttendanceManagerPermission::getUserId, userId); + AttendanceManagerPermission attendanceManagerPermission = attendanceManagerPermissionMapper.selectOne(queryWrapper); + if (attendanceManagerPermission == null) { + AttendanceManagerPermission insert = new AttendanceManagerPermission(); + insert.setId(RandomUtil.uuId()); + insert.setGroupId(groupId); + insert.setUserId(userId); + insert.setPermissionId(permissionId); + insert.setType(type); + insert.setCreatorUserId(UserProvider.getLoginUserId()); + insert.setCreatorTime(new Date()); + attendanceManagerPermissionMapper.insert(insert); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceRecordServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceRecordServiceImpl.java new file mode 100644 index 0000000..17055d0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceRecordServiceImpl.java @@ -0,0 +1,643 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.attendance.mapper.AttendanceBalanceUseRecordMapper; +import jnpf.attendance.mapper.AttendanceLeaveRulesMapper; +import jnpf.attendance.mapper.AttendanceUserBalanceMapper; +import jnpf.attendance.mapper.AttendanceUserBalanceRecordMapper; +import jnpf.attendance.service.AttendanceUserBalanceRecordService; +import jnpf.attendance.service.AttendanceUserBalanceService; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceBalanceRecordEntity; +import jnpf.entity.attendance.AttendanceBalanceUseRecord; +import jnpf.entity.attendance.AttendanceLeaveRules; +import jnpf.enums.attendance.BalanceEnum; +import jnpf.model.attendance.vo.attendance.AttendanceLeaveRulesVo; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayBalance; +import jnpf.model.attendance.vo.attendance.AttendanceUserBalanceDetailVo; +import jnpf.model.attendance.vo.attendance.LeaveConsumptionDetailVo; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author + * @version V2.0 + * @description 用户余额获取记录表 Service实现类 + */ +@Service +public class AttendanceUserBalanceRecordServiceImpl extends ServiceImpl implements AttendanceUserBalanceRecordService { + @Resource + @Lazy + private AttendanceUserBalanceService attendanceUserBalanceService; + + @Resource + private AttendanceUserBalanceMapper attendanceUserBalanceMapper; + + @Resource + @Lazy + private UserProvider userProvider; + + @Resource + private AttendanceBalanceUseRecordMapper attendanceBalanceUseRecordMapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void rollbackUserBalanceRecord(List needRetirementLeave, String balanceId, String leaveName, boolean isPublicHoliday) { + UserInfo info = userProvider.get(); + List userIds = needRetirementLeave.stream().map(AttendancePublicHolidayBalance::getUserId).distinct().collect(Collectors.toList()); + // 组织存休抵扣日志 + List detailVoList = new ArrayList<>(); + // 查询用户已有的记录列表快过期的排前面 + List userBalanceRecordList = this.list(new QueryWrapper() + .lambda() + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + .and(x -> x.eq(StringUtil.isNotEmpty(balanceId), AttendanceBalanceRecordEntity::getObjectId, balanceId) + .or() + .eq(StringUtil.isEmpty(balanceId), AttendanceBalanceRecordEntity::getType, 3)) + .in(AttendanceBalanceRecordEntity::getUserId, userIds) + .orderByDesc(AttendanceBalanceRecordEntity::getExpireTime)); + boolean b = true; + // 需修改的劵 + List list = new ArrayList<>(); + if (null != userBalanceRecordList && !userBalanceRecordList.isEmpty()) { + b = false; + // 需退劵的map + Map userNeedMap = needRetirementLeave.stream().collect(Collectors.toMap(AttendancePublicHolidayBalance::getUserId, Function.identity())); + // 用户已有劵的map + Map> userMap = userBalanceRecordList.stream().collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getUserId)); + // 劵不够的用户集合 + List needList = new ArrayList<>(); + userNeedMap.forEach((userId, needBalance) -> { + BigDecimal inputTypeBalance = consumption(userId, needBalance, userMap, list, needList); + //【XX】手动发放X天/小时余额 保留2位小数 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(balanceId) + .userId(userId) + .content("【" + info.getUserName() + "】手动去除" + inputTypeBalance.setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(inputTypeBalance.negate()) + .build(); + detailVoList.add(detailVo); + }); + // 过滤出需要使用存休抵扣的集合 + List collect = needList.stream().filter(x -> x.getRetirementLeave() == 1).collect(Collectors.toList()); + if (null != balanceId && !collect.isEmpty()) { + // 还需使用存休抵扣的用户 + List needRetirementUserIds = collect.stream().map(AttendancePublicHolidayBalance::getUserId).distinct().collect(Collectors.toList()); + // 查询用户已有的记录列表快过期的排前面 + List userRetirementList = this.list(new QueryWrapper() + .lambda() + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + .eq(AttendanceBalanceRecordEntity::getType, 3) + .in(AttendanceBalanceRecordEntity::getUserId, needRetirementUserIds) + .orderByDesc(AttendanceBalanceRecordEntity::getExpireTime)); + Map> userMap1 = userRetirementList.stream().collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getUserId)); + collect.forEach(needBalance -> { + // 还需要使用存休抵扣 + BigDecimal consumption = consumption(needBalance.getUserId(), needBalance, userMap1, list, needList); + //【"事假"】使用X.X天/小时余额 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(null) + .userId(needBalance.getUserId()) + .content("【" + leaveName + "】使用" + consumption.setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(consumption.negate()) + .build(); + detailVoList.add(detailVo); + }); + } + } + // 如果没有本类型的劵但是可以使用存休抵扣 + if (b) { + List collect = needRetirementLeave.stream().filter(x -> x.getRetirementLeave() == 1).collect(Collectors.toList()); + if (null != balanceId && !collect.isEmpty()) { + // 劵不够的用户集合 + List needList = new ArrayList<>(); + // 还需使用存休抵扣的用户 + List needRetirementUserIds = collect.stream().map(AttendancePublicHolidayBalance::getUserId).distinct().collect(Collectors.toList()); + // 查询用户已有的记录列表快过期的排前面 + List userRetirementList = this.list(new QueryWrapper() + .lambda() + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + .eq(AttendanceBalanceRecordEntity::getType, 3) + .in(AttendanceBalanceRecordEntity::getUserId, needRetirementUserIds) + .orderByDesc(AttendanceBalanceRecordEntity::getExpireTime)); + Map> userMap1 = userRetirementList.stream().collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getUserId)); + collect.forEach(needBalance -> { + // 还需要使用存休抵扣 + BigDecimal consumption = consumption(needBalance.getUserId(), needBalance, userMap1, list, needList); + //【"事假"】使用X.X天/小时余额 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(null) + .userId(needBalance.getUserId()) + .content("【" + leaveName + "】使用" + consumption.setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(consumption.negate()) + .build(); + detailVoList.add(detailVo); + }); + } + } + this.updateBatchById(list); + if (!isPublicHoliday && !detailVoList.isEmpty()) { + // 消费记录 + attendanceUserBalanceMapper.batchAddBalanceDetail(detailVoList); + } + + } + + /** + * 需要转存劵的用户余额记录 (存休不能触发) + * 对应类型的劵 消费,同时生成新的劵(过期时间相同) + * + * @param ruleIds 假日类型Id(ftb_attendance_leave_rules) + * @param userIds 用户id + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void needRetirementBalance(List ruleIds, List userIds) { + // 查询用户对应类型的劵记录 (余额大于0) + List userBalanceRecordList = this.list(new QueryWrapper() + .lambda() + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + .eq(AttendanceBalanceRecordEntity::getType, 2) + .in(AttendanceBalanceRecordEntity::getObjectId, ruleIds) + .in(AttendanceBalanceRecordEntity::getUserId, userIds) + .and(x -> x.gt(AttendanceBalanceRecordEntity::getBalance, BigDecimal.ZERO))); + if (null != userBalanceRecordList && !userBalanceRecordList.isEmpty()) { + // 需修改的劵 + List addList = new ArrayList<>(); + for (AttendanceBalanceRecordEntity record : userBalanceRecordList) { + // 生成新的劵 + AttendanceBalanceRecordEntity newRecord = new AttendanceBalanceRecordEntity(); + newRecord.setUserId(record.getUserId()); + newRecord.setBalance(record.getBalance()); + newRecord.setTotal(record.getBalance()); + newRecord.setUnit(record.getUnit()); + newRecord.setType(3); + newRecord.setExpireTime(record.getExpireTime()); + addList.add(newRecord); + record.setBalance(BigDecimal.ZERO); + record.setState(1); + record.setIsOver(1); + } + // 批量使用劵 + this.updateBatchById(userBalanceRecordList); + // 批量添加劵 + this.saveBatch(addList); + } + + } + + @Override + public LeaveConsumptionDetailVo leaveSimulationConsumption(String userId, BigDecimal balance, AttendanceLeaveRulesVo userLeaveDetail) { + // 类型 + LeaveConsumptionDetailVo leaveConsumptionDetailVo = new LeaveConsumptionDetailVo(); + leaveConsumptionDetailVo.setUnconsumedBalance(balance); + // 查询用户已有的记录列表快过期的排前面 + List userBalanceRecordList = getAttendanceUserBalanceRecords(userId, 1 == userLeaveDetail.getBuiltIn() ? null : userLeaveDetail.getLeaveTypeId()); + if (null != userBalanceRecordList && !userBalanceRecordList.isEmpty()) { + simulationConsumption(balance, userBalanceRecordList, 1 == userLeaveDetail.getBuiltIn() ? null : userLeaveDetail.getLeaveTypeId(), leaveConsumptionDetailVo, userId, userLeaveDetail); + return leaveConsumptionDetailVo; + } + // 如果入参balanceId不为空,但没有查询到假余额,需进行补偿逻辑,校验是否需要使用存休抵扣 + if (StringUtil.isNotEmpty(userLeaveDetail.getLeaveTypeId()) && 1 == userLeaveDetail.getRetirementLeave()) { + BigDecimal balanceCopy = balance; + // 该类型的假期开启存休抵扣 + balance = getRetirementLeave(balance, leaveConsumptionDetailVo, userId); + leaveConsumptionDetailVo.setUnconsumedBalance(balance); + // 查询余额表,计算剩余总余额 + BigDecimal totalBalance = getTotalBalance(userLeaveDetail.getLeaveTypeId(), userId, 1 == userLeaveDetail.getRetirementLeave()); + leaveConsumptionDetailVo.setBalance(Objects.isNull(totalBalance) ? BigDecimal.ZERO : totalBalance.subtract(balanceCopy).max(BigDecimal.ZERO)); + } + return leaveConsumptionDetailVo; + } + + /** + * 请假模拟消费 + * + * @param balance 需消费的余额 + * @param userBalanceRecordList 假对应劵信息 + * @param balanceId 假Id 存休时为空 + * @param leaveConsumptionDetailVo 返回的组装对象数据 + * @param userId 用户id + * @param userLeaveDetail 请假规则 可能为空 + **/ + private void simulationConsumption(BigDecimal balance, List userBalanceRecordList, String balanceId, LeaveConsumptionDetailVo leaveConsumptionDetailVo, String userId, AttendanceLeaveRulesVo userLeaveDetail) { + BigDecimal balanceCopy = balance; + boolean isRetirementLeave = 1 == userLeaveDetail.getRetirementLeave(); + // 需回退的余额 + // 根据过期时间排序,快过期的排前面 + balanceSort(userBalanceRecordList); + // 消费的入参类型使用的余额 + BigDecimal inputTypeBalance = BigDecimal.ZERO; + // 进行劵消费 + for (AttendanceBalanceRecordEntity record : userBalanceRecordList) { + if (balance.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + // 消费劵 + if (record.getBalance().compareTo(balance) >= 0) { + inputTypeBalance = inputTypeBalance.add(balance); + balance = BigDecimal.ZERO; + } else { + // 劵不足,消费劵 + inputTypeBalance = inputTypeBalance.add(record.getBalance()); + balance = balance.subtract(record.getBalance()); + } + } + leaveConsumptionDetailVo.setInputTypeBalance(inputTypeBalance); + if (null == balanceId) { + leaveConsumptionDetailVo.setInputTypeBalanceStr(inputTypeBalance.setScale(2, RoundingMode.HALF_UP) + "天存休"); + } else { + leaveConsumptionDetailVo.setInputTypeBalanceStr(inputTypeBalance.setScale(2, RoundingMode.HALF_UP) + "天" + userLeaveDetail.getLeaveTypeName()); + if (isRetirementLeave) { + // 该类型的假期开启存休抵扣 + balance = getRetirementLeave(balance, leaveConsumptionDetailVo, userId); + } + } + leaveConsumptionDetailVo.setUnconsumedBalance(balance); + if (balance.compareTo(BigDecimal.ZERO) > 0) { + // 未抵扣的余额大于0,则不用再次计算总的剩余总余额 + leaveConsumptionDetailVo.setBalance(BigDecimal.ZERO); + } else { + // 查询余额表,计算剩余总余额 + BigDecimal totalBalance = getTotalBalance(balanceId, userId, false); + leaveConsumptionDetailVo.setBalance(totalBalance.subtract(balanceCopy)); + } + } + + private BigDecimal getTotalBalance(String balanceId, String userId, boolean isRetirementLeave) { + return attendanceUserBalanceService.getTotalBalance(balanceId, userId, isRetirementLeave); + } + + /** + * 其他类型的假开启了存休抵扣调用 + * + * @param userId 用户id + * @param balance 需消费的余额 + * @return 剩余的未消费余额 + **/ + @NotNull + private BigDecimal getRetirementLeave(BigDecimal balance, LeaveConsumptionDetailVo leaveConsumptionDetailVo, String userId) { + if (balance.compareTo(BigDecimal.ZERO) > 0) { + // 不是存休且余额大于0,说明有劵未使用完 ,判断该类型的假是否开启了余额抵扣 + // 查询存休并进行抵扣 + List retirementLeaves = getAttendanceUserBalanceRecords(userId, null); + if (null != retirementLeaves && !retirementLeaves.isEmpty()) { + balanceSort(retirementLeaves); + BigDecimal retirementLeave = BigDecimal.ZERO; + // 进行劵消费 + for (AttendanceBalanceRecordEntity record : retirementLeaves) { + if (balance.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + // 消费劵 + if (record.getBalance().compareTo(balance) >= 0) { + retirementLeave = retirementLeave.add(balance); + balance = BigDecimal.ZERO; + } else { + // 劵不足,消费劵 + balance = balance.subtract(record.getBalance()); + retirementLeave = retirementLeave.add(record.getBalance()); + } + } + leaveConsumptionDetailVo.setRetirementLeave(retirementLeave); + leaveConsumptionDetailVo.setRetirementLeaveStr(retirementLeave.setScale(2, RoundingMode.HALF_UP) + "天存休"); + } + } + return balance; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public LeaveConsumptionDetailVo leaveConsumption(String userId, BigDecimal balance, AttendanceLeaveRulesVo userLeaveDetail, Date startTime, Date endTime, String applyId) { + LeaveConsumptionDetailVo leaveConsumptionDetailVo = new LeaveConsumptionDetailVo(); +// AttendanceLeaveRules attendanceLeaveRules = null; + // 需消费的劵 + List list = new ArrayList<>(); + // 消费的劵记录 + List useRecordList = new ArrayList<>(); + // 查询用户已有的记录列表快过期的排前面 + List userBalanceRecordList = getAttendanceUserBalanceRecords(userId, 1 == userLeaveDetail.getBuiltIn() ? null : userLeaveDetail.getLeaveTypeId()); + // 预防重复进入 + boolean flag = true; + if (null != userBalanceRecordList && !userBalanceRecordList.isEmpty()) { + flag = false; + consumptionOne(balance, userBalanceRecordList, 1 == userLeaveDetail.getBuiltIn() ? null : userLeaveDetail.getLeaveTypeId(), leaveConsumptionDetailVo, userId, userLeaveDetail, list, useRecordList, startTime, endTime, applyId); + } + // 如果入参balanceId不为空,但没有查询到假余额,需进行补偿逻辑,校验是否需要使用存休抵扣 + if (flag && StringUtil.isNotEmpty(userLeaveDetail.getLeaveTypeId()) && 1 == userLeaveDetail.getRetirementLeave()) { +// BigDecimal balanceCopy = balance; + // 该类型的假期开启存休抵扣 + balance = getRetirementLeaveOne(balance, leaveConsumptionDetailVo, userId, list, useRecordList, startTime, endTime, applyId); + leaveConsumptionDetailVo.setUnconsumedBalance(balance); + // 查询余额表,计算剩余总余额 + // 进入这里代表是其他请假类型且开启存休抵扣且自身没有余额 +// BigDecimal totalBalance = getTotalBalance(userLeaveDetail.getLeaveTypeId(), userId, 1 == userLeaveDetail.getRetirementLeave()); + leaveConsumptionDetailVo.setBalance(BigDecimal.ZERO); + } + if (flag && StringUtil.isNotEmpty(userLeaveDetail.getLeaveTypeId()) && 0 == userLeaveDetail.getRetirementLeave()) { + // 处理对应类型没有余额且不开启存休抵扣的情况 + leaveConsumptionDetailVo.setUnconsumedBalance(balance); + } + if (!list.isEmpty()) { + // 修改余额记录表 + this.updateBatchById(list); + } + // 修改用户余额表与余额日志表 + updateUserBalance(1 == userLeaveDetail.getBuiltIn() ? null : userLeaveDetail.getLeaveTypeId(), leaveConsumptionDetailVo, userLeaveDetail, userId); + // 添加使用记录表 + if (!useRecordList.isEmpty()) { + attendanceBalanceUseRecordMapper.addBatch(useRecordList); + } + return leaveConsumptionDetailVo; + } + + private void updateUserBalance(String balanceId, LeaveConsumptionDetailVo leaveConsumptionDetailVo, AttendanceLeaveRulesVo userLeaveDetail, String userId) { + List list = new ArrayList<>(); + // 重新统计用户的余额 + if (null != balanceId) { + if (leaveConsumptionDetailVo.getRetirementLeave().compareTo(BigDecimal.ZERO) > 0) { + //【"事假"】使用X.X天/小时余额 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(null) + .userId(userId) + .content("【" + userLeaveDetail.getLeaveTypeName() + "】使用" + leaveConsumptionDetailVo.getRetirementLeave().setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(leaveConsumptionDetailVo.getRetirementLeave().negate()) + .build(); + list.add(detailVo); + } + if (leaveConsumptionDetailVo.getInputTypeBalance().compareTo(BigDecimal.ZERO) > 0) { + //使用X.X天/小时余额 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(balanceId) + .userId(userId) + .content("使用" + leaveConsumptionDetailVo.getInputTypeBalance().setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(leaveConsumptionDetailVo.getInputTypeBalance().negate()) + .build(); + list.add(detailVo); + } + } else { + if (leaveConsumptionDetailVo.getInputTypeBalance().compareTo(BigDecimal.ZERO) > 0) { + //【"事假"】使用X.X天/小时余额 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(null) + .userId(userId) + .content("使用" + leaveConsumptionDetailVo.getInputTypeBalance().setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(leaveConsumptionDetailVo.getInputTypeBalance().negate()) + .build(); + list.add(detailVo); + } + } + if (!list.isEmpty()) { + // 记录余额使用日志 + attendanceUserBalanceMapper.batchAddBalanceDetail(list); + } + } + + + /** + * 查询用户已有的假期类型记录列表 + * 查询存休时只用关注type为3的记录(包含了加班转的存休) + * @param userId 用户id + * @param balanceId 假日类型Id(ftb_attendance_leave_rules)(存休时为空) + * @return 劵列表 + */ + private List getAttendanceUserBalanceRecords(String userId, String balanceId) { + return this.list(new QueryWrapper() + .lambda() + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + .and(x -> x.eq(StringUtil.isNotEmpty(balanceId), AttendanceBalanceRecordEntity::getObjectId, balanceId) + .or() + .eq(StringUtil.isEmpty(balanceId), AttendanceBalanceRecordEntity::getType, 3)) + .eq(AttendanceBalanceRecordEntity::getUserId, userId)); + } + + /** + * 消费一个人的劵 + * + * @param userBalanceRecordList 劵列表 + */ + private void consumptionOne(BigDecimal balance, List userBalanceRecordList, String balanceId, LeaveConsumptionDetailVo leaveConsumptionDetailVo, String userId, AttendanceLeaveRulesVo userLeaveDetail, List list, List useRecordList, Date startTime, Date endTime, String applyId) { + balanceSort(userBalanceRecordList); + // 需回退的余额 + // 根据过期时间排序,快过期的排前面 + BigDecimal balanceCopy = balance; + boolean isRetirementLeave = null != userLeaveDetail && 1 == userLeaveDetail.getRetirementLeave(); + // 消费的入参类型使用的余额 + BigDecimal inputTypeBalance = BigDecimal.ZERO; + // 需回退的余额 + // 根据过期时间排序,快过期的排前面 + // 进行劵消费 + for (AttendanceBalanceRecordEntity record : userBalanceRecordList) { + if (balance.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + // 本张劵消费余额 + BigDecimal quota = new BigDecimal("0"); + // 消费劵 + if (record.getBalance().compareTo(balance) >= 0) { + quota = balance; + inputTypeBalance = inputTypeBalance.add(balance); + record.setBalance(record.getBalance().subtract(balance)); + balance = BigDecimal.ZERO; + } else { + // 劵不足,消费劵 + quota = record.getBalance(); + inputTypeBalance = inputTypeBalance.add(record.getBalance()); + balance = balance.subtract(record.getBalance()); + record.setBalance(BigDecimal.ZERO); + } + // 标记已使用完 + if (record.getBalance().compareTo(BigDecimal.ZERO) <= 0) { + record.setIsOver(1); + } + // 更新劵 + list.add(record); + // 使用记录 + AttendanceBalanceUseRecord useRecord = AttendanceBalanceUseRecord.builder() + .balanceId(record.getId()) + .quota(quota) + .unit(BalanceEnum.BALANCE_UNIT_T.getCode()) + .useType(0) + .objectId(applyId) + .startTime(startTime) + .endTime(endTime) + .lock(BalanceEnum.BALANCE_LOCK_YXF.getCode()) + .build(); + useRecord.setId(FtbUtil.getId()); + useRecordList.add(useRecord); + } + leaveConsumptionDetailVo.setInputTypeBalance(inputTypeBalance); + if (null == balanceId) { + leaveConsumptionDetailVo.setInputTypeBalanceStr(inputTypeBalance + "天存休"); + } else { + leaveConsumptionDetailVo.setInputTypeBalanceStr(inputTypeBalance + "天" + userLeaveDetail.getLeaveTypeName()); + if (isRetirementLeave) { + // 该类型的假期开启存休抵扣 + balance = getRetirementLeaveOne(balance, leaveConsumptionDetailVo, userId, list, useRecordList, startTime, endTime, applyId); + } + } + leaveConsumptionDetailVo.setUnconsumedBalance(balance); + if (balance.compareTo(BigDecimal.ZERO) > 0) { + // 未抵扣的余额大于0,则不用再次计算总的剩余总余额 + leaveConsumptionDetailVo.setBalance(BigDecimal.ZERO); + } else { + // 查询余额表,计算剩余总余额 + BigDecimal totalBalance = getTotalBalance(balanceId, userId, false); + leaveConsumptionDetailVo.setBalance(Objects.isNull(totalBalance) ? BigDecimal.ZERO : totalBalance.subtract(balanceCopy).max(BigDecimal.ZERO)); + } + } + + @NotNull + private BigDecimal getRetirementLeaveOne(BigDecimal balance, LeaveConsumptionDetailVo leaveConsumptionDetailVo, String userId, List list, List useRecordList, Date startTime, Date endTime, String applyId) { + if (balance.compareTo(BigDecimal.ZERO) > 0) { + // 不是存休且余额大于0,说明有劵未使用完 ,判断该类型的假是否开启了余额抵扣 + // 查询存休并进行抵扣 + List retirementLeaveS = getAttendanceUserBalanceRecords(userId, null); + if (null != retirementLeaveS && !retirementLeaveS.isEmpty()) { + balanceSort(retirementLeaveS); + BigDecimal retirementLeave = BigDecimal.ZERO; + // 进行劵消费 + for (AttendanceBalanceRecordEntity record : retirementLeaveS) { + if (balance.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + // 本张劵消费余额 + BigDecimal quota = new BigDecimal("0"); + // 消费劵 + if (record.getBalance().compareTo(balance) >= 0) { + quota = balance; + retirementLeave = retirementLeave.add(balance); + record.setBalance(record.getBalance().subtract(balance)); + balance = BigDecimal.ZERO; + } else { + // 劵不足,消费劵 + quota = record.getBalance(); + balance = balance.subtract(record.getBalance()); + retirementLeave = retirementLeave.add(record.getBalance()); + record.setBalance(BigDecimal.ZERO); + } + // 标记已使用完 + if (record.getBalance().compareTo(BigDecimal.ZERO) <= 0) { + record.setIsOver(1); + } + // 更新劵 + list.add(record); + // 使用记录 + AttendanceBalanceUseRecord useRecord = AttendanceBalanceUseRecord.builder() + .balanceId(record.getId()) + .quota(quota) + .unit(BalanceEnum.BALANCE_UNIT_T.getCode()) + .useType(0) + .objectId(applyId) + .startTime(startTime) + .endTime(endTime) + .lock(BalanceEnum.BALANCE_LOCK_YXF.getCode()) + .build(); + useRecord.setId(FtbUtil.getId()); + useRecordList.add(useRecord); + } + leaveConsumptionDetailVo.setRetirementLeave(retirementLeave); + leaveConsumptionDetailVo.setRetirementLeaveStr(retirementLeave + "天存休"); + } + } + return balance; + } + + /** + * 劵排序 快过期的排前面 + * + * @param userBalanceRecordList 劵列表 + */ + private static void balanceSort(List userBalanceRecordList) { + userBalanceRecordList.sort((o1, o2) -> { + if (o1.getExpireTime() == null && o2.getExpireTime() == null) { + return 0; + } + if (o1.getExpireTime() == null) { + return 1; // null值放最后 + } + if (o2.getExpireTime() == null) { + return -1; // null值放最后 + } + return o1.getExpireTime().before(o2.getExpireTime()) ? -1 : 1; + }); + } + + /** + * 劵消费 + * + * @param userId 用户id + * @param balancevo 需退的劵 + * @param userMap 劵的map + * @param list 修改的劵 + * @param needList 劵不够的用户集合 + * @return 抵扣的劵余额 + */ + private static BigDecimal consumption(String userId, AttendancePublicHolidayBalance balancevo, Map> userMap, List list, List needList) { + BigDecimal balance = balancevo.getBalance(); + BigDecimal inputTypeBalance = BigDecimal.ZERO; + // 需回退的余额 + List userBalanceRecord = userMap.get(userId); + if (null != userBalanceRecord && !userBalanceRecord.isEmpty()) { + // 根据过期时间排序,快过期的排前面 + balanceSort(userBalanceRecord); + // 进行劵消费 + for (AttendanceBalanceRecordEntity record : userBalanceRecord) { + if (balance.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + // 消费劵 + if (record.getBalance().compareTo(balance) >= 0) { + inputTypeBalance = inputTypeBalance.add(balance); + record.setBalance(record.getBalance().subtract(balance)); + balance = BigDecimal.ZERO; + } else { + // 劵不足,消费劵 + inputTypeBalance = inputTypeBalance.add(record.getBalance()); + balance = balance.subtract(record.getBalance()); + record.setBalance(BigDecimal.ZERO); + } + // 标记已使用完 + if (record.getBalance().compareTo(BigDecimal.ZERO) <= 0) { + record.setIsOver(1); + } + // 更新劵 + list.add(record); + } + } + if (balance.compareTo(BigDecimal.ZERO) > 0) { + // 劵不够,记录该用户 + AttendancePublicHolidayBalance balance1 = new AttendancePublicHolidayBalance(); + balance1.setUserId(userId); + balance1.setBalance(balance); + balance1.setRetirementLeave(balancevo.getRetirementLeave()); + needList.add(balance1); + } + return inputTypeBalance; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceServiceImpl.java new file mode 100644 index 0000000..adb9a03 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserBalanceServiceImpl.java @@ -0,0 +1,706 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.cloud.commons.lang.StringUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jnpf.attendance.mapper.*; +import jnpf.attendance.service.*; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PaginationVO; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.*; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.HandleException; +import jnpf.file.FileUploadApi; +import jnpf.model.attendance.dto.AttendanceUserBalanceDto; +import jnpf.model.attendance.dto.AttendanceUserBalanceListQueryDto; +import jnpf.model.attendance.dto.OrgTeamDto; +import jnpf.model.attendance.vo.ClockInVo; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.ExpiresTimeUtil; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author panpan + */ +@Service +@Slf4j +public class AttendanceUserBalanceServiceImpl implements AttendanceUserBalanceService { + + @Resource + private AttendanceUserBalanceMapper attendanceUserBalanceMapper; + + @Resource + @Lazy + private RuleScopeUtil ruleScopeUtil; + + @Autowired + private AttendanceUserService attendanceUserService; + + @Resource + private AttendanceLeaveRulesMapper attendanceLeaveRulesMapper; + + @Resource + private AttendanceLeaveGrantSettingMapper attendanceLeaveGrantSettingMapper; + + @Resource + private AttendanceClockInMapper attendanceClockInMapper; + + @Resource + private EnableBalanceService enableBalanceService; + + @Resource + @Lazy + private V2UserApi v2UserApi; + + @Resource + @Lazy + private UserProvider userProvider; + + @Resource + @Lazy + private FileUploadApi fileUploadApi; + + @Resource + @Lazy + private AttendanceUserBalanceRecordService attendanceUserBalanceRecordService; + + @Resource + @Lazy + private ExpiresTimeUtil expiresTimeUtil; + + @Autowired + private AttendanceBaseSettingService attendanceBaseSettingService; + + @Resource + @Lazy + private AttendanceLeaveTypeService attendanceLeaveTypeService; + + @Autowired + @Lazy + private AttendanceGroupService attendanceGroupService; + + @Autowired + private CustomTenantUtil tenantUtil; + + @Override + public AttendanceUserBalanceListVo list(AttendanceUserBalanceListQueryDto queryDto) throws HandleException { + AttendanceUserBalanceListVo attendanceUserBalanceListVo = new AttendanceUserBalanceListVo(); + List userIds = null; + List gIds = Optional.ofNullable(queryDto.getOrgTeamDtoList()) + .orElse(Collections.emptyList()) + .stream() + .filter(Objects::nonNull) + .map(OrgTeamDto::getOrganizeId) + .filter(Objects::nonNull) + .filter(orgId -> !orgId.isEmpty()) + .collect(Collectors.toList()); + List list1 = attendanceGroupService.list(new LambdaQueryWrapper() + .in(AttendanceGroup::getId, gIds)); + if (CollUtil.isEmpty(list1)) { + new HandleException("未找到对应考勤组,请检查考勤组是否存在"); + } + try { + Map groupMap = list1.stream().collect(Collectors.toMap(AttendanceGroup::getId, Function.identity())); + queryDto.getOrgTeamDtoList().forEach(orgTeamDto -> { + orgTeamDto.setOrgId(groupMap.get(orgTeamDto.getOrganizeId()).getOrgId()); + }); + List orgIds = queryDto.getOrgTeamDtoList().stream().map(OrgTeamDto::getOrgId).collect(Collectors.toList()); + if (CollUtil.isEmpty(orgIds)) { + new HandleException("未找到对应组织,请检查组织是否存在"); + } + // 用户及工作状态筛选 + userIds = getUserIds(queryDto, userIds,orgIds); + } catch (Exception e) { + return attendanceUserBalanceListVo; + } + // 设置动态表头 + List allType = attendanceLeaveTypeService.list(new LambdaQueryWrapper() + .eq(AttendanceLeaveType::getDeleteMark, 0) + .eq(AttendanceLeaveType::getBuiltIn, 0) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + List typeIds = allType.stream().map(AttendanceLeaveType::getId).collect(Collectors.toList()); + // 获取动态请假类型名称 + attendanceUserBalanceListVo.setLeaveNames(allType); + if (CollUtil.isEmpty(userIds) && (StringUtil.isNotEmpty(queryDto.getWorkStatus()) || StringUtil.isNotEmpty(queryDto.getIText()))) { + setPagination(queryDto, List.of(), attendanceUserBalanceListVo); + return attendanceUserBalanceListVo; + } + //通过组织ID查询考勤组ids + if (CollUtil.isEmpty(list1) || list1.isEmpty()) { + setPagination(queryDto, List.of(), attendanceUserBalanceListVo); + return attendanceUserBalanceListVo; + } + // 获取在考勤组中的用户,不包含借调用户 + Date date = new Date(); + List attendanceGroupUsers = attendanceUserService.queryByUsersAndGroupFilterSecondment(date, date, userIds, gIds); + if (CollUtil.isEmpty(attendanceGroupUsers) || attendanceGroupUsers.isEmpty()) { + return attendanceUserBalanceListVo; + } + // 组装数据 + List list = new ArrayList<>(); + // 计算分页参数 + int startIndex = (queryDto.getCurrentPage() - 1) * queryDto.getPageSize(); + int endIndex = Math.min(startIndex + queryDto.getPageSize(), attendanceGroupUsers.size()); + // 设置分页参数 + setPagination(queryDto, attendanceGroupUsers, attendanceUserBalanceListVo); + // 根据查出来的attendanceGroupUsers中的userID顺序手动分页 + attendanceGroupUsers = attendanceGroupUsers.subList(startIndex, endIndex); + // 转为有序的 List + attendanceGroupUsers.forEach(attendanceGroupUser -> { + AttendanceUserBalanceListDataVo vo = new AttendanceUserBalanceListDataVo(); + vo.setUserId(attendanceGroupUser.getUserId()); + list.add(vo); + }); + // 当前页的用户ids集合 + List listUserIds = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(listUserIds, null); + Map userMap = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userMap = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + Map finalUserMap = userMap; + list.forEach(v -> { + UserBoundVO userBoundVO = finalUserMap.get(v.getUserId()); + v.setUserName(null == userBoundVO ? "--" : userBoundVO.getUserName()); + v.setGroupName(null == userBoundVO ? "--" : userBoundVO.getOrganizeTreeName()); + v.setPostName(null == userBoundVO ? "--" : userBoundVO.getPositionName()); + v.setWorkStatus(null == userBoundVO ? "--" : userBoundVO.getWorkStatusName()); + v.setOrgId(null == userBoundVO ? "--" : userBoundVO.getOrganizeId()); + }); + // 获取员工现在拥有的假期规则列表(可能比余额表记录中拥有的多) 不需要优先级查询(里面包含没有启用、内置、不发放余额的规则) + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(listUserIds, ScopeBizType.LEAVE_RULES, ConstantUtil.NUM_TRUE, typeIds); + // 当前列表用户对应的启动中的假期规则(除内置调休)包含不发放余额的规则 + List rules = attendanceLeaveRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveRules::getDeleteMark, 0) + .eq(AttendanceLeaveRules::getState, 1) + .eq(AttendanceLeaveRules::getBuiltIn, 0) + .ne(AttendanceLeaveRules::getScopeOfAdaptation, -1) + .in(CollUtil.isNotEmpty(attendanceRuleScopes), AttendanceLeaveRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + ); + Map ruleMap = rules.stream().collect(Collectors.toMap(AttendanceLeaveRules::getId, Function.identity())); + // 启动中的假期规则Id集合(除内置调休)包含不发放余额的规则 + List startRuleIds = rules.stream().map(AttendanceLeaveRules::getId).distinct().collect(Collectors.toList()); + // 过滤出启动中的(除内置调休)包含不发放余额的规则 + List scopeList = attendanceRuleScopes.stream().filter(v -> startRuleIds.contains(v.getRuleId())).collect(Collectors.toList()); + // 根据用户Id进行分组 value 包含不发放余额的规则 + Map> userRulesMap = scopeList.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getUserId)); + // 查询所有类型且有余额对应的劵记录 + List records = attendanceUserBalanceRecordService.list(new QueryWrapper() + .lambda() + .and(x1 -> + x1.and(x -> + x.eq(AttendanceBalanceRecordEntity::getType, 2) + .in(AttendanceBalanceRecordEntity::getObjectId, typeIds)) + .or() + .eq(AttendanceBalanceRecordEntity::getType, 3)) + .in(AttendanceBalanceRecordEntity::getUserId, listUserIds) + .eq(AttendanceBalanceRecordEntity::getDeleteMark, 0) + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + ); + // 先根据userId再根据objectId过滤出objectId假类型数据并分组计算余额 + Map> userBalanceMap = records.stream().collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getUserId)); + list.forEach(value -> { + // 用户相关的余额列表 + List balanceRecordList = userBalanceMap.get(value.getUserId()); + value.setBalance(BigDecimal.ZERO); + // 用户生效中的规则列表 对应的值一个扩展字段类型只能最多一条数据 + List scopes = userRulesMap.get(value.getUserId()); + // 获取该用户有的类型且发放了余额的的假期余额列表 + List typeHitIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(balanceRecordList) && CollUtil.isNotEmpty(scopes)) { + // 保留两位小数 + BigDecimal balance = balanceRecordList.stream().filter(x -> x.getType() == 3).map(AttendanceBalanceRecordEntity::getBalance).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + value.setBalance(balance.setScale(2, RoundingMode.HALF_UP)); + balanceRecordList.removeIf(x -> x.getType() == 3); + // 该用户有的类型 + List startTypeIds = scopes.stream().map(AttendanceRuleScope::getExpand).distinct().collect(Collectors.toList()); + // 有该类型且发过劵 + Map map = balanceRecordList.stream().filter(x -> startTypeIds.contains(x.getObjectId())).collect(Collectors.groupingBy(AttendanceBalanceRecordEntity::getObjectId, Collectors.reducing(BigDecimal.ZERO, AttendanceBalanceRecordEntity::getBalance, BigDecimal::add))); + typeHitIds = balanceRecordList.stream().map(AttendanceBalanceRecordEntity::getObjectId).distinct().collect(Collectors.toList()); + value.getBalanceNum().putAll(map); + } + List finalTypeHitIds = typeHitIds; + if (CollUtil.isNotEmpty(scopes)) { + scopes.stream().filter(v -> !finalTypeHitIds.contains(v.getExpand())).forEach(v -> { + AttendanceLeaveRules attendanceLeaveRules = ruleMap.get(v.getRuleId()); + if (null != attendanceLeaveRules) { + if (attendanceLeaveRules.getDistributeBalance() == 1) { + value.getBalanceNum().put(v.getExpand(), BigDecimal.ZERO); + } else { + value.getBalanceNum().put(v.getExpand(), new BigDecimal(-2)); + } + } + }); + } + }); + attendanceUserBalanceListVo.setAttendanceUserBalanceListDataVo(list); + return attendanceUserBalanceListVo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserBalance(AttendanceUserBalanceDto userBalanceDto) { + // 查询类型 + AttendanceLeaveType byId = attendanceLeaveTypeService.getOne(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveType::getId, userBalanceDto.getObjectId()) + .eq(AttendanceLeaveType::getDeleteMark, 0) + ); + // 入参为假期类型 + UserInfo info = userProvider.get(); + // 需要发劵的用户余额记录 + List balanceRecordList = new ArrayList<>(); + // 需要消费劵的用户余额记录 + List needRetirementLeave = new ArrayList<>(); + // 消费记录 + List list = new ArrayList<>(); + // 存休另算 + if (StringUtil.isEmpty(userBalanceDto.getObjectId()) || 1 == byId.getBuiltIn()) { + if (userBalanceDto.getBalance().compareTo(BigDecimal.ZERO) > 0) { + + userBalanceDto.getUserIds().forEach(userId -> { + // 发放劵 + AttendanceBalanceRecordEntity record = new AttendanceBalanceRecordEntity(); + record.setUserId(userId); + record.setType(3); + record.setBalance(userBalanceDto.getBalance()); + record.setTotal(userBalanceDto.getBalance()); + record.setUnit(2); + record.setExpireTime(null); + balanceRecordList.add(record); + // 记录用户的存休变动信息 + //【XX】手动发放X天/小时余额 保留2位小数 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .userId(userId) + .objectId(null) + .content("【" + info.getUserName() + "】手动发放" + userBalanceDto.getBalance().setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(userBalanceDto.getBalance()) + .build(); + list.add(detailVo); + }); + } else { + // 消费劵 劵记录需要在扣除的时候计算生成 + userBalanceDto.getUserIds().forEach(userId -> { + AttendancePublicHolidayBalance record = new AttendancePublicHolidayBalance(); + record.setUserId(userId); + record.setRetirementLeave(1); + record.setBalance(userBalanceDto.getBalance().negate()); + needRetirementLeave.add(record); + }); + } + } else { + // 其他类型的假 + // 如果批量修改需校验用户有没有该类型的假期规则,没有跳过发放 + // 当前列表用户对应的启动中的假期规则(除内置调休)且不包含不发放余额的规则 + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(userBalanceDto.getUserIds(), ScopeBizType.LEAVE_RULES, ConstantUtil.NUM_TRUE, Collections.singletonList(userBalanceDto.getObjectId())); + if (CollUtil.isEmpty(attendanceRuleScopes)) { + return; + } + Map> userRuleMap = attendanceRuleScopes.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getRuleId)); + // 过滤出没有删除的且启动中且发劵的规则 + List rules = attendanceLeaveRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveRules::getDeleteMark, 0) + .in(AttendanceLeaveRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + .eq(AttendanceLeaveRules::getState, 1) + .eq(AttendanceLeaveRules::getBuiltIn, 0) + .ne(AttendanceLeaveRules::getScopeOfAdaptation, -1) + .eq(AttendanceLeaveRules::getDistributeBalance, 1) + ); + if (CollUtil.isEmpty(rules)) { + return; + } + List grantSettings = attendanceLeaveGrantSettingMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveGrantSetting::getDeleteMark, 0) + .in(AttendanceLeaveGrantSetting::getLeaveRulesId, rules.stream().map(AttendanceLeaveRules::getId).collect(Collectors.toList())) + ); + Map ruleSettingMap = grantSettings.stream().collect(Collectors.toMap(AttendanceLeaveGrantSetting::getLeaveRulesId, Function.identity())); + for (AttendanceLeaveRules rule : rules) { + // 规则对应的人员信息 + List scopes = userRuleMap.get(rule.getId()); + if (CollUtil.isEmpty(scopes)) { + continue; + } + AttendanceLeaveGrantSetting attendanceLeaveGrantSetting = ruleSettingMap.get(rule.getId()); + if (null == attendanceLeaveGrantSetting) { + continue; + } + // 计算过期时间 假期只有 不过期或者指定日期 + Date expiresTime = expiresTimeUtil.getExpiresTime(0 == attendanceLeaveGrantSetting.getExpiredType() ? 0 : 1, attendanceLeaveGrantSetting.getExpiredDayNum(), null); + List userList = scopes.stream().map(AttendanceRuleScope::getUserId).collect(Collectors.toList()); + if (userBalanceDto.getBalance().compareTo(BigDecimal.ZERO) > 0) { + // 发放劵 + userList.forEach(v -> { + // 添加劵 + AttendanceBalanceRecordEntity record = new AttendanceBalanceRecordEntity(); + record.setUserId(v); + record.setType(2); + record.setObjectId(userBalanceDto.getObjectId()); + record.setBalance(userBalanceDto.getBalance()); + record.setTotal(userBalanceDto.getBalance()); + record.setUnit(2); + record.setExpireTime(expiresTime); + balanceRecordList.add(record); + // 添加使用记录 ftb_attendance_user_balance_detail + //【XX】手动发放X天/小时余额 保留1位小数 + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(userBalanceDto.getObjectId()) + .userId(v) + .content("【" + info.getUserName() + "】手动发放" + userBalanceDto.getBalance().setScale(2, RoundingMode.HALF_UP) + "天余额") + .quota(userBalanceDto.getBalance()) + .build(); + list.add(detailVo); + }); + } else { + // 消费劵 + userList.forEach(v -> { + // 消费劵 + AttendancePublicHolidayBalance record = new AttendancePublicHolidayBalance(); + record.setUserId(v); + record.setBalance(userBalanceDto.getBalance().negate()); + record.setRetirementLeave(rule.getRetirementLeave()); + needRetirementLeave.add(record); + }); + } + } + } + if (!balanceRecordList.isEmpty()) { + // 发放劵 + attendanceUserBalanceRecordService.saveBatch(balanceRecordList); + } + if (!needRetirementLeave.isEmpty()) { + // 消费劵 劵记录需要在扣除的时候计算生成 + attendanceUserBalanceRecordService.rollbackUserBalanceRecord(needRetirementLeave, StringUtil.isEmpty(userBalanceDto.getObjectId()) ? null : 1 == byId.getBuiltIn() ? null : userBalanceDto.getObjectId(), StringUtil.isEmpty(userBalanceDto.getObjectId()) ? "存休" : byId.getName(), false); + } + if (!list.isEmpty()) { + // 消费记录 仅记录生成劵的使用记录 + attendanceUserBalanceMapper.batchAddBalanceDetail(list); + } + + } + + @Override + public AttendanceUserBalanceVo getDetail(AttendanceUserBalanceDto userBalanceDto) { + // 假期类型 + if (null == userBalanceDto.getUserId()) { + userBalanceDto.setUserId(userProvider.get().getId()); + } + AttendanceUserBalanceVo bean = new AttendanceUserBalanceVo(); + List records = attendanceUserBalanceRecordService.list(new QueryWrapper() + .lambda() + .and(x -> x.eq(StringUtil.isNotEmpty(userBalanceDto.getObjectId()), AttendanceBalanceRecordEntity::getObjectId, userBalanceDto.getObjectId()) + .or() + .eq(StringUtil.isEmpty(userBalanceDto.getObjectId()), AttendanceBalanceRecordEntity::getType, 3)) + .eq(AttendanceBalanceRecordEntity::getUserId, userBalanceDto.getUserId()) + .eq(AttendanceBalanceRecordEntity::getDeleteMark, 0) + .eq(AttendanceBalanceRecordEntity::getState, 0) + .eq(AttendanceBalanceRecordEntity::getIsOver, 0) + ); + // 计算余额 保留两位小数 + bean.setBalance(records.stream().map(AttendanceBalanceRecordEntity::getBalance).reduce(BigDecimal.ZERO, BigDecimal::add).max(new BigDecimal("0")).setScale(2, RoundingMode.HALF_UP)); + // 查询假期日志详情 + List detail = attendanceUserBalanceMapper.getDetail(userBalanceDto.getObjectId(), userBalanceDto.getUserId()); + bean.setDetail(detail); + return bean; + } + + @Override + public AttendanceUserTitleVo getTitle(AttendanceUserBalanceDto userBalanceDto) { + // 假期类型 + AttendanceUserTitleVo bean = new AttendanceUserTitleVo(); + ActionResult usersBound = v2UserApi.getUsersBound(userBalanceDto.getUserId(), userProvider.get().getTenantId()); + Assert.isTrue(usersBound.getCode().equals(200), "查询登录用户岗位信息异常"); + UserBoundInfoVO userBoundInfo = usersBound.getData(); + bean.setUserId(userBoundInfo.getUserId()); + bean.setUserName(userBoundInfo.getUserName()); + bean.setDeptName(userBoundInfo.getOrganizeName()); + bean.setPostName(userBoundInfo.getPositionName()); + bean.setSystemWorkerId(userBoundInfo.getSystemWorkerId()); + bean.setHeadIcon(fileUploadApi.getHeadIcon(UploaderUtil.uploaderImg(userBoundInfo.getHeadIcon()))); + bean.setWorkStatus(userBoundInfo.getWorkStatusName()); + // 查询动态请假类型表头 + List leaveNames = new ArrayList<>(); + // 理论上返回一个扩展字段,只对应一个规则Id,不会重复 + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(Collections.singletonList(userBalanceDto.getUserId()), ScopeBizType.LEAVE_RULES, ConstantUtil.NUM_TRUE, null); + // 先固定存休 + leaveNames.add(BalanceTitelVo.builder().name("存休").type(1).build()); + // 当前列表用户对应的启动中的假期规则(除内置调休)包含不发放余额的规则 + List rules = attendanceLeaveRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendanceLeaveRules::getDeleteMark, 0) + .eq(AttendanceLeaveRules::getState, 1) + .eq(AttendanceLeaveRules::getBuiltIn, 0) + .ne(AttendanceLeaveRules::getScopeOfAdaptation, -1) + .in(CollUtil.isNotEmpty(attendanceRuleScopes), AttendanceLeaveRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).distinct().collect(Collectors.toList())) + .orderByAsc(AttendanceLeaveRules::getCreatorTime) + ); + // 转换为动态表头格式 + if (CollUtil.isNotEmpty(rules)) { + List typeIds = rules.stream().map(AttendanceLeaveRules::getLeaveTypeId).collect(Collectors.toList()); + // 移除规则没有命中的假期类型 + Map ruleMap = rules.stream().collect(Collectors.toMap(AttendanceLeaveRules::getLeaveTypeId, Function.identity())); + // 命中且规则启用的类型 + List allType = attendanceLeaveTypeService.list(new LambdaQueryWrapper() + .in(AttendanceLeaveType::getId, typeIds) + .eq(AttendanceLeaveType::getDeleteMark, 0) + .orderByAsc(AttendanceLeaveType::getCreatorTime)); + allType.forEach(v -> { + AttendanceLeaveRules attendanceLeaveRules = ruleMap.get(v.getId()); + leaveNames.add(BalanceTitelVo.builder().id(v.getId()).type(2).name(v.getName()).distributeBalance(attendanceLeaveRules.getDistributeBalance()).build()); + }); + } + bean.setLeaveNames(leaveNames); + return bean; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addUserBalances(List needRetirementLeave) { + List list = new ArrayList<>(); + // 记录用户的存休变动信息 + // 系统已将yyyy-mm公休未排班的X.X天/小时,自动转换为存休 保留1位小数 + needRetirementLeave.forEach(v -> { + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .objectId(null) + .userId(v.getUserId()) + .content("系统已将" + v.getYearMonth() + "公休未排班的" + v.getBalance().setScale(1, RoundingMode.HALF_UP) + "天,自动转换为存休") + .quota(v.getBalance()) + .build(); + list.add(detailVo); + }); + attendanceUserBalanceMapper.batchAddBalanceDetail(list); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void rollbackUserBalance(List needRetirementLeave) { + // 记录用户的存休变动信息 + List list = new ArrayList<>(); + // 因解封yyyy-mm的封账数据,已将公休转存的XX天/小时,自动清除 保留1位小数 + needRetirementLeave.forEach(v -> { + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .userId(v.getUserId()) + .objectId("1") + .content("因解封" + v.getYearMonth() + "的封账数据,已将公休转存的" + v.getBalance().setScale(1, RoundingMode.HALF_UP) + "天,自动清除") + .quota(v.getBalance().negate()) + .build(); + list.add(detailVo); + }); + attendanceUserBalanceMapper.batchAddBalanceDetail(list); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void overTime(List overtimeInfoList, String tenantId) { + + tenantUtil.checkOutTenant(tenantId); + log.error("2.0加班触发 overTime :{}", JSONUtil.toJsonStr(overtimeInfoList)); + if (null == overtimeInfoList || overtimeInfoList.isEmpty()) { + return; + } + List groupList = overtimeInfoList.stream().map(OvertimeInfoVo::getGroupId).collect(Collectors.toList()); + Map settingMap = attendanceBaseSettingService.getEnableBaseSettingAll(groupList); + log.error("2.0加班触发 settingMap :{}", JSONUtil.toJsonStr(settingMap)); + // 根据出勤规则查询上下班 + Map map = overtimeInfoList.stream().collect(Collectors.toMap(OvertimeInfoVo::getRuleId, Function.identity())); + List clockInResultList = attendanceClockInMapper.getClockInResultByRuleList(new ArrayList<>(map.keySet())); + log.error("2.0加班触发 clockInResultList :{}", JSONUtil.toJsonStr(clockInResultList)); + ConcurrentMap> clockMap = clockInResultList.stream().collect(Collectors.groupingByConcurrent(ClockInVo::getRuleId)); + //系统已将yyyy-mm-dd工作日加班的X.X天/小时,自动转换为存休 保留1位小数 + List balanceList = new ArrayList<>(); + List recordList = new ArrayList<>(); + List enableBalanceList = new ArrayList<>(); + clockMap.forEach((k, list) -> { + if (list.size() == 2) { + ClockInVo clockInVo = list.stream().filter(v -> v.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())).findFirst().orElse(null); + // 无缺卡 + if (null == clockInVo) { + OvertimeInfoVo overtimeInfo = map.get(k); + ClockInVo c1 = list.get(0); + ClockInVo c2 = list.get(1); + // 秒 + int seconds = DateDetail.calculateSecondDiff(c1.getEffectiveTime(), c2.getEffectiveTime()); + // 小时 + BigDecimal balance = new BigDecimal(Math.abs(seconds)).divide(new BigDecimal(3600), 4, RoundingMode.HALF_UP); + // 获取对应的出勤换算比 获取对应的时长换算存休倍数,综合计算 + AttendanceBaseSetting baseSetting = settingMap.get(overtimeInfo.getGroupId()); + // 加班天数 + BigDecimal compensateRatio = overtimeInfo.getOvertimeRuleDetail().getCompensateRatio(); + balance = balance.divide(baseSetting.getAttendanceRatio(), 2, RoundingMode.HALF_UP) + .multiply(null == compensateRatio ? new BigDecimal(1) : compensateRatio); + // 判断compensateType 是否发劵还是记录 + if (overtimeInfo.getOvertimeRuleDetail().getCompensateType().equals(ConstantUtil.BALANCE_ENABLE)) { + AttendanceEnableBalance enableBalance = AttendanceEnableBalance.builder() + .id(FtbUtil.getId()) + .ruleId(overtimeInfo.getRuleId()) + .overtimeType(overtimeInfo.getOvertimeRuleDetail().getOvertimeType()) + .userId(c1.getUserId()) + .day(overtimeInfo.getDay()) + .balance(balance) + .festivalStr(overtimeInfo.getOvertimeRuleDetail().getFestivalStr()) + .build(); + enableBalanceList.add(enableBalance); + } else { + AttendanceUserBalanceDetailVo detailVo = AttendanceUserBalanceDetailVo.builder() + .id(FtbUtil.getId()) + .userId(c1.getUserId()) + .content("系统已将" + DateDetail.getDate2Str(overtimeInfo.getDay(), DateDetail.DF) + getType(overtimeInfo.getOvertimeRuleDetail().getOvertimeType()) + "加班的" + balance + "天,自动转换为存休") + .quota(balance) + .build(); + balanceList.add(detailVo); + // 添加劵 + AttendanceBalanceRecordEntity record = new AttendanceBalanceRecordEntity(); + record.setUserId(c1.getUserId()); + record.setType(3); + record.setObjectId(overtimeInfo.getRuleId()); + record.setBalance(balance); + record.setTotal(balance); + record.setOverTime(overtimeInfo.getOvertimeRuleDetail().getOvertimeType()); + record.setOverTimeDay(overtimeInfo.getDay()); + record.setUnit(2); + record.setFestivalStr(overtimeInfo.getOvertimeRuleDetail().getFestivalStr()); + recordList.add(record); + } + } + } + }); + List ruleIdList = overtimeInfoList.stream().map(OvertimeInfoVo::getRuleId).collect(Collectors.toList()); + if (!recordList.isEmpty()) { + List dbRecordList = attendanceUserBalanceRecordService.list(new LambdaQueryWrapper() + .in(AttendanceBalanceRecordEntity::getObjectId, ruleIdList) + .eq(AttendanceBalanceRecordEntity::getType, 3) + .eq(AttendanceBalanceRecordEntity::getDeleteMark, ConstantUtil.NUM_FALSE)); + List objIdList = dbRecordList.stream().map(AttendanceBalanceRecordEntity::getObjectId).distinct().collect(Collectors.toList()); + if (!dbRecordList.isEmpty()) { + recordList.removeIf(v -> objIdList.contains(v.getObjectId())); + balanceList.removeIf(v -> objIdList.contains(v.getObjectId())); + } + if (!recordList.isEmpty()) { + attendanceUserBalanceRecordService.saveBatch(recordList); + attendanceUserBalanceMapper.batchAddBalanceDetail(balanceList); + } + } + if (!enableBalanceList.isEmpty()) { + List dbRecordList = enableBalanceService.list(new LambdaQueryWrapper() + .in(AttendanceEnableBalance::getRuleId, ruleIdList) + .eq(AttendanceEnableBalance::getDeleteMark, ConstantUtil.NUM_FALSE)); + List objIdList = dbRecordList.stream().map(AttendanceEnableBalance::getRuleId).distinct().collect(Collectors.toList()); + if (!dbRecordList.isEmpty()) { + enableBalanceList.removeIf(v -> objIdList.contains(v.getRuleId())); + } + if (!enableBalanceList.isEmpty()) { + enableBalanceService.saveBatch(enableBalanceList); + } + } + } + + private String getType(Integer type) { + + String str; + switch (type) { + case 1: + str = "工作日"; + break; + case 2: + str = "公休日"; + break; + case 3: + str = "节假日"; + break; + default: + str = "--"; + break; + } + return str; + } + + @Override + public BigDecimal getTotalBalance(String balanceId, String userId, boolean isRetirementLeave) { + return attendanceUserBalanceMapper.getTotalBalance(balanceId, userId, isRetirementLeave); + } + + + private static void setPagination(AttendanceUserBalanceListQueryDto queryDto, List attendanceGroupUsers, AttendanceUserBalanceListVo attendanceUserBalanceListVo) { + PaginationVO paginationVO = new PaginationVO(); + paginationVO.setTotal(attendanceGroupUsers.size()); + paginationVO.setPageSize((long) queryDto.getPageSize()); + paginationVO.setCurrentPage((long) queryDto.getCurrentPage()); + attendanceUserBalanceListVo.setPagination(paginationVO); + } + + + /** + * 获取符合标准的用户ids集合 + * + * @param queryDto 查询参数 + * @return List + */ + private List getUserIds(AttendanceUserBalanceListQueryDto queryDto, List userIds,List orgIds) throws Exception { + ActionResult> orgUserList = v2UserApi.listTargetOrganizesOrHaveChild(orgIds, false, + StringUtil.isNotEmpty(queryDto.getWorkStatus()) ? UserWorkStatusEnums.getEnumExceptValue(queryDto.getWorkStatus()) + : List.of(UserWorkStatusEnums.RESIGNED), userProvider.get().getTenantId()); + if (200 != orgUserList.getCode() || ObjectUtil.isEmpty(orgUserList)) { + throw new Exception("获取组织用户列表失败"); + } + List data = orgUserList.getData(); + // 备份data + List copyData = new ArrayList<>(); + Map> orgMap = data.stream().collect(Collectors.groupingBy(UserBoundVO::getOrganizeId)); + // 是否筛选班组 + List finalCopyData = copyData; + queryDto.getOrgTeamDtoList().forEach(orgTeamDto -> { + List list = orgMap.get(orgTeamDto.getOrgId()); + if (!orgTeamDto.isSelected() && null != list && !list.isEmpty() && null != orgTeamDto.getWorkGroupId() && !orgTeamDto.getWorkGroupId().isEmpty()) { + list = list.stream().filter(x -> orgTeamDto.getWorkGroupId().contains(x.getStoreTeamId())).collect(Collectors.toList()); + } + if (null != list && !list.isEmpty()) { + finalCopyData.addAll(list); + } + }); + // 如果有名字筛选 + if (StringUtils.isNotBlank(queryDto.getIText())) { + // 收到过滤用户名称模糊匹配 + copyData = copyData.stream().filter(x -> StringUtils.isNotBlank(x.getUserName()) && x.getUserName().contains(queryDto.getIText())).collect(Collectors.toList()); + } + if (!copyData.isEmpty()) { + userIds = copyData.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + return userIds; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserFaceServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserFaceServiceImpl.java new file mode 100644 index 0000000..13a9f8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserFaceServiceImpl.java @@ -0,0 +1,139 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.attendance.mapper.AttendanceGroupUserMapper; +import jnpf.attendance.mapper.AttendanceUserFaceMapper; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceUserFaceService; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.dto.UserFaceDto; +import jnpf.model.attendance.dto.MachineDealDto; +import jnpf.model.attendance.vo.UserFaceVo; +import jnpf.util.*; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 人脸服务实现 + * + * @author yanwenfu + * @create 2024-04-09 + */ +@Slf4j +@Service +public class AttendanceUserFaceServiceImpl implements AttendanceUserFaceService { + + @Resource + private AttendanceUserFaceMapper attendanceUserFaceMapper; + @Resource + private AttenceMachineService attenceMachineService; + @Resource + private AttendanceGroupUserMapper attendanceGroupUserMapper; + @Resource + private RuleScopeUtil ruleScopeUtil; + + @Override + public UserFaceVo getUserFace(String userId) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, userId) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(queryWrapper); + return JsonUtil.getJsonToBean(userFace, UserFaceVo.class); + } + + @Override + public void addUserFace(UserFaceDto userFaceDto) { + + AttendanceUserFace userFace = JsonUtil.getJsonToBean(userFaceDto, AttendanceUserFace.class); + userFace.setId(FtbUtil.getId()); + attendanceUserFaceMapper.insert(userFace); + MachineDealDto machineDealDto = new MachineDealDto(userFaceDto.getUserId(), ConstantUtil.CAL_ADD); + machineDeal(machineDealDto); + } + + @Override + public List machineDeal(MachineDealDto machineDealDto) { + String userId = machineDealDto.getUserId(); + String dealType = machineDealDto.getDealType(); + List machineList = new ArrayList<>(); + // 判断用户当前在哪些考勤组 + List groupIds = getUserGroupList(userId); + log.error("用户的考勤组:{}", groupIds); + if (groupIds.isEmpty()) { + return machineList; + } + // 判断是否绑定考勤机 + List scopeList = StringUtil.isNotEmpty(machineDealDto.getOrgId()) ? ruleScopeUtil.selectUserEffectList(userId, machineDealDto.getOrgId(), ScopeBizType.ATTENDANCE_MACHINE) + : ruleScopeUtil.selectUserEffectList(userId, ScopeBizType.ATTENDANCE_MACHINE, UserProvider.getUser().getTenantId()); + log.error("适配范围:{}", scopeList); + if (null == scopeList || scopeList.isEmpty()) { + return machineList; + } + List ruleIds = scopeList.stream().map(AttendanceRuleScope::getRuleId).collect(Collectors.toList()); + List list = attenceMachineService.list(new LambdaQueryWrapper() + .in(AttendanceMachineManage::getId, ruleIds) + .eq(AttendanceMachineManage::getDeleteMark, ConstantUtil.NUM_FALSE)); + log.error("关联ruleId:{}", ruleIds); + if (list.isEmpty()) { + return machineList; + } + list.forEach(machine -> { + if (dealType.equals(ConstantUtil.CAL_REDUCE)) { + attenceMachineService.deleteUserList(machine.getType(), machine.getMac(), List.of(userId)); + attenceMachineService.sendUserToMachine(machine.getType(), userId, machine.getMac()); + } else if (dealType.equals(ConstantUtil.CAL_DELETE)) { + attenceMachineService.deleteUserList(machine.getType(), machine.getMac(), List.of(userId)); + } else { + attenceMachineService.sendUserToMachine(machine.getType(), userId, machine.getMac()); + } + machineList.add(machine.getMac()); + }); + return machineList; + } + + private List getUserGroupList(String userId) { + + String groupId = attendanceGroupUserMapper.getSelfGroup(userId); + return Stream.of(groupId).collect(Collectors.toList()); + } + + @Override + public void updateUserFace(UserFaceDto userFaceDto) { + + LambdaQueryWrapper query = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, userFaceDto.getUserId()); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(query); + if (null == userFace) { + addUserFace(userFaceDto); + } else { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .eq(AttendanceUserFace::getUserId, userFaceDto.getUserId()) + .set(AttendanceUserFace::getFaceData, userFaceDto.getFaceData()); + attendanceUserFaceMapper.update(null, updateWrapper); + MachineDealDto machineDealDto = new MachineDealDto(userFaceDto.getUserId(), ConstantUtil.CAL_REDUCE); + machineDeal(machineDealDto); + } + } + + @Override + public void deleteUserFace(String userId) { + + LambdaUpdateWrapper queryWrapper = new LambdaUpdateWrapper() + .eq(AttendanceUserFace::getUserId, userId) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_TRUE); + attendanceUserFaceMapper.update(null, queryWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserServiceImpl.java new file mode 100644 index 0000000..3fbd544 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserServiceImpl.java @@ -0,0 +1,1469 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.attendance.mapper.AttendanceGroupUserMapper; +import jnpf.attendance.mapper.AttendanceLeaveApproveMapper; +import jnpf.attendance.mapper.AttendanceManagerPermissionMapper; +import jnpf.attendance.service.*; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.ActionResult; +import jnpf.base.service.SuperServiceImpl; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.AttendanceManagerPermission; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.GroupUserTypeEnum; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.exception.HandleException; +import jnpf.exception.LoginException; +import jnpf.message.enums.PermissionOrgReUserEnum; +import jnpf.message.model.permission.PermissionRelationOrganizeUserListDTO; +import jnpf.model.attendance.dto.*; +import jnpf.model.attendance.model.UserChangeNoticeModel; +import jnpf.model.attendance.vo.AttendanceGroupUserVo; +import jnpf.model.attendance.vo.AttendanceSelfApproveVo; +import jnpf.model.attendance.vo.GroupUserLineScheduleVo; +import jnpf.model.attendance.vo.SecondmentDateVo; +import jnpf.model.attendance.vo.attendance.ClockInExportVo; +import jnpf.model.attendance.vo.attendance.JoinGroupVo; +import jnpf.model.common.DateRangeDto; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsTurnoverManagementApi; +import jnpf.util.*; +import jnpf.util.attendance.DayStatisticsUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.compress.utils.Lists; +import org.apache.commons.lang3.tuple.MutablePair; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.lang.reflect.Type; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static jnpf.constants.RedisConstant.GROUP_JOIN_USERS_KEY; +import static jnpf.util.DateConvertUtil.setOverlap; + +@Service +@Slf4j +public class AttendanceUserServiceImpl extends SuperServiceImpl implements AttendanceUserService { + + @Resource + private AttendanceGroupUserMapper attendanceGroupUserMapper; + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private RedissonClient redissonClient; + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Autowired + private AttendanceClockInService attendanceClockInService; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private AttendanceLeaveApproveMapper attendanceLeaveApproveMapper; + @Autowired + private AttendanceGroupService attendanceGroupService; + @Resource + private AttendanceManagerPermissionMapper attendanceManagerPermissionMapper; + @Resource + private AppStatisticsService appStatisticsService; + @Autowired + private StringRedisTemplate stringRedisTemplate; + @Resource + private V2UserApi v2UserApi; + @Autowired + private AttendanceUserService attendanceUserService; + @Autowired + private AttendanceUserFaceService attendanceUserFaceService; + @Autowired + private FtbPersonnelsTurnoverManagementApi ftbPersonnelsTurnoverManagementApi; + + /** + * 根据考勤组查询成员列表 + * + * @param groupId 考勤组id + * @param type 1.本组(包含离组) + * @return List + */ + @Override + public List queryUsersByGroupId(String groupId, String name, Integer type, List userIds, Date start, Date end) { + + List groupUserList = getAttendanceGroupUsersOfSecondment(start, end, userIds, List.of(groupId)); + if (CollectionUtil.isEmpty(groupUserList)) { + return new ArrayList<>(); + } + if (Objects.nonNull(type)) { + groupUserList.removeIf(groupUser -> !groupUser.getType().equals(type)); + } + List groupUserVos = JsonUtil.getJsonToList(groupUserList, AttendanceGroupUserVo.class); + /* 查询用户信息*/ +// List userVoList = userApi.getInfoByIds(groupUserVos.stream().map(AttendanceGroupUserVo::getUserId).collect(Collectors.toList())); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(groupUserVos.stream().map(AttendanceGroupUserVo::getUserId).distinct().collect(Collectors.toList()), null); + if (allUserInfoBatch.getCode() != 200 && allUserInfoBatch.getData() == null) { + return Lists.newArrayList(); + } + List userVoList = allUserInfoBatch.getData(); + /* 用户姓名模糊查询*/ + if (StrUtil.isNotBlank(name)) { + userVoList = userVoList.stream().filter(userVo -> { + return userVo.getUserName().contains(name); + }).collect(Collectors.toList()); + } + if (CollectionUtil.isEmpty(userVoList)) { + return Lists.newArrayList(); + } + Map listToMap = userVoList.stream().collect(Collectors.toMap(UserBoundVO::getId, user -> user)); + + List newGroupUserVoList = new ArrayList<>(); + groupUserVos.stream().collect(Collectors.toMap(vo -> vo.getGroupId() + vo.getUserId(), Function.identity(), (r1, r2) -> r1)).forEach((key, vo) -> { + UserBoundVO partUserInfoVo = listToMap.get(vo.getUserId()); + if (partUserInfoVo != null) { + vo.setRealName(partUserInfoVo.getUserName()); + String headIcon = partUserInfoVo.getHeadIcon(); + vo.setHeadIcon(UploaderUtil.uploaderImg(headIcon)); + /* 岗位信息*/ + String positionName = partUserInfoVo.getPositionName(); + vo.setPositionName(positionName); + newGroupUserVoList.add(vo); + } + }); + return newGroupUserVoList; + } + + @Override + public List getUserIds(List organizeList, List userIdList, Date start, Date end, Integer scopeOfAdaptation) { + return getUserIdsAndGroupIds(organizeList, userIdList, start, end, scopeOfAdaptation).stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + } + @Override + public List getUserIdsAndGroupIds(List organizeList, List userIdList, Date start, Date end, Integer scopeOfAdaptation) { + // 自动排休逻辑 + List byOrgIds = CollUtil.isEmpty(organizeList) ? List.of() : attendanceGroupService.getByOrgIds(organizeList.stream().distinct().collect(Collectors.toList())); + List groupIds = byOrgIds.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList()); + List attendanceGroupUsersOfSecondment = attendanceUserService.getAttendanceGroupUsersOfSecondment(start, end, null, 0 == scopeOfAdaptation ? null : groupIds); + if (CollUtil.isNotEmpty(userIdList)) { + List attendanceGroupUsersOfSecondment1 = attendanceUserService.getAttendanceGroupUsersOfSecondment(start, end, userIdList, null); + attendanceGroupUsersOfSecondment.addAll(attendanceGroupUsersOfSecondment1); + } + return attendanceGroupUsersOfSecondment; + } + + @Override + public void sort(UserSortModel userSortModel) { + Map collect = userSortModel.getUserSortList().stream().collect(Collectors.toMap(UserSortModel.UserSort::getUserId, UserSortModel.UserSort::getSort)); + List list = lambdaQuery().eq(AttendanceGroupUser::getGroupId, userSortModel.getGroupId()) + .in(AttendanceGroupUser::getUserId, collect.keySet()) + .list(); + list.forEach(user -> user.setSort(collect.getOrDefault(user.getUserId(), null))); + updateBatchById(list); + } + + @Override + public List queryByUsersGroupIds(List groupIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getDeleteMark, 0) + .in(AttendanceGroupUser::getGroupId, groupIds); + return attendanceGroupUserMapper.selectList(queryWrapper); + } + + @Override + public List queryByAllUsersGroupIds(List groupIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().in(AttendanceGroupUser::getGroupId, groupIds); + return attendanceGroupUserMapper.selectList(queryWrapper); + } + + @Override + public List queryAll() { + return lambdaQuery() + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .list(); + } + + @Override + public List queryAllForNotSecondment() { + return lambdaQuery() + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .eq(AttendanceGroupUser::getType, 1) + .list(); + } + + + @Override + public List queryByUsersIds(List userIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .in(AttendanceGroupUser::getUserId, userIds); + return attendanceGroupUserMapper.selectList(queryWrapper); + } + + @Override + public List queryAllByUsersIds(List userIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .in(AttendanceGroupUser::getUserId, userIds); + return attendanceGroupUserMapper.selectList(queryWrapper); + } + + @Override + public List queryByUsersAndGroup(Date start, Date end, List userIds, List groupIds) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .le(Objects.nonNull(end), AttendanceGroupUser::getCreatorTime, end) + .and(Objects.nonNull(start), x -> x.ge(AttendanceGroupUser::getRemoveTime, start) + .or() + .isNull(AttendanceGroupUser::getRemoveTime)) + .in(AttendanceGroupUser::getGroupId, groupIds) + .in(CollUtil.isNotEmpty(userIds), AttendanceGroupUser::getUserId, userIds); + return attendanceGroupUserMapper.selectList(queryWrapper); + } + + @Override + public List queryByUsersAndGroupFilterSecondment(Date start, Date end, List userIds, List groupIds) { + return queryByUsersAndGroupFilterSecondment(start, end, userIds, groupIds, false); + } + + @Override + public List queryByUsersAndGroupFilterSecondment(Date start, Date end, List userIds, List groupIds, Boolean isContainsDeleteGroup) { + List attendanceGroupUsersOfSecondmentAll = getAttendanceGroupUsersOfSecondmentAll(start, end, userIds, groupIds, isContainsDeleteGroup); + return attendanceGroupUsersOfSecondmentAll.stream().filter(vo -> Objects.equals(vo.getType(), 1)).collect(Collectors.toList()); + } + + @Override + public List getAttendanceGroupUsersOfSecondment(Date start, Date end, List userIds, List groupIds, boolean isContainsDeleteGroup) { + List attendanceGroupUsers = getAttendanceGroupUsersOfSecondmentAll(start, end, userIds, groupIds, isContainsDeleteGroup); + if (Objects.isNull(start) || Objects.isNull(end)) { + return attendanceGroupUsers; + } + return getAttendanceGroupUsers(start, end, attendanceGroupUsers); + } + + @Override + public List getAttendanceGroupUsers(Date start, Date end, List attendanceGroupUsers) { + return attendanceGroupUsers.stream().filter(user -> { + List secondmentDateVos = StringUtil.isEmpty(user.getTimeJson()) ? CollUtil.newArrayList() : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class); + if (!Objects.equals(user.getUserGroupType(), 2)) { + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.TRUE; + } + SecondmentDateVo secondmentType = getSecondmentType(secondmentDateVos, start, end); + if (Objects.isNull(secondmentType)) { + return Boolean.TRUE; + } + if (secondmentType.getStartTime().after(start) || secondmentType.getEndTime().before(end)) { + return Boolean.TRUE; + } + + return Boolean.FALSE; + } + //0.无借调为true + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.FALSE; + } + SecondmentDateVo secondmentType = getSecondmentType(secondmentDateVos, start, end); + if (Objects.nonNull(secondmentType)) { + return Boolean.TRUE; + } + return Boolean.FALSE; + }).collect(Collectors.toList()); + } + + @Override + public List getAttendanceGroupUsersOfSecondment(Date start, Date end, List userIds, List groupIds) { + return getAttendanceGroupUsersOfSecondment(start, end, userIds, groupIds, false); + } + + + private List getAttendanceGroupUsersOfSecondmentAll(Date start, Date end, List userIds, List groupIds, boolean isContainsDeleteGroup) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .and(Objects.nonNull(end), x -> x.le(AttendanceGroupUser::getCreatorTime, end) + .or() + .eq(AttendanceGroupUser::getUserGroupType, 2)) + .and(Objects.nonNull(start), x -> x.ge(AttendanceGroupUser::getRemoveTime, start) + .or() + .isNull(AttendanceGroupUser::getRemoveTime)) + .in(CollUtil.isNotEmpty(groupIds), AttendanceGroupUser::getGroupId, groupIds) + .in(CollUtil.isNotEmpty(userIds), AttendanceGroupUser::getUserId, userIds) + .orderByAsc(AttendanceGroupUser::getSort); + List attendanceGroupUsers = attendanceGroupUserMapper.selectList(queryWrapper); + List collect = attendanceGroupUsers.stream().map(AttendanceGroupUser::getGroupId).filter(Objects::nonNull).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)) { + return CollUtil.newArrayList(); + } + List attendanceGroups = attendanceGroupService.listByIds(collect); + if (CollUtil.isEmpty(attendanceGroups)) { + return CollUtil.newArrayList(); + } + List groupIdList = attendanceGroups.stream().filter(group -> isContainsDeleteGroup || Objects.equals(group.getDeleteMark(), 0)).map(AttendanceGroup::getId).collect(Collectors.toList()); + Map> collect1 = attendanceGroupUsers.stream().filter(vo -> Objects.equals(vo.getDeleteMark(), 0)).collect(Collectors.groupingBy(AttendanceGroupUser::getUserId, Collectors.mapping(AttendanceGroupUser::getCreatorTime, Collectors.toList()))); + return attendanceGroupUsers.stream() + .filter(group -> groupIdList.contains(group.getGroupId())) + .filter(vo->Objects.equals(vo.getDeleteMark(), 0) || !collect1.containsKey(vo.getUserId()) || collect1.get(vo.getUserId()).stream().allMatch(date -> date.after(vo.getCreatorTime()))) + .collect(Collectors.toList()); + } + + @Override + public List getSelfUsersBeLongToGroup(String groupId) { + Date now = new Date(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getUserGroupType, 2) + .isNull(AttendanceGroupUser::getRemoveTime) + .eq(StringUtil.isNotEmpty(groupId), AttendanceGroupUser::getGroupId, groupId) + ; + List attendanceGroupUsers = attendanceGroupUserMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + return CollUtil.newArrayList(); + } + // 借调人员集合 + List userIdList = new ArrayList<>(); + Map> userMap = attendanceGroupUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + userMap.forEach((k, v) -> { + if (!userMap.get(k).isEmpty()) { + for (AttendanceGroupUser attendanceGroupUserVo : userMap.get(k)) { + List secondmentDateVos = StringUtil.isEmpty(attendanceGroupUserVo.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUserVo.getTimeJson(), SecondmentDateVo.class); + if (secondmentDateVos.isEmpty()) { + userIdList.add(k); + break; + } else { + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(now, dateVo.getStartTime(), dateVo.getEndTime()) + ) { + userIdList.add(k); + break; + } + } + } + } + } + }); + if (!userIdList.isEmpty()) { + // 查询这些用户所属的考勤组 + QueryWrapper queryWrapper1 = new QueryWrapper<>(); + queryWrapper1.lambda() + .eq(AttendanceGroupUser::getUserGroupType, 1) + .isNull(AttendanceGroupUser::getRemoveTime) + .in(CollUtil.isNotEmpty(userIdList), AttendanceGroupUser::getUserId, userIdList) + ; + return attendanceGroupUserMapper.selectList(queryWrapper1); + } + + return CollUtil.newArrayList(); + } + + public List getUserIdList(List userIds, List reqUserIds) { + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + if (CollUtil.isNotEmpty(reqUserIds)) { + if (CollUtil.isEmpty(org.apache.commons.collections4.CollectionUtils.intersection(userIds, reqUserIds))) { + return new ArrayList<>(); + } else { + return (List) CollectionUtils.intersection(userIds, reqUserIds); + } + } else { + return userIds; + } + } + + @Override + public List getUsersDayByGroupIds(GroupUserQueryDto groupUserQueryDto, DayStatisticsPageListDto req) { + int pageSize = (int) req.getPageSize(); + int pageNum = (int) req.getCurrentPage(); + int start = (pageNum - 1) * pageSize; + int end = start + pageSize; + + String value = stringRedisTemplate.opsForValue().get(req.getKey()); + if (StringUtil.isNotEmpty(value)) { + // 有缓存,拿去缓存数据 + List jsonToList = JsonUtil.getJsonToList(value, ClockInExportVo.class); + if (end > jsonToList.size()) { + end = jsonToList.size(); + } + req.setTotal(jsonToList.size()); + if (start > jsonToList.size()) { + return null; + } + return jsonToList.subList(start, end); + } + List usersByGroupVos = new ArrayList<>(); + if (StringUtil.isEmpty(groupUserQueryDto.getTenantId())) { + groupUserQueryDto.setTenantId(UserProvider.getUser().getTenantId()); + } + List dayList = FtbUtil.getDateStr(groupUserQueryDto.getStartTime(), groupUserQueryDto.getEndTime()).getUseList(); + // 批量处理用户在时间段内考勤组数据 + SimpleDateFormat sdfhm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + List users = queryByAllUsersGroupIds(groupUserQueryDto.getFilterList() + .stream().map(GroupFilterDto::getGroupId).collect(Collectors.toList())); + if (CollUtil.isEmpty(users)) { + return CollUtil.newArrayList(); + } + List finalAllUser = groupUserQueryDto.getUserIds(); + Map> groupUserMap = CollUtil.isNotEmpty(users) ? users.stream().filter(user -> + finalAllUser.contains(user.getUserId())).collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)) : new HashMap<>(); + + for (String s : dayList) { + Date startTime; + Date endTime; + try { + startTime = sdfhm.parse(s + " 00:00:00"); + endTime = sdfhm.parse(s + " 23:59:59"); + } catch (ParseException e) { + throw new RuntimeException(e); + } + // 满足条件的用户 + for (Map.Entry> listEntry : groupUserMap.entrySet()) { + for (AttendanceGroupUser groupUser : listEntry.getValue()) { + List list = new ArrayList<>(); + list.add(groupUser); + List keyList = usersByGroupVos.stream().map(ClockInExportVo::getKeyString).distinct().collect(Collectors.toList()); + if (attendanceDailyRuleService.findUserIsExistsByDay(list, startTime, endTime) > 0 && !keyList.contains(groupUser.getUserId() + "-" + s + "-" + groupUser.getGroupId())) { + ClockInExportVo groupUserDayVo = new ClockInExportVo(); + groupUserDayVo.setUserId(groupUser.getUserId()); + groupUserDayVo.setGroupId(groupUser.getGroupId()); + groupUserDayVo.setDate(s); + groupUserDayVo.setKeyString(groupUser.getUserId() + "-" + s + "-" + groupUser.getGroupId()); + usersByGroupVos.add(groupUserDayVo); + } + } + } + } + if (usersByGroupVos.isEmpty()) { + return usersByGroupVos; + } + usersByGroupVos = usersByGroupVos.stream().filter(v -> groupUserQueryDto.getUserIds().contains(v.getUserId())).collect(Collectors.toList()); + if (usersByGroupVos.isEmpty()) { + return usersByGroupVos; + } + usersByGroupVos.forEach(v -> { + UserBoundVO staffRosterDto = groupUserQueryDto.getStaffRosterMap().get(v.getUserId()); + v.setDeptName(StringUtil.isNotEmpty(staffRosterDto.getOrganizeName()) ? staffRosterDto.getOrganizeName() : ""); + v.setPostName(StringUtil.isNotEmpty(staffRosterDto.getPositionName()) ? staffRosterDto.getPositionName() : ""); + v.setUserName(staffRosterDto.getName()); + }); + // 排序 + List collect = usersByGroupVos.stream().sorted(Comparator.comparing(ClockInExportVo::getUserId).thenComparing(ClockInExportVo::getDate)).collect(Collectors.toList()); + // 存入缓存 + stringRedisTemplate.opsForValue().set(req.getKey(), JsonUtil.getObjectToString(collect), 10 * 60 * 1000); + // 对usersByGroupVos 手动分页 + if (end > collect.size()) { + end = collect.size(); + } + req.setTotal(collect.size()); + if (start > collect.size()) { + return null; + } + return collect.subList(start, end); + } + + @Override + public List getClockInExportVoList(GroupUserQueryDto groupUserQueryDto, List usersByGroupVos) { + DateRangeDto dateRangeDto = new DateRangeDto(groupUserQueryDto.getStartTime(), groupUserQueryDto.getEndTime()); + // 处理时段及打卡信息 + usersByGroupVos = appStatisticsService.getDayClockInPageListExport(usersByGroupVos, dateRangeDto, groupUserQueryDto.getTenantId()); + return usersByGroupVos; + } + + @Override + public List getGroupUserIds(List groupIds) { + List groupUserIds = attendanceGroupUserMapper.getGroupUserIds(groupIds); + if (CollUtil.isEmpty(groupUserIds)) { + groupUserIds = CollUtil.newArrayList(); + } + // 查看对应考勤组今日有无借调 + List todaySecondUser = getTodaySecondUser(groupIds); + if (CollUtil.isEmpty(todaySecondUser)) { + return groupUserIds.stream().distinct().collect(Collectors.toList()); + } + groupUserIds.addAll(todaySecondUser); + return groupUserIds.stream().distinct().collect(Collectors.toList()); + } + + @Override + public List getGroupNowAllUserList(String groupId) { + List list = new ArrayList<>(); + // 获取考勤组下的所有用户包含借调 + List users = getAttendanceGroupUsersOfSecondment(new Date(), new Date(), null, List.of(groupId)); + List userBoundVO = v2UserApi.getUserPrimaryBoundBatch(users.stream().map(AttendanceGroupUser::getUserId).distinct().collect(Collectors.toList()), null).getData(); + if (CollectionUtil.isNotEmpty(userBoundVO)) { + for (UserBoundVO boundVo : userBoundVO) { + AttendanceGroupUserVo groupUserVo = new AttendanceGroupUserVo(); + groupUserVo.setUserId(boundVo.getId()); + groupUserVo.setRealName(boundVo.getUserName()); + groupUserVo.setNickname(boundVo.getNickname()); + groupUserVo.setHeadIcon(UploaderUtil.uploaderImg(boundVo.getHeadIcon())); + groupUserVo.setPositionName(boundVo.getPositionName()); + list.add(groupUserVo); + } + } + return list; + } + + + public List getTodaySecondUser(List groupIds) { + List userIds = new ArrayList<>(); + Date now = new Date(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getUserGroupType, 2) + .isNull(AttendanceGroupUser::getRemoveTime) + .in(AttendanceGroupUser::getGroupId, groupIds) + ; + List attendanceGroupUsers = attendanceGroupUserMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + // 没有发生借调 + return null; + } + + for (AttendanceGroupUser attendanceGroupUser : attendanceGroupUsers) { + List secondmentDateVos = StringUtil.isEmpty(attendanceGroupUser.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUser.getTimeJson(), SecondmentDateVo.class); + if (secondmentDateVos.isEmpty()) { + userIds.add(attendanceGroupUser.getUserId()); + } else { + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(now, dateVo.getStartTime(), dateVo.getEndTime())) { + userIds.add(attendanceGroupUser.getUserId()); + } + } + } + } + return userIds.stream().distinct().collect(Collectors.toList()); + } + + @Override + public List checkUserJoinGroupToObject(String userIds) { + log.error("checkUserJoinGroupToObject 入参-- userIds: {}", userIds); + Gson gson = new Gson(); + Type mapType = new TypeToken>() { + }.getType(); + Map map = gson.fromJson(userIds, mapType); + // 3. 提取值 + userIds = map.get("userIds"); + if (StrUtil.isBlank(userIds)) { + return new ArrayList<>(); + } + String[] userIdArr = userIds.split(","); + ArrayList userIdList = CollectionUtil.newArrayList(userIdArr); + List groupUserVoList = attendanceGroupUserMapper.getGroupInUserIds(userIdList, null); + if (CollectionUtil.isEmpty(groupUserVoList)) { + return new ArrayList<>(); + } + List groupUserVos = groupUserVoList.stream().filter(groupUser -> { + return GroupUserTypeEnum.CUR.getCode().equals(groupUser.getType()); + }).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(groupUserVos)) { + List userNameList = new ArrayList<>(); + Map userVoMap = groupUserVos.stream().collect(Collectors.toMap(AttendanceGroupUserVo::getUserId, Function.identity())); + log.error("checkUserJoinGroupToObject -- groupUserVos: {}", groupUserVos); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(groupUserVos.stream().map(AttendanceGroupUserVo::getUserId).collect(Collectors.toList()), null); + if (allUserInfoBatch.getCode() != 200 && allUserInfoBatch.getData() == null) { + return Lists.newArrayList(); + } + List userList = allUserInfoBatch.getData(); + // 拼接考勤组名称 + userList.forEach(user -> { + JoinGroupVo joinGroupVo = new JoinGroupVo(); + String append = user.getUserName() + "已加入" + userVoMap.get(user.getId()).getGroupName() + "考勤组"; + joinGroupVo.setContent(append); + joinGroupVo.setUserId(user.getId()); + userNameList.add(joinGroupVo); + }); + return userNameList; + } + return new ArrayList<>(); + } + + @Override + public List getUserBoundVO(String groupId, String workGroupId) { + AttendanceGroup byId = attendanceGroupService.getById(groupId); + if (Objects.isNull(byId)) { + return List.of(); + } + return userAntifreeze.getInfoByIds(byId.getOrgId(), workGroupId); + } + + @Override + public List batchGetUserBoundVO(List groupIds, List workStatusEnums) { + List groupList = attendanceGroupService.listByIds(groupIds); + if (CollUtil.isEmpty(groupList)) { + return List.of(); + } + List orgIdList = groupList.stream().map(AttendanceGroup::getOrgId).collect(Collectors.toList()); + List userBoundVos = userAntifreeze.batchGetInfoByIds(orgIdList, workStatusEnums); + userBoundVos.forEach(user -> { + // 入职失败需要你归属预入值 + if (UserWorkStatusEnums.ONBOARDING_FAILED.equals(user.getWorkStatusEnums())) { + user.setWorkStatusEnums(UserWorkStatusEnums.PRE_ONBOARDING); + } + }); + return userBoundVos; + } + + /** + * 获取用户当前所属的借调考勤组Id + * + * @return String + */ + @Override + public String getUserNowGroup() { + Date now = new Date(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getUserGroupType, 2) + .isNull(AttendanceGroupUser::getRemoveTime) + .eq(AttendanceGroupUser::getUserId, UserProvider.getUser().getUserId()) + ; + List attendanceGroupUsers = attendanceGroupUserMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(attendanceGroupUsers)) { + // 没有发生借调 + return null; + } + for (AttendanceGroupUser attendanceGroupUser : attendanceGroupUsers) { + List secondmentDateVos = StringUtil.isEmpty(attendanceGroupUser.getTimeJson()) ? new ArrayList<>() : JsonUtil.getJsonToList(attendanceGroupUser.getTimeJson(), SecondmentDateVo.class); + if (secondmentDateVos.isEmpty()) { + return attendanceGroupUser.getGroupId(); + } else { + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(now, dateVo.getStartTime(), dateVo.getEndTime())) { + return attendanceGroupUser.getGroupId(); + } + } + } + } + return null; + } + + /** + * 获取目标日期涉及的借调时段 + * + * @param secondmentDateVos 借调时段 + * @param inPoint 入点 + * @param outPoint 出点 + * @return 借调时段 + */ + private SecondmentDateVo getSecondmentType(List secondmentDateVos, Date inPoint, Date outPoint) { + if (CollUtil.isEmpty(secondmentDateVos)) { + return null; + } + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (dateVo.getStartTime().before(outPoint) && dateVo.getEndTime().after(inPoint)) { + return dateVo; + } + } + return null; + } + + /** + * 查询系统用户 + * + * @param orgId 组织机构id + * @param name 用户姓名 用于字段模糊搜索 + * @return List + */ + @Override + public List querySysUserList(String orgId, String name) { + return attendanceGroupUserMapper.querySysUsers(orgId, name); + } + + @Override + @Transactional + public void orgUpdateHandle(List userList) { + if (CollUtil.isEmpty(userList)) { + return; + } + try { + TenantDataSourceUtil.switchTenant(userList.get(0).getTenantId()); + } catch (LoginException e) { + throw new RuntimeException(e); + } + List reduceUser = userList.stream().filter(vo -> Objects.equals(vo.getType(), PermissionOrgReUserEnum.USER_REDUCE)).collect(Collectors.toList()); + List addUser = userList.stream().filter(vo -> Objects.equals(vo.getType(), PermissionOrgReUserEnum.USER_ADD)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(reduceUser) && CollUtil.isNotEmpty(addUser)) { + return; + } + RLock lock = null; + try { + String key = String.format(GROUP_JOIN_USERS_KEY, userList.get(0).getUserId()); + lock = redissonClient.getLock(key); + if (lock.isLocked()) { + log.error("接收组织人员添加通知,目前用户【{}】正在被变更!", userList.get(0).getUserId()); + return; + } + if (!lock.tryLock(2, 10, TimeUnit.SECONDS)) { + return; + } + removeUsers(reduceUser, true); + addUsers(addUser, true); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (Objects.nonNull(lock) && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + @Override + @Transactional + public void addUsers(List userList, Boolean isOnboarding) { + if (CollUtil.isEmpty(userList)) { + return; + } + List collect = userList.stream().map(PermissionRelationOrganizeUserListDTO::getOrganizeId).collect(Collectors.toList()); + Map org2GroupMap = attendanceGroupService.batchSaveGroupByOrgIds(userList.get(0).getTenantId(), collect); + List userIds = userList.stream().map(PermissionRelationOrganizeUserListDTO::getUserId).collect(Collectors.toList()); + List groupIds = userList.stream().map(dto -> org2GroupMap.get(dto.getOrganizeId())).collect(Collectors.toList()); + Date date = new Date(); + List attendanceGroupUsers = attendanceUserService.queryByUsersAndGroupFilterSecondment(date, date, userIds, null); + List userIds1 = attendanceGroupUsers.stream().filter(vo -> groupIds.contains(vo.getGroupId())).map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + List collect1 = userList.stream().filter(user -> !CollUtil.contains(userIds1, user.getUserId())).map(user -> { + AttendanceGroupUser groupUser = new AttendanceGroupUser(); + groupUser.setId(RandomUtil.uuId()); + groupUser.setUserId(user.getUserId()); + groupUser.setSort(1000); + groupUser.setGroupId(org2GroupMap.get(user.getOrganizeId())); + groupUser.setType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setUserGroupType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setCreatorUserId("admin"); + groupUser.setCreatorTime(isOnboarding && Objects.nonNull(user.getEntryDate()) ? user.getEntryDate() : DateUtil.getDayBegin()); + groupUser.setDeleteMark(0); + return groupUser; + }).collect(Collectors.toList()); + saveBatch(collect1); + List collect2 = attendanceGroupUsers.stream().filter(user -> userList.stream().anyMatch(vo -> StringUtil.equals(vo.getUserId(), user.getUserId()))).collect(Collectors.toList()); + Map collect3 = userList.stream().filter(vo -> Objects.nonNull(vo.getEntryDate())).collect(Collectors.toMap(PermissionRelationOrganizeUserListDTO::getUserId, PermissionRelationOrganizeUserListDTO::getEntryDate)); + if (CollUtil.isNotEmpty(collect2)) { + collect2.forEach(user -> { + if (CollUtil.contains(groupIds, user.getGroupId())) { + user.setCreatorTime(collect3.getOrDefault(user.getUserId(), user.getCreatorTime())); + return; + } + user.setRemoveTime(Objects.requireNonNullElse(user.getRemoveTime(), collect3.getOrDefault(user.getUserId(), new Date()))); + user.setDeleteMark(1); + }); + updateBatchById(collect2); + } + String tenantId = userList.get(0).getTenantId(); + collect1.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId, Collectors.mapping(AttendanceGroupUser::getUserId, Collectors.toList()))).forEach((groupId, groupUserIds) -> { + groupUserIds.forEach(userId -> { + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_ADD); + attendanceUserFaceService.machineDeal(machineDealDto); + }); + attendanceDailyRuleService.addUserFixedHandle(groupId, tenantId, groupUserIds); + groupUserIds.forEach(userId -> attendanceClockInService.generateRepairNumForUser(groupId, userId, 1)); + sendNotice(groupId, groupUserIds, UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.JOIN_GROUP); + }); + } + + /** + * 移除用户 + * + * @param userList 用户列表 + */ + @Override + @Transactional + public void removeUsers(List userList, Boolean isTurnover) { + if (CollUtil.isEmpty(userList)) { + return; + } + //查询涉及考勤组 + List collect = userList.stream().map(PermissionRelationOrganizeUserListDTO::getOldOrganizeId).collect(Collectors.toList()); + List groupListByOrgIds = attendanceGroupService.getGroupListByOrgIds(collect); + Map collect1 = groupListByOrgIds.stream().collect(Collectors.toMap(AttendanceGroup::getOrgId, AttendanceGroup::getId, (r1, r2) -> r2)); + Map departMap = Maps.newHashMap(); + String tenantId = userList.get(0).getTenantId(); + if (isTurnover) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + FtbDepUserDTO ftbDepUserDTO = new FtbDepUserDTO(); + ftbDepUserDTO.setUserIds(userList.stream().map(PermissionRelationOrganizeUserListDTO::getUserId).collect(Collectors.toList())); + ftbDepUserDTO.setTenantId(tenantId); + List informationAboutTheDepartingPerson = ftbPersonnelsTurnoverManagementApi.getInformationAboutTheDepartingPersonNotToken(ftbDepUserDTO); + log.error("查询离职数据为{}", JSONObject.toJSONString(informationAboutTheDepartingPerson)); + if (CollUtil.isEmpty(informationAboutTheDepartingPerson)) { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + informationAboutTheDepartingPerson = ftbPersonnelsTurnoverManagementApi.getInformationAboutTheDepartingPersonNotToken(ftbDepUserDTO); + log.error("2次查询离职数据为{}", JSONObject.toJSONString(informationAboutTheDepartingPerson)); + } + if (CollUtil.isNotEmpty(informationAboutTheDepartingPerson)) { + departMap.putAll(informationAboutTheDepartingPerson.stream().collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getDepartDate))); + } + } else { + Map collect2 = userList.stream().filter(user -> Objects.nonNull(user.getEntryDate())).collect(Collectors.toMap(PermissionRelationOrganizeUserListDTO::getUserId, PermissionRelationOrganizeUserListDTO::getEntryDate)); + departMap.putAll(collect2); + } + userList.stream().collect(Collectors.groupingBy(PermissionRelationOrganizeUserListDTO::getOldOrganizeId, Collectors.mapping(PermissionRelationOrganizeUserListDTO::getUserId, Collectors.toList()))).forEach((orgId, groupUserIds) -> { + if (!collect1.containsKey(orgId)) { + return; + } + String groupId = collect1.get(orgId); + removeUsersHandle(groupId, groupUserIds, departMap, tenantId, orgId); + }); + } + + private void updateUsers(List userList) { + if (CollUtil.isEmpty(userList)) { + return; + } + //查询涉及考勤组 + List orgIds = userList.stream().map(PermissionRelationOrganizeUserListDTO::getOrganizeId).collect(Collectors.toList()); + List groupListByOrgIds = attendanceGroupService.getGroupListByOrgIds(orgIds); + Map collect1 = groupListByOrgIds.stream().collect(Collectors.toMap(AttendanceGroup::getId, AttendanceGroup::getOrgId, (r1, r2) -> r2)); + List collect = new ArrayList<>(collect1.values()); + if (CollUtil.isNotEmpty(collect)) { + JoinUserDto joinUserDto = new JoinUserDto(); + joinUserDto.setTenantId(userList.get(0).getTenantId()); + userList.stream().collect(Collectors.groupingBy(PermissionRelationOrganizeUserListDTO::getOldOrganizeId, Collectors.mapping(PermissionRelationOrganizeUserListDTO::getUserId, Collectors.toList()))).forEach((orgId, groupUserIds) -> { + if (!collect1.containsKey(orgId)) { + return; + } + String groupId = collect1.get(orgId); + joinUserDto.setUserIds(groupUserIds); + joinUserDto.setGroupId(groupId); + batchDeleteUsers(joinUserDto); + }); + userList.stream().collect(Collectors.groupingBy(PermissionRelationOrganizeUserListDTO::getOrganizeId, Collectors.mapping(PermissionRelationOrganizeUserListDTO::getUserId, Collectors.toList()))).forEach((orgId, groupUserIds) -> { + if (!collect1.containsKey(orgId)) { + return; + } + String groupId = collect1.get(orgId); + joinUserDto.setGroupId(groupId); + joinUserDto.setUserIds(groupUserIds); + batchAddUsers(joinUserDto); + sendNotice(groupId, joinUserDto.getUserIds(), joinUserDto.getTenantId(), AttendanceNoticeEnum.GROUP_CHANGE); + }); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void joinUsers(JoinUserDto joinUserDto) throws HandleException { + RLock lock = redissonClient.getLock(String.format(GROUP_JOIN_USERS_KEY, joinUserDto.getGroupId())); + Assert.isFalse(lock.isLocked(), "当前考勤组添加用户操作正在执行中,请稍后再试"); + try { + lock.tryLock(2, TimeUnit.MINUTES); + QueryWrapper groupUserQueryWrapper = new QueryWrapper<>(); + groupUserQueryWrapper.lambda() + .eq(AttendanceGroupUser::getGroupId, joinUserDto.getGroupId()) + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .eq(AttendanceGroupUser::getType, GroupUserTypeEnum.CUR.getCode()); + List attendanceGroupUsers = attendanceGroupUserMapper.selectList(groupUserQueryWrapper); + List userIds = attendanceGroupUsers.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + // 查询考勤组使用的考勤机 + if (CollectionUtil.isEmpty(joinUserDto.getUserIds())) { + /* 删除考勤规则*/ + if (CollectionUtil.isNotEmpty(userIds)) { + + attendanceDailyRuleService.clearGroupRule(joinUserDto.getGroupId(), userIds); + /* 移除考勤机*/ + removeAttendanceMachine(userIds); + sendNotice(null, userIds, UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.REMOVE_USER); + /* 如果用户选择为空则删除该考勤组下所有成员*/ + lambdaUpdate() + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .in(AttendanceGroupUser::getUserId, userIds) + .set(AttendanceGroupUser::getRemoveTime, new Date()) + .set(AttendanceGroupUser::getDeleteMark, 1) + .update(); + return; + } + } + if (CollectionUtil.isNotEmpty(attendanceGroupUsers)) { + List collect = attendanceGroupUsers.stream().filter(groupUser -> !joinUserDto.getUserIds().contains(groupUser.getUserId())).collect(Collectors.toList()); + /* 要移除考勤组的成员*/ + if (CollectionUtil.isNotEmpty(collect)) { + List removeUserIds = collect.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + removeUsersHandle(joinUserDto.getGroupId(), removeUserIds, Maps.newHashMap(), UserProvider.getUser().getTenantId()); + } + } + + /* 新增的成员*/ + if (CollectionUtil.isEmpty(joinUserDto.getUserIds())) { + return; + } + + /* 查询新增用户是否已经在考勤组*/ + List addMachineUserIds = joinUserDto.getUserIds().stream().filter(userId -> !userIds.contains(userId)).collect(Collectors.toList()); + List addUserList = new ArrayList<>(); + for (String userId : addMachineUserIds) { + AttendanceGroupUser groupUser = new AttendanceGroupUser(); + groupUser.setId(RandomUtil.uuId()); + groupUser.setUserId(userId); + groupUser.setGroupId(joinUserDto.getGroupId()); + groupUser.setType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setUserGroupType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setCreatorUserId(UserProvider.getLoginUserId()); + groupUser.setCreatorTime(new Date()); + groupUser.setDeleteMark(0); + addUserList.add(groupUser); + } + + if (CollectionUtil.isNotEmpty(addMachineUserIds)) { + // 用户加入考勤机 + log.info("用户加入考勤机, 考勤组id: {}, 用户id: {}", joinUserDto.getGroupId(), addMachineUserIds); + addMachineUserIds.forEach(userId -> { + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_ADD); + attendanceUserFaceService.machineDeal(machineDealDto); + }); + } + if (CollectionUtil.isNotEmpty(addUserList)) { + saveBatch(addUserList); + attendanceDailyRuleService.addUserFixedHandle(joinUserDto.getGroupId(), addMachineUserIds); + addMachineUserIds.forEach(userId -> attendanceClockInService.generateRepairNumForUser(joinUserDto.getGroupId(), userId, 1)); + sendNotice(joinUserDto.getGroupId(), addMachineUserIds, UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.JOIN_GROUP); + attendanceDayStatisticsService.userJoinHandleData(UserProvider.getUser().getTenantId(), joinUserDto.getGroupId(), addMachineUserIds); + } + } catch (Exception e) { + log.error("考勤组添加成员失败{}", e.getMessage()); + throw new HandleException(e.getMessage()); + } finally { + lock.unlock(); + } + } + + private void removeUsersHandle(String groupId, List removeUserIds, Map departMap, String tenantId) { + removeUsersHandle(groupId, removeUserIds, departMap, UserProvider.getUser().getTenantId(), null); + } + + private void removeUsersHandle(String groupId, List removeUserIds, Map departMap, String tenantId, String orgId) { + List list = lambdaQuery().in(AttendanceGroupUser::getUserId, removeUserIds) + .eq(AttendanceGroupUser::getGroupId, groupId) + .list(); + /* 用户移除考勤机*/ + removeUserIds.forEach(userId -> { + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_DELETE, orgId); + attendanceUserFaceService.machineDeal(machineDealDto); + }); + list.forEach(remove -> { + String nullJson = JsonUtil.getJsonToBean(new ArrayList<>(), String.class); + remove.setTimeJson(nullJson); + remove.setRemoveTime(Objects.isNull(remove.getRemoveTime()) ? departMap.getOrDefault(remove.getUserId(), new Date()) : remove.getRemoveTime()); + remove.setSort(2000); + remove.setDeleteMark(1); + }); + updateBatchById(list); + /* 移除考勤规则*/ + Map.Entry stringDateEntry = departMap.entrySet().stream().findFirst().orElse(null); + attendanceDailyRuleService.clearGroupRule(groupId, removeUserIds, Objects.isNull(stringDateEntry) ? new Date() : stringDateEntry.getValue(), tenantId); + // 移除考勤组负责人 + attendanceGroupService.removeManager(groupId, removeUserIds); + sendNotice(null, removeUserIds, tenantId, AttendanceNoticeEnum.REMOVE_USER); + } + + private void sendNotice(String joinGroupId, List userIds, String tenantId, AttendanceNoticeEnum attendanceNoticeEnum) { + sendNotice(null, joinGroupId, userIds, tenantId, attendanceNoticeEnum); + } + + private void sendNotice(Date date, String joinGroupId, List userIds, String tenantId, AttendanceNoticeEnum attendanceNoticeEnum) { + UserChangeNoticeModel userChangeNoticeModel = new UserChangeNoticeModel(); + userChangeNoticeModel.setJoinGroupId(joinGroupId); + userChangeNoticeModel.setUserIds(userIds); + userChangeNoticeModel.setTenantId(tenantId); + userChangeNoticeModel.setStart(date); + userChangeNoticeModel.setAttendanceNoticeEnum(attendanceNoticeEnum); + attendanceNoticeHandler.send(userChangeNoticeModel); + } + + @Override + public void batchDeleteUsersForSendNotice(JoinUserDto joinUserDto) { + batchDeleteUsers(joinUserDto); + sendNotice(joinUserDto.getGroupId(), joinUserDto.getUserIds(), UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.REMOVE_USER); + } + + @Override + public void batchDeleteUsers(JoinUserDto joinUserDto) { + /* 移除考勤规则*/ + attendanceDailyRuleService.clearGroupRule(joinUserDto.getGroupId(), joinUserDto.getUserIds()); + /* 移除考勤机*/ + this.removeAttendanceMachine(joinUserDto.getUserIds()); + UpdateWrapper groupUserUpdateWrapper = new UpdateWrapper<>(); + groupUserUpdateWrapper.lambda() + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .in(AttendanceGroupUser::getUserId, joinUserDto.getUserIds()); + AttendanceGroupUser update = new AttendanceGroupUser(); + update.setRemoveTime(new Date()); + update.setDeleteMark(1); + attendanceGroupUserMapper.update(update, groupUserUpdateWrapper); + } + + @Override + public void groupUpdateByPersonnel(GroupUpdateByUserDTO groupUpdateByUserDTO) { +// JoinUserDto joinUserDto = new JoinUserDto(); +// joinUserDto.setUserIds(groupUpdateByUserDTO.getUserIds()); +// joinUserDto.setTenantId(groupUpdateByUserDTO.getTenantId()); +// if (Objects.equals(groupUpdateByUserDTO.getType(), 1)) { +// if (StringUtils.isBlank(groupUpdateByUserDTO.getToGroupId())) { +// return; +// } +// joinUserDto.setGroupId(groupUpdateByUserDTO.getToGroupId()); +// joinUserDto.setJoinTime(groupUpdateByUserDTO.getJoiningDate()); +// batchAddUsers(joinUserDto); +// sendNotice(groupUpdateByUserDTO.getToGroupId(), joinUserDto.getUserIds(), groupUpdateByUserDTO.getTenantId(), AttendanceNoticeEnum.JOIN_GROUP); +// return; +// } +// List list = lambdaQuery() +// .in(AttendanceGroupUser::getUserId, joinUserDto.getUserIds()) +// .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) +// .list(); +// if (Objects.equals(groupUpdateByUserDTO.getType(), 3) || Objects.equals(groupUpdateByUserDTO.getType(), 4)) { +// List collect = list.stream().map(AttendanceGroupUser::getGroupId).collect(Collectors.toList()); +// if (CollUtil.isNotEmpty(collect)) { +// collect.forEach(groupId -> { +// if (StringUtil.equals(groupId, groupUpdateByUserDTO.getToGroupId())) { +// return; +// } +// joinUserDto.setGroupId(groupId); +// batchDeleteUsers(joinUserDto); +// }); +// } +// if (StringUtil.isNotEmpty(groupUpdateByUserDTO.getToGroupId())) { +// joinUserDto.setGroupId(groupUpdateByUserDTO.getToGroupId()); +// // v1.8.1 +// joinUserDto.setOldUserList(list); +// batchAddUsers(joinUserDto); +// sendNotice(groupUpdateByUserDTO.getToGroupId(), joinUserDto.getUserIds(), groupUpdateByUserDTO.getTenantId(), Objects.equals(groupUpdateByUserDTO.getType(), 3) ? AttendanceNoticeEnum.GROUP_CHANGE_TRANSFER_POSITION : AttendanceNoticeEnum.GROUP_CHANGE_PROMOTION); +// } +// return; +// } +// if (Objects.equals(groupUpdateByUserDTO.getType(), 2) || Objects.equals(groupUpdateByUserDTO.getType(), 5)) { +// List collect = list.stream().map(AttendanceGroupUser::getGroupId).collect(Collectors.toList()); +// collect.forEach(groupId -> { +// if (StringUtil.equals(groupId, groupUpdateByUserDTO.getToGroupId())) { +// return; +// } +// joinUserDto.setGroupId(groupId); +// batchDeleteUsers(joinUserDto); +// }); +// sendNotice(null, joinUserDto.getUserIds(), groupUpdateByUserDTO.getTenantId(), AttendanceNoticeEnum.REMOVE_USER); +// } + } + + @Override + public List getGroupUserList(String id, Boolean isPermissions) { + List list = new ArrayList<>(); + List userBoundVO; + List userIds = attendanceGroupUserMapper.getGroupUserListByIds(List.of(id)); + if (Objects.isNull(isPermissions) || isPermissions.equals(Boolean.FALSE)) { + userBoundVO = v2UserApi.getUserPrimaryBoundBatch(userIds, null).getData(); + } else { + // 获取有权限的人员列表 + userBoundVO = getUserBoundVO(id, null); + } + if (CollectionUtil.isNotEmpty(userBoundVO)) { + for (UserBoundVO boundVo : userBoundVO) { + AttendanceGroupUserVo groupUserVo = new AttendanceGroupUserVo(); + groupUserVo.setUserId(boundVo.getId()); + groupUserVo.setRealName(boundVo.getUserName()); + groupUserVo.setNickname(boundVo.getNickname()); + groupUserVo.setHeadIcon(UploaderUtil.uploaderImg(boundVo.getHeadIcon())); + groupUserVo.setPositionName(boundVo.getPositionName()); + list.add(groupUserVo); + } + } + return list; + } + + private void batchAddUsers(JoinUserDto joinUserDto) { + /* 查询新增用户是否已经在考勤组*/ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(AttendanceGroupUser::getGroupId, joinUserDto.getGroupId()) + .in(AttendanceGroupUser::getUserId, joinUserDto.getUserIds()) + .eq(AttendanceGroupUser::getDeleteMark, Boolean.FALSE) + .ne(AttendanceGroupUser::getType, GroupUserTypeEnum.BORROW.getCode()); + List dbGroupUserList = attendanceGroupUserMapper.selectList(queryWrapper); + + Map groupUserMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(dbGroupUserList)) { + for (AttendanceGroupUser groupUser : dbGroupUserList) { + groupUserMap.put(groupUser.getUserId(), groupUser); + } + } + List addUserList = new ArrayList<>(); + List addMachineUserIds = new ArrayList<>(); + for (String userId : joinUserDto.getUserIds()) { + AttendanceGroupUser groupUser = groupUserMap.get(userId); + if (groupUser != null && !GroupUserTypeEnum.REMOVE.getCode().equals(groupUser.getType())) { + continue; + } + groupUser = new AttendanceGroupUser(); + groupUser.setId(RandomUtil.uuId()); + groupUser.setUserId(userId); + groupUser.setGroupId(joinUserDto.getGroupId()); + groupUser.setType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setUserGroupType(GroupUserTypeEnum.CUR.getCode()); + groupUser.setCreatorUserId(UserProvider.getLoginUserId()); + groupUser.setCreatorTime(Objects.nonNull(joinUserDto.getJoinTime()) ? joinUserDto.getJoinTime() : new Date()); + addUserList.add(groupUser); + addMachineUserIds.add(userId); + } + + if (CollectionUtil.isNotEmpty(addMachineUserIds)) { + for (String userId : addMachineUserIds) { + /* 加入考勤机*/ + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_ADD); + attendanceUserFaceService.machineDeal(machineDealDto); + } + } + if (CollectionUtil.isNotEmpty(addUserList)) { + saveBatch(addUserList); + attendanceDayStatisticsService.userJoinHandleData(joinUserDto.getTenantId(), Objects.nonNull(joinUserDto.getJoinTime()) ? joinUserDto.getJoinTime() : new Date(), joinUserDto.getGroupId(), addMachineUserIds); + attendanceDailyRuleService.addUserFixedHandle(joinUserDto.getGroupId(), joinUserDto.getTenantId(), addMachineUserIds); + addMachineUserIds.forEach(userId -> attendanceClockInService.generateRepairNumForUser(joinUserDto.getGroupId(), userId, 1)); + + // v1.8.1 批量移动考勤组用户在原组的管理员权限 + if (!joinUserDto.getOldUserList().isEmpty()) { + List oldUserIds = joinUserDto.getOldUserList().stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + // 获取用户在新组的管理员权限 + List newGroupPermission = attendanceManagerPermissionMapper.getGroupManagerPermissionByUserIds(oldUserIds, joinUserDto.getGroupId()); + joinUserDto.getOldUserList().forEach(v -> { + // + if (null != newGroupPermission && !newGroupPermission.isEmpty()) { + List myList = newGroupPermission.stream().filter(item -> + item.getUserId().equals(v.getUserId()) + ).collect(Collectors.toList()); + if (!myList.isEmpty()) { + // 获取用户在原组的管理员权限 + List yuanGroup = attendanceManagerPermissionMapper.getGroupManagerPermission(v.getUserId(), v.getGroupId()); + attendanceManagerPermissionMapper.deleteByGroupUserId(v.getUserId(), v.getGroupId()); + if (CollectionUtil.isNotEmpty(yuanGroup)) { + List newGroupPermissionIds = myList.stream().map(AttendanceManagerPermission::getPermissionId).collect(Collectors.toList()); + List list = new ArrayList<>(); + // 用户在新组也有权限 移除用户在旧组中重复的权限 + yuanGroup.forEach(yuan -> { + if (!newGroupPermissionIds.contains(yuan.getPermissionId())) { + list.add(yuan); + } + }); + // 新增非重复的权限 + if (CollectionUtil.isNotEmpty(list)) { + attendanceManagerPermissionMapper.addBatch(list); + } + } + } else { + // 循环修改转移用户权限 + attendanceManagerPermissionMapper.updateUserGroupManagerPermission(v.getUserId(), v.getGroupId(), joinUserDto.getGroupId()); + } + } else { + // 循环修改转移用户权限 + attendanceManagerPermissionMapper.updateUserGroupManagerPermission(v.getUserId(), v.getGroupId(), joinUserDto.getGroupId()); + } + }); + } + } + } + + /** + * 检查用户是否已经加入考勤组 + * + * @param userIds 用户id + */ + @Override + public List checkUserGroup(String userIds) { + if (StrUtil.isBlank(userIds)) { + return new ArrayList<>(); + } + String[] userIdArr = userIds.split(","); + ArrayList userIdList = CollectionUtil.newArrayList(userIdArr); + List groupUserVoList = attendanceGroupUserMapper.getGroupInUserIds(userIdList, null); + if (CollectionUtil.isEmpty(groupUserVoList)) { + return new ArrayList<>(); + } + List groupUserVos = groupUserVoList.stream().filter(groupUser -> { + return GroupUserTypeEnum.CUR.getCode().equals(groupUser.getType()); + }).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(groupUserVos)) { + List userNameList = new ArrayList<>(); + Map userVoMap = groupUserVos.stream().collect(Collectors.toMap(AttendanceGroupUserVo::getUserId, Function.identity())); + List userBoundVos = v2UserApi.userListAndCopy(groupUserVos.stream().map(AttendanceGroupUserVo::getUserId).collect(Collectors.toList()), false, null); + // 拼接考勤组名称 + userBoundVos.forEach(user -> { + String append = user.getUserName() + "已加入" + userVoMap.get(user.getId()).getGroupName() + "考勤组"; + userNameList.add(append); + }); + return userNameList; + } + return new ArrayList<>(); + } + + @Override + public Map>> batchGetUserCycleList(DateRangeDto dateRangeDto, List groupIds, List userIds) throws Exception { + DateTime startDate = cn.hutool.core.date.DateUtil.beginOfDay(cn.hutool.core.date.DateUtil.parse(dateRangeDto.getStartDate())); + DateTime endDate = cn.hutool.core.date.DateUtil.endOfDay(cn.hutool.core.date.DateUtil.parse(dateRangeDto.getEndDate())); + DateRangeDto validDateRange = new DateRangeDto(startDate.toDateStr(), endDate.toDateStr()); + List groupUserList = this.lambdaQuery().in(CollUtil.isNotEmpty(groupIds), AttendanceGroupUser::getGroupId, groupIds).in(CollUtil.isNotEmpty(userIds), AttendanceGroupUser::getUserId, userIds).list(); + Map>> userMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(groupUserList)) { + Map> groupUsers = groupUserList.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getGroupId)); + // 【关键修复】收集每个用户的最早借调开始时间,用于计算本组结束时间 + Map userFirstSecondmentTimeMap = new HashMap<>(); + for (List users : groupUsers.values()) { + for (AttendanceGroupUser item : users) { + if (Integer.valueOf(2).equals(item.getUserGroupType())) { + // 借调记录,解析借调时间 + List dateVoList = JsonUtil.getJsonToList(item.getTimeJson(), SecondmentDateVo.class); + if (CollUtil.isNotEmpty(dateVoList)) { + for (SecondmentDateVo vo : dateVoList) { + if (vo.getStartTime() != null) { + DateTime secondmentStart = new DateTime(vo.getStartTime()); + String userId = item.getUserId(); + // 保留最早的借调开始时间 + if (!userFirstSecondmentTimeMap.containsKey(userId) || secondmentStart.isBefore(userFirstSecondmentTimeMap.get(userId))) { + userFirstSecondmentTimeMap.put(userId, secondmentStart); + } + } + } + } + } + } + } + for (Map.Entry> groupEntry : groupUsers.entrySet()) { + Map> userCycleMap = new HashMap<>(); + // 获取本考勤组数据 (userGroupType = 1) + Map> thisGroupUserMap = groupEntry.getValue().stream().filter(item -> Integer.valueOf(1).equals(item.getUserGroupType())).collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + // 获取借调考勤组数据 (userGroupType = 2) + Map> otherGroupUserMap = groupEntry.getValue().stream().filter(item -> Integer.valueOf(2).equals(item.getUserGroupType())).collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + // 传递借调时间映射 + dataAssemble(validDateRange, userCycleMap, thisGroupUserMap, true, userFirstSecondmentTimeMap); + dataAssemble(validDateRange, userCycleMap, otherGroupUserMap, false, userFirstSecondmentTimeMap); + userMap.put(groupEntry.getKey(), userCycleMap); + } + } + return userMap; + } + + private void dataAssemble(DateRangeDto dateRangeDto, Map> userCycleMap, Map> groupUserMap, boolean isThisGroup, Map userFirstSecondmentTimeMap) throws Exception { + if (CollUtil.isNotEmpty(groupUserMap)) { + // 解析查询日期范围(保持时间精度) + DateTime queryStart = cn.hutool.core.date.DateUtil.beginOfDay(cn.hutool.core.date.DateUtil.parse(dateRangeDto.getStartDate())); + DateTime queryEnd = cn.hutool.core.date.DateUtil.endOfDay(cn.hutool.core.date.DateUtil.parse(dateRangeDto.getEndDate())); + for (Map.Entry> entry : groupUserMap.entrySet()) { + String userId = entry.getKey(); + List cycleList = new ArrayList<>(); + List userList = entry.getValue(); + // 按创建时间排序 + userList = userList.stream().sorted(Comparator.comparing(AttendanceGroupUser::getCreatorTime)).collect(Collectors.toList()); + for (AttendanceGroupUser item : userList) { + if (isThisGroup) { + // ===== 本组周期计算 ===== + Date creatorTime = item.getCreatorTime(); + Date removeTime = item.getRemoveTime(); + // 如果创建时间在查询结束时间之后,跳过 + if (creatorTime != null && creatorTime.after(queryEnd.toJdkDate())) { + continue; + } + // 【关键修复】计算有效结束时间 + DateTime effectiveEnd; + if (removeTime != null) { + // 优先使用 removeTime + DateTime removeDateTime = new DateTime(removeTime); + effectiveEnd = removeDateTime.isAfter(queryEnd) ? queryEnd : removeDateTime; + } else if (userFirstSecondmentTimeMap.containsKey(userId)) { + // removeTime 为 null 时,使用最早借调开始时间作为本组结束时间 + DateTime firstSecondmentTime = userFirstSecondmentTimeMap.get(userId); + effectiveEnd = firstSecondmentTime.isAfter(queryEnd) ? queryEnd : firstSecondmentTime; + } else { + // 没有借调记录,使用查询结束时间 + effectiveEnd = queryEnd; + } + // 如果结束时间在查询开始时间之前,跳过 + if (effectiveEnd.isBefore(queryStart)) { + continue; + } + // 计算有效开始时间:max(加入时间,查询开始时间) + DateTime effectiveStart = (creatorTime != null && creatorTime.after(queryStart.toJdkDate())) ? new DateTime(creatorTime) : queryStart; + // 只有当开始时间 <= 结束时间时才计算重叠 + if (effectiveStart.isAfter(effectiveEnd)) { + continue; + } + // 计算重叠时间段 + HashMap dateTimeHashMap = setOverlap(effectiveStart, effectiveEnd, queryStart, queryEnd, false); + if (CollUtil.isNotEmpty(dateTimeHashMap)) { + DateTime startDt = dateTimeHashMap.get("startDate"); + DateTime endDt = dateTimeHashMap.get("endDate"); + String startStr = formatDateTime(startDt); + String endStr = formatDateTime(endDt); + cycleList.add(startStr + "至" + endStr); + } + } else { + // ===== 借调组周期计算 ===== + List dateVoList = JsonUtil.getJsonToList(item.getTimeJson(), SecondmentDateVo.class); + if (CollUtil.isNotEmpty(dateVoList)) { + for (SecondmentDateVo secondmentDateVo : dateVoList) { + Date startTime = secondmentDateVo.getStartTime(); + Date endTime = secondmentDateVo.getEndTime(); + // 借调结束时间在查询开始时间之前,跳过 + if (endTime != null && endTime.before(queryStart.toJdkDate())) { + continue; + } + // 借调开始时间在查询结束时间之后,跳过 + if (startTime != null && startTime.after(queryEnd.toJdkDate())) { + continue; + } + DateTime secondmentStart = new DateTime(startTime); + DateTime secondmentEnd = new DateTime(endTime); + // 借调结束时间不能超过查询结束时间 + DateTime effectiveEnd = secondmentEnd.isAfter(queryEnd) ? queryEnd : secondmentEnd; + // 只有当开始时间 <= 结束时间时才计算重叠 + if (secondmentStart.isAfter(effectiveEnd)) { + continue; + } + HashMap dateTimeHashMap = setOverlap(secondmentStart, effectiveEnd, queryStart, queryEnd, false); + if (CollUtil.isNotEmpty(dateTimeHashMap)) { + DateTime startDt = dateTimeHashMap.get("startDate"); + DateTime endDt = dateTimeHashMap.get("endDate"); + String startStr = formatDateTime(startDt); + String endStr = formatDateTime(endDt); + cycleList.add(startStr + "至" + endStr); + } + } + } + } + } + userCycleMap.put(userId, cycleList); + } + } + } + + /** + * 格式化日期时间:只显示日期部分,不显示时分秒 + */ + private String formatDateTime(DateTime dateTime) { + if (dateTime == null) { + return ""; + } + // 直接返回日期字符串,不显示时间 + return dateTime.toDateStr(); + } + /** + * 获取有权限的用户列表 + * + * @param filterList 筛选条件 + * @param workStatus 用户状态 + * @return 用户列表 + */ + private List getUserIdArr(List filterList, String workStatus) { + List groupIds = filterList.stream() + .map(GroupFilterDto::getGroupId) + .distinct() + .collect(Collectors.toList()); + List teamIds = filterList.stream() + .map(GroupFilterDto::getTeamId) + .distinct() + .collect(Collectors.toList()); + StatisticsEnumUtil.WorkStatusEnum workStatusEnum = StatisticsEnumUtil.WorkStatusEnum.getWorkStatusEnum(workStatus); + Assert.notNull(workStatusEnum, "工作状态不正确"); + List workStatusEnums = DayStatisticsUtils.getWorkStatusEnumList(Objects.requireNonNull(workStatusEnum)); + List userBoundVoList = batchGetUserBoundVO(groupIds, workStatusEnums); + return CollUtil.isNotEmpty(userBoundVoList) ? userBoundVoList : CollUtil.newArrayList(); + } + + /** + * 用户移除考勤机 + * + * @param removeUserIds 移除用户ids + */ + private void removeAttendanceMachine(List removeUserIds) { + System.out.println("移除考勤机用户..."); + /* 用户移除考勤机*/ + removeUserIds.forEach(userId -> { + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_DELETE); + attendanceUserFaceService.machineDeal(machineDealDto); + }); + } + + @Override + public Boolean userGroupUpdateBySecondNotice(String tenantId) { + String startTime = DateUtil.getmmNow() + ":00"; + String endTime = DateUtil.getmmNow() + ":59"; + List selfApproveVoList = attendanceLeaveApproveMapper.getSelfApproveList(startTime, endTime); + if (CollUtil.isEmpty(selfApproveVoList)) { + return Boolean.TRUE; + } + selfApproveVoList.forEach(item -> { + sendNotice(item.getStartTime(), item.getGroupId(), item.getUserIds(), UserProvider.getUser().getTenantId(), AttendanceNoticeEnum.GROUP_CHANGE_SECONDMENT); + }); + return Boolean.TRUE; + } + + @Override + public List listLineScheduleUsersByGroupId(String groupId) { + if (StringUtil.isEmpty(groupId)) { + return CollUtil.newArrayList(); + } + // 以当前时刻作为有效区间,过滤掉已离组与外派借出的人员,口径与排班界面 canLineSchedule 一致 + Date now = new Date(); + List groupUsers = getAttendanceGroupUsersOfSecondment(now, now, null, List.of(groupId)); + if (CollUtil.isEmpty(groupUsers)) { + return CollUtil.newArrayList(); + } + List userIds = groupUsers.stream() + .map(AttendanceGroupUser::getUserId) + .filter(StrUtil::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return CollUtil.newArrayList(); + } + // 批量取主岗位信息(id/userName/positionId/positionName) + ActionResult> userResult = v2UserApi.getAllUserInfoBatch(userIds, null); + List userInfoList = (userResult != null && userResult.getCode() == 200 && userResult.getData() != null) + ? userResult.getData() : CollUtil.newArrayList(); + Map userInfoMap = userInfoList.stream() + .collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (a, b) -> a)); + // 按考勤组划线排班配置过滤,留下的即为允许划线排班的成员 + List canLineUserIds = new ArrayList<>(userIds); + attendanceDailyRuleService.lineSchedulesConfigFilter(groupId, canLineUserIds); + Set canLineUserIdSet = new HashSet<>(canLineUserIds); + return userIds.stream().map(uid -> { + GroupUserLineScheduleVo vo = new GroupUserLineScheduleVo(); + vo.setUserId(uid); + UserBoundVO info = userInfoMap.get(uid); + if (Objects.nonNull(info)) { + vo.setUserName(info.getUserName()); + vo.setPositionId(info.getPositionId()); + vo.setPositionName(info.getPositionName()); + } + vo.setCanLineSchedule(canLineUserIdSet.contains(uid)); + return vo; + }).collect(Collectors.toList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserSettingServiceImpl.java new file mode 100644 index 0000000..6dbd205 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AttendanceUserSettingServiceImpl.java @@ -0,0 +1,547 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.mapper.AttendanceUserFaceMapper; +import jnpf.attendance.mapper.AttendanceUserSettingMapper; +import jnpf.attendance.service.AttendanceBaseSettingService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.AttendanceUserSettingService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceAppUserSetting; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.enums.attendance.UserSettingEnum; +import jnpf.enums.attendance.UserSettingTypeEnum; +import jnpf.model.attendance.dto.AppUserSettingQueryDto; +import jnpf.model.attendance.dto.AttendanceAppUserSettingDto; +import jnpf.model.attendance.vo.AttendanceBaseSettingVo; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.attendance.vo.RemindClockInVo; +import jnpf.model.attendance.vo.UserSettingVo; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @Author huanglinpan + * @Date 2024/8/8 10:55 + * @Version 1.0 (版本号) + */ +@Slf4j +@Service +public class AttendanceUserSettingServiceImpl extends SuperServiceImpl implements AttendanceUserSettingService { + + @Autowired + private UserProvider userProvider; + + @Autowired + private RedisUtil redisUtil; + + @Resource + private AttendanceUserSettingMapper attendanceUserSettingMapper; + + @Resource + private AttendanceGroupService attendanceGroupService; + + @Resource + private AttendanceUserService attendanceUserService; + + @Resource + private AttendanceBaseSettingService attendanceBaseSettingService; + + @Resource + private AttendanceUserFaceMapper attendanceUserFaceMapper; + + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveOrUpdate(AttendanceAppUserSettingDto attendanceAppUserSettingDto) { + String userId = userProvider.get().getUserId(); + attendanceAppUserSettingDto.setAssociationId(1 == attendanceAppUserSettingDto.getType() ? userId : attendanceAppUserSettingDto.getAssociationId()); + // 先根据Id 判断是新增还是修改 + if (null == attendanceAppUserSettingDto.getId() || attendanceAppUserSettingDto.getId().isEmpty()) { + // 校验是否重复生成 + String id = attendanceUserSettingMapper.getDetail(attendanceAppUserSettingDto); + if (null == id) { + // 新增时,直接将参数保存到ftb_attendance_app_user_setting表 + attendanceAppUserSettingDto.setId(FtbUtil.getId()); + attendanceUserSettingMapper.add(attendanceAppUserSettingDto); + if (1 == attendanceAppUserSettingDto.getType()) { + if (UserSettingEnum.PRE_WORK_REMINDER.equals(attendanceAppUserSettingDto.getCode()) || UserSettingEnum.END_WORK_REMINDER.equals(attendanceAppUserSettingDto.getCode())) { + refreshRemindRedisCache(attendanceAppUserSettingDto.getCode(), userId, attendanceAppUserSettingDto.getValue(), userProvider.get().getTenantId()); + } + } + } else { + attendanceAppUserSettingDto.setId(id); + //修改 + attendanceUserSettingMapper.update(attendanceAppUserSettingDto); + if (1 == attendanceAppUserSettingDto.getType()) { + if (UserSettingEnum.PRE_WORK_REMINDER.equals(attendanceAppUserSettingDto.getCode()) || UserSettingEnum.END_WORK_REMINDER.equals(attendanceAppUserSettingDto.getCode())) { + refreshRemindRedisCache(attendanceAppUserSettingDto.getCode(), userId, attendanceAppUserSettingDto.getValue(), userProvider.get().getTenantId()); + } + } + } + } else { + // 修改时,根据Id修改 用户配置 + attendanceUserSettingMapper.update(attendanceAppUserSettingDto); + if (1 == attendanceAppUserSettingDto.getType()) { + if (UserSettingEnum.PRE_WORK_REMINDER.equals(attendanceAppUserSettingDto.getCode()) || UserSettingEnum.END_WORK_REMINDER.equals(attendanceAppUserSettingDto.getCode())) { + refreshRemindRedisCache(attendanceAppUserSettingDto.getCode(), userId, attendanceAppUserSettingDto.getValue(), userProvider.get().getTenantId()); + } + } + + } + + } + + @Override + public List getList(AppUserSettingQueryDto appUserSettingQueryDto) { + String userId = userProvider.get().getUserId(); + // 根据type ,判断走考勤组逻辑还是个人设置逻辑 + if (1 == appUserSettingQueryDto.getType()) { + // 个人设置 根据PId查询个人设置列表 + appUserSettingQueryDto.setPId(null == appUserSettingQueryDto.getPId() ? "0000" : appUserSettingQueryDto.getPId()); + appUserSettingQueryDto.setAssociationId(userId); + List list = getUserSettingList(appUserSettingQueryDto); + if (null == list) { + return null; + } + // 是否包含查询考勤消息接收 + UserSettingVo filter = list.stream().filter(t -> t.getCode().equals(UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode())).findFirst().orElse(null); + // 如果考勤消息接受打开,查询子集 + if (null != filter && 1 == filter.getStatus()) { + appUserSettingQueryDto.setPId(filter.getSettingId()); + filter.setChild(getUserSettingList(appUserSettingQueryDto)); + } + // 查询是否包含人脸录入信息 + UserSettingVo face = list.stream().filter(t -> t.getCode().equals(UserSettingEnum.ATTENDANCE_SETTING_FACE.getCode())).findFirst().orElse(null); + if (null != face) { + // 查看user_face表有无该用户的人脸信息 + LambdaQueryWrapper faceQuery = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, userId) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace attendanceUserFace = attendanceUserFaceMapper.selectOne(faceQuery); + face.setStatus(null != attendanceUserFace ? 1 : 0); + } + return list; + } else if (2 == appUserSettingQueryDto.getType()){ + // 考勤组逻辑,查询用户能看到的考勤组列表并将对应考勤组的设置返回 + return getGroupSetting(appUserSettingQueryDto.getGroupName()); + + }else if (3 == appUserSettingQueryDto.getType()){ + // web全局设置 + // 查询设置列表 web 全局设置 查询全部 + List userSettingVos = attendanceUserSettingMapper.getSettingList(null,appUserSettingQueryDto.getType()); + if (null == userSettingVos) { + return new ArrayList<>(); + } + // 查看现有配置 + List userSetting = attendanceUserSettingMapper.getUserSetting(appUserSettingQueryDto); + // 设置默认数据 + List one = userSettingVos.stream().filter(v -> "000000".equals(v.getPId())).sorted(Comparator.comparing(UserSettingVo::getSort)).collect(Collectors.toList()); + // 赋值默认值 + one.forEach(v->{ + // 获取子集 + List collect = userSettingVos.stream().filter(va -> v.getSettingId().equals(va.getPId())).sorted(Comparator.comparing(UserSettingVo::getSort)).collect(Collectors.toList()); + Map map; + if (userSetting != null && !userSetting.isEmpty()) { + map = userSetting.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + } else { + map = null; + } + collect.forEach(value -> { + if (null == map || null == map.get(value.getCode())) { + // 设置默认值 默认全部打开 + value.setStatus(1); + setDefault(value.getCode(), value); + } else { + UserSettingVo userSettingVo = map.get(value.getCode()); + value.setStatus(userSettingVo.getStatus()); + value.setValue(userSettingVo.getValue()); + } + }); + v.setChild(collect); + }); + return one; + } else { + return null; + } + } + + @Nullable + private List getGroupSetting(String groupName) { + List attendanceGroupVos = attendanceGroupService.viewApprovalGroupList(groupName); + if (null == attendanceGroupVos || attendanceGroupVos.isEmpty()) { + return null; + } + // 查询考勤组的设置 + List groupIds = attendanceGroupVos.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + return setGroupSetting(groupIds, attendanceGroupVos); + } + + @NotNull + private List setGroupSetting(List groupIds, List attendanceGroupVos) { + List list = new ArrayList<>(); + List groupSetting = attendanceUserSettingMapper.getGroupSetting(groupIds, 2); + Map map; + if (groupSetting != null && !groupSetting.isEmpty()) { + map = groupSetting.stream().collect(Collectors.toMap(UserSettingVo::getAssociationId, Function.identity())); + } else { + map = null; + } + attendanceGroupVos.forEach(v -> { + UserSettingVo userSettingVo = new UserSettingVo(v.getGroupName(), 2, v.getId()); + if (null != map && !map.isEmpty() && null != map.get(v.getId())) { + UserSettingVo vo = map.get(v.getId()); + userSettingVo.setId(vo.getId()); + userSettingVo.setStatus(vo.getStatus()); + } else { + userSettingVo.setStatus(1); + } + list.add(userSettingVo); + }); + return list; + } + + + private List getUserSettingList(AppUserSettingQueryDto appUserSettingQueryDto) { + List list = new ArrayList<>(); + // 查询设置列表 + List userSettingVos = attendanceUserSettingMapper.getSettingList(appUserSettingQueryDto.getPId(),appUserSettingQueryDto.getType()); + if (null == userSettingVos) { + return list; + } + // 查询用户设置信息 + List userSetting = attendanceUserSettingMapper.getUserSetting(appUserSettingQueryDto); + Map map = null; + if (userSetting != null && !userSetting.isEmpty()) { + map = userSetting.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + // 如果查的是最高级且是个人设置 + if ("0000".equals(appUserSettingQueryDto.getPId()) && 1 == appUserSettingQueryDto.getType()) { + // 查询用户实时所属考勤组的 内勤打卡是否需要拍照设置 + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(new Date(), new Date(), List.of(userProvider.get().getUserId()), null); + if (CollUtil.isNotEmpty(attendanceGroupUsers)){ + AttendanceBaseSettingVo one = attendanceBaseSettingService.getOne(attendanceGroupUsers.get(0).getGroupId()); + if (null != one && (1 == one.getAttendancePhoto() || 1 == one.getFace())){ + // 如果该设置打开 ,上下班极速打卡默认为关闭。 + // 因为这里查找的是用户配置后的数据,可能会没有 + if (null != map.get(UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode())) { + map.get(UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode()).setStatus(0); + } + if (null != map.get(UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode())) { + map.get(UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode()).setStatus(0); + } + } + } + } + } else { + // 没有设置过个人设置 + // 如果查的是最高级且是个人设置,这时上下班极速打卡需要再次判断,是否打开需要拍照与人脸设置 + if ("0000".equals(appUserSettingQueryDto.getPId()) && 1 == appUserSettingQueryDto.getType()) { + map = new HashMap<>(); + UserSettingVo userSettingVo = new UserSettingVo(); + userSettingVo.setCode(UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode()); + UserSettingVo userSettingVo1 = new UserSettingVo(); + userSettingVo1.setCode(UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode()); + // 查询用户实时所属考勤组的 内勤打卡是否需要拍照设置,=与人脸设置是否打开 + List attendanceGroupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment(new Date(), new Date(), List.of(userProvider.get().getUserId()), null); + if (CollUtil.isNotEmpty(attendanceGroupUsers)){ + AttendanceBaseSettingVo one = attendanceBaseSettingService.getOne(attendanceGroupUsers.get(0).getGroupId()); + if (Objects.isNull(one)) { + userSettingVo.setStatus(1); + userSettingVo1.setStatus(1); + } else { + userSettingVo.setStatus(( 1 == one.getFace() || 1 == one.getAttendancePhoto()) ? 0 : 1); + userSettingVo1.setStatus(( 1 == one.getFace() || 1 == one.getAttendancePhoto()) ? 0 : 1); + } + map.put(UserSettingEnum.ATTENDANCE_SETTING_START_SPEED_CHECK.getCode(), userSettingVo); + map.put(UserSettingEnum.ATTENDANCE_SETTING_END_SPEED_CHECK.getCode(), userSettingVo1); + } + } + } + // 因没有初始化数据,所以可能查不到数据,查不到默认开启 + Map finalMap = map; + userSettingVos.forEach(v -> { + UserSettingVo userSettingVo = new UserSettingVo(v.getSettingId(), v.getSettingName(), v.getCode(), appUserSettingQueryDto.getType(), appUserSettingQueryDto.getAssociationId(),v.getSort()); + if (null != finalMap && !finalMap.isEmpty()) { + UserSettingVo vo = finalMap.get(v.getCode()); + if (null != vo) { + userSettingVo.setId(vo.getId()); + userSettingVo.setStatus(vo.getStatus()); + userSettingVo.setValue(vo.getValue()); + } else { + userSettingVo.setStatus(1); + setDefault(v.getCode(), userSettingVo); + } + } else { + userSettingVo.setStatus(1); + setDefault(v.getCode(), userSettingVo); + } + list.add(userSettingVo); + }); + // 根据sort字段正序排序 + return list.stream().sorted(Comparator.comparing(UserSettingVo::getSort)).collect(Collectors.toList()); + } + + /** + * 默认设置value + * + * @param code code + * @param userSettingVo 用户配置 + */ + private static void setDefault(String code, UserSettingVo userSettingVo) { + if (UserSettingEnum.ATTENDANCE_SETTING_PRE_WORK_REMINDER.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 10); + } + if (UserSettingEnum.ATTENDANCE_SETTING_END_WORK_REMINDER.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 0); + } + if (UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_DAILY_REPORT_SEND.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 1); + } + if (UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_MONTHLY_REPORT_SEND.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 1); + } + if (UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_GROUP_NO_CHECKIN_REMIND_ADMIN.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 3); + } + if (UserSettingEnum.ATTENDANCE_SETTING_ATTENDANCE_TEAM_MONTHLY_REPORT_SEND.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 1); + } + if (UserSettingEnum.ATTENDANCE_SETTING_UN_SCHEDULED_REMIND_ADMIN.getCode().equals(code)) { + userSettingVo.setValue(Objects.nonNull(userSettingVo.getValue()) ? userSettingVo.getValue() : 3); + } + } + + @Override + public List getSettingList(String associationId, Integer type, String code) { + return getSettingList(CollUtil.newArrayList(associationId), type, CollUtil.newArrayList(code)); + } + + @Override + public List getSettingList(List associationId, Integer type, String code) { + return getSettingList(associationId, type, CollUtil.newArrayList(code)); + } + + @Override + public List getSettingList(List associationId, Integer type, List code) { + List list = new ArrayList<>(); + // 根据类型走不同逻辑 + if (1 == type) { + // 个人设置 + List groupSetting = attendanceUserSettingMapper.getUserSettingByCode(associationId, type, code); + Map> map; + if (groupSetting != null && !groupSetting.isEmpty()) { + map = groupSetting.stream().collect(Collectors.groupingBy(UserSettingVo::getAssociationId)); + } else { + map = null; + } + associationId.forEach(v -> { + List userSettingVosByAssociationId = null; + if (map != null) { + userSettingVosByAssociationId = map.get(v); + } + if (null != userSettingVosByAssociationId && !userSettingVosByAssociationId.isEmpty()) { + Map codeMap = userSettingVosByAssociationId.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + code.forEach(v1 -> { + if (null != codeMap.get(v1)) { + UserSettingVo userSettingVo = new UserSettingVo(type, v, codeMap.get(v1).getStatus(), v1, codeMap.get(v1).getValue()); + setDefault(v1, userSettingVo); + list.add(userSettingVo); + } else { + UserSettingVo userSettingVo = new UserSettingVo(type, v, 1, v1); + setDefault(v1, userSettingVo); + list.add(userSettingVo); + } + }); + } else { + code.forEach(v1 -> { + UserSettingVo userSettingVo = new UserSettingVo(type, v, 1, v1); + setDefault(v1, userSettingVo); + list.add(userSettingVo); + }); + } + }); + return list; + } else { + // 考勤组设置 + List groupSetting = attendanceUserSettingMapper.getGroupSetting(associationId, type); + Map map; + if (groupSetting != null && !groupSetting.isEmpty()) { + map = groupSetting.stream().collect(Collectors.toMap(UserSettingVo::getAssociationId, Function.identity())); + } else { + map = null; + } + associationId.forEach(v -> { + UserSettingVo userSettingVo = new UserSettingVo(type, v); + if (null != map && !map.isEmpty() && null != map.get(v)) { + userSettingVo.setStatus(map.get(v).getStatus()); + } else { + userSettingVo.setStatus(1); + } + list.add(userSettingVo); + }); + return list; + } + } + + @Override + public UserSettingVo checkWebSetting(String code) { + // 校验指定的web全局设置是否打开 + List groupSetting = attendanceUserSettingMapper.getUserSettingByCode(null, UserSettingTypeEnum.WEB_MESSAGE_SETTING.getCode(), CollUtil.newArrayList(code)); + if (null != groupSetting && !groupSetting.isEmpty()) { + return groupSetting.get(0); + } + // 设置默认值 + UserSettingVo userSettingVo = new UserSettingVo(); + userSettingVo.setCode(code); + userSettingVo.setStatus(1); + setDefault(code, userSettingVo); + return userSettingVo; + } + + @Override + public void refreshRemindRedisCache(String code, String userId, Integer value, String tenantId) { + + DateDetail dateDetail = new DateDetail(); + String todayStr = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + String hashKey = tenantId + "_" + todayStr; + String json = redisUtil.getHashValues(ConstantUtil.FTB_REMIND_KEY, hashKey); + Map> remindMap = new HashMap<>(); + Map map = JSONUtil.toBean(json, Map.class); + for (Object key : map.keySet()) { + JSONArray jsonArray = JSONUtil.parseArray(map.get(key)); + remindMap.put(key.toString(), JSONUtil.toList(jsonArray, RemindClockInVo.class)); + } + // 重新计算对应用户未处理的记录 + remindMap.forEach((k, v) -> { + if (k.equals(userId)) { + for (RemindClockInVo clockIn : v) { + try { + if (clockIn.getDeal().equals(ConstantUtil.NUM_TRUE)) { + continue; + } + // 计算工作时间与提醒时间的差 + Date date = DateDetail.getStr2DateTime(clockIn.getWorkTime()); + Date remindTime; + if (code.equals(UserSettingEnum.PRE_WORK_REMINDER)) { + // 工作时间 - value + remindTime = dateDetail.addMinute(date, -value); + } else { + remindTime = dateDetail.addMinute(date, value); + } + clockIn.setRemindTime(DateDetail.getDate2Str(remindTime, DateDetail.DF2)); + } catch (Exception e) { + log.info(e.getMessage()); + } + } + } + }); + redisUtil.insertHash(ConstantUtil.FTB_REMIND_KEY, hashKey, JSONUtil.toJsonStr(remindMap)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveOrUpdateList(List attendanceAppUserSettingDto) { + // 循环保存,因一部分设置需要做消息通知 + attendanceAppUserSettingDto.forEach(this::saveOrUpdate); + } + + @Override + public List getListNew(AppUserSettingQueryDto appUserSettingQueryDto) { + String userId = userProvider.get().getUserId(); + // 根据type ,判断走考勤组逻辑还是个人设置逻辑 + if (1 == appUserSettingQueryDto.getType()) { + // 个人设置 根据PId查询个人设置列表 + appUserSettingQueryDto.setPId(null == appUserSettingQueryDto.getPId() ? "0000" : appUserSettingQueryDto.getPId()); + appUserSettingQueryDto.setAssociationId(userId); + List list = getUserSettingList(appUserSettingQueryDto); + if (null == list) { + return null; + } + // 是否包含查询考勤消息接收 + UserSettingVo filter = list.stream().filter(t -> t.getCode().equals(UserSettingEnum.ATTENDANCE_SETTING_MESSAGE_RECEIVE.getCode())).findFirst().orElse(null); + // 如果考勤消息接受打开,查询子集 + if (null != filter && 1 == filter.getStatus()) { + appUserSettingQueryDto.setPId(filter.getSettingId()); + filter.setChild(getUserSettingList(appUserSettingQueryDto)); + } + // 查询是否包含人脸录入信息 + UserSettingVo face = list.stream().filter(t -> t.getCode().equals(UserSettingEnum.ATTENDANCE_SETTING_FACE.getCode())).findFirst().orElse(null); + if (null != face) { + // 查看user_face表有无该用户的人脸信息 + LambdaQueryWrapper faceQuery = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, userId) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace attendanceUserFace = attendanceUserFaceMapper.selectOne(faceQuery); + face.setStatus(null != attendanceUserFace ? 1 : 0); + } + return list; + } else if (2 == appUserSettingQueryDto.getType()){ + // 考勤组逻辑,查询用户能看到的考勤组列表并将对应考勤组的设置返回 考勤V1.9优化 + return getGroupSettingNew(appUserSettingQueryDto.getGroupName()); + + }else if (3 == appUserSettingQueryDto.getType()){ + // web全局设置 + // 查询设置列表 web 全局设置 查询全部 + List userSettingVos = attendanceUserSettingMapper.getSettingList(null,appUserSettingQueryDto.getType()); + if (null == userSettingVos) { + return new ArrayList<>(); + } + // 查看现有配置 + List userSetting = attendanceUserSettingMapper.getUserSetting(appUserSettingQueryDto); + // 设置默认数据 + List one = userSettingVos.stream().filter(v -> "000000".equals(v.getPId())).sorted(Comparator.comparing(UserSettingVo::getSort)).collect(Collectors.toList()); + // 赋值默认值 + one.forEach(v->{ + // 获取子集 + List collect = userSettingVos.stream().filter(va -> v.getSettingId().equals(va.getPId())).sorted(Comparator.comparing(UserSettingVo::getSort)).collect(Collectors.toList()); + Map map; + if (userSetting != null && !userSetting.isEmpty()) { + map = userSetting.stream().collect(Collectors.toMap(UserSettingVo::getCode, Function.identity())); + } else { + map = null; + } + collect.forEach(value -> { + if (null == map || null == map.get(value.getCode())) { + // 设置默认值 默认全部打开 + value.setStatus(1); + setDefault(value.getCode(), value); + } else { + UserSettingVo userSettingVo = map.get(value.getCode()); + value.setStatus(userSettingVo.getStatus()); + value.setValue(userSettingVo.getValue()); + } + }); + v.setChild(collect); + }); + return one; + } else { + return null; + } + } + + @Nullable + private List getGroupSettingNew(String groupName) { + List attendanceGroupVos = attendanceGroupService.viewApprovalGroupListNew(groupName); + if (null == attendanceGroupVos || attendanceGroupVos.isEmpty()) { + return null; + } + // 查询考勤组的设置 + List groupIds = attendanceGroupVos.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + return setGroupSetting(groupIds, attendanceGroupVos); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AutoScheduleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AutoScheduleService.java new file mode 100644 index 0000000..6dd3d19 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/AutoScheduleService.java @@ -0,0 +1,833 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.schedule.*; +import jnpf.attendance.service.*; +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.UserWorkSituationDto; +import jnpf.model.attendance.vo.DayShiftRevenueStatVo; +import jnpf.model.attendance.vo.ShiftPeriodVo; +import jnpf.model.attendance.vo.WorkstationGroupUserVo; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; +import jnpf.model.attendance.vo.attendance.UserWorkSituationVo; +import jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo; +import jnpf.permission.V2PositionApi; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.util.UserProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 自动排班业务入口(实现待补全)。 + */ +@Service +public class AutoScheduleService { + + private static final Logger log = LoggerFactory.getLogger(AutoScheduleService.class); + + /** + * 「按营业额带状」选相似日时,tier1~3(及 tier2+tier3 合并)的最少样本天数默认值,与算法侧一致。 + *

可在 {@code application.yml} 中用键 {@value #PROPERTY_TEMPLATE_SIMILAR_DAYS_MIN_BAND_SAMPLES} 覆盖,无需改代码。 + */ + public static final int DEFAULT_TEMPLATE_SIMILAR_DAYS_MIN_BAND_SAMPLES = + ScheduleTemplateSimilarDaysAlgorithm.DEFAULT_MIN_BAND_SAMPLE_DAYS; + + /** 配置键:带状相似档最少样本天数(1~90,算法_ctor 再 clamp)。 */ + public static final String PROPERTY_TEMPLATE_SIMILAR_DAYS_MIN_BAND_SAMPLES = + "jnpf.ftb.schedule.template-similar-days-min-band-sample-days"; + + /** 配置键:相似日营业额严带 λ₁(tier1),开区间 (0,1),默认 0.10。 */ + public static final String PROPERTY_TEMPLATE_SIMILAR_DAYS_BAND_STRICT = + "jnpf.ftb.schedule.template-similar-days-band-strict"; + + /** 配置键:相似日营业额宽带 λ₂(tier2/tier3),开区间 (0,1),默认 0.15。 */ + public static final String PROPERTY_TEMPLATE_SIMILAR_DAYS_BAND_RELAXED = + "jnpf.ftb.schedule.template-similar-days-band-relaxed"; + + /** 配置键:考勤班次模糊匹配——计划时段相对考勤班次的覆盖率下限 {@code [0,1]} ,见 {@link AttendanceGroupShiftMatchConfig}。 */ + public static final String PROPERTY_ATTEND_MATCH_MIN_HISTORY_COVERAGE = + "jnpf.ftb.schedule.attendance-shift-match-min-history-coverage"; + + /** 配置键:考勤班次模糊匹配——考勤班次相对重叠的效率下限 {@code [0,1]} 。 */ + public static final String PROPERTY_ATTEND_MATCH_MIN_CANDIDATE_EFFICIENCY = + "jnpf.ftb.schedule.attendance-shift-match-min-candidate-efficiency"; + + @Autowired + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Autowired + private AttendanceShiftSettingService attendanceShiftSettingService; + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + @Autowired + private AttendanceUserService attendanceUserService; + + @Autowired + private WorkstationService workstationService; + + @Autowired + private ScheduleGroupRuleConfigService scheduleGroupRuleConfigService; + + @Autowired + private V2PositionApi v2PositionApi; + + /** 产品与文档 §7.4:历史中出现的岗位在主数据已无定义时的事后文案。 */ + private static final String MSG_HISTORICAL_POST_NOT_IN_MASTER = + "某时段下的历史岗位已被删除,请手动排班。"; + + /** + * 营业额带状相似日的最少样本天数;默认 10。 + *

YAML:{@code jnpf.ftb.schedule.template-similar-days-min-band-sample-days: 7} + */ + @Value("${jnpf.ftb.schedule.template-similar-days-min-band-sample-days:10}") + private int templateSimilarDaysMinBandSampleDays = DEFAULT_TEMPLATE_SIMILAR_DAYS_MIN_BAND_SAMPLES; + + /** tier1 营业额半宽 λ₁;YAML 见 {@link #PROPERTY_TEMPLATE_SIMILAR_DAYS_BAND_STRICT}。 */ + @Value("${jnpf.ftb.schedule.template-similar-days-band-strict:0.10}") + private String templateSimilarDaysBandStrict = + ScheduleTemplateSimilarDaysAlgorithm.DEFAULT_BAND_STRICT.toPlainString(); + + /** tier2/tier3 营业额半宽 λ₂;YAML 见 {@link #PROPERTY_TEMPLATE_SIMILAR_DAYS_BAND_RELAXED}。 */ + @Value("${jnpf.ftb.schedule.template-similar-days-band-relaxed:0.15}") + private String templateSimilarDaysBandRelaxed = + ScheduleTemplateSimilarDaysAlgorithm.DEFAULT_BAND_RELAXED.toPlainString(); + + /** YAML:{@linkplain #PROPERTY_ATTEND_MATCH_MIN_HISTORY_COVERAGE 历史覆盖比例下限(0~1)}。 */ + @Value("${jnpf.ftb.schedule.attendance-shift-match-min-history-coverage:0.7}") + private String attendanceShiftMatchMinHistoryCoverage = "0.7"; + + /** YAML:{@linkplain #PROPERTY_ATTEND_MATCH_MIN_CANDIDATE_EFFICIENCY 候选班次效率下限(0~1)}。 */ + @Value("${jnpf.ftb.schedule.attendance-shift-match-min-candidate-efficiency:0.6}") + private String attendanceShiftMatchMinCandidateEfficiency = "0.6"; + + /** + * 按排班区间与每日预估营业额启动排班。 + * + * @param scheduleStartDate 排班区间起始日(含) + * @param scheduleEndDate 排班区间结束日(含) + * @param estimatedRevenueByDay 各自然日预估营业额,key 为排班日 + * @return 排班结果;占位实现返回 {@code null},接入算法后返回非空 + */ + public ShiftPlanAssignmentResult start( + String groupId, + LocalDate scheduleStartDate, + LocalDate scheduleEndDate, + Map estimatedRevenueByDay) throws HandleException, QueryException { + int scheduleDayCount = + estimatedRevenueByDay == null ? 0 : (int) estimatedRevenueByDay.keySet().stream().distinct().count(); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_START, + String.format( + "groupId=%s schedule=[%s..%s] revenueDays(unique)=%d", + groupId, scheduleStartDate, scheduleEndDate, scheduleDayCount)); + //查询最近90天的历史 + List dayShiftRevenueStatVoList = + getLast90DaysHistoryData(groupId, scheduleStartDate, scheduleEndDate); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_HISTORY_SNAPSHOT, + "historyDayRows(group90dWindow)=" + dayShiftRevenueStatVoList.size()); + + ScheduleGroupRuleConfigVo scheduleGroupRuleConfigVo = queryScheduleRuleConfig(groupId); + Boolean revenueAvailableToggle = + scheduleGroupRuleConfigVo == null ? null : scheduleGroupRuleConfigVo.getIntelligentSchedulingHistoricalRevenueAvailable(); + boolean usePatternSimilarDays = + SchedulingSimilarDaysMode.shouldUseSchedulingPatternSimilarDays( + revenueAvailableToggle, dayShiftRevenueStatVoList); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_SIMILAR_DAYS, + "similar-days mode=" + + (usePatternSimilarDays ? "SCHED_PATTERN_NO_REVENUE" : "REVENUE_BAND") + + ", historyRevenueAvailableFlag=" + + revenueAvailableToggle + + ", autoPatternDueToBlankRevenue=" + + SchedulingSimilarDaysMode.allHistoricalRevenueNullOrZero(dayShiftRevenueStatVoList) + + (!usePatternSimilarDays + ? ", templateMinBandSamples(config)=" + templateSimilarDaysMinBandSampleDays + + ", revenueBandλ12(config)=" + + templateSimilarDaysBandStrict + + "/" + + templateSimilarDaysBandRelaxed + : "")); + + BigDecimal bandStrictCfg = + parseBigDecimalConfig( + templateSimilarDaysBandStrict, ScheduleTemplateSimilarDaysAlgorithm.DEFAULT_BAND_STRICT); + BigDecimal bandRelaxedCfg = + parseBigDecimalConfig( + templateSimilarDaysBandRelaxed, ScheduleTemplateSimilarDaysAlgorithm.DEFAULT_BAND_RELAXED); + + Map similarTemplateDays = + usePatternSimilarDays + ? new SchedulePatternSimilarDaysAlgorithm() + .findForScheduleDays(dayShiftRevenueStatVoList, estimatedRevenueByDay) + : new ScheduleTemplateSimilarDaysAlgorithm( + 10, templateSimilarDaysMinBandSampleDays, bandStrictCfg, bandRelaxedCfg) + .findForScheduleDays(dayShiftRevenueStatVoList, estimatedRevenueByDay); + for (Map.Entry e : similarTemplateDays.entrySet()) { + ScheduleTemplateSimilarDaysResult r = e.getValue(); + SchedulingForTestCheckLog.info( + log, + SchedulingForTestCheckLog.TEST_ISSUE_1_SIMILAR_HISTORICAL_DATES, + "pipeline_similar_days_per_schedule_day", + String.format( + "algo=AutoScheduleService.groupId=%s | scheduleDay=%s | similarDaysMode=%s | success=%s | tier=%s " + + "| similarReferenceDayCount=%d | message=%s | similarDatesBrief=%s", + groupId, + e.getKey(), + usePatternSimilarDays ? "PATTERN_SCHEDULE_NO_REVENUE_BAND" : "REVENUE_BAND_SIMILAR_DAYS", + r == null ? "null_result" : Boolean.toString(r.isSuccess()), + r == null ? "NONE" : r.getTier().name(), + r == null ? -1 : r.getSimilarDates().size(), + r == null ? "" : r.getMessage(), + r == null || r.getSimilarDates().isEmpty() + ? "[]" + : SchedulingForTestCheckLog.formatDatesBrief( + r.getSimilarDates(), SchedulingForTestCheckLog.DEFAULT_DATE_CAP))); + } + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_SIMILAR_DAYS, + "scheduleDaysWithSimilarTemplateKey=" + similarTemplateDays.size()); +// if(CollectionUtils.isEmpty(similarTemplateDays)){ +// +// } + + List similarHistoricalHalfHourRows = + SimilarHistoricalDaySlotTableBuilder.buildHalfHourRowsFromSuccessfulSimilarDays( + dayShiftRevenueStatVoList, similarTemplateDays); + List similarDaysNoHistoricalShiftNotes = + buildSuccessfulSimilarDaysNoHistoricalShiftExpansionNotes(similarHistoricalHalfHourRows, similarTemplateDays); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_HALF_HOUR_TABLE, + "halfHourHistoricalRows(forSuccessfulSimilarDays)=" + similarHistoricalHalfHourRows.size()); + log.error( + "similar historical half-hour staffing rows: count={} (for successful template days only)", + similarHistoricalHalfHourRows.size()); + + SlotPostRobustTargetConfig slotPostRobustTargetConfig = SlotPostRobustTargetConfig.defaults(); + Map halfHourPostDemandByScheduleDay = + SimilarDayDemandSteps4And5.buildPerScheduleDayDemandMatrices( + similarHistoricalHalfHourRows, similarTemplateDays, slotPostRobustTargetConfig); + SchedulingForTestCheckLog.info( + log, + SchedulingForTestCheckLog.TEST_ISSUE_2_HALF_HOUR_DEMAND_MATRIX, + "pipeline_after_demand_matrices", + String.format( + "algo=AutoScheduleService | groupId=%s | scheduleDaysWithDemandMatrix=%d | slotRobustConfig.maxMinusMinUseMedianThreshold=%d " + + "| imputeZeroForMissingSimilarDays=%s", + groupId, + halfHourPostDemandByScheduleDay.size(), + slotPostRobustTargetConfig.getMaxMinusMinUseMedianThreshold(), + slotPostRobustTargetConfig.isImputeZeroForMissingSimilarDays())); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_DEMAND_MATRIX, + "scheduleDaysWithDemandMatrix=" + halfHourPostDemandByScheduleDay.size()); + log.error( + "half-hour post demand matrices (step 4-5): scheduleDayCount={}", + halfHourPostDemandByScheduleDay.size()); + for (Map.Entry e : halfHourPostDemandByScheduleDay.entrySet()) { + log.error( + "demand matrix scheduleDay={} slots={} posts={}\n{}", + e.getKey(), + e.getValue().getSlotRangeLabelsOrdered().size(), + e.getValue().getPostIdsOrdered().size(), + e.getValue().toTsvTable()); + } + + // 第六~八步:历史常见固定班 + 贪心覆盖 + 划线兜底(规则配置已在流水线前段加载) + LineShiftCoverConfig lineShiftCoverConfig = resolveLineShiftCoverConfig(scheduleGroupRuleConfigVo); + Map historyByDay = + ScheduleDemandCoverSteps678.indexHistoryByDay(dayShiftRevenueStatVoList); + AttendanceGroupShiftMatchConfig attendanceShiftMatchConfig = resolveAttendanceGroupShiftMatchConfig(); + List shiftPeriodVoList = getPeriodListForIntelligentSchedule(groupId); + if (shiftPeriodVoList == null) { + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_COVER_678, + "attendance period list unresolved (null) → step6-8 no attendance pre-filter; §6.1 matcher will preserve blocks without shift id"); + } + Map demandCoverByScheduleDay = + ScheduleDemandCoverSteps678.coverAllScheduleDays( + halfHourPostDemandByScheduleDay, + similarTemplateDays, + historyByDay, + CommonFixedShiftDiscoveryConfig.defaults(), + lineShiftCoverConfig, + shiftPeriodVoList, + attendanceShiftMatchConfig, + null); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_COVER_678, + "scheduleDaysWithCoverResult=" + demandCoverByScheduleDay.size()); + log.error( + "demand cover steps 6-8: scheduleDayCount={}", + demandCoverByScheduleDay.size()); + + Map> shiftPlanByScheduleDay = + ShiftPlanBlocksFromDemandCoverBuilder.build( + demandCoverByScheduleDay, halfHourPostDemandByScheduleDay); + + Map draftBlockCountByDay = countBlocksPerScheduleDay(shiftPlanByScheduleDay); + int draftBlockCount = countShiftPlanBlocks(shiftPlanByScheduleDay); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_SHIFT_PLAN_DRAFT, + summarizeShiftPlan(shiftPlanByScheduleDay) + + String.format( + " | attendancePeriodCandidates=%d (before attendance shift-id match)", + shiftPeriodVoList == null ? 0 : shiftPeriodVoList.size())); + log.error( + "shift plan from demand cover (6-8): scheduleDayCount={}", + shiftPlanByScheduleDay.size()); + for (Map.Entry> e : shiftPlanByScheduleDay.entrySet()) { + log.error("shift plan scheduleDay={} blockCount={}", e.getKey(), e.getValue().size()); + for (ShiftPlanBlock block : e.getValue()) { + log.error( + " {} fixed={} posts={}", + block.getTimeRangeText(), + block.isFixedScheduling(), + block.getPostNeeds()); + } + } + + shiftPlanByScheduleDay = + ShiftPlanAttendanceShiftIdResolver.assignGroupShiftIdsOrRemoveUnmatched( + shiftPlanByScheduleDay, + shiftPeriodVoList, + attendanceShiftMatchConfig, + null); + int blocksAfterMatcher = countShiftPlanBlocks(shiftPlanByScheduleDay); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_ATTEND_SHIFT_MATCH, + summarizeShiftPlan(shiftPlanByScheduleDay) + + String.format(" | droppedByMatcher=%d", draftBlockCount - blocksAfterMatcher)); + if (shiftPeriodVoList != null && shiftPeriodVoList.isEmpty()) { + AutoSchedulePipelineLog.warn( + log, + AutoSchedulePipelineLog.PHASE_ATTEND_SHIFT_MATCH, + "attendance period list is empty → matcher cleared all draft blocks"); + } else if (shiftPeriodVoList == null) { + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_ATTEND_SHIFT_MATCH, + "attendance period list is null → preserving draft blocks without shift id binding (§6.1)"); + } + log.error( + "shift plan after attendance period id match: scheduleDayCount={}", + shiftPlanByScheduleDay.size()); + for (Map.Entry> e : shiftPlanByScheduleDay.entrySet()) { + for (ShiftPlanBlock block : e.getValue()) { + log.error( + " shiftId={} {} fixed={} posts={}", + block.getShiftId(), + block.getTimeRangeText(), + block.isFixedScheduling(), + block.getPostNeeds()); + } + } + + List matcherIncompleteNotes = + buildAttendanceMatcherIncompleteNotes(draftBlockCountByDay, shiftPlanByScheduleDay); + List pipelineIncompleteNotes = new ArrayList<>(); + pipelineIncompleteNotes.addAll( + buildPatternScheduleInsufficientHistoryNotes(similarTemplateDays)); + pipelineIncompleteNotes.addAll(similarDaysNoHistoricalShiftNotes); + pipelineIncompleteNotes.addAll(matcherIncompleteNotes); + pipelineIncompleteNotes.addAll( + buildHistoricalPostsMissingMasterDataNotes(shiftPlanByScheduleDay)); + + List workstationWithUsersVoList = + queryWorkstationConfig(groupId, scheduleStartDate, scheduleEndDate); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_WORKSTATION_POOL, + "workstations=" + workstationWithUsersVoList.size()); + // 排班规则(已与第六~八步划线参数、第九~十步人员规则共用同一次查询) + //用户的已工作状态 + Map userWorkSituationVoMap = + queryUserWorkSituation( + countScheduleDays(scheduleStartDate, scheduleEndDate), workstationWithUsersVoList); + + SchedulePeriodWorkTracker workTracker = SchedulePeriodWorkTracker.from(userWorkSituationVoMap); + StaffRuleEvaluationPort staffRuleEvaluationPort = + buildStaffRuleEvaluationPort(scheduleGroupRuleConfigVo, workTracker.asSituationMap()); + AutoSchedulePipelineLog.info( + log, + AutoSchedulePipelineLog.PHASE_STAFF_RULES, + "evaluator=" + staffRuleEvaluationPort.getClass().getSimpleName()); + log.error("staff rule evaluation (steps 9-10): {}", staffRuleEvaluationPort.getClass().getSimpleName()); + + ShiftPlanAssignmentResult finalAssignment = + ShiftPlanAssignmentResult.prependIncompleteReasons( + pipelineIncompleteNotes, + ShiftPlanFinalStaffAssigner.assign( + shiftPlanByScheduleDay, + workstationWithUsersVoList, + scheduleGroupRuleConfigVo, + staffRuleEvaluationPort, + workTracker, + groupId, + null)); + log.error( + "final staff assignment: shiftViews={} employeeViews={} assigned={}/need={}", + finalAssignment.getByShift().size(), + finalAssignment.getByEmployee().size(), + finalAssignment.countAssignedStaff(), + finalAssignment.countTotalNeed()); + ShiftPlanAssignmentResultLogger.logInfo(log, finalAssignment); + ShiftPlanAssignmentResultLogger.logDebug(log, finalAssignment); + SchedulingForTestCheckLog.info( + log, + SchedulingForTestCheckLog.TEST_ISSUE_3_STAFF_ASSIGNMENT, + "pipeline_staff_assignment_summary", + String.format( + "algo=AutoScheduleService | groupId=%s | incompleteReasonCount=%d | shiftBlockViews=%d | employeeViews=%d | assignedHeadcount=%d | totalNeed=%d", + groupId, + finalAssignment.getIncompleteScheduleReasons() == null + ? 0 + : finalAssignment.getIncompleteScheduleReasons().size(), + finalAssignment.getByShift().size(), + finalAssignment.getByEmployee().size(), + finalAssignment.countAssignedStaff(), + finalAssignment.countTotalNeed())); + return finalAssignment; + } + + private static int countShiftPlanBlocks(Map> shiftPlanByScheduleDay) { + if (shiftPlanByScheduleDay == null || shiftPlanByScheduleDay.isEmpty()) { + return 0; + } + int n = 0; + for (List blocks : shiftPlanByScheduleDay.values()) { + if (blocks != null) { + n += blocks.size(); + } + } + return n; + } + + /** + * 单行摘要:各日块数合计、固定/划线块、已绑定考勤班次 id 的块数(用于线上快速判断「计划是否被 matcher 剔空」)。 + */ + private static String summarizeShiftPlan(Map> shiftPlanByScheduleDay) { + if (shiftPlanByScheduleDay == null || shiftPlanByScheduleDay.isEmpty()) { + return "scheduleDays=0 totalBlocks=0 fixedBlocks=0 lineBlocks=0 blocksWithAttendanceShiftId=0"; + } + int days = shiftPlanByScheduleDay.size(); + int total = 0; + int fixed = 0; + int line = 0; + int withShiftId = 0; + for (List blocks : shiftPlanByScheduleDay.values()) { + if (blocks == null) { + continue; + } + for (ShiftPlanBlock b : blocks) { + total++; + if (b.isFixedScheduling()) { + fixed++; + } else { + line++; + } + String sid = b.getShiftId(); + if (sid != null && !sid.trim().isEmpty()) { + withShiftId++; + } + } + } + return String.format( + "scheduleDays=%d totalBlocks=%d fixedBlocks=%d lineBlocks=%d blocksWithAttendanceShiftId=%d", + days, total, fixed, line, withShiftId); + } + + + private static Map countBlocksPerScheduleDay( + Map> planByDay) { + if (planByDay == null || planByDay.isEmpty()) { + return Collections.emptyMap(); + } + Map m = new LinkedHashMap<>(); + for (Map.Entry> e : planByDay.entrySet()) { + LocalDate d = e.getKey(); + List blocks = e.getValue(); + if (d == null || blocks == null) { + continue; + } + m.put(d, blocks.size()); + } + return m; + } + + private static BigDecimal parseBigDecimalConfig(String raw, BigDecimal fallback) { + if (raw == null || raw.trim().isEmpty()) { + return fallback; + } + try { + return new BigDecimal(raw.trim()); + } catch (NumberFormatException ex) { + return fallback; + } + } + + /** + * 规律模式第三步失败(租户无营业额补充文档之 (3):历史排班数据不足)。 + * + *

每条对应一个无法在相似日维度启动的 {@code estimatedRevenueByDay.key}(其它日期仍照常预排)。 + */ + private static List buildPatternScheduleInsufficientHistoryNotes( + Map similarTemplateDaysByScheduleDay) { + if (similarTemplateDaysByScheduleDay == null || similarTemplateDaysByScheduleDay.isEmpty()) { + return Collections.emptyList(); + } + List notes = new ArrayList<>(); + List days = new ArrayList<>(similarTemplateDaysByScheduleDay.keySet()); + days.sort(Comparator.naturalOrder()); + for (LocalDate d : days) { + if (d == null) { + continue; + } + ScheduleTemplateSimilarDaysResult sim = similarTemplateDaysByScheduleDay.get(d); + if (sim == null || sim.isSuccess()) { + continue; + } + if (!ScheduleTemplateSimilarDaysResult.isInsufficientHistoryForPatternScheduleMessage(sim.getMessage())) { + continue; + } + notes.add( + new ShiftPlanAssignmentResult.IncompleteScheduleReason( + d, + "", + "", + ShiftPlanAssignmentResult.INCOMPLETE_PATTERN_HISTORY_INSUFFICIENT, + sim.getMessage())); + } + return notes; + } + + /** §2.4:第三步 success,但召回日下均无可用班次明细 → 单日阻断说明(不影响其他已成功展开的排班日)。 */ + private static List buildSuccessfulSimilarDaysNoHistoricalShiftExpansionNotes( + List halfHourRows, + Map similarTemplateDaysByScheduleDay) { + List scheduleDays = + SimilarHistoricalDaySlotTableBuilder.listScheduleDaysWithSuccessfulSimilarityButZeroHalfHourRows( + similarTemplateDaysByScheduleDay, halfHourRows); + if (scheduleDays.isEmpty()) { + return Collections.emptyList(); + } + List notes = new ArrayList<>(scheduleDays.size()); + for (LocalDate d : scheduleDays) { + notes.add( + new ShiftPlanAssignmentResult.IncompleteScheduleReason( + d, + "", + "", + ShiftPlanAssignmentResult.INCOMPLETE_SIMILAR_HISTORY_NO_SHIFT_DATA, + d.toString() + + " 没找到可参考的历史排班数据,无法进行预排班。")); + } + return notes; + } + + /** 考勤班次 matcher 剔除的班段数 → 单日一条说明(仅剔除数 > 0)。 */ + private static List buildAttendanceMatcherIncompleteNotes( + Map draftBlockCountByDay, + Map> planAfterMatcher) { + if (draftBlockCountByDay == null || draftBlockCountByDay.isEmpty()) { + return Collections.emptyList(); + } + List notes = new ArrayList<>(); + for (Map.Entry e : draftBlockCountByDay.entrySet()) { + LocalDate d = e.getKey(); + int before = e.getValue() == null ? 0 : Math.max(0, e.getValue()); + List kept = + planAfterMatcher == null ? Collections.emptyList() : planAfterMatcher.get(d); + int after = kept == null ? 0 : kept.size(); + if (before <= after) { + continue; + } + int dropped = before - after; + notes.add( + new ShiftPlanAssignmentResult.IncompleteScheduleReason( + d, + "", + "", + ShiftPlanAssignmentResult.INCOMPLETE_ATTEND_SHIFT_BLOCKS_DROPPED, + String.format( + "有%d个班段未匹配考勤组班次时段并已移除(请核对班段时间与班次配置是否一致)", + dropped))); + } + notes.sort( + Comparator.comparing(ShiftPlanAssignmentResult.IncompleteScheduleReason::getScheduleDay)); + return notes; + } + + /** + * 排班区间包含的自然日天数({@code scheduleStartDate}、{@code scheduleEndDate} 均为闭区间,含首尾两日)。 + *

+ * 例如 2025-01-15~2025-01-17 返回 3。 + */ + public static int countScheduleDays(LocalDate scheduleStartDate, LocalDate scheduleEndDate) { + Objects.requireNonNull(scheduleStartDate, "scheduleStartDate"); + Objects.requireNonNull(scheduleEndDate, "scheduleEndDate"); + if (scheduleEndDate.isBefore(scheduleStartDate)) { + throw new IllegalArgumentException("scheduleEndDate must not be before scheduleStartDate"); + } + return (int) ChronoUnit.DAYS.between(scheduleStartDate, scheduleEndDate) + 1; + } + + /** + * 获取最近90天的营业额及排班数据 + * + * @param scheduleStartDate + * @param scheduleEndDate + */ + private List getLast90DaysHistoryData( + String groupId, LocalDate scheduleStartDate, LocalDate scheduleEndDate) { + return attendanceDailyRuleService.getGroupShiftHistory90Days(groupId); + + } + + /** + * 查询用户已工作状态 + */ + private Map queryUserWorkSituation(int days, List workstations) { + if (workstations == null || workstations.isEmpty()) { + return Collections.emptyMap(); + } + Set userIds = new LinkedHashSet<>(); + for (WorkstationWithUsersVo w : workstations) { + if (w == null || w.getUserList() == null) { + continue; + } + for (WorkstationGroupUserVo u : w.getUserList()) { + if (u != null && u.getUserId() != null && !u.getUserId().trim().isEmpty()) { + userIds.add(u.getUserId().trim()); + } + } + } + if (userIds.isEmpty()) { + return Collections.emptyMap(); + } + UserWorkSituationDto workSituationDto = new UserWorkSituationDto(new ArrayList<>(userIds), days); + return attendanceDayStatisticsService.queryUserWorkSituation(workSituationDto); + } + + /** + * 第九、十步人员规则:{@link ScheduleGroupRuleConfigVo#getFixedScheduling()} + {@link UserWorkSituationVo}; + * 无配置且无工作状态数据时使用全放行实现便于联调。 + */ + private StaffRuleEvaluationPort buildStaffRuleEvaluationPort( + ScheduleGroupRuleConfigVo scheduleGroupRuleConfigVo, + Map userWorkSituationVoMap) { + if (scheduleGroupRuleConfigVo == null + && (userWorkSituationVoMap == null || userWorkSituationVoMap.isEmpty())) { + return new StaffRuleEvaluationPortPermissive(); + } + return new StaffRuleEvaluationPortUsingScheduleRulesAndWorkSituation( + scheduleGroupRuleConfigVo == null ? null : scheduleGroupRuleConfigVo.getFixedScheduling(), + userWorkSituationVoMap == null ? Collections.emptyMap() : userWorkSituationVoMap); + } + + /** + * 查询工作站配置(门店/组织在排班区间内可用工作站、岗位能力、可排人员绑定等)。 + * + * @param groupId 考勤组 ID + * @param scheduleStartDate 排班区间起始日(含) + * @param scheduleEndDate 排班区间结束日(含) + */ + private List queryWorkstationConfig( + String groupId, LocalDate scheduleStartDate, LocalDate scheduleEndDate) { + return workstationService.listWorkstationsByGroupId( + groupId, toDateAtStartOfDay(scheduleStartDate), toDateAtStartOfDay(scheduleEndDate)); + } + + private static Date toDateAtStartOfDay(LocalDate day) { + Objects.requireNonNull(day, "day"); + return Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + /** + * 查询排班规则(第二步~第八步算法侧配置:相似日、需求聚合、固定班发现、划线兜底等)。 + * + * @param groupId 门店/组织等业务主键 + + * @return 规则数据;占位返回 {@code null},实现时建议组装为 {@link } 所需各 Config + */ + private ScheduleGroupRuleConfigVo queryScheduleRuleConfig(String groupId) throws HandleException, QueryException { + return scheduleGroupRuleConfigService.getRuleConfig(groupId); + + } + + /** + * 第八步划线参数:有考勤组划线配置时映射最短/最长/段数;否则算法默认(2h~4h、每日每岗最多 4 段)。 + */ + private static LineShiftCoverConfig resolveLineShiftCoverConfig(ScheduleGroupRuleConfigVo scheduleGroupRuleConfigVo) { + if (scheduleGroupRuleConfigVo != null && scheduleGroupRuleConfigVo.getLineScheduling() != null) { + return LineShiftCoverConfig.fromLineSchedulingRule(scheduleGroupRuleConfigVo.getLineScheduling()); + } + return LineShiftCoverConfig.defaults(); + } + + /** + * 获取指定考勤组班次列表——智能排班:`null` 与空列表语义见 §6.1 / §6.2。 + * + * @param groupId 考勤组 ID + * @return 班次 VO 列表,或 {@code null} + */ + private List getPeriodListForIntelligentSchedule(String groupId) { + return attendanceShiftSettingService.periodListForIntelligentSchedule(groupId); + } + + /** 考勤班次匹配阈值:YAML {@value #PROPERTY_ATTEND_MATCH_MIN_HISTORY_COVERAGE} / {@value #PROPERTY_ATTEND_MATCH_MIN_CANDIDATE_EFFICIENCY}。 */ + private AttendanceGroupShiftMatchConfig resolveAttendanceGroupShiftMatchConfig() { + AttendanceGroupShiftMatchConfig defaults = AttendanceGroupShiftMatchConfig.defaults(); + double cov = parseRatio01Inclusive(attendanceShiftMatchMinHistoryCoverage, defaults.getMinHistoryCoverageRatio()); + double eff = + parseRatio01Inclusive( + attendanceShiftMatchMinCandidateEfficiency, + defaults.getMinCandidateEfficiencyRatio()); + return new AttendanceGroupShiftMatchConfig(cov, eff); + } + + private static double parseRatio01Inclusive(String raw, double fallback) { + if (raw == null || raw.trim().isEmpty()) { + return clamp01(fallback); + } + try { + double v = Double.parseDouble(raw.trim().replace(",", ".")); + return clamp01(v); + } catch (NumberFormatException ex) { + return clamp01(fallback); + } + } + + private static double clamp01(double v) { + if (v < 0) { + return 0; + } + if (v > 1) { + return 1; + } + return v; + } + + private String resolveTenantIdForPermissionApi() { + try { + if (UserProvider.getUser() != null) { + return UserProvider.getUser().getTenantId(); + } + } catch (Exception ex) { + log.error( + "{} phase=POSITION_LOOKUP | tenant unavailable ({}); permission tenantId=null", + AutoSchedulePipelineLog.MARKER, + ex.toString()); + } + return null; + } + + /** 产品与文档 §7.4:历史需求中的岗位 id 在主数据不存在时追加 incomplete(不阻断流水线)。 */ + private List buildHistoricalPostsMissingMasterDataNotes( + Map> planAfterMatcher) { + if (planAfterMatcher == null || planAfterMatcher.isEmpty()) { + return Collections.emptyList(); + } + LinkedHashSet postIds = new LinkedHashSet<>(); + for (List blocks : planAfterMatcher.values()) { + if (blocks == null) { + continue; + } + for (ShiftPlanBlock block : blocks) { + for (ShiftPlanPostNeed pn : block.getPostNeeds()) { + if (pn == null) { + continue; + } + String pid = pn.getPostId(); + if (pid == null || pid.isBlank()) { + continue; + } + postIds.add(pid.trim()); + } + } + } + if (postIds.isEmpty()) { + return Collections.emptyList(); + } + ActionResult> resolved = + v2PositionApi.listPositionBaseInfoByIds(new ArrayList<>(postIds), resolveTenantIdForPermissionApi()); + if (resolved == null || resolved.getCode() != 200 || resolved.getData() == null) { + log.error( + "{} phase=POSITION_LOOKUP | listPositionBaseInfoByIds failed — skip §7.4 master-post checks (code={})", + AutoSchedulePipelineLog.MARKER, + resolved == null ? "null" : resolved.getCode()); + return Collections.emptyList(); + } + Set existingIds = + resolved.getData().stream() + .map(PositionBaseInfoVO::getId) + .filter(Objects::nonNull) + .map(String::trim) + .filter(id -> !id.isEmpty()) + .collect(Collectors.toSet()); + LinkedHashSet missing = new LinkedHashSet<>(); + for (String pid : postIds) { + if (!existingIds.contains(pid)) { + missing.add(pid); + } + } + if (missing.isEmpty()) { + return Collections.emptyList(); + } + + ArrayList sortedDays = new ArrayList<>(planAfterMatcher.keySet()); + sortedDays.sort(Comparator.naturalOrder()); + LinkedHashMap byDayPost = new LinkedHashMap<>(); + for (LocalDate d : sortedDays) { + List blocks = planAfterMatcher.get(d); + if (d == null || blocks == null) { + continue; + } + for (ShiftPlanBlock block : blocks) { + for (ShiftPlanPostNeed pn : block.getPostNeeds()) { + if (pn == null) { + continue; + } + String pid = pn.getPostId(); + if (pid == null || pid.isBlank()) { + continue; + } + pid = pid.trim(); + if (!missing.contains(pid)) { + continue; + } + String composite = d + "|" + pid; + byDayPost.putIfAbsent( + composite, + new ShiftPlanAssignmentResult.IncompleteScheduleReason( + d, + "", + pid, + ShiftPlanAssignmentResult.INCOMPLETE_HISTORICAL_POST_MISSING_MASTER, + MSG_HISTORICAL_POST_NOT_IN_MASTER + "(岗位ID=" + pid + ")")); + } + } + } + return new ArrayList<>(byDayPost.values()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ClockInResultServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ClockInResultServiceImpl.java new file mode 100644 index 0000000..25ffbe6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ClockInResultServiceImpl.java @@ -0,0 +1,207 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.mapper.AttendanceDailyRuleMapper; +import jnpf.attendance.mapper.ClockInResultMapper; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.ClockInResultService; +import jnpf.attendance.service.OvertimeRuleService; +import jnpf.attendance.service.handle.chain.RuleFilterChain; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import jnpf.util.UserProvider; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +/** + * 打卡服务实现2.0 + * + * @author yanwenfu + * @create 2025-09-23 + */ +@Slf4j +@Service +public class ClockInResultServiceImpl extends SuperServiceImpl implements ClockInResultService { + + @Resource + private AttendanceDailyRuleMapper attendanceDailyRuleMapper; + @Resource + private RuleFilterChain ruleFilterChain; + @Resource + private OvertimeRuleService overtimeRuleService; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private RuleScopeUtil ruleScopeUtil; + @Resource + private AttenceMachineService attenceMachineService; + + @Override + public List getTodayRuleList(Date today, UserInfo userInfo) { + + // 查询今日可以打卡的出勤规则 + List ruleList = attendanceDailyRuleMapper.getRuleClockTimeContainsToday(today, userInfo.getUserId()); + ruleList = clearErrorRule(ruleList, userInfo.getUserId(), today, null); + // 查询加班规则 + Map map = overtimeRuleService.getEffectDetailByUserList(List.of(userInfo.getUserId()), today); + return ruleFilterChain.execute(today, ruleList, map, userInfo); + } + + @Override + public List clearErrorRule(List ruleList, String userId, Date date, List effectGroupIds) { + + if (ruleList.isEmpty()) { + return new ArrayList<>(); + } + if (null == userId) { + return ruleList; + } + int ruleSize = ruleList.size(); + // 判断考勤组用户信息是否正常 + List egIds; + if (CollUtil.isEmpty(effectGroupIds)) { + List groupIds = ruleList.stream().distinct().map(FtbAttendanceDailyRule::getGroupId).collect(Collectors.toList()); + List groupUserList = attendanceUserService.getAttendanceGroupUsersOfSecondment(DateUtil.beginOfDay(date), DateUtil.endOfDay(date), List.of(userId), groupIds); + egIds = groupUserList.stream().map(AttendanceGroupUser::getGroupId).collect(Collectors.toList()); + } else { + egIds = effectGroupIds; + } + ruleList.removeIf(v -> !egIds.contains(v.getGroupId())); + if (ruleList.isEmpty()) { + return new ArrayList<>(); + } + int currentSize = 1; + if (ruleSize != currentSize && ruleList.get(0).getRn() != null) { + // 给rn重新赋值 + for (FtbAttendanceDailyRule dailyRule : ruleList) { + dailyRule.setRn(currentSize++); + } + } + return ruleList; + } + + @Override + public List getMachineList(String userId, String tenantId) { + + List machineList = new ArrayList<>(); + List scopeList = ruleScopeUtil.selectUserEffectList(userId, ScopeBizType.ATTENDANCE_MACHINE, tenantId); + if (null == scopeList || scopeList.isEmpty()) { + return machineList; + } + List ruleIds = scopeList.stream().map(AttendanceRuleScope::getRuleId).collect(Collectors.toList()); + if (ruleIds.isEmpty()) { + return machineList; + } + return attenceMachineService.list(new LambdaQueryWrapper() + .in(AttendanceMachineManage::getId, ruleIds) + .eq(AttendanceMachineManage::getDeleteMark, ConstantUtil.NUM_FALSE)); + } + + @Override + public Map> getTodayRuleListByUser(Date today, String userId) { + + // 查询今日可以打卡的出勤规则 + List ruleList = attendanceDailyRuleMapper.getRuleClockTimeContainsToday(today, userId); + ruleList = clearErrorRule(ruleList, userId, today, null); + if (ruleList.isEmpty()) { + return new HashMap<>(); + } + UserInfo user = UserProvider.getUser(); + Map> returnMap = new HashMap<>(); + // 用户 -> 出勤规则列表 + ConcurrentMap> map = ruleList.stream().collect(Collectors.groupingByConcurrent(FtbAttendanceDailyRule::getUserId)); + // 查询加班规则 + Map overtimeMap = overtimeRuleService.getEffectDetailByUserList(new ArrayList<>(map.keySet()), today); + map.forEach((k, v) -> { + v.sort(Comparator.comparingInt(FtbAttendanceDailyRule::getRn)); + user.setUserId(k); + List list = ruleFilterChain.execute(today, v, overtimeMap, user); + returnMap.put(k, list); + }); + return returnMap; + } + + @Override + public Map> getRuleListByUserDay(Map> groupedMap, Map> userGroupMap) { + + if (groupedMap.isEmpty()) { + return Map.of(); + } + // 查询加班规则 1. 合并所有规则列表 + List allRules = groupedMap.values().stream() + .flatMap(List::stream) + // 2. 过滤掉 targetDate != queryDate 的记录 + .filter(rule -> Objects.equals(rule.getTargetDate(), rule.getQueryDate())) + .collect(Collectors.toList()); + // 3. 按 day 分组,并将 userId 去重收集 + Map> dayUserMap = allRules.stream() + .collect(Collectors.groupingBy( + FtbAttendanceDailyRule::getDay, // 分组依据 + Collectors.mapping(FtbAttendanceDailyRule::getUserId, + Collectors.collectingAndThen(Collectors.toSet(), ArrayList::new)) // 去重后转List + )); + // 开始查询 + Map> overtimeDayMap = new HashMap<>(); + dayUserMap.forEach((day, userIds) -> { + Map overtimeMap = overtimeRuleService.getEffectDetailByUserList(userIds, day); + overtimeDayMap.put(DateDetail.getDate2Str(day, DateDetail.DF), overtimeMap); + }); + UserInfo currUser = UserProvider.getUser(); + Map> returnMap = new HashMap<>(); + groupedMap.forEach((k, v) -> { + String[] split = k.split("_"); + String userId = split[0]; + String targetDate = split[1]; + v.sort(Comparator.comparingInt(FtbAttendanceDailyRule::getRn)); + UserInfo user = new UserInfo(); + BeanUtils.copyProperties(currUser, user); + user.setUserId(userId); + List groupIds = userGroupMap.getOrDefault(k, List.of()); + v = clearErrorRule(v, userId, DateDetail.getStr2Date(targetDate), groupIds); + List list = ruleFilterChain.execute(DateDetail.getStr2Date(targetDate), v, overtimeDayMap.get(targetDate), user); + DateDetail dateDetail = new DateDetail(); + dateDetail.changeDay(DateDetail.getStr2Date(targetDate)); + dateDetail.getTomorrow(); + String tomorrow = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + List crossDayList = v.stream().filter(r -> { + if (r.getInPoint() == null || r.getOutPoint() == null) { + return false; + } + String inDate = DateDetail.getDate2Str(r.getInPoint(), DateDetail.DF); + String outDate = DateDetail.getDate2Str(r.getOutPoint(), DateDetail.DF); + return tomorrow.equals(inDate) && inDate.equals(outDate); + }).collect(Collectors.toList()); + if (!crossDayList.isEmpty()) { + List otherList = ruleFilterChain.execute(dateDetail.getCurrentDate(), crossDayList, overtimeDayMap.get(targetDate), user); + if (!otherList.isEmpty()) { + Set ruleIdSet = list.stream() + .map(FtbAttendanceDailyRule::getId) + .collect(Collectors.toSet()); + otherList.removeIf(o -> ruleIdSet.contains(o.getId())); + list.addAll(otherList); + } + } + returnMap.put(k, list); + }); + return returnMap; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/CommonSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/CommonSettingServiceImpl.java new file mode 100644 index 0000000..cf5d747 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/CommonSettingServiceImpl.java @@ -0,0 +1,17 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.CommonSettingMapper; +import jnpf.attendance.service.CommonSettingService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceCommonSetting; +import org.springframework.stereotype.Service; + +/** + * 公共配置服务实现 + * + * @author yanwenfu + * @create 2025-09-19 + */ +@Service +public class CommonSettingServiceImpl extends SuperServiceImpl implements CommonSettingService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/DailyRuleChangeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/DailyRuleChangeServiceImpl.java new file mode 100644 index 0000000..37b413c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/DailyRuleChangeServiceImpl.java @@ -0,0 +1,65 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.mapper.DailyRuleChangeMapper; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.DailyRuleChangeService; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.DailyRuleChange; +import jnpf.model.attendance.vo.UserDayVo; +import jnpf.util.DateDetail; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 出勤规则变更服务实现 + * + * @author yanwenfu + * @create 2026-05-20 + */ +@Service +public class DailyRuleChangeServiceImpl extends SuperServiceImpl implements DailyRuleChangeService { + + @Resource + private AttendanceClockInService attendanceClockInService; + + @Override + public boolean dailyRuleChangeExecute() { + // 查询是否有出勤变更未执行的记录 + List list = this.list(); + if (!list.isEmpty()) { + UserInfo userInfo = UserProvider.getUser(); + attendanceClockInService.changeAttendanceRuleBatch(change(list), userInfo); + } + return true; + } + + public static List change(List list) { + + if (CollUtil.isEmpty(list)) { + return List.of(); + } + return list.stream().map(item -> { + UserDayVo vo = new UserDayVo(); + vo.setTenantId(item.getTenantId()); + vo.setGroupId(item.getGroupId()); + vo.setUserId(item.getUserId()); + vo.setDay(item.getDay()); + if (item.getDay() != null) { + vo.setDayStr(DateDetail.getDate2Str(item.getDay(), DateDetail.DF)); + } + return vo; + }).collect(Collectors.toList()); + } + + @Override + public void saveRecordBatch(List list) { + + baseMapper.saveRecordBatch(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/EnableBalanceServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/EnableBalanceServiceImpl.java new file mode 100644 index 0000000..9ae3652 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/EnableBalanceServiceImpl.java @@ -0,0 +1,35 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.mapper.EnableBalanceMapper; +import jnpf.attendance.service.EnableBalanceService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceEnableBalance; +import jnpf.model.attendance.vo.attendance.OvertimeSalaryHoursJsonVo; +import jnpf.model.attendance.vo.attendance.OvertimeSalaryHoursVo; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 加班余额记录[不存休,仅记录]服务实现 + * + * @author yanwenfu + * @create 2025-10-03 + */ +@Service +public class EnableBalanceServiceImpl extends SuperServiceImpl implements EnableBalanceService { + @Override + public Map getOvertimeSalary(String startDate, String endDate, List userIds) { + List list = this.baseMapper.getOvertimeSalary(startDate, endDate, userIds); + return CollUtil.isNotEmpty(list) ? list.stream().collect(Collectors.toMap(OvertimeSalaryHoursVo::getUserId, v -> v)) : new HashMap<>(); + } + + @Override + public List getOvertimeSalaryJson(String startDate, String endDate, List userIds) { + return this.baseMapper.getOvertimeSalaryJson(startDate, endDate, userIds); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/InitializationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/InitializationServiceImpl.java new file mode 100644 index 0000000..97feaef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/InitializationServiceImpl.java @@ -0,0 +1,122 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.InitializationMapper; +import jnpf.attendance.mapper.StorageRestMapper; +import jnpf.attendance.service.InitializationService; +import jnpf.entity.attendance.AttendanceStorageRest; +import jnpf.model.attendance.vo.AttendanceBalanceRecordVo; +import jnpf.model.attendance.vo.attendance.BalanceUseRecordVo; +import jnpf.util.DateDetail; +import jnpf.util.FtbUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Author huanglinpan + * @Date 2024/7/1 9:16 + * @Version 1.0 (版本号) + */ +@Service +public class InitializationServiceImpl implements InitializationService { + + @Resource + private InitializationMapper initializationMapper; + + @Resource + private StorageRestMapper storageRestMapper; + + + @Override + @Deprecated + public Integer storageRest() { + DateDetail dateDetail = new DateDetail(); + SimpleDateFormat Y_M = new SimpleDateFormat("yyyy-MM"); + SimpleDateFormat Y_M_D = new SimpleDateFormat("yyyy-MM-dd"); + // 该代码段用于获取当前时间的上一个月的年-月格式字符串 + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.MONTH, -1); + // 获取上个月的年月 + String lastMonthDate = Y_M.format(calendar.getTime()); + // 初始化表 + initializationMapper.truncatStorageRest(); + // 查出用户拥有的劵余额 + List allBalance = initializationMapper.getBalanceRecord(); + // 查出用户拥有的劵使用记录 + List allBalanceUseRecord = initializationMapper.getAllBalanceUseRecord(); + // 过滤出所有的用户 + List userIds = allBalance.stream().map(AttendanceBalanceRecordVo::getUserId).distinct().collect(Collectors.toList()); + // 通过用户进行分组 + Map> allBalanceMap = allBalance.stream().collect(Collectors.groupingBy(AttendanceBalanceRecordVo::getUserId)); + Map> allBalanceUseMap = allBalanceUseRecord.stream().collect(Collectors.groupingBy(BalanceUseRecordVo::getBalanceId)); + List list = new ArrayList<>(); + // 处理用户数据 + allBalanceMap.forEach((key, value) -> { + // 获取数据集合中最小的创建时间日期 + AttendanceBalanceRecordVo min = Collections.min(value, Comparator.comparing(AttendanceBalanceRecordVo::getCreatorTimeStr)); + String minYM = Y_M.format(min.getCreatortime()); + List monthsBetweenStr = DateDetail.getMonthsBetweenStr(minYM, lastMonthDate); + monthsBetweenStr.forEach(month -> { + // 月底最后一天 + String lastDayOfMonthStr = dateDetail.getLastDayOfMonthStr(month); + Date lastDayOfMonth = null; + try { + lastDayOfMonth = Y_M_D.parse(lastDayOfMonthStr); + } catch (ParseException e) { + throw new RuntimeException(e); + } + Date finalLastDayOfMonth = lastDayOfMonth; + // 过滤出创建时间小于月末并且(过期时间为空或者过期时间大于月末)的数据 + List recordVos = value.stream().filter(t -> t.getCreatortime().before(finalLastDayOfMonth) && (null == t.getExpireTime() || t.getExpireTime().after(finalLastDayOfMonth))).collect(Collectors.toList()); + // 遍历处理数据 计算余额 + BigDecimal num = new BigDecimal("0.00"); + if (!recordVos.isEmpty()) { + // 找出有使用记录的劵记录 + for (AttendanceBalanceRecordVo recordVo : recordVos) { + List balanceUseRecordVos = allBalanceUseMap.get(recordVo.getId()); + // 查看有无使用记录 + if (null != balanceUseRecordVos){ + // 有使用记录,找到月末前的记录 + List useRecordVos = balanceUseRecordVos.stream().filter(t1 -> t1.getCreatorTime().before(finalLastDayOfMonth)).collect(Collectors.toList()); + if (!useRecordVos.isEmpty()){ + // 用户时间前使用额度 + BigDecimal balanceNum = new BigDecimal("0.00"); + for (BalanceUseRecordVo useRecordVo : useRecordVos) { + balanceNum = balanceNum.add(useRecordVo.getQuota()); + } + // 未使用额度 劵总额 - 已使用额度 + BigDecimal subtract = recordVo.getTotal().subtract(balanceNum); + num = num.add(subtract); + }else { + // 没有使用记录 + num = num.add(recordVo.getTotal()); + } + }else { + // 没有记录,将劵余额相加 + num = num.add(recordVo.getTotal()); + } + } + } + AttendanceStorageRest attendanceStorageRest = new AttendanceStorageRest(); + attendanceStorageRest.setId(FtbUtil.getId()); + attendanceStorageRest.setUserId(key); + attendanceStorageRest.setYearMonth(month); + attendanceStorageRest.setNum(num); + list.add(attendanceStorageRest); + }); + }); + if (!list.isEmpty()) { + // 批量保存存休信息 + storageRestMapper.saveBatch(list); + return list.size(); + } + return 0; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/IsPerfMachineServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/IsPerfMachineServiceImpl.java new file mode 100644 index 0000000..fba0590 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/IsPerfMachineServiceImpl.java @@ -0,0 +1,67 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.json.JSONObject; +import jnpf.SocketApi; +import jnpf.attendance.service.IsPerfMachineService; +import jnpf.enums.attendance.MachineEnum; +import jnpf.util.ConstantUtil; +import jnpf.util.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/** + * KIPS服务实现 + * + * @author yanwenfu + * @create 2024-04-01 + */ +@Slf4j +@Service +public class IsPerfMachineServiceImpl implements IsPerfMachineService { + + @Autowired + private RedisUtil redisUtil; + + @Autowired + private SocketApi socketApi; + + @Override + public void welcome() { + + try { + // new NettyServer(nettyPort).bind(); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + @Override + public void updateApp(Map params) { + + String apkUrl = params.get("apkUrl").toString(); + String md5 = params.get("md5").toString(); + JSONObject json = new JSONObject(); + json.set("method", "updateApp"); + json.set("timestamp", System.currentTimeMillis()); + JSONObject body = new JSONObject(); + body.set("apkUrl", apkUrl); + body.set("md5", md5); + json.set("body", body); + List macList = redisUtil.getHashKeys(String.format(ConstantUtil.ONLINE_MAC, MachineEnum.KAI_JIA_YI.getValue())); + if (!macList.isEmpty()) { + macList.forEach(mac -> { + socketApi.sendMsg2Client(MachineEnum.KAI_JIA_YI.getValue(), mac, json.toString()); + }); + } + } + + @Override + public List getAllOnlineClient() { + + return redisUtil.getHashKeys(String.format(ConstantUtil.ONLINE_MAC, MachineEnum.KAI_JIA_YI.getValue())); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKaiJiaYiStrategyImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKaiJiaYiStrategyImpl.java new file mode 100644 index 0000000..48f7d85 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKaiJiaYiStrategyImpl.java @@ -0,0 +1,129 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import jnpf.SocketApi; +import jnpf.attendance.annotation.Machine; +import jnpf.attendance.service.AttendanceUserFaceService; +import jnpf.attendance.service.MachineStrategy; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.model.attendance.vo.UserFaceVo; +import jnpf.permission.V2UserApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 开架易 - 考勤策略模式 + * + * @author yanwenfu + * @create 2024-04-03 + */ +@Slf4j +@Component(value = "machineKaiJiaYiStrategy") +public class MachineKaiJiaYiStrategyImpl implements MachineStrategy { + + @Autowired + private AttendanceUserFaceService attendanceUserFaceService; + + @Resource + private V2UserApi v2UserApi; + + @Autowired + private SocketApi socketApi; + + @Override + public MachineEnum getMachine() { + return MachineEnum.KAI_JIA_YI; + } + + @Machine(dealAction = ActionEnum.XIA_FA, factory = MachineEnum.KAI_JIA_YI) + @Override + public void addUserToMachine(UserInfo userInfo, PartUserInfoVo user, String sn) { + + // 查询用户人脸数据 + UserFaceVo userFace = attendanceUserFaceService.getUserFace(user.getUserId()); + if (null == userFace) { + log.error("用户暂未录入人脸: {}", user.getUserId()); + return; + } + JSONObject json = new JSONObject(); + json.set("method", "person/create"); + json.set("timestamp", System.currentTimeMillis()); + JSONArray array = new JSONArray(); + JSONObject userJson = new JSONObject(); + userJson.set("age", 0); + userJson.set("name", user.getRealName()); + userJson.set("imgUrl", userFace.getFaceData()); + Integer sex = user.getGender(); + if (null != sex) { + if (2 == sex) { + sex = 0; + } + if (3 == sex) { + sex = 1; + } + } else { + sex = 1; + } + userJson.set("sex", sex); + userJson.set("type", 3); + userJson.set("vipId", user.getUserNo()); + JSONObject extraJson = new JSONObject(); + extraJson.set("tenantId", userInfo.getTenantId()); + extraJson.set("userId", user.getUserId()); + userJson.set("extra", extraJson); + array.add(userJson); + json.set("body", array); + socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); + } + + @Machine(dealAction = ActionEnum.SHAN_CHU, factory = MachineEnum.KAI_JIA_YI) + @Override + public void deleteUserList(UserInfo userInfo, List userIds, String sn) { + + JSONObject json = new JSONObject(); + json.set("method", "person/delete"); + json.set("timestamp", System.currentTimeMillis()); + JSONObject jsonBody = new JSONObject(); + StringBuilder sb = new StringBuilder(); + // 根据userIds查询userNo + + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(userIds, null); + if (200 == allUserInfoBatch.getCode() && !allUserInfoBatch.getData().isEmpty()) { + allUserInfoBatch.getData().forEach(n -> sb.append(n.getUserNo()).append(",")); + jsonBody.set("deleteId", sb.substring(0, sb.length() - 1)); + json.set("body", jsonBody); + socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); + } + // List noList = userApi.getUserNoByIds(userIds); +// if (null != noList && !noList.isEmpty()) { +// noList.forEach(n -> sb.append(n.getUserNo()).append(",")); +// jsonBody.set("deleteId", sb.substring(0, sb.length() - 1)); +// json.set("body", jsonBody); +// socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); +// } + } + + @Override + public void syncMachineMember(String sn) { + + JSONObject json = new JSONObject(); + json.set("method", "person/list"); + json.set("timestamp", System.currentTimeMillis()); + JSONObject body = new JSONObject(); + body.set("current", 1); + body.set("size", 999); + body.set("needImage", false); + json.set("body", body); + socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKeMiStrategyImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKeMiStrategyImpl.java new file mode 100644 index 0000000..8cdbf7a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineKeMiStrategyImpl.java @@ -0,0 +1,92 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.FeignConfig; +import jnpf.SocketApi; +import jnpf.attendance.annotation.Machine; +import jnpf.attendance.mapper.AttendanceUserFaceMapper; +import jnpf.attendance.service.MachineStrategy; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.permission.UserApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.ConstantUtil; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 科密考勤策略模式 + * + * @author yanwenfu + * @create 2025-08-21 + */ +@Slf4j +@Component(value = "machineKeMiStrategy") +public class MachineKeMiStrategyImpl implements MachineStrategy { + + @Autowired + private SocketApi socketApi; + @Autowired + private UserApi userApi; + @Resource + private AttendanceUserFaceMapper attendanceUserFaceMapper; + + @Override + public MachineEnum getMachine() { + return MachineEnum.KE_MI; + } + + @Machine(dealAction = ActionEnum.XIA_FA, factory = MachineEnum.KE_MI) + @Override + public void addUserToMachine(UserInfo userInfo, PartUserInfoVo user, String sn) { + + // 查询用户照片 + LambdaQueryWrapper userFaceQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, user.getUserId()) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(userFaceQueryWrapper); + log.error("用户照片信息:{}", userFace); + JSONObject j1 = new JSONObject(); + j1.set("cmd_code", "SET_USER_INFO"); + j1.set("uId", user.getUserId()); + JSONArray array = new JSONArray(); + JSONObject j2 = new JSONObject(); + j2.set("userId", user.getUserId()); + j2.set("name", user.getRealName()); + if (null != userFace) { + j2.set("photo", userFace.getFaceData()); + j2.set("photoEnroll", 1); + } + array.set(j2); + j1.set("users", array); + log.error("FeignHolder: {}", FeignHolder.get()); + socketApi.sendMsg2ClientNoToken(getMachine().getValue(), sn, j1.toString(), userInfo.getTenantId()); + } + + @Machine(dealAction = ActionEnum.SHAN_CHU, factory = MachineEnum.KE_MI) + @Override + public void deleteUserList(UserInfo userInfo, List userIds, String sn) { + + if (!userIds.isEmpty()) { + JSONObject json = new JSONObject(); + json.set("cmd_code", "DELETE_USER"); + json.set("usersCount", userIds.size()); + json.set("usersId", userIds); + socketApi.sendMsg2ClientNoToken(getMachine().getValue(), sn, json.toString(), userInfo.getTenantId()); + } + } + + @Override + public void syncMachineMember(String sn) { + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineMaoTongStrategyImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineMaoTongStrategyImpl.java new file mode 100644 index 0000000..b1809d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineMaoTongStrategyImpl.java @@ -0,0 +1,166 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.lang.UUID; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.SocketApi; +import jnpf.attendance.annotation.Machine; +import jnpf.attendance.mapper.AttendanceUserFaceMapper; +import jnpf.attendance.mapper.AttendanceUserFingerprintMapper; +import jnpf.attendance.service.MachineStrategy; +import jnpf.base.UserInfo; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.entity.attendance.AttendanceUserFingerprint; +import jnpf.enums.attendance.ActionEnum; +import jnpf.enums.attendance.MachineEnum; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.ConstantUtil; +import jnpf.util.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.io.InputStream; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * 猫瞳 - 考勤策略模式 + * + * @author yanwenfu + * @create 2024-04-03 + */ +@Slf4j +@Component(value = "machineMaoTongStrategy") +public class MachineMaoTongStrategyImpl implements MachineStrategy { + + @Autowired + private SocketApi socketApi; + + @Resource + private AttendanceUserFaceMapper attendanceUserFaceMapper; + + @Resource + private AttendanceUserFingerprintMapper attendanceUserFingerprintMapper; + + @Resource + private RedisUtil redisUtil; + + @Override + public MachineEnum getMachine() { + return MachineEnum.MAO_TONG; + } + + @Machine(dealAction = ActionEnum.XIA_FA, factory = MachineEnum.MAO_TONG) + @Override + public void addUserToMachine(UserInfo userInfo, PartUserInfoVo user, String sn) { + + // 查询用户照片 + LambdaQueryWrapper userFaceQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getUserId, user.getUserId()) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(userFaceQueryWrapper); + // 查询用户指纹 + LambdaQueryWrapper fingerprintQueryWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFingerprint::getUserId, user.getUserId()) + .eq(AttendanceUserFingerprint::getDeleteMark, ConstantUtil.NUM_FALSE); + List fingerprintList = attendanceUserFingerprintMapper.selectList(fingerprintQueryWrapper); + try { + String uuid = UUID.randomUUID().toString().replace("-", ""); + JSONObject json = new JSONObject(); + json.set("cmd", "to_device"); + json.set("from", "server"); + json.set("to", sn); + json.set("extra", uuid); + JSONObject json1 = new JSONObject(); + json1.set("cmd", "addUser"); + json1.set("user_id", userInfo.getTenantId() + "@" + user.getUserId()); + json1.set("name", user.getRealName()); + if (null != userFace) { + // String base64 = changeUrlToBase64(userFace.getFaceData()); + json1.set("face_template", userFace.getFaceData() + "?imageMogr2/format/jpeg"); + // json1.set("vlface_template", userFace.getFaceData()); + } + if (!fingerprintList.isEmpty()) { + JSONArray jsonArray = new JSONArray(); + fingerprintList.forEach(v -> { + JSONObject jo = new JSONObject(); + jo.set("id", v.getDataId()); + jo.set("data", v.getData()); + jo.set("name", v.getDataName()); + jsonArray.add(jo); + }); + json1.set("fp", jsonArray); + } + json1.set("id_valid", ""); + json.set("data", json1); + String key = String.format(ConstantUtil.MACHINE_MESSAGE, sn) + user.getUserId(); + JSONObject machineJson; + if (redisUtil.exists(key)) { + String str = redisUtil.getString(key).toString(); + machineJson = JSONUtil.parseObj(str); + } else { + machineJson = new JSONObject(); + } + machineJson.set(sn, 0); + redisUtil.insert(key, machineJson.toString(), 60L); + redisUtil.insert(ConstantUtil.SEND_USER_CHECK + sn + ":" + uuid, json.toString(), 45L); + socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); + } catch (Exception e) { + log.error("下发成员失败: {}", e.getMessage()); + } + } + + @Machine(dealAction = ActionEnum.SHAN_CHU, factory = MachineEnum.MAO_TONG) + @Override + public void deleteUserList(UserInfo userInfo, List userIds, String sn) { + + try { + List list = getUserIds(userInfo, userIds); + JSONObject json = new JSONObject(); + json.set("cmd", "to_device"); + json.set("from", "server"); + json.set("to", sn); + json.set("extra", list); + JSONObject json1 = new JSONObject(); + json1.set("cmd", "delMultiUser"); + json1.set("user_ids", list); + json.set("data", json1); + socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + private List getUserIds(UserInfo userInfo, List userIds) { + + List list = new ArrayList<>(); + userIds.forEach(userId -> list.add(userInfo.getTenantId() + "@" + userId)); + return list; + } + + @Override + public void syncMachineMember(String sn) { + + try { + JSONObject json = new JSONObject(); + json.set("cmd", "to_device"); + json.set("from", "server"); + json.set("to", sn); + JSONObject json1 = new JSONObject(); + json1.set("cmd", "getUserInfo"); + json1.set("value", 1); + json.set("data", json1); + socketApi.sendMsg2Client(getMachine().getValue(), sn, json.toString()); + } catch (Exception e) { + log.error(e.getMessage()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineYuQueStrategyImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineYuQueStrategyImpl.java new file mode 100644 index 0000000..cab7189 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/MachineYuQueStrategyImpl.java @@ -0,0 +1,91 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import jnpf.attendance.service.MachineStrategy; +import jnpf.base.UserInfo; +import jnpf.enums.attendance.MachineEnum; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.attendance.MqttSender; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 语雀 - 考勤策略模式 + * + * @author yanwenfu + * @create 2024-04-09 + */ +@Slf4j +@Component(value = "machineYuQueStrategy") +public class MachineYuQueStrategyImpl implements MachineStrategy { + + @Autowired + private MqttSender mqttSender; + + @Value("${spring.mqtt.topics}") + private String topic; + + @Value("${spring.mqtt.servicePath}") + private String servicePath; + + @Override + public MachineEnum getMachine() { + return MachineEnum.YU_QUE; + } + + @Override + public void addUserToMachine(UserInfo userInfo, PartUserInfoVo user, String sn) { + + // sn为设备mac地址 + JSONObject json = new JSONObject(); + json.set("method", "sync_person"); + JSONObject dataJson = new JSONObject(); + dataJson.set("path", servicePath + "/device/sync_person"); + JSONObject pathParams = new JSONObject(); + pathParams.set("dev_sno", sn); + pathParams.set("limit", 1); + pathParams.set("offset", 0); + pathParams.set("total", 1); + JSONArray array = new JSONArray(); + array.add(userInfo.getTenantId() + "@" + user.getUserId()); + pathParams.set("person_list", array); + pathParams.set("person_type", "4"); + dataJson.set("path_params", pathParams); + json.set("data", dataJson); + json.set("notify", servicePath + "/device/notify"); + JSONObject params = new JSONObject(); + params.set("tenantId", userInfo.getTenantId()); + json.set("params", params); + mqttSender.send(sn, json.toString()); + } + + @Override + public void deleteUserList(UserInfo userInfo, List userIds, String sn) { + + if (userIds.isEmpty()) { + log.error("userIds为空, 无可删除用户..."); + return; + } + JSONObject json = new JSONObject(); + json.set("method", "delete_person"); + json.set("notify", servicePath + "/device/notify"); + json.set("params", ""); + JSONObject dataJson = new JSONObject(); + JSONArray array = new JSONArray(); + userIds.forEach(userId -> array.add(userInfo.getTenantId() + "@" + userId)); + dataJson.set("person_list", array); + dataJson.set("person_type", "4"); + json.set("data", dataJson); + mqttSender.send(sn, json.toString()); + } + + @Override + public void syncMachineMember(String sn) { + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleDetailServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleDetailServiceImpl.java new file mode 100644 index 0000000..a03853d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleDetailServiceImpl.java @@ -0,0 +1,17 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.OvertimeRuleDetailMapper; +import jnpf.attendance.service.OvertimeRuleDetailService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceOvertimeRuleDetail; +import org.springframework.stereotype.Service; + +/** + * 加班规则明细表服务实现 + * + * @author yanwenfu + * @create 2025-09-18 + */ +@Service +public class OvertimeRuleDetailServiceImpl extends SuperServiceImpl implements OvertimeRuleDetailService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleServiceImpl.java new file mode 100644 index 0000000..7d5a058 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/OvertimeRuleServiceImpl.java @@ -0,0 +1,339 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.mapper.OvertimeRuleMapper; +import jnpf.attendance.service.AttendanceFestivalRulesService; +import jnpf.attendance.service.OvertimeRuleDetailService; +import jnpf.attendance.service.OvertimeRuleService; +import jnpf.attendance.service.RuleScopeService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceOvertimeRule; +import jnpf.entity.attendance.AttendanceOvertimeRuleDetail; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.OvertimeType; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.FestivalDto; +import jnpf.model.attendance.dto.OvertimeRuleDetailDto; +import jnpf.model.attendance.dto.OvertimeRuleDto; +import jnpf.model.attendance.dto.OvertimeRuleQueryDto; +import jnpf.model.attendance.vo.attendance.OvertimeRuleDetailVo; +import jnpf.model.attendance.vo.attendance.OvertimeRulePageVo; +import jnpf.model.attendance.vo.attendance.OvertimeRuleVo; +import jnpf.util.*; +import jnpf.util.attendance.RuleScopeUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 加班规则服务实现 + * + * @author yanwenfu + * @create 2025-09-18 + */ +@Service +public class OvertimeRuleServiceImpl extends SuperServiceImpl implements OvertimeRuleService { + + @Resource + private OvertimeRuleMapper overtimeRuleMapper; + @Resource + private OvertimeRuleDetailService overtimeRuleDetailService; + @Resource + private RuleScopeService ruleScopeService; + @Resource + private RuleScopeUtil ruleScopeUtil; + @Resource + private AttendanceFestivalRulesService festivalRulesService; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + + @Transactional(rollbackFor = Exception.class) + @Override + public void addOvertimeRule(OvertimeRuleDto overtimeRuleDto) throws HandleException { + + // 生成主表信息 + AttendanceOvertimeRule overtimeRule = getAttendanceOvertimeRule(overtimeRuleDto); + // 生成数据范围信息(全部则无value) + List ruleScopeList = ruleScopeUtil.getRuleScopeList(overtimeRule.getId(), overtimeRuleDto.getRuleScope(), + MutablePair.of(overtimeRuleDto.getOrganizeList(), overtimeRuleDto.getUserIdList()), ScopeBizType.OVERTIME_RULE); + // 生成子表信息 + List detailList = getOvertimeDetailList(overtimeRule.getId(), overtimeRuleDto.getList()); + this.save(overtimeRule); + if (!ruleScopeList.isEmpty()) { + ruleScopeUtil.saveBatch(ruleScopeList); + } + if (!detailList.isEmpty()) { + overtimeRuleDetailService.saveBatch(detailList); + } + } + + @NotNull + private static AttendanceOvertimeRule getAttendanceOvertimeRule(OvertimeRuleDto overtimeRuleDto) { + + AttendanceOvertimeRule overtimeRule = new AttendanceOvertimeRule(); + overtimeRule.setId(FtbUtil.getId()); + overtimeRule.setRuleName(overtimeRuleDto.getRuleName()); + overtimeRule.setRuleScope(overtimeRuleDto.getRuleScope()); + overtimeRule.setEnabled(ConstantUtil.NUM_FALSE); + overtimeRule.setCreatorUserId(UserProvider.getLoginUserId()); + overtimeRule.setLastModifyUserId(UserProvider.getLoginUserId()); + overtimeRule.setDeleteMark(ConstantUtil.NUM_FALSE); + return overtimeRule; + } + + private List getOvertimeDetailList(String ruleId, List list) { + + List returnList = new ArrayList<>(); + list.forEach(v -> { + AttendanceOvertimeRuleDetail detail = JsonUtil.getJsonToBean(v, AttendanceOvertimeRuleDetail.class); + if (StringUtils.isEmpty(detail.getId())) detail.setId(FtbUtil.getId()); + if (StringUtils.isEmpty(detail.getRuleId())) detail.setRuleId(ruleId); + detail.setLastModifyUserId(UserProvider.getLoginUserId()); + returnList.add(detail); + }); + return returnList; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateOvertimeRule(String id, OvertimeRuleDto overtimeRuleDto) throws HandleException { + + // 查询数据库主表信息 + AttendanceOvertimeRule overtimeRule = this.getById(id); + if (null == overtimeRule) { + throw new HandleException("未查询到加班规则"); + } + Integer oldScope = overtimeRule.getRuleScope(); + Integer newScope = overtimeRuleDto.getRuleScope(); + // 更新主表字段 + overtimeRule.setRuleName(overtimeRuleDto.getRuleName()); + overtimeRule.setRuleScope(overtimeRuleDto.getRuleScope()); + overtimeRule.setLastModifyTime(DateUtil.getNowDate()); + overtimeRule.setLastModifyUserId(UserProvider.getLoginUserId()); + this.updateById(overtimeRule); + // 更新适配范围 + ruleScopeUtil.updateRuleScopeList(id, oldScope, newScope, MutablePair.of(overtimeRuleDto.getOrganizeList(), overtimeRuleDto.getUserIdList()), ScopeBizType.OVERTIME_RULE); + // 更新子表字段 + List detailList = getOvertimeDetailList(overtimeRule.getId(), overtimeRuleDto.getList()); + if (!detailList.isEmpty()) { + overtimeRuleDetailService.updateBatchById(detailList); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteOvertimeRule(String id) { + + // 主表标识删除 + this.update(new LambdaUpdateWrapper() + .set(AttendanceOvertimeRule::getDeleteMark, ConstantUtil.NUM_TRUE) + .set(AttendanceOvertimeRule::getDeleteTime, DateUtil.getNowDate()) + .set(AttendanceOvertimeRule::getDeleteUserId, UserProvider.getLoginUserId()) + .eq(AttendanceOvertimeRule::getId, id)); + // 适配范围直接删除 + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, id)); + } + + @Override + public void updateEnableStatus(String id) throws HandleException { + + AttendanceOvertimeRule overtimeRule = this.getById(id); + if (null == overtimeRule) { + throw new HandleException("未找到加班规则"); + } + this.update(new LambdaUpdateWrapper() + .set(AttendanceOvertimeRule::getEnabled, overtimeRule.getEnabled().equals(ConstantUtil.NUM_TRUE) ? ConstantUtil.NUM_FALSE : ConstantUtil.NUM_TRUE) + .set(AttendanceOvertimeRule::getLastModifyTime, DateUtil.getNowDate()) + .set(AttendanceOvertimeRule::getLastModifyUserId, UserProvider.getLoginUserId()) + .eq(AttendanceOvertimeRule::getId, id)); + } + + @Override + public OvertimeRuleVo getDetail(String id) { + + AttendanceOvertimeRule overtimeRule = this.getById(id); + return setDetailInfo(overtimeRule); + } + + private OvertimeRuleVo setDetailInfo(AttendanceOvertimeRule overtimeRule) { + // 查主表 + OvertimeRuleVo vo = JsonUtil.getJsonToBean(overtimeRule, OvertimeRuleVo.class); + // 查子表 + List detailList = overtimeRuleDetailService.list(new LambdaQueryWrapper() + .eq(AttendanceOvertimeRuleDetail::getRuleId, vo.getId())); + List voDetailList = JsonUtil.getJsonToList(detailList, OvertimeRuleDetailVo.class); + vo.getList().addAll(voDetailList); + // 查适配范围 + MutablePair, List> data = ruleScopeUtil.selectScopeList(vo.getId(), ScopeBizType.OVERTIME_RULE); + if (null != data) { + if (data.getLeft().isEmpty() && data.getRight().isEmpty()) { + // 无适配范围 + vo.setRuleScope(-1); + } else { + vo.getOrganizeList().addAll(data.getLeft()); + vo.getUserIdList().addAll(data.getRight()); + } + } + return vo; + } + + @Override + public OvertimeRuleVo getActiveDetail(String ruleId) { + + AttendanceOvertimeRule overtimeRule = this.getById(ruleId); + if (overtimeRule.getEnabled().equals(ConstantUtil.NUM_FALSE)) { + return null; + } + OvertimeRuleVo ruleVo = setDetailInfo(overtimeRule); + List collect = ruleVo.getList().stream().filter(v -> v.getEnabled().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList()); + ruleVo.getList().clear(); + ruleVo.setList(collect); + return ruleVo; + } + + @Override + public OvertimeRuleVo getEffectDetail(String userId, Integer attendanceType, Date date) { + + // 查询用户加班配置 + OvertimeRuleVo overtimeRule = null; + List scopeList = ruleScopeUtil.selectUserEffectList(userId, ScopeBizType.OVERTIME_RULE, UserProvider.getUser().getTenantId()); + if (!scopeList.isEmpty()) { + AttendanceRuleScope scope = scopeList.get(0); + overtimeRule = getActiveDetail(scope.getRuleId()); + } + if (null != overtimeRule) { + // 查询昨天是否节假日 + Map> map = festivalRulesService.checkFestivalRulesBatch(date, List.of(userId)); + Map checkMap = map.getOrDefault(userId, getDefaultMap(date)); + OvertimeRuleDetailVo vo = getOvertimeRuleDetail(overtimeRule, checkMap.get(DateDetail.getDate2Str(date, DateDetail.DF)), attendanceType); + overtimeRule.setFestival(checkMap); + overtimeRule.setEffectRuleDetail(vo); + } + return overtimeRule; + } + + @Override + public OvertimeRuleVo getGroupOvertimeRule(String groupId) { + + String orgId = attendanceGroupMapper.getOrgId(groupId); + List scopeList = ruleScopeUtil.selectUserEffectList(null, orgId, ScopeBizType.OVERTIME_RULE); + // 查询用户加班配置 + OvertimeRuleVo overtimeRule = null; + if (!scopeList.isEmpty()) { + AttendanceRuleScope scope = scopeList.get(0); + overtimeRule = getActiveDetail(scope.getRuleId()); + } + return overtimeRule; + } + + @Override + public Map getEffectDetailByUserList(List userIds, Date date) { + + // 查询用户加班配置 + List scopeList = ruleScopeUtil.selectUserEffectListBatch(userIds, ScopeBizType.OVERTIME_RULE, ConstantUtil.NUM_TRUE); + if (scopeList.isEmpty()) { + return new HashMap<>(); + } + ConcurrentMap> scopeMap = scopeList.stream().collect(Collectors.groupingByConcurrent(AttendanceRuleScope::getRuleId)); + LambdaQueryWrapper overtimeRuleQuery = new LambdaQueryWrapper() + .in(AttendanceOvertimeRule::getId, scopeMap.keySet()) + .eq(AttendanceOvertimeRule::getEnabled, ConstantUtil.NUM_TRUE) + .eq(AttendanceOvertimeRule::getDeleteMark, ConstantUtil.NUM_FALSE); + // 查询生效中的加班规则 + List overtimeList = this.list(overtimeRuleQuery); + if (overtimeList.isEmpty()) { + return new HashMap<>(); + } + // 查询生效中的加班规则详情 + Map ruleMap = overtimeList.stream().collect(Collectors.toMap(AttendanceOvertimeRule::getId, Function.identity())); + List detailList = overtimeRuleDetailService.list(new LambdaQueryWrapper() + .in(AttendanceOvertimeRuleDetail::getRuleId, ruleMap.keySet()) + .eq(AttendanceOvertimeRuleDetail::getEnabled, ConstantUtil.NUM_TRUE)); + ConcurrentMap> detailMap; + if (!detailList.isEmpty()) { + List voDetailList = JsonUtil.getJsonToList(detailList, OvertimeRuleDetailVo.class); + detailMap = voDetailList.stream().collect(Collectors.groupingByConcurrent(OvertimeRuleDetailVo::getRuleId)); + } else { + detailMap = new ConcurrentHashMap<>(); + } + // 判定节假日 + Map> checkMap = festivalRulesService.checkFestivalRulesBatch(date, userIds); + // 返回结果 + Map returnMap = new HashMap<>(); + ruleMap.forEach((k, v) -> { + List list = scopeMap.get(k); + List users = list.stream().map(AttendanceRuleScope::getUserId).collect(Collectors.toList()); + OvertimeRuleVo vo = JsonUtil.getJsonToBean(v, OvertimeRuleVo.class); + List dList = detailMap.getOrDefault(k, List.of()); + vo.setList(dList); + users.forEach(user -> { + Map check = checkMap.getOrDefault(user, getDefaultMap(date)); + vo.setFestival(check); + returnMap.put(user, vo); + }); + }); + return returnMap; + } + + private Map getDefaultMap(Date date) { + + Map map = new HashMap<>(); + DateDetail dateDetail = new DateDetail(date); + String endDate = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + dateDetail.getYesterday(); + String startDate = DateDetail.getDate2Str(dateDetail.getCurrentDate(), DateDetail.DF); + map.put(endDate, new FestivalDto(Boolean.FALSE)); + map.put(startDate, new FestivalDto(Boolean.FALSE)); + return map; + } + + @Override + public OvertimeRuleDetailVo getEffectWorkDetail(OvertimeRuleVo rule, Date date, Integer attendanceType) { + String day = DateDetail.getDate2Str(date, DateDetail.DF); + return getOvertimeRuleDetail(rule, rule.getFestival().get(day), attendanceType); + } + + private OvertimeRuleDetailVo getOvertimeRuleDetail(OvertimeRuleVo overtimeRule, FestivalDto festival, Integer attendanceType) { + + if (null == overtimeRule || null == attendanceType) { + return null; + } + if (festival.isFestival()) { + OvertimeRuleDetailVo overtimeRuleDetailVo = overtimeRule.getList().stream().filter(v -> v.getOvertimeType().equals(OvertimeType.HOLIDAY.getValue())).findFirst().orElse(null); + if (null != overtimeRuleDetailVo){ + overtimeRuleDetailVo.setFestivalStr(festival.getFestivalStr()); + } + return overtimeRuleDetailVo; + } + if (attendanceType.equals(AttendanceTypeEnum.ORDINARY.getCode())) { + return overtimeRule.getList().stream().filter(v -> v.getOvertimeType().equals(OvertimeType.WORK_DAY.getValue())).findFirst().orElse(null); + } + if (attendanceType.equals(AttendanceTypeEnum.REST.getCode())) { + return overtimeRule.getList().stream().filter(v -> v.getOvertimeType().equals(OvertimeType.REST_DAY.getValue())).findFirst().orElse(null); + } + return null; + } + + @Override + public PageInfo getPage(OvertimeRuleQueryDto queryDto) { + + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + return new PageInfo<>(overtimeRuleMapper.selectOvertimeRulePage(queryDto.getRuleName())); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/PublicHolidayRulesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/PublicHolidayRulesServiceImpl.java new file mode 100644 index 0000000..47f6ba8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/PublicHolidayRulesServiceImpl.java @@ -0,0 +1,310 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.mapper.PublicHolidayRulesMapper; +import jnpf.attendance.service.*; +import jnpf.entity.attendance.AttendanceBalanceRecordEntity; +import jnpf.entity.attendance.AttendancePublicHolidayRules; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.dto.AttendancePublicHolidayRulesDto; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayBalance; +import jnpf.model.attendance.vo.attendance.AttendancePublicHolidayRulesVo; +import jnpf.model.common.PageDto; +import jnpf.util.ConstantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import jnpf.util.attendance.ExpiresTimeUtil; +import jnpf.util.attendance.RuleScopeUtil; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author panpan + */ +@Service +public class PublicHolidayRulesServiceImpl implements PublicHolidayRulesService { + + @Resource + private PublicHolidayRulesMapper publicHolidayRulesMapper; + + @Resource + private RuleScopeUtil ruleScopeUtil; + + @Resource + private UserProvider userProvider; + + @Resource + private RuleScopeService ruleScopeService; + + @Resource + @Lazy + private AttendanceDailyRuleService dailyRuleService; + + @Resource + @Lazy + private AttendanceUserBalanceService attendanceUserBalanceService; + + @Resource + @Lazy + private AttendanceUserBalanceRecordService attendanceUserBalanceRecordService; + + @Resource + private ExpiresTimeUtil expiresTimeUtil; + + @Override + public PageInfo list(String iText, PageDto pageDto) { + PageHelper.startPage(pageDto.getCurrentPage(), pageDto.getPageSize()); + PageInfo attendancePublicHolidayRulesVoPageInfo = new PageInfo<>(publicHolidayRulesMapper.list(iText)); + // 当适配范围为指定组织或成员时查询适配范围 + if (null != attendancePublicHolidayRulesVoPageInfo.getList() && !attendancePublicHolidayRulesVoPageInfo.getList().isEmpty()) { + // 过滤出适配范围为指定组织或成员的规则id集合 + List scopeOfAdaptationIds = attendancePublicHolidayRulesVoPageInfo.getList().stream().filter(v -> v.getScopeOfAdaptation().equals(1)).map(AttendancePublicHolidayRulesVo::getId).collect(Collectors.toList()); + if (!scopeOfAdaptationIds.isEmpty()) { + // 拼接适配范围 + Map, List>> stringMutablePairMap = ruleScopeUtil.selectScopeListBatch(scopeOfAdaptationIds, ScopeBizType.PUBLIC_HOLIDAY_RULE); + // 遍历vo列表,根据规则id设置适配范围 + for (AttendancePublicHolidayRulesVo v : attendancePublicHolidayRulesVoPageInfo.getList()) { + MutablePair, List> pair = stringMutablePairMap.get(v.getId()); + if (null != pair) { + v.setOrganizeNum(pair.getLeft().size()); + v.setUserIdNum(pair.getRight().size()); + } + } + } + } + return attendancePublicHolidayRulesVoPageInfo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(AttendancePublicHolidayRulesDto publicHolidayRulesDto) throws Exception { + // 新增的范围为全部清空其他 + if (0 == publicHolidayRulesDto.getScopeOfAdaptation()) { + publicHolidayRulesMapper.updateScopeOfAdaptation(); + } + publicHolidayRulesDto.setId(FtbUtil.getId()); + publicHolidayRulesMapper.insert(publicHolidayRulesDto, userProvider.get().getId()); + // 当适配范围为指定组织或成员时绑定适配范围 + List ruleScopeList = ruleScopeUtil.getRuleScopeList(publicHolidayRulesDto.getId(), publicHolidayRulesDto.getScopeOfAdaptation(), MutablePair.of(publicHolidayRulesDto.getOrganizeList(), publicHolidayRulesDto.getUserIdList()), ScopeBizType.PUBLIC_HOLIDAY_RULE); + ruleScopeUtil.saveBatch(ruleScopeList); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(AttendancePublicHolidayRulesDto publicHolidayRulesDto) throws Exception { + // 修改的范围为全部清空其他 + if (0 == publicHolidayRulesDto.getScopeOfAdaptation()) { + publicHolidayRulesMapper.updateScopeOfAdaptation(); + } + // 查询旧数据 + AttendancePublicHolidayRules oldEntity = publicHolidayRulesMapper.selectById(publicHolidayRulesDto.getId()); + publicHolidayRulesMapper.update(publicHolidayRulesDto, userProvider.get().getId()); + // 当适配范围为指定组织或成员时绑定适配范围 + ruleScopeUtil.updateRuleScopeList(publicHolidayRulesDto.getId(), oldEntity.getScopeOfAdaptation(), publicHolidayRulesDto.getScopeOfAdaptation(), MutablePair.of(publicHolidayRulesDto.getOrganizeList(), publicHolidayRulesDto.getUserIdList()), ScopeBizType.PUBLIC_HOLIDAY_RULE); + } + + @Override + public AttendancePublicHolidayRulesVo selectOne(String id) { + AttendancePublicHolidayRules attendancePublicHolidayRules = publicHolidayRulesMapper.selectById(id); + // 将attendancePublicHolidayRules转换为AttendancePublicHolidayRulesVo + AttendancePublicHolidayRulesVo bean = BeanUtil.toBean(attendancePublicHolidayRules, AttendancePublicHolidayRulesVo.class); + // 当适配范围为指定组织或成员时查询适配范围 + if (bean.getScopeOfAdaptation().equals(1)) { + MutablePair, List> pair = ruleScopeUtil.selectScopeList(bean.getId(), ScopeBizType.PUBLIC_HOLIDAY_RULE); + if (null != pair) { + bean.setOrganizeList(pair.getLeft()); + bean.setUserIdList(pair.getRight()); + } + } else if (Objects.equals(bean.getScopeOfAdaptation(), -1)) { + bean.setScopeOfAdaptation(null); + } + return bean; + } + + @Override + public void delete(String id) { + publicHolidayRulesMapper.deleteById(id); + // 删除适配范围 + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, id) + .eq(AttendanceRuleScope::getBizType, ScopeBizType.PUBLIC_HOLIDAY_RULE)); + // + + } + + @Override + public void updateState(AttendancePublicHolidayRulesDto publicHolidayRulesDto) { + publicHolidayRulesMapper.updateState(publicHolidayRulesDto.getId(), publicHolidayRulesDto.getState(), userProvider.get().getId()); + } + + /** + * 获取公休余额列表 + * 公休记录 在封账时触发缓存(缓存公休总额及仅记录发放薪资的天数),没有缓存的默认为动态的 + * + * @param yearMonth 年月 + * @param userIds 用户ID集合 + * @return 公休余额列表 + */ + @Override + public List getBalanceList(String yearMonth, List userIds) { + // 公休记录 在封账时触发缓存,没有缓存的默认为动态的 + List balanceList = publicHolidayRulesMapper.getBalanceList(yearMonth, userIds); + if (balanceList.isEmpty() || balanceList.size() != userIds.size()) { + // 找出缺少的用户ID + List missingUserIds = userIds.stream().filter(v -> balanceList.stream().noneMatch(b -> b.getUserId().equals(v))).collect(Collectors.toList()); + // 动态查询当前年月的公休余额 + if (CollUtil.isEmpty(missingUserIds)) { + return balanceList; + } + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(missingUserIds, ScopeBizType.PUBLIC_HOLIDAY_RULE, ConstantUtil.NUM_TRUE); + Map> map = attendanceRuleScopes.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getUserId)); + List rules = publicHolidayRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendancePublicHolidayRules::getDeleteMark, 0) + .eq(AttendancePublicHolidayRules::getState, 1) + .in(CollUtil.isNotEmpty(attendanceRuleScopes), AttendancePublicHolidayRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).collect(Collectors.toList())) + ); + // 根据规则Id 启用的公休规则 + Map ruleMap = rules.stream().collect(Collectors.toMap(AttendancePublicHolidayRules::getId, entity -> entity)); + // 没有的用户ID,默认值为0 + missingUserIds.forEach(v -> { + AttendancePublicHolidayBalance balance = new AttendancePublicHolidayBalance(); + // 计算公休总额 + List userRules = map.get(v); + if (CollUtil.isNotEmpty(userRules)) { + BigDecimal total = BigDecimal.ZERO; + for (AttendanceRuleScope ruleScope : userRules) { + AttendancePublicHolidayRules rule = ruleMap.get(ruleScope.getRuleId()); + if (rule != null) { + total = total.add(BigDecimal.valueOf(rule.getDayNum())); + } + } + balance.setTotal(total); + } + balance.setUserId(v); + balanceList.add(balance); + }); + } + return balanceList; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void processPublicHoliday(String yearMonth, List userIds, Map ratioMap) { + // 获取当月有多少天 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + // 解析为 YearMonth 对象 + YearMonth ym = YearMonth.parse(yearMonth, formatter); + // 获取当月天数 + int daysInMonth = ym.lengthOfMonth(); + + // 仅记录 + List list = new ArrayList<>(); + // 需要发劵的用户余额记录 + List balanceRecordList = new ArrayList<>(); + // 查询用户对应的公休规则 + List attendanceRuleScopes = ruleScopeUtil.selectUserEffectListBatch(userIds, ScopeBizType.PUBLIC_HOLIDAY_RULE, ConstantUtil.NUM_TRUE); + Map map = attendanceRuleScopes.stream().collect(Collectors.toMap(AttendanceRuleScope::getUserId, entity -> entity)); + List rules = publicHolidayRulesMapper.selectList(new QueryWrapper() + .lambda() + .eq(AttendancePublicHolidayRules::getDeleteMark, 0) + .eq(AttendancePublicHolidayRules::getState, 1) + .in(CollUtil.isNotEmpty(attendanceRuleScopes), AttendancePublicHolidayRules::getId, attendanceRuleScopes.stream().map(AttendanceRuleScope::getRuleId).collect(Collectors.toList())) + ); + // 根据规则Id 启用的公休规则 + Map ruleMap = rules.stream().collect(Collectors.toMap(AttendancePublicHolidayRules::getId, entity -> entity)); + // 获取用户当月使用的公休天数 + Map userPublicHoliday = dailyRuleService.getUserPublicHoliday(yearMonth, userIds); + // 没有的用户ID,默认值为0 + userIds.forEach(v -> { + AttendancePublicHolidayBalance balance = new AttendancePublicHolidayBalance(); + balance.setId(FtbUtil.getId()); + balance.setUserId(v); + // 计算公休总额 理论上只会命中一个userRules + AttendanceRuleScope userRules = map.get(v); + if (null != userRules) { + AttendancePublicHolidayRules rule = ruleMap.get(userRules.getRuleId()); + if (rule != null) { + // 考虑月份大小,最大值为当月的天数 + balance.setTotal(daysInMonth > rule.getDayNum() ? BigDecimal.valueOf(rule.getDayNum()) : BigDecimal.valueOf(daysInMonth)); + balance.setTransformationType(rule.getTransformationType()); + balance.setRuleId(rule.getId()); + balance.setYearMonth(yearMonth); + BigDecimal use = userPublicHoliday.get(v); + // 总数-使用了的,剩余数小于0 取0 + balance.setBalance(balance.getTotal().subtract(null == use ? BigDecimal.ZERO : use).max(BigDecimal.ZERO)); + balance.setAttendanceRatio(ratioMap.get(v)); + list.add(balance); + } + } + }); + // 移除已经有记录的用户 + List uIds = publicHolidayRulesMapper.selectPublicHolidayBalanceList(yearMonth, userIds); + List list1 = list; + if (CollUtil.isNotEmpty(uIds)) { + list1 = list.stream().filter(v -> !uIds.contains(v.getUserId())).collect(Collectors.toList()); + } + // 批量添加公休余额 + if (CollUtil.isNotEmpty(list1)) { + publicHolidayRulesMapper.batchAddPublicHolidayBalance(list1, yearMonth); + // 过滤出需要转存休且余额大于0的数据 + List needRetirementLeave = list1.stream().filter(v -> v.getTransformationType() == 0 && v.getBalance().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(needRetirementLeave)) { + attendanceUserBalanceService.addUserBalances(needRetirementLeave); + // 组装劵信息 + needRetirementLeave.forEach(v -> { + AttendanceBalanceRecordEntity record = new AttendanceBalanceRecordEntity(); + record.setUserId(v.getUserId()); + record.setType(3); + record.setBalance(v.getBalance()); + record.setTotal(v.getBalance()); + record.setUnit(2); + //计算过期时间 + AttendancePublicHolidayRules rule = ruleMap.get(v.getRuleId()); + record.setExpireTime(expiresTimeUtil.getExpiresTime(rule.getLifespanType(), rule.getFixedDay(), rule.getSpecifyDay())); + balanceRecordList.add(record); + }); + // 发放劵 + attendanceUserBalanceRecordService.saveBatch(balanceRecordList); + } + } + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void rollbackPublicHoliday(String yearMonth, List userIds) { + // 查出对应的公休余额列表 + List publicHolidayBalanceList = publicHolidayRulesMapper.getPublicHolidayBalanceList(yearMonth, userIds); + // 过滤出转存存休且余额大于0的数据 + List needRetirementLeave = publicHolidayBalanceList.stream().filter(v -> v.getTransformationType() == 0 && v.getBalance().compareTo(BigDecimal.ZERO) > 0).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(needRetirementLeave)) { + // 转存休数据删除 + attendanceUserBalanceService.rollbackUserBalance(needRetirementLeave); + // 回退对应的劵数据 + attendanceUserBalanceRecordService.rollbackUserBalanceRecord(needRetirementLeave, null, null, true); + } + // 删除对应数据 + publicHolidayRulesMapper.deletePublicHolidayBalance(yearMonth, userIds); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RV1109MachineServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RV1109MachineServiceImpl.java new file mode 100644 index 0000000..af93605 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RV1109MachineServiceImpl.java @@ -0,0 +1,42 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import jnpf.attendance.service.RV1109MachineService; +import jnpf.base.ActionResult; +import jnpf.permission.V2UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.user.UserBoundVO; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * RV1109考勤机服务实现 + * + * @author yanwenfu + * @create 2024-04-09 + */ +@Service +public class RV1109MachineServiceImpl implements RV1109MachineService { + + @Resource + private V2UserApi v2UserApi; + + @Override + public UserEntity getUserInfoById(String userId, String tenantId) { + +// List userEntityList = userApi.getUserListNoData(Stream.of(userId).collect(Collectors.toList()), tenantId); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(Stream.of(userId).collect(Collectors.toList()), tenantId); + if (200 != allUserInfoBatch.getCode() || allUserInfoBatch.getData().isEmpty()) { + return null; + } +// return userEntityList.get(0); + UserBoundVO userBoundVO = allUserInfoBatch.getData().get(0); + UserEntity userEntity = BeanUtil.copyProperties(userBoundVO, UserEntity.class); + userEntity.setRealName(userBoundVO.getUserName()); + return userEntity; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RuleScopeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RuleScopeServiceImpl.java new file mode 100644 index 0000000..d165360 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/RuleScopeServiceImpl.java @@ -0,0 +1,42 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.RuleScopeMapper; +import jnpf.attendance.service.RuleScopeService; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.model.attendance.dto.UserOrgDto; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 适配范围服务实现 + * + * @author yanwenfu + * @create 2025-09-18 + */ +@Service +public class RuleScopeServiceImpl extends SuperServiceImpl implements RuleScopeService { + + @Resource + private RuleScopeMapper ruleScopeMapper; + + @Override + public List selectUserEffectList(String userId, String organizeId, ScopeBizType bizType) { + + return ruleScopeMapper.selectUserEffectList(userId, organizeId, bizType.getValue()); + } + + @Override + public List selectUserEffectListBatch(List userOrgList, ScopeBizType bizType, Integer priority,List leaveTypeIds) { + + return ruleScopeMapper.selectUserEffectListBatch(userOrgList, bizType.getValue(), priority, leaveTypeIds); + } + + @Override + public List selectOrgEffectListBatch(String organizeId) { + return ruleScopeMapper.selectOrgEffectListBatch(organizeId, ScopeBizType.LEAVE_RULES.getValue()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ScheduleGroupRuleConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ScheduleGroupRuleConfigServiceImpl.java new file mode 100644 index 0000000..191d7af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/ScheduleGroupRuleConfigServiceImpl.java @@ -0,0 +1,369 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.mapper.FtbScheduleGroupDrawingParamMapper; +import jnpf.attendance.mapper.FtbScheduleGroupFixedParamMapper; +import jnpf.attendance.service.ScheduleGroupRuleConfigService; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.FtbScheduleGroupDrawingParamEntity; +import jnpf.entity.attendance.FtbScheduleGroupFixedParamEntity; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.scheduling.FixedSchedulingRuleDto; +import jnpf.model.attendance.dto.scheduling.LineSchedulingRuleDto; +import jnpf.model.attendance.dto.scheduling.ScheduleGroupRuleConfigDto; +import jnpf.model.attendance.vo.scheduling.FixedSchedulingRuleVo; +import jnpf.model.attendance.vo.scheduling.LineSchedulingRuleVo; +import jnpf.model.attendance.vo.scheduling.ScheduleGroupRuleConfigVo; +import jnpf.util.RandomUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.Date; +import java.util.function.Consumer; + +/** + * 考勤组排班规则配置:固定排班核心参数 + 划线排班参数查询与保存。 + * + * @author xiaofeng + * @since 2026-05-13 + */ +@Slf4j +@Service +public class ScheduleGroupRuleConfigServiceImpl + extends SuperServiceImpl + implements ScheduleGroupRuleConfigService { + + /** 约束级别:0=优先满足(软约束) */ + private static final byte PRIORITY_PREFER = 0; + /** 约束级别:1=必须满足(硬约束) */ + private static final byte PRIORITY_MUST = 1; + + @Resource + private FtbScheduleGroupFixedParamMapper fixedParamMapper; + + @Resource + private FtbScheduleGroupDrawingParamMapper drawingParamMapper; + + /** + * 按考勤组查询固定排班与划线排班规则;库中无记录时对子块填充产品默认结构后返回 VO。 + */ + @Override + public ScheduleGroupRuleConfigVo getRuleConfig(String groupId) throws HandleException, QueryException { + long start = System.currentTimeMillis(); + assertGroupIdNotBlank(groupId); + + ScheduleGroupRuleConfigVo result = new ScheduleGroupRuleConfigVo(); + result.setGroupId(groupId); + result.setFixedScheduling( + toFixedVo(normalizeFixedRuleForGet(fixedParamMapper.selectFixedRuleDtoByGroupId(groupId)))); + result.setLineScheduling( + toLineVo(normalizeLineRuleForGet(drawingParamMapper.selectLineRuleDtoByGroupId(groupId)))); + + log.info("获取排班规则配置,groupId=>{},耗时=>{} 毫秒", groupId, System.currentTimeMillis() - start); + return result; + } + + /** + * 校验两块规则后,在同一事务内分别 upsert 固定参数表与划线参数表,并回查最新配置。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ScheduleGroupRuleConfigVo saveRuleConfig(String groupId, ScheduleGroupRuleConfigDto dto) + throws HandleException, QueryException { + long start = System.currentTimeMillis(); + + // 1、参数校验 + if (dto == null) { + throw new HandleException("请求体不能为空"); + } + String effectiveGroupId = StringUtil.isNotBlank(groupId) ? groupId : dto.getGroupId(); + assertGroupIdNotBlank(effectiveGroupId); + if (StringUtil.isNotBlank(dto.getGroupId()) && !effectiveGroupId.equals(dto.getGroupId())) { + throw new HandleException("请求体 groupId 与入参不一致"); + } + + if (dto.getFixedScheduling() == null || dto.getLineScheduling() == null) { + throw new HandleException("fixedScheduling、lineScheduling 不能为空"); + } + + validateFixedRule(dto.getFixedScheduling()); + validateLineRule(dto.getLineScheduling()); + + Date operateTime = new Date(); + String operatorId = currentUserId(); + + upsertFixedRule(effectiveGroupId, dto.getFixedScheduling(), operateTime, operatorId); + upsertLineRule(effectiveGroupId, dto.getLineScheduling(), operateTime, operatorId); + + log.info("保存排班规则配置,groupId=>{},耗时=>{} 毫秒", effectiveGroupId, System.currentTimeMillis() - start); + return getRuleConfig(effectiveGroupId); + } + + private static void assertGroupIdNotBlank(String groupId) throws HandleException { + if (StringUtil.isBlank(groupId)) { + throw new HandleException("考勤组ID不能为空"); + } + } + + private static String currentUserId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo == null ? null : userInfo.getUserId(); + } + + /** + * 校验固定排班五项约束:启用时数值非空且非负,优先级仅允许 0/1。 + */ + private static void validateFixedRule(FixedSchedulingRuleDto rule) throws HandleException { + assertValueWhenEnabled("单日工作小时数", rule.getDailyWorkHoursEnabled(), rule.getDailyWorkHoursValue()); + assertPriorityRange("单日工作小时数", rule.getDailyWorkHoursPriority()); + assertValueWhenEnabled("连续工作天数", rule.getConsecutiveWorkDaysEnabled(), rule.getConsecutiveWorkDaysValue()); + assertPriorityRange("连续工作天数", rule.getConsecutiveWorkDaysPriority()); + assertValueWhenEnabled("每周工作天数", rule.getWeeklyWorkDaysEnabled(), rule.getWeeklyWorkDaysValue()); + assertPriorityRange("每周工作天数", rule.getWeeklyWorkDaysPriority()); + assertValueWhenEnabled("每周工作小时数", rule.getWeeklyWorkHoursEnabled(), rule.getWeeklyWorkHoursValue()); + assertPriorityRange("每周工作小时数", rule.getWeeklyWorkHoursPriority()); + assertValueWhenEnabled("班次间最小休息间隔", rule.getMinRestBetweenShiftsEnabled(), rule.getMinRestBetweenShiftsValue()); + assertPriorityRange("班次间最小休息间隔", rule.getMinRestBetweenShiftsPriority()); + } + + /** + * 校验划线排班三项约束:启用时数值非空且非负,优先级仅允许 0/1。 + */ + private static void validateLineRule(LineSchedulingRuleDto rule) throws HandleException { + assertValueWhenEnabled("单日班段个数", rule.getMaxDailySegmentsEnabled(), rule.getMaxDailySegmentsValue()); + assertPriorityRange("单日班段个数", rule.getMaxDailySegmentsPriority()); + assertValueWhenEnabled("单段最长", rule.getMaxSingleSegmentHoursEnabled(), rule.getMaxSingleSegmentHoursValue()); + assertPriorityRange("单段最长", rule.getMaxSingleSegmentHoursPriority()); + assertDecimalValueWhenEnabled("单段最短", rule.getMinSingleSegmentHoursEnabled(), rule.getMinSingleSegmentHoursValue()); + assertPriorityRange("单段最短", rule.getMinSingleSegmentHoursPriority()); + } + + private static void assertDecimalValueWhenEnabled(String label, Boolean enabled, BigDecimal value) throws HandleException { + if (Boolean.TRUE.equals(enabled) && value == null) { + throw new HandleException(label + "启用时数值不能为空"); + } + if (Boolean.TRUE.equals(enabled) && value != null && value.compareTo(BigDecimal.ZERO) < 0) { + throw new HandleException(label + "数值不能为负数"); + } + } + + private static void assertValueWhenEnabled(String label, Boolean enabled, Integer value) throws HandleException { + if (Boolean.TRUE.equals(enabled) && value == null) { + throw new HandleException(label + "启用时数值不能为空"); + } + if (Boolean.TRUE.equals(enabled) && value != null && value < 0) { + throw new HandleException(label + "数值不能为负数"); + } + } + + /** + * 约束级别取值校验:{@link #PRIORITY_PREFER} 或 {@link #PRIORITY_MUST},null 表示未设置由落库默认补齐。 + */ + private static void assertPriorityRange(String label, Byte priority) throws HandleException { + if (priority != null && priority != PRIORITY_PREFER && priority != PRIORITY_MUST) { + throw new HandleException(label + "约束级别取值须为 0 或 1"); + } + } + + private FtbScheduleGroupFixedParamEntity selectFixedParamByGroupId(String groupId) { + return lambdaQuery() + .eq(FtbScheduleGroupFixedParamEntity::getGroupId, groupId) + .one(); + } + + private FtbScheduleGroupDrawingParamEntity selectDrawingParamByGroupId(String groupId) { + return drawingParamMapper.selectOne(Wrappers.lambdaQuery() + .eq(FtbScheduleGroupDrawingParamEntity::getGroupId, groupId)); + } + + private static FixedSchedulingRuleVo toFixedVo(FixedSchedulingRuleDto dto) { + return BeanUtil.toBean(dto, FixedSchedulingRuleVo.class); + } + + private static LineSchedulingRuleVo toLineVo(LineSchedulingRuleDto dto) { + return BeanUtil.toBean(dto, LineSchedulingRuleVo.class); + } + + /** + * 查询结果归一化:无库记录时返回与表 DEFAULT 一致的全量默认 DTO。 + */ + private static FixedSchedulingRuleDto normalizeFixedRuleForGet(FixedSchedulingRuleDto fromDb) { + if (fromDb == null) { + return applyFixedDefaultsToDto(); + } + return fromDb; + } + + /** + * 查询结果归一化:无库记录时返回与表 DEFAULT 一致的全量默认 DTO。 + */ + private static LineSchedulingRuleDto normalizeLineRuleForGet(LineSchedulingRuleDto fromDb) { + if (fromDb == null) { + return applyLineDefaultsToDto(); + } + return fromDb; + } + + /** 固定排班无记录时的接口层默认结构(与产品约定及表 DEFAULT 对齐)。 */ + private static FixedSchedulingRuleDto applyFixedDefaultsToDto() { + FixedSchedulingRuleDto d = new FixedSchedulingRuleDto(); + d.setDailyWorkHoursEnabled(Boolean.TRUE); + d.setDailyWorkHoursValue(8); + d.setDailyWorkHoursPriority(PRIORITY_MUST); + d.setConsecutiveWorkDaysEnabled(Boolean.TRUE); + d.setConsecutiveWorkDaysValue(6); + d.setConsecutiveWorkDaysPriority(PRIORITY_MUST); + d.setWeeklyWorkDaysEnabled(Boolean.TRUE); + d.setWeeklyWorkDaysValue(6); + d.setWeeklyWorkDaysPriority(PRIORITY_MUST); + d.setWeeklyWorkHoursEnabled(Boolean.TRUE); + d.setWeeklyWorkHoursValue(40); + d.setWeeklyWorkHoursPriority(PRIORITY_MUST); + d.setMinRestBetweenShiftsEnabled(Boolean.TRUE); + d.setMinRestBetweenShiftsValue(10); + d.setMinRestBetweenShiftsPriority(PRIORITY_MUST); + return d; + } + + /** 划线排班无记录时的接口层默认结构(与产品约定及表 DEFAULT 对齐)。 */ + private static LineSchedulingRuleDto applyLineDefaultsToDto() { + LineSchedulingRuleDto d = new LineSchedulingRuleDto(); + d.setMaxDailySegmentsEnabled(Boolean.TRUE); + d.setMaxDailySegmentsValue(2); + d.setMaxDailySegmentsPriority(PRIORITY_MUST); + d.setMaxSingleSegmentHoursEnabled(Boolean.TRUE); + d.setMaxSingleSegmentHoursValue(4); + d.setMaxSingleSegmentHoursPriority(PRIORITY_MUST); + d.setMinSingleSegmentHoursEnabled(Boolean.TRUE); + d.setMinSingleSegmentHoursValue(new BigDecimal("2")); + d.setMinSingleSegmentHoursPriority(PRIORITY_MUST); + return d; + } + + /** + * 落库前补齐固定排班实体中 null 的开关与优先级字段,避免写入不完整行。 + */ + private static void applyFixedDefaults(FtbScheduleGroupFixedParamEntity entity) { + if (entity.getDailyWorkHoursEnabled() == null) { + entity.setDailyWorkHoursEnabled(Boolean.TRUE); + } + if (entity.getDailyWorkHoursPriority() == null) { + entity.setDailyWorkHoursPriority(PRIORITY_MUST); + } + if (entity.getConsecutiveWorkDaysEnabled() == null) { + entity.setConsecutiveWorkDaysEnabled(Boolean.TRUE); + } + if (entity.getConsecutiveWorkDaysPriority() == null) { + entity.setConsecutiveWorkDaysPriority(PRIORITY_MUST); + } + if (entity.getWeeklyWorkDaysEnabled() == null) { + entity.setWeeklyWorkDaysEnabled(Boolean.TRUE); + } + if (entity.getWeeklyWorkDaysPriority() == null) { + entity.setWeeklyWorkDaysPriority(PRIORITY_MUST); + } + if (entity.getWeeklyWorkHoursEnabled() == null) { + entity.setWeeklyWorkHoursEnabled(Boolean.TRUE); + } + if (entity.getWeeklyWorkHoursPriority() == null) { + entity.setWeeklyWorkHoursPriority(PRIORITY_MUST); + } + if (entity.getMinRestBetweenShiftsEnabled() == null) { + entity.setMinRestBetweenShiftsEnabled(Boolean.TRUE); + } + if (entity.getMinRestBetweenShiftsPriority() == null) { + entity.setMinRestBetweenShiftsPriority(PRIORITY_MUST); + } + } + + /** + * 落库前补齐划线排班实体中 null 的开关、数值与优先级;单段最短默认关闭但保留默认时长。 + */ + private static void applyLineDefaults(FtbScheduleGroupDrawingParamEntity entity) { + if (entity.getMaxDailySegmentsEnabled() == null) { + entity.setMaxDailySegmentsEnabled(Boolean.TRUE); + } + if (entity.getMaxDailySegmentsPriority() == null) { + entity.setMaxDailySegmentsPriority(PRIORITY_MUST); + } + if (entity.getMaxSingleSegmentHoursEnabled() == null) { + entity.setMaxSingleSegmentHoursEnabled(Boolean.TRUE); + } + if (entity.getMaxSingleSegmentHoursPriority() == null) { + entity.setMaxSingleSegmentHoursPriority(PRIORITY_MUST); + } + if (entity.getMinSingleSegmentHoursEnabled() == null) { + entity.setMinSingleSegmentHoursEnabled(Boolean.FALSE); + } + if (entity.getMinSingleSegmentHoursValue() == null) { + entity.setMinSingleSegmentHoursValue(new BigDecimal("2")); + } + if (entity.getMinSingleSegmentHoursPriority() == null) { + entity.setMinSingleSegmentHoursPriority(PRIORITY_MUST); + } + } + + /** + * 按 groupId 对固定排班参数表 insert 或 update,保留原创建人/创建时间。 + */ + private void upsertFixedRule(String groupId, FixedSchedulingRuleDto dto, Date operateTime, String operatorId) { + FtbScheduleGroupFixedParamEntity existing = selectFixedParamByGroupId(groupId); + FtbScheduleGroupFixedParamEntity row = BeanUtil.toBean(dto, FtbScheduleGroupFixedParamEntity.class); + row.setGroupId(groupId); + applyFixedDefaults(row); + if (existing == null) { + row.setId(StringUtil.isNotBlank(dto.getId()) ? dto.getId() : RandomUtil.uuId()); + fillAuditOnInsert(row::setCreateTime, row::setUpdateTime, row::setCreator, row::setUpdater, operateTime, operatorId); + save(row); + } else { + row.setId(existing.getId()); + row.setCreateTime(existing.getCreateTime()); + row.setCreator(existing.getCreator()); + row.setUpdateTime(operateTime); + row.setUpdater(operatorId); + updateById(row); + } + } + + /** + * 按 groupId 对划线排班参数表 insert 或 update,保留原创建人/创建时间。 + */ + private void upsertLineRule(String groupId, LineSchedulingRuleDto dto, Date operateTime, String operatorId) { + FtbScheduleGroupDrawingParamEntity existing = selectDrawingParamByGroupId(groupId); + FtbScheduleGroupDrawingParamEntity row = BeanUtil.toBean(dto, FtbScheduleGroupDrawingParamEntity.class); + row.setGroupId(groupId); + applyLineDefaults(row); + if (existing == null) { + row.setId(StringUtil.isNotBlank(dto.getId()) ? dto.getId() : RandomUtil.uuId()); + fillAuditOnInsert(row::setCreateTime, row::setUpdateTime, row::setCreator, row::setUpdater, operateTime, operatorId); + drawingParamMapper.insert(row); + } else { + row.setId(existing.getId()); + row.setCreateTime(existing.getCreateTime()); + row.setCreator(existing.getCreator()); + row.setUpdateTime(operateTime); + row.setUpdater(operatorId); + drawingParamMapper.updateById(row); + } + } + + private static void fillAuditOnInsert(Consumer setCreateTime, + Consumer setUpdateTime, + Consumer setCreator, + Consumer setUpdater, + Date operateTime, + String operatorId) { + setCreateTime.accept(operateTime); + setUpdateTime.accept(operateTime); + setCreator.accept(operatorId); + setUpdater.accept(operatorId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/SmartPreScheduleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/SmartPreScheduleServiceImpl.java new file mode 100644 index 0000000..7819fc2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/SmartPreScheduleServiceImpl.java @@ -0,0 +1,147 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedEmployeeView; +import jnpf.attendance.service.SmartPreScheduleService; +import jnpf.attendance.service.WorkstationService; +import jnpf.attendance.service.impl.preschedule.PreScheduleByEmployeeFilter; +import jnpf.attendance.service.impl.preschedule.PreScheduleIncompleteMsgBuilder; +import jnpf.attendance.service.impl.preschedule.PreScheduleQueryValidator; +import jnpf.attendance.service.impl.preschedule.PreScheduleQueryValidator.ValidatedPreScheduleQuery; +import jnpf.attendance.service.impl.preschedule.PreScheduleRedisSupport; +import jnpf.attendance.service.impl.preschedule.PreScheduleResultMapper; +import jnpf.attendance.service.impl.preschedule.PreScheduleSaveValidator; +import jnpf.attendance.service.impl.preschedule.PreScheduleSaveValidator.ValidatedPreScheduleSave; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.scheduling.PreScheduleTableQueryDto; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; +import jnpf.util.StringUtil; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; + +/** + * 预排班主表:校验请求、调用 {@link AutoScheduleService} 排班并映射为界面主表结构。 + */ +@Slf4j +@Service +public class SmartPreScheduleServiceImpl implements SmartPreScheduleService { + + @Resource + private AutoScheduleService autoScheduleService; + + @Resource + private WorkstationService workstationService; + + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + + @Resource + private PreScheduleRedisSupport preScheduleRedisSupport; + + /** + * 预排班主表生成流水线:入参校验 → 自动排班引擎 → 拉取工位人员 → 组装表格 VO(剔除全程无人员的岗位/工作站),并将 byEmployee 写入 Redis。 + * 排班结果若含不完整说明仅记 warn,不阻断返回。 + */ + @SneakyThrows + @Override + public PreScheduleTableVo buildPreScheduleTable(PreScheduleTableQueryDto dto) { + long startMs = System.currentTimeMillis(); + log.info("预排班生成主表,入参=>{}", dto); + + ValidatedPreScheduleQuery query = + PreScheduleQueryValidator.validate(dto, attendanceGroupMapper); + + String groupId = query.getGroupId(); + LocalDate startDate = query.getStartDate(); + LocalDate endDate = query.getEndDate(); + + ShiftPlanAssignmentResult assignmentResult = + autoScheduleService.start(groupId, startDate, endDate, query.getRevenueByDate()); + + List workstations = + workstationService.listWorkstationsByGroupId( + groupId, toDateAtStartOfDay(startDate), toDateAtStartOfDay(endDate)); + + PreScheduleTableVo result = + PreScheduleResultMapper.toTableVo( + groupId, + startDate, + endDate, + query.getRevenueByDate(), + query.getEffTarget(), + assignmentResult, + workstations); + + String scheduleMsg = PreScheduleIncompleteMsgBuilder.build(assignmentResult); + result.setMsg(scheduleMsg); + if (StringUtil.isNotBlank(scheduleMsg)) { + log.warn("预排班存在不完整说明,groupId=>{},msg=>{}", groupId, scheduleMsg); + } + + List byEmployee = + assignmentResult == null ? List.of() : assignmentResult.getByEmployee(); + preScheduleRedisSupport.saveByEmployee(groupId, startDate, endDate, byEmployee); + + log.info( + "预排班生成主表完成,groupId=>{},行数=>{},byEmployee人数=>{},msg=>{},耗时=>{}毫秒", + groupId, + result.getRows() == null ? 0 : result.getRows().size(), + byEmployee.size(), + scheduleMsg, + System.currentTimeMillis() - startMs); + return result; + } + + /** + * 按提交主表过滤 Redis 中的 byEmployee,写回缓存并返回 key 可变片段;不保存主表 VO、不重新跑排班。 + */ + @Override + public String savePreScheduleTable(PreScheduleTableVo vo) throws HandleException, QueryException { + long startMs = System.currentTimeMillis(); + log.info("预排班主表提交保存,入参=>{}", vo); + try { + ValidatedPreScheduleSave validated = + PreScheduleSaveValidator.validate(vo, attendanceGroupMapper); + String groupId = validated.getGroupId(); + LocalDate startDate = validated.getStartDate(); + LocalDate endDate = validated.getEndDate(); + + List cachedByEmployee = + preScheduleRedisSupport.requireByEmployee(groupId, startDate, endDate); + List filteredByEmployee = + PreScheduleByEmployeeFilter.filterByTable(cachedByEmployee, validated.getTable()); + preScheduleRedisSupport.saveByEmployee(groupId, startDate, endDate, filteredByEmployee); + + String redisKeySuffix = + PreScheduleRedisSupport.formatRedisKeySuffix(groupId, startDate, endDate); + + log.info( + "预排班主表提交保存完成,groupId=>{},区间=>{}..{},耗时=>{}毫秒", + groupId, + startDate, + endDate, + System.currentTimeMillis() - startMs); + return redisKeySuffix; + } catch (HandleException | QueryException ex) { + throw ex; + } catch (Exception ex) { + log.error("预排班主表提交保存失败", ex); + throw new HandleException("预排班保存失败:" + ex.getMessage()); + } + } + + /** 将 {@link LocalDate} 转为系统默认时区当日 00:00:00 的 {@link Date},供工位查询等遗留 API 使用。 */ + private static Date toDateAtStartOfDay(LocalDate day) { + return Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/StatisticsUtilServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/StatisticsUtilServiceImpl.java new file mode 100644 index 0000000..78ca512 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/StatisticsUtilServiceImpl.java @@ -0,0 +1,653 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.mapper.AttendanceLeaveApproveMapper; +import jnpf.attendance.mapper.StatisticsMapper; +import jnpf.attendance.service.AttendanceBaseSettingService; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.attendance.service.AttendanceRepairService; +import jnpf.attendance.service.StatisticsUtilService; +import jnpf.entity.attendance.AttendanceBaseSetting; +import jnpf.entity.attendance.AttendanceDayStatistics; +import jnpf.entity.attendance.AttendanceRepair; +import jnpf.entity.workflow.AttendanceBusinessTripApprove; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.enums.attendance.LeaveUnitEnum; +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.attendance.DayStatisticsQueryVo; +import jnpf.model.common.DateRangeDto; +import jnpf.model.common.DateRangeDto1; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.ConstantUtil; +import jnpf.util.DateConvertUtil; +import jnpf.util.DateUtil; +import jnpf.util.StringUtil; +import jnpf.util.attendance.DayStatisticsUtils; +import jnpf.workflow.service.BusinessTripApproveService; +import jnpf.workflow.service.GoOutApproveService; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static jnpf.util.DateUtil.dateFormat; +import static jnpf.util.DateUtil.strToDate; +import static jnpf.util.attendance.DayStatisticsUtils.*; + +/** + * 统计公共方法服务实现 + * + * @author shitou + * @ date 2023/11/21 + */ +@Slf4j +@Service +public class StatisticsUtilServiceImpl implements StatisticsUtilService { + @Resource + private StatisticsMapper statisticsMapper; + @Resource + private GoOutApproveService goOutApproveService; + @Resource + private AttendanceRepairService attendanceRepairService; + @Resource + private AttendanceBaseSettingService baseSettingService; + @Resource + private AttendanceLeaveApproveMapper leaveApproveMapper; + @Resource + private BusinessTripApproveService businessTripApproveService; + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + + @Override + public List getUserClockDateArrayList(String userId, DateRangeDto selecteDateRangeDto, + List dateArrayList, List dayStatisticsQueryVoList) { + List clockInfoList = new ArrayList<>(); + for (Date date : dateArrayList) { + List dayStatisticsQueryVos = dayStatisticsQueryVoList.stream().filter(item -> item.getDate().equals(date)).collect(Collectors.toList()); + clockInfoList.add(ClockInfo.builder().date(date).clockStatus(getClockStatus(dayStatisticsQueryVos)).build()); + } + return clockInfoList; + } + + + @Override + public Map> getLeaveMap(Map userMap, Map ratioMap, + List userIds, DateRangeDto dateRangeDto) { + Map> leaveRecordMap = new HashMap<>(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(AttendanceDayStatistics::getUserId, AttendanceDayStatistics::getGroupId, AttendanceDayStatistics::getLeaveBatchNumber, AttendanceDayStatistics::getCustomLeaveJson); + queryWrapper.in(AttendanceDayStatistics::getUserId, userIds); + queryWrapper.in(AttendanceDayStatistics::getGroupId, new ArrayList<>(ratioMap.keySet())); + queryWrapper.between(AttendanceDayStatistics::getDate, strToDate(dateRangeDto.getStartDate()), strToDate(dateRangeDto.getEndDate())); + queryWrapper.isNotNull(AttendanceDayStatistics::getLeaveBatchNumber); + queryWrapper.gt(AttendanceDayStatistics::getLeaveHours, BigDecimal.ZERO); + List dayStatisticsList = attendanceDayStatisticsService.list(queryWrapper); + if (CollUtil.isEmpty(dayStatisticsList)) { + return leaveRecordMap; + } + List applyList = dayStatisticsList.stream().filter(a -> StringUtil.isNotEmpty(a.getCustomLeaveJson())) + .flatMap(a -> JSONArray.parseArray(a.getCustomLeaveJson(), LeaveTypeJsonData.class).stream()).collect(Collectors.toList()); + Map applyMap = CollUtil.isNotEmpty(applyList) ? applyList.stream().map(LeaveTypeJsonData::getApplyList) + .flatMap(Collection::stream).collect(Collectors.groupingBy( + LeaveTypeStaDetailsModel::getApplyId, + Collectors.reducing(new LeaveTypeStaDetailsModel(), (model1, model2) -> { + LeaveTypeStaDetailsModel result = new LeaveTypeStaDetailsModel(); + result.setApplyId(model1.getApplyId()); + result.setLeaveDays(model1.getLeaveDays().add(model2.getLeaveDays())); + result.setLeaveHours(model1.getLeaveHours().add(model2.getLeaveHours())); + result.setLeavePayrollHours(model1.getLeavePayrollHours().add(model2.getLeavePayrollHours())); + return result; + }) + )) : new HashMap<>(); + List applyAllIds = dayStatisticsList.stream().flatMap(a -> Arrays.stream(a.getLeaveBatchNumber().split(","))).collect(Collectors.toList()); + List leaveForApplyList = CollUtil.isEmpty(applyAllIds) ? new ArrayList<>() : leaveApproveMapper.getUserLeaveListByApplyIds(applyAllIds); + if (CollUtil.isEmpty(leaveForApplyList)) { + return leaveRecordMap; + } + Map leaveApproveMap = leaveForApplyList.stream().collect(Collectors.toMap(LeaveSituationData::getApplyId, Function.identity())); + Map> userLeavedMap = leaveForApplyList.stream().collect(Collectors.groupingBy(leave -> leave.getUserId() + "&" + leave.getGroupId())); + for (Map.Entry> entry : userLeavedMap.entrySet()) { + String userAndGroupKey = entry.getKey(); + String userId = userAndGroupKey.split("&")[0]; + String groupId = userAndGroupKey.split("&")[1]; + List leaveList = entry.getValue(); + if (!userMap.containsKey(userId) || !ratioMap.containsKey(groupId)) { + continue; + } + UserBoundVO user = userMap.get(userId); + List leaveRecordList = new ArrayList<>(); + List applyIds = leaveList.stream().map(LeaveSituationData::getApplyId).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); + Map userLeaveApproveMap = applyIds.stream().filter(leaveApproveMap::containsKey).collect(Collectors.toMap(Function.identity(), leaveApproveMap::get)); + for (Map.Entry approveEntry : userLeaveApproveMap.entrySet()) { + LeaveSituationData approve = approveEntry.getValue(); + LeaveRecord leaveRecord = new LeaveRecord(); + leaveRecord.setUserName(user.getUserName()); + DateTimeFormatter formatterDayStr = approve.getUnit().equals(1) ? DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") : DateTimeFormatter.ofPattern("yyyy-MM-dd"); + leaveRecord.setRangeStr(DayStatisticsUtils.formatDayRange(approve.getStartTime(), approve.getEndTime(), formatterDayStr)); + leaveRecord.setLeaveDays(applyMap.get(approve.getApplyId()).getLeaveDays()); + leaveRecord.setType(approve.getLeaveName()); + leaveRecordList.add(leaveRecord); + } + leaveRecordMap.put(userAndGroupKey, leaveRecordList); + } + return leaveRecordMap; + } + + @Override + public Map> getLateMap(Map userMap, Map sealMap, List groupIds, + List userIds, DateRangeDto dateRangeDto) { + Map> lateRecordMap = new HashMap<>(); + List userClockInResultList = this.statisticsMapper.getUserClockInResultList( + List.of(AttendanceTypeEnum.ORDINARY.getCode()), userIds, + dateRangeDto, groupIds, ClockInStatusEnum.WORK_LATE.getValue(), 0, null, null); + if (CollUtil.isEmpty(userClockInResultList)) { + return lateRecordMap; + } + //查询考勤组配置信息 + Map baseSettingMap = baseSettingService.getEnableBaseSetting(userClockInResultList + .stream().map(ClockInResultRecord::getGroupId).distinct().collect(Collectors.toList())); + //按照用户+&考勤组分组获取数据 + Map> userClockMap = userClockInResultList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, list) -> { + String userId = userAndGroupKey.split("&")[0]; + String groupId = userAndGroupKey.split("&")[1]; + // 获取补卡次数 + AttendanceRepair repairByDate = attendanceRepairService.getAttendanceRepairByDate(DateUtil.daFormat(new Date()), userId, groupId); + int repairNum = repairByDate != null ? repairByDate.getRepairNum() : 0; + // 获取审批中的补卡次数 + int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(userId, groupId, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + // 是否允许补卡 + boolean isApply = (repairNum - applyCount) > 0; + List lateRecordList = new ArrayList<>(); + for (ClockInResultRecord inResultRecord : list) { + LateRecord lateRecord = new LateRecord(); + BeanUtils.copyProperties(inResultRecord, lateRecord); + lateRecord.setLateDate(inResultRecord.getDay()); + lateRecord.setLateMinutes(DateConvertUtil.secondConvert(inResultRecord.getAbnormalSecond(), 1, 2, null)); + lateRecord.setUserName(userMap.get(userId).getUserName()); + lateRecord.setSeal(sealMap.getOrDefault(userId, false) ? 1 : 0); + if (baseSettingMap.containsKey(inResultRecord.getGroupId())) { + AttendanceBaseSetting baseSetting = baseSettingMap.get(inResultRecord.getGroupId()); + lateRecord.setIsOperate(Objects.isNull(baseSetting.getPatchClockStatus()) || baseSetting.getPatchClockStatus().equals(0) ? 0 : + Objects.nonNull(baseSetting.getPatchType()) && baseSetting.getPatchType().contains("1") && isApply ? 1 : 0); + } + lateRecordList.add(lateRecord); + } + if (CollUtil.isNotEmpty(lateRecordList)) { + lateRecordList = lateRecordList.stream().sorted(Comparator.comparing(LateRecord::getLateDate) + .thenComparing(LateRecord::getClockTime)).collect(Collectors.toList()); + } + lateRecordMap.put(userAndGroupKey, lateRecordList); + }); + return lateRecordMap; + } + + @Override + public Map> getEarlyMap(Map userMap, Map sealMap, List groupIds, + List userIds, DateRangeDto dateRangeDto) { + Map> earlyListMap = new HashMap<>(); + List userClockInResultList = this.statisticsMapper.getUserClockInResultList( + List.of(AttendanceTypeEnum.ORDINARY.getCode()), userIds, + dateRangeDto, groupIds, ClockInStatusEnum.HOME_EARLY.getValue(), 0, null, null); + if (CollUtil.isEmpty(userClockInResultList)) { + return earlyListMap; + } + //查询考勤组配置信息 + Map baseSettingMap = baseSettingService.getEnableBaseSetting(userClockInResultList + .stream().map(ClockInResultRecord::getGroupId).distinct().collect(Collectors.toList())); + //按照用户+&考勤组分组获取数据 + Map> userClockMap = userClockInResultList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, list) -> { + String userId = userAndGroupKey.split("&")[0]; + String groupId = userAndGroupKey.split("&")[1]; + // 获取补卡次数 + AttendanceRepair repairByDate = attendanceRepairService.getAttendanceRepairByDate(DateUtil.daFormat(new Date()), userId, groupId); + int repairNum = repairByDate != null ? repairByDate.getRepairNum() : 0; + // 获取审批中的补卡次数 + int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(userId, groupId, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + // 是否允许补卡 + boolean isApply = (repairNum - applyCount) > 0; + List earlyLeaveRecordList = CollUtil.newArrayList(); + for (ClockInResultRecord inResultRecord : list) { + EarlyLeaveRecord earlyLeaveRecord = new EarlyLeaveRecord(); + BeanUtils.copyProperties(inResultRecord, earlyLeaveRecord); + earlyLeaveRecord.setEarlyLeaveDate(inResultRecord.getDay()); + earlyLeaveRecord.setEarlyLeaveMinutes(DateConvertUtil.secondConvert(inResultRecord.getAbnormalSecond(), 1, 2, null)); + earlyLeaveRecord.setUserName(userMap.get(userId).getUserName()); + earlyLeaveRecord.setSeal(sealMap.getOrDefault(userId, false) ? 1 : 0); + if (baseSettingMap.containsKey(inResultRecord.getGroupId())) { + AttendanceBaseSetting baseSetting = baseSettingMap.get(inResultRecord.getGroupId()); + earlyLeaveRecord.setIsOperate(Objects.isNull(baseSetting.getPatchClockStatus()) || baseSetting.getPatchClockStatus().equals(0) ? 0 : + Objects.nonNull(baseSetting.getPatchType()) && baseSetting.getPatchType().contains("2") && isApply ? 1 : 0); + } + earlyLeaveRecordList.add(earlyLeaveRecord); + } + if (CollUtil.isNotEmpty(earlyLeaveRecordList)) { + earlyLeaveRecordList = earlyLeaveRecordList.stream().sorted(Comparator.comparing(EarlyLeaveRecord::getEarlyLeaveDate) + .thenComparing(EarlyLeaveRecord::getClockTime)).collect(Collectors.toList()); + } + earlyListMap.put(userAndGroupKey, earlyLeaveRecordList); + }); + return earlyListMap; + } + + @Override + public Map> getAbsenceCardMap(Map userMap, Map sealMap, List groupIds, + List userIds, DateRangeDto dateRangeDto) { + Map> absenceCardMap = new HashMap<>(); + List userClockInResultList = this.statisticsMapper.getUserClockInResultList( + List.of(AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()), userIds, + dateRangeDto, groupIds, ClockInStatusEnum.NO_CLOCK.getValue(), 0, null, null); + if (CollUtil.isEmpty(userClockInResultList)) { + return absenceCardMap; + } + //查询考勤组配置信息 + Map baseSettingMap = baseSettingService.getEnableBaseSetting(userClockInResultList + .stream().map(ClockInResultRecord::getGroupId).distinct().collect(Collectors.toList())); + //按照用户+&考勤组分组获取数据 + Map> userClockMap = userClockInResultList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, list) -> { + String userId = userAndGroupKey.split("&")[0]; + String groupId = userAndGroupKey.split("&")[1]; + // 获取补卡次数 + AttendanceRepair repairByDate = attendanceRepairService.getAttendanceRepairByDate(DateUtil.daFormat(new Date()), userId, groupId); + int repairNum = repairByDate != null ? repairByDate.getRepairNum() : 0; + // 获取审批中的补卡次数 + int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(userId, groupId, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + // 是否允许补卡 + boolean isApply = (repairNum - applyCount) > 0; + List absenceCardList = CollUtil.newArrayList(); + for (ClockInResultRecord inResultRecord : list) { + AbsenceCardRecord absenceCardRecord = new AbsenceCardRecord(); + BeanUtils.copyProperties(inResultRecord, absenceCardRecord); + absenceCardRecord.setAbsenceCardDate(inResultRecord.getDay()); + absenceCardRecord.setPointOfTime(inResultRecord.getClockInType().equals(ConstantUtil.ON_WORK) ? + inResultRecord.getInPoint() : inResultRecord.getOutPoint()); + absenceCardRecord.setUserName(userMap.get(userId).getUserName()); + absenceCardRecord.setSeal(sealMap.getOrDefault(userId, false) ? 1 : 0); + if (baseSettingMap.containsKey(inResultRecord.getGroupId())) { + AttendanceBaseSetting baseSetting = baseSettingMap.get(inResultRecord.getGroupId()); + absenceCardRecord.setIsOperate(Objects.isNull(baseSetting.getPatchClockStatus()) || baseSetting.getPatchClockStatus().equals(0) ? 0 : + Objects.nonNull(baseSetting.getPatchType()) && baseSetting.getPatchType().contains("3") && isApply ? 1 : 0); + } + absenceCardList.add(absenceCardRecord); + } + if (CollUtil.isNotEmpty(absenceCardList)) { + absenceCardList = absenceCardList.stream().sorted(Comparator.comparing(AbsenceCardRecord::getAbsenceCardDate) + .thenComparing(AbsenceCardRecord::getPointOfTime)).collect(Collectors.toList()); + } + absenceCardMap.put(userAndGroupKey, absenceCardList); + }); + return absenceCardMap; + } + + @Override + public Map> getAbsenceMap(List groupIds, + Map sealMap, List userIds, DateRangeDto dateRangeDto) { + Map> absenceMap = new HashMap<>(); + List userClockInResultList = this.statisticsMapper.getUserClockInResultList( + List.of(AttendanceTypeEnum.ORDINARY.getCode()), userIds, + dateRangeDto, groupIds, null, 1, null, null); + if (CollUtil.isEmpty(userClockInResultList)) { + return absenceMap; + } + //查询考勤组配置信息 + Map baseSettingMap = baseSettingService.getEnableBaseSetting(userClockInResultList + .stream().map(ClockInResultRecord::getGroupId).distinct().collect(Collectors.toList())); + //按照用户+&考勤组分组获取数据 + Map> userClockMap = userClockInResultList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, list) -> { + String userId = userAndGroupKey.split("&")[0]; + String groupId = userAndGroupKey.split("&")[1]; + // 获取补卡次数 + AttendanceRepair repairByDate = attendanceRepairService.getAttendanceRepairByDate(DateUtil.daFormat(new Date()), userId, groupId); + int repairNum = repairByDate != null ? repairByDate.getRepairNum() : 0; + // 获取审批中的补卡次数 + int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(userId, groupId, dateRangeDto.getStartDate(), dateRangeDto.getEndDate()); + // 是否允许补卡 + boolean isApply = (repairNum - applyCount) > 0; + List absenceRecordList = CollUtil.newArrayList(); + for (ClockInResultRecord inResultRecord : list) { + AbsenceRecord absenceRecord = new AbsenceRecord(); + BeanUtils.copyProperties(inResultRecord, absenceRecord); + absenceRecord.setAbsenceDate(inResultRecord.getDay()); + absenceRecord.setStartTime(inResultRecord.getInPoint()); + absenceRecord.setEndTime(inResultRecord.getOutPoint()); + absenceRecord.setIsAdminUpdate(StringUtil.isNotEmpty(inResultRecord.getAbsenceLeader()) ? 1 : 0); + absenceRecord.setSeal(sealMap.getOrDefault(userId, false) ? 1 : 0); + if (baseSettingMap.containsKey(inResultRecord.getGroupId())) { + AttendanceBaseSetting baseSetting = baseSettingMap.get(inResultRecord.getGroupId()); + absenceRecord.setIsOperate(Objects.isNull(baseSetting.getPatchClockStatus()) || baseSetting.getPatchClockStatus().equals(0) ? 0 : + Objects.nonNull(baseSetting.getPatchType()) && baseSetting.getPatchType().contains("4") && isApply ? 1 : 0); + } + absenceRecordList.add(absenceRecord); + } + if (CollUtil.isNotEmpty(absenceRecordList)) { + absenceRecordList = absenceRecordList.stream().sorted(Comparator.comparing(AbsenceRecord::getAbsenceDate) + .thenComparing(AbsenceRecord::getStartTime).thenComparing(AbsenceRecord::getClockInType)).collect(Collectors.toList()); + } + absenceMap.put(userAndGroupKey, absenceRecordList); + }); + return absenceMap; + } + + @Override + public Map> getMakeUpCardMap(List groupIds, + List userIds, DateRangeDto dateRangeDto) { + Map> makeUpCardMap = new HashMap<>(); + List userClockInResultList = this.statisticsMapper.getUserClockInResultList( + List.of(AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()), userIds, + dateRangeDto, groupIds, null, null, 1, null); + if (CollUtil.isEmpty(userClockInResultList)) { + return makeUpCardMap; + } + //按照用户+&考勤组分组获取数据 + Map> userClockMap = userClockInResultList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, list) -> { + List makeUpCardRecordList = CollUtil.newArrayList(); + for (ClockInResultRecord inResultRecord : list) { + MakeUpCardRecord makeUpCardRecord = new MakeUpCardRecord(); + BeanUtils.copyProperties(inResultRecord, makeUpCardRecord); + makeUpCardRecord.setMakeUpCardDate(inResultRecord.getDay()); + makeUpCardRecord.setMakeUpCardTime(inResultRecord.getEffectiveTime()); + makeUpCardRecordList.add(makeUpCardRecord); + } + if (CollUtil.isNotEmpty(makeUpCardRecordList)) { + makeUpCardRecordList = makeUpCardRecordList.stream().sorted(Comparator.comparing(MakeUpCardRecord::getMakeUpCardDate) + .thenComparing(MakeUpCardRecord::getMakeUpCardTime)).collect(Collectors.toList()); + } + makeUpCardMap.put(userAndGroupKey, makeUpCardRecordList); + }); + return makeUpCardMap; + } + + @Override + public Map> getOutworkRecordList(List groupIds, + List userIds, DateRangeDto dateRangeDto) { + Map> outworkMap = new HashMap<>(); + List userClockInResultList = this.statisticsMapper.getUserClockInResultList( + List.of(AttendanceTypeEnum.ORDINARY.getCode(), AttendanceTypeEnum.WORKOVERTIME.getCode()), userIds, + dateRangeDto, groupIds, null, null, null, 2); + if (CollUtil.isEmpty(userClockInResultList)) { + return outworkMap; + } + //按照用户+&考勤组分组获取数据 + Map> userClockMap = userClockInResultList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, list) -> { + List outworkRecordList = CollUtil.newArrayList(); + for (ClockInResultRecord inResultRecord : list) { + OutworkRecord outworkRecord = new OutworkRecord(); + BeanUtils.copyProperties(inResultRecord, outworkRecord); + outworkRecord.setOutworkDate(inResultRecord.getDay()); + outworkRecord.setOutworkTime(inResultRecord.getClockTime()); + outworkRecordList.add(outworkRecord); + } + if (CollUtil.isNotEmpty(outworkRecordList)) { + outworkRecordList = outworkRecordList.stream().sorted(Comparator.comparing(OutworkRecord::getOutworkDate) + .thenComparing(OutworkRecord::getOutworkTime)).collect(Collectors.toList()); + } + outworkMap.put(userAndGroupKey, outworkRecordList); + }); + return outworkMap; + } + + @Override + public Map> getOvertimeRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto) { + Map> overtimeMap = new HashMap<>(); + List userOvertimeRecordList = this.statisticsMapper.getUserRuleRecordList(userIds, + dateRangeDto, groupIds, List.of(AttendanceTypeEnum.WORKOVERTIME.getCode())); + if (CollUtil.isEmpty(userOvertimeRecordList)) { + return overtimeMap; + } + // 排除掉加班未结束的班次 + userOvertimeRecordList.removeIf(record -> CollUtil.isEmpty(record.getClockInResultList()) || record.getClockInResultList().size() != 2); + userOvertimeRecordList.forEach(record -> { + if (record.getClockInResultList() != null) { + record.getClockInResultList().removeIf(result -> + Objects.equals(result.getDeleteMark(), 1) || StringUtil.isEmpty(result.getId())); + } + }); + // 按照用户分组获取数据 + Map> userClockMap = userOvertimeRecordList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userClockMap.forEach((userAndGroupKey, userClockList) -> { + List overtimeRecordList = CollUtil.newArrayList(); + for (UserRuleRecord userRuleRecord : userClockList) { + OvertimeRecord overtimeRecord = new OvertimeRecord(); + List clockInResults = userRuleRecord.getClockInResultList(); + if (CollUtil.isNotEmpty(clockInResults)) { + DayStatisticsUtils.processClockInResults(clockInResults, userRuleRecord, overtimeRecord); + } else { + DayStatisticsUtils.handleEmptyClockInResults(userRuleRecord, overtimeRecord); + } + overtimeRecordList.add(overtimeRecord); + } + if (CollUtil.isNotEmpty(overtimeRecordList)) { + overtimeRecordList = overtimeRecordList.stream() + .sorted(Comparator.comparing(OvertimeRecord::getOvertimeDate) + .thenComparing(OvertimeRecord::getStartTime)) + .collect(Collectors.toList()); + } + overtimeMap.put(userAndGroupKey, overtimeRecordList); + }); + return overtimeMap; + } + + @SneakyThrows + @Override + public Map> getSecondRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto) { + Map> secondMap = new HashMap<>(); + //被借调记录 + List userSecondRecordList = statisticsMapper.getUserSecondRecordList(userIds, groupIds); + if (CollUtil.isEmpty(userSecondRecordList)) { + return secondMap; + } + //按照用户+&考勤组分组获取数据 + Map> userMap = userSecondRecordList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + Date queryStartDate = strToDate(dateRangeDto.getStartDate()); + Date queryEndDate = strToDate(dateRangeDto.getEndDate()); + for (Map.Entry> entry : userMap.entrySet()) { + List secondRecordList = new ArrayList<>(); + //处理借调记录(json对象) + List timeAllList = new ArrayList<>(); + handleDateJsonObject(dateRangeDto, timeAllList, entry.getValue()); + if (CollUtil.isEmpty(timeAllList)) { + continue; + } + for (TimeJson item : timeAllList) { + LocalDateTime startDate = LocalDateTime.parse(dateFormat(item.getStartTime()), formatter); + LocalDateTime endDate = LocalDateTime.parse(dateFormat(item.getEndTime()), formatter); + SecondRecord secondRecord = new SecondRecord(); + if (isCrossMonth(startDate, endDate)) { + Date start = queryStartDate.compareTo(item.getStartTime()) <= 0 ? + item.getStartTime() : queryStartDate; + Date end = queryEndDate.compareTo(item.getEndTime()) <= 0 ? + cn.hutool.core.date.DateUtil.endOfDay(item.getEndTime()) : queryEndDate; + secondRecord.setSecondDate(start); + secondRecord.setStartTime(start); + secondRecord.setEndTime(end); + secondRecord.setSecondDuration(DateConvertUtil.dateConvert(start, end, 3, 2, new BigDecimal(24))); + } else { + secondRecord.setSecondDate(item.getStartTime()); + secondRecord.setStartTime(item.getStartTime()); + secondRecord.setEndTime(item.getEndTime()); + secondRecord.setSecondDuration(DateConvertUtil.dateConvert(secondRecord.getStartTime(), + secondRecord.getEndTime(), 3, 2, new BigDecimal(24))); + } + secondRecordList.add(secondRecord); + } + if (CollUtil.isNotEmpty(secondRecordList)) { + secondMap.put(entry.getKey(), secondRecordList); + } + } + return secondMap; + } + + + @Override + public Map> getBusRecordList(List groupIds, List userIds, DateRangeDto dateRangeDto) { + Map> busMap = new HashMap<>(); + //获取日统计数据 + List dayStatisticsList = attendanceDayStatisticsService.lambdaQuery() + .in(AttendanceDayStatistics::getUserId, userIds) + .in(CollUtil.isNotEmpty(groupIds), AttendanceDayStatistics::getGroupId, groupIds) + .ge(AttendanceDayStatistics::getDate, DateUtil.strToDate(dateRangeDto.getStartDate())) + .le(AttendanceDayStatistics::getDate, DateUtil.strToDate(dateRangeDto.getEndDate())) + .isNotNull(AttendanceDayStatistics::getBusBatchNumber) + .list(); + if (CollUtil.isEmpty(dayStatisticsList)) { + return busMap; + } + List applyList = new ArrayList<>(); + dayStatisticsList.forEach(item -> applyList.addAll(List.of(item.getBusBatchNumber().split(",")))); + List busApproveMap = businessTripApproveService.getBatchByIds(groupIds, applyList); + if (CollUtil.isEmpty(busApproveMap)) { + return busMap; + } + //按照用户+&考勤组分组 + Map> userMap = busApproveMap.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + userMap.forEach((userAndGroupKey, approveList) -> { + List busRecordList = new ArrayList<>(); + for (AttendanceBusinessTripApprove approve : approveList) { + BusOrOutRecord busOrOutRecord = new BusOrOutRecord(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime startDate = LocalDateTime.parse(dateFormat(approve.getStartTime()), formatter); + LocalDateTime endDate = LocalDateTime.parse(dateFormat(approve.getEndTime()), formatter); + if (isCrossMonth(startDate, endDate)) { + DateRangeDto1 dateRange = getIntersection(startDate, endDate, + LocalDateTime.parse(dateRangeDto.getStartDate() + " 00:00:00", formatter), + LocalDateTime.parse(dateRangeDto.getEndDate() + " 23:59:59", formatter)); + if (Objects.nonNull(dateRange)) { + busOrOutRecord.setStartTime(Date.from(dateRange.getStartDate().atZone(ZoneId.systemDefault()).toInstant())); + busOrOutRecord.setEndTime(Date.from(dateRange.getEndDate().atZone(ZoneId.systemDefault()).toInstant())); + busOrOutRecord.setBusOrOutDays(DateConvertUtil.dateConvert(busOrOutRecord.getStartTime(), + busOrOutRecord.getEndTime(), 3, 2, new BigDecimal(24))); + } + } else { + busOrOutRecord.setStartTime(approve.getStartTime()); + busOrOutRecord.setEndTime(approve.getEndTime()); + busOrOutRecord.setBusOrOutDays(approve.getDayNum()); + } + busRecordList.add(busOrOutRecord); + } + if (CollUtil.isNotEmpty(busRecordList)) { + busRecordList = busRecordList.stream().sorted(Comparator.comparing(BusOrOutRecord::getStartTime)).collect(Collectors.toList()); + } + busMap.put(userAndGroupKey, busRecordList); + }); + return busMap; + } + + @Override + public Map> getOutRecordList(Map ratioMap, List userIds, DateRangeDto dateRangeDto) { + Map> outMap = new HashMap<>(); + //获取日统计数据 + ArrayList groupIds = new ArrayList<>(ratioMap.keySet()); + List dayStatisticsList = attendanceDayStatisticsService.lambdaQuery() + .in(AttendanceDayStatistics::getUserId, userIds) + .in(AttendanceDayStatistics::getGroupId, groupIds) + .ge(AttendanceDayStatistics::getDate, DateUtil.strToDate(dateRangeDto.getStartDate())) + .le(AttendanceDayStatistics::getDate, DateUtil.strToDate(dateRangeDto.getEndDate())) + .isNotNull(AttendanceDayStatistics::getOutBatchNumber) + .list(); + if (CollUtil.isEmpty(dayStatisticsList)) { + return outMap; + } + List applyList = new ArrayList<>(); + List shiftsJsonVoList = dayStatisticsList.stream() + .map(AttendanceDayStatistics::getOutBatchNumber) + .filter(item -> Objects.nonNull(item) && item.startsWith("[") && item.endsWith("]")) + .flatMap(item -> JSONArray.parseArray(item, BatchNumberResult.class).stream()) + .collect(Collectors.toList()); + Map> applyMap = new HashMap<>(); + if (CollUtil.isNotEmpty(shiftsJsonVoList)) { + shiftsJsonVoList.removeIf(item -> Objects.isNull(item.getDay())); + applyMap = shiftsJsonVoList.stream().collect(Collectors.groupingBy(BatchNumberResult::getBatchNumberId, + Collectors.toMap(BatchNumberResult::getDay, BatchNumberResult::getDays, BigDecimal::add))); + } + dayStatisticsList.forEach(item -> applyList.addAll(parseBatchNumber(item.getOutBatchNumber()))); + List outList = goOutApproveService.getBatchByIds(groupIds, applyList); + if (CollUtil.isEmpty(outList)) { + return outMap; + } + //按照用户+&考勤组分组 + Map> outApproveMap = outList.stream() + .collect(Collectors.groupingBy(approve -> approve.getUserId() + "&" + approve.getGroupId())); + Map> finalApplyMap = applyMap; + outApproveMap.forEach((userAndGroupKey, outApproveList) -> { + List outRecordList = CollUtil.newArrayList(); + for (AttendanceGoOutApprove approve : outApproveList) { + BusOrOutRecord busOrOutRecord = new BusOrOutRecord(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime startDate = LocalDateTime.parse(dateFormat(approve.getStartTime()), formatter); + LocalDateTime endDate = LocalDateTime.parse(dateFormat(approve.getEndTime()), formatter); + String groupId = approve.getGroupId(); + if (isCrossMonth(startDate, endDate)) { + DateRangeDto1 dateRange = getIntersection(startDate, endDate, + LocalDateTime.parse(dateRangeDto.getStartDate() + " 00:00:00", formatter), + LocalDateTime.parse(dateRangeDto.getEndDate() + " 23:59:59", formatter)); + if (Objects.nonNull(dateRange)) { + busOrOutRecord.setStartTime(Date.from(dateRange.getStartDate().atZone(ZoneId.systemDefault()).toInstant())); + busOrOutRecord.setEndTime(Date.from(dateRange.getEndDate().atZone(ZoneId.systemDefault()).toInstant())); + } + } else { + busOrOutRecord.setStartTime(approve.getStartTime()); + busOrOutRecord.setEndTime(approve.getEndTime()); + } + // 日统计是否计算外出天数 + if (finalApplyMap.containsKey(approve.getId())) { + Map dateBigDecimalMap = finalApplyMap.get(approve.getId()); + // 获取实际时长 要在busOrOutRecord的范围内的数据汇总 + busOrOutRecord.setBusOrOutDays(dateBigDecimalMap.entrySet().stream() + .filter(entry -> entry.getKey().compareTo(cn.hutool.core.date.DateUtil.beginOfDay(busOrOutRecord.getStartTime())) >= 0 && + entry.getKey().compareTo(cn.hutool.core.date.DateUtil.beginOfDay(busOrOutRecord.getEndTime())) <= 0) + .map(Map.Entry::getValue) + .reduce(BigDecimal::add) + .orElse(BigDecimal.ZERO)); + outRecordList.add(busOrOutRecord); + continue; + } + BigDecimal bigDecimal = DateConvertUtil.dateConvert(busOrOutRecord.getStartTime(), busOrOutRecord.getEndTime(), 3, 2, ratioMap.get(groupId)); + if (approve.getUnit().equals(LeaveUnitEnum.DAY.getCode())) { + busOrOutRecord.setBusOrOutDays(bigDecimal); + } else if (approve.getUnit().equals(LeaveUnitEnum.HOUR.getCode())) { + busOrOutRecord.setBusOrOutDays(bigDecimal.divide(ratioMap.get(groupId), 2, RoundingMode.HALF_UP)); + } + outRecordList.add(busOrOutRecord); + } + if (CollUtil.isNotEmpty(outRecordList)) { + outRecordList = outRecordList.stream().sorted(Comparator.comparing(BusOrOutRecord::getStartTime)).collect(Collectors.toList()); + } + outMap.put(userAndGroupKey, outRecordList); + }); + return outMap; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserConfigServiceImpl.java new file mode 100644 index 0000000..ab6da5f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserConfigServiceImpl.java @@ -0,0 +1,45 @@ +package jnpf.attendance.service.impl; + +import jnpf.attendance.mapper.AttendanceUserConfigMapper; +import jnpf.attendance.service.UserConfigService; +import jnpf.model.attendance.vo.attendance.UserConfigVo; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * @Author huanglinpan + * @Date 2024/6/25 9:31 + * @Version 1.0 (版本号) + */ +@Service("attendanceUserConfigService") +public class UserConfigServiceImpl implements UserConfigService { + + @Resource + private UserProvider userProvider; + + @Resource + private AttendanceUserConfigMapper userConfigMapper; + + + @Override + public UserConfigVo getUserConfig() { + return userConfigMapper.getUserConfig(userProvider.get().getUserId()); + } + + @Override + public void updateUserConfig(UserConfigVo userConfigVo) { + String userId = userProvider.get().getUserId(); + UserConfigVo userConfig = userConfigMapper.getUserConfig(userId); + if (null != userConfig && null != userConfig.getConfigJson()){ + //修改用户配置 + userConfigMapper.updateUSerConfig(userConfigVo.getConfigJson(),userId); + }else{ + userConfigVo.setId(FtbUtil.getId()); + //没有配置 新增保存用户配置 + userConfigMapper.addUserConfig(userId,userConfigVo); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceServiceImpl.java new file mode 100644 index 0000000..5899dd2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceServiceImpl.java @@ -0,0 +1,643 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.xuyanwu.spring.file.storage.FileInfo; +import cn.xuyanwu.spring.file.storage.MockMultipartFile; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.fantaibao.permission.handling.PermissionHandling; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.attendance.mapper.AttendanceUserFaceMapper; +import jnpf.attendance.mapper.FtbAttendanceFaceChangeLogMapper; +import jnpf.attendance.service.AttenceMachineService; +import jnpf.attendance.service.AttendanceUserFaceService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.UserFaceService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.constant.FileTypeConstant; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.attendance.AttendanceMachineManage; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.entity.attendance.FtbAttendanceFaceChangeLog; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.HandleException; +import jnpf.file.FileApi; +import jnpf.file.FileUploadApi; +import jnpf.model.attendance.dto.FaceChangeQueryDto; +import jnpf.model.attendance.dto.FaceQueryDto; +import jnpf.model.attendance.dto.MachineDealDto; +import jnpf.model.attendance.dto.UserDto; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.GroupMiniVo; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.BaseUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import jnpf.util.attendance.RuleScopeUtil; +import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 人脸识别服务实现 + * + * @author yanwenfu + * @create 2025-04-08 + */ +@Slf4j +@Service +public class UserFaceServiceImpl implements UserFaceService { + + @Resource + private AttendanceUserFaceService attendanceUserFaceService; + @Resource + private AttendanceUserFaceMapper attendanceUserFaceMapper; + @Resource + private FtbAttendanceFaceChangeLogMapper ftbAttendanceFaceChangeLogMapper; + @Resource + private AttendanceUserService attendanceUserService; + @Resource + private AttendanceGroupMapper attendanceGroupMapper; + @Autowired + private UserApi userApi; + @Resource + private V2UserApi v2UserApi; + @Resource + private FileUploadApi fileUploadApi; + @Resource + private FileApi fileApi; + @Autowired + private SqlSessionFactory sqlSessionFactory; + @Autowired + private RedisUtil redisUtil; + @Autowired + private PermissionHandling permissionHandling; + @Autowired + private RuleScopeUtil ruleScopeUtil; + @Autowired + private AttenceMachineService attenceMachineService; + + @Override + public Boolean getPhotoCheck(MultipartFile file, UserInfo userInfo) throws HandleException { + + /* + // 查询用户数据库人脸信息 + UserFaceVo userFace = attendanceUserFaceService.getUserFace(userInfo.getUserId()); + if (null == userFace) { + throw new HandleException("请先上传人脸"); + } + // 获取特征 + float[] image1; + if (StringUtils.isNotEmpty(userFace.getFaceFeature())) { + JSONArray array = JSONUtil.parseArray(userFace.getFaceFeature()); + image1 = new float[array.size()]; + for (int i = 0; i < array.size(); i++) { + image1[i] = NumberUtils.safeToFloat(array.get(i)); + } + } else { + image1 = SeetaFaceUtil.analyzeEcognizer(null, userFace.getFaceData()); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .eq(AttendanceUserFace::getUserId, userFace.getUserId()) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(AttendanceUserFace::getFaceFeature, JSONUtil.toJsonStr(image1)); + attendanceUserFaceMapper.update(null, updateWrapper); + } + float[] image2 = SeetaFaceUtil.analyzeEcognizer(file, null); + // 开始比对 + return SeetaFaceUtil.compareEigenvalue(image1, image2); + */ + return Boolean.TRUE; + } + + @Override + public Integer sendToMachine(String userId) throws Exception { + + log.error("数据库执行完成 ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + // 下发设备 + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_ADD); + List machineList = attendanceUserFaceService.machineDeal(machineDealDto); + // 等待考勤机回复等待结果 + return syncMachineDealResult(machineList, userId); + } + + @Transactional(rollbackFor = Exception.class) + public void updateMachineSyncStatus(String userId, Integer result) { + log.info("考勤机同步完成 ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + LambdaUpdateWrapper updateFace = new LambdaUpdateWrapper() + .eq(AttendanceUserFace::getUserId, userId) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(AttendanceUserFace::getSyncStatus, result); + attendanceUserFaceMapper.update(null, updateFace); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveUserFace(UserDto userDto, UserInfo userInfo, String faceUrl, String thumbnailUrl) { + + LambdaUpdateWrapper updateWrapper = null; + String faceId; + UserFaceVo userFace = attendanceUserFaceService.getUserFace(userDto.getUserId()); + if (userFace != null) { + updateWrapper = new LambdaUpdateWrapper() + .eq(AttendanceUserFace::getUserId, userDto.getUserId()) + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(AttendanceUserFace::getLastModifyUserId, userInfo.getUserId()) + .set(AttendanceUserFace::getLastModifyTime, DateUtil.getNowDate()); + } + if (updateWrapper != null) { + faceId = userFace.getId(); + updateWrapper + .set(AttendanceUserFace::getFaceData, faceUrl) + .set(AttendanceUserFace::getFaceDataThumbnail, thumbnailUrl); + attendanceUserFaceMapper.update(null, updateWrapper); + } else { + AttendanceUserFace face = new AttendanceUserFace(); + face.setId(FtbUtil.getId()); + face.setUserId(userDto.getUserId()); + face.setFaceData(faceUrl); + face.setFaceDataThumbnail(thumbnailUrl); + face.setCreatorUserId(userInfo.getUserId()); + face.setLastModifyUserId(userInfo.getUserId()); + face.setLastModifyTime(DateUtil.getNowDate()); + faceId = face.getId(); + attendanceUserFaceMapper.insert(face); + } + // 变动记录 + BaseUserInfoVo user = userApi.getUserInfoById(userDto.getUserId()); + FtbAttendanceFaceChangeLog changeLog = new FtbAttendanceFaceChangeLog( + userDto.getUserId(), + faceId, + "[" + user.getUserName() + "]已录入人脸", + userInfo.getUserId() + ); + ftbAttendanceFaceChangeLogMapper.insert(changeLog); + } + + private String uploadFile(File file, String suffix) throws IOException { + + FileInputStream input = new FileInputStream(file); + String uuid = UUID.randomUUID().toString(); + MultipartFile multiFile = new MockMultipartFile(uuid + TemplateExcelUtils.SUFFIX, file.getName(), MediaType.MULTIPART_FORM_DATA_VALUE, input); + input.close(); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multiFile, fileApi.getPath(FileTypeConstant.TEMPORARY), uuid + suffix); + return fileInfo.getUrl().replace(ConstantUtil.EXTRA_PATH, ""); + } + + private Integer syncMachineDealResult(List machineList, String userId) throws Exception { + + log.error("等待考勤机回复:{}, 考勤机列表:{}", userId, machineList); + int returnResult = ConstantUtil.NUM_FALSE; + if (machineList.isEmpty()) { + return returnResult; + } + List waitMachines = new ArrayList<>(machineList); + int index = 0; + while (index < 15) { + // 警告!!!警告!!!产品要求 在同步中获取异步产生的结果 最多等待30秒 + Thread.sleep(2000L); + Iterator it = waitMachines.iterator(); + while (it.hasNext()) { + String machine = it.next(); + Object strObj = redisUtil.getString(String.format(ConstantUtil.MACHINE_MESSAGE, machine) + userId); + if (null != strObj) { + JSONObject json = JSONUtil.parseObj(strObj); + int result = Integer.parseInt(json.get(machine).toString()); + if (result == ConstantUtil.NUM_TRUE) { + it.remove(); + redisUtil.remove(String.format(ConstantUtil.MACHINE_MESSAGE, machine) + userId); + } + } + } + if (waitMachines.isEmpty()) { + returnResult = ConstantUtil.NUM_TRUE; + break; + } + index += 1; + } + log.error("返回结果:{}", returnResult); + return returnResult; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteUserFace(String userId, UserInfo userInfo) throws Exception { + + UserFaceVo userFace = attendanceUserFaceService.getUserFace(userId); + if (null == userFace) { + throw new HandleException("未找到您的人脸数据"); + } + attendanceUserFaceService.deleteUserFace(userId); + BaseUserInfoVo user = userApi.getUserInfoById(userId); + String msg; + if (userId.equals(userInfo.getUserId())) { + msg = "[" + user.getUserName() + "]已清空人脸信息"; + } else { + msg = "管理员[" + userInfo.getUserName() + "]已清空[" + user.getUserName() + "]人脸信息"; + } + FtbAttendanceFaceChangeLog log = new FtbAttendanceFaceChangeLog(userId, msg, userInfo.getUserId()); + ftbAttendanceFaceChangeLogMapper.insert(log); + // 删除考勤机数据 + MachineDealDto machineDealDto = new MachineDealDto(userId, ConstantUtil.CAL_DELETE); + attendanceUserFaceService.machineDeal(machineDealDto); + } + + @Override + public UserFaceVo getUserFace(String userId) { + + return attendanceUserFaceService.getUserFace(userId); + } + + @Override + public PageInfo getChangeLogPage(FaceChangeQueryDto queryDto) { + + List userIds = null; + String tenantId = UserProvider.getUser().getTenantId(); + if (StringUtils.isNotEmpty(queryDto.getUserName())) { + List list = v2UserApi.userListAndCopyLikeName(queryDto.getUserName(), null, tenantId); + userIds = new ArrayList<>(); + if (null != list && !list.isEmpty()) { + if (StringUtils.isNotEmpty(queryDto.getOrgId())) { + userIds.addAll(list.stream().filter(v -> v.getOrganizeId().equals(queryDto.getOrgId())).map(UserBoundVO::getId).collect(Collectors.toList())); + } else { + userIds.addAll(list.stream().map(UserBoundVO::getId).collect(Collectors.toList())); + } + } + } else { + if (StringUtils.isNotEmpty(queryDto.getOrgId())) { + userIds = new ArrayList<>(); + ActionResult> actionResult = v2UserApi.listTargetOrganizesOrHaveChild(List.of(queryDto.getOrgId()), false, List.of(UserWorkStatusEnums.RESIGNED), tenantId); + if (null != actionResult && actionResult.getCode() == 200 && null != actionResult.getData() && !actionResult.getData().isEmpty()) { + List list = actionResult.getData(); + List collect = list.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + userIds.addAll(collect); + } + } + } + if (null != userIds && userIds.isEmpty()) { + return new PageInfo<>(); + } + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(ftbAttendanceFaceChangeLogMapper.getChangeLogList(queryDto.getStartDate(), queryDto.getEndDate(), userIds)); + if (page.getList().isEmpty()) { + return page; + } + List collect = page.getList().stream().map(ChangeLogVo::getCreatorUserId).collect(Collectors.toList()); + List list = v2UserApi.userListAndCopy(collect, false, UserProvider.getUser().getTenantId()); + if (null != list && !list.isEmpty()) { + Map userMap = list.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + page.getList().forEach(v -> { + UserBoundVO user = userMap.get(v.getCreatorUserId()); + if (null != user) { + v.setCreatorUserName(user.getUserName()); + v.setPostName(user.getPositionName()); + } + }); + } + return page; + } + + @Override + public PageInfo getUserFacePage(FaceQueryDto queryDto) { + + // 查询考勤组下的成员 + String[] split = StringUtil.isEmpty(queryDto.getGroupId()) ? null : queryDto.getGroupId().split(","); + List groupIdlist = null == split ? new ArrayList<>() : Arrays.stream(split).collect(Collectors.toList()); + // 需要查询人脸的用户信息 + List userList = new ArrayList<>(); + Map userMap = null; + Map groupMap; + // 根据权限过滤用户 + UserInfo loginUser = UserProvider.getUser(); + List filterUserList = new ArrayList<>(); + if (!loginUser.getIsAdministrator()) { + List collectUserList = permissionHandling.getUserIdsByUserId(loginUser.getUserId()); + // collectUserList 为 null 等同于 超级管理员 不进行过滤 + if (null != collectUserList) { + if (collectUserList.isEmpty()) { + // 可查看的人为empty + return new PageInfo<>(); + } + filterUserList.addAll(collectUserList); + } + } + if (groupIdlist.contains("-1")) { + groupIdlist.remove("-1"); + // 查询没有考勤组的用户 + ActionResult> actionResult = v2UserApi.getAllUserInfoBatch(List.of(), UserProvider.getUser().getTenantId()); + List userBoundList = actionResult.getData(); + userMap = userBoundList.stream().filter(v -> !v.getWorkStatusEnums().equals(UserWorkStatusEnums.RESIGNED)).collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 查询所有考勤组用户 + List allGroupUserList = attendanceUserService.queryAllForNotSecondment(); + if (null != allGroupUserList && !allGroupUserList.isEmpty()) { + List allUser = new ArrayList<>(userMap.keySet()); + List collect = allGroupUserList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (!filterUserList.isEmpty()) { + allUser.removeIf(v -> !filterUserList.contains(v)); + } + allUser.removeAll(collect); + allUser.forEach(userId -> userList.add(new AttendanceGroupUserVo(userId))); + } + } + Date today = new Date(); + List queryList = new ArrayList<>(); + if (!groupIdlist.isEmpty()) { + groupIdlist = groupIdlist.stream().distinct().collect(Collectors.toList()); + List groupUserList = attendanceUserService.queryByUsersAndGroupFilterSecondment(today, today, null, groupIdlist, Boolean.FALSE); + if (null != groupUserList && !groupUserList.isEmpty()) { + queryList.addAll(groupUserList); + } + } + // 有班组, 进行班组人员过滤 + if (StringUtils.isNotEmpty(queryDto.getTeamId())) { + String[] teamArray = queryDto.getTeamId().split(","); + ActionResult> actionResult = v2UserApi.listTargetOrganizesOrHaveChild(List.of(teamArray), false, List.of(UserWorkStatusEnums.RESIGNED), loginUser.getTenantId()); + if (null != actionResult && 200 == actionResult.getCode()) { + List orgIds = actionResult.getData().stream().map(UserBoundVO::getOrganizeId).distinct().collect(Collectors.toList()); + // 根据组织id查询考勤组 + List orgGroupList = attendanceGroupMapper.getGroupListByOrgId(null, orgIds); + List orgGroupIds = orgGroupList.stream().map(AttendanceGroupVo::getId).collect(Collectors.toList()); + List teamUserList = actionResult.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + Set collect = queryList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toSet()); + List queryUsers = new ArrayList<>(); + teamUserList.forEach(teamUser -> { + if (collect.add(teamUser)) { + queryUsers.add(teamUser); + } + }); + if (!queryUsers.isEmpty()) { + List groupUserList = attendanceUserService.queryByUsersAndGroupFilterSecondment(today, today, queryUsers, orgGroupIds, Boolean.FALSE); + queryList.addAll(groupUserList); + } + } + } + // 过滤没有权限的用户 + if (!filterUserList.isEmpty()) { + queryList.removeIf(v -> !filterUserList.contains(v.getUserId())); + } + if (queryList.isEmpty()) { + return new PageInfo<>(); + } + List gUserList = BeanUtil.copyToList(queryList, AttendanceGroupUserVo.class); + Set groupIds = gUserList.stream().map(AttendanceGroupUserVo::getGroupId).collect(Collectors.toSet()); + List groupList = attendanceGroupMapper.getAllGroupByLevel(new ArrayList<>(groupIds)); + groupMap = groupList.stream().collect(Collectors.toMap(GroupMiniVo::getGroupId, Function.identity())); + List userIds = queryList.stream().map(AttendanceGroupUser::getUserId).collect(Collectors.toList()); + if (null == userMap) { + ActionResult> actionResult = v2UserApi.getAllUserInfoBatch(userIds, UserProvider.getUser().getTenantId()); + List userBoundList = actionResult.getData(); + userMap = userBoundList.stream().filter(v -> !v.getWorkStatusEnums().equals(UserWorkStatusEnums.RESIGNED)).collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + List filterUser = new ArrayList<>(userMap.keySet()); + gUserList.removeIf(v -> !filterUser.contains(v.getUserId())); + userList.addAll(gUserList); + // 用户是否绑定了考勤机 + List queryUserIds = userList.stream() + .map(AttendanceGroupUserVo::getUserId) + .collect(Collectors.toList()); + List scopeList = ruleScopeUtil.selectUserEffectListBatch( + queryUserIds, + ScopeBizType.ATTENDANCE_MACHINE, + ConstantUtil.NUM_FALSE + ); + // 提前准备 Map 避免多次查询 + Map> scopeMap = scopeList.isEmpty() + ? Collections.emptyMap() + : scopeList.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getUserId)); + Map machineMap = Collections.emptyMap(); + if (!scopeList.isEmpty()) { + List ruleIds = scopeList.stream() + .map(AttendanceRuleScope::getRuleId) + .distinct() + .collect(Collectors.toList()); + List machineList = attenceMachineService.list(new LambdaQueryWrapper() + .in(AttendanceMachineManage::getId, ruleIds) + .eq(AttendanceMachineManage::getDeleteMark, ConstantUtil.NUM_FALSE)); + if (!machineList.isEmpty()) { + machineMap = machineList.stream().collect(Collectors.toMap( + AttendanceMachineManage::getId, + Function.identity() + )); + } + } + // 遍历用户,填充属性 + for (AttendanceGroupUserVo v : userList) { + String userId = v.getUserId(); + // 绑定考勤机逻辑 + List userScopes = scopeMap.getOrDefault(userId, Collections.emptyList()); + boolean hasMachine = userScopes.stream() + .map(AttendanceRuleScope::getRuleId) + .distinct() + .anyMatch(machineMap::containsKey); + v.setBindingMachine(hasMachine); + // 设置组信息 + Optional.ofNullable(groupMap.get(v.getGroupId())) + .ifPresent(group -> v.setGroupName(group.getGroupName())); + // 设置用户信息 + Optional.ofNullable(userMap.get(userId)) + .ifPresent(userBound -> { + v.setRealName(userBound.getUserName()); + v.setOrgName(userBound.getOrganizeName()); + v.setPositionName(userBound.getPositionName()); + v.setWorkStatus(userBound.getWorkStatusName()); + }); + } + // 根据考勤组成员查询人脸信息 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + return new PageInfo<>(attendanceUserFaceMapper.getUserFaceList(queryDto, userList)); + } + + @Override + public Map> getUserFaceList(List userIds, String tenantId) { + List scopeList = ruleScopeUtil.selectUserEffectListBatch2( + userIds, + ScopeBizType.ATTENDANCE_MACHINE, + ConstantUtil.NUM_FALSE, + tenantId + ); + // 提前准备 Map 避免多次查询 + Map> scopeMap = scopeList.isEmpty() + ? Collections.emptyMap() + : scopeList.stream().collect(Collectors.groupingBy(AttendanceRuleScope::getUserId, Collectors.mapping(AttendanceRuleScope::getRuleId, Collectors.toList()))); + Map machineMap = Maps.newHashMap(); + if (!scopeList.isEmpty()) { + List ruleIds = scopeList.stream() + .map(AttendanceRuleScope::getRuleId) + .distinct() + .collect(Collectors.toList()); + List machineList = attenceMachineService.list(new LambdaQueryWrapper() + .in(AttendanceMachineManage::getId, ruleIds) + .eq(AttendanceMachineManage::getDeleteMark, ConstantUtil.NUM_FALSE)); + if (!machineList.isEmpty()) { + machineMap.putAll(machineList.stream().collect(Collectors.toMap( + AttendanceMachineManage::getId, + Function.identity() + ))); + } + } + Map> userMachineMap = Maps.newHashMap(); + scopeMap.forEach((userId, ruleIds) -> { + userMachineMap.put(userId, machineMap.values().stream().filter(v -> ruleIds.contains(v.getId())).collect(Collectors.toList())); + }); + return userMachineMap; + } + + @Override + public Boolean hasUserFace(String userId) { + + UserFaceVo userFace = getUserFace(userId); + return null != userFace; + } + + @Override + public FaceMiniVo getUserFaceInfo(String userId) { + + UserFaceVo userFace = getUserFace(userId); + if (null == userFace) { + return null; + } + return new FaceMiniVo(userFace.getUserId(), userFace.getFaceData(), DateDetail.getDate2Str(userFace.getLastModifyTime(), DateDetail.DF4)); + } + + @Override + public UserFaceVo getUserFaceDetail(String id) { + + AttendanceUserFace userFace = attendanceUserFaceMapper.selectById(id); + if (null == userFace) { + return null; + } + return JsonUtil.getJsonToBean(userFace, UserFaceVo.class); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void initializationFace(String tenantId) { + // 查询face表中有多少个没有缩略图的数据 + List userFaceDetailVos = attendanceUserFaceMapper.getNoThumbnail(); + if (null == userFaceDetailVos || userFaceDetailVos.isEmpty()) { + log.error(tenantId + "没有需要初始化的人脸数据"); +// return "没有需要初始化的人脸数据"; + } + int num = 0; + for (UserFaceDetailVo userFaceDetailVo : Objects.requireNonNull(userFaceDetailVos)) { + // 下载图片到临时文件 + File outputFile = null; + File thumbnailFile = null; + try { + // 截取链接的文件格式 + String suffix = userFaceDetailVo.getFaceData().substring(userFaceDetailVo.getFaceData().lastIndexOf(".")); + outputFile = downloadImageToTempFile(userFaceDetailVo.getFaceData(), suffix); +// thumbnailFile = downloadImageToTempFile(userFaceDetailVo.getFaceDataThumbnail(), suffix); + thumbnailFile = Files.createTempFile("thumbnail-", suffix).toFile(); + Thumbnails.of(outputFile) + .scale(0.5) + .outputQuality(0.5) + .toFile(thumbnailFile); + String thumbnailUrl = uploadFile(thumbnailFile, suffix); + userFaceDetailVo.setFaceDataThumbnail(thumbnailUrl); + num++; + } catch (IOException e) { + e.printStackTrace(); + } + } + SqlSession sqlSession = null; + try { + // 1. 获取批处理模式的 SqlSession + sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false); // 关闭自动提交 + // 将userFaceDetailVos 500个分一组 + for (int i = 0; i < userFaceDetailVos.size(); i++) { + UserFaceDetailVo userFaceDetailVo = userFaceDetailVos.get(i); + attendanceUserFaceMapper.updateUserFaceThumbnail(userFaceDetailVo); + // 每处理 500 条提交一次 + if (i % 500 == 0 && i > 0) { + sqlSession.commit(); + sqlSession.clearCache(); // 清理缓存避免内存占用 + } + } + // 4. 统一提交事务 + sqlSession.commit(); + + } catch (Exception e) { + if (sqlSession != null) { + sqlSession.rollback(); // 回滚事务 + } + throw new RuntimeException("Batch update failed", e); + } finally { + if (sqlSession != null) { + sqlSession.close(); // 关闭会话 + } + } + log.error(tenantId + "初始化成功,共初始化 " + num + " 个人脸缩略数据"); +// return "初始化成功,共初始化 " + num + " 个人脸缩略数据"; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public AttendanceUserFace updateUserFaceInfo(String userId, String updateUserId) { + + // 查询用户关联的人脸 + LambdaQueryWrapper faceWrapper = new LambdaQueryWrapper() + .eq(AttendanceUserFace::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(AttendanceUserFace::getUserId, userId); + AttendanceUserFace userFace = attendanceUserFaceMapper.selectOne(faceWrapper); + if (null == userFace) { + return null; + } + userFace.setLastModifyTime(DateUtil.getNowDate()); + userFace.setLastModifyUserId(updateUserId); + attendanceUserFaceMapper.updateById(userFace); + // 生成变动记录 + // FtbAttendanceFaceChangeLog log = new FtbAttendanceFaceChangeLog(user.getUserId(), userFace.getId(), user.getRealName() + "已录入人脸", updateUser.getUserId()); + // ftbAttendanceFaceChangeLogMapper.insert(log); + return userFace; + } + + /** + * 从 URL 下载图片到临时文件 + * @param imageUrl 图片的 URL 地址 + * @return 临时文件路径 (Path 对象) + * @throws IOException 如果下载或文件操作失败 + */ + public static File downloadImageToTempFile(String imageUrl, String suffix) throws IOException { + // 1. 创建临时文件(自动生成唯一文件名) + Path tempFile = Files.createTempFile("download-", suffix); + + // 2. 打开 URL 连接并获取输入流 + try (InputStream in = new URL(imageUrl).openStream()) { + // 3. 将输入流内容复制到临时文件 + Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); + } + + return tempFile.toFile(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceTxServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceTxServiceImpl.java new file mode 100644 index 0000000..2dbef21 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UserFaceTxServiceImpl.java @@ -0,0 +1,260 @@ +package jnpf.attendance.service.impl; + +import cn.xuyanwu.spring.file.storage.FileInfo; +import cn.xuyanwu.spring.file.storage.MockMultipartFile; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.service.UserFaceService; +import jnpf.attendance.service.UserFaceTxService; +import jnpf.base.UserInfo; +import jnpf.constant.FileTypeConstant; +import jnpf.entity.attendance.AttendanceUserFace; +import jnpf.file.FileApi; +import jnpf.file.FileUploadApi; +import jnpf.model.attendance.dto.MachineDealDto; +import jnpf.model.attendance.dto.UserDto; +import jnpf.util.*; +import jnpf.util.attendance.ImageCompressUtil; +import lombok.extern.slf4j.Slf4j; +import net.coobird.thumbnailator.Thumbnails; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * 人脸服务实现 + * + * @author yanwenfu + * @create 2025-12-11 + */ +@Slf4j +@Service +public class UserFaceTxServiceImpl implements UserFaceTxService { + + @Resource + private UserFaceService userFaceService; + @Resource + private FileUploadApi fileUploadApi; + @Resource + private FileApi fileApi; + + @Override + public Integer uploadUserFace(MultipartFile mFile, UserDto userDto, UserInfo userInfo) throws Exception { + + log.error("init ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + String suffix = Objects.requireNonNull(mFile.getOriginalFilename()) + .substring(mFile.getOriginalFilename().lastIndexOf(".")); + File sourceFile = new File( + System.getProperty("java.io.tmpdir"), + "src_" + System.nanoTime() + suffix + ); + String endWith = ".jpg"; + File firstFile = Files.createTempFile("source-", endWith).toFile(); + File outputFile = Files.createTempFile("face-", endWith).toFile(); + File thumbnailFile = Files.createTempFile("thumb-", endWith).toFile(); + try { + // 接收原图 + mFile.transferTo(sourceFile); + Thumbnails.of(sourceFile) + .useExifOrientation(true) + .scale(1.0) + .outputFormat("jpg") + .toFile(firstFile); + // 压缩 + log.info("人脸压缩 ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + ImageCompressUtil.compressFace(firstFile, outputFile, 400); + log.info("缩略图压缩 ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + ImageCompressUtil.compressThumbnail(outputFile, thumbnailFile, 40); + log.info("压缩完成 ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + // 上传 OSS + String url = uploadFile(outputFile, endWith); + String thumbnailUrl = uploadFile(thumbnailFile, suffix); + log.info("上传完成 ... , {}", DateDetail.getDate2Str(new Date(), DateDetail.DF4)); + // 保存人脸信息 + userFaceService.saveUserFace(userDto, userInfo, url, thumbnailUrl); + // 下发考勤机 + Integer result = userFaceService.sendToMachine(userDto.getUserId()); + // 更新结果 + userFaceService.updateMachineSyncStatus(userDto.getUserId(), result); + log.error("执行结束:{}", result); + return result; + } finally { + sourceFile.delete(); + outputFile.delete(); + thumbnailFile.delete(); + } + } + + @Override + public Integer syncUserFaceToMachine(String userId) throws Exception { + + Integer result = ConstantUtil.NUM_FALSE; + UserInfo updateUser = UserProvider.getUser(); + AttendanceUserFace userFace = userFaceService.updateUserFaceInfo(userId, updateUser.getUserId()); + if (null == userFace) { + return result; + } + // 下发至考勤机 + result = userFaceService.sendToMachine(userFace.getUserId()); + // 更新结果 + userFaceService.updateMachineSyncStatus(userFace.getUserId(), result); + return result; + } + + public void compressForFace(File input, File output, long targetKB) throws Exception { + + long targetBytes = targetKB * 1024; + + if (input.length() <= targetBytes) { + Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + return; + } + + BufferedImage img = ImageIO.read(input); + int width = img.getWidth(); + int height = img.getHeight(); + + int maxSide = 1024; + double scale = (width > maxSide || height > maxSide) + ? Math.min(maxSide / (double) width, maxSide / (double) height) + : 1.0; + + File scaledTemp = new File( + System.getProperty("java.io.tmpdir"), + "face_scaled_" + System.nanoTime() + ".jpg" + ); + + try { + Thumbnails.of(input) + .scale(scale) + .outputQuality(1.0) + .toFile(scaledTemp); + + int maxLoop = calcLoopCount(input.length() / 1024, targetKB); + + binaryCompressAccurate( + scaledTemp, output, targetBytes, + 0.5, 0.95, 0.85, maxLoop + ); + + } finally { + scaledTemp.delete(); + } + } + + public void compressForThumbnail(File input, File output, long targetKB) throws Exception { + + long targetBytes = targetKB * 1024; + + if (input.length() <= targetBytes) { + Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + return; + } + + BufferedImage img = ImageIO.read(input); + int width = img.getWidth(); + int height = img.getHeight(); + + int maxSide = 300; + double scale = (width > maxSide || height > maxSide) + ? Math.min(maxSide / (double) width, maxSide / (double) height) + : 1.0; + + File scaledTemp = new File( + System.getProperty("java.io.tmpdir"), + "thumb_scaled_" + System.nanoTime() + ".jpg" + ); + + try { + Thumbnails.of(input) + .scale(scale) + .outputQuality(1.0) + .toFile(scaledTemp); + + int maxLoop = calcLoopCount(input.length() / 1024, targetKB); + + binaryCompressAccurate( + scaledTemp, output, targetBytes, + 0.35, 0.65, 0.5, maxLoop + ); + + } finally { + scaledTemp.delete(); + } + } + + private void binaryCompressAccurate(File input, File output, + long targetBytes, + double minQ, + double maxQ, + double startQ, + int maxLoop) throws Exception { + double quality = startQ; + for (int i = 0; i < maxLoop; i++) { + Thumbnails.of(input) + .scale(1.0) + .outputQuality(quality) + .toFile(output); + long size = output.length(); + double ratio = size * 1.0 / targetBytes; + // 已经非常接近目标 + if (ratio > 0.8 && ratio < 1.2) { + return; + } + if (size > targetBytes) { + maxQ = quality; + } else { + minQ = quality; + } + quality = (minQ + maxQ) / 2; + } + // 最后一轮兜底:如果压过头太多,回弹一次 + if (output.length() < targetBytes * 0.7) { + double reboundQ = Math.min(maxQ, quality * 1.15); + Thumbnails.of(input) + .scale(1.0) + .outputQuality(reboundQ) + .toFile(output); + } + } + + /** + * 动态计算循环次数 + * @param inputKB 原图大小 + * @param targetKB 目标大小 + */ + private int calcLoopCount(long inputKB, long targetKB) { + double ratio = inputKB * 1.0 / targetKB; + if (ratio > 10) { // 原图比目标大 > 10 倍 + return 5; + } else if (ratio > 5) { // 5~10 倍 + return 4; + } else if (ratio > 2) { // 2~5 倍 + return 3; + } else { // 小图 + return 2; + } + } + + private String uploadFile(File file, String suffix) throws IOException { + + FileInputStream input = new FileInputStream(file); + String uuid = UUID.randomUUID().toString(); + MultipartFile multiFile = new MockMultipartFile(uuid + TemplateExcelUtils.SUFFIX, file.getName(), MediaType.MULTIPART_FORM_DATA_VALUE, input); + input.close(); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multiFile, fileApi.getPath(FileTypeConstant.TEMPORARY), uuid + suffix); + return fileInfo.getUrl().replace(ConstantUtil.EXTRA_PATH, ""); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UsualPhoneServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UsualPhoneServiceImpl.java new file mode 100644 index 0000000..9cbd74f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/UsualPhoneServiceImpl.java @@ -0,0 +1,178 @@ +package jnpf.attendance.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.mapper.UserPhoneMapper; +import jnpf.attendance.service.CommonSettingService; +import jnpf.attendance.service.UsualPhoneService; +import jnpf.base.ActionResult; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.attendance.AttendanceCommonSetting; +import jnpf.entity.attendance.AttendanceUserPhone; +import jnpf.enums.attendance.SettingGroup; +import jnpf.model.attendance.dto.CancelPhoneDto; +import jnpf.model.attendance.dto.UsualPhoneDto; +import jnpf.model.attendance.dto.UsualPhoneQueryDto; +import jnpf.model.attendance.dto.UsualPhoneSettingDto; +import jnpf.model.attendance.vo.attendance.UsualPhonePageVo; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.ConstantUtil; +import jnpf.util.DateUtil; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 常用设备服务实现 + * + * @author yanwenfu + * @create 2025-09-18 + */ +@Service +public class UsualPhoneServiceImpl extends SuperServiceImpl implements UsualPhoneService { + + @Resource + private CommonSettingService commonSettingService; + @Resource + private UserPhoneMapper userPhoneMapper; + @Autowired + private V2UserApi v2UserApi; + + @Override + public void cancelPhoneBatch(CancelPhoneDto cancelPhoneDto) { + + this.update(new LambdaUpdateWrapper() + .set(AttendanceUserPhone::getDeleteMark, ConstantUtil.NUM_TRUE) + .set(AttendanceUserPhone::getDeleteTime, DateUtil.getNowDate()) + .set(AttendanceUserPhone::getDeleteUserId, UserProvider.getLoginUserId()) + .in(AttendanceUserPhone::getId, cancelPhoneDto.getCancelIdList())); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateUsualPhoneSetting(UsualPhoneSettingDto usualPhoneSettingDto) { + + List list = FtbUtil.generateCommonSetting(SettingGroup.USUAL_PHONE, usualPhoneSettingDto, UserProvider.getLoginUserId()); + // 删除数据库配置 + commonSettingService.remove(new LambdaQueryWrapper() + .eq(AttendanceCommonSetting::getSettingGroup, SettingGroup.USUAL_PHONE.getValue())); + // 添加新的配置 + commonSettingService.saveBatch(list); + } + + @Override + public PageInfo getUsualPhonePage(UsualPhoneQueryDto queryDto) { + + if (StringUtils.isEmpty(queryDto.getUserName())) { + // 分页查询 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(userPhoneMapper.getUsualPhoneList(null)); + if (page.getList().isEmpty()) { + return page; + } + List userIds = page.getList().stream().map(UsualPhonePageVo::getUserId).collect(Collectors.toList()); + List data = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId()); + if (null != data && !data.isEmpty()) { + fillUserInfo(data, page.getList()); + } + return page; + } else { + List userList = v2UserApi.userListAndCopyLikeName(queryDto.getUserName(), null, UserProvider.getUser().getTenantId()); + if (userList.isEmpty()) { + return new PageInfo<>(); + } + List collect = userList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + PageInfo page = new PageInfo<>(userPhoneMapper.getUsualPhoneList(collect)); + if (page.getList().isEmpty()) { + return new PageInfo<>(); + } + fillUserInfo(userList, page.getList()); + return page; + } + } + + private void fillUserInfo(List data, List list) { + Map userMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + list.forEach(v -> { + UserBoundVO user = userMap.get(v.getUserId()); + if (null != user) { + v.setUserName(user.getFormCopy() ? user.getUserName() + "(离职)" : user.getUserName()); + v.setOrganizeId(user.getOrganizeId()); + v.setOrganizeName(user.getOrganizeName()); + } + }); + } + + @Override + public void addUsualPhone(UsualPhoneDto usualPhoneDto) { + + // 查询常用手机记录 + List phoneList = this.list(new LambdaQueryWrapper() + .eq(AttendanceUserPhone::getUserId, usualPhoneDto.getUserId())); + // 查询配置 + UsualPhoneSettingDto dto = getUsualPhoneSetting(); + // 判定记录是否存在 是否超出设备数量限制 + if (phoneList.size() >= dto.getUsualPhoneNum()) { + return; + } + // 新增常用设备 + addNewUsualPhone(usualPhoneDto, UserProvider.getLoginUserId()); + } + + private void addNewUsualPhone(UsualPhoneDto usualPhoneDto, String userId) { + + AttendanceUserPhone userPhone = new AttendanceUserPhone(); + userPhone.setId(FtbUtil.getId()); + userPhone.setUserId(usualPhoneDto.getUserId()); + userPhone.setPhoneName(usualPhoneDto.getPhoneName()); + userPhone.setPhoneCode(usualPhoneDto.getPhoneCode()); + userPhone.setCreatorUserId(userId); + userPhone.setLastModifyUserId(userId); + userPhone.setDeleteMark(ConstantUtil.NUM_FALSE); + this.save(userPhone); + } + + @Override + public UsualPhoneSettingDto getUsualPhoneSetting() { + + List list = commonSettingService.list(new LambdaQueryWrapper() + .eq(AttendanceCommonSetting::getSettingGroup, SettingGroup.USUAL_PHONE.getValue())); + return FtbUtil.changeListToSetting(list, UsualPhoneSettingDto.class); + } + + @Override + public Boolean checkUsualPhone(String phoneName, String phoneCode) { + + String userId = UserProvider.getLoginUserId(); + List phoneList = this.list(new LambdaQueryWrapper() + .eq(AttendanceUserPhone::getUserId, userId) + .eq(AttendanceUserPhone::getDeleteMark, ConstantUtil.NUM_FALSE)); + // 判定手机名称是否在常用列表中 + AttendanceUserPhone userPhone = phoneList.stream().filter(v -> v.getPhoneCode().equals(phoneCode)).findFirst().orElse(null); + if (null != userPhone) { + // 未异常 + return Boolean.FALSE; + } + // 判定是否超出常用设备数量 + UsualPhoneSettingDto dto = getUsualPhoneSetting(); + // 判定记录是否存在 是否超出设备数量限制 + if (phoneList.size() >= dto.getUsualPhoneNum()) { + return Boolean.TRUE; + } + // 未超出数量, 新增常用设备 + addNewUsualPhone(new UsualPhoneDto(userId, phoneName, phoneCode), userId); + return Boolean.FALSE; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/WorkstationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/WorkstationServiceImpl.java new file mode 100644 index 0000000..e2e6b95 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/WorkstationServiceImpl.java @@ -0,0 +1,594 @@ +package jnpf.attendance.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.WorkstationMapper; +import jnpf.attendance.mapper.WorkstationUserMapper; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserService; +import jnpf.attendance.service.WorkstationService; +import jnpf.authority.FtbAuthorityApi; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.AttendanceGroupUser; +import jnpf.entity.Workstation; +import jnpf.entity.WorkstationUser; +import jnpf.model.attendance.dto.WorkstationQueryDto; +import jnpf.model.attendance.dto.WorkstationSaveDto; +import jnpf.model.attendance.dto.WorkstationUserAddDto; +import jnpf.model.attendance.dto.WorkstationUserRemoveDto; +import jnpf.model.attendance.vo.*; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.util.DateDetail; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 考勤工作站服务实现 + * + * @author AI Generated + * @create 2026-05-11 + */ +@Slf4j +@Service +public class WorkstationServiceImpl extends SuperServiceImpl implements WorkstationService { + + private static final int MAX_IN_GROUP_QUERY_DAYS = 366; + + @Resource + private WorkstationUserMapper workstationUserMapper; + + + @Resource + private AttendanceGroupService attendanceGroupService; + + @Resource + private AttendanceUserService attendanceUserService; + + @Resource + private UserAntifreeze userAntifreeze; + @Resource + private FtbAuthorityApi ftbAuthorityApi; + + @Autowired + private AttendanceDailyRuleService attendanceDailyRuleService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveWorkstation(WorkstationSaveDto dto) { + assertWorkstationUniqueUnderStore(dto.getStoreId(), dto.getName(), dto.getPositionId(), null); + + Workstation workstation = new Workstation(); + BeanUtils.copyProperties(dto, workstation); + workstation.setId(RandomUtil.uuId()); + workstation.setCreatorUserId(UserProvider.getUser().getUserId()); + workstation.setCreatorTime(new Date()); + workstation.setTenantId(UserProvider.getUser().getTenantId()); + workstation.setDeleteMark(0); + workstation.setEnabledMark(1); + if (workstation.getSortCode() == null) { + workstation.setSortCode(0); + } + + this.save(workstation); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateWorkstation(String id, WorkstationSaveDto dto) { + Workstation workstation = this.getById(id); + Assert.notNull(workstation, "工作站不存在"); + + assertWorkstationUniqueUnderStore(dto.getStoreId(), dto.getName(), dto.getPositionId(), id); + + BeanUtils.copyProperties(dto, workstation); + workstation.setLastModifyUserId(UserProvider.getUser().getUserId()); + workstation.setLastModifyTime(new Date()); + + this.updateById(workstation); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteWorkstation(String id) { + Workstation workstation = this.getById(id); + Assert.notNull(workstation, "工作站不存在"); + + // 软删除工作站 + workstation.setDeleteMark(1); + workstation.setDeleteUserId(UserProvider.getUser().getUserId()); + workstation.setDeleteTime(new Date()); + this.updateById(workstation); + + // 级联删除关联人员 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(WorkstationUser::getWorkstationId, id) + .eq(WorkstationUser::getDeleteMark, 0); + List userList = workstationUserMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(userList)) { + String deleteUserId = UserProvider.getUser().getUserId(); + Date deleteTime = new Date(); + userList.forEach(user -> { + user.setDeleteMark(1); + user.setDeleteUserId(deleteUserId); + user.setDeleteTime(deleteTime); + workstationUserMapper.updateById(user); + }); + } + } + + @Override + public PageInfo listWorkstations(WorkstationQueryDto dto) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Workstation::getDeleteMark, 0); + if(Objects.equals(dto.getHasAuth(),1)){ + List storeBaseListInfos = ftbAuthorityApi.authStoreBaseListInfo(); + wrapper.in(Workstation::getStoreId, storeBaseListInfos.stream().map(StoreBaseListInfo::getId).distinct().collect(Collectors.toList())); + } + if (StringUtils.isNotBlank(dto.getKeyword())) { + wrapper.and(v->v.like(Workstation::getName, dto.getKeyword()) + .or() + .like(Workstation::getStoreName, dto.getKeyword())); + } + if (StringUtils.isNotBlank(dto.getName())) { + wrapper.like(Workstation::getName, dto.getName()); + } + if (StringUtils.isNotBlank(dto.getStoreId())) { + wrapper.eq(Workstation::getStoreId, dto.getStoreId()); + } + if (StringUtils.isNotBlank(dto.getPositionId())) { + wrapper.eq(Workstation::getPositionId, dto.getPositionId()); + } + + wrapper.orderByAsc(Workstation::getSortCode) + .orderByDesc(Workstation::getCreatorTime); + + PageHelper.startPage(dto.getCurrentPage(), dto.getPageSize()); + PageInfo entityPage = new PageInfo<>(this.list(wrapper)); + + List voList = entityPage.getList().stream().map(w -> { + WorkstationVo vo = new WorkstationVo(); + BeanUtils.copyProperties(w, vo); + return vo; + }).collect(Collectors.toList()); + + PageInfo result = new PageInfo<>(); + BeanUtils.copyProperties(entityPage, result); + result.setList(voList); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addUsers(WorkstationUserAddDto dto) { + requireActiveWorkstation(dto.getWorkstationId()); + + // 批量添加人员(幂等操作) + String tenantId = UserProvider.getUser().getTenantId(); + String userId = UserProvider.getUser().getUserId(); + Date now = new Date(); + + for (String addUserId : dto.getUserIds()) { + // 检查是否已存在 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(WorkstationUser::getWorkstationId, dto.getWorkstationId()) + .eq(WorkstationUser::getUserId, addUserId) + .eq(WorkstationUser::getDeleteMark, 0); + long count = workstationUserMapper.selectCount(wrapper); + if (count > 0) { + continue; // 已存在,跳过 + } + + WorkstationUser workstationUser = new WorkstationUser(); + workstationUser.setId(RandomUtil.uuId()); + workstationUser.setWorkstationId(dto.getWorkstationId()); + workstationUser.setUserId(addUserId); + workstationUser.setCreatorUserId(userId); + workstationUser.setCreatorTime(now); + workstationUser.setTenantId(tenantId); + workstationUser.setDeleteMark(0); + + workstationUserMapper.insert(workstationUser); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeUser(WorkstationUserRemoveDto dto) { + requireActiveWorkstation(dto.getWorkstationId()); + + // 查找关联记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(WorkstationUser::getWorkstationId, dto.getWorkstationId()) + .eq(WorkstationUser::getUserId, dto.getUserId()) + .eq(WorkstationUser::getDeleteMark, 0); + WorkstationUser workstationUser = workstationUserMapper.selectOne(wrapper); + Assert.notNull(workstationUser, "该人员不存在于工作站中"); + + // 软删除 + workstationUser.setDeleteMark(1); + workstationUser.setDeleteUserId(UserProvider.getUser().getUserId()); + workstationUser.setDeleteTime(new Date()); + workstationUserMapper.updateById(workstationUser); + } + + @Override + public WorkstationDetailVo getDetail(String id) { + Workstation workstation = this.getById(id); + Assert.notNull(workstation, "工作站不存在"); + + WorkstationDetailVo detailVo = new WorkstationDetailVo(); + BeanUtils.copyProperties(workstation, detailVo); + detailVo.setUserList(buildDetailUserList(workstation)); + return detailVo; + } + + /** + * 组装工作站详情关联人员:含岗位默认归属(主岗位匹配)与关联表另加入员工,并以 {@code isExtra} 区分。 + */ + private List buildDetailUserList(Workstation workstation) { + List byOrgId = attendanceGroupService.getByOrgId(workstation.getStoreId()); + if (CollUtil.isEmpty(byOrgId)){ + return buildDetailUserListFromRelations(workstation.getId()); + } + Date snapshotEnd = new Date(); + List groupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment( + snapshotEnd, snapshotEnd, null, byOrgId.stream().map(AttendanceGroup::getId).distinct().collect(Collectors.toList())); + List groupUserIds = CollUtil.isEmpty(groupUsers) ? CollUtil.newArrayList() : + groupUsers.stream() + .map(AttendanceGroupUser::getUserId) + .filter(StrUtil::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + + Set extraUserIds = loadExtraUserIdsByWorkstation(List.of(workstation.getId()), groupUserIds) + .getOrDefault(workstation.getId(), new HashSet<>()); + Map userInfoMap = batchGetUserInfoMap( + new ArrayList<>(CollUtil.unionDistinct(groupUserIds, extraUserIds))); + + List userList = new ArrayList<>(); + Set appendedUserIds = new HashSet<>(); + + if (StrUtil.isNotEmpty(workstation.getPositionId()) && CollUtil.isNotEmpty(groupUserIds)) { + for (String uid : groupUserIds) { + PartUserInfoVo info = userInfoMap.get(uid); + if (Objects.nonNull(info) && Objects.equals(workstation.getPositionId(), info.getPositionId())) { + userList.add(toWorkstationUserVo(uid, info, Boolean.FALSE)); + appendedUserIds.add(uid); + } + } + } + + for (String uid : extraUserIds) { + if (appendedUserIds.contains(uid)) { + continue; + } + userList.add(toWorkstationUserVo(uid, userInfoMap.get(uid), Boolean.TRUE)); + appendedUserIds.add(uid); + } + return userList; + } + + /** + * 无考勤组上下文时,仅返回关联表中另加入的员工。 + */ + private List buildDetailUserListFromRelations(String workstationId) { + List relations = workstationUserMapper.selectList( + new LambdaQueryWrapper() + .eq(WorkstationUser::getWorkstationId, workstationId) + .eq(WorkstationUser::getDeleteMark, 0)); + if (CollUtil.isEmpty(relations)) { + return CollUtil.newArrayList(); + } + + List userIds = relations.stream() + .map(WorkstationUser::getUserId) + .collect(Collectors.toList()); + Map userInfoMap = batchGetUserInfoMap(userIds); + return relations.stream() + .map(rel -> toWorkstationUserVo(rel.getUserId(), userInfoMap.get(rel.getUserId()), Boolean.TRUE)) + .collect(Collectors.toList()); + } + + private WorkstationUserVo toWorkstationUserVo(String userId, PartUserInfoVo info, Boolean isExtra) { + WorkstationUserVo userVo = new WorkstationUserVo(); + userVo.setUserId(userId); + userVo.setIsExtra(isExtra); + if (Objects.nonNull(info)) { + userVo.setUserName(info.getRealName()); + userVo.setHeadIcon(StrUtil.isNotEmpty(info.getHeadIcon()) ? info.getHeadIcon() : null); + userVo.setPositionId(info.getPositionId()); + userVo.setPositionName(info.getPositionName()); + } else { + userVo.setUserName(""); + } + return userVo; + } + + @Override + public List listWorkstationsByGroupId(String groupId, Date startTime, Date endTime) { + if (StrUtil.isEmpty(groupId)) { + return CollUtil.newArrayList(); + } + Assert.notNull(startTime, "开始时间不能为空"); + Assert.notNull(endTime, "结束时间不能为空"); + Date rangeStart = cn.hutool.core.date.DateUtil.beginOfDay(startTime); + Date rangeEnd = cn.hutool.core.date.DateUtil.endOfDay(endTime); + Assert.isFalse(rangeStart.after(rangeEnd), "开始时间不能晚于结束时间"); + long dayCount = cn.hutool.core.date.DateUtil.betweenDay(rangeStart, rangeEnd, true) + 1; + Assert.isTrue(dayCount <= MAX_IN_GROUP_QUERY_DAYS, + "查询跨度不能超过" + MAX_IN_GROUP_QUERY_DAYS + "天"); + + AttendanceGroup group = attendanceGroupService.getById(groupId); + if (Objects.isNull(group) || Objects.equals(group.getDeleteMark(), 1)) { + return CollUtil.newArrayList(); + } + String storeId = group.getOrgId(); + + // 1. 查询考勤组归属门店下的全部启用且未删除工作站 + LambdaQueryWrapper wsWrapper = new LambdaQueryWrapper() + .eq(Workstation::getDeleteMark, 0) + .eq(StrUtil.isNotEmpty(storeId), Workstation::getStoreId, storeId) + .orderByAsc(Workstation::getSortCode) + .orderByDesc(Workstation::getCreatorTime); + List workstations = this.list(wsWrapper); + if (CollUtil.isEmpty(workstations)) { + return CollUtil.newArrayList(); + } + + // 2. 截止 endTime 的考勤组成员(含借调过滤) + List groupUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment( + rangeEnd, rangeEnd, null, List.of(groupId), true); + List groupUserIds = CollUtil.isEmpty(groupUsers) ? CollUtil.newArrayList() : + groupUsers.stream() + .map(AttendanceGroupUser::getUserId) + .filter(StrUtil::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + + // 3. 查询时间范围内的成员关系,计算每人 inGroupDays + List datesByPeriod = DateDetail.getDatesByPeriod(rangeStart, rangeEnd); + Map> inGroupDaysByUserId = buildInGroupDaysByUserId( + groupId, groupUserIds, rangeStart, rangeEnd, datesByPeriod); + + // 4. 划线排班配置过滤 + List canLineUserIds = new ArrayList<>(groupUserIds); + attendanceDailyRuleService.lineSchedulesConfigFilter(groupId, canLineUserIds); + Set canLineUserIdSet = new HashSet<>(canLineUserIds); + + // 5. 批量取用户主岗位与姓名 + Map userInfoMap = batchGetUserInfoMap(groupUserIds); + + // 6. 查询门店下工作站的额外人员关联,仅取仍在考勤组成员范围内的关系 + Map> extraUserIdsByWs = loadExtraUserIdsByWorkstation( + workstations.stream().map(Workstation::getId).collect(Collectors.toList()), + groupUserIds); + + Set groupUserIdSet = new HashSet<>(groupUserIds); + return workstations.stream().map(ws -> buildWorkstationWithUsersVo( + ws, groupUserIdSet, userInfoMap, extraUserIdsByWs.getOrDefault(ws.getId(), new HashSet<>()), + canLineUserIdSet, inGroupDaysByUserId) + ).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void resetWorkstationUsers(String workstationId) { + Assert.notEmpty(workstationId, "工作站ID不能为空"); + Workstation workstation = this.getById(workstationId); + Assert.notNull(workstation, "工作站不存在"); + if (Objects.equals(workstation.getDeleteMark(), 1)) { + return; + } + + List relations = workstationUserMapper.selectList( + new LambdaQueryWrapper() + .eq(WorkstationUser::getDeleteMark, 0) + .eq(WorkstationUser::getWorkstationId, workstationId)); + if (CollUtil.isEmpty(relations)) { + return; + } + + String deleteUserId = UserProvider.getUser().getUserId(); + Date deleteTime = new Date(); + for (WorkstationUser relation : relations) { + relation.setDeleteMark(1); + relation.setDeleteUserId(deleteUserId); + relation.setDeleteTime(deleteTime); + workstationUserMapper.updateById(relation); + } + } + + private Map> buildInGroupDaysByUserId(String groupId, List groupUserIds, + Date rangeStart, Date rangeEnd, List datesByPeriod) { + Map> result = new HashMap<>(); + if (CollUtil.isEmpty(groupUserIds)) { + return result; + } + List historyUsers = attendanceUserService.getAttendanceGroupUsersOfSecondment( + rangeStart, rangeEnd, null, List.of(groupId), true); + Map> groupUserMap = CollUtil.isEmpty(historyUsers) + ? new HashMap<>() + : historyUsers.stream().collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + for (String userId : groupUserIds) { + result.put(userId, resolveInGroupDays(groupUserMap.get(userId), datesByPeriod)); + } + return result; + } + + private List resolveInGroupDays(List userRecords, List datesByPeriod) { + if (CollUtil.isEmpty(userRecords) || CollUtil.isEmpty(datesByPeriod)) { + return CollUtil.newArrayList(); + } + List inGroupDays = new ArrayList<>(); + for (Date day : datesByPeriod) { + if (isUserInGroupOnDay(userRecords, day)) { + inGroupDays.add(DateDetail.getDate2Str(day, DateDetail.DF)); + } + } + return inGroupDays; + } + + private boolean isUserInGroupOnDay(List userRecords, Date day) { + Integer existStatus = attendanceDailyRuleService.isExistStatus(userRecords, day); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + return Objects.equals(existStatus, 1); + } + + /** + * 批量获取用户基础信息(id、userName、positionId、positionName)映射 + */ + private Map batchGetUserInfoMap(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new java.util.HashMap<>(); + } + List result = userAntifreeze.getInfoByIds(userIds,null); + if (CollUtil.isEmpty(result)) { + return new java.util.HashMap<>(); + } + return result.stream() + .collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity(), (a, b) -> a)); + } + + /** + * 查询每个工作站下显式额外加入且仍在考勤组成员范围内的 userId 集合 + */ + private Map> loadExtraUserIdsByWorkstation(List workstationIds, List groupUserIds) { + Map> result = new LinkedHashMap<>(); + if (CollUtil.isEmpty(workstationIds) || CollUtil.isEmpty(groupUserIds)) { + return result; + } + List relations = workstationUserMapper.selectList( + new LambdaQueryWrapper() + .eq(WorkstationUser::getDeleteMark, 0) + .in(WorkstationUser::getWorkstationId, workstationIds) + .in(WorkstationUser::getUserId, groupUserIds)); + if (CollUtil.isEmpty(relations)) { + return result; + } + for (WorkstationUser rel : relations) { + result.computeIfAbsent(rel.getWorkstationId(), k -> new HashSet<>()).add(rel.getUserId()); + } + return result; + } + + /** + * 装配单个工作站的对外 VO(含员工列表) + * + * @param ws 工作站实体 + * @param groupUserIds 考勤组当前成员集合(用于按主岗位自动归属) + * @param userInfoMap 用户主岗位/姓名信息映射 + * @param extraUserIds 通过工作站-人员关联表显式额外加入的成员集合(已限定为考勤组成员) + */ + private WorkstationWithUsersVo buildWorkstationWithUsersVo(Workstation ws, + Set groupUserIds, + Map userInfoMap, + Set extraUserIds, + Set canLineUserIdSet, + Map> inGroupDaysByUserId) { + WorkstationWithUsersVo vo = new WorkstationWithUsersVo(); + vo.setWorkstationId(ws.getId()); + vo.setWorkstationName(ws.getName()); + vo.setPositionId(ws.getPositionId()); + vo.setPositionName(ws.getPositionName()); + + List userList = new ArrayList<>(); + Set appendedUserIds = new HashSet<>(); + + // 主岗位与工作站岗位一致 → 自动归属(isExtra=false) + if (StrUtil.isNotEmpty(ws.getPositionId()) && CollUtil.isNotEmpty(groupUserIds)) { + for (String uid : groupUserIds) { + PartUserInfoVo info = userInfoMap.get(uid); + if (Objects.nonNull(info) && Objects.equals(ws.getPositionId(), info.getPositionId())) { + userList.add(toWorkstationGroupUserVo(uid, info, Boolean.FALSE, canLineUserIdSet, inGroupDaysByUserId)); + appendedUserIds.add(uid); + } + } + } + + // 关联表显式额外加入 → 标记为另加入员工(isExtra=true),主岗位匹配者已在上方,避免重复 + for (String uid : extraUserIds) { + if (appendedUserIds.contains(uid)) { + continue; + } + userList.add(toWorkstationGroupUserVo(uid, userInfoMap.get(uid), Boolean.TRUE, canLineUserIdSet, inGroupDaysByUserId)); + appendedUserIds.add(uid); + } + vo.setUserList(userList); + return vo; + } + + private WorkstationGroupUserVo toWorkstationGroupUserVo(String userId, PartUserInfoVo info, Boolean isExtra, + Set canLineUserIdSet, Map> inGroupDaysByUserId) { + WorkstationGroupUserVo userVo = new WorkstationGroupUserVo(); + userVo.setUserId(userId); + if (Objects.nonNull(info)) { + userVo.setUserName(info.getRealName()); + userVo.setPositionId(info.getPositionId()); + } + userVo.setIsExtra(isExtra); + userVo.setCanLineSchedule(canLineUserIdSet.contains(userId)); + userVo.setInGroupDays(inGroupDaysByUserId.getOrDefault(userId, CollUtil.newArrayList())); + return userVo; + } + + private Workstation requireActiveWorkstation(String workstationId) { + Workstation workstation = this.getById(workstationId); + Assert.notNull(workstation, "工作站不存在"); + Assert.isFalse(Objects.equals(workstation.getDeleteMark(), 1), "该工作站已被删除,请返回上一页重新操作"); + return workstation; + } + + /** + * 同组织(门店)下工作站名称、岗位均不可重复;不同组织允许重复。 + * + * @param storeId 组织/门店 ID({@link Workstation#getStoreId()}) + * @param name 工作站名称 + * @param positionId 岗位 ID + * @param excludeId 排除的工作站 ID(编辑时传) + */ + private void assertWorkstationUniqueUnderStore(String storeId, String name, String positionId, String excludeId) { + Assert.notBlank(storeId, "门店ID不能为空"); + String tenantId = UserProvider.getUser().getTenantId(); + + LambdaQueryWrapper nameWrapper = new LambdaQueryWrapper<>(); + nameWrapper.eq(Workstation::getName, name) + .eq(Workstation::getStoreId, storeId) + .eq(Workstation::getDeleteMark, 0) + .eq(Workstation::getTenantId, tenantId); + if (StringUtils.isNotBlank(excludeId)) { + nameWrapper.ne(Workstation::getId, excludeId); + } + Assert.isFalse(this.count(nameWrapper) > 0, "同组织下工作站名称已存在"); + + LambdaQueryWrapper positionWrapper = new LambdaQueryWrapper<>(); + positionWrapper.eq(Workstation::getPositionId, positionId) + .eq(Workstation::getStoreId, storeId) + .eq(Workstation::getDeleteMark, 0) + .eq(Workstation::getTenantId, tenantId); + if (StringUtils.isNotBlank(excludeId)) { + positionWrapper.ne(Workstation::getId, excludeId); + } + Assert.isFalse(this.count(positionWrapper) > 0, "同组织下该岗位已绑定其他工作站"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/ByEmployeeSchedulesV2Converter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/ByEmployeeSchedulesV2Converter.java new file mode 100644 index 0000000..810e865 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/ByEmployeeSchedulesV2Converter.java @@ -0,0 +1,295 @@ +package jnpf.attendance.service.impl.preschedule; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedEmployeeView; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedShiftRef; +import jnpf.entity.attendance.AttendanceShiftNameEntity; +import jnpf.enums.attendance.SchedulesTypeEnum; +import jnpf.model.attendance.vo.SchedulesDayVo; +import jnpf.model.attendance.vo.SchedulesItemVo; +import jnpf.model.attendance.vo.SchedulesV2Vo; +import jnpf.util.DateDetail; +import jnpf.util.DateUtil; +import jnpf.util.StringUtil; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.function.BiFunction; + +/** + * 将 {@link ShiftPlanAssignmentResult#getByEmployee()}({@link AssignedEmployeeView} 列表) + * 转为 {@link SchedulesV2Vo},供预排班草稿查询接口使用。 + * + *

字段对应关系: + *

    + *
  • {@link AssignedEmployeeView#getUserId()} → {@link SchedulesV2Vo#getUserId()}
  • + *
  • {@link AssignedEmployeeView#getUserName()} → {@link SchedulesV2Vo#getRealName()}
  • + *
  • {@link AssignedShiftRef#getScheduleDay()} → {@code val} 的 key(yyyy-MM-dd)
  • + *
  • {@link AssignedShiftRef#getTimeRangeText()}(如 {@code 9:00-14:00})→ {@link SchedulesItemVo#getStart()} / + * {@link SchedulesItemVo#getEnd()}(统一为 {@code HH:mm},与正式排班一致)、{@link SchedulesItemVo#getValidDuration()}
  • + *
  • {@link AssignedShiftRef} 其余字段 → {@link SchedulesItemVo}(附图字段;{@code postId} 不参与 V2 展示)
  • + *
+ */ +public final class ByEmployeeSchedulesV2Converter { + + /** 与接口约定:itemVos.type 默认 1。 */ + private static final int ITEM_TYPE_DEFAULT = 2; + /** 与接口约定:schedulesType 默认 0(全天)。 */ + private static final int SCHEDULES_TYPE_ALL_DAY = 0; + private static final String LINE_SCHEDULE_NAME = "划线排班"; + /** 与 {@link DateDetail#DF10}、正式排班 {@code start}/{@code end} 展示一致。 */ + private static final DateTimeFormatter HM_STANDARD = DateTimeFormatter.ofPattern("HH:mm"); + + private ByEmployeeSchedulesV2Converter() {} + + /** + * 由 {@code byEmployee} 转为考勤组全员维度的 {@link SchedulesV2Vo} 列表(区间内每日补全)。 + */ + public static List toSchedulesV2List( + List byEmployee, + List userIdsInOrder, + Map realNameByUserId, + Map shiftMap, + List datesByPeriod, + BiFunction dayVoBuilder) { + if (CollUtil.isEmpty(userIdsInOrder)) { + return Collections.emptyList(); + } + Map>> shiftsByUserAndDay = + indexShiftsByUserAndDay(byEmployee); + mergeRealNamesFromByEmployee(byEmployee, realNameByUserId); + + List result = new ArrayList<>(userIdsInOrder.size()); + for (String userId : userIdsInOrder) { + if (StringUtil.isEmpty(userId)) { + continue; + } + String uid = userId.trim(); + SchedulesV2Vo vo = new SchedulesV2Vo(); + vo.setUserId(uid); + vo.setRealName(realNameByUserId.getOrDefault(uid, "")); + Map val = new TreeMap<>(); + Map> userDays = + shiftsByUserAndDay.getOrDefault(uid, Collections.emptyMap()); + for (Date day : datesByPeriod) { + String dayKey = DateUtil.daFormat(day); + SchedulesDayVo dayVo = dayVoBuilder.apply(uid, day); + List refs = userDays.get(dayKey); + if (CollUtil.isNotEmpty(refs)) { + List items = new ArrayList<>(refs.size()); + for (AssignedShiftRef ref : refs) { + items.add(toSchedulesItemVo(ref, shiftMap, day)); + } + items.sort( + Comparator.comparing( + SchedulesItemVo::getStart, Comparator.nullsLast(String::compareTo))); + dayVo.setItemVos(items); + dayVo.setIsLineSchedule( + refs.stream().anyMatch(ref -> ref != null && !ref.isFixedScheduling())); + } else if (CollUtil.isEmpty(dayVo.getItemVos())) { + dayVo.setItemVos( + List.of( + SchedulesItemVo.builder() + .type(SchedulesTypeEnum.NONE.getCode()) + .build())); + } + val.put(dayKey, dayVo); + } + vo.setVal(val); + result.add(vo); + } + return result; + } + + public static Map>> indexShiftsByUserAndDay( + List byEmployee) { + if (CollUtil.isEmpty(byEmployee)) { + return Collections.emptyMap(); + } + Map>> index = new LinkedHashMap<>(); + for (AssignedEmployeeView emp : byEmployee) { + if (emp == null || StringUtil.isEmpty(emp.getUserId()) || CollUtil.isEmpty(emp.getShifts())) { + continue; + } + String uid = emp.getUserId().trim(); + for (AssignedShiftRef ref : emp.getShifts()) { + if (ref == null || ref.getScheduleDay() == null) { + continue; + } + String dayKey = ref.getScheduleDay().toString(); + index.computeIfAbsent(uid, k -> new LinkedHashMap<>()) + .computeIfAbsent(dayKey, k -> new ArrayList<>()) + .add(ref); + } + } + return index; + } + + public static Set collectShiftIds(List byEmployee) { + if (CollUtil.isEmpty(byEmployee)) { + return Collections.emptySet(); + } + Set ids = new LinkedHashSet<>(); + for (AssignedEmployeeView emp : byEmployee) { + if (emp == null || CollUtil.isEmpty(emp.getShifts())) { + continue; + } + for (AssignedShiftRef ref : emp.getShifts()) { + if (ref != null && StringUtil.isNotEmpty(ref.getShiftId())) { + ids.add(ref.getShiftId().trim()); + } + } + } + return ids; + } + + static void mergeRealNamesFromByEmployee( + List byEmployee, Map realNameByUserId) { + if (CollUtil.isEmpty(byEmployee) || realNameByUserId == null) { + return; + } + for (AssignedEmployeeView emp : byEmployee) { + if (emp != null && StringUtil.isNotEmpty(emp.getUserId())) { + realNameByUserId.putIfAbsent( + emp.getUserId().trim(), + emp.getUserName() == null ? "" : emp.getUserName()); + } + } + } + + public static SchedulesItemVo toSchedulesItemVo( + AssignedShiftRef ref, + Map shiftMap, + Date scheduleDay) { + TimeRangeParts parts = parseTimeRangeText(ref.getTimeRangeText()); + SchedulesItemVo item = new SchedulesItemVo(); + item.setType(ITEM_TYPE_DEFAULT); + item.setSchedulesType(SCHEDULES_TYPE_ALL_DAY); + item.setStart(parts.start); + item.setEnd(parts.end); + item.setValidDuration(parts.validDurationMinutes); + Date startPoint = combineDayAndTime(scheduleDay, parts.start); + Date endPoint = combineDayAndTime(scheduleDay, parts.end); + item.setIsStartTomorrow(isTomorrow(scheduleDay, startPoint)); + item.setIsEndTomorrow(isTomorrow(scheduleDay, endPoint)); + if (!ref.isFixedScheduling()) { + item.setShiftId(""); + item.setName(LINE_SCHEDULE_NAME); + return item; + } + item.setShiftId(ref.getShiftId() == null ? "" : ref.getShiftId()); + AttendanceShiftNameEntity shiftEntity = + shiftMap == null ? null : shiftMap.get(ref.getShiftId()); + if (shiftEntity != null) { + item.setName(StringUtil.isNotEmpty(shiftEntity.getName()) ? shiftEntity.getName() : ""); + item.setShortName( + StringUtil.isNotEmpty(shiftEntity.getShortName()) + ? shiftEntity.getShortName() + : ""); + item.setColour( + StringUtil.isNotEmpty(shiftEntity.getColour()) ? shiftEntity.getColour() : ""); + } else { + item.setName(""); + item.setShortName(""); + item.setColour(""); + } + return item; + } + + public static Integer normalizeExistStatus(Integer existStatus) { + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + return 1; + } + return existStatus; + } + + private static Integer isTomorrow(Date day, Date date) { + if (Objects.isNull(date)) { + return 1; + } + Date nextDay = DateUtil.dateAddDays(day, 1); + if (nextDay.before(date)) { + return 2; + } + return 1; + } + + private static Date combineDayAndTime(Date day, String hm) { + if (day == null || StringUtil.isEmpty(hm)) { + return null; + } + String dayStr = DateDetail.getDate2Str(day, DateDetail.DF); + return DateUtil.stringToDates(dayStr + " " + hm.trim()); + } + + /** + * 解析 {@code timeRangeText},并将可识别的起止时间规范为 {@code HH:mm}(如 {@code 9:00} → {@code 09:00})。 + */ + private static TimeRangeParts parseTimeRangeText(String timeRangeText) { + TimeRangeParts parts = new TimeRangeParts(); + if (StringUtil.isEmpty(timeRangeText)) { + return parts; + } + String trimmed = timeRangeText.trim(); + int dash = trimmed.indexOf('-'); + if (dash <= 0 || dash >= trimmed.length() - 1) { + String normalized = formatMinuteToHm(parseHmToMinute(trimmed)); + parts.start = StringUtil.isNotEmpty(normalized) ? normalized : trimmed; + return parts; + } + String rawStart = trimmed.substring(0, dash).trim(); + String rawEnd = trimmed.substring(dash + 1).trim(); + int startMin = parseHmToMinute(rawStart); + int endMin = parseHmToMinute(rawEnd); + if (endMin == 0 && startMin > 0) { + endMin = 24 * 60; + } + if (startMin >= 0 && endMin > startMin) { + parts.validDurationMinutes = endMin - startMin; + } else if (startMin >= 0 && endMin >= 0) { + parts.validDurationMinutes = Math.max(0, 24 * 60 - startMin + endMin); + } + parts.start = startMin >= 0 ? formatMinuteToHm(startMin) : rawStart; + parts.end = endMin >= 0 ? formatMinuteToHm(endMin) : rawEnd; + return parts; + } + + /** 自然日分钟坐标 → {@code HH:mm};1440 表示当日 24:00,展示为 {@code 00:00}。 */ + private static String formatMinuteToHm(int minutes) { + if (minutes < 0) { + return ""; + } + if (minutes >= 24 * 60) { + return LocalTime.MIDNIGHT.format(HM_STANDARD); + } + return LocalTime.of(minutes / 60, minutes % 60).format(HM_STANDARD); + } + + private static int parseHmToMinute(String hm) { + if (StringUtil.isEmpty(hm)) { + return -1; + } + String[] seg = hm.split(":"); + if (seg.length < 2) { + return -1; + } + try { + int h = Integer.parseInt(seg[0].trim()); + int m = Integer.parseInt(seg[1].trim()); + if (h == 24 && m == 0) { + return 24 * 60; + } + return h * 60 + m; + } catch (NumberFormatException e) { + return -1; + } + } + + private static final class TimeRangeParts { + String start = ""; + String end = ""; + Integer validDurationMinutes; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleByEmployeeFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleByEmployeeFilter.java new file mode 100644 index 0000000..a513261 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleByEmployeeFilter.java @@ -0,0 +1,137 @@ +package jnpf.attendance.service.impl.preschedule; + +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedEmployeeView; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedShiftRef; +import jnpf.model.attendance.vo.scheduling.PreSchedulePostVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleStationVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableRowVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleUserVo; +import jnpf.util.StringUtil; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 按主表提交的岗位、工作站人员过滤 Redis 中缓存的 {@code byEmployee}:排除在 posts、stations 中均未出现的人员排班。 + */ +public final class PreScheduleByEmployeeFilter { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + + private PreScheduleByEmployeeFilter() {} + + /** + * 排除在提交主表对应日期的 posts、stations 人员并集中均不存在的排班条目。 + */ + public static List filterByTable( + List cached, PreScheduleTableVo table) { + if (cached == null || cached.isEmpty()) { + return Collections.emptyList(); + } + Map> dayValidUsers = indexDayValidUsers(table); + if (dayValidUsers.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (AssignedEmployeeView emp : cached) { + if (emp == null || StringUtil.isBlank(emp.getUserId())) { + continue; + } + String userId = emp.getUserId().trim(); + List empShifts = emp.getShifts(); + if (empShifts == null || empShifts.isEmpty()) { + continue; + } + List keptShifts = new ArrayList<>(); + for (AssignedShiftRef shift : empShifts) { + if (shift == null || shift.getScheduleDay() == null) { + continue; + } + Set validUsers = dayValidUsers.get(shift.getScheduleDay()); + if (validUsers != null && validUsers.contains(userId)) { + keptShifts.add(shift); + } + } + if (!keptShifts.isEmpty()) { + result.add(new AssignedEmployeeView(emp.getUserId(), emp.getUserName(), keptShifts)); + } + } + result.sort(Comparator.comparing(AssignedEmployeeView::getUserId)); + return result; + } + + /** 每日有效人员 = 该行 posts 与 stations 中所有人员 ID 的并集。 */ + private static Map> indexDayValidUsers(PreScheduleTableVo table) { + Map> map = new LinkedHashMap<>(); + if (table == null || table.getRows() == null) { + return map; + } + for (PreScheduleTableRowVo row : table.getRows()) { + if (row == null || StringUtil.isBlank(row.getDate())) { + continue; + } + LocalDate day = parseDateQuiet(row.getDate()); + if (day == null) { + continue; + } + Set users = map.computeIfAbsent(day, k -> new HashSet<>()); + indexPostsUsers(users, row.getPosts()); + indexStationsUsers(users, row.getStations()); + } + return map; + } + + private static void indexPostsUsers(Set users, List posts) { + if (posts == null) { + return; + } + for (PreSchedulePostVo post : posts) { + if (post == null) { + continue; + } + addUsers(users, post.getUsers()); + } + } + + private static void indexStationsUsers(Set users, List stations) { + if (stations == null) { + return; + } + for (PreScheduleStationVo station : stations) { + if (station == null) { + continue; + } + addUsers(users, station.getUsers()); + } + } + + private static void addUsers(Set users, List list) { + if (list == null) { + return; + } + for (PreScheduleUserVo user : list) { + if (user != null && StringUtil.isNotBlank(user.getId())) { + users.add(user.getId().trim()); + } + } + } + + private static LocalDate parseDateQuiet(String text) { + try { + return LocalDate.parse(text.trim(), DATE_FMT); + } catch (DateTimeParseException ex) { + return null; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleIncompleteMsgBuilder.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleIncompleteMsgBuilder.java new file mode 100644 index 0000000..b065003 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleIncompleteMsgBuilder.java @@ -0,0 +1,281 @@ +package jnpf.attendance.service.impl.preschedule; + +import jnpf.attendance.schedule.ShiftPlanAssignmentResult; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.IncompleteScheduleReason; +import jnpf.util.StringUtil; + +import java.util.EnumMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * 将 {@link ShiftPlanAssignmentResult} 中排班/人员未满说明转为预排班主表简短 {@code msg}, + * 按自动排班链路环节归类,便于前端展示「在哪一步出了问题」。 + */ +public final class PreScheduleIncompleteMsgBuilder { + + private static final Map> STAGE_REASON_CODES = buildStageReasonCodes(); + + private PreScheduleIncompleteMsgBuilder() {} + + /** + * 排班完整且无说明时返回 {@code null};否则返回按排班环节归类的概略原因。 + */ + public static String build(ShiftPlanAssignmentResult result) { + if (result == null) { + return "排班未成功:引擎未返回结果,请稍后重试"; + } + + int picked = result.countAssignedStaff(); + int need = result.countTotalNeed(); + boolean hasReasons = result.hasIncompleteScheduleExplanations(); + boolean emptyRoster = result.getByShift().isEmpty(); + + if (!hasReasons && !emptyRoster && (need <= 0 || picked >= need)) { + return null; + } + + Map> stageCodes = groupReasonCodesByStage(result.getIncompleteScheduleReasons()); + String stepHints = formatStageHints(stageCodes); + String example = pickOneReasonExample(result.getIncompleteScheduleReasons()); + + if (emptyRoster && need <= 0) { + return appendExample( + prefixOutcome("未生成班次计划", stepHints, "请检查历史排班数据、营业额与考勤组班次配置"), + example); + } + + if (need > 0 && picked < need) { + return appendExample( + prefixOutcome( + String.format("人员未排满(%d/%d)", picked, need), + stepHints, + "请检查工作站人员绑定与排班规则配置"), + example); + } + + if (StringUtil.isNotBlank(stepHints)) { + return appendExample(prefixOutcome("排班不完整", stepHints, null), example); + } + + return emptyRoster ? "未生成班次计划:请检查历史排班数据与考勤组班次配置" : null; + } + + private static String appendExample(String msg, String example) { + if (StringUtil.isBlank(msg) || StringUtil.isBlank(example)) { + return msg; + } + return msg + "。示例:" + example; + } + + /** + * 从结果中取一条最早环节的具体说明,便于用户对照真实数据排查。 + */ + private static String pickOneReasonExample(List reasons) { + if (reasons == null || reasons.isEmpty()) { + return ""; + } + IncompleteScheduleReason best = null; + int bestOrder = Integer.MAX_VALUE; + for (IncompleteScheduleReason reason : reasons) { + if (reason == null || StringUtil.isBlank(reason.getMessage())) { + continue; + } + PipelineStage stage = resolveStage(reason.getReasonCode()); + int order = stage == null ? Integer.MAX_VALUE - 1 : stage.order; + if (order < bestOrder) { + bestOrder = order; + best = reason; + } + } + if (best == null) { + for (IncompleteScheduleReason reason : reasons) { + if (reason != null && StringUtil.isNotBlank(reason.getMessage())) { + best = reason; + break; + } + } + } + return best == null ? "" : formatReasonExample(best); + } + + private static String formatReasonExample(IncompleteScheduleReason reason) { + StringBuilder sb = new StringBuilder(); + if (reason.getScheduleDay() != null) { + sb.append(reason.getScheduleDay()); + } + if (StringUtil.isNotBlank(reason.getTimeRangeText())) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append(reason.getTimeRangeText().trim()); + } + if (StringUtil.isNotBlank(reason.getPostId())) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append("岗位").append(reason.getPostId().trim()); + } + if (StringUtil.isNotBlank(reason.getMessage())) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(reason.getMessage().trim()); + } + return sb.toString(); + } + + private static String prefixOutcome(String headline, String stepHints, String fallback) { + StringBuilder sb = new StringBuilder(headline); + if (StringUtil.isNotBlank(stepHints)) { + sb.append("。问题环节:").append(stepHints); + } else if (StringUtil.isNotBlank(fallback)) { + sb.append(",").append(fallback); + } + return sb.toString(); + } + + private static Map> groupReasonCodesByStage( + List reasons) { + Map> grouped = new EnumMap<>(PipelineStage.class); + if (reasons == null) { + return grouped; + } + for (IncompleteScheduleReason reason : reasons) { + if (reason == null || StringUtil.isBlank(reason.getReasonCode())) { + continue; + } + PipelineStage stage = resolveStage(reason.getReasonCode().trim()); + if (stage == null) { + continue; + } + grouped.computeIfAbsent(stage, k -> new LinkedHashSet<>()).add(reason.getReasonCode().trim()); + } + return grouped; + } + + private static String formatStageHints(Map> stageCodes) { + if (stageCodes == null || stageCodes.isEmpty()) { + return ""; + } + TreeMap ordered = new TreeMap<>(); + for (Map.Entry> entry : stageCodes.entrySet()) { + PipelineStage stage = entry.getKey(); + String hint = summarizeStage(stage, entry.getValue()); + if (StringUtil.isNotBlank(hint)) { + ordered.put(stage.order, stage.label + ":" + hint); + } + } + if (ordered.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + int index = 1; + for (String part : ordered.values()) { + if (index > 1) { + sb.append(";"); + } + sb.append(index++).append('.').append(part); + } + return sb.toString(); + } + + private static String summarizeStage(PipelineStage stage, Set codes) { + if (codes == null || codes.isEmpty()) { + return ""; + } + LinkedHashSet hints = new LinkedHashSet<>(); + for (String code : codes) { + String hint = mapReasonCodeToStageHint(stage, code); + if (StringUtil.isNotBlank(hint)) { + hints.add(hint); + } + } + return String.join("、", hints); + } + + private static PipelineStage resolveStage(String reasonCode) { + for (Map.Entry> e : STAGE_REASON_CODES.entrySet()) { + if (e.getValue().contains(reasonCode)) { + return e.getKey(); + } + } + return null; + } + + private static String mapReasonCodeToStageHint(PipelineStage stage, String reasonCode) { + switch (reasonCode) { + case ShiftPlanAssignmentResult.INCOMPLETE_PATTERN_HISTORY_INSUFFICIENT: + return "历史手动排班不足,无法推算排班规律"; + case ShiftPlanAssignmentResult.INCOMPLETE_SIMILAR_HISTORY_NO_SHIFT_DATA: + return "相似历史日缺少可参考的班次数据"; + case ShiftPlanAssignmentResult.INCOMPLETE_HISTORICAL_POST_MISSING_MASTER: + return "历史排班引用的岗位已失效或已删除"; + case ShiftPlanAssignmentResult.INCOMPLETE_ATTEND_SHIFT_BLOCKS_DROPPED: + return "班段时间与考勤组班次配置不一致,部分班段被移除"; + case ShiftPlanAssignmentResult.INCOMPLETE_NO_CANDIDATE: + return "过滤后在组/划线资格下无可排人员"; + case ShiftPlanAssignmentResult.INCOMPLETE_WORKSTATION_POOL_EMPTY: + return "候选人不落在工作站岗位绑定名单内"; + case ShiftPlanAssignmentResult.INCOMPLETE_FIXED_MUST_BLOCK: + return "固定班「必须」规则拦截(工时/休息/连续上班等)"; + case ShiftPlanAssignmentResult.INCOMPLETE_RULES_OR_CONFLICT: + return "排班硬约束、时段重叠或同人占用导致未满"; + default: + return stage == null ? "" : "存在未识别的排班异常"; + } + } + + private static Map> buildStageReasonCodes() { + Map> map = new EnumMap<>(PipelineStage.class); + map.put( + PipelineStage.SIMILAR_DAYS, + setOf( + ShiftPlanAssignmentResult.INCOMPLETE_PATTERN_HISTORY_INSUFFICIENT, + ShiftPlanAssignmentResult.INCOMPLETE_SIMILAR_HISTORY_NO_SHIFT_DATA)); + map.put( + PipelineStage.POST_CONFIG, + setOf(ShiftPlanAssignmentResult.INCOMPLETE_HISTORICAL_POST_MISSING_MASTER)); + map.put( + PipelineStage.ATTEND_SHIFT_MATCH, + setOf(ShiftPlanAssignmentResult.INCOMPLETE_ATTEND_SHIFT_BLOCKS_DROPPED)); + map.put( + PipelineStage.STAFF_ASSIGN, + setOf( + ShiftPlanAssignmentResult.INCOMPLETE_NO_CANDIDATE, + ShiftPlanAssignmentResult.INCOMPLETE_WORKSTATION_POOL_EMPTY, + ShiftPlanAssignmentResult.INCOMPLETE_FIXED_MUST_BLOCK, + ShiftPlanAssignmentResult.INCOMPLETE_RULES_OR_CONFLICT)); + return map; + } + + @SafeVarargs + private static Set setOf(T... items) { + LinkedHashSet set = new LinkedHashSet<>(); + if (items != null) { + for (T item : items) { + set.add(item); + } + } + return set; + } + + /** 自动排班主链路环节(与 {@link jnpf.attendance.schedule.AutoSchedulePipelineLog} 阶段对应)。 */ + private enum PipelineStage { + SIMILAR_DAYS(1, "相似日匹配"), + POST_CONFIG(2, "岗位配置"), + ATTEND_SHIFT_MATCH(3, "考勤班次匹配"), + STAFF_ASSIGN(4, "人员分配"); + + private final int order; + private final String label; + + PipelineStage(int order, String label) { + this.order = order; + this.label = label; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleQueryValidator.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleQueryValidator.java new file mode 100644 index 0000000..652086d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleQueryValidator.java @@ -0,0 +1,191 @@ +package jnpf.attendance.service.impl.preschedule; + +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.entity.AttendanceGroup; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.dto.scheduling.PreScheduleDayRevenueDto; +import jnpf.model.attendance.dto.scheduling.PreScheduleTableQueryDto; +import jnpf.util.StringUtil; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 预排班生成主表请求业务校验。 + */ +public final class PreScheduleQueryValidator { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + + /** 预排班区间最大自然日数(闭区间)。 */ + public static final int MAX_SCHEDULE_DAY_SPAN = 62; + + private PreScheduleQueryValidator() { + } + + /** + * 校验请求并解析日期区间与按日营业额。 + * + * @return key: startDate, endDate, revenueByDate + */ + public static ValidatedPreScheduleQuery validate( + PreScheduleTableQueryDto dto, AttendanceGroupMapper attendanceGroupMapper) + throws HandleException, QueryException { + if (dto == null) { + throw new HandleException("请求体不能为空"); + } + + LocalDate start = parseDate(dto.getStartDate(), "startDate"); + LocalDate end = parseDate(dto.getEndDate(), "endDate"); + if (end.isBefore(start)) { + throw new HandleException("结束日期不能早于开始日期"); + } + + long daySpan = ChronoUnit.DAYS.between(start, end) + 1; + if (daySpan > MAX_SCHEDULE_DAY_SPAN) { + throw new HandleException("排班区间不能超过" + MAX_SCHEDULE_DAY_SPAN + "天"); + } + + BigDecimal effTarget = dto.getEffTarget(); + if (effTarget == null || effTarget.compareTo(BigDecimal.ZERO) < 0) { + throw new HandleException("人效目标须大于等于 0"); + } + + String groupId = dto.getGroupId(); + if (StringUtil.isBlank(groupId)) { + throw new HandleException("考勤组ID不能为空"); + } + assertAttendanceGroupExists(groupId, attendanceGroupMapper); + + Map revenueByDate = parseDailyRevenues(dto.getDailyRevenues(), start, end, (int) daySpan); + + ValidatedPreScheduleQuery validated = new ValidatedPreScheduleQuery(); + validated.setGroupId(groupId.trim()); + validated.setStartDate(start); + validated.setEndDate(end); + validated.setEffTarget(effTarget); + validated.setRevenueByDate(revenueByDate); + return validated; + } + + private static LocalDate parseDate(String text, String fieldLabel) throws HandleException { + if (StringUtil.isBlank(text)) { + throw new HandleException(fieldLabel + "不能为空"); + } + try { + return LocalDate.parse(text.trim(), DATE_FMT); + } catch (DateTimeParseException ex) { + throw new HandleException(fieldLabel + "格式须为 yyyy-MM-dd"); + } + } + + private static Map parseDailyRevenues( + List dailyRevenues, + LocalDate start, + LocalDate end, + int expectedDayCount) + throws HandleException { + if (dailyRevenues == null || dailyRevenues.isEmpty()) { + throw new HandleException("按日预估营业额不能为空"); + } + if (dailyRevenues.size() != expectedDayCount) { + throw new HandleException( + "按日预估营业额条数须等于排班区间自然日数(期望 " + expectedDayCount + " 条)"); + } + + Map revenueByDate = new LinkedHashMap<>(); + Set seen = new HashSet<>(); + for (PreScheduleDayRevenueDto item : dailyRevenues) { + if (item == null) { + throw new HandleException("按日预估营业额存在空项"); + } + LocalDate day = parseDate(item.getDate(), "dailyRevenues.date"); + if (day.isBefore(start) || day.isAfter(end)) { + throw new HandleException("按日预估营业额日期须落在排班区间内:" + day); + } + if (!seen.add(day)) { + throw new HandleException("按日预估营业额日期重复:" + day); + } + if (item.getRevenue() == null) { + throw new HandleException("按日预估营业额不能为空:" + day); + } + revenueByDate.put(day, item.getRevenue()); + } + + for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) { + if (!revenueByDate.containsKey(d)) { + throw new HandleException("缺少排班日预估营业额:" + d); + } + } + return revenueByDate; + } + + public static void assertAttendanceGroupExists(String groupId, AttendanceGroupMapper attendanceGroupMapper) + throws QueryException { + AttendanceGroup group = attendanceGroupMapper.selectById(groupId); + if (group == null || (group.getDeleteMark() != null && group.getDeleteMark() != 0)) { + throw new QueryException("考勤组不存在或已删除"); + } + } + + /** + * 校验通过后的查询上下文。 + */ + public static final class ValidatedPreScheduleQuery { + private String groupId; + private LocalDate startDate; + private LocalDate endDate; + private BigDecimal effTarget; + private Map revenueByDate = new HashMap<>(); + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public BigDecimal getEffTarget() { + return effTarget; + } + + public void setEffTarget(BigDecimal effTarget) { + this.effTarget = effTarget; + } + + public Map getRevenueByDate() { + return revenueByDate; + } + + public void setRevenueByDate(Map revenueByDate) { + this.revenueByDate = revenueByDate; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleRedisSupport.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleRedisSupport.java new file mode 100644 index 0000000..dcf5b32 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleRedisSupport.java @@ -0,0 +1,211 @@ +package jnpf.attendance.service.impl.preschedule; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedEmployeeView; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedShiftRef; +import jnpf.constants.RedisConstant; +import jnpf.exception.HandleException; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 预排班按员工排班结果 Redis 暂存。 + */ +@Component +public class PreScheduleRedisSupport { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + + /** 预排班 byEmployee 暂存过期时间(分钟)。 */ + public static final long PRE_SCHEDULE_CACHE_TTL_MINUTES = 10L; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 写入排班算法 {@code byEmployee} 列表(直接 JSON 序列化,不包装额外类型)。 + */ + public void saveByEmployee( + String groupId, + LocalDate startDate, + LocalDate endDate, + List byEmployee) { + String redisKey = resolveByEmployeeRedisKey(groupId, startDate, endDate); + List payload = byEmployee == null ? Collections.emptyList() : byEmployee; + stringRedisTemplate + .opsForValue() + .set( + redisKey, + JSON.toJSONString(payload), + PRE_SCHEDULE_CACHE_TTL_MINUTES, + TimeUnit.MINUTES); + } + + /** + * 读取缓存的 {@code byEmployee};不存在或已过期时返回空列表。 + */ + public List loadByEmployee( + String groupId, LocalDate startDate, LocalDate endDate) { + String redisKey = resolveByEmployeeRedisKey(groupId, startDate, endDate); + String json = stringRedisTemplate.opsForValue().get(redisKey); + if (StringUtil.isBlank(json)) { + return Collections.emptyList(); + } + return parseByEmployeeJson(json); + } + + /** + * 读取并校验 {@code byEmployee} 缓存必须存在(提交保存前须先执行预排班生成)。 + */ + public List requireByEmployee( + String groupId, LocalDate startDate, LocalDate endDate) throws HandleException { + List cached = loadByEmployee(groupId, startDate, endDate); + if (cached.isEmpty()) { + throw new HandleException("排班数据已失效,请重新执行预排班。"); + } + return cached; + } + + /** + * 完整 Redis key(租户 + 考勤组 + 排班区间)。 + */ + public static String resolveByEmployeeRedisKey( + String tenantId, String groupId, LocalDate startDate, LocalDate endDate) { + return String.format(RedisConstant.ATTENDANCE_SMART_PRE_SCHEDULE_BY_EMPLOYEE_PREFIX, tenantId, formatRedisKeySuffix(groupId, startDate, endDate)); + } + + /** + * 除固定前缀与租户外的 key 片段:{@code 考勤组:yyyy-MM-dd_yyyy-MM-dd},供前端后续确认落库时回传。 + */ + public static String formatRedisKeySuffix( + String groupId, LocalDate startDate, LocalDate endDate) { + String dateRangeKey = PreScheduleSaveValidator.formatDateRangeKey(startDate, endDate); + return groupId + ":" + dateRangeKey; + } + + private String resolveByEmployeeRedisKey(String groupId, LocalDate startDate, LocalDate endDate) { + return resolveByEmployeeRedisKey( + UserProvider.getUser().getTenantId(), groupId, startDate, endDate); + } + + private static List parseByEmployeeJson(String json) { + JSONArray arr = JSON.parseArray(json); + if (arr == null || arr.isEmpty()) { + return Collections.emptyList(); + } + List list = new ArrayList<>(arr.size()); + for (int i = 0; i < arr.size(); i++) { + JSONObject empObj = arr.getJSONObject(i); + if (empObj == null) { + continue; + } + String userId = empObj.getString("userId"); + String userName = empObj.getString("userName"); + JSONArray shiftsArr = empObj.getJSONArray("shifts"); + List shifts = parseShifts(shiftsArr); + list.add(new AssignedEmployeeView(userId, userName, shifts)); + } + return list; + } + + private static List parseShifts(JSONArray shiftsArr) { + if (shiftsArr == null || shiftsArr.isEmpty()) { + return Collections.emptyList(); + } + List shifts = new ArrayList<>(shiftsArr.size()); + for (int j = 0; j < shiftsArr.size(); j++) { + JSONObject shiftObj = shiftsArr.getJSONObject(j); + if (shiftObj == null) { + continue; + } + LocalDate scheduleDay = parseLocalDate(shiftObj.get("scheduleDay")); + if (scheduleDay == null) { + continue; + } + shifts.add( + new AssignedShiftRef( + scheduleDay, + shiftObj.getString("shiftId"), + shiftObj.getString("timeRangeText"), + shiftObj.getBooleanValue("fixedScheduling"), + shiftObj.getString("postId"))); + } + return shifts; + } + + private static LocalDate parseLocalDate(Object raw) { + if (raw == null) { + return null; + } + if (raw instanceof LocalDate) { + return (LocalDate) raw; + } + if (raw instanceof String) { + String text = ((String) raw).trim(); + if (text.isEmpty()) { + return null; + } + try { + return LocalDate.parse(text, DATE_FMT); + } catch (DateTimeParseException ex) { + return null; + } + } + if (raw instanceof JSONObject) { + JSONObject o = (JSONObject) raw; + Integer year = o.getInteger("year"); + Integer month = o.getInteger("monthValue"); + if (month == null) { + month = o.getInteger("month"); + } + Integer day = o.getInteger("dayOfMonth"); + if (day == null) { + day = o.getInteger("day"); + } + if (year != null && month != null && day != null) { + return LocalDate.of(year, month, day); + } + } + return null; + } + + /** + * 按租户 + 草稿 id 读取 {@link ShiftPlanAssignmentResult#getByEmployee()} 序列化结果。 + * + *

Redis 值须为 {@link AssignedEmployeeView} JSON 数组;key 不存在或解析失败时返回空列表。 + * 若仍为旧版 {@link PreScheduleTableVo},则只读转换为 byEmployee(不写 Redis)。 + */ + public List getByEmployeeByDraftId(String draftId) { + if (StringUtil.isEmpty(draftId)) { + return Collections.emptyList(); + } + String tenantId = UserProvider.getUser().getTenantId(); + String redisKey = String.format(RedisConstant.ATTENDANCE_SMART_PRE_SCHEDULE_BY_EMPLOYEE_PREFIX, tenantId, draftId); + String json = stringRedisTemplate.opsForValue().get(redisKey); + if (StringUtil.isEmpty(json)) { + return Collections.emptyList(); + } + String trimmed = json.trim(); + if (trimmed.startsWith("[")) { + List list = JSON.parseArray(trimmed, AssignedEmployeeView.class); + return list == null ? Collections.emptyList() : list; + } + + return Collections.emptyList(); + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleResultMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleResultMapper.java new file mode 100644 index 0000000..2d17ef8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleResultMapper.java @@ -0,0 +1,448 @@ +package jnpf.attendance.service.impl.preschedule; + +import jnpf.attendance.schedule.ShiftPlanAssignmentResult; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedPostStaffView; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedShiftBlockView; +import jnpf.attendance.schedule.ShiftPlanAssignmentResult.AssignedStaffRef; +import jnpf.model.attendance.vo.WorkstationGroupUserVo; +import jnpf.model.attendance.vo.WorkstationWithUsersVo; +import jnpf.model.attendance.vo.scheduling.PreSchedulePostVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleStationVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableRowVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleUserVo; +import jnpf.util.StringUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * 将 {@link ShiftPlanAssignmentResult} 映射为预排班主表 VO。 + */ +public final class PreScheduleResultMapper { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + private static final DateTimeFormatter WEEK_FMT = + DateTimeFormatter.ofPattern("EEEE", Locale.CHINA); + + private PreScheduleResultMapper() { + } + + public static PreScheduleTableVo toTableVo( + String groupId, + LocalDate startDate, + LocalDate endDate, + Map revenueByDate, + BigDecimal effTarget, + ShiftPlanAssignmentResult assignmentResult, + List workstations) { + PreScheduleTableVo vo = new PreScheduleTableVo(); + vo.setGroupId(groupId); + + Map positionNameByPostId = indexPositionNameByPostId(workstations); + Map> userWorkstationIds = indexUserWorkstationIds(workstations); + Map workstationById = indexWorkstationById(workstations); + + List rows = new ArrayList<>(); + for (LocalDate day = startDate; !day.isAfter(endDate); day = day.plusDays(1)) { + rows.add( + toRow( + day, + revenueByDate.get(day), + effTarget, + assignmentResult, + positionNameByPostId, + userWorkstationIds, + workstationById)); + } + vo.setRows(rows); + filterPostsAndStationsWithoutAnyUsers(vo); + return vo; + } + + /** + * 剔除在整个日期区间内从未出现过人员的岗位 / 工作站;若仅在部分日期有人员、其余日期无人员或不在当行列表中,则保留。 + */ + static void filterPostsAndStationsWithoutAnyUsers(PreScheduleTableVo vo) { + if (vo == null || vo.getRows() == null || vo.getRows().isEmpty()) { + return; + } + + Set postIdsWithUsers = new HashSet<>(); + Set stationIdsWithUsers = new HashSet<>(); + for (PreScheduleTableRowVo row : vo.getRows()) { + collectPostIdsWithUsers(row.getPosts(), postIdsWithUsers); + collectStationIdsWithUsers(row.getStations(), stationIdsWithUsers); + } + + for (PreScheduleTableRowVo row : vo.getRows()) { + row.setPosts(filterPostsByIdsWithUsers(row.getPosts(), postIdsWithUsers)); + row.setStations(filterStationsByIdsWithUsers(row.getStations(), stationIdsWithUsers)); + } + } + + private static void collectPostIdsWithUsers(List posts, Set idsWithUsers) { + if (posts == null) { + return; + } + for (PreSchedulePostVo post : posts) { + if (post == null || StringUtil.isBlank(post.getId()) || !hasUsers(post.getUsers())) { + continue; + } + idsWithUsers.add(post.getId().trim()); + } + } + + private static void collectStationIdsWithUsers( + List stations, Set idsWithUsers) { + if (stations == null) { + return; + } + for (PreScheduleStationVo station : stations) { + if (station == null || StringUtil.isBlank(station.getId()) || !hasUsers(station.getUsers())) { + continue; + } + idsWithUsers.add(station.getId().trim()); + } + } + + private static List filterPostsByIdsWithUsers( + List posts, Set idsWithUsers) { + if (posts == null || posts.isEmpty()) { + return Collections.emptyList(); + } + List filtered = new ArrayList<>(); + for (PreSchedulePostVo post : posts) { + if (post == null || StringUtil.isBlank(post.getId())) { + continue; + } + if (idsWithUsers.contains(post.getId().trim())) { + filtered.add(post); + } + } + return filtered; + } + + private static List filterStationsByIdsWithUsers( + List stations, Set idsWithUsers) { + if (stations == null || stations.isEmpty()) { + return Collections.emptyList(); + } + List filtered = new ArrayList<>(); + for (PreScheduleStationVo station : stations) { + if (station == null || StringUtil.isBlank(station.getId())) { + continue; + } + if (idsWithUsers.contains(station.getId().trim())) { + filtered.add(station); + } + } + return filtered; + } + + private static boolean hasUsers(List users) { + if (users == null || users.isEmpty()) { + return false; + } + for (PreScheduleUserVo user : users) { + if (user != null && StringUtil.isNotBlank(user.getId())) { + return true; + } + } + return false; + } + + private static PreScheduleTableRowVo toRow( + LocalDate day, + BigDecimal revenue, + BigDecimal effTarget, + ShiftPlanAssignmentResult assignmentResult, + Map positionNameByPostId, + Map> userWorkstationIds, + Map workstationById) { + PreScheduleTableRowVo row = new PreScheduleTableRowVo(); + row.setDate(day.format(DATE_FMT)); + row.setWeek(day.format(WEEK_FMT)); + row.setRevenue(revenue); + row.setEffTarget(effTarget); + + List dayShifts = filterShiftsByDay(assignmentResult, day); + Map userNameById = indexUserNames(dayShifts); + Set distinctEmployeeIds = new HashSet<>(userNameById.keySet()); + row.setPosts(buildPosts(dayShifts, positionNameByPostId)); + row.setStations(buildStations(distinctEmployeeIds, userNameById, userWorkstationIds, workstationById)); + enrichEfficiencyMetrics(row, distinctEmployeeIds.size()); + return row; + } + + private static List filterShiftsByDay( + ShiftPlanAssignmentResult result, LocalDate day) { + if (result == null || result.getByShift() == null || result.getByShift().isEmpty()) { + return Collections.emptyList(); + } + List list = new ArrayList<>(); + for (AssignedShiftBlockView shift : result.getByShift()) { + if (shift != null && day.equals(shift.getScheduleDay())) { + list.add(shift); + } + } + return list; + } + + private static Map indexUserNames(List dayShifts) { + Map map = new HashMap<>(); + for (AssignedShiftBlockView shift : dayShifts) { + for (AssignedPostStaffView post : shift.getPostAssignments()) { + for (AssignedStaffRef staff : post.getStaff()) { + if (staff == null || StringUtil.isBlank(staff.getUserId())) { + continue; + } + String uid = staff.getUserId().trim(); + map.putIfAbsent(uid, resolveUserName(staff)); + } + } + } + return map; + } + + private static List buildPosts( + List dayShifts, Map positionNameByPostId) { + Map postById = new TreeMap<>(); + + for (AssignedShiftBlockView shift : dayShifts) { + for (AssignedPostStaffView postView : shift.getPostAssignments()) { + String postId = postView.getPostId(); + if (StringUtil.isBlank(postId)) { + continue; + } + String key = postId.trim(); + PreSchedulePostVo post = + postById.computeIfAbsent( + key, + id -> { + PreSchedulePostVo p = new PreSchedulePostVo(); + p.setId(id); + String name = positionNameByPostId.get(id); + if (StringUtil.isBlank(name)) { + name = postView.getPositionName(); + } + p.setName(StringUtil.isNotBlank(name) ? name : id); + p.setUsers(new ArrayList<>()); + return p; + }); + mergeStaffIntoPost(post, postView.getStaff()); + } + } + + List posts = new ArrayList<>(postById.values()); + for (PreSchedulePostVo post : posts) { + List users = dedupeAndSortUsers(post.getUsers()); + post.setUsers(users); + post.setCount(users.size()); + } + return posts; + } + + private static void mergeStaffIntoPost(PreSchedulePostVo post, List staffList) { + if (staffList == null || staffList.isEmpty()) { + return; + } + List users = post.getUsers(); + if (users == null) { + users = new ArrayList<>(); + post.setUsers(users); + } + Set existing = new HashSet<>(); + for (PreScheduleUserVo u : users) { + if (u != null && StringUtil.isNotBlank(u.getId())) { + existing.add(u.getId().trim()); + } + } + for (AssignedStaffRef staff : staffList) { + if (staff == null || StringUtil.isBlank(staff.getUserId())) { + continue; + } + String uid = staff.getUserId().trim(); + if (!existing.add(uid)) { + continue; + } + PreScheduleUserVo user = new PreScheduleUserVo(); + user.setId(uid); + user.setName(resolveUserName(staff)); + users.add(user); + } + } + + private static List buildStations( + Set assignedUserIds, + Map userNameById, + Map> userWorkstationIds, + Map workstationById) { + if (assignedUserIds == null || assignedUserIds.isEmpty()) { + return Collections.emptyList(); + } + Map stationById = new HashMap<>(); + + for (String userId : assignedUserIds) { + List wsIds = userWorkstationIds.get(userId); + if (wsIds == null || wsIds.isEmpty()) { + continue; + } + for (String wsId : wsIds) { + WorkstationWithUsersVo ws = workstationById.get(wsId); + if (ws == null) { + continue; + } + PreScheduleStationVo station = + stationById.computeIfAbsent( + wsId, + id -> { + PreScheduleStationVo s = new PreScheduleStationVo(); + s.setId(id); + s.setName( + StringUtil.isNotBlank(ws.getWorkstationName()) + ? ws.getWorkstationName() + : id); + s.setUsers(new ArrayList<>()); + return s; + }); + addUserToStation(station, userId, userNameById.get(userId)); + } + } + + List stations = new ArrayList<>(stationById.values()); + for (PreScheduleStationVo station : stations) { + station.setUsers(dedupeAndSortUsers(station.getUsers())); + } + stations.sort(Comparator.comparing(PreScheduleStationVo::getId, Comparator.nullsLast(String::compareTo))); + return stations; + } + + private static void addUserToStation(PreScheduleStationVo station, String userId, String userName) { + List users = station.getUsers(); + if (users == null) { + users = new ArrayList<>(); + station.setUsers(users); + } + for (PreScheduleUserVo existing : users) { + if (existing != null && userId.equals(existing.getId())) { + return; + } + } + PreScheduleUserVo user = new PreScheduleUserVo(); + user.setId(userId); + user.setName(StringUtil.isNotBlank(userName) ? userName : userId); + users.add(user); + } + + private static void enrichEfficiencyMetrics(PreScheduleTableRowVo row, int headcount) { + row.setHeadcount(headcount); + BigDecimal revenue = row.getRevenue(); + BigDecimal effTarget = row.getEffTarget(); + if (headcount <= 0) { + BigDecimal effEst = BigDecimal.ZERO; + row.setEffEst(effEst); + row.setEffDiff(effTarget != null ? effTarget.subtract(effEst) : BigDecimal.ZERO.subtract(effEst)); + return; + } + if (revenue == null) { + row.setEffEst(BigDecimal.ZERO); + row.setEffDiff(null); + return; + } + BigDecimal effEst = revenue.divide(BigDecimal.valueOf(headcount), 2, RoundingMode.HALF_UP); + row.setEffEst(effEst); + row.setEffDiff(effTarget != null ? effTarget.subtract(effEst) : null); + } + + private static List dedupeAndSortUsers(List users) { + if (users == null || users.isEmpty()) { + return Collections.emptyList(); + } + Map map = new TreeMap<>(); + for (PreScheduleUserVo u : users) { + if (u == null || StringUtil.isBlank(u.getId())) { + continue; + } + map.putIfAbsent(u.getId().trim(), u); + } + return new ArrayList<>(map.values()); + } + + private static String resolveUserName(AssignedStaffRef staff) { + if (StringUtil.isNotBlank(staff.getUserName())) { + return staff.getUserName().trim(); + } + return staff.getUserId(); + } + + private static Map indexPositionNameByPostId(List workstations) { + Map map = new HashMap<>(); + if (workstations == null) { + return map; + } + for (WorkstationWithUsersVo ws : workstations) { + if (ws == null || StringUtil.isBlank(ws.getPositionId())) { + continue; + } + String postId = ws.getPositionId().trim(); + map.putIfAbsent(postId, ws.getPositionName()); + } + return map; + } + + private static Map indexWorkstationById( + List workstations) { + Map map = new HashMap<>(); + if (workstations == null) { + return map; + } + for (WorkstationWithUsersVo ws : workstations) { + if (ws == null || StringUtil.isBlank(ws.getWorkstationId())) { + continue; + } + map.put(ws.getWorkstationId().trim(), ws); + } + return map; + } + + /** + * userId → 所属工作站 ID 列表(员工可在多站出现)。 + */ + private static Map> indexUserWorkstationIds(List workstations) { + Map> map = new HashMap<>(); + if (workstations == null) { + return map; + } + for (WorkstationWithUsersVo ws : workstations) { + if (ws == null || StringUtil.isBlank(ws.getWorkstationId()) || ws.getUserList() == null) { + continue; + } + String wsId = ws.getWorkstationId().trim(); + for (WorkstationGroupUserVo u : ws.getUserList()) { + if (u == null || StringUtil.isBlank(u.getUserId())) { + continue; + } + String uid = u.getUserId().trim(); + map.computeIfAbsent(uid, k -> new ArrayList<>()); + List ids = map.get(uid); + if (!ids.contains(wsId)) { + ids.add(wsId); + } + } + } + return map; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleSaveValidator.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleSaveValidator.java new file mode 100644 index 0000000..df3c022 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/attendance/service/impl/preschedule/PreScheduleSaveValidator.java @@ -0,0 +1,122 @@ +package jnpf.attendance.service.impl.preschedule; + +import jnpf.attendance.mapper.AttendanceGroupMapper; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableRowVo; +import jnpf.model.attendance.vo.scheduling.PreScheduleTableVo; +import jnpf.util.StringUtil; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; + +/** + * 预排班主表提交保存请求校验。 + */ +public final class PreScheduleSaveValidator { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + + private PreScheduleSaveValidator() { + } + + public static ValidatedPreScheduleSave validate( + PreScheduleTableVo vo, AttendanceGroupMapper attendanceGroupMapper) + throws HandleException, QueryException { + if (vo == null) { + throw new HandleException("请求体不能为空"); + } + String groupId = vo.getGroupId(); + if (StringUtil.isBlank(groupId)) { + throw new HandleException("考勤组ID不能为空"); + } + PreScheduleQueryValidator.assertAttendanceGroupExists(groupId.trim(), attendanceGroupMapper); + + List rows = vo.getRows(); + if (rows == null || rows.isEmpty()) { + throw new HandleException("主表行不能为空"); + } + + LocalDate startDate = null; + LocalDate endDate = null; + for (PreScheduleTableRowVo row : rows) { + if (row == null) { + throw new HandleException("主表行存在空项"); + } + LocalDate day = parseDate(row.getDate(), "rows.date"); + if (startDate == null || day.isBefore(startDate)) { + startDate = day; + } + if (endDate == null || day.isAfter(endDate)) { + endDate = day; + } + } + + ValidatedPreScheduleSave validated = new ValidatedPreScheduleSave(); + validated.setGroupId(groupId.trim()); + validated.setStartDate(startDate); + validated.setEndDate(endDate); + validated.setTable(vo); + return validated; + } + + /** 排班区间键片段:{@code yyyy-MM-dd_yyyy-MM-dd}。 */ + public static String formatDateRangeKey(LocalDate startDate, LocalDate endDate) { + return startDate.format(DATE_FMT) + "_" + endDate.format(DATE_FMT); + } + + private static LocalDate parseDate(String text, String fieldLabel) throws HandleException { + if (StringUtil.isBlank(text)) { + throw new HandleException(fieldLabel + "不能为空"); + } + try { + return LocalDate.parse(text.trim(), DATE_FMT); + } catch (DateTimeParseException ex) { + throw new HandleException(fieldLabel + "格式须为 yyyy-MM-dd"); + } + } + + /** + * 保存校验通过后的上下文。 + */ + public static final class ValidatedPreScheduleSave { + private String groupId; + private LocalDate startDate; + private LocalDate endDate; + private PreScheduleTableVo table; + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public PreScheduleTableVo getTable() { + return table; + } + + public void setTable(PreScheduleTableVo table) { + this.table = table; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionFunctionMenuController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionFunctionMenuController.java new file mode 100644 index 0000000..f27328e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionFunctionMenuController.java @@ -0,0 +1,125 @@ +package jnpf.authority.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.authority.service.FtbPermissionFunctionMenuService; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.model.module.MenuListVO; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.authority.dto.menu.FtbPermissionFunctionMenuDTO; +import jnpf.model.authority.dto.menu.FtbPermissionMenuDirectoryDTO; +import jnpf.model.authority.dto.menu.FunctionMenuRemoteDTO; +import jnpf.model.authority.dto.menu.FuntionMenuDTO; +import jnpf.model.authority.po.FtbPermissionFunctionMenu; +import jnpf.model.authority.vo.menu.FtbPermissionFunctionMenuVO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuConfigTreeVO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuDirectoryVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.util.NoDataSourceBind; +import lombok.SneakyThrows; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web权限管理功能菜单配置模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/permission-function-menu") +public class FtbPermissionFunctionMenuController { + /** + * 服务对象 + */ + @Resource + private FtbPermissionFunctionMenuService ftbPermissionFunctionMenuService; + + + /** + * 获取菜单列表 + * @param category web/app/PC + * @param keyword 关键字 + */ + @RequestMapping("/getAListOfMenus") + public ActionResult> getAListOfMenus(String category, String keyword) { + return ActionResult.success(ftbPermissionFunctionMenuService.getAListOfMenus(category,keyword)); + } + /** + * 角色管理获取菜单目录列表 + * @param roleId 编辑是需要携带roleId查询回显 + */ + @GetMapping("/getAListOfMenuDirectory") + public ActionResult> getAListOfMenuDirectory(String roleId) { + return ActionResult.success(ftbPermissionFunctionMenuService.getAListOfMenuDirectory(roleId)); + } + /** + * 获取功能菜单和按钮配置 + */ + @GetMapping("/getAListOfFunctionMenuConfigurations") + public ActionResult> getAListOfFunctionMenuConfigurations(FtbPermissionMenuDirectoryDTO dto) { + return ActionResult.success(ftbPermissionFunctionMenuService.getAListOfFunctionMenuConfigurations(dto)); + } + /** + * 查询子页面列表 + * @param menuId 菜单id + * + */ + @GetMapping("/getAListOfSubPages") + public ActionResult> getAListOfSubPages(String menuId, CultivatePage page) { + return ActionResult.success(ftbPermissionFunctionMenuService.getAListOfSubPages(menuId,page)); + } + /** + * 查询子页面按钮 + * @param subPageId 子页面id + */ + @GetMapping("/querySubpageButton") + public ActionResult> querySubpageButton(@RequestParam("subPageId") String subPageId) { + return ActionResult.success(ftbPermissionFunctionMenuService.querySubpageButton(subPageId)); + } + /** + * 添加子页面/按钮 + */ + @PostMapping("/addAListOfSubPages") + public ActionResult addAListOfSubPages(@Validated @RequestBody FtbPermissionFunctionMenuDTO dto) { + ftbPermissionFunctionMenuService.addAListOfSubPages(dto); + return ActionResult.success("保存成功"); + } + + /** + * 编辑子页面/按钮 + */ + @PutMapping("/updateAListOfSubPages") + public ActionResult updateAListOfSubPages(@Validated @RequestBody FtbPermissionFunctionMenuDTO dto) { + ftbPermissionFunctionMenuService.updateAListOfSubPages(dto); + return ActionResult.success("保存成功"); + } + /** + * 删除子页面/按钮 + */ + @DeleteMapping("/deleteAListOfSubPages/{id}") + public ActionResult deleteAListOfSubPages(@PathVariable String id) { + ftbPermissionFunctionMenuService.removeById(id); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionFunctionMenu::getParentId, id); + ftbPermissionFunctionMenuService.remove(queryWrapper); + return ActionResult.success("删除成功"); + } + + /** + * 远程client + * 包含新增修改 + */ + @SneakyThrows + @PostMapping("/remoteClient") + @NoDataSourceBind + public void remoteClient(FunctionMenuRemoteDTO dto) { + TenantDataSourceUtil.switchTenant(dto.getTenantId()); + ftbPermissionFunctionMenuService.saveOrUpdateBatch(dto.getFunctionMenuList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionGradesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionGradesController.java new file mode 100644 index 0000000..31b05af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionGradesController.java @@ -0,0 +1,35 @@ +package jnpf.authority.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.authority.service.FtbPermissionGradesService; +import jnpf.base.ActionResult; +import jnpf.permission.vo.v2.grades.GradeNodeVO; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 带权限的职级 + * + * @author Flynn Chan + * @create 2025-05-07 + */ +@RestController +@RequestMapping("/permission/grades") +@Slf4j +public class FtbPermissionGradesController { + @Resource + private FtbPermissionGradesService gradesService; + + @Operation(summary = "[树]权限过滤职级树(职类,职级别,职级)") + @GetMapping(value = "/tree") + @NoDataSourceBind + public ActionResult> gradeTree() { + return ActionResult.success(gradesService.gradeTree()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionMigrateController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionMigrateController.java new file mode 100644 index 0000000..b3cecd6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionMigrateController.java @@ -0,0 +1,121 @@ +package jnpf.authority.controller; + +import com.google.common.collect.ImmutableMap; +import jnpf.authority.service.FtbPermissionMigrateService; +import jnpf.base.ActionResult; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.authority.dto.menu.FtbPermissionMigrareRoleAddDTO; +import jnpf.model.authority.dto.role.FtbPermissionPositionAuthorizationDTO; +import jnpf.model.authority.dto.role.FtbPermissionPositionMenuDTO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuDirectoryVO; +import jnpf.model.authority.vo.menu.FtbPermissionModuleInfoVO; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; +import jnpf.util.AuthUtil; +import jnpf.util.Constants; +import jnpf.util.NoDataSourceBind; +import jnpf.utils.FeignHolder; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * 权限迁移模块 + * + * @author wangchunxiang + * @date 2025/08/22 + */ +@RestController +@RequestMapping("/permission/migrate") +public class FtbPermissionMigrateController { + + @Resource + private FtbPermissionMigrateService ftbPermissionMigrateService; + + + /** + * 查询岗位权限菜单 + * + * @return {@link ActionResult }<{@link List }<{@link FtbPermissionPositionMenuVO }>> + */ + @GetMapping("/position-menu") + @NoDataSourceBind + public ActionResult> getPositionMenuByTenantAndModule(FtbPermissionPositionMenuDTO positionMenuDTO) + throws LoginException { + try { + String loginTempUserToken = AuthUtil.loginTempUser("349057407209541", positionMenuDTO.getTenantId()); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), loginTempUserToken); + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(positionMenuDTO.getTenantId()); + List results = ftbPermissionMigrateService.getPositionMenuByTenantAndModule(positionMenuDTO); + return ActionResult.success(results); + } finally { + FeignHolder.clear(); + } + } + + /** + * 同步岗位权限授权 + */ + @PostMapping("/authority/sync-position-authorization") + @NoDataSourceBind + public ActionResult syncPositionAuthorization(@Validated @RequestBody FtbPermissionPositionAuthorizationDTO authorizationDTO) + throws LoginException { + TenantDataSourceUtil.switchTenant(authorizationDTO.getTenantId()); + ftbPermissionMigrateService.syncPositionAuthorization(authorizationDTO); + return ActionResult.success(); + } + /** + * 根据租户id查询角色列表 + */ + @GetMapping("/get-role-list") + @NoDataSourceBind + public ActionResult>> getRoleList(@RequestParam String tenantId) { + return ActionResult.success(ftbPermissionMigrateService.getRoleList(tenantId)); + } + + /** + * 新增角色 + */ + @PostMapping("/add-role") + @NoDataSourceBind + public ActionResult addRole(@RequestBody @Validated FtbPermissionMigrareRoleAddDTO ftbPermissionRole) { + ftbPermissionMigrateService.addRole(ftbPermissionRole); + return ActionResult.success(); + } + /** + * 查询当前角色绑定1.0权限信息 + */ + @GetMapping("/query-role-permission") + @NoDataSourceBind + public ActionResult queryRolePermission(@RequestParam String roleId, + @RequestParam String tenantId) { + FtbPermissionModuleInfoVO ftbPermissionModuleVOList = ftbPermissionMigrateService.queryRolePermission(roleId, tenantId); + return ActionResult.success(ftbPermissionModuleVOList); + } + + /** + * 根据租户Id获取菜单目录 + * + * @param type 0岗位权限1角色权限 + * @param tenantId 租户Id + */ + @GetMapping("/get-menu-directory") + @NoDataSourceBind + public ActionResult> getMenuDirectory(@RequestParam String tenantId, @RequestParam Integer type) throws LoginException { + try { + String loginTempUserToken = AuthUtil.loginTempUser("349057407209541", tenantId); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), loginTempUserToken); + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantId); + List ftbPermissionMenuDirectoryVOList = ftbPermissionMigrateService.getAListOfMenuDirectory(type, tenantId); + return ActionResult.success(ftbPermissionMenuDirectoryVOList); + } finally { + FeignHolder.clear(); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionOrganizeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionOrganizeController.java new file mode 100644 index 0000000..9580614 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionOrganizeController.java @@ -0,0 +1,332 @@ +package jnpf.authority.controller; + +import cn.hutool.core.bean.BeanUtil; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.permission.dto.v2.organzie.QueryOrganizeNodeDTO; +import jnpf.permission.eum.v2.NodeTypeEnum; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 带权限的组织管理功能接口 + * + * @author Flynn Chan + * @create 2025-05-06 + */ +@RestController +@RequestMapping("/permission/organize") +@Slf4j +public class FtbPermissionOrganizeController { + + @Resource + private FtbPermissionOrganizeService ftbPermissionOrganizeService; + + @Autowired + private ConfigValueUtil configValueUtil; + + @Operation(summary = "[树形全]组织(公司,部门,门店,班主)+组织下的人[含人事离职的人]") + @PostMapping(value = "/tree/user") + public ActionResult> allOrganizeUsersTree(@RequestBody QueryOrganizeNodeDTO dto) { + return ActionResult.success(ftbPermissionOrganizeService.allOrganizeUsersTree(dto)); + } + + @Operation(summary = "[树形]人员权限过滤后的组织(公司,部门,门店,班主),人员") + @GetMapping(value = "/tree/users") + public ActionResult> listOrganizeTreeUsers(@RequestParam(value = "organizeCategoryEnums", required = false) List organizeCategoryEnums, + @RequestParam(value = "workStatusEnums", required = false) List workStatusEnums, + @RequestParam(value = "organizeKeyword", required = false) String organizeKeyword, + //是否包含人员 + @RequestParam(value = "withEmployee", required = false) Boolean withEmployee, + @RequestParam(value = "includeResigned", required = false) Boolean includeResigned, + HttpServletRequest request) { + //默认包含人员 + if (withEmployee == null) { + withEmployee = Boolean.TRUE; + } + //默认不包含离职人员 + if (includeResigned == null) { + includeResigned = Boolean.FALSE; + } + + List result = ftbPermissionOrganizeService.listOrganizeTreeUsers(organizeCategoryEnums, workStatusEnums, organizeKeyword, withEmployee, includeResigned); + + // 判断是否为原生端请求 + String clientType = request.getHeader("clientType"); + boolean isNativeClient = StringUtil.isNotEmpty(clientType) && + ("android".equalsIgnoreCase(clientType) || "ios".equalsIgnoreCase(clientType)); + + // 如果是原生端请求,过滤掉子组织不含门店的父级组织 + if (isNativeClient) { + result = filterOrganizesWithoutStore(result); + } + + return ActionResult.success(result); + } + + /** + * @param organizeCategoryEnums 组织类型 + * @param workStatusEnums 工作状态 + * @param withEmployee 包含人员 + * @param filterBindOtherStore 过滤绑定第三方信息门店 + * @param filterBindPayStore 过滤收银平台门店 + * @return 组织树(with auth) + */ + @Operation(summary = "[树形,滤掉没有的分支]人员权限过滤后的组织(公司,部门,门店,班主),人员") + @GetMapping(value = "/tree/users/filterNode") + public ActionResult> listOrganizeTreeUsersFilterNodeJson( + @RequestParam(value = "organizeCategoryEnums", required = false) List organizeCategoryEnums, + @RequestParam(value = "workStatusEnums", required = false) List workStatusEnums, + @RequestParam(value = "withEmployee", required = false) Boolean withEmployee, + @RequestParam(value = "filterBindOtherStore", required = false) Boolean filterBindOtherStore, + @RequestParam(value = "filterBindPayStore", required = false) Boolean filterBindPayStore + ) { + withEmployee = withEmployee == null ? Boolean.TRUE : withEmployee; + filterBindOtherStore = filterBindOtherStore == null ? Boolean.FALSE : filterBindOtherStore; + filterBindPayStore = filterBindPayStore == null ? Boolean.FALSE : filterBindPayStore; + return ActionResult.success(ftbPermissionOrganizeService. + listOrganizeTreeUsersFilterNodeJson( + organizeCategoryEnums, + workStatusEnums, + withEmployee, + filterBindOtherStore, + filterBindPayStore + )); + } + + /** + * @param organizeCategoryEnums 组织类型 + * @param workStatusEnums 工作状态 + * @param withEmployee 是否包含人员 + * @param filterBindOtherStore 过滤绑定第三方信息门店 + * @param filterBindPayStore 过滤收银平台门店 + * @return 组织树(with auth) + */ + @Operation(summary = "[api树形,滤掉没有的分支]人员权限过滤后的组织(公司,部门,门店,班主),默认不带人员") + @GetMapping(value = "/api/tree/users/filterNode") + public List listOrganizeTreeFilterNode( + @RequestParam(value = "organizeCategoryEnums", required = false) List organizeCategoryEnums, + @RequestParam(value = "workStatusEnums", required = false) List workStatusEnums, + @RequestParam(value = "withEmployee", required = false) Boolean withEmployee, + @RequestParam(value = "filterBindOtherStore", required = false) Boolean filterBindOtherStore, + @RequestParam(value = "filterBindPayStore", required = false) Boolean filterBindPayStore + ) { + withEmployee = withEmployee == null ? Boolean.FALSE : withEmployee; + filterBindOtherStore = filterBindOtherStore == null ? Boolean.FALSE : filterBindOtherStore; + filterBindPayStore = filterBindPayStore == null ? Boolean.FALSE : filterBindPayStore; + return ftbPermissionOrganizeService. + listOrganizeTreeUsersFilterNodeJson( + organizeCategoryEnums, + workStatusEnums, + withEmployee, + filterBindOtherStore, + filterBindPayStore + ); + } + + /** + * @param organizeCategoryEnums 组织类型 + * @param organizeKeyword 关键词 + * @param filterBindOtherStore 过滤绑定第三方信息门店 + * @param filterBindPayStore 过滤收银平台门店 + * @return 组织列表(with auth) + */ + @Operation(summary = "[列表]人员权限过滤后的组织列表") + @GetMapping(value = "/info/list/users") + @NoDataSourceBind + public ActionResult> authOrganizesByUserBound( + @RequestParam(value = "organizeCategoryEnums", required = false) List organizeCategoryEnums, + @RequestParam(value = "organizeKeyword", required = false) String organizeKeyword, + @RequestParam(value = "filterBindOtherStore", required = false) Boolean filterBindOtherStore, + @RequestParam(value = "filterBindPayStore", required = false) Boolean filterBindPayStore + ) { + filterBindOtherStore = filterBindOtherStore == null ? Boolean.FALSE : filterBindOtherStore; + filterBindPayStore = filterBindPayStore == null ? Boolean.FALSE : filterBindPayStore; + return ActionResult.success(ftbPermissionOrganizeService. + authOrganizesByUserBound( + organizeCategoryEnums, + organizeKeyword, + filterBindOtherStore, + filterBindPayStore + ) + ); + } + + @GetMapping(value = "/user-auth") + public TargetAuthIdsVO getUserAuth() { + + return ftbPermissionOrganizeService.getUserAuth(); + } + + /** + * @param userIds 用户列表 + * @param status 状态 1:禁用 0:启用 -1-所有 + * @return + */ + @Operation(summary = "[列表]根据用户id批量查询权限范围内的门店") + @PostMapping(value = "/batchForUserIds/{status}") + public Map> batchAuthOrganizesForUserIds(@RequestBody List userIds, @PathVariable("status") Integer status) { + return ftbPermissionOrganizeService.batchAuthOrganizesForUserIds(userIds, status); + } + + + @Operation(summary = "[列表]根据用户列表批量查询权限范围的门店") + @PostMapping(value = "/batchForUserIds/{status}/{moduleId}/{tenantId}") + @NoDataSourceBind + Map> batchAuthOrganizesForUserIdsAndTenantId(@RequestBody List userIds, @PathVariable("status") Integer status, @PathVariable("moduleId") String moduleId, @PathVariable("tenantId") String tenantId) { + checkOutTenant(tenantId); + return ftbPermissionOrganizeService.batchAuthOrganizesForUserIdsAndTenantId(userIds, status, moduleId, tenantId); + } + @Operation(summary = "[列表]根据用户列表批量查询权限范围的组织及门店") + @PostMapping(value = "/batchAllForUserIds/{status}/{moduleId}/{tenantId}") + @NoDataSourceBind + Map> batchAuthOrganizesAllForUserIdsAndTenantId(@RequestBody List userIds, @PathVariable("status") Integer status, @PathVariable("moduleId") String moduleId, @PathVariable("tenantId") String tenantId) { + checkOutTenant(tenantId); + return ftbPermissionOrganizeService.batchAuthOrganizesAllForUserIdsAndTenantId(userIds, status, moduleId, tenantId); + } + /** + * 批量查询权限范围组织及门店 + */ + @Operation(summary = "[列表]根据用户列表批量查询权限范围组织及门店") + @PostMapping(value = "/batchAllForUserIds") + public Map batchAuthOrganizesAllForUserIds(@RequestBody List userIds) { + return ftbPermissionOrganizeService.batchAuthOrganizesAllForUserIds(userIds); + } + + + /** + * api + **/ + @Operation(summary = "[门店基础信息列表auth api] 根据登录人权限得到权限范围内门店基础信息") + @GetMapping(value = "/list/store/login") + public List authStoreBaseListInfo() { + return ftbPermissionOrganizeService.authStoreBaseListInfo(); + } + + /** + * 过滤掉子组织不含门店的父级组织(仅用于原生端) + * + * @param organizeList 组织树列表 + * @return 过滤后的组织树列表 + */ + private List filterOrganizesWithoutStore(List organizeList) { + if (organizeList == null || organizeList.isEmpty()) { + return organizeList; + } + + List filteredList = new ArrayList<>(); + for (OrganizeManagerNodeVO organize : organizeList) { + // 递归检查该组织及其子组织是否包含门店 + if (hasStoreInChildren(organize)) { + // 如果包含门店,保留该组织,但需要递归过滤其子组织 + OrganizeManagerNodeVO filteredOrganize = new OrganizeManagerNodeVO(); + // 复制组织的基本属性 + copyOrganizeProperties(organize, filteredOrganize); + // 递归过滤子组织 + if (organize.getChildren() != null && !organize.getChildren().isEmpty()) { + filteredOrganize.setChildren(filterOrganizesWithoutStore(organize.getChildren())); + } + filteredList.add(filteredOrganize); + } + // 如果不包含门店,则过滤掉该组织及其所有子组织 + } + return filteredList; + } + + /** + * 递归检查组织及其子组织是否包含门店 + * + * @param organize 组织节点 + * @return 如果包含门店返回true,否则返回false + */ + private boolean hasStoreInChildren(OrganizeManagerNodeVO organize) { + if (organize == null) { + return false; + } + + // 如果当前节点是门店,直接返回true + if (organize.getNodeTypeEnum() != null && NodeTypeEnum.STORE.equals(organize.getNodeTypeEnum())) { + return true; + } + + // 如果当前节点是用户或其他非组织类型,返回false(用户节点不参与门店判断) + if (organize.getNodeTypeEnum() != null && + (NodeTypeEnum.USER.equals(organize.getNodeTypeEnum()) || + NodeTypeEnum.POSITION.equals(organize.getNodeTypeEnum()))) { + return false; + } + + // 递归检查子组织 + if (organize.getChildren() != null && !organize.getChildren().isEmpty()) { + for (OrganizeManagerNodeVO child : organize.getChildren()) { + if (hasStoreInChildren(child)) { + return true; + } + } + } + + return false; + } + + /** + * 复制组织的基本属性(不包含children) + * + * @param source 源组织 + * @param target 目标组织 + */ + private void copyOrganizeProperties(OrganizeManagerNodeVO source, OrganizeManagerNodeVO target) { + // 使用 BeanUtil 复制属性,但排除 children 字段 + BeanUtil.copyProperties(source, target, "children"); + } + + /** + * 根据租户ID检查或切换数据库连接 + * + * @param tenantId 租户ID + */ + public void checkOutTenant(String tenantId) { + // 判断是否启用了多租户功能 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + // 切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + // 获取当前用户信息 + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + // 设置数据库连接信息 + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionPositionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionPositionController.java new file mode 100644 index 0000000..b90915d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionPositionController.java @@ -0,0 +1,58 @@ +package jnpf.authority.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.authority.service.FtbPermissionPositionService; +import jnpf.base.ActionResult; +import jnpf.model.authority.dto.permission.OrganizeWithPositionsDTO; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.organize.OrganizeAndPositionListVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.position.PositionListOrganizeUserVO; +import jnpf.permission.vo.v2.position.PositionListUserVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 带权限的岗位功能 + * + * @author Flynn Chan + * @create 2025-05-06 + */ +@RestController +@RequestMapping("/permission/position") +@Slf4j +public class FtbPermissionPositionController { + @Resource + private FtbPermissionPositionService ftbPermissionPositionService; + + @Operation(summary = "[列表]人员权限过滤后的岗位用户列表") + @GetMapping("/tree/users") + public ActionResult> authListPositionTreeUser(@RequestParam(value = "filterUserWorkStatusEnumsList", required = false) List filterUserWorkStatusEnumsList, + @RequestParam(value = "includeResigned", required = false) Boolean includeResigned) { + if (includeResigned == null) { + includeResigned = false; + } + return ActionResult.success(ftbPermissionPositionService.authListPositionTreeUser(filterUserWorkStatusEnumsList, includeResigned)); + } + + @Operation(summary = "[列表]人员权限过滤后的岗位列表") + @GetMapping("/list") + public ActionResult> authListPositionAuth() { + return ActionResult.success(ftbPermissionPositionService.authListPositionAuth()); + } + + @Operation(summary = "[列表-含离职]全岗位挂组织再挂用户列表") + @GetMapping("/List/organize/users") + public ActionResult> listPositionOrganizeUser(@RequestParam(value = "filterUserWorkStatusEnumsList", required = false) List filterUserWorkStatusEnumsList) { + return ActionResult.success(ftbPermissionPositionService.listPositionOrganizeUser(filterUserWorkStatusEnumsList)); + } + + @Operation(summary = "[api列表]权限范围内的多个组织及其岗位列表(不查人)") + @PostMapping("/api/list/auth/organize-with-positions") + public List authOrganizeWithPositions(@RequestBody OrganizeWithPositionsDTO dto){ + return ftbPermissionPositionService.authOrganizeWithPositions(dto); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePersonController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePersonController.java new file mode 100644 index 0000000..4b30b0b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePersonController.java @@ -0,0 +1,193 @@ +package jnpf.authority.controller; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.authority.service.FtbPermissionRoleAuthorizePersonService; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.authorize.FtbPermissionAuthorizeVO; +import jnpf.model.authority.dto.menu.FuntionMenuDTO; +import jnpf.model.authority.dto.person.FtbPermissionRoleBatchDeleteDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonAddDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonRelationDTO; +import jnpf.model.authority.vo.person.FtbAuthorizedPersonnelVO; +import jnpf.model.authority.vo.person.FtbEmployeePermissionPersonnelVO; +import jnpf.model.authority.vo.person.FtbRoleListDropDownVO; +import jnpf.model.authority.vo.person.FunctionVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.util.NoDataSourceBind; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * web权限管理角色人员授权模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/permission-role-authorize-person") +public class FtbPermissionRoleAuthorizePersonController { + + @Resource + private FtbPermissionRoleAuthorizePersonService ftbPermissionRoleAuthorizePersonService; + + @Resource + private PermissionsUtils permissionsUtils; + + /** + * 添加人员授权 + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Validated FtbPermissionRolePersonAddDTO rolePersonAddDTO) { + ftbPermissionRoleAuthorizePersonService.add(rolePersonAddDTO); + return ActionResult.success("操作成功"); + } + + /** + * 批量删除人员授权 + */ + @DeleteMapping("batch-delete") + public ActionResult batchDelete(@RequestBody @Validated FtbPermissionRoleBatchDeleteDTO rolePersonDeleteDTO) { + ftbPermissionRoleAuthorizePersonService.batchDelete(rolePersonDeleteDTO); + return ActionResult.success("操作成功"); + } + + /** + * 授权人员列表 + * + * @param roleId 角色主键id(必传) + */ + @GetMapping("/authorized-personnel") + public ActionResult> authorizedPersonnel(CultivatePage cultivatePage, @RequestParam(value = "roleId") String roleId) { + Page page = cultivatePage.coverCultivatePage(); + PageListVO result = ftbPermissionRoleAuthorizePersonService.authorizedPersonnel(page, roleId); + return ActionResult.success(result); + } + + /** + * 员工权限列表 + * + * @param userName 员工姓名 + */ + @GetMapping("/employee-permission") + public ActionResult> employeePermission(CultivatePage cultivatePage, String userName) { + Page page = cultivatePage.coverCultivatePage(); + PageListVO result = ftbPermissionRoleAuthorizePersonService.employeePermission(page, userName, null); + return ActionResult.success(result); + } + /** + * 添加个人权限 + */ + @PostMapping("/add-personal-permission") + public ActionResult addPersonalPermission(@RequestBody @Validated FtbPermissionRolePersonRelationDTO rolePersonAddDTO) { + ftbPermissionRoleAuthorizePersonService.addPersonalPermission(rolePersonAddDTO); + return ActionResult.success("操作成功"); + } + /** + * 根据userId查询个人权限 + */ + @GetMapping("/getPersonalPermission") + public ActionResult> getPersonalPermission(@RequestParam String userId) { + List list = ftbPermissionRoleAuthorizePersonService.getPersonalPermission(userId); + return ActionResult.success(list); + } + + /** + * 角色列表下拉 + */ + @GetMapping("/role-list-drop") + public ActionResult> roleListDropDown(@RequestParam String userId) { + List list = ftbPermissionRoleAuthorizePersonService.roleListDropDown(userId); + return ActionResult.success(list); + } + + /** + * 批量编辑员工角色权限 + * + * @param roleIds 角色主键id集合(必传) + */ + @PostMapping("/edit-employee-permission/{userId}") + public ActionResult editEmployeePermission(@RequestBody @Validated List roleIds, @PathVariable("userId") String userId) { + ftbPermissionRoleAuthorizePersonService.editEmployeePermission(roleIds, userId); + return ActionResult.success("操作成功"); + } + /** + * 根据userId返回角色未绑定userId集合 + * @param vo userId集合(必传) + */ + @PostMapping("/getRoleIdsByUserId") + public ActionResult> getRoleIdsByUserId(@RequestBody FtbPermissionAuthorizeVO vo) { + return ActionResult.success(ftbPermissionRoleAuthorizePersonService.getRoleIdsByUserId(vo)); + } + + /** + * 权限用户、组织、按钮等内容获取(内部测试) + */ + @GetMapping("/permission-user-test") + public List permissionUserTest(String userId, String permissionModule, String category, Integer type) { + if (type == 1) { + return permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, permissionModule, category); + } else if (type == 2) { + return permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userId, permissionModule, category); + } else { + return permissionsUtils.queryCollectionOfUserButtonPermissions(userId, null, category, permissionModule); + } + } + + /** + * 当前人员所有角色 + */ + @GetMapping("/allRolesOfTheCurrentPerson") + @NoDataSourceBind + public ActionResult> allRolesOfTheCurrentPerson(@RequestParam("userId") String userId, @RequestParam("tenantId") String tenantId) { + List list = ftbPermissionRoleAuthorizePersonService.allRolesOfTheCurrentPerson(userId, tenantId); + return ActionResult.success(list); + } + + /** + * 当前登录用户所有权限用户id + * @return 用户id集合 + */ + @GetMapping("/permission-user") + public ActionResult> permissionUser(@RequestParam("userId") String userId, @RequestParam("permissionModule") String permissionModule, @RequestParam("category") String category) { + List userIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, permissionModule, category); + return ActionResult.success(userIds); + } + /** + * 查询那些人员具有当前按钮权限 + */ + @PostMapping("/queryButtonPermission") + public List queryButtonPermission(@RequestBody FuntionMenuDTO funtion) { + try { + return ftbPermissionRoleAuthorizePersonService.queryButtonPermission(funtion); + } catch (Exception e) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + + /** + * 根据用户Id获取用户权限的用户Id集合 + * @return 用户id->所具有的权限用户id集合 + */ + @PostMapping("/collection-of-user") + public Map> getUserPermissionUserCollection(@RequestBody List userIds) { + if (CollUtil.isNotEmpty(userIds)) { + return new HashMap<>(); + } + Map> result = new HashMap<>(userIds.size()); + String moduleId = permissionsUtils.getRequestModule(); + for (String userId : userIds) { + List userIdDataPermissions = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, moduleId); + result.put(userId, userIdDataPermissions); + } + return result; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePostController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePostController.java new file mode 100644 index 0000000..3bfa03f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleAuthorizePostController.java @@ -0,0 +1,84 @@ +package jnpf.authority.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.authority.service.FtbPermissionRoleAuthorizePostService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.authorize.FtbPermissionAuthorizeVO; +import jnpf.model.authority.dto.person.FtbPermissionRoleBatchDeleteDTO; +import jnpf.model.authority.dto.post.FtbPermissionRolePostAddDTO; +import jnpf.model.authority.vo.post.FtbAuthorizedPostVO; +import jnpf.model.cultivate.CultivatePage; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web权限管理角色岗位授权模块 +* +* @author xxxxx +*/ +@RestController +@RequestMapping("/web/permission-role-authorize-post") +public class FtbPermissionRoleAuthorizePostController { + + @Resource + private FtbPermissionRoleAuthorizePostService ftbPermissionRoleAuthorizePostService; + + /** + * 添加岗位授权 + */ + @PostMapping("/add-post") + public ActionResult addPost(@RequestBody @Validated FtbPermissionRolePostAddDTO ftbPermissionRolePostAddDTO) { + ftbPermissionRoleAuthorizePostService.addPost(ftbPermissionRolePostAddDTO); + return ActionResult.success("操作成功"); + } + + /** + * 批量删除岗位授权 + */ + @DeleteMapping("batch-delete") + public ActionResult batchDelete(@RequestBody @Validated FtbPermissionRoleBatchDeleteDTO rolePersonDeleteDTO) { + ftbPermissionRoleAuthorizePostService.batchDelete(rolePersonDeleteDTO); + return ActionResult.success("操作成功"); + } + + /** + * 授权岗位列表 + * + * @param cultivatePage 培养页面 + * @param roleId 角色主键id(必传) + * @return {@link ActionResult }<{@link PageListVO }<{@link FtbAuthorizedPostVO }>> + */ + @GetMapping("/authorized-post") + public ActionResult> authorizePost(CultivatePage cultivatePage, @RequestParam(value = "roleId") String roleId) { + Page page = cultivatePage.coverCultivatePage(); + PageListVO result = ftbPermissionRoleAuthorizePostService.authorizePost(page, roleId); + return ActionResult.success(result); + } + + /** + * 根据 postId 返回角色未绑定 postId 集合 + * + */ + @PostMapping("/getRoleIdsByPostIds") + public ActionResult> getRoleIdsByPostIds(@RequestBody FtbPermissionAuthorizeVO vo) { + return ActionResult.success(ftbPermissionRoleAuthorizePostService.getRoleIdsByPostIds(vo)); + } + + /** + * 更具岗位id清除所有绑定的角色 + */ + @PostMapping("/clearRoleByPostId") + public void clearRoleByPostId(@RequestBody List postIds) { + ftbPermissionRoleAuthorizePostService.clearRoleByPostId(postIds); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleController.java new file mode 100644 index 0000000..044dbc3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionRoleController.java @@ -0,0 +1,238 @@ +package jnpf.authority.controller; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.handling.PermissionHandling; +import jnpf.authority.service.FtbPermissionRoleService; +import jnpf.base.ActionResult; +import jnpf.base.SystemApi; +import jnpf.base.UserInfo; +import jnpf.base.entity.SystemEntity; +import jnpf.base.model.module.ModuleModel; +import jnpf.base.vo.PageListVO; +import jnpf.model.UserMenuModel; +import jnpf.model.authority.dto.menu.FtbPermissionRoleAddDTO; +import jnpf.model.authority.dto.role.FtbPermissionDataDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleCopyDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleInfoDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleUpdateDTO; +import jnpf.model.authority.vo.role.FtbPermissionRoleDetailsVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.login.MenuTreeVO; +import jnpf.permission.AuthorizeApi; +import jnpf.permission.model.authorize.AuthorizeVO; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import jnpf.util.treeutil.SumTree; +import jnpf.util.treeutil.newtreeutil.TreeDotUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * web权限管理角色模块 +* +* @author xxxxx +*/ +@Slf4j +@RestController +@RequestMapping("/web/permission-role") +public class FtbPermissionRoleController { + + @Resource + private FtbPermissionRoleService ftbPermissionRoleService; + + @Autowired + private AuthorizeApi authorizeApi; + + @Autowired + private SystemApi systemApi; + + @Autowired + private PermissionHandling permissionHandling; + + /** + * 新增角色 + */ + @PostMapping("/add-role") + public ActionResult addRole(@RequestBody @Validated FtbPermissionRoleAddDTO ftbPermissionRole) { + ftbPermissionRoleService.addRole(ftbPermissionRole); + return ActionResult.success(); + } + + /** + * 编辑角色 + */ + @PutMapping("/edit-role") + public ActionResult editRole(@RequestBody @Validated FtbPermissionRoleUpdateDTO roleUpdateDTO) { + ftbPermissionRoleService.editRole(roleUpdateDTO); + return ActionResult.success(); + } + + /** + * 删除角色 + * + * @param id 角色主键Id必传 + */ + @DeleteMapping("/delete-role/{id}") + public ActionResult deleteRole(@PathVariable(value = "id") String id) { + ftbPermissionRoleService.deleteRole(id); + return ActionResult.success(); + } + + /** + * 角色列表 + * + * @return {@link ActionResult }<{@link PageListVO }<{@link FtbPermissionRoleInfoVO }>> + */ + @GetMapping("/role-list") + public ActionResult> permissionList(CultivatePage cultivatePage, + FtbPermissionRoleInfoDTO roleInfoDTO) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbPermissionRoleService.permissionList(page, roleInfoDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 员工已有的功能权限标识集合 + */ + @GetMapping("/permission-identification-collection") + public ActionResult permissionIdentificationCollection() { + FtbPermissionRoleIdentificationVO result = ftbPermissionRoleService.permissionIdentificationCollection(); + return ActionResult.success(result); + } + + /** + * 角色详情 + * + * @param roleId 角色ID + * @param menuWebId 菜单Web ID + * @param menuAppId 菜单APP ID + * @param menuPcId 菜单PC ID + * @return {@link ActionResult }<{@link FtbPermissionRoleInfoVO }> + */ + @GetMapping("/role-details") + public ActionResult roleDetails(String roleId, String menuWebId, String menuAppId, String menuPcId) { + FtbPermissionRoleDetailsVO ftbPermissionRole = ftbPermissionRoleService.roleDetails(roleId, menuWebId, menuAppId,menuPcId); + return ActionResult.success(ftbPermissionRole); + } + + /** + * 获取当前人员的数据权限范围 + * + * @return + */ + @GetMapping("/query-user-scope-permission") + public Map queryUserScopeOfPermission(@RequestParam("userId") String userId) { + return ftbPermissionRoleService.queryUserScopeOfPermissionNoTenant(userId); + } + + /** + * 复制角色 + */ + @PostMapping("/copy-role") + public ActionResult copyRole(@RequestBody @Validated FtbPermissionRoleCopyDTO ftbPermissionRole) { + ftbPermissionRoleService.copyRole(ftbPermissionRole); + return ActionResult.success(); + } + + /** + * 数据权限应用至其他模块 + */ + @PostMapping("/data-permission-application-to-other-modules") + public ActionResult dataPermissionApplicationToOtherModules(@RequestBody @Validated FtbPermissionDataDTO ftbPermissionDataDTO) { + ftbPermissionRoleService.dataPermissionApplicationToOtherModules(ftbPermissionDataDTO); + return ActionResult.success(); + } + + /** + * 根据token获取用户菜单列表 + * @return + */ + @GetMapping("/obtain-menus-on-token") + public ActionResult> obtainTheListOfUserMenusBasedOnTheToken(String type) { + type = StringUtils.isEmpty(type) ? "Web" : type; + AuthorizeVO authorizeModel = authorizeApi.getAuthorize(false); + // 获取菜单权限 + List moduleList = authorizeModel.getModuleList(); + String systemId = "App".equals(type) ? UserProvider.getUser().getAppSystemId() : UserProvider.getUser().getSystemId(); + boolean isNormalPlatform = true; + if (!"App".equals(type)) { + moduleList = moduleList.stream().filter(t -> t.getSystemId() != null && t.getSystemId().equals(systemId)).collect(Collectors.toList()); + // 判断是否为Web + // 判断是否需要切换系统 + if (moduleList.size() > 0) { + String systemId1 = moduleList.get(0).getSystemId(); + SystemEntity systemEntity = systemApi.getInfoById(systemId1); + if (systemEntity.getIsMain() == 1) { + isNormalPlatform = false; + } + if (systemEntity.getEnabledMark() == 0) { + moduleList = new ArrayList<>(); + } + } + } + String finalType = type; + List menuList = moduleList.stream().filter(t -> finalType.equals(t.getCategory())).sorted(Comparator.comparing(ModuleModel::getSortCode)).collect(Collectors.toList()); + UserInfo user = UserProvider.getUser(); + if (!user.getIsAdministrator()) { + // 登录时 实时查询用户当前的数据权限范围 + Set menuPermissions = ftbPermissionRoleService.queryTheLatestListOfMenuPermissions(user.getUserId()); + // 获取登陆时的缓存菜单 + if (CollUtil.isNotEmpty(menuPermissions) && isNormalPlatform) { + // 过滤权限菜单 + menuList = menuList.stream().filter(t -> menuPermissions.contains(t.getId())).collect(Collectors.toList()); + } + } + List menu = JsonUtil.getJsonToList(menuList, UserMenuModel.class); + List> menus = TreeDotUtils.convertListToTreeDot(menu, "-1"); + //返回前台tree的list + List list = JsonUtil.getJsonToList(menus, MenuTreeVO.class); + log.debug("用户菜单:{}", list); + return ActionResult.success(list); + } + + /** + * 根据模块编码获取当前人员的数据权限范围 + * + * @return + */ + @GetMapping("/query-user-scope-permission-for-module-code") + public InnerPowerUserVO queryUserScopeOfPermissionForModuleCode() { + InnerPowerUserVO vo = new InnerPowerUserVO(); + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + vo.setCode(0); + return vo; + } + try { + //往header中添加一个字段 + List userIds = permissionHandling.getUserIdsByUserId(userInfo.getUserId()); + // 权限适用范围为全部 + if (userIds == null) { + vo.setCode(0); + return vo; + } + if (userIds.isEmpty()) { + vo.setCode(2); + return vo; + } + vo.setCode(1); + vo.setUserIds(userIds); + } catch (Exception e) { + vo.setCode(2); + return vo; + } + return vo; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionUsersController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionUsersController.java new file mode 100644 index 0000000..2be38ca --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/controller/FtbPermissionUsersController.java @@ -0,0 +1,91 @@ +package jnpf.authority.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.authority.service.FtbPermissionUsersService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.permission.AuthGetTargetUserInfoBatchDTO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryPageUserMoreKeywordDTO; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.ListIdDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 带权限的用户管理 + * + * @author Flynn Chan + * @create 2025-05-21 + */ +@RestController +@RequestMapping("/permission/users") +@Slf4j +public class FtbPermissionUsersController { + + @Resource + private FtbPermissionUsersService ftbPermissionUsersService; + + @Operation(summary = "[分页] 在职人员列表") + @PostMapping("/page") + @NoDataSourceBind + public ActionResult> pagePost(@RequestBody QueryPageUserDTO dto) { + return ActionResult.success(ftbPermissionUsersService.pagePost(dto)); + } + + @Operation(summary = "[分页] 在职人员列表POST更多关键字信息") + @PostMapping("/page/moreKeyword") + @NoDataSourceBind + public ActionResult> pagePostMoreKeyword(@RequestBody QueryPageUserMoreKeywordDTO dto) { + return ActionResult.success(ftbPermissionUsersService.pagePostMoreKeyword(dto)); + } + + @Operation(summary = "[列表]权限过滤,获取所有用户信息,含绑定关系,或指定用户. 不使用权限则返回全部") + @PostMapping("/info/all/auth") + @NoDataSourceBind + public ActionResult> authGetAllUserInfoBatch() { + return ActionResult.success(ftbPermissionUsersService.authGetAllUserInfoBatch()); + } + + @Operation(summary = "[列表]权限范围内仅返回用户 id 列表,不查关系与 VO") + @PostMapping("/ids/auth") + @NoDataSourceBind + public ActionResult> authGetPermissionScopeUserIds() { + return ActionResult.success(ftbPermissionUsersService.authGetPermissionScopeUserIds()); + } + + @Operation(summary = "[列表] 指定组织下得所有员工信息(FTB带权限)") + @GetMapping("/list/targetOrganize/auth") + public ActionResult> listTargetOrganizeIdAuth(@RequestParam(value = "organizeId") String organizeId) { + return ActionResult.success(ftbPermissionUsersService.listTargetOrganizeIdAuth(organizeId, null)); + } + + @Operation(summary = "[列表] 指定组织下得所有员工信息(FTB带权限)") + @GetMapping("/list/targetOrganize/auth/api") + public List listTargetOrganizeIdAuthApi(@RequestParam(value = "organizeId") String organizeId, @RequestParam(value = "userWorkStatusEnumsList", required = false) List userWorkStatusEnumsList) { + return ftbPermissionUsersService.listTargetOrganizeIdAuth(organizeId, userWorkStatusEnumsList); + } + + @Operation(summary = "[列表] 指定组织集合下得所有员工信息(FTB带权限)") + @PostMapping("/list/targetOrganizeIds/auth/api") + public List listTargetOrganizeIdsAuthApi(@RequestBody List organizeIds, @RequestParam(value = "userWorkStatusEnumsList", required = false) List userWorkStatusEnumsList) { + return ftbPermissionUsersService.listTargetOrganizeIdsAuth(organizeIds, userWorkStatusEnumsList); + } + + @Operation(summary = "[列表]权限过滤,获取指定id的用户信息,含绑定关系. 不使用权限则返回全部") + @PostMapping("/info/targetUserIds/auth") + public ActionResult> authGetTargetUserInfoBatch(@RequestBody AuthGetTargetUserInfoBatchDTO dto) { + return ActionResult.success(ftbPermissionUsersService.authGetTargetUserInfoBatch(dto)); + } + + @Operation(summary = "[列表含离职] 查询指定用户id的用户信息") + @PostMapping("/info/targetUserIds") + public ActionResult> getTargetUserInfoBatchByIds(@RequestBody ListIdDTO dto) { + return ActionResult.success(ftbPermissionUsersService.getTargetUserInfoBatchByIds(dto.getIds())); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionFunctionMenuMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionFunctionMenuMapper.java new file mode 100644 index 0000000..f522335 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionFunctionMenuMapper.java @@ -0,0 +1,22 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.authority.po.FtbPermissionFunctionMenu; +import jnpf.model.authority.vo.menu.FtbPermissionFunctionMenuVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import org.apache.ibatis.annotations.Param; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionFunctionMenuMapper extends BaseMapper { + Page getAListOfSubPages(@Param("page") Page objectPage, + @Param("menuId") String menuId); + + FtbPermissionRoleInfoVO getRoleWithMenu(@Param("roleId") String roleId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionMigrateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionMigrateMapper.java new file mode 100644 index 0000000..dfa1494 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionMigrateMapper.java @@ -0,0 +1,25 @@ +package jnpf.authority.mapper; + +import jnpf.base.entity.ModuleButtonEntity; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +public interface FtbPermissionMigrateMapper { + + List personnelMenuAuthorities(@Param("userIds") List userIds); + + List personnelMenu(@Param("type") Integer type); + + List> getRoleList(); + + @Select("SELECT F_UserId FROM base_userrelation WHERE F_ObjectType = 'Role' AND F_ObjectId =#{roleId}") + List getUserListWithRoleId(@Param("roleId") String roleId); + + List characterCheckedMenusAndButtons(@Param("roleId") String roleId); + + List inquiryButton(@Param("moduleId") String moduleId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePersonMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePersonMapper.java new file mode 100644 index 0000000..bb2ab11 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePersonMapper.java @@ -0,0 +1,99 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.authority.po.FtbPermissionRoleAuthorizePerson; +import jnpf.model.authority.vo.person.FtbAuthorizedPersonnelVO; +import jnpf.model.authority.vo.person.FtbEmployeePermissionPersonnelVO; +import jnpf.model.authority.vo.role.FtbPermissionRolePersonVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleAuthorizePersonMapper extends BaseMapper { + /** + * 查询用户权限范围 + * + * @param userId 用户ID + * @return 用户权限范围列表 + */ + List queryUserScopeOfPermission(@Param("userId") String userId); + + /** + * 查询用户权限范围 + * + * @param userId 用户ID + * @return 用户权限范围列表 + */ + List queryUserScopeOfPermissionWithUserIds(@Param("userIds") List userIds); + + /** + * 查询岗位授权信息 + * + * @param positionId 岗位ID + * @return 岗位权限范围列表 + */ + List jobAuthorizationInformation(@Param("positionId") String positionId); + /** + * 查询岗位授权信息 + * + * @param positionId 岗位ID + * @return 岗位权限范围列表 + */ + List jobAuthorizationInformationWithPositionIds(@Param("positionIds") List positionIds); + + + /** + * 根据用户ID列表查询员工权限信息 + * + * @param userIds 用户ID列表 + * @return 员工权限信息列表 + */ + List employeePermission(@Param("userIds") List userIds); + + /** + * 查询角色已授权人员列表 + * + * @param page 分页参数 + * @param roleId 角色ID + * @return 已授权人员分页列表 + */ + Page authorizedPersonnel(@Param("page") Page page,@Param("roleId") String roleId); + + /** + * 分页查询员工权限列表 + * + * @param page 分页参数 + * @param userName 用户姓名 + * @param userId 用户ID + * @param userIds + * @return 员工权限信息分页列表 + */ + Page employeePermissionLPage(@Param("page") Page page, + @Param("userName") String userName, + @Param("userId")String userId, + @Param("userIds") List userIds); + + /** + * 获取人员数据权限 + * + * @param userId 用户ID + * @return 人员数据权限列表 + */ + List getPeopleDataPermissions(@Param("userId") String userId); + /** + * 获取人员数据权限 + * + * @param userId 用户ID + * @return 人员数据权限列表 + */ + List getPeopleDataPermissionsWithUserIds(@Param("userIds") List userIds); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePostMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePostMapper.java new file mode 100644 index 0000000..ea05bbf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleAuthorizePostMapper.java @@ -0,0 +1,14 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.authority.po.FtbPermissionRoleAuthorizePost; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleAuthorizePostMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMapper.java new file mode 100644 index 0000000..3a8e30a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMapper.java @@ -0,0 +1,46 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.authority.dto.role.FtbPermissionRoleInfoDTO; +import jnpf.model.authority.po.FtbPermissionRole; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleMapper extends BaseMapper { + + /** + * 根据用户ID获取权限标识列表 + * + * @param userId 用户ID,用于查询权限信息 + * @return 包含权限标识的列表 + */ + List permissionIdentifications(@Param("userId") String userId); + + /** + * 根据用户ID和角色信息参数,查询权限列表 + * 此方法用于分页查询用户相关的权限信息,根据用户ID进行筛选,并按照角色信息DTO中的其他参数进一步细化查询条件 + * + * @param page 分页对象,包含分页信息和数据容器,用于存储查询结果 + * @param roleInfoDTO 角色信息数据传输对象,包含查询条件,如角色名称、权限代码等 + * @return 返回填充了查询结果的分页对象,包括权限信息列表和分页元数据 + */ + Page permissionList(@Param("page") Page page, @Param("params") FtbPermissionRoleInfoDTO roleInfoDTO); + + /** + * 根据职位ID查询工作权限 + * + * @param positionId 职位ID,用于标识特定的职位 + * @return 返回该职位对应的工作权限列表 + */ + List queryJobPermissions(@Param("positionId") String positionId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuMapper.java new file mode 100644 index 0000000..68c4296 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuMapper.java @@ -0,0 +1,25 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.authority.po.FtbPermissionRoleMenu; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleMenuMapper extends BaseMapper { + + /** + * 根据角色菜单ID列表查询权限编码 + * + * @param roleMenuIds 角色菜单ID列表 + * @return 权限编码列表 + */ + List queryFuncCodingsByRoleMenuIds(@Param("roleMenuIds") List roleMenuIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuRelationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuRelationMapper.java new file mode 100644 index 0000000..86cd85e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRoleMenuRelationMapper.java @@ -0,0 +1,14 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.authority.po.FtbPermissionRoleMenuRelation; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleMenuRelationMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRolePersonUserRelationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRolePersonUserRelationMapper.java new file mode 100644 index 0000000..8a682e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/mapper/FtbPermissionRolePersonUserRelationMapper.java @@ -0,0 +1,14 @@ +package jnpf.authority.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.authority.po.FtbPermissionRolePersonUserRelation; + +/** +* +* +*@Author: peng.hao +*@create: 2025/8/21 +* +*/ +public interface FtbPermissionRolePersonUserRelationMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionFunctionMenuService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionFunctionMenuService.java new file mode 100644 index 0000000..ad63948 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionFunctionMenuService.java @@ -0,0 +1,39 @@ +package jnpf.authority.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.model.module.MenuListVO; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.menu.FtbPermissionFunctionMenuDTO; +import jnpf.model.authority.dto.menu.FtbPermissionMenuDirectoryDTO; +import jnpf.model.authority.po.FtbPermissionFunctionMenu; +import jnpf.model.authority.vo.menu.FtbPermissionFunctionMenuVO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuConfigTreeVO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuDirectoryVO; +import jnpf.model.cultivate.CultivatePage; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionFunctionMenuService extends IService { + + + PageListVO getAListOfSubPages(String menuId, CultivatePage page); + + void addAListOfSubPages(FtbPermissionFunctionMenuDTO dto); + + void updateAListOfSubPages(FtbPermissionFunctionMenuDTO dto); + + List getAListOfMenus(String category, String keyword); + + List getAListOfMenuDirectory(String roleId); + + List getAListOfFunctionMenuConfigurations(FtbPermissionMenuDirectoryDTO dto); + + List querySubpageButton(String subPageId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionGradesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionGradesService.java new file mode 100644 index 0000000..34a77f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionGradesService.java @@ -0,0 +1,23 @@ +package jnpf.authority.service; + +import jnpf.permission.vo.v2.grades.GradeNodeVO; + +import java.util.List; + +/** + * 带权限的职级功能 + * + * @author Flynn Chan + * @create 2025-05-07 + */ +public interface FtbPermissionGradesService { + + /** + * 职级树 + * + * @param positionModuleName 岗位模块名称 + * @param userId 用户id + * @return + */ + List gradeTree(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionMigrateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionMigrateService.java new file mode 100644 index 0000000..56912e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionMigrateService.java @@ -0,0 +1,26 @@ +package jnpf.authority.service; + +import jnpf.model.authority.dto.menu.FtbPermissionMigrareRoleAddDTO; +import jnpf.model.authority.dto.role.FtbPermissionPositionAuthorizationDTO; +import jnpf.model.authority.dto.role.FtbPermissionPositionMenuDTO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuDirectoryVO; +import jnpf.model.authority.vo.menu.FtbPermissionModuleInfoVO; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; + +import java.util.List; +import java.util.Map; + +public interface FtbPermissionMigrateService { + + List getPositionMenuByTenantAndModule(FtbPermissionPositionMenuDTO ftbPermissionPositionMenuDTO); + + void syncPositionAuthorization(FtbPermissionPositionAuthorizationDTO ftbPermissionPositionAuthorizationDTO); + + List> getRoleList(String tenantId); + + void addRole(FtbPermissionMigrareRoleAddDTO ftbPermissionRole); + + FtbPermissionModuleInfoVO queryRolePermission(String roleId, String tenantId); + + List getAListOfMenuDirectory(Integer type, String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionOrganizeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionOrganizeService.java new file mode 100644 index 0000000..08480bf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionOrganizeService.java @@ -0,0 +1,87 @@ +package jnpf.authority.service; + +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.permission.dto.v2.organzie.QueryOrganizeNodeDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; + +import java.util.List; +import java.util.Map; + +/** + * 加入权限的组织查询功能 + * + * @author Flynn Chan + * @create 2025-05-06 + */ +public interface FtbPermissionOrganizeService { + + List allOrganizeUsersTree(QueryOrganizeNodeDTO dto); + + /** + * 人员权限过滤后的组织(公司,部门,门店,班主),人员 + * + * @return + */ + List listOrganizeTreeUsers(List organizeCategoryEnums, List workStatusEnums, String organizeKeyword, Boolean withEmployee, Boolean includeResigned); + + List listOrganizeTreeUsersFilterNodeJson(List organizeCategoryEnums, + List workStatusEnums, + Boolean withEmployee, + Boolean filterBindOtherStore, + Boolean filterBindPayStore + ); + + /** + * 人员权限过滤后的组织列表 + * + * @return + */ + List authOrganizesByUserBound(List organizeCategoryEnums, + String organizeKeyword, + Boolean filterBindOtherStore, + Boolean filterBindPayStore + ); + + /** + * 批量用户有权限的门店 + * + * @param userIds 用户ids + * @param status 状态 1:禁用 0:启用 -1-所有 + * @return + */ + Map> batchAuthOrganizesForUserIds(List userIds, Integer status); + + Map> batchAuthOrganizesAllForUserIdsAndTenantId(List userIds, Integer status, String moduleId, String tenantId); + + /** + * 获取门店基础信息列表auth api + * + * @return + */ + List authStoreBaseListInfo(); + + /** + * 批量用户有权限的门店 + * + * @param userIds 用户ids + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param moduleId 模块id + * @param tenantId 租户id + * @return + */ + Map> batchAuthOrganizesForUserIdsAndTenantId(List userIds, Integer status,String moduleId, String tenantId); + + Map batchAuthOrganizesAllForUserIds(List userIds); + + /** + * 获取用户权限 + * @return jnpf.permission.vo.v2.TargetAuthIdsVO + */ + TargetAuthIdsVO getUserAuth(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionPositionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionPositionService.java new file mode 100644 index 0000000..a080239 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionPositionService.java @@ -0,0 +1,27 @@ +package jnpf.authority.service; + +import jnpf.model.authority.dto.permission.OrganizeWithPositionsDTO; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.organize.OrganizeAndPositionListVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.position.PositionListOrganizeUserVO; +import jnpf.permission.vo.v2.position.PositionListUserVO; + +import java.util.List; + +/** + * 带权限的岗位功能 + * + * @author Flynn Chan + * @create 2025-05-07 + */ +public interface FtbPermissionPositionService { + + List authListPositionTreeUser(List workStatusEnums, Boolean includeResigned); + + List authListPositionAuth(); + + List listPositionOrganizeUser(List filterUserWorkStatusEnumsList); + + List authOrganizeWithPositions(OrganizeWithPositionsDTO thisDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePersonService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePersonService.java new file mode 100644 index 0000000..1c978dd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePersonService.java @@ -0,0 +1,56 @@ +package jnpf.authority.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.exception.LoginException; +import jnpf.model.authority.dto.authorize.FtbPermissionAuthorizeVO; +import jnpf.model.authority.dto.menu.FuntionMenuDTO; +import jnpf.model.authority.dto.person.FtbPermissionRoleBatchDeleteDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonAddDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonRelationDTO; +import jnpf.model.authority.po.FtbPermissionRoleAuthorizePerson; +import jnpf.model.authority.vo.person.FtbAuthorizedPersonnelVO; +import jnpf.model.authority.vo.person.FtbEmployeePermissionPersonnelVO; +import jnpf.model.authority.vo.person.FtbRoleListDropDownVO; +import jnpf.model.authority.vo.person.FunctionVO; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleAuthorizePersonService extends IService { + + + PageListVO authorizedPersonnel(Page page, String roleId); + + PageListVO employeePermission(Page page, String userName, List userIds); + + List roleListDropDown(String userId); + + void editEmployeePermission(List roleIds, String userId); + + void add(FtbPermissionRolePersonAddDTO rolePersonAddDTO); + + void batchDelete(FtbPermissionRoleBatchDeleteDTO rolePersonDeleteDTO); + + List getRoleIdsByUserId(FtbPermissionAuthorizeVO vo); + + /** + * 删除员工所有权限 + */ + void deleteEmployeeAllPermission(List userIds); + + List allRolesOfTheCurrentPerson(String userId, String tenantId); + + void addPersonalPermission(FtbPermissionRolePersonRelationDTO rolePersonAddDTO); + + List getPersonalPermission(String userId); + + List queryButtonPermission(FuntionMenuDTO funtion) throws LoginException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePostService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePostService.java new file mode 100644 index 0000000..7c0b57f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleAuthorizePostService.java @@ -0,0 +1,33 @@ +package jnpf.authority.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.authorize.FtbPermissionAuthorizeVO; +import jnpf.model.authority.dto.person.FtbPermissionRoleBatchDeleteDTO; +import jnpf.model.authority.dto.post.FtbPermissionRolePostAddDTO; +import jnpf.model.authority.po.FtbPermissionRoleAuthorizePost; +import jnpf.model.authority.vo.post.FtbAuthorizedPostVO; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleAuthorizePostService extends IService { + + + void addPost(FtbPermissionRolePostAddDTO ftbPermissionRolePostAddDTO); + + PageListVO authorizePost(Page page, String roleId); + + List getRoleIdsByPostIds(FtbPermissionAuthorizeVO vo); + + void batchDelete(FtbPermissionRoleBatchDeleteDTO rolePersonDeleteDTO); + + void clearRoleByPostId(List postIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuRelationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuRelationService.java new file mode 100644 index 0000000..55eee4a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuRelationService.java @@ -0,0 +1,16 @@ +package jnpf.authority.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.authority.po.FtbPermissionRoleMenuRelation; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleMenuRelationService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuService.java new file mode 100644 index 0000000..7e02afe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleMenuService.java @@ -0,0 +1,16 @@ +package jnpf.authority.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.authority.po.FtbPermissionRoleMenu; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleMenuService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleService.java new file mode 100644 index 0000000..f99385b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionRoleService.java @@ -0,0 +1,149 @@ +package jnpf.authority.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.authority.dto.menu.FtbPermissionRoleAddDTO; +import jnpf.model.authority.dto.role.FtbPermissionDataDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleCopyDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleInfoDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleUpdateDTO; +import jnpf.model.authority.po.FtbPermissionRole; +import jnpf.model.authority.vo.role.FtbPermissionRoleDetailsVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +public interface FtbPermissionRoleService extends IService { + + + /** + * 查询权限列表 + *

+ * 该方法用于根据给定的查询条件(FtbPermissionRoleInfoDTO)和分页信息(Page), + * 查询出符合查询条件的权限角色信息列表,并将结果以分页的形式返回 + * + * @param page 分页对象,包含分页查询所需的信息,如当前页码、每页记录数等 + * @param roleInfoDTO 查询条件对象,包含查询权限角色信息所需的条件,如角色名称、权限代码等 + * @return 返回一个分页对象,其中包含符合查询条件的权限角色信息列表 + */ + Page permissionList(Page page, FtbPermissionRoleInfoDTO roleInfoDTO); + + /** + * 添加角色 + * + * 该方法用于将一个新的角色添加到系统中 + * 它接收一个包含角色相关信息的数据对象作为参数 + * + * @param ftbPermissionRoleAddDTO 包含待添加角色信息的数据传输对象 + */ + void addRole(FtbPermissionRoleAddDTO ftbPermissionRoleAddDTO); + + /** + * 编辑角色信息 + * + * 该方法用于更新角色的权限信息通过接收一个包含角色更新数据的DTO对象来实现这一功能 + * + * @param roleUpdateDTO 角色更新数据传输对象,包含了需要更新的角色信息和权限数据 + */ + void editRole(FtbPermissionRoleUpdateDTO roleUpdateDTO); + + /** + * 删除指定的角色 + * + * 通过角色的唯一标识符id来删除角色这个方法表明了在角色管理系统中, + * 一个角色可以通过其唯一的id来被删除这是一个基础的操作,用于维护角色信息的更新和管理 + * + * @param id 角色的唯一标识符,用于标识和定位要删除的角色 + */ + void deleteRole(String id); + + /** + * 获取权限角色识别信息集合 + *

+ * 该方法用于收集并返回一系列的权限角色识别信息,这些信息帮助确定用户在系统中的权限级别和角色 + * 它不接受任何参数,返回一个包含多个FtbPermissionRoleIdentificationVO对象的集合 + * + * @return FtbPermissionRoleIdentificationVO 返回一个权限角色识别信息对象集合 + */ + FtbPermissionRoleIdentificationVO permissionIdentificationCollection(); + + /** + * 根据角色ID和菜单ID获取角色详细信息 + * 此方法用于获取特定角色在特定菜单下的权限详情 + * + * @return 返回一个包含角色权限详细信息的对象 + */ + FtbPermissionRoleDetailsVO roleDetails(String roleId, String menuWebId, String menuAppId, String menuPcId); + + /** + * 查询用户的权限范围 + *

+ * 通过用户ID查询该用户具有哪些资源的访问权限,以及对每个资源的具体操作权限 + * 查询结果以列表形式返回,列表中的每个元素都是一个映射,映射的键是资源名称,值是该用户对该资源的操作权限 + * + * @param userId 用户ID,用于标识和查询特定用户的权限信息 + * @return 返回一个列表,列表中的每个元素都是一个映射,映射的键是资源名称,值是该用户对该资源的操作权限 + */ + Map queryUserScopeOfPermission(String userId,String... tenantId); + + /** + * 查询用户的权限范围 + *

+ * 通过用户ID查询该用户具有哪些资源的访问权限,以及对每个资源的具体操作权限 + * 查询结果以列表形式返回,列表中的每个元素都是一个映射,映射的键是资源名称,值是该用户对该资源的操作权限 + * + * @param userId 用户ID,用于标识和查询特定用户的权限信息 + * @return 返回一个列表,列表中的每个元素都是一个映射,映射的键是资源名称,值是该用户对该资源的操作权限 + */ + Map queryUserScopeOfPermissionNoTenant(String userId); + + /** + * 复制角色及其权限信息 + * 此方法用于在系统中创建一个现有角色的副本,包括其所有权限和设置 + * 主要用途是当需要创建一个与现有角色权限相同的新角色时,避免手动重新配置所有权限 + * + * @param ftbPermissionRole 要复制的角色信息,包含角色ID、名称等 + * 此参数是复制操作的唯一输入,通过它传递角色的详细信息 + */ + void copyRole(FtbPermissionRoleCopyDTO ftbPermissionRole); + + /** + * 当前人员最新具有的菜单权限列表 + * @param userId + * @return + */ + Set queryTheLatestListOfMenuPermissions(String userId); + + /** + * 将数据权限应用到其他模块 + *

+ * 此方法用于将当前配置的数据权限设置应用到系统中的其他相关模块, + * 以确保数据权限规则在整个系统中保持一致性 + * + * @param ftbPermissionDataDTO 包含数据权限配置信息的数据传输对象, + * 包括权限范围、授权对象等信息 + */ + void dataPermissionApplicationToOtherModules(FtbPermissionDataDTO ftbPermissionDataDTO); + + /** + * 查询用户的权限范围 + *

+ * 通过用户ID查询该用户具有哪些资源的访问权限,以及对每个资源的具体操作权限 + * 查询结果以列表形式返回,列表中的每个元素都是一个映射,映射的键是资源名称,值是该用户对该资源的操作权限 + * + * @param userId 用户ID,用于标识和查询特定用户的权限信息 + * @return 返回一个列表,列表中的每个元素都是一个映射,映射的键是资源名称,值是该用户对该资源的操作权限 + */ + Map> queryUserScopeOfPermission(List userId, String... tenantId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionUsersService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionUsersService.java new file mode 100644 index 0000000..946dc99 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/FtbPermissionUsersService.java @@ -0,0 +1,47 @@ +package jnpf.authority.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.permission.AuthGetTargetUserInfoBatchDTO; +import jnpf.permission.dto.v2.user.AuthUserBoundInfoBatchDTO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryPageUserMoreKeywordDTO; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import org.apache.commons.collections.CollectionUtils; + +import java.util.List; + +/** + * 带权限的用户管理功能 + * + * @author Flynn Chan + * @create 2025-05-21 + */ +public interface FtbPermissionUsersService { + + PageListVO pagePost(QueryPageUserDTO dto); + + PageListVO pagePostMoreKeyword(QueryPageUserMoreKeywordDTO dto); + + List authGetAllUserInfoBatch(); + + /** + * 查询拥有数据权限的非userWorkStatusEnumsNotList中的用户 + * @param userWorkStatusEnumsNotList + * @return + */ + List authGetAllUserInfoBatch(List userWorkStatusEnumsNotList); + + /** + * 权限范围内仅返回用户 id 列表(不查关系与 VO,用于仅需 id 的调用方) + */ + List authGetPermissionScopeUserIds(); + + List listTargetOrganizeIdAuth(String organizeId, List userWorkStatusEnumsList); + + List listTargetOrganizeIdsAuth(List organizeIds, List userWorkStatusEnumsList); + + List authGetTargetUserInfoBatch(AuthGetTargetUserInfoBatchDTO dto); + + List getTargetUserInfoBatchByIds(List ids); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionFunctionMenuServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionFunctionMenuServiceImpl.java new file mode 100644 index 0000000..f20bbd4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionFunctionMenuServiceImpl.java @@ -0,0 +1,294 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.authority.mapper.FtbPermissionFunctionMenuMapper; +import jnpf.authority.mapper.FtbPermissionRoleMenuRelationMapper; +import jnpf.authority.service.FtbPermissionFunctionMenuService; +import jnpf.base.ModuleApi; +import jnpf.base.SystemApi; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.entity.SystemEntity; +import jnpf.base.model.module.MenuListVO; +import jnpf.base.vo.PageListVO; +import jnpf.model.UserMenuModel; +import jnpf.model.authority.dto.menu.FtbPermissionFunctionMenuDTO; +import jnpf.model.authority.dto.menu.FtbPermissionMenuDirectoryDTO; +import jnpf.model.authority.po.FtbPermissionFunctionMenu; +import jnpf.model.authority.po.FtbPermissionRoleMenuRelation; +import jnpf.model.authority.vo.menu.FtbPermissionFunctionMenuVO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuConfigTree; +import jnpf.model.authority.vo.menu.FtbPermissionMenuConfigTreeVO; +import jnpf.model.authority.vo.menu.FtbPermissionMenuDirectoryVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import jnpf.util.treeutil.SumTree; +import jnpf.util.treeutil.newtreeutil.TreeDotUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * + * + *@Author: peng.hao + *@create: 2025/3/5 + * + */ +@Service +public class FtbPermissionFunctionMenuServiceImpl extends ServiceImpl implements FtbPermissionFunctionMenuService { + + @Resource + private FtbPermissionFunctionMenuMapper ftbPermissionFunctionMenuMapper; + + @Resource + private SystemApi systemApi; + + @Resource + private ModuleApi moduleApi; + + @Resource + private FtbPermissionRoleMenuRelationMapper ftbPermissionRoleMenuRelationMapper; + + @Override + public PageListVO getAListOfSubPages(String menuId, CultivatePage page) { + Page objectPage = page.coverCultivatePage(); + Page menuPage = ftbPermissionFunctionMenuMapper.getAListOfSubPages(objectPage, menuId); + return CultivatePage.coverPageList(menuPage); + } + + @Override + public void addAListOfSubPages(FtbPermissionFunctionMenuDTO dto) { + String enCode = dto.getEnCode(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionFunctionMenu::getFuncCoding, enCode); + queryWrapper.eq(FtbPermissionFunctionMenu::getEnableMark, 0); + Long aLong = baseMapper.selectCount(queryWrapper); + if (aLong > 0) { + throw new RuntimeException("编码重复"); + } + baseMapper.insert(FtbPermissionFunctionMenuDTO.convert(dto)); + } + + @Override + public void updateAListOfSubPages(FtbPermissionFunctionMenuDTO dto) { + String enCode = dto.getEnCode(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionFunctionMenu::getFuncCoding, enCode); + queryWrapper.eq(FtbPermissionFunctionMenu::getEnableMark, 0); + queryWrapper.ne(FtbPermissionFunctionMenu::getId, dto.getId()); + Long aLong = baseMapper.selectCount(queryWrapper); + if (aLong > 0) { + throw new RuntimeException("编码重复"); + } + baseMapper.updateById(FtbPermissionFunctionMenuDTO.convert(dto)); + } + + @Override + public List getAListOfMenus(String category, String keyword) { + SystemEntity systemVO = systemApi.getSystemVO(); + String systemId = systemVO.getId(); + String tenantId = UserProvider.getUser().getTenantId(); + List data = moduleApi.getListWithoutSystemId(systemId, category, keyword, tenantId); + if (CollUtil.isEmpty(data)) { + return new ArrayList<>(); + } + List jsonToList; + if (StringUtils.isNotEmpty(keyword)){ + jsonToList = getTheDataset(data); + }else { + List list = JsonUtil.getJsonToList(data, UserMenuModel.class); + List> menuList = TreeDotUtils.convertListToTreeDot(list, "-1"); + jsonToList= JsonUtil.getJsonToList(menuList, MenuListVO.class); + } + return jsonToList; + } + + public static final Set EXCLUDED_DIRECTORIES = Set.of( + "资料库", "权限管理" + ); + @Override + public List getAListOfMenuDirectory(String roleId) { + SystemEntity systemVO = systemApi.getSystemVO(); + String systemId = systemVO.getId(); + String tenantId = UserProvider.getUser().getTenantId(); + FtbPermissionRoleInfoVO vo = new FtbPermissionRoleInfoVO(); + if (roleId != null ){ + vo = baseMapper.getRoleWithMenu(roleId); + } + List data = moduleApi.getListWithoutSystemId(systemId, null, null, tenantId); + // 获取顶级目录菜单 + List entities = data.stream().filter(item -> item.getType() != null && item.getEnabledMark() != null && 1 == item.getEnabledMark() && "-1".equals(item.getParentId())).collect(Collectors.toList()); + List directoryVOS = entities.stream().map(FtbPermissionMenuDirectoryVO::convert).distinct().collect(Collectors.toList()); + for (FtbPermissionMenuDirectoryVO directoryVO : directoryVOS) { + List infoVOS = entities.stream().filter(v -> v.getFullName().equals(directoryVO.getFullName())).map(v -> { + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO infoVO = new FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO(); + String vCategory = v.getCategory(); + String vId = v.getId(); + infoVO.setId(vId); + infoVO.setCategory(vCategory); + return infoVO; + }).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(roleId) && vo != null) { + List functionIds = new ArrayList<>(); + if ( vo.getFunctionWebId() != null ){ + String[] split = vo.getFunctionWebId().split(","); + functionIds .addAll(List.of(split)); + } + if ( vo.getFunctionAppId() != null ){ + functionIds.addAll(List.of(vo.getFunctionAppId().split(","))); + } + if (vo.getFunctionPcId() != null) { + functionIds.addAll(List.of(vo.getFunctionPcId().split(","))); + } + // 拼接并过滤非空字段 + functionIds.removeIf(StrUtil::isBlank); + List fullNames = entities.stream() + .filter(item -> functionIds.contains(item.getId())) + .map(ModuleEntity::getFullName).distinct().collect(Collectors.toList()); + if (fullNames.contains(directoryVO.getFullName())) { + directoryVO.setIsUse(true); + } + } + directoryVO.setChildren(infoVOS); + } + return directoryVOS.stream() + .filter(item -> !EXCLUDED_DIRECTORIES.contains(item.getFullName())) + .collect(Collectors.toList()); + + } + + @Override + public List getAListOfFunctionMenuConfigurations(FtbPermissionMenuDirectoryDTO dto) { + String tenantId = UserProvider.getUser().getTenantId(); + if (StrUtil.isBlank(tenantId)) { + tenantId = dto.getTenantId(); + } + ModuleEntity data = moduleApi.getModuleByList(dto.getId()); + if (ObjectUtils.isEmpty(data)){ + return null; + } + String id =data.getId(); + // 查询所有子集 + List newData = new ArrayList<>(); + getTreeMenuWithPreantId(id, tenantId,newData); + List list = JsonUtil.getJsonToList(newData, UserMenuModel.class); + List> menuList = TreeDotUtils.convertListToTreeDot(list); + List toList = JsonUtil.getJsonToList(menuList, MenuListVO.class); + // 根据所有的子集查询是否配置的菜单且需要递归获取配置进行复原 + List configTrees = new ArrayList<>(); + buildMenuDetails(toList,configTrees); + if (dto.getRoleId() != null){ + // 查询当前角色的菜单配置 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleMenuRelation::getRoleId, dto.getRoleId()); + List relations = ftbPermissionRoleMenuRelationMapper.selectList(wrapper); + configTrees.forEach(item -> { + boolean anyMatch = relations.stream().anyMatch(relation -> relation.getRoleMenuId().equals(item.getId())); + if (anyMatch) { + item.setIsChecked(true); + } + }); + } + List> trees = TreeDotUtils.convertListToTreeDot(configTrees); + return JsonUtil.getJsonToList(trees, FtbPermissionMenuConfigTreeVO.class); + } + + @Override + public List querySubpageButton(String subPageId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionFunctionMenu::getParentId, subPageId); + wrapper.eq(FtbPermissionFunctionMenu::getFunType,1); + List functionMenus = baseMapper.selectList(wrapper); + return functionMenus.stream().map(FtbPermissionFunctionMenuVO::convert).collect(Collectors.toList()); + } + + public void buildMenuDetails(List newData, List list){ + for (MenuListVO moduleEntity : newData) { + String id = moduleEntity.getId(); + FtbPermissionMenuConfigTree configTree = new FtbPermissionMenuConfigTree(); + configTree.setId(id); + configTree.setName(moduleEntity.getFullName()); + configTree.setParentId(moduleEntity.getParentId()); + configTree.setModuleType(moduleEntity.getType()); + list.add(configTree); + // 查询当前菜单是否配置了子页面 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionFunctionMenu::getFunType,0); + queryWrapper.eq(FtbPermissionFunctionMenu::getParentId,id); + List functionMenuList = baseMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(functionMenuList)){ + // 查询当前菜单是否有子集 + list.addAll(functionMenuList.stream().map(FtbPermissionMenuConfigTree::convert).collect(Collectors.toList())); + for (FtbPermissionFunctionMenu functionMenu : functionMenuList) { + LambdaQueryWrapper menuLambdaQueryWrapper = Wrappers.lambdaQuery(); + menuLambdaQueryWrapper.eq(FtbPermissionFunctionMenu::getFunType,1); + menuLambdaQueryWrapper.eq(FtbPermissionFunctionMenu::getParentId,functionMenu.getId()); + List buttonList = baseMapper.selectList(menuLambdaQueryWrapper); + if (CollUtil.isNotEmpty(buttonList)){ + list.addAll(buttonList.stream().map(FtbPermissionMenuConfigTree::convert).collect(Collectors.toList())); + } + } + }else if (moduleEntity.getChildren() != null){ + // 子集递归 + List children = moduleEntity.getChildren(); + buildMenuDetails(children, list); + } + } + + } + + + /** + * 查询子集 + * @param meuId + * @param tenantId + * @param data + */ + private void getTreeMenuWithPreantId(String meuId, String tenantId,List data){ + List moduleEntityList = moduleApi.findAllSubsetsBasedOnParent(meuId,tenantId); + if (CollUtil.isNotEmpty(moduleEntityList)){ + data.addAll(moduleEntityList); + moduleEntityList.forEach(vo->{ + getTreeMenuWithPreantId(vo.getId(),tenantId,data); + }); + } + } + + public List getTheDataset(List data){ + List result = new ArrayList<>(); + for (ModuleEntity datum : data) { + result.add(datum); + if (!datum.getParentId().equals("-1")) { + getTopModule(result, datum.getParentId()); + } + } + List entities = result.stream().distinct().collect(Collectors.toList()); + List list = JsonUtil.getJsonToList(entities, UserMenuModel.class); + List> menuList = TreeDotUtils.convertListToTreeDot(list); + return JsonUtil.getJsonToList(menuList, MenuListVO.class); + + } + private void getTopModule(List data,String peuId){ + String tenantId = UserProvider.getUser().getTenantId(); + ModuleEntity byParentId = moduleApi.getTenantInfoById(peuId, tenantId); + if (ObjectUtils.isNotEmpty(byParentId)){ + data.add(byParentId); + if (!byParentId.getParentId().equals("-1")) { + getTopModule(data, byParentId.getParentId()); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionGradesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionGradesServiceImpl.java new file mode 100644 index 0000000..6319fb1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionGradesServiceImpl.java @@ -0,0 +1,38 @@ +package jnpf.authority.service.impl; + +import jnpf.authority.service.FtbPermissionGradesService; +import jnpf.enums.PermissionSourceCategoryEnum; +import jnpf.permission.V2GradesApi; +import jnpf.permission.dto.v2.AuthUserNodeDTO; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.grades.GradeNodeVO; +import jnpf.util.UserProvider; +import jnpf.util.auth.V2AuthPermissionUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 带权限的职级管理 + * + * @author Flynn Chan + * @create 2025-05-07 + */ +@Service +@Slf4j +public class FtbPermissionGradesServiceImpl implements FtbPermissionGradesService { + @Resource + private V2GradesApi gradesApi; + @Resource + private V2AuthPermissionUtils authPermissionUtils; + + @Override + public List gradeTree() { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthUserNodeDTO dto = new AuthUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(authIdsVO != null); + return gradesApi.gradeTree(dto).getData(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionMigrateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionMigrateServiceImpl.java new file mode 100644 index 0000000..8dcd9be --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionMigrateServiceImpl.java @@ -0,0 +1,801 @@ +package jnpf.authority.service.impl; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import jnpf.UpgradeApi; +import jnpf.attendance.service.AttendanceSuperAdminService; +import jnpf.authority.mapper.*; +import jnpf.authority.service.FtbPermissionFunctionMenuService; +import jnpf.authority.service.FtbPermissionMigrateService; +import jnpf.authority.service.FtbPermissionRoleAuthorizePersonService; +import jnpf.authority.service.FtbPermissionRoleService; +import jnpf.base.ActionResult; +import jnpf.base.ModuleApi; +import jnpf.base.SystemApi; +import jnpf.base.entity.ModuleBakUpEntity; +import jnpf.base.entity.ModuleButtonEntity; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.entity.SystemEntity; +import jnpf.base.model.module.MenuListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.UserMenuModel; +import jnpf.model.authority.dto.menu.FtbPermissionMenuDirectoryDTO; +import jnpf.model.authority.dto.menu.FtbPermissionMigrareRoleAddDTO; +import jnpf.model.authority.dto.menu.FtbPermissionRoleAddDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonAddDTO; +import jnpf.model.authority.dto.role.FtbPermissionPositionAuthorizationDTO; +import jnpf.model.authority.dto.role.FtbPermissionPositionMenuDTO; +import jnpf.model.authority.po.*; +import jnpf.model.authority.vo.menu.*; +import jnpf.model.authority.vo.role.FtbPermissionPositionMenuVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import jnpf.model.config.PatrolUpgradeConfigUserVo; +import jnpf.model.personnels.po.FtbPersonnelsPermissions; +import jnpf.permission.V2PositionApi; +import jnpf.permission.dto.v2.position.QueryPositionUserListDTO; +import jnpf.permission.vo.v2.position.PositionListUserVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsPermissionsMapper; +import jnpf.util.AuthUtil; +import jnpf.util.JsonUtil; +import jnpf.util.treeutil.SumTree; +import jnpf.util.treeutil.newtreeutil.TreeDotUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Service +public class FtbPermissionMigrateServiceImpl implements FtbPermissionMigrateService { + + @Resource + private UpgradeApi upgradeApi; + + @Resource + private FtbPermissionFunctionMenuService ftbPermissionFunctionMenuService; + + @Resource + private FtbPermissionMigrateMapper ftbPermissionMigrateMapper; + + @Resource + private FtbPersonnelsPermissionsMapper ftbPersonnelsPermissionsMapper; + + @Resource + private V2PositionApi v2PositionApi; + + @Resource + private AttendanceSuperAdminService attendanceSuperAdminService; + + @Resource + private FtbPermissionRoleService ftbPermissionRoleService; + + @Resource + private FtbPermissionRoleAuthorizePersonService ftbPermissionRoleAuthorizePersonService; + + @Resource + private SystemApi systemApi; + + @Resource + private ModuleApi moduleApi; + + @Resource + private FtbPermissionFunctionMenuMapper ftbPermissionFunctionMenuMapper; + + + @Resource + private FtbPermissionRoleMenuRelationMapper ftbPermissionRoleMenuRelationMapper; + + @Resource + FtbPermissionRoleMenuMapper ftbPermissionRoleMenuMapper; + + @Resource + FtbPermissionRolePersonUserRelationMapper ftbPermissionRolePersonUserRelationMapper; + + @Override + public List getPositionMenuByTenantAndModule(FtbPermissionPositionMenuDTO ftbPermissionPositionMenuDTO) { + // 获取所有角色 + List ftbPermissionRoles = ftbPermissionRoleService.list(); + List roleNames = ftbPermissionRoles.stream().map(FtbPermissionRole::getRoleName).collect(Collectors.toList()); + // 查询所有岗位 + QueryPositionUserListDTO dto = new QueryPositionUserListDTO(); + ActionResult> listPositionTreeUser = v2PositionApi.listPositionTreeUser(dto); + // 获取每个岗位下的人,没有人则不执行以下逻辑 + List result = new ArrayList<>(); + List positionTreeUserData = listPositionTreeUser.getData(); + positionTreeUserData = positionTreeUserData.stream() + .filter(data -> !roleNames.contains(ftbPermissionPositionMenuDTO.getModuleName() + "-" + data.getFullName())) + .collect(Collectors.toList()); + for (PositionListUserVO datum : positionTreeUserData) { + FtbPermissionPositionMenuVO ftbPermissionPositionMenuVO = new FtbPermissionPositionMenuVO(); + ftbPermissionPositionMenuVO.setPostName(datum.getFullName()); + ftbPermissionPositionMenuVO.setPostId(datum.getId()); + if (CollUtil.isNotEmpty(datum.getUserList())) { + List userIds = datum.getUserList().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + ftbPermissionPositionMenuVO.setUserIds(userIds); + // 获取岗位下用户的权限菜单 + if ("人事管理".equals(ftbPermissionPositionMenuDTO.getModuleName())) { + List ftbPermissionPositionMenuInnerVOS = personnelMenu(userIds, 0); + ftbPermissionPositionMenuVO.setFtbPermissionPositionMenuInnerVOList(ftbPermissionPositionMenuInnerVOS); + } else if ("培训管理".equals(ftbPermissionPositionMenuDTO.getModuleName())) { + List ftbPermissionPositionMenuInnerVOS = personnelMenu(userIds, 1); + ftbPermissionPositionMenuVO.setFtbPermissionPositionMenuInnerVOList(ftbPermissionPositionMenuInnerVOS); + } else if ("考勤管理".equals(ftbPermissionPositionMenuDTO.getModuleName())) { + List ftbPermissionPositionMenuInnerVOS = attendanceSuperAdminService.queryPermissionListByUserIds(userIds); + ftbPermissionPositionMenuVO.setFtbPermissionPositionMenuInnerVOList(ftbPermissionPositionMenuInnerVOS); + } + } + result.add(ftbPermissionPositionMenuVO); + } + result = result.stream().filter(a -> CollUtil.isNotEmpty(a.getFtbPermissionPositionMenuInnerVOList())).collect(Collectors.toList()); + // 获取2.0权限菜单 + FtbPermissionMenuDirectoryDTO ftbPermissionMenuDTO = new FtbPermissionMenuDirectoryDTO(); + ftbPermissionMenuDTO.setTenantId(ftbPermissionPositionMenuDTO.getTenantId()); + ftbPermissionMenuDTO.setId(ftbPermissionPositionMenuDTO.getMenuWebId()); + List menuConfigurationsWeb = ftbPermissionFunctionMenuService.getAListOfFunctionMenuConfigurations(ftbPermissionMenuDTO); + ftbPermissionMenuDTO.setId(ftbPermissionPositionMenuDTO.getMenuAppId()); + List menuConfigurationsApp = ftbPermissionFunctionMenuService.getAListOfFunctionMenuConfigurations(ftbPermissionMenuDTO); + // 判断结果数组不为空 + if (CollUtil.isEmpty(result)) { + return result; + } + result.get(0).setFtbPermissionMenuConfigTreeVOListWeb(menuConfigurationsWeb); + result.get(0).setFtbPermissionMenuConfigTreeVOListApp(menuConfigurationsApp); + return result; + } + + @Override + @Transactional + public void syncPositionAuthorization(FtbPermissionPositionAuthorizationDTO ftbPermissionPositionAuthorizationDTO) { + List roleList = new ArrayList<>(); + List roleMenuList = new ArrayList<>(); + List roleMenuRelationList = new ArrayList<>(); + List roleAuthorizePostList = new ArrayList<>(); + List rolePersonUserRelationList = new ArrayList<>(); + // 人事管理和培训同步用户数据权限 + List userIds = ftbPermissionPositionAuthorizationDTO.getPositionList() + .stream() + .filter(a -> CollUtil.isNotEmpty(a.getUserIds())) + .flatMap(a -> a.getUserIds().stream()) + .collect(Collectors.toList()); + LambdaQueryWrapper ftbPersonnelsPermissionsLambdaQueryWrapper = new LambdaQueryWrapper<>(); + ftbPersonnelsPermissionsLambdaQueryWrapper.in(FtbPersonnelsPermissions::getUserId, userIds); + ftbPersonnelsPermissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + List ftbPersonnelsPermissions = new ArrayList<>(); + if ("人事管理".equals(ftbPermissionPositionAuthorizationDTO.getModuleName())) { + ftbPersonnelsPermissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getPermissionType, 0); + ftbPersonnelsPermissions = ftbPersonnelsPermissionsMapper.selectList(ftbPersonnelsPermissionsLambdaQueryWrapper); + } else if ("培训管理".equals(ftbPermissionPositionAuthorizationDTO.getModuleName())) { + ftbPersonnelsPermissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getPermissionType, 1); + ftbPersonnelsPermissions = ftbPersonnelsPermissionsMapper.selectList(ftbPersonnelsPermissionsLambdaQueryWrapper); + } + final List finalFtbPersonnelsPermissions = ftbPersonnelsPermissions; + // 数据组装 + ftbPermissionPositionAuthorizationDTO.getPositionList().forEach(positionInnerDTO -> { + if (CollUtil.isNotEmpty(positionInnerDTO.getUserIds()) && CollUtil.isNotEmpty(positionInnerDTO.getPermissionRoleInners())) { + FtbPermissionRole ftbPermissionRole = new FtbPermissionRole(); + ftbPermissionRole.setRoleName(ftbPermissionPositionAuthorizationDTO.getModuleName() + "-" + positionInnerDTO.getPostName()); + ftbPermissionRole.setId(IdWorker.getIdStr()); + // 权限管理角色菜单 + positionInnerDTO.getPermissionRoleInners().stream() + .filter(a -> CollUtil.isNotEmpty(a.getMemberIds())) + .forEach(a -> { + FtbPermissionRoleMenu ftbPermissionRoleMenu = a.convert(a); + ftbPermissionRoleMenu.setRoleEnabled(1); + ftbPermissionRoleMenu.setScopePermission(1); + ftbPermissionRoleMenu.setRoleId(ftbPermissionRole.getId()); + ftbPermissionRoleMenu.setId(IdWorker.getIdStr()); + List ftbPermissionRoleMenuRelations = a.getMemberIds().stream() + .map(b -> { + FtbPermissionRoleMenuRelation formRelation = new FtbPermissionRoleMenuRelation(); + formRelation.setId(IdWorker.getIdStr()); + formRelation.setRoleId(ftbPermissionRole.getId()); + formRelation.setRoleMenuConfigId(b); + formRelation.setRoleMenuId(ftbPermissionRoleMenu.getId()); + return formRelation; + }).collect(Collectors.toList()); + // 角色菜单表 + roleMenuList.add(ftbPermissionRoleMenu); + // 角色菜单关联表 + roleMenuRelationList.addAll(ftbPermissionRoleMenuRelations); + }); + // 岗位授权表 + FtbPermissionRoleAuthorizePost ftbPermissionRoleAuthorizePost = new FtbPermissionRoleAuthorizePost(); + ftbPermissionRoleAuthorizePost.setRoleId(ftbPermissionRole.getId()); + ftbPermissionRoleAuthorizePost.setPostId(positionInnerDTO.getPostId()); + ftbPermissionRoleAuthorizePost.setEnableMark(0L); + ftbPermissionRoleAuthorizePost.setId(IdWorker.getIdStr()); + roleAuthorizePostList.add(ftbPermissionRoleAuthorizePost); + // 人员个人权限表 + if (CollUtil.isNotEmpty(finalFtbPersonnelsPermissions)) { + Map ftbPersonnelsPermissionsMap = finalFtbPersonnelsPermissions.stream() + .collect(Collectors.toMap(FtbPersonnelsPermissions::getUserId, a -> a)); + positionInnerDTO.getUserIds().forEach(a -> { + FtbPersonnelsPermissions ftbPersonnelPermissions = ftbPersonnelsPermissionsMap.get(a); + if (ftbPersonnelPermissions != null) { + FtbPermissionRolePersonUserRelation followerUserRelation = new FtbPermissionRolePersonUserRelation(); + followerUserRelation.setId(IdWorker.getIdStr()); + followerUserRelation.setUserId(a); + followerUserRelation.setRoleEnabled(1); + followerUserRelation.setFunctionAppId(ftbPermissionPositionAuthorizationDTO.getMenuAppId()); + followerUserRelation.setFunctionWebId(ftbPermissionPositionAuthorizationDTO.getMenuWebId()); + // 根据汉字转换映射 + Integer convertScopePermission = convertScopePermission(ftbPersonnelPermissions.getScopePermission()); + followerUserRelation.setScopePermission(convertScopePermission); + if (convertScopePermission == 4) { + followerUserRelation.setSpecifyOrgIds(ftbPersonnelPermissions.getSpecifyOrgIds()); + } + rolePersonUserRelationList.add(followerUserRelation); + } + }); + } + // 角色表 + roleList.add(ftbPermissionRole); + } + }); + Db.saveBatch(roleList); + Db.saveBatch(roleMenuList); + Db.saveBatch(roleMenuRelationList); + Db.saveBatch(roleAuthorizePostList); + Db.saveBatch(rolePersonUserRelationList); + } + + @Override + public List> getRoleList(String tenantId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + return ftbPermissionMigrateMapper.getRoleList(); + } + + @Override + @Transactional + public void addRole(FtbPermissionMigrareRoleAddDTO ftbPermissionRole) { + String tenantId = ftbPermissionRole.getTenantId(); + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + // 迁移同一个id + String roleId = ftbPermissionRole.getRoleId(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRole::getId, roleId); + FtbPermissionRole one = ftbPermissionRoleService.getOne(queryWrapper); + if (ObjectUtil.isEmpty(one)) { + ftbPermissionRole.setId(roleId); + // 角色名称不能重复 + LambdaQueryWrapper queryWrapper2 = Wrappers.lambdaQuery(); + queryWrapper2.eq(FtbPermissionRole::getRoleName, ftbPermissionRole.getRoleName()); + queryWrapper2.eq(FtbPermissionRole::getEnableMark, 0); + if (ftbPermissionRoleService.count(queryWrapper) > 0) { + throw new RuntimeException("角色名称不可重复!"); + } + FtbPermissionRole permissionRole = new FtbPermissionRole(); + permissionRole.setId(ftbPermissionRole.getId()); + permissionRole.setRoleName(ftbPermissionRole.getRoleName()); + permissionRole.setRoleDescription(ftbPermissionRole.getRoleDescription()); + ftbPermissionRoleService.save(permissionRole); + List permissionRoleInners = ftbPermissionRole.getPermissionRoleInners(); + extracted(permissionRoleInners, roleId); + }else { + // 修改逻辑 + LambdaQueryWrapper roleMenuLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(FtbPermissionRoleMenu::getRoleId, roleId); + // 删除菜单关联信息 + ftbPermissionRoleMenuMapper.delete(roleMenuLambdaQueryWrapper); + LambdaQueryWrapper deleteRoleWrapper = new LambdaQueryWrapper() + .eq(FtbPermissionRoleMenuRelation::getRoleId, roleId); + ftbPermissionRoleMenuRelationMapper.delete(deleteRoleWrapper); + List permissionRoleInners = ftbPermissionRole.getPermissionRoleInners(); + if (CollUtil.isNotEmpty(permissionRoleInners)) { + extracted(permissionRoleInners, roleId); + } + } + // 多个已勾选模块id + List functionWebIds = ftbPermissionRole.getPermissionRoleInners().stream().map(FtbPermissionRoleAddDTO.PermissionRoleInnerDTO::getFunctionPageWebId).collect(Collectors.toList()); + // 根据勾选id查询模块名称 巡店同步权限 + List moduleApiById = moduleApi.getById(functionWebIds, tenantId); + ModuleEntity entity = moduleApiById.stream().filter(v -> "巡店管理".equals(v.getFullName())).findFirst().orElse(null); + if (ObjectUtils.isNotEmpty(entity)) { + String id = entity.getId(); + ActionResult> configUser = upgradeApi.getConfigUser(tenantId); + if (configUser.getCode() == 200 && CollUtil.isNotEmpty(configUser.getData())){ + // 分别角色授权 + // 1 + List isStoreHeadUserId = filterPersonnelInformation(configUser,v -> 1 == v.getIsStoreHeadUserId()); + addedPersonalAuthorization(isStoreHeadUserId, id,1); + // 0 + List dbUser = filterPersonnelInformation(configUser,v -> 1 == v.getIsDdUser()); + addedPersonalAuthorization(dbUser, id,0); + // 0 + List isCheckUser = filterPersonnelInformation(configUser,v -> 1 == v.getIsCheckUser()); + addedPersonalAuthorization(isCheckUser, id,0); + } + } + // 根据id查询角色绑定人员信息 + List userIds = ftbPermissionMigrateMapper.getUserListWithRoleId(roleId); + // 进行授权 + FtbPermissionRolePersonAddDTO personAddDTO = new FtbPermissionRolePersonAddDTO(); + personAddDTO.setRoleId(roleId); + personAddDTO.setUserIds(userIds); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getRoleId, roleId); + ftbPermissionRoleAuthorizePersonService.remove(wrapper); + // 新增数据 + ftbPermissionRoleAuthorizePersonService.add(personAddDTO); + // 下线数据 + userIds.forEach(AuthUtil::kickoutByUserId) ; + } + + private void extracted(List permissionRoleInners, String roleId) { + // 权限管理角色菜单 + permissionRoleInners.stream() + .filter(a -> CollUtil.isNotEmpty(a.getMemberIds())) + .forEach(a -> { + FtbPermissionRoleMenu ftbPermissionRoleMenu = a.convert(a); + // 默认设置为所在组织及下级组织 + ftbPermissionRoleMenu.setScopePermission(1); + ftbPermissionRoleMenu.setRoleId(roleId); + ftbPermissionRoleMenuMapper.insert(ftbPermissionRoleMenu); + List ftbPermissionRoleMenuRelations = a.getMemberIds().stream() + .map(b -> { + FtbPermissionRoleMenuRelation formRelation = new FtbPermissionRoleMenuRelation(); + formRelation.setRoleId(roleId); + formRelation.setRoleMenuConfigId(b); + formRelation.setRoleMenuId(ftbPermissionRoleMenu.getId()); + return formRelation; + }).collect(Collectors.toList()); + Db.saveBatch(ftbPermissionRoleMenuRelations); + }); + } + + /** + * 给个人授权 + * @param userIds + * @param moduleId + */ + public void addedPersonalAuthorization(List userIds,String moduleId,Integer scopePermission){ + if (CollUtil.isEmpty(userIds)){ + return; + } + // 先清空数据 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPermissionRolePersonUserRelation::getUserId, userIds); + queryWrapper.eq(FtbPermissionRolePersonUserRelation::getFunctionWebId,moduleId); + ftbPermissionRolePersonUserRelationMapper.delete(queryWrapper); + List userRelations = userIds.stream().map(v -> { + FtbPermissionRolePersonUserRelation userRelation = new FtbPermissionRolePersonUserRelation(); + userRelation.setUserId(v); + userRelation.setFunctionWebId(moduleId); + userRelation.setScopePermission(scopePermission); + userRelation.setRoleEnabled(1); + return userRelation; + }).collect(Collectors.toList()); + Db.saveBatch(userRelations); + userIds.forEach(AuthUtil::kickoutByUserId); + } + @NotNull + private List filterPersonnelInformation(ActionResult> configUser, + Predicate predicate) { + return configUser.getData().stream().filter(predicate).map(PatrolUpgradeConfigUserVo::getUserId).collect(Collectors.toList()); + } + + @Override + public FtbPermissionModuleInfoVO queryRolePermission(String roleId, String tenantId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + return permissionMenuBuild(roleId, tenantId); + } + + @Override + public List getAListOfMenuDirectory(Integer type, String tenantId) { + List aListOfMenuDirectory = getAListOfMenuDirectory(tenantId); + List menuNames = List.of("人事管理", "培训管理", "考勤管理"); + return aListOfMenuDirectory.stream().filter(a -> { + if (type == 0) { + return menuNames.contains(a.getFullName()); + } else { + return !menuNames.contains(a.getFullName()); + } + }).collect(Collectors.toList()); + } + + /** + * 角色权限菜单构建 + * @param roleId + * @param tenantId + * @return + */ + public FtbPermissionModuleInfoVO permissionMenuBuild(String roleId, String tenantId){ + SystemEntity systemVO = systemApi.getSystemVONoToken(tenantId); + String systemId = systemVO.getId(); + FtbPermissionRoleInfoVO vo = new FtbPermissionRoleInfoVO(); + if (roleId != null ){ + vo = ftbPermissionFunctionMenuMapper.getRoleWithMenu(roleId); + } + FtbPermissionModuleInfoVO infoVO = new FtbPermissionModuleInfoVO(); + List moduleVOSBak = buildData1(systemId, tenantId, vo, roleId); + infoVO.setPermissionsOne(moduleVOSBak); + List moduleVOS = buildData2(systemId, tenantId, vo, roleId); + infoVO.setPermissionsTwo(moduleVOS); + return infoVO; + } + public List buildData1(String systemId, String tenantId, FtbPermissionRoleInfoVO vo, String + roleId){ + List data = moduleApi.getModuleWithSystemId(systemId, tenantId); + if (CollUtil.isEmpty(data)) { + throw new RuntimeException("未找到1.0菜单模块备份表,请先执行1.0备份语句进行权限同步!"); + } + List permissionMenuDirectoryVOS = getMenuDirectoryVOS(roleId, data, vo); + List relations = ftbPermissionMigrateMapper.characterCheckedMenusAndButtons(roleId); + return permissionMenuDirectoryVOS.stream().map(v -> { + FtbPermissionModuleVO moduleVO = new FtbPermissionModuleVO(); + moduleVO.setModuleName(v.getFullName()); + List directoryInfoVOS = v.getChildren(); + List directoryList = new ArrayList<>(); + // 代码复用 + shareCode1("Web", directoryInfoVOS, tenantId, roleId, relations, directoryList); + shareCode1("App", directoryInfoVOS, tenantId, roleId, relations, directoryList); + moduleVO.setDirectoryInfoVO(directoryList); + return moduleVO; + }).collect(Collectors.toList()); + } + public List buildData2(String systemId, String tenantId, FtbPermissionRoleInfoVO vo, String + roleId){ + List dataBakUp = moduleApi.getListWithoutSystemId(systemId, null, null, tenantId); + List permissionMenuDirectoryVOS = getMenuDirectoryVOS(roleId, dataBakUp, vo); + // 查询当前角色的菜单配置 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleMenuRelation::getRoleId, roleId); + List relations2 = ftbPermissionRoleMenuRelationMapper.selectList(wrapper); + return permissionMenuDirectoryVOS.stream().map(v -> { + FtbPermissionModuleVO moduleVO = new FtbPermissionModuleVO(); + moduleVO.setModuleName(v.getFullName()); + List directoryInfoVOS = v.getChildren(); + List directoryList = new ArrayList<>(); + // 代码复用 + shareCode2("Web", directoryInfoVOS, tenantId, roleId, relations2, directoryList); + shareCode2("App", directoryInfoVOS, tenantId, roleId, relations2, directoryList); + moduleVO.setDirectoryInfoVO(directoryList); + return moduleVO; + }).collect(Collectors.toList()); + } + + /** + * 构建统一数据2.0 + * @return + */ + public void shareCode2(String category, + List directoryInfoVOS, + String tenantId, + String roleId, + List relations2, + List directoryList){ + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO infoVO = directoryInfoVOS.stream().filter(c->category.equals(c.getCategory())).findFirst().orElse(null); + if (infoVO == null) return; + String mod = infoVO.getId(); + FtbPermissionModuleVO.FtbPermissionMenuDirectory directory = new FtbPermissionModuleVO.FtbPermissionMenuDirectory(); + directory.setId(infoVO.getId()); + directory.setCategory(infoVO.getCategory()); + // 查询所有子集 + List newData = new ArrayList<>(); + getTreeMenuWithPreantId2(mod, tenantId, newData); + List list = JsonUtil.getJsonToList(newData, UserMenuModel.class); + List> menuList = TreeDotUtils.convertListToTreeDot(list); + List toList = JsonUtil.getJsonToList(menuList, MenuListVO.class); + // 根据所有的子集查询是否配置的菜单且需要递归获取配置进行复原 + // 构建2.0菜单详情 + List configTreeVOS = menuDetails2(roleId, toList, relations2); + directory.setPermissionsMenu(configTreeVOS); + directoryList.add(directory); + } + + /** + * 构建统一数据1.0 + * @return + */ + public void shareCode1(String category, + List directoryInfoVOS, + String tenantId, + String roleId, + List relations, + List directoryList){ + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO infoVO = directoryInfoVOS.stream().filter(c->category.equals(c.getCategory())).findFirst().orElse(null); + if (infoVO == null) return; + String mod = infoVO.getId(); + FtbPermissionModuleVO.FtbPermissionMenuDirectory directory = new FtbPermissionModuleVO.FtbPermissionMenuDirectory(); + directory.setId(infoVO.getId()); + directory.setCategory(infoVO.getCategory()); + // 查询所有子集 + List newData = new ArrayList<>(); + getTreeMenuWithPreantId(mod, tenantId, newData); + List list = JsonUtil.getJsonToList(newData, UserMenuModel.class); + List> menuList = TreeDotUtils.convertListToTreeDot(list); + List toList = JsonUtil.getJsonToList(menuList, MenuListVO.class); + // 根据所有的子集查询是否配置的菜单且需要递归获取配置进行复原 + // 构建1.0菜单详情 + List configTreeVOS = menuDetails1(roleId, toList, relations); + directory.setPermissionsMenu(configTreeVOS); + directoryList.add(directory); + } + + /** + * 构建菜单 + * @param roleId + * @param data + * @param vo + * @return + */ + @NotNull + private static List getMenuDirectoryVOS(String roleId, List data, FtbPermissionRoleInfoVO vo) { + // 获取顶级目录菜单 + List entities = data.stream().filter(item -> item.getType() != null && item.getEnabledMark() != null && 1 == item.getEnabledMark() && "-1".equals(item.getParentId())).collect(Collectors.toList()); + // 父级菜单 + List directoryVOS = entities.stream().map(FtbPermissionMenuDirectoryVO::convert).distinct().collect(Collectors.toList()); + for (FtbPermissionMenuDirectoryVO directoryVO : directoryVOS) { + List infoVOS = entities.stream().filter(v -> v.getFullName().equals(directoryVO.getFullName())).map(v -> { + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO infoVO = new FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO(); + String vCategory = v.getCategory(); + String vId = v.getId(); + infoVO.setId(vId); + infoVO.setCategory(vCategory); + return infoVO; + }).collect(Collectors.toList()); + if (StringUtils.isNotEmpty(roleId) && vo != null) { + List functionIds = new ArrayList<>(); + if ( vo.getFunctionWebId() != null ){ + String[] split = vo.getFunctionWebId().split(","); + functionIds .addAll(List.of(split)); + } + if ( vo.getFunctionAppId() != null ){ + functionIds.addAll(List.of(vo.getFunctionAppId().split(","))); + } + // 拼接并过滤非空字段 + functionIds.removeIf(StrUtil::isBlank); + List fullNames = entities.stream() + .filter(item -> functionIds.contains(item.getId())) + .map(ModuleEntity::getFullName).distinct().collect(Collectors.toList()); + if (fullNames.contains(directoryVO.getFullName())) { + directoryVO.setIsUse(true); + } + } + // 全部保留 + directoryVO.setChildren(infoVOS); + } + return directoryVOS.stream() + .filter(item -> !FtbPermissionFunctionMenuServiceImpl.EXCLUDED_DIRECTORIES.contains(item.getFullName())) + .collect(Collectors.toList()); + } + + /** + * 构建1.0菜单详情 + * + * @param roleId + * @param toList + * @param relations + * @return + */ + private List menuDetails1(String roleId, List toList, List relations) { + List configTrees = new ArrayList<>(); + buildMenuDetails1(toList, configTrees); + if (roleId != null && CollUtil.isNotEmpty(relations)) { + configTrees.forEach(item -> { + boolean anyMatch = relations.stream().anyMatch(relation -> relation.equals(item.getId())); + if (anyMatch) { + item.setIsChecked(true); + } + }); + } + List> trees = TreeDotUtils.convertListToTreeDot(configTrees); + return JsonUtil.getJsonToList(trees, FtbPermissionMigrateRoleTreeVO.class); + } + + + /** + * 构建2.0菜单详情 + * + * @param roleId + * @param toList + * @return + */ + private List menuDetails2(String roleId, List toList, List relations) { + List configTrees = new ArrayList<>(); + buildMenuDetails2(toList, configTrees); + if (roleId != null && CollUtil.isNotEmpty(relations)) { + // 查询当前角色的菜单配置 + configTrees.forEach(item -> { + boolean anyMatch = relations.stream().anyMatch(relation -> relation.getRoleMenuConfigId().equals(item.getId())); + if (anyMatch) { + item.setIsChecked(true); + } + }); + } + List> trees = TreeDotUtils.convertListToTreeDot(configTrees); + List jsonToList = JsonUtil.getJsonToList(trees, FtbPermissionMenuConfigTreeVO.class); + return jsonToList; + } + + /** + * 查询子集 + * @param meuId + * @param tenantId + * @param data + */ + private void getTreeMenuWithPreantId(String meuId, String tenantId,List data){ + List moduleEntityList = moduleApi.findAllSubsetsBasedOnParentBakUp(meuId,tenantId); + if (CollUtil.isNotEmpty(moduleEntityList)){ + List moduleEntities = moduleEntityList.stream().map(ModuleBakUpEntity::convert).collect(Collectors.toList()); + data.addAll(moduleEntities); + moduleEntityList.forEach(vo->{ + getTreeMenuWithPreantId(vo.getId(),tenantId,data); + }); + } + } + + + private void getTreeMenuWithPreantId2(String meuId, String tenantId, List data) { + List moduleEntityList = moduleApi.findAllSubsetsBasedOnParent(meuId,tenantId); + if (CollUtil.isNotEmpty(moduleEntityList)){ + data.addAll(moduleEntityList); + moduleEntityList.forEach(vo->{ + getTreeMenuWithPreantId2(vo.getId(),tenantId,data); + }); + } + } + public void buildMenuDetails2(List newData, List list){ + for (MenuListVO moduleEntity : newData) { + String id = moduleEntity.getId(); + FtbPermissionMenuConfigTree configTree = new FtbPermissionMenuConfigTree(); + configTree.setId(id); + configTree.setName(moduleEntity.getFullName()); + configTree.setParentId(moduleEntity.getParentId()); + configTree.setModuleType(moduleEntity.getType()); + list.add(configTree); + // 查询当前菜单是否配置了子页面 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionFunctionMenu::getFunType,0); + queryWrapper.eq(FtbPermissionFunctionMenu::getParentId,id); + List functionMenuList = ftbPermissionFunctionMenuMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(functionMenuList)){ + // 查询当前菜单是否有子集 + list.addAll(functionMenuList.stream().map(FtbPermissionMenuConfigTree::convert).collect(Collectors.toList())); + for (FtbPermissionFunctionMenu functionMenu : functionMenuList) { + LambdaQueryWrapper menuLambdaQueryWrapper = Wrappers.lambdaQuery(); + menuLambdaQueryWrapper.eq(FtbPermissionFunctionMenu::getFunType,1); + menuLambdaQueryWrapper.eq(FtbPermissionFunctionMenu::getParentId,functionMenu.getId()); + List buttonList = ftbPermissionFunctionMenuMapper.selectList(menuLambdaQueryWrapper); + if (CollUtil.isNotEmpty(buttonList)){ + list.addAll(buttonList.stream().map(FtbPermissionMenuConfigTree::convert).collect(Collectors.toList())); + } + } + }else if (moduleEntity.getChildren() != null){ + // 子集递归 + List children = moduleEntity.getChildren(); + buildMenuDetails2(children, list); + } + } + + } + private void buildMenuDetails1(List newData, List list) { + for (MenuListVO moduleEntity : newData) { + String id = moduleEntity.getId(); + FtbPermissionMigrateRoleTree configTree = new FtbPermissionMigrateRoleTree(); + configTree.setId(id); + configTree.setName(moduleEntity.getFullName()); + configTree.setParentId(moduleEntity.getParentId()); + configTree.setModuleType(1); + list.add(configTree); + // 查询当前菜单是否配置了按钮 + List buttonEntities = ftbPermissionMigrateMapper.inquiryButton(id); + if (buttonEntities != null && !buttonEntities.isEmpty()) { + List collected = buttonEntities.stream().map(v -> { + FtbPermissionMigrateRoleTree buttonTree = new FtbPermissionMigrateRoleTree(); + buttonTree.setId(v.getId()); + buttonTree.setName(v.getFullName()); + buttonTree.setParentId(id); + buttonTree.setModuleType(2); + return buttonTree; + }).collect(Collectors.toList()); + list.addAll(collected); + }else if (moduleEntity.getChildren() != null){ + buildMenuDetails1(moduleEntity.getChildren(), list); + } + } + } + private List personnelMenu(List userIds, Integer type) { + List authorities = ftbPermissionMigrateMapper.personnelMenuAuthorities(userIds); + List ftbPermissionPositionMenuInnerVOS = ftbPermissionMigrateMapper.personnelMenu(type); + if (CollUtil.isEmpty(authorities)) { + return new ArrayList<>(); + } + return recursivePermissions(ftbPermissionPositionMenuInnerVOS, "0", authorities); + } + + + /** + * 递归权限 + */ + private List recursivePermissions( + List ftbPersonnelsAuthoritysList, + String parentId, List includeMap) { + List result = new ArrayList<>(); + for (FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO authority : ftbPersonnelsAuthoritysList) { + FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO permissionVO = new FtbPermissionPositionMenuVO.FtbPermissionPositionMenuInnerVO(); + permissionVO.setMenuId(authority.getMenuId()); + permissionVO.setMenuName(authority.getMenuName()); + permissionVO.setParentId(authority.getParentId()); + permissionVO.setChecked(includeMap != null && includeMap.contains(authority.getMenuId())); + if (parentId.equals(permissionVO.getParentId())) { + permissionVO.setChildrenList(recursivePermissions(ftbPersonnelsAuthoritysList, authority.getMenuId(), includeMap)); + result.add(permissionVO); + } + } + return result; + } + + private Integer convertScopePermission(Integer scopePermission) { + // 根据这个数字0所在组织和下级组织员工,1所在组织员工,2仅下属,3指定组织 转换为-> 0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + switch (scopePermission) { + case 0: + return 1; + case 1: + return 2; + case 2: + return 3; + case 3: + return 4; + default: + return 0; + } + } + + public List getAListOfMenuDirectory(String tenantId) { + SystemEntity systemVO = systemApi.getSystemVO(); + String systemId = systemVO.getId(); + FtbPermissionRoleInfoVO vo = new FtbPermissionRoleInfoVO(); + List data = moduleApi.getListWithoutSystemId(systemId, null, null, tenantId); + // 获取顶级目录菜单 + List entities = data.stream().filter(item -> item.getType() != null && item.getEnabledMark() != null && 1 == item.getEnabledMark() && "-1".equals(item.getParentId())).collect(Collectors.toList()); + List directoryVOS = entities.stream().map(FtbPermissionMenuDirectoryVO::convert).distinct().collect(Collectors.toList()); + for (FtbPermissionMenuDirectoryVO directoryVO : directoryVOS) { + List infoVOS = entities.stream().filter(v -> v.getFullName().equals(directoryVO.getFullName())).map(v -> { + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO infoVO = new FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO(); + String vCategory = v.getCategory(); + String vId = v.getId(); + infoVO.setId(vId); + infoVO.setCategory(vCategory); + return infoVO; + }).collect(Collectors.toList()); + directoryVO.setChildren(infoVOS); + } + return directoryVOS.stream() + .filter(item -> !EXCLUDED_DIRECTORIES.contains(item.getFullName())) + .collect(Collectors.toList()); + } + + public static final Set EXCLUDED_DIRECTORIES = Set.of( + "资料库", "权限管理", "组织架构管理" + ); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionOrganizeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionOrganizeServiceImpl.java new file mode 100644 index 0000000..eaa65f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionOrganizeServiceImpl.java @@ -0,0 +1,259 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.permission.StoreApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.dto.store.StoreListInfoDTO; +import jnpf.permission.dto.v2.AuthUserNodeDTO; +import jnpf.permission.dto.v2.organzie.AuthOrganizeUserNodeDTO; +import jnpf.permission.dto.v2.organzie.QueryOrganizeNodeDTO; +import jnpf.permission.eum.StoreDisabledEnum; +import jnpf.permission.eum.v2.NodeTypeEnum; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.util.UserProvider; +import jnpf.util.auth.V2AuthPermissionUtils; +import jnpf.util.permssion.V2Utils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 带权限过滤的组织功能 + * + * @author Flynn Chan + * @create 2025-05-06 + */ +@Service +@Slf4j +public class FtbPermissionOrganizeServiceImpl implements FtbPermissionOrganizeService { + @Resource + private V2AuthPermissionUtils authPermissionUtils; + @Resource + private V2OrganizeApi organizeApi; + @Resource + private StoreApi storeApi; + @Resource + private FtbPersonnelsTurnoverManagementService turnoverManagementService; + + @Override + public List allOrganizeUsersTree(QueryOrganizeNodeDTO dto) { + List vos = organizeApi.allOrganizeUsersListApi(dto); + //查询离职的人 + if (CollUtil.isNotEmpty(vos)) { + FtbDepUserDTO ftbDepUserDTO = new FtbDepUserDTO(); + ftbDepUserDTO.setOrganizeIds(vos.stream().map(OrganizeManagerNodeVO::getId).collect(Collectors.toList())); + List ftbDepUsers = turnoverManagementService.getInformationAboutTheDepartingPerson(ftbDepUserDTO); + if (CollUtil.isEmpty(ftbDepUsers)) { + log.error("没有离职的人,查询参数情况[{}], 离职请求参数情况[{}]", dto, ftbDepUserDTO); + } else { + ftbDepUsers = ftbDepUsers.stream().filter(data -> StrUtil.isNotBlank(data.getOrganizeId())).collect(Collectors.toList()); + vos.addAll( + ftbDepUsers.stream().map(userBoundVO -> { + OrganizeManagerNodeVO organizeManagerNodeVO = new OrganizeManagerNodeVO(); + organizeManagerNodeVO.setName(userBoundVO.getUserName()); + organizeManagerNodeVO.setNodeTypeEnum(NodeTypeEnum.USER); + organizeManagerNodeVO.setNodeType(organizeManagerNodeVO.getNodeTypeEnum().getNum()); + organizeManagerNodeVO.setNodeTypeName(organizeManagerNodeVO.getNodeTypeEnum().getCode()); + organizeManagerNodeVO.setNodeTypeValue(organizeManagerNodeVO.getNodeTypeEnum().getDescription()); + organizeManagerNodeVO.setLeaderId(userBoundVO.getLeaderId()); + organizeManagerNodeVO.setPid(userBoundVO.getOrganizeId()); + organizeManagerNodeVO.setId(userBoundVO.getId()); + organizeManagerNodeVO.setHeadIcon(userBoundVO.getHeadIcon()); + return organizeManagerNodeVO; + }).collect(Collectors.toList()) + ); + } + } + vos = V2Utils.changeToTreeNoParent(vos, OrganizeManagerNodeVO.class); + return vos; + } + + @Override + public List listOrganizeTreeUsers(List organizeCategoryEnums, + List workStatusEnums, String organizeKeyword, Boolean withEmployee, Boolean includeResigned) { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthOrganizeUserNodeDTO dto = new AuthOrganizeUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(authIdsVO != null); + dto.setHaveUsers(withEmployee); + dto.setOrganizeKeyword(organizeKeyword); + dto.setOrganizeCategoryEnums(organizeCategoryEnums); + if (CollUtil.isEmpty(workStatusEnums)) { + dto.setWorkStatusEnums(UserWorkStatusEnums.getQueryUserFilterWorkStatusEnums()); + } else { + dto.setWorkStatusEnums(workStatusEnums); + } + List vos = organizeApi.listOrganizeTreeUsersApi(dto); + //搜索离职的人 + if (includeResigned) { + FtbDepUserDTO ftbDepUserDTO = new FtbDepUserDTO(); + if (authIdsVO != null) { + //组织权限 + if (authIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.ORGANIZE)) { + ftbDepUserDTO.setOrganizeIds(authIdsVO.getIds()); + } + //岗位权限 + if (authIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.POSITION)) { + ftbDepUserDTO.setPositionIds(authIdsVO.getIds()); + } + //人员权限 + if (authIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.USER)) { + ftbDepUserDTO.setUserIds(authIdsVO.getIds()); + } + } else { + ftbDepUserDTO = new FtbDepUserDTO(); + } + List userIds = vos.stream().filter(data -> data.getNodeTypeEnum().equals(NodeTypeEnum.USER)).map(OrganizeManagerNodeVO::getId).collect(Collectors.toList()); + List ftbDepUsers = turnoverManagementService.getInformationAboutTheDepartingPerson(ftbDepUserDTO); + if (CollUtil.isEmpty(ftbDepUsers)) { + log.error("没有离职的人,权限参数情况[{}], 离职请求参数情况[{}]", authIdsVO, ftbDepUserDTO); + } else { + ftbDepUsers = ftbDepUsers.stream().filter(data -> + StrUtil.isNotBlank(data.getOrganizeId()) && !userIds.contains(data.getId()) + ).collect(Collectors.toList()); + vos.addAll( + ftbDepUsers.stream().map(userBoundVO -> { + OrganizeManagerNodeVO organizeManagerNodeVO = new OrganizeManagerNodeVO(); + organizeManagerNodeVO.setName(userBoundVO.getUserName()); + organizeManagerNodeVO.setNodeTypeEnum(NodeTypeEnum.USER); + organizeManagerNodeVO.setNodeType(organizeManagerNodeVO.getNodeTypeEnum().getNum()); + organizeManagerNodeVO.setNodeTypeName(organizeManagerNodeVO.getNodeTypeEnum().getCode()); + organizeManagerNodeVO.setNodeTypeValue(organizeManagerNodeVO.getNodeTypeEnum().getDescription()); + organizeManagerNodeVO.setLeaderId(userBoundVO.getLeaderId()); + organizeManagerNodeVO.setPid(userBoundVO.getOrganizeId()); + organizeManagerNodeVO.setId(userBoundVO.getId()); + organizeManagerNodeVO.setHeadIcon(userBoundVO.getHeadIcon()); + return organizeManagerNodeVO; + }).collect(Collectors.toList()) + ); + } + } + + vos = V2Utils.changeToTreeNoParent(vos, OrganizeManagerNodeVO.class); + return vos; + } + + @Override + public List listOrganizeTreeUsersFilterNodeJson(List organizeCategoryEnums, + List workStatusEnums, + Boolean withEmployee, + Boolean filterBindOtherStore, + Boolean filterBindPayStore + ) { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthOrganizeUserNodeDTO dto = new AuthOrganizeUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(authIdsVO != null); + dto.setHaveUsers(null == withEmployee ? Boolean.TRUE : withEmployee); + dto.setFilterBindOtherStore(filterBindOtherStore); + dto.setFilterBindPayStore(filterBindPayStore); + dto.setOrganizeCategoryEnums(organizeCategoryEnums); + if (CollUtil.isEmpty(workStatusEnums)) { + dto.setWorkStatusEnums(UserWorkStatusEnums.getQueryUserFilterWorkStatusEnums()); + } else { + dto.setWorkStatusEnums(workStatusEnums); + } + List organizeManagerFilterNodes = organizeApi.listOrganizeTreeUsersFilterNodeJson(dto); +// log.error("ftb组织树过滤节点isEnable 数量为:{}", organizeManagerFilterNodes.stream().filter(o -> o.getIsEnabled() != null && o.getIsEnabled()).count()); + return organizeManagerFilterNodes; + } + + @Override + public List authOrganizesByUserBound(List organizeCategoryEnums, + String organizeKeyword, + Boolean filterBindOtherStore, + Boolean filterBindPayStore + ) { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthUserNodeDTO dto = new AuthUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(authIdsVO != null); + dto.setOrganizeCategoryEnums(organizeCategoryEnums); + dto.setOrganizeKeyword(organizeKeyword); + dto.setFilterBindOtherStore(filterBindOtherStore); + dto.setFilterBindPayStore(filterBindPayStore); + dto.setOrganizeKeyword(organizeKeyword); + return organizeApi.authOrganizesByUserBound(dto).getData(); + } + + /** + * 批量获取有权限的门店 + * + * @param userIds 用户id集合 + * @param status 状态 1:禁用 0:启用 -1-所有 + * @return 用户ID的map + */ + @Override + public Map> batchAuthOrganizesForUserIds(List userIds, Integer status) { + return authPermissionUtils.batchAuthOrganizesForUserIds(userIds, status); + } + /** + * 批量用户有权限的门店 + * + * @param userIds 用户ids + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param moduleId 模块id + * @param tenantId 租户id + * @return + */ + @Override + public Map> batchAuthOrganizesForUserIdsAndTenantId(List userIds, Integer status, String moduleId, String tenantId) { + return authPermissionUtils.batchAuthOrganizesForUserIdsAndTenantId(userIds, status, moduleId, tenantId); + } + + @Override + public Map batchAuthOrganizesAllForUserIds(List userIds) { + return authPermissionUtils.batchAuthOrganizesAllForUserIds(userIds); + } + + @Override + public TargetAuthIdsVO getUserAuth() { + + return authPermissionUtils.processAuthIds(); + } + + /** + * 批量用户有权限的门店 + * + * @param userIds 用户ids + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param moduleId 模块id + * @param tenantId 租户id + * @return + */ + @Override + public Map> batchAuthOrganizesAllForUserIdsAndTenantId(List userIds, Integer status, String moduleId, String tenantId) { + return authPermissionUtils.batchAuthOrganizesAll(userIds, status, moduleId, tenantId); + } + + @Override + public List authStoreBaseListInfo() { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthUserNodeDTO dto1 = new AuthUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto1.setHavaAuth(authIdsVO != null); + dto1.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.STORE)); + List organizeGeneralDetailVOS = organizeApi.authOrganizesByUserBound(dto1).getData(); + if (CollUtil.isEmpty(organizeGeneralDetailVOS)) { + return new ArrayList<>(); + } + StoreListInfoDTO dto = new StoreListInfoDTO(); + dto.setStoreIds(organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + dto.setStoreDisabledEnum(StoreDisabledEnum.ENABLED); + return storeApi.storeBaseInfoList(dto).getData(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionPositionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionPositionServiceImpl.java new file mode 100644 index 0000000..8a9a04c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionPositionServiceImpl.java @@ -0,0 +1,159 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import jnpf.authority.service.FtbPermissionPositionService; +import jnpf.model.authority.dto.permission.OrganizeWithPositionsDTO; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.permission.V2PositionApi; +import jnpf.permission.dto.v2.position.AuthOrganizePositionDTO; +import jnpf.permission.dto.v2.position.AuthPositionUserNodeDTO; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.organize.OrganizeAndPositionListVO; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.position.PositionListOrganizeUserVO; +import jnpf.permission.vo.v2.position.PositionListUserVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.util.UserProvider; +import jnpf.util.auth.V2AuthPermissionUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 带权限的岗位管理功能 + * + * @author Flynn Chan + * @create 2025-05-07 + */ +@Service +@Slf4j +public class FtbPermissionPositionServiceImpl implements FtbPermissionPositionService { + @Resource + private V2AuthPermissionUtils authPermissionUtils; + @Resource + private V2PositionApi positionApi; + @Resource + private FtbPersonnelsTurnoverManagementService turnoverManagementService; + + @Override + public List authListPositionTreeUser(List workStatusEnums, Boolean includeResigned) { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthPositionUserNodeDTO dto = new AuthPositionUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + if (CollUtil.isEmpty(workStatusEnums)) { + dto.setWorkStatusEnums(UserWorkStatusEnums.getSelectUserFilterWorkStatusEnums()); + } else { + dto.setWorkStatusEnums(workStatusEnums); + } + dto.setHavaAuth(authIdsVO != null); + List vos = positionApi.authListPositionTreeUser(dto).getData(); + //搜索离职的人 + if (includeResigned) { + FtbDepUserDTO ftbDepUserDTO = new FtbDepUserDTO(); + if (authIdsVO != null) { + //组织权限 + if (authIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.ORGANIZE)) { + ftbDepUserDTO.setOrganizeIds(authIdsVO.getIds()); + } + //岗位权限 + if (authIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.POSITION)) { + ftbDepUserDTO.setPositionIds(authIdsVO.getIds()); + } + //人员权限 + if (authIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.USER)) { + ftbDepUserDTO.setUserIds(authIdsVO.getIds()); + } + } else { + ftbDepUserDTO = new FtbDepUserDTO(); + } + List ftbDepUsers = turnoverManagementService.getInformationAboutTheDepartingPerson(ftbDepUserDTO); + if (CollUtil.isEmpty(ftbDepUsers)) { + log.error("没有离职的人,权限参数情况[{}], 离职请求参数情况[{}]", authIdsVO, ftbDepUserDTO); + } else { + // 预过滤,去掉 organizeId 或 positionId 为空的 + Map> usersByPosition = ftbDepUsers.stream() + .filter(u -> StrUtil.isNotBlank(u.getOrganizeId()) && StrUtil.isNotBlank(u.getPositionId())) + .collect(Collectors.groupingBy(UserBoundVO::getPositionId)); + // 遍历 vos,直接按 positionId 取对应离职人员列表 + vos.forEach(vo -> { + List matchedUsers = usersByPosition.get(vo.getId()); + if (CollUtil.isNotEmpty(matchedUsers)) { + vo.getUserList().addAll(matchedUsers); + } + }); + } + } + + return vos; + } + + @Override + public List authListPositionAuth() { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthPositionUserNodeDTO dto = new AuthPositionUserNodeDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(authIdsVO != null); + return positionApi.authListPositionAuth(dto); + } + + @Override + public List listPositionOrganizeUser(List filterUserWorkStatusEnumsList) { + List vos = positionApi.listPositionOrganizeUserApi(UserProvider.getUser().getTenantId(), filterUserWorkStatusEnumsList); + if (CollUtil.isNotEmpty(vos)) { + //搜索离职的人 + FtbDepUserDTO ftbDepUserDTO = new FtbDepUserDTO(); + List positionIds = vos.stream().map(PositionListOrganizeUserVO::getId).collect(Collectors.toList()); + ftbDepUserDTO.setPositionIds(positionIds); + List ftbDepUsers = turnoverManagementService.getInformationAboutTheDepartingPerson(ftbDepUserDTO); + + if (CollUtil.isNotEmpty(ftbDepUsers)) { + // 一次性过滤掉 organizeId、positionId 为空的 + List filteredUsers = ftbDepUsers.stream() + .filter(u -> StrUtil.isNotBlank(u.getOrganizeId()) && StrUtil.isNotBlank(u.getPositionId())) + .collect(Collectors.toList()); + + // 构建两层 map:positionId -> organizeId -> List + Map>> userMap = filteredUsers.stream() + .collect(Collectors.groupingBy( + UserBoundVO::getPositionId, + Collectors.groupingBy(UserBoundVO::getOrganizeId) + )); + + // 遍历 vos,直接按 map 获取对应数据 + vos.forEach(vo -> { + Map> organizeMap = userMap.get(vo.getId()); + if (CollUtil.isEmpty(organizeMap)) { + return; + } + + vo.getList().forEach(organizeUserListVO -> { + List matchedUsers = organizeMap.get(organizeUserListVO.getId()); + if (CollUtil.isNotEmpty(matchedUsers)) { + organizeUserListVO.getUserList().addAll(matchedUsers); + } + }); + }); + } + + return vos; + } else { + return new ArrayList<>(); + } + } + + @Override + public List authOrganizeWithPositions(OrganizeWithPositionsDTO thisDto) { + TargetAuthIdsVO authIdsVO = authPermissionUtils.processAuthIds(); + AuthOrganizePositionDTO dto = new AuthOrganizePositionDTO(authIdsVO, UserProvider.getUser().getTenantId()); + dto.setOrganizeIds(dto.getOrganizeIds()); + dto.setHavaAuth(authIdsVO != null); + return positionApi.authOrganizeWithPositions(dto); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePersonServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePersonServiceImpl.java new file mode 100644 index 0000000..769e2ed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePersonServiceImpl.java @@ -0,0 +1,603 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import jnpf.authority.mapper.*; +import jnpf.authority.service.FtbPermissionFunctionMenuService; +import jnpf.authority.service.FtbPermissionRoleAuthorizePersonService; +import jnpf.base.ActionResult; +import jnpf.base.ModuleApi; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.authority.dto.authorize.FtbPermissionAuthorizeVO; +import jnpf.model.authority.dto.menu.FuntionMenuDTO; +import jnpf.model.authority.dto.person.FtbPermissionRoleBatchDeleteDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonAddDTO; +import jnpf.model.authority.dto.person.FtbPermissionRolePersonRelationDTO; +import jnpf.model.authority.dto.person.FunctionDTO; +import jnpf.model.authority.po.*; +import jnpf.model.authority.vo.menu.FtbPermissionMenuDirectoryVO; +import jnpf.model.authority.vo.person.FtbAuthorizedPersonnelVO; +import jnpf.model.authority.vo.person.FtbEmployeePermissionPersonnelVO; +import jnpf.model.authority.vo.person.FtbRoleListDropDownVO; +import jnpf.model.authority.vo.person.FunctionVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.AuthUtil; +import jnpf.util.CustomTenantUtil; +import jnpf.util.UserProvider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * + * + *@Author: peng.hao + *@create: 2025/3/5 + * + */ +@Service +public class FtbPermissionRoleAuthorizePersonServiceImpl extends ServiceImpl implements FtbPermissionRoleAuthorizePersonService{ + + + + @Autowired + V2UserApi v2UserApi; + + @Resource + private FtbPermissionRoleMapper permissionRoleMapper; + + @Resource + private FtbPermissionRolePersonUserRelationMapper ftbPermissionRolePersonUserRelationMapper; + + @Resource + private FtbPermissionRoleAuthorizePostMapper ftbPermissionRoleAuthorizePostMapper; + + @Resource + FtbPermissionFunctionMenuService ftbPermissionFunctionMenuService; + + @Resource + ModuleApi moduleApi; + + @Resource + private CustomTenantUtil customTenantUtil; + + @Resource + FtbPermissionRoleMenuRelationMapper ftbPermissionRoleMenuRelationMapper; + + @Override + public PageListVO authorizedPersonnel(Page page, String roleId) { + Page pageListVO = baseMapper.authorizedPersonnel(page,roleId); + if (CollUtil.isEmpty(pageListVO.getRecords())) return CultivatePage.coverPageList(page); + getPersonnelVOS(pageListVO.getRecords()); + return CultivatePage.coverPageList(pageListVO); + } + + @NotNull + private void getPersonnelVOS(List list) { + List userIds = list.stream().map(FtbAuthorizedPersonnelVO::getUserId).collect(Collectors.toList()); + ActionResult> boundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + List data = boundBatch.getData(); + Map userIdMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (r1, r2) -> r1)); + list.forEach(v -> { + if (userIdMap.containsKey(v.getUserId())) { + UserBoundVO infoVO = userIdMap.get(v.getUserId()); + v.setOrgName(infoVO.getOrganizeName()); + v.setPostName(infoVO.getPositionName()); + } + }); + } + + @Override + public PageListVO employeePermission(Page page, String userName, List userIds) { + Page newPageList = baseMapper.employeePermissionLPage(page,userName,null,userIds); + List listRecords = newPageList.getRecords(); + supplementalData(listRecords); + return CultivatePage.coverPageList(newPageList); + } + private void supplementalData(List list) { + List userIds = list.stream().map(FtbEmployeePermissionPersonnelVO::getUserId).collect(Collectors.toList()); + List records = baseMapper.employeePermission(userIds); + ActionResult> listActionResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + List data = listActionResult.getData(); + if (data == null) return; + Map userBoundVOMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + List positionIds = data.stream().filter(v-> StringUtils.isNotEmpty(v.getPositionId())).map(UserBoundVO::getPositionId).filter(Objects::nonNull).collect(Collectors.toList()); + Map> postMaps = new HashMap<>(); + if (CollUtil.isNotEmpty(positionIds)) { + LambdaQueryWrapper postQuery = Wrappers.lambdaQuery(); + postQuery.in(FtbPermissionRoleAuthorizePost::getPostId, positionIds); + postQuery.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List authorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(postQuery); + postMaps = authorizePosts.stream().collect(Collectors.groupingBy(FtbPermissionRoleAuthorizePost::getPostId)); + } + Map personnelVOMap = records.stream().collect(Collectors.toMap(FtbEmployeePermissionPersonnelVO::getUserId, Function.identity(), (r1, r2) -> r1)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRole::getEnableMark,0); + List rolesList = permissionRoleMapper.selectList(wrapper); + Map roleMap = rolesList.stream().collect(Collectors.toMap(FtbPermissionRole::getId, Function.identity(), (r1, r2) -> r1)); + Map> finalPostMaps = postMaps; + list.forEach(v->{ + List roleIds = new ArrayList<>(); + if (userBoundVOMap.containsKey(v.getUserId())){ + UserBoundVO infoVO = userBoundVOMap.get(v.getUserId()); + v.setOrgName(infoVO.getOrganizeName()); + v.setPostId(infoVO.getPositionId()); + v.setPostName(infoVO.getPositionName()); + } + if (finalPostMaps.containsKey(v.getPostId())){ + finalPostMaps.get(v.getPostId()).stream().map(FtbPermissionRoleAuthorizePost::getRoleId).forEach(roleIds::add); + } + if (personnelVOMap.containsKey(v.getUserId())){ + String[] split = personnelVOMap.get(v.getUserId()).getRoleIds().split(","); + roleIds.addAll(List.of(split)); + } + if (CollUtil.isNotEmpty(roleIds)){ + String roleIdsStr = roleIds.stream().distinct().collect(Collectors.joining(",")); + v.setRoleIds(roleIdsStr); + List roleNames = roleIds.stream().distinct().map(roleId -> { + if (!roleMap.containsKey(roleId)) return null; + return roleMap.get(roleId).getRoleName(); + }).filter(Objects::nonNull).collect(Collectors.toList()); + v.setRoleName(String.join(",", roleNames)); + } + }); + } + private void supplementalDataWithPost(List list) { + List userIds = list.stream().map(FtbEmployeePermissionPersonnelVO::getUserId).collect(Collectors.toList()); + ActionResult> listActionResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + List data = listActionResult.getData(); + if (data == null) return; + Map userBoundVOMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + List positionIds = data.stream().filter(v-> StringUtils.isNotEmpty(v.getPositionId())).map(UserBoundVO::getPositionId).filter(Objects::nonNull).collect(Collectors.toList()); + Map> postMaps = new HashMap<>(); + if (CollUtil.isNotEmpty(positionIds)) { + LambdaQueryWrapper postQuery = Wrappers.lambdaQuery(); + postQuery.in(FtbPermissionRoleAuthorizePost::getPostId, positionIds); + List authorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(postQuery); + postMaps = authorizePosts.stream().collect(Collectors.groupingBy(FtbPermissionRoleAuthorizePost::getPostId)); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + List rolesList = permissionRoleMapper.selectList(wrapper); + Map roleMap = rolesList.stream().collect(Collectors.toMap(FtbPermissionRole::getId, Function.identity(), (r1, r2) -> r1)); + Map> finalPostMaps = postMaps; + list.forEach(v->{ + List roleIds = new ArrayList<>(); + if (userBoundVOMap.containsKey(v.getUserId())){ + UserBoundVO infoVO = userBoundVOMap.get(v.getUserId()); + v.setOrgName(infoVO.getOrganizeName()); + v.setPostId(infoVO.getPositionId()); + v.setPostName(infoVO.getPositionName()); + } + if (finalPostMaps.containsKey(v.getPostId())){ + finalPostMaps.get(v.getPostId()).stream().map(FtbPermissionRoleAuthorizePost::getRoleId).forEach(roleIds::add); + } + if (CollUtil.isNotEmpty(roleIds)){ + String roleIdsStr = roleIds.stream().distinct().collect(Collectors.joining(",")); + v.setRoleIds(roleIdsStr); + List roleNames = roleIds.stream().distinct().map(roleId -> roleMap.get(roleId).getRoleName()).collect(Collectors.toList()); + v.setRoleName(String.join(",", roleNames)); + } + }); + } + + @Override + public List roleListDropDown(String userId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getUserId, userId); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark,0); + List people = baseMapper.selectList(wrapper); + ActionResult> primaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), UserProvider.getUser().getTenantId()); + List roleIds = new ArrayList<>(); + if(primaryBoundBatch != null && primaryBoundBatch.getData() != null){ + List data = primaryBoundBatch.getData(); + if (CollUtil.isNotEmpty(data) ){ + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getPostId, data.get(0).getPositionId()); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark, 0); + List roleAuthorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(roleAuthorizePosts)) { + roleIds = roleAuthorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getRoleId).collect(Collectors.toList()); + } + } + } + List roles = permissionRoleMapper.selectList(new LambdaQueryWrapper().eq(FtbPermissionRole::getEnableMark,0)); + List finalRoleIds = roleIds; + return roles.stream().map(v->{ + FtbRoleListDropDownVO covert = FtbRoleListDropDownVO.covert(v); + if (people.stream().anyMatch(p->p.getRoleId().equals(v.getId()))){ + covert.setIsOpen(true); + }else if (CollUtil.isNotEmpty(finalRoleIds) && finalRoleIds.contains(v.getId())){ + covert.setIsOpen(true); + } + return covert; + }).collect(Collectors.toList()); + } + @Override + public List allRolesOfTheCurrentPerson(String userId, String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getUserId, userId); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark,0); + List people = baseMapper.selectList(wrapper); + List roleIds = new ArrayList<>() ; + if (CollUtil.isNotEmpty(people)) { + roleIds.addAll(people.stream().map(FtbPermissionRoleAuthorizePerson::getRoleId).collect(Collectors.toList())) ; + } + ActionResult actionResult = v2UserApi.getUsersBound(userId, UserProvider.getUser().getTenantId()); + if(actionResult != null && actionResult.getData() != null) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getPostId, actionResult.getData().getPositionId()); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List authorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(authorizePosts)) { + roleIds.addAll(authorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getRoleId).collect(Collectors.toList())); + } + } + if (CollUtil.isEmpty(roleIds)) return null; + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPermissionRole::getEnableMark,0); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId,roleIds); + List roles = permissionRoleMapper.selectList(queryWrapper); + return roles.stream().map(v->{ + FtbRoleListDropDownVO covert = FtbRoleListDropDownVO.covert(v); + if (roles.contains(v)){ + covert.setIsOpen(true); + } + return covert; + }).collect(Collectors.toList()); + } + + @Override + public void addPersonalPermission(FtbPermissionRolePersonRelationDTO rolePersonAddDTO) { + String userId = rolePersonAddDTO.getUserId(); + // 先清空数据 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRolePersonUserRelation::getUserId, userId); + int delete = ftbPermissionRolePersonUserRelationMapper.delete(queryWrapper); + List functionDTOList = rolePersonAddDTO.getFunctionDTOList(); + long count = functionDTOList.stream().filter(v -> v.getRoleEnabled() == 1).count(); + // 无修改 + if (count == 0 && delete != 0) { + AuthUtil.kickoutByUserId(userId); + return; + } + List userRelationsList = functionDTOList.stream().map(v -> { + FtbPermissionRolePersonUserRelation userRelation = new FtbPermissionRolePersonUserRelation(); + userRelation.setUserId(userId); + userRelation.setFunctionWebId(v.getFunctionWebId()); + userRelation.setFunctionAppId(v.getFunctionAppId()); + userRelation.setFunctionPcId(v.getFunctionPcId()); + userRelation.setScopePermission(v.getScopePermission()); + if(CollUtil.isNotEmpty(v.getSpecifyOrgIds())) userRelation.setSpecifyOrgIds(v.getSpecifyOrgIds().stream().map(Object::toString).collect(Collectors.joining(","))); + userRelation.setRoleEnabled(v.getRoleEnabled()); + return userRelation; + }).collect(Collectors.toList()); + Db.saveBatch(userRelationsList); + AuthUtil.kickoutByUserId(userId); + } + + @Override + public List getPersonalPermission(String userId) { + LambdaQueryWrapper userRelationLambdaQueryWrapper = Wrappers.lambdaQuery(); + userRelationLambdaQueryWrapper.eq(FtbPermissionRolePersonUserRelation::getUserId, userId); + userRelationLambdaQueryWrapper.eq(FtbPermissionRolePersonUserRelation::getEnableMark, 0); + List userRelations = ftbPermissionRolePersonUserRelationMapper.selectList(userRelationLambdaQueryWrapper); + List roleIds = new ArrayList<>(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePerson::getUserId, userId); + queryWrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark, 0); + List roleAuthorizePeople = baseMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(roleAuthorizePeople)) { + roleIds.addAll(roleAuthorizePeople.stream().map(FtbPermissionRoleAuthorizePerson::getRoleId).collect(Collectors.toList())); + } + // 查询岗位信息绑定角色 + ActionResult> listActionResult = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), UserProvider.getUser().getTenantId()); + List data = listActionResult.getData(); + if (data == null) throw new RuntimeException("获取用户信息失败"); + List positionIds = data.stream().map(UserBoundVO::getPositionId).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(positionIds)) { + LambdaQueryWrapper postQuery = Wrappers.lambdaQuery(); + postQuery.in(FtbPermissionRoleAuthorizePost::getPostId, positionIds); + postQuery.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List authorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(postQuery); + if (CollUtil.isNotEmpty(authorizePosts)) { + roleIds.addAll(authorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getRoleId).collect(Collectors.toList())); + } + } + if (CollUtil.isEmpty(roleIds)) return null; + // 多个角色信息出现重合情况 + List> listList = roleIds.stream().map(v -> { + List directory = ftbPermissionFunctionMenuService.getAListOfMenuDirectory(v); + return directory.stream().filter(FtbPermissionMenuDirectoryVO::getIsUse).collect(Collectors.toList()); + }).collect(Collectors.toList()); + // 多个模块进行平铺 + List directory = listList.stream().flatMap(List::stream).distinct().collect(Collectors.toList()); + // 库回显 + Map functionVOMap = new HashMap<>(); + if (CollUtil.isNotEmpty(userRelations)) { + List functionVOList = userRelations.stream().map(v -> { + FunctionVO functionVO = new FunctionVO(); + functionVO.setFunctionWebId(v.getFunctionWebId()); + ModuleEntity moduleApiModuleByList = moduleApi.getModuleByList(v.getFunctionWebId()); + if (moduleApiModuleByList != null) { + functionVO.setModuleName(moduleApiModuleByList.getFullName()); + } else { + ModuleEntity moduleEntity = moduleApi.getModuleByList(v.getFunctionAppId()); + if (moduleEntity != null) { + functionVO.setModuleName(moduleEntity.getFullName()); + } else { + moduleEntity = moduleApi.getModuleByList(v.getFunctionPcId()); + if (moduleEntity != null) { + functionVO.setModuleName(moduleEntity.getFullName()); + } + } + } + functionVO.setFunctionPcId(v.getFunctionPcId()); + functionVO.setFunctionAppId(v.getFunctionAppId()); + functionVO.setScopePermission(v.getScopePermission()); + if (StringUtils.isNotEmpty(v.getSpecifyOrgIds())) + functionVO.setSpecifyOrgIds(List.of(v.getSpecifyOrgIds().split(","))); + functionVO.setRoleEnabled(v.getRoleEnabled()); + return functionVO; + }).collect(Collectors.toList()); + functionVOMap = functionVOList.stream().collect(Collectors.toMap(FunctionVO::getModuleName, v -> v, (a, b) -> a)); + } + Map finalFunctionVOMap = functionVOMap; + return directory.stream().map(v -> { + FunctionVO functionVO = new FunctionVO(); + functionVO.setModuleName(v.getFullName()); + FunctionVO vo = finalFunctionVOMap.get(v.getFullName()); + if (finalFunctionVOMap.containsKey(v.getFullName())){ + if (CollUtil.isNotEmpty(vo.getSpecifyOrgIds())) functionVO.setSpecifyOrgIds(vo.getSpecifyOrgIds()); + functionVO.setScopePermission(vo.getScopePermission()); + functionVO.setRoleEnabled(vo.getRoleEnabled()); + }else { + functionVO.setRoleEnabled(0); + } + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO orElseWeb = getDirectoryInfoVO("Web", v); + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO orElseApp = getDirectoryInfoVO("App", v); + FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO orElsePc = getDirectoryInfoVO("PC", v); + if (orElseWeb != null) { + functionVO.setFunctionWebId(orElseWeb.getId()); + } + if (orElseApp != null) { + functionVO.setFunctionAppId(orElseApp.getId()); + } + if (orElsePc != null) { + functionVO.setFunctionPcId(orElsePc.getId()); + } + return functionVO; + }).collect(Collectors.toList()); + } + + @Override + public List queryButtonPermission(FuntionMenuDTO funtion) throws LoginException { + TenantDataSourceUtil.switchTenant(funtion.getTenantId()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionFunctionMenu::getEnableMark,0); + queryWrapper.in(FtbPermissionFunctionMenu::getOriginalCoding, funtion.getButtonEncoding()); + List list = ftbPermissionFunctionMenuService.list(queryWrapper); + if (CollUtil.isEmpty(list)) return null; + Page objectPage = new Page<>(); + objectPage.setSize(-1); + PageListVO pageListVO = employeePermission(objectPage, null, funtion.getUserIds()); + List listRecords = pageListVO.getList(); + List roleIds = listRecords.stream().map(v -> { + if (StringUtils.isNotEmpty(v.getRoleIds())) { + return v.getRoleIds().split(","); + } + return null; + }).filter(Objects::nonNull).flatMap(Arrays::stream).collect(Collectors.toList()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleMenuRelation::getEnableMark,0); + wrapper.in(FtbPermissionRoleMenuRelation::getRoleId, roleIds); + wrapper.in(FtbPermissionRoleMenuRelation::getRoleMenuConfigId, list.stream().map(FtbPermissionFunctionMenu::getId).collect(Collectors.toList())); + List personUserRelations = ftbPermissionRoleMenuRelationMapper.selectList(wrapper); + if (CollUtil.isEmpty(personUserRelations)) return null; + // 包含按钮 + List roleids = personUserRelations.stream().map(FtbPermissionRoleMenuRelation::getRoleId).collect(Collectors.toList()); + Set roleIdSet = new HashSet<>(roleids); + List userIds = listRecords.stream().filter(v -> { + if (StringUtils.isNotEmpty(v.getRoleIds())) { + String[] split = v.getRoleIds().split(","); + return Arrays.stream(split).anyMatch(roleIdSet::contains); + } + return false; + }).map(FtbEmployeePermissionPersonnelVO::getUserId).collect(Collectors.toList()); + return userIds; + } + + @Nullable + private static FtbPermissionMenuDirectoryVO.FtbPermissionMenuDirectoryInfoVO getDirectoryInfoVO(String str, FtbPermissionMenuDirectoryVO v) { + return v.getChildren().stream().filter(c -> str.equals(c.getCategory())).findFirst().orElse(null); + } + + @Override + public void editEmployeePermission(List roleIds, String userId) { + Page page = new Page<>(); + page.setSize(-1); + Page newPageList = baseMapper.employeePermissionLPage(page,null,userId, null); + List listRecords = newPageList.getRecords(); + supplementalDataWithPost(listRecords); + FtbEmployeePermissionPersonnelVO personnelVO = listRecords.stream().filter(item -> userId.equals(item.getUserId())).findFirst().orElse(new FtbEmployeePermissionPersonnelVO()); + if (StringUtils.isNotEmpty(personnelVO.getRoleIds())) { + List stringList = List.of(personnelVO.getRoleIds().split(",")); + List postRoleIds = stringList.stream().filter(v -> !roleIds.contains(v)).collect(Collectors.toList()); + // 如果岗位授权到了角色,不能解绑 + if (CollUtil.isNotEmpty(postRoleIds)) { + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.in(FtbPermissionRoleAuthorizePost::getRoleId, postRoleIds); + postWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List posts = ftbPermissionRoleAuthorizePostMapper.selectList(postWrapper); + if (CollUtil.isNotEmpty(posts)) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.in(SuperBaseEntity.SuperIBaseEntity::getId, postRoleIds); + lambdaQuery.eq(FtbPermissionRole::getEnableMark,0); + List permissionRoles = permissionRoleMapper.selectList(lambdaQuery); + String collected = permissionRoles.stream().map(FtbPermissionRole::getRoleName).collect(Collectors.joining(",")); + throw new RuntimeException(collected + "已授权到岗位,不可解绑!"); + } + } + } + // 首先将管理员数据全部移除 + v2UserApi.userAdminEnable(userId, false); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getUserId, userId); + wrapper.set(FtbPermissionRoleAuthorizePerson::getEnableMark,1); + baseMapper.update(new FtbPermissionRoleAuthorizePerson(),wrapper); + if (CollUtil.isEmpty(roleIds)) { + // 删除所有员工权限 + // 员工踢下线 + AuthUtil.kickoutByUserId(userId); + return; + } + List collected = roleIds.stream().map(roleId -> { + FtbPermissionRoleAuthorizePerson person = new FtbPermissionRoleAuthorizePerson(); + person.setUserId(userId); + person.setRoleId(roleId); + LambdaQueryWrapper roleLambdaQueryWrapper = Wrappers.lambdaQuery(); + roleLambdaQueryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, roleId); + roleLambdaQueryWrapper.eq(FtbPermissionRole::getEnableMark,0); + FtbPermissionRole ftbPermissionRole = permissionRoleMapper.selectOne(roleLambdaQueryWrapper); + Integer isSuperAdmin = ftbPermissionRole.getIsSuperAdmin(); + // 同步超级管理员到base_user表 + if (isSuperAdmin == 1){ + syncSuperAdmin(userId, true); + } + return person; + }).collect(Collectors.toList()); + Db.saveBatch(collected); + // 员工踢下线 + collected.stream().map(FtbPermissionRoleAuthorizePerson::getUserId).forEach(AuthUtil::kickoutByUserId); + } + + @Override + public void add(FtbPermissionRolePersonAddDTO rolePersonAddDTO) { + String roleId = rolePersonAddDTO.getRoleId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, roleId); + FtbPermissionRole ftbPermissionRole = permissionRoleMapper.selectOne(wrapper); + Integer isSuperAdmin = ftbPermissionRole.getIsSuperAdmin(); + // 查询生效的数据 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePerson::getRoleId, roleId); + queryWrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark,0); + queryWrapper.ne(FtbPermissionRoleAuthorizePerson::getUserId,"349057407209541"); + // 清空所有绑定关系 + List people = baseMapper.selectList(queryWrapper); + List userIds = rolePersonAddDTO.getUserIds(); + List collect = people.stream().map(FtbPermissionRoleAuthorizePerson::getUserId).collect(Collectors.toList()); + // 空提交删除之前所有绑定的用户 + if (CollUtil.isEmpty(userIds) && CollUtil.isNotEmpty(people)){ + if (isSuperAdmin == 1) collect.forEach(v->syncSuperAdmin(v, false)); + // 删除所有员工权限 + baseMapper.deleteBatchIds(people.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList())); + // 员工踢下线 + collect.forEach(AuthUtil::kickoutByUserId); + return; + } + if (CollUtil.isNotEmpty(people)) { + // 排除之前以绑定的员工 + List newUserIds = userIds.stream().filter(item -> !collect.contains(item)).collect(Collectors.toList()); + List finalUserIds = userIds; + List deleteUserIds = collect.stream().filter(v->!finalUserIds.contains(v)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(deleteUserIds)) { + // 员工踢下线 + deleteUserIds.forEach(AuthUtil::kickoutByUserId); + if (isSuperAdmin == 1) deleteUserIds.forEach(v->syncSuperAdmin(v, false)); + // 关闭以前的超管 + baseMapper.deleteBatchIds(people.stream().filter(v->deleteUserIds.contains(v.getUserId())).map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList())); + } + userIds = newUserIds; + } + if (CollUtil.isEmpty(userIds)) return; + // 同步超级管理员到base_user表 + if (isSuperAdmin == 1){ + for (String userId : userIds) { + syncSuperAdmin(userId, true); + } + } + List collected = userIds.stream().map(userId -> { + FtbPermissionRoleAuthorizePerson person = new FtbPermissionRoleAuthorizePerson(); + person.setUserId(userId); + person.setRoleId(roleId); + return person; + }).collect(Collectors.toList()); + Db.saveBatch(collected); + // 员工踢下线 + userIds.forEach(AuthUtil::kickoutByUserId); + + } + + @Override + public void batchDelete(FtbPermissionRoleBatchDeleteDTO rolePersonDeleteDTO) { + List people = baseMapper.selectBatchIds(rolePersonDeleteDTO.getPrimaryKeyIds()); + for (FtbPermissionRoleAuthorizePerson person : people) { + String roleId = person.getRoleId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, roleId); + FtbPermissionRole ftbPermissionRole = permissionRoleMapper.selectOne(wrapper); + Integer isSuperAdmin = ftbPermissionRole.getIsSuperAdmin(); + // 同步超级管理员到base_user表 + if (isSuperAdmin == 1){ + // 移除超级管理员信息 + syncSuperAdmin(person.getUserId(), false); + } + } + Db.removeByIds(rolePersonDeleteDTO.getPrimaryKeyIds(),FtbPermissionRoleAuthorizePerson.class); + // 员工踢下线 + people.stream().map(FtbPermissionRoleAuthorizePerson::getUserId).forEach(AuthUtil::kickoutByUserId); + } + + private void syncSuperAdmin(String person, boolean enabled) { + ActionResult actionResult = v2UserApi.userAdminEnable(person, enabled); + if (actionResult == null || actionResult.getCode() != 200) { + throw new RuntimeException("同步超级管理员状态失败!请重试!"); + } + } + + @Override + public List getRoleIdsByUserId(FtbPermissionAuthorizeVO vo) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getRoleId,vo.getAuthorizationId()); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark,0); + List people = baseMapper.selectList(wrapper); + List hasBandUserIds = people.stream().map(FtbPermissionRoleAuthorizePerson::getUserId).distinct().collect(Collectors.toList()); + List userIds = vo.getIds(); + if (CollUtil.isNotEmpty(hasBandUserIds)){ + return userIds.stream().filter(v->!hasBandUserIds.contains(v)).collect(Collectors.toList()); + } + return userIds; + } + + @Override + public void deleteEmployeeAllPermission(List userIds) { + // 首先将管理员数据全部移除 + v2UserApi.userAdminEnableBatch(userIds, false); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPermissionRoleAuthorizePerson::getUserId,userIds); + baseMapper.delete(wrapper); + userIds.forEach(AuthUtil::kickoutByUserId); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePostServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePostServiceImpl.java new file mode 100644 index 0000000..77d981a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleAuthorizePostServiceImpl.java @@ -0,0 +1,174 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import jnpf.authority.mapper.FtbPermissionRoleAuthorizePostMapper; +import jnpf.authority.service.FtbPermissionRoleAuthorizePostService; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.authorize.FtbPermissionAuthorizeVO; +import jnpf.model.authority.dto.person.FtbPermissionRoleBatchDeleteDTO; +import jnpf.model.authority.dto.post.FtbPermissionRolePostAddDTO; +import jnpf.model.authority.po.FtbPermissionRoleAuthorizePost; +import jnpf.model.authority.vo.post.FtbAuthorizedPostVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.vo.v2.organzie.OrganizeFullBasicVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.AuthUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * + * + *@Author: peng.hao + *@create: 2025/3/5 + * + */ +@Service +public class FtbPermissionRoleAuthorizePostServiceImpl extends ServiceImpl implements FtbPermissionRoleAuthorizePostService{ + + + @Autowired + V2UserApi v2UserApi; + + @Autowired + V2PositionApi v2PositionApi; + + @Override + public void addPost(FtbPermissionRolePostAddDTO ftbPermissionRolePostAddDTO) { + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + lambdaQueryWrapper.eq(FtbPermissionRoleAuthorizePost::getRoleId,ftbPermissionRolePostAddDTO.getRoleId()); + List authorizePosts = baseMapper.selectList(lambdaQueryWrapper); + List postIds = ftbPermissionRolePostAddDTO.getPostIds(); + // 清空所有绑定岗位 + List collected = authorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getPostId).collect(Collectors.toList()); + if(CollUtil.isEmpty(postIds) && CollUtil.isNotEmpty(authorizePosts)){ + baseMapper.deleteBatchIds(authorizePosts.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList())); + clearUserLogins(collected); + return; + } + if (CollUtil.isNotEmpty(authorizePosts)){ + List newPostIds = postIds.stream().filter(v -> !collected.contains(v)).collect(Collectors.toList()); + List finalPostIds = postIds; + List deletePostIds = collected.stream().filter(v->!finalPostIds.contains(v)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(deletePostIds)){ + baseMapper.deleteBatchIds(authorizePosts.stream().filter(v->deletePostIds.contains(v.getPostId())).map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList())); + } + postIds = newPostIds; + } + if (CollUtil.isEmpty(postIds)) return; + List savePost = postIds.stream().map(v -> { + FtbPermissionRoleAuthorizePost ftbPermissionRoleAuthorizePost = new FtbPermissionRoleAuthorizePost(); + ftbPermissionRoleAuthorizePost.setRoleId(ftbPermissionRolePostAddDTO.getRoleId()); + ftbPermissionRoleAuthorizePost.setPostId(v); + return ftbPermissionRoleAuthorizePost; + }).collect(Collectors.toList()); + Db.saveBatch(savePost); + clearUserLogins(postIds); + } + + /** + * + * @param postIds + */ + private void clearUserLogins(List postIds) { + if (CollUtil.isEmpty(postIds)) return; + QueryUserBatchDTO userBatchDTO = new QueryUserBatchDTO(); + userBatchDTO.setPositionIds(postIds); + userBatchDTO.setTenantId(UserProvider.getUser().getTenantId()); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(userBatchDTO); + if (userInfoBatch == null || userInfoBatch.getData() == null) return; + List userIds = userInfoBatch.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + // 员工踢下线 + userIds.forEach(AuthUtil::kickoutByUserId); + } + + @Override + public PageListVO authorizePost(Page page, String roleId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePost::getRoleId, roleId); + wrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List authorizePosts = baseMapper.selectList(wrapper); + if(CollUtil.isEmpty(authorizePosts)) return new PageListVO<>(); + List postIds = authorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getPostId).collect(Collectors.toList()); + List userIds = authorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getCreatorUserId).collect(Collectors.toList()); + ActionResult> boundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + Map idsPost = new HashMap<>(); + if(boundBatch != null && CollUtil.isNotEmpty(boundBatch.getData())){ + List data = boundBatch.getData(); + idsPost = data.stream().collect(Collectors.toMap(UserBoundVO::getId, item -> item, (k1, k2) -> k1)); + } + ActionResult> listActionResult = v2PositionApi.listPositionDetailInfo(postIds); + Map finalIdsPost = new HashMap<>(); + if (listActionResult !=null && CollUtil.isNotEmpty(listActionResult.getData())) { + finalIdsPost= listActionResult.getData().stream() + .collect(Collectors.toMap(PositionVO::getId, item -> item, (k1, k2) -> k1)); + } + Map finalIdsPost1 = idsPost; + Map finalIdsPost2 = finalIdsPost; + List authorizedPostVOS = authorizePosts.stream().map(v -> { + FtbAuthorizedPostVO convert = FtbAuthorizedPostVO.convert(v); + if(finalIdsPost1.containsKey(v.getCreatorUserId())){ + UserBoundVO userInfoVo = finalIdsPost1.get(v.getCreatorUserId()); + convert.setOperatorUserName(userInfoVo.getUserName()); + } + if(finalIdsPost2.containsKey(v.getPostId())) { + PositionVO dimensionVO = finalIdsPost2.get(v.getPostId()); + convert.setPostName(dimensionVO.getFullName()); + List organizes = dimensionVO.getOrganizes(); + if(CollUtil.isEmpty(organizes)) return convert; + convert.setOrgName( organizes.stream().map(OrganizeFullBasicVO::getName).collect(Collectors.joining(","))); + } + return convert; + }).collect(Collectors.toList()); + return CultivatePage.paginate(authorizedPostVOS,page); + } + + @Override + public List getRoleIdsByPostIds(FtbPermissionAuthorizeVO vo) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + wrapper.in(FtbPermissionRoleAuthorizePost::getRoleId,vo.getAuthorizationId()); + List list = baseMapper.selectList(wrapper); + List hasBandPostIds = list.stream().map(FtbPermissionRoleAuthorizePost::getPostId).distinct().collect(Collectors.toList()); + List postIds = vo.getIds(); + if(CollUtil.isNotEmpty(hasBandPostIds)){ + return postIds.stream().filter(v -> !hasBandPostIds.contains(v)).collect(Collectors.toList()); + } + return postIds; + } + + @Override + public void batchDelete(FtbPermissionRoleBatchDeleteDTO rolePersonDeleteDTO) { + List posts = baseMapper.selectBatchIds(rolePersonDeleteDTO.getPrimaryKeyIds()); + baseMapper.deleteBatchIds(rolePersonDeleteDTO.getPrimaryKeyIds()); + clearUserLogins(posts.stream().map(FtbPermissionRoleAuthorizePost::getPostId).distinct().collect(Collectors.toList())); + } + + @Override + public void clearRoleByPostId(List postIds) { + if(CollUtil.isEmpty(postIds)) return; + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + queryWrapper.in(FtbPermissionRoleAuthorizePost::getPostId,postIds); + baseMapper.delete(queryWrapper); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuRelationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuRelationServiceImpl.java new file mode 100644 index 0000000..932ef30 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuRelationServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.authority.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.authority.mapper.FtbPermissionRoleMenuRelationMapper; +import jnpf.authority.service.FtbPermissionRoleMenuRelationService; +import jnpf.model.authority.po.FtbPermissionRoleMenuRelation; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +@Service +public class FtbPermissionRoleMenuRelationServiceImpl extends ServiceImpl implements FtbPermissionRoleMenuRelationService{ + + @Resource + private FtbPermissionRoleMenuRelationMapper ftbPermissionRoleMenuRelationMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuServiceImpl.java new file mode 100644 index 0000000..e23dd2e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleMenuServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.authority.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.authority.mapper.FtbPermissionRoleMenuMapper; +import jnpf.authority.service.FtbPermissionRoleMenuService; +import jnpf.model.authority.po.FtbPermissionRoleMenu; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2025/3/5 +* +*/ +@Service +public class FtbPermissionRoleMenuServiceImpl extends ServiceImpl implements FtbPermissionRoleMenuService{ + + @Resource + private FtbPermissionRoleMenuMapper ftbPermissionRoleMenuMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleServiceImpl.java new file mode 100644 index 0000000..78354e9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionRoleServiceImpl.java @@ -0,0 +1,731 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import com.fantaibao.permission.enums.FtbPermissionRedisKeys; +import jnpf.authority.mapper.*; +import jnpf.authority.service.FtbPermissionRoleService; +import jnpf.base.ActionResult; +import jnpf.base.ModuleApi; +import jnpf.base.SystemApi; +import jnpf.base.UserInfo; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.entity.SystemEntity; +import jnpf.model.authority.dto.menu.FtbPermissionRoleAddDTO; +import jnpf.model.authority.dto.role.FtbPermissionDataDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleCopyDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleInfoDTO; +import jnpf.model.authority.dto.role.FtbPermissionRoleUpdateDTO; +import jnpf.model.authority.po.*; +import jnpf.model.authority.vo.role.FtbPermissionRoleDetailsVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleInfoVO; +import jnpf.model.authority.vo.role.FtbPermissionRolePersonVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * + * + *@Author: peng.hao + *@create: 2025/3/5 + * + */ +@Service +public class FtbPermissionRoleServiceImpl extends ServiceImpl implements FtbPermissionRoleService { + + @Resource + private FtbPermissionRoleMapper ftbPermissionRoleMapper; + + @Resource + private FtbPermissionRoleMenuMapper ftbPermissionRoleMenuMapper; + + @Resource + private FtbPermissionRoleMenuRelationMapper ftbPermissionRoleMenuRelationMapper; + + @Resource + private FtbPermissionRoleAuthorizePersonMapper ftbPermissionRoleAuthorizePersonMapper; + + @Resource + private FtbPermissionRoleAuthorizePostMapper ftbPermissionRoleAuthorizePostMapper; + + @Resource + private SystemApi systemApi; + + @Resource + private ModuleApi moduleApi; + + @Resource + private V2UserApi v2UserApi; + + @Resource + private RedisTemplate redisTemplate; + + @Override + public Page permissionList(Page page, FtbPermissionRoleInfoDTO roleInfoDTO) { + if (StrUtil.isNotBlank(roleInfoDTO.getModuleIds())) { + roleInfoDTO.setInnerModuleIds(Arrays.stream(roleInfoDTO.getModuleIds().split(",")).collect(Collectors.toList())); + } + Page pages = ftbPermissionRoleMapper.permissionList(page, roleInfoDTO); + if (CollUtil.isNotEmpty(roleInfoDTO.getInnerModuleIds())) { + pages.getRecords().forEach(a -> { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleMenu::getRoleId, a.getRoleId()); + queryWrapper.eq(FtbPermissionRoleMenu::getRoleEnabled, 1); + List ftbPermissionRoleMenus = ftbPermissionRoleMenuMapper.selectList(queryWrapper); + String functionWebId = ftbPermissionRoleMenus.stream().map(FtbPermissionRoleMenu::getFunctionWebId).collect(Collectors.joining(",")); + String functionAppId = ftbPermissionRoleMenus.stream().map(FtbPermissionRoleMenu::getFunctionAppId).collect(Collectors.joining(",")); + String functionPcId = ftbPermissionRoleMenus.stream().map(FtbPermissionRoleMenu::getFunctionPcId).collect(Collectors.joining(",")); + a.setFunctionWebId(functionWebId); + a.setFunctionAppId(functionAppId); + a.setFunctionPcId(functionPcId); + }); + } + SystemEntity systemVO = systemApi.getSystemVO(); + String systemId = systemVO.getId(); + String tenantId = UserProvider.getUser().getTenantId(); + List data = moduleApi.getListWithoutSystemId(systemId, null, null, tenantId); + // 获取顶级目录菜单 + if (data == null) { + throw new RuntimeException("system服务异常,无法远程获取系统菜单"); + } + List entities = data.stream().filter(item -> item.getType() != null && item.getEnabledMark() != null && 1 == item.getEnabledMark() && "-1".equals(item.getParentId())).collect(Collectors.toList()); + pages.getRecords().forEach(a -> { + if (a.getIsSuperAdmin() == 1) { + a.setManageModule("全部"); + } else { + // 拼接并过滤非空字段 + Stream stream = Stream.empty(); + if (StrUtil.isNotBlank(a.getFunctionWebId())) { + stream = Arrays.stream(a.getFunctionWebId().split(",")); + } + if (StrUtil.isNotBlank(a.getFunctionAppId())) { + stream = Stream.concat(stream, Arrays.stream(a.getFunctionAppId().split(","))); + } + if (StrUtil.isNotBlank(a.getFunctionPcId())) { + stream = Stream.concat(stream, Arrays.stream(a.getFunctionPcId().split(","))); + } + List functionIds = stream + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + String manageModule = entities.stream() + .filter(item -> functionIds.contains(item.getId())) + .map(ModuleEntity::getFullName) + .distinct() + .collect(Collectors.joining("、")); + a.setManageModule(manageModule); + } + }); + return pages; + } + + @Override + @Transactional + public void addRole(FtbPermissionRoleAddDTO ftbPermissionRoleAddDTO) { + // 角色名称不能重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRole::getRoleName, ftbPermissionRoleAddDTO.getRoleName()); + queryWrapper.eq(FtbPermissionRole::getEnableMark, 0); + if (ftbPermissionRoleMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("角色名称不可重复!"); + } + FtbPermissionRole ftbPermissionRole = ftbPermissionRoleAddDTO.convertFtbPermissionRole(ftbPermissionRoleAddDTO); + ftbPermissionRoleMapper.insert(ftbPermissionRole); + fillRoleInfo(ftbPermissionRoleAddDTO.getPermissionRoleInners(), ftbPermissionRole.getId()); + } + + @Override + @Transactional + public void editRole(FtbPermissionRoleUpdateDTO roleUpdateDTO) { + if ("1".equals(roleUpdateDTO.getRoleId())) { + throw new RuntimeException("不能修改超级管理员角色!"); + } + // 角色名称不能重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.ne(SuperBaseEntity.SuperIBaseEntity::getId, roleUpdateDTO.getRoleId()); + queryWrapper.eq(FtbPermissionRole::getRoleName, roleUpdateDTO.getRoleName()); + queryWrapper.eq(FtbPermissionRole::getEnableMark, 0); + if (ftbPermissionRoleMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("角色名称不可重复!"); + } + LambdaUpdateWrapper updateWrapper = roleUpdateDTO.update(roleUpdateDTO); + ftbPermissionRoleMapper.update(new FtbPermissionRole(), updateWrapper); + // 删除历史旧有的数据 + if (CollUtil.isNotEmpty(roleUpdateDTO.getPermissionRoleInners())) { + roleUpdateDTO.getPermissionRoleInners().forEach(a -> { + LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper() + .select(FtbPermissionRoleMenu::getId) + .eq(FtbPermissionRoleMenu::getRoleId, roleUpdateDTO.getRoleId()); + if (StrUtil.isNotBlank(a.getFunctionWebId()) && StrUtil.isNotBlank(a.getFunctionAppId()) && StrUtil.isNotBlank(a.getFunctionPcId())) { + // 组装or语句 + deleteWrapper.and(b -> { + b.eq(FtbPermissionRoleMenu::getFunctionWebId, a.getFunctionWebId()); + b.or(); + b.eq(FtbPermissionRoleMenu::getFunctionAppId, a.getFunctionAppId()); + b.or(); + b.eq(FtbPermissionRoleMenu::getFunctionPcId, a.getFunctionPcId()); + }); + } else { + if (StrUtil.isNotBlank(a.getFunctionWebId())) { + deleteWrapper.eq(FtbPermissionRoleMenu::getFunctionWebId, a.getFunctionWebId()); + } + if (StrUtil.isNotBlank(a.getFunctionAppId())) { + deleteWrapper.eq(FtbPermissionRoleMenu::getFunctionAppId, a.getFunctionAppId()); + } + if (StrUtil.isNotBlank(a.getFunctionPcId())) { + deleteWrapper.eq(FtbPermissionRoleMenu::getFunctionPcId, a.getFunctionPcId()); + } + } + List ftbPermissionRoleMenu = ftbPermissionRoleMenuMapper.selectList(deleteWrapper); + if (CollUtil.isNotEmpty(ftbPermissionRoleMenu)) { + List roleMenuIds = ftbPermissionRoleMenu.stream().map(FtbPermissionRoleMenu::getId).collect(Collectors.toList()); + LambdaQueryWrapper deleteRoleWrapper = new LambdaQueryWrapper() + .eq(FtbPermissionRoleMenuRelation::getRoleId, roleUpdateDTO.getRoleId()) + .in(FtbPermissionRoleMenuRelation::getRoleMenuId, roleMenuIds); + ftbPermissionRoleMenuMapper.delete(deleteWrapper); + ftbPermissionRoleMenuRelationMapper.delete(deleteRoleWrapper); + } + }); + } + fillRoleInfo(roleUpdateDTO.getPermissionRoleInners(), roleUpdateDTO.getRoleId()); + // 角色绑定人员踢出下线 + personnelKickedOffTheLine(roleUpdateDTO.getRoleId()); + } + + private void fillRoleInfo(List permissionRoleInners, String roleId) { + if (CollUtil.isNotEmpty(permissionRoleInners)) { + // 权限管理角色菜单 + permissionRoleInners.stream() + .filter(a -> CollUtil.isNotEmpty(a.getMemberIds())) + .forEach(a -> { + FtbPermissionRoleMenu ftbPermissionRoleMenu = a.convert(a); + ftbPermissionRoleMenu.setRoleId(roleId); + ftbPermissionRoleMenuMapper.insert(ftbPermissionRoleMenu); + List ftbPermissionRoleMenuRelations = a.getMemberIds().stream() + .map(b -> { + FtbPermissionRoleMenuRelation formRelation = new FtbPermissionRoleMenuRelation(); + formRelation.setRoleId(roleId); + formRelation.setRoleMenuConfigId(b); + formRelation.setRoleMenuId(ftbPermissionRoleMenu.getId()); + return formRelation; + }).collect(Collectors.toList()); + Db.saveBatch(ftbPermissionRoleMenuRelations); + }); + } + } + + @Override + @Transactional + public void deleteRole(String id) { + if ("1".equals(id)) { + throw new RuntimeException("不能删除超级管理员角色!"); + } + // 角色绑定人员踢出下线 + personnelKickedOffTheLine(id); + // 根据角色id删除角色菜单 + LambdaUpdateWrapper a = new LambdaUpdateWrapper<>(); + a.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + a.set(FtbPermissionRole::getEnableMark,1); + ftbPermissionRoleMapper.update(new FtbPermissionRole(), a); + // 根据角色id删除角色功能关联 + LambdaUpdateWrapper b = new LambdaUpdateWrapper<>(); + b.eq(FtbPermissionRoleMenuRelation::getRoleId, id); + b.set(FtbPermissionRoleMenuRelation::getEnableMark,1); + ftbPermissionRoleMenuRelationMapper.update(new FtbPermissionRoleMenuRelation(),b); + // 根据角色id删除角色菜单关联 + LambdaUpdateWrapper c = new LambdaUpdateWrapper<>(); + c.eq(FtbPermissionRoleMenu::getRoleId, id); + c.set(FtbPermissionRoleMenu::getEnableMark,1); + ftbPermissionRoleMenuMapper.update(new FtbPermissionRoleMenu(),c); + // 根据角色id删除权限管理角色人员授权 + LambdaUpdateWrapper d = new LambdaUpdateWrapper<>(); + d.eq(FtbPermissionRoleAuthorizePerson::getRoleId, id); + d.set(FtbPermissionRoleAuthorizePerson::getEnableMark,1); + ftbPermissionRoleAuthorizePersonMapper.update(new FtbPermissionRoleAuthorizePerson(),d); + // 根据角色id删除权限管理角色岗位授权 + LambdaUpdateWrapper e = new LambdaUpdateWrapper<>(); + e.eq(FtbPermissionRoleAuthorizePost::getRoleId, id); + e.set(FtbPermissionRoleAuthorizePost::getEnableMark,1); + ftbPermissionRoleAuthorizePostMapper.update(new FtbPermissionRoleAuthorizePost(),e); + } + + @Override + public FtbPermissionRoleIdentificationVO permissionIdentificationCollection() { + UserInfo userInfo = UserProvider.getUser(); + FtbPermissionRoleIdentificationVO ftbPermissionRoleIdentificationVO = new FtbPermissionRoleIdentificationVO(); + if (userInfo.getIsAdministrator()) { + ftbPermissionRoleIdentificationVO.setIsSuperAdmin(1); + return ftbPermissionRoleIdentificationVO; + } else { + ftbPermissionRoleIdentificationVO.setIsSuperAdmin(0); + } + // 当前人员权限 + List permissionIdentifications = ftbPermissionRoleMapper.permissionIdentifications(userInfo.getUserId()); + // 获取人员岗位并获取岗位权限 + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(userInfo.getUserId()), null); + if (userPrimaryBoundBatch.getCode() == 200 && !userPrimaryBoundBatch.getData().isEmpty()) { + String positionId = userPrimaryBoundBatch.getData().get(0).getPositionId(); + List jobPermissions = ftbPermissionRoleMapper.queryJobPermissions(positionId); + permissionIdentifications = Stream.concat(permissionIdentifications.stream(), jobPermissions.stream()) + .distinct() + .collect(Collectors.toList()); + } + ftbPermissionRoleIdentificationVO.setPermissionIdentifications(permissionIdentifications); + return ftbPermissionRoleIdentificationVO; + } + + @Override + public FtbPermissionRoleDetailsVO roleDetails(String roleId, String menuWebId, String menuAppId, String menuPcId) { + FtbPermissionRoleDetailsVO ftbPermissionRoleDetails = new FtbPermissionRoleDetailsVO(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(FtbPermissionRoleMenu::getRoleId, roleId); + if (StrUtil.isNotBlank(menuAppId) && StrUtil.isNotBlank(menuWebId) && StrUtil.isNotBlank(menuPcId)) { + // 组装or语句 + queryWrapper.and(a -> { + a.eq(FtbPermissionRoleMenu::getFunctionWebId, menuWebId); + a.or(); + a.eq(FtbPermissionRoleMenu::getFunctionAppId, menuAppId); + a.or(); + a.eq(FtbPermissionRoleMenu::getFunctionPcId, menuPcId); + }); + } else { + if (StrUtil.isNotBlank(menuWebId)) { + queryWrapper.eq(FtbPermissionRoleMenu::getFunctionWebId, menuWebId); + } + if (StrUtil.isNotBlank(menuAppId)) { + queryWrapper.eq(FtbPermissionRoleMenu::getFunctionAppId, menuAppId); + } + if (StrUtil.isNotBlank(menuPcId)) { + queryWrapper.eq(FtbPermissionRoleMenu::getFunctionPcId, menuPcId); + } + } + queryWrapper.orderByDesc(FtbPermissionRoleMenu::getCreatorTime); + queryWrapper.last("limit 1"); + FtbPermissionRoleMenu ftbPermissionRoleMenus = ftbPermissionRoleMenuMapper.selectOne(queryWrapper); + if (ftbPermissionRoleMenus == null) { + return ftbPermissionRoleDetails; + } + ftbPermissionRoleDetails.setRoleEnabled(ftbPermissionRoleMenus.getRoleEnabled()); + ftbPermissionRoleDetails.setScopePermission(ftbPermissionRoleMenus.getScopePermission()); + ftbPermissionRoleDetails.setSpecifyOrgIds(ftbPermissionRoleMenus.getSpecifyOrgIds()); + ftbPermissionRoleDetails.setFunctionPageWebId(ftbPermissionRoleMenus.getFunctionPageWebId()); + ftbPermissionRoleDetails.setFunctionPageAppId(ftbPermissionRoleMenus.getFunctionPageAppId()); + ftbPermissionRoleDetails.setFunctionPagePcId(ftbPermissionRoleMenus.getFunctionPagePcId()); + // 已勾选的菜单Id集合 + List memberIds = ftbPermissionRoleMenuRelationMapper.selectList(new LambdaQueryWrapper() + .eq(FtbPermissionRoleMenuRelation::getRoleMenuId, ftbPermissionRoleMenus.getId())) + .stream() + .map(FtbPermissionRoleMenuRelation::getRoleMenuConfigId) + .collect(Collectors.toList()); + ftbPermissionRoleDetails.setMemberIds(memberIds); + return ftbPermissionRoleDetails; + } + + // 定义权限优先级映射 + private static final Map SCOPE_PRIORITY_MAP = new HashMap<>(); + + static { + // 权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + // 数据权限优先级为:全部>指定权限>所属组织及下级组织员工>所在组织员工>仅下属 + SCOPE_PRIORITY_MAP.put(0, 0); + SCOPE_PRIORITY_MAP.put(4, 1); + SCOPE_PRIORITY_MAP.put(1, 2); + SCOPE_PRIORITY_MAP.put(2, 3); + SCOPE_PRIORITY_MAP.put(3, 4); + } + + @Override + public Map queryUserScopeOfPermission(String userId,String... tenantIds) { + String tenantId = tenantIds.length > 0 ? tenantIds[0] : UserProvider.getUser().getTenantId(); + return doQueryUserScopeOfPermission(userId, tenantId, false); + } + + @Override + public Map queryUserScopeOfPermissionNoTenant(String userId) { + String tenantId = UserProvider.getUser().getTenantId(); + return doQueryUserScopeOfPermission(userId, tenantId, true); + } + + /** + * 查询用户权限范围(公共方法) + * + * @param userId 用户ID + * @param tenantId 租户ID + * @param refreshMenuPermission 是否刷新Redis菜单权限缓存 + * @return 用户权限范围Map + */ + private Map doQueryUserScopeOfPermission(String userId, String tenantId, boolean refreshMenuPermission) { + // 获取人员岗位并获取岗位权限 + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), tenantId); + if (userPrimaryBoundBatch.getCode() != 200 && (userPrimaryBoundBatch.getData() == null || userPrimaryBoundBatch.getData().isEmpty())) { + throw new RuntimeException("获取用户主岗信息失败"); + } + String positionId = userPrimaryBoundBatch.getData().get(0).getPositionId(); + // 用户授权信息 + List ftbPermissionRolePersonVOS = ftbPermissionRoleAuthorizePersonMapper.queryUserScopeOfPermission(userId); + Map first = new HashMap<>(ftbPermissionRolePersonVOS.size() * 2); + populateMap(ftbPermissionRolePersonVOS, first); + // 岗位授权信息 + List jobAuthorizationInformation = ftbPermissionRoleAuthorizePersonMapper.jobAuthorizationInformation(positionId); + Map second = new HashMap<>(jobAuthorizationInformation.size() * 2); + populateMap(jobAuthorizationInformation, second); + // 刷新用户菜单权限缓存到Redis + if (refreshMenuPermission) { + userMenuPermissions(userId, ftbPermissionRolePersonVOS, jobAuthorizationInformation); + } + // 整合web和app的权限 + Map resultMaps = mergePermissions(first, second); + // 员工个人特殊权限 + List peopleDataPermissions = ftbPermissionRoleAuthorizePersonMapper.getPeopleDataPermissions(userId); + // 无角色但是已配置了个人特殊权限,只有一种情况,之前绑定了角色但是又删除了 + if (resultMaps.isEmpty() && CollUtil.isNotEmpty(peopleDataPermissions)) { + return resultMaps; + } + // 将特殊权限进行替换 + peopleDataPermissions.forEach(peopleDataPermission -> { + String designateTheOrganization = buildScopeValue(peopleDataPermission); + resultMaps.put(peopleDataPermission.getWebId(), designateTheOrganization); + resultMaps.put(peopleDataPermission.getAppId(), designateTheOrganization); + }); + return resultMaps; + } + @Override + public Map> queryUserScopeOfPermission(List userIds, String... tenantIds) { + // 获取人员岗位并获取岗位权限 + String tenantId = tenantIds.length > 0 ? tenantIds[0] : UserProvider.getUser().getTenantId(); + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, tenantId); + List userPrimaryBoundBatchData = userPrimaryBoundBatch.getData(); + if(CollUtil.isEmpty(userPrimaryBoundBatchData)){ + throw new RuntimeException("获取用户主岗信息失败"); + } + List userIdsData = userPrimaryBoundBatchData.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + + List positionIds =userPrimaryBoundBatchData.stream().map(UserBoundVO::getPositionId).collect(Collectors.toList()) ; + // 用户授权信息 + List ftbPermissionRolePersonVOSDb = ftbPermissionRoleAuthorizePersonMapper.queryUserScopeOfPermissionWithUserIds(userIdsData); + Map> userPermissionRolePersonVOS = ftbPermissionRolePersonVOSDb.stream().collect(Collectors.groupingBy(FtbPermissionRolePersonVO::getUserId)); + // 岗位授权信息 + List jobAuthorizationInformationListVO = ftbPermissionRoleAuthorizePersonMapper.jobAuthorizationInformationWithPositionIds(positionIds); + Map> postUserListMap = jobAuthorizationInformationListVO.stream().collect(Collectors.groupingBy(FtbPermissionRolePersonVO::getPostId)); + // 员工个人特殊权限 + List peopleDataPermissions = ftbPermissionRoleAuthorizePersonMapper.getPeopleDataPermissionsWithUserIds(userIds); + Map> peopleDataPermissionsListMap = peopleDataPermissions.stream().collect(Collectors.groupingBy(FtbPermissionRolePersonVO::getUserId)); + Map> hashMap = new HashMap<>(); + userPrimaryBoundBatchData.forEach(vo -> { + String userId = vo.getId(); + Map first = new HashMap<>(); + Map second = new HashMap<>(); + if(userPermissionRolePersonVOS.containsKey(userId)) { + List ftbPermissionRolePersonVOS = userPermissionRolePersonVOS.get(userId); + populateMap(ftbPermissionRolePersonVOS, first); + } + if(postUserListMap.containsKey(vo.getPositionId())) { + List permissionRolePersonVOS = postUserListMap.get(vo.getPositionId()); + populateMap(permissionRolePersonVOS, second); + } + // 整合web和app的权限 + Map resultMaps = mergePermissions(first, second); + if (!peopleDataPermissionsListMap.containsKey(userId)) { + hashMap.put(userId, resultMaps); + return; + } + List rolePersonVOS = peopleDataPermissionsListMap.get(userId); + // 将特殊权限进行替换 + rolePersonVOS.forEach(peopleDataPermission -> { + String designateTheOrganization = buildScopeValue(peopleDataPermission); + resultMaps.put(peopleDataPermission.getWebId(), designateTheOrganization); + resultMaps.put(peopleDataPermission.getAppId(), designateTheOrganization); + }); + hashMap.put(userId, resultMaps); + }); + return hashMap; + } + @Override + public Set queryTheLatestListOfMenuPermissions(String userId) { + // 获取人员岗位并获取岗位权限 + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), UserProvider.getUser().getTenantId()); + if (userPrimaryBoundBatch.getCode() != 200 && CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + throw new RuntimeException("获取用户主岗信息失败"); + } + if (CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + throw new RuntimeException("获取用户信息失败"); + } + String positionId = userPrimaryBoundBatch.getData().get(0).getPositionId(); + // 用户授权信息 + List ftbPermissionRolePersonVOS = ftbPermissionRoleAuthorizePersonMapper.queryUserScopeOfPermission(userId); + Map first = new HashMap<>(ftbPermissionRolePersonVOS.size() * 2); + populateMap(ftbPermissionRolePersonVOS, first); + // 岗位授权信息 + List jobAuthorizationInformation = ftbPermissionRoleAuthorizePersonMapper.jobAuthorizationInformation(positionId); + Map second = new HashMap<>(jobAuthorizationInformation.size() * 2); + populateMap(jobAuthorizationInformation, second); + // 用户菜单权限 + return userMenuPermissionsWithUserId(ftbPermissionRolePersonVOS, jobAuthorizationInformation); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void dataPermissionApplicationToOtherModules(FtbPermissionDataDTO ftbPermissionDataDTO) { + if (CollUtil.isEmpty(ftbPermissionDataDTO.getFtbPermissionDataInnerDTOs())) { + return; + } + LambdaUpdateWrapper roleMenuLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + roleMenuLambdaUpdateWrapper.eq(FtbPermissionRoleMenu::getRoleId, ftbPermissionDataDTO.getRoleId()); + roleMenuLambdaUpdateWrapper.set(FtbPermissionRoleMenu::getScopePermission, ftbPermissionDataDTO.getScopePermission()); + roleMenuLambdaUpdateWrapper.set(FtbPermissionRoleMenu::getSpecifyOrgIds, ftbPermissionDataDTO.getSpecifyOrgIds()); + List webIds = ftbPermissionDataDTO.getFtbPermissionDataInnerDTOs() + .stream() + .map(FtbPermissionDataDTO.FtbPermissionDataInnerDTO::getFunctionWebId) + .filter(StrUtil::isNotBlank).collect(Collectors.toList()); + List appIds = ftbPermissionDataDTO.getFtbPermissionDataInnerDTOs() + .stream() + .map(FtbPermissionDataDTO.FtbPermissionDataInnerDTO::getFunctionAppId) + .filter(StrUtil::isNotBlank).collect(Collectors.toList()); + List pcIds = ftbPermissionDataDTO.getFtbPermissionDataInnerDTOs() + .stream() + .map(FtbPermissionDataDTO.FtbPermissionDataInnerDTO::getFunctionPcId) + .filter(StrUtil::isNotBlank).collect(Collectors.toList()); + if (CollUtil.isEmpty(webIds) && CollUtil.isEmpty(appIds) && CollUtil.isEmpty(pcIds)) { + return; + } + roleMenuLambdaUpdateWrapper.in(CollUtil.isNotEmpty(webIds),FtbPermissionRoleMenu::getFunctionWebId, webIds); + roleMenuLambdaUpdateWrapper.in(CollUtil.isNotEmpty(appIds),FtbPermissionRoleMenu::getFunctionAppId, appIds); + roleMenuLambdaUpdateWrapper.in(CollUtil.isNotEmpty(pcIds),FtbPermissionRoleMenu::getFunctionPcId, pcIds); + ftbPermissionRoleMenuMapper.update(new FtbPermissionRoleMenu(), roleMenuLambdaUpdateWrapper); + } + + + + @Override + @Transactional + public void copyRole(FtbPermissionRoleCopyDTO ftbPermissionRole) { + LambdaQueryWrapper roleMenuLambdaQueryWrapper = Wrappers.lambdaQuery(); + roleMenuLambdaQueryWrapper.eq(FtbPermissionRoleMenu::getRoleId, ftbPermissionRole.getRoleId()); + roleMenuLambdaQueryWrapper.eq(FtbPermissionRoleMenu::getEnableMark, 0); + List ftbPermissionRoleMenus = ftbPermissionRoleMenuMapper.selectList(roleMenuLambdaQueryWrapper); + if (CollUtil.isNotEmpty(ftbPermissionRole.getPermissionRoleInners())) { + List webIds = ftbPermissionRole.getPermissionRoleInners().stream() + .flatMap(a -> Stream.of(a.getFunctionWebId(), a.getFunctionAppId(), a.getFunctionPcId())) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + ftbPermissionRoleMenus = ftbPermissionRoleMenus.stream() + .filter(a -> Stream.of(a.getFunctionWebId(), a.getFunctionAppId(), a.getFunctionPcId()) + .filter(StrUtil::isNotBlank) + .noneMatch(webIds::contains)) + .collect(Collectors.toList()); + } + ftbPermissionRoleMenus.forEach(a -> { + FtbPermissionRoleAddDTO.PermissionRoleInnerDTO permissionRoleInnerDTO = new FtbPermissionRoleAddDTO.PermissionRoleInnerDTO(); + permissionRoleInnerDTO.setRoleEnabled(a.getRoleEnabled()); + permissionRoleInnerDTO.setFunctionWebId(a.getFunctionWebId()); + permissionRoleInnerDTO.setFunctionAppId(a.getFunctionAppId()); + permissionRoleInnerDTO.setFunctionPcId(a.getFunctionPcId()); + permissionRoleInnerDTO.setScopePermission(a.getScopePermission()); + permissionRoleInnerDTO.setSpecifyOrgIds(a.getSpecifyOrgIds()); + permissionRoleInnerDTO.setFunctionPageAppId(a.getFunctionPageAppId()); + permissionRoleInnerDTO.setFunctionPageWebId(a.getFunctionPageWebId()); + permissionRoleInnerDTO.setFunctionPagePcId(a.getFunctionPagePcId()); + // 功能菜单及子页面主键id集合 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPermissionRoleMenuRelation::getRoleMenuConfigId); + queryWrapper.eq(FtbPermissionRoleMenuRelation::getRoleId, ftbPermissionRole.getRoleId()); + queryWrapper.eq(FtbPermissionRoleMenuRelation::getRoleMenuId, a.getId()); + List ftbPermissionRoleMenuRelations = ftbPermissionRoleMenuRelationMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(ftbPermissionRoleMenuRelations)) { + List roleMenuConfigs = ftbPermissionRoleMenuRelations.stream().map(FtbPermissionRoleMenuRelation::getRoleMenuConfigId).collect(Collectors.toList()); + permissionRoleInnerDTO.setMemberIds(roleMenuConfigs); + } + ftbPermissionRole.getPermissionRoleInners().add(permissionRoleInnerDTO); + }); + // 添加角色 + addRole(ftbPermissionRole); + } + + + private void userMenuPermissions(String userId, List ftbPermissionRolePersonVOS, List jobAuthorizationInformation) { + Set result = new HashSet<>(); + Stream.concat(ftbPermissionRolePersonVOS.stream(), jobAuthorizationInformation.stream()) + .forEach(a -> { + Stream.of(a.getFunctionPageAppId(), a.getFunctionPageWebId(), a.getWebId() + , a.getAppId(), a.getPcId(),a.getFunctionPagePcId()) + .filter(StrUtil::isNotBlank) + .flatMap(b -> Arrays.stream(b.split(StringPool.COMMA))) + .forEach(result::add); + }); + // 存放Redis,获取用户信息时使用 + redisTemplate.opsForValue().set("user:permission:menu:" + userId, String.join(",", result)); + // 查询权限编码并存入redis + List roleMenuIds = Stream.concat(ftbPermissionRolePersonVOS.stream(), jobAuthorizationInformation.stream()) + .map(FtbPermissionRolePersonVO::getRoleMenuId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(roleMenuIds)) { + // 查询权限编码并存入redis + List funcCodings = ftbPermissionRoleMenuMapper.queryFuncCodingsByRoleMenuIds(roleMenuIds); + if (CollUtil.isNotEmpty(funcCodings)) { + redisTemplate.opsForValue().set(FtbPermissionRedisKeys.PERMISSION_ENCODING_KEY + userId, String.join(",", funcCodings)); + } + } + } + + private Set userMenuPermissionsWithUserId(List ftbPermissionRolePersonVOS, List jobAuthorizationInformation) { + Set result = new HashSet<>(); + Stream.concat(ftbPermissionRolePersonVOS.stream(), jobAuthorizationInformation.stream()) + .forEach(a -> { + Stream.of(a.getFunctionPageAppId(), a.getFunctionPageWebId(), a.getWebId(), a.getAppId()) + .filter(StrUtil::isNotBlank) + .flatMap(b -> Arrays.stream(b.split(StringPool.COMMA))) + .forEach(result::add); + }); + // 存放Redis,获取用户信息时使用 + return result; + } + + // 合并权限方法 + private Map mergePermissions(Map first, Map second) { + // 确定主从集合:较大的作为基础,较小的用于更新 + Map base = first.size() > second.size() ? first : second; + Map update = first.size() > second.size() ? second : first; + // 初始化结果集 + Map result = new HashMap<>(base); + // 遍历更新集合并按优先级更新 + update.forEach((key, value) -> { + if (result.containsKey(key)) { + int currentPriority = getScopePriority(result.get(key)); + int newPriority = getScopePriority(value); + if (newPriority < currentPriority) { + result.put(key, value); + } else if (newPriority == currentPriority) { + // 优先级相同时,取并集 + result.put(key, mergeScopeValues(result.get(key), value)); + } + } else { + result.put(key, value); + } + }); + return result; + } + + // 获取权限优先级的方法 + private int getScopePriority(String scopeValue) { + String[] parts = scopeValue.split("#"); + int scope = Integer.parseInt(parts[0]); + // 如果没有匹配到,默认为最低优先级 + return SCOPE_PRIORITY_MAP.getOrDefault(scope, Integer.MAX_VALUE); + } + + private void populateMap(List dataList, Map targetMap) { + // 过滤模块未设置数据权限的数据 + dataList.stream().filter(vo -> Objects.nonNull(vo.getScope())).forEach(vo -> + Stream.of(vo.getWebId(), vo.getAppId(), vo.getPcId()) + .filter(StrUtil::isNotBlank) + .forEach(key -> { + if (targetMap.containsKey(key)) { + // 如果已存在,则合并权限 + String existingScopeValue = targetMap.get(key); + String newScopeValue = buildScopeValue(vo); + int existingPriority = getScopePriority(existingScopeValue); + int newPriority = getScopePriority(newScopeValue); + if (newPriority < existingPriority) { + targetMap.put(key, buildScopeValue(vo)); + } else if (newPriority == existingPriority) { + // 优先级相同时,取并集 + targetMap.put(key, mergeScopeValues(existingScopeValue, newScopeValue)); + } + } else { + targetMap.put(key, buildScopeValue(vo)); + } + }) + ); + } + + private String mergeScopeValues(String existingValue, String newValue) { + // 解析现有值和新值 + String[] existingParts = existingValue.split("#"); + String[] newParts = newValue.split("#"); + + int existingScope = Integer.parseInt(existingParts[0]); + int newScope = Integer.parseInt(newParts[0]); + + // 只有当权限范围类型相同时才合并(理论上这里existingScope == newScope) + if (existingScope == newScope) { + // 对于指定组织类型(4)需要合并组织ID + if (existingScope == 4 && existingParts.length > 1 && newParts.length > 1) { + // 合并组织ID列表,去重 + Set orgIds = new HashSet<>(); + String[] existingOrgIds = existingParts[1].split(","); + String[] newOrgIds = newParts[1].split(","); + + orgIds.addAll(Arrays.asList(existingOrgIds)); + orgIds.addAll(Arrays.asList(newOrgIds)); + + return existingScope + "#" + String.join(",", orgIds); + } else { + // 对于其他类型,直接返回其中一个即可(因为它们是相同的) + return existingValue; + } + } + // 正常情况下不应该到达这里,但如果到达了,返回优先级更高的那个 + return getScopePriority(existingValue) <= getScopePriority(newValue) ? existingValue : newValue; + } + + + private String buildScopeValue(FtbPermissionRolePersonVO vo) { + return vo.getScope() == 4 ? + String.format("%d#%s", vo.getScope(), vo.getOrgIds()) : + String.valueOf(vo.getScope()); + } + + + /** + * 人员被踢下线 + * + * @param roleId 角色ID + */ + private void personnelKickedOffTheLine(String roleId) { + String tenantId = UserProvider.getUser().getTenantId(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPermissionRoleAuthorizePerson::getUserId); + queryWrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark,0); + queryWrapper.eq(FtbPermissionRoleAuthorizePerson::getRoleId, roleId); + List ftbPermissionRoleAuthorizePeople = ftbPermissionRoleAuthorizePersonMapper.selectList(queryWrapper); + List userIds = ftbPermissionRoleAuthorizePeople.stream().map(FtbPermissionRoleAuthorizePerson::getUserId).collect(Collectors.toList()); + // 查询岗位绑定的权限 + LambdaQueryWrapper queryWrapperPost = Wrappers.lambdaQuery(); + queryWrapperPost.select(FtbPermissionRoleAuthorizePost::getPostId); + queryWrapperPost.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + queryWrapperPost.eq(FtbPermissionRoleAuthorizePost::getRoleId, roleId); + List ftbPermissionRoleAuthorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(queryWrapperPost); + List postIds = ftbPermissionRoleAuthorizePosts.stream().map(FtbPermissionRoleAuthorizePost::getPostId).collect(Collectors.toList()); + // 根据岗位查询岗位所绑定的人员 + if (CollUtil.isNotEmpty(postIds)) { + QueryUserBatchDTO dto = new QueryUserBatchDTO(); + dto.setPositionIds(postIds); + dto.setTenantId(tenantId); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(dto); + if (userInfoBatch != null && userInfoBatch.getCode() == 200) { + userIds.addAll(userInfoBatch.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList())); + } + } + userIds.forEach(a -> { + UserProvider.kickoutByUserId(a, tenantId); + }); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionUsersServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionUsersServiceImpl.java new file mode 100644 index 0000000..5b9ae77 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/service/impl/FtbPermissionUsersServiceImpl.java @@ -0,0 +1,148 @@ +package jnpf.authority.service.impl; + +import cn.hutool.core.collection.CollUtil; +import jnpf.authority.service.FtbPermissionUsersService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.authority.dto.permission.AuthGetTargetUserInfoBatchDTO; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.*; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.util.UserProvider; +import jnpf.util.auth.V2AuthPermissionUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 带权限用的用户管理功能 + * + * @author Flynn Chan + * @create 2025-05-21 + */ +@Service +@Slf4j +public class FtbPermissionUsersServiceImpl implements FtbPermissionUsersService { + @Resource + private V2AuthPermissionUtils authPermissionUtils; + @Resource + private V2UserApi v2UserApi; + @Resource + private FtbPersonnelsTurnoverManagementService turnoverManagementService; + + @Override + public PageListVO pagePost(QueryPageUserDTO dto) { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + dto.setTenantId(UserProvider.getUser().getTenantId()); + dto.setHavaAuth(targetAuthIdsVO != null); + dto.setTargetAuthIdsVO(targetAuthIdsVO); + return v2UserApi.pagePost(dto).getData(); + } + + @Override + public PageListVO pagePostMoreKeyword(QueryPageUserMoreKeywordDTO dto) { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + dto.setTenantId(UserProvider.getUser().getTenantId()); + dto.setHavaAuth(targetAuthIdsVO != null); + dto.setTargetAuthIdsVO(targetAuthIdsVO); + return v2UserApi.pagePostMoreKeyword(dto).getData(); + } + + @Override + public List authGetAllUserInfoBatch() { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + AuthUserBoundInfoBatchDTO dto = new AuthUserBoundInfoBatchDTO(targetAuthIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(targetAuthIdsVO != null); + return v2UserApi.authGetAllUserInfoBatch(dto).getData(); + } + + @Override + public List authGetAllUserInfoBatch(List userWorkStatusEnumsNotList) { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + AuthUserBoundInfoBatchDTO dto = new AuthUserBoundInfoBatchDTO(targetAuthIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(targetAuthIdsVO != null); + dto.setUserWorkStatusEnumsList(userWorkStatusEnumsNotList); + return v2UserApi.authGetAllUserInfoBatch(dto).getData(); + } + + @Override + public List authGetPermissionScopeUserIds() { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + AuthUserBoundInfoBatchDTO dto = new AuthUserBoundInfoBatchDTO(targetAuthIdsVO, UserProvider.getUser().getTenantId()); + dto.setHavaAuth(targetAuthIdsVO != null); + ActionResult> result = v2UserApi.authGetPermissionScopeUserIds(dto); + return (result != null && result.getData() != null) ? result.getData() : new ArrayList<>(); + } + + @Override + public List listTargetOrganizeIdAuth(String organizeId, List userWorkStatusEnumsList) { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + AuthUserListTargetOrganizeDTO dto = new AuthUserListTargetOrganizeDTO(targetAuthIdsVO); + dto.setHavaAuth(targetAuthIdsVO != null); + dto.setOrganizeId(organizeId); + if (CollUtil.isNotEmpty(userWorkStatusEnumsList)) { + dto.setUserWorkStatusEnumsList(userWorkStatusEnumsList); + } + return v2UserApi.listTargetOrganizeIdAuth(dto); + } + + @Override + public List listTargetOrganizeIdsAuth(List organizeIds, List userWorkStatusEnumsList) { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + AuthUserListTargetOrganizeIdsDTO dto = new AuthUserListTargetOrganizeIdsDTO(targetAuthIdsVO); + dto.setHavaAuth(targetAuthIdsVO != null); + dto.setOrganizeIds(organizeIds); + if (CollUtil.isNotEmpty(userWorkStatusEnumsList)) { + dto.setUserWorkStatusEnumsList(userWorkStatusEnumsList); + } + + return v2UserApi.listTargetOrganizeIdsAuth(dto); + } + + @Override + public List authGetTargetUserInfoBatch(AuthGetTargetUserInfoBatchDTO dto) { + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + AuthTargetUserBatchDTO authTargetUserBatchDTO = new AuthTargetUserBatchDTO(targetAuthIdsVO, UserProvider.getUser().getTenantId()); + authTargetUserBatchDTO.setUserIds(dto.getUserIds()); + + return v2UserApi.authGetTargetUserInfoBatch(authTargetUserBatchDTO); + } + + @Override + public List getTargetUserInfoBatchByIds(List ids) { + if (CollUtil.isEmpty(ids)) { + log.error("getTargetUserInfoBatchByIds(List ids), ids为空"); + return new ArrayList<>(); + } + List userBoundVOS = v2UserApi.getAllUserInfoBatch(ids, UserProvider.getUser().getTenantId()).getData(); + //查询离职人员 + // 提取已查到的用户ID + Set foundIds = userBoundVOS.stream() + .map(UserBoundVO::getId) + .collect(Collectors.toSet()); + + // 找出没查到的(可能离职或被删除的) + List missingIds = ids.stream() + .filter(id -> !foundIds.contains(id)) + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(missingIds)) { + FtbDepUserDTO ftbDepUserDTO = new FtbDepUserDTO(); + ftbDepUserDTO.setUserIds(missingIds); + List ftbDepUsers = turnoverManagementService.getInformationAboutTheDepartingPerson(ftbDepUserDTO); + if (CollUtil.isNotEmpty(ftbDepUsers)) { + userBoundVOS.addAll(ftbDepUsers); + } + } + return userBoundVOS; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableEnums.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableEnums.java new file mode 100644 index 0000000..4fadd1c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableEnums.java @@ -0,0 +1,37 @@ +package jnpf.authority.utils; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 权限适用范围枚举 + * + * @author wangchunxiang + * @date 2025/06/05 + */ +@Getter +@RequiredArgsConstructor +public enum PermissionsApplicableEnums { + // 权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + ALL("0", "全部"), + SCOPE_ORGANIZATION_AND_SUBORDINATE_EMPLOYEES("1", "所在组织和下级组织员工"), + SCOPE_ORGANIZATION_EMPLOYEES("2", "所在组织员工"), + SCOPE_SUBORDINATE("3", "仅下属"), + SCOPE_SPECIFIC_ORGANIZATION("4", "指定组织"); + + private final String value; + private final String name; + + /** + * 根据 value 获取枚举 + */ + public static PermissionsApplicableEnums fromValue(String value) { + for (PermissionsApplicableEnums e : PermissionsApplicableEnums.values()) { + if (e.getValue().equals(value)) { + return e; + } + } + throw new IllegalArgumentException("No enum constant with value: " + value); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableObject.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableObject.java new file mode 100644 index 0000000..e28cd70 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsApplicableObject.java @@ -0,0 +1,26 @@ +package jnpf.authority.utils; + +import lombok.Data; + +import java.util.List; + +/** + * 权限范围适用对象 + * + * @author wangchunxiang + * @date 2025/06/05 + */ +@Data +public class PermissionsApplicableObject { + + /** + * 权限范围 + */ + private PermissionsApplicableEnums permissionsApplicableEnums; + + /** + * 组织ID集合 + */ + private List orgIds; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsEnums.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsEnums.java new file mode 100644 index 0000000..7cb44d4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsEnums.java @@ -0,0 +1,39 @@ +package jnpf.authority.utils; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 权限大模块枚举 + * + * @author wangchunxiang + * @date 2025/04/23 + */ +@Getter +@RequiredArgsConstructor +public enum PermissionsEnums { + /** + * 人事管理 + */ + PERSONNEL_MANAGEMENT_WEB("personel","人事管理"), + /** + * 人事管理 + */ + PERSONNEL_MANAGEMENT_APP("pages.personnel","人事管理"), + /** + * 人事管理 按钮 + */ + PERSONNEL_MANAGEMENT_Button("DirectProcessing,ResignApproval,DirectProcessingApp,ResignApprovalApp","人事管理"), + + + + /** + * 考勤管理 value:取值来自于 base_module表 且 type = 1 取值 : F_EnCode + */ + ATTENDANCE_MANAGEMENT("workAttendance","考勤管理") + + ; + private final String value; + private final String name; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsUtils.java new file mode 100644 index 0000000..e498677 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/authority/utils/PermissionsUtils.java @@ -0,0 +1,750 @@ +package jnpf.authority.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.authority.mapper.*; +import jnpf.authority.service.FtbPermissionRoleService; +import jnpf.base.ActionResult; +import jnpf.base.ModuleApi; +import jnpf.base.SystemApi; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.entity.SystemEntity; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.authority.po.*; +import jnpf.model.authority.vo.person.FtbEmployeePermissionPersonnelVO; +import jnpf.model.authority.vo.person.FtbUserPermissionVO; +import jnpf.model.authority.vo.role.FtbPermissionRoleIdentificationVO; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +; + +/** + * 权限内部调用 + * + * @author wangchunxiang + * @date 2025/04/10 + */ +@Component +@Slf4j +public class PermissionsUtils { + + @Resource + private V2UserApi v2UserApi; + @Resource + private V2PositionApi v2PositionApi; + @Resource + private V2OrganizeApi v2OrganizeApi; + + @Resource + private RedisUtil redisUtil; + + @Resource + private SystemApi systemApi; + + @Resource + private ModuleApi moduleApi; + @Resource + private FtbPermissionRoleAuthorizePersonMapper ftbPermissionRoleAuthorizePersonMapper; + @Resource + private FtbPermissionRoleAuthorizePostMapper ftbPermissionRoleAuthorizePostMapper; + @Resource + private FtbPermissionRoleMapper permissionRoleMapper; + @Resource + private FtbPermissionFunctionMenuMapper ftbPermissionFunctionMenuMapper; + @Resource + private FtbPermissionRoleMenuRelationMapper ftbPermissionRoleMenuRelationMapper; + @Resource + private FtbPermissionRoleMenuMapper ftbPermissionRoleMenuMapper; + @Resource + private FtbPermissionRoleService ftbPermissionRoleService; + // 用户权限缓存key + // 前缀,用于区分不同的缓存数据 + private static final String PREFIX = "user:permission:"; + + private static final Map cache = new ConcurrentHashMap<>(); + + /** + * 根据用户ID生成Redis键 + * + * @param userId 用户ID + * @return Redis键 + */ + public static String getUserPermissionKey(String userId) { + return PREFIX + "user:" + userId; + } + + /** + * 获取当前登录用户所具有的数据权限用户Id,(超级管理员调动方自行判断) + * + * @param userId 用户ID + * @param permissionModule 权限模块汉字 + * @param category 类别,传WEB或者APP + * @return {@link List }<{@link String }> + */ + public List obtainPersonnelUserIdDataPermissions(String userId, String permissionModule, String category) { + String hashValue = obtainTheScopeOfUserPermissions(userId, permissionModule, category); + return processScope(hashValue, userId, null); + } + + /** + * 获取指定用户所具有的数据权限用户Id,(超级管理员调动方自行判断) + * + * @param userId 用户ID + * @param moduleId 权限模块id + * @return {@link List }<{@link String }> + */ + public List obtainPersonnelUserIdDataPermissions(String userId, String moduleId) { + Map queryUserScopeOfPermissions = ftbPermissionRoleService.queryUserScopeOfPermission(userId); + String hashValue = queryUserScopeOfPermissions.get(moduleId); + // 无此模块权限,人员返回空数组 + if (StrUtil.isBlank(hashValue)) { + return new ArrayList<>(); + } + return processScope(hashValue, userId, null); + } + + /** + * 获取用户权限适用范围 + * + * @param userId 用户 ID + * @param moduleId 权限模块id + * @return {@link PermissionsApplicableEnums } 返回#分割的数组,如果是指定组织,则#后面跟着组织id,权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + */ + public PermissionsApplicableObject obtainTheScopeOfUserPermissionsEnums(String userId, String moduleId, String tenantId) { + Map queryUserScopeOfPermissions = ftbPermissionRoleService.queryUserScopeOfPermission(userId,tenantId); + String hashValue = queryUserScopeOfPermissions.get(moduleId); + return getPermissionsApplicableObject(hashValue); + } + /** + * 获取用户权限适用范围 + * + * @param userId 用户 ID + * @param moduleId 权限模块id + * @return {@link PermissionsApplicableEnums } 返回#分割的数组,如果是指定组织,则#后面跟着组织id,权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + */ + public Map obtainTheScopeOfUserPermissionsEnums(List userIds, String moduleId, String tenantId) { + Map> userScopeOfPermission = ftbPermissionRoleService.queryUserScopeOfPermission(userIds, tenantId); + log.info("userScopeOfPermission:{},moduleId:{},tenantId:{}", userScopeOfPermission,moduleId,tenantId); + Map hashMap = new HashMap<>(); + userIds.forEach(userId -> { + if (userScopeOfPermission.containsKey(userId)) { + String hashValue = userScopeOfPermission.get(userId).get(moduleId); + hashMap.put(userId,getPermissionsApplicableObject( hashValue)) ; + } + }); + return hashMap; + } + + /** + * 获取用户权限适用范围 + * + * @param userId 用户 ID + * @return {@link PermissionsApplicableEnums } 返回#分割的数组,如果是指定组织,则#后面跟着组织id,权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + */ + public PermissionsApplicableObject obtainTheScopeOfUserPermissionsEnums(String userId) { + String requestModule = getRequestModule(); + String hashValue = getValidatedHashValue(userId, requestModule); + return getPermissionsApplicableObject(hashValue); + } + + private PermissionsApplicableObject getPermissionsApplicableObject(String hashValue) { + PermissionsApplicableObject permissionsApplicableObject = new PermissionsApplicableObject(); + // 无此模块权限,人员返回空数组 + if (StrUtil.isBlank(hashValue)) { + return permissionsApplicableObject; + } + String[] splits = hashValue.split("#"); + PermissionsApplicableEnums permissionsApplicableEnums = PermissionsApplicableEnums.fromValue(splits[0]); + permissionsApplicableObject.setPermissionsApplicableEnums(permissionsApplicableEnums); + if (PermissionsApplicableEnums.SCOPE_SPECIFIC_ORGANIZATION.equals(permissionsApplicableEnums)) { + permissionsApplicableObject.setOrgIds(List.of(splits[1].split(","))); + } + return permissionsApplicableObject; + } + + /** + * 获取用户权限适用范围 + * + * @param userId 用户 ID + * @param permissionModule 权限模块 + * @param category 类别 + * @return {@link String } 返回#分割的数组,如果是指定组织,则#后面跟着组织id,权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + */ + public String obtainTheScopeOfUserPermissions(String userId, String permissionModule, String category) { + String cacheKey = permissionModule + "#" + category; + String module; + if (cache.get(cacheKey) != null) { + module = cache.get(cacheKey); + } else { + module = getModule(permissionModule, category); + cache.put(cacheKey, module); + } + // 提取模块Id + return getValidatedHashValue(userId, module); + } + + /** + * 获取当前登录用户所具有的数据权限组织Id,(超级管理员调动方自行判断) + * + * @param userId 用户ID + * @param permissionModule 权限模块汉字 + * @param category 类别,传WEB或者APP + * @return {@link List }<{@link String }> + */ + public List obtainPersonnelOrganizationIdDataPermissions(String userId, String permissionModule, String category) { + String hashValue = obtainTheScopeOfUserPermissions(userId, permissionModule, category); + return processScopeOrgIds(hashValue, userId); + } + + /** + * 获取当前登录用户所具有的数据权限组织Id,(超级管理员调动方自行判断) + * + * @param userId 用户ID + * @return {@link List }<{@link String }> + */ + public List obtainPersonnelOrganizationIdDataPermissions(String userId) { + // 提取模块获取方法 + String module = getRequestModule(); + // 提取hash值验证 + String hashValue = getValidatedHashValue(userId, module); + return processScopeOrgIds(hashValue, userId); + } + + /** + * 当前登录员工已有的按钮及页面权限标识集合 + */ + public FtbPermissionRoleIdentificationVO permissionIdentificationCollection() { + return ftbPermissionRoleService.permissionIdentificationCollection(); + } + + + private String getModule(String permissionModule, String category) { + String module; + String tenantId = UserProvider.getUser().getTenantId(); + SystemEntity systemVO = systemApi.getSystemVO(); + String systemId = systemVO.getId(); +// String tenantId = UserProvider.getUser().getTenantId(); + List data = moduleApi.getListWithoutSystemId(systemId, category, permissionModule, tenantId); + // 获取顶级目录菜单 + ModuleEntity entities = data.stream() + .filter(item -> item.getType() != null && item.getEnabledMark() != null && 1 == item.getEnabledMark() && "-1".equals(item.getParentId())) + .findFirst().orElse(null); + if (entities == null) { + throw new RuntimeException("暂未找到模块ID"); + } + module = entities.getId(); + return module; + } + private List getModuleWithTenantId(List permissionModule, String category,String tenantId) { + ActionResult> listByTenantId = systemApi.getListByTenantId(tenantId); + if (listByTenantId == null) { + throw new RuntimeException("系统管理服务获取数据失败!"); + } + List list = listByTenantId.getData(); + SystemEntity systemEntity = list.stream().filter(item -> !"mainSystem".equals(item.getEnCode())).findFirst().orElse(null); + if (systemEntity == null) { + throw new RuntimeException("暂未找到模块ID"); + } + String systemId = systemEntity.getId(); + return permissionModule.stream().map(v->{ + List data = moduleApi.getListWithoutSystemId(systemId, category, v, tenantId); + // 获取顶级目录菜单 + ModuleEntity entities = data.stream() + .filter(item -> item.getType() != null && item.getEnabledMark() != null && 1 == item.getEnabledMark() && "-1".equals(item.getParentId())) + .findFirst().orElse(null); + if (entities == null) { + throw new RuntimeException("暂未找到模块ID"); + } + return entities.getId(); + }).collect(Collectors.toList()); + } + + + + /** + * 根据用户id,查询具有改用户权限的人员 + * + * @param userId 需要查询的人员 + * @param buttonPermissions 按钮权限 + * @param category web / app + * @param currOrg + * @param moduleId + * @return 具备该权限的人员列表 + */ + public List getUserPersonnelOrganizationIdDataPermissions(String userId, List buttonPermissions, String category, List module, String tenantId, String currOrg, String moduleId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + List data = selectedOrganizationAndPositionEmployees(tenantId); + if (CollUtil.isEmpty(data)) return null; + List userIds = data.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + List adminUserIds = data.stream().filter(UserBoundVO::getIsAdministrator).map(UserBoundVO::getId).collect(Collectors.toList()); + // 过滤超管 + userIds = userIds.stream().filter(v->!adminUserIds.contains(v)).collect(Collectors.toList()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(CollUtil.isNotEmpty(userIds), FtbPermissionRoleAuthorizePerson::getUserId, userIds); + List people = ftbPermissionRoleAuthorizePersonMapper.selectList(wrapper); + if (CollUtil.isEmpty(people)) return null; + // 列表中所有人员和角色信息集合 + List personnelVOS = getEmpPersonnelVOS(data, people); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPermissionFunctionMenu::getOriginalCoding, buttonPermissions); + List functionMenus = ftbPermissionFunctionMenuMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(functionMenus)) return null; + // 查询具有权限的角色ids + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPermissionRoleMenuRelation::getRoleMenuConfigId, functionMenus.stream().map(FtbPermissionFunctionMenu::getId).collect(Collectors.toList())); + List relations = ftbPermissionRoleMenuRelationMapper.selectList(lambdaed); + if (CollUtil.isEmpty(relations)) return null; + // 具有权限的角色ids + List idsOfTheRoleWithPermissions = relations.stream().map(FtbPermissionRoleMenuRelation::getRoleId).collect(Collectors.toList()); + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.in(FtbPermissionRoleMenu::getFunctionWebId, getModuleWithTenantId(module, category ,tenantId)); + // 人事数据角色权限list + query.eq(FtbPermissionRoleMenu::getEnableMark, 0); + List roleMenus = ftbPermissionRoleMenuMapper.selectList(query); + if (CollUtil.isEmpty(roleMenus)) return null; + // Map roleMenuMap = roleMenus.stream().collect(Collectors.toMap(FtbPermissionRoleMenu::getRoleId, Function.identity(), (key1, key2) -> key1)); + List peopleWithThesePermissions = personnelVOS.stream().filter(v -> { + if (StringUtils.isEmpty(v.getRoleIds())) { + return false; + } + String roleIds = v.getRoleIds(); + String[] split = roleIds.split(","); + List stringList = Arrays.stream(split).filter(idsOfTheRoleWithPermissions::contains).collect(Collectors.toList()); + if (CollUtil.isEmpty(stringList)) { + return false; + } + return true; + }).collect(Collectors.toList()); + List peouUserIds = peopleWithThesePermissions.stream().map(FtbEmployeePermissionPersonnelVO::getUserId).collect(Collectors.toList()); + Map userPermssion =new HashMap<>();; + peouUserIds.parallelStream().forEach(v -> { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + Map queryUserScopeOfPermissions = ftbPermissionRoleService.queryUserScopeOfPermission(v,tenantId); + String hashValue = queryUserScopeOfPermissions.get(moduleId); + if ("null".equals(hashValue) || hashValue == null) return; + userPermssion.put(v, hashValue); + }); + List collect = peopleWithThesePermissions.stream().filter(item -> { + if (!userPermssion.containsKey(item.getUserId())) return false; + String haves = userPermssion.get(item.getUserId()); + try { + List processScope = processScopeOrgIds(haves, item.getUserId(), moduleId, tenantId); + if (processScope == null) return true; + if (CollUtil.isEmpty(processScope)) return false; + return processScope.contains(currOrg); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + }).map(FtbEmployeePermissionPersonnelVO::getUserId).collect(Collectors.toList()); + +// // 过滤出具有权限的人 +// List collect = peopleWithThesePermissions.stream().filter(item -> { +// if (StringUtils.isEmpty(item.getRoleIds())) { +// return false; +// } +// String roleIds = item.getRoleIds(); +// String[] split = roleIds.split(","); +// List userPermissionVOS = Arrays.stream(split).map(roleKey -> { +// FtbPermissionRoleMenu roleMenu = roleMenuMap.get(roleKey); +// if (roleMenu == null) { +// return null; +// } +// String haves = roleMenu.getScopePermission() + "#" + "--------"; +// try { +// +// List processScope = processScopeOrgIds(haves, item.getUserId(),moduleId,tenantId); +// FtbUserPermissionVO permissionVO = new FtbUserPermissionVO(); +// permissionVO.setUserId(item.getUserId()); +// permissionVO.setPermissionIds(processScope); +// return permissionVO; +// } catch (Exception e) { +// e.printStackTrace(); +// return null; +// } +// }).filter(Objects::nonNull).collect(Collectors.toList()); +// long count = userPermissionVOS.stream().filter(v -> { +// // 超级管理员 +// if (v.getPermissionIds() == null) return true; +// // 无权限 +// if (CollUtil.isEmpty(v.getPermissionIds())) return false; +// return v.getPermissionIds().contains(currOrg); +// } +// ).count(); +// return count != 0; +// }).map(FtbEmployeePermissionPersonnelVO::getUserId).distinct().collect(Collectors.toList()); + log.info("获取人员权限:{}", collect); + adminUserIds.addAll(collect); + return adminUserIds; + } + + /** + * 查询具有某个按钮功能的用户权限的人员 + * + * @param buttonPermissions 按钮权限编码 + * @return 具备该权限的人员列表(用户Id) + */ + public List getUserPersonnelOrganizationIdDataPermissions(String buttonPermissions, String tenantId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + // 判断勾选了按钮的权限 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPermissionFunctionMenu::getOriginalCoding, buttonPermissions); + queryWrapper.eq(FtbPermissionFunctionMenu::getEnableMark,0); + List functionMenus = ftbPermissionFunctionMenuMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(functionMenus)) { + return null; + } + // 查询具有权限的角色ids,岗位授权 + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPermissionRoleMenuRelation::getRoleMenuConfigId, functionMenus.stream().map(FtbPermissionFunctionMenu::getId).collect(Collectors.toList())); + lambdaed.eq(FtbPermissionRoleMenuRelation::getEnableMark,0); + List relations = ftbPermissionRoleMenuRelationMapper.selectList(lambdaed); + if (CollUtil.isEmpty(relations)) { + return null; + } + List roleIds = relations.stream().map(FtbPermissionRoleMenuRelation::getRoleId).collect(Collectors.toList()); + List roles = permissionRoleMapper.selectBatchIds(roleIds); + roleIds = roles.stream().filter(a->a.getEnableMark() == 0).map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(roleIds)) { + return null; + } + // 人员授权 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPermissionRoleAuthorizePerson::getRoleId,roleIds); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getEnableMark,0); + List people = ftbPermissionRoleAuthorizePersonMapper.selectList(wrapper); + List userIds = people.stream().map(FtbPermissionRoleAuthorizePerson::getUserId).collect(Collectors.toList()); + // 岗位授权 + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.in(FtbPermissionRoleAuthorizePost::getRoleId,roleIds); + postWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List posts = ftbPermissionRoleAuthorizePostMapper.selectList(postWrapper); + List postIds = posts.stream().map(FtbPermissionRoleAuthorizePost::getPostId).collect(Collectors.toList()); + // 获取岗位下绑定了那些人 + if (CollUtil.isNotEmpty(postIds)) { + QueryUserBatchDTO dto = new QueryUserBatchDTO(); + dto.setPositionIds(postIds); + dto.setTenantId(tenantId); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(dto); + if (userInfoBatch.getCode() == 200 && CollUtil.isNotEmpty(userInfoBatch.getData())) { + List postionUserIds = userInfoBatch.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + userIds.addAll(postionUserIds); + } + } + return userIds; + } + + /** + * 所选组织和岗位员工 + */ + private List selectedOrganizationAndPositionEmployees(String tenantId) { + QueryPageUserDTO queryPageUserDTO = new QueryPageUserDTO(); + queryPageUserDTO.setIsPage(false); + queryPageUserDTO.setTenantId(tenantId); + ActionResult> pageListVOActionResult = v2UserApi.pagePost(queryPageUserDTO); + if (pageListVOActionResult.getCode() == 200 && Objects.nonNull(pageListVOActionResult.getData())) { + List list = pageListVOActionResult.getData().getList(); + return list; + } + return null; + } + /** + * 查询当前用户的操作权限集合 + * + * @param userId 用户id + * @param permissionModule 操作权限集合 + * @param category web / app + * @param module {@link PermissionsEnums} + * @return 可具有的操作权限, 没有范围空 + */ + public List queryCollectionOfUserButtonPermissions(String userId, List permissionModule, String category, String module) { + ActionResult> byLikeName = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), null); + if (byLikeName == null || CollUtil.isEmpty(byLikeName.getData())) { + return null; + } + UserBoundVO userBoundVO = byLikeName.getData().stream().findFirst().orElse(null); + if (userBoundVO == null) return null; + // 人员id + String positionId = userBoundVO.getPositionId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPermissionRoleAuthorizePerson::getUserId, userId); + List people = ftbPermissionRoleAuthorizePersonMapper.selectList(wrapper); + List roleIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(people)) + roleIds = people.stream().map(FtbPermissionRoleAuthorizePerson::getRoleId).collect(Collectors.toList()); + // 授权的岗位 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getPostId, positionId); + FtbPermissionRoleAuthorizePost post = ftbPermissionRoleAuthorizePostMapper.selectOne(queryWrapper); + if (ObjectUtil.isNotEmpty(post)) { + roleIds.add(post.getRoleId()); + roleIds = roleIds.stream().distinct().collect(Collectors.toList()); + } + if (CollUtil.isEmpty(roleIds)) return null; + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbPermissionRoleMenu::getFunctionWebId, getModule(module, category)); + query.eq(FtbPermissionRoleMenu::getRoleId, roleIds); + // 人事数据角色权限list + query.eq(FtbPermissionRoleMenu::getEnableMark, 0); + List roleMenus = ftbPermissionRoleMenuMapper.selectList(query); + if (CollUtil.isEmpty(roleMenus)) return null; + + LambdaQueryWrapper buttonPermissionsQueryWrapper = Wrappers.lambdaQuery(); + buttonPermissionsQueryWrapper.in(FtbPermissionFunctionMenu::getFunName, permissionModule); + List functionMenus = ftbPermissionFunctionMenuMapper.selectList(buttonPermissionsQueryWrapper); + if (CollUtil.isEmpty(functionMenus)) return null; + Map functionMenuMap = functionMenus.stream().collect(Collectors.toMap(FtbPermissionFunctionMenu::getId, Function.identity())); + // 查询具有权限的角色ids + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPermissionRoleMenuRelation::getRoleMenuConfigId, functionMenus.stream().map(FtbPermissionFunctionMenu::getId).collect(Collectors.toList())); + List relations = ftbPermissionRoleMenuRelationMapper.selectList(lambdaed); + if (CollUtil.isEmpty(relations)) return null; + List finalRoleIds = roleIds; + return relations.stream().filter(v -> finalRoleIds.contains(v.getRoleId())).map(v -> functionMenuMap.get(v.getRoleMenuId()).getFunName()).collect(Collectors.toList()); + } + + @NotNull + private List getEmpPersonnelVOS(List orgUserIds, List people) { + Map> listMap = people.stream().collect(Collectors.groupingBy(FtbPermissionRoleAuthorizePerson::getUserId)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + List rolesList = permissionRoleMapper.selectList(wrapper); + Map roleMap = rolesList.stream().collect(Collectors.toMap(FtbPermissionRole::getId, Function.identity(), (r1, r2) -> r1)); + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPermissionRoleAuthorizePost::getEnableMark,0); + List authorizePosts = ftbPermissionRoleAuthorizePostMapper.selectList(queryWrapper); + Map> postIdMap = new HashMap<>(); + if(CollUtil.isNotEmpty(authorizePosts)){ + postIdMap = authorizePosts.stream().collect(Collectors.groupingBy(FtbPermissionRoleAuthorizePost::getPostId)); + } + Map> finalPostIdMap = postIdMap; + return orgUserIds.stream().map(k -> { + if (k == null || k.getId() == null) { + return null; + } + String userId = k.getId(); + FtbEmployeePermissionPersonnelVO personnelVO = new FtbEmployeePermissionPersonnelVO(); + personnelVO.setUserId(userId); + personnelVO.setOrgName(k.getOrganizeName()); + personnelVO.setPostId(k.getPositionId()); + personnelVO.setPostName(k.getPositionName()); + personnelVO.setUserName(k.getName()); + + List roleIds = new ArrayList<>(); + if (listMap.containsKey(userId)){ + roleIds.addAll(listMap.get(userId).stream() + .map(FtbPermissionRoleAuthorizePerson::getRoleId) + .collect(Collectors.toList())); + } + + if (k.getPositionId() != null && finalPostIdMap.containsKey(k.getPositionId())) { + List postRoleIds = finalPostIdMap.get(k.getPositionId()).stream() + .map(FtbPermissionRoleAuthorizePost::getRoleId) + .collect(Collectors.toList()); + Set uniqueRoleIds = new HashSet<>(roleIds); + uniqueRoleIds.addAll(postRoleIds); + roleIds = new ArrayList<>(uniqueRoleIds); + } + personnelVO.setRoleIds(String.join(",", roleIds)); + personnelVO.setRoleName(roleIds.stream() + .map(roleId -> { + FtbPermissionRole ftbPermissionRole = roleMap.get(roleId); + return ftbPermissionRole == null ? null : ftbPermissionRole.getRoleName(); + }) + .filter(Objects::nonNull) + .collect(Collectors.joining(","))); + return personnelVO; + }).filter(Objects::nonNull).collect(Collectors.toList()); + + } + + /** + * @param userId + * @param module + * @return + */ + + private String getValidatedHashValue(String userId, String module) { + String hashValue = redisUtil.getHashValues(getUserPermissionKey(userId), module); + if (StrUtil.isBlank(hashValue)) { + // 改为日志打印,由上层处理 + log.error("当前用户暂未设置此模块数据权限适用范围,module值为:{},用户Id为:{}",module,userId); + return null; + } + return hashValue; + } + + private List processScope(String hashValue, String userId, List userWorkStatusEnumsList, String... tenantIds) { + String tenantId = tenantIds.length > 0 ? tenantIds[0] : UserProvider.getUser().getTenantId(); + String[] parts = hashValue.split("#"); + String scope = parts[0]; + // 权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + switch (scope) { + case "0": + return null; + case "1": + return getUserOrganizeUsers(userId, true, tenantId, userWorkStatusEnumsList); + case "2": + return getUserOrganizeUsers(userId, false, tenantId, userWorkStatusEnumsList); + case "3": + return getUnderlingUsers(userId,tenantId); + case "4": + return getSpecifiedOrganizeUsers(parts, tenantId, userWorkStatusEnumsList); + default: + throw new IllegalArgumentException("未知的权限范围类型: " + scope); + } + } + private List processScopeOrgIds(String hashValue, String userId,String moduleId, String... tenantIds) { + String tenantId = tenantIds.length > 0 ? tenantIds[0] : UserProvider.getUser().getTenantId(); + String[] parts = hashValue.split("#"); + String scope = parts[0]; + if(scope == null || "null".equals(scope)) return new ArrayList<>(); + // 权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + switch (scope) { + case "0": + return null; + case "1": + return getUserOrganizeIds(userId, true,tenantId); + case "2": + return getUserOrganizeIds(userId, false,tenantId); + case "3": + // throw new RuntimeException("仅下属组织员工暂不支持"); + return new ArrayList<>(); + case "4": + return getSpecifiedOrganizeOrgIds(parts); + default: + throw new IllegalArgumentException("未知的权限范围类型: " + scope); + } + } + + private List processScopeOrgIds(String hashValue, String userId) { + String[] parts = hashValue.split("#"); + String scope = parts[0]; + // 权限适用范围,0全部1所在组织和下级组织员工,2所在组织员工,3仅下属,4指定组织 + switch (scope) { + case "0": + return null; + case "1": + return getUserOrganizeIds(userId, true); + case "2": + return getUserOrganizeIds(userId, false); + case "3": + // throw new RuntimeException("仅下属组织员工暂不支持"); + // 仅下属权限时,同步返回组织名称,已发邮件 + return getUserOrganizeIds(userId, false); + case "4": + return getSpecifiedOrganizeOrgIds(parts); + default: + throw new IllegalArgumentException("未知的权限范围类型: " + scope); + } + } + + // 公共结果处理方法 + private List processApiResult(ActionResult> result) { + if (result.getCode() != 200 || Objects.isNull(result.getData())) { + throw new RuntimeException("获取用户数据失败,状态码: " + result.getCode()); + } + return result.getData().stream() + .map(UserPageListVO::getId) + .collect(Collectors.toList()); + } + + private List processApiResultOrg(ActionResult> result) { + if (result.getCode() != 200 || Objects.isNull(result.getData())) { + throw new RuntimeException("获取用户数据失败,状态码: " + result.getCode()); + } + return result.getData().stream() + .map(OrganizeGeneralDetailVO::getId) + .collect(Collectors.toList()); + } + + // 具体类型处理方法 + private List getUserOrganizeUsers(String userId, boolean includeSubOrg, String tenantId, List userWorkStatusEnumsList) { + return processApiResult(v2UserApi.listTargetUserOrganize(userId, includeSubOrg, tenantId, userWorkStatusEnumsList, null)); + } + + private List getUserOrganizeIds(String userId, boolean includeSubOrg) { + return processApiResultOrg(v2OrganizeApi.organizesByUserBound(userId, includeSubOrg)); + } + private List getUserOrganizeIds(String userId, boolean includeSubOrg,String tenantId) { + return processApiResultOrg(v2OrganizeApi.organizesByUserBoundSwitchDb(userId, includeSubOrg,tenantId)); + } + + private List getUnderlingUsers(String userId, String tenantId) { + return processApiResult(v2UserApi.listUnderlingTargetUser(userId, tenantId)); + } + + private List getSpecifiedOrganizeUsers(String[] parts, String tenantId, List userWorkStatusEnumsList) { + if (parts.length < 2 || StrUtil.isBlank(parts[1])) { + throw new RuntimeException("未配置指定组织范围"); + } + return processApiResult(v2UserApi.listTargetOrganizes(Arrays.asList(parts[1].split(",")), tenantId, userWorkStatusEnumsList)); + } + + private List getSpecifiedOrganizeOrgIds(String[] parts) { + if (parts.length < 2 || StrUtil.isBlank(parts[1])) { + throw new RuntimeException("未配置指定组织范围"); + } + return Arrays.asList(parts[1].split(",")); + } + + public String getRequestModule() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) throw new IllegalStateException("No active request context"); + HttpServletRequest request = attributes.getRequest(); + if (ObjectUtil.isEmpty(request.getHeader("module"))) throw new RuntimeException("请求头参数缺少module!"); + return request.getHeader("module"); + } + + /** + * 人事管理入职模块人员权限专属 + */ + public List getPersonnelUserIdDataPermissions(String userId, List userWorkStatusEnumsList) { + String requestModule = getRequestModule(); + String hashValue = getValidatedHashValue(userId, requestModule); + return processScope(hashValue, userId, userWorkStatusEnumsList); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/config/FoodSafetyOcrConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/config/FoodSafetyOcrConfig.java new file mode 100644 index 0000000..86569a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/config/FoodSafetyOcrConfig.java @@ -0,0 +1,18 @@ +package jnpf.certificate.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Set; + +@Data +@Configuration +@ConfigurationProperties(prefix = "config.food-safety-ocr") +public class FoodSafetyOcrConfig { + private Set titleNames = Set.of("标题"); + private Set titleValues = Set.of("食品经营许可证"); + private Set issueDateNames = Set.of("日期"); + private Set expireDateNames = Set.of("有效期至"); + private Set businessItemsNames = Set.of("经营项目"); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumer.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumer.java new file mode 100644 index 0000000..580e158 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumer.java @@ -0,0 +1,338 @@ +package jnpf.certificate.consumer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.base.ActionResult; +import jnpf.certificate.service.CertificateManageApiService; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.message.enums.permission.v2.OperationTypeMessageEnums; +import jnpf.message.enums.permission.v2.OrganizeCategoryMessageEnums; +import jnpf.message.model.permission.v2.OrganizeUpdateMessageDTO; +import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.util.DateUtil; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.integration.acks.AcknowledgmentCallback; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * 证照关联处理组织架构消费者。 + */ +@Slf4j +@Component +@EnableBinding(CertificateConsumerSource.class) +public class CertificateConsumer { + + private static final String IDEMPOTENT_KEY_PREFIX = "certificate:consume"; + private static final String IDEMPOTENT_VALUE = "1"; + private static final long IDEMPOTENT_EXPIRE_DAYS = 1L; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + @Lazy + private ConfigValueUtil configValueUtil; + + @Resource + private CertificateManageApiService certificateManageApiService; + + /** + * 监听组织架构变更消息并同步证照数据。 + */ + @StreamListener(target = CertificateConsumerSource.INPUT, condition = "headers['ROCKET_TAGS'] == 'TAG_ORGANIZE'") + public void receiveStoreFranchiseeChange(Message message) { + AcknowledgmentCallback acknowledgmentCallback = null; + String idempotentKey = null; + try { + MessageHeaders headers = message.getHeaders(); + acknowledgmentCallback = headers.get(IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK, AcknowledgmentCallback.class); + if (acknowledgmentCallback != null) { + acknowledgmentCallback.noAutoAck(); + } + + String payload = message.getPayload(); + if (StrUtil.isBlank(payload)) { + acknowledgeAccept(acknowledgmentCallback); + return; + } + + String rocketKeys = headers.get("ROCKET_KEYS", String.class); + if (StrUtil.isBlank(rocketKeys)) { + log.warn("证照组织变更消息缺少ROCKET_KEYS,跳过处理。payload={}", payload); + acknowledgeAccept(acknowledgmentCallback); + return; + } + + idempotentKey = IDEMPOTENT_KEY_PREFIX + ":" + rocketKeys; + Boolean firstConsume = stringRedisTemplate.opsForValue() + .setIfAbsent(idempotentKey, IDEMPOTENT_VALUE, IDEMPOTENT_EXPIRE_DAYS, TimeUnit.DAYS); + if (!Boolean.TRUE.equals(firstConsume)) { + acknowledgeAccept(acknowledgmentCallback); + return; + } + + List messageList; + try { + messageList = JSONUtil.toList(JSONUtil.parseArray(payload), OrganizeUpdateMessageDTO.class); + } catch (Exception e) { + log.error("证照组织变更消息解析失败,payload={}", payload, e); + throw e; + } + if (CollUtil.isEmpty(messageList)) { + acknowledgeAccept(acknowledgmentCallback); + return; + } + + for (OrganizeUpdateMessageDTO item : messageList) { + processOrganizeChange(item); + } + acknowledgeAccept(acknowledgmentCallback); + } catch (Exception e) { + //todo 异常处理 + String msg = e.getMessage(); + if(msg.equals("组织不存在,无法同步营业执照") || msg.equals("切换租户失败")){ + acknowledgeRequeue(acknowledgmentCallback); + } + if (StrUtil.isNotBlank(idempotentKey)) { + stringRedisTemplate.delete(idempotentKey); + } + log.error("处理证照组织变更消息失败,message={}", JSONUtil.toJsonStr(message), e); + throw e; + } finally { +// TenantDataSourceUtil.clearSwitchDataSource(); + } + } + + /** + * 分发处理组织变更。 + */ + private void processOrganizeChange(OrganizeUpdateMessageDTO item) { + if (item == null) { + return; + } + OperationTypeMessageEnums operationType = item.getOperationTypeEnum(); + OrganizeCategoryMessageEnums category = item.getOrganizeCategoryEnum(); + if (operationType == null || category == null) { + return; + } + if (!OrganizeCategoryMessageEnums.STORE.equals(category) + && !OrganizeCategoryMessageEnums.COMPANY.equals(category)) { + return; + } + + String tenantId = StrUtil.trim(item.getTenantId()); + if (StrUtil.isBlank(tenantId)) { + return; + } + switchTenant(tenantId); + + if (OperationTypeMessageEnums.ADD.equals(operationType)) { + handleAdd(item); + return; + } + if (OperationTypeMessageEnums.UPDATE.equals(operationType)) { + handleUpdate(item); + return; + } + if (OperationTypeMessageEnums.DELETE.equals(operationType)) { + handleDelete(item); + } + } + + /** + * 处理新增。 + * 门店:新增营业执照+食品经营许可证两条缺失证照。 + * 公司:同步新增营业执照。 + */ + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + protected void handleAdd(OrganizeUpdateMessageDTO item) { + if (OrganizeCategoryMessageEnums.STORE.equals(item.getOrganizeCategoryEnum())) { + handleStoreAdd(item.getId()); + return; + } + if (OrganizeCategoryMessageEnums.COMPANY.equals(item.getOrganizeCategoryEnum())) { + CertificateOrganizeBusinessLicenseVO req = buildCompanyLicense(item.getId(), item.getJsonEntity()); + certificateManageApiService.saveBusinessLicense(req); + } + } + + /** + * 处理编辑。 + * 门店:无需处理。 + * 公司:同步更新营业执照。 + */ + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + protected void handleUpdate(OrganizeUpdateMessageDTO item) { + if (OrganizeCategoryMessageEnums.STORE.equals(item.getOrganizeCategoryEnum())) { + return; + } + if (OrganizeCategoryMessageEnums.COMPANY.equals(item.getOrganizeCategoryEnum())) { + CertificateOrganizeBusinessLicenseVO req = buildCompanyLicense(item.getId(), item.getJsonEntity()); + certificateManageApiService.saveBusinessLicense(req); + } + } + + /** + * 处理删除。 + * 按组织ID删除该组织主体下全部证照。 + */ + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + protected void handleDelete(OrganizeUpdateMessageDTO item) { + String organizeId = StrUtil.trim(item.getId()); + if (StrUtil.isBlank(organizeId)) { + return; + } + certificateManageApiService.deleteBusinessLicense(organizeId, null); + } + + /** + * 门店新增时补齐默认缺失证照。 + */ + private void handleStoreAdd(String storeId) { + String targetStoreId = StrUtil.trim(storeId); + if (StrUtil.isBlank(targetStoreId)) { + return; + } + certificateManageApiService.initStoreDefaultCertificates(targetStoreId); + } + + /** + * 构建公司营业执照同步参数,逻辑与 V2OrganizeServiceImpl 的公司新增/编辑保持一致。 + */ + private CertificateOrganizeBusinessLicenseVO buildCompanyLicense(String organizeId, String jsonEntity) { + String targetOrganizeId = StrUtil.trim(organizeId); + ServiceException.isTrue(StrUtil.isNotBlank(targetOrganizeId), "组织ID不能为空"); + + CertificateOrganizeBusinessLicenseVO licenseVO = new CertificateOrganizeBusinessLicenseVO(); + licenseVO.setOrganizeId(targetOrganizeId); + + JSONObject jsonObject = parseJSONEntity(jsonEntity); + + licenseVO.setCertificateImage(jsonObject.getStr("licenseImg")); + licenseVO.setCompanyName(jsonObject.getStr("fullName")); + + JSONObject propertyJson = parsePropertyJson(jsonObject); + + //前端如果没值传的是""空串,直接去获取JsonArry会报错,这里判断一下 + String businessTermText = propertyJson.getStr("businessTerm"); + if(StringUtil.isNotBlank(businessTermText)){ + licenseVO.setBusinessTerm(businessTermText); + + JSONArray businessTerm = propertyJson.getJSONArray("businessTerm"); + if(businessTerm.size() == 2){ + licenseVO.setIssueDate(businessTerm.getStr(0)); + licenseVO.setExpireDate(businessTerm.getStr(1)); + } + } + + String managerName = propertyJson.getStr("managerName"); + if (managerName != null) { + licenseVO.setLegalRepresentative(managerName); + } + String address = propertyJson.getStr("address"); + if (address!= null) { + licenseVO.setCompanyAddress(address); + } + Integer businessTermLong = propertyJson.getInt("businessTermLong"); + if (businessTermLong!= null) { + licenseVO.setIsLongTerm(businessTermLong); + } + + Date establishDate = propertyJson.getDate("foundedTime"); + if(Objects.nonNull(establishDate)){ + licenseVO.setEstablishDate(DateUtil.daFormat(establishDate)); + } + return licenseVO; + } + + private JSONObject parseJSONEntity(String jsonEntity) { + if (StrUtil.isBlank(jsonEntity) || !JSONUtil.isTypeJSONObject(jsonEntity)) { + return new JSONObject(); + } + return JSONUtil.parseObj(jsonEntity); + } + /** + * 从组织消息 jsonEntity 中解析 propertyJson。 + */ + private JSONObject parsePropertyJson(JSONObject jsonEntity) { + if (Objects.isNull(jsonEntity)) { + return new JSONObject(); + } + JSONObject propertyObj = jsonEntity.getJSONObject("propertyJson"); + if(Objects.isNull(propertyObj)){ + return new JSONObject(); + } + return propertyObj; + } + + private Integer toInteger(Object value) { + if (value == null) { + return null; + } + try { + return Integer.parseInt(String.valueOf(value)); + } catch (Exception e) { + return null; + } + } + + /** + * 切换租户数据源。 + */ + private void switchTenant(String tenantId) { + if (!configValueUtil.isMultiTenancy()) { + log.info("证照组织变更消息:配置为非多租户,跳过切换租户。tenantId={}", tenantId); + return; + } + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (Exception e) { + throw new RuntimeException("切换租户失败tenantId:"+tenantId, e); + } + } + + /** + * 手动确认消费成功。 + */ + private void acknowledgeAccept(AcknowledgmentCallback acknowledgmentCallback) { + if (acknowledgmentCallback == null || acknowledgmentCallback.isAcknowledged()) { + return; + } + acknowledgmentCallback.acknowledge(AcknowledgmentCallback.Status.ACCEPT); + } + + /** + * 手动回执重试。 + */ + private void acknowledgeRequeue(AcknowledgmentCallback acknowledgmentCallback) { + if (acknowledgmentCallback == null || acknowledgmentCallback.isAcknowledged()) { + return; + } + acknowledgmentCallback.acknowledge(AcknowledgmentCallback.Status.REQUEUE); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumerSource.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumerSource.java new file mode 100644 index 0000000..c0d2ce7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/consumer/CertificateConsumerSource.java @@ -0,0 +1,17 @@ +package jnpf.certificate.consumer; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +/** + * 证照消息通道定义 + */ +public interface CertificateConsumerSource { + /** + * 消费通道 + */ + String INPUT = "permission-certificate-input"; + + @Input(INPUT) + SubscribableChannel input(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateInstanceController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateInstanceController.java new file mode 100644 index 0000000..bbef846 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateInstanceController.java @@ -0,0 +1,182 @@ +package jnpf.certificate.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.certificate.service.CertificateManageService; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.model.certificate.req.CertificateHealthManageQueryReq; +import jnpf.model.certificate.req.CertificateInstanceQueryReq; +import jnpf.model.certificate.req.CertificateStoreManageQueryReq; +import jnpf.model.certificate.req.CertificateStoreDashboardReq; +import jnpf.model.certificate.req.CertificateSyncHealthReq; +import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHealthCertificateUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq; +import jnpf.model.certificate.vo.CertificateHealthManageVO; +import jnpf.model.certificate.vo.CertificateInstanceVO; +import jnpf.model.certificate.vo.CertificateStoreManageVO; +import jnpf.model.certificate.vo.CertificateStoreDashboardVO; +import jnpf.model.certificate.vo.CertificateStoreCustomStatusTableVO; +import jnpf.model.certificate.vo.CertificateTypeOptionVO; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.util.FtbUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.List; +import java.util.Optional; + +/** + * web证照实例控制器。 + */ +@RestController +@Validated +@RequestMapping("/web/certificate-instance") +public class CertificateInstanceController { + + @Autowired + private CertificateInstanceService certificateInstanceService; + @Autowired + private CertificateManageService certificateManageService; + + /** + * 更新健康证。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-health") + public ActionResult updateHealth(@Validated @RequestBody CertificateAppHealthCertificateUpdateReq req) { + certificateManageService.updateHealthCertificate(req); + return ActionResult.success(); + } + + /** + * 更新营业执照。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-business-license") + public ActionResult updateBusinessLicense(@Validated @RequestBody CertificateAppBusinessLicenseUpdateReq req) { + certificateManageService.updateBusinessLicense(req); + return ActionResult.success(); + } + + /** + * 更新食品经营许可证。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-hygiene-license") + public ActionResult updateHygieneLicense(@Validated @RequestBody CertificateAppHygieneLicenseUpdateReq req) { + certificateManageService.updateHygieneLicense(req); + return ActionResult.success(); + } + + /** + * 更新门店自定义证照。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-store-custom") + public ActionResult updateStoreCustom(@Valid @RequestBody CertificateAppStoreCustomUpdateReq req) { + certificateManageService.updateStoreCustomCertificate(req); + return ActionResult.success(); + } + + /** + * 按ID查询详情。 + * + * @param id 实例ID + * @return 详情 + */ + @GetMapping("/query-info/{id}") + public ActionResult queryInfo(@PathVariable("id") @NotBlank(message = "实例ID不能为空") String id) { + return ActionResult.success(certificateInstanceService.queryInfo(id)); + } + + /** + * 分页查询列表。 + * + * @param req 查询参数 + * @return 分页结果 + */ + @GetMapping("/query-page") + @Deprecated + public ActionResult> queryPage(@Valid CertificateInstanceQueryReq req) { + PageInfo pageInfo = certificateInstanceService.queryPage(req); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 健康证管理分页查询。 + * + * @param req 查询参数 + * @return 分页结果 + */ + @PostMapping("/query-health-page") + public ActionResult> queryHealthPage(@RequestBody @Valid CertificateHealthManageQueryReq req) { + PageInfo pageInfo = certificateInstanceService.queryHealthPage(req); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 门店证照分页查询。 + * + * @param req 查询参数 + * @return 分页结果 + */ + @PostMapping("/query-store-page") + public ActionResult> queryStorePage(@RequestBody @Valid CertificateStoreManageQueryReq req) { + PageInfo pageInfo = certificateInstanceService.queryStorePage(req); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 门店证照看板统计。 + * + * @param req 查询参数 + * @return 看板统计结果 + */ + @PostMapping("/store-dashboard") + public ActionResult storeDashboard(@Validated @RequestBody CertificateStoreDashboardReq req) { + return ActionResult.success(certificateInstanceService.storeCertificateDashboard(req)); + } + + /** + * 门店证照看板表格分页查询。 + * + * @param req 查询参数 + * @return 分页结果 + */ + @PostMapping("/store-dashboard-table-page") + public ActionResult> storeDashboardTablePage(@Validated @RequestBody CertificateStoreDashboardReq req) { + PageInfo pageInfo = certificateInstanceService.storeCertificateDashboardTablePage(req); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 查询证照类型选项。 + * + * @return 证照类型选项 + */ + @GetMapping("/query-certificate-type-list") + public ActionResult> queryCertificateTypeList() { + return ActionResult.success(certificateInstanceService.queryCertificateTypeList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateManageApiController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateManageApiController.java new file mode 100644 index 0000000..812cf3a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateManageApiController.java @@ -0,0 +1,59 @@ +package jnpf.certificate.controller; + +import jnpf.base.ActionResult; +import jnpf.certificate.CertificateManageApi; +import jnpf.certificate.service.CertificateManageApiService; +import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 组织营业执照管理接口。 + */ +@Validated +@RestController +@RequestMapping("/web/certificate-manage-api") +public class CertificateManageApiController implements CertificateManageApi { + + @Autowired + private CertificateManageApiService certificateManageApiService; + + @Override + @GetMapping("/query-business-license") + public ActionResult queryBusinessLicense(@RequestParam("organizeId") String organizeId) { + return ActionResult.success(certificateManageApiService.queryBusinessLicense(organizeId)); + } + + @Override + @PostMapping("/query-business-license-batch") + public ActionResult> queryBusinessLicenseBatch(@RequestBody Collection organizeIds) { + return ActionResult.success(certificateManageApiService.queryBusinessLicenseBatch(organizeIds)); + } + + @Override + @PostMapping("/save-business-license") + public ActionResult saveBusinessLicense(@Valid @RequestBody CertificateOrganizeBusinessLicenseVO req) { + certificateManageApiService.saveBusinessLicense(req); + return ActionResult.success(); + } + + @Override + @DeleteMapping("/delete-business-license") + public ActionResult deleteBusinessLicense(@RequestParam("organizeId") String organizeId, + @RequestParam(value = "loginUserId", required = false) String loginUserId) { + certificateManageApiService.deleteBusinessLicense(organizeId, loginUserId); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateOcrController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateOcrController.java new file mode 100644 index 0000000..a58055b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateOcrController.java @@ -0,0 +1,190 @@ +package jnpf.certificate.controller; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.ocr.v20181119.OcrClient; +import com.tencentcloudapi.ocr.v20181119.models.*; +import jnpf.base.ActionResult; +import jnpf.certificate.config.FoodSafetyOcrConfig; +import jnpf.model.certificate.req.CertificateFoodSafetyOcrReq; +import jnpf.model.certificate.vo.CertificateFoodSafetyOcrVO; +import jnpf.permission.vo.LicenseVo; +import jnpf.personnels.config.TengxunLicenseConfig; +import jnpf.util.JsonUtil; +import jnpf.util.RedisUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 证照 OCR 控制器。 + */ +@RestController +@Validated +@RequestMapping("/web/certificate-ocr") +@Slf4j +public class CertificateOcrController { + + private static final String FOOD_SAFETY_OCR_CACHE_KEY_PREFIX = "ftb:certificate:ocr:food:safety:"; + private static final long FOOD_SAFETY_OCR_CACHE_SECONDS = 24 * 60 * 60L; + + @Autowired + private TengxunLicenseConfig licenseConfig; + @Autowired + private FoodSafetyOcrConfig foodSafetyOcrConfig; + @Autowired + private RedisUtil redisUtil; + + + /** + * 食品安全许可证识别(空实现)。 + * + * @param req 图片地址 + * @return 识别结果 + */ + @PostMapping("/recognize-food-safety-license") + public ActionResult recognizeFoodSafetyLicense(@Validated @RequestBody CertificateFoodSafetyOcrReq req) { + return ActionResult.success(recognizeFoodSafetyLicense(req.getImageUrl()) + .orElse(new CertificateFoodSafetyOcrVO(false))); + } + + private Optional recognizeFoodSafetyLicense(String imageUrl) { + String tenantId = UserProvider.getUser().getTenantId(); + String cacheKey = buildFoodSafetyOcrCacheKey(tenantId,imageUrl); + try { + Object cached = redisUtil.getString(cacheKey); + if (cached != null && StringUtil.isNotBlank(cached.toString())) { + CertificateFoodSafetyOcrVO cacheVo = JsonUtil.getJsonToBean(cached.toString(), CertificateFoodSafetyOcrVO.class); + if (cacheVo != null) { + return Optional.of(cacheVo); + } + } + } catch (Exception e) { + log.warn("read food safety ocr cache error, imageUrl={}", imageUrl, e); + } + + try{ + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 + // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 + // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + Credential cred = new Credential(licenseConfig.getSecretId(), licenseConfig.getSecretKey()); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint(licenseConfig.getDomain()); + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile); + // 实例化一个请求对象,每个接口都会对应一个request对象 + + SmartStructuralOCRRequest smartStructuralOCRRequest = new SmartStructuralOCRRequest(); + smartStructuralOCRRequest.setImageUrl(imageUrl); + SmartStructuralOCRResponse resp = client.SmartStructuralOCR(smartStructuralOCRRequest); + if(Objects.isNull(resp)){ + return Optional.empty(); + } + StructuralItem [] structuralItems = resp.getStructuralItems(); + if(structuralItems == null || structuralItems.length == 0){ + return Optional.empty(); + } + + Set titleNames = foodSafetyOcrConfig.getTitleNames(); + Set issueDateNames = foodSafetyOcrConfig.getIssueDateNames(); + Set expireDateNames = foodSafetyOcrConfig.getExpireDateNames(); + Set businessItemsNames = foodSafetyOcrConfig.getBusinessItemsNames(); + String title = null; + String issueDate = null; + String expireDate = null; + String businessItems = null; + for (StructuralItem structuralItem : structuralItems){ + String name = structuralItem.getName(); + String value = structuralItem.getValue(); + if(titleNames.contains(name)){ + title = value; + }else if(issueDateNames.contains(name)){ + issueDate = value; + } else if (expireDateNames.contains(name)) { + expireDate = value; + } else if (businessItemsNames.contains(name)) { + businessItems = value; + } + } + if(Objects.isNull(title) || Objects.isNull(issueDate) || Objects.isNull(expireDate) || Objects.isNull(businessItems)){ + log.error("ocr fail title,issueDate,expireDate,businessItems is null.structuralItems:{}",JsonUtil.getObjectToString(structuralItems)); + return Optional.empty(); + } + CertificateFoodSafetyOcrVO result = new CertificateFoodSafetyOcrVO(issueDate, expireDate, businessItems,true); + try { + redisUtil.insert(cacheKey, JsonUtil.getObjectToString(result), FOOD_SAFETY_OCR_CACHE_SECONDS); + } catch (Exception e) { + log.warn("write food safety ocr cache error, imageUrl={}", imageUrl, e); + } + return Optional.of(result); + } catch (TencentCloudSDKException e) { + log.error("Tencent ocr error ",e); + } + return Optional.empty(); + } + + private String buildFoodSafetyOcrCacheKey(String tenantId,String imageUrl) { + return FOOD_SAFETY_OCR_CACHE_KEY_PREFIX +tenantId+":"+ DigestUtil.md5Hex(StrUtil.nullToEmpty(imageUrl)); + } + + + public static void main(String[] args) { + String imgUrl = "https://img.cdn1.vip/i/69dda78d48f89_1776134029.webp"; + try{ + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 + // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 + // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + Credential cred = new Credential("AKIDJTbdT7ayRuIAC848D7mKm2Ji5XHua7es", "HOI1iFakDZidu461qaEweHtNfjBY5Rfp"); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint("ocr.tencentcloudapi.com"); + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile); + // 实例化一个请求对象,每个接口都会对应一个request对象 + + EnterpriseLicenseOCRRequest req = new EnterpriseLicenseOCRRequest(); + req.setImageUrl(imgUrl); + // 返回的resp是一个BizLicenseOCRResponse的实例,与请求对象对应 + EnterpriseLicenseOCRResponse resp = client.EnterpriseLicenseOCR(req); + + SmartStructuralOCRV2Request smartStructuralOCRV2Request = new SmartStructuralOCRV2Request(); + smartStructuralOCRV2Request.setImageUrl(imgUrl); + SmartStructuralOCRV2Response smartStructuralOCRV2Response = client.SmartStructuralOCRV2(smartStructuralOCRV2Request); + + SmartStructuralOCRRequest smartStructuralOCRRequest = new SmartStructuralOCRRequest(); + smartStructuralOCRRequest.setImageUrl(imgUrl); + SmartStructuralOCRResponse smartStructuralOCRResponse = client.SmartStructuralOCR(smartStructuralOCRRequest); + System.out.println(resp); + System.out.println(JsonUtil.getObjectToString(smartStructuralOCRResponse.getStructuralItems())); + // 输出json格式的字符串回包 + } catch (TencentCloudSDKException e) { + log.error("Tencent ocr error ",e); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateStoreController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateStoreController.java new file mode 100644 index 0000000..60ccf1c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateStoreController.java @@ -0,0 +1,75 @@ +package jnpf.certificate.controller; + +import io.seata.spring.annotation.GlobalTransactional; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.certificate.service.CertificateStoreService; +import jnpf.exception.HandleException; +import jnpf.model.certificate.req.CertificateStoreSaveReq; +import jnpf.model.certificate.vo.CertificateStoreAndCertificatesVO; +import jnpf.model.certificate.vo.CertificateStoreTabVO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * web门店证照控制器。 + */ +@RestController +@Validated +@RequestMapping("/web/certificate-store") +public class CertificateStoreController { + + /** + * 门店证照服务。 + */ + @Autowired + private CertificateStoreService certificateStoreService; + + /** + * 保存门店及证照数据。 + * 核心参数包含门店信息、健康证、营业执照、食品经营许可证和门店自定义证照集合。 + * + * @param req 保存参数 + * @return 操作结果 + */ + @PostMapping("/save-store-and-certificates") + @GlobalTransactional + public ActionResult saveStoreAndCertificates(@Validated @RequestBody CertificateStoreSaveReq req) { + return ActionResult.success("success",certificateStoreService.saveStoreAndCertificates(req)); + } + + @Operation(summary = "[删除]删除门店和证照") + @DeleteMapping(value = "/{id}") + @GlobalTransactional + public ActionResult deleteStore(@PathVariable("id") String id) throws HandleException { + return ActionResult.success(certificateStoreService.deleteStore(id)); + } + + /** + * 根据门店ID查询门店证照详情。营业执照、食品许可证、门店自定义证照集合。 + * + * @param storeId 门店ID + * @return 门店证照详情 + */ + @GetMapping("/get-store-certificates") + public ActionResult getStoreCertificates(@RequestParam("storeId") + @NotBlank(message = "门店ID不能为空") + String storeId) { + return ActionResult.success(certificateStoreService.getStoreAndCertificates(storeId)); + } + + /** + * 查询门店证照Tab列表。 + * + * @return 门店证照Tab列表 + */ + @GetMapping("/query-store-certificate-tab-list") + public ActionResult> queryStoreCertificateTabList() { + return ActionResult.success(certificateStoreService.queryStoreCertificateTabList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateWarningController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateWarningController.java new file mode 100644 index 0000000..0a0ffd3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/CertificateWarningController.java @@ -0,0 +1,932 @@ +package jnpf.certificate.controller; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.certificate.CertificateWarningApi; +import jnpf.certificate.helper.NoticeHelper; +import jnpf.certificate.helper.OrganizationHelper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.vo.WarningNoticeTargetVO; +import jnpf.model.warningnotice.vo.WarningNoticeUserConfigVO; +import jnpf.model.warningnotice.vo.WarningNoticeVO; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.organzie.OrganizeBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoMapper; +import jnpf.storecertificatephoto.service.WarningNoticeService; +import jnpf.util.CustomTenantUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static jnpf.certificate.helper.NoticeHelper.*; + +/** + * 证照预警接口实现。 + */ +@Slf4j +@RestController +@RequestMapping("/web/certificate-warning-api") +public class CertificateWarningController implements CertificateWarningApi { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_EXPIRED = 2; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_NORMAL = 4; + + private static final int SUBJECT_TYPE_EMPLOYEE = 1; + private static final int DEFAULT_NEAR_EXPIRE_DAYS = 30; + private static final int DEFAULT_NOTICE_FREQUENCY_DAYS = 1; + private static final int STORE_CUSTOM_TEMPLATE_DISABLED_STATUS = 0; + private static final int USER_ADMINISTRATOR_FLAG = 1; + + private static final String NOTICE_USER_TYPE_POSITION = "position"; + private static final String NOTICE_USER_TYPE_PERSONNEL = "personnel"; + private static final String NOTICE_USER_TYPE_PERSON = "person"; + + @Value("${config.certificate.module.id:813679492035805253}") + private String HEALTH_CERTIFICATE_PERMISSION_MODULE_ID; + + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private WarningNoticeService warningNoticeService; + @Autowired + private StoreCertificatePhotoMapper storeCertificatePhotoMapper; + @Autowired + private NoticeHelper noticeHelper; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private UserApi userApi; + @Autowired + private PermissionsUtils permissionsUtils; + @Autowired + private CustomTenantUtil customTenantUtil; + @Autowired + private OrganizationHelper organizationHelper; + + /** + * 检查证照状态并发送临期通知: + * 1. 将正常/临期状态按规则纠正为临期或过期。 + * 2. 向临期对象发送预警消息(健康证通知本人,组织/门店证照通知组织负责人)。 + * + * @param tenantId 租户ID + * @return 处理结果 + */ + @Override + @NoDataSourceBind + @PostMapping("/checkAndSendCertificateWarning") + public ActionResult checkAndSendCertificateWarning(@RequestParam("tenantId") String tenantId) { + try { + log.error("checkAndSendCertificateWarning tenantId:{}",tenantId); + customTenantUtil.checkOutTenant(tenantId); + doCheckAndSend(StrUtil.trim(tenantId)); + return ActionResult.success(Boolean.TRUE); + } catch (Exception e) { + log.error("检查并发送证照预警失败,tenantId={}", tenantId, e); + return ActionResult.success(Boolean.FALSE); + } + } + + /** + * 执行检查与发送。 + */ + private void doCheckAndSend(String tenantId) { + List candidateList = queryCandidateCertificateList(); + log.error("doCheckAndSend candidateList size:{}", CollUtil.isEmpty(candidateList) ? 0 : candidateList.size()); + if (CollUtil.isEmpty(candidateList)) { + return; + } + + Map warningConfigMap = queryWarningConfigMap(); + Map templateMap = queryTemplateMap(candidateList); + Map templateNearExpireDaysMap = buildTemplateNearExpireDaysMap(templateMap); + + List needUpdateList = new ArrayList<>(); + List noticeTaskList = new ArrayList<>(); + for (CertificateInstanceEntity entity : candidateList) { + if (entity == null || StrUtil.isBlank(entity.getId()) || StrUtil.isBlank(entity.getCertificateType())) { + continue; + } + WarningNoticeVO warningNoticeVO = buildWarningNoticeVO(entity, warningConfigMap); + boolean skipByExpiryReminderDays = shouldSkipWarningNoticeByExpiryReminderDays(warningNoticeVO); + boolean skipWarningNotice = isStoreCustomTemplateWarningDisabled(entity, templateMap); + int nearExpireDays = resolveNearExpireDays(entity, warningConfigMap, templateNearExpireDaysMap); + int targetStatus = calculateStatus(entity, nearExpireDays,templateMap); + + if ((targetStatus == STATUS_NEAR_EXPIRE || targetStatus == STATUS_EXPIRED || targetStatus == STATUS_NORMAL) + && !Objects.equals(entity.getStatus(), targetStatus)) { + entity.setStatus(targetStatus); + needUpdateList.add(entity); + } + + int effectiveStatus = (targetStatus == STATUS_NEAR_EXPIRE || targetStatus == STATUS_EXPIRED) + ? targetStatus : (entity.getStatus() == null ? STATUS_MISSING : entity.getStatus()); + if (effectiveStatus != STATUS_NEAR_EXPIRE || skipWarningNotice || skipByExpiryReminderDays) { + continue; + } + Integer daysToExpire = calculateDaysToExpire(entity.getExpireDate()); + if (daysToExpire == null || daysToExpire < 0) { + continue; + } + int noticeFrequencyDays = resolveNoticeFrequencyDays(warningNoticeVO); + if (!shouldSendByFrequency(daysToExpire, noticeFrequencyDays)) { + continue; + } + String templateName = null; + if(isStoreCustomCertificate(entity)){ + StoreCertificatePhotoEntity storeCertificatePhotoEntity = templateMap.get(entity.getTemplateId()); + if(Objects.nonNull(storeCertificatePhotoEntity)){ + templateName = storeCertificatePhotoEntity.getCertificateName(); + } + } + noticeTaskList.add(new NearExpireNoticeTask(entity, daysToExpire, warningNoticeVO,templateName)); + } + + updateCertificateStatusBatch(needUpdateList); + sendNearExpireNoticeBatch(noticeTaskList, tenantId); + } + + private WarningNoticeVO buildWarningNoticeVO(CertificateInstanceEntity entity, Map warningConfigMap) { + String certificateType = entity.getCertificateType(); + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) { + return warningConfigMap.get(entity.getTemplateId()); + } + return warningConfigMap.get(certificateType); + } + + /** + * 查询待检查的证照数据(仅正常、临期)。 + */ + private List queryCandidateCertificateList() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + queryWrapper.eq(CertificateInstanceEntity::getTemplateStatus,1); + queryWrapper.in(CertificateInstanceEntity::getStatus, Arrays.asList(STATUS_NORMAL, STATUS_NEAR_EXPIRE)); + queryWrapper.in(CertificateInstanceEntity::getCertificateType, Arrays.asList( + CertificateTypeEnum.HEALTH_CERTIFICATE.getType(), + CertificateTypeEnum.BUSINESS_LICENSE.getType(), + CertificateTypeEnum.HYGIENE_LICENSE.getType(), + CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType() + )); + queryWrapper.orderByAsc(CertificateInstanceEntity::getCertificateType); + queryWrapper.orderByAsc(CertificateInstanceEntity::getSubjectType); + queryWrapper.orderByAsc(CertificateInstanceEntity::getSubjectId); + return certificateInstanceMapper.selectList(queryWrapper); + } + + /** + * 查询预警设置(健康证、营业执照、食品经营许可证、门店自定义证照)。 + */ + private Map queryWarningConfigMap() { + Map result = new HashMap<>(4); + List warningNoticeVOList = warningNoticeService.queryAll(); + for (WarningNoticeVO warningNoticeVO:warningNoticeVOList){ + result.put(warningNoticeVO.getTypeOrTemplateId(), warningNoticeVO); + } + return result; + } + + /** + * 查询门店自定义证照模板临期天数配置。 + */ + private Map queryTemplateMap(List instanceList) { + List templateIds = instanceList.stream() + .filter(Objects::nonNull) + .filter(entity -> StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) + .map(CertificateInstanceEntity::getTemplateId) + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(templateIds)) { + return Collections.emptyMap(); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(StoreCertificatePhotoEntity::getId, templateIds); + queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + List templateList = storeCertificatePhotoMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(templateList)) { + return Collections.emptyMap(); + } + + Map result = new HashMap<>(templateList.size()); + for (StoreCertificatePhotoEntity template : templateList) { + if (template == null || StrUtil.isBlank(template.getId())) { + continue; + } + result.put(StrUtil.trim(template.getId()), template); + } + return result; + } + + /** + * 构建门店自定义证照模板临期天数映射。 + */ + private Map buildTemplateNearExpireDaysMap(Map templateMap) { + if (CollUtil.isEmpty(templateMap)) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(templateMap.size()); + for (Map.Entry entry : templateMap.entrySet()) { + String templateId = StrUtil.trim(entry.getKey()); + if (StrUtil.isBlank(templateId)) { + continue; + } + StoreCertificatePhotoEntity template = entry.getValue(); + Integer reminderDays = template == null ? null : template.getExpiryReminderDays(); + result.put(templateId, reminderDays == null || reminderDays < 0 ? DEFAULT_NEAR_EXPIRE_DAYS : reminderDays); + } + return result; + } + + /** + * 按证照类型解析临期阈值天数。 + */ + private int resolveNearExpireDays(CertificateInstanceEntity entity, + Map warningConfigMap, + Map templateNearExpireDaysMap) { + if (entity == null || StrUtil.isBlank(entity.getCertificateType())) { + return DEFAULT_NEAR_EXPIRE_DAYS; + } + String certificateType = StrUtil.trim(entity.getCertificateType()); + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) { + String templateId = StrUtil.trim(entity.getTemplateId()); + Integer templateDays = templateNearExpireDaysMap.get(templateId); + return templateDays == null || templateDays < 0 ? DEFAULT_NEAR_EXPIRE_DAYS : templateDays; + } + WarningNoticeVO warningNoticeVO = warningConfigMap.get(certificateType); + Integer reminderDays = warningNoticeVO == null ? null : warningNoticeVO.getExpiryReminderDays(); + return reminderDays == null || reminderDays < 0 ? DEFAULT_NEAR_EXPIRE_DAYS : reminderDays; + } + + /** + * 若是门店自定义证照,且模板状态为0,则不发送预警通知。 + */ + private boolean isStoreCustomTemplateWarningDisabled(CertificateInstanceEntity entity, + Map templateMap) { + if (entity == null || !StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) { + return false; + } + String templateId = StrUtil.trim(entity.getTemplateId()); + if (StrUtil.isBlank(templateId)) { + return false; + } + StoreCertificatePhotoEntity template = templateMap.get(templateId); + return template != null && Integer.valueOf(STORE_CUSTOM_TEMPLATE_DISABLED_STATUS).equals(template.getStatus()); + } + + /** + * 按证照类型解析通知频率天数。 + */ + private int resolveNoticeFrequencyDays(WarningNoticeVO warningNoticeVO) { + Integer frequencyDays = warningNoticeVO == null ? null : warningNoticeVO.getNoticeFrequencyDays(); + return frequencyDays == null || frequencyDays <= 0 ? DEFAULT_NOTICE_FREQUENCY_DAYS : frequencyDays; + } + + /** + * 当预警配置的临期提醒天数小于等于0时,跳过预警通知发送。 + */ + private boolean shouldSkipWarningNoticeByExpiryReminderDays(WarningNoticeVO warningNoticeVO) { + Integer expiryReminderDays = warningNoticeVO == null ? null : warningNoticeVO.getExpiryReminderDays(); + return expiryReminderDays == null || expiryReminderDays <= 0; + } + + /** + * 批量更新证照状态。 + */ + private void updateCertificateStatusBatch(List needUpdateList) { + if (CollUtil.isEmpty(needUpdateList)) { + return; + } + for (CertificateInstanceEntity entity : needUpdateList) { + if (entity == null || StrUtil.isBlank(entity.getId()) || entity.getStatus() == null) { + continue; + } + CertificateInstanceEntity updateEntity = new CertificateInstanceEntity(); + updateEntity.setId(entity.getId()); + updateEntity.setStatus(entity.getStatus()); + certificateInstanceMapper.updateById(updateEntity); + } + } + + /** + * 批量发送临期通知。 + */ + private void sendNearExpireNoticeBatch(List noticeTaskList, String tenantId) { + if (CollUtil.isEmpty(noticeTaskList) || StrUtil.isBlank(tenantId)) { + return; + } + + Pair,Map> organizeMapAndUserOrgMap = queryOrganizeInfoMap(noticeTaskList,tenantId); + Map organizeMap = organizeMapAndUserOrgMap.getLeft(); + Map userBaseInfoByUserId = organizeMapAndUserOrgMap.getRight(); + for (NearExpireNoticeTask task : noticeTaskList) { + if (task == null || task.getEntity() == null) { + continue; + } + CertificateInstanceEntity entity = task.getEntity(); + OrganizeBaseInfoVO organizeBaseInfoVO = organizeMap.get(entity.getSubjectId()); + //组织被禁用,则不推送 + if(!isHealthEmployeeCertificate(entity) && Objects.nonNull(organizeBaseInfoVO) && organizeBaseInfoVO.isDisabled()){ + continue; + } + + //如果不是健康证 + if(!isHealthEmployeeCertificate(entity)){ + List receiverUserIds = resolveReceiverUserIds(entity, organizeMap, task.getWarningNoticeVO(), tenantId); + if(CollUtil.isEmpty(receiverUserIds)){ + continue; + } + NoticeMessage noticeMessage = buildNearExpireMessage(entity, task, organizeMap,false,null); + noticeHelper.sendMessage(receiverUserIds, tenantId, noticeMessage.getTitle(), noticeMessage.getContent(), null, null, null); + continue; + } + + //如果是健康证的通知 + //获取健康证本人 + List receiverUserIds = resolveReceiverUserIds(entity, organizeMap, task.getWarningNoticeVO(), tenantId); + if (CollUtil.isNotEmpty(receiverUserIds)){ + NoticeMessage noticeMessage = buildNearExpireMessage(entity, task, organizeMap,true,null); + noticeHelper.sendMessage(receiverUserIds, tenantId, noticeMessage.getTitle(), noticeMessage.getContent(), BUTTON_NAME_HANDLER, buildJumpHealthCertificateUrl(entity), MP_ID_CERTIFICATE); + } + //获取健康证所属组织的负责人 + OrganizeBaseInfoVO healthEmployeeOrgLeaderReceiver = resolveHealthEmployeeOrgLeaderReceiver(entity, organizeMap, userBaseInfoByUserId); + List otherReceiverUserIds = new ArrayList<>(); + if(Objects.nonNull(healthEmployeeOrgLeaderReceiver)){ + otherReceiverUserIds.add(healthEmployeeOrgLeaderReceiver.getLeaderId()); + } + //获取配置的通知者 + List configUserIds = resolveWarningConfigUserIds(entity, task.getWarningNoticeVO(), tenantId); + if(CollUtil.isNotEmpty(configUserIds)){ + otherReceiverUserIds.addAll(configUserIds); + } + + if (CollUtil.isEmpty(otherReceiverUserIds)) { + continue; + } + + //健康证风险,发给该健康证的组织负责人以及配置的人员 + NoticeMessage healthEmployLeaderNoticeMessage = buildNearExpireMessage(entity, task, organizeMap,false,userBaseInfoByUserId); + if(Objects.isNull(healthEmployLeaderNoticeMessage)){ + continue; + } + noticeHelper.sendMessage(otherReceiverUserIds, tenantId, healthEmployLeaderNoticeMessage.getTitle(), healthEmployLeaderNoticeMessage.getContent(), BUTTON_NAME_HANDLER, buildJumpHealthCertificateUrl(entity), MP_ID_CERTIFICATE); + } + } + + private String buildJumpHealthCertificateUrl(CertificateInstanceEntity entity) { + if(Objects.isNull(entity)){ + return null; + } + String subjectId = entity.getSubjectId(); + if(StringUtil.isBlank(subjectId)){ + return null; + } + return String.format(URL_CERTIFICATE,entity.getId()); + } + + /** + * 左侧 查询组织信息映射(用于拿负责人和组织名称)。 key为组织id,value为基础组织信息。 + * 右侧 如果是健康证还需要查询员工所属组织信息。key为userId,value为该userId的基础信息 + * 包含非员工主体的组织信息,以及健康证员工所属组织的信息。 + */ + private Pair,Map> queryOrganizeInfoMap(List noticeTaskList,String tenantId) { + Set organizeIdSet = new LinkedHashSet<>(); + // 收集健康证员工的 subjectId,用于查询其所属组织 + Set healthEmployeeUserIds = new LinkedHashSet<>(); + for (NearExpireNoticeTask task : noticeTaskList) { + if (task == null || task.getEntity() == null) { + continue; + } + CertificateInstanceEntity entity = task.getEntity(); + if (Integer.valueOf(SUBJECT_TYPE_EMPLOYEE).equals(entity.getSubjectType())) { + if (isHealthEmployeeCertificate(entity) && StrUtil.isNotBlank(entity.getSubjectId())) { + healthEmployeeUserIds.add(StrUtil.trim(entity.getSubjectId())); + } + continue; + } + String organizeId = StrUtil.trim(entity.getSubjectId()); + if (StrUtil.isNotBlank(organizeId)) { + organizeIdSet.add(organizeId); + } + } + + Map userBaseInfoByUserId = Collections.emptyMap(); + // 查询健康证员工所属的组织ID(批量SQL查询) + if (CollUtil.isNotEmpty(healthEmployeeUserIds)) { + userBaseInfoByUserId = organizationHelper.buildUserBaseInfoByUserIds(healthEmployeeUserIds,tenantId); + organizeIdSet.addAll(userBaseInfoByUserId.values() + .stream() + .map(UserBaseInfoVO::getOrganizeId) + .collect(Collectors.toSet())); + } + + if (CollUtil.isEmpty(organizeIdSet)) { + return Pair.of(Collections.emptyMap(),userBaseInfoByUserId); + } + + return Pair.of(organizationHelper.buildBaseOrganizeVO(organizeIdSet,tenantId),userBaseInfoByUserId); + } + + /** + * 解析接收人: + * 1. 健康证通知当前人员 + * 2. 组织/门店证照通知组织负责人。 + */ + private List resolveReceiverUserIds(CertificateInstanceEntity entity, + Map organizeMap, + WarningNoticeVO warningNoticeVO, + String tenantId) { + if (entity == null) { + return Collections.emptyList(); + } + String subjectId = StrUtil.trim(entity.getSubjectId()); + if (StrUtil.isBlank(subjectId)) { + return Collections.emptyList(); + } + + List receiverUserIds = new ArrayList<>(); + if (isHealthEmployeeCertificate(entity)) { + receiverUserIds.add(subjectId); + } else { + addOrganizationLeaderReceiver(entity, organizeMap, receiverUserIds); + } + + if (warningNoticeVO != null && !isHealthEmployeeCertificate(entity)) { + List configUserIds = resolveWarningConfigUserIds(entity, warningNoticeVO, tenantId); + if (CollUtil.isNotEmpty(configUserIds)) { + receiverUserIds.addAll(configUserIds); + } + } + return normalizeIdList(receiverUserIds); + } + + /** + * 非健康证默认通知组织负责人。 + */ + private void addOrganizationLeaderReceiver(CertificateInstanceEntity entity, + Map organizeMap, + List receiverUserIds) { + if (entity == null || CollUtil.isEmpty(organizeMap) || receiverUserIds == null) { + return; + } + String subjectId = StrUtil.trim(entity.getSubjectId()); + if (StrUtil.isBlank(subjectId)) { + return; + } + OrganizeBaseInfoVO organize = organizeMap.get(subjectId); + if (organize == null || StrUtil.isBlank(organize.getLeaderId())) { + return; + } + receiverUserIds.add(StrUtil.trim(organize.getLeaderId())); + } + + /** + * 健康证通知该员工所属组织的负责人。 + */ + private OrganizeBaseInfoVO resolveHealthEmployeeOrgLeaderReceiver(CertificateInstanceEntity entity, + Map organizeMap, + Map userBaseInfoByUserId) { + if (Objects.isNull(entity) || CollUtil.isEmpty(organizeMap)) { + return null; + } + if(!isHealthEmployeeCertificate(entity)){ + return null; + } + String subjectId = StrUtil.trim(entity.getSubjectId()); + UserBaseInfoVO userBaseInfoVO = userBaseInfoByUserId.get(subjectId); + if(Objects.isNull(userBaseInfoVO)){ + log.error("resolveHealthEmployeeOrgLeaderReceiver 用户的基础为空!.subjectId:{}",subjectId); + return null; + } + String orgId = userBaseInfoVO.getOrganizeId(); + if(StrUtil.isBlank(orgId)){ + log.error("resolveHealthEmployeeOrgLeaderReceiver 用户的组织id为空!.subjectId:{}",subjectId); + return null; + } + OrganizeBaseInfoVO organize = organizeMap.get(orgId); + if (organize != null && StrUtil.isNotBlank(organize.getLeaderId())) { + return organize; + } + return null; + } + + /** + * 解析预警配置接收人。 + */ + private List resolveWarningConfigUserIds(CertificateInstanceEntity entity, + WarningNoticeVO warningNoticeVO, + String tenantId) { + if (entity == null || warningNoticeVO == null || StrUtil.isBlank(tenantId)) { + return Collections.emptyList(); + } + List noticeConfigList = warningNoticeVO.getNoticeConfigList(); + if (CollUtil.isEmpty(noticeConfigList)) { + return Collections.emptyList(); + } + + boolean healthCertificate = isHealthEmployeeCertificate(entity); + List receiverUserIds = new ArrayList<>(); + for (WarningNoticeUserConfigVO config : noticeConfigList) { + if (config == null || StrUtil.isBlank(config.getNoticeUserType()) || CollUtil.isEmpty(config.getNoticeUserList())) { + continue; + } + String noticeUserType = StrUtil.trim(config.getNoticeUserType()); + if (StrUtil.equalsAnyIgnoreCase(noticeUserType, NOTICE_USER_TYPE_POSITION)) { + receiverUserIds.addAll(resolvePositionUserIds(config.getNoticeUserList(), entity, tenantId)); + continue; + } + if (StrUtil.equalsAnyIgnoreCase(noticeUserType, NOTICE_USER_TYPE_PERSONNEL, NOTICE_USER_TYPE_PERSON)) { + if (healthCertificate) { + receiverUserIds.addAll(resolveHealthPersonnelConfigUserIds(config.getNoticeUserList(), entity, tenantId)); + } else { + receiverUserIds.addAll(extractTargetIds(config.getNoticeUserList())); + } + } + } + return normalizeIdList(receiverUserIds); + } + + /** + * 按岗位解析通知人。健康证场景下需按数据权限过滤。 + */ + private List resolvePositionUserIds(List noticeUserList, + CertificateInstanceEntity entity, + String tenantId) { + List positionIds = extractTargetIds(noticeUserList); + if (CollUtil.isEmpty(positionIds) || StrUtil.isBlank(tenantId)) { + return Collections.emptyList(); + } + + QueryUserBatchDTO dto = new QueryUserBatchDTO(); + dto.setPositionIds(positionIds); + dto.setTenantId(tenantId); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(dto); + if (userInfoBatch == null || !Integer.valueOf(200).equals(userInfoBatch.getCode()) || CollUtil.isEmpty(userInfoBatch.getData())) { + return Collections.emptyList(); + } + List positionUserIds = normalizeIdList(userInfoBatch.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList())); + + // 健康证场景:按数据权限过滤岗位人员 + if (isHealthEmployeeCertificate(entity) && CollUtil.isNotEmpty(positionUserIds)) { + return filterByDataPermission(positionUserIds, entity, tenantId); + } + return positionUserIds; + } + + /** + * 按数据权限过滤通知人员(健康证场景)。 + */ + private List filterByDataPermission(List candidateUserIds, + CertificateInstanceEntity entity, + String tenantId) { + String subjectId = entity == null ? null : StrUtil.trim(entity.getSubjectId()); + if (StrUtil.isBlank(subjectId) || StrUtil.isBlank(tenantId) || CollUtil.isEmpty(candidateUserIds)) { + return Collections.emptyList(); + } + + List userEntityList = userApi.getUserListNoData(candidateUserIds, tenantId); + if (CollUtil.isEmpty(userEntityList)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (UserEntity userEntity : userEntityList) { + if (userEntity == null || StrUtil.isBlank(userEntity.getId())) { + continue; + } + String userId = StrUtil.trim(userEntity.getId()); + if (Integer.valueOf(USER_ADMINISTRATOR_FLAG).equals(userEntity.getIsAdministrator())) { + result.add(userId); + continue; + } + List dataPermissionUserIds; + try { + dataPermissionUserIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID); + } catch (Exception ex) { + log.warn("query health certificate position user permission failed,userId={},module={}", userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID, ex); + continue; + } + if (dataPermissionUserIds == null) { + result.add(userId); + continue; + } + if (CollUtil.isNotEmpty(dataPermissionUserIds) && dataPermissionUserIds.contains(subjectId)) { + result.add(userId); + } + } + return normalizeIdList(result); + } + + /** + * 健康证-按人员配置时,按数据权限过滤可通知人员。 + */ + private List resolveHealthPersonnelConfigUserIds(List noticeUserList, + CertificateInstanceEntity entity, + String tenantId) { + String subjectId = entity == null ? null : StrUtil.trim(entity.getSubjectId()); + if (StrUtil.isBlank(subjectId) || StrUtil.isBlank(tenantId)) { + return Collections.emptyList(); + } + List targetUserIds = extractTargetIds(noticeUserList); + if (CollUtil.isEmpty(targetUserIds)) { + return Collections.emptyList(); + } + + List userEntityList = userApi.getUserListNoData(targetUserIds, tenantId); + if (CollUtil.isEmpty(userEntityList)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (UserEntity userEntity : userEntityList) { + if (userEntity == null || StrUtil.isBlank(userEntity.getId())) { + continue; + } + String userId = StrUtil.trim(userEntity.getId()); + if (Integer.valueOf(USER_ADMINISTRATOR_FLAG).equals(userEntity.getIsAdministrator())) { + result.add(userId); + continue; + } + List dataPermissionUserIds; + try { + dataPermissionUserIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID); + } catch (Exception ex) { + log.warn("query health certificate user permission failed,userId={},module={}", userId, HEALTH_CERTIFICATE_PERMISSION_MODULE_ID, ex); + continue; + } + if (dataPermissionUserIds == null || CollUtil.isEmpty(dataPermissionUserIds)) { + if (dataPermissionUserIds == null) { + result.add(userId); + } + continue; + } + if (dataPermissionUserIds.contains(subjectId)) { + result.add(userId); + } + } + return normalizeIdList(result); + } + + /** + * 提取配置对象ID。 + */ + private List extractTargetIds(List noticeUserList) { + if (CollUtil.isEmpty(noticeUserList)) { + return Collections.emptyList(); + } + return normalizeIdList(noticeUserList.stream().map(WarningNoticeTargetVO::getId).collect(Collectors.toList())); + } + + /** + * 标准化ID列表。 + */ + private List normalizeIdList(Collection idList) { + if (CollUtil.isEmpty(idList)) { + return Collections.emptyList(); + } + return idList.stream() + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 判断是否健康证+人员主体。 + */ + private boolean isHealthEmployeeCertificate(CertificateInstanceEntity entity) { + return entity != null + && StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.HEALTH_CERTIFICATE.getType()) + && Integer.valueOf(SUBJECT_TYPE_EMPLOYEE).equals(entity.getSubjectType()); + } + + /** + * 判断是否自定义证照 + */ + private boolean isStoreCustomCertificate(CertificateInstanceEntity entity) { + return entity != null + && StrUtil.equalsIgnoreCase(entity.getCertificateType(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()); + } + + /** + * 计算状态。 + */ + private int calculateStatus(CertificateInstanceEntity entity, Integer nearExpireDays,Map templateMap) { + Integer isLongTerm = entity.getIsLongTerm(); + Date expireDate = entity.getExpireDate(); + String certificateType = entity.getCertificateType(); + String templateId = entity.getTemplateId(); + StoreCertificatePhotoEntity storeCertificatePhotoEntity = templateMap.get(templateId); + if(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType().equals(certificateType) &&//自定义证照,如果临期提醒设置为0,则计算为正常 + Objects.nonNull(storeCertificatePhotoEntity) && + Integer.valueOf(0).equals(nearExpireDays)){ + return STATUS_NORMAL; + } + if (Integer.valueOf(1).equals(isLongTerm)) { + return STATUS_NORMAL; + } + if (expireDate == null) { + return STATUS_MISSING; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + if (target.before(today)) { + return STATUS_EXPIRED; + } + int threshold = nearExpireDays == null || nearExpireDays < 0 ? 0 : nearExpireDays; + long daysDiff = DateUtil.betweenDay(today, target, false); + return daysDiff <= threshold ? STATUS_NEAR_EXPIRE : STATUS_NORMAL; + } + + /** + * 计算距离到期天数。 + */ + private Integer calculateDaysToExpire(Date expireDate) { + if (expireDate == null) { + return null; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + return (int) DateUtil.betweenDay(today, target, false); + } + + /** + * 按通知频率判断是否发送。 + */ + private boolean shouldSendByFrequency(Integer daysToExpire, Integer noticeFrequencyDays) { + if (daysToExpire == null || daysToExpire < 0) { + return false; + } + int frequency = noticeFrequencyDays == null || noticeFrequencyDays <= 0 ? DEFAULT_NOTICE_FREQUENCY_DAYS : noticeFrequencyDays; + return frequency <= 1 || daysToExpire % frequency == 0; + } + + /** + * 构建临期通知文案。 + */ + private NoticeMessage buildNearExpireMessage(CertificateInstanceEntity entity, + NearExpireNoticeTask task, + Map organizeMap, + boolean healthCertificateReceiverBySelf, + Map userBaseInfoByUserId) { + Integer daysToExpire = task.getDaysToExpire(); + String certificateName = resolveCertificateName(entity == null ? null : entity.getCertificateType(),task.getCertificateTemplateName()); + int remainDays = daysToExpire == null ? 0 : Math.max(daysToExpire, 0); + + if(isHealthEmployeeCertificate(entity)){ + return buildHealthCertificateNearExpireMessage(entity,healthCertificateReceiverBySelf,userBaseInfoByUserId,remainDays); + } + + String subjectName = "-"; + if (entity != null && StrUtil.isNotBlank(entity.getSubjectId())) { + OrganizeBaseInfoVO organize = organizeMap.get(StrUtil.trim(entity.getSubjectId())); + if (organize != null && StrUtil.isNotBlank(organize.getName())) { + subjectName = StrUtil.trim(organize.getName()); + } + } + String title = subjectName + certificateName + "即将到期,请及时处理"; + String content; + if(remainDays <= 0){ + content = String.format("%s%s今天到期,请及时更新。", subjectName, certificateName); + }else{ + content = String.format("%s%s离到期时间还有%s天,请及时更新。", subjectName, certificateName, remainDays); + } + return new NoticeMessage(title, content); + } + + private NoticeMessage buildHealthCertificateNearExpireMessage(CertificateInstanceEntity entity, + boolean receiverBySelf, + Map userBaseInfoByUserId, + int remainDays) { + if(receiverBySelf){ + return new NoticeMessage("您的健康证存在风险,请及时处理", buildHealthCertificateNearExpireContent(remainDays)); + } + UserBaseInfoVO baseInfoVO = userBaseInfoByUserId.get(entity.getSubjectId()); + if(Objects.isNull(baseInfoVO)){ + log.error("buildNearExpireMessage 通知健康证所属负责人,该健康证的用户名称获取为空。entity:{}",entity); + return null; + } + return new NoticeMessage("您管理组织的健康证存在风险,请及时处理", buildNearExpireContentByNoSelf(baseInfoVO.getUserName(),remainDays)); + } + + private String buildHealthCertificateNearExpireContent(int remainDays) { + if(remainDays <= 0){ + return "您的健康证今天过期,请及时更新健康证。"; + } + return String.format("您的健康证离到期时间还有%s天,请及时更新健康证。", remainDays); + } + + private String buildNearExpireContentByNoSelf(String username, int remainDays) { + if(remainDays <= 0){ + return String.format("%s的健康证今天过期,请及时更新健康证。",username); + } + return String.format("%s的健康证离到期时间还有%s天,请及时更新健康证。",username,remainDays); + } + + /** + * 证照类型转中文名称。 + */ + private String resolveCertificateName(String certificateType,String certificateTemplateName) { + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType())) { + return "健康证"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType())) { + return "营业执照"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HYGIENE_LICENSE.getType())) { + return "食品经营许可证"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) { + return certificateTemplateName; + } + return "证照"; + } + + /** + * 临期通知任务。 + */ + @Data + private static class NearExpireNoticeTask { + private final CertificateInstanceEntity entity; + private final Integer daysToExpire; + private final WarningNoticeVO warningNoticeVO; + private final String certificateTemplateName; + + private NearExpireNoticeTask(CertificateInstanceEntity entity, Integer daysToExpire, WarningNoticeVO warningNoticeVO,String certificateTemplateName) { + this.entity = entity; + this.daysToExpire = daysToExpire; + this.warningNoticeVO = warningNoticeVO; + this.certificateTemplateName = certificateTemplateName; + } + + private CertificateInstanceEntity getEntity() { + return entity; + } + + private Integer getDaysToExpire() { + return daysToExpire; + } + + private WarningNoticeVO getWarningNoticeVO() { + return warningNoticeVO; + } + } + + /** + * 通知文案。 + */ + private static class NoticeMessage { + private final String title; + private final String content; + + private NoticeMessage(String title, String content) { + this.title = title; + this.content = content; + } + + private String getTitle() { + return title; + } + + private String getContent() { + return content; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateManageController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateManageController.java new file mode 100644 index 0000000..465dd29 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateManageController.java @@ -0,0 +1,112 @@ +package jnpf.certificate.controller.app; + +import jnpf.base.ActionResult; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.certificate.service.CertificateManageService; +import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHealthCertificateUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq; +import jnpf.model.certificate.vo.app.CertificateAppCertificateDetailVO; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.certificate.vo.app.HealthCertificateVO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.Optional; + +/** + * App端证照管理控制器。 + */ +@RestController +@Validated +@RequestMapping("/app/certificate-manage") +public class AppCertificateManageController { + + /** + * App端证照管理服务。 + */ + @Autowired + private CertificateManageService certificateManageService; + @Autowired + private CertificateInstanceService certificateInstanceService; + + /** + * 根据证照实例ID查询证照详情。 + * 返回结果中包含certificateType,并按类型返回不同明细对象。 + * + * @param certificateInstanceId 证照实例ID + * @return 证照详情 + */ + @GetMapping("/query-info") + public ActionResult queryInfo(@RequestParam("certificateInstanceId") + @NotBlank(message = "证照实例ID不能为空") + String certificateInstanceId) { + return ActionResult.success(certificateManageService.queryInfo(certificateInstanceId)); + } + + /** + * 更新健康证。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-health") + public ActionResult updateHealth(@Validated @RequestBody CertificateAppHealthCertificateUpdateReq req) { + certificateManageService.updateHealthCertificate(req); + return ActionResult.success(); + } + + /** + * 查询某个人的健康证信息 + * + * @return 健康证信息 + */ + @GetMapping("/query-health-certificate/{userId}") + public ActionResult getHealthCertificateDetail(@PathVariable("userId") String userId) { + Optional healthCertificateDetailVOOptional = certificateInstanceService.getHealthCertificateDetail(userId); + return healthCertificateDetailVOOptional.map(h->ActionResult.success(HealthCertificateVO.of(h))) + .orElseGet(() -> ActionResult.fail(404, "未查询到健康证信息")); + } + + /** + * 更新营业执照。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-business-license") + public ActionResult updateBusinessLicense(@Validated @RequestBody CertificateAppBusinessLicenseUpdateReq req) { + certificateManageService.updateBusinessLicense(req); + return ActionResult.success(); + } + + /** + * 更新食品经营许可证。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-hygiene-license") + public ActionResult updateHygieneLicense(@Validated @RequestBody CertificateAppHygieneLicenseUpdateReq req) { + certificateManageService.updateHygieneLicense(req); + return ActionResult.success(); + } + + /** + * 更新门店自定义证照。 + * + * @param req 更新参数 + * @return 操作结果 + */ + @PutMapping("/update-store-custom") + public ActionResult updateStoreCustom(@Valid @RequestBody CertificateAppStoreCustomUpdateReq req) { + certificateManageService.updateStoreCustomCertificate(req); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateReminderController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateReminderController.java new file mode 100644 index 0000000..50ab53f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateReminderController.java @@ -0,0 +1,83 @@ +package jnpf.certificate.controller.app; + +import jnpf.base.ActionResult; +import jnpf.certificate.service.CertificateAppReminderService; +import jnpf.model.certificate.req.app.CertificateAppBatchRemindReq; +import jnpf.model.certificate.req.app.CertificateAppSingleRemindReq; +import jnpf.util.UserProvider; +import jnpf.util.context.ThreadContext; +import lombok.RequiredArgsConstructor; +import org.redisson.Redisson; +import org.redisson.api.RLock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * App端证照提醒控制器。 + */ +@RestController +@Validated +@RequestMapping("/app/certificate-reminder") +public class AppCertificateReminderController { + + /** + * 证照提醒服务。 + */ + @Autowired + private CertificateAppReminderService certificateAppReminderService; + @Autowired + private ThreadPoolTaskExecutor commonExecutor; + @Autowired + private StringRedisTemplate stringRedisTemplate; + + private static final String REMIND_LOCK_KEY_PREFIX = "certificate-reminder:"; + + /** + * 一键提醒。 + * + * @param req 提醒参数(主体类型) + * @return 操作结果 + */ + @PostMapping("/batch-remind") + public ActionResult batchRemind(@Validated @RequestBody CertificateAppBatchRemindReq req) { + Boolean succ = stringRedisTemplate.opsForValue().setIfAbsent(buildRemindLockKey(),"1", 10, TimeUnit.SECONDS); + if(Objects.isNull(succ) || !succ){ + return ActionResult.fail("操作太快了,请稍后在操作!"); + } + //可能数据过多,但由于内部过滤数据,无法通过单独的线程执行,所以先暂时这样。 + certificateAppReminderService.batchRemind(req); + return ActionResult.success(); + } + + /** + * 单个提醒。 + * + * @param req 提醒参数(证照实例ID) + * @return 操作结果 + */ + @PostMapping("/single-remind") + public ActionResult singleRemind(@Validated @RequestBody CertificateAppSingleRemindReq req) { + Boolean succ = stringRedisTemplate.opsForValue().setIfAbsent(buildRemindLockKey(),"1", 10, TimeUnit.SECONDS); + if(Objects.isNull(succ) || !succ){ + return ActionResult.fail("操作太快了,请稍后在操作!"); + } + certificateAppReminderService.singleRemind(req); + return ActionResult.success(); + } + + private String buildRemindLockKey() { + String userId = UserProvider.getLoginUserId(); + return REMIND_LOCK_KEY_PREFIX + userId; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateRiskController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateRiskController.java new file mode 100644 index 0000000..645fd7f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/controller/app/AppCertificateRiskController.java @@ -0,0 +1,88 @@ +package jnpf.certificate.controller.app; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.certificate.service.CertificateAppRiskService; +import jnpf.model.certificate.req.app.CertificateAppEmployeeRiskQueryReq; +import jnpf.model.certificate.req.app.CertificateAppRiskChartReq; +import jnpf.model.certificate.req.app.CertificateAppStoreRiskQueryReq; +import jnpf.model.certificate.vo.app.CertificateAppEmployeeRiskVO; +import jnpf.model.certificate.vo.app.CertificateAppRiskChartVO; +import jnpf.model.certificate.vo.app.CertificateAppRiskReminderCountVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskVO; +import jnpf.util.FtbUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * App端证照风险管理控制器。 + */ +@RestController +@Validated +@RequestMapping("/app/certificate-risk") +public class AppCertificateRiskController { + + /** + * 证照风险服务。 + */ + @Autowired + private CertificateAppRiskService certificateAppRiskService; + + /** + * 查询风险图表统计。 + * + * @param req 查询参数(组织门店、证照类型) + * @return 风险图表统计数据 + */ + @GetMapping("/query-chart") + public ActionResult queryChart(@Valid CertificateAppRiskChartReq req) { + return ActionResult.success(certificateAppRiskService.queryChart(req)); + } + + /** + * 分页查询员工证照风险列表。 + * + * @param req 查询参数(组织门店、分页) + * @return 员工证照风险分页结果(含分页信息与列表) + */ + @GetMapping("/query-employee-page") + public ActionResult> queryEmployeePage(@Valid CertificateAppEmployeeRiskQueryReq req) { + PageInfo pageInfo = certificateAppRiskService.queryEmployeePage(req); + // 返回员工证照风险分页数据。 + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 分页查询门店证照风险列表。 + * 默认仅查询缺失、过期、临期状态(1、2、3)。 + * + * @param req 查询参数(组织门店、证照类型、分页) + * @return 门店证照风险分页结果 + */ + @GetMapping("/query-store-page") + public ActionResult> queryStorePage(@Valid CertificateAppStoreRiskQueryReq req) { + PageInfo pageInfo = certificateAppRiskService.queryStorePage(req); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 查询风险提醒总数量。 + * 无入参,统计缺失、临期、过期的员工风险数量和组织风险数量。 + * + * @return 风险提醒数量统计 + */ + @GetMapping("/query-reminder-count") + public ActionResult queryReminderCount(@RequestParam(value = "orgId",required = false)String orgId) { + return ActionResult.success(certificateAppRiskService.queryRiskReminderCount(orgId)); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/NoticeHelper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/NoticeHelper.java new file mode 100644 index 0000000..7043ea4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/NoticeHelper.java @@ -0,0 +1,151 @@ +package jnpf.certificate.helper; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.alibaba.nacos.shaded.io.grpc.netty.shaded.io.netty.handler.codec.http.HttpUtil; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.*; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.methods.HttpGet; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * 证照提醒消息发送工具。 + * 参考健康证定时任务的消息结构,实现按用户集合发送提醒。 + */ +@Component +@Slf4j +public class NoticeHelper { + + private static final String APP_NAME = "证照管理"; + public static final String MP_ID_CERTIFICATE = "__UNI__C5F3D72"; + public static final String URL_CERTIFICATE = "/pages/license/detail?id=%s&label=健康证"; + public static final String BUTTON_NAME_HANDLER = "去处理"; + private static final String LOGO_URL = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/665ecb69e4b0ae5df114c87a.png"; + private static final int SEND_BATCH_SIZE = 450; + + @Resource + private ImRobotApi imRobotApi; + + /** + * 发送机器人提醒消息。 + * + * @param userIds 接收用户ID集合 + * @param tenantId 租户ID + * @param title 消息标题 + * @param content 消息内容 + * @param buttonName 跳转按钮名称(可为空) + * @param url 跳转地址(可为空) + * @param mpId 小程序ID(可为空) + */ + public void sendMessage(Collection userIds, String tenantId, String title, String content, + String buttonName, String url, String mpId) { + List targetUserIds = normalizeUserIds(userIds); + if (CollUtil.isEmpty(targetUserIds) || StrUtil.isBlank(tenantId) || StrUtil.isBlank(title) || StrUtil.isBlank(content)) { + return; + } + + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + if (StrUtil.isNotBlank(mpId)) { + form.setMessageAttributionRobotMpId(mpId); + } + form.setRobotNoticeDataForm(buildNoticeData(title, content, buttonName, url, mpId)); + + for (int start = 0; start < targetUserIds.size(); start += SEND_BATCH_SIZE) { + int end = Math.min(start + SEND_BATCH_SIZE, targetUserIds.size()); + form.setToUserIds(targetUserIds.subList(start, end)); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + if (actionResult == null || !Integer.valueOf(200).equals(actionResult.getCode())) { + log.error("证照提醒消息发送失败,req={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + } + } + + /** + * 构建消息体。 + */ + private SendRobotNoticeDataForm buildNoticeData(String title, String content, String buttonName, String url, String mpId) { + SendRobotNoticeDataForm noticeData = new SendRobotNoticeDataForm(); + noticeData.setLogo(LOGO_URL); + noticeData.setAppName(APP_NAME); + noticeData.setTitle(title); + noticeData.setContent(content); + + if (StrUtil.isNotBlank(buttonName) && StrUtil.isNotBlank(url) && StrUtil.isNotBlank(mpId)) { + JumpUrlListModel jumpUrl = new JumpUrlListModel(); + jumpUrl.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jumpUrl.setButtonName(buttonName); + jumpUrl.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jumpUrl.setType(1); + jumpUrl.setUrl(url); + jumpUrl.setMpId(mpId); + //fixme 有空改为传参,不这样处理 + if(url.contains("?")){ + jumpUrl.setMiniAppUrl(buildMiniAppUrl(mpId,url)); + } + LinkedList jumpUrlList = new LinkedList<>(); + jumpUrlList.add(jumpUrl); + noticeData.setJumpUrlList(jumpUrlList); + } + return noticeData; + } + + private String buildUrl(String url) { + if(!url.contains("?")){ + return url; + } + return url.substring(0,url.indexOf("?")); + } + + private MiniAppUrl buildMiniAppUrl(String mpId,String url) { + String params = url.substring(url.indexOf("?")+1); + if(StringUtil.isBlank(params)){ + return null; + } + JSONObject mpParam = new JSONObject(); + for (String kv:params.split("&")){ + String [] kvs = kv.split("="); + if(kvs.length != 2){ + return null; + } + mpParam.set(kvs[0],kvs[1]); + } + return MiniAppUrl.builder() + .mpId(mpId) + .mpPage(buildUrl(url)) + .mpParam(mpParam.toString()) + .build(); + } + + /** + * 规整接收用户集合:去空、去重、去首尾空格。 + */ + private List normalizeUserIds(Collection userIds) { + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + Set userIdSet = new LinkedHashSet<>(); + for (String userId : userIds) { + String trimUserId = StrUtil.trim(userId); + if (StrUtil.isNotBlank(trimUserId)) { + userIdSet.add(trimUserId); + } + } + return new ArrayList<>(userIdSet); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/OrganizationHelper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/OrganizationHelper.java new file mode 100644 index 0000000..87a38ac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/helper/OrganizationHelper.java @@ -0,0 +1,243 @@ +package jnpf.certificate.helper; + +import cn.hutool.core.collection.CollectionUtil; +import jnpf.base.ActionResult; +import jnpf.permission.StoreApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.organzie.BaseOrganizeQueryDTO; +import jnpf.permission.dto.v2.user.QueryUserOrgRelationDTO; +import jnpf.permission.dto.v2.user.QueryUserBaseRelationDTO; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.store.StoreInfoDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeBaseInfoVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserBaseRelationSimpleVO; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class OrganizationHelper { + @Autowired + private V2OrganizeApi v2OrganizeApi; + @Autowired + private V2PositionApi v2PositionApi; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private StoreApi storeApi; + + public List getUserIdsByOrgId(String orgId){ + if(StringUtil.isBlank(orgId)){ + return Collections.emptyList(); + } + ActionResult> actionResult = v2UserApi.listUserIdsByOrganizeId(orgId, UserProvider.getUser().getTenantId()); + return getDataByResult(actionResult,"v2UserApi.listUserIdsByOrganizeId",orgId) + .orElse(Collections.emptyList()); + } + + /** + * 根据多个userId查询多个用户信息 + * @return + */ + public Map getUserBaseRelationMapByOrgId(String orgId){ + if(StringUtil.isBlank(orgId)){ + return Collections.emptyMap(); + } + QueryUserBaseRelationDTO dto = new QueryUserBaseRelationDTO(); + dto.setTenantId(UserProvider.getUser().getTenantId()); + dto.setOrgIds(Collections.singletonList(orgId)); + return getUserBaseRelationMapByUserIds(dto); + } + + public Map getUserBaseRelationMapByUserIds(Collection userIds){ + if(CollectionUtil.isEmpty(userIds)){ + return Collections.emptyMap(); + } + QueryUserBaseRelationDTO dto = new QueryUserBaseRelationDTO(); + dto.setTenantId(UserProvider.getUser().getTenantId()); + dto.setUserIds(userIds); + return getUserBaseRelationMapByUserIds(dto); + } + + public Map getUserBaseRelationMapByUserIds(QueryUserBaseRelationDTO dto){ + if(Objects.isNull(dto)){ + return Collections.emptyMap(); + } + dto.setTenantId(UserProvider.getUser().getTenantId()); + ActionResult> actionResult = v2UserApi.queryUserBaseRelationList(dto); + Optional> listOptional = getDataByResult(actionResult,"v2UserApi.queryUserBaseRelationList",dto); + return listOptional.map(l->l.stream() + .filter(item -> StringUtil.isNotBlank(item.getUserId())) + .collect(Collectors.toMap(UserBaseRelationSimpleVO::getUserId, item -> item, (v1, v2) -> v1))) + .orElse(Collections.emptyMap()); + } + + public Map getUserPrimaryBoundBatch(Collection userIds) { + if(CollectionUtil.isEmpty(userIds)){ + return Collections.emptyMap(); + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(new ArrayList<>(userIds),UserProvider.getUser().getTenantId()); + Optional> listOptional = getDataByResult(actionResult,"v2UserApi.getUserPrimaryBoundBatch",userIds); + return listOptional + .map(l->l.stream() + .collect(Collectors.toMap(UserBoundVO::getId, item -> item, (v1, v2) -> v1))) + .orElse(Collections.emptyMap()); + } + + public Map getAllUserInfoBatch(Collection userIds){ + if(CollectionUtil.isEmpty(userIds)){ + return Collections.emptyMap(); + } + ActionResult> actionResult = v2UserApi.getAllUserInfoBatch(new ArrayList<>(userIds),UserProvider.getUser().getTenantId()); + Optional> listOptional = getDataByResult(actionResult,"v2UserApi.getAllUserInfoBatch",userIds); + return listOptional.map(l->l.stream() + .collect(Collectors.toMap(UserBoundVO::getId,u->u))) + .orElse(Collections.emptyMap()); + } + + public Map> buildUserBoundVOs(Collection orgIds, + boolean hasChild, + List notUserWorkStatusEnumsList, + String tenantId) { + if(CollectionUtil.isEmpty(orgIds)){ + return Collections.emptyMap(); + } + ActionResult> actionResult = v2UserApi.listTargetOrganizesOrHaveChild(new ArrayList<>(orgIds),hasChild,notUserWorkStatusEnumsList,tenantId); + + Optional> listOptional = getDataByResult(actionResult,"v2UserApi.listTargetOrganizesOrHaveChild",orgIds,hasChild,notUserWorkStatusEnumsList,tenantId); + return listOptional.map(l->l.stream() + .collect(Collectors.groupingBy(UserBoundVO::getOrganizeId))) + .orElse(Collections.emptyMap()); + } + + public Map buildPositionBaseInfos(Collection positionIds) { + if(CollectionUtil.isEmpty(positionIds)){ + return Collections.emptyMap(); + } + String tenantId = UserProvider.getUser().getTenantId(); + ActionResult> actionResult = v2PositionApi.listPositionBaseInfoByIds(new ArrayList<>(positionIds), tenantId); + + Optional> positionBaseInfoVOS = getDataByResult(actionResult,"v2PositionApi.listPositionBaseInfoByIds",positionIds); + return positionBaseInfoVOS.map(l->l.stream() + .collect(Collectors.toMap(PositionBaseInfoVO::getId, item -> item, (v1, v2) -> v1))) + .orElse(Collections.emptyMap()); + } + + public Map buildOrganizeGenerals(Collection orgIds) { + if(CollectionUtil.isEmpty(orgIds)){ + return Collections.emptyMap(); + } + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(new ArrayList<>(orgIds)); + + Optional> listOptional = getDataByResult(listActionResult,"v2OrganizeApi.organizesByOrganizeIds",orgIds); + return listOptional.map(l->l.stream() + .collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item, (v1, v2) -> v1))) + .orElse(Collections.emptyMap()); + } + + public Map> buildStoreInfoDetailsByFranchiseeId(String ...franchiseeId) { + ActionResult> listActionResult = storeApi.listInfoDetailsByFranchiseeIds(List.of(franchiseeId)); + + Optional> listOptional = getDataByResult(listActionResult,"storeApi.listInfoDetailsByFranchiseeIds", (Object) franchiseeId); + return listOptional.map(storeInfoDetailVOS -> storeInfoDetailVOS.stream() + .collect(Collectors.groupingBy(StoreInfoDetailVO::getFranchiseeId))) + .orElse(Collections.emptyMap()); + } + + public Map> buildUserIdsByOrgIds(Collection orgIds,boolean orgIdsEmptyQueryAll,String storeTag){ + if(CollectionUtil.isEmpty(orgIds) && !orgIdsEmptyQueryAll){ + return Collections.emptyMap(); + } + + QueryUserOrgRelationDTO queryUserOrgRelationDTO = new QueryUserOrgRelationDTO(orgIdsEmptyQueryAll,orgIds,UserProvider.getUser().getTenantId(),storeTag); + ActionResult>> actionResult = v2UserApi.listUserIdsByQueryDTO(queryUserOrgRelationDTO); + return getDataByResult(actionResult,"v2UserApi.listUserIdsByQueryDTO",queryUserOrgRelationDTO) + .orElse(Collections.emptyMap()); + } + + /** + * 根据用户id获取组织id。key为userId,value为组织id + * @param userIds + * @return + */ + public Map buildOrgIdsByUserIds(Collection userIds,String tenantId){ + if(CollectionUtil.isEmpty(userIds)){ + return Collections.emptyMap(); + } + ActionResult> actionResult = v2UserApi.listOrganizeIdsByUserIds(userIds,tenantId); + return getDataByResult(actionResult,"v2UserApi.listOrganizeIdsByUserIds",userIds) + .orElse(Collections.emptyMap()); + } + + + /** + * 根据用户id获取用户基础信息,用户id,名称,组织id + * @param userIds + * @return + */ + public Map buildUserBaseInfoByUserIds(Collection userIds, String tenantId){ + if(CollectionUtil.isEmpty(userIds)){ + return Collections.emptyMap(); + } + ActionResult> actionResult = v2UserApi.listUserBaseInfoByUserIds(userIds,tenantId); + return getDataByResult(actionResult,"v2UserApi.listUserBaseInfoByUserIds",userIds,tenantId) + .orElse(Collections.emptyList()) + .stream() + .collect(Collectors.toMap(UserBaseInfoVO::getUserId, item -> item)); + } + + public Map buildBaseOrganizeVO(Collection orgIds,String tenantId) { + if(CollectionUtil.isEmpty(orgIds)){ + return Collections.emptyMap(); + } + ActionResult> listActionResult = v2OrganizeApi.baseOrganizesByOrganizeIds(orgIds,tenantId); + + Optional> listOptional = getDataByResult(listActionResult,"v2OrganizeApi.baseOrganizesByOrganizeIds",orgIds); + return listOptional.map(l->l.stream() + .collect(Collectors.toMap(OrganizeBaseInfoVO::getId, item -> item))) + .orElse(Collections.emptyMap()); + } + + + public Map buildBaseOrganizeVO(Collection orgIds,String storeTag,String tenantId) { + if(CollectionUtil.isEmpty(orgIds) && StringUtil.isBlank(storeTag)){ + return Collections.emptyMap(); + } + BaseOrganizeQueryDTO dto = new BaseOrganizeQueryDTO(); + dto.setOrganizeIds(orgIds); + dto.setStoreTag(storeTag); + dto.setTenantId(tenantId); + dto.setQueryParentName(true); + ActionResult> listActionResult = v2OrganizeApi.baseOrganizesByQueryDTO(dto); + + Optional> listOptional = getDataByResult(listActionResult,"v2OrganizeApi.baseOrganizesByOrganizeIds",orgIds); + return listOptional.map(l->l.stream() + .collect(Collectors.toMap(OrganizeBaseInfoVO::getId, item -> item))) + .orElse(Collections.emptyMap()); + } + + private Optional getDataByResult(ActionResult actionResult,String url,Object...params) { + if(Objects.isNull(actionResult)){ + log.error("{} return null.params:{}",url,params); + return Optional.empty(); + } + if(actionResult.getCode() != 200){ + log.error("{} return code is not 200.actionResult:{},params:{}",url,actionResult,params); + return Optional.empty(); + } + return Optional.of(actionResult.getData()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppReminderMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppReminderMapper.java new file mode 100644 index 0000000..ab21679 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppReminderMapper.java @@ -0,0 +1,14 @@ +package jnpf.certificate.mapper; + +import jnpf.certificate.model.CertificateAppReminderRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * App端证照提醒Mapper。 + */ +public interface CertificateAppReminderMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppRiskMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppRiskMapper.java new file mode 100644 index 0000000..5fe541e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateAppRiskMapper.java @@ -0,0 +1,40 @@ +package jnpf.certificate.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.DataScope; +import com.fantaibao.permission.enums.FilterWhereTypeEnum; +import jnpf.model.certificate.req.app.CertificateAppEmployeeRiskQueryReq; +import jnpf.model.certificate.req.app.CertificateAppRiskChartReq; +import jnpf.model.certificate.req.app.CertificateAppStoreRiskQueryReq; +import jnpf.model.certificate.vo.app.CertificateAppEmployeeRiskVO; +import jnpf.model.certificate.vo.app.CertificateAppStatusCountVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskItemVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +public interface CertificateAppRiskMapper { + + @DataScope(tableField = "F_SubjectId",tableAlias = "ci") + List queryHealthRiskStatusCount(@Param("req") CertificateAppRiskChartReq req); + @DataScope(tableFieldOrg = "F_SubjectId",tableAlias = "ci",type = FilterWhereTypeEnum.ORGANIZATION_FILTER) + List queryStoreRiskStatusCount(@Param("req") CertificateAppRiskChartReq req); + + @DataScope(tableField = "F_SubjectId",tableAlias = "ci") + Page queryEmployeeRiskPageByUserIds(@Param("page") Page page, + @Param("userIds") Collection userIds); + @DataScope(tableFieldOrg = "F_SubjectId",tableAlias = "ci",type = FilterWhereTypeEnum.ORGANIZATION_FILTER) + Page queryStoreRiskPage(@Param("page") Page page, + @Param("req") CertificateAppStoreRiskQueryReq req); + + List queryStoreRiskDetailList(@Param("subjectIds") Collection subjectIds); + + @DataScope(tableField = "F_SubjectId",tableAlias = "ci") + Long queryEmployeeRiskTotalCount(@Param("userIds") Collection userIds); + + @DataScope(tableFieldOrg = "F_SubjectId",tableAlias = "ci",type = FilterWhereTypeEnum.ORGANIZATION_FILTER) + Long queryOrganizationRiskTotalCount(@Param("orgIds") Collection orgIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateBusinessLicenseExtMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateBusinessLicenseExtMapper.java new file mode 100644 index 0000000..80041a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateBusinessLicenseExtMapper.java @@ -0,0 +1,11 @@ +package jnpf.certificate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.certificate.po.CertificateBusinessLicenseExtEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 营业执照扩展Mapper。 + */ +public interface CertificateBusinessLicenseExtMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateHygieneLicenseExtMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateHygieneLicenseExtMapper.java new file mode 100644 index 0000000..2e24a9f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateHygieneLicenseExtMapper.java @@ -0,0 +1,11 @@ +package jnpf.certificate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.certificate.po.CertificateHygieneLicenseExtEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 食品许可证扩展Mapper。 + */ +public interface CertificateHygieneLicenseExtMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceItemMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceItemMapper.java new file mode 100644 index 0000000..24cf80f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceItemMapper.java @@ -0,0 +1,11 @@ +package jnpf.certificate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.certificate.po.CertificateInstanceItemEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 证照实例明细Mapper。 + */ +public interface CertificateInstanceItemMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceMapper.java new file mode 100644 index 0000000..90be738 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/mapper/CertificateInstanceMapper.java @@ -0,0 +1,115 @@ +package jnpf.certificate.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.DataScope; +import com.fantaibao.permission.enums.FilterWhereTypeEnum; +import jnpf.base.mapper.SuperMapper; +import jnpf.model.certificate.dto.EmployeeOrganizeDTO; +import jnpf.model.certificate.dto.HealthCertificateStatusDTO; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.req.CertificateHealthManageQueryReq; +import jnpf.model.certificate.req.CertificateStoreManageQueryReq; +import jnpf.model.certificate.vo.CertificateHealthDashboardStatusStatVO; +import jnpf.model.certificate.vo.CertificateHealthManageVO; +import jnpf.model.certificate.vo.CertificateStoreCustomStatusTableVO; +import jnpf.model.certificate.vo.CertificateStoreManageVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 证照实例Mapper。 + */ +public interface CertificateInstanceMapper extends SuperMapper { + + /** + * 健康证管理分页查询。 + * + * @param page 分页参数 + * @param req 查询参数 + * @param nearExpireDays 临期天数阈值 + * @return 分页结果 + */ + @DataScope(tableAlias = "fci",tableField = "F_SubjectId") + Page queryHealthManagePage(@Param("page") Page page, + @Param("req") CertificateHealthManageQueryReq req, + @Param("nearExpireDays") Integer nearExpireDays); + + @DataScope(tableAlias = "fci",tableField = "F_SubjectId") + List queryHealthCertificateStatus(@Param("userIds") Collection userIds); + + /** + * 非健康证看板按状态直接聚合统计(不按组织分组) + * + * @param certificateType 证照类型 + * @param templateId 模板ID(仅门店自定义证照场景) + * @param storeIds 门店IDs筛选 + * @param storeType 门店类型筛选 + * @return 状态聚合结果 + */ + @DataScope(tableAlias = "fci", type = FilterWhereTypeEnum.STORE_FILTER, tableFieldOrg = "F_SubjectId") + List queryStoreDashboardStatusStats(@Param("certificateType") String certificateType, + @Param("templateId") String templateId, + @Param("storeIds") List storeIds, + @Param("storeType") String storeType); + + /** + * 非健康证看板表格分页查询(按门店聚合)。 + * + * @param page 分页参数 + * @param certificateType 证照类型 + * @param templateId 模板ID(仅门店自定义证照场景) + * @param storeIds 门店IDs筛选 + * @param storeType 门店类型筛选 + * @return 门店表格分页数据 + */ + @DataScope(tableAlias = "fci", type = FilterWhereTypeEnum.ORGANIZATION_FILTER, tableFieldOrg = "F_SubjectId") + Page queryStoreDashboardTablePage(@Param("page") Page page, + @Param("certificateType") String certificateType, + @Param("templateId") String templateId, + @Param("storeIds") List storeIds, + @Param("storeType") String storeType); + + /** + * 批量初始化员工健康证实例(仅插入不存在的数据)。 + * + * @param entities 员工用户ID集合 + * @return 插入条数 + */ + int batchInitHealthCertificate(@Param("entities") Collection entities); + + /** + * 根据id查询证照实例 + * @param id + * @return + */ + CertificateInstanceEntity selectInstanceById(@Param("id") String id); + + /** + * 根据多个subjectType和多个status查询证照实例 + * @param subjectTypes + * @param statuses + * @return + */ + @DataScope(tableAlias = "fci",tableField = "F_SubjectId",type = FilterWhereTypeEnum.USER_FILTER) + List selectBySubjectTypesAndStatusesFilterUser(@Param("subjectTypes") Collection subjectTypes, + @Param("statuses") Collection statuses, + @Param("userIds") Collection userIds); + + /** + * 根据多个subjectType和多个status查询证照实例 + * @param subjectTypes + * @param statuses + * @return + */ + @DataScope(tableAlias = "fci",tableFieldOrg = "F_SubjectId",type = FilterWhereTypeEnum.ORGANIZATION_FILTER) + List selectBySubjectTypesAndStatusesFilterOrganization(@Param("subjectTypes") Collection subjectTypes, + @Param("statuses") Collection statuses, + @Param("orgId") String orgId); + + @DataScope(tableAlias = "fci",tableFieldOrg = "F_SubjectId",type = FilterWhereTypeEnum.ORGANIZATION_FILTER) + Page selectCertificateStoreManageVOPage(@Param("page") Page page, + @Param("queryReq")CertificateStoreManageQueryReq queryReq); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/model/CertificateAppReminderRecord.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/model/CertificateAppReminderRecord.java new file mode 100644 index 0000000..69e563a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/model/CertificateAppReminderRecord.java @@ -0,0 +1,57 @@ +package jnpf.certificate.model; + +import lombok.Data; + +import java.util.Date; + +/** + * App证照提醒查询结果模型。 + */ +@Data +public class CertificateAppReminderRecord { + + /** + * 证照实例ID。 + */ + private String certificateInstanceId; + + /** + * 证照类型。 + */ + private String certificateType; + + /** + * 主体类型:1-员工,2-组织,3-门店。 + */ + private Integer subjectType; + + /** + * 主体ID。 + */ + private String subjectId; + + /** + * 证照状态:1-缺失,2-过期,3-临期,4-正常。 + */ + private Integer status; + + /** + * 到期日期。 + */ + private Date expireDate; + + /** + * 接收提醒的用户ID。 + */ + private String receiverUserId; + + /** + * 主体名称(组织/门店名称)。 + */ + private String subjectName; + + /** + * 组织权限匹配ID。 + */ + private String orgPermissionId; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppReminderService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppReminderService.java new file mode 100644 index 0000000..34d7241 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppReminderService.java @@ -0,0 +1,24 @@ +package jnpf.certificate.service; + +import jnpf.model.certificate.req.app.CertificateAppBatchRemindReq; +import jnpf.model.certificate.req.app.CertificateAppSingleRemindReq; + +/** + * App端证照提醒服务。 + */ +public interface CertificateAppReminderService { + + /** + * 一键提醒。 + * + * @param req 提醒参数 + */ + void batchRemind(CertificateAppBatchRemindReq req); + + /** + * 单个提醒。 + * + * @param req 提醒参数 + */ + void singleRemind(CertificateAppSingleRemindReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppRiskService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppRiskService.java new file mode 100644 index 0000000..bfe21bf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateAppRiskService.java @@ -0,0 +1,47 @@ +package jnpf.certificate.service; + +import com.github.pagehelper.PageInfo; +import jnpf.model.certificate.req.app.CertificateAppEmployeeRiskQueryReq; +import jnpf.model.certificate.req.app.CertificateAppRiskChartReq; +import jnpf.model.certificate.req.app.CertificateAppStoreRiskQueryReq; +import jnpf.model.certificate.vo.app.CertificateAppEmployeeRiskVO; +import jnpf.model.certificate.vo.app.CertificateAppRiskChartVO; +import jnpf.model.certificate.vo.app.CertificateAppRiskReminderCountVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskVO; + +/** + * App 端证照风险服务。 + */ +public interface CertificateAppRiskService { + + /** + * 查询证照风险图表统计。 + * + * @param req 查询参数(组织门店、证照类型) + * @return 图表统计结果 + */ + CertificateAppRiskChartVO queryChart(CertificateAppRiskChartReq req); + + /** + * 分页查询员工证照风险列表(缺失、临期、过期)。 + * + * @param req 查询参数(组织门店、分页) + * @return 分页结果 + */ + PageInfo queryEmployeePage(CertificateAppEmployeeRiskQueryReq req); + + /** + * 分页查询门店证照风险列表(缺失、临期、过期)。 + * + * @param req 查询参数(组织门店、证照类型、状态、分页) + * @return 分页结果 + */ + PageInfo queryStorePage(CertificateAppStoreRiskQueryReq req); + + /** + * 查询风险提醒总数量(员工+组织)。 + * + * @return 风险提醒数量统计 + */ + CertificateAppRiskReminderCountVO queryRiskReminderCount(String orgId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateInstanceService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateInstanceService.java new file mode 100644 index 0000000..46af1be --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateInstanceService.java @@ -0,0 +1,181 @@ +package jnpf.certificate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.req.CertificateInstanceAddReq; +import jnpf.model.certificate.req.CertificateHealthManageQueryReq; +import jnpf.model.certificate.req.CertificateInstanceQueryReq; +import jnpf.model.certificate.req.CertificateStoreManageQueryReq; +import jnpf.model.certificate.req.CertificateStoreDashboardReq; +import jnpf.model.certificate.req.CertificateSyncHealthReq; +import jnpf.model.certificate.req.CertificateInstanceUpdateReq; +import jnpf.model.certificate.vo.CertificateHealthManageVO; +import jnpf.model.certificate.vo.CertificateInstanceVO; +import jnpf.model.certificate.vo.CertificateTypeOptionVO; +import jnpf.model.certificate.vo.CertificateStoreManageVO; +import jnpf.model.certificate.vo.CertificateStoreDashboardVO; +import jnpf.model.certificate.vo.CertificateStoreCustomStatusTableVO; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 证照实例服务。 + */ +public interface CertificateInstanceService extends IService { + + /** + * 新增证照实例。 + * + * @param req 请求参数 + */ + void add(CertificateInstanceAddReq req); + + /** + * 更新证照实例。 + * + * @param req 请求参数 + */ + void update(CertificateInstanceUpdateReq req); + + /** + * 按ID查询详情。 + * + * @param id 实例ID + * @return 详情 + */ + CertificateInstanceVO queryInfo(String id); + + /** + * 分页查询列表。 + * + * @param req 查询参数 + * @return 分页结果 + */ + PageInfo queryPage(CertificateInstanceQueryReq req); + + /** + * 健康证管理分页查询。 + * + * @param req 查询参数 + * @return 分页结果 + */ + PageInfo queryHealthPage(CertificateHealthManageQueryReq req); + + /** + * 门店证照分页查询。 + * + * @param req 查询参数 + * @return 分页结果 + */ + PageInfo queryStorePage(CertificateStoreManageQueryReq req); + + /** + * 逻辑删除。 + * + * @param id 实例ID + */ + void delete(String id); + + /** + * 保存用户的健康证信息 + * @param userId + * @param healthCertificate + * @param startHealthDate + * @param endHealthDate + */ + void saveHealthCertificate(String userId, String healthCertificate, String startHealthDate, String endHealthDate); + + /** + * 删除员工健康证 + * @param userId + */ + void deleteHealthCertificate(String userId); + + /** + * 根据用户id查询健康证 + * @param userId + * @return + */ + Optional getHealthCertificateDetail(String userId); + + /** + * 根据用户id查询健康证 + * @param userIds + * @return + */ + List getHealthCertificateDetails(Collection userIds); + + /** + * 根据用户id查询健康证 + * @param userIds + * @return + */ + default Map getHealthCertificateDetailMap(Collection userIds){ + return getHealthCertificateDetails(userIds) + .stream() + .collect(Collectors.toMap(HealthCertificateDetailVO::getUserId, v -> v)); + } + + /** + * 根据状态,证照类型 查询用户id列表 + * @param status + * @return + */ + List getUserIdsByStatus(Integer status,String certificateType); + + /** + * 查询健康证用户id列表 + * @param status + * @return + */ + default List getHealthCertificateUserIdsByStatus(Integer status){ + return getUserIdsByStatus(status, CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + } + /** + * 门店自定义证照看板统计。 + * + * @param req 查询参数 + * @return 看板统计结果 + */ + CertificateStoreDashboardVO storeCertificateDashboard(CertificateStoreDashboardReq req); + + /** + * 门店证照看板表格分页查询。 + * + * @param req 查询参数 + * @return 分页结果 + */ + PageInfo storeCertificateDashboardTablePage(CertificateStoreDashboardReq req); + + /** + * 查询证照类型下拉选项。 + * + * @return 证照类型选项 + */ + List queryCertificateTypeList(); + + /** + * 批量初始化员工健康证实例(缺失状态)。 + * + * @param userIds 员工用户ID集合 + * @return 初始化条数 + */ + int batchInitHealthCertificate(Collection userIds); + + /** + * 初始化员工健康证实例(缺失状态)。 + * + * @param userId 员工用户ID + * @return 是否成功 + */ + default boolean initHealthCertificate(String userId){ + return batchInitHealthCertificate(List.of(userId)) > 0; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageApiService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageApiService.java new file mode 100644 index 0000000..1c5b7f5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageApiService.java @@ -0,0 +1,50 @@ +package jnpf.certificate.service; + +import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO; + +import java.util.Collection; +import java.util.List; + +/** + * 组织营业执照开放接口服务。 + */ +public interface CertificateManageApiService { + + /** + * 根据组织ID查询营业执照信息。 + * + * @param organizeId 组织ID + * @return 营业执照信息 + */ + CertificateOrganizeBusinessLicenseVO queryBusinessLicense(String organizeId); + + /** + * 根据组织ID列表批量查询营业执照信息。 + * + * @param organizeIds 组织ID列表 + * @return 营业执照信息列表 + */ + List queryBusinessLicenseBatch(Collection organizeIds); + + /** + * 保存营业执照信息。 + * + * @param req 营业执照参数 + */ + void saveBusinessLicense(CertificateOrganizeBusinessLicenseVO req); + + /** + * 初始化门店默认缺失证照(营业执照、食品经营许可证)。 + * + * @param storeId 门店ID + */ + void initStoreDefaultCertificates(String storeId); + + /** + * 根据组织ID删除该主体下全部证照信息。 + * + * @param organizeId 组织ID + * @param loginUserId 当前登录用户ID + */ + void deleteBusinessLicense(String organizeId, String loginUserId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageService.java new file mode 100644 index 0000000..16ff8fe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateManageService.java @@ -0,0 +1,50 @@ +package jnpf.certificate.service; + +import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHealthCertificateUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq; +import jnpf.model.certificate.vo.app.CertificateAppCertificateDetailVO; + +/** + * 端证照管理服务。 + */ +public interface CertificateManageService { + + /** + * 根据证照实例ID查询详情。 + * + * @param certificateInstanceId 证照实例ID + * @return 证照详情 + */ + CertificateAppCertificateDetailVO queryInfo(String certificateInstanceId); + + /** + * 更新健康证。 + * + * @param req 更新参数 + */ + void updateHealthCertificate(CertificateAppHealthCertificateUpdateReq req); + + /** + * 更新营业执照。 + * + * @param req 更新参数 + */ + void updateBusinessLicense(CertificateAppBusinessLicenseUpdateReq req); + + /** + * 更新食品经营许可证。 + * + * @param req 更新参数 + */ + void updateHygieneLicense(CertificateAppHygieneLicenseUpdateReq req); + + /** + * 更新门店自定义证照。 + * + * @param req 更新参数 + */ + void updateStoreCustomCertificate(CertificateAppStoreCustomUpdateReq req); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateStoreService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateStoreService.java new file mode 100644 index 0000000..11dda8d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/CertificateStoreService.java @@ -0,0 +1,46 @@ +package jnpf.certificate.service; + +import jnpf.base.ActionResult; +import jnpf.exception.HandleException; +import jnpf.model.certificate.req.CertificateStoreSaveReq; +import jnpf.model.certificate.vo.CertificateStoreAndCertificatesVO; +import jnpf.model.certificate.vo.CertificateStoreTabVO; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +/** + * 门店证照服务。 + */ +public interface CertificateStoreService { + + /** + * 保存门店及证照数据。 + * + * @param req 保存参数 + */ + String saveStoreAndCertificates(CertificateStoreSaveReq req); + + /** + * 删除门店。 + * + * @param id 门店ID + * @return 是否成功 + */ + boolean deleteStore(@PathVariable("id") String id) throws HandleException; + + /** + * 根据门店ID查询门店及门店证照详情。 + * + * @param storeId 门店ID + * @return 门店及门店证照详情 + */ + CertificateStoreAndCertificatesVO getStoreAndCertificates(String storeId); + + /** + * 查询门店证照Tab列表。 + * + * @return 门店证照Tab列表 + */ + List queryStoreCertificateTabList(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppReminderServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppReminderServiceImpl.java new file mode 100644 index 0000000..b2af1ad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppReminderServiceImpl.java @@ -0,0 +1,628 @@ +package jnpf.certificate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import jnpf.authority.utils.PermissionsApplicableEnums; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.certificate.helper.NoticeHelper; +import jnpf.certificate.helper.OrganizationHelper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.certificate.model.CertificateAppReminderRecord; +import jnpf.certificate.service.CertificateAppReminderService; +import jnpf.certificate.util.SubjectNameUtils; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.req.app.CertificateAppBatchRemindReq; +import jnpf.model.certificate.req.app.CertificateAppSingleRemindReq; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.organzie.OrganizeBaseInfoVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.storecertificatephoto.helper.StoreCertificatePhotoHelper; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.MutableTriple; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static jnpf.certificate.helper.NoticeHelper.URL_CERTIFICATE; + +/** + * App端证照提醒服务实现。 + */ +@Service +@Slf4j +public class CertificateAppReminderServiceImpl implements CertificateAppReminderService { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_EXPIRED = 2; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_NORMAL = 4; + + private static final int SUBJECT_TYPE_EMPLOYEE = 1; + private static final int SUBJECT_TYPE_ORGANIZATION = 2; + private static final int SUBJECT_TYPE_STORE = 3; + + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private OrganizationHelper organizationHelper; + @Autowired + private PermissionsUtils permissionsUtils; + @Autowired + private NoticeHelper noticeHelper; + @Autowired + private StoreCertificatePhotoHelper storeCertificatePhotoHelper; + @Autowired + private ThreadPoolTaskExecutor commonExecutor; + + /** + * 一键提醒。 + * 健康证提醒员工本人,非健康证提醒组织负责人。 + */ + @Override + public void batchRemind(CertificateAppBatchRemindReq req) { + log.error("batchRemind req:{}",req); + ServiceException.notNull(req, "请求参数不能为空"); + Integer subjectType = req.getSubjectType(); + ServiceException.isTrue(subjectType != null && subjectType >= SUBJECT_TYPE_EMPLOYEE && subjectType <= SUBJECT_TYPE_STORE, + "主体类型不合法"); + + // 构建查询的subjectType列表,subjectType=2时同时查3 + List subjectTypes = new ArrayList<>(); + subjectTypes.add(subjectType); + if (Integer.valueOf(SUBJECT_TYPE_ORGANIZATION).equals(subjectType)) { + subjectTypes.add(SUBJECT_TYPE_STORE); + } + // 查询非正常状态的证照实例 + List statuses = Arrays.asList(STATUS_MISSING, STATUS_EXPIRED, STATUS_NEAR_EXPIRE); + + List records= null; + List userIds = buildQueryUserIds(req.getOrgId()); + if(Objects.nonNull(userIds) && userIds.isEmpty()){//没查到数据,返回 + return; + } + if(req.getSubjectType() == SUBJECT_TYPE_EMPLOYEE){ + records = certificateInstanceMapper.selectBySubjectTypesAndStatusesFilterUser(subjectTypes, statuses,userIds); + }else{ + records = certificateInstanceMapper.selectBySubjectTypesAndStatusesFilterOrganization(subjectTypes, statuses,req.getOrgId()); + } + + if (CollUtil.isEmpty(records)) { + return; + } + + String tenantId = UserProvider.getUser().getTenantId(); + String remindUserName = StrUtil.blankToDefault(StrUtil.trim(UserProvider.getUser().getUserName()), "系统"); + + // 按是否健康证分组,批量解析接收人 + Map> grouped = records.stream() + .collect(Collectors.partitioningBy(r -> isHealthCertificate(r.getCertificateType()))); + + // 健康证:接收人就是员工本人(subjectId) + Map healthReceiverMap = resolveHealthReceivers(grouped.getOrDefault(true, Collections.emptyList())); + // 非健康证:接收人是组织负责人 + MutableTriple,Map,Map> + orgReceiversAndOrgNamesAndTemplateNames = buildOrgReceiversAndOrgNamesAndTemplateNames(grouped.getOrDefault(false, Collections.emptyList())); + + Map orgReceivers = orgReceiversAndOrgNamesAndTemplateNames.getLeft(); + Map orgNames = orgReceiversAndOrgNamesAndTemplateNames.getMiddle(); + Map templateNames = orgReceiversAndOrgNamesAndTemplateNames.getRight(); + + List finalRecords = records; + commonExecutor.execute(() -> sendNotices(finalRecords, healthReceiverMap, orgReceivers, orgNames, templateNames, tenantId, remindUserName)); + } + + private List buildQueryUserIds(String orgId) { + if(StringUtil.isBlank(orgId)){ + return null; + } + Map> userIdsByOrgIds = organizationHelper.buildUserIdsByOrgIds(List.of(orgId),false,null); + if(CollUtil.isEmpty(userIdsByOrgIds)){ + return Collections.emptyList(); + } + List userIds = userIdsByOrgIds.get(orgId); + if(CollectionUtil.isEmpty(userIds)){ + return Collections.emptyList(); + } + return userIds; + } + + private void sendNotices(List records, + Map healthReceiverMap, + Map orgReceivers, + Map orgNames, + Map templateNames, + String tenantId, + String remindUserName){ + for (CertificateInstanceEntity record : records) { + String receiverUserId = isHealthCertificate(record.getCertificateType()) + ? healthReceiverMap.get(StrUtil.trim(record.getSubjectId())) + : orgReceivers.get(StrUtil.trim(record.getSubjectId())); + if (StrUtil.isBlank(receiverUserId)) { + continue; + } + String subjectId = record.getSubjectId(); + String orgName = orgNames.get(subjectId); + String templateName = null; + + String templateId = record.getTemplateId(); + if(StringUtil.isNotBlank(templateId)){ + templateName = templateNames.get(templateId); + } + sendNotice(record, receiverUserId, tenantId, remindUserName,templateName,orgName); + } + } + + // 批量解析健康证接收人:subjectId -> userId + private Map resolveHealthReceivers(List records) { + if (CollUtil.isEmpty(records)) { + return Collections.emptyMap(); + } + Set subjectIds = records.stream() + .map(r -> StrUtil.trim(r.getSubjectId())) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + if (CollUtil.isEmpty(subjectIds)) { + return Collections.emptyMap(); + } + // 健康证subjectId就是userId,直接映射 + Map result = new HashMap<>(subjectIds.size()); + subjectIds.forEach(id -> result.put(id, id)); + return result; + } + + // 批量解析非健康证接收人:subjectId(组织/门店) -> leaderId + private Map resolveOrgReceivers(List records) { + if (CollUtil.isEmpty(records)) { + return Collections.emptyMap(); + } + Set subjectIds = records.stream() + .map(r -> StrUtil.trim(r.getSubjectId())) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + if (CollUtil.isEmpty(subjectIds)) { + return Collections.emptyMap(); + } + Map organizeMap = organizationHelper.buildOrganizeGenerals(subjectIds); + Map result = new HashMap<>(subjectIds.size()); + for (String subjectId : subjectIds) { + OrganizeGeneralDetailVO org = organizeMap.get(subjectId); + if (org != null && StrUtil.isNotBlank(org.getLeaderId())) { + result.put(subjectId, StrUtil.trim(org.getLeaderId())); + } + } + return result; + } + + + /** + * 左边是 组织接收人。key为subjectId,value为组织负责人id + * 中间是 组织名称,key为组织id,value为组织名称 + * 右边是 模板名称,key为模板id,value为模板名称 + * @param records + * @return + */ + private MutableTriple,Map,Map> + buildOrgReceiversAndOrgNamesAndTemplateNames(List records) { + if (CollUtil.isEmpty(records)) { + return MutableTriple.of(Collections.emptyMap(),Collections.emptyMap(),Collections.emptyMap()); + } + + Set templateIds = new HashSet<>(); + Set orgIds = new HashSet<>(); + for (CertificateInstanceEntity record:records){ + String templateId = record.getTemplateId(); + if(StringUtil.isNotBlank(templateId)){ + templateIds.add(templateId); + } + orgIds.add(record.getSubjectId()); + } + + Map orgReceivers = new HashMap<>(); + Map orgNames = new HashMap<>(); + + Map organizeMap = organizationHelper.buildOrganizeGenerals(orgIds); + Map templateNames = storeCertificatePhotoHelper.buildStoreCertificateIdAndNames(templateIds,null); + for (String orgId : orgIds) { + OrganizeGeneralDetailVO org = organizeMap.get(orgId); + if(Objects.isNull(org)){ + continue; + } + if (StrUtil.isNotBlank(org.getLeaderId())) { + orgReceivers.put(orgId, StrUtil.trim(org.getLeaderId())); + } + orgNames.put(orgId,org.getName()); + } + return MutableTriple.of(orgReceivers,orgNames,templateNames); + } + + /** + * 单个提醒。 + * 根据证照实例ID查询后,按证照类型解析接收人并发送提醒。 + */ + @Override + public void singleRemind(CertificateAppSingleRemindReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + List certificateInstanceIds = req.getCertificateInstanceIds(); + ServiceException.isTrue(CollUtil.isNotEmpty(certificateInstanceIds), "证照实例ID不能为空"); + + // 查询证照实例 + List certificateInstanceEntities = certificateInstanceMapper.selectBatchIds(certificateInstanceIds); + if(CollUtil.isEmpty(certificateInstanceEntities)){ + log.warn("证照实例不存在,certificateInstanceId={}", certificateInstanceIds); + return; + } + + Set templateIds = new HashSet<>(); + Set orgIds = new HashSet<>(); + for (CertificateInstanceEntity record:certificateInstanceEntities) { + if (record.getStatus() == null || Integer.valueOf(STATUS_NORMAL).equals(record.getStatus())) { + log.warn("证照状态无需提醒,certificateInstanceId={}, status={}", record.getId(), record.getStatus()); + continue; + } + String templateId = record.getTemplateId(); + Integer subjectType = record.getSubjectType(); + String subjectId = record.getSubjectId(); + if(StringUtil.isNotBlank(templateId)){ + templateIds.add(templateId); + } + if(Objects.nonNull(subjectType) && subjectType != SUBJECT_TYPE_EMPLOYEE){ + orgIds.add(subjectId); + } + } + + Map templateIdAndNames = storeCertificatePhotoHelper.buildStoreCertificateIdAndNames(templateIds,null); + Map organizeBaseInfoById = organizationHelper.buildBaseOrganizeVO(orgIds,UserProvider.getUser().getTenantId()); + + for (CertificateInstanceEntity record:certificateInstanceEntities){ + if (record.getStatus() == null || Integer.valueOf(STATUS_NORMAL).equals(record.getStatus())) { + log.warn("证照状态无需提醒,certificateInstanceId={}, status={}", record.getId(), record.getStatus()); + continue; + } + + // 解析接收人 + String receiverUserId = resolveSingleReceiverUserId(record,organizeBaseInfoById); + if (StrUtil.isBlank(receiverUserId)) { + log.warn("未找到提醒接收人,certificateInstanceId={}, subjectType={}, subjectId={}", + record.getId(), record.getSubjectType(), record.getSubjectId()); + continue; + } + + String tenantId = UserProvider.getUser().getTenantId(); + String remindUserName = StrUtil.blankToDefault(StrUtil.trim(UserProvider.getUser().getUserName()), "系统"); + + String templateName = templateIdAndNames.get(record.getTemplateId()); + String orgName = null; + OrganizeBaseInfoVO organizeBaseInfoVO = organizeBaseInfoById.get(record.getSubjectId()); + if(Objects.nonNull(organizeBaseInfoVO)){ + orgName = organizeBaseInfoVO.getName(); + } + + sendNotice(record, receiverUserId, tenantId, remindUserName,templateName,orgName); + } + } + + /** + * 解析单条提醒的接收人用户ID。 + * 健康证:通过V2UserApi#getUsersBound查询员工本人信息。 + * 非健康证:通过OrganizationHelper#buildOrganizeGenerals查询组织负责人。 + */ + private String resolveSingleReceiverUserId(CertificateInstanceEntity record,Map organizeBaseInfoById) { + String subjectId = StrUtil.trim(record.getSubjectId()); + if (StrUtil.isBlank(subjectId)) { + return null; + } + if (isHealthCertificate(record.getCertificateType())) { + return subjectId; + } + OrganizeBaseInfoVO organize = organizeBaseInfoById.get(subjectId); + if (organize == null) { + log.error("组织信息不存在,subjectId={}", subjectId); + return null; + } + return StrUtil.trim(organize.getLeaderId()); + } + + /** + * 发送单条提醒消息。 + */ + private void sendNotice(CertificateInstanceEntity record, + String receiverUserId, + String tenantId, + String remindUserName, + String templateName, + String orgName) { + if (record == null) { + return; + } + if (StrUtil.isBlank(receiverUserId)) { + return; + } + NoticeMessage noticeMessage = buildNoticeMessage(record, remindUserName,templateName,orgName); + + if(isHealthCertificate(record.getCertificateType())){ + noticeHelper.sendMessage( + Collections.singletonList(receiverUserId), + tenantId, + noticeMessage.getTitle(), + noticeMessage.getContent(), + NoticeHelper.BUTTON_NAME_HANDLER, + buildJumpHealthCertificateUrl(record), + NoticeHelper.MP_ID_CERTIFICATE + ); + return; + } + noticeHelper.sendMessage( + Collections.singletonList(receiverUserId), + tenantId, + noticeMessage.getTitle(), + noticeMessage.getContent(), + null, + null, + null + ); + } + + private String buildJumpHealthCertificateUrl(CertificateInstanceEntity record) { + if(Objects.isNull(record)){ + return null; + } + String subjectId = record.getSubjectId(); + if(StringUtil.isBlank(subjectId)){ + return null; + } + return String.format(URL_CERTIFICATE,record.getId()); + } + + /** + * 构建提醒文案。 + * 文案依据原型:标题“存在风险,请及时处理”,正文按缺失/临期/过期区分。 + */ + private NoticeMessage buildNoticeMessage(CertificateInstanceEntity record, + String remindUserName, + String templateName, + String orgName) { + String certificateType = StrUtil.trim(record.getCertificateType()); + Integer status = record.getStatus(); + String certificateName = resolveCertificateTypeName(certificateType,templateName); + int remainDays = calculateRemainDays(record.getExpireDate()); + String subjectName = StrUtil.blankToDefault(orgName,StrUtil.blankToDefault(SubjectNameUtils.getSubjectName(record.getSubjectType()), "该主体")); + + if (isHealthCertificate(certificateType)) { + String title = "您的健康证存在风险,请及时处理!"; + if (Integer.valueOf(STATUS_NEAR_EXPIRE).equals(status)) { + return new NoticeMessage(title, buildHealthCertificateNearExpireContent(remainDays,remindUserName)); + } + if (Integer.valueOf(STATUS_EXPIRED).equals(status)) { + return new NoticeMessage(title, String.format("您的健康证已过期,%s提醒您及时更新健康证。", remindUserName)); + } + return new NoticeMessage(title, String.format("您的健康证缺失,%s提醒您及时更新健康证。", remindUserName)); + } + + String title = subjectName + certificateName + "存在风险,请及时处理!"; + if (Integer.valueOf(STATUS_NEAR_EXPIRE).equals(status)) { + return new NoticeMessage(title, buildCertificateNearExpireContent(subjectName,certificateName,remainDays)); + } + if (Integer.valueOf(STATUS_EXPIRED).equals(status)) { + return new NoticeMessage(title, String.format("%s%s已过期,请及时更新%s。", subjectName, certificateName, certificateName)); + } + return new NoticeMessage(title, String.format("%s%s缺失,请及时上传%s。", subjectName, certificateName, certificateName)); + } + + private String buildCertificateNearExpireContent(String subjectName, String certificateName, int remainDays) { + if(remainDays <= 0){ + return String.format("%s%s今天过期,请及时更新%s。", subjectName, certificateName, certificateName); + } + return String.format("%s%s离到期时间还有%s天,请及时更新%s。", subjectName, certificateName, remainDays, certificateName); + } + + private String buildHealthCertificateNearExpireContent(int remainDays,String remindUserName) { + if(remainDays <= 0){ + return String.format("您的健康证今天过期,%s提醒您请及时更新健康证。", remindUserName); + } + return String.format("您的健康证离到期时间还有%s天,%s提醒您请及时更新健康证。", remainDays,remindUserName); + } + + /** + * 单条提醒权限校验。 + */ + private void validateSinglePermission(CertificateAppReminderRecord record, String userId, String moduleId) { + Integer subjectType = record.getSubjectType(); + if (Integer.valueOf(SUBJECT_TYPE_EMPLOYEE).equals(subjectType)) { + PermissionUserScope userScope = resolvePermissionUserScope(userId, moduleId); + if (Boolean.TRUE.equals(userScope.getAllDataPermission())) { + return; + } + ServiceException.isTrue(CollUtil.isNotEmpty(userScope.getUserIds()), "暂无数据权限"); + ServiceException.isTrue(userScope.getUserIds().contains(StrUtil.trim(record.getReceiverUserId())), "暂无当前证照数据权限"); + return; + } + + if (Integer.valueOf(SUBJECT_TYPE_ORGANIZATION).equals(subjectType) || Integer.valueOf(SUBJECT_TYPE_STORE).equals(subjectType)) { + PermissionOrgScope orgScope = resolvePermissionOrgScope(userId, moduleId,UserProvider.getUser().getTenantId()); + if (Boolean.TRUE.equals(orgScope.getAllDataPermission())) { + return; + } + String orgPermissionId = StrUtil.trim(record.getOrgPermissionId()); + ServiceException.isTrue(StrUtil.isNotBlank(orgPermissionId), "组织数据不存在,无法提醒"); + ServiceException.isTrue(CollUtil.isNotEmpty(orgScope.getOrgIds()), "暂无数据权限"); + ServiceException.isTrue(orgScope.getOrgIds().contains(orgPermissionId), "暂无当前证照数据权限"); + return; + } + throw new ServiceException("主体类型不支持提醒"); + } + + /** + * 解析员工维度权限范围。 + * allDataPermission=true 代表全量权限,false 时使用 userIds 过滤。 + */ + private PermissionUserScope resolvePermissionUserScope(String userId, String moduleId) { + List permissionUserIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, moduleId); + if (permissionUserIds == null) { + return new PermissionUserScope(true, Collections.emptyList()); + } + return new PermissionUserScope(false, normalizeIdList(permissionUserIds)); + } + + /** + * 解析组织维度权限范围。 + * allDataPermission=true 代表全量权限,false 时使用 orgIds 过滤。 + */ + private PermissionOrgScope resolvePermissionOrgScope(String userId, String moduleId,String tenantId) { + if (Boolean.TRUE.equals(UserProvider.getUser().getIsAdministrator())) { + return new PermissionOrgScope(true, Collections.emptyList()); + } + + PermissionsApplicableObject applicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userId, moduleId,tenantId); + PermissionsApplicableEnums applicableEnums = applicableObject == null ? null : applicableObject.getPermissionsApplicableEnums(); + if (PermissionsApplicableEnums.ALL.equals(applicableEnums)) { + return new PermissionOrgScope(true, Collections.emptyList()); + } + + List orgIds = normalizeIdList(applicableObject == null ? Collections.emptyList() : applicableObject.getOrgIds()); + if (CollUtil.isEmpty(orgIds)) { + try { + List fallbackOrgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userId); + if (fallbackOrgIds == null) { + return new PermissionOrgScope(true, Collections.emptyList()); + } + orgIds = normalizeIdList(fallbackOrgIds); + } catch (Exception e) { + return new PermissionOrgScope(false, Collections.emptyList()); + } + } + return new PermissionOrgScope(false, orgIds); + } + + /** + * 规整ID集合:去空、去重、去首尾空格。 + */ + private List normalizeIdList(Collection idList) { + if (CollUtil.isEmpty(idList)) { + return Collections.emptyList(); + } + return idList.stream() + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 是否健康证类型。 + */ + private boolean isHealthCertificate(String certificateType) { + return StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + } + + /** + * 证照类型转中文名称。 + */ + private String resolveCertificateTypeName(String certificateType,String templateName) { + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType())) { + return "健康证"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType())) { + return "营业执照"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HYGIENE_LICENSE.getType())) { + return "食品经营许可证"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType())) { + return templateName; + } + return "证照"; + } + + /** + * 计算距离到期的剩余天数。 + */ + private int calculateRemainDays(Date expireDate) { + if (expireDate == null) { + return 0; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + long days = DateUtil.betweenDay(today, target, false); + return (int) Math.max(days, 0L); + } + + /** + * 员工权限范围。 + */ + private static class PermissionUserScope { + private final Boolean allDataPermission; + private final List userIds; + + private PermissionUserScope(Boolean allDataPermission, List userIds) { + this.allDataPermission = allDataPermission; + this.userIds = userIds; + } + + public Boolean getAllDataPermission() { + return allDataPermission; + } + + public List getUserIds() { + return userIds; + } + } + + /** + * 组织权限范围。 + */ + private static class PermissionOrgScope { + private final Boolean allDataPermission; + private final List orgIds; + + private PermissionOrgScope(Boolean allDataPermission, List orgIds) { + this.allDataPermission = allDataPermission; + this.orgIds = orgIds; + } + + public Boolean getAllDataPermission() { + return allDataPermission; + } + + public List getOrgIds() { + return orgIds; + } + } + + /** + * 提醒文案对象。 + */ + private static class NoticeMessage { + private final String title; + private final String content; + + private NoticeMessage(String title, String content) { + this.title = title; + this.content = content; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppRiskServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppRiskServiceImpl.java new file mode 100644 index 0000000..2946f4c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateAppRiskServiceImpl.java @@ -0,0 +1,514 @@ +package jnpf.certificate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.authority.utils.PermissionsApplicableEnums; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.certificate.helper.OrganizationHelper; +import jnpf.certificate.mapper.CertificateAppRiskMapper; +import jnpf.certificate.service.CertificateAppRiskService; +import jnpf.model.certificate.req.app.CertificateAppEmployeeRiskQueryReq; +import jnpf.model.certificate.req.app.CertificateAppRiskChartReq; +import jnpf.model.certificate.req.app.CertificateAppStoreRiskQueryReq; +import jnpf.model.certificate.vo.app.CertificateAppEmployeeRiskVO; +import jnpf.model.certificate.vo.app.CertificateAppRiskChartVO; +import jnpf.model.certificate.vo.app.CertificateAppRiskReminderCountVO; +import jnpf.model.certificate.vo.app.CertificateAppStatusCountVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskItemVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.permission.eum.v2.FtbStoreTagEnum; +import jnpf.permission.vo.v2.organzie.OrganizeBaseInfoVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBaseRelationSimpleVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.storecertificatephoto.helper.StoreCertificatePhotoHelper; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * App 端证照风险服务实现。 + */ +@Service +public class CertificateAppRiskServiceImpl implements CertificateAppRiskService { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_EXPIRED = 2; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_NORMAL = 4; + + /** + * 证照风险查询 Mapper。 + */ + @Autowired + private CertificateAppRiskMapper certificateAppRiskMapper; + + /** + * 数据权限工具。 + */ + @Autowired + private PermissionsUtils permissionsUtils; + @Autowired + private OrganizationHelper organizationHelper; + @Autowired + private StoreCertificatePhotoHelper storeCertificatePhotoHelper; + + + /** + * 查询证照风险图表统计。 + * 健康证按用户数据权限统计,非健康证按组织数据权限统计。 + */ + @Override + public CertificateAppRiskChartVO queryChart(CertificateAppRiskChartReq req) { + CertificateAppRiskChartReq queryReq = req == null ? new CertificateAppRiskChartReq() : req; + queryReq.setOrgId(StrUtil.trim(queryReq.getOrgId())); + queryReq.setCertificateType(StrUtil.blankToDefault( + StrUtil.trim(queryReq.getCertificateType()), + CertificateTypeEnum.HEALTH_CERTIFICATE.getType() + )); + + String orgId = queryReq.getOrgId(); + boolean healthCertificateQuery = isHealthCertificate(queryReq.getCertificateType()); + buildQueryUserIds(queryReq,healthCertificateQuery); + //如果查询组织id,但是没有查询到该组织的用户id列表,则返回空 + if(healthCertificateQuery && StringUtil.isNotBlank(orgId) && CollectionUtil.isEmpty(queryReq.getUserIds())){ + return buildChart(null); + } + List statusCountList; + if (healthCertificateQuery) { + statusCountList = certificateAppRiskMapper.queryHealthRiskStatusCount(queryReq); + } else { + statusCountList = certificateAppRiskMapper.queryStoreRiskStatusCount(queryReq); + } + return buildChart(statusCountList); + } + + private void buildQueryUserIds(CertificateAppRiskChartReq queryReq,boolean healthCertificateQuery) { + if(!healthCertificateQuery){ + return; + } + String queryReqOrgId = queryReq.getOrgId(); + if(StringUtil.isBlank(queryReqOrgId)){ + return; + } + List userIdsByOrgId = organizationHelper.getUserIdsByOrgId(queryReqOrgId); + queryReq.setUserIds(userIdsByOrgId); + } + + /** + * 分页查询员工证照风险数据。 + */ + @Override + public PageInfo queryEmployeePage(CertificateAppEmployeeRiskQueryReq req) { + CertificateAppEmployeeRiskQueryReq queryReq = req == null ? new CertificateAppEmployeeRiskQueryReq() : req; + queryReq.setOrgId(StrUtil.trim(queryReq.getOrgId())); + long current = queryReq.getCurrentPage() == null || queryReq.getCurrentPage() <= 0 ? 1L : queryReq.getCurrentPage(); + long pageSize = queryReq.getPageSize() == null || queryReq.getPageSize() <= 0 ? 10L : queryReq.getPageSize(); + String orgId = queryReq.getOrgId(); + Collection queryUserIds; + Map relationMap; + if (StringUtil.isNotBlank(orgId)) { + relationMap = organizationHelper.getUserBaseRelationMapByOrgId(orgId); + queryUserIds = relationMap.keySet(); + } else { + relationMap = Collections.emptyMap(); + queryUserIds = null; + } + if (StringUtil.isNotBlank(orgId) && CollectionUtil.isEmpty(queryUserIds)) { + return buildEmptyEmployeePage(current, pageSize); + } + + Page page = new Page<>(current, pageSize); + Page queryPage = certificateAppRiskMapper.queryEmployeeRiskPageByUserIds(page, queryUserIds); + List records = queryPage.getRecords() == null ? new ArrayList<>() : queryPage.getRecords(); + if (CollectionUtil.isEmpty(records)) { + return buildEmptyEmployeePage(current, pageSize); + } + + Set pageUserIds = records.stream() + .map(CertificateAppEmployeeRiskVO::getUserId) + .filter(StringUtil::isNotBlank) + .collect(Collectors.toSet()); + if (CollectionUtil.isEmpty(pageUserIds)) { + return buildEmptyEmployeePage(current, pageSize); + } + if (StringUtil.isBlank(orgId)) { + relationMap = organizationHelper.getUserBaseRelationMapByUserIds(pageUserIds); + } + + for (CertificateAppEmployeeRiskVO item : records){ + UserBaseRelationSimpleVO relation = relationMap.get(item.getUserId()); + if(Objects.isNull(relation)){ + continue; + } + item.setWorkerName(StrUtil.blankToDefault(StrUtil.trim(relation.getUserName()), "-")); + item.setPhone(StrUtil.blankToDefault(StrUtil.trim(relation.getMobilePhone()), "-")); + item.setOrgId(relation.getOrganizeId()); + item.setPositionId(relation.getPositionId()); + item.setOrgName(relation.getOrganizeName()); + item.setPositionName(relation.getPositionName()); + item.setPhone(relation.getMobilePhone()); + } + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setTotal(queryPage.getTotal()); + return pageInfo; + } + + /** + * 分页查询门店证照风险数据。 + */ + @Override + public PageInfo queryStorePage(CertificateAppStoreRiskQueryReq req) { + CertificateAppStoreRiskQueryReq queryReq = req == null ? new CertificateAppStoreRiskQueryReq() : req; + queryReq.setOrgId(StrUtil.trim(queryReq.getOrgId())); + queryReq.setCertificateType(StrUtil.trim(queryReq.getCertificateType())); + long current = queryReq.getCurrentPage() == null || queryReq.getCurrentPage() <= 0 ? 1L : queryReq.getCurrentPage(); + long pageSize = queryReq.getPageSize() == null || queryReq.getPageSize() <= 0 ? 10L : queryReq.getPageSize(); + + PageInfo emptyPage = buildEmptyStorePage(current, pageSize); + + Page page = new Page<>(current, pageSize); + Page queryPage = certificateAppRiskMapper.queryStoreRiskPage(page, queryReq); + + List records = queryPage.getRecords() == null ? Collections.emptyList() : queryPage.getRecords(); + if (CollectionUtil.isEmpty(records)) { + return emptyPage; + } + + Set orgIds = new HashSet<>(); + records.forEach(vo -> { + orgIds.add(vo.getSubjectId()); + }); + List detailList = certificateAppRiskMapper.queryStoreRiskDetailList(orgIds); + + Set templateIds = detailList.stream() + .map(CertificateAppStoreRiskItemVO::getTemplateId) + .filter(StringUtil::isNotBlank) + .collect(Collectors.toSet()); + Map templateIdAndNames = null; + if(CollUtil.isNotEmpty(templateIds)){ + templateIdAndNames = storeCertificatePhotoHelper.buildStoreCertificateIdAndNames(templateIds,1); + }else { + templateIdAndNames = new HashMap<>(); + } + Map finalTemplateIdAndNames = templateIdAndNames; + Map> detailMap = CollectionUtil.emptyIfNull(detailList).stream() + .peek(item -> item.setCertificateTypeName(resolveCertificateTypeName(item.getCertificateType(),item.getTemplateId(), finalTemplateIdAndNames))) + .filter(item -> StringUtil.isNotBlank(item.getCertificateTypeName()))//过滤掉被禁用的证照 + .collect(Collectors.groupingBy(CertificateAppStoreRiskItemVO::getSubjectId, LinkedHashMap::new, Collectors.toList())); + for (CertificateAppStoreRiskVO record : records) { + List certificateList = detailMap.getOrDefault(record.getSubjectId(), Collections.emptyList()); + record.setCertificateList(certificateList); + } + Map organizeBaseInfoVOMap = organizationHelper.buildBaseOrganizeVO(orgIds,UserProvider.getUser().getTenantId()); + + fillStoreRiskItem(records,organizeBaseInfoVOMap); + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setTotal(queryPage.getTotal()); + return pageInfo; + } + + /** + * 查询有风险的员工、组织、总数量 + */ + @Override + public CertificateAppRiskReminderCountVO queryRiskReminderCount(String orgId) { + + List orgIds = new ArrayList<>(1); + Map> userIdsByOrgId = null; + if(StringUtil.isNotBlank(orgId)){ + userIdsByOrgId = organizationHelper.buildUserIdsByOrgIds(List.of(orgId),true,""); + orgIds.add(orgId); + }else{ + userIdsByOrgId = organizationHelper.buildUserIdsByOrgIds(null,true,""); + } + + Set userIds = userIdsByOrgId.values() + .stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + long employeeCount = 0L; + if(!CollUtil.isEmpty(userIds)){ + employeeCount = certificateAppRiskMapper.queryEmployeeRiskTotalCount(userIds); + } + + Long organizationCount = certificateAppRiskMapper.queryOrganizationRiskTotalCount(orgIds); + if(Objects.isNull(organizationCount)){ + organizationCount = 0L; + } + + CertificateAppRiskReminderCountVO vo = new CertificateAppRiskReminderCountVO(); + vo.setEmployeeCount(employeeCount); + vo.setOrganizationCount(organizationCount); + vo.setTotalCount(employeeCount + organizationCount); + return vo; + } + + /** + * 解析员工数据权限范围。 + * 通过 allDataPermission 标识是否全量权限,userIds 为受限时可访问用户。 + */ + private PermissionUserScope resolvePermissionUserScope(String userId, String moduleId) { + List permissionUserIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, moduleId); + if (permissionUserIds == null) { + return new PermissionUserScope(true, Collections.emptyList()); + } + return new PermissionUserScope(false, normalizeIdList(permissionUserIds)); + } + + /** + * 解析组织数据权限范围。 + * 通过 allDataPermission 标识是否全量权限,orgIds 为受限时可访问组织。 + */ + private PermissionOrgScope resolvePermissionOrgScope(String userId, String moduleId,String tenantId) { + if (Boolean.TRUE.equals(UserProvider.getUser().getIsAdministrator())) { + return new PermissionOrgScope(true, Collections.emptyList()); + } + + PermissionsApplicableObject applicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userId, moduleId,tenantId); + PermissionsApplicableEnums applicableEnums = applicableObject == null ? null : applicableObject.getPermissionsApplicableEnums(); + if (PermissionsApplicableEnums.ALL.equals(applicableEnums)) { + return new PermissionOrgScope(true, Collections.emptyList()); + } + + List orgIds = normalizeIdList(applicableObject == null ? Collections.emptyList() : applicableObject.getOrgIds()); + if (CollUtil.isEmpty(orgIds)) { + try { + List fallbackOrgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userId); + if (fallbackOrgIds == null) { + return new PermissionOrgScope(true, Collections.emptyList()); + } + orgIds = normalizeIdList(fallbackOrgIds); + } catch (Exception e) { + return new PermissionOrgScope(false, Collections.emptyList()); + } + } + return new PermissionOrgScope(false, orgIds); + } + + /** + * 规整 ID 列表:去空、去重、去前后空格。 + */ + private List normalizeIdList(List idList) { + if (CollUtil.isEmpty(idList)) { + return Collections.emptyList(); + } + return idList.stream() + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + } + + /** + * 是否为健康证类型。 + */ + private boolean isHealthCertificate(String certificateType) { + return StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + } + + /** + * 构建图表统计结果对象。 + */ + private CertificateAppRiskChartVO buildChart(List statusCountList) { + Map countMap = CollUtil.emptyIfNull(statusCountList) + .stream() + .filter(item -> item != null && item.getStatus() != null) + .collect(Collectors.toMap( + CertificateAppStatusCountVO::getStatus, + item -> item.getCount() == null ? 0 : item.getCount(), + Integer::sum + )); + + int missingCount = countMap.getOrDefault(STATUS_MISSING, 0); + int nearExpireCount = countMap.getOrDefault(STATUS_NEAR_EXPIRE, 0); + int expiredCount = countMap.getOrDefault(STATUS_EXPIRED, 0); + int normalCount = countMap.getOrDefault(STATUS_NORMAL, 0); + int totalCount = missingCount + nearExpireCount + expiredCount + normalCount; + + CertificateAppRiskChartVO chartVO = new CertificateAppRiskChartVO(); + chartVO.setMissingCount(missingCount); + chartVO.setNearExpireCount(nearExpireCount); + chartVO.setExpiredCount(expiredCount); + chartVO.setNormalCount(normalCount); + chartVO.setTotalCount(totalCount); + + chartVO.setStatusList(Arrays.asList( + buildStatusCount(STATUS_MISSING, missingCount), + buildStatusCount(STATUS_NEAR_EXPIRE, nearExpireCount), + buildStatusCount(STATUS_EXPIRED, expiredCount), + buildStatusCount(STATUS_NORMAL, normalCount) + )); + return chartVO; + } + + /** + * 构建单个状态统计项。 + */ + private CertificateAppStatusCountVO buildStatusCount(Integer status, Integer count) { + CertificateAppStatusCountVO item = new CertificateAppStatusCountVO(); + item.setStatus(status); + item.setStatusName(resolveStatusName(status)); + item.setCount(count == null ? 0 : count); + return item; + } + + /** + * 构建空图表结果。 + */ + private CertificateAppRiskChartVO buildEmptyChart() { + return buildChart(Collections.emptyList()); + } + + /** + * 构建员工风险空分页。 + */ + private PageInfo buildEmptyEmployeePage(long current, long pageSize) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(Collections.emptyList()); + pageInfo.setPageNum((int) current); + pageInfo.setPageSize((int) pageSize); + pageInfo.setTotal(0); + return pageInfo; + } + + /** + * 构建门店风险空分页。 + */ + private PageInfo buildEmptyStorePage(long current, long pageSize) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(Collections.emptyList()); + pageInfo.setPageNum((int) current); + pageInfo.setPageSize((int) pageSize); + pageInfo.setTotal(0); + return pageInfo; + } + + /** + * 填充门店风险返回项的展示字段。 + */ + private void fillStoreRiskItem(List records,Map organizeBaseInfoVOMap) { + if (CollectionUtil.isEmpty(records)) { + return; + } + for (CertificateAppStoreRiskVO item : records){ + OrganizeBaseInfoVO organizeBaseInfoVO = organizeBaseInfoVOMap.get(item.getSubjectId()); + if(Objects.isNull(organizeBaseInfoVO)){ + continue; + } + item.setOrganizeId(organizeBaseInfoVO.getId()); + item.setOrganizeName(organizeBaseInfoVO.getName()); + String storeTag = organizeBaseInfoVO.getStoreTag(); + item.setStoreTag(storeTag); + item.setOrganizeLeaderName(organizeBaseInfoVO.getLeaderName()); + item.setOrganizeLeaderId(organizeBaseInfoVO.getLeaderId()); + FtbStoreTagEnum storeTagEnum = FtbStoreTagEnum.fromName(storeTag); + if(Objects.nonNull(storeTagEnum)){ + item.setStoreTagName(storeTagEnum.getLabel()); + } + } + + } + + /** + * 状态编码转中文名称。 + */ + private String resolveStatusName(Integer status) { + if (Integer.valueOf(STATUS_MISSING).equals(status)) { + return "缺失"; + } + if (Integer.valueOf(STATUS_EXPIRED).equals(status)) { + return "过期"; + } + if (Integer.valueOf(STATUS_NEAR_EXPIRE).equals(status)) { + return "临期"; + } + if (Integer.valueOf(STATUS_NORMAL).equals(status)) { + return "正常"; + } + return "-"; + } + + /** + * 证照类型编码转中文名称。 + */ + private String resolveCertificateTypeName(String certificateType,String templateId,Map templateIdAndNames) { + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType())) { + return "健康证"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType())) { + return "营业执照"; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HYGIENE_LICENSE.getType())) { + return "食品经营许可证"; + } + if (StrUtil.isBlank(certificateType)) { + return "证照"; + } + String templateName = templateIdAndNames.get(templateId); + if(StringUtil.isNotBlank(templateName)){ + return templateName; + } + return ""; + } + + /** + * 用户维度权限范围。 + */ + private static class PermissionUserScope { + private final Boolean allDataPermission; + private final List userIds; + + private PermissionUserScope(Boolean allDataPermission, List userIds) { + this.allDataPermission = allDataPermission; + this.userIds = userIds; + } + + public Boolean getAllDataPermission() { + return allDataPermission; + } + + public List getUserIds() { + return userIds; + } + } + + /** + * 组织维度权限范围。 + */ + private static class PermissionOrgScope { + private final Boolean allDataPermission; + private final List orgIds; + + private PermissionOrgScope(Boolean allDataPermission, List orgIds) { + this.allDataPermission = allDataPermission; + this.orgIds = orgIds; + } + + public Boolean getAllDataPermission() { + return allDataPermission; + } + + public List getOrgIds() { + return orgIds; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateInstanceServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateInstanceServiceImpl.java new file mode 100644 index 0000000..afabc73 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateInstanceServiceImpl.java @@ -0,0 +1,1541 @@ +package jnpf.certificate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.certificate.helper.OrganizationHelper; +import jnpf.certificate.mapper.CertificateBusinessLicenseExtMapper; +import jnpf.certificate.mapper.CertificateHygieneLicenseExtMapper; +import jnpf.certificate.mapper.CertificateInstanceItemMapper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.certificate.util.CertificateStatusUtils; +import jnpf.certificate.util.StoreTypeUtils; +import jnpf.certificate.util.WorkerStatusUtils; +import jnpf.model.certificate.dto.HealthCertificateStatusDTO; +import jnpf.model.certificate.po.CertificateBusinessLicenseExtEntity; +import jnpf.model.certificate.po.CertificateHygieneLicenseExtEntity; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.po.CertificateInstanceItemEntity; +import jnpf.model.certificate.req.CertificateInstanceAddReq; +import jnpf.model.certificate.req.CertificateHealthManageQueryReq; +import jnpf.model.certificate.req.CertificateInstanceItemReq; +import jnpf.model.certificate.req.CertificateInstanceQueryReq; +import jnpf.model.certificate.req.CertificateInstanceUpdateReq; +import jnpf.model.certificate.req.CertificateStoreManageQueryReq; +import jnpf.model.certificate.req.CertificateStoreDashboardReq; +import jnpf.model.certificate.req.CertificateSyncHealthReq; +import jnpf.model.certificate.vo.CertificateHealthDashboardStatusStatVO; +import jnpf.model.certificate.vo.CertificateHealthManageVO; +import jnpf.model.certificate.vo.CertificateInstanceItemVO; +import jnpf.model.certificate.vo.CertificateInstanceVO; +import jnpf.model.certificate.vo.CertificateStoreManageVO; +import jnpf.model.certificate.vo.CertificateStoreDashboardVO; +import jnpf.model.certificate.vo.CertificateStoreCustomStatusBarVO; +import jnpf.model.certificate.vo.CertificateStoreCustomStatusPieVO; +import jnpf.model.certificate.vo.CertificateStoreCustomStatusTableVO; +import jnpf.model.certificate.vo.CertificateTypeOptionVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreRiskVO; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.vo.WarningNoticeVO; +import jnpf.permission.UserApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.dto.v2.user.QueryUserBaseRelationDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeBaseInfoVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBaseRelationSimpleVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoMapper; +import jnpf.storecertificatephoto.service.WarningNoticeService; +import jnpf.util.FtbUtil; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +@Service +@Slf4j +public class CertificateInstanceServiceImpl extends ServiceImpl + implements CertificateInstanceService { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_NORMAL = 4; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_EXPIRED = 2; + + private static final int BOARD_STATUS_MISSING = 1; + private static final int BOARD_STATUS_NEAR_EXPIRE = 3; + private static final int BOARD_STATUS_EXPIRED = 2; + private static final int BOARD_STATUS_NORMAL = 4; + + private static final int SUBJECT_TYPE_EMPLOYEE = 1; + private static final int SUBJECT_TYPE_STORE = 3; + private static final int NEAR_EXPIRE_DAYS = 30; + @Autowired + private CertificateInstanceItemMapper certificateInstanceItemMapper; + @Autowired + private CertificateBusinessLicenseExtMapper certificateBusinessLicenseExtMapper; + @Autowired + private CertificateHygieneLicenseExtMapper certificateHygieneLicenseExtMapper; + @Autowired + private FtbPermissionOrganizeService ftbPermissionOrganizeService; + @Autowired + private StoreCertificatePhotoMapper storeCertificatePhotoMapper; + @Autowired + private PermissionsUtils permissionsUtils; + @Autowired + private WarningNoticeService warningNoticeService; + @Autowired + private V2OrganizeApi v2OrganizeApi; + @Autowired + private V2PositionApi v2PositionApi; + @Autowired + private UserApi userApi; + @Autowired + private OrganizationHelper organizationHelper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(CertificateInstanceAddReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + validateSaveReq(req); + + Date issueDate = parseDate(req.getIssueDate(), "发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + Integer status = calculateStatus(req.getIsLongTerm(), expireDate); + + CertificateInstanceEntity entity = req.convert(issueDate, expireDate, status); + normalizeSaveEntity(entity); + baseMapper.insert(entity); + + saveItems(entity.getId(), req.getItemList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(CertificateInstanceUpdateReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + ServiceException.isTrue(StrUtil.isNotBlank(req.getId()), "实例ID不能为空"); + validateSaveReq(req); + + CertificateInstanceEntity dbEntity = queryActiveEntity(req.getId().trim()); + Date issueDate = parseDate(req.getIssueDate(), "发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + Integer status = calculateStatus(req.getIsLongTerm(), expireDate); + + CertificateInstanceEntity entity = req.convert(issueDate, expireDate, status); + entity.setId(dbEntity.getId()); + entity.setEnabledMark(dbEntity.getEnabledMark()); + normalizeSaveEntity(entity); + baseMapper.updateById(entity); + + LambdaQueryWrapper itemDeleteWrapper = Wrappers.lambdaQuery(); + itemDeleteWrapper.eq(CertificateInstanceItemEntity::getInstanceId, dbEntity.getId()); + certificateInstanceItemMapper.delete(itemDeleteWrapper); + saveItems(dbEntity.getId(), req.getItemList()); + } + + @Override + public CertificateInstanceVO queryInfo(String id) { + ServiceException.isTrue(StrUtil.isNotBlank(id), "实例ID不能为空"); + CertificateInstanceEntity entity = queryActiveEntity(id.trim()); + CertificateInstanceVO vo = CertificateInstanceVO.convert(entity); + vo.setStatus(calculateStatus(entity.getIsLongTerm(), entity.getExpireDate())); + fillCertificateExtInfo(vo, entity); + + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.eq(CertificateInstanceItemEntity::getInstanceId, entity.getId()); + itemWrapper.eq(CertificateInstanceItemEntity::getEnabledMark, 0); + itemWrapper.orderByAsc(CertificateInstanceItemEntity::getSorts); + List itemEntities = certificateInstanceItemMapper.selectList(itemWrapper); + if (CollUtil.isNotEmpty(itemEntities)) { + vo.setItemList(itemEntities.stream().map(CertificateInstanceItemVO::convert).collect(Collectors.toList())); + } + return vo; + } + + private void fillCertificateExtInfo(CertificateInstanceVO vo, CertificateInstanceEntity entity) { + if (vo == null || entity == null) { + return; + } + String certificateType = StrUtil.trim(entity.getCertificateType()); + if (StrUtil.isBlank(certificateType)) { + return; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType())) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CertificateBusinessLicenseExtEntity::getInstanceId, entity.getId()); + queryWrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + queryWrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getLastModifyTime); + queryWrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getCreatorTime); + List extList = certificateBusinessLicenseExtMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(extList)) { + return; + } + CertificateBusinessLicenseExtEntity ext = extList.get(0); + vo.setCompanyName(ext.getCompanyName()); + vo.setCompanyAddress(ext.getCompanyAddress()); + vo.setLegalRepresentative(ext.getLegalRepresentative()); + vo.setEstablishDate(ext.getEstablishDate()); + vo.setBusinessScope(ext.getBusinessScope()); + return; + } + if (StrUtil.equalsIgnoreCase(certificateType, CertificateTypeEnum.HYGIENE_LICENSE.getType())) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CertificateHygieneLicenseExtEntity::getInstanceId, entity.getId()); + queryWrapper.eq(CertificateHygieneLicenseExtEntity::getEnabledMark, 0); + queryWrapper.orderByDesc(CertificateHygieneLicenseExtEntity::getLastModifyTime); + queryWrapper.orderByDesc(CertificateHygieneLicenseExtEntity::getCreatorTime); + List extList = certificateHygieneLicenseExtMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(extList)) { + return; + } + CertificateHygieneLicenseExtEntity ext = extList.get(0); + vo.setBusinessProject(ext.getBusinessProject()); + } + } + + @Override + public PageInfo queryPage(CertificateInstanceQueryReq req) { + CertificateInstanceQueryReq queryReq = req == null ? new CertificateInstanceQueryReq() : req; + long current = queryReq.getCurrentPage() == null || queryReq.getCurrentPage() <= 0 ? 1L : queryReq.getCurrentPage(); + long pageSize = queryReq.getPageSize() == null || queryReq.getPageSize() <= 0 ? 10L : queryReq.getPageSize(); + + Page page = new Page<>(current, pageSize); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + + if (StrUtil.isNotBlank(queryReq.getType())) { + Optional certificateTypeEnumOptional = CertificateTypeEnum.getByType(queryReq.getType()); + ServiceException.isTrue(certificateTypeEnumOptional.isPresent(), "证照类型不正确"); + wrapper.eq(CertificateInstanceEntity::getCertificateType, certificateTypeEnumOptional.get().getType()); + } + wrapper.eq(queryReq.getSubjectType() != null, CertificateInstanceEntity::getSubjectType, queryReq.getSubjectType()); + wrapper.eq(StrUtil.isNotBlank(queryReq.getSubjectId()), CertificateInstanceEntity::getSubjectId, StrUtil.trim(queryReq.getSubjectId())); + + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date nearExpireLimit = DateUtil.endOfDay(DateUtil.offsetDay(today, NEAR_EXPIRE_DAYS)); + if (queryReq.getStatus() != null) { + if (queryReq.getStatus() == STATUS_NORMAL) { + wrapper.and(w -> w.eq(CertificateInstanceEntity::getIsLongTerm, 1) + .or() + .isNull(CertificateInstanceEntity::getExpireDate) + .or() + .gt(CertificateInstanceEntity::getExpireDate, nearExpireLimit)); + } else if (queryReq.getStatus() == STATUS_NEAR_EXPIRE) { + wrapper.eq(CertificateInstanceEntity::getIsLongTerm, 0); + wrapper.ge(CertificateInstanceEntity::getExpireDate, today); + wrapper.le(CertificateInstanceEntity::getExpireDate, nearExpireLimit); + } else if (queryReq.getStatus() == STATUS_EXPIRED) { + wrapper.eq(CertificateInstanceEntity::getIsLongTerm, 0); + wrapper.isNotNull(CertificateInstanceEntity::getExpireDate); + wrapper.lt(CertificateInstanceEntity::getExpireDate, today); + } + } + + Date expireStart = parseDate(queryReq.getExpireStart(), "到期开始日期"); + Date expireEnd = parseDate(queryReq.getExpireEnd(), "到期结束日期"); + if (expireStart != null) { + wrapper.ge(CertificateInstanceEntity::getExpireDate, DateUtil.beginOfDay(expireStart)); + } + if (expireEnd != null) { + wrapper.le(CertificateInstanceEntity::getExpireDate, DateUtil.endOfDay(expireEnd)); + } + + if (queryReq.getDaysToExpireLte() != null) { + Date limitDate = DateUtil.endOfDay(DateUtil.offsetDay(today, queryReq.getDaysToExpireLte())); + wrapper.eq(CertificateInstanceEntity::getIsLongTerm, 0); + wrapper.isNotNull(CertificateInstanceEntity::getExpireDate); + wrapper.ge(CertificateInstanceEntity::getExpireDate, today); + wrapper.le(CertificateInstanceEntity::getExpireDate, limitDate); + } + + if (StrUtil.isNotBlank(queryReq.getKeyword())) { + String keyword = StrUtil.trim(queryReq.getKeyword()); + wrapper.and(w -> w.like(CertificateInstanceEntity::getCertificateNo, keyword) + .or() + .like(CertificateInstanceEntity::getSubjectId, keyword)); + } + + wrapper.orderByDesc(CertificateInstanceEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateInstanceEntity::getCreatorTime); + + Page queryPage = baseMapper.selectPage(page, wrapper); + List voList = queryPage.getRecords().stream() + .map(CertificateInstanceVO::convert) + .collect(Collectors.toList()); + for (CertificateInstanceVO vo : voList) { + vo.setType(vo.getType() == null ? null : vo.getType().trim()); + vo.setStatus(calculateStatus(vo.getIsLongTerm(), vo.getExpireDate())); + } + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(voList); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public PageInfo queryHealthPage(CertificateHealthManageQueryReq req) { + CertificateHealthManageQueryReq queryReq = req == null ? new CertificateHealthManageQueryReq() : req; + long current = queryReq.getCurrentPage() == null || queryReq.getCurrentPage() <= 0 ? 1L : queryReq.getCurrentPage(); + long pageSize = queryReq.getPageSize() == null || queryReq.getPageSize() <= 0 ? 10L : queryReq.getPageSize(); + + //无法查询离职员工 + String workerStatus = buildWorkStatus(queryReq); + if(StaffWorkerStatus.RESIGNED.getCode().equals(workerStatus)){ + return new PageInfo<>(Collections.emptyList()); + } + //装载非离职员工 + if(StringUtil.isBlank(workerStatus)){ + queryReq.setWorkerStatusCollection(Stream.of(StaffWorkerStatus.values()) + .filter(w->!StaffWorkerStatus.RESIGNED.equals(w)) + .map(StaffWorkerStatus::getCode) + .collect(Collectors.toSet())); + } + Map userBaseVoByUserId = buildQueryUserIds(queryReq); + if(org.springframework.util.CollectionUtils.isEmpty(userBaseVoByUserId)){ + return new PageInfo<>(Collections.emptyList()); + } + Page page = new Page<>(current, pageSize); + Page queryPage = baseMapper.queryHealthManagePage( + page, + queryReq, + NEAR_EXPIRE_DAYS + ); + + fillCertificateHealthManageVO(queryPage.getRecords(),userBaseVoByUserId); + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(queryPage.getRecords() == null ? Collections.emptyList() : queryPage.getRecords()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + private Map buildQueryUserIds(CertificateHealthManageQueryReq queryReq) { + String workerStatus = queryReq.getWorkerStatus(); + String positionId = queryReq.getPositionId(); + List orgIds = queryReq.getOrgIds(); + String keyword = queryReq.getKeyword(); + + QueryUserBaseRelationDTO dto = new QueryUserBaseRelationDTO(); + if(StringUtil.isNotBlank(workerStatus)){ + dto.setWorkerStatusCollection(List.of(workerStatus)); + }else{ + dto.setWorkerStatusCollection(queryReq.getWorkerStatusCollection()); + } + dto.setPositionId(positionId); + dto.setKeyword(keyword); + dto.setOrgIds(orgIds); + Map userBaseVOByUserId = organizationHelper.getUserBaseRelationMapByUserIds(dto); + if(org.springframework.util.CollectionUtils.isEmpty(userBaseVOByUserId)){ + return Collections.emptyMap(); + } + queryReq.setUserIds(userBaseVOByUserId.keySet()); + return userBaseVOByUserId; + } + + private String buildWorkStatus(CertificateHealthManageQueryReq req) { + if(Objects.isNull(req)){ + return ""; + } + String workerStatus = req.getWorkerStatus(); + if(StringUtil.isBlank(workerStatus)){ + return ""; + } + return workerStatus; + } + + private void fillCertificateHealthManageVO(List records, + Map userBaseVoByUserId) { + if(CollectionUtils.isEmpty(records)){ + return; + } + for (CertificateHealthManageVO certificateHealthManageVO:records){ + UserBaseRelationSimpleVO userBaseRelationSimpleVO = userBaseVoByUserId.get(certificateHealthManageVO.getUserId()); + if(Objects.isNull(userBaseRelationSimpleVO)){ + continue; + } + certificateHealthManageVO.setOrgId(userBaseRelationSimpleVO.getOrganizeId()); + certificateHealthManageVO.setOrgName(userBaseRelationSimpleVO.getOrganizeName()); + certificateHealthManageVO.setPositionId(userBaseRelationSimpleVO.getPositionId()); + certificateHealthManageVO.setPositionName(userBaseRelationSimpleVO.getPositionName()); + certificateHealthManageVO.setCertificateStatusName(CertificateStatusUtils.buildStatusName(certificateHealthManageVO.getCertificateStatus())); + certificateHealthManageVO.setWorkerStatus(userBaseRelationSimpleVO.getWorkerStatus()); + certificateHealthManageVO.setWorkerStatusName(WorkerStatusUtils.buildStatusName(userBaseRelationSimpleVO.getWorkerStatus())); + certificateHealthManageVO.setPhone(userBaseRelationSimpleVO.getMobilePhone()); + certificateHealthManageVO.setName(userBaseRelationSimpleVO.getUserName()); + } + } + + @Override + public PageInfo queryStorePage(CertificateStoreManageQueryReq req) { + CertificateStoreManageQueryReq queryReq = req == null ? new CertificateStoreManageQueryReq() : req; + long current = queryReq.getCurrentPage() == null || queryReq.getCurrentPage() <= 0 ? 1L : queryReq.getCurrentPage(); + long pageSize = queryReq.getPageSize() == null || queryReq.getPageSize() <= 0 ? 10L : queryReq.getPageSize(); + + PageInfo emptyPageInfo = new PageInfo<>(); + emptyPageInfo.setList(Collections.emptyList()); + emptyPageInfo.setPageNum((int) current); + emptyPageInfo.setPageSize((int) pageSize); + emptyPageInfo.setTotal(0); + + Set orgIds = new HashSet<>(queryReq.getOrgIds()); + Map organizeBaseInfoVOMap; + String storeTag = queryReq.getStoreTag(); + if(StringUtil.isNotBlank(storeTag)){ + organizeBaseInfoVOMap = organizationHelper.buildBaseOrganizeVO(queryReq.getOrgIds(),storeTag,UserProvider.getUser().getTenantId()); + Map finalOrganizeBaseInfoVOMap = organizeBaseInfoVOMap; + orgIds.removeIf(oi->!finalOrganizeBaseInfoVOMap.containsKey(oi)); + orgIds.addAll(organizeBaseInfoVOMap.keySet()); + } else { + organizeBaseInfoVOMap = Collections.emptyMap(); + } + + //查询的组织id列表为空,且想要查询的组织id列表不为空,说明未查询到 + if(CollUtil.isEmpty(orgIds) && CollUtil.isNotEmpty(queryReq.getOrgIds())){ + return emptyPageInfo; + } + + queryReq.setOrgIds(orgIds); + buildTemplateId(queryReq); + Page page = new Page<>(current, pageSize); + Page queryPage = baseMapper.selectCertificateStoreManageVOPage(page,queryReq); + List certificateStoreManageVOS = queryPage.getRecords(); + if(CollUtil.isEmpty(certificateStoreManageVOS)){ + return emptyPageInfo; + } + + Set needQueryOrgIds = new HashSet<>(); + for (CertificateStoreManageVO certificateStoreManageVO:certificateStoreManageVOS){ + String orgId = certificateStoreManageVO.getOrgId(); + if(StringUtil.isBlank(orgId)){ + continue; + } + + OrganizeBaseInfoVO organizeBaseInfoVO = organizeBaseInfoVOMap.get(orgId); + if(Objects.isNull(organizeBaseInfoVO)){ + needQueryOrgIds.add(orgId); + continue; + } + fillCertificateStoreManageVO(certificateStoreManageVO,organizeBaseInfoVO); + } + + if(CollUtil.isEmpty(needQueryOrgIds)){ + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(certificateStoreManageVOS); + pageInfo.setPageNum((int) current); + pageInfo.setPageSize((int) pageSize); + pageInfo.setTotal(queryPage.getTotal()); + return pageInfo; + } + + organizeBaseInfoVOMap = organizationHelper.buildBaseOrganizeVO(needQueryOrgIds,storeTag,UserProvider.getUser().getTenantId()); + fillCertificateStoreManageVOs(certificateStoreManageVOS,organizeBaseInfoVOMap); + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(certificateStoreManageVOS); + pageInfo.setPageNum((int) current); + pageInfo.setPageSize((int) pageSize); + pageInfo.setTotal(queryPage.getTotal()); + return pageInfo; + } + + private void buildTemplateId(CertificateStoreManageQueryReq queryReq) { + String certificateType = queryReq.getCertificateType(); + Optional certificateTypeEnumOptional = CertificateTypeEnum.getByType(certificateType); + if(certificateTypeEnumOptional.isEmpty()){ + throw new ServiceException("证照类型不能为空!"); + } + CertificateTypeEnum certificateTypeEnum = certificateTypeEnumOptional.get(); + queryReq.setCertificateType(certificateTypeEnum.getType()); + if(certificateTypeEnum == CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE){ + queryReq.setTemplateId(certificateType); + } + } + + private void fillCertificateStoreManageVOs(List certificateStoreManageVOS, Map organizeBaseInfoVOMap) { + if(CollUtil.isEmpty(certificateStoreManageVOS) || CollUtil.isEmpty(organizeBaseInfoVOMap)){ + return; + } + for (CertificateStoreManageVO certificateStoreManageVO : certificateStoreManageVOS){ + OrganizeBaseInfoVO organizeBaseInfoVO = organizeBaseInfoVOMap.get(certificateStoreManageVO.getOrgId()); + fillCertificateStoreManageVO(certificateStoreManageVO, organizeBaseInfoVO); + } + } + + private void fillCertificateStoreManageVO(CertificateStoreManageVO certificateStoreManageVO, OrganizeBaseInfoVO organizeBaseInfoVO) { + if(Objects.isNull(certificateStoreManageVO) || Objects.isNull(organizeBaseInfoVO)){ + return; + } + certificateStoreManageVO.setOrgName(organizeBaseInfoVO.getName()); + certificateStoreManageVO.setOrgCategory(organizeBaseInfoVO.getOrganizeCategory()); + certificateStoreManageVO.setOrgCategoryName(organizeBaseInfoVO.getOrganizeCategoryEnums().getDescription()); + certificateStoreManageVO.setParentOrgName(organizeBaseInfoVO.getParentName()); + certificateStoreManageVO.setLeaderName(organizeBaseInfoVO.getLeaderName()); + certificateStoreManageVO.setStoreTypeName(StoreTypeUtils.buildStoreType(organizeBaseInfoVO.getStoreTag())); + certificateStoreManageVO.setCertificateStatusName(CertificateStatusUtils.buildStatusName(certificateStoreManageVO.getCertificateStatus())); + certificateStoreManageVO.setBusinessTerm(buildBusinessTerm(certificateStoreManageVO)); + } + + private String buildBusinessTerm(CertificateStoreManageVO certificateStoreManageVO) { + if(Objects.isNull(certificateStoreManageVO)){ + return "-"; + } + Integer isLongTerm = certificateStoreManageVO.getIsLongTerm(); + if(Objects.nonNull(isLongTerm) && isLongTerm == 1){ + return "长期"; + } + String startDate = DateUtil.format(certificateStoreManageVO.getIssueDate(), "yyyy-MM-dd"); + String endDate = DateUtil.format(certificateStoreManageVO.getExpireDate(), "yyyy-MM-dd"); + if(StringUtil.isBlank(startDate) || StringUtil.isBlank(endDate)){ + return "-"; + } + return startDate + "至" + endDate; + } + + @Override + public List queryCertificateTypeList() { + List result = new ArrayList<>(); + List defaultTypeList = Arrays.asList( + CertificateTypeEnum.HEALTH_CERTIFICATE, + CertificateTypeEnum.BUSINESS_LICENSE, + CertificateTypeEnum.HYGIENE_LICENSE + ); + for (CertificateTypeEnum certificateType : defaultTypeList) { + CertificateTypeOptionVO optionVO = new CertificateTypeOptionVO(); + optionVO.setLabel(certificateType.getLabel()); + optionVO.setKey(certificateType.getType()); + optionVO.setType(certificateType.getType()); + result.add(optionVO); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + queryWrapper.eq(StoreCertificatePhotoEntity::getStatus, 1); + List templateList = storeCertificatePhotoMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(templateList)) { + return result; + } + + for (StoreCertificatePhotoEntity template : templateList) { + if (template == null || StrUtil.isBlank(template.getId()) || StrUtil.isBlank(template.getCertificateName())) { + continue; + } + CertificateTypeOptionVO optionVO = new CertificateTypeOptionVO(); + optionVO.setLabel(StrUtil.trim(template.getCertificateName())); + optionVO.setKey(StrUtil.trim(template.getId())); + optionVO.setType(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()); + result.add(optionVO); + } + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(String id) { + ServiceException.isTrue(StrUtil.isNotBlank(id), "实例ID不能为空"); + CertificateInstanceEntity entity = queryActiveEntity(id.trim()); + entity.setEnabledMark(1); + baseMapper.updateById(entity); + + CertificateInstanceItemEntity itemUpdateEntity = new CertificateInstanceItemEntity(); + itemUpdateEntity.setEnabledMark(1); + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.eq(CertificateInstanceItemEntity::getInstanceId, entity.getId()); + itemWrapper.eq(CertificateInstanceItemEntity::getEnabledMark, 0); + certificateInstanceItemMapper.update(itemUpdateEntity, itemWrapper); + } + + @Override + public void saveHealthCertificate(String userId, String healthCertificate, String startHealthDate, String endHealthDate){ + log.error("saveHealthCertificate info userId:{},healthCertificate:{},startHealthDate:{},endHealthDate:{}",userId,healthCertificate,startHealthDate,endHealthDate); + CertificateInstanceEntity entity = buildEntity(userId, healthCertificate, startHealthDate, endHealthDate); + int r = updateByWrapper(entity); + if(r>0){ + return; + } + try { + entity.setId(FtbUtil.getId()); + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(UserProvider.getLoginUserId()); + baseMapper.insert(entity); + }catch (DuplicateKeyException duplicateKeyException){ + r = updateByWrapper(entity); + if(r<=0){ + log.error("insert CertificateInstanceEntity fail.and update fail"); + } + } + } + + @Override + public void deleteHealthCertificate(String userId) { + baseMapper.delete(new LambdaQueryWrapper() + .eq(CertificateInstanceEntity::getSubjectId,userId) + .eq(CertificateInstanceEntity::getSubjectType,1) + .eq(CertificateInstanceEntity::getCertificateType,CertificateTypeEnum.HEALTH_CERTIFICATE.getType()) + .eq(CertificateInstanceEntity::getTemplateId,"")); + } + + private int updateByWrapper(CertificateInstanceEntity entity) { + return baseMapper.update(null,new LambdaUpdateWrapper() + .eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType()) + .eq(CertificateInstanceEntity::getSubjectId, entity.getSubjectId()) + .set(CertificateInstanceEntity::getCertificateImage,entity.getCertificateImage()) + .set(CertificateInstanceEntity::getIssueDate,entity.getIssueDate()) + .set(CertificateInstanceEntity::getExpireDate,entity.getExpireDate()) + .set(CertificateInstanceEntity::getExpireTimestamp,entity.getExpireTimestamp()) + .set(CertificateInstanceEntity::getStatus,entity.getStatus()) + .set(CertificateInstanceEntity::getLastModifyTime,entity.getLastModifyTime()) + .set(CertificateInstanceEntity::getLastModifyUserId,entity.getLastModifyUserId())); + } + + @Override + public Optional getHealthCertificateDetail(String userId) { + CertificateInstanceEntity certificateInstanceEntity = baseMapper.selectOne(new LambdaUpdateWrapper() + .eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType()) + .eq(CertificateInstanceEntity::getSubjectId, userId)); + if(Objects.isNull(certificateInstanceEntity)){ + return Optional.empty(); + } + return Optional.of(new HealthCertificateDetailVO(certificateInstanceEntity.getSubjectId(), + certificateInstanceEntity.getId(), + certificateInstanceEntity.getCertificateImage(), + certificateInstanceEntity.getIssueDate(), + certificateInstanceEntity.getExpireDate(), + certificateInstanceEntity.getStatus())); + } + + @Override + public List getHealthCertificateDetails(Collection userIds) { + if(CollectionUtils.isEmpty(userIds)){ + return Collections.emptyList(); + } + return baseMapper.selectList(new LambdaUpdateWrapper().in(CertificateInstanceEntity::getSubjectId,userIds) + .eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType())) + .stream() + .map(certificateInstanceEntity->new HealthCertificateDetailVO(certificateInstanceEntity.getSubjectId(), + certificateInstanceEntity.getId(), + certificateInstanceEntity.getCertificateImage(), + certificateInstanceEntity.getIssueDate(), + certificateInstanceEntity.getExpireDate(), + certificateInstanceEntity.getStatus())) + .collect(Collectors.toList()); + } + + @Override + public List getUserIdsByStatus(Integer status,String certificateType) { + if(Objects.isNull(status) && StringUtil.isBlank(certificateType)){ + return Collections.emptyList(); + } + return baseMapper.selectList(new LambdaUpdateWrapper() + .eq(CertificateInstanceEntity::getCertificateType, certificateType) + .eq(CertificateInstanceEntity::getStatus, status)) + .stream() + .map(CertificateInstanceEntity::getSubjectId) + .distinct() + .collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int batchInitHealthCertificate(Collection userIds) { + if (CollectionUtils.isEmpty(userIds)) { + return 0; + } + + List targetUserIds = userIds.stream() + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(targetUserIds)) { + return 0; + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(CertificateInstanceEntity::getSubjectId); + queryWrapper.eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + queryWrapper.in(CertificateInstanceEntity::getSubjectId, targetUserIds); + List existList = baseMapper.selectList(queryWrapper); + Set existUserIds = existList.stream() + .map(CertificateInstanceEntity::getSubjectId) + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .collect(Collectors.toSet()); + + List needInsertUserIds = targetUserIds.stream() + .filter(userId -> !existUserIds.contains(userId)) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(needInsertUserIds)) { + return 0; + } + List initEntities = needInsertUserIds.stream() + .map(userId -> { + CertificateInstanceEntity entity = new CertificateInstanceEntity(); + entity.setId(FtbUtil.getId()); + entity.setCertificateType(CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + entity.setSubjectType(SUBJECT_TYPE_EMPLOYEE); + entity.setSubjectId(userId); + entity.setStatus(STATUS_MISSING); + entity.setEnabledMark(0); + return entity; + }) + .collect(Collectors.toList()); + return baseMapper.batchInitHealthCertificate(initEntities); + } + + public CertificateInstanceEntity buildEntity(String userId, String healthCertificate, String startHealthDate, String endHealthDate) { + CertificateInstanceEntity entity = new CertificateInstanceEntity(); + entity.setCertificateType(CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + entity.setSubjectType(SUBJECT_TYPE_EMPLOYEE); + entity.setSubjectId(userId); + entity.setCertificateImage(healthCertificate); + int status = 1;//默认缺失 + if(StringUtil.isNotBlank(startHealthDate)){ + entity.setIssueDate(parseDate(startHealthDate,"startHealthDate")); + } + if(StringUtil.isNotBlank(endHealthDate)){ + WarningNoticeVO warningNoticeVO = warningNoticeService.queryByType(CertificateTypeEnum.HEALTH_CERTIFICATE.getType()); + Date expireDate = parseDate(endHealthDate,"endHealthDate"); + entity.setExpireDate(expireDate); + entity.setExpireTimestamp(expireDate.getTime()); + int nearExpireDays = NEAR_EXPIRE_DAYS; + if(Objects.nonNull(warningNoticeVO)){ + nearExpireDays = warningNoticeVO.getExpiryReminderDays(); + } + status = calculateStatus(0,entity.getExpireDate(),nearExpireDays); + } + + entity.setIsLongTerm(0); + entity.setStatus(status); + entity.setEnabledMark(0); + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(UserProvider.getLoginUserId()); + return entity; + } + + @Override + public CertificateStoreDashboardVO storeCertificateDashboard(CertificateStoreDashboardReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + + List storeIdFilter = req.getStoreIds(); + String storeTypeFilter = StrUtil.trim(req.getStoreType()); + String certificateTypeText = StrUtil.trim(req.getCertificateType()); + ServiceException.isTrue(StrUtil.isNotBlank(certificateTypeText), "证照类型不能为空"); + + String queryCertificateType; + String queryTemplateId = null; + Optional certificateTypeEnumOptional = CertificateTypeEnum.getByType(certificateTypeText); + if(certificateTypeEnumOptional.isEmpty()){ + throw new ServiceException("证照类型错误!",400); + } + CertificateTypeEnum certificateTypeEnum = certificateTypeEnumOptional.get(); + queryCertificateType = certificateTypeEnum.getType(); + if(certificateTypeEnum.equals(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE)){ + queryTemplateId = certificateTypeText; + } + + if (CertificateTypeEnum.HEALTH_CERTIFICATE.equals(certificateTypeEnum)) { + return buildHealthCertificateDashboard(storeIdFilter, storeTypeFilter); + } + List statusStatList = baseMapper.queryStoreDashboardStatusStats( + queryCertificateType, queryTemplateId, storeIdFilter, storeTypeFilter); + if (CollectionUtils.isEmpty(statusStatList)) { + return buildEmptyDashboard(); + } + + int totalMissingCount = 0; + int totalNearExpireCount = 0; + int totalExpiredCount = 0; + int totalNormalCount = 0; + Set orgIds = new HashSet<>(); + Map storeCustomStatusTableVOMap = new HashMap<>(); + for (CertificateHealthDashboardStatusStatVO row : statusStatList) { + if (row == null) { + continue; + } + String storeId = row.getStoreId(); + orgIds.add(storeId); + CertificateStoreCustomStatusTableVO storeCustomStatusTableVO = storeCustomStatusTableVOMap.computeIfAbsent(storeId,v->new CertificateStoreCustomStatusTableVO()); + int status = row.getStatus() == null ? STATUS_MISSING : row.getStatus(); + int count = row.getTotalCount() == null ? 0 : row.getTotalCount(); + storeCustomStatusTableVO.addStatusCount(status, count); + if (count <= 0) { + continue; + } + if (status == STATUS_MISSING) { + totalMissingCount += count; + continue; + } + if (status == STATUS_NEAR_EXPIRE) { + totalNearExpireCount += count; + continue; + } + if (status == STATUS_EXPIRED) { + totalExpiredCount += count; + continue; + } + totalNormalCount += count; + } + + Map organizeGeneralDetailVOMap = organizationHelper.buildOrganizeGenerals(orgIds); + for (CertificateStoreCustomStatusTableVO certificateStoreCustomStatusTableVO : storeCustomStatusTableVOMap.values()){ + OrganizeGeneralDetailVO organizeGeneralDetailVO = organizeGeneralDetailVOMap.get(certificateStoreCustomStatusTableVO.getStoreId()); + if(Objects.nonNull(organizeGeneralDetailVO)){ + certificateStoreCustomStatusTableVO.setStoreName(organizeGeneralDetailVO.getName()); + } + int totalCount = certificateStoreCustomStatusTableVO.getTotalCount(); + if(totalCount <= 0){ + continue; + } + certificateStoreCustomStatusTableVO.setMissingRatio(formatPercent(certificateStoreCustomStatusTableVO.getMissingCount(), totalCount)); + certificateStoreCustomStatusTableVO.setNearExpireRatio(formatPercent(certificateStoreCustomStatusTableVO.getNearExpireCount(), totalCount)); + certificateStoreCustomStatusTableVO.setExpiredRatio(formatPercent(certificateStoreCustomStatusTableVO.getExpiredCount(), totalCount)); + certificateStoreCustomStatusTableVO.setNormalRatio(formatPercent(certificateStoreCustomStatusTableVO.getNormalCount(), totalCount)); + } + int totalCount = totalMissingCount + totalNearExpireCount + totalExpiredCount + totalNormalCount; + + CertificateStoreDashboardVO result = new CertificateStoreDashboardVO(); + result.setMissingCount(totalMissingCount); + result.setNearExpireCount(totalNearExpireCount); + result.setExpiredCount(totalExpiredCount); + result.setNormalCount(totalNormalCount); + result.setTotalCount(totalCount); + result.setBarList(Collections.singletonList(new CertificateStoreCustomStatusBarVO("","",totalCount,totalMissingCount,totalNearExpireCount,totalExpiredCount,totalNormalCount))); + + List pieList = new ArrayList<>(); + pieList.add(buildPieItem(BOARD_STATUS_MISSING, "缺失", totalMissingCount, totalCount)); + pieList.add(buildPieItem(BOARD_STATUS_NEAR_EXPIRE, "临期", totalNearExpireCount, totalCount)); + pieList.add(buildPieItem(BOARD_STATUS_EXPIRED, "过期", totalExpiredCount, totalCount)); + pieList.add(buildPieItem(BOARD_STATUS_NORMAL, "正常", totalNormalCount, totalCount)); + result.setPieList(pieList); + return result; + } + + @Override + public PageInfo storeCertificateDashboardTablePage(CertificateStoreDashboardReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + long current = req.getCurrentPage() == null || req.getCurrentPage() <= 0 ? 1L : req.getCurrentPage(); + long pageSize = req.getPageSize() == null || req.getPageSize() <= 0 ? 10L : req.getPageSize(); + + List storeIdFilter = req.getStoreIds(); + String storeTypeFilter = StrUtil.trim(req.getStoreType()); + String certificateTypeText = StrUtil.trim(req.getCertificateType()); + ServiceException.isTrue(StrUtil.isNotBlank(certificateTypeText), "证照类型不能为空"); + + String queryCertificateType; + String queryTemplateId = null; + Optional certificateTypeEnumOptional = CertificateTypeEnum.getByType(certificateTypeText); + if(certificateTypeEnumOptional.isEmpty()){ + throw new ServiceException("证照类型错误!",400); + } + CertificateTypeEnum certificateTypeEnum = certificateTypeEnumOptional.get(); + queryCertificateType = certificateTypeEnum.getType(); + if(certificateTypeEnum.equals(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE)){ + queryTemplateId = certificateTypeText; + } + + if (CertificateTypeEnum.HEALTH_CERTIFICATE.equals(certificateTypeEnum)) { + List healthTableList = buildHealthCertificateDashboardTableList(storeIdFilter, storeTypeFilter); + return pageByMemory(healthTableList, current, pageSize); + } + + Page page = new Page<>(current, pageSize); + Page queryPage = baseMapper.queryStoreDashboardTablePage( + page, + queryCertificateType, + queryTemplateId, + storeIdFilter, + storeTypeFilter + ); + List records = queryPage.getRecords() == null ? Collections.emptyList() : queryPage.getRecords(); + Set orgIds = new HashSet<>(); + for (CertificateStoreCustomStatusTableVO record : records) { + orgIds.add(record.getStoreId()); + int totalCount = record.getTotalCount(); + if (totalCount <= 0) { + continue; + } + record.setMissingRatio(formatPercent(record.getMissingCount(), totalCount)); + record.setNearExpireRatio(formatPercent(record.getNearExpireCount(), totalCount)); + record.setExpiredRatio(formatPercent(record.getExpiredCount(), totalCount)); + record.setNormalRatio(formatPercent(record.getNormalCount(), totalCount)); + } + Map organizeBaseInfoVOMap = organizationHelper.buildBaseOrganizeVO(orgIds,UserProvider.getUser().getTenantId()); + for (CertificateStoreCustomStatusTableVO record : records) { + String orgId = record.getStoreId(); + OrganizeBaseInfoVO organizeBaseInfoVO = organizeBaseInfoVOMap.get(orgId); + if(Objects.nonNull(organizeBaseInfoVO)){ + record.setStoreName(organizeBaseInfoVO.getName()); + } + } + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + private CertificateStoreDashboardVO buildHealthCertificateDashboard(List storeIdFilter, + String storeTypeFilter) { + List statusStatList = buildHealthCertificateStatusStat(storeIdFilter,storeTypeFilter); + if(CollectionUtils.isEmpty(statusStatList)){ + return buildEmptyDashboard(); + } + Map storeCounterMap = new LinkedHashMap<>(); + Set orgIds = new HashSet<>(); + for (CertificateHealthDashboardStatusStatVO row : statusStatList) { + if (row == null) { + continue; + } + String storeId = StrUtil.trim(row.getStoreId()); + if (StrUtil.isBlank(storeId)) { + continue; + } + String storeName = StrUtil.trim(row.getStoreName()); + if(StringUtil.isBlank(storeName)){ + orgIds.add(storeId); + } + HealthDashboardCounter counter = storeCounterMap.computeIfAbsent(storeId,v->new HealthDashboardCounter(storeId,storeName)); + int status = row.getStatus() == null ? 0 : row.getStatus(); + int count = row.getTotalCount() == null ? 0 : row.getTotalCount(); + counter.addStatusCount(status, count); + } + + Map organizeGeneralDetailVOMap = organizationHelper.buildOrganizeGenerals(orgIds); + + List barList = new ArrayList<>(); + List tableList = new ArrayList<>(); + + int totalMissingCount = 0; + int totalNearExpireCount = 0; + int totalExpiredCount = 0; + int totalNormalCount = 0; + int totalCount = 0; + + for (HealthDashboardCounter counter : storeCounterMap.values()) { + String storeId = counter.getStoreId(); + String storeName = counter.getStoreName(); + if(StringUtil.isBlank(storeName)){ + OrganizeGeneralDetailVO organizeGeneralDetailVO = organizeGeneralDetailVOMap.get(storeId); + if(Objects.nonNull(organizeGeneralDetailVO)){ + counter.setStoreName(organizeGeneralDetailVO.getName()); + } + } + int storeTotalCount = counter.getTotalCount(); + int missingCount = counter.getMissingCount(); + int nearExpireCount = counter.getNearExpireCount(); + int expiredCount = counter.getExpiredCount(); + int normalCount = counter.getNormalCount(); + + totalMissingCount += missingCount; + totalNearExpireCount += nearExpireCount; + totalExpiredCount += expiredCount; + totalNormalCount += normalCount; + totalCount += storeTotalCount; + + CertificateStoreCustomStatusBarVO barVO = new CertificateStoreCustomStatusBarVO(); + barVO.setStoreId(counter.getStoreId()); + barVO.setStoreName(counter.getStoreName()); + barVO.setTotalCount(storeTotalCount); + barVO.setMissingCount(missingCount); + barVO.setNearExpireCount(nearExpireCount); + barVO.setExpiredCount(expiredCount); + barVO.setNormalCount(normalCount); + barList.add(barVO); + + CertificateStoreCustomStatusTableVO tableVO = new CertificateStoreCustomStatusTableVO(); + tableVO.setStoreId(counter.getStoreId()); + tableVO.setStoreName(counter.getStoreName()); + tableVO.setTotalCount(storeTotalCount); + tableVO.setMissingCount(missingCount); + tableVO.setMissingRatio(formatPercent(missingCount, storeTotalCount)); + tableVO.setNearExpireCount(nearExpireCount); + tableVO.setNearExpireRatio(formatPercent(nearExpireCount, storeTotalCount)); + tableVO.setExpiredCount(expiredCount); + tableVO.setExpiredRatio(formatPercent(expiredCount, storeTotalCount)); + tableVO.setNormalCount(normalCount); + tableVO.setNormalRatio(formatPercent(normalCount, storeTotalCount)); + tableList.add(tableVO); + } + + CertificateStoreDashboardVO result = new CertificateStoreDashboardVO(); + result.setMissingCount(totalMissingCount); + result.setNearExpireCount(totalNearExpireCount); + result.setExpiredCount(totalExpiredCount); + result.setNormalCount(totalNormalCount); + result.setTotalCount(totalCount); + result.setBarList(barList); + + List pieList = new ArrayList<>(); + pieList.add(buildPieItem(BOARD_STATUS_MISSING, "缺失", totalMissingCount, totalCount)); + pieList.add(buildPieItem(BOARD_STATUS_NEAR_EXPIRE, "临期", totalNearExpireCount, totalCount)); + pieList.add(buildPieItem(BOARD_STATUS_EXPIRED, "过期", totalExpiredCount, totalCount)); + pieList.add(buildPieItem(BOARD_STATUS_NORMAL, "正常", totalNormalCount, totalCount)); + result.setPieList(pieList); + return result; + } + + private List buildHealthCertificateStatusStat(List storeIdFilter, String storeTypeFilter) { + boolean isAdministrator = BooleanUtil.isTrue(UserProvider.getUser().getIsAdministrator()); + Map> orgIdAndUserIds = null; + if(isAdministrator){ + orgIdAndUserIds = organizationHelper.buildUserIdsByOrgIds(storeIdFilter,true,storeTypeFilter); + }else{ + List orgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(UserProvider.getLoginUserId()); + orgIdAndUserIds = organizationHelper.buildUserIdsByOrgIds(buildQueryOrgIds(orgIds,storeIdFilter),false,storeTypeFilter); + } + + List healthCertificateStatusDTOList = baseMapper.queryHealthCertificateStatus(orgIdAndUserIds.values() + .stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet())); + + return buildHealthCertificateStatusStat(orgIdAndUserIds,healthCertificateStatusDTOList); + } + + private List buildHealthCertificateStatusStat(Map> orgIdAndUserIds, + List healthCertificateStatusDTOList) { + if(CollectionUtils.isEmpty(healthCertificateStatusDTOList)){ + return Collections.emptyList(); + } + + Map userIdAndStatusDTOMap = healthCertificateStatusDTOList.stream() + .collect(Collectors.toMap(HealthCertificateStatusDTO::getUserId,v->v)); + Map> orgIdAndStatusCountMap = new HashMap<>(); + for (Map.Entry> entry:orgIdAndUserIds.entrySet()){ + String orgId = entry.getKey(); + List userIds = entry.getValue(); + Map statusCountMap = orgIdAndStatusCountMap.computeIfAbsent(orgId,v->new HashMap<>()); + for (String userId:userIds){ + HealthCertificateStatusDTO statusDTO = userIdAndStatusDTOMap.get(userId); + if(Objects.isNull(statusDTO)){ + continue; + } + int status = statusDTO.getStatus(); + int count = statusCountMap.getOrDefault(status,0); + count++; + statusCountMap.put(status,count); + } + } + List result = new ArrayList<>(); + for (Map.Entry> entry:orgIdAndStatusCountMap.entrySet()){ + String orgId = entry.getKey(); + Map statusCountMap = entry.getValue(); + for (Map.Entry entry1:statusCountMap.entrySet()){ + Integer status = entry1.getKey(); + Integer count = entry1.getValue(); + CertificateHealthDashboardStatusStatVO statusStatVO = new CertificateHealthDashboardStatusStatVO(); + statusStatVO.setStoreId(orgId); + statusStatVO.setStatus(status); + statusStatVO.setTotalCount(count); + result.add(statusStatVO); + } + } + return result; + } + + private Collection buildQueryOrgIds(List hasPermissionOrgIds, List queryOrgIds) { + if(CollectionUtils.isEmpty(hasPermissionOrgIds)){ + return Collections.emptyList(); + } + if(CollectionUtils.isEmpty(queryOrgIds)){ + return hasPermissionOrgIds; + } + Set hasPermissionOrgIdsSet = new HashSet<>(hasPermissionOrgIds); + return queryOrgIds.stream() + .filter(hasPermissionOrgIdsSet::contains) + .collect(Collectors.toSet()); + } + + private static class HealthDashboardCounter { + private final String storeId; + private String storeName; + private int missingCount; + private int nearExpireCount; + private int expiredCount; + private int normalCount; + private int totalCount; + + private HealthDashboardCounter(String storeId, String storeName) { + this.storeId = storeId; + this.storeName = storeName; + } + + private void addStatusCount(int status, int count) { + if (count <= 0) { + return; + } + this.totalCount += count; + if (status == STATUS_MISSING) { + this.missingCount += count; + return; + } + if (status == STATUS_NEAR_EXPIRE) { + this.nearExpireCount += count; + return; + } + if (status == STATUS_EXPIRED) { + this.expiredCount += count; + return; + } + if (status == STATUS_NORMAL) { + this.normalCount += count; + } + } + + private String getStoreId() { + return storeId; + } + + private String getStoreName() { + return storeName; + } + + private void setStoreName(String storeName) { + this.storeName = storeName; + } + + private int getMissingCount() { + return missingCount; + } + + private int getNearExpireCount() { + return nearExpireCount; + } + + private int getExpiredCount() { + return expiredCount; + } + + private int getNormalCount() { + return normalCount; + } + + private int getTotalCount() { + return totalCount; + } + } + + private String normalizeStoreCertificateType(String certificateType) { + String type = StrUtil.blankToDefault(StrUtil.trim(certificateType), CertificateTypeEnum.BUSINESS_LICENSE.getType()); + boolean supported = StrUtil.equalsIgnoreCase(type, CertificateTypeEnum.BUSINESS_LICENSE.getType()) + || StrUtil.equalsIgnoreCase(type, CertificateTypeEnum.HYGIENE_LICENSE.getType()); + ServiceException.isTrue(supported, "门店证照类型只支持营业执照或食品许可"); + return StrUtil.equalsIgnoreCase(type, CertificateTypeEnum.HYGIENE_LICENSE.getType()) + ? CertificateTypeEnum.HYGIENE_LICENSE.getType() + : CertificateTypeEnum.BUSINESS_LICENSE.getType(); + } + + private Integer calculateStoreCertificateStatus(CertificateInstanceEntity instanceEntity) { + if (instanceEntity == null) { + return BOARD_STATUS_MISSING; + } + return instanceEntity.getStatus(); + } + + private String resolveStoreCertificateStatusName(Integer status) { + if (Integer.valueOf(BOARD_STATUS_MISSING).equals(status)) { + return "缺失"; + } + if (Integer.valueOf(BOARD_STATUS_NEAR_EXPIRE).equals(status)) { + return "临期"; + } + if (Integer.valueOf(BOARD_STATUS_EXPIRED).equals(status)) { + return "过期"; + } + return "正常"; + } + + private String resolveOrganizeCategoryName(OrganizeCategoryEnums categoryEnums) { + if (OrganizeCategoryEnums.COMPANY.equals(categoryEnums)) { + return "公司"; + } + if (OrganizeCategoryEnums.STORE.equals(categoryEnums)) { + return "门店"; + } + if (OrganizeCategoryEnums.DEPARTMENT.equals(categoryEnums)) { + return "部门"; + } + if (OrganizeCategoryEnums.TEAM.equals(categoryEnums)) { + return "班组"; + } + return "-"; + } + + private String resolveParentOrgName(OrganizeGeneralDetailVO store, Map orgNameMap) { + if (store == null) { + return "-"; + } + String parentName = orgNameMap.get(StrUtil.trim(store.getParentId())); + if (StrUtil.isNotBlank(parentName)) { + return parentName; + } + List treeNodes = splitOrganizeTreeName(store.getOrganizeTreeName()); + if (treeNodes.size() >= 2) { + return treeNodes.get(treeNodes.size() - 2); + } + return "-"; + } + + private String resolveStoreTypeName(OrganizeGeneralDetailVO store, Map orgNameMap) { + if (store == null || !OrganizeCategoryEnums.STORE.equals(store.getOrganizeCategoryEnums())) { + return "-"; + } + return StoreTypeUtils.buildStoreType(store.getStoreTag()); + } + + private List splitOrganizeTreeName(String organizeTreeName) { + if (StrUtil.isBlank(organizeTreeName)) { + return Collections.emptyList(); + } + String normalized = organizeTreeName + .replace("\\", "/") + .replace(">", "/") + .replace("/", "/"); + return StrUtil.split(normalized, '/') + .stream() + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .collect(Collectors.toList()); + } + + private String formatBusinessTerm(CertificateInstanceEntity entity) { + if (entity == null) { + return "-"; + } + if (Integer.valueOf(1).equals(entity.getIsLongTerm()) || entity.getExpireDate() == null) { + return "长期"; + } + String issueText = entity.getIssueDate() == null ? "-" : DateUtil.formatDate(entity.getIssueDate()); + String expireText = DateUtil.formatDate(entity.getExpireDate()); + return issueText + " ~ " + expireText; + } + + private List queryPermissionStores(Set storeIdFilter, String storeTypeFilter) { + List storeList = ftbPermissionOrganizeService.authOrganizesByUserBound( + List.of(OrganizeCategoryEnums.STORE,OrganizeCategoryEnums.COMPANY), null, false, false); + if (CollUtil.isEmpty(storeList)) { + return Collections.emptyList(); + } + Map targetStoreMap = new LinkedHashMap<>(); + for (OrganizeGeneralDetailVO store : storeList) { + //被禁用了过滤下 + if(BooleanUtil.isFalse(store.getEnabled())){ + continue; + } + if (store == null || StrUtil.isBlank(store.getId())) { + continue; + } + String storeId = StrUtil.trim(store.getId()); + if (CollectionUtils.isNotEmpty(storeIdFilter) && !storeIdFilter.contains(storeId)) { + continue; + } + String storeType = StrUtil.trim(store.getStoreTag()); + if (StrUtil.isNotBlank(storeTypeFilter) && !StrUtil.equals(storeType, storeTypeFilter)) { + continue; + } + //覆盖后者 + targetStoreMap.put(storeId, store); + } + return new ArrayList<>(targetStoreMap.values()); + } + + private CertificateInstanceEntity queryActiveEntity(String id) { + CertificateInstanceEntity entity = baseMapper.selectById(id); + ServiceException.notNull(entity, "证照实例不存在"); + ServiceException.isTrue(Integer.valueOf(0).equals(entity.getEnabledMark()), "证照实例不存在"); + return entity; + } + + + private void validateSaveReq(CertificateInstanceAddReq req) { + ServiceException.notNull(req.getType(), "证照类型不能为空"); + if (CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.equals(req.getType())) { + ServiceException.isTrue(StrUtil.isNotBlank(req.getTemplateId()), "门店自定义证照必须传模板ID"); + } + if (req.getIsLongTerm() != null && req.getIsLongTerm() == 0) { + ServiceException.isTrue(StrUtil.isNotBlank(req.getExpireDate()), "非长期证照必须传到期日期"); + } + if (req.getItemList() == null) { + req.setItemList(new ArrayList<>()); + } + for (CertificateInstanceItemReq itemReq : req.getItemList()) { + ServiceException.notNull(itemReq, "证照明细不能为空"); + ServiceException.isTrue(StrUtil.isNotBlank(itemReq.getItemName()), "证照明细名称不能为空"); + } + } + + + private void normalizeSaveEntity(CertificateInstanceEntity entity) { + entity.setCertificateType(StrUtil.trim(entity.getCertificateType())); + entity.setTemplateId(StrUtil.trim(entity.getTemplateId())); + entity.setSubjectId(StrUtil.trim(entity.getSubjectId())); + entity.setCertificateNo(StrUtil.trim(entity.getCertificateNo())); + entity.setCertificateImage(StrUtil.trim(entity.getCertificateImage())); + } + + + private void saveItems(String instanceId, List itemList) { + List safeList = itemList == null ? Collections.emptyList() : itemList; + long defaultSort = 1L; + for (CertificateInstanceItemReq itemReq : safeList) { + if (itemReq == null || StrUtil.isBlank(itemReq.getItemName())) { + continue; + } + CertificateInstanceItemEntity itemEntity = new CertificateInstanceItemEntity(); + itemEntity.setId(FtbUtil.getId()); + itemEntity.setInstanceId(instanceId); + itemEntity.setTemplateItemId(StrUtil.trim(itemReq.getTemplateItemId())); + itemEntity.setItemName(StrUtil.trim(itemReq.getItemName())); + itemEntity.setItemType(itemReq.getItemType()); + itemEntity.setItemValue(StrUtil.trim(itemReq.getItemValue())); + itemEntity.setSorts(itemReq.getSorts() == null ? defaultSort : itemReq.getSorts()); + itemEntity.setEnabledMark(0); + certificateInstanceItemMapper.insert(itemEntity); + defaultSort++; + } + } + + + private Date parseDate(String dateText, String fieldName) { + if (StrUtil.isBlank(dateText)) { + return null; + } + try { + return DateUtil.parseDate(StrUtil.trim(dateText)); + } catch (Exception e) { + throw new ServiceException(fieldName + "格式不正确,正确格式为yyyy-MM-dd"); + } + } + + + private Integer calculateStatus(Integer isLongTerm, Date expireDate) { + return calculateStatus(isLongTerm, expireDate, NEAR_EXPIRE_DAYS); + } + + + private Integer calculateStatus(Integer isLongTerm, Date expireDate, Integer nearExpireDays) { + if (isLongTerm != null && isLongTerm == 1) { + return STATUS_NORMAL; + } + if (expireDate == null) { + return STATUS_MISSING; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + if (target.before(today)) { + return STATUS_EXPIRED; + } + int limitDays = nearExpireDays == null || nearExpireDays < 0 ? 0 : nearExpireDays; + long daysDiff = DateUtil.betweenDay(today, target, false); + if (daysDiff <= limitDays) { + return STATUS_NEAR_EXPIRE; + } + return STATUS_NORMAL; + } + + + private Map queryCertificateInstanceMapBySubject(List subjectIds, Integer subjectType, + String certificateType, String templateId) { + if (CollUtil.isEmpty(subjectIds) || subjectType == null || StrUtil.isBlank(certificateType)) { + return Collections.emptyMap(); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + queryWrapper.eq(CertificateInstanceEntity::getCertificateType, StrUtil.trim(certificateType)); + queryWrapper.eq(CertificateInstanceEntity::getSubjectType, subjectType); + queryWrapper.in(CertificateInstanceEntity::getSubjectId, subjectIds); + if (StrUtil.isNotBlank(templateId)) { + queryWrapper.eq(CertificateInstanceEntity::getTemplateId, StrUtil.trim(templateId)); + } + queryWrapper.orderByDesc(CertificateInstanceEntity::getLastModifyTime); + queryWrapper.orderByDesc(CertificateInstanceEntity::getCreatorTime); + + List instanceList = baseMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(instanceList)) { + return Collections.emptyMap(); + } + + Map result = new HashMap<>(); + for (CertificateInstanceEntity entity : instanceList) { + if (entity == null || StrUtil.isBlank(entity.getSubjectId())) { + continue; + } + result.putIfAbsent(StrUtil.trim(entity.getSubjectId()), entity); + } + return result; + } + + + private int queryStoreCustomTemplateReminderDays(String templateId) { + if (StrUtil.isBlank(templateId)) { + return NEAR_EXPIRE_DAYS; + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + queryWrapper.eq(StoreCertificatePhotoEntity::getId, StrUtil.trim(templateId)); + + StoreCertificatePhotoEntity templateEntity = storeCertificatePhotoMapper.selectOne(queryWrapper); + if (templateEntity == null || templateEntity.getExpiryReminderDays() == null || templateEntity.getExpiryReminderDays() < 0) { + return NEAR_EXPIRE_DAYS; + } + return templateEntity.getExpiryReminderDays(); + } + + + private CertificateStoreCustomStatusPieVO buildPieItem(Integer status, String statusName, Integer count, Integer total) { + CertificateStoreCustomStatusPieVO pieVO = new CertificateStoreCustomStatusPieVO(); + pieVO.setStatus(status); + pieVO.setStatusName(statusName); + pieVO.setCount(count == null ? 0 : count); + pieVO.setRatio(formatPercent(pieVO.getCount(), total == null ? 0 : total)); + return pieVO; + } + + + private BigDecimal formatPercent(int part, int total) { + if (part <= 0 || total <= 0) { + return BigDecimal.ZERO; + } + return BigDecimal.valueOf(part) + .multiply(BigDecimal.valueOf(100)) + .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP); + } + + + private CertificateStoreDashboardVO buildEmptyDashboard() { + CertificateStoreDashboardVO result = new CertificateStoreDashboardVO(); + List pieList = new ArrayList<>(); + pieList.add(buildPieItem(BOARD_STATUS_MISSING, "缺失", 0, 0)); + pieList.add(buildPieItem(BOARD_STATUS_NEAR_EXPIRE, "临期", 0, 0)); + pieList.add(buildPieItem(BOARD_STATUS_EXPIRED, "过期", 0, 0)); + pieList.add(buildPieItem(BOARD_STATUS_NORMAL, "正常", 0, 0)); + result.setPieList(pieList); + return result; + } + + + private List buildHealthCertificateDashboardTableList(List storeIdFilter, + String storeTypeFilter) { + List statusStatList = buildHealthCertificateStatusStat(storeIdFilter,storeTypeFilter); + if (CollectionUtils.isEmpty(statusStatList)) { + return Collections.emptyList(); + } + + Map storeCounterMap = new LinkedHashMap<>(); + Set orgIds = new HashSet<>(); + for (CertificateHealthDashboardStatusStatVO row : statusStatList) { + if (row == null) { + continue; + } + String storeId = StrUtil.trim(row.getStoreId()); + if (StrUtil.isBlank(storeId)) { + continue; + } + String storeName = StrUtil.trim(row.getStoreName()); + if (StringUtil.isBlank(storeName)) { + orgIds.add(storeId); + } + HealthDashboardCounter counter = storeCounterMap.computeIfAbsent(storeId, v -> new HealthDashboardCounter(storeId, storeName)); + int status = row.getStatus() == null ? 0 : row.getStatus(); + int count = row.getTotalCount() == null ? 0 : row.getTotalCount(); + counter.addStatusCount(status, count); + } + + Map organizeGeneralDetailVOMap = organizationHelper.buildOrganizeGenerals(orgIds); + List tableList = new ArrayList<>(); + for (HealthDashboardCounter counter : storeCounterMap.values()) { + String storeId = counter.getStoreId(); + String storeName = counter.getStoreName(); + if (StringUtil.isBlank(storeName)) { + OrganizeGeneralDetailVO organizeGeneralDetailVO = organizeGeneralDetailVOMap.get(storeId); + if (Objects.nonNull(organizeGeneralDetailVO)) { + storeName = organizeGeneralDetailVO.getName(); + } + } + + int storeTotalCount = counter.getTotalCount(); + int missingCount = counter.getMissingCount(); + int nearExpireCount = counter.getNearExpireCount(); + int expiredCount = counter.getExpiredCount(); + int normalCount = counter.getNormalCount(); + + CertificateStoreCustomStatusTableVO tableVO = new CertificateStoreCustomStatusTableVO(); + tableVO.setStoreId(storeId); + tableVO.setStoreName(storeName); + tableVO.setTotalCount(storeTotalCount); + tableVO.setMissingCount(missingCount); + tableVO.setMissingRatio(formatPercent(missingCount, storeTotalCount)); + tableVO.setNearExpireCount(nearExpireCount); + tableVO.setNearExpireRatio(formatPercent(nearExpireCount, storeTotalCount)); + tableVO.setExpiredCount(expiredCount); + tableVO.setExpiredRatio(formatPercent(expiredCount, storeTotalCount)); + tableVO.setNormalCount(normalCount); + tableVO.setNormalRatio(formatPercent(normalCount, storeTotalCount)); + tableList.add(tableVO); + } + tableList.sort(Comparator.comparing(CertificateStoreCustomStatusTableVO::getStoreName, Comparator.nullsLast(String::compareTo))); + return tableList; + } + + private PageInfo pageByMemory(List allData, long current, long pageSize) { + List safeData = allData == null ? Collections.emptyList() : allData; + int total = safeData.size(); + int fromIndex = (int) ((current - 1) * pageSize); + int toIndex = (int) Math.min(fromIndex + pageSize, total); + List pageList; + if (fromIndex >= total || fromIndex < 0) { + pageList = Collections.emptyList(); + } else { + pageList = new ArrayList<>(safeData.subList(fromIndex, toIndex)); + } + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(pageList); + pageInfo.setPageNum((int) current); + pageInfo.setPageSize((int) pageSize); + pageInfo.setTotal(total); + return pageInfo; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageApiServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageApiServiceImpl.java new file mode 100644 index 0000000..5c9e4fb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageApiServiceImpl.java @@ -0,0 +1,574 @@ +package jnpf.certificate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.certificate.mapper.CertificateBusinessLicenseExtMapper; +import jnpf.certificate.mapper.CertificateHygieneLicenseExtMapper; +import jnpf.certificate.mapper.CertificateInstanceItemMapper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.certificate.service.CertificateManageApiService; +import jnpf.model.certificate.po.CertificateBusinessLicenseExtEntity; +import jnpf.model.certificate.po.CertificateHygieneLicenseExtEntity; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.po.CertificateInstanceItemEntity; +import jnpf.model.certificate.vo.CertificateOrganizeBusinessLicenseVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.util.FtbUtil; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 组织营业执照开放接口服务实现。 + */ +@Slf4j +@Service +public class CertificateManageApiServiceImpl implements CertificateManageApiService { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_EXPIRED = 2; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_NORMAL = 4; + private static final int SUBJECT_TYPE_ORGANIZE = 2; + private static final int SUBJECT_TYPE_STORE = 3; + private static final int IS_LONG_TERM_NO = 0; + private static final int NEAR_EXPIRE_DAYS = 30; + + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private CertificateBusinessLicenseExtMapper certificateBusinessLicenseExtMapper; + @Autowired + private CertificateHygieneLicenseExtMapper certificateHygieneLicenseExtMapper; + @Autowired + private CertificateInstanceItemMapper certificateInstanceItemMapper; + + /** + * 根据组织ID查询营业执照信息。 + */ + @Override + public CertificateOrganizeBusinessLicenseVO queryBusinessLicense(String organizeId) { + ServiceException.isTrue(StrUtil.isNotBlank(organizeId), "组织ID不能为空"); + return buildOrganizeBusinessLicenseVO(StrUtil.trim(organizeId)); + } + + /** + * 根据组织ID列表批量查询营业执照信息。 + */ + @Override + public List queryBusinessLicenseBatch(Collection organizeIds) { + if (CollUtil.isEmpty(organizeIds)) { + return Collections.emptyList(); + } + List targetIds = organizeIds.stream() + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(targetIds)) { + return Collections.emptyList(); + } + + Map instanceMap = queryBusinessInstanceMap(targetIds); + Map extMap = queryBusinessExtMap(instanceMap.values()); + + List result = new ArrayList<>(targetIds.size()); + for (String organizeId : targetIds) { + CertificateInstanceEntity instance = instanceMap.get(organizeId); + CertificateBusinessLicenseExtEntity ext = instance == null ? null : extMap.get(instance.getId()); + result.add(convertToVO(organizeId, instance, ext)); + } + return result; + } + + /** + * 保存组织营业执照信息。 + */ + @Override + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + public void saveBusinessLicense(CertificateOrganizeBusinessLicenseVO req) { + ServiceException.notNull(req, "请求参数不能为空"); + String organizeId = StrUtil.trim(req.getOrganizeId()); + ServiceException.isTrue(StrUtil.isNotBlank(organizeId), "组织ID不能为空"); + + Date establishDate = parseDate(req.getEstablishDate(), "成立时间"); + Date issueDate = parseDate(req.getIssueDate(), "发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + Integer isLongTerm = req.getIsLongTerm() == null ? 0 : req.getIsLongTerm(); + if (Integer.valueOf(1).equals(isLongTerm)) { + expireDate = null; + } + + String certificateType = CertificateTypeEnum.BUSINESS_LICENSE.getType(); + String certificateImage = trimToNull(req.getCertificateImage()); + int status = calculateStatus(isLongTerm, expireDate); + + String instanceId = upsertBusinessInstance( + organizeId, + certificateType, + certificateImage, + issueDate, + expireDate, + isLongTerm, + status + ); + if(Objects.isNull(instanceId)){ + return; + } + upsertBusinessExt(instanceId, req, establishDate); + } + + /** + * 初始化门店默认缺失证照(营业执照、食品经营许可证)。 + */ + @Override + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + public void initStoreDefaultCertificates(String storeId) { + String targetStoreId = StrUtil.trim(storeId); + ServiceException.isTrue(StrUtil.isNotBlank(targetStoreId), "门店ID不能为空"); + + List defaultTypes = Arrays.asList( + CertificateTypeEnum.BUSINESS_LICENSE.getType(), + CertificateTypeEnum.HYGIENE_LICENSE.getType() + ); + for (String certificateType : defaultTypes) { + initStoreDefaultCertificate(targetStoreId, certificateType); + } + } + + /** + * 根据组织ID删除该主体下全部证照信息。 + */ + @Override + @GlobalTransactional + @Transactional(rollbackFor = Exception.class) + public void deleteBusinessLicense(String organizeId, String loginUserId) { + String targetOrganizeId = StrUtil.trim(organizeId); + ServiceException.isTrue(StrUtil.isNotBlank(targetOrganizeId), "组织ID不能为空"); + + Date now = new Date(); + LambdaUpdateWrapper instanceUpdateWrapper = Wrappers.lambdaUpdate(); + instanceUpdateWrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + instanceUpdateWrapper.eq(CertificateInstanceEntity::getSubjectId, targetOrganizeId); + instanceUpdateWrapper.set(CertificateInstanceEntity::getEnabledMark, 1); + instanceUpdateWrapper.set(CertificateInstanceEntity::getLastModifyTime, now); + if(StringUtil.isNotBlank(loginUserId)){ + instanceUpdateWrapper.set(CertificateInstanceEntity::getLastModifyUserId, loginUserId); + } + certificateInstanceMapper.update(null, instanceUpdateWrapper); + + LambdaQueryWrapper instanceQueryWrapper = Wrappers.lambdaQuery(); + instanceQueryWrapper.eq(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_ORGANIZE); + instanceQueryWrapper.eq(CertificateInstanceEntity::getSubjectId, targetOrganizeId); + List allBusinessInstances = certificateInstanceMapper.selectList(instanceQueryWrapper); + if (CollUtil.isEmpty(allBusinessInstances)) { + return; + } + + List instanceIds = allBusinessInstances.stream() + .map(CertificateInstanceEntity::getId) + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .distinct() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(instanceIds)) { + return; + } + + LambdaUpdateWrapper extUpdateWrapper = Wrappers.lambdaUpdate(); + extUpdateWrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + extUpdateWrapper.in(CertificateBusinessLicenseExtEntity::getInstanceId, instanceIds); + extUpdateWrapper.set(CertificateBusinessLicenseExtEntity::getEnabledMark, 1); + extUpdateWrapper.set(CertificateBusinessLicenseExtEntity::getLastModifyTime, now); + extUpdateWrapper.set(CertificateBusinessLicenseExtEntity::getLastModifyUserId, loginUserId); + certificateBusinessLicenseExtMapper.update(null, extUpdateWrapper); + + LambdaUpdateWrapper hygieneExtUpdateWrapper = Wrappers.lambdaUpdate(); + hygieneExtUpdateWrapper.eq(CertificateHygieneLicenseExtEntity::getEnabledMark, 0); + hygieneExtUpdateWrapper.in(CertificateHygieneLicenseExtEntity::getInstanceId, instanceIds); + hygieneExtUpdateWrapper.set(CertificateHygieneLicenseExtEntity::getEnabledMark, 1); + hygieneExtUpdateWrapper.set(CertificateHygieneLicenseExtEntity::getLastModifyTime, now); + hygieneExtUpdateWrapper.set(CertificateHygieneLicenseExtEntity::getLastModifyUserId, loginUserId); + certificateHygieneLicenseExtMapper.update(null, hygieneExtUpdateWrapper); + + LambdaUpdateWrapper itemUpdateWrapper = Wrappers.lambdaUpdate(); + itemUpdateWrapper.eq(CertificateInstanceItemEntity::getEnabledMark, 0); + itemUpdateWrapper.in(CertificateInstanceItemEntity::getInstanceId, instanceIds); + itemUpdateWrapper.set(CertificateInstanceItemEntity::getEnabledMark, 1); + itemUpdateWrapper.set(CertificateInstanceItemEntity::getLastModifyTime, now); + itemUpdateWrapper.set(CertificateInstanceItemEntity::getLastModifyUserId, loginUserId); + certificateInstanceItemMapper.update(null, itemUpdateWrapper); + } + + /** + * 查询并组装组织营业执照返回对象。 + */ + private CertificateOrganizeBusinessLicenseVO buildOrganizeBusinessLicenseVO(String organizeId) { + CertificateInstanceEntity instance = queryLatestBusinessInstance(organizeId); + CertificateBusinessLicenseExtEntity ext = instance == null ? null : queryLatestBusinessExt(instance.getId()); + return convertToVO(organizeId, instance, ext); + } + + /** + * 批量查询营业执照主表数据映射。 + */ + private Map queryBusinessInstanceMap(List organizeIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + wrapper.eq(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_ORGANIZE); + wrapper.eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType()); + wrapper.in(CertificateInstanceEntity::getSubjectId, organizeIds); + wrapper.orderByDesc(CertificateInstanceEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateInstanceEntity::getCreatorTime); + List instanceList = certificateInstanceMapper.selectList(wrapper); + if (CollUtil.isEmpty(instanceList)) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap<>(); + for (CertificateInstanceEntity entity : instanceList) { + if (entity == null || StrUtil.isBlank(entity.getSubjectId())) { + continue; + } + String subjectId = StrUtil.trim(entity.getSubjectId()); + result.putIfAbsent(subjectId, entity); + } + return result; + } + + /** + * 批量查询营业执照扩展表数据映射。 + */ + private Map queryBusinessExtMap(Iterable instances) { + List instanceIds = new ArrayList<>(); + for (CertificateInstanceEntity entity : instances) { + if (entity != null && StrUtil.isNotBlank(entity.getId())) { + instanceIds.add(StrUtil.trim(entity.getId())); + } + } + if (CollUtil.isEmpty(instanceIds)) { + return Collections.emptyMap(); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + wrapper.in(CertificateBusinessLicenseExtEntity::getInstanceId, instanceIds); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getCreatorTime); + List extList = certificateBusinessLicenseExtMapper.selectList(wrapper); + if (CollUtil.isEmpty(extList)) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap<>(); + for (CertificateBusinessLicenseExtEntity ext : extList) { + if (ext == null || StrUtil.isBlank(ext.getInstanceId())) { + continue; + } + result.putIfAbsent(StrUtil.trim(ext.getInstanceId()), ext); + } + return result; + } + + /** + * 查询组织最新营业执照主表记录。 + */ + private CertificateInstanceEntity queryLatestBusinessInstance(String organizeId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + wrapper.eq(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_ORGANIZE); + wrapper.eq(CertificateInstanceEntity::getSubjectId, organizeId); + wrapper.eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.BUSINESS_LICENSE.getType()); + wrapper.orderByDesc(CertificateInstanceEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateInstanceEntity::getCreatorTime); + List list = certificateInstanceMapper.selectList(wrapper); + return CollUtil.isEmpty(list) ? null : list.get(0); + } + + /** + * 查询实例最新营业执照扩展记录。 + */ + private CertificateBusinessLicenseExtEntity queryLatestBusinessExt(String instanceId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + wrapper.eq(CertificateBusinessLicenseExtEntity::getInstanceId, instanceId); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getCreatorTime); + List list = certificateBusinessLicenseExtMapper.selectList(wrapper); + return CollUtil.isEmpty(list) ? null : list.get(0); + } + + /** + * 保存或更新营业执照主表信息。 + */ + private String upsertBusinessInstance(String organizeId, + String certificateType, + String certificateImage, + Date issueDate, + Date expireDate, + Integer isLongTerm, + Integer status) { + LambdaUpdateWrapper updateWrapper = buildBusinessInstanceUpdateWrapper( + organizeId, certificateType, certificateImage, issueDate, expireDate, isLongTerm, status + ); + int updateRows = certificateInstanceMapper.update(null, updateWrapper); + if (updateRows > 0) { + CertificateInstanceEntity latest = queryLatestBusinessInstance(organizeId); + ServiceException.notNull(latest, "营业执照实例更新成功但未查询到数据"); + return latest.getId(); + } + + CertificateInstanceEntity insertEntity = new CertificateInstanceEntity(); + insertEntity.setId(FtbUtil.getId()); + insertEntity.setCertificateType(certificateType); + insertEntity.setSubjectType(SUBJECT_TYPE_ORGANIZE); + insertEntity.setSubjectId(organizeId); + insertEntity.setCertificateImage(certificateImage); + insertEntity.setIssueDate(issueDate); + insertEntity.setExpireDate(expireDate); + if(Objects.nonNull(expireDate)){ + insertEntity.setExpireTimestamp(expireDate.getTime()); + } + insertEntity.setIsLongTerm(isLongTerm); + insertEntity.setStatus(status); + insertEntity.setEnabledMark(0); + try { + certificateInstanceMapper.insert(insertEntity); + return insertEntity.getId(); + } catch (DuplicateKeyException e) { + log.warn("营业执照实例插入出现重复键,执行重试更新。organizeId={}, certificateType={}", organizeId, certificateType, e); + LambdaUpdateWrapper retryWrapper = buildBusinessInstanceUpdateWrapper( + organizeId, certificateType, certificateImage, issueDate, expireDate, isLongTerm, status + ); + int retryRows = certificateInstanceMapper.update(null, retryWrapper); + if(retryRows <= 0){ + log.error("upsertBusinessInstance fail.insertEntity:{}",insertEntity); + return null; + } + CertificateInstanceEntity latest = queryLatestBusinessInstance(organizeId); + ServiceException.notNull(latest, "营业执照实例重试更新成功但未查询到数据"); + return latest.getId(); + } + } + + /** + * 初始化单个门店默认证照记录。 + */ + private void initStoreDefaultCertificate(String storeId, String certificateType) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + queryWrapper.eq(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_STORE); + queryWrapper.eq(CertificateInstanceEntity::getSubjectId, storeId); + queryWrapper.eq(CertificateInstanceEntity::getCertificateType, certificateType); + queryWrapper.last("limit 1"); + CertificateInstanceEntity existed = certificateInstanceMapper.selectOne(queryWrapper); + if (existed != null) { + return; + } + + CertificateInstanceEntity insertEntity = new CertificateInstanceEntity(); + insertEntity.setId(FtbUtil.getId()); + insertEntity.setCertificateType(certificateType); + insertEntity.setSubjectType(SUBJECT_TYPE_STORE); + insertEntity.setSubjectId(storeId); + insertEntity.setIsLongTerm(IS_LONG_TERM_NO); + insertEntity.setStatus(STATUS_MISSING); + insertEntity.setEnabledMark(0); + try { + certificateInstanceMapper.insert(insertEntity); + } catch (DuplicateKeyException e) { + log.warn("初始化门店默认证照出现重复键,忽略。storeId={}, certificateType={}", storeId, certificateType, e); + } + } + + /** + * 保存或更新营业执照扩展表信息。 + */ + private void upsertBusinessExt(String instanceId, CertificateOrganizeBusinessLicenseVO req, Date establishDate) { + String companyName = trimToNull(req.getCompanyName()); + String companyAddress = trimToNull(req.getCompanyAddress()); + String legalRepresentative = trimToNull(req.getLegalRepresentative()); + + LambdaUpdateWrapper updateWrapper = buildBusinessExtUpdateWrapper( + instanceId, companyName, companyAddress, legalRepresentative, establishDate + ); + int updateRows = certificateBusinessLicenseExtMapper.update(null, updateWrapper); + if (updateRows > 0) { + return; + } + + CertificateBusinessLicenseExtEntity insertEntity = new CertificateBusinessLicenseExtEntity(); + insertEntity.setId(FtbUtil.getId()); + insertEntity.setInstanceId(instanceId); + insertEntity.setCompanyName(companyName); + insertEntity.setCompanyAddress(companyAddress); + insertEntity.setLegalRepresentative(legalRepresentative); + insertEntity.setEstablishDate(establishDate); + insertEntity.setEnabledMark(0); + try { + certificateBusinessLicenseExtMapper.insert(insertEntity); + } catch (DuplicateKeyException e) { + log.warn("营业执照扩展插入出现重复键,执行重试更新。instanceId={}", instanceId, e); + LambdaUpdateWrapper retryWrapper = buildBusinessExtUpdateWrapper( + instanceId, companyName, companyAddress, legalRepresentative, establishDate + ); + int retryRows = certificateBusinessLicenseExtMapper.update(null, retryWrapper); + ServiceException.isTrue(retryRows > 0, "营业执照扩展保存失败,请稍后重试"); + } + } + + /** + * 组装营业执照返回对象。 + */ + private CertificateOrganizeBusinessLicenseVO convertToVO(String organizeId, + CertificateInstanceEntity instance, + CertificateBusinessLicenseExtEntity ext) { + CertificateOrganizeBusinessLicenseVO vo = new CertificateOrganizeBusinessLicenseVO(); + vo.setOrganizeId(organizeId); + vo.setIsLongTerm(instance == null || instance.getIsLongTerm() == null ? 0 : instance.getIsLongTerm()); + if (instance != null) { + vo.setCertificateImage(instance.getCertificateImage()); + vo.setIssueDate(formatDate(instance.getIssueDate())); + vo.setExpireDate(formatDate(instance.getExpireDate())); + vo.setBusinessTerm(buildBusinessTerm(instance.getIssueDate(), instance.getExpireDate(), vo.getIsLongTerm())); + } else { + vo.setBusinessTerm("[]"); + } + if (ext != null) { + vo.setCompanyName(ext.getCompanyName()); + vo.setLegalRepresentative(ext.getLegalRepresentative()); + vo.setCompanyAddress(ext.getCompanyAddress()); + vo.setEstablishDate(formatDate(ext.getEstablishDate())); + } + return vo; + } + + /** + * 计算证照状态。 + */ + private Integer calculateStatus(Integer isLongTerm, Date expireDate) { + if (Integer.valueOf(1).equals(isLongTerm)) { + return STATUS_NORMAL; + } + if (expireDate == null) { + return STATUS_MISSING; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + if (target.before(today)) { + return STATUS_EXPIRED; + } + long daysDiff = DateUtil.betweenDay(today, target, false); + if (daysDiff <= NEAR_EXPIRE_DAYS) { + return STATUS_NEAR_EXPIRE; + } + return STATUS_NORMAL; + } + + /** + * 解析日期字符串。 + */ + private Date parseDate(String dateText, String fieldName) { + if (StrUtil.isBlank(dateText)) { + return null; + } + int strLen = dateText.length(); + if(StringUtil.isNumeric(dateText) && (strLen == 10 || strLen == 13)){ + long date = Long.parseLong(dateText); + if(dateText.length() == 10){ + date = date * 1000; + } + return new Date(date); + } + try { + return DateUtil.parseDate(StrUtil.trim(dateText)); + } catch (Exception e) { + throw new ServiceException(fieldName + "格式不正确,正确格式为yyyy-MM-dd"); + } + } + + /** + * 格式化日期。 + */ + private String formatDate(Date date) { + return date == null ? null : DateUtil.formatDate(date); + } + + /** + * 组装营业期限展示文案。 + */ + private String buildBusinessTerm(Date issueDate, Date expireDate, Integer isLongTerm) { + if (Integer.valueOf(1).equals(isLongTerm)) { + return "[]"; + } + if (expireDate == null) { + return "[]"; + } + String issueText = issueDate == null ? "" : DateUtil.formatDate(issueDate); + return "[\""+issueText + "\",\"" + DateUtil.formatDate(expireDate)+"\"]"; + } + + /** + * 构建营业执照主表更新条件。 + */ + private LambdaUpdateWrapper buildBusinessInstanceUpdateWrapper(String organizeId, + String certificateType, + String certificateImage, + Date issueDate, + Date expireDate, + Integer isLongTerm, + Integer status) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CertificateInstanceEntity::getSubjectId, organizeId); + updateWrapper.eq(CertificateInstanceEntity::getCertificateType, certificateType); + updateWrapper.set(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_ORGANIZE); + updateWrapper.set(CertificateInstanceEntity::getEnabledMark, 0); + updateWrapper.set(CertificateInstanceEntity::getCertificateImage, certificateImage); + updateWrapper.set(CertificateInstanceEntity::getIssueDate, issueDate); + updateWrapper.set(CertificateInstanceEntity::getExpireDate, expireDate); + if(Objects.nonNull(expireDate)){ + updateWrapper.set(CertificateInstanceEntity::getExpireTimestamp, expireDate.getTime()); + } + updateWrapper.set(CertificateInstanceEntity::getIsLongTerm, isLongTerm); + updateWrapper.set(CertificateInstanceEntity::getStatus, status); + return updateWrapper; + } + + /** + * 构建营业执照扩展表更新条件。 + */ + private LambdaUpdateWrapper buildBusinessExtUpdateWrapper(String instanceId, + String companyName, + String companyAddress, + String legalRepresentative, + Date establishDate) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + updateWrapper.eq(CertificateBusinessLicenseExtEntity::getInstanceId, instanceId); + updateWrapper.set(CertificateBusinessLicenseExtEntity::getCompanyName, companyName); + updateWrapper.set(CertificateBusinessLicenseExtEntity::getCompanyAddress, companyAddress); + updateWrapper.set(CertificateBusinessLicenseExtEntity::getLegalRepresentative, legalRepresentative); + updateWrapper.set(CertificateBusinessLicenseExtEntity::getEstablishDate, establishDate); + return updateWrapper; + } + + /** + * 去首尾空格,空串返回 null。 + */ + private String trimToNull(String text) { + String value = StrUtil.trim(text); + return StrUtil.isBlank(value) ? null : value; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageServiceImpl.java new file mode 100644 index 0000000..e5439cb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateManageServiceImpl.java @@ -0,0 +1,481 @@ +package jnpf.certificate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Assert; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.certificate.mapper.CertificateBusinessLicenseExtMapper; +import jnpf.certificate.mapper.CertificateHygieneLicenseExtMapper; +import jnpf.certificate.mapper.CertificateInstanceItemMapper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.certificate.service.CertificateManageService; +import jnpf.model.certificate.po.CertificateBusinessLicenseExtEntity; +import jnpf.model.certificate.po.CertificateHygieneLicenseExtEntity; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.po.CertificateInstanceItemEntity; +import jnpf.model.certificate.req.CertificateInstanceItemReq; +import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHealthCertificateUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq; +import jnpf.model.certificate.vo.CertificateInstanceItemVO; +import jnpf.model.certificate.vo.app.CertificateAppBusinessLicenseDetailVO; +import jnpf.model.certificate.vo.app.CertificateAppCertificateDetailVO; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.certificate.vo.app.CertificateAppHygieneLicenseDetailVO; +import jnpf.model.certificate.vo.app.CertificateAppStoreCustomDetailVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.util.FtbUtil; +import jnpf.util.ServiceException; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * App端证照管理服务实现。 + */ +@Service +public class CertificateManageServiceImpl implements CertificateManageService { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_EXPIRED = 2; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_NORMAL = 4; + private static final int NEAR_EXPIRE_DAYS = 30; + + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private CertificateBusinessLicenseExtMapper certificateBusinessLicenseExtMapper; + @Autowired + private CertificateHygieneLicenseExtMapper certificateHygieneLicenseExtMapper; + @Autowired + private CertificateInstanceItemMapper certificateInstanceItemMapper; + + /** + * 根据证照实例ID查询详情,按证照类型返回不同明细对象。 + */ + @Override + public CertificateAppCertificateDetailVO queryInfo(String certificateInstanceId) { + ServiceException.isTrue(StrUtil.isNotBlank(certificateInstanceId), "证照实例ID不能为空"); + CertificateInstanceEntity entity = queryActiveEntity(StrUtil.trim(certificateInstanceId)); + Optional certificateTypeEnumOptional = CertificateTypeEnum.getByType(entity.getCertificateType()); + ServiceException.isTrue(certificateTypeEnumOptional.isPresent(), "不支持的证照类型"); + CertificateTypeEnum certificateTypeEnum = certificateTypeEnumOptional.get(); + + Integer status = calculateStatus(entity.getIsLongTerm(), entity.getExpireDate()); + CertificateAppCertificateDetailVO result = new CertificateAppCertificateDetailVO(); + result.setCertificateInstanceId(entity.getId()); + result.setCertificateType(certificateTypeEnum.getType()); + result.setStatus(status); + result.setStatusName(resolveStatusName(status)); + + if (CertificateTypeEnum.HEALTH_CERTIFICATE.equals(certificateTypeEnum)) { + result.setDetail(buildHealthDetail(entity)); + return result; + } + if (CertificateTypeEnum.BUSINESS_LICENSE.equals(certificateTypeEnum)) { + result.setDetail(buildBusinessLicenseDetail(entity)); + return result; + } + if (CertificateTypeEnum.HYGIENE_LICENSE.equals(certificateTypeEnum)) { + result.setDetail(buildHygieneLicenseDetail(entity)); + return result; + } + result.setDetail(buildStoreCustomDetail(entity)); + return result; + } + + /** + * 更新健康证数据。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateHealthCertificate(CertificateAppHealthCertificateUpdateReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + CertificateInstanceEntity entity = queryAndAssertType(req.getCertificateInstanceId(), CertificateTypeEnum.HEALTH_CERTIFICATE, "健康证"); + + Date issueDate = parseDate(req.getIssueDate(), "发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + + //原生app增量更新,可能不传值,则使用数据库值 + if(Objects.isNull(issueDate)){ + issueDate = entity.getIssueDate(); + } + if(Objects.isNull(expireDate)){ + expireDate = entity.getExpireDate(); + } + if(Objects.nonNull(issueDate) && Objects.nonNull(expireDate) && issueDate.after(expireDate)){ + throw new ServiceException("发证日期不能大于到期日期"); + } + + entity.setCertificateImage(trimToNull(req.getCertificateImage())); + entity.setIssueDate(issueDate); + entity.setExpireDate(expireDate); + entity.setIsLongTerm(0); + entity.setStatus(calculateStatus(entity.getIsLongTerm(), entity.getExpireDate())); + int r = certificateInstanceMapper.updateById(entity); + if(r <= 0){ + return; + } + + } + + /** + * 更新营业执照数据,并同步营业执照扩展表。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateBusinessLicense(CertificateAppBusinessLicenseUpdateReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + CertificateInstanceEntity entity = queryAndAssertType(req.getCertificateInstanceId(), CertificateTypeEnum.BUSINESS_LICENSE, "营业执照"); + + Date establishDate = parseDate(req.getEstablishDate(), "成立时间"); + Date issueDate = parseDate(req.getIssueDate(), "发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + Integer isLongTerm = req.getIsLongTerm() == null ? 0 : req.getIsLongTerm(); + if (Integer.valueOf(1).equals(isLongTerm)) { + expireDate = null; + } + + entity.setCertificateImage(trimToNull(req.getCertificateImage())); + entity.setCertificateNo(trimToNull(req.getCertificateNo())); + entity.setIssueDate(issueDate); + entity.setExpireDate(expireDate); + entity.setIsLongTerm(isLongTerm); + entity.setStatus(calculateStatus(entity.getIsLongTerm(), entity.getExpireDate())); + certificateInstanceMapper.updateById(entity); + + saveBusinessLicenseExt(entity.getId(), req, establishDate); + } + + /** + * 更新食品经营许可证数据,并同步食品许可扩展表。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateHygieneLicense(CertificateAppHygieneLicenseUpdateReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + CertificateInstanceEntity entity = queryAndAssertType(req.getCertificateInstanceId(), CertificateTypeEnum.HYGIENE_LICENSE, "食品经营许可证"); + + Date issueDate = parseDate(req.getIssueDate(), "发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + + entity.setCertificateImage(trimToNull(req.getCertificateImage())); + entity.setIssueDate(issueDate); + entity.setExpireDate(expireDate); + entity.setIsLongTerm(0); + entity.setStatus(calculateStatus(entity.getIsLongTerm(), entity.getExpireDate())); + certificateInstanceMapper.updateById(entity); + + saveHygieneLicenseExt(entity.getId(), req); + } + + /** + * 更新门店自定义证照数据,并重写自定义字段明细。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateStoreCustomCertificate(CertificateAppStoreCustomUpdateReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + CertificateInstanceEntity entity = queryAndAssertType(req.getCertificateInstanceId(), CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE, "门店自定义证照"); + + Date expireDate = parseDate(req.getExpireDate(), "到期日期"); + entity.setCertificateImage(""); + entity.setIssueDate(null); + entity.setExpireDate(expireDate); + entity.setIsLongTerm(0); + entity.setStatus(calculateStatus(entity.getIsLongTerm(), entity.getExpireDate())); + certificateInstanceMapper.updateById(entity); + + deleteAndSaveStoreCustomItems(entity.getId(), req.getItemList()); + } + + /** + * 构建健康证明细返回对象。 + */ + private HealthCertificateDetailVO buildHealthDetail(CertificateInstanceEntity entity) { + HealthCertificateDetailVO detailVO = new HealthCertificateDetailVO(); + detailVO.setCertificateImage(entity.getCertificateImage()); + detailVO.setIssueDate(entity.getIssueDate()); + detailVO.setExpireDate(entity.getExpireDate()); + detailVO.setStatus(entity.getStatus()); + return detailVO; + } + + /** + * 构建营业执照明细返回对象。 + */ + private CertificateAppBusinessLicenseDetailVO buildBusinessLicenseDetail(CertificateInstanceEntity entity) { + CertificateBusinessLicenseExtEntity extEntity = queryBusinessLicenseExt(entity.getId()); + CertificateAppBusinessLicenseDetailVO detailVO = new CertificateAppBusinessLicenseDetailVO(); + detailVO.setCertificateImage(entity.getCertificateImage()); + detailVO.setCertificateNo(entity.getCertificateNo()); + detailVO.setIssueDate(entity.getIssueDate()); + detailVO.setExpireDate(entity.getExpireDate()); + detailVO.setIsLongTerm(entity.getIsLongTerm()); + detailVO.setStatus(entity.getStatus()); + if (extEntity != null) { + detailVO.setCompanyName(extEntity.getCompanyName()); + detailVO.setLegalRepresentative(extEntity.getLegalRepresentative()); + detailVO.setEstablishDate(extEntity.getEstablishDate()); + detailVO.setBusinessScope(extEntity.getBusinessScope()); + detailVO.setAddress(extEntity.getCompanyAddress()); + } + return detailVO; + } + + /** + * 构建食品经营许可证明细返回对象。 + */ + private CertificateAppHygieneLicenseDetailVO buildHygieneLicenseDetail(CertificateInstanceEntity entity) { + CertificateHygieneLicenseExtEntity extEntity = queryHygieneLicenseExt(entity.getId()); + CertificateAppHygieneLicenseDetailVO detailVO = new CertificateAppHygieneLicenseDetailVO(); + detailVO.setCertificateImage(entity.getCertificateImage()); + detailVO.setIssueDate(entity.getIssueDate()); + detailVO.setExpireDate(entity.getExpireDate()); + detailVO.setStatus(entity.getStatus()); + if (extEntity != null) { + detailVO.setBusinessProject(extEntity.getBusinessProject()); + } + return detailVO; + } + + /** + * 构建门店自定义证照明细返回对象。 + */ + private CertificateAppStoreCustomDetailVO buildStoreCustomDetail(CertificateInstanceEntity entity) { + CertificateAppStoreCustomDetailVO detailVO = new CertificateAppStoreCustomDetailVO(); + detailVO.setCertificateImage(entity.getCertificateImage()); + detailVO.setExpireDate(entity.getExpireDate()); + + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.eq(CertificateInstanceItemEntity::getInstanceId, entity.getId()); + itemWrapper.eq(CertificateInstanceItemEntity::getEnabledMark, 0); + itemWrapper.orderByAsc(CertificateInstanceItemEntity::getSorts); + List itemList = certificateInstanceItemMapper.selectList(itemWrapper); + if (CollUtil.isNotEmpty(itemList)) { + detailVO.setItemList(itemList.stream().map(CertificateInstanceItemVO::convert).collect(Collectors.toList())); + } + return detailVO; + } + + /** + * 保存营业执照扩展信息,不存在则新增,存在则更新。 + */ + private void saveBusinessLicenseExt(String instanceId, CertificateAppBusinessLicenseUpdateReq req, Date establishDate) { + CertificateBusinessLicenseExtEntity extEntity = queryBusinessLicenseExt(instanceId); + if (extEntity == null) { + extEntity = new CertificateBusinessLicenseExtEntity(); + extEntity.setId(FtbUtil.getId()); + extEntity.setInstanceId(instanceId); + extEntity.setEnabledMark(0); + fillBusinessLicenseExt(extEntity, req, establishDate); + certificateBusinessLicenseExtMapper.insert(extEntity); + return; + } + fillBusinessLicenseExt(extEntity, req, establishDate); + certificateBusinessLicenseExtMapper.update(null,new LambdaUpdateWrapper() + .eq(CertificateBusinessLicenseExtEntity::getId, extEntity.getId()) + .set(CertificateBusinessLicenseExtEntity::getCompanyName,extEntity.getCompanyName()) + .set(CertificateBusinessLicenseExtEntity::getCompanyAddress,extEntity.getCompanyAddress()) + .set(CertificateBusinessLicenseExtEntity::getBusinessScope,extEntity.getBusinessScope()) + .set(CertificateBusinessLicenseExtEntity::getEstablishDate,extEntity.getEstablishDate()) + .set(CertificateBusinessLicenseExtEntity::getLegalRepresentative,extEntity.getLegalRepresentative()) + .set(CertificateBusinessLicenseExtEntity::getLastModifyUserId, UserProvider.getLoginUserId()) + .set(CertificateBusinessLicenseExtEntity::getLastModifyTime,new Date())); + } + + /** + * 填充营业执照扩展字段。 + */ + private void fillBusinessLicenseExt(CertificateBusinessLicenseExtEntity extEntity, CertificateAppBusinessLicenseUpdateReq req, Date establishDate) { + extEntity.setCompanyName(trimToNull(req.getCompanyName())); + extEntity.setLegalRepresentative(trimToNull(req.getLegalRepresentative())); + extEntity.setEstablishDate(establishDate); + extEntity.setBusinessScope(trimToNull(req.getBusinessScope())); + extEntity.setCompanyAddress(req.getAddress()); + } + + /** + * 保存食品经营许可扩展信息,不存在则新增,存在则更新。 + */ + private void saveHygieneLicenseExt(String instanceId, CertificateAppHygieneLicenseUpdateReq req) { + CertificateHygieneLicenseExtEntity extEntity = queryHygieneLicenseExt(instanceId); + if (extEntity == null) { + extEntity = new CertificateHygieneLicenseExtEntity(); + extEntity.setId(FtbUtil.getId()); + extEntity.setInstanceId(instanceId); + extEntity.setEnabledMark(0); + extEntity.setBusinessProject(trimToNull(req.getBusinessProject())); + certificateHygieneLicenseExtMapper.insert(extEntity); + return; + } + extEntity.setBusinessProject(trimToNull(req.getBusinessProject())); + certificateHygieneLicenseExtMapper.update(null,new LambdaUpdateWrapper() + .eq(CertificateHygieneLicenseExtEntity::getId, extEntity.getId()) + .set(CertificateHygieneLicenseExtEntity::getBusinessProject,extEntity.getBusinessProject())); + } + + /** + * 删除旧的门店自定义明细并保存新的明细列表。 + */ + private void deleteAndSaveStoreCustomItems(String instanceId, List itemList) { + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(CertificateInstanceItemEntity::getInstanceId, instanceId); + certificateInstanceItemMapper.delete(deleteWrapper); + + List safeList = itemList == null ? Collections.emptyList() : itemList; + long defaultSort = 1L; + for (CertificateInstanceItemReq itemReq : safeList) { + if (itemReq == null || StrUtil.isBlank(itemReq.getItemName())) { + continue; + } + CertificateInstanceItemEntity itemEntity = new CertificateInstanceItemEntity(); + itemEntity.setId(FtbUtil.getId()); + itemEntity.setInstanceId(instanceId); + itemEntity.setTemplateItemId(trimToNull(itemReq.getTemplateItemId())); + itemEntity.setItemName(trimToNull(itemReq.getItemName())); + itemEntity.setItemType(itemReq.getItemType()); + itemEntity.setItemValue(trimToNull(itemReq.getItemValue())); + itemEntity.setSorts(itemReq.getSorts() == null ? defaultSort : itemReq.getSorts()); + itemEntity.setEnabledMark(0); + certificateInstanceItemMapper.insert(itemEntity); + defaultSort++; + } + } + + /** + * 根据实例ID查询营业执照扩展信息。 + */ + private CertificateBusinessLicenseExtEntity queryBusinessLicenseExt(String instanceId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateBusinessLicenseExtEntity::getInstanceId, instanceId); + wrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getCreatorTime); + List extList = certificateBusinessLicenseExtMapper.selectList(wrapper); + return CollUtil.isEmpty(extList) ? null : extList.get(0); + } + + /** + * 根据实例ID查询食品许可扩展信息。 + */ + private CertificateHygieneLicenseExtEntity queryHygieneLicenseExt(String instanceId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateHygieneLicenseExtEntity::getInstanceId, instanceId); + wrapper.eq(CertificateHygieneLicenseExtEntity::getEnabledMark, 0); + wrapper.orderByDesc(CertificateHygieneLicenseExtEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateHygieneLicenseExtEntity::getCreatorTime); + List extList = certificateHygieneLicenseExtMapper.selectList(wrapper); + return CollUtil.isEmpty(extList) ? null : extList.get(0); + } + + /** + * 查询有效证照实例。 + */ + private CertificateInstanceEntity queryActiveEntity(String id) { + CertificateInstanceEntity entity = certificateInstanceMapper.selectById(id); + ServiceException.notNull(entity, "证照数据不存在"); + ServiceException.isTrue(Integer.valueOf(0).equals(entity.getEnabledMark()), "证照数据不存在"); + return entity; + } + + /** + * 查询实例并校验证照类型。 + */ + private CertificateInstanceEntity queryAndAssertType(String certificateInstanceId, CertificateTypeEnum expectType, String typeName) { + ServiceException.isTrue(StrUtil.isNotBlank(certificateInstanceId), "证照实例ID不能为空"); + CertificateInstanceEntity entity = queryActiveEntity(StrUtil.trim(certificateInstanceId)); + ServiceException.isTrue(StrUtil.equalsIgnoreCase(StrUtil.trim(entity.getCertificateType()), expectType.getType()), + "当前证照不是" + typeName); + return entity; + } + + /** + * 解析日期字符串。 + */ + private Date parseDate(String dateText, String fieldName) { + if (StrUtil.isBlank(dateText)) { + return null; + } + try { + return DateUtil.parseDate(StrUtil.trim(dateText)); + } catch (Exception e) { + throw new ServiceException(fieldName + "格式不正确,正确格式为yyyy-MM-dd"); + } + } + + /** + * 解析日期时间戳 + */ + private Date parseDate(Long dateTimestamp, String fieldName) { + if (Objects.isNull(dateTimestamp) || dateTimestamp <= 0L) { + return null; + } + try { + return new Date(dateTimestamp); + } catch (Exception e) { + throw new ServiceException(fieldName + "格式不正确,正确格式为yyyy-MM-dd"); + } + } + + /** + * 计算证照状态。 + */ + private Integer calculateStatus(Integer isLongTerm, Date expireDate) { + if (Integer.valueOf(1).equals(isLongTerm)) { + return STATUS_NORMAL; + } + if (expireDate == null) { + return STATUS_MISSING; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + if (target.before(today)) { + return STATUS_EXPIRED; + } + long daysDiff = DateUtil.betweenDay(today, target, false); + if (daysDiff <= NEAR_EXPIRE_DAYS) { + return STATUS_NEAR_EXPIRE; + } + return STATUS_NORMAL; + } + + /** + * 状态转中文名称。 + */ + private String resolveStatusName(Integer status) { + if (Integer.valueOf(STATUS_MISSING).equals(status)) { + return "缺失"; + } + if (Integer.valueOf(STATUS_EXPIRED).equals(status)) { + return "过期"; + } + if (Integer.valueOf(STATUS_NEAR_EXPIRE).equals(status)) { + return "临期"; + } + if (Integer.valueOf(STATUS_NORMAL).equals(status)) { + return "正常"; + } + return "-"; + } + + /** + * 字符串去空格,空串按null处理。 + */ + private String trimToNull(String value) { + String trimValue = StrUtil.trim(value); + return StrUtil.isBlank(trimValue) ? null : trimValue; + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateStoreServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateStoreServiceImpl.java new file mode 100644 index 0000000..78b9762 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/service/impl/CertificateStoreServiceImpl.java @@ -0,0 +1,906 @@ +package jnpf.certificate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.base.ActionResult; +import jnpf.certificate.mapper.CertificateBusinessLicenseExtMapper; +import jnpf.certificate.mapper.CertificateHygieneLicenseExtMapper; +import jnpf.certificate.mapper.CertificateInstanceItemMapper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.certificate.service.CertificateStoreService; +import jnpf.exception.HandleException; +import jnpf.model.certificate.po.CertificateBusinessLicenseExtEntity; +import jnpf.model.certificate.po.CertificateHygieneLicenseExtEntity; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.po.CertificateInstanceItemEntity; +import jnpf.model.certificate.req.CertificateInstanceItemReq; +import jnpf.model.certificate.req.CertificateStoreSaveReq; +import jnpf.model.certificate.req.app.CertificateAppBusinessLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppHygieneLicenseUpdateReq; +import jnpf.model.certificate.req.app.CertificateAppStoreCustomUpdateReq; +import jnpf.model.certificate.vo.CertificateStoreAndCertificatesVO; +import jnpf.model.certificate.vo.CertificateStoreTabVO; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoItemEntity; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoItemVO; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.permission.OrganizeApi; +import jnpf.permission.dto.v2.organzie.SaveStoreDTO; +import jnpf.storecertificatephoto.helper.StoreCertificatePhotoHelper; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoItemMapper; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoMapper; +import jnpf.util.FtbUtil; +import jnpf.util.ServiceException; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; + +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 门店证照服务实现。 + */ +@Slf4j +@Service +public class CertificateStoreServiceImpl implements CertificateStoreService { + + private static final int STATUS_MISSING = 1; + private static final int STATUS_EXPIRED = 2; + private static final int STATUS_NEAR_EXPIRE = 3; + private static final int STATUS_NORMAL = 4; + private static final int SUBJECT_TYPE_STORE = 3; + private static final int NEAR_EXPIRE_DAYS = 30; + + @Autowired + private OrganizeApi organizeApi; + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private CertificateBusinessLicenseExtMapper certificateBusinessLicenseExtMapper; + @Autowired + private CertificateHygieneLicenseExtMapper certificateHygieneLicenseExtMapper; + @Autowired + private CertificateInstanceItemMapper certificateInstanceItemMapper; + @Autowired + private StoreCertificatePhotoMapper storeCertificatePhotoMapper; + @Autowired + private StoreCertificatePhotoItemMapper storeCertificatePhotoItemMapper; + @Autowired + private StoreCertificatePhotoHelper storeCertificatePhotoHelper; + + /** + * 保存门店及门店证照。 + * 先调用组织服务保存门店,再保存证照实例及扩展表。 + */ + @Override + public String saveStoreAndCertificates(CertificateStoreSaveReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + SaveStoreDTO store = req.getStore(); + ServiceException.notNull(store, "门店信息不能为空"); + validCustomerCertificates(req.getStoreCustomCertificates()); + + String storeId = saveStore(store); + saveBusinessLicense(storeId,store.getDisabled(), req.getBusinessLicense()); + saveHygieneLicense(storeId,store.getDisabled(), req.getHygieneLicense()); + saveStoreCustomCertificates(storeId, store.getDisabled(),req.getStoreCustomCertificates()); + return storeId; + } + + private void validCustomerCertificates(Collection storeCustomCertificates) { + if(CollectionUtil.isEmpty(storeCustomCertificates)){ + return; + } + for (CertificateAppStoreCustomUpdateReq req : storeCustomCertificates){ + ServiceException.notNull(req.getTemplateId(), "门店自定义证照模板id不能为空"); + } + } + + @Override + public boolean deleteStore(String id) throws HandleException { + ActionResult actionResult = organizeApi.deleteStore(id); + if(Objects.isNull(actionResult) || actionResult.getCode() != 200){ + log.error("deleteStore fail.id:{},actionResult:{}",id,actionResult); + throw new HandleException("组织服务异常"); + } + if(!actionResult.getData()){ + log.error("deleteStore organize fail.id:{}",id); + throw new HandleException("删除失败!"); + } + certificateInstanceMapper.update(new CertificateInstanceEntity(),new LambdaUpdateWrapper() + .eq(CertificateInstanceEntity::getSubjectId, id) + .set(CertificateInstanceEntity::getEnabledMark,1)); + return true; + } + + /** + * 根据门店ID查询门店及门店证照详情。 + */ + @Override + public CertificateStoreAndCertificatesVO getStoreAndCertificates(String storeId) { + ServiceException.isTrue(StrUtil.isNotBlank(storeId), "门店ID不能为空"); + String targetStoreId = StrUtil.trim(storeId); + + CertificateStoreAndCertificatesVO result = new CertificateStoreAndCertificatesVO(); + result.setBusinessLicense(queryBusinessLicense(targetStoreId)); + result.setHygieneLicense(queryHygieneLicense(targetStoreId)); + result.setStoreCustomCertificates(queryStoreCustomCertificates(targetStoreId)); + return result; + } + + @Override + public List queryStoreCertificateTabList() { + List result = new ArrayList<>(); + List defaultTypeList = Arrays.asList( + CertificateTypeEnum.BUSINESS_LICENSE, + CertificateTypeEnum.HYGIENE_LICENSE + ); + for (CertificateTypeEnum certificateType : defaultTypeList) { + CertificateStoreTabVO tabVO = new CertificateStoreTabVO(); + tabVO.setLabel(certificateType.getLabel()); + tabVO.setKey(certificateType.getType()); + tabVO.setType(certificateType.getType()); + result.add(tabVO); + } + + List templateList = queryStoreCustomTemplateList(1); + if (CollUtil.isEmpty(templateList)) { + return result; + } + for (StoreCertificatePhotoVO template : templateList) { + if (template == null || StrUtil.isBlank(template.getId()) || StrUtil.isBlank(template.getCertificateName())) { + continue; + } + CertificateStoreTabVO tabVO = new CertificateStoreTabVO(); + tabVO.setLabel(StrUtil.trim(template.getCertificateName())); + tabVO.setKey(StrUtil.trim(template.getId())); + tabVO.setType(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()); + tabVO.setStoreCustomConfig(template); + result.add(tabVO); + } + return result; + } + + /** + * 调用组织服务保存门店并返回门店ID。 + */ + private String saveStore(SaveStoreDTO storeDTO) { + ActionResult actionResult; + try { + actionResult = organizeApi.saveStore(storeDTO); + } catch (Exception e) { + throw new ServiceException("保存门店失败: " + e.getMessage()); + } + ServiceException.notNull(actionResult, "保存门店失败"); + ServiceException.isTrue(ActionResult.success().getCode().equals(actionResult.getCode()), + StrUtil.blankToDefault(actionResult.getMsg(), "保存门店失败")); + String storeId = StrUtil.trim(actionResult.getData()); + if (StrUtil.isBlank(storeId)) { + storeId = StrUtil.trim(storeDTO.getId()); + } + ServiceException.isTrue(StrUtil.isNotBlank(storeId), "保存门店成功但未返回门店ID"); + return storeId; + } + + /** + * 批量查询启用中的门店自定义证照模板及明细。 + */ + private List queryStoreCustomTemplateList(Integer status) { + LambdaQueryWrapper templateWrapper = Wrappers.lambdaQuery(); + templateWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + if(Objects.nonNull(status)){ + templateWrapper.eq(StoreCertificatePhotoEntity::getStatus, status); + } + templateWrapper.orderByDesc(StoreCertificatePhotoEntity::getCreatorTime); + List templateEntities = storeCertificatePhotoMapper.selectList(templateWrapper); + if (CollUtil.isEmpty(templateEntities)) { + return Collections.emptyList(); + } + + List templateIds = templateEntities.stream() + .map(StoreCertificatePhotoEntity::getId) + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .collect(Collectors.toList()); + Map> itemMap = queryStoreCustomTemplateItemMap(templateIds); + return templateEntities.stream() + .map(entity -> buildStoreCustomTemplateVO(entity, itemMap)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * 按模板ID列表查询模板明细并按模板分组。 + */ + private Map> queryStoreCustomTemplateItemMap(List templateIds) { + if (CollUtil.isEmpty(templateIds)) { + return Collections.emptyMap(); + } + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.in(StoreCertificatePhotoItemEntity::getPhotoId, templateIds); + itemWrapper.eq(StoreCertificatePhotoItemEntity::getEnabledMark, 0); + itemWrapper.orderByAsc(StoreCertificatePhotoItemEntity::getPhotoId) + .orderByAsc(StoreCertificatePhotoItemEntity::getItemType) + .orderByAsc(StoreCertificatePhotoItemEntity::getSorts); + List itemEntities = storeCertificatePhotoItemMapper.selectList(itemWrapper); + if (CollUtil.isEmpty(itemEntities)) { + return Collections.emptyMap(); + } + return itemEntities.stream().collect(Collectors.groupingBy(StoreCertificatePhotoItemEntity::getPhotoId)); + } + + /** + * 构建门店自定义证照模板返回对象。 + */ + private StoreCertificatePhotoVO buildStoreCustomTemplateVO(StoreCertificatePhotoEntity entity, + Map> itemMap) { + if (entity == null || StrUtil.isBlank(entity.getId())) { + return null; + } + StoreCertificatePhotoVO vo = StoreCertificatePhotoVO.convert(entity); + List itemEntities = itemMap.getOrDefault(entity.getId(), Collections.emptyList()); + List imageItemList = new ArrayList<>(); + List textItemList = new ArrayList<>(); + for (StoreCertificatePhotoItemEntity itemEntity : itemEntities) { + StoreCertificatePhotoItemVO itemVO = StoreCertificatePhotoItemVO.convert(itemEntity); + if (itemVO == null) { + continue; + } + if (Integer.valueOf(1).equals(itemEntity.getItemType())) { + imageItemList.add(itemVO); + continue; + } + if (Integer.valueOf(2).equals(itemEntity.getItemType())) { + textItemList.add(itemVO); + } + } + imageItemList.sort(Comparator.comparing(item -> item.getSorts() == null ? Long.MAX_VALUE : item.getSorts())); + textItemList.sort(Comparator.comparing(item -> item.getSorts() == null ? Long.MAX_VALUE : item.getSorts())); + vo.setImageItemList(imageItemList); + vo.setTextItemList(textItemList); + return vo; + } + + /** + * 保存营业执照实例和扩展数据。 + */ + private void saveBusinessLicense(String storeId,Integer disable, CertificateAppBusinessLicenseUpdateReq req) { + if (req == null) { + req = new CertificateAppBusinessLicenseUpdateReq(); + } + CertificateInstanceEntity instance = findOrCreateSingleInstance( + storeId, + req.getCertificateInstanceId(), + CertificateTypeEnum.BUSINESS_LICENSE.getType() + ); + Date issueDate = parseDate(req.getIssueDate(), "营业执照发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "营业执照到期日期"); + Date establishDate = parseDate(req.getEstablishDate(), "营业执照成立时间"); + Integer isLongTerm = req.getIsLongTerm() == null ? 0 : req.getIsLongTerm(); + if (Integer.valueOf(1).equals(isLongTerm)) { + expireDate = null; + } + + instance.setIssueDate(issueDate); + instance.setExpireDate(expireDate); + if(Objects.nonNull(expireDate)){ + instance.setExpireTimestamp(expireDate.getTime()); + }else{ + instance.setExpireTimestamp(0); + } + instance.setIsLongTerm(isLongTerm); + instance.setCertificateNo(trimToNull(req.getCertificateNo())); + instance.setCertificateImage(trimToNull(req.getCertificateImage())); + instance.setStatus(calculateStatus(instance.getIsLongTerm(), instance.getExpireDate())); + instance.setStoreDisable(buildStoreDisable(disable)); + saveOrUpdateInstance(instance); + + upsertBusinessLicenseExt(instance.getId(), req, establishDate); + } + + private Integer buildStoreDisable(Integer disable) { + if(Objects.isNull(disable)){ + return 0; + } + return disable == 1?1:0; + } + + /** + * 查询门店营业执照详情。 + */ + private CertificateAppBusinessLicenseUpdateReq queryBusinessLicense(String storeId) { + CertificateInstanceEntity instance = findLatestByStoreAndType(storeId, CertificateTypeEnum.BUSINESS_LICENSE.getType(),null); + if (instance == null) { + return null; + } + CertificateBusinessLicenseExtEntity extEntity = findBusinessExt(instance.getId()); + CertificateAppBusinessLicenseUpdateReq result = new CertificateAppBusinessLicenseUpdateReq(); + result.setCertificateInstanceId(instance.getId()); + result.setCertificateImage(instance.getCertificateImage()); + result.setCertificateNo(instance.getCertificateNo()); + result.setIssueDate(formatDate(instance.getIssueDate())); + result.setExpireDate(formatDate(instance.getExpireDate())); + result.setIsLongTerm(instance.getIsLongTerm() == null ? 0 : instance.getIsLongTerm()); + if (extEntity != null) { + result.setCompanyName(extEntity.getCompanyName()); + result.setLegalRepresentative(extEntity.getLegalRepresentative()); + result.setEstablishDate(formatDate(extEntity.getEstablishDate())); + result.setBusinessScope(extEntity.getBusinessScope()); + result.setAddress(extEntity.getCompanyAddress()); + } + return result; + } + + /** + * 保存食品经营许可证实例和扩展数据。 + */ + private void saveHygieneLicense(String storeId, Integer storeDisable,CertificateAppHygieneLicenseUpdateReq req) { + if (req == null) { + req = new CertificateAppHygieneLicenseUpdateReq(); + } + CertificateInstanceEntity instance = findOrCreateSingleInstance( + storeId, + req.getCertificateInstanceId(), + CertificateTypeEnum.HYGIENE_LICENSE.getType() + ); + Date issueDate = parseDate(req.getIssueDate(), "食品经营许可证发证日期"); + Date expireDate = parseDate(req.getExpireDate(), "食品经营许可证到期日期"); + + instance.setIssueDate(issueDate); + instance.setExpireDate(expireDate); + if(Objects.nonNull(expireDate)){ + instance.setExpireTimestamp(expireDate.getTime()); + } + instance.setIsLongTerm(0); + instance.setCertificateImage(trimToNull(req.getCertificateImage())); + instance.setStatus(calculateStatus(instance.getIsLongTerm(), instance.getExpireDate())); + instance.setStoreDisable(buildStoreDisable(storeDisable)); + saveOrUpdateInstance(instance); + + upsertHygieneLicenseExt(instance.getId(), req); + } + + /** + * 查询门店食品经营许可证详情。 + */ + private CertificateAppHygieneLicenseUpdateReq queryHygieneLicense(String storeId) { + CertificateInstanceEntity instance = findLatestByStoreAndType(storeId, CertificateTypeEnum.HYGIENE_LICENSE.getType(),null); + if (instance == null) { + return null; + } + CertificateHygieneLicenseExtEntity extEntity = findHygieneExt(instance.getId()); + CertificateAppHygieneLicenseUpdateReq result = new CertificateAppHygieneLicenseUpdateReq(); + result.setCertificateInstanceId(instance.getId()); + result.setCertificateImage(instance.getCertificateImage()); + result.setIssueDate(formatDate(instance.getIssueDate())); + result.setExpireDate(formatDate(instance.getExpireDate())); + if (extEntity != null) { + result.setBusinessProject(extEntity.getBusinessProject()); + } + return result; + } + + /** + * 保存门店自定义证照实例和明细数据。 + */ + private void saveStoreCustomCertificates(String storeId,Integer disabled, Collection reqList) { + if (CollUtil.isEmpty(reqList)) { + return; + } + + Map templateNameById = storeCertificatePhotoHelper.buildStoreCertificateIdAndNames(reqList.stream() + .map(CertificateAppStoreCustomUpdateReq::getTemplateId) + .collect(Collectors.toSet()),1); + for (CertificateAppStoreCustomUpdateReq req : reqList) { + if (req == null) { + continue; + } + + //如果是禁用状态,则跳过 + String templateId = req.getTemplateId(); + int templateStatus = 1; + if(!templateNameById.containsKey(templateId)){ + templateStatus = 0; + continue; + } + CertificateInstanceEntity instance = findOrCreateCustomInstance(storeId, req.getCertificateInstanceId(), templateId); + + Date expireDate = parseDate(req.getExpireDate(), "门店自定义证照到期日期"); + + instance.setCertificateImage(buildCustomCertificateImage(req.getItemList())); + instance.setIssueDate(null); + instance.setExpireDate(expireDate); + if(Objects.nonNull(expireDate)){ + instance.setExpireTimestamp(expireDate.getTime()); + } + instance.setIsLongTerm(0); + instance.setStatus(calculateStatus(instance.getIsLongTerm(), instance.getExpireDate())); + + instance.setTemplateStatus(templateStatus); + instance.setStoreDisable(buildStoreDisable(disabled)); + saveOrUpdateInstance(instance); + + rewriteCustomItems(instance.getId(), req.getItemList()); + } + } + + private String buildCustomCertificateImage(List itemList) { + if(CollectionUtil.isEmpty(itemList)){ + return ""; + } + for (CertificateInstanceItemReq item : itemList) { + if (item == null) { + continue; + } + if (item.getItemType() == 1) { + return item.getItemValue(); + } + } + return ""; + } + + /** + * 查询门店自定义证照详情集合。 + */ + private List queryStoreCustomCertificates(String storeId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + wrapper.eq(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_STORE); + wrapper.eq(CertificateInstanceEntity::getSubjectId, storeId); + wrapper.eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()); + wrapper.orderByDesc(CertificateInstanceEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateInstanceEntity::getCreatorTime); + List instanceList = certificateInstanceMapper.selectList(wrapper); + if (CollUtil.isEmpty(instanceList)) { + return Collections.emptyList(); + } + + Set enabledTemplateIds = storeCertificatePhotoMapper.selectList(new LambdaQueryWrapper() + .select(StoreCertificatePhotoEntity::getId) + .in(StoreCertificatePhotoEntity::getId,instanceList.stream() + .map(CertificateInstanceEntity::getTemplateId) + .collect(Collectors.toSet())) + .eq(StoreCertificatePhotoEntity::getEnabledMark,0) + .eq(StoreCertificatePhotoEntity::getStatus,1)) + .stream() + .map(StoreCertificatePhotoEntity::getId) + .collect(Collectors.toSet()); + + Map> itemMap = queryStoreCustomItemMap(instanceList.stream() + .filter(instance->{ + String templateId = instance.getTemplateId(); + if(StringUtils.isBlank(templateId)){ + return true; + } + return enabledTemplateIds.contains(templateId); + }) + .map(CertificateInstanceEntity::getId) + .collect(Collectors.toSet())); + return instanceList.stream() + .filter(instance->{ + String templateId = instance.getTemplateId(); + if(StringUtils.isBlank(templateId)){ + return true; + } + return enabledTemplateIds.contains(templateId); + }) + .map(instance -> convertStoreCustomCertificate(instance, itemMap)) + .collect(Collectors.toList()); + } + + /** + * 按证照实例ID批量查询自定义证照明细并分组。 + */ + private Map> queryStoreCustomItemMap(Collection instanceIds) { + if (CollUtil.isEmpty(instanceIds)) { + return Collections.emptyMap(); + } + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.in(CertificateInstanceItemEntity::getInstanceId, instanceIds); + itemWrapper.eq(CertificateInstanceItemEntity::getEnabledMark, 0); + itemWrapper.orderByAsc(CertificateInstanceItemEntity::getInstanceId); + itemWrapper.orderByAsc(CertificateInstanceItemEntity::getSorts); + List itemEntities = certificateInstanceItemMapper.selectList(itemWrapper); + if (CollUtil.isEmpty(itemEntities)) { + return Collections.emptyMap(); + } + + return itemEntities.stream() + .collect(Collectors.groupingBy(CertificateInstanceItemEntity::getInstanceId)); + } + + /** + * 根据证照实例ID或门店+证照类型查询单条实例,不存在则创建。 + */ + private CertificateInstanceEntity findOrCreateSingleInstance(String storeId, String certificateInstanceId, String certificateType) { + CertificateInstanceEntity instance = findByInstanceId(certificateInstanceId, certificateType); + if (instance == null) { + instance = findLatestByStoreAndType(storeId, certificateType,null); + } + if (instance == null) { + instance = createNewInstance(storeId, certificateType); + } + normalizeStoreInstance(instance, storeId, certificateType); + return instance; + } + + /** + * 根据证照实例ID查询自定义证照实例,不存在则创建新实例。 + */ + private CertificateInstanceEntity findOrCreateCustomInstance(String storeId, String certificateInstanceId, String templateId) { + String certificateType = CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType(); + CertificateInstanceEntity instance = findByInstanceId(certificateInstanceId, certificateType); + if (instance == null) { + instance = createNewInstance(storeId, certificateType); + instance.setTemplateId(templateId); + } + normalizeStoreInstance(instance, storeId, certificateType); + return instance; + } + + /** + * 按证照实例ID查询。 + */ + private CertificateInstanceEntity findByInstanceId(String certificateInstanceId, String certificateType) { + if (StrUtil.isBlank(certificateInstanceId)) { + return null; + } + String instanceId = StrUtil.trim(certificateInstanceId); + CertificateInstanceEntity instance = certificateInstanceMapper.selectById(instanceId); + ServiceException.isTrue(instance != null && Integer.valueOf(0).equals(instance.getEnabledMark()), "证照实例不存在"); + ServiceException.isTrue(StrUtil.equalsIgnoreCase(StrUtil.trim(instance.getCertificateType()), certificateType), "证照类型不匹配"); + return instance; + } + + /** + * 按门店和证照类型查询最新实例。 + */ + private CertificateInstanceEntity findLatestByStoreAndType(String storeId, String certificateType,String templateId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateInstanceEntity::getEnabledMark, 0); + wrapper.eq(CertificateInstanceEntity::getSubjectType, SUBJECT_TYPE_STORE); + wrapper.eq(CertificateInstanceEntity::getSubjectId, storeId); + wrapper.eq(CertificateInstanceEntity::getCertificateType, certificateType); + if(StringUtils.isNotBlank(templateId)){ + wrapper.eq(CertificateInstanceEntity::getTemplateId, templateId); + } + wrapper.orderByDesc(CertificateInstanceEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateInstanceEntity::getCreatorTime); + List list = certificateInstanceMapper.selectList(wrapper); + return CollUtil.isEmpty(list) ? null : list.get(0); + } + + /** + * 构建新证照实例。 + */ + private CertificateInstanceEntity createNewInstance(String storeId, String certificateType) { + CertificateInstanceEntity instance = new CertificateInstanceEntity(); + instance.setSubjectType(SUBJECT_TYPE_STORE); + instance.setSubjectId(storeId); + instance.setCertificateType(certificateType); + instance.setEnabledMark(0); + instance.setTemplateId(""); + instance.setCreatorUserId(UserProvider.getLoginUserId()); + instance.setCreatorTime(new Date()); + instance.setLastModifyUserId(UserProvider.getLoginUserId()); + instance.setLastModifyTime(new Date()); + return instance; + } + + /** + * 规范化门店证照实例字段。 + */ + private void normalizeStoreInstance(CertificateInstanceEntity instance, String storeId, String certificateType) { + instance.setSubjectType(SUBJECT_TYPE_STORE); + instance.setSubjectId(storeId); + instance.setCertificateType(certificateType); + if (instance.getEnabledMark() == null) { + instance.setEnabledMark(0); + } + } + + /** + * 保存或更新证照实例。 + */ + private void saveOrUpdateInstance(CertificateInstanceEntity instance) { + if(StringUtils.isNotBlank(instance.getId())){ + updateInstanceByWrapper(instance); + return; + } + instance.setId(FtbUtil.getId()); + try { + certificateInstanceMapper.insert(instance); + } catch (DuplicateKeyException e) { + updateInstance(instance); + } catch (DataIntegrityViolationException e) { + if (isDuplicateKeyException(e)) { + updateInstance(instance); + return; + } + throw e; + } catch (Exception e) { + if (isDuplicateKeyException(e)) { + updateInstance(instance); + return; + } + throw e; + } +// catch (SQLIntegrityConstraintViolationException e){ +// String msg = e.getMessage(); +// if (msg.contains("Duplicate entry")){ +// updateInstance(instance); +// return; +// } +// throw e; +// } + } + + /** + * 判断异常链中是否包含唯一键冲突。 + * 兼容 Seata/MyBatis 对 JDBC 异常的包装场景。 + */ + private boolean isDuplicateKeyException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof DuplicateKeyException || current instanceof SQLIntegrityConstraintViolationException) { + return true; + } + String message = current.getMessage(); + if (StringUtils.isNotBlank(message) + && StringUtils.containsIgnoreCase(message, "Duplicate entry") + && StringUtils.containsIgnoreCase(message, "for key")) { + return true; + } + current = current.getCause(); + } + return false; + } + + private void updateInstance(CertificateInstanceEntity instance) { + if(Objects.isNull(instance)){ + return; + } + CertificateInstanceEntity dbInstance = findLatestByStoreAndType(instance.getSubjectId(), instance.getCertificateType(),instance.getTemplateId()); + if(Objects.isNull(dbInstance)){ + log.error("updateInstance fail.findLatestByStoreAndType null.instance:{}",instance); + return; + } + instance.setId(dbInstance.getId()); + updateInstanceByWrapper(instance); + } + + private void updateInstanceByWrapper(CertificateInstanceEntity instance) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper().eq(CertificateInstanceEntity::getId,instance.getId()) + .set(CertificateInstanceEntity::getCertificateImage, instance.getCertificateImage()) + .set(CertificateInstanceEntity::getIssueDate, instance.getIssueDate()) + .set(CertificateInstanceEntity::getExpireDate, instance.getExpireDate()) + .set(CertificateInstanceEntity::getExpireTimestamp, instance.getExpireTimestamp()) + .set(CertificateInstanceEntity::getStatus, instance.getStatus()) + .set(CertificateInstanceEntity::getLastModifyTime, instance.getLastModifyTime()) + .set(CertificateInstanceEntity::getLastModifyUserId, instance.getLastModifyUserId()) + .set(CertificateInstanceEntity::getEnabledMark, instance.getEnabledMark()) + .set(CertificateInstanceEntity::getCreatorTime, instance.getCreatorTime()) + .set(CertificateInstanceEntity::getCreatorUserId, instance.getCreatorUserId()) + .set(CertificateInstanceEntity::getStoreDisable,instance.getStoreDisable()); + Integer templateStatus = instance.getTemplateStatus(); + if(Objects.nonNull(templateStatus)){ + updateWrapper.set(CertificateInstanceEntity::getTemplateStatus,templateStatus); + } + int r = certificateInstanceMapper.update(null,updateWrapper); + if(r<=0){ + log.error("saveOrUpdateInstance insert fail.update also fail.instance:{}",instance); + } + } + + /** + * 保存营业执照扩展表。 + */ + private void upsertBusinessLicenseExt(String instanceId, CertificateAppBusinessLicenseUpdateReq req, Date establishDate) { + CertificateBusinessLicenseExtEntity extEntity = new CertificateBusinessLicenseExtEntity(); + extEntity.setId(FtbUtil.getId()); + extEntity.setInstanceId(instanceId); + extEntity.setEnabledMark(0); + fillBusinessExt(extEntity, req, establishDate); + try { + certificateBusinessLicenseExtMapper.insert(extEntity); + }catch (DuplicateKeyException e){ + int r = certificateBusinessLicenseExtMapper.update(null,new LambdaUpdateWrapper() + .eq(CertificateBusinessLicenseExtEntity::getInstanceId,instanceId) + .eq(CertificateBusinessLicenseExtEntity::getEnabledMark,0) + .set(CertificateBusinessLicenseExtEntity::getCompanyName,extEntity.getCompanyName()) + .set(CertificateBusinessLicenseExtEntity::getCompanyAddress,extEntity.getCompanyAddress()) + .set(CertificateBusinessLicenseExtEntity::getLegalRepresentative,extEntity.getLegalRepresentative()) + .set(CertificateBusinessLicenseExtEntity::getEstablishDate,extEntity.getEstablishDate()) + .set(CertificateBusinessLicenseExtEntity::getBusinessScope,extEntity.getBusinessScope()) + .set(CertificateBusinessLicenseExtEntity::getCreatorTime,extEntity.getCreatorTime()) + .set(CertificateBusinessLicenseExtEntity::getCreatorUserId,extEntity.getCreatorUserId()) + .set(CertificateBusinessLicenseExtEntity::getLastModifyTime,extEntity.getLastModifyTime()) + .set(CertificateBusinessLicenseExtEntity::getLastModifyUserId,extEntity.getLastModifyUserId()) + ); + if(r <= 0){ + log.error("upsertBusinessLicenseExt insert fail.update row count is:{}.instanceId:{},extEntity:{}",r,instanceId,extEntity); + } + } + } + + /** + * 保存食品经营许可证扩展表。 + */ + private void upsertHygieneLicenseExt(String instanceId, CertificateAppHygieneLicenseUpdateReq req) { + CertificateHygieneLicenseExtEntity extEntity = findHygieneExt(instanceId); + if (extEntity == null) { + extEntity = new CertificateHygieneLicenseExtEntity(); + extEntity.setId(FtbUtil.getId()); + extEntity.setInstanceId(instanceId); + extEntity.setEnabledMark(0); + extEntity.setBusinessProject(trimToNull(req.getBusinessProject())); + certificateHygieneLicenseExtMapper.insert(extEntity); + return; + } + extEntity.setBusinessProject(trimToNull(req.getBusinessProject())); + certificateHygieneLicenseExtMapper.updateById(extEntity); + } + + /** + * 重写门店自定义证照明细。 + */ + private void rewriteCustomItems(String instanceId, List itemList) { + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(CertificateInstanceItemEntity::getInstanceId, instanceId); + certificateInstanceItemMapper.delete(deleteWrapper); + + List safeItemList = itemList == null ? Collections.emptyList() : itemList; + long sort = 1L; + for (CertificateInstanceItemReq itemReq : safeItemList) { + if (itemReq == null || StrUtil.isBlank(itemReq.getItemName())) { + continue; + } + CertificateInstanceItemEntity itemEntity = new CertificateInstanceItemEntity(); + itemEntity.setId(FtbUtil.getId()); + itemEntity.setInstanceId(instanceId); + itemEntity.setTemplateItemId(trimToNull(itemReq.getTemplateItemId())); + itemEntity.setItemName(trimToNull(itemReq.getItemName())); + itemEntity.setItemType(itemReq.getItemType()); + itemEntity.setItemValue(trimToNull(itemReq.getItemValue())); + itemEntity.setSorts(itemReq.getSorts() == null ? sort : itemReq.getSorts()); + itemEntity.setEnabledMark(0); + certificateInstanceItemMapper.insert(itemEntity); + sort++; + } + } + + /** + * 查询营业执照扩展数据。 + */ + private CertificateBusinessLicenseExtEntity findBusinessExt(String instanceId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateBusinessLicenseExtEntity::getInstanceId, instanceId); + wrapper.eq(CertificateBusinessLicenseExtEntity::getEnabledMark, 0); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateBusinessLicenseExtEntity::getCreatorTime); + List extList = certificateBusinessLicenseExtMapper.selectList(wrapper); + return CollUtil.isEmpty(extList) ? null : extList.get(0); + } + + /** + * 查询食品经营许可证扩展数据。 + */ + private CertificateHygieneLicenseExtEntity findHygieneExt(String instanceId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CertificateHygieneLicenseExtEntity::getInstanceId, instanceId); + wrapper.eq(CertificateHygieneLicenseExtEntity::getEnabledMark, 0); + wrapper.orderByDesc(CertificateHygieneLicenseExtEntity::getLastModifyTime); + wrapper.orderByDesc(CertificateHygieneLicenseExtEntity::getCreatorTime); + List extList = certificateHygieneLicenseExtMapper.selectList(wrapper); + return CollUtil.isEmpty(extList) ? null : extList.get(0); + } + + /** + * 将自定义证照实例转换为查询返回对象。 + */ + private CertificateAppStoreCustomUpdateReq convertStoreCustomCertificate( + CertificateInstanceEntity instance, + Map> itemMap) { + CertificateAppStoreCustomUpdateReq result = new CertificateAppStoreCustomUpdateReq(); + result.setCertificateInstanceId(instance.getId()); + result.setExpireDate(formatDate(instance.getExpireDate())); + result.setTemplateId(instance.getTemplateId()); + + List itemEntities = itemMap.getOrDefault(instance.getId(), Collections.emptyList()); + if (CollUtil.isEmpty(itemEntities)) { + return result; + } + result.setItemList(itemEntities.stream().map(entity -> { + CertificateInstanceItemReq itemReq = new CertificateInstanceItemReq(); + itemReq.setTemplateItemId(entity.getTemplateItemId()); + itemReq.setItemName(entity.getItemName()); + itemReq.setItemType(entity.getItemType()); + itemReq.setItemValue(entity.getItemValue()); + itemReq.setSorts(entity.getSorts()); + return itemReq; + }).filter(Objects::nonNull).collect(Collectors.toList())); + return result; + } + + /** + * 填充营业执照扩展字段。 + */ + private void fillBusinessExt(CertificateBusinessLicenseExtEntity extEntity, CertificateAppBusinessLicenseUpdateReq req, Date establishDate) { + extEntity.setCompanyName(trimToNull(req.getCompanyName())); + extEntity.setLegalRepresentative(trimToNull(req.getLegalRepresentative())); + extEntity.setEstablishDate(establishDate); + extEntity.setBusinessScope(trimToNull(req.getBusinessScope())); + extEntity.setCompanyAddress(req.getAddress()); + extEntity.setCreatorTime(new Date()); + extEntity.setCreatorUserId(UserProvider.getLoginUserId()); + extEntity.setLastModifyTime(new Date()); + extEntity.setLastModifyUserId(UserProvider.getLoginUserId()); + } + + /** + * 解析日期文本,格式为 yyyy-MM-dd。 + */ + private Date parseDate(String dateText, String fieldName) { + if (StrUtil.isBlank(dateText)) { + return null; + } + try { + return DateUtil.parseDate(StrUtil.trim(dateText)); + } catch (Exception e) { + throw new ServiceException(fieldName + "格式不正确,正确格式为yyyy-MM-dd"); + } + } + + /** + * 日期格式化为 yyyy-MM-dd 字符串。 + */ + private String formatDate(Date date) { + if (date == null) { + return null; + } + return DateUtil.formatDate(date); + } + + /** + * 根据结束时间计算证照状态。 + */ + private Integer calculateStatus(Integer isLongTerm, Date expireDate) { + if (Integer.valueOf(1).equals(isLongTerm)) { + return STATUS_NORMAL; + } + if (expireDate == null) { + return STATUS_MISSING; + } + Date today = DateUtil.beginOfDay(DateUtil.date()); + Date target = DateUtil.beginOfDay(expireDate); + if (target.before(today)) { + return STATUS_EXPIRED; + } + long daysDiff = DateUtil.betweenDay(today, target, false); + if (daysDiff <= NEAR_EXPIRE_DAYS) { + return STATUS_NEAR_EXPIRE; + } + return STATUS_NORMAL; + } + + /** + * 字符串去首尾空格,空串转为null。 + */ + private String trimToNull(String value) { + String trimValue = StrUtil.trim(value); + return StrUtil.isBlank(trimValue) ? null : trimValue; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/CertificateStatusUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/CertificateStatusUtils.java new file mode 100644 index 0000000..3b41960 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/CertificateStatusUtils.java @@ -0,0 +1,17 @@ +package jnpf.certificate.util; + +public class CertificateStatusUtils { + public static String buildStatusName(Integer status){ + switch (status){ + case 1: + return "缺失"; + case 2: + return "过期"; + case 3: + return "临期"; + case 4: + return "正常"; + } + return ""; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/StoreTypeUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/StoreTypeUtils.java new file mode 100644 index 0000000..f8571c8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/StoreTypeUtils.java @@ -0,0 +1,18 @@ +package jnpf.certificate.util; + +import jnpf.permission.eum.v2.FtbStoreTagEnum; + +import java.util.Objects; + +public class StoreTypeUtils { + public static String buildStoreType(String storeType) { + if(Objects.isNull(storeType)){ + return ""; + } + FtbStoreTagEnum storeTypeEnum = FtbStoreTagEnum.fromName(storeType); + if(Objects.isNull(storeTypeEnum)){ + return ""; + } + return storeTypeEnum.getLabel(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/SubjectNameUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/SubjectNameUtils.java new file mode 100644 index 0000000..35dc5ad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/SubjectNameUtils.java @@ -0,0 +1,17 @@ +package jnpf.certificate.util; + +public class SubjectNameUtils { + //主体类型,1为员工、2为公司、3为门店 + public static String getSubjectName(Integer subjectType) { + switch (subjectType){ + case 1: + return "员工"; + case 2: + return "公司"; + case 3: + return "门店"; + default: + return ""; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/WorkerStatusUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/WorkerStatusUtils.java new file mode 100644 index 0000000..8caebfb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/certificate/util/WorkerStatusUtils.java @@ -0,0 +1,14 @@ +package jnpf.certificate.util; + +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.util.StringUtil; + +public class WorkerStatusUtils { + //301、预入职 302、试用 303、正式 304、待离职 305 离职 + public static String buildStatusName(String workerStatus) { + if(StringUtil.isBlank(workerStatus)){ + return ""; + } + return StaffWorkerStatus.getWorkerStatusNameByValue(workerStatus); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/AsyncConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/AsyncConfig.java new file mode 100644 index 0000000..024bfd4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/AsyncConfig.java @@ -0,0 +1,56 @@ +package jnpf.config; + +import jnpf.util.context.ThreadContext; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import javax.annotation.PreDestroy; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * 激活异步处理能力 + * + * @author yanwenfu + * @create 2025-07-04 + */ +@Configuration +@EnableAsync +@Slf4j +public class AsyncConfig { + + @Bean(name = "threadPoolTaskExecutor") + @Primary + public Executor absenceExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("absence-async-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setTaskDecorator(new ContextCopyingTaskDecorator()); + executor.initialize(); + return executor; + } + + // 自定义 TaskDecorator + static class ContextCopyingTaskDecorator implements TaskDecorator { + + @NotNull + @Override + public Runnable decorate(@NotNull Runnable runnable) { + + return ThreadContext.wrap(runnable); + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/CustomValueFilter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/CustomValueFilter.java new file mode 100644 index 0000000..f7da17b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/CustomValueFilter.java @@ -0,0 +1,66 @@ +package jnpf.config; + +import com.alibaba.fastjson.serializer.ValueFilter; +import jnpf.annotation.Sensitive; +import jnpf.enums.SensitiveTypeEnum; +import jnpf.util.DesensitizedUtils; + +import java.lang.reflect.Field; +import java.util.Date; + +/** + * 自定义浮点过滤器 + * + * @author yanwenfu + * @create 2020-11-12 + */ +public class CustomValueFilter implements ValueFilter { + + /** 最小转换位数 */ + private static final int CHANGE_MIN_LEN = 15; + + @Override + public Object process(Object object, String name, Object value) { + + if (null != value) { + /*if (value instanceof BigDecimal) { + return String.format("%.2f", ((BigDecimal) value).doubleValue()); + }*/ + if (value instanceof Long) { + // long类型数值大于15位时转换为string类型返回 + if (String.valueOf(value).length() > CHANGE_MIN_LEN) { + return String.valueOf(value); + } + } + if (value instanceof Date) { + Date date = (Date) value; + return date.getTime(); + } + // 脱敏处理 + try { + Field field = object.getClass().getDeclaredField(name); + Sensitive sensitive; + if (String.class != field.getType() || (sensitive = field.getAnnotation(Sensitive.class)) == null) { + return value; + } + String valueStr = value.toString(); + SensitiveTypeEnum sensitiveTypeEnum = sensitive.type(); + switch (sensitiveTypeEnum) { + case CUSTOMER: + return DesensitizedUtils.desValue(valueStr, sensitive.prefixNoMaskLen(), sensitive.suffixNoMaskLen(), sensitive.symbol()); + case NAME: + return DesensitizedUtils.chineseName(valueStr); + case ID_NUM: + return DesensitizedUtils.idCardNum(valueStr); + case PHONE_NUM: + return DesensitizedUtils.mobilePhone(valueStr); + default: + throw new IllegalArgumentException("unknown sensitive type enum " + sensitiveTypeEnum); + } + } catch (NoSuchFieldException e) { + return value; + } + } + return value; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/GlobalExceptionHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..f9f47f6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/GlobalExceptionHandler.java @@ -0,0 +1,135 @@ +package jnpf.config; + +import cn.dev33.satoken.exception.NotPermissionException; +import jnpf.base.ActionResult; +import jnpf.base.ActionResultCode; +import jnpf.exception.ApproveException; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.exceptions.PersistenceException; +import org.mybatis.spring.MyBatisSystemException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; + +import javax.validation.ConstraintViolationException; +import java.util.HashSet; +import java.util.Set; + +/** + * 自定义全局异常 + * + * @author yanwenfu + * @create 2023-07-24 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public Object processDefaultException(Exception e) { + log.error(e.getClass().getName()); + log.error(e.getMessage(), e); + return ActionResult.fail("服务异常:" + e.getMessage()); + } + + @ExceptionHandler(HandleException.class) + public Object processHandleException(HandleException e) { + log.error(e.getClass().getName()); + log.error(e.getMessage(), e); + return ActionResult.fail(e.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + public Object processRuntimeException(RuntimeException e) { + log.error(e.getClass().getName()); + log.error(e.getMessage(), e); + return ActionResult.fail(e.getMessage()); + } + + @ExceptionHandler(value = MaxUploadSizeExceededException.class) + public ActionResult fileUploadExceptionHandler(MaxUploadSizeExceededException e) { + log.error(e.getClass().getName()); + log.error(e.getMessage(), e); + return ActionResult.fail("上传文件超过500MB限制"); + } + + @ExceptionHandler(QueryException.class) + public Object processQueryException(QueryException e) { + log.error(e.getClass().getName()); + log.error(e.getMessage(), e); + return ActionResult.fail(e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseBody + public Object handleBindException(MethodArgumentNotValidException e) { + //打印校验住的所有的错误信息 + Set set = new HashSet<>(); + e.getBindingResult().getAllErrors().forEach(item -> set.add(item.getDefaultMessage())); + StringBuilder sb = getErrorMessage(set); + log.error(e.getMessage(), e); + return ActionResult.fail(sb.toString()); + } + + @ExceptionHandler(BindException.class) + @ResponseBody + public Object handleBindException(BindException e) { + //打印校验住的所有的错误信息 + Set set = new HashSet<>(); + log.error(e.getMessage(), e); + e.getBindingResult().getAllErrors().forEach(item -> set.add(item.getDefaultMessage())); + StringBuilder sb = getErrorMessage(set); + return ActionResult.fail(sb.toString()); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseBody + public Object handleBindException(ConstraintViolationException e) { + //打印校验住的所有的错误信息 + Set set = new HashSet<>(); + e.getConstraintViolations().forEach(item -> set.add(item.getMessage())); + StringBuilder sb = getErrorMessage(set); + log.error(e.getMessage(), e); + return ActionResult.fail(sb.toString()); + } + + @ExceptionHandler(ApproveException.class) + public Object processApproveException(ApproveException e) throws ApproveException { + log.error(e.getClass().getName()); + log.error(e.getMessage(), e); + throw new ApproveException(e.getMessage()); + } + + /** + * 权限码异常 + */ + @ResponseBody + @ExceptionHandler(NotPermissionException.class) + public ActionResult handleNotPermissionException(NotPermissionException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), "没有访问权限,请联系管理员授权"); + } + + @ExceptionHandler(MyBatisSystemException.class) + public ActionResult handlePersistenceException(MyBatisSystemException e) { + log.error(e.getMessage(), e); + if (e.getCause() instanceof PersistenceException) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getCause().getCause().getCause().getMessage()); + } + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getMessage()); + } + + + private StringBuilder getErrorMessage(Set set) { + + StringBuilder sb = new StringBuilder("["); + set.forEach(item -> sb.append(item).append(',')); + sb.deleteCharAt(sb.length() - 1); + sb.append(']'); + return sb; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/MqttConfiguration.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/MqttConfiguration.java new file mode 100644 index 0000000..7dcd394 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/MqttConfiguration.java @@ -0,0 +1,36 @@ +package jnpf.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +public class MqttConfiguration { + + @Value("${spring.mqtt.broker}") + private String host; + + @Value("${spring.mqtt.clientId}") + private String clientId; + + @Value("${spring.mqtt.username}") + private String username; + + @Value("${spring.mqtt.password}") + private String password; + + @Value("${spring.mqtt.timeout}") + private int timeout; + + @Value("${spring.mqtt.KeepAlive}") + private int KeepAlive; + + @Value("${spring.mqtt.topics}") + private String topics; + + @Value("${spring.mqtt.qos}") + private int qos; +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/StringToDateConverter.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/StringToDateConverter.java new file mode 100644 index 0000000..f039541 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/StringToDateConverter.java @@ -0,0 +1,107 @@ +package jnpf.config; + +import cn.hutool.core.util.StrUtil; +import org.jetbrains.annotations.Nullable; +import org.springframework.core.convert.converter.Converter; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public final class StringToDateConverter implements Converter { + + //静态初始化定义日期字符串参数列表(需要转换的) + private static final List paramList = new ArrayList<>(); + + //静态初始化可能初夏你的日期格式 + private static final String param1 = "yyyy-MM"; + private static final String param2 = "yyyy-MM-dd"; + private static final String param3 = "yyyy-MM-dd HH:mm"; + private static final String param4 = "yyyy-MM-dd HH:mm:ss"; + private static final String param5 = "yyyy/MM/dd"; + private static final String param6 = "yyyyMMdd"; + + //静态代码块,将日期参数加入到列表中 + static { + paramList.add(param1); + paramList.add(param2); + paramList.add(param3); + paramList.add(param4); + } + + + @Override + public Date convert(String source) { + if (StrUtil.isNotBlank(source)) { + try { + source = source.trim(); //去除首尾空格 + //正则表达式判断是哪一种格式参数 + if (source.matches("^\\d{4}-\\d{1,2}$")) { + return parseDate(source, paramList.get(0)); + } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) { + return parseDate(source, paramList.get(1)); + } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) { + return parseDate(source, paramList.get(2)); + } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) { + return parseDate(source, paramList.get(3)); + } else { + throw new IllegalArgumentException("还未定义该种字符串转Date的日期转换格式 --> 【日期格式】:" + source); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return null; + } + + // 自定义函数,将字符串转Date 参1:传入的日期字符串 参2:格式参数 + private Date parseDate(String source, String format) throws ParseException { + //日期格式转换器 + DateFormat dateFormat = new SimpleDateFormat(format); + return dateFormat.parse(source); + } + + + public static String dateFormatCheck(String data) { + Date date = getDate(data); + DateFormat dateFormat; + if (date != null) { + dateFormat = new SimpleDateFormat(param2); + return dateFormat.format(date); + } + return null; + } + + public static Date dateFormatParser(String data) { + Date date = getDate(data); + if (date != null) { + return date; + } + return null; + } + + @Nullable + private static Date getDate(String data) { + DateFormat dateFormat = new SimpleDateFormat(param5); + Date date = null; + try { + date = dateFormat.parse(data); + } catch (ParseException e) { + dateFormat = new SimpleDateFormat(param2); + try { + date = dateFormat.parse(data); + } catch (ParseException ee) { + dateFormat = new SimpleDateFormat(param6); + try { + date = dateFormat.parse(data); + } catch (ParseException eee) { + } + } + } + return date; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/TempDevOnlyConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/TempDevOnlyConfig.java new file mode 100644 index 0000000..a495be5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/TempDevOnlyConfig.java @@ -0,0 +1,26 @@ +package jnpf.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration +public class TempDevOnlyConfig { + @Bean(name = "taskExecutor") + @ConditionalOnMissingBean(ThreadPoolTaskExecutor.class) + public ThreadPoolTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(10); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(200); + executor.setThreadNamePrefix("ftb-task-executor-"); + executor.setKeepAliveSeconds(60); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.initialize(); + return executor; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/WebConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/WebConfig.java new file mode 100644 index 0000000..230fcc2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/config/WebConfig.java @@ -0,0 +1,88 @@ +package jnpf.config; + +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.format.FormatterRegistry; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.ArrayList; +import java.util.List; + +/** + * web相关的定制化配置 + * WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现WebMvcConfigurer 这个接口 + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + + /** + * 将 FastJsonHttpMessageConverter 加入spring管理,fegin 请求时,默认使用jackson + * @return + */ + @Bean + @Order(1) + public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() { + return getFastJsonHttpMessageConverter(); + } + + @NotNull + private static FastJsonHttpMessageConverter getFastJsonHttpMessageConverter() { + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); + //自定义fastjson配置 + FastJsonConfig config = new FastJsonConfig(); + config.setSerializerFeatures( + // SerializerFeature.WriteMapNullValue, // 是否输出值为null的字段,默认为false,我们将它打开 + SerializerFeature.WriteNullListAsEmpty, // 将Collection类型字段的字段空值输出为[] + SerializerFeature.WriteNullStringAsEmpty, // 将字符串类型字段的空值输出为空字符串 + // SerializerFeature.WriteNullNumberAsZero // 将数值类型字段的空值输出为0 + // SerializerFeature.WriteDateUseDateFormat + SerializerFeature.DisableCircularReferenceDetect // 禁用循环引用 + ); + config.setSerializeFilters(new CustomValueFilter()); + fastJsonHttpMessageConverter.setFastJsonConfig(config); + // 添加支持的MediaTypes;不添加时默认为*/*,也就是默认支持全部 + // 但是MappingJackson2HttpMessageConverter里面支持的MediaTypes为application/json + // 参考它的做法, fastjson也只添加application/json的MediaType + List fastMediaTypes = new ArrayList<>(); + fastMediaTypes.add(MediaType.APPLICATION_JSON); + fastMediaTypes.add(MediaType.APPLICATION_XML); + fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes); + return fastJsonHttpMessageConverter; + } + + /** + * 使用fastjson代替jackson + * + * @param converters 转换器 + */ + @Override + public void configureMessageConverters(List> converters) { + /* + 先把JackSon的消息转换器删除. + 备注: (1)源码分析可知,返回json的过程为: + Controller调用结束后返回一个数据对象,for循环遍历conventers,找到支持application/json的HttpMessageConverter,然后将返回的数据序列化成json。 + 具体参考org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法 + (2)由于是list结构,我们添加的fastjson在最后。因此必须要将jackson的转换器删除,不然会先匹配上jackson,导致没使用fastjson + */ +// for (int i = converters.size() - 1; i >= 0; i--) { +// if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) { +// converters.remove(i); +// } +// } + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = getFastJsonHttpMessageConverter(); + converters.add(0, fastJsonHttpMessageConverter); + } + + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new StringToDateConverter()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/constant/AttendanceStatusConstant.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/constant/AttendanceStatusConstant.java new file mode 100644 index 0000000..d98892b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/constant/AttendanceStatusConstant.java @@ -0,0 +1,77 @@ +package jnpf.constant; + +/** + * 考勤状态常量 + * + * @author Generated + * @create 2026-04-15 + */ +public class AttendanceStatusConstant { + + /** + * 时段类型:上班卡 + */ + public static final int PERIOD_TYPE_MORNING = 1; + + /** + * 时段类型:下班卡 + */ + public static final int PERIOD_TYPE_AFTERNOON = 2; + + /** + * 考勤状态:正常 + */ + public static final int STATUS_NORMAL = 1001; + + /** + * 考勤状态:迟到 + */ + public static final int STATUS_LATE = 1002; + + /** + * 考勤状态:早退 + */ + public static final int STATUS_EARLY_LEAVE = 1003; + + /** + * 考勤状态:缺卡 + */ + public static final int STATUS_MISSING_CARD = 1004; + + /** + * 考勤状态:请假 + */ + public static final int STATUS_LEAVE = 3005; + + /** + * 考勤状态:出差 + */ + public static final int STATUS_BUSINESS_TRIP = 2006; + + /** + * 考勤状态:外出 + */ + public static final int STATUS_OUT_OFFICE = 2007; + + /** + * 考勤状态:外勤 + */ + public static final int STATUS_OUTER = 2008; + + /** + * 考勤状态:旷工 + */ + public static final int STATUS_ABSENT = 1009; + + /** + * 考勤状态:公休 + */ + public static final int STATUS_PUBLIC_HOLIDAY = 1010; + + /** + * 私有构造函数,防止实例化 + */ + private AttendanceStatusConstant() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/apply/FtbCultivatePromotionPostApplyForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/apply/FtbCultivatePromotionPostApplyForAppController.java new file mode 100644 index 0000000..d0043af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/apply/FtbCultivatePromotionPostApplyForAppController.java @@ -0,0 +1,72 @@ +package jnpf.cultivate.controller.app.apply; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePromotionPostApplyService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyCreateDto; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyDto; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyVO; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyWithPerVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * app 岗位晋升申请模块 + * @author penghao + */ +@RestController +@RequestMapping("/cul-post-apply-app") +public class FtbCultivatePromotionPostApplyForAppController { + + + @Resource + FtbCultivatePromotionPostApplyService postApplyService; + + + /** + * 获取岗位申请列表 + * @param dto 岗位申请查询参数 + * @param page 分页信息 + * @return 岗位申请列表结果 + */ + @GetMapping("/getList") + @Operation(summary ="查看岗位申请列表") + public ActionResult> getList(FtbCultivatePromotionPostApplyDto dto, CultivatePage page) { + PageListVO voPageListVO = postApplyService.getList(dto, page); + return ActionResult.success(voPageListVO); + } + + + /** + * 初始化岗位申请 + * + * @param createDto 岗位申请创建数据传输对象 + * @return + */ + @PostMapping("/initPromApplication") + @Operation(summary = "初始化岗位申请") + public ActionResult initiateAPromotionApplication(@Validated @RequestBody FtbCultivatePromotionPostApplyCreateDto createDto) { + createDto.setSource(1); + postApplyService.initiateAPromotionApplication(createDto); + return ActionResult.success(); + } + + + /** + * 查询岗位申请详情 + * @param dto 岗位申请数据传输对象 + * @return ActionResult 响应结果对象,包含岗位申请及相关信息 + */ + @GetMapping("/viewPromApplication") + @Operation(summary = "查询岗位申请详情") + public ActionResult viewPromotionApplications(FtbCultivatePromotionPostApplyDto dto) { + FtbCultivatePromotionPostApplyWithPerVO withPerVO = postApplyService.viewPromotionApplications(dto,"1"); + return ActionResult.success(withPerVO); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseAppController.java new file mode 100644 index 0000000..bb74908 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseAppController.java @@ -0,0 +1,77 @@ +package jnpf.cultivate.controller.app.casebase; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCaseBaseService; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseDTO; +import jnpf.model.cultivate.dto.casebase.app.FtbCultivateCaseBaseCreatDTO; +import jnpf.model.cultivate.vo.casebase.FtbCultivateCaseBaseVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * app案例库 + * @Author: peng.hao + * @create: 2024/7/22:11:28 + */ +@RestController +@RequestMapping("/app/case-base") +public class FtbCultivateCaseBaseAppController { + + @Resource + FtbCultivateCaseBaseService caseBaseService; + + /** + * 列表展示 + * @param dto + * @return + */ + @GetMapping("/listDisplay") + public ActionResult> listDisplay(FtbCultivateCaseBaseDTO dto){ + return ActionResult.success(caseBaseService.listDisplayForApp(dto)); + } + + /** + * 详情 + * @param id + * @return + */ + @GetMapping("/getDetail/{id}") + public ActionResult getDetail(@PathVariable("id") String id){ + return ActionResult.success(caseBaseService.getDetail(id)); + } + /** + * 删除案例 + * @param id + * @return + */ + @GetMapping("/deleteCase/{id}") + public ActionResult deleteCase(@PathVariable("id") String id){ + caseBaseService.removeById(id); + return ActionResult.success(); + } + + + /** + * 创建案例库 + * @return + */ + @PostMapping("/createCaseLibrary") + public ActionResult createCaseLibrary(@RequestBody @Validated FtbCultivateCaseBaseCreatDTO caseBaseCreatDTO){ + caseBaseService.caseBaseCreatDTO(caseBaseCreatDTO); + return ActionResult.success(); + } + /** + * 修改案例库 + * @return + */ + @PutMapping("/updateCaseLibrary") + public ActionResult updateCaseLibrary(@RequestBody @Validated FtbCultivateCaseBaseCreatDTO caseBaseCreatDTO){ + caseBaseService.updateCaseLibrary(caseBaseCreatDTO); + return ActionResult.success(); + } +} + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseLikeAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseLikeAppController.java new file mode 100644 index 0000000..5ac225c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/casebase/FtbCultivateCaseBaseLikeAppController.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.controller.app.casebase; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.FtbCultivateCaseBaseLikeMapper; +import jnpf.model.cultivate.po.casebase.FtbCultivateCaseBaseLike; +import jnpf.util.UserProvider; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * @Title: web案例库点赞 + * @Author: peng.hao + * @create: 2024/7/22:11:30 + */ +@RestController +@RequestMapping("/app/case-base/like") +public class FtbCultivateCaseBaseLikeAppController { + + @Resource + FtbCultivateCaseBaseLikeMapper caseBaseLikeMapper; + + /** + * 点赞案例 + * @param id 主键 + * @return + */ + @GetMapping("/likeTheCase") + public ActionResult likeTheCase(@RequestParam String id){ + UserInfo user = UserProvider.getUser(); + FtbCultivateCaseBaseLike like = new FtbCultivateCaseBaseLike(); + like.setLikeUserId(user.getUserId()); + like.setGainedId(id); + caseBaseLikeMapper.insert(like); + return ActionResult.success(); + } + + /** + * 点赞取消 + * @param id + * @return + */ + @DeleteMapping("/likeCancel") + public ActionResult likeCancel(@RequestParam String id){ + UserInfo user = UserProvider.getUser(); + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.eq(FtbCultivateCaseBaseLike::getLikeUserId,user.getUserId()); + lambdaed.eq(FtbCultivateCaseBaseLike::getGainedId,id); + caseBaseLikeMapper.delete(lambdaed); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/course/FtbCultivateCourseAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/course/FtbCultivateCourseAppController.java new file mode 100644 index 0000000..b70bc3a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/course/FtbCultivateCourseAppController.java @@ -0,0 +1,218 @@ +package jnpf.cultivate.controller.app.course; + +import cn.hutool.core.lang.UUID; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.cultivate.service.CultivateCourseMsgService; +import jnpf.cultivate.service.CultivateCourseMsgUserService; +import jnpf.cultivate.service.FtbCultivateCourseAppService; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.course.app.FtbChapterStudyDTO; +import jnpf.model.cultivate.dto.course.app.FtbCultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.dto.course.app.FtbGlobalCurriculumAppWrapDTO; +import jnpf.model.cultivate.dto.course.app.FtbUserLevelMessageReadDTO; +import jnpf.model.cultivate.vo.course.app.*; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * app端课程模块 + * + * @author fantaibao + * @date 2023/12/19 + */ +@RestController +@RequestMapping("/app/ftb-cultivate-course") +public class FtbCultivateCourseAppController { + @Resource + private FtbCultivateCourseAppService ftbCultivateCourseAppService; + @Resource + private CultivateCourseMsgService cultivateCourseMsgService; + @Resource + private CultivateCourseMsgUserService msgUserService; + @Resource + private FtbCultivateCourseChapterService ftbCultivateCourseChapterService; + + @Autowired + private RedisTemplate redisTemplate; + /** + * App查询用户未读信息列表 + * @return + */ + @GetMapping("/queryUserUnreadMsg") + public ActionResult> queryUserUnreadMsg() { + List list = cultivateCourseMsgService.queryUserUnreadMsg(); + return ActionResult.success(list); + } + + /** + * App查询用户课程未勾选“课程内容更新需要重新学习”信息列表 + * + * @param courseId 课程id(必传) + * @return + */ + @GetMapping("/query-user-unread-update") + public ActionResult> queryUserUnreadMsgUpdate(String courseId) { + List list = cultivateCourseMsgService.queryUserUnreadMsgUpdate(courseId); + return ActionResult.success(list); + } + + /** + * App更新用户已读信息 + * @param dto + * @return + */ + @PutMapping("/updateInfoByMsgId") + public ActionResult updateInfoByMsgId(@RequestBody FtbCultivateCourseMsgForAppDTO dto) { + msgUserService.updateInfoByMsgId(dto); + return ActionResult.success(); + } + + /** + * APP课程学习模块-课程详情 + * + * @param courseId 课程主键id,必填 + * @return {@link ActionResult}<{@link FtbCourseDetailsAppVO}> + */ + @GetMapping("/course-details") + public ActionResult courseDetails(@RequestParam("courseId") String courseId, + @RequestParam("userId") String userId) { + FtbCourseDetailsAppVO ftbCourseDetailsAppVO = ftbCultivateCourseAppService.courseDetails(courseId,userId); + return ActionResult.success(ftbCourseDetailsAppVO); + } + + /** + * APP课程学习模块-课程大纲 + * + * @param courseId 课程主键id,必填 + * @return {@link ActionResult}<{@link FtbCourseDetailsAppVO}> + */ + @GetMapping("/course-outline") + public ActionResult courseOutline(@RequestParam("courseId") String courseId, + @RequestParam("userId") String userId) { + String lockKey = "course-outline:" + courseId + ":" + userId; + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.fastUUID(), 3, TimeUnit.SECONDS)) { + try { + + FtbCourseOutlineAppVO ftbCourseOutlineAppVOS = ftbCultivateCourseAppService.courseOutline(courseId, userId); + return ActionResult.success(ftbCourseOutlineAppVOS); + } catch (Exception e) { + throw e; + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("访问太快了"); + } + + } + + + + /** + * App课程学习模块-增加当前课程浏览量 + * + * @param courseId 课程主键id,必填 + * @return {@link ActionResult} + */ + @PutMapping("/course-browsing/{id}") + public ActionResult courseBrowsing(@PathVariable("id") String courseId) { + ftbCultivateCourseAppService.courseBrowsing(courseId); + return ActionResult.success(); + } + + /** + * App课程学习模块-完成章节学习 + * + * @param ftbChapterStudyDTO + * @return {@link ActionResult} + */ + @PostMapping("/chapter-study") + public ActionResult chapterStudy(@Validated @RequestBody FtbChapterStudyDTO ftbChapterStudyDTO) { + UserInfo userInfo = UserProvider.getUser(); + + String lockKey = "chapterStudy:" + userInfo.getUserId()+":"+ftbChapterStudyDTO.getType()+":"+ftbChapterStudyDTO.getCourseId()+":"+ftbChapterStudyDTO.getChapterId(); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) { + try { + ftbCultivateCourseAppService.chapterStudy(userInfo, ftbChapterStudyDTO); + return ActionResult.success(); + } catch (Exception e) { + throw e; + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("请不要重复提交"); + } + } + + /** + * App课程学习模块-时长记录 + * + * @param ftbChapterStudyDTO + * @return {@link ActionResult} + */ + @PostMapping("/duration-record") + public ActionResult durationRecord(@Validated @RequestBody FtbChapterStudyDTO ftbChapterStudyDTO) { + UserInfo userInfo = UserProvider.getUser(); + ftbCultivateCourseAppService.durationRecord(userInfo, ftbChapterStudyDTO); + return ActionResult.success(); + } + + /** + * App课程学习模块-章节详情 + */ + @GetMapping("/chapter-details/{id}") + public ActionResult chapterDetails(@PathVariable("id") String id) { + FtbChapterAppDetails ftbChapterAppDetails = ftbCultivateCourseAppService.chapterDetails(id); + return ActionResult.success(ftbChapterAppDetails); + } + + + /** + * 查询用户职等是否发生变化 + * + * @param type 类型(必传)0晋升通道,1岗位学习 + * @return {@link ActionResult} + */ + @GetMapping("/check-job-grade-changes") + public ActionResult> checkJobGradeChanges(Integer type) { + List result = cultivateCourseMsgService.checkJobGradeChanges(type); + return ActionResult.success(result); + } + + /** + * 岗位职等发生变化时标记消息已读 + * + * @return {@link ActionResult} + */ + @PutMapping("/user-level-message-has-been-read") + public ActionResult userLevelMessageHasBeenRead(@RequestBody FtbUserLevelMessageReadDTO ftbUserLevelMessageReadDTO) { + cultivateCourseMsgService.userLevelMessageHasBeenRead(ftbUserLevelMessageReadDTO); + return ActionResult.success(); + } + + /** + * 通用课程列表 + * + * @param cultivatePage 分页参数 + * @return {@link ActionResult} + */ + @GetMapping("/general-course-list") + public ActionResult generalCourseList(CultivatePage cultivatePage + , FtbGlobalCurriculumAppWrapDTO ftbGlobalCurriculumAppWrapDTO) { + Page page = cultivatePage.coverCultivatePage(); + FtbGlobalCurriculumAppWrapVO result = ftbCultivateCourseChapterService.generalCourseList(page, ftbGlobalCurriculumAppWrapDTO); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/exam/ExamController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/exam/ExamController.java new file mode 100644 index 0000000..c745402 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/exam/ExamController.java @@ -0,0 +1,401 @@ +package jnpf.cultivate.controller.app.exam; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.model.cultivate.dto.statistics.CultivateUserExamCountDTO; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.req.exam.*; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.CourseEnums; +import jnpf.permission.entity.PositionEntity; +import jnpf.util.FtbUtil; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * app考试模块 + * + * @author xxxxx + */ +@Slf4j +@RestController +@RequestMapping("/app/exam") +public class ExamController { + /** + * 考试服务 + */ + @Resource + private FtbCultivateExamService examService; + + /** + * 考试用户服务 + */ + @Resource + private FtbCultivateExamUserService examUserService; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 开始考试界面,根据考试ID查询考试信息 + * + * @param examId 考试id + * @return + */ + @GetMapping("/queryExamDetail/{examId}") + public ActionResult queryExamDetail(@PathVariable("examId") String examId) { + ExamAppVo vo = examService.examInfo(examId); + if (vo != null) { +// 查询这个考试已经完成的考试人数 + vo.setCompleteUserNum(examUserService.queryCompleteNumForExamId(examId)); + } + return ActionResult.success(vo); + } + + /** + * app批阅考试查询考试信息(简单) + * + * @param userExamId 用户考试记录id + * @return + */ + @GetMapping("/queryUserExamSimpleMsg/{userExamId}") + public ActionResult queryUserExamSimpleMsg(@PathVariable("userExamId") String userExamId) { + return ActionResult.success(examUserService.queryUserExamSimpleMsg(userExamId)); + } + + /** + * 查询用户考试记录基本信息 + * + * @param userExamId + * @return + */ + @GetMapping("/queryBaseUserExamInfo/{userExamId}") + public ActionResult queryBaseUserExamInfo(@PathVariable("userExamId") String userExamId) { + return ActionResult.success(examUserService.queryBaseUserExamInfo(userExamId)); + } + + + /** + * 开始考试-查询考试的所有题目信息 + * + * @param userExamId 用户考试id + * @param req + * @return + */ + @GetMapping("/queryQuestionListForExam/{userExamId}") + public ActionResult queryQuestionListForExam(@PathVariable("userExamId") String userExamId, @Valid AppQueryExamQuestionReq req) { + return ActionResult.success( QuestionAnalysisUtil.convertPaperQuestionVo(examService.queryExamQuestionListByUserExamId(userExamId), Collections.emptyList())); + } + + + /** + * 重新开始考试 + * + * @param userExamId 用户考试id + * @return + */ + @GetMapping("/reStartExam/{userExamId}") + public ActionResult reStartExam(@PathVariable("userExamId") String userExamId) { + //如果是随机考试 还要随机抽题 + return ActionResult.success(BeanUtil.copyProperties(examUserService.reStartExam(userExamId), UserExamDetailVo.class)); + } + + /** + * 提交试卷 + * + * @param userExamId 用户考试ID + * @param req + * @return + */ + @PostMapping("/sub-exam/{userExamId}") + public ActionResult subExam(@PathVariable("userExamId") String userExamId, @RequestBody @Valid SubExamQuestionReq req) { + + FtbCultivateExamUser examUser = examUserService.getById(userExamId); + + if (null == examUser || examUser.getEnabledMark() == 0) { + return ActionResult.fail(1000, "考试记录已经删除"); + } + String lockKey = "subExam" + userExamId; + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(), 10, TimeUnit.SECONDS)) { + try { + SubExamVo subExamVo = examUserService.subExam(userExamId, req); + return ActionResult.success(subExamVo); + } catch (Exception e) { + throw e; + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("考试提交中,请不要重复提交"); + } + } + + + /** + * 视为作弊提交试卷 + * + * @param cheatType 作弊类型 1-超过切屏次数视为作弊,2-超过考试时间无交互视为作弊 + * @param userExamId 用户考试ID + * @param req + * @return + */ + @PostMapping("/setCheat/{cheatType}/{userExamId}") + public ActionResult setCheat(@PathVariable("cheatType") Integer cheatType, + @PathVariable("userExamId") String userExamId, + @RequestBody @Valid SubExamQuestionReq req) throws Exception { + + String lockKey = "subExam" + userExamId; + String lockValue = UUID.randomUUID().toString(); + Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(lock)) { + try { + examUserService.setCheat(userExamId, cheatType, req); + return ActionResult.success(true); + } catch (Exception e) { + // ✔ 建议加日志 + log.error("setCheat异常 userExamId: {}, cheatType: {}", userExamId, cheatType, e); + throw e; + } finally { + // ✔ 防止误删锁(关键) + Object value = redisTemplate.opsForValue().get(lockKey); + if (null != value && lockValue.equals(value.toString())) { + redisTemplate.delete(lockKey); + } + } + } else { + throw new RuntimeException("请不要重复提交"); + } + } + + /** + * 根据考试id查询考试整体结果 + * + * @param userExamId 用户考试id + * @return + */ + @GetMapping("/queryExamResult/{userExamId}") + public ActionResult queryExamResult(@PathVariable("userExamId") String userExamId) { + UserExamDetailVo examVo = examUserService.getUserExamDetail(userExamId, false); + AppExamUserVo vo = BeanUtil.copyProperties(examVo, AppExamUserVo.class); + //计算正确lv 和错误lv + int totleNum = vo.getQuestionNumber(); + int correctNum = vo.getCorrectCount(); + int errorNum = totleNum - correctNum; + if(errorNum<=0){ + errorNum=0; + } + vo.setErrorCount(errorNum); + vo.setCorrectLv(QuestionAnalysisUtil.calculatePercentage(correctNum, totleNum) + ""); + vo.setErrorLv(QuestionAnalysisUtil.calculatePercentage(errorNum, totleNum) + ""); + return ActionResult.success(vo); + } + + /** + * 根据考试id查询考试试题结果列表 + * + * @param userExamId 用户考试id + * @return + */ + @GetMapping("/queryExamQuestionList/{userExamId}") + public ActionResult>> queryExamQuestionList(@PathVariable("userExamId") String userExamId) { + UserExamDetailVo examVo = examUserService.getUserExamDetail(userExamId, true); + return ActionResult.success(examVo.getQuestionMap()); + } + + + /** + * 查询我的考试列表 + * + * @param req + * @return + */ + @GetMapping("/queryMyExamList") + public ActionResult> queryMyExamList(@Valid QueryExamListReq req) { + PageInfo pageVo = examUserService.queryMyExamList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 查询我的岗位考试列表(完成考试合格且需要考试合格才能鉴定 切未鉴定的岗位) + * + * @return + */ + @GetMapping("/queryAlertPostionIdentify") + public ActionResult> queryAlertPostionIdentify() { + return ActionResult.success(examUserService.queryAlertPostionIdentify(UserProvider.getLoginUserId())); + } + + + /** + * 删除我的考试记录(已经作废的常规考试) + * + * @param userExamId 用户考试ID,以逗号分开 + * @return + */ + @GetMapping("/hiddenMyExamRecord/{userExamId}") + public ActionResult hiddenMyExamRecord(@PathVariable("userExamId") String userExamId) { + examUserService.hiddenMyExamRecord(userExamId); + return ActionResult.success(true); + } + + /** + * 查询我下属考试数量,用于是否展示Tab + * + * @return + */ + @GetMapping("/queryMyBlowExamNum") + public ActionResult queryMyBlowExamNum() { + return ActionResult.success(examUserService.queryMyBlowExamNum()); + } + + + /** + * 待我批阅(下属的) + * + * @param req + * @return + */ + @GetMapping("/queryWaitMyReadOver") + public ActionResult> queryWaitMyReadOver(@Valid QueryExamListReq req) { + PageInfo pageVo = examUserService.queryWaitMyReadOver(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 我完成的批阅(下属的) + * + * @param req + * @return + */ + @GetMapping("/queryMyCompleteReadOver") + public ActionResult> queryMyCompleteReadOver(@Valid QueryExamListReq req) { + PageInfo pageVo = examUserService.queryMyCompleteReadOver(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 逾期未考(下属的) + * + * @param req + * @return + */ + @GetMapping("/expireExam") + public ActionResult> expireExam(QueryExpireListReq req) { + PageInfo pageVo = examUserService.expireExam(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 清除逾期未考(下属的) + * + * @param userExamIds 用户考试ID,以逗号分开 + * @return + */ + @GetMapping("/clearExpireExam") + public ActionResult clearExpireExam(@RequestParam("userExamIds") String userExamIds) { + examUserService.clearExpireExam(userExamIds); + return ActionResult.success(true); + } + + /** + * 批阅考试 + * + * @param req + * @return + */ + @PostMapping("/readOver") + public ActionResult readOver(@RequestBody @Valid ReadOverExamReq req) { + FtbCultivateExamUser examUser = examUserService.getById(req.getUserExamId()); + if (null == examUser || examUser.getEnabledMark() == 0) { + return ActionResult.fail(1000, "考试记录已经删除"); + } + examUserService.readOver(req); + return ActionResult.success(true); + } + + /** + * 考试排行版 + * + * @param examId 考试id + * @return + */ + @GetMapping("/ranking/{examId}") + public ActionResult> rankingList(@PathVariable("examId") String examId, + QueryExamRankListReq req) { + PageInfo pageVo = examUserService.rankIngList(examId, req); + + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + + } + + /** + * 查询我在这次考试的数据 + * + * @param examId 考试id + * @return + */ + @GetMapping("/ranking/my/{examId}") + public ActionResult queryMyRank(@PathVariable("examId") String examId) { + return ActionResult.success(examUserService.queryMyRank(examId)); + + } + + + /** + * 培训功能优化,统计组织下用户考试合格率、合格人数、考试总人数 + * + * @param dto + * @return + */ + @GetMapping("/queryCultivateCout") + public ActionResult queryCultivateCout(CultivateUserExamCountDTO dto) { + return ActionResult.success(examUserService.queryCultivateCount(dto, new ArrayList<>())); + } + + + /** + * 根据岗位ID,统计 岗位考试人数、优秀人数、合格人数、不合格人数 + * + * @param positionId 岗位ID + * @return + */ + @GetMapping("/queryCultivateCountForPositionId") + public ActionResult queryCultivateCountForPositionId(String positionId) { + return ActionResult.success(examUserService.queryCultivateCountForPositionId(positionId)); + } + + + /** + * 员工首页-查询考试成绩列表 + * + * @param req + * @return + */ + @GetMapping("/queryCultivateExamList") + public ActionResult> queryCultivateExamList(@Valid QueryCultivateExamReq req) { + String loginUserId = UserProvider.getLoginUserId(); + if (StringUtils.isEmpty(req.getUserId())) { + req.setUserId(loginUserId); + } + PageInfo pageVo = examUserService.queryExamList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/identify/CultivateIdentifyAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/identify/CultivateIdentifyAppController.java new file mode 100644 index 0000000..8012dab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/identify/CultivateIdentifyAppController.java @@ -0,0 +1,258 @@ +package jnpf.cultivate.controller.app.identify; + + +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.model.cultivate.dto.identify.*; +import jnpf.model.cultivate.vo.identify.*; +import jnpf.personnels.service.FtbPersonnelsPermissionsService; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * app实操鉴定 + * + * @author mouzeping + */ +@Slf4j +@RestController +@RequestMapping(value = "/cultivate/apply/app") +public class CultivateIdentifyAppController { + @Resource + private CultivateIdentifyApplyService applyService; + @Resource + private FtbPersonnelsPermissionsService ftbPersonnelsPermissionsService; + + /** + * app/列表 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "列表") + @GetMapping(value = "/list") + public ActionResult> getPageList(@Valid IdentifyApplyListAppDto req) { + try { + log.info("app/列表,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + PageInfo page = applyService.getPageAppList(req); + long ent = System.currentTimeMillis(); + log.info("app/列表,耗时=>{}", ent - st + " 毫秒"); + PaginationVO pagination = FtbUtil.getPagination(page); +// pagination.setTotal(applyService.getPageAppListCount(req)); + return ActionResult.page(page.getList(), pagination); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app/清空-清空全部 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "清空-清空全部") + @DeleteMapping(value = "/deleteIds") + public ActionResult deleteIds(@Valid @RequestBody IdentifyApplyDeleteAppDto req) { + try { + log.info("app/清空-清空全部,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + applyService.deleteIds(req); + long ent = System.currentTimeMillis(); + log.info("app/清空-清空全部,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU003.get()); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app/鉴定详情 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "鉴定详情") + @GetMapping(value = "/identifyInfo") + public ActionResult getApplyIdentifyInfoApp(@Valid IdentifyTableInfoDto req) { + try { + log.info("app/鉴定详情,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyInfoAppVo infoAppVo = applyService.getIdentifyApplyInfoApp(req); + long ent = System.currentTimeMillis(); + log.info("app/鉴定详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(infoAppVo); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app/鉴定项详情 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "鉴定项详情") + @GetMapping(value = "/identifyItemsInfo") + public ActionResult getApplyIdentifyItemsInfoApp(@Valid IdentifyTableInfoDto req) { + try { + log.info("app/鉴定项详情,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyItemsInfoAppVo infoAppVo = applyService.getApplyIdentifyItemsInfoApp(req); + long ent = System.currentTimeMillis(); + log.info("app/鉴定项详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(infoAppVo); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app/鉴定提交 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "鉴定提交") + @PostMapping(value = "/submit") + public ActionResult submit(@Valid @RequestBody IdentifyApplySubmitDto req) { + try { + applyService.applyDataSubmit(req); + return ActionResult.success(MsgCode.SU005.get()); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app/设置鉴定时间 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "设置鉴定时间") + @PutMapping(value = "/setTime") + public ActionResult setTime(@Valid @RequestBody IdentifyApplySetTimeAppDto req) { + try { + log.info("app/设置鉴定时间,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + applyService.setApplyDataTime(req); + long ent = System.currentTimeMillis(); + log.info("app/设置鉴定时间,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU005.get()); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * app/鉴定结果详情 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "鉴定结果详情") + @GetMapping(value = "/basicInfo") + public ActionResult getApplyBasicInfoApp(@Valid IdentifyTableInfoDto req) { + try { + log.info("app/鉴定结果详情,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyApplyBasicInfoAppVo basicInfoAppVo = applyService.getApplyBasicInfoApp(req); + long ent = System.currentTimeMillis(); + log.info("app/鉴定结果详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(basicInfoAppVo); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + + /** + * app/鉴定项结果详情 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "鉴定项结果详情") + @GetMapping(value = "/info") + public ActionResult getApplyInfoApp(@Valid IdentifyTableInfoDto req) { + try { + log.info("app/鉴定项结果详情,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyApplyInfoAppVo infoAppVo = applyService.getApplyInfoApp(req); + long ent = System.currentTimeMillis(); + log.info("app/鉴定项结果详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(infoAppVo); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 本人申请实操鉴定 + * + * @param req 入参 + * @return {@link ActionResult} + */ + @PostMapping("/my-own-practical-appraisal") + public ActionResult iApplyForPracticalAppraisal(@Valid @RequestBody IdentifyApplySaveDto req) { + req.setSource(ApplySourceEnum.APPLY_IN_PERSON.getCode()); + applyService.applyDataSave(req); + return ActionResult.success(); + } + + /** + * 根据组织获取具有鉴定权限的鉴定人 + * + * @param orgId 组织 ID + * @return {@link ActionResult }<> + */ + @GetMapping("/query-authority-appraiser") + public ActionResult> queryAuthorityAppraiser(@RequestParam(value = "orgId") String orgId) { + List result = ftbPersonnelsPermissionsService.queryAuthorityAppraiser(orgId); + return ActionResult.success(result); + } + + /** + * b本地test打开可见性 + * @param userId + * @param postId + * @return + */ + @GetMapping("/applyService") + public ActionResult applyService(@RequestParam(value = "userId") String userId, + @RequestParam(value = "postId") String postId ) { + applyService.turnOnVisibility(postId, userId); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateLearnTaskForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateLearnTaskForAppController.java new file mode 100644 index 0000000..8f61571 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateLearnTaskForAppController.java @@ -0,0 +1,81 @@ +package jnpf.cultivate.controller.app.learn; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateLearnTaskInfoService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishStatisticsVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskListVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * app培训任务管理 + * @Author: peng.hao + * @create: 2024/9/9:11:21 + */ +@Slf4j +@RestController +@RequestMapping("/app/learn-task") +public class FtbCultivateLearnTaskForAppController { + + @Resource + private FtbCultivateLearnTaskInfoService cultivateLearnTaskInfoService; + + /** + * 培训任务列表 + */ + @GetMapping("/getTaskList") + public ActionResult> getTaskList(FtbCultivateLearnTaskListDto taskListDto) { + return ActionResult.success(cultivateLearnTaskInfoService.getTaskList(taskListDto)); + } + + + /** + * 培训任务列表列表小气泡计数 + */ + @GetMapping("/getTaskCount") + public ActionResult> getTaskCount(@RequestParam(value = "keyWords",defaultValue = "",required = false) String keyWords) { + return ActionResult.success(cultivateLearnTaskInfoService.getTaskCount(keyWords)); + } + + + /** + * 任务详情展示 + */ + @GetMapping("/getLearnTaskInfo") + public ActionResult getLearnTaskInfo(@RequestParam("taskId") String taskId) { + return ActionResult.success(cultivateLearnTaskInfoService.getLearnTaskInfo(taskId)); + } + + /** + * 完成情况统计 + * @param taskId 任务id + * @return + */ + @GetMapping("/getCompletionStatistics") + public ActionResult getCompletionStatistics(@RequestParam("taskId") String taskId) { + return ActionResult.success(cultivateLearnTaskInfoService.getCompletionStatistics(taskId)); + } + /** + * 完成情况列表 + * @param taskId 任务id + * @return + */ + @GetMapping("/getListOfCompletions") + public ActionResult> getListOfCompletions(@RequestParam("taskId") String taskId, + CultivatePage page) { + PageListVO listOfCompletions = cultivateLearnTaskInfoService.getListOfCompletions(taskId, page, null); + return ActionResult.success(listOfCompletions); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateMyLearnTaskForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateMyLearnTaskForAppController.java new file mode 100644 index 0000000..faf52c6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/learn/FtbCultivateMyLearnTaskForAppController.java @@ -0,0 +1,116 @@ +package jnpf.cultivate.controller.app.learn; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateMyLearnTaskInfoService; +import jnpf.exception.HandleException; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.vo.course.app.FtbAppTaskCountVO; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskExamInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskIdentificationInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoForAppVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateMyLearnTaskListVO; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * app我的培训任务 + * @Author: peng.hao + * @create: 2024/9/9:11:21 + */ +@Slf4j +@RestController +@RequestMapping("/app/my-learn-task") +public class FtbCultivateMyLearnTaskForAppController { + + @Resource + private FtbCultivateMyLearnTaskInfoService cultivateMyLearnTaskInfoService; + + /** + * 培训任务列表 + */ + @GetMapping("/getTaskList") + public ActionResult> taskList(FtbCultivateLearnTaskListDto taskListDto) { + return ActionResult.success(cultivateMyLearnTaskInfoService.taskList(taskListDto)); + } + + /** + * 立即完成 + */ + @PutMapping("/completeTask/{taskId}") + public ActionResult completeTask(@PathVariable("taskId") String taskId) { + cultivateMyLearnTaskInfoService.completeTask(taskId); + return ActionResult.success(); + } + + /** + * 培训任务列表计数 + */ + @GetMapping("/getTaskCount") + public ActionResult> taskListCount(@RequestParam(value = "keyWords",defaultValue = "",required = false) String keyWords) { + return ActionResult.success(cultivateMyLearnTaskInfoService.taskListCount(keyWords)); + } + + + /** + * 任务详情(基本信息) + * @param taskId 任务id + */ + @GetMapping("/getTaskInfo") + public ActionResult getLearnTaskInfoForApp(@RequestParam("taskId") String taskId) { + return ActionResult.success(cultivateMyLearnTaskInfoService.getLearnTaskInfoForApp(taskId)); + } + + /** + * 任务课程列表 + * @param taskId 任务主键ID(必传) + * @param compulsory 是否必修,0必修,1选修 + */ + @GetMapping("/task-course-list") + public ActionResult> taskCourseList(@RequestParam("taskId") String taskId,@RequestParam("compulsory") Integer compulsory) throws HandleException { + // 当前用户id + String userId = UserProvider.getUser().getUserId(); + return ActionResult.success(cultivateMyLearnTaskInfoService.taskCourseList(taskId, userId,compulsory)); + } + + + /** + * 任务课程(数量) + * @param taskId 任务主键ID(必传) + */ + @GetMapping("/task-course-num") + public ActionResult taskCourseNum(@RequestParam("taskId") String taskId) throws HandleException { + // 当前用户id + String userId = UserProvider.getUser().getUserId(); + return ActionResult.success(cultivateMyLearnTaskInfoService.taskCourseNum(taskId, userId)); + } + /** + * 任务考试列表 + * @param taskId 任务主键ID(必传) + */ + @GetMapping("/task-exam-list") + public ActionResult> taskExamList(@RequestParam("taskId") String taskId) { + // 当前用户id + String userId = UserProvider.getUser().getUserId(); + return ActionResult.success(cultivateMyLearnTaskInfoService.taskExamList(taskId, userId)); + } + + /** + * 任务实操鉴定列表 + * @param taskId 任务主键ID(必传) + */ + @GetMapping("/task-identification-list") + public ActionResult> taskIdentificationList(@RequestParam("taskId") String taskId) { + // 当前用户id + String userId = UserProvider.getUser().getUserId(); + return ActionResult.success(cultivateMyLearnTaskInfoService.taskIdentificationList(taskId, userId)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/offline/FtbCultivateOfflineTrainAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/offline/FtbCultivateOfflineTrainAppController.java new file mode 100644 index 0000000..f8688e4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/offline/FtbCultivateOfflineTrainAppController.java @@ -0,0 +1,94 @@ +package jnpf.cultivate.controller.app.offline; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateOfflineTrainService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineTrainDTO; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineTrainSignInDTO; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineTrainUpdateDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainDetailsAppVO; +import jnpf.model.cultivate.vo.offline.FtbOfflineTrainingAppPageVO; +import jnpf.util.UserProvider; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * app线下培训模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/app/ftb-cultivate-offline-train") +public class FtbCultivateOfflineTrainAppController { + + @Resource + private FtbCultivateOfflineTrainService ftbCultivateOfflineTrainService; + + + /** + * 修改培训结果 + * + * @return {@link ActionResult} + */ + @PostMapping("/modify-training-results") + public ActionResult modifyTrainingResults(@RequestBody @Validated FtbCultivateOfflineTrainUpdateDTO offlineTrainUpdateDTO) { + ftbCultivateOfflineTrainService.updateTrainingResults(UserProvider.getUser(), offlineTrainUpdateDTO); + return ActionResult.success(); + } + + /** + * 新增线下培训 + * + * @return {@link ActionResult} + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Validated FtbCultivateOfflineTrainDTO ftbCultivateOfflineTrainDTO) { + ftbCultivateOfflineTrainService.add(ftbCultivateOfflineTrainDTO); + return ActionResult.success(); + } + + /** + * 线下培训详情 + * + * @param id 课程主键id + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @GetMapping("/details") + public ActionResult courseOfflineDetails(String id) { + FtbCultivateOfflineTrainDetailsAppVO appVO = ftbCultivateOfflineTrainService.offlineAppDetails(id); + return ActionResult.success(appVO); + } + + + /** + * 线下培训分页 + * + * @param cultivatePage 分页模型 + * @param type 查询类型,不传为全部培训,1为我发起的,2为我参与的,3为我负责的 + * @return {@link ActionResult}<{@link Page}<{@link FtbOfflineTrainingAppPageVO}>> + */ + @GetMapping("/offline-training-pagination") + public ActionResult> offlineTrainingApp(CultivatePage cultivatePage, Integer type) { + Page page = cultivatePage.coverCultivatePage("a.F_Id"); + page = ftbCultivateOfflineTrainService.offlineTrainingApp(page, type); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 线下培训签到 + * + * @return {@link ActionResult } + */ + @PostMapping("/signIn") + public ActionResult signIn(@RequestBody FtbCultivateOfflineTrainSignInDTO freightTrainSignInDTO) { + ftbCultivateOfflineTrainService.signIn(freightTrainSignInDTO); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/position/FtbCultivatePositionForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/position/FtbCultivatePositionForAppController.java new file mode 100644 index 0000000..fbe1a8c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/position/FtbCultivatePositionForAppController.java @@ -0,0 +1,88 @@ +package jnpf.cultivate.controller.app.position; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePositionForAppService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.app.FtbCultivatePositionForAppNewDTO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.position.app.FtbPopUpPromptVO; +import jnpf.model.cultivate.vo.position.app.FtbSubordinateLearningCoursesVO; +import jnpf.model.cultivate.vo.position.app.OnTheJobLearningCourseVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * app岗位学习模块 + * + * @author penghao + */ +@RestController +@RequestMapping("/position-app") +@Validated +public class FtbCultivatePositionForAppController { + + @Resource + private FtbCultivatePositionForAppService service; + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 岗位学习本岗课程 + */ + @GetMapping("/on-job-learning-course") + public ActionResult onTheJobLearningCourse(FtbCultivatePositionForAppNewDTO dto) { + OnTheJobLearningCourseVO result = service.onTheJobLearningCourse(dto); + return ActionResult.success(result); + } + + /** + * 岗位学习-下属学习课程 + */ + @GetMapping("/subordinate-learning-courses") + public ActionResult> subordinateLearningCourses(FtbCultivatePositionForAppNewDTO dto, CultivatePage page) { + Page result = page.coverCultivatePage(); + result = service.subordinateLearningCoursess(dto, result); + return ActionResult.success(CultivatePage.coverPageList(result)); + } + + /** + * 岗位学习-下属实操鉴定 + */ + @GetMapping("/subordinate-practical-appraisal") + public ActionResult> subordinatePracticalAppraisal(FtbCultivatePositionForAppNewDTO dto, CultivatePage page) { + PageListVO result = service.subordinatePracticalAppraisal(dto, page); + return ActionResult.success(result); + } + + /** + * 弹出提示 + * + * @param userId 用户id(必传) + * @param positionId 岗位id(必传) + * @return {@link ActionResult } + */ + @GetMapping("/pop-up-prompt") + public ActionResult popUpPrompt(String userId, String positionId) { + FtbPopUpPromptVO ftbPopUpPromptVO = service.popUpPrompt(userId, positionId); + return ActionResult.success(ftbPopUpPromptVO); + } + + /** + * 获取当前登录的用户信息 + * @return {@link ActionResult } + */ + @GetMapping("/get-curr-login-user-info") + public ActionResult getCurrLoginUserInfo() { + return ActionResult.success(userApiV2Util.getUserPrimaryBoundOne(userApiV2Util.getCurrentLoginUserId(), null)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCulProPostForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCulProPostForAppController.java new file mode 100644 index 0000000..53cd49d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCulProPostForAppController.java @@ -0,0 +1,96 @@ +package jnpf.cultivate.controller.app.promotion; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivatePromotionPostService; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionForAppDto; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionForAppVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * app晋升通道级别模块 + * + */ +@RestController +@RequestMapping("/cul-post-app") +public class FtbCulProPostForAppController { + + @Resource + private FtbCultivatePromotionPostService postService; + + + /** + * 根据当前用户id和岗位id 判断 当前是否启用 晋升通道 + * + * @param userId 当前用户id + * @param postId 岗位id + * @return false为未开启,true为开启 + * + */ + @GetMapping("/queryChannelIsEnb") + @Operation(method = "根据当前用户id和岗位id查询当前是否启用 晋升通道") + public ActionResult checkWhetherThePromotionChannelIsEnabled( + @RequestParam("userId") String userId, + @RequestParam("postId") String postId){ + boolean isEnb = postService.checkWhetherThePromotionChannelIsEnabled(userId,postId); + return ActionResult.success(isEnb); + } + + + + /** + * 根据当前用户id和岗位id查询当前已启动晋升通道和其他可选晋升通道 + * + * @param userId 用户id + * @param postId 岗位职能id + * @return {@link ActionResult}<{@link FtbCultivatePromotionForAppVO}> + */ + @GetMapping("/queryById") + @Operation(method = "根据当前用户id和岗位id查询当前已启动晋升通道和其他可选晋升通道") + public ActionResult queryThePromotedChannelsThatAreInitiated( + @RequestParam("userId") String userId, + @RequestParam("postId") String postId){ + FtbCultivatePromotionForAppVO promotionVO = postService.queryThePromotedChannelsThatAreInitiated(userId,postId); + return ActionResult.success(promotionVO); + } + + /** + * 更改启用状态 + * + * @param forAppDto + */ + @PutMapping("/changeStatus") + @Operation(method = "更改启用状态") + public ActionResult changeTheEnablementStatus(@RequestBody FtbCultivatePromotionForAppDto forAppDto){ + String s = postService.changeTheEnablementStatus(forAppDto); + return ActionResult.success(s); + } + + + /** + * 查询当前用户是否允许修改通道 + * + */ + @GetMapping("/userIsAllowed/{userId}") + @Operation(method = "查询当前用户是否允许修改通道") + public ActionResult userIsAllowed(@PathVariable("userId") String userId){ + return ActionResult.success(postService.userIsAllowed(userId)); + } + + /** + * 修改是否允许下属变更 + * 此接口用于修改是否允许下属变更 + * + * @param forAppDto 修改设置的请求数据 + * @return 操作结果 + */ + @PutMapping("/changeIsAllowed") + @Operation(method = "更改是否允许下属变更") + public ActionResult changeWhetherTheChangeIsAllowed(@RequestBody FtbCultivatePromotionForAppDto forAppDto){ + postService.changeWhetherTheChangeIsAllowed(forAppDto); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCultivatePromotionNewForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCultivatePromotionNewForAppController.java new file mode 100644 index 0000000..669ff07 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/promotion/FtbCultivatePromotionNewForAppController.java @@ -0,0 +1,154 @@ +package jnpf.cultivate.controller.app.promotion; + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.model.cultivate.dto.promotion.FtbCultivateCreatMeMapInfoDTO; +import jnpf.model.cultivate.vo.promotion.*; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * app 学习地图管理(v1.1) + * + * @author hao.peng + */ +@RestController +@RequestMapping("/cul-pro-map-app") +public class FtbCultivatePromotionNewForAppController { + /** + * 服务对象 + */ + @Resource + private FtbCultivatePromotionNewService promotionService; + + /** + * 根据岗位id获取地图层级 + * + * @param postId + * @return + */ + @GetMapping("/getLearnMapLevels") + public ActionResult getMapLevel(@RequestParam("postId") String postId) { + FtbCultivatePromotionLevelMapVO learnMapLevels = promotionService.getLearnMapLevels(postId); + return ActionResult.success(learnMapLevels); + } + + + /** + * 查询当前用户的学习地图 学习阶段 + * + * @param userId 用户id + * @param postId 岗位id + * @return 学习阶段初始化为 1 阶段 + */ + @GetMapping("/queryTheCurrentUserLearningMapLevel") + public ActionResult queryTheCurrentUserLearningMapLevel(@RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + return ActionResult.success(promotionService.queryTheCurrentUserLearningMapLevel(userId, postId)); + } + + /** + * 校验前一个阶段岗位是否已经学习完成 + * + * @param level 需要校验的岗位阶段 + * @param userId 用户id + * @param postId 用户岗位id + * @return 空为已完成, 1 当前阶段未学习完成, 2岗位学习考试未完成, 3岗位学习实操鉴定未完成 , + * ( 4,岗位学习考试未合格, 5 岗位学习实操鉴定未合格 ) v1.2 新增 + */ + @GetMapping("/check-studying") + public ActionResult checkPreviousStagePostAveYouFinishedStudying(@RequestParam Integer level, + @RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + + return ActionResult.success(promotionService.checkStudying(level, userId, postId)); + } + + /** + * 查询对应阶段选择岗位列表 + * + * @param userId 用户id + * @param postId 岗位id + * @param level 等级 + * @param compulsory 是否必修,0必修,1选修 + * @return + */ + @GetMapping("/selectPositionsCorrespondingToTheStages") + public ActionResult selectPositionsCorrespondingToTheStages(@RequestParam("userId") String userId, + @RequestParam("postId") String postId, + @RequestParam("level") Integer level, + @RequestParam("compulsory") Integer compulsory) { + return ActionResult.success(promotionService.selectPositionsCorrespondingToTheStages(userId, postId, level, compulsory)); + } + + /** + * 根据初始岗位 和对应学习阶段 查询对应选择的岗位 + * + * @param userId 用户id + * @param postId 岗位id + * @param level 学习阶段 + * @return + */ + @GetMapping("/viewLearningMapBasedOnPosition") + public ActionResult> viewLearningMapBasedOnPosition(@RequestParam("userId") String userId, + @RequestParam("postId") String postId, + @RequestParam("level") Integer level) { + List result = promotionService.viewLearningMapBasedOnPosition(userId, postId, level); + return ActionResult.success(result); + } + + /** + * 对应人员选择岗位 + * + * @return + */ + @PostMapping("/correspondingPersonnelSelectPositions") + public ActionResult correspondingPersonnelSelectPositions(@Validated @RequestBody FtbCultivateCreatMeMapInfoDTO creatMeMapInfoDTO) { + promotionService.correspondingPersonnelSelectPositions(creatMeMapInfoDTO); + return ActionResult.success(); + } + + /** + * 查询下属学习阶段 + * + * @param userId 下属id + * @param postId 下属岗位id + * @return + */ + @GetMapping("/queryTheLearningStageOfSubordinates") + public ActionResult queryTheLearningStageOfSubordinates(@RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + return ActionResult.success(promotionService.queryTheLearningStageOfSubordinates(userId, postId)); + } + + /** + * 岗位学习地图发生变更重新选择 + * + * @param userId + * @param postId + * @return ture 变更 false 未变更 + */ + @GetMapping("/whetherTheJobLearningMapHasChanged") + public ActionResult whetherTheJobLearningMapHasChanged(@RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + return ActionResult.success(promotionService.whetherTheJobLearningMapHasChanged(userId, postId)); + } + + /** + * 学习地图-学习进度 + * + * @param userId 用户id(必传) + * @param postId 岗位id(必传) + * @return + */ + @GetMapping("/learningProgress") + public ActionResult learningProgress(@RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + return ActionResult.success(promotionService.learningProgress(userId, postId)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/statistic/FtbCultivateStatisticsForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/statistic/FtbCultivateStatisticsForAppController.java new file mode 100644 index 0000000..9dbb57f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/statistic/FtbCultivateStatisticsForAppController.java @@ -0,0 +1,111 @@ +package jnpf.cultivate.controller.app.statistic; + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.base.entity.ModuleEntity; +import jnpf.cultivate.service.FtbCultivateStatisticsService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.vo.statistics.CultivateStatisticsForAppCommonParam; +import jnpf.model.cultivate.vo.statistics.NumberofAppSessions; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * app培训统计 + * + * @Author: peng.hao + * @create: 2024/3/22 19:31 + */ +@Slf4j +@RestController +@RequestMapping("/app/cultivate-statistics") +public class FtbCultivateStatisticsForAppController { + + + @Resource + private FtbCultivateStatisticsService ftbCultivateStatisticsService; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 培训统计 + * + * @param dto + * @return + */ + @GetMapping("/query-number-sessions") + public ActionResult> trainingStatistics(FtbCultivateStatisticsDTO dto) { + List number = ftbCultivateStatisticsService.trainingStatistics(dto); + return ActionResult.success(number); + } + + /** + * 培训统计 + * + * @param dto + * @return + */ + @PostMapping("/query-number-sessions") + public ActionResult> trainingStatisticsPost(@RequestBody FtbCultivateStatisticsDTO dto) { + List number = ftbCultivateStatisticsService.trainingStatistics(dto); + return ActionResult.success(number); + } + /** + * 培训统计子 菜单有数据验证 + * + * @param dto + * @return flag = "1"(无数据) flag = null (有数据) + */ + @PostMapping("/have-exit-data") + public ActionResult> doesTheMenuHaveDataVerification(@RequestBody FtbCultivateStatisticsDTO dto) { + List batchOrgIds = dto.getBatchOrgIds(); + List orgListForCurrUser = userApiV2Util.getOrgListForCurrUser( + List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.TEAM),null); + if (CollUtil.isEmpty(orgListForCurrUser)) { + return ActionResult.success(new ArrayList<>()); + } + if (CollUtil.isEmpty(dto.getBatchOrgIds())) { + batchOrgIds = orgListForCurrUser.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } else { + List intersection = UserApiV2Util.getIntersection(batchOrgIds, orgListForCurrUser.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + if (CollUtil.isEmpty(intersection)) { + return ActionResult.success(new ArrayList<>()); + } else { + batchOrgIds = intersection; + } + } + + List commonParams = batchOrgIds.stream().map(item -> { + + CultivateStatisticsForAppCommonParam param = new CultivateStatisticsForAppCommonParam(); + String flag = null; + // 验证子集 +// ActionResult> oneLevelById = organizeApi.getChildrenOneLevelById(item); + List oneLevelById = userApiV2Util.organizeNextLevel(item, null); + if (CollUtil.isNotEmpty(oneLevelById)) { + List data = oneLevelById.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + List organizeApi = data.stream().filter(str -> !item.equals(str)).collect(Collectors.toList()); + if (CollUtil.isEmpty(organizeApi)) { + flag = "1"; + } + } else { + flag = "1"; + } + param.setOrgId(item); + param.setFlag(flag); + return param; + }).collect(Collectors.toList()); + return ActionResult.success(commonParams); + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/teaching/AppTeachingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/teaching/AppTeachingController.java new file mode 100644 index 0000000..50b5fbe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/app/teaching/AppTeachingController.java @@ -0,0 +1,182 @@ +package jnpf.cultivate.controller.app.teaching; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.TeachingRecordService; +import jnpf.model.cultivate.dto.teaching.QueryTeachingRecordDto; +import jnpf.model.cultivate.dto.teaching.TeachingSaveDto; +import jnpf.model.cultivate.vo.teaching.*; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * 带教管理 + */ +@Slf4j +@RestController +@RequestMapping("/app/teachingRecord") +public class AppTeachingController{ + + @Autowired + private TeachingRecordService teachingRecordService; + + /** + * 获取学员下拉列表 + * @param storeId 门店ID,用于筛选指定门店的学员 + * @return 返回学员信息列表,封装在ActionResult中 + */ + @GetMapping("/getStudentList") + public ActionResult> getStudentList(@RequestParam("storeId") String storeId) { + List postUserList = teachingRecordService.getPostUserList(storeId); + return ActionResult.success(postUserList); + } + + /** + * 我的带教-列表分页查询 + * @param queryTeachingRecordDto 查询条件封装对象,包含分页参数和查询条件 + * @return 分页查询结果,包含带教记录列表和分页信息 + */ + @GetMapping("/page") + public ActionResult> page(QueryTeachingRecordDto queryTeachingRecordDto) { + PageInfo page = teachingRecordService.page(queryTeachingRecordDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 新增带教记录 + * @param teachingSaveDto 带教记录保存数据传输对象 + * @return 操作结果,成功则返回空内容 + */ + @PostMapping("/add") + public ActionResult add(@Valid @RequestBody TeachingSaveDto teachingSaveDto) { + teachingSaveDto.setType(1); + teachingRecordService.add(teachingSaveDto); + return ActionResult.success(); + } + + /** + * 修改带教记录 + * @param teachingSaveDto 带教记录修改数据传输对象 + * @return 操作结果,成功则返回空内容 + */ + @PutMapping("/update") + public ActionResult update(@Valid @RequestBody TeachingSaveDto teachingSaveDto) { + teachingSaveDto.setType(1); + teachingRecordService.update(teachingSaveDto); + return ActionResult.success(); + } + + /** + * 删除带教记录 + * @param id 带教记录ID + * @return 删除操作结果,成功则返回空内容 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + teachingRecordService.delete(id); + return ActionResult.success(); + } + + /** + * 我的带教 - 上级带教分页查询 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return 分页查询结果,包含上级带教记录列表和分页信息 + */ + @PostMapping("/learderPage") + public ActionResult> learderPage(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + PageInfo voPageInfo = teachingRecordService.queryMyTeachingRecordList(queryTeachingRecordDto); + return ActionResult.page(voPageInfo.getList(), FtbUtil.getPagination(voPageInfo)); + } + + /** + * 我的带教-查看数据统计 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return 带教数据统计列表和分页信息 + */ + @PostMapping("/dataList") + public ActionResult> dataList(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + PageInfo voPageInfo = teachingRecordService.teachingStudentDataCount(queryTeachingRecordDto); + return ActionResult.page(voPageInfo.getList(), FtbUtil.getPagination(voPageInfo)); + } + + /** + * 上级带教-查看技能统计数据 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return 技能统计列表 + */ + @PostMapping("/learderDataList") + public ActionResult> learderDataList(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + List skillCountVoList = teachingRecordService.queryMyTeachingData(queryTeachingRecordDto); + return ActionResult.success(skillCountVoList); + } + + /** + * 查看带教员下拉列表 + * @param queryTeachingRecordDto 查询条件封装对象,包含门店ID等参数 + * @return 带教员信息列表 + */ + @PostMapping("/teachUserList") + public ActionResult> teachUserList(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + List teachingUserVos = teachingRecordService.teachingSelectList(queryTeachingRecordDto); + return ActionResult.success(teachingUserVos); + } + + /** + * 查看带教员下拉列表-上级带教专用 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return 上级带教员信息列表 + */ + @PostMapping("/teachingSuperiorSelectList") + public ActionResult> teachingSuperiorSelectList(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + List teachingUserVos = teachingRecordService.teachingSuperiorSelectList(queryTeachingRecordDto); + return ActionResult.success(teachingUserVos); + } + + /** + * 获取带教记录详情 + * @param id 带教记录ID + * @return 带教记录详细信息 + */ + @GetMapping("/detail/{id}") + public ActionResult detail(@PathVariable("id") String id) { + TeachingRecordDetailVo teachingDetail = teachingRecordService.getTeachingDetail(id); + return ActionResult.success(teachingDetail); + } + + /** + * 店长界面我的带教统计 + * @param storeId 门店ID + * @return 门店带教统计信息 + */ + @GetMapping("/storeTeachingCount") + public TeachingStoreCountVo storeTeachingCount(@RequestParam("storeId") String storeId) { + return teachingRecordService.storeTeachingCount(storeId); + } + + /** + * 带教统计总览 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return 带教统计信息 + */ + @PostMapping("/teachingCount") + public ActionResult teachingCount(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + TeachingCountVo teachingCountVo = teachingRecordService.teachingCount(queryTeachingRecordDto); + return ActionResult.success(teachingCountVo); + } + + /** + * 员工界面-上级带教统计 + * @param storeId 门店id + * @return TeachingStoreCountVo + */ + @GetMapping("/getSuperiorTeachingSummary") + public SuperiorTeachingSummaryVo getSuperiorTeachingSummary(@RequestParam("storeId") String storeId) { + return teachingRecordService.getSuperiorTeachingSummary(storeId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/MigrateController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/MigrateController.java new file mode 100644 index 0000000..36c9aa4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/MigrateController.java @@ -0,0 +1,135 @@ +package jnpf.cultivate.controller.web; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.cultivate.mapper.*; +import jnpf.database.util.DataSourceUtil; +import jnpf.database.util.DynamicDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.po.position.*; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.jdbc.ScriptRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +/** + * 培训课程数据迁移 + * + * @author fantaibao + * @date 2024/01/17 + */ +@RestController +@Slf4j +@RequestMapping("/migrate") +public class MigrateController { + + @Resource + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + @Resource + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + @Resource + private FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + @Resource + private FtbCultivatePositionExamIdentifyMapper ftbCultivatePositionExamIdentifyMapper; + @Resource + private FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + @Resource + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Autowired + private DataSourceUtil dataSourceUtil; + + + /** + * 课程迁移数据时更新courseId + * 警告,非迁移数据时请勿使用,会造成所有课程规则ID更新 + * + * @return + * @throws LoginException + */ + @GetMapping("/training-data-migration") + @SaIgnore + @Transactional + public ActionResult course() throws LoginException { + // 岗位学习表 + List ftbCultivatePositions = ftbCultivatePositionMapper.selectList(Wrappers.emptyWrapper()); + for (FtbCultivatePosition ftbCultivatePosition : ftbCultivatePositions) { + // 岗位学习课程关联表 + LambdaUpdateWrapper updateWrappera = Wrappers.lambdaUpdate(); + updateWrappera.set(FtbCultivatePositionCourse::getPostRankId, ftbCultivatePosition.getPostId()); + updateWrappera.eq(FtbCultivatePositionCourse::getPostLearnId, ftbCultivatePosition.getId()); + ftbCultivatePositionCourseMapper.update(null, updateWrappera); + // 岗位学习课程关联考试表 + LambdaUpdateWrapper updateWrapperb = Wrappers.lambdaUpdate(); + updateWrapperb.set(FtbCultivatePositionCourseExam::getPostRankId, ftbCultivatePosition.getPostId()); + updateWrapperb.eq(FtbCultivatePositionCourseExam::getPostLearnId, ftbCultivatePosition.getId()); + ftbCultivatePositionCourseExamMapper.update(null, updateWrapperb); + // 岗位学习课程关联鉴定表 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivatePositionCourseIdentity::getPostRankId, ftbCultivatePosition.getPostId()); + updateWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, ftbCultivatePosition.getId()); + ftbCultivatePositionCourseIdentityMapper.update(null, updateWrapper); + + // 岗位学习考试关联表 + LambdaUpdateWrapper updateWrapperc = Wrappers.lambdaUpdate(); + updateWrapperc.set(FtbCultivatePositionExam::getPostRankId, ftbCultivatePosition.getPostId()); + updateWrapperc.eq(FtbCultivatePositionExam::getPostLearnId, ftbCultivatePosition.getId()); + ftbCultivatePositionExamMapper.update(null, updateWrapperc); + // 岗位学习鉴定关联表 + LambdaUpdateWrapper updateWrapperd = Wrappers.lambdaUpdate(); + updateWrapperd.set(FtbCultivatePositionExamIdentify::getPostRankId, ftbCultivatePosition.getPostId()); + updateWrapperd.eq(FtbCultivatePositionExamIdentify::getPostLearnId, ftbCultivatePosition.getId()); + ftbCultivatePositionExamIdentifyMapper.update(null, updateWrapperd); + } + return ActionResult.success(); + } + + + /** + * 升级数据库 + * + * @param dataSourceName 数据源名称 + * @return {@link ActionResult} + */ + @SaIgnore + @PostMapping("/upgrade/sql") + @NoDataSourceBind + public ActionResult upgradeDatabase(@RequestParam("dataSourceName") String dataSourceName + , @RequestPart("file") MultipartFile file) throws SQLException { + DataSource dataSource = DynamicDataSourceUtil.createDataSource(dataSourceUtil); + try (InputStream inputStream = file.getInputStream()) { + Connection dataSourceConnection = DataSourceUtils.doGetConnection(dataSource); + dataSourceConnection.setCatalog(dataSourceName); + var scriptRunner = new ScriptRunner(dataSourceConnection); + scriptRunner.setSendFullScript(false); + scriptRunner.setAutoCommit(false); + scriptRunner.setStopOnError(true); + try { + scriptRunner.runScript(new InputStreamReader(inputStream)); + } finally { + DataSourceUtils.doReleaseConnection(dataSourceConnection, dataSource); + } + } catch (DataAccessException e) { + log.error("execute sql error", e); + } catch (Exception e1) { + log.error("failed to initialize dataSource from schema file {} ", dataSourceName, e1); + } + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/aisupport/FtbCultivateAiSupportController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/aisupport/FtbCultivateAiSupportController.java new file mode 100644 index 0000000..c880d83 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/aisupport/FtbCultivateAiSupportController.java @@ -0,0 +1,121 @@ +package jnpf.cultivate.controller.web.aisupport; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.cultivate.service.FtbCultivateCourseService; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.vo.course.web.CultivateCourseAiVo; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web课程AI支持模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/cultivate") +public class FtbCultivateAiSupportController { + + @Autowired + private FtbCultivateCourseService ftbCultivateCourseService; + + @Autowired + private FtbCultivateCourseChapterService ftbCultivateCourseChapterService; + + /** + * 查询所有课程信息 + * + * @return {@link ActionResult} + */ + @GetMapping("/course/ai/query-list") + public ActionResult> list() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateCourse::getId, FtbCultivateCourse::getName, FtbCultivateCourse::getHighLights) + .eq(FtbCultivateCourse::getEnableMark, 0) + .eq(FtbCultivateCourse::getIsGroundIng, 1) + .orderByAsc(FtbCultivateCourse::getCreatorTime); + + List list = ftbCultivateCourseService.list(queryWrapper); + if (CollUtil.isEmpty(list)) { + return ActionResult.success("成功", Collections.emptyList()); + } + + + // 查询所有章节并按课程 ID 分组 + Map> courseChapterMap = buildCourseChapterMap(); + + // 为缺少亮点的课程填充章节名称 + fillCourseHighLights(list, courseChapterMap); + + return ActionResult.success("成功", convertCultivateCourseAiVo(list)); + } + + /** + * 构建课程与章节名称的映射关系 + * + * @return 课程 ID 到章节名称列表的映射 + */ + private Map> buildCourseChapterMap() { + List chapterList = queryAllCourseChapter(); + if (CollUtil.isEmpty(chapterList)) { + return Collections.emptyMap(); + } + + return chapterList.stream() + .collect(Collectors.groupingBy( + FtbCultivateCourseChapter::getCourseId, + Collectors.mapping(FtbCultivateCourseChapter::getName, Collectors.toList()) + )); + } + + private void fillCourseHighLights(List courseList, Map> courseChapterMap) { + courseList.forEach(course -> { + if (StringUtil.isEmpty(course.getHighLights())) { + List chapterNames = courseChapterMap.get(course.getId()); + if (CollUtil.isNotEmpty(chapterNames)) { + course.setHighLights(String.join(",", chapterNames)); + } + } + }); + } + + private List queryAllCourseChapter() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateCourseChapter::getId, FtbCultivateCourseChapter::getName, FtbCultivateCourseChapter::getCourseId) + .eq(FtbCultivateCourseChapter::getEnableMark, 0) + .orderByAsc(FtbCultivateCourseChapter::getCreatorTime) + ; + List list = ftbCultivateCourseChapterService.list(queryWrapper); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + private List convertCultivateCourseAiVo(List list) { + return CollectionUtil.isEmpty(list) ? Collections.emptyList() : list.stream().map(item -> { + CultivateCourseAiVo aiVo = new CultivateCourseAiVo(); + aiVo.setId(item.getId()); + aiVo.setName(item.getName()); + aiVo.setHighLights(item.getHighLights()); + return aiVo; + }).collect(Collectors.toList()); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/apply/FtbCultivatePromotionPostApplyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/apply/FtbCultivatePromotionPostApplyController.java new file mode 100644 index 0000000..6fd31d8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/apply/FtbCultivatePromotionPostApplyController.java @@ -0,0 +1,105 @@ +package jnpf.cultivate.controller.web.apply; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePromotionPostApplyService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyCreateDto; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyDto; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyVO; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyWithPerVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** +* web岗位晋升申请模块 +* @author penghao +*/ +@RestController +@RequestMapping("/cul-post-apply") +public class FtbCultivatePromotionPostApplyController { + + + @Resource + FtbCultivatePromotionPostApplyService postApplyService; + + + /** + * 获取岗位申请列表 + * @param dto 岗位申请查询参数 + * @param page 分页信息 + * @return 岗位申请列表结果 + */ + @GetMapping("/getList") + @Operation(summary ="查看岗位申请列表") + public ActionResult> getList(FtbCultivatePromotionPostApplyDto dto, CultivatePage page) { + PageListVO voPageListVO = postApplyService.getList(dto, page); + return ActionResult.success(voPageListVO); + } + + + /** + * 初始化岗位申请 + * + * @param createDto 岗位申请创建数据传输对象 + * @return + */ + @PostMapping("/initPromApplication") + @Operation(summary = "初始化岗位申请") + //@SaCheckPermission("jobPromotionApplications::btn_promotion_application") + public ActionResult initiateAPromotionApplication(@Validated @RequestBody FtbCultivatePromotionPostApplyCreateDto createDto) { + createDto.setSource(0); + postApplyService.initiateAPromotionApplication(createDto); + return ActionResult.success(); + } + + + /** + * 查询岗位申请详情 + * @param dto 岗位申请数据传输对象 + * @return ActionResult 响应结果对象,包含岗位申请及相关信息 + */ + @GetMapping("/viewPromApplication") + @Operation(summary = "查询岗位申请详情") + //@SaCheckPermission("jobPromotionApplications::btn_view") + public ActionResult viewPromotionApplications(FtbCultivatePromotionPostApplyDto dto) { + FtbCultivatePromotionPostApplyWithPerVO withPerVO = postApplyService.viewPromotionApplications(dto, "0"); + return ActionResult.success(withPerVO); + } + + + /** + * 审核岗位申请的接口 + * @param dto 岗位申请数据传输对象 + * @return 审核结果 + */ + @PutMapping("/auditPromApplication") + @Operation(summary = "审核岗位申请") + public ActionResult auditPromotionPostApplication(@RequestBody FtbCultivatePromotionPostApplyDto dto) { + postApplyService.auditPromotionPostApplication(dto); + return ActionResult.success(); + } + + /** + * 获取是否有人岗位职等的晋升申请 + * @param postId 公司岗位ID + * @param grandId 职等ID + * @param userId 用户ID + * @return 成功结果 + */ + @GetMapping("/userIsHasApply") + @Operation(summary = "查看当前的人岗位职等是否存在晋升申请") + public ActionResult isThereAnAppForThePosLevel(@RequestParam("postId") String postId, + @RequestParam("grandId") String grandId, + @RequestParam("userId") String userId) { + return ActionResult.success(postApplyService.isThereAnAppForThePosLevel(postId,grandId,userId)); + } + + + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/authoritys/FtbCultivatePermissionUserController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/authoritys/FtbCultivatePermissionUserController.java new file mode 100644 index 0000000..6b8a5f8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/authoritys/FtbCultivatePermissionUserController.java @@ -0,0 +1,148 @@ +package jnpf.cultivate.controller.web.authoritys; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsBatchDTO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsDTO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsUpdateDTO; +import jnpf.model.personnels.dto.authoritys.FtbPermissionInfoDTO; +import jnpf.model.personnels.vo.authoritys.FtbPermissionInfoVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionUserVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionVO; +import jnpf.model.personnels.vo.uchisuike.FtbinternalRecommendationPoolVO; +import jnpf.personnels.service.FtbPersonnelsPermissionsService; +import jnpf.util.UserProvider; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * web培训员工权限模块(培训优化版本) + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/training/personnels-permission-user") +public class FtbCultivatePermissionUserController { + + @Resource + private FtbPersonnelsPermissionsService ftbPersonnelsPermissionsService; + + /** + * 删除权限 + * + * @param id 员工权限主键id(必传) + * @return {@link ActionResult} + */ + @DeleteMapping("/delete-permissions/{id}") + public ActionResult deletePermissions(@PathVariable(value = "id") String id) { + ftbPersonnelsPermissionsService.deletePermissions(id); + return ActionResult.success(); + } + + /** + * 权限列表 + * + * @return {@link ActionResult}<{@link PageListVO}<{@link FtbinternalRecommendationPoolVO}>> + */ + @GetMapping("/internal-recommendation-pool-list-query") + public ActionResult> permissionList(CultivatePage cultivatePage, + FtbPermissionInfoDTO ftbPermissionInfoDTO) { + Page page = cultivatePage.coverCultivatePage("a.F_CreatorTime"); + ftbPermissionInfoDTO.setPermissionType(1); + page = ftbPersonnelsPermissionsService.permissionList(page, ftbPermissionInfoDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 管理按钮时查询权限详情 + * + * @param id 员工权限主键id(必传) + * @return {@link ActionResult} + */ + @GetMapping("/admin-details") + public ActionResult adminDetails(String id) { + FtbPersonnelsPermissionUserVO ftbPersonnelsPermissionUserVO = ftbPersonnelsPermissionsService.adminDetails(id, 1L); + return ActionResult.success(ftbPersonnelsPermissionUserVO); + } + + /** + * 新增权限 + * + * @return {@link ActionResult} + */ + @PostMapping("/admin-details") + @Transactional + @SuppressWarnings("Duplicates") + public ActionResult addNewPermissions(@RequestBody @Validated FtbAddNewPermissionsBatchDTO ftbAddNewPermissionsDTO) { + List ftbAddNewPermissionsDTOs = new ArrayList<>(); + for (FtbAddNewPermissionsBatchDTO.UserInnerInfo userInnerInfo : ftbAddNewPermissionsDTO.getUserInnerInfos()) { + FtbAddNewPermissionsDTO temp = new FtbAddNewPermissionsDTO(); + temp.setPermissionType(1); + temp.setUserId(userInnerInfo.getUserId()); + temp.setUserName(userInnerInfo.getUserName()); + temp.setPhone(userInnerInfo.getPhone()); + temp.setUserCustomId(userInnerInfo.getUserCustomId()); + temp.setScopePermission(ftbAddNewPermissionsDTO.getScopePermission()); + temp.setPermissionIds(ftbAddNewPermissionsDTO.getPermissionIds()); + + temp.setPostIds(userInnerInfo.getPostIds()); + temp.setOrgIds(userInnerInfo.getOrgIds()); + temp.setPostRankIds(userInnerInfo.getPostRankIds()); + + temp.setOrgNames(userInnerInfo.getOrgNames()); + temp.setPostNames(userInnerInfo.getPostNames()); + temp.setPostRankNames(userInnerInfo.getPostNameIds()); + temp.setSpecifyOrgIds(ftbAddNewPermissionsDTO.getSpecifyOrgIds()); + + ftbAddNewPermissionsDTOs.add(temp); + } + for (FtbAddNewPermissionsDTO t : ftbAddNewPermissionsDTOs) { + ftbPersonnelsPermissionsService.addNewPermissions(t); + } + return ActionResult.success(); + } + + /** + * 修改权限 + * + * @return {@link ActionResult} + */ + @PutMapping("/admin-details-change") + public ActionResult addNewPermissionsChange(@RequestBody @Validated FtbAddNewPermissionsUpdateDTO ftbAddNewPermissionsUpdateDTO) { + ftbAddNewPermissionsUpdateDTO.setPermissionType(1); + ftbPersonnelsPermissionsService.addNewPermissionsChange(ftbAddNewPermissionsUpdateDTO); + return ActionResult.success(); + } + + /** + * 根据父级权限查询按钮权限列表(返回权限标识) + * + * @param parentPermissions 父级权限标识(必传) + * @return {@link ActionResult}<{@link List} + */ + @GetMapping("/query-permission") + public ActionResult> queryPermissionList(String parentPermissions) { + UserInfo userInfo = UserProvider.getUser(); + List result = ftbPersonnelsPermissionsService.queryPermissionList(userInfo, parentPermissions, 1); + return ActionResult.success(result); + } + + /** + * 新增时查询权限列表 + */ + @GetMapping("/query-the-permission-list-when-adding") + public ActionResult> queryThePermissionListWhenAdding() { + List ftbPersonnelsPermissionVOS = ftbPersonnelsPermissionsService.queryThePermissionListWhenAdding(1L); + return ActionResult.success(ftbPersonnelsPermissionVOS); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/casebase/FtbCultivateCaseBaseController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/casebase/FtbCultivateCaseBaseController.java new file mode 100644 index 0000000..ab0f589 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/casebase/FtbCultivateCaseBaseController.java @@ -0,0 +1,56 @@ +package jnpf.cultivate.controller.web.casebase; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCaseBaseService; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseAuditDTO; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseDTO; +import jnpf.model.cultivate.vo.casebase.FtbCultivateCaseBaseVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * @Title: web案例库 + * @Author: peng.hao + * @create: 2024/7/22:11:28 + */ +@RestController +@RequestMapping("/case-base") +public class FtbCultivateCaseBaseController { + + @Resource + FtbCultivateCaseBaseService caseBaseService; + + /** + * 列表展示 + * @param dto + * @return + */ + @GetMapping("/listDisplay") + public ActionResult> listDisplay(FtbCultivateCaseBaseDTO dto){ + return ActionResult.success(caseBaseService.listDisplay(dto)); + } + /** + * 详情 + * @param id + * @return + */ + @GetMapping("/getDetail/{id}") + public ActionResult getDetail(@PathVariable("id") String id){ + return ActionResult.success(caseBaseService.getDetail(id)); + } + + /** + * 审核案例 + * @return + */ + @PutMapping("/reviewCase") + public ActionResult reviewCase(@RequestBody @Validated FtbCultivateCaseBaseAuditDTO baseAuditDTO){ + caseBaseService.reviewCase(baseAuditDTO); + return ActionResult.success(); + } +} + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateController.java new file mode 100644 index 0000000..3e9c1d5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateController.java @@ -0,0 +1,145 @@ +package jnpf.cultivate.controller.web.certificate; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.Pagination; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCultivateCertificateService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.certificate.FtbCertificateForm; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.vo.certificate.FtbCertificateInfoVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificateListVO; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.util.JsonUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * web证书模板模块 + * + * @author hao.peng + */ +@RestController +@Tag(name = "证书", description = "证书") +@RequestMapping("/certificate/new") +public class FtbCultivateCertificateController { + + /** + * 服务对象 + */ + @Resource + private FtbCultivateCertificateService service; + + @Autowired + private UserApi userApi; + + /** + * 获取列表 + * @param pagination + * @return listVo + */ + @Operation(summary = "获取列表") + @GetMapping("/list") + public ActionResult> list(Pagination pagination) { + List entity = service.getList(pagination); + List userIds = entity.stream().map(FtbCertificateEntity::getCreatorUserId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); + List userNameList = userApi.getUserName(userIds); + Map userIdList = userNameList.stream().collect(Collectors.toMap(UserEntity::getId,UserEntity::getRealName,(a,b)->a)); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToString(entity), FtbCertificateListVO.class); + listVo.forEach(item->{ + String name = userIdList.get(item.getCreatorUserId()); + item.setCreatorUserName(name); + }); + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + return ActionResult.page(listVo, vo); + } + + /** + * 获取详情 + * @param id + * @return vo + */ + @Operation(summary = "获取详情") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCertificateEntity entity = service.getInfo(id); + FtbCertificateInfoVO vo = JsonUtil.getJsonToBean(entity, FtbCertificateInfoVO.class); + return ActionResult.success(vo); + } + + /** + * 新建 + * @param FtbCertificateForm + * @return + */ + @Operation(summary = "新建") + @PostMapping + public ActionResult create(@RequestBody @Valid FtbCertificateForm FtbCertificateForm) { + FtbCertificateEntity entity = JsonUtil.getJsonToBean(FtbCertificateForm, FtbCertificateEntity.class); + service.create(entity); + return ActionResult.success("保存成功"); + } + /** + * 修改 + * @param FtbCertificateForm + * @return + */ + @Operation(summary = "修改") + @PutMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult update(@PathVariable("id") String id, @RequestBody @Validated FtbCertificateForm FtbCertificateForm) { + FtbCertificateEntity entity = JsonUtil.getJsonToBean(FtbCertificateForm, FtbCertificateEntity.class); + String s = service.updateEntity(id, entity); + if (!StringUtils.isEmpty(s)) { + return ActionResult.fail(s); + } + return ActionResult.success("修改成功"); + } + /** + * 删除 + * @param id + * @return + */ + @Operation(summary = "删除") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + service.deleteCertificate(id); + return ActionResult.success("删除成功"); + } + + /** + * 岗位学习查所有证书 + * @return + */ + @GetMapping("/queryCertificateListByNotChoose") + public ActionResult> queryCertificateListByNotChoose(@RequestParam(value = "keyWords",required = false) + String keyWords, CultivatePage page){ + + return ActionResult.success(service.queryCertificateListByNotChoose(keyWords,page)); + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateImageController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateImageController.java new file mode 100644 index 0000000..8c0a63f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateImageController.java @@ -0,0 +1,45 @@ +package jnpf.cultivate.controller.web.certificate; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateCertificateImagesService; +import jnpf.model.cultivate.po.certificate.FtbCertificateImagesEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web证书模板图片控制器 + * + * @author xgl + */ +@RestController +@Tag(name = "证书图片", description = "证书图片") +@RequestMapping("/certificate-images") +public class FtbCultivateCertificateImageController { + + /** + * 服务对象 + */ + @Resource + private FtbCultivateCertificateImagesService ftbCultivateCertificateImagesService; + + + /** + * 查询所有图片 + * + * @return listVo + */ + @Operation(summary = "获取列表") + @GetMapping("/list-all") + public ActionResult> listAll() { + List ftbCertificateImagesEntities = ftbCultivateCertificateImagesService.listAll(); + return ActionResult.success(ftbCertificateImagesEntities); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateStatisticController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateStatisticController.java new file mode 100644 index 0000000..471d6af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateStatisticController.java @@ -0,0 +1,113 @@ +package jnpf.cultivate.controller.web.certificate; + +import com.google.common.collect.Maps; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCertificateService; +import jnpf.model.cultivate.dto.certificate.FtbCertificateOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.certificate.FtbCertificatePersonWisdomStatisticDTO; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.vo.certificate.FtbCertificateOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificatePersonWisdomStatisticVO; +import jnpf.util.EasyExcelUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web证书统计 + * @Author: peng.hao + */ +@RestController +@RequestMapping("/certificate/statistic") +public class FtbCultivateCertificateStatisticController { + + @Resource + private FtbCultivateCertificateService service; + + /** + * 组织维度统计 + * @param statisticDTO + * @return + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbCertificateOrgWisdomStatisticDTO statisticDTO){ + PageListVO listVO = service.organizationListStatistics(statisticDTO); + return ActionResult.success(listVO); + } + + /** + * 组织维度统计 导出 + * @param statisticDTO + */ + @PostMapping("/org-list-statistics/export") + public void exportInformationorganization(@RequestBody FtbCertificateOrgWisdomStatisticDTO statisticDTO, HttpServletResponse response) throws IOException { + statisticDTO.setPageSize(-1); + PageListVO listVO = service.organizationListStatistics(statisticDTO); + List list = listVO.getList(); + list.forEach(item->{ + // 状态(0下架,1上架) + if (item.getOnShelfStatus().equals("0")){ + item.setOnShelfStatus("下架"); + }else if (item.getOnShelfStatus().equals("1")){ + item.setOnShelfStatus("上架"); + } + }); + EasyExcelUtil.simpleWrite(list, "组织维度统计", FtbCertificateOrgWisdomStatisticVO.class, response); + } + /** + * 个人维度统计 + * @param personWisdomStatisticDTO + * @return + */ + @PostMapping("/person-list-statistics") + public ActionResult> personListStatistics( + @RequestBody FtbCertificatePersonWisdomStatisticDTO personWisdomStatisticDTO){ + PageListVO listVO = service.personalDimensionStatistics(personWisdomStatisticDTO); + return ActionResult.success(listVO); + } + + /** + * 个人维度统计导出信息 + * @param personWisdomStatisticDTO + */ + @PostMapping("/person-list-statistics/export") + public void exportInformationPerson(@RequestBody FtbCertificatePersonWisdomStatisticDTO personWisdomStatisticDTO, HttpServletResponse response) throws IOException { + personWisdomStatisticDTO.setPageSize(-1); + PageListVO listVO = service.personalDimensionStatistics(personWisdomStatisticDTO); + + List list = listVO.getList(); + list.forEach(item->{ + if (item.getEffectiveStatus().equals("0")){ + item.setEffectiveStatus("生效中"); + }else if (item.getEffectiveStatus().equals("1")){ + item.setEffectiveStatus("已失效"); + }else if (item.getEffectiveStatus().equals("2")){ + item.setEffectiveStatus("已吊销"); + } + }); + EasyExcelUtil.simpleWrite(list, "个人维度统计", FtbCertificatePersonWisdomStatisticVO.class, response); + } + /** + * 查询证书列表 + * @return + */ + @GetMapping("/query-correspond-list") + public ActionResult>> queryCorrespondingList(){ + List list = service.lambdaQuery().eq(FtbCertificateEntity::getEnabledMark,"1").list(); + List> collect = list.stream().map(item -> { + Map< String, Object> map = Maps.newHashMap(); + map.put("name",item.getName()); + map.put("id",item.getId()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(collect); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateUserController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateUserController.java new file mode 100644 index 0000000..9cb98c0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/certificate/FtbCultivateCertificateUserController.java @@ -0,0 +1,276 @@ +package jnpf.cultivate.controller.web.certificate; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCultivateCertificateService; +import jnpf.cultivate.service.FtbCultivateCertificateUserService; +import jnpf.cultivate.utils.CultivateImUtil; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.certificate.FtbCertificateUserForm; +import jnpf.model.cultivate.entiy.BaseEntity; +import jnpf.model.cultivate.po.certificate.CertificateUserPagination; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.vo.certificate.FtbCertificateUserAppListVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificateUserInfoVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificateUserListVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.JsonUtil; +import jnpf.util.NoDataSourceBind; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * web员工证书模块 + * + * @author hao.peng + */ +@RestController +@RequestMapping("/certificate_user/new") +public class FtbCultivateCertificateUserController { + /** + * 服务对象 + */ + @Resource + private FtbCultivateCertificateUserService userService; + + + @Autowired + private UserApiV2Util userApiV2Util; + + + @Autowired + CultivateImUtil cultivateImUtil; + + /** + * 服务对象 + */ + @Resource + private FtbCultivateCertificateService service; + + /** + * 获取列表 + * + * @param pagination + * @param certificateId + * @return + */ + @Operation(summary = "获取列表") + @GetMapping("/{certificateId}/list") + @Parameters({ + @Parameter(name = "certificateId", description = "证书ID", required = true), + }) + public ActionResult> list(CertificateUserPagination pagination, @PathVariable String certificateId) { + pagination.setCertificateId(certificateId); + List entity = userService.getList(pagination); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToString(entity), FtbCertificateUserListVO.class); + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + List userIds = listVo.stream().map(FtbCertificateUserListVO::getUserId).collect(Collectors.toList()); + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(BaseEntity::getId, pagination.getCertificateId()); + FtbCertificateEntity one = service.getOne(wrapper); + for (FtbCertificateUserListVO certificateUser : listVo) { +// if (Objects.nonNull(certificateUser.getOrganizeId())) { +//// OrganizeEntity organizeEntity = organizeApi.getInfoById(certificateUser.getOrganizeId()); +// OrganizeGeneralDetailVO organizeEntity = userApiV2Util.organizeInfoById(certificateUser.getOrganizeId(), null); +// if(organizeEntity!=null){ +// certificateUser.setOrganizeName(organizeEntity.getName()); +// } +// } +// if (Objects.nonNull(certificateUser.getPositionId())) { +//// PositionEntity positionEntity = positionApi.queryInfoById(certificateUser.getPositionId()); +// PositionVO positionEntity = userApiV2Util.infoPosition(certificateUser.getPositionId(), null); +// if (positionEntity != null) { +// certificateUser.setPositionName(positionEntity.getFullName()); +// } +// } + + UserBoundVO userPrimaryBoundVO = userPrimaryBoundBatch.get(certificateUser.getUserId()); + if (userPrimaryBoundVO != null) { + certificateUser.setUserName(userPrimaryBoundVO.getUserName()); + certificateUser.setOrganizeName(userPrimaryBoundVO.getOrganizeName()); + certificateUser.setPositionName(userPrimaryBoundVO.getPositionName()); + } + // 证书模版地址 + if (ObjectUtil.isNotEmpty(one)) { + certificateUser.setPoints(one.getPoints()); + certificateUser.setTemplate(one.getTemplate()); + certificateUser.setCompanyName(one.getCompanyName()); + certificateUser.setReason(one.getReason()); + if (StringUtils.isNotEmpty(one.getMotivational())) { + certificateUser.setMotivational(one.getMotivational()); + } + } + } + return ActionResult.page(listVo, vo); + } + + /** + * 用户证书列表(移动端) + * + * @param userId + * @return + */ + @Operation(summary = "用户证书列表(移动端)") + @GetMapping("/{userId}/certList") + @Parameters({ + @Parameter(name = "userId", description = "用户ID", required = true), + }) + public ActionResult userList(@PathVariable String userId) { + if (StringUtils.isEmpty(userId)) { + ActionResult.fail("用户ID为空"); + } + List entity = userService.userList(userId); + List strings = entity.stream().map(FtbCertificateUserEntity::getCertificateId).collect(Collectors.toList()); + Map entityMap = new HashMap<>(); + if (CollUtil.isNotEmpty(strings)) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(BaseEntity::getId, strings); + List list = service.list(wrapper); + entityMap = list.stream().collect(Collectors.toMap(FtbCertificateEntity::getId, a -> a, (k1, k2) -> k1)); + } + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToString(entity), FtbCertificateUserListVO.class); + FtbCertificateUserAppListVO userAppListVO = new FtbCertificateUserAppListVO(); + Map finalEntityMap = entityMap; +// ActionResult info = userApi.getInfo(userId); + UserBoundVO info = userApiV2Util.getUserPrimaryBoundOne(userId, null); + String realName; + if (info != null) { + realName = info.getUserName(); + } else { + realName = ""; + } + listVo.forEach(item -> { + if (finalEntityMap.containsKey(item.getCertificateId())) { + FtbCertificateEntity ftbCertificate = finalEntityMap.get(item.getCertificateId()); + if (StringUtils.isNotEmpty(ftbCertificate.getTemplate())) + item.setTemplate(ftbCertificate.getTemplate()); + item.setCompanyName(ftbCertificate.getCompanyName()); + item.setReason(ftbCertificate.getReason()); + item.setPoints(ftbCertificate.getPoints()); + if (StringUtils.isNotEmpty(ftbCertificate.getMotivational())) + item.setMotivational(ftbCertificate.getMotivational()); + } + if (StringUtils.isNotEmpty(realName)) item.setUserName(realName); + }); + userAppListVO.setCertList(listVo); + userAppListVO.setCertNumber(listVo.size()); + return ActionResult.success(userAppListVO); + } + + /** + * 获取详情 + * + * @param id + * @return + */ + @Operation(summary = "获取详情") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCertificateUserEntity entity = userService.getInfo(id); + FtbCertificateUserInfoVO vo = JsonUtil.getJsonToBean(entity, FtbCertificateUserInfoVO.class); + return ActionResult.success(vo); + } + + /** + * 颁发 + * + * @param FtbCertificateUserForm + * @return + */ + @Operation(summary = "颁发") + @PostMapping + //@SaCheckPermission("certificateMgrs::btn_issue") + public ActionResult> create(@RequestBody @Valid FtbCertificateUserForm FtbCertificateUserForm) { + userService.create(FtbCertificateUserForm); + return ActionResult.success(); + } + + /** + * 批量上传用户证书图片 + * + * @param certUserList + * @return + */ + @Operation(summary = "批量上传用户证书图片") + @PutMapping("/batchUploadImage") + public ActionResult batchUploadImage(@RequestBody @Valid List certUserList) { + userService.updateBatchById(certUserList); + return ActionResult.success("上传成功"); + } + + /** + * 修改 + * + * @param id + * @param FtbCertificateUserForm + * @return + */ + @Operation(summary = "修改") + @PutMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid FtbCertificateUserForm FtbCertificateUserForm) { + FtbCertificateUserEntity entity = JsonUtil.getJsonToBean(FtbCertificateUserForm, FtbCertificateUserEntity.class); + userService.update(id, entity); + return ActionResult.success("修改成功"); + } + + /** + * 删除(吊销) + * + * @param id + * @return + */ + @Operation(summary = "删除(吊销)") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + if (userService.delete(id)) { + return ActionResult.success("吊销成功"); + } + return ActionResult.fail("已吊销或已失效"); + } + + /** + * 测试im通知 + * + * @param userId + * @return + */ + @GetMapping("/testIm") + @NoDataSourceBind + @SaIgnore + public ActionResult test(@RequestParam("userId") String userId, + @RequestParam("tenantId") String tenantId) { + cultivateImUtil.sendMessage(List.of(userId), tenantId, ""); + return ActionResult.success("测试成功"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/chapter/FtbCultivateChapterTestStatisticController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/chapter/FtbCultivateChapterTestStatisticController.java new file mode 100644 index 0000000..725f5b8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/chapter/FtbCultivateChapterTestStatisticController.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.controller.web.chapter; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateChapterTestService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestStatisticVO; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @Title: web随堂测试统计 + * @Author: peng.hao + * @create: 2024/6/11:9:27 + */ +@RestController +@RequestMapping("/chapter-test") +public class FtbCultivateChapterTestStatisticController { + + + @Resource + private FtbCultivateChapterTestService chapterTestService; + + /** + * 获取随堂测试统计 + * @return + */ + @RequestMapping("/get-chapter-test-statistic") + public ActionResult> getChapterTestStatistic(@RequestParam(value = "keyWords" ,required = false) String keyWords, + CultivatePage page) { + return ActionResult.success(chapterTestService.getChapterTestStatistic(keyWords, page)); + } + + /** + * 导出随堂测试统计 + * @param keyWords 关键字 + * @return + */ + @RequestMapping("/export-chapter-test-statistic") + public void exportChapterTestStatistic(@RequestParam(value = "keyWords" ,required = false) String keyWords, + HttpServletResponse response) throws Exception { + CultivatePage cultivatePage = new CultivatePage(); + cultivatePage.setPageSize(999999); + PageListVO statistic = chapterTestService.getChapterTestStatistic(keyWords, cultivatePage); + List list = statistic.getList(); + EasyExcelUtils.exportExcel( response,"随堂测试统计",list,FtbCultivateChapterTestStatisticVO.class); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseChapterController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseChapterController.java new file mode 100644 index 0000000..c53d5fe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseChapterController.java @@ -0,0 +1,93 @@ +package jnpf.cultivate.controller.web.course; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseChapterDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseChapterUpdateDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterDetailsVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseTypeVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** +* web课程章节模块 +* +* @author xxxxx +*/ +@RestController +@RequestMapping("/web/ftb-cultivate-course-chapter") +public class FtbCultivateCourseChapterController { + + @Resource + private FtbCultivateCourseChapterService ftbCultivateCourseChapterService; + + /** + * 添加课程章节 + * + * @param ftbCultivateCourseDTO 课程章节参数 + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FtbCultivateCourseChapterDTO ftbCultivateCourseDTO) { + ftbCultivateCourseChapterService.add(ftbCultivateCourseDTO); + return ActionResult.success(); + } + + /** + * 删除课程章节 + * + * @param id 课程主键id + * @return {@link ActionResult} + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbCultivateCourseChapterService.delete(id); + return ActionResult.success(); + } + + /** + * 更新课程信息章节 + * + * @param ftbCultivateCourseDTO 课程章节参数 + * @return {@link ActionResult} + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FtbCultivateCourseChapterUpdateDTO ftbCultivateCourseDTO) { + ftbCultivateCourseChapterService.updateChapter(ftbCultivateCourseDTO); + return ActionResult.success(); + } + + /** + * 课程章节分页列表 + * + * @param cultivatePage 分页参数 + * @param courseId 课程id,必传 + * @return {@link ActionResult}<{@link Page}<{@link FtbCultivateCourseTypeVO}>> + */ + @GetMapping("/list") + public ActionResult> list(CultivatePage cultivatePage, String courseId) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbCultivateCourseChapterService.queryList(page, courseId); + return ActionResult.success(CultivatePage.coverPageList(page)); + } + + /** + * 课程章节详情-编辑时回显调用 + * + * @param id 课程主键id + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @GetMapping("/details/{id}") + public ActionResult courseDetails(@PathVariable("id") String id) { + FtbCultivateCourseChapterDetailsVO result = ftbCultivateCourseChapterService.courseDetails(id); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseController.java new file mode 100644 index 0000000..1d3bd81 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseController.java @@ -0,0 +1,180 @@ +package jnpf.cultivate.controller.web.course; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCourseService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseQueryDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseUpdateDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateShelvesDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.vo.course.web.*; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainPageVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** +* web课程模块 +* +* @author wcx +*/ +@RestController +@RequestMapping("/web/ftb-cultivate-course") +public class FtbCultivateCourseController { + @Resource + private FtbCultivateCourseService ftbCultivateCourseService; + + /** + * 添加课程,返回主键id,用于章节操作 + * + * @param ftbCultivateCourseDTO 课程参数 + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FtbCultivateCourseDTO ftbCultivateCourseDTO) { + String courseId = ftbCultivateCourseService.add(ftbCultivateCourseDTO); + return ActionResult.success(courseId); + } + + /** + * 删除课程 + * + * @param id 课程主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/{id}/{flag}") + public ActionResult delete(@PathVariable("id") String id) { + if (StrUtil.isBlank(id)) { + throw new IllegalArgumentException("参数错误"); + } + ftbCultivateCourseService.delete(id); + return ActionResult.success(); + } + + /** + * 课程删除检查判断 + * + * @param id 课程主键id,必传 + * @return {@link ActionResult}<{@link FtbCourseDeleteVO}> + */ + @GetMapping("/course-deletion-verification/{id}") + public ActionResult deleteCheck(@PathVariable("id") String id) { + if (StrUtil.isBlank(id)) { + throw new IllegalArgumentException("参数错误"); + } + return ftbCultivateCourseService.deleteCheck(id); + } + + /** + * 更新课程信息 + * + * @param ftbCultivateCourseUpdateDTO + * @return {@link ActionResult} + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FtbCultivateCourseUpdateDTO ftbCultivateCourseUpdateDTO) { + ftbCultivateCourseService.updateCourse(ftbCultivateCourseUpdateDTO); + return ActionResult.success(); + } + + /** + * 课程上下架 + * + * @param ftbCultivateShelvesDTO + * @return {@link ActionResult} + */ + @PostMapping("/upper-lower-shelves") + public ActionResult upperLower(@Validated @RequestBody FtbCultivateShelvesDTO ftbCultivateShelvesDTO) { + ftbCultivateCourseService.upperLower(ftbCultivateShelvesDTO); + return ActionResult.success(); + } + + /** + * 课程管理分页查询 + * + * @param cultivatePage 分页模型 + * @param ftbCultivateCourseQueryDTO 条件参数 + * @return {@link ActionResult} + */ + @GetMapping("/query-list") + public ActionResult> list(CultivatePage cultivatePage, FtbCultivateCourseQueryDTO ftbCultivateCourseQueryDTO) { + Page page = cultivatePage.coverCultivatePage("a.F_Id"); + page = ftbCultivateCourseService.pagingQuery(page, ftbCultivateCourseQueryDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 课程管理-分页上方的总数统计 + */ + @GetMapping("/query-list-count") + public ActionResult listCount(){ + FtbCultivateCourseNumberVO ftbCultivateCourseNumberVO = ftbCultivateCourseService.listCount(); + return ActionResult.success(ftbCultivateCourseNumberVO); + } + + /** + * 课程详情-编辑时回显调用 + * + * @param id 课程主键id + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @GetMapping("/details/{id}") + public ActionResult courseDetails(@PathVariable("id") String id) { + FtbCultivateCourseDetailsVO ftbCultivateCourse = ftbCultivateCourseService.courseDetails(id); + return ActionResult.success(ftbCultivateCourse); + } + + /** + * 二次删除时,岗位学习分页列表 + * + * @param cultivatePage 分页 + * @param courseId 课程id必填 + * @return {@link ActionResult}<{@link PageListVO}<{@link FtbCultivateOfflineTrainPageVO}>> + */ + @GetMapping("/job-learning-paginated-list") + public ActionResult> deleteJobLearningList(CultivatePage cultivatePage, String courseId) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbCultivateCourseService.deleteJobLearningList(page, courseId); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 二次删除时,关联题库分页列表 + * + * @param cultivatePage 分页 + * @param courseId 课程id必填 + * @return {@link ActionResult}<{@link Integer}> + */ + @GetMapping("/query-list-offline-train-count") + public ActionResult> deleteRelatedQuestionBankList(CultivatePage cultivatePage, String courseId) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbCultivateCourseService.deleteRelatedQuestionBankList(page, courseId); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 校验岗位是否建立岗位学习,返回不存在的岗位id信息 + * + * @param postIds 待校验岗位id集合(必传) + * @return {@link ActionResult } + */ + @PostMapping("/query-job-binding-courses") + public ActionResult> queryJobBindingCourses(@RequestBody List postIds) { + if (CollUtil.isEmpty(postIds)) { + throw new RuntimeException("请选择学习岗位"); + } + List ftbCultivateCoursePageVOS = ftbCultivateCourseService.queryJobBindingCourses(postIds); + return ActionResult.success(ftbCultivateCoursePageVOS); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseStatisticesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseStatisticesController.java new file mode 100644 index 0000000..32db65f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseStatisticesController.java @@ -0,0 +1,147 @@ +package jnpf.cultivate.controller.web.course; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCourseService; +import jnpf.cultivate.service.FtbCultivateCourseStatisticesService; +import jnpf.cultivate.service.FtbCultivateCourseTypeService; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseOrgStatisticsDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCoursePersonStatisticesDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseType; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseOrgStatisticesVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCoursePersonStatisticesVO; +import jnpf.util.EasyExcelUtil; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * web培训数据看板 -课程统计 + * + * @author wcx + */ +@RestController +@RequestMapping("/web/course/statistices") +public class FtbCultivateCourseStatisticesController { + + @Resource + private FtbCultivateCourseStatisticesService ftbCultivateCourseStatisticesService; + + @Autowired + private FtbCultivateCourseService service; + + @Resource + private FtbCultivateCourseTypeService ftbCultivateCourseTypeService; + + /** + * 组织维度统计 + * + * @param statisticDTO + * @return + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbCultivateCourseOrgStatisticsDTO statisticDTO) { + PageListVO listVO = ftbCultivateCourseStatisticesService.organizationListStatistics(statisticDTO); + return ActionResult.success(listVO); + } + + /** + * 组织维度统计-导出 + * + * @param statisticDTO + */ + @PostMapping("/org-list-statistics/export") + public void exportInformationorganization(@RequestBody FtbCultivateCourseOrgStatisticsDTO statisticDTO, HttpServletResponse response) throws IOException { + statisticDTO.setPageSize(99999); + PageListVO listVO = ftbCultivateCourseStatisticesService.organizationListStatistics(statisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "组织维度统计", FtbCultivateCourseOrgStatisticesVO.class, response); + } + + /** + * 个人维度统计 + * + * @param ftbCultivatePositionPersonStatisticesDTO + */ + @PostMapping("/person-list-statistics") + public ActionResult> personListStatistics( + @RequestBody FtbCultivateCoursePersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO) { + PageListVO listVO = ftbCultivateCourseStatisticesService.personListStatistics(ftbCultivatePositionPersonStatisticesDTO); + return ActionResult.success(listVO); + } + + /** + * 个人维度统计导出信息 + * + * @param personWisdomStatisticDTO + */ + @PostMapping("/person-list-statistics/export") + public void exportInformationPerson(@RequestBody FtbCultivateCoursePersonStatisticesDTO personWisdomStatisticDTO, HttpServletResponse response) throws IOException { + PageListVO listVO = ftbCultivateCourseStatisticesService.personListStatistics(personWisdomStatisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "个人维度统计", FtbCultivateCoursePersonStatisticesVO.class, response); + } + + /** + * 课程查询列表 + * @return + */ + @GetMapping("/query-correspond-list") + public ActionResult> queryCorrespondingList() { + List list = service.lambdaQuery() + .eq(FtbCultivateCourse::getEnableMark, "0").list(); + List result = list.stream() + .map(FtbCultivateStatisticCourseVO::convertA) + .collect(Collectors.toList()); + return ActionResult.success(result); + } + + /** + * 课程类型查询列表 + * @return + */ + @GetMapping("/query-name-type-list") + public ActionResult> queryNameTypeList() { + List courseTypes = ftbCultivateCourseTypeService.lambdaQuery() + .eq(FtbCultivateCourseType::getEnableMark, "0").list(); + List result = courseTypes.stream() + .map(FtbCultivateStatisticCourseVO::convertB) + .collect(Collectors.toList()); + return ActionResult.success(result); + } + + + @Data + public static class FtbCultivateStatisticCourseVO { + /** + * 名称 + */ + private String name; + /** + * 主键id + */ + private String id; + + public static FtbCultivateStatisticCourseVO convertA(FtbCultivateCourse course) { + FtbCultivateStatisticCourseVO ftbCultivateStatisticCourseVO = new FtbCultivateStatisticCourseVO(); + ftbCultivateStatisticCourseVO.setId(course.getId()); + ftbCultivateStatisticCourseVO.setName(course.getName()); + return ftbCultivateStatisticCourseVO; + } + + public static FtbCultivateStatisticCourseVO convertB(FtbCultivateCourseType course) { + FtbCultivateStatisticCourseVO ftbCultivateStatisticCourseVO = new FtbCultivateStatisticCourseVO(); + ftbCultivateStatisticCourseVO.setId(course.getId()); + ftbCultivateStatisticCourseVO.setName(course.getName()); + return ftbCultivateStatisticCourseVO; + } + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseTypeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseTypeController.java new file mode 100644 index 0000000..7945591 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/course/FtbCultivateCourseTypeController.java @@ -0,0 +1,88 @@ +package jnpf.cultivate.controller.web.course; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCourseTypeService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseTypeDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseTypeUpdateDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseTypeVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** +* web课程类型模块 +* +* @author wcx +*/ +@RestController +@RequestMapping("/web/ftb-cultivate-course-type") +public class FtbCultivateCourseTypeController { + @Resource + private FtbCultivateCourseTypeService ftbCultivateCourseTypeService; + + /** + * 添加课程类型 + * + * @param ftbCultivateCourseDTO 课程类型参数 + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FtbCultivateCourseTypeDTO ftbCultivateCourseDTO) { + ftbCultivateCourseTypeService.add(ftbCultivateCourseDTO); + return ActionResult.success(); + } + + /** + * 删除课程类型 + * + * @param id 课程主键id + * @return {@link ActionResult} + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + return ftbCultivateCourseTypeService.delete(id); + } + + /** + * 更新课程信息类型 + * + * @param ftbCultivateCourseTypeDTO 课程章节参数 + * @return {@link ActionResult} + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FtbCultivateCourseTypeUpdateDTO ftbCultivateCourseTypeDTO) { + ftbCultivateCourseTypeService.updateInfo(ftbCultivateCourseTypeDTO); + return ActionResult.success(); + } + + /** + * 课程类型分页列表 + * + * @param cultivatePage 分页参数 + * @return {@link ActionResult}<{@link Page}<{@link FtbCultivateCourseTypeVO}>> + */ + @GetMapping("/list") + public ActionResult> list(CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbCultivateCourseTypeService.pageList(page); + return ActionResult.success(CultivatePage.coverPageList(page)); + } + + /** + * 获取所有课程类型 + * + * @return {@link ActionResult}<{@link List}<{@link FtbCultivateCourseTypeVO}>> + */ + @GetMapping("/get-all") + public ActionResult> getAll() { + List list = ftbCultivateCourseTypeService.getAll(); + return ActionResult.success(list); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/coursepackage/FtbCultivateCoursePackageController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/coursepackage/FtbCultivateCoursePackageController.java new file mode 100644 index 0000000..93ad6ee --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/coursepackage/FtbCultivateCoursePackageController.java @@ -0,0 +1,104 @@ +package jnpf.cultivate.controller.web.coursepackage; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCoursePackageService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageAddDTO; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageQueryDTO; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageUpdateDTO; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackageDetailsVO; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackagePageVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web课程包模块 + * + * @author wcx + */ +@RestController +@RequestMapping("/web/ftb-cultivate-course-package") +public class FtbCultivateCoursePackageController { + + @Resource + private FtbCultivateCoursePackageService ftbCultivateCoursePackageService; + + + /** + * 添加课程包 + * + * @param ftbCultivateCoursePackageAddDTO + * @return {@link ActionResult } + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Validated FtbCultivateCoursePackageAddDTO ftbCultivateCoursePackageAddDTO) { + ftbCultivateCoursePackageService.add(ftbCultivateCoursePackageAddDTO); + return ActionResult.success(); + } + + /** + * 更新课程包 + */ + @PutMapping("/update") + public ActionResult updateCoursePackage(@RequestBody @Validated FtbCultivateCoursePackageUpdateDTO ftbCultivateCoursePackageUpdateDTO) { + ftbCultivateCoursePackageService.updateCoursePackage(ftbCultivateCoursePackageUpdateDTO); + return ActionResult.success(); + } + + /** + * 删除课程包 + * + * @param id 课程包id(必传) + * @return {@link ActionResult } + */ + @DeleteMapping("/delete/{id}") + public ActionResult deleteCoursePackage(@PathVariable("id") String id) { + ftbCultivateCoursePackageService.deleteCoursePackage(id); + return ActionResult.success(); + } + + /** + * 课程包列表 + */ + @GetMapping("/list") + public ActionResult> list(CultivatePage cultivatePage, FtbCultivateCoursePackageQueryDTO ftbCultivateCoursePackageQueryDTO) { + Page page = cultivatePage.coverCultivatePage("a.F_Id"); + page = ftbCultivateCoursePackageService.pagingQuery(page, ftbCultivateCoursePackageQueryDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 课程包详情-课程列表分页 + * + * @param id 课程包id(必传) + * @return + */ + @GetMapping("/details/{id}") + public ActionResult> get(@PathVariable("id") String id) { + List result = ftbCultivateCoursePackageService.get(id); + return ActionResult.success(result); + } + + /** + * 查询课程包课程 + * + * @param coursePackageIds 课程包主键id集合 + * @return {@link ActionResult } + */ + @PostMapping("/query-course-package-courses") + public ActionResult> queryCoursePackageCourses(@RequestBody List coursePackageIds) { + if (CollUtil.isEmpty(coursePackageIds)) { + throw new RuntimeException("请勾选课程包"); + } + List result = ftbCultivateCoursePackageService.queryCoursePackageCourses(coursePackageIds); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamController.java new file mode 100644 index 0000000..a110e56 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamController.java @@ -0,0 +1,239 @@ +package jnpf.cultivate.controller.web.exam; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.req.exam.QueryExamForPostReq; +import jnpf.model.cultivate.req.exam.QueryExamReq; +import jnpf.model.cultivate.req.exam.SaveExamReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.CourseEnums; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * web考试模块 + * + * @author xuguilin + */ +@RestController +@RequestMapping("/exam") +public class FtbCultivateExamController { + /** + * 考试服务 + */ + @Resource + private FtbCultivateExamService examService; + + + /** + * 分页列出考试列表 + * + * @param req + * @return + */ + @GetMapping("/pageLists") + public ActionResult> pageLists(QueryExamReq req) { + PageInfo pageVo = examService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 根据岗位和职能查询考试列表 + * + * @param queryPaper + * @return + */ + @GetMapping("/pageListsForPostAndPosition") + public ActionResult> pageListsForPostAndPosition(QueryExamForPostReq queryPaper) { + QueryExamReq req = new QueryExamReq(); + req.setPostId(queryPaper.getPostId()); + req.setPostionId(queryPaper.getPostionId()); + req.setKeyword(queryPaper.getKeyword()); + req.setCurrentPage(queryPaper.getCurrentPage()); + req.setPageSize(queryPaper.getPageSize()); + req.setExamType(CourseEnums.ExamType.POSITION.getCode()); + PageInfo pageVo = examService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 查询所有批阅角色 + * + * @return + */ + @Deprecated + @GetMapping("/query-all-reviewer-role") + public ActionResult> queryRoleReviewerList() { + return ActionResult.success( examService.queryRoleReviewerList()); + } + + + /** + * 查询所有批阅人 + * + * @return + */ + @GetMapping("/query-all-reviewer") + public ActionResult> queryReviewerUserList() { + return ActionResult.success(examService.queryReviewerUserList()); + } + + + /** + * 考试列表-编辑-回显接口 + * + * @param examId 考试id + * @return + */ + @GetMapping("/get/{examId}") + public ActionResult get(@PathVariable("examId") String examId) { + return ActionResult.success(examService.getInfo(examId)); + } + + + /** + * 考试管理-考试列表-批阅-考试详情【考试信息,试卷信息】 + * + * @param examId 考试ID + * @return + */ + @RequestMapping("/queryExamDetail/{examId}") + public ActionResult queryExamDetail(@PathVariable("examId") String examId) { + return ActionResult.success(examService.queryExamDetail(examId)); + } + + + /** + * 查询当前考试的试卷信息包括试卷的题目数量 + * + * @param examId 考试ID + * @return + */ + @GetMapping("/queryCurrExamPaperInfo/{examId}") + public ActionResult queryCurrExamPaperInfo(@PathVariable("examId") String examId) { + return ActionResult.success(examService.queryCurrExamPaperInfo(examId)); + } + + + /** + * 添加考试 + * + * @param req + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid SaveExamReq req) { + //新增考试 + examService.insertData(req); + return ActionResult.success(true); + } + + + /** + * 编辑考试 + * + * @param req + * @return + */ + @PostMapping("/update") + public ActionResult update(@RequestBody @Valid SaveExamReq req) { + if (StringUtils.isEmpty(req.getId())) { + //新增考试 + throw new RuntimeException("考试ID为空"); + } + //修改考试 + examService.updateData(req); + + return ActionResult.success(true); + } + + + /** + * 检测是否可以删除考试 + * + * @param id 考试ID + * @return + */ + @GetMapping("/check-can-del/{id}") + public ActionResult checkCanDel(@PathVariable("id") String id) { + return ActionResult.success(examService.checkExamCanDelete(id)); + } + + + /** + * 删除考试 + * + * @param id 考试ID + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + examService.deleteData(id); + return ActionResult.success(true); + } + + /** + * 查询所有的岗位和职等 + * + * @return + */ + @GetMapping("/query-post-and-rank") + public ActionResult> queryPostAndRank() { + return ActionResult.success(examService.queryPostAndRank()); + } + + /** + * 根据当前用户所属组织查询有批阅权限的用户列表 + * + * @return + */ + @GetMapping("/query-exam-review-list") + public ActionResult> queryExamReviewList() { + return ActionResult.success("成功", examService.queryExamReviewList(UserProvider.getLoginUserId())); + } + + /** + * 查询所有的已经配置的岗位学习 + */ + @GetMapping("/query-all-positon") + public ActionResult> queryAllPostion() { + return ActionResult.success("成功", examService.queryAllPostion()); + } + + + /** + * 考试作废-常规考试 + * + * @param id 考试ID + * @return + */ + @PutMapping("/nullify/{id}") + public ActionResult nullify(@PathVariable("id") String id) { + examService.nullify(id); + return ActionResult.success(true); + } + + + /** + * 考试作废-校验前置接口,判断是否有关联任务 + * + * @param id 考试ID + * @return + */ + @GetMapping("/checkNullify/{id}") + public ActionResult> checkNullify(@PathVariable("id") String id) { + return ActionResult.success(examService.checkNullify(id)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamOldDataController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamOldDataController.java new file mode 100644 index 0000000..6919098 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamOldDataController.java @@ -0,0 +1,73 @@ +package jnpf.cultivate.controller.web.exam; + +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 考试老数据处理 + * + * @author xuguilin + */ +@RestController +@RequestMapping("/examOldData") +public class FtbCultivateExamOldDataController { + /** + * 考试服务 + */ + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private ConfigValueUtil configValueUtil; + + /** + * 考试处理老数据 + * + * @return + */ + @NoDataSourceBind + @GetMapping("/initOldExamData") + public ActionResult> initOldExamData(@RequestParam("tenantId") String tenantId) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + //2检测和修改常规考试过期 + List list = examService.initOldExamData(); + +// //3、检测已经删除的岗位考试,并删除考试记录 +// examService.deleteExamUserForDeleteExam(); + return ActionResult.success(list); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamReadOverController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamReadOverController.java new file mode 100644 index 0000000..4e41fce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamReadOverController.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.controller.web.exam; + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.model.cultivate.req.exam.WebQueryExamReadOverReq; +import jnpf.model.cultivate.resp.WebReadOverExamAndPaperDetailVo; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * web考试批阅模块 + * + * @author xuguilin + */ +@RestController +@RequestMapping("/examReadOver") +public class FtbCultivateExamReadOverController { + /** + * 考试服务 + */ + @Resource + private FtbCultivateExamService examService; + + + /** + * 批阅查询历史试卷列表 + * + * @param req + * @return + */ + @GetMapping("/historyPaperList") + public ActionResult> historyPaperList(WebQueryExamReadOverReq req) { + return ActionResult.success("成功", examService.historyPaperList(req)); + } + + + /** + * 批阅查询历史试卷未考试的角标 + * + * @param req + * @return + */ + @GetMapping("/waitNumberForBatch") + public ActionResult> waitNumberForBatch(WebQueryExamReadOverReq req) { + return ActionResult.success("成功", examService.waitNumberForBatch(req)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamUserController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamUserController.java new file mode 100644 index 0000000..1ebf27e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/exam/FtbCultivateExamUserController.java @@ -0,0 +1,318 @@ +package jnpf.cultivate.controller.web.exam; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivateLearnTaskListService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.dto.statistics.CultivatePermissionModuleParam; +import jnpf.model.cultivate.dto.statistics.ExamStatisticsForOrgDTO; +import jnpf.model.cultivate.dto.statistics.ExamStatisticsForPersonDTO; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.req.exam.*; +import jnpf.model.cultivate.resp.*; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.util.*; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * web用户考试模块 + * + * @author xuguilin + */ +@RestController +@Slf4j +@RequestMapping("/user-exam") +public class FtbCultivateExamUserController { + @Autowired + private ConfigValueUtil configValueUtil; + /** + * 考试用户服务 + */ + @Autowired + private FtbCultivateExamUserService examUserService; + + /** + * 考试服务 + */ + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private FtbCultivateLearnTaskListService learnTaskListService; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 权限前缀 + */ +// private static final String EXAM_PERMISSION_PRE = "examList::"; + + + /** + * web考试列表-批阅--列出考试用户列表 + * + * @param req + * @return + */ + @GetMapping("/pageLists") + public ActionResult> pageLists(@Valid QueryExamUserReq req) { + PageInfo pageVo = examUserService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 根据当前用户获取数据权限的用户 + * + * @return + */ + @GetMapping("/getPermissionUser") + public ActionResult getPermissionUser(CultivatePermissionModuleParam dto) { + return ActionResult.success(userApiV2Util.getLoginManagerUserIdsForEncode()); + } + + + /** + * web考试列表-批阅-考试用户列表-查看用户的考试试卷信息 + * + * @param userExamId 考试用户ID + * @return + */ + @GetMapping("/get/{userExamId}") + public ActionResult get(@PathVariable("userExamId") String userExamId) { + return ActionResult.success(examUserService.getUserExamDetail(userExamId, true)); + } + + /** + * web考试列表-批阅-考试用户列表-发起重新考试 + * + * @param req 考试用户ID + * @return + */ + @PostMapping("/restartExam") + public ActionResult restartExam(@Valid @RequestBody WebRestartExamReq req) { + examUserService.webRestartExam(req); + return ActionResult.success("操作成功", true); + } + + + /** + * 批阅考试 + * + * @param req + * @return + */ + @PostMapping("/readOver") +// @SaCheckPermission(EXAM_PERMISSION_PRE + "btn_exam_read_over") + public ActionResult readOver(@RequestBody ReadOverExamReq req) { + examUserService.readOver(req); + return ActionResult.success(true); + } + + + /** + * 检测是否可以删除用户考试 + * + * @param id 用户考试ID + * @return + */ + @GetMapping("/check-can-del/{id}") + public ActionResult checkCanDel(@PathVariable("id") String id) { + return ActionResult.success(examUserService.checkUserExamCanDelete(id)); + } + + + /** + * 删除用户考试 + * + * @param id 用户考试ID + * @param isRepeat 0 直接删除,1 删除后重新创建考试记录 + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id, @RequestParam(name = "isRepeat", defaultValue = "0") Integer isRepeat) { + if (isRepeat == 1) { + //删除并重新创建考试记录,让用户重新考试 + examUserService.deleteAndCreateExam(id); + } else { + //直接删除 + examUserService.deleteData(id); + } + return ActionResult.success(true); + } + + + /** + * 考试排行版 + * + * @param examId 考试id + * @return + */ + @GetMapping("/ranking/{examId}") + public ActionResult> rankingList(@PathVariable("examId") String examId, + QueryExamRankListReq req) { + PageInfo pageVo = examUserService.rankIngList(examId, req); + + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + + } + + + /** + * 查询考试统计 + * + * @param req + * @return + */ + @GetMapping("/statistics") + public ActionResult statistics(FtbCultivateStatisticsDTO req) { + return ActionResult.success(examUserService.statistics(req)); + } + + /** + * 检测过期的考试,并修改状态为过期 + * + * @return + */ + @NoDataSourceBind + @GetMapping("/checkAndUpdateExpireUserExamStatus") + public ActionResult checkAndUpdateExpireUserExamStatus(@RequestParam("tenantId") String tenantId) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + log.error("检测过期的考试, 切换租户失败, {}", tenantId); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + //1检测和修改用户的过期考试 + Integer num = examUserService.checkAndUpdateExpireUserExamStatus(); + //2检测和修改常规考试过期 + examService.changeExamStatus(); + return ActionResult.success(num); + } + + /** + * 员工首页-查询考试成绩列表 + * + * @param req + * @return + */ + @GetMapping("/queryCultivateExamList") + public ActionResult> queryCultivateExamList(@Valid QueryCultivateExamReq req) { + String loginUserId = UserProvider.getLoginUserId(); + if (StringUtils.isEmpty(req.getUserId())) { + req.setUserId(loginUserId); + } + PageInfo pageVo = examUserService.queryExamList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 考试统计,组织维度 + */ + @GetMapping("/queryExamStatisticsForOrg") + public ActionResult> queryExamStatisticsForOrg(ExamStatisticsForOrgDTO dto) { + return ActionResult.success("成功", examUserService.queryExamStatisticsForOrg(dto, new ArrayList<>())); + } + + /** + * 考试统计,组织维度,导出 + */ + @GetMapping("/queryExamStatisticsForOrg/export") + public void exportOrg(ExamStatisticsForOrgDTO dto, HttpServletResponse response) throws IOException { + String fileName = "考试统计_组织维度_" + DateUtil.format(new Date(), "yyyyMMdd"); + List userIds = new ArrayList<>(); + List list = examUserService.queryExamStatisticsForOrg(dto, userIds); + List excelList = BeanUtil.copyToList(list, ExamStatisticsForOrgExcelVo.class); + EasyExcelUtils.exportExcel(response, fileName, excelList, ExamStatisticsForOrgExcelVo.class); + } + + /** + * 考试统计,个人维度 + */ + @PostMapping("/queryExamStatisticsForPerson") + public ActionResult> queryExamStatisticsForPerson(@RequestBody ExamStatisticsForPersonDTO dto) { + PageInfo pageVo = examUserService.queryExamStatisticsForPerson(dto); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 考试统计,个人维度,导出 + */ + @PostMapping("/queryExamStatisticsForPerson/export") + public void exportPerson(@RequestBody ExamStatisticsForPersonDTO dto, HttpServletResponse response) throws IOException { + + PageInfo pageVo = examUserService.queryExamStatisticsForPerson(dto); + List list = pageVo.getList(); + List excelVOList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(list)) { + for (ExamStatisticsForPersonVo vo : list) { + excelVOList.add(QuestionAnalysisUtil.convertToExcelPersonvo(vo)); + } + } + String fileName = "考试统计_个人维度_" + DateUtil.format(new Date(), "yyyyMMdd"); + EasyExcelUtils.exportExcel(response, fileName, excelVOList, ExamStatisticsForPersonExcelVo.class); + } + + /** + * 处理老数据 + */ + @GetMapping("/dealExamUserPositionForOldData") + @NoDataSourceBind + @Deprecated(since = "已经作废,之前上线用户处理老数据的") + public void dealExamUserPositionForOldData(@RequestParam("tenantId") String tenantId) { + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + examUserService.dealExamUserPositionForOldData(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedCommentController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedCommentController.java new file mode 100644 index 0000000..dd68f11 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedCommentController.java @@ -0,0 +1,245 @@ +package jnpf.cultivate.controller.web.gained; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCourseGainedCommentService; +import jnpf.cultivate.service.FtbCourseGainedLikeService; +import jnpf.cultivate.service.FtbCourseGainedService; +import jnpf.cultivate.service.FtbCourseGainedShareService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.gained.FtbCommentPagination; +import jnpf.model.cultivate.po.gained.FtbCourseGainedCommentEntity; +import jnpf.model.cultivate.po.gained.FtbCourseGainedEntity; +import jnpf.model.cultivate.po.gained.FtbCourseGainedLikeEntity; +import jnpf.model.cultivate.po.gained.FtbCourseGainedShareEntity; +import jnpf.model.cultivate.vo.gained.FtbCourseGainedCommentVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.JsonUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.simpleframework.xml.core.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * app岗位学习课程-心得评论 + */ +@RestController +@Tag(name = "心得评论", description = "心得评论") +@RequestMapping("/course_gained_comment") +public class FtbCourseGainedCommentController { + + @Autowired + private FtbCourseGainedCommentService ftbCourseGainedCommentService; + + @Autowired + private FtbCourseGainedService gainedService; + + @Autowired + FtbCourseGainedLikeService likeService; + + @Autowired + FtbCourseGainedShareService shareService; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * (移动端/web共用) + * + * @param pagination + * @return + */ + @Operation(summary = "获取心得评论获取列表") + @GetMapping("/list") + public ActionResult> list(FtbCommentPagination pagination) { + List entity = ftbCourseGainedCommentService.getList(pagination); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToString(entity), FtbCourseGainedCommentVO.class); + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + List userIds = listVo.stream().map(FtbCourseGainedCommentVO::getCreatorUserId).collect(Collectors.toList()); +// Map map = userApi.getInfoByIds(userIds).stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + Map map = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + Map nameMap = entity.stream() + .collect(Collectors.toMap(FtbCourseGainedCommentEntity::getId, Function.identity(), (v1, v2) -> v1)); + listVo.forEach(c -> { + c.setReplyNumber(getReplyNumber(c.getId())); + FtbCourseGainedCommentEntity e = nameMap.get(c.getId()); + if (ObjectUtils.isNotEmpty(e)) { + c.setUserName(e.getRealName()); + } + UserBoundVO infoVo = map.get(c.getCreatorUserId()); + if (ObjectUtils.isNotEmpty(infoVo)) { + c.setOrganizeName(infoVo.getOrganizeName()); + c.setPositionName(infoVo.getPositionName()); + c.setHeadIcon(UploaderUtil.uploaderImg(infoVo.getHeadIcon())); + } + }); + return ActionResult.page(listVo, vo); + } + + /** + * 查看对应的心得评论回复 + * + * @param id + * @return + */ + @Operation(summary = "查看对应的心得评论回复(移动端)") + @GetMapping("/{id}/listReply") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult> listReply(@PathVariable("id") String id) { + FtbCourseGainedCommentEntity commentEntity = ftbCourseGainedCommentService.getInfo(id); + FtbCourseGainedEntity gainedEntity = gainedService.getInfo(commentEntity.getGainedId()); + List entity = ftbCourseGainedCommentService.list(new LambdaQueryWrapper() + .eq(FtbCourseGainedCommentEntity::getParentId, id)); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToString(entity), FtbCourseGainedCommentVO.class); + List userIds = listVo.stream().map(FtbCourseGainedCommentVO::getCreatorUserId).collect(Collectors.toList()); +// List userInfoVoList = userApi.getInfoByIds(userIds); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + listVo.forEach(c -> { + c.setReplyNumber(getReplyNumber(c.getId())); + c.setReplyId(id); + c.setIsAuthor(0); + if (gainedEntity.getCreatorUserId().equals(c.getCreatorUserId())) { + c.setIsAuthor(1); + } +// Optional infoVoOptional = userInfoVoList.stream() +// .filter(u -> c.getCreatorUserId().equals(u.getUserId())).findFirst(); + UserBoundVO userBoundVO = userMap.get(c.getCreatorUserId()); + if (userBoundVO != null) { + c.setUserName(userBoundVO.getUserName()); + c.setOrganizeName(userBoundVO.getOrganizeName()); + c.setPositionName(userBoundVO.getPositionName()); + c.setHeadIcon(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + } + }); + return ActionResult.success(listVo); + } + + private Long getReplyNumber(String id) { + return ftbCourseGainedCommentService.count(new LambdaQueryWrapper() + .eq(FtbCourseGainedCommentEntity::getParentId, id)); + } + + @Operation(summary = "获取心得评论详情") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCourseGainedCommentEntity entity = ftbCourseGainedCommentService.getInfo(id); + return ActionResult.success(entity); + } + + @Operation(summary = "心得评论删除") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + ftbCourseGainedCommentService.removeById(id); + return ActionResult.success("删除成功"); + } + + + @Operation(summary = "心得评论点赞") + @PostMapping("/{gainedId}/like") + @Parameters({ + @Parameter(name = "gainedId", description = "心得ID", required = true), + }) + public ActionResult saveGainedLike(@PathVariable("gainedId") String gainedId, + @RequestBody @Validate FtbCourseGainedLikeEntity gainedLikeEntity) { + FtbCourseGainedEntity gainedEntity = gainedService.getInfo(gainedId); + gainedLikeEntity.setCourseId(gainedEntity.getCourseId()); + gainedLikeEntity.setChapterId(gainedEntity.getChapterId()); + gainedLikeEntity.setGainedId(gainedId); + gainedLikeEntity.setLikeUserId(UserProvider.getUser().getUserId()); + //验证当前用户有无点赞 已点赞 删除之前的记录 未点赞新增点赞记录 + Integer myLike = likeService.getMyLike(gainedLikeEntity); + if (null != myLike && myLike > 0) { + likeService.deleteLikeByGainedId(gainedLikeEntity.getGainedId(), gainedLikeEntity.getLikeUserId()); + } else { + likeService.create(gainedLikeEntity); + } + return ActionResult.success("保存成功"); + } + + @Operation(summary = "心得评论分享") + @PostMapping("/{gainedId}/share") + @Parameters({ + @Parameter(name = "gainedId", description = "心得ID", required = true), + }) + public ActionResult saveGainedShare(@PathVariable("gainedId") String gainedId, + @RequestBody @Validate FtbCourseGainedShareEntity gainedShareEntity) { + + FtbCourseGainedEntity gainedEntity = gainedService.getInfo(gainedId); + gainedShareEntity.setCourseId(gainedEntity.getCourseId()); + gainedShareEntity.setChapterId(gainedEntity.getChapterId()); + gainedShareEntity.setGainedId(gainedId); + shareService.create(gainedShareEntity); + return ActionResult.success("保存成功"); + } + + @Operation(summary = "根据心得进行评论") + @PostMapping("/{gainedId}/comment") + @Parameters({ + @Parameter(name = "gainedId", description = "心得ID", required = true), + }) + public ActionResult saveGainedComment(@PathVariable("gainedId") String gainedId, + @RequestBody @Validate FtbCourseGainedCommentEntity gainedCommentEntity) { + FtbCourseGainedEntity gainedEntity = gainedService.getInfo(gainedId); + if (ObjectUtils.isEmpty(gainedEntity)) { + return ActionResult.fail("未找到对应心得体会"); + } + gainedCommentEntity.setCourseId(gainedEntity.getCourseId()); + gainedCommentEntity.setChapterId(gainedEntity.getChapterId()); + gainedCommentEntity.setGainedId(gainedId); + UserInfo userInfo = UserProvider.getUser(); + if (ObjectUtils.isNotEmpty(userInfo)) { + gainedCommentEntity.setRealName(userInfo.getUserName()); + gainedCommentEntity.setAccount(userInfo.getUserAccount()); + if (Objects.isNull(gainedCommentEntity.getOrganizeId())) { + gainedCommentEntity.setOrganizeId(userInfo.getOrganizeId()); + } + if (Objects.isNull(gainedCommentEntity.getPositionId())) { + gainedCommentEntity.setPositionId(userInfo.getPortalId()); + } + } + if (StringUtils.isNotEmpty(gainedCommentEntity.getParentId())) { + String path = calAndQueryPath(gainedCommentEntity.getParentId()); + gainedCommentEntity.setPath(path); + } + ftbCourseGainedCommentService.create(gainedCommentEntity); + return ActionResult.success("保存成功"); + } + + private String calAndQueryPath(String parentId) { + String path = ""; + FtbCourseGainedCommentEntity parent = ftbCourseGainedCommentService.getById(parentId); + if (ObjectUtils.isNotEmpty(parent)) { + if (StringUtils.isEmpty(parent.getPath())) { + path = parent.getId(); + } else { + path = parent.getPath() + "/" + parent.getId(); + } + } + return path; + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedController.java new file mode 100644 index 0000000..204d5ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedController.java @@ -0,0 +1,331 @@ +package jnpf.cultivate.controller.web.gained; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.FtbCultivateCourseChapterMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.gained.FtbGainedPagination; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.gained.*; +import jnpf.model.cultivate.vo.gained.FtbCourseGainedForm; +import jnpf.model.cultivate.vo.gained.FtbCourseGainedInfoVO; +import jnpf.permission.UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.JsonUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.simpleframework.xml.core.Validate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * app岗位学习课程-心得体会 + */ +@RestController +@Tag(name = "心得体会", description = "心得体会") +@RequestMapping("/course_gained") +public class FtbCourseGainedController { + @Autowired + private FtbCourseGainedService ftbCourseGainedService; + @Autowired + private FtbCourseGainedLikeService likeService; + @Autowired + private FtbCourseGainedShareService shareService; + @Autowired + private FtbCourseGainedCommentService commentService; + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + @Autowired + private FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + @Autowired + private UserProvider userProvider; + @Autowired + private UserApiV2Util userApiV2Util; + + @Operation(summary = "获取列表(移动端/web共用)") + @GetMapping("/list") + public ActionResult> list(FtbGainedPagination pagination) { + List list = ftbCourseGainedService.getList(pagination); + List listVo = list.isEmpty() ? new ArrayList<>() : getInfoVO(list, pagination.getType()); + List appAttentionList = new ArrayList<>();//shareApi.getAppAttentionList(new FtbAppAttentionPage()); +// if (CollectionUtils.isNotEmpty(appAttentionList)){ +// Map FtbAppAttentionListVOMap = appAttentionList.stream().filter(e -> e.getUserId() != null).collect(Collectors.toMap(FtbAppAttentionListVO::getUserId, Function.identity())); +// if (MapUtils.isNotEmpty(FtbAppAttentionListVOMap)){ +// listVo.forEach(e->{ +// FtbAppAttentionListVO FtbAppAttentionListVO = FtbAppAttentionListVOMap.get(e.getCreatorUserId()); +// if (ObjectUtils.isNotEmpty(FtbAppAttentionListVO)){ +// e.setIsAttention(FtbAppAttentionListVO.getFollowStatus()); +// }else { +// e.setIsAttention(CourseEnums.FollowStatus.FALSE.getCode()); +// } +// }); +// } +// } + + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + return ActionResult.page(listVo, vo); + } + + @Operation(summary = "发布心得") + @PostMapping("/{chapterId}/saveGained") + @Parameters({ + @Parameter(name = "chapterId", description = "章节ID", required = true), + }) + public ActionResult saveGained(@PathVariable("chapterId") String chapterId, + @RequestBody @Validate FtbCourseGainedForm gainedForm) { + UserInfo userInfo = UserProvider.getUser(); +// UserEntity userEntityActionResult = userApi.getInfoById(userInfo.getUserId()); + UserBoundVO userEntityActionResult = userApiV2Util.getUserPrimaryBoundOne(userInfo.getUserId(), null); + FtbCourseGainedEntity gainedEntity = gainedForm.getGained(); + if (Objects.isNull(gainedEntity.getOrganizeId())) { + gainedEntity.setOrganizeId(userInfo.getOrganizeId()); + } + if (Objects.isNull(gainedEntity.getPositionId()) && userEntityActionResult != null) { + gainedEntity.setPositionId(userEntityActionResult.getPositionId()); + } + gainedEntity.setChapterId(chapterId); + ftbCourseGainedService.create(gainedEntity); + +// List userList = gainedForm.getUserList(); +// userList.forEach(u -> { +// u.setGainedId(gainedEntity.getId()); +// u.setTenantId(userInfo.getTenantId()); +// u.setCreatorTime(new Date()); +// u.setCreatorUserId(userInfo.getUserId()); +// }); +// //心得分享至广场 +// gainedService.sharingSquare(gainedEntity); +// gainedReaderService.saveBatch(userList); + return ActionResult.success("保存成功"); + } + + // @Operation(summary = "获取章节末尾心得列表") +// @GetMapping("/endGained") +// public ActionResult> getGainedListOfChapterEnd(@RequestParam String chapterId){ +// return ActionResult.success(ftbCourseGainedService.getGainedListOfChapterEnd(chapterId)); +// } + @Operation(summary = "获取详情(移动端/web共用)") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCourseGainedInfoVO infoVO = getInfoVO(id); + List appAttentionList = new ArrayList<>();// = shareApi.getAppAttentionList(new FtbAppAttentionPage()); +// if (CollectionUtils.isNotEmpty(appAttentionList)){ +// Map FtbAppAttentionListVOMap = appAttentionList.stream().filter(e -> e.getUserId() != null).collect(Collectors.toMap(FtbAppAttentionListVO::getUserId, Function.identity())); +// if (MapUtils.isNotEmpty(FtbAppAttentionListVOMap)){ +// FtbAppAttentionListVO FtbAppAttentionListVO = FtbAppAttentionListVOMap.get(infoVO.getCreatorUserId()); +// if (ObjectUtils.isNotEmpty(FtbAppAttentionListVO)){ +// infoVO.setIsAttention(FtbAppAttentionListVO.getFollowStatus()); +// }else { +// infoVO.setIsAttention(CourseEnums.FollowStatus.FALSE.getCode()); +// } +// } +// } + return ActionResult.success(infoVO); + } + + @NoDataSourceBind + @Operation(summary = "获取详情(移动端/web共用)-不需要登录") + @GetMapping("/other/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult otherInfo(@PathVariable("id") String id) { + return ActionResult.success(getInfoVO(id)); + } + + + @Operation(summary = "删除(移动端/web共用)") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + UserInfo userInfo = userProvider.get(); + FtbCourseGainedEntity info = ftbCourseGainedService.getInfo(id); + if (userInfo.getIsAdministrator()) { + ftbCourseGainedService.delete(info); + return ActionResult.success("删除成功"); + } + if (ObjectUtils.isNotEmpty(userInfo) && ObjectUtils.isNotEmpty(info) + && !userInfo.getUserId().equals(info.getCreatorUserId())) { + return ActionResult.fail("删除失败,只能删除当前用户的心得体会"); + } + ftbCourseGainedService.delete(info); + return ActionResult.success("删除成功"); + } +// +// @Operation(summary = "分页") +// @GetMapping("/page") +// public PageListVO getGainedInfoPage(FtbGainedQueryDto queryDto) { +// List listVo = new ArrayList<>(); +// List entityList = ftbCourseGainedService.list(queryDto); +// PageInfo pageInfo = new PageInfo<>(entityList); +// entityList.forEach(g -> listVo.add(getInfoVO(g.getId()))); +// List userIds = listVo.stream().map(FtbCourseGainedInfoVO::getCreatorUserId).collect(Collectors.toList()); +// List userInfoVoList = userApi.getInfoByIds(userIds); +// listVo.forEach(c -> { +// Optional infoVoOptional = userInfoVoList.stream() +// .filter(u -> c.getCreatorUserId().equals(u.getUserId())).findFirst(); +// if (infoVoOptional.isPresent()){ +// PartUserInfoVo partUserInfoVo = infoVoOptional.get(); +// c.setUserName(partUserInfoVo.getRealName()); +// c.setOrganizeName(partUserInfoVo.getOrganizeName()); +// c.setPositionName(partUserInfoVo.getPositionName()); +// } +// if (Objects.nonNull(c.getReadType()) && c.getReadType() == 2){ +// List readerEntityList = readerService.list(new LambdaQueryWrapper() +// .eq(FtbCourseGainedReaderEntity::getGainedId, c.getId())); +// c.setReaderList(readerEntityList.stream().map(FtbCourseGainedReaderEntity::getUserId).collect(Collectors.toList())); +// } +// }); +// +// PageListVO vo = new PageListVO<>(); +// PaginationVO paginationVO = new PaginationVO(); +// paginationVO.setTotal((int) pageInfo.getTotal()); +// paginationVO.setPageSize((long) pageInfo.getPageSize()); +// paginationVO.setCurrentPage((long) pageInfo.getPageNum()); +// vo.setList(listVo); +// vo.setPagination(paginationVO); +// return vo; +// } + + private List getInfoVO(List list, Integer type) { + String userId = userProvider.get().getUserId(); + List ids = list.stream().map(FtbCourseGainedEntity::getId).collect(Collectors.toList()); + List infoVOs = new ArrayList<>(); + List shareNumberList = shareService.list(new LambdaQueryWrapper() + .in(FtbCourseGainedShareEntity::getGainedId, ids)); + Map> shareNumberGroup = CollectionUtils.isNotEmpty(shareNumberList) ? + shareNumberList.stream().collect(Collectors.groupingBy(FtbCourseGainedShareEntity::getGainedId)) : new HashMap<>(); + + List likeNumberList = likeService.list(new LambdaQueryWrapper() + .in(FtbCourseGainedLikeEntity::getGainedId, ids)); + Map> likeNumberGroup = CollectionUtils.isNotEmpty(likeNumberList) ? + likeNumberList.stream().collect(Collectors.groupingBy(FtbCourseGainedLikeEntity::getGainedId)) : new HashMap<>(); + + List commentNumberList = commentService.list(new LambdaQueryWrapper() + .in(FtbCourseGainedCommentEntity::getGainedId, ids)); + Map> commentNumberGroup = CollectionUtils.isNotEmpty(commentNumberList) ? + commentNumberList.stream().collect(Collectors.groupingBy(FtbCourseGainedCommentEntity::getGainedId)) : new HashMap<>(); + + + List userIds = list.stream().map(FtbCourseGainedEntity::getCreatorUserId).collect(Collectors.toList()); +// List userList = userApi.getInfoByIds(userIds); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + if (Objects.isNull(type) || type != 1) { + userId = ""; + } + + List likeEntityList = likeService.list(new LambdaQueryWrapper() + .in(FtbCourseGainedLikeEntity::getGainedId, ids) + .eq(FtbCourseGainedLikeEntity::getLikeUserId, userId)); + + list.forEach(entity -> { + FtbCourseGainedInfoVO vo = JsonUtil.getJsonToBean(entity, FtbCourseGainedInfoVO.class); + if (MapUtils.isNotEmpty(shareNumberGroup)) { + vo.setShareNumber(CollectionUtils.isNotEmpty(shareNumberGroup.get(entity.getId())) ? (long) shareNumberGroup.get(entity.getId()).size() : 0L); + } + + if (MapUtils.isNotEmpty(likeNumberGroup)) { + vo.setLikeNumber(CollectionUtils.isNotEmpty(likeNumberGroup.get(entity.getId())) ? (long) likeNumberGroup.get(entity.getId()).size() : 0L); + } + + if (MapUtils.isNotEmpty(commentNumberGroup)) { + vo.setCommentNumber(CollectionUtils.isNotEmpty(commentNumberGroup.get(entity.getId())) ? (long) commentNumberGroup.get(entity.getId()).size() : 0L); + } + // 根据课程id获取到课程信息 + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(entity.getCourseId()); + if (ObjectUtils.isNotEmpty(ftbCultivateCourse)) { + vo.setCourseName(ftbCultivateCourse.getName()); + } + // 根据章节id 获取章节信息 + FtbCultivateCourseChapter ftbCultivateCourseChapter = ftbCultivateCourseChapterMapper.selectById(entity.getChapterId()); + if (ObjectUtils.isNotEmpty(ftbCultivateCourseChapter)) { + vo.setChapterName(ftbCultivateCourseChapter.getName()); + } +// List userInfoVoList = userList.stream() +// .filter(u -> u.getUserId().equals(entity.getCreatorUserId())).collect(Collectors.toList()); + UserBoundVO userBoundVO = userMap.get(entity.getCreatorUserId()); + if (userBoundVO != null) { + vo.setUserName(userBoundVO.getUserName()); + vo.setOrganizeName(userBoundVO.getOrganizeName()); + vo.setPositionName(userBoundVO.getPositionName()); + vo.setHeadIcon(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + } + long count = likeEntityList.stream().filter(l -> l.getGainedId().equals(vo.getId())).count(); + vo.setLiked(0); + if (count > 0) { + vo.setLiked(1); + } + infoVOs.add(vo); + }); + return infoVOs; + } + + private FtbCourseGainedInfoVO getInfoVO(String id) { + String userId = userProvider.get().getUserId(); + FtbCourseGainedEntity entity = ftbCourseGainedService.getInfo(id); + FtbCourseGainedInfoVO vo = JsonUtil.getJsonToBean(entity, FtbCourseGainedInfoVO.class); + long shareNumber = shareService.count(new LambdaQueryWrapper() + .eq(FtbCourseGainedShareEntity::getGainedId, id)); + vo.setShareNumber(shareNumber); + + long likeNumber = likeService.count(new LambdaQueryWrapper() + .eq(FtbCourseGainedLikeEntity::getGainedId, id)); + vo.setLikeNumber(likeNumber); + + long commentNumber = commentService.count(new LambdaQueryWrapper() + .eq(FtbCourseGainedCommentEntity::getGainedId, id)); + vo.setCommentNumber(commentNumber); + // 根据课程id 查询课程信息 + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(entity.getCourseId()); + if (ObjectUtils.isNotEmpty(ftbCultivateCourse)) { + vo.setCourseName(ftbCultivateCourse.getName()); + } + // 根据章节id 获取章节信息 + FtbCultivateCourseChapter ftbCultivateCourseChapter = ftbCultivateCourseChapterMapper.selectById(entity.getChapterId()); + if (ObjectUtils.isNotEmpty(ftbCultivateCourseChapter)) { + vo.setChapterName(ftbCultivateCourseChapter.getName()); + } +// List userList = userApi.getInfoByIds(Collections.singletonList(entity.getCreatorUserId())); + UserBoundVO userInfoVo = userApiV2Util.getUserPrimaryBoundOne(entity.getCreatorUserId(), null); + if (userInfoVo != null) { + vo.setUserName(userInfoVo.getUserName()); + vo.setOrganizeName(userInfoVo.getOrganizeName()); + vo.setPositionName(userInfoVo.getPositionName()); + vo.setHeadIcon(UploaderUtil.uploaderImg(userInfoVo.getHeadIcon())); + } + //设置点赞标识 + List likeEntityList = likeService.list(new LambdaQueryWrapper() + .eq(FtbCourseGainedLikeEntity::getGainedId, vo.getId()) + .eq(FtbCourseGainedLikeEntity::getLikeUserId, userId)); + vo.setLiked(0); + if (likeEntityList.size() > 0) { + vo.setLiked(1); + } + return vo; + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedLikeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedLikeController.java new file mode 100644 index 0000000..c22616a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedLikeController.java @@ -0,0 +1,116 @@ +package jnpf.cultivate.controller.web.gained; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCourseGainedLikeService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.gained.FtbLikePagination; +import jnpf.model.cultivate.po.gained.FtbCourseGainedLikeEntity; +import jnpf.model.cultivate.vo.gained.FtbCourseGainedLikeVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.JsonUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * app岗位学习课程-心得点赞 + */ +@RestController +@Tag(name = "心得点赞", description = "心得点赞") +@RequestMapping("/course_gained_like") +public class FtbCourseGainedLikeController { + @Autowired + private FtbCourseGainedLikeService ftbCourseGainedLikeService; + + @Autowired + private UserApiV2Util userApiV2Util; + @Autowired + private UserProvider userProvider; + + @Operation(summary = "心得点赞获取列表(移动端)") + @GetMapping("/list") + public ActionResult> list(FtbLikePagination pagination) { + List entity = ftbCourseGainedLikeService.getList(pagination); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToString(entity), FtbCourseGainedLikeVO.class); + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + List userIds = listVo.stream().map(FtbCourseGainedLikeVO::getLikeUserId).collect(Collectors.toList()); + Map map = userApiV2Util.getUserPrimaryBoundBatch(userIds,null); + listVo.forEach(c -> { + UserBoundVO userBoundVO = map.get(c.getLikeUserId()); + + if (userBoundVO != null) { + c.setUserName(userBoundVO.getUserName()); + c.setOrganizeName(userBoundVO.getOrganizeName()); + c.setPositionName(userBoundVO.getPositionName()); + c.setHeadIcon(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + } + }); + return ActionResult.page(listVo, vo); + } + + @Operation(summary = "心得点赞获取详情") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCourseGainedLikeEntity entity = ftbCourseGainedLikeService.getInfo(id); + //ContractInfoVO vo = JsonUtil.getJsonToBean(entity, ContractInfoVO.class); + return ActionResult.success(entity); + } + + @Operation(summary = "心得点赞新建") + @PostMapping + public ActionResult create(@RequestBody @Valid FtbCourseGainedLikeEntity entity) { + //ContractEntity entity = JsonUtil.getJsonToBean(contractForm, ContractEntity.class); + ftbCourseGainedLikeService.create(entity); + return ActionResult.success("保存成功"); + } + + @Operation(summary = "修改") + @PutMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid FtbCourseGainedLikeEntity entity) { + entity.setId(id); + ftbCourseGainedLikeService.updateById(entity); + return ActionResult.success("修改成功"); + } + + @Operation(summary = "删除") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + ftbCourseGainedLikeService.removeById(id); + return ActionResult.success("删除成功"); + } + + @Operation(summary = "取消点赞") + @DeleteMapping("/{gainedId}/cancel") + @Parameters({ + @Parameter(name = "gainedId", description = "心得ID", required = true), + }) + public ActionResult cancel(@PathVariable("gainedId") String gainedId) { + ftbCourseGainedLikeService.remove(new LambdaQueryWrapper() + .eq(FtbCourseGainedLikeEntity::getGainedId, gainedId) + .eq(FtbCourseGainedLikeEntity::getLikeUserId, userProvider.get().getUserId())); + return ActionResult.success("取消成功"); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedReaderController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedReaderController.java new file mode 100644 index 0000000..06e0810 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedReaderController.java @@ -0,0 +1,79 @@ +package jnpf.cultivate.controller.web.gained; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.Pagination; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCourseGainedReaderService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedReaderEntity; +import jnpf.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * app岗位学习课程-心得可查看用户关联表 + * + */ +@RestController +@Tag(name = "心得可查看用户关联表", description = "心得可查看用户关联表") +@RequestMapping("/course_gained_reader") +public class FtbCourseGainedReaderController { + @Autowired + private FtbCourseGainedReaderService ftbCourseGainedReaderService; + + @Operation(summary = "获取列表") + @GetMapping("/list") + public ActionResult> list(Pagination pagination) { + List entity = ftbCourseGainedReaderService.getList(pagination); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToStringDateFormat(entity, "yyyy-MM-dd HH:mm:ss"), FtbCourseGainedReaderEntity.class); + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + return ActionResult.page(listVo, vo); + } + + @Operation(summary = "获取详情") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCourseGainedReaderEntity entity = ftbCourseGainedReaderService.getInfo(id); + //ContractInfoVO vo = JsonUtil.getJsonToBean(entity, ContractInfoVO.class); + return ActionResult.success(entity); + } + + @Operation(summary = "新建") + @PostMapping + public ActionResult create(@RequestBody @Valid FtbCourseGainedReaderEntity entity) { + //ContractEntity entity = JsonUtil.getJsonToBean(contractForm, ContractEntity.class); + ftbCourseGainedReaderService.create(entity); + return ActionResult.success("保存成功"); + } + + @Operation(summary = "修改") + @PutMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid FtbCourseGainedReaderEntity entity) { + entity.setId(id); + ftbCourseGainedReaderService.updateById(entity); + return ActionResult.success("修改成功"); + } + + @Operation(summary = "删除") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + ftbCourseGainedReaderService.removeById(id); + return ActionResult.success("删除成功"); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedShareController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedShareController.java new file mode 100644 index 0000000..dd328df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/gained/FtbCourseGainedShareController.java @@ -0,0 +1,84 @@ +package jnpf.cultivate.controller.web.gained; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.Pagination; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCourseGainedShareService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedShareEntity; +import jnpf.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * app岗位学习课程-心得分享 + * + */ +@RestController +@Tag(name = "心得分享", description = "心得分享") +@RequestMapping("/course_gained_share") +public class FtbCourseGainedShareController { + @Autowired + private FtbCourseGainedShareService ftbCourseGainedShareService; + + @Operation(summary = "获取列表") + @GetMapping("/list") + public ActionResult> list(Pagination pagination) { + List entity = ftbCourseGainedShareService.getList(pagination); + List listVo = JsonUtil.getJsonToList(JsonUtil.getObjectToStringDateFormat(entity, "yyyy-MM-dd HH:mm:ss"), FtbCourseGainedShareEntity.class); + PaginationVO vo = JsonUtil.getJsonToBean(pagination, PaginationVO.class); + return ActionResult.page(listVo, vo); + } + + @Operation(summary = "获取详情") + @GetMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult info(@PathVariable("id") String id) { + FtbCourseGainedShareEntity entity = ftbCourseGainedShareService.getInfo(id); + return ActionResult.success(entity); + } + + @Operation(summary = "新建") + @PostMapping + public ActionResult create(@RequestBody @Valid FtbCourseGainedShareEntity entity) { + ftbCourseGainedShareService.create(entity); + return ActionResult.success("保存成功"); + } + + @Operation(summary = "修改") + @PutMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid FtbCourseGainedShareEntity entity) { + entity.setId(id); + ftbCourseGainedShareService.updateById(entity); + return ActionResult.success("修改成功"); + } + + @Operation(summary = "删除") + @DeleteMapping("/{id}") + @Parameters({ + @Parameter(name = "id", description = "主键", required = true), + }) + public ActionResult delete(@PathVariable("id") String id) { + ftbCourseGainedShareService.removeById(id); + return ActionResult.success("删除成功"); + } + +// @Operation(summary = "心得分享到广场") +// @PostMapping("/sharingSquare") +// public ActionResult sharingSquare(@RequestBody SharingSquareVO sharingSquareVO) { +// ftbCourseGainedShareService.sharingSquare(sharingSquareVO.getId()); +// return ActionResult.success("分享成功"); +// } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/CultivateCultivateIdentifyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/CultivateCultivateIdentifyController.java new file mode 100644 index 0000000..75c2741 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/CultivateCultivateIdentifyController.java @@ -0,0 +1,367 @@ +package jnpf.cultivate.controller.web.identify; + +import cn.hutool.core.collection.CollectionUtil; +import com.alibaba.fastjson.JSONObject; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.FtbCultivateIdentifyApi; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.CultivateIdentifyTableService; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.model.cultivate.dto.identify.*; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.vo.identify.*; +import jnpf.util.CustomTenantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * web实操鉴定 + * + * @author mouzeping + */ +@Slf4j +@RestController +@RequestMapping(value = "/cultivate") +public class CultivateCultivateIdentifyController implements FtbCultivateIdentifyApi { + @Autowired + private CustomTenantUtil customTenantUtil; + @Resource + private CultivateIdentifyTableService tableService; + @Resource + private CultivateIdentifyApplyService applyService; + + /** + * 鉴定表/列表 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "列表") + @GetMapping(value = "/identify/list") + public ActionResult> getPageList(@Valid IdentifyTableListDto req) { + log.info("列表,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + PageInfo page = tableService.getPageList(req); + long ent = System.currentTimeMillis(); + log.info("列表,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 鉴定表/详情 + * + * @return + * @ + */ + @Operation(summary = "详情") + @GetMapping(value = "/identify/info") + public ActionResult getInfo(@Valid IdentifyTableInfoDto req) { + log.info("详情,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyTableInfoVo tableInfoVo = tableService.getInfo(req); + long ent = System.currentTimeMillis(); + log.info("详情,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(tableInfoVo); + } + + /** + * 鉴定表/新增 + * + * @return + * @ + */ + @Operation(summary = "新增") + @PostMapping(value = "/identify/save") + public ActionResult save(@Valid @RequestBody IdentifyTableSaveDto req) { + log.info("鉴定表/新增,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + tableService.saveData(req); + long ent = System.currentTimeMillis(); + log.info("鉴定表/新增,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU018.get()); + } + + /** + * 鉴定表/修改 + * + * @return + * @ + */ + @Operation(summary = "修改") + @PutMapping(value = "/identify/update") + public ActionResult update(@Valid @RequestBody IdentifyTableUpdateDto req) { + log.info("鉴定表/修改,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + tableService.updateData(req); + long ent = System.currentTimeMillis(); + log.info("鉴定表/修改,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU004.get()); + } + + + /** + * 鉴定表/删除 + * + * @return + * @ + */ + @Operation(summary = "删除") + @DeleteMapping(value = "/identify/delete") + public ActionResult delete(@Valid IdentifyTableInfoDto req) { + IdentifyTableDeleteVo tableDeleteVo = tableService.deleteData(req); + if (CollectionUtil.isNotEmpty(tableDeleteVo.getManuallyInitiateVoList()) || + CollectionUtil.isNotEmpty(tableDeleteVo.getPostAndCourseVoList())) { + ActionResult actionResult = new ActionResult<>(); + actionResult.setCode(400); + if (CollectionUtil.isNotEmpty(tableDeleteVo.getManuallyInitiateVoList())) { + actionResult.setMsg("删除失败"); + } + actionResult.setData(tableDeleteVo); + return actionResult; + } else { + return ActionResult.success("删除成功"); + } + } + + + /** + * 实操鉴定申请/列表 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "列表") + @GetMapping(value = "/apply/list") + public ActionResult> getPageApplyList(@Valid IdentifyApplyListDto req) { + log.info("实操鉴定申请/列表,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + PageInfo page = applyService.getPageApplyList(req); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/列表,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + + /** + * 实操鉴定申请/详情(基本信息) + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "详情(基本信息)") + @GetMapping(value = "/apply/basicInfo") + public ActionResult getApplyBasicInfo(@Valid IdentifyTableInfoDto req) { + log.info("实操鉴定申请/详情(基本信息),入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyApplyBasicInfoVo basicInfo = applyService.getApplyBasicInfo(req); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/详情(基本信息),耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(basicInfo); + } + + /** + * 实操鉴定申请/详情(鉴定详情) + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "详情(鉴定详情)") + @GetMapping(value = "/apply/info") + public ActionResult getApplyInfo(@Valid IdentifyTableInfoDto req) { + log.info("实操鉴定申请/详情(鉴定详情),入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyApplyInfoVo infoVo = applyService.getApplyInfo(req); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/详情(鉴定详情),耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(infoVo); + } + + /** + * 实操鉴定申请/新增 + * + * @param req 帅选条件 + * @return + * @ + */ + @PostMapping(value = "/apply/save") + public ActionResult applyDataSave(@Valid @RequestBody BatchIdentifyApplySaveDto req) { + req.setSource(ApplySourceEnum.SDFQ.getCode()); + applyService.applyDataSaveBatch(req); + return ActionResult.success(MsgCode.SU018.get()); + } + + + /** + * 实操鉴定申请/推送 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "新增") + @PostMapping(value = "/apply/push") + public ActionResult applyDataSave(@Valid @RequestBody IdentifyApplyDataPushDto req) { + log.info("实操鉴定申请/推送,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + applyService.applyDataPush(req); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/推送,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU018.get()); + } + + /** + * 实操鉴定申请/重新鉴定 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "重新鉴定") + @PutMapping(value = "/apply/reIdentify") + public ActionResult applyDataReIdentify(@Valid @RequestBody IdentifyApplyReIdentifyDto req) { + log.info("实操鉴定申请/重新鉴定,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + applyService.applyDataReIdentify(req); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/重新鉴定,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU005.get()); + } + + /** + * 实操鉴定申请/删除检验 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "删除检验") + @GetMapping(value = "/apply/deleteCheck") + public ActionResult> applyDataDeleteCheck(@Valid IdentifyTableInfoDto req) { + log.info("实操鉴定申请/删除检验,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + List applyListVoList = applyService.applyDataDeleteCheck(req); + if (applyListVoList.size() > 0) { + ActionResult> actionResult = new ActionResult<>(); + actionResult.setCode(400); + actionResult.setMsg("检验失败"); + actionResult.setData(applyListVoList); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/删除检验,耗时=>{}", ent - st + " 毫秒"); + return actionResult; + } else { + return ActionResult.success("检验成功"); + } + } + + /** + * 实操鉴定申请/删除 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "删除") + @DeleteMapping(value = "/apply/delete") + public ActionResult applyDataDelete(@Valid IdentifyTableInfoDto req) { + try { + log.info("实操鉴定申请/删除,入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + applyService.applyDataDelete(req); + long ent = System.currentTimeMillis(); + log.info("实操鉴定申请/删除,耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(MsgCode.SU003.get()); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 数据看板/统计(鉴定) + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "统计(鉴定)") + @GetMapping(value = "/apply/statistics") + public ActionResult getIdentifyStatistics(@Valid FtbCultivateStatisticsDTO req) { + try { + log.info("数据看板/统计(鉴定),入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + IdentifyStatisticsVo statisticsVo = applyService.getIdentifyStatistics(req); + long ent = System.currentTimeMillis(); + log.info("数据看板/统计(鉴定),耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(statisticsVo); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 数据看板/排名(鉴定) + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "排名(鉴定)") + @GetMapping(value = "/apply/top") + public ActionResult> getIdentifyTop(@Valid FtbCultivateStatisticsDTO req) { + try { + log.info("数据看板/排名(鉴定),入参=>{}", JSONObject.toJSON(req)); + long st = System.currentTimeMillis(); + List topVoList = applyService.getIdentifyTop(req); + long ent = System.currentTimeMillis(); + log.info("数据看板/排名(鉴定),耗时=>{}", ent - st + " 毫秒"); + return ActionResult.success(topVoList); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 处理鉴定逾期数据 + * + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/apply/setIdentifyBeOverdue") + @NoDataSourceBind + public Boolean setIdentifyBeOverdue(@RequestParam(value = "tenantId") String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return applyService.setIdentifyBeOverdue(); + } + + /** + * 计划鉴定时间提醒 + * + * @return java.lang.Boolean + */ + @Override + @PostMapping(value = "/apply/setIdentifyRemind") + @NoDataSourceBind + public Boolean setIdentifyRemind(@RequestParam(value = "tenantId") String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return applyService.setIdentifyRemind(tenantId); + } +} + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/FtbCultivateIdentifyStatisticController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/FtbCultivateIdentifyStatisticController.java new file mode 100644 index 0000000..a0ffe9c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/identify/FtbCultivateIdentifyStatisticController.java @@ -0,0 +1,126 @@ +package jnpf.cultivate.controller.web.identify; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.google.common.collect.Maps; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.CultivateIdentifyTableService; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.model.cultivate.dto.identify.FtbIdentityOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.identify.FtbIdentityPersonWisdomStatisticDTO; +import jnpf.model.cultivate.vo.identify.FtbCultivateIdentityOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.identify.FtbCultivateIdentityPersonWisdomStatisticVO; +import jnpf.util.EasyExcelUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web 实操鉴定统计 + * @Author: peng.hao + */ +@RestController +@RequestMapping(value = "/identify/statistic") +public class FtbCultivateIdentifyStatisticController { + @Autowired + CultivateIdentifyTableService service; + /** + * 组织维度统计 + * @param statisticDTO + * @return + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbIdentityOrgWisdomStatisticDTO statisticDTO){ + PageListVO listVO = service.organizationListStatistics(statisticDTO); + return ActionResult.success(listVO); + } + + /** + * 组织维度统计 导出 + * @param statisticDTO + */ + @PostMapping("/org-list-statistics/export") + public void exportInformationorganization(@RequestBody FtbIdentityOrgWisdomStatisticDTO statisticDTO, HttpServletResponse response) throws IOException { + statisticDTO.setPageSize(999999); + PageListVO listVO = service.organizationListStatistics(statisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "组织维度统计", FtbCultivateIdentityOrgWisdomStatisticVO.class, response); + } + /** + * 个人维度统计 + * @param personWisdomStatisticDTO + * @return + */ + @PostMapping("/person-list-statistics") + public ActionResult> personListStatistics( + @RequestBody FtbIdentityPersonWisdomStatisticDTO personWisdomStatisticDTO){ + PageListVO listVO = service.personListStatistics(personWisdomStatisticDTO); + return ActionResult.success(listVO); + } + + /** + * 个人维度统计导出信息 + * @param personWisdomStatisticDTO + */ + @PostMapping("/person-list-statistics/export") + public void exportInformationPerson(@RequestBody FtbIdentityPersonWisdomStatisticDTO personWisdomStatisticDTO, HttpServletResponse response) throws IOException{ + PageListVO listVO = service.personListStatistics(personWisdomStatisticDTO); + List list = listVO.getList(); + list.forEach(item->{ + if (StringUtils.isNotEmpty(item.getAuthenticationStatus())) { + if (item.getAuthenticationStatus().equals("0")) { + item.setAuthenticationStatus("未鉴定"); + } else if (item.getAuthenticationStatus().equals("1")) { + item.setAuthenticationStatus("已鉴定"); + } else if (item.getAuthenticationStatus().equals("2")) { + item.setAuthenticationStatus("逾期未鉴定"); + } + } + if (StringUtils.isNotEmpty(item.getIdentificationResults())) { + if (item.getIdentificationResults().equals("0")) { + item.setIdentificationResults("合格"); + } else if (item.getIdentificationResults().equals("1")) { + item.setIdentificationResults("优秀"); + } else if (item.getIdentificationResults().equals("2")) { + item.setIdentificationResults("不合格"); + } + } + if (StringUtils.isNotEmpty(item.getAuthenticationType())) { + if (item.getAuthenticationType().equals("0")) { + item.setAuthenticationType("手动发起"); + } else if (item.getAuthenticationType().equals("1")) { + item.setAuthenticationType("课程学习鉴定"); + } else if (item.getAuthenticationType().equals("2")) { + item.setAuthenticationType("岗位学习鉴定"); + } else if (item.getAuthenticationType().equals("3")) { + item.setAuthenticationType("本人申请"); + } + } + }); + EasyExcelUtil.simpleWrite(list, "个人维度统计", FtbCultivateIdentityPersonWisdomStatisticVO.class, response); + } + /** + * 查询列表 + * @return + */ + @GetMapping("/query-correspond-list") + public ActionResult>> queryCorrespondingList(){ + List list = service.lambdaQuery() + .eq(SuperBaseEntity.SuperCUDBaseEntity::getDeleteMark, "0").list(); + List> collect = list.stream().map(item -> { + Map< String, Object> map = Maps.newHashMap(); + map.put("name",item.getName()); + map.put("id",item.getId()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(collect); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnCategoriesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnCategoriesController.java new file mode 100644 index 0000000..448a932 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnCategoriesController.java @@ -0,0 +1,81 @@ +package jnpf.cultivate.controller.web.learn; + + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateLearnCategoriesService; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnCategoriesDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnCategories; +import jnpf.model.cultivate.req.learn.AddLearnCategoryReq; +import jnpf.model.cultivate.req.learn.QueryLearnCategoryListReq; +import jnpf.model.cultivate.req.learn.UpdateLearnCategoryReq; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * web任务分类模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/web/learn/Categories") +public class FtbCultivateLearnCategoriesController { + + @Autowired + private FtbCultivateLearnCategoriesService cultivateLearnCategoriesService; + + + /** + * 查询分类接口 + * + * @param req + * @return + */ + @GetMapping("/lists") + public ActionResult> lists(@Valid QueryLearnCategoryListReq req) { + return ActionResult.success("成功", cultivateLearnCategoriesService.listCategory(req)); + } + + + /** + * 添加分类 + * + * @param req + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid AddLearnCategoryReq req) { + + return ActionResult.success("添加成功", cultivateLearnCategoriesService.insertData(req)); + } + + /** + * 修改分类 + * + * @param id 分类ID + * @param req + * @return + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid UpdateLearnCategoryReq req) { + return ActionResult.success("编辑成功", cultivateLearnCategoriesService.updateData(id, req)); + } + + /** + * 删除分类 + * + * @param id 分类ID + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + cultivateLearnCategoriesService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskContentController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskContentController.java new file mode 100644 index 0000000..6ad19af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskContentController.java @@ -0,0 +1,115 @@ +package jnpf.cultivate.controller.web.learn; + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateLearnTaskInfoContentService; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnAllocationDTO; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskInfoDto; +import jnpf.model.cultivate.vo.learn.FtbLearnQueryTaskDetailsEditingVO; +import jnpf.valid.ValidInsert; +import jnpf.valid.ValidUpdate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.groups.Default; +import java.util.List; + +/** + * web学习任务模块 + * + * @Author: wcx + * @create: 2024/9/9 + */ +@RestController +@Slf4j +@RequestMapping("/web/learn-task") +public class FtbCultivateLearnTaskContentController { + + @Resource + private FtbCultivateLearnTaskInfoContentService ftbCultivateLearnTaskInfoContentService; + + /** + * 保存学习任务 + * + * @param taskInfoDto + * @return + */ + @PostMapping("/save-learn-task") + public ActionResult saveLearnTaskList(@RequestBody @Validated(value = {Default.class, ValidInsert.class}) FtbCultivateLearnTaskInfoDto taskInfoDto) { + ftbCultivateLearnTaskInfoContentService.saveLearnTaskList(taskInfoDto); + return ActionResult.success(); + } + + /** + * 编辑学习任务 + * + * @param taskInfoDto + * @return + */ + @PostMapping("/update-learn-task") + public ActionResult updateLearnTaskList(@RequestBody @Validated(value = {Default.class, ValidUpdate.class}) FtbCultivateLearnTaskInfoDto taskInfoDto) { + ftbCultivateLearnTaskInfoContentService.updateLearnTaskList(taskInfoDto); + return ActionResult.success(); + } + + /** + * 任务分配-已分配的人员集合 + * + * @param taskId 任务主键ID(必传) + * @return {@link ActionResult } + */ + @GetMapping("/collection-of-assigned-people") + public ActionResult> getLearnTaskAllocationList(String taskId) { + List result = ftbCultivateLearnTaskInfoContentService.getLearnTaskAllocationList(taskId); + return ActionResult.success(result); + } + + /** + * 分配任务 + * + * @return {@link ActionResult } + */ + @PostMapping("/allocation-task") + public ActionResult allocation(@RequestBody @Validated FtbCultivateLearnAllocationDTO allocationDTO) { + ftbCultivateLearnTaskInfoContentService.allocation(allocationDTO); + return ActionResult.success(); + } + + /** + * 中止任务 + * + * @param id 任务主键ID(必传) + * @return {@link ActionResult } + */ + @PutMapping("/abort-task/{id}") + public ActionResult abort(@PathVariable("id") String id) { + ftbCultivateLearnTaskInfoContentService.abort(id); + return ActionResult.success(); + } + + /** + * 删除任务 + * + * @param id 任务主键ID(必传) + * @return {@link ActionResult } + */ + @DeleteMapping("/delete-task/{id}") + public ActionResult deleteTask(@PathVariable("id") String id) { + ftbCultivateLearnTaskInfoContentService.deleteTask(id); + return ActionResult.success(); + } + + /** + * 编辑时查询任务详情 + * + * @param id 任务主键ID(必传) + * @return {@link ActionResult } + */ + @GetMapping("/query-task-details-while-editing/{id}") + public ActionResult queryTaskDetailsWhileEditing(@PathVariable("id") String id) { + FtbLearnQueryTaskDetailsEditingVO ftbLearnQueryTaskDetailsEditingVO = ftbCultivateLearnTaskInfoContentService.queryTaskDetailsWhileEditing(id); + return ActionResult.success(ftbLearnQueryTaskDetailsEditingVO); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskCountController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskCountController.java new file mode 100644 index 0000000..74796e9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskCountController.java @@ -0,0 +1,75 @@ +package jnpf.cultivate.controller.web.learn; + + +import cn.hutool.core.date.DateUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateLearnTaskInfoCountService; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.model.cultivate.req.learn.QueryLearnTaskCountListReq; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoCountListVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoListVO; +import jnpf.model.cultivate.vo.learn.FtbExportCultivateLearnTaskInfoCountListVO; +import jnpf.util.FtbUtil; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +/** + * web学习任务统计模块 + * + * @Author: xuguilin + * @create: 2024/9/9:11:20 + */ +@RestController +@Slf4j +@RequestMapping("/web/learnTask/count") +public class FtbCultivateLearnTaskCountController { + + @Autowired + private FtbCultivateLearnTaskInfoCountService countService; + + + /** + * 查询任务统计数据列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbCultivateLearnTaskInfoListVO}> + */ + @GetMapping("/queryCountList") + public ActionResult> queryCountList(@Validated QueryLearnTaskCountListReq req) { + PageInfo pageVo = countService.queryCountList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 导出搜索的任务列表 + * + * @param req + * @param response + * @throws IOException + */ + @GetMapping("/exportSearchTaskList") + public void exportSearchTaskList(@Validated QueryLearnTaskCountListReq req, HttpServletResponse response) throws IOException { + req.setPageSize(-1); + req.setCurrentPage(1); + PageInfo pageVo = countService.queryCountList(req); + List list = pageVo.getList(); + List exportList = CultivateLearnUtils.convertFtbExportCultivateLearnTaskInfoCountListVO(list); + EasyExcelUtils.exportExcel(response, "培训任务统计-"+ DateUtil.format(new Date(), "yyyyMMddHHmmss"), exportList, FtbExportCultivateLearnTaskInfoCountListVO.class); + } + + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskInfoController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskInfoController.java new file mode 100644 index 0000000..fd6040b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskInfoController.java @@ -0,0 +1,115 @@ +package jnpf.cultivate.controller.web.learn; + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateLearnTaskInfoService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskInfoExprotDto; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishStatisticsVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoVO; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskUserFinishInfoVO; +import jnpf.model.enums.TaskLearnStatusEnums; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * web学习任务模块 + * + * @Author: peng.hao + * @create: 2024/9/9:11:20 + */ +@RestController +@Slf4j +@RequestMapping("/web/learn-task") +public class FtbCultivateLearnTaskInfoController { + + @Resource + private FtbCultivateLearnTaskInfoService taskInfoService; + + + /** + * 任务内容详情 + */ + @GetMapping("/getLearnTaskInfo") + public ActionResult getLearnTaskInfo(@RequestParam("taskId") String taskId) { + return ActionResult.success(taskInfoService.getLearnTaskInfo(taskId)); + } + + /** + * 完成情况统计 + * + * @param taskId 任务id + * @return + */ + @GetMapping("/getCompletionStatistics") + public ActionResult getCompletionStatistics(@RequestParam("taskId") String taskId) { + return ActionResult.success(taskInfoService.getCompletionStatistics(taskId)); + } + + /** + * 完成情况列表 + * + * @param taskId 任务id + * @return + */ + @GetMapping("/getListOfCompletions") + public ActionResult> getListOfCompletions(@RequestParam("taskId") String taskId, CultivatePage page) { + PageListVO listOfCompletions = taskInfoService.getListOfCompletions(taskId, page, null); + return ActionResult.success(listOfCompletions); + } + + /** + * 查看任务完成情况 + * + * @param taskId 任务id + * @param userId 用户id + */ + @GetMapping("/viewTaskCompletion") + public ActionResult viewTaskCompletion(@RequestParam("taskId") String taskId, + @RequestParam("userId") String userId) { + return ActionResult.success(taskInfoService.viewTaskCompletion(taskId, userId)); + } + + /** + * 导出完成情况列表 + * + * @return + */ + @PostMapping("/exportListOfCompletions") + public void exportListOfCompletions(@RequestBody FtbCultivateLearnTaskInfoExprotDto dto, + HttpServletResponse response) throws IOException { + CultivatePage page = new CultivatePage(); + page.setPageSize(-1); + PageListVO listOfCompletions = taskInfoService.getListOfCompletions(dto.getTaskId(), page,dto.getIds()); + List list = listOfCompletions.getList(); + dealExportExcelData(list); + EasyExcelUtils.exportExcel(response, "任务完成情况", list, FtbCultivateLearnTaskFinishInfoVO.class); + } + + /** + * 处理导入数据 + * @param list + */ + private void dealExportExcelData(List list) { + if(CollUtil.isEmpty(list)){ + return; + } + for (FtbCultivateLearnTaskFinishInfoVO vo : list) { + //处理状态 0未开始,1进行中,2已完成,3已逾期 + String completionStatusStr=""; + TaskLearnStatusEnums taskLearnStatusEnums = TaskLearnStatusEnums.fromCode(vo.getCompletionStatus()); + if(taskLearnStatusEnums!=null){ + completionStatusStr=taskLearnStatusEnums.getMsg(); + } + vo.setCompletionStatusStr(completionStatusStr); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskListController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskListController.java new file mode 100644 index 0000000..6132227 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/learn/FtbCultivateLearnTaskListController.java @@ -0,0 +1,219 @@ +package jnpf.cultivate.controller.web.learn; + +import cn.hutool.core.date.DateUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.cultivate.FtbCultivateLearnTaskListApi; +import jnpf.cultivate.event.impl.certificate.JnpfApplicationEventCertificateService; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateLearnTaskListService; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.req.learn.QueryLearnTaskListReq; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoListVO; +import jnpf.model.cultivate.vo.learn.FtbExportCultivateLearnTaskInfoListVO; +import jnpf.model.cultivate.vo.learn.WebLearnTaskDto; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +/** + * web学习任务列表模块 + * + * @Author: xuguilin + * @create: 2024/9/9:11:20 + */ +@RestController +@Slf4j +@RequestMapping("/web/learnTaskList/") +public class FtbCultivateLearnTaskListController implements FtbCultivateLearnTaskListApi { + + @Autowired + private FtbCultivateLearnTaskListService taskListService; + + + @Autowired + private CultivateLearnUtils cultivateLearnUtils; + + @Autowired + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Autowired + JnpfApplicationEventCertificateService jnpfApplicationEventCertificateService; + + + @Autowired + private ConfigValueUtil configValueUtil; + + /** + * 查询任务列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbCultivateLearnTaskInfoListVO}> + */ + @GetMapping("/queryList") + public ActionResult> pageLists(@Validated QueryLearnTaskListReq req) { + PageInfo pageVo = taskListService.getWebPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 导出搜索的任务列表 + * + * @param req + * @param response + * @throws IOException + */ + @GetMapping("/exportSearchTaskList") + public void exportSearchTaskList(@Validated QueryLearnTaskListReq req, HttpServletResponse response) throws IOException { + req.setPageSize(-1); + req.setCurrentPage(1); + PageInfo pageVo = taskListService.getWebPageList(req); + List list = pageVo.getList(); + List exportList = CultivateLearnUtils.convertFtbExportCultivateLearnTaskInfoListVO(list); + EasyExcelUtils.exportExcel(response, "任务列表-" + DateUtil.format(new Date(), "yyyyMMddHHmmss"), exportList, FtbExportCultivateLearnTaskInfoListVO.class); + } + + + /** + * 统计任务数量 + * + * @return + */ + @GetMapping("/countTaskNumber") + public ActionResult countTaskNumber() { + return ActionResult.success(taskListService.countTaskNumber()); + } + + /** + * 入职加入任务 + * + * @return + */ + @GetMapping("/entryCompany/{userId}") + public void entryCompany(@PathVariable("userId") String userId) { + cultivateLearnUtils.addNewPersonToTask(List.of(userId), UserProvider.getUser().getTenantId()); + } + + + + /** + * 定时提醒任务(每天定点) + * + * @return + */ + @GetMapping("/timingTaskLearningAlert/{tenantId}") + public void timingTaskLearningAlert(@PathVariable("tenantId") String tenantId) { + cultivateLearnUtils.timingTaskLearningAlert(tenantId); + } + + + /** + * 定时加入任务 + * + * @return + */ + @GetMapping("/timing/timingAddNewPersonToTaskNew/{tenantId}") + @NoDataSourceBind + @Override + public void timingAddNewPersonToTaskNew(@PathVariable("tenantId") String tenantId) { + log.error("timingAddNewPersonToTaskNew{}",tenantId); + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + cultivateLearnUtils.timingAddNewPersonToTask(tenantId); + } + + + /** + * 定时提醒任务(每天定点) + * + * @return + */ + @GetMapping("/timing/timingTaskLearningAlertNew/{tenantId}") + @NoDataSourceBind + @Override + public void timingTaskLearningAlertNew(@PathVariable("tenantId") String tenantId) { + log.error("timingTaskLearningAlertNew{}",tenantId); + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + cultivateLearnUtils.timingTaskLearningAlert(tenantId); + } + + + + /** + * 触发考试完成(完成任务) + * + * @return + */ + @GetMapping("/timingTaskExam/{tastkId}/{userId}") + public void timingTaskExam(@PathVariable("tastkId") String tastkId, @PathVariable("userId") String userId) { + cultivateIdentifyApplyService.turnOnVisibilityWithTaskId(tastkId, userId,0); + + } + + + /** + * 测试接口(触发鉴定接口) + * + * @return + */ + @GetMapping("/jnpfApplicationEventCertificateService") + public void timingTaskExam() { +// CertificateEventDTO(postId=null, userId=583985483894499333, courseId=null, taskId=1845023199187787777, source=1, issuanceType=2) + CertificateEventDTO dto =CertificateEventDTO.builder().userId("583985483894499333").taskId("1848549705067352066").source(2).issuanceType(2).build(); + jnpfApplicationEventCertificateService.handlerCourseEvent(dto); + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/offline/FtbCultivateOfflineTrainController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/offline/FtbCultivateOfflineTrainController.java new file mode 100644 index 0000000..f5c787b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/offline/FtbCultivateOfflineTrainController.java @@ -0,0 +1,173 @@ +package jnpf.cultivate.controller.web.offline; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateOfflineTrainService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.offline.*; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainDetailsVO; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainPageVO; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainPeopleSigningInVO; +import jnpf.permission.entity.UserEntity; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** +* web线下培训模块 +* + * @author wcx +*/ +@RestController +@RequestMapping("/web/ftb-cultivate-offline-train") +public class FtbCultivateOfflineTrainController { + + @Resource + private FtbCultivateOfflineTrainService ftbCultivateOfflineTrainService; + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 添加线下培训 + * + * @param ftbCultivateCourseDTO 线下培训参数 + * @return {@link ActionResult} + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FtbCultivateOfflineTrainDTO ftbCultivateCourseDTO) { + ftbCultivateOfflineTrainService.add(ftbCultivateCourseDTO); + return ActionResult.success(); + } + + /** + * 查看线下培训时-培训结果变更 + * + * @param ftbCultivateCourseDTO 线下培训参数 + * @return {@link ActionResult} + */ + @PostMapping("/update") + public ActionResult update(@Validated @RequestBody FtbCultivateOfflineTrainUpdateDTO ftbCultivateCourseDTO) { + ftbCultivateOfflineTrainService.updateTrainingResults(UserProvider.getUser(), ftbCultivateCourseDTO); + return ActionResult.success(); + } + + /** + * 线下培训分页查询 + * + * @param cultivatePage 分页模型 + * @param keywords 关键字查询 + * @return {@link ActionResult} + */ + @GetMapping("/query-list") + public ActionResult> list(CultivatePage cultivatePage, String keywords) { + Page page = cultivatePage.coverCultivatePage("a.F_Id"); + page = ftbCultivateOfflineTrainService.listPage(page, keywords); + PageListVO ftbCultivateOfflineTrainPageVOPageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(ftbCultivateOfflineTrainPageVOPageListVO); + } + + /** + * 线下培训详情-编辑时回显调用 + * + * @param id 线下培训主键id + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @GetMapping("/details/{id}") + public ActionResult courseDetails(@PathVariable("id") String id) { + FtbCultivateOfflineTrainDetailsVO ftbCultivateOfflineTrainDetailsVO = ftbCultivateOfflineTrainService.courseDetails(id); + return ActionResult.success(ftbCultivateOfflineTrainDetailsVO); + } + + /** + * 删除线下培训 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult deleteOfflineTrain(@PathVariable("id") String id) { + ftbCultivateOfflineTrainService.deleteOfflineTrain(id); + return ActionResult.success(); + } + + /** + * 成员管理-查询已有成员,返回用户主键id + * + * @param id 线下培训主键id(必传) + * @return {@link ActionResult } + */ + @GetMapping("/query-existing-members") + public ActionResult queryExistingMembers(String id) { + List results = ftbCultivateOfflineTrainService.queryExistingMembers(id); + return ActionResult.success(results); + } + + /** + * 成员管理-修改线下培训成员 + * + * @return {@link ActionResult } + */ + @PostMapping("/modify-offline-training-members") + public ActionResult modifyOfflineTrainingMembers(@Validated @RequestBody FtbModifyOfflineTrainingMembersDTO data) { + ftbCultivateOfflineTrainService.modifyOfflineTrainingMembers(data); + return ActionResult.success(); + } + + /** + * 撤回 + * + * @param id 线下培训主键id(必传) + * @return {@link ActionResult } + */ + @PutMapping("/withdraw/{id}") + public ActionResult withdraw(@PathVariable("id") String id) { + ftbCultivateOfflineTrainService.withdraw(id); + return ActionResult.success(); + } + + /** + * 线下培训签到人数分页查询 + * + * @param cultivatePage 分页模型 + * @return {@link ActionResult} + */ + @GetMapping("/query-list-people-signing") + public ActionResult> numberOfflineTraining(CultivatePage cultivatePage, + FtbCultivateOfflineTrainPeopleSigningInDTO data) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbCultivateOfflineTrainService.numberOfflineTraining(page, data); + if(CollUtil.isNotEmpty(page.getRecords())){ + List userIds = page.getRecords().stream() + .map(FtbCultivateOfflineTrainPeopleSigningInVO::getUserId).collect(Collectors.toList()); + Map userNameAndCopyForUserIds = userApiV2Util.getUserNameAndCopyForUserIds(userIds); + page.getRecords().forEach(item -> { + UserEntity userEntity = userNameAndCopyForUserIds.get(item.getUserId()); + if(userEntity != null){ + item.setSignInPersonName(userEntity.getRealName()); + } + }); + } + PageListVO ftbCultivateOfflineTrainPageVOPageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(ftbCultivateOfflineTrainPageVOPageListVO); + } + + /** + * 编辑线下培训 + * @return {@link ActionResult} + */ + @PostMapping("/edit-offline-training") + public ActionResult editOfflineTraining(@Validated @RequestBody FtbCultivateOfflineTrainChangeDTO ftbCultivateOfflineTrainDTO) { + ftbCultivateOfflineTrainService.editOfflineTraining(ftbCultivateOfflineTrainDTO); + return ActionResult.success(); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/org/FtbCultivateOrgController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/org/FtbCultivateOrgController.java new file mode 100644 index 0000000..4eccf0a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/org/FtbCultivateOrgController.java @@ -0,0 +1,241 @@ +package jnpf.cultivate.controller.web.org; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.cultivate.mapper.FtbCultivatePromotionPostNewMapper; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.po.org.FtbCultivatePositionGradeVO; +import jnpf.model.cultivate.po.org.FtbPositionGradesInfoBoundVO; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.BasePositionGradesEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.position.PositionInfoNewVO; +import jnpf.permission.model.user.OrganizeInfoVo; +import jnpf.permission.model.user.PositionMoreBoundVO; +import jnpf.permission.model.user.SubordinateUserInfoVO; +import jnpf.permission.model.user.UserBoundMoreInfoVO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.UploaderUtil; +import jnpf.yozo.utils.HttpRequestUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web和app模块组织信息 + * + * @Author:peng.hao + * @create: 2024/1/211:31 + */ +@RestController +@RequestMapping("/cultivate-org") +public class FtbCultivateOrgController { + + @Autowired + private UserApi userApi; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Resource + private PositionApi positionApi; + + @Resource + FtbCultivatePromotionPostNewMapper ftbCultivatePromotionPostNewMapper; + + + /** + * 根据用户id 查询当前岗位的信息包含职等信息 岗位详情信息(包含职级) + */ + @GetMapping("/queryUserSCurrPoInfo") + public ActionResult> queryUserSCurrentPositionInformation(@RequestParam("userId") String userId, + @RequestParam("orgId") String orgId) { + List list = getNewOrg(userId, orgId); + return ActionResult.success(list); + } + + private List getNewOrg(String userId, String orgId) { + Map userPrimaryBoundBatchCompatible = userApiV2Util.getUserPrimaryBoundBatch(List.of(userId), null); + UserBoundVO userPrimaryBoundVOS = userPrimaryBoundBatchCompatible.get(userId); + if (userPrimaryBoundVOS != null) { + FtbPositionGradesInfoBoundVO ftbPositionGradesInfoBoundVO = this.covertVoNew(userPrimaryBoundVOS); + if(ftbPositionGradesInfoBoundVO!=null){ + return List.of(ftbPositionGradesInfoBoundVO); + } + } + return null; + } + + + public FtbPositionGradesInfoBoundVO covertVoNew(UserBoundVO userBoundMoreInfoVO) { + if (userBoundMoreInfoVO == null) { + return null; + } + FtbPositionGradesInfoBoundVO ftbPositionGradesInfoBoundVO = new FtbPositionGradesInfoBoundVO(); + ftbPositionGradesInfoBoundVO.setUserId(userBoundMoreInfoVO.getId()); + ftbPositionGradesInfoBoundVO.setOrganizeId(userBoundMoreInfoVO.getOrganizeId()); + ftbPositionGradesInfoBoundVO.setOrganizeName(userBoundMoreInfoVO.getOrganizeName()); + ftbPositionGradesInfoBoundVO.setPositionId(userBoundMoreInfoVO.getPositionId()); + ftbPositionGradesInfoBoundVO.setPositionName(userBoundMoreInfoVO.getPositionName()); + ftbPositionGradesInfoBoundVO.setPositionGradesId(userBoundMoreInfoVO.getGradeId()); +// ftbPositionGradesInfoBoundVO.setPositionGradesLevel(userBoundMoreInfoVO.get()); + ftbPositionGradesInfoBoundVO.setPositionGradesName(userBoundMoreInfoVO.getGradeName()); +// UserEntity infoById = userApi.getInfoById(userBoundMoreInfoVO.getId()); + ftbPositionGradesInfoBoundVO.setUserName(userBoundMoreInfoVO.getUserName()); + return ftbPositionGradesInfoBoundVO; + } + + + /** + * 根据用户id 查询当前组织的信息包含所有的岗位信息(当前组织内所有岗位信息) + */ + @GetMapping("/queryUserOrganizeInfo/{userId}") + public ActionResult> queryUserOrganizeInfo(@PathVariable("userId") String userId) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (userPrimaryBoundOne != null) { + List positionAndGradesVOS = userApiV2Util.listPositionAndGradesByPositionNameForOrgIds(userPrimaryBoundOne.getOrganizeId(), null); + return ActionResult.success(positionAndGradesVOS); + } +// List byUserId = userApi.getUserOrganizeInfoByUserId(userId); + return ActionResult.success(new ArrayList<>()); + } + + /** + * 根据用户id 该用户下属的成员信息 + */ + @GetMapping("/queryUnderlingByUserId") + public ActionResult> queryUnderlingByUserId(@RequestParam("userId") String userId, + @RequestParam("orgId") String orgId) { +// List subordinateUserInfoVOS = userApi.userInfoByLeaderId(userId); + List subordinateUserInfoVOS = userApiV2Util.listUnderlingTargetUser(userId, null); + List list = new ArrayList<>(); + if (subordinateUserInfoVOS != null) { + //筛选同组织下属 + + for (UserPageListVO vo : subordinateUserInfoVOS) { + if(!vo.getOrganizeId().equals(orgId)){ + continue; + } + FtbCultivatePositionGradeVO newSub = new FtbCultivatePositionGradeVO(); + newSub.setRealName(vo.getName()); + newSub.setUserId(vo.getId()); + newSub.setOrganizeId(vo.getOrganizeId()); + newSub.setHeadIcon(UploaderUtil.uploaderImg(vo.getHeadIcon())); + newSub.setOrganizeId(vo.getOrganizeId()); + newSub.setOrganizeName(vo.getOrganizeName()); + newSub.setPositionId(vo.getPositionId()); + newSub.setPositionName(vo.getPositionName()); + + PositionGradesInfoBoundVO positionMoreBoundVO = new PositionGradesInfoBoundVO(); + positionMoreBoundVO.setUserId(vo.getId()); + positionMoreBoundVO.setOrganizeIds(List.of(vo.getOrganizeId())); + positionMoreBoundVO.setOrganizeNames(List.of(vo.getOrganizeName())); + positionMoreBoundVO.setPositionId(vo.getPositionId()); + positionMoreBoundVO.setPositionName(vo.getPositionName()); + positionMoreBoundVO.setPositionGradesId(vo.getGradeId()); + positionMoreBoundVO.setPositionGradesName(vo.getGradeName()); + newSub.setPositionGradesInfoBoundVO(positionMoreBoundVO); + list.add(newSub); + } + + } + return ActionResult.success(list); + } + + + /** + * 根据用户id和组织id 筛选晋升岗位信息 + */ + @GetMapping("/queryPostInfoByOrgAndUserId") + public ActionResult> queryPostInfoByOrgAndUserId(@RequestParam("userId") String userId, + @RequestParam("orgId") String orgId) { + // 获取用户的岗位职等 + List orgPositionInfoList = getNewOrg(userId, orgId); + List result = new LinkedList<>(); + if (CollUtil.isEmpty(orgPositionInfoList)) { + return ActionResult.success(result); + } + Map stringStringMap = orgPositionInfoList.stream() + .collect( + Collectors.toMap(FtbPositionGradesInfoBoundVO::getPositionId, + FtbPositionGradesInfoBoundVO::getPositionGradesId, + (a, b) -> a)); + // 查询当前岗位下的所有岗位加职等 + ActionResult> listByOrganizeIds = positionApi.getListByOrganizeIds(orgId); + if (listByOrganizeIds.getCode() == 200) { + List data = listByOrganizeIds.getData(); + for (PositionInfoNewVO positionInfoNewVO : data) { + List positionGradesList = positionInfoNewVO.getPositionGradesList(); + // 通过岗位ID获取职等id 进行过滤 + String positionGradesId = stringStringMap.get(positionInfoNewVO.getId()); + if (stringStringMap.containsKey(positionInfoNewVO.getId())) { + // 通过职等ID过滤已经存在的职等 + List newPositionGradesList = + positionGradesList.stream().filter(item -> !item.getId().equals(positionGradesId)) + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(newPositionGradesList)) { + positionInfoNewVO.setPositionGradesList(null); + positionInfoNewVO.setPositionGradesList(newPositionGradesList); + // 过滤已存在的 + result.add(positionInfoNewVO); + } + } else { + // 封装每一个不存在的岗位 + result.add(positionInfoNewVO); + } + } + } + return ActionResult.success(result); + } + + /** + * 岗位详情信息查询 + */ + @GetMapping("/getInfoNew") + public ActionResult getInfoNew(@RequestParam("postId") String postId) throws Exception { + ActionResult infoNew = positionApi.getInfoNew(postId); + if (infoNew.getCode() == 200) { + return ActionResult.success(infoNew.getData()); + } + return ActionResult.fail("该岗位没有职等"); + } + + /** + * 获取岗位信息列表过滤初始岗位 + * + * @return + */ + @GetMapping("/getLearnMapFilterInformation") + public ActionResult> getLearnMapFilterInformation() { + ActionResult> result = positionApi.allInfoList(); + if (result != null && result.getCode() != 200) { + return ActionResult.fail("获取岗位信息失败"); + } + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getLevel, "1"); + List postNews = ftbCultivatePromotionPostNewMapper.selectList(lambdaQuery); + if (CollUtil.isNotEmpty(postNews)) { + List postIds = postNews.stream().map(FtbCultivatePromotionPostNew::getPostId).collect(Collectors.toList()); + List data = result.getData(); + // data = data.stream().filter(item->!postIds.contains(item.getId())).collect(Collectors.toList()); + return ActionResult.success(data); + } + return ActionResult.success(result.getData()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/paper/FtbCultivateTestPaperController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/paper/FtbCultivateTestPaperController.java new file mode 100644 index 0000000..7179b33 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/paper/FtbCultivateTestPaperController.java @@ -0,0 +1,189 @@ +package jnpf.cultivate.controller.web.paper; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateTestPaperService; +import jnpf.model.cultivate.req.paper.QueryPaperReq; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.resp.PaperDetailVo; +import jnpf.model.cultivate.resp.PaperListVo; +import jnpf.util.FtbUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * web试卷表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/paper") +public class FtbCultivateTestPaperController { + + /** + * 服务对象 + */ + @Resource + private FtbCultivateTestPaperService paperService; + + + /** + * 分页列出试卷列表 + * + * @param req + * @return + */ + @GetMapping("/pageLists") + public ActionResult> pageLists(QueryPaperReq req) { + + PageInfo pageVo = paperService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 添加/修改试卷 + * + * @param req + * @return + */ + @PostMapping("/save") + public ActionResult save(@RequestBody @Valid SavePaperReq req) { + if (StringUtils.isEmpty(req.getId())) { + //新增试卷 + paperService.insertData(req); + } else { + //修改试卷 + paperService.updatePaperAndQuestion(req); + } + return ActionResult.success(true); + } + + + /** + * 修改试卷 + * + * @param req + * @return + */ + @PostMapping("/edit") + public ActionResult edit(@RequestBody @Valid SavePaperReq req) { + if (StringUtils.isEmpty(req.getId())) { + throw new RuntimeException("参数错误,试卷ID不能够为空"); + } + paperService.updatePaperAndQuestion(req); + + return ActionResult.success(true); + } + + + /** + * 添加试卷 + * + * @param req + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid SavePaperReq req) { + //新增试卷 + paperService.insertData(req); + return ActionResult.success(true); + } + + /** + * 修改试卷(新) + * + * @param req + * @return + */ + @PostMapping("/update") + public ActionResult update(@RequestBody @Valid SavePaperReq req) { + if (StringUtils.isEmpty(req.getId())) { + //新增试卷 + throw new RuntimeException("试卷ID不能为空"); + } + //修改试卷 + paperService.updatePaperAndQuestion(req); + + return ActionResult.success(true); + } + + /** + * 删除试卷 + * + * @param id 试卷ID + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + paperService.deleteData(id); + return ActionResult.success(true); + } + + + /** + * 启用禁用试卷 + * + * @param id 试卷ID + * @param status 状态(0启用,1禁用) + * @return + */ + @PutMapping("/switch-status/{id}/{status}") +// @SaCheckPermission(PAPER_PERMISSION_PRE + "btn_paper_switch_status") + public ActionResult switchEnabledMark(@PathVariable("id") String id, @PathVariable("status") Integer status) { + paperService.switchEnabledMark(id, status); + return ActionResult.success(true); + } + + + /** + * 设置正式试卷 + * + * @param id + * @return + */ + @PutMapping("/set-formal-paper/{id}") + public ActionResult setFormalPaper(@PathVariable("id") String id) { + paperService.setFormalPaper(id); + return ActionResult.success(true); + } + + /** + * web试卷列表-查看试卷基本信息,主要用于复制回显 + * + * @param id 试卷ID + * @return + */ + @GetMapping("/get/{id}") + public ActionResult queryDetail(@PathVariable("id") String id) { + return ActionResult.success(paperService.getInfo(id)); + } + + + /** + * 检测试卷是否可以删除 + * + * @param id 试卷ID + * @return + */ + @GetMapping("/check-can-del/{id}") + public ActionResult checkCanDel(@PathVariable("id") String id) { + return ActionResult.success(paperService.checkPaperCanDelete(id)); + } + + /** + * 查询试卷详情和试卷题目列表 + * + * @param id 试卷id + * @return + */ + @RequestMapping("/queryPaperInfoAndQuestionList/{id}") + public ActionResult queryQuestionList(@PathVariable("id") String id) { + return ActionResult.success(paperService.queryPaperInfoAndQuestionList(id)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionAssessmentController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionAssessmentController.java new file mode 100644 index 0000000..edd3c37 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionAssessmentController.java @@ -0,0 +1,85 @@ +package jnpf.cultivate.controller.web.position; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePositionAssessmentService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionAssessmentOrgStatisticVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionAssessmentListVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionAssessmentVO; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * web岗位考核统计 + * @Author: peng.hao + * @create: 2024/7/22:15:59 + * + */ +@RestController +@RequestMapping("/position-assessment") +public class FtbCultivatePositionAssessmentController { + + @Resource + FtbCultivatePositionAssessmentService service; + + + /** + * 组织维度统计 + * + * @param statisticDTO + * @return + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbCultivatePositionOrgStatisticesDTO statisticDTO) { + PageListVO listVO = service.organizationListStatistics(statisticDTO); + return ActionResult.success(listVO); + } + + /** + * 组织维度统计-导出 + * + * @param statisticDTO + */ + @PostMapping("/org-list-statistics/export") + public void exportOrganizationListStatistics(@RequestBody FtbCultivatePositionOrgStatisticesDTO statisticDTO, HttpServletResponse response) throws IOException { + statisticDTO.setPageSize(999999); + PageListVO listVO = service.organizationListStatistics(statisticDTO); + EasyExcelUtils.exportExcel(response, "组织维度统计", listVO.getList(), FtbCultivatePositionAssessmentOrgStatisticVO.class); + } + + /** + * 岗位考核详情 + * @param orgId 组织id + * @param positionId 岗位id + * @return + */ + @GetMapping("/job-assessment-list") + public ActionResult jobAssessmentList(@RequestParam("orgId") String orgId, + @RequestParam("positionId") String positionId, + CultivatePage page){ + FtbCultivatePositionAssessmentVO listVO = service.jobAssessmentList(orgId, positionId, page); + return ActionResult.success(listVO); + } + /** + * 岗位考核详情导出 + * @param orgId 组织id + * @param positionId 岗位id + * @return + */ + @GetMapping("/job-assessment-list/export") + public void exportJobAssessmentList(@RequestParam("orgId") String orgId, + @RequestParam("positionId") String positionId, + CultivatePage page, + HttpServletResponse response) throws IOException { + page.setPageSize(999999); + FtbCultivatePositionAssessmentVO listVO = service.jobAssessmentList(orgId, positionId, page); + EasyExcelUtils.exportExcel(response, "岗位考核详情列表", listVO.getList().getList(), FtbCultivatePositionAssessmentListVO.class); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionController.java new file mode 100644 index 0000000..a4c7028 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionController.java @@ -0,0 +1,475 @@ +package jnpf.cultivate.controller.web.position; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePositionService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.*; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionJobLearnCourseCertificateDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionJobLearnCourseStateSwitchDTO; +import jnpf.model.cultivate.dto.position.web.FtbJobLearnCourseRetakeCertificateDTO; +import jnpf.model.cultivate.dto.statistics.CultivatePermissionModuleParam; +import jnpf.model.cultivate.vo.common.InnerPowerPositionVO; +import jnpf.model.cultivate.vo.position.*; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.model.BaseListIdDTO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * web岗位学习模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/position") +public class FtbCultivatePositionController { + + @Resource + private FtbCultivatePositionService ftbCultivatePositionService; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 添加岗位学习,返回岗位学习主键id,用于操作岗位 + * + * @param ftbCultivatePositionJobLearnDTO + * @return + */ + @PostMapping("/add-job-learning") + public ActionResult addJobLearning(@RequestBody @Validated FtbCultivatePositionJobLearnDTO ftbCultivatePositionJobLearnDTO) { + String id = ftbCultivatePositionService.addJobLearning(ftbCultivatePositionJobLearnDTO); + return ActionResult.success(id); + } + + /** + * 岗位学习上下架 + * + * @param ftbCultivateShelvesDTO + * @return {@link ActionResult} + */ + @PostMapping("/upper-lower-shelves") + public ActionResult upperLower(@Validated @RequestBody FtbCultivatePositionShelvesDTO ftbCultivateShelvesDTO) { + ftbCultivatePositionService.upperLower(ftbCultivateShelvesDTO); + return ActionResult.success(); + } + + /** + * 添加岗位学习考试 + * + * @param ftbCultivatePositionJobLearnExamDTO + * @return {@link ActionResult} + */ + @PostMapping("/job-learning-exam") + public ActionResult addJobLearningExam(@Validated @RequestBody FtbCultivatePositionJobLearnExamDTO ftbCultivatePositionJobLearnExamDTO) { + ftbCultivatePositionService.addJobLearningExam(ftbCultivatePositionJobLearnExamDTO); + return ActionResult.success("操作成功"); + } + + /** + * 添加岗位学习实操鉴定 + * + * @param ftbCultivatePositionLearnPracticalDTO + * @return {@link ActionResult} + */ + @PostMapping("/post-learning-practical-appraisal") + public ActionResult addPostLearningPracticalAppraisal(@Validated @RequestBody FtbCultivatePositionLearnPracticalDTO ftbCultivatePositionLearnPracticalDTO) { + ftbCultivatePositionService.addPostLearningPracticalAppraisal(ftbCultivatePositionLearnPracticalDTO); + return ActionResult.success("操作成功"); + } + + /** + * 添加岗位学习课程 + * + * @param ftbCultivatePositionLearnPracticalDTO + * @return {@link ActionResult} + */ + @PostMapping("/job-learning-courses") + public ActionResult addJobLearningCourses(@Validated @RequestBody FtbCultivatePositionLearingCourselDTO ftbCultivatePositionLearnPracticalDTO) { + ftbCultivatePositionService.addJobLearningCourses(ftbCultivatePositionLearnPracticalDTO); + return ActionResult.success("操作成功"); + } + + + /** + * 添加岗位学习课程时,课程列表选择 + * + * @param ftbCultivatePositionCoursePageDTO + * @return {@link ActionResult} + */ + @GetMapping("/job-learn-course-list") + public ActionResult> jobLearningCourseList(FtbCultivatePositionCoursePageDTO ftbCultivatePositionCoursePageDTO) { + ftbCultivatePositionCoursePageDTO.setLabel(2); + List result = ftbCultivatePositionService.jobLearningCourseList(ftbCultivatePositionCoursePageDTO); + return ActionResult.success(result); + } + + /** + * 岗位学习中课程列表页 + * + * @param ftbCultivatePositionCourseLevelPageDTO + * @param cultivatePage + * @return {@link ActionResult}<{@link List}<{@link FtbCultivatePositionJobLearnCourseVO}>> + */ + @GetMapping("/job-level-course-list") + public ActionResult> jobLevelCourseList(CultivatePage cultivatePage, + FtbCultivatePositionCourseLevelPageDTO ftbCultivatePositionCourseLevelPageDTO) { + Page page = cultivatePage.coverCultivatePage("a.F_Id"); + page = ftbCultivatePositionService.jobLevelCourseList(page, ftbCultivatePositionCourseLevelPageDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 岗位学习课程-下拉列表 + * + * @param ftbCourseDropDownListDTO + * @return {@link ActionResult} + */ + @GetMapping("/course-drop-down-list") + public ActionResult courseDropDownList(FtbCourseDropDownListDTO ftbCourseDropDownListDTO) { + FtbPositionCourseDropDownListVO ftbPositionCourseDropDownListVO = ftbCultivatePositionService.courseDropDownList(ftbCourseDropDownListDTO); + return ActionResult.success(ftbPositionCourseDropDownListVO); + } + + /** + * 岗位学习中课程列表-添加课程考试 + * + * @param ftbStudyCourseExamAddDTO + * @return {@link ActionResult} + */ + @PostMapping("/study-course-exam-added") + public ActionResult studyCourseExamAdded(@Validated @RequestBody FtbStudyCourseExamAddDTO ftbStudyCourseExamAddDTO) { + ftbCultivatePositionService.studyCourseExamAdded(ftbStudyCourseExamAddDTO); + return ActionResult.success(); + } + + /** + * 岗位学习中课程列表-添加课程鉴定 + * + * @param ftbStudyCourseIdentityAddDTO + * @return {@link ActionResult} + */ + @PostMapping("/study-course-identity-added") + public ActionResult studyCourseIdentityAdded(@Validated @RequestBody FtbStudyCourseIdentityAddDTO ftbStudyCourseIdentityAddDTO) { + ftbCultivatePositionService.studyCourseIdentityAdded(ftbStudyCourseIdentityAddDTO); + return ActionResult.success(); + } + + /** + * 岗位学习中课程列表-课程考试删除接口 + * + * @param courseExamId 岗位学习课程考试关联主键id,必填 + * @return 删除结果 + */ + @DeleteMapping("/course-exam-deletion/{id}") + public ActionResult courseExamDeletion(@PathVariable("id") String courseExamId) { + ftbCultivatePositionService.courseExamDeletion(courseExamId); + return ActionResult.success(); + } + + /** + * 岗位学习中课程列表-课程鉴定删除接口 + * + * @param courseIdentityId 岗位学习课程鉴定关联主键id + * @return 删除结果 + */ + @DeleteMapping("/course-identity-deletion/{id}") + public ActionResult courseIdentityDeletion(@PathVariable("id") String courseIdentityId) { + ftbCultivatePositionService.courseIdentityDeletion(courseIdentityId); + return ActionResult.success(); + } + + /** + * 岗位学习中课程列表-删除岗位学习课程 + * + * @param id 岗位学习课程主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/deletion-job-learning-courses") + public ActionResult deletionOfOnTheJobLearningCourses(String id) { + ftbCultivatePositionService.deletionOfOnTheJobLearningCourses(id); + return ActionResult.success(); + } + + /** + * 岗位学习课程列表-重选考试 + * + * @return {@link ActionResult} + */ + @PutMapping("/retake-exam") + public ActionResult retakeExam(@Validated @RequestBody FtbRetakeExamDTO ftbRetakeExamDTO) { + ftbCultivatePositionService.retakeExam(ftbRetakeExamDTO); + return ActionResult.success(); + } + + /** + * 岗位学习课程列表-重选鉴定 + * + * @return {@link ActionResult} + */ + @PutMapping("/reselection-appraisal") + public ActionResult reselectionAppraisal(@Validated @RequestBody FtbReselectionAppraisalDTO ftbReselectionAppraisalDTO) { + ftbCultivatePositionService.reselectionAppraisal(ftbReselectionAppraisalDTO); + return ActionResult.success(); + } + + + /** + * 岗位学习管理-岗位学习分页列表 + * + * @param ftbJobLearningPaginDTO + * @return {@link ActionResult}<{@link PageListVO}<{@link FtbJobLearningPaginatedVO}>> + */ + @GetMapping("/job-learning-paginated-list") + public ActionResult> jobLearningPaginatedList(CultivatePage cultivatePage, FtbJobLearningPaginDTO ftbJobLearningPaginDTO) { + Page page = cultivatePage.coverCultivatePage("F_Id"); + page = ftbCultivatePositionService.jobLearningPaginatedList(page, ftbJobLearningPaginDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 岗位学习管理-删除岗位学习 + * + * @param id 岗位学习主键id(必传),二次删除判断,通过列表中职等数、试卷数、鉴定数、课程数、鉴定数等数量任意是否大于0,弹出内容提示框 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete-job-learning/{id}/{flag}") + public ActionResult deletionOfJobLearning(@PathVariable("id") String id) { + if (StrUtil.isBlank(id)) { + throw new IllegalArgumentException("参数错误"); + } + return ftbCultivatePositionService.deletionOfJobLearning(id); + } + + /** + * 岗位学习配置总览 + * + * @param postLearnId 岗位学习主键ID(必传) + * @param postId 岗位主键ID(必传) + * @return {@link ActionResult} + */ + @GetMapping("/overview-job-learning-configuration") + public ActionResult overviewOfJobLearningConfiguration(String postLearnId, String postId) { + FtbJobLearningConfigurationVO ftbJobLearningConfigurationVO = ftbCultivatePositionService.overviewOfJobLearningConfiguration(postLearnId, postId); + return ActionResult.success(ftbJobLearningConfigurationVO); + } + + /** + * 岗位学习,根据岗位ID和岗位学习ID,获取实操鉴定和考试及证书 + * + * @param postLearnId 岗位学习主键ID(必传) + * @param postIdID 岗位ID(必传) + * @return {@link ActionResult} + */ + @GetMapping("/job-learning-exam-appraisal") + public ActionResult jobLearningExamAppraisal(String postLearnId, String postIdID) { + FtbJobLearningExamAppraisalVO ftbJobLearningExamAppraisalVO = ftbCultivatePositionService.jobLearningExamAppraisal(postLearnId, postIdID); + return ActionResult.success(ftbJobLearningExamAppraisalVO); + } + + /** + * 岗位学习-考试删除 + * + * @param postLearnId 岗位学习主键ID(必传) + * @param postRankId 岗位ID(必传) + * @return {@link ActionResult} + */ + @DeleteMapping("/post-study-exam-deleted") + public ActionResult postStudyExamDeleted(String postLearnId, String postRankId) { + ftbCultivatePositionService.postStudyExamDeleted(postLearnId, postRankId); + return ActionResult.success(); + } + + /** + * 岗位学习-实操鉴定删除 + * + * @param postLearnId 岗位学习主键ID(必传) + * @param postRankId 岗位ID(必传) + * @return {@link ActionResult} + */ + @DeleteMapping("/job-learning-appraisal-delete") + public ActionResult jobLearningPracticalAppraisalDelete(String postLearnId, String postRankId) { + ftbCultivatePositionService.jobLearningPracticalAppraisalDelete(postLearnId, postRankId); + return ActionResult.success(); + } + + /** + * 成员管理模块调用此接口-删除该员工绑定职等对应的岗位学习 + * + * @param id 岗位id(必填) + * @param userId 用户id(必填) + * @return {@link ActionResult} + */ + @DeleteMapping("/delete-member-grade/{id}") + public ActionResult deleteMemberGrade(@PathVariable("id") String id, String userId) { + ftbCultivatePositionService.deleteMemberGrade(id, userId); + return ActionResult.success(); + } + + /** + * 获取绑定岗位的人数 + * + * @param postionId 岗位主键id(必传) + * @return {@link ActionResult} + */ + @GetMapping("/get-people-bound-to-the-position") + public ActionResult getTheNumberOfPeopleBoundToThePosition(String postionId) { + // 获取该岗位下所有人 + List userListForPositions = userApiV2Util.getUserListForPositions(List.of(postionId), null); + return ActionResult.success(userListForPositions.size()); + } + + /** + * 添加岗位学习证书 + * + * @param certificateDTO DTO 证书 + * @return {@link ActionResult } + */ + @PostMapping("/on-the-job-learning-certificate") + public ActionResult onTheJobLearningCertificate(@Validated @RequestBody FtbCultivatePositionJobLearnCertificateDTO certificateDTO) { + ftbCultivatePositionService.onTheJobLearningCertificate(certificateDTO); + return ActionResult.success(); + } + + /** + * 岗位学习-证书删除 + * + * @param postLearnId 岗位学习主键ID(必传) + * @param postRankId 岗位ID(必传) + * @return {@link ActionResult} + */ + @DeleteMapping("/certificate-deletion") + public ActionResult certificateDeletion(String postLearnId, String postRankId) { + ftbCultivatePositionService.certificateDeletion(postLearnId, postRankId); + return ActionResult.success(); + } + + + /** + * 添加岗位学习课程证书 + * + * @param certificateDTO DTO 证书 + * @return {@link ActionResult } + */ + @PostMapping("/add-job-learning-course-certificate") + public ActionResult addJobLearningCourseCertificate(@Validated @RequestBody FtbCultivatePositionJobLearnCourseCertificateDTO certificateDTO) { + ftbCultivatePositionService.addJobLearningCourseCertificate(certificateDTO); + return ActionResult.success(); + } + + /** + * 岗位学习-课程证书删除 + * + * @param postLearnId 岗位学习主键ID(必传) + * @param courseId 课程ID(必传) + * @return {@link ActionResult} + */ + @DeleteMapping("/certificate-course-deletion/{postLearnId}/{courseId}") + public ActionResult certificateCourseDeletion(@PathVariable("postLearnId") String postLearnId, @PathVariable("courseId") String courseId) { + ftbCultivatePositionService.certificateCourseDeletion(postLearnId, courseId); + return ActionResult.success(); + } + + /** + * 更改课程选修和必修切换 + * + * @return {@link ActionResult } + */ + @PostMapping("/switch-compulsory-courses") + public ActionResult changeCourseElectiveAndRequiredSwitching(@Validated @RequestBody FtbCultivatePositionJobLearnCourseStateSwitchDTO stateSwitchDTO) { + ftbCultivatePositionService.changeCourseElectiveAndRequiredSwitching(stateSwitchDTO); + return ActionResult.success(); + } + + /** + * 重选课程证书 + * + * @return {@link ActionResult } + */ + @PutMapping("/course-retake-certificate") + public ActionResult courseRetakeCertificate(@Validated @RequestBody FtbJobLearnCourseRetakeCertificateDTO certificateDTO) { + ftbCultivatePositionService.courseRetakeCertificate(certificateDTO); + return ActionResult.success(); + } + + + /** + * 根据用户ids获取用户信息,返回map + * + * @param dto + * @return + */ + @PostMapping("/get-userinfo-for-user-ids") + public ActionResult> getUserInfoForUserIds(@Validated @RequestBody BaseListIdDTO dto) { + // 获取该岗位下所有人 + return ActionResult.success(userApiV2Util.getUserPrimaryBoundBatchReturnList(dto.getIds(), null)); + } + + + /** + * 查询所有的组织、门店、班组信息列表 + * + * @return + */ + @GetMapping("/get-all-org-store-team") + public ActionResult> getAllOrg() { + return ActionResult.success(userApiV2Util.getOrgListByWhere("", new ArrayList<>(), new ArrayList<>(), null)); + } + + + /** + * 获取当前登录的用户信息 + * + * @return {@link ActionResult } + */ + @GetMapping("/get-curr-login-user-info") + public ActionResult getCurrLoginUserInfo() { + return ActionResult.success(userApiV2Util.getUserPrimaryBoundOne(userApiV2Util.getCurrentLoginUserId(), null)); + } + + /** + * 获取当前登录的数据权限类型 + * + * @return 0-全部 1-所在组织和下级组织员工 2-所在组织员工 3-仅下属 4-指定组织 -1-无效权限信息 + */ + @GetMapping("/get-curr-login-user-permission-type") + public ActionResult getCurrLoginUserPermissionType(CultivatePermissionModuleParam dto) { + return ActionResult.success(userApiV2Util.getPermissionModuleType()); + } + + + @GetMapping("/get-curr-login-user-org-list") + public ActionResult> getCurrLoginUserOrgList(CultivatePermissionModuleParam dto) { + List orgListForCurrUser = userApiV2Util.getOrgListForCurrUser( + List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.TEAM),null); + return ActionResult.success(orgListForCurrUser); + } + + /** + * 获取当前登录用户有权限的岗位ID列表 + * + * @return + */ + @GetMapping("/get-curr-login-user-has-permission-position-id") + public InnerPowerPositionVO getCurrLoginUserHasPermissionPositionId(@RequestParam(name = "permissionModule", defaultValue = "") String permissionModule) { + return userApiV2Util.getPositionListForCurrUser(null); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionCopyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionCopyController.java new file mode 100644 index 0000000..9f8c8c3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionCopyController.java @@ -0,0 +1,66 @@ +package jnpf.cultivate.controller.web.position; + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivatePositionCopyService; +import jnpf.model.cultivate.dto.position.web.FtbCultivateAssociatedExamsCoursesDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionCopyDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionPassExaminationDTO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionCopyVO; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web岗位学习复制模块 + * + * @author wangchunxiang + * @date 2024/07/22 + */ +@RestController +@RequestMapping("/web/position") +public class FtbCultivatePositionCopyController { + + @Resource + private FtbCultivatePositionCopyService ftbCultivatePositionCopyService; + + /** + * 岗位学习复制 + */ + @PostMapping("/copy-position") + public ActionResult copyPosition(@Validated @RequestBody FtbCultivatePositionCopyDTO data) { + FtbCultivatePositionCopyVO ftbCultivatePositionCopyVO = ftbCultivatePositionCopyService.copyPosition(data); + return ActionResult.success(ftbCultivatePositionCopyVO); + } + + /** + * 岗位考试合格后才能进行鉴定修改 + */ + @PutMapping("/passing-examination-revised") + public ActionResult passingTheJobExaminationRevised(@Validated @RequestBody FtbCultivatePositionPassExaminationDTO data) { + ftbCultivatePositionCopyService.passingTheJobExaminationRevised(data); + return ActionResult.success(); + } + + /** + * 复制岗位关联考试和课程 + */ + @PostMapping + public ActionResult associatedExamsAndCourses(@Validated @RequestBody FtbCultivateAssociatedExamsCoursesDTO data) { + ftbCultivatePositionCopyService.associatedExamsAndCourses(data); + return ActionResult.success(); + } + + /** + * 已存在的岗位学习,返回岗位id + * + * @return {@link ActionResult } + */ + @GetMapping("/learning-from-existing-positions") + public ActionResult> learningFromExistingPositions() { + List result = ftbCultivatePositionCopyService.learningFromExistingPositions(); + return ActionResult.success(result); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionMemberController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionMemberController.java new file mode 100644 index 0000000..1769aa9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionMemberController.java @@ -0,0 +1,84 @@ +package jnpf.cultivate.controller.web.position; + +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePositionMemberService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.resp.AppExamListVo; +import jnpf.model.cultivate.vo.identify.UserIdentifyPageVo; +import jnpf.model.cultivate.vo.position.FtbCultivateCourseListVO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionUserInfoVo; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * @Title: web岗位学习成员培训进展 + * @Author: peng.hao + * @create: 2023/12/2819:18 + */ +@RestController +@RequestMapping("/position-member") +public class FtbCultivatePositionMemberController { + + @Resource + FtbCultivatePositionMemberService service; + + /** + * 成员列表 + * @return + */ + @Operation(summary = "成员列表") + @PostMapping("/listUser") + public ActionResult> listUser(@Valid @RequestBody QueryPageUserDTO dto) { + PageListVO listVO = service.listUser(dto); + return ActionResult.success(listVO); + } + /** + * 课程列表 + * @param userId + * @param page + * @return + */ + @Operation(summary = "课程列表") + @GetMapping("/listCourse") + public ActionResult> listCourse(String userId ,CultivatePage page) { + PageListVO listVO =service.listCourse(userId,page); + return ActionResult.success(listVO); + } + /** + * 考试列表 + * @param userId 用户ID + * @param page + * @return + */ + @Operation(summary = "考试列表") + @GetMapping("/list_exam_user") + public ActionResult> listExam(String userId, CultivatePage page) { + if (StringUtil.isEmpty(userId)) { + throw new RuntimeException("用户ID不能为空"); + } + PageInfo pageVo = service.listExam(userId, page); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 鉴定列表 + * @param userId + * @param page + * @return + */ + @Operation(summary = "鉴定列表") + @GetMapping("/listIdentify") + public ActionResult> listIdentify(String userId , CultivatePage page) { + PageListVO listVO = service.listIdentify(userId,page); + return ActionResult.success(listVO); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionStatisticesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionStatisticesController.java new file mode 100644 index 0000000..c236e9f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/position/FtbCultivatePositionStatisticesController.java @@ -0,0 +1,187 @@ +package jnpf.cultivate.controller.web.position; + + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePositionStatisticesService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.position.FtbCultivatePersonStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionPersonStatisticesDTO; +import jnpf.model.cultivate.vo.position.*; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.EasyExcelUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web岗位学习统计 + * + * @author fantaibao + * @date 2024/03/22 + */ +@RequestMapping("/web/position/statistices") +@RestController +public class FtbCultivatePositionStatisticesController { + + @Resource + private FtbCultivatePositionStatisticesService ftbCultivatePositionStatisticesService; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 组织维度统计 + * + * @param statisticDTO + * @return + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbCultivatePositionOrgStatisticesDTO statisticDTO) { + PageListVO listVO = ftbCultivatePositionStatisticesService.organizationListStatistics(statisticDTO); + return ActionResult.success(listVO); + } + + /** + * 组织维度统计-导出 + * + * @param statisticDTO + */ + @PostMapping("/org-list-statistics/export") + public void exportInformationorganization(@RequestBody FtbCultivatePositionOrgStatisticesDTO statisticDTO, HttpServletResponse response) throws IOException { + statisticDTO.setPageSize(9999999); + PageListVO listVO = ftbCultivatePositionStatisticesService.organizationListStatistics(statisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "组织维度统计", FtbCultivatePositionOrgStatisticesVO.class, response); + } + + /** + * 个人维度统计 + * + * @param ftbCultivatePositionPersonStatisticesDTO + */ + @PostMapping("/person-list-statistics") + public ActionResult> personListStatistics( + @RequestBody FtbCultivatePositionPersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO) { + PageListVO listVO = ftbCultivatePositionStatisticesService.personListStatistics(ftbCultivatePositionPersonStatisticesDTO); + return ActionResult.success(listVO); + } + + /** + * 个人维度统计导出信息 + * + * @param personWisdomStatisticDTO + */ + @PostMapping("/person-list-statistics/export") + public void exportInformationPerson(@RequestBody FtbCultivatePositionPersonStatisticesDTO personWisdomStatisticDTO, HttpServletResponse response) throws IOException { + PageListVO listVO = ftbCultivatePositionStatisticesService.personListStatistics(personWisdomStatisticDTO); + List statisticesExportVOS = listVO.getList() + .stream() + .map(FtbCultivatePositionPersonStatisticesExportVO::convert) + .collect(Collectors.toList()); + EasyExcelUtil.simpleWrite(statisticesExportVOS, "个人维度统计", FtbCultivatePositionPersonStatisticesExportVO.class, response); + } + + /** + * 岗位维度统计 + * + * @param ftbCultivatePositionPersonStatisticesDTO + */ + @PostMapping("/position-list-statistics") + public ActionResult> positionListStatistics( + @RequestBody FtbCultivatePersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO) { + PageListVO listVO = ftbCultivatePositionStatisticesService.positionListStatistics(ftbCultivatePositionPersonStatisticesDTO); + return ActionResult.success(listVO); + } + + /** + * 岗位统计导出信息 + * + * @param personWisdomStatisticDTO + */ + @PostMapping("/position-list-statistics/export") + public void exportInformationPerson(@RequestBody FtbCultivatePersonStatisticesDTO personWisdomStatisticDTO, HttpServletResponse response) throws IOException { + personWisdomStatisticDTO.setPageSize(-1); + PageListVO listVO = ftbCultivatePositionStatisticesService.positionListStatistics(personWisdomStatisticDTO); + List statisticesExportVOS = listVO.getList() + .stream() + .map(FtbCultivatePersonStatisticesExportVO::convert) + .collect(Collectors.toList()); + EasyExcelUtil.simpleWrite(statisticesExportVOS, "职等维度统计", FtbCultivatePersonStatisticesExportVO.class, response); + } + + + /** + * 组织维度-课程详情 + * + * @param startDate 开始日期格式yyyy-mm-dd hh:mm:ss + * @param endDate 结束日期格式yyyy-mm-dd hh:mm:ss + * @param orgId 组织id主键(必传) + * @return {@link ActionResult}<{@link List}<{@link OrganizeCourseDetails}>> + */ + @GetMapping("/organize-course-details") + public ActionResult> organizeCourseDetails(String startDate, String endDate, String orgId) { + List listVO = ftbCultivatePositionStatisticesService.organizeCourseDetails(startDate, endDate, orgId); + return ActionResult.success(listVO); + } + + /** + * 个人维度-课程详情 + * + * @param startDate 开始日期格式yyyy-mm-dd hh:mm:ss + * @param endDate 结束日期格式yyyy-mm-dd hh:mm:ss + * @param userId 用户id主键(必传) + * @return {@link ActionResult}<{@link List}<{@link OrganizeCourseDetails}>> + */ + @GetMapping("/personal-dimension-course-details") + public ActionResult> personalDimensionCourseDetails(String startDate, String endDate, String userId) { + List listVO = ftbCultivatePositionStatisticesService.personalDimensionCourseDetails(startDate, endDate, userId); + return ActionResult.success(listVO); + } + + /** + * 个人信息-所属组织查询 + * + * @param userId 用户主键id + * @return {@link ActionResult} + */ + @GetMapping("/personal-organizational-information") + public ActionResult> personalInformation(String userId) { + Map> userPrimaryBoundBatchCompatible = userApiV2Util.getUserPrimaryBoundBatchCompatible(List.of(userId),null); + List userPrimaryBoundVOS = userPrimaryBoundBatchCompatible.get(userId); + if (CollUtil.isNotEmpty(userPrimaryBoundVOS)) { + List result = userPrimaryBoundVOS.stream().map(userBoundMoreInfoVO -> { + PersonalDimensionCourseDetails.PositionInformation positionInformation = new PersonalDimensionCourseDetails.PositionInformation(); + positionInformation.setNameOfAssociation(userBoundMoreInfoVO.getOrganizeName()); + positionInformation.setPositionName(userBoundMoreInfoVO.getPositionName()); + positionInformation.setPostGradeName(userBoundMoreInfoVO.getGradeName()); + positionInformation.setOrganizationId(userBoundMoreInfoVO.getOrganizeId()); + return positionInformation; + }).collect(Collectors.toList()); + return ActionResult.success(result); + } + return ActionResult.success(new ArrayList<>()); + } + + /** + * 岗位维度-点击岗位名称数据统计 + * + * @param postId 岗位主键ID(必传) + * @return {@link ActionResult}<{@link JobTitleStatistics}> + */ + @GetMapping("/job-title-statistics") + public ActionResult jobTitleStatistics(String postId) { + return ActionResult.success(ftbCultivatePositionStatisticesService.jobTitleStatistics(postId)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivateMapsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivateMapsController.java new file mode 100644 index 0000000..7300a45 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivateMapsController.java @@ -0,0 +1,71 @@ +package jnpf.cultivate.controller.web.promotion; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePromotionService; +import jnpf.model.cultivate.dto.promotion.FtbMapsOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.promotion.FtbMapsPersonWisdomStatisticDTO; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMapsOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMapsPersonWisdomStatisticVO; +import jnpf.util.EasyExcelUtil; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * web学习地图统计 + * @author hao.peng + */ +@RestController +@RequestMapping("/cul_pro/statistic") +public class FtbCultivateMapsController { + + @Resource + FtbCultivatePromotionService service; + + /** + * 组织维度统计 + * @param statisticDTO + * @return + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbMapsOrgWisdomStatisticDTO statisticDTO){ + PageListVO listVO = service.organizationListStatistics(statisticDTO); + return ActionResult.success(listVO); + } + /** + * 组织维度统计 导出 + * @param statisticDTO + */ + @PostMapping("/org-list-statistics/export") + public void exportInformationorganization(@RequestBody FtbMapsOrgWisdomStatisticDTO statisticDTO, HttpServletResponse response) throws IOException { + PageListVO listVO = service.organizationListStatistics(statisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "组织维度统计", FtbCultivateMapsOrgWisdomStatisticVO.class, response); + } + /** + * 个人维度统计 + * @param personWisdomStatisticDTO + * @return + */ + @PostMapping("/person-list-statistics") + public ActionResult> personListStatistics(@RequestBody FtbMapsPersonWisdomStatisticDTO personWisdomStatisticDTO){ + PageListVO listVO = service.personListStatistics(personWisdomStatisticDTO); + return ActionResult.success(listVO); + } + + /** + * 个人维度统计导出信息 + * @param personWisdomStatisticDTO + */ + @PostMapping("/person-list-statistics/export") + public void exportInformationPerson(@RequestBody FtbMapsPersonWisdomStatisticDTO personWisdomStatisticDTO, HttpServletResponse response) throws IOException { + PageListVO listVO = service.personListStatistics(personWisdomStatisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "个人维度统计", FtbCultivateMapsPersonWisdomStatisticVO.class, response); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionController.java new file mode 100644 index 0000000..80c59eb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionController.java @@ -0,0 +1,200 @@ +package jnpf.cultivate.controller.web.promotion; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.collection.CollUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.cultivate.service.FtbCultivatePromotionService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionCreatDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionMemberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionPostVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * web晋升通道管理 + * @author hao.peng + */ +@RestController +@RequestMapping("/cul_pro") +@Slf4j +public class FtbCultivatePromotionController { + /** + * 服务对象 + */ + @Resource + private FtbCultivatePromotionService promotionService; + + @Resource + private ConfigValueUtil configValueUtil; + + @Resource + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + /** + * 获取晋升通道管理列表 + * @param page 分页参数 + * @return + */ + @Operation(summary = "获取晋升通道管理列表") + @GetMapping("/list") + public ActionResult> list(FtbCultivatePromotionDto dto, CultivatePage page) { + PageListVO listVo = promotionService.getList(page,dto); + return ActionResult.success(listVo); + } + + /** + * 校验该岗位是否岗位通道和岗位学习是否有值 + * @param postId 岗位id + * @param gradeId 职等id + * @return + */ + @Operation(summary = "校验该岗位是否岗位通道和岗位学习是否有值") + @GetMapping("/VerifyPromotion") + public ActionResult VerifyPromotion(@RequestParam(required = false,name = "postId") String postId, + @RequestParam(required = false,name = "gradeId") String gradeId) { + //校验办理入职列表是否有用户使用 + staffEmploymentApplyService.verifyUserBound(postId,gradeId); + + promotionService.VerifyPromotion(postId,gradeId); + return ActionResult.success(); + } + + /** + * 新增晋升通道 + * @param creatDto + * @return + */ + @Operation(summary = "新增晋升通道") + @PostMapping("/save") + public ActionResult save(@Validated @RequestBody FtbCultivatePromotionCreatDto creatDto) { + promotionService.add(creatDto); + return ActionResult.success("新增成功"); + + } + /** + * 批量新增晋升通道 + * @param creatDto + * @return + */ + @Operation(summary = "批量新增晋升通道") + @PostMapping("/saveList") + public ActionResult saveList(@Validated @RequestBody List creatDto) { + promotionService.addList(creatDto); + return ActionResult.success("新增成功"); + + } + + /** + * 修改晋升通道 + * @param creatDto + * @return + */ + @Operation(summary = "修改晋升通道") + @PutMapping("/update") + public ActionResult update(@RequestBody FtbCultivatePromotionCreatDto creatDto) { + promotionService.updatePromotion(creatDto); + return ActionResult.success("修改成功"); + } + + /** + * 删除是需要进行成员列表获取 + * 获取启用该晋升通道的成员列表 + * @param page 分页参数 + * @return + */ + @Operation(summary = "删除是需要进行成员列表获取,获取启用该晋升通道的成员列表") + @GetMapping("/getPromotionMbeList") + public ActionResult> getPromotionMbeList(FtbCultivatePromotionDto dto, + CultivatePage page ) { + PageListVO listVo =promotionService.getPromotionMbeList(dto,page); + return ActionResult.success(listVo); + } + + /** + * 删除晋升通道 + * @param id 主键ID + * @param isTwoDelete 是否执行2次删除操作。如果`isTwoDelete`为`0`,则进行一次删除操作;如果为`1`,则执行2次删除操作。 + * @return 删除结果 + */ + @Operation(summary = "删除晋升通道") + @DeleteMapping("/{id}/{isTwoDelete}") + @Parameter(name = "id", description = "主键ID", required = true) + public ActionResult delete(@PathVariable("id") String id, + @PathVariable("isTwoDelete") int isTwoDelete) { + boolean isDeleted = promotionService.deleteById(id, isTwoDelete == 1); + if (!isDeleted) { + return ActionResult.success(false); + } + return ActionResult.success(true); + } + + /** + * 导出晋升通道数据 + * @param response + * @param tenantId + * @throws IOException + */ + @GetMapping("/exportPromotionChannelData") + @SaIgnore + @NoDataSourceBind + public void exportPromotionChannelData(HttpServletResponse response, String tenantId) throws IOException { + if (log.isDebugEnabled()) { + log.debug("closeUserAccountRegularlyAfterResignation params :{}", tenantId); + } + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + CultivatePage page =new CultivatePage(); + page.setPageSize(100000); + page.setCurrentPage(1); + PageListVO listVo = promotionService.getList(page,new FtbCultivatePromotionDto()); + List list = listVo.getList(); + if (CollUtil.isNotEmpty(list)){ + list.forEach(item->{ + List postChannelLevel = item.getPostChannelLevel(); + String collect = postChannelLevel.stream().map(FtbCultivatePromotionPostVO::getPostName).collect(Collectors.joining("<")); + item.setPostChannelLevelName(collect); + }); + List collect = list.stream().filter(item->StringUtil.isNotEmpty(item.getChannelIniName())).sorted(Comparator.comparing(FtbCultivatePromotionVO::getChannelIniName)).collect(Collectors.toList()); + listVo.setList(collect); + } + EasyExcelUtils.exportExcel(response,tenantId+"晋升通道数据详情",list,FtbCultivatePromotionVO.class); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionMemberController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionMemberController.java new file mode 100644 index 0000000..b7269f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionMemberController.java @@ -0,0 +1,95 @@ +package jnpf.cultivate.controller.web.promotion; + + +import io.swagger.v3.oas.annotations.Parameter; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivatePromotionMemberService; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionMemberDto; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMeberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.permission.model.userrelation.UserRelationPositionGrades; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web晋升通道成员管理模块 + * @author penghao + */ +@RestController +@RequestMapping("/cul_pro_member") +public class FtbCultivatePromotionMemberController { + /** + * 服务对象 + */ + @Resource + public FtbCultivatePromotionMemberService promotionMemberService; + + + /** + * 根据当前晋升通道id查询通道成员(包含未启用的通道成员,已启用的) + * @param id + * @return + */ + @GetMapping("/{id}") + @Parameter(name = "id", description = "通道id主键", required = true) + // @SaCheckPermission("promotionChannelMgrs::btn_channel_member") + public ActionResult queryChannelMembers(@PathVariable("id") String id){ + FtbCultivateMeberVO meberVO = promotionMemberService.queryChannelMembers(id); + return ActionResult.success(meberVO); + } + + /** + * 拖动添加成员启用对应晋升通道 + * @param dto 用户id + * @return + */ + @PostMapping("/addMembersToProChannel") + public ActionResult addMembersToProChannel(@RequestBody FtbCultivatePromotionMemberDto dto){ + promotionMemberService.addMembersToProChannel(dto); + return ActionResult.success(); + } + /** + * 拖动删除成员启用对应晋升通道 + * @param userIds 用户id + * @return + */ + @PutMapping("/deleteMembersToProChannel") + public ActionResult deleteMembersToProChannel(@RequestBody List userIds){ + promotionMemberService.deleteMembersToProChannel(userIds); + return ActionResult.success(); + } + + /** + * 更改组织信息删除成员启用对应晋升通道和岗位学习对应职等开启的课程 + * @param organizeId + * @param userRelationPositionGrades + * @return + */ + @PutMapping("/deleteRelation/{organizeId}") + public ActionResult deleteRelation(@PathVariable("organizeId") String organizeId, + @RequestBody UserRelationPositionGrades userRelationPositionGrades){ + promotionMemberService.deleteRelation(organizeId,userRelationPositionGrades); + return ActionResult.success(); + } + + /** + * 根据用户ID 或者岗位id 获取对应的晋升通道。 + * 如果用户不存在或相关数据有问题,将返回失败的ActionResult。 + * + * @param userId 用户ID + * @return 用户ID对应的晋升通道列表 + */ + @GetMapping("/userId") + public ActionResult getPromotionsByUserId(@RequestParam(value = "userId" ,required = false) String userId, + @RequestParam(value = "postId",required = false) String postId) { + FtbCultivatePromotionVO promotionVO = promotionMemberService.queryPromotionByUser(userId,postId); + // 记录日志或统计数据 + return ActionResult.success(promotionVO); + } + + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionNewController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionNewController.java new file mode 100644 index 0000000..734eaa8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionNewController.java @@ -0,0 +1,146 @@ +package jnpf.cultivate.controller.web.promotion; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionCreatNewDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.vo.promotion.*; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * web 学习地图管理(v1.2) + * @author hao.peng + */ +@RestController +@RequestMapping("/cul-pro-map") +public class FtbCultivatePromotionNewController { + /** + * 服务对象 + */ + @Resource + private FtbCultivatePromotionNewService promotionService; + + /** + * 获取学习地图管理列表 + * @param page 分页参数 + * @return + */ + @GetMapping("/list") + public ActionResult> list(FtbCultivatePromotionDto dto, CultivatePage page) { + PageListVO listVo = promotionService.getList(page,dto); + return ActionResult.success(listVo); + } + + /** + * 新增学习地图 + * @param creatDto + * @return + */ + @PostMapping("/save") + public ActionResult save(@Validated @RequestBody FtbCultivatePromotionCreatNewDto creatDto) { + promotionService.add(creatDto); + return ActionResult.success("新增成功"); + + } + + /** + * 修改学习地图 + * @param creatDto + * @return + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FtbCultivatePromotionCreatNewDto creatDto) { + promotionService.updatePromotion(creatDto); + return ActionResult.success("修改成功"); + } + + /** + * 获取学习地图详情 + * @param id + * @return + */ + @GetMapping("/getDetails/{id}") + public ActionResult getDetails(@PathVariable("id") String id) { + FtbCultivateStudyMemberVO memberVO = promotionService.getDetails(id); + return ActionResult.success(memberVO); + } + + + /** + * 删除学习地图 + * @param id 主键ID + * @param isTwoDelete 是否执行2次删除操作。 + * 如果`isTwoDelete`为`0`, + * 则进行一次删除操作; + * 如果为`1`,则执行2次删除操作。 + * @return 删除结果 "1"表示删除失败需要弹窗展示数据,空 表示删除成功。 + */ + @Operation(summary = "删除学习地图") + @DeleteMapping("/{id}/{isTwoDelete}") + public ActionResult delete(@PathVariable("id") String id, + @PathVariable("isTwoDelete") int isTwoDelete) { + String isDeleted = promotionService.deleteById(id, isTwoDelete == 1); + return ActionResult.success(isDeleted); + } + + /** + * 根据岗位id获取地图层级 + * @param postId + * @return + */ + @GetMapping("/getLearnMapLevels") + public ActionResult getMapLevel(@RequestParam("postId")String postId){ + FtbCultivatePromotionLevelMapVO learnMapLevels = promotionService.getLearnMapLevels(postId); + return ActionResult.success(learnMapLevels); + } + + /** + * 删除时二次校验 获取当前地图绑定成员信息 + * @param id 地图主键id + * @return + */ + @GetMapping("/getTheCurrentMapBindingMembers") + public ActionResult getTheCurrentMapBindingMembers(@RequestParam("id")String id){ + FtbCultivatePromotionStudyDeleteInfo resultList = promotionService.getTheCurrentMapBindingMembers(id); + return ActionResult.success(resultList); + } + + /** + * 根据地图主键id 获取地图成员信息 + * @param id + * @return + */ + @GetMapping("/getPromotionById") + public ActionResult getPromotionById(@RequestParam("id")String id,CultivatePage page){ + FtbCultivateStudyMemberInfo promotionNewVO = promotionService.getPromotionById(id,page); + return ActionResult.success(promotionNewVO); + } + + /** + * 校验岗位是否再学习地图 (岗位删除限制) + * @param postId + * @return false 不存在 ture 存在 + */ + @GetMapping("/check-post-have-map") + public ActionResult verifyingThePostWillRelearnTheMap(@RequestParam("postId") String postId){ + return ActionResult.success(promotionService.verifyingThePostWillRelearnTheMap(postId)); + } + + /** + * 根据人查询学习地图详情 + * @param userId + * @param postId + * @return + */ + @GetMapping("/queryMapLearningStatusBasedOnPeople") + public ActionResult queryMapLearningStatusBasedOnPeople(String userId,String postId){ + FtbCultivatePromotionWithPersonelVO personelVO = promotionService.queryTrainData(userId, postId); + return ActionResult.success(personelVO); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionPostController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionPostController.java new file mode 100644 index 0000000..bac9d93 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/promotion/FtbCultivatePromotionPostController.java @@ -0,0 +1,83 @@ +package jnpf.cultivate.controller.web.promotion; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivatePromotionPostService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPost; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web晋升通道级别模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/cul_pro_post") +public class FtbCultivatePromotionPostController { + /** + * 服务对象 + */ + @Resource + private FtbCultivatePromotionPostService postService; + + + /** + * 晋升通道级别列表查询 + * @param promotionId 晋升通道id + * @return + */ + @Operation(summary = "晋升通道级别列表查询") + @GetMapping("/list") + public ActionResult> list(@RequestParam("promotionId") String promotionId ) { + + List list = postService.getList(promotionId); + return ActionResult.success(list); + } + + + /** + * 新增晋升通道级别 + * @param creatDto + * @return + */ + @Operation(summary = "新增晋升通道级别") + @PostMapping("/save") + public ActionResult save(@RequestBody FtbCultivatePromotionPost creatDto) { + postService.savePost(creatDto); + return ActionResult.success("新增成功"); + + } + + + /** + * 修改晋升通道级别 + * @param dto + * @return + */ + @Operation(summary = "修改晋升通道级别") + @PutMapping("/update") + public ActionResult update(@RequestBody FtbCultivatePromotionPost dto) { + postService.updatePost(dto); + return ActionResult.success("修改成功"); + } + + /** + * 删除岗位级别 + * @param id 主键id + * @return + * + */ + @Operation(summary = "删除岗位级别") + @DeleteMapping("/{id}") + @Parameter(name = "id", description = "主键", required = true) + public ActionResult delete(@PathVariable("id") String id) { + if (postService.removeById(id)) { + return ActionResult.success("删除成功"); + } + return ActionResult.fail("删除失败"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateAssessmentPointsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateAssessmentPointsController.java new file mode 100644 index 0000000..a098792 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateAssessmentPointsController.java @@ -0,0 +1,139 @@ +package jnpf.cultivate.controller.web.question; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateAssessmentPointsService; +import jnpf.model.cultivate.po.FtbCultivateAssessmentPoints; +import jnpf.model.cultivate.req.exam.AssessmentPointsReq; +import jnpf.model.cultivate.req.exam.QueryAssessmentPointsReq; +import jnpf.model.cultivate.resp.AssessmentPointsVo; +import jnpf.model.enums.CourseEnums; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Date; +import java.util.List; + +/** + * web题目考核点模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/assessment/points") +public class FtbCultivateAssessmentPointsController { + /** + * 服务对象 + */ + @Resource + private FtbCultivateAssessmentPointsService assessmentPointsService; + + + /** + * 查询考点列表 + * + * @return + */ + @PostMapping("/lists") + public ActionResult> lists(@RequestBody QueryAssessmentPointsReq req) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(StringUtils.isNotEmpty(req.getKeyword()), FtbCultivateAssessmentPoints::getName, req.getKeyword()) + .eq(FtbCultivateAssessmentPoints::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List assessmentPointsList = assessmentPointsService.list(wrapper); + if (CollectionUtil.isEmpty(assessmentPointsList)) { + return ActionResult.success(CollectionUtil.newArrayList()); + } + return ActionResult.success(BeanUtil.copyToList(assessmentPointsList, AssessmentPointsVo.class)); + } + + + /** + * 添加考点 + * + * @param req + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid AssessmentPointsReq req) { + //1、检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateAssessmentPoints::getName, req.getName()) + .eq(FtbCultivateAssessmentPoints::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = assessmentPointsService.count(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("考点名称重复"); + } + + //2、保存数据 + FtbCultivateAssessmentPoints assessmentPoints = new FtbCultivateAssessmentPoints(); + assessmentPoints.setName(req.getName()); + assessmentPoints.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + assessmentPoints.setCreatorTime(new Date()); + assessmentPointsService.save(assessmentPoints); + return ActionResult.success(true); + } + + /** + * 编辑考点 + * + * @param req + * @return + */ + @PostMapping("/edit") + public ActionResult edit(@RequestBody @Valid AssessmentPointsReq req) { + + //1、检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateAssessmentPoints::getName, req.getName()) + .ne(FtbCultivateAssessmentPoints::getId, req.getId()) + .eq(FtbCultivateAssessmentPoints::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = assessmentPointsService.count(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("考点名称重复"); + } + //2、写入数据 + FtbCultivateAssessmentPoints assessmentPoints = new FtbCultivateAssessmentPoints(); + assessmentPoints.setName(req.getName()); + assessmentPoints.setId(req.getId()); + assessmentPointsService.updateById(assessmentPoints); + return ActionResult.success(true); + } + + /** + * 删除考点 + * + * @param id 考点id + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + + //1、检测考点 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateAssessmentPoints::getId, id) + .eq(FtbCultivateAssessmentPoints::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCultivateAssessmentPoints points = assessmentPointsService.getOne(wrapper); + if (null == points) { + throw new RuntimeException("考点不存在"); + } + if (points.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("考点已经被删除"); + } + //2、删除 + FtbCultivateAssessmentPoints assessmentPoints = new FtbCultivateAssessmentPoints(); + assessmentPoints.setId(id); + assessmentPoints.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + assessmentPointsService.updateById(assessmentPoints); + return ActionResult.success(true); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionBankController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionBankController.java new file mode 100644 index 0000000..f5db9a8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionBankController.java @@ -0,0 +1,163 @@ +package jnpf.cultivate.controller.web.question; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateQuestionBankService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.req.questionbank.AddQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.QueryQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.UnbindQuestionBankReq; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.resp.QuestionBankVo; +import jnpf.model.enums.CourseEnums; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * web题库模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/questionbank") +public class FtbCultivateQuestionBankController { + + /** + * 服务对象 + */ + @Resource + private FtbCultivateQuestionBankService questionBankService; + + + /** + * 分页列出题库列表 + * + * @param req + * @return + */ + @GetMapping("/pageLists") + public ActionResult> pageLists(QueryQuestionBankReq req) { + + PageInfo pageVo = questionBankService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 添加题库 + * + * @param addQuestionBank + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid AddQuestionBankReq addQuestionBank) { + + //1、检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestionBank::getBankContent, addQuestionBank.getBankContent()) + .eq(FtbCultivateQuestionBank::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = questionBankService.count(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("题库已经存在"); + } + //2、新增 + questionBankService.insertData(addQuestionBank); + return ActionResult.success(true); + } + + /** + * 编辑题库 + * + * @param req + * @return + */ + @PostMapping("/edit") + public ActionResult edit(@RequestBody @Valid EditQuestionBankReq req) { + //1、检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestionBank::getBankContent, req.getBankContent()) + .ne(FtbCultivateQuestionBank::getId, req.getId()) + .eq(FtbCultivateQuestionBank::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = questionBankService.count(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("题库名称重复"); + } + //2修改 + questionBankService.updateData(req); + return ActionResult.success(true); + } + + + /** + * 查询题库详情,包括题库关联的课程 + * + * @param id 题库ID + * @return + */ + @GetMapping("/get/{id}") + public ActionResult queryInfo(@PathVariable("id") String id) { + return ActionResult.success(questionBankService.getInfo(id)); + } + + /** + * 删除题库 + * + * @param id + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + //1、删除题库 + questionBankService.deleteData(id); + return ActionResult.success(true); + } + + + /** + * 检测题库是否可以删除 + * + * @param id 题库ID + * @return + */ + @GetMapping("/check-can-del/{id}") + public ActionResult checkCanDel(@PathVariable("id") String id) { + return ActionResult.success(questionBankService.checkQuestionBankCanDelete(id)); + } + + /** + * 题库和课程解绑 + * + * @param req + * @return + */ + @PostMapping("/unbind") + public ActionResult unbind(@RequestBody @Valid UnbindQuestionBankReq req) { + questionBankService.unbind(req); + return ActionResult.success(true); + } + + + /** + * 查询题库中题目类型分析数据,当选题、多选题多少数据 + * + * @param id 题库ID + * @return Map 题目类型->数量 + */ + @GetMapping("/analys/question-count/{id}") + public ActionResult> analysQuestionCount(@PathVariable("id") String id) { + return ActionResult.success(questionBankService.analysQuestionCount(id)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionController.java new file mode 100644 index 0000000..8b89da9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/question/FtbCultivateQuestionController.java @@ -0,0 +1,525 @@ +package jnpf.cultivate.controller.web.question; + +import cn.afterturn.easypoi.excel.ExcelImportUtil; +import cn.afterturn.easypoi.excel.entity.ImportParams; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.DownloadVO; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateQuestionBankService; +import jnpf.cultivate.service.FtbCultivateQuestionService; +import jnpf.cultivate.utils.QuestionExcelExportUtil; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; +import jnpf.model.cultivate.req.paper.PreQuestionImportReq; +import jnpf.model.cultivate.req.questionbank.AddQuestionReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionReq; +import jnpf.model.cultivate.req.questionbank.QueryQuestionReq; +import jnpf.model.cultivate.req.questionbank.QuestionOptionReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.CourseEnums; +import jnpf.model.personnels.req.employment.BatchByPrimaryIdReq; +import jnpf.util.FtbUtil; +import jnpf.util.RedisUtil; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * web题目模块 + * + * @author xxxxx + */ +@Slf4j +@RestController +@RequestMapping("/question") +public class FtbCultivateQuestionController { + + /** + * 题目服务对象 + */ + @Autowired + private FtbCultivateQuestionService questionService; + @Autowired + private FtbCultivateQuestionBankService questionBankService; + + @Autowired + private RedisUtil redisUtil; + + /** + * 分页列出题库中题目列表【根据题库ID】 + * + * @param req + * @return + */ + @GetMapping("/pageLists") + public ActionResult> pageLists(QueryQuestionReq req) { + + PageInfo pageVo = questionService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 分页列出重复题目 + * + * @param req + * @return + */ + @GetMapping("/queryRepeatQuestion") + public ActionResult> queryRepeatQuestion(@Valid QueryQuestionReq req) { + PageInfo pageVo = questionService.queryRepeatQuestion(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 添加题目 + * + * @param id 题库ID + * @param addQuestion + * @return + */ + @PostMapping("/add/{id}") +// @SaCheckPermission(QUESTION_PERMISSION_PRE + "btn_question_add") + public ActionResult addQuestion(@PathVariable("id") String id, @RequestBody @Valid AddQuestionReq addQuestion) { + QuestionExcelExportUtil.checkAddQuestionParam(addQuestion); + questionService.insertData(id, addQuestion); + return ActionResult.success(true); + } + + /** + * 添加题目【兼容没有创建题库导致正确的添加地址匹配错误,而没有给用户正确提示】兼容【/add/{id}】 + * + * @param addQuestion + * @return + */ + @PostMapping("/add") + public ActionResult addQuestionNoQuestionBank(@RequestBody @Valid AddQuestionReq addQuestion) { + throw new RuntimeException("请先创建题库"); + } + + /** + * 编辑题目 + * + * @param editQuestion + * @return + */ + @PostMapping("/edit") +// @SaCheckPermission(QUESTION_PERMISSION_PRE + "btn_question_edit") + public ActionResult editQuestion(@RequestBody EditQuestionReq editQuestion) { + QuestionExcelExportUtil.checkEditQuestionParam(editQuestion); + questionService.updateData(editQuestion); + return ActionResult.success(true); + } + + + /** + * 查询题目详情,编辑时使用 + * + * @param id 题目ID + * @return + */ + @GetMapping("/get/{id}") +// @SaCheckPermission(QUESTION_PERMISSION_PRE + "btn_question_get") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success(questionService.getInfo(id)); + } + + /** + * 删除题目 + * + * @param id 题目ID + * @return + */ + @DeleteMapping("/del/{id}") +// @SaCheckPermission(QUESTION_PERMISSION_PRE + "btn_question_delete") + public ActionResult del(@PathVariable("id") String id) { + questionService.deleteData(id); + return ActionResult.success(true); + } + + /** + * 批量删除题目 + * + * @param req 题目ID + * @return + */ + @DeleteMapping("/batchDel") + public ActionResult batchDel(@RequestBody @Validated BatchByPrimaryIdReq req) { + if (req == null || CollectionUtil.isEmpty(req.getIds())) { + throw new RuntimeException("请选择批量删除的题目"); + } + questionService.batchDel(req.getIds()); + return ActionResult.success(true); + } + + /** + * 检测题目是否可以删除 + * + * @param id 题目ID + * @return + */ + @GetMapping("/check-can-del/{id}") + public ActionResult checkCanDel(@PathVariable("id") String id) { + return ActionResult.success(questionService.checkQuestionCanDelete(id)); + } + + /** + * 批量检测题目是否可以删除 + * + * @param req 题目ID + * @return + */ + @PostMapping("/batchCheckCanDel") + public ActionResult batchCheckCanDel(@RequestBody @Validated BatchByPrimaryIdReq req) { + if (req == null || CollectionUtil.isEmpty(req.getIds())) { + throw new RuntimeException("请选择批量删除的题目"); + } + return ActionResult.success(questionService.batchCheckCanDel(req.getIds())); + } + + + /** + * 模版下载 + * + * @return + */ + @GetMapping("/download/template") + public ActionResult templateDownload() { + DownloadVO vo = DownloadVO.builder().build(); + try { + vo.setName("题目导入模板.xlsx"); + vo.setUrl("https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/xgl/%E9%A2%98%E7%9B%AE%E6%A8%A1%E6%9D%BF.xlsx"); + } catch (Exception e) { + log.error("下载模板错误:" + e.getMessage()); + } + return ActionResult.success(vo); + } + + + /** + * 统计字符串中相邻下划线视为一个时的下划线总数 + * + * @param str + * @return + */ + public int countUnderscoresConsideringAdjacentAsOne(String str) { + // 使用正则表达式替换连续的下划线为单个下划线 + String normalizedStr = str.replaceAll("_+", "_"); + // 遍历处理后的字符串,计算下划线数量 + int count = 0; + for (int i = 0; i < normalizedStr.length(); i++) { + if (normalizedStr.charAt(i) == '_') { + count++; + } + } + return count; + } + + + /** + * 预检测题目导入接口 + * + * @param preQuestionImportReq + * @param questionBankId 题库ID + * @return + */ + @PostMapping("/preImportData/{questionBankId}") + public ActionResult preImportData(@RequestBody @Validated PreQuestionImportReq preQuestionImportReq, @PathVariable(name = "questionBankId") String questionBankId) { + String rediskey = QuestionExcelExportUtil.qeneralQuestionImportKey(); + redisUtil.remove(rediskey); + ImportParams importParams = new ImportParams(); + importParams.setStartSheetIndex(1); // 读取第二个Sheet,因为索引是从0开始 + List allQuestionList = new ArrayList<>(); + try { + InputStream inputStream = EasyExcelUtils.checkExcelFile(preQuestionImportReq.getFileUrl()); + List> list = ExcelImportUtil.importExcel(inputStream, Map.class, importParams); + log.info("题库导入数据:{}", JSON.toJSONString(list)); + inputStream.close(); + + int line = 2; + for (Map o : list) { + ExcelImportQuestionResultReq addQuestionReq = new ExcelImportQuestionResultReq(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.getByDesc(String.valueOf(o.get("题型"))); + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.getByDesc(String.valueOf(o.get("难度"))); + addQuestionReq.setContent(ObjectUtils.isNotEmpty(o.get("题目")) ? String.valueOf(o.get("题目")) : ""); + addQuestionReq.setAnalysis(ObjectUtils.isNotEmpty(o.get("解析")) ? String.valueOf(o.get("解析")) : ""); + if (null == difficulty || null == type || StringUtils.isEmpty(addQuestionReq.getContent())) { + addQuestionReq.setMsg("题目内容为空"); + addQuestionReq.setSuccess(1); + continue; + } + addQuestionReq.setDifficulty(difficulty.getCode()); + addQuestionReq.setType(type.getCode()); + List optionFormList = new ArrayList<>(); + + Set keySet = o.keySet(); + long sortCode = 0; + for (String key : keySet) { + QuestionOptionReq optionForm = new QuestionOptionReq(); + if (key.contains("选项")) { + String answer = String.valueOf(o.get("正确答案")); + String option = key.substring(2); + if (Objects.isNull(o.get(key))) { + continue; + } + optionForm.setContent(String.valueOf(o.get(key))); + if (!StringUtils.isEmpty(answer)) { + optionForm.setIsRightOption(answer.contains(option) ? 1 : 0); + } + optionForm.setSortCode(sortCode++); + optionFormList.add(optionForm); + } + if (CourseEnums.QuestionType.FILL.getCode().equals(addQuestionReq.getType())) { + optionForm.setRightAnswer(optionForm.getContent()); + } + } + addQuestionReq.setOptionList(optionFormList); + allQuestionList.add(addQuestionReq); + line++; + } + + } catch (Exception e) { + e.printStackTrace(); + log.error("题目与导入失败"); + return ActionResult.fail("文件解析异常,导入失败!"); + } + // + if (CollectionUtil.isEmpty(allQuestionList)) { + throw new RuntimeException("导入题列表不能为空"); + } + QuestionExcelExportUtil.dealFillQuestion(allQuestionList); + PreQuestionImportVO vo = new PreQuestionImportVO(); + vo.setUniqueId(rediskey); + + List normal = new ArrayList<>(); + List error = new ArrayList<>(); + for (ExcelImportQuestionResultReq item : allQuestionList) { + if (item.getSuccess() == 1) {//fail + error.add(item); + } else {//success + normal.add(item); + } + } + vo.setNormal(normal); + vo.setError(error); + vo.setNormalNumber(normal.size()); + vo.setErrorNumber(error.size()); + redisUtil.insert(rediskey, vo, 60 * 60); + return ActionResult.success("成功", vo); + + } + + + /** + * 真正导入题目到题库 + * + * @param questionBankId 题库ID + * @param key 预检测导入时返回的唯一标识(必传),用于导出异常数据 + * @return + */ + @PostMapping("/realImportData/{questionBankId}/{key}") + public ActionResult realImportData(@PathVariable(name = "questionBankId") String questionBankId, @PathVariable(name = "key") String key) { + if (!redisUtil.exists(key)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + PreQuestionImportVO data = JSON.parseObject(redisUtil.getString(key).toString(), PreQuestionImportVO.class); + if(data==null || CollectionUtil.isEmpty(data.getNormal())){ + throw new RuntimeException("无正常题目可以导入"); + } + ImportQuestionResultVo importQuestionResultVo = questionService.realImportData(questionBankId, data.getNormal()); + if(CollectionUtil.isEmpty(importQuestionResultVo.getFailList())){ + String rediskey = QuestionExcelExportUtil.qeneralQuestionImportKey(); + redisUtil.remove(rediskey); + } + return ActionResult.success("导入成功", importQuestionResultVo); + } + + /** + * 异常数据导出 + * + * @param key 预检测导入时返回的唯一标识(必传),用于导出异常数据 + */ + @GetMapping("/exceptionDataExport/{key}") + public void exceptionDataExport(@PathVariable(name = "key") String key, HttpServletResponse response) throws IOException { + if (!redisUtil.exists(key)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + PreQuestionImportVO data = JSON.parseObject(redisUtil.getString(key).toString(), PreQuestionImportVO.class); + String fileName = "异常题目" + DateUtil.format(new Date(), "yyyyMMdd"); + if(data.getErrorNumber()<=0){ + throw new RuntimeException("无异常数据"); + } + List list = QuestionExcelExportUtil.convertErrorQuestionList(data.getError()); + QuestionExcelExportUtil.questionExportExcel(response, fileName, list, ExcelUserQuestionVo.class); + } + + + /** + * 根据题库导出题目 + * + * @param questionBankId + */ + + @GetMapping("/questionExport/{questionBankId}") + public void questionExport(@PathVariable(name = "questionBankId") String questionBankId, HttpServletResponse response) throws Exception { + + FtbCultivateQuestionBank questionBank = questionBankService.getById(questionBankId); + if (null == questionBank) { + throw new RuntimeException("题库不存在"); + } + List userQuestionVoList = questionService.queryAllQuestionByQuestionBankId(questionBankId); + if (CollUtil.isEmpty(userQuestionVoList)) { + throw new Exception("暂无可导出题目"); + } + String fileName = questionBank.getBankContent() + "_题目" + DateUtil.format(new Date(), "yyyyMMdd"); + List list = QuestionExcelExportUtil.convertQuestionList(userQuestionVoList); + QuestionExcelExportUtil.questionExportExcel1(response, fileName, list, ExcelUserQuestionVo.class); + } + + /** + * 题目导入 + * + * @param file + * @param questionBankId 题库ID + * @return + */ + @PostMapping("/importData/{questionBankId}") + public ActionResult importData(MultipartFile file, @PathVariable(name = "questionBankId") String questionBankId) { + ImportParams importParams = new ImportParams(); + importParams.setStartSheetIndex(1); // 读取第二个Sheet,因为索引是从0开始 + List allQuestionList = new ArrayList<>(); + try { + InputStream inputStream = file.getInputStream(); + List> list = ExcelImportUtil.importExcel(inputStream, Map.class, importParams); + log.info("题库导入数据:{}", JSON.toJSONString(list)); + inputStream.close(); + + int line = 2; + for (Map o : list) { + AddQuestionReq addQuestionReq = new AddQuestionReq(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.getByDesc(String.valueOf(o.get("题型"))); + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.getByDesc(String.valueOf(o.get("难度"))); + addQuestionReq.setContent(ObjectUtils.isNotEmpty(o.get("题目")) ? String.valueOf(o.get("题目")) : ""); + addQuestionReq.setAnalysis(ObjectUtils.isNotEmpty(o.get("解析")) ? String.valueOf(o.get("解析")) : ""); + if (null == difficulty || null == type || StringUtils.isEmpty(addQuestionReq.getContent())) { + return ActionResult.fail("导入失败!请检查第" + line + "行数据"); + } + addQuestionReq.setDifficulty(difficulty.getCode()); + addQuestionReq.setType(type.getCode()); + List optionFormList = new ArrayList<>(); + + Set keySet = o.keySet(); + long sortCode = 0; + for (String key : keySet) { + QuestionOptionReq optionForm = new QuestionOptionReq(); + if (key.contains("选项")) { + String answer = String.valueOf(o.get("正确答案")); + String option = key.substring(2); + if (Objects.isNull(o.get(key))) { + continue; + } + optionForm.setContent(String.valueOf(o.get(key))); + if (!StringUtils.isEmpty(answer)) { + optionForm.setIsRightOption(answer.contains(option) ? 1 : 0); + } + optionForm.setSortCode(sortCode++); + optionFormList.add(optionForm); + } + if (CourseEnums.QuestionType.FILL.getCode().equals(addQuestionReq.getType())) { + optionForm.setRightAnswer(optionForm.getContent()); + } + } + addQuestionReq.setOptionList(optionFormList); + allQuestionList.add(addQuestionReq); + line++; + } + + } catch (Exception e) { + log.error("导入题目异常:{}", e); + return ActionResult.fail("参数异常,导入失败!"); + } + // + if (CollectionUtil.isNotEmpty(allQuestionList)) { + dealFillQuestion(allQuestionList); + ImportQuestionResultVo importQuestionResultVo = questionService.importData(questionBankId, allQuestionList); + log.info(importQuestionResultVo.getFailList().toString()); + return ActionResult.success("导入成功", importQuestionResultVo); + } + return ActionResult.fail("导入失败"); + } + + /** + * 检测并填充加入的题目数据 + * @param allQuestionList 请求的题目数据 + */ + private void dealFillQuestion(List allQuestionList) { + for (AddQuestionReq addQuestionReq : allQuestionList) { + String content = addQuestionReq.getContent(); + if (StringUtils.isNotEmpty(content) && CourseEnums.QuestionType.FILL.getCode().equals(addQuestionReq.getType())) { + int count = countUnderscoresConsideringAdjacentAsOne(content); + if (addQuestionReq.getOptionList().size() > count) { + addQuestionReq.setOptionList(addQuestionReq.getOptionList().subList(0, count)); + } else if (addQuestionReq.getOptionList().size() < count) { + long size = addQuestionReq.getOptionList().size(); + for (int i = 0; i < count - size; i++) { + QuestionOptionReq optionReq = new QuestionOptionReq(); + optionReq.setContent(""); + optionReq.setSortCode(size + i); + addQuestionReq.getOptionList().add(optionReq); + } + } + } else if (CourseEnums.QuestionType.MULTI.getCode().equals(addQuestionReq.getType())) { + if (CollectionUtil.isEmpty(addQuestionReq.getOptionList())) { + throw new RuntimeException("题目:" + addQuestionReq.getContent() + ",多选题选项不能为空"); + } + if (addQuestionReq.getOptionList().size() < 2) { + throw new RuntimeException("题目:" + addQuestionReq.getContent() + ",多选题选项不能少于2个"); + } + int num = 0; + for (QuestionOptionReq optionReq : addQuestionReq.getOptionList()) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num < 2) { + throw new RuntimeException("题目:" + addQuestionReq.getContent() + ",多选题正确答案不能小于2个"); + } + + } else if (CourseEnums.QuestionType.SINGLE.getCode().equals(addQuestionReq.getType())) { + if (CollectionUtil.isEmpty(addQuestionReq.getOptionList())) { + throw new RuntimeException("题目:" + addQuestionReq.getContent() + ",单选题选项不能为空"); + } + if (addQuestionReq.getOptionList().size() < 2) { + throw new RuntimeException("题目:" + addQuestionReq.getContent() + ",单选题选项不能少于2个"); + } + int num = 0; + for (QuestionOptionReq optionReq : addQuestionReq.getOptionList()) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num != 1) { + throw new RuntimeException("题目:" + addQuestionReq.getContent() + ",单选题正确答案应该只有1个"); + } + + } + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/rule/FtbCultivateRuleController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/rule/FtbCultivateRuleController.java new file mode 100644 index 0000000..1892b00 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/rule/FtbCultivateRuleController.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.controller.web.rule; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.service.FtbCultivateRuleService; +import jnpf.model.cultivate.dto.rule.FtbCultivateRuleDto; +import jnpf.model.cultivate.po.FtbCultivateRule; +import jnpf.model.cultivate.vo.rule.FtbCultivateRuleVO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * web规则配置 + * @Author: peng.hao + * @create: 2024/9/9:15:23 + */ +@RestController +@Slf4j +@RequestMapping("/web/cultivate-rule") +public class FtbCultivateRuleController { + + @Resource + private FtbCultivateRuleService ftbCultivateRuleService; + + /** + * 获取规则列表 + * @return + */ + @RequestMapping("/getRuleList") + public ActionResult getRuleList(){ + FtbCultivateRule entity = ftbCultivateRuleService.lambdaQuery().eq(FtbCultivateRule::getAttachments, 0).one(); + return ActionResult.success(FtbCultivateRuleVO.covert(entity)); + } + + /** + * 开关附件下载规则 + * @param cultivateRuleDto + * @return + */ + @PutMapping("/updateRuleInfo") + public ActionResult updateRuleInfo(@RequestBody FtbCultivateRuleDto cultivateRuleDto){ + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateRule::getAttachmentsRule, cultivateRuleDto.getAttachmentsRule()); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId,cultivateRuleDto.getId()); + ftbCultivateRuleService.update(updateWrapper); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStatisticsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStatisticsController.java new file mode 100644 index 0000000..9431ba7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStatisticsController.java @@ -0,0 +1,163 @@ +package jnpf.cultivate.controller.web.statistics; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivateStatisticsService; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.resp.ExamStatisticsVo; +import jnpf.model.cultivate.vo.identify.IdentifyStatisticsVo; +import jnpf.model.cultivate.vo.statistics.FtbCultivateStatisticsVO; +import jnpf.model.cultivate.vo.statistics.NumberOfTrainingSessions; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * web数据看板 + * + * @Author:peng.hao + * @create: 2023/12/2915:03 + */ +@Slf4j +@RestController +@RequestMapping("/cultivate-statistics") +public class FtbCultivateStatisticsController { + + @Resource + private FtbCultivateStatisticsService ftbCultivateStatisticsService; + + @Resource + private FtbCultivateExamUserService examUserService; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + private CultivateIdentifyApplyService applyService; + + /** + * web 全局概览【顶部数据】 + * + * @param dto 统计数据DTO对象 + * @return 受训学员统计数据结果ActionResult对象 + */ + @GetMapping("/getNumberOfTrainees") + public ActionResult getNumberOfTraineesStatistics(FtbCultivateStatisticsDTO dto) { + getUserList(dto); + NumberOfTrainingSessions number = ftbCultivateStatisticsService.getNumberOfTraineesStatistics(dto); + return ActionResult.success(number); + } + + /** + * 获取考试统计相关信息 + * + * @param dto 统计数据DTO对象 + * @return 返回考试通过率的结果 ActionResult 对象 + */ + @GetMapping("/getExamStatistics") + public ActionResult getExamStatistics(FtbCultivateStatisticsDTO dto) { + return ActionResult.success(examUserService.statistics(dto)); + } + + /** + * web 全局概览【热门课程】 + * + * @param dto 统计数据DTO对象 + * @return 热门课程的ActionResult对象 + */ + @GetMapping("/getPopularCourses") + public ActionResult> getPopularCourses(FtbCultivateStatisticsDTO dto) { + getUserList(dto); + return ActionResult.success(ftbCultivateStatisticsService.getPopularCourses(dto)); + } + + /** + * web 全局概览【学习人数和学习时长折线图统计】 + * + * @param dto + * @return + */ + @GetMapping("/numberOfStudents") + public ActionResult> numberOfStudents(FtbCultivateStatisticsDTO dto) { + getUserList(dto); + List popularCourses = ftbCultivateStatisticsService.numberOfStudentsLineGraph(dto); + return ActionResult.success(popularCourses); + } + + /** + * 数据看板/统计(鉴定) + * + * @param req + * @return {@link ActionResult}<{@link IdentifyStatisticsVo}> + */ + @GetMapping(value = "/apply/statistics") + public ActionResult getIdentifyStatistics(@Valid FtbCultivateStatisticsDTO req) { + getUserList(req); + if (CollectionUtils.isEmpty(req.getUserIds())) { + return ActionResult.success(new IdentifyStatisticsVo()); + } + return ActionResult.success(applyService.getIdentifyStatistics(req)); + } + + /** + * web 全局概览 课程学习 成员排行 + * + * @param dto 统计数据DTO对象 + * @param rankingType 排名类型,1学时,2考试,3鉴定,必传 + * @return 统计数据视图对象中的顶级成员结果 + */ + @GetMapping("/getTopMembers") + public ActionResult> getTopMembers(FtbCultivateStatisticsDTO dto, Integer rankingType) { + getUserList(dto); + List result = ftbCultivateStatisticsService.getTopMembers(dto, rankingType); + return ActionResult.success(result); + } + + /** + * 获取用户列表 + * + * @param dto + */ + private void getUserList(FtbCultivateStatisticsDTO dto) { + // 如果组织ID为空,直接返回空列表 + if (StringUtils.isEmpty(dto.getOrgId())) { + dto.setUserIds(Collections.emptyList()); + return; + } + + // 构造查询条件 + QueryPageUserDTO req = new QueryPageUserDTO(); + req.setOrganizeIds(List.of(dto.getOrgId().split(","))); + req.setIsPage(false); + if (dto.getIsSelectNext() != null && dto.getIsSelectNext().equals(1)) { + req.setHaveChildOrganizeId(true); + } else { + req.setHaveChildOrganizeId(false); + } + ActionResult> listByLevel = v2UserApi.pagePost(req); + List userSubQueryList = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + + // 查询结果为空时,设置空列表 + dto.setUserIds(userSubQueryList); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStoreStatisticController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStoreStatisticController.java new file mode 100644 index 0000000..11fb0d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/statistics/FtbCultivateStoreStatisticController.java @@ -0,0 +1,195 @@ +package jnpf.cultivate.controller.web.statistics; + +import jnpf.base.ActionResult; +import jnpf.cultivate.FtbCultivateStoreStatisticApi; +import jnpf.cultivate.service.FtbCultivateStatisticsService; +import jnpf.cultivate.service.FtbCultivateStoreStatisticService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.dto.storestatistics.*; +import jnpf.model.cultivate.vo.position.*; +import jnpf.model.cultivate.vo.statistics.NumberofAppSessions; +import jnpf.model.thousandsfaces.TodayWorkVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.Calendar; +import java.util.Date; + + +/** + * 培训店长统计模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/web/store-index-statistics") +@Validated +@Slf4j +public class FtbCultivateStoreStatisticController implements FtbCultivateStoreStatisticApi { + + @Autowired + private FtbCultivateStoreStatisticService ftbCultivateStoreStatisticService; + + @Resource + private FtbCultivateStatisticsService ftbCultivateStatisticsService; + + /** + * 店长页面 培训统计数量 + */ + @Override + @PostMapping("/get-cultivate-count") + public ActionResult getCultivateCount(@RequestBody FtbCultivateStoreStatisticsReq req) { + FtbCultivateStoreCountVO vo = ftbCultivateStoreStatisticService.getCultivateCount(req); + return ActionResult.success(vo); + } + + + /** + * 查询我的考试列表 + * + * @param req + * @return + */ + @Override + @PostMapping("/queryMyExamList") + public ActionResult> queryMyExamList(@RequestBody StoreStatisticsMyExamReq req) { + List list = ftbCultivateStoreStatisticService.queryMyExamList(req); + return ActionResult.success(list); + } + + /** + * 待我批阅(下属的) + * + * @param req + * @return + */ + @Override + @PostMapping("/queryWaitMyReadOver") + public ActionResult> queryWaitMyReadOver(@RequestBody StoreStatisticsWaitMyCheckExamReq req) { + List list = ftbCultivateStoreStatisticService.queryWaitMyReadOver(req); + return ActionResult.success(list); + } + + /** + * 今日工作-待参与及参与中的线下培训<负责人及参与人均属于相关人员>列表 + * + * @param req + * @return + */ + @Override + @PostMapping("/queryOfflineTrainList") + public ActionResult> queryOfflineTrainList(@RequestBody StoreOfflineTrainReq req) { + List list = ftbCultivateStoreStatisticService.queryOfflineTrainList(req); + return ActionResult.success(list); + } + + + /** + * 今日工作-我的任务(当日开始的培训任务)列表 + * + * @param req + * @return + */ + @Override + @PostMapping("/myTaskList") + public ActionResult> storeMyTaskList(@RequestBody StoreOfflineTrainReq req) { + List list = ftbCultivateStoreStatisticService.storeMyTaskList(req); + return ActionResult.success(list); + } + + + /** + * 今日工作-鉴定他人(当日的待鉴定)列表 + * + * @param req + * @return + */ + @Override + @PostMapping("/myIdentityList") + public ActionResult> storeMyIdentityList(@RequestBody StoreIdentityReq req) { + List list = ftbCultivateStoreStatisticService.storeMyIdentityList(req); + return ActionResult.success(list); + } + + /** + * 员工界面 查询未学习的通用课程数量 + * + * @param req + * @return + */ + @Override + @PostMapping("/get-worker-cultivate-count") + public ActionResult getWorkerCultivateCount(@RequestBody FtbCultivateStoreStatisticsReq req) { + FtbCultivateWorkerCountVO vo = ftbCultivateStoreStatisticService.getWorkerCultivateCount(req); + return ActionResult.success(vo); + } + + /** + * 工作台-员工 + */ + @Override + @PostMapping("/person/training-statistics") + public ActionResult personTrainingStatistics(@RequestBody FtbCultivateStoreStatisticsReq req) { + FtbPersonTrainingStatisticsVO vo = ftbCultivateStoreStatisticService.personTrainingStatistics(req); + return ActionResult.success(vo); + } + + + /** + * 工作台-店长 + */ + + @PostMapping("/store-manager/training-statistics") + public ActionResult storeManagerTrainingStatistics(@RequestBody FtbCultivateStoreStatisticsReq req) { + FtbStoreManagerTrainingStatisticsVO vo = ftbCultivateStoreStatisticService.storeManagerTrainingStatistics(req); + return ActionResult.success(vo); + } + + /** + * 工作台-管理层 + */ + + @PostMapping("/manager/training-statistics") + public ActionResult managerTrainingStatistics(@RequestBody FtbCultivateStoreStatisticsReq req) { + FtbManagerTrainingStatisticsVO vo = new FtbManagerTrainingStatisticsVO(); + vo.initData(); + FtbCultivateStatisticsDTO dto = new FtbCultivateStatisticsDTO(); + dto.setOrgId(req.getStoreId()); + dto.setOrgIds(req.getStoreIds()); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MONTH, -1); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date startTime = calendar.getTime(); + dto.setEndTime(new Date()); + dto.setStartTime(startTime); + NumberofAppSessions numberofAppSessions = ftbCultivateStatisticsService.trainingStatisticsV2(dto); + if (Objects.nonNull(numberofAppSessions)) { + BigDecimal identPass = numberofAppSessions.getNumberOfOperation(); + BigDecimal identTotal = numberofAppSessions.getTotalNumberOfAppraisals(); + BigDecimal examPass = numberofAppSessions.getNumberOfExam(); + BigDecimal examTotal = new BigDecimal(numberofAppSessions.getTotalNumberExam()); + if (identTotal.compareTo(BigDecimal.ZERO) != 0) { + vo.setIdentificationPassRate(UserApiV2Util.dealWithRate(identPass.divide(identTotal, 4, RoundingMode.HALF_UP), "")); + } + if (examTotal.compareTo(BigDecimal.ZERO) != 0) { + vo.setExamPassRate(UserApiV2Util.dealWithRate(examPass.divide(examTotal, 4, RoundingMode.HALF_UP), "")); + } + } + return ActionResult.success(vo); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingController.java new file mode 100644 index 0000000..3daaf23 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingController.java @@ -0,0 +1,113 @@ +package jnpf.cultivate.controller.web.teaching; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.TeachingRecordService; +import jnpf.model.cultivate.dto.teaching.QueryTeachingRecordDto; +import jnpf.model.cultivate.vo.teaching.TeachingDataListVo; +import jnpf.model.cultivate.vo.teaching.TeachingRecordVo; +import jnpf.model.cultivate.vo.teaching.TeachingStoreListVo; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 带教管理 + */ +@Slf4j +@RestController +@RequestMapping("/web/teachingRecord") +public class TeachingController { + + @Autowired + private TeachingRecordService teachingRecordService; + + + /** + * 汇总数据-带教-分页列表 + * @param queryTeachingRecordDto + * @return ActionResult> + */ + @PostMapping("/summaryTeach") + public ActionResult> summaryTeach(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + PageInfo pageInfo = teachingRecordService.teachingSummaryPage(queryTeachingRecordDto); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 汇总数据-带教-导出 + * @param queryTeachingRecordDto + * @return void + */ + @PostMapping("/summaryTeachExport") + public void summaryTeachExport(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + teachingRecordService.summaryTeachExport(queryTeachingRecordDto); + } + + /** + * 查看带教门店下拉列表 + * @return ActionResult + */ + @GetMapping("/storeList") + public ActionResult> storeList() { + List teachingStoreList = teachingRecordService.getTeachingStoreList(); + return ActionResult.success(teachingStoreList); + } + + /** + * 带教汇总详情页-学员统计 + * @param queryTeachingRecordDto + * @return + */ + @PostMapping("/summaryTeachDetailPage") + public ActionResult> summaryTeachDetailPage(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + PageInfo voPageInfo = teachingRecordService.teachingStudentDataCount(queryTeachingRecordDto); + return ActionResult.page(voPageInfo.getList(), FtbUtil.getPagination(voPageInfo)); + } + + /** + * 带教汇总详情页-学员统计-导出 + * @param queryTeachingRecordDto + * @return + */ + @PostMapping("/studentDetailExport") + public void studentDetailExport(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + teachingRecordService.studentDetailExport(queryTeachingRecordDto); + } + + /** + * 带教明细 - 分页列表查询 + * @param queryTeachingRecordDto + * @return + */ + @PostMapping("/detailItemTeachPage") + public ActionResult> detailItemTeachPage(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + queryTeachingRecordDto.setIsDataPermission(true); + PageInfo page = teachingRecordService.page(queryTeachingRecordDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 带教明细-导出 + * @param queryTeachingRecordDto + */ + @PostMapping("/detailItemTeachExport") + public void detailItemTeachExport(@RequestBody QueryTeachingRecordDto queryTeachingRecordDto) { + teachingRecordService.detailItemTeachExport(queryTeachingRecordDto); + } + + /** + * 带教明细-批量删除 + * @param ids 记录id集合 + * @return Void + */ + @DeleteMapping("/detailItemDelete") + public ActionResult detailItemDelete(@RequestBody List ids) { + teachingRecordService.batchDeleteRecord(ids); + return ActionResult.success(); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingRecordController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingRecordController.java new file mode 100644 index 0000000..e22b6bf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingRecordController.java @@ -0,0 +1,218 @@ +package jnpf.cultivate.controller.web.teaching; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.TeachingRecordPracticeService; +import jnpf.model.cultivate.dto.teaching.*; +import jnpf.model.cultivate.vo.teaching.*; +import jnpf.util.CustomTenantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.List; + +/** + * 练习管理 + */ +@Slf4j +@RestController +@RequestMapping("/teachingRecord") +public class TeachingRecordController { + @Resource + private CustomTenantUtil customTenantUtil; + @Autowired + private TeachingRecordPracticeService recordPracticeService; + + /** + * Web-练习门店下拉 + * + * @return 练习门店集合 + */ + @PostMapping("/web/getStoreList") + public ActionResult> getStoreList() { + return ActionResult.success(recordPracticeService.getStoreList()); + } + + /** + * Web汇总-分页列表 + * + * @param pageDto 查询参数 + * @return 汇总数据集合 + */ + @PostMapping("/web/summaryPageList") + public ActionResult> summaryPageList(@Valid @RequestBody TeachingBaseFilter pageDto) { + PageInfo page = recordPracticeService.summaryPageList(pageDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * Web汇总-导出 + * @param response 响应 + * @param pageDto 查询参数 + */ + @PostMapping("/web/summaryExport") + public void summaryExport(HttpServletResponse response, @Valid @RequestBody TeachingBaseFilter pageDto) throws Exception { + recordPracticeService.summaryExport(response, pageDto); + } + + /** + * Web记录-分页列表 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + @PostMapping("/web/recordPageList") + public ActionResult> recordPageList(@Valid @RequestBody TeachingBaseFilter pageDto) { + PageInfo page = recordPracticeService.recordPageList(pageDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * Web记录-导出 + * @param response 响应 + * @param pageDto 查询参数 + */ + @PostMapping("/web/recordExport") + public void recordExport(HttpServletResponse response, @Valid @RequestBody TeachingBaseFilter pageDto) throws Exception { + recordPracticeService.recordExport(response, pageDto); + } + + /** + * Web记录-批量删除 + * @param deleteDto 删除参数 + * @return 是否成功 + */ + @DeleteMapping("/web/recordBatchDelete") + public ActionResult recordBatchDelete(@Valid @RequestBody RecordBatchDeleteDto deleteDto) { + return ActionResult.success(recordPracticeService.recordBatchDelete(deleteDto)); + } + + /** + * App我的练习-分页列表 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + @PostMapping("/app/myRecordPageList") + public ActionResult> myRecordPageList(@Valid @RequestBody MyRecordPageListDto pageDto) { + PageInfo page = recordPracticeService.myRecordPageList(pageDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * App我的练习-新增 + * + * @param saveDto 保存参数 + * @return 是否成功 + */ + @PostMapping("/app/mySave") + public ActionResult mySave(@Valid @RequestBody RecordSaveDto saveDto) throws Exception { + return ActionResult.success(recordPracticeService.mySave(saveDto)); + } + + /** + * App我的练习-详情 + * + * @param id 数据id + * @return 练习详情 + */ + @PostMapping("/app/myInfo/{id}") + public ActionResult myInfo(@PathVariable("id") String id) { + return ActionResult.success(recordPracticeService.myInfo(id)); + } + + /** + * App我的练习-修改 + * + * @param updateDto 修改参数 + * @return 是否成功 + */ + @PutMapping("/app/myUpdate") + public ActionResult myUpdate(@Valid @RequestBody RecordUpdateDto updateDto) { + return ActionResult.success(recordPracticeService.myUpdate(updateDto)); + } + + /** + * App我的练习-删除 + * + * @param id 数据id + * @return 是否成功 + */ + @DeleteMapping("/app/myDelete/{id}") + public ActionResult myDelete(@PathVariable("id") String id) { + return ActionResult.success(recordPracticeService.myDelete(id)); + } + + /** + * App我的练习-查看数据 + * + * @param viewDataDto 查询条件 + * @return 技能点信息集合 + */ + @PostMapping("/app/myViewData") + public ActionResult myViewData(@Valid @RequestBody ViewDataDto viewDataDto) { + return ActionResult.success(recordPracticeService.myViewData(viewDataDto)); + } + + /** + * App员工练习-分页列表 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + @PostMapping("/app/employeePageList") + public ActionResult> employeePageList(@Valid @RequestBody EmployeePageListDto pageDto) { + PageInfo page = recordPracticeService.employeePageList(pageDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * App员工练习-查看数据 + * + * @param viewDataDto 查询参数 + * @return 练习集合 + */ + @PostMapping("/app/employeeViewData") + public ActionResult> employeeViewData(@Valid @RequestBody ViewDataDto viewDataDto) { + return ActionResult.success(recordPracticeService.employeeViewData(viewDataDto)); + } + + /** + * App店长界面-今日练习汇总数据 + * + * @param storeId 查询参数 + * @return 练习汇总数据 + */ + @GetMapping("/app/getTodaySummary") + public TodaySummaryDataVo getTodaySummary(@RequestParam("storeId") String storeId) { + return recordPracticeService.getTodaySummary(storeId); + } + + /** + * 生成带教/练习的模拟数据 + */ + @NoDataSourceBind + @GetMapping("/createSimulatedData") + public ActionResult createSimulatedData(@RequestParam String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return ActionResult.success(recordPracticeService.createSimulatedData(tenantId)); + } + + /** + * A员工界面--我的练习汇总数据 + * + * @param storeId 查询参数 + * @return 练习汇总数据 + */ + @GetMapping("/app/getMyPracticeSummary") + public MyPracticeSummaryVo getMyPracticeSummary(@RequestParam("storeId") String storeId) { + return recordPracticeService.getMyPracticeSummary(storeId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingSkillController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingSkillController.java new file mode 100644 index 0000000..b004029 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/controller/web/teaching/TeachingSkillController.java @@ -0,0 +1,126 @@ +package jnpf.cultivate.controller.web.teaching; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.TeachingSkillService; +import jnpf.model.cultivate.dto.teaching.TeachingSkillAddDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillPageDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillSortDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillUpdateDto; +import jnpf.model.cultivate.vo.teaching.SkillInfoVo; +import jnpf.model.cultivate.vo.teaching.TeachingSkillVo; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * 技能点配置管理 + */ +@RestController +@RequestMapping("/teachingSkill") +public class TeachingSkillController { + + @Autowired + private TeachingSkillService teachingSkillService; + + /** + * 新增 + * + * @param addDto 技能点DTO + * @return 是否成功 + */ + @PostMapping("/add") + public ActionResult add(@Valid @RequestBody TeachingSkillAddDto addDto) { + + if (StringUtil.isNotBlank(addDto.getCategoryId()) && "-1".equals(addDto.getCategoryId())) { + addDto.setCategoryId(null); + } + return ActionResult.success(teachingSkillService.addData(addDto)); + } + + /** + * 删除 + * + * @param id 删除参数 + * @return 是否成功 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + return ActionResult.success(teachingSkillService.deleteData(id)); + } + + /** + * 修改 + * + * @param updateDto 技能点DTO + * @return 是否成功 + */ + @PutMapping("/update") + public ActionResult update(@Valid @RequestBody TeachingSkillUpdateDto updateDto) { + + if (StringUtil.isNotBlank(updateDto.getCategoryId()) && "-1".equals(updateDto.getCategoryId())) { + updateDto.setCategoryId(null); + } + return ActionResult.success(teachingSkillService.updateData(updateDto)); + } + + /** + * 查询 + * + * @param id 查询参数 + * @return 技能点VO + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success(teachingSkillService.getData(id)); + } + + /** + * 分页列表 + * + * @param pageDto 分页查询参数 + * @return 技能点集合 + */ + @GetMapping("/pageList") + public ActionResult> pageList(@Valid TeachingSkillPageDto pageDto) { + PageInfo page = teachingSkillService.pageList(pageDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 下拉列表 + * + * @return 技能点集合 + */ + @GetMapping("/selectDataList") + public ActionResult> selectDataList() { + return ActionResult.success(teachingSkillService.selectDataList()); + } + + /** + * 排序 + * + * @param sortDto 数据集合 + * @return 是否成功 + */ + @PutMapping("/sort") + public ActionResult sort(@Valid @RequestBody TeachingSkillSortDto sortDto) { + return ActionResult.success(teachingSkillService.sort(sortDto)); + } + + /** + * 查询技能点[树形] + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/all-list") + public ActionResult> getAllSkillList() { + + List list = teachingSkillService.getAllSkillList(); + return ActionResult.success(list); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/EventHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/EventHandler.java new file mode 100644 index 0000000..6b37653 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/EventHandler.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.event; + +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Optional; + +/** + * 事件处理程序 + * + * @author fantaibao + * @date 2023/12/26 + */ +@Component +public class EventHandler { + + @Resource + private List jnpfApplicationEventServices; + + + @EventListener(value = JnpfApplicationEvent.class) + public void courseEvent(JnpfApplicationEvent jnpfApplicationEvent) { + Object data = jnpfApplicationEvent.getData(); + // 事件处理分发 + Optional first = jnpfApplicationEventServices.stream() + .filter(t -> t.isSupportedCourseEvent(data)).findFirst(); + first.ifPresent(jnpfApplicationEventService -> jnpfApplicationEventService.handlerCourseEvent(data)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JnpfApplicationEventService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JnpfApplicationEventService.java new file mode 100644 index 0000000..d736914 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JnpfApplicationEventService.java @@ -0,0 +1,20 @@ +package jnpf.cultivate.event; + +public interface JnpfApplicationEventService { + + /** + * 是否支持此事件 + * + * @param event 事件 + * @return boolean + */ + boolean isSupportedCourseEvent(Object event); + + /** + * 处理事件 + * + * @param courseEvent 事件入参 + */ + void handlerCourseEvent(Object courseEvent); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JsonToListTypeHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JsonToListTypeHandler.java new file mode 100644 index 0000000..c4ebc0f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/JsonToListTypeHandler.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jnpf.model.cultivate.vo.teaching.SkillCountVo; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +public class JsonToListTypeHandler extends BaseTypeHandler> { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, toJson(parameter)); + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + return parseJson(rs.getString(columnName)); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return parseJson(rs.getString(columnIndex)); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return parseJson(cs.getString(columnIndex)); + } + + private String toJson(List list) { + try { + return objectMapper.writeValueAsString(list); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private List parseJson(String json) { + try { + if (json == null || json.isEmpty()) return null; + return objectMapper.readValue(json, + objectMapper.getTypeFactory().constructCollectionType(List.class, SkillCountVo.class) + ); + } catch (Exception e) { + throw new RuntimeException("JSON解析失败: " + json, e); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/TriggerProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/TriggerProcessor.java new file mode 100644 index 0000000..401735a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/TriggerProcessor.java @@ -0,0 +1,28 @@ +package jnpf.cultivate.event.base; + +import jnpf.model.cultivate.bo.TriggerEventBO; + +/** + * 触发处理器 + * + * @author wangchunxiang + * @date 2024/09/14 + */ +public interface TriggerProcessor { + + /** + * 触发器类型确认 + * + * @param triggerEventType 触发器事件类型 + * @return boolean + */ + boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType); + + /** + * 事件触发器 + * + * @param triggerEvent trigger事件 + */ + void triggerHandle(T triggerEvent); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/course/CourseProcessLearnAbstract.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/course/CourseProcessLearnAbstract.java new file mode 100644 index 0000000..4c6325d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/course/CourseProcessLearnAbstract.java @@ -0,0 +1,130 @@ +package jnpf.cultivate.event.base.course; + + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceChapterLearningMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.service.FtbCultivateCourseSettingService; +import jnpf.cultivate.service.FtbCultivateCourseTriggerLogService; +import jnpf.model.cultivate.event.dto.course.CourseProcessLearn; +import jnpf.model.cultivate.po.course.FtbCultivateCourseTriggerLog; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +public abstract class CourseProcessLearnAbstract { + + @Resource + protected FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Resource + protected FtbCultivatePositionCourceChapterLearningMapper ftbCultivatePositionCourceChapterLearningMapper; + + @Resource + protected FtbCultivateCourseSettingService ftbCultivateCourseSettingService; + + @Resource + private FtbCultivateCourseTriggerLogService ftbCultivateCourseTriggerLogService; + + protected void courseProcessLearn(T baseData) { + // 学习课程章节进度 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionCourceChapterLearning::getChapterId, baseData.getChapterId()); + updateWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, baseData.getUserId()); + updateWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + updateWrapper.set(FtbCultivatePositionCourceChapterLearning::getState, 1); + updateWrapper.set(FtbCultivatePositionCourceChapterLearning::getChapterTime, baseData.getDuration()); + ftbCultivatePositionCourceChapterLearningMapper.update(new FtbCultivatePositionCourceChapterLearning(), updateWrapper); + // 判断章节是否全部学习完成,学习完成,变更课程为全部学习完成状态 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getCourceId, baseData.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, baseData.getUserId()); + // 是否存在章节状态未学习 + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getState, 0); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + // 学习课程状态变更 + LambdaQueryWrapper courceLearningLambdaQueryWrapper = Wrappers.lambdaQuery(); + courceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, baseData.getCourseId()); + courceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, baseData.getUserId()); + courceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = ftbCultivatePositionCourceLearningMapper.selectOne(courceLearningLambdaQueryWrapper); + if (ftbCultivatePositionCourceLearning.getState() == 0) { + // 课程学习中 + ftbCultivatePositionCourceLearning.setState(2); + } + // 课程学习完后实操鉴定和考试触发 + if (ftbCultivatePositionCourceChapterLearningMapper.selectCount(queryWrapper) == 0) { + ftbCultivatePositionCourceLearning.setState(1); + courseLearnCompleteHandle(baseData); + saveTriggerLog(baseData); + } + ftbCultivatePositionCourceLearningMapper.updateById(ftbCultivatePositionCourceLearning); + // 随堂测试结果存储 + ftbCultivateCourseSettingService.onSiteTestResultStorage(baseData.getFtbChapterTestDTOs(), + baseData.getCourseId(), baseData.getChapterId()); + } + + + private void saveTriggerLog(T baseData) { + if(baseData.getStudyType()==null || StringUtils.isEmpty(baseData.getRelationBusinessId())){ + return; + } + FtbCultivateCourseTriggerLog log = new FtbCultivateCourseTriggerLog(); + log.setBusinessId(baseData.getCourseId()); + log.setType(baseData.getStudyType()); + log.setUserId(baseData.getUserId()); + log.setCourseId(baseData.getCourseId()); + log.setBusinessId(baseData.getRelationBusinessId()); + log.setCreatorUserId(baseData.getUserId()); + log.setCreatorTime(new Date()); + ftbCultivateCourseTriggerLogService.save(log); + } + + + /** + * 查询课是否开始学习 + * + * @param userId 用户ID + * @param courseIds 课程ID集合 + * @return boolean false-未开始学习 true-开始学习 + */ + protected boolean checkCourseStudyStatus(String userId, List courseIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + query.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + query.in(FtbCultivatePositionCourceLearning::getState, List.of("1"));//学习状态 1已学习,0未学习,2学习中 + query.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + return ftbCultivatePositionCourceLearningMapper.selectCount(query) >0; + } + + /** + * 该用户是否完成课程 + * + * @param userId 用户ID + * @param courseIds 课程ID集合 + * @return boolean + */ + protected boolean hasTheCourseBeenCompleted(String userId, List courseIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + query.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + query.in(FtbCultivatePositionCourceLearning::getState, List.of("0", "2")); + query.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + return ftbCultivatePositionCourceLearningMapper.selectCount(query) == 0; + } + + /** + * 课程学习完成后置处理,用于触发课程相关的逻辑 + * + * @param baseData + */ + public abstract void courseLearnCompleteHandle(T baseData); + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/CoursePositionTaskTriggerAbstract.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/CoursePositionTaskTriggerAbstract.java new file mode 100644 index 0000000..2b9b74c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/CoursePositionTaskTriggerAbstract.java @@ -0,0 +1,160 @@ +package jnpf.cultivate.event.base.examidentify; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionIdentifyResultMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.model.cultivate.dto.identify.IdentifyApplyDataPushDto; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.cultivate.resp.TriggerExamDto; +import lombok.Builder; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Date; + +/** + * 岗位课程、鉴定和考试触发抽象类 + * + * @author wangchunxiang + * @date 2024/09/14 + */ +public abstract class CoursePositionTaskTriggerAbstract extends PositionTaskTriggerAbstract { + + @Autowired + protected FtbCultivatePositionIdentifyResultMapper ftbCultivatePositionIdentifyResultMapper; + + @Autowired + protected FtbCultivateCourseMapper cultivateCourseMapper; + + @Autowired + protected CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Autowired + protected FtbCultivateExamUserService ftbCultivateExamUserService; + + /** + * 通用岗位和课程考试触发 + */ + protected TriggerExamDto handleExamTrigger(String userId, String examId, String postRankId, String postLearnId) { + TriggerExamDto triggerExamDto = new TriggerExamDto(); + triggerExamDto.setUserId(userId); + triggerExamDto.setExamId(examId); + triggerExamDto.setRelationPositionId(postRankId); + triggerExamDto.setRelationId(postLearnId); + triggerExamHandle(triggerExamDto); + return triggerExamDto; + } + + /** + * 岗位和课程实操鉴定触发 + */ + protected IdentifyApplyDataPushDto handlePracticalIdentification(CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger practicalIdentificationTrigger) { + // 鉴定触发一次判断,岗位id+用户id + LambdaQueryWrapper ftbCultivatePositionIdentifyResultLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionIdentifyResultLambdaQueryWrapper.eq(FtbCultivatePositionIdentifyResult::getUserId, practicalIdentificationTrigger.getUserId()); + if(practicalIdentificationTrigger.type==2) { + //岗位只能一个鉴定 而且触发了就不在触发 课程可以有多个鉴定 + ftbCultivatePositionIdentifyResultLambdaQueryWrapper.eq(FtbCultivatePositionIdentifyResult::getIdentifyId, practicalIdentificationTrigger.getIdentityId()); + } + ftbCultivatePositionIdentifyResultLambdaQueryWrapper.eq(FtbCultivatePositionIdentifyResult::getEnabledMark, 0); + ftbCultivatePositionIdentifyResultLambdaQueryWrapper.eq(FtbCultivatePositionIdentifyResult::getPostRankId, practicalIdentificationTrigger.getPostId()); + ftbCultivatePositionIdentifyResultLambdaQueryWrapper.eq(FtbCultivatePositionIdentifyResult::getType, practicalIdentificationTrigger.type); + ftbCultivatePositionIdentifyResultLambdaQueryWrapper.last("limit 1"); + if (ftbCultivatePositionIdentifyResultMapper.selectCount(ftbCultivatePositionIdentifyResultLambdaQueryWrapper) == 0) { + IdentifyApplyDataPushDto identifyApplyDataPushDto = new IdentifyApplyDataPushDto(); + identifyApplyDataPushDto.setDataId(practicalIdentificationTrigger.getPostLearnId()); + identifyApplyDataPushDto.setTableId(practicalIdentificationTrigger.getIdentityId()); + identifyApplyDataPushDto.setIdentifyUserId(practicalIdentificationTrigger.getUserId()); + identifyApplyDataPushDto.setStudyFinishTime(new Date()); + identifyApplyDataPushDto.setIsTurnOnVisibility(practicalIdentificationTrigger.getIsTurnOnVisibility()); + identifyApplyDataPushDto.setPostId(practicalIdentificationTrigger.postId); + // 鉴定特殊处理 + identifyApplyDataHandle(identifyApplyDataPushDto, practicalIdentificationTrigger); + return identifyApplyDataPushDto; + } + return null; + } + + @Override + public void afterTriggerIdentifyApply(IdentifyApplyDataPushDto identifyApplyDataPushDto, ExamIdentifyTriggerEventDTO triggerEvent, String identificationRecordId) { + + if (StringUtils.isBlank(identificationRecordId)) { + return; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbCultivatePositionIdentifyResult::getIdentifyRecordId, identificationRecordId); + wrapper.eq(FtbCultivatePositionIdentifyResult::getEnabledMark, 0); + Long count = ftbCultivatePositionIdentifyResultMapper.selectCount(wrapper); + if (count > 0) { + return; + } + + FtbCultivatePositionIdentifyResult ftbCultivatePositionIdentifyResult = new FtbCultivatePositionIdentifyResult(); + ftbCultivatePositionIdentifyResult.setPostLearnId(identifyApplyDataPushDto.getDataId()); + ftbCultivatePositionIdentifyResult.setIdentifyRecordId(identificationRecordId); + ftbCultivatePositionIdentifyResult.setUserId(identifyApplyDataPushDto.getIdentifyUserId()); + ftbCultivatePositionIdentifyResult.setPostRankId(triggerEvent.getPostId()); + ftbCultivatePositionIdentifyResult.setEnabledMark(0); + ftbCultivatePositionIdentifyResult.setIdentifyId(identifyApplyDataPushDto.getTableId()); + // 鉴定结果特殊处理 + positionIdentifyDataHandle(ftbCultivatePositionIdentifyResult); + ftbCultivatePositionIdentifyResultMapper.insert(ftbCultivatePositionIdentifyResult); + } + + /** + * 考试触发特殊处理 + */ + public abstract void triggerExamHandle(TriggerExamDto triggerExamDto); + + /** + * 鉴定特殊处理 + */ + public abstract void identifyApplyDataHandle(IdentifyApplyDataPushDto identifyApplyDataPushDto, CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger practicalIdentificationTrigger); + + /** + * 鉴定结果特殊处理 + */ + public abstract void positionIdentifyDataHandle(FtbCultivatePositionIdentifyResult ftbCultivatePositionIdentifyResult); + + + @Data + @Builder + public static class PracticalIdentificationTrigger { + /** + * 用户id(必传) + */ + String userId; + /** + * 岗位id(必传) + */ + String postId; + /** + * 实操鉴定id(必传) + */ + String identityId; + /** + * 岗位学习id + */ + String postLearnId; + /** + * 课程id + */ + String courseId; + /** + * 是否打开可见性, 0 打开, 1 关闭 + * 默认为0 + */ + Integer isTurnOnVisibility; + + /** + * 来源,0岗位学习实操鉴定,2课程实操鉴定 + */ + Integer type; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerAbstract.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerAbstract.java new file mode 100644 index 0000000..0271a09 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerAbstract.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.event.base.examidentify; + +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.model.cultivate.dto.identify.IdentifyApplyDataPushDto; +import jnpf.model.cultivate.resp.TriggerExamDto; + +import javax.annotation.Resource; + +/** + * 考试和鉴定触发通用抽象类 + * + * @author wangchunxiang + * @date 2024/09/14 + */ +public abstract class ExamIdentifyTriggerAbstract implements ExamIdentifyTriggerProcessor { + + @Resource + protected CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Resource + protected FtbCultivateExamUserService ftbCultivateExamUserService; + + @Override + public void handleTriggerExam(TriggerExamDto triggerExam) { + ftbCultivateExamUserService.triggerPositionExam(triggerExam); + } + + @Override + public String handleTriggerIdentifyApply(IdentifyApplyDataPushDto identifyApplyDataPushDto) { + return cultivateIdentifyApplyService.applyDataPush(identifyApplyDataPushDto); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerProcessor.java new file mode 100644 index 0000000..0cd7f0b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/ExamIdentifyTriggerProcessor.java @@ -0,0 +1,114 @@ +package jnpf.cultivate.event.base.examidentify; + + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.base.TriggerProcessor; +import jnpf.model.cultivate.dto.identify.IdentifyApplyDataPushDto; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.cultivate.resp.TriggerExamDto; + +import java.util.Date; +import java.util.Objects; + +/** + * 考试鉴定触发器处理器 + * + * @author wangchunxiang + * @date 2024/09/14 + */ +public interface ExamIdentifyTriggerProcessor extends TriggerProcessor { + int original = 0; + /** + * 未触发考试标识 + */ + int EXAM_NOT_TRIGGER = 2; + /** + * 未触发鉴定标识 + */ + int IDENTIFY_NOT_TRIGGER = 4; + + @Override + default void triggerHandle(ExamIdentifyTriggerEventDTO triggerEvent) { + int i = original; + // 考试参数构建 + TriggerExamDto triggerExamDto = buildTriggerExamDto(triggerEvent); + // 考试触发 + if (Objects.nonNull(triggerExamDto)) { + handleTriggerExam(triggerExamDto); + } else { + i = i | EXAM_NOT_TRIGGER; + } + // 鉴定参数构建 + IdentifyApplyDataPushDto identifyApplyDataPushDto = buildIdentifyApplyDataPushDto(triggerEvent); + // 鉴定触发 + if (Objects.nonNull(identifyApplyDataPushDto)) { + String identificationRecordId = handleTriggerIdentifyApply(identifyApplyDataPushDto); + // 鉴定触发后置处理 + if (StrUtil.isNotBlank(identificationRecordId)) { + afterTriggerIdentifyApply(identifyApplyDataPushDto, triggerEvent, identificationRecordId); + } + } else { + boolean hasIndetify = isHasIndetify(triggerEvent); + if (hasIndetify) { + return; + } + i = i | IDENTIFY_NOT_TRIGGER; + + } + // 实操鉴定和考试触发后置处理 + postProcessing(i, triggerEvent); + } + + /** + * 触发考试 + */ + void handleTriggerExam(TriggerExamDto triggerExam); + + /** + * 鉴定触发 + * + * @param identifyApplyDataPushDto 触发鉴定参数 + * @return {@link String } 鉴定记录id + */ + String handleTriggerIdentifyApply(IdentifyApplyDataPushDto identifyApplyDataPushDto); + + /** + * 鉴定触发后置处理器 + * + * @param identifyApplyDataPushDto 触发鉴定参数 + * @param triggerEvent trigger事件 + * @param identificationRecordId 鉴定记录id + */ + void afterTriggerIdentifyApply(IdentifyApplyDataPushDto identifyApplyDataPushDto, ExamIdentifyTriggerEventDTO triggerEvent, String identificationRecordId); + + /** + * 考试参数构建 + */ + TriggerExamDto buildTriggerExamDto(ExamIdentifyTriggerEventDTO triggerEvent); + + /** + * 鉴定参数构建 + */ + IdentifyApplyDataPushDto buildIdentifyApplyDataPushDto(ExamIdentifyTriggerEventDTO triggerEvent); + + /** + * 是否有鉴定 主要处理幂等性问题 + * + * @param triggerEvent + * @return boolean false-没有 true-有 + */ + boolean isHasIndetify(ExamIdentifyTriggerEventDTO triggerEvent); + + /** + * 后置处理(主要是证书颁发) + * + * @param i 根据或与运算判断那个执行成功 + * @param triggerEvent trigger事件 + */ + default void postProcessing(int i, ExamIdentifyTriggerEventDTO triggerEvent) { + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/PositionTaskTriggerAbstract.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/PositionTaskTriggerAbstract.java new file mode 100644 index 0000000..41f80ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/base/examidentify/PositionTaskTriggerAbstract.java @@ -0,0 +1,36 @@ +package jnpf.cultivate.event.base.examidentify; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.FtbCultivatePositionMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; + +import javax.annotation.Resource; + +/** + * 岗位、任务基础抽象类 + * + * @author wangchunxiang + * @date 2024/07/29 + */ +public abstract class PositionTaskTriggerAbstract extends ExamIdentifyTriggerAbstract { + + @Resource + protected FtbCultivatePositionMapper cultivatePositionMapper; + + /** + * 岗位考试合格后才能进行鉴定 + * + * @param postId 岗位id + * @return 0否1是 + */ + protected Integer doJobQualificationTrigger(String postId) { + LambdaQueryWrapper positionLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionLambdaQueryWrapper.select(FtbCultivatePosition::getQualified); + positionLambdaQueryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + positionLambdaQueryWrapper.eq(FtbCultivatePosition::getPostId, postId); + return cultivatePositionMapper.selectOne(positionLambdaQueryWrapper).getQualified(); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/certificate/JnpfApplicationEventCertificateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/certificate/JnpfApplicationEventCertificateService.java new file mode 100644 index 0000000..470c57c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/certificate/JnpfApplicationEventCertificateService.java @@ -0,0 +1,598 @@ +package jnpf.cultivate.event.impl.certificate; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.event.JnpfApplicationEventService; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.utils.CultivateImUtil; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.course.FtbCultivateCertificateResult; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCertificate; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +@Component +@RequiredArgsConstructor +@SuppressWarnings("Duplicates") +@Slf4j +public class JnpfApplicationEventCertificateService implements JnpfApplicationEventService { + + private final FtbCultivatePromotionNewMapper ftbCultivatePromotionNewMapper; + private final FtbCultivateCertificateUserMapper ftbCultivateCertificateUserMapper; + private final FtbCultivateCertificateMapper ftbCultivateCertificateMapper; + private final FtbCultivatePositionCertificateMapper ftbCultivatePositionCertificateMapper; + private final FtbCultivatePositionCourseCertificateMapper ftbCultivatePositionCourseCertificateMapper; + private final FtbCultivateCourseMapper ftbCultivateCourseMapper; + private final FtbCultivateLearnTaskCertificateMapper ftbCultivateLearnTaskCertificateMapper; + private final FtbCultivateLearnTaskMapper learnTaskMapper; + private final FtbCultivateLearnTaskExamMapper learnTaskExamMapper; + private final FtbCultivateLearnTaskIdentificationMapper learnTaskIdentificationMapper; + private final FtbCultivateLearnTaskAssignmentMapper learnTaskAssignmentMapper; + private final CultivateIdentifyApplyMapper cultivateIdentifyApplyMapper; + private final FtbCultivateExamUserService ftbCultivateExamUserService; + private final CultivateImUtil cultivateImUtil; + private final FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + private final FtbCultivatePositionIdentifyResultMapper ftbCultivatePositionIdentifyResultMapper; + private final FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + private final FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + private final FtbCultivateCertificateResultMapper ftbCultivateCertificateResultMapper; + @Autowired + private UserApiV2Util userApiV2Util; + + + @Override + public boolean isSupportedCourseEvent(Object event) { + return event instanceof CertificateEventDTO; + } + + @Override + public void handlerCourseEvent(Object courseEvent) { + CertificateEventDTO certEventDTO = (CertificateEventDTO) courseEvent; + //颁发类型 0 课程证书 1 岗位学习证书, 2 任务证书 + log.error("自动颁发证书参数:" + certEventDTO.toString()); + Integer issuanceType = certEventDTO.getIssuanceType(); + // 岗位学习颁发 + if (issuanceType == 1) awardedForOnTheJobLearning(certEventDTO); + // 课程颁发 + if (issuanceType == 0) courseAwarded(certEventDTO); + // 任务颁发 + if (issuanceType == 2) taskIssuance(certEventDTO); + + } + + /** + * 课程颁发 + * + * @param certEventDTO + */ + private void courseAwarded(CertificateEventDTO certEventDTO) { + String userId = certEventDTO.getUserId(); + String courseId = certEventDTO.getCourseId(); + String postId = certEventDTO.getPostId(); + String reason = "课程学习自动颁发证书"; + PositionVO positionEntity = userApiV2Util.infoPosition(postId, null); + if (positionEntity ==null) { + positionEntity = new PositionVO(); + } + + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(courseId); + String contentMessage = "您已完成【" + positionEntity.getFullName() + "】岗位中【" + ftbCultivateCourse.getName() + "】课程学习"; + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourseCertificate::getPostRankId, postId); + queryWrapper.eq(FtbCultivatePositionCourseCertificate::getCourseId, courseId); + queryWrapper.eq(FtbCultivatePositionCourseCertificate::getEnabledMark, 0); + // 课程绑定证书 + FtbCultivatePositionCourseCertificate certificate = ftbCultivatePositionCourseCertificateMapper.selectOne(queryWrapper); + if (certificate == null) { + return; + } + // 证书是否下架及删除 + FtbCertificateEntity ftbCertificateEntity = ftbCultivateCertificateMapper.selectById(certificate.getCertificateId()); + if (ftbCertificateEntity.getStatus() == 0 || CourseEnums.EnabledMarkType.INVALID.getCode().equals(ftbCertificateEntity.getEnabledMark())) { + return; + } + + //查询是否颁发 + LambdaQueryWrapper certificateResultLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getUserId, certEventDTO.getUserId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getPostRankId, certEventDTO.getPostId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCertificateId, certificate.getCertificateId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCourseId, certificate.getCourseId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getType, 2);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + FtbCultivateCertificateResult ftbCultivateCertificateResult = ftbCultivateCertificateResultMapper.selectOne(certificateResultLambdaQueryWrapper); + if (null != ftbCultivateCertificateResult) { + return; + } + //查询课程是否有考试 + LambdaQueryWrapper courseExamWraper = Wrappers.lambdaQuery(); + courseExamWraper.eq(FtbCultivatePositionCourseExam::getPostRankId, postId); + courseExamWraper.eq(FtbCultivatePositionCourseExam::getCourseId, courseId); + courseExamWraper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + FtbCultivatePositionCourseExam courseExam = ftbCultivatePositionCourseExamMapper.selectOne(courseExamWraper); + if (null != courseExam) { + LambdaQueryWrapper userExamWraper = Wrappers.lambdaQuery(); + userExamWraper.eq(FtbCultivateExamUser::getUserId, userId); + userExamWraper.eq(FtbCultivateExamUser::getExamId, courseExam.getExamId()); + userExamWraper.eq(FtbCultivateExamUser::getExamSource, 1); + userExamWraper.eq(FtbCultivateExamUser::getEnabledMark, 1); + userExamWraper.eq(FtbCultivateExamUser::getRelationPositionId, postId); + userExamWraper.orderByDesc(FtbCultivateExamUser::getCreatorTime); + userExamWraper.last("limit 1"); + FtbCultivateExamUser examUser = ftbCultivateExamUserMapper.selectOne(userExamWraper); + if (null == examUser) { + log.error("课程关联考试未触发{}", certEventDTO); + return; + } + //(0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + if (examUser.getStatus() == 0 || examUser.getStatus() == 1 || examUser.getStatus() == 2 || examUser.getStatus() == 4) { + log.error("课程关联考试不合格{}", examUser); + return; + } + } + //查课程是否有鉴定 + LambdaQueryWrapper courseIdentifyWraper = Wrappers.lambdaQuery(); + courseIdentifyWraper.eq(FtbCultivatePositionCourseIdentity::getPostRankId, postId); + courseIdentifyWraper.eq(FtbCultivatePositionCourseIdentity::getCourseId, courseId); + courseIdentifyWraper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + FtbCultivatePositionCourseIdentity courseIdentity = ftbCultivatePositionCourseIdentityMapper.selectOne(courseIdentifyWraper); + if (null != courseIdentity) { + //查询鉴定结果表 + LambdaQueryWrapper identityResultWraper = Wrappers.lambdaQuery(); + identityResultWraper.eq(FtbCultivatePositionIdentifyResult::getPostRankId, postId); + identityResultWraper.eq(FtbCultivatePositionIdentifyResult::getUserId, userId); + identityResultWraper.eq(FtbCultivatePositionIdentifyResult::getType, 2); + identityResultWraper.eq(FtbCultivatePositionIdentifyResult::getIdentifyId, courseIdentity.getIdentityId()); + identityResultWraper.eq(FtbCultivatePositionIdentifyResult::getEnabledMark, 0); + identityResultWraper.orderByDesc(FtbCultivatePositionIdentifyResult::getCreatorTime); + identityResultWraper.last("limit 1"); + FtbCultivatePositionIdentifyResult ftbCultivatePositionIdentifyResult = ftbCultivatePositionIdentifyResultMapper.selectOne(identityResultWraper); + if (null == ftbCultivatePositionIdentifyResult) { + log.error("课程的鉴定为触发{}", certEventDTO); + return; + } + //查询鉴定申请表 + CultivateIdentifyApply identifyApply = cultivateIdentifyApplyMapper.selectById(ftbCultivatePositionIdentifyResult.getIdentifyRecordId()); + if (identifyApply == null) { + log.error("课程鉴定申请表为null{}", certEventDTO); + return; + } + if (identifyApply.getStatus() != 1) { + log.error("课程鉴定申请表未鉴定{},", certEventDTO, identifyApply); + return; + } + + //鉴定结果(0-合格,1-优秀,2-不合格) + Integer result = identifyApply.getResult(); + // 颁发规则鉴定(0 合格, 1 优秀) + if (result == 2) { + log.error("课程关联鉴定不合格{},{}", certEventDTO, identifyApply); + return; + } + } + // 课程学习完成 + doCreateCertificate(userId, postId, certificate.getCertificateId(), reason, contentMessage); + + FtbCultivateCertificateResult cultivateCertificateResult = new FtbCultivateCertificateResult(); + cultivateCertificateResult.setUserId(certEventDTO.getUserId()); + cultivateCertificateResult.setCertificateId(certificate.getCertificateId()); + cultivateCertificateResult.setPostRankId(certEventDTO.getPostId()); + cultivateCertificateResult.setCourseId(certEventDTO.getCourseId()); + cultivateCertificateResult.setType(2);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + ftbCultivateCertificateResultMapper.insert(cultivateCertificateResult); + + } + + /** + * 任务颁发 + * + * @param certEventDTO + */ + private void taskIssuance(CertificateEventDTO certEventDTO) { + // 任务存在 + // 用户id + String userId = certEventDTO.getUserId(); + // 任务id + String taskId = certEventDTO.getTaskId(); + // 来源(必传),0无绑定考试和实操鉴定,1考试,2实操 + Integer source = certEventDTO.getSource(); + LambdaQueryWrapper taskQuery = Wrappers.lambdaQuery(); + taskQuery.eq(FtbCultivateLearnTask::getEnableMark, 0); + taskQuery.eq(SuperBaseEntity.SuperIBaseEntity::getId, taskId); + FtbCultivateLearnTask learnTask = learnTaskMapper.selectOne(taskQuery); + if (learnTask == null) return; + // 查询任务关联证书 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateLearnTaskCertificate::getTaskId, taskId); + FtbCultivateLearnTaskCertificate learnTaskCertificate = ftbCultivateLearnTaskCertificateMapper.selectOne(queryWrapper); + if (learnTaskCertificate == null) return; + // 证书是否下架及删除 + FtbCertificateEntity ftbCertificateEntity = ftbCultivateCertificateMapper.selectById(learnTaskCertificate.getCertificateId()); + if (ftbCertificateEntity.getStatus() == 0 || CourseEnums.EnabledMarkType.INVALID.getCode().equals(ftbCertificateEntity.getEnabledMark())) { + return; + } + + //查询是否颁发 + LambdaQueryWrapper certificateResultLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getUserId, certEventDTO.getUserId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getTaskId, certEventDTO.getTaskId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCertificateId, learnTaskCertificate.getCertificateId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getType, 3);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + FtbCultivateCertificateResult ftbCultivateCertificateResult = ftbCultivateCertificateResultMapper.selectOne(certificateResultLambdaQueryWrapper); + if (null != ftbCultivateCertificateResult) { + return; + } + // 查询任务绑定考试 + LambdaQueryWrapper learnTaskExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskExamLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, taskId); + FtbCultivateLearnTaskExam ftbCultivateLearnTaskExam = learnTaskExamMapper.selectOne(learnTaskExamLambdaQueryWrapper); + // 查询任务绑定实操鉴定 + LambdaQueryWrapper learnTaskIdentificationLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskIdentificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, taskId); + FtbCultivateLearnTaskIdentification learnTaskIdentification = learnTaskIdentificationMapper.selectOne(learnTaskIdentificationLambdaQueryWrapper); + // 绑定鉴定 + String reason = "任务学习自动颁发证书"; + String contentMessage = "您已完成【" + learnTask.getTaskName() + "】"; + // 任务颁发规则配置1.考试 合格或者优秀 + // 颁发规则考试(0 合格, 1 优秀) + Integer examRule = learnTaskCertificate.getExamRule(); + // 任务颁发规则配置2.鉴定 合格或者优秀 + Integer identificationRule = learnTaskCertificate.getIdentificationRule(); + if (source == 0) { + // 记录证书颁发 + recordCertificateIssuance(userId, taskId); + recordTaskComplete(userId, taskId); + doCreateCertificate(userId, null, learnTaskCertificate.getCertificateId(), reason, contentMessage); + } else { + boolean examFlag = false, identificationFlag = false; + boolean userTaskIsComplete = true;//true记录完成 false 不记录完成 + if (ftbCultivateLearnTaskExam == null) { + examFlag = true; + } else { + // 考试情况 + // 考试id + // 根据任务id加考试id+用户id查询当前人的考试情况 + String examId = ftbCultivateLearnTaskExam.getExamId(); + //-1 没有考试记录 0:未完成考试 3合格 5优秀 + Integer forTask = ftbCultivateExamUserService.queryExamResultForTask(taskId, userId); + if (examRule == 0 && (forTask == 3 || forTask == 5)) { + examFlag = true; + } else if (examRule == 1 && forTask == 5) { + examFlag = true; + } + if (ftbCultivateLearnTaskExam.getIsPassComplete() == 1 && !(forTask == 3 || forTask == 5)) { + userTaskIsComplete = false; + } + if (ftbCultivateLearnTaskExam.getIsPassComplete() == 0 && !(forTask == 3 || forTask == 5 || forTask == 4)) { + userTaskIsComplete = false; + } + } + // 不存在鉴定 + if (learnTaskIdentification == null) { + identificationFlag = true; + } else { + String identificationId = learnTaskIdentification.getIdentificationId(); + // 根据任务id加鉴定id+用户id查询当前人的鉴定情况 + LambdaQueryWrapper identifyApplyLambdaQueryWrapper = Wrappers.lambdaQuery(); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getSourceId, taskId); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getSource, 4); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getDeleteMark, 0); + CultivateIdentifyApply identifyApply = cultivateIdentifyApplyMapper.selectOne(identifyApplyLambdaQueryWrapper); + if (identifyApply == null) { + identificationFlag = false; + userTaskIsComplete = false; + } else { + if (identifyApply.getStatus() == 1) {//鉴定状态(0待鉴定,1已鉴定,2逾期未鉴定) + //鉴定结果(0-合格,1-优秀,2-不合格) + Integer result = identifyApply.getResult(); + // 颁发规则鉴定(0 合格, 1 优秀) + if (identificationRule == 0 && result != null && (result == 0 || result == 1)) { + identificationFlag = true; + } else if (identificationRule == 1 && result != null && result == 1) { + identificationFlag = true; + } + + if (learnTaskIdentification.getIsPassComplete() == 1 && !(result == 0 || result == 1)) { + userTaskIsComplete = false; + } + } else { + identificationFlag = false; + userTaskIsComplete = false; + } + } + + } + log.error("examFlag && identificationFlag={},{},certEventDTO=", examFlag, identificationFlag, certEventDTO); + // 满足条件颁发证书 + if (examFlag && identificationFlag) { + // 记录证书颁发 + recordCertificateIssuance(userId, taskId); + doCreateCertificate(userId, null, learnTaskCertificate.getCertificateId(), reason, contentMessage); + FtbCultivateCertificateResult cultivateCertificateResult = new FtbCultivateCertificateResult(); + cultivateCertificateResult.setUserId(certEventDTO.getUserId()); + cultivateCertificateResult.setCertificateId(learnTaskCertificate.getCertificateId()); + cultivateCertificateResult.setTaskId(certEventDTO.getTaskId()); + cultivateCertificateResult.setType(3);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + ftbCultivateCertificateResultMapper.insert(cultivateCertificateResult); + } + if (userTaskIsComplete) { + recordTaskComplete(userId, taskId); + } + } + + + } + + /** + * 记录证书颁发 + * + * @param userId + * @param taskId + */ + private void recordCertificateIssuance(String userId, String taskId) { + // 任务记录证书颁发 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateLearnTaskAssignment::getIssuedCertificate, 1); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + learnTaskAssignmentMapper.update(null, updateWrapper); + } + + /** + * 记录任务完成 + * + * @param userId + * @param taskId + */ + private void recordTaskComplete(String userId, String taskId) { + // 任务记录证书颁发 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateLearnTaskAssignment::getStudyStats, 2); + updateWrapper.set(FtbCultivateLearnTaskAssignment::getLearningEndTime, new Date()); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + learnTaskAssignmentMapper.update(null, updateWrapper); + } + + /** + * 证书颁发 + * + * @param userId 用户id + * @param postId 岗位id + * @param certificateId 证书id + * @param reason 原因 + * @param content 通知前缀 + */ + private void doCreateCertificate(String userId, + String postId, + String certificateId, + String reason, + String content) { + FtbCertificateEntity ftbCertificateEntity = ftbCultivateCertificateMapper.selectById(certificateId); + assert ftbCertificateEntity != null; + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCertificateUserEntity::getCertificateId, certificateId); + queryWrapper.eq(FtbCertificateUserEntity::getUserId, userId); + queryWrapper.eq(FtbCertificateUserEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + queryWrapper.last("limit 1"); + FtbCertificateUserEntity certificateUserEntity = ftbCultivateCertificateUserMapper.selectOne(queryWrapper); + //说明证书存在但是是 1已失效,2已吊销 就删除重新颁发 + if (certificateUserEntity != null && (certificateUserEntity.getStatus() == 1 || certificateUserEntity.getStatus() == 2)) { + certificateUserEntity.setEnabledMark(0); + ftbCultivateCertificateUserMapper.updateById(certificateUserEntity); + certificateUserEntity = null; + } + // 移除永久颁发跳过,进行原因覆盖 + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + FtbCertificateUserEntity ftbCertificateUserEntity = new FtbCertificateUserEntity(); + ftbCertificateUserEntity.setUserId(userId); + if (StringUtils.isNotEmpty(postId)) ftbCertificateUserEntity.setPositionId(postId); + ftbCertificateUserEntity.setUserName(userPrimaryBoundOne.getUserName()); + // 组织id,取花名册取默认组织id + + if (userPrimaryBoundOne != null) { + ftbCertificateUserEntity.setOrganizeId(userPrimaryBoundOne.getOrganizeId()); + } + ftbCertificateUserEntity.setReason(reason); + ftbCertificateUserEntity.setStatus(0); + // 前端要求屏蔽证书地址 + //ftbCertificateUserEntity.setImageUrl(ftbCertificateEntity.getTemplate()); + // 证书编号 + ftbCertificateUserEntity.setNumber(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.CERTIFICATE_ENCODING_PREFIX, 4)); + ftbCertificateUserEntity.setCertificateId(certificateId); + ftbCertificateUserEntity.setCertificateName(ftbCertificateEntity.getName()); + ftbCertificateUserEntity.setCompanyName(ftbCertificateEntity.getCompanyName()); + ftbCertificateUserEntity.setStatus(CourseEnums.CertUserStatus.USING.getCode()); + ftbCertificateUserEntity.setEffectTime(LocalDateTime.now()); + ftbCertificateUserEntity.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + Integer entityExpireTime = ftbCertificateEntity.getExpireTime(); + //计算过期日期 + if (certificateUserEntity != null) { + //说明是原来是永久无需更改时间 + if (certificateUserEntity.getExpireTime() == null) { + + } else { + //说明原来是有时间的 + if (!CourseEnums.CertExpireType.NEVER.getCode().equals(ftbCertificateEntity.getExpireType())) { + //设置失效时间 ExpireTime=0时 null表示永久 + Date expireTime = certificateUserEntity.getExpireTime(); + if (CourseEnums.CertExpireType.DAY.getCode().equals(ftbCertificateEntity.getExpireType())) { + //添加天数 + expireTime = DateUtil.offsetDay(expireTime, ftbCertificateEntity.getExpireTime()); + } else if (CourseEnums.CertExpireType.MONTH.getCode().equals(ftbCertificateEntity.getExpireType())) { + expireTime = DateUtil.offsetMonth(expireTime, ftbCertificateEntity.getExpireTime()); + } + ftbCertificateUserEntity.setExpireTime(expireTime); + } else { + //说明原来不是永久 但是现在是永久 不用设置日期 + + } + } + + ftbCertificateUserEntity.setId(certificateUserEntity.getId()); + ftbCultivateCertificateUserMapper.updateById(ftbCertificateUserEntity); + } else { + //如果没有 就直接颁发证书 + if (!CourseEnums.CertExpireType.NEVER.getCode().equals(ftbCertificateEntity.getExpireType())) { + //设置失效时间 ExpireTime=0时 null表示永久 + LocalDateTime expireTime = LocalDateTime.now(); + if (CourseEnums.CertExpireType.DAY.getCode().equals(ftbCertificateEntity.getExpireType())) { + expireTime = expireTime.plusDays(entityExpireTime.longValue()); + } else if (CourseEnums.CertExpireType.MONTH.getCode().equals(ftbCertificateEntity.getExpireType())) { + expireTime = expireTime.plusMonths(entityExpireTime.longValue()); + } + Date date = Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()); + ftbCertificateUserEntity.setExpireTime(date); + } + ftbCultivateCertificateUserMapper.insert(ftbCertificateUserEntity); + } + + String contentMessage = content + ",恭喜获得" + ftbCertificateEntity.getName() + "证书。您可前往“我的证书”模块查看更多证书!"; + cultivateImUtil.sendMessage(List.of(userId), UserProvider.getUser().getTenantId(), contentMessage); + } + + /** + * 岗位学习颁发 + * + * @param certEventDTO + */ + private void awardedForOnTheJobLearning(CertificateEventDTO certEventDTO) { + LambdaQueryWrapper positionCertificateQueryWrapper = Wrappers.lambdaQuery(); + positionCertificateQueryWrapper.select(FtbCultivatePositionCertificate::getCertificateId); + positionCertificateQueryWrapper.eq(FtbCultivatePositionCertificate::getPostRankId, certEventDTO.getPostId()); + positionCertificateQueryWrapper.eq(FtbCultivatePositionCertificate::getEnableMark, 0); + positionCertificateQueryWrapper.last("limit 1"); + FtbCultivatePositionCertificate certificateCertificate = ftbCultivatePositionCertificateMapper.selectOne(positionCertificateQueryWrapper); + // 该岗位未绑定证书 + if (certificateCertificate == null) { + return; + } + // 证书是否下架及删除 + FtbCertificateEntity ftbCertificateEntity = ftbCultivateCertificateMapper.selectById(certificateCertificate.getCertificateId()); + if (ftbCertificateEntity.getStatus() == 0 || CourseEnums.EnabledMarkType.INVALID.getCode().equals(ftbCertificateEntity.getEnabledMark())) { + return; + } + //查询是否颁发 + LambdaQueryWrapper certificateResultLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getUserId, certEventDTO.getUserId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getPostRankId, certEventDTO.getPostId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCertificateId, certificateCertificate.getCertificateId()); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getType, 1);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + FtbCultivateCertificateResult ftbCultivateCertificateResult = ftbCultivateCertificateResultMapper.selectOne(certificateResultLambdaQueryWrapper); + if (null != ftbCultivateCertificateResult) { + return; + } + + // 重复颁发+失效时间 + String userId = certEventDTO.getUserId(); + String reason = "岗位学习自动颁发证书"; + // 来源0无绑定考试和实操鉴定,1考试,2实操 + String postId = certEventDTO.getPostId(); +// PositionEntity positionEntity = personnelOrgUtils.queryPosition(postId); + PositionVO positionEntity = userApiV2Util.infoPosition(postId, null); + if(positionEntity==null){ + positionEntity = new PositionVO(); + } + String content = "您已完成【" + positionEntity.getFullName() + "】学习"; + if (certEventDTO.getSource() == 0) { + doCreateCertificate(userId, postId, certificateCertificate.getCertificateId(), reason, content); + recodePositionResult(certEventDTO, certificateCertificate); + } else if (certEventDTO.getSource() == 1) { + // 根据人去查询是否有鉴定 + List applyIds = ftbCultivatePromotionNewMapper.checkWhetherThereIsAPracticalAppraisalWithNew(certEventDTO.getPostId(), userId); + // 未绑定实操鉴定直接颁发,后添加的去app界面查询时触发 + if (CollUtil.isEmpty(applyIds)) { + doCreateCertificate(userId, postId, certificateCertificate.getCertificateId(), reason, content); + recodePositionResult(certEventDTO, certificateCertificate); + return; + } + // 实操鉴定是否合格 + Integer qualified = ftbCultivatePromotionNewMapper.queryTheNumberOfPracticalAppraisalStudies(List.of(certEventDTO.getPostId()) + , userId, applyIds, 1); + if (qualified > 0) { + doCreateCertificate(userId, postId, certificateCertificate.getCertificateId(), reason, content); + recodePositionResult(certEventDTO, certificateCertificate); + } + } else if (certEventDTO.getSource() == 2) { + // 未绑定考试直接颁发,后添加的去app界面查询时触发 + String examId = ftbCultivatePositionCertificateMapper.queryJobPracticalExamByPostId(certEventDTO.getPostId()); + if (StrUtil.isBlank(examId)) { + doCreateCertificate(userId, postId, certificateCertificate.getCertificateId(), reason, content); + recodePositionResult(certEventDTO, certificateCertificate); + return; + } + // 考试是否合格 + Integer qualified = ftbCultivateExamUserService.queryExamIsCompleteForUserIdAndPostion(userId, + certEventDTO.getPostId(), 1); + if (qualified == 1) { + doCreateCertificate(userId, postId, certificateCertificate.getCertificateId(), reason, content); + recodePositionResult(certEventDTO, certificateCertificate); + } + } + } + + /** + * 记录证书颁发结果幂等性考虑 + * + * @param certEventDTO + * @param certificateCertificate + */ + private void recodePositionResult(CertificateEventDTO certEventDTO, FtbCultivatePositionCertificate certificateCertificate) { + FtbCultivateCertificateResult cultivateCertificateResult = new FtbCultivateCertificateResult(); + cultivateCertificateResult.setUserId(certEventDTO.getUserId()); + cultivateCertificateResult.setCertificateId(certificateCertificate.getCertificateId()); + cultivateCertificateResult.setPostRankId(certEventDTO.getPostId()); + cultivateCertificateResult.setType(1);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + ftbCultivateCertificateResultMapper.insert(cultivateCertificateResult); + } + + /** + * 将两个 Date 对象相加 + * + * @param date1 第一个日期 + * @param date2 第二个日期 + * @return 相加后的新日期 + */ + public Date addDates(Date date1, Date date2) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date1); // 设置为 date1 的时间 + // 将 date2 的时间差加到 calendar 上 + long timeInMillis = date2.getTime(); + calendar.add(Calendar.MILLISECOND, (int) (timeInMillis / 1000)); + + return calendar.getTime(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventCourseService.java new file mode 100644 index 0000000..f40056d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventCourseService.java @@ -0,0 +1,228 @@ +package jnpf.cultivate.event.impl.course; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.JnpfApplicationEventService; +import jnpf.cultivate.mapper.FtbCultivateCourseChapterMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.service.CultivateCourseMsgService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourceChapterLearningService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourceLearningService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.event.dto.course.CourseEventDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsg; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.enums.CourseEnums; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class JnpfApplicationEventCourseService implements JnpfApplicationEventService { + + private final FtbCultivatePositionCourceLearningService ftbCultivatePositionCourceLearningService; + private final FtbCultivatePositionCourceChapterLearningService ftbCultivatePositionCourceChapterLearningService; + private final FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + private final FtbCultivateCourseMapper ftbCultivateCourseMapper; + private final CultivateCourseMsgService cultivateCourseMsgService; + + + private static final ReentrantLock lock = new ReentrantLock(); + + + @Override + public boolean isSupportedCourseEvent(Object event) { + return event instanceof CourseEventDTO; + } + + @Override + public void handlerCourseEvent(Object courseEvent) { + CourseEventDTO courseEventDTO = (CourseEventDTO) courseEvent; + if (CollUtil.isEmpty(courseEventDTO.getCourseIds())) { + return; + } + List userIds = courseEventDTO.getUserIds(); + if (CollUtil.isNotEmpty(userIds)) { + userIds = UserApiV2Util.uniqueStringList(userIds); + courseEventDTO.setUserIds(userIds); + } + if(CollUtil.isNotEmpty(courseEventDTO.getCourseIds())){ + courseEventDTO.setCourseIds(UserApiV2Util.uniqueStringList(courseEventDTO.getCourseIds())); + } + try { + // 课程并发操作数据库造成数据不一致,加锁 + lock.lock(); + if (courseEventDTO.getWhetherToChange()) { + // 变更章节去进行重新学习 + if (CollectionUtils.isEmpty(userIds)) { + // 课程是否勾选已学习的才进行 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateCourse::getId); + queryWrapper.in(FtbCultivateCourse::getId, courseEventDTO.getCourseIds()); + queryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + queryWrapper.eq(FtbCultivateCourse::getCourseUpdate, 1); + List oldCourses = ftbCultivateCourseMapper.selectList(queryWrapper); + List courseIds = oldCourses.stream() + .map(FtbCultivateCourse::getId).collect(Collectors.toList()); + // 原始值替换 + courseEventDTO.setCourseIds(courseIds); + } + if (CollUtil.isNotEmpty(courseEventDTO.getCourseIds())) { + // 课程变更,导致所有学习课程重新学一遍 + LambdaQueryWrapper positionQueryWrapper = Wrappers.lambdaQuery(); + positionQueryWrapper.in(FtbCultivatePositionCourceLearning::getCourceId, courseEventDTO.getCourseIds()); + // 有用户则为学习地图清空,无则为变更课程章节 + positionQueryWrapper.in(CollectionUtils.isNotEmpty(userIds), FtbCultivatePositionCourceLearning::getUserId, userIds); + List oldPositionCourceLearnings = ftbCultivatePositionCourceLearningService.list(positionQueryWrapper); + if (CollUtil.isNotEmpty(oldPositionCourceLearnings)) { + userIds = oldPositionCourceLearnings.stream() + .map(FtbCultivatePositionCourceLearning::getUserId) + .distinct() + .collect(Collectors.toList()); + // 课程学习清空 + ftbCultivatePositionCourceLearningService.remove(positionQueryWrapper); + // 课程章节学习清空 + LambdaQueryWrapper chapterQueryWrapper = Wrappers.lambdaQuery(); + chapterQueryWrapper.in(FtbCultivatePositionCourceChapterLearning::getCourceId, courseEventDTO.getCourseIds()); + // 有用户则为学习地图清空,无则为变更课程章节 + chapterQueryWrapper.in(CollectionUtils.isNotEmpty(userIds), FtbCultivatePositionCourceChapterLearning::getUserId, userIds); + ftbCultivatePositionCourceChapterLearningService.remove(chapterQueryWrapper); + // 课程变更提醒 + LambdaQueryWrapper courseQueryWrapper = Wrappers.lambdaQuery(); + List strings = oldPositionCourceLearnings.stream() + .map(FtbCultivatePositionCourceLearning::getCourceId) + .collect(Collectors.toList()); + courseQueryWrapper.in(FtbCultivateCourse::getId, strings); + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(courseQueryWrapper); + Map collect = ftbCultivateCourses.stream() + .collect(Collectors.toMap(FtbCultivateCourse::getId, k -> k, (a, b) -> a)); + List cultivateCourseMsgs = oldPositionCourceLearnings.stream() + .map(positionCourceLearning -> { + FtbCultivateCourse ftbCultivateCourse = collect.getOrDefault(positionCourceLearning.getCourceId(), null); + // 课程内容更新需要重新学习 + if (Objects.nonNull(ftbCultivateCourse) && ftbCultivateCourse.getCourseUpdate() == 1) { + return positionCourceLearning.getState() == 0 ? null + : getCultivateCourseMsg(positionCourceLearning, ftbCultivateCourse); + } + return null; + }).filter(Objects::nonNull).collect(Collectors.toList()); + cultivateCourseMsgService.saveBatch(cultivateCourseMsgs); + } + } + } + // 课程进度,参与培训所有人员需要学习的课程,线下培训课程需要和岗位学习课程同步 + List ftbCultivatePositionCourceLearnings = new ArrayList<>(); + List ftbCultivatePositionCourceChapterLearnings = new ArrayList<>(); + userIds.forEach(userId -> { + courseEventDTO.getCourseIds().forEach(courseId -> { + if (courseEventDTO.getWhetherToChange()) { + studyCourseAssembly(userId, courseId, + ftbCultivatePositionCourceLearnings, ftbCultivatePositionCourceChapterLearnings); + return; + } + // 课程是否已学习 + LambdaQueryWrapper positionQueryWrapper = Wrappers.lambdaQuery(); + positionQueryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + positionQueryWrapper.eq(FtbCultivatePositionCourceLearning::getState, 1); + positionQueryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId); + if (ftbCultivatePositionCourceLearningService.count(positionQueryWrapper) == 0) { + studyCourseAssembly(userId, courseId, + ftbCultivatePositionCourceLearnings, ftbCultivatePositionCourceChapterLearnings); + } + }); + }); + // 课程进度和课程章节 + if(CollUtil.isNotEmpty(ftbCultivatePositionCourceLearnings)) { + ftbCultivatePositionCourceLearningService.saveBatch(ftbCultivatePositionCourceLearnings); + } + if(CollUtil.isNotEmpty(ftbCultivatePositionCourceChapterLearnings)) { + ftbCultivatePositionCourceChapterLearningService.saveBatch(ftbCultivatePositionCourceChapterLearnings); + } + } finally { + if (lock.isLocked()) { + lock.unlock(); + } + } + } + + @NotNull + private static CultivateCourseMsg getCultivateCourseMsg(FtbCultivatePositionCourceLearning positionCourceLearning, + FtbCultivateCourse ftbCultivateCourse) { + CultivateCourseMsg cultivateCourseMsg = new CultivateCourseMsg(); + if (positionCourceLearning.getState() == 1) { + cultivateCourseMsg.setDesc("您正在学习的课程" + ftbCultivateCourse.getName() + "有更新,请重新学习"); + } else if (positionCourceLearning.getState() == 2) { + cultivateCourseMsg.setDesc("您已学习过的课程" + ftbCultivateCourse.getName() + "学习内容有更新,您可以重新学习"); + } + cultivateCourseMsg.setCourseId(ftbCultivateCourse.getId()); + return cultivateCourseMsg; + } + + /** + * 学习课程组装 + * + * @param userId 用户 ID + * @param courseId 课程 ID + * @param ftbCultivatePositionCourceLearnings FtbCultivatePositionCourceLearning 对象的列表,用于存储学习课程信息 + * @param ftbCultivatePositionCourceChapterLearnings FtbCultivatePositionCourceChapterLearning 对象的列表,用于存储学习课程章节信息 + */ + private void studyCourseAssembly(String userId, String courseId, + List ftbCultivatePositionCourceLearnings, + List ftbCultivatePositionCourceChapterLearnings) { + + //查询用户课程进度,如果已存在则不继续添加课程进度 + QueryWrapper postionCourseLearningQueryWrapper = new QueryWrapper<>(); + postionCourseLearningQueryWrapper.lambda() + .eq(FtbCultivatePositionCourceLearning::getCourceId, courseId) + .eq(FtbCultivatePositionCourceLearning::getUserId, userId); + List isExist = ftbCultivatePositionCourceLearningService.getBaseMapper().selectList(postionCourseLearningQueryWrapper); + if (CollectionUtil.isEmpty(isExist)) { + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = new FtbCultivatePositionCourceLearning(); + ftbCultivatePositionCourceLearning.setCourceId(courseId); + ftbCultivatePositionCourceLearning.setUserId(userId); + ftbCultivatePositionCourceLearnings.add(ftbCultivatePositionCourceLearning); + } + + // 课程章节 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId); + queryWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + List ftbCultivateCourseChapters = ftbCultivateCourseChapterMapper.selectList(queryWrapper); + QueryWrapper learnerQueryWrapper = new QueryWrapper<>(); + learnerQueryWrapper.lambda() + .eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId) + .eq(FtbCultivatePositionCourceChapterLearning::getCourceId, courseId) + .eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List learnList = ftbCultivatePositionCourceChapterLearningService.list(learnerQueryWrapper); + List chapterIds = new ArrayList<>(); + if(CollUtil.isNotEmpty(learnList)) { + chapterIds = learnList.stream().map(FtbCultivatePositionCourceChapterLearning::getChapterId).collect(Collectors.toList()); + } + for (FtbCultivateCourseChapter ftbCultivateCourseChapter : ftbCultivateCourseChapters) { + if(chapterIds.contains(ftbCultivateCourseChapter.getId())){ + continue; + } + FtbCultivatePositionCourceChapterLearning ftbCultivatePositionCourceChapterLearning = new FtbCultivatePositionCourceChapterLearning(); + ftbCultivatePositionCourceChapterLearning.setUserId(userId); + ftbCultivatePositionCourceChapterLearning.setChapterId(ftbCultivateCourseChapter.getId()); + ftbCultivatePositionCourceChapterLearning.setCourceId(courseId); + ftbCultivatePositionCourceChapterLearnings.add(ftbCultivatePositionCourceChapterLearning); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventPositionCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventPositionCourseService.java new file mode 100644 index 0000000..7bc20be --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/course/JnpfApplicationEventPositionCourseService.java @@ -0,0 +1,72 @@ +package jnpf.cultivate.event.impl.course; + +import cn.hutool.core.collection.CollUtil; +import jnpf.cultivate.event.JnpfApplicationEventService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.event.dto.course.CourseEventDTO; +import jnpf.model.cultivate.event.dto.course.PositionCourseEventDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class JnpfApplicationEventPositionCourseService implements JnpfApplicationEventService { + + private final FtbCultivatePromotionNewService ftbCultivatePromotionNewService; + private final JnpfApplicationEventCourseService jnpfApplicationEventCourseService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public boolean isSupportedCourseEvent(Object event) { + return event instanceof PositionCourseEventDTO; + } + + @Override + public void handlerCourseEvent(Object courseEvent) { + PositionCourseEventDTO positionCourseEventDTO = (PositionCourseEventDTO) courseEvent; + List userIds = null; + // 确定课程学习人员 + if (positionCourseEventDTO.getEventType() == PositionCourseEventDTO.EventType.USER) { + userIds = positionCourseEventDTO.getUserIds(); + } else if (positionCourseEventDTO.getEventType() == PositionCourseEventDTO.EventType.POST) { + // 查询该岗位下所有用户 + + List userBaseInfoByPositions = userApiV2Util.getUserListForPositions(positionCourseEventDTO.getPostIds(),null); + // 根据岗位id获取其所有用户,本岗课程 + userIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(userBaseInfoByPositions)) { + userIds = userBaseInfoByPositions.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + // 学习地图选择了此岗位且该岗位所对应的学习阶段未完成的用户 + List unfinishedUser = ftbCultivatePromotionNewService.querySelectedPersonnelBasedOnPositionID(positionCourseEventDTO.getPostIds()); + userIds.addAll(unfinishedUser); + userIds = userIds.stream().distinct().collect(Collectors.toList()); + } else if (positionCourseEventDTO.getEventType() == PositionCourseEventDTO.EventType.ORGANIZATION) { + // 组织 + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(positionCourseEventDTO.getOrganizationIds(),null); + if (CollUtil.isNotEmpty(userListForOrgIds)) { + userIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + } + } + + // 课程进度,参与培训所有人员需要学习的课程,线下培训课程需要和岗位学习课程同步 + jnpfApplicationEventCourseService.handlerCourseEvent( + CourseEventDTO.builder() + .courseIds(positionCourseEventDTO.getCourseIds()) + .userIds(userIds) + .whetherToChange(positionCourseEventDTO.getWhetherToChange()) + .build()); + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/file/JnpfApplicationEventFileServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/file/JnpfApplicationEventFileServiceImpl.java new file mode 100644 index 0000000..984fc6a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/file/JnpfApplicationEventFileServiceImpl.java @@ -0,0 +1,53 @@ +package jnpf.cultivate.event.impl.file; + +import cn.hutool.core.collection.CollUtil; +import jnpf.cultivate.event.JnpfApplicationEventService; +import jnpf.cultivate.service.impl.FtbCultivateFileService; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Title:JnpfApplicationEventFileServiceImpl + * @Author:peng.hao + * @create: 2023/12/2618:01 + */ +@Service +public class JnpfApplicationEventFileServiceImpl implements JnpfApplicationEventService { + + @Resource + private FtbCultivateFileService ftbCultivateFileService; + + @Override + public boolean isSupportedCourseEvent(Object event) { + return event instanceof FileEventDTO; + } + + @Override + public void handlerCourseEvent(Object object) { + FileEventDTO fileEventDTO = (FileEventDTO) object; + // 附件文件 + if (CollUtil.isNotEmpty(fileEventDTO.getFiles())) { + List ftbCultivateFiles = fileEventDTO.getFiles() + .stream() + .map(ftbCultivateFileDTO -> { + FtbCultivateFile ftbCultivateFile = new FtbCultivateFile(); + ftbCultivateFile.setFileName(ftbCultivateFileDTO.getName()); + ftbCultivateFile.setUrl(ftbCultivateFileDTO.getUrl()); + ftbCultivateFile.setExtension(ftbCultivateFileDTO.getFileExtension()); + ftbCultivateFile.setSize(ftbCultivateFileDTO.getFileSize()); + ftbCultivateFile.setType(fileEventDTO.getType().getType()); + ftbCultivateFile.setBusinessId(fileEventDTO.getBusinessTypeID()); + ftbCultivateFile.setVideoActionType(ftbCultivateFileDTO.getVideoActionType()); + ftbCultivateFile.setDuration(ftbCultivateFileDTO.getDuration()); + ftbCultivateFile.setEnabledMark(0); + return ftbCultivateFile; + }).collect(Collectors.toList()); + ftbCultivateFileService.saveBatch(ftbCultivateFiles); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/course/CommonCourseProcessLearnProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/course/CommonCourseProcessLearnProcessor.java new file mode 100644 index 0000000..939dcb1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/course/CommonCourseProcessLearnProcessor.java @@ -0,0 +1,31 @@ +package jnpf.cultivate.event.impl.trigger.course; + +import jnpf.cultivate.event.base.TriggerProcessor; +import jnpf.cultivate.event.base.course.CourseProcessLearnAbstract; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.event.dto.course.CommonCourseProcessLearnDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CommonCourseProcessLearnProcessor extends CourseProcessLearnAbstract implements TriggerProcessor { + + @Override + public boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType) { + return triggerEventType == TriggerEventBO.TriggerEventType.COMMON_COURSE; + } + + @Override + public void triggerHandle(CommonCourseProcessLearnDTO triggerEvent) { + super.courseProcessLearn(triggerEvent); + } + + @Override + public void courseLearnCompleteHandle(CommonCourseProcessLearnDTO baseData) { + // ignore 通用课程学习完成后不做任何处理 + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/distribute/JnpfApplicationEventExamIdentificationTrigger.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/distribute/JnpfApplicationEventExamIdentificationTrigger.java new file mode 100644 index 0000000..69c5eaf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/distribute/JnpfApplicationEventExamIdentificationTrigger.java @@ -0,0 +1,40 @@ +package jnpf.cultivate.event.impl.trigger.distribute; + +import jnpf.cultivate.event.JnpfApplicationEventService; +import jnpf.cultivate.event.base.TriggerProcessor; +import jnpf.model.cultivate.bo.TriggerEventBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +@Component +@Slf4j +@SuppressWarnings("unchecked") +public class JnpfApplicationEventExamIdentificationTrigger implements JnpfApplicationEventService { + + @Resource + private List triggerProcessors; + + + @Override + public boolean isSupportedCourseEvent(Object event) { + return event instanceof TriggerEventBO; + } + + @Override + public void handlerCourseEvent(Object courseEvent) { + TriggerEventBO triggerEventDTO = (TriggerEventBO) courseEvent; + // 事件处理 + triggerProcessors.forEach(triggerProcessor -> { + if (triggerProcessor.triggerTypeConfirmation(triggerEventDTO.getTriggerEventType())) { + triggerProcessor.triggerHandle(triggerEventDTO); + } + }); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/CourseTriggerProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/CourseTriggerProcessor.java new file mode 100644 index 0000000..d8118a6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/CourseTriggerProcessor.java @@ -0,0 +1,141 @@ +package jnpf.cultivate.event.impl.trigger.position; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.base.examidentify.CoursePositionTaskTriggerAbstract; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseExamMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseIdentityMapper; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.dto.identify.IdentifyApplyDataPushDto; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.cultivate.resp.TriggerExamDto; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 岗位学习课程触发器处理器 + * + * @author wangchunxiang + * @date 2024/09/14 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class CourseTriggerProcessor extends CoursePositionTaskTriggerAbstract { + + private final FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + private final FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + + @Override + public boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType) { + return triggerEventType == TriggerEventBO.TriggerEventType.POSITION_COURSE; + } + + + @Override + public void triggerExamHandle(TriggerExamDto triggerExamDto) { + triggerExamDto.setExamSource(1); + } + + @Override + public void identifyApplyDataHandle(IdentifyApplyDataPushDto identifyApplyDataPushDto, CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger practicalIdentificationTrigger) { + identifyApplyDataPushDto.setSource(1); + } + + @Override + public void positionIdentifyDataHandle(FtbCultivatePositionIdentifyResult ftbCultivatePositionIdentifyResult) { + ftbCultivatePositionIdentifyResult.setType(2); + } + + @Override + public TriggerExamDto buildTriggerExamDto(ExamIdentifyTriggerEventDTO triggerEvent) { + // 发起课程考试 + LambdaQueryWrapper courseExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getCourseId, triggerEvent.getCourseId()); + courseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getPostRankId, triggerEvent.getPostId()); + courseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + courseExamLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectOne(courseExamLambdaQueryWrapper); + log.info("忽略此日志,发起课程学习考试参数:{}", ftbCultivatePositionCourseExam); + if (Objects.nonNull(ftbCultivatePositionCourseExam)) { + return super.handleExamTrigger(triggerEvent.getUserId(), ftbCultivatePositionCourseExam.getExamId(), + ftbCultivatePositionCourseExam.getPostRankId(), ftbCultivatePositionCourseExam.getId()); + } + return null; + } + + @Override + public IdentifyApplyDataPushDto buildIdentifyApplyDataPushDto(ExamIdentifyTriggerEventDTO triggerEvent) { + // 发起课程实操鉴定 + LambdaQueryWrapper courseIdentityLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getCourseId, triggerEvent.getCourseId()); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostRankId, triggerEvent.getPostId()); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + courseIdentityLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourseIdentity ftbCultivatePositionCourseIdentity = ftbCultivatePositionCourseIdentityMapper.selectOne(courseIdentityLambdaQueryWrapper); + log.info("忽略此日志,发起课程学习实操鉴定参数:{}", ftbCultivatePositionCourseIdentity); + if (Objects.nonNull(ftbCultivatePositionCourseIdentity)) { + CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger practicalIdentificationTrigger = CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger.builder() + .userId(triggerEvent.getUserId()) + .postId(triggerEvent.getPostId()) + .identityId(ftbCultivatePositionCourseIdentity.getIdentityId()) + .postLearnId(ftbCultivatePositionCourseIdentity.getPostLearnId()) + .courseId(triggerEvent.getCourseId()) + .type(2) + .build(); + return super.handlePracticalIdentification(practicalIdentificationTrigger); + } + return null; + } + + @Override + public boolean isHasIndetify(ExamIdentifyTriggerEventDTO triggerEvent) { + // 发起课程实操鉴定 + LambdaQueryWrapper courseIdentityLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getCourseId, triggerEvent.getCourseId()); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostRankId, triggerEvent.getPostId()); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + courseIdentityLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourseIdentity ftbCultivatePositionCourseIdentity = ftbCultivatePositionCourseIdentityMapper.selectOne(courseIdentityLambdaQueryWrapper); + log.info("忽略此日志,发起课程学习实操鉴定参数:{}", ftbCultivatePositionCourseIdentity); + if (Objects.nonNull(ftbCultivatePositionCourseIdentity)) { + return true; + } + return false; + } + + @Override + public void postProcessing(int i, ExamIdentifyTriggerEventDTO triggerEvent) { + // 无实操鉴定和考试,直接颁发课程证书 + if (((i & EXAM_NOT_TRIGGER) == EXAM_NOT_TRIGGER) && ((i & IDENTIFY_NOT_TRIGGER) == IDENTIFY_NOT_TRIGGER)) { + // 颁发证书 0 课程证书 1 岗位学习证书, 2 任务证书 + Integer issuanceType=0; +// if (StringUtils.isNotEmpty(triggerEvent.getPostId())) { +// issuanceType=1; +// } + if (StringUtils.isNotEmpty(triggerEvent.getTaskId())) { + issuanceType=2; + } + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(0) + .courseId(triggerEvent.getCourseId()) + .issuanceType(issuanceType) + .postId(triggerEvent.getPostId()) + .userId(triggerEvent.getUserId()) + .taskId(triggerEvent.getTaskId()) + .build())); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionProcessLearnProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionProcessLearnProcessor.java new file mode 100644 index 0000000..c1875db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionProcessLearnProcessor.java @@ -0,0 +1,94 @@ +package jnpf.cultivate.event.impl.trigger.position; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.base.TriggerProcessor; +import jnpf.cultivate.event.base.course.CourseProcessLearnAbstract; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseMapper; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.event.dto.position.PositionCourseProcessDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class PositionProcessLearnProcessor extends CourseProcessLearnAbstract implements TriggerProcessor { + + private final FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + private final FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Override + public boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType) { + return triggerEventType == TriggerEventBO.TriggerEventType.POSITION_COURSE_EVENT; + } + + @Override + public void triggerHandle(PositionCourseProcessDTO triggerEvent) { + super.courseProcessLearn(triggerEvent); + // 本岗位必修课程学习是否完成 + LambdaQueryWrapper ftbCultivatePositionCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionCourseLambdaQueryWrapper.select(FtbCultivatePositionCourse::getCourseId); + ftbCultivatePositionCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getPostRankId, triggerEvent.getPostId()); + ftbCultivatePositionCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + ftbCultivatePositionCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getCompulsory, 0); + List ftbCultivatePositionCourses = ftbCultivatePositionCourseMapper.selectList(ftbCultivatePositionCourseLambdaQueryWrapper); + List courseIds = ftbCultivatePositionCourses.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList()); + //岗位学习的课程必须有一个必学 + if (CollUtil.isEmpty(courseIds)) { + log.info("本岗位课程被删除,不触发岗位考试和岗位实操鉴定,参数为:{}", triggerEvent); + return; + } + + //找到上架的岗位学习课程 + QueryWrapper courseQuery = new QueryWrapper<>(); + courseQuery.lambda() + .in(FtbCultivateCourse::getId, courseIds) + .eq(FtbCultivateCourse::getEnableMark, 0) + .eq(FtbCultivateCourse::getIsGroundIng, 1); + List courseList = ftbCultivateCourseMapper.selectList(courseQuery); + if (CollectionUtil.isEmpty(courseList)) { + return; + } + courseIds = courseList.stream().map(FtbCultivateCourse::getId).collect(Collectors.toList()); + + + boolean b = super.hasTheCourseBeenCompleted(triggerEvent.getUserId(), courseIds); + log.error("courseIds:{},userId={},b={}", courseIds, triggerEvent.getUserId(), b); + if (b) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + ExamIdentifyTriggerEventDTO.builder() + .postId(triggerEvent.getPostId()) + .userId(triggerEvent.getUserId()) + .triggerEventType(TriggerEventBO.TriggerEventType.POST) + .build())); + } + } + + @Override + public void courseLearnCompleteHandle(PositionCourseProcessDTO baseData) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + ExamIdentifyTriggerEventDTO.builder() + .courseId(baseData.getCourseId()) + .postId(baseData.getPostId()) + .userId(baseData.getUserId()) + .triggerEventType(TriggerEventBO.TriggerEventType.POSITION_COURSE) + .build())); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionTriggerProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionTriggerProcessor.java new file mode 100644 index 0000000..346cacf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/position/PositionTriggerProcessor.java @@ -0,0 +1,153 @@ +package jnpf.cultivate.event.impl.trigger.position; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.base.examidentify.CoursePositionTaskTriggerAbstract; +import jnpf.cultivate.mapper.FtbCultivatePositionExamIdentifyMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionExamMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionIdentifyResultMapper; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.dto.identify.IdentifyApplyDataPushDto; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.cultivate.resp.TriggerExamDto; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 岗位学习考试鉴定触发处理器 + * + * @author wangchunxiang + * @date 2024/09/14 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class PositionTriggerProcessor extends CoursePositionTaskTriggerAbstract { + + private final FtbCultivatePositionExamIdentifyMapper ftbCultivatePositionExamIdentifyMapper; + private final FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + private final FtbCultivatePromotionNewService ftbCultivatePromotionNewService; + + private final FtbCultivatePositionIdentifyResultMapper ftbCultivatePositionIdentifyResultMapper; + + @Override + public void triggerExamHandle(TriggerExamDto triggerExamDto) { + triggerExamDto.setExamSource(2); + } + + @Override + public void identifyApplyDataHandle(IdentifyApplyDataPushDto identifyApplyDataPushDto, CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger practicalIdentificationTrigger) { + identifyApplyDataPushDto.setSource(2); + } + + @Override + public void positionIdentifyDataHandle(FtbCultivatePositionIdentifyResult ftbCultivatePositionIdentifyResult) { + ftbCultivatePositionIdentifyResult.setType(0); + } + + @Override + public void postProcessing(int i, ExamIdentifyTriggerEventDTO triggerEvent) { + // 学习地图标识改岗位学习完成计算学习阶段 + ftbCultivatePromotionNewService.changeTheCurrentPersonSLearningStage(triggerEvent.getUserId(), triggerEvent.getPostId()); + + // 无实操鉴定和考试,直接颁发岗位学习证书 + if (((i & EXAM_NOT_TRIGGER) == EXAM_NOT_TRIGGER) && ((i & IDENTIFY_NOT_TRIGGER) == IDENTIFY_NOT_TRIGGER)) { + // 颁发证书 + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(0) + .postId(triggerEvent.getPostId()) + .issuanceType(1) + .userId(triggerEvent.getUserId()) + .build())); + } + + } + + @Override + public boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType) { + return triggerEventType == TriggerEventBO.TriggerEventType.POST; + } + + @Override + public TriggerExamDto buildTriggerExamDto(ExamIdentifyTriggerEventDTO triggerEvent) { + String postId = triggerEvent.getPostId(); + String userId = triggerEvent.getUserId(); + // 发起岗位考试 + LambdaQueryWrapper ftbCultivatePositionExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionExamLambdaQueryWrapper.eq(FtbCultivatePositionExam::getPostRankId, postId); + ftbCultivatePositionExamLambdaQueryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + ftbCultivatePositionExamLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionExam ftbCultivatePositionExam = ftbCultivatePositionExamMapper.selectOne(ftbCultivatePositionExamLambdaQueryWrapper); + log.info("课程列表页调用触发忽略此日志,发起岗位学习考试参数:{}", ftbCultivatePositionExam); + TriggerExamDto triggerExamDto = null; + if (Objects.nonNull(ftbCultivatePositionExam)) { + triggerExamDto = super.handleExamTrigger(userId, ftbCultivatePositionExam.getExamId() + , ftbCultivatePositionExam.getPostRankId(), ftbCultivatePositionExam.getId()); + } + return triggerExamDto; + } + + @Override + public IdentifyApplyDataPushDto buildIdentifyApplyDataPushDto(ExamIdentifyTriggerEventDTO triggerEvent) { + String postId = triggerEvent.getPostId(); + String userId = triggerEvent.getUserId(); + // 发起岗位实操鉴定 + LambdaQueryWrapper examIdentifyLambdaQueryWrapper = Wrappers.lambdaQuery(); + examIdentifyLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getPostRankId, postId); + examIdentifyLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + examIdentifyLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionExamIdentify ftbCultivatePositionExamIdentify = ftbCultivatePositionExamIdentifyMapper.selectOne(examIdentifyLambdaQueryWrapper); + log.info("课程列表页调用触发忽略此日志,发起岗位学习实操鉴定参数:{}", ftbCultivatePositionExamIdentify); + if (Objects.nonNull(ftbCultivatePositionExamIdentify)) { + // 岗位考试合格后才能进行鉴定 + Integer qualified = super.doJobQualificationTrigger(postId); + CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger practicalIdentificationTrigger = CoursePositionTaskTriggerAbstract.PracticalIdentificationTrigger.builder() + .userId(userId) + .postId(postId) + .identityId(ftbCultivatePositionExamIdentify.getIdentifyId()) + .postLearnId(ftbCultivatePositionExamIdentify.getPostLearnId()) + .isTurnOnVisibility(qualified == 0 ? 0 : 1) + .type(0) + .build(); + return super.handlePracticalIdentification(practicalIdentificationTrigger); + } + return null; + } + + /** + * 是否有鉴定 + * @param triggerEvent + * @return + */ + @Override + public boolean isHasIndetify(ExamIdentifyTriggerEventDTO triggerEvent) { + String postId = triggerEvent.getPostId(); + String userId = triggerEvent.getUserId(); + // 发起岗位实操鉴定 + LambdaQueryWrapper examIdentifyLambdaQueryWrapper = Wrappers.lambdaQuery(); + examIdentifyLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getPostRankId, postId); + examIdentifyLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + examIdentifyLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionExamIdentify ftbCultivatePositionExamIdentify = ftbCultivatePositionExamIdentifyMapper.selectOne(examIdentifyLambdaQueryWrapper); + log.info("课程列表页调用触发忽略此日志,发起岗位学习实操鉴定参数:{}", ftbCultivatePositionExamIdentify); + if (Objects.nonNull(ftbCultivatePositionExamIdentify)) { + return true; + } + return false; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskProcessLearnProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskProcessLearnProcessor.java new file mode 100644 index 0000000..8340c18 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskProcessLearnProcessor.java @@ -0,0 +1,94 @@ +package jnpf.cultivate.event.impl.trigger.task; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.base.TriggerProcessor; +import jnpf.cultivate.event.base.course.CourseProcessLearnAbstract; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskCourseMapper; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.event.dto.task.TaskCourseProcessDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCourse; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TaskProcessLearnProcessor extends CourseProcessLearnAbstract implements TriggerProcessor { + + @Autowired + private FtbCultivateLearnTaskCourseMapper ftbCultivateLearnTaskCourseMapper; + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Override + public boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType) { + return triggerEventType == TriggerEventBO.TriggerEventType.TASK_COURSE_EVENT; + } + @Override + public void triggerHandle(TaskCourseProcessDTO triggerEvent) { + super.courseProcessLearn(triggerEvent); + // 任务必修课程是否全部学完 + LambdaQueryWrapper taskCourseQueryWrapper = Wrappers.lambdaQuery(); + taskCourseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, triggerEvent.getTaskId()); + taskCourseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getEnableMark, 0); + taskCourseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getIsRequired, 0); + List ftbCultivateLearnTaskCourses = ftbCultivateLearnTaskCourseMapper.selectList(taskCourseQueryWrapper); + List courseIds = ftbCultivateLearnTaskCourses.stream() + .map(FtbCultivateLearnTaskCourse::getCourseId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(courseIds)) { + log.info("任务课程为null,不能触发后续逻辑,参数为:{}", triggerEvent); + return; + } + + boolean isStartStudyCourse = super.checkCourseStudyStatus(triggerEvent.getUserId(), courseIds); + if(isStartStudyCourse==false){ + log.info("未开始学习"); + return; + } + + int needStudyNum = 0; + LambdaQueryWrapper courseWraper = Wrappers.lambdaQuery(); + courseWraper.in(FtbCultivateCourse::getId, courseIds); + courseWraper.eq(FtbCultivateCourse::getEnableMark, 0); + courseWraper.eq(FtbCultivateCourse::getIsGroundIng, 1);//上下架(0下架,1上架) + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(courseWraper); + if(CollUtil.isEmpty(ftbCultivateCourses)){ + needStudyNum=0; + }else{ + needStudyNum= ftbCultivateCourses.size(); + courseIds = ftbCultivateCourses.stream() + .map(FtbCultivateCourse::getId) + .collect(Collectors.toList()); + } + + if (needStudyNum==0 || super.hasTheCourseBeenCompleted(triggerEvent.getUserId(), courseIds)) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + ExamIdentifyTriggerEventDTO.builder() + .taskId(triggerEvent.getTaskId()) + .userId(triggerEvent.getUserId()) + .triggerEventType(TriggerEventBO.TriggerEventType.TASK) + .build())); + } + } + + @Override + public void courseLearnCompleteHandle(TaskCourseProcessDTO baseData) { + // ignore 单个课程学完不触发任何任务操作 + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskTriggerProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskTriggerProcessor.java new file mode 100644 index 0000000..9552318 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/event/impl/trigger/task/TaskTriggerProcessor.java @@ -0,0 +1,130 @@ +package jnpf.cultivate.event.impl.trigger.task; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.event.base.examidentify.PositionTaskTriggerAbstract; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskExamMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskIdentificationMapper; +import jnpf.cultivate.service.FtbCultivateMyLearnTaskInfoService; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.dto.identify.IdentifyApplyDataPushDto; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskExam; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.resp.TriggerExamDto; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 任务学习触发器处理器 + * + * @author wangchunxiang + * @date 2024/09/18 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class TaskTriggerProcessor extends PositionTaskTriggerAbstract { + + private final FtbCultivateLearnTaskExamMapper ftbCultivateLearnTaskExamMapper; + private final FtbCultivateLearnTaskIdentificationMapper ftbCultivateLearnTaskIdentificationMapper; + private final FtbCultivateMyLearnTaskInfoService ftbCultivateMyLearnTaskInfoService; + + @Override + public boolean triggerTypeConfirmation(TriggerEventBO.TriggerEventType triggerEventType) { + return triggerEventType == TriggerEventBO.TriggerEventType.TASK; + } + + @Override + public void afterTriggerIdentifyApply(IdentifyApplyDataPushDto identifyApplyDataPushDto, ExamIdentifyTriggerEventDTO triggerEvent, String identificationRecordId) { + // ignore 鉴定触发后不做任何处理 + } + + + @Override + public TriggerExamDto buildTriggerExamDto(ExamIdentifyTriggerEventDTO triggerEvent) { + // 发起任务考试 + LambdaQueryWrapper taskExamQueryWrapper = Wrappers.lambdaQuery(); + taskExamQueryWrapper.eq(FtbCultivateLearnTaskExam::getEnableMark, 0); + taskExamQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, triggerEvent.getTaskId()); + taskExamQueryWrapper.last("limit 1"); + FtbCultivateLearnTaskExam ftbCultivateLearnTaskExam = ftbCultivateLearnTaskExamMapper.selectOne(taskExamQueryWrapper); + log.info("任务学习触发器发起考试参数:{}", ftbCultivateLearnTaskExam); + if (Objects.nonNull(ftbCultivateLearnTaskExam)) { + TriggerExamDto triggerExamDto = new TriggerExamDto(); + triggerExamDto.setExamSource(4); + triggerExamDto.setRelationTaskId(triggerEvent.getTaskId()); + triggerExamDto.setExamId(ftbCultivateLearnTaskExam.getExamId()); + triggerExamDto.setUserId(triggerEvent.getUserId()); + return triggerExamDto; + } + return null; + } + + @Override + public IdentifyApplyDataPushDto buildIdentifyApplyDataPushDto(ExamIdentifyTriggerEventDTO triggerEvent) { + // 发起任务鉴定 + LambdaQueryWrapper identityQuery = Wrappers.lambdaQuery(); + identityQuery.eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + identityQuery.eq(FtbCultivateLearnTaskIdentification::getTaskId, triggerEvent.getTaskId()); + identityQuery.last("limit 1"); + FtbCultivateLearnTaskIdentification ftbCultivateLearnTaskIdentification = ftbCultivateLearnTaskIdentificationMapper.selectOne(identityQuery); + log.info("任务学习触发器发起鉴定参数:{}", ftbCultivateLearnTaskIdentification); + if (Objects.nonNull(ftbCultivateLearnTaskIdentification)) { + IdentifyApplyDataPushDto identifyApplyDataPushDto = new IdentifyApplyDataPushDto(); + identifyApplyDataPushDto.setTableId(ftbCultivateLearnTaskIdentification.getIdentificationId()); + identifyApplyDataPushDto.setIdentifyUserId(triggerEvent.getUserId()); + identifyApplyDataPushDto.setSource(4);//鉴定来源(0手动发起,1课程学习鉴定,2岗位学习鉴定,3本人申请4,任务鉴定)' + identifyApplyDataPushDto.setDataId(triggerEvent.getTaskId()); + identifyApplyDataPushDto.setPostId(triggerEvent.getPostId()); + // 鉴定规则(0否,1,是 考试合格才能鉴定) 是否打开可见性, 0 打开, 1 关闭 + identifyApplyDataPushDto.setIsTurnOnVisibility(0); + if (ftbCultivateLearnTaskIdentification.getIdentificationRule() == 1) { + identifyApplyDataPushDto.setIsTurnOnVisibility(1); + } + return identifyApplyDataPushDto; + } + return null; + } + + @Override + public boolean isHasIndetify(ExamIdentifyTriggerEventDTO triggerEvent) { + // 发起任务鉴定 + LambdaQueryWrapper identityQuery = Wrappers.lambdaQuery(); + identityQuery.eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + identityQuery.eq(FtbCultivateLearnTaskIdentification::getTaskId, triggerEvent.getTaskId()); + identityQuery.last("limit 1"); + FtbCultivateLearnTaskIdentification ftbCultivateLearnTaskIdentification = ftbCultivateLearnTaskIdentificationMapper.selectOne(identityQuery); + log.info("任务学习触发器发起鉴定参数:{}", ftbCultivateLearnTaskIdentification); + if (Objects.nonNull(ftbCultivateLearnTaskIdentification)) { + return true; + } + return false; + } + + @Override + public void postProcessing(int i, ExamIdentifyTriggerEventDTO triggerEvent) { + // 无考试,无鉴定,触发任务证书颁发 + if (((i & EXAM_NOT_TRIGGER) == EXAM_NOT_TRIGGER) && ((i & IDENTIFY_NOT_TRIGGER) == IDENTIFY_NOT_TRIGGER)) { + // 更新用户完成时间 + ftbCultivateMyLearnTaskInfoService.updateUserFinishTime(triggerEvent.getTaskId(), triggerEvent.getUserId(), 0); + // 颁发证书 + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(0) + .taskId(triggerEvent.getTaskId()) + .issuanceType(2) + .userId(triggerEvent.getUserId()) + .build())); + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgMapper.java new file mode 100644 index 0000000..0ddb1ab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgMapper.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.course.app.FtbCultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsg; +import jnpf.model.cultivate.v2.course.vo.app.V2CultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.vo.course.app.FtbCultivateCourseMsgForAppVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Title:CultivateCourseMsgMapper + * @Author:peng.hao + * @create: 2023/12/2913:38 + */ +public interface CultivateCourseMsgMapper extends BaseMapper { + + /** + * 查询用户未读的培养课程消息 + * + * @param dto 查询条件传输对象,包含用户相关的查询条件 + * @return 返回一个列表,包含用户未读的培养课程消息 + */ + List queryUserUnreadMsg(@Param("dto") FtbCultivateCourseMsgForAppDTO dto); + List queryUserUnreadMsgV2(@Param("dto") V2CultivateCourseMsgForAppDTO dto); + + int deleteInfoByMsgIdV2(@Param("userId") String userId, @Param("courseId") String courseId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgUserMapper.java new file mode 100644 index 0000000..3af8871 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCourseMsgUserMapper.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsgUser; + +/** + * @Title:CultivateCourseUserMsgMapper + * @Author:peng.hao + * @create: 2023/12/2913:42 + */ +public interface CultivateCourseMsgUserMapper extends BaseMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverCategoryMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverCategoryMapper.java new file mode 100644 index 0000000..b6e148f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverCategoryMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.common.FtbCultivateCoverCategoryEntity; + +/** + * @Desc 封面分类mapper + * @Author shangyi + * @Date 2026/1/22 11:39 + */ +public interface CultivateCoverCategoryMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverInfoMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverInfoMapper.java new file mode 100644 index 0000000..c3ad20b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateCoverInfoMapper.java @@ -0,0 +1,19 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.common.FtbCultivateCoverInfoEntity; +import jnpf.model.cultivate.v2.exam.req.V2QueryCoverReq; +import jnpf.model.cultivate.v2.exam.vo.V2CoverVo; +import org.apache.ibatis.annotations.Param; + +/** + * @Desc 封面mapper + * + * @Author shangyi + * @Date 2026/1/22 + */ +public interface CultivateCoverInfoMapper extends BaseMapper { + + Page getPageList(@Param("page") Page page, @Param("params") V2QueryCoverReq params); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamDrawRuleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamDrawRuleMapper.java new file mode 100644 index 0000000..90db23e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamDrawRuleMapper.java @@ -0,0 +1,24 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.exam.vo.ConnectDrawRuleVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 抽题规则mapper + * + * @author yanwenfu + * @create 2026-03-05 + */ +public interface CultivateExamDrawRuleMapper extends SuperMapper { + + /** + * 查询关联题目的规则 + * @param questionIds 题目ids + * @return java.util.List + */ + List getConnectQuestionRule(@Param("questionIds") List questionIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamMapper.java new file mode 100644 index 0000000..c6665cd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamMapper.java @@ -0,0 +1,59 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.resp.ExamPaperVo; +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.vo.ExamDrawRuleVo; +import jnpf.model.cultivate.v2.exam.vo.MyExamWebVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamListVo; +import org.apache.ibatis.annotations.Param; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * 考试mapper[v2] + * + * @author yanwenfu + * @create 2026-03-05 + */ +public interface CultivateExamMapper extends SuperMapper { + + /** + * 查询考试列表 + * @param examName 考试名称 + * @param examType 考试类型(0: 岗位考试, 1: 自定义考试) + * @param needUpdate 考试标签(1: 不需要更新[正常], 2: 需要更新[异常]) + * @param limitedDate 日期限制(1: 有, 0: 没有) + * @return java.util.List + */ + List selectListV2(@Param("examName") String examName, @Param("examType") Integer examType, + @Param("needUpdate") Integer needUpdate, @Param("limitedDate") Integer limitedDate); + + /** + * 查询受影响的抽题规则(删题后) + * @param bankId 题库id + * @param typeList 题型列表 + * @return java.util.List + */ + List getAffectedExamRule(@Param("bankId") String bankId, @Param("typeList") List typeList); + + /** + * 查询老版本考试列表 + * @return java.util.List + */ + List getOldExamList(); + + /** + * 批量更新老版本考试信息 + * @param examList 考试列表 + */ + void updateOldExamBatch(@Param("examList") List examList); + + /** + * 查询我的考试列表[web] + * @param userId 用户id + * @return java.util.List + */ + List getMyExamWebList(@Param("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamSettingMapper.java new file mode 100644 index 0000000..536d17a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateExamSettingMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.exam.FtbCultivateExamSettingEntity; + +/** + * @Desc 考试配置mapper + * + * @Author shangyi + * @Date 2026/1/22 + */ +public interface CultivateExamSettingMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateFileMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateFileMapper.java new file mode 100644 index 0000000..4208cf9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateFileMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.teaching.CultivateFile; + +/** + * 附件mapper + * + * @author yanwenfu + * @create 2026-03-02 + */ +public interface CultivateFileMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsBackupsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsBackupsMapper.java new file mode 100644 index 0000000..a67458b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsBackupsMapper.java @@ -0,0 +1,19 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetailsBackups; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +/** + * 实操鉴定项_关联_鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Mapper +@Component +public interface CultivateIdentifyApplyDetailsBackupsMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsMapper.java new file mode 100644 index 0000000..58552f8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyDetailsMapper.java @@ -0,0 +1,36 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetails; +import jnpf.model.cultivate.vo.identify.IdentifyApplyDetailsInfoVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 实操鉴定申请详情表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Mapper +@Component +public interface CultivateIdentifyApplyDetailsMapper extends BaseMapper { + /** + * 查询鉴定项得分集合 + * @param id + * @return + */ + List getIdentifyItemList(@Param("id") String id); + + /** + * 查询总分数 + * @param identificationId 申请表id + * @return + */ + BigDecimal countSumScore(@Param("identificationId") String identificationId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyMapper.java new file mode 100644 index 0000000..64e5da6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyMapper.java @@ -0,0 +1,171 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.dto.identify.IdentifyApplyListAppDto; +import jnpf.model.cultivate.dto.identify.IdentifyApplyListDto; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplyListAppReq; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplyListReq; +import jnpf.model.cultivate.v2.apply.req.V2MyIdentifyApplyListAppReq; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyListAppVo; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyListVo; +import jnpf.model.cultivate.vo.identify.IdentifyApplyListAppDbVo; +import jnpf.model.cultivate.vo.identify.IdentifyApplyListDbVo; +import jnpf.model.cultivate.vo.identify.IdentifyApplyStatisticsApiVo; +import jnpf.model.cultivate.vo.identify.UserIdentifyPageVo; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskIdentificationInfoVO; +import jnpf.model.cultivate.v2.course.vo.UserLearningStatusVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; + +/** + * 实操鉴定申请表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Mapper +@Component +public interface CultivateIdentifyApplyMapper extends BaseMapper { + /** + * 实操鉴定申请/列表 + * + * @param req + * @return + */ + List getPageList(@Param("req") IdentifyApplyListDto req); + + /** + * 实操鉴定申请/列表 + * + * @param ids + * @return + */ + List getList(@Param("ids") List ids); + + /** + * 成员培训进展/分页列表 + * + * @return + */ + Page getUserIdentifyPage(@Param("page") Page page, @Param("userId") String userId); + + /** + * 实操鉴定申请/app列表 + * + * @param req + * @param userId + * @return + */ + List getPageAppList(@Param("req") IdentifyApplyListAppDto req, @Param("userId") String userId, @Param("powerUserIds") List powerUserIds); + + Integer getPageAppListCount(@Param("req") IdentifyApplyListAppDto req, @Param("userId") String userId); + + /** + * 查询鉴定表有多少人使用 + * + * @param tableId + * @return + */ + Integer getTableUseNumber(@Param("tableId") String tableId); + + /** + * 成员培训进展/用户鉴定数据统计 + * + * @param userIds + * @return + */ + List getUserIdentifyStatistics(@Param("userIds") List userIds); + + /** + * 查询申请中的用户ID + * 该方法用于根据任务ID和用户ID查询申请中的用户ID列表 + * + * @param id 任务ID,用于定位特定的任务 + * @param userId 用户ID,用于过滤特定用户的任务 + * @return 申请中的用户ID列表,返回包含申请中用户ID的字符串 + */ + String querAppplyUserIds(@Param("id") String id, @Param("userId") String userId); + + /** + * 查询识别申请信息 + * 该方法用于根据任务ID和用户ID查询识别申请的详细信息 + * + * @param taskId 任务ID,用于定位特定的识别任务 + * @param userId 用户ID,用于过滤特定用户的申请信息 + * @return 识别申请信息列表,返回包含识别申请详细信息的列表 + */ + List queryIdentifyApplyInfo(@Param("taskId") String taskId, @Param("userId") String userId); + + /** + * 统计每个鉴定的鉴定人数 + * + * @param taskIds + * @param type 0-查询所有 1-查询已合格的 + * @return + */ + List groupIdentifyCountNum(@Param("taskIds") List taskIds, @Param("type") Integer type); + + Page getPageListV2(@Param("page") Page page, @Param("req") V2IdentifyApplyListReq req); + + Page queryMyIdentifyApplyList(@Param("page") Page page, + @Param("req") V2MyIdentifyApplyListAppReq req, + @Param("userId") String userId); + + Page queryAppIdentifyApplyList(@Param("page") Page page, + @Param("req") V2IdentifyApplyListAppReq req, + @Param("orgUserIds") List orgUserIds, + @Param("powerUserIds") List powerUserIds, + @Param("myUserIds") List myUserIds, + @Param("loginUserId") String loginUserId + ); + + Page queryAppIdentifyApplyListWaiting( + @Param("page") Page page, + @Param("req") V2IdentifyApplyListAppReq req, + @Param("userIds") List userIds, + @Param("myUserIds") List myUserIds, + @Param("loginUserId") String loginUserId + ); + + Page queryAppIdentifyApplyListComplete( + @Param("page") Page page, + @Param("req") V2IdentifyApplyListAppReq req, + @Param("userIds") List userIds, + @Param("loginUserId") String loginUserId + ); + + List queryIdentifyApply(@Param("userId") String userId, + @Param("identifyIds") List identifyIds, + @Param("source") Integer source, + @Param("sourceId") String sourceId); + + List queryMyIdentifyUserList(@Param("loginUserId") String loginUserId); + + /** + * 查询用户已完成的鉴定列表(关联查询鉴定表信息) + * @param userId 用户ID + * @return 已完成鉴定列表 + */ + List queryCompletedIdentitiesWithInfo(@Param("userId") String userId); + + /** + * 统计岗位学习中已完成的鉴定数量(关联查询鉴定表和备份表) + * @param userId 用户ID + * @param courseIdentityIds 课程鉴定ID列表(对应备份表的F_TableId) + * @param source 鉴定来源 + * @param sourceId 鉴定来源ID(岗位ID) + * @return 已完成的鉴定数量 + */ + Long countCompletedIdentifyByPost(@Param("userId") String userId, + @Param("courseIdentityIds") Collection courseIdentityIds, + @Param("source") Integer source, + @Param("sourceId") String sourceId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyTableBackupsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyTableBackupsMapper.java new file mode 100644 index 0000000..6aa5911 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyApplyTableBackupsMapper.java @@ -0,0 +1,19 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.CultivateIdentifyApplyTableBackups; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +/** + * 实操鉴定申请_关联_鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Mapper +@Component +public interface CultivateIdentifyApplyTableBackupsMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyItemsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyItemsMapper.java new file mode 100644 index 0000000..05db748 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyItemsMapper.java @@ -0,0 +1,33 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.vo.identify.IdentifyItemsWithCategoryVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 实操鉴定项表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Mapper +@Component +public interface CultivateIdentifyItemsMapper extends BaseMapper { + + List countForTableId(@Param("ids") List ids); + + /** + * 查询鉴定项列表并关联分类名称 + * + * @param tableId 查询条件 + * @return 鉴定项列表(含分类名称) + */ + List listWithCategory(@Param("tableId") String tableId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyTableMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyTableMapper.java new file mode 100644 index 0000000..24ba8ae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateIdentifyTableMapper.java @@ -0,0 +1,103 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.model.cultivate.dto.identify.FtbIdentityOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.identify.FtbIdentityPersonWisdomStatisticDTO; +import jnpf.model.cultivate.dto.identify.IdentifyTableListDto; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableListReq; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableListVo; +import jnpf.model.cultivate.vo.identify.FtbCultivateIdentityOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.identify.FtbCultivateIdentityPersonWisdomStatisticVO; +import jnpf.model.cultivate.vo.identify.IdentifyTableListVo; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionIdentifyPersonVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionIdentifyVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 实操鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Mapper +@Component +public interface CultivateIdentifyTableMapper extends BaseMapper { + /** + * 鉴定表/列表 + * @param req + * @return + */ + List getPageList(@Param("req") IdentifyTableListDto req); + /** + * 组织列表统计 + * + * @param statisticDTO 统计DTO对象,包含查询条件 + * @param page 分页对象,用于分页查询 + * @return 返回组织列表统计结果 + */ + Page organizationListStatistics(@Param("dto") FtbIdentityOrgWisdomStatisticDTO statisticDTO, + @Param("page") Page page); + + /** + * 个人列表统计 + * + * @param personWisdomStatisticDTO 统计DTO对象,包含查询条件 + * @param page 分页对象,用于分页查询 + * @return 返回个人列表统计结果 + */ + Page personListStatistics(@Param("dto") FtbIdentityPersonWisdomStatisticDTO personWisdomStatisticDTO, + @Param("page") Page page); + + /** + * 获取评估人数量 + * + * @param postionIds 职位ID列表 + * @param tableId 表ID + * @return 返回评估人数量列表 + */ + List numberOfAppraisers(@Param("postionIds") List postionIds, @Param("tableId") String tableId); + + /** + * 用户相关组织列表统计 + * + * @param statisticDTO 统计DTO对象,包含查询条件 + * @return 返回用户相关组织列表统计结果 + */ + List organizationListStatisticsForUser(@Param("dto")FtbIdentityOrgWisdomStatisticDTO statisticDTO); + + /** + * 查询资格信息 + * + * @param userIdsByGradesId 按等级分组的用户ID列表 + * @param dbPostId 职位ID + * @return 返回资格信息 + */ + FtbCultivatePositionIdentifyVO queryQualificationInformation(@Param("userIdsByGradesId") List userIdsByGradesId,@Param("postId") String dbPostId); + + /** + * 查询合格人员列表 + * + * @param userIds 用户ID列表 + * @param positionId 职位ID + * @return 返回合格人员列表 + */ + List queryTheListOfQualifiedPersons(@Param("userIds") List userIds,@Param("positionId") String positionId); + + /** + * 查询当前人员的评估信息 + * + * @param userId 用户ID + * @param applyIds 申请ID列表 + * @return 返回当前人员的评估信息 + */ + FtbCultivatePositionIdentifyPersonVO currentPersonSAssessmentInformation(@Param("userId")String userId,@Param("applyIds") List applyIds); + + Page getPageListV2(@Param("page") Page page, @Param("params") V2IdentifyTableListReq params); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivatePositionCourseLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivatePositionCourseLogMapper.java new file mode 100644 index 0000000..c6b919b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivatePositionCourseLogMapper.java @@ -0,0 +1,23 @@ + +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.CultivatePositionCourseLogEntity; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +public interface CultivatePositionCourseLogMapper extends BaseMapper { + + /** + * 根据用户ID和学习地图ID查询岗位学习课程记录 + * + * @param userId 用户ID + * @param postLearnId 学习地图ID(岗位学习ID) + * @return 岗位学习课程记录列表 + */ + List queryByUserIdAndPostLearnId(@Param("userId") String userId, @Param("postLearnId") String postLearnId); + + List queryMyAllCompletePositionCourse(@Param("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateUserViewMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateUserViewMapper.java new file mode 100644 index 0000000..2e1f680 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/CultivateUserViewMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.teaching.CultivateUserView; + +/** + * 用户浏览记录mapper + * + * @author yanwenfu + * @create 2026-03-04 + */ +public interface CultivateUserViewMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedCommentMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedCommentMapper.java new file mode 100644 index 0000000..ef057b6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedCommentMapper.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.gained.FtbCourseGainedCommentEntity; +import jnpf.model.cultivate.v2.gained.req.V2AppCommentPageListReq; +import jnpf.model.cultivate.v2.gained.vo.V2AppCourseGainedCommentVO; +import jnpf.model.cultivate.v2.gained.vo.V2SimpleCourseGainedCommentVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 心得评论 Mapper 接口 + * + * @author yanglei + * @since 2023-07-19 + */ +public interface FtbCourseGainedCommentMapper extends SuperMapper { + +/** + * APP 端评论分页列表查询 + * + * @param page 分页对象,包含当前页码和每页大小 + * @param req 评论查询请求参数,包含心得 ID 等筛选条件 + * @return 分页查询结果,包含 V2AppCourseGainedCommentVO 列表 + */ + Page appCommentPage(@Param("page") Page page, @Param("req") V2AppCommentPageListReq req); + + /** + * APP 端获取简单的评论树形结构数据 + * + * @param gainedId 心得 ID,用于筛选该心得下的所有评论 + * @return 简单评论 VO 列表,包含评论 ID 和父级 ID,用于构建树形结构 + */ + List appSimpleComment(@Param("gainedId") String gainedId); + + /** + * 根据 ID 集合批量查询评论实体 + * + * @param childIds 评论 ID 集合,用于批量查询 + * @return 评论实体列表,按创建时间升序排列 + */ + List queryAllByIds(@Param("childIds") List childIds); + + /** + * 查询指定评论下的所有回复评论 + * + * @param firstCommentId 一级评论 ID,用于查询该评论下的所有子评论 + * @return 评论实体列表,包含该评论下的所有回复 + */ + List queryNextAllComment(@Param("firstCommentId") String firstCommentId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedLikeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedLikeMapper.java new file mode 100644 index 0000000..d1a3167 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedLikeMapper.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.gained.FtbCourseGainedLikeEntity; +import org.apache.ibatis.annotations.Param; + +/** + * + * 心得点赞 Mapper 接口 + * + */ +public interface FtbCourseGainedLikeMapper extends SuperMapper { + + + /** + * 获取用户有无对该心得点赞 + * @param GainedId 心得id + * @param likeUserId 用户id + * @return 返回值 + */ + Integer getMyLike(@Param("GainedId") String GainedId , @Param("likeUserId") String likeUserId); + + /** + *删除指定用户在指定心得下的点赞 + * @param gainedId 心得id + * @param likeUserId 用户id + */ + void deleteLikeByGainedId(@Param("gainedId") String gainedId, @Param("likeUserId") String likeUserId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedMapper.java new file mode 100644 index 0000000..cbb64f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedMapper.java @@ -0,0 +1,25 @@ +package jnpf.cultivate.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.gained.FtbCourseGainedEntity; + +import java.util.List; + +/** + * + * 心得体会 Mapper 接口 + * + * @author yanglei + * @since 2023-07-19 + */ +public interface FtbCourseGainedMapper extends SuperMapper { + + /** + * 获取指定章节末尾已获得的课程列表 + * + * @param chapterId 章节的唯一标识符,用于标识特定的章节 + * @return 返回一个列表,包含与指定章节末尾相关的所有已获得的课程实体 + */ + List getGainedListOfChapterEnd(String chapterId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedReaderMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedReaderMapper.java new file mode 100644 index 0000000..5d70543 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedReaderMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.gained.FtbCourseGainedReaderEntity; + +/** + * + * 心得可查看用户关联表 Mapper 接口 + * + */ +public interface FtbCourseGainedReaderMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedShareMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedShareMapper.java new file mode 100644 index 0000000..5a2bb7c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCourseGainedShareMapper.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.gained.FtbCourseGainedShareEntity; + +/** + * + * 心得分享 Mapper 接口 + * + * @author yanglei + * @since 2023-07-19 + */ +public interface FtbCourseGainedShareMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateAssessmentPointsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateAssessmentPointsMapper.java new file mode 100644 index 0000000..3a140a1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateAssessmentPointsMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivateAssessmentPoints; + +public interface FtbCultivateAssessmentPointsMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseLikeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseLikeMapper.java new file mode 100644 index 0000000..f70e1f8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseLikeMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.casebase.FtbCultivateCaseBaseLike; + +/** +* +*@Title: 案例库点赞 +*@Author: peng.hao +*@create: 2024/7/22:11:03 +* +*/ +public interface FtbCultivateCaseBaseLikeMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseMapper.java new file mode 100644 index 0000000..94c3ffe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCaseBaseMapper.java @@ -0,0 +1,45 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseDTO; +import jnpf.model.cultivate.po.casebase.FtbCultivateCaseBase; +import jnpf.model.cultivate.vo.casebase.FtbCultivateCaseBaseVO; +import org.apache.ibatis.annotations.Param; + +/** +* +*@Title: 案例库 +*@Author: peng.hao +*@create: 2024/7/22:11:03 +* +*/ +public interface FtbCultivateCaseBaseMapper extends BaseMapper { + + /** + * 分页查询培养案例列表(用于后台展示) + * + * @param page 分页参数,包含了页码、每页数量等信息 + * @param dto 查询条件封装对象,用于筛选查询结果 + * @return 分页查询结果,包含培养案例的基础信息 + *

+ * 该方法用于后台管理系统中分页展示培养案例,根据提供的分页参数和查询条件, + * 返回符合条件的培养案例列表。主要用于页面展示,因此命名为listDisplay。 + */ + Page listDisplay(@Param("page") Page page, + @Param("dto") FtbCultivateCaseBaseDTO dto); + + /** + * 分页查询培养案例列表(用于前端App展示) + * + * @param page 分页参数,包含了页码、每页数量等信息 + * @param dto 查询条件封装对象,用于筛选查询结果 + * @return 分页查询结果,包含培养案例的基础信息 + *

+ * 该方法用于前端App中分页展示培养案例,根据提供的分页参数和查询条件, + * 返回符合条件的培养案例列表。因为服务于前端App,命名中特别指出了用途(forApp), + * 以便于与服务于后台管理系统的listDisplay方法相区分。 + */ + Page listDisplayForApp(@Param("page") Page page, + @Param("dto") FtbCultivateCaseBaseDTO dto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateImagesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateImagesMapper.java new file mode 100644 index 0000000..1346cf6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateImagesMapper.java @@ -0,0 +1,28 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.certificate.FtbCertificateImagesEntity; + +import java.util.List; + + +/** + * 证书图片 + * + * @author xuguilin + */ +public interface FtbCultivateCertificateImagesMapper extends BaseMapper { + /** + * 查询所有证书图片列表 + * + * @return 列表 + */ + List listAll(); + + /** + * 查询最大排序 + * + * @return 最大排序 + */ + Integer queryMaxSort(); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateMapper.java new file mode 100644 index 0000000..254da0a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateMapper.java @@ -0,0 +1,46 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.certificate.FtbCertificateOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.certificate.FtbCertificatePersonWisdomStatisticDTO; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.vo.certificate.FtbCertificateOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificatePersonWisdomStatisticVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificateQueryStatisticAllVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + + +/** + * 证书模版表 + * @author penghao + */ +public interface FtbCultivateCertificateMapper extends BaseMapper { + /** + * 统计组织列表信息 + * 该方法用于对组织列表进行统计,根据提供的统计参数 + * + * @param statisticDTO 统计DTO对象,包含统计所需的条件和信息 + * @param page 分页对象,用于控制和返回分页结果 + * @return 统计结果的分页对象,包含组织列表的统计信息 + */ + Page organizationListStatistics( + @Param("statisticDTO") FtbCertificateOrgWisdomStatisticDTO statisticDTO, + @Param("page") Page page); + + List organizationListStatisticsAll( + @Param("statisticDTO") FtbCertificateOrgWisdomStatisticDTO statisticDTO); + + /** + * 统计个人维度信息 + * 该方法用于从个人维度进行统计,根据提供的个人统计参数 + * + * @param dto 个人统计DTO对象,包含统计所需的条件和信息 + * @param page 分页对象,用于控制和返回分页结果 + * @return 统计结果的分页对象,包含个人维度的统计信息 + */ + Page personalDimensionStatistics(@Param("dto") FtbCertificatePersonWisdomStatisticDTO dto, + @Param("page") Page page); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateResultMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateResultMapper.java new file mode 100644 index 0000000..9f773e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateResultMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.FtbCultivateCertificateResult; + + +/** + * @Title:证书颁发结果表 + * @Author:xgl + * @create: 2024/10/21 + */ +public interface FtbCultivateCertificateResultMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateUserMapper.java new file mode 100644 index 0000000..5becd44 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCertificateUserMapper.java @@ -0,0 +1,11 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; + +/** + * @author hao.peng + */ +public interface FtbCultivateCertificateUserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestMapper.java new file mode 100644 index 0000000..badec48 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestMapper.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTest; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestResultVO; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestStatisticVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateChapterTestMapper extends BaseMapper { + /** + * 根据关键词获取章节测试统计信息 + * 该方法用于根据提供的关键词搜索并获取相关章节的测试统计信息 + * + * @param keyWords 关键词,用于搜索匹配的章节测试统计信息 + * @return 返回一个包含FtbCultivateChapterTestStatisticVO对象的列表,每个对象代表一个章节的测试统计信息 + */ + List getChapterTestStatistic(@Param("keyWords") String keyWords, @Param("innerPowerUserIds") List innerPowerUserIds); + + /** + * 根据课程ID列表获取章节测试信息 + * 该方法用于根据提供的课程ID列表,获取这些课程的相关章节测试信息 + * + * @param courseIds 课程ID列表,用于检索对应的章节测试信息 + * @return 返回一个包含FtbCultivateChapterTestResultVO对象的列表,每个对象代表一个章节测试的信息 + */ + List getChapterTestInfos(@Param("courseIds") List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestOptionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestOptionMapper.java new file mode 100644 index 0000000..8beee16 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestOptionMapper.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestOption; + +public interface FtbCultivateChapterTestOptionMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestResultMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestResultMapper.java new file mode 100644 index 0000000..d7b5485 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateChapterTestResultMapper.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateChapterTestResultMapper extends BaseMapper { + /** + * 根据课程ID和用户ID查询章节测试结果列表 + * + * 本函数的作用是通过特定的课程ID和用户ID从数据库中查询出该用户在所有章节中的测试结果 + * 它主要用于展示学生在特定课程中的学习成效和测试表现,以便于学生和教师回顾和评估学习进度 + * + * @param courseId 课程的唯一标识符,用于指定查询的课程 + * @param userId 用户的唯一标识符,用于指定查询的用户 + * @return 返回一个列表,包含该用户在所有章节中的测试结果 + */ + List selectChatperList(@Param("courseId") String courseId, + @Param("userId") String userId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCommonSettingGlobalMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCommonSettingGlobalMapper.java new file mode 100644 index 0000000..78bec83 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCommonSettingGlobalMapper.java @@ -0,0 +1,15 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivateCommonSettingGlobal; +import org.apache.ibatis.annotations.Mapper; + +/** + * 培训通用配置 Mapper + * + * @author fantaibao + * @date 2026/02/11 + */ +@Mapper +public interface FtbCultivateCommonSettingGlobalMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseChapterMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseChapterMapper.java new file mode 100644 index 0000000..cdfb522 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseChapterMapper.java @@ -0,0 +1,22 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterVO; +import org.apache.ibatis.annotations.Param; + +public interface FtbCultivateCourseChapterMapper extends BaseMapper { + /** + * 课程章节分页查询 + */ + Page queryList(@Param("page") Page page, @Param("courseId") String courseId); + + /** + * 查询课程的总时长 + * @param id 课程id + * @return + */ + Long querySumDuration(@Param("id") String id); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseLearningLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseLearningLogMapper.java new file mode 100644 index 0000000..5adcbb3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseLearningLogMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.FtbCultivateCourseLearningLogEntity; + +/** + * 学习课程日志表 Mapper + * + * @author JNPF + * @since 2026-04-08 + */ +public interface FtbCultivateCourseLearningLogMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseMapper.java new file mode 100644 index 0000000..7e374ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseMapper.java @@ -0,0 +1,153 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.course.FtbCommonKeyAndValDto; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseQueryDTO; +import jnpf.model.cultivate.dto.course.app.FtbGlobalCurriculumAppWrapDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.v2.course.vo.AiHelperCourseStatisticsVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCommonCourseSimpleVo; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseListReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseSelectReq; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCoursePageVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseSelectVo; +import jnpf.model.cultivate.v2.position.req.V2CultivateCoursePageReq; +import jnpf.model.cultivate.v2.position.req.V2MyCultivateCommonCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.V2CultivateJobLearnCourseVo; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.course.web.FtbCourseDeleteJobLearnVO; +import jnpf.model.cultivate.vo.course.web.FtbCourseDeleteQuestionBankVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCoursePageVO; +import jnpf.model.cultivate.vo.course.web.PromotionChannelLearnCourseVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateCourseMapper extends BaseMapper { + /** + * 课程格式统计 + */ + Integer getCount(@Param("flag") Integer flag); + + /** + * 课程列表分页查询 + */ + Page pagingQuery(@Param("page") Page page + , @Param("params") FtbCultivateCourseQueryDTO ftbCultivateCourseQueryDTO); + + /** + * 校验岗位学习 + */ + Integer getCountLearnByCourseId(@Param("courseId") String courseId); + + /** + * 校验关联题库 + */ + Integer getCountQuestionBankByCourseId(@Param("courseId") String courseId); + + /** + * 二次删除时,岗位学习分页列表 + */ + Page listJobLearningByCourseId(@Param("page") Page page, + @Param("courseId") String courseId); + + /** + * 关联题库分页列表 + */ + Page listRelatedQuestionBankList(@Param("page") Page page, + @Param("courseId") String courseId); + + /** + * 涵盖题库题目的试卷数 + */ + Integer getCountTestPaperByQuestionBankId(@Param("questionBankId") String questionBankId); + + /** + * 已学习课程列表 + */ + List promotionPathwayCourses(@Param("userId") String userId); + + /** + * 通用课程列表 + */ + Page generalCourseList(@Param("page") Page page, @Param("userId") String userId + , @Param("params") FtbGlobalCurriculumAppWrapDTO ftbCultivateCourseQueryDTO); + + /** + * 已学习课程数 + */ + Integer learnTotalNumber(@Param("userId") String userId, @Param("typeId") String typeId); + + /** + * 根据考试id查询考试名称 + */ + String queryExamNameByExamId(@Param("examId") String examId); + + /** + * 校验岗位是否建立岗位学习 + */ + List queryJobBindingCourses(@Param("postIds") List postIds); + + List getCountMediaList(); + + List getCountLabelList(); + + /** + * 查询通用课程未学习的数量 + * + * @param userId + * @return + */ + Long queryNoStudyCommonCourseNum(@Param("userId") String userId); + + /** + * @param page + * @param params + * @return + */ + Page queryBindingCourses(@Param("page") Page page, + @Param("params") V2CultivateCoursePageReq params); + + + /** + * 岗位学习课程列表 + * + * @param page 分页参数 + * @param params 查询参数 + * @return 岗位学习课程列表 + */ + Page jobLearningCourseList(@Param("page") Page page, + @Param("params") V2CultivateCourseSelectReq params); + + /** + * 岗位学习课程列表 + * + * @param page 分页参数 + * @param params 岗位学习课程列表参数 + * @return 岗位学习课程列表 + */ + Page webList(@Param("page") Page page, + @Param("params") V2CultivateCourseListReq params); + + /** + * 查询通用课程列表 + * + * @param page 分页参数 + * @param params 查询参数 + * @return 通用课程列表 + */ + Page queryCommonCourseApp(@Param("page") Page page, + @Param("params") V2MyCultivateCommonCourseForAppReq params); + + List commonCourseCount(@Param("params") V2MyCultivateCommonCourseForAppReq params); + + /** + * 根据课程ID统计课程学习数据 + * @param courseId 课程ID + * @return jnpf.model.cultivate.v2.course.vo.CourseStatisticsVo + */ + AiHelperCourseStatisticsVo getCourseStatistics(@Param("courseId") String courseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCoursePackageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCoursePackageMapper.java new file mode 100644 index 0000000..499a5f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCoursePackageMapper.java @@ -0,0 +1,28 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageQueryDTO; +import jnpf.model.cultivate.po.coursepackage.FtbCultivateCoursePackage; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackageDetailsVO; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackagePageVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateCoursePackageMapper extends BaseMapper { + /** + * 查询课程包课程 + */ + List getPackageDetails(@Param("id") String id); + + /** + * 课程包分页查询 + */ + Page pagingQuery(@Param("page") Page page, @Param("params") FtbCultivateCoursePackageQueryDTO ftbCultivateCoursePackageQueryDTO); + + /** + * 根据课程包id查询课程数据 + */ + List queryCoursePackageCourses(@Param("coursePackageIds") List coursePackageIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingGlobalMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingGlobalMapper.java new file mode 100644 index 0000000..c742a82 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingGlobalMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivateCourseSettingGlobal; + + +public interface FtbCultivateCourseSettingGlobalMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingMapper.java new file mode 100644 index 0000000..c974548 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseSettingMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.FtbCultivateCourseSetting; +import org.apache.ibatis.annotations.Param; + +public interface FtbCultivateCourseSettingMapper extends BaseMapper { + /** + * 是否存在课程学习设置 + */ + Integer isThereACourseLearningSetting(@Param("courseId") String courseId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseStatisticesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseStatisticesMapper.java new file mode 100644 index 0000000..21e937e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseStatisticesMapper.java @@ -0,0 +1,74 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseOrgStatisticsDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCoursePersonStatisticesDTO; +import jnpf.model.cultivate.v2.course.web.vo.V2InnerCultivateChapterStatisticsDto; +import jnpf.model.cultivate.v2.course.web.vo.V2InnerCultivateCourseOrgStatisticsDto; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseOrgStatisticesVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCoursePersonStatisticesVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateCourseStatisticesMapper { + /** + * 组织列表统计方法 + * 对给定的统计参数进行组织列表的相关统计分析,并返回统计结果列表 + * + * @param statisticDTO 统计参数,包含了需要进行统计的组织相关的信息 + * @return 返回一个包含组织统计结果的列表 + */ + List organizationListStatistics( + @Param("params") FtbCultivateCourseOrgStatisticsDTO statisticDTO); + + /** + * 人员列表统计方法 + * 根据给定的分页参数和统计参数,对人员列表进行相关的统计分析,并返回分页统计结果 + * + * @param page 分页参数,包含了分页信息如当前页码和每页大小 + * @param params 统计参数,包含了需要进行统计的人员相关的信息 + * @return 返回一个分页的人员统计结果 + */ + Page personListStatistics(@Param("page") Page page, + @Param("params") FtbCultivateCoursePersonStatisticesDTO params); + + /** + * 课程信息查询方法 + * 根据给定的课程ID列表,查询并返回相关的课程统计信息 + * + * @param courseIds 课程ID列表,包含了需要查询的课程的唯一标识 + * @return 返回一个包含课程统计信息的列表 + */ + List courseInfo(@Param("courseIds") List courseIds); + + /** + * 学习章节数量统计方法 + * 统计特定用户在特定课程中的学习章节数量 + * + * @param courseId 课程ID,标识了需要统计的课程 + * @param userId 用户ID,标识了需要统计的用户 + * @param type 统计类型,不同的类型可能影响统计的方式或结果 + * @return 返回特定用户在特定课程中的学习章节数量 + */ + Integer numberOfStudyChapters(@Param("courseId") String courseId, @Param("userId") String userId, + @Param("type") Integer type); + /** + * 组织维度统计方法 + * 根据给定的统计参数,对组织维度进行相关的统计分析,并返回统计结果列表 + * + * @param params 统计参数,包含了需要进行统计的组织相关的信息 + * @return 返回一个包含组织统计结果的列表 + */ + List organizationListStatisticsV2(@Param("params") FtbCultivateCourseOrgStatisticsDTO params); + + /** + * 人员维度统计方法 + * 根据给定的用户ID列表和状态列表,对人员维度进行相关的统计分析,并返回统计结果列表 + * + * @param userIds 用户ID列表,标识了需要统计的用户 + * @param state 状态列表,标识了需要统计的用户的当前状态 + * @return 返回一个包含人员统计结果的列表 + */ + List personListStatisticsV2(@Param("userIds") List userIds,@Param("state") List state); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTriggerLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTriggerLogMapper.java new file mode 100644 index 0000000..b1aa770 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTriggerLogMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.course.FtbCultivateCourseTriggerLog; + +public interface FtbCultivateCourseTriggerLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTypeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTypeMapper.java new file mode 100644 index 0000000..534a21b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateCourseTypeMapper.java @@ -0,0 +1,22 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.course.FtbCultivateCourseType; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseTypeVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateCourseTypeMapper extends BaseMapper { + /** + * 课程类型分页 + */ + Page pageList(@Param("page") Page page); + + /** + * 获取所有课程类型 + */ + List getAll(); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamFrequncyLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamFrequncyLogMapper.java new file mode 100644 index 0000000..3737279 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamFrequncyLogMapper.java @@ -0,0 +1,33 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.exam.FtbCultivateExamFrequencyLog; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; + +/** + * @author 许贵林 + * @description 数据库操作Mapper + * @createDate 2024-05-08 09:42:09 + * @Entity jnpf.model.cultivate.po.exam.FtbCultivateExamFrequencyLog + */ +public interface FtbCultivateExamFrequncyLogMapper extends BaseMapper { + + /** + * 查询用户在一定时间范围内的考试次数 + * + * @param examId 考试ID + * @param userId 用户ID + * @param startTime 开始考试日期 + * @param endTime 结束考试日期 + * @return + */ + Integer queryExamNum(@Param("examId") String examId, @Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + Integer queryExamNumByDateStr(@Param("examId") String examId, @Param("userId") String userId, @Param("startDateStr") String startDateStr, @Param("endDateStr") String endDateStr); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamHistoryPaperMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamHistoryPaperMapper.java new file mode 100644 index 0000000..28acad1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamHistoryPaperMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamHistoryPaper; +import jnpf.model.cultivate.req.exam.QueryExamReq; +import jnpf.model.cultivate.resp.ExamListVo; +import org.apache.ibatis.annotations.Param; + +public interface FtbCultivateExamHistoryPaperMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamMapper.java new file mode 100644 index 0000000..53d77d0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamMapper.java @@ -0,0 +1,31 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.req.exam.QueryExamReq; +import jnpf.model.cultivate.resp.ExamListVo; +import org.apache.ibatis.annotations.Param; +/** + * 考试表mapper + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbCultivateExamMapper extends BaseMapper { + /** + * 管理端 查询考试列表 + * @param page + * @param params + * @return + */ + Page pagingQuery(@Param("page") Page page + , @Param("params") QueryExamReq params); + /** + * 根据岗位查询是否考试完成才能够鉴定的标志 + * @param positionId 岗位 id + * @return 岗位考试合格后才能进行鉴定,0否 1是 + */ + Integer queryExamAndIdentifyConfig(@Param("positionId") String positionId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamPaperMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamPaperMapper.java new file mode 100644 index 0000000..2a72496 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamPaperMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.exam.FtbCultivateExamPaper; + +public interface FtbCultivateExamPaperMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserDetailMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserDetailMapper.java new file mode 100644 index 0000000..fe8b0f4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserDetailMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail; + +public interface FtbCultivateExamUserDetailMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserMapper.java new file mode 100644 index 0000000..ce7e927 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateExamUserMapper.java @@ -0,0 +1,349 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.statistics.InnerCultivateStatisticsDTO; +import jnpf.model.cultivate.dto.statistics.InnerExamStatisticsForOrgDTO; +import jnpf.model.cultivate.dto.statistics.InnerExamStatisticsForPersonDTO; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.req.exam.*; +import jnpf.model.cultivate.resp.*; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForOrgReq; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForPersonReq; +import jnpf.model.cultivate.v2.exam.vo.*; +import jnpf.model.cultivate.v2.course.vo.UserLearningStatusVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +/** + * 用户考试记录表mapper + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbCultivateExamUserMapper extends BaseMapper { + /** + * 查询我的考试列表 + * + * @param page + * @param params + * @return + */ + Page pagingQueryMyExamList(@Param("page") Page page + , @Param("params") QueryMyExamListReq params); + /** + * 查询需要提醒的岗位考试的列表 + * @userId 用户ID + * @return + */ + List queryAlertPostionIdentify(@Param("userId") String userId); + + /** + * 根据用户ID 查询考试列表 + * @param userId 用户ID + * @param page + * @return + */ + Page pagingQueryExamListForUserId(@Param("page") Page page + , @Param("userId") String userId); + + /** + * 查询待我批阅的考试列表 + * + * @param page + * @param params + * @return + */ + Page pagingQueryWaitMyExamList(@Param("page") Page page + , @Param("params") QueryWaitMyExamListReq params,@Param("managerUserIds") List managerUserIds); + + /** + * 查询我已经完成的批阅 + * + * @param page + * @param params + * @return + */ + Page pagingQueryMyCompleteExamList(@Param("page") Page page + , @Param("params") QueryMyCompleteExamListReq params); + + + /** + * 查询过期的考试列表 + * + * @param page + * @param params + * @return + */ + Page pagingQueryExpireExamList(@Param("page") Page page + , @Param("params") QueryExpireExamListReq params,@Param("managerUserIds") List managerUserIds); + + + /** + * 根据用户ID和岗位ID 过滤考试列表 + * + * @param page + * @param params + * @return + */ + Page pagingQueryExamList(@Param("page") Page page + , @Param("params") QueryUserExamListDto params); + + /** + * 根据用户ID和岗位ID 过滤考试列表 + * + * @param page + * @param params + * @return + */ + Page pagingQueryExamListByPosition(@Param("page") Page page + , @Param("params") QueryUserExamListDto params); + + + /** + * 查询待我批阅的考试列表 + * + * @param params + * @return + */ + int queryWaitMyExamNum(@Param("params") QueryWaitMyExamListReq params); + + /** + * 查询我已经完成的批阅 + * + * @param params + * @return + */ + int queryMyCompleteExamNum(@Param("params") QueryMyCompleteExamListReq params); + + + /** + * 查询过期的考试列表 + * + * @param params + * @return + */ + int queryExpireExamNum(@Param("params") QueryExpireExamListReq params); + + /** + * 查询一段时间的内的平均分数排行榜 + * @param dto + * @return + */ + List queryAvgScore(@Param("dto") InnerCultivateStatisticsDTO dto); + + /** + * 查询一个考试下面的 按照分数最高排序 + * + * @param page + * @param params + * @return + */ + Page pagingQueryRankingExamList(@Param("page") Page page + , @Param("params") QueryExamRankListReq params, @Param("examId") String examId); + + /** + * 查询用户一个考试的列表 + * @param examId 考试id + * @param userId 用户id + * @return + */ + List queryMyRank(@Param("examId") String examId, @Param("userId") String userId); + + /** + * 考试个人统计分析列表 + * @param page + * @param params + * @return + */ + Page queryExamStatisticsForPerson(@Param("page") Page page, @Param("params") InnerExamStatisticsForPersonDTO params); + + /** + * 考试组织统计分析列表 + * @param dto + * @return + */ + List countForOrg(@Param("dto") InnerExamStatisticsForOrgDTO dto); + + /** + * 考试组织统计分析列表 + * @param dto + * @return + */ + List webStatisticsForOrg(@Param("dto") InnerCultivateStatisticsDTO dto); + /** + * 根据考试ID查询待考试的列表 + * @param examId 考试ID + * @return + */ + List queryWaitReadOverNum(@Param("examId") String examId); + + /** + * 批量查询一个岗位的指定人员的考试信息 + * @param userIds 用户id集合 + * @param positionId 岗位id + * @param exmIds 考试id + * @return + */ + FtbCultivateExamUser queryTheListOfQualifiedPersons(@Param("userIds") List userIds, @Param("positionId") String positionId,@Param("exmIds") List exmIds); + + /** + * 批量查询一个岗位的指定人员的考试信息 + * @param userIdsByGradesId 用户id集合 + * @param positionId 岗位ID + * @return + */ + List queryTheListOfQualifiedPersonsNew(@Param("userIds")List userIdsByGradesId, @Param("positionId") String positionId); + + /** + * 统计通过的任务考试数量 + * @param taskIds 任务集合ID + * @return + */ + List groupPassCountNum(@Param("taskIds") List taskIds); + /** + * 批量统计所有的任务考试数量 + * @param taskIds 任务id集合 + * @return + */ + List groupAllCountNum(@Param("taskIds") List taskIds); + + List storeMyExamList(@Param("userId") String userId); + + /** + * 查询我的待考试的数量 + * @param userId + * @return + */ + Long queryMyWaitingExamNum(@Param("userId") String userId); + + /** + * 查询我批阅的 的数量 + * @param userId 用户ID + * @return + */ + Long queryAllMyReadOver(@Param("userId") String userId); + + /** + * 查询我通过批阅的 的数量 + * @param userId 用户ID + * @return + */ + Long queryPassMyReadOver(@Param("userId") String userId); + + List groupAllCountNumV2(@Param("taskIds") List taskIds); + + /** + * 考试组织统计分析列表 + * + * @param params 请求参数 + * @return 考试组织统计分析列表 + */ + List countForOrgV2(@Param("params") V2ExamStatisticsForOrgReq params, + @Param("userIds") List userIds); + + /** + * 考试个人统计分析列表 + * @param page + * @param params + * @return + */ + Page queryExamStatisticsForPersonV2(@Param("page") Page page, @Param("params") V2ExamStatisticsForPersonReq params, + @Param("innerUserIds") List userIds, @Param("innerStatus") List innerStatus); + + + /** + * 统计通过的任务考试数量 + * @param taskIds 任务集合ID + * @return 统计通过的任务考试数量 + */ + List groupPassCountNumV2(@Param("taskIds") List taskIds); + + + /** + * 查询用户考试列表 + * @param id 考试id + * @param userIds 用户过滤 + * @param scoreSort 得分排序(1: 升序, 0: 降序) 为空时不排序 + * @return java.util.List + */ + List getExamUserList(@Param("id") String id, @Param("versionBatch") String versionBatch, + @Param("userIds") List userIds, @Param("scoreSort") Integer scoreSort); + + /** + * 查询我的考试列表 + * @param status 考试状态(0: 待考试, 1: 待批阅, 2: 已逾期, 3: 合格, 4: 不合格, 5: 优秀) + * @param userId 用户id + * @return java.util.List + */ + List getMyExamList(@Param("status") Integer status, @Param("userId") String userId); + + /** + * 根据状态查询用户考试数 + * @param examStatus 考试状态(0: 待考试, 1: 待批阅, 2: 已逾期, 3: 合格, 4: 不合格, 5: 优秀) + * @param userId 用户id + * @return java.lang.Integer + */ + Integer getStatusCount(@Param("examStatus") Integer examStatus, @Param("userId") String userId); + + /** + * 查询用户考试详情 - 基础信息 + * @param userExamId 用户考试id + * @return jnpf.model.cultivate.v2.exam.vo.MyExamDetailVo + */ + MyExamDetailVo getMyExamDetail(@Param("userExamId") String userExamId); + + /** + * 查询用户考试详情 - 考试结果 + * @param userExamId 用户考试id + * @return jnpf.model.cultivate.v2.exam.vo.ExamResultVo + */ + ExamResultVo getExamResult(@Param("userExamId") String userExamId); + + /** + * 批阅考试 - 考试列表 + * @param keyword 关键字(考试名称) + * @param status 审批状态(1: 待批阅, 2: 已批阅, 3: 逾期未考) + * @param userIds 用户过滤, 无则忽略 + * @return java.util.List + */ + List readOverExamList(@Param("keyword") String keyword, @Param("status") Integer status, @Param("userIds") List userIds); + + /** + * 查询考试下用户排名列表 + * @param examId 考试id + * @param userIds 用户ids + * @return java.util.List + */ + List getUserExamRankList(@Param("examId") String examId, @Param("userIds") List userIds); + + /** + * 批量更新旧版本用户考试 + * @param examList 考试列表 + */ + void updateOldExamUserBatch(@Param("examList") List examList); + + /** + * 查询待批阅数量 + * @param examStatus 考试状态 + * @param userIds 用户ids + * @return java.lang.Integer + */ + Integer getWaitCheckCount(@Param("examStatus") Integer examStatus, @Param("userIds") List userIds); + + /** + * 根据考试ID统计考试数据 + * @param examId 考试ID + * @return jnpf.model.cultivate.v2.exam.vo.ExamStatisticsVo + */ + AiHelperExamStatisticsVo getExamStatistics(@Param("examId") String examId); + + /** + * 查询用户已完成的考试列表(关联查询考试信息) + * @param userId 用户ID + * @return 已完成考试列表 + */ + List queryCompletedExamsWithInfo(@Param("userId") String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateFileMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateFileMapper.java new file mode 100644 index 0000000..9f440bd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateFileMapper.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivateFile; + +public interface FtbCultivateFileMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyCategoriesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyCategoriesMapper.java new file mode 100644 index 0000000..1c45d3e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyCategoriesMapper.java @@ -0,0 +1,15 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; + +/** + * 鉴定项分类Mapper接口 + * + * @author lingma + * @since 2026-02-24 + */ +public interface FtbCultivateIdentifyCategoriesMapper extends BaseMapper { + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyItemsPoolMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyItemsPoolMapper.java new file mode 100644 index 0000000..13d3e43 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateIdentifyItemsPoolMapper.java @@ -0,0 +1,26 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.item_pool.req.IdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.vo.FtbCultivateIdentifyItemsPoolVo; +import jnpf.model.cultivate.v2.position.vo.WebPositionLearningListVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 鉴定项池Mapper接口 + * + * @author lingma + * @since 2026-02-24 + */ +public interface FtbCultivateIdentifyItemsPoolMapper extends BaseMapper { + + + Page webPageList(@Param("page") Page page, @Param("params") IdentifyItemsPoolReq params); + + List countByCateId(); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLabelMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLabelMapper.java new file mode 100644 index 0000000..8a96175 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLabelMapper.java @@ -0,0 +1,19 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.model.cultivate.v2.position.req.V2CultivateCommonCourseForAppReq; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 培训标签关联表 Mapper + */ +public interface FtbCultivateLabelMapper extends BaseMapper { + /** + * 通用课程标签列表 + */ + List commonCourseLabelList(@Param("params") V2CultivateCommonCourseForAppReq params); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnCategoriesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnCategoriesMapper.java new file mode 100644 index 0000000..731d595 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnCategoriesMapper.java @@ -0,0 +1,19 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnCategories; +import jnpf.model.notice.domain.FtbNoticeCategories; + +/** +* @author 许贵林 +* @description 数据库操作Mapper +* @createDate 2024-05-08 09:42:09 +* @Entity jjnpf.model.cultivate.po.learn.FtbCultivateLearnCategories +*/ +public interface FtbCultivateLearnCategoriesMapper extends BaseMapper { + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskAssignmentMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskAssignmentMapper.java new file mode 100644 index 0000000..3731f69 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskAssignmentMapper.java @@ -0,0 +1,61 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.storestatistics.FtbCultivateStoreStatisticsReq; +import jnpf.model.cultivate.dto.storestatistics.dto.StoreCultivateTaskDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskFinishUserListVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskAssignmentMapper extends BaseMapper { + /** + * 批量查询统计任务的学习人数 + * + * @param taskIds 任务id + * @param status 0-查询全部 1-查询已经完成的 + * @return + */ + List groupCountNum(@Param("taskIds") List taskIds, @Param("status") Integer status); + + /** + * 查询任务学习人员列表 + * + * @param taskIds 任务ID列表 + * @return + */ + List groupListAssignment(@Param("taskIds") List taskIds); + + /** + * 查询我的任务列表 + * + * @param userId 用户ID + * @return + */ + List storeMyTaskList(@Param("userId") String userId); + + /** + * 查询任务数量 + * + * @param userId 用户ID + * @param type:0-全部 1-进行中 2-已完成 + * @param req 查询参数 + * @return 任务数量 + */ + Long queryTaskNum(@Param("userId") String userId, @Param("type") Integer type, @Param("req") FtbCultivateStoreStatisticsReq req); + + /** + * 批量插入任务分配记录 + */ + int insertBatch(@Param("list") List list); + + + Page queryTaskUserListForManagerApp(@Param("page") Page page, @Param("taskId") String taskId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCertificateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCertificateMapper.java new file mode 100644 index 0000000..c3381f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCertificateMapper.java @@ -0,0 +1,42 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCertificate; +import jnpf.model.cultivate.resp.TaskRelationCertificateVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskCertificateVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskCertificateMapper extends BaseMapper { + /** + * 批量根据任务id查询证书名称 + * + * @param taskIds 任务ID集合 + * @return + */ + List queryCertificateName(@Param("taskIds") List taskIds); + + int insertBatch(@Param("list") List list); + + /** + * 根据任务 ID 查询证书列表(关联证书表,过滤上架且未删除) + * + * @param taskId 任务 ID + * @return 证书列表 + */ + List listByTaskId(@Param("taskId") String taskId); + + /** + * 根据任务ID和阶段ID查询证书列表(关联证书表,过滤上架且未删除) + * + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @return 证书列表 + */ + List listByTaskIdAndPhaseId(@Param("taskId") String taskId, @Param("phaseId") String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCourseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCourseMapper.java new file mode 100644 index 0000000..f06856d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskCourseMapper.java @@ -0,0 +1,42 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCourse; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskCourseVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskCourseMapper extends BaseMapper { + /** + * 批量查询任务的课程数量 + * + * @param taskIds 任务id集合 + * @return + */ + List groupCountNum(@Param("taskIds") List taskIds); + + /** + * 批量查询任务的课程列表 + * + * @param taskIds 任务ID集合 + * @return + */ + List groupListTaskCourse(@Param("taskIds") List taskIds); + + /** + * 根据任务 ID 查询课程列表 (联表查询课程表,要求课程上架且未删除) + * + * @param taskId 任务 ID + * @param required 是否必修 (0 是 1 否) + * @return + */ + List listByTaskId(@Param("taskId") String taskId, @Param("required") Integer required); + + List listByTaskIdAndPhaseId(@Param("taskId") String taskId, @Param("phaseId") String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskExamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskExamMapper.java new file mode 100644 index 0000000..36d6ccc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskExamMapper.java @@ -0,0 +1,66 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskExam; +import jnpf.model.cultivate.resp.TaskRelationExamVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskExamVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskExamMapper extends BaseMapper { + /** + * 批量查询任务管理的考试信息 + * + * @param taskIds 任务id集合 + * @return 任务关联的考试 + */ + List queryExamBaseInfo(@Param("taskIds") List taskIds); + + /** + * 根据考试ID查询任务列表(进行中) + * + * @param examId 考试ID + */ + List queryTaskListForExam(@Param("examId") String examId); + + + /** + * 根据考试ID查询任务列表(未发布,未开始) + * + * @param examId 考试ID + */ + List queryTaskListForExamNoPublish(@Param("examId") String examId); + + /** + * 根据考试ID查询任务列表(未发布,未开始,进行中) + * + * @param examId 考试ID + */ + List queryTaskListForExamAll(@Param("examId") String examId); + + int insertBatch(@Param("list") List list); + + /** + * 根据任务 ID 查询考试列表 (关联考试表,要求考试未删除且有效) + * + * @param taskId 任务 ID + * @return 任务关联的考试列表 + */ + List listByTaskIdWithExam(@Param("taskId") String taskId); + + List listByTaskIdAndPhaseId(@Param("taskId") String taskId, @Param("phaseId") String phaseId); + + /** + * 检查考试是否被学习任务绑定(关联任务主表) + * + * @param examId 考试ID + * @return 绑定数量(0或1) + */ + Integer checkExamBinding(@Param("examId") String examId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskIdentificationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskIdentificationMapper.java new file mode 100644 index 0000000..75a1efa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskIdentificationMapper.java @@ -0,0 +1,50 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.resp.TaskRelationIdentificationVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskIdentificationVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +public interface FtbCultivateLearnTaskIdentificationMapper extends BaseMapper { + /** + * 根据任务id查询关联的认证信息 + * @param taskIds 任务id集合 + * @return + */ + List queryIdentificationName(@Param("taskIds") List taskIds); + + /** + * 根据任务id查询关联的认证信息 + * @param taskId 任务ID + * @return + */ + FtbCultivateLearnTaskIdentification queryByTaskId(@Param("taskId") String taskId); + + int insertBatch(@Param("list") List list); + + /** + * 根据任务 id 查询关联的鉴定表信息(联表查询) + * @param taskId 任务 ID + * @return + */ + List listByTaskId(@Param("taskId") String taskId); + + /** + * 根据任务ID和阶段ID查询关联的鉴定信息(联表查询) + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @return + */ + List listByTaskIdAndPhaseId(@Param("taskId") String taskId, @Param("phaseId") String phaseId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoContentMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoContentMapper.java new file mode 100644 index 0000000..74d39a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoContentMapper.java @@ -0,0 +1,21 @@ +package jnpf.cultivate.mapper; + +import org.apache.ibatis.annotations.Param; + +public interface FtbCultivateLearnTaskInfoContentMapper { + + /** + * 查询考试名称 + */ + String queryNameBasedOnExamId(@Param("examId") String examId); + + /** + * 查询鉴定名称 + */ + String queryNameBasedOnIdentificaId(@Param("identificationId") String identificationId); + + /** + * 查询任务名称 + */ + String queryNameBasedOnCertificateId(@Param("certificateId") String certificateId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoMapper.java new file mode 100644 index 0000000..2348b21 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskInfoMapper.java @@ -0,0 +1,80 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskListVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateMyLearnTaskListVO; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskCertificateUserInfoVO; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskCourseUserInfoVO; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskExamUserInfoVO; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskIdentificationUserInfoVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/13:16:36 + */ +public interface FtbCultivateLearnTaskInfoMapper { + /** + * 查询我的任务列表 + * @param tPage + * @param taskListDto + * @return + */ + Page queryTaskList(@Param("page") Page tPage, @Param("taskListDto") FtbCultivateLearnTaskListDto taskListDto); + + /** + * 查看任务详情列表 + * @param taskId + * @param ids + * @return + */ + List getListOfCompletions( @Param("taskId") String taskId, @Param("ids") List ids); + + /** + * 查询对应课程完成情况 + * @param taskId + * @param userId + * @return + */ + List queryCourseUserInfo(@Param("taskId") String taskId, @Param("userId") String userId); + + /** + * 查询证书信息 + * @param taskId + * @param userId + * @return + */ + FtbCultivateLearnTaskCertificateUserInfoVO queryCertificateInfo(@Param("taskId") String taskId, @Param("userId") String userId); + + /** + * 查询实操鉴定 + * @param taskId + * @param userId + * @return + */ + FtbCultivateLearnTaskIdentificationUserInfoVO queryIdentificationInfo(@Param("taskId") String taskId, @Param("userId") String userId); + + /** + * 查询考试信息 + * @param taskId + * @param userId + * @return + */ + FtbCultivateLearnTaskExamUserInfoVO queryExamInfo(@Param("taskId") String taskId, @Param("userId") String userId); + + /** + * 查询任务列表 + * @param page + * @param taskListDto + * @return + */ + Page getTaskList(@Param("taskId")Page page, + @Param("taskListDto") FtbCultivateLearnTaskListDto taskListDto); + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskMapper.java new file mode 100644 index 0000000..48b332b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskMapper.java @@ -0,0 +1,111 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.learn.NeedPerDayAlertDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.req.learn.QueryLearnTaskCountListReq; +import jnpf.model.cultivate.req.learn.QueryLearnTaskListReq; +import jnpf.model.cultivate.v2.task.req.V2MyCultivateTaskListForReq; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskCountVo; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskListForManagerVo; +import jnpf.model.cultivate.v2.task.vo.V2MyCultivateLearnTaskListVo; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoCountListVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoListVO; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskMapper extends BaseMapper { + /** + * 查询分类下任务数量 + * + * @return + */ + List groupCountCateNum(); + + /** + * web查询任务列表 + * + * @param page + * @param params + * @return + */ + Page getMyWebPageList(@Param("page") Page page, @Param("params") QueryLearnTaskListReq params); + + /** + * 查询学习任务统计列表 + * + * @param page + * @param params + * @return + */ + Page queryLearnCountList(@Param("page") Page page, @Param("params") QueryLearnTaskCountListReq params); + + /** + * 查询需要提醒的任务列表 + * + * @return + */ + List queryNeedPerDayAlert(); + + /** + * 查询任务列表及其统计信息 + * + * @param page + * @param keyWord + * @param status + * @return + */ + Page queryTaskListForApp( + @Param("page") Page page, + @Param("keyWord") String keyWord, + @Param("status") Integer status); + + Long queryTaskCountForApp(@Param("keyWord") String keyWord, @Param("status") Integer status); + + Page queryMyTaskListForApp(@Param("page") Page page, + @Param("params") V2MyCultivateTaskListForReq params, + @Param("userId") String userId); + + Integer queryMyTaskCountForApp(@Param("keyWord") String keyWord, @Param("userId") String userId, @Param("status") Integer status); + + /** + * 查询未完成任务列表 + * + * @param userId 用户id + * @return 列表 + */ + List queryMyNoCompleteTaskListForUserId(@Param("userId") String userId); + + List queryTaskHasExam(@Param("taskIds") List taskIds, @Param("examId") String examId); + + List queryTaskHasPracticeId(@Param("taskIds") List taskIds, @Param("practiceId") String practiceId); + + List queryTaskHasIdentityId(@Param("taskIds") List taskIds, @Param("identityId") String identityId); + + List queryMyRunningTask(@Param("userId") String userId, @Param("courseId") String courseId); + + /** + * V2查询任务统计列表 + * + * @param page + * @param taskName + * @param status + * @param creatorTimeStart + * @param creatorTimeEnd + * @return + */ + Page queryV2TaskCountList( + @Param("page") Page page, + @Param("taskName") String taskName, + @Param("status") Integer status, + @Param("creatorTimeStart") Date creatorTimeStart, + @Param("creatorTimeEnd") Date creatorTimeEnd); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPhaseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPhaseMapper.java new file mode 100644 index 0000000..47e51a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPhaseMapper.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPhase; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Title: 学习任务阶段表 + * @Author: + * @create: 2025-01-14 + */ +public interface FtbCultivateLearnTaskPhaseMapper extends BaseMapper { + /** + * 批量查询统计任务的阶段数量 + * @param taskIds 任务id + * @return + */ + List groupCountNum(@Param("taskIds") List taskIds); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPracticeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPracticeMapper.java new file mode 100644 index 0000000..c77e559 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskPracticeMapper.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPractice; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskPracticeVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Title: 学习任务关联练习表 + * @Author: + * @create: 2025-01-14 + */ +public interface FtbCultivateLearnTaskPracticeMapper extends BaseMapper { + + int insertBatch(@Param("list") List list); + + /** + * 根据任务 ID 查询练习列表(关联技能表,过滤未删除) + * @param taskId 任务 ID + * @return 练习列表 + */ + List listByTaskId(@Param("taskId") String taskId); + + /** + * 根据任务ID和阶段ID查询练习列表(关联技能表,过滤未删除) + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @return 练习列表 + */ + List listByTaskIdAndPhaseId(@Param("taskId") String taskId, @Param("phaseId") String phaseId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskReminderRuleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskReminderRuleMapper.java new file mode 100644 index 0000000..d4a0684 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateLearnTaskReminderRuleMapper.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskCourseInfoDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskReminderRule; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +public interface FtbCultivateLearnTaskReminderRuleMapper extends BaseMapper { + /** + * 任务课程 + */ + List getLearnTaskCourseList(@Param("taskId") String taskId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMessageInfoMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMessageInfoMapper.java new file mode 100644 index 0000000..8499aa3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMessageInfoMapper.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.mesgg.CultivateMessageInfo; + +/** + * @Title: 消息类 + * @Author:peng.hao + * @create: 2024/1/22 19:08 + */ +public interface FtbCultivateMessageInfoMapper extends BaseMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMyLearnTaskInfoMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMyLearnTaskInfoMapper.java new file mode 100644 index 0000000..47cfd31 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateMyLearnTaskInfoMapper.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.mapper; + +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/13:11:10 + */ +public interface FtbCultivateMyLearnTaskInfoMapper { + /** + * 查询任务课程列表 + * + * @param taskId + * @param userId + * @param compulsory + * @return + */ + List queryCourseList(@Param("taskId") String taskId , @Param("userId") String userId,@Param("compulsory") Integer compulsory); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineCourseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineCourseMapper.java new file mode 100644 index 0000000..68a126e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineCourseMapper.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineCourse; + +public interface FtbCultivateOfflineCourseMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineTrainMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineTrainMapper.java new file mode 100644 index 0000000..5dc6154 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineTrainMapper.java @@ -0,0 +1,66 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineTrainPeopleSigningInDTO; +import jnpf.model.cultivate.dto.storestatistics.dto.StoreOfflineTrainDto; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineTrain; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainPageVO; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineTrainPeopleSigningInVO; +import jnpf.model.cultivate.vo.offline.FtbOfflineTrainingAppPageVO; +import jnpf.model.cultivate.vo.offline.OfflineCourseVO; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +public interface FtbCultivateOfflineTrainMapper extends BaseMapper { + /** + * 线下培训课程信息 + */ + List listOfflineCourseVO(@Param("offlineId") String offlineId, @Param("userId") String userId); + + /** + * 线下培训课程信息根据线下培训id查询 + */ + List listOfflineWebCourseVO(@Param("offlineId") String offlineId); + + /** + * 线下培训分页 + */ + Page listPage(@Param("page") Page page, @Param("keywords") String keywords); + + /** + * 线下培训app查询 + */ + Page listOfflineTrainingApp(@Param("page") Page page + , @Param("type") Integer type + , @Param("userId") String userId); + + /** + * 线下培训附件删除 + */ + Integer deleteOfflineTrainFileById(@Param("id") String id); + + /** + * 线下培训已绑定课程 + */ + List queryCourseIdsByOfflineId(@Param("offlineId") String offlineId); + + /** + * 线下培训签到人数分页查询 + */ + Page numberOfflineTraining(@Param("page") Page page + , @Param("params") FtbCultivateOfflineTrainPeopleSigningInDTO data); + + List queryMyOfflineTrainList(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + List queryMyOfflineTrainingHeader(@Param("userId") String userId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); + + /** + * 离职删除签到用户 + * @param userIds 用户列表 + */ + void deleteLeaveUser(@Param("userIds") List userIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineUserMapper.java new file mode 100644 index 0000000..f7796a6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateOfflineUserMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineUser; + +public interface FtbCultivateOfflineUserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePackageCourseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePackageCourseMapper.java new file mode 100644 index 0000000..a883609 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePackageCourseMapper.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.coursepackage.FtbCultivatePackageCourse; + +public interface FtbCultivatePackageCourseMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCertificateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCertificateMapper.java new file mode 100644 index 0000000..74c5f8d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCertificateMapper.java @@ -0,0 +1,18 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCertificate; +import org.apache.ibatis.annotations.Param; + +public interface FtbCultivatePositionCertificateMapper extends BaseMapper { + /** + * 查询岗位学习鉴定 + */ + String queryJobPracticalAppraisalByPostId(@Param("postId") String postId); + + /** + * 查询岗位学习考试 + */ + String queryJobPracticalExamByPostId(@Param("postId") String postId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceChapterLearningMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceChapterLearningMapper.java new file mode 100644 index 0000000..2de66e4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceChapterLearningMapper.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.vo.course.app.ChapterInformationVO; +import jnpf.model.cultivate.vo.position.FtbCultivateCourseListVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePositionCourceChapterLearningMapper extends BaseMapper { + /** + * 课程大纲 + */ + List listChapterByCourseId(@Param("courseId") String courseId, @Param("userId") String userId); + + /** + * 课程列表 + */ + Page getCourseInformationList(@Param("page") Page pageList, @Param("userId") String userId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceLearningMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceLearningMapper.java new file mode 100644 index 0000000..e8953aa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourceLearningMapper.java @@ -0,0 +1,100 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.position.app.FtbCultivatePositionForAppNewDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.course.vo.UserLearningStatusVo; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.position.app.FtbSubordinateLearningCoursesVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePositionCourceLearningMapper extends BaseMapper { + + /** + * 岗位学习课程列表 + * + * @param userId 用户 ID + * @param postId 岗位 ID + * @param type 查询类型,0全局课程,1本岗位课程 + * @return {@link Page}<{@link FtbGlobalCurriculumAppVO}> + */ + Page globalCurriculumList(@Param("page") Page page, @Param("userId") String userId, + @Param("postId") String postId, + @Param("gradeId") String gradeId, @Param("type") Integer type); + + /** + * 是否为线下培训课程 + */ + Integer whetherToAddOfflineTrainingCourses(@Param("userId") String userId, @Param("courseId") String courseId); + + /** + * 岗位学习-下属实操鉴定 + */ + Page practicalAppraisalList(@Param("page") Page voPage, @Param("userId") String userId, @Param("gradeIds") List gradeIds); + + /** + * 本岗课程内容 + */ + List onTheJobLearningCourse(@Param("params") FtbCultivatePositionForAppNewDTO dto); + + /** + * 查看课程培训数据 + */ + Page subordinateLearningCourses(@Param("page") Page result, @Param("params") FtbCultivatePositionForAppNewDTO dto); + + /** + * 下属学习课程 + */ + Page subordinateLearningCoursess(@Param("page") Page result, @Param("params") FtbCultivatePositionForAppNewDTO dto + , @Param("postIds") List postIds); + + /** + * 批量查询用户完成的学习课程 + * @param userIds 用户ID + * @return + */ + List listCompleteCourseForUserIds(@Param("userIds") List userIds); + + /** + * 计算学习时长 + * @param courseIds 课程ID + * @param userId 用户ID + */ + Long queryStudyTime(@Param("courseIds") List courseIds, @Param("userId") String userId); + + /** + * 批量查询课程完成情况 + * @param courseIds 课程ID + * @return + */ + List countForCourseIds(@Param("courseIds") List courseIds); + + List queryUserUnreadMsgV2(@Param("userId") String userId, @Param("courseId") String courseId); + + /** + * 根据课程ID列表和用户ID统计已完成的学习课程数量 + * + * @param courseIds 课程ID列表 + * @param userId 用户ID + * @return 已完成课程数量 + */ + Long countCompletedCoursesByUserAndCourseIds(@Param("courseIds") List courseIds, + @Param("userId") String userId); + + /** + * 列表课程统计 + */ + List accessCourseStatistics(@Param("userIds") List userIds); + + /** + * 查询用户已完成的课程列表(关联查询课程信息) + * @param userId 用户ID + * @return 已完成课程列表 + */ + List queryCompletedCoursesWithInfo(@Param("userId") String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseCertificateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseCertificateMapper.java new file mode 100644 index 0000000..c797e60 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseCertificateMapper.java @@ -0,0 +1,52 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseCertificateWithNameVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/10:10:30 +*/ +public interface FtbCultivatePositionCourseCertificateMapper extends BaseMapper { + + /** + * 根据岗位学习 ID 列表统计证书数量 + */ + List countByPositionLearnIds(@Param("positionLearnIds") List positionLearnIds); + + /** + * 根据岗位学习 ID 查询证书列表(关联证书表,过滤上架且未删除) + */ + List listByPostLearnId(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 IDs 查询证书列表(关联证书表,过滤上架且未删除) + */ + List listByPostLearnIdAndCourseIds(@Param("postLearnId") String postLearnId, @Param("courseIds") List courseIds); + + /** + * 根据岗位学习ID查询证书列表(包含证书名称) + * + * @param postLearnId 岗位学习 ID + * @return FtbCultivatePositionCourseCertificateWithNameVo 对象列表 + */ + List listByPostLearnIdWithName(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 IDs 查询证书列表(包含证书名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return FtbCultivatePositionCourseCertificateWithNameVo 对象列表 + */ + List listByPostLearnIdAndCourseIdsWithName( + @Param("postLearnId") String postLearnId, + @Param("courseIds") List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseExamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseExamMapper.java new file mode 100644 index 0000000..ce91183 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseExamMapper.java @@ -0,0 +1,70 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseExamWithNameVo; +import jnpf.model.cultivate.v2.position.vo.V2AllCultivatePositionCourseExam; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePositionCourseExamMapper extends BaseMapper { + + /** + * 根据岗位学习 ID 列表统计考试数量 + */ + List countByPositionLearnIds(@Param("positionLearnIds") List positionLearnIds); + + /** + * 根据岗位学习 ID 查询考试(关联考试表,要求考试未删除且有效) + */ + List listByPostLearnIdWithExam( + @Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询考试(关联考试表,要求考试未删除且有效) + */ + List listByPostLearnIdAndCourseIdsWithExam( + @Param("postLearnId") String postLearnId, + @Param("courseIds") List courseIds, + @Param("gradeId") String gradeId); + + /** + * 查询所有岗位学习考试(关联考试表,要求考试未删除且有效) + */ + List queryAllPositionLearnExamListsWithExam( + @Param("req") Object req); + + List queryPositionBindItem(@Param("position") FtbCultivatePosition position, @Param("gradeId") String gradeId); + + List queryAllConfigExamId(@Param("postLearnIds") List postLearnIds, @Param("courseId") String courseId, @Param("gradeId") String gradeId); + + /** + * 根据岗位学习ID查询所有有效考试(包含考试名称) + * + * @param postLearnId 岗位学习 ID + * @return FtbCultivatePositionCourseExamWithNameVo 对象列表 + */ + List listByPostLearnIdWithName(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询考试(包含考试名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return FtbCultivatePositionCourseExamWithNameVo 对象列表 + */ + List listByPostLearnIdAndCourseIdsWithName( + @Param("postLearnId") String postLearnId, + @Param("courseIds") List courseIds); + + /** + * 检查考试是否被岗位学习绑定(关联岗位学习主表和课程表) + * + * @param examId 考试ID + * @return 绑定数量(0或1) + */ + Integer checkExamBinding(@Param("examId") String examId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseIdentityMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseIdentityMapper.java new file mode 100644 index 0000000..2c468b9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseIdentityMapper.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseIdentityWithNameVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePositionCourseIdentityMapper extends BaseMapper { + + /** + * 根据岗位学习 ID 列表统计鉴定数量 + */ + List countByPositionLearnIds(@Param("positionLearnIds") List positionLearnIds); + + /** + * 根据岗位学习 ID 查询鉴定列表(关联鉴定表) + */ + List listByPostLearnId(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询鉴定列表(关联鉴定表) + */ + List listByPostLearnIdAndCourseIds(@Param("postLearnId") String postLearnId, @Param("courseIds") List courseIds, @Param("gradeId") String gradeId); + + /** + * 查询所有岗位学习鉴定列表(关联鉴定表) + */ + List queryAllPositionLearnIdentityLists(); + + List queryPositionBindItem(@Param("position") FtbCultivatePosition position, @Param("gradeId") String gradeId); + + /** + * 根据岗位学习ID查询鉴定列表(包含鉴定名称) + * + * @param postLearnId 岗位学习 ID + * @return FtbCultivatePositionCourseIdentityWithNameVo 对象列表 + */ + List listByPostLearnIdWithName(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询鉴定列表(包含鉴定名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return FtbCultivatePositionCourseIdentityWithNameVo 对象列表 + */ + List listByPostLearnIdAndCourseIdsWithName( + @Param("postLearnId") String postLearnId, + @Param("courseIds") List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseMapper.java new file mode 100644 index 0000000..d7fc4d0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCourseMapper.java @@ -0,0 +1,150 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.v2.course.vo.app.AppCourseSimpleVo; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.CultivatePositionSimpleVo; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseWithNameVo; +import jnpf.model.cultivate.v2.position.vo.V2CultivatePositionDetailForApp; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePositionCourseMapper extends BaseMapper { + /** + * 学习课程数 + */ + Long numberOfStudyCourses(@Param("postLearnId") String postLearnId); + + /** + * 该岗位是否绑定课程 + */ + List learnMapRelearn(@Param("positionId") String positionId); + + /** + * 课程是否初始化 + */ + List getAnExistingCoursePostId(@Param("postId") String postId); + + /** + * 课程是否存在 + */ + List getAnExistingCourse(@Param("courseIds") List courseIds, @Param("userId") String userId); + + /** + * 判断是否为最后一个岗位学习课程 + */ + Integer determineTheLastPostStudyCourse(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习ID列表统计课程数量 + * + * @param positionLearnIds 岗位学习ID列表 + * @return BatchCommonCountDto对象列表,包含统计结果 + */ + List countByPositionLearnIds(@Param("positionLearnIds") List positionLearnIds); + + /** + * 查询所有岗位学习信息 + * + * @param courseName 课程名称(可选查询条件) + * @return CultivatePositionSimpleVo对象列表,包含岗位学习基本信息 + */ + List queryAllPositionLearn(@Param("courseName") String courseName); + + /** + * 查询岗位学习课程列表(移动端使用) + * + * @param params 查询参数对象,包含V2OtherCultivatePositionCourseForAppReq类型参数 + * @return AppCourseSimpleVo对象列表,包含课程简单信息 + */ + List queryAllPositionLearnCourseLists(@Param("params") V2OtherCultivatePositionCourseForAppReq params); + + /** + * 根据岗位学习ID、年级ID和考试ID查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习ID + * @param gradeId 职级ID + * @param examId 考试ID + * @return FtbCultivatePositionCourse对象列表,包含培养岗位课程信息 + */ + List listByPostLearnIdAndExamId(@Param("positionLearnId") String positionLearnId, @Param("gradeId") String gradeId, @Param("examId") String examId); + + /** + * 根据岗位学习ID、年级ID和实践ID查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习ID + * @param gradeId 职级ID + * @param practiceId 练习id + * @return FtbCultivatePositionCourse对象列表,包含培养岗位课程信息 + */ + List listByPostLearnIdAndPracticeId(@Param("positionLearnId") String positionLearnId, @Param("gradeId") String gradeId, @Param("practiceId") String practiceId); + + /** + * 根据岗位学习ID、年级ID和身份ID查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习ID + * @param gradeId 职级ID + * @param identityId 鉴定id + * @return FtbCultivatePositionCourse对象列表,包含培养岗位课程信息 + */ + List listByPostLearnIdAndIdentityId(@Param("positionLearnId") String positionLearnId, @Param("gradeId") String gradeId, @Param("practiceId") String identityId); + + /** + * 查询所有岗位学习课程列表 + * + * @param positionLearnId 岗位学习ID + * @param gradeId 职级ID + * @return FtbCultivatePositionCourse对象列表,包含岗位学习课程信息 + */ + List queryAllPositionCourseAndLearnIdAndGradeId(@Param("positionLearnId") String positionLearnId, @Param("gradeId") String gradeId); + + List listPositionCourseList( + @Param("position") FtbCultivatePosition position, + @Param("req") V2CultivatePositionDetailForApp req); + + /** + * 根据岗位学习 ID 查询所有有效课程(关联课程表,过滤上架且未删除) + * + * @param postLearnId 岗位学习 ID + * @param courseId 课程 ID + * @return FtbCultivatePositionCourse 对象列表 + */ + List listByPostLearnId(@Param("postLearnId") String postLearnId, @Param("courseId") String courseId); + + /** + * 根据岗位学习 ID 和职级 ID 查询所有有效课程(关联课程表,过滤上架且未删除) + * + * @param postLearnId 岗位学习 ID + * @param gradeId 职级 ID + * @param courseId 课程 ID + * @return FtbCultivatePositionCourse 对象列表 + */ + List listByPostLearnIdAndGradeId(@Param("postLearnId") String postLearnId, @Param("gradeId") String gradeId, @Param("courseId") String courseId); + + List queryPositionBindItem(@Param("position") FtbCultivatePosition position, @Param("gradeId") String gradeId); + + /** + * 根据岗位学习ID查询所有有效课程(包含课程名称) + * + * @param postLearnId 岗位学习 ID + * @param courseId 课程 ID + * @return FtbCultivatePositionCourseWithNameVo 对象列表 + */ + List listByPostLearnIdWithName(@Param("postLearnId") String postLearnId, @Param("courseId") String courseId); + + /** + * 根据岗位学习配置查询所有课程ID列表(关联课程表过滤) + * + * @param postLearnId 岗位学习ID + * @param isConfiguredToGrade 是否配置到职级(0-否,1-是) + * @param gradeId 职级ID(仅当isConfiguredToGrade=1时有效) + * @return 课程ID列表 + */ + List queryCourseIdsByPositionAndGrade(@Param("postLearnId") String postLearnId, + @Param("isConfiguredToGrade") Integer isConfiguredToGrade, + @Param("gradeId") String gradeId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCoursePracticeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCoursePracticeMapper.java new file mode 100644 index 0000000..622ae44 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionCoursePracticeMapper.java @@ -0,0 +1,59 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCoursePractice; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCoursePracticeWithNameVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Title: 岗位学习课程关联练习表 + * @Author: + * @create: 2025-01-14 + */ +public interface FtbCultivatePositionCoursePracticeMapper extends BaseMapper { + + /** + * 根据岗位学习 ID 列表统计练习数量 + */ + List countByPositionLearnIds(@Param("positionLearnIds") List positionLearnIds); + + /** + * 根据岗位学习 ID 查询练习列表(关联技能表) + */ + List listByPostLearnId(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询练习列表(关联技能表) + */ + List listByPostLearnIdAndCourseIds(@Param("postLearnId") String postLearnId, @Param("courseIds") List courseIds); + + /** + * 查询所有岗位学习课程练习列表(关联技能表) + */ + List queryAllPositionLearnParacticeLists(@Param("req") Object req); + + List queryPositionBindItem(@Param("position") FtbCultivatePosition position, @Param("gradeId") String gradeId); + + /** + * 根据岗位学习ID查询练习列表(包含技能点名称) + * + * @param postLearnId 岗位学习 ID + * @return FtbCultivatePositionCoursePracticeWithNameVo 对象列表 + */ + List listByPostLearnIdWithName(@Param("postLearnId") String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询练习列表(包含技能点名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return FtbCultivatePositionCoursePracticeWithNameVo 对象列表 + */ + List listByPostLearnIdAndCourseIdsWithName( + @Param("postLearnId") String postLearnId, + @Param("courseIds") List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamIdentifyMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamIdentifyMapper.java new file mode 100644 index 0000000..eb36b61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamIdentifyMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; + +public interface FtbCultivatePositionExamIdentifyMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamMapper.java new file mode 100644 index 0000000..72c21c6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionExamMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; + +public interface FtbCultivatePositionExamMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionIdentifyResultMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionIdentifyResultMapper.java new file mode 100644 index 0000000..c527112 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionIdentifyResultMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; + +public interface FtbCultivatePositionIdentifyResultMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionLogMapper.java new file mode 100644 index 0000000..2808030 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionLogMapper.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionLog; + + +public interface FtbCultivatePositionLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionMapper.java new file mode 100644 index 0000000..9b420f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionMapper.java @@ -0,0 +1,70 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionCourseLevelPageDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionCoursePageDTO; +import jnpf.model.cultivate.dto.position.FtbJobLearningPaginDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.v2.course.vo.app.AppCourseSimpleVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCultivateCourseExamVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCultivateCourseIdentityVo; +import jnpf.model.cultivate.v2.course.web.req.V2NextUserCultivateCourseListReq; +import jnpf.model.cultivate.v2.position.vo.AppPracticeCountVo; +import jnpf.model.cultivate.v2.position.vo.WebPositionLearningListVo; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionJobLearnCourseVO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionLearnLevelVO; +import jnpf.model.cultivate.vo.position.FtbJobLearningPaginatedVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePositionMapper extends BaseMapper { + /** + * 岗位学习已学习课程列表 + */ + List jobLearningCourseList(@Param("params") FtbCultivatePositionCoursePageDTO ftbCultivatePositionCoursePageDTO); + + /** + * 岗位学习中课程列表页 + */ + Page jobLevelCourseList(@Param("page") Page page, + @Param("params") FtbCultivatePositionCourseLevelPageDTO ftbCultivatePositionCourseLevelPageDTO); + + /** + * 岗位学习分页列表 + */ + Page jobLearningPaginatedList(@Param("page") Page page, + @Param("params") FtbJobLearningPaginDTO ftbJobLearningPaginDTO); + + /** + * 实操鉴定,根据岗位id+用户id查询,解决换绑问题 + */ + String queryPracticalAppraisalByPostId(@Param("postId") String postId, @Param("userId") String userId); + + /** + * 是否已经鉴定 + */ + Integer queryIdentificationResults(@Param("postId") String postId, @Param("userId") String userId, @Param("identifyId") String identifyId); + + Page webPageList(@Param("page") Page page, @Param("params") FtbJobLearningPaginDTO params); + + Page allCompleteCourseLists(@Param("page") Page page, + @Param("req") V2NextUserCultivateCourseListReq req, + @Param("courseIds") List courseIds + ); + + Page allCompleteExamLists(@Param("page") Page page, + @Param("req") V2NextUserCultivateCourseListReq req, + @Param("examIds") List examIds); + + Page allCompleteIdentityLists( + @Param("page") Page page, + @Param("req") V2NextUserCultivateCourseListReq req, + @Param("identityIds") List identityIds, + @Param("positionLearnIds") List positionLearnIds); + + List allCompletePracticeLists( + @Param("req") V2NextUserCultivateCourseListReq req, + @Param("practiceIds") List practiceIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionSettingMapper.java new file mode 100644 index 0000000..029bef3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionSettingMapper.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionSetting; +import org.apache.ibatis.annotations.Mapper; + + +public interface FtbCultivatePositionSettingMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionStatisticesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionStatisticesMapper.java new file mode 100644 index 0000000..f379e55 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionStatisticesMapper.java @@ -0,0 +1,224 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.position.FtbCultivatePersonStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionPersonStatisticesDTO; +import jnpf.model.cultivate.v2.position.vo.CultivatePositionCourseVo; +import jnpf.model.cultivate.v2.position.vo.PersonForLeaderVo; +import jnpf.model.cultivate.v2.position.vo.V2StudyCountVo; +import jnpf.model.cultivate.v2.statistics.V2UserCourseStudyVo; +import jnpf.model.cultivate.vo.course.web.UserCourseStudyVo; +import jnpf.model.cultivate.vo.position.*; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface FtbCultivatePositionStatisticesMapper { + + /** + * 个人维度统计 + */ + Page personListStatistics(@Param("page") Page page, @Param("params") FtbCultivatePositionPersonStatisticesDTO params); + + /** + * 下属参与学习人数 + */ + Integer numberOfPeopleParticipatingInTheStudy1(@Param("userIds") List userIds, @Param("innerCourseIds") List innerCourseIds); + + /** + * 查询课程id + */ + List queryCourseIds(); + + /** + * 下属人均学习时长 + */ + Integer averageStudyHoursPerSubordinate1(@Param("userIds") List userIds, @Param("innerCourseIds") List innerCourseIds); + + /** + * 统计课程数 + * + * @param userId 用户id + * @param type 0学习课程数,1 课程完成数 + */ + Integer numberOfStudyCourses(@Param("userId") String userId, @Param("type") Integer type); + + /** + * 统计课程数 + * + * @param userIds 用户id + * @param type 0学习课程数,1 课程完成数 + * @param innerCourseIds + */ + List batchNumberOfStudyCourses2(@Param("userIds") List userIds, @Param("type") Integer type, @Param("innerCourseIds") List innerCourseIds); + + /** + * 岗位维度统计 + */ + Page positionListStatistics(@Param("page") Page page, + @Param("params") FtbCultivatePersonStatisticesDTO params); + + /** + * 岗位学习课程总数 + */ + List totalNumberOfOnTheJobLearningCourses(@Param("postLearnIds") List postLearnIds); + + /** + * 已学习课程数 + */ + List numberOfCoursesTaken(@Param("postLearnIds") List postLearnIds); + + /** + * 已学习课程数 + */ + Long numberOfCoursesTakenStoreIndex(@Param("postLearnIds") List postLearnIds, @Param("userId") String userId); + + /** + * 岗位学习人数 + */ + List numberOfPeopleStudyingOnTheJob(@Param("userIds") List userIds); + + /** + * 学习时长 + */ + Long studyDuration(@Param("userIds") List userIds); + + /** + * 参与学习人数 + */ + List numberOfPeopleParticipatingInTheStudyOrg(@Param("params") FtbCultivatePositionOrgStatisticesDTO params, @Param("userIds") List userIds, @Param("positionIds") String positionIds); + + /** + * 学习总时长 + */ + Integer totalStudyTime(@Param("params") FtbCultivatePositionOrgStatisticesDTO statisticDTO, @Param("userIds") List userIds, @Param("postionIds") String postionIds); + + /** + * 已学课程数 + */ + Integer numberOfCoursesTakens(@Param("params") FtbCultivatePositionOrgStatisticesDTO statisticDTO, @Param("userIds") List userIds, @Param("postionIds") String postionIds); + + /** + * 课程总数 + */ + Integer totalNumberOfCourses(); + + /** + * 组织课程信息 + */ + List organizeCourseDetails(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("postionIds") String postionIds, + @Param("userIds") List userIds); + + /** + * 已参与学习人数 + */ + List numberOfPeopleParticipatingInTheFinishStudy(@Param("courseIds") List courseIds, @Param("postionIds") String postionIds, @Param("states") List states, @Param("userIds") Set userIds); + + /** + * 个人维度-课程详情 + */ + List personalDimensionCourseDetails(@Param("startDate") String startDate, @Param("endDate") String endDate, + @Param("userId") String userId); + + /** + * 课程学习进度 + */ + Integer courseLearningProgress(@Param("courseId") String courseId, @Param("userId") String userId, @Param("type") Integer type); + + /** + * 岗位学习总数 + */ + Integer jobLearningProgress(@Param("postId") String postId, @Param("userId") String userId, @Param("type") Integer type); + + /** + * 学习地图课程总数 + */ + Integer totalNumberOfLearningMapCourses1(@Param("postIds") List postIds, @Param("innerCourseIds") List innerCourseIds); + + /** + * 员工姓名和员工ID + */ + List> fuzzySearchForRosterKeywords(@Param("keyWords") String keyWords); + + /** + * 岗位学习人数 + */ + Integer numberOfPeopleWhoCompletedTheStudy(@Param("userIds") List userIds); + + /** + * 获取岗位名称 + */ + List getPostIds(@Param("postIds") List postIds); + + /** + * 根据查询岗位查询对应的岗位考试合格分 + */ + @Select("SELECT ex.F_PassMark FROM ftb_cultivate_position_exam as po INNER JOIN ftb_cultivate_exam as ex ON po.F_ExamId = ex.F_Id where po.F_PostRankId =#{positionId} AND po.F_EnabledMark = 0 ") + BigDecimal qualifyingPointsForJobRelatedExaminations(@Param("positionId") String positionId); + + /** + * 根据查询岗位查询对应的岗位实操合格分 + */ + @Select("SELECT fy.F_PassScore FROM ftb_cultivate_position_exam_identify as po INNER JOIN ftb_cultivate_identify_table as fy ON po.F_IdentifyId = fy.F_Id where po.F_PostRankId =#{positionId} AND po.F_EnabledMark = 0") + BigDecimal qualifyingPointsForJobRelatedAppraisal(@Param("positionId") String positionId); + + /** + * 查询岗位下课程完成情况 + */ + List queryCourseCompletionStatusOfThePosition(@Param("positionId") String positionId, + @Param("idsByGradesId") List idsByGradesId, + @Param("type") Integer type); + + /** + * 岗位考试合格率 + */ + List checkJobQualificationRate(@Param("postIds") List postIds); + + /** + * 根据岗位查询课程总数 + * + * @param positionIds 岗位id集合 + * @return + */ + Long courseTotleNum(@Param("positionIds") List positionIds); + + /** + * 查询岗位下已学课程列表 + * + * @param postLearnIds 岗位学习id + * @param userId 用户id + * @return + */ + List queryComplateCourseLists(@Param("postLearnIds") List postLearnIds, @Param("userId") String userId); + + /** + * 查询岗位下已学和学习中的课程列表 + * + * @param postLearnIds 岗位学习id + * @param userId 用户id + * @return + */ + List queryComplateAndStudyingCourseLists(@Param("postLearnIds") List postLearnIds, @Param("userId") String userId); + + + /** + * 查询岗位下所有需要学习的课程 + * + * @param positionIds 岗位id + * @return + */ + List queryAllNeedStudyCourseForPost(@Param("positionIds") List positionIds); + + List batchQueryAllCourseStudyV2(@Param("userIds") List userIds); + + List queryAllNextUser(@Param("leaderUserIds") List leaderUserIds); + + V2StudyCountVo numberOfPeopleParticipatingInTheStudy(@Param("userIds") List userIds, @Param("innerCourseIds") List innerCourseIds); + + List batchQueryAllCourseStudy(@Param("userIds") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionUserMapper.java new file mode 100644 index 0000000..d316f92 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePositionUserMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionUser; + +public interface FtbCultivatePositionUserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionLogMapper.java new file mode 100644 index 0000000..934e3f7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionLogMapper.java @@ -0,0 +1,15 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionLog; +import org.apache.ibatis.annotations.Mapper; + +/** + * 学习地图完成记录 + * + * @author xgl + * @date 2026-01-27 14:04:01 + */ +public interface FtbCultivatePromotionLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMapper.java new file mode 100644 index 0000000..fbcda0f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMapper.java @@ -0,0 +1,76 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.dto.promotion.FtbMapsOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.promotion.FtbMapsPersonWisdomStatisticDTO; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotion; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.vo.promotion.*; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 查询岗位通道 + * @author hao.peng + */ +public interface FtbCultivatePromotionMapper extends BaseMapper { + /** + * 查询通道层级 + * @param page + * @param dto + * @return + */ + Page getList(@Param("page") Page page, @Param("dto") FtbCultivatePromotionDto dto); + + /** + * 根据通道id 查询通道层级 + * @return + */ + FtbCultivatePromotionVO getPromotionChannel( @Param("id") String id); + + /** + * 查询已经开启的成员列表 + * @param dto + * @param page + * @return + */ + Page getPromotionMbeList(@Param("dto")FtbCultivatePromotionDto dto,@Param("page") Page page); + /** + * 根据促销活动ID列表查询对应的促销活动信息 + * + * @param promotionIds 促销活动ID列表 + * @return 返回查询到的促销活动信息列表 + */ + List queryListByIds(@Param("promotionIds") List promotionIds); + + /** + * 查询组织机构列表的智慧统计信息,并进行分页显示 + * + * @param statisticDTO 统计信息的数据传输对象,包含查询条件 + * @param page 分页对象,用于显示分页数据 + * @return 返回分页后的组织机构智慧统计信息 + */ + Page organizationListStatistics(@Param("dto")FtbMapsOrgWisdomStatisticDTO statisticDTO, + @Param("page") Page page); + + /** + * 查询个人列表的智慧统计信息,并进行分页显示 + * + * @param personWisdomStatisticDTO 个人智慧统计信息的数据传输对象,包含查询条件 + * @param page 分页对象,用于显示分页数据 + * @return 返回分页后的个人智慧统计信息 + */ + Page personListStatistics(@Param("dto")FtbMapsPersonWisdomStatisticDTO personWisdomStatisticDTO, + @Param("page")Page page); + + + List queryStudyMapList(@Param("dto") FtbMapsOrgWisdomStatisticDTO dto); + + List queryStudyUser(@Param("dto") FtbMapsOrgWisdomStatisticDTO dto, @Param("queryPromotionIds") List queryPromotionIds); + + List queryMapPostNum(@Param("queryPromotionIds") List queryPromotionIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberMapper.java new file mode 100644 index 0000000..465b7e6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberMapper.java @@ -0,0 +1,62 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMember; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionMemberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionPostVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePromotionMemberMapper extends BaseMapper { + + /** + * 根据用户ID查询用户的推广信息。 + * + * @param userId 用户ID + * @param postId + * @return 对应的FtbCultivatePromotionVO对象 + */ + List queryPromotionByUserOrPostId(@Param("userId") String userId, + @Param("postId") String postId); + /** + * 根据用户ID查询用户的推广信息。 + * + * @param userId 用户ID + * @param OrgId + * @return 对应的FtbCultivatePromotionVO对象 + */ + List queryPromotionByUserOrOrgId(@Param("userId") String userId, + @Param("OrgId") String OrgId); + /** + * 根据用户ID和帖子ID查询推广信息 + * 此方法用于根据特定的用户和帖子信息,查询相关的推广信息 + * + * @param userId 用户ID,用于识别特定的用户 + * @param postId 帖子ID,用于识别特定的帖子 + * @return 返回一个FtbCultivatePromotionVO对象,包含查询到的推广信息 + */ + FtbCultivatePromotionVO queryPromotionByUser(@Param("userId") String userId, + @Param("postId") String postId); + + /** + * 根据用户ID查询用户信息 + * 此方法用于根据特定的用户ID,查询相关的用户信息 + * + * @param userId 用户ID,用于识别特定的用户 + * @return 返回一个FtbCultivatePromotionMemberVO对象,包含查询到的用户信息 + */ + FtbCultivatePromotionMemberVO queryUserInfo(@Param("userId")String userId); + + /** + * 查询已存在的帖子列表 + * 此方法用于查询数据库中所有已存在的帖子,以确定哪些帖子已经被系统处理过 + * + * @return 返回一个FtbCultivatePromotionPostVO对象的列表,包含所有已存在的帖子信息 + */ + List queryPostHasExist(); + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberNewMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberNewMapper.java new file mode 100644 index 0000000..d83fdce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionMemberNewMapper.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePromotionMemberNewMapper extends BaseMapper { + /** + * 查询当前人的学习地图等级 + * + * @param userId + * @param postId + * @param promtionId + * @return + */ + Integer queryTheCurrentUserLearningMapLevel(@Param("userId") String userId, @Param("postId") String postId, @Param("promtionId") String promtionId); + + List selectStudyStep(@Param("userId") String userId, @Param("mainPositionId") String mainPositionId); + + /** + * 查询岗位是否有学习地图 + * + * @param mainPositionId 岗位id + * @return + */ + String queryHasStudyMap(@Param("mainPositionId") String mainPositionId); + + /** + * 查询当前人的学习地图等级 + * + * @param userId 用户id + * @param pomotionId 学习地图id + * @return + */ + Integer queryCurrLevel(@Param("userId") String userId, @Param("pomotionId") String pomotionId); + + /** + * 查询岗位关联的学习地图 + * + * @param postId 岗位id + * @return 学习地图id + */ + List queryRelationMap(@Param("postId") String postId); + + + List queryMyCurrentPhase(@Param("userId") String userId, @Param("promotionIds") List promotionIds); + + List queryMyAllSelectPosition(@Param("userId") String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMapper.java new file mode 100644 index 0000000..c4eac0e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMapper.java @@ -0,0 +1,360 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.course.FtbCultivateSelectPositionDto; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.position.vo.CultivateUserPositionVo; +import jnpf.model.cultivate.v2.promotion.req.FtbCultivatePromotionReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionOrgStatisticReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionPersonStatisticReq; +import jnpf.model.cultivate.v2.promotion.vo.PromotionAndPostVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionOrgStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionPersonStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.WebCultivatePromotionListVo; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivateLearnMapInfoVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionMeberPostInfo; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionNewVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsCourseVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 查询岗位通道 + * @author hao.peng + */ +public interface FtbCultivatePromotionNewMapper extends BaseMapper { +/** + * 获取培养推广列表 + * + * @param page 分页对象,包含分页参数和数据 + * @param dto 查询条件对象,用于筛选列表数据 + * @return 包含分页数据的Page对象 + */ +Page getList(@Param("page") Page page, + @Param("dto") FtbCultivatePromotionDto dto); + +/** + * 查询学习范围 + * + * @param id 查询对象的唯一标识符 + * @return 学习范围的数量 + */ +Integer queryStudyScope(@Param("id") String id); + +/** + * 查询初始化岗位 + * + * @param promotionId 推广活动的唯一标识符 + * @return 包含渠道名称和ID的Map对象 + */ +@Select(" SELECT\n" + + " po.F_PostName channelIniName,\n" + + " po.F_PostId channelId\n" + + " FROM\n" + + " ftb_cultivate_promotion_new AS pr\n" + + " LEFT JOIN ftb_cultivate_promotion_post_new AS po ON pr.F_Id = po.F_PromotionId\n" + + " where\n" + + " pr.F_EnableMark=0\n" + + " AND po.F_Level = 1 and pr.F_Id = #{promotionId}") +Map queryInitializationPositions(@Param("promotionId") String promotionId); + +/** + * 获取某人选择的岗位信息 + * + * @param id 人员的唯一标识符 + * @return 该人员选择的岗位信息列表 + */ +List getThePositionThisPersonHasChosen(@Param("id") String id); + +/** + * 查询用户选择的岗位 + * + * @param userId 用户的唯一标识符 + * @param integer 数字参数,用途需文档化 + * @param id 用户的唯一标识符(冗余参数,应与userId相同) + * @return 用户选择的岗位列表 + */ +List queryUserChosesPosts(@Param("userId")String userId,@Param("integer") Integer integer,@Param("id") String id); + +/** + * 基于岗位查看学习地图 + * + * @param userId 用户的唯一标识符 + * @param postId 岗位的唯一标识符 + * @param level 岗位级别 + * @return 学习地图信息列表 + */ +List viewLearningMapBasedOnPosition(@Param("userId") String userId, + @Param("postId") String postId, + @Param("level") Integer level); +@Select(" SELECT\n" + + " pr.F_Id\n" + + " FROM\n" + + " ftb_cultivate_promotion_new AS pr\n" + + " inner JOIN ftb_cultivate_promotion_post_new AS po ON pr.F_Id = po.F_PromotionId\n" + + " where\n" + + " pr.F_EnableMark=0\n" + + " AND po.F_Level = 1\n" + + "\t\t\t\tAND po.F_PostId =#{postId}") +String initialPositionQueryLearningMapPrimaryKey(@Param("postId") String postId); + +/** + * 查询用户选择的岗位及其ID + * + * @param userId 用户的唯一标识符 + * @param level 岗位级别 + * @param promotionId 推广活动的唯一标识符 + * @return 包含用户选择的岗位及其ID的列表 + */ +List> queryUserChosesPostsWitId(@Param("userId") String userId, + @Param("level") Integer level, + @Param("promotionId") String promotionId); + + /** + * 查询用户选择的岗位及其ID + * + * @param userId 用户的唯一标识符 + * @param promotionId 推广活动的唯一标识符 + * @return 包含用户选择的岗位及其ID的列表 + */ + List queryUserChosesPositionIdWidthId(@Param("userId") String userId, + @Param("promotionId") String promotionId); + +/** + * 查询推广活动的阶段数量 + * + * @param promotionId 推广活动的唯一标识符 + * @return 推广活动的阶段数量 + */ +Integer queryHowManyStages(@Param("promotionId") String promotionId); + +/** + * 在职学习课程查询 + * + * @param userId 用户的唯一标识符 + * @param postIds 岗位ID列表 + * @param compulsory 是否为必修课程的标志(0:非必修,1:必修) + * @return 课程信息列表 + */ +List onTheJobLearningCourse(@Param("userId") String userId, @Param("postIds") List postIds, @Param("compulsory") Integer compulsory); + +/** + * 在职学习课程查询 + * + * @param userId 用户的唯一标识符 + * @param postIds 岗位ID列表 + * @param compulsory 是否为必修课程的标志(0:非必修,1:必修) + * @return 课程信息列表 + */ +List onTheJobLearningCourseNew(@Param("userId") String userId, @Param("postIds") List postIds, @Param("compulsory") Integer compulsory); + +/** + * 基于岗位ID查询已选人员 + * + * @param postIds 岗位ID列表 + * @return 选中人员的列表 + */ +List querySelectedPersonnelBasedOnPositionID(@Param("postIds") List postIds); + +/** + * 查询实践考核学习数量 + * + * @param studyPostIds 学习岗位ID列表 + * @param userId 用户的唯一标识符 + * @param isAPracticalAppraisalIds 是否为实践考核的ID列表 + * @param learningMapSetting 学习地图设置标志 + * @return 符合条件的学习数量 + */ +Integer queryTheNumberOfPracticalAppraisalStudies(@Param("studyPostIds") List studyPostIds, + @Param("userId") String userId, + @Param("isAPracticalAppraisalIds") List isAPracticalAppraisalIds, + @Param("learningMapSetting") Integer learningMapSetting); + +/** + * 检查岗位是否有考试 + * + * @param postId 岗位的唯一标识符 + * @return 是否有考试的标志(0:否,1:是) + */ +Integer checkIfThereAreExamsForThisPosition(@Param("postId")String postId); + +/** + * 检查岗位是否有实践考核 + * + * @param postId 岗位的唯一标识符 + * @return 是否有实践考核的标志列表 + */ +List checkWhetherThereIsAPracticalAppraisal(@Param("postId")String postId); + +/** + * 查询用户选择的岗位ID + * + * @param userId 用户的唯一标识符 + * @param id 推广活动的唯一标识符 + * @return 用户选择的岗位ID列表 + */ +List queryChosePostId(@Param("userId") String userId,@Param("id") String id); + +/** + * 查询用户学习过的岗位信息 + * + * @param id 推广活动的唯一标识符 + * @param channelIniId 渠道初始化ID + * @return 用户学习过的岗位信息列表 + */ +List queryTheUserSLearnedPositionInformation(@Param("id") String id, + @Param("channelIniId") String channelIniId); + +/** + * 查询学习过的岗位ID + * + * @param postId 岗位的唯一标识符 + * @return 包含学习过的岗位ID的Map列表 + */ +List> doQueryTheLearnedPositionIdFirst(@Param("postId") String postId); + +/** + * 查询用户学习进度 + * + * @param userId 用户的唯一标识符 + * @param studyPostIds 学习过的岗位ID列表 + * @param state 学习状态列表 + * @return 学习进度数量 + */ +Integer doQueryTheLearnedPositionIdSecond(@Param("userId") String userId, @Param("studyPostIds") List studyPostIds, + @Param("state") List state); + +/** + * 查询用户学习进度(新方法) + * + * @param userIds 用户的唯一标识符列表 + * @param studyPostIds 学习过的岗位ID列表 + * @param state 学习状态列表 + * @return 学习进度信息列表 + */ +List doQueryTheLearnedPositionIdSecondNew(@Param("userIds") List userIds , @Param("studyPostIds") List studyPostIds, + @Param("state") List state); + +/** + * 默认方法:查询用户已学习的岗位 + * + * @param userId 用户的唯一标识符 + * @param state 学习状态列表 + * @param postId 岗位的唯一标识符 + * @return 用户已学习的岗位ID字符串,以逗号分隔 + */ +default String queryThePositionsThatUsersHaveLearned(String userId, List state,String postId) { + List> maps = doQueryTheLearnedPositionIdFirst(postId); + List result = new ArrayList<>(); + for (Map map : maps) { + List courseIds = List.of(map.get("courseIds").split(",")); + if (doQueryTheLearnedPositionIdSecond(userId, courseIds,state) == courseIds.size()) { + result.add(map.get("postId")); + } + } + return String.join(",", result); +} + +/** + * 默认方法:查询用户已学习的岗位 + * + * @param userId 用户的唯一标识符 + * @param list 学习状态列表 + * @return 用户已学习的岗位ID字符串,以逗号分隔 + */ +default String queryThePositionsThatUsersHaveLearned(String userId,List list) { + List> maps = doQueryTheLearnedPositionIdFirst(null); + List result = new ArrayList<>(); + for (Map map : maps) { + List courseIds = List.of(map.get("courseIds").split(",")); + if (doQueryTheLearnedPositionIdSecond(userId, courseIds,list) > 0) { + result.add(map.get("postId")); + } + } + return String.join(",", result); +} + +/** + * 查询推广活动的岗位ID列表 + * + * @param promotionId 推广活动的唯一标识符 + * @return 岗位ID列表 + */ +List queryMapPostId(@Param("promotionId") String promotionId); + +/** + * 检查岗位是否有课程 + * + * @param postId 岗位的唯一标识符 + * @return 是否有课程的标志(0:否,1:是) + */ +Integer checkIfThePositionHasCourses(@Param("postId")String postId); + +/** + * 检查岗位是否有实践考核(新方法) + * + * @param postId 岗位的唯一标识符 + * @param userId 用户的唯一标识符 + * @return 是否有实践考核的标志列表 + */ +List checkWhetherThereIsAPracticalAppraisalWithNew(@Param("postId") String postId,@Param("userId") String userId); + +/** + * 检查岗位是否有考试(新方法) + * + * @param positionId 岗位的唯一标识符 + * @param userId 用户的唯一标识符 + * @return 是否有考试的标志列表 + */ +List checkIfThereAreExamsForThisPositionWithNew(@Param("postId") String positionId,@Param("userId") String userId); + + /** + * 查询学习地图的岗位配置 + * @param pomotionId 学习地图id + * @param currentLearningStage 阶段 + * @return + */ + List selectPromoteList(@Param("pomotionId") String pomotionId, @Param("currentLearningStage") Integer currentLearningStage); + + +/** + * 查询推广岗位列表 + * + * @param pomotionId 学习地图id + * @return 学习地图下的岗位 + */ +List selectAllPromoteList(@Param("pomotionId") String pomotionId); + +List countByPositionIds(@Param("positionIds") List positionIds); + +Page webList(@Param("page") Page page, @Param("params") FtbCultivatePromotionReq params); + + List queryMaxLevel(@Param("promotionIds") List promotionIds); + + Page personStatisticsV2(@Param("params") V2PromotionPersonStatisticReq params, + @Param("userIds") List userIds, + @Param("page") Page page); + + List queryPromotionPosition(@Param("userIds") List userIds); + + Page mapStatisticPageV2(@Param("page") Page page, + @Param("params") V2PromotionOrgStatisticReq params, + @Param("userIds") List userIds); + + List userCountForPromotion(@Param("userIds") List userIds); + + List queryPositionNumForMap(@Param("promotionIds") List promotionIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMessageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMessageMapper.java new file mode 100644 index 0000000..6496f53 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionNewMessageMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNewMessage; + +/** + * @Title:CultivateCourseMsgMapper + * @Author:peng.hao + * @create: 2023/12/2913:38 + */ +public interface FtbCultivatePromotionNewMessageMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostApplyMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostApplyMapper.java new file mode 100644 index 0000000..9802fdf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostApplyMapper.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyDto; +import jnpf.model.cultivate.po.apply.FtbCultivatePromotionPostApply; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyVO; +import org.apache.ibatis.annotations.Param; + +public interface FtbCultivatePromotionPostApplyMapper extends BaseMapper { + + /** + * 获取培养推广岗位申请的分页信息 + * 本方法用于处理分页获取培养推广岗位申请的信息请求 + * + * @param page 分页信息对象,用于控制和返回分页数据 + * @param dto 查询参数对象,用于筛选数据 + * @return 返回分页查询结果,包含培养推广岗位申请的VO列表 + */ + Page getListInfo(Page page, + @Param("dto") FtbCultivatePromotionPostApplyDto dto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostMapper.java new file mode 100644 index 0000000..b189140 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostMapper.java @@ -0,0 +1,40 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPost; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionPostVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePromotionPostMapper extends BaseMapper { + /** + * 查询同一岗位的其他任职信息 + * + * 本方法用于查询与当前任职信息相同岗位但不同的任职信息,以便于比较和分析 + * 同一岗位的不同任职路径和条件 + * + * @param postId 当前任职信息的岗位ID + * @param promotionVOId 当前任职信息的ID,用于排除自身 + * @return 返回一个列表,包含所有与当前岗位相同但ID不同的任职信息 + */ + List queryPromonChanOfTheSamePosition( + @Param("postId") String postId, + @Param("promotionVOId") String promotionVOId); + + + /** + * 查询个人下一次晋职信息 + * + * 通过个人ID和当前岗位ID,查询个人下一次可能的晋职信息 + * 这包括了下一次晋职所需的条件、路径等详细信息 + * + * @param postId 当前任职信息的岗位ID + * @param userId 个人用户ID + * @return 返回个人下一次晋职信息的详细对象,包括所有相关条件和路径 + */ + FtbCultivatePromotionPostVO findOutAboutTheNextPromotion(@Param("postId") String postId, + @Param("userId") String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostNewMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostNewMapper.java new file mode 100644 index 0000000..f8883d3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionPostNewMapper.java @@ -0,0 +1,28 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePromotionPostNewMapper extends BaseMapper { + /** + * 检查配置状态 + * + * @param postId 帖子ID,用于识别特定的配置 + * @return 返回一个字符串列表,列表中的每个字符串代表某个配置的状态 + */ + List isConfigured(@Param("postId") String postId); + + /** + * 计算学习进度 + * + * @param userId 用户ID,用于识别特定用户的学习进度 + * @param couseIds 课程ID列表,用于识别特定课程 + * @param states 课程状态列表,每个状态对应一个课程的学习状态 + * @return 返回一个整数,代表用户的学习进度百分比 + */ + Integer learningProgress(@Param("userId") String userId, @Param("couseIds") List couseIds, @Param("states") List states); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionSettingMapper.java new file mode 100644 index 0000000..77fc577 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionSettingMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionSetting; + +/** + * @Title: 学习地图人员和岗位配置表 + * @Author: + * @create: 2025-01-14 + */ +public interface FtbCultivatePromotionSettingMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionUserMapper.java new file mode 100644 index 0000000..e5f71a5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivatePromotionUserMapper.java @@ -0,0 +1,43 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionUser; +import jnpf.model.cultivate.v2.promotion.vo.MyCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionMemberVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivatePromotionUserMapper extends BaseMapper { + + + /** + * 分页查询用户信息 + * + * @param page 分页对象,包含分页参数和结果 + * @param queryUserIds 需要查询的用户ID列表 + * @param promotionId 推广活动ID + * @return V2CultivatePromotionMemberVo类型的分页结果 + */ + Page queryUserForPage(@Param("page") Page page, + @Param("queryUserIds")List queryUserIds, + @Param("promotionId") String promotionId); + + /** + * 查询用户的全部推广活动列表 + * + * @param userId 用户ID + * @return MyCultivatePromotionListVo类型的推广活动列表 + */ + List queryMyAllPromotionList(@Param("userId") String userId); + + /** + * 查询用户的指定推广活动 + * + * @param userId 用户ID + * @param promotionId 推广活动ID + * @return MyCultivatePromotionListVo类型的推广活动信息 + */ + MyCultivatePromotionListVo queryMyPromotionById(@Param("userId") String userId, @Param("promotionId") String promotionId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionAnalysisMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionAnalysisMapper.java new file mode 100644 index 0000000..22b87d7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionAnalysisMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionAnalysis; + +public interface FtbCultivateQuestionAnalysisMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankCourseMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankCourseMapper.java new file mode 100644 index 0000000..04515d1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankCourseMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBankCourse; + +public interface FtbCultivateQuestionBankCourseMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankMapper.java new file mode 100644 index 0000000..668e185 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionBankMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; + +public interface FtbCultivateQuestionBankMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionMapper.java new file mode 100644 index 0000000..b741bc5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionMapper.java @@ -0,0 +1,80 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.resp.PaperDrawRuleVo; +import jnpf.model.cultivate.resp.QuestionCountDto; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.exam.vo.BankPickVo; +import jnpf.model.cultivate.v2.exam.vo.QuestionTypeAnalysisVo; +import jnpf.model.cultivate.v2.exam.vo.QuestionVo; +import jnpf.model.cultivate.v2.exam.vo.V2AppQuestionVo; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; +/** + * 题目表mapper + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbCultivateQuestionMapper extends SuperMapper { + /** + * 根据题库id查询题库中重复题目 + * + * @param questionBankId 题库ID + * @return + */ + List queryRepeatQuestion(@Param("questionBankId") String questionBankId); + /** + * 根据题库id统计各题库的题目数量 + * @param ids 题库id集合 + * @return + */ + List countForClassifyId(@Param("ids") List ids); + + /** + * 在题库中根据规则随机抽题 + * @param ruleList 规则列表 + * @param drawDataBase 题库 + * @return java.util.List + */ + List getRandQuestionByRule(@Param("ruleList") List ruleList, @Param("drawDataBase") String drawDataBase); + + /** + * 根据题目ids查询题目 + * @param questionIds 题目ids + * @return java.util.List + */ + List selectListById(@Param("questionIds") List questionIds); + + /** + * 题型统计 + * @param bankIdList 题库ids + * @return java.util.List + */ + List getTypeAnalysis(@Param("list") List bankIdList); + + /** + * 查询题目列表 + * @param bankIds 题库ids + * @param keyword 关键字 + * @return java.util.List + */ + List getQuestionList(@Param("list") List bankIds, @Param("keyword") String keyword); + + /** + * 查询题库题型数量 + * @param bankPickList 题库抽题列表 + * @return java.util.List + */ + List getBankQuestionTypeCount(@Param("bankPickList") List bankPickList); + + /** + * 查询试卷抽题规则 + * @param paperIds 试卷ids + * @return java.util.List + */ + List getPaperDrawRuleList(@Param("paperIds") List paperIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionOptionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionOptionMapper.java new file mode 100644 index 0000000..bdb8f9c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionOptionMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; + +public interface FtbCultivateQuestionOptionMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionPointsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionPointsMapper.java new file mode 100644 index 0000000..5751029 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateQuestionPointsMapper.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionPoints; + +public interface FtbCultivateQuestionPointsMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateRuleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateRuleMapper.java new file mode 100644 index 0000000..2717770 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateRuleMapper.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.FtbCultivateRule; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:15:19 +* +*/ +public interface FtbCultivateRuleMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateStatisticsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateStatisticsMapper.java new file mode 100644 index 0000000..537387a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateStatisticsMapper.java @@ -0,0 +1,71 @@ +package jnpf.cultivate.mapper; + +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.v2.statistics.V2CultivateLineNumDto; +import jnpf.model.cultivate.vo.statistics.FtbCultivateStatisticsVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbCultivateStatisticsMapper { + /** + * 统计培训学员数量 + */ + Integer numOfTrainees(@Param("params") FtbCultivateStatisticsDTO dto); + + /** + * 获取热门课程信息 + */ + List getPopularCourses(@Param("dto") FtbCultivateStatisticsDTO dto); + + /** + * 统计学习中的课程数量 + * + * @param dto 统计参数封装对象 + * @return 学习中的课程数量 + */ + Integer numberOfStudyCourses(@Param("params") FtbCultivateStatisticsDTO dto); + + /** + * 计算学员累计学习时长 + * @param dto 统计参数封装对象 + * @return 累计学习时长 + */ + Integer accumulatedTime(@Param("params") FtbCultivateStatisticsDTO dto); + + /** + * 计算课程完成率 + * @param dto 统计参数封装对象 + * @param states 课程的状态列表,用于计算完成率 + * @return 课程完成率 + */ + Integer courseCompletionRate(@Param("params") FtbCultivateStatisticsDTO dto, @Param("states") List states); + + /** + * 获取学习进度排名靠前的成员列表 + * @param dto 统计参数封装对象 + * @return 学习进度排名靠前的成员列表 + */ + List getTopMembers(@Param("params") FtbCultivateStatisticsDTO dto); + + /** + * 统计新学习的课程数量 + * @param dto 统计参数封装对象 + * @return 新学习的课程数量 + */ + Integer numberOfStudyCoursesNew(@Param("params") FtbCultivateStatisticsDTO dto); + + V2CultivateLineNumDto queryStudyUserNumAndTime(@Param("params") FtbCultivateStatisticsDTO params); + V2CultivateLineNumDto queryStudyUserNumAndTimeV2(@Param("params") FtbCultivateStatisticsDTO params); + + Integer numOfTraineesV2(@Param("params") FtbCultivateStatisticsDTO dto); + + Integer numberOfStudyCoursesV2(@Param("params") FtbCultivateStatisticsDTO dto); + Integer accumulatedTimeV2(@Param("params") FtbCultivateStatisticsDTO dto); + + Integer courseCompletionRateV2(@Param("params") FtbCultivateStatisticsDTO dto, @Param("states") List states); + + List getTopMembersV2(@Param("params") FtbCultivateStatisticsDTO dto); + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTaskLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTaskLogMapper.java new file mode 100644 index 0000000..1368388 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTaskLogMapper.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; + +/** + * @Title: 培训任务完成记录表 + * @Author: + * @create: 2025-01-14 + */ +public interface FtbCultivateTaskLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperMapper.java new file mode 100644 index 0000000..07ead6a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperMapper.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; + +public interface FtbCultivateTestPaperMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperQuestionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperQuestionMapper.java new file mode 100644 index 0000000..18809c3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperQuestionMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; + +public interface FtbCultivateTestPaperQuestionMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperRuleMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperRuleMapper.java new file mode 100644 index 0000000..ad9fa75 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/FtbCultivateTestPaperRuleMapper.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperRule; + +public interface FtbCultivateTestPaperRuleMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingApproveMapper.java new file mode 100644 index 0000000..ff2161a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingApproveMapper.java @@ -0,0 +1,18 @@ +package jnpf.cultivate.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.cultivate.po.teaching.TeachingApprove; + +/** + * 带教审批mapper + * + * @author yanwenfu + * @create 2026-03-04 + */ +public interface TeachingApproveMapper extends SuperMapper { + + /** + * 旧数据维护: 生成以前带教的审批数据 + */ + void generateOldApproveData(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingRecordMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingRecordMapper.java new file mode 100644 index 0000000..7f0a0bb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingRecordMapper.java @@ -0,0 +1,143 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.DataScope; +import com.fantaibao.permission.enums.FilterWhereTypeEnum; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.model.cultivate.dto.teaching.*; +import jnpf.model.cultivate.v2.teaching.vo.StoreTeachingVo; +import jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo; +import jnpf.model.cultivate.v2.teaching.vo.V2TeachingRecordVo; +import jnpf.model.cultivate.vo.teaching.*; +import org.apache.ibatis.annotations.Param; + +import java.util.LinkedList; +import java.util.List; + +public interface TeachingRecordMapper extends BaseMapper { + + /** + * 上级带教-查看数据(技能点分组统计) + * + * @return + */ + List skillCount(@Param("recordIds") List recordIds); + + /** + * 带教汇总查询 + */ + List teachingSummaryQuery(@Param("queryTeachingRecordDto") QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 查询技能点 + * @param queryTeachingRecordDto + * @return List + */ + List queryCountSkillList(@Param("queryTeachingRecordDto") QueryTeachingRecordDto queryTeachingRecordDto); + /** + * 汇总-分页列表 + * + * @param page 分页参数 + * @param pageDto 查询参数 + * @return 汇总数据集合 + */ + @DataScope(tableAlias = "fctr", type = FilterWhereTypeEnum.STORE_FILTER, tableFieldOrg = "F_StoreId") + List summaryPageList(@Param("page") Page page, @Param("pageDto") TeachingBaseFilter pageDto); + + /** + * 汇总-技能点集合 + * + * @param pageDto 查询参数 + * @return 汇总数据集合 + */ + LinkedList summarySkillList(@Param("pageDto") TeachingBaseFilter pageDto); + + /** + * 记录-分页列表 + * + * @param page 分页参数 + * @param pageDto 查询参数 + * @param skillIds 技能点ids过滤 + * @return 练习集合 + */ + @DataScope(tableAlias = "fctr", type = FilterWhereTypeEnum.USER_AND_STORE_FILTER, tableFieldOrg = "F_StoreId") + List recordPageList(@Param("page") Page page, @Param("pageDto") TeachingBaseFilter pageDto, @Param("skillIds") List skillIds); + + /** + * App我的练习-分页列表 + * + * @param page 分页参数 + * @param pageDto 查询参数 + * @return 练习集合 + */ + List myRecordPageList(@Param("page") Page page, @Param("pageDto") MyRecordPageListDto pageDto, @Param("userId") String userId); + + /** + * 获取我的技能表 + * + * @param viewDataDto 技能表查询参数,包含日期范围和分页信息 + * @return 技能表集合 + */ + List getMySkillTable(@Param("pageDto") ViewDataDto viewDataDto, @Param("userId") String userId); + + /** + * App员工练习-分页列表 + * + * @param page 分页参数 + * @param pageDto 查询参数 + * @return 练习集合 + */ + @DataScope(tableAlias = "fctr", type = FilterWhereTypeEnum.USER_AND_STORE_FILTER, tableFieldOrg = "F_StoreId") + List employeePageList(@Param("page") Page page, @Param("pageDto") EmployeePageListDto pageDto); + + /** + * App员工练习-查看数据 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + @DataScope(tableAlias = "fctr", type = FilterWhereTypeEnum.STORE_FILTER, tableFieldOrg = "F_StoreId") + List employeeViewData(@Param("pageDto") ViewDataDto pageDto); + + /** + * App员工练习-技能点集合 + * + * @param pageDto 查询参数 + * @return 汇总数据集合 + */ + @DataScope(tableAlias = "fctr", tableField = "F_UserId", type = FilterWhereTypeEnum.USER_FILTER) + LinkedList employeeSkillList(@Param("pageDto") ViewDataDto pageDto); + + /** + * 上级带教-查看数据 + * + * @param storeId 门店ID + * @param loginUserId 登录用户ID + * @return 带教数据 + */ + SuperiorTeachingSummaryVo getSuperiorTeachingSummary(@Param("storeId") String storeId, @Param("loginUserId") String loginUserId); + + /** + * 根据storeIds查询每个门店下的带教数量 + * @param queryDto 查询条件 + * @param recordIds 满足学员名称条件的带教记录 + * @return java.util.List + */ + List getStoreRecordCount(@Param("queryDto") RecordQueryDto queryDto, @Param("recordIds") List recordIds); + + /** + * 根据storeId查询门店下的带教记录 + * @param queryDto 查询条件 + * @param recordIds 满足学员名称条件的带教记录 + * @return java.util.List + */ + List getStoreRecordList(@Param("queryDto") RecordQueryDto queryDto, @Param("recordIds") List recordIds, @Param("userId") String userId); + + /** + * 查询带教详情 + * @param bizId 带教/练习id + * @return jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo + */ + TeachingDetailVo getTeachingDetailById(String bizId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingSkillMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingSkillMapper.java new file mode 100644 index 0000000..f6a12f5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingSkillMapper.java @@ -0,0 +1,41 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.model.cultivate.v2.exam.vo.ImportObjectVo; +import jnpf.model.cultivate.v2.exam.vo.SkillVo; +import jnpf.model.cultivate.vo.teaching.SkillInfoVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface TeachingSkillMapper extends BaseMapper { + + /** + * 查询重复的技能点index + * @param list 技能点导入数据 + * @return java.util.List + */ + List getDistinctCountBatch(@Param("list") List list); + + /** + * 查询技能点列表[下拉] + * @param skillId 技能点id + * @param categoryId 分类id(为null查询所有的技能点, 为-1查询没有分类的技能点) + * @return java.util.List + */ + List getDownSkillList(@Param("skillId") String skillId, @Param("categoryId") String categoryId); + + /** + * 查询所有技能点 + * @return java.util.List + */ + List getAllSkillList(); + + /** + * 查询有子级的分类 + * @param categoryIds 分类ids + * @return java.util.List + */ + List getCategoryHasChild(@Param("categoryIds") List categoryIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingStudentMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingStudentMapper.java new file mode 100644 index 0000000..3ef1221 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mapper/TeachingStudentMapper.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.entity.cultivate.TeachingStudent; +import jnpf.model.cultivate.dto.teaching.QueryTeachingRecordDto; +import jnpf.model.cultivate.vo.teaching.SkillKeyVo; +import jnpf.model.cultivate.vo.teaching.TeachingDataListVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface TeachingStudentMapper extends BaseMapper { + + /** + * 我的带教-学员信息统计 + * @param queryTeachingRecordDto 带教记录id集合 + * @return List + */ + List queryTeachingDataList(@Param("queryTeachingRecordDto")QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 查询技能点,用于分组统计 + * @param queryTeachingRecordDto + * @return List + */ + List querySkillKeyList(@Param("queryTeachingRecordDto")QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 根据用户名查询带教记录ids + * @param keyword 用户名 + * @return java.util.List + */ + List getRecordIdByKeyword(String keyword); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockController.java new file mode 100644 index 0000000..d4caa92 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockController.java @@ -0,0 +1,181 @@ +package jnpf.cultivate.mock; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.nacos.shaded.com.google.common.collect.ImmutableMap; +import jnpf.base.ActionResult; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskAssignmentMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateCertificateUserService; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.cultivate.service.FtbCultivateOfflineTrainService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.dto.certificate.FtbCertificateUserForm; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineTrainDTO; +import jnpf.model.cultivate.po.certificate.CertificateUserPagination; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.AuthUtil; +import jnpf.util.Constants; +import jnpf.util.NoDataSourceBind; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +/** + * 培训模拟数据 + * + * @author wangchunxiang + * @date 2025/09/02 + */ +@RestController +@Slf4j +@RequestMapping("/cultivate-mock") +public class CultivateMockController { + + + @Resource + FtbCultivateOfflineTrainService ftbCultivateOfflineTrainService; + + @Resource + private FtbCultivateCertificateUserService ftbCultivateCertificateUserService; + + @Autowired + UserApi userApi; + + @Autowired + FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + @Resource + CultivateIdentifyApplyService applyService; + + @Resource + FtbCultivateExamService examService; + + @Resource + V2UserApi v2UserApi; + + /** + * 线下培训mock + */ + private final static String offlineTrainingDataMock="{\n" + + " \"id\": null,\n" + + " \"name\": \"线下全员培训\",\n" + + " \"userId\": \"349057407209541\",\n" + + " \"userName\": \"管理员\",\n" + + " \"headUserId\": \"712307395245893253\",\n" + + " \"userIds\": [\n" + + " \"732526335339233477\"\n" + + " ],\n" + + " \"offlinePlace\": \"公司办公室\",\n" + + " \"trainStartTime\": 1756875600000,\n" + + " \"trainEndTime\": 1756980000000,\n" + + " \"timeList\": [\n" + + "\n" + + " ],\n" + + " \"trainContent\": \"线下全员培训\",\n" + + " \"courseIds\": [\n" + + "\n" + + " ],\n" + + " \"files\": [\n" + + "\n" + + " ],\n" + + " \"courseInfo\": [\n" + + "\n" + + " ],\n" + + " \"signWay\": 0\n" + + "}"; + /** + * 证书颁发mock + * + */ + private final static String certificateDataMock="{\"certificateId\":\"1945677502821244930\",\"userInfoFormList\":[{\"userId\":\"349057407209541\",\"userName\":\"管理员\",\"positionId\":\"\",\"organizeId\":\"\",\"reason\":\"经岗位学习考试合格颁发\"}]}"; + /** + * mock数据培训统一接口 + */ + @GetMapping("/mock") + @NoDataSourceBind + public void mock(@RequestParam String tenantId) { + log.info("开始模拟数据"); + String token = AuthUtil.loginTempUser("349057407209541", tenantId); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + ActionResult> userInfoBatch = v2UserApi.getAllUserInfoBatch(List.of(), tenantId); + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantId); + userInfoBatch.getData().stream().filter(a->!OrganizeCategoryEnums.TEAM.getCode().equals(a.getOrganizeCategory())) + .forEach(usersBoundData -> { + String userId = usersBoundData.getId(); + // 证书颁发 + FtbCertificateUserForm certificateUserForm = JSONObject.parseObject(certificateDataMock, FtbCertificateUserForm.class); + List userInfoFormList = certificateUserForm.getUserInfoFormList(); + FtbCertificateUserForm.UserInfoForm userInfoForm = userInfoFormList.get(0); + userInfoForm.setUserName(usersBoundData.getUserName()); + userInfoForm.setPositionId(usersBoundData.getPositionId()); + userInfoForm.setOrganizeId(usersBoundData.getOrganizeId()); + userInfoForm.setUserId(userId); + CertificateUserPagination certificateUserPagination = new CertificateUserPagination(); + certificateUserPagination.setCertificateId(certificateUserForm.getCertificateId()); + certificateUserPagination.setUserId(userId); + List list = ftbCultivateCertificateUserService.getList(certificateUserPagination); + List collect = list.stream().filter(vo -> vo.getStatus() == 0).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)){ + ftbCultivateCertificateUserService.create(certificateUserForm); + } + // 线下培训 + FtbCultivateOfflineTrainDTO offlineTrainDTO = JSONObject.parseObject(offlineTrainingDataMock, FtbCultivateOfflineTrainDTO.class); + offlineTrainDTO.setTrainStartTime(getNextDayTimestamp(14)); + offlineTrainDTO.setTrainEndTime(getNextDayTimestamp(16)); + offlineTrainDTO.setHeadUserId(userId); + offlineTrainDTO.setUserIds(List.of(userId)); + ftbCultivateOfflineTrainService.add(offlineTrainDTO); + // 培训任务 + FtbCultivateLearnTaskAssignment taskAssignment = new FtbCultivateLearnTaskAssignment(); + // 已有任务 + taskAssignment.setTaskId("1950031757292150786"); + taskAssignment.setUserId(userId); + taskAssignment.setStudyStats(0); + ftbCultivateLearnTaskAssignmentMapper.insert(taskAssignment); + // 实操鉴定 + CultivateMockUtils.mockPracticalAppraisal(userId, usersBoundData, applyService); + // 自定义考试 + CultivateMockUtils.mockCustomExamination(userId, usersBoundData, examService); + }); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + FeignHolder.clear(); + } + log.info("模拟数据结束"); + } + public static Date getNextDayTimestamp(int hour) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, 1); // 加一天 + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockUtils.java new file mode 100644 index 0000000..4c45eda --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/mock/CultivateMockUtils.java @@ -0,0 +1,56 @@ +package jnpf.cultivate.mock; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.model.cultivate.dto.identify.IdentifyApplySaveDto; +import jnpf.model.cultivate.req.exam.SaveExamReq; +import jnpf.permission.vo.v2.user.UserBoundVO; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@UtilityClass +public class CultivateMockUtils { + + String practicalAppraisalMockData = "{\"tableId\":\"712666643662366213\",\"beIdentifyUserId\":\"712295601244726981\",\"identifyUserId\":\"712295601399916229\",\"appraisalResults\":1,\"beIdentifyOrgId\":\"712278038133597893\",\"beIdentifyPostId\":\"665102345222415301\",\"beIdentifyPostRankId\":\"1895297695136899073\"}"; + + /** + * 实操鉴定模拟数据 + */ + public static void mockPracticalAppraisal(String userId, UserBoundVO usersBoundData, CultivateIdentifyApplyService applyService) { + IdentifyApplySaveDto req = JSON.parseObject(practicalAppraisalMockData, IdentifyApplySaveDto.class); + req.setSource(ApplySourceEnum.SDFQ.getCode()); + req.setBeIdentifyUserId(userId); + req.setBeIdentifyOrgId(usersBoundData.getOrganizeId()); + req.setBeIdentifyPostId(usersBoundData.getPositionId()); + req.setBeIdentifyPostRankId(usersBoundData.getGradeId()); + req.setAppraisalResults(0); + if (!req.getBeIdentifyUserId().equals(req.getIdentifyUserId())) { + try { + applyService.applyDataSave(req); + } catch (Exception e){ + log.error("实操鉴定模拟数据保存失败:{}", e.getMessage()); + } + } + } + + String customExaminationMockData = "{\"examName\":\"企业文化考试\",\"paperId\":\"1943126977902878721\",\"examType\":1,\"id\":null,\"postAndPositionList\":[],\"examlimitation\":0,\"examTime\":3600,\"passType\":1,\"passMark\":60,\"excellentType\":1,\"excellentMark\":80,\"description\":\"\",\"reviewer\":\"1\",\"reviewerAppoint\":\"712295601399916229\",\"reviewerRole\":\"\",\"examMemberId\":\"712307395245893253\",\"reviewerAppointList\":[],\"makeUpCount\":null,\"waterMark\":0,\"cutScreen\":0,\"maxCutScreenCount\":0,\"screenshot\":0,\"copyAndPaste\":0,\"openOverTime\":0,\"overTime\":null,\"allowSelectDetail\":0,\"allowChangeLastQuestion\":0,\"reExamFrequencyType\":null,\"reExamFrequencyNum\":null,\"examQuestionRandom\":0,\"reviewerAppointListConfig\":[{\"userId\":\"712295601399916229\",\"userName\":\"运营总监·演示\",\"orgName\":\"东大路店\",\"orgId\":\"712278038133597893\",\"positionId\":\"665101627140792261\",\"positionName\":\"运营总监\",\"rankId\":\"1895296977056915458\",\"rankName\":\"初级\"}]}"; + + /** + * 自定义考试 + */ + public static void mockCustomExamination(String userId, UserBoundVO usersBoundData, FtbCultivateExamService examService) { + SaveExamReq req = JSON.parseObject(customExaminationMockData, SaveExamReq.class); + req.setExamMemberId(userId); + req.setExamName("企业文化考试-" + usersBoundData.getUserName()+ IdWorker.getId()); + try { + examService.insertData(req); + } catch (Exception e){ + log.error("自定义考试模拟数据保存失败:{}", e.getMessage()); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgService.java new file mode 100644 index 0000000..e4f16c1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgService.java @@ -0,0 +1,51 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.course.app.FtbUserLevelMessageReadDTO; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsg; +import jnpf.model.cultivate.vo.course.app.FtbCheckJobGradeChangeVO; +import jnpf.model.cultivate.vo.course.app.FtbCultivateCourseMsgForAppVO; + +import java.util.List; + +/** + * @Title:CultivateCourseMsgService + * @Author:peng.hao + * @create: 2023/12/2913:35 + */ +public interface CultivateCourseMsgService extends IService { + + /** + * 查询用户未读的培养课程消息 + * + * @return 用户未读的培养课程消息列表 + */ + List queryUserUnreadMsg(); + + /** + * 检查岗位变化 + * + * @param type 变化类型 + * @return 职务等级变化列表 + */ + List checkJobGradeChanges(Integer type); + + /** + * 标记用户等级消息为已读 + * + * @param ftbUserLevelMessageReadDTO 用户等级消息已读的详细信息 + */ + void userLevelMessageHasBeenRead(FtbUserLevelMessageReadDTO ftbUserLevelMessageReadDTO); + + /** + * 查询用户未读的培养课程消息(根据课程ID进行更新) + * + * @param courseId 课程ID + * @return 用户未读的培养课程消息列表(已更新) + */ + List queryUserUnreadMsgUpdate(String courseId); + + List queryUserUnreadMsgV2( String courseId); + + void updateInfoByMsgIdV2(String courseId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgUserService.java new file mode 100644 index 0000000..82e03ee --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCourseMsgUserService.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.course.app.FtbCultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsgUser; + +/** + * @Title:CultivateCourseMsgService + * @Author:peng.hao + * @create: 2023/12/2913:35 + */ +public interface CultivateCourseMsgUserService extends IService { + + /** + * 根据消息ID更新培养课程信息 + * + * 此方法的主要目的是在接收到新的培养课程消息后,通过消息中包含的消息ID, + * 更新系统中已有的课程信息,确保系统中的课程数据是最新的 + * + * @param dto 包含需要更新的课程信息的DTO对象,该对象主要通过消息ID关联相应的课程数据 + */ + void updateInfoByMsgId(FtbCultivateCourseMsgForAppDTO dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverCategoryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverCategoryService.java new file mode 100644 index 0000000..445ee58 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverCategoryService.java @@ -0,0 +1,49 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.common.FtbCultivateCoverCategoryEntity; +import jnpf.model.cultivate.v2.common.vo.V2BaseIdNameVo; +import jnpf.model.cultivate.v2.exam.req.V2SaveCoverCategoryReq; +import jnpf.model.cultivate.v2.exam.vo.V2TreeCoverVo; + +import java.util.List; + +/** + * @Desc 封面分类服务 + * + * @Author shangyi + * @Date 2026/1/22 + */ +public interface CultivateCoverCategoryService extends IService { + + /** + * 添加/编辑封面分类 + * + * @param req + * @return + */ + String saveCategory(V2SaveCoverCategoryReq req); + + /** + * 删除封面分类 + * + * @param id + * @return + */ + void delCategory(String id); + + /** + * 获取封面分类列表 + * + * @return + */ + List getCategoryList(Integer isSystem); + + + /** + * 获取所有封面,两级结构 + * + * @return + */ + List treeCover(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverInfoService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverInfoService.java new file mode 100644 index 0000000..67c1bf3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateCoverInfoService.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.po.common.FtbCultivateCoverInfoEntity; +import jnpf.model.cultivate.v2.exam.req.V2QueryCoverReq; +import jnpf.model.cultivate.v2.exam.req.V2SaveCoverReq; +import jnpf.model.cultivate.v2.exam.vo.V2CoverVo; + +/** + * @Desc 封面服务 + * + * @Author shangyi + * @Date 2026/1/22 + */ +public interface CultivateCoverInfoService extends IService { + + /** + * 添加/修改封面 + * + * @param req + */ + void saveCover(V2SaveCoverReq req); + + /** + * 获取封面分页列表 + * + * @param req + * @return + */ + Page getPageList(V2QueryCoverReq req); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateExamSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateExamSettingService.java new file mode 100644 index 0000000..7e07cdc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateExamSettingService.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamSettingEntity; +import jnpf.model.cultivate.v2.exam.req.V2ExamSettingReq; + +import javax.validation.Valid; + +/** + * @Desc 考试配置服务 + * + * @Author shangyi + * @Date 2026/1/22 + */ +public interface CultivateExamSettingService extends IService { + + /** + * 保存考试配置 + * + * @param req + */ + void saveExamSetting(@Valid V2ExamSettingReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateFileService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateFileService.java new file mode 100644 index 0000000..d95ff83 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateFileService.java @@ -0,0 +1,31 @@ +package jnpf.cultivate.service; + +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.po.teaching.CultivateFile; +import jnpf.model.cultivate.v2.teaching.model.V2Attachment; + +import java.util.List; +import java.util.Map; + +/** + * 附件服务 + * + * @author yanwenfu + * @create 2026-03-02 + */ +public interface CultivateFileService extends SuperService { + + /** + * 查询业务关联的附件 + * @param bizIds 业务ids + * @return java.util.Map> + */ + Map> getFileListByBizIds(List bizIds); + + /** + * 对象转换 + * @param file 文件 + * @return jnpf.model.cultivate.v2.teaching.model.V2Attachment + */ + V2Attachment convertToV2Attachment(CultivateFile file); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApiService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApiService.java new file mode 100644 index 0000000..3b24584 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApiService.java @@ -0,0 +1,49 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.vo.identify.UserOrgInfoAllVo; +import jnpf.permission.model.user.UserInfoVO; + +import java.util.List; +import java.util.Map; + +/** + * 实操鉴定Api方法 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyApiService { + /** + * 查询用户直系领导 + * + * @param userId 用户ID + */ + UserInfoVO selectUserDirectLeadership(String userId); + + /** + * 选择“用户直接领导(新的) + * + * @param orgId 组织 ID + * @param userId 用户 ID + * @param positionId 职位 ID + * @param positionGradesId 职位等级 ID + * @return {@link UserInfoVO} + */ + UserInfoVO selectUserDirectLeadershipNew(String orgId, String userId, String positionId, String positionGradesId); + + /** + * 查询用户组织岗位列表 + * + * @param userIds 用户ID集合 + */ + Map> batchSelectUserOrgInfoByUserIds(List userIds); + + /** + * 根据职等批量查询组织信息 + * + * @param officialRankCodes 职等编码集合 + */ + Map> batchSelectOfficialRankInfo(List officialRankCodes); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsBackupsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsBackupsService.java new file mode 100644 index 0000000..5ea510a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsBackupsService.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetailsBackups; + +/** + * 实操鉴定项_关联_鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyApplyDetailsBackupsService extends IService { + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsService.java new file mode 100644 index 0000000..2e54094 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyDetailsService.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetails; + +/** + * 实操鉴定申请详情表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyApplyDetailsService extends IService { + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyService.java new file mode 100644 index 0000000..6a13f5f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyService.java @@ -0,0 +1,287 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.identify.*; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.vo.identify.*; + +import java.util.List; +import java.util.Map; + +/** + * 实操鉴定申请表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyApplyService extends IService { + /** + * 实操鉴定申请/列表 + * + * @param req 筛选条件 + * @return 分页数据,包含鉴定申请列表信息 + */ + PageInfo getPageApplyList(IdentifyApplyListDto req); + + /** + * 实操鉴定申请/详情(基本信息) + * + * @param req 筛选条件 + * @return 鉴定申请的基本信息 + */ + IdentifyApplyBasicInfoVo getApplyBasicInfo(IdentifyTableInfoDto req); + + /** + * 实操鉴定申请/详情(鉴定详情) + * + * @param req 筛选条件 + * @return 鉴定申请的详细信息 + */ + IdentifyApplyInfoVo getApplyInfo(IdentifyTableInfoDto req); + + /** + * 实操鉴定申请/新增 + * + * @param req 新增鉴定申请的数据传输对象 + * @throws RuntimeException 操作失败时抛出异常 + */ + void applyDataSave(IdentifyApplySaveDto req) throws RuntimeException; + + /** + * 实操鉴定申请/批量新增 + * + * @param req 批量新增鉴定申请的数据传输对象 + * @throws RuntimeException 操作失败时抛出异常 + */ + void applyDataSaveBatch(BatchIdentifyApplySaveDto req) throws RuntimeException; + + /** + * 实操鉴定申请/重新鉴定 + * + * @param req 重新鉴定的数据传输对象 + * @throws RuntimeException 操作失败时抛出异常 + */ + void applyDataReIdentify(IdentifyApplyReIdentifyDto req) throws RuntimeException; + + + /** + * 实操鉴定申请/cultivateMyLearnTaskInfoService检验 + * + * @param req 筛选条件 + * @return 鉴定申请列表信息 + */ + List applyDataDeleteCheck(IdentifyTableInfoDto req); + + /** + * 实操鉴定申请/删除 + * + * @param req 筛选条件 + * @throws RuntimeException 操作失败时抛出异常 + */ + void applyDataDelete(IdentifyTableInfoDto req) throws RuntimeException; + + /** + * app/列表 + * + * @param req 筛选条件 + * @return 分页数据,包含APP端鉴定申请列表信息 + */ + PageInfo getPageAppList(IdentifyApplyListAppDto req); + + /** + * app/列表数量统计 + * + * @param req 筛选条件 + * @return 列表总数 + */ + Integer getPageAppListCount(IdentifyApplyListAppDto req); + + /** + * app/清空-清空全部 + * + * @param req 删除条件 + * @throws RuntimeException 操作失败时抛出异常 + */ + void deleteIds(IdentifyApplyDeleteAppDto req) throws RuntimeException; + + /** + * app/鉴定详情 + * + * @param req 筛选条件 + * @return APP端鉴定详情信息 + */ + IdentifyInfoAppVo getIdentifyApplyInfoApp(IdentifyTableInfoDto req); + + /** + * app/鉴定项详情 + * + * @param req 筛选条件 + * @return APP端鉴定项详情信息 + */ + IdentifyItemsInfoAppVo getApplyIdentifyItemsInfoApp(IdentifyTableInfoDto req); + + /** + * app/鉴定提交 + * + * @param req 提交鉴定申请的数据传输对象 + * @throws RuntimeException 操作失败时抛出异常 + */ + void applyDataSubmit(IdentifyApplySubmitDto req) throws RuntimeException; + + /** + * app/设置鉴定时间 + * + * @param req 设置鉴定时间的数据传输对象 + * @throws RuntimeException 操作失败时抛出异常 + */ + void setApplyDataTime(IdentifyApplySetTimeAppDto req) throws RuntimeException; + + /** + * app/鉴定结果详情 + * + * @param req 筛选条件 + * @return APP端鉴定结果详情信息 + */ + IdentifyApplyBasicInfoAppVo getApplyBasicInfoApp(IdentifyTableInfoDto req); + + /** + * app/鉴定项结果详情 + * + * @param req 筛选条件 + * @return APP端鉴定项结果详情信息 + */ + IdentifyApplyInfoAppVo getApplyInfoApp(IdentifyTableInfoDto req); + + /** + * 岗位鉴定/课程鉴定推送/任务推送 + * + * @param req 推送数据传输对象 + * @return 返回鉴定记录ID + * @throws RuntimeException 操作失败时抛出异常 + */ + String applyDataPush(IdentifyApplyDataPushDto req) throws RuntimeException; + + /** + * 打开可见性 + * + * @param id 岗位ID + * @param userId 用户ID + */ + void turnOnVisibility(String id, String userId); + + + /** + * 任务鉴定考试合格后打开可见性 + * + * @param taskId 任务ID + * @param userId 用户ID + * @param status 考试状态:3-合格,4-不合格,5-优秀 + */ + void turnOnVisibilityWithTaskId(String taskId, String userId, Integer status); + + + /** + * 岗位晋升/批量查看用户鉴定结果 + * + * @param ids 鉴定申请记录ID集合 + * @return 用户鉴定结果列表 + */ + List getUserIdentifyInfoApi(List ids); + + /** + * 成员培训进展/用户鉴定数据统计 + * + * @param userIds 被鉴定用户ID集合 + * @return 用户鉴定统计数据映射 + */ + Map getUserIdentifyStatistics(List userIds); + + /** + * 成员培训进展/分页列表 + * + * @param userId 用户ID + * @param page 分页参数 + * @return 用户鉴定分页列表 + */ + PageListVO getUserIdentifyPage(String userId, CultivatePage page); + + /** + * 数据看板/统计 + * + * @param req 筛选条件 + * @return 鉴定统计数据 + */ + IdentifyStatisticsVo getIdentifyStatistics(FtbCultivateStatisticsDTO req); + + /** + * 数据看板/排名 + * + * @param req 筛选条件 + * @return 鉴定排名列表 + */ + List getIdentifyTop(FtbCultivateStatisticsDTO req); + + /** + * 处理鉴定逾期数据 + * + * @return 是否处理成功 + */ + boolean setIdentifyBeOverdue(); + + /** + * 设置了计划鉴定时间提醒 + * + * @param tenantId 租户ID + * @return 是否设置成功 + */ + boolean setIdentifyRemind(String tenantId); + + /** + * 查看用户鉴定结果 + * + * @param userId 用户ID + * @return 用户鉴定结果列表 + */ + List getUserIdentifyInfoWithUserId(String userId); + + /** + * 获取实操鉴定统计数据(新版) + * + * @param dto 统计筛选条件 + * @return 实操鉴定统计信息 + */ + IdentifyStatisticsForNewVo getHandsOnStatistics(FtbCultivateStatisticsDTO dto); + + /** + * 判断用户是否已经鉴定 + * + * @param userId 用户ID + * @param positionId 岗位ID + * @return null 无鉴定记录, false 未鉴定, true 已鉴定 + */ + Boolean whetherTheUserPositionHasBeenIdentified(String userId, String positionId); + + /** + * 删除已鉴定的数据 + * + * @param taskId 任务ID + */ + void deleteLearnTaskApplyIdentified(String taskId); + + /** + * 统计每个鉴定的鉴定人数 + * + * @param taskIds 任务ID集合 + * @param type 类型:0-查询所有,1-查询已鉴定合格 + * @return 鉴定人数统计映射 + */ + Map groupIdentifyCountNum(List taskIds, Integer type); + + List queryIdentifyApply(String userId, List identifyIds, Integer source, String sourceId); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyTableBackupsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyTableBackupsService.java new file mode 100644 index 0000000..c4d855b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyApplyTableBackupsService.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivateIdentifyApplyTableBackups; + +/** + * 实操鉴定申请_关联_鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyApplyTableBackupsService extends IService { + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyItemsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyItemsService.java new file mode 100644 index 0000000..2b1482d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyItemsService.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.vo.identify.IdentifyItemsWithCategoryVo; + +import java.util.List; + +/** + * 实操鉴定项表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyItemsService extends IService { + + List countForTableId(List tableIdList); + + /** + * 查询鉴定项列表并关联分类名称 + * + * @param tableId 查询条件 + * @return 鉴定项列表(含分类名称) + */ + List listWithCategory(String tableId); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyTableService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyTableService.java new file mode 100644 index 0000000..a324539 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/CultivateIdentifyTableService.java @@ -0,0 +1,86 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.model.cultivate.dto.identify.*; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableListReq; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableListVo; +import jnpf.model.cultivate.vo.identify.*; + +import java.util.List; + +/** + * 实操鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +public interface CultivateIdentifyTableService extends IService { + + /** + * 鉴定表/列表 + * + * @param req 帅选条件 + * @return + */ + PageInfo getPageList(IdentifyTableListDto req); + + /** + * 鉴定表/详情 + * + * @param req 帅选条件 + * @return + */ + IdentifyTableInfoVo getInfo(IdentifyTableInfoDto req) ; + + /** + * 鉴定表/新增 + * + * @param req 帅选条件 + */ + void saveData(IdentifyTableSaveDto req) ; + + /** + * 鉴定表/修改 + * + * @param req 帅选条件 + */ + void updateData(IdentifyTableUpdateDto req); + + /** + * 鉴定表/删除 + * + * @param req 帅选条件 + */ + IdentifyTableDeleteVo deleteData(IdentifyTableInfoDto req); + + /** + * 查询鉴定表集合信息 + * @param ids + * @return 返回鉴定记录id + */ + List getTableListInfo(List ids); + + /** + * 组织列表统计方法 + * 该方法用于对组织的相关数据进行统计,具体统计内容和规则由参数statisticDTO指定 + * + * @param statisticDTO 统计DTO对象,包含统计所需的参数和配置 + * @return 返回一个PageListVO对象,包含按照指定条件统计的组织数据 + */ + PageListVO organizationListStatistics(FtbIdentityOrgWisdomStatisticDTO statisticDTO); + + /** + * 个人列表统计方法 + * 该方法用于对个人的相关数据进行统计,具体统计内容和规则由参数personWisdomStatisticDTO指定 + * + * @param personWisdomStatisticDTO 统计DTO对象,包含统计所需的参数和配置 + * @return 返回一个PageListVO对象,包含按照指定条件统计的个人数据 + */ + PageListVO personListStatistics(FtbIdentityPersonWisdomStatisticDTO personWisdomStatisticDTO); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/ExamFrequencyLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/ExamFrequencyLogService.java new file mode 100644 index 0000000..78b464f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/ExamFrequencyLogService.java @@ -0,0 +1,15 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamFrequencyLog; + +/** + * 考试次数记录服务 + * + * @author yanwenfu + * @create 2026-03-23 + */ +public interface ExamFrequencyLogService extends IService { + + Integer queryExamNumByDateStr(String examId, String userId, String startDateStr, String endDateStr); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedCommentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedCommentService.java new file mode 100644 index 0000000..a2fa384 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedCommentService.java @@ -0,0 +1,51 @@ +package jnpf.cultivate.service; + +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.dto.gained.FtbCommentPagination; +import jnpf.model.cultivate.po.gained.FtbCourseGainedCommentEntity; +import jnpf.model.cultivate.vo.gained.FtbGroupCommentVO; + +import java.util.List; + +/** + * + * 心得评论 服务类 + * + * @author yanglei + * @since 2023-07-19 + */ +public interface FtbCourseGainedCommentService extends SuperService { + /** + * 列表查询 + * @param pagination 列表参数 + * @return 分页结果 + */ + List getList(FtbCommentPagination pagination); + + /** + * 详情 + * @param id id + * @return 结果 + */ + FtbCourseGainedCommentEntity getInfo(String id); + + + /** + * 新增 + * @param entity 实体 + */ + void create(FtbCourseGainedCommentEntity entity); + + /** + * 更新 + * @param id 主键 + * @param entity 实体 + */ + void update(String id, FtbCourseGainedCommentEntity entity); + + /** + * 删除 + * @param entity 实体 + */ + void delete(FtbCourseGainedCommentEntity entity); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedLikeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedLikeService.java new file mode 100644 index 0000000..99a9a1a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedLikeService.java @@ -0,0 +1,62 @@ +package jnpf.cultivate.service; + +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.dto.gained.FtbLikePagination; +import jnpf.model.cultivate.po.gained.FtbCourseGainedLikeEntity; + +import java.util.List; + +/** + * + * 心得点赞 服务类 + * + */ +public interface FtbCourseGainedLikeService extends SuperService { + /** + * 列表查询 + * @param pagination 列表参数 + * @return 分页结果 + */ + List getList(FtbLikePagination pagination); + + /** + * 详情 + * @param id id + * @return 结果 + */ + FtbCourseGainedLikeEntity getInfo(String id); + + /** + * 新增 + * @param entity 实体 + */ + void create(FtbCourseGainedLikeEntity entity); + + /** + * 更新 + * @param id 主键 + * @param entity 实体 + */ + void update(String id, FtbCourseGainedLikeEntity entity); + + /** + * 删除 + * @param entity 实体 + */ + void delete(FtbCourseGainedLikeEntity entity); + /** + * 根据获得点赞的实体对象获取当前用户点赞信息 + * + * @param gainedLikeEntity 获得点赞的实体对象,包含获得点赞的相关信息 + * @return 返回当前用户点赞信息,如果找不到则返回null + */ + Integer getMyLike(FtbCourseGainedLikeEntity gainedLikeEntity); + + /** + * 根据获得点赞的ID和点赞用户的ID删除点赞记录 + * + * @param gainedId 获得点赞的ID + * @param likeUserId 点赞用户的ID + */ + void deleteLikeByGainedId(String gainedId, String likeUserId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedReaderService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedReaderService.java new file mode 100644 index 0000000..2a62fbc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedReaderService.java @@ -0,0 +1,48 @@ +package jnpf.cultivate.service; + +import jnpf.base.Pagination; +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedReaderEntity; + +import java.util.List; + + +/** + * + * 心得可查看用户关联表 服务类 + * + */ +public interface FtbCourseGainedReaderService extends SuperService { + /** + * 列表查询 + * @param pagination 列表参数 + * @return 分页结果 + */ + List getList(Pagination pagination); + + /** + * 详情 + * @param id id + * @return 结果 + */ + FtbCourseGainedReaderEntity getInfo(String id); + + /** + * 新增 + * @param entity 实体 + */ + void create(FtbCourseGainedReaderEntity entity); + + /** + * 更新 + * @param id 主键 + * @param entity 实体 + */ + void update(String id, FtbCourseGainedReaderEntity entity); + + /** + * 删除 + * @param entity 实体 + */ + void delete(FtbCourseGainedReaderEntity entity); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedService.java new file mode 100644 index 0000000..245bd8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedService.java @@ -0,0 +1,78 @@ +package jnpf.cultivate.service; + +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.dto.gained.FtbGainedPagination; +import jnpf.model.cultivate.dto.gained.FtbGainedQueryDto; +import jnpf.model.cultivate.po.gained.FtbCourseGainedEntity; +import jnpf.model.cultivate.vo.gained.FtbChapterEndGainedVO; +import jnpf.model.cultivate.vo.gained.FtbGroupGainedVO; + +import java.util.List; + +/** + * + * 心得体会 服务类 + * + */ +public interface FtbCourseGainedService extends SuperService { + /** + * 列表查询 + * @param pagination 列表参数 + * @return 分页结果 + */ + List getList(FtbGainedPagination pagination); + + /** + * 获取章节结束心得列表 + * @param chapterId + * @return + */ + @Deprecated + List getGainedListOfChapterEnd(String chapterId); + + /** + * 详情 + * @param id id + * @return 结果 + */ + FtbCourseGainedEntity getInfo(String id); + + /** + * 新增 + * @param entity 实体 + */ + void create(FtbCourseGainedEntity entity); + + /** + * 更新 + * @param id 主键 + * @param entity 实体 + */ + void update(String id, FtbCourseGainedEntity entity); + + /** + * 删除 + * @param entity 实体 + */ + void delete(FtbCourseGainedEntity entity); + + /** + * 列表查询 + * @param queryDto 列表参数 + * @return 分页结果 + */ + List list(FtbGainedQueryDto queryDto); + + /** + * 根据一组给定的ID获取奖励详情 + * + * @param gainedIds 需要查询的奖励ID列表这些ID唯一标识了系统中的奖励记录 + * @return 返回一个FtbGroupGainedVO对象的列表,包含了每个ID对应的奖励详细信息 + * + * 该方法主要用于根据奖励ID获取详细的奖励信息,避免了多次单独查询的效率问题, + * 通过一次性查询多个ID,提高了数据检索的效率 + */ + List getGainedDetailByIds(List gainedIds); + +// void sharingSquare(FtbCourseGainedEntity gainedEntity); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedShareService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedShareService.java new file mode 100644 index 0000000..ef46372 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCourseGainedShareService.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.service; + +import jnpf.base.Pagination; +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedShareEntity; + +import java.util.List; + +/** + * + * 心得分享 服务类 + * + */ +public interface FtbCourseGainedShareService extends SuperService { + /** + * 列表查询 + * @param pagination 列表参数 + * @return 分页结果 + */ + List getList(Pagination pagination); + + /** + * 详情 + * @param id id + * @return 结果 + */ + FtbCourseGainedShareEntity getInfo(String id); + + /** + * 新增 + * @param entity 实体 + */ + void create(FtbCourseGainedShareEntity entity); + + /** + * 更新 + * @param id 主键 + * @param entity 实体 + */ + void update(String id, FtbCourseGainedShareEntity entity); + + /** + * 删除 + * @param entity 实体 + */ + void delete(FtbCourseGainedShareEntity entity); + + /** + * 心得分享到广场 + * @param id + * @return + */ + +// ActionResult sharingSquare(String id); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateAssessmentPointsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateAssessmentPointsService.java new file mode 100644 index 0000000..d351680 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateAssessmentPointsService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.FtbCultivateAssessmentPoints; + +public interface FtbCultivateAssessmentPointsService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCaseBaseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCaseBaseService.java new file mode 100644 index 0000000..c283cd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCaseBaseService.java @@ -0,0 +1,71 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseAuditDTO; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseDTO; +import jnpf.model.cultivate.dto.casebase.app.FtbCultivateCaseBaseCreatDTO; +import jnpf.model.cultivate.po.casebase.FtbCultivateCaseBase; +import jnpf.model.cultivate.vo.casebase.FtbCultivateCaseBaseVO; + +/** +* +*@Title: +*@Author: peng.hao +*@create: 2024/7/22:11:21 +* +*/ +public interface FtbCultivateCaseBaseService extends IService { + + /** + * 分页查询培养案例列表 + * 该方法用于展示培养案例的基础信息列表,通常用于Web界面的分页显示 + * + * @param dto 查询参数对象,包含查询培养案例所需的条件和分页信息 + * @return 返回一个PageListVO对象,包含分页后的培养案例列表和总记录数 + */ + PageListVO listDisplay(FtbCultivateCaseBaseDTO dto); + + /** + * 审核培养案例 + * 该方法用于对培养案例进行审核操作,包括通过或驳回案例 + * + * @param baseAuditDTO 审核参数对象,包含审核结果、审核意见等信息 + */ + void reviewCase(FtbCultivateCaseBaseAuditDTO baseAuditDTO); + + /** + * 获取培养案例详情 + * 该方法用于获取单个培养案例的详细信息,通常用于查看或编辑案例时 + * + * @param id 培养案例的唯一标识符 + * @return 返回一个FtbCultivateCaseBaseVO对象,包含培养案例的详细信息 + */ + FtbCultivateCaseBaseVO getDetail(String id); + + /** + * 创建培养案例基础信息 + * 该方法用于录入新的培养案例基础信息,包括案例的基本属性和元数据 + * + * @param caseBaseCreatDTO 创建参数对象,包含新培养案例的基础信息 + */ + void caseBaseCreatDTO(FtbCultivateCaseBaseCreatDTO caseBaseCreatDTO); + + /** + * 更新培养案例库 + * 该方法用于更新或修改培养案例库中的案例信息,可能涉及案例内容的变更或更新 + * + * @param caseBaseCreatDTO 更新参数对象,包含需要更新的培养案例信息 + */ + void updateCaseLibrary(FtbCultivateCaseBaseCreatDTO caseBaseCreatDTO); + + /** + * 分页查询培养案例列表(用于App端) + * 该方法与listDisplay方法类似,但专门用于App端展示培养案例列表 + * 可能会包含或强调一些适合移动设备显示的字段或格式 + * + * @param dto 查询参数对象,包含查询培养案例所需的条件和分页信息 + * @return 返回一个PageListVO对象,包含分页后的培养案例列表和总记录数,格式适合App端显示 + */ + PageListVO listDisplayForApp(FtbCultivateCaseBaseDTO dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateImagesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateImagesService.java new file mode 100644 index 0000000..603979d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateImagesService.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.certificate.FtbCertificateImagesEntity; +import jnpf.model.cultivate.v2.certificate.req.V2SaveCertificateReq; + +import java.util.List; + +/** + * 证书模版图片 + * + * @author xuguilin + */ +public interface FtbCultivateCertificateImagesService extends IService { + + /** + * 查询所有证书模板图片列表 + * + * @return 证书模板图片实体列表 + */ + List listAll(); + + + + /** + * 添加证书模板 + * + * @param req 保存证书请求参数 + * @return 是否添加成功 + */ + boolean add(V2SaveCertificateReq req); + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateService.java new file mode 100644 index 0000000..fa03e96 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateService.java @@ -0,0 +1,90 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.Pagination; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.certificate.FtbCertificateOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.certificate.FtbCertificatePersonWisdomStatisticDTO; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.v2.common.vo.V2BaseIdNameVo; +import jnpf.model.cultivate.vo.certificate.FtbCertificateListVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificateOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.certificate.FtbCertificatePersonWisdomStatisticVO; + +import java.util.List; + +/** + * 证书模版表 + * @author penghao + */ +public interface FtbCultivateCertificateService extends IService{ + + /** + * 获取证书列表 + * @param pagination 分页信息,用于控制返回列表的页数和大小 + * @return 返回一个证书实体列表 + */ + List getList(Pagination pagination); + + /** + * 根据ID获取证书信息 + * @param id 证书的唯一标识符 + * @return 返回一个包含指定ID的证书信息的实体 + */ + FtbCertificateEntity getInfo(String id); + + /** + * 创建新的证书 + * @param entity 要创建的证书的实体对象 + */ + void create(FtbCertificateEntity entity); + + /** + * 更新证书信息 + * @param id 要更新的证书的唯一标识符 + * @param entity 包含更新后证书信息的实体对象 + * @return 返回一个表示更新结果的字符串,具体含义取决于实现 + */ + String updateEntity(String id, FtbCertificateEntity entity); + + /** + * 删除证书 + * @param info 要删除的证书的信息实体 + */ + void delete(FtbCertificateEntity info); + + /** + * 组织列表统计 + * @param statisticDTO 统计参数对象,包含需要进行统计的条件和范围 + * @return 返回一个分页视图对象,包含组织智慧统计信息 + */ + PageListVO organizationListStatistics(FtbCertificateOrgWisdomStatisticDTO statisticDTO); + + /** + * 个人维度统计 + * @param personWisdomStatisticDTO 统计参数对象,包含个人智慧统计的条件和范围 + * @return 返回一个分页视图对象,包含个人智慧统计信息 + */ + PageListVO personalDimensionStatistics(FtbCertificatePersonWisdomStatisticDTO personWisdomStatisticDTO); + + /** + * 删除指定的证书 + * @param id 要删除的证书的唯一标识符 + */ + void deleteCertificate(String id); + + /** + * 根据未选择的关键词查询证书列表 + * @param keyWords 关键词,用于过滤证书列表 + * @param page 分页信息,用于控制返回结果的页数和大小 + * @return 返回一个分页视图对象,包含根据关键词过滤后的证书列表 + */ + PageListVO queryCertificateListByNotChoose(String keyWords, CultivatePage page); + + /** + * 获取证书选项下拉数据 + * @return 获取证书选项列表 + */ + List getOption(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateUserService.java new file mode 100644 index 0000000..f25597c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCertificateUserService.java @@ -0,0 +1,64 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.certificate.FtbCertificateUserForm; +import jnpf.model.cultivate.po.certificate.CertificateUserPagination; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; + +import java.util.List; + +/** + * @author hao.peng + */ +public interface FtbCultivateCertificateUserService extends IService { + + + /** + * 列表查询 + * @param pagination 列表参数 + * @return 分页结果 + */ + List getList(CertificateUserPagination pagination); + + /** + * 详情 + * @param id id + * @return 结果 + */ + FtbCertificateUserEntity getInfo(String id); + + /** + * 新增 + * @param form 实体 + */ + void create(FtbCertificateUserForm form); + + /** + * 更新 + * @param id 主键 + * @param entity 实体 + */ + void update(String id, FtbCertificateUserEntity entity); + + /** + * 吊销 + * @param id 主键 + */ + boolean delete(String id); + /** + * 根据用户ID查询关联证书用户列表 + * + * @param userId 用户ID + * @return 用户列表,包含与指定用户ID关联的所有证书用户实体 + */ + List userList(String userId); + + /** + * 更新证书用户状态 + * + * @param queryWrapper 查询包装器,用于构建查询条件并执行更新操作 + */ + void updateStatus(QueryWrapper queryWrapper); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestOptionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestOptionService.java new file mode 100644 index 0000000..0184e03 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestOptionService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestOption; + +public interface FtbCultivateChapterTestOptionService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestResultService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestResultService.java new file mode 100644 index 0000000..592bdbf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestResultService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbCultivateChapterTestResultService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestService.java new file mode 100644 index 0000000..149ce0a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateChapterTestService.java @@ -0,0 +1,21 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTest; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestStatisticVO; + +public interface FtbCultivateChapterTestService extends IService { + /** + * 根据关键词分页获取章节测试统计信息 + * 此方法用于根据关键词搜索并分页获取章节测试的统计信息,旨在支持批量数据的高效检索与管理 + * 通过结合关键词搜索和分页技术,实现了对大量测试数据的有效导航和呈现 + * + * @param keyWords 关键词 用于模糊匹配查询条件的关键信息 可能是章节标题、测试名称等 + * @param page 分页参数对象 用于指定返回数据的页码和每页数量 以便于分页显示大量数据 + * @return 返回一个PageListVO对象 包含分页后的章节测试统计信息 包括总记录数、当前页数据等 + */ + PageListVO getChapterTestStatistic(String keyWords, + CultivatePage page); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseAppService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseAppService.java new file mode 100644 index 0000000..dc103a9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseAppService.java @@ -0,0 +1,56 @@ +package jnpf.cultivate.service; + +import jnpf.base.UserInfo; +import jnpf.model.cultivate.dto.course.app.FtbChapterStudyDTO; +import jnpf.model.cultivate.vo.course.app.FtbChapterAppDetails; +import jnpf.model.cultivate.vo.course.app.FtbCourseDetailsAppVO; +import jnpf.model.cultivate.vo.course.app.FtbCourseOutlineAppVO; + +/** + * @Title:FtbCultivateCourseAppService + * @Author:peng.hao + * @create: 2023/12/2911:21 + */ +public interface FtbCultivateCourseAppService { + /** + * 课程学习模块-课程详情 + * + * @param courseId 课程id + * @param userId 用户id + * @return 课程详情 + */ + FtbCourseDetailsAppVO courseDetails(String courseId, String userId); + + /** + * 完成章节学习 + * @param userInfo 用户信息 + * @param ftbChapterStudyDTO 章节学习参数 + */ + void chapterStudy(UserInfo userInfo, FtbChapterStudyDTO ftbChapterStudyDTO); + + /** + * 增加当前课程浏览量 + * @param courseId 课程id + */ + void courseBrowsing(String courseId); + + /** + * 课程大纲 + * @param courseId 课程id + * @param userId 用户id + */ + FtbCourseOutlineAppVO courseOutline(String courseId, String userId); + + /** + * 时长记录 + * @param userInfo 用户信息 + * @param ftbChapterStudyDTO 时长记录参数 + */ + void durationRecord(UserInfo userInfo, FtbChapterStudyDTO ftbChapterStudyDTO); + + /** + * 章节详情 + * @param id 章节id + */ + FtbChapterAppDetails chapterDetails(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseChapterService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseChapterService.java new file mode 100644 index 0000000..4a75099 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseChapterService.java @@ -0,0 +1,81 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseChapterDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseChapterUpdateDTO; +import jnpf.model.cultivate.dto.course.app.FtbGlobalCurriculumAppWrapDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterAppDetails; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppWrapVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterDetailsVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterVO; + +public interface FtbCultivateCourseChapterService extends IService { + + + /** + * 添加课程章节 + * + * @param ftbCultivateCourseDTO 课程章节参数 + */ + void add(FtbCultivateCourseChapterDTO ftbCultivateCourseDTO); + + /** + * 删除课程章节记录 + * + * @param id 课程章节id + */ + void delete(String id); + + /** + * 查询课程章节列表 + * + * @param page 分页对象,包含了分页信息和查询结果 + * @param courseId 课程ID,用于标识需要查询的课程章节 + * @return 返回包含课程章节信息的分页对象 + */ + Page queryList(Page page, String courseId); + + /** + * 更新章节信息 + * + * @param ftbCultivateCourseDTO 章节信息参数 + */ + void updateChapter(FtbCultivateCourseChapterUpdateDTO ftbCultivateCourseDTO); + + /** + * 获取通用课程列表的分页数据 + * + * @param page 页面对象,包含分页信息和已有的课程数据 + * @param ftbGlobalCurriculumAppWrapDTO 封装的查询条件对象,用于筛选课程 + * @return 返回一个 FtbGlobalCurriculumAppWrapVO 对象,包含根据条件筛选后的课程列表和分页信息 + */ + FtbGlobalCurriculumAppWrapVO generalCourseList(Page page, FtbGlobalCurriculumAppWrapDTO ftbGlobalCurriculumAppWrapDTO); + + /** + * 根据ID获取培养课程章节详情 + * + * @param id 章节的唯一标识符 + * @return 返回包含章节详细信息的FtbCultivateCourseChapterDetailsVO对象 + */ + FtbCultivateCourseChapterDetailsVO courseDetails(String id); + + /** + * 同步岗位 + * + * @param ftbCultivateCourse + */ + void asyncPosition(FtbCultivateCourse ftbCultivateCourse); + + /** + * 获取V2版本课程章节详情 + * + * @param id 课程章节ID + * @return 课程章节详情 + */ + V2ChapterAppDetails courseDetailsV2(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCoursePackageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCoursePackageService.java new file mode 100644 index 0000000..16fa70a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCoursePackageService.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageAddDTO; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageQueryDTO; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageUpdateDTO; +import jnpf.model.cultivate.po.coursepackage.FtbCultivateCoursePackage; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackageDetailsVO; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackagePageVO; + +import java.util.List; + +public interface FtbCultivateCoursePackageService extends IService { + + /** + * 添加课程包 + * + * @param ftbCultivateCoursePackageAddDTO 添加课程包参数 + */ + void add(FtbCultivateCoursePackageAddDTO ftbCultivateCoursePackageAddDTO); + + /** + * 课程包列表 + * @param page 分页对象 + * @param ftbCultivateCoursePackageQueryDTO 搜索参数 + * @return 课程包列表 + */ + Page pagingQuery(Page page, FtbCultivateCoursePackageQueryDTO ftbCultivateCoursePackageQueryDTO); + + /** + * 更新课程包 + * @param ftbCultivateCoursePackageUpdateDTO 更新课程包参数 + */ + void updateCoursePackage(FtbCultivateCoursePackageUpdateDTO ftbCultivateCoursePackageUpdateDTO); + + /** + * 删除课程包 + * @param id 课程包id + */ + void deleteCoursePackage(String id); + + /** + * 课程包详情-课程列表分页 + * @param id 课程包id + */ + List get(String id); + + /** + * 查询课程包课程 + * @param coursePackageIds 课程包主键id集合 + */ + List queryCoursePackageCourses(List coursePackageIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseService.java new file mode 100644 index 0000000..57bf14e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseService.java @@ -0,0 +1,125 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.ActionResult; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseQueryDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseUpdateDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateShelvesDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.vo.course.web.*; + +import java.util.List; + +public interface FtbCultivateCourseService extends IService { + /** + * 添加课程 + * + * @param ftbCultivateCourseDTO 课程信息 + */ + String add(FtbCultivateCourseDTO ftbCultivateCourseDTO); + + FtbCultivateCourse realAdd(FtbCultivateCourseDTO ftbCultivateCourseDTO); + + /** + * 课程上下架 + * + * @param ftbCultivateShelvesDTO 课程上下架信息 + */ + void upperLower(FtbCultivateShelvesDTO ftbCultivateShelvesDTO); + + /** + * 获取课程数量 + * + * @return 课程数量 + */ + FtbCultivateCourseNumberVO listCount(); + + /** + * 获取课程详情 + * + * @param id 课程ID + * @return FtbCultivateCourse对象 + */ + FtbCultivateCourseDetailsVO courseDetails(String id); + + /** + * 分页查询课程信息 + * + * @param page 分页对象 + * @param ftbCultivateCourseQueryDTO 课程查询条件 + * @return 分页课程信息 + */ + Page pagingQuery(Page page, FtbCultivateCourseQueryDTO ftbCultivateCourseQueryDTO); + + /** + * 删除指定id的数据,并根据flag标记删除方式执行删除操作 + * + * @param id 要删除的数据的id + * @return 删除结果的动作返回值 + */ + void delete(String id); + + /** + * 更新课程信息 + * + * @param ftbCultivateCourseUpdateDTO 课程更新数据对象 + */ + void updateCourse(FtbCultivateCourseUpdateDTO ftbCultivateCourseUpdateDTO); + + void realUpdateCourse(FtbCultivateCourseUpdateDTO ftbCultivateCourseUpdateDTO); + + /** + * 删除岗位学习列表 + * + * @param page 分页列表 + * @param courseId 课程id + * @return {@link Page}<{@link FtbCourseDeleteJobLearnVO}> + */ + Page deleteJobLearningList(Page page, String courseId); + + /** + * 删除相关题库列表 + * + * @param page 分页列表 + * @param courseId 课程id + * @return {@link Page}<{@link FtbCourseDeleteQuestionBankVO}> + */ + Page deleteRelatedQuestionBankList(Page page, String courseId); + + + /** + * 根据用户ID获取推广渠道学习课程的列表 + * + * @param userId 用户ID + * @return 推广渠道学习课程列表 + */ + List promotionPathwayCourses(String userId); + + /** + * 课程删除检查判断 + * + * @param id 课程id + */ + ActionResult deleteCheck(String id); + + + /** + * 学习地图初始化 + * + * @param userId 用户ID + * @param positionId 岗位ID + */ + void learnMapRelearn(String userId, String positionId); + + /** + * 校验岗位是否建立岗位学习 + * + * @param postIds 不存在的岗位id信息 + * @return 岗位信息 + */ + List queryJobBindingCourses(List postIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseSettingService.java new file mode 100644 index 0000000..488b869 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseSettingService.java @@ -0,0 +1,22 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseSettingDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourseSetting; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestResultVO; + +import java.util.List; + +public interface FtbCultivateCourseSettingService extends IService { + /** + * 课程学习设置 + */ + void buildUpCourseLearningSettings(FtbCultivateCourseSettingDTO ftbCultivateCourseSettingDTO, String courseId); + + /** + * 随堂测试结果存储 + */ + void onSiteTestResultStorage(List ftbChapterTestDTOs, String courseId, String chapterId); + void onSiteTestResultStorageV2(List ftbChapterTestDTOs, String courseId, String chapterId, String userId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseStatisticesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseStatisticesService.java new file mode 100644 index 0000000..33a9524 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseStatisticesService.java @@ -0,0 +1,25 @@ +package jnpf.cultivate.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseOrgStatisticsDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCoursePersonStatisticesDTO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseOrgStatisticesVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCoursePersonStatisticesVO; + +public interface FtbCultivateCourseStatisticesService { + /** + * 组织维度统计 + * + * @param req 统计查询条件对象,包含组织维度统计所需的筛选参数 + * @return 返回组织维度统计结果的分页列表,数据类型为FtbCultivateCourseOrgStatisticesVO + */ + PageListVO organizationListStatistics(FtbCultivateCourseOrgStatisticsDTO req); + + /** + * 个人维度统计 + * + * @param req 统计查询条件对象,包含个人维度统计所需的筛选参数 + * @return 返回个人维度统计结果的分页列表,数据类型为FtbCultivateCoursePersonStatisticesVO + */ + PageListVO personListStatistics(FtbCultivateCoursePersonStatisticesDTO req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTriggerLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTriggerLogService.java new file mode 100644 index 0000000..18ea3d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTriggerLogService.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.course.FtbCultivateCourseTriggerLog; + +/** + * 课程触发记录 + * + * @author xuguilin + */ +public interface FtbCultivateCourseTriggerLogService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTypeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTypeService.java new file mode 100644 index 0000000..4074074 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateCourseTypeService.java @@ -0,0 +1,52 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.ActionResult; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseTypeDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseTypeUpdateDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourseType; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseTypeVO; + +import java.util.List; + +public interface FtbCultivateCourseTypeService extends IService { + + /** + * 添加课程类型 + * + * @param ftbCultivateCourseDTO 课程类型参数 + */ + void add(FtbCultivateCourseTypeDTO ftbCultivateCourseDTO); + + /** + * 删除操作 + * + * @param id 课程id + * @return 删除操作的结果 + */ + ActionResult delete(String id); + + /** + * 更新课程类型信息 + * + * @param ftbCultivateCourseTypeDTO 课程类型参数 + */ + void updateInfo(FtbCultivateCourseTypeUpdateDTO ftbCultivateCourseTypeDTO); + + /** + * 分页查询课程类型列表 + * + * @param page 分页对象,用于承载查询条件及分页结果 + * @return 分页查询结果,包含查询到课程类型列表及分页信息 + */ + Page pageList(Page page); + + /** + * 获取所有课程类型 + * + * @return 所有课程类型的列表 + */ + List getAll(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamHistoryPaperService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamHistoryPaperService.java new file mode 100644 index 0000000..a75e16f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamHistoryPaperService.java @@ -0,0 +1,47 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamHistoryPaper; +import jnpf.model.cultivate.resp.InnerQueryExamResultDto; + +import java.util.List; +import java.util.Map; + +public interface FtbCultivateExamHistoryPaperService extends IService { + /** + * 根据考试名称 模糊查询历史考试信息 + * @param keyword 考试名称 + * @return + */ + List searchFrzzExamName(String keyword); + + /** + * 根据考试ID 批量查询历史试卷详情 + * @param examIds 考试ID集合 + * @return + */ + Map queryHistoryExamListByIds(List examIds); + + /** + * 根据考试ID 和具体批次 查询历史试卷详情 + * @param id 考试ID + * @param versionBatch 批次 + * @return + */ + FtbCultivateExamHistoryPaper queryByEamIdAndVersionBatch(String id, String versionBatch); + + /** + * 记录考试历史记录 + * @param oldExam + */ + void recordHistory(FtbCultivateExam oldExam); + + /** + * 根据考试id查询历史试卷 + * @param examId 考试id + * @return + */ + List queryByExamId(String examId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamService.java new file mode 100644 index 0000000..3c90d0a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamService.java @@ -0,0 +1,291 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.req.exam.QueryExamReq; +import jnpf.model.cultivate.req.exam.SaveExamReq; +import jnpf.model.cultivate.req.exam.WebQueryExamReadOverReq; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.personnels.dto.authoritys.PermissionsCacheDTO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; + +import java.util.List; +import java.util.Map; + +/** + * 考试服务接口 + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbCultivateExamService extends IService { + + /** + * 根据试卷ID查询考试列表 + * + * @param paperId 试卷ID + * @return + */ + List queryExamListByPaperId(String paperId); + + + /** + * 查询考试列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryExamReq req); + + /** + * 根据考试ID 查询试卷信息 + * + * @param examId 考试ID + * @return + */ + PaperVo queryPaperInfoForExamId(String examId); + + /** + * 查询批阅角色 + * + * @return + */ + List queryRoleReviewerList(); + + /** + * 根据考试ID列表 查询试卷信息 + * + * @param examIds 考试ID列表 + * @return examId -> PaperVo 的map + */ + Map queryPaperInfoForExamIds(List examIds); + + /** + * 根据考试ID列表查询考试信息和考试的试卷基本信息 + * + * @param examIds + * @return + */ + List queryExamAndPaperInfo(List examIds); + + /** + * 查询考试详情 + * + * @param examId 考试ID + * @return + */ + ExamVo getInfo(String examId); + + /** + * 新增考试 + * + * @param req + */ + void insertData(SaveExamReq req); + + /** + * 修改考试 + * + * @param req + */ + void updateData(SaveExamReq req); + + /** + * 删除考试 + * + * @param examId 考试ID + */ + void deleteData(String examId); + + /** + * 检查是否可以删除 + * + * @param examId 考试ID + * @return + */ + CanDeleteMsg checkExamCanDelete(String examId); + + /** + * 查询所有的岗位和考试 + * + * @return + */ + List queryPostAndRank(); + + /** + * 查询考试详情[考试 和 试卷] + * + * @param examId 考试ID + * @return + */ + ExamAndPaperDetailVo queryExamDetail(String examId); + + /** + * APP查询考试信息 + * + * @param examId + * @return + */ + ExamAppVo examInfo(String examId); + + /** + * 根据考试id查询考试题目 + * + * @param examId 考试id + * @return + */ + List queryExamQuestionList(String examId); + + /** + * 根据用户考试记录ID查询考试题目 + * + * @param userExamId 用户考试记录id + * @return 考试的题目列表 + */ + + List queryExamQuestionListByUserExamId(String userExamId); + + /** + * 查询考试的试卷信息 + * + * @param examId 考试ID + * @return + */ + PaperDetailVo queryCurrExamPaperInfo(String examId); + + /** + * 查询考试信息 + * + * @param examId 考试id + * @return + */ + FtbCultivateExam queryExamInfo(String examId); + + /** + * 查询考试 和岗位是否有绑定关系 + * + * @param examId 考试ID + * @param positionId 岗位ID + * @return 非null--有绑定 null--无绑定 + */ + FtbCultivateExam queryExamAndPositionRelation(String examId, String positionId); + + /** + * 考试 和岗位进行绑定 + * + * @param examId 考试ID + * @param positionId 岗位ID + * @return true--成功 false--失败 + */ + Boolean bindExamAndPositionRelation(String examId, String positionId); + + /** + * 查询批阅人列表 + * + * @return + */ + List queryReviewerUserList(); + + /** + * 修改常规考试状态 + */ + void changeExamStatus(); + + /** + * 分析试卷 各类型题目数量 + * + * @param paperId 试卷ID + * @return + */ + Map analysPaperQuestionCount(String paperId); + + /** + * 填充批阅人 和阅卷角色 + * + * @param examUser 用户考试记录 + * @param exam 考试信息 + */ + void fillRevierwer(FtbCultivateExamUser examUser, FtbCultivateExam exam); + + /** + * 查询批阅人列表 + * + * @param loginUserId 用户id + * @return + */ + List queryExamReviewList(String loginUserId); + + /** + * 查询所有的岗位 + * + * @return + */ + List queryAllPostion(); + + /** + * 根据考试id查询考试相关的历史试卷 + * + * @param req + * @return + */ + List historyPaperList(WebQueryExamReadOverReq req); + + /** + * 考试作废-常规考试 + * + * @param id 考试ID + * @return + */ + void nullify(String id); + + /** + * 根据岗位查询是否考试完成才能够鉴定的标志 + * + * @param positionId 岗位 id + * @return + */ + Integer queryExamAndIdentifyConfig(String positionId); + + /** + * 查询考试历史试卷待考试的数量 + * + * @param req + * @return + */ + Map waitNumberForBatch(WebQueryExamReadOverReq req); + + /** + * 初始化考试数据(兼容旧数据) + * + * @return + */ + List initOldExamData(); + + /** + * 旧数据清洗,删除已经删除的考试 的相关考试记录 + */ + void deleteExamUserForDeleteExam(); + + /** + * 填充考试的批阅人信息 + * + * @param examUser 考试用户记录薪酬 + * @param exam 考试信息 + * @param permissionsCacheDTO 考试的权限信息 + */ + void fillRevierwerNew(FtbCultivateExamUser examUser, FtbCultivateExam exam, PermissionsCacheDTO permissionsCacheDTO); + + /** + * 查询考试关联的任务列表 + * + * @param id 考试ID + * @return + */ + List checkNullify(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserDetailService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserDetailService.java new file mode 100644 index 0000000..85cc996 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserDetailService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail; + +public interface FtbCultivateExamUserDetailService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserService.java new file mode 100644 index 0000000..8f3f11b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateExamUserService.java @@ -0,0 +1,550 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.statistics.CultivateUserExamCountDTO; +import jnpf.model.cultivate.dto.statistics.ExamStatisticsForOrgDTO; +import jnpf.model.cultivate.dto.statistics.ExamStatisticsForPersonDTO; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail; +import jnpf.model.cultivate.req.exam.*; +import jnpf.model.cultivate.resp.*; +import jnpf.model.cultivate.v2.exam.vo.V2TestPaperVo; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionExamPersonVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionExamVO; +import jnpf.model.personnels.dto.authoritys.PermissionsCacheDTO; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public interface FtbCultivateExamUserService extends IService { + + + /** + * 根据考试ID 分页查询考试的用户考试列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryExamUserReq req); + + + /** + * 查询用户考试详情 + * + * @param userExamId 用户考试ID + * @return + */ + FtbCultivateExamUser getInfo(String userExamId); + + + /** + * 删除用户考试 + * + * @param userExamId 用户考试ID + */ + void deleteData(String userExamId); + + /** + * 检查是否可以删除用户考试 + * + * @param userExamId 用户考试ID + * @return + */ + CanDeleteMsg checkUserExamCanDelete(String userExamId); + + /** + * 批阅用户考试 + * + * @param req + */ + void readOver(ReadOverExamReq req); + + /** + * 查看用户的考试 试卷详情 + * + * @param userExamId 用户考试ID + * @param isQueryQuestionList 是否用户的题目列表 true:是 false:否 + * @return + */ + UserExamDetailVo getUserExamDetail(String userExamId, Boolean isQueryQuestionList); + + /** + * 排行榜 + * + * @param examId 考试ID + * @return + */ + PageInfo rankIngList(String examId, QueryExamRankListReq req); + + /** + * 开始考试 + * + * @param userExamId 用户考试ID + * @return + */ + FtbCultivateExamUser startExam(String userExamId); + + /** + * 查询用户的完成的题目 + * + * @param examId 考试ID + * @param userExamId 用户考试ID + * @param userId 用户ID + */ + List queryExamUserQuestion(String examId, String userExamId, String userId); + + + /** + * 提交试卷 + * + * @param userExamId 用户考试ID + * @param req + */ + + SubExamVo subExam(String userExamId, SubExamQuestionReq req); + + /** + * 查询我的考试列表 + * + * @param req + * @return + */ + PageInfo queryMyExamList(QueryExamListReq req); + + /** + * 根据用户ID 查询考试列表 + * + * @param userId 用户ID + * @return + */ + PageInfo queryExamListForUserId(String userId, CultivatePage page); + + /** + * 等待我批阅的考试列表 + * + * @param req + * @return + */ + PageInfo queryWaitMyReadOver(QueryExamListReq req); + + /** + * 查询我已经完成批阅 + * + * @param req + * @return + */ + PageInfo queryMyCompleteReadOver(QueryExamListReq req); + + /** + * 查询下属逾期未考的,仅仅常规考试 + * + * @param req + * @return + */ + PageInfo expireExam(QueryExpireListReq req); + + /** + * 清除下属逾期未考的,仅仅常规考试 + * + * @param userExamIds + */ + + void clearExpireExam(String userExamIds); + + /** + * 触发岗位学习考试 + * + * @param triggerExamDto + * @return + */ + Boolean triggerPositionExam(TriggerExamDto triggerExamDto); + + + /** + * 取消 岗位学习考试 + * 取消学习了 但是未完成考试的 + * + * @param innerExamDto + * @return + */ + Boolean cancelPositionExam(InnerExamDto innerExamDto); + + /** + * 根据用户查询完成的考试 + * + * @param userId + * @return + */ + List queryCompleteExamListForUserId(String userId); + + /** + * 重新开始考试 + * + * @param userExamId 用户考试ID + * @return + */ + FtbCultivateExamUser reStartExam(String userExamId); + + /** + * 查询用户的总的考试数量和已经完成的考试数量 + * + * @param userIds + * @return + */ + Map queryExamTotalAndCompleteNumForUserIds(List userIds); + + /** + * 数据看板 考试统计 + * + * @param req + * @return + */ + ExamStatisticsVo statistics(FtbCultivateStatisticsDTO req); + + /** + * 数据看板 考试统计 + * + * @param req + * @return + */ + ExamStatisticsVo statisticsNew(FtbCultivateStatisticsDTO req); + + /** + * 根据用户ID 和岗位ID 查询用户的考试列表 + * + * @param dto + * @return + */ + PageInfo queryExamListForUserIdAndPostId(QueryUserExamListDto dto); + + /** + * 检测并修改过期考试 + * + * @return + */ + Integer checkAndUpdateExpireUserExamStatus(); + + /** + * 查询我下属 等待、完成 、过期数量 + * + * @return + */ + MyBlowExamNum queryMyBlowExamNum(); + + /** + * 考试排行榜 + * + * @param dto + * @return + */ + List statisticsExamRankingList(FtbCultivateStatisticsDTO dto); + + /** + * 考试排行榜 (web端 不要再按照用户权限过滤了) + * + * @param dto + * @return + */ + List statisticsExamRankingListNoPower(FtbCultivateStatisticsDTO dto); + + /** + * 根据用户ID 查询用户的组织 + * + * @param userId 用户id + * @return + */ + List queryUserOrganization(String userId); + + /** + * 根据用户id批量查询用户的岗位信息 + * @param userIds 用户ID 集合 + * @return + */ + Map> queryUserOrganizationByUserIds(List userIds) ; + + /** + * 根据考试Id查考试已经完成的人数 + * + * @param examId 考试id + * @return + */ + Long queryCompleteNumForExamId(String examId); + + /** + * 查询用户考试记录基本信息 + * + * @param userExamId 用户考试记录ID + * @return + */ + UserExamDetailVo queryBaseUserExamInfo(String userExamId); + + /** + * 根据考试id查询排行版 + * + * @param examId 考试id + * @return + */ + UserRankingVo queryMyRank(String examId); + + /** + * 删除考试,同时创建考试 + * + * @param id 删除id并重新创建考试 + */ + void deleteAndCreateExam(String id); + + /** + * 培训功能优化,统计组织下用户考试合格率、合格人数、考试总人数 + * + * @param dto + * @return + */ + ExamCultivateCountVo queryCultivateCount(CultivateUserExamCountDTO dto, List userIds); + + /** + * 根据岗位id统计 考试人数,优秀人数 合格人数 不合格人数 + * + * @param positionId 岗位id + * @return + */ + ExamCultivateForPositionVo queryCultivateCountForPositionId(String positionId); + + /** + * 查询指定用户的考试列表 + * @param req + * @return + */ + PageInfo queryExamList(QueryCultivateExamReq req); + + /** + * 考试按照组织维度统计信息 + * @param dto 查询信息 + * @return + */ + List queryExamStatisticsForOrg(ExamStatisticsForOrgDTO dto, List userIds); + + /** + * 考试按照个人维度统计信息 + * @param dto 查询信息 + * @return + */ + PageInfo queryExamStatisticsForPerson(ExamStatisticsForPersonDTO dto); + + /** + * 根据岗位ID统计合格率 + * + * @param positionId + * @return + */ + BigDecimal queryExamStatisticsForPositionId(String positionId); + + /** + * 处理考试用户的岗位数据,兼容老数据 + */ + + void dealExamUserPositionForOldData(); + + /** + * 查询考试是否完成 + * + * @param userId 用户ID + * @param positionId 岗位ID + * @param settings 是否开启合格设置 1是 0 否 + * @return [1:已经完成考试 2:未完成考试, 3:考试未合格] + */ + Integer queryExamIsCompleteForUserIdAndPostion(String userId, String positionId, Integer settings); + + /** + * 查询 + * @param str 用户考试记录id + * @param learningMapSetting 是否开启合格设置 1是 0 否 + * @return (0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + */ + Integer queryExamIsCompleteForUserIdAndPostionWithNew(String str, Integer learningMapSetting); + + /** + * Web端发起重考 + * + * @param req + */ + void webRestartExam(WebRestartExamReq req); + /** + * 视为作弊提交试卷 + * + * @param cheatType 作弊类型 1-超过切屏次数视为作弊,2-超过考试时间无交互视为作弊 + * @param userExamId 用户考试ID + * @param req + * @return + */ + void setCheat(String userExamId, Integer cheatType, SubExamQuestionReq req) throws Exception; + + /** + * 用户考试的批阅人权限 + */ + void refreshUserExamPower(); + + /** + * app批阅考试查询考试信息(简单) + * + * @param userExamId 用户考试记录id + * @return + */ + UserExamDetailVo queryUserExamSimpleMsg(String userExamId); + + /** + * Web端考试历史试卷列表 + * @param req + * @return + */ + PageInfo webHistoryExamUserList(WebHistoryQueryExamUserReq req); + + /** + * 隐藏我的考试记录 + * @param userExamId 用户考试记录ID + */ + void hiddenMyExamRecord(String userExamId); + + /** + * 查询考试合格率和考试合格人数 + * + * @param dbPostId + * @param userIdsByGradesId + * @return + */ + FtbCultivatePositionExamVO queryExamPassingInformation(String dbPostId, List userIdsByGradesId); + + /** + * 批量查询一个岗位的指定人员的考试信息 + * @param userIds 用户id集合 + * @param positionId 岗位id + * @param exmIds 考试id + * @return + */ + FtbCultivatePositionExamPersonVO queryTheListOfQualifiedPersons(List userIds, String positionId, List exmIds); + + /** + * 查询是否有完成的考试 如果有完成的考试就生成 + * @param id 考试id + * @param versionBatch 批次 + * @return + */ + List queryCompleteExamUserListForExamIdAndBatch(String id, String versionBatch); + + /** + * 根据考试ID查询待考试的列表 + * @param examId 考试ID + * @return + */ + List queryWaitReadOverNum(String examId); + + /** + * 检查用户是否完成考试 + * @param positionId 岗位ID + * @param userId 用户ID + * @return + */ + CompleteExamVo checkIsCompleteExamForUserId(String positionId, String userId); + + /** + * 查询我的岗位考试列表(完成考试合格且需要考试合格才能鉴定 却未鉴定的岗位) + * @loginUserId 用户ID + * @return + */ + List queryAlertPostionIdentify(String loginUserId); + + /** + * 岗位完成鉴定 + * @param userId 用户id + * @param positionId 岗位ID + */ + void completeIdentifyCallBack(String userId, String positionId); + + + /** + * 岗位学习的考试 或者 是否是考试合格才能触发鉴定 发生变化时回调 + * @param positionId 岗位学ID + * @param examId 考试ID + * @param examIdentifyConfig 是否是考试合格才能触发鉴定,0-否 1-是 + */ + void positionConfigChangeCallBack(String positionId, String examId,Integer examIdentifyConfig); + + /** + * 查询用户的权限信息 + * @param userId 用户id + * @param permissionsCacheDTO 权限信息 + * @param localOrganizeIds 用户当前组织集合 + * @return + */ + List getPowerUserIdsForPermission(String userId, PermissionsCacheDTO permissionsCacheDTO, List localOrganizeIds) ; + + + /** + * 查询任务考试结果 + * + * @param taskId 任务id + * @param userId 用户ID + * @return + * -1:没有考试记录 + * 0:未完成考试 + * 1:已经完成考试(待批阅,未出考试结果) + * 3合格 + * 4不合格 + * 5优秀 + */ + Integer queryExamResultForTask(String taskId, String userId); + + /*** + * 查询任务考试结果 + * @param taskId 任务id + * @param userId 用户id + * @return + */ + List queryExamResultForTaskInfo(String taskId, String userId); + + /** + * 统计通过的任务考试数量 + * @param taskIds 任务集合ID + * @return + */ + Map groupPassCountNum(List taskIds); + + /** + * 批量统计所有的任务考试数量 + * @param taskIds + * @return + */ + Map groupAllCountNum(List taskIds); + + /** + * 删除任务考试记录(用户任务终止和任务删除) + * @param taskId 任务id + */ + void deleteTaskExamUserRecord(String taskId); + + Boolean checkMyIsRevierverDynamic(FtbCultivateExam exam, FtbCultivateExamUser examUser, String userId, PermissionsCacheDTO permissionsCacheDTO); + + + void batchFillExamUserInfo(List examUserList); + + void fillExtraInfo(String loginUserId, List list); + + void fillRelationPositionName(List list) ; + + Map groupAllCountNumV2(List taskIds); + + /** + * 统计通过的任务考试数量 + * @param taskIds 任务集合ID + * @return + */ + Map groupPassCountNumV2(List taskIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnCategoriesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnCategoriesService.java new file mode 100644 index 0000000..3c3ecae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnCategoriesService.java @@ -0,0 +1,93 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnCategoriesDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnCategories; +import jnpf.model.cultivate.req.learn.AddLearnCategoryReq; +import jnpf.model.cultivate.req.learn.QueryLearnCategoryListReq; +import jnpf.model.cultivate.req.learn.UpdateLearnCategoryReq; + +import java.util.List; +import java.util.Map; + +/** + * @author 许贵林 + * @descriptionService + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbCultivateLearnCategoriesService extends IService { + + /** + * 查询全部分类并构建分类tree + * + * @param req + * @return + */ + List listCategory(QueryLearnCategoryListReq req); + + /** + * 新增分类 + * + * @param req + * @return + */ + FtbCultivateLearnCategories insertData(AddLearnCategoryReq req); + + /** + * 删除分类 + * + * @param id 分类ID + */ + + void deleteData(String id); + + /** + * 修改分类 + * + * @param id 分类ID + * @param req + * @return + */ + FtbCultivateLearnCategories updateData(String id, UpdateLearnCategoryReq req); + + /** + * 根据分类查询子分类 + * + * @param parentId 分类ID + * @return + */ + List queryChildCategory(String parentId); + + /** + * 根据id查询并检查分类信息 + * + * @param id 分类ID + * @return + */ + FtbCultivateLearnCategories queryAndCheckById(String id); + + /** + * 查询子分类的数量 + * + * @param parentId 分类ID + * @return + */ + Long queryChildCategoryNum(String parentId); + + /** + * 根据分类id查询分类信息 + * + * @param ids 分类ID集合 + * @return + */ + Map queryCateByIds(List ids); + + /** + * 根据分类id 查询该分类和他父级分类信息 + * + * @param cateId 分类id + * @return 分类信息列表 父级和子级 + */ + List queryCategoryAndParentCategory(String cateId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAppContentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAppContentService.java new file mode 100644 index 0000000..c2fe19a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAppContentService.java @@ -0,0 +1,5 @@ +package jnpf.cultivate.service; + +public interface FtbCultivateLearnTaskAppContentService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAssignmentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAssignmentService.java new file mode 100644 index 0000000..66af93d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskAssignmentService.java @@ -0,0 +1,30 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskAssignmentService extends IService { + + /** + * 统计任务学习人数 + * @param taskIds 任务id + * @param status 0-查询全部 1-查询已经完成的 + * @return + */ + Map groupCountNum(List taskIds, Integer status); + + /** + * 查询任务学习人员列表 + * @param taskIds 任务ID + * @return 任务ID->人员列表 + */ + Map> groupListAssignment(ArrayList taskIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCertificateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCertificateService.java new file mode 100644 index 0000000..e06f3b9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCertificateService.java @@ -0,0 +1,40 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCertificate; +import jnpf.model.cultivate.resp.TaskRelationCertificateVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskCertificateVo; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskCertificateService extends IService { + + /** + * 批量查询正式名称 + * + * @param taskIds 任务id集合 + * @return 返回证书信息 + */ + List queryCertificateName(List taskIds); + + /** + * 根据任务ID查询所有有效证书 + * + * @param taskId 任务ID + * @return 证书列表 + */ + List listByTaskId(String taskId); + + /** + * 根据任务ID和阶段ID查询所有有效证书 + * + * @param taskId 任务ID + * @param phaseId 阶段ID + * @return 证书列表 + */ + List listByTaskIdAndPhaseId(String taskId, String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCourseService.java new file mode 100644 index 0000000..3041904 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskCourseService.java @@ -0,0 +1,43 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCourse; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskCourseVo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskCourseService extends IService { + + /** + * 批量查询任务的课程数量 + * + * @param taskIds 任务id集合 + * @return + */ + Map groupCountNum(List taskIds); + + /** + * 批量查询任务的课程列表 + * + * @param taskIds 任务ID集合 + * @return + */ + Map> groupListTaskCourse(ArrayList taskIds); + + /** + * 根据任务ID查询所有有效课程 + * + * @param taskId 任务ID + * @param required 是否必修(0-是 1-否) null全部 + * @return 课程列表 + */ + List listByTaskId(String taskId, Integer required); + + List listByTaskIdAndPhaseId(String taskId, String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskExamService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskExamService.java new file mode 100644 index 0000000..94c7990 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskExamService.java @@ -0,0 +1,41 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskExam; +import jnpf.model.cultivate.resp.TaskRelationExamVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskExamVo; + +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +public interface FtbCultivateLearnTaskExamService extends IService { + + /** + * 批量查询任务关联的考试信息 + * + * @param taskIds 任务ID集合 + * @return + */ + List queryExamBaseInfo(List taskIds); + + /** + * 根据任务ID查询所有有效考试 + * + * @param taskId 任务ID + * @return 考试列表 + */ + List listByTaskId(String taskId); + + List listByTaskIdAndPhaseId(String taskId, String phaseId); + + /** + * 检查考试是否被学习任务绑定(关联任务主表) + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + Boolean checkExamBinding(String examId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskIdentificationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskIdentificationService.java new file mode 100644 index 0000000..7462cc1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskIdentificationService.java @@ -0,0 +1,47 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.resp.TaskRelationIdentificationVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskIdentificationVo; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +public interface FtbCultivateLearnTaskIdentificationService extends IService { + + /** + * 批量查询任务关联的鉴定信息 + * @param taskIds 任务id集合 + * @return + */ + List queryIdentificationName(List taskIds); + + /** + * 更加任务id查询鉴定信息 + * @param taskId 任务id + * @return + */ + FtbCultivateLearnTaskIdentification queryByTaskId(String taskId); + + /** + * 根据任务ID查询所有有效鉴定 + * @param taskId 任务ID + * @return 鉴定列表 + */ + List listByTaskId(String taskId); + + /** + * 根据任务ID和阶段ID查询所有有效鉴定 + * @param taskId 任务ID + * @param phaseId 阶段ID + * @return 鉴定列表 + */ + List listByTaskIdAndPhaseId(String taskId, String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoContentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoContentService.java new file mode 100644 index 0000000..d94fc89 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoContentService.java @@ -0,0 +1,75 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnAllocationDTO; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskInfoDto; +import jnpf.model.cultivate.vo.learn.FtbLearnQueryTaskDetailsEditingVO; + +import java.util.List; + +/** + * 学习任务详情信息保存接口 + * + * @Author: peng.hao + * @create: 2024/9/9:14:47 + */ +public interface FtbCultivateLearnTaskInfoContentService { + + /** + * 中止指定任务 + * + * @param taskId 任务ID,用于标识需要中止的任务 + */ + void abort(String taskId); + + /** + * 保存学习任务列表 + * + * @param taskInfoDto + */ + void saveLearnTaskList(FtbCultivateLearnTaskInfoDto taskInfoDto); + + /** + * 更新学习任务列表 + * + * @param taskInfoDto 学习任务信息DTO,包含需要更新的学习任务数据 + */ + void updateLearnTaskList(FtbCultivateLearnTaskInfoDto taskInfoDto); + + /** + * 获取学习任务分配列表 + * + * @param taskId 任务ID,用于标识特定的学习任务 + * @return 返回一个字符串列表,包含任务的分配详情 + */ + List getLearnTaskAllocationList(String taskId); + + /** + * 查询未完成的人员根据任务id + * @param taskId 任务id + * @return + */ + public List queryUserListForTaskNoComplete(String taskId) ; + + /** + * 分配任务 + * + * @param allocationDTO 分配数据传输对象,包含分配培养学习所需的各项数据 + */ + void allocation(FtbCultivateLearnAllocationDTO allocationDTO); + + /** + * 删除指定ID的任务 + * + * @param id 任务的唯一标识符 + */ + void deleteTask(String id); + + /** + * 在编辑过程中查询学习任务详情 + * + * @param id 任务的唯一标识符 + * @return FtbLearnQueryTaskDetailsEditingVO 返回包含任务详细信息的对象 + */ + FtbLearnQueryTaskDetailsEditingVO queryTaskDetailsWhileEditing(String id); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoCountService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoCountService.java new file mode 100644 index 0000000..989829d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoCountService.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.req.learn.QueryLearnTaskCountListReq; +import jnpf.model.cultivate.req.learn.QueryLearnTaskListReq; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.learn.*; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskUserFinishInfoVO; + +import java.util.List; + +/** + * 学习任务详情信息统计接口 + * @Author: xuguilin + * @create: 2024/9/9:14:47 + */ +public interface FtbCultivateLearnTaskInfoCountService { + + + /** + * 查询学习任务统计列表 + * @param req + * @return + */ + PageInfo queryCountList(QueryLearnTaskCountListReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoService.java new file mode 100644 index 0000000..657a144 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskInfoService.java @@ -0,0 +1,61 @@ +package jnpf.cultivate.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskFinishStatisticsVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskListVO; +import jnpf.model.cultivate.vo.learn.info.FtbCultivateLearnTaskUserFinishInfoVO; + +import java.util.List; +import java.util.Map; + +/** + * 学习任务详情信息保存接口 + * @Author: peng.hao + * @create: 2024/9/9:14:47 + */ +public interface FtbCultivateLearnTaskInfoService { + /** + * 任务内容详情 + */ + FtbCultivateLearnTaskInfoVO getLearnTaskInfo(String taskId); + /** + * 完成情况列表 + * + * @param taskId 任务id + * @return + */ + PageListVO getListOfCompletions(String taskId, CultivatePage page, List ids); + /** + * 完成情况统计 + * + * @param taskId 任务id + * @return + */ + FtbCultivateLearnTaskFinishStatisticsVO getCompletionStatistics(String taskId); + /** + * 查看任务完成情况 + * + * @param taskId 任务id + * @param userId 用户id + */ + FtbCultivateLearnTaskUserFinishInfoVO viewTaskCompletion(String taskId, String userId); + /** + * 培训任务列表 + */ + PageListVO getTaskList(FtbCultivateLearnTaskListDto taskListDto); + /** + * 更新任务列表状态,更新对应任务人员状态 + */ + void updateUserFinishTime(String userId); + + /** + * 培训任务列表列表小气泡计数 + * @param keyWords 任务名称 + * @return + */ + Map getTaskCount(String keyWords); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskListService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskListService.java new file mode 100644 index 0000000..2a0e97f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskListService.java @@ -0,0 +1,51 @@ +package jnpf.cultivate.service; + +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.req.learn.QueryLearnTaskListReq; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoListVO; +import jnpf.model.cultivate.vo.learn.WebLearnTaskDto; + +import java.util.List; +import java.util.Map; + +/** + * 学习任务列表信息 + * + * @Author: 许贵林 + * @create: 2024/9/9:14:47 + */ +public interface FtbCultivateLearnTaskListService { + + /** + * web端查询任务列表接口 + * @param req + * @return + */ + PageInfo getWebPageList(QueryLearnTaskListReq req); + + /** + * web端统计所有任务数量接口 + * @return + */ + WebLearnTaskDto countTaskNumber(); + + /** + * 分类统计任务数量 + * @return + */ + Map groupCountCateNum(); + + /** + * 查询分类下是否有任务 + * + * @param cateIds 分类IDS + * @return true-有 false-没有 + */ + Boolean checkHasTask(List cateIds); + + + /** + * 预更新任务状态 + */ + void preUpdateTaskStatus(String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskReminderRuleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskReminderRuleService.java new file mode 100644 index 0000000..271e145 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskReminderRuleService.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskReminderRule; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +public interface FtbCultivateLearnTaskReminderRuleService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskService.java new file mode 100644 index 0000000..64a4378 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateLearnTaskService.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.v2.task.vo.V2MyCultivateLearnTaskListVo; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +public interface FtbCultivateLearnTaskService extends IService { + + + /** + * 查询用户未完成的学习任务列表 + * + * @param userId 用户ID,用于标识查询哪个用户的未完成任务 + * @return V2MyCultivateLearnTaskListVo对象列表,包含用户的所有未完成学习任务信息 + */ + List queryMyNoCompleteTaskListForUserId(String userId); + + /** + * 查询指定考试ID在给定任务列表中已存在的任务ID列表 + * + * @param taskIds 任务ID列表,需要检查的任务集合 + * @param examId 考试ID,用于匹配任务中是否包含该考试 + * @return 包含指定考试ID的任务ID字符串列表 + */ + List queryTaskHasExam(List taskIds, String examId); + + /** + * 查询指定练习ID在给定任务列表中已存在的任务ID列表 + * + * @param taskIds 任务ID列表,需要检查的任务集合 + * @param practiceId 练习ID,用于匹配任务中是否包含该练习 + * @return 包含指定练习ID的任务ID字符串列表 + */ + List queryTaskHasPracticeId(List taskIds, String practiceId); + + /** + * 查询指定身份ID在给定任务列表中已存在的任务ID列表 + * + * @param taskIds 任务ID列表,需要检查的任务集合 + * @param identityId 身份ID,用于匹配任务中是否包含该身份 + * @return 包含指定身份ID的任务ID字符串列表 + */ + List queryTaskHasIdentityId(List taskIds, String identityId); + + List queryMyRunningTask(String userId, String courseId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateMyLearnTaskInfoService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateMyLearnTaskInfoService.java new file mode 100644 index 0000000..89fc193 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateMyLearnTaskInfoService.java @@ -0,0 +1,76 @@ +package jnpf.cultivate.service; + +import jnpf.base.vo.PageListVO; +import jnpf.exception.HandleException; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.vo.course.app.FtbAppTaskCountVO; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskExamInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskIdentificationInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoForAppVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateMyLearnTaskListVO; + +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/9/11:14:50 + */ +public interface FtbCultivateMyLearnTaskInfoService { + + /** + * 培训任务列表 + */ + PageListVO taskList(FtbCultivateLearnTaskListDto taskListDto); + /** + * 任务课程列表 + * + * @param taskId 任务主键ID(必传) + * @param compulsory + */ + List taskCourseList(String taskId, String userId, Integer compulsory) throws HandleException; + + /** + * 任务详情(基本信息) + * @param taskId 任务id + */ + FtbCultivateLearnTaskInfoForAppVO getLearnTaskInfoForApp(String taskId); + /** + * 立即完成 + */ + void completeTask(String taskId); + /** + * 任务考试列表 + * @param taskId 任务主键ID(必传) + */ + List taskExamList(String taskId, String userId); + /** + * 任务实操鉴定列表 + * @param taskId 任务主键ID(必传) + */ + List taskIdentificationList(String taskId, String userId); + + /** + * 更新用户完成时间 + * @param taskId 任务id + * @param userId 用户主键ID + * @param source 0:课程 1:考试 2:鉴定 + */ + void updateUserFinishTime(String taskId, String userId,Integer source); + + /** + * 获取任务列表总数 + * @param keyWords 任务名称 + * @return + */ + Map taskListCount( String keyWords); + + /** + * 查询任务数量 + * @param taskId 任务id + * @param userId 用户ID + * @return + */ + FtbAppTaskCountVO taskCourseNum(String taskId, String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineCourseService.java new file mode 100644 index 0000000..631f415 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineCourseService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineCourse; + +public interface FtbCultivateOfflineCourseService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineTrainService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineTrainService.java new file mode 100644 index 0000000..e728868 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineTrainService.java @@ -0,0 +1,102 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.UserInfo; +import jnpf.model.cultivate.dto.offline.*; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineTrain; +import jnpf.model.cultivate.vo.offline.*; + +import java.util.List; + +public interface FtbCultivateOfflineTrainService extends IService { + + + /** + * 添加线下培训 + * + * @param ftbCultivateCourseDTO 线下培训参数 + */ + void add(FtbCultivateOfflineTrainDTO ftbCultivateCourseDTO); + + /** + * 培训结果变更 + * + * @param userInfo 用户信息对象 + * @param ftbCultivateCourseDTO 培训结果变更参数 + */ + void updateTrainingResults(UserInfo userInfo, FtbCultivateOfflineTrainUpdateDTO ftbCultivateCourseDTO); + + /** + * 线下培训编辑时详情 + * + * @param id 线下培训主键id + */ + FtbCultivateOfflineTrainDetailsVO courseDetails(String id); + + /** + * 分页查询线下培训信息列表 + * + * @param page 分页对象,用于指定分页查询的页码、每页数量等信息 + * @param keywords 关键字,用于根据一定条件过滤培训信息 + * @return 返回分页查询的结果,包含经过筛选的线下培训信息列表 + * + */ + Page listPage(Page page, String keywords); + + /** + * 线下培训详情 + * @param id 线下培训id + */ + FtbCultivateOfflineTrainDetailsAppVO offlineAppDetails(String id); + + /** + * app线下培训分页 + * @param page 分页对象 + * @param type type 查询类型,不传为全部培训,1为我发起的,2为我参与的,3为我负责的 + */ + Page offlineTrainingApp(Page page, Integer type); + + /** + * 删除线下培训 + * @param id 线下培训id + */ + void deleteOfflineTrain(String id); + + /** + * 查询已有线下培训成员 + * @param id 线下培训主键id + * @return 用户主键id + */ + List queryExistingMembers(String id); + + /** + * 修改线下培训成员 + * @param data 线下培训参数 + */ + void modifyOfflineTrainingMembers(FtbModifyOfflineTrainingMembersDTO data); + + /** + * 撤回 + * @param id 线下培训主键id + */ + void withdraw(String id); + + /** + * 线下培训签到人数分页查询 + */ + Page numberOfflineTraining(Page page, FtbCultivateOfflineTrainPeopleSigningInDTO data); + + /** + * 编辑线下培训 + * @param ftbCultivateOfflineTrainDTO 编辑线下培训参数 + */ + void editOfflineTraining(FtbCultivateOfflineTrainChangeDTO ftbCultivateOfflineTrainDTO); + + /** + * 线下培训签到 + * @param freightTrainSignInDTO 线下培训签到参数 + */ + void signIn(FtbCultivateOfflineTrainSignInDTO freightTrainSignInDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineUserService.java new file mode 100644 index 0000000..cb2b23d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateOfflineUserService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineUser; + +public interface FtbCultivateOfflineUserService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePackageCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePackageCourseService.java new file mode 100644 index 0000000..432d830 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePackageCourseService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.po.coursepackage.FtbCultivatePackageCourse; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbCultivatePackageCourseService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionAssessmentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionAssessmentService.java new file mode 100644 index 0000000..7fbdde6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionAssessmentService.java @@ -0,0 +1,39 @@ +package jnpf.cultivate.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionAssessmentOrgStatisticVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionAssessmentVO; + +/** + * @Author: peng.hao + * @create: 2024/7/22:16:00 + */ +public interface FtbCultivatePositionAssessmentService { + /** + * 组织机构列表统计方法 + * + * @param statisticDTO 统计参数对象,包含需要进行统计的组织机构信息 + * @return 返回一个分页数据对象,其中包含该组织机构下的培养职位评估统计数据 + * + * 方法用途: + * - 用于对特定组织机构下的所有培养职位评估数据进行统计 + * - 通过传入的统计参数对象来过滤和计算相关数据 + */ + PageListVO organizationListStatistics(FtbCultivatePositionOrgStatisticesDTO statisticDTO); + + /** + * 职位评估列表方法 + * + * @param orgId 组织机构ID,用于筛选特定组织下的职位评估数据 + * @param positionId 职位ID,用于筛选特定职位的评估数据 + * @param page 分页对象,包含分页查询的相关参数 + * @return 返回一个分页数据对象,其中包含按组织机构和职位筛选的培养职位评估数据 + * + * 方法用途: + * - 用于获取特定组织机构和职位下的培养职位评估列表 + * - 支持分页查询,便于处理大量数据时的高效检索 + */ + FtbCultivatePositionAssessmentVO jobAssessmentList(String orgId, String positionId, CultivatePage page); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCertificateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCertificateService.java new file mode 100644 index 0000000..2ba4ed4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCertificateService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.po.position.FtbCultivatePositionCertificate; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbCultivatePositionCertificateService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCopyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCopyService.java new file mode 100644 index 0000000..20c1144 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCopyService.java @@ -0,0 +1,37 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.dto.position.web.FtbCultivateAssociatedExamsCoursesDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionCopyDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionPassExaminationDTO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionCopyVO; + +import java.util.List; + +public interface FtbCultivatePositionCopyService { + /** + * 岗位学习复制 + * + * @param data 复制参数 + * @return 复制关联岗位数据 + */ + FtbCultivatePositionCopyVO copyPosition(FtbCultivatePositionCopyDTO data); + + /** + * 岗位考试合格后才能进行鉴定修改 + * @param data 合格参数 + */ + void passingTheJobExaminationRevised(FtbCultivatePositionPassExaminationDTO data); + + /** + * 复制岗位关联考试和课程 + * @param data + */ + void associatedExamsAndCourses(FtbCultivateAssociatedExamsCoursesDTO data); + + /** + * 已存在的岗位学习 + * @return 岗位id集合 + */ + List learningFromExistingPositions(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseCertificateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseCertificateService.java new file mode 100644 index 0000000..e231df7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseCertificateService.java @@ -0,0 +1,47 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseCertificateWithNameVo; + +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/9/10:10:30 + */ +public interface FtbCultivatePositionCourseCertificateService extends IService { + + /** + * 根据岗位学习ID列表统计证书数量 + */ + Map countByPositionLearnIds(List positionLearnIds); + + /** + * 根据岗位学习ID查询所有有效证书 + */ + List listByPostLearnId(String postLearnId); + + /** + * 根据岗位学习ID和课程ID列表查询所有有效证书 + */ + List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds); + + /** + * 根据岗位学习ID查询证书列表(包含证书名称) + * + * @param postLearnId 岗位学习ID + * @return 证书列表(包含证书名称) + */ + List listByPostLearnIdWithName(String postLearnId); + + /** + * 根据岗位学习 ID 和课程 IDs 查询证书列表(包含证书名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 证书列表(包含证书名称) + */ + List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCoursePracticeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCoursePracticeService.java new file mode 100644 index 0000000..6eec33c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCoursePracticeService.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCoursePractice; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCoursePracticeWithNameVo; + +import java.util.List; +import java.util.Map; + +/** + * 岗位学习课程关联练习表服务接口 + * + * @author xgl + * @since 2025-01-19 + */ +public interface FtbCultivatePositionCoursePracticeService extends IService { + + /** + * 根据岗位学习ID列表统计练习数量 + */ + Map countByPositionLearnIds(List positionLearnIds); + + /** + * 根据岗位学习ID查询所有有效练习 + */ + List listByPostLearnId(String postLearnId); + + /** + * 根据岗位学习ID和课程ID列表查询所有有效练习 + */ + List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds); + + List queryAllPositionLearnParacticeLists(V2OtherCultivatePositionCourseForAppReq v2OtherCultivatePositionCourseForAppReq); + + List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId); + + /** + * 根据岗位学习ID查询练习列表(包含技能点名称) + * + * @param postLearnId 岗位学习ID + * @return 练习列表(包含技能点名称) + */ + List listByPostLearnIdWithName(String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询练习列表(包含技能点名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 练习列表(包含技能点名称) + */ + List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseService.java new file mode 100644 index 0000000..4f52c78 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionCourseService.java @@ -0,0 +1,97 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.Page; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.v2.course.vo.app.AppCourseSimpleVo; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.CultivatePositionSimpleVo; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseWithNameVo; +import jnpf.model.cultivate.v2.position.vo.V2CultivatePositionDetailForApp; + +import java.util.List; +import java.util.Map; + +public interface FtbCultivatePositionCourseService extends IService { + + /** + * 根据岗位学习ID列表统计课程数量 + * + * @param positionLearnIds 岗位学习ID列表 + * @return 课程数量 + */ + Map countByPositionLearnIds(List positionLearnIds); + + /** + * 根据岗位学习ID查询所有有效课程 + * + * @param postLearnId 岗位学习ID + * @param courseId 课程ID + * @return 课程列表 + */ + List listByPostLearnId(String postLearnId, String courseId); + + /** + * 根据岗位学习ID和职级ID查询所有有效课程 + * + * @param postLearnId 岗位学习ID + * @param gradeId 职级ID + * @param courseId 课程ID + * @return 课程列表 + */ + List listByPostLearnIdAndGradeId(String postLearnId, String gradeId, String courseId); + + /** + * 查询所有岗位学习 + * + * @param courseName 课程名称 + * @return 岗位学习列表 + */ + List queryAllPositionLearn(String courseName); + + /** + * 查询所有岗位学习课程列表 + * + * @param dto 请求参数 + * @return 岗位学习课程列表 + */ + List queryAllPositionLearnCourseLists(V2OtherCultivatePositionCourseForAppReq dto); + + List listByPostLearnIdAndExamId(String positionLearnId, String gradeId, String examId); + + List listByPostLearnIdAndPracticeId(String positionLearnId, String gradeId, String practiceId); + + List listByPostLearnIdAndIdentityId(String positionLearnId, String gradeId, String identityId); + + /** + * 根据岗位学习ID、年级ID查询培训岗位课程列表 + * + * @param positionLearnId 岗位学习ID + * @param gradeId 职级iD + * @return FtbCultivatePositionCourse对象列表 + */ + List queryAllPositionCourseAndLearnIdAndGradeId(String positionLearnId, String gradeId); + + List listPositionCourseList(FtbCultivatePosition existingPosition, V2CultivatePositionDetailForApp req); + + /** + * 根据岗位学习ID查询所有课程 + * + * @param positionLearnId 岗位学习ID + * @param ompulsory 是否必修,0必修,1选修 null-所有 + * @return 课程列表 + */ + List listAllCourseByPostLearnId(String positionLearnId,String postId, String gradeId, Integer ompulsory); + + List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId); + + /** + * 根据岗位学习ID查询所有有效课程(包含课程名称) + * + * @param postLearnId 岗位学习ID + * @param courseId 课程ID + * @return 课程列表(包含课程名称) + */ + List listByPostLearnIdWithName(String postLearnId, String courseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamCourseService.java new file mode 100644 index 0000000..40cbe95 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamCourseService.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.bo.FtbCultivateCourseBO; +import jnpf.model.cultivate.bo.FtbCultivatePositionExamBO; + +import java.util.List; + +public interface FtbCultivatePositionExamCourseService { + + /** + * 已考试卷,内部调用 + * + * @param examId 考试id + * @param relationPositonId 岗位学习id + * @param examSource 考试来源,1课程考试,2岗位学习考试 + * @param businessId 业务id,1、岗位学习中课程关联考试表中id,2、岗位学习 关联考试表的ID + * @return {@link FtbCultivatePositionExamBO} + */ + FtbCultivatePositionExamBO testPaperCall(String examId, String relationPositonId, String businessId, Integer examSource); + + + /** + * 获取课程名称,内部调用 + * + * @param courseIds 课程主键id + * @return {@link List}<{@link FtbCultivateCourseBO}> + */ + List getCourseName(List courseIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamIdentifyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamIdentifyService.java new file mode 100644 index 0000000..940ff03 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamIdentifyService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; + +public interface FtbCultivatePositionExamIdentifyService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamService.java new file mode 100644 index 0000000..acc610a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionExamService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; + +public interface FtbCultivatePositionExamService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionForAppService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionForAppService.java new file mode 100644 index 0000000..e520830 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionForAppService.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.app.FtbCultivatePositionForAppNewDTO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.position.app.FtbPopUpPromptVO; +import jnpf.model.cultivate.vo.position.app.FtbSubordinateLearningCoursesVO; +import jnpf.model.cultivate.vo.position.app.OnTheJobLearningCourseVO; + +/** + * @Title:FtbCultivatePositionForAppService + * @Author:peng.hao + * @create: 2023/12/2717:35 + */ +public interface FtbCultivatePositionForAppService { + /** + * 查询app岗位学习本岗课程 + * + * @param dto 本岗课程参数 + * @return 本岗课程信息 + */ + OnTheJobLearningCourseVO onTheJobLearningCourse(FtbCultivatePositionForAppNewDTO dto); + + /** + * 花名册查看学习课程 + */ + Page subordinateLearningCourses(FtbCultivatePositionForAppNewDTO dto, Page result); + + /** + * 花名册查看鉴定信息 + */ + PageListVO queryIdentityTrainDataDetail(FtbCultivatePositionForAppNewDTO dto, CultivatePage page); + + /** + * 岗位学习-下属实操鉴定 + */ + PageListVO subordinatePracticalAppraisal(FtbCultivatePositionForAppNewDTO dto, + CultivatePage result); + + /** + * 岗位学习-下属学习课程 + */ + Page subordinateLearningCoursess(FtbCultivatePositionForAppNewDTO dto, Page result); + + /** + * 弹出提示 + * @param userId 用户id + * @param positionId 岗位id + * @return 弹出提示信息 + */ + FtbPopUpPromptVO popUpPrompt(String userId, String positionId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionMemberService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionMemberService.java new file mode 100644 index 0000000..2730cf6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionMemberService.java @@ -0,0 +1,52 @@ +package jnpf.cultivate.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.resp.AppExamListVo; +import jnpf.model.cultivate.vo.identify.UserIdentifyPageVo; +import jnpf.model.cultivate.vo.position.FtbCultivateCourseListVO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionUserInfoVo; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; + +/** + * @Title:FtbCultivatePositionMemberService + * @Author:peng.hao + * @create: 2023/12/2819:23 + */ +public interface FtbCultivatePositionMemberService { + /** + * 根据关键词和分页信息查询用户列表 + * + * @param dto + * @return 返回包含用户信息的分页列表 + */ + PageListVO listUser(QueryPageUserDTO dto); + + /** + * 根据用户ID和分页信息查询课程列表 + * + * @param userId 用户ID,用于识别特定用户 + * @param page 分页信息,包含页码和每页数量等 + * @return 返回包含课程信息的分页列表 + */ + PageListVO listCourse(String userId, CultivatePage page); + + /** + * 根据用户ID和分页信息查询考试列表 + * + * @param userId 用户ID,用于识别特定用户 + * @param page 分页信息,包含页码和每页数量等 + * @return 返回包含考试信息的分页列表 + */ + PageInfo listExam(String userId, CultivatePage page); + + /** + * 根据用户ID和分页信息查询认证列表 + * + * @param userId 用户ID,用于识别特定用户 + * @param page 分页信息,包含页码和每页数量等 + * @return 返回包含认证信息的分页列表 + */ + PageListVO listIdentify(String userId, CultivatePage page); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionService.java new file mode 100644 index 0000000..0f4b82a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionService.java @@ -0,0 +1,196 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.ActionResult; +import jnpf.model.cultivate.dto.position.*; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionJobLearnCourseCertificateDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionJobLearnCourseStateSwitchDTO; +import jnpf.model.cultivate.dto.position.web.FtbJobLearnCourseRetakeCertificateDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.vo.position.*; + +import java.util.List; + +public interface FtbCultivatePositionService extends IService { + /** + * 添加岗位学习 + * + * @param ftbCultivatePositionJobLearnDTO 岗位学习参数 + * @return 岗位学习主键id + */ + String addJobLearning(FtbCultivatePositionJobLearnDTO ftbCultivatePositionJobLearnDTO); + + /** + * 岗位学习上下架 + * @param ftbCultivateShelvesDTO 岗位学习上下架参数 + */ + void upperLower(FtbCultivatePositionShelvesDTO ftbCultivateShelvesDTO); + + /** + * 添加岗位学习考试 + * @param ftbCultivatePositionJobLearnExamDTO 考试参数 + */ + void addJobLearningExam(FtbCultivatePositionJobLearnExamDTO ftbCultivatePositionJobLearnExamDTO); + + /** + * 添加岗位学习实操鉴定 + * @param ftbCultivatePositionLearnPracticalDTO 实操鉴定参数 + */ + void addPostLearningPracticalAppraisal(FtbCultivatePositionLearnPracticalDTO ftbCultivatePositionLearnPracticalDTO); + + /** + * 添加岗位学习课程 + * @param ftbCultivatePositionLearnPracticalDTO 岗位学习课程参数 + */ + void addJobLearningCourses(FtbCultivatePositionLearingCourselDTO ftbCultivatePositionLearnPracticalDTO); + + /** + * 添加岗位学习课程时,课程列表 + * @param ftbCultivatePositionCoursePageDTO 课程列表搜索参数 + * @return 课程列表 + */ + List jobLearningCourseList(FtbCultivatePositionCoursePageDTO ftbCultivatePositionCoursePageDTO); + + /** + * 岗位学习添加课程考试 + * @param ftbStudyCourseExamAddDTO 课程考试参数 + */ + void studyCourseExamAdded(FtbStudyCourseExamAddDTO ftbStudyCourseExamAddDTO); + + /** + * 岗位学习中添加课程鉴定 + * @param ftbStudyCourseIdentityAddDTO 课程鉴定参数 + */ + void studyCourseIdentityAdded(FtbStudyCourseIdentityAddDTO ftbStudyCourseIdentityAddDTO); + + /** + * 岗位学习中课程列表-课程考试删除 + * @param courseExamId 考试id + */ + void courseExamDeletion(String courseExamId); + + /** + * 岗位学习中课程列表-课程鉴定删除 + * @param courseIdentityId 鉴定id + */ + void courseIdentityDeletion(String courseIdentityId); + + /** + * 岗位学习课程列表-重选鉴定 + * @param ftbReselectionAppraisalDTO 重选鉴定参数 + */ + void reselectionAppraisal(FtbReselectionAppraisalDTO ftbReselectionAppraisalDTO); + + /** + * 岗位学习课程列表-重选考试 + * @param ftbRetakeExamDTO 重选考试参数 + */ + void retakeExam(FtbRetakeExamDTO ftbRetakeExamDTO); + + /** + * 岗位学习中课程列表-删除岗位学习课程 + * @param id 岗位学习课程主键id + */ + void deletionOfOnTheJobLearningCourses(String id); + + /** + * 岗位学习中课程列表页 + * @return 课程列表 + */ + Page jobLevelCourseList(Page page, FtbCultivatePositionCourseLevelPageDTO ftbCultivatePositionCourseLevelPageDTO); + + /** + * 岗位学习课程列表下拉选项 + */ + FtbPositionCourseDropDownListVO courseDropDownList(FtbCourseDropDownListDTO ftbCourseDropDownListDTO); + + /** + * 岗位学习管理-岗位学习分页列表 + * @param page 分页参数 + * @param ftbJobLearningPaginDTO 搜索参数 + * @return 岗位学习分页列表 + */ + Page jobLearningPaginatedList(Page page, FtbJobLearningPaginDTO ftbJobLearningPaginDTO); + + /** + * 岗位学习管理-删除岗位学习 + * @param postLearnId 岗位学习主键id + */ + ActionResult deletionOfJobLearning(String postLearnId); + + /** + * 岗位学习配置总览 + * @param postLearnId 岗位学习主键ID + * @param postId 岗位主键ID + */ + FtbJobLearningConfigurationVO overviewOfJobLearningConfiguration(String postLearnId, String postId); + + /** + * 岗位学习,获取实操鉴定和考试及证书 + * @param postLearnId 岗位学习ID + * @param postIdID 岗位ID + * @return + */ + FtbJobLearningExamAppraisalVO jobLearningExamAppraisal(String postLearnId, String postIdID); + + /** + * 岗位学习-考试删除 + * @param postLearnId 岗位学习主键ID + * @param postRankId 岗位ID + */ + void postStudyExamDeleted(String postLearnId, String postRankId); + + /** + * 岗位学习-实操鉴定删除 + * @param postLearnId 岗位学习主键ID + * @param postRankId 岗位ID + */ + void jobLearningPracticalAppraisalDelete(String postLearnId, String postRankId); + + /** + * 成员管理模块-删除该员工绑定职等对应的岗位学习 + * @param id 岗位id + * @param userId 用户id + */ + void deleteMemberGrade(String id, String userId); + + /** + * 岗位学习-证书删除 + * @param postLearnId 岗位学习主键ID + * @param postRankId 岗位ID + */ + void certificateDeletion(String postLearnId, String postRankId); + + /** + * 添加岗位学习证书 + * @param certificateDTO 岗位学习证书参数 + */ + void onTheJobLearningCertificate(FtbCultivatePositionJobLearnCertificateDTO certificateDTO); + + /** + * 添加岗位学习课程证书 + * @param certificateDTO 课程证书参数 + */ + void addJobLearningCourseCertificate(FtbCultivatePositionJobLearnCourseCertificateDTO certificateDTO); + + /** + * 岗位学习-课程证书删除 + * @param postLearnId 岗位学习主键ID + * @param courseId 课程ID + */ + void certificateCourseDeletion(String postLearnId, String courseId); + + /** + * 更改课程选修和必修切换 + * @param stateSwitchDTO 课程选修和必修切换参数 + */ + void changeCourseElectiveAndRequiredSwitching(FtbCultivatePositionJobLearnCourseStateSwitchDTO stateSwitchDTO); + + /** + * 重选课程证书 + * @param certificateDTO 重选课程证书参数 + */ + void courseRetakeCertificate(FtbJobLearnCourseRetakeCertificateDTO certificateDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionStatisticesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionStatisticesService.java new file mode 100644 index 0000000..344c955 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionStatisticesService.java @@ -0,0 +1,52 @@ +package jnpf.cultivate.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.dto.position.FtbCultivatePersonStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionPersonStatisticesDTO; +import jnpf.model.cultivate.vo.position.*; + +import java.math.BigDecimal; +import java.util.List; + +public interface FtbCultivatePositionStatisticesService { + /** + * 组织维度统计 + */ + PageListVO organizationListStatistics(FtbCultivatePositionOrgStatisticesDTO statisticDTO); + + /** + * 个人维度统计 + */ + PageListVO personListStatistics(FtbCultivatePositionPersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO); + + /** + * 岗位维度统计 + */ + PageListVO positionListStatistics(FtbCultivatePersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO); + + /** + * 组织维度-课程详情 + */ + List organizeCourseDetails(String startDate, String endDate, String orgId); + + /** + * 个人维度-课程详情 + */ + List personalDimensionCourseDetails(String startDate, String endDate, String userId); + + /** + * 岗位学习进度 + * + * @param postId 岗位Id + * @param userId 用户ID + * @return {@link BigDecimal} + */ + BigDecimal jobLearningProgress(String postId, String userId); + + /** + * 岗位维度-点击岗位名称数据统计 + */ + JobTitleStatistics jobTitleStatistics(String postId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionUserService.java new file mode 100644 index 0000000..ac700c7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePositionUserService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionUser; + +public interface FtbCultivatePositionUserService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionMemberService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionMemberService.java new file mode 100644 index 0000000..c0455c7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionMemberService.java @@ -0,0 +1,68 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionMemberDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMember; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMeberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.userrelation.UserRelationPositionGrades; + +import java.util.List; +import java.util.Map; + +/** + * web晋升通道成员管理模块 service + */ +public interface FtbCultivatePromotionMemberService extends IService { + + /** + * 根据当前晋升通道id查询通道成员(包含未启用的通道成员,已启用的) + * @param id + * @return + */ + FtbCultivateMeberVO queryChannelMembers(String id); + + /** + * 根据用户id或者岗位id查询当前用户的晋升通道 + * + * @param userId + * @param postId + * @return + */ + List queryPromotionByUserOrPostId(String userId, String postId); + /** + * 将成员添加到晋升通道 + * + * @param dto 用户ID + */ + void addMembersToProChannel(FtbCultivatePromotionMemberDto dto); + /** + * 根据用户ID和岗位ID查询培养晋升信息 + * 该方法用于根据特定的用户和岗位查询相关的培养晋升详情 + * + * @param userId 用户ID + * @param postId 岗位ID + * @return 返回用户的培养晋升信息对象 + */ + FtbCultivatePromotionVO queryPromotionByUser(String userId, String postId); + + /** + * 批量删除会员到专业通道的关联信息 + * 该方法用于从专业通道中批量删除会员的关联信息 + * + * @param dto 包含会员ID的列表 + */ + void deleteMembersToProChannel(List dto); + + /** + * 删除组织和岗位等级的关系 + * 该方法用于删除特定组织和岗位等级之间的关系 + * + * @param organizeId 组织ID + * @param userRelationPositionGrades 用户和岗位等级的关系对象 + */ + void deleteRelation(String organizeId, UserRelationPositionGrades userRelationPositionGrades); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionNewService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionNewService.java new file mode 100644 index 0000000..b2fea03 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionNewService.java @@ -0,0 +1,233 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivateCreatMeMapInfoDTO; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionCreatNewDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNew; +import jnpf.model.cultivate.v2.position.vo.CultivateUserPositionVo; +import jnpf.model.cultivate.v2.promotion.req.FtbCultivatePromotionReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionOrgStatisticReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionPersonStatisticReq; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionOrgStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionPersonStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.WebCultivatePromotionListVo; +import jnpf.model.cultivate.vo.promotion.*; +import jnpf.permission.vo.v2.user.UserPageListVO; + +import java.util.List; + +/** + * @Title: FtbCultivatePromotionNewService + * @Author: peng.hao + * @create: 2024/3/27 11:37 + */ +public interface FtbCultivatePromotionNewService extends IService { + /** + * 获取促销列表 + * @param page 分页信息 + * @param dto 查询条件 + * @return 返回分页的促销列表 + */ + PageListVO getList(CultivatePage page, FtbCultivatePromotionDto dto); + + /** + * 添加促销信息 + * @param creatDto 添加促销的信息 + */ + void add(FtbCultivatePromotionCreatNewDto creatDto); + + /** + * 更新促销信息 + * @param creatDto 更新后的促销信息 + */ + void updatePromotion(FtbCultivatePromotionCreatNewDto creatDto); + + /** + * 根据ID删除促销信息 + * @param id 促销信息ID + * @param b 删除标志 + * @return 返回删除结果 + */ + String deleteById(String id, boolean b); + + /** + * 根据初始化岗位,查看学习地图 + * @param channelIniPoId 岗位ID + * @return 返回学习地图信息 + */ + FtbCultivatePromotionLevelMapVO getLearnMapLevels(String channelIniPoId); + + /** + * 根据初始化岗位,获取岗位个数 + * @param channelIniPoId 岗位ID + * @return 返回岗位个数 + */ + Integer getMapPostNumber(String channelIniPoId); + + /** + * 根据学习地图id,查看学习地图 + * @param mapId 学习地图ID + * @return 返回学习地图信息 + */ + FtbCultivatePromotionLevelMapVO getLearnMapLevelsWithMapId(String mapId); + + /** + * 根据ID获取当前学习地图绑定成员信息 + * @param id 地图ID + * @return 返回当前学习地图绑定成员信息 + */ + FtbCultivatePromotionStudyDeleteInfo getTheCurrentMapBindingMembers(String id); + + /** + * 根据ID和分页信息获取促销详情 + * @param id 促销信息ID + * @param page 分页信息 + * @return 返回促销详情 + */ + FtbCultivateStudyMemberInfo getPromotionById(String id, CultivatePage page); + + /** + * 查询当前用户学习地图等级 + * @param id 地图ID + * @param postId 岗位ID + * @return 返回当前用户学习地图等级 + */ + Integer queryTheCurrentUserLearningMapLevel(String id, String postId); + + /** + * 根据岗位查看学习地图信息 + * @param userId 用户ID + * @param postId 岗位ID + * @param level 地图等级 + * @return 返回学习地图信息 + */ + List viewLearningMapBasedOnPosition(String userId, String postId, Integer level); + + /** + * 获取学习地图详情 + * @param id 地图ID + * @return 返回学习地图详情 + */ + FtbCultivateStudyMemberVO getDetails(String id); + + /** + * 人员选择岗位 + * @param creatMeMapInfoDTO 人员选择岗位信息 + */ + void correspondingPersonnelSelectPositions(FtbCultivateCreatMeMapInfoDTO creatMeMapInfoDTO); + + /** + * 查询下属学习阶段 + * @param userId 上级用户ID + * @param postId 岗位ID + * @return 返回下属学习阶段信息 + */ + FtbCultivateUnderMemberInfo queryTheLearningStageOfSubordinates(String userId, String postId); + + /** + * 根据阶段选择岗位 + * @param userId 用户ID + * @param postId 岗位ID + * @param level 地图等级 + * @param compulsory 是否必修 + * @return 返回可选岗位信息 + */ + FtbCultivateNextCourseVO selectPositionsCorrespondingToTheStages(String userId, String postId, Integer level, Integer compulsory); + + /** + * 更改当前人学习阶段 + * @param userId 当前人 + * @param postId 完成岗位 + */ + void changeTheCurrentPersonSLearningStage(String userId,String postId); + + /** + * 检查学习状态 + * @param level 学习等级 + * @param userId 用户ID + * @param postId 岗位ID + * @return 返回检查结果 + */ + String checkStudying(Integer level, String userId, String postId); + /** + * 内部检查学习状态 + * @param level 学习等级 + * @param userId 用户ID + * @param postId 岗位ID + * @param promotionId 地图id + * @return 状态 + */ + String innerCheckStudying(Integer level, String userId, String postId,String promotionId); + + /** + * 判断岗位学习地图是否变更 + * @param userId 用户ID + * @param postId 岗位ID + * @return 返回是否变更 + */ + Boolean whetherTheJobLearningMapHasChanged(String userId, String postId); + + /** + * 查询培训数据 + * @param userId 用户ID + * @param postId 岗位ID + * @return 返回培训数据 + */ + FtbCultivatePromotionWithPersonelVO queryTrainData(String userId, String postId); + + /** + * 验证岗位是否重新学习地图 + * @param postId 岗位ID + * @return 返回是否需要重新学习 + */ + Boolean verifyingThePostWillRelearnTheMap(String postId); + + /** + * 学习进度 + * @param userId 用户ID + * @param postId 岗位ID + * @return 返回学习进度信息 + */ + FtbCultivatelearningProgressVO learningProgress(String userId, String postId); + /** + * 根据用户id和当前岗位id获取已选择的岗位 + * @param userId + * @param postId + * @return postIds + */ + List currentPersonChoosesAllPosition(String userId, String postId); + /** + * 根据初始岗位 查询学习地图岗位集合 + * @param postId + * @return postIds + */ + List queryByInitPostIdMapPostId( String postId); + + /** + * 根据岗位id查询已经选择的人员 + * @param postId + * @return userIds + */ + List querySelectedPersonnelBasedOnPositionID(List postId); + + String deleteByPromotionId(String id); + + Page webList(Page page, FtbCultivatePromotionReq dto); + + List queryMaxLevel(List promotionIds); + + Page personStatisticsV2(V2PromotionPersonStatisticReq req, List userIds, Page page); + + List queryPromotionPosition(List userIds); + + Page mapStatisticPageV2(Page page, V2PromotionOrgStatisticReq req, List userIds); + + List userCountForPromotion(List userIds); + + List queryPositionNumForMap(List promotionIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostApplyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostApplyService.java new file mode 100644 index 0000000..45d32ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostApplyService.java @@ -0,0 +1,58 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyCreateDto; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyDto; +import jnpf.model.cultivate.po.apply.FtbCultivatePromotionPostApply; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyVO; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyWithPerVO; + +public interface FtbCultivatePromotionPostApplyService extends IService { + + /** + * 初始化 + * + * @param createDto 申请的创建数据对象 + */ + void initiateAPromotionApplication(FtbCultivatePromotionPostApplyCreateDto createDto); + + /** + * 获取列表 + * + * @param dto 请求参数 + * @param page 分页信息 + * @return 列表对象 + */ + PageListVO getList(FtbCultivatePromotionPostApplyDto dto, CultivatePage page); + + /** + * 审核岗位申请的接口 + * @param dto 岗位申请数据传输对象 + * @return 审核结果 + */ + void auditPromotionPostApplication(FtbCultivatePromotionPostApplyDto dto); + /** + * 查询岗位申请详情 + * + * @param dto 岗位申请数据传输对象 + * @param appFlag + * @return ActionResult 响应结果对象,包含岗位申请及相关信息 + */ + FtbCultivatePromotionPostApplyWithPerVO viewPromotionApplications(FtbCultivatePromotionPostApplyDto dto, String appFlag); + /** + * 检查是否为指定岗位层级的用户提供了应用 + * + * 此方法用于确定是否存在已经为特定岗位层级(PostLevel)的用户配置的应用程序 + * 它通过检查岗位ID、岗位层级ID和用户ID来判断是否存在对应的应用程序 + * + * @param postId 岗位ID,标识特定的岗位 + * @param grandId 岗位层级ID,标识特定的岗位层级 + * @param userId 用户ID,标识特定的用户 + * @return 如果为该岗位层级的用户提供了应用,则返回true;否则返回false + */ + Boolean isThereAnAppForThePosLevel(String postId, String grandId, String userId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostService.java new file mode 100644 index 0000000..998ad6c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionPostService.java @@ -0,0 +1,84 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionForAppDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPost; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionForAppVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionPostVO; + +import java.util.List; + +public interface FtbCultivatePromotionPostService extends IService { + + /** + * 新增晋升通道级别 + * + * @param creatDto 职位信息的DTO对象 + */ + void savePost(FtbCultivatePromotionPost creatDto); + + /** + * 获取指定晋升通道id对应的列表 + * + * @param promotionId 推广id + * @return 职位列表 + */ + List getList(String promotionId); + + + /** + * 更新职位信息 + * + * @param dto + */ + void updatePost(FtbCultivatePromotionPost dto); + + + /** + * 根据用户ID和帖子ID查询已发起的晋升通道 + * + * @param userId 用户ID + * @param postId 岗位ID + * @return FtbCultivatePromotionForAppVO + */ + FtbCultivatePromotionForAppVO queryThePromotedChannelsThatAreInitiated(String userId, String postId); + + /** + * 修改应用推广状态 + * + * @param forAppDto + */ + String changeTheEnablementStatus(FtbCultivatePromotionForAppDto forAppDto); + + + /** + * 修改是否允许更改 + * + * @param forAppDto + */ + void changeWhetherTheChangeIsAllowed(FtbCultivatePromotionForAppDto forAppDto); + + + /** + * 检查晋升通道是否可用 + * @param userId 用户ID + * @param postId 岗位ID + * @return 是否可用,true为可用,false为不可用 + */ + boolean checkWhetherThePromotionChannelIsEnabled(String userId, String postId); + /** + * 判断用户是否被允许访问 + * + * @param userId 用户ID + * @return 返回值为Integer类型,可能的值为1或0,1表示用户被允许访问,0表示用户不允许访问 + */ + Integer userIsAllowed(String userId); + + /** + * 查询已存在的推广帖子 + * + * @return 返回一个FtbCultivatePromotionPostVO类型的列表,包含所有已存在的推广帖子信息 + */ + List queryPostHasExist(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionService.java new file mode 100644 index 0000000..7d6e123 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivatePromotionService.java @@ -0,0 +1,107 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionCreatDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.dto.promotion.FtbMapsOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.promotion.FtbMapsPersonWisdomStatisticDTO; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotion; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMapsOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMapsPersonWisdomStatisticVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionMemberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; + +import java.util.List; + +public interface FtbCultivatePromotionService extends IService { + + + /** + * 获取晋升通道管理列表 + * + * @param page + * @param dto + * @return + */ + PageListVO getList(CultivatePage page, FtbCultivatePromotionDto dto); + + + /** + * 根据id进行查询 + * @param promotionIds + * @return + */ + List queryListByIds(List promotionIds); + + /** + * + * 新增晋升通道 + * @param creatDto + * @return + */ + void add(FtbCultivatePromotionCreatDto creatDto); + + /** + * 修改晋升通道 + * @param creatDto + */ + void updatePromotion(FtbCultivatePromotionCreatDto creatDto); + + /** + * 删除晋升通道 + * + * @param id + * @param flag + * @return + */ + boolean deleteById(String id, boolean flag); + + /** + * 删除是需要进行成员列表获取 + * 获取启用该晋升通道的成员列表 + * + * @param dto + * @param page + * @return + */ + PageListVO getPromotionMbeList(FtbCultivatePromotionDto dto, CultivatePage page); + /** + * 添加晋升创意列表 + * + * @param creatDto 晋升创意数据传输对象列表 + */ + void addList(List creatDto); + + /** + * 执行职位删除前的验证 + * + * @param postRankId 待删除的职位ID列表 + */ + void postLevelDeletionVerification(List postRankId); + + /** + * 验证晋升操作的合法性 + * + * @param postId 岗位ID + * @param gradeId 职级ID + */ + void VerifyPromotion(String postId, String gradeId); + + /** + * 组织列表统计 + * + * @param statisticDTO 统计参数数据传输对象 + * @return 返回组织智慧统计信息的分页列表 + */ + PageListVO organizationListStatistics(FtbMapsOrgWisdomStatisticDTO statisticDTO); + + /** + * 人员列表统计 + * + * @param personWisdomStatisticDTO 人员智慧统计参数数据传输对象 + * @return 返回人员智慧统计信息的分页列表 + */ + PageListVO personListStatistics(FtbMapsPersonWisdomStatisticDTO personWisdomStatisticDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankCourseService.java new file mode 100644 index 0000000..14947d0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankCourseService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBankCourse; + +public interface FtbCultivateQuestionBankCourseService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankService.java new file mode 100644 index 0000000..4d75d11 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionBankService.java @@ -0,0 +1,79 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.req.questionbank.AddQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.QueryQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.UnbindQuestionBankReq; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.resp.QuestionBankVo; + +import java.util.List; +import java.util.Map; + +public interface FtbCultivateQuestionBankService extends IService { + + /** + * 分页查询题库列表 + * @param req + * @return + */ + PageInfo getPageList(QueryQuestionBankReq req); + + /** + * 查询题目详情 + * @param id 题库ID + * @return + */ + QuestionBankVo getInfo(String id); + + /** + * 插入题库 + * @param addQuestionBank + */ + void insertData(AddQuestionBankReq addQuestionBank); + + /** + * 修改题库 + * @param req + */ + void updateData(EditQuestionBankReq req); + + /** + * 删除题库 + * @param id 题库ID + */ + void deleteData(String id); + + /** + * 解除题库和课程的绑定 + * @param req + */ + void unbind(UnbindQuestionBankReq req); + + + /** + * 检测题库是否可以删除 + * + * @param questionBankId 题库ID + * @return + */ + CanDeleteMsg checkQuestionBankCanDelete(String questionBankId); + + /** + * 统计分析题库中各种题目类型的数量 + * @param questionBankId 题库ID + * @return + */ + Map analysQuestionCount(String questionBankId); + + /** + * 删除题库和课程的关联关系 + * + * @param courseIds 课程ID集合 + */ + void deleteQuestionBankRelationCourse(List courseIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionOptionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionOptionService.java new file mode 100644 index 0000000..4073230 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionOptionService.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; + +import java.util.List; + +public interface FtbCultivateQuestionOptionService extends IService { + + /** + * 根据题目id查询题目的选项列表 + * @param questionIds 题目id集合 + * @return + */ + List queryOptionListByQuestionIds(List questionIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionPointsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionPointsService.java new file mode 100644 index 0000000..2a256e1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionPointsService.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionPoints; + +public interface FtbCultivateQuestionPointsService extends IService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionService.java new file mode 100644 index 0000000..9756b86 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateQuestionService.java @@ -0,0 +1,130 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.req.questionbank.AddQuestionReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionReq; +import jnpf.model.cultivate.req.questionbank.QueryQuestionReq; +import jnpf.model.cultivate.resp.*; + +import java.util.List; +/** + * 题目业务接口 + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbCultivateQuestionService extends IService { + + /** + * 分页列出题库中题目列表【根据题库ID】 + * + * @param req + * @return + */ + PageInfo getPageList(QueryQuestionReq req); + + /** + * 查询题目信息 + * + * @param questionId 题目ID + * @return + */ + QuestionVo getInfo(String questionId); + + /** + * 查询题目信息,包含选项 + * @param questionId 题目ID + * @return + */ + QuestionVo getQuestionAndOption(String questionId); + + /** + * 新增题目 + * + * @param questionBankId 题库ID + * @param req 题目信息 + */ + void insertData(String questionBankId, AddQuestionReq req); + + /** + * 修改题目 + * + * @param req + */ + void updateData(EditQuestionReq req); + + /** + * 删除题目 + * @param id 题目ID + */ + void deleteData(String id); + + /** + * 检测题库是否可以被删除 + * + * @param questionBankId 题库ID + * @return + */ + CanDeleteMsg checkQuestionBankCanDelete(String questionBankId); + + /** + * 检测题目是否可以删除 + * + * @param id 题目ID + * @return + */ + QuestionCanDeleteMsg checkQuestionCanDelete(String id); + + /** + * 导入题目 + * + * @param questionBankId 题库ID + * @param allQuestionList 题目列表 + */ + ImportQuestionResultVo importData(String questionBankId, List allQuestionList); + + /** + * 查询重复题目 + * @param req + * @return + */ + PageInfo queryRepeatQuestion(QueryQuestionReq req); + + /** + * 批量检测题目是否可以删除 + * @param ids 题目id集合 + * @return + */ + QuestionCanDeleteMsg batchCheckCanDel(List ids); + + /** + * 批量删除题目 + * @param ids 题目id集合 + */ + void batchDel(List ids); + + /** + * 根据题库id查询所有题目列表 + * @param questionBankId 题库ID + * @return + */ + List queryAllQuestionByQuestionBankId(String questionBankId); + + /** + * 给题库中导入题目 + * @param questionBankId 题目ID + * @param normal 题目数据 + * @return + */ + ImportQuestionResultVo realImportData(String questionBankId, List normal); + + /** + * 根据题库id统计各题库的题目数量 + * @param ids 题库id集合 + * @return + */ + List countForClassifyId(List ids); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateRuleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateRuleService.java new file mode 100644 index 0000000..9e55a86 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateRuleService.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.FtbCultivateRule; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:15:19 +* +*/ +public interface FtbCultivateRuleService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStatisticsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStatisticsService.java new file mode 100644 index 0000000..2ce19a8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStatisticsService.java @@ -0,0 +1,54 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.vo.statistics.FtbCultivateStatisticsVO; +import jnpf.model.cultivate.vo.statistics.NumberOfTrainingSessions; +import jnpf.model.cultivate.vo.statistics.NumberofAppSessions; + +import java.util.List; + +public interface FtbCultivateStatisticsService { + /** + * 获取培训课程的统计数据 + * + * @param dto 包含统计条件的DTO对象 + * @return 返回一个NumberOfTrainingSessions对象,包含培训课程的数量统计信息 + */ + NumberOfTrainingSessions getNumberOfTraineesStatistics(FtbCultivateStatisticsDTO dto); + + /** + * 获取热门课程列表 + * + * @param dto 包含统计条件的DTO对象 + * @return 返回一个包含受欢迎课程的List,每个课程的信息封装在FtbCultivateStatisticsVO.PopularCourses对象中 + */ + List getPopularCourses(FtbCultivateStatisticsDTO dto); + + /** + * 获取累积学习者数量的折线图数据 + * + * @param dto 包含统计条件的DTO对象 + * @return 返回一个List,包含FtbCultivateStatisticsVO.LineChartCumulativeLearners对象,每个对象包含折线图的一个数据点 + */ + List numberOfStudentsLineGraph(FtbCultivateStatisticsDTO dto); + + /** + * 获取排名靠前的成员列表 + * + * @param dto 包含统计条件的DTO对象 + * @param rankingType 排名类型,可能影响排名的逻辑 + * @return 返回一个List,包含FtbCultivateStatisticsVO.TopMembers对象,每个对象代表一个排名靠前的成员 + */ + List getTopMembers(FtbCultivateStatisticsDTO dto, Integer rankingType); + + NumberofAppSessions trainingStatisticsV2(FtbCultivateStatisticsDTO dto); + + /** + * 获取培训课程的会话统计数据 + * + * @param dto 包含统计条件的DTO对象 + * @return 返回一个List,包含NumberofAppSessions对象,每个对象代表一个培训课程的会话统计数据 + */ + List trainingStatistics(FtbCultivateStatisticsDTO dto); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStoreStatisticService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStoreStatisticService.java new file mode 100644 index 0000000..3334fe8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateStoreStatisticService.java @@ -0,0 +1,64 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.dto.storestatistics.*; +import jnpf.model.cultivate.vo.position.FtbCultivateStoreCountVO; +import jnpf.model.cultivate.vo.position.FtbCultivateWorkerCountVO; +import jnpf.model.cultivate.vo.position.FtbPersonTrainingStatisticsVO; +import jnpf.model.cultivate.vo.position.FtbStoreManagerTrainingStatisticsVO; +import jnpf.model.thousandsfaces.TodayWorkVo; + +import java.util.List; + +public interface FtbCultivateStoreStatisticService { + + + FtbCultivateStoreCountVO getCultivateCount(FtbCultivateStoreStatisticsReq req); + + /** + * 我的考试列表 + * + * @param req + * @return + */ + List queryMyExamList(StoreStatisticsMyExamReq req); + + /** + * 待我批阅 + * + * @param req + * @return + */ + List queryWaitMyReadOver(StoreStatisticsWaitMyCheckExamReq req); + + List queryOfflineTrainList(StoreOfflineTrainReq req); + + List storeMyTaskList(StoreOfflineTrainReq req); + + /** + * 员工界面-鉴定他人列表 + * @param req + * @return + */ + List storeMyIdentityList(StoreIdentityReq req); + + /** + * 员工界面统计 + * @param req + * @return + */ + FtbCultivateWorkerCountVO getWorkerCultivateCount(FtbCultivateStoreStatisticsReq req); + + /** + * 工作台-员工界面统计 + * @param req 统计参数 + * @return 统计结果 + */ + FtbPersonTrainingStatisticsVO personTrainingStatistics(FtbCultivateStoreStatisticsReq req); + + /** + * 工作台-店长界面统计 + * @param req 统计参数 + * @return 统计结果 + */ + FtbStoreManagerTrainingStatisticsVO storeManagerTrainingStatistics(FtbCultivateStoreStatisticsReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperQuestionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperQuestionService.java new file mode 100644 index 0000000..1d6e752 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperQuestionService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; + +public interface FtbCultivateTestPaperQuestionService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperRuleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperRuleService.java new file mode 100644 index 0000000..9331eaa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperRuleService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperRule; + +public interface FtbCultivateTestPaperRuleService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperService.java new file mode 100644 index 0000000..042f4ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/FtbCultivateTestPaperService.java @@ -0,0 +1,146 @@ +package jnpf.cultivate.service; + + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.req.paper.QueryPaperReq; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.cultivate.resp.*; + +import java.util.List; +import java.util.Map; +/** + * 试卷表服务层 + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbCultivateTestPaperService extends IService { + + /** + * 获取试卷列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryPaperReq req); + + /** + * 获取试卷详情 + * + * @param paperId 试卷ID + * @return + */ + PaperDetailVo getInfo(String paperId); + + /** + * 新增试卷 + * + * @param req + */ + void insertData(SavePaperReq req); + + /** + * 修改试卷 + * + * @param req + */ + void updateData(SavePaperReq req); + + /** + * 修改试卷和题目 + * + * @param req + */ + void updatePaperAndQuestion(SavePaperReq req); + + /** + * 删除试卷 + * + * @param paperId 试卷ID + */ + void deleteData(String paperId); + + /** + * 检查试卷是否可以删除 + * + * @param paperId 试卷ID + * @return + */ + CanDeleteMsg checkPaperCanDelete(String paperId); + + /** + * 试卷状态切换,启用/禁用 + * + * @param paperId 试卷ID + * @param status 1启用 0禁用 + */ + void switchEnabledMark(String paperId, Integer status); + + /** + * 获取试卷信息和试题列表 + * + * @param paperId 试卷ID + * @return + */ + PaperDetailVo queryPaperInfoAndQuestionList(String paperId); + + /** + * 根据试卷id查询 试卷中试题列表 + * + * @param paperId 试卷ID + * @return + */ + List queryQuestionListForPaperId(String paperId); + + /** + * 根据试卷id查询 试卷中试题列表,题目ID和分数 + * + * @param paperId 试卷ID + * @return + */ + List queryPaperQuestionRelationList(String paperId); + + /** + * 设置正式试卷 + * + * @param id + */ + void setFormalPaper(String id); + /** + * 从试卷中删除题目 + * @param paperId 试卷id + * @param questionId 题目id + */ + void deleteQuestionFormPaper(String paperId, String questionId); + + /** + * 批量删除题目 从试卷中 + * @param paperId 试卷ID + * @param questionIds 题目集合 + */ + void batchDeleteQuestionFormPaper(String paperId, List questionIds); + /** + * 查询试卷中配置的题目 + * @param paperId 试卷id + * @return 题目列表 + */ + List queryConfigQuestionList(String paperId); + /** + * 根据题库ID批量查询题目 + * + * @param questionBankIdList 题库ID列表 + * @return 题库id->题目列表 + */ + Map> BatchQueryQuestionForQuestionBankIds(List questionBankIdList) ; + + /** + * 填充题目选项 + * @param questionVoList 题目列表 + */ + void fillQuestionOption(List questionVoList) ; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/PositionCultivateIdentifyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/PositionCultivateIdentifyService.java new file mode 100644 index 0000000..a987ea1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/PositionCultivateIdentifyService.java @@ -0,0 +1,28 @@ +package jnpf.cultivate.service; + +import jnpf.model.cultivate.vo.identify.PostAndCourseCorrelationVo; + +import java.util.List; + +/** + * 仅供实操鉴定调用岗位学习模块服务 + * + * @author fantaibao + * @date 2023/12/29 + */ +public interface PositionCultivateIdentifyService { + /** + * 查询岗位、课程关联的鉴定表集合 + * + * @param tableId 鉴定表ID + */ + List selectCorrelationDataList(String tableId); + + /** + * 批量删除岗位学习鉴定申请记录 + * + * @param sourceIds + */ + void batchDeleteCorrelationRecord(List sourceIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordPracticeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordPracticeService.java new file mode 100644 index 0000000..988393e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordPracticeService.java @@ -0,0 +1,141 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.model.cultivate.dto.teaching.*; +import jnpf.model.cultivate.vo.teaching.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +public interface TeachingRecordPracticeService extends IService { + /** + * Web-练习门店下拉 + * + * @return 练习门店集合 + */ + List getStoreList(); + + /** + * 汇总-分页列表 + * + * @param pageDto 查询参数 + * @return 汇总数据集合 + */ + PageInfo summaryPageList(TeachingBaseFilter pageDto); + + /** + * 汇总导出 + * @param response 响应 + * @param pageDto 查询参数 + */ + void summaryExport(HttpServletResponse response, TeachingBaseFilter pageDto) throws Exception; + + /** + * 记录-分页列表 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + PageInfo recordPageList(TeachingBaseFilter pageDto); + + /** + * 记录导出 + * @param response 响应 + * @param pageDto 查询参数 + */ + void recordExport(HttpServletResponse response, TeachingBaseFilter pageDto) throws Exception; + + /** + * 记录-批量删除 + * @param deleteDto 删除参数 + * @return 是否成功 + */ + Boolean recordBatchDelete(RecordBatchDeleteDto deleteDto); + + /** + * App我的练习-分页列表 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + PageInfo myRecordPageList(MyRecordPageListDto pageDto); + + /** + * App我的练习-新增 + * + * @param saveDto 保存参数 + * @return 是否成功 + */ + Boolean mySave(RecordSaveDto saveDto) throws Exception; + + /** + * App我的练习-详情 + * + * @param id 数据id + * @return 练习详情 + */ + RecordInfoVo myInfo(String id); + + /** + * App我的练习-修改 + * + * @param updateDto 修改参数 + * @return 是否成功 + */ + Boolean myUpdate(RecordUpdateDto updateDto); + + /** + * App我的练习-删除 + * + * @param id 数据id + * @return 是否成功 + */ + Boolean myDelete(String id); + + /** + * App我的练习-查看数据 + * + * @param viewDataDto 查询条件 + * @return 技能点信息集合 + */ + ViewDataVo myViewData(ViewDataDto viewDataDto); + + /** + * App员工练习-分页列表 + * + * @param pageDto 查询参数 + * @return 练习集合 + */ + PageInfo employeePageList(EmployeePageListDto pageDto); + + /** + * App员工练习-查看数据 + * + * @param viewDataDto 查询参数 + * @return 练习集合 + */ + List employeeViewData(ViewDataDto viewDataDto); + + /** + * App店长界面-今日练习汇总数据 + * + * @param storeId 查询参数 + * @return 练习汇总数据 + */ + TodaySummaryDataVo getTodaySummary(String storeId); + + /** + * 生成带教/练习的模拟数据 + * @return 是否成功 + */ + Boolean createSimulatedData(String tenantId); + + /** + * 获取我的练习汇总数据 + * @param storeId 店铺id + * @return 我的练习汇总数据 + */ + MyPracticeSummaryVo getMyPracticeSummary(String storeId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordService.java new file mode 100644 index 0000000..10c8219 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingRecordService.java @@ -0,0 +1,219 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.model.cultivate.dto.teaching.QueryTeachingRecordDto; +import jnpf.model.cultivate.dto.teaching.RecordQueryDto; +import jnpf.model.cultivate.dto.teaching.TeachingSaveDto; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.teaching.CultivateFile; +import jnpf.model.cultivate.v2.teaching.model.V2Attachment; +import jnpf.model.cultivate.v2.teaching.vo.StoreTeachingVo; +import jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo; +import jnpf.model.cultivate.v2.teaching.vo.V2TeachingRecordVo; +import jnpf.model.cultivate.vo.teaching.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface TeachingRecordService extends IService { + /** + * 选择学员-下拉列表(当前用户的主岗以及下级成员) + * @param storeId 门店ID + * @return PositionUserVo 列表 + */ + List getPostUserList(String storeId); + + /** + * 我的带教-分页查询 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return TeachingRecordVo 分页结果 + */ + PageInfo page(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 新增带教记录 + * @param teachingSaveDto 带教保存数据传输对象 + */ + void add(TeachingSaveDto teachingSaveDto); + + /** + * 更新带教记录 + * @param teachingSaveDto 带教更新数据传输对象 + */ + void update(TeachingSaveDto teachingSaveDto); + + /** + * 更新附件信息 + * @param bizId 业务id + * @param fileList 文件列表 + * @param fileType 文件类型 + */ + void updateFileInfo(String bizId, List fileList, FileEventDTO.FileType fileType); + + /** + * 转换文件list + * @param recordId 记录id + * @param fileList 文件列表 + * @param userId 用户id + * @param fileType 文件类型 + * @return java.util.List + */ + List changeToFileList(String recordId, List fileList, String userId, FileEventDTO.FileType fileType); + + /** + * 我的带教-删除指定ID的带教记录 + * @param id 带教记录ID + */ + void delete(String id); + + /** + * 上级带教-查询我被带教的记录列表(分页) + * @param queryTeachingRecordDto 查询条件封装对象 + * @return TeachingRecordVo 分页结果 + */ + PageInfo queryMyTeachingRecordList(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 我的带教-查看数据-统计学员信息(分页) + * @param queryTeachingRecordDto 查询条件封装对象 + * @return PageInfo 学员统计数据分页结果 + */ + PageInfo teachingStudentDataCount(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 上级带教-查看我的带教数据 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return List 技能统计列表 + */ + List queryMyTeachingData(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 带教员下拉列表 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return List 带教员列表 + */ + List teachingSelectList(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 我的带教-带教员下拉列表-上级带教 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return List 上级带教员列表 + */ + List teachingSuperiorSelectList(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 获取带教记录详情(用于修改时的数据回显) + * @param id 带教记录ID + * @return TeachingRecordDetailVo 带教记录详细信息 + */ + TeachingRecordDetailVo getTeachingDetail(String id); + + /** + * 获取带教相关的门店下拉列表 + * @return List 门店列表 + */ + List getTeachingStoreList(); + + /** + * 带教-汇总查询(分页) + * @param queryTeachingRecordDto 查询条件封装对象 + * @return PageInfo 汇总数据分页结果 + */ + PageInfo teachingSummaryPage(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 带教-汇总查询-导出Excel文件 + * @param queryTeachingRecordDto 查询条件封装对象 + */ + void summaryTeachExport(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 汇总带教详情-学员统计-导出Excel文件 + * @param queryTeachingRecordDto 查询条件封装对象 + */ + void studentDetailExport(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 带教明细分页查询 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return PageInfo 明细数据分页结果 + */ + PageInfo teachingItemPage(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 带教明细导出Excel文件 + * @param queryTeachingRecordDto 查询条件封装对象 + */ + void detailItemTeachExport(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 带教明细-批量删除多条记录 + * @param ids 要删除的记录ID集合 + */ + void batchDeleteRecord(List ids); + + /** + * 店长界面带教统计 + * @param storeId 门店ID + * @return TeachingStoreCountVo 门店带教统计信息 + */ + TeachingStoreCountVo storeTeachingCount(String storeId); + + /** + * 带教统计 + * @param queryTeachingRecordDto 查询条件封装对象 + * @return TeachingCountVo 统计结果 + */ + TeachingCountVo teachingCount(QueryTeachingRecordDto queryTeachingRecordDto); + + /** + * 获取上级带教统计信息 + * @param storeId 门店ID + * @return SuperiorTeachingSummaryVo 上级带教统计信息 + */ + SuperiorTeachingSummaryVo getSuperiorTeachingSummary(String storeId); + + /** + * 根据storeIds查询每个门店下的带教数量 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getStoreRecordCount(RecordQueryDto queryDto); + + /** + * 根据storeId查询门店下的带教记录[分页] + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getStoreRecordPage(RecordQueryDto queryDto); + + /** + * 查询带教详情 + * @param bizId 带教/练习id + * @return jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo + */ + TeachingDetailVo getTeachingDetailById(String bizId); + + /** + * 附件map + * @param recordIds 记录ids + * @return java.util.Map> + */ + Map> getFileMap(List recordIds); + + /** + * 获取用户已浏览的记录 + * @param bizIds 业务ids + * @param userId 用户id + * @return java.util.Set + */ + Set getViewedSet(List bizIds, String userId); + + /** + * 移动所有旧文件到文件表中 + */ + void removeAllOldFile(); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingSkillService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingSkillService.java new file mode 100644 index 0000000..efdd160 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingSkillService.java @@ -0,0 +1,108 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.model.cultivate.dto.teaching.TeachingSkillAddDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillPageDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillSortDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillUpdateDto; +import jnpf.model.cultivate.v2.exam.vo.ImportObjectVo; +import jnpf.model.cultivate.v2.exam.vo.SkillVo; +import jnpf.model.cultivate.vo.teaching.SkillInfoVo; +import jnpf.model.cultivate.vo.teaching.TeachingSkillVo; + +import javax.validation.Valid; +import java.util.List; + +public interface TeachingSkillService extends IService { + /** + * 新增 + * + * @param addDto 新增 + * @return boolean + */ + Boolean addData(TeachingSkillAddDto addDto); + + /** + * 删除 + * @param id 删除参数 + * @return boolean + */ + Boolean deleteData(String id); + + /** + * 修改 + * @param updateDto 修改参数 + * @return boolean + */ + Boolean updateData(TeachingSkillUpdateDto updateDto); + + /** + * 查询 + * @param id 查询参数 + * @return APP技能点 + */ + TeachingSkillVo getData(String id); + + /** + * 列表 + * @param pageDto 列表参数 + * @return APP技能点 + */ + PageInfo pageList(TeachingSkillPageDto pageDto); + + /** + * 排序 + * @return 技能点集合 + */ + List selectDataList(); + + /** + * 排序 + * @param sortDto 排序参数 + * @return 排序结果 + */ + Boolean sort(@Valid TeachingSkillSortDto sortDto); + + /** + * 查询分类下的技能点 + * @param categoryId 分类id + * @return java.util.List + */ + List querySkillIds(String categoryId); + + /** + * 查询重复的技能点index + * @param list 技能点导入数据 + * @return java.util.List + */ + List getDistinctCountBatch(List list); + + /** + * 保存导入的技能点 + * @param list 技能点数据 + */ + void saveImportData(List list); + + /** + * 查询技能点列表[下拉] + * @param skillId 技能点id + * @param categoryId 分类id + * @return java.util.List + */ + List getDownSkillList(String skillId, String categoryId); + + /** + * 查询技能点[树形] + * @return java.util.List + */ + List getAllSkillList(); + + /** + * 查询有子级的分类 + * @param categoryIds 分类ids + * @return java.util.List + */ + List getCategoryHasChild(List categoryIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingStudentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingStudentService.java new file mode 100644 index 0000000..1241968 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/TeachingStudentService.java @@ -0,0 +1,7 @@ +package jnpf.cultivate.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.TeachingStudent; + +public interface TeachingStudentService extends IService { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/V2TeachingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/V2TeachingService.java new file mode 100644 index 0000000..b2077a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/V2TeachingService.java @@ -0,0 +1,38 @@ +package jnpf.cultivate.service; + +import jnpf.exception.HandleException; +import jnpf.model.cultivate.dto.teaching.RecordQueryDto; +import jnpf.model.cultivate.dto.teaching.TeachingApproveDto; +import jnpf.model.cultivate.v2.teaching.vo.StoreTeachingVo; +import jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo; + +import java.util.List; + +/** + * 带教/练习服务[v2] + * + * @author yanwenfu + * @create 2026-03-02 + */ +public interface V2TeachingService { + + /** + * 带教记录[分页] + * @param queryDto 查询条件 + * @return java.util.List + */ + List getPage(RecordQueryDto queryDto); + + /** + * 带教/练习详情 + * @param bizId 带教/练习id + * @return jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo + */ + TeachingDetailVo getTeachingDetail(String bizId); + + /** + * 带教 - 审核 + * @param approveDto 审核dto + */ + void approveTeachingRecord(TeachingApproveDto approveDto) throws HandleException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgServiceImpl.java new file mode 100644 index 0000000..c848b9a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgServiceImpl.java @@ -0,0 +1,122 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.CultivateCourseMsgService; +import jnpf.model.cultivate.dto.course.app.FtbCultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.dto.course.app.FtbUserLevelMessageReadDTO; +import jnpf.model.cultivate.entiy.BaseEntity; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsg; +import jnpf.model.cultivate.po.mesgg.CultivateMessageInfo; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.course.vo.app.V2CultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.vo.course.app.FtbCheckJobGradeChangeVO; +import jnpf.model.cultivate.vo.course.app.FtbCultivateCourseMsgForAppVO; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Title:CultivateCourseMsgServiceImpl + * @Author:peng.hao + * @create: 2023/12/2913:36 + */ +@Slf4j +@Service +public class CultivateCourseMsgServiceImpl extends ServiceImpl implements CultivateCourseMsgService { + + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Resource + private FtbCultivateMessageInfoMapper ftbCultivateMessageInfoMapper; + + + @Override + public List queryUserUnreadMsg() { + String loginUserId = UserProvider.getLoginUserId(); + // 全部都是未学习课程,不提示,Special circumstances will not be considered + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, loginUserId); + queryWrapper.in(FtbCultivatePositionCourceLearning::getState, List.of(1, 2)); + if (ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper) > 0) { + FtbCultivateCourseMsgForAppDTO dto = new FtbCultivateCourseMsgForAppDTO(); + dto.setUserId(loginUserId); + dto.setStates(0); + return baseMapper.queryUserUnreadMsg(dto); + } + return new ArrayList<>(); + } + + @Override + public List queryUserUnreadMsgV2(String courseId) { + String userId = UserProvider.getLoginUserId(); + + List courseList = ftbCultivatePositionCourceLearningMapper.queryUserUnreadMsgV2(userId, courseId); + + if (CollUtil.isNotEmpty(courseList)) { + V2CultivateCourseMsgForAppDTO dto = new V2CultivateCourseMsgForAppDTO(); + dto.setUserId(userId); + dto.setCourseIds(courseList.stream().map(FtbCultivateCourse::getId).collect(Collectors.toList())); + return baseMapper.queryUserUnreadMsgV2(dto); + } + return new ArrayList<>(); + } + + @Override + public void updateInfoByMsgIdV2(String courseId) { + String loginUserId = UserProvider.getLoginUserId(); + baseMapper.deleteInfoByMsgIdV2(loginUserId, courseId); + } + + @Override + public List checkJobGradeChanges(Integer type) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateMessageInfo::getUserId, UserProvider.getLoginUserId()); + queryWrapper.eq(CultivateMessageInfo::getSource, type); + List cultivateMessageInfos = ftbCultivateMessageInfoMapper.selectList(queryWrapper); + return cultivateMessageInfos.stream().map(cultivateMessageInfo -> { + FtbCheckJobGradeChangeVO ftbCheckJobGradeChangeVO = new FtbCheckJobGradeChangeVO(); + ftbCheckJobGradeChangeVO.setId(cultivateMessageInfo.getId()); + return ftbCheckJobGradeChangeVO; + }).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void userLevelMessageHasBeenRead(FtbUserLevelMessageReadDTO ftbUserLevelMessageReadDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(BaseEntity::getId, ftbUserLevelMessageReadDTO.getPrimaryKeyId()); + ftbCultivateMessageInfoMapper.delete(queryWrapper); + } + + @Override + public List queryUserUnreadMsgUpdate(String courseId) { + String loginUserId = UserProvider.getLoginUserId(); + // 全部都是未学习课程,不提示,Special circumstances will not be considered + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, loginUserId); + queryWrapper.in(FtbCultivatePositionCourceLearning::getState, List.of(1, 2)); + if (ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper) > 0) { + FtbCultivateCourseMsgForAppDTO dto = new FtbCultivateCourseMsgForAppDTO(); + dto.setUserId(loginUserId); + dto.setStates(1); + dto.setCourseId(courseId); + return baseMapper.queryUserUnreadMsg(dto); + } + return new ArrayList<>(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgUserServiceImpl.java new file mode 100644 index 0000000..d5ea000 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCourseMsgUserServiceImpl.java @@ -0,0 +1,38 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateCourseMsgUserMapper; +import jnpf.cultivate.service.CultivateCourseMsgUserService; +import jnpf.model.cultivate.dto.course.app.FtbCultivateCourseMsgForAppDTO; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsgUser; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Title:CultivateCourseMsgServiceImpl + * @Author:peng.hao + * @create: 2023/12/2913:36 + */ +@Service +public class CultivateCourseMsgUserServiceImpl extends ServiceImpl implements CultivateCourseMsgUserService { + @Override + @Transactional(rollbackFor = Exception.class) + public void updateInfoByMsgId(FtbCultivateCourseMsgForAppDTO dto) { + String loginUserId = UserProvider.getLoginUserId(); + List collect = dto.getMsgId().stream().map(msg -> { + CultivateCourseMsgUser cultivateCourseMsgUser = new CultivateCourseMsgUser(); + cultivateCourseMsgUser.setMsgId(msg); + cultivateCourseMsgUser.setStatus(1); + cultivateCourseMsgUser.setUserId(loginUserId); + return cultivateCourseMsgUser; + }).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(collect)) { + this.saveBatch(collect); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverCategoryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverCategoryServiceImpl.java new file mode 100644 index 0000000..3d6dd4f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverCategoryServiceImpl.java @@ -0,0 +1,128 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateCoverCategoryMapper; +import jnpf.cultivate.mapper.CultivateCoverInfoMapper; +import jnpf.cultivate.service.CultivateCoverCategoryService; +import jnpf.enums.cultivate.CultivateIsSystemEnum; +import jnpf.model.cultivate.po.common.FtbCultivateCoverCategoryEntity; +import jnpf.model.cultivate.po.common.FtbCultivateCoverInfoEntity; +import jnpf.model.cultivate.v2.common.vo.V2BaseIdNameVo; +import jnpf.model.cultivate.v2.exam.req.V2SaveCoverCategoryReq; +import jnpf.model.cultivate.v2.exam.vo.V2TreeCoverVo; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @Desc 封面分类-实现类 + * @Author shangyi + * @Date 2026/1/22 + */ +@Slf4j +@Service +public class CultivateCoverCategoryServiceImpl extends ServiceImpl implements CultivateCoverCategoryService { + + @Autowired + private CultivateCoverInfoMapper coverInfoMapper; + + @Override + public String saveCategory(V2SaveCoverCategoryReq req) { + // 检查名称是否重复(排除当前ID) + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(FtbCultivateCoverCategoryEntity::getName, req.getName()); + checkWrapper.ne(StringUtil.isNotEmpty(req.getId()), FtbCultivateCoverCategoryEntity::getId, req.getId()); + if (this.count(checkWrapper) > 0) { + throw new RuntimeException("封面分类名称已存在,请勿重复添加!"); + } + FtbCultivateCoverCategoryEntity entity = new FtbCultivateCoverCategoryEntity(); + entity.setName(req.getName()); + entity.setIsSystem(CultivateIsSystemEnum.NO.getCode()); + if (StringUtil.isNotEmpty(req.getId())) { + FtbCultivateCoverCategoryEntity byId = getById(req.getId()); + if (byId == null) { + throw new RuntimeException("当前封面分类不存在!"); + } + if (byId.getIsSystem() == 1) { + throw new RuntimeException("系统封面分类不允许修改!"); + } + entity.setId(req.getId()); + updateById(entity); + return entity.getId(); + } else { + //查询分类个数不能超过50个 + Long count = querySelfCount(); + if (count >= 50) { + throw new RuntimeException("封面分类个数不能超过50个!"); + } + } + save(entity); + return entity.getId(); + } + + private Long querySelfCount() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FtbCultivateCoverCategoryEntity::getIsSystem, CultivateIsSystemEnum.NO.getCode()); + return this.count(wrapper); + } + + @Override + public void delCategory(String id) { + FtbCultivateCoverCategoryEntity byId = getById(id); + if (byId == null) { + throw new RuntimeException("当前封面分类不存在!"); + } + if (Objects.equals(byId.getIsSystem(), CultivateIsSystemEnum.YES.getCode())) { + throw new RuntimeException("系统封面分类不允许删除!"); + } + //先删除此分类下面所有的分类 + coverInfoMapper.delete(new LambdaQueryWrapper().eq(FtbCultivateCoverInfoEntity::getCategoryId, id)); + this.baseMapper.deleteById(id); + } + + @Override + public List getCategoryList(Integer isSystem) { + List list = this.list(new LambdaQueryWrapper() + .eq(isSystem != null, FtbCultivateCoverCategoryEntity::getIsSystem, isSystem) + .orderByDesc(FtbCultivateCoverCategoryEntity::getIsSystem) + .orderByAsc(FtbCultivateCoverCategoryEntity::getCreatorTime)); + return list.stream().map(e -> + new V2BaseIdNameVo().setId(e.getId()).setName(e.getName()).setIsSystem(e.getIsSystem())).collect(Collectors.toList()); + } + + @Override + public List treeCover() { + //所有的封面分类 + List categorylist = this.list(new LambdaQueryWrapper() + .orderByDesc(FtbCultivateCoverCategoryEntity::getIsSystem).orderByAsc(FtbCultivateCoverCategoryEntity::getCreatorTime)); + if (CollectionUtil.isEmpty(categorylist)) { + return Collections.emptyList(); + } + //所有的封面 + List coverList = coverInfoMapper.selectList(new LambdaQueryWrapper<>()); + //封装分类封面树 + List treeCoverList = categorylist.stream().map(e -> { + V2TreeCoverVo parent = new V2TreeCoverVo(); + parent.setCategoryId(e.getId()).setCategoryName(e.getName()).setIsSystem(e.getIsSystem()); + List children = coverList.stream().filter(cover -> cover.getCategoryId().equals(e.getId())).collect(Collectors.toList()); + parent.setCoverList(children.stream().map(c -> { + V2TreeCoverVo child = new V2TreeCoverVo(); + child.setCategoryId(e.getId()).setCategoryName(e.getName()) + .setPath(c.getPath()).setIsSystem(c.getIsSystem()); + return child; + }).collect(Collectors.toList())); + return parent; + }).collect(Collectors.toList()); + return treeCoverList; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverInfoServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverInfoServiceImpl.java new file mode 100644 index 0000000..983aff0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateCoverInfoServiceImpl.java @@ -0,0 +1,82 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateCoverInfoMapper; +import jnpf.cultivate.service.CultivateCoverInfoService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.po.common.FtbCultivateCoverInfoEntity; +import jnpf.model.cultivate.v2.exam.req.V2QueryCoverReq; +import jnpf.model.cultivate.v2.exam.req.V2SaveCoverReq; +import jnpf.model.cultivate.v2.exam.vo.V2CoverVo; +import jnpf.permission.entity.UserEntity; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Desc 封面-实现类 + * @Author shangyi + * @Date 2026/1/22 + */ +@Slf4j +@Service +public class CultivateCoverInfoServiceImpl extends ServiceImpl implements CultivateCoverInfoService { + + + @Autowired + private UserApiV2Util userApiV2Util; + + + @Override + public void saveCover(V2SaveCoverReq req) { + + FtbCultivateCoverInfoEntity entity = new FtbCultivateCoverInfoEntity(); + entity.setIsSystem(0); + entity.setCategoryId(req.getCategoryId()); + entity.setPath(req.getPath()); + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(UserProvider.getLoginUserId()); + if (StringUtil.isNotEmpty(req.getId())) { + FtbCultivateCoverInfoEntity old = getById(req.getId()); + if (old == null) { + throw new RuntimeException("封面不存在"); + } + if (old.getIsSystem().equals(1)) { + throw new RuntimeException("系统内置封面不允许修改"); + } + entity.setId(req.getId()); + updateById(entity); + return; + } + save(entity); + } + + @Override + public Page getPageList(V2QueryCoverReq req) { + //构建分页 + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + //根据分类id查询分页列表 + page = baseMapper.getPageList(page, req); + + if (CollUtil.isNotEmpty(page.getRecords())) { + List userIds = page.getRecords().stream().map(V2CoverVo::getUserId).distinct().collect(Collectors.toList()); + Map userNameForUserIds = userApiV2Util.getUserNameAndCopyForUserIds(userIds); + for (V2CoverVo record : page.getRecords()) { + UserEntity userEntity = userNameForUserIds.get(record.getUserId()); + if (userEntity != null) { + record.setUserName(userEntity.getRealName()); + } + } + } + return page; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateExamSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateExamSettingServiceImpl.java new file mode 100644 index 0000000..924e9fc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateExamSettingServiceImpl.java @@ -0,0 +1,37 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateExamSettingMapper; +import jnpf.cultivate.service.CultivateExamSettingService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamSettingEntity; +import jnpf.model.cultivate.v2.exam.req.V2ExamSettingReq; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +/** + * @Desc 考试配置-实现类 + * @Author shangyi + * @Date 2026/1/22 + */ +@Slf4j +@Service +public class CultivateExamSettingServiceImpl extends ServiceImpl implements CultivateExamSettingService { + + @Override + public void saveExamSetting(V2ExamSettingReq req) { + //id = 1就是全局配置 + req.setId("1"); + FtbCultivateExamSettingEntity entity = this.getById(req.getId()); + if (Objects.isNull(entity)) { + throw new ServiceException("要更新的考试配置不存在"); + } + BeanUtil.copyProperties(req, entity); + this.updateById(entity); + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateFileServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateFileServiceImpl.java new file mode 100644 index 0000000..3a4b4ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateFileServiceImpl.java @@ -0,0 +1,51 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.CultivateFileMapper; +import jnpf.cultivate.service.CultivateFileService; +import jnpf.model.cultivate.po.teaching.CultivateFile; +import jnpf.model.cultivate.v2.teaching.model.V2Attachment; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 文件服务实现 + * + * @author yanwenfu + * @create 2026-03-02 + */ +@Service +public class CultivateFileServiceImpl extends SuperServiceImpl implements CultivateFileService { + + @Override + public Map> getFileListByBizIds(List bizIds) { + + LambdaQueryWrapper fileQuery = new LambdaQueryWrapper() + .in(CultivateFile::getBusinessId, bizIds); + List list = this.list(fileQuery); + if (list == null || list.isEmpty()) { + return Collections.emptyMap(); + } + return list.stream() + .map(this::convertToV2Attachment) + .collect(Collectors.groupingBy(V2Attachment::getBizId)); + } + + @Override + public V2Attachment convertToV2Attachment(CultivateFile file) { + + V2Attachment attachment = new V2Attachment(); + attachment.setBizId(file.getBusinessId()); + attachment.setFileName(file.getFileName()); + attachment.setUrl(file.getUrl()); + attachment.setSize(file.getSize()); + attachment.setVideoActionType(file.getVideoActionType()); + attachment.setExtension(file.getExtension()); + return attachment; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApiServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApiServiceImpl.java new file mode 100644 index 0000000..f27db00 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApiServiceImpl.java @@ -0,0 +1,100 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.google.common.base.Joiner; +import jnpf.cultivate.service.CultivateIdentifyApiService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.vo.identify.UserOrgInfoAllVo; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.user.UserInfoVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +public class CultivateIdentifyApiServiceImpl implements CultivateIdentifyApiService { + + @Autowired + private UserApi userApi; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Qualifier("jnpf.permission.PositionApi") + @Autowired + private PositionApi positionApi; + + @Override + @Deprecated(since = "培训优化1.1", forRemoval = true) + public UserInfoVO selectUserDirectLeadership(String userId) { + return userApiV2Util.getBossByUserId(userId, null); + } + + @Override + public UserInfoVO selectUserDirectLeadershipNew(String orgId, String userId, String positionId, String positionGradesId) { + return userApi.getLeaderInfo(orgId, userId, positionId, positionGradesId); + } + + @Override + public Map> batchSelectUserOrgInfoByUserIds(List userIds) { + List organizeListApiVOList = positionApi.getOrgPositionInfoList(Joiner.on(",").join(userIds)); + List userOrgInfoAllVoList = CollectionUtil.newArrayList(); + if (CollectionUtil.isEmpty(organizeListApiVOList)) { + return null; + } + for (PositionGradesInfoBoundVO item : organizeListApiVOList) { + List organizeIds = item.getOrganizeIds(); + List organizeNames = item.getOrganizeNames(); + if (CollectionUtil.isEmpty(organizeIds)) { + continue; + } + IntStream.range(0, organizeIds.size()).forEach(i -> { + UserOrgInfoAllVo orgInfoAllVo = new UserOrgInfoAllVo(); + orgInfoAllVo.setUseId(item.getUserId()); + orgInfoAllVo.setUserOrgCode(organizeIds.get(i)); + orgInfoAllVo.setUserOrgName(organizeNames.get(i)); + orgInfoAllVo.setUserPostCode(item.getPositionId()); + orgInfoAllVo.setUserPostName(item.getPositionName()); + orgInfoAllVo.setUserOfficialRankCode(item.getPositionGradesId()); + orgInfoAllVo.setUserOfficialRankName(item.getPositionGradesName()); + userOrgInfoAllVoList.add(orgInfoAllVo); + }); + } + return userOrgInfoAllVoList.stream().collect(Collectors.groupingBy(UserOrgInfoAllVo::getUseId)); + } + + @Override + public Map> batchSelectOfficialRankInfo(List officialRankCodes) { + List organizeListApiVOList = positionApi.listPositionGradesInfoByPositionGrades(Joiner.on(",").join(officialRankCodes)); + List userOrgInfoAllVoList = CollectionUtil.newArrayList(); + if (CollectionUtil.isEmpty(organizeListApiVOList)) { + return null; + } + for (PositionGradesInfoBoundVO item : organizeListApiVOList) { + List organizeIds = item.getOrganizeIds(); + List organizeNames = item.getOrganizeNames(); + if (CollectionUtil.isEmpty(organizeIds)) { + continue; + } + IntStream.range(0, organizeIds.size()).forEach(i -> { + UserOrgInfoAllVo orgInfoAllVo = new UserOrgInfoAllVo(); + orgInfoAllVo.setUseId(item.getUserId()); + orgInfoAllVo.setUserOrgCode(organizeIds.get(i)); + orgInfoAllVo.setUserOrgName(organizeNames.get(i)); + orgInfoAllVo.setUserPostCode(item.getPositionId()); + orgInfoAllVo.setUserPostName(item.getPositionName()); + orgInfoAllVo.setUserOfficialRankCode(item.getPositionGradesId()); + orgInfoAllVo.setUserOfficialRankName(item.getPositionGradesName()); + userOrgInfoAllVoList.add(orgInfoAllVo); + }); + } + return userOrgInfoAllVoList.stream().collect(Collectors.groupingBy(UserOrgInfoAllVo::getUseId)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsBackupsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsBackupsServiceImpl.java new file mode 100644 index 0000000..2062ba7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsBackupsServiceImpl.java @@ -0,0 +1,18 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateIdentifyApplyDetailsBackupsMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyDetailsBackupsService; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetailsBackups; +import org.springframework.stereotype.Service; + +/** + * 实操鉴定项_关联_鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Service("CultivateIdentifyApplyDetailsBackupsService") +public class CultivateIdentifyApplyDetailsBackupsServiceImpl extends ServiceImpl implements CultivateIdentifyApplyDetailsBackupsService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsServiceImpl.java new file mode 100644 index 0000000..e2795db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyDetailsServiceImpl.java @@ -0,0 +1,20 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateIdentifyApplyDetailsMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyDetailsService; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetails; +import org.springframework.stereotype.Service; + +/** + * 实操鉴定申请详情表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Service("cultivateIdentifyApplyDetailsService") +public class CultivateIdentifyApplyDetailsServiceImpl extends ServiceImpl implements CultivateIdentifyApplyDetailsService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyServiceImpl.java new file mode 100644 index 0000000..67f9cbe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyServiceImpl.java @@ -0,0 +1,2171 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.nacos.shaded.com.google.common.base.Splitter; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.google.common.base.Joiner; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.cultivate.*; +import jnpf.enums.cultivate.ApplyResultEnum; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.enums.cultivate.ApplyStatusEnum; +import jnpf.enums.cultivate.ApplyTypeEnum; +import jnpf.message.SentMessageApi; +import jnpf.message.model.SentMessageIdentify; +import jnpf.message.model.SentMessageInfo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.identify.*; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskExam; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.identify.*; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.user.UserInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.*; +import jnpf.util.context.SpringContext; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 实操鉴定申请表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Slf4j +@Service("cultivateIdentifyApplyService") +public class CultivateIdentifyApplyServiceImpl extends ServiceImpl implements CultivateIdentifyApplyService { + + @Resource + FtbCultivatePositionIdentifyResultMapper ftbCultivatePositionIdentifyResultMapper; + @Autowired + FtbCultivatePromotionNewMapper promotionNewMapper; + @Autowired + FtbCultivateExamUserService ftbCultivateExamUserService; + @Autowired + FtbCultivateMyLearnTaskInfoService ftbCultivateMyLearnTaskInfoService; + @Autowired + FtbCultivateLearnTaskExamService ftbCultivateLearnTaskExamService; + @Autowired + FtbCultivateLearnTaskIdentificationService ftbCultivateLearnTaskIdentificationService; + @Autowired + private UserApi userApi; + @Autowired + private UserApiV2Util userApiV2Util; + @Autowired + private UserProvider userProvider; + @Autowired + private SentMessageApi sentMessageApi; + @Autowired + private CultivateIdentifyTableService identifyTableService; + @Autowired + private CultivateIdentifyItemsService identifyItemsService; + @Autowired + private CultivateIdentifyApplyDetailsMapper applyDetailsMapper; + @Autowired + private CultivateIdentifyApplyDetailsService applyDetailsService; + @Autowired + private PositionCultivateIdentifyService cultivateIdentifyService; + @Autowired + private CultivateIdentifyApplyTableBackupsService applyTableBackupsService; + @Autowired + private CultivateIdentifyApplyDetailsBackupsService applyDetailsBackupsService; + @Autowired + private CultivatePerUtils cultivatePerUtils; + @Resource + private FtbCultivateFileService ftbCultivateFileService; + @Resource + private FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + @Resource + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + @Autowired + private RedissonClient redissonClient; + + @NotNull + private static List getStrings(String[] beIdentifyUserIds, Map userMap) { + List listName = new ArrayList<>(); + for (String userId : beIdentifyUserIds) { + if (userMap.containsKey(userId)) { + String realName = userMap.get(userId).getUserName(); + listName.add(realName); + } + } + return listName; + } + + @Override + public PageInfo getPageApplyList(IdentifyApplyListDto req) { + com.github.pagehelper.Page objects = PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + req.setInnerPowerUserIds(innerPowerUserVO.getUserIds()); + } else if (innerPowerUserVO.getCode() == 0) { + req.setInnerPowerUserIds(new ArrayList<>()); + } else { + PageInfo page = new PageInfo<>(); + page.setPageSize((int) req.getPageSize()); + page.setPageNum((int) req.getCurrentPage()); + page.setTotal(0); + page.setSize(0); + page.setList(new ArrayList<>()); + return page; + } +// req.setLeaverUserIds(userApiV2Util.queryLeaveUserId()); + + PageInfo pageEntity = new PageInfo<>(this.baseMapper.getPageList(req)); + List applyListVoList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(pageEntity.getList())) { + //查询到人员名称-服务调用 + List userIdList = new ArrayList<>(); + pageEntity.getList().forEach(item -> { + userIdList.add(item.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(item.getIdentifyUserId())) { + String[] split = item.getIdentifyUserId().split(","); + userIdList.addAll(Arrays.asList(split)); + } + }); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIdList, null); + pageEntity.getList().forEach(item -> { + IdentifyApplyListVo applyListVo = new IdentifyApplyListVo(); + BeanUtils.copyProperties(item, applyListVo); + applyListVo.setUserTime(item.getUseTime()); + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO beUserInfoVo = userMap.get(item.getBeIdentifyUserId()); + // 获取用户头像 + if (Objects.nonNull(beUserInfoVo) && !StringUtil.isEmpty(beUserInfoVo.getHeadIcon())) { + beIdentifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(beUserInfoVo.getHeadIcon())); + } + beIdentifyUserInfo.setUserId(item.getBeIdentifyUserId()); + beIdentifyUserInfo.setUserName(Objects.isNull(userMap.get(item.getBeIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getBeIdentifyUserId()).getUserName()); + //被鉴定人职等的组织信息 + + if (beUserInfoVo != null) { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + orgInfoVo.setUserOrgName(beUserInfoVo.getOrganizeName()); + orgInfoVo.setUserPostName(beUserInfoVo.getPositionName()); + orgInfoVo.setUserOfficialRankName(beUserInfoVo.getGradeName()); + beIdentifyUserInfo.setUserOrgInfoPushVoList(List.of(orgInfoVo)); + } + + + List identifyUserInfos = new ArrayList<>(); + if (StringUtil.isNotEmpty(item.getIdentifyUserId())) { + for (String tempId : item.getIdentifyUserId().split(",")) { + //鉴定用户信息 + IdentifyUserOrgInfoVo identifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO userInfoVo = userMap.get(tempId); + // 获取用户头像 + if (Objects.nonNull(userInfoVo) && !StringUtil.isEmpty(userInfoVo.getHeadIcon())) { + identifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(userInfoVo.getHeadIcon())); + } + identifyUserInfo.setUserId(item.getIdentifyUserId()); + identifyUserInfo.setUserName(Objects.isNull(userMap.get(item.getIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getIdentifyUserId()).getUserName()); + //鉴定人职等的组织信息 + List orgInfoAll1VoList = JSON.parseArray(item.getOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(orgInfoAll1VoList)) { + identifyUserInfo.setUserOrgInfoPushVoList(orgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + identifyUserInfos.add(identifyUserInfo); + } + } + applyListVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + applyListVo.setIdentifyUserInfo(identifyUserInfos); + applyListVoList.add(applyListVo); + }); + } + PageInfo page = new PageInfo<>(); + BeanUtils.copyProperties(pageEntity, page); + page.setList(applyListVoList); + page.setTotal(objects.getTotal()); + page.setPageNum(objects.getPageNum()); + page.setPageSize(objects.getPageSize()); + return page; + } + + @Override + public IdentifyApplyBasicInfoVo getApplyBasicInfo(IdentifyTableInfoDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + IdentifyApplyBasicInfoVo applyBasicInfoVo = new IdentifyApplyBasicInfoVo(); + BeanUtils.copyProperties(apply, applyBasicInfoVo); + + applyBasicInfoVo.setId(apply.getId()); + applyBasicInfoVo.setTableId(table.getTableId()); + applyBasicInfoVo.setTableName(table.getName()); + applyBasicInfoVo.setRuleId(table.getRuleId()); + String beIdentifyUserId = apply.getBeIdentifyUserId(); + String identifyUserId = apply.getIdentifyUserId(); + String[] userIds = new String[]{}; + if (StringUtil.isNotEmpty(identifyUserId)) { + userIds = identifyUserId.split(","); + applyBasicInfoVo.setIdentifyUserId(List.of(userIds)); + } else { + applyBasicInfoVo.setIdentifyUserId(new ArrayList<>()); + } + String[] beIdentifyUserIds = beIdentifyUserId.split(","); + Map userMap = getUserMaps(identifyUserId, beIdentifyUserId); + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + beIdentifyUserInfo.setUserId(beIdentifyUserId); + if (StringUtils.isNotEmpty(beIdentifyUserId)) { + List listName = getStrings(beIdentifyUserIds, userMap); + beIdentifyUserInfo.setUserName(CollUtil.isEmpty(listName) ? + "未查询到用户" : String.join(",", listName)); + } + //被鉴定人职等的组织信息 + List beOrgInfoAll1VoList = JSON.parseArray(apply.getBeOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(beOrgInfoAll1VoList)) { + beIdentifyUserInfo.setUserOrgInfoPushVoList(beOrgInfoAll1VoList.stream().map(item -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(item, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + + //鉴定用户信息 + IdentifyUserOrgInfoVo identifyUserInfo = new IdentifyUserOrgInfoVo(); + identifyUserInfo.setUserId(apply.getIdentifyUserId()); + if (StringUtils.isNotEmpty(beIdentifyUserId)) { + List listName = getStrings(userIds, userMap); + identifyUserInfo.setUserName(CollUtil.isEmpty(listName) ? + "未查询到用户" : String.join(",", listName)); + } + //鉴定人职等的组织信息 + List orgInfoAll1VoList = JSON.parseArray(apply.getOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(orgInfoAll1VoList)) { + identifyUserInfo.setUserOrgInfoPushVoList(orgInfoAll1VoList.stream().map(item -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(item, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + applyBasicInfoVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + applyBasicInfoVo.setIdentifyUserInfo(identifyUserInfo); + return applyBasicInfoVo; + } + + @Override + public IdentifyApplyInfoVo getApplyInfo(IdentifyTableInfoDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + IdentifyApplyInfoVo applyInfoVo = new IdentifyApplyInfoVo(); + BeanUtils.copyProperties(apply, applyInfoVo); + applyInfoVo.setUserTime(apply.getUseTime()); + applyInfoVo.setTableName(table.getName()); + applyInfoVo.setRuleDesc(table.getRuleDesc()); + String identifyUserId = apply.getIdentifyUserId(); + String[] userIds = new String[]{}; + if (StringUtil.isNotEmpty(identifyUserId)) { + userIds = identifyUserId.split(","); + } + String beIdentifyUserId = apply.getBeIdentifyUserId(); + + String[] beIdentifyUserIds = beIdentifyUserId.split(","); + Map userMap = getUserMaps(identifyUserId, beIdentifyUserId); + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + beIdentifyUserInfo.setUserId(beIdentifyUserId); + if (StringUtils.isNotEmpty(beIdentifyUserId)) { + List listName = getStrings(beIdentifyUserIds, userMap); + beIdentifyUserInfo.setUserName(CollUtil.isEmpty(listName) ? + "未查询到用户" : String.join(",", listName)); + } + //被鉴定人职等的组织信息 + List beOrgInfoAll1VoList = JSON.parseArray(apply.getBeOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(beOrgInfoAll1VoList)) { + beIdentifyUserInfo.setUserOrgInfoPushVoList(beOrgInfoAll1VoList.stream().map(item -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(item, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + IdentifyUserOrgInfoVo identifyUserInfo = new IdentifyUserOrgInfoVo(); + identifyUserInfo.setUserId(identifyUserId); + if (StringUtils.isNotEmpty(identifyUserId)) { + List listName = getStrings(userIds, userMap); + identifyUserInfo.setUserName(CollUtil.isEmpty(listName) ? + "未查询到用户" : String.join(",", listName)); + } + //被鉴定人职等的组织信息 + List orgInfoAll1VoList = JSON.parseArray(apply.getOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(orgInfoAll1VoList)) { + identifyUserInfo.setUserOrgInfoPushVoList(orgInfoAll1VoList.stream().map(item -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(item, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + applyInfoVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + applyInfoVo.setIdentifyUserInfo(identifyUserInfo); + //设置鉴定项得分集合 + List detailsInfoVos = this.applyDetailsMapper.getIdentifyItemList(apply.getId()); + applyInfoVo.setIdentifyItemList(CollectionUtil.isNotEmpty(detailsInfoVos) ? detailsInfoVos : CollectionUtil.newArrayList()); + BigDecimal totalScore = BigDecimal.ZERO; + if (CollectionUtil.isNotEmpty(detailsInfoVos) && apply.getStatus().equals(ApplyStatusEnum.YJD.getCode())) { + for (IdentifyApplyDetailsInfoVo detailsInfoVo : detailsInfoVos) { + totalScore = totalScore.add(detailsInfoVo.getScore()); + } + applyInfoVo.setTotalScore(totalScore); + } else { + applyInfoVo.setTotalScore(null); + applyInfoVo.setUserTime(null); + applyInfoVo.setResult(null); + } + // 查询当前申请时上传的文件信息 + + LambdaQueryWrapper wrapperFile = Wrappers.lambdaQuery(); + wrapperFile.eq(FtbCultivateFile::getBusinessId, apply.getId()); + List fileLists = ftbCultivateFileService.list(wrapperFile); + LambdaQueryWrapper wrapperFile2 = Wrappers.lambdaQuery(); + wrapperFile2.eq(FtbCultivateFile::getBusinessId, apply.getId() + apply.getBeIdentifyUserId()); + List fileLists2 = ftbCultivateFileService.list(wrapperFile2); + List fileVOS = fileLists.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + List fileVOS2 = fileLists2.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + List file = new ArrayList<>(); + file.addAll(fileVOS2); + file.addAll(fileVOS); + applyInfoVo.setFiles(file); + return applyInfoVo; + } + + public Map getUserMaps(String identifyUserId, String beIdentifyUserId) { + List list = new ArrayList<>(); + List identifyUserIdList = new ArrayList<>(); + if (StringUtil.isNotEmpty(identifyUserId)) { + identifyUserIdList = List.of(identifyUserId.split(",")); + } + if (CollectionUtil.isNotEmpty(identifyUserIdList)) { + list.addAll(identifyUserIdList); + } + List beIdentifyUserIds = List.of(beIdentifyUserId.split(",")); + if (CollectionUtil.isNotEmpty(beIdentifyUserIds)) { + list.addAll(beIdentifyUserIds); + } + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } + return userApiV2Util.getUserPrimaryBoundBatch(list, null); + } + + @Override + @Transactional + public void applyDataSave(IdentifyApplySaveDto req) throws RuntimeException { + //判断鉴定人与被鉴定人是否同一个人 + ServiceException.isTrue(!req.getBeIdentifyUserId().equals(req.getIdentifyUserId()), "鉴定人与被鉴定人不能是同一个"); + CultivateIdentifyTable table = this.identifyTableService.getById(req.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + + List identifyItemsList = this.identifyItemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, req.getTableId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list(); + //生成鉴定项数据 + ServiceException.notNull(identifyItemsList, "未查询到鉴定项数据"); + + CultivateIdentifyApply apply = new CultivateIdentifyApply(); + apply.setId(RandomUtil.uuId()); + BeanUtils.copyProperties(req, apply); + apply.setStatus(ApplyStatusEnum.DJD.getCode()); + apply.setIsReIdentify(ConstantUtil.NUM_FALSE); + //用户userId集合 + List userIdList = new ArrayList<>(); + userIdList.add(req.getBeIdentifyUserId()); + //查询用户直系领导组织信息 + Map> userOrgMap = userApiV2Util.getUserPrimaryBoundBatchCompatible(userIdList, null); + apply.setIsChoose(ConstantUtil.NUM_FALSE); + + //设置被鉴定人信息 + List beIdentifyUserInfoJsonList = CollectionUtil.newArrayList(); + List beIdentifyUserInfoList = userOrgMap.get(req.getBeIdentifyUserId()); + ServiceException.notNull(beIdentifyUserInfoList, "未查询到鉴定用户直系领导组织信息"); + + beIdentifyUserInfoJsonList.addAll(beIdentifyUserInfoList.stream().map(item -> { + UserOrgInfoAll1Vo all1Vo = new UserOrgInfoAll1Vo(); + all1Vo.setUserOrgName(item.getOrganizeName()); + all1Vo.setUserOrgCode(item.getOrganizeEnCode()); + all1Vo.setUserPostName(item.getPositionName()); + all1Vo.setUserPostCode(item.getPositionEnCode()); + all1Vo.setUserOfficialRankName(item.getGradeName()); + return all1Vo; + }).collect(Collectors.toList())); + apply.setBeIdentifyOrgList(Joiner.on(",").join(beIdentifyUserInfoList.stream().map( + UserBoundVO::getOrganizeId).distinct().collect(Collectors.toList()))); + apply.setBeIdentifyUserId(req.getBeIdentifyUserId()); + apply.setBeOfficialRankInfoJson(JSON.toJSONString(beIdentifyUserInfoJsonList)); + //生成鉴定项数据 + apply.setSource(req.getSource()); + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + apply.setTableId(applyTableBackups.getId()); + apply.setSource(req.getSource()); + //生成鉴定项数据 + + List applyDetailsBackupsList = CollectionUtil.newArrayList(); + if (CollectionUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + applyDetailsBackups.setItemsId(item.getId()); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List applyDetailsList = CollectionUtil.newArrayList(); + if (CollectionUtil.isNotEmpty(applyDetailsBackupsList)) { + applyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(apply.getId()); + applyDetails.setItemsId(item.getId()); + return applyDetails; + }).collect(Collectors.toList())); + } else { + ServiceException.notNull(apply, "未查询到鉴定项数据"); + } + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollectionUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + //保存鉴定数据 + this.save(apply); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.saveBatch(applyDetailsList); + } + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(req.getFiles()) + .businessTypeID(apply.getId() + apply.getBeIdentifyUserId()) + .type(FileEventDTO.FileType.PRACTICAL_APPRAISAL) + .build())); + } + + + private IdentifyApplySaveDto convertToIdentifyApplySaveDto(@Valid BatchIdentifyApplySaveDto req) { + IdentifyApplySaveDto identifyApplySaveDto = new IdentifyApplySaveDto(); + identifyApplySaveDto.setTableId(req.getTableId()); + identifyApplySaveDto.setAppraisalResults(req.getAppraisalResults()); + identifyApplySaveDto.setSource(req.getSource()); + identifyApplySaveDto.setFiles(req.getFiles()); + return identifyApplySaveDto; + } + + private void checkBatchParam(@Valid BatchIdentifyApplySaveDto req) { + List beIdentifyUserInfoDtoList = req.getBeIdentifyUserInfoDtoList(); + if (CollUtil.isEmpty(beIdentifyUserInfoDtoList)) { + throw new RuntimeException("被鉴定人信息不能为空"); + } + } + + @Override + @Transactional + public void applyDataSaveBatch(BatchIdentifyApplySaveDto req) throws RuntimeException { + checkBatchParam(req); + CultivateIdentifyTable table = this.identifyTableService.getById(req.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + IdentifyApplySaveDto identifyApplySaveDto = convertToIdentifyApplySaveDto(req); + List applyList = CollectionUtil.newArrayList(); + + List identifyItemsList = this.identifyItemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, req.getTableId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list(); + ServiceException.notNull(identifyItemsList, "未查询到鉴定项数据"); + + List identifyUserIds = req.getBeIdentifyUserInfoDtoList().stream().map(IdentifyUserInfoDto::getBeIdentifyUserId).collect(Collectors.toList()); + Map> userPrimaryBoundBatchCompatibleMap = userApiV2Util.getUserPrimaryBoundBatchCompatible(identifyUserIds, null); + + for (IdentifyUserInfoDto identifyUserInfoDto : req.getBeIdentifyUserInfoDtoList()) { + List userBoundVOS = userPrimaryBoundBatchCompatibleMap.get(identifyUserInfoDto.getBeIdentifyUserId()); + if (CollUtil.isEmpty(userBoundVOS)) { + throw new RuntimeException("用户:" + identifyUserInfoDto.getBeIdentifyUserName() + ",未查询到该被鉴定人组织、岗位信息"); + } + } + + for (IdentifyUserInfoDto identifyUserInfoDto : req.getBeIdentifyUserInfoDtoList()) { + identifyApplySaveDto.setBeIdentifyUserId(identifyUserInfoDto.getBeIdentifyUserId()); + identifyApplySaveDto.setBeIdentifyOrgId(identifyUserInfoDto.getBeIdentifyOrgId()); + identifyApplySaveDto.setBeIdentifyPostId(identifyUserInfoDto.getBeIdentifyPostId()); + identifyApplySaveDto.setBeIdentifyPostRankId(identifyUserInfoDto.getBeIdentifyPostRankId()); + applyList.add(this.applyDataSaveNew(identifyApplySaveDto, table, identifyItemsList, userPrimaryBoundBatchCompatibleMap)); + } + + for (CultivateIdentifyApply apply : applyList) { + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(req.getFiles()) + .businessTypeID(apply.getId() + apply.getBeIdentifyUserId()) + .type(FileEventDTO.FileType.PRACTICAL_APPRAISAL) + .build())); + } + } + + public CultivateIdentifyApply applyDataSaveNew(IdentifyApplySaveDto req, CultivateIdentifyTable table, List identifyItemsList, Map> userOrgMap) throws RuntimeException { + CultivateIdentifyApply apply = new CultivateIdentifyApply(); + apply.setId(RandomUtil.uuId()); + apply.setName(table.getName()); + BeanUtils.copyProperties(req, apply); + apply.setStatus(ApplyStatusEnum.DJD.getCode()); + apply.setIsReIdentify(ConstantUtil.NUM_FALSE); + //设置被鉴定人信息 + List beIdentifyUserInfoJsonList = CollectionUtil.newArrayList(); + List beIdentifyUserInfoList = userOrgMap.get(req.getBeIdentifyUserId()); + beIdentifyUserInfoJsonList.addAll(beIdentifyUserInfoList.stream().map(item -> { + UserOrgInfoAll1Vo all1Vo = new UserOrgInfoAll1Vo(); + all1Vo.setUserOrgCode(item.getOrganizeEnCode()); + all1Vo.setUserOrgName(item.getOrganizeName()); + all1Vo.setUserPostCode(item.getPositionEnCode()); + all1Vo.setUserPostName(item.getPositionName()); + all1Vo.setUserOfficialRankCode(item.getGradeId()); + all1Vo.setUserOfficialRankName(item.getGradeName()); + return all1Vo; + }).collect(Collectors.toList())); + apply.setBeIdentifyOrgList(Joiner.on(",").join(beIdentifyUserInfoList.stream().map( + UserBoundVO::getOrganizeId).distinct().collect(Collectors.toList()))); + apply.setBeIdentifyUserId(req.getBeIdentifyUserId()); + apply.setBeOfficialRankInfoJson(JSON.toJSONString(beIdentifyUserInfoJsonList)); + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + apply.setTableId(applyTableBackups.getId()); + apply.setSource(req.getSource()); + + //生成鉴定项数据 + List applyDetailsBackupsList = CollectionUtil.newArrayList(); + if (CollectionUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + applyDetailsBackups.setItemsId(item.getId()); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List applyDetailsList = CollectionUtil.newArrayList(); + if (CollectionUtil.isNotEmpty(applyDetailsBackupsList)) { + applyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(apply.getId()); + applyDetails.setItemsId(item.getId()); + return applyDetails; + }).collect(Collectors.toList())); + } else { + ServiceException.notNull(apply, "未查询到鉴定项数据"); + } + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollectionUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + //保存鉴定数据 + this.save(apply); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.saveBatch(applyDetailsList); + } + return apply; + } + + @Override + @Transactional + public String applyDataPush(IdentifyApplyDataPushDto req) throws RuntimeException { + // 构建带分隔符的锁键,防止键冲突 + String lockKey = String.format("lock:applyDataPush:%s:%s:%s:%s:%s", + req.getDataId(), + req.getSource(), + req.getTableId(), + req.getIdentifyUserId(), + req.getPostId()); + RLock lock = redissonClient.getLock(lockKey); + try { + // 尝试获取锁,等待0秒,锁自动释放时间30秒 + if (lock.tryLock(0, 30, TimeUnit.SECONDS)) { + return applyDataPushReal(req); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // 恢复中断状态 + throw new RuntimeException("Lock acquisition interrupted", e); + } finally { + if (lock.isHeldByCurrentThread() && lock.isLocked()) { + lock.unlock(); + } + } + return ""; + } + + + public String applyDataPushReal(IdentifyApplyDataPushDto req) throws RuntimeException { + CultivateIdentifyTable table = this.identifyTableService.getById(req.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CultivateIdentifyApply::getSourceId, req.getDataId()); + wrapper.eq(CultivateIdentifyApply::getSource, req.getSource()); + wrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, req.getIdentifyUserId()); + wrapper.eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + // 任务鉴定不需要去校验table申请 + wrapper.eq(req.getSource() != 4, CultivateIdentifyApply::getTableId, req.getTableId()); + wrapper.last("limit 1"); + CultivateIdentifyApply oldApply = baseMapper.selectOne(wrapper); + CultivateIdentifyApply apply = null; + if (oldApply != null) { + if (oldApply.getStatus().equals(1) || oldApply.getStatus().equals(0)) { + return null; + } + oldApply.setDeleteMark(1); + deleteApplyDetails(oldApply.getId()); + baseMapper.updateById(oldApply); + apply = oldApply; + } else { + apply = new CultivateIdentifyApply(); + } + + + apply.setId(RandomUtil.uuId()); + BeanUtils.copyProperties(req, apply); + apply.setIdentifyUserId(""); + apply.setIdentifyOrgList(""); + apply.setSourceId(req.getDataId()); + apply.setAppraisalResults(ConstantUtil.NUM_FALSE); + + List identifyItemsList = this.identifyItemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, req.getTableId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list(); + ServiceException.notNull(identifyItemsList, "未查询到鉴定项数据"); + + // 默认申请的打开可见性, 默认为0 + if (req.getSource() == 2 && (req.getIsTurnOnVisibility() != null && req.getIsTurnOnVisibility() == 1)) { + apply.setIsVisible(1); + } + if (req.getSource() == 4 && (req.getIsTurnOnVisibility() != null && req.getIsTurnOnVisibility() == 1)) { + apply.setIsVisible(1); + } + //设置被鉴定人信息 + apply.setBeIdentifyUserId(req.getIdentifyUserId()); + apply.setIsChoose(ConstantUtil.NUM_FALSE); + apply.setStatus(ApplyStatusEnum.DJD.getCode()); + apply.setStudyFinishTime(req.getStudyFinishTime()); + apply.setIsReIdentify(ConstantUtil.NUM_FALSE); + apply.setDeleteMark(0); + // 岗位学习和课程实操鉴定,不进行直属主管判定,因为直属主管需要组织、岗位、职等进行确定 + UserBoundVO beIdentifyUserInfoList = userApiV2Util.getUserPrimaryBoundOne(req.getIdentifyUserId(), null); + ServiceException.notNull(beIdentifyUserInfoList, "被鉴定人信息为null"); + + //被鉴定人组织信息 + List beIdentifyUserInfoJsonList = CollectionUtil.newArrayList(); + ServiceException.notNull(beIdentifyUserInfoList, "未查询到被鉴定人组织信息"); + UserOrgInfoAll1Vo all1Vo = new UserOrgInfoAll1Vo(); + all1Vo.setUserOrgCode(beIdentifyUserInfoList.getOrganizeEnCode()); + all1Vo.setUserOrgName(beIdentifyUserInfoList.getOrganizeName()); + all1Vo.setUserPostCode(beIdentifyUserInfoList.getPositionEnCode()); + all1Vo.setUserPostName(beIdentifyUserInfoList.getPositionName()); + all1Vo.setUserOfficialRankCode(beIdentifyUserInfoList.getGradeId()); + all1Vo.setUserOfficialRankName(beIdentifyUserInfoList.getGradeName()); + beIdentifyUserInfoJsonList.add(all1Vo); + + apply.setBeIdentifyOrgList(beIdentifyUserInfoList.getOrganizeId()); + apply.setBeIdentifyUserId(req.getIdentifyUserId()); + apply.setBeOfficialRankInfoJson(JSON.toJSONString(beIdentifyUserInfoJsonList)); + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + apply.setTableId(applyTableBackups.getId()); + + List applyDetailsBackupsList = CollectionUtil.newArrayList(); + if (CollectionUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List applyDetailsList = CollectionUtil.newArrayList(); + CultivateIdentifyApply finalApply = apply; + applyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(finalApply.getId()); + applyDetails.setItemsId(item.getId()); + return applyDetails; + }).collect(Collectors.toList())); + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollectionUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + //保存鉴定数据 + apply.setPostId(req.getPostId()); + this.save(apply); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.saveBatch(applyDetailsList); + } + return apply.getId(); + } + + private void deleteApplyDetails(String appId) { + this.applyDetailsService.update(new LambdaUpdateWrapper() + .set(CultivateIdentifyApplyDetails::getDeleteMark, 1) + .eq(CultivateIdentifyApplyDetails::getApplyId, appId) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, 0)) + ; + } + + @Override + public void turnOnVisibility(String id, String userId) { + // 根据岗位id查询岗位学习绑定的实操鉴定主键 + log.info("turnOnVisibility={},{}", id, userId); + String appply = baseMapper.querAppplyUserIds(id, userId); + if (StringUtils.isEmpty(appply)) { + return; + } + log.info("applyId=--->{}", appply); + // 考试合格后设置为可见 + CultivateIdentifyApply identifyApply = new CultivateIdentifyApply(); + identifyApply.setId(appply); + identifyApply.setIsVisible(0); + identifyApply.setDeleteMark(0); + baseMapper.updateById(identifyApply); + } + + /** + * 任务鉴定考试合格后打开可见性 + * + * @param taskId 任务id + * @param userId 用户id + * @param status 合格- 3 不合格-4 5-优秀 + */ + @Override + public void turnOnVisibilityWithTaskId(String taskId, String userId, Integer status) { + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateIdentifyApply::getSourceId, taskId); + queryWrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + queryWrapper.eq(CultivateIdentifyApply::getDeleteMark, 0); + CultivateIdentifyApply apply = baseMapper.selectOne(queryWrapper); + LambdaQueryWrapper learnTaskExamWrapper = Wrappers.lambdaQuery(); + learnTaskExamWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, taskId); + learnTaskExamWrapper.eq(FtbCultivateLearnTaskExam::getEnableMark, 0); + + List learnTaskExams = ftbCultivateLearnTaskExamService.list(learnTaskExamWrapper); + if (CollUtil.isEmpty(learnTaskExams)) { + throw new RuntimeException("任务关联考试信息不存在"); + } + FtbCultivateLearnTaskExam ftbCultivateLearnTaskExam = learnTaskExams.get(0); + if (apply != null) { + // 考试合格后设置为可见 + if (apply.getIsVisible() == 1) { + CultivateIdentifyApply updateApply = new CultivateIdentifyApply(); + updateApply.setId(apply.getId()); + updateApply.setIsVisible(0); + updateApply.setDeleteMark(0); + baseMapper.updateById(updateApply); + } + LambdaQueryWrapper learnTaskIdentificateionWrapper = Wrappers.lambdaQuery(); + learnTaskIdentificateionWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, taskId); + learnTaskIdentificateionWrapper.eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + + List learnTaskIdentification = ftbCultivateLearnTaskIdentificationService.list(learnTaskIdentificateionWrapper); + if (CollUtil.isEmpty(learnTaskIdentification)) { + throw new RuntimeException("任务关联鉴定信息不存在"); + } + //这里考试必定合格 + FtbCultivateLearnTaskIdentification ftbCultivateLearnTaskIdentification = learnTaskIdentification.get(0); + if (ftbCultivateLearnTaskIdentification.getIsPassComplete() == 1 && (Objects.equals(apply.getResult(), ApplyResultEnum.HG.getCode()) + || Objects.equals(apply.getResult(), ApplyResultEnum.YX.getCode()))) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(2) + .taskId(apply.getSourceId()) + .issuanceType(2) + .userId(apply.getBeIdentifyUserId()).build())); + // 如果是任务鉴定进行完成时间更新 + ftbCultivateMyLearnTaskInfoService.updateUserFinishTime(apply.getSourceId(), apply.getBeIdentifyUserId(), 2); + } else if (ftbCultivateLearnTaskIdentification.getIsPassComplete() == 0 && (Objects.equals(apply.getResult(), ApplyResultEnum.HG.getCode()) + || Objects.equals(apply.getResult(), ApplyResultEnum.YX.getCode()) || Objects.equals(apply.getResult(), ApplyResultEnum.BHG.getCode()))) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(2) + .taskId(apply.getSourceId()) + .issuanceType(2) + .userId(apply.getBeIdentifyUserId()).build())); + // 如果是任务鉴定进行完成时间更新 + ftbCultivateMyLearnTaskInfoService.updateUserFinishTime(apply.getSourceId(), apply.getBeIdentifyUserId(), 2); + } + + } else { + //考试合格才能完成 + if (ftbCultivateLearnTaskExam.getIsPassComplete() == 1 && (status == 3 || status == 5)) { + //如果没有配置鉴定,就置为已完成 并颁发证书 + ftbCultivateMyLearnTaskInfoService.updateUserFinishTime(taskId, userId, 1); + // 颁发证书 + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(1) + .taskId(taskId) + .issuanceType(2) + .userId(userId) + .build())); + } else if (ftbCultivateLearnTaskExam.getIsPassComplete() == 0 && (status == 3 || status == 4 || status == 5)) { + //如果没有配置鉴定,就置为已完成 并颁发证书 + ftbCultivateMyLearnTaskInfoService.updateUserFinishTime(taskId, userId, 1); + // 颁发证书 + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(1) + .taskId(taskId) + .issuanceType(2) + .userId(userId) + .build())); + } + } + + } + + @Override + @DSTransactional + public void applyDataReIdentify(IdentifyApplyReIdentifyDto req) { + CultivateIdentifyApply apply = this.lambdaQuery() + .eq(CultivateIdentifyApply::getId, req.getId()) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0) + .last("limit 1") + .one(); + ServiceException.notNull(apply, "未查询到实操鉴定数据"); + if (!apply.getResult().equals(ApplyResultEnum.BHG.getCode())) { + throw new ServiceException("鉴定不合格的数据才能重新鉴定"); + } + CultivateIdentifyApplyTableBackups applyTableBackupsOld = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(applyTableBackupsOld, "未查询到实操鉴定备份表记录"); + //重新鉴定需拉取最新的鉴定表数据 + CultivateIdentifyTable table = this.identifyTableService.getById(applyTableBackupsOld.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + //生成鉴定申请 + CultivateIdentifyApply reIdentifyApply = JsonUtil.getJsonToBean(apply, CultivateIdentifyApply.class); + reIdentifyApply.setId(RandomUtil.uuId()); + reIdentifyApply.setPlanIdentifyTime(null); + reIdentifyApply.setIdentifyTime(null); + reIdentifyApply.setUseTime(null); + reIdentifyApply.setStatus(ApplyStatusEnum.DJD.getCode()); + reIdentifyApply.setResult(null); + reIdentifyApply.setIsReIdentify(ConstantUtil.NUM_TRUE); + if (!apply.getSource().equals(ApplySourceEnum.SDFQ.getCode())) { + apply.setSource(null); + } + apply.setIsInitiate(ConstantUtil.NUM_TRUE); + StringBuilder reIdentifyId = new StringBuilder(StringUtil.isNotEmpty(apply.getReIdentifyId()) ? apply.getReIdentifyId() : ""); + reIdentifyApply.setReIdentifyId(StringUtil.isNotEmpty(apply.getReIdentifyId()) ? + reIdentifyId.append(",").append(apply.getId()).toString() : + reIdentifyId.append(apply.getId()).toString()); + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + reIdentifyApply.setTableId(applyTableBackups.getId()); + List identifyItemsList = this.identifyItemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, table.getId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list(); + ServiceException.notNull(identifyItemsList, "未查询到鉴定项数据"); + List applyDetailsBackupsList = CollectionUtil.newArrayList(); + if (CollectionUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List reIdentifyApplyDetailsList = CollectionUtil.newArrayList(); + reIdentifyApplyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(reIdentifyApply.getId()); + applyDetails.setItemsId(item.getId()); + return applyDetails; + }).collect(Collectors.toList())); + + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollectionUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + //保存鉴定数据 + this.updateById(apply); + this.save(reIdentifyApply); + if (CollectionUtil.isNotEmpty(reIdentifyApplyDetailsList)) { + this.applyDetailsService.saveBatch(reIdentifyApplyDetailsList); + } + } + + @Override + public List applyDataDeleteCheck(IdentifyTableInfoDto req) { + List applyListVoList = CollectionUtil.newArrayList(); + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + //判断是否重新鉴定 + if (apply.getIsReIdentify().equals(ConstantUtil.NUM_TRUE)) { + //查询重新鉴定的源数据 + List ids = Splitter.on(",").splitToList(apply.getReIdentifyId()); + List listDbVoList = this.baseMapper.getList(ids); + if (CollectionUtil.isNotEmpty(listDbVoList)) { + //查询到人员名称-服务调用 + List userIdList = new ArrayList<>(); + for (IdentifyApplyListDbVo identifyApplyListDbVo : listDbVoList) { + userIdList.add(identifyApplyListDbVo.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(identifyApplyListDbVo.getIdentifyUserId())) { + String[] split = identifyApplyListDbVo.getIdentifyUserId().split(","); + userIdList.addAll(Arrays.asList(split)); + } + } + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIdList, null); + listDbVoList.forEach(item -> { + IdentifyApplyListVo applyListVo = new IdentifyApplyListVo(); + BeanUtils.copyProperties(item, applyListVo); + applyListVo.setUserTime(item.getUseTime()); + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO beUserInfoVo = userMap.get(item.getBeIdentifyUserId()); + // 获取用户头像 + if (Objects.nonNull(beUserInfoVo) && !StringUtil.isEmpty(beUserInfoVo.getHeadIcon())) { + beIdentifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(beUserInfoVo.getHeadIcon())); + } + beIdentifyUserInfo.setUserId(item.getBeIdentifyUserId()); + beIdentifyUserInfo.setUserName(Objects.isNull(userMap.get(item.getBeIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getBeIdentifyUserId()).getUserName()); + //被鉴定人职等的组织信息 + List beOrgInfoAll1VoList = JSON.parseArray(item.getBeOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(beOrgInfoAll1VoList)) { + beIdentifyUserInfo.setUserOrgInfoPushVoList(beOrgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + + //鉴定用户信息 + List identifyUserInfos = new ArrayList<>(); + if (StringUtil.isNotEmpty(item.getIdentifyUserId())) { + for (String templateId : item.getIdentifyUserId().split(",")) { + IdentifyUserOrgInfoVo identifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO userInfoVo = userMap.get(templateId); + // 获取用户头像 + if (Objects.nonNull(userInfoVo) && !StringUtil.isEmpty(userInfoVo.getHeadIcon())) { + identifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(userInfoVo.getHeadIcon())); + } + identifyUserInfo.setUserId(item.getIdentifyUserId()); + identifyUserInfo.setUserName(Objects.isNull(userMap.get(item.getIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getIdentifyUserId()).getUserName()); + //鉴定人职等的组织信息 + List orgInfoAll1VoList = JSON.parseArray(item.getOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(orgInfoAll1VoList)) { + identifyUserInfo.setUserOrgInfoPushVoList(orgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + identifyUserInfos.add(identifyUserInfo); + } + } + applyListVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + applyListVo.setIdentifyUserInfo(identifyUserInfos); + applyListVoList.add(applyListVo); + }); + } + } + return applyListVoList; + } + + @Override + @Transactional + public void applyDataDelete(IdentifyTableInfoDto req) { + Set deleteIds = new LinkedHashSet<>(); + List deleteApplyList = CollectionUtil.newArrayList(); + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + deleteIds.add(apply.getId()); + apply.setDeleteMark(ConstantUtil.NUM_TRUE); + deleteApplyList.add(apply); + //判断是否重新鉴定 + if (apply.getIsReIdentify().equals(ConstantUtil.NUM_TRUE)) { + //查询重新鉴定的源数据 + List ids = Splitter.on(",").splitToList(apply.getReIdentifyId()); + deleteIds.addAll(ids); + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getId, ids) + .eq(CultivateIdentifyApply::getIsVisible, 0) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyList)) { + applyList.forEach(item -> { + item.setDeleteMark(ConstantUtil.NUM_TRUE); + }); + deleteApplyList.addAll(applyList); + } + } + //删除实操鉴定申请详情记录 + LambdaQueryWrapper applyDetailsLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApplyDetails::getApplyId, deleteIds) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyDetailsList = this.applyDetailsService.list(applyDetailsLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + applyDetailsList.forEach(item -> { + item.setDeleteMark(ConstantUtil.NUM_TRUE); + }); + } + //删除岗位学习鉴定申请记录id集合(调用岗位学习的方法) + Set sourceIds = new LinkedHashSet<>(); + //删除实操鉴定数据 + if (CollectionUtil.isNotEmpty(deleteApplyList)) { + sourceIds.addAll(deleteApplyList.stream().map(CultivateIdentifyApply::getSourceId).collect(Collectors.toList())); + this.updateBatchById(deleteApplyList); + } + //删除实操鉴定详情 + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.updateBatchById(applyDetailsList); + } + if (CollectionUtil.isNotEmpty(sourceIds)) { + //删除岗位学习鉴定申请记录 + this.cultivateIdentifyService.batchDeleteCorrelationRecord(new ArrayList<>(sourceIds)); + } + } + + @Override + public Integer getPageAppListCount(IdentifyApplyListAppDto req) { + + String userId = userProvider.get().getUserId(); + return this.baseMapper.getPageAppListCount(req, userId); + + } + + @Override + public PageInfo getPageAppList(IdentifyApplyListAppDto req) { + com.github.pagehelper.Page objects = PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + String userId = userProvider.get().getUserId(); + List powerUserIds = new ArrayList<>(); + if (req.getStatus() != null && req.getStatus() >= 0) { + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + powerUserIds = innerPowerUserVO.getUserIds(); + } else if (innerPowerUserVO.getCode().equals(2)) { + powerUserIds.add("-1"); + } + } + List applyListAppDbVoList = this.baseMapper.getPageAppList(req, userId, powerUserIds); + if (CollUtil.isEmpty(applyListAppDbVoList)) { + return new PageInfo<>(); + } + List applyListVoList = CollectionUtil.newArrayList(); + + List refPostId = applyListAppDbVoList.stream().map(IdentifyApplyListAppDbVo::getSourceId).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(refPostId)) { + refPostId.add("-1"); + } + + QueryWrapper positionQueryWrapper = new QueryWrapper<>(); + positionQueryWrapper.lambda() + .in(FtbCultivatePosition::getId, refPostId); + List positionList = ftbCultivatePositionMapper.selectList(positionQueryWrapper); + Map refPostMap = positionList.stream().collect(Collectors.toMap(FtbCultivatePosition::getId, post -> post)); + + if (CollectionUtil.isNotEmpty(applyListAppDbVoList)) { + List subordinateUserInfoVOS = userApiV2Util.listUnderlingTargetUser(userId, null); + List userSubordin = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(subordinateUserInfoVOS)) { + userSubordin = subordinateUserInfoVOS.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + } + //查询到人员名称-服务调用 + List userIdList = new ArrayList<>(); + for (IdentifyApplyListAppDbVo identifyApplyListAppDbVo : applyListAppDbVoList) { + userIdList.add(identifyApplyListAppDbVo.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(identifyApplyListAppDbVo.getIdentifyUserId())) { + String[] split = identifyApplyListAppDbVo.getIdentifyUserId().split(","); + userIdList.addAll(Arrays.asList(split)); + } + } + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIdList, null); + List finalUserSubordin = userSubordin; + applyListVoList.addAll(applyListAppDbVoList.stream().map(item -> { + IdentifyApplyListAppVo applyListAppVo = new IdentifyApplyListAppVo(); + BeanUtils.copyProperties(item, applyListAppVo); + + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + + String beIdentifyUserId = item.getBeIdentifyUserId(); + UserBoundVO beUserInfoVo = userMap.get(beIdentifyUserId); + if (finalUserSubordin.contains(beIdentifyUserId)) { + beIdentifyUserInfo.setSubordinateMark("1"); + } else { + beIdentifyUserInfo.setSubordinateMark("0"); + } + // 获取用户头像 + if (Objects.nonNull(beUserInfoVo) && !StringUtil.isEmpty(beUserInfoVo.getHeadIcon())) { + beIdentifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(beUserInfoVo.getHeadIcon())); + } + beIdentifyUserInfo.setUserId(item.getBeIdentifyUserId()); + beIdentifyUserInfo.setUserName(Objects.isNull(userMap.get(item.getBeIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getBeIdentifyUserId()).getUserName()); + //被鉴定人职等的组织信息 + List beOrgInfoAll1VoList = JSON.parseArray(item.getBeOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(beOrgInfoAll1VoList)) { + beIdentifyUserInfo.setUserOrgInfoPushVoList(beOrgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + FtbCultivatePosition ftbCultivatePosition = refPostMap.get(item.getSourceId()); + if (ObjectUtil.isNotNull(ftbCultivatePosition)) { + String postId = ftbCultivatePosition.getPostId(); + List postList = beOrgInfoAll1VoList.stream().filter(userOrg -> { + return userOrg.getUserPostCode().equals(postId); + }).collect(Collectors.toList()); + beIdentifyUserInfo.setTriggerPostName(CollectionUtil.isNotEmpty(postList) ? postList.get(0).getUserPostName() : null); + } + + } + //鉴定用户信息 + IdentifyUserOrgInfoVo identifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO userInfoVo = userMap.get(item.getIdentifyUserId()); + // 获取用户头像 + if (Objects.nonNull(userInfoVo) && !StringUtil.isEmpty(userInfoVo.getHeadIcon())) { + identifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(userInfoVo.getHeadIcon())); + } + identifyUserInfo.setUserId(item.getIdentifyUserId()); + identifyUserInfo.setUserName(Objects.isNull(userMap.get(item.getIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getIdentifyUserId()).getUserName()); + //鉴定人职等的组织信息 + List orgInfoAll1VoList = JSON.parseArray(item.getOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(orgInfoAll1VoList)) { + identifyUserInfo.setUserOrgInfoPushVoList(orgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + applyListAppVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + applyListAppVo.setIdentifyUserInfo(identifyUserInfo); + return applyListAppVo; + }).collect(Collectors.toList())); + } + //怎么获取总条数 + + PageInfo identifyApplyListAppVoPageInfo = new PageInfo<>(applyListVoList); + identifyApplyListAppVoPageInfo.setTotal(objects.getTotal()); + identifyApplyListAppVoPageInfo.setPageNum(objects.getPageNum()); + identifyApplyListAppVoPageInfo.setPageSize(objects.getPageSize()); + return identifyApplyListAppVoPageInfo; + } + + @Override + @Transactional + public void deleteIds(IdentifyApplyDeleteAppDto req) { + List deleteApplyList = CollectionUtil.newArrayList(); + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getId, req.getIds()) + .eq(CultivateIdentifyApply::getIsVisible, 0) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyList = this.list(applyLambdaQueryWrapper); + ServiceException.notNull(applyList, "未查询到实操鉴定记录"); + Set reDeleteIds = new LinkedHashSet<>(); + if (CollectionUtil.isNotEmpty(applyList)) { + applyList.forEach(item -> { + if (item.getIsReIdentify().equals(ConstantUtil.NUM_TRUE)) { + //查询重新鉴定的源数据 + List ids = Splitter.on(",").splitToList(item.getReIdentifyId()); + reDeleteIds.addAll(ids); + } + item.setDeleteMark(ConstantUtil.NUM_TRUE); + }); + deleteApplyList.addAll(applyList); + } + if (CollectionUtil.isNotEmpty(reDeleteIds)) { + LambdaQueryWrapper reApplyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getId, reDeleteIds) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List reApplyList = this.list(reApplyLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(reApplyList)) { + reApplyList.forEach(item -> { + item.setDeleteMark(ConstantUtil.NUM_TRUE); + }); + deleteApplyList.addAll(reApplyList); + } + } + //删除实操鉴定申请详情记录 + Set deleteIds = new LinkedHashSet<>(CollectionUtil.isNotEmpty(deleteApplyList) ? deleteApplyList.stream().map(CultivateIdentifyApply::getId).collect(Collectors.toList()) : CollectionUtil.newArrayList()); + LambdaQueryWrapper applyDetailsLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApplyDetails::getApplyId, deleteIds) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyDetailsList = this.applyDetailsService.list(applyDetailsLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + applyDetailsList.forEach(item -> { + item.setDeleteMark(ConstantUtil.NUM_TRUE); + }); + } + //删除岗位学习鉴定申请记录id集合(调用岗位学习的方法) + Set sourceIds = new LinkedHashSet<>(); + //删除实操鉴定数据 + if (CollectionUtil.isNotEmpty(deleteApplyList)) { + sourceIds.addAll(deleteApplyList.stream().map(CultivateIdentifyApply::getSourceId).collect(Collectors.toList())); + this.updateBatchById(deleteApplyList); + } + //删除实操鉴定详情 + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.updateBatchById(applyDetailsList); + } + if (CollectionUtil.isNotEmpty(sourceIds)) { + //删除岗位学习鉴定申请记录 + this.cultivateIdentifyService.batchDeleteCorrelationRecord(new ArrayList<>(sourceIds)); + } + } + + @Override + public IdentifyInfoAppVo getIdentifyApplyInfoApp(IdentifyTableInfoDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + LambdaQueryWrapper itemsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, table.getId()); + List identifyItemsList = this.applyDetailsBackupsService.list(itemsLambdaQueryWrapper); + ServiceException.notNull(identifyItemsList, "未查询到实操鉴定项记录"); + IdentifyInfoAppVo infoAppVo = new IdentifyInfoAppVo(); + infoAppVo.setId(apply.getId()); + infoAppVo.setTableName(table.getName()); + infoAppVo.setIdentifyItemSize(identifyItemsList.size()); + BigDecimal sum = BigDecimal.ZERO; + for (CultivateIdentifyApplyDetailsBackups identifyItems : identifyItemsList) { + sum = sum.add(identifyItems.getScore()); + } + infoAppVo.setTotalScore(sum); + //计算合格分数 + BigDecimal qualifiedScore; + if (table.getPassType().equals(ApplyTypeEnum.GDF.getCode())) { + qualifiedScore = table.getPassScore(); + } else { + qualifiedScore = sum.multiply(table.getPassScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + //计算优秀分数 + BigDecimal excellentScore; + if (table.getExcellentType().equals(ApplyTypeEnum.GDF.getCode())) { + excellentScore = table.getExcellentScore(); + } else { + excellentScore = sum.multiply(table.getExcellentScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + infoAppVo.setPassScore(qualifiedScore); + infoAppVo.setExcellentScore(excellentScore); + infoAppVo.setPlanIdentifyTime(Objects.isNull(apply.getPlanIdentifyTime()) ? null : apply.getPlanIdentifyTime()); + infoAppVo.setSource(apply.getSource()); + infoAppVo.setRuleDesc(table.getRuleDesc()); + infoAppVo.setStatus(apply.getStatus()); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(Collections.singletonList(apply.getBeIdentifyUserId()), null); + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO beUserInfoVo = userMap.get(apply.getBeIdentifyUserId()); + // 获取用户头像 + if (Objects.nonNull(beUserInfoVo) && !StringUtil.isEmpty(beUserInfoVo.getHeadIcon())) { + beIdentifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(beUserInfoVo.getHeadIcon())); + } + beIdentifyUserInfo.setUserId(apply.getBeIdentifyUserId()); + beIdentifyUserInfo.setUserName(Objects.isNull(userMap.get(apply.getBeIdentifyUserId())) ? + "未查询到用户" : userMap.get(apply.getBeIdentifyUserId()).getUserName()); + //被鉴定人职等的组织信息 + List beOrgInfoAll1VoList = JSON.parseArray(apply.getBeOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(beOrgInfoAll1VoList)) { + beIdentifyUserInfo.setUserOrgInfoPushVoList(beOrgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + + infoAppVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + // 查询当前申请时上传的文件信息 + LambdaQueryWrapper wrapperFile = Wrappers.lambdaQuery(); + wrapperFile.eq(FtbCultivateFile::getBusinessId, apply.getId()); + List fileLists = ftbCultivateFileService.list(wrapperFile); + LambdaQueryWrapper wrapperFile2 = Wrappers.lambdaQuery(); + wrapperFile2.eq(FtbCultivateFile::getBusinessId, apply.getId() + apply.getBeIdentifyUserId()); + List fileLists2 = ftbCultivateFileService.list(wrapperFile2); + List fileVOS = fileLists.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + List fileVOS2 = fileLists2.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + List file = new ArrayList<>(); + file.addAll(fileVOS2); + file.addAll(fileVOS); + infoAppVo.setFiles(file); + return infoAppVo; + } + + @Override + public IdentifyItemsInfoAppVo getApplyIdentifyItemsInfoApp(IdentifyTableInfoDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + IdentifyItemsInfoAppVo appVo = new IdentifyItemsInfoAppVo(); + appVo.setId(apply.getId()); + LambdaQueryWrapper applyDetailsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetails::getApplyId, apply.getId()) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyDetailsList = this.applyDetailsService.list(applyDetailsLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + List itemsIds = applyDetailsList.stream().map(CultivateIdentifyApplyDetails::getItemsId).collect(Collectors.toList()); + LambdaQueryWrapper identifyItemsLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApplyDetailsBackups::getId, itemsIds); + List itemsList = this.applyDetailsBackupsService.list(identifyItemsLambdaQueryWrapper); + Map fileMap = itemsList.stream().collect(Collectors.toMap(CultivateIdentifyApplyDetailsBackups::getId, a -> a, (k1, k2) -> k1)); + appVo.setIdentifyItemList(applyDetailsList.stream().map(item -> { + IdentifyItemsInfoVo infoVo = new IdentifyItemsInfoVo(); + CultivateIdentifyApplyDetailsBackups identifyItems = fileMap.get(item.getItemsId()); + infoVo.setId(item.getId()); + infoVo.setName(identifyItems.getName()); + infoVo.setScore(identifyItems.getScore()); + return infoVo; + }).collect(Collectors.toList())); + } + return appVo; + } + + @Override + @Transactional + public void applyDataSubmit(IdentifyApplySubmitDto req) { + String identifyUserId = UserProvider.getLoginUserId(); + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + if (identifyUserId.equals(apply.getBeIdentifyUserId())) { + throw new ServiceException("对不起,不能够自己给自己鉴定"); + } + // 重新鉴定先清除之前上传的附件 + if (Objects.equals(apply.getStatus(), ApplyStatusEnum.YJD.getCode())) { + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(); + update.eq(FtbCultivateFile::getBusinessId, apply.getId()); + update.eq(FtbCultivateFile::getType, FileEventDTO.FileType.PRACTICAL_APPRAISAL.getType()); + ftbCultivateFileService.remove(update); + } + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + LambdaQueryWrapper itemsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, table.getId()) + .eq(CultivateIdentifyApplyDetailsBackups::getDeleteMark, ConstantUtil.NUM_FALSE); + List identifyItemsList = this.applyDetailsBackupsService.list(itemsLambdaQueryWrapper); + ServiceException.notNull(identifyItemsList, "未查询到实操鉴定项记录"); + List ids = req.getIdentifyItemList().stream().map(IdentifyApplyItemsSubmitDto::getId).collect(Collectors.toList()); + Map fileMap = req.getIdentifyItemList().stream().collect(Collectors.toMap(IdentifyApplyItemsSubmitDto::getId, a -> a, (k1, k2) -> k1)); + LambdaQueryWrapper applyDetailsLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApplyDetails::getId, ids) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyDetailsList = this.applyDetailsService.list(applyDetailsLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + applyDetailsList.forEach(item -> { + item.setScore(fileMap.get(item.getId()).getIdentifyScore()); + }); + this.applyDetailsService.updateBatchById(applyDetailsList); + } + //变更为已鉴定 + apply.setStatus(ApplyStatusEnum.YJD.getCode()); + apply.setIdentifyTime(new Date()); + apply.setUseTime(req.getUseTime()); + // 变更鉴定人为当前登录用户 + List identifyUserInfoJsonList = CollectionUtil.newArrayList(); + Map> userOrgMap = userApiV2Util.getUserPrimaryBoundBatchCompatible(List.of(identifyUserId), null); + List identifyUserInfoList = userOrgMap.get(identifyUserId); + ServiceException.notNull(!identifyUserInfoList.isEmpty(), "获取鉴定人组织信息失败,鉴定人id:!" + identifyUserId); + identifyUserInfoJsonList.addAll(identifyUserInfoList.stream().map(item -> { + UserOrgInfoAll1Vo all1Vo = new UserOrgInfoAll1Vo(); + all1Vo.setUserOrgName(item.getOrganizeName()); + all1Vo.setUserOrgCode(item.getOrganizeEnCode()); + all1Vo.setUserPostCode(item.getPositionEnCode()); + all1Vo.setUserPostName(item.getPositionName()); + all1Vo.setUserOfficialRankCode(item.getGradeId()); + all1Vo.setUserOfficialRankName(item.getGradeName()); + return all1Vo; + }).collect(Collectors.toList())); + apply.setIdentifyUserId(identifyUserId); + apply.setOfficialRankInfoJson(JSON.toJSONString(identifyUserInfoJsonList)); + apply.setIdentifyOrgList(Joiner.on(",").join(identifyUserInfoList.stream().map( + UserBoundVO::getOrganizeId).distinct().collect(Collectors.toList()))); + /*设置鉴定结果*/ + //鉴定总得分 + BigDecimal identifyTotalScore = BigDecimal.ZERO; + for (CultivateIdentifyApplyDetailsBackups identifyItems : identifyItemsList) { + identifyTotalScore = identifyTotalScore.add(identifyItems.getScore()); + } + BigDecimal totalScore = BigDecimal.ZERO; + for (IdentifyApplyItemsSubmitDto identifyItems : req.getIdentifyItemList()) { + totalScore = totalScore.add(identifyItems.getIdentifyScore()); + } + //计算合格分数 + BigDecimal qualifiedScore; + if (table.getPassType().equals(ApplyTypeEnum.GDF.getCode())) { + qualifiedScore = table.getPassScore(); + } else { + qualifiedScore = identifyTotalScore.multiply(table.getPassScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + //计算优秀分数 + BigDecimal excellentScore; + if (table.getExcellentType().equals(ApplyTypeEnum.GDF.getCode())) { + excellentScore = table.getExcellentScore(); + } else { + excellentScore = identifyTotalScore.multiply(table.getExcellentScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + if (totalScore.compareTo(qualifiedScore) < 0) { + apply.setResult(ApplyResultEnum.BHG.getCode()); + } else if (totalScore.compareTo(qualifiedScore) >= 0 && totalScore.compareTo(excellentScore) < 0) { + apply.setResult(ApplyResultEnum.HG.getCode()); + } else if (totalScore.compareTo(excellentScore) >= 0) { + apply.setResult(ApplyResultEnum.YX.getCode()); + } + this.baseMapper.updateById(apply); + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(req.getFiles()) + .businessTypeID(apply.getId()) + .type(FileEventDTO.FileType.PRACTICAL_APPRAISAL) + .build())); + // 自动颁发证书 + String string = table.getTableId(); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePositionIdentifyResult::getIdentifyId, string); + lambdaQuery.eq(FtbCultivatePositionIdentifyResult::getIdentifyRecordId, apply.getId()); + lambdaQuery.eq(FtbCultivatePositionIdentifyResult::getUserId, apply.getBeIdentifyUserId()); + lambdaQuery.eq(FtbCultivatePositionIdentifyResult::getEnabledMark, 0); + + FtbCultivatePositionIdentifyResult result = ftbCultivatePositionIdentifyResultMapper.selectOne(lambdaQuery); + + + // 实操鉴定触发自动颁发 + if (result == null && apply.getSource() != 4) { + return; + } + String postRankId = null; + if (apply.getSource() != 4) { + //任务鉴定 + postRankId = result.getPostRankId(); + } + + if (apply.getSource() == 2 && (Objects.equals(apply.getResult(), ApplyResultEnum.HG.getCode()) + || Objects.equals(apply.getResult(), ApplyResultEnum.YX.getCode()))) { + // 颁发证书 + // 鉴定表id + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(2) + .postId(postRankId) + .issuanceType(1) + .userId(apply.getBeIdentifyUserId()).build())); + } else if (apply.getSource() == 4 && (Objects.equals(apply.getResult(), ApplyResultEnum.HG.getCode()) + || Objects.equals(apply.getResult(), ApplyResultEnum.YX.getCode()))) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(2) + .taskId(apply.getSourceId()) + .postId(postRankId) + .issuanceType(2) + .userId(apply.getBeIdentifyUserId()).build())); + } else if (apply.getSource() == 1 && (Objects.equals(apply.getResult(), ApplyResultEnum.HG.getCode()) + || Objects.equals(apply.getResult(), ApplyResultEnum.YX.getCode()))) { + //1课程学习鉴定 证书颁发 + +// QueryWrapper courseIdentityQueryWrapper = new QueryWrapper<>(); +// courseIdentityQueryWrapper.lambda() +// .eq(FtbCultivatePositionCourseIdentity::getPostRankId, postRankId) +// .eq(FtbCultivatePositionCourseIdentity::getPostLearnId, apply.getSourceId()); +// FtbCultivatePositionCourseIdentity courseIdentity = ftbCultivatePositionCourseIdentityMapper.selectOne(courseIdentityQueryWrapper); +// + QueryWrapper identifyResultQueryWrapper = new QueryWrapper<>(); + identifyResultQueryWrapper.lambda() + .eq(FtbCultivatePositionIdentifyResult::getIdentifyRecordId, apply.getId()); + FtbCultivatePositionIdentifyResult identifyResult = ftbCultivatePositionIdentifyResultMapper.selectOne(identifyResultQueryWrapper); + + QueryWrapper positionCourseIdentityQueryWrapper = new QueryWrapper<>(); + positionCourseIdentityQueryWrapper.lambda() + .eq(FtbCultivatePositionCourseIdentity::getIdentityId, identifyResult.getIdentifyId()) + .eq(FtbCultivatePositionCourseIdentity::getPostRankId, postRankId) + .eq(FtbCultivatePositionCourseIdentity::getPostLearnId, identifyResult.getPostLearnId()); + + List ftbCultivatePositionCourseIdentityList = ftbCultivatePositionCourseIdentityMapper.selectList(positionCourseIdentityQueryWrapper); + + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(1) + .courseId(ftbCultivatePositionCourseIdentityList.get(0).getCourseId()) + .issuanceType(0) + .postId(postRankId) + .userId(apply.getBeIdentifyUserId()).build() + + )); + } + // 提醒考试是否需要鉴定 + ftbCultivateExamUserService.completeIdentifyCallBack(apply.getBeIdentifyUserId(), postRankId); + // 如果是任务鉴定进行完成时间更新 + if (apply.getSource() == 4) { + ftbCultivateMyLearnTaskInfoService.updateUserFinishTime(apply.getSourceId(), apply.getBeIdentifyUserId(), 2); + } + } + + @Override + @DSTransactional + public void setApplyDataTime(IdentifyApplySetTimeAppDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + apply.setPlanIdentifyTime(DateUtil.stringToDate(req.getTime())); + this.baseMapper.updateById(apply); + } + + @Override + public IdentifyApplyBasicInfoAppVo getApplyBasicInfoApp(IdentifyTableInfoDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + if (apply.getStatus().equals(ApplyStatusEnum.DJD.getCode())) { + ServiceException.isTrue(Boolean.FALSE, "暂无鉴定结果"); + } + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + LambdaQueryWrapper applyDetailsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetails::getApplyId, apply.getId()) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyDetailsList = this.applyDetailsService.list(applyDetailsLambdaQueryWrapper); + List identifyItemsList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + List itemsIds = applyDetailsList.stream().map(CultivateIdentifyApplyDetails::getItemsId).collect(Collectors.toList()); + LambdaQueryWrapper itemsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, table.getId()) + .in(CultivateIdentifyApplyDetailsBackups::getId, itemsIds); + identifyItemsList = this.applyDetailsBackupsService.list(itemsLambdaQueryWrapper); + } + IdentifyApplyBasicInfoAppVo appVo = new IdentifyApplyBasicInfoAppVo(); + appVo.setId(apply.getId()); + appVo.setTableName(table.getName()); + appVo.setIdentifyTime(apply.getIdentifyTime()); + appVo.setIdentifyItemSize(identifyItemsList.size()); + BigDecimal identifyTotalScore = BigDecimal.ZERO; + for (CultivateIdentifyApplyDetailsBackups identifyItems : identifyItemsList) { + identifyTotalScore = identifyTotalScore.add(identifyItems.getScore()); + } + appVo.setTotalScore(identifyTotalScore); + BigDecimal totalScore = BigDecimal.ZERO; + for (CultivateIdentifyApplyDetails identifyItems : applyDetailsList) { + totalScore = totalScore.add(identifyItems.getScore()); + } + appVo.setIdentifyScore(totalScore); + appVo.setUserTime(apply.getUseTime()); + appVo.setStatus(apply.getStatus()); + appVo.setResult(apply.getResult()); + appVo.setSource(apply.getSource()); + appVo.setRuleDesc(table.getRuleDesc()); + //查询鉴定表有多少人使用 + appVo.setUseNumber(this.baseMapper.getTableUseNumber(table.getId())); + List userList = new ArrayList<>(); + userList.add(apply.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + String[] split = apply.getIdentifyUserId().split(","); + userList.addAll(Arrays.asList(split)); + } + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userList, null); + //被鉴定用户信息 + IdentifyUserOrgInfoVo beIdentifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO beUserInfoVo = userMap.get(apply.getBeIdentifyUserId()); + // 获取用户头像 + if (Objects.nonNull(beUserInfoVo) && !StringUtil.isEmpty(beUserInfoVo.getHeadIcon())) { + beIdentifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(beUserInfoVo.getHeadIcon())); + } + beIdentifyUserInfo.setUserId(apply.getBeIdentifyUserId()); + beIdentifyUserInfo.setUserName(Objects.isNull(userMap.get(apply.getBeIdentifyUserId())) ? + "未查询到用户" : userMap.get(apply.getBeIdentifyUserId()).getUserName()); + //被鉴定人职等的组织信息 + List beOrgInfoAll1VoList = JSON.parseArray(apply.getBeOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(beOrgInfoAll1VoList)) { + beIdentifyUserInfo.setUserOrgInfoPushVoList(beOrgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + //鉴定用户信息 + IdentifyUserOrgInfoVo identifyUserInfo = new IdentifyUserOrgInfoVo(); + UserBoundVO userInfoVo = null; + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + String[] split = apply.getIdentifyUserId().split(","); + userInfoVo = userMap.get(split[0]); + } + + // 获取用户头像 + if (Objects.nonNull(userInfoVo) && !StringUtil.isEmpty(userInfoVo.getHeadIcon())) { + identifyUserInfo.setUserHeadIcon(UploaderUtil.uploaderImg(userInfoVo.getHeadIcon())); + } + identifyUserInfo.setUserId(apply.getIdentifyUserId()); + identifyUserInfo.setUserName(Objects.isNull(userMap.get(apply.getIdentifyUserId())) ? + "未查询到用户" : userMap.get(apply.getIdentifyUserId()).getUserName()); + //鉴定人职等的组织信息 + List orgInfoAll1VoList = JSON.parseArray(apply.getOfficialRankInfoJson(), UserOrgInfoAll1Vo.class); + if (CollectionUtil.isNotEmpty(orgInfoAll1VoList)) { + identifyUserInfo.setUserOrgInfoPushVoList(orgInfoAll1VoList.stream().map(orgInfoAll1Vo -> { + UserOrgInfoVo orgInfoVo = new UserOrgInfoVo(); + BeanUtils.copyProperties(orgInfoAll1Vo, orgInfoVo); + return orgInfoVo; + }).collect(Collectors.toList())); + } + appVo.setBeIdentifyUserInfo(beIdentifyUserInfo); + appVo.setIdentifyUserInfo(identifyUserInfo); + return appVo; + } + + @Override + public IdentifyApplyInfoAppVo getApplyInfoApp(IdentifyTableInfoDto req) { + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(apply, "未查询到实操鉴定记录"); + if (apply.getStatus().equals(ApplyStatusEnum.DJD.getCode())) { + ServiceException.isTrue(Boolean.FALSE, "暂无鉴定结果"); + } + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + LambdaQueryWrapper applyDetailsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetails::getApplyId, apply.getId()) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE); + List applyDetailsList = this.applyDetailsService.list(applyDetailsLambdaQueryWrapper); + List identifyItemsList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + List itemsIds = applyDetailsList.stream().map(CultivateIdentifyApplyDetails::getItemsId).collect(Collectors.toList()); + LambdaQueryWrapper itemsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, table.getId()) + .in(CultivateIdentifyApplyDetailsBackups::getId, itemsIds); + identifyItemsList = this.applyDetailsBackupsService.list(itemsLambdaQueryWrapper); + } + IdentifyApplyInfoAppVo appVo = new IdentifyApplyInfoAppVo(); + BigDecimal identifyTotalScore = BigDecimal.ZERO; + for (CultivateIdentifyApplyDetailsBackups identifyItems : identifyItemsList) { + identifyTotalScore = identifyTotalScore.add(identifyItems.getScore()); + } + appVo.setTotalScore(identifyTotalScore); + BigDecimal totalScore = BigDecimal.ZERO; + for (CultivateIdentifyApplyDetails identifyItems : applyDetailsList) { + totalScore = totalScore.add(identifyItems.getScore()); + } + appVo.setScore(totalScore); + if (CollectionUtil.isNotEmpty(applyDetailsList)) { + Map fileMap = identifyItemsList.stream().collect( + Collectors.toMap(CultivateIdentifyApplyDetailsBackups::getId, a -> a, (k1, k2) -> k1)); + appVo.setDetailsList(applyDetailsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups items = fileMap.get(item.getItemsId()); + IdentifyApplyDetailsAppVo applyDetailsAppVo = new IdentifyApplyDetailsAppVo(); + applyDetailsAppVo.setId(item.getId()); + applyDetailsAppVo.setName(items.getName()); + applyDetailsAppVo.setTotalScore(items.getScore()); + applyDetailsAppVo.setScore(item.getScore()); + return applyDetailsAppVo; + }).collect(Collectors.toList())); + } + // 查询当前申请时上传的文件信息 + LambdaQueryWrapper wrapperFile = Wrappers.lambdaQuery(); + wrapperFile.eq(FtbCultivateFile::getBusinessId, apply.getId()); + List fileLists = ftbCultivateFileService.list(wrapperFile); + LambdaQueryWrapper wrapperFile2 = Wrappers.lambdaQuery(); + wrapperFile2.eq(FtbCultivateFile::getBusinessId, apply.getId() + apply.getBeIdentifyUserId()); + List fileLists2 = ftbCultivateFileService.list(wrapperFile2); + List fileVOS = fileLists.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + List fileVOS2 = fileLists2.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + List file = new ArrayList<>(); + file.addAll(fileVOS2); + file.addAll(fileVOS); + appVo.setFiles(file); + return appVo; + } + + @Override + public List getUserIdentifyInfoApi(List ids) { + log.error("查询鉴定IDs:{}", JSONObject.toJSON(ids)); + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getId, ids) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollectionUtil.isEmpty(applyList)) { + return CollectionUtil.newArrayList(); + } + List userList = new ArrayList<>(); + for (CultivateIdentifyApply cultivateIdentifyApply : applyList) { + userList.add(cultivateIdentifyApply.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(cultivateIdentifyApply.getIdentifyUserId())) { + String[] split = cultivateIdentifyApply.getIdentifyUserId().split(","); + userList.addAll(Arrays.asList(split)); + } + } + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userList, null); + LambdaQueryWrapper tableLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApplyTableBackups::getId, applyList.stream().map( + CultivateIdentifyApply::getTableId).collect(Collectors.toList())); + Map tableMap = this.applyTableBackupsService.list(tableLambdaQueryWrapper) + .stream().collect(Collectors.toMap(CultivateIdentifyApplyTableBackups::getId, a -> a, (k1, k2) -> k1)); + return applyList.stream().map(item -> { + IdentifyApplyInfoApiVo infoApiVo = new IdentifyApplyInfoApiVo(); + BeanUtils.copyProperties(item, infoApiVo); + infoApiVo.setIdIdentification(item.getId()); + infoApiVo.setTableName(Objects.isNull(tableMap.get(item.getTableId())) ? + "未查询到鉴定表" : tableMap.get(item.getTableId()).getName()); + if (StringUtil.isNotEmpty(item.getIdentifyUserId())) { + String[] split = item.getIdentifyUserId().split(","); + infoApiVo.setUserName(userMap.get(split[0]).getUserName()); + } else { + infoApiVo.setUserName("未查询到用户"); + } + + infoApiVo.setBeUserName(Objects.isNull(userMap.get(item.getBeIdentifyUserId())) ? + "未查询到用户" : userMap.get(item.getBeIdentifyUserId()).getUserName()); + return infoApiVo; + }).collect(Collectors.toList()); + } + + @Override + public List getUserIdentifyInfoWithUserId(String userId) { + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getBeIdentifyUserId, userId) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollectionUtil.isEmpty(applyList)) { + return CollectionUtil.newArrayList(); + } + List allUserIds = new ArrayList<>(); + for (CultivateIdentifyApply cultivateIdentifyApply : applyList) { + if (StringUtil.isNotEmpty(cultivateIdentifyApply.getIdentifyUserId())) { + allUserIds.addAll(Arrays.asList(cultivateIdentifyApply.getIdentifyUserId().split(","))); + } + if (StringUtil.isNotEmpty(cultivateIdentifyApply.getBeIdentifyUserId())) { + allUserIds.add(cultivateIdentifyApply.getBeIdentifyUserId()); + } + } + Map infoMapByIds = userApiV2Util.getUserPrimaryBoundBatch(allUserIds, null); + Map userIdMap = cultivatePerUtils.coverPersonalIds(allUserIds); + + LambdaQueryWrapper tableLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApplyTableBackups::getId, applyList.stream().map( + CultivateIdentifyApply::getTableId).collect(Collectors.toList())); + Map tableMap = this.applyTableBackupsService.list(tableLambdaQueryWrapper) + .stream().collect(Collectors.toMap(CultivateIdentifyApplyTableBackups::getId, a -> a, (k1, k2) -> k1)); + return applyList.stream().map(item -> { + IdentifyApplyInfoApiVo infoApiVo = new IdentifyApplyInfoApiVo(); + BeanUtils.copyProperties(item, infoApiVo); + infoApiVo.setIdIdentification(item.getId()); + infoApiVo.setTableName(Objects.isNull(tableMap.get(item.getTableId())) ? + "未查询到鉴定表" : tableMap.get(item.getTableId()).getName()); + + if (StringUtil.isNotEmpty(item.getIdentifyUserId())) { + String[] split = item.getIdentifyUserId().split(","); + List tmpNameList = new ArrayList<>(); + List workerIds = new ArrayList<>(); + for (String s : split) { + UserBoundVO userBoundVO = infoMapByIds.get(s); + if (userBoundVO != null) { + tmpNameList.add(userBoundVO.getUserName()); + } + String workerId = userIdMap.get(s); + if (StringUtil.isNotEmpty(workerId)) { + workerIds.add(workerId); + } + } + if (CollUtil.isNotEmpty(tmpNameList)) { + infoApiVo.setUserName(String.join(",", tmpNameList)); + infoApiVo.setSysUserId(String.join(",", workerIds)); + } else { + infoApiVo.setUserName("未查询到用户"); + } + } else { + infoApiVo.setUserName("未查询到用户"); + } + if (StringUtil.isNotEmpty(item.getBeIdentifyUserId())) { + UserBoundVO userBoundVO = infoMapByIds.get(item.getBeIdentifyUserId()); + if (userBoundVO != null) { + infoApiVo.setBeUserName(userBoundVO.getUserName()); + } else { + infoApiVo.setBeUserName("未查询到用户"); + } + } + return infoApiVo; + }).collect(Collectors.toList()); + } + + @Override + public IdentifyStatisticsForNewVo getHandsOnStatistics(FtbCultivateStatisticsDTO dto) { + IdentifyStatisticsForNewVo statisticsVo = new IdentifyStatisticsForNewVo(); + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.in(CultivateIdentifyApply::getBeIdentifyUserId, dto.getUserIds()); + if (dto.getStartTime() != null && dto.getEndTime() != null) { + query.between(CultivateIdentifyApply::getIdentifyTime, dto.getStartTime(), dto.getEndTime()); + } + query.eq(CultivateIdentifyApply::getStatus, ApplyStatusEnum.YJD.getCode()); + query.eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + query.eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(query); + if (CollUtil.isEmpty(applyList)) { + return statisticsVo; + } + // 总人数 + long rowNumber = applyList.stream().map(CultivateIdentifyApply::getBeIdentifyUserId).count(); + statisticsVo.setTotalPeople(rowNumber); + // 合格人数 + long passNumber = applyList.stream().filter(item -> item.getResult() != null && item.getResult() != 2).count(); + statisticsVo.setPassNumber(passNumber); + // 合格率 = 合格人数 / 总人数 + BigDecimal passRate = BigDecimal.valueOf(passNumber).divide(BigDecimal.valueOf(rowNumber), 2, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP); + statisticsVo.setPassRate(passRate); + return statisticsVo; + } + + @Override + public Boolean whetherTheUserPositionHasBeenIdentified(String userId, String positionId) { + // 是否存在岗位学习鉴定 + List strings = promotionNewMapper.checkWhetherThereIsAPracticalAppraisalWithNew(positionId, userId); + if (CollUtil.isEmpty(strings)) { + return null; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CultivateIdentifyApply::getStatus, 1); + wrapper.eq(CultivateIdentifyApply::getSource, 2); + wrapper.in(CultivateIdentifyApply::getTableId, strings); + wrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + Long aLong = baseMapper.selectCount(wrapper); + return aLong > 0; + } + + @Override + public void deleteLearnTaskApplyIdentified(String taskId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(CultivateIdentifyApply::getId); + queryWrapper.eq(CultivateIdentifyApply::getSourceId, taskId); +// queryWrapper.last("limit 1"); + // 查询主键 + List list = baseMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(list)) { + return; + } + for (CultivateIdentifyApply cultivateIdentifyApply : list) { + LambdaQueryWrapper listWrapper = Wrappers.lambdaQuery(); + String id = cultivateIdentifyApply.getId(); + listWrapper.eq(CultivateIdentifyApplyDetails::getApplyId, id); + // 删除详情 + applyDetailsMapper.delete(listWrapper); + // 删除主键 + baseMapper.deleteById(id); + } + + } + + @Override + public Map getUserIdentifyStatistics(List userIds) { + List statisticsApiVoList = this.baseMapper.getUserIdentifyStatistics(userIds); + if (CollectionUtil.isNotEmpty(statisticsApiVoList)) { + return statisticsApiVoList.stream().collect(Collectors.toMap(IdentifyApplyStatisticsApiVo::getUserId, a -> a, (k1, k2) -> k1)); + } else { + return new HashMap<>(); + } + } + + @Override + public PageListVO getUserIdentifyPage(String userId, CultivatePage page) { + Page objectPage = page.coverCultivatePage(); + Page list = this.baseMapper.getUserIdentifyPage(objectPage, userId); + List records = list.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + //查询到人员名称-服务调用 + List userIdList = records.stream().map(UserIdentifyPageVo::getUserId).collect(Collectors.toList()); +// Map userMap = userApi.getInfoMapByIds(userIdList); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIdList, null); + records.forEach(item -> { + UserBoundVO tempUser = userMap.get(item.getUserId()); + if (tempUser != null) { + item.setUserName(tempUser.getName()); + } else { + item.setUserName("未查询到用户"); + } + String identifyUserId = item.getIdentifyUserId(); + if (StringUtil.isNotEmpty(identifyUserId)) { + String[] split = identifyUserId.split(","); + if (split.length > 0) { +// List infoByIdsMany = userApi.getInfoByIdsMany(List.of(split)); + List infoByIdsMany = userApiV2Util.getUserPrimaryBoundBatchReturnList(List.of(split), null); + if (CollUtil.isNotEmpty(infoByIdsMany)) { + item.setIdentifyUserName(infoByIdsMany.stream().map(UserBoundVO::getUserName).collect(Collectors.joining(","))); + } + } + } + }); + } + return CultivatePage.coverPageList(list); + } + + @Override + public IdentifyStatisticsVo getIdentifyStatistics(FtbCultivateStatisticsDTO req) { + IdentifyStatisticsVo statisticsVo = new IdentifyStatisticsVo(); + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getBeIdentifyUserId, req.getUserIds()) + .between(CultivateIdentifyApply::getIdentifyTime, req.getStartTime(), req.getEndTime()) + .eq(CultivateIdentifyApply::getStatus, ApplyStatusEnum.YJD.getCode()) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyList)) { + Map> stringListMap = applyList.stream().collect(Collectors.groupingBy(CultivateIdentifyApply::getBeIdentifyUserId)); + //鉴定总人数 + statisticsVo.setTotalPeople(stringListMap.size()); + //鉴定总次数 + statisticsVo.setTotalNumber(applyList.size()); + //合格次数 + List hgApplyList = applyList.stream().filter(m -> !m.getResult().equals(ApplyResultEnum.BHG.getCode())).collect(Collectors.toList()); + statisticsVo.setPassNumber(CollectionUtil.isNotEmpty(hgApplyList) ? hgApplyList.size() : 0); + //合格率 + statisticsVo.setPassRate(CollectionUtil.isNotEmpty(hgApplyList) ? + new BigDecimal(statisticsVo.getPassNumber()).divide( + new BigDecimal(statisticsVo.getTotalNumber()), 2, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)) : + BigDecimal.ZERO); + //不合格次数 + List bhgApplyList = applyList.stream().filter(m -> m.getResult().equals(ApplyResultEnum.BHG.getCode())).collect(Collectors.toList()); + statisticsVo.setNoPassNumber(CollectionUtil.isNotEmpty(bhgApplyList) ? bhgApplyList.size() : 0); + //不合格率 + statisticsVo.setNoPassRate(CollectionUtil.isNotEmpty(bhgApplyList) ? + new BigDecimal(statisticsVo.getNoPassNumber()).divide( + new BigDecimal(statisticsVo.getTotalNumber()), 2, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)) : BigDecimal.ZERO); + } + //查询年度合格率 + Calendar calendar = Calendar.getInstance(); + int year = calendar.get(Calendar.YEAR); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, Calendar.JANUARY); + calendar.set(Calendar.DAY_OF_MONTH, 1); + long startTime = calendar.getTimeInMillis(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, Calendar.DECEMBER); + calendar.set(Calendar.DAY_OF_MONTH, 31); + long endTime = calendar.getTimeInMillis(); + LambdaQueryWrapper annualApplyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getBeIdentifyUserId, req.getUserIds()) + .between(CultivateIdentifyApply::getIdentifyTime, new Date(startTime), new Date(endTime)) + .eq(CultivateIdentifyApply::getStatus, ApplyStatusEnum.YJD.getCode()) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List annualApplyList = this.list(annualApplyLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(annualApplyList)) { + //鉴定总次数 + int yearTotal = annualApplyList.size(); + //合格次数 + List hgApplyList = annualApplyList.stream().filter(m -> !m.getResult().equals(ApplyResultEnum.BHG.getCode())).collect(Collectors.toList()); + int yearPass = CollectionUtil.isNotEmpty(hgApplyList) ? hgApplyList.size() : 0; + //合格率 + statisticsVo.setAnnualPassRate(CollectionUtil.isNotEmpty(hgApplyList) ? + new BigDecimal(yearPass).divide( + new BigDecimal(yearTotal), 2, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)) : + BigDecimal.ZERO); + } + return statisticsVo; + } + + @Override + public List getIdentifyTop(FtbCultivateStatisticsDTO req) { + List topVoList = new ArrayList<>(); + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .in(CultivateIdentifyApply::getBeIdentifyUserId, req.getUserIds()) + .isNotNull(CultivateIdentifyApply::getIdentifyTime) + .between(CultivateIdentifyApply::getIdentifyTime, req.getStartTime(), req.getEndTime()) + .eq(CultivateIdentifyApply::getStatus, ApplyStatusEnum.YJD.getCode()) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollUtil.isEmpty(applyList)) { + return topVoList; + } + + Map userMap = userApiV2Util.getUserNameAndCopyForUserIds(applyList.stream().map(CultivateIdentifyApply::getBeIdentifyUserId).distinct().collect(Collectors.toList())); + Map> stringListMap = applyList.stream().collect(Collectors.groupingBy(CultivateIdentifyApply::getBeIdentifyUserId)); + for (Map.Entry> entry : stringListMap.entrySet()) { + IdentifyTopVo topVo = new IdentifyTopVo(); + topVo.setUserId(entry.getKey()); + topVo.setUserName(Objects.isNull(userMap.get(entry.getKey())) ? + "未查询到用户" : userMap.get(entry.getKey()).getRealName()); + List identifyApplyList = entry.getValue(); + //合格率 + List htApplyList = identifyApplyList.stream().filter(m -> !m.getResult().equals(ApplyResultEnum.BHG.getCode())).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(htApplyList)) { + topVo.setPassRate(new BigDecimal(htApplyList.size()).divide(new BigDecimal(identifyApplyList.size()), 2, RoundingMode.HALF_UP)); + } + topVoList.add(topVo); + } + //按照合格率降序排列 + topVoList = topVoList.stream().sorted(Comparator.comparing(IdentifyTopVo::getPassRate).reversed()).collect(Collectors.toList()); + + return topVoList; + } + + @Override + @Transactional + public boolean setIdentifyBeOverdue() { + //查询设置了计划鉴定时间并且到期未鉴定 + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .isNotNull(CultivateIdentifyApply::getPlanIdentifyTime) + .lt(CultivateIdentifyApply::getPlanIdentifyTime, DateUtil.getNowDate()) + .eq(CultivateIdentifyApply::getStatus, ApplyStatusEnum.DJD.getCode()) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyList)) { + applyList.forEach(item -> { + item.setStatus(ApplyStatusEnum.YQWJD.getCode()); + item.setLastModifyTime(new Date()); + }); + this.updateBatchById(applyList); + } + return true; + } + + @Override + @Transactional + public boolean setIdentifyRemind(String tenantId) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime futureTime = now.plusHours(2); + Date date = Date.from(futureTime.atZone(ZoneId.systemDefault()).toInstant()); + LambdaQueryWrapper applyLambdaQueryWrapper = new LambdaQueryWrapper() + .isNotNull(CultivateIdentifyApply::getPlanIdentifyTime) + .le(CultivateIdentifyApply::getPlanIdentifyTime, date) + .eq(CultivateIdentifyApply::getIsRemind, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getStatus, ApplyStatusEnum.DJD.getCode()) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0); + List applyList = this.list(applyLambdaQueryWrapper); + if (CollectionUtil.isNotEmpty(applyList)) { + log.error("ID分别为:{}", JSONObject.toJSON(applyList.stream().map(CultivateIdentifyApply::getId).collect(Collectors.toList()))); + SentMessageIdentify req = new SentMessageIdentify(); + List infoList = new ArrayList<>(); + applyList.forEach(item -> { + item.setIsRemind(ConstantUtil.NUM_TRUE); + SentMessageInfo messageInfo = new SentMessageInfo(); + String identifyUserId = item.getIdentifyUserId(); + messageInfo.setToUserId(identifyUserId); + UserInfoVO bossByUserId = userApi.getBossByUserId(identifyUserId); + if (ObjectUtil.isNotEmpty(bossByUserId)) { + messageInfo.setTitle(String.format("请去鉴定%s的鉴定申请", bossByUserId.getRealName())); + } + infoList.add(messageInfo); + }); + req.setInfoList(infoList); + req.setBodyText("鉴定提醒"); + req.setSource(2); + req.setType(1); + req.setUserId(applyList.get(0).getIdentifyUserId()); + req.setTenantId(tenantId); + log.error("鉴定提醒消息发送请求参数:{}", JSONObject.toJSON(req)); + sentMessageApi.sentMessageIdentify(req); + return this.updateBatchById(applyList); + } else { + return true; + } + } + + @Override + public Map groupIdentifyCountNum(List taskIds, Integer type) { + Map ret = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return ret; + } + List list = baseMapper.groupIdentifyCountNum(taskIds, type); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + @Override + public List queryIdentifyApply(String userId, List identifyIds, Integer source, String sourceId) { + return baseMapper.queryIdentifyApply(userId, identifyIds, source, sourceId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyTableBackupsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyTableBackupsServiceImpl.java new file mode 100644 index 0000000..db98b51 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyApplyTableBackupsServiceImpl.java @@ -0,0 +1,18 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateIdentifyApplyTableBackupsMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyTableBackupsService; +import jnpf.entity.cultivate.CultivateIdentifyApplyTableBackups; +import org.springframework.stereotype.Service; + +/** + * 实操鉴定申请_关联_鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Service("CultivateIdentifyApplyTableBackupsService") +public class CultivateIdentifyApplyTableBackupsServiceImpl extends ServiceImpl implements CultivateIdentifyApplyTableBackupsService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyItemsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyItemsServiceImpl.java new file mode 100644 index 0000000..f878b65 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyItemsServiceImpl.java @@ -0,0 +1,34 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateIdentifyItemsMapper; +import jnpf.cultivate.service.CultivateIdentifyItemsService; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.vo.identify.IdentifyItemsWithCategoryVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 实操鉴定项表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Slf4j +@Service("cultivateIdentifyItemsService") +public class CultivateIdentifyItemsServiceImpl extends ServiceImpl implements CultivateIdentifyItemsService { + + @Override + public List countForTableId(List tableIdList) { + return baseMapper.countForTableId(tableIdList); + } + + @Override + public List listWithCategory(String tableId) { + return baseMapper.listWithCategory(tableId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyTableServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyTableServiceImpl.java new file mode 100644 index 0000000..ccf3198 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/CultivateIdentifyTableServiceImpl.java @@ -0,0 +1,546 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.CultivateIdentifyTableMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskIdentificationMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyApplyTableBackups; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.enums.cultivate.ApplyStatusEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.identify.*; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.identify.*; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.UserApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * 实操鉴定表 + * + * @author shitou + * @email shitou@niujiekeji.com + * @date 2023-12-19 11:10:53 + */ +@Slf4j +@Service("cultivateIdentifyTableService") +public class CultivateIdentifyTableServiceImpl extends ServiceImpl implements CultivateIdentifyTableService { + private static final List EMPTY_USER_LIST = List.of("-1"); + + @Autowired + private UserApi userApi; + @Autowired + private UserApiV2Util userApiV2Util; + @Autowired + private CultivateIdentifyItemsService itemsService; + @Autowired + private CultivateIdentifyApplyService applyService; + @Autowired + private PositionCultivateIdentifyService cultivateIdentifyService; + @Autowired + private CultivateIdentifyApplyTableBackupsService applyTableBackupsService; + @Autowired + private CultivatePerUtils cultivatePerUtils; + + @Autowired + private FtbCultivateLearnTaskIdentificationMapper ftbCultivateLearnTaskIdentificationMapper; + + @Autowired + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Override + public PageInfo getPageList(IdentifyTableListDto req) { + PageHelper.startPage(Math.toIntExact(req.getCurrentPage()), Math.toIntExact(req.getPageSize())); + return new PageInfo<>(this.baseMapper.getPageList(req)); + } + + @Override + public IdentifyTableInfoVo getInfo(IdentifyTableInfoDto req) { + CultivateIdentifyTable table = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(table, "未查询到鉴定表记录"); + IdentifyTableInfoVo tableInfoVo = new IdentifyTableInfoVo(); + BeanUtils.copyProperties(table, tableInfoVo); + tableInfoVo.setId(table.getId()); + tableInfoVo.setIdentifyItemList(this.itemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, table.getId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list().stream().map(items -> { + IdentifyItemsInfoVo infoVo = new IdentifyItemsInfoVo(); + BeanUtils.copyProperties(items, infoVo); + infoVo.setId(items.getId()); + return infoVo; + }).collect(Collectors.toList())); + return tableInfoVo; + } + + @Override + @DSTransactional + public void saveData(IdentifyTableSaveDto req) { + OptionalUtils.ifPresent(req.getName(), (name) -> { + ServiceException.isTrue(this.lambdaQuery() + .eq(CultivateIdentifyTable::getName, name) + .eq(CultivateIdentifyTable::getDeleteMark, ConstantUtil.NUM_FALSE) + .count() <= 0, String.format("鉴定表%s已存在,请勿重复添加", name)); + }); + CultivateIdentifyTable tableSave = new CultivateIdentifyTable(); + tableSave.setId(RandomUtil.uuId()); + tableSave.setRuleId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.IDENTIFY)); + BeanUtils.copyProperties(req, tableSave); + tableSave.setCreatorTime(new Date()); + tableSave.setLastModifyTime(new Date()); + ServiceException.isTrue(CollectionUtil.isNotEmpty(req.getIdentifyItemList()), "未查询到鉴定表记录"); + //批量新增鉴定项 + List batchSaveItemsList = req.getIdentifyItemList().stream().map(p -> { + CultivateIdentifyItems identifyItems = new CultivateIdentifyItems(); + identifyItems.setId(RandomUtil.uuId()); + identifyItems.setTableId(tableSave.getId()); + BeanUtils.copyProperties(p, identifyItems); + return identifyItems; + }).collect(Collectors.toList()); + this.baseMapper.insert(tableSave); + if (CollectionUtil.isNotEmpty(batchSaveItemsList)) { + this.itemsService.saveBatch(batchSaveItemsList); + } + } + + @Override + @DSTransactional + public void updateData(IdentifyTableUpdateDto req) { + CultivateIdentifyTable tableUpdate = this.baseMapper.selectById(req.getId()); + ServiceException.notNull(tableUpdate, "未查询到鉴定表记录"); + if (!tableUpdate.getName().equals(req.getName())) { + OptionalUtils.ifPresent(req.getName(), (name) -> { + ServiceException.isTrue(this.lambdaQuery() + .eq(CultivateIdentifyTable::getName, name) + .eq(CultivateIdentifyTable::getDeleteMark, ConstantUtil.NUM_FALSE) + .count() <= 0, String.format("鉴定表%s已存在,请勿重复添加", name)); + }); + } + BeanUtils.copyProperties(req, tableUpdate); + this.baseMapper.updateById(tableUpdate); + //批量修改鉴定项 + List batchUpdateItemsList = this.itemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, tableUpdate.getId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list(); + Map identifyItemsMap = req.getIdentifyItemList().stream().filter(m -> StringUtil.isNotEmpty(m.getId())) + .collect(Collectors.toMap(IdentifyItemsUpdateVo::getId, a -> a, (k1, k2) -> k1)); + if (CollectionUtil.isNotEmpty(batchUpdateItemsList)) { + batchUpdateItemsList.forEach(item -> { + IdentifyItemsUpdateVo updateVo = identifyItemsMap.get(item.getId()); + if (Objects.isNull(updateVo)) { + item.setDeleteMark(ConstantUtil.NUM_TRUE); + } else { + BeanUtils.copyProperties(updateVo, item); + } + }); + this.itemsService.updateBatchById(batchUpdateItemsList); + } + //处理新增鉴定项 + List addVoList = req.getIdentifyItemList().stream().filter(m -> StringUtil.isEmpty(m.getId())).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(addVoList)) { + this.itemsService.saveBatch(addVoList.stream().map(item -> { + CultivateIdentifyItems identifyItems = new CultivateIdentifyItems(); + identifyItems.setId(RandomUtil.uuId()); + BeanUtils.copyProperties(item, identifyItems); + identifyItems.setTableId(tableUpdate.getId()); + return identifyItems; + }).collect(Collectors.toList())); + } + } + + @Override + @DSTransactional + public IdentifyTableDeleteVo deleteData(IdentifyTableInfoDto req) { + IdentifyTableDeleteVo tableDeleteVo = new IdentifyTableDeleteVo(); + CultivateIdentifyTable tableDelete = this.baseMapper.selectById(req.getId()); + // 任务必修课程是否全部学完 + ServiceException.notNull(tableDelete, "未查询到鉴定表记录"); + + + LambdaQueryWrapper taskIdenWrapper = Wrappers.lambdaQuery(); + taskIdenWrapper.eq(FtbCultivateLearnTaskIdentification::getIdentificationId, req.getId()); + taskIdenWrapper.eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + Long taskIdentNum = ftbCultivateLearnTaskIdentificationMapper.selectCount(taskIdenWrapper); + if (taskIdentNum > 0) { + throw new RuntimeException("有培训任务配置了该鉴定表,不能删除"); + } + List applyTableBackupsList = this.applyTableBackupsService.lambdaQuery() + .eq(CultivateIdentifyApplyTableBackups::getTableId, tableDelete.getId()) + .eq(CultivateIdentifyApplyTableBackups::getDeleteMark, ConstantUtil.NUM_FALSE).list(); + List manuallyInitiateVoList = CollectionUtil.newArrayList(); + List postAndCourseVoList = CollectionUtil.newArrayList(); + //判断手动发起的鉴定是否有未走完流程的数据 + if (CollectionUtil.isNotEmpty(applyTableBackupsList)) { + List tableIds = applyTableBackupsList.stream().map(CultivateIdentifyApplyTableBackups::getId).distinct().collect(Collectors.toList()); + List applyList = this.applyService.lambdaQuery() + .in(CultivateIdentifyApply::getTableId, tableIds) + .eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultivateIdentifyApply::getIsVisible, 0) + .ne(CultivateIdentifyApply::getStatus, ApplyStatusEnum.YJD.getCode()).list(); + if (CollectionUtil.isNotEmpty(applyList)) { + //查询到人员名称-服务调用 + List userIdList = applyList.stream().map(CultivateIdentifyApply::getBeIdentifyUserId).collect(Collectors.toList()); + Map userMap = userApi.getInfoMapByIds(userIdList); + manuallyInitiateVoList = applyList.stream().map(apply -> { + ManuallyInitiateVo manuallyInitiateVo = new ManuallyInitiateVo(); + manuallyInitiateVo.setName(apply.getName()); + manuallyInitiateVo.setUserName(Objects.isNull(userMap.get(apply.getBeIdentifyUserId())) ? "未找到该用户" : + userMap.get(apply.getBeIdentifyUserId()).getRealName()); + return manuallyInitiateVo; + }).collect(Collectors.toList()); + tableDeleteVo.setManuallyInitiateVoList(manuallyInitiateVoList); + } + } + + List correlationVoList = cultivateIdentifyService.selectCorrelationDataList(tableDelete.getId()); + if (CollectionUtil.isNotEmpty(correlationVoList)) { + postAndCourseVoList.addAll(correlationVoList.stream().map(item -> { + PostAndCourseVo vo = new PostAndCourseVo(); + vo.setPost(item.getPostName()); + vo.setOfficialRank(item.getOfficialRankName()); + vo.setCourseName(item.getCourseName()); + return vo; + }).collect(Collectors.toList())); + } + tableDeleteVo.setPostAndCourseVoList(postAndCourseVoList); + if (CollectionUtil.isEmpty(manuallyInitiateVoList) && CollectionUtil.isEmpty(postAndCourseVoList)) { + tableDelete.setDeleteMark(ConstantUtil.NUM_TRUE); + this.baseMapper.updateById(tableDelete); + //删除鉴定项 + List identifyItemsList = this.itemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, tableDelete.getId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE) + .list(); + if (CollectionUtil.isNotEmpty(identifyItemsList)) { + identifyItemsList.forEach(item -> { + item.setDeleteMark(ConstantUtil.NUM_TRUE); + }); + this.itemsService.updateBatchById(identifyItemsList); + } + } + return tableDeleteVo; + } + + @Override + public List getTableListInfo(List ids) { + List tableList = this.lambdaQuery() + .in(CultivateIdentifyTable::getId, ids) + .list(); + if (CollectionUtil.isNotEmpty(tableList)) { + return tableList.stream().map(item -> { + IdentifyTableInfoApiVo identifyTable = new IdentifyTableInfoApiVo(); + BeanUtils.copyProperties(item, identifyTable); + identifyTable.setId(item.getId()); + return identifyTable; + }).collect(Collectors.toList()); + } + return CollectionUtil.newArrayList(); + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbIdentityOrgWisdomStatisticDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO organizationListStatistics(FtbIdentityOrgWisdomStatisticDTO statisticDTO) { + Page page = statisticDTO.coverCultivatePage(); + List orgIds = new ArrayList<>(); + + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return returnEmptyList(statisticDTO); + } + // 勾选 + + if ("1".equals(statisticDTO.getSelectLogo()) && CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List allOrg = new ArrayList<>(); + allOrg.addAll(statisticDTO.getOrgId()); + for (String orgId : statisticDTO.getOrgId()) { + List allOrganize = cultivatePerUtils.getAllOrganize(orgId); + if (CollUtil.isNotEmpty(allOrganize)) { + allOrg.addAll(allOrganize); + } + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, allOrg); + if (CollUtil.isNotEmpty(intersection)) { + orgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + + } else if (CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List intersection = UserApiV2Util.getIntersection(powerOrgList, statisticDTO.getOrgId()); + if (CollUtil.isNotEmpty(intersection)) { + orgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + } else { + orgIds = powerOrgList; + } + orgIds = UserApiV2Util.uniqueStringList(orgIds); + Map> userListForOrgIdsReturnMap = userApiV2Util.getUserListForOrgIdsReturnMap(orgIds, null); + List organizeEntityList = userApiV2Util.organizesByOrganizeIds(orgIds, null); + Map orgNameMap = organizeEntityList.stream().collect(Collectors + .toMap(OrganizeGeneralDetailVO::getId, OrganizeGeneralDetailVO::getName, (a, b) -> a)); + Map orgIdMap = cultivatePerUtils.convertOrganizationalId(orgIds); + List records = new ArrayList<>(); + for (String orgId : orgIds) { + List listFeign = userListForOrgIdsReturnMap.get(orgId); + if (CollUtil.isNotEmpty(listFeign)) { + List userIds = listFeign.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + statisticDTO.setUserIds(userIds); + } else { + log.error("统计,获取组织id:{},所对应的人员为空", orgId); + statisticDTO.setUserIds(EMPTY_USER_LIST); + } + List list = baseMapper.organizationListStatisticsForUser(statisticDTO); + list.forEach(item -> { + if (orgIdMap.containsKey(orgId)) { + item.setSysOrganizationID(orgIdMap.get(orgId)); + } + if (orgNameMap.containsKey(orgId)) { + item.setNameOfAssociation(orgNameMap.get(orgId)); + } + item.setOrganizationID(orgId); + // 岗位id +// ActionResult> listByOrganizeIds = positionApi.getListByOrganizeIds(orgId); + List listByOrganizeIds = userApiV2Util.listPositionBaseInfoByIds(List.of(orgId), null); + List postionIds = listByOrganizeIds + .stream() + .map(PositionBaseInfoVO::getId) + .collect(Collectors.toList()); + // 岗位学习应鉴定人数 + if (CollUtil.isEmpty(postionIds)) { + item.setNumberOfAppraisers(0L); + } else { + List numberOfAppraiserss = baseMapper.numberOfAppraisers(postionIds, item.getTableId()); + Long numberOfAppraisers = 0L; + for (String numberOfAppraiser : numberOfAppraiserss) { + ActionResult userCount = userApi.getUserCount(orgId, numberOfAppraiser); + if (userCount != null && userCount.getCode() == 200) { + numberOfAppraisers = numberOfAppraisers + userCount.getData(); + } + } + item.setNumberOfAppraisers(numberOfAppraisers); + } + }); + records.addAll(list); + } + return CultivatePage.paginate(records, page); + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbIdentityPersonWisdomStatisticDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO personListStatistics(FtbIdentityPersonWisdomStatisticDTO personWisdomStatisticDTO) { + if (StringUtil.isNotEmpty(personWisdomStatisticDTO.getPostId())) { + List partUserInfoVos = userApiV2Util.getUserListForPositions(List.of(personWisdomStatisticDTO.getPostId()), null); + if (CollUtil.isNotEmpty(partUserInfoVos)) { + List collect = partUserInfoVos.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + personWisdomStatisticDTO.setSelectPeoples(collect); + } else { + List intersection = UserApiV2Util.getIntersection(collect, personWisdomStatisticDTO.getSelectPeoples()); + if (CollUtil.isNotEmpty(intersection)) { + personWisdomStatisticDTO.setSelectPeoples(intersection); + } else { + return returnEmptyList(personWisdomStatisticDTO); + } + } + } else { + return returnEmptyList(personWisdomStatisticDTO); + } + } + // 人员跳转问题 + if (StringUtil.isNotEmpty(personWisdomStatisticDTO.getOrganizationID())) { + List listFeign = userApiV2Util.getUserListForOrgIds(List.of(personWisdomStatisticDTO.getOrganizationID()), null); + if (CollUtil.isNotEmpty(listFeign)) { + List userIds = listFeign.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + personWisdomStatisticDTO.setSelectPeoples(userIds); + } else { + List intersection = UserApiV2Util.getIntersection(userIds, personWisdomStatisticDTO.getSelectPeoples()); + if (CollUtil.isNotEmpty(intersection)) { + personWisdomStatisticDTO.setSelectPeoples(intersection); + } else { + return returnEmptyList(personWisdomStatisticDTO); + } + } + } else { + log.error("统计,获取组织id:{},所对应的人员为空", personWisdomStatisticDTO.getOrganizationID()); + return returnEmptyList(personWisdomStatisticDTO); + } + } + // + if (StringUtil.isNotEmpty(personWisdomStatisticDTO.getKeyWords())) { + List keywordUserIds = userApiV2Util.matchUserNameOrSystemId(personWisdomStatisticDTO.getKeyWords()); + if (CollUtil.isEmpty(keywordUserIds)) { + return returnEmptyList(personWisdomStatisticDTO); + } + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + List intersection = UserApiV2Util.getIntersection(keywordUserIds, personWisdomStatisticDTO.getSelectPeoples()); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(personWisdomStatisticDTO); + } + personWisdomStatisticDTO.setSelectPeoples(intersection); + } else { + personWisdomStatisticDTO.setSelectPeoples(keywordUserIds); + } + } + + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + List intersection = UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), personWisdomStatisticDTO.getSelectPeoples()); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(personWisdomStatisticDTO); + } else { + personWisdomStatisticDTO.setSelectPeoples(intersection); + } + } else { + personWisdomStatisticDTO.setSelectPeoples(innerPowerUserVO.getUserIds()); + } + } else if (innerPowerUserVO.getCode() == 2) { + return returnEmptyList(personWisdomStatisticDTO); + } +// personWisdomStatisticDTO.setLeaverUserIds(userApiV2Util.queryLeaveUserId()); + Page pageResult = baseMapper.personListStatistics(personWisdomStatisticDTO, Page.of(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize())); + + List records = pageResult.getRecords(); + + // 收集所有需要查询的用户ID(员工ID + 考核人ID) + List employeeIdList = records.stream() + .flatMap(vo -> { + List ids = new ArrayList<>(); + if (StringUtil.isNotEmpty(vo.getEmployeeID())) { + ids.addAll(Arrays.asList(vo.getEmployeeID().split(","))); + } + if (StringUtil.isNotEmpty(vo.getExaminerId())) { + ids.addAll(Arrays.asList(vo.getExaminerId().split(","))); + } + return ids.stream(); + }) + .distinct() + .collect(Collectors.toList()); + + // 批量查询用户信息 + Map userIdMap = userApiV2Util.getUserPrimaryBoundBatch(employeeIdList, null); + + // 填充员工信息和考核人名称 + records.forEach(item -> { + fillEmployeeInfo(item, userIdMap); + fillExaminerName(item, userIdMap); + }); + return CultivatePage.coverPageList(pageResult); + } + + /** + * 填充员工信息 + * + * @param item 统计VO对象 + * @param userIdMap 用户ID映射表 + */ + private void fillEmployeeInfo(FtbCultivateIdentityPersonWisdomStatisticVO item, Map userIdMap) { + UserBoundVO userBoundVO = userIdMap.get(item.getEmployeeID()); + if (userBoundVO == null) { + return; + } + + // 构建岗位名称(岗位_职级) + String positionName = userBoundVO.getPositionName(); + if (StringUtil.isNotEmpty(userBoundVO.getGradeName())) { + positionName = positionName + "_" + userBoundVO.getGradeName(); + } + + // 设置员工相关信息 + item.setSysJobID(userBoundVO.getPositionEnCode()); + item.setJobID(userBoundVO.getPositionId()); + item.setThisPost(positionName); + item.setEmployeeSName(userBoundVO.getUserName()); + item.setNameOfAssociation(userBoundVO.getOrganizeName()); + item.setSysOrganizationID(userBoundVO.getOrganizeEnCode()); + item.setSysEmployeeID(userBoundVO.getSystemWorkerId()); + } + + /** + * 填充考核人名称 + * + * @param item 统计VO对象 + * @param userIdMap 用户ID映射表 + */ + private void fillExaminerName(FtbCultivateIdentityPersonWisdomStatisticVO item, Map userIdMap) { + if (StringUtils.isEmpty(item.getExaminerId())) { + return; + } + + String examinerNames = Arrays.stream(item.getExaminerId().split(",")) + .map(userIdMap::get) + .filter(ObjectUtil::isNotEmpty) + .map(UserBoundVO::getUserName) + .collect(Collectors.joining(",")); + + item.setExaminerName(examinerNames); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/ExamFrequencyLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/ExamFrequencyLogServiceImpl.java new file mode 100644 index 0000000..0fa5782 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/ExamFrequencyLogServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateExamFrequncyLogMapper; +import jnpf.cultivate.service.ExamFrequencyLogService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamFrequencyLog; +import org.springframework.stereotype.Service; + +/** + * 考试次数记录服务实现 + * + * @author yanwenfu + * @create 2026-03-23 + */ +@Service +public class ExamFrequencyLogServiceImpl extends ServiceImpl implements ExamFrequencyLogService { + + @Override + public Integer queryExamNumByDateStr(String examId, String userId, String startDateStr, String endDateStr) { + + return this.baseMapper.queryExamNumByDateStr(examId, userId, startDateStr, endDateStr); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedCommentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedCommentServiceImpl.java new file mode 100644 index 0000000..79a9c8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedCommentServiceImpl.java @@ -0,0 +1,102 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.FtbCourseGainedCommentMapper; +import jnpf.cultivate.service.FtbCourseGainedCommentService; +import jnpf.cultivate.service.FtbCourseGainedService; +import jnpf.model.cultivate.dto.gained.FtbCommentPagination; +import jnpf.model.cultivate.po.gained.FtbCourseGainedCommentEntity; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * + * 心得评论 服务实现类 + * + * @author yanglei + * @since 2023-07-19 + */ +@Service +public class FtbCourseGainedCommentServiceImpl extends SuperServiceImpl implements FtbCourseGainedCommentService { + + @Autowired + private UserProvider userProvider; + + + @Override + public List getList(FtbCommentPagination pagination){ + //通过UserProvider获取用户信息 + UserInfo userInfo = userProvider.get(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isEmpty(pagination.getGainedId())){ + queryWrapper.lambda().eq(FtbCourseGainedCommentEntity::getGainedId, pagination.getGainedId()); + } + if (pagination.getIsApp() == 1){ + queryWrapper.lambda().isNull(FtbCourseGainedCommentEntity::getParentId); + } + if (StringUtils.isNotEmpty(pagination.getOrganizeId())) { + queryWrapper.lambda().like(FtbCourseGainedCommentEntity::getOrganizeId, pagination.getOrganizeId()); + } + if (StringUtils.isNotEmpty(pagination.getKeyword())) { + queryWrapper.lambda().and( + t -> t.like(FtbCourseGainedCommentEntity::getAccount, pagination.getKeyword()) + .or().like(FtbCourseGainedCommentEntity::getRealName, pagination.getKeyword())); + } + //排序 + if (StringUtils.isEmpty(pagination.getSidx())) { + } else { + queryWrapper = "asc".equalsIgnoreCase(pagination.getSort()) ? queryWrapper.orderByAsc(pagination.getSidx()) : queryWrapper.orderByDesc(pagination.getSidx()); + } + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public FtbCourseGainedCommentEntity getInfo(String id){ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCourseGainedCommentEntity::getId, id); + return this.getOne(queryWrapper); + } + + + @Override + @DSTransactional + public void create(FtbCourseGainedCommentEntity entity){ + UserInfo userInfo = userProvider.get(); + entity.setId(RandomUtil.uuId()); + entity.setCreatorUserId(userInfo.getUserId()); + entity.setTenantId(userInfo.getTenantId()); + entity.setRealName(userInfo.getUserName()); + entity.setOrganizeId(userInfo.getOrganizeId()); + entity.setAccount(userInfo.getUserAccount()); + this.save(entity); + //FtbGroupEventService.add(entity.getCourseId(), entity.getId(), entity.getGainedId(), FtbGroupEventTypeEnum.COMMENT.getCode()); + } + + @Override + @Transactional + public void update(String id, FtbCourseGainedCommentEntity entity){ + entity.setId(id); + this.updateById(entity); + } + + @Override + public void delete(FtbCourseGainedCommentEntity entity) { + if (entity != null) { + this.removeById(entity.getId()); + // FtbGroupEventService.del(entity.getId()); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedLikeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedLikeServiceImpl.java new file mode 100644 index 0000000..cd447af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedLikeServiceImpl.java @@ -0,0 +1,96 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.FtbCourseGainedLikeMapper; +import jnpf.cultivate.service.FtbCourseGainedLikeService; +import jnpf.model.cultivate.dto.gained.FtbLikePagination; +import jnpf.model.cultivate.po.gained.FtbCourseGainedLikeEntity; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; + +/** + * + * 心得点赞 服务实现类 + * + */ +@Service +public class FtbCourseGainedLikeServiceImpl extends SuperServiceImpl implements FtbCourseGainedLikeService { + + @Autowired + private UserProvider userProvider; + @Resource + private FtbCourseGainedLikeMapper ftbCourseGainedLikeMapper; + + @Override + public List getList(FtbLikePagination pagination){ + //通过UserProvider获取用户信息 + UserInfo userInfo = userProvider.get(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isEmpty(pagination.getGainedId())){ + queryWrapper.lambda().eq(FtbCourseGainedLikeEntity::getGainedId, pagination.getGainedId()); + } + //排序 + if (StringUtils.isEmpty(pagination.getSidx())) { + } else { + queryWrapper = "asc".equalsIgnoreCase(pagination.getSort()) ? queryWrapper.orderByAsc(pagination.getSidx()) : queryWrapper.orderByDesc(pagination.getSidx()); + } + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public FtbCourseGainedLikeEntity getInfo(String id){ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCourseGainedLikeEntity::getId, id); + return this.getOne(queryWrapper); + } + + @Override + @DSTransactional + public void create(FtbCourseGainedLikeEntity entity){ + UserInfo userInfo = userProvider.get(); + entity.setId(RandomUtil.uuId()); + entity.setCreatorUserId(userInfo.getUserId()); + entity.setTenantId(userInfo.getTenantId()); + this.save(entity); + //ftbGroupEventService.addInteractionScore(entity.getCourseId(),entity.getGainedId()); + } + + @Override + @Transactional + public void update(String id, FtbCourseGainedLikeEntity entity){ + entity.setId(id); + this.updateById(entity); + } + + @Override + public void delete(FtbCourseGainedLikeEntity entity) { + if (entity != null) { + this.removeById(entity.getId()); + } + } + + @Override + public Integer getMyLike(FtbCourseGainedLikeEntity gainedLikeEntity) { + return ftbCourseGainedLikeMapper.getMyLike(gainedLikeEntity.getGainedId(),gainedLikeEntity.getLikeUserId()); + + } + + @Override + public void deleteLikeByGainedId(String gainedId, String likeUserId) { + ftbCourseGainedLikeMapper.deleteLikeByGainedId(gainedId,likeUserId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedReaderServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedReaderServiceImpl.java new file mode 100644 index 0000000..43befef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedReaderServiceImpl.java @@ -0,0 +1,78 @@ +package jnpf.cultivate.service.impl; + +import com.alibaba.cloud.commons.lang.StringUtils; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.Pagination; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.FtbCourseGainedReaderMapper; +import jnpf.cultivate.service.FtbCourseGainedReaderService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedReaderEntity; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * + * 心得可查看用户关联表 服务实现类 + * + */ +@Service +public class FtbCourseGainedReaderServiceImpl extends SuperServiceImpl implements FtbCourseGainedReaderService { + + @Autowired + private UserProvider userProvider; + + @Override + public List getList(Pagination pagination){ + //通过UserProvider获取用户信息 + UserInfo userInfo = userProvider.get(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + //排序 + if (StringUtils.isEmpty(pagination.getSidx())) { + } else { + queryWrapper = "asc".equalsIgnoreCase(pagination.getSort()) ? queryWrapper.orderByAsc(pagination.getSidx()) : queryWrapper.orderByDesc(pagination.getSidx()); + } + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public FtbCourseGainedReaderEntity getInfo(String id){ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCourseGainedReaderEntity::getId, id); + return this.getOne(queryWrapper); + } + + @Override + @DSTransactional + public void create(FtbCourseGainedReaderEntity entity){ + UserInfo userInfo = userProvider.get(); + entity.setId(RandomUtil.uuId()); + entity.setCreatorUserId(userInfo.getUserId()); + entity.setTenantId(userInfo.getTenantId()); + this.save(entity); + } + + @Override + @Transactional + public void update(String id, FtbCourseGainedReaderEntity entity){ + entity.setId(id); + this.updateById(entity); + } + + @Override + public void delete(FtbCourseGainedReaderEntity entity) { + if (entity != null) { + this.removeById(entity.getId()); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedServiceImpl.java new file mode 100644 index 0000000..9799452 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedServiceImpl.java @@ -0,0 +1,288 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageHelper; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.FtbCourseGainedMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.gained.FtbGainedPagination; +import jnpf.model.cultivate.dto.gained.FtbGainedQueryDto; +import jnpf.model.cultivate.po.gained.FtbCourseGainedEntity; +import jnpf.model.cultivate.po.gained.FtbCourseGainedLikeEntity; +import jnpf.model.cultivate.po.gained.FtbCourseGainedReaderEntity; +import jnpf.model.cultivate.vo.gained.FtbChapterEndGainedVO; +import jnpf.model.cultivate.vo.gained.FtbGroupGainedVO; +import jnpf.model.enums.CourseEnums; +import jnpf.permission.UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.RandomUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +//import jnpf.memoo.share.ShareApi; + +/** + * 心得体会 服务实现类 + */ +@Service +@Slf4j +public class FtbCourseGainedServiceImpl extends SuperServiceImpl implements FtbCourseGainedService { + + @Autowired + private UserProvider userProvider; + @Autowired + private FtbCourseGainedReaderService readerService; + @Autowired + private FtbCourseGainedLikeService likeService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public List getList(FtbGainedPagination pagination) { + //通过UserProvider获取用户信息 + UserInfo userInfo = userProvider.get(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isEmpty(pagination.getChapterId())) { + queryWrapper.lambda().eq(FtbCourseGainedEntity::getChapterId, pagination.getChapterId()); + } + if (!StringUtils.isEmpty(pagination.getOrganizeId())) { + queryWrapper.lambda().eq(FtbCourseGainedEntity::getOrganizeId, pagination.getOrganizeId()); + } + + if (!StringUtils.isEmpty(pagination.getCourseId())) { + queryWrapper.lambda().eq(FtbCourseGainedEntity::getCourseId, pagination.getCourseId()); + } + if (!StringUtils.isEmpty(pagination.getKeyword())) { +// List userVoList = userApi.getUserByName(null, null, pagination.getKeyword()); + List userVoList = userApiV2Util.getUserForNameOrPhone(pagination.getKeyword(), "", null); + List userIdList = userVoList.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (userIdList.size() > 0) { + queryWrapper.lambda().in(FtbCourseGainedEntity::getCreatorUserId, userIdList); + } else { + queryWrapper.lambda().in(FtbCourseGainedEntity::getCreatorUserId, "-1"); + } + } + if (Objects.nonNull(pagination.getType()) && pagination.getType() == 1) { + List readerEntityList = readerService.list(new LambdaQueryWrapper() + .eq(FtbCourseGainedReaderEntity::getUserId, userInfo.getUserId())); + List list = readerEntityList.stream().map(FtbCourseGainedReaderEntity::getGainedId).collect(Collectors.toList()); + if (list.size() > 0) { + queryWrapper.lambda().and( + t -> t.eq(FtbCourseGainedEntity::getReadType, 0) + .or().eq(FtbCourseGainedEntity::getCreatorUserId, userInfo.getUserId()) + .or().in(FtbCourseGainedEntity::getId, list) + ); + } else { + queryWrapper.lambda().and( + t -> t.eq(FtbCourseGainedEntity::getReadType, 0) + .or().eq(FtbCourseGainedEntity::getCreatorUserId, userInfo.getUserId()) + ); + } + } + //排序 + if (StringUtils.isEmpty(pagination.getSidx())) { + } else { + queryWrapper = "asc".equalsIgnoreCase(pagination.getSort()) ? queryWrapper.orderByAsc(pagination.getSidx()) : queryWrapper.orderByDesc(pagination.getSidx()); + } + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public List getGainedListOfChapterEnd(String chapterId) { + List list = new ArrayList<>();//ftbCourseGainedMapper.getGainedListOfChapterEnd(chapterId); + //查询用户信息 +// List infoByIds = userApi.getInfoByIds(list.stream().map(FtbCourseGainedEntity::getCreatorUserId).collect(Collectors.toList())); + List userPrimaryBoundBatchReturnList = userApiV2Util.getUserPrimaryBoundBatchReturnList(list.stream().map(FtbCourseGainedEntity::getCreatorUserId).collect(Collectors.toList()), null); + Map collect1 = userPrimaryBoundBatchReturnList.stream().collect(Collectors.toMap(UserBoundVO::getId, user -> user)); + List gainedDetailByIds = getGainedDetailByIds(list.stream().map(FtbCourseGainedEntity::getId).collect(Collectors.toList())); + Map gainedMap = gainedDetailByIds.stream().collect(Collectors.toMap(FtbGroupGainedVO::getEventId, gained -> gained)); + return list.stream().map(v -> { + UserBoundVO user = collect1.getOrDefault(v.getCreatorUserId(), new UserBoundVO()); + FtbGroupGainedVO detail = gainedMap.getOrDefault(v.getId(), new FtbGroupGainedVO()); + FtbChapterEndGainedVO build = FtbChapterEndGainedVO.builder().realName(user.getUserName()).headIcon(UploaderUtil.uploaderImg(user.getHeadIcon())).time(v.getCreatorTime()).build(); + BeanUtils.copyProperties(detail, build); + return build; + }).collect(Collectors.toList()); + } + + @Override + public FtbCourseGainedEntity getInfo(String id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCourseGainedEntity::getId, id); + return this.getOne(queryWrapper); + } + + @Override + @DSTransactional + public void create(FtbCourseGainedEntity entity) { + UserInfo userInfo = userProvider.get(); + entity.setId(RandomUtil.uuId()); + entity.setCreatorUserId(userInfo.getUserId()); + entity.setTenantId(userInfo.getTenantId()); + entity.setLastModifyTime(new Date()); + entity.setStatus(1); + entity.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + this.save(entity); + // ftbGroupEventService.add(entity.getCourseId(), entity.getId(), null, FtbGroupEventTypeEnum.GAINED.getCode()); + } + + @Override + @Transactional + public void update(String id, FtbCourseGainedEntity entity) { + entity.setId(id); + this.updateById(entity); + } + + @Override + public void delete(FtbCourseGainedEntity entity) { + if (entity != null) { + this.removeById(entity.getId()); + // ftbGroupEventService.del(entity.getId()); + } + } + + @Override + public List list(FtbGainedQueryDto queryDto) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (!StringUtils.isEmpty(queryDto.getUserId()) && queryDto.getType() == 0) { + //查询用户发布和可查看的 + List readerEntityList = readerService.list(new LambdaQueryWrapper() + .eq(FtbCourseGainedReaderEntity::getUserId, queryDto.getUserId())); + List readList = readerEntityList.stream().map(FtbCourseGainedReaderEntity::getGainedId).collect(Collectors.toList()); + if (readList.size() == 0) { + queryWrapper.eq(FtbCourseGainedEntity::getCreatorUserId, queryDto.getUserId()); + } else { + queryWrapper.eq(FtbCourseGainedEntity::getCreatorUserId, queryDto.getUserId()) + .or().in(FtbCourseGainedEntity::getId, readList); + } + } + if (queryDto.getType() == 1) { + //查询用户发布的 + queryWrapper.eq(FtbCourseGainedEntity::getCreatorUserId, queryDto); + } else if (queryDto.getType() == 2) { + //查询用户点赞的 + List likeEntityList = likeService.list(new LambdaQueryWrapper() + .eq(FtbCourseGainedLikeEntity::getLikeUserId, queryDto.getUserId())); + List likeList = likeEntityList.stream().map(FtbCourseGainedLikeEntity::getGainedId).collect(Collectors.toList()); + if (likeList.size() == 0) { + return new ArrayList<>(); + } + queryWrapper.in(FtbCourseGainedEntity::getId, likeList); + } + queryWrapper.orderByDesc(FtbCourseGainedEntity::getCreatorTime); + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + return list(queryWrapper); + } + + @Override + public List getGainedDetailByIds(List gainedIds) { + if (CollUtil.isEmpty(gainedIds)) { + return CollUtil.newArrayList(); + } + List FtbCourseGainedEntities = listByIds(gainedIds); + if (CollUtil.isEmpty(FtbCourseGainedEntities)) { + return CollUtil.newArrayList(); + } + // TODO +// //查询课程详情 +// List courseIds = FtbCourseGainedEntities.stream().map(FtbCourseGainedEntity::getCourseId).collect(Collectors.toList()); +// List FtbCourseEntities = courseService.listByIds(courseIds); +// Map courseMap = FtbCourseEntities.stream().collect(Collectors.toMap(course -> course.getId(), course -> course)); +// //查询章节详情 +// List chapterIds = FtbCourseGainedEntities.stream().map(FtbCourseGainedEntity::getChapterId).collect(Collectors.toList()); +// List FtbCourseChapterEntities = courseChapterService.listByIds(chapterIds); +// Map chapterMap = FtbCourseChapterEntities.stream().collect(Collectors.toMap(chapter -> chapter.getId(), chapter -> chapter)); +// List shareNumberList = shareService.list(new LambdaQueryWrapper() +// .in(FtbCourseGainedShareEntity::getGainedId, gainedIds)); +// Map> shareNumberGroup = CollectionUtils.isNotEmpty(shareNumberList) ? +// shareNumberList.stream().collect(Collectors.groupingBy(FtbCourseGainedShareEntity::getGainedId)) : new HashMap<>(); +// +// List likeNumberList = likeService.list(new LambdaQueryWrapper() +// .in(FtbCourseGainedLikeEntity::getGainedId, gainedIds)); +// Map> likeNumberGroup = CollectionUtils.isNotEmpty(likeNumberList) ? +// likeNumberList.stream().collect(Collectors.groupingBy(FtbCourseGainedLikeEntity::getGainedId)) : new HashMap<>(); +// +// List commentNumberList = commentService.list(new LambdaQueryWrapper() +// .in(FtbCourseGainedCommentEntity::getGainedId, gainedIds)); +// Map> commentNumberGroup = CollectionUtils.isNotEmpty(commentNumberList) ? +// commentNumberList.stream().collect(Collectors.groupingBy(FtbCourseGainedCommentEntity::getGainedId)) : new HashMap<>(); +// String loginUserId = UserProvider.getLoginUserId(); +// return FtbCourseGainedEntities.stream().map(gained -> { +// FtbGroupGainedVO FtbCourseGainedInfoVO = new FtbGroupGainedVO(); +// FtbCourseGainedInfoVO.setContent(gained.getContent()); +// FtbCourseGainedInfoVO.setEventId(gained.getId()); +// //课程 +// FtbCourseEntity FtbCourseEntity = courseMap.get(gained.getCourseId()); +// FtbCourseGainedInfoVO.setCourseId(gained.getCourseId()); +// FtbCourseGainedInfoVO.setCourseName(FtbCourseEntity.getName()); +// FtbCourseGainedInfoVO.setCourseIcon(FtbCourseEntity.getCover()); +// //章节 +// FtbCourseChapterEntity FtbCourseChapterEntity = chapterMap.get(gained.getChapterId()); +// FtbCourseGainedInfoVO.setChapter(FtbCourseChapterEntity.getName()); +// FtbCourseGainedInfoVO.setChapterId(FtbCourseChapterEntity.getId()); +// //分享数 +// FtbCourseGainedInfoVO.setForwardNum(shareNumberGroup.getOrDefault(gained.getId(), CollUtil.newArrayList()).size()); +// //点赞数 +// List orDefault = likeNumberGroup.getOrDefault(gained.getId(), CollUtil.newArrayList()); +// FtbCourseGainedInfoVO.setLikeNum(orDefault.size()); +// //是否已点赞 +// List collect = orDefault.stream().filter(like -> StringUtil.equals(loginUserId, like.getLikeUserId())).collect(Collectors.toList()); +// FtbCourseGainedInfoVO.setIsLike(!collect.isEmpty()); +// //评论数 +// FtbCourseGainedInfoVO.setReplyNum(commentNumberGroup.getOrDefault(gained.getId(), CollUtil.newArrayList()).size()); +// return FtbCourseGainedInfoVO; +// }).collect(Collectors.toList()); + return null; + } + +// @Override +// public void sharingSquare(FtbCourseGainedEntity gainedEntity) { +// //add +// ShareContentEntityFormContent shareContentEntityFormContent = new ShareContentEntityFormContent(); +// +// //用户id +// UserInfo userInfo = userProvider.get(); +// String userId = userInfo.getId(); +// +// String content = gainedEntity.getContent(); +// String contentId = gainedEntity.getCourseId(); +// +// FtbCourseEntity FtbCourse = FtbCourseService.getById(contentId); +// +// shareContentEntityFormContent.setUserId(userId); +// shareContentEntityFormContent.setContent(StringUtils.isEmpty(content) ? "" : content); +// //fixme 未提供图片地址 +// shareContentEntityFormContent.setPicture(""); +// shareContentEntityFormContent.setContentId(ObjectUtils.isEmpty(FtbCourse) ? null : contentId); +// shareContentEntityFormContent.setCourseName(ObjectUtils.isEmpty(FtbCourse) ? "" : +// FtbCourse.getName()); +// try { +// Boolean shareFeignSate = shareApi.publishContent(shareContentEntityFormContent); +// log.info("--------分享到广场内部接口调用成功--------"); +// log.info("--------分享到广场是否成功:{}", shareFeignSate + "---------"); +// } catch (Exception e) { +// log.debug("--------分享到广场内部接口调用失败--------"); +// log.error("失败原因:{}", e.getMessage()); +// } +// } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedShareServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedShareServiceImpl.java new file mode 100644 index 0000000..85328da --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCourseGainedShareServiceImpl.java @@ -0,0 +1,110 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.Pagination; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.FtbCourseGainedShareMapper; +import jnpf.cultivate.service.FtbCourseGainedService; +import jnpf.cultivate.service.FtbCourseGainedShareService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedShareEntity; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 心得分享 服务实现类 + * + */ +@Service +public class FtbCourseGainedShareServiceImpl extends SuperServiceImpl implements FtbCourseGainedShareService { + + @Autowired + private UserProvider userProvider; + + @Override + public List getList(Pagination pagination) { + //通过UserProvider获取用户信息 + UserInfo userInfo = userProvider.get(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + //排序 + if (StringUtils.isEmpty(pagination.getSidx())) { + } else { + queryWrapper = "asc".equalsIgnoreCase(pagination.getSort()) ? queryWrapper.orderByAsc(pagination.getSidx()) : queryWrapper.orderByDesc(pagination.getSidx()); + } + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public FtbCourseGainedShareEntity getInfo(String id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCourseGainedShareEntity::getId, id); + return this.getOne(queryWrapper); + } + + @Override + @DSTransactional + public void create(FtbCourseGainedShareEntity entity) { + entity.setId(RandomUtil.uuId()); + this.save(entity); + // ftbGroupEventService.addInteractionScore(entity.getCourseId(),entity.getGainedId()); + } + + @Override + @Transactional + public void update(String id, FtbCourseGainedShareEntity entity) { + entity.setId(id); + this.updateById(entity); + } + + @Override + public void delete(FtbCourseGainedShareEntity entity) { + if (entity != null) { + this.removeById(entity.getId()); + } + } + +// @Override +// public ActionResult sharingSquare(String id) { +// ShareContentEntityFormContent shareContentEntityFormContent = new ShareContentEntityFormContent(); +// +// //用户id +// UserInfo userInfo = userProvider.get(); +// String userId = userInfo.getId(); +// +// FtbCourseGainedEntity gainedEntity = FtbCourseGainedService.getById(id); +// +// if (ObjectUtils.isEmpty(gainedEntity)) { +// return ActionResult.fail("分享内容不存在"); +// } +// String content = gainedEntity.getContent(); +// String contentId = gainedEntity.getCourseId(); +// +// FtbCourseEntity FtbCourse = FtbCourseService.getById(contentId); +// +// shareContentEntityFormContent.setUserId(userId); +// shareContentEntityFormContent.setContent(StringUtils.isEmpty(content) ? "" : content); +// //fixme 未提供图片地址 +// shareContentEntityFormContent.setPicture(""); +// shareContentEntityFormContent.setContentId(ObjectUtils.isEmpty(FtbCourse) ? null : contentId); +// shareContentEntityFormContent.setCourseName(ObjectUtils.isEmpty(FtbCourse) ? "" : +// FtbCourse.getName()); +// try { +// shareApi.publishContent(shareContentEntityFormContent); +// return ActionResult.success("分享成功"); +// } catch (Exception e) { +// return ActionResult.fail("网络异常请稍后再试"); +// } +// } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateAssessmentPointsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateAssessmentPointsServiceImpl.java new file mode 100644 index 0000000..c7ebb61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateAssessmentPointsServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateAssessmentPointsMapper; +import jnpf.cultivate.service.FtbCultivateAssessmentPointsService; +import jnpf.model.cultivate.po.FtbCultivateAssessmentPoints; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateAssessmentPointsServiceImpl extends ServiceImpl implements FtbCultivateAssessmentPointsService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCaseBaseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCaseBaseServiceImpl.java new file mode 100644 index 0000000..9d219f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCaseBaseServiceImpl.java @@ -0,0 +1,171 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.FtbCultivateCaseBaseLikeMapper; +import jnpf.cultivate.mapper.FtbCultivateCaseBaseMapper; +import jnpf.cultivate.service.FtbCultivateCaseBaseService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseAuditDTO; +import jnpf.model.cultivate.dto.casebase.FtbCultivateCaseBaseDTO; +import jnpf.model.cultivate.dto.casebase.app.FtbCultivateCaseBaseCreatDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.casebase.FtbCultivateCaseBase; +import jnpf.model.cultivate.po.casebase.FtbCultivateCaseBaseLike; +import jnpf.model.cultivate.vo.casebase.FtbCultivateCaseBaseVO; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Title: + * @Author: peng.hao + * @create: 2024/7/22:11:26 + */ +@Service +public class FtbCultivateCaseBaseServiceImpl extends ServiceImpl implements FtbCultivateCaseBaseService { + + @Resource + private FtbPersonnelsStaffRosterService staffRosterservice; + + @Resource + private FtbCultivateFileService ftbCultivateFileService; + + @Resource + private FtbCultivateCaseBaseLikeMapper caseBaseLikeMapper; + + @Override + public PageListVO listDisplay(FtbCultivateCaseBaseDTO dto) { + Page page = dto.coverCultivatePage(); + String userId = UserProvider.getUser().getUserId(); + dto.setUserId(userId); + page = baseMapper.listDisplay(page, dto); + return CultivatePage.coverPageList(page); + } + @Override + public PageListVO listDisplayForApp(FtbCultivateCaseBaseDTO dto) { + Page page = dto.coverCultivatePage(); + dto.setUserId(UserProvider.getUser().getUserId()); + page = baseMapper.listDisplayForApp(page, dto); + return CultivatePage.coverPageList(page); + } + + @Override + public void reviewCase(FtbCultivateCaseBaseAuditDTO baseAuditDTO) { + String flag = baseAuditDTO.getFlag(); + // 2 审核通过, 3 审核不通过 + int status = "0".equals(flag) ? 3 : 2; + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.eq(SuperBaseEntity.SuperIBaseEntity::getId,baseAuditDTO.getId()); + lambdaed.set(FtbCultivateCaseBase::getStatus,status); + lambdaed.set(FtbCultivateCaseBase::getAuditOpinion,baseAuditDTO.getAuditOpinion()); + // 审核时间 + lambdaed.set(FtbCultivateCaseBase::getAuditTime,new Date()); + lambdaed.set(FtbCultivateCaseBase::getAuditUserId,UserProvider.getUser().getUserId()); + // 同步更新更新时间 + lambdaed.set(FtbCultivateCaseBase::getLastModifyTime,new Date()); + baseMapper.update(null,lambdaed); + } + + @Override + public FtbCultivateCaseBaseVO getDetail(String id) { + FtbCultivateCaseBase caseBase = baseMapper.selectById(id); + FtbCultivateCaseBaseVO convert = FtbCultivateCaseBaseVO.convert(caseBase); + if(StringUtils.isNotEmpty(convert.getAuditUserId())){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRoster::getUserId,convert.getAuditUserId()); + FtbPersonnelsStaffRoster rosterserviceOne = staffRosterservice.getOne(wrapper); + if(ObjectUtil.isNotNull(rosterserviceOne)) convert.setAuditUserName(rosterserviceOne.getName()); + } + LambdaQueryWrapper wrapperN = Wrappers.lambdaQuery(); + wrapperN.eq(FtbPersonnelsStaffRoster::getUserId,convert.getCreatorUserId()); + FtbPersonnelsStaffRoster rosterserviceOne = staffRosterservice.getOne(wrapperN); + if(ObjectUtil.isNotNull(rosterserviceOne)) convert.setCreatorUserName(rosterserviceOne.getName()); + // 点赞量 + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbCultivateCaseBaseLike::getGainedId,id); + Long aLong = caseBaseLikeMapper.selectCount(lambdaed); + convert.setNumberOfLikes(aLong.intValue()); + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.eq(FtbCultivateCaseBaseLike::getLikeUserId, UserProvider.getUser().getUserId()); + lambdaQueryWrapper.eq(FtbCultivateCaseBaseLike::getGainedId,id); + Long aLong1 = caseBaseLikeMapper.selectCount(lambdaQueryWrapper); + // 是否已经点赞 + convert.setHaveYouLikedIt(aLong1.intValue() > 0 ? 1 : 0); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbCultivateFile::getBusinessId, id); + List fileLists = ftbCultivateFileService.list(wrapper); + List fileVOS = fileLists.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + convert.setFiles(fileVOS); + return convert; + } + + @Override + public void caseBaseCreatDTO(FtbCultivateCaseBaseCreatDTO caseBaseCreatDTO) { + FtbCultivateCaseBase covert = FtbCultivateCaseBaseCreatDTO.covert(caseBaseCreatDTO); + // 获取展示id + String systemCaseId = SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.CASE_LIBRARY); + covert.setSystemCaseId(systemCaseId); + baseMapper.insert(covert); + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(caseBaseCreatDTO.getFiles()) + .businessTypeID(covert.getId()) + .type(FileEventDTO.FileType.CASE_LIBRARY) + .build())); + } + + @Override + public void updateCaseLibrary(FtbCultivateCaseBaseCreatDTO updateVO) { + FtbCultivateCaseBase covert = FtbCultivateCaseBaseCreatDTO.covert(updateVO); + if (updateVO.getFiles() != null) { + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.eq(FtbCultivateFile::getBusinessId, updateVO.getId()); + ftbCultivateFileService.remove(lambdaed); + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(updateVO.getFiles()) + .businessTypeID(updateVO.getId()) + .type(FileEventDTO.FileType.CASE_LIBRARY) + .build())); + } + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(); + update.eq(FtbCultivateCaseBase::getId, covert.getId()); + update.set(FtbCultivateCaseBase::getCaseDescription, covert.getCaseDescription()); + update.set(FtbCultivateCaseBase::getCaseName, covert.getCaseName()); + update.set(FtbCultivateCaseBase::getStatus, covert.getStatus()); + update.set(FtbCultivateCaseBase::getLastModifyTime, new Date()); + // 重新编辑清空审核人数据 + update.set(FtbCultivateCaseBase::getAuditOpinion,null); + update.set(FtbCultivateCaseBase::getAuditTime,null); + update.set(FtbCultivateCaseBase::getAuditUserId,null); + baseMapper.update(null, update); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateImagesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateImagesServiceImpl.java new file mode 100644 index 0000000..bfca9e8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateImagesServiceImpl.java @@ -0,0 +1,59 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateCertificateImagesMapper; +import jnpf.cultivate.service.FtbCultivateCertificateImagesService; +import jnpf.model.cultivate.po.certificate.FtbCertificateImagesEntity; +import jnpf.model.cultivate.v2.certificate.req.V2SaveCertificateReq; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +/** + * 证书图片 + * + * @author xgl + */ +@Service +public class FtbCultivateCertificateImagesServiceImpl extends ServiceImpl implements FtbCultivateCertificateImagesService { + + + /** + * 查询所有证书图片列表 + * + * @return 证书图片实体列表 + */ + @Override + public List listAll() { + return this.baseMapper.listAll(); + } + + /** + * 添加证书图片 + * + * @param req 保存证书图片请求参数 + * @return 是否添加成功 + */ + @Override + public boolean add(V2SaveCertificateReq req) { + String userId = UserProvider.getUser().getUserId(); + + //查询出排序最大的一条数据 + Integer max = baseMapper.queryMaxSort(); + if (max == null) { + max = 0; + } + FtbCertificateImagesEntity entity = new FtbCertificateImagesEntity(); + entity.setIsSystem(1); + entity.setType(req.getType()); + entity.setPath(req.getPath()); + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(userId); + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(userId); + entity.setSorts(max); + return this.save(entity); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateServiceImpl.java new file mode 100644 index 0000000..8faba5f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateServiceImpl.java @@ -0,0 +1,601 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.base.Pagination; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.FtbCultivateCertificateMapper; +import jnpf.cultivate.service.FtbCultivateCertificateService; +import jnpf.cultivate.service.FtbCultivateCertificateUserService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.certificate.FtbCertificateOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.certificate.FtbCertificatePersonWisdomStatisticDTO; +import jnpf.model.cultivate.po.certificate.CertificateUserPagination; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.v2.common.vo.V2BaseIdNameVo; +import jnpf.model.cultivate.vo.certificate.*; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionPersonStatisticesVO; +import jnpf.model.enums.CourseEnums; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 证书模版表 + * + * @author penghao + */ +@Service +public class FtbCultivateCertificateServiceImpl extends ServiceImpl implements FtbCultivateCertificateService { + + private static final List EMPTY_LIST = List.of("-1"); + @Autowired + private UserProvider userProvider; + @Autowired + private FtbCultivateCertificateUserService certificateUserService; + + @Autowired + CultivatePerUtils cultivatePerUtils; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbPersonnelsStaffRosterMapper rosterMapper; + + @Autowired + private FtbPermissionOrganizeService ftbPermissionOrganizeService; + + @Override + public List getList(Pagination pagination) { + //通过UserProvider获取用户信息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isEmpty(pagination.getKeyword())) { + queryWrapper.lambda().and( + t -> t.like(FtbCertificateEntity::getName, pagination.getKeyword()) + ); + } + queryWrapper.lambda().eq(FtbCertificateEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + //排序 + queryWrapper.orderByDesc("F_LASTMODIFYTIME"); + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public FtbCertificateEntity getInfo(String id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCertificateEntity::getId, id); + return this.getOne(queryWrapper); + } + + @Override + @DSTransactional + public void create(FtbCertificateEntity entity) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.and( + wp -> wp.eq(FtbCertificateEntity::getName, entity.getName()) + ); + wrapper.eq(FtbCertificateEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + wrapper.last("limit 1"); + FtbCertificateEntity ftbCertificateEntity = baseMapper.selectOne(wrapper); + if (ftbCertificateEntity != null && ftbCertificateEntity.getName().equals(entity.getName())) { + throw new RuntimeException("证书名称已存在请勿重复添加!"); + } + UserInfo userInfo = userProvider.get(); + entity.setId(IdWorker.getIdStr()); + entity.setCreatorUserId(userInfo.getUserId()); + entity.setTenantId(userInfo.getTenantId()); + entity.setLastModifyTime(new Date()); + entity.setStatus(CourseEnums.CertStatus.UP.getCode()); + entity.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + fill(entity); + this.save(entity); + } + + private void fill(FtbCertificateEntity entity) { +// List organizeEntityList = organizeApi.getOrganizeId(userProvider.get().getOrganizeId()); + OrganizeGeneralDetailVO organizeGeneralDetailVO = userApiV2Util.organizeInfoById(userProvider.get().getOrganizeId(), null); + if (organizeGeneralDetailVO != null) { + entity.setCompanyName(organizeGeneralDetailVO.getOrganizeTreeName()); + } + } + + + @Transactional + @Override + public String updateEntity(String id, FtbCertificateEntity entity) { + entity.setId(id); + String string = repeatTheCheck(entity); + if (!StringUtils.isEmpty(string)) { + return string; + } + UserInfo userInfo = userProvider.get(); + entity.setId(id); + entity.setLastModifyUserId(userInfo.getUserId()); + FtbCertificateEntity info = this.getInfo(id); + if (!info.getName().equals(entity.getName())) { + //更新用户证书名称 + certificateUserService.update(new FtbCertificateUserEntity().setCertificateName(entity.getName()) + , new LambdaQueryWrapper().eq(FtbCertificateUserEntity::getCertificateId, id)); + } + fill(entity); + this.updateById(entity); + return null; + } + + /** + * 重复校验 + * + * @param entity + */ + private String repeatTheCheck(FtbCertificateEntity entity) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.and( + wp -> wp.eq(FtbCertificateEntity::getName, entity.getName()) + ); + wrapper.eq(FtbCertificateEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + wrapper.last("limit 1"); + FtbCertificateEntity ftbCertificateEntity = baseMapper.selectOne(wrapper); + // 同时进行修改 + if (ObjectUtil.isEmpty(ftbCertificateEntity)) { + return null; + } + // 修改状态 + if (entity.getId().equals(ftbCertificateEntity.getId()) && + !entity.getStatus().equals(ftbCertificateEntity.getStatus())) { + return null; + } + // 判断是不是 + if (entity.getId().equals(ftbCertificateEntity.getId()) && + (entity.getName().equals(ftbCertificateEntity.getName())) + ) { + return null; + } + String tishi = ""; + boolean nameFlag = false; + // 其他的用户想要修改为当前的名称 + if (!ftbCertificateEntity.getName().equals(entity.getName())) { + nameFlag = true; + } else if (ftbCertificateEntity.getName().equals(entity.getName()) && !ftbCertificateEntity.getId().equals(entity.getId())) { + return null; + } else { + tishi += "该证书名称已存在!"; + } + if (nameFlag) { + tishi = null; + } + return tishi; + } + + @Override + public void delete(FtbCertificateEntity entity) { + if (entity != null) { + UserInfo userInfo = userProvider.get(); + FtbCertificateUserEntity userEntity = new FtbCertificateUserEntity(); + userEntity.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + userEntity.setDeleteUserId(userInfo.getUserId()); + userEntity.setDeleteTime(new Date()); + certificateUserService.update(userEntity, new LambdaQueryWrapper().eq(FtbCertificateUserEntity::getCertificateId, entity.getId())); + entity.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + entity.setDeleteUserId(userInfo.getUserId()); + entity.setDeleteTime(new Date()); + this.updateById(entity); + } + } + + @Override + public void deleteCertificate(String id) { + if (StringUtils.isEmpty(id)) { + throw new RuntimeException("主键不能为空!"); + } + CertificateUserPagination pagination = new CertificateUserPagination(); + pagination.setCertificateId(id); + List list = certificateUserService.getList(pagination); + if (CollUtil.isNotEmpty(list)) { + List collect = + list.stream().filter(item -> 0 == item.getStatus()).collect(Collectors.toList()); + // 查询所有未过期的数据 + if (CollUtil.isNotEmpty(collect)) { + throw new RuntimeException("抱歉!删除失败,已颁发证书存在生效中的状态,请吊销后再进行删除!"); + } + } + baseMapper.deleteById(id); + } + + @Override + public PageListVO queryCertificateListByNotChoose(String keyWords, CultivatePage page) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + if (StringUtils.isNotEmpty(keyWords)) lambdaQuery.like(FtbCertificateEntity::getName, keyWords); + lambdaQuery.eq(FtbCertificateEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + lambdaQuery.eq(FtbCertificateEntity::getStatus, 1); + lambdaQuery.orderByDesc(FtbCertificateEntity::getLastModifyTime); + List entity = baseMapper.selectList(lambdaQuery); +// // 查询岗位学习已经绑定的证书 +// LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); +// lambdaed.eq(FtbCultivatePositionCertificate::getEnableMark,0); +// List positionCertificates = cultivatePositionCertificateMapper.selectList(lambdaed); +// List certificateIds = positionCertificates.stream().map(FtbCultivatePositionCertificate::getCertificateId).collect(Collectors.toList()); +// entity= entity.stream().filter(item -> !certificateIds.contains(item.getId())).collect(Collectors.toList()); + List certificateListVOS = entity.stream().map(FtbCertificateListVO::covertVO).collect(Collectors.toList()); + //补充创建人 + if (CollUtil.isNotEmpty(certificateListVOS)) { + fillCreateUserName(certificateListVOS); + } + Page cultivatePage = page.coverCultivatePage(); + return CultivatePage.paginate(certificateListVOS, cultivatePage); + } + + @Override + public List getOption() { + List list = this.list(new LambdaQueryWrapper() + .eq(FtbCertificateEntity::getStatus, CourseEnums.CourseGroundingType.UP.getCode()) + .orderByDesc(FtbCertificateEntity::getCreatorTime)); + return BeanUtil.copyToList(list, V2BaseIdNameVo.class); + } + + /** + * 补充创建人姓名 + * + * @param list + * @return + */ + public void fillCreateUserName(List list) { + Set userIds = new HashSet<>(); + for (FtbCertificateListVO ftbCertificateListVO : list) { + if (StringUtils.isNotEmpty(ftbCertificateListVO.getCreatorUserId())) { + userIds.add(ftbCertificateListVO.getCreatorUserId()); + } + } + if (CollUtil.isEmpty(userIds)) { + return; + } + //查询用户的姓名信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getName) + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = rosterMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return; + } + + + Map map = new HashMap<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { + map.put(roster.getUserId(), roster); + } + for (FtbCertificateListVO ftbCertificateListVO : list) { + if (StringUtils.isNotEmpty(ftbCertificateListVO.getCreatorUserId())) { + FtbPersonnelsStaffRoster roster = map.get(ftbCertificateListVO.getCreatorUserId()); + if (roster != null) { + ftbCertificateListVO.setCreatorUserName(roster.getName()); + } + } + } + } + + @Override + public PageListVO organizationListStatistics(FtbCertificateOrgWisdomStatisticDTO statisticDTO) { + if (statisticDTO.getPageSize() <= 0) { + statisticDTO.setPageSize(20L); + } + if (statisticDTO.getCurrentPage() <= 0) { + statisticDTO.setCurrentPage(1L); + } + + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return returnEmptyList(statisticDTO); + } + List selectoAllorgIds = powerOrgList; + + + if ("1".equals(statisticDTO.getSelectLogo()) && CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List allOrg = new ArrayList<>(); + for (String orgId : statisticDTO.getOrgId()) { + List allOrganize = cultivatePerUtils.getAllOrganize(orgId); + if (CollUtil.isNotEmpty(allOrganize)) { + allOrg.addAll(allOrganize); + } + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, allOrg); + if (CollUtil.isNotEmpty(intersection)) { + selectoAllorgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + } else { + if (CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List intersection = UserApiV2Util.getIntersection(powerOrgList, statisticDTO.getOrgId()); + if (CollUtil.isNotEmpty(intersection)) { + selectoAllorgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + } + } + + // 默认情况 + // 手动输入 + String dtoOrgIds = statisticDTO.getOrgIds(); + if (StringUtils.isNotEmpty(dtoOrgIds)) { + List organizationalIdCollection = cultivatePerUtils.convertOrganizationalIdCollection(dtoOrgIds); + if (CollUtil.isNotEmpty(organizationalIdCollection)) { + List intersection = UserApiV2Util.getIntersection(selectoAllorgIds, organizationalIdCollection); + if (CollUtil.isNotEmpty(intersection)) { + selectoAllorgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + } else { + return returnEmptyList(statisticDTO); + } + } + List organizeGeneralDetailVOS = userApiV2Util.organizesByOrganizeIds(selectoAllorgIds, null); + if (CollUtil.isEmpty(organizeGeneralDetailVOS)) { + return returnEmptyList(statisticDTO); + } + + List userCertList = baseMapper.organizationListStatisticsAll(statisticDTO); + if (CollUtil.isEmpty(userCertList)) { + return returnEmptyList(statisticDTO); + } + List allUserIds = new ArrayList<>(); + Map allUserCertMap = new HashMap<>(); //证书id 到证书 + List allUserCertList = new ArrayList<>(); + Map> certToUserMap = new HashMap<>(); //证书 的人员列表 + Map orgMap = organizeGeneralDetailVOS.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item)); + + + for (FtbCertificateQueryStatisticAllVO ftbCertificateQueryStatisticAllVO : userCertList) { + allUserIds.add(ftbCertificateQueryStatisticAllVO.getUserId()); + FtbCertificateInfoVO ftbCertificateInfoVO = allUserCertMap.get(ftbCertificateQueryStatisticAllVO.getId()); + if (ftbCertificateInfoVO == null) { + ftbCertificateInfoVO = new FtbCertificateInfoVO(); + ftbCertificateInfoVO.setId(ftbCertificateQueryStatisticAllVO.getId()); + ftbCertificateInfoVO.setName(ftbCertificateQueryStatisticAllVO.getCertificateName()); + ftbCertificateInfoVO.setStatus(ftbCertificateQueryStatisticAllVO.getOnShelfStatus()); + allUserCertMap.put(ftbCertificateInfoVO.getId(), ftbCertificateInfoVO); + } + + List userBoundVOS = certToUserMap.get(ftbCertificateQueryStatisticAllVO.getId()); + if (userBoundVOS == null) { + userBoundVOS = new ArrayList<>(); + } + userBoundVOS.add(ftbCertificateQueryStatisticAllVO.getUserId()); + certToUserMap.put(ftbCertificateQueryStatisticAllVO.getId(), userBoundVOS); + + + } + if (allUserCertMap.size() > 0) { + allUserCertList = allUserCertMap.values().stream().collect(Collectors.toList()); + } + + + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(allUserIds, null); + + List ret = buildReturnData(allUserCertList, certToUserMap, userPrimaryBoundBatch, orgMap); + return UserApiV2Util.buildPage(ret, statisticDTO.getPageSize(), statisticDTO.getCurrentPage()); + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbCertificateOrgWisdomStatisticDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + private List buildReturnData(List allUserCertList, Map> certToUserMap, Map userBoundVOMap, Map orgMap) { + List ret = new ArrayList<>(); + for (FtbCertificateInfoVO certificateInfoVO : allUserCertList) { + List currUserIds = certToUserMap.get(certificateInfoVO.getId()); + List currUserBoundVOList = new ArrayList<>(); + currUserIds.forEach(userId -> { + UserBoundVO userBoundVOS = userBoundVOMap.get(userId); + if (userBoundVOS != null) { + currUserBoundVOList.add(userBoundVOS); + } + }); + if (CollUtil.isEmpty(currUserBoundVOList)) { + continue; + } + //按照组织分组 + Map> orgUserMap = currUserBoundVOList.stream().collect(Collectors.groupingBy(UserBoundVO::getOrganizeId)); + for (Map.Entry> stringListEntry : orgUserMap.entrySet()) { + String orgId = stringListEntry.getKey(); + List value = stringListEntry.getValue(); + OrganizeGeneralDetailVO organizeGeneralDetailVO = orgMap.get(orgId); + if (organizeGeneralDetailVO == null) { + continue; + } + FtbCertificateOrgWisdomStatisticVO vo = new FtbCertificateOrgWisdomStatisticVO(); + vo.setCertificateId(certificateInfoVO.getId()); + vo.setCertificateName(certificateInfoVO.getName()); + vo.setOnShelfStatus(String.valueOf(certificateInfoVO.getStatus())); + vo.setOrgID(organizeGeneralDetailVO.getId()); + vo.setOrgName(organizeGeneralDetailVO.getOrganizeTreeName()); + vo.setNumberOfPeopleIssued(value.size()); + vo.setSysOrgID(organizeGeneralDetailVO.getEnCode()); + ret.add(vo); + } + } + return ret; + } + + @Override + public PageListVO personalDimensionStatistics(FtbCertificatePersonWisdomStatisticDTO personWisdomStatisticDTO) { + Page page = personWisdomStatisticDTO.coverCultivatePage(); + if (StringUtils.isNotEmpty(personWisdomStatisticDTO.getEmployeeIds())) { + List strings = cultivatePerUtils.convertEmployeeIdCollection(personWisdomStatisticDTO.getEmployeeIds()); + if (CollUtil.isEmpty(strings)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + personWisdomStatisticDTO.setRealUserIds(strings); + } + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getRealUserIds())) { + List intersection = UserApiV2Util.getIntersection(personWisdomStatisticDTO.getRealUserIds(), personWisdomStatisticDTO.getSelectPeoples()); + if (CollUtil.isEmpty(intersection)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + personWisdomStatisticDTO.setRealUserIds(intersection); + } + personWisdomStatisticDTO.setRealUserIds(personWisdomStatisticDTO.getSelectPeoples()); + } + if (StringUtils.isNotEmpty(personWisdomStatisticDTO.getKeyWords())) { + List userForNameOrPhone = userApiV2Util.getUserForNameOrPhone(personWisdomStatisticDTO.getKeyWords(), "", UserProvider.getUser().getTenantId()); + if (CollUtil.isEmpty(userForNameOrPhone)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + List nameUserIds = userForNameOrPhone.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getRealUserIds())) { + List intersection = UserApiV2Util.getIntersection(nameUserIds, personWisdomStatisticDTO.getRealUserIds()); + if (CollUtil.isEmpty(intersection)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + personWisdomStatisticDTO.setRealUserIds(nameUserIds); + } + } + if (StringUtils.isNotEmpty(personWisdomStatisticDTO.getOrgID())) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(List.of(personWisdomStatisticDTO.getOrgID()), null); + if (CollectionUtil.isEmpty(userListForOrgIds)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + List orgUserIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getRealUserIds())) { + List intersection = UserApiV2Util.getIntersection(orgUserIds, personWisdomStatisticDTO.getRealUserIds()); + if (CollUtil.isEmpty(intersection)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + personWisdomStatisticDTO.setRealUserIds(intersection); + } else { + personWisdomStatisticDTO.setRealUserIds(orgUserIds); + } + } + List searchPostId = new ArrayList<>(); + if (StringUtils.isNotEmpty(personWisdomStatisticDTO.getPostIds())) { + searchPostId = cultivatePerUtils.convertPositionIdCollection(personWisdomStatisticDTO.getPostIds()); + if (CollUtil.isEmpty(searchPostId)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + } + if (StringUtils.isNotEmpty(personWisdomStatisticDTO.getPostId())) { + searchPostId.add(personWisdomStatisticDTO.getPostId()); + } + if (CollUtil.isNotEmpty(searchPostId)) { + List userListForPositions = userApiV2Util.getUserListForPositions(searchPostId, UserProvider.getUser().getTenantId()); + if (CollUtil.isEmpty(userListForPositions)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + List positionUserIds = userListForPositions.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getRealUserIds())) { + List intersection = UserApiV2Util.getIntersection(positionUserIds, personWisdomStatisticDTO.getRealUserIds()); + if (CollUtil.isEmpty(intersection)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + personWisdomStatisticDTO.setRealUserIds(intersection); + } else { + personWisdomStatisticDTO.setRealUserIds(positionUserIds); + } + } + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getRealUserIds())) { + List intersection = UserApiV2Util.getIntersection(personWisdomStatisticDTO.getRealUserIds(), innerPowerUserVO.getUserIds()); + if (CollUtil.isEmpty(intersection)) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + personWisdomStatisticDTO.setRealUserIds(intersection); + } else { + personWisdomStatisticDTO.setRealUserIds(innerPowerUserVO.getUserIds()); + } + } else if (innerPowerUserVO.getCode() == 2) { + return UserApiV2Util.returnEmptyListGeneric(personWisdomStatisticDTO.getCurrentPage(), personWisdomStatisticDTO.getPageSize()); + } + page = baseMapper.personalDimensionStatistics(personWisdomStatisticDTO, page); +// List orgCollect = page.getRecords().stream().map(FtbCertificatePersonWisdomStatisticVO::getOrgID).collect(Collectors.toList()); +// List postCollect = page.getRecords().stream().map(FtbCertificatePersonWisdomStatisticVO::getSysPostId).collect(Collectors.toList()); + List employeeIdList = page.getRecords().stream().map(FtbCertificatePersonWisdomStatisticVO::getEmployeeID).collect(Collectors.toList()); + // 获取转换id +// Map orgMap = cultivatePerUtils.convertOrganizationalId(orgCollect); + Map perMap = cultivatePerUtils.coverPersonalIds(employeeIdList); +// Map postMap = cultivatePerUtils.convertPostId(postCollect); + // 岗位id转换 + Map> userIdMap = userApiV2Util.getUserPrimaryBoundBatchCompatible(employeeIdList, null); + page.getRecords().forEach(item -> { + if (userIdMap != null && userIdMap.containsKey(item.getEmployeeID())) { + List userBoundMoreInfoVOS = userIdMap.get(item.getEmployeeID()); + List positionInfos = userBoundMoreInfoVOS.stream().map(a -> { + FtbCultivatePositionPersonStatisticesVO.PositionInfo positionInfo = new FtbCultivatePositionPersonStatisticesVO.PositionInfo(); + positionInfo.setPositionName(a.getPositionName()); + positionInfo.setPositionGradesName(a.getGradeName()); + return positionInfo; + }).collect(Collectors.toList()); +// String string = positionInfos.stream().map(vo -> vo.getPositionName() + "_" + vo.getPositionGradesName()).collect(Collectors.joining(";")); + String string = ""; + if (CollUtil.isNotEmpty(userBoundMoreInfoVOS)) { + UserBoundVO userBoundVO = userBoundMoreInfoVOS.get(0); + string = userBoundVO.getPositionName(); + if (StringUtil.isNotEmpty(userBoundVO.getGradeName())) { + string = string + "_" + userBoundVO.getGradeName(); + } + item.setOrganizationSystemDisplayId(userBoundVO.getOrganizeEnCode()); + item.setOrgName(userBoundVO.getOrganizeTreeName()); + item.setSysEmployeeID(perMap.get(item.getEmployeeID())); + item.setPostId(userBoundVO.getPositionEnCode()); + } + item.setPositionLevel(string); + + } + +// OrganizeEntity infoById = organizeApi.getInfoById(item.getOrgID()); +// OrganizeGeneralDetailVO infoById = userApiV2Util.organizeInfoById(item.getOrgID(), null); +// if (infoById != null) { +// item.setOrgName(infoById.getOrganizeTreeName()); +// } +// item.setSysEmployeeID(perMap.get(item.getEmployeeID())); + + }); + return CultivatePage.coverPageList(page); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateUserServiceImpl.java new file mode 100644 index 0000000..3a9d088 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCertificateUserServiceImpl.java @@ -0,0 +1,200 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.FtbCultivateCertificateUserMapper; +import jnpf.cultivate.service.FtbCultivateCertificateService; +import jnpf.cultivate.service.FtbCultivateCertificateUserService; +import jnpf.model.cultivate.dto.certificate.FtbCertificateUserForm; +import jnpf.model.cultivate.po.certificate.CertificateUserPagination; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author hao.peng + */ +@Service +public class FtbCultivateCertificateUserServiceImpl extends ServiceImpl implements FtbCultivateCertificateUserService { + @Autowired + + private UserProvider userProvider; + @Autowired + private FtbCultivateCertificateService certificateService; + @Override + public List getList(CertificateUserPagination pagination) { + //通过UserProvider获取用户信息 + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtils.isEmpty(pagination.getKeyword())) { + queryWrapper.lambda().and( + t -> t.like(FtbCertificateUserEntity::getUserName, pagination.getKeyword()) + ); + } + if (!StringUtils.isEmpty(pagination.getCertificateId())) { + queryWrapper.lambda().eq(FtbCertificateUserEntity::getCertificateId, pagination.getCertificateId()); + } + if (!StringUtils.isEmpty(pagination.getUserId())) { + queryWrapper.lambda().eq(FtbCertificateUserEntity::getUserId, pagination.getUserId()); + } + queryWrapper.lambda().eq(FtbCertificateUserEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + //排序 + if (StringUtils.isEmpty(pagination.getSidx())) { + } else { + queryWrapper = "asc".equalsIgnoreCase(pagination.getSort()) ? queryWrapper.orderByAsc(pagination.getSidx()) : queryWrapper.orderByDesc(pagination.getSidx()); + } + queryWrapper.lambda().eq(FtbCertificateUserEntity::getEnabledMark, 1); + updateStatus(queryWrapper); + Page page = new Page<>(pagination.getCurrentPage(), pagination.getPageSize()); + IPage userIPage = this.page(page, queryWrapper); + return pagination.setData(userIPage.getRecords(), page.getTotal()); + } + + @Override + public FtbCertificateUserEntity getInfo(String id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda().eq(FtbCertificateUserEntity::getId, id); + return this.getOne(queryWrapper); + } + + @Override + @DSTransactional + public void create(FtbCertificateUserForm form) { + List userInfoFormList = form.getUserInfoFormList(); + if (CollectionUtils.isEmpty(userInfoFormList)) { + return; + } + FtbCertificateEntity certificateEntity = certificateService.getById(form.getCertificateId()); + FtbCertificateUserEntity entity = new FtbCertificateUserEntity(); + entity.setCertificateId(form.getCertificateId()); + entity.setCertificateName(certificateEntity.getName()); + entity.setCompanyName(certificateEntity.getCompanyName()); + entity.setStatus(CourseEnums.CertUserStatus.USING.getCode()); + entity.setEffectTime(LocalDateTime.now()); + entity.setReason(form.getReason()); + if (certificateEntity.getExpireTime() > 0 && !CourseEnums.CertExpireType.NEVER.getCode().equals(certificateEntity.getExpireType())) { + //设置失效时间 ExpireTime=0时 null表示永久 + LocalDateTime expireTime = LocalDateTime.now(); + if (CourseEnums.CertExpireType.DAY.getCode().equals(certificateEntity.getExpireType())) { + expireTime = expireTime.plusDays(certificateEntity.getExpireTime().longValue()); + } else if (CourseEnums.CertExpireType.MONTH.getCode().equals(certificateEntity.getExpireType())) { + expireTime = expireTime.plusMonths(certificateEntity.getExpireTime().longValue()); + } + Date date = Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()); + entity.setExpireTime(date); + } + UserInfo userInfo = userProvider.get(); + List arrayList = new ArrayList<>(); + userInfoFormList.stream().forEach(item -> { + String userId = item.getUserId(); + CertificateUserPagination certificateUserPagination = new CertificateUserPagination(); + certificateUserPagination.setCertificateId(form.getCertificateId()); + certificateUserPagination.setUserId(userId); + List list = getList(certificateUserPagination); + List collect = list.stream().filter(vo -> vo.getStatus() == 0).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)){ + arrayList.add(item.getUserName()); + } + }); + if (CollUtil.isNotEmpty(arrayList)){ + throw new RuntimeException(String.join(",",arrayList)+"用户已存在证书,请勿重复颁发!"); + } + List entityList = new ArrayList<>(); + for (FtbCertificateUserForm.UserInfoForm userInfoForm : userInfoFormList) { + entity.setId(IdWorker.getIdStr()); + entity.setUserId(userInfoForm.getUserId()); + entity.setUserName(userInfoForm.getUserName()); + entity.setOrganizeId(userInfoForm.getOrganizeId()); + entity.setPositionId(userInfoForm.getPositionId()); + entity.setCreatorUserId(userInfo.getUserId()); + String basedOnTheModule = SelfGrowthUtil.provideACustomIDBasedOnTheModule( + SelfrowingEnum.CERTIFICATE_ENCODING_PREFIX,4); + entity.setNumber(basedOnTheModule); + entity.setTenantId(userInfo.getTenantId()); + entity.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCertificateUserEntity entitySave = new FtbCertificateUserEntity(); + BeanUtils.copyProperties(entity, entitySave); + entityList.add(entitySave); + } + this.saveBatch(entityList); + } + + @Override + @Transactional + public void update(String id, FtbCertificateUserEntity entity) { + entity.setId(id); + entity.setLastModifyUserId(userProvider.get().getUserId()); + this.updateById(entity); + } + + @Override + public boolean delete(String id) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FtbCertificateUserEntity::getId, id); + List users = this.list(wrapper); + FtbCertificateUserEntity user = users.get(0); + if (ObjectUtils.isEmpty(user)) { + return false; + } + if (1 == user.getStatus() || 2 == user.getStatus()) { + return false; + } + user.setStatus(2); + this.updateById(user); + return true; + } + + @Override + public List userList(String userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbCertificateUserEntity::getUserId, userId); + wrapper.lambda().eq(FtbCertificateUserEntity::getStatus, 0); + wrapper.lambda().eq(FtbCertificateUserEntity::getEnabledMark, 1); + updateStatus(wrapper); + // 更改失效不显示 + return this.list(new LambdaQueryWrapper() + .eq(FtbCertificateUserEntity::getUserId, userId) + .eq(FtbCertificateUserEntity::getEnabledMark, 1) + ); + } + + /** + * 更新证书状态 + * + * @param queryWrapper queryWrapper + */ + @Override + public void updateStatus(QueryWrapper queryWrapper) { + //更新证书状态 + List users = super.list(queryWrapper); + List updates = users.stream().filter(r -> ObjectUtils.isNotEmpty(r.getExpireTime()) + && cn.hutool.core.date.DateUtil.date().after(r.getExpireTime())) + .map(r -> r.setStatus(1)).collect(Collectors.toList()); + this.updateBatchById(updates); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestOptionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestOptionServiceImpl.java new file mode 100644 index 0000000..c285aa1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestOptionServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestOption; +import jnpf.cultivate.mapper.FtbCultivateChapterTestOptionMapper; +import jnpf.cultivate.service.FtbCultivateChapterTestOptionService; + +@Service +public class FtbCultivateChapterTestOptionServiceImpl extends ServiceImpl implements FtbCultivateChapterTestOptionService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestResultServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestResultServiceImpl.java new file mode 100644 index 0000000..395ccc5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestResultServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateChapterTestResultMapper; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import jnpf.cultivate.service.FtbCultivateChapterTestResultService; + +@Service +public class FtbCultivateChapterTestResultServiceImpl extends ServiceImpl implements FtbCultivateChapterTestResultService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestServiceImpl.java new file mode 100644 index 0000000..2bd2a3c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateChapterTestServiceImpl.java @@ -0,0 +1,103 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.FtbCultivateChapterTestMapper; +import jnpf.cultivate.mapper.FtbCultivateChapterTestResultMapper; +import jnpf.cultivate.service.FtbCultivateChapterTestService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.identify.FtbIdentityOrgWisdomStatisticDTO; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTest; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestResultVO; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestStatisticVO; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.identify.FtbCultivateIdentityOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyListVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +public class FtbCultivateChapterTestServiceImpl extends ServiceImpl implements FtbCultivateChapterTestService { + + @Resource + private FtbCultivateChapterTestResultMapper resultMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(CultivatePage statisticDTO){ + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + @Override + public PageListVO getChapterTestStatistic(String keyWords, + CultivatePage page) { + List innerPowerUserIds = new ArrayList<>(); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + innerPowerUserIds = innerPowerUserVO.getUserIds(); + } else if (innerPowerUserVO.getCode() == 2) { + return returnEmptyList(page); + } + Page pageInfo = page.coverCultivatePage(); + List statistic = baseMapper.getChapterTestStatistic(keyWords,innerPowerUserIds); + // 处理题型 + if (CollUtil.isNotEmpty(statistic)){ + statistic.forEach(item->{ + List chapterTests = resultMapper.selectChatperList(item.getCourseId(),item.getUserId()); + int numberOfTest = 0; + long numberOfCorrect = 0; + long numberOfWrong = 0; + List> list = new ArrayList<>(); + for(FtbCultivateChapterTestResult chapterTest : chapterTests){ + String chapterTestOptions = chapterTest.getChapterTestOptions(); + List resultVOS = JSONObject.parseArray(chapterTestOptions, FtbCultivateChapterTestResultVO.class); + if(CollUtil.isEmpty(resultVOS)) continue; + list.add(resultVOS); + // 单章题目数量 + numberOfTest += resultVOS.size(); + // 单章正确题数量 + numberOfCorrect += resultVOS.stream().filter(resultVO -> resultVO.getIsRight() == 1).count(); + // 单章错误题数量 + numberOfWrong += resultVOS.stream().filter(resultVO -> resultVO.getIsRight() == 0).count(); + } + item.setNumberOfTest(numberOfTest); + // 正确率 + BigDecimal correctRate = BigDecimal.ZERO; + if (numberOfTest != 0 ) correctRate = BigDecimal.valueOf(numberOfCorrect).divide(BigDecimal.valueOf(numberOfTest), 2, BigDecimal.ROUND_HALF_UP).multiply(BigDecimal.valueOf(100)); + item.setNumberOfCorrect((int) numberOfCorrect); + item.setCorrectRate(correctRate); + // 错误率 + BigDecimal errorRate = BigDecimal.ZERO; + if (numberOfTest != 0 ) errorRate = BigDecimal.valueOf(numberOfWrong).divide(BigDecimal.valueOf(numberOfTest), 2, BigDecimal.ROUND_HALF_UP).multiply(BigDecimal.valueOf(100)); + item.setNumberOfWrong((int) numberOfWrong); + item.setErrorRate(errorRate); + item.setInClassTestResults(list); + }); + } + return CultivatePage.paginate(statistic,pageInfo); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseAppServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseAppServiceImpl.java new file mode 100644 index 0000000..1d06e80 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseAppServiceImpl.java @@ -0,0 +1,397 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskAssignmentMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceChapterLearningMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.service.FtbCultivateCourseAppService; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.cultivate.service.FtbCultivateCourseSettingService; +import jnpf.cultivate.service.FtbCultivateCourseTriggerLogService; +import jnpf.model.cultivate.dto.course.app.FtbChapterStudyDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.CommonCourseProcessLearnDTO; +import jnpf.model.cultivate.event.dto.position.PositionCourseProcessDTO; +import jnpf.model.cultivate.event.dto.task.TaskCourseProcessDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.course.FtbCultivateCourseTriggerLog; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.vo.course.app.ChapterInformationVO; +import jnpf.model.cultivate.vo.course.app.FtbChapterAppDetails; +import jnpf.model.cultivate.vo.course.app.FtbCourseDetailsAppVO; +import jnpf.model.cultivate.vo.course.app.FtbCourseOutlineAppVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterDetailsVO; +import jnpf.util.context.SpringContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Title:FtbCultivateCourseAppServiceImpl + * @Author:peng.hao + * @create: 2023/12/2911:21 + */ +@Slf4j +@Service +public class FtbCultivateCourseAppServiceImpl implements FtbCultivateCourseAppService { + @Resource + private FtbCultivateCourseMapper cultivateCourseMapper; + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + @Resource + private FtbCultivatePositionCourceChapterLearningMapper ftbCultivatePositionCourceChapterLearningMapper; + @Resource + private FtbCultivateCourseSettingService ftbCultivateCourseSettingService; + @Resource + private FtbCultivateCourseChapterService ftbCultivateCourseChapterService; + + + @Resource + private FtbCultivateCourseTriggerLogService ftbCultivateCourseTriggerLogService; + + @Resource + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + + + @Override + public FtbCourseDetailsAppVO courseDetails(String courseId, String userId) { + FtbCourseDetailsAppVO ftbCourseDetailsAppVO = new FtbCourseDetailsAppVO(); + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectById(courseId); + if (Objects.nonNull(ftbCultivateCourse)) { + ftbCourseDetailsAppVO.setCourseId(courseId); + ftbCourseDetailsAppVO.setCourseName(ftbCultivateCourse.getName()); + ftbCourseDetailsAppVO.setHighLights(ftbCultivateCourse.getHighLights()); + ftbCourseDetailsAppVO.setUpdateTime(ftbCultivateCourse.getLastModifyTime()); + ftbCourseDetailsAppVO.setViews(ftbCultivateCourse.getViews()); + ftbCourseDetailsAppVO.setSubtitle(ftbCultivateCourse.getSubtitle()); + } + // 学习总时长 + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId); + lambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + lambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = ftbCultivatePositionCourceLearningMapper.selectOne(lambdaQueryWrapper); + if (Objects.nonNull(ftbCultivatePositionCourceLearning)) { + ftbCourseDetailsAppVO.setDuration(ftbCultivatePositionCourceLearning.getLearnTime()); + } + // 课程学习设置 + ftbCultivateCourseSettingService.buildUpCourseLearningSettings(ftbCourseDetailsAppVO, courseId); + return ftbCourseDetailsAppVO; + } + + @Override + @Transactional + public void chapterStudy(UserInfo userInfo, FtbChapterStudyDTO ftbChapterStudyDTO) { + log.error("chapterStudy={}", ftbChapterStudyDTO); + // 课程学习类型,0岗位学习,1通用课程,2任务学习 + JnpfApplicationEvent jpfApplicationEvent = new JnpfApplicationEvent<>(Void.TYPE); + if (ftbChapterStudyDTO.getType() == 0) { + jpfApplicationEvent = new JnpfApplicationEvent<>( + new PositionCourseProcessDTO( + userInfo.getUserId(), + ftbChapterStudyDTO.getCourseId(), + ftbChapterStudyDTO.getChapterId(), + ftbChapterStudyDTO.getDuration(), + ftbChapterStudyDTO.getFtbChapterTestDTOs(), + ftbChapterStudyDTO.getGradeId(), + ftbChapterStudyDTO.getType() + )); + } else if (ftbChapterStudyDTO.getType() == 1) { + jpfApplicationEvent = new JnpfApplicationEvent<>( + new CommonCourseProcessLearnDTO( + userInfo.getUserId(), + ftbChapterStudyDTO.getCourseId(), + ftbChapterStudyDTO.getChapterId(), + ftbChapterStudyDTO.getDuration(), + ftbChapterStudyDTO.getFtbChapterTestDTOs() + )); + } else if (ftbChapterStudyDTO.getType() == 2) { + //任务完成也就不再触发 + LambdaQueryWrapper taskWrapper = Wrappers.lambdaQuery(); + taskWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, ftbChapterStudyDTO.getTaskId()); + taskWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userInfo.getUserId()); + taskWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + + List ftbCultivateLearnTaskAssignments = ftbCultivateLearnTaskAssignmentMapper.selectList(taskWrapper); + if (CollUtil.isEmpty(ftbCultivateLearnTaskAssignments)) { + return; + } + if (ftbCultivateLearnTaskAssignments.get(0).getStudyStats().equals(2)) { + return; + } + jpfApplicationEvent = new JnpfApplicationEvent<>( + new TaskCourseProcessDTO( + userInfo.getUserId(), + ftbChapterStudyDTO.getCourseId(), + ftbChapterStudyDTO.getChapterId(), + ftbChapterStudyDTO.getDuration(), + ftbChapterStudyDTO.getFtbChapterTestDTOs(), + ftbChapterStudyDTO.getTaskId(), + ftbChapterStudyDTO.getType() + )); + } else if (ftbChapterStudyDTO.getType() == 3) { + jpfApplicationEvent = new JnpfApplicationEvent<>( + new CommonCourseProcessLearnDTO( + userInfo.getUserId(), + ftbChapterStudyDTO.getCourseId(), + ftbChapterStudyDTO.getChapterId(), + ftbChapterStudyDTO.getDuration(), + ftbChapterStudyDTO.getFtbChapterTestDTOs() + )); + } + SpringContext.getApplicationContext().publishEvent(jpfApplicationEvent); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void courseBrowsing(String courseId) { + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectById(courseId); + if (Objects.nonNull(ftbCultivateCourse)) { + LambdaUpdateWrapper ftbCultivateCourseLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + ftbCultivateCourseLambdaUpdateWrapper.eq(FtbCultivateCourse::getId, courseId); + Integer views = ftbCultivateCourse.getViews() == null ? 1 : ftbCultivateCourse.getViews() + 1; + ftbCultivateCourseLambdaUpdateWrapper.set(FtbCultivateCourse::getViews, views); + cultivateCourseMapper.update(null, ftbCultivateCourseLambdaUpdateWrapper); + } + } + + + /** + * 获取课程所有章节 + * + * @param courseId 课程ID + * @return 章节列表 + */ + private List getCourseChapters(String courseId) { + LambdaQueryWrapper courseChapterWrapper = Wrappers.lambdaQuery(); + courseChapterWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId); + courseChapterWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + return ftbCultivateCourseChapterService.list(courseChapterWrapper); + } + + private Map batchQueryCourseChapterLearningState(String userId, List chapterList) { + + Map chapterLearningMap = new HashMap<>(); + LambdaQueryWrapper queryChapterWrapper = Wrappers.lambdaQuery(); + queryChapterWrapper.in(FtbCultivatePositionCourceChapterLearning::getChapterId, chapterList); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List list = ftbCultivatePositionCourceChapterLearningMapper.selectList(queryChapterWrapper); + if (CollUtil.isEmpty(list)) { + return chapterLearningMap; + } + + for (FtbCultivatePositionCourceChapterLearning chapterLearning : list) { + chapterLearningMap.put(chapterLearning.getChapterId(),chapterLearning); + } + return chapterLearningMap; + } + + @Override + public FtbCourseOutlineAppVO courseOutline(String courseId, String userId) { + FtbCourseOutlineAppVO ftbCourseOutlineAppVO = new FtbCourseOutlineAppVO(); + LambdaQueryWrapper ftbCultivateCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseLambdaQueryWrapper.select(FtbCultivateCourse::getFormat); + ftbCultivateCourseLambdaQueryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, courseId); + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectOne(ftbCultivateCourseLambdaQueryWrapper); + if (ftbCultivateCourse==null) { + throw new RuntimeException("课程不存在"); + } + List chapterInformations = new ArrayList<>(); + List courseChaptersList = getCourseChapters(courseId); + if (CollUtil.isNotEmpty(courseChaptersList)) { + List chapterIds = courseChaptersList.stream().map(FtbCultivateCourseChapter::getId).collect(Collectors.toList()); + Map chapterLearningMap = batchQueryCourseChapterLearningState(userId, chapterIds); + chapterInformations = buildChapterInformations(courseChaptersList, chapterLearningMap,ftbCultivateCourse,courseId,userId); + } + ftbCourseOutlineAppVO.setChapterInformations(chapterInformations); + ftbCourseOutlineAppVO.setChapterTotalNumber(chapterInformations.size()); + List collect = chapterInformations.stream() + .filter(chapterInformationVO -> chapterInformationVO.getState() == 1).collect(Collectors.toList()); + ftbCourseOutlineAppVO.setChapterFinishedNumber(collect.size()); + return ftbCourseOutlineAppVO; + } + + private List buildChapterInformations(List courseChaptersList, + Map chapterLearningMap, + FtbCultivateCourse ftbCultivateCourse, + String courseId, String userId + ) { + List ret = new ArrayList<>(); + List addChapterLearningList = new ArrayList<>(); + for (FtbCultivateCourseChapter chapter : courseChaptersList) { + FtbCultivatePositionCourceChapterLearning chapterLearning = chapterLearningMap.get(chapter.getId()); + ChapterInformationVO chapterInformationVO = new ChapterInformationVO(); + chapterInformationVO.setChapterId(chapter.getId()); + chapterInformationVO.setChapterName(chapter.getName()); + chapterInformationVO.setSortCode(chapter.getSortCode()); + if (chapterLearning != null) { + chapterInformationVO.setState(chapterLearning.getState()); + chapterInformationVO.setChapterTime(chapterLearning.getChapterTime()); + }else{ + chapterInformationVO.setState(0); + chapterInformationVO.setChapterTime(0); + + FtbCultivatePositionCourceChapterLearning newLearnChapterLearning = new FtbCultivatePositionCourceChapterLearning(); + newLearnChapterLearning.setUserId(userId); + newLearnChapterLearning.setChapterId(chapter.getId()); + newLearnChapterLearning.setCourceId(courseId); + newLearnChapterLearning.setState(0); + newLearnChapterLearning.setEnabledMark(0); + addChapterLearningList.add(newLearnChapterLearning); + } + chapterInformationVO.setFormat(ftbCultivateCourse.getFormat()); + ret.add(chapterInformationVO); + } + if(CollUtil.isNotEmpty(addChapterLearningList)){ + for (FtbCultivatePositionCourceChapterLearning chapterLearning : addChapterLearningList) { + ftbCultivatePositionCourceChapterLearningMapper.insert(chapterLearning); + } + } + return ret; + } + + @Override + @Transactional + public void durationRecord(UserInfo userInfo, FtbChapterStudyDTO ftbChapterStudyDTO) { + // 学习课程章节时长增加 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionCourceChapterLearning::getChapterId, ftbChapterStudyDTO.getChapterId()); + updateWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userInfo.getUserId()); + updateWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + updateWrapper.set(FtbCultivatePositionCourceChapterLearning::getChapterTime, ftbChapterStudyDTO.getDuration()); + ftbCultivatePositionCourceChapterLearningMapper.update(new FtbCultivatePositionCourceChapterLearning(), updateWrapper); + // 学习课程状态变更 + LambdaQueryWrapper courceLearningLambdaQueryWrapper = Wrappers.lambdaQuery(); + courceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, ftbChapterStudyDTO.getCourseId()); + courceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userInfo.getUserId()); + courceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + courceLearningLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = ftbCultivatePositionCourceLearningMapper.selectOne(courceLearningLambdaQueryWrapper); + ftbCultivatePositionCourceLearning.setLearnTime(ftbCultivatePositionCourceLearning.getLearnTime() + ftbChapterStudyDTO.getDuration()); + ftbCultivatePositionCourceLearningMapper.updateById(ftbCultivatePositionCourceLearning); + + + //补充触发 + if (ftbCultivatePositionCourceLearning != null && ftbCultivatePositionCourceLearning.getState().equals(1)) { + supplementLearningTrigger(userInfo.getUserId(), ftbChapterStudyDTO); + } + } + + + private void supplementLearningTrigger(String userId, FtbChapterStudyDTO ftbChapterStudyDTO) { + log.error("补充触发:chapterStudy={}", ftbChapterStudyDTO); + // 课程学习类型,0岗位学习,1通用课程,2任务学习 + LambdaQueryWrapper triggerLogLambdaQueryWrapper = Wrappers.lambdaQuery(); + triggerLogLambdaQueryWrapper.eq(FtbCultivateCourseTriggerLog::getUserId, userId); + triggerLogLambdaQueryWrapper.eq(FtbCultivateCourseTriggerLog::getCourseId, ftbChapterStudyDTO.getCourseId()); + triggerLogLambdaQueryWrapper.eq(FtbCultivateCourseTriggerLog::getType, ftbChapterStudyDTO.getType()); + if (ftbChapterStudyDTO.getType() == 0) { + triggerLogLambdaQueryWrapper.eq(FtbCultivateCourseTriggerLog::getBusinessId, ftbChapterStudyDTO.getGradeId()); + long count = ftbCultivateCourseTriggerLogService.count(triggerLogLambdaQueryWrapper); + if (count > 0) { + return; + } + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + new PositionCourseProcessDTO( + userId, + ftbChapterStudyDTO.getCourseId(), + ftbChapterStudyDTO.getChapterId(), + ftbChapterStudyDTO.getDuration(), + ftbChapterStudyDTO.getFtbChapterTestDTOs(), + ftbChapterStudyDTO.getGradeId(), + ftbChapterStudyDTO.getType() + ))); + } else if (ftbChapterStudyDTO.getType() == 2) { + if (StringUtils.isNotEmpty(ftbChapterStudyDTO.getTaskId())) { + log.error("补充触发:chapterStudy={},任务id不能为null", ftbChapterStudyDTO); + return; + } + //任务完成也就不再触发 + LambdaQueryWrapper taskWrapper = Wrappers.lambdaQuery(); + taskWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, ftbChapterStudyDTO.getTaskId()); + taskWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + taskWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + + List ftbCultivateLearnTaskAssignments = ftbCultivateLearnTaskAssignmentMapper.selectList(taskWrapper); + if (CollUtil.isEmpty(ftbCultivateLearnTaskAssignments)) { + return; + } + if (ftbCultivateLearnTaskAssignments.get(0).getStudyStats().equals(2)) { + return; + } + triggerLogLambdaQueryWrapper.eq(FtbCultivateCourseTriggerLog::getBusinessId, ftbChapterStudyDTO.getTaskId()); + long count = ftbCultivateCourseTriggerLogService.count(triggerLogLambdaQueryWrapper); + if (count > 0) { + return; + } + + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + new TaskCourseProcessDTO( + userId, + ftbChapterStudyDTO.getCourseId(), + ftbChapterStudyDTO.getChapterId(), + ftbChapterStudyDTO.getDuration(), + ftbChapterStudyDTO.getFtbChapterTestDTOs(), + ftbChapterStudyDTO.getTaskId(), + ftbChapterStudyDTO.getType() + ))); + } + } + + + @Override + public FtbChapterAppDetails chapterDetails(String id) { + FtbCultivateCourseChapterDetailsVO ftbCultivateCourseChapterDetailsVO = ftbCultivateCourseChapterService.courseDetails(id); + FtbChapterAppDetails ftbChapterAppDetails = FtbChapterAppDetails.convertFtbChapterAppDetails(ftbCultivateCourseChapterDetailsVO); + // 上一章和下一章节id + LambdaQueryWrapper ftbCultivateCourseChapterLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(FtbCultivateCourseChapter::getCourseId, ftbChapterAppDetails.getCourseId()); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + ftbCultivateCourseChapterLambdaQueryWrapper.orderByAsc(FtbCultivateCourseChapter::getSortCode, SuperBaseEntity.SuperIBaseEntity::getId); + List ftbCultivateCourseChapters = ftbCultivateCourseChapterService.getBaseMapper() + .selectList(ftbCultivateCourseChapterLambdaQueryWrapper); + FtbCultivateCourseChapter[] ftbCultivateCourseChaptersArray = ftbCultivateCourseChapters.toArray(new FtbCultivateCourseChapter[0]); + int j = 0; + for (int i = 0; i < ftbCultivateCourseChaptersArray.length; i++) { + if (ftbCultivateCourseChaptersArray[i].getId().equals(ftbChapterAppDetails.getId())) { + j = i; + break; + } + } + if (j + 1 > ftbCultivateCourseChaptersArray.length - 1) { + ftbChapterAppDetails.setNextId(null); + } else { + ftbChapterAppDetails.setNextId(ftbCultivateCourseChaptersArray[j + 1].getId()); + } + if (j - 1 < 0) { + ftbChapterAppDetails.setPreId(null); + } else { + ftbChapterAppDetails.setPreId(ftbCultivateCourseChaptersArray[j - 1].getId()); + } + // 章节详情 + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectById(ftbChapterAppDetails.getCourseId()); + if (ftbCultivateCourse != null) { + ftbChapterAppDetails.setFormat(ftbCultivateCourse.getFormat()); + } + return ftbChapterAppDetails; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseChapterServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseChapterServiceImpl.java new file mode 100644 index 0000000..17c5621 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseChapterServiceImpl.java @@ -0,0 +1,418 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.CultivateCourseMsgMapper; +import jnpf.cultivate.mapper.FtbCultivateChapterTestResultMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseChapterMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.service.FtbCultivateChapterTestOptionService; +import jnpf.cultivate.service.FtbCultivateChapterTestService; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.cultivate.service.FtbCultivateCourseSettingService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseChapterDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseChapterUpdateDTO; +import jnpf.model.cultivate.dto.course.app.FtbGlobalCurriculumAppWrapDTO; +import jnpf.model.cultivate.dto.course.testoption.FtbCultivateChapterTestDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.CourseEventDTO; +import jnpf.model.cultivate.event.dto.course.PositionCourseEventDTO; +import jnpf.model.cultivate.po.course.*; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsg; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterAppDetails; +import jnpf.model.cultivate.v2.course.vo.app.V2CultivateChapterTestDto; +import jnpf.model.cultivate.v2.course.vo.app.V2CultivateChapterTestOptionDto; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppWrapVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterDetailsVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseChapterVO; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbCultivateCourseChapterServiceImpl extends ServiceImpl implements FtbCultivateCourseChapterService { + @Resource + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Resource + private FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + + @Resource + private FtbCultivatePositionCourceLearningService ftbCultivatePositionCourceLearningService; + + @Resource + private FtbCultivatePositionCourceChapterLearningService ftbCultivatePositionCourceChapterLearningService; + + @Resource + private FtbCultivateChapterTestService ftbCultivateChapterTestService; + + @Resource + private FtbCultivateChapterTestOptionService ftbCultivateChapterTestOptionService; + + @Resource + private FtbCultivateCourseSettingService ftbCultivateCourseSettingService; + + @Resource + private CultivateCourseMsgMapper cultivateCourseMsgMapper; + + + @Override + @Transactional + public void add(FtbCultivateCourseChapterDTO ftbCultivateCourseDTO) { + FtbCultivateCourseChapter ftbCultivateCourseChapter = ftbCultivateCourseDTO.convertFtbCultivateCourseChapter(ftbCultivateCourseDTO); + // 课程章节数+1 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseDTO.getCourseId()); + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectOne(queryWrapper); + ftbCultivateCourse.setChapterNumber(ftbCultivateCourse.getChapterNumber() + 1); + ftbCultivateCourseMapper.updateById(ftbCultivateCourse); + baseMapper.insert(ftbCultivateCourseChapter); + // 随堂测试 + doAddInClassTest(ftbCultivateCourseDTO, ftbCultivateCourse.getFormat(), ftbCultivateCourseChapter.getId()); + + // 学习课程章节发生变化 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(CourseEventDTO.builder() + .courseIds(List.of(ftbCultivateCourseDTO.getCourseId())).userIds(new ArrayList<>()).whetherToChange(true).build())); + asyncPosition(ftbCultivateCourse); + } + + /** + * 同步位置课程 + * + * @param ftbCultivateCourse + */ + @Override + public void asyncPosition(FtbCultivateCourse ftbCultivateCourse) { + Integer coursePositionSync = ftbCultivateCourse.getCoursePositionSync(); +// if(ftbCultivateCourse.getIsGroundIng()==0){ +// return; +// } + if (coursePositionSync == null || coursePositionSync == 0) { + return; + } + String learnJob = ftbCultivateCourse.getLearnJob(); + if (StringUtils.isEmpty(learnJob)) { + return; + } + List postion = List.of(learnJob.split(",")); + Boolean whetherToChange = false; + if (ftbCultivateCourse.getCourseUpdate() == 1) { + whetherToChange = true; + } + // 岗位课程事件 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + PositionCourseEventDTO.builder() + .courseIds(List.of(ftbCultivateCourse.getId())) + .postIds(postion) + .eventType(PositionCourseEventDTO.EventType.POST) + .whetherToChange(whetherToChange) + .build())); + } + + @Override + @Transactional + public void delete(String id) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbCultivateCourseChapter::getEnableMark, 1); + baseMapper.update(new FtbCultivateCourseChapter(), updateWrapper); + // 查询章节所绑定的课程id + LambdaQueryWrapper ftbCultivateCourseChapterLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + FtbCultivateCourseChapter ftbCultivateCourseChapter = baseMapper.selectOne(ftbCultivateCourseChapterLambdaQueryWrapper); + // 更新课程章节 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseChapter.getCourseId()); + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectOne(queryWrapper); + ftbCultivateCourse.setChapterNumber(ftbCultivateCourse.getChapterNumber() - 1); + ftbCultivateCourseMapper.updateById(ftbCultivateCourse); + // 随堂测试删除 + LambdaUpdateWrapper chapterTestLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + chapterTestLambdaUpdateWrapper.eq(FtbCultivateChapterTest::getCourseChapterId, id); + chapterTestLambdaUpdateWrapper.set(FtbCultivateChapterTest::getEnableMark, 1); + ftbCultivateChapterTestService.update(new FtbCultivateChapterTest(), chapterTestLambdaUpdateWrapper); + // 学习课程章节发生变化 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(CourseEventDTO.builder() + .courseIds(List.of(ftbCultivateCourseChapter.getCourseId())).userIds(new ArrayList<>()).whetherToChange(true).build())); + asyncPosition(ftbCultivateCourse); + } + + @Override + public Page queryList(Page page, String courseId) { + return baseMapper.queryList(page, courseId); + } + + @Override + @Transactional + public void updateChapter(FtbCultivateCourseChapterUpdateDTO ftbCultivateCourseDTO) { + // 章节更新 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseDTO.getId()); + updateWrapper.set(FtbCultivateCourseChapter::getName, ftbCultivateCourseDTO.getName()); + updateWrapper.set(FtbCultivateCourseChapter::getSortCode, ftbCultivateCourseDTO.getSortCode()); + updateWrapper.set(FtbCultivateCourseChapter::getAnnex, ftbCultivateCourseDTO.getAnnex()); + updateWrapper.set(FtbCultivateCourseChapter::getAnnexName, ftbCultivateCourseDTO.getAnnexName()); + updateWrapper.set(FtbCultivateCourseChapter::getDetailDesc, ftbCultivateCourseDTO.getDetailDesc()); + updateWrapper.set(FtbCultivateCourseChapter::getDuration, ftbCultivateCourseDTO.getDuration()); + updateWrapper.set(FtbCultivateCourseChapter::getWhetherTesting, ftbCultivateCourseDTO.getWhetherTesting()); + updateWrapper.set(FtbCultivateCourseChapter::getForcedStudy, ftbCultivateCourseDTO.getForcedStudy()); + updateWrapper.set(FtbCultivateCourseChapter::getChapterType, ftbCultivateCourseDTO.getChapterType()); + updateWrapper.set(FtbCultivateCourseChapter::getLibraryId, ftbCultivateCourseDTO.getLibraryId()); + baseMapper.update(new FtbCultivateCourseChapter(), updateWrapper); + // 随堂测试删除 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateCourse::getFormat, FtbCultivateCourse::getCourseUpdate, SuperBaseEntity.SuperIBaseEntity::getId); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseDTO.getCourseId()); + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectOne(queryWrapper); + // 视频格式且启用随堂测试 + if (ftbCultivateCourse.getFormat() == 1) { + if (ftbCultivateCourseDTO.getWhetherTesting() == 0) { + LambdaQueryWrapper deleteCourseChapterWrapper = Wrappers.lambdaQuery(); + deleteCourseChapterWrapper.eq(FtbCultivateChapterTest::getCourseChapterId, ftbCultivateCourseDTO.getId()); + List ftbCultivateChapterTests = ftbCultivateChapterTestService.list(deleteCourseChapterWrapper); + + List chapterTestIds = ftbCultivateChapterTests.stream() + .map(SuperBaseEntity.SuperIBaseEntity::getId) + .collect(Collectors.toList()); + + LambdaQueryWrapper deleteCourseChapterOptionWrapper = Wrappers.lambdaQuery(); + deleteCourseChapterOptionWrapper.in(FtbCultivateChapterTestOption::getChapterTestId, chapterTestIds); + if (CollUtil.isNotEmpty(chapterTestIds)) { + ftbCultivateChapterTestOptionService.getBaseMapper().delete(deleteCourseChapterOptionWrapper); + ftbCultivateChapterTestService.getBaseMapper().delete(deleteCourseChapterWrapper); + + } + doAddInClassTest(ftbCultivateCourseDTO, ftbCultivateCourse.getFormat(), ftbCultivateCourseDTO.getId()); + } + } + + // 课程内容更新需要重新学习(1是,2否) + if (ftbCultivateCourse.getCourseUpdate() == 2) { + // 有附件发生变化时才提示 + if (StrUtil.isNotBlank(ftbCultivateCourseDTO.getAnnex())) { + CultivateCourseMsg cultivateCourseMsg = new CultivateCourseMsg(); + cultivateCourseMsg.setDesc("该课程附件已发生变化。"); + cultivateCourseMsg.setState(1); + cultivateCourseMsg.setCourseId(ftbCultivateCourse.getId()); + cultivateCourseMsgMapper.insert(cultivateCourseMsg); + } + } else if (ftbCultivateCourse.getCourseUpdate() == 1) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(CourseEventDTO.builder() + .courseIds(List.of(ftbCultivateCourseDTO.getCourseId())).userIds(new ArrayList<>()).whetherToChange(true).build())); + } + + asyncPosition(ftbCultivateCourse); + } + + @Override + @Transactional + public FtbGlobalCurriculumAppWrapVO generalCourseList(Page page, FtbGlobalCurriculumAppWrapDTO ftbGlobalCurriculumAppWrapDTO) { + FtbGlobalCurriculumAppWrapVO ftbGlobalCurriculumAppWrapVO = new FtbGlobalCurriculumAppWrapVO(); + String userId = UserProvider.getUser().getUserId(); + // 检测当前全局课程 + LambdaQueryWrapper ftbCultivateCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseLambdaQueryWrapper.select(FtbCultivateCourse::getId); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getLabel, 1); + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(ftbCultivateCourseLambdaQueryWrapper); + // 查询此用户不在课程中的学习记录 + LambdaQueryWrapper ftbCultivatePositionCourceLearningLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionCourceLearningLambdaQueryWrapper.select(FtbCultivatePositionCourceLearning::getCourceId); + ftbCultivatePositionCourceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + ftbCultivatePositionCourceLearningLambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List ftbCultivatePositionCourceLearnings = ftbCultivatePositionCourceLearningService.list(ftbCultivatePositionCourceLearningLambdaQueryWrapper); + if (CollectionUtils.isNotEmpty(ftbCultivateCourses) && CollectionUtils.isNotEmpty(ftbCultivatePositionCourceLearnings)) { + List existCourseIds = ftbCultivatePositionCourceLearnings.stream() + .map(FtbCultivatePositionCourceLearning::getCourceId) + .collect(Collectors.toList()); + ftbCultivateCourses.removeIf(ftbCultivateCourse -> existCourseIds.contains(ftbCultivateCourse.getId())); + } + // 校验是否有全局课程更新 + checkCourse(ftbCultivateCourses, userId); + page = ftbCultivateCourseMapper.generalCourseList(page, userId, ftbGlobalCurriculumAppWrapDTO); + page.getRecords().forEach(ftbGlobalCurriculumAppVO -> { + // 学习总人数 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(FtbCultivatePositionCourceLearning::getState, List.of(1, 2)); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, ftbGlobalCurriculumAppVO.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + ftbGlobalCurriculumAppVO.setLearnTotalNumber(ftbCultivatePositionCourceLearningService.getBaseMapper().selectCount(queryWrapper)); + // 课程学习设置 + ftbCultivateCourseSettingService.buildUpCourseLearningSettings(ftbGlobalCurriculumAppVO, ftbGlobalCurriculumAppVO.getCourseId()); + }); + ftbGlobalCurriculumAppWrapVO.setPageListVO(CultivatePage.coverPageList(page)); + Integer learnTotalNumber = ftbCultivateCourseMapper.learnTotalNumber(userId, ftbGlobalCurriculumAppWrapDTO.getCourseTypeId()); + ftbGlobalCurriculumAppWrapVO.setLearnTotalNumber(learnTotalNumber); + return ftbGlobalCurriculumAppWrapVO; + } + + @Override + public FtbCultivateCourseChapterDetailsVO courseDetails(String id) { + FtbCultivateCourseChapter currentChapter = baseMapper.selectById(id); + FtbCultivateCourseChapterDetailsVO chapterDetailsVO = FtbCultivateCourseChapterDetailsVO.convert(currentChapter); + // 开启随堂测试 + if (currentChapter.getWhetherTesting() == 0) { + LambdaQueryWrapper chapterTestLambdaQueryWrapper = Wrappers.lambdaQuery(); + chapterTestLambdaQueryWrapper.eq(FtbCultivateChapterTest::getCourseChapterId, id); + chapterTestLambdaQueryWrapper.eq(FtbCultivateChapterTest::getEnableMark, 0); + chapterTestLambdaQueryWrapper.orderByAsc(FtbCultivateChapterTest::getForcedStudy); + List ftbCultivateChapterTests = ftbCultivateChapterTestService.list(chapterTestLambdaQueryWrapper); + List cultivateChapterTestUpdate = new ArrayList<>(); + for (FtbCultivateChapterTest ftbCultivateChapterTest : ftbCultivateChapterTests) { + // 随堂测试 + FtbCultivateCourseChapterDetailsVO.FtbCultivateChapterTestUpdateDTO ftbCultivateChapterTestUpdateDTO = + FtbCultivateCourseChapterDetailsVO.convertFtbCultivateChapterTestUpdateDTO(ftbCultivateChapterTest); + // 随堂测试选项 + LambdaQueryWrapper chapterTestOptionsQuery = Wrappers.lambdaQuery(); + chapterTestOptionsQuery.eq(FtbCultivateChapterTestOption::getChapterTestId, ftbCultivateChapterTest.getId()); + chapterTestOptionsQuery.eq(FtbCultivateChapterTestOption::getEnableMark, 0); + chapterTestOptionsQuery.orderByAsc(FtbCultivateChapterTestOption::getSortCode); + List chapterTestOptions = ftbCultivateChapterTestOptionService.list(chapterTestOptionsQuery); + List chapterTestOptionUpdateDTOS = chapterTestOptions.stream() + .map(FtbCultivateCourseChapterDetailsVO::convertFtbCultivateChapterTestOptionUpdateDTO) + .collect(Collectors.toList()); + ftbCultivateChapterTestUpdateDTO.setChapterTestOptionUpdateDTOS(chapterTestOptionUpdateDTOS); + cultivateChapterTestUpdate.add(ftbCultivateChapterTestUpdateDTO); + } + chapterDetailsVO.setCultivateChapterTestUpdate(cultivateChapterTestUpdate); + } + return chapterDetailsVO; + } + + private void checkCourse(List ftbCultivateCourses, String userId) { + if (CollectionUtils.isNotEmpty(ftbCultivateCourses)) { + List ftbCultivatePositionCourceLearnings = new ArrayList<>(); + List ftbCultivatePositionCourceChapterLearningList = new ArrayList<>(); + ftbCultivateCourses.forEach(ftbCultivateCourse -> { + // 课程 + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = new FtbCultivatePositionCourceLearning(); + ftbCultivatePositionCourceLearning.setCourceId(ftbCultivateCourse.getId()); + ftbCultivatePositionCourceLearning.setLearnTime(0); + ftbCultivatePositionCourceLearning.setUserId(userId); + ftbCultivatePositionCourceLearning.setState(0); + ftbCultivatePositionCourceLearnings.add(ftbCultivatePositionCourceLearning); + // 章节 + LambdaQueryWrapper ftbCultivateCourseChapterLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(FtbCultivateCourseChapter::getCourseId, ftbCultivateCourse.getId()); + List ftbCultivateCourseChapters = ftbCultivateCourseChapterMapper.selectList(ftbCultivateCourseChapterLambdaQueryWrapper); + if (CollectionUtils.isNotEmpty(ftbCultivateCourseChapters)) { + ftbCultivateCourseChapters.forEach(ftbCultivateCourseChapter -> { + FtbCultivatePositionCourceChapterLearning ftbCultivatePositionCourceChapterLearning = new FtbCultivatePositionCourceChapterLearning(); + ftbCultivatePositionCourceChapterLearning.setChapterId(ftbCultivateCourseChapter.getId()); + ftbCultivatePositionCourceChapterLearning.setState(0); + ftbCultivatePositionCourceChapterLearning.setCourceId(ftbCultivateCourse.getId()); + ftbCultivatePositionCourceChapterLearning.setChapterTime(0); + ftbCultivatePositionCourceChapterLearning.setUserId(userId); + ftbCultivatePositionCourceChapterLearningList.add(ftbCultivatePositionCourceChapterLearning); + }); + } + }); + ftbCultivatePositionCourceLearningService.saveBatch(ftbCultivatePositionCourceLearnings); + ftbCultivatePositionCourceChapterLearningService.saveBatch(ftbCultivatePositionCourceChapterLearningList); + } + } + + private void doAddInClassTest(FtbCultivateCourseChapterDTO ftbCultivateCourseDTO, Integer format, String chapterId) { + // 视频格式和音频格式校验 + ftbCultivateCourseDTO.audioAndVideoVerification(format, ftbCultivateCourseDTO); + // 随堂测试,视频格式 + if (format == 1) { + // 启用随堂测试 + if (ftbCultivateCourseDTO.getWhetherTesting() == 0) { + List chapterTests = new ArrayList<>(); + List chapterOptionList = new ArrayList<>(); + FtbCultivateChapterTestDTO.onTheGoTestDataAssembly(chapterTests, chapterOptionList, chapterId, + ftbCultivateCourseDTO); + ftbCultivateChapterTestService.saveBatch(chapterTests); + ftbCultivateChapterTestOptionService.saveBatch(chapterOptionList); + } + } + } + + + @Override + public V2ChapterAppDetails courseDetailsV2(String id) { + FtbCultivateCourseChapter chapter = baseMapper.selectById(id); + + V2ChapterAppDetails vo = new V2ChapterAppDetails(); + vo.setId(chapter.getId()); + vo.setCourseId(chapter.getCourseId()); + vo.setName(chapter.getName()); + vo.setSortCode(chapter.getSortCode()); + vo.setAnnex(chapter.getAnnex()); + vo.setAnnexName(chapter.getAnnexName()); + vo.setDetailDesc(chapter.getDetailDesc()); + vo.setDuration(chapter.getDuration()); + vo.setForcedStudy(chapter.getForcedStudy()); + vo.setWhetherTesting(chapter.getWhetherTesting()); + vo.setChapterType(chapter.getChapterType()); + vo.setSource(chapter.getSource()); + vo.setParentId(chapter.getParentId()); + vo.setPath(chapter.getPath()); + vo.setVideoActionType(chapter.getVideoActionType()); + vo.setSize(chapter.getSize()); + vo.setLibraryId(chapter.getLibraryId()); + + // 开启随堂测试 + if (chapter.getWhetherTesting() == 0) { + LambdaQueryWrapper chapterTestLambdaQueryWrapper = Wrappers.lambdaQuery(); + chapterTestLambdaQueryWrapper.eq(FtbCultivateChapterTest::getCourseChapterId, id); + chapterTestLambdaQueryWrapper.eq(FtbCultivateChapterTest::getEnableMark, 0); + chapterTestLambdaQueryWrapper.orderByAsc(FtbCultivateChapterTest::getForcedStudy); + List ftbCultivateChapterTests = ftbCultivateChapterTestService.list(chapterTestLambdaQueryWrapper); + List testDtoList = new ArrayList<>(); + for (FtbCultivateChapterTest ftbCultivateChapterTest : ftbCultivateChapterTests) { + // 随堂测试 + V2CultivateChapterTestDto testDto = new V2CultivateChapterTestDto(); + testDto.setChapterTestId(ftbCultivateChapterTest.getId()); + testDto.setTopicName(ftbCultivateChapterTest.getTopicName()); + testDto.setAnswerAnalysis(ftbCultivateChapterTest.getAnswerAnalysis()); + testDto.setForcedStudy(ftbCultivateChapterTest.getForcedStudy()); + // 随堂测试选项 + LambdaQueryWrapper chapterTestOptionsQuery = Wrappers.lambdaQuery(); + chapterTestOptionsQuery.eq(FtbCultivateChapterTestOption::getChapterTestId, ftbCultivateChapterTest.getId()); + chapterTestOptionsQuery.eq(FtbCultivateChapterTestOption::getEnableMark, 0); + chapterTestOptionsQuery.orderByAsc(FtbCultivateChapterTestOption::getSortCode); + List chapterTestOptions = ftbCultivateChapterTestOptionService.list(chapterTestOptionsQuery); + if (CollUtil.isNotEmpty(chapterTestOptions)) { + List optionDtoList = new ArrayList<>(); + for (FtbCultivateChapterTestOption chapterTestOption : chapterTestOptions) { + V2CultivateChapterTestOptionDto optionDto = new V2CultivateChapterTestOptionDto(); + optionDto.setIsRightOption(chapterTestOption.getIsRightOption()); + optionDto.setContent(chapterTestOption.getContent()); + optionDto.setSortCode(chapterTestOption.getSortCode()); + optionDto.setChapterTestOptionId(chapterTestOption.getId()); + optionDtoList.add(optionDto); + } + testDto.setOptionDtoList(optionDtoList); + } + + testDtoList.add(testDto); + } + vo.setChapterTestDtoList(testDtoList); + + } + return vo; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCoursePackageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCoursePackageServiceImpl.java new file mode 100644 index 0000000..24db72e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCoursePackageServiceImpl.java @@ -0,0 +1,136 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateCoursePackageMapper; +import jnpf.cultivate.service.FtbCultivateCoursePackageService; +import jnpf.cultivate.service.FtbCultivatePackageCourseService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.enums.cultivate.course.CourseTypeEnum; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageAddDTO; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageQueryDTO; +import jnpf.model.cultivate.dto.coursepackage.FtbCultivateCoursePackageUpdateDTO; +import jnpf.model.cultivate.po.coursepackage.FtbCultivateCoursePackage; +import jnpf.model.cultivate.po.coursepackage.FtbCultivatePackageCourse; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackageDetailsVO; +import jnpf.model.cultivate.vo.coursepackage.FtbCultivateCoursePackagePageVO; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.util.SelfGrowthUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FtbCultivateCoursePackageServiceImpl extends ServiceImpl implements FtbCultivateCoursePackageService { + + @Resource + private FtbCultivatePackageCourseService ftbCultivatePackageCourseService; + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + @Transactional + public void add(FtbCultivateCoursePackageAddDTO ftbCultivateCoursePackageAddDTO) { + // 课程包名称是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCoursePackage::getEnabledMark, 0); + queryWrapper.eq(FtbCultivateCoursePackage::getName, ftbCultivateCoursePackageAddDTO.getName()); + if (this.count(queryWrapper) > 0) { + throw new RuntimeException("课程包名称重复"); + } + FtbCultivateCoursePackage ftbCultivateCoursePackage = ftbCultivateCoursePackageAddDTO.convertFtbCultivateCoursePackage(ftbCultivateCoursePackageAddDTO); + // 自定义课程包ID + ftbCultivateCoursePackage.setCoursePackageId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.COURSE_PACKAGE)); + getBaseMapper().insert(ftbCultivateCoursePackage); + // 关联课程包 + List ftbCultivatePackageCourses = ftbCultivateCoursePackageAddDTO.convertFtbCultivatePackageCourse(ftbCultivateCoursePackage.getId()); + ftbCultivatePackageCourseService.saveBatch(ftbCultivatePackageCourses); + } + + @Override + public Page pagingQuery(Page page, FtbCultivateCoursePackageQueryDTO ftbCultivateCoursePackageQueryDTO) { + Page ftbCultivateCoursePackagePageVOPage = getBaseMapper().pagingQuery(page, ftbCultivateCoursePackageQueryDTO); + List creatorUserIds = ftbCultivateCoursePackagePageVOPage.getRecords() + .stream() + .map(FtbCultivateCoursePackagePageVO::getCreateUserId) + .collect(Collectors.toList()); + List updateUserIds = ftbCultivateCoursePackagePageVOPage.getRecords() + .stream() + .map(FtbCultivateCoursePackagePageVO::getUpdateUserId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + creatorUserIds.addAll(updateUserIds); + Map userNameMaps = userApiV2Util.getUserNameAndCopyForUserIds(creatorUserIds); + ftbCultivateCoursePackagePageVOPage.getRecords().forEach(a -> { + UserEntity userEntity = userNameMaps.get(a.getCreateUserId()); + if(userEntity!=null) { + a.setCreateUserName(userEntity.getRealName()); + } + UserEntity userEntity1 = userNameMaps.get(a.getUpdateUserId()); + if(userEntity1!=null) { + a.setUpdateUserName(userEntity1.getRealName()); + } + }); + return ftbCultivateCoursePackagePageVOPage; + } + + @Override + @Transactional + public void updateCoursePackage(FtbCultivateCoursePackageUpdateDTO ftbCultivateCoursePackageUpdateDTO) { + // 课程包名称是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCoursePackage::getEnabledMark, 0); + queryWrapper.eq(FtbCultivateCoursePackage::getName, ftbCultivateCoursePackageUpdateDTO.getName()); + queryWrapper.ne(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCoursePackageUpdateDTO.getCoursePackageId()); + if (this.count(queryWrapper) > 0) { + throw new RuntimeException("课程包名称重复"); + } + LambdaUpdateWrapper updateWrapper = ftbCultivateCoursePackageUpdateDTO.convertUpdate(ftbCultivateCoursePackageUpdateDTO); + getBaseMapper().update(new FtbCultivateCoursePackage(), updateWrapper); + // 关联课程包 + LambdaQueryWrapper ftbCultivatePackageCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePackageCourseLambdaQueryWrapper.eq(FtbCultivatePackageCourse::getCoursePackageId, ftbCultivateCoursePackageUpdateDTO.getCoursePackageId()); + ftbCultivatePackageCourseService.remove(ftbCultivatePackageCourseLambdaQueryWrapper); + List ftbCultivatePackageCourses = ftbCultivateCoursePackageUpdateDTO + .convertFtbCultivatePackageCourse(ftbCultivateCoursePackageUpdateDTO.getCoursePackageId()); + ftbCultivatePackageCourseService.saveBatch(ftbCultivatePackageCourses); + } + + @Override + @Transactional + public void deleteCoursePackage(String id) { + // 忽略课程包关联的课程,由主表控制 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateCoursePackage::getId, id); + updateWrapper.set(FtbCultivateCoursePackage::getEnabledMark, 1); + this.update(new FtbCultivateCoursePackage(), updateWrapper); + } + + @Override + public List get(String id) { + return getBaseMapper().getPackageDetails(id); + } + + @Override + public List queryCoursePackageCourses(List coursePackageIds) { + List voList = getBaseMapper().queryCoursePackageCourses(coursePackageIds); + for (FtbCultivateCoursePackageDetailsVO vo : voList) { + String msg = CourseTypeEnum.getMsg(vo.getFormat()); + vo.setCourseType(msg); + } + return voList; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseServiceImpl.java new file mode 100644 index 0000000..a6153e9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseServiceImpl.java @@ -0,0 +1,474 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.event.impl.course.JnpfApplicationEventCourseService; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.course.*; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.CourseEventDTO; +import jnpf.model.cultivate.event.dto.course.PositionCourseEventDTO; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTest; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseSetting; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.resp.InnerExamDto; +import jnpf.model.cultivate.vo.course.web.*; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.context.SpringContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbCultivateCourseServiceImpl extends ServiceImpl implements FtbCultivateCourseService { + + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + @Resource + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + @Resource + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + @Resource + private FtbCultivateQuestionBankService ftbCultivateQuestionBankService; + @Resource + private FtbCultivateCourseSettingService ftbCultivateCourseSettingService; + @Resource + private FtbCultivateChapterTestMapper ftbCultivateChapterTestMapper; + @Resource + private FtbCultivatePositionCourseService ftbCultivatePositionCourseService; + + @Autowired + private FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + @Autowired + private JnpfApplicationEventCourseService jnpfApplicationEventCourseService; + @Autowired + @Lazy + private FtbCultivateCourseService ftbCultivateCourseService; + + + + + @Override + public String add(FtbCultivateCourseDTO ftbCultivateCourseDTO) { + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseService.realAdd(ftbCultivateCourseDTO); + // 岗位学习课程同步 + synchronousInitializationOfJobLearningCourses(ftbCultivateCourseDTO.getLabel(), ftbCultivateCourseDTO.getLearnJob(), + ftbCultivateCourseDTO.getCoursePositionSync(), ftbCultivateCourse.getId(), ftbCultivateCourse.getCourseUpdate()); + return ftbCultivateCourse.getId(); + } + + @Transactional + @Override + public FtbCultivateCourse realAdd(FtbCultivateCourseDTO ftbCultivateCourseDTO) { + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseDTO.convertFtbCultivateCourse(ftbCultivateCourseDTO); + // 校验课程名是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + queryWrapper.eq(FtbCultivateCourse::getName, ftbCultivateCourse.getName()); + if (baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("课程名不能重复"); + } + // 自定义课程 + ftbCultivateCourse.setCourseId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.COURSE)); + ftbCultivateCourse.setChapterNumber(0); + ftbCultivateCourse.setLastModifyTime(new Date()); + baseMapper.insert(ftbCultivateCourse); + FtbCultivateCourseSetting ftbCultivateCourseSetting = ftbCultivateCourseDTO.convertFtbCultivateCourseSetting(ftbCultivateCourseDTO); + ftbCultivateCourseSetting.setCourseId(ftbCultivateCourse.getId()); + ftbCultivateCourseSettingService.save(ftbCultivateCourseSetting); + return ftbCultivateCourse; + } + + @Override + @Transactional + public void upperLower(FtbCultivateShelvesDTO ftbCultivateShelvesDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateShelvesDTO.getId()); + updateWrapper.set(FtbCultivateCourse::getIsGroundIng, ftbCultivateShelvesDTO.getIsGroundIng()); + baseMapper.update(new FtbCultivateCourse(), updateWrapper); + baseMapper.selectById(ftbCultivateShelvesDTO.getId()); + } + + @Override + public FtbCultivateCourseNumberVO listCount() { + FtbCultivateCourseNumberVO ftbCultivateCourseNumberVO = new FtbCultivateCourseNumberVO(); + ftbCultivateCourseNumberVO.initData(); + //课程格式(1视频,2音频,3图文,4通用格式) + List media = baseMapper.getCountMediaList(); + if (CollUtil.isNotEmpty(media)) { + Map mediaMap = media.stream().collect(Collectors.toMap(FtbCommonKeyAndValDto::getK, FtbCommonKeyAndValDto::getV)); + Integer video = mediaMap.get(1); + if (video != null) { + ftbCultivateCourseNumberVO.setNumberOfVideoCourses(video); + } + + Integer audio = mediaMap.get(2); + if (audio != null) { + ftbCultivateCourseNumberVO.setNumberOfAudioCourses(audio); + } + + Integer graphic = mediaMap.get(3); + if (graphic != null) { + ftbCultivateCourseNumberVO.setNumberOfGraphicCourses(graphic); + } + + Integer numberOfCommonFormatCourses = mediaMap.get(4); + if (numberOfCommonFormatCourses != null) { + ftbCultivateCourseNumberVO.setNumberOfCommonFormatCourses(numberOfCommonFormatCourses); + } + } + //课程格式(1全局课程,2岗位学习课程) + List typeList = baseMapper.getCountLabelList(); + if (CollUtil.isNotEmpty(typeList)) { + Map typeMap = typeList.stream().collect(Collectors.toMap(FtbCommonKeyAndValDto::getK, FtbCommonKeyAndValDto::getV)); + + Integer global = typeMap.get(1); + if (global != null) { + ftbCultivateCourseNumberVO.setNumberOfGlobalCourses(global); + } + Integer onTheJobLearning = typeMap.get(2); + if (onTheJobLearning != null) { + ftbCultivateCourseNumberVO.setNumberOfOnTheJobLearningCourses(onTheJobLearning); + } + ftbCultivateCourseNumberVO.setTotalNumberOfCourses(ftbCultivateCourseNumberVO.getNumberOfGlobalCourses() + ftbCultivateCourseNumberVO.getNumberOfOnTheJobLearningCourses()); + } + + return ftbCultivateCourseNumberVO; + } + + + @Override + public FtbCultivateCourseDetailsVO courseDetails(String id) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + FtbCultivateCourse ftbCultivateCourse = baseMapper.selectOne(queryWrapper); + FtbCultivateCourseDetailsVO ftbCultivateCourseDetailsVO = FtbCultivateCourseDetailsVO.convertFtbCultivateCourseDetailsVO(ftbCultivateCourse); + // 课程学习设置 + ftbCultivateCourseSettingService.buildUpCourseLearningSettings(ftbCultivateCourseDetailsVO, id); + return ftbCultivateCourseDetailsVO; + } + + @Override + public Page pagingQuery(Page page, FtbCultivateCourseQueryDTO ftbCultivateCourseQueryDTO) { + Page ftbCultivateCoursePageVOPage = baseMapper.pagingQuery(page, ftbCultivateCourseQueryDTO); + if (CollUtil.isNotEmpty(ftbCultivateCoursePageVOPage.getRecords())) { + if (ftbCultivateCourseQueryDTO.getIsSelectDuration() != null && ftbCultivateCourseQueryDTO.getIsSelectDuration() == 1) { + fillDuration(ftbCultivateCoursePageVOPage.getRecords()); + } + } + return ftbCultivateCoursePageVOPage; + } + + /** + * 填充课程的时长 + * + * @param records + */ + private void fillDuration(List records) { + records.forEach(record -> { + Long duration = ftbCultivateCourseChapterMapper.querySumDuration(record.getId()); + if (duration == null) { + duration = 0L; + } + record.setSumDuration(duration); + }); + } + + @Override + @Transactional + public void delete(String id) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbCultivateCourse::getEnableMark, 1); + baseMapper.update(new FtbCultivateCourse(), updateWrapper); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionCourseExam::getCourseId, id); + // 课程关联考试 + List ftbCultivatePositionCourseExams = ftbCultivatePositionCourseExamMapper.selectList(queryWrapper); + ftbCultivatePositionCourseExams.forEach(ftbCultivatePositionCourseExam -> { + InnerExamDto innerExamDto = new InnerExamDto(); + innerExamDto.setExamId(ftbCultivatePositionCourseExam.getExamId()); + innerExamDto.setRelationRankId(ftbCultivatePositionCourseExam.getPostRankId()); + innerExamDto.setExamSource(1); + innerExamDto.setRelationId(ftbCultivatePositionCourseExam.getId()); + ftbCultivateExamUserService.cancelPositionExam(innerExamDto); + }); + // 删除题库 + ftbCultivateQuestionBankService.deleteQuestionBankRelationCourse(List.of(id)); + // 课程学习设置 + LambdaUpdateWrapper courseSettingLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + courseSettingLambdaUpdateWrapper.eq(FtbCultivateCourseSetting::getCourseId, id); + courseSettingLambdaUpdateWrapper.set(FtbCultivateCourseSetting::getEnableMark, 1); + ftbCultivateCourseSettingService.update(new FtbCultivateCourseSetting(), courseSettingLambdaUpdateWrapper); + // 随堂测试 + LambdaUpdateWrapper chapterTestLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + chapterTestLambdaUpdateWrapper.eq(FtbCultivateChapterTest::getCourseId, id); + chapterTestLambdaUpdateWrapper.set(FtbCultivateChapterTest::getEnableMark, 1); + ftbCultivateChapterTestMapper.update(new FtbCultivateChapterTest(), chapterTestLambdaUpdateWrapper); + } + + @Override + public void updateCourse(FtbCultivateCourseUpdateDTO ftbCultivateCourseUpdateDTO) { + ftbCultivateCourseService.realUpdateCourse(ftbCultivateCourseUpdateDTO); + FtbCultivateCourse ftbCultivateCourse = baseMapper.selectById(ftbCultivateCourseUpdateDTO.getId()); + // 岗位学习课程同步 + synchronousInitializationOfJobLearningCourses(ftbCultivateCourse.getLabel(), ftbCultivateCourse.getLearnJob(), + ftbCultivateCourse.getCoursePositionSync(), ftbCultivateCourse.getId(), ftbCultivateCourse.getCourseUpdate()); + } + + + @Override + @Transactional + public void realUpdateCourse(FtbCultivateCourseUpdateDTO ftbCultivateCourseUpdateDTO) { + // 校验课程名是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + queryWrapper.eq(FtbCultivateCourse::getName, ftbCultivateCourseUpdateDTO.getName()); + queryWrapper.ne(FtbCultivateCourse::getId, ftbCultivateCourseUpdateDTO.getId()); + if (baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("该课程名称已经存在,修改失败!"); + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseUpdateDTO.getId()); + updateWrapper.set(FtbCultivateCourse::getName, ftbCultivateCourseUpdateDTO.getName()); + updateWrapper.set(FtbCultivateCourse::getFormat, ftbCultivateCourseUpdateDTO.getFormat()); + if (StringUtils.isNotEmpty(ftbCultivateCourseUpdateDTO.getSubtitle())) { + updateWrapper.set(FtbCultivateCourse::getSubtitle, ftbCultivateCourseUpdateDTO.getSubtitle()); + } else { + updateWrapper.set(FtbCultivateCourse::getSubtitle, ""); + } + updateWrapper.set(FtbCultivateCourse::getHighLights, ftbCultivateCourseUpdateDTO.getHighLights()); + updateWrapper.set(FtbCultivateCourse::getTypeId, ftbCultivateCourseUpdateDTO.getTypeId()); + updateWrapper.set(FtbCultivateCourse::getLabel, ftbCultivateCourseUpdateDTO.getLabel()); + updateWrapper.set(FtbCultivateCourse::getLearnJob, ftbCultivateCourseUpdateDTO.getLearnJob()); + updateWrapper.set(FtbCultivateCourse::getCourseUpdate, ftbCultivateCourseUpdateDTO.getCourseUpdate()); + updateWrapper.set(FtbCultivateCourse::getCoursePositionSync, ftbCultivateCourseUpdateDTO.getCoursePositionSync()); + baseMapper.update(new FtbCultivateCourse(), updateWrapper); + // 课程学习设置 + FtbCultivateCourseSettingMapper mapper = (FtbCultivateCourseSettingMapper) ftbCultivateCourseSettingService.getBaseMapper(); + if (mapper.isThereACourseLearningSetting(ftbCultivateCourseUpdateDTO.getId()) > 0) { + LambdaUpdateWrapper courseSettingLambdaUpdateWrapper = ftbCultivateCourseUpdateDTO.updateFtbCultivateCourseSetting(ftbCultivateCourseUpdateDTO); + courseSettingLambdaUpdateWrapper.eq(FtbCultivateCourseSetting::getCourseId, ftbCultivateCourseUpdateDTO.getId()); + ftbCultivateCourseSettingService.update(new FtbCultivateCourseSetting(), courseSettingLambdaUpdateWrapper); + } else { + FtbCultivateCourseSetting ftbCultivateCourseSetting = ftbCultivateCourseUpdateDTO.convertFtbCultivateCourseSetting(ftbCultivateCourseUpdateDTO); + ftbCultivateCourseSetting.setCourseId(ftbCultivateCourseUpdateDTO.getId()); + ftbCultivateCourseSettingService.save(ftbCultivateCourseSetting); + } + + } + + @Override + public Page deleteJobLearningList(Page page, String courseId) { + Page ftbCourseDeleteJobLearnVOPage = baseMapper.listJobLearningByCourseId(page, courseId); + ftbCourseDeleteJobLearnVOPage.getRecords().forEach(ftbCourseDeleteJobLearnVO -> { + // 当前学习人数 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId); + ftbCourseDeleteJobLearnVO.setCurrentNumberOfStudents(ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper)); + // 岗位名称 + PositionVO positionGradesInfoBoundVOS = userApiV2Util.infoPosition(ftbCourseDeleteJobLearnVO.getPositionId(), null); + if(positionGradesInfoBoundVOS!=null) { + ftbCourseDeleteJobLearnVO.setPositionTitle(positionGradesInfoBoundVOS.getFullName()); + } + }); + return ftbCourseDeleteJobLearnVOPage; + } + + @Override + public Page deleteRelatedQuestionBankList(Page page, String courseId) { + Page ftbCourseDeleteQuestionBankVOPage = baseMapper.listRelatedQuestionBankList(page, courseId); + ftbCourseDeleteQuestionBankVOPage.getRecords().forEach(ftbCourseDeleteJobLearnVO -> { + // 涵盖题库题目的试卷数 + ftbCourseDeleteJobLearnVO.setNumberOfTestPapers( + Long.valueOf(baseMapper.getCountTestPaperByQuestionBankId(ftbCourseDeleteJobLearnVO.getId()))); + }); + return ftbCourseDeleteQuestionBankVOPage; + } + + @Override + public List promotionPathwayCourses(String userId) { + return baseMapper.promotionPathwayCourses(userId); + } + + @Override + public ActionResult deleteCheck(String id) { + // 校验岗位学习 + if (baseMapper.getCountLearnByCourseId(id) > 0) { + return ActionResult.success(new FtbCourseDeleteVO(true, false)); + } + // 校验关联题库 + if (baseMapper.getCountQuestionBankByCourseId(id) > 0) { + return ActionResult.success(new FtbCourseDeleteVO(false, true)); + } + return ActionResult.success(new FtbCourseDeleteVO(false, false)); + } + + @Override + public void learnMapRelearn(String userId, String positionId) { + // 该岗位是否绑定课程 + List couseIds = ftbCultivatePositionCourseMapper.learnMapRelearn(positionId); + if (CollectionUtils.isNotEmpty(couseIds)) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(CourseEventDTO.builder() + .courseIds(couseIds).userIds(List.of(userId)).whetherToChange(false).build())); + } + } + + @Override + public List queryJobBindingCourses(List postIds) { + List queryJobBindingCourses = getBaseMapper().queryJobBindingCourses(postIds); + List existPostIds = queryJobBindingCourses.stream() + .map(FtbCultivateCourseDTO.PositionLearnCourseInternal::getPostId) + .collect(Collectors.toList()); + List noPositionExists = postIds.stream() + .filter(a -> !existPostIds.contains(a)) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(noPositionExists)) { + return new ArrayList<>(); + } + List positionNames = userApiV2Util.listPositionDetailInfoByIds(noPositionExists, null); + + Map positionNameMaps = positionNames.stream() + .collect(Collectors.toMap(PositionVO::getId, PositionVO::getFullName)); + return noPositionExists.stream().map(a -> { + FtbCourseDeleteJobLearnVO.FtbCultivateJobBindingCoursesVO ftbCultivateJobBindingCoursesVO = new FtbCourseDeleteJobLearnVO.FtbCultivateJobBindingCoursesVO(); + ftbCultivateJobBindingCoursesVO.setPositionId(a); + ftbCultivateJobBindingCoursesVO.setPositionTitle(positionNameMaps.get(a)); + return ftbCultivateJobBindingCoursesVO; + }).collect(Collectors.toList()); + } + + + /** + * 岗位学习课程同步 + * + * @param label 课程标签(1全局课程,2岗位学习课程) + * @param learnJob 学习岗位 + * @param coursePositionSync 同步添加至对应岗位学习课程列表0否1是 + * @param courseId 课程主键ID + * @param courseId 课程内容更新需要重新学习(1是,2否) + */ + private void synchronousInitializationOfJobLearningCourses(Integer label, String learnJob, Integer coursePositionSync, String courseId, Integer courseUpdate) { + if (label != 2) { + if (courseUpdate == 1) { + // 课程进度,参与培训所有人员需要学习的课程,线下培训课程需要和岗位学习课程同步 + jnpfApplicationEventCourseService.handlerCourseEvent( + CourseEventDTO.builder() + .courseIds(List.of(courseId)) + .userIds(new ArrayList<>()) + .whetherToChange(true) + .build()); + + } + return; + } + + List postIds = new ArrayList<>(); + List queryJobBindingCourses = new ArrayList<>(); + if (StringUtils.isNotEmpty(learnJob)) { + postIds = List.of(learnJob.split(StringPool.COMMA)); + queryJobBindingCourses = getBaseMapper().queryJobBindingCourses(postIds); + } + if (CollUtil.isEmpty(queryJobBindingCourses)) { + return; + } + List pendingPositions = queryJobBindingCourses.stream() + .map(FtbCultivateCourseDTO.PositionLearnCourseInternal::getPostId) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(pendingPositions)) { + return; + } + + //已经存在的岗位课程关联数据 + QueryWrapper existPositionCourseQuery = new QueryWrapper<>(); + existPositionCourseQuery.lambda() + .eq(FtbCultivatePositionCourse::getEnabledMark, 0) + .eq(FtbCultivatePositionCourse::getCourseId, courseId); + List existPositionCourseList = ftbCultivatePositionCourseMapper.selectList(existPositionCourseQuery); + + + List existPostionCourseIdList = new ArrayList<>(); + for (FtbCultivatePositionCourse ftbCultivatePositionCourse : existPositionCourseList) { + existPostionCourseIdList.add(ftbCultivatePositionCourse.getPostRankId() + "-" + ftbCultivatePositionCourse.getPostLearnId()); + } + // 已存在的岗位学习课程 + if (coursePositionSync == 1) { + // 岗位学习课程 + List ftbCultivatePositionCourses = queryJobBindingCourses.stream().map(a -> { + FtbCultivatePositionCourse ftbCultivatePositionCourse = new FtbCultivatePositionCourse(); + ftbCultivatePositionCourse.setCourseId(courseId); + ftbCultivatePositionCourse.setPostRankId(a.getPostId()); + ftbCultivatePositionCourse.setPostLearnId(a.getPostLearnId()); + return ftbCultivatePositionCourse; + }).collect(Collectors.toList()); + + //过滤掉已存在的岗位学习关联数据 + ftbCultivatePositionCourses = ftbCultivatePositionCourses.stream().filter(postionCourse -> { + String idCode = postionCourse.getPostRankId() + "-" + postionCourse.getPostLearnId(); + return !existPostionCourseIdList.contains(idCode); + }).collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(ftbCultivatePositionCourses)) { + ftbCultivatePositionCourseService.saveBatch(ftbCultivatePositionCourses); + } + Boolean whetherToChange = false; + if (courseUpdate == 1) { + whetherToChange = true; + } + // 岗位课程事件 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + PositionCourseEventDTO.builder() + .courseIds(List.of(courseId)) + .postIds(pendingPositions) + .eventType(PositionCourseEventDTO.EventType.POST) + .whetherToChange(whetherToChange) + .build())); + + } else { + if (courseUpdate == 1) { + // 课程进度,参与培训所有人员需要学习的课程,线下培训课程需要和岗位学习课程同步 + jnpfApplicationEventCourseService.handlerCourseEvent( + CourseEventDTO.builder() + .courseIds(List.of(courseId)) + .userIds(new ArrayList<>()) + .whetherToChange(true) + .build()); + + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseSettingServiceImpl.java new file mode 100644 index 0000000..14ef729 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseSettingServiceImpl.java @@ -0,0 +1,68 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateCourseSettingMapper; +import jnpf.cultivate.service.FtbCultivateChapterTestResultService; +import jnpf.cultivate.service.FtbCultivateCourseSettingService; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseSettingDTO; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import jnpf.model.cultivate.po.course.FtbCultivateCourseSetting; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestResultVO; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +@Service +public class FtbCultivateCourseSettingServiceImpl extends ServiceImpl implements FtbCultivateCourseSettingService { + + @Resource + private FtbCultivateChapterTestResultService fileResultService; + + @Override + public void buildUpCourseLearningSettings(FtbCultivateCourseSettingDTO ftbCultivateCourseSettingDTO, String courseId) { + LambdaQueryWrapper courseSettingLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseSettingLambdaQueryWrapper.eq(FtbCultivateCourseSetting::getEnableMark, 0); + courseSettingLambdaQueryWrapper.eq(FtbCultivateCourseSetting::getCourseId, courseId); + courseSettingLambdaQueryWrapper.last("limit 1"); + FtbCultivateCourseSetting ftbCultivateCourseSetting = getBaseMapper().selectOne(courseSettingLambdaQueryWrapper); + FtbCultivateCourseSettingDTO.convertFtbCultivateCourseSetting(ftbCultivateCourseSettingDTO, ftbCultivateCourseSetting); + } + + @Override + public void onSiteTestResultStorage(List ftbChapterTestDTOs, String courseId, String chapterId) { + if (CollUtil.isNotEmpty(ftbChapterTestDTOs)) { + FtbCultivateChapterTestResult fileResult = new FtbCultivateChapterTestResult(); + fileResult.setCourseId(courseId); + fileResult.setCourseChapterId(chapterId); + fileResult.setUserId(UserProvider.getLoginUserId()); + fileResult.setChapterTestOptions(JSON.toJSONString(ftbChapterTestDTOs)); + fileResultService.save(fileResult); + } + } + @Override + public void onSiteTestResultStorageV2(List ftbChapterTestDTOs, String courseId, String chapterId, String userId) { + if (CollUtil.isNotEmpty(ftbChapterTestDTOs)) { + + LambdaQueryWrapper testWrapper = Wrappers.lambdaQuery(); + testWrapper.eq(FtbCultivateChapterTestResult::getEnableMark, 0); + testWrapper.eq(FtbCultivateChapterTestResult::getCourseId, courseId); + testWrapper.eq(FtbCultivateChapterTestResult::getCourseChapterId, chapterId); + testWrapper.eq(FtbCultivateChapterTestResult::getUserId, userId); + fileResultService.remove(testWrapper); + + FtbCultivateChapterTestResult fileResult = new FtbCultivateChapterTestResult(); + fileResult.setCourseId(courseId); + fileResult.setCourseChapterId(chapterId); + fileResult.setUserId(userId); + fileResult.setChapterTestOptions(JSON.toJSONString(ftbChapterTestDTOs)); + fileResultService.save(fileResult); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseStatisticesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseStatisticesServiceImpl.java new file mode 100644 index 0000000..54f02ab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseStatisticesServiceImpl.java @@ -0,0 +1,339 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.FtbCultivateCourseStatisticesMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionStatisticesMapper; +import jnpf.cultivate.service.FtbCultivateCourseStatisticesService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseOrgStatisticsDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCoursePersonStatisticesDTO; +import jnpf.model.cultivate.v2.course.web.vo.V2InnerCultivateChapterStatisticsDto; +import jnpf.model.cultivate.v2.course.web.vo.V2InnerCultivateCourseOrgStatisticsDto; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseOrgStatisticesVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCoursePersonStatisticesVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbCultivateCourseStatisticesServiceImpl implements FtbCultivateCourseStatisticesService { + + private static final List EMPTY_LIST = List.of("-1"); + + @Resource + private FtbCultivateCourseStatisticesMapper ftbCultivateCourseStatisticesMapper; + + @Resource + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Resource + private FtbCultivatePositionStatisticesMapper ftbCultivatePositionStatisticesMapper; + + + @Autowired + private UserApiV2Util userApiV2Util; + + + @Autowired + CultivatePerUtils cultivatePerUtils; + + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbCultivateCourseOrgStatisticsDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO organizationListStatistics(FtbCultivateCourseOrgStatisticsDTO statisticDTO) { + Page page = statisticDTO.coverCultivatePage(); + List records = new ArrayList<>(); + // 多选组织情况 根据每一个组织维度统计 + List orgIds; + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return returnEmptyList(statisticDTO); + } + // 勾选 + + if ("1".equals(statisticDTO.getSelectLogo()) && CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List allOrg = new ArrayList<>(statisticDTO.getOrgId()); + List allOrganize = userApiV2Util.queryChildOrgForOrgIdsReturnOrgIds(statisticDTO.getOrgId(), true, null); + if (CollUtil.isNotEmpty(allOrganize)) { + allOrg.addAll(allOrganize); + } + List intersection = UserApiV2Util.getIntersection(allOrg, powerOrgList); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + } else if (CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List intersection = UserApiV2Util.getIntersection(statisticDTO.getOrgId(), powerOrgList); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + } else { + orgIds = powerOrgList; + } + // 手动输入 + String dtoOrgIds = statisticDTO.getOrgIds(); + if (StringUtils.isNotEmpty(dtoOrgIds)) { + List organizationalIdCollection = cultivatePerUtils.convertOrganizationalIdCollection(dtoOrgIds); + if (CollUtil.isEmpty(organizationalIdCollection)) { + return returnEmptyList(statisticDTO); + } + List intersection = UserApiV2Util.getIntersection(orgIds, organizationalIdCollection); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + } + + //优化批量 + List allUserCourseStudyList = ftbCultivateCourseStatisticesMapper.organizationListStatisticsV2(statisticDTO); + if (CollUtil.isEmpty(allUserCourseStudyList)) { + return returnEmptyList(statisticDTO); + } + orgIds = UserApiV2Util.uniqueStringList(orgIds); + Map orgMap = userApiV2Util.organizesByOrganizeIdsReturenMap(orgIds, null); + Map> userListForOrgIdsReturnMap = userApiV2Util.getUserListForOrgIdsReturnMap(orgIds, null); + + Map> userIdCourseStudyMap = allUserCourseStudyList.stream().collect(Collectors.groupingBy(V2InnerCultivateCourseOrgStatisticsDto::getUserId)); + for (String orgId : orgIds) { + List listFeign = userListForOrgIdsReturnMap.get(orgId);// userApiV2Util.getUserListForOrgIds(List.of(orgId), null); + if (CollUtil.isNotEmpty(listFeign)) { + List userIds = listFeign.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + List statisticsDtoList = getUserStudyCourseList(userIds, userIdCourseStudyMap); + if (CollUtil.isEmpty(statisticsDtoList)) { + continue; + } +// statisticsDtoList按照课程id分组 + Map> statisticsDtoListMap = statisticsDtoList.stream().collect(Collectors.groupingBy(V2InnerCultivateCourseOrgStatisticsDto::getCourseId)); + List userCouseList = new ArrayList<>(); + for (Map.Entry> entry : statisticsDtoListMap.entrySet()) { + List dtoList = entry.getValue(); // 获取对应的统计列表 + FtbCultivateCourseOrgStatisticesVO item = new FtbCultivateCourseOrgStatisticesVO(); + item.setOrgId(orgId); + item.setOrganizationName(orgMap.get(orgId).getName()); + item.setSysOrganizationID(orgMap.get(orgId).getEnCode()); + item.setCourseTitle(dtoList.get(0).getCourseName()); + item.setCourseId(entry.getKey()); + item.setCourseType(dtoList.get(0).getCourseTypeName()); + item.setCourseCreationTime(dtoList.get(0).getCourseCreationTime()); + item.setNumberInLearning(dtoList.size()); + item.setCourseType(dtoList.get(0).getCourseTypeName()); + + Integer studyTimeTotal = 0; + Integer completeNum = 0; + for (V2InnerCultivateCourseOrgStatisticsDto dto : dtoList) { + studyTimeTotal += dto.getLearnTime(); + if (dto.getState().equals(1)) { + completeNum++; + } + } + item.setNumberHaveCourse(completeNum); + item.setTotalDurationOfStudy(new BigDecimal(studyTimeTotal).divide(new BigDecimal(3600), 2, RoundingMode.HALF_UP)); + item.setAverageLearningPerson(new BigDecimal(studyTimeTotal).divide(new BigDecimal(item.getNumberInLearning()), 2, RoundingMode.HALF_UP).divide(new BigDecimal(3600), 2, RoundingMode.HALF_UP)); + userCouseList.add(item); + } + records.addAll(userCouseList); + } + } + return CultivatePage.paginate(records, page); + } + + private List getUserStudyCourseList(List userIds, Map> userIdCourseStudyMap) { + List ret = new ArrayList<>(); + for (String userId : userIds) { + List statisticsDtoList = userIdCourseStudyMap.get(userId); + if(CollUtil.isNotEmpty(statisticsDtoList)){ + ret.addAll(statisticsDtoList); + } + } + return ret; + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbCultivateCoursePersonStatisticesDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + /** + * 组织列表统计方法 + * 对给定的统计参数进行组织列表的相关统计分析,并返回统计结果列表 + * + * @param pr 统计参数,包含了需要进行统计的组织相关的信息 + * @return 返回一个包含组织统计结果的列表 + */ + @Override + public PageListVO personListStatistics(FtbCultivateCoursePersonStatisticesDTO pr) { + String orgId = pr.getOrgId(); + Page page = pr.coverCultivatePage(); + List queryPeoples = new ArrayList<>(); + + + if (StringUtils.isNotEmpty(orgId)) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(List.of(orgId), null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return returnEmptyList(pr); + } + List userIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(pr.getSelectPeoples())) { + List intersection = UserApiV2Util.getIntersection(userIds, pr.getSelectPeoples()); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(pr); + } + queryPeoples = intersection; + } else { + queryPeoples = userIds; + } + } else { + if (CollUtil.isNotEmpty(pr.getSelectPeoples())) { + queryPeoples = pr.getSelectPeoples(); + } + } +// List selectPeoples = new ArrayList<>(); +// // 关键词搜索 +// if (StringUtils.isNotEmpty(pr.getKeyWords())) { +// boolean keywordSearch = false; +// // 员工姓名和员工ID +// List> userIds = ftbCultivatePositionStatisticesMapper.fuzzySearchForRosterKeywords(pr.getKeyWords()); +// if (CollUtil.isNotEmpty(userIds)) { +// selectPeoples.addAll(userIds.stream().map(a -> a.get("userId")).collect(Collectors.toList())); +// keywordSearch = true; +// } +// // 组织ID +// List organizationalIdCollection = cultivatePerUtils.convertOrganizationalIdCollection(pr.getKeyWords()); +// if (CollUtil.isNotEmpty(organizationalIdCollection)) { +// List tempList = userApiV2Util.getUserListForOrgIds(organizationalIdCollection, null); +// if (CollUtil.isNotEmpty(tempList)) { +// List userIdSe = tempList.stream().map(UserPageListVO::getId).collect(Collectors.toList()); +// selectPeoples.addAll(userIdSe); +// keywordSearch = true; +// } +// } +// // 岗位名称 +// List userByPositionName = userApiV2Util.getUserListForPositions(List.of(pr.getKeyWords()), null); +// if (CollUtil.isNotEmpty(userByPositionName)) { +// List datas = userByPositionName.stream().map(UserBoundVO::getId).collect(Collectors.toList()); +// selectPeoples.addAll(datas); +// keywordSearch = true; +// } +// if (!keywordSearch) { +// return returnEmptyList(pr); +// } +// } + if (CollUtil.isNotEmpty(queryPeoples)) { + pr.setSelectPeoples(queryPeoples); + } + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(pr.getSelectPeoples())) { + List intersection = UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), pr.getSelectPeoples()); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(pr); + } + pr.setSelectPeoples(intersection); + } else { + pr.setSelectPeoples(innerPowerUserVO.getUserIds()); + } + } else if (innerPowerUserVO.getCode() == 2) { + return returnEmptyList(pr); + } + + page = ftbCultivateCourseStatisticesMapper.personListStatistics(page, pr); + if (CollectionUtils.isNotEmpty(page.getRecords())) { + List courseIds = page.getRecords().stream().map(FtbCultivateCoursePersonStatisticesVO::getCourseId).distinct().collect(Collectors.toList()); + List courseInfo = ftbCultivateCourseStatisticesMapper.courseInfo(courseIds); + Map courseMap = courseInfo.stream().collect(Collectors.toMap(FtbCultivateCoursePersonStatisticesVO::getCourseId, item -> item)); + // 用户id + List userIds = page.getRecords().stream().map(FtbCultivateCoursePersonStatisticesVO::getUserId).collect(Collectors.toList()); + Map userMap = userApiV2Util.getUserPrimaryBoundBatchReturnMap(userIds, null); + List allUserCourseChapterStudyList = ftbCultivateCourseStatisticesMapper.personListStatisticsV2(userIds, List.of(1)); + Map> userIdCourseChapterStudyMap = new HashMap<>(); + if (CollUtil.isNotEmpty(allUserCourseChapterStudyList)) { + userIdCourseChapterStudyMap = allUserCourseChapterStudyList.stream() + .collect(Collectors.groupingBy( + item -> item.getUserId() + "_" + item.getCourseId() + )); + } + for (FtbCultivateCoursePersonStatisticesVO item : page.getRecords()) { + FtbCultivateCoursePersonStatisticesVO ftbCultivateCoursePersonStatisticesVO = courseMap.get(item.getCourseId()); + item.setCourseTitle(ftbCultivateCoursePersonStatisticesVO.getCourseTitle()); + item.setCourseType(ftbCultivateCoursePersonStatisticesVO.getCourseType()); + item.setCourseCreationTime(ftbCultivateCoursePersonStatisticesVO.getCourseCreationTime()); + item.setCourseFormat(ftbCultivateCoursePersonStatisticesVO.getCourseFormat()); + // 所属组织_岗位_职等 + UserBoundVO userBoundVO = userMap.get(item.getUserId()); + if (userBoundVO != null) { + item.setEmployeeSName(userBoundVO.getUserName()); + item.setEmployeeId(userBoundVO.getSystemWorkerId()); + String tempName = userBoundVO.getOrganizeName(); + if (StringUtils.isNotEmpty(userBoundVO.getPositionName())) { + tempName = tempName + "_" + userBoundVO.getPositionName(); + } + if (StringUtils.isNotEmpty(userBoundVO.getGradeName())) { + tempName = tempName + "_" + userBoundVO.getGradeName(); + } + item.setAffiliatedOrgPosLevel(tempName); + } + + // 参与学习时长(h) + item.setParticipationTimeInStudy(CultivatePerUtils.computeDivision(item.getParticipationTimeInStudy())); + // 学习进度 已学习章节数/总章节数 + List v2InnerCultivateChapterStatisticsDtos = userIdCourseChapterStudyMap.get(item.getUserId() + "_" + item.getCourseId()); + Integer numberOfStudyChapters = 0; + if (CollUtil.isNotEmpty(v2InnerCultivateChapterStatisticsDtos)) { + numberOfStudyChapters = v2InnerCultivateChapterStatisticsDtos.size(); + } + Integer totalOfStudyChapters = ftbCultivateCoursePersonStatisticesVO.getChapterNumber(); + item.setLearningProgress(CultivatePerUtils.computeDivision(numberOfStudyChapters, totalOfStudyChapters)); + } + } + return CultivatePage.coverPageList(page); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTriggerLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTriggerLogServiceImpl.java new file mode 100644 index 0000000..8e201d7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTriggerLogServiceImpl.java @@ -0,0 +1,15 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateCourseTriggerLogMapper; +import jnpf.cultivate.service.FtbCultivateCourseTriggerLogService; +import jnpf.model.cultivate.po.course.FtbCultivateCourseTriggerLog; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class FtbCultivateCourseTriggerLogServiceImpl extends ServiceImpl implements FtbCultivateCourseTriggerLogService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTypeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTypeServiceImpl.java new file mode 100644 index 0000000..35a0073 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateCourseTypeServiceImpl.java @@ -0,0 +1,82 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseTypeMapper; +import jnpf.cultivate.service.FtbCultivateCourseTypeService; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseTypeDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseTypeUpdateDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseType; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseTypeVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; + +@Service +public class FtbCultivateCourseTypeServiceImpl extends ServiceImpl implements FtbCultivateCourseTypeService { + @Resource + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Override + @Transactional + public void add(FtbCultivateCourseTypeDTO ftbCultivateCourseDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourseType::getEnableMark, 0); + queryWrapper.eq(FtbCultivateCourseType::getName, ftbCultivateCourseDTO.getName()); + if (this.baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("系统已存在该课程类型名称,请勿重复添加"); + } + FtbCultivateCourseType ftbCultivateCourseType = FtbCultivateCourseTypeDTO + .convertFtbCultivateCourseType(ftbCultivateCourseDTO); + baseMapper.insert(ftbCultivateCourseType); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ActionResult delete(String id) { + // 校验课程 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourse::getTypeId, id); + queryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + queryWrapper.last("limit 1"); + if (ftbCultivateCourseMapper.selectCount(queryWrapper) > 0) { + return ActionResult.success(false); + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbCultivateCourseType::getEnableMark, 1); + baseMapper.update(null, updateWrapper); + return ActionResult.success(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateInfo(FtbCultivateCourseTypeUpdateDTO ftbCultivateCourseTypeDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseTypeDTO.getCourseTypeId()); + updateWrapper.set(FtbCultivateCourseType::getName, ftbCultivateCourseTypeDTO.getName()); + // 新建对象,防止MybatisPlusMetaObjectHandler.updateFill方法失效 + baseMapper.update(new FtbCultivateCourseType(), updateWrapper); + } + + @Override + public Page pageList(Page page) { + return baseMapper.pageList(page); + } + + @Override + public List getAll() { + return baseMapper.getAll(); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamHistoryPaperServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamHistoryPaperServiceImpl.java new file mode 100644 index 0000000..acd0a8b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamHistoryPaperServiceImpl.java @@ -0,0 +1,114 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateExamHistoryPaperMapper; +import jnpf.cultivate.service.FtbCultivateExamHistoryPaperService; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamHistoryPaper; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class FtbCultivateExamHistoryPaperServiceImpl extends ServiceImpl implements FtbCultivateExamHistoryPaperService { + + /** + * 根据关键字搜索考试名称 + * @param keyword 考试名称 + * @return + */ + @Override + public List searchFrzzExamName(String keyword) { + List examIds = new ArrayList<>(); + if (StringUtils.isEmpty(keyword)) { + return examIds; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamHistoryPaper::getId, FtbCultivateExamHistoryPaper::getExamId, FtbCultivateExamHistoryPaper::getPrimaryExamId) + .like(FtbCultivateExamHistoryPaper::getExamName, keyword); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(list)) { + for (FtbCultivateExamHistoryPaper ftbCultivateExam : list) { + examIds.add(ftbCultivateExam.getPrimaryExamId()); + } + } + return examIds; + } + + /** + * 根据考试ID批量查询历史考试试卷信息 + * @param examIds 考试ID集合 + * @return + */ + @Override + public Map queryHistoryExamListByIds(List examIds) { + Map ret = new HashMap<>(); + if (CollectionUtil.isEmpty(examIds)) { + return ret; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbCultivateExamHistoryPaper::getPrimaryExamId, examIds); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(list)) { + for (FtbCultivateExamHistoryPaper entity : list) { + ret.put(entity.getPrimaryExamId() + entity.getVersionBatch(), entity); + } + } + return ret; + } + + /** + * 根据考试id和批次查询历史试卷 + * @param id 考试ID + * @param versionBatch 批次 + * @return + */ + @Override + public FtbCultivateExamHistoryPaper queryByEamIdAndVersionBatch(String id, String versionBatch) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamHistoryPaper::getVersionBatch, versionBatch) + .eq(FtbCultivateExamHistoryPaper::getPrimaryExamId, id); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(list)) { + return list.get(0); + } + return null; + } + + /** + * 记录考试的历史试卷 + * @param oldExam + */ + @Override + public void recordHistory(FtbCultivateExam oldExam) { + FtbCultivateExamHistoryPaper entity = BeanUtil.copyProperties(oldExam, FtbCultivateExamHistoryPaper.class); + entity.setPrimaryExamId(entity.getId()); + entity.setId(null); + baseMapper.insert(entity); + } + + /** + * 根据考试id查询历史试卷 + * @param examId 考试id + * @return + */ + @Override + public List queryByExamId(String examId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamHistoryPaper::getPrimaryExamId, examId) + .orderByDesc(FtbCultivateExamHistoryPaper::getCreatorTime); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(list)) { + return list; + } + return new ArrayList<>(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamServiceImpl.java new file mode 100644 index 0000000..6762adc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamServiceImpl.java @@ -0,0 +1,2221 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.HistoryPaperUtils; +import jnpf.cultivate.utils.QuestionExcelExportUtil; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.utils.UserExamUtil; +import jnpf.enums.cultivate.ExamQuestionShuffleEnum; +import jnpf.enums.cultivate.ExamRetakeFrequencyEnum; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamHistoryPaper; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskExam; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; +import jnpf.model.cultivate.req.exam.QueryExamReq; +import jnpf.model.cultivate.req.exam.ReviewerAppointDto; +import jnpf.model.cultivate.req.exam.SaveExamReq; +import jnpf.model.cultivate.req.exam.WebQueryExamReadOverReq; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.*; +import jnpf.model.personnels.dto.authoritys.PermissionsCacheDTO; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.RoleApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.RoleEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UploaderUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbCultivateExamServiceImpl extends ServiceImpl implements FtbCultivateExamService { + + @Autowired + private UserApi userApi; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private RoleApi roleApi; + + + @Autowired + private PersonnelPerUtils personnelPerUtils; + /** + * 考试用户服务 + */ + + @Autowired + private FtbCultivateExamUserService examUserService; + + /** + * 试卷服务 + */ + @Autowired + private FtbCultivateTestPaperService paperService; + + /** + * 岗位学习和考试关联表 + */ + @Autowired + private FtbCultivatePositionExamService positionExamService; + /** + * 题目选项服务 + */ + @Autowired + FtbCultivateQuestionOptionService questionOptionService; + + /** + * 岗位学习课程 考试 关联表 + */ + @Autowired + private FtbCultivatePositionCourseExamService positionCourseExamService; + + @Autowired + private FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + @Autowired + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + + @Autowired + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Autowired + private FtbCultivateExamHistoryPaperService ftbCultivateExamHistoryPaperService; + + @Autowired + private UserExamUtil userExamUtil; + + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private FtbCultivateQuestionService questionService; + + @Autowired + private FtbCultivateLearnTaskExamMapper ftbCultivateLearnTaskExamMapper; + + + @Autowired + private FtbCultivateLearnTaskInfoContentService ftbCultivateLearnTaskInfoContentService; + + @Autowired + private FtbCultivateLearnTaskIdentificationMapper learnTaskIdentificationMapper; + + private final static String DEFAULT_VERSION_BATCH = "dfa"; + + /** + * 根据试卷ID查询考试列表 + * + * @param paperId 试卷ID + * @return + */ + @Override + public List queryExamListByPaperId(String paperId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbCultivateExam::getPaperId, paperId).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return CollectionUtil.newArrayList(); + } + return BeanUtil.copyToList(list, ExamVo.class); + } + + /** + * 分页查询考试列表 + * + * @param req + * @return + */ + @Override + public PageInfo getPageList(QueryExamReq req) { + if (StringUtils.isNotEmpty(req.getKeyword())) { + req.setKeyword(StringUtils.trim(req.getKeyword())); + } + //构建分页 + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + Page queryPage = baseMapper.pagingQuery(page, req); + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + for (ExamListVo record : records) { + //填充考试的用户统计数据 + fillExamUserData(record); + fillNeedPassScore(record); + checkExamExpire(record); + } + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 检测并修改考试状态 + * + * @param record + */ + private void checkExamExpire(ExamListVo record) { + if (record.getExamType() == 1 && record.getStatus() != 3) { + if (record.getStartTime() != null && record.getEndTime() != null) { + Date now = new Date(); + if (now.before(record.getStartTime())) { + record.setStatus(CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()); + } else if (now.after(record.getEndTime())) { + record.setStatus(CourseEnums.ExamTimeStatus.DONE.getCode()); + record.setNeedUpdate(ExamUpdateStatus.NO_UPDATE.getCode()); + } else if (now.after(record.getStartTime()) && now.before(record.getEndTime())) { + record.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } else { + record.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } + } + + private void fillNeedPassScore(ExamListVo vo) { + FtbCultivateTestPaper paper = null; + if (StringUtils.isNotEmpty(vo.getPaperInfo())) { + paper = JSONUtil.toBean(vo.getPaperInfo(), FtbCultivateTestPaper.class); + } else { + paper = paperService.getById(vo.getPaperId()); + } + if (paper == null) { + return; + } + //计算用户需要考多少分才及格 + int needScore = 0; + Integer passType = vo.getPassType();//合格分数类型(1固定分,2百分比) + Integer passMark = vo.getPassMark();//合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(passType)) { + needScore = passMark; + } else { + needScore = QuestionAnalysisUtil.calculateScore(passMark, paper.getTotalScore()); + } + vo.setNeedExamPassScore(needScore); + + + //计算用户需要考多少分才优秀 + int needExcellentScore = 0; + Integer excellentType = vo.getExcellentType();//优秀分数类型(1固定分,2百分比) + Integer excellentMark = vo.getExcellentMark();//优秀分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(excellentType)) { + needExcellentScore = excellentMark; + } else { + needExcellentScore = QuestionAnalysisUtil.calculateScore(excellentMark, paper.getTotalScore()); + } + vo.setNeedExamExcellentScore(needExcellentScore); + } + + /** + * 查询所有批阅角色 + * + * @return + */ + @Override + public List queryRoleReviewerList() { + List listAll = roleApi.getListAll(); + if (CollectionUtil.isEmpty(listAll)) { + return new ArrayList<>(); + } + return BeanUtil.copyToList(listAll, ReviewerRole.class); + } + + @Override + public PaperVo queryPaperInfoForExamId(String examId) { + //1、查询考试信息 + FtbCultivateExam exam = queryExamInfo(examId); + //2、查询试卷信息 + FtbCultivateTestPaper paper = queryPaper(exam.getPaperId()); + + return BeanUtil.copyProperties(paper, PaperVo.class); + } + + /** + * 根据考试ID列表 查询试卷信息 + * + * @param examIds 考试ID列表 + * @return examId -> PaperVo 的map + */ + @Override + public Map queryPaperInfoForExamIds(List examIds) { + //1、查询考试信息 + List examList = queryExamListForExamIds(examIds); + if (CollectionUtil.isEmpty(examList)) { + return new HashMap<>(); + } + //2、查询试卷信息 + List paperIds = examList.stream().map(FtbCultivateExam::getPaperId).collect(Collectors.toList()); + + List paperList = queryPaperInfoForPaperId(paperIds); + //考试ID->vo map + Map examPaperVoMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(paperList)) { + //试卷id->vo map + Map paperVoMap = paperList.stream().collect(Collectors.toMap(FtbCultivateTestPaper::getId, paper -> BeanUtil.copyProperties(paper, PaperVo.class))); + for (FtbCultivateExam exam : examList) { + examPaperVoMap.put(exam.getId(), paperVoMap.get(exam.getPaperId())); + } + } + return examPaperVoMap; + } + + /** + * 根据考试id列表查询考试信息 + * + * @param examIds 考试ID列表 + * @return + */ + private List queryExamListForExamIds(List examIds) { + List examList = baseMapper.selectList(Wrappers.lambdaQuery(FtbCultivateExam.class).in(FtbCultivateExam::getId, examIds).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + if (CollectionUtil.isEmpty(examList)) { + return Collections.emptyList(); + } + return examList; + } + + /** + * 根据试卷ID查询试卷信息 + * + * @param paperIds 试卷ID列表 + * @return + */ + private List queryPaperInfoForPaperId(List paperIds) { + List paperList = paperService.list(Wrappers.lambdaQuery(FtbCultivateTestPaper.class).in(FtbCultivateTestPaper::getId, paperIds).eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + if (CollectionUtil.isEmpty(paperList)) { + return Collections.emptyList(); + } + return paperList; + } + + /** + * 根据考试ID批量查询考试信息和考试试卷信息 + * + * @param examIds + * @return + */ + @Override + public List queryExamAndPaperInfo(List examIds) { + //1、查询考试列表 + List examList = queryExamListForExamIds(examIds); + if (CollectionUtil.isEmpty(examList)) { + return Collections.emptyList(); + } + //2、查询试卷信息 + List paperIds = examList.stream().map(FtbCultivateExam::getPaperId).collect(Collectors.toList()); + List appExamVoList = BeanUtil.copyToList(examList, AppExamVo.class); + if (CollectionUtil.isNotEmpty(paperIds)) { + List testPaperList = queryPaperInfoForPaperId(paperIds); + //试卷id->vo map + Map paperVoMap = testPaperList.stream().collect(Collectors.toMap(FtbCultivateTestPaper::getId, paper -> BeanUtil.copyProperties(paper, PaperVo.class))); + for (AppExamVo appExamVo : appExamVoList) { + appExamVo.setPaperVo(paperVoMap.get(appExamVo.getPaperId())); + } + } + return appExamVoList; + } + + /** + * 填充考试的用户统计数据 + * + * @param vo + */ + private void fillExamUserData(ExamListVo vo) { + //查询考试的用户 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, FtbCultivateExamUser::getRelationCourseExamId, FtbCultivateExamUser::getRelationPositionExamId, FtbCultivateExamUser::getRelationRankId).eq(FtbCultivateExamUser::getExamId, vo.getId()).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List examUserList = examUserService.list(wrapper); + if (CollectionUtil.isEmpty(examUserList)) { + return; + } + UserExamCount userExamCount = QuestionAnalysisUtil.countPassAndTotleExamNum(examUserList); + vo.setParticipantsNum(userExamCount.getTotleNum()); + vo.setEligibleNum(userExamCount.getPassTotleNum()); + vo.setNoEligibleNum(userExamCount.getNoPassNum()); + vo.setWaitNum(userExamCount.getWaitNum()); + } + + /** + * 填充试卷的题目数量和总分数 + * + * @param vo + */ + private void fillPaperData(ExamListVo vo) { + String paperId = vo.getPaperId(); + FtbCultivateTestPaper paper = paperService.getById(paperId); + if (null != paper) { + vo.setQuestionNum(paper.getQuestionNumber()); + vo.setTotleScore(paper.getTotalScore()); + } + } + + /** + * 查询考试详情 + * + * @param examId 考试ID + * @return + */ + @Override + public ExamVo getInfo(String examId) { + //1、查询和检测试卷是否存在 + FtbCultivateExam exam = queryExamInfo(examId); + ExamVo examVo = convertExamVo(exam); + if (StringUtils.isNotEmpty(exam.getPaperId())) { + FtbCultivateTestPaper paper = paperService.getById(exam.getPaperId()); + if (null == paper || paper.getEnabledMark() == CourseEnums.EnabledMarkType.INVALID.getCode()) { + examVo.setPaperDeleteStatus(CourseEnums.EnabledMarkType.INVALID.getCode()); + } + examVo.setPaperNeedUpdate(paper.getNeedUpdate()); + + } + return examVo; + } + + /** + * 转换考试对象 + * + * @param exam + * @return + */ + private ExamVo convertExamVo(FtbCultivateExam exam) { + ExamVo vo = new ExamVo(); + vo.setId(exam.getId()); + vo.setExamId(exam.getExamId()); + vo.setExamTime(exam.getExamTime()); + vo.setPassType(exam.getPassType()); + vo.setPassMark(exam.getPassMark()); + vo.setExcellentType(exam.getExcellentType()); + vo.setExcellentMark(exam.getExcellentMark()); + vo.setStartTime(exam.getStartTime()); + vo.setEndTime(exam.getEndTime()); + vo.setPaperId(exam.getPaperId()); + vo.setExamName(exam.getExamName()); + vo.setPostRankId(exam.getPostRankId()); + vo.setPostId(exam.getPostId()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setStatus(exam.getStatus()); + vo.setReviewer(exam.getReviewer()); + vo.setReviewerRole(exam.getReviewerRole()); + vo.setReviewerAppoint(exam.getReviewerAppoint()); + vo.setExamMemberId(exam.getExamMemberId()); + vo.setDescription(exam.getDescription()); + vo.setExamType(exam.getExamType()); + vo.setVersionBatch(exam.getVersionBatch()); + vo.setNeedUpdate(exam.getNeedUpdate()); + + + if (StringUtils.isNotEmpty(exam.getPostId())) { + List positionIdList = List.of(exam.getPostId().split(",")); + List positionEntityList = userExamUtil.getPostInfoList(positionIdList); + List postAndPositionList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(positionEntityList)) { + for (PositionVO positionEntity : positionEntityList) { + PostAndPosition postAndPosition = new PostAndPosition(); + postAndPosition.setId(positionEntity.getId()); + postAndPosition.setEnCode(positionEntity.getEnCode()); + postAndPosition.setPostName(positionEntity.getFullName()); + postAndPositionList.add(postAndPosition); + } + } + vo.setPostAndPositionList(postAndPositionList); + } + + List userIdList = new ArrayList<>(); + if (StringUtils.isNotEmpty(exam.getExamMemberId())) { + userIdList.addAll(Arrays.asList(exam.getExamMemberId().split(","))); + } + //查询批阅人信息 + if (StringUtils.isNotEmpty(exam.getReviewerAppoint())) { + userIdList.addAll(Arrays.asList(exam.getReviewerAppoint().split(","))); + } + Map userMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(userIdList)) { + userMap = userApiV2Util.getUserNameForUserIds(userIdList); + } + //处理考试用户信息 回显 + if (StringUtils.isNotEmpty(exam.getExamMemberId())) { + //MemberId转list + String[] split = exam.getExamMemberId().split(","); + List examBaseUserList = new ArrayList<>(); + //获取用户信息 + for (int i = 0; i < split.length; i++) { + String userId = split[i]; + UserEntity userEntity = userMap.get(userId); + if (userEntity != null) { + ExamBaseUser examBaseUser = new ExamBaseUser(); + examBaseUser.setUserId(userEntity.getId()); + examBaseUser.setUserName(userEntity.getRealName()); + examBaseUser.setHeadLogo(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + examBaseUserList.add(examBaseUser); + } + } + vo.setExamUserList(examBaseUserList); + } + + //回显批阅人 + if (StringUtils.isNotEmpty(exam.getReviewerAppoint())) { + //MemberId转list + String[] split = exam.getReviewerAppoint().split(","); + List reviewerList = new ArrayList<>(); + //获取用户信息 + for (int i = 0; i < split.length; i++) { + String userId = split[i]; + UserEntity userEntity = userMap.get(userId); + if (userEntity != null) { + ExamBaseUser examBaseUser = new ExamBaseUser(); + examBaseUser.setUserId(userEntity.getId()); + examBaseUser.setUserName(userEntity.getRealName()); + examBaseUser.setHeadLogo(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + reviewerList.add(examBaseUser); + } + } + vo.setReviewerAppointList(reviewerList); + } + + + if (StringUtils.isNotEmpty(exam.getReviewerAppointListConfig())) { + vo.setReviewerAppointListConfig(exam.getReviewerAppointListConfig()); + } else { + vo.setReviewerAppointListConfig(""); + } + //考试配置 + vo.setMakeUpCount(exam.getMakeUpCount()); + vo.setWaterMark(exam.getWaterMark()); + vo.setCutScreen(exam.getCutScreen()); + vo.setMaxCutScreenCount(exam.getMaxCutScreenCount()); + vo.setScreenshot(exam.getScreenshot()); + vo.setCopyAndPaste(exam.getCopyAndPaste()); + vo.setOpenOverTime(exam.getOpenOverTime()); + vo.setOverTime(exam.getOverTime()); + vo.setAllowSelectDetail(exam.getAllowSelectDetail()); + vo.setAllowChangeLastQuestion(exam.getAllowChangeLastQuestion()); + vo.setReExamFrequencyType(exam.getReExamFrequencyType()); + vo.setReExamFrequencyNum(exam.getReExamFrequencyNum()); + vo.setExamQuestionRandom(exam.getExamQuestionRandom()); + return vo; + + } + + /** + * 新增考试 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void insertData(SaveExamReq req) { + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(req.getExamType()); + List examUserList = new ArrayList<>(); + Map defaultPowerReviewer = new HashMap<>(); //userid-> + Map rosterMap = new HashMap<>();//userid-> + Map> userOrgMap = new HashMap<>();//userid-> + checkExamCommonParam(req); + if (CourseEnums.ExamType.BASE == examType) { + if (StringUtils.isEmpty(req.getExamMemberId())) { + throw new RuntimeException("考试用户不能为空"); + } + examUserList = QuestionExcelExportUtil.stringToList(req.getExamMemberId(), ","); + rosterMap = userExamUtil.queryRosterSimpleInfo(examUserList); + userOrgMap = userExamUtil.getUserOrgBoundInfoForUserList(examUserList); + } + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbCultivateExam::getExamName, req.getExamName()).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("考试名称已经存在"); + } + //1、添加考试 + //1.1根据试卷ID查询试卷信息 + FtbCultivateTestPaper paper = queryPaper(req.getPaperId()); + if (CourseEnums.DraftStatus.YES.getCode().equals(paper.getIsDraft())) { + throw new RuntimeException("试卷未定稿,不能绑定考试"); + } + if (ExamUpdateStatus.NEED_UPDATE.getCode().equals(paper.getNeedUpdate())) { + throw new RuntimeException("试卷异常,请先编辑试卷"); + } + FtbCultivateExam updateExam = new FtbCultivateExam(); + checkParamAndFillExam(req, updateExam); + updateExam.setExamName(req.getExamName()); + updateExam.setPaperId(req.getPaperId()); + updateExam.setPaperName(paper.getName()); + updateExam.setPaperInfo(JSONUtil.toJsonStr(paper)); + updateExam.setVersionBatch(DEFAULT_VERSION_BATCH); + if (CourseEnums.ExamType.POSITION == examType) { + if (CollectionUtil.isEmpty(req.getPostAndPositionList())) { + throw new RuntimeException("请选择考试岗位"); + } + updateExam.setPostRankConf(JSONUtil.toJsonStr(req.getPostAndPositionList())); + List postIdsList = new ArrayList<>();//岗位ID集合 + for (PostAndPosition postAndPosition : req.getPostAndPositionList()) { + postIdsList.add(postAndPosition.getId()); + } + updateExam.setPostId(String.join(",", postIdsList)); + } else { + updateExam.setPostRankConf(""); + updateExam.setPostId(""); + } + updateExam.setPassType(req.getPassType()); + updateExam.setPassMark(req.getPassMark()); + updateExam.setExcellentType(req.getExcellentType()); + updateExam.setExcellentMark(req.getExcellentMark()); + updateExam.setDescription(req.getDescription()); + updateExam.setExamMemberId(req.getExamMemberId()); + updateExam.setReviewer(req.getReviewer());//批阅人/直接主管 + updateExam.setReviewerRole(req.getReviewerRole());//批阅角色 + updateExam.setExamId(generateExamId()); + updateExam.setStatus(CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()); + updateExam.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + //判断是否是常规考试 + fillExamStatus(updateExam); + fillExamPaperQuestion(updateExam); + updateExam.setNeedUpdate(ExamUpdateStatus.NO_UPDATE.getCode()); + baseMapper.insert(updateExam); + + //2、添加考试成员 + switch (examType) { + case BASE: + batchInsertExamMemberForMemberIdNew(updateExam, examUserList, rosterMap, userOrgMap, defaultPowerReviewer); + break; + case POSITION: + //岗位学习考试 + break; + default: + break; + } + } + + /** + * 校验补考频次参数 + * + * @param req + */ + private void checkExamCommonParam(SaveExamReq req) { + //重复考试 + if (CourseEnums.RepeatExam.REPEAT_EXAM.getCode().equals(req.getExamlimitation())) { + ExamRetakeFrequencyEnum examRetakeFrequencyEnum = ExamRetakeFrequencyEnum.of(req.getReExamFrequencyType()); + if (examRetakeFrequencyEnum == null) { + throw new RuntimeException("参数异常,请选择补考频次"); + } + if (examRetakeFrequencyEnum != ExamRetakeFrequencyEnum.UNLIMITED) { + if (req.getReExamFrequencyNum() == null) { + throw new RuntimeException("参数异常,请选择补考频次次数"); + } + if (req.getReExamFrequencyNum() > 10) { + throw new RuntimeException("参数异常,补考频次不能大于10次"); + } + + if (req.getReExamFrequencyNum() < 0) { + throw new RuntimeException("参数异常,补考频次不能小于0次"); + } + + } + } + + ExamQuestionShuffleEnum sh = ExamQuestionShuffleEnum.of(req.getExamQuestionRandom()); + if (sh == null) { + throw new RuntimeException("参数异常,请选择试题设置"); + } + } + + private void batchCheckAndRevierwer(Map rosterMap, Map> userOrgMap, List examUserList, Map defaultPowerReviewer) { + PermissionsCacheDTO permissionsCacheDTO = personnelPerUtils.doQueryUserPermissions(List.of("70")); + + for (String tempUserId : examUserList) { + List localOrganizeIds = null; + List workerGroupDataDtoList = userOrgMap.get(tempUserId); + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + localOrganizeIds = workerGroupDataDtoList.stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()); + } + List powerUserIdsList = examUserService.getPowerUserIdsForPermission(tempUserId, permissionsCacheDTO, localOrganizeIds); + String powerUserIds = ""; + if (CollectionUtil.isNotEmpty(powerUserIdsList)) { + powerUserIds = String.join(",", powerUserIdsList); + } + log.error("power examUser=" + powerUserIds); + if (StringUtils.isEmpty(powerUserIds)) { + FtbPersonnelsStaffRoster roster = rosterMap.get(tempUserId); + if (null != roster) { + throw new RuntimeException("员工[" + roster.getName() + "]的考试没有批阅人"); + } else { + throw new RuntimeException("用户ID" + tempUserId + "没有批阅人"); + } + } else { + defaultPowerReviewer.put(tempUserId, powerUserIds); + } + } + } + + /** + * 填充考试题目数量和分数 和题目备份 + * + * @param exam + */ + private void fillExamPaperQuestion(FtbCultivateExam exam) { + List list = paperService.queryConfigQuestionList(exam.getPaperId()); + if (CollectionUtil.isEmpty(list)) { + throw new RuntimeException("试卷没有配置题目"); + } + exam.setCurrQuestionNumber(list.size()); + exam.setCurrTotalScore(list.stream().mapToInt(examQuestionBakVo -> examQuestionBakVo.getScore()).sum()); + exam.setCurrQuestionList(JSONUtil.toJsonStr(list)); + } + + private void fillExamPaperQuestionOld(FtbCultivateExam exam) { + List list = paperService.queryConfigQuestionList(exam.getPaperId()); + if (CollectionUtil.isEmpty(list)) { + return; + } + exam.setCurrQuestionNumber(list.size()); + exam.setCurrTotalScore(list.stream().mapToInt(examQuestionBakVo -> examQuestionBakVo.getScore()).sum()); + exam.setCurrQuestionList(JSONUtil.toJsonStr(list)); + } + + /** + * 检测考试配置参数 + * + * @param req + */ + private void checkParamAndFillExam(SaveExamReq req, FtbCultivateExam exam) { + CourseEnums.RepeatExam examlimitation = CourseEnums.RepeatExam.fromCode(req.getExamlimitation()); + if (null == examlimitation) { + throw new RuntimeException("考试限制参数错误"); + } + exam.setExamlimitation(examlimitation.getCode()); + if (examlimitation == CourseEnums.RepeatExam.REPEAT_EXAM) { + exam.setReExamFrequencyType(req.getReExamFrequencyType()); + exam.setReExamFrequencyNum(req.getReExamFrequencyNum()); + } + + ExamConfigEnums.WatermarkStatus watermarkStatus = ExamConfigEnums.WatermarkStatus.fromCode(req.getWaterMark()); + if (null == watermarkStatus) { + throw new RuntimeException("水印设置参数错误"); + } + exam.setWaterMark(watermarkStatus.getCode()); + + + ExamConfigEnums.ScreenLockStatus cutScreen = ExamConfigEnums.ScreenLockStatus.fromCode(req.getCutScreen()); + if (null == cutScreen) { + throw new RuntimeException("切屏限制参数错误"); + } + exam.setCutScreen(cutScreen.getCode()); + if (cutScreen == ExamConfigEnums.ScreenLockStatus.ENABLED) { + if (req.getMaxCutScreenCount() < 1) { + throw new RuntimeException("切屏次数不能小于1"); + } + exam.setMaxCutScreenCount(req.getMaxCutScreenCount()); + } + + + ExamConfigEnums.ScreenshotRestriction screenshot = ExamConfigEnums.ScreenshotRestriction.fromCode(req.getScreenshot()); + if (null == screenshot) { + throw new RuntimeException("禁止截屏参数错误"); + } + exam.setScreenshot(screenshot.getCode()); + + + ExamConfigEnums.CopyPasteRestriction copyAndPaste = ExamConfigEnums.CopyPasteRestriction.fromCode(req.getCopyAndPaste()); + if (null == copyAndPaste) { + throw new RuntimeException("禁止复制粘贴参数错误"); + } + exam.setCopyAndPaste(copyAndPaste.getCode()); + + + ExamConfigEnums.UserInactivityTimeoutCheatingPolicy openOverTime = ExamConfigEnums.UserInactivityTimeoutCheatingPolicy.fromCode(req.getOpenOverTime()); + if (null == openOverTime) { + throw new RuntimeException("超时无交互退出配置参数错误"); + } + exam.setOpenOverTime(openOverTime.getCode()); + if (openOverTime == ExamConfigEnums.UserInactivityTimeoutCheatingPolicy.ENABLED) { + if (null == req.getOverTime()) { + throw new RuntimeException("超时无交互时间不能为空"); + } + if (req.getOverTime() < 1) { + throw new RuntimeException("超时无交互时间不能小于1秒"); + } + exam.setOverTime(req.getOverTime()); + } + + + ExamConfigEnums.ExamDetailVisibility allowSelectDetail = ExamConfigEnums.ExamDetailVisibility.fromCode(req.getAllowSelectDetail()); + if (null == allowSelectDetail) { + throw new RuntimeException("考试完成后允许学员本人查看考卷详情配置参数错误"); + } + exam.setAllowSelectDetail(allowSelectDetail.getCode()); + + ExamConfigEnums.BackToPreviousQuestionPolicy allowChangeLastQuestion = ExamConfigEnums.BackToPreviousQuestionPolicy.fromCode(req.getAllowChangeLastQuestion()); + if (null == allowChangeLastQuestion) { + throw new RuntimeException("考试过程中不允许回到上一题配置参数错误"); + } + exam.setAllowChangeLastQuestion(allowChangeLastQuestion.getCode()); + + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(req.getExamType()); + if (null == examType) { + throw new RuntimeException("考试类型参数错误"); + } + exam.setExamType(examType.getCode()); + if (examType == CourseEnums.ExamType.BASE) { + + if (null != req.getStartTime() && null != req.getEndTime()) { + //判断开始时间不能小于结束时间 + if (req.getStartTime().after(req.getEndTime())) { + throw new RuntimeException("考试开始时间不能大于结束时间"); + } + exam.setStartTime(req.getStartTime()); + exam.setEndTime(req.getEndTime()); + long maxSecond = DateUtil.between(req.getStartTime(), req.getEndTime(), DateUnit.SECOND); + if (req.getExamTime() != null) { + long examTime = Long.valueOf(req.getExamTime()); + if (examTime > maxSecond) { + throw new RuntimeException("考试时长不能大于考试范围时长"); + } + } + } + } else if (examType == CourseEnums.ExamType.POSITION) { + if (req.getExamTime() == null || req.getExamTime() < 1) { + throw new RuntimeException("考试时长不能小于1"); + } + } + exam.setExamTime(req.getExamTime()); + exam.setExamQuestionRandom(req.getExamQuestionRandom()); + } + + /** + * 根据考试开始日期和结束日期 判断修改考试状态 + * + * @param exam + */ + private void fillExamStatus(FtbCultivateExam exam) { + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + Date now = new Date(); + if (exam.getStartTime() != null && exam.getEndTime() != null) { + if (now.after(exam.getEndTime())) { + exam.setStatus(CourseEnums.ExamTimeStatus.DONE.getCode()); + } else { + if (now.after(exam.getStartTime())) { + exam.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } else { + exam.setStatus(CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()); + } + } + } else { + exam.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } else { + exam.setStatus(CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()); + } + } + + /** + * 查询试卷信息 + * + * @param paperId 试卷ID + * @return + */ + private FtbCultivateTestPaper queryPaper(String paperId) { + FtbCultivateTestPaper paper = paperService.getById(paperId); + if (paper == null) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + return paper; + } + + + /** + * 查询试卷信息 + * + * @param paperId 试卷ID + * @return + */ + private FtbCultivateTestPaper queryPaperNocheckDelete(String paperId) { + FtbCultivateTestPaper paper = paperService.getById(paperId); + if (paper == null) { + throw new RuntimeException("试卷不存在"); + } + return paper; + } + + /** + * 生成考试ID + * + * @return + */ + private String generateExamId() { + return SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.EXAM); + } + + + /** + * 批量添加考试成员 + * + * @param exam 考试 + * @param examMemberList 考试用户ID集合 + */ + private void batchInsertExamMemberForMemberId(FtbCultivateExam exam, List examMemberList) { + + List examUserList = new ArrayList<>(); + for (String examMemberId : examMemberList) { + FtbCultivateExamUser examUser = new FtbCultivateExamUser(); + examUser.setExamId(exam.getId()); + examUser.setUserId(examMemberId); + examUser.setStatus(CourseEnums.ExamStatus.WAIT.getCode()); + examUser.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + examUser.setPaperId(exam.getPaperId()); + examUser.setStartTime(exam.getStartTime()); + examUser.setEndTime(exam.getEndTime()); + examUser.setExamType(exam.getExamType()); + examUser.setExamSource(CourseEnums.RelationExamSource.REGULAR_EXAM.getCode()); + examUser.setBatch(UUID.randomUUID().toString().replaceAll("-", "")); + examUser.setUserExamCount(1); + //查询用户所属组织 + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(examMemberId, exam.getTenantId()); + List selectOrgList = List.of(userPrimaryBoundOne.getOrganizeId()); + if (CollectionUtil.isNotEmpty(selectOrgList)) { + examUser.setUserOrgList(String.join(",", selectOrgList)); + } + //填充批阅人信息 和阅卷角色 +// fillRevierwer(examUser, exam); +// if (exam.getExamType() == 1 && StringUtils.isEmpty(examUser.getReviewerUserIds())) { +// UserEntity userEntity = userApi.getInfoById(examUser.getUserId()); +// String userName = ""; +// if (userEntity != null) { +// userName = userEntity.getRealName(); +// } +// throw new RuntimeException("员工:" + userName + "的考试没有批阅人"); +// } + examUserList.add(examUser); + } + examUserService.saveBatch(examUserList); + } + + private void batchInsertExamMemberForMemberIdNew(FtbCultivateExam exam, List examUserList, Map rosterMap, Map> userOrgMap, Map defaultPowerReviewer) { + List addList = new ArrayList<>(); + for (String examMemberId : examUserList) { + FtbCultivateExamUser examUser = new FtbCultivateExamUser(); + examUser.setExamId(exam.getId()); + examUser.setUserId(examMemberId); + examUser.setStatus(CourseEnums.ExamStatus.WAIT.getCode()); + examUser.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + examUser.setPaperId(exam.getPaperId()); + examUser.setStartTime(exam.getStartTime()); + examUser.setEndTime(exam.getEndTime()); + examUser.setExamType(exam.getExamType()); + examUser.setExamSource(CourseEnums.RelationExamSource.REGULAR_EXAM.getCode()); + examUser.setBatch(UUID.randomUUID().toString().replaceAll("-", "")); + examUser.setUserExamCount(1); + //查询用户所属组织 + List workerGroupDataDtoList = userOrgMap.get(examMemberId); + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + examUser.setUserOrgList(String.join(",", QuestionExcelExportUtil.getOrgIds(workerGroupDataDtoList))); + } + examUser.setVersionBatch(exam.getVersionBatch()); + addList.add(examUser); + } + examUserService.saveBatch(addList); + } + + @Override + public void fillRevierwer(FtbCultivateExamUser examUser, FtbCultivateExam exam) { + + //阅卷人 + Set reviewerUserIdList = new HashSet<>(); + if (StringUtils.isNotEmpty(exam.getReviewerAppoint())) { + reviewerUserIdList.addAll(Arrays.asList(exam.getReviewerAppoint().split(","))); + } + if ("0".equals(exam.getReviewer())) { + //查询当前用户的直接主管 + UserBoundVO bossByUserIdSimple = userApiV2Util.getBossByUserIdSimple(examUser.getUserId(), null); + if (bossByUserIdSimple != null && StringUtils.isNotEmpty(bossByUserIdSimple.getLeaderId())) { + reviewerUserIdList.add(bossByUserIdSimple.getLeaderId()); + } + } + String powerUserIds = personnelPerUtils.obtainTrainingApproverPermissions(examUser.getUserId()); + if (StringUtils.isNotEmpty(powerUserIds)) { + reviewerUserIdList.addAll(Arrays.asList(powerUserIds.split(","))); + } + + if (CollectionUtil.isNotEmpty(reviewerUserIdList)) { + examUser.setReviewerUserIds(String.join(",", reviewerUserIdList)); + } + } + + /** + * 填充考试的批阅人信息 + * + * @param examUser 考试用户记录薪酬 + * @param exam 考试信息 + * @param permissionsCacheDTO 考试的权限信息 + */ + @Override + public void fillRevierwerNew(FtbCultivateExamUser examUser, FtbCultivateExam exam, PermissionsCacheDTO permissionsCacheDTO) { + //阅卷人 + Set reviewerUserIdList = new HashSet<>(); + if (StringUtils.isNotEmpty(exam.getReviewerAppoint())) { + reviewerUserIdList.addAll(Arrays.asList(exam.getReviewerAppoint().split(","))); + } + List localOrganizeIds = null; + if ("0".equals(exam.getReviewer())) { + //查询当前用户的直接主管 + UserBoundVO userBoundVO = userApiV2Util.getBossByUserIdSimple(examUser.getUserId(), null); + if (userBoundVO != null && StringUtils.isNotEmpty(userBoundVO.getLeaderId())) { + reviewerUserIdList.add(userBoundVO.getLeaderId()); + } + } + List powerUserIdsList = examUserService.getPowerUserIdsForPermission(examUser.getUserId(), permissionsCacheDTO, localOrganizeIds); + String powerUserIds = ""; + if (CollectionUtil.isNotEmpty(powerUserIdsList)) { + powerUserIds = String.join(",", powerUserIdsList); + } +// String powerUserIds = personnelPerUtils.obtainTrainingApproverPermissions(examUser.getUserId()); + if (StringUtils.isNotEmpty(powerUserIds)) { + reviewerUserIdList.addAll(Arrays.asList(powerUserIds.split(","))); + } + + if (CollectionUtil.isNotEmpty(reviewerUserIdList)) { + examUser.setReviewerUserIds(String.join(",", reviewerUserIdList)); + } + } + + + /** + * 修改考试 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateData(SaveExamReq req) { + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(req.getExamType()); + List examUserList = new ArrayList<>(); + Map defaultPowerReviewer = new HashMap<>(); //userid-> + Map rosterMap = new HashMap<>();//userid-> + Map> userOrgMap = new HashMap<>();//userid-> + FtbCultivateExam oldExam = queryExamInfo(req.getId()); + checkExamCommonParam(req); + if (CourseEnums.ExamType.BASE == examType) { + if (StringUtils.isEmpty(req.getExamMemberId())) { + throw new RuntimeException("考试用户不能为空"); + } + if (CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode().equals(oldExam.getStatus())) { + throw new RuntimeException("考试进行中,不能修改"); + } + if (CourseEnums.ExamTimeStatus.DONE.getCode().equals(oldExam.getStatus())) { + throw new RuntimeException("考试已经完成,不能修改"); + } + if (CourseEnums.ExamTimeStatus.NULLIFY.getCode().equals(oldExam.getStatus())) { + throw new RuntimeException("考试已经作废,不能修改"); + } + examUserList = QuestionExcelExportUtil.stringToList(req.getExamMemberId(), ","); + rosterMap = userExamUtil.queryRosterSimpleInfo(examUserList); + userOrgMap = userExamUtil.getUserOrgBoundInfoForUserList(examUserList); + } + if (StringUtils.isEmpty(req.getReviewer())) { + req.setReviewer(""); + } + //1、检测名称是否重复 试卷是否存在 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbCultivateExam::getExamName, req.getExamName()).ne(FtbCultivateExam::getId, req.getId()).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("考试名称已经存在"); + } + + FtbCultivateTestPaper paper = queryPaper(req.getPaperId()); + if (CourseEnums.DraftStatus.YES.getCode().equals(paper.getIsDraft())) { + throw new RuntimeException("试卷未定稿,不能绑定考试"); + } + if (ExamUpdateStatus.NEED_UPDATE.getCode().equals(paper.getNeedUpdate())) { + throw new RuntimeException("试卷异常,请先编辑试卷"); + } + + //2、修改考试 + FtbCultivateExam updateExam = new FtbCultivateExam(); + checkParamAndFillExam(req, updateExam); + updateExam.setId(oldExam.getId()); + updateExam.setVersionBatch(oldExam.getVersionBatch()); + updateExam.setExamName(req.getExamName()); + updateExam.setPaperId(req.getPaperId()); + updateExam.setPaperName(paper.getName()); + updateExam.setPassType(req.getPassType()); + updateExam.setPassMark(req.getPassMark()); + updateExam.setExcellentType(req.getExcellentType()); + updateExam.setExcellentMark(req.getExcellentMark()); + updateExam.setDescription(req.getDescription()); + updateExam.setExamMemberId(req.getExamMemberId()); + updateExam.setReviewer(req.getReviewer());//批阅人/直接主管 + updateExam.setNeedUpdate(ExamUpdateStatus.NO_UPDATE.getCode()); + updateExam.setCreatorTime(oldExam.getCreatorTime()); + updateExam.setCreatorUserId(oldExam.getCreatorUserId()); + + updateExam.setReviewerRole(req.getReviewerRole());//批阅角色 + updateExam.setLastModifyTime(new Date()); + fillExamStatus(updateExam); + fillExamPaperQuestion(updateExam); + countQuestionTypeNum(paper); + updateExam.setPaperInfo(JSONUtil.toJsonStr(paper)); + //处理岗位 是否有批次 + Boolean isNewBatch = false; + if (CourseEnums.ExamType.POSITION == examType) { + if (CollectionUtil.isEmpty(req.getPostAndPositionList())) { + throw new RuntimeException("请选择考试岗位"); + } + updateExam.setPostRankConf(JSONUtil.toJsonStr(req.getPostAndPositionList())); + List postIdsList = new ArrayList<>();//岗位ID集合 + for (PostAndPosition postAndPosition : req.getPostAndPositionList()) { + postIdsList.add(postAndPosition.getId()); + } + updateExam.setPostId(String.join(",", postIdsList)); + isNewBatch = fillPaperInfoAndCheckIsNewBatch(updateExam, oldExam, paper); + if (isNewBatch) { + updateExam.setVersionBatch(UUID.randomUUID().toString().replaceAll("-", "")); + ftbCultivateExamHistoryPaperService.recordHistory(oldExam); + } + } else { + updateExam.setPostRankConf(""); + updateExam.setPostId(""); + } + updateExam.setExamId(oldExam.getExamId()); + baseMapper.deleteById(updateExam.getId());//兼容考试要清楚考试时间和要修改为null的值 + baseMapper.insert(updateExam); + + //3、处理考试成员 + switch (examType) { + case BASE: + //常规考试 + batchUpdateExamMemberNew(updateExam, examUserList, rosterMap, userOrgMap, defaultPowerReviewer); + break; + case POSITION: + //把没有考试的调整到最新批次 + if (isNewBatch) { + updateNoExamUserToLastest(updateExam); + } + break; + default: + break; + } + } + + private void countQuestionTypeNum(FtbCultivateTestPaper paper) { + if (paper.getType().equals(2)) { + + PaperConfigReq configReq = JSONUtil.toBean(paper.getPaperConfig(), PaperConfigReq.class); + Map> questionConfig = configReq.getQuestionConfig(); + Map scoreConfig = configReq.getScoreConfig(); + + + List questionIdList = new ArrayList<>(); + List list = paperService.queryConfigQuestionList(paper.getId()); + if (CollectionUtil.isEmpty(list)) { + throw new RuntimeException("试卷没有题目"); + } + for (ExamQuestionBakVo examQuestionBakVo : list) { + questionIdList.add(examQuestionBakVo.getQuestionId()); + } + + List questionList = questionService.listByIds(questionIdList); + if (CollectionUtil.isEmpty(questionList)) { + throw new RuntimeException("试卷没有题目"); + } + //按照题库ID分组 + Map> currPaperMap = questionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestion::getClassifyId)); + + for (Map.Entry> entry : questionConfig.entrySet()) { + String questionBankId = entry.getKey(); + Map questionNumbers = entry.getValue(); + List tempQuestionList = currPaperMap.get(questionBankId); + Map analyMap = QuestionAnalysisUtil.initAnalysQuestionCount(); + QuestionAnalysisUtil.analysQuestionCount(analyMap, tempQuestionList); + + + // 遍历内层 Map + for (Map.Entry qEntry : questionNumbers.entrySet()) { + String type = qEntry.getKey(); + PaperConfigReq.QuestionNum questionNum = qEntry.getValue(); + PaperConfigReq.QuestionNum currQuestionNum = analyMap.get(type); + if (currQuestionNum == null) { + questionNum.setSimpleNum(0); + questionNum.setHardNum(0); + questionNum.setGeneralNum(0); + } else { + questionNum.setSimpleNum(currQuestionNum.getSimpleNum()); + questionNum.setGeneralNum(currQuestionNum.getGeneralNum()); + questionNum.setHardNum(currQuestionNum.getHardNum()); + } + + } + } + + paper.setPaperConfig(JSONUtil.toJsonStr(configReq)); + } + } + + private void updateNoExamUserToLastest(FtbCultivateExam updateExam) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateExamUser::getVersionBatch, updateExam.getVersionBatch()); + updateWrapper.set(FtbCultivateExamUser::getPaperId, updateExam.getPaperId()); + updateWrapper.in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode(), CourseEnums.ExamStatus.OVERDUE.getCode()); + updateWrapper.eq(FtbCultivateExamUser::getExamId, updateExam.getId()); + examUserService.update(null, updateWrapper); + } + + + /** + * @param updateExam + * @param oldExam + * @return true-需要生成新批次 false-不需要生成新批次 + */ + private Boolean fillPaperInfoAndCheckIsNewBatch(FtbCultivateExam updateExam, FtbCultivateExam oldExam, FtbCultivateTestPaper paper) { + Boolean batch = false; + updateExam.setPaperInfo(JSONUtil.toJsonStr(paper)); + if (oldExam.getNeedUpdate() == 2) { + batch = true; + } else if (!oldExam.getPaperId().equals(updateExam.getPaperId())) { + batch = true; + } else if (!oldExam.getPassType().equals(updateExam.getPassType()) || !oldExam.getPassMark().equals(updateExam.getPassMark()) + || !oldExam.getExcellentType().equals(updateExam.getExcellentType()) || !oldExam.getExcellentMark().equals(updateExam.getExcellentMark())) { + batch = true; + } + if (batch) { + //查询是否有完成的考试 如果有完成的考试就生成 没有就不生成 + List examUserList = examUserService.queryCompleteExamUserListForExamIdAndBatch(oldExam.getId(), oldExam.getVersionBatch()); + if (CollectionUtil.isEmpty(examUserList)) { + return false; + } else { + return true; + } + + } else { + return false; + } + } + + private String getReviewerAppoints(List reviewerAppointList) { + if (CollectionUtil.isEmpty(reviewerAppointList)) { + return ""; + } + List userList = new ArrayList<>(); + for (ReviewerAppointDto reviewerAppointDto : reviewerAppointList) { + if (StringUtils.isNotEmpty(reviewerAppointDto.getUserId())) { + userList.add(reviewerAppointDto.getUserId()); + } + } + return String.join(",", userList); + } + + /** + * 批量修改考试成员 + * + * @param exam 考试信息 + * @param examMemberId 前端传入的考试用户ID + */ + private void batchUpdateExamMember(FtbCultivateExam exam, String examMemberId) { + //1、查询已经存在的考试用户列表 + List exiestMemberList = queryExamMemberListForExamId(exam.getId()); + //2、根据传入的考试用户ID处理 + if (StringUtils.isEmpty(examMemberId)) { + if (CollectionUtil.isNotEmpty(exiestMemberList)) { + deleteExamUserExamId(exam.getId()); + } + return; + } + List reqMemberList = Arrays.asList(examMemberId.split(",")); + if (CollectionUtil.isEmpty(reqMemberList)) { + if (CollectionUtil.isNotEmpty(exiestMemberList)) { + deleteExamUserExamId(exam.getId()); + } + return; + } + + //需要添加的 + List addList = new ArrayList<>(); + //存在的用户集合 + Set set = exiestMemberList.stream().map(FtbCultivateExamUser::getUserId).collect(Collectors.toSet()); + for (String memberId : reqMemberList) { + if (!set.contains(memberId)) { + addList.add(memberId); + } + } + if (CollectionUtil.isNotEmpty(addList)) { + batchInsertExamMemberForMemberId(exam, addList); + } + + //需要删除的 + List removeList = new ArrayList<>(); + Set reqSet = reqMemberList.stream().collect(Collectors.toSet()); + for (FtbCultivateExamUser examUser : exiestMemberList) { + if (!reqSet.contains(examUser.getUserId())) { + removeList.add(examUser.getId()); + } + } + if (CollectionUtil.isNotEmpty(removeList)) { + deleteExamUserForIds(removeList); + } + //去存在的 + if (CollectionUtil.isNotEmpty(set) && CollectionUtil.isNotEmpty(reqSet)) { + Set intersection = getIntersection(reqSet, set); + if (CollectionUtil.isNotEmpty(intersection)) { + + List stringList = intersection.stream().collect(Collectors.toList()); + + deleteExamUserForUserIds(exam.getId(), stringList); + batchInsertExamMemberForMemberId(exam, stringList); + } + } + } + + private void batchUpdateExamMemberNew(FtbCultivateExam exam, List reqMemberList, Map rosterMap, Map> userOrgMap, Map defaultPowerReviewer) { + //1、查询已经存在的考试用户列表 + List exiestMemberList = queryExamMemberListForExamId(exam.getId()); + + if (CollectionUtil.isEmpty(reqMemberList)) { + if (CollectionUtil.isNotEmpty(exiestMemberList)) { + deleteExamUserExamId(exam.getId()); + } + return; + } + + //需要添加的 + List addList = new ArrayList<>(); + //存在的用户集合 + Set set = exiestMemberList.stream().map(FtbCultivateExamUser::getUserId).collect(Collectors.toSet()); + for (String memberId : reqMemberList) { + if (!set.contains(memberId)) { + addList.add(memberId); + } + } + if (CollectionUtil.isNotEmpty(addList)) { + batchInsertExamMemberForMemberIdNew(exam, addList, rosterMap, userOrgMap, defaultPowerReviewer); + } + + //需要删除的 + List removeList = new ArrayList<>(); + Set reqSet = reqMemberList.stream().collect(Collectors.toSet()); + for (FtbCultivateExamUser examUser : exiestMemberList) { + if (!reqSet.contains(examUser.getUserId())) { + removeList.add(examUser.getId()); + } + } + if (CollectionUtil.isNotEmpty(removeList)) { + deleteExamUserForIds(removeList); + } + //去存在的 + if (CollectionUtil.isNotEmpty(set) && CollectionUtil.isNotEmpty(reqSet)) { + Set intersection = getIntersection(reqSet, set); + if (CollectionUtil.isNotEmpty(intersection)) { + + List stringList = intersection.stream().collect(Collectors.toList()); + + deleteExamUserForUserIds(exam.getId(), stringList); + batchInsertExamMemberForMemberIdNew(exam, stringList, rosterMap, userOrgMap, defaultPowerReviewer); + } + } + } + + public static Set getIntersection(Set set1, Set set2) { + Set intersectionSet = new HashSet<>(); + + for (String element : set1) { + if (set2.contains(element)) { + intersectionSet.add(element); + } + } + + return intersectionSet; + } + + /** + * 根据考试ID删除考试用户 + * + * @param examId 考试ID + */ + private void deleteExamUserExamId(String examId) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()).set(FtbCultivateExamUser::getDeleteTime, new Date()).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()).in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode(), CourseEnums.ExamStatus.OVERDUE.getCode()).eq(FtbCultivateExamUser::getExamId, examId); + examUserService.update(wrapper); + } + + /** + * 根据考试ID删除考试用户 + * + * @param examId 考试ID + */ + private void deleteAllExamUserExamId(String examId) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda() + .set(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .set(FtbCultivateExamUser::getDeleteTime, new Date()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .eq(FtbCultivateExamUser::getExamId, examId); + examUserService.update(wrapper); + } + + + /** + * 删除考试用户 + * + * @param removeList 需要删除的考试用户表中的ID + */ + + private void deleteExamUserForIds(List removeList) { + examUserService.update(Wrappers.lambdaUpdate().set(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()).set(FtbCultivateExamUser::getDeleteTime, new Date()).in(FtbCultivateExamUser::getId, removeList)); + } + + /** + * 删除考试用户 + * + * @param examId 考试ID + * @param userIds 需要删除的考试用户表中的ID + */ + + private void deleteExamUserForUserIds(String examId, List userIds) { + examUserService.update(Wrappers.lambdaUpdate() + .set(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .set(FtbCultivateExamUser::getDeleteTime, new Date()) + .eq(FtbCultivateExamUser::getExamId, examId) + .in(FtbCultivateExamUser::getUserId, userIds)); + } + + /** + * 查询考试成员 + * + * @param examId 考试ID + * @return + */ + + private List queryExamMemberListForExamId(String examId) { + QueryWrapper wrap = new QueryWrapper<>(); + wrap.lambda().eq(FtbCultivateExamUser::getExamId, examId).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return examUserService.list(wrap); + } + + /** + * 删除考试 + * + * @param examId 考试ID + */ + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String examId) { + //1、检测并检查考试 + FtbCultivateExam exam = queryExamInfo(examId); + if (CourseEnums.ExamType.POSITION.getCode().equals(exam.getExamType())) { + // 岗位学习考试和课程考试删除 + LambdaUpdateWrapper ftbCultivatePositionCourseExamLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + ftbCultivatePositionCourseExamLambdaUpdateWrapper.eq(FtbCultivatePositionCourseExam::getExamId, examId); + ftbCultivatePositionCourseExamLambdaUpdateWrapper.set(FtbCultivatePositionCourseExam::getEnabledMark, 1); + ftbCultivatePositionCourseExamMapper.update(new FtbCultivatePositionCourseExam(), ftbCultivatePositionCourseExamLambdaUpdateWrapper); + + LambdaUpdateWrapper ftbCultivatePositionExamLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + ftbCultivatePositionExamLambdaUpdateWrapper.eq(FtbCultivatePositionExam::getExamId, examId); + ftbCultivatePositionExamLambdaUpdateWrapper.set(FtbCultivatePositionExam::getEnabledMark, 1); + ftbCultivatePositionExamMapper.update(new FtbCultivatePositionExam(), ftbCultivatePositionExamLambdaUpdateWrapper); + } + //2、删除考试 + FtbCultivateExam updateExam = new FtbCultivateExam(); + updateExam.setId(exam.getId()); + updateExam.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + updateExam.setDeleteTime(new Date()); + baseMapper.updateById(updateExam); + if (CourseEnums.ExamType.POSITION.getCode().equals(exam.getExamType())) { + //岗位考试 全部删除 + deleteAllExamUserExamId(exam.getId()); + } else { + //删除考试用户 + deleteExamUserExamId(exam.getId()); + } + + } + + /** + * 检查考试是否可以删除 + * + * @param examId 考试ID + * @return + */ + @Override + public CanDeleteMsg checkExamCanDelete(String examId) { + FtbCultivateExam exam = queryExamInfo(examId); + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + //常规学习考试 + if (CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode().equals(exam.getStatus())) { + return new CanDeleteMsg(false, "考试正在进行中,请谨慎操作"); + } + if (CourseEnums.ExamTimeStatus.DONE.getCode().equals(exam.getStatus())) { + return new CanDeleteMsg(false, "考试已经完成,请谨慎操作"); + } + } else { + //检测考试是否与岗位学习有关联 + LambdaQueryWrapper positionExamWrapper = new LambdaQueryWrapper().eq(FtbCultivatePositionExam::getExamId, examId).eq(FtbCultivatePositionExam::getEnabledMark, 0); + List positionExamList = positionExamService.list(positionExamWrapper); + if (CollectionUtil.isNotEmpty(positionExamList)) { + return new CanDeleteMsg(false, "有岗位学习关联该考试,请谨慎操作"); + } + //检测考试是否与课程有关联 + LambdaQueryWrapper courseExamWraper = new LambdaQueryWrapper().eq(FtbCultivatePositionCourseExam::getExamId, examId).eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + List courseExamList = positionCourseExamService.list(courseExamWraper); + if (CollectionUtil.isNotEmpty(courseExamList)) { + return new CanDeleteMsg(false, "有岗位学习 课程关联该考试"); + } + } + return new CanDeleteMsg(true, "可以删除"); + } + + /** + * 查询所有的岗位和考试 + * + * @return + */ + @Override + public List queryPostAndRank() { + return userApiV2Util.listAllPositionAndGradesByPositionName(null); + + } + + /** + * 查询考试详情[考试 和 试卷] + * + * @param examId + * @return + */ + @Override + public ExamAndPaperDetailVo queryExamDetail(String examId) { + //1、查询考试信息 + FtbCultivateExam exam = queryExamInfo(examId); + ExamAndPaperDetailVo vo = BeanUtil.copyProperties(exam, ExamAndPaperDetailVo.class); + //1.1 补充岗位职能信息 + if (StringUtils.isNotEmpty(exam.getPostRankConf())) { + vo.setPostAndPositionList(JSONUtil.toList(JSONUtil.parseArray(exam.getPostRankConf()), PostAndPosition.class)); + } + //2、查看考试中试卷信息 + FtbCultivateTestPaper paper = queryPaper(vo.getPaperId()); + if (null != paper) { + PaperVo paperVo = BeanUtil.copyProperties(paper, PaperVo.class); + vo.setPaperVo(paperVo); + //3、统计试卷的题目数量【各种题型的数量】 + paperVo.setQuestionAnalys(analysPaperQuestionCount(vo.getPaperId())); + } + + return vo; + } + + /** + * APP开始考试,查询考试 + * + * @param examId 考试ID + * @return + */ + @Override + public ExamAppVo examInfo(String examId) { + //1、查询和检测试卷是否存在 + FtbCultivateExam exam = queryExamInfo(examId); + ExamAppVo vo = new ExamAppVo(); + FtbCultivateTestPaper paper = queryPaperNocheckDelete(exam.getPaperId()); + if (exam.getStatus() == 3) { + vo.setIsNullify(1); + } + vo.setPaperName(paper.getName()); + vo.setId(exam.getId()); + vo.setExamId(exam.getExamId()); + vo.setExamTime(exam.getExamTime()); + vo.setStartTime(exam.getStartTime()); + vo.setEndTime(exam.getEndTime()); + vo.setPaperId(exam.getPaperId()); + vo.setExamName(exam.getExamName()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setStatus(exam.getStatus()); + vo.setDescription(exam.getDescription()); + vo.setExamType(exam.getExamType()); + vo.setTotleScore(paper.getTotalScore()); + //合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(exam.getPassType())) { + vo.setPassMark(exam.getPassMark()); + } else { + vo.setPassMark(paper.getTotalScore() * exam.getPassMark() / 100); + } + //优秀分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(exam.getExcellentType())) { + vo.setExcellentMark(exam.getExcellentMark()); + } else { + vo.setExcellentMark(paper.getTotalScore() * exam.getExcellentMark() / 100); + } + //常规考试 根据时间判断状态 + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + if (exam.getStartTime() != null && exam.getEndTime() != null) { + Date currentTime = new Date(); + if (currentTime.before(exam.getStartTime())) { + //考试还未开始 + vo.setStatus(CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()); + } else if (currentTime.after(exam.getEndTime())) { + //考试已经结束 + vo.setStatus(CourseEnums.ExamTimeStatus.DONE.getCode()); + } else { + //考试正在进行中 + vo.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } else { + vo.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } + //查询试卷题目统计数据 + vo.setQuestionNumMap(analysPaperQuestionCount(exam.getPaperId())); + + //考试配置 + vo.setMakeUpCount(exam.getMakeUpCount()); + vo.setWaterMark(exam.getWaterMark()); + vo.setCutScreen(exam.getCutScreen()); + vo.setMaxCutScreenCount(exam.getMaxCutScreenCount()); + vo.setScreenshot(exam.getScreenshot()); + vo.setCopyAndPaste(exam.getCopyAndPaste()); + vo.setOpenOverTime(exam.getOpenOverTime()); + vo.setOverTime(exam.getOverTime()); + vo.setAllowSelectDetail(exam.getAllowSelectDetail()); + vo.setAllowChangeLastQuestion(exam.getAllowChangeLastQuestion()); + vo.setNeedUpdate(exam.getNeedUpdate()); + return vo; + } + + @Override + public Map analysPaperQuestionCount(String paperId) { + //1、查询试卷中的题目 + List questionVoList = paperService.queryQuestionListForPaperId(paperId); + //2、初始化返回值 + Map analyMap = QuestionAnalysisUtil.initAnalysQuestionCount(); + if (CollectionUtil.isNotEmpty(questionVoList)) { + QuestionAnalysisUtil.analysQuestionCount(analyMap, questionVoList); + } + return analyMap; + } + + /** + * 查询并检查考试[是否存在或者是否删除] + * + * @param examId 考试ID + */ + @Override + public FtbCultivateExam queryExamInfo(String examId) { + FtbCultivateExam exam = getById(examId); + if (exam == null) { + throw new RuntimeException("考试不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(exam.getEnabledMark())) { + throw new RuntimeException("考试已删除"); + } + return exam; + } + + @Override + public FtbCultivateExam queryExamAndPositionRelation(String examId, String positionId) { + FtbCultivateExam exam = examService.queryExamInfo(examId); + if (exam.getExamType() == 1) {//常规考试 + return null; + } + if (StringUtils.isEmpty(exam.getPostId())) { + return null; + } + List positionIdList = Arrays.asList(exam.getPostId().split(",")); + if (positionIdList.contains(positionId)) { + return exam; + } + return null; + } + + @Override + @Transactional + public Boolean bindExamAndPositionRelation(String examId, String positionId) { + FtbCultivateExam exam = examService.queryExamInfo(examId); + List positionIdList = new ArrayList<>(); + if (StringUtils.isNotEmpty(exam.getPostId())) { + positionIdList.addAll(Arrays.asList(exam.getPostId().split(","))); + } + if (positionIdList.contains(positionId)) { + return true; + } + positionIdList.add(positionId); + exam.setPostId(StringUtils.join(positionIdList, ",")); + List positionEntityList = userExamUtil.getPostInfoList(positionIdList); + List postAndPositionList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(positionEntityList)) { + for (PositionVO positionEntity : positionEntityList) { + PostAndPosition postAndPosition = new PostAndPosition(); + postAndPosition.setId(positionEntity.getId()); + postAndPosition.setEnCode(positionEntity.getEnCode()); + postAndPosition.setPostName(positionEntity.getFullName()); + postAndPositionList.add(postAndPosition); + } + } + exam.setPostRankConf(JSONUtil.toJsonStr(postAndPositionList)); + examService.updateById(exam); + return true; + } + + /** + * 查询审核人列表 + * + * @return + */ + @Override + public List queryReviewerUserList() { + List list = userApi.getList(); + if (CollectionUtil.isEmpty(list)) { + return Collections.EMPTY_LIST; + } + //转换 + List baseUserList = new ArrayList<>(); + for (UserEntity userEntity : list) { + ExamBaseUser examBaseUser = new ExamBaseUser(); + examBaseUser.setUserName(userEntity.getRealName()); + examBaseUser.setUserId(userEntity.getId()); + examBaseUser.setHeadLogo(userEntity.getHeadIcon()); + baseUserList.add(examBaseUser); + } + return baseUserList; + } + + /** + * 修改考试状态 + */ + @Override + public void changeExamStatus() { + //修改未开始 + baseMapper.update(null, new LambdaUpdateWrapper().set(FtbCultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()).ge(FtbCultivateExam::getStartTime, new Date()).eq(FtbCultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode()).ne(FtbCultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + //修改进行中 + baseMapper.update(null, new LambdaUpdateWrapper().set(FtbCultivateExam::getStatus, CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()).le(FtbCultivateExam::getStartTime, new Date()).ge(FtbCultivateExam::getEndTime, new Date()).eq(FtbCultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()).eq(FtbCultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode()).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + //修改结束 + baseMapper.update(null, new LambdaUpdateWrapper().set(FtbCultivateExam::getNeedUpdate, ExamUpdateStatus.NO_UPDATE.getCode()).set(FtbCultivateExam::getStatus, CourseEnums.ExamTimeStatus.DONE.getCode()).le(FtbCultivateExam::getEndTime, new Date()).eq(FtbCultivateExam::getStatus, CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()).eq(FtbCultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode()).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + } + + + /** + * 查询考试信息 + * + * @param examIds 考试id列表 + * @return + */ + private List queryExamInfoForExamIds(List examIds) { + return baseMapper.selectBatchIds(examIds); + } + + /** + * 查询考试题目列表 + * + * @param examId 考试id + * @return + */ + @Override + public List queryExamQuestionList(String examId) { + + //1、查询考试 + FtbCultivateExam exam = queryExamInfo(examId); + //2、查询试卷中的题目 + List questionList = paperService.queryQuestionListForPaperId(exam.getPaperId()); + if (CollectionUtil.isEmpty(questionList)) { + return Collections.EMPTY_LIST; + } + List userQuestionVoList = new ArrayList<>(); + List paperQuestionList = paperService.queryPaperQuestionRelationList(exam.getPaperId()); + Map questionMap = paperQuestionList.stream().collect(Collectors.toMap(FtbCultivateTestPaperQuestion::getQuestionId, Function.identity())); + + // 转换题目列表 + for (FtbCultivateQuestion ftbCultivateQuestion : questionList) { + UserQuestionVo userQuestionVo = BeanUtil.copyProperties(ftbCultivateQuestion, UserQuestionVo.class); + userQuestionVo.setQuestionId(ftbCultivateQuestion.getId()); + FtbCultivateTestPaperQuestion paperQuestion = questionMap.get(ftbCultivateQuestion.getId()); + userQuestionVo.setQuestionScore(paperQuestion.getScore()); + userQuestionVoList.add(userQuestionVo); + } + //3、获取题目选项 + List questionIds = questionList.stream().map(FtbCultivateQuestion::getId).collect(Collectors.toList()); + List questionOptionList = queryOptionListForQuestionIds(questionIds); + if (CollectionUtil.isEmpty(questionOptionList)) { + return userQuestionVoList; + } + + Map> questionOptionMap = questionOptionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestionOption::getQuestionId)); + //填充题目选项 + for (UserQuestionVo userQuestionVo : userQuestionVoList) { + List questionOptionVoList = questionOptionMap.get(userQuestionVo.getId()); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + userQuestionVo.setQuestionOptionVoList(BeanUtil.copyToList(questionOptionVoList, QuestionOptionVo.class)); + } + } + + + return userQuestionVoList; + } + + @Override + public List queryExamQuestionListByUserExamId(String userExamId) { + FtbCultivateExamUser examUser = examUserService.getById(userExamId); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + //1、查询考试 + FtbCultivateExam currExam = queryExamInfo(examUser.getExamId()); + if (currExam.getStatus().equals(3)) { + throw new RuntimeException("考试已经作废"); + } + FtbCultivateExam exam = userExamUtil.checkAndUpdateExam(currExam, examUser.getVersionBatch()); + + //2、查询试卷中的题目 + FtbCultivateTestPaper testPaper = null; + if (StringUtils.isNotEmpty(exam.getPaperInfo())) { + testPaper = JSONUtil.toBean(exam.getPaperInfo(), FtbCultivateTestPaper.class); + } else { + testPaper = paperService.getById(exam.getPaperId()); + } + if (null == testPaper) { + throw new RuntimeException("试卷信息不存在"); + } + Boolean isNewQuestion = checkIsGeneraNewQuestion(examUser, testPaper); + List questionList = new ArrayList<>(); + List selectPaperQuestionList = new ArrayList<>(); + if (isNewQuestion == false) { + selectPaperQuestionList = HistoryPaperUtils.convertQuestionList(exam.getCurrQuestionList()); + questionList = userExamUtil.queryHalfFullQuestionList(selectPaperQuestionList); + } else { + //生成随机试卷 + GeneralPaperQuestionVo newQuestionVo = userExamUtil.randomQuestion(exam); + if (newQuestionVo.getSuccess()) { + selectPaperQuestionList = newQuestionVo.getRandomList(); + questionList = newQuestionVo.getQuestionList(); + } else { + selectPaperQuestionList = HistoryPaperUtils.convertQuestionList(exam.getCurrQuestionList()); + questionList = userExamUtil.queryHalfFullQuestionList(selectPaperQuestionList); + } + } + if (CollectionUtil.isEmpty(questionList)) { + return Collections.EMPTY_LIST; + } + List userQuestionVoList = new ArrayList<>(); + + Map questionMap = selectPaperQuestionList.stream().collect(Collectors.toMap(ExamQuestionBakVo::getQuestionId, Function.identity())); + + // 转换题目列表 + for (FtbCultivateQuestion ftbCultivateQuestion : questionList) { + UserQuestionVo userQuestionVo = BeanUtil.copyProperties(ftbCultivateQuestion, UserQuestionVo.class); + userQuestionVo.setQuestionId(ftbCultivateQuestion.getId()); + ExamQuestionBakVo paperQuestion = questionMap.get(ftbCultivateQuestion.getId()); + userQuestionVo.setQuestionScore(paperQuestion.getScore()); + userQuestionVoList.add(userQuestionVo); + } + //3、获取题目选项 + List questionIds = questionList.stream().map(FtbCultivateQuestion::getId).collect(Collectors.toList()); + List questionOptionList = queryOptionListForQuestionIds(questionIds); + if (CollectionUtil.isEmpty(questionOptionList)) { + return userQuestionVoList; + } + + Map> questionOptionMap = questionOptionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestionOption::getQuestionId)); + //填充题目选项 + for (UserQuestionVo userQuestionVo : userQuestionVoList) { + List questionOptionVoList = questionOptionMap.get(userQuestionVo.getId()); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + userQuestionVo.setQuestionOptionVoList(BeanUtil.copyToList(questionOptionVoList, QuestionOptionVo.class)); + } + } + if (ExamQuestionShuffleEnum.SHUFFLE.getCode().equals(exam.getExamQuestionRandom())) { + Collections.shuffle(userQuestionVoList); + } + return userQuestionVoList; + } + + + /** + * 查询当前考试试卷信息 + * + * @param examId 考试ID + * @return + */ + @Override + public PaperDetailVo queryCurrExamPaperInfo(String examId) { + PaperDetailVo paperDetailVo = new PaperDetailVo(); + //1、查询考试 + FtbCultivateExam exam = queryExamInfo(examId); + + //2、查询试卷中的题目 + FtbCultivateTestPaper testPaper = null; + if (StringUtils.isNotEmpty(exam.getPaperInfo())) { + testPaper = JSONUtil.toBean(exam.getPaperInfo(), FtbCultivateTestPaper.class); + } else { + testPaper = paperService.getById(exam.getPaperId()); + } + + if (null == testPaper) { + throw new RuntimeException("试卷信息不存在"); + } + paperDetailVo.setId(testPaper.getId()); + paperDetailVo.setName(testPaper.getName()); + paperDetailVo.setQuestionNumber(testPaper.getQuestionNumber()); + paperDetailVo.setTotalScore(testPaper.getTotalScore()); + paperDetailVo.setType(testPaper.getType()); + paperDetailVo.setPaperType(testPaper.getPaperType()); + paperDetailVo.setPaperId(testPaper.getPaperId()); + List paperQuestionRelationList = new ArrayList<>(); + if (StringUtils.isNotEmpty(exam.getCurrQuestionList())) { + List selectPaperQuestionList = HistoryPaperUtils.convertQuestionList(exam.getCurrQuestionList()); + if (CollectionUtil.isNotEmpty(selectPaperQuestionList)) { + paperQuestionRelationList = BeanUtil.copyToList(selectPaperQuestionList, FtbCultivateTestPaperQuestion.class); + } + } else { + paperQuestionRelationList = paperService.queryPaperQuestionRelationList(exam.getPaperId()); + + } + if (CollectionUtil.isEmpty(paperQuestionRelationList)) { + return paperDetailVo; + } + + Map testQuestionMap = paperQuestionRelationList.stream().collect(Collectors.toMap(FtbCultivateTestPaperQuestion::getQuestionId, Function.identity())); + + //3、根据关联的试卷题目ID,查询题目列表 + if (CollectionUtil.isNotEmpty(paperQuestionRelationList)) { + List questionIdList = paperQuestionRelationList.stream().map(FtbCultivateTestPaperQuestion::getQuestionId).collect(Collectors.toList()); + List questionList = questionService.listByIds(questionIdList); + if (CollectionUtil.isNotEmpty(questionList)) { + List questionVoList = BeanUtil.copyToList(questionList, AppQuestionVo.class); + paperService.fillQuestionOption(questionVoList); + Map> map = new HashMap<>(); + for (AppQuestionVo questionVo : questionVoList) { + FtbCultivateTestPaperQuestion paperQuestion = testQuestionMap.get(questionVo.getId()); + questionVo.setQuestionScore(paperQuestion.getScore()); + String type = String.valueOf(questionVo.getType()); + List list = map.get(type); + if (CollectionUtil.isEmpty(list)) { + list = new ArrayList<>(); + } + list.add(questionVo); + map.put(type, list); + } + paperDetailVo.setQuestionMap(map); + } + } + return paperDetailVo; + } + + /** + * 是否生成新题目 + * + * @param examUser + * @param testPaper + * @return false-不需要 true-需要 + */ + private Boolean checkIsGeneraNewQuestion(FtbCultivateExamUser examUser, FtbCultivateTestPaper testPaper) { + if (examUser.getUserExamCount() <= 1) { + return false; + } + if (testPaper.getType().equals(1)) { + return false; + } + return true; + } + + /** + * 根据题目ID 批量查询题目的选项 + * + * @param questionIds 题目id集合 + * @return + */ + private List queryOptionListForQuestionIds(List questionIds) { + return questionOptionService.queryOptionListByQuestionIds(questionIds); + } + + /** + * 查询批阅人列表 + * + * @param userId 用户id + * @return + */ + @Override + public List queryExamReviewList(String userId) { + String powerUserIds = personnelPerUtils.obtainTrainingApproverPermissions(userId); + if (StringUtils.isEmpty(powerUserIds)) { + return new ArrayList<>(); + } + return Arrays.asList(powerUserIds.split(",")); + } + + /** + * 查询所有的岗位 + * + * @return + */ + @Override + public List queryAllPostion() { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbCultivatePosition::getEnabledMark, 0); + List list = ftbCultivatePositionMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + //获取id list + List postionList = list.stream().map(FtbCultivatePosition::getPostId).collect(Collectors.toList()); +// List entityList = positionApi.getPositionName(postionList); + List entityList = userApiV2Util.listPositionDetailInfoByIds(postionList, null); + if (CollectionUtil.isEmpty(entityList)) { + return new ArrayList<>(); + } + return entityList; + } + + /** + * 根据考试id查询考试相关的历史试卷 + * + * @param req + * @return + */ + @Override + public List historyPaperList(WebQueryExamReadOverReq req) { + List list = new ArrayList<>(); + FtbCultivateExam exam = examService.queryExamInfo(req.getExamId()); + list.add(userExamUtil.convertToWebReadOverExamAndPaperDetailVo(exam)); + + + List historyList = ftbCultivateExamHistoryPaperService.queryByExamId(req.getExamId()); + + List historyVoList = userExamUtil.convertWebReadOverExamAndPaperDetailVoForHistory(historyList); + if (CollectionUtil.isNotEmpty(historyVoList)) { + list.addAll(historyVoList); + } + + //查询待批阅人数量 + List waitReadOverNumVoList = examUserService.queryWaitReadOverNum(req.getExamId()); + Map numMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(waitReadOverNumVoList)) { + for (WaitReadOverNumVo waitReadOverNumVo : waitReadOverNumVoList) { + numMap.put(waitReadOverNumVo.getVersionBatch(), waitReadOverNumVo); + } + } + for (WebReadOverExamAndPaperDetailVo vo : list) { + WaitReadOverNumVo waitReadOverNumVo = numMap.get(vo.getVersionBatch()); + if (null != waitReadOverNumVo) { + vo.setWaitReadOverNum(waitReadOverNumVo.getNum()); + } + } + return list; + } + + /** + * 查询考试历史试卷待考试的数量 + * + * @param req + * @return + */ + @Override + public Map waitNumberForBatch(WebQueryExamReadOverReq req) { + + List waitReadOverNumVoList = examUserService.queryWaitReadOverNum(req.getExamId()); + Map numMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(waitReadOverNumVoList)) { + for (WaitReadOverNumVo waitReadOverNumVo : waitReadOverNumVoList) { + numMap.put(waitReadOverNumVo.getVersionBatch(), waitReadOverNumVo.getNum()); + } + } + + return numMap; + } + + /** + * 考试作废-常规考试 + * + * @param examId 考试ID + * @return + */ + @Override + @Transactional + public void nullify(String examId) { + FtbCultivateExam oldExam = queryExamInfo(examId); + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(oldExam.getExamType()); + //1、根据考试ID查询考试基本信息 + if (CourseEnums.ExamType.BASE == examType) { + if (CourseEnums.ExamTimeStatus.NOT_STARTED.getCode().equals(oldExam.getStatus())) { + throw new RuntimeException("考试未开始不能作废"); + } + + if (CourseEnums.ExamTimeStatus.NULLIFY.getCode().equals(oldExam.getStatus())) { + throw new RuntimeException("考试已经作废,不能重复操作"); + } + } else { + throw new RuntimeException("岗位学习考试不能作废"); + } + oldExam.setStatus(CourseEnums.ExamTimeStatus.NULLIFY.getCode()); + oldExam.setNeedUpdate(ExamUpdateStatus.NO_UPDATE.getCode()); + baseMapper.updateById(oldExam); + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getIsJoinCount, ExamUserStatisticsEnums.NOT_PARTICIPATE.getCode()) + .in(FtbCultivateExamUser::getStatus, 3, 4, 5) + .eq(FtbCultivateExamUser::getExamId, examId); + examUserService.update(updateWrapper); + + LambdaUpdateWrapper updateWrapper1 = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getAppShow, 1) + .set(FtbCultivateExamUser::getIsJoinCount, ExamUserStatisticsEnums.NOT_PARTICIPATE.getCode()) + .in(FtbCultivateExamUser::getStatus, 0, 1, 2) + .eq(FtbCultivateExamUser::getExamId, examId); + examUserService.update(updateWrapper1); + + //终止未开始和进行中 + List list = ftbCultivateLearnTaskExamMapper.queryTaskListForExam(examId); + if (CollUtil.isNotEmpty(list)) { + for (FtbCultivateLearnTask ftbCultivateLearnTask : list) { + ftbCultivateLearnTaskInfoContentService.abort(ftbCultivateLearnTask.getId()); + } + } + //未发布的 + List listNoPublish = ftbCultivateLearnTaskExamMapper.queryTaskListForExamNoPublish(examId); + if (CollUtil.isNotEmpty(listNoPublish)) { + for (FtbCultivateLearnTask noPublish : listNoPublish) { + //删除任务管理的考试 + QueryWrapper taskExamUpdateWrapper = new QueryWrapper<>(); + taskExamUpdateWrapper.lambda() + .eq(FtbCultivateLearnTaskExam::getEnableMark, 0) + .eq(FtbCultivateLearnTaskExam::getTaskId, noPublish.getId()) + .eq(FtbCultivateLearnTaskExam::getExamId, examId); + ftbCultivateLearnTaskExamMapper.delete(taskExamUpdateWrapper); + + LambdaUpdateWrapper taskIdentificationLambdaUpdateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateLearnTaskIdentification::getIdentificationRule, 0) + .eq(FtbCultivateLearnTaskIdentification::getTaskId, noPublish.getId()); + learnTaskIdentificationMapper.update(null, taskIdentificationLambdaUpdateWrapper); + } + + } + + + } + + /** + * 根据考试id查询任务列表 + * + * @param examId 考试ID + * @return + */ + @Override + public List checkNullify(String examId) { + List list = ftbCultivateLearnTaskExamMapper.queryTaskListForExamAll(examId); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + /** + * 根据岗位查询是否考试完成才能够鉴定的标志 + * + * @param positionId 岗位 id + * @return 岗位考试合格后才能进行鉴定, 0否 1是 + */ + @Override + public Integer queryExamAndIdentifyConfig(String positionId) { + Integer integer = baseMapper.queryExamAndIdentifyConfig(positionId); + if (null == integer) { + integer = 0; + } + return integer; + } + + + /** + * 初始化考试数据(兼容旧数据) + * + * @return + */ + @Override + @Transactional + public List initOldExamData() { + + List examList = baseMapper.selectList(Wrappers.lambdaQuery(FtbCultivateExam.class).eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + if (CollectionUtil.isEmpty(examList)) { + return Collections.emptyList(); + } + List retList = new ArrayList<>(); + for (FtbCultivateExam exam : examList) { + if (StringUtils.isNotEmpty(exam.getCurrQuestionList())) { + continue; + } + FtbCultivateTestPaper paper = paperService.getById(exam.getPaperId()); + if (paper == null) { + continue; + } + //修改考试 + FtbCultivateExam updateExam = new FtbCultivateExam(); + updateExam.setId(exam.getId()); + updateExam.setPaperId(exam.getPaperId()); + try { + countQuestionTypeNum(paper); + } catch (Exception e) { + log.error("修改考试数据失败", e); + } + updateExam.setPaperInfo(JSONUtil.toJsonStr(paper)); + fillExamPaperQuestionOld(updateExam); + baseMapper.updateById(updateExam); + retList.add(exam.getId()); + } + return retList; + + } + + /** + * 旧数据清洗,删除已经删除的考试 的相关考试记录 + */ + @Override + public void deleteExamUserForDeleteExam() { + + List examList = baseMapper.selectList(Wrappers.lambdaQuery(FtbCultivateExam.class).eq(FtbCultivateExam::getExamType, CourseEnums.ExamType.POSITION.getCode()) + .eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode())); + if (CollectionUtil.isEmpty(examList)) { + return; + } + for (FtbCultivateExam exam : examList) { + //岗位考试 全部删除 + deleteAllExamUserExamId(exam.getId()); + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserDetailServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserDetailServiceImpl.java new file mode 100644 index 0000000..932a5ed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserDetailServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateExamUserDetailMapper; +import jnpf.cultivate.service.FtbCultivateExamUserDetailService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateExamUserDetailServiceImpl extends ServiceImpl implements FtbCultivateExamUserDetailService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserServiceImpl.java new file mode 100644 index 0000000..48440b6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateExamUserServiceImpl.java @@ -0,0 +1,3893 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.cultivate.mapper.FtbCultivateExamFrequncyLogMapper; +import jnpf.cultivate.mapper.FtbCultivateExamUserMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseExamMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.HistoryPaperUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.utils.UserExamUtil; +import jnpf.cultivate.v2.util.CultivateMqSendUtil; +import jnpf.enums.cultivate.ExamRetakeFrequencyEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.statistics.*; +import jnpf.model.cultivate.po.exam.*; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.req.exam.*; +import jnpf.model.cultivate.resp.*; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionExamPersonVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionExamVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.ExamConfigEnums; +import jnpf.model.enums.ExamUserStatisticsEnums; +import jnpf.model.personnels.dto.authoritys.PermissionsCacheDTO; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsPermissions; +import jnpf.permission.OrganizeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.RoleApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.RoleEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.util.ConstantUtil; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbCultivateExamUserServiceImpl extends ServiceImpl implements FtbCultivateExamUserService { + private static final List EMPTY_USER_LIST = List.of("-1"); + /** + * 用户服务 + */ + @Autowired + private UserApi userApi; + + /** + * 角色服务 + */ + @Autowired + private RoleApi roleApi; + + /** + * 岗位服务 + */ + @Autowired + private PositionApi positionApi; + + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private PersonnelPerUtils personnelPerUtils; + + /** + * 考试详情服务 + */ + @Autowired + private FtbCultivateExamUserDetailService userDetailService; + /** + * 考试服务 + */ + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private CultivateExamSettingService cultivateExamSettingService; + + /** + * 考试服务(试卷) + */ + @Autowired + private FtbCultivateExamHistoryPaperService examHistoryPaperService; + + /** + * 试卷服务 + */ + @Autowired + private FtbCultivateTestPaperService paperService; + + /** + * 用户考试服务 + */ + @Autowired + private FtbCultivateExamUserMapper examUserMapper; + + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + + @Autowired + private UserExamUtil userExamUtil; + + @Autowired + private FtbCultivatePromotionMemberService promotionMemberService; + + @Autowired + private FtbCultivateExamFrequncyLogMapper ftbCultivateExamFrequncyLogMapper; + + @Autowired + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + + + @Autowired + private CultivateMqSendUtil cultivateMqSendUtil; + + /** + * 获取分页列表 + * + * @param req + * @return + */ + @Override + public PageInfo getPageList(QueryExamUserReq req) { + String loginUserId = UserProvider.getLoginUserId(); + //构建分页 + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + //构建查询 查询条件是 匹配 考试名称 或者试卷ID + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getExamId, req.getExamId()) + .eq(req.getStatus() != null, FtbCultivateExamUser::getStatus, req.getStatus()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .eq(FtbCultivateExamUser::getVersionBatch, req.getVersionBatch()) + .orderByDesc(FtbCultivateExamUser::getCreatorTime); + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(records)) { + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + FtbCultivateExam exam = examService.getById(req.getExamId()); + //查询我的角色 + for (FtbCultivateExamUser entity : records) { + ExamListUserVo vo = new ExamListUserVo(); + vo.setId(entity.getId()); + vo.setScore(entity.getScore()); + vo.setExamid(entity.getExamId()); + vo.setDuration(entity.getDuration()); + vo.setUserId(entity.getUserId()); + vo.setStatus(entity.getStatus()); + vo.setUserStartTime(entity.getUserStartTime()); + vo.setFinishtime(entity.getFinishTime()); + if (CourseEnums.ExamStatus.WAIT_CHECK.getCode().equals(vo.getStatus())) { + if (innerPowerUserVO.getCode().equals(1)) { + if (innerPowerUserVO.getUserIds().contains(vo.getUserId())) { + vo.setCanReviewer(true); + } else { + vo.setCanReviewer(false); + } + } else if (innerPowerUserVO.getCode().equals(0)) { + vo.setCanReviewer(true); + } else if (innerPowerUserVO.getCode().equals(2)) { + vo.setCanReviewer(false); + } + } + vo.setScoreRatio(entity.getScoreRatio()); + if (CourseEnums.ExamStatus.PASS.getCode().equals(vo.getStatus()) || CourseEnums.ExamStatus.NO_PASS.getCode().equals(vo.getStatus()) || CourseEnums.ExamStatus.VERY_PASS.getCode().equals(vo.getStatus())) { + if (entity.getScoreRatio() == null) { + if (exam.getCurrTotalScore() != null && exam.getCurrTotalScore() > 0) { + vo.setScoreRatio(new BigDecimal(entity.getScore()).divide(new BigDecimal(exam.getCurrTotalScore()), 2, BigDecimal.ROUND_HALF_UP)); + } + } + } + vo.setUserExamCount(entity.getUserExamCount()); + vo.setUserExamResultValid(entity.getUserExamResultValid()); + list.add(vo); + } + + + fillExamUserInfo(list); + //填充组织和岗位 + fillOrgAndPost(list); + + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 查询指定用户的考试列表 + * + * @param req + * @return + */ + @Override + public PageInfo queryExamList(QueryCultivateExamReq req) { + List positionIdList = new ArrayList<>(); + if (StringUtils.isNotEmpty(req.getPositionId())) { + positionIdList.addAll(Arrays.asList(req.getPositionId().split(","))); + } + //构建分页 + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + //构建查询 查询条件是 匹配 考试名称 或者试卷ID + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, req.getUserId()) + .in(FtbCultivateExamUser::getStatus, + CourseEnums.ExamStatus.PASS.getCode(), + CourseEnums.ExamStatus.NO_PASS.getCode(), + CourseEnums.ExamStatus.VERY_PASS.getCode() + ) + .in(CollectionUtil.isNotEmpty(positionIdList), FtbCultivateExamUser::getRelationPositionId, positionIdList) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .eq(FtbCultivateExamUser::getIsJoinCount, ExamUserStatisticsEnums.PARTICIPATE.getCode()) + .orderByDesc(FtbCultivateExamUser::getCreatorTime); + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(records)) { + + records.forEach(entity -> { + MyExamListVo vo = new MyExamListVo(); + vo.setId(entity.getId()); + vo.setFinishtime(entity.getFinishTime()); + vo.setScore(entity.getScore()); + vo.setExamId(entity.getExamId()); + vo.setDuration(entity.getDuration()); + + if (StringUtils.isNotEmpty(entity.getExamId())) { + FtbCultivateExam exam = examService.getById(entity.getExamId()); + if (null != exam) { + vo.setExamName(exam.getExamName()); + vo.setExamType(exam.getExamType()); + vo.setExamTime(exam.getExamTime()); + } + } + if (StringUtils.isNotEmpty(entity.getPaperId())) { + FtbCultivateTestPaper paper = paperService.getById(entity.getPaperId()); + if (null != paper) { + vo.setPaperName(paper.getName()); + vo.setExamTotleScore(paper.getTotalScore()); + } + } + vo.setExamSource(entity.getExamSource()); + vo.setStatus(entity.getStatus()); + if (vo.getExamSource() == 1 || vo.getExamSource() == 2) { + if (StringUtils.isNotEmpty(entity.getRelationPositionId())) { + //查询岗位 +// PositionEntity positionEntity = positionApi.queryInfoById(entity.getRelationPositionId()); + PositionVO positionEntity = userApiV2Util.infoPosition(entity.getRelationPositionId(), null); + if (null != positionEntity) { + vo.setCurrPostName(positionEntity.getFullName()); + } + } + } + list.add(vo); + }); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 考试按照组织维度统计信息 + * + * @param dto 查询信息 + * @return + */ + @Override + public List queryExamStatisticsForOrg(ExamStatisticsForOrgDTO dto, List userIds) { + + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return new ArrayList<>(); + } + + if (StringUtils.isEmpty(dto.getOrgIds())) { + return new ArrayList<>(); + } + + + List selectOrgList = new ArrayList<>(Arrays.asList(dto.getOrgIds().split(","))); + + if (dto.getIsNext() == 1) { + List organizeGeneralDetailVOS = userApiV2Util.organizesOrHaveChildByOrganizeIds(selectOrgList, true, null); + if (CollectionUtil.isNotEmpty(organizeGeneralDetailVOS)) { + List collect = organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + selectOrgList.addAll(collect); + } + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, selectOrgList); + if (CollUtil.isEmpty(intersection)) { + return new ArrayList<>(); + } + selectOrgList = intersection; + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(selectOrgList, null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return new ArrayList<>(); + } + Map userPageListVOMap = userListForOrgIds.stream().collect(Collectors.toMap(UserPageListVO::getId, v -> v)); + InnerExamStatisticsForOrgDTO innerExamStatisticsForOrgDTO = BeanUtil.copyProperties(dto, InnerExamStatisticsForOrgDTO.class); + + + innerExamStatisticsForOrgDTO.setSelectUserIds(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + List queryList = baseMapper.countForOrg(innerExamStatisticsForOrgDTO); + + if (CollectionUtil.isEmpty(queryList)) { + return new ArrayList<>(); + } + changeCurrOrg(userPageListVOMap, queryList); + + + //查询组织 + Map organizeEntityMap = userApiV2Util.organizesByOrganizeIdsReturenMap(selectOrgList, null); + +//userOrgList分组 + + Map> orgIdToExamUserMap = queryList.stream().collect(Collectors.groupingBy(AppExamListVo::getUserOrgList)); + + List retList = new ArrayList<>(); + for (Map.Entry> entry : orgIdToExamUserMap.entrySet()) { + String orgId = entry.getKey(); // 组织id + List examUserList = new ArrayList<>(entry.getValue()); + //按照考试分组 + Map> examGroupUserMap = examUserList.stream() + .collect(Collectors.groupingBy(AppExamListVo::getExamId)); + for (Map.Entry> examGroupEntity : examGroupUserMap.entrySet()) { + String examId = examGroupEntity.getKey(); // 当前键 + String examName = ""; + Integer examType = null; + String examTypeName = ""; + List examGroupList = examGroupEntity.getValue(); // 当前值(List) + if (CollectionUtil.isNotEmpty(examGroupList)) { + examName = examGroupList.get(0).getExamName(); + examType = examGroupList.get(0).getExamType(); + if (examGroupList.get(0).getExamType() == 1) { + examTypeName = "岗位学习考试"; + } else { + examTypeName = "自定义考试"; + } + } + //按照试卷分组 + Map> paperGroupUserMap = examGroupList.stream() + .collect(Collectors.groupingBy(AppExamListVo::getPaperId)); + //遍历 paperGroupUserMap + for (Map.Entry> paperGroupEntity : paperGroupUserMap.entrySet()) { + String paperId = paperGroupEntity.getKey(); // 当前键 + String paperName = ""; + String paperTypeName = ""; + Integer paperType = null; + List paperGroupList = paperGroupEntity.getValue(); // 当前值(List) + if (CollectionUtil.isNotEmpty(paperGroupList)) { + paperName = paperGroupList.get(0).getPaperName(); + paperType = paperGroupList.get(0).getPaperType(); + if (paperGroupList.get(0).getPaperType() == 1) { + paperTypeName = "岗位学习试卷"; + } else { + paperTypeName = "常规试卷"; + } + } + StatisticsResultDto statisticsResultDto = QuestionAnalysisUtil.statisticeLvForAppExam(paperGroupList); + ExamStatisticsForOrgVo vo = new ExamStatisticsForOrgVo(); + vo.setOrgId(orgId); + OrganizeGeneralDetailVO organizeEntity = organizeEntityMap.get(orgId); + if (null != organizeEntity) { + vo.setOrgName(organizeEntity.getName()); + vo.setOrgCode(organizeEntity.getEnCode()); + } + vo.setExamId(examId); + vo.setExamName(examName); + vo.setExamType(examType); + vo.setExamTypeName(examTypeName); + + vo.setPaperId(paperId); + vo.setPaperName(paperName); + vo.setPaperTypeName(paperTypeName); + vo.setPaperType(paperType); + + vo.setTotleNum(String.valueOf(statisticsResultDto.getTotle())); + vo.setExcellentNum(String.valueOf(statisticsResultDto.getExcellent())); + vo.setPassNum(String.valueOf(statisticsResultDto.getPass())); + vo.setNoPassNum(String.valueOf(statisticsResultDto.getNoPass())); + + vo.setNoPassRate(String.valueOf(statisticsResultDto.getNoPassLv())); + vo.setPassRate(String.valueOf(statisticsResultDto.getPassLv())); + vo.setExcellentRate(String.valueOf(statisticsResultDto.getExcellentLv())); + retList.add(vo); + } + + } + + } + return retList; + } + + private void changeCurrOrg(Map userPageListVOMap, List queryList) { + for (AppExamListVo appExamListVo : queryList) { + UserPageListVO userPageListVO = userPageListVOMap.get(appExamListVo.getUserId()); + if (userPageListVO != null) { + appExamListVo.setUserOrgList(userPageListVO.getOrganizeId()); + } + } + } + + private InnerAnalysisOrgInfoDto analysisOrgInfo(List currExamUserList, List selectOrgList, Boolean selectALL) { + Set allOrgSet = new HashSet<>();//所有组织集合 + Set allPaperIdSet = new HashSet<>();//所有试卷ID + Map> orgIdToExamUserMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(currExamUserList)) { + for (AppExamListVo examUser : currExamUserList) { + allPaperIdSet.add(examUser.getPaperId()); + if (StringUtils.isNotEmpty(examUser.getUserOrgList())) { + String[] splits = examUser.getUserOrgList().split(","); + if (selectALL) { + for (String split : splits) { + allOrgSet.add(split); + Set itemSet = orgIdToExamUserMap.get(split); + if (null == itemSet) { + itemSet = new HashSet<>(); + } + itemSet.add(examUser); + orgIdToExamUserMap.put(split, itemSet); + } + } else { + for (String split : splits) { + if (selectOrgList.contains(split)) { + allOrgSet.add(split); + Set itemSet = orgIdToExamUserMap.get(split); + if (null == itemSet) { + itemSet = new HashSet<>(); + } + itemSet.add(examUser); + orgIdToExamUserMap.put(split, itemSet); + } + } + } + } + } + } + InnerAnalysisOrgInfoDto dto = new InnerAnalysisOrgInfoDto(); + dto.setAllOrgSet(allOrgSet); + dto.setAllPaperIdSet(allPaperIdSet); + dto.setOrgIdToExamUserMap(orgIdToExamUserMap); + return dto; + } + + /** + * 考试按照个人维度统计信息 + * + * @param dto 查询信息 + * @return + */ + @Override + public PageInfo queryExamStatisticsForPerson(ExamStatisticsForPersonDTO dto) { + + if (null == dto.getCurrentPage() || null == dto.getPageSize()) { + throw new RuntimeException("请传入分页信息"); + } + + InnerExamStatisticsForPersonDTO innerDto = BeanUtil.copyProperties(dto, InnerExamStatisticsForPersonDTO.class); + //0待考试,1待批阅,2已逾期,3合格,4不合格 5、优秀 + List innerStatus = buildExamStatus(dto); + innerDto.setInnerStatus(innerStatus); + + List innerUserIds = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(dto.getUserIds()) && CollectionUtil.isNotEmpty(dto.getPersionPositonIds())) { + List stringList = queryUserIdsForPostionIds(dto.getPersionPositonIds()); + if (CollectionUtil.isEmpty(stringList)) { + return new PageInfo<>(); + } + innerUserIds = dto.getUserIds().stream().filter(stringList::contains).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(innerUserIds)) { + return new PageInfo<>(); + } + innerUserIds.addAll(dto.getUserIds()); + } else if (CollectionUtil.isNotEmpty(dto.getUserIds())) { + innerUserIds.addAll(dto.getUserIds()); + } else if (CollectionUtil.isNotEmpty(dto.getPersionPositonIds())) { + List stringList = queryUserIdsForPostionIds(dto.getPersionPositonIds()); + if (CollectionUtil.isEmpty(stringList)) { + return new PageInfo<>(); + } + innerUserIds.addAll(stringList); + } + if (StringUtils.isNotEmpty(dto.getOrgId())) { + String[] split = dto.getOrgId().split(","); + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(Arrays.asList(split), null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return new PageInfo<>(); + } + if (CollUtil.isEmpty(innerUserIds)) { + innerUserIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + } else { + List intersection = UserApiV2Util.getIntersection(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()), innerUserIds); + if (CollUtil.isEmpty(intersection)) { + return new PageInfo<>(); + } + innerUserIds = intersection; + } + } + innerDto.setInnerUserIds(innerUserIds); + + + List innerPaperIds = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(dto.getPaperIds())) { + innerPaperIds = dto.getPaperIds(); + } + innerDto.setInnerPaperIds(innerPaperIds); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(innerDto.getInnerUserIds())) { + List intersection = UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), innerDto.getInnerUserIds()); + if (CollUtil.isEmpty(intersection)) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPageSize(dto.getPageSize()); + pageInfo.setPageNum(dto.getCurrentPage()); + pageInfo.setTotal(0); + return pageInfo; + } else { + innerDto.setInnerUserIds(intersection); + } + } else { + innerDto.setInnerUserIds(innerPowerUserVO.getUserIds()); + } + } else if (innerPowerUserVO.getCode() == 2) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPageSize(dto.getPageSize()); + pageInfo.setPageNum(dto.getCurrentPage()); + pageInfo.setTotal(0); + return pageInfo; + } +// innerDto.setLeaverUserIds(userApiV2Util.queryLeaveUserId()); + + + Page queryPage = baseMapper.queryExamStatisticsForPerson(Page.of(dto.getCurrentPage(), dto.getPageSize()), + innerDto); + List list = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(list)) { + List tempUserId = new ArrayList<>(); + List tempPostionId = new ArrayList<>(); + for (ExamStatisticsForPersonVo examStatisticsForPersonVo : list) { + tempUserId.add(examStatisticsForPersonVo.getUserId()); + if (examStatisticsForPersonVo.getExamSource() == 1 || examStatisticsForPersonVo.getExamSource() == 2) { + tempPostionId.add(examStatisticsForPersonVo.getRelationPositionId()); + } + } + + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(tempUserId, null); + List positionVOS = userApiV2Util.listPositionDetailInfoByIds(tempPostionId, null); + Map tempPositionMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(positionVOS)) { + tempPositionMap = positionVOS.stream() + .collect(Collectors.toMap(PositionVO::getId, Function.identity())); + } + for (ExamStatisticsForPersonVo examStatisticsForPersonVo : list) { + if (examStatisticsForPersonVo.getExamSource() == 1 || examStatisticsForPersonVo.getExamSource() == 2) { + PositionVO positionEntity = tempPositionMap.get(examStatisticsForPersonVo.getRelationPositionId()); + if (null != positionEntity) { + examStatisticsForPersonVo.setStudyPostionName(positionEntity.getFullName()); + } + } + UserBoundVO userPrimaryBoundOne = userPrimaryBoundBatch.get(examStatisticsForPersonVo.getUserId()); + if (userPrimaryBoundOne != null) { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(userPrimaryBoundOne.getOrganizeEnCode()); + workerGroupDataDto.setAffiliatedPosition(userPrimaryBoundOne.getPositionEnCode()); + workerGroupDataDto.setAffiliatedRank(userPrimaryBoundOne.getGradeId()); + workerGroupDataDto.setAffiliatedOrgName(userPrimaryBoundOne.getOrganizeName()); + workerGroupDataDto.setAffiliatedPositionName(userPrimaryBoundOne.getPositionName()); + workerGroupDataDto.setAffiliatedRankName(userPrimaryBoundOne.getGradeName()); + workerGroupDataDto.setOrgEncode(userPrimaryBoundOne.getOrganizeEnCode()); + workerGroupDataDto.setPositionEncode(userPrimaryBoundOne.getPositionEnCode()); + examStatisticsForPersonVo.setUserOrgList(List.of(workerGroupDataDto)); + examStatisticsForPersonVo.setUserName(userPrimaryBoundOne.getUserName()); + } else { + examStatisticsForPersonVo.setUserOrgList(new ArrayList<>()); + } + if (null != examStatisticsForPersonVo.getExamTime()) { + examStatisticsForPersonVo.setExamTime(examStatisticsForPersonVo.getExamTime() / 60); + } + } + } + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + private List queryUserIdsForPostionIds(List persionPositonIds) { + List voList = userApiV2Util.getUserListForPositions(persionPositonIds, null); + if (CollectionUtil.isEmpty(voList)) { + return new ArrayList<>(); + } + return voList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + + + /** + * 根据职等ID统计合格率 + * + * @param positionId + * @return + */ + + @Override + public BigDecimal queryExamStatisticsForPositionId(String positionId) { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, + FtbCultivateExamUser::getRelationCourseExamId, FtbCultivateExamUser::getRelationPositionExamId, FtbCultivateExamUser::getRelationRankId, FtbCultivateExamUser::getPaperId) + .notIn(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode(), CourseEnums.ExamStatus.WAIT.getCode()) + .in(FtbCultivateExamUser::getExamSource, 1, 2) + .in(FtbCultivateExamUser::getRelationPositionId, positionId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List examUserList = baseMapper.selectList(wrapper); + + StatisticsResultDto curr = QuestionAnalysisUtil.statisticeLv(examUserList); + return new BigDecimal(curr.getPassLv()); + } + + /** + * 处理考试用户的岗位数据,兼容老数据 + */ + @Override + @Deprecated + public void dealExamUserPositionForOldData() { + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, + FtbCultivateExamUser::getRelationCourseExamId, FtbCultivateExamUser::getRelationPositionExamId, FtbCultivateExamUser::getRelationRankId, FtbCultivateExamUser::getPaperId, + FtbCultivateExamUser::getUserOrgList, FtbCultivateExamUser::getRelationPositionId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List examUserList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(examUserList)) { + for (FtbCultivateExamUser examUser : examUserList) { + if (StringUtils.isEmpty(examUser.getRelationPositionId()) && StringUtils.isNotEmpty(examUser.getRelationRankId())) { + //查询岗位 + if (examUser.getExamSource() == 1 || examUser.getExamSource() == 2) { + ActionResult gradesInfo = null; + try { + gradesInfo = positionApi.getGradesInfo(examUser.getRelationRankId()); + if (null != gradesInfo && null != gradesInfo.getData()) { + examUser.setRelationPositionId(gradesInfo.getData().getPositionId()); + } + } catch (Exception e) { + log.error("查询岗位信息失败"); + } + } + } + + //查询组织 + List selectOrgList = organizeApi.getOrganizeIdsByUserId(examUser.getUserId()); + if (CollectionUtil.isNotEmpty(selectOrgList)) { + examUser.setUserOrgList(String.join(",", selectOrgList)); + } + baseMapper.updateById(examUser); + } + } + } + + private List buildExamStatus(ExamStatisticsForPersonDTO dto) { + List innerStatus = new ArrayList<>(); + if (null != dto.getExamStatus() && dto.getExamStatus() == 1) { + innerStatus.add(0); + innerStatus.add(2); + } else if (null != dto.getExamStatus() && dto.getExamStatus() == 2) { + if (null != dto.getExamResult() && dto.getExamResult() == 0) { + innerStatus.add(1); + innerStatus.add(3); + innerStatus.add(4); + innerStatus.add(5); + } else if (null != dto.getExamResult() && dto.getExamResult() == 1) { + innerStatus.add(5); + } else if (null != dto.getExamResult() && dto.getExamResult() == 2) { + innerStatus.add(3); + } else if (null != dto.getExamResult() && dto.getExamResult() == 3) { + innerStatus.add(4); + } else { + innerStatus.add(1); + innerStatus.add(3); + innerStatus.add(4); + innerStatus.add(5); + } + } else { + if (null != dto.getExamResult() && dto.getExamResult() == 0) { + innerStatus.add(0); + innerStatus.add(1); + innerStatus.add(2); + innerStatus.add(3); + innerStatus.add(4); + innerStatus.add(5); + } else if (null != dto.getExamResult() && dto.getExamResult() == 1) { + innerStatus.add(5); + } else if (null != dto.getExamResult() && dto.getExamResult() == 2) { + innerStatus.add(3); + } else if (null != dto.getExamResult() && dto.getExamResult() == 3) { + innerStatus.add(4); + } else { + innerStatus.add(0); + innerStatus.add(1); + innerStatus.add(2); + innerStatus.add(3); + innerStatus.add(4); + innerStatus.add(5); + } + } + return innerStatus; + } + + + /** + * @param exam + * @param examUser + * @param userId + * @return false 不能批阅,true 可以批阅 + */ + public Boolean checkMyIsRevierverDynamic(FtbCultivateExam exam, FtbCultivateExamUser examUser, String userId, PermissionsCacheDTO permissionsCacheDTO) { + //阅卷人 + Set reviewerUserIdList = new HashSet<>(); + if (StringUtils.isNotEmpty(exam.getReviewerAppoint())) { + reviewerUserIdList.addAll(Arrays.asList(exam.getReviewerAppoint().split(","))); + if (reviewerUserIdList.contains(userId)) { + return true; + } + } + List localOrganizeIds = null; + if ("0".equals(exam.getReviewer())) { + //查询当前用户的直接主管 + UserBoundVO bossByUserIdSimple = userApiV2Util.getBossByUserIdSimple(examUser.getUserId(), null); + if (bossByUserIdSimple != null && StringUtils.isNotEmpty(bossByUserIdSimple.getLeaderId())) { + reviewerUserIdList.add(bossByUserIdSimple.getLeaderId()); + } + if (reviewerUserIdList.contains(userId)) { + return true; + } + } + //todo 下一班 优化查询权 + List powerUserIdsList = getPowerUserIdsForPermission(examUser.getUserId(), permissionsCacheDTO, localOrganizeIds); + String powerUserIds = ""; + if (CollectionUtil.isNotEmpty(powerUserIdsList)) { + powerUserIds = String.join(",", powerUserIdsList); + } +// String powerUserIds = personnelPerUtils.obtainTrainingApproverPermissions(examUser.getUserId()); + if (StringUtils.isNotEmpty(powerUserIds)) { + reviewerUserIdList.addAll(Arrays.asList(powerUserIds.split(","))); + if (reviewerUserIdList.contains(userId)) { + return true; + } + } + return false; + } + + /** + * 查询用户有批阅权限的用户集合 + * + * @param userId 用户id + * @param permissionsCacheDTO 批阅权限 + * @param localOrganizeIds 用户的组织id集合 + * @return + */ + @Override + public List getPowerUserIdsForPermission(String userId, PermissionsCacheDTO permissionsCacheDTO, List localOrganizeIds) { + Map> allUserListIdsMap = permissionsCacheDTO.getAllUserListIdsMap(); + Map> allOrgListMap = permissionsCacheDTO.getAllOrgListMap(); + List retUserListIds = new ArrayList<>(); + + for (Map.Entry> entry : allUserListIdsMap.entrySet()) { + FtbPersonnelsPermissions key = entry.getKey(); + List values = entry.getValue(); + if (values.contains(userId)) { + retUserListIds.add(key.getUserId()); + } + } + if (localOrganizeIds == null) { + localOrganizeIds = organizeApi.getOrganizeIdsByUserId(userId); + } + + for (Map.Entry> entry : allOrgListMap.entrySet()) { + FtbPersonnelsPermissions key = entry.getKey(); + List values = entry.getValue(); + if (personnelPerUtils.haveIntersection(localOrganizeIds, values)) { + retUserListIds.add(key.getUserId()); + } + } + return retUserListIds; + } + + /** + * 查询任务考试结果 + * + * @param taskId 任务id + * @param userId 用户ID + * @return -1:没有考试记录 + * 0:未完成考试 + * 1:已经完成考试(待批阅,未出考试结果) + * 3合格 + * 4不合格 + * 5优秀 + */ + @Override + public Integer queryExamResultForTask(String taskId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, userId) + .eq(FtbCultivateExamUser::getExamSource, 4) + .eq(FtbCultivateExamUser::getRelationTaskId, taskId) + .eq(FtbCultivateExamUser::getEnabledMark, 1) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()).orderByDesc(FtbCultivateExamUser::getCreatorTime); + List examUserList = baseMapper.selectList(wrapper); + + if (CollectionUtil.isEmpty(examUserList)) { + return -1; + } + FtbCultivateExamUser examUser = examUserList.get(0); + //(0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + if (examUser.getStatus() == 0 || examUser.getStatus() == 2) { + return 0; + } + if (examUser.getStatus() == 1) { + return 1; + } + return examUser.getStatus(); + } + + /*** + * 查询任务考试结果 + * @param taskId 任务id + * @param userId 用户id + * @return + */ + @Override + public List queryExamResultForTaskInfo(String taskId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, userId) + .eq(FtbCultivateExamUser::getExamSource, 4) + .eq(FtbCultivateExamUser::getRelationTaskId, taskId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()).orderByDesc(FtbCultivateExamUser::getCreatorTime); + return baseMapper.selectList(wrapper); + } + + + /** + * 填充组织和岗位 + * + * @param list + */ + private void fillOrgAndPost(List list) { + + List userIds = new ArrayList<>(); + for (ExamListUserVo examListUserVo : list) { + userIds.add(examListUserVo.getUserId()); + } + Map> map = queryUserOrganizationByUserIds(userIds); + for (ExamListUserVo examListUserVo : list) { + List userOrganizationVoList = map.get(examListUserVo.getUserId()); + examListUserVo.setUserOrganizationVoList(userOrganizationVoList); + } + } + + /** + * 查询用户考试详情 + * + * @param userExamId 用户考试ID + * @return + */ + @Override + public FtbCultivateExamUser getInfo(String userExamId) { + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getId, userExamId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCultivateExamUser examUser = baseMapper.selectOne(wrapper); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + return examUser; + } + + /** + * 删除用户考试 + * + * @param userExamId 用户考试ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String userExamId) { + //1、查询检测用户考试 + FtbCultivateExamUser examUser = queryAndCheckUserExam(userExamId); + //2、删除考试 + examUser.setDeleteTime(new Date()); + examUser.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + baseMapper.updateById(examUser); + //3、删除考试记录 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateExamUserDetail::getBatch, examUser.getBatch()) + .eq(FtbCultivateExamUserDetail::getUserExamId, examUser.getId()) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + userDetailService.update(updateWrapper); + + delExamCountRecord(examUser.getUserId(), examUser.getExamId()); + } + + /** + * 检测用户考试是否可以删除 + * + * @param userExamId 用户考试ID + * @return + */ + @Override + public CanDeleteMsg checkUserExamCanDelete(String userExamId) { + FtbCultivateExamUser examUser = queryAndCheckUserExam(userExamId); +// if (CourseEnums.ExamStatus.WAIT_CHECK.getCode().equals(examUser.getStatus())) { +// return new CanDeleteMsg(false, "考试已完成,等待批阅", examUser); +// } +// if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus()) +// || CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { +// return new CanDeleteMsg(false, "考试已经合格", examUser); +// } + return new CanDeleteMsg(true, "可以删除", examUser); + } + + /** + * 批阅用户考试 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void readOver(ReadOverExamReq req) { + String loginUserId = UserProvider.getLoginUserId(); + //1、检测 + FtbCultivateExamUser examUser = queryAndCheckUserExam(req.getUserExamId()); + if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("考试已合格,不能再次批阅"); + } + + if (CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("考试已合格,不能再次批阅"); + } + if (CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) + || CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("用户未考试,不能批阅"); + } + //2、查询考试用户的题目列表 + List examUserDetailList = queryUserQuestion(examUser.getId(), examUser.getUserId(), examUser.getBatch()); + if (CollectionUtil.isEmpty(examUserDetailList)) { + throw new RuntimeException("用户考试的试题信息为空,不能批阅"); + } + //3、查看考试信息和试卷 + FtbCultivateExam exam = examService.getById(examUser.getExamId()); + if (exam == null) { + throw new RuntimeException("用户考试的信息不存在,不能批阅"); + } + if (exam.getStatus().equals(3)) { + userExamUtil.updateUserExamNullify(examUser.getId()); + return; + } + + if (loginUserId.equals(examUser.getUserId())) { + throw new RuntimeException("不能批阅自己的考试"); + } + //不用再查询历史表 再用户的考试记录中保存有相关配置 +// exam = userExamUtil.queryExamByIdAndBatch(exam.getId(), examUser.getVersionBatch()); + + int readOverScore = 0;//批阅分数 + int correctCount = 0;//正确数量 + List batchUpdateList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(req.getAnswerScoreList())) { + //转换成map questionId->FtbCultivateExamUserDetail + Map userDetailMap = examUserDetailList.stream().collect(Collectors.toMap(FtbCultivateExamUserDetail::getQuestionId, detail -> detail)); + //处理用户的答题结果 + List answerScoreList = req.getAnswerScoreList(); + for (ReadOverExamReq.QuestionAnswerScore answerScore : answerScoreList) { + FtbCultivateExamUserDetail userDetail = userDetailMap.get(answerScore.getQuestionId()); + if (null == userDetail) { + log.error("用户考试的试题信息为空,questionId={}", answerScore.getQuestionId()); + continue; + } + if (CourseEnums.QuestionType.SINGLE.getCode().equals(userDetail.getType()) + || CourseEnums.QuestionType.MULTI.getCode().equals(userDetail.getType()) + || CourseEnums.QuestionType.ONE_OR_MULTI.getCode().equals(userDetail.getType()) + || CourseEnums.QuestionType.JUDGE.getCode().equals(userDetail.getType())) { + continue; + } + //满分算对 + if (answerScore.getScore().equals(userDetail.getQuestionScore())) { + correctCount++; + userDetail.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + userDetail.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + if (answerScore.getScore() < 0) { + throw new RuntimeException("[" + userDetail.getContent() + "]的评分不能小于0分"); + } + if (answerScore.getScore() > userDetail.getQuestionScore()) { + throw new RuntimeException("[" + userDetail.getContent() + "]的评分不能大于" + userDetail.getQuestionScore() + "分"); + } + userDetail.setUserScore(answerScore.getScore()); + readOverScore += answerScore.getScore(); + batchUpdateList.add(userDetail); + } + } + examUser.setReadOverScore(readOverScore); + //4、批量修改考试用户详情 + userDetailService.saveOrUpdateBatch(batchUpdateList); + //5、修改考试的批阅分数和总分数 和根据分数判断考试是否合格 + FtbCultivateExamUser updateExamUser = new FtbCultivateExamUser(); + updateExamUser.setReviewerUserId(loginUserId); + updateExamUser.setId(examUser.getId()); + updateExamUser.setExamId(examUser.getExamId()); + updateExamUser.setUserId(examUser.getUserId()); + + updateExamUser.setCorrectCount(examUser.getCorrectCount() + correctCount);//题目正确数量 + updateExamUser.setReadOverScore(readOverScore);//批阅分数 + updateExamUser.setScore(examUser.getAutoScore() + readOverScore);//总分数 + int paperTotalScore = examUser.getTotalScore(); + if (examUser.getTotalScore() == 0) { + paperTotalScore = exam.getCurrTotalScore(); + } + updateExamUser.setStatus(QuestionAnalysisUtil.calculateUserExamStatus(exam, paperTotalScore, updateExamUser.getScore())); + BigDecimal score = BigDecimal.valueOf(updateExamUser.getScore()); + BigDecimal totalScore = BigDecimal.valueOf(paperTotalScore); + BigDecimal divide = score.divide(totalScore, 2, RoundingMode.HALF_UP); + updateExamUser.setScoreRatio(divide); + updateExamUser.setLastModifyTime(new Date()); + + baseMapper.updateById(updateExamUser); +// if (updateExamUser.getStatus().equals(CourseEnums.ExamStatus.PASS.getCode()) +// || updateExamUser.getStatus().equals(CourseEnums.ExamStatus.VERY_PASS.getCode())) { +// if (StringUtils.isNotEmpty(examUser.getRelationPositionId())) { +// String courseId = ""; +// Integer issuanceType = 0; +// if (examUser.getExamSource() == 1) { +// //岗位学习课程 +// issuanceType = 0; +// FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectById(examUser.getRelationCourseExamId()); +// courseId = ftbCultivatePositionCourseExam.getCourseId(); +// } else if (examUser.getExamSource() == 2) { +// //岗位学习 +// issuanceType = 1; +// } +// userExamUtil.publishExam(examUser.getUserId(), examUser.getRelationPositionId(), issuanceType, courseId); +// } +// } +// if (updateExamUser.getStatus().equals(CourseEnums.ExamStatus.PASS.getCode()) +// || updateExamUser.getStatus().equals(CourseEnums.ExamStatus.NO_PASS.getCode()) +// || updateExamUser.getStatus().equals(CourseEnums.ExamStatus.VERY_PASS.getCode())) { +// if (StringUtils.isNotEmpty(examUser.getRelationPositionId()) && examUser.getExamSource() == 2) { +// userExamUtil.publishExamCallBack(examUser.getUserId(), examUser.getRelationPositionId(), updateExamUser.getStatus()); +// // updateExamUser.setExamIdentifyConfig(integer); +// } else if (StringUtils.isNotEmpty(examUser.getRelationTaskId())) { +// userExamUtil.publishTaskExamCallBack(examUser.getUserId(), examUser.getRelationTaskId(), updateExamUser.getStatus()); +// } +// } + + cultivateMqSendUtil.sendExamMessage(updateExamUser); + } + + + /** + * 获取用户考试详情 + * + * @param userExamId 用户考试ID + * @param isQueryQuestionList 是否查询题目 + * @return + */ + @Override + public UserExamDetailVo getUserExamDetail(String userExamId, Boolean isQueryQuestionList) { + //查用户基本信息 + UserExamDetailVo vo = queryBaseUserExamInfo(userExamId); + //查看用户的考试题目详情 + if (isQueryQuestionList) { + List questionList = queryUserQuestion(vo.getId(), vo.getUserId(), vo.getBatch()); + List userQuestionVoList = new ArrayList<>(); + for (FtbCultivateExamUserDetail question : questionList) { + UserQuestionVo userQuestionVo = BeanUtil.copyProperties(question, UserQuestionVo.class); + //转换题目选项 + if (StringUtils.isNotEmpty(question.getQuestionOption())) { + userQuestionVo.setQuestionOptionVoList(JSON.parseArray(question.getQuestionOption(), QuestionOptionVo.class)); + } + userQuestionVoList.add(userQuestionVo); + } + vo.setQuestionMap(convertUserQuestionListForType(userQuestionVoList)); + } + //查询晋升通道 + + List userOrganizationList = vo.getUserOrganizationList(); + List promotionList = new ArrayList<>(); + for (PositionGradesInfoBoundVO positionGradesInfoBoundVO : userOrganizationList) { + AppPromotionVO appPromotionVO = BeanUtil.copyProperties(positionGradesInfoBoundVO, AppPromotionVO.class); + FtbCultivatePromotionVO promotionVO = promotionMemberService.queryPromotionByUser(vo.getUserId(), positionGradesInfoBoundVO.getPositionId()); + if (promotionVO != null) { + appPromotionVO.setPromotionVO(promotionVO); + } + promotionList.add(appPromotionVO); + } + vo.setPromotion(promotionList); + if (CourseEnums.ExamType.POSITION.getCode().equals(vo.getExamType())) { + String relationRankId = vo.getRelationRankId(); + if (StringUtils.isNotEmpty(relationRankId)) { + try { + + GradeVO gradesInfoData = userApiV2Util.infoGrade(relationRankId, vo.getTenantId()); + if (null != gradesInfoData) { + + vo.setCurrPostName(""); + vo.setCurrRankName(gradesInfoData.getFullName()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else if (StringUtils.isNotEmpty(vo.getRelationPositionId())) { +// PositionEntity positionEntity = positionApi.queryInfoById(vo.getRelationPositionId()); + PositionVO positionEntity = userApiV2Util.infoPosition(vo.getRelationPositionId(), null); + if (null != positionEntity) { + vo.setCurrPostName(positionEntity.getFullName()); + } + } + } + + return vo; + } + + /** + * 根据用户ID 补充用户信息 + * + * @param vo + * @param + */ + private void fillUserInfoForUserId(T vo) { + UserEntity userEntity = userApi.getInfoById(vo.getUserId()); + if (userEntity != null) { + vo.setUserName(userEntity.getRealName()); + vo.setHeadLogo(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + } + } + + private Map> convertUserQuestionListForType(List userQuestionVoList) { + Map> questionMap = new HashMap<>(); + for (UserQuestionVo userQuestionVo : userQuestionVoList) { + String type = String.valueOf(userQuestionVo.getType()); + List temp = questionMap.get(type); + if (CollectionUtil.isEmpty(temp)) { + temp = new ArrayList<>(); + } + temp.add(userQuestionVo); + questionMap.put(type, temp); + } + return questionMap; + } + + + /** + * 查询用户绑定的组织 岗位职等 + * + * @param userId 用户id + * @return + */ + @Override + public List queryUserOrganization(String userId) { + if (StringUtils.isEmpty(userId)) { + return CollectionUtil.newArrayList(); + } + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(userId, null); + + if (userBoundVO == null) { + return CollectionUtil.newArrayList(); + } + List orgPositionInfoList = new ArrayList<>(); + PositionGradesInfoBoundVO vo = new PositionGradesInfoBoundVO(); + vo.setOrganizeIds(List.of(userBoundVO.getOrganizeId())); + vo.setOrganizeNames(List.of(userBoundVO.getOrganizeName())); + vo.setUserId(userId); + vo.setPositionId(userBoundVO.getPositionId()); + vo.setPositionName(userBoundVO.getPositionName()); + vo.setPositionGradesId(userBoundVO.getGradeId()); + vo.setPositionGradesName(userBoundVO.getGradeName()); + orgPositionInfoList.add(vo); + return orgPositionInfoList; + } + + /** + * 根据用户id批量查询用户的岗位信息 + * + * @param userIds 用户ID 集合 + * @return + */ + @Override + public Map> queryUserOrganizationByUserIds(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + userIds = userExamUtil.uniqueStringList(userIds); + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + + if (CollectionUtil.isEmpty(userPrimaryBoundBatch)) { + return new HashMap<>(); + } + Map> map = new HashMap<>(); + //遍历userPrimaryBoundBatch + for (Map.Entry entry : userPrimaryBoundBatch.entrySet()) { + String userId = entry.getKey(); + UserBoundVO value = entry.getValue(); + PositionGradesInfoBoundVO vo = new PositionGradesInfoBoundVO(); + vo.setOrganizeIds(List.of(value.getOrganizeId())); + vo.setOrganizeNames(List.of(value.getOrganizeName())); + vo.setPositionId(value.getPositionId()); + vo.setPositionName(value.getPositionName()); + vo.setPositionGradesId(value.getGradeId()); + vo.setPositionGradesName(value.getGradeName()); + map.put(userId, List.of(vo)); + } + + return map; + } + + /** + * 查询考试人数 + * + * @param examId + * @return + */ + @Override + public Long queryCompleteNumForExamId(String examId) { + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getExamId, examId) + .notIn(FtbCultivateExamUser::getStatus, 1, 2) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return baseMapper.selectCount(wrapper); + } + + /** + * 查询用户考试记录基本信息 + * + * @param userExamId 用户考试记录ID + * @return + */ + @Override + public UserExamDetailVo queryBaseUserExamInfo(String userExamId) { + UserExamDetailVo vo = new UserExamDetailVo(); + //查询用户的考试汇总信息 + FtbCultivateExamUser examUser = queryAndCheckUserExam(userExamId); + //查询考试信息 + FtbCultivateExam exam = examService.getById(examUser.getExamId()); + if (exam.getStatus().equals(3)) { + vo.setIsNullify(1); + } else { + vo.setIsNullify(0); + } + if (examUser.getVersionBatch().equals(exam.getVersionBatch())) { + if (CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) || CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + vo.setNeedUpdate(exam.getNeedUpdate()); + } + } + exam = userExamUtil.checkAndUpdateExam(exam, examUser.getVersionBatch()); + vo.setId(examUser.getId()); + vo.setScore(examUser.getScore()); + vo.setExamId(examUser.getExamId()); + vo.setDuration(examUser.getDuration()); + vo.setUserId(examUser.getUserId()); + vo.setStatus(examUser.getStatus()); + vo.setReadOverScore(examUser.getReadOverScore()); + vo.setAutoScore(examUser.getAutoScore()); + vo.setCorrectCount(examUser.getCorrectCount()); + vo.setPaperId(examUser.getPaperId()); + vo.setUserStartTime(examUser.getUserStartTime()); + vo.setStartTime(examUser.getStartTime()); + vo.setEndTime(examUser.getEndTime()); + vo.setFinishtime(examUser.getFinishTime()); + vo.setBatch(examUser.getBatch()); + vo.setExamSource(examUser.getExamSource()); + vo.setRelationPositionExamId(examUser.getRelationPositionExamId()); + vo.setRelationCourseExamId(examUser.getRelationCourseExamId()); + vo.setRelationRankId(examUser.getRelationRankId()); + vo.setRelationPositionId(examUser.getRelationPositionId()); + //查询考试信息 + if (CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) || CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + vo.setExamTotleScore(exam.getCurrTotalScore()); + vo.setQuestionNumber(exam.getCurrQuestionNumber()); + } else { + vo.setExamTotleScore(examUser.getTotalScore()); + if (examUser.getQuestionNumber() == null || examUser.getQuestionNumber() == 0) { + LambdaQueryWrapper examUserDetailLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUserDetail::getUserExamId, examUser.getId()) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + long count = userDetailService.count(examUserDetailLambdaQueryWrapper); + vo.setQuestionNumber((int) count); + } else { + vo.setQuestionNumber(examUser.getQuestionNumber()); + } + } + //考试相关的岗位 + if (StringUtils.isNotEmpty(exam.getPostId())) { + List positionEntityList = userApiV2Util.listPositionDetailInfoByIds(Arrays.asList(exam.getPostId().split(",")), null); + List postAndPositionList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(positionEntityList)) { + for (PositionVO positionEntity : positionEntityList) { + PostAndPosition postAndPosition = new PostAndPosition(); + postAndPosition.setId(positionEntity.getId()); + postAndPosition.setPostName(positionEntity.getFullName()); + postAndPosition.setEnCode(positionEntity.getEnCode());//todo岗位编码 + postAndPositionList.add(postAndPosition); + } + } + vo.setExamRelationPositionList(postAndPositionList); + } + vo.setExamName(exam.getExamName()); + vo.setExamType(exam.getExamType()); + vo.setExamTime(exam.getExamTime()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setPassMark(exam.getPassMark()); + vo.setPassType(exam.getPassType()); + vo.setExcellentType(exam.getExcellentType()); + vo.setExcellentMark(exam.getExcellentMark()); + vo.setDescription(exam.getDescription()); + //补充试卷题型数量统计 + List questionList = new ArrayList<>(); + if (StringUtils.isNotEmpty(exam.getCurrQuestionList())) { + questionList = HistoryPaperUtils.convertQuestionList(exam.getCurrQuestionList()); + } else { + questionList = paperService.queryConfigQuestionList(exam.getPaperId()); + } + List halfFullQuestion = userExamUtil.queryHalfFullQuestionList(questionList); + vo.setQuestionNumMap(HistoryPaperUtils.analysPaperQuestionCount(halfFullQuestion)); + //补充用户信息 + fillUserInfoForUserId(vo); + List userOrganizationList = queryUserOrganization(vo.getUserId()); + vo.setUserOrganizationList(userOrganizationList); + //计算用户需要考多少分才及格 + int needScore = 0; + Integer passType = exam.getPassType();//合格分数类型(1固定分,2百分比) + Integer passMark = exam.getPassMark();//合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(passType)) { + needScore = passMark; + } else { + needScore = QuestionAnalysisUtil.calculateScore(passMark, vo.getExamTotleScore()); + } + vo.setNeedExamPassScore(needScore); + //计算用户需要考多少分才优秀 + int needExcellentScore = 0; + Integer excellentType = exam.getExcellentType();//优秀分数类型(1固定分,2百分比) + Integer excellentMark = exam.getExcellentMark();//优秀分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(excellentType)) { + needExcellentScore = excellentMark; + } else { + needExcellentScore = QuestionAnalysisUtil.calculateScore(excellentMark, vo.getExamTotleScore()); + } + vo.setNeedExamExcellentScore(needExcellentScore); + return vo; + } + + /** + * 根据考试id查询排行版 + * + * @param examId 考试id + * @return + */ + @Override + public UserRankingVo queryMyRank(String examId) { + String loginUserId = UserProvider.getLoginUserId(); + + //1、查看考试信息 + FtbCultivateExam exam = examService.getById(examId); + if (exam == null) { + throw new RuntimeException("考试信息不存在"); + } + UserRankingVo vo = new UserRankingVo(); + List list = baseMapper.queryMyRank(examId, loginUserId); + if (CollectionUtil.isEmpty(list)) { + vo.setScore("-1"); + } else { + vo = QuestionAnalysisUtil.getMaxScore(list); + } + vo.setUserId(loginUserId); + //补充组织 + List userOrganizationVoList = queryUserOrganization(vo.getUserId()); + vo.setUserOrganizationVoList(userOrganizationVoList); + fillUserInfoForUserId(vo); + return vo; + } + + /** + * 删除考试,同时创建考试 + * + * @param userExamId 删除id并重新创建考试 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAndCreateExam(String userExamId) { + //检测是否已经删除 + FtbCultivateExamUser examUser = queryAndCheckUserExam(userExamId); + FtbCultivateExam exam = examService.queryExamInfo(examUser.getExamId()); + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(exam.getExamType()); + switch (examType) { + case BASE://常规考试 + //判断当前时间是否已经过了考试结束时间 + if (examUser.getEndTime() != null) { + if (examUser.getEndTime().getTime() < new Date().getTime()) { + throw new RuntimeException("当前时间已经超过了考试结束时间"); + } + } + break; + case POSITION://岗位学习考试 + break; + } + //删除考试记录和考试记录详情 + examUser.setDeleteTime(new Date()); + examUser.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + baseMapper.updateById(examUser); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateExamUserDetail::getBatch, examUser.getBatch()) + .eq(FtbCultivateExamUserDetail::getUserExamId, examUser.getId()) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + userDetailService.update(updateWrapper); + + //重新生成一条记录 + FtbCultivateExamUser user = FtbCultivateExamUser.builder() + .examId(exam.getId()) + .examType(exam.getExamType()) + .userId(examUser.getUserId()) + .status(CourseEnums.ExamStatus.WAIT.getCode()) + .reviewerUserIds(examUser.getReviewerUserIds()) + .reviewerRoleId(examUser.getReviewerRoleId()) + .paperId(exam.getPaperId()) + .examSource(examUser.getExamSource()) + .relationRankId(examUser.getRelationRankId()) + .relationPositionExamId(examUser.getRelationPositionExamId()) + .relationPositionId(examUser.getRelationPositionId()) + .relationCourseExamId(examUser.getRelationCourseExamId()) + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .batch(UUID.randomUUID().toString().replaceAll("-", "")) + .versionBatch(exam.getVersionBatch()) + .relationTaskId(examUser.getRelationTaskId()) + .userExamCount(1) + .build(); + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + if (exam.getStartTime() != null) { + user.setStartTime(exam.getStartTime()); + } + if (exam.getEndTime() != null) { + user.setEndTime(exam.getEndTime()); + } + } +// examService.fillRevierwer(user, exam); + //查询用户所属组织 +// List selectOrgList = organizeApi.getOrganizeIdsByUserId(examUser.getUserId()); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(examUser.getUserId(), null); + if (userPrimaryBoundOne != null && userPrimaryBoundOne.getOrganizeId() != null) { + user.setUserOrgList(userPrimaryBoundOne.getOrganizeId()); + } + baseMapper.insert(user); + + delExamCountRecord(examUser.getUserId(), examUser.getExamId()); + + } + + /** + * 删除考试统计数量 + * + * @param userId 用户id + * @param examId 考试id + */ + public void delExamCountRecord(String userId, String examId) { + LambdaQueryWrapper updateWrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamFrequencyLog::getUserId, userId) + .eq(FtbCultivateExamFrequencyLog::getExamId, examId); + ftbCultivateExamFrequncyLogMapper.delete(updateWrapper); + } + + /** + * 培训功能优化,统计组织下用户考试合格率、合格人数、考试总人数 + * + * @param dto + * @return + */ + @Override + public ExamCultivateCountVo queryCultivateCount(CultivateUserExamCountDTO dto, List userIds) { + //1、根据组织查询用户列表 + if (CollectionUtil.isEmpty(userIds)) { + return new ExamCultivateCountVo(); + } + //2、根据用户ID查询考试信息 当前时间段 + Date start = dto.getStartTime(); + Date end = dto.getEndTime(); + List currExamUserList = queryUserExamForDate(userIds, start, end); + StatisticsResultDto curr = QuestionAnalysisUtil.statisticeLv(currExamUserList); + + ExamCultivateCountVo vo = new ExamCultivateCountVo(); + + + vo.setCurrPassRate(String.valueOf(curr.getPassLv())); + vo.setCurrExamTotleNum(String.valueOf(curr.getTotle())); + vo.setCurrExamPassNum(String.valueOf(curr.getPass())); + return vo; + } + + /** + * 根据岗位id统计 考试人数,优秀人数 合格人数 不合格人数 + * + * @param positionId 岗位id + * @return + */ + @Override + public ExamCultivateForPositionVo queryCultivateCountForPositionId(String positionId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, + FtbCultivateExamUser::getRelationCourseExamId, FtbCultivateExamUser::getRelationPositionExamId, FtbCultivateExamUser::getRelationRankId, FtbCultivateExamUser::getPaperId) + .eq(FtbCultivateExamUser::getRelationPositionId, positionId) + .notIn(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode(), CourseEnums.ExamStatus.WAIT.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List currExamUserList = baseMapper.selectList(wrapper); + + StatisticsResultDto curr = QuestionAnalysisUtil.statisticeLv(currExamUserList); + + ExamCultivateForPositionVo vo = new ExamCultivateForPositionVo(); + + vo.setTotle(curr.getTotle()); + vo.setPass(curr.getPass()); + vo.setNoPass(curr.getNoPass()); + vo.setExcellent(curr.getExcellent()); + return vo; + } + + /** + * 排行榜 + * + * @param examId 考试ID + * @param req + * @return + */ + @Override + public PageInfo rankIngList(String examId, QueryExamRankListReq req) { + //1、查看考试信息 + FtbCultivateExam exam = examService.getById(examId); + if (exam == null) { + throw new RuntimeException("考试信息不存在"); + } + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + Page queryPage = baseMapper.pagingQueryRankingExamList(page, req, examId); + List list = queryPage.getRecords(); + //3、补充用户信息 + if (CollectionUtil.isNotEmpty(list)) { + Map userMap = new HashMap<>(); + List userIdList = list.stream().map(UserRankingVo::getUserId).collect(Collectors.toList()); + List userEntityList = userApi.getUserName(userIdList); + if (CollectionUtil.isNotEmpty(userEntityList)) { + userMap = userEntityList.stream().collect(Collectors.toMap(UserEntity::getId, userEntity -> userEntity)); + } + for (int i = 0; i < list.size(); i++) { + UserRankingVo vo = list.get(i); + vo.setRanking(i + 1); + UserEntity userEntity = userMap.get(vo.getUserId()); + if (null != userEntity) { + vo.setUserName(userEntity.getRealName()); + vo.setHeadLogo(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + } + //补充组织 + List userOrganizationVoList = queryUserOrganization(vo.getUserId()); + vo.setUserOrganizationVoList(userOrganizationVoList); + } + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 开始考试 + * + * @param userExamId 用户考试ID + * @return + */ + @Override + public FtbCultivateExamUser startExam(String userExamId) { + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, loginUserId) + .eq(FtbCultivateExamUser::getId, userExamId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCultivateExamUser examUser = baseMapper.selectOne(wrapper); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + + if (examUser.getUserStartTime() != null) { + examUser.setUserStartTime(new Date()); + baseMapper.updateById(examUser); + } + return examUser; + } + + /** + * 查询用户考试详情 + * + * @param examId 考试ID + * @param userExamId 用户考试ID + * @param userId 用户ID + * @return + */ + @Override + public List queryExamUserQuestion(String examId, String userExamId, String userId) { + FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUserDetail::getUserExamId, userExamId) + .eq(FtbCultivateExamUserDetail::getExamId, examId) + .eq(FtbCultivateExamUserDetail::getBatch, examUser.getBatch()) + .eq(FtbCultivateExamUserDetail::getUserId, userId) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return userDetailService.list(wrapper); + } + + /** + * 提交考试试卷 + * + * @param userExamId 用户考试记录表ID + * @param req + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public SubExamVo subExam(String userExamId, SubExamQuestionReq req) { + SubExamVo vo = new SubExamVo(); + if (CollectionUtil.isEmpty(req.getAnswerDetail())) { + throw new RuntimeException("提交的答案信息为空"); + } + if (null == req.getDuration()) { + throw new RuntimeException("考试时长为空"); + } + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, loginUserId) + .eq(FtbCultivateExamUser::getId, userExamId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCultivateExamUser examUser = baseMapper.selectOne(wrapper); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(examUser.getEnabledMark())) { + throw new RuntimeException("参加考试的人员考试取消"); + } + //查看考试信息 + FtbCultivateExam exam = examService.queryExamInfo(examUser.getExamId()); + if (exam.getStatus().equals(3)) { + userExamUtil.updateUserExamNullify(userExamId); + return vo; + } + //检测是否可以提交考试 + checkSubmitExam(examUser, exam); + FtbCultivateExam batchExam = userExamUtil.queryExamByIdAndBatch(exam.getId(), examUser.getVersionBatch()); + //写入用户考试的题目和答案信息 + List userDetailList = writeUserQuestion(req, examUser, batchExam); + int autoScore = 0;//试卷自动阅卷得分 + int correctCount = 0;//正确数 + boolean isNeedReview = false;//是否需要批阅试卷 true:需要批阅 false:不需要批阅 + for (FtbCultivateExamUserDetail userDetail : userDetailList) { + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(userDetail.getType()); + switch (questionType) { + case SINGLE://单选 + case JUDGE://判断 + case MULTI://多选 + case ONE_OR_MULTI://不定项 + if (userDetail.getIsRight() == CourseEnums.IsRightAnswer.YES.getCode()) { + autoScore += userDetail.getQuestionScore(); + correctCount++; + } + break; + case FILL://填空题 + case INPUT://问答题 + isNeedReview = true; + break; + } + } + //修改考试题目信息 + Date now = new Date(); + FtbCultivateExamUser updateExamUser = new FtbCultivateExamUser(); + updateExamUser.setId(examUser.getId()); + updateExamUser.setUserId(examUser.getUserId()); + updateExamUser.setExamId(examUser.getExamId()); + updateExamUser.setCorrectCount(correctCount); + updateExamUser.setAutoScore(autoScore); + updateExamUser.setScore(autoScore); + updateExamUser.setFinishTime(now); + updateExamUser.setUserStartTime(new Date(now.getTime() - req.getDuration() * 1000)); + updateExamUser.setDuration(req.getDuration()); + updateExamUser.setStatus(CourseEnums.ExamStatus.WAIT_CHECK.getCode()); + fillTotleQuestionAndTotleScore(updateExamUser, userDetailList); + //填充阅卷人和阅卷角色 +// examService.fillRevierwer(updateExamUser, exam); + //修改用户考试 + if (!isNeedReview) { + //根据试卷ID查询试卷信息 + updateExamUser.setStatus(QuestionAnalysisUtil.calculateUserExamStatus(exam, updateExamUser.getTotalScore(), updateExamUser.getScore())); + BigDecimal score = BigDecimal.valueOf(updateExamUser.getScore()); + BigDecimal totleScore = BigDecimal.valueOf(updateExamUser.getTotalScore()); + BigDecimal divide = score.divide(totleScore, 2, RoundingMode.HALF_UP); + updateExamUser.setScoreRatio(divide); + } + baseMapper.updateById(updateExamUser); + if (examUser.getUserExamCount() > 1) { + ftbCultivateExamFrequncyLogMapper.insert(FtbCultivateExamFrequencyLog.builder().examId(examUser.getExamId()).userId(examUser.getUserId()).build()); + } +// if (updateExamUser.getStatus().equals(CourseEnums.ExamStatus.PASS.getCode()) +// || updateExamUser.getStatus().equals(CourseEnums.ExamStatus.VERY_PASS.getCode())) { +// if (StringUtils.isNotEmpty(examUser.getRelationPositionId())) { +// Integer issuanceType = 0; +// String courseId = ""; +// if (examUser.getExamSource() == 1) { +// //岗位学习课程 +// issuanceType = 0; +// FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectById(examUser.getRelationCourseExamId()); +// courseId = ftbCultivatePositionCourseExam.getCourseId(); +// } else if (examUser.getExamSource() == 2) { +// //岗位学习 +// issuanceType = 1; +// } +// userExamUtil.publishExam(examUser.getUserId(), examUser.getRelationPositionId(), issuanceType, courseId); +// } +// } +// if (updateExamUser.getStatus().equals(CourseEnums.ExamStatus.PASS.getCode()) +// || updateExamUser.getStatus().equals(CourseEnums.ExamStatus.NO_PASS.getCode()) +// || updateExamUser.getStatus().equals(CourseEnums.ExamStatus.VERY_PASS.getCode())) { +// if (StringUtils.isNotEmpty(examUser.getRelationPositionId()) && examUser.getExamSource() == 2) { +// Integer integer = userExamUtil.publishExamCallBack(examUser.getUserId(), examUser.getRelationPositionId(), updateExamUser.getStatus()); +// vo.setAlertFlag(integer); +//// updateExamUser.setExamIdentifyConfig(integer); +// } else if (StringUtils.isNotEmpty(examUser.getRelationTaskId())) { +//// userExamUtil.publishTaskExamCallBack(examUser.getUserId(), examUser.getRelationTaskId(), updateExamUser.getStatus()); +// } +// } + cultivateMqSendUtil.sendExamMessage(updateExamUser); + return vo; + } + + private void fillTotleQuestionAndTotleScore(FtbCultivateExamUser updateExamUser, List userDetailList) { + int totleScore = 0; + int sum = 0; + if (CollectionUtil.isNotEmpty(userDetailList)) { + sum = userDetailList.size(); + for (FtbCultivateExamUserDetail userDetail : userDetailList) { + if (userDetail.getQuestionScore() != null) { + totleScore += userDetail.getQuestionScore(); + } + } + } + updateExamUser.setTotalScore(totleScore); + updateExamUser.setQuestionNumber(sum); + } + + /** + * 检测是否可以提交考试 + * + * @param examUser + * @param exam + */ + private void checkSubmitExam(FtbCultivateExamUser examUser, FtbCultivateExam exam) { + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(exam.getExamType()); + switch (examType) { + case BASE://常规考试 + if (StringUtils.isEmpty(examUser.getRelationTaskId()) && examUser.getStartTime() != null && examUser.getEndTime() != null) { + //判断当前时间是否已经过了考试结束时间 + long time = new Date().getTime();//当前时间毫秒数 + /*long delay = 10 * 1000;//延迟时间10s,主要是避免前端时间到了,强制交卷时,时间过了 + if (examUser.getEndTime().getTime() < time + delay) { + throw new RuntimeException("考试已经结束"); + }*/ + //判断当前时间是否已经过了考试开始时间 + if (examUser.getStartTime().getTime() > time) { + throw new RuntimeException("考试未开始"); + } + } + break; + case POSITION://岗位学习考试 + break; + + } + + if (!CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) && !CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("考试已经提交,请不要重复提交"); + } + } + + /** + * 检测是否可以重复考试 + * + * @param examUser 用户考试信息 + * @param exam 考试信息 + */ + private void checkCanReExam(FtbCultivateExamUser examUser, FtbCultivateExam exam) { + if (CourseEnums.RepeatExam.SINGLE_EXAM.getCode().equals(exam.getExamlimitation())) { + throw new RuntimeException("该考试不能重复考试"); + } else { + checkReExamNum(examUser, exam); + } + if (exam.getStatus().equals(3)) { + throw new RuntimeException("考试已经作废"); + } + + if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus()) + || CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("考试已经合格,不能够重复考试"); + } else if (CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("当前考试未完成,不能够重复考试"); + } else if (CourseEnums.ExamStatus.WAIT_CHECK.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("当前考试待批阅,不能够重复考试"); + } + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(exam.getExamType()); + switch (examType) { + case BASE://常规考试 + if (StringUtils.isEmpty(examUser.getRelationTaskId()) && examUser.getStartTime() != null && examUser.getEndTime() != null) { + //判断当前时间是否已经过了考试结束时间 + if (examUser.getEndTime().getTime() < new Date().getTime()) { + throw new RuntimeException("考试已经结束"); + } + //判断当前时间是否已经过了考试开始时间 + if (examUser.getStartTime().getTime() > new Date().getTime()) { + throw new RuntimeException("考试未开始"); + } + } + break; + case POSITION://岗位学习考试 + break; + + } + } + + /** + * 检测用户考试频次是否满足 + * + * @param examUser 用户考试信息 + * @param exam 考试信息 + */ + private void checkReExamNum(FtbCultivateExamUser examUser, FtbCultivateExam exam) { + ExamRetakeFrequencyEnum frequencyEnum = ExamRetakeFrequencyEnum.of(exam.getReExamFrequencyType()); + if (frequencyEnum == null) { + throw new RuntimeException("重复考试频次配置错误"); + } + + Date startTime = null; + Date endTime = new Date(); + Integer num = 0; + + switch (frequencyEnum) { + case DAILY: + startTime = UserExamUtil.getTodayStart(); + num = ftbCultivateExamFrequncyLogMapper.queryExamNum(exam.getId(), examUser.getUserId(), startTime, endTime); + if (null != num && num >= exam.getReExamFrequencyNum()) { + throw new RuntimeException("已经超过当天最大考试次数,不能再重复考试了"); + } + break; + case WEEKLY: + startTime = UserExamUtil.getWeekStart(); + num = ftbCultivateExamFrequncyLogMapper.queryExamNum(exam.getId(), examUser.getUserId(), startTime, endTime); + if (null != num && num >= exam.getReExamFrequencyNum()) { + throw new RuntimeException("已经超过本周最大考试次数,不能再重复考试了"); + } + break; + case MONTHLY: + startTime = UserExamUtil.getMonthStart(); + num = ftbCultivateExamFrequncyLogMapper.queryExamNum(exam.getId(), examUser.getUserId(), startTime, endTime); + if (null != num && num >= exam.getReExamFrequencyNum()) { + throw new RuntimeException("已经超过本月最大考试次数,不能再重复考试了"); + } + break; + case CUMULATIVE: + if (examUser.getUserExamCount() >= exam.getReExamFrequencyNum()) { + throw new RuntimeException("已经超过最大考试次数,不能再重复考试了"); + } + break; + case UNLIMITED: + break; + } + } + + /** + * 提交考试写入考试题目记录 + * + * @param req 请求信息 + * @param examUser 考试用户记录信息 + * @param exam 考试信息 + * @return + */ + + private List writeUserQuestion(SubExamQuestionReq req, FtbCultivateExamUser examUser, FtbCultivateExam exam) { + + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + List answerDetailList = req.getAnswerDetail(); + if (CollectionUtil.isEmpty(answerDetailList)) { + CollectionUtil.newArrayList(); + } + List addList = new ArrayList<>(); + for (AppQuestionVo answerDetail : answerDetailList) { + String questionId = answerDetail.getQuestionId(); + String userAnswer = answerDetail.getUserAnswer(); + //查询题目分数 + Integer questionScore = answerDetail.getQuestionScore(); + FtbCultivateExamUserDetail user = FtbCultivateExamUserDetail.builder() + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .userExamId(examUser.getId()) + .examId(exam.getId()) + .questionId(questionId) + .questionOption(JSONUtil.toJsonStr(answerDetail.getQuestionOptionVoList())) + .classifyId(answerDetail.getClassifyId()) + .type(answerDetail.getType()) + .difficulty(answerDetail.getDifficulty()) + .userAnswer(userAnswer) + .content(answerDetail.getContent()) + .analysis(answerDetail.getAnalysis()) + .paperId(exam.getPaperId()) + .userId(loginUserId) + .answer(answerDetail.getAnswer()) + .questionScore(questionScore) + .batch(examUser.getBatch()).build(); + + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(answerDetail.getType()); + switch (questionType) { + case SINGLE://单选 + case JUDGE://判断 + if (answerDetail.getAnswer().equals(userAnswer)) { + user.setUserScore(user.getQuestionScore()); + user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + break; + case MULTI://多选 + case ONE_OR_MULTI://不定项选择题 + if (QuestionAnalysisUtil.checkMultiRight(user.getAnswer(), user.getUserAnswer())) { + user.setUserScore(user.getQuestionScore()); + user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + break; + case FILL://填空题 + case INPUT://问答题 + user.setUserScore(0);//给一个默认分数 + break; + + } + addList.add(user); + } + userDetailService.saveBatch(addList); + return addList; + } + + private List writeUserQuestionNoScore(SubExamQuestionReq req, FtbCultivateExamUser examUser, FtbCultivateExam exam) { + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + List answerDetailList = req.getAnswerDetail(); + if (CollectionUtil.isEmpty(answerDetailList)) { + CollectionUtil.newArrayList(); + } + //根据试卷ID 查询试卷中的题目和分数 + List paperQuestionList = paperService.queryPaperQuestionRelationList(examUser.getPaperId()); + Map questionMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(paperQuestionList)) { + questionMap = paperQuestionList.stream().collect(Collectors.toMap(FtbCultivateTestPaperQuestion::getQuestionId, Function.identity())); + } + + + List addList = new ArrayList<>(); + for (AppQuestionVo answerDetail : answerDetailList) { + String questionId = answerDetail.getQuestionId(); + String userAnswer = answerDetail.getUserAnswer(); + + //查询题目分数 + Integer questionScore = answerDetail.getQuestionScore(); + FtbCultivateTestPaperQuestion paperQuestion = questionMap.get(questionId); + if (null != paperQuestion) { + questionScore = paperQuestion.getScore(); + } + FtbCultivateExamUserDetail user = FtbCultivateExamUserDetail.builder() + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .userExamId(examUser.getId()) + .examId(exam.getId()) + .questionId(questionId) + .questionOption(JSONUtil.toJsonStr(answerDetail.getQuestionOptionVoList())) + .classifyId(answerDetail.getClassifyId()) + .type(answerDetail.getType()) + .difficulty(answerDetail.getDifficulty()) + .userAnswer(userAnswer) + .content(answerDetail.getContent()) + .analysis(answerDetail.getAnalysis()) + .paperId(exam.getPaperId()) + .userId(loginUserId) + .answer(answerDetail.getAnswer()) + .questionScore(questionScore) + .batch(examUser.getBatch()).build(); + + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(answerDetail.getType()); + switch (questionType) { + case SINGLE://单选 + case JUDGE://判断 + if (answerDetail.getAnswer().equals(userAnswer)) { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + break; + case MULTI://多选 + if (QuestionAnalysisUtil.checkMultiRight(user.getAnswer(), user.getUserAnswer())) { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + break; + case FILL://填空题 + case INPUT://问答题 + user.setUserScore(0);//给一个默认分数 + break; + + } + + addList.add(user); + } + userDetailService.saveBatch(addList); + return addList; + } + + + /** + * 查询用户考试题目 + * + * @param userExamId 用户考试ID + * @param userId 用户ID + * @param batch 批次ID + * @return + */ + private List queryUserQuestion(String userExamId, String userId, String batch) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUserDetail::getUserExamId, userExamId) + .eq(FtbCultivateExamUserDetail::getBatch, batch) + .eq(FtbCultivateExamUserDetail::getUserId, userId) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return userDetailService.list(wrapper); + } + + /** + * 查询用户考试信息 + * + * @param userExamId 用户考试ID + * @return + */ + private FtbCultivateExamUser queryAndCheckUserExam(String userExamId) { + FtbCultivateExamUser examUser = getById(userExamId); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(examUser.getEnabledMark())) { + throw new RuntimeException("用户考试信息已删除"); + } + return examUser; + } + + /** + * 填充用户信息 + * + * @param list + */ + + private void fillExamUserInfo(List list) { + //获取用户列表 + List userIdList = list.stream().map(ExamBaseUser::getUserId).collect(Collectors.toList()); + Map userMap = userApiV2Util.getUserNameAndCopyForUserIds(userIdList); + for (ExamBaseUser vo : list) { + UserEntity userEntity = userMap.get(vo.getUserId()); + if (userEntity != null) { + vo.setUserName(userEntity.getRealName()); + vo.setHeadLogo(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + } + } + } + + + /** + * 查询我的考试列表 + * + * @param req + * @return + */ + @Override + public PageInfo queryMyExamList(QueryExamListReq req) { + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + if (!CourseEnums.ExamType.BASE.getCode().equals(req.getExamType()) + && !CourseEnums.ExamType.POSITION.getCode().equals(req.getExamType())) { + throw new RuntimeException("查询类型错误"); + } + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + QueryMyExamListReq queryMyExamListReq = BeanUtil.copyProperties(req, QueryMyExamListReq.class); + queryMyExamListReq.setUserId(loginUserId); + queryMyExamListReq.setInnerExamIds(searchFrzzExamName(req.getKeyword())); + Page queryPage = baseMapper.pagingQueryMyExamList(page, queryMyExamListReq); + List list = queryPage.getRecords(); + batchFillExamUserInfo(list); + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 查询我的岗位考试列表(完成考试合格且需要考试合格才能鉴定 却未鉴定的岗位) + * + * @return + * @userId 用户ID + */ + @Override + public List queryAlertPostionIdentify(String userId) { + List appExamListVos = baseMapper.queryAlertPostionIdentify(userId); + if (CollectionUtil.isEmpty(appExamListVos)) { + return new ArrayList<>(); + } + List positionIds = new ArrayList<>(); + for (AppExamListVo vo : appExamListVos) { + if (StringUtils.isNotEmpty(vo.getRelationPositionId())) { + positionIds.add(vo.getRelationPositionId()); + } + } + if (CollectionUtil.isEmpty(positionIds)) { + return new ArrayList<>(); + } + + List positionEntityList = userApiV2Util.listPositionDetailInfoByIds(positionIds, null); + List postAndPositionList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(positionEntityList)) { + for (PositionVO positionEntity : positionEntityList) { + PositionEntity postAndPosition = new PositionEntity(); + postAndPosition.setId(positionEntity.getId()); + postAndPosition.setFullName(positionEntity.getFullName()); + postAndPosition.setEnCode(positionEntity.getEnCode());//todo岗位编码 + postAndPositionList.add(postAndPosition); + } + } + return postAndPositionList; + + } + + /** + * 岗位完成鉴定 + * + * @param userId 用户id + * @param positionId 岗位ID + */ + @Override + public void completeIdentifyCallBack(String userId, String positionId) { + log.error("completeIdentifyCallBack={},{}", userId, positionId); + //构建修改wrap + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getCompleteIdentify, 1) + .in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.PASS.getCode(), + CourseEnums.ExamStatus.VERY_PASS.getCode()) + .eq(FtbCultivateExamUser::getRelationPositionId, positionId) + .eq(FtbCultivateExamUser::getUserId, userId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + baseMapper.update(null, wrapper); + + } + + /** + * 批量填充用户考试信息 + * + * @param examUserList + */ + @Override + public void batchFillExamUserInfo(List examUserList) { + List examIds = new ArrayList<>(); + for (AppExamListVo appExamListVo : examUserList) { + if (StringUtils.isNotEmpty(appExamListVo.getExamId())) { + examIds.add(appExamListVo.getExamId()); + } + } + InnerQueryExamResultDto innerQueryExamResultDto = queryCurrExamListByIds(examIds); + Map examMap = innerQueryExamResultDto.getExamMap(); + Map versionBatchMap = innerQueryExamResultDto.getVersionBatchMap(); + + List historyExamIds = new ArrayList<>(); + for (AppExamListVo vo : examUserList) { + FtbCultivateExam exam = examMap.get(vo.getExamId()); + if (null != exam) { + if (exam.getStatus() != null && exam.getStatus().equals(3)) { + vo.setIsNullify(1); + } + vo.setNeedUpdate(exam.getNeedUpdate()); + } + //补充当前考试 + String verKey = vo.getExamId() + vo.getVersionBatch(); + FtbCultivateExam exam1 = versionBatchMap.get(verKey); + if (null != exam1) { + vo.setExamName(exam1.getExamName()); + vo.setExamTime(exam1.getExamTime()); + vo.setInnerExamFlag(0); + FtbCultivateTestPaper paper = null; + if (StringUtils.isNotEmpty(exam1.getPaperInfo())) { + paper = JSONUtil.toBean(exam1.getPaperInfo(), FtbCultivateTestPaper.class); + } else { + paper = paperService.getById(exam.getPaperId()); + } + if (null != paper) { + vo.setPaperName(paper.getName()); + } + vo.setExamTotleScore(exam1.getCurrTotalScore()); + vo.setQuestionNum(exam1.getCurrQuestionNumber()); + } else { + vo.setInnerExamFlag(1); + historyExamIds.add(vo.getExamId()); + } + } + + //补充历史考试 + if (CollectionUtil.isNotEmpty(historyExamIds)) { + Map historyExam = examHistoryPaperService.queryHistoryExamListByIds(examIds); + for (AppExamListVo vo : examUserList) { + String verKey = vo.getExamId() + vo.getVersionBatch(); + FtbCultivateExamHistoryPaper exam2 = historyExam.get(verKey); + if (null != exam2) { + vo.setExamName(exam2.getExamName()); + vo.setExamTime(exam2.getExamTime()); + FtbCultivateTestPaper paper = null; + if (StringUtils.isNotEmpty(exam2.getPaperInfo())) { + paper = JSONUtil.toBean(exam2.getPaperInfo(), FtbCultivateTestPaper.class); + } else { + paper = paperService.getById(exam2.getPaperId()); + } + if (null != paper) { + vo.setPaperName(paper.getName()); + } + if (vo.getStatus() == 1 || vo.getStatus() == 3 || vo.getStatus() == 4 || vo.getStatus() == 5) { + vo.setExamTotleScore(exam2.getCurrTotalScore()); + vo.setQuestionNum(exam2.getCurrQuestionNumber()); + } + } + } + } + + } + + + /** + * 查询当前考试表 + * + * @param examIds + * @return + */ + private InnerQueryExamResultDto queryCurrExamListByIds(List examIds) { + InnerQueryExamResultDto dto = new InnerQueryExamResultDto(); + if (CollectionUtil.isEmpty(examIds)) { + return dto; + } + Map examMap = new HashMap<>(); + Map versionBatchMap = new HashMap<>(); + + List examList = examService.listByIds(examIds); + if (CollectionUtil.isNotEmpty(examList)) { + for (FtbCultivateExam exam : examList) { + examMap.put(exam.getId(), exam); + versionBatchMap.put(exam.getId() + exam.getVersionBatch(), exam); + } + } + dto.setExamMap(examMap); + dto.setVersionBatchMap(versionBatchMap); + return dto; + } + + private List searchFrzzExamName(String keyword) { + List examIds = new ArrayList<>(); + if (StringUtils.isEmpty(keyword)) { + return examIds; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExam::getId, FtbCultivateExam::getExamId) + .like(FtbCultivateExam::getExamName, keyword) + .eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List list = examService.list(wrapper); + if (CollectionUtil.isNotEmpty(list)) { + for (FtbCultivateExam ftbCultivateExam : list) { + examIds.add(ftbCultivateExam.getExamId()); + } + } + examIds.addAll(examHistoryPaperService.searchFrzzExamName(keyword)); + return QuestionAnalysisUtil.uniqueStringList(examIds); + + } + + /** + * 根据用户ID 查询考试列表 + * + * @param userId 用户ID + * @param reqPage + * @return + */ + @Override + public PageInfo queryExamListForUserId(String userId, CultivatePage reqPage) { + + Page page = Page.of(reqPage.getCurrentPage(), reqPage.getPageSize()); + Page queryPage = baseMapper.pagingQueryExamListForUserId(page, userId); + List list = queryPage.getRecords(); + PageInfo pageInfo = new PageInfo<>(); + if (CollectionUtil.isNotEmpty(list)) { + for (AppExamListVo appExamListVo : list) { + fillNeedPassScore(appExamListVo); + } + } + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + private void fillNeedPassScore(AppExamListVo vo) { + //计算用户需要考多少分才及格 + int needScore = 0; + Integer passType = vo.getPassType();//合格分数类型(1固定分,2百分比) + Integer passMark = vo.getPassMark();//合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(passType)) { + needScore = passMark; + } else { + needScore = QuestionAnalysisUtil.calculateScore(passMark, vo.getExamTotleScore()); + } + vo.setNeedExamPassScore(needScore); + + + //计算用户需要考多少分才优秀 + int needExcellentScore = 0; + Integer excellentType = vo.getExcellentType();//优秀分数类型(1固定分,2百分比) + Integer excellentMark = vo.getExcellentMark();//优秀分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(excellentType)) { + needExcellentScore = excellentMark; + } else { + needExcellentScore = QuestionAnalysisUtil.calculateScore(excellentMark, vo.getExamTotleScore()); + } + vo.setNeedExamExcellentScore(needExcellentScore); + } + + /** + * 根据用户ID 和岗位ID 查询用户的考试列表 + * + * @param dto + * @return + */ + public PageInfo queryExamListForUserIdAndPostId(QueryUserExamListDto dto) { + //获取当前用户信息 + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize()); + Page queryPage = new Page<>(); + if (StringUtils.isNotEmpty(dto.getRankId())) { + queryPage = baseMapper.pagingQueryExamList(page, dto); + } else { + queryPage = baseMapper.pagingQueryExamListByPosition(page, dto); + } + List list = queryPage.getRecords(); + PageInfo pageInfo = new PageInfo<>(); + List userExamList = new ArrayList<>(); + //补充考试信息 + if (CollectionUtil.isNotEmpty(list)) { + for (PositionExamDto vo : list) { + vo.setUserId(dto.getUserId()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getExamId, vo.getExamId()) + .eq(FtbCultivateExamUser::getUserId, dto.getUserId()) + .eq(FtbCultivateExamUser::getExamSource, vo.getExamSource()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateExamUser::getCreatorTime); + if (vo.getExamSource().equals(2)) { + wrapper.eq(FtbCultivateExamUser::getRelationPositionExamId, vo.getInnerBuinessId()); + } else { + wrapper.eq(FtbCultivateExamUser::getRelationCourseExamId, vo.getInnerBuinessId()); + } + List examUserList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(examUserList)) { + vo.setStatus(-1); + continue; + } + //补充用户考试数据 + FtbCultivateExamUser user = examUserList.get(0); + + for (FtbCultivateExamUser examUser : examUserList) { + if (examUser.getCreatorTime().after(user.getCreatorTime())) { + user = examUser; + } + } + vo.setStatus(user.getStatus()); + vo.setUserStartTime(user.getUserStartTime()); + vo.setFinishtime(user.getFinishTime()); + vo.setDuration(user.getDuration()); + vo.setUserExamId(user.getId()); + vo.setScore(user.getScore()); + vo.setRelationCourseExamId(user.getRelationCourseExamId()); + vo.setRelationPositionExamId(user.getRelationPositionExamId()); + userExamList.add(vo); + } + userExamList = sortByStatus(userExamList); + + } + + pageInfo.setList(userExamList); + pageInfo.setPageSize(userExamList.size()); + pageInfo.setPageNum(1); + pageInfo.setTotal(userExamList.size()); + return pageInfo; + } + + /** + * 按照状态排序 + * + * @param list + * @return + */ + private List sortByStatus(List list) { + return list.stream() + .sorted(Comparator.comparing(PositionExamDto::getStatus)) + .collect(Collectors.toList()); + } + + + /** + * 查询待我批阅的 + * + * @param req + * @return + */ + @Override + public PageInfo queryWaitMyReadOver(QueryExamListReq req) { + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + if (!CourseEnums.ExamType.BASE.getCode().equals(req.getExamType()) + && !CourseEnums.ExamType.POSITION.getCode().equals(req.getExamType())) { + throw new RuntimeException("查询类型错误"); + } + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + QueryWaitMyExamListReq queryReq = BeanUtil.copyProperties(req, QueryWaitMyExamListReq.class); + List managerUserIds = new ArrayList<>(); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + managerUserIds = innerPowerUserVO.getUserIds(); + } else if (innerPowerUserVO.getCode().equals(2)) { + managerUserIds.add("-1"); + } + Page queryPage = baseMapper.pagingQueryWaitMyExamList(page, queryReq, managerUserIds); + List list = queryPage.getRecords(); + //补充组织信息 和用户信息 + fillExtraInfo(loginUserId, list); + + fillRelationPositionName(list); + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + //异步处理动态为考试权限 +// examAsyncDealUtil.asyncRefreshUserExamPower(userExamUtil.getTenantId(), userExamUtil.getHeadersForLogin()); + return pageInfo; + } + + @Override + public void fillRelationPositionName(List list) { + if (CollectionUtil.isEmpty(list)) { + return; + } + Set positonIdSet = new HashSet<>(); + for (AppExamListVo vo : list) { + if (StringUtils.isNotEmpty(vo.getRelationPositionId())) { + positonIdSet.add(vo.getRelationPositionId()); + } + } + if (CollectionUtil.isEmpty(positonIdSet)) { + return; + } + List positionEntities = personnelOrgUtils.queryPostionInfoForIds(new ArrayList<>(positonIdSet)); + //转换成map + if (CollectionUtil.isEmpty(positionEntities)) { + return; + } + Map positionMap = positionEntities.stream().collect(Collectors.toMap(PositionEntity::getId, Function.identity())); + for (AppExamListVo vo : list) { + if (StringUtils.isNotEmpty(vo.getRelationPositionId())) { + PositionEntity positionEntity = positionMap.get(vo.getRelationPositionId()); + if (positionEntity != null) { + vo.setRelationPositionName(positionEntity.getFullName()); + } + } + } + } + + /** + * 查询我的下属 + * + * @param userId + * @return + */ + private Set queryMyBelowUser(String userId) { + Set userIds = new HashSet<>(); + List listVOS = userApiV2Util.listUnderlingTargetUser(userId, null); + if (CollectionUtil.isNotEmpty(listVOS)) { + userIds = listVOS.stream().map(UserPageListVO::getId).collect(Collectors.toSet()); + } + return userIds; + } + + /** + * 查询我的已批阅 + * + * @param req + * @return + */ + @Override + public PageInfo queryMyCompleteReadOver(QueryExamListReq req) { + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + if (!CourseEnums.ExamType.BASE.getCode().equals(req.getExamType()) + && !CourseEnums.ExamType.POSITION.getCode().equals(req.getExamType())) { + throw new RuntimeException("查询类型错误"); + } + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + QueryMyCompleteExamListReq queryReq = BeanUtil.copyProperties(req, QueryMyCompleteExamListReq.class); + queryReq.setUserId(loginUserId); + Page queryPage = baseMapper.pagingQueryMyCompleteExamList(page, queryReq); + List list = queryPage.getRecords(); + + //补充组织信息 和用户信息 + fillExtraInfo(loginUserId, list); + + fillRelationPositionName(list); + + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 查询下属逾期未考试的考试列表 + * + * @param req + * @return + */ + @Override + public PageInfo expireExam(QueryExpireListReq req) { + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + QueryExpireExamListReq queryReq = BeanUtil.copyProperties(req, QueryExpireExamListReq.class); + queryReq.setUserId(loginUserId); + + List managerUserIds = new ArrayList<>(); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + managerUserIds = innerPowerUserVO.getUserIds(); + } else if (innerPowerUserVO.getCode().equals(2)) { + managerUserIds.add("-1"); + } + + Page queryPage = baseMapper.pagingQueryExpireExamList(page, queryReq, managerUserIds); + List list = queryPage.getRecords(); + //补充组织信息 和用户信息 + fillExtraInfo(loginUserId, list); + fillRelationPositionName(list); + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public void fillExtraInfo(String loginUserId, List list) { + if (CollectionUtil.isNotEmpty(list)) { + //补充组织信息 和用户信息 + for (AppExamListVo appExamListVo : list) { + List userOrganizationList = queryUserOrganization(appExamListVo.getUserId()); + appExamListVo.setOrganizationVoList(userOrganizationList); + } + fillExamUserInfo(list); + //检测是否是我的下属 + Set queryMyBelowUser = queryMyBelowUser(loginUserId); + for (AppExamListVo appExamListVo : list) { + if (queryMyBelowUser.contains(appExamListVo.getUserId())) { + appExamListVo.setIsMyBelow(true); + } else { + appExamListVo.setIsMyBelow(false); + } + } + } + } + + /** + * 清楚逾期未考试的考试记录 + * + * @param userExamIds 用户考试记录id集合 + */ + @Override + public void clearExpireExam(String userExamIds) { + if (StringUtils.isEmpty(userExamIds)) { + return; + } + List userExamIdsList = Arrays.asList(userExamIds.split(",")); + if (CollectionUtil.isEmpty(userExamIdsList)) { + return; + } + //1、查询 + List examUserList = baseMapper.selectList(Wrappers.lambdaQuery() + .in(FtbCultivateExamUser::getId, userExamIdsList) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .eq(FtbCultivateExamUser::getAppShow, CourseEnums.ExamRecordDisplay.SHOW.getCode()) + .eq(FtbCultivateExamUser::getExamType, CourseEnums.ExamType.BASE.getCode()) + ); + if (CollectionUtil.isEmpty(examUserList)) { + throw new RuntimeException("未查询到相关逾期考试记录"); + } + //2、批量删除已经过期的 + Date now = new Date(); + List deleteExamUserList = new ArrayList<>(); + examUserList.forEach(examUser -> { + if (CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + examUser.setAppShow(CourseEnums.ExamRecordDisplay.HIDDEN.getCode()); + deleteExamUserList.add(examUser); + } + }); + //批量删除 + if (CollectionUtil.isNotEmpty(deleteExamUserList)) { + saveOrUpdateBatch(deleteExamUserList); + } + } + + /** + * 触发考试 + * + * @param req + * @return + */ + @Override + public Boolean triggerPositionExam(TriggerExamDto req) { + + log.error("临时日志,触发考试={}", JSONUtil.toJsonStr(req)); + + + //1、查看考试信息 + FtbCultivateExam exam = examService.queryExamInfo(req.getExamId()); + if (CourseEnums.RelationExamSource.COURSE_RELATED_EXAM.getCode().equals(req.getExamSource())) { + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, req.getUserId()) + .eq(FtbCultivateExamUser::getExamSource, req.getExamSource()) + .eq(FtbCultivateExamUser::getExamId, req.getExamId()) + .eq(StringUtils.isNotEmpty(req.getRelationPositionId()), FtbCultivateExamUser::getRelationPositionId, req.getRelationPositionId()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + + wrapper.eq(FtbCultivateExamUser::getRelationCourseExamId, req.getRelationId()); + + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + return true; + } + } else if (CourseEnums.RelationExamSource.JOB_LEARNING_RELATED_EXAM.getCode().equals(req.getExamSource())) { + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, req.getUserId()) + .eq(FtbCultivateExamUser::getExamSource, req.getExamSource()) + .eq(FtbCultivateExamUser::getRelationPositionId, req.getRelationPositionId()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + log.error("用户{},岗位={},用户已经有这个岗位的考试了", req.getUserId(), req.getRelationPositionId()); + return true; + } + } else if (CourseEnums.RelationExamSource.TASK_EXAM.getCode().equals(req.getExamSource())) { + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, req.getUserId()) + .eq(FtbCultivateExamUser::getExamSource, req.getExamSource()) + .eq(FtbCultivateExamUser::getRelationTaskId, req.getRelationTaskId()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + log.error("用户{},taskId={},用户已经有这个岗位的考试了", req.getUserId(), req.getRelationTaskId()); + return true; + } + } + + //2、没有就写入一条记录 + FtbCultivateExamUser user = FtbCultivateExamUser.builder() + .examId(req.getExamId()) + .examType(exam.getExamType()) + .userId(req.getUserId()) + .status(CourseEnums.ExamStatus.WAIT.getCode()) + .paperId(exam.getPaperId()) + .examSource(req.getExamSource()) + .relationPositionId(req.getRelationPositionId()) + .relationRankId(req.getRelationRankId()) + .relationTaskId(req.getRelationTaskId()) + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .batch(UUID.randomUUID().toString().replaceAll("-", "")) + .userExamCount(1) + .build(); + if (CourseEnums.RelationExamSource.COURSE_RELATED_EXAM.getCode().equals(req.getExamSource())) { + user.setRelationCourseExamId(req.getRelationId()); + } else if (CourseEnums.RelationExamSource.JOB_LEARNING_RELATED_EXAM.getCode().equals(req.getExamSource())) { + user.setRelationPositionExamId(req.getRelationId()); + } else if (CourseEnums.RelationExamSource.TASK_EXAM.getCode().equals(req.getExamSource())) { + user.setRelationTaskId(req.getRelationTaskId()); + } + //调整根据权限来确定批阅人 +// examService.fillRevierwer(user, exam); + //查询用户所属组织 +// List selectOrgList = organizeApi.getOrganizeIdsByUserId(req.getUserId()); +// if (CollectionUtil.isNotEmpty(selectOrgList)) { +// user.setUserOrgList(String.join(",", selectOrgList)); +// } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userPrimaryBoundOne != null && userPrimaryBoundOne.getOrganizeId() != null) { + user.setUserOrgList(userPrimaryBoundOne.getOrganizeId()); + } + user.setVersionBatch(exam.getVersionBatch()); + baseMapper.insert(user); + return true; + } + + /** + * 取消学习了 但是未考试的用户数据 + * + * @param req + * @return + */ + @Override + public Boolean cancelPositionExam(InnerExamDto req) { + + log.error("取消未完成考试的用户数据={}", JSONUtil.toJsonStr(req)); + if (StringUtils.isEmpty(req.getExamId())) { + throw new RuntimeException("取消未完成的用户考试,考试ID为空"); + } + if (StringUtils.isEmpty(req.getRelationRankId())) { + throw new RuntimeException("取消未完成的用户考试,岗位为空"); + } + if (StringUtils.isEmpty(req.getRelationId())) { + throw new RuntimeException("取消未完成的用户考试,关联业务ID为空"); + } + if (null == req.getExamSource()) { + throw new RuntimeException("取消未完成的用户考试,考试来源为空"); + } + + //构建修改wrap + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode()) + .eq(FtbCultivateExamUser::getExamSource, req.getExamSource()) + .eq(FtbCultivateExamUser::getRelationPositionId, req.getRelationRankId()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + if (CourseEnums.RelationExamSource.COURSE_RELATED_EXAM.getCode().equals(req.getExamSource())) { + wrapper.eq(FtbCultivateExamUser::getRelationCourseExamId, req.getRelationId()); + } else { + wrapper.eq(FtbCultivateExamUser::getRelationPositionExamId, req.getRelationId()); + } + baseMapper.update(null, wrapper); + if (CourseEnums.RelationExamSource.JOB_LEARNING_RELATED_EXAM.getCode().equals(req.getExamSource())) { + LambdaUpdateWrapper wrapper1 = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getExamIdentifyConfig, 0) + .eq(FtbCultivateExamUser::getRelationPositionId, req.getRelationRankId()) + .eq(FtbCultivateExamUser::getExamId, req.getRelationId()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + baseMapper.update(null, wrapper1); + } + + return true; + } + + + /** + * 查询用户已完成的考试列表 + * + * @param userId 用户id + * @return + */ + @Override + public List queryCompleteExamListForUserId(String userId) { + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, userId) + .notIn(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode(), CourseEnums.ExamStatus.OVERDUE.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List examUserList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(examUserList)) { + return new ArrayList<>(); + } + List list = new ArrayList<>(); + for (FtbCultivateExamUser examUser : examUserList) { + UserExamDetailVo vo = new UserExamDetailVo(); + vo.setId(examUser.getId()); + vo.setScore(examUser.getScore()); + vo.setExamId(examUser.getExamId()); + vo.setDuration(examUser.getDuration()); + vo.setUserId(examUser.getUserId()); + vo.setStatus(examUser.getStatus()); + vo.setCorrectCount(examUser.getCorrectCount()); + vo.setPaperId(examUser.getPaperId()); + vo.setUserStartTime(examUser.getUserStartTime()); + vo.setStartTime(examUser.getStartTime()); + vo.setEndTime(examUser.getEndTime()); + vo.setFinishtime(examUser.getFinishTime()); + //2、查询考试信息和试卷信息 + FtbCultivateTestPaper paper = paperService.getById(examUser.getPaperId()); + vo.setExamTotleScore(paper.getTotalScore()); + vo.setQuestionNumber(paper.getQuestionNumber()); + vo.setPaperName(paper.getName()); + FtbCultivateExam exam = examService.getById(examUser.getExamId()); + vo.setExamName(exam.getExamName()); + vo.setExamType(exam.getExamType()); + vo.setExamTime(exam.getExamTime()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setExamSource(examUser.getExamSource()); + vo.setRelationRankId(examUser.getRelationRankId()); + vo.setRelationPositionExamId(examUser.getRelationPositionExamId()); + vo.setRelationCourseExamId(examUser.getRelationCourseExamId()); + vo.setRelationPositionId(examUser.getRelationPositionId()); + if (CourseEnums.ExamType.POSITION.getCode().equals(examUser.getExamType()) && StringUtils.isNotEmpty(examUser.getRelationPositionId())) { + + PositionVO positionVO = userApiV2Util.infoPosition(examUser.getRelationPositionId(), null); + if (null != positionVO) { + vo.setCurrPostName(positionVO.getFullName()); + } + } + + list.add(vo); + } + return list; + } + + /** + * 重新考试 + * + * @param userExamId 用户考试ID + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public FtbCultivateExamUser reStartExam(String userExamId) { + + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + //查看用户考试信息 + FtbCultivateExamUser examUserRecord = baseMapper.selectOne(new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, loginUserId) + .eq(FtbCultivateExamUser::getId, userExamId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + if (null == examUserRecord) { + throw new RuntimeException("用户考试信息不存在"); + } + + //查看考试信息 + FtbCultivateExam exam = examService.queryExamInfo(examUserRecord.getExamId()); + checkCanReExam(examUserRecord, exam); + //没有就写入一条记录 + FtbCultivateExamUser user = FtbCultivateExamUser.builder() + .examId(exam.getId()) + .examType(exam.getExamType()) + .userId(loginUserId) + .status(CourseEnums.ExamStatus.WAIT.getCode()) + .userStartTime(new Date()) + .reviewerUserIds(examUserRecord.getReviewerUserIds()) + .reviewerRoleId(examUserRecord.getReviewerRoleId()) + .paperId(exam.getPaperId()) + .examSource(examUserRecord.getExamSource()) + .relationRankId(examUserRecord.getRelationRankId()) + .relationPositionExamId(examUserRecord.getRelationPositionExamId()) + .relationPositionId(examUserRecord.getRelationPositionId()) + .relationCourseExamId(examUserRecord.getRelationCourseExamId()) + .relationTaskId(examUserRecord.getRelationTaskId()) + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .batch(UUID.randomUUID().toString().replaceAll("-", "")) + .build(); + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + if (exam.getStartTime() != null) { + user.setStartTime(exam.getStartTime()); + } + if (exam.getEndTime() != null) { + user.setEndTime(exam.getEndTime()); + } + } + if (exam.getExamType().equals(0)) { + user.setVersionBatch(exam.getVersionBatch()); + } + //查询用户所属组织 +// List selectOrgList = organizeApi.getOrganizeIdsByUserId(examUserRecord.getUserId()); +// if (CollectionUtil.isNotEmpty(selectOrgList)) { +// user.setUserOrgList(String.join(",", selectOrgList)); +// } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(examUserRecord.getUserId(), null); + if (userPrimaryBoundOne != null && userPrimaryBoundOne.getOrganizeId() != null) { + user.setUserOrgList(userPrimaryBoundOne.getOrganizeId()); + } + user.setId(examUserRecord.getId()); + user.setAppShow(0); + user.setDuration(0L); + user.setScore(0); + user.setAutoScore(0); + user.setReadOverScore(0); + user.setReviewerUserId(""); + if (examUserRecord.getStatus().equals(CourseEnums.ExamStatus.WAIT.getCode()) || examUserRecord.getStatus().equals(CourseEnums.ExamStatus.OVERDUE.getCode())) { + user.setUserExamCount(examUserRecord.getUserExamCount()); + } else { + user.setUserExamCount(examUserRecord.getUserExamCount() + 1); + } + user.setUserExamResultValid(0);//用户考试结果是否有效:0-有效,1-超过切屏次数视为作弊,2-超过考试时间无交互视为作弊 + baseMapper.updateById(user); + + //删除考试记录 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateExamUserDetail::getBatch, examUserRecord.getBatch()) + .eq(FtbCultivateExamUserDetail::getUserExamId, examUserRecord.getId()) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + userDetailService.update(updateWrapper); + return user; + } + + /** + * 查询用户考试总数和已完成数 + * + * @param userIds + */ + @Override + public Map queryExamTotalAndCompleteNumForUserIds(List userIds) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, + FtbCultivateExamUser::getRelationCourseExamId, FtbCultivateExamUser::getRelationPositionExamId, FtbCultivateExamUser::getRelationRankId) + .in(FtbCultivateExamUser::getUserId, userIds) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List examUserList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(examUserList)) { + return new HashMap<>(); + } + //转成成 userId ->map + Map> userIdToListMap = examUserList.stream() + .collect(Collectors.groupingBy(FtbCultivateExamUser::getUserId)); + Map map = new HashMap<>(); + userIdToListMap.forEach((userId, list) -> { + map.put(userId, QuestionAnalysisUtil.countCompleteAndTotleExamNum(list)); + }); + return map; + } + + + /** + * 统计合格率 + * + * @param req + * @return + */ + @Override + public ExamStatisticsVo statistics(FtbCultivateStatisticsDTO req) { + //1、查询组织 + List selectOrgList = new ArrayList<>(); + if (StringUtils.isEmpty(req.getOrgId())) { + return new ExamStatisticsVo(); + } + + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return new ExamStatisticsVo(); + } + + if (!powerOrgList.contains(req.getOrgId())) { + return new ExamStatisticsVo(); + } + selectOrgList.add(req.getOrgId()); + if (req.getIsSelectNext() == 1) { + List childrenById = userApiV2Util.organizesOrHaveChildByOrganizeIds(List.of(req.getOrgId()), true, null); +// List childrenById = organizeApi.getChildrenById(req.getOrgId()); + if (CollectionUtil.isNotEmpty(childrenById)) { + selectOrgList.addAll(childrenById.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + } + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, selectOrgList); + if (CollUtil.isEmpty(intersection)) { + return new ExamStatisticsVo(); + } + selectOrgList = userExamUtil.uniqueStringList(intersection); + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(selectOrgList, null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return new ExamStatisticsVo(); + } + + + InnerCultivateStatisticsDTO innerCultivateStatisticsDTO = new InnerCultivateStatisticsDTO(); + innerCultivateStatisticsDTO.setStartTime(req.getStartTime()); + innerCultivateStatisticsDTO.setEndTime(req.getEndTime()); + innerCultivateStatisticsDTO.setSelectUserIds(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + //2、根据用户ID查询考试信息 当前时间段 + List currExamUserList = baseMapper.webStatisticsForOrg(innerCultivateStatisticsDTO); + StatisticsResultDto curr = QuestionAnalysisUtil.statisticeLv(currExamUserList); + //3、查询整体的,应该是不需要查询整体的去掉 + innerCultivateStatisticsDTO.setStartTime(DateUtil.beginOfYear(req.getStartTime())); + innerCultivateStatisticsDTO.setEndTime(DateUtil.endOfYear(req.getEndTime())); + List allExamUserList = baseMapper.webStatisticsForOrg(innerCultivateStatisticsDTO); + StatisticsResultDto all = QuestionAnalysisUtil.statisticeLv(allExamUserList); + ExamStatisticsVo vo = new ExamStatisticsVo(); + + vo.setTotleRate(String.valueOf(all.getPassLv())); + vo.setTotleNum(String.valueOf(all.getTotle())); + vo.setPassNum(String.valueOf(all.getPass())); + vo.setPassRate(String.valueOf(all.getPassLv())); + vo.setNoPassNum(String.valueOf(all.getNoPass())); + vo.setNoPassRate(String.valueOf(all.getNoPassLv())); + + + vo.setCurrPassRate(String.valueOf(curr.getPassLv())); + vo.setCurrExamTotleNum(String.valueOf(curr.getTotle())); + vo.setCurrExamPassNum(String.valueOf(curr.getPass())); + vo.setCurrExamNoPassNum(String.valueOf(curr.getNoPass())); + vo.setCurrExamNoPassRate(String.valueOf(curr.getNoPassLv())); + return vo; + } + + /** + * 统计合格率 + * + * @param req + * @return + */ + @Override + public ExamStatisticsVo statisticsNew(FtbCultivateStatisticsDTO req) { + + //1、根据组织查询用户列表 + ExamStatisticsVo vo = new ExamStatisticsVo(); + + //2、根据用户ID查询考试信息 当前时间段 + Date start = req.getStartTime(); + Date end = req.getEndTime(); + + InnerCultivateStatisticsDTO dto = new InnerCultivateStatisticsDTO(); + dto.setStartTime(start); + dto.setEndTime(end); + dto.setSelectOrgList(req.getOrgIds()); + dto.setSelectUserIds(req.getUserIds()); + List examUserList = examUserMapper.webStatisticsForOrg(dto); + StatisticsResultDto curr = QuestionAnalysisUtil.statisticeLv(examUserList); + + + vo.setCurrPassRate(String.valueOf(curr.getPassLv())); + vo.setCurrExamTotleNum(String.valueOf(curr.getTotle())); + vo.setCurrExamPassNum(String.valueOf(curr.getPass())); + vo.setCurrExamNoPassNum(String.valueOf(curr.getNoPass())); + vo.setCurrExamNoPassRate(String.valueOf(curr.getNoPassLv())); + return vo; + } + + /** + * 统计考试排名 + * + * @param req + * @return + */ + + @Override + public List statisticsExamRankingList(FtbCultivateStatisticsDTO req) { + //1、查询组织 + + if (StringUtils.isEmpty(req.getOrgId())) { + return new ArrayList<>(); + } + + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return new ArrayList<>(); + } + + if (!powerOrgList.contains(req.getOrgId())) { + return new ArrayList<>(); + } + List selectOrgList = new ArrayList<>(); + selectOrgList.add(req.getOrgId()); + if (req.getIsSelectNext() == 1) { + List childrenById = userApiV2Util.organizesOrHaveChildByOrganizeIds(List.of(req.getOrgId()), true, null); +// List childrenById = organizeApi.getChildrenById(req.getOrgId()); + if (CollectionUtil.isNotEmpty(childrenById)) { + selectOrgList.addAll(childrenById.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + } + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, selectOrgList); + if (CollUtil.isEmpty(intersection)) { + return new ArrayList<>(); + } + selectOrgList = intersection; + selectOrgList = userExamUtil.uniqueStringList(selectOrgList); + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(selectOrgList, null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return new ArrayList<>(); + } + + InnerCultivateStatisticsDTO innerCultivateStatisticsDTO = new InnerCultivateStatisticsDTO(); + innerCultivateStatisticsDTO.setStartTime(req.getStartTime()); + innerCultivateStatisticsDTO.setEndTime(req.getEndTime()); +// innerCultivateStatisticsDTO.setSelectOrgList(selectOrgList); + innerCultivateStatisticsDTO.setSelectUserIds(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + //根据用户分组 查询平均分排行榜 + List userRankingVoList = baseMapper.queryAvgScore(innerCultivateStatisticsDTO); + fillExamUserInfo(userRankingVoList); + return userRankingVoList; + } + + + /** + * 统计考试排名(不按照权限过滤) + * + * @param req 请求参数 + * @return 考试排名 + */ + + @Override + public List statisticsExamRankingListNoPower(FtbCultivateStatisticsDTO req) { + InnerCultivateStatisticsDTO innerCultivateStatisticsDTO = new InnerCultivateStatisticsDTO(); + innerCultivateStatisticsDTO.setStartTime(req.getStartTime()); + innerCultivateStatisticsDTO.setEndTime(req.getEndTime()); + innerCultivateStatisticsDTO.setSelectUserIds(req.getUserIds()); + //根据用户分组 查询平均分排行榜 + List userRankingVoList = baseMapper.queryAvgScore(innerCultivateStatisticsDTO); + fillExamUserInfo(userRankingVoList); + return userRankingVoList; + } + + + /** + * 查询用户考试信息 未考试的不记入合格率 + * + * @param userIds 用户ID + * @param start 开始时间 + * @param end 结束时间 + * @return + */ + private List queryUserExamForDate(List userIds, Date start, Date end) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, + FtbCultivateExamUser::getRelationCourseExamId, FtbCultivateExamUser::getRelationPositionExamId, FtbCultivateExamUser::getRelationRankId, FtbCultivateExamUser::getPaperId) + .in(FtbCultivateExamUser::getUserId, userIds) + .notIn(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode(), CourseEnums.ExamStatus.WAIT.getCode()) + .between(null != start && null != end, FtbCultivateExamUser::getFinishTime, start, end) + .eq(FtbCultivateExamUser::getIsJoinCount, ExamUserStatisticsEnums.PARTICIPATE.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return baseMapper.selectList(wrapper); + + } + + + /** + * 检测并修改过期考试 + * + * @return + */ + @Override + public Integer checkAndUpdateExpireUserExamStatus() { + //构建修改wrap + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode()) + .set(FtbCultivateExamUser::getLastModifyTime, new Date()) + .lt(FtbCultivateExamUser::getEndTime, new Date()) + .eq(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode()) + .eq(FtbCultivateExamUser::getExamType, CourseEnums.ExamType.BASE.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return baseMapper.update(null, wrapper); + } + + /** + * 查询我的考试数量,统计数量 + * + * @return + */ + @Override + public MyBlowExamNum queryMyBlowExamNum() { + String loginUserId = UserProvider.getLoginUserId(); + QueryMyCompleteExamListReq completeReq = new QueryMyCompleteExamListReq(); + completeReq.setUserId(loginUserId); + + QueryWaitMyExamListReq waitReq = new QueryWaitMyExamListReq(); + waitReq.setUserId(loginUserId); + + QueryExpireExamListReq expireReq = new QueryExpireExamListReq(); + expireReq.setUserId(loginUserId); + List userRole = roleApi.getByUserId(loginUserId); + if (CollectionUtil.isNotEmpty(userRole)) { + //获取角色id列表 + for (RoleEntity roleEntity : userRole) { + expireReq.getRoleIdList().add(roleEntity.getId()); + waitReq.getRoleIdList().add(roleEntity.getId()); + } + } + //查询过期 + int expire = baseMapper.queryExpireExamNum(expireReq); + + //查询完成的 + int complete = baseMapper.queryMyCompleteExamNum(completeReq); + + //查询带我批阅的 + int wait = baseMapper.queryWaitMyExamNum(waitReq); + + return new MyBlowExamNum(wait, complete, expire); + } + + /** + * @param userId 用户ID + * @param positionId 岗位ID + * @param settings 是否开启合格设置 1是 0 否 + * @return [1:已经完成考试或者合格 2:未完成考试, 3:考试未合格] + */ + @Override + public Integer queryExamIsCompleteForUserIdAndPostion(String userId, String positionId, Integer settings) { +// LambdaQueryWrapper wrapper = new LambdaQueryWrapper() +// .eq(FtbCultivateExamUser::getUserId, userId) +// .eq(FtbCultivateExamUser::getRelationPositionId, positionId) +// .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) +// .orderByDesc(FtbCultivateExamUser::getCreatorTime).last("limit 1"); +// FtbCultivateExamUser examUser = baseMapper.selectOne(wrapper); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, userId) + .eq(FtbCultivateExamUser::getRelationPositionId, positionId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .eq(FtbCultivateExamUser::getExamSource, 2) + .orderByDesc(FtbCultivateExamUser::getCreatorTime).last("limit 1"); + FtbCultivateExamUser examUser = baseMapper.selectOne(wrapper); + if (null == examUser) { + return 0; + } + //(0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + if (examUser.getStatus() == 0 || examUser.getStatus() == 2 || examUser.getStatus() == 1) { + return 2; + } + if (1 == settings && (examUser.getStatus() == 4)) { + return 3; + } + return 1; + } + + /** + * 查询 + * + * @param str 用户考试记录id + * @param learningMapSetting 是否开启合格设置 1是 0 否 + * @return (0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + */ + @Override + public Integer queryExamIsCompleteForUserIdAndPostionWithNew(String str, Integer learningMapSetting) { + FtbCultivateExamUser examUser = baseMapper.selectById(str); + if (null == examUser) { + return 0; + } + //(0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + if (examUser.getStatus() == 0 || examUser.getStatus() == 2 || examUser.getStatus() == 1) { + return 2; + } + if (1 == learningMapSetting && (examUser.getStatus() == 4)) { + return 3; + } + return 1; + } + + @Override + @Transactional + public void webRestartExam(WebRestartExamReq req) { + FtbCultivateExam exam = examService.queryExamInfo(req.getExamId()); + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + if (null == req.getStartTime()) { + throw new RuntimeException("开始考试时间不能为空"); + } else if (null == req.getEndTime()) { + throw new RuntimeException("结束考试时间不能为空"); + } + if (req.getStartTime().after(req.getEndTime())) { + throw new RuntimeException("考试开始时间不能大于结束时间"); + } + } + if (CollectionUtil.isEmpty(req.getUserExamIdList())) { + throw new RuntimeException("请选择需要重考的员工"); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getExamId, exam.getId()) + .in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode(), CourseEnums.ExamStatus.OVERDUE.getCode(), CourseEnums.ExamStatus.NO_PASS.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .in(FtbCultivateExamUser::getId, req.getUserExamIdList()) + .orderByDesc(FtbCultivateExamUser::getCreatorTime); + List userExamList = baseMapper.selectList(wrapper); + + + if (CollectionUtil.isEmpty(userExamList)) { + return; + } + for (FtbCultivateExamUser oldExamUser : userExamList) { + FtbCultivateExamUser examUser = new FtbCultivateExamUser(); + examUser.setId(oldExamUser.getId()); + examUser.setExamType(exam.getExamType()); + examUser.setPaperId(exam.getPaperId()); + examUser.setDuration(0L); + examUser.setScore(0); + examUser.setAutoScore(0); + examUser.setReadOverScore(0); + examUser.setReviewerUserId(""); + examUser.setStatus(CourseEnums.ExamStatus.WAIT.getCode()); + examUser.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + examUser.setAppShow(0); + examUser.setUserExamResultValid(0); + examUser.setUserId(oldExamUser.getUserId()); + examUser.setTotalScore(0); + examUser.setQuestionNumber(0); + examUser.setRemark(null); + examUser.setBatch(UUID.randomUUID().toString().replaceAll("-", "")); + if (oldExamUser.getStatus().equals(CourseEnums.ExamStatus.WAIT.getCode()) || oldExamUser.getStatus().equals(CourseEnums.ExamStatus.OVERDUE.getCode())) { + examUser.setUserExamCount(oldExamUser.getUserExamCount()); + } else { + examUser.setUserExamCount(oldExamUser.getUserExamCount() + 1); + } + examUser.setUserExamResultValid(0);//用户考试结果是否有效:0-有效,1-超过切屏次数视为作弊,2-超过考试时间无交互视为作弊 + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + examUser.setStartTime(req.getStartTime()); + examUser.setEndTime(req.getEndTime()); + } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(examUser.getUserId(), null); +// List selectOrgList = organizeApi.getOrganizeIdsByUserId(examUser.getUserId()); + if (userPrimaryBoundOne != null && userPrimaryBoundOne.getOrganizeId() != null) { + examUser.setUserOrgList(userPrimaryBoundOne.getOrganizeId()); + } + examUser.setVersionBatch(exam.getVersionBatch()); + baseMapper.updateById(examUser); + //删除考试记录 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateExamUserDetail::getBatch, oldExamUser.getBatch()) + .eq(FtbCultivateExamUserDetail::getUserExamId, oldExamUser.getId()) + .eq(FtbCultivateExamUserDetail::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + userDetailService.update(updateWrapper); + } + } + + /** + * 视为作弊提交试卷 + * + * @param cheatType 作弊类型 1-超过切屏次数视为作弊,2-超过考试时间无交互视为作弊 + * @param userExamId 用户考试ID + * @param req + * @return + */ + @Override + public void setCheat(String userExamId, Integer cheatType, SubExamQuestionReq req) throws Exception { + ExamConfigEnums.ExamValidity cheatTypeEnum = ExamConfigEnums.ExamValidity.fromCode(cheatType); + if (null == cheatTypeEnum || cheatTypeEnum == ExamConfigEnums.ExamValidity.VALID) { + throw new RuntimeException("作弊类型参数错误"); + } + if (CollectionUtil.isEmpty(req.getAnswerDetail())) { + throw new RuntimeException("提交的答案信息为空"); + } + if (null == req.getDuration()) { + throw new RuntimeException("考试时长为空"); + } + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, loginUserId) + .eq(FtbCultivateExamUser::getId, userExamId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCultivateExamUser examUser = baseMapper.selectOne(wrapper); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(examUser.getEnabledMark())) { + throw new RuntimeException("参加考试的人员考试取消"); + } + // 查询考试作弊配置 + FtbCultivateExamSettingEntity setting = cultivateExamSettingService.getById(ConstantUtil.STR_TRUE); + if (null == setting) { + throw new Exception("查询考试配置失败"); + } + //查看考试信息 + // FtbCultivateExam exam = examService.queryExamInfo(examUser.getExamId()); + if (cheatTypeEnum == ExamConfigEnums.ExamValidity.CHEATING_SCREEN_LIMIT_EXCEEDED) { + if (setting.getBanScreenshot() == ConstantUtil.NUM_FALSE) { + //未开启切屏防作弊 + log.error("未开启切屏防作弊,userExamId={}", userExamId); + return; + } + } else if (cheatTypeEnum == ExamConfigEnums.ExamValidity.CHEATING_NO_INTERACTION_TIME_EXCEEDED) { + if (setting.getNoInteractTimeout() == ConstantUtil.NUM_FALSE) { + //未开启超时无交互退出是作弊 + log.error("未开启超时无交互退出是作弊,userExamId={}", userExamId); + return; + } + } + // 检测是否可以提交考试 + // checkSubmitExam(examUser, exam); + //修改考试题目信息 + Date now = new Date(); + FtbCultivateExamUser updateExamUser = new FtbCultivateExamUser(); + updateExamUser.setId(examUser.getId()); + updateExamUser.setUserId(examUser.getUserId()); + updateExamUser.setCorrectCount(0); + updateExamUser.setAutoScore(0); + updateExamUser.setScore(0); + updateExamUser.setFinishTime(now); + updateExamUser.setUserStartTime(new Date(now.getTime() - req.getDuration() * 1000)); + updateExamUser.setDuration(req.getDuration()); + updateExamUser.setStatus(CourseEnums.ExamStatus.NO_PASS.getCode()); + updateExamUser.setUserExamResultValid(cheatTypeEnum.getCode()); + updateExamUser.setLastModifyTime(new Date()); + updateExamUser.setQuestionNumber(examUser.getQuestionNumber()); + updateExamUser.setTotalScore(examUser.getTotalScore()); + updateExamUser.setRemark(req.getRemark()); + baseMapper.updateById(updateExamUser); + } + + /** + * 用户考试的批阅人权限 + */ + @Override + public void refreshUserExamPower() { + //查询所有待批阅的 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT_CHECK.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateExamUser::getCreatorTime); + List waitUserExamList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(waitUserExamList)) { + return; + } + //获取所有的考试id + List examIds = new ArrayList<>(); + for (FtbCultivateExamUser examUser : waitUserExamList) { + examIds.add(examUser.getExamId()); + } + examIds = userExamUtil.uniqueStringList(examIds); + //查询考试信息 + List examList = examService.listByIds(examIds); + Map examIdMap = new HashMap<>();//考试id-考试信息 + + for (FtbCultivateExam exam : examList) { + examIdMap.put(exam.getId(), exam); + } + + //处理每一个考试的动态批阅人 + List plExamUserList = new ArrayList<>(); + PermissionsCacheDTO permissionsCacheDTO = personnelPerUtils.doQueryUserPermissions(List.of("70")); + for (FtbCultivateExamUser oldUserExam : waitUserExamList) { + FtbCultivateExam exam = examIdMap.get(oldUserExam.getExamId()); + FtbCultivateExamUser examUser = new FtbCultivateExamUser(); + examUser.setId(oldUserExam.getId()); + examUser.setUserId(oldUserExam.getUserId()); + //填充批阅人信息 和阅卷角色 + if (null != exam) { + examService.fillRevierwerNew(examUser, exam, permissionsCacheDTO); + } else { + examUser.setReviewerUserIds(oldUserExam.getReviewerUserIds()); + } + + if (StringUtils.isNotEmpty(examUser.getReviewerUserIds()) && examUser.getReviewerUserIds().equals(oldUserExam.getReviewerUserIds())) { + continue; + } + plExamUserList.add(examUser); + } + log.info("异步修改用户权限:={}", JSONUtil.toJsonStr(plExamUserList)); + + if (CollectionUtil.isNotEmpty(plExamUserList)) { + updateBatchById(plExamUserList); + } + + + } + + /** + * app批阅考试查询考试信息(简单) + * + * @param userExamId 用户考试记录id + * @return + */ + @Override + public UserExamDetailVo queryUserExamSimpleMsg(String userExamId) { + UserExamDetailVo vo = new UserExamDetailVo(); + //查询用户的考试汇总信息 + FtbCultivateExamUser examUser = queryAndCheckUserExam(userExamId); + //查询考试信息 + FtbCultivateExam exam = examService.getById(examUser.getExamId()); + if (exam.getStatus().equals(3)) { + vo.setIsNullify(1); + } else { + vo.setIsNullify(0); + } + if (examUser.getVersionBatch().equals(exam.getVersionBatch())) { + if (CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) || CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + vo.setNeedUpdate(exam.getNeedUpdate()); + } + } + exam = userExamUtil.checkAndUpdateExam(exam, examUser.getVersionBatch()); + vo.setId(examUser.getId()); + vo.setScore(examUser.getScore()); + vo.setExamId(examUser.getExamId()); + vo.setDuration(examUser.getDuration()); + vo.setUserId(examUser.getUserId()); + vo.setStatus(examUser.getStatus()); + vo.setReadOverScore(examUser.getReadOverScore()); + vo.setAutoScore(examUser.getAutoScore()); + vo.setCorrectCount(examUser.getCorrectCount()); + vo.setPaperId(examUser.getPaperId()); + vo.setUserStartTime(examUser.getUserStartTime()); + vo.setStartTime(examUser.getStartTime()); + vo.setEndTime(examUser.getEndTime()); + vo.setFinishtime(examUser.getFinishTime()); + vo.setBatch(examUser.getBatch()); + vo.setExamSource(examUser.getExamSource()); + vo.setRelationPositionExamId(examUser.getRelationPositionExamId()); + vo.setRelationCourseExamId(examUser.getRelationCourseExamId()); + vo.setRelationRankId(examUser.getRelationRankId()); + //查询考试信息和试卷信息 + + if (CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) || CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + vo.setExamTotleScore(exam.getCurrTotalScore()); + vo.setQuestionNumber(exam.getCurrQuestionNumber()); + } else { + vo.setExamTotleScore(examUser.getTotalScore()); + vo.setQuestionNumber(examUser.getQuestionNumber()); + } + + vo.setPaperName(exam.getPaperName()); + vo.setExamName(exam.getExamName()); + vo.setExamType(exam.getExamType()); + vo.setExamTime(exam.getExamTime()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setDescription(exam.getDescription()); + //补充用户信息 + fillUserInfoForUserId(vo); + if (exam.getStatus().equals(3)) { + vo.setIsNullify(1); + } + + + //合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(exam.getPassType())) { + vo.setPassMark(exam.getPassMark()); + } else { + vo.setPassMark(examUser.getTotalScore() * exam.getPassMark() / 100); + } + //优秀分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(exam.getExcellentType())) { + vo.setExcellentMark(exam.getExcellentMark()); + } else { + vo.setExcellentMark(examUser.getTotalScore() * exam.getExcellentMark() / 100); + } + //常规考试 根据时间判断状态 + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + Date currentTime = new Date(); + if (examUser.getStartTime() != null && examUser.getEndTime() != null) { + if (currentTime.before(examUser.getStartTime())) { + //考试还未开始 + vo.setStatus(CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()); + } else if (currentTime.after(examUser.getEndTime())) { + //考试已经结束 + vo.setStatus(CourseEnums.ExamTimeStatus.DONE.getCode()); + } else { + //考试正在进行中 + vo.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } else { + vo.setStatus(CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()); + } + } + //查询试卷题目统计数据 + List questionList = HistoryPaperUtils.convertQuestionList(exam.getCurrQuestionList()); + List halfFullQuestion = userExamUtil.queryHalfFullQuestionList(questionList); + vo.setQuestionNumMap(HistoryPaperUtils.analysPaperQuestionCount(halfFullQuestion)); + + return vo; + } + + /** + * 历史考试记录列表 + * + * @param req + * @return + */ + @Override + public PageInfo webHistoryExamUserList(WebHistoryQueryExamUserReq req) { + return null; + } + + /** + * 隐藏我的考试记录 + * + * @param userExamId 用户考试记录ID + */ + @Override + public void hiddenMyExamRecord(String userExamId) { + if (StringUtils.isEmpty(userExamId)) { + throw new RuntimeException("用户考试ID为空"); + } + + //1、查询 + FtbCultivateExamUser examUser = baseMapper.selectById(userExamId); + if (null == examUser) { + throw new RuntimeException("未查询到相关考试记录"); + } + FtbCultivateExam exam = examService.getById(examUser.getExamId()); + if (null == exam) { + throw new RuntimeException("考试信息不存在"); + } + if (!exam.getStatus().equals(3)) { + throw new RuntimeException("考试正常不能删除"); + } + examUser.setAppShow(CourseEnums.ExamRecordDisplay.HIDDEN.getCode()); + baseMapper.updateById(examUser); + } + + /** + * 根据岗位查询指定用户的考试信息 + * + * @param positionId 岗位id + * @param userIdsByGradesId 用户列表 + * @return + */ + @Override + public FtbCultivatePositionExamVO queryExamPassingInformation(String positionId, List userIdsByGradesId) { + FtbCultivatePositionExamVO examVO = new FtbCultivatePositionExamVO(); + // 查寻这些岗位下那些人参与了岗位学习考试 + List cultivateExamUsers = baseMapper.queryTheListOfQualifiedPersonsNew(userIdsByGradesId, positionId); + StatisticsResultDto curr = QuestionAnalysisUtil.statisticeLv(cultivateExamUsers); + examVO.setNumberExam(curr.getPass()); + examVO.setExamPassRate(BigDecimal.valueOf(curr.getPassLv())); + return examVO; + } + + /** + * 批量查询一个岗位的指定人员的考试信息 + * + * @param userIds 用户id集合 + * @param positionId 岗位id + * @param exmIds 考试id + * @return + */ + @Override + public FtbCultivatePositionExamPersonVO queryTheListOfQualifiedPersons(List userIds, String positionId, List exmIds) { + FtbCultivateExamUser cultivateExamUsers = baseMapper.queryTheListOfQualifiedPersons(userIds, positionId, exmIds); + return FtbCultivatePositionExamPersonVO.covert(cultivateExamUsers); + } + + /** + * 根据考试id和批次查询考试人员列表 + * + * @param examId 考试id + * @param versionBatch 批次 + * @return + */ + @Override + public List queryCompleteExamUserListForExamIdAndBatch(String examId, String versionBatch) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.notIn(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode()) + .eq(FtbCultivateExamUser::getExamId, examId) + .eq(FtbCultivateExamUser::getVersionBatch, versionBatch) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + return baseMapper.selectList(wrapper); + } + + /** + * 根据考试ID查询待考试的列表 + * + * @param examId 考试ID + * @return + */ + @Override + public List queryWaitReadOverNum(String examId) { + return baseMapper.queryWaitReadOverNum(examId); + } + + /** + * 检查用户是否完成考试 + * + * @param positionId 岗位ID + * @param userId 用户ID + * @return + */ + @Override + public CompleteExamVo checkIsCompleteExamForUserId(String positionId, String userId) { + CompleteExamVo vo = new CompleteExamVo(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getRelationPositionId, positionId) + .eq(FtbCultivateExamUser::getExamSource, 2) + .eq(FtbCultivateExamUser::getUserId, userId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()).orderByDesc(FtbCultivateExamUser::getCreatorTime); + List examUserList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(examUserList)) { + return vo; + } + FtbCultivateExamUser examUser = examUserList.get(0); + vo.setIsHasExam(true); + if (examUser.getStatus().equals(CourseEnums.ExamStatus.WAIT_CHECK.getCode()) + || examUser.getStatus().equals(CourseEnums.ExamStatus.PASS.getCode()) + || examUser.getStatus().equals(CourseEnums.ExamStatus.VERY_PASS.getCode()) + || examUser.getStatus().equals(CourseEnums.ExamStatus.NO_PASS.getCode())) { + vo.setIsComplete(true); + } + return vo; + } + + /** + * 岗位学习的考试 或者 是否是考试合格才能触发鉴定 发生变化时回调 + * + * @param positionId 岗位学ID + * @param examId 考试ID + * @param examIdentifyConfig 是否是考试合格才能触发鉴定,0-否 1-是 + */ + @Override + public void positionConfigChangeCallBack(String positionId, String examId, Integer examIdentifyConfig) { + log.error("positionConfigChangeCallBack positionId={},examId={},examIdentifyConfig={}", positionId, examId, examIdentifyConfig); + + + LambdaUpdateWrapper updateWrapper1 = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getExamIdentifyConfig, 0) + .eq(FtbCultivateExamUser::getRelationPositionId, positionId) + .eq(FtbCultivateExamUser::getExamSource, 2) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + this.update(updateWrapper1); + if (examIdentifyConfig == 1 && StringUtils.isNotEmpty(examId)) { + LambdaUpdateWrapper updateWrapper2 = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getExamIdentifyConfig, examIdentifyConfig) + .eq(FtbCultivateExamUser::getRelationPositionId, positionId) + .eq(FtbCultivateExamUser::getExamId, examId) + .eq(FtbCultivateExamUser::getExamSource, 2) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + this.update(updateWrapper2); + } + + + } + + + /** + * 统计通过的任务考试数量 + * + * @param taskIds 任务集合ID + * @return + */ + @Override + public Map groupPassCountNum(List taskIds) { + Map ret = new HashMap<>(); + List list = baseMapper.groupPassCountNum(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + /** + * 批量统计所有的任务考试数量 + * + * @param taskIds 任务ID集合 + * @return + */ + @Override + public Map groupAllCountNum(List taskIds) { + Map ret = new HashMap<>(); + List list = baseMapper.groupAllCountNum(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + @Override + public Map groupAllCountNumV2(List taskIds) { + Map ret = new HashMap<>(); + List list = baseMapper.groupAllCountNumV2(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + /** + * 删除任务考试记录 + * + * @param taskId 任务ID + */ + @Override + public void deleteTaskExamUserRecord(String taskId) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateExamUser::getRelationTaskId, taskId) + .eq(FtbCultivateExamUser::getExamSource, 4) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + this.update(updateWrapper); + } + + /** + * 统计通过的任务考试数量 + * + * @param taskIds 任务集合ID + * @return 统计结果 + */ + @Override + public Map groupPassCountNumV2(List taskIds) { + Map ret = new HashMap<>(); + List list = baseMapper.groupPassCountNumV2(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileService.java new file mode 100644 index 0000000..ac5a257 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service.impl; + +import jnpf.model.cultivate.po.FtbCultivateFile; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbCultivateFileService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileServiceImpl.java new file mode 100644 index 0000000..039a14f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateFileServiceImpl.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.service.impl; + +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateFileMapper; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.cultivate.service.impl.FtbCultivateFileService; + +@Service +public class FtbCultivateFileServiceImpl extends ServiceImpl implements FtbCultivateFileService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnCategoriesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnCategoriesServiceImpl.java new file mode 100644 index 0000000..29499e8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnCategoriesServiceImpl.java @@ -0,0 +1,418 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnCategoriesMapper; +import jnpf.cultivate.service.FtbCultivateLearnCategoriesService; +import jnpf.cultivate.service.FtbCultivateLearnTaskListService; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnCategoriesDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnCategories; +import jnpf.model.cultivate.req.learn.AddLearnCategoryReq; +import jnpf.model.cultivate.req.learn.QueryLearnCategoryListReq; +import jnpf.model.cultivate.req.learn.UpdateLearnCategoryReq; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author 许贵林 + * @description 数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbCultivateLearnCategoriesServiceImpl extends ServiceImpl + implements FtbCultivateLearnCategoriesService { + + + @Autowired + private FtbCultivateLearnTaskListService taskListService; + + /** + * 查询所有 返回tree + * + * @param req + * @return + */ + @Override + public List listCategory(QueryLearnCategoryListReq req) { + //type 0 查询所有分类(默认) 1 查询一级分类 + if (req.getType() == null) {//默认查询所有 + req.setType(0); + } + if (!req.getType().equals(0) && !req.getType().equals(1)) { + throw new RuntimeException("type参数错误"); + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(req.getType().equals(1), FtbCultivateLearnCategories::getParentId, "") + .eq(FtbCultivateLearnCategories::getEnabledMark, 0) + .orderByAsc(FtbCultivateLearnCategories::getCreatorTime); + List categoriesList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(categoriesList)) { + return new ArrayList<>(); + } + List retList = new ArrayList<>(); + if (req.getType().equals(1)) { + //一级分类不需要够级分类树 + retList = BeanUtil.copyToList(categoriesList, FtbCultivateLearnCategoriesDto.class); + } else { + retList = buildCateTree(categoriesList); + } + if (req.getIsQueryNum().equals(1)) { + Map countMap = taskListService.groupCountCateNum(); + fillCateNum(retList, countMap); + } + return retList; + } + + /** + * 填充分类的任务数量 + * + * @param retList + * @param countMap + */ + private void fillCateNum(List retList, Map countMap) { + if (CollectionUtil.isEmpty(retList) || CollectionUtil.isEmpty(countMap)) { + return; + } + for (FtbCultivateLearnCategoriesDto dto : retList) { + Integer count = countMap.get(dto.getId()); + if (count != null) { + dto.setTotleNum(count); + } + List children = dto.getChildren(); + if (CollectionUtil.isNotEmpty(children)) { + fillCateNum(children, countMap); + } + } + } + + + /** + * 添加分类 + * + * @param req + * @return + */ + @Override + public FtbCultivateLearnCategories insertData(AddLearnCategoryReq req) { + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateLearnCategories::getName, req.getName()) + .eq(FtbCultivateLearnCategories::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分类名称已经存在"); + } + //查询检测父级分类是否存在 + FtbCultivateLearnCategories parent = null; + if (StringUtils.isNotEmpty(req.getParentId())) { + parent = queryAndCheckParent(req.getParentId()); + } + + //添加分类 + FtbCultivateLearnCategories categories = new FtbCultivateLearnCategories(); + categories.setName(req.getName()); + categories.setParentId(req.getParentId()); + String path = ""; + if (null != parent) { + //检查是否超过2级 + if (StringUtils.isNotEmpty(parent.getPath())) { + if (parent.getPath().split("-").length > 2) { + throw new RuntimeException("分类最多支持2级"); + } + } + + if (StringUtils.isNotEmpty(parent.getPath())) { + path = parent.getPath() + "-" + parent.getId(); + } else { + path = parent.getId(); + } + } + categories.setPath(path); + categories.setEnabledMark(0); + baseMapper.insert(categories); + return categories; + } + + /** + * 修改分类 + * + * @param id 修改的分类ID + * @param req + * @return + */ + @Override + public FtbCultivateLearnCategories updateData(String id, UpdateLearnCategoryReq req) { + //检测分类是否删除 + queryAndCheckById(id); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateLearnCategories::getName, req.getName()) + .ne(FtbCultivateLearnCategories::getId, id) + .eq(FtbCultivateLearnCategories::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分类名称已经存在"); + } + + FtbCultivateLearnCategories parent = null; + if (StringUtils.isNotEmpty(req.getParentId())) { + parent = queryAndCheckParent(req.getParentId()); + } + + //修改分类 + FtbCultivateLearnCategories categories = new FtbCultivateLearnCategories(); + categories.setName(req.getName()); + categories.setId(id); + categories.setEnabledMark(0); + baseMapper.updateById(categories); + return categories; + + } + + /** + * 查询分类信息 + * + * @param parentId 分类ID + * @return + */ + private FtbCultivateLearnCategories queryAndCheckParent(String parentId) { + FtbCultivateLearnCategories parent = baseMapper.selectById(parentId); + if (null == parent) { + throw new RuntimeException("父级分类不存在"); + } + if (parent.getEnabledMark().equals(1)) { + throw new RuntimeException("父级分类已被删除"); + } + return parent; + } + + /** + * 根据分类ID查询分类信息 + * + * @param id 分类ID + * @return + */ + @Override + public FtbCultivateLearnCategories queryAndCheckById(String id) { + FtbCultivateLearnCategories entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("分类不存在"); + } + if (entity.getEnabledMark().equals(1)) { + throw new RuntimeException("分类已经删除"); + } + return entity; + } + + /** + * 删除分类 + * + * @param id 分类ID + */ + @Override + public void deleteData(String id) { + List deleteIds = new ArrayList<>(); + deleteIds.add(id); + //检测分类是否删除 + FtbCultivateLearnCategories currCategory = queryAndCheckById(id); + + //检测分类下是否有下级分类 + List childCategoryIds = queryChildCategoryIds(currCategory); + if (CollectionUtil.isNotEmpty(childCategoryIds)) { + deleteIds.addAll(childCategoryIds); + } + + //检测分类是是否有任务 + if (taskListService.checkHasTask(deleteIds)) { + throw new RuntimeException("分类下存在任务,请先删除任务"); + } + //删除分类 + List updateList = new ArrayList<>(); + for (String deleteId : deleteIds) { + FtbCultivateLearnCategories categories = new FtbCultivateLearnCategories(); + categories.setId(deleteId); + categories.setEnabledMark(1); + updateList.add(categories); + } + updateBatchById(updateList); + } + + /** + * 查询分类下所有子分类ID + * + * @param currCategory 父分类ID + * @return + */ + public List queryChildCategoryIds(FtbCultivateLearnCategories currCategory) { + String path = ""; + if (StringUtils.isNotEmpty(currCategory.getPath())) { + path = currCategory.getPath() + "-" + currCategory.getId(); + } else { + path = currCategory.getId(); + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .likeRight(FtbCultivateLearnCategories::getPath, path) + .eq(FtbCultivateLearnCategories::getEnabledMark, 0); + List categoriesList = baseMapper.selectList(wrapper); + List childIds = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(categoriesList)) { + for (FtbCultivateLearnCategories ftbNoticeCategories : categoriesList) { + childIds.add(ftbNoticeCategories.getId()); + } + } + return childIds; + } + + /** + * 查询分类下所有子分类 + * + * @param parentId 父分类ID + * @return + */ + @Override + public List queryChildCategory(String parentId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateLearnCategories::getParentId, parentId) + .eq(FtbCultivateLearnCategories::getEnabledMark, 0); + return baseMapper.selectList(wrapper); + } + + /** + * 查询分类下子分类数量 + * + * @param parentId 父分类ID + * @return + */ + @Override + public Long queryChildCategoryNum(String parentId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateLearnCategories::getParentId, parentId) + .eq(FtbCultivateLearnCategories::getEnabledMark, 0); + return baseMapper.selectCount(wrapper); + } + + + /** + * 构建分类树 + * + * @param categoriesList + * @return + */ + private List buildCateTree(List categoriesList) { + List categoriesDtoList = BeanUtil.copyToList(categoriesList, FtbCultivateLearnCategoriesDto.class); + List retList = new ArrayList<>(); + Map> map = new HashMap<>(); + for (FtbCultivateLearnCategoriesDto dto : categoriesDtoList) { + fillCategoryLevel(dto); + if (StringUtils.isEmpty(dto.getParentId())) { + retList.add(dto); + } else { + List tempList = map.get(dto.getParentId()); + if (CollectionUtil.isEmpty(tempList)) { + tempList = new ArrayList<>(); + } + tempList.add(dto); + map.put(dto.getParentId(), tempList); + } + } + + for (FtbCultivateLearnCategoriesDto dto : categoriesDtoList) { + List child = map.get(dto.getId()); + if (CollectionUtil.isNotEmpty(child)) { + dto.setChildren(child); + } + } + return retList; + } + + /** + * 填充分类级别 + * + * @param dto + */ + private void fillCategoryLevel(FtbCultivateLearnCategoriesDto dto) { + if (StringUtils.isEmpty(dto.getPath())) { + dto.setLevel(0); + return; + } + String[] split = dto.getPath().split("-"); + dto.setLevel(split.length); + } + + /** + * 批量查询分类信息 + * + * @param ids 分类ID 集合 + * @return + */ + public Map queryCateByIds(List ids) { + Map ret = new HashMap<>(); + if (CollectionUtil.isEmpty(ids)) { + return ret; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbCultivateLearnCategories::getId, ids) + .eq(FtbCultivateLearnCategories::getEnabledMark, 0); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (FtbCultivateLearnCategories cat : list) { + ret.put(cat.getId(), cat); + } + return ret; + } + + /** + * 查询分类和父分类 + * + * @param cateId 分类id + * @return + */ + @Override + public List queryCategoryAndParentCategory(String cateId) { + List list = new ArrayList<>(); + if (StringUtils.isEmpty(cateId)) { + return list; + } + FtbCultivateLearnCategories currCate = baseMapper.selectById(cateId); + if (currCate == null) { + return list; + } + if (StringUtils.isNotEmpty(currCate.getPath())) { + String[] split = currCate.getPath().split("-"); + for (int i = 0; i < split.length; i++) { + if (StringUtils.isNotEmpty(split[i])) { + FtbCultivateLearnCategories last = baseMapper.selectById(split[i]); + if (last == null) { + break; + } + list.add(last); + } + } + } + list.add(currCate); + return list; + } + + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAppContentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAppContentServiceImpl.java new file mode 100644 index 0000000..edaefba --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAppContentServiceImpl.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service.impl; + +import jnpf.cultivate.service.FtbCultivateLearnTaskAppContentService; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateLearnTaskAppContentServiceImpl implements FtbCultivateLearnTaskAppContentService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAssignmentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAssignmentServiceImpl.java new file mode 100644 index 0000000..23e2a10 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskAssignmentServiceImpl.java @@ -0,0 +1,76 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskAssignmentMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskAssignmentService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +@Service +public class FtbCultivateLearnTaskAssignmentServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskAssignmentService { + + @Resource + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + /** + * 批量查询统计任务的学习人数 + * @param taskIds 任务id + * @param status 0-查询全部 1-查询已经完成的 + * @return + */ + @Override + public Map groupCountNum(List taskIds, Integer status) { + Map ret = new HashMap<>(); + if(CollUtil.isEmpty(taskIds)){ + return ret; + } + List list = ftbCultivateLearnTaskAssignmentMapper.groupCountNum(taskIds, status); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + /** + * 批量查询任务学习人员列表 + * @param taskIds 任务ID集合 + * @return + */ + @Override + public Map> groupListAssignment(ArrayList taskIds) { + Map> ret = new HashMap<>(); + if(CollUtil.isEmpty(taskIds)){ + return ret; + } + List list = ftbCultivateLearnTaskAssignmentMapper.groupListAssignment(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (FtbCultivateLearnTaskAssignment dto : list) { + List ftbCultivateLearnTaskAssignments = ret.get(dto.getTaskId()); + if (CollUtil.isEmpty(ftbCultivateLearnTaskAssignments)) { + ftbCultivateLearnTaskAssignments = new ArrayList<>(); + } + ftbCultivateLearnTaskAssignments.add(dto); + ret.put(dto.getTaskId(), ftbCultivateLearnTaskAssignments); + } + return ret; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCertificateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCertificateServiceImpl.java new file mode 100644 index 0000000..ad62e53 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCertificateServiceImpl.java @@ -0,0 +1,44 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskCertificateMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskCertificateService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCertificate; +import jnpf.model.cultivate.resp.TaskRelationCertificateVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskCertificateVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +@Service +public class FtbCultivateLearnTaskCertificateServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskCertificateService { + + @Resource + private FtbCultivateLearnTaskCertificateMapper ftbCultivateLearnTaskCertificateMapper; + + /** + * 批量查询正式名称 + * + * @param taskIds 任务id集合 + * @return 返回证书信息 + */ + @Override + public List queryCertificateName(List taskIds) { + return ftbCultivateLearnTaskCertificateMapper.queryCertificateName(taskIds); + } + + @Override + public List listByTaskId(String taskId) { + return ftbCultivateLearnTaskCertificateMapper.listByTaskId(taskId); + } + + @Override + public List listByTaskIdAndPhaseId(String taskId, String phaseId) { + return ftbCultivateLearnTaskCertificateMapper.listByTaskIdAndPhaseId(taskId, phaseId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCourseServiceImpl.java new file mode 100644 index 0000000..8fd8193 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskCourseServiceImpl.java @@ -0,0 +1,85 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskCourseMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskCourseService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCourse; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskCourseVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +@Service +public class FtbCultivateLearnTaskCourseServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskCourseService { + + @Resource + private FtbCultivateLearnTaskCourseMapper ftbCultivateLearnTaskCourseMapper; + + /** + * 批量查询任务的课程数量 + * + * @param taskIds 任务id集合 + * @return + */ + @Override + public Map groupCountNum(List taskIds) { + Map ret = new HashMap<>(); + List list = ftbCultivateLearnTaskCourseMapper.groupCountNum(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + /** + * 批量查询任务的课程列表 + * + * @param taskIds 任务ID集合 + * @return + */ + @Override + public Map> groupListTaskCourse(ArrayList taskIds) { + Map> ret = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return ret; + } + List list = ftbCultivateLearnTaskCourseMapper.groupListTaskCourse(taskIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (FtbCultivateLearnTaskCourse dto : list) { + List ftbCultivateLearnTaskAssignments = ret.get(dto.getTaskId()); + if (CollUtil.isEmpty(ftbCultivateLearnTaskAssignments)) { + ftbCultivateLearnTaskAssignments = new ArrayList<>(); + } + ftbCultivateLearnTaskAssignments.add(dto); + ret.put(dto.getTaskId(), ftbCultivateLearnTaskAssignments); + } + return ret; + } + + @Override + public List listByTaskId(String taskId, Integer required) { +// 是否必修 (0 是 1 否) + return ftbCultivateLearnTaskCourseMapper.listByTaskId(taskId, required); + } + + @Override + public List listByTaskIdAndPhaseId(String taskId, String phaseId) { + return ftbCultivateLearnTaskCourseMapper.listByTaskIdAndPhaseId(taskId, phaseId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskExamServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskExamServiceImpl.java new file mode 100644 index 0000000..818f288 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskExamServiceImpl.java @@ -0,0 +1,56 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskExamMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskExamService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskExam; +import jnpf.model.cultivate.resp.TaskRelationExamVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskExamVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +@Service +public class FtbCultivateLearnTaskExamServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskExamService { + + @Resource + private FtbCultivateLearnTaskExamMapper ftbCultivateLearnTaskExamMapper; + + /** + * 批量查询任务考试信息 + * + * @param taskIds 任务ID集合 + * @return + */ + @Override + public List queryExamBaseInfo(List taskIds) { + return ftbCultivateLearnTaskExamMapper.queryExamBaseInfo(taskIds); + } + + @Override + public List listByTaskId(String taskId) { + return ftbCultivateLearnTaskExamMapper.listByTaskIdWithExam(taskId); + } + + @Override + public List listByTaskIdAndPhaseId(String taskId, String phaseId) { + return ftbCultivateLearnTaskExamMapper.listByTaskIdAndPhaseId(taskId, phaseId); + } + + /** + * 检查考试是否被学习任务绑定(关联任务主表) + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + @Override + public Boolean checkExamBinding(String examId) { + Integer count = ftbCultivateLearnTaskExamMapper.checkExamBinding(examId); + return count != null && count > 0; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskIdentificationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskIdentificationServiceImpl.java new file mode 100644 index 0000000..f16a885 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskIdentificationServiceImpl.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskIdentificationMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskIdentificationService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.resp.TaskRelationIdentificationVo; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskIdentificationVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/9/9:11:05 + */ +@Service +public class FtbCultivateLearnTaskIdentificationServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskIdentificationService { + + @Resource + private FtbCultivateLearnTaskIdentificationMapper ftbCultivateLearnTaskIdentificationMapper; + + /** + * 根据任务id查询关联的认证id + * + * @param taskIds 任务ID集合 + * @return + */ + @Override + public List queryIdentificationName(List taskIds) { + return ftbCultivateLearnTaskIdentificationMapper.queryIdentificationName(taskIds); + } + + /** + * 根据任务id查询关联的认证信息 + * + * @param taskId 任务ID + * @return + */ + @Override + public FtbCultivateLearnTaskIdentification queryByTaskId(String taskId) { + return ftbCultivateLearnTaskIdentificationMapper.queryByTaskId(taskId); + } + + @Override + public List listByTaskId(String taskId) { + return ftbCultivateLearnTaskIdentificationMapper.listByTaskId(taskId); + } + + @Override + public List listByTaskIdAndPhaseId(String taskId, String phaseId) { + return ftbCultivateLearnTaskIdentificationMapper.listByTaskIdAndPhaseId(taskId, phaseId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoContentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoContentServiceImpl.java new file mode 100644 index 0000000..b2de0e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoContentServiceImpl.java @@ -0,0 +1,449 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.CultivateImUtil; +import jnpf.cultivate.utils.CultivateLearnTaskIMUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.learn.*; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.PositionCourseEventDTO; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.vo.learn.FtbLearnQueryTaskDetailsEditingVO; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.service.FtbPersonnelsMetaDataService; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import jnpf.util.context.ThreadContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +@Service +public class FtbCultivateLearnTaskInfoContentServiceImpl implements FtbCultivateLearnTaskInfoContentService { + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Resource + private CultivateImUtil cultivateImUtil; + + @Resource + private CultivateLearnTaskIMUtils captchaImUtil; + + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Resource + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + @Resource + private FtbCultivateLearnTaskAssignmentService ftbCultivateLearnTaskAssignmentService; + + @Resource + private FtbCultivateLearnTaskCertificateMapper ftbCultivateLearnTaskCertificateMapper; + + @Resource + private FtbCultivateLearnTaskCourseService ftbCultivateLearnTaskCourseService; + + @Resource + private FtbCultivateLearnTaskExamMapper ftbCultivateLearnTaskExamMapper; + + @Resource + private FtbCultivateLearnTaskIdentificationMapper ftbCultivateLearnTaskIdentificationMapper; + + @Resource + private FtbPersonnelsMetaDataService ftbPersonnelsMetaDataService; + + @Resource + private FtbCultivateLearnTaskReminderRuleMapper ftbCultivateLearnTaskReminderRuleMapper; + + @Resource + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Resource + private FtbCultivateLearnTaskInfoContentMapper ftbCultivateLearnTaskInfoContentMapper; + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + @Override + @Transactional + public void saveLearnTaskList(FtbCultivateLearnTaskInfoDto taskInfoDto) { + taskInfoDto.beforeCheck(); + // 任务名称不能重复 + LambdaQueryWrapper taskQueryWrapper = Wrappers.lambdaQuery(); + taskQueryWrapper.eq(FtbCultivateLearnTask::getTaskName, taskInfoDto.getTaskName()); + taskQueryWrapper.eq(FtbCultivateLearnTask::getEnableMark, 0); + if (ftbCultivateLearnTaskMapper.selectCount(taskQueryWrapper) > 0) { + throw new RuntimeException("任务名称不能重复"); + } + initLearnTaskData(taskInfoDto, null, null); + } + + @Override + @Transactional + public void updateLearnTaskList(FtbCultivateLearnTaskInfoDto taskInfoDto) { + + taskInfoDto.beforeCheck(); + LambdaQueryWrapper taskNameQueryWrapper = Wrappers.lambdaQuery(); + taskNameQueryWrapper.eq(FtbCultivateLearnTask::getTaskName, taskInfoDto.getTaskName()); + taskNameQueryWrapper.eq(FtbCultivateLearnTask::getEnableMark, 0); + FtbCultivateLearnTask findByName = ftbCultivateLearnTaskMapper.selectOne(taskNameQueryWrapper); + if (findByName != null && !findByName.getId().equals(taskInfoDto.getId())) { + throw new RuntimeException("任务名称不能重复"); + } + + // 上次任务展示ID + LambdaQueryWrapper taskQueryWrapper = Wrappers.lambdaQuery(); + taskQueryWrapper.eq(FtbCultivateLearnTask::getId, taskInfoDto.getId()); + FtbCultivateLearnTask ftbCultivateLearnTask = ftbCultivateLearnTaskMapper.selectOne(taskQueryWrapper); + // 任务进行中时,编辑任务后将重置任务,已完成的部分清空。已有统计数据一并清除 + deleteTask(taskInfoDto.getId()); + initLearnTaskData(taskInfoDto, ftbCultivateLearnTask.getLearnTaskShowId(), ftbCultivateLearnTask); + } + /** + * 获取学习任务分配列表 + * + * @param taskId 任务ID,用于标识特定的学习任务 + * @return 返回一个字符串列表,包含任务的分配详情 + */ + @Override + public List getLearnTaskAllocationList(String taskId) { + LambdaQueryWrapper assignmentQueryWrapper = Wrappers.lambdaQuery(); + assignmentQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + assignmentQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + return ftbCultivateLearnTaskAssignmentService.list(assignmentQueryWrapper) + .stream() + .map(FtbCultivateLearnTaskAssignment::getUserId) + .collect(Collectors.toList()); + } + + /** + * 查询未完成的学院 + * @param taskId 任务id + * @return + */ + @Override + public List queryUserListForTaskNoComplete(String taskId) { + LambdaQueryWrapper assignmentQueryWrapper = Wrappers.lambdaQuery(); + assignmentQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + assignmentQueryWrapper.ne(FtbCultivateLearnTaskAssignment::getStudyStats, 2); + assignmentQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + return ftbCultivateLearnTaskAssignmentService.list(assignmentQueryWrapper) + .stream() + .map(FtbCultivateLearnTaskAssignment::getUserId) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void allocation(FtbCultivateLearnAllocationDTO allocationDTO) { + // 人员清洗,补充分配(回显张三,并添加李四、王五)覆盖分配(回显张三,移除张三;再添加李四、王五) + LambdaQueryWrapper taskAssignmentLambdaQueryWrapper = Wrappers.lambdaQuery(); + taskAssignmentLambdaQueryWrapper.select(FtbCultivateLearnTaskAssignment::getUserId); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, allocationDTO.getTaskId()); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List oldLearnTaskAssignments = ftbCultivateLearnTaskAssignmentService.list(taskAssignmentLambdaQueryWrapper); + List oldUserIds = oldLearnTaskAssignments.stream() + .map(FtbCultivateLearnTaskAssignment::getUserId) + .collect(Collectors.toList()); + // 覆盖分配 + List overwritePersonnelIds = oldUserIds.stream() + .filter(a -> !allocationDTO.getUserIds().contains(a)) + .collect(Collectors.toList()); + if (!overwritePersonnelIds.isEmpty()) { + // 移除覆盖分配人员 + taskAssignmentLambdaQueryWrapper.clear(); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, allocationDTO.getTaskId()); + taskAssignmentLambdaQueryWrapper.in(FtbCultivateLearnTaskAssignment::getUserId, overwritePersonnelIds); + ftbCultivateLearnTaskAssignmentService.remove(taskAssignmentLambdaQueryWrapper); + } + List processingUserIds = allocationDTO.getUserIds() + .stream() + .filter(a -> !oldUserIds.contains(a)) + .collect(Collectors.toList()); + // 任务课程 + LambdaQueryWrapper courseQueryWrapper = Wrappers.lambdaQuery(); + courseQueryWrapper.select(FtbCultivateLearnTaskCourse::getCourseId); + courseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, allocationDTO.getTaskId()); + courseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getEnableMark, 0); + List courseList = ftbCultivateLearnTaskCourseService.list(courseQueryWrapper); + // 新人员绑定 + List ftbCultivateLearnTaskAssignments = processingUserIds.stream().map(a -> { + FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment = new FtbCultivateLearnTaskAssignment(); + ftbCultivateLearnTaskAssignment.setTaskId(allocationDTO.getTaskId()); + ftbCultivateLearnTaskAssignment.setUserId(a); + ftbCultivateLearnTaskAssignment.setStudyStats(0); + return ftbCultivateLearnTaskAssignment; + }).collect(Collectors.toList()); + ftbCultivateLearnTaskAssignmentService.saveBatch(ftbCultivateLearnTaskAssignments); + // 人员课程绑定 + List courseIds = courseList.stream() + .map(FtbCultivateLearnTaskCourse::getCourseId) + .collect(Collectors.toList()); + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + PositionCourseEventDTO.builder() + .courseIds(courseIds) + .userIds(processingUserIds) + .eventType(PositionCourseEventDTO.EventType.USER) + .whetherToChange(false) + .build())); + } + + @Override + @Transactional + public void deleteTask(String id) { + // 任务删除 + LambdaQueryWrapper deleteLearnTaskWrapper = Wrappers.lambdaQuery(); + deleteLearnTaskWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + ftbCultivateLearnTaskMapper.delete(deleteLearnTaskWrapper); + // 任务人员 + LambdaQueryWrapper deleteLearnTaskAssignmentWrapper = Wrappers.lambdaQuery(); + deleteLearnTaskAssignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, id); + ftbCultivateLearnTaskAssignmentService.remove(deleteLearnTaskAssignmentWrapper); + // 任务课程 + LambdaQueryWrapper deleteCourseWrapper = Wrappers.lambdaQuery(); + deleteCourseWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, id); + ftbCultivateLearnTaskCourseService.remove(deleteCourseWrapper); + // 任务证书 + LambdaQueryWrapper deleteCertificateWrapper = Wrappers.lambdaQuery(); + deleteCertificateWrapper.eq(FtbCultivateLearnTaskCertificate::getTaskId, id); + ftbCultivateLearnTaskCertificateMapper.delete(deleteCertificateWrapper); + // 任务考试 + LambdaQueryWrapper deleteExamWrapper = Wrappers.lambdaQuery(); + deleteExamWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, id); + ftbCultivateLearnTaskExamMapper.delete(deleteExamWrapper); + // 任务鉴定 + LambdaQueryWrapper deleteIdentificationWrapper = Wrappers.lambdaQuery(); + deleteIdentificationWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, id); + ftbCultivateLearnTaskIdentificationMapper.delete(deleteIdentificationWrapper); + // 任务提醒鉴定 + LambdaQueryWrapper deleteReminderWrapper = Wrappers.lambdaQuery(); + deleteReminderWrapper.eq(FtbCultivateLearnTaskReminderRule::getTaskId, id); + ftbCultivateLearnTaskReminderRuleMapper.delete(deleteReminderWrapper); + // 删除任务关联考试 + ftbCultivateExamUserService.deleteTaskExamUserRecord(id); + // 删除任务鉴定 + cultivateIdentifyApplyService.deleteLearnTaskApplyIdentified(id); + } + + @Override + public FtbLearnQueryTaskDetailsEditingVO queryTaskDetailsWhileEditing(String id) { + FtbCultivateLearnTask ftbCultivateLearnTask = ftbCultivateLearnTaskMapper.selectById(id); + FtbLearnQueryTaskDetailsEditingVO ftbLearnQueryTaskDetailsEditingVO = FtbLearnQueryTaskDetailsEditingVO.convert(ftbCultivateLearnTask); + if (null == ftbLearnQueryTaskDetailsEditingVO) { + return ftbLearnQueryTaskDetailsEditingVO; + } + if (ftbCultivateLearnTask != null && ftbCultivateLearnTask.getAssignmentRule() == 1) { + List userIds = new ArrayList<>(); + LambdaQueryWrapper taskAssignmentWraper = Wrappers.lambdaQuery(); + taskAssignmentWraper.eq(FtbCultivateLearnTaskAssignment::getTaskId, id); + taskAssignmentWraper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List taskAssignmentList = ftbCultivateLearnTaskAssignmentService.list(taskAssignmentWraper); + if (CollUtil.isNotEmpty(taskAssignmentList)) { + //获取用户id列表 + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : taskAssignmentList) { + userIds.add(ftbCultivateLearnTaskAssignment.getUserId()); + } + } + ftbLearnQueryTaskDetailsEditingVO.setUserIds(userIds); + } + // 任务提醒规则 + LambdaQueryWrapper reminderRuleLambdaQueryWrapper = Wrappers.lambdaQuery(); + reminderRuleLambdaQueryWrapper.eq(FtbCultivateLearnTaskReminderRule::getTaskId, id); + FtbCultivateLearnTaskReminderRule ftbCultivateLearnTaskReminderRule = ftbCultivateLearnTaskReminderRuleMapper.selectOne(reminderRuleLambdaQueryWrapper); + ftbLearnQueryTaskDetailsEditingVO.setTaskStartReminder(ftbCultivateLearnTaskReminderRule.getTaskStartReminder()); + ftbLearnQueryTaskDetailsEditingVO.setTaskEndReminder(ftbCultivateLearnTaskReminderRule.getTaskEndReminder()); + ftbLearnQueryTaskDetailsEditingVO.setPerInterval(ftbCultivateLearnTaskReminderRule.getPerInterval()); + // 任务课程 + List learnTaskCourseList = ftbCultivateLearnTaskReminderRuleMapper.getLearnTaskCourseList(id); + + ftbLearnQueryTaskDetailsEditingVO.setCourseList(learnTaskCourseList); + // 任务考试 + LambdaQueryWrapper examQueryWrapper = Wrappers.lambdaQuery(); + examQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, id); + FtbCultivateLearnTaskExam ftbCultivateLearnTaskExam = ftbCultivateLearnTaskExamMapper.selectOne(examQueryWrapper); + ftbLearnQueryTaskDetailsEditingVO.setExamList(FtbCultivateLearnTaskExamInfoDto.convert(ftbCultivateLearnTaskExam)); + // 任务鉴定 + LambdaQueryWrapper identificationQueryWrapper = Wrappers.lambdaQuery(); + identificationQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, id); + FtbCultivateLearnTaskIdentification ftbCultivateLearnTaskIdentification = ftbCultivateLearnTaskIdentificationMapper.selectOne(identificationQueryWrapper); + ftbLearnQueryTaskDetailsEditingVO.setIdentificationList(FtbCultivateLearnTaskIdentificationInfoDto.convert(ftbCultivateLearnTaskIdentification)); + // 任务证书 + LambdaQueryWrapper certificateQueryWrapper = Wrappers.lambdaQuery(); + certificateQueryWrapper.eq(FtbCultivateLearnTaskCertificate::getTaskId, id); + FtbCultivateLearnTaskCertificate ftbCultivateLearnTaskCertificate = ftbCultivateLearnTaskCertificateMapper.selectOne(certificateQueryWrapper); + ftbLearnQueryTaskDetailsEditingVO.setCertificateList(FtbCultivateLearnTaskCertificateInfoDto.convert(ftbCultivateLearnTaskCertificate)); + // 考试名称 + if (Objects.nonNull(ftbCultivateLearnTaskExam)) { + String examName = ftbCultivateLearnTaskInfoContentMapper.queryNameBasedOnExamId(ftbCultivateLearnTaskExam.getExamId()); + ftbLearnQueryTaskDetailsEditingVO.getExamList().setExamName(examName); + } + // 鉴定名称 + if (Objects.nonNull(ftbCultivateLearnTaskIdentification)) { + String identificationName = ftbCultivateLearnTaskInfoContentMapper.queryNameBasedOnIdentificaId(ftbCultivateLearnTaskIdentification.getIdentificationId()); + ftbLearnQueryTaskDetailsEditingVO.getIdentificationList().setIdentificationName(identificationName); + } + // 证书名称 + if (Objects.nonNull(ftbCultivateLearnTaskCertificate)) { + String certificateName = ftbCultivateLearnTaskInfoContentMapper.queryNameBasedOnCertificateId(ftbCultivateLearnTaskCertificate.getCertificateId()); + ftbLearnQueryTaskDetailsEditingVO.getCertificateList().setCertificateName(certificateName); + } + return ftbLearnQueryTaskDetailsEditingVO; + } + + @Override + @Transactional + public void abort(String taskId) { + // 任务中止 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, taskId); + updateWrapper.set(FtbCultivateLearnTask::getStatus, 3); + ftbCultivateLearnTaskMapper.update(new FtbCultivateLearnTask(), updateWrapper); + String tenantId = UserProvider.getUser().getTenantId(); + // im消息通知 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateLearnTask::getTaskName); + queryWrapper.eq(FtbCultivateLearnTask::getId, taskId); + FtbCultivateLearnTask ftbCultivateLearnTask = ftbCultivateLearnTaskMapper.selectOne(queryWrapper); + String pushMessageContent = "您的任务【" + ftbCultivateLearnTask.getTaskName() + "】已终止,无法继续进行任务!"; + cultivateImUtil.sendTaskAbortMessage(queryUserListForTaskNoComplete(taskId), tenantId, pushMessageContent); + } + + + /** + * 学习任务数据 + * + * @param taskInfoDto 任务信息 + * @param learnTaskShowId 学习任务展示id + */ + private void initLearnTaskData(FtbCultivateLearnTaskInfoDto taskInfoDto, String learnTaskShowId, FtbCultivateLearnTask oldTaskInfo) { + // 任务分配规则(1,指定成员,2指定组织,3指定岗位,4自定义) + List userIds = new ArrayList<>(); + if (taskInfoDto.getAssignmentRule() == 1) { + userIds = taskInfoDto.getUserIds(); + } else if (taskInfoDto.getAssignmentRule() == 2) { + // 指定组织下所有用户 + if(StringUtil.isNotEmpty(taskInfoDto.getAssociationIds())) { + List split = List.of(taskInfoDto.getAssociationIds().split(StringPool.COMMA)); + List userBaseInfoByOrganizes = userApiV2Util.getUserListForOrgIds(split,null); + if(CollUtil.isNotEmpty(userBaseInfoByOrganizes)){ + userIds = userBaseInfoByOrganizes.stream().map(UserPageListVO::getId).distinct().collect(Collectors.toList()); + } + } + } else if (taskInfoDto.getAssignmentRule() == 3) { + // 指定岗位下所有用户 + List userBaseInfoByPositions = userApiV2Util.getUserListForPositions(List.of(taskInfoDto.getAssociationIds().split(StringPool.COMMA)),null); + if(CollUtil.isNotEmpty(userBaseInfoByPositions)){ + userIds = userBaseInfoByPositions.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList()); + } + + } else if (taskInfoDto.getAssignmentRule() == 4) { + // 自定义 + userIds = ftbPersonnelsMetaDataService.queryUserIdsForCompanyAge(taskInfoDto.getStartEntryPeriod(), taskInfoDto.getEndEntryPeriod()); + } + // 任务主表 + FtbCultivateLearnTask ftbCultivateLearnTask = taskInfoDto.convertFtbCultivateLearnTask(taskInfoDto); + // 任务展示Id,更新是之前的,新增则为新的 + ftbCultivateLearnTask.setLearnTaskShowId(learnTaskShowId); + if (learnTaskShowId == null) { + ftbCultivateLearnTask.setLearnTaskShowId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.LEARN_TASK)); + } + if (oldTaskInfo != null) { + ftbCultivateLearnTask.setCreatorTime(oldTaskInfo.getCreatorTime()); + ftbCultivateLearnTask.setCreatorUserId(oldTaskInfo.getCreatorUserId()); + } + ftbCultivateLearnTaskMapper.insert(ftbCultivateLearnTask); + // 学习任务关联人 + List ftbCultivateLearnTaskAssignments = userIds.stream().map(a -> { + FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment = new FtbCultivateLearnTaskAssignment(); + ftbCultivateLearnTaskAssignment.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskAssignment.setStudyStats(0); + ftbCultivateLearnTaskAssignment.setUserId(a); + return ftbCultivateLearnTaskAssignment; + }).collect(Collectors.toList()); + // 任务课程 + List ftbCultivateLearnTaskCourses = taskInfoDto.getCourseList().stream().map(a -> { + FtbCultivateLearnTaskCourse ftbCultivateLearnTaskCourse = new FtbCultivateLearnTaskCourse(); + ftbCultivateLearnTaskCourse.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskCourse.setCourseId(a.getCourseId()); + ftbCultivateLearnTaskCourse.setIsRequired(a.getIsRequired()); + return ftbCultivateLearnTaskCourse; + }).collect(Collectors.toList()); + // 任务学习关联规则提醒 + FtbCultivateLearnTaskReminderRule ftbCultivateLearnTaskReminderRule = taskInfoDto.convertTaskReminderRule(taskInfoDto); + ftbCultivateLearnTaskReminderRule.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskReminderRuleMapper.insert(ftbCultivateLearnTaskReminderRule); + // 任务考试 + if (Objects.nonNull(taskInfoDto.getExamList())) { + FtbCultivateLearnTaskExam ftbCultivateLearnTaskExam = taskInfoDto.convertFtbCultivateLearnTaskExam(taskInfoDto); + ftbCultivateLearnTaskExam.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskExamMapper.insert(ftbCultivateLearnTaskExam); + } + // 任务鉴定 + if (Objects.nonNull(taskInfoDto.getIdentificationList())) { + FtbCultivateLearnTaskIdentification ftbCultivateLearnTaskIdentification = taskInfoDto.convertFtbCultivateLearnTaskIdentification(taskInfoDto); + ftbCultivateLearnTaskIdentification.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskIdentificationMapper.insert(ftbCultivateLearnTaskIdentification); + } + // 任务证书 + if (Objects.nonNull(taskInfoDto.getCertificateList())) { + FtbCultivateLearnTaskCertificate ftbCultivateLearnTaskCertificate = taskInfoDto.convertFtbCultivateLearnTaskCertificate(taskInfoDto); + ftbCultivateLearnTaskCertificate.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskCertificateMapper.insert(ftbCultivateLearnTaskCertificate); + } + ftbCultivateLearnTaskAssignmentService.saveBatch(ftbCultivateLearnTaskAssignments); + ftbCultivateLearnTaskCourseService.saveBatch(ftbCultivateLearnTaskCourses); + // 任务课程事件 + List courseIds = taskInfoDto.getCourseList().stream() + .map(FtbCultivateLearnTaskCourseInfoDto::getCourseId) + .collect(Collectors.toList()); + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + PositionCourseEventDTO.builder() + .courseIds(courseIds) + .userIds(userIds) + .eventType(PositionCourseEventDTO.EventType.USER) + .whetherToChange(false) + .build())); + // 任务进行中,启用提醒后,任务开始时将提醒员工 + if (ftbCultivateLearnTask.getStatus() == 2 + && ftbCultivateLearnTaskReminderRule.getTaskStartReminder() == 1 + && CollUtil.isNotEmpty(userIds)) { + List alertUserList = userIds.stream().map(a -> { + NeedAlertUserDto needAlertUserDto = new NeedAlertUserDto(); + needAlertUserDto.setUserId(a); + needAlertUserDto.setTaskId(ftbCultivateLearnTask.getId()); + needAlertUserDto.setTaskName(ftbCultivateLearnTask.getTaskName()); + return needAlertUserDto; + }).collect(Collectors.toList()); + String tenantId = UserProvider.getUser().getTenantId(); + + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + captchaImUtil.sendMsg(alertUserList, tenantId); + }), cultivateThreadPool); + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoCountServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoCountServiceImpl.java new file mode 100644 index 0000000..01afc59 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoCountServiceImpl.java @@ -0,0 +1,56 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskInfoCountService; +import jnpf.cultivate.service.FtbCultivateLearnTaskListService; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.model.cultivate.req.learn.QueryLearnTaskCountListReq; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoCountListVO; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @Author: xuguilin + * @create: 2024/9/9:14:50 + */ +@Service +public class FtbCultivateLearnTaskInfoCountServiceImpl implements FtbCultivateLearnTaskInfoCountService { + + @Autowired + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskInfoMapper; + + @Autowired + private FtbCultivateLearnTaskListService ftbCultivateLearnTaskListService; + + + @Autowired + private CultivateLearnUtils cultivateLearnUtils; + + + /** + * 查询学习任务统计列表 + * @param req + * @return + */ + @Override + public PageInfo queryCountList(QueryLearnTaskCountListReq req) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(req.getKeyWord().trim()); + } + Page queryPage = ftbCultivateLearnTaskInfoMapper.queryLearnCountList(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + cultivateLearnUtils.fillLearnCountList(records); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoServiceImpl.java new file mode 100644 index 0000000..eb8ae93 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskInfoServiceImpl.java @@ -0,0 +1,433 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivateLearnCategoriesService; +import jnpf.cultivate.service.FtbCultivateLearnTaskAssignmentService; +import jnpf.cultivate.service.FtbCultivateLearnTaskInfoService; +import jnpf.cultivate.service.FtbCultivateLearnTaskListService; +import jnpf.cultivate.utils.CultivateDateTimeUtils; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.enums.cultivate.v2.LearnTaskStatusEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseType; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.vo.learn.*; +import jnpf.model.cultivate.vo.learn.info.*; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @Author: peng.hao + * @create: 2024/9/9:14:50 + */ +@Slf4j +@Service +public class FtbCultivateLearnTaskInfoServiceImpl implements FtbCultivateLearnTaskInfoService { + + @Resource + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + @Resource + private FtbCultivateLearnTaskInfoMapper ftbCultivateLearnTaskInfoMapper; + + @Resource + private FtbCultivateLearnTaskCourseMapper cultivateLearnTaskCourseMapper; + + @Resource + private FtbCultivateLearnTaskExamMapper cultivateLearnTaskExamMapper; + + @Resource + private FtbCultivateExamMapper ftbCultivateExamMapper; + + @Resource + private FtbCultivateLearnTaskIdentificationMapper cultivateLearnTaskIdentificationMapper; + + @Resource + private CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Resource + private FtbCultivateLearnTaskCertificateMapper cultivateLearnTaskCertificateMapper; + + @Resource + private FtbCultivateCertificateMapper certificateMapper; + + @Resource + private FtbCultivateCourseTypeMapper cultivateCourseTypeMapper; + + @Resource + private FtbCultivateCourseMapper cultivateCourseMapper; + + @Resource + private FtbCultivateLearnTaskAssignmentMapper assignmentMapper; + + // @Autowired +// private V2UserApi userApi; + @Autowired + private UserApiV2Util userApiV2Util; + + + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + @Resource + private FtbCultivatePositionStatisticesMapper ftbCultivatePositionStatisticesMapper; + + @Autowired + private FtbCultivateLearnCategoriesService cultivateLearnCategoriesService; + + @Autowired + CultivateLearnUtils cultivateLearnUtils; + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + @Autowired + private FtbCultivateLearnTaskAssignmentService taskAssignmentService; + + @Autowired + private FtbCultivateLearnTaskListService learnTaskListService; + + @Override + public FtbCultivateLearnTaskInfoVO getLearnTaskInfo(String taskId) { + FtbCultivateLearnTask learnTask = ftbCultivateLearnTaskMapper.selectById(taskId); + if (learnTask == null) throw new RuntimeException("任务不存在!"); + FtbCultivateLearnTaskInfoVO convert = FtbCultivateLearnTaskInfoVO.convert(learnTask); + // 添加分类名称 + FtbCultivateLearnCategories categories = cultivateLearnCategoriesService.getById(convert.getTaskClass()); + convert.setTaskClassName(categories.getName()); + //补充当前分类和父接分类 + convert.setAllCategoryList(cultivateLearnCategoriesService.queryCategoryAndParentCategory(convert.getTaskClass())); + // 任务内容 + // 课程 + LambdaQueryWrapper learnTaskCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskCourseLambdaQueryWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, taskId); + List learnTaskCourses = cultivateLearnTaskCourseMapper.selectList(learnTaskCourseLambdaQueryWrapper); + if (CollUtil.isEmpty(learnTaskCourses)) { + throw new RuntimeException("该任务没有课程!"); + } + List courseIds = learnTaskCourses.stream().map(FtbCultivateLearnTaskCourse::getCourseId).collect(Collectors.toList()); + LambdaQueryWrapper courseLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseLambdaQueryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, courseIds); + List ftbCultivateCourses = cultivateCourseMapper.selectList(courseLambdaQueryWrapper); + // 获取课程信息 + Map courseNameMaps = ftbCultivateCourses.stream().collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, vo -> vo)); + List ftbCultivateCourseTypes = cultivateCourseTypeMapper.selectList(null); + // 获取课程类型 + Map courseTypeMaps = ftbCultivateCourseTypes.stream().collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, FtbCultivateCourseType::getName)); + + List covertCourses = learnTaskCourses.stream().map(item -> { + FtbCultivateLearnTaskCourseInfoVO convertInfo = FtbCultivateLearnTaskCourseInfoVO.convert(item); + if (courseNameMaps.containsKey(convertInfo.getCourseId())) { + FtbCultivateCourse ftbCultivateCourse = courseNameMaps.get(convertInfo.getCourseId()); + convertInfo.setCourseName(ftbCultivateCourse.getName()); + convertInfo.setCourseType(courseTypeMaps.get(ftbCultivateCourse.getTypeId())); + Integer format = ftbCultivateCourse.getFormat(); + convertInfo.setFormat(format); + convertInfo.setIsGroundIng(ftbCultivateCourse.getIsGroundIng()); + } + return convertInfo; + }).collect(Collectors.toList()); + // 添加课程内容 + convert.setCourseList(covertCourses); + // 添加考试 + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getEnableMark, 0); + examLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, taskId); + + FtbCultivateLearnTaskExam taskExam = cultivateLearnTaskExamMapper.selectOne(examLambdaQueryWrapper); + if (taskExam != null) { + FtbCultivateLearnTaskExamInfoVO examList = FtbCultivateLearnTaskExamInfoVO.convertExam(taskExam); + FtbCultivateExam ftbCultivateExam = ftbCultivateExamMapper.selectById(examList.getExamId()); + examList.setExamName(ftbCultivateExam.getExamName()); + convert.setExamList(examList); + } + // 添加鉴定 + LambdaQueryWrapper identificationLambdaQueryWrapper = Wrappers.lambdaQuery(); + identificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, taskId); + identificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + FtbCultivateLearnTaskIdentification learnTaskIdentification = cultivateLearnTaskIdentificationMapper.selectOne(identificationLambdaQueryWrapper); + if (learnTaskIdentification != null) { + FtbCultivateLearnTaskIdentificationInfoVO identificationInfoVO = FtbCultivateLearnTaskIdentificationInfoVO.convert(learnTaskIdentification); + CultivateIdentifyTable cultivateIdentifyTable = cultivateIdentifyTableMapper.selectById(identificationInfoVO.getIdentificationId()); + identificationInfoVO.setIdentificationName(cultivateIdentifyTable.getName()); + convert.setIdentificationList(identificationInfoVO); + } + // 添加证书 + LambdaQueryWrapper learnTaskCertificateLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskCertificateLambdaQueryWrapper.eq(FtbCultivateLearnTaskCertificate::getTaskId, taskId); + learnTaskCertificateLambdaQueryWrapper.eq(FtbCultivateLearnTaskCertificate::getEnableMark, 0); + FtbCultivateLearnTaskCertificate learnTaskCertificate = cultivateLearnTaskCertificateMapper.selectOne(learnTaskCertificateLambdaQueryWrapper); + if (learnTaskCertificate != null) { + FtbCultivateLearnTaskCertificateInfoVO certificateList = FtbCultivateLearnTaskCertificateInfoVO.convert(learnTaskCertificate); + certificateList.setCertificateName(certificateMapper.selectById(certificateList.getCertificateId()).getName()); + convert.setCertificateList(certificateList); + } + return convert; + } + + @Override + public PageListVO getListOfCompletions(String taskId, CultivatePage oldPage, List ids) { + Page page = oldPage.coverCultivatePage(); + List records = ftbCultivateLearnTaskInfoMapper.getListOfCompletions(taskId, ids); + List userIds = records.stream().map(FtbCultivateLearnTaskFinishInfoVO::getUserId).collect(Collectors.toList()); + Map finalInfosByUserIdMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + List taskFinishInfoVOS = records.stream().filter(v -> finalInfosByUserIdMap.containsKey(v.getUserId())).map(item -> { + if (finalInfosByUserIdMap.containsKey(item.getUserId())) { + // 获取默认组织 + UserBoundVO userBoundMoreInfoVO = finalInfosByUserIdMap.get(item.getUserId()); + if (userBoundMoreInfoVO != null) { + item.setUserName(userBoundMoreInfoVO.getUserName()); + item.setOrgName(userBoundMoreInfoVO.getOrganizeName()); + String positionName = userBoundMoreInfoVO.getPositionName(); + if (userBoundMoreInfoVO.getGradeName() != null) { + positionName = positionName + "/" + userBoundMoreInfoVO.getGradeName(); + } + item.setPositionAndGradesName(positionName); + item.setEmployeeID(userBoundMoreInfoVO.getSystemWorkerId()); + } + } + if (item.getTaskStartTime() != null && item.getTaskEndTime() != null) { + // 计算任务时长 + TimeDifference timeDifference = CultivateDateTimeUtils.calculateDifference(item.getTaskStartTime(), item.getTaskEndTime()); + item.setTaskDuration(timeDifference.toString()); + } + return item; + }).collect(Collectors.toList()); + return CultivatePage.paginate(taskFinishInfoVOS, page); + } + + @Override + public FtbCultivateLearnTaskFinishStatisticsVO getCompletionStatistics(String taskId) { + // 根据任务统计该任务下人员完成情况 + FtbCultivateLearnTaskFinishStatisticsVO statisticsVO = new FtbCultivateLearnTaskFinishStatisticsVO(); + LambdaQueryWrapper taskAssignmentLambdaQueryWrapper = Wrappers.lambdaQuery(); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List taskAssignmentList = assignmentMapper.selectList(taskAssignmentLambdaQueryWrapper); + Map> listMap = taskAssignmentList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskAssignment::getStudyStats, Collectors.toList())); + // 应完成人数 + // 0未开始,1进行中,2已完成,3已逾期 + statisticsVO.setBeCompleted(taskAssignmentList.size()); + // 0未开始 + if (listMap.containsKey(0)) statisticsVO.setDidnTStart(listMap.get(0).size()); + // 1进行中 + if (listMap.containsKey(1)) statisticsVO.setOngoing(listMap.get(1).size()); + // 2已完成 + if (listMap.containsKey(2)) statisticsVO.setDone(listMap.get(2).size()); + // 3已逾期 + if (listMap.containsKey(3)) statisticsVO.setOverdue(listMap.get(3).size()); + return statisticsVO; + } + + @Override + public FtbCultivateLearnTaskUserFinishInfoVO viewTaskCompletion(String taskId, String userId) { + // 构建返回对象 + FtbCultivateLearnTaskUserFinishInfoVO infoVO = new FtbCultivateLearnTaskUserFinishInfoVO(); + // 查询当前人任务完成情况 + List learnings = ftbCultivateLearnTaskInfoMapper.queryCourseUserInfo(taskId, userId); + learnings.forEach(item -> { + // 课程学习进度(%) + Integer a = ftbCultivatePositionStatisticesMapper.courseLearningProgress(item.getCourseId(), userId, null); + Integer b = ftbCultivatePositionStatisticesMapper.courseLearningProgress(item.getCourseId(), userId, 1); + item.setLearningProgress(FtbCultivatePositionStatisticesServiceImpl.computeDivision(b, a)); + }); + infoVO.setCourseInfoVOS(learnings); + // 添加考试 + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, taskId); + FtbCultivateLearnTaskExam taskExam = cultivateLearnTaskExamMapper.selectOne(examLambdaQueryWrapper); + if (taskExam != null) { + FtbCultivateLearnTaskExamUserInfoVO examUserInfoVO = new FtbCultivateLearnTaskExamUserInfoVO(); + FtbCultivateExam ftbCultivateExam = ftbCultivateExamMapper.selectById(taskExam.getExamId()); + examUserInfoVO.setExamName(ftbCultivateExam.getExamName()); + // 考试信息 +// FtbCultivateLearnTaskExamUserInfoVO examUserInfoVO1 = ftbCultivateLearnTaskInfoMapper.queryExamInfo(taskId, userId); + LambdaQueryWrapper userExamWraper = Wrappers.lambdaQuery(); + userExamWraper.eq(FtbCultivateExamUser::getExamSource, 4); + userExamWraper.eq(FtbCultivateExamUser::getRelationTaskId, taskId); + userExamWraper.eq(FtbCultivateExamUser::getUserId, userId); + userExamWraper.eq(FtbCultivateExamUser::getEnabledMark, 1); + userExamWraper.orderByDesc(FtbCultivateExamUser::getCreatorTime); + List examUserList = ftbCultivateExamUserMapper.selectList(userExamWraper); + + /** 考试状态 是否完成 (0否 1 是)*/ + Integer isFinish = 0; + /** 用时 (单位秒)*/ + Long unavailable = 0L; + if (CollUtil.isNotEmpty(examUserList)) { + FtbCultivateExamUser examUser = examUserList.get(0); + if (examUser.getStatus() == 0 || examUser.getStatus() == 2) { + isFinish = 0; + } else { + isFinish = 1; + unavailable = examUser.getDuration(); + } + examUserInfoVO.setUserExamId(examUser.getId()); + examUserInfoVO.setExamStatus(examUser.getStatus()); + } + examUserInfoVO.setIsFinish(isFinish); + examUserInfoVO.setUnavailable(unavailable); + examUserInfoVO.setExamId(taskExam.getExamId()); + infoVO.setExamInfoVOS(examUserInfoVO); + } + // 添加鉴定 + LambdaQueryWrapper identificationLambdaQueryWrapper = Wrappers.lambdaQuery(); + identificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, taskId); + FtbCultivateLearnTaskIdentification learnTaskIdentification = cultivateLearnTaskIdentificationMapper.selectOne(identificationLambdaQueryWrapper); + if (learnTaskIdentification != null) { + FtbCultivateLearnTaskIdentificationUserInfoVO identificationUserInfoVO = new FtbCultivateLearnTaskIdentificationUserInfoVO(); + CultivateIdentifyTable cultivateIdentifyTable = cultivateIdentifyTableMapper.selectById(learnTaskIdentification.getIdentificationId()); + identificationUserInfoVO.setIdentificationName(cultivateIdentifyTable.getName()); + // 鉴定 + FtbCultivateLearnTaskIdentificationUserInfoVO identificationInfoVOS = ftbCultivateLearnTaskInfoMapper.queryIdentificationInfo(taskId, userId); + identificationUserInfoVO.setIsFinish(identificationInfoVOS == null ? 0 : identificationInfoVOS.getIsFinish()); + if (identificationInfoVOS != null) { + identificationUserInfoVO.setIdentifyResult(identificationInfoVOS.getIdentifyResult()); + identificationUserInfoVO.setIdentifyStatus(identificationInfoVOS.getIdentifyStatus()); + } + infoVO.setIdentificationInfoVOS(identificationUserInfoVO); + } + // 添加证书 + LambdaQueryWrapper learnTaskCertificateLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskCertificateLambdaQueryWrapper.eq(FtbCultivateLearnTaskCertificate::getTaskId, taskId); + FtbCultivateLearnTaskCertificate learnTaskCertificate = cultivateLearnTaskCertificateMapper.selectOne(learnTaskCertificateLambdaQueryWrapper); + if (learnTaskCertificate != null) { + FtbCultivateLearnTaskCertificateUserInfoVO ftbCultivateLearnTaskCertificateUserInfoVO = new FtbCultivateLearnTaskCertificateUserInfoVO(); + ftbCultivateLearnTaskCertificateUserInfoVO.setCertificateName(certificateMapper.selectById(learnTaskCertificate.getCertificateId()).getName()); + // 证书 + FtbCultivateLearnTaskCertificateUserInfoVO certificateInfoVOS = ftbCultivateLearnTaskInfoMapper.queryCertificateInfo(taskId, userId); + ftbCultivateLearnTaskCertificateUserInfoVO.setIsFinish(certificateInfoVOS == null ? 0 : certificateInfoVOS.getIsFinish()); + infoVO.setCertificateInfoVOS(ftbCultivateLearnTaskCertificateUserInfoVO); + } + return infoVO; + } + + @Override + @Transactional + public PageListVO getTaskList(FtbCultivateLearnTaskListDto taskListDto) { + Page page = taskListDto.coverCultivatePage(); +// updateUserFinishTime(null);// +// cultivateLearnUtils.updateUserTaskStatus(UserProvider.getUser().getTenantId()); +// learnTaskListService.preUpdateTaskStatus(cultivateLearnUtils.getTenantId()); + page = ftbCultivateLearnTaskInfoMapper.getTaskList(page, taskListDto); + return CultivatePage.coverPageList(page); + } + + /** + * 培训任务列表列表小气泡计数 + * + * @param keyWords 任务名称 + * @return + */ + @Override + public Map getTaskCount(String keyWords) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.ne(FtbCultivateLearnTask::getStatus, LearnTaskStatusEnum.UNPUBLISHED.getCode()); + wrapper.like(StringUtils.isNotEmpty(keyWords), FtbCultivateLearnTask::getTaskName, keyWords); + List learnTasks = ftbCultivateLearnTaskMapper.selectList(wrapper); + if (learnTasks == null) return null; + Map hashMap = new HashMap<>(); + // 1 未开始 ,2 进行中,3 已结束 + hashMap.put("total", learnTasks.size()); + hashMap.put("didnTStart", countStatus(learnTasks, LearnTaskStatusEnum.NOT_STARTED.getCode())); + hashMap.put("ongoing", countStatus(learnTasks, LearnTaskStatusEnum.IN_PROGRESS.getCode())); + hashMap.put("ended", countStatus(learnTasks, LearnTaskStatusEnum.FINISHED.getCode())); + + return hashMap; + } + + private Integer countStatus(List learnTasks, Integer status) { + Predicate predicate = item -> Objects.equals(item.getStatus(), status); + return Math.toIntExact(learnTasks.stream().filter(predicate).count()); + } + + @Override + @Transactional + public void updateUserFinishTime(String userId) { + /** + * 获取学习任务详情进行实时更新 + */ + List taskIds = new ArrayList<>(); + + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + lambdaQuery.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + lambdaQuery.notIn(FtbCultivateLearnTaskAssignment::getStudyStats, 2, 3); + List taskAssignments = assignmentMapper.selectList(lambdaQuery); + if (CollUtil.isEmpty(taskAssignments)) return; + taskIds = taskAssignments.stream() + .map(FtbCultivateLearnTaskAssignment::getTaskId) + .collect(Collectors.toList()); + + // 进入状态更新 + // 1.更新任务状态 + LambdaQueryWrapper taskLambdaQueryWrapper = Wrappers.lambdaQuery(); + taskLambdaQueryWrapper.in(FtbCultivateLearnTask::getStatus, + LearnTaskStatusEnum.NOT_STARTED.getCode(), + LearnTaskStatusEnum.IN_PROGRESS.getCode(), + LearnTaskStatusEnum.FINISHED.getCode()); + taskLambdaQueryWrapper.eq(FtbCultivateLearnTask::getTaskType, 1); + if (CollUtil.isNotEmpty(taskIds)) taskLambdaQueryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, taskIds); + List learnTasks = ftbCultivateLearnTaskMapper.selectList(taskLambdaQueryWrapper); + if (CollUtil.isEmpty(learnTasks)) return; + + //转换成map + Map taskMap = learnTasks.stream().collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, Function.identity())); + Date nowDate = new Date(); + List needUpdate = new ArrayList<>(); + for (FtbCultivateLearnTaskAssignment taskAssignment : taskAssignments) { + FtbCultivateLearnTask task = taskMap.get(taskAssignment.getTaskId()); + if (task == null) { + continue; + } + // 任务结束时间 + Date timeLimitEndTime = task.getTimeLimitEndTime(); + /** 学习状态 0未开始,1进行中,2已完成,3已逾期*/ + if (nowDate.after(timeLimitEndTime)) { + taskAssignment.setStudyStats(3); + needUpdate.add(taskAssignment); + } + if (taskAssignment.getStudyStats() == 1 && task.getStudentTimeCompletion() == 0) { + //进行中判断是否限时 + Date endCompleteTime = taskAssignment.getLearningStartTime(); + //学员限时完成单位(0,天,1,小时) + if (task.getTimeCompletionUnit() == 0) { + endCompleteTime = DateUtil.offsetDay(endCompleteTime, task.getTimeCompletionDays()); + } else { + endCompleteTime = DateUtil.offsetHour(endCompleteTime, task.getTimeCompletionDays()); + } + if (nowDate.after(endCompleteTime)) { + taskAssignment.setStudyStats(3); + } + needUpdate.add(taskAssignment); + } + } + if (CollUtil.isNotEmpty(needUpdate)) { + taskAssignmentService.updateBatchById(needUpdate, needUpdate.size()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskListServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskListServiceImpl.java new file mode 100644 index 0000000..29fbad6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskListServiceImpl.java @@ -0,0 +1,193 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskListService; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.cultivate.utils.CultivateTaskLeanAsyncDealUtil; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.req.learn.QueryLearnTaskListReq; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoListVO; +import jnpf.model.cultivate.vo.learn.WebLearnTaskDto; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: xuguilin + * @create: 2024/9/9:14:50 + */ +@Service +public class FtbCultivateLearnTaskListServiceImpl implements FtbCultivateLearnTaskListService { + + @Autowired + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + + @Autowired + private CultivateLearnUtils cultivateLearnUtils; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private CultivateTaskLeanAsyncDealUtil cultivateTaskLeanAsyncDealUtil; + + + /** + * 获取任务列表 + * + * @param req + * @return + */ + @Override + public PageInfo getWebPageList(QueryLearnTaskListReq req) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(req.getKeyWord().trim()); + } +// preUpdateTaskStatus(cultivateLearnUtils.getTenantId()); + Page queryPage = ftbCultivateLearnTaskMapper.getMyWebPageList(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + cultivateLearnUtils.fillTaskInfoList(records); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * web端统计所有任务数量接口 + * + * @return + */ + + @Override + public WebLearnTaskDto countTaskNumber() { + + // 使用单次聚合查询替代6次独立查询,提升性能 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select( + "COUNT(*) as all_num", + "SUM(CASE WHEN F_TaskType = 1 THEN 1 ELSE 0 END) as time_limit_num", + "SUM(CASE WHEN F_TaskType = 0 THEN 1 ELSE 0 END) as long_term_num", + "SUM(CASE WHEN F_Status = 2 THEN 1 ELSE 0 END) as running_num", + "SUM(CASE WHEN F_Status = 3 THEN 1 ELSE 0 END) as over_num", + "SUM(CASE WHEN F_Status = 4 THEN 1 ELSE 0 END) as stop_num" + ).eq("F_EnableMark", 0); + + Map result = ftbCultivateLearnTaskMapper.selectMaps(wrapper).get(0); + + WebLearnTaskDto dto = new WebLearnTaskDto(); + dto.setAllNum(getLongValue(result.get("all_num"))); + dto.setTimeLimitNum(getLongValue(result.get("time_limit_num"))); + dto.setLongTermNum(getLongValue(result.get("long_term_num"))); + dto.setRunningNum(getLongValue(result.get("running_num"))); + dto.setOverNum(getLongValue(result.get("over_num"))); + dto.setStopNum(getLongValue(result.get("stop_num"))); + + return dto; + } + + /** + * 安全地将Object转换为Long + */ + private Long getLongValue(Object value) { + if (value == null) { + return 0L; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.parseLong(value.toString()); + } + + /** + * 分类统计任务数量 + * + * @return 分类id->任务数量 + */ + @Override + public Map groupCountCateNum() { + Map ret = new HashMap<>(); + List list = ftbCultivateLearnTaskMapper.groupCountCateNum(); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + return ret; + } + + /** + * 查询分类下是否有任务 + * + * @param cateIds 分类IDS + * @return true-有 false-没有 + */ + @Override + public Boolean checkHasTask(List cateIds) { + if (CollectionUtil.isEmpty(cateIds)) { + return false; + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .in(FtbCultivateLearnTask::getTaskClass, cateIds) + .eq(FtbCultivateLearnTask::getEnableMark, 0); + Long count = ftbCultivateLearnTaskMapper.selectCount(wrapper); + if (count == null || count <= 0) { + return false; + } + return true; + } + + /** + * 预更新任务状态 + */ + @Override + public void preUpdateTaskStatus(String tenantId) { + String lockKey = "preUpdateTaskStatus:" + tenantId; + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(), 3, TimeUnit.SECONDS)) { + try { + Date now = new Date(); + //修改进行中 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(FtbCultivateLearnTask::getId) + .le(FtbCultivateLearnTask::getTimeLimitStartTime, now) + .ge(FtbCultivateLearnTask::getTimeLimitEndTime, now) + .eq(FtbCultivateLearnTask::getStatus, 1) + .eq(FtbCultivateLearnTask::getTaskType, 1) + .eq(FtbCultivateLearnTask::getEnableMark, 0); + List taskList = ftbCultivateLearnTaskMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(taskList)) { + //获取任务ids集合 + List taskIds = taskList.stream().map(FtbCultivateLearnTask::getId).collect(Collectors.toList()); + ftbCultivateLearnTaskMapper.update(null, new LambdaUpdateWrapper().set(FtbCultivateLearnTask::getStatus, 2).in(FtbCultivateLearnTask::getId, taskIds).eq(FtbCultivateLearnTask::getEnableMark, 0)); + cultivateTaskLeanAsyncDealUtil.asyncSendStartTaskLearningAlert(taskList, tenantId); + } + //修改结束 + ftbCultivateLearnTaskMapper.update(null, new LambdaUpdateWrapper().set(FtbCultivateLearnTask::getStatus, 3).le(FtbCultivateLearnTask::getTimeLimitEndTime, now).notIn(FtbCultivateLearnTask::getStatus, 0, 3, 4).eq(FtbCultivateLearnTask::getTaskType, 1).eq(FtbCultivateLearnTask::getEnableMark, 0)); + //异步处理所有用户任务的状态 + cultivateTaskLeanAsyncDealUtil.asyncUpdateUserTaskStatus(tenantId); + } catch (Exception e) { + e.printStackTrace(); + } finally { + redisTemplate.delete(lockKey); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskReminderRuleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskReminderRuleServiceImpl.java new file mode 100644 index 0000000..1363870 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskReminderRuleServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskReminderRuleMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskReminderRuleService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskReminderRule; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +@Service +public class FtbCultivateLearnTaskReminderRuleServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskReminderRuleService{ + + @Resource + private FtbCultivateLearnTaskReminderRuleMapper ftbCultivateLearnTaskReminderRuleMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskServiceImpl.java new file mode 100644 index 0000000..9331c57 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateLearnTaskServiceImpl.java @@ -0,0 +1,75 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskService; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.v2.task.vo.V2MyCultivateLearnTaskListVo; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:11:05 +* +*/ +@Service +public class +FtbCultivateLearnTaskServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskService{ + + /** + * 查询用户未完成的任务列表 + * + * @param userId 用户ID + * @return 返回V2MyCultivateLearnTaskListVo类型的任务列表,包含用户所有未完成的学习任务信息 + */ + @Override + public List queryMyNoCompleteTaskListForUserId(String userId) { + return baseMapper.queryMyNoCompleteTaskListForUserId(userId); + } + + /** + * 查询任务是否关联指定考试 + * + * @param taskIds 任务ID列表 + * @param examId 考试ID + * @return 返回关联了指定考试的任务ID列表 + */ + @Override + public List queryTaskHasExam(List taskIds, String examId) { + return baseMapper.queryTaskHasExam(taskIds, examId); + } + + /** + * 查询任务是否关联指定练习 + * + * @param taskIds 任务ID列表 + * @param practiceId 练习ID + * @return 返回关联了指定练习的任务ID列表 + */ + @Override + public List queryTaskHasPracticeId(List taskIds, String practiceId) { + return baseMapper.queryTaskHasPracticeId(taskIds, practiceId); + } + + /** + * 查询任务是否关联指定身份 + * + * @param taskIds 任务ID列表 + * @param identityId 身份ID + * @return 返回关联了指定身份的任务ID列表 + */ + @Override + public List queryTaskHasIdentityId(List taskIds, String identityId) { + return baseMapper.queryTaskHasIdentityId(taskIds, identityId); + } + + @Override + public List queryMyRunningTask(String userId, String courseId) { + return baseMapper.queryMyRunningTask(userId,courseId); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateMyLearnTaskInfoServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateMyLearnTaskInfoServiceImpl.java new file mode 100644 index 0000000..7ec8b6d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateMyLearnTaskInfoServiceImpl.java @@ -0,0 +1,527 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.CultivateDateTimeUtils; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.exception.HandleException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnTaskListDto; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.vo.course.app.FtbAppTaskCountVO; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskExamInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskIdentificationInfoVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoForAppVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateMyLearnTaskListVO; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @Author: peng.hao + * @create: 2024/9/11:14:51 + */ +@Slf4j +@Service +public class FtbCultivateMyLearnTaskInfoServiceImpl implements FtbCultivateMyLearnTaskInfoService { + + @Resource + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + @Resource + private FtbCultivateLearnTaskInfoMapper ftbCultivateLearnTaskInfoMapper; + + @Resource + private FtbCultivateLearnTaskInfoService ftbCultivateLearnTaskInfoService; + + @Resource + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + @Resource + private FtbCultivateLearnTaskCourseMapper ftbCultivateLearnTaskCourseMapper; + + @Resource + private FtbCultivateLearnTaskExamMapper learnTaskExamMapper; + + @Resource + private FtbCultivateLearnTaskIdentificationMapper learnTaskIdentificationMapper; + + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Resource + private CultivateIdentifyApplyMapper cultivateIdentifyApplyMapper; + + @Resource + private FtbCultivateExamService ftbCultivateExamService; + + @Resource + private FtbCultivateMyLearnTaskInfoMapper ftbCultivateMyLearnTaskInfoMapper; + + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + private FtbCultivateLearnCategoriesService cultivateLearnCategoriesService; + + @Resource + private FtbCultivateExamMapper ftbCultivateExamMapper; + + @Resource + private FtbCultivateTestPaperMapper ftbCultivateTestPaperMapper; + + @Resource + private CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Resource + private CultivateIdentifyApplyDetailsMapper cultivateIdentifyApplyDetailsMapper; + + @Resource + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Override + @Transactional + public PageListVO taskList(FtbCultivateLearnTaskListDto taskListDto) { + String userId = UserProvider.getUser().getUserId(); + // 更新任务状态 + ftbCultivateLearnTaskInfoService.updateUserFinishTime(userId); + taskListDto.setUserId(userId); + Page tPage = taskListDto.coverCultivatePage(); + Page taskPage = ftbCultivateLearnTaskInfoMapper.queryTaskList(tPage, taskListDto); + taskPage.getRecords().forEach(item -> { + if (item.getTaskType() == 1) { + // 获取任务状态 + Date learningEndTime = item.getTimeLimitEndTime(); + String remainingValue = CultivateDateTimeUtils.calculateRemainingValue(new Date(), learningEndTime, 0); + item.setDaysRemaining(remainingValue); + } + }); + return CultivatePage.coverPageList(taskPage); + } + + /** + * 查询任务课程列表 + * + * @param taskId 任务主键ID(必传) + * @param userId + * @param compulsory + * @return + * @throws HandleException + */ + @Override + public List taskCourseList(String taskId, String userId, Integer compulsory) throws HandleException { + + List curriculumAppVOS = ftbCultivateMyLearnTaskInfoMapper.queryCourseList(taskId, userId, compulsory); + curriculumAppVOS.forEach(item -> { + // 学习总人数 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(FtbCultivatePositionCourceLearning::getState, List.of(1, 2)); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, item.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + item.setLearnTotalNumber(ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper)); + }); + // 查询当前任务是否开始 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + wrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + wrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + FtbCultivateLearnTaskAssignment taskAssignment = ftbCultivateLearnTaskAssignmentMapper.selectOne(wrapper); + if (ObjectUtil.isNull(taskAssignment)) { + throw new HandleException("已变更,无法继续完成"); + } + // 0是必修 && 学习状态为开始 + if (compulsory == 0 && taskAssignment.getStudyStats() == 1) { + // 任务必修课程 + LambdaQueryWrapper taskCourseQueryWrapper = Wrappers.lambdaQuery(); + taskCourseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, taskId); + taskCourseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getEnableMark, 0); + taskCourseQueryWrapper.eq(FtbCultivateLearnTaskCourse::getIsRequired, 0); + List ftbCultivateLearnTaskCourses = ftbCultivateLearnTaskCourseMapper.selectList(taskCourseQueryWrapper); + List courseIds = ftbCultivateLearnTaskCourses.stream() + .map(FtbCultivateLearnTaskCourse::getCourseId) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(courseIds)) { + boolean isStartStudyCourse = checkCourseStudyStatus(userId, courseIds); + if (isStartStudyCourse == true) { + int needStudyNum = 0; + LambdaQueryWrapper courseWraper = Wrappers.lambdaQuery(); + courseWraper.in(FtbCultivateCourse::getId, courseIds); + courseWraper.eq(FtbCultivateCourse::getEnableMark, 0); + courseWraper.eq(FtbCultivateCourse::getIsGroundIng, 1);//上下架(0下架,1上架) + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(courseWraper); + if (CollUtil.isEmpty(ftbCultivateCourses)) { + needStudyNum = 0; + } else { + needStudyNum = ftbCultivateCourses.size(); + courseIds = ftbCultivateCourses.stream() + .map(FtbCultivateCourse::getId) + .collect(Collectors.toList()); + } + + if (needStudyNum == 0 || hasTheCourseBeenCompleted(userId, courseIds)) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + ExamIdentifyTriggerEventDTO.builder() + .taskId(taskId) + .userId(userId) + .triggerEventType(TriggerEventBO.TriggerEventType.TASK) + .build())); + } + } + } + } + return curriculumAppVOS; + } + + /** + * 查询课程是否开始学习了 + * + * @param userId 用户ID + * @param courseIds 课程id + * @return true -开始学习 false-未开始学习 + */ + private boolean checkCourseStudyStatus(String userId, List courseIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + query.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + query.in(FtbCultivatePositionCourceLearning::getState, List.of("1"));//学习状态 1已学习,0未学习,2学习中 + query.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + return ftbCultivatePositionCourceLearningMapper.selectCount(query) > 0; + } + + /** + * 查询是否完成了课程学习 + * + * @param userId 用户id + * @param courseIds 课程id + * @return + */ + private boolean hasTheCourseBeenCompleted(String userId, List courseIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + query.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + query.in(FtbCultivatePositionCourceLearning::getState, List.of("0", "2")); + query.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + return ftbCultivatePositionCourceLearningMapper.selectCount(query) == 0; + } + + @Override + public FtbCultivateLearnTaskInfoForAppVO getLearnTaskInfoForApp(String taskId) { + // 任务已变更,无法继续完成; + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateLearnTask::getId, taskId); + FtbCultivateLearnTask ftbCultivateLearnTask = ftbCultivateLearnTaskMapper.selectOne(queryWrapper); + if (ftbCultivateLearnTask == null) throw new RuntimeException("任务已变更,无法继续完成!"); + // if (ftbCultivateLearnTask.getStatus() == 3) throw new RuntimeException("任务已结束,无法继续完成!"); + // 查询当前人的任务状态 + LambdaQueryWrapper taskAssignmentLambdaQueryWrapper = Wrappers.lambdaQuery(); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, UserProvider.getUser().getUserId()); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + FtbCultivateLearnTaskAssignment taskAssignment = ftbCultivateLearnTaskAssignmentMapper.selectOne(taskAssignmentLambdaQueryWrapper); + FtbCultivateLearnTaskInfoForAppVO convert = FtbCultivateLearnTaskInfoForAppVO.convert(ftbCultivateLearnTask); + convert.setStudyStats(taskAssignment.getStudyStats()); + // 添加分类名称 + FtbCultivateLearnCategories categories = cultivateLearnCategoriesService.getById(convert.getTaskClass()); + convert.setTaskClassName(categories.getName()); + return convert; + } + + @Override + public void completeTask(String taskId) { + String userId = UserProvider.getUser().getUserId(); + // 更新学习开始时间 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId) + .eq(FtbCultivateLearnTaskAssignment::getUserId, userId) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark,0) + .set(FtbCultivateLearnTaskAssignment::getLearningStartTime, new Date()) + .set(FtbCultivateLearnTaskAssignment::getStudyStats, 1); + ftbCultivateLearnTaskAssignmentMapper.update(null, updateWrapper); + } + + @Override + public List taskExamList(String taskId, String userId) { + List reult = new ArrayList<>(); + // 添加考试 + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, taskId); + FtbCultivateLearnTaskExam taskExam = learnTaskExamMapper.selectOne(examLambdaQueryWrapper); + if (taskExam == null) return reult; + + // 根据任务ID和用户ID查询考试结果 + List cultivateExamUser = ftbCultivateExamUserService.queryExamResultForTaskInfo(taskId, userId); + // 只返回考试 + if (CollUtil.isEmpty(cultivateExamUser)) { + FtbCultivateExam ftbCultivateExam = ftbCultivateExamMapper.selectById(taskExam.getExamId()); + FtbCultivateLearnTaskExamInfoVO learnTaskExamInfoVO = FtbCultivateLearnTaskExamInfoVO.convertExam(taskExam); + learnTaskExamInfoVO.setExamName(ftbCultivateExam.getExamName()); + learnTaskExamInfoVO.setExamStatus(0); + learnTaskExamInfoVO.setExamType(ftbCultivateExam.getExamType()); + learnTaskExamInfoVO.setExamTime(ftbCultivateExam.getExamTime()); + learnTaskExamInfoVO.setExamId(ftbCultivateExam.getId()); + //由于任务只关联一个考试 查询考试关联的试卷信息 + if (null != ftbCultivateExam && StringUtils.isNotEmpty(ftbCultivateExam.getPaperId())) { + FtbCultivateTestPaper paper = ftbCultivateTestPaperMapper.selectById(ftbCultivateExam.getPaperId()); + if (null != paper) { + learnTaskExamInfoVO.setPaperName(paper.getName()); + } + } + reult.add(learnTaskExamInfoVO); + return reult; + } + // 使用Lambda表达式创建查询包装器 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + // 根据考试ID列表查询考试详情 + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, cultivateExamUser.stream().map(FtbCultivateExamUser::getExamId).collect(Collectors.toList())); + List list = ftbCultivateExamService.list(queryWrapper); + // 初始化一个映射考试ID到考试详情的Map + Map collect = new HashMap<>(); + if (CollUtil.isNotEmpty(list)) { + // 将查询到的考试详情列表转换为Map,以考试ID为键 + collect = list.stream().collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, Function.identity())); + } + Map finalCollect = collect; + // 将考试结果和考试详情合并 + return cultivateExamUser.stream().map(vo -> { + // 根据考试ID获取考试详情 + FtbCultivateExam ftbCultivateExam = finalCollect.get(vo.getExamId()); + // 将考试结果转换为VO + FtbCultivateLearnTaskExamInfoVO learnTaskExamInfoVO = FtbCultivateLearnTaskExamInfoVO.convert(vo); + // 如果考试详情存在,则设置考试分数和名称 + if (ftbCultivateExam != null) { + learnTaskExamInfoVO.setExamScore(ftbCultivateExam.getCurrTotalScore()); + learnTaskExamInfoVO.setExamName(ftbCultivateExam.getExamName()); + learnTaskExamInfoVO.setExamTime(ftbCultivateExam.getExamTime()); + } + if ( StringUtils.isNotEmpty(vo.getPaperId())) { + FtbCultivateTestPaper paper = ftbCultivateTestPaperMapper.selectById(vo.getPaperId()); + if (null != paper) { + learnTaskExamInfoVO.setPaperName(paper.getName()); + } + } + return learnTaskExamInfoVO; + }).collect(Collectors.toList()); + } + + @Override + public List taskIdentificationList(String taskId, String userId) { + List reult = new ArrayList<>(); + // 添加鉴定 + LambdaQueryWrapper identificationLambdaQueryWrapper = Wrappers.lambdaQuery(); + identificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, taskId); + FtbCultivateLearnTaskIdentification learnTaskIdentification = learnTaskIdentificationMapper.selectOne(identificationLambdaQueryWrapper); + if (learnTaskIdentification == null) return reult; + FtbCultivateLearnTaskIdentificationInfoVO identificationUserInfoVO = new FtbCultivateLearnTaskIdentificationInfoVO(); + CultivateIdentifyTable cultivateIdentifyTable = cultivateIdentifyTableMapper.selectById(learnTaskIdentification.getIdentificationId()); + if (cultivateIdentifyTable != null) { + identificationUserInfoVO.setIdentificationName(cultivateIdentifyTable.getName()); + identificationUserInfoVO.setIdentificationId(cultivateIdentifyTable.getId()); + identificationUserInfoVO.setIdentificationName(cultivateIdentifyTable.getName()); + identificationUserInfoVO.setIdentificationType(4); + } + List ftbCultivateLearnTaskIdentificationInfoVOS = cultivateIdentifyApplyMapper.queryIdentifyApplyInfo(taskId, userId); + // 鉴定 + if (CollUtil.isEmpty(ftbCultivateLearnTaskIdentificationInfoVOS)) { + reult.add(identificationUserInfoVO); + return reult; + } + for (FtbCultivateLearnTaskIdentificationInfoVO ftbCultivateLearnTaskIdentificationInfoVO : ftbCultivateLearnTaskIdentificationInfoVOS) { + BigDecimal identificationScore = cultivateIdentifyApplyDetailsMapper.countSumScore(ftbCultivateLearnTaskIdentificationInfoVO.getIdentificationId()); + ftbCultivateLearnTaskIdentificationInfoVO.setIdentificationScore(identificationScore); + } + return ftbCultivateLearnTaskIdentificationInfoVOS; + } + + @Override + public void updateUserFinishTime(String taskId, String userId, Integer source) { + log.error("updateUserFinishTime: taskId:{}, userId:{} , source:{}", taskId, userId, source); + // 查询是否有 + // 0:课程 1:考试 2:鉴定 + LambdaQueryWrapper courseLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseLambdaQueryWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, taskId); + courseLambdaQueryWrapper.eq(FtbCultivateLearnTaskCourse::getEnableMark, 0); + List cultivateLearnTaskCourse = ftbCultivateLearnTaskCourseMapper.selectList(courseLambdaQueryWrapper); + // 查询任务绑定考试 + LambdaQueryWrapper learnTaskExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskExamLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, taskId); + learnTaskExamLambdaQueryWrapper.eq(FtbCultivateLearnTaskExam::getEnableMark, 0); + FtbCultivateLearnTaskExam ftbCultivateLearnTaskExam = learnTaskExamMapper.selectOne(learnTaskExamLambdaQueryWrapper); + // 查询任务绑定实操鉴定 + LambdaQueryWrapper learnTaskIdentificationLambdaQueryWrapper = Wrappers.lambdaQuery(); + learnTaskIdentificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, taskId); + learnTaskIdentificationLambdaQueryWrapper.eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + FtbCultivateLearnTaskIdentification learnTaskIdentification = learnTaskIdentificationMapper.selectOne(learnTaskIdentificationLambdaQueryWrapper); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + FtbCultivateLearnTaskAssignment taskAssignment = ftbCultivateLearnTaskAssignmentMapper.selectOne(queryWrapper); + + + if (taskAssignment != null && 2 == taskAssignment.getStudyStats()) return; + if (source == 0 && CollUtil.isNotEmpty(cultivateLearnTaskCourse) + && (ftbCultivateLearnTaskExam == null && learnTaskIdentification == null)) { + // 更新学习开始时间 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId) + .eq(FtbCultivateLearnTaskAssignment::getUserId, userId) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .set(FtbCultivateLearnTaskAssignment::getStudyStats, 2) + .set(FtbCultivateLearnTaskAssignment::getLearningEndTime, new Date()); + ftbCultivateLearnTaskAssignmentMapper.update(null, updateWrapper); + } else { + boolean examFlag = false, identificationFlag = false; + if (ftbCultivateLearnTaskExam == null) { + examFlag = true; + } else { + Integer isPassComplete = ftbCultivateLearnTaskExam.getIsPassComplete(); + // 考试情况 + // 考试id + // 根据任务id加考试id+用户id查询当前人的考试情况 + String examId = ftbCultivateLearnTaskExam.getExamId(); + //-1 没有考试记录 0:未完成考试 3合格 5优秀 + + Integer forTask = ftbCultivateExamUserService.queryExamResultForTask(taskId, userId); +// if (forTask != -1) examFlag = true; + if (forTask == 3 || forTask == 5) { + examFlag = true; + } + if (isPassComplete == 0 && forTask > 0 && forTask != 1) { + //不需要考试合格才算完成 + examFlag = true; + } + + } + // 不存在鉴定 + if (learnTaskIdentification == null) { + identificationFlag = true; + } else { + Integer isPassComplete = learnTaskIdentification.getIsPassComplete(); + // 根据任务id加鉴定id+用户id查询当前人的鉴定情况 + LambdaQueryWrapper identifyApplyLambdaQueryWrapper = Wrappers.lambdaQuery(); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getSourceId, taskId); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getDeleteMark, 0); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getSource, 4); + CultivateIdentifyApply identifyApply = cultivateIdentifyApplyMapper.selectOne(identifyApplyLambdaQueryWrapper); + if (identifyApply == null) return; + //鉴定结果(0-合格,1-优秀,2-不合格) + Integer result = identifyApply.getResult(); + // 颁发规则鉴定(0 合格, 1 优秀) + if (result != null && result != 2) identificationFlag = true; + + if (isPassComplete == 0) { + identificationFlag = true; + } + } + if (examFlag && identificationFlag) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId) + .eq(FtbCultivateLearnTaskAssignment::getUserId, userId) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .set(FtbCultivateLearnTaskAssignment::getStudyStats, 2) + .set(FtbCultivateLearnTaskAssignment::getLearningEndTime, new Date()); + ftbCultivateLearnTaskAssignmentMapper.update(null, updateWrapper); + } + } + } + /** + * 获取任务列表总数 + * @param keyWords 任务名称 + * @return + */ + @Override + public Map taskListCount( String keyWords) { + String userId = UserProvider.getUser().getUserId(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + queryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List taskAssignments = ftbCultivateLearnTaskAssignmentMapper.selectList(queryWrapper); + Map maps = new HashMap<>(); + if (CollUtil.isEmpty(taskAssignments)) { + return maps; + } + + //查询已发布任务信息 + List taskIds = taskAssignments.stream().map(FtbCultivateLearnTaskAssignment::getTaskId).collect(Collectors.toList()); + QueryWrapper taskQuery = new QueryWrapper<>(); + taskQuery.lambda() + .ne(FtbCultivateLearnTask::getStatus, 0) + .like(StringUtil.isNotEmpty(keyWords), FtbCultivateLearnTask::getTaskName, keyWords) + .in(FtbCultivateLearnTask::getId, taskIds); + List taskList = ftbCultivateLearnTaskMapper.selectList(taskQuery); + + if (taskAssignments != null) { + maps.put("toBeCompleted", getTaskStatus(taskAssignments, 1, taskList)); + maps.put("done", getTaskStatus(taskAssignments, 0, taskList)); + } + return maps; + } + + /** + * 查询任务数量 + * @param taskId 任务id + * @param userId 用户ID + * @return + */ + @Override + public FtbAppTaskCountVO taskCourseNum(String taskId, String userId) { + List vo = ftbCultivateMyLearnTaskInfoMapper.queryCourseList(taskId, userId, null); + int totalCount = CollUtil.isEmpty(vo) ? 0 : vo.size(); + FtbAppTaskCountVO ftbAppTaskCountVO =new FtbAppTaskCountVO(); + ftbAppTaskCountVO.setAllCourseNum(totalCount); + return ftbAppTaskCountVO; + } + + /** + * 获取任务状态 + */ + private Integer getTaskStatus(List taskAssignments, Integer flag, List publishTaskList) { + Predicate predicate; + if (flag == 0) { + predicate = item -> item.getStudyStats() == 2; + } else { + predicate = item -> item.getStudyStats() != 2; + } + long count = taskAssignments.stream().filter(predicate) + .filter(f -> { + //未发布不计入统计 + return publishTaskList.stream().map(FtbCultivateLearnTask::getId).collect(Collectors.toList()).contains(f.getTaskId()); + }) + .count(); + return Math.toIntExact(count); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineCourseServiceImpl.java new file mode 100644 index 0000000..ad9c35c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineCourseServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateOfflineCourseMapper; +import jnpf.cultivate.service.FtbCultivateOfflineCourseService; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineCourse; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateOfflineCourseServiceImpl extends ServiceImpl implements FtbCultivateOfflineCourseService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineTrainServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineTrainServiceImpl.java new file mode 100644 index 0000000..869c6fc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineTrainServiceImpl.java @@ -0,0 +1,517 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateOfflineTrainMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.service.FtbCultivateOfflineCourseService; +import jnpf.cultivate.service.FtbCultivateOfflineTrainService; +import jnpf.cultivate.service.FtbCultivateOfflineUserService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.offline.*; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.CourseEventDTO; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineCourse; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineTrain; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineUser; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.vo.offline.*; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class FtbCultivateOfflineTrainServiceImpl extends ServiceImpl implements FtbCultivateOfflineTrainService { + + @Resource + private FtbCultivateOfflineUserService ftbCultivateOfflineUserService; + @Resource + private FtbCultivateOfflineCourseService ftbCultivateOfflineCourseService; + @Resource + private FtbCultivateFileService ftbCultivateFileService; + @Autowired + private UserApi userApi; + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(FtbCultivateOfflineTrainDTO ftbCultivateCourseDTO) { + FtbCultivateOfflineTrain ftbCultivateOfflineTrain = ftbCultivateCourseDTO.convertFtbCultivateOfflineTrain(ftbCultivateCourseDTO); + baseMapper.insert(ftbCultivateOfflineTrain); + // 参与培训人员 + List ftbCultivateOfflineUsers = ftbCultivateCourseDTO.getUserIds() + .stream() + .map(FtbCultivateOfflineTrainDTO::convertFtbCultivateOfflineUser) + .peek(ftbCultivateOfflineUser -> { + ftbCultivateOfflineUser.setOffilneId(ftbCultivateOfflineTrain.getId()); + }).collect(Collectors.toList()); + ftbCultivateOfflineUserService.saveBatch(ftbCultivateOfflineUsers); + if (CollUtil.isNotEmpty(ftbCultivateCourseDTO.getCourseIds())) { + // 培训课程 + List ftbCultivateOfflineCourses = ftbCultivateCourseDTO.getCourseIds() + .stream() + .map(FtbCultivateOfflineTrainDTO::convertFtbCultivateOfflineCourse) + .peek(ftbCultivateOfflineCourse -> { + ftbCultivateOfflineCourse.setOffilneId(ftbCultivateOfflineTrain.getId()); + }).collect(Collectors.toList()); + ftbCultivateOfflineCourseService.saveBatch(ftbCultivateOfflineCourses); + // 课程进度,参与培训所有人员需要学习的课程,线下培训课程需要和岗位学习课程同步 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + CourseEventDTO.builder() + .courseIds(ftbCultivateCourseDTO.getCourseIds()) + .userIds(ftbCultivateCourseDTO.getUserIds()).whetherToChange(false).build())); + } + // 附件文件 + if (CollUtil.isNotEmpty(ftbCultivateCourseDTO.getFiles())) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(ftbCultivateCourseDTO.getFiles()) + .businessTypeID(ftbCultivateOfflineTrain.getId()) + .type(FileEventDTO.FileType.OFFLINE_TRAINING) + .build())); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateTrainingResults(UserInfo userInfo, FtbCultivateOfflineTrainUpdateDTO ftbCultivateCourseDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateCourseDTO.getId()); + updateWrapper.set(FtbCultivateOfflineTrain::getFeedHeadUserId, userInfo.getUserId()); + updateWrapper.set(FtbCultivateOfflineTrain::getHeadContent, ftbCultivateCourseDTO.getHeadContent()); + updateWrapper.set(FtbCultivateOfflineTrain::getFeedBackTime, new Date()); + baseMapper.update(new FtbCultivateOfflineTrain(), updateWrapper); + } + + @Override + public FtbCultivateOfflineTrainDetailsVO courseDetails(String id) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + FtbCultivateOfflineTrain ftbCultivateOfflineTrain = baseMapper.selectOne(queryWrapper); + FtbCultivateOfflineTrainDetailsVO ftbCultivateOfflineTrainDetailsVO = FtbCultivateOfflineTrainDetailsVO + .convertFtbCultivateOfflineTrainDetailsVO(ftbCultivateOfflineTrain); + // 培训参与人员 + LambdaQueryWrapper offlineUserQueryWrapper = Wrappers.lambdaQuery(); + offlineUserQueryWrapper.eq(FtbCultivateOfflineUser::getOffilneId, id); + List list = ftbCultivateOfflineUserService.list(offlineUserQueryWrapper); + List collect = list.stream().map(FtbCultivateOfflineUser::getUserId).collect(Collectors.toList()); + ftbCultivateOfflineTrainDetailsVO.setParticipantsInTheTraining(collect); + // 参与培训人员姓名集合 +// List userApiInfoByIds = userApi.getInfoByIds(collect); + List userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatchReturnList(collect, null); + List userNames = userPrimaryBoundBatch.stream().map(UserBoundVO::getName).collect(Collectors.toList()); + ftbCultivateOfflineTrainDetailsVO.setParticipantsInTheTrainingName(userNames); + // 课程信息 + ftbCultivateOfflineTrainDetailsVO.setCourseInfo(baseMapper.listOfflineWebCourseVO(id)); + // 上传附件信息 + LambdaQueryWrapper fileQueryWrapper = Wrappers.lambdaQuery(); + fileQueryWrapper.eq(FtbCultivateFile::getBusinessId, ftbCultivateOfflineTrain.getId()); + fileQueryWrapper.eq(FtbCultivateFile::getType, 0); + List ftbCultivateFiles = ftbCultivateFileService.list(fileQueryWrapper); + List ftbCultivateOfflineFileVOS = ftbCultivateFiles.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + ftbCultivateOfflineTrainDetailsVO.setFiles(ftbCultivateOfflineFileVOS); + + LambdaQueryWrapper fileFeedbackWrapper = Wrappers.lambdaQuery(); + fileFeedbackWrapper.eq(FtbCultivateFile::getBusinessId, ftbCultivateOfflineTrain.getId()); + fileFeedbackWrapper.eq(FtbCultivateFile::getType,FileEventDTO.FileType.OFFLINE_TRAINING_FEEDBACK.getType()); + fileFeedbackWrapper.eq(FtbCultivateFile::getEnabledMark, 0); + List feedBackFiles = ftbCultivateFileService.list(fileFeedbackWrapper); + if(CollUtil.isNotEmpty(feedBackFiles)) { + List feedBackFilesVo = feedBackFiles.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + ftbCultivateOfflineTrainDetailsVO.setFeedBackFiles(feedBackFilesVo); + } + // 反馈负责人姓名 + List userIds = new ArrayList<>(); + if(StringUtils.isNotEmpty(ftbCultivateOfflineTrainDetailsVO.getFeedHeadUserId())){ + userIds.add(ftbCultivateOfflineTrainDetailsVO.getFeedHeadUserId()); + } + if(StringUtils.isNotEmpty(ftbCultivateOfflineTrainDetailsVO.getUserId())){ + userIds.add(ftbCultivateOfflineTrainDetailsVO.getUserId()); + } + if(StringUtils.isNotEmpty(ftbCultivateOfflineTrainDetailsVO.getHeadUserId())){ + userIds.add(ftbCultivateOfflineTrainDetailsVO.getHeadUserId()); + } + if (CollUtil.isNotEmpty(userIds)) { + Map userNameAndCopyForUserIds = userApiV2Util.getUserNameAndCopyForUserIds(userIds); + UserEntity feedUser = userNameAndCopyForUserIds.get(ftbCultivateOfflineTrainDetailsVO.getFeedHeadUserId()); + if (feedUser != null) { + ftbCultivateOfflineTrainDetailsVO.setFeedHeadUserName(feedUser.getRealName()); + } + + UserEntity userEntity = userNameAndCopyForUserIds.get(ftbCultivateOfflineTrainDetailsVO.getUserId()); + if (userEntity != null) { + ftbCultivateOfflineTrainDetailsVO.setUserName(userEntity.getRealName()); + } + + UserEntity headUser = userNameAndCopyForUserIds.get(ftbCultivateOfflineTrainDetailsVO.getHeadUserId()); + if (headUser != null) { + ftbCultivateOfflineTrainDetailsVO.setHeadUserName(headUser.getRealName()); + } + } + return ftbCultivateOfflineTrainDetailsVO; + } + + @Override + public Page listPage(Page page, String keywords) { + Page result = baseMapper.listPage(page, keywords); + List userIds = result.getRecords().stream() + .map(FtbCultivateOfflineTrainPageVO::getUserId).collect(Collectors.toList()); + List headUserIds = result.getRecords().stream() + .map(FtbCultivateOfflineTrainPageVO::getHeadUserId).collect(Collectors.toList()); + userIds.addAll(headUserIds); +// List infoByIds = userApi.getInfoByIds(userIds); + Map userInfoMap = userApiV2Util.getUserNameAndCopyForUserIds(userIds); + result.getRecords().forEach(ftbCultivateOfflineTrainPageVO -> { + // 申请人姓名 + UserEntity userEntity = userInfoMap.get(ftbCultivateOfflineTrainPageVO.getUserId()); + if(userEntity!=null) { + ftbCultivateOfflineTrainPageVO.setUserName(userEntity.getRealName()); + } + // 负责人姓名 + UserEntity userEntity1 = userInfoMap.get(ftbCultivateOfflineTrainPageVO.getHeadUserId()); + if(userEntity1!=null) { + ftbCultivateOfflineTrainPageVO.setHeadUserName(userEntity1.getRealName()); + } + // 参与培训人员数量 + LambdaQueryWrapper offlineUserQueryWrapper = Wrappers.lambdaQuery(); + offlineUserQueryWrapper.eq(FtbCultivateOfflineUser::getOffilneId, ftbCultivateOfflineTrainPageVO.getId()); + ftbCultivateOfflineTrainPageVO.setParticipateTrainingMembers(Math.toIntExact( + ftbCultivateOfflineUserService.count(offlineUserQueryWrapper))); + // 培训课程数量 + LambdaQueryWrapper offlineCourseQueryWrapper = Wrappers.lambdaQuery(); + offlineCourseQueryWrapper.eq(FtbCultivateOfflineCourse::getOffilneId, ftbCultivateOfflineTrainPageVO.getId()); + ftbCultivateOfflineTrainPageVO.setTrainingCourses(Math.toIntExact( + ftbCultivateOfflineCourseService.count(offlineCourseQueryWrapper))); + // 培训状态 + Integer state = doTrainingStatus(ftbCultivateOfflineTrainPageVO.getState(), + ftbCultivateOfflineTrainPageVO.getTrainStartTime(), + ftbCultivateOfflineTrainPageVO.getTrainEndTime()); + ftbCultivateOfflineTrainPageVO.setState(state); + }); + return result; + } + + @Override + public FtbCultivateOfflineTrainDetailsAppVO offlineAppDetails(String id) { + FtbCultivateOfflineTrainDetailsVO offlineTrainWeb = courseDetails(id); + // 课程信息 + String loginUserId = UserProvider.getLoginUserId(); + List offlineCourseVOS = baseMapper.listOfflineCourseVO(id, loginUserId); + fillStudyState(offlineCourseVOS,loginUserId); + offlineTrainWeb.setCourseInfo(offlineCourseVOS); + FtbCultivateOfflineTrainDetailsAppVO result = FtbCultivateOfflineTrainDetailsAppVO.convertFtbCultivateOfflineTrainDetailsVO(offlineTrainWeb); + // 参与培训人员姓名头像信息 + LambdaQueryWrapper offlineUserQueryWrapper = Wrappers.lambdaQuery(); + offlineUserQueryWrapper.eq(FtbCultivateOfflineUser::getOffilneId, id); + Map cultivateOfflineUsers = ftbCultivateOfflineUserService.list(offlineUserQueryWrapper) + .stream() + .collect(Collectors.toMap(FtbCultivateOfflineUser::getUserId, a -> a, (k1, k2) -> k1)); + List userInfoVOS = userApiV2Util.getUserPrimaryBoundBatchReturnList(result.getParticipantsInTheTraining(), null) + .stream() + .map(UserInfoVO::convertUserInfoVO) + .peek(a -> { + FtbCultivateOfflineUser ftbCultivateOfflineUser = cultivateOfflineUsers.get(a.getUserId()); + if (ftbCultivateOfflineUser != null) { + a.setCheckInOrNot(ftbCultivateOfflineUser.getSignWhether()); + } + }) + .collect(Collectors.toList()); + result.setUserInfos(userInfoVOS); + // 反馈负责人姓名 + // 反馈负责人姓名 + List userIds = new ArrayList<>(); + if(StringUtils.isNotEmpty(result.getFeedHeadUserId())){ + userIds.add(result.getFeedHeadUserId()); + } + if(StringUtils.isNotEmpty(result.getUserId())){ + userIds.add(result.getUserId()); + } + if(StringUtils.isNotEmpty(result.getHeadUserId())){ + userIds.add(result.getHeadUserId()); + } + if (CollUtil.isNotEmpty(userIds)) { + Map userNameAndCopyForUserIds = userApiV2Util.getUserNameAndCopyForUserIds(userIds); + UserEntity feedUser = userNameAndCopyForUserIds.get(result.getFeedHeadUserId()); + if (feedUser != null) { + result.setFeedHeadUserName(feedUser.getRealName()); + } + + UserEntity userEntity = userNameAndCopyForUserIds.get(result.getUserId()); + if (userEntity != null) { + result.setUserName(userEntity.getRealName()); + } + + UserEntity headUser = userNameAndCopyForUserIds.get(result.getHeadUserId()); + if (headUser != null) { + result.setHeadUserName(headUser.getRealName()); + } + } + // 已学习课程数 + long count = result.getCourseInfo().stream() + .filter(offlineCourseVO -> offlineCourseVO.getState() == 1).count(); + result.setNumberOfCoursesTaken(Math.toIntExact(count)); + // 总学习课程数 + result.setTotalNumberOfStudyCourses(result.getCourseInfo().size()); + // 培训状态 + // 当前时间小于开始时间,未开始 + Integer state = doTrainingStatus(result.getState(), result.getTrainStartTime(), result.getTrainEndTime()); + result.setState(state); + List ftbCultivateFiles = queryFilesLists(List.of(offlineTrainWeb.getId()), FileEventDTO.FileType.OFFLINE_TRAINING_FEEDBACK.getType()); + if(CollUtil.isNotEmpty(ftbCultivateFiles)) { + result.setFeedBackFiles(ftbCultivateFiles.stream().map(ftbCultivateFile -> FtbCultivateOfflineFileVO.convertFtbCultivateOfflineFileVO(ftbCultivateFile)).collect(Collectors.toList())); + } + return result; + } + + private void fillStudyState(List offlineCourseVOS, String userId) { + List courseIds = offlineCourseVOS.stream().map(OfflineCourseVO::getCourseId).collect(Collectors.toList()); + if (CollUtil.isEmpty(courseIds)) { + return; + } + + // 构建查询条件,查询用户在指定课程中的学习情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePositionCourceLearning::getId, FtbCultivatePositionCourceLearning::getCourceId, FtbCultivatePositionCourceLearning::getState, + FtbCultivatePositionCourceLearning::getLearnTime); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + queryWrapper.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); // 有效标志为0表示有效 + + List courseLearnings = ftbCultivatePositionCourceLearningMapper.selectList(queryWrapper); + + // 构建key为userId-courseId的映射 + Map resultMap = new HashMap<>(); + for (FtbCultivatePositionCourceLearning learning : courseLearnings) { + resultMap.put(learning.getCourceId(), learning); + } + for (OfflineCourseVO offlineCourseVO : offlineCourseVOS) { + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = resultMap.get(offlineCourseVO.getCourseId()); + if (ftbCultivatePositionCourceLearning == null) { + offlineCourseVO.setState(0); + continue; + } + offlineCourseVO.setState(ftbCultivatePositionCourceLearning.getState()); + } + } + + public List queryFilesLists(List businessIds, Integer type) { + LambdaQueryWrapper wrapperItemFile = Wrappers.lambdaQuery(); + wrapperItemFile.in(FtbCultivateFile::getBusinessId, businessIds); + wrapperItemFile.eq(FtbCultivateFile::getType, type); + wrapperItemFile.eq(FtbCultivateFile::getEnabledMark,0); + wrapperItemFile.orderByAsc(FtbCultivateFile::getCreatorTime); + List list = ftbCultivateFileService.list(wrapperItemFile); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + @Override + public Page offlineTrainingApp(Page page, Integer type) { + String userId = UserProvider.getUser().getUserId(); + // 查询类型,null为全部培训,1为我发起的,2为我参与的,3为我负责的 + Page result = baseMapper.listOfflineTrainingApp(page, type, userId); + result.getRecords().forEach(ftbOfflineTrainingAppPageVO -> { + // 负责人姓名 + UserEntity userEntity = userApi.getInfoById(ftbOfflineTrainingAppPageVO.getHeadUserId()); + if (Objects.nonNull(userEntity)) { + ftbOfflineTrainingAppPageVO.setHeadUserName(userEntity.getRealName()); + } + // 参与人员 + LambdaQueryWrapper offlineUserQueryWrapper = Wrappers.lambdaQuery(); + offlineUserQueryWrapper.eq(FtbCultivateOfflineUser::getOffilneId, ftbOfflineTrainingAppPageVO.getId()); + List ftbCultivateOfflineUsers = ftbCultivateOfflineUserService.list(offlineUserQueryWrapper); + List userIds = ftbCultivateOfflineUsers.stream() + .map(FtbCultivateOfflineUser::getUserId).collect(Collectors.toList()); +// List infoByIds = userApi.getInfoByIds(userIds); + List userPrimaryBoundBatchReturnList = userApiV2Util.getUserPrimaryBoundBatchReturnList(userIds, null); + List userNames = userPrimaryBoundBatchReturnList.stream().map(UserBoundVO::getName).collect(Collectors.toList()); + ftbOfflineTrainingAppPageVO.setUserInfos(userNames); + // 培训状态 + Integer state = doTrainingStatus(ftbOfflineTrainingAppPageVO.getState(), + ftbOfflineTrainingAppPageVO.getTrainStartTime(), + ftbOfflineTrainingAppPageVO.getTrainEndTime()); + ftbOfflineTrainingAppPageVO.setState(state); + // 只有【进行中】,且参与人员包含登陆人,则显示【签到】按钮 + ftbOfflineTrainingAppPageVO.setShowSignInButton(1); + if (state != null && state == 2) { + ftbOfflineTrainingAppPageVO.setShowSignInButton(userIds.contains(userId) ? 0 : 1); + } + }); + return result; + } + + @Override + @Transactional + public void deleteOfflineTrain(String id) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateOfflineTrain::getId, id); + updateWrapper.set(FtbCultivateOfflineTrain::getEnableMark, 1); + update(new FtbCultivateOfflineTrain(), updateWrapper); + } + + @Override + public List queryExistingMembers(String id) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateOfflineUser::getUserId); + queryWrapper.eq(FtbCultivateOfflineUser::getOffilneId, id); + return ftbCultivateOfflineUserService.list(queryWrapper) + .stream() + .map(FtbCultivateOfflineUser::getUserId) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void modifyOfflineTrainingMembers(FtbModifyOfflineTrainingMembersDTO data) { + LambdaQueryWrapper removeWrapper = Wrappers.lambdaQuery(); + removeWrapper.eq(FtbCultivateOfflineUser::getOffilneId, data.getOfflineTrainId()); + // 原有用户 + List oldOfflineUsers = ftbCultivateOfflineUserService.list(removeWrapper); + removeWrapper.notIn(FtbCultivateOfflineUser::getUserId, data.getUserIds()); + List oldUserIds = oldOfflineUsers.stream().map(FtbCultivateOfflineUser::getUserId).collect(Collectors.toList()); + List ftbCultivateOfflineUsers = data.getUserIds() + .stream() + .filter(a -> !oldUserIds.contains(a)) + .map(a -> { + FtbCultivateOfflineUser ftbCultivateOfflineUser = new FtbCultivateOfflineUser(); + ftbCultivateOfflineUser.setUserId(a); + ftbCultivateOfflineUser.setOffilneId(data.getOfflineTrainId()); + ftbCultivateOfflineUser.setSignWhether(0); + return ftbCultivateOfflineUser; + }).collect(Collectors.toList()); + // 删除不存在的用户 + ftbCultivateOfflineUserService.remove(removeWrapper); + ftbCultivateOfflineUserService.saveBatch(ftbCultivateOfflineUsers); + // 维护参与人员冗余字段 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateOfflineTrain::getId, data.getOfflineTrainId()); + updateWrapper.set(FtbCultivateOfflineTrain::getParticipants, String.join(",", data.getUserIds())); + update(new FtbCultivateOfflineTrain(), updateWrapper); + // 课程绑定 + List courseIds = this.getBaseMapper().queryCourseIdsByOfflineId(data.getOfflineTrainId()); + // 课程进度,参与培训所有人员需要学习的课程,线下培训课程需要和岗位学习课程同步 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + CourseEventDTO.builder() + .courseIds(courseIds) + .userIds(data.getUserIds()) + .whetherToChange(false) + .build())); + } + + @Override + @Transactional + public void withdraw(String id) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbCultivateOfflineTrain::getState, 3); + update(new FtbCultivateOfflineTrain(), updateWrapper); + } + + @Override + public Page numberOfflineTraining(Page page, FtbCultivateOfflineTrainPeopleSigningInDTO data) { + return this.getBaseMapper().numberOfflineTraining(page, data); + } + + @Override + @Transactional + public void editOfflineTraining(FtbCultivateOfflineTrainChangeDTO ftbCultivateOfflineTrainDTO) { + LambdaUpdateWrapper updateWrapper = FtbCultivateOfflineTrainChangeDTO.convert(ftbCultivateOfflineTrainDTO); + update(new FtbCultivateOfflineTrain(), updateWrapper); + // 线下培训课程 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateOfflineCourse::getOffilneId, ftbCultivateOfflineTrainDTO.getId()); + ftbCultivateOfflineCourseService.remove(queryWrapper); + List ftbCultivateOfflineCourses = ftbCultivateOfflineTrainDTO.getCourseIds() + .stream() + .map(a -> { + FtbCultivateOfflineCourse ftbCultivateOfflineCourse = new FtbCultivateOfflineCourse(); + ftbCultivateOfflineCourse.setCourseId(a); + ftbCultivateOfflineCourse.setOffilneId(ftbCultivateOfflineTrainDTO.getId()); + return ftbCultivateOfflineCourse; + }).collect(Collectors.toList()); + ftbCultivateOfflineCourseService.saveBatch(ftbCultivateOfflineCourses); + // 附件 + this.getBaseMapper().deleteOfflineTrainFileById(ftbCultivateOfflineTrainDTO.getId()); + if (CollUtil.isNotEmpty(ftbCultivateOfflineTrainDTO.getFiles())) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(ftbCultivateOfflineTrainDTO.getFiles()) + .businessTypeID(ftbCultivateOfflineTrainDTO.getId()) + .type(FileEventDTO.FileType.OFFLINE_TRAINING) + .build())); + } + // 线下培训参与人员 + FtbModifyOfflineTrainingMembersDTO modifyOfflineTrainingMembersDTO = new FtbModifyOfflineTrainingMembersDTO(); + modifyOfflineTrainingMembersDTO.setOfflineTrainId(ftbCultivateOfflineTrainDTO.getId()); + modifyOfflineTrainingMembersDTO.setUserIds(ftbCultivateOfflineTrainDTO.getUserIds()); + modifyOfflineTrainingMembers(modifyOfflineTrainingMembersDTO); + } + + @Override + @Transactional + public void signIn(FtbCultivateOfflineTrainSignInDTO freightTrainSignInDTO) { + if (freightTrainSignInDTO.getSignWay() == 1 && StrUtil.isBlank(freightTrainSignInDTO.getSignPhoto())) { + throw new RuntimeException("请上传拍照签到的照片"); + } + LambdaUpdateWrapper offlineUserLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + offlineUserLambdaUpdateWrapper.eq(FtbCultivateOfflineUser::getOffilneId, freightTrainSignInDTO.getOfflineTrainId()); + offlineUserLambdaUpdateWrapper.eq(FtbCultivateOfflineUser::getUserId, UserProvider.getLoginUserId()); + offlineUserLambdaUpdateWrapper.set(FtbCultivateOfflineUser::getSignWhether, 1); + offlineUserLambdaUpdateWrapper.set(FtbCultivateOfflineUser::getSignTime, new Date()); + offlineUserLambdaUpdateWrapper.set(FtbCultivateOfflineUser::getSignPhoto, freightTrainSignInDTO.getSignPhoto()); + ftbCultivateOfflineUserService.update(new FtbCultivateOfflineUser(), offlineUserLambdaUpdateWrapper); + } + + + /** + * 培训状态 + * + * @param state 培训状态,0未开始,1已结束,2进行中,3已撤回 + * @param trainStartTime 培训开始时间 + * @param trainEndTime 培训结束时间 + * @return {@link Integer} + */ + private Integer doTrainingStatus(Integer state, Date trainStartTime, Date trainEndTime) { + if (state == 3) { + return state; + } + Date now = new Date(); + if (DateUtil.compare(now, trainStartTime) < 0) { + return 0; + } else if (DateUtil.compare(now, trainStartTime) >= 0 && + DateUtil.compare(now, trainEndTime) <= 0) { + // 当前时间大于开始时间,小于结束时间,进行中 + return 2; + } else if (DateUtil.compare(now, trainEndTime) > 0) { + // 当前时间大于结束时间,已结束 + return 1; + } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineUserServiceImpl.java new file mode 100644 index 0000000..34f23db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateOfflineUserServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateOfflineUserMapper; +import jnpf.cultivate.service.FtbCultivateOfflineUserService; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineUser; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateOfflineUserServiceImpl extends ServiceImpl implements FtbCultivateOfflineUserService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePackageCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePackageCourseServiceImpl.java new file mode 100644 index 0000000..865518f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePackageCourseServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePackageCourseMapper; +import jnpf.model.cultivate.po.coursepackage.FtbCultivatePackageCourse; +import jnpf.cultivate.service.FtbCultivatePackageCourseService; + +@Service +public class FtbCultivatePackageCourseServiceImpl extends ServiceImpl implements FtbCultivatePackageCourseService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionAssessmentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionAssessmentServiceImpl.java new file mode 100644 index 0000000..7434c9b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionAssessmentServiceImpl.java @@ -0,0 +1,445 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.entity.ModuleEntity; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.CultivateIdentifyTableMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionStatisticesMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionNewMapper; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivatePositionAssessmentService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionAssessmentOrgStatisticVO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionOrgStatisticesVO; +import jnpf.model.cultivate.vo.position.PersonStatisticesDto; +import jnpf.model.cultivate.vo.position.web.*; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.position.PositionBoundOrganizeVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.permission.vo.v2.user.UserRelationBaseVO; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @Author: peng.hao + * @create: 2024/7/22:16:01 + */ +@Service +public class FtbCultivatePositionAssessmentServiceImpl implements FtbCultivatePositionAssessmentService { + + @Autowired + CultivatePerUtils cultivatePerUtils; + + @Autowired + FtbCultivatePositionStatisticesMapper ftbCultivatePositionStatisticesMapper; + + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Resource + CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Autowired + FtbCultivatePromotionNewMapper cultivatePromotionNewMapper; + + @Autowired + UserApiV2Util userApiV2Util; + + + @Autowired + private UserProvider userProvider; + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbCultivatePositionOrgStatisticesDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO organizationListStatistics(FtbCultivatePositionOrgStatisticesDTO statisticDTO) { + Page page = statisticDTO.coverCultivatePage(); + // 多选组织情况 根据每一个组织维度统计 + List orgIds = new ArrayList<>(); + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)){ + return returnEmptyList(statisticDTO); + } + // 勾选 + + if ("1".equals(statisticDTO.getSelectLogo()) && CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List allOrg = new ArrayList<>(statisticDTO.getOrgId()); + List allOrganize = cultivatePerUtils.getAllOrganizationInformation(statisticDTO.getOrgId()); + if (CollUtil.isNotEmpty(allOrganize)) { + allOrg.addAll(allOrganize); + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, allOrg); + if(CollUtil.isEmpty( intersection)){ + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + }else if(CollUtil.isNotEmpty(statisticDTO.getOrgId())){ + List intersection = UserApiV2Util.getIntersection(powerOrgList, statisticDTO.getOrgId()); + if(CollUtil.isEmpty( intersection)){ + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + }else{ + orgIds = powerOrgList; + } + + // 默认情况 + String orgIdBySpecial = statisticDTO.getOrgIdBySpecial(); + if (StringUtils.isNotEmpty(orgIdBySpecial) && !orgIds.contains(orgIdBySpecial)) { + orgIds.add(orgIdBySpecial); + } + // 手动输入 + String dtoOrgIds = statisticDTO.getOrgIds(); + if (StringUtils.isNotEmpty(dtoOrgIds)) { + List positionBaseInfoVOS = userApiV2Util.listPositionBaseInfoByEncodes(Arrays.asList(dtoOrgIds.split(",")), null); + if(CollUtil.isEmpty(positionBaseInfoVOS)){ + return returnEmptyList(statisticDTO); + } + List intersection = UserApiV2Util.getIntersection(orgIds, positionBaseInfoVOS.stream().map(PositionBaseInfoVO::getId).collect(Collectors.toList())); + if(CollUtil.isEmpty( intersection)){ + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + } + // 选择岗位 反向查询那些组织下有这个岗位 + String postId = statisticDTO.getPostId(); + List chooseATypes = new ArrayList<>(); + if (StringUtils.isNotEmpty(postId)) { + chooseATypes = List.of(postId.split(",")); + List organizeGeneralDetailVOS = userApiV2Util.organizeInfoListByPositionIds(chooseATypes, null); + if (CollUtil.isNotEmpty(organizeGeneralDetailVOS)) { + List dimensionByIds = organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + List intersection = UserApiV2Util.getIntersection(orgIds, dimensionByIds); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + } + orgIds = UserApiV2Util.uniqueStringList(orgIds); + if (orgIds.isEmpty()) { + return returnEmptyList(statisticDTO); + } + List records = new ArrayList<>(); + List organizeEntityList = userApiV2Util.organizesByOrganizeIds(orgIds, null); + Map orgMap = organizeEntityList.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, v -> v)); + // 根据组织查询岗位 +// Map> batchListByOrganizeIds = positionApi.batchListByOrganizeIds(orgIds); +// Map> batchListByOrganizeIds = userApiV2Util.listPositionBaseInfoByIdsReturnMap(orgIds, null); + List positionBoundOrganizeVOS = userApiV2Util.batchQueryPositionForOrgIdsReturnList(orgIds, null); + Map> batchListByOrganizeIds= new HashMap<>();//组织id 到岗位信息 + if(CollUtil.isNotEmpty(positionBoundOrganizeVOS)){ + batchListByOrganizeIds = positionBoundOrganizeVOS.stream().collect(Collectors.groupingBy(PositionBoundOrganizeVO::getOrganizeId)); + } + + List allPostId = new ArrayList<>(); + for (PositionBoundOrganizeVO positionBoundOrganizeVO : positionBoundOrganizeVOS) { + allPostId.add(positionBoundOrganizeVO.getId()); + } + List dbExiestPostId = new ArrayList<>(); + if(CollUtil.isNotEmpty(allPostId)) { + dbExiestPostId = ftbCultivatePositionStatisticesMapper.getPostIds(allPostId); + } + if(CollUtil.isNotEmpty(chooseATypes)){ + allPostId.addAll(chooseATypes); + } + String tenantId = UserProvider.getUser().getTenantId(); + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(allPostId, tenantId); + + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(orgIds, tenantId); + Map> userMap = new HashMap<>();//组织id_岗位id ->用户 + if(CollUtil.isNotEmpty(userListForOrgIds)){ + for (UserPageListVO userListForOrgId : userListForOrgIds) { + String k = userListForOrgId.getOrganizeId() +"_" + userListForOrgId.getPositionId(); + List list = userMap.get(k); + if (list == null) { + list = new ArrayList<>(); + } + list.add(userListForOrgId); + userMap.put(k, list); + } + } + + // 岗位学习岗位 + for (String orgId : orgIds) { + List orgIdsCollect = new ArrayList<>(); + if (CollUtil.isNotEmpty(batchListByOrganizeIds.get(orgId))) { + for (PositionBaseInfoVO positionBaseInfoVO : batchListByOrganizeIds.get(orgId)) { + orgIdsCollect.add(positionBaseInfoVO.getId()); + } + } + if (CollUtil.isEmpty(orgIdsCollect)) continue; +// if (CollUtil.isNotEmpty(chooseATypes)) { +// postIds = chooseATypes; +// } else { +// postIds = ftbCultivatePositionStatisticesMapper.getPostIds(orgIdsCollect); +// } + List postIds = UserApiV2Util.getIntersection(dbExiestPostId, orgIdsCollect); + if(CollUtil.isNotEmpty(chooseATypes)){ + postIds = UserApiV2Util.getIntersection(postIds, chooseATypes); + } + if (CollUtil.isEmpty(postIds)) continue; + postIds.forEach(dbPostId -> { + List userBoundVOS = userMap.get(orgId+"_"+dbPostId);//userApiV2Util.getUserListForOrgIdAndPositionId(orgId,dbPostId, tenantId); + + FtbCultivatePositionAssessmentOrgStatisticVO orgStatisticVO = new FtbCultivatePositionAssessmentOrgStatisticVO(); + // 组织名称和组织id + OrganizeGeneralDetailVO organizeGeneralDetailVO = orgMap.get(orgId); + if(organizeGeneralDetailVO!=null) { + orgStatisticVO.setOrganizationCustomId(organizeGeneralDetailVO.getEnCode()); + orgStatisticVO.setOrganization(organizeGeneralDetailVO.getName()); + } + orgStatisticVO.setOrgId(orgId); + + List userIdsByGradesId = new ArrayList<>(); + if (CollUtil.isNotEmpty(userBoundVOS)) { + userIdsByGradesId = userBoundVOS.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + } + // 展示id + PositionVO positionInfoDimensionVO = stringPositionVOMap.get(dbPostId); + if (positionInfoDimensionVO != null) { + orgStatisticVO.setPositionCustomId(positionInfoDimensionVO.getEnCode()); + orgStatisticVO.setPositionTitle(positionInfoDimensionVO.getFullName()); + } + orgStatisticVO.setPositionId(dbPostId); + // 获取该岗位下所有人 + // 岗位所有人员数 + orgStatisticVO.setNumberOfPositions(userIdsByGradesId.size()); + // 查询这些人是否产生岗位学习记录 + // 根据岗位查询 + if (CollUtil.isNotEmpty(userIdsByGradesId)) { + + // 考试合格率 + // 考试合格人数 + FtbCultivatePositionExamVO examAndIdentifyVO = ftbCultivateExamUserService.queryExamPassingInformation(dbPostId, userIdsByGradesId); + orgStatisticVO.setNumberExam(examAndIdentifyVO.getNumberExam()); + orgStatisticVO.setExamPassRate(examAndIdentifyVO.getExamPassRate()); + // 鉴定合格率 + // 鉴定合格人数 + FtbCultivatePositionIdentifyVO cultivatePositionIdentifyVO = cultivateIdentifyTableMapper.queryQualificationInformation(userIdsByGradesId, dbPostId); + orgStatisticVO.setNumberAppraisal(cultivatePositionIdentifyVO.getNumberAppraisal()); + orgStatisticVO.setAppraisalPassRate(cultivatePositionIdentifyVO.getAppraisalPassRate()); + } + records.add(orgStatisticVO); + }); + } + return CultivatePage.paginate(records, page); + } + + @Override + public FtbCultivatePositionAssessmentVO jobAssessmentList(String orgId, String positionId, CultivatePage page) { + FtbCultivatePositionAssessmentVO assessmentVO = new FtbCultivatePositionAssessmentVO(); + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setTenantId(userProvider.get().getTenantId()); + + dto.setPageSize(100000L); + dto.setCurrentPage(1L); + dto.setOrganizeId(orgId); + dto.setPositionId(positionId); + PageListVO userBoundVOPageListVO = userApiV2Util.pageListUserForWhere(dto); + List stringList = new ArrayList<>(); + if (userBoundVOPageListVO != null && CollectionUtil.isNotEmpty(userBoundVOPageListVO.getList())) { + stringList = userBoundVOPageListVO.getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } else { + return assessmentVO; + } + + Page cultivatePage = page.coverCultivatePage(); + // 根据组织岗位查询下面所有的人 + // 根据查询岗位查询对应的岗位考试合格分 + BigDecimal examPassingScore = ftbCultivatePositionStatisticesMapper.qualifyingPointsForJobRelatedExaminations(positionId); + // 根据查询岗位查询对应的岗位实操合格分 + BigDecimal appraisalPassingPoints = ftbCultivatePositionStatisticesMapper.qualifyingPointsForJobRelatedAppraisal(positionId); + + assessmentVO.setExamPassingScore(examPassingScore); + assessmentVO.setAppraisalPassingPoints(appraisalPassingPoints); + //根据组织和岗位id查询人 + + +// Map userNameMap = cultivatePerUtils.queryRosterNameBasedOnUserId(stringList); + Map userNameMap = userApiV2Util.getUserPrimaryBoundBatch(stringList, null); + List list = new ArrayList<>(); +// OrganizeEntity infoById = organizeApi.getInfoById(orgId); + OrganizeGeneralDetailVO infoById = userApiV2Util.organizeInfoById(orgId, null); + + PositionVO entity = userApiV2Util.infoPosition(positionId, null); + // 查询岗位下人的完课情况 + // 1 课程完成数 2为全部 + Map map1 = batchQueryCount(stringList, 1, positionId); + Map map2 = batchQueryCount(stringList, 2, positionId); + stringList.forEach(userId -> { + FtbCultivatePositionAssessmentListVO vo = new FtbCultivatePositionAssessmentListVO(); + UserBoundVO userBoundVO = userNameMap.get(userId); + if(userBoundVO!=null) { + vo.setAppraiser(userBoundVO.getUserName()); + } + if (infoById != null) { + vo.setOrgName(infoById.getName()); + } + if (ObjectUtil.isNotEmpty(entity)) { + vo.setPostName(entity.getFullName()); + } + List exmIds = cultivatePromotionNewMapper.checkIfThereAreExamsForThisPositionWithNew(positionId, userId); + // 是否有岗位学习考试 + boolean thereIsAnExam = CollUtil.isNotEmpty(exmIds); + // 查询这些人是否产生岗位学习记录 + // 考试合格率 + // 考试合格人数 + if (thereIsAnExam) { + FtbCultivatePositionExamPersonVO personVO = ftbCultivateExamUserService.queryTheListOfQualifiedPersons(List.of(userId), positionId, exmIds); + if (ObjectUtil.isNotEmpty(personVO)) { + vo.setJobExamScores(personVO.getTestScores()); + vo.setIsPassJobExamination(personVO.getIsPassTheExam()); + } + } + List applyIds = cultivatePromotionNewMapper.checkWhetherThereIsAPracticalAppraisalWithNew(positionId, userId); + boolean identified = CollUtil.isNotEmpty(applyIds); + if (identified) { + FtbCultivatePositionIdentifyPersonVO personVO = cultivateIdentifyTableMapper.currentPersonSAssessmentInformation(userId, applyIds); + if (ObjectUtil.isNotEmpty(personVO)) { + vo.setJobAppraisalScore(personVO.getIdentificationScore()); + Integer identification = personVO.getIsPassTheIdentification(); + if (identification == 0) { + vo.setIsPassJobAppraisalScore("合格"); + } else if (identification == 1) { + vo.setIsPassJobAppraisalScore("优秀"); + } else { + vo.setIsPassJobAppraisalScore("不合格"); + } + } + } + // [1:已经完成考试或者合格 2:未完成考试, 3:考试未合格] + Integer i = ftbCultivateExamUserService.queryExamIsCompleteForUserIdAndPostion(userId, positionId, 0); + // 当前考试已经完成 + boolean finishExam = false; + if (thereIsAnExam && 1 == i) { + // 当前考试已经完成 + finishExam = true; + } + Integer count = 0; + // 查询当前鉴定是否完成 + boolean finishIdentification = false; + if (!applyIds.isEmpty()) + count = cultivatePromotionNewMapper.queryTheNumberOfPracticalAppraisalStudies(List.of(positionId), userId, applyIds, 0); + if (!applyIds.isEmpty() && count > 0) { + finishIdentification = true; + } + // 岗位完课率 + // 完课率 已经学习完的课程书/(未学习的课程数+学习中的课程数+已学习的课程数) + PersonStatisticesDto count1 = map1.get(userId); + Integer numberOfCourses = 0; + // 岗位学习数 + boolean courseBloo = false; + if (null != count1) { + numberOfCourses = count1.getNum(); + courseBloo = true; + } + PersonStatisticesDto count2 = map2.get(userId); + Integer totalOfCourses = 0; + if (null != count2) { + totalOfCourses = count2.getNum(); + } + vo.setJobCompletionRate(FtbCultivatePositionStatisticesServiceImpl.computeDivision(numberOfCourses, totalOfCourses)); + //同时绑定有鉴定和考试时,要两个都合格岗位考核才算合格 + //只绑定了鉴定时,鉴定合格岗位考核算合格 + //只绑定了考试时,考试合格岗位考核算合格 + //只绑定了课程时,课程学习完岗位考核算合格 + extracted(vo, thereIsAnExam, identified, courseBloo, finishExam, finishIdentification); + list.add(vo); + }); + long count = list.stream().filter(item -> "合格".equals(item.getIsPassJobAssessment())).count(); + assessmentVO.setQualifiedNumberOfPeople(count); + assessmentVO.setList(CultivatePage.paginate(list, cultivatePage)); + return assessmentVO; + } + + /** + * @param vo + * @param thereIsAnExam 是否有考试 + * @param identified 是否有鉴定 + * @param courseBloo 是否完成课程 + * @param finishExam 是否已经完成考试 + * @param finishIdentification 是否已经完成鉴定 + */ + private void extracted(FtbCultivatePositionAssessmentListVO vo, boolean thereIsAnExam, boolean identified, boolean courseBloo, boolean finishExam, boolean finishIdentification) { + // 有考试和鉴定 + if (thereIsAnExam && identified && finishExam && finishIdentification && StringUtils.isNotEmpty(vo.getIsPassJobExamination()) && StringUtils.isNotEmpty(vo.getIsPassJobAppraisalScore())) { + String string = (!"不合格".equals(vo.getIsPassJobExamination()) && !"不合格".equals(vo.getIsPassJobAppraisalScore())) ? "合格" : "不合格"; + vo.setIsPassJobAssessment(string); + // 有考试没有鉴定 + } else if (thereIsAnExam && finishExam && !identified && StringUtils.isNotEmpty(vo.getIsPassJobExamination())) { + // String string = "不合格".equals(vo.getIsPassJobExamination()) ? "不合格" : "合格"; + //vo.setIsPassJobAssessment(string); + // 有鉴定没有考试 + } else if (!thereIsAnExam && identified && finishIdentification && StringUtils.isNotEmpty(vo.getIsPassJobAppraisalScore())) { + // String string = "不合格".equals(vo.getIsPassJobAppraisalScore()) ? "不合格" : "合格"; + // vo.setIsPassJobAssessment(string); + //只绑定了课程时,课程学习完岗位考核算合格 + } else if (!thereIsAnExam && !identified && courseBloo) { + // vo.setIsPassJobAssessment("合格"); + } + } + + /** + * 查询岗位下课程完成情况 + * + * @param idsByGradesId + * @param type + * @param positionId + * @return + */ + private Map batchQueryCount(List idsByGradesId, int type, String positionId) { + Map ret = new HashMap<>(); + if (CollectionUtil.isEmpty(idsByGradesId)) { + return ret; + } + List list = ftbCultivatePositionStatisticesMapper.queryCourseCompletionStatusOfThePosition(positionId, idsByGradesId, type); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + ret = list.stream().collect(Collectors.toMap(PersonStatisticesDto::getUserId, Function.identity(), (k1, k2) -> k1)); + return ret; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCertificateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCertificateServiceImpl.java new file mode 100644 index 0000000..ff38179 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCertificateServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionCertificateMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCertificate; +import jnpf.cultivate.service.FtbCultivatePositionCertificateService; + +@Service +public class FtbCultivatePositionCertificateServiceImpl extends ServiceImpl implements FtbCultivatePositionCertificateService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCopyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCopyServiceImpl.java new file mode 100644 index 0000000..1999c13 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCopyServiceImpl.java @@ -0,0 +1,244 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.service.*; +import jnpf.model.cultivate.dto.position.web.FtbCultivateAssociatedExamsCoursesDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionCopyDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionPassExaminationDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.PositionCourseEventDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; +import jnpf.model.cultivate.vo.position.web.FtbCopyRelatedPositionsVO; +import jnpf.model.cultivate.vo.position.web.FtbCultivatePositionCopyVO; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FtbCultivatePositionCopyServiceImpl implements FtbCultivatePositionCopyService { + + private final FtbCultivatePositionService ftbCultivatePositionService; + private final FtbCultivatePositionCourseService ftbCultivatePositionCourseService; + private final FtbCultivatePositionExamService ftbCultivatePositionExamService; + private final FtbCultivatePositionExamIdentifyService ftbCultivatePositionExamIdentifyService; + private final FtbCultivateCourseMapper ftbCultivateCourseMapper; + private final FtbCultivateExamService ftbCultivateExamService; + + + @Override + @Transactional + @SuppressWarnings("Duplicates") + public FtbCultivatePositionCopyVO copyPosition(FtbCultivatePositionCopyDTO data) { + FtbCultivatePositionCopyVO ftbCultivatePositionCopyVO = new FtbCultivatePositionCopyVO(); + // 旧岗位考试 + LambdaQueryWrapper positionExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionExamLambdaQueryWrapper.select(FtbCultivatePositionExam::getExamId); + positionExamLambdaQueryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + positionExamLambdaQueryWrapper.eq(FtbCultivatePositionExam::getPostRankId, data.getOldPositionId()); + positionExamLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionExam ftbCultivatePositionExam = ftbCultivatePositionExamService.getOne(positionExamLambdaQueryWrapper); + FtbCultivateExam ftbCultivateExam = null; + if (Objects.nonNull(ftbCultivatePositionExam)) { + // 是否和新岗位有绑定关系 + ftbCultivateExam = ftbCultivateExamService.queryExamAndPositionRelation(ftbCultivatePositionExam.getExamId() + , data.getNewPositionId()); + // 无绑定关系 + if (ftbCultivateExam == null) { + String examName = ftbCultivateCourseMapper.queryExamNameByExamId(ftbCultivatePositionExam.getExamId()); + FtbCopyRelatedPositionsVO ftbCopyRelatedPositionsVO = new FtbCopyRelatedPositionsVO(); + ftbCopyRelatedPositionsVO.setType(1); + ftbCopyRelatedPositionsVO.setName(examName); + ftbCopyRelatedPositionsVO.setFieldId(ftbCultivatePositionExam.getExamId()); + ftbCultivatePositionCopyVO.getCopyRelatedPositions().add(ftbCopyRelatedPositionsVO); + } + } + // 旧岗位课程 + LambdaQueryWrapper cultivatePositionCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + cultivatePositionCourseLambdaQueryWrapper.select(FtbCultivatePositionCourse::getCourseId); + cultivatePositionCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + cultivatePositionCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getPostRankId, data.getOldPositionId()); + List ftbCultivatePositionCourses = ftbCultivatePositionCourseService.list(cultivatePositionCourseLambdaQueryWrapper); + // 是否和旧岗位有绑定关系,返回有关联关系的课程id + List existingCoursePositions = handleExistingExistingPositions(ftbCultivatePositionCopyVO, ftbCultivatePositionCourses, data.getNewPositionId()); + // 旧岗位实操鉴定 + LambdaQueryWrapper positionExamIdentifyQueryWrapper = Wrappers.lambdaQuery(); + positionExamIdentifyQueryWrapper.select(FtbCultivatePositionExamIdentify::getIdentifyId); + positionExamIdentifyQueryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + positionExamIdentifyQueryWrapper.eq(FtbCultivatePositionExamIdentify::getPostRankId, data.getOldPositionId()); + positionExamIdentifyQueryWrapper.last("limit 1"); + FtbCultivatePositionExamIdentify ftbCultivatePositionExamIdentify = ftbCultivatePositionExamIdentifyService.getOne(positionExamIdentifyQueryWrapper); + + // 新岗位岗位学习数据插入 + FtbCultivatePosition ftbCultivatePosition = new FtbCultivatePosition(); + ftbCultivatePosition.setIsGrounding(0); + ftbCultivatePosition.setPostId(data.getNewPositionId()); + ftbCultivatePositionService.save(ftbCultivatePosition); + String postLearnId = ftbCultivatePosition.getId(); + + // 新岗位实操鉴定插入 + if (Objects.nonNull(ftbCultivatePositionExamIdentify)) { + FtbCultivatePositionExamIdentify newFtbCultivatePositionExamIdentify = new FtbCultivatePositionExamIdentify(); + newFtbCultivatePositionExamIdentify.setIdentifyId(ftbCultivatePositionExamIdentify.getIdentifyId()); + newFtbCultivatePositionExamIdentify.setEnabledMark(0); + newFtbCultivatePositionExamIdentify.setPostLearnId(postLearnId); + newFtbCultivatePositionExamIdentify.setPostRankId(data.getNewPositionId()); + ftbCultivatePositionExamIdentifyService.save(newFtbCultivatePositionExamIdentify); + } + + // 新岗位考试插入 + if (Objects.nonNull(ftbCultivatePositionExam) && Objects.nonNull(ftbCultivateExam)) { + FtbCultivatePositionExam newFtbCultivatePositionExam = new FtbCultivatePositionExam(); + newFtbCultivatePositionExam.setEnabledMark(0); + newFtbCultivatePositionExam.setExamId(ftbCultivatePositionExam.getExamId()); + newFtbCultivatePositionExam.setPostLearnId(postLearnId); + newFtbCultivatePositionExam.setPostRankId(data.getNewPositionId()); + ftbCultivatePositionExamService.save(newFtbCultivatePositionExam); + } + + // 岗位学习课程 + List newCultivatePositionCourses = existingCoursePositions.stream().map(a -> { + FtbCultivatePositionCourse forgottenCoursePositionsCourse = new FtbCultivatePositionCourse(); + forgottenCoursePositionsCourse.setCourseId(a); + forgottenCoursePositionsCourse.setEnabledMark(0); + forgottenCoursePositionsCourse.setPostLearnId(postLearnId); + forgottenCoursePositionsCourse.setPostRankId(data.getNewPositionId()); + return forgottenCoursePositionsCourse; + }).collect(Collectors.toList()); + ftbCultivatePositionCourseService.saveBatch(newCultivatePositionCourses); + // 岗位学习课程进度 + courseLearningProgressBinding(data.getNewPositionId(), existingCoursePositions); + // 返回关联岗位数据 + ftbCultivatePositionCopyVO.getCopyRelatedPositions().forEach(a -> a.setPostLearnId(postLearnId)); + ftbCultivatePositionCopyVO.setPostLearnId(postLearnId); + return ftbCultivatePositionCopyVO; + } + + @Override + @Transactional + public void passingTheJobExaminationRevised(FtbCultivatePositionPassExaminationDTO data) { + LambdaUpdateWrapper updateQueryWrapper = Wrappers.lambdaUpdate(); + updateQueryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, data.getPostLearnId()); + updateQueryWrapper.set(FtbCultivatePosition::getQualified, data.getQualified()); + ftbCultivatePositionService.update(new FtbCultivatePosition(), updateQueryWrapper); + } + + @Override + @Transactional + public void associatedExamsAndCourses(FtbCultivateAssociatedExamsCoursesDTO data) { + // 类型,0课程,1考试 + if (data.getType() == 0) { + // 岗位学习课程 + FtbCultivatePositionCourse forgottenCoursePositionsCourse = new FtbCultivatePositionCourse(); + forgottenCoursePositionsCourse.setCourseId(data.getFieldId()); + forgottenCoursePositionsCourse.setEnabledMark(0); + forgottenCoursePositionsCourse.setPostLearnId(data.getPostLearnId()); + forgottenCoursePositionsCourse.setPostRankId(data.getNewPostionId()); + ftbCultivatePositionCourseService.save(forgottenCoursePositionsCourse); + // 课程修改增加学习岗位 + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(data.getFieldId()); + LambdaUpdateWrapper courseLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + courseLambdaUpdateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, data.getFieldId()); + courseLambdaUpdateWrapper.set(FtbCultivateCourse::getLearnJob, + doHandleLearnJob(ftbCultivateCourse.getLearnJob(), data.getNewPostionId())); + ftbCultivateCourseMapper.update(new FtbCultivateCourse(), courseLambdaUpdateWrapper); + courseLearningProgressBinding(data.getNewPostionId(), List.of(data.getFieldId())); + } else { + // 岗位考试 + FtbCultivatePositionExam ftbCultivatePositionExam = new FtbCultivatePositionExam(); + ftbCultivatePositionExam.setEnabledMark(0); + ftbCultivatePositionExam.setExamId(data.getFieldId()); + ftbCultivatePositionExam.setPostLearnId(data.getPostLearnId()); + ftbCultivatePositionExam.setPostRankId(data.getNewPostionId()); + ftbCultivatePositionExamService.save(ftbCultivatePositionExam); + // 考试与岗位进行绑定 + Boolean aBoolean = ftbCultivateExamService.bindExamAndPositionRelation(data.getFieldId(), data.getNewPostionId()); + if (!aBoolean) { + throw new RuntimeException("考试与岗位绑定失败"); + } + } + + } + + private void courseLearningProgressBinding(String postId, List courseIds) { + // 岗位课程事件 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + PositionCourseEventDTO.builder() + .courseIds(courseIds) + .postIds(List.of(postId)) + .eventType(PositionCourseEventDTO.EventType.POST) + .whetherToChange(false) + .build())); + } + + @Override + public List learningFromExistingPositions() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePosition::getPostId); + queryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + List ftbCultivatePositions = ftbCultivatePositionService.list(queryWrapper); + return ftbCultivatePositions.stream().map(FtbCultivatePosition::getPostId).collect(Collectors.toList()); + } + + private List handleExistingExistingPositions(FtbCultivatePositionCopyVO ftbCultivatePositionCopyVO, + List ftbCultivatePositionCourses, + String postId) { + if (CollUtil.isEmpty(ftbCultivatePositionCourses)) { + return Collections.emptyList(); + } + List courseIds = ftbCultivatePositionCourses.stream() + .map(FtbCultivatePositionCourse::getCourseId) + .collect(Collectors.toList()); + LambdaQueryWrapper courseLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseLambdaQueryWrapper.select( + SuperBaseEntity.SuperIBaseEntity::getId + , FtbCultivateCourse::getLearnJob + , FtbCultivateCourse::getName); + courseLambdaQueryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + courseLambdaQueryWrapper.eq(FtbCultivateCourse::getLabel, 2); + courseLambdaQueryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, courseIds); + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(courseLambdaQueryWrapper); + List result = new ArrayList<>(); + for (FtbCultivateCourse ftbCultivateCourse : ftbCultivateCourses) { + // 是否课程学习岗位包含新岗位 + String learnJob = ftbCultivateCourse.getLearnJob(); + if (learnJob.contains(postId)) { + result.add(ftbCultivateCourse.getId()); + } else { + FtbCopyRelatedPositionsVO ftbCopyRelatedPositionsVO = new FtbCopyRelatedPositionsVO(); + ftbCopyRelatedPositionsVO.setType(0); + ftbCopyRelatedPositionsVO.setName(ftbCultivateCourse.getName()); + ftbCopyRelatedPositionsVO.setFieldId(ftbCultivateCourse.getId()); + ftbCultivatePositionCopyVO.getCopyRelatedPositions().add(ftbCopyRelatedPositionsVO); + } + } + return result; + } + + private String doHandleLearnJob(String learJob, String postId) { + if (StrUtil.isBlank(learJob)) { + return postId; + } + return learJob + StringPool.COMMA + postId; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningService.java new file mode 100644 index 0000000..8de977c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service.impl; + +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbCultivatePositionCourceChapterLearningService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningServiceImpl.java new file mode 100644 index 0000000..8a2c561 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceChapterLearningServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceChapterLearningMapper; + +@Service +public class FtbCultivatePositionCourceChapterLearningServiceImpl extends ServiceImpl implements FtbCultivatePositionCourceChapterLearningService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningService.java new file mode 100644 index 0000000..3c237c7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.service.impl; + +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbCultivatePositionCourceLearningService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningServiceImpl.java new file mode 100644 index 0000000..0a658ce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourceLearningServiceImpl.java @@ -0,0 +1,11 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivatePositionCourceLearningServiceImpl extends ServiceImpl implements FtbCultivatePositionCourceLearningService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseCertificateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseCertificateServiceImpl.java new file mode 100644 index 0000000..5946f50 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseCertificateServiceImpl.java @@ -0,0 +1,83 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseCertificateMapper; +import jnpf.cultivate.service.FtbCultivatePositionCourseCertificateService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseCertificateWithNameVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** +* +* +*@Author: peng.hao +*@create: 2024/9/10:10:30 +*/ +@Service +public class FtbCultivatePositionCourseCertificateServiceImpl extends ServiceImpl implements FtbCultivatePositionCourseCertificateService { + + @Resource + private FtbCultivatePositionCourseCertificateMapper ftbCultivatePositionCourseCertificateMapper; + + @Override + public Map countByPositionLearnIds(List positionLearnIds) { + if (positionLearnIds == null || positionLearnIds.isEmpty()) { + return Map.of(); + } + + List result = ftbCultivatePositionCourseCertificateMapper.countByPositionLearnIds(positionLearnIds); + return result.stream() + .collect(Collectors.toMap( + BatchCommonCountDto::getSelectKey, + dto -> dto.getNum().longValue(), + (existing, replacement) -> existing + )); + } + + @Override + public List listByPostLearnId(String postLearnId) { + return ftbCultivatePositionCourseCertificateMapper.listByPostLearnId(postLearnId); + } + + @Override + public List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + + return ftbCultivatePositionCourseCertificateMapper.listByPostLearnIdAndCourseIds(postLearnId, courseIds); + } + + /** + * 根据岗位学习ID查询证书列表(包含证书名称) + * + * @param postLearnId 岗位学习ID + * @return 证书列表(包含证书名称) + */ + @Override + public List listByPostLearnIdWithName(String postLearnId) { + return ftbCultivatePositionCourseCertificateMapper.listByPostLearnIdWithName(postLearnId); + } + + /** + * 根据岗位学习 ID 和课程 IDs 查询证书列表(包含证书名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 证书列表(包含证书名称) + */ + @Override + public List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + return ftbCultivatePositionCourseCertificateMapper.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamService.java new file mode 100644 index 0000000..ab313cf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamService.java @@ -0,0 +1,60 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseExamWithNameVo; +import jnpf.model.cultivate.v2.position.vo.V2AllCultivatePositionCourseExam; + +import java.util.List; +import java.util.Map; + +public interface FtbCultivatePositionCourseExamService extends IService { + + /** + * 根据岗位学习ID列表统计考试数量 + */ + Map countByPositionLearnIds(List positionLearnIds); + + /** + * 根据岗位学习ID查询所有有效考试 + */ + List listByPostLearnId(String postLearnId); + + /** + * 根据岗位学习ID和课程ID列表查询所有有效考试 + */ + List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds, String gradeId); + + List queryAllPositionLearnExamLists(V2OtherCultivatePositionCourseForAppReq v2OtherCultivatePositionCourseForAppReq); + + List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId); + + List queryAllConfigExamId(List postLearnIds, String courseId,String gradeID); + + /** + * 根据岗位学习ID查询所有有效考试(包含考试名称) + * + * @param postLearnId 岗位学习ID + * @return 考试列表(包含考试名称) + */ + List listByPostLearnIdWithName(String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询考试(包含考试名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 考试列表(包含考试名称) + */ + List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds); + + /** + * 检查考试是否被岗位学习绑定(关联岗位学习主表和课程表) + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + Boolean checkExamBinding(String examId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamServiceImpl.java new file mode 100644 index 0000000..45db64d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseExamServiceImpl.java @@ -0,0 +1,106 @@ +package jnpf.cultivate.service.impl; + +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseExamWithNameVo; +import jnpf.model.cultivate.v2.position.vo.V2AllCultivatePositionCourseExam; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseExamMapper; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePositionCourseExamServiceImpl extends ServiceImpl implements FtbCultivatePositionCourseExamService { + + @Resource + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + + @Override + public Map countByPositionLearnIds(List positionLearnIds) { + if (positionLearnIds == null || positionLearnIds.isEmpty()) { + return Map.of(); + } + + List result = ftbCultivatePositionCourseExamMapper.countByPositionLearnIds(positionLearnIds); + return result.stream() + .collect(Collectors.toMap( + BatchCommonCountDto::getSelectKey, + dto -> dto.getNum().longValue(), + (existing, replacement) -> existing + )); + } + + @Override + public List listByPostLearnId(String postLearnId) { + return ftbCultivatePositionCourseExamMapper.listByPostLearnIdWithExam(postLearnId); + } + + @Override + public List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds,String gradeId) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + + return ftbCultivatePositionCourseExamMapper.listByPostLearnIdAndCourseIdsWithExam(postLearnId, courseIds,gradeId); + } + + @Override + public List queryAllPositionLearnExamLists(V2OtherCultivatePositionCourseForAppReq v2OtherCultivatePositionCourseForAppReq) { + return ftbCultivatePositionCourseExamMapper.queryAllPositionLearnExamListsWithExam(v2OtherCultivatePositionCourseForAppReq); + } + + @Override + public List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId) { + return ftbCultivatePositionCourseExamMapper.queryPositionBindItem(cultivatePosition, gradeId); + } + + @Override + public List queryAllConfigExamId(List postLearnIds, String courseId, String gradeId) { + return ftbCultivatePositionCourseExamMapper.queryAllConfigExamId(postLearnIds, courseId, gradeId); + } + + /** + * 根据岗位学习ID查询所有有效考试(包含考试名称) + * + * @param postLearnId 岗位学习ID + * @return 考试列表(包含考试名称) + */ + @Override + public List listByPostLearnIdWithName(String postLearnId) { + return ftbCultivatePositionCourseExamMapper.listByPostLearnIdWithName(postLearnId); + } + + /** + * 根据岗位学习 ID 和课程 ID 列表查询考试(包含考试名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 考试列表(包含考试名称) + */ + @Override + public List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + return ftbCultivatePositionCourseExamMapper.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + } + + /** + * 检查考试是否被岗位学习绑定(关联岗位学习主表和课程表) + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + @Override + public Boolean checkExamBinding(String examId) { + Integer count = ftbCultivatePositionCourseExamMapper.checkExamBinding(examId); + return count != null && count > 0; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityService.java new file mode 100644 index 0000000..bcb9a07 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityService.java @@ -0,0 +1,48 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseIdentityWithNameVo; + +import java.util.List; +import java.util.Map; + +public interface FtbCultivatePositionCourseIdentityService extends IService { + + /** + * 根据岗位学习ID列表统计鉴定数量 + */ + Map countByPositionLearnIds(List positionLearnIds); + + /** + * 根据岗位学习ID查询所有有效鉴定 + */ + List listByPostLearnId(String postLearnId); + + /** + * 根据岗位学习ID和课程ID列表查询所有有效鉴定 + */ + List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds,String gradeId); + + List queryAllPositionLearnIdentityLists(); + + List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId); + + /** + * 根据岗位学习ID查询鉴定列表(包含鉴定名称) + * + * @param postLearnId 岗位学习ID + * @return 鉴定列表(包含鉴定名称) + */ + List listByPostLearnIdWithName(String postLearnId); + + /** + * 根据岗位学习 ID 和课程 ID 列表查询鉴定列表(包含鉴定名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 鉴定列表(包含鉴定名称) + */ + List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityServiceImpl.java new file mode 100644 index 0000000..b708b4d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseIdentityServiceImpl.java @@ -0,0 +1,87 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseIdentityWithNameVo; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseIdentityMapper; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePositionCourseIdentityServiceImpl extends ServiceImpl implements FtbCultivatePositionCourseIdentityService { + + @Resource + private FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + + @Override + public Map countByPositionLearnIds(List positionLearnIds) { + if (positionLearnIds == null || positionLearnIds.isEmpty()) { + return Map.of(); + } + + List result = ftbCultivatePositionCourseIdentityMapper.countByPositionLearnIds(positionLearnIds); + return result.stream() + .collect(Collectors.toMap( + BatchCommonCountDto::getSelectKey, + dto -> dto.getNum().longValue(), + (existing, replacement) -> existing + )); + } + + @Override + public List listByPostLearnId(String postLearnId) { + return ftbCultivatePositionCourseIdentityMapper.listByPostLearnId(postLearnId); + } + + @Override + public List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds, String gradeId) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + return ftbCultivatePositionCourseIdentityMapper.listByPostLearnIdAndCourseIds(postLearnId, courseIds, gradeId); + } + + @Override + public List queryAllPositionLearnIdentityLists() { + return ftbCultivatePositionCourseIdentityMapper.queryAllPositionLearnIdentityLists(); + } + + @Override + public List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId) { + return ftbCultivatePositionCourseIdentityMapper.queryPositionBindItem(cultivatePosition, gradeId); + } + + /** + * 根据岗位学习ID查询鉴定列表(包含鉴定名称) + * + * @param postLearnId 岗位学习ID + * @return 鉴定列表(包含鉴定名称) + */ + @Override + public List listByPostLearnIdWithName(String postLearnId) { + return ftbCultivatePositionCourseIdentityMapper.listByPostLearnIdWithName(postLearnId); + } + + /** + * 根据岗位学习 ID 和课程 ID 列表查询鉴定列表(包含鉴定名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 鉴定列表(包含鉴定名称) + */ + @Override + public List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + return ftbCultivatePositionCourseIdentityMapper.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCoursePracticeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCoursePracticeServiceImpl.java new file mode 100644 index 0000000..942d311 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCoursePracticeServiceImpl.java @@ -0,0 +1,94 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionCoursePracticeMapper; +import jnpf.cultivate.service.FtbCultivatePositionCoursePracticeService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCoursePractice; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCoursePracticeWithNameVo; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 岗位学习课程关联练习表服务实现类 + * + * @author + * @since 2025-01-19 + */ +@Service +public class FtbCultivatePositionCoursePracticeServiceImpl extends ServiceImpl implements FtbCultivatePositionCoursePracticeService { + + @Resource + private FtbCultivatePositionCoursePracticeMapper ftbCultivatePositionCoursePracticeMapper; + + @Override + public Map countByPositionLearnIds(List positionLearnIds) { + if (positionLearnIds == null || positionLearnIds.isEmpty()) { + return Map.of(); + } + + List result = ftbCultivatePositionCoursePracticeMapper.countByPositionLearnIds(positionLearnIds); + return result.stream() + .collect(Collectors.toMap( + BatchCommonCountDto::getSelectKey, + dto -> dto.getNum().longValue(), + (existing, replacement) -> existing + )); + } + + @Override + public List listByPostLearnId(String postLearnId) { + return ftbCultivatePositionCoursePracticeMapper.listByPostLearnId(postLearnId); + } + + @Override + public List listByPostLearnIdAndCourseIds(String postLearnId, List courseIds) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + + return ftbCultivatePositionCoursePracticeMapper.listByPostLearnIdAndCourseIds(postLearnId, courseIds); + } + + @Override + public List queryAllPositionLearnParacticeLists(V2OtherCultivatePositionCourseForAppReq v2OtherCultivatePositionCourseForAppReq) { + return ftbCultivatePositionCoursePracticeMapper.queryAllPositionLearnParacticeLists(v2OtherCultivatePositionCourseForAppReq); + } + + @Override + public List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId) { + return ftbCultivatePositionCoursePracticeMapper.queryPositionBindItem(cultivatePosition, gradeId); + } + + /** + * 根据岗位学习ID查询练习列表(包含技能点名称) + * + * @param postLearnId 岗位学习ID + * @return 练习列表(包含技能点名称) + */ + @Override + public List listByPostLearnIdWithName(String postLearnId) { + return ftbCultivatePositionCoursePracticeMapper.listByPostLearnIdWithName(postLearnId); + } + + /** + * 根据岗位学习 ID 和课程 ID 列表查询练习列表(包含技能点名称) + * + * @param postLearnId 岗位学习 ID + * @param courseIds 课程 ID 列表 + * @return 练习列表(包含技能点名称) + */ + @Override + public List listByPostLearnIdAndCourseIdsWithName(String postLearnId, List courseIds) { + if (courseIds == null || courseIds.isEmpty()) { + return List.of(); + } + return ftbCultivatePositionCoursePracticeMapper.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseServiceImpl.java new file mode 100644 index 0000000..d275c70 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionCourseServiceImpl.java @@ -0,0 +1,188 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseMapper; +import jnpf.cultivate.service.FtbCultivatePositionCourseService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.v2.course.vo.app.AppCourseSimpleVo; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.CultivatePositionSimpleVo; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseWithNameVo; +import jnpf.model.cultivate.v2.position.vo.V2CultivatePositionDetailForApp; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePositionCourseServiceImpl extends ServiceImpl implements FtbCultivatePositionCourseService { + + @Resource + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + + /** + * @param positionLearnIds 岗位学习 id 列表 + * @return 数量 + */ + @Override + public Map countByPositionLearnIds(List positionLearnIds) { + if (positionLearnIds == null || positionLearnIds.isEmpty()) { + return Map.of(); + } + + List result = ftbCultivatePositionCourseMapper.countByPositionLearnIds(positionLearnIds); + return result.stream() + .collect(Collectors.toMap( + BatchCommonCountDto::getSelectKey, + dto -> dto.getNum().longValue(), + (existing, replacement) -> existing + )); + } + + /** + * 根据岗位学习 ID 查询所有有效课程 + * + * @param postLearnId 岗位学习 ID + * @param courseId 课程 ID + * @return 课程列表 + */ + @Override + public List listByPostLearnId(String postLearnId, String courseId) { + return ftbCultivatePositionCourseMapper.listByPostLearnId(postLearnId, courseId); + } + + /** + * 根据岗位学习 ID 和职级 ID 查询所有有效课程 + * + * @param postLearnId 岗位学习 ID + * @param gradeId 职级 ID + * @param courseId 课程 ID + * @return 课程列表 + */ + @Override + public List listByPostLearnIdAndGradeId(String postLearnId, String gradeId, String courseId) { + return ftbCultivatePositionCourseMapper.listByPostLearnIdAndGradeId(postLearnId, gradeId, courseId); + } + + /** + * 查询所有岗位学习 + * + * @param courseName 课程名称 + * @return 岗位学习列表 + */ + @Override + public List queryAllPositionLearn(String courseName) { + return ftbCultivatePositionCourseMapper.queryAllPositionLearn(courseName); + } + + /** + * 查询所有岗位学习课程列表 + * + * @param dto 请求参数 + * @return 岗位学习课程列表 + */ + @Override + public List queryAllPositionLearnCourseLists(V2OtherCultivatePositionCourseForAppReq dto) { + List list = ftbCultivatePositionCourseMapper.queryAllPositionLearnCourseLists(dto); + if (CollUtil.isEmpty(list)) { + return list; + } + + list.forEach(course -> course.setBusinessSource(PositionBusinessSourceEnum.POST_LEARNING)); + return list; + } + + /** + * 根据岗位学习 ID、年级 ID 和考试 ID 查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习 ID + * @param gradeId 职级 id + * @param examId 考试 ID + * @return FtbCultivatePositionCourse 对象列表 + */ + @Override + public List listByPostLearnIdAndExamId(String positionLearnId, String gradeId, String examId) { + return ftbCultivatePositionCourseMapper.listByPostLearnIdAndExamId(positionLearnId, gradeId, examId); + } + + /** + * 根据岗位学习 ID、年级 ID 和实践 ID 查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习 ID + * @param gradeId 职级 + * @param practiceId 实践 ID + * @return FtbCultivatePositionCourse 对象列表 + */ + @Override + public List listByPostLearnIdAndPracticeId(String positionLearnId, String gradeId, String practiceId) { + return ftbCultivatePositionCourseMapper.listByPostLearnIdAndPracticeId(positionLearnId, gradeId, practiceId); + } + + /** + * 根据岗位学习 ID、年级 ID 和身份 ID 查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习 ID + * @param gradeId 职级 iD + * @param identityId 鉴定 iD + * @return FtbCultivatePositionCourse 对象列表 + */ + @Override + public List listByPostLearnIdAndIdentityId(String positionLearnId, String gradeId, String identityId) { + return ftbCultivatePositionCourseMapper.listByPostLearnIdAndIdentityId(positionLearnId, gradeId, identityId); + } + + /** + * 根据岗位学习 ID、年级 ID 查询培养岗位课程列表 + * + * @param positionLearnId 岗位学习 ID + * @param gradeId 职级 iD + * @return FtbCultivatePositionCourse 对象列表 + */ + @Override + public List queryAllPositionCourseAndLearnIdAndGradeId(String positionLearnId, String gradeId) { + return ftbCultivatePositionCourseMapper.queryAllPositionCourseAndLearnIdAndGradeId(positionLearnId, gradeId); + } + + @Override + public List listPositionCourseList(FtbCultivatePosition existingPosition, V2CultivatePositionDetailForApp req) { + return ftbCultivatePositionCourseMapper.listPositionCourseList(existingPosition, req); + } + + @Override + public List listAllCourseByPostLearnId(String positionLearnId, String postId, String gradeId, Integer compulsory) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FtbCultivatePositionCourse::getPostLearnId, positionLearnId); + wrapper.eq(FtbCultivatePositionCourse::getPostRankId, postId); + wrapper.eq(StringUtils.isNotEmpty(gradeId), FtbCultivatePositionCourse::getGradeId, gradeId); + wrapper.eq(compulsory != null, FtbCultivatePositionCourse::getCompulsory, compulsory); + wrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); // 只查询有效的记录 + wrapper.orderByAsc(FtbCultivatePositionCourse::getSortCode); + return list(wrapper); + } + + @Override + public List queryPositionBindItem(FtbCultivatePosition cultivatePosition, String gradeId) { + return ftbCultivatePositionCourseMapper.queryPositionBindItem(cultivatePosition, gradeId); + } + + /** + * 根据岗位学习ID查询所有有效课程(包含课程名称) + * + * @param postLearnId 岗位学习ID + * @param courseId 课程ID + * @return 课程列表(包含课程名称) + */ + @Override + public List listByPostLearnIdWithName(String postLearnId, String courseId) { + return ftbCultivatePositionCourseMapper.listByPostLearnIdWithName(postLearnId, courseId); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamCourseServiceImpl.java new file mode 100644 index 0000000..19f3003 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamCourseServiceImpl.java @@ -0,0 +1,90 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseExamMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionExamMapper; +import jnpf.cultivate.service.FtbCultivatePositionExamCourseService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.bo.FtbCultivateCourseBO; +import jnpf.model.cultivate.bo.FtbCultivatePositionExamBO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; +import jnpf.permission.vo.v2.position.PositionVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePositionExamCourseServiceImpl implements FtbCultivatePositionExamCourseService { + + @Resource + private FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + @Resource + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + @Resource + private FtbCultivateCourseMapper cultivateCourseMapper; + @Autowired + private UserApiV2Util userApiV2Util; + + + @Override + public FtbCultivatePositionExamBO testPaperCall(String examId, String relationPositonId, String businessId, Integer examSource) { + FtbCultivatePositionExamBO ftbCultivatePositionExamBO = new FtbCultivatePositionExamBO(); + // 职等id + String postRankId = null; + // 考试来源,1岗位学习考试,2课程考试 + if (examSource == 1) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExam::getExamId, examId); + queryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, relationPositonId); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, businessId); + queryWrapper.last("limit 1"); + FtbCultivatePositionExam ftbCultivatePositionExam = ftbCultivatePositionExamMapper.selectOne(queryWrapper); + postRankId = ftbCultivatePositionExam.getPostRankId(); + } else if (examSource == 2) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourseExam::getExamId, examId); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, relationPositonId); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, businessId); + queryWrapper.last("limit 1"); + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectOne(queryWrapper); + postRankId = ftbCultivatePositionCourseExam.getPostRankId(); + } + if (postRankId != null) { + ftbCultivatePositionExamBO.setPostRankId(postRankId); + try { + // 职等名称 +// ActionResult gradesInfo = positionApi.getGradesInfo(postRankId); + PositionVO gradesInfoData = userApiV2Util.infoPosition(postRankId, null); + if (gradesInfoData != null) { + // 岗位名称 + ftbCultivatePositionExamBO.setPostName(gradesInfoData.getFullName()); + ftbCultivatePositionExamBO.setPostId(gradesInfoData.getId()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return ftbCultivatePositionExamBO; + } + + @Override + public List getCourseName(List courseIds) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, courseIds); + List ftbCultivateCourseList = cultivateCourseMapper.selectList(queryWrapper); + return ftbCultivateCourseList.stream().map(ftbCultivateCourse -> { + FtbCultivateCourseBO ftbCultivateCourseBO = new FtbCultivateCourseBO(); + ftbCultivateCourseBO.setCourseId(ftbCultivateCourse.getId()); + ftbCultivateCourseBO.setCourseName(ftbCultivateCourse.getName()); + return ftbCultivateCourseBO; + }).collect(Collectors.toList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamIdentifyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamIdentifyServiceImpl.java new file mode 100644 index 0000000..f35ed1f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamIdentifyServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.service.impl; + + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionExamIdentifyMapper; +import jnpf.cultivate.service.FtbCultivatePositionExamIdentifyService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivatePositionExamIdentifyServiceImpl extends ServiceImpl implements FtbCultivatePositionExamIdentifyService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamServiceImpl.java new file mode 100644 index 0000000..99e49f7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionExamServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionExamMapper; +import jnpf.cultivate.service.FtbCultivatePositionExamService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivatePositionExamServiceImpl extends ServiceImpl implements FtbCultivatePositionExamService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionForAppServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionForAppServiceImpl.java new file mode 100644 index 0000000..148195c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionForAppServiceImpl.java @@ -0,0 +1,311 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivatePositionForAppService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.dto.position.app.FtbCultivatePositionForAppNewDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.resp.CompleteExamVo; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.position.app.FtbPopUpPromptVO; +import jnpf.model.cultivate.vo.position.app.FtbSubordinateLearningCoursesVO; +import jnpf.model.cultivate.vo.position.app.OnTheJobLearningCourseVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.util.context.SpringContext; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Title: FtbCultivatePositionForAppServiceImpl + * @Author:peng.hao + * @create: 2023/12/2717:35 + */ +@Slf4j +@Service +public class FtbCultivatePositionForAppServiceImpl implements FtbCultivatePositionForAppService { + + @Resource + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + @Resource + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + @Resource + private FtbCultivatePositionCourceLearningService ftbCultivatePositionCourceLearningService; + @Resource + private FtbCultivatePositionCourceChapterLearningService ftbCultivatePositionCourceChapterLearningService; + @Resource + private FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + @Resource + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Resource + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + @Transactional + public OnTheJobLearningCourseVO onTheJobLearningCourse(FtbCultivatePositionForAppNewDTO dto) { + // 课程是否初始化 + isTheCourseInitialized(dto.getUserId(), dto.getPostId()); + OnTheJobLearningCourseVO onTheJobLearningCourse = new OnTheJobLearningCourseVO(); + // 课程内容 + List fullCurriculumAppList = ftbCultivatePositionCourceLearningMapper.onTheJobLearningCourse(dto); + // 课程总数 + onTheJobLearningCourse.setTotalCourse(fullCurriculumAppList.size()); + // 已学习课程数 + long alreadyLearnedCourse = fullCurriculumAppList.stream().filter(a -> a.getLearnState() == 1).count(); + onTheJobLearningCourse.setAlreadyLearnedCourse(alreadyLearnedCourse); + fullCurriculumAppList.forEach(item -> { + // 学习总人数 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(FtbCultivatePositionCourceLearning::getState, List.of(1, 2)); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, item.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + item.setLearnTotalNumber(ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper)); + item.setIsOffline(0); + // 是否为线下培训课程,0否,1是 + Integer trainingCourses = ftbCultivatePositionCourceLearningMapper.whetherToAddOfflineTrainingCourses(dto.getUserId(), item.getCourseId()); + if (trainingCourses > 0) { + item.setIsOffline(1); + } + }); + onTheJobLearningCourse.setFullCurriculumAppList(fullCurriculumAppList); + // bug编号12888,修复如果岗位学习中存在1,2, 3 课程时,如果1,2 课程已学习完,然后把3课程下架或者删除,无法触发考试与鉴定 + // 下层做业务幂等性,会存在重复调用的情况 + + String userId = dto.getUserId(); + Integer compulsory = dto.getCompulsory(); + + if (compulsory == 0) { + // 任务必修课程 + LambdaQueryWrapper courseWrper = new LambdaQueryWrapper<>(); + courseWrper.eq(FtbCultivatePositionCourse::getPostRankId, dto.getPostId()); + courseWrper.eq(FtbCultivatePositionCourse::getCompulsory, 0); + courseWrper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + List ftbCultivatePositionCourses = ftbCultivatePositionCourseMapper.selectList(courseWrper); + List courseIds = ftbCultivatePositionCourses.stream() + .map(FtbCultivatePositionCourse::getCourseId) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(courseIds)) { + boolean isStartStudyCourse = checkCourseStudyStatus(userId, courseIds); + if (isStartStudyCourse) { + int needStudyNum = 0; + LambdaQueryWrapper courseWraper = Wrappers.lambdaQuery(); + courseWraper.in(FtbCultivateCourse::getId, courseIds); + courseWraper.eq(FtbCultivateCourse::getEnableMark, 0); + courseWraper.eq(FtbCultivateCourse::getIsGroundIng, 1);//上下架(0下架,1上架) + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(courseWraper); + if (CollUtil.isEmpty(ftbCultivateCourses)) { + needStudyNum = 0; + } else { + needStudyNum = ftbCultivateCourses.size(); + courseIds = ftbCultivateCourses.stream() + .map(FtbCultivateCourse::getId) + .collect(Collectors.toList()); + } + if (needStudyNum == 0 || hasTheCourseBeenCompleted(userId, courseIds)) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + ExamIdentifyTriggerEventDTO.builder() + .postId(dto.getPostId()) + .userId(dto.getUserId()) + .triggerEventType(TriggerEventBO.TriggerEventType.POST) + .build())); + } + } + } + } + return onTheJobLearningCourse; + } + + + /** + * 查询课是否开始学习 + * + * @param userId 用户ID + * @param courseIds 课程ID集合 + * @return boolean false-未开始学习 true-开始学习 + */ + protected boolean checkCourseStudyStatus(String userId, List courseIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + query.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + query.in(FtbCultivatePositionCourceLearning::getState, List.of("1"));//学习状态 1已学习,0未学习,2学习中 + query.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + return ftbCultivatePositionCourceLearningMapper.selectCount(query) > 0; + } + + /** + * 查询是否完成了课程学习 + * + * @param userId 用户id + * @param courseIds 课程id + * @return + */ + private boolean hasTheCourseBeenCompleted(String userId, List courseIds) { + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + query.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + query.in(FtbCultivatePositionCourceLearning::getState, List.of("0", "2")); + query.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + return ftbCultivatePositionCourceLearningMapper.selectCount(query) == 0; + } + + private void isTheCourseInitialized(String userId, String postId) { + List courseIds = ftbCultivatePositionCourseMapper.getAnExistingCoursePostId(postId); + if (CollectionUtils.isNotEmpty(courseIds)) { + List anExistingCourse = ftbCultivatePositionCourseMapper.getAnExistingCourse(courseIds, userId); + List courseUnExistingCourseIds = courseIds.stream() + .filter(a -> !anExistingCourse.contains(a)) + .collect(Collectors.toList()); + checkCourse(courseUnExistingCourseIds, userId); + } + } + + private void checkCourse(List courseIds, String userId) { + if (CollectionUtils.isNotEmpty(courseIds)) { + List ftbCultivatePositionCourceLearnings = new ArrayList<>(); + List ftbCultivatePositionCourceChapterLearningList = new ArrayList<>(); + courseIds.forEach(courseId -> { + // 课程 + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = new FtbCultivatePositionCourceLearning(); + ftbCultivatePositionCourceLearning.setCourceId(courseId); + ftbCultivatePositionCourceLearning.setLearnTime(0); + ftbCultivatePositionCourceLearning.setUserId(userId); + ftbCultivatePositionCourceLearning.setState(0); + ftbCultivatePositionCourceLearnings.add(ftbCultivatePositionCourceLearning); + // 章节 + LambdaQueryWrapper ftbCultivateCourseChapterLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + ftbCultivateCourseChapterLambdaQueryWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId); + List ftbCultivateCourseChapters = ftbCultivateCourseChapterMapper.selectList(ftbCultivateCourseChapterLambdaQueryWrapper); + if (CollectionUtils.isNotEmpty(ftbCultivateCourseChapters)) { + ftbCultivateCourseChapters.forEach(ftbCultivateCourseChapter -> { + FtbCultivatePositionCourceChapterLearning ftbCultivatePositionCourceChapterLearning = new FtbCultivatePositionCourceChapterLearning(); + ftbCultivatePositionCourceChapterLearning.setChapterId(ftbCultivateCourseChapter.getId()); + ftbCultivatePositionCourceChapterLearning.setState(0); + ftbCultivatePositionCourceChapterLearning.setCourceId(courseId); + ftbCultivatePositionCourceChapterLearning.setChapterTime(0); + ftbCultivatePositionCourceChapterLearning.setUserId(userId); + ftbCultivatePositionCourceChapterLearningList.add(ftbCultivatePositionCourceChapterLearning); + }); + } + }); + ftbCultivatePositionCourceLearningService.saveBatch(ftbCultivatePositionCourceLearnings); + ftbCultivatePositionCourceChapterLearningService.saveBatch(ftbCultivatePositionCourceChapterLearningList); + } + } + + @Override + public Page subordinateLearningCourses(FtbCultivatePositionForAppNewDTO dto, Page result) { + result = ftbCultivatePositionCourceLearningMapper.subordinateLearningCourses(result, dto); + return result; + } + + + @Override + public PageListVO queryIdentityTrainDataDetail(FtbCultivatePositionForAppNewDTO dto, + CultivatePage page) { + List userIdentifyInfoApi = cultivateIdentifyApplyService.getUserIdentifyInfoWithUserId(dto.getUserId()); + return CultivatePage.paginate(userIdentifyInfoApi, page.coverCultivatePage()); + } + + @Override + public PageListVO subordinatePracticalAppraisal(FtbCultivatePositionForAppNewDTO dto, CultivatePage page) { + Page voPage = page.coverCultivatePage(); + String[] split = dto.getPostId().split(","); + voPage = ftbCultivatePositionCourceLearningMapper.practicalAppraisalList(voPage, dto.getUserId(), List.of(split)); + PageListVO result = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setTotal((int) voPage.getTotal()); + pagination.setPageSize(voPage.getSize()); + pagination.setCurrentPage(voPage.getCurrent()); + result.setPagination(pagination); + if (CollUtil.isNotEmpty(voPage.getRecords())) { + List userIdentifyInfoApi = cultivateIdentifyApplyService.getUserIdentifyInfoApi(voPage.getRecords()); + result.setList(userIdentifyInfoApi); + } + return result; + } + + @Override + public Page subordinateLearningCoursess(FtbCultivatePositionForAppNewDTO dto, Page result) { + result = ftbCultivatePositionCourceLearningMapper.subordinateLearningCoursess(result, dto, List.of(dto.getPostId().split(","))); + return result; + } + + @Override + public FtbPopUpPromptVO popUpPrompt(String userId, String positionId) { + FtbPopUpPromptVO ftbPopUpPromptVO = new FtbPopUpPromptVO(); + // 是否已经考试,根据岗位id+用户id查询 + CompleteExamVo completeExamVo = ftbCultivateExamUserService.checkIsCompleteExamForUserId(positionId, userId); + if (Objects.nonNull(completeExamVo)) { + if (completeExamVo.getIsHasExam()) { + ftbPopUpPromptVO.setIsExam(1); + } + if (completeExamVo.getIsComplete()) { + ftbPopUpPromptVO.setExamResults(1); + } + } + // 实操鉴定,根据岗位id+用户id查询,解决换绑问题 + String identifyId = ftbCultivatePositionMapper.queryPracticalAppraisalByPostId(positionId, userId); + if (StrUtil.isNotBlank(identifyId)) { + ftbPopUpPromptVO.setIsIdentify(1); + // 是否已经鉴定 + if (ftbCultivatePositionMapper.queryIdentificationResults(positionId, userId, identifyId) > 0) { + ftbPopUpPromptVO.setIdentificationResults(1); + } + } + // 岗位考试合格后才能进行鉴定,0否1是 + LambdaQueryWrapper ftbCultivatePositionLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionLambdaQueryWrapper.select(FtbCultivatePosition::getQualified); + ftbCultivatePositionLambdaQueryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + ftbCultivatePositionLambdaQueryWrapper.eq(FtbCultivatePosition::getPostId, positionId); + ftbCultivatePositionLambdaQueryWrapper.eq(FtbCultivatePosition::getIsGrounding, 1); + FtbCultivatePosition ftbCultivatePosition = ftbCultivatePositionMapper.selectOne(ftbCultivatePositionLambdaQueryWrapper); + if (Objects.nonNull(ftbCultivatePosition)) { + ftbPopUpPromptVO.setQualified(ftbCultivatePosition.getQualified()); + } + // 岗位名称 +// PositionEntity positionEntity = positionApi.queryInfoById(positionId); + PositionVO positionEntity = userApiV2Util.infoPosition(positionId, null); + if(positionEntity != null) { + ftbPopUpPromptVO.setPositionName(positionEntity.getFullName()); + } + return ftbPopUpPromptVO; + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionMemberServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionMemberServiceImpl.java new file mode 100644 index 0000000..22fbb53 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionMemberServiceImpl.java @@ -0,0 +1,293 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.authority.service.FtbPermissionUsersService; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceChapterLearningMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivatePositionMemberService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivateBatchQueryService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.resp.AppExamListVo; +import jnpf.model.cultivate.resp.UserExamCount; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterVo; +import jnpf.model.cultivate.v2.course.vo.app.V2CourseOutlineAppVo; +import jnpf.model.cultivate.vo.identify.IdentifyApplyStatisticsApiVo; +import jnpf.model.cultivate.vo.identify.UserIdentifyPageVo; +import jnpf.model.cultivate.vo.position.FtbCultivateCourseListVO; +import jnpf.model.cultivate.vo.position.FtbCultivatePositionUserInfoVo; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import jnpf.util.UploaderUtil; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @Title:FtbCultivatePositionMemberServiceImpl + * @Author:peng.hao + * @create: 2023/12/2819:23 + */ +@Service +public class FtbCultivatePositionMemberServiceImpl implements FtbCultivatePositionMemberService { + + @Resource + FtbCultivateExamUserService ftbCultivateExamUserService; + + @Resource + private FtbCultivatePositionCourceChapterLearningMapper ftbCultivatePositionCourceChapterLearningMapper; + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Resource + CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbPermissionUsersService ftbPermissionUsersService; + + @Autowired + private FtbCultivateCourseChapterService courseChapterService; + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + @Override + public PageListVO listUser(QueryPageUserDTO searchDto) { +// UserQueryDto queryDto = new UserQueryDto(); +// queryDto.setQueryCondition(keyWords); +// queryDto.setCurrentPage((int) page.getCurrentPage()); +// queryDto.setPageSize((int) page.getPageSize()); + //查询所有用户 +// PageListVO partUserInfoPage = userApi.getPartUserInfoPage(queryDto); +// PageListVO partUserInfoPage = userApiV2Util.listAllUserForPage(page.getPageSize(), page.getCurrentPage(), keyWords, null); + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setTenantId(userApiV2Util.getCurrentLoginTenantId()); + if (searchDto.getPageSize() == 0) { + dto.setPageSize(10L); + } else { + dto.setPageSize(searchDto.getPageSize()); + } + if (searchDto.getCurrentPage() <= 1) { + dto.setCurrentPage(1L); + } else { + dto.setCurrentPage(searchDto.getCurrentPage()); + } + if (StringUtil.isNotEmpty(searchDto.getKeyword())) { + dto.setKeyword(searchDto.getKeyword()); + } + if (CollUtil.isNotEmpty(searchDto.getOrganizeIds())) { + dto.setOrganizeIds(searchDto.getOrganizeIds()); + } + if (StringUtil.isNotEmpty(searchDto.getPositionId())) { + dto.setPositionId(searchDto.getPositionId()); + } + PageListVO partUserInfoPage = ftbPermissionUsersService.pagePost(dto); + + PageListVO userInfoPage = new PageListVO<>(); + userInfoPage.setPagination(partUserInfoPage.getPagination()); + + List list = partUserInfoPage.getList(); + List infoVoList = JsonUtil.getJsonToList(JsonUtil.getObjectToString(list), FtbCultivatePositionUserInfoVo.class); + infoVoList.forEach(item -> { + item.setHeadIcon(UploaderUtil.uploaderImg(item.getHeadIcon())); + }); + userInfoPage.setList(infoVoList); + List positionIds = infoVoList.stream().map(FtbCultivatePositionUserInfoVo::getPositionId).collect(Collectors.toList()); + //筛选出所有的用户ids + List userIds = infoVoList.stream().map(FtbCultivatePositionUserInfoVo::getId).collect(Collectors.toList()); + if (ObjectUtils.isEmpty(positionIds)) { + return userInfoPage; + } + // 鉴定统计 + Map userIdentifyStatistics = cultivateIdentifyApplyService.getUserIdentifyStatistics(userIds); + // 接入课程统计 + Map stringFtbCultivateCourseListVOMap = doAccessCourseStatistics(userIds); + // 考试统计 + Map stringUserExamCountMap = ftbCultivateExamUserService.queryExamTotalAndCompleteNumForUserIds(userIds); + infoVoList.forEach(item -> { + UserExamCount userExamCount = stringUserExamCountMap.get(item.getId()); + if (Objects.nonNull(userExamCount)) { + item.setFinishExamNumber(userExamCount.getCompleteNum().longValue()); + item.setTotalExamNumber(userExamCount.getTotleNum().longValue()); + } + FtbCultivateCourseListVO ftbCultivateCourseListVO = stringFtbCultivateCourseListVOMap.get(item.getId()); + if (Objects.nonNull(ftbCultivateCourseListVO)) { + item.setTotalCourseNumber(ftbCultivateCourseListVO.getTotalNumberOfStudies().longValue()); + item.setFinishCourseNumber(ftbCultivateCourseListVO.getNumbersLearned().longValue()); + } + IdentifyApplyStatisticsApiVo identifyApplyStatisticsApiVo = userIdentifyStatistics.get(item.getId()); + if (Objects.nonNull(identifyApplyStatisticsApiVo)) { + item.setTotalIdentifyNumber(identifyApplyStatisticsApiVo.getTotalNumber().longValue()); + item.setFinishIdentifyNumber(identifyApplyStatisticsApiVo.getAlreadyNumber().longValue()); + } + }); + return userInfoPage; + } + + /** + * 获取课程所有章节 + * + * @param courseId 课程ID + * @return 章节列表 + */ + private List getCourseChapters(String courseId) { + LambdaQueryWrapper courseChapterWrapper = Wrappers.lambdaQuery(); + courseChapterWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId); + courseChapterWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + return courseChapterService.list(courseChapterWrapper); + } + + /** + * 批量查询用户学习状态 + * + * @param userId 用户ID + * @param chapterList 章节ID列表 + * @return 章节学习状态列表 + */ + public Map batchQueryCourseChapterLearningState(String userId, List chapterList) { + + Map chapterLearningMap = new HashMap<>(); + LambdaQueryWrapper queryChapterWrapper = Wrappers.lambdaQuery(); + queryChapterWrapper.in(FtbCultivatePositionCourceChapterLearning::getChapterId, chapterList); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List list = ftbCultivatePositionCourceChapterLearningMapper.selectList(queryChapterWrapper); + if (CollUtil.isEmpty(list)) { + return chapterLearningMap; + } + return list.stream() + .collect(Collectors.toMap(FtbCultivatePositionCourceChapterLearning::getId, item -> item)); + } + + /** + * 获取用户课程列表 + * + * @param userId 用户ID + * @param page 分页参数 + * @return 课程列表 + */ + @Override + public PageListVO listCourse(String userId, CultivatePage page) { + Page pageList = page.coverCultivatePage(); + pageList = ftbCultivatePositionCourceChapterLearningMapper.getCourseInformationList(pageList, userId); + + // 提取所有课程ID + List courseIds = pageList.getRecords().stream() + .map(FtbCultivateCourseListVO::getCourseId) + .collect(Collectors.toList()); + + // 使用V2批量查询接口,一次性获取所有课程的章节及学习状态 + Map> courseChapterMap = v2CultivateBatchQueryService.batchQueryUserCourseChapterLearnStatus(userId, courseIds); + + // 填充每个课程的信息 + pageList.getRecords().forEach(ftbCultivateCourseListVO -> { + String courseId = ftbCultivateCourseListVO.getCourseId(); + List chapterTree = courseChapterMap.getOrDefault(courseId, new java.util.ArrayList<>()); + + // 计算已学章节数和总章节数 + long completedCount = chapterTree.stream() + .flatMap(this::flattenChapters) + .filter(chapter -> chapter.getState() != null && chapter.getState() == 1) + .count(); + long totalCount = chapterTree.stream() + .flatMap(this::flattenChapters) + .count(); + + ftbCultivateCourseListVO.setNumbersLearned((int) completedCount); + ftbCultivateCourseListVO.setTotalNumberOfStudies((int) totalCount); + ftbCultivateCourseListVO.setChapterInformationVOS(chapterTree); + ftbCultivateCourseListVO.setUserId(userId); + }); + return CultivatePage.coverPageList(pageList); + } + + /** + * 递归展平章节树,用于统计所有章节数量 + */ + private java.util.stream.Stream flattenChapters(V2ChapterVo chapter) { + java.util.stream.Stream selfStream = java.util.stream.Stream.of(chapter); + if (CollUtil.isNotEmpty(chapter.getChild())) { + return java.util.stream.Stream.concat( + selfStream, + chapter.getChild().stream().flatMap(this::flattenChapters) + ); + } + return selfStream; + } + + private Map doAccessCourseStatistics(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + List positionCourceLearningList = + ftbCultivatePositionCourceLearningMapper.accessCourseStatistics(userIds); + if (CollUtil.isEmpty(positionCourceLearningList)) { + return new HashMap<>(); + } + // 按用户分组 + Map> collected = positionCourceLearningList + .stream() + .collect(Collectors.groupingBy(FtbCultivatePositionCourceLearning::getUserId)); + // 组装数据 + return collected + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> { + FtbCultivateCourseListVO ftbCultivateCourseListVO = new FtbCultivateCourseListVO(); + ftbCultivateCourseListVO.setTotalNumberOfStudies(entry.getValue().size()); + List collect = entry.getValue().stream().filter(item -> item.getState() == 1).collect(Collectors.toList()); + ftbCultivateCourseListVO.setNumbersLearned(collect.size()); + return ftbCultivateCourseListVO; + })); + } + + /** + * 考试用户服务 + */ + + @Autowired + private FtbCultivateExamUserService examUserService; + + /** + * 根据用户ID 分页查询考试列表 + * + * @param userId 用户ID + * @param page + * @return + */ + @Override + public PageInfo listExam(String userId, CultivatePage page) { + return examUserService.queryExamListForUserId(userId, page); + } + + @Override + public PageListVO listIdentify(String userId, CultivatePage page) { + PageListVO userIdentifyPage = cultivateIdentifyApplyService.getUserIdentifyPage(userId, page); + return userIdentifyPage; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionServiceImpl.java new file mode 100644 index 0000000..a5674cf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionServiceImpl.java @@ -0,0 +1,954 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.model.cultivate.dto.position.*; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionJobLearnCourseCertificateDTO; +import jnpf.model.cultivate.dto.position.web.FtbCultivatePositionJobLearnCourseStateSwitchDTO; +import jnpf.model.cultivate.dto.position.web.FtbJobLearnCourseRetakeCertificateDTO; +import jnpf.model.cultivate.entiy.BaseEntity; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.course.PositionCourseEventDTO; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.mesgg.CultivateMessageInfo; +import jnpf.model.cultivate.po.position.*; +import jnpf.model.cultivate.resp.InnerExamDto; +import jnpf.model.cultivate.resp.PaperVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePositionVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePostAndGradeVo; +import jnpf.model.cultivate.vo.identify.IdentifyTableInfoApiVo; +import jnpf.model.cultivate.vo.position.*; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionLevelMapVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.util.StringUtil; +import jnpf.util.context.SpringContext; +import lombok.RequiredArgsConstructor; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FtbCultivatePositionServiceImpl extends ServiceImpl implements FtbCultivatePositionService { + + protected Log log = LogFactory.getLog(getClass()); + + private final FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + private final FtbCultivatePositionExamIdentifyMapper ftbCultivatePositionExamIdentifyMapper; + private final FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + private final FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + private final FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + private final FtbCultivateExamMapper ftbCultivateExamMapper; + private final CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + private final FtbCultivateExamService ftbCultivateExamService; + private final CultivateIdentifyTableService cultivateIdentifyTableService; + private final FtbCultivateCourseMapper ftbCultivateCourseMapper; + private final FtbCultivateMessageInfoMapper ftbCultivateMessageInfoMapper; + private final FtbCultivateExamUserService ftbCultivateExamUserService; + private final FtbCultivatePromotionNewService ftbCultivatePromotionNewService; + private final FtbCultivatePositionCertificateMapper ftbCultivatePositionCertificateMapper; + private final FtbCultivateCertificateMapper ftbCultivateCertificateMapper; + private final FtbCultivatePositionCourseCertificateMapper ftbCultivatePositionCourseCertificateMapper; + private final FtbCultivatePositionSettingMapper ftbCultivatePositionSettingMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + @Transactional(rollbackFor = Exception.class) + public String addJobLearning(FtbCultivatePositionJobLearnDTO ftbCultivatePositionJobLearnDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePosition::getPostId, ftbCultivatePositionJobLearnDTO.getPostId()); + queryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + if (baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("该岗位已存在学习记录"); + } + FtbCultivatePosition ftbCultivatePosition = ftbCultivatePositionJobLearnDTO.convertFtbCultivatePosition(ftbCultivatePositionJobLearnDTO); + baseMapper.insert(ftbCultivatePosition); + return ftbCultivatePosition.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void upperLower(FtbCultivatePositionShelvesDTO ftbCultivateShelvesDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbCultivateShelvesDTO.getId()); + updateWrapper.set(FtbCultivatePosition::getIsGrounding, ftbCultivateShelvesDTO.getIsGrounding()); + baseMapper.update(new FtbCultivatePosition(), updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addJobLearningExam(FtbCultivatePositionJobLearnExamDTO ftbCultivatePositionJobLearnExamDTO) { + jobLearningExaminationJudgment(ftbCultivatePositionJobLearnExamDTO.getPostLearnId(), ftbCultivatePositionJobLearnExamDTO.getExamId()); + // 是否存在,存在则更新 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, ftbCultivatePositionJobLearnExamDTO.getPostLearnId()); + queryWrapper.eq(FtbCultivatePositionExam::getPostRankId, ftbCultivatePositionJobLearnExamDTO.getPostRankId()); + FtbCultivatePositionExam exam = ftbCultivatePositionExamMapper.selectOne(queryWrapper); + if (Objects.nonNull(exam)) { + exam.setExamId(ftbCultivatePositionJobLearnExamDTO.getExamId()); + ftbCultivatePositionExamMapper.updateById(exam); + return; + } + FtbCultivatePositionExam ftbCultivatePositionExam = ftbCultivatePositionJobLearnExamDTO + .convertFtbCultivatePositionExam(ftbCultivatePositionJobLearnExamDTO); + ftbCultivatePositionExamMapper.insert(ftbCultivatePositionExam); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addPostLearningPracticalAppraisal(FtbCultivatePositionLearnPracticalDTO ftbCultivatePositionLearnPracticalDTO) { + onTheJobLearningPracticalAppraisal(ftbCultivatePositionLearnPracticalDTO.getPostLearnId(), + ftbCultivatePositionLearnPracticalDTO.getIdentifyId()); + // 是否存在,存在则更新 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostLearnId, ftbCultivatePositionLearnPracticalDTO.getPostLearnId()); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostRankId, ftbCultivatePositionLearnPracticalDTO.getPostRankId()); + FtbCultivatePositionExamIdentify identify = ftbCultivatePositionExamIdentifyMapper.selectOne(queryWrapper); + if (Objects.nonNull(identify)) { + identify.setIdentifyId(ftbCultivatePositionLearnPracticalDTO.getIdentifyId()); + ftbCultivatePositionExamIdentifyMapper.updateById(identify); + return; + } + FtbCultivatePositionExamIdentify ftbCultivatePositionExamIdentify = ftbCultivatePositionLearnPracticalDTO + .convertFtbCultivatePositionExamIdentify(ftbCultivatePositionLearnPracticalDTO); + ftbCultivatePositionExamIdentifyMapper.insert(ftbCultivatePositionExamIdentify); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void addJobLearningCourses(FtbCultivatePositionLearingCourselDTO ftbCultivatePositionLearnPracticalDTO) { + // 校验是否重复添加 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourse::getPostRankId, ftbCultivatePositionLearnPracticalDTO.getPostRankId()); + queryWrapper.eq(FtbCultivatePositionCourse::getPostLearnId, ftbCultivatePositionLearnPracticalDTO.getPostLearnId()); + queryWrapper.in(FtbCultivatePositionCourse::getCourseId, ftbCultivatePositionLearnPracticalDTO.getCourseIds()); + queryWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + if (ftbCultivatePositionCourseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("存在课程重复,请勿添加重复岗位学习课程"); + } + + List ftbCultivatePositionCourses = ftbCultivatePositionLearnPracticalDTO.getCourseIds() + .stream() + .map(courseId -> { + FtbCultivatePositionCourse ftbCultivatePositionCourse = ftbCultivatePositionLearnPracticalDTO + .convertFtbCultivatePositionCourse(ftbCultivatePositionLearnPracticalDTO); + ftbCultivatePositionCourse.setCourseId(courseId); + return ftbCultivatePositionCourse; + }).collect(Collectors.toList()); + + String sqlStatement = SqlHelper.getSqlStatement(FtbCultivatePositionCourseMapper.class, SqlMethod.INSERT_ONE); + SqlHelper.executeBatch(FtbCultivatePositionCourse.class, + log, + ftbCultivatePositionCourses, + IService.DEFAULT_BATCH_SIZE, + (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); + + // 岗位课程事件 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>( + PositionCourseEventDTO.builder() + .courseIds(ftbCultivatePositionLearnPracticalDTO.getCourseIds()) + .postIds(List.of(ftbCultivatePositionLearnPracticalDTO.getPostRankId())) + .eventType(PositionCourseEventDTO.EventType.POST) + .whetherToChange(false) + .build())); + + } + + @Override + public List jobLearningCourseList(FtbCultivatePositionCoursePageDTO ftbCultivatePositionCoursePageDTO) { + return baseMapper.jobLearningCourseList(ftbCultivatePositionCoursePageDTO); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void studyCourseExamAdded(FtbStudyCourseExamAddDTO ftbStudyCourseExamAddDTO) { + jobLearningExaminationJudgment(ftbStudyCourseExamAddDTO.getPostLearnId(), ftbStudyCourseExamAddDTO.getExamId()); + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbStudyCourseExamAddDTO + .convertFtbCultivatePositionCourseExam(ftbStudyCourseExamAddDTO); + ftbCultivatePositionCourseExamMapper.insert(ftbCultivatePositionCourseExam); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void studyCourseIdentityAdded(FtbStudyCourseIdentityAddDTO ftbStudyCourseIdentityAddDTO) { + onTheJobLearningPracticalAppraisal(ftbStudyCourseIdentityAddDTO.getPostLearnId(), ftbStudyCourseIdentityAddDTO.getIdentityId()); + FtbCultivatePositionCourseIdentity ftbCultivatePositionCourseIdentity = ftbStudyCourseIdentityAddDTO + .convertFtbCultivatePositionCourseIdentity(ftbStudyCourseIdentityAddDTO); + ftbCultivatePositionCourseIdentityMapper.insert(ftbCultivatePositionCourseIdentity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void courseExamDeletion(String courseExamId) { + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectById(courseExamId); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, courseExamId); + updateWrapper.set(FtbCultivatePositionCourseExam::getEnabledMark, 1); + ftbCultivatePositionCourseExamMapper.update(new FtbCultivatePositionCourseExam(), updateWrapper); + // 级联删除 + InnerExamDto innerExamDto = new InnerExamDto(); + innerExamDto.setExamId(ftbCultivatePositionCourseExam.getExamId()); + innerExamDto.setRelationRankId(ftbCultivatePositionCourseExam.getPostRankId()); + innerExamDto.setExamSource(1); + innerExamDto.setRelationId(courseExamId); + ftbCultivateExamUserService.cancelPositionExam(innerExamDto); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void courseIdentityDeletion(String courseIdentityId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, courseIdentityId); + updateWrapper.set(FtbCultivatePositionCourseIdentity::getEnabledMark, 1); + ftbCultivatePositionCourseIdentityMapper.update(new FtbCultivatePositionCourseIdentity(), updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void reselectionAppraisal(FtbReselectionAppraisalDTO ftbReselectionAppraisalDTO) { + FtbCultivatePositionCourseIdentity ftbCultivatePositionCourseIdentity = ftbCultivatePositionCourseIdentityMapper.selectById(ftbReselectionAppraisalDTO.getCourseIdentityId()); + onTheJobLearningPracticalAppraisal(ftbCultivatePositionCourseIdentity.getPostLearnId(), ftbReselectionAppraisalDTO.getIdentityId()); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionCourseIdentity::getId, ftbReselectionAppraisalDTO.getCourseIdentityId()); + updateWrapper.set(FtbCultivatePositionCourseIdentity::getIdentityId, ftbReselectionAppraisalDTO.getIdentityId()); + ftbCultivatePositionCourseIdentityMapper.update(new FtbCultivatePositionCourseIdentity(), updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void retakeExam(FtbRetakeExamDTO ftbRetakeExamDTO) { + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectById(ftbRetakeExamDTO.getCourseExamId()); + jobLearningExaminationJudgment(ftbCultivatePositionCourseExam.getPostLearnId(), ftbRetakeExamDTO.getExamId()); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionCourseExam::getId, ftbRetakeExamDTO.getCourseExamId()); + updateWrapper.set(FtbCultivatePositionCourseExam::getExamId, ftbRetakeExamDTO.getExamId()); + ftbCultivatePositionCourseExamMapper.update(new FtbCultivatePositionCourseExam(), updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deletionOfOnTheJobLearningCourses(String id) { + FtbCultivatePositionCourse ftbCultivatePositionCourse = ftbCultivatePositionCourseMapper.selectById(id); + // 删除岗位学习课程 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivatePositionCourse::getEnabledMark, 1); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + ftbCultivatePositionCourseMapper.update(new FtbCultivatePositionCourse(), updateWrapper); + // 查询绑定的课程考试 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourseExam::getCourseId, ftbCultivatePositionCourse.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostRankId, ftbCultivatePositionCourse.getPostRankId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, ftbCultivatePositionCourse.getPostLearnId()); + queryWrapper.last("limit 1"); + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectOne(queryWrapper); + // 级联删除考试 + LambdaUpdateWrapper ftbCultivatePositionCourseExamLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + ftbCultivatePositionCourseExamLambdaUpdateWrapper.eq(FtbCultivatePositionCourseExam::getCourseId, ftbCultivatePositionCourse.getCourseId()); + ftbCultivatePositionCourseExamLambdaUpdateWrapper.eq(FtbCultivatePositionCourseExam::getPostRankId, ftbCultivatePositionCourse.getPostRankId()); + ftbCultivatePositionCourseExamLambdaUpdateWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, ftbCultivatePositionCourse.getPostLearnId()); + ftbCultivatePositionCourseExamLambdaUpdateWrapper.set(FtbCultivatePositionCourseExam::getEnabledMark, 1); + ftbCultivatePositionCourseExamMapper.update(new FtbCultivatePositionCourseExam(), ftbCultivatePositionCourseExamLambdaUpdateWrapper); + // 级联删除实操鉴定 + LambdaUpdateWrapper ftbCultivatePositionCourseIdentityLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + ftbCultivatePositionCourseIdentityLambdaUpdateWrapper.eq(FtbCultivatePositionCourseIdentity::getCourseId, ftbCultivatePositionCourse.getCourseId()); + ftbCultivatePositionCourseIdentityLambdaUpdateWrapper.eq(FtbCultivatePositionCourseIdentity::getPostRankId, ftbCultivatePositionCourse.getPostRankId()); + ftbCultivatePositionCourseIdentityLambdaUpdateWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, ftbCultivatePositionCourse.getPostLearnId()); + ftbCultivatePositionCourseIdentityLambdaUpdateWrapper.set(FtbCultivatePositionCourseIdentity::getEnabledMark, 1); + ftbCultivatePositionCourseIdentityMapper.update(new FtbCultivatePositionCourseIdentity(), ftbCultivatePositionCourseIdentityLambdaUpdateWrapper); + // 级联删除课程证书 + LambdaUpdateWrapper ftbCultivatePositionCourseCertificateLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + ftbCultivatePositionCourseCertificateLambdaUpdateWrapper.eq(FtbCultivatePositionCourseCertificate::getCourseId, ftbCultivatePositionCourse.getCourseId()); + ftbCultivatePositionCourseCertificateLambdaUpdateWrapper.eq(FtbCultivatePositionCourseCertificate::getPostRankId, ftbCultivatePositionCourse.getPostRankId()); + ftbCultivatePositionCourseCertificateLambdaUpdateWrapper.eq(FtbCultivatePositionCourseCertificate::getPostLearnId, ftbCultivatePositionCourse.getPostLearnId()); + ftbCultivatePositionCourseCertificateLambdaUpdateWrapper.set(FtbCultivatePositionCourseCertificate::getEnabledMark, 1); + ftbCultivatePositionCourseCertificateMapper.update(new FtbCultivatePositionCourseCertificate(), ftbCultivatePositionCourseCertificateLambdaUpdateWrapper); + // 删除课程绑定的考试 + if (Objects.nonNull(ftbCultivatePositionCourseExam)) { + InnerExamDto innerExamDto = new InnerExamDto(); + innerExamDto.setExamId(ftbCultivatePositionCourseExam.getExamId()); + innerExamDto.setRelationRankId(ftbCultivatePositionCourseExam.getPostRankId()); + innerExamDto.setExamSource(1); + innerExamDto.setRelationId(ftbCultivatePositionCourseExam.getId()); + ftbCultivateExamUserService.cancelPositionExam(innerExamDto); + } + // 删除岗位学习证书,判断是否为最后一个岗位学习课程 + if (ftbCultivatePositionCourseMapper.determineTheLastPostStudyCourse(ftbCultivatePositionCourse.getPostLearnId()) + == 0) { + LambdaUpdateWrapper certificateLambdaQueryWrapper = Wrappers.lambdaUpdate(); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCertificate::getPostLearnId, ftbCultivatePositionCourse.getPostLearnId()); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCertificate::getPostRankId, ftbCultivatePositionCourse.getPostRankId()); + certificateLambdaQueryWrapper.set(FtbCultivatePositionCertificate::getEnableMark, 1); + ftbCultivatePositionCertificateMapper.update(new FtbCultivatePositionCertificate(), certificateLambdaQueryWrapper); + } + } + + @Override + public Page jobLevelCourseList(Page page, + FtbCultivatePositionCourseLevelPageDTO ftbCultivatePositionCourseLevelPageDTO) { + Page result = baseMapper.jobLevelCourseList(page, ftbCultivatePositionCourseLevelPageDTO); + result.getRecords().forEach(ftbCultivatePositionLearnLevelVO -> { + // 是否添加了课程考试,0已添加,1未添加 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, ftbCultivatePositionCourseLevelPageDTO.getJobLearningId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostRankId, ftbCultivatePositionCourseLevelPageDTO.getPostRankId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getCourseId, ftbCultivatePositionLearnLevelVO.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + ftbCultivatePositionLearnLevelVO.setIncludesCourseExams(1); + ftbCultivatePositionLearnLevelVO.setContainsAssessment(1); + ftbCultivatePositionLearnLevelVO.setContainsCourseCertificate(1); + if (ftbCultivatePositionCourseExamMapper.selectCount(queryWrapper) > 0) { + ftbCultivatePositionLearnLevelVO.setIncludesCourseExams(0); + } + // 是否添加了实操鉴定,0已添加,1未添加 + LambdaQueryWrapper identityLambdaQueryWrapper = Wrappers.lambdaQuery(); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getCourseId, ftbCultivatePositionLearnLevelVO.getCourseId()); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, ftbCultivatePositionCourseLevelPageDTO.getJobLearningId()); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostRankId, ftbCultivatePositionCourseLevelPageDTO.getPostRankId()); + if (ftbCultivatePositionCourseIdentityMapper.selectCount(identityLambdaQueryWrapper) > 0) { + ftbCultivatePositionLearnLevelVO.setContainsAssessment(0); + } + // 是否添加了课程证书,0已添加,1未添加 + LambdaQueryWrapper certificateLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getEnabledMark, 0); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getCourseId, ftbCultivatePositionLearnLevelVO.getCourseId()); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getPostLearnId, ftbCultivatePositionCourseLevelPageDTO.getJobLearningId()); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getPostRankId, ftbCultivatePositionCourseLevelPageDTO.getPostRankId()); + if (ftbCultivatePositionCourseCertificateMapper.selectCount(certificateLambdaQueryWrapper) > 0) { + ftbCultivatePositionLearnLevelVO.setContainsCourseCertificate(0); + } + }); + return result; + } + + @Override + public FtbPositionCourseDropDownListVO courseDropDownList(FtbCourseDropDownListDTO ftbCourseDropDownListDTO) { + FtbPositionCourseDropDownListVO ftbPositionCourseDropDownListVO = new FtbPositionCourseDropDownListVO(); + ftbPositionCourseDropDownListVO.setCourseId(ftbCourseDropDownListDTO.getCourseId()); + ftbPositionCourseDropDownListVO.setIdentityDelete(1); + ftbPositionCourseDropDownListVO.setExamDelete(1); + ftbPositionCourseDropDownListVO.setCertificateDelete(1); + + // 课程考试 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourseExam::getCourseId, ftbCourseDropDownListDTO.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, ftbCourseDropDownListDTO.getJobLearningId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getPostRankId, ftbCourseDropDownListDTO.getPostRankId()); + queryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectOne(queryWrapper); + if (ftbCultivatePositionCourseExam != null) { + ftbPositionCourseDropDownListVO.setCourseExamId(ftbCultivatePositionCourseExam.getId()); + ftbPositionCourseDropDownListVO.setExamDelete(ftbCultivatePositionCourseExam.getEnabledMark()); + // 考试名 + FtbCultivateExam ftbCultivateExam = getFtbCultivateExamNameById(ftbCultivatePositionCourseExam.getExamId()); + if (ftbCultivateExam != null) { + ftbPositionCourseDropDownListVO.setExamName(ftbCultivateExam.getExamName()); + } + } + + // 课程实操鉴定 + LambdaQueryWrapper identityLambdaQueryWrapper = Wrappers.lambdaQuery(); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getCourseId, ftbCourseDropDownListDTO.getCourseId()); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, ftbCourseDropDownListDTO.getJobLearningId()); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostRankId, ftbCourseDropDownListDTO.getPostRankId()); + identityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + FtbCultivatePositionCourseIdentity ftbCultivatePositionCourseIdentity = ftbCultivatePositionCourseIdentityMapper.selectOne(identityLambdaQueryWrapper); + if (ftbCultivatePositionCourseIdentity != null) { + ftbPositionCourseDropDownListVO.setCourseIdentityId(ftbCultivatePositionCourseIdentity.getId()); + ftbPositionCourseDropDownListVO.setIdentityDelete(ftbCultivatePositionCourseIdentity.getEnabledMark()); + // 实操鉴定名 + CultivateIdentifyTable cultivateIdentifyTable = getCultivateIdentifyNameById(ftbCultivatePositionCourseIdentity.getIdentityId()); + if (cultivateIdentifyTable != null) { + ftbPositionCourseDropDownListVO.setIdentityName(cultivateIdentifyTable.getName()); + } + } + + // 课程证书 + LambdaQueryWrapper certificateLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getCourseId, ftbCourseDropDownListDTO.getCourseId()); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getPostLearnId, ftbCourseDropDownListDTO.getJobLearningId()); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getPostRankId, ftbCourseDropDownListDTO.getPostRankId()); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCourseCertificate::getEnabledMark, 0); + certificateLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourseCertificate ftbCultivatePositionCourseCertificate = ftbCultivatePositionCourseCertificateMapper.selectOne(certificateLambdaQueryWrapper); + if (ftbCultivatePositionCourseCertificate != null) { + ftbPositionCourseDropDownListVO.setCourseCertificateId(ftbCultivatePositionCourseCertificate.getId()); + ftbPositionCourseDropDownListVO.setCertificateDelete(ftbCultivatePositionCourseCertificate.getEnabledMark()); + // 证书名 + FtbCertificateEntity ftbCertificateEntity = getFtbCertificateNameById(ftbCultivatePositionCourseCertificate.getCertificateId()); + if (ftbCertificateEntity != null) { + ftbPositionCourseDropDownListVO.setCertificateName(ftbCertificateEntity.getName()); + } + } + return ftbPositionCourseDropDownListVO; + } + + + @Override + public Page jobLearningPaginatedList(Page page, FtbJobLearningPaginDTO ftbJobLearningPaginDTO) { + Page result = baseMapper.jobLearningPaginatedList(page, ftbJobLearningPaginDTO); + result.getRecords().forEach(ftbJobLearningPaginatedVO -> { + ftbJobLearningPaginatedVO.setNumberOfGrades(0); + ftbJobLearningPaginatedVO.setNumberOfStudyCourses(0L); + ftbJobLearningPaginatedVO.setNumberOfPracticalAppraisals(0L); + ftbJobLearningPaginatedVO.setNumberOfTestPapers(0); + ftbJobLearningPaginatedVO.setNumberOfPromotionChannels(0); + // 岗位名 + PositionVO positionEntity = userApiV2Util.infoPosition(ftbJobLearningPaginatedVO.getPostId(), null); + if(positionEntity!=null) { + ftbJobLearningPaginatedVO.setPostName(positionEntity.getFullName()); + ftbJobLearningPaginatedVO.setPostCustomId(positionEntity.getEnCode());//todo岗位id + } + // 学习课程数 + ftbJobLearningPaginatedVO.setNumberOfStudyCourses(ftbCultivatePositionCourseMapper.numberOfStudyCourses(ftbJobLearningPaginatedVO.getJobLearningId())); + // 地图岗位数 + Integer numberOfMapPositions = ftbCultivatePromotionNewService + .getMapPostNumber(ftbJobLearningPaginatedVO.getPostId()); + ftbJobLearningPaginatedVO.setNumberOfPromotionChannels(numberOfMapPositions); + // 实操鉴定数 + LambdaQueryWrapper identityLambdaQueryWrapper = Wrappers.lambdaQuery(); + identityLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + identityLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getPostLearnId, ftbJobLearningPaginatedVO.getJobLearningId()); + ftbJobLearningPaginatedVO.setNumberOfPracticalAppraisals(ftbCultivatePositionExamIdentifyMapper.selectCount(identityLambdaQueryWrapper)); + // 课程实操鉴定数 + LambdaQueryWrapper courseIdentityLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, ftbJobLearningPaginatedVO.getJobLearningId()); + Long numberOfCourseIdentity = ftbCultivatePositionCourseIdentityMapper.selectCount(courseIdentityLambdaQueryWrapper); + ftbJobLearningPaginatedVO.setNumberOfPracticalAppraisals(ftbJobLearningPaginatedVO.getNumberOfPracticalAppraisals() + numberOfCourseIdentity); + // 试卷数 + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, ftbJobLearningPaginatedVO.getJobLearningId()); + List ftbCultivatePositionExams = ftbCultivatePositionExamMapper.selectList(examLambdaQueryWrapper); + // 课程考试数 + LambdaQueryWrapper positionCourseExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, ftbJobLearningPaginatedVO.getJobLearningId()); + List ftbCultivatePositionCourseExams = ftbCultivatePositionCourseExamMapper.selectList(positionCourseExamLambdaQueryWrapper); + // 考试集合 + List ftbCultivateExamIds = ftbCultivatePositionExams.stream() + .map(FtbCultivatePositionExam::getExamId).collect(Collectors.toList()); + List ftbCultivateExamIdSe = ftbCultivatePositionCourseExams.stream() + .map(FtbCultivatePositionCourseExam::getExamId).collect(Collectors.toList()); + ftbCultivateExamIds.addAll(ftbCultivateExamIdSe); + if (CollUtil.isNotEmpty(ftbCultivateExamIds)) { + Map stringPaperVoMap = ftbCultivateExamService.queryPaperInfoForExamIds(ftbCultivateExamIds); + ftbJobLearningPaginatedVO.setNumberOfTestPapers(stringPaperVoMap.size()); + } + }); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ActionResult deletionOfJobLearning(String postLearnId) { + // 岗位学习表 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, postLearnId); + updateWrapper.set(FtbCultivatePosition::getEnabledMark, 1); + baseMapper.update(new FtbCultivatePosition(), updateWrapper); + // 岗位学习课程表 + LambdaUpdateWrapper a = Wrappers.lambdaUpdate(); + a.set(FtbCultivatePositionCourse::getEnabledMark, 1); + a.eq(FtbCultivatePositionCourse::getPostLearnId, postLearnId); + ftbCultivatePositionCourseMapper.update(new FtbCultivatePositionCourse(), a); + // 岗位实操鉴定表 + LambdaUpdateWrapper b = Wrappers.lambdaUpdate(); + b.set(FtbCultivatePositionExamIdentify::getEnabledMark, 1); + b.eq(FtbCultivatePositionExamIdentify::getPostLearnId, postLearnId); + ftbCultivatePositionExamIdentifyMapper.update(new FtbCultivatePositionExamIdentify(), b); + // 岗位学习考试表 + LambdaUpdateWrapper d = Wrappers.lambdaUpdate(); + d.set(FtbCultivatePositionExam::getEnabledMark, 1); + d.eq(FtbCultivatePositionExam::getPostLearnId, postLearnId); + ftbCultivatePositionExamMapper.update(new FtbCultivatePositionExam(), d); + // 学习课程考试表 + LambdaUpdateWrapper c = Wrappers.lambdaUpdate(); + c.set(FtbCultivatePositionCourseExam::getEnabledMark, 1); + c.eq(FtbCultivatePositionCourseExam::getPostLearnId, postLearnId); + ftbCultivatePositionCourseExamMapper.update(new FtbCultivatePositionCourseExam(), c); + // 学习课程实操鉴定表 + LambdaUpdateWrapper e = Wrappers.lambdaUpdate(); + e.set(FtbCultivatePositionCourseIdentity::getEnabledMark, 1); + e.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, postLearnId); + ftbCultivatePositionCourseIdentityMapper.update(new FtbCultivatePositionCourseIdentity(), e); + // 学习课程证书表 + LambdaUpdateWrapper f = Wrappers.lambdaUpdate(); + f.set(FtbCultivatePositionCourseCertificate::getEnabledMark, 1); + f.eq(FtbCultivatePositionCourseCertificate::getPostLearnId, postLearnId); + ftbCultivatePositionCourseCertificateMapper.update(new FtbCultivatePositionCourseCertificate(), f); + + // 学习证书表 + LambdaUpdateWrapper pc = Wrappers.lambdaUpdate(); + pc.set(FtbCultivatePositionCertificate::getEnableMark, 1); + pc.eq(FtbCultivatePositionCertificate::getPostLearnId, postLearnId); + ftbCultivatePositionCertificateMapper.update(new FtbCultivatePositionCertificate(), pc); + return ActionResult.success(true); + } + + @Override + public FtbJobLearningConfigurationVO overviewOfJobLearningConfiguration(String postLearnId, String postId) { + FtbJobLearningConfigurationVO result = new FtbJobLearningConfigurationVO(); + result.setPostId(postId); + // 岗位名和职等信息 + try { + PositionVO positionVO = userApiV2Util.infoPosition(postId, null); + if (positionVO != null) { + result.setPostName(positionVO.getFullName()); + result.setPostCustomId(positionVO.getEnCode()); + List positionGradesList = userApiV2Util.listGrades(postId, null); + List gradeInformations = positionGradesList.stream().map( + basePositionGradesEntity -> { + FtbJobLearningConfigurationVO.GradeInformation gradeInformation = new FtbJobLearningConfigurationVO.GradeInformation(); + gradeInformation.setFullName(basePositionGradesEntity.getFullName()); + gradeInformation.setGradesLevel(basePositionGradesEntity.getLinkLevel()); + return gradeInformation; + } + ).collect(Collectors.toList()); + result.setGradeDisplay(gradeInformations); + } + + + } catch (Exception ignored) { + } + // 学习地图变更 + FtbCultivatePromotionLevelMapVO learnMapLevels = ftbCultivatePromotionNewService.getLearnMapLevels(postId); + result.setPromotionChannels(learnMapLevels); + result.setNumberOfPromotionChannels(ftbCultivatePromotionNewService.getMapPostNumber(postId)); + // 学习课程 + LambdaQueryWrapper learingCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + learingCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + learingCourseLambdaQueryWrapper.eq(FtbCultivatePositionCourse::getPostLearnId, postLearnId); + List ftbCultivatePositionCourses = ftbCultivatePositionCourseMapper.selectList(learingCourseLambdaQueryWrapper); + List strings = ftbCultivatePositionCourses.stream() + .map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(strings)) { + LambdaQueryWrapper ftbCultivateCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getIsGroundIng, 1); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getLabel, 2); + ftbCultivateCourseLambdaQueryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, strings); + List ftbCultivateCourses = ftbCultivateCourseMapper.selectList(ftbCultivateCourseLambdaQueryWrapper); + List courses = ftbCultivateCourses.stream().map(ftbCultivateCourse -> { + FtbJobLearningConfigurationVO.LearningCourses learningCourses = new FtbJobLearningConfigurationVO.LearningCourses(); + learningCourses.setCourseId(ftbCultivateCourse.getCourseId()); + learningCourses.setName(ftbCultivateCourse.getName()); + learningCourses.setFormat(ftbCultivateCourse.getFormat()); + return learningCourses; + }).collect(Collectors.toList()); + result.setLearningCourses(courses); + result.setNumberOfStudyCourses(courses.size()); + } + // 实操鉴定表 + LambdaQueryWrapper identityLambdaQueryWrapper = Wrappers.lambdaQuery(); + identityLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + identityLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getPostLearnId, postLearnId); + List practicalIdentifications = new ArrayList<>(); + // 岗位学习实操鉴定 + ftbCultivatePositionExamIdentifyMapper.selectList(identityLambdaQueryWrapper) + .forEach(ftbCultivatePositionCourseIdentity -> { + FtbJobLearningConfigurationVO.PracticalIdentification practicalIdentification = new FtbJobLearningConfigurationVO.PracticalIdentification(); + practicalIdentification.setType(0); + practicalIdentification.setId(ftbCultivatePositionCourseIdentity.getIdentifyId()); + + practicalIdentifications.add(practicalIdentification); + }); + // 课程学习实操鉴定 + LambdaQueryWrapper courseIdentityLambdaQueryWrapper = Wrappers.lambdaQuery(); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + courseIdentityLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, postLearnId); + List ftbCultivatePositionCourseIdentities = ftbCultivatePositionCourseIdentityMapper.selectList(courseIdentityLambdaQueryWrapper); + ftbCultivatePositionCourseIdentities.forEach(ftbCultivatePositionCourseIdentity -> { + FtbJobLearningConfigurationVO.PracticalIdentification practicalIdentification = new FtbJobLearningConfigurationVO.PracticalIdentification(); + practicalIdentification.setType(1); + practicalIdentification.setId(ftbCultivatePositionCourseIdentity.getIdentityId()); + practicalIdentifications.add(practicalIdentification); + }); + if (CollUtil.isNotEmpty(practicalIdentifications)) { + List tableListInfo = cultivateIdentifyTableService.getTableListInfo( + practicalIdentifications.stream() + .map(FtbJobLearningConfigurationVO.PracticalIdentification::getId) + .collect(Collectors.toList())); + Map collect = tableListInfo.stream() + .collect(Collectors.toMap(IdentifyTableInfoApiVo::getId, k -> k, (a, b) -> a)); + practicalIdentifications.forEach(practicalIdentification -> { + IdentifyTableInfoApiVo identifyTableInfoApiVo = collect.get(practicalIdentification.getId()); + if (Objects.nonNull(identifyTableInfoApiVo)) { + practicalIdentification.setIdentifyName(identifyTableInfoApiVo.getName()); + practicalIdentification.setIdentifyId(identifyTableInfoApiVo.getRuleId()); + } + }); + result.setPracticalIdentifications(practicalIdentifications); + } + result.setNumberOfPracticalAppraisals(practicalIdentifications.size()); + // 试卷 + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, postLearnId); + // 岗位考试 + List ftbCultivatePositionExams = ftbCultivatePositionExamMapper.selectList(examLambdaQueryWrapper); + List testPapers = new ArrayList<>(); + ftbCultivatePositionExams.forEach(ftbCultivatePositionExam -> { + FtbJobLearningConfigurationVO.TestPaper testPaper = new FtbJobLearningConfigurationVO.TestPaper(); + testPaper.setId(ftbCultivatePositionExam.getExamId()); + testPaper.setType(0); + testPapers.add(testPaper); + }); + // 课程考试 + LambdaQueryWrapper positionCourseExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, postLearnId); + List ftbCultivatePositionCourseExams = ftbCultivatePositionCourseExamMapper.selectList(positionCourseExamLambdaQueryWrapper); + ftbCultivatePositionCourseExams.forEach(ftbCultivatePositionCourseExam -> { + FtbJobLearningConfigurationVO.TestPaper testPaper = new FtbJobLearningConfigurationVO.TestPaper(); + testPaper.setId(ftbCultivatePositionCourseExam.getExamId()); + testPaper.setType(1); + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(ftbCultivatePositionCourseExam.getCourseId()); + if (Objects.nonNull(ftbCultivateCourse)) { + testPaper.setCourseName(ftbCultivateCourse.getName()); + } + testPapers.add(testPaper); + }); + if (CollUtil.isNotEmpty(testPapers)) { + List collected = testPapers.stream().map(FtbJobLearningConfigurationVO.TestPaper::getId).collect(Collectors.toList()); + Map stringPaperVoMap = ftbCultivateExamService.queryPaperInfoForExamIds(collected); + testPapers.forEach(testPaper -> { + PaperVo paperVo = stringPaperVoMap.get(testPaper.getId()); + if (Objects.nonNull(paperVo)) { + testPaper.setPaperName(paperVo.getName()); + testPaper.setPaperId(paperVo.getPaperId()); + } + }); + } + result.setTestPapers(testPapers); + result.setNumberOfTestPapers(testPapers.size()); + return result; + } + + @Override + public FtbJobLearningExamAppraisalVO jobLearningExamAppraisal(String postLearnId, String postIdID) { + FtbJobLearningExamAppraisalVO ftbJobLearningExamAppraisalVO = new FtbJobLearningExamAppraisalVO(); + // 考试 + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.select(FtbCultivatePositionExam::getExamId); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, postLearnId); + examLambdaQueryWrapper.eq(FtbCultivatePositionExam::getPostRankId, postIdID); + examLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionExam ftbCultivatePositionExam = ftbCultivatePositionExamMapper.selectOne(examLambdaQueryWrapper); + if (Objects.nonNull(ftbCultivatePositionExam)) { + ftbJobLearningExamAppraisalVO.setExamId(ftbCultivatePositionExam.getExamId()); + // 考试名 + FtbCultivateExam ftbCultivateExam = getFtbCultivateExamNameById(ftbCultivatePositionExam.getExamId()); + if (ftbCultivateExam != null) { + ftbJobLearningExamAppraisalVO.setExamName(ftbCultivateExam.getExamName()); + ftbJobLearningExamAppraisalVO.setExamCustomId(ftbCultivateExam.getExamId()); + } + } + // 实操鉴定 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePositionExamIdentify::getIdentifyId); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostLearnId, postLearnId); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostRankId, postIdID); + queryWrapper.last("limit 1"); + FtbCultivatePositionExamIdentify ftbCultivatePositionExamIdent = ftbCultivatePositionExamIdentifyMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbCultivatePositionExamIdent)) { + ftbJobLearningExamAppraisalVO.setIdentifyId(ftbCultivatePositionExamIdent.getIdentifyId()); + // 实操鉴定名 + CultivateIdentifyTable cultivateIdentifyTable = getCultivateIdentifyNameById(ftbCultivatePositionExamIdent.getIdentifyId()); + if (cultivateIdentifyTable != null) { + ftbJobLearningExamAppraisalVO.setPracticalIdentificationName(cultivateIdentifyTable.getName()); + ftbJobLearningExamAppraisalVO.setIdentifyCustomId(cultivateIdentifyTable.getRuleId()); + } + } + // 证书 + LambdaQueryWrapper certificateLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateLambdaQueryWrapper.select(FtbCultivatePositionCertificate::getCertificateId); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCertificate::getEnableMark, 0); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCertificate::getPostLearnId, postLearnId); + certificateLambdaQueryWrapper.eq(FtbCultivatePositionCertificate::getPostRankId, postIdID); + certificateLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCertificate ftbCultivatePositionCertificate = ftbCultivatePositionCertificateMapper.selectOne(certificateLambdaQueryWrapper); + if (Objects.nonNull(ftbCultivatePositionCertificate)) { + ftbJobLearningExamAppraisalVO.setCertificateId(ftbCultivatePositionCertificate.getCertificateId()); + // 证书名 + FtbCertificateEntity ftbCertificateEntity = getFtbCertificateNameById(ftbCultivatePositionCertificate.getCertificateId()); + if (ftbCertificateEntity != null) { + ftbJobLearningExamAppraisalVO.setCertificateName(ftbCertificateEntity.getName()); + } + } + // 岗位考试合格后才能进行鉴定,0否1是 + FtbCultivatePosition ftbCultivatePosition = this.getById(postLearnId); + ftbJobLearningExamAppraisalVO.setQualified(ftbCultivatePosition.getQualified()); + return ftbJobLearningExamAppraisalVO; + } + + @Override + @Transactional + public void postStudyExamDeleted(String postLearnId, String postRankId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, postLearnId); + queryWrapper.eq(FtbCultivatePositionExam::getPostRankId, postRankId); + queryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + queryWrapper.last("limit 1"); + FtbCultivatePositionExam ftbCultivatePositionCourseExam = ftbCultivatePositionExamMapper.selectOne(queryWrapper); + // 删除岗位学习考试 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionExam::getPostLearnId, postLearnId); + updateWrapper.eq(FtbCultivatePositionExam::getPostRankId, postRankId); + updateWrapper.set(FtbCultivatePositionExam::getEnabledMark, 1); + ftbCultivatePositionExamMapper.update(new FtbCultivatePositionExam(), updateWrapper); + // 级联删除 +// InnerExamDto innerExamDto = new InnerExamDto(); +// innerExamDto.setExamId(ftbCultivatePositionCourseExam.getExamId()); +// innerExamDto.setRelationRankId(ftbCultivatePositionCourseExam.getPostRankId()); +// innerExamDto.setExamSource(2); +// innerExamDto.setRelationId(ftbCultivatePositionCourseExam.getId()); +// ftbCultivateExamUserService.cancelPositionExam(innerExamDto); + // 提示语回调 + ftbCultivateExamUserService.positionConfigChangeCallBack(postRankId, ftbCultivatePositionCourseExam.getExamId(), 0); + eligibilityStatusChange(postRankId); + } + + @Override + @Transactional + public void jobLearningPracticalAppraisalDelete(String postLearnId, String postRankId) { + LambdaUpdateWrapper queryWrapper = Wrappers.lambdaUpdate(); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostLearnId, postLearnId); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostRankId, postRankId); + queryWrapper.set(FtbCultivatePositionExamIdentify::getEnabledMark, 1); + ftbCultivatePositionExamIdentifyMapper.update(new FtbCultivatePositionExamIdentify(), queryWrapper); + eligibilityStatusChange(postRankId); + } + + @Override + @Transactional + public void deleteMemberGrade(String id, String userId) { + CultivateMessageInfo cultivateMessageInfo = new CultivateMessageInfo(); + cultivateMessageInfo.setSource(1); + cultivateMessageInfo.setState(0); + cultivateMessageInfo.setUserId(userId); + cultivateMessageInfo.setBusinessId(id); + ftbCultivateMessageInfoMapper.insert(cultivateMessageInfo); + } + + @Override + @Transactional + public void certificateDeletion(String postLearnId, String postRankId) { + LambdaUpdateWrapper deleteWrapper = Wrappers.lambdaUpdate(); + deleteWrapper.eq(FtbCultivatePositionCertificate::getPostLearnId, postLearnId); + deleteWrapper.eq(FtbCultivatePositionCertificate::getPostRankId, postRankId); + deleteWrapper.set(FtbCultivatePositionCertificate::getEnableMark, 1); + ftbCultivatePositionCertificateMapper.update(new FtbCultivatePositionCertificate(), deleteWrapper); + } + + @Override + @Transactional + public void onTheJobLearningCertificate(FtbCultivatePositionJobLearnCertificateDTO certificateDTO) { + FtbCultivatePositionCertificate ftbCultivatePositionCertificate = certificateDTO.convertFtbCultivatePositionCertificate(certificateDTO); + ftbCultivatePositionCertificateMapper.insert(ftbCultivatePositionCertificate); + } + + @Override + @Transactional + public void addJobLearningCourseCertificate(FtbCultivatePositionJobLearnCourseCertificateDTO certificateDTO) { + FtbCultivatePositionCourseCertificate ftbCultivatePositionCourseCertificate = certificateDTO.convert(certificateDTO); + ftbCultivatePositionCourseCertificateMapper.insert(ftbCultivatePositionCourseCertificate); + } + + @Override + @Transactional + public void certificateCourseDeletion(String postLearnId, String courseId) { + LambdaUpdateWrapper deleteWrapper = Wrappers.lambdaUpdate(); + deleteWrapper.eq(FtbCultivatePositionCourseCertificate::getPostLearnId, postLearnId); + deleteWrapper.eq(FtbCultivatePositionCourseCertificate::getCourseId, courseId); + deleteWrapper.set(FtbCultivatePositionCourseCertificate::getEnabledMark, 1); + ftbCultivatePositionCourseCertificateMapper.update(new FtbCultivatePositionCourseCertificate(), deleteWrapper); + } + + @Override + @Transactional + public void changeCourseElectiveAndRequiredSwitching(FtbCultivatePositionJobLearnCourseStateSwitchDTO stateSwitchDTO) { + // 至少有一门必修课程 + if (stateSwitchDTO.getCompulsory() == 1) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionCourse::getPostLearnId, stateSwitchDTO.getPostLearnId()); + queryWrapper.eq(FtbCultivatePositionCourse::getPostRankId, stateSwitchDTO.getPostRankId()); + queryWrapper.eq(FtbCultivatePositionCourse::getCompulsory, 0); + queryWrapper.ne(FtbCultivatePositionCourse::getCourseId, stateSwitchDTO.getCourseId()); + List ftbCultivatePositionCourses = ftbCultivatePositionCourseMapper.selectList(queryWrapper); + if (CollectionUtil.isEmpty(ftbCultivatePositionCourses)) { + throw new RuntimeException("至少有一门必修课程"); + } + List courseIds = ftbCultivatePositionCourses.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList()); + QueryWrapper courseQueryWrapper = new QueryWrapper<>(); + courseQueryWrapper.lambda() + .in(FtbCultivateCourse::getId, courseIds); + List courseList = ftbCultivateCourseMapper.selectList(courseQueryWrapper); + List effectiveCourseList = courseList.stream().filter(course -> { + return course.getIsGroundIng() == 1 && course.getLabel() == 2; + }).collect(Collectors.toList()); + + if (CollectionUtil.isEmpty(effectiveCourseList)) { + throw new RuntimeException("至少有一门必修课程"); + } + + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionCourse::getPostLearnId, stateSwitchDTO.getPostLearnId()); + updateWrapper.eq(FtbCultivatePositionCourse::getCourseId, stateSwitchDTO.getCourseId()); + updateWrapper.eq(FtbCultivatePositionCourse::getPostRankId, stateSwitchDTO.getPostRankId()); + updateWrapper.set(FtbCultivatePositionCourse::getCompulsory, stateSwitchDTO.getCompulsory()); + ftbCultivatePositionCourseMapper.update(new FtbCultivatePositionCourse(), updateWrapper); + } + + @Override + @Transactional + public void courseRetakeCertificate(FtbJobLearnCourseRetakeCertificateDTO certificateDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePositionCourseCertificate::getId, certificateDTO.getCourseCertificateId()); + updateWrapper.eq(FtbCultivatePositionCourseCertificate::getPostRankId, certificateDTO.getPostId()); + updateWrapper.set(FtbCultivatePositionCourseCertificate::getCertificateId, certificateDTO.getCertificateId()); + ftbCultivatePositionCourseCertificateMapper.update(new FtbCultivatePositionCourseCertificate(), updateWrapper); + } + + /** + * 岗位学习考试判断 + * + * @param postLearnId 岗位id + * @param examId 考试id + */ + private void jobLearningExaminationJudgment(String postLearnId, String examId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionExam::getPostLearnId, postLearnId); + queryWrapper.eq(FtbCultivatePositionExam::getExamId, examId); + if (ftbCultivatePositionExamMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("此岗位已经绑定了该考试了"); + } + LambdaQueryWrapper positionCourseExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, postLearnId); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseExam::getExamId, examId); + positionCourseExamLambdaQueryWrapper.last("limit 1"); + FtbCultivatePositionCourseExam ftbCultivatePositionCourseExam = ftbCultivatePositionCourseExamMapper.selectOne(positionCourseExamLambdaQueryWrapper); + if (Objects.nonNull(ftbCultivatePositionCourseExam)) { + // 校验是否下架 + LambdaQueryWrapper ftbCultivateCourseLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getId, ftbCultivatePositionCourseExam.getCourseId()); + ftbCultivateCourseLambdaQueryWrapper.eq(FtbCultivateCourse::getIsGroundIng, 0); + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectOne(ftbCultivateCourseLambdaQueryWrapper); + if (Objects.nonNull(ftbCultivateCourse)) { + throw new RuntimeException("该考试绑定的课程" + ftbCultivateCourse.getName() + "已下架,重新上架后可启用考试"); + } + throw new RuntimeException("此岗位已经绑定了该考试了,不可重复绑定"); + + } + } + + /** + * 岗位学习实操鉴定判断 + * + * @param postLearnId 岗位学习 ID + * @param identificationId 考试id + */ + private void onTheJobLearningPracticalAppraisal(String postLearnId, String identificationId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getPostLearnId, postLearnId); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getIdentifyId, identificationId); + if (ftbCultivatePositionExamIdentifyMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("此岗位已经绑定了该实操鉴定了"); + } + LambdaQueryWrapper positionCourseExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, postLearnId); + positionCourseExamLambdaQueryWrapper.eq(FtbCultivatePositionCourseIdentity::getIdentityId, identificationId); + if (ftbCultivatePositionCourseIdentityMapper.selectCount(positionCourseExamLambdaQueryWrapper) > 0) { + throw new RuntimeException("此岗位已经绑定了该实操鉴定了"); + } + } + + /** + * 更新合格状态 + * + * @param postId + */ + private void eligibilityStatusChange(String postId) { + LambdaUpdateWrapper updateQueryWrapper = Wrappers.lambdaUpdate(); + updateQueryWrapper.eq(FtbCultivatePosition::getPostId, postId); + updateQueryWrapper.set(FtbCultivatePosition::getQualified, 0); + this.update(new FtbCultivatePosition(), updateQueryWrapper); + } + + /** + * 根据证书id获取证书名称 + * + * @param certificateId 证书id + */ + private FtbCertificateEntity getFtbCertificateNameById(String certificateId) { + LambdaQueryWrapper certificateEntityLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateEntityLambdaQueryWrapper.select(FtbCertificateEntity::getName); + certificateEntityLambdaQueryWrapper.eq(BaseEntity::getId, certificateId); + return ftbCultivateCertificateMapper.selectOne(certificateEntityLambdaQueryWrapper); + } + + /** + * 根据考试id获取考试名称 + * + * @param examId 考试id + */ + private FtbCultivateExam getFtbCultivateExamNameById(String examId) { + LambdaQueryWrapper examLambdaQueryWrapper = Wrappers.lambdaQuery(); + examLambdaQueryWrapper.select(FtbCultivateExam::getExamName); + examLambdaQueryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, examId); + return ftbCultivateExamMapper.selectOne(examLambdaQueryWrapper); + } + + /** + * 根据实操鉴定id获取实操鉴定名称 + * + * @param identityId 实操鉴定id + */ + private CultivateIdentifyTable getCultivateIdentifyNameById(String identityId) { + LambdaQueryWrapper identifyTableLambdaQueryWrapper = Wrappers.lambdaQuery(); + identifyTableLambdaQueryWrapper.select(CultivateIdentifyTable::getName); + identifyTableLambdaQueryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, identityId); + return cultivateIdentifyTableMapper.selectOne(identifyTableLambdaQueryWrapper); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionStatisticesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionStatisticesServiceImpl.java new file mode 100644 index 0000000..b077413 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionStatisticesServiceImpl.java @@ -0,0 +1,1074 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.FtbCultivatePositionStatisticesMapper; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivatePositionStatisticesService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivateBatchQueryService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbCultivatePersonStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionOrgStatisticesDTO; +import jnpf.model.cultivate.dto.position.FtbCultivatePositionPersonStatisticesDTO; +import jnpf.model.cultivate.resp.ExamCultivateForPositionVo; +import jnpf.model.cultivate.v2.position.vo.CultivatePositionCourseVo; +import jnpf.model.cultivate.v2.position.vo.CultivateUserPositionVo; +import jnpf.model.cultivate.v2.position.vo.PersonForLeaderVo; +import jnpf.model.cultivate.v2.position.vo.V2StudyCountVo; +import jnpf.model.cultivate.v2.statistics.V2UserCourseStudyVo; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.course.web.UserCourseStudyVo; +import jnpf.model.cultivate.vo.position.*; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeFullBasicVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.permission.vo.v2.user.UserRelationBaseVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.Constants; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class FtbCultivatePositionStatisticesServiceImpl implements FtbCultivatePositionStatisticesService { + + @Resource + private FtbCultivatePositionStatisticesMapper ftbCultivatePositionStatisticesMapper; + + @Resource + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Autowired + private UserApiV2Util userApiV2Util; + + + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Autowired + private CultivatePerUtils cultivatePerUtils; + + @Resource + private FtbCultivatePromotionNewService ftbCultivatePromotionNewService; + + @Resource(name = "threadPoolTaskExecutor") + ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Resource + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbCultivatePositionOrgStatisticesDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO organizationListStatistics(FtbCultivatePositionOrgStatisticesDTO statisticDTO) { + Page page = statisticDTO.coverCultivatePage(); + // 多选组织情况 根据每一个组织维度统计 + List orgIds = new ArrayList<>(); + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return returnEmptyList(statisticDTO); + } + // 勾选 + if ("1".equals(statisticDTO.getSelectLogo()) && CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List allOrg = new ArrayList<>(statisticDTO.getOrgId()); + List allOrganize = cultivatePerUtils.getAllOrganizationInformation(statisticDTO.getOrgId()); + if (CollUtil.isNotEmpty(allOrganize)) { + allOrg.addAll(allOrganize); + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, allOrg); + if (CollUtil.isNotEmpty(intersection)) { + orgIds = (intersection); + } else { + return returnEmptyList(statisticDTO); + } + } else if (CollUtil.isNotEmpty(statisticDTO.getOrgId())) { + List intersection = UserApiV2Util.getIntersection(powerOrgList, statisticDTO.getOrgId()); + if (CollUtil.isNotEmpty(intersection)) { + orgIds = intersection; + } else { + return returnEmptyList(statisticDTO); + } + } else { + orgIds = powerOrgList; + } + if (StrUtil.isNotEmpty(statisticDTO.getOrgIds())) { + List organizeGeneralDetailVOS = userApiV2Util.organizesOrHaveChildByOrganizeEncodes(Arrays.asList(statisticDTO.getOrgIds().split(",")), null); + if (CollUtil.isEmpty(organizeGeneralDetailVOS)) { + return returnEmptyList(statisticDTO); + } + List intersection = UserApiV2Util.getIntersection(orgIds, organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(statisticDTO); + } + orgIds = intersection; + } + // 默认情况 + String orgIdBySpecial = statisticDTO.getOrgIdBySpecial(); + if (StringUtils.isNotEmpty(orgIdBySpecial) && !orgIds.contains(orgIdBySpecial)) { + orgIds.add(orgIdBySpecial); + } + + orgIds = UserApiV2Util.uniqueStringList(orgIds); +// List organizeEntityList = organizeApi.getOrganizeByIds(orgIds); + List organizeEntityList = userApiV2Util.organizesByOrganizeIds(orgIds, null); + orgIds = new ArrayList<>(); + for (OrganizeGeneralDetailVO organizeGeneralDetailVO : organizeEntityList) { + if (!organizeGeneralDetailVO.getOrganizeCategoryEnums().equals(OrganizeCategoryEnums.TEAM)) { + orgIds.add(organizeGeneralDetailVO.getId()); + } + } + + Map orgMap = organizeEntityList.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, v -> v)); +// Map orgIdMap = cultivatePerUtils.convertOrganizationalId(orgIds); + // 批量统计对应查询组织下人数 +// Map listFeignMap = organizeApi.batchOrganizeUsersCount(orgIds); + List userList = userApiV2Util.getUserListForOrgIds(orgIds, null); + Map listFeignMap = userApiV2Util.convertUserCount(userList); + // 根据组织查询岗位 +// Map> batchListByOrganizeIds = positionApi.batchListByOrganizeIds(orgIds); + Map> batchListByOrganizeIds = userApiV2Util.listPositionBaseInfoByIdsReturnMapSimple(orgIds, null); + + + // 根据组织查询组织下的人 +// Map> orgUserRelationMap = userApi.batchGetAllUser(orgIds); + Map> orgUserRelationMap = userApiV2Util.convertUserIdList(userList); + UserInfo user = UserProvider.getUser(); + String tenantId = user.getTenantId(); + // 分块处理数据 + List> partition = new ArrayList<>(); + if (orgIds.size() > 1000) { + partition = Lists.partition(orgIds, 10); + } else if (500 < orgIds.size() && orgIds.size() < 1000) { + partition = Lists.partition(orgIds, 5); + } else { + partition = Lists.partition(orgIds, 1); + } + List>> futures = new ArrayList<>(); + // 多线程分块处理 + // 课程总数 + Integer totalNumberOfCourses = ftbCultivatePositionStatisticesMapper.totalNumberOfCourses(); + for (List listOrg : partition) { + futures.add(CompletableFuture.supplyAsync(() -> { + return listOrg.parallelStream().map(orgId -> { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + FtbCultivatePositionOrgStatisticesVO freightCultivate = new FtbCultivatePositionOrgStatisticesVO(); + if (listFeignMap.containsKey(orgId)) { + Long data = listFeignMap.get(orgId); + // 组织人数 + freightCultivate.setNumberOfPeopleInTheOrganization(data); + } + // 组织名称和组织id + freightCultivate.setOrgId(orgId); + OrganizeGeneralDetailVO organizeGeneralDetailVO = orgMap.get(orgId); + if (organizeGeneralDetailVO != null) { + freightCultivate.setOrganizationCustomId(organizeGeneralDetailVO.getEnCode()); + freightCultivate.setNameOfAssociation(organizeGeneralDetailVO.getName()); + } + List userIds = null; + if (orgUserRelationMap.containsKey(orgId)) { + userIds = orgUserRelationMap.get(orgId); + } + if (CollUtil.isNotEmpty(userIds)) { + String postionIds = "-1"; + if (batchListByOrganizeIds.containsKey(orgId)) + postionIds = batchListByOrganizeIds.get(orgId).stream() + .collect(Collectors.joining(",")); + // 参与学习人数 + List numberOfPeopleParticipatingInTheStudy = ftbCultivatePositionStatisticesMapper.numberOfPeopleParticipatingInTheStudyOrg(statisticDTO, userIds, postionIds); + freightCultivate.setNumberOfPeopleParticipatingInTheStudy(numberOfPeopleParticipatingInTheStudy.size()); + freightCultivate.setParticipantsInTheStudy(numberOfPeopleParticipatingInTheStudy); + // 学习总时长(h) + Integer totalStudyTime = ftbCultivatePositionStatisticesMapper.totalStudyTime(statisticDTO, userIds, postionIds); + freightCultivate.setTotalStudyTime(CultivatePerUtils.computeDivision(new BigDecimal(totalStudyTime))); + // 人均学习时长(h) + freightCultivate.setAverageStudyTimePerPerson(computeDivision(freightCultivate.getTotalStudyTime(), freightCultivate.getNumberOfPeopleParticipatingInTheStudy())); + freightCultivate.setTotalNumberOfCourses(totalNumberOfCourses); + // 已学课程数 + Integer numberOfCoursesTaken = ftbCultivatePositionStatisticesMapper.numberOfCoursesTakens(statisticDTO, userIds, postionIds); + freightCultivate.setNumberOfCoursesTaken(numberOfCoursesTaken); + } + return freightCultivate; + }).collect(Collectors.toList()); + }, threadPoolTaskExecutor)); + } + // 使用CompletableFuture.allOf等待所有任务完成 + CompletableFuture allTasks = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + // 等待所有任务完成 + allTasks.join(); + // 收集所有任务的结果 + List results = futures.stream() + .map(CompletableFuture::join) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + // 线程等待 + return CultivatePage.paginate(results, page); + } + + @Override + public PageListVO personListStatistics(FtbCultivatePositionPersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO) { + Page page = ftbCultivatePositionPersonStatisticesDTO.coverCultivatePage(); + + // 关键词搜索 + if (StringUtils.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getKeyWords())) { + boolean keywordSearch = false; + // 员工姓名和员工ID + List> userIds = ftbCultivatePositionStatisticesMapper.fuzzySearchForRosterKeywords(ftbCultivatePositionPersonStatisticesDTO.getKeyWords()); + if (CollUtil.isNotEmpty(userIds)) { + + ftbCultivatePositionPersonStatisticesDTO.getUserIds().addAll(userIds.stream() + .map(a -> a.get("userId")).collect(Collectors.toList())); + keywordSearch = true; + } + // 组织ID + List organizationalIdCollection = cultivatePerUtils.convertOrganizationalIdCollection(ftbCultivatePositionPersonStatisticesDTO.getKeyWords()); + if (CollUtil.isNotEmpty(organizationalIdCollection)) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(organizationalIdCollection, null); + if (CollUtil.isNotEmpty(userListForOrgIds)) { + List userIdSe = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + ftbCultivatePositionPersonStatisticesDTO.getUserIds().addAll(userIdSe); + keywordSearch = true; + } + } + // 岗位名称 +// List userByPositionName = userApi.getUserByPositionName(ftbCultivatePositionPersonStatisticesDTO.getKeyWords()); + List userByPositionName = userApiV2Util.getUserListForPositionName(ftbCultivatePositionPersonStatisticesDTO.getKeyWords(), null); + if (CollUtil.isNotEmpty(userByPositionName)) { + List datas = userByPositionName.stream().map(UserRelationBaseVO::getUserId).collect(Collectors.toList()); + ftbCultivatePositionPersonStatisticesDTO.getUserIds().addAll(datas); + keywordSearch = true; + } + if (!keywordSearch) {//说明keyword搜索条件无数据 + return CultivatePage.coverPageList(page); + } + } + // 选择组织搜索 + if (CollUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getOrgId())) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(ftbCultivatePositionPersonStatisticesDTO.getOrgId(), null); + if (CollUtil.isNotEmpty(userListForOrgIds)) { + List userIdSe = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getUserIds())) { + ftbCultivatePositionPersonStatisticesDTO.getUserIds().retainAll(userIdSe); + } else { + ftbCultivatePositionPersonStatisticesDTO.getUserIds().addAll(userIdSe); + } + } else { + return CultivatePage.coverPageList(page); + } + } + if (CollUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getSelectPeoples())) { + if (CollUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getUserIds())) { + ftbCultivatePositionPersonStatisticesDTO.getUserIds().retainAll(ftbCultivatePositionPersonStatisticesDTO.getSelectPeoples()); + } else { + ftbCultivatePositionPersonStatisticesDTO.getUserIds().addAll(ftbCultivatePositionPersonStatisticesDTO.getSelectPeoples()); + } + } + + List courseIdList = ftbCultivatePositionStatisticesMapper.queryCourseIds(); + if (CollectionUtil.isEmpty(courseIdList)) { + return CultivatePage.coverPageList(page); + } + List courseIds = courseIdList.stream().map(CultivatePositionCourseVo::getCourseId).collect(Collectors.toList()); +// courseIdList 按照岗位分组 + Map> postCourseMap = new HashMap<>(); + for (CultivatePositionCourseVo cultivatePositionCourseVo : courseIdList) { + if (StrUtil.isEmpty(cultivatePositionCourseVo.getGradeId())) { + postCourseMap.computeIfAbsent(cultivatePositionCourseVo.getPostId(), k -> new ArrayList<>()).add(cultivatePositionCourseVo); + } else { + postCourseMap.computeIfAbsent(cultivatePositionCourseVo.getPostId() + "_" + cultivatePositionCourseVo.getGradeId(), k -> new ArrayList<>()).add(cultivatePositionCourseVo); + } + } + ftbCultivatePositionPersonStatisticesDTO.setInnerCourseIds(courseIds); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getUserIds())) { + List intersection = UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), ftbCultivatePositionPersonStatisticesDTO.getUserIds()); + if (CollUtil.isNotEmpty(intersection)) { + ftbCultivatePositionPersonStatisticesDTO.setUserIds(intersection); + } else { + return CultivatePage.coverPageList(page); + } + } else { + ftbCultivatePositionPersonStatisticesDTO.setUserIds(innerPowerUserVO.getUserIds()); + } + } else if (innerPowerUserVO.getCode() == 2) { + return CultivatePage.coverPageList(page); + } + + page = ftbCultivatePositionStatisticesMapper.personListStatistics(page, ftbCultivatePositionPersonStatisticesDTO); + if (CollectionUtils.isNotEmpty(page.getRecords())) { + List userIds = page.getRecords().stream().map(FtbCultivatePositionPersonStatisticesVO::getUserId).collect(Collectors.toList()); + List records = page.getRecords(); + Map userToOrgMap = userApiV2Util.getUserPrimaryBoundBatchReturnMap(userIds, null); + Map> promotionMap = batchQueryPositionToLearnPositionIdsV2(userToOrgMap); //用户-到岗位列表 + List userCourseStudyVos = batchQueryAllCourseStudyV2(userIds); + //按照用户id分组 + Map> userCourseStudyMap = userCourseStudyVos.stream().collect(Collectors.groupingBy(V2UserCourseStudyVo::getUserId)); + + Map> leaderMap = batchQueryUserInfoByLeaderIdsV2(userIds); + List> allList = batchRecords(records, 100); + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + //多线程调用 convertToFtbRosterImportTemplateBatch 这个方法 + List>> futures = allList.stream() + .map(item -> CompletableFuture.supplyAsync(() -> { + try { + FeignHolder.set(headers); + batchFillStatisticsV2(tenantId, postCourseMap, item, userToOrgMap, promotionMap, userCourseStudyMap, leaderMap); + return List.of("成功"); + } finally { + FeignHolder.clear(); + } + }, threadPoolTaskExecutor)) + .collect(Collectors.toList()); + //搜集结果并返回 + List collect = futures.stream() + .map(CompletableFuture::join) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + log.error("成功"); + } + return CultivatePage.coverPageList(page); + } + + private List batchQueryAllCourseStudyV2(List userIds) { + List v2UserCourseStudyVos = ftbCultivatePositionStatisticesMapper.batchQueryAllCourseStudyV2(userIds); + if (CollUtil.isEmpty(v2UserCourseStudyVos)) { + return new ArrayList<>(); + } + return v2UserCourseStudyVos; + } + + private List batchQueryAllCourseStudy(List userIds) { + List v2UserCourseStudyVos = ftbCultivatePositionStatisticesMapper.batchQueryAllCourseStudy(userIds); + if (CollUtil.isEmpty(v2UserCourseStudyVos)) { + return new ArrayList<>(); + } + return v2UserCourseStudyVos; + } + + public List> batchRecords(List records, int batchSize) { + List> batches = new ArrayList<>(); + int totalBatches = (int) Math.ceil((double) records.size() / batchSize); + + for (int i = 0; i < totalBatches; i++) { + int start = i * batchSize; + int end = Math.min(start + batchSize, records.size()); + List batch = records.subList(start, end); + batches.add(batch); + } + + return batches; + } + + + private void batchFillStatisticsV2(String tenantCode, + Map> postCourseMap, + List records, + Map userToOrgMap, + Map> postionToLearnPostionIdsMap, + Map> userCourseStudyMap, + Map> leaderMap) { + try { + TenantDataSourceUtil.switchTenant(tenantCode); + } catch (LoginException e) { + throw new RuntimeException(e); + } + records.forEach(item -> { + // 所属组织 + UserBoundVO userBoundVO = userToOrgMap.get(item.getUserId()); + int localPositionStudyCourses = 0;//已经完成学习的本岗课程数量 + int localPositionStudyCoursesNeed = 0;//已经完成学习的本岗课程数量(必修) + //本岗 + List localPostionCourseVos = new ArrayList<>();//本岗所有 + List localPostionCourseVosNees = new ArrayList<>();//本岗所有(必须) + // 组织ID + if (userBoundVO != null) { + item.setOrganization(List.of(userBoundVO.getOrganizeName())); + if (StringUtils.isNotEmpty(userBoundVO.getOrganizeEnCode())) { + item.setOrganizationCustomId(List.of(userBoundVO.getOrganizeEnCode())); + } + item.setEmployeeName(userBoundVO.getUserName()); + item.setEmployeeCustomId(userBoundVO.getSystemWorkerId()); + List positionInfos = new ArrayList<>(); + if (StrUtil.isNotEmpty(userBoundVO.getPositionId())) { + FtbCultivatePositionPersonStatisticesVO.PositionInfo positionInfo = new FtbCultivatePositionPersonStatisticesVO.PositionInfo(); + positionInfo.setPositionName(userBoundVO.getPositionName()); + positionInfo.setPositionGradesName(userBoundVO.getGradeName()); + positionInfos.add(positionInfo); + } + item.setThisPosition(positionInfos); + // 学习-岗位 + List learnPostionIds = postionToLearnPostionIdsMap.getOrDefault(userBoundVO.getId(),new ArrayList<>()); + List v2UserCourseStudyVos = userCourseStudyMap.getOrDefault(item.getUserId(),new ArrayList<>()); + Map v2UserCourseStudyMap = v2UserCourseStudyVos.stream() + .collect(Collectors.toMap(V2UserCourseStudyVo::getCourseId, a -> a)); + if (CollUtil.isNotEmpty(learnPostionIds)) { + Integer numberOfStudyCourses = 0; + for (CultivateUserPositionVo tempVo : learnPostionIds) { + String learnPostionId = tempVo.getPostId(); + if (StrUtil.isNotEmpty(tempVo.getGradeId())) { + learnPostionId = learnPostionId + "_" + tempVo.getGradeId(); + } + List cultivatePositionCourseVos = postCourseMap.get(learnPostionId); + if (CollUtil.isNotEmpty(cultivatePositionCourseVos)) { + numberOfStudyCourses += cultivatePositionCourseVos.size(); + } + } + // 学习地图课程总数 + item.setTotalNumberOfLearningMapCourses(numberOfStudyCourses); + } + String localPostionKey = userBoundVO.getPositionId(); + if (postCourseMap.containsKey(localPostionKey)) { + localPostionCourseVos = postCourseMap.get(localPostionKey); + } else { + if (StrUtil.isNotEmpty(userBoundVO.getGradeId())) { + localPostionKey = localPostionKey + "_" + userBoundVO.getGradeId(); + } + localPostionCourseVos = postCourseMap.get(localPostionKey); + } + if (CollUtil.isNotEmpty(localPostionCourseVos)) { + for (CultivatePositionCourseVo localPostionCourseVo : localPostionCourseVos) { + if (localPostionCourseVo.getCompulsory().equals(0)) { + localPostionCourseVosNees.add(localPostionCourseVo); + } + V2UserCourseStudyVo v2UserCourseStudyVo = v2UserCourseStudyMap.get(localPostionCourseVo.getCourseId()); + if (v2UserCourseStudyVo != null) { + if (!v2UserCourseStudyVo.getState().equals(0)) { + localPositionStudyCourses++; + if (localPostionCourseVo.getCompulsory().equals(0) && v2UserCourseStudyVo.getState() == 1) { + localPositionStudyCoursesNeed++; + } + } + } + } + } + item.setNumberOfStudyCourses(localPositionStudyCourses); + } + // 学习时长(h) + item.setStudyDuration(CultivatePerUtils.computeDivision(item.getStudyDuration())); + + item.setCourseCompletionRate(computeDivision(localPositionStudyCoursesNeed, localPostionCourseVosNees.size())); + // 下属人数 + List subordinateUserId = leaderMap.get(item.getUserId()); + // 下属参与学习人数 + if (CollectionUtils.isNotEmpty(subordinateUserId)) { + item.setNumberOfSubordinates(subordinateUserId.size()); + List tempCourseIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(localPostionCourseVos)) { + tempCourseIds.addAll(localPostionCourseVos.stream().map(CultivatePositionCourseVo::getCourseId).collect(Collectors.toList())); + } else { + tempCourseIds.add("-1"); + } + V2StudyCountVo v2StudyCountVo = ftbCultivatePositionStatisticesMapper.numberOfPeopleParticipatingInTheStudy(subordinateUserId, tempCourseIds); +// Integer numberOfPeopleParticipatingInTheStudy = ftbCultivatePositionStatisticesMapper.numberOfPeopleParticipatingInTheStudy1(subordinateUserId, innerCourseIds); + item.setNumberOfSubordinatesParticipatingInLearning(v2StudyCountVo.getUserNumber()); + // 下属人均学习时长 +// Integer averageStudyHoursPerSubordinate = ftbCultivatePositionStatisticesMapper.averageStudyHoursPerSubordinate1(subordinateUserId, innerCourseIds); + item.setAverageStudyHoursPerSubordinate(computeDivision(CultivatePerUtils.computeDivision(new BigDecimal(v2StudyCountVo.getTotalSeconds())) + , subordinateUserId.size())); + } else { + item.setNumberOfSubordinatesParticipatingInLearning(0); + item.setAverageStudyHoursPerSubordinate(BigDecimal.ZERO); + } + + }); + } + + private Map batchQueryPosition(Map> userToOrgMap) { + List positionList = new ArrayList<>(); + for (Map.Entry> entry : userToOrgMap.entrySet()) { + UserBoundVO workerGroupDataDto = entry.getValue().get(0); + if (StringUtils.isNotEmpty(workerGroupDataDto.getPositionId())) { + positionList.add(workerGroupDataDto.getPositionId()); + } + } + if (CollectionUtil.isEmpty(positionList)) { + return new HashMap<>(); + } + return userApiV2Util.listPositionDetailInfoByIdsReturnMap(UserApiV2Util.uniqueStringList(positionList), null); + } + + private Map batchQueryPositionV2(Map userToOrgMap) { + List positionList = new ArrayList<>(); + for (Map.Entry entry : userToOrgMap.entrySet()) { + if (StringUtils.isNotEmpty(entry.getValue().getPositionId())) { + positionList.add(entry.getValue().getPositionId()); + } + } + if (CollectionUtil.isEmpty(positionList)) { + return new HashMap<>(); + } + return userApiV2Util.listPositionDetailInfoByIdsReturnMap(UserApiV2Util.uniqueStringList(positionList), null); + } + + + private Map> batctQueryUserInfoByLeaderIds(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + Map> map = new HashMap<>(); + for (String userId : userIds) { + List listVOS = userApiV2Util.listUnderlingTargetUser(userId, null); + if (CollUtil.isNotEmpty(listVOS)) { + map.put(userId, listVOS.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } else { + map.put(userId, new ArrayList<>()); + } + } + return map; + } + + private Map> batchQueryUserInfoByLeaderIdsV2(List userIds) { + Map> map = new HashMap<>(); + if (CollUtil.isEmpty(userIds)) { + return map; + } + List list = ftbCultivatePositionStatisticesMapper.queryAllNextUser(userIds); + if (CollUtil.isEmpty(list)) { + return map; + } + for (PersonForLeaderVo personForLeaderVo : list) { + if (map.containsKey(personForLeaderVo.getLeaderId())) { + map.get(personForLeaderVo.getLeaderId()).add(personForLeaderVo.getUserId()); + } else { + List tempList = new ArrayList<>(); + tempList.add(personForLeaderVo.getUserId()); + map.put(personForLeaderVo.getLeaderId(), tempList); + } + } + return map; + } + + private Map batchQueryCount(List userIds, int type, List courseIds) { + Map ret = new HashMap<>(); + if (CollectionUtil.isEmpty(userIds)) { + return ret; + } + List list = ftbCultivatePositionStatisticesMapper.batchNumberOfStudyCourses2(userIds, type, courseIds); + if (CollectionUtil.isEmpty(list)) { + return ret; + } + for (PersonStatisticesDto personStatisticesDto : list) { + ret.put(personStatisticesDto.getUserId(), personStatisticesDto); + } + return ret; + } + + private Map> batchQueryPositionToLearnPositionIds(Map> userToOrgMap) { + // 初始化返回结果 + Map> ret = new HashMap<>(); + + // 空值校验 + if (CollectionUtil.isEmpty(userToOrgMap)) { + return ret; + } + + // 提取所有用户ID + List userIds = new ArrayList<>(userToOrgMap.keySet()); + + // 查询用户的晋升岗位信息 + List promotionList = ftbCultivatePromotionNewService.queryPromotionPosition(userIds); + + // 构建用户ID到晋升岗位的映射 + Map> promotionMap = promotionList.stream() + .collect(Collectors.groupingBy(CultivateUserPositionVo::getUserId)); + + // 遍历用户,构建学习路径 + userToOrgMap.forEach((userId, userBoundList) -> { + // 获取晋升岗位ID列表 + List promotionPostIds = Optional.ofNullable(promotionMap.get(userId)) + .orElse(Collections.emptyList()) + .stream() + .map(CultivateUserPositionVo::getPostId) + .collect(Collectors.toList()); + + // 获取当前岗位ID(取第一个岗位) + String currentPositionId = Optional.ofNullable(userBoundList) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(0).getPositionId()) + .filter(StringUtils::isNotEmpty) + .orElse(null); + + // 合并晋升岗位和当前岗位ID + List learnPositionIds = new ArrayList<>(promotionPostIds); + if (currentPositionId != null) { + learnPositionIds.add(currentPositionId); + } + + // 存入结果 + ret.put(userId, learnPositionIds); + }); + + return ret; + } + + + private Map> batchQueryPositionToLearnPositionIdsV2(Map userToOrgMap) { + // 初始化返回结果 + Map> ret = new HashMap<>(); + + // 空值校验 + if (CollectionUtil.isEmpty(userToOrgMap)) { + return ret; + } + + // 提取所有用户ID + List userIds = new ArrayList<>(userToOrgMap.keySet()); + + // 查询用户的晋升岗位信息 + List promotionList = ftbCultivatePromotionNewService.queryPromotionPosition(userIds); + if (CollUtil.isNotEmpty(promotionList)) { + // 构建用户ID到晋升岗位的映射 + ret = promotionList.stream() + .collect(Collectors.groupingBy(CultivateUserPositionVo::getUserId)); + } + return ret; + } + + private Map convertStaffRosterMap(List ftbPersonnelsStaffRosters) { + Map userToRosterMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(ftbPersonnelsStaffRosters)) { + for (FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster : ftbPersonnelsStaffRosters) { + userToRosterMap.put(ftbPersonnelsStaffRoster.getUserId(), ftbPersonnelsStaffRoster); + } + } + return userToRosterMap; + } + + private Map> BatchQueryUserOrgPositionRank(List records) { + Map> retMap = new HashMap<>(); + if (CollectionUtil.isEmpty(records)) { + return retMap; + } + List userIdList = new ArrayList<>(); + for (FtbCultivatePositionPersonStatisticesVO record : records) { + userIdList.add(record.getUserId()); + } + +// List selectList = userApi.getUserBoundMoreInfosByUserIds(userIdList); + Map> map = userApiV2Util.getUserPrimaryBoundBatchCompatible(userIdList, null); + if (CollectionUtil.isEmpty(map)) { + return new HashMap<>(); + } + + for (Map.Entry> entry : map.entrySet()) { + String userId = entry.getKey(); // 用户ID + List list = entry.getValue(); // 获取值列表 + List workerGroupDataDtoList = new ArrayList<>(); + for (UserBoundVO vo : list) { + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getGradeId()); + dto.setAffiliatedRankName(vo.getGradeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + workerGroupDataDtoList.add(dto); + } + retMap.put(userId, workerGroupDataDtoList); + } + return retMap; + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbCultivatePersonStatisticesDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO positionListStatistics(FtbCultivatePersonStatisticesDTO ftbCultivatePositionPersonStatisticesDTO) { + String tenantId = UserProvider.getUser().getTenantId(); + Page page = ftbCultivatePositionPersonStatisticesDTO.coverCultivatePage(); + List powerPositionList = new ArrayList<>(); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + List userPrimaryBoundBatchReturnList = userApiV2Util.getUserPrimaryBoundBatchReturnList(innerPowerUserVO.getUserIds(), tenantId); + if (CollUtil.isEmpty(userPrimaryBoundBatchReturnList)) { + page.setRecords(new ArrayList<>()); + return CultivatePage.coverPageList(page); + } + powerPositionList = UserApiV2Util.uniqueStringList(userPrimaryBoundBatchReturnList.stream().map(UserBoundVO::getPositionId).collect(Collectors.toList())); + if (StrUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getPostId())) { + List queryPositoinList = List.of(ftbCultivatePositionPersonStatisticesDTO.getPostId().split(",")); + List intersection = UserApiV2Util.getIntersection(powerPositionList, queryPositoinList); + + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(ftbCultivatePositionPersonStatisticesDTO); + } else { + powerPositionList = intersection; + } + } + + } else if (innerPowerUserVO.getCode().equals(2)) { + // 线程等待 + return returnEmptyList(ftbCultivatePositionPersonStatisticesDTO); + } else { + if (StrUtil.isNotEmpty(ftbCultivatePositionPersonStatisticesDTO.getPostId())) { + powerPositionList = List.of(ftbCultivatePositionPersonStatisticesDTO.getPostId().split(",")); + } + } + ftbCultivatePositionPersonStatisticesDTO.setPowerPositionList(powerPositionList); + + page = ftbCultivatePositionStatisticesMapper.positionListStatistics(page, ftbCultivatePositionPersonStatisticesDTO); + if (CollectionUtil.isEmpty(page.getRecords())) return CultivatePage.coverPageList(page); + List postionIds = page.getRecords().stream() + .map(FtbCultivatePersonStatisticesVO::getPositionId).collect(Collectors.toList()); + List postionLearnIds = page.getRecords().stream() + .map(FtbCultivatePersonStatisticesVO::getPostLearnId).collect(Collectors.toList()); + // 岗位信息 +// List morePositionInfoDimensionByIds = positionApi.getMorePositionInfoDimensionByIds(postionIds); + List morePositionInfoDimensionByIds = userApiV2Util.listPositionDetailInfoByIds(postionIds, tenantId); + List userListForPositions = userApiV2Util.getUserListForPositions(postionIds, tenantId); + Map> userNumMaptemp = new HashMap<>(); //岗位->用户列表 + if (CollUtil.isNotEmpty(userListForPositions)) { + userNumMaptemp = userListForPositions.stream().collect(Collectors.groupingBy(UserBoundVO::getPositionId)); + } + final Map> userNumMap = userNumMaptemp; + Map stringPositionInfoDimensionVOMap = new HashMap<>(); + + if (CollUtil.isNotEmpty(morePositionInfoDimensionByIds)) { + for (PositionVO morePositionInfoDimensionById : morePositionInfoDimensionByIds) { + stringPositionInfoDimensionVOMap.put(morePositionInfoDimensionById.getId(), morePositionInfoDimensionById); + } + } + // 岗位考试合格率 + List passedTheExams = ftbCultivatePositionStatisticesMapper.checkJobQualificationRate(postionIds); + Map passedTheExamsMap = new HashMap<>(); + if (CollUtil.isNotEmpty(passedTheExams)) { + for (FtbCultivatePersonStatisticesVO.PassedTheExam passedTheExam : passedTheExams) { + passedTheExamsMap.put(passedTheExam.getPositionId(), passedTheExam.getAverageStudyHoursPerSubordinate()); + } + } + // 已学习课程数 + List numberOfCoursesTakens = ftbCultivatePositionStatisticesMapper.numberOfCoursesTaken(postionIds); + Map numberOfCoursesTakensMap = numberOfCoursesTakens.stream() + .collect(Collectors.toMap(FtbCultivatePersonStatisticesVO.NumberOfCoursesTaken::getPositionId, + FtbCultivatePersonStatisticesVO.NumberOfCoursesTaken::getNumberOfCoursesTaken, (k1, k2) -> k1)); + // 岗位学习课程总数 + List totalNumberOfOnTheJobLearningCoursess = ftbCultivatePositionStatisticesMapper + .totalNumberOfOnTheJobLearningCourses(postionLearnIds); + Map totalNumberOfOnTheJobLearningCoursesMap = totalNumberOfOnTheJobLearningCoursess.stream() + .collect(Collectors.toMap(FtbCultivatePersonStatisticesVO.TotalNumberOfOnTheJobLearningCourses::getPositionId, + FtbCultivatePersonStatisticesVO.TotalNumberOfOnTheJobLearningCourses::getTotalNumberOfOnTheJobLearningCourses, (k1, k2) -> k1)); + List records = page.getRecords(); + List> partition = new ArrayList<>(); + if (records.size() > 2000) { + partition = Lists.partition(records, 10); + } else if (500 < records.size() && records.size() < 1000) { + partition = Lists.partition(records, 5); + } else { + partition = Lists.partition(records, 1); + } + List>> futures = new ArrayList<>(); + for (List personStatisticesVOList : partition) { + futures.add(CompletableFuture.supplyAsync(() -> { + return personStatisticesVOList.parallelStream().map(vo -> { + FtbCultivatePersonStatisticesVO item = new FtbCultivatePersonStatisticesVO(); + // 所属组织相关 + item.setPositionId(vo.getPositionId()); + String positionId = vo.getPositionId(); + String postLearnId = vo.getPostLearnId(); + PositionVO positionInfoDimensionVO = stringPositionInfoDimensionVOMap.get(positionId); + log.info("获取岗位维度远程调用:{}", positionInfoDimensionVO); + item.setPositionTitle(positionInfoDimensionVO.getFullName()); + item.setPositionCustomId(positionInfoDimensionVO.getEnCode()); + if (CollUtil.isNotEmpty(positionInfoDimensionVO.getOrganizes())) { + List organizes = positionInfoDimensionVO.getOrganizes(); + List orgEncode = new ArrayList<>(); + List orgFullName = new ArrayList<>(); + List orgIds = new ArrayList<>(); + for (OrganizeFullBasicVO organize : organizes) { + orgIds.add(organize.getId()); + orgEncode.add(organize.getEnCode()); + orgFullName.add(organize.getName()); + } + item.setOrganizationCustomId(orgEncode); + item.setOrganization(orgFullName); + item.setOrgIds(orgIds); + } + + // 获取该岗位下所有人 + List userIdsByGradesId = new ArrayList<>(); + List positionListUserVO = userNumMap.get(positionId); + if (CollUtil.isNotEmpty(positionListUserVO)) { + userIdsByGradesId = positionListUserVO.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + item.setNumberOfPositions(userIdsByGradesId.size()); + try { + TenantDataSourceUtil.switchTenant(tenantId); + if (CollectionUtils.isNotEmpty(userIdsByGradesId)) { + // 岗位学习人数 + List numberOfPeopleStudyingOnTheJobs = ftbCultivatePositionStatisticesMapper.numberOfPeopleStudyingOnTheJob(userIdsByGradesId); + item.setParticipantsInTheStudy(numberOfPeopleStudyingOnTheJobs); + Integer numberOfPeopleStudyingOnTheJob = numberOfPeopleStudyingOnTheJobs.size(); + item.setNumberOfPeopleStudyingOnTheJob(numberOfPeopleStudyingOnTheJob); + // 学习时长 + Long studyDuration = ftbCultivatePositionStatisticesMapper.studyDuration(userIdsByGradesId); + item.setStudyDuration(CultivatePerUtils.computeDivision(new BigDecimal(studyDuration))); + // 人均学习时长 + item.setAverageStudyTimePerPerson(computeDivision(item.getStudyDuration(), numberOfPeopleStudyingOnTheJob)); + } + // 岗位学习课程总数 + + Integer totalNumberOfOnTheJobLearningCourses = totalNumberOfOnTheJobLearningCoursesMap.get(postLearnId); + item.setTotalNumberOfOnTheJobLearningCourses(totalNumberOfOnTheJobLearningCourses); + // 已学习课程数 + Integer numberOfCoursesTaken = numberOfCoursesTakensMap.get(positionId); + item.setNumberOfCoursesTaken(numberOfCoursesTaken); + // 岗位学习完课率 + item.setCourseCompletionRate(computeDivision(numberOfCoursesTaken, totalNumberOfOnTheJobLearningCourses)); + // 岗位学习考试合格率 + item.setAverageStudyHoursPerSubordinate(passedTheExamsMap.get(positionId)); + } catch (LoginException e) { + throw new RuntimeException(e); + } finally { + DataSourceContextHolder.clearDatasourceType(); + } + return item; + }).collect(Collectors.toList()); + }, threadPoolTaskExecutor)); + } + // 使用CompletableFuture.allOf等待所有任务完成 + CompletableFuture allTasks = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + // 等待所有任务完成 + allTasks.join(); + // 收集所有任务的结果 + List results = futures.stream() + .map(CompletableFuture::join) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + // 线程等待 + page.setRecords(results); + return CultivatePage.coverPageList(page); + } + + @Override + public List organizeCourseDetails(String startDate, String endDate, String orgId) { + List listByOrganizeIds = userApiV2Util.listPositionAndGradesByPositionNameForOrgIds(orgId, null); + + if (CollUtil.isEmpty(listByOrganizeIds)) { + return new ArrayList<>(); + } + String postionIds = listByOrganizeIds.stream().map(PositionAndGradesVO::getId).collect(Collectors.joining(",")); + List allUser = userApiV2Util.getUserListForOrgIds(List.of(orgId), null); + List userIds = allUser.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + List result = ftbCultivatePositionStatisticesMapper.organizeCourseDetails(startDate, endDate, postionIds, userIds); + if (CollUtil.isEmpty(result)) { + return new ArrayList<>(); + } + List allPositionIds = result.stream() + .flatMap(v -> Arrays.stream(v.getPositionIds().split(","))) + .collect(Collectors.toList()); + //查询岗位下的用户 + List userBoundList = new ArrayList<>(); + if (CollUtil.isNotEmpty(allPositionIds)) { + userBoundList = userApiV2Util.getUserListForPositions(allPositionIds, UserProvider.getUser().getTenantId()); + } + userBoundList = userBoundList.stream() + .filter(v -> orgId.equals(v.getOrganizeId())) + .collect(Collectors.toList()); + Set positionUserIds = userBoundList.stream() + .map(UserBoundVO::getId) + .collect(Collectors.toSet()); + Map> positionUserIdMap = userBoundList.stream() + .filter(Objects::nonNull) + .filter(v -> StrUtil.isNotBlank(v.getPositionId())) + .filter(v -> StrUtil.isNotBlank(v.getId())) + .collect(Collectors.groupingBy( + UserBoundVO::getPositionId, + Collectors.mapping(UserBoundVO::getId, Collectors.toSet()) + )); + List courseIds = result.stream() + .map(OrganizeCourseDetails::getCourseId) + .collect(Collectors.toList()); + // 已参与学习人数 + List numberOfPeopleParticipatingInTheStudy = + CollUtil.isNotEmpty(positionUserIdMap.keySet()) ? + ftbCultivatePositionStatisticesMapper.numberOfPeopleParticipatingInTheFinishStudy( + courseIds, String.join(",", positionUserIdMap.keySet()), List.of(1, 2), positionUserIds + ) : new ArrayList<>(); + Map finishStudyMap = numberOfPeopleParticipatingInTheStudy.stream() + .collect(Collectors.toMap(CourseStudyCountVO::getCourseId, CourseStudyCountVO::getNum, (a, b) -> b)); + //根据岗位分组 + result.forEach(item -> { + // 学习时长(h) + item.setStudyDuration(CultivatePerUtils.computeDivision(item.getStudyDuration())); + // 已参与学习人数 + Integer finishStudyMapOrDefault = finishStudyMap.getOrDefault(item.getCourseId(), 0); + item.setNumberOfPeopleWhoHaveParticipatedInLearning(finishStudyMapOrDefault); + // 应参与学习人数 + Set shouldStudyUserIds = Optional.of(Arrays.asList(item.getPositionIds().split(","))) + .orElse(Collections.emptyList()) + .stream() + .filter(StrUtil::isNotBlank) + .distinct() + .map(positionUserIdMap::get) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + item.setNumberOfPeopleParticipatingInTheStudy(shouldStudyUserIds.size()); + // 人均学习时长 + BigDecimal averageStudyTimePerPerson = computeDivision(item.getStudyDuration(), finishStudyMapOrDefault); + item.setAverageStudyTimePerPerson(averageStudyTimePerPerson); + }); + return result; + } + + @Override + public List personalDimensionCourseDetails(String startDate, String endDate, String userId) { + List result = ftbCultivatePositionStatisticesMapper.personalDimensionCourseDetails(startDate, endDate, userId); + if (CollUtil.isNotEmpty(result)) { + // 只保留未完成的课程ID(state != 1) + List courseIds = result.stream() + .filter(item -> item.getState() == null || item.getState() != 1) + .map(PersonalDimensionCourseDetails::getCourseId) + .collect(Collectors.toList()); + Map learningStateMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnProgress(userId, courseIds); + //查询所有学习的章节 根据课程id + result.forEach(item -> { + // 学习时长(h) + item.setStudyDuration(CultivatePerUtils.computeDivision(item.getStudyDuration())); + // 课程学习进度(%) + if (item.getState().equals(1)) { + // 已完成状态,进度为100% + item.setCourseLearningProgress(new BigDecimal("1")); + } else { + BigDecimal bigDecimal = learningStateMap.get(item.getCourseId()); + item.setCourseLearningProgress(bigDecimal != null ? bigDecimal : new BigDecimal("0")); + } + }); + } + return result; + } + + @Override + public BigDecimal jobLearningProgress(String postId, String userId) { + // 总数 + Integer a = ftbCultivatePositionStatisticesMapper.jobLearningProgress(postId, userId, null); + // 已学课程数 + Integer b = ftbCultivatePositionStatisticesMapper.jobLearningProgress(postId, userId, 1); + return computeDivision(b, a); + } + + @Override + public JobTitleStatistics jobTitleStatistics(String postId) { + JobTitleStatistics result = new JobTitleStatistics(); + ExamCultivateForPositionVo examCultivateForPositionVo = ftbCultivateExamUserService.queryCultivateCountForPositionId(postId); + // 参与岗位考试人数 + result.setNumberOfPeopleParticipatingInTheJobExam(examCultivateForPositionVo.getTotle()); + // 考试优秀人数 + result.setNumberOfPeopleWhoExcelledInTheExam(examCultivateForPositionVo.getExcellent()); + // 考试合格人数 + result.setNumberOfPeopleWhoPassedTheExam(examCultivateForPositionVo.getPass()); + // 考试不合格人数 + result.setNumberOfPeopleWhoFailedTheExam(examCultivateForPositionVo.getNoPass()); + // 完成学习人数 + // 获取该岗位下所有人 +// List userIdsByGradesId = userApi.getUserIdsByGradesId("", postId, ""); + List userIdsByGradesId = userApiV2Util.getUserListForPositions(List.of(postId), null); + log.info("获取该岗位下所有人:{}", userIdsByGradesId); + if (CollectionUtils.isNotEmpty(userIdsByGradesId)) { + // 岗位学习人数 + List userIds = userIdsByGradesId.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + Integer numberOfPeopleStudyingOnTheJobs = ftbCultivatePositionStatisticesMapper.numberOfPeopleWhoCompletedTheStudy(userIds); + result.setNumberOfPeopleWhoCompletedTheStudy(numberOfPeopleStudyingOnTheJobs); + } + + return result; + } + + + public static BigDecimal computeDivision(Object a, Object b) { + if (a == null || b == null || b.equals(0)) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.divide(b0, 2, RoundingMode.HALF_UP); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionUserServiceImpl.java new file mode 100644 index 0000000..3298762 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePositionUserServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionUserMapper; +import jnpf.cultivate.service.FtbCultivatePositionUserService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionUser; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivatePositionUserServiceImpl extends ServiceImpl implements FtbCultivatePositionUserService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionMemberServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionMemberServiceImpl.java new file mode 100644 index 0000000..42586e7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionMemberServiceImpl.java @@ -0,0 +1,333 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.druid.util.StringUtils; +import com.alibaba.nacos.shaded.com.google.common.collect.ImmutableMap; +import com.alibaba.nacos.shaded.com.google.common.collect.Maps; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.FtbCultivateMessageInfoMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionMemberMapper; +import jnpf.cultivate.service.FtbCultivatePositionService; +import jnpf.cultivate.service.FtbCultivatePromotionMemberService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionMemberDto; +import jnpf.model.cultivate.po.mesgg.CultivateMessageInfo; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMember; +import jnpf.model.cultivate.vo.promotion.FtbCultivateMeberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionMemberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionPostVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.permission.UserApi; +import jnpf.permission.UserRelationApi; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.model.userrelation.UserRelationPositionGrades; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.Constants; +import jnpf.util.UserProvider; +import jnpf.utils.FeignHolder; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePromotionMemberServiceImpl extends ServiceImpl implements FtbCultivatePromotionMemberService { + + @Resource + FtbCultivatePromotionMemberMapper memberMapper; + + @Resource + FtbCultivatePromotionMapper promotionMapper; + + @Autowired + UserApi userApi; + + @Autowired + UserApiV2Util userApiV2Util; + + @Autowired + private Executor threadPoolExecutor; + + @Resource + FtbCultivateMessageInfoMapper messageInfoMapper; + + @Autowired + UserRelationApi userRelationApi; + @Autowired + FtbCultivatePositionService ftbCultivatePositionService; + + @Override + @SneakyThrows + public FtbCultivateMeberVO queryChannelMembers(String id) { + // 通道层级 + FtbCultivatePromotionVO promotionChannel = promotionMapper.getPromotionChannel(id); + if (ObjectUtil.isEmpty(promotionChannel)) { + throw new RuntimeException("查询的通道id不存在,请核对后重新查询"); + } + FtbCultivateMeberVO memberVO = new FtbCultivateMeberVO(); + memberVO.setPromotionId(promotionChannel.getId()); + memberVO.setPromotion(promotionChannel.getPromotion()); + memberVO.setPromotionBusinessId(promotionChannel.getPromotionBusinessId()); + memberVO.setChannelLevel(promotionChannel.getPostChannelLevel()); + // 查询组织结构用户列表 + List userApiInfoByIds = userApi.getInfoByIds(new ArrayList<>()); + Map userInfo = Maps.newHashMap(); + userApiInfoByIds.forEach(item -> userInfo.put(item.getUserId(), item)); + // 所有用户的信息id + List userIds = userApiInfoByIds.stream().map(PartUserInfoVo::getUserId).distinct().filter(userId -> userId.length() > 10).collect(Collectors.toList()); + // 异步数据优化 + List> partition = partition(userIds, 20); + List userList = aggregateData(partition); +// ListIdDTO listIdDTO = new ListIdDTO(); +// listIdDTO.setIds(userIds); +// List userList = positionApi.postOrgPositionInfoList(listIdDTO); + // 过滤空的用户信息 + List infoBoundVOList = userList.stream().filter(item -> com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(item.getUserId())).collect(Collectors.toList()); + // 查询已启用成员列表 + Page mbeList = promotionMapper.getPromotionMbeList(new FtbCultivatePromotionDto().setPromotionId(id), + new Page()); + List mbeListRecords = mbeList.getRecords(); + // 获取当前传入用户绑定信息 + // 已启用成员列表 + memberVO.setListOfMembers(mbeListRecords); + // 进行数据剔除得到未启用成员列表 + List partUserInfoVos = infoBoundVOList.stream().filter(item -> mbeListRecords.parallelStream().noneMatch(mbe -> mbe.getUserId().equals(item.getUserId()))).collect(Collectors.toList()); + FtbCultivatePromotionPostVO promotionPostVO = promotionChannel.getPostChannelLevel().stream().filter(item -> 1 == item.getChannelLevel()).findFirst().orElse(new FtbCultivatePromotionPostVO()); + String postId = promotionPostVO.getPostId(); + // 只过滤该岗位的组织成员 + // 过滤当前岗位的成员 + List infoBoundVOS = partUserInfoVos.stream().filter(item -> postId.equals(item.getPositionId())).collect(Collectors.toList()); + // 获取到当前用户的岗位id + List enabledListOfMembers = infoBoundVOS.stream().map(vo -> { + FtbCultivatePromotionMemberVO promotionMemberVO = new FtbCultivatePromotionMemberVO(); + promotionMemberVO.setUserId(vo.getUserId()); + PartUserInfoVo partUserInfoVo = userInfo.get(vo.getUserId()); + promotionMemberVO.setUserName(partUserInfoVo.getRealName()); + promotionMemberVO.setOrgName(partUserInfoVo.getOrganizeName()); + promotionMemberVO.setOrgId(partUserInfoVo.getOrganizeId()); + promotionMemberVO.setPositionId(vo.getPositionId()); + promotionMemberVO.setPositionName(vo.getPositionName()); + promotionMemberVO.setPositionGradesId(vo.getPositionGradesId()); + promotionMemberVO.setPositionGradesName(vo.getPositionGradesName()); + return promotionMemberVO; + }).collect(Collectors.toList()); + Map> stringListMap = enabledListOfMembers.stream().filter( + item -> !StringUtils.isEmpty(item.getOrgName())) + .collect(Collectors.groupingBy(FtbCultivatePromotionMemberVO::getOrgName)); + memberVO.setNotEnabledListOfMembers(stringListMap); + return memberVO; + } + + /** + * 数据分块 + * + * @param list + * @param size + * @param + * @return + */ + public List> partition(List list, int size) { + return list.stream() + .collect(Collectors.groupingBy(it -> list.indexOf(it) / size)) + .values().stream() + .map(listPart -> new ArrayList<>(listPart)) + .collect(Collectors.toList()); + } + + /** + * 异步统计组织信息数据 + * + * @param queries + * @return + * @throws InterruptedException + */ + @SneakyThrows + public List aggregateData(List> queries) throws InterruptedException { + int threadCount = queries.size(); + CountDownLatch countDownLatch = new CountDownLatch(threadCount); + List> results = Collections.synchronizedList(new ArrayList<>(400)); + final UserInfo userInfo = UserProvider.getUser(); + final String tenantId = userInfo.getTenantId(); + ; + String token = userInfo.getToken(); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + for (List query : queries) { + threadPoolExecutor.execute(() -> { + try { + // 执行查询并获取结果 + List dataSummary = executeQuerys(query, tenantId, headers); + // 将结果添加到列表中 + if (dataSummary != null) { + results.add(dataSummary); + } + } finally { + // 计数器减一,表示一个任务已完成 + countDownLatch.countDown(); + } + }); + } + // 主线程等待所有子线程完成 + countDownLatch.await(); + // 合并或汇总所有数据结果 + return mergeResults(results); + } + + /** + * 将多个查询结果合并成一个总的统计数据 + * + * @param summaries + * @return + */ + private List mergeResults(List> summaries) { + // 将多个查询结果合并成一个总的统计数据 + if (!summaries.isEmpty()) { + return summaries.stream().flatMap(List::stream).collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + /** + * 实际查询接口 + * + * @param query + * @param tenantId + * @param headers + * @return + */ + private List executeQuerys(List query, String tenantId, Map headers) { + return FeignHolder.sendFeign(headers, () -> { + try { + Map userPrimaryBoundBatchCompatible = userApiV2Util.getUserPrimaryBoundBatch(query, tenantId); + if (CollUtil.isEmpty(userPrimaryBoundBatchCompatible)) { + return new ArrayList<>(); + } + List list = new ArrayList<>(); + //遍历 + for (Map.Entry entry : userPrimaryBoundBatchCompatible.entrySet()) { + PositionGradesInfoBoundVO vo = new PositionGradesInfoBoundVO(); + vo.setUserId(entry.getKey()); + UserBoundVO value = entry.getValue(); + vo.setOrganizeIds(List.of(value.getOrganizeId())); + vo.setOrganizeNames(List.of(value.getOrganizeName())); + vo.setPositionId(value.getPositionId()); + vo.setPositionName(value.getPositionName()); + vo.setPositionGradesId(value.getGradeId()); + vo.setPositionGradesName(value.getGradeName()); + vo.setUserId(entry.getKey()); + list.add(vo); + } + return list; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteRelation(String organizeId, UserRelationPositionGrades userRelationPositionGrades) { + List userIds = userRelationApi.saveRelationReturn(organizeId, userRelationPositionGrades); + // 删除用户对应岗位的启动的晋升通道 + if (ObjectUtil.isNotEmpty(userIds)) { + deleteMembersToProChannel(userIds); + // 删除对应的岗位学习职等开启的课程 + String positionGradesId = userRelationPositionGrades.getPositionGradesId(); + userIds.forEach(str -> { + ftbCultivatePositionService.deleteMemberGrade(positionGradesId, str); + }); + + } + } + + @Override + public List queryPromotionByUserOrPostId(String userId, String postId) { + return memberMapper.queryPromotionByUserOrPostId(userId, postId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addMembersToProChannel(FtbCultivatePromotionMemberDto dto) { + List list = new ArrayList<>(); + // 原始新增数据 + List userIdVos = dto.getUserIds(); + List userIds = userIdVos.stream().map(FtbCultivatePromotionMemberDto.PromotionUser::getUserId).collect(Collectors.toList()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbCultivatePromotionMember::getUserId, userIds); + List members = memberMapper.selectList(queryWrapper); + // 查询是否存在数据 + List updateList = members.stream().filter(item -> userIds.contains(item.getUserId())).collect(Collectors.toList()); + // 存在数据进行晋升通道更新 + updateList.forEach(item -> item.setPromotionId(dto.getPromotionId())); + // 同步更新 + boolean flag = this.updateBatchById(updateList); + // 过滤不存在的晋升通道的用户进行更新 + List collect = userIdVos.stream().filter(item -> !updateList.stream() + .map(FtbCultivatePromotionMember::getUserId).collect(Collectors.toList()).contains(item.getUserId())).collect(Collectors.toList()); + // 新增用户为空且修改用户为true 直接为全部都是修改 + if (flag && CollUtil.isEmpty(collect)) { + return; + } + // 进行新增条件构建 + collect.forEach(userInfoVo -> { + FtbCultivatePromotionMember member = new FtbCultivatePromotionMember(); + member.setPromotionId(dto.getPromotionId()); + member.setUserId(userInfoVo.getUserId()); + //用户姓名 + member.setUserName(userInfoVo.getUserName()); + member.setOrgId(userInfoVo.getOrgId()); + member.setOrgName(userInfoVo.getOrgName()); + member.setPositionId(userInfoVo.getPositionId()); + member.setPositionName(userInfoVo.getPositionName()); + member.setGradeId(userInfoVo.getGradeId()); + member.setGradeName(userInfoVo.getGradeName()); + member.setEnableStatus(1); + // 默认允许启动晋升通道 + member.setState(0); + list.add(member); + }); + this.saveBatch(list); + } + + @Override + public FtbCultivatePromotionVO queryPromotionByUser(String userId, String postId) { + return memberMapper.queryPromotionByUser(userId, postId); + } + + @Override + public void deleteMembersToProChannel(List userIds) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.in(FtbCultivatePromotionMember::getUserId, userIds); + int delete = memberMapper.delete(wrapper); + // 删除成功后 + if (delete > 0) { + userIds.forEach(userId -> { + CultivateMessageInfo messageInfo = new CultivateMessageInfo(); + messageInfo.setUserId(userId); + messageInfo.setBusinessId(userId); + messageInfo.setSource(0); + messageInfo.setState(0); + messageInfoMapper.insert(messageInfo); + }); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionNewServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionNewServiceImpl.java new file mode 100644 index 0000000..5595885 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionNewServiceImpl.java @@ -0,0 +1,1420 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivateCourseService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivatePositionStatisticesService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.bo.TriggerEventBO; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivateCreatMeMapInfoDTO; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionCreatNewDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.entiy.BaseEntity; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.examidentify.ExamIdentifyTriggerEventDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNewMessage; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.position.vo.CultivateUserPositionVo; +import jnpf.model.cultivate.v2.promotion.req.FtbCultivatePromotionReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionOrgStatisticReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionPersonStatisticReq; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionOrgStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionPersonStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.WebCultivatePromotionListVo; +import jnpf.model.cultivate.vo.course.app.FtbGlobalCurriculumAppVO; +import jnpf.model.cultivate.vo.promotion.*; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.ListIdDTO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +/** + * @Title: FtbCultivatePromotionNewServiceImpl + * @Author: peng.hao + * @create: 2024/3/27 11:38 + */ +@Service +public class FtbCultivatePromotionNewServiceImpl extends ServiceImpl implements FtbCultivatePromotionNewService { + + @Resource + FtbCultivatePromotionPostNewMapper ftbCultivatePromotionPostNewMapper; + + @Resource + FtbCultivatePromotionMemberNewMapper promotionMemberNewMapper; + + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + UserApi userApi; + + @Resource + FtbCultivatePositionStatisticesService statisticesService; + + @Resource + FtbCultivatePromotionNewMessageMapper messageMapper; + + @Autowired + CultivatePerUtils cultivatePerUtils; + + @Autowired + FtbCultivateCourseService cultivateCourseService; + + @Resource + FtbCultivateExamUserService cultivateExamUserService; + + /** + * 取出线程池 + */ + @Autowired + private Executor threadPoolExecutor; + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public PageListVO getList(CultivatePage oldPage, FtbCultivatePromotionDto dto) { + // 通过旧的分页对象获取培养推广的新分页对象 + Page page = oldPage.coverCultivatePage(); + // 调用baseMapper的getList方法更新page对象,传入分页信息和DTO + page = baseMapper.getList(page, dto); + + // 遍历page对象中的记录,为每个记录添加额外的处理逻辑 + page.getRecords().forEach(vo -> { + // 创建Lambda查询包装器,用于后续的数据库查询 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + // 设置查询条件,查找与当前vo对象的ID匹配的推广帖子 + queryWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, vo.getId()); + + // 执行查询,获取与当前vo对象关联的所有推广帖子 + List ftbCultivatePromotionPostNews = ftbCultivatePromotionPostNewMapper.selectList(queryWrapper); + // 将查询结果按照帖子的级别进行分组 + Map> collect = ftbCultivatePromotionPostNews.stream() + .collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + + // 查询当前vo对象的学习范围 + Integer learningScope = baseMapper.queryStudyScope(vo.getId()); + // 获取分组后的所有级别 + Set integers = collect.keySet(); + + // 创建一个用于存储学习地图级别的VO对象 + FtbCultivatePromotionLevelMapVO learnMapLevelsVO = new FtbCultivatePromotionLevelMapVO(); + // 设置学习地图级别的基本信息 + learnMapLevelsVO.setPromotionId(vo.getId()); + learnMapLevelsVO.setPromotion(vo.getPromotion()); + learnMapLevelsVO.setChannelIniName(vo.getChannelIniName()); + learnMapLevelsVO.setPromotionBusinessId(vo.getPromotionBusinessId()); + learnMapLevelsVO.setLearningScope(learningScope); + + // 创建一个列表,用于存储每个级别的选课信息 + List arrayList = new ArrayList<>(); + // 遍历每个级别,获取相应的选课信息并添加到列表中 + for (Integer integer : integers) { + FtbCultivatePromotionPostSelectCourseInfo selectCourseInfo = getCourseInfo(integer, collect, vo.getId()); + arrayList.add(selectCourseInfo); + } + // 将选课信息列表设置到学习地图级别的VO对象中 + learnMapLevelsVO.setPostChannelLevel(arrayList); + // 将学习地图级别的VO对象设置到vo对象中 + vo.setPostChannelLevel(learnMapLevelsVO); + }); + + // 对page对象进行最终的处理并返回 + return CultivatePage.coverPageList(page); + } + + @Override + @Transactional + public void add(FtbCultivatePromotionCreatNewDto creatDto) { + FtbCultivatePromotionNew promotionNew = FtbCultivatePromotionCreatNewDto.coverFtbCultivatePromotionCreatDto(creatDto); + // 新增岗位通道 + String promotionBusinessId = SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.PROMOTION); + //设置对应的key + promotionNew.setPromotionBusinessId(promotionBusinessId); + String promotion = promotionNew.getPromotion(); + if (StringUtils.isNotBlank(promotion)) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionNew::getPromotion, promotion); + lambdaQuery.eq(FtbCultivatePromotionNew::getEnableMark, 0); + Long aLong = baseMapper.selectCount(lambdaQuery); + if (aLong > 0) { + throw new RuntimeException("该地图名称已存在!"); + } + } + baseMapper.insert(promotionNew); + //晋升通道id + String promotionId = promotionNew.getId(); + List postChannelLevel = creatDto.getPostChannelLevel(); + String postId = postChannelLevel.stream().filter(item -> 1 == item.getChannelLevel()).findFirst().orElse(null).getPostId(); + verifyThatTheInitialPositionIsDuplicated(postId); + // 添加岗位 + extractedAddNewJobInformation(postChannelLevel, postId, promotionId); + } + + /** + * 提取 + * + * @param postChannelLevel + * @param postId + * @param promotionId + */ + private void extractedAddNewJobInformation(List postChannelLevel, String postId, String promotionId) { + Map> listMap = postChannelLevel.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNewVO::getChannelLevel)); + Set integers = listMap.keySet(); + List list = new ArrayList<>(); + for (Integer integer : integers) { + List vos = listMap.get(integer); + if (integer != 1) { + List collect = vos.stream().map(item -> item.getPostId().split(",")).collect(Collectors.toList()); + List stringList = collect.stream().flatMap(Arrays::stream).collect(Collectors.toList()); + if (stringList.contains(postId)) { + throw new RuntimeException("初始岗位不可以存在于其他阶段,请移除后重新添加学习地图!"); + } + } + if (integer == 1) { + FtbCultivatePromotionPostNewVO postNewVO = vos.stream().findFirst().orElse(null); + FtbCultivatePromotionPostNew postNew = FtbCultivatePromotionPostNewVO.coverFtbCultivatePromotionPostVO(postNewVO); + postNew.setPromotionId(promotionId); + postNew.setPostId(postNewVO.getPostId()); + postNew.setPostName(postNewVO.getPostName()); + postNew.setSelectCourseNumber(1); + list.add(postNew); + } else { + FtbCultivatePromotionPostNewVO postNewVO = vos.stream().findFirst().orElse(null); + String itemPostId = postNewVO.getPostId(); + String postName = postNewVO.getPostName(); + String[] split = itemPostId.split(","); + String[] postNameS = postName.split(","); +// List positionName = positionApi.getPositionName(List.of(split)); + List positionName = userApiV2Util.listPositionDetailInfoByIds(List.of(split), null); + if (CollUtil.isEmpty(positionName)) { + throw new RuntimeException("对应岗位未匹配对应的数据!"); + } + Map positionMap = positionName.stream().collect(Collectors.toMap(PositionVO::getId, PositionVO::getFullName)); + if (split.length != postNameS.length) { + throw new RuntimeException("岗位名称与岗位id数量不匹配,请重新添加!"); + } + for (int i = 0; i < split.length; i++) { + FtbCultivatePromotionPostNew postNew = FtbCultivatePromotionPostNewVO.coverFtbCultivatePromotionPostVO(postNewVO); + postNew.setPromotionId(promotionId); + String rePostId = split[i]; + // String postName1 = postNameS[i]; + String postName1 = positionMap.get(rePostId); + postNew.setPostId(rePostId); + postNew.setPostName(postName1); + list.add(postNew); + } + } + } + String tenantId = UserProvider.getUser().getTenantId(); + list.forEach(postNew -> { + ftbCultivatePromotionPostNewMapper.insert(postNew); + if (postNew.getLevel() == 1) { + // 学习地图岗位id + String postNewId = postNew.getId(); + List partUserInfoVos = userApiV2Util.getUserListForPositions(List.of(postNew.getPostId()), null); + if (CollUtil.isNotEmpty(partUserInfoVos)) { + Map stringMap = partUserInfoVos.stream().collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getName)); + ListIdDTO listIdDTO = new ListIdDTO(); + Set keySet = stringMap.keySet(); + listIdDTO.setIds(new ArrayList<>(keySet)); +// List positionGradesInfoBoundVOS = positionApi.postOrgPositionInfoList(listIdDTO); + List userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatchReturnList(listIdDTO.getIds(), tenantId); + // 过滤数据初始岗位相同数据进行新增 + List infoBoundVOS = userPrimaryBoundBatch.stream().filter(vo -> vo.getPositionId().equals(postNew.getPostId())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(infoBoundVOS)) { + threadPoolExecutor.execute(() -> { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + for (UserBoundVO partUserInfoVo : infoBoundVOS) { + FtbCultivateCreatMeMapInfoDTO meMapInfoDTO = FtbCultivateCreatMeMapInfoDTO.covertMe(partUserInfoVo); + meMapInfoDTO.setLevel(postNew.getLevel()); + meMapInfoDTO.setUserName(stringMap.get(meMapInfoDTO.getUserId())); + meMapInfoDTO.setPromotionId(postNew.getPromotionId()); + meMapInfoDTO.setStudyMapJobId(Arrays.asList(postNewId)); + correspondingPersonnelSelectPositions(meMapInfoDTO); + } + }); + } + } + } + }); + } + + @Override + @Transactional + public void updatePromotion(FtbCultivatePromotionCreatNewDto creatDto) { + FtbCultivatePromotionNew promotionNew = FtbCultivatePromotionCreatNewDto. + coverFtbCultivatePromotionCreatDto(creatDto); + // 修改后重新学习需要清除之前的对应学习地图创建的用户学习记录 + // List postInfoList = baseMapper.getThePositionThisPersonHasChosen(promotionNew.getId()); +// Map> userPostMap = postInfoList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionMeberPostInfo::getUserId)); + // Set userSet = userPostMap.keySet(); +// for (String userId : userSet){ +// List postInfos = userPostMap.get(userId); +// postInfos.forEach(item->{ +// // 清除学习记录 +// cultivateCourseService.learnMapRelearn(userId,item.getPostId()); +// }); +// } + // 删除岗位信息重新构建 + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(); + String id = promotionNew.getId(); + update.eq(FtbCultivatePromotionPostNew::getPromotionId, id); + ftbCultivatePromotionPostNewMapper.delete(update); + baseMapper.updateById(promotionNew); + // 移除晋升通道岗位信息 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, id); + ftbCultivatePromotionPostNewMapper.delete(wrapper); + // 移除关联数据 地图成员管理关联数据 + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionMemberNew::getPromotionId, id); + List memberNews = promotionMemberNewMapper.selectList(lambdaQuery); + // 新增通知表 成员重新选择 + List collect = memberNews.stream().map(item -> { + FtbCultivatePromotionNewMessage message = new FtbCultivatePromotionNewMessage(); + message.setPromotionId(id); + message.setUserId(item.getUserId()); + return message; + }).collect(Collectors.toList()); + collect.forEach(item -> messageMapper.insert(item)); + LambdaUpdateWrapper memberLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + memberLambdaUpdateWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, id); + promotionMemberNewMapper.delete(memberLambdaUpdateWrapper); + String postId = creatDto.getPostChannelLevel().stream().filter(item -> 1 == item.getChannelLevel()).findFirst().orElse(null).getPostId(); + extractedAddNewJobInformation(creatDto.getPostChannelLevel(), postId, id); + } + + /** + * 校验初始岗位是否重复添加 + * + * @param postId + */ + private void verifyThatTheInitialPositionIsDuplicated(String postId) { + List postNews = getFtbCultivatePromotionPostNews(postId); + if (CollUtil.isNotEmpty(postNews)) { + throw new RuntimeException("初始岗位不可添加重复学习地图,请添加其他岗位学习地图!"); + } + } + + private List getFtbCultivatePromotionPostNews(String postId) { + // 校验初始岗位是否已经添加了学习地图 + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPostId, postId); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getLevel, "1"); + return ftbCultivatePromotionPostNewMapper.selectList(lambdaQuery); + } + + + @Override + @Transactional + public String deleteById(String id, boolean flag) { + // 判断结果1:是-二次确认提示:请谨慎删除, + // 该通道的初始岗位在岗位学习进行了学习配置, + // 删除该通道会导致APP端应用了该通道的用户重新选择晋升通道,目前共有X位用户应用了该通道。 + // 确定删除后在APP端将触发重新选择晋升通道 + if (!flag) { //一次删除 + FtbCultivatePromotionStudyDeleteInfo theCurrentMapBindingMembers = getTheCurrentMapBindingMembers(id); + // 查询是否存在关联数据 + if (CollUtil.isNotEmpty(theCurrentMapBindingMembers.getDeleteInfoVOS())) { + return "1"; + } + } + return deleteByPromotionId(id); + } + + /** + * 删除晋升通道 + * + * @param id + * @return + */ + @Override + @Transactional + public String deleteByPromotionId(String id) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(FtbCultivatePromotionNew::getEnableMark, "1"); + updateWrapper.eq(BaseEntity::getId, id); + // 判断结果2:否-二次确认提示:您确认要删除该通道吗?点击确认后删除 + // 软删除 + baseMapper.update(new FtbCultivatePromotionNew(), updateWrapper); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + // 移除晋升通道岗位信息 + wrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, id); + ftbCultivatePromotionPostNewMapper.delete(wrapper); + // 移除关联数据 地图成员管理关联数据 + LambdaUpdateWrapper memberLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + memberLambdaUpdateWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, id); + promotionMemberNewMapper.delete(memberLambdaUpdateWrapper); + return null; + } + + @Override + public Page webList(Page page, FtbCultivatePromotionReq dto) { + return baseMapper.webList(page, dto); + } + + @Override + public List queryMaxLevel(List promotionIds) { + List countDtoList = baseMapper.queryMaxLevel(promotionIds); + if (CollUtil.isEmpty(countDtoList)) { + return new ArrayList<>(); + } + return countDtoList; + } + + @Override + public Page personStatisticsV2(V2PromotionPersonStatisticReq req, List userIds, Page page) { + return baseMapper.personStatisticsV2(req, userIds, page); + } + + @Override + public List queryPromotionPosition(List userIds) { + return baseMapper.queryPromotionPosition(userIds); + } + + @Override + public Page mapStatisticPageV2(Page page, V2PromotionOrgStatisticReq req, List userIds) { + return baseMapper.mapStatisticPageV2(page, req, userIds); + } + + @Override + public List userCountForPromotion(List userIds) { + List countDtoList = baseMapper.userCountForPromotion(userIds); + if (CollUtil.isEmpty(countDtoList)) { + return new ArrayList<>(); + } + return countDtoList; + } + + @Override + public List queryPositionNumForMap(List promotionIds) { + List countDtoList = baseMapper.queryPositionNumForMap(promotionIds); + if (CollUtil.isEmpty(countDtoList)) { + return new ArrayList<>(); + } + return countDtoList; + } + + @Override + public FtbCultivatePromotionStudyDeleteInfo getTheCurrentMapBindingMembers(String id) { + Map map = baseMapper.queryInitializationPositions(id); + String channelId = map.get("channelId"); + FtbCultivatePromotionStudyDeleteInfo deleteInfo = new FtbCultivatePromotionStudyDeleteInfo(); + FtbCultivatePromotionNew promotionNew = baseMapper.selectById(id); + deleteInfo.setPromotion(promotionNew.getPromotion()); + deleteInfo.setPromotionBusinessId(promotionNew.getPromotionBusinessId()); + Integer learningScope = baseMapper.queryStudyScope(id); + deleteInfo.setNumberOfStudyPositions(learningScope); + List memberNews = baseMapper.queryTheUserSLearnedPositionInformation(id, channelId); + // 获取用户选择岗位集合 + List vos = new ArrayList<>(); + for (FtbCultivatePromotionMemberNew me : memberNews) { + FtbCultivatePromotionDeleteInfoVO deleteInfoVO = new FtbCultivatePromotionDeleteInfoVO(); + String userId = me.getUserId(); + deleteInfoVO.setUserId(userId); + deleteInfoVO.setUserName(me.getUserName()); + deleteInfoVO.setPostInfo(me.getPostInfo()); + // 查询用户已学习岗位信息 + List list = new ArrayList<>(); + list.add(1); + list.add(2); + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, list); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + List hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + List postIds = baseMapper.queryChosePostId(userId, id); + List excludeUnselectedList = hasStudyPostIdsList.stream().filter(postIds::contains).collect(Collectors.toList()); + List positionNameList = userApiV2Util.listPositionDetailInfoByIds(excludeUnselectedList, null); + if (CollUtil.isNotEmpty(positionNameList)) { + List stringListNames = positionNameList.stream().map(PositionVO::getFullName).collect(Collectors.toList()); + deleteInfoVO.setStudyPostNames(stringListNames); + } + } + // 将对应的数据添加到列表 + vos.add(deleteInfoVO); + } + deleteInfo.setDeleteInfoVOS(vos); + return deleteInfo; + } + + @Override + public FtbCultivateStudyMemberInfo getPromotionById(String id, CultivatePage oldPage) { + Page page = oldPage.coverCultivatePage(); + FtbCultivateStudyMemberInfo memberInfo = new FtbCultivateStudyMemberInfo(); + // 学习范围 + Integer learningScope = baseMapper.queryStudyScope(id); + FtbCultivatePromotionNew promotionNew = baseMapper.selectById(id); + memberInfo.setPromotion(promotionNew.getPromotion()); + memberInfo.setPromotionBusinessId(promotionNew.getPromotionBusinessId()); + // 初始岗位名称 + Map map = baseMapper.queryInitializationPositions(id); + memberInfo.setChannelIniName(map.get("channelIniName")); + memberInfo.setChannelIniId(map.get("channelId")); + memberInfo.setLearningScope(learningScope); + LambdaQueryWrapper postQueryWrapper = Wrappers.lambdaQuery(); + postQueryWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, id); + List ftbCultivatePromotionPostNews = ftbCultivatePromotionPostNewMapper.selectList(postQueryWrapper); + Map> collect = ftbCultivatePromotionPostNews.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + Set integers = collect.keySet(); + memberInfo.setLearningPhase(integers.size()); + // 根据学习地图id查询所有开启的用户的人员 +// LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); +// lambdaQuery.eq(FtbCultivatePromotionMemberNew::getPromotionId,id); +// List memberNews = promotionMemberNewMapper.selectList(lambdaQuery); + List learStutaList = new ArrayList<>(); + learStutaList.add(1); + learStutaList.add(2); + // 查询用户已学习岗位信息 + List memberNews = baseMapper.queryTheUserSLearnedPositionInformation(id, memberInfo.getChannelIniId()); + List stringList = memberNews.stream().map(item -> item.getUserId() + "_" + item.getCurrentLearningStage()).distinct().collect(Collectors.toList()); + Map> userPosition = memberNews.stream().collect(Collectors.groupingBy(FtbCultivatePromotionMemberNew::getUserId, Collectors.toList())); +// Map userPosition = memberNews.stream() +// .collect(Collectors.toMap(FtbCultivatePromotionMemberNew::getUserId, FtbCultivatePromotionMemberNew::getOrganizationInformation,(a,b)->a)); + List list = new ArrayList<>(); + // 封装数据集 + List userIds = memberNews.stream().map(FtbCultivatePromotionMemberNew::getUserId).collect(Collectors.toList()); + Map userNameMap = new HashMap<>(); + Map userIdSysMap = new HashMap<>(); + Map userPrimaryBoundBatch = new HashMap<>(); + if (CollUtil.isNotEmpty(userIds)) { + userNameMap = cultivatePerUtils.queryRosterNameBasedOnUserId(userIds); + userIdSysMap = cultivatePerUtils.coverPersonalIds(userIds); + userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + } + for (String userInfo : stringList) { + FtbCultivateMemberInfo ftbCultivateMemberInfo = new FtbCultivateMemberInfo(); + String[] split = userInfo.split("_"); + // 用户信息 + String userId = split[0]; + UserBoundVO userBoundVO = userPrimaryBoundBatch.get(userId); + if (userBoundVO!=null) { + ftbCultivateMemberInfo.setUserName(userBoundVO.getUserName()); + String orgStr = userBoundVO.getOrganizeName(); + if(StringUtils.isNotEmpty(userBoundVO.getPositionName())){ + orgStr = orgStr+"_"+userBoundVO.getPositionName(); + } + if(StringUtils.isNotEmpty(userBoundVO.getGradeName())){ + orgStr = orgStr+"_"+userBoundVO.getGradeName(); + } + ftbCultivateMemberInfo.setOrganizationInformation(orgStr); + } + + ftbCultivateMemberInfo.setUserId(userId); + // 当前学习阶段 + String currentLearningStage = split[1]; + // 查询对应地图选择的人员 对应的选择阶段选择 + ftbCultivateMemberInfo.setCurrentLearningStage(currentLearningStage); + if (!"1".equals(currentLearningStage)) { + List postIdsChose = baseMapper.queryUserChosesPosts(userId, Integer.valueOf(currentLearningStage), id); + ftbCultivateMemberInfo.setSelectedStudyPosition(postIdsChose); + } + // 查询用户已学习岗位信息 + List relist = new ArrayList<>(); + relist.add(1); + relist.add(2); + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, relist); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + List hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + List postIds = baseMapper.queryChosePostId(userId, id); + List excludeUnselectedList = hasStudyPostIdsList.stream().filter(postIds::contains).collect(Collectors.toList()); +// List positionNameList = positionApi.getPositionName(excludeUnselectedList); + List positionNameList = userApiV2Util.listPositionDetailInfoByIds(excludeUnselectedList, null); + if (CollUtil.isNotEmpty(positionNameList)) { + List stringListNames = positionNameList.stream().map(PositionVO::getFullName).collect(Collectors.toList()); + ftbCultivateMemberInfo.setPositionLearned(stringListNames); + } + } + if(CollUtil.isEmpty(ftbCultivateMemberInfo.getPositionLearned()) && userBoundVO!=null && "1".equals(currentLearningStage)){ + ftbCultivateMemberInfo.setPositionLearned(List.of(userBoundVO.getPositionName())); + } + if (userIdSysMap.containsKey(userId)) { + ftbCultivateMemberInfo.setUserId(userIdSysMap.get(userId)); + } + list.add(ftbCultivateMemberInfo); + } + PageListVO paginate = CultivatePage.paginate(list, page); + memberInfo.setMemberList(paginate); + return memberInfo; + } + + @Override + public Integer queryTheCurrentUserLearningMapLevel(String userId, String postId) { + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + return 0; + } + Integer mapLevel = promotionMemberNewMapper.queryTheCurrentUserLearningMapLevel(userId, postId, promotionId); + if (ObjectUtil.isEmpty(mapLevel)) { + return 1; + } + String studying = checkStudying(mapLevel, userId, postId); + if (StringUtils.isEmpty(studying)) { + // 学习地图共有多少个阶段 + Integer learningScope = baseMapper.queryHowManyStages(promotionId); + return checkAndIncrement(mapLevel, learningScope); + } + return mapLevel; + } + + /** + * 判断当前数字加1是否是最后一个等级,如果不是返回加1后的数,如果是返回最后等级的数。 + * + * @param currentGrade 当前的数字 + * @return 如果加1不是最后一个等级,返回加1后的数;否则返回最后一个等级的数 + */ + private int checkAndIncrement(int currentGrade, int MAX_GRADE) { + int incrementedGrade = currentGrade + 1; + + if (incrementedGrade <= MAX_GRADE) { + return incrementedGrade; + } else { + return MAX_GRADE; + } + } + + @Override + public FtbCultivatePromotionLevelMapVO getLearnMapLevels(String channelIniPoId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePromotionPostNew::getPostId, channelIniPoId); + queryWrapper.eq(FtbCultivatePromotionPostNew::getLevel, "1"); + FtbCultivatePromotionPostNew vo = ftbCultivatePromotionPostNewMapper.selectOne(queryWrapper); + if (ObjectUtil.isEmpty(vo)) { + return null; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(BaseEntity::getId, vo.getPromotionId()); + wrapper.eq(FtbCultivatePromotionNew::getEnableMark, 0); + FtbCultivatePromotionNew promotionNew = baseMapper.selectOne(wrapper); + if (ObjectUtil.isEmpty(promotionNew)) { + return null; + } + // 学习地图id + String promotionId = vo.getPromotionId(); + LambdaQueryWrapper postQueryWrapper = Wrappers.lambdaQuery(); + postQueryWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + List ftbCultivatePromotionPostNews = ftbCultivatePromotionPostNewMapper.selectList(postQueryWrapper); + Map> collect = ftbCultivatePromotionPostNews.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + // 学习范围 + Integer learningScope = baseMapper.queryStudyScope(vo.getPromotionId()); + Map str = baseMapper.queryInitializationPositions(promotionId); + String channelIniName = ""; + if (ObjectUtils.isNotEmpty(str)) { + channelIniName = str.get("channelIniName"); + } + Set integers = collect.keySet(); + + FtbCultivatePromotionLevelMapVO learnMapLevelsVO = new FtbCultivatePromotionLevelMapVO(); + learnMapLevelsVO.setPromotionId(promotionId); + learnMapLevelsVO.setPromotion(promotionNew.getPromotion()); + learnMapLevelsVO.setChannelIniName(channelIniName); + learnMapLevelsVO.setPromotionBusinessId(promotionNew.getPromotionBusinessId()); + learnMapLevelsVO.setLearningScope(learningScope); + List arrayList = new ArrayList<>(); + for (Integer integer : integers) { + FtbCultivatePromotionPostSelectCourseInfo selectCourseInfo = getCourseInfo(integer, collect, promotionId); + arrayList.add(selectCourseInfo); + } + learnMapLevelsVO.setChannelLevel(integers.size()); + learnMapLevelsVO.setPostChannelLevel(arrayList); + return learnMapLevelsVO; + } + + + @Override + public FtbCultivatePromotionLevelMapVO getLearnMapLevelsWithMapId(String promotionId) { + FtbCultivatePromotionNew promotionNew = baseMapper.selectById(promotionId); + // 学习地图id + LambdaQueryWrapper postQueryWrapper = Wrappers.lambdaQuery(); + postQueryWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + List ftbCultivatePromotionPostNews = ftbCultivatePromotionPostNewMapper.selectList(postQueryWrapper); + Map> collect = ftbCultivatePromotionPostNews.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + // 学习范围 + Integer learningScope = baseMapper.queryStudyScope(promotionId); + Map str = baseMapper.queryInitializationPositions(promotionId); + Set integers = collect.keySet(); + + FtbCultivatePromotionLevelMapVO learnMapLevelsVO = new FtbCultivatePromotionLevelMapVO(); + learnMapLevelsVO.setPromotionId(promotionId); + learnMapLevelsVO.setPromotion(promotionNew.getPromotion()); + learnMapLevelsVO.setChannelIniName(str.get("channelIniName")); + learnMapLevelsVO.setPromotionBusinessId(promotionNew.getPromotionBusinessId()); + learnMapLevelsVO.setLearningScope(learningScope); + List arrayList = new ArrayList<>(); + for (Integer integer : integers) { + FtbCultivatePromotionPostSelectCourseInfo selectCourseInfo = getCourseInfo(integer, collect, promotionId); + arrayList.add(selectCourseInfo); + } + learnMapLevelsVO.setChannelLevel(integers.size()); + learnMapLevelsVO.setPostChannelLevel(arrayList); + return learnMapLevelsVO; + } + + @Override + public List viewLearningMapBasedOnPosition(String userId, String postId, Integer level) { + // 查询用户已经选择的对应阶段学习地图 + return baseMapper.viewLearningMapBasedOnPosition(userId, postId, level); + } + + @Override + public FtbCultivateStudyMemberVO getDetails(String id) { + FtbCultivatePromotionNew promotionNew = baseMapper.selectById(id); + FtbCultivateStudyMemberVO memberVO = FtbCultivateStudyMemberVO.coverFtbCultivateStudyMemberVO(promotionNew); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPromotionId, id); + List postNews = ftbCultivatePromotionPostNewMapper.selectList(lambdaQuery); + List newVOList = new ArrayList<>(); + Map> collect = postNews.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + Set integers = collect.keySet(); + for (Integer integer : integers) { + List postNews1 = collect.get(integer); + FtbCultivatePromotionPostNew postNew; + if (integer == 1) { + postNew = postNews1.stream().findFirst().orElse(null); + } else { + String postId = postNews1.stream().map(FtbCultivatePromotionPostNew::getPostId).collect(Collectors.joining(",")); + String postName = postNews1.stream().map(FtbCultivatePromotionPostNew::getPostName).collect(Collectors.joining(",")); + postNew = postNews1.stream().findFirst().orElse(null); + assert postNew != null; + postNew.setPostId(postId); + postNew.setPostName(postName); + } + FtbCultivatePromotionPostNewVO covert = FtbCultivatePromotionPostNew.covert(postNew); + newVOList.add(covert); + } + memberVO.setPostChannelLevel(newVOList); + return memberVO; + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void correspondingPersonnelSelectPositions(FtbCultivateCreatMeMapInfoDTO creatMeMapInfoDTO) { + List studyMapJobId = creatMeMapInfoDTO.getStudyMapJobId(); + // 二次选择 根据选择岗位加 选学数量确定学习进度 + // 查询用户已学习岗位信息 + Integer currentLearningStage = 0; + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(creatMeMapInfoDTO.getUserId(), + Collections.singletonList(1), null); + List hasStudyPostIdsList = new ArrayList<>(); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + } + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.in(BaseEntity::getId, studyMapJobId); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPromotionId, creatMeMapInfoDTO.getPromotionId()); + List postNews = ftbCultivatePromotionPostNewMapper.selectList(lambdaQuery); + List stringList = postNews.stream().map(FtbCultivatePromotionPostNew::getPostId).collect(Collectors.toList()); + int selectCourseNumber = creatMeMapInfoDTO.getStudyMapJobId().size(); + // 查询当前人已学习岗位信息 + List studyPostIds = hasStudyPostIdsList.stream().filter(stringList::contains).distinct().collect(Collectors.toList()); + if (studyPostIds.size() >= selectCourseNumber) { + currentLearningStage = creatMeMapInfoDTO.getLevel(); + // 已经到下一阶段了 + } else if (creatMeMapInfoDTO.getLevel() > currentLearningStage) { + currentLearningStage = creatMeMapInfoDTO.getLevel(); + } + for (String item : studyMapJobId) { + FtbCultivatePromotionMemberNew promotionMemberNew = FtbCultivateCreatMeMapInfoDTO.covert(creatMeMapInfoDTO); + promotionMemberNew.setPromotionPostId(item); + promotionMemberNew.setCurrentLearningStage(currentLearningStage); + promotionMemberNewMapper.insert(promotionMemberNew); + // 本岗不需要同步状态 + if (currentLearningStage != 1) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionMemberNew.getPromotionId()); + updateWrapper.eq(FtbCultivatePromotionMemberNew::getUserId, promotionMemberNew.getUserId()); + updateWrapper.set(FtbCultivatePromotionMemberNew::getCurrentLearningStage, currentLearningStage); + promotionMemberNewMapper.update(null, updateWrapper); + } + } + // 课程数据初始化 + stringList.forEach(str -> cultivateCourseService.learnMapRelearn(creatMeMapInfoDTO.getUserId(), str)); + } + + @Override + public FtbCultivateUnderMemberInfo queryTheLearningStageOfSubordinates(String userId, String postId) { + FtbCultivateUnderMemberInfo memberInfo = new FtbCultivateUnderMemberInfo(); + // 根据岗位id查询学习地图主键 + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + throw new RuntimeException("该岗位没有学习地图!"); + } + UserEntity userEntity = userApi.getInfoById(userId); + memberInfo.setUserId(userEntity.getId()); + memberInfo.setUserName(userEntity.getRealName()); + if (ObjectUtils.isEmpty(promotionId)) { + BigDecimal bigDecimal = statisticesService.jobLearningProgress(postId, userId); + memberInfo.setCurrentLearningStage(1); + memberInfo.setLearningProgress(bigDecimal); + memberInfo.setLearningScope(0); + // 查询用户已学习岗位信息 + List relist = new ArrayList<>(); + relist.add(1); + relist.add(2); + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, relist); + List hasStudyPostIdsList = new ArrayList<>(); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); +// List positionName = positionApi.getPositionName(hasStudyPostIdsList); + List positionName = userApiV2Util.listPositionDetailInfoByIds(hasStudyPostIdsList, null); + memberInfo.setSelectedStudyPositionIds(hasStudyPostIds); + memberInfo.setSelectedStudyPosition(positionName.stream().map(PositionVO::getFullName).collect(Collectors.toList())); + } + return memberInfo; + } + // 学习地图共有多少个阶段 + Integer learningScope = baseMapper.queryHowManyStages(promotionId); + memberInfo.setLearningScope(learningScope); + // 当前学习阶段 + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId); + lambdaQuery.eq(FtbCultivatePromotionMemberNew::getUserId, userId); + lambdaQuery.eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); + lambdaQuery.orderByDesc(FtbCultivatePromotionMemberNew::getCurrentLearningStage); + lambdaQuery.last("limit 1"); + FtbCultivatePromotionMemberNew memberNew = promotionMemberNewMapper.selectOne(lambdaQuery); + // 查询用户已学习岗位信息 + List relist = new ArrayList<>(); + relist.add(1); + relist.add(2); + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, relist); + List hasStudyPostIdsList = new ArrayList<>(); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + } + if (ObjectUtils.isEmpty(memberNew)) { + BigDecimal bigDecimal = statisticesService.jobLearningProgress(postId, userId); + memberInfo.setCurrentLearningStage(1); + BigDecimal learningProgress = bigDecimal.multiply(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_HALF_UP); + memberInfo.setLearningProgress(learningProgress); + if (CollUtil.isNotEmpty(hasStudyPostIdsList)) { +// List positionName = positionApi.getPositionName(hasStudyPostIdsList); + List positionName = userApiV2Util.listPositionDetailInfoByIds(hasStudyPostIdsList, null); + memberInfo.setSelectedStudyPosition(positionName.stream().map(PositionVO::getFullName).collect(Collectors.toList())); + memberInfo.setSelectedStudyPositionIds(hasStudyPostIds); + } + return memberInfo; + } + Integer currentLearningStage = memberNew.getCurrentLearningStage(); + memberInfo.setCurrentLearningStage(currentLearningStage); + // 已选学岗位 + List postIds = baseMapper.queryChosePostId(userId, promotionId); + if (CollUtil.isNotEmpty(hasStudyPostIdsList)) { + List excludeUnselectedList = hasStudyPostIdsList.stream().filter(postIds::contains).collect(Collectors.toList()); +// List positionNameList = positionApi.getPositionName(excludeUnselectedList); + List positionNameList = userApiV2Util.listPositionDetailInfoByIds(excludeUnselectedList, null); + if (CollUtil.isNotEmpty(positionNameList)) { + List stringListNames = positionNameList.stream().map(PositionVO::getFullName).collect(Collectors.toList()); + memberInfo.setSelectedStudyPosition(stringListNames); + memberInfo.setSelectedStudyPositionIds(excludeUnselectedList.stream().collect(Collectors.joining(","))); + } + } + // 学习进度 + List list = new ArrayList<>(); + for (String str : postIds) { + BigDecimal bigDecimal = statisticesService.jobLearningProgress(str, userId); + list.add(bigDecimal); + } + BigDecimal reduce = list.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + if (reduce.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal divide = reduce.divide(new BigDecimal(postIds.size()), 2, BigDecimal.ROUND_HALF_UP); + BigDecimal learningProgress = divide.multiply(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_HALF_UP); + memberInfo.setLearningProgress(learningProgress); + } else { + memberInfo.setLearningProgress(BigDecimal.ZERO); + } + return memberInfo; + } + + @Override + @Transactional + public FtbCultivateNextCourseVO selectPositionsCorrespondingToTheStages(String userId, String postId, Integer level, Integer compulsory) { + FtbCultivateNextCourseVO ftbCultivateNextCourseVO = new FtbCultivateNextCourseVO(); + // 根据岗位id查询学习地图主键 + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + throw new RuntimeException("该岗位没有学习地图!"); + } + List> maps = baseMapper.queryUserChosesPostsWitId(userId, level, promotionId); + ftbCultivateNextCourseVO.setMaps(maps); + List postIds = maps.stream().map(a -> a.get("postId").toString()).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(postIds)) { + // 课程内容 + List fullCurriculumAppList = new ArrayList<>(); + long resultAlreadyLearnCourseNum = 0L; + for (String traversePostId : postIds) { + List tempFullCurriculumAppList = baseMapper.onTheJobLearningCourseNew(userId, List.of(traversePostId), compulsory); + tempFullCurriculumAppList.forEach(item -> { + // 学习总人数 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(FtbCultivatePositionCourceLearning::getState, List.of(1, 2)); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, item.getCourseId()); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + item.setLearnTotalNumber(ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper)); + }); + // 修复如果岗位学习中存在1,2, 3 课程时,如果1,2 课程已学习完,然后把3课程下架或者删除,无法触发考试与鉴定 + // 下层做业务幂等性,会存在重复调用的情况 + long alreadyLearnCourseNum = tempFullCurriculumAppList.stream() + .filter(b -> b.getLearnState() == 1).count(); + resultAlreadyLearnCourseNum = resultAlreadyLearnCourseNum + alreadyLearnCourseNum; + if (alreadyLearnCourseNum == tempFullCurriculumAppList.size() && !tempFullCurriculumAppList.isEmpty()) { + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + ExamIdentifyTriggerEventDTO.builder() + .postId(traversePostId) + .userId(userId) + .triggerEventType(TriggerEventBO.TriggerEventType.POST) + .build())); + } + fullCurriculumAppList.addAll(tempFullCurriculumAppList); + } + ftbCultivateNextCourseVO.setFullCurriculumAppList(fullCurriculumAppList); + // 已学课程数 + ftbCultivateNextCourseVO.setAlreadyLearnCourseNum(resultAlreadyLearnCourseNum); + // 已选择学习岗位的总课程数 + ftbCultivateNextCourseVO.setTotalCourseNum(fullCurriculumAppList.size()); + } + return ftbCultivateNextCourseVO; + } + + @Override + public void changeTheCurrentPersonSLearningStage(String userId, String postId) { + // 当前选择岗位 初始化岗位地图() + // 学习地图id + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + // 初始岗位无学习地图 + if (StringUtils.isEmpty(promotionId)) { + return; + } + // 根据学习地图查询学习层级 + FtbCultivateStudyMemberVO details = getDetails(promotionId); + // 查询用户已学习岗位信息 + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, + Collections.singletonList(1), null); + List hasStudyPostIdsList = new ArrayList<>(); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + } + // 学习层级 + List postChannelLevel = details.getPostChannelLevel(); + List objects = new LinkedList<>(); + for (FtbCultivatePromotionPostNewVO postChannelLevelVO : postChannelLevel) { + // 获取等级 + Integer channelLevel = postChannelLevelVO.getChannelLevel(); + // 获取选学数量 + Integer selectCourseNumber = postChannelLevelVO.getSelectCourseNumber(); + // 按阶段筛选用户选择岗位进行学习阶段变更 + List> postsWitId = baseMapper.queryUserChosesPostsWitId(userId, channelLevel, promotionId); + // 当前用户没有选择地图 + if (CollUtil.isEmpty(postsWitId)) { + continue; + } + // 选择的岗位id + List stringList = postsWitId.stream().map(map -> String.valueOf(map.get("postId"))).collect(Collectors.toList()); + // 查询当前人已学习岗位信息 + List studyPostIds = hasStudyPostIdsList.stream().filter(stringList::contains).distinct().collect(Collectors.toList()); + if (studyPostIds.size() >= selectCourseNumber) { + objects.add(channelLevel); + } + } + // 更改当前人的学习阶段 + if (CollUtil.isNotEmpty(objects)) { + // 当前学习阶段 + Integer integer = objects.get(objects.size() - 1); + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.set(FtbCultivatePromotionMemberNew::getCurrentLearningStage, integer); + lambdaUpdate.eq(FtbCultivatePromotionMemberNew::getUserId, userId); + lambdaUpdate.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId); + promotionMemberNewMapper.update(null, lambdaUpdate); + } + } + + @Override + public String checkStudying(Integer level, String userId, String postId) { + + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + throw new RuntimeException("该岗位没有学习地图!"); + } + FtbCultivatePromotionNew promotionNew = baseMapper.selectById(promotionId); + // 查询用户已学习岗位信息 + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, + Collections.singletonList(1), null); + List hasStudyPostIdsList = new ArrayList<>(); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + } + if (StringUtils.isNotEmpty(promotionId)) { + // 查询用户学习阶段 + LambdaQueryWrapper memberNewLambdaQueryWrapper = Wrappers.lambdaQuery(); + memberNewLambdaQueryWrapper.last("limit 1"); + memberNewLambdaQueryWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId); + memberNewLambdaQueryWrapper.eq(FtbCultivatePromotionMemberNew::getUserId, userId); + memberNewLambdaQueryWrapper.eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); + FtbCultivatePromotionMemberNew memberNew = promotionMemberNewMapper.selectOne(memberNewLambdaQueryWrapper); + // 有通道但是没有选择 + if (ObjectUtil.isNotEmpty(memberNew)) { + // 当前学习阶段 + Integer currentLearningStage = memberNew.getCurrentLearningStage(); + // 如果当前阶段已经学习过了 则不需要重复学习 + if (currentLearningStage > level) { + return null; + } + } + } + // 本岗校验本岗是否学习完成且参加了 考试和鉴定 + Integer learningMapSetting = promotionNew.getLearningMapSetting(); + if (level == 1) { + // 查询当前岗位是否存在学习课程 + Integer numberOfCourses = baseMapper.checkIfThePositionHasCourses(postId); + if (numberOfCourses == 0) { + throw new RuntimeException("当前阶段未绑定学习!无法进行下阶段学习"); + } + if (!hasStudyPostIdsList.contains(postId)) { + return "1"; + } + // 查询本岗岗位学习是否有考试 + List strings = baseMapper.checkIfThereAreExamsForThisPositionWithNew(postId, userId); + int exmCount = 0; + if (CollUtil.isNotEmpty(strings)) exmCount = strings.size(); + // [1:已经完成考试或者合格 2:未完成考试, 3:考试未合格] + Integer i = cultivateExamUserService.queryExamIsCompleteForUserIdAndPostion(userId, postId, + learningMapSetting); + if (exmCount > 0 && 2 == i) { + return "2"; + } else if (exmCount > 0 && 3 == i) { + return "4"; + } + // 或者实操鉴定 + // List isAPracticalAppraisalIds = baseMapper.checkWhetherThereIsAPracticalAppraisal(postId); + // 查询当前人实操鉴定是否是二次触发,如果二次触发就不需要校验 + List applyIds = baseMapper.checkWhetherThereIsAPracticalAppraisalWithNew(postId, userId); + Integer count = 0; + if (!applyIds.isEmpty()) + count = baseMapper.queryTheNumberOfPracticalAppraisalStudies(List.of(postId), userId, applyIds, learningMapSetting); + if (!applyIds.isEmpty() && learningMapSetting == 0 && count == 0) { + return "3"; + } else if (!applyIds.isEmpty() && learningMapSetting == 1 && count == 0) { + return "5"; + } + return null; + } + List> postsWitId = baseMapper.queryUserChosesPostsWitId(userId, level, promotionId); + // 选择的岗位id + List stringList = postsWitId.stream().map(map -> String.valueOf(map.get("postId"))).collect(Collectors.toList()); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getLevel, level); + lambdaQuery.last("limit 1"); + FtbCultivatePromotionPostNew postNew = ftbCultivatePromotionPostNewMapper.selectOne(lambdaQuery); + // 选学数量(根据选学数量控制学习数量) + Integer selectCourseNumber = postNew.getSelectCourseNumber(); + // 查询当前人已学习岗位信息 + List studyPostIds = hasStudyPostIdsList.stream().filter(stringList::contains).distinct().collect(Collectors.toList()); + // 空为已完成, 1 当前阶段未学习完成, 2岗位学习考试未完成, 3岗位学习实操鉴定未完成 + // 标记选学数量没有完成 + if (studyPostIds.size() < selectCourseNumber) { + return "1"; + } + List exmList = new ArrayList<>(); + List idemList = new ArrayList<>(); + int appCount = 0; + List strings = new ArrayList<>(); + for (String str : studyPostIds) { + List strings1 = baseMapper.checkIfThereAreExamsForThisPositionWithNew(str, userId); + if (CollUtil.isNotEmpty(strings1)) exmList.addAll(strings1); + List isAPracticalAppraisal = baseMapper.checkWhetherThereIsAPracticalAppraisalWithNew(str, userId); + if (!isAPracticalAppraisal.isEmpty()) { + appCount += isAPracticalAppraisal.size(); + idemList.add(str); + strings.addAll(isAPracticalAppraisal); + } + } + // 考试判断 + List collect = exmList.stream().map(str -> + cultivateExamUserService.queryExamIsCompleteForUserIdAndPostionWithNew(str, learningMapSetting) + ).collect(Collectors.toList()); + // [1:已经完成考试或者合格 2:未完成考试, 3:考试未合格] + if (!exmList.isEmpty() && collect.contains(2)) { + return "2"; + } else if (!exmList.isEmpty() && collect.contains(3)) { + return "4"; + } + // 实操鉴定判断 + Integer count = 0; + if (CollUtil.isNotEmpty(strings)) + count = baseMapper.queryTheNumberOfPracticalAppraisalStudies(idemList, userId, strings, learningMapSetting); + if (appCount > count && learningMapSetting == 0) { + return "3"; + } else if (appCount > count && learningMapSetting == 1) { + return "5"; + } + return null; + } + + /** + * 内部检查学习状态 + * @param level 学习等级 + * @param userId 用户ID + * @param postId 岗位ID + * @param promotionId 地图id + * @return 状态 + */ + @Override + public String innerCheckStudying(Integer level, String userId, String postId,String promotionId) { + + FtbCultivatePromotionNew promotionNew = baseMapper.selectById(promotionId); + // 查询用户已学习岗位信息 + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, + Collections.singletonList(1), null); + List hasStudyPostIdsList = new ArrayList<>(); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + } + + // 查询用户学习阶段 + LambdaQueryWrapper memberNewLambdaQueryWrapper = Wrappers.lambdaQuery(); + memberNewLambdaQueryWrapper.last("limit 1"); + memberNewLambdaQueryWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId); + memberNewLambdaQueryWrapper.eq(FtbCultivatePromotionMemberNew::getUserId, userId); + memberNewLambdaQueryWrapper.eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); + FtbCultivatePromotionMemberNew memberNew = promotionMemberNewMapper.selectOne(memberNewLambdaQueryWrapper); + // 有通道但是没有选择 + if (ObjectUtil.isNotEmpty(memberNew)) { + // 当前学习阶段 + Integer currentLearningStage = memberNew.getCurrentLearningStage(); + // 如果当前阶段已经学习过了 则不需要重复学习 + if (currentLearningStage > level) { + return null; + } + } + + // 本岗校验本岗是否学习完成且参加了 考试和鉴定 + Integer learningMapSetting = promotionNew.getLearningMapSetting(); + if (level == 1) { + // 查询当前岗位是否存在学习课程 + Integer numberOfCourses = baseMapper.checkIfThePositionHasCourses(postId); + if (numberOfCourses == 0) { + throw new RuntimeException("当前阶段未绑定学习!无法进行下阶段学习"); + } + if (!hasStudyPostIdsList.contains(postId)) { + return "1"; + } + // 查询本岗岗位学习是否有考试 + List strings = baseMapper.checkIfThereAreExamsForThisPositionWithNew(postId, userId); + int exmCount = 0; + if (CollUtil.isNotEmpty(strings)) exmCount = strings.size(); + // [1:已经完成考试或者合格 2:未完成考试, 3:考试未合格] + Integer i = cultivateExamUserService.queryExamIsCompleteForUserIdAndPostion(userId, postId, + learningMapSetting); + if (exmCount > 0 && 2 == i) { + return "2"; + } else if (exmCount > 0 && 3 == i) { + return "4"; + } + // 或者实操鉴定 + // List isAPracticalAppraisalIds = baseMapper.checkWhetherThereIsAPracticalAppraisal(postId); + // 查询当前人实操鉴定是否是二次触发,如果二次触发就不需要校验 + List applyIds = baseMapper.checkWhetherThereIsAPracticalAppraisalWithNew(postId, userId); + Integer count = 0; + if (!applyIds.isEmpty()) + count = baseMapper.queryTheNumberOfPracticalAppraisalStudies(List.of(postId), userId, applyIds, learningMapSetting); + if (!applyIds.isEmpty() && learningMapSetting == 0 && count == 0) { + return "3"; + } else if (!applyIds.isEmpty() && learningMapSetting == 1 && count == 0) { + return "5"; + } + return null; + } + List> postsWitId = baseMapper.queryUserChosesPostsWitId(userId, level, promotionId); + // 选择的岗位id + List stringList = postsWitId.stream().map(map -> String.valueOf(map.get("postId"))).collect(Collectors.toList()); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getLevel, level); + lambdaQuery.last("limit 1"); + FtbCultivatePromotionPostNew postNew = ftbCultivatePromotionPostNewMapper.selectOne(lambdaQuery); + // 选学数量(根据选学数量控制学习数量) + Integer selectCourseNumber = postNew.getSelectCourseNumber(); + // 查询当前人已学习岗位信息 + List studyPostIds = hasStudyPostIdsList.stream().filter(stringList::contains).distinct().collect(Collectors.toList()); + // 空为已完成, 1 当前阶段未学习完成, 2岗位学习考试未完成, 3岗位学习实操鉴定未完成 + // 标记选学数量没有完成 + if (studyPostIds.size() < selectCourseNumber) { + return "1"; + } + List exmList = new ArrayList<>(); + List idemList = new ArrayList<>(); + int appCount = 0; + List strings = new ArrayList<>(); + for (String str : studyPostIds) { + List strings1 = baseMapper.checkIfThereAreExamsForThisPositionWithNew(str, userId); + if (CollUtil.isNotEmpty(strings1)) exmList.addAll(strings1); + List isAPracticalAppraisal = baseMapper.checkWhetherThereIsAPracticalAppraisalWithNew(str, userId); + if (!isAPracticalAppraisal.isEmpty()) { + appCount += isAPracticalAppraisal.size(); + idemList.add(str); + strings.addAll(isAPracticalAppraisal); + } + } + // 考试判断 + List collect = exmList.stream().map(str -> + cultivateExamUserService.queryExamIsCompleteForUserIdAndPostionWithNew(str, learningMapSetting) + ).collect(Collectors.toList()); + // [1:已经完成考试或者合格 2:未完成考试, 3:考试未合格] + if (!exmList.isEmpty() && collect.contains(2)) { + return "2"; + } else if (!exmList.isEmpty() && collect.contains(3)) { + return "4"; + } + // 实操鉴定判断 + Integer count = 0; + if (CollUtil.isNotEmpty(strings)) + count = baseMapper.queryTheNumberOfPracticalAppraisalStudies(idemList, userId, strings, learningMapSetting); + if (appCount > count && learningMapSetting == 0) { + return "3"; + } else if (appCount > count && learningMapSetting == 1) { + return "5"; + } + return null; + } + + + @Override + public Boolean whetherTheJobLearningMapHasChanged(String userId, String postId) { + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + return false; + } + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionNewMessage::getPromotionId, promotionId); + lambdaQuery.eq(FtbCultivatePromotionNewMessage::getUserId, userId); + lambdaQuery.eq(FtbCultivatePromotionNewMessage::getEnableMark, 0); + List messages = messageMapper.selectList(lambdaQuery); + if (CollUtil.isEmpty(messages)) { + return false; + } + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.in(BaseEntity::getId, messages.stream().map(BaseEntity::getId).collect(Collectors.toList())); + lambdaUpdate.set(FtbCultivatePromotionNewMessage::getEnableMark, 1); + messageMapper.update(null, lambdaUpdate); + return true; + } + + @Override + public FtbCultivatePromotionWithPersonelVO queryTrainData(String userId, String postId) { + FtbCultivatePromotionLevelMapVO vo = getLearnMapLevels(postId); + FtbCultivatePromotionWithPersonelVO withPersonelVO = FtbCultivatePromotionWithPersonelVO.covert(vo); + if (withPersonelVO == null) { + return null; + } + String promotionId = withPersonelVO.getPromotionId(); + // 岗位层级 + List postChannelLevel = vo.getPostChannelLevel(); + List list = new ArrayList<>(); + List personnelInfos = new ArrayList<>(); + for (FtbCultivatePromotionPostSelectCourseInfo postChannelLevelVO : postChannelLevel) { + Integer level = postChannelLevelVO.getLevel(); + FtbCultivatePromotionWithPersonnelInfo info = new FtbCultivatePromotionWithPersonnelInfo(); + List postInfo = new ArrayList<>(); + info.setLevel(level); + if (level == 1) { + // 当前岗位 + BigDecimal bigDecimal = statisticesService.jobLearningProgress(postId, userId); + list.add(bigDecimal); + info.setSelectCourseNumber(1); + FtbCultivateLearnMapInfoVO ftbCultivateLearnMapInfoVO = new FtbCultivateLearnMapInfoVO(); + ftbCultivateLearnMapInfoVO.setPostId(postId); +// PositionEntity positionEntity = positionApi.queryInfoById(postId); + PositionVO positionEntity = userApiV2Util.infoPosition(postId, null); + ftbCultivateLearnMapInfoVO.setPostName(positionEntity.getFullName()); + postInfo.add(ftbCultivateLearnMapInfoVO); + } else { + List> postsWitId = baseMapper.queryUserChosesPostsWitId(userId, level, promotionId); + for (Map map : postsWitId) { + String newPostId = String.valueOf(map.get("postId")); + BigDecimal bigDecimal = statisticesService.jobLearningProgress(newPostId, userId); + list.add(bigDecimal); + } + List ftbCultivateLearnMapInfoVOS = baseMapper.viewLearningMapBasedOnPosition(userId, postId, level); + info.setSelectCourseNumber(postChannelLevelVO.getSelectCourseNumber()); + postInfo.addAll(ftbCultivateLearnMapInfoVOS); + } + info.setPostInfo(postInfo); + personnelInfos.add(info); + + } + withPersonelVO.setLearnMapInfoVOS(personnelInfos); + // 学习范围 + Integer learningScope = withPersonelVO.getLearningScope(); + // 获取所有已经选择的岗位学习进度 + BigDecimal reduce = list.stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + if (reduce.compareTo(BigDecimal.ZERO) > 0) { + BigDecimal divide = reduce.divide(new BigDecimal(learningScope), 2, RoundingMode.HALF_UP); + BigDecimal learningProgress = divide.multiply(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_HALF_UP); + withPersonelVO.setLearningProgress(learningProgress); + } else { + withPersonelVO.setLearningProgress(BigDecimal.ZERO); + } + // 查询用户已学习岗位信息 + String hasStudyPostIds = baseMapper.queryThePositionsThatUsersHaveLearned(userId, + Collections.singletonList(1), null); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + List hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); +// List positionNameList = positionApi.getPositionName(hasStudyPostIdsList); + List positionNameList = userApiV2Util.listPositionDetailInfoByIds(hasStudyPostIdsList, null); + if (CollUtil.isNotEmpty(positionNameList)) { + List stringListNames = positionNameList.stream().map(PositionVO::getFullName).collect(Collectors.toList()); + withPersonelVO.setPositionLearned(stringListNames); + } + } + Map map = cultivatePerUtils.coverPersonalIds(Collections.singletonList(userId)); + withPersonelVO.setUserId(userId); + UserEntity userEntity = userApi.getInfoById(userId); + if (userEntity != null) { + withPersonelVO.setUserName(userEntity.getRealName()); + } + withPersonelVO.setSysUserId(map.get(userId)); + return withPersonelVO; + } + + @Override + public Boolean verifyingThePostWillRelearnTheMap(String postId) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPostId, postId); + List postNews = ftbCultivatePromotionPostNewMapper.selectList(lambdaQuery); + if (CollUtil.isNotEmpty(postNews)) { + return true; + } + return false; + } + + @Override + public FtbCultivatelearningProgressVO learningProgress(String userId, String postId) { + FtbCultivatelearningProgressVO ftbCultivatelearningProgressVO = new FtbCultivatelearningProgressVO(); + ftbCultivatelearningProgressVO.setIsConfigured(false); + // 是否配置了岗位学习课程 + List isConfigured = ftbCultivatePromotionPostNewMapper.isConfigured(postId); + if (!isConfigured.isEmpty()) { + ftbCultivatelearningProgressVO.setIsConfigured(true); + // 学习进度 + Integer learnCount = ftbCultivatePromotionPostNewMapper.learningProgress(userId, isConfigured, List.of(1)); + Integer totalCount = ftbCultivatePromotionPostNewMapper.learningProgress(userId, isConfigured, null); + BigDecimal bigDecimal = CultivatePerUtils.computeDivision(learnCount, totalCount); + ftbCultivatelearningProgressVO.setLearningProgress(bigDecimal); + } + return ftbCultivatelearningProgressVO; + } + + @Override + public List currentPersonChoosesAllPosition(String userId, String postId) { + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + return new ArrayList<>(); + } + List postIds = baseMapper.queryChosePostId(userId, promotionId); + if (CollUtil.isNotEmpty(postIds)) { + return postIds; + } + return new ArrayList<>(); + } + + @Override + public List queryByInitPostIdMapPostId(String postId) { + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(postId); + if (StringUtils.isEmpty(promotionId)) { + return new ArrayList<>(); + } + return baseMapper.queryMapPostId(promotionId); + } + + @Override + public List querySelectedPersonnelBasedOnPositionID(List postId) { + List strings = baseMapper.querySelectedPersonnelBasedOnPositionID(postId); +// for (String str : strings) { +// // 查询当前人有没有学习完该岗位课程 +// // 查询学习中的进行初始化 +// String postIds = baseMapper.queryThePositionsThatUsersHaveLearned(str, Collections.singletonList(2),postId); +// if (StringUtils.isNotEmpty(postIds) ){ +// List postIdsList = Arrays.stream(postIds.split(",")).distinct().collect(Collectors.toList()); +// if (postIdsList.contains(postId)){ +// usrIds.add(str); +// } +// } +// } + return strings; + } + + @Override + public Integer getMapPostNumber(String channelIniPoId) { + // 学习范围 + String promotionId = baseMapper.initialPositionQueryLearningMapPrimaryKey(channelIniPoId); + return baseMapper.queryStudyScope(promotionId); + } + + /** + * 获取对应的地图配置信息 + * + * @param integer + * @param collect + * @param promotionId + * @return + */ + @NotNull + private FtbCultivatePromotionPostSelectCourseInfo getCourseInfo(Integer integer, Map> collect, String promotionId) { + FtbCultivatePromotionPostSelectCourseInfo selectCourseInfo = new FtbCultivatePromotionPostSelectCourseInfo(); + List dbPost = collect.get(integer); + selectCourseInfo.setLevel(integer); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.last("limit 1"); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + lambdaQuery.eq(FtbCultivatePromotionPostNew::getLevel, integer); + FtbCultivatePromotionPostNew postNew = ftbCultivatePromotionPostNewMapper.selectOne(lambdaQuery); + selectCourseInfo.setSelectCourseNumber(postNew.getSelectCourseNumber()); + // 岗位详情 + List stringList = dbPost.stream().map(FtbCultivatePromotionPostNew::getPostName).collect(Collectors.toList()); + selectCourseInfo.setPostInfo(stringList); + return selectCourseInfo; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostApplyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostApplyServiceImpl.java new file mode 100644 index 0000000..e092cc3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostApplyServiceImpl.java @@ -0,0 +1,313 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.FtbCultivatePositionIdentifyResultMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionPostApplyMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyCreateDto; +import jnpf.model.cultivate.dto.apply.FtbCultivatePromotionPostApplyDto; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.apply.FtbCultivatePromotionPostApply; +import jnpf.model.cultivate.po.org.FtbPositionGradesInfoBoundVO; +import jnpf.model.cultivate.po.position.FtbCultivatePositionIdentifyResult; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPost; +import jnpf.model.cultivate.resp.UserExamDetailVo; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyVO; +import jnpf.model.cultivate.vo.apply.FtbCultivatePromotionPostApplyWithPerVO; +import jnpf.model.cultivate.vo.course.web.PromotionChannelLearnCourseVO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.dto.v2.UserBoundInfoDTO; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.user.UserInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePromotionPostApplyServiceImpl extends ServiceImpl implements FtbCultivatePromotionPostApplyService { + + + @Resource + private FtbCultivatePromotionPostService postService; + + @Resource + private FtbCultivatePromotionService promotionService; + + @Resource + private FtbCultivateFileService ftbCultivateFileService; + + @Resource + private FtbCultivateCourseService ftbCultivateCourseService; + + @Resource + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Resource + private FtbCultivatePositionIdentifyResultMapper ftbCultivatePositionIdentifyResultMapper; + + @Autowired + private UserApi userApi; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public void initiateAPromotionApplication(FtbCultivatePromotionPostApplyCreateDto createDto) { + FtbCultivatePromotionPostApply promotionPostApply = FtbCultivatePromotionPostApplyCreateDto. + coverFtbCultivatePromotionPostApplyCreateDto(createDto); + //晋升时需要判断是否不处于同一个组织 + if (!promotionPostApply.getOrgId().equals(promotionPostApply.getOrgCurrentId())) { + throw new RuntimeException("当前岗位和晋升岗位不在同一组织无法进行申请!"); + } + if (promotionPostApply.getUserId().equals(promotionPostApply.getReviewUserId())) { + throw new RuntimeException("当前审批人无法指定自己,无法进行申请!"); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbCultivatePromotionPostApply::getUserId, promotionPostApply.getUserId()); + wrapper.eq(FtbCultivatePromotionPostApply::getState, 0); + FtbCultivatePromotionPostApply postApply = baseMapper.selectOne(wrapper); + if (ObjectUtil.isNotNull(postApply)) { + throw new RuntimeException("您上次提交的晋升申请还在审核中,请等待审核完成再提交!"); + } + // 指定审批人web端 + if (promotionPostApply.getReviewUserId() == null) { + // app端默认为当前主管 + UserInfoVO apiBossByUser = userApiV2Util.getBossByUserId(promotionPostApply.getUserId(),null); + if (ObjectUtil.isNull(apiBossByUser)) { + throw new RuntimeException("当前用户未分配上级,无法进行申请!"); + } + // 获取当前用户上级 + promotionPostApply.setReviewUserId(apiBossByUser.getId()); + } + List positionGradesInfoBoundVOS = getPositionGradesInfoBoundVOS(promotionPostApply.getUserId(), promotionPostApply.getOrgId()); + if (ObjectUtil.isEmpty(positionGradesInfoBoundVOS)) { + throw new RuntimeException("当前用户未分配岗位,无法进行申请!"); + } + Set collect = positionGradesInfoBoundVOS.stream().map(FtbPositionGradesInfoBoundVO::getPositionGradesId).collect(Collectors.toSet()); + if (!collect.contains(promotionPostApply.getPostRankId())) { + throw new RuntimeException("当前申请变更岗位,在个人岗位中不存在请重新选择进行重试!"); + } + // 根据管理id查询上级 + UserEntity infoById = userApi.getInfoById(promotionPostApply.getReviewUserId()); + if (ObjectUtil.isNotEmpty(infoById)) { + promotionPostApply.setReviewUserName(infoById.getRealName()); + } else { + throw new RuntimeException("当前用户未分配上级,无法进行申请!"); + } + if (StringUtils.isNotEmpty(promotionPostApply.getReviewComment()) && promotionPostApply.getReviewComment().length() > 500) { + throw new RuntimeException("审核评语过长,不能超过500个字,请修改后重试!"); + } + if (StringUtils.isNotEmpty(promotionPostApply.getPromotionReason()) && promotionPostApply.getPromotionReason().length() > 500) { + throw new RuntimeException("晋升原因过长,不能超过500个字,请修改后重试!"); + } + promotionPostApply.setState(0); + this.save(promotionPostApply); + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(createDto.getFiles()) + .businessTypeID(promotionPostApply.getId()) + .type(FileEventDTO.FileType.JOB_PROMOTION_APPLICATION) + .build())); + } + + /** + * 过滤当前组织用户 + * + * @param userId + * @param orgId + * @return + */ + @NotNull + public List getPositionGradesInfoBoundVOS(String userId, String orgId) { +// List orgPositionInfoList = positionApi.getOrgPositionInfoList(userId); + List orgPositionInfoList = new ArrayList<>(); + Map> userPrimaryBoundBatchCompatible = userApiV2Util.getUserPrimaryBoundBatchCompatible(List.of(userId), null); + if(CollUtil.isNotEmpty(userPrimaryBoundBatchCompatible)){ + orgPositionInfoList = userPrimaryBoundBatchCompatible.get(userId); + } + List list = new ArrayList<>(); + if (CollUtil.isNotEmpty(orgPositionInfoList)) { + orgPositionInfoList.forEach(item -> { + FtbPositionGradesInfoBoundVO post = new FtbPositionGradesInfoBoundVO(); + if (StringUtils.isEmpty(item.getOrganizeId())) { + return; + } + List organizeIds = List.of(item.getOrganizeId()); + List organizeNames = List.of(item.getOrganizeName()); + for (int i = 0; i < organizeIds.size(); i++) { + if (organizeIds.get(i).equals(orgId)) { + post.setOrganizeId(organizeIds.get(i)); + post.setOrganizeName(organizeNames.get(i)); + post.setPositionId(item.getPositionId()); + post.setPositionName(item.getPositionName()); + post.setPositionGradesName(item.getGradeName()); + post.setPositionGradesId(item.getGradeId()); + post.setUserId(item.getId()); + list.add(post); + } + } + }); + } + return list; + } + + + @Override + public PageListVO getList(FtbCultivatePromotionPostApplyDto dto, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + //查询当前申请列表 + UserInfo user = UserProvider.getUser(); + String userId = user.getUserId(); + dto.setUserId(userId); + Page mapperList = baseMapper.getListInfo(page, dto); + //根据对应的人岗位获取对应的不同 晋升岗位通道 + mapperList.getRecords().forEach(item -> { + // 当前岗位的id 反查询对应的晋升通道id + String postId = item.getPostId(); + LambdaQueryChainWrapper lambdaQuery = postService.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPost::getPostId, postId); + // 根据岗位确定该岗位启用的晋升通道 + List promotionIds = lambdaQuery.list().stream().map(FtbCultivatePromotionPost::getPromotionId).collect(Collectors.toList()); + item.setChannelCount(promotionIds.size()); + if (CollUtil.isNotEmpty(promotionIds)) { + //获取对应的查询信息 + List promotionVOList = promotionService.queryListByIds(promotionIds); + item.setChannelInfos(promotionVOList); + } + }); + return CultivatePage.coverPageList(mapperList); + } + + @Override + @Transactional(rollbackFor = {Exception.class, RuntimeException.class}) + public void auditPromotionPostApplication(FtbCultivatePromotionPostApplyDto dto) { + if (dto.getState().equals(2) && !ObjectUtil.isNotEmpty(dto.getReviewComment())) { + throw new RuntimeException("请填写审核意见!"); + } + FtbCultivatePromotionPostApply postApply = this.getById(dto.getApplyId()); + if (ObjectUtil.isNull(postApply)) { + throw new RuntimeException("该申请不存在!"); + } + UserInfo user = UserProvider.getUser(); + String userId = user.getUserId(); + if (!userId.equals(postApply.getReviewUserId())) { + throw new RuntimeException("当前指定审批人与登录用户不合符,无法审核该岗位晋升申请!"); + } + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + //审核评语 + wrapper.set(FtbCultivatePromotionPostApply::getReviewComment, dto.getReviewComment()); + wrapper.set(FtbCultivatePromotionPostApply::getState, dto.getState()); + wrapper.eq(FtbCultivatePromotionPostApply::getId, dto.getApplyId()); + int update = baseMapper.update(new FtbCultivatePromotionPostApply(), wrapper); + if (update == 0) { + throw new RuntimeException("审核失败!"); + } + // 申请组织接口额变更 + // 记录通过时间和历史岗位职等及对应的在岗时长, + // 同步至员工主页的用户成长板块 + // (相关功能模块:人事入转调离,若本需求上线时人事未上线, + // 则将晋升申请放置在培训板块;待人事入转调离上线后再进行迁移 + if (dto.getState().equals(1)) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostApply::getId, dto.getApplyId()); + FtbCultivatePromotionPostApply promotionPostApply = baseMapper.selectOne(lambdaQuery); + // 当前岗位职等 + String postRankId = promotionPostApply.getPostRankId(); + // 晋升岗位职等 + String postRankPromotionId = promotionPostApply.getPostRankPromotionId(); + // 变更用户id + String userId1 = promotionPostApply.getUserId(); + String orgId = postApply.getOrgId(); + String orgCurrentId = postApply.getOrgCurrentId(); + // 晋升通过后,变更当前人员岗位职等, +// userApiV2Util.updateUserBoundInfo(userId1, new UserBoundInfoDTO(orgCurrentId, postRankPromotionId)); + Boolean b = userApi.changeUserPosition(userId1, postRankId, postRankPromotionId, orgId, orgCurrentId); + if (!b) { + throw new RuntimeException("审核变更岗位失败!"); + } + } + } + + @Override + public FtbCultivatePromotionPostApplyWithPerVO viewPromotionApplications(FtbCultivatePromotionPostApplyDto dto, String appFlag) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbCultivatePromotionPostApply::getId, dto.getApplyId()); + FtbCultivatePromotionPostApply postApply = baseMapper.selectOne(wrapper); + FtbCultivatePromotionPostApplyWithPerVO withPerVO = FtbCultivatePromotionPostApplyWithPerVO.coverFtbCultivatePromotionPostApplyWithPerVO(postApply); + // 查询当前申请时上传的文件信息 + LambdaQueryWrapper wrapperFile = Wrappers.lambdaQuery(); + wrapperFile.eq(FtbCultivateFile::getBusinessId, postApply.getId()); + LambdaQueryChainWrapper fileLambdaQueryChainWrapper = ftbCultivateFileService.lambdaQuery(); + fileLambdaQueryChainWrapper.eq(FtbCultivateFile::getBusinessId, postApply.getId()); + List fileLists = ftbCultivateFileService.list(wrapperFile); + List fileVOS = fileLists.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + withPerVO.setFiles(fileVOS); + if (appFlag.equals("1")) { + return withPerVO; + } + String userId = withPerVO.getUserId(); + // 考试成绩列表 + List userExamDetailVos = ftbCultivateExamUserService.queryCompleteExamListForUserId(userId); + withPerVO.setAListOfTestScores(userExamDetailVos); + // 实操鉴定列表 + LambdaQueryWrapper wrapperIdentify = Wrappers.lambdaQuery(); + wrapperIdentify.select(FtbCultivatePositionIdentifyResult::getIdentifyRecordId); + wrapperIdentify.eq(FtbCultivatePositionIdentifyResult::getUserId, userId); + wrapperIdentify.eq(FtbCultivatePositionIdentifyResult::getPostRankId, postApply.getPostRankId()); + List identifyResultList = ftbCultivatePositionIdentifyResultMapper.selectList(wrapperIdentify); + List identifyRecordIds = identifyResultList.stream().map(FtbCultivatePositionIdentifyResult::getIdentifyRecordId).distinct().collect(Collectors.toList()); + List userIdentifyInfoApi = new ArrayList<>(); + if (CollUtil.isNotEmpty(identifyRecordIds)) { + userIdentifyInfoApi = cultivateIdentifyApplyService.getUserIdentifyInfoApi(identifyRecordIds); + } + withPerVO.setListOfPracticalQualifications(userIdentifyInfoApi); + // 已学习课程列表 + // 当前申请人id + List promotionChannelLearnCourseVOS = ftbCultivateCourseService.promotionPathwayCourses(userId); + withPerVO.setHaveStudyCourseLists(promotionChannelLearnCourseVOS); + return withPerVO; + } + + @Override + public Boolean isThereAnAppForThePosLevel(String postId, String grandId, String userId) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionPostApply::getUserId, userId); + lambdaQuery.eq(FtbCultivatePromotionPostApply::getPostId, postId); + lambdaQuery.eq(FtbCultivatePromotionPostApply::getPostRankId, grandId); + lambdaQuery.eq(FtbCultivatePromotionPostApply::getState, 0); + List ftbCultivatePromotionPostApplies = baseMapper.selectList(lambdaQuery); + return !CollUtil.isNotEmpty(ftbCultivatePromotionPostApplies); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostServiceImpl.java new file mode 100644 index 0000000..7f7e355 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionPostServiceImpl.java @@ -0,0 +1,168 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePromotionMemberMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionPostMapper; +import jnpf.cultivate.service.FtbCultivatePromotionMemberService; +import jnpf.cultivate.service.FtbCultivatePromotionPostService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionForAppDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionMemberDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMember; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPost; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionForAppVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionMemberVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionPostVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.permission.UserApi; +import jnpf.permission.model.user.UserInfoVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FtbCultivatePromotionPostServiceImpl extends ServiceImpl implements FtbCultivatePromotionPostService { + + private final FtbCultivatePromotionMemberMapper memberMapper; + + private final FtbCultivatePromotionMemberService memberService; + private final UserApiV2Util userApiV2Util; + + @Override + public void savePost(FtbCultivatePromotionPost creatDto) { + baseMapper.insert(creatDto); + + } + + @Override + public List getList(String promotionId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("F_PromotionId", promotionId); + queryWrapper.orderByAsc("F_Level"); + return baseMapper.selectList(queryWrapper); + } + + @Override + public void updatePost(FtbCultivatePromotionPost dto) { + baseMapper.updateById(dto); + } + + @Override + public FtbCultivatePromotionForAppVO queryThePromotedChannelsThatAreInitiated(String userId, String postId) { + FtbCultivatePromotionForAppVO result = new FtbCultivatePromotionForAppVO(); + // 根据userId查询已启用的晋升通道 + FtbCultivatePromotionVO promotionVO = memberMapper.queryPromotionByUser(userId, postId); + String promotionVOId=null; + if (promotionVO != null) { + result.setOptPro(promotionVO); + //已启用的id + promotionVOId = promotionVO.getId(); + } + // 根据岗位查询相同岗位可选的晋升通道 + List baseList = baseMapper.queryPromonChanOfTheSamePosition(postId, promotionVOId); + result.setOtherOptPro(baseList); + return result; + } + + @Override + public String changeTheEnablementStatus(FtbCultivatePromotionForAppDto forAppDto) { + // 标记是否为下属设置启用晋升通道 + FtbCultivatePromotionMember promotionMember = getFtbCultivatePromotionMember(forAppDto); + if ("1".equals(forAppDto.getFlag())) { + // 当前这个成员没有开启晋升通道 + if (promotionMember == null){ + newPromotionChannels(forAppDto); + return null; + } + }else { + if (promotionMember == null) { + newPromotionChannels(forAppDto); + return null; + } + if (1 == promotionMember.getState()) { + //您好,若想更换晋升通道,请与您的直接主管“主管姓名”联系变更修改。 + UserInfoVO bossByUserId = userApiV2Util.getBossByUserId(forAppDto.getUserId(),null); + return ("您好,若想更换晋升通道,请与您的直接主管“" +bossByUserId.getRealName() + "”联系变更修改。"); + } + } + // 更改当前晋升通道 + // 更换启用其他的晋升通道 + promotionMember.setPromotionId(forAppDto.getPromotionId()); + promotionMember.setEnableStatus(1); + promotionMember.setUserId(forAppDto.getUserId()); + memberMapper.updateById(promotionMember); + return null; + } + + /** + * 用户自己启用晋升通道 + * @param forAppDto + */ + private void newPromotionChannels(FtbCultivatePromotionForAppDto forAppDto) { + FtbCultivatePromotionMemberDto dto = new FtbCultivatePromotionMemberDto(); + List strings = Collections.singletonList(forAppDto.getUserId()); + List userIds = new ArrayList<>(); + FtbCultivatePromotionMemberDto.PromotionUser promotionUser = new FtbCultivatePromotionMemberDto.PromotionUser(); + promotionUser.setUserId(forAppDto.getUserId()); + promotionUser.setUserName(forAppDto.getUserName()); + promotionUser.setOrgId(forAppDto.getOrgId()); + promotionUser.setOrgName(forAppDto.getOrgName()); + promotionUser.setPositionId(forAppDto.getPositionId()); + promotionUser.setPositionName(forAppDto.getPositionName()); + promotionUser.setGradeId(forAppDto.getGradeId()); + promotionUser.setGradeName(forAppDto.getGradeName()); + userIds.add(promotionUser); + dto.setUserIds(userIds); + dto.setPromotionId(forAppDto.getPromotionId()); + memberService.addMembersToProChannel(dto); + } + + @Override + public void changeWhetherTheChangeIsAllowed(FtbCultivatePromotionForAppDto forAppDto) { + FtbCultivatePromotionMember promotionMember = getFtbCultivatePromotionMember(forAppDto); + // 查询是否为下属 + if (promotionMember == null ){ + throw new RuntimeException("当前用户暂时没有开启晋升通道请核对后重试"); + } + promotionMember.setState(forAppDto.getState()); + memberMapper.updateById(promotionMember); + } + + @Override + public boolean checkWhetherThePromotionChannelIsEnabled(String userId, String postId) { + FtbCultivatePromotionVO promotionVO = memberMapper.queryPromotionByUser(userId, postId); + if (ObjectUtil.isNotEmpty(promotionVO)) { + return true; + } + return false; + } + + @Override + public Integer userIsAllowed(String userId) { + FtbCultivatePromotionMemberVO promotionMember = memberMapper.queryUserInfo(userId); + // 默认是可以启用晋升通道 + if (promotionMember == null){ + return 0; + } + return promotionMember.getState(); + } + + @Override + public List queryPostHasExist() { + return memberMapper.queryPostHasExist(); + } + + private FtbCultivatePromotionMember getFtbCultivatePromotionMember(FtbCultivatePromotionForAppDto forAppDto) { + //查询出当前启用的晋升通道id + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePromotionMember::getUserId, forAppDto.getUserId()); + return memberMapper.selectOne(queryWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionServiceImpl.java new file mode 100644 index 0000000..abf09ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivatePromotionServiceImpl.java @@ -0,0 +1,603 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivatePromotionMemberService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.service.FtbCultivatePromotionPostService; +import jnpf.cultivate.service.FtbCultivatePromotionService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionCreatDto; +import jnpf.model.cultivate.dto.promotion.FtbCultivatePromotionDto; +import jnpf.model.cultivate.dto.promotion.FtbMapsOrgWisdomStatisticDTO; +import jnpf.model.cultivate.dto.promotion.FtbMapsPersonWisdomStatisticDTO; +import jnpf.model.cultivate.entiy.BaseEntity; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExam; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotion; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMember; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPost; +import jnpf.model.cultivate.vo.certificate.FtbCertificateOrgWisdomStatisticVO; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.promotion.*; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.permission.vo.v2.user.UserRelationBaseVO; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePromotionServiceImpl extends ServiceImpl implements FtbCultivatePromotionService { + private static final List EMPTY_USER_LIST = List.of("-1"); + + @Resource + private FtbCultivatePromotionMapper promotionMapper; + + @Resource + private FtbCultivatePromotionPostService postService; + + @Resource + private FtbCultivatePromotionMemberService promotionMemberService; + @Resource + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + @Resource + private FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + @Resource + private FtbCultivatePositionExamIdentifyMapper ftbCultivatePositionExamIdentifyMapper; + @Resource + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Resource + PositionApi positionApi; + + @Autowired + private CultivatePerUtils cultivatePerUtils; + + @Autowired + private FtbCultivatePromotionNewService promotionNewService; + + @Autowired + private FtbCultivatePromotionNewMapper ftbCultivatePromotionNewMapper; + + @Autowired + UserApi userApi; + @Autowired + private UserApiV2Util userApiV2Util; + + + @Override + public PageListVO getList(CultivatePage oldPage, FtbCultivatePromotionDto dto) { + Page page = oldPage.coverCultivatePage(); + page = promotionMapper.getList(page, dto); + return CultivatePage.coverPageList(page); + } + + @Override + public void VerifyPromotion(String postId, String gradeId) { + // 岗位池判断岗位是否存在晋升通道 + if (StringUtil.isNotEmpty(postId)) { + FtbCultivatePromotionDto dto = new FtbCultivatePromotionDto(); + dto.setPostId(postId); + Page page = new Page<>(); + // v1.1进行调整使用最新的岗位地图 + Page list = ftbCultivatePromotionNewMapper.getList(page, dto); + // Page list = promotionMapper.getList(page, dto); + if (CollUtil.isNotEmpty(list.getRecords())) { + throw new RuntimeException("该岗位存在于学习地图内,不允许删除"); + } + // 该岗位已绑定岗位学习,不允许删除 + try { + // ActionResult infoNew = positionApi.getInfoNew(postId); +// if (infoNew.getCode().equals(HttpStatus.BAD_REQUEST)){ +// throw new RuntimeException(infoNew.getMsg()); +// } +// PositionInfoNewVO data = infoNew.getData(); + List gradeVOS = userApiV2Util.listGrades(postId, null); + List collect = gradeVOS.stream().distinct().map(GradeVO::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)) { + postLevelDeletionVerification(collect); + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + if (StringUtil.isNotEmpty(gradeId)) { + postLevelDeletionVerification(List.of(gradeId)); + } + } + + @Override + public List queryListByIds(List promotionIds) { + + return promotionMapper.queryListByIds(promotionIds); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(FtbCultivatePromotionCreatDto creatDto) { + if (ObjectUtil.isNotEmpty(creatDto.getId())) { + throw new RuntimeException("新增失败"); + } + extracted(creatDto); + } + + private void extracted(FtbCultivatePromotionCreatDto creatDto) { + FtbCultivatePromotion cultivatePromotion = FtbCultivatePromotionCreatDto. + coverFtbCultivatePromotionCreatDto(creatDto); + // 新增岗位通道 + String promotionBusinessId = SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.PROMOTION); + //设置对应的key + cultivatePromotion.setPromotionBusinessId(promotionBusinessId); + promotionMapper.insert(cultivatePromotion); + //晋升通道id + String promotionId = cultivatePromotion.getId(); + //新增对应的岗位级别 + List postVOList = creatDto.getPostChannelLevel(); + Set postIds = postVOList.stream().map(FtbCultivatePromotionPostVO::getPostId).collect(Collectors.toSet()); + if (postIds.size() != postVOList.size()) { + throw new RuntimeException("职位列表中存在重复的岗位,请重新检查!"); + } + // 添加职位数据库校验是否为相同职位相同级别 + whetherThereIsTheSamePromotionChannel(postIds); + // 添加通道岗位 + savePromotion(postVOList, promotionId); + } + + /** + * 校验是否存在相同的晋升通道 + * + * @param postIds + */ + private void whetherThereIsTheSamePromotionChannel(Set postIds) { + List list = postService.queryPostHasExist(); + // 不存在数据的情况 + if (ObjectUtil.isEmpty(list)) { + return; + } + Map> postVOMap = list.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostVO::getPromotionId, Collectors.toList())); + Set strings = postVOMap.keySet(); + for (String str : strings) { + List postVOListOld = postVOMap.get(str); + Set postIdsOld = postVOListOld.stream().map(FtbCultivatePromotionPostVO::getPostId).collect(Collectors.toSet()); + if (postIds.equals(postIdsOld)) { + throw new RuntimeException("系统已存在相同岗位晋升通道,请重新检查!"); + } + } + } + + /** + * 新增数据 + * + * @param postVOList 岗位列表 + * @param promotionId 通道主键 + */ + private void savePromotion(List postVOList, String promotionId) { + postVOList.forEach(item -> { + FtbCultivatePromotionPost ftbCultivatePromotionPost = FtbCultivatePromotionPostVO.coverFtbCultivatePromotionPostVO(item); + //设置晋升通道id + ftbCultivatePromotionPost.setPromotionId(promotionId); + postService.save(ftbCultivatePromotionPost); + }); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void addList(List creatDtoList) { + creatDtoList.forEach(this::extracted); + } + + @Override + public void postLevelDeletionVerification(List postRankId) { + if (CollectionUtils.isEmpty(postRankId)) { + return; + } + // 岗位id获取 + List positionGradesInfoBoundVOS = positionApi.listPositionGradesInfoByPositionGrades(postRankId.get(0)); + if (CollUtil.isNotEmpty(positionGradesInfoBoundVOS)) { + String positionId = positionGradesInfoBoundVOS.get(0).getPositionId(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + queryWrapper.eq(FtbCultivatePosition::getPostId, positionId); + if (ftbCultivatePositionMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("该岗位已绑定岗位学习,不允许删除!"); + } + } + // 岗位学习课程 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + queryWrapper.in(FtbCultivatePositionCourse::getPostRankId, postRankId); + if (ftbCultivatePositionCourseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("该岗位已绑定岗位学习,不允许删除!"); + } + // 岗位考试 + LambdaQueryWrapper ftbCultivatePositionExamLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionExamLambdaQueryWrapper.eq(FtbCultivatePositionExam::getEnabledMark, 0); + ftbCultivatePositionExamLambdaQueryWrapper.in(FtbCultivatePositionExam::getPostRankId, postRankId); + if (ftbCultivatePositionExamMapper.selectCount(ftbCultivatePositionExamLambdaQueryWrapper) > 0) { + throw new RuntimeException("该岗位已绑定岗位考试,不允许删除!"); + } + // 岗位实操鉴定 + LambdaQueryWrapper ftbCultivatePositionExamIdentifyLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivatePositionExamIdentifyLambdaQueryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + ftbCultivatePositionExamIdentifyLambdaQueryWrapper.in(FtbCultivatePositionExamIdentify::getPostRankId, postRankId); + if (ftbCultivatePositionExamIdentifyMapper.selectCount(ftbCultivatePositionExamIdentifyLambdaQueryWrapper) > 0) { + throw new RuntimeException("该岗位已绑定岗位实操鉴定,不允许删除!"); + } + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbMapsOrgWisdomStatisticDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO organizationListStatistics(FtbMapsOrgWisdomStatisticDTO statisticDTO) { +// Page page = statisticDTO.coverCultivatePage(); + List orgIds = new ArrayList<>(); + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return returnEmptyList(statisticDTO); + } + + if ("1".equals(statisticDTO.getSelectLogo()) && CollUtil.isNotEmpty(statisticDTO.getSelectOrganization())) { + List allOrganize = cultivatePerUtils.batchAllOrganize(statisticDTO.getSelectOrganization()); + allOrganize.addAll(statisticDTO.getSelectOrganization()); + List intersection = UserApiV2Util.getIntersection(powerOrgList, allOrganize); + if (CollUtil.isEmpty(intersection)) { + returnEmptyList(statisticDTO); + } + orgIds = intersection; + } else if (CollUtil.isNotEmpty(statisticDTO.getSelectOrganization())) { + List intersection = UserApiV2Util.getIntersection(powerOrgList, statisticDTO.getSelectOrganization()); + if (CollUtil.isEmpty(intersection)) { + returnEmptyList(statisticDTO); + } + orgIds = intersection; + } else if (CollUtil.isEmpty(statisticDTO.getSelectOrganization())) { + orgIds = powerOrgList; + } + Map orgMap = userApiV2Util.organizesByOrganizeIdsReturenMap(orgIds, null); + //查询学习地图 + List mapLists = baseMapper.queryStudyMapList(statisticDTO); + if (CollUtil.isEmpty(mapLists)) { + return returnEmptyList(statisticDTO); + } + List queryPromotionIds = mapLists.stream().map(FtbCultivateMapSimpleDto::getPromotionId).collect(Collectors.toList()); + List studyUserDtoList = baseMapper.queryStudyUser(statisticDTO, queryPromotionIds); + if (CollUtil.isEmpty(studyUserDtoList)) { + return returnEmptyList(statisticDTO); + } +// List mapPostNum = baseMapper.queryMapPostNum(queryPromotionIds); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(studyUserDtoList.stream().map(FtbCultivateMapStudyUserDto::getUserId).collect(Collectors.toList()), null); + List ret = buildReturnList(orgMap, mapLists, studyUserDtoList, userMap); + if (CollUtil.isEmpty(ret)) { + return returnEmptyList(statisticDTO); + } +// statisticDTO.setSelectOrganization(orgIds); +// page = baseMapper.organizationListStatistics(statisticDTO, page); +// List records = page.getRecords(); +// records.forEach(item -> { +// String id = item.getId(); +// FtbCultivatePromotionLevelMapVO learnMapLevels = promotionNewService.getLearnMapLevelsWithMapId(id); +// ActionResult userCount = userApi.getUserCount(item.getEnableOrgId(), item.getIniPostId()); +// if (ObjectUtil.isNotEmpty(userCount)) { +// Long data = userCount.getData(); +// item.setCurrentNum(data); +// } +// item.setNumberOfMapPositions(learnMapLevels.getLearningScope()); +// item.setMapLevel(learnMapLevels); +// OrganizeGeneralDetailVO organizeGeneralDetailVO = orgMap.get(item.getEnableOrgId()); +// if (organizeGeneralDetailVO != null) { +// item.setEnableOrgName(organizeGeneralDetailVO.getName()); +// } +// }); +// return CultivatePage.coverPageList(page); + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(ret.size()); + pageInfo.setPagination(pagination); + + List> batchList = UserApiV2Util.batchList(ret, (int) statisticDTO.getPageSize()); + if (statisticDTO.getCurrentPage() <= batchList.size()) { + pageInfo.setList(batchList.get((int) statisticDTO.getCurrentPage() - 1)); + } else { + pageInfo.setList(new ArrayList<>()); + } + + return pageInfo; + } + + /** + * 构建学习地图组织维度统计列表 + * + * @param orgMap 组织信息映射,key为组织ID,value为组织详情 + * @param mapLists 学习地图列表 + * @param studyUserDtoList 学习用户列表 + * @param userMap 用户信息映射,key为用户ID,value为用户详情 + * @return 学习地图组织维度统计列表 + */ + private List buildReturnList(Map orgMap, List mapLists, + List studyUserDtoList, Map userMap) { + List ret = new ArrayList<>(); + //按照学习地图id分组 + Map> mapProUserListMap = studyUserDtoList.stream().collect(Collectors.groupingBy(FtbCultivateMapStudyUserDto::getPromotionId)); + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(mapLists.stream().map(FtbCultivateMapSimpleDto::getPostId).collect(Collectors.toList()), null); + for (FtbCultivateMapSimpleDto mapList : mapLists) { + List studyList = mapProUserListMap.get(mapList.getPromotionId()); + if (CollUtil.isEmpty(studyList)) { + continue; + } + List tempUser = new ArrayList<>(); + for (FtbCultivateMapStudyUserDto ftbCultivateMapStudyUserDto : studyList) { + UserBoundVO userBoundVO = userMap.get(ftbCultivateMapStudyUserDto.getUserId()); + if (userBoundVO != null) { + tempUser.add(userBoundVO); + } + } + if (CollUtil.isEmpty(tempUser)) { + continue; + } + FtbCultivatePromotionLevelMapVO learnMapLevels = promotionNewService.getLearnMapLevelsWithMapId(mapList.getPromotionId()); + //按照组织分组 + Map> orgUserMap = tempUser.stream().collect(Collectors.groupingBy(UserBoundVO::getOrganizeId)); + for (Map.Entry> stringListEntry : orgUserMap.entrySet()) { + String orgId = stringListEntry.getKey(); + OrganizeGeneralDetailVO organizeGeneralDetailVO = orgMap.get(orgId); + if (organizeGeneralDetailVO == null) { + continue; + } + List value = stringListEntry.getValue(); + FtbCultivateMapsOrgWisdomStatisticVO vo = new FtbCultivateMapsOrgWisdomStatisticVO(); + vo.setMapName(mapList.getPromotionName()); + vo.setMapID(mapList.getPromotionBusinessId()); + vo.setCreationTime(mapList.getCreatorTime()); + vo.setEnableOrgId(orgId); + vo.setEnableOrgName(organizeGeneralDetailVO.getName()); + PositionVO positionVO = stringPositionVOMap.get(mapList.getPostId()); + if (positionVO != null) { + vo.setIniPosition(positionVO.getFullName()); + } + vo.setNumberOfEmplo(value.size() + ""); + + vo.setId(mapList.getPromotionId()); + vo.setIniPostId(mapList.getPostId()); + vo.setNumberOfMapPositions(learnMapLevels.getLearningScope()); + vo.setMapLevel(learnMapLevels); + List userListForOrgIdAndPositionId = userApiV2Util.getUserListForOrgIdAndPositionId(orgId, mapList.getPostId(), null); + if (CollUtil.isNotEmpty(userListForOrgIdAndPositionId)) { + vo.setCurrentNum(Long.valueOf(userListForOrgIdAndPositionId.size())); + } + ret.add(vo); + } + + } + return ret; + } + + /** + * 返回一个空的分页结果列表 + * + * @param statisticDTO 统计查询参数DTO + * @return 空的分页结果列表 + */ + private PageListVO returnEmptyList(FtbMapsPersonWisdomStatisticDTO statisticDTO) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(statisticDTO.getCurrentPage()); + pagination.setPageSize(statisticDTO.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + @Override + public PageListVO personListStatistics(FtbMapsPersonWisdomStatisticDTO personWisdomStatisticDTO) { + Page page = personWisdomStatisticDTO.coverCultivatePage(); + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getSelectOrganization())) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(personWisdomStatisticDTO.getSelectOrganization(), null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return returnEmptyList(personWisdomStatisticDTO); + } + if (CollUtil.isEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + personWisdomStatisticDTO.setSelectPeoples(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } else { + List intersection = UserApiV2Util.getIntersection(personWisdomStatisticDTO.getSelectPeoples(), userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + if (CollUtil.isEmpty(intersection)) { + return returnEmptyList(personWisdomStatisticDTO); + } + personWisdomStatisticDTO.setSelectPeoples(intersection); + } + } + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(personWisdomStatisticDTO.getSelectPeoples())) { + List intersection = UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), personWisdomStatisticDTO.getSelectPeoples()); + if (CollUtil.isNotEmpty(intersection)) { + personWisdomStatisticDTO.setSelectPeoples(intersection); + } else { + return returnEmptyList(personWisdomStatisticDTO); + } + } else { + personWisdomStatisticDTO.setSelectPeoples(innerPowerUserVO.getUserIds()); + } + } else if (innerPowerUserVO.getCode() == 2) { + return returnEmptyList(personWisdomStatisticDTO); + } + + + page = baseMapper.personListStatistics(personWisdomStatisticDTO, page); + List records = page.getRecords(); + List employeeIdList = page.getRecords().stream().map(FtbCultivateMapsPersonWisdomStatisticVO::getEmployeeID).collect(Collectors.toList()); + // 获取转换id + Map perMap = cultivatePerUtils.coverPersonalIds(employeeIdList); + + if (CollUtil.isNotEmpty(records)) { + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(records.stream().map(FtbCultivateMapsPersonWisdomStatisticVO::getEmployeeID).collect(Collectors.toList()), null); + for (FtbCultivateMapsPersonWisdomStatisticVO item : records) { + UserBoundVO userBoundVO = userPrimaryBoundBatch.get(item.getEmployeeID()); + if(userBoundVO!=null){ + item.setFullName(userBoundVO.getUserName()); + item.setOrganization(userBoundVO.getOrganizeName()); + String employeeSCurrentPosition = ""; + if(StringUtil.isNotEmpty(userBoundVO.getPositionName())){ + employeeSCurrentPosition = userBoundVO.getPositionName(); + } + if(StringUtil.isNotEmpty(userBoundVO.getGradeName())){ + employeeSCurrentPosition =employeeSCurrentPosition+"_"+userBoundVO.getGradeName(); + } + item.setEmployeeSCurrentPosition(employeeSCurrentPosition); + } + String promotionId = item.getPromotionId(); + FtbCultivatePromotionLevelMapVO learnMapLevels = promotionNewService.getLearnMapLevelsWithMapId(promotionId); + // 学习岗位数 + item.setNumberOfMapPositions(learnMapLevels.getLearningScope()); + item.setMapLevel(learnMapLevels); + String employeeID = item.getEmployeeID(); + item.setSysEmployeeID(perMap.get(employeeID)); + // 查询用户已学习岗位信息 + List relist = new ArrayList<>(); + relist.add(1); + relist.add(2); + String hasStudyPostIds = ftbCultivatePromotionNewMapper.queryThePositionsThatUsersHaveLearned(employeeID, relist); + if (StringUtils.isNotEmpty(hasStudyPostIds)) { + List hasStudyPostIdsList = Arrays.stream(hasStudyPostIds.split(",")).distinct().collect(Collectors.toList()); + List postIds = ftbCultivatePromotionNewMapper.queryChosePostId(employeeID, promotionId); + List excludeUnselectedList = hasStudyPostIdsList.stream().filter(postIds::contains).collect(Collectors.toList()); +// List positionNameList = positionApi.getPositionName(excludeUnselectedList); + List positionNameList = userApiV2Util.listPositionDetailInfoByIds(excludeUnselectedList, null); + if (CollUtil.isNotEmpty(positionNameList)) { + List stringListNames = positionNameList.stream().map(PositionVO::getFullName).collect(Collectors.toList()); + item.setPositionLearned(stringListNames.stream().collect(Collectors.joining(","))); + } + } + } + } + return CultivatePage.coverPageList(page); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void updatePromotion(FtbCultivatePromotionCreatDto creatDto) { + FtbCultivatePromotion cultivatePromotion = FtbCultivatePromotionCreatDto. + coverFtbCultivatePromotionCreatDto(creatDto); + //新增对应的岗位级别 + List postVOList = creatDto.getPostChannelLevel(); + Set postIds = postVOList.stream().map(FtbCultivatePromotionPostVO::getPostId).collect(Collectors.toSet()); + if (postIds.size() != postVOList.size()) { + throw new RuntimeException("职位列表中存在重复的岗位,请重新检查!"); + } + promotionMapper.updateById(cultivatePromotion); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbCultivatePromotionPost::getPromotionId, cultivatePromotion.getId()); + // 修改重构数据 + postService.remove(wrapper); + // 添加通道岗位 + savePromotion(postVOList, cultivatePromotion.getId()); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public boolean deleteById(String id, boolean flag) { + + // 判断结果1:是-二次确认提示:请谨慎删除, + // 该通道的初始岗位在岗位学习进行了学习配置, + // 删除该通道会导致APP端应用了该通道的用户重新选择晋升通道,目前共有X位用户应用了该通道。 + // 确定删除后在APP端将触发重新选择晋升通道 + if (!flag) { //一次删除 + FtbCultivatePromotionDto dto = new FtbCultivatePromotionDto(); + dto.setPromotionId(id); + PageListVO list = getPromotionMbeList(dto, new CultivatePage()); + // 查询是否存在关联数据 + if (!list.getList().isEmpty()) { + return false; + } + } + // 二次确认删除 + return deleteByPromotionId(id); + } + + /** + * 删除晋升通道 + * + * @param id + * @return + */ + public boolean deleteByPromotionId(String id) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(FtbCultivatePromotion::getEnableMark, "1"); + updateWrapper.eq(BaseEntity::getId, id); + // 判断结果2:否-二次确认提示:您确认要删除该通道吗?点击确认后删除 + // 软删除 + promotionMapper.update(new FtbCultivatePromotion(), updateWrapper); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + // 移除晋升通道岗位信息 + wrapper.eq(FtbCultivatePromotionPost::getPromotionId, id); + postService.remove(wrapper); + // 移除关联数据 + LambdaUpdateWrapper memberLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + memberLambdaUpdateWrapper.eq(FtbCultivatePromotionMember::getPromotionId, id); + promotionMemberService.remove(memberLambdaUpdateWrapper); + return true; + } + + @Override + public PageListVO getPromotionMbeList(FtbCultivatePromotionDto dto, CultivatePage objectPage) { + Page page = objectPage.coverCultivatePage(); + page = promotionMapper.getPromotionMbeList(dto, page); + List mbeListRecords = page.getRecords(); + // 获取当前传入用户绑定信息 + mbeListRecords.forEach(vo -> { + vo.setPoAndGradesName(vo.getPositionName() + "(" + vo.getPositionGradesName() + ");"); + }); + return CultivatePage.coverPageList(page); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankCourseServiceImpl.java new file mode 100644 index 0000000..bd9c03c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankCourseServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateQuestionBankCourseMapper; +import jnpf.cultivate.service.FtbCultivateQuestionBankCourseService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBankCourse; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateQuestionBankCourseServiceImpl extends ServiceImpl implements FtbCultivateQuestionBankCourseService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankServiceImpl.java new file mode 100644 index 0000000..bf42ee1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionBankServiceImpl.java @@ -0,0 +1,465 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.FtbCultivateQuestionBankMapper; +import jnpf.cultivate.service.FtbCultivatePositionExamCourseService; +import jnpf.cultivate.service.FtbCultivateQuestionBankCourseService; +import jnpf.cultivate.service.FtbCultivateQuestionBankService; +import jnpf.cultivate.service.FtbCultivateQuestionService; +import jnpf.model.cultivate.bo.FtbCultivateCourseBO; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBankCourse; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.req.questionbank.AddQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.QueryQuestionBankReq; +import jnpf.model.cultivate.req.questionbank.UnbindQuestionBankReq; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.resp.QuestionBankVo; +import jnpf.model.cultivate.resp.QuestionCountDto; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.SelfGrowthUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbCultivateQuestionBankServiceImpl extends ServiceImpl implements FtbCultivateQuestionBankService { + + /** + * 题库课程关联服务 + */ + @Autowired + private FtbCultivateQuestionBankCourseService questionBankCourseService; + + /** + * 题目服务 + */ + @Autowired + private FtbCultivateQuestionService questionService; + + @Autowired + private FtbCultivatePositionExamCourseService positionExamCourseService; + + /** + * 分页查询题库列表 + * + * @param req + * @return + */ + @Override + public PageInfo getPageList(QueryQuestionBankReq req) { + if (StringUtils.isNotEmpty(req.getKeyword())) { + req.setKeyword(StringUtils.trim(req.getKeyword())); + } + //构建分页 + Page page = new Page(req.getCurrentPage(), req.getPageSize()); + //构建查询 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .like(!StringUtils.isEmpty(req.getKeyword()), FtbCultivateQuestionBank::getBankContent, req.getKeyword()) + .eq(FtbCultivateQuestionBank::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateQuestionBank::getLastModifyTime); + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + Map> map = new HashMap<>(); + Map courseNameMap = new HashMap<>(); + Map countMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(records)) { + List questionBankId = new ArrayList<>(); + for (FtbCultivateQuestionBank vo : records) { + questionBankId.add(vo.getId()); + } + List questionCountDtoList = questionService.countForClassifyId(questionBankId); + if (CollectionUtil.isNotEmpty(questionCountDtoList)) { + for (QuestionCountDto questionCountDto : questionCountDtoList) { + countMap.put(questionCountDto.getQuestionBankId(), questionCountDto); + } + } + List ftbCultivateQuestionBankCourses = batchQueryQuestionBankCourse(questionBankId); + Set allCourseSet = new HashSet<>(); + if (CollectionUtil.isNotEmpty(ftbCultivateQuestionBankCourses)) { + //题库id分组 + Map> ftbCultivateQuestionBankCourseMap = ftbCultivateQuestionBankCourses.stream().collect(Collectors.groupingBy(FtbCultivateQuestionBankCourse::getClassifyId)); + for (Map.Entry> entry : ftbCultivateQuestionBankCourseMap.entrySet()) { + String key = entry.getKey(); // 获取当前 entry 的 key + List value = entry.getValue(); // 获取当前 entry 的 value + + List courseList = new ArrayList<>();//已经存在的课程ID + if (CollectionUtil.isNotEmpty(value)) { + for (FtbCultivateQuestionBankCourse questionBankCourse : value) { + courseList.add(questionBankCourse.getCourseId()); + } + } + map.put(key, courseList); + if (CollectionUtil.isNotEmpty(courseList)) { + allCourseSet.addAll(courseList); + } + } + } + + if (CollectionUtil.isNotEmpty(allCourseSet)) { + List courseName = positionExamCourseService.getCourseName(new ArrayList<>(allCourseSet)); + if (CollectionUtil.isNotEmpty(courseName)) { + for (FtbCultivateCourseBO ftbCultivateCourseBO : courseName) { + courseNameMap.put(ftbCultivateCourseBO.getCourseId(), ftbCultivateCourseBO); + } + } + } + } + records.forEach(entity -> { + QuestionBankVo vo = BeanUtil.copyProperties(entity, QuestionBankVo.class); + + //统计题库中题目数量 + QuestionCountDto questionCountDto = countMap.get(entity.getId()); + if(questionCountDto!=null){ + vo.setQuestionNum(questionCountDto.getNum()); + }else{ + vo.setQuestionNum(0L); + } + + //查看题库关联的课程信息 + List courseList = map.get(vo.getId()); + vo.setCourseList(courseList); + //查询课程名称 + if (CollectionUtil.isNotEmpty(courseList)) { + List courseDTOList = new ArrayList<>(); + for (String s : courseList) { + FtbCultivateCourseBO ftbCultivateCourseBO = courseNameMap.get(s); + if (ftbCultivateCourseBO != null) { + courseDTOList.add(ftbCultivateCourseBO); + } + } + vo.setCourseDTOList(courseDTOList); + } + list.add(vo); + }); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 获取题库信息 + * @param id 题库ID + * @return + */ + @Override + public QuestionBankVo getInfo(String id) { + //1、查询题库信息 + FtbCultivateQuestionBank questionBank = baseMapper.selectById(id); + if (null == questionBank) { + throw new RuntimeException("题库不存在"); + } + if (questionBank.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("题库已经被删除"); + } + QuestionBankVo vo = new QuestionBankVo(); + vo.setId(questionBank.getId()); + vo.setBankContent(questionBank.getBankContent()); + vo.setClassifyId(questionBank.getClassifyId()); + //2、查询题库关联的课程信息 + List courseList = queryQuestionBankCourse(id); + + vo.setCourseList(courseList); + return vo; + } + + + /** + * 新增题库 + * + * @param addQuestionBank + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void insertData(AddQuestionBankReq addQuestionBank) { + //1、保存题库 + FtbCultivateQuestionBank questionBank = new FtbCultivateQuestionBank(); + questionBank.setBankContent(addQuestionBank.getBankContent()); + questionBank.setClassifyId(generateClassId()); + questionBank.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + questionBank.setLastModifyTime(new Date()); + baseMapper.insert(questionBank); + //2、保存题库和课程的关联 + String courseIds = addQuestionBank.getCourseIds(); + if (StringUtils.isEmpty(courseIds)) { + return; + } + List courseList = Arrays.asList(courseIds.split(",")); + if (CollectionUtil.isEmpty(courseList)) { + return; + } + //3、保存题库和课程的关联关系 + saveQuestionBankAndCourseRelation(questionBank.getId(), courseList); + } + + /** + * 保存题库和课程的关联关系 + * + * @param questionBankId + * @param courseList + */ + private void saveQuestionBankAndCourseRelation(String questionBankId, List courseList) { + List questionBankCourseList = new ArrayList<>(); + for (String courseId : courseList) { + if (StringUtils.isEmpty(courseId)) { + continue; + } + FtbCultivateQuestionBankCourse questionBankCourse = new FtbCultivateQuestionBankCourse(); + questionBankCourse.setClassifyId(questionBankId); + questionBankCourse.setCourseId(courseId); + questionBankCourse.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + questionBankCourseList.add(questionBankCourse); + } + questionBankCourseService.saveBatch(questionBankCourseList); + } + + /** + * 删除已经存在的课程ID + * + * @param questionBankId 题库ID + * @param courseIdList 课程ID列表 + */ + private void deleteQuestionBankCourses(String questionBankId, List courseIdList) { + UpdateWrapper questionBankCourseWrapper = new UpdateWrapper<>(); + questionBankCourseWrapper.lambda().set(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateQuestionBankCourse::getClassifyId, questionBankId) + .in(FtbCultivateQuestionBankCourse::getCourseId, courseIdList); + questionBankCourseService.update(questionBankCourseWrapper); + } + + /** + * 生成题库ID + * + * @return + */ + private String generateClassId() { + return SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.QUESTION_BANK); + } + + + /** + * 修改题库 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateData(EditQuestionBankReq req) { + //1、修改题库 + FtbCultivateQuestionBank questionBank = new FtbCultivateQuestionBank(); + questionBank.setBankContent(req.getBankContent()); + questionBank.setClassifyId(generateClassId()); + questionBank.setId(req.getId()); + baseMapper.updateById(questionBank); + //获取整理前段传入的课程ID + List reqCourseList = new ArrayList<>(); + String reqCourseIds = req.getCourseIds(); + if (StringUtils.isNotEmpty(reqCourseIds)) { + reqCourseList = Arrays.asList(reqCourseIds.split(",")); + } + + + //2、查询题库已经存在课程 + List exiestList = queryQuestionBankCourse(req.getId());//已经存在的课程ID + //3、如果前段没有传入课程ID,就清楚所有的课程id + if (CollectionUtil.isEmpty(reqCourseList) && CollectionUtil.isNotEmpty(exiestList)) { + deleteQuestionBankCourses(req.getId(), exiestList); + return; + } + //4、如果前段传入课程id,保存新增的课程ID + if (CollectionUtil.isNotEmpty(reqCourseList)) { + //需要添加的 + List addCourseList = new ArrayList<>(); + Set set = exiestList.stream().collect(Collectors.toSet()); + for (String courseId : reqCourseList) { + if (!set.contains(courseId)) { + addCourseList.add(courseId); + } + } + if (CollectionUtil.isNotEmpty(addCourseList)) { + saveQuestionBankAndCourseRelation(req.getId(), addCourseList); + } + + //需要删除的 + List removeCourseList = new ArrayList<>(); + Set reqSet = reqCourseList.stream().collect(Collectors.toSet()); + for (String courseId : exiestList) { + if (!reqSet.contains(courseId)) { + removeCourseList.add(courseId); + } + } + if (CollectionUtil.isNotEmpty(removeCourseList)) { + deleteQuestionBankCourses(req.getId(), removeCourseList); + } + } + + } + + + /** + * 删除课程的时候,删除题库关联课程 + * + * @param courseIds 课程ID列表 + */ + @Override + public void deleteQuestionBankRelationCourse(List courseIds) { + if (CollectionUtil.isEmpty(courseIds)) { + return; + } + //删除题库关联的课程 + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .in(FtbCultivateQuestionBankCourse::getCourseId, courseIds) + .eq(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + questionBankCourseService.update(null, wrapper); + } + + /** + * 查询题库关联的课程id列表 + * + * @param questionBankId 题库ID + * @return + */ + private List queryQuestionBankCourse(String questionBankId) { + QueryWrapper questionBankCourseWrapper = new QueryWrapper<>(); + questionBankCourseWrapper.lambda() + .eq(FtbCultivateQuestionBankCourse::getClassifyId, questionBankId) + .eq(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List questionBankCoursesList = questionBankCourseService.list(questionBankCourseWrapper); + List courseList = new ArrayList<>();//已经存在的课程ID + if (CollectionUtil.isEmpty(questionBankCoursesList)) { + return courseList; + } + for (FtbCultivateQuestionBankCourse questionBankCourse : questionBankCoursesList) { + courseList.add(questionBankCourse.getCourseId()); + } + return courseList; + } + + /** + * 批量查询题库关联的课程 + * @param questionBankId 题库id集合 + * @return + */ + private List batchQueryQuestionBankCourse(List questionBankId) { + QueryWrapper questionBankCourseWrapper = new QueryWrapper<>(); + questionBankCourseWrapper.lambda() + .in(FtbCultivateQuestionBankCourse::getClassifyId, questionBankId) + .eq(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List questionBankCoursesList = questionBankCourseService.list(questionBankCourseWrapper); + + if (CollectionUtil.isEmpty(questionBankCoursesList)) { + return new ArrayList<>(); + } + + return questionBankCoursesList; + } + + + /** + * 删除题库 + * 1、检测题库是否存在 + * 2、检测题库中所有题目是否被试卷收录,如果有被试卷收录,则不能删除 + * 3、删除题库 和 题库的题目 + * + * @param id 题库ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String id) { + FtbCultivateQuestionBank questionBank = baseMapper.selectById(id); + if (null == questionBank) { + throw new RuntimeException("题库不存在"); + } + if (questionBank.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + return; + } + + + //删除题库 + questionBank.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + baseMapper.updateById(questionBank); + //删除题库关联的课程 + UpdateWrapper questionBankCourseWrapper = new UpdateWrapper<>(); + questionBankCourseWrapper.lambda().set(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateQuestionBankCourse::getClassifyId, id); + questionBankCourseService.update(questionBankCourseWrapper); + } + + /** + * 解除题库和课程的绑定 + * + * @param req + */ + @Override + public void unbind(UnbindQuestionBankReq req) { + + UpdateWrapper questionBankCourseWrapper = new UpdateWrapper<>(); + questionBankCourseWrapper.lambda().set(FtbCultivateQuestionBankCourse::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateQuestionBankCourse::getClassifyId, req.getId()) + .eq(FtbCultivateQuestionBankCourse::getCourseId, req.getCourseId()); + questionBankCourseService.update(questionBankCourseWrapper); + + } + + /** + * 检测题库是否可以删除 + * + * @param questionBankId 题库ID + * @return + */ + @Override + public CanDeleteMsg checkQuestionBankCanDelete(String questionBankId) { + return questionService.checkQuestionBankCanDelete(questionBankId); + } + + /** + * 统计题库中各题目类型数量 + * + * @param questionBankId 题库ID + * @return + */ + @Override + public Map analysQuestionCount(String questionBankId) { + + FtbCultivateQuestionBank questionBank = baseMapper.selectById(questionBankId); + if (null == questionBank) { + throw new RuntimeException("题库不存在"); + } + if (questionBank.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("题库已经被删除"); + } + + //初始化返回值 + Map analyMap = QuestionAnalysisUtil.initAnalysQuestionCount(); + + List questionList = questionService.list(new QueryWrapper().lambda().eq(FtbCultivateQuestion::getClassifyId, questionBankId).eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + QuestionAnalysisUtil.analysQuestionCount(analyMap, questionList); + return analyMap; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionOptionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionOptionServiceImpl.java new file mode 100644 index 0000000..e36dba2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionOptionServiceImpl.java @@ -0,0 +1,30 @@ +package jnpf.cultivate.service.impl; + + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateQuestionOptionMapper; +import jnpf.cultivate.service.FtbCultivateQuestionOptionService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; +import jnpf.model.enums.CourseEnums; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class FtbCultivateQuestionOptionServiceImpl extends ServiceImpl implements FtbCultivateQuestionOptionService { + + /** + * 根据题目id查询题目的选项列表 + * @param questionIds 题目id集合 + * @return + */ + @Override + public List queryOptionListByQuestionIds(List questionIds) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().in(FtbCultivateQuestionOption::getQuestionId, questionIds) + .eq(FtbCultivateQuestionOption::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByAsc(FtbCultivateQuestionOption::getSortCode); + return baseMapper.selectList(wrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionPointsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionPointsServiceImpl.java new file mode 100644 index 0000000..4576f6e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionPointsServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateQuestionPointsMapper; +import jnpf.cultivate.service.FtbCultivateQuestionPointsService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionPoints; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateQuestionPointsServiceImpl extends ServiceImpl implements FtbCultivateQuestionPointsService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionServiceImpl.java new file mode 100644 index 0000000..67dfc67 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateQuestionServiceImpl.java @@ -0,0 +1,823 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.FtbCultivateQuestionMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.AsyncExamQuestionUtils; +import jnpf.model.cultivate.po.FtbCultivateAssessmentPoints; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionPoints; +import jnpf.model.cultivate.req.questionbank.AddQuestionReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionReq; +import jnpf.model.cultivate.req.questionbank.QueryQuestionReq; +import jnpf.model.cultivate.req.questionbank.QuestionOptionReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.CourseEnums; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class FtbCultivateQuestionServiceImpl extends ServiceImpl implements FtbCultivateQuestionService { + + /** + * 题目选项服务 + */ + @Autowired + private FtbCultivateQuestionOptionService questionOptionService; + + /** + * 题目考点关系 + */ + @Autowired + private FtbCultivateQuestionPointsService questionPointsService; + + /** + * 考点服务 + */ + @Autowired + private FtbCultivateAssessmentPointsService assessmentPointsService; + + /** + * 试卷题目关联 + */ + @Autowired + private FtbCultivateTestPaperQuestionService paperQuestionService; + /** + * 试卷 + */ + @Autowired + private FtbCultivateTestPaperService testPaperService; + + /** + * 考试 + */ + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private AsyncExamQuestionUtils asyncExamQuestionUtils; + + /** + * 分页列出题库中题目列表【根据题库ID】 + * + * @param req + * @return + */ + @Override + public PageInfo getPageList(QueryQuestionReq req) { + if(StringUtils.isNotEmpty(req.getKeyword())){ + req.setKeyword(StringUtils.trim(req.getKeyword())); + } + //构建分页 + Page page = new Page(req.getCurrentPage(), req.getPageSize()); + //构建查询 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateQuestion::getClassifyId, req.getQuestionBankId()) + .like(!StringUtils.isEmpty(req.getKeyword()), FtbCultivateQuestion::getContent, req.getKeyword()) + .eq(req.getType() != null, FtbCultivateQuestion::getType, req.getType()) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateQuestion::getLastModifyTime); + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + records.forEach(entity -> { + QuestionVo vo = BeanUtil.copyProperties(entity, QuestionVo.class); + list.add(vo); + }); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + public QuestionVo getQuestionAndOption(String questionId) { + //1、检测题目信息 + FtbCultivateQuestion question = baseMapper.selectById(questionId); + if (null == question) { + throw new RuntimeException("题目不存在"); + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("题目已经被删除,不能操作"); + } + //2、题目po转vo + QuestionVo vo = BeanUtil.copyProperties(question, QuestionVo.class); + //3、查询题目的选项信息 + if (!CourseEnums.QuestionType.INPUT.getCode().equals(question.getType()) + && !CourseEnums.QuestionType.COMBINE.getCode().equals(question.getType())) { + List questionOptionList = queryOptionListForQuestionId(questionId); + if (CollectionUtil.isNotEmpty(questionOptionList)) { + vo.setQuestionOptionVoList(BeanUtil.copyToList(questionOptionList, QuestionOptionVo.class)); + } + } + return vo; + } + + @Override + public QuestionVo getInfo(String questionId) { + //1、查询题目和选项 + QuestionVo QuestionVo = getQuestionAndOption(questionId); + //2、查询题目的考点信息 + List assessmentPointsVoList = queryQuestionAssessmentPoints(questionId); + QuestionVo.setAssessmentPointsVoList(assessmentPointsVoList); + return QuestionVo; + } + + /** + * 查询题目ID查询考点列表 + * + * @param questionId 题目ID + * @return + */ + + private List queryQuestionAssessmentPoints(String questionId) { + + //1、查询题目关联的考点 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestionPoints::getQuestionId, questionId); + List questionPointsList = questionPointsService.list(wrapper); + if (CollectionUtil.isEmpty(questionPointsList)) { + return CollectionUtil.newArrayList(); + } + //2、根据考点Ids查询考点列表 + List assessmentPointsIds = questionPointsList.stream().map(FtbCultivateQuestionPoints::getPointsId).collect(Collectors.toList()); + QueryWrapper assessmentPointsQueryWrapper = new QueryWrapper<>(); + assessmentPointsQueryWrapper.lambda() + .in(FtbCultivateAssessmentPoints::getId, assessmentPointsIds) + .eq(FtbCultivateAssessmentPoints::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List assessmentPointsList = assessmentPointsService.list(assessmentPointsQueryWrapper); + if (CollectionUtil.isEmpty(assessmentPointsList)) { + return CollectionUtil.newArrayList(); + } + return BeanUtil.copyToList(assessmentPointsList, AssessmentPointsVo.class); + } + + /** + * 新增题目 + * + * @param questionBankId 题库ID + * @param req 题目信息 + */ + @Override + @Transactional + public void insertData(String questionBankId, AddQuestionReq req) { + //1、添加题目信息 + FtbCultivateQuestion question = new FtbCultivateQuestion(); + question.setClassifyId(questionBankId); + question.setType(req.getType()); + question.setDifficulty(req.getDifficulty()); + question.setContent(req.getContent()); + question.setAnalysis(req.getAnalysis()); + question.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + question.setCreatorTime(new Date()); + question.setLastModifyTime(new Date()); + baseMapper.insert(question); + //2、添加题目选项信息 + if (CourseEnums.QuestionType.INPUT.getCode().equals(question.getType()) || CourseEnums.QuestionType.COMBINE.getCode().equals(question.getType())) { + return; + } + List optionList = req.getOptionList(); + if (CollectionUtil.isEmpty(optionList)) { + return; + } + List questionOptionList = new ArrayList<>(); + for (QuestionOptionReq option : optionList) { + FtbCultivateQuestionOption questionOption = new FtbCultivateQuestionOption(); + questionOption.setQuestionId(question.getId()); + questionOption.setContent(option.getContent()); + questionOption.setImage(option.getImage()); + questionOption.setDescription(option.getDescription()); + questionOption.setSortCode(option.getSortCode()); + questionOption.setIsRightOption(option.getIsRightOption()); + questionOption.setRightAnswer(option.getRightAnswer()); + questionOption.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + questionOptionList.add(questionOption); + } + questionOptionService.saveBatch(questionOptionList); + log.info(questionOptionList.toString()); + + //3、写入正确答案 + writeRightAnswer(question, questionOptionList); + //4、写入关联的考点信息 + String pointIds = req.getQuestionPointIds(); + if (StringUtils.isEmpty(pointIds)) { + return; + } + List pointLists = Arrays.asList(pointIds.split(",")); + if (CollectionUtil.isEmpty(pointLists)) { + return; + } + saveQuestionPoint(question.getId(), pointLists); + } + + /** + * 写入正确答案 + * + * @param question 题目信息 + * @param questionOptionList 题目选项信息 + */ + private void writeRightAnswer(FtbCultivateQuestion question, List questionOptionList) { + List rightAnswerList = new ArrayList<>(); + if (CourseEnums.QuestionType.SINGLE.getCode().equals(question.getType()) + || CourseEnums.QuestionType.MULTI.getCode().equals(question.getType()) + || CourseEnums.QuestionType.JUDGE.getCode().equals(question.getType()) + || CourseEnums.QuestionType.FILL.getCode().equals(question.getType()) + || CourseEnums.QuestionType.ONE_OR_MULTI.getCode().equals(question.getType()) + ) { + if (CollectionUtil.isEmpty(questionOptionList)) { + throw new RuntimeException("单选、多选、判断题、填空题、不定项选择题的选项不能为空"); + } + if (questionOptionList.size() > 8) { + throw new RuntimeException("单选、多选、判断题、填空题、不定项选择题的选项数量不能大于8个"); + } + } + for (FtbCultivateQuestionOption questionOption : questionOptionList) { + if (CourseEnums.QuestionType.SINGLE.getCode().equals(question.getType()) + || CourseEnums.QuestionType.MULTI.getCode().equals(question.getType()) + || CourseEnums.QuestionType.JUDGE.getCode().equals(question.getType()) + || CourseEnums.QuestionType.ONE_OR_MULTI.getCode().equals(question.getType()) + ) { + if (CourseEnums.IsRightAnswer.YES.getCode().equals(questionOption.getIsRightOption())) { + rightAnswerList.add(questionOption.getId()); + } + } else if (CourseEnums.QuestionType.FILL.getCode().equals(question.getType())) { + rightAnswerList.add(questionOption.getId()); + } + } + FtbCultivateQuestion updateQuestion = new FtbCultivateQuestion(); + updateQuestion.setAnswer(rightAnswerList.stream().collect(Collectors.joining(","))); + updateQuestion.setId(question.getId()); + baseMapper.updateById(updateQuestion); + } + + /** + * 批量写入题目关联的考点信息 + * + * @param questionId 题目ID + * @param pointLists 考点列表 + */ + private void saveQuestionPoint(String questionId, List pointLists) { + List list = new ArrayList<>(); + for (String pointId : pointLists) { + FtbCultivateQuestionPoints points = new FtbCultivateQuestionPoints(); + points.setQuestionId(questionId); + points.setPointsId(pointId); + list.add(points); + } + questionPointsService.saveBatch(list); + } + + + /** + * 修改题目 + * 1、检测题目信息是否存咋存并修改题目信息 + * 2、处理题目选项信息 + * 3、处理题目的正确答案 + * 4、处理题目关联的考点信息 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateData(EditQuestionReq req) { + + //1、检测题目信息 + FtbCultivateQuestion question = baseMapper.selectById(req.getId()); + if (null == question) { + throw new RuntimeException("题目不存在"); + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("题目已经被删除,不能操作"); + } + //接口传入选项 + List reqOptionList = req.getOptionList(); + //1、修改题目信息 + question.setType(req.getType()); + question.setDifficulty(req.getDifficulty()); + question.setContent(req.getContent()); + question.setAnalysis(req.getAnalysis()); + baseMapper.updateById(question); + //2、查询题目选项 + List questionOptionList = queryOptionListForQuestionId(req.getId()); + //3、获取已经存在 + List exiestList = new ArrayList<>();//已经存在的选项 + for (FtbCultivateQuestionOption questionOption : questionOptionList) { + exiestList.add(questionOption.getId()); + } + List allDealOptionList = new ArrayList<>(); + //4、处理题目选项信息 + if (CollectionUtil.isEmpty(reqOptionList) && CollectionUtil.isNotEmpty(exiestList)) { + deleteQuestionOption(req.getId(), exiestList); + } else { + //处理新增和编辑题目选项 + List addList = new ArrayList<>();//新增列表 + List editList = new ArrayList<>();//编辑列表 + Set reqSet = new HashSet<>();//提交的有选项ID的集合 + for (QuestionOptionReq optionReq : reqOptionList) { + if (StringUtils.isEmpty(optionReq.getId())) { + addList.add(optionReq); + } else { + editList.add(optionReq); + reqSet.add(optionReq.getId()); + } + } + + if (CollectionUtil.isNotEmpty(addList)) { + allDealOptionList.addAll(addQustionOption(req.getId(), addList)); + } + if (CollectionUtil.isNotEmpty(editList)) { + allDealOptionList.addAll(updateQustionOption(editList)); + } + //处理删除选项 + List removeList = new ArrayList<>(); + for (String optionId : exiestList) { + if (!reqSet.contains(optionId)) { + removeList.add(optionId); + } + } + if (CollectionUtil.isNotEmpty(removeList)) { + deleteQuestionOption(req.getId(), removeList); + } + } + + //5、给题目写入正确答案 + writeRightAnswer(question, allDealOptionList); + //6、写入关联的考点信息 + //6.1删除已经存在的考点 + removeQuestionPointRelation(question.getId()); + //6.2新增题目关联考点信息 + String pointIds = req.getQuestionPointIds(); + if (StringUtils.isEmpty(pointIds)) { + return; + } + List pointLists = Arrays.asList(pointIds.split(",")); + if (CollectionUtil.isEmpty(pointLists)) { + return; + } + saveQuestionPoint(question.getId(), pointLists); + } + + /** + * 清除题目关联考点信息 + * + * @param questionId 题目ID + */ + private void removeQuestionPointRelation(String questionId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestionPoints::getQuestionId, questionId); + questionPointsService.remove(wrapper); + } + + /** + * 给题目中增加选项 + * + * @param questionId 题目ID + * @param optionReqList 选项 + */ + private List addQustionOption(String questionId, List optionReqList) { + List optionList = new ArrayList<>(); + for (QuestionOptionReq optionReq : optionReqList) { + FtbCultivateQuestionOption questionOption = new FtbCultivateQuestionOption(); + questionOption.setQuestionId(questionId); + questionOption.setContent(optionReq.getContent()); + questionOption.setImage(optionReq.getImage()); + questionOption.setDescription(optionReq.getDescription()); + questionOption.setSortCode(optionReq.getSortCode()); + questionOption.setIsRightOption(optionReq.getIsRightOption()); + questionOption.setRightAnswer(optionReq.getRightAnswer()); + questionOption.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + optionList.add(questionOption); + } + questionOptionService.saveBatch(optionList); + return optionList; + } + + /** + * 修改题目选项 + * + * @param optionReqList + */ + private List updateQustionOption(List optionReqList) { + List optionList = new ArrayList<>(); + for (QuestionOptionReq optionReq : optionReqList) { + FtbCultivateQuestionOption questionOption = new FtbCultivateQuestionOption(); + questionOption.setId(optionReq.getId()); + questionOption.setContent(optionReq.getContent()); + questionOption.setImage(optionReq.getImage()); + questionOption.setDescription(optionReq.getDescription()); + questionOption.setSortCode(optionReq.getSortCode()); + questionOption.setIsRightOption(optionReq.getIsRightOption()); + questionOption.setRightAnswer(optionReq.getRightAnswer()); + questionOption.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + optionList.add(questionOption); + } + questionOptionService.saveOrUpdateBatch(optionList); + return optionList; + } + + /** + * 删除问题选项 + * + * @param questionId 问题ID + * @param optionIdList 选项ID列表 + */ + private void deleteQuestionOption(String questionId, List optionIdList) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbCultivateQuestionOption::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .eq(FtbCultivateQuestionOption::getQuestionId, questionId) + .in(FtbCultivateQuestionOption::getId, optionIdList); + questionOptionService.update(wrapper); + } + + /** + * 根据题目ID查 题目选项列表 + * + * @param questionId 题目ID + */ + private List queryOptionListForQuestionId(String questionId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestionOption::getQuestionId, questionId) + .eq(FtbCultivateQuestionOption::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByAsc(FtbCultivateQuestionOption::getSortCode); + return questionOptionService.list(wrapper); + } + + /** + * 删除题库 + * + * @param id 题库ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String id) { + //1、查询题目信息 + FtbCultivateQuestion question = baseMapper.selectById(id); + if (null == question) { + throw new RuntimeException("题目不存在"); + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + return; + } + //2、删除题目 + question.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + baseMapper.updateById(question); +// UpdateWrapper updateWrapper = new UpdateWrapper<>(); +// updateWrapper.lambda() +// .set(FtbCultivateQuestionOption::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) +// .eq(FtbCultivateQuestionOption::getQuestionId, question.getId()); +// questionOptionService.update(updateWrapper); + //删除考点 + removeQuestionPointRelation(question.getId()); + //删除题目后 异步处理试卷和考试 + asyncPaperQuestion(List.of(question.getId())); + } + + /** + * 批量删除题目 + * @param questionIds 题目id集合 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void batchDel(List questionIds) { + List needDelQuestionIds = new ArrayList<>(); + for (String questionId : questionIds) { + FtbCultivateQuestion question = baseMapper.selectById(questionId); + if (null == question) { + continue; + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + continue; + } + needDelQuestionIds.add(questionId); + } + if (CollectionUtil.isEmpty(needDelQuestionIds)) { + return; + } + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda() + .set(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .in(FtbCultivateQuestion::getId, needDelQuestionIds); + update(updateWrapper); + asyncPaperQuestion(needDelQuestionIds); + } + + /** + * 根据题库ID查询题目列表 + * @param questionBankId 题库ID + * @return + */ + @Override + public List queryAllQuestionByQuestionBankId(String questionBankId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateQuestion::getClassifyId, questionBankId) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateQuestion::getLastModifyTime); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + throw new RuntimeException("题库中暂无题目"); + } + + List userQuestionVoList = BeanUtil.copyToList(list, UserQuestionVo.class); + List questionIds = list.stream().map(FtbCultivateQuestion::getId).collect(Collectors.toList()); + List questionOptionList = questionOptionService.queryOptionListByQuestionIds(questionIds); + Map> questionOptionMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(questionOptionList)) { + questionOptionMap = questionOptionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestionOption::getQuestionId)); + } + //填充题目选项 + for (UserQuestionVo userQuestionVo : userQuestionVoList) { + List questionOptionVoList = questionOptionMap.get(userQuestionVo.getId()); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + userQuestionVo.setQuestionOptionVoList(BeanUtil.copyToList(questionOptionVoList, QuestionOptionVo.class)); + } + } + return userQuestionVoList; + } + /** + * 给题库中导入题目 + * @param questionBankId 题目ID + * @param allQuestionList 题目数据 + * @return + */ + @Override + @Transactional + public ImportQuestionResultVo realImportData(String questionBankId, List allQuestionList) { + + List failList = new ArrayList<>(); + List successList = new ArrayList<>(); + allQuestionList.forEach(req -> { + AddQuestionReq addQuestionReq = BeanUtil.copyProperties(req, AddQuestionReq.class); + try { + insertData(questionBankId, addQuestionReq); + successList.add(addQuestionReq); + } catch (Exception e) { + log.error("导入题目失败", e); + failList.add(addQuestionReq); + } + }); + return ImportQuestionResultVo.builder().successList(successList).failList(failList).build(); + } + /** + * 根据题库id统计各题库的题目数量 + * @param ids 题库id集合 + * @return + */ + @Override + public List countForClassifyId(List ids) { + return baseMapper.countForClassifyId(ids); + } + + /** + * 异步处理题目删除 1、给试卷大更新标记 2、给考试打跟新标记 3、更新试卷的题目数量和分数 + * @param questionIds 题目ids + */ + private void asyncPaperQuestion(List questionIds) { + String tenantId = asyncExamQuestionUtils.getTenantId(); + Map headers = asyncExamQuestionUtils.getHeadersForLogin(); + // asyncExamQuestionUtils.asyncPaperQuestion(tenantId, questionIds, headers); + asyncExamQuestionUtils.asyncExamQuestion(tenantId, questionIds, headers); + } + + /** + * 检测题库是否可以被删除 + * 1、查询题库的所有题目信息 + * 2、循环检测题目是否可以被删除 + * + * @param questionBankId 题库ID + * @return + */ + @Override + public CanDeleteMsg checkQuestionBankCanDelete(String questionBankId) { + //1、查询题库所有题目 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestion::getClassifyId, questionBankId) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List questionList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(questionList)) { + return new CanDeleteMsg(true, "题库中没有题目"); + } + //2、循环检测每个题目是否可以被删除 + for (FtbCultivateQuestion question : questionList) { + QuestionCanDeleteMsg canDeleteMsg = checkQuestionCanDelete(question.getId()); + //不能删除直接返回了 + if (!canDeleteMsg.getCanDelete()) { + return new CanDeleteMsg(false, canDeleteMsg.getMsg(), canDeleteMsg.getData()); + } + } + return new CanDeleteMsg(true, "可以删除"); + } + + /** + * 检测题目是否可以被删除 + *

+ * 【判断一】判断该题目有无纳入试卷 + * 判断结果1:有-继续判断二 + * 判断结果2:无-删除该题目 + * 【判断二】判断纳入该题目的试卷有无纳入考试 + * 判断结果2:无-二次确认后删除,提示语:该题目纳入的试卷(***试卷名称**ID:***),直接删除题目不影响历史已有试卷,若需从考卷中剔除该题目须要重建试卷 + * + * @param questionId 题目ID + * @return + */ + public QuestionCanDeleteMsg checkQuestionCanDelete(String questionId) { + + //1、查询题目信息 + FtbCultivateQuestion question = baseMapper.selectById(questionId); + if (null == question) { + + return new QuestionCanDeleteMsg(false, "题目不存在"); + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + return new QuestionCanDeleteMsg(false, "题目已经被删除"); + } + //查询题目收录的试卷列表 + QueryWrapper testPaperQuestionQueryWrapper = new QueryWrapper<>(); + testPaperQuestionQueryWrapper.lambda() + .eq(FtbCultivateTestPaperQuestion::getQuestionId, questionId) + .eq(FtbCultivateTestPaperQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List testPaperQuestionList = paperQuestionService.list(testPaperQuestionQueryWrapper); + if (CollectionUtil.isEmpty(testPaperQuestionList)) { + return new QuestionCanDeleteMsg(true, "无考试试卷收录该题目"); + } + //查询题目收率的试卷列表 + List paperIdList = testPaperQuestionList.stream().map(FtbCultivateTestPaperQuestion::getPaperId).collect(Collectors.toList()); + QueryWrapper testPaperQueryWrapper = new QueryWrapper<>(); + testPaperQueryWrapper.lambda() + .in(FtbCultivateTestPaper::getId, paperIdList) + .eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List testPaperList = testPaperService.list(testPaperQueryWrapper); + if (CollectionUtil.isEmpty(testPaperList)) { + return new QuestionCanDeleteMsg(true, "无考试试卷收录该题目"); + } + + List voList = new ArrayList<>(); + for (FtbCultivateTestPaper paper : testPaperList) { + RecordPaperAndExam record = new RecordPaperAndExam(); + record.setQuestionId(question.getId()); + record.setQuestionName(question.getContent()); + record.setPaperPrimaryId(paper.getId()); + record.setPaperId(paper.getPaperId()); + record.setPaperName(paper.getName()); + List examList = examService.queryExamListByPaperId(paper.getId()); + record.setExamList(examList.stream().map(ExamVo::getExamName).collect(Collectors.toList())); + voList.add(record); + } + return new QuestionCanDeleteMsg(false, "有考试试卷收录该题目", voList); + } + + /** + * 批量检测题目是否可以删除 + * @param questionIds 题目id集合 + * @return + */ + @Override + public QuestionCanDeleteMsg batchCheckCanDel(List questionIds) { + List voList = new ArrayList<>(); + for (String questionId : questionIds) { + FtbCultivateQuestion question = baseMapper.selectById(questionId); + if (null == question) { + continue; + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + continue; + } + //查询题目收录的试卷列表 + QueryWrapper testPaperQuestionQueryWrapper = new QueryWrapper<>(); + testPaperQuestionQueryWrapper.lambda() + .eq(FtbCultivateTestPaperQuestion::getQuestionId, questionId) + .eq(FtbCultivateTestPaperQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List testPaperQuestionList = paperQuestionService.list(testPaperQuestionQueryWrapper); + if (CollectionUtil.isEmpty(testPaperQuestionList)) { + continue; + } + //查询题目收率的试卷列表 + List paperIdList = testPaperQuestionList.stream().map(FtbCultivateTestPaperQuestion::getPaperId).collect(Collectors.toList()); + QueryWrapper testPaperQueryWrapper = new QueryWrapper<>(); + testPaperQueryWrapper.lambda() + .in(FtbCultivateTestPaper::getId, paperIdList) + .eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List testPaperList = testPaperService.list(testPaperQueryWrapper); + if (CollectionUtil.isEmpty(testPaperList)) { + continue; + } + + for (FtbCultivateTestPaper paper : testPaperList) { + RecordPaperAndExam record = new RecordPaperAndExam(); + record.setQuestionId(question.getId()); + record.setQuestionName(question.getContent()); + record.setPaperPrimaryId(paper.getId()); + record.setPaperId(paper.getPaperId()); + record.setPaperName(paper.getName()); + List examList = examService.queryExamListByPaperId(paper.getId()); + record.setExamList(examList.stream().map(ExamVo::getExamName).collect(Collectors.toList())); + voList.add(record); + } + } + if (CollectionUtil.isEmpty(voList)) { + return new QuestionCanDeleteMsg(true, "无考试试卷关联题目"); + } else { + return new QuestionCanDeleteMsg(false, "有考试试卷关联题目", voList); + } + } + + + /** + * 导入题目 + * + * @param questionBankId 题库ID + * @param allQuestionList 题目列表 + */ + @Override + @Transactional + public ImportQuestionResultVo importData(String questionBankId, List allQuestionList) { + if (CollectionUtil.isEmpty(allQuestionList)) { + return ImportQuestionResultVo.builder().build(); + } + List failList = new ArrayList<>(); + List successList = new ArrayList<>(); + allQuestionList.forEach(req -> { + try { + if (StringUtils.isEmpty(req.getContent())) { + failList.add(req); + } else { + insertData(questionBankId, req); + successList.add(req); + } + } catch (Exception e) { + log.error("导入题目失败", e); + failList.add(req); + } + }); + return ImportQuestionResultVo.builder().successList(successList).failList(failList).build(); + } + + /** + * 查询重复题目 + * + * @param req + * @return + */ + @Override + public PageInfo queryRepeatQuestion(QueryQuestionReq req) { + PageInfo pageInfo = new PageInfo<>(); + + //1、查询重复的题目名称 + List repeatList = baseMapper.queryRepeatQuestion(req.getQuestionBankId()); + if (CollectionUtil.isEmpty(repeatList)) { + pageInfo.setList(new ArrayList<>()); + pageInfo.setPageSize(req.getPageSize()); + pageInfo.setPageNum(req.getCurrentPage()); + pageInfo.setTotal(0); + return pageInfo; + } + //转换 题目名称列表 + List nameList = repeatList.stream().map(FtbCultivateQuestion::getContent).collect(Collectors.toList()); + + Page page = new Page(req.getCurrentPage(), req.getPageSize()); + //构建查询 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateQuestion::getClassifyId, req.getQuestionBankId()) + .in(FtbCultivateQuestion::getContent, nameList) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateQuestion::getLastModifyTime); + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + records.forEach(entity -> { + QuestionVo vo = BeanUtil.copyProperties(entity, QuestionVo.class); + list.add(vo); + }); + //构建分页返回数据 + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateRuleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateRuleServiceImpl.java new file mode 100644 index 0000000..06176a5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateRuleServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateRuleMapper; +import jnpf.cultivate.service.FtbCultivateRuleService; +import jnpf.model.cultivate.po.FtbCultivateRule; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2024/9/9:15:19 +* +*/ +@Service +public class FtbCultivateRuleServiceImpl extends ServiceImpl implements FtbCultivateRuleService{ + + @Resource + private FtbCultivateRuleMapper ftbCultivateRuleMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStatisticsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStatisticsServiceImpl.java new file mode 100644 index 0000000..2683730 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStatisticsServiceImpl.java @@ -0,0 +1,459 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import jnpf.cultivate.mapper.FtbCultivateStatisticsMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivateStatisticsService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.statistics.FtbCultivateStatisticsDTO; +import jnpf.model.cultivate.resp.ExamStatisticsVo; +import jnpf.model.cultivate.resp.UserRankingVo; +import jnpf.model.cultivate.v2.statistics.V2CultivateLineNumDto; +import jnpf.model.cultivate.vo.identify.IdentifyStatisticsForNewVo; +import jnpf.model.cultivate.vo.identify.IdentifyTopVo; +import jnpf.model.cultivate.vo.statistics.FtbCultivateStatisticsVO; +import jnpf.model.cultivate.vo.statistics.NumberOfTrainingSessions; +import jnpf.model.cultivate.vo.statistics.NumberofAppSessions; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbCultivateStatisticsServiceImpl implements FtbCultivateStatisticsService { + + @Resource + private FtbCultivateStatisticsMapper ftbCultivateStatisticsMapper; + @Autowired + private UserApi userApi; + @Resource + private FtbCultivateExamUserService examUserService; + @Resource + private CultivateIdentifyApplyService applyService; + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public NumberOfTrainingSessions getNumberOfTraineesStatistics(FtbCultivateStatisticsDTO dto) { + NumberOfTrainingSessions number = new NumberOfTrainingSessions(); + if (CollectionUtils.isEmpty(dto.getUserIds())) { + return number; + } + FtbCultivateStatisticsDTO ftbCultivateStatisticsDTO = new FtbCultivateStatisticsDTO(); + ftbCultivateStatisticsDTO.setUserIds(dto.getUserIds()); + ftbCultivateStatisticsDTO.setDateType(dto.getDateType()); + ftbCultivateStatisticsDTO.setOrgId(dto.getOrgId()); + // 学习数 + number.setNumOfTrainees(ftbCultivateStatisticsMapper.numOfTraineesV2(dto)); + // 累计学习人数 + number.setCumulativeNumberOfTrainees(ftbCultivateStatisticsMapper.numOfTraineesV2(ftbCultivateStatisticsDTO)); + // 学习课程数 + number.setNumberOfStudyCourses(ftbCultivateStatisticsMapper.numberOfStudyCoursesV2(dto)); + // 累计学习课程数 + number.setCumulativeNumberOfCoursesTaken(ftbCultivateStatisticsMapper.numberOfStudyCoursesV2(ftbCultivateStatisticsDTO)); + // 时长 + number.setDuration(ftbCultivateStatisticsMapper.accumulatedTimeV2(dto)); + // 累计时长 + Integer accumulatedTime = ftbCultivateStatisticsMapper.accumulatedTimeV2(ftbCultivateStatisticsDTO); + number.setAccumulatedTime(accumulatedTime); + // 完课率 已学习/学习中+已学习 + Integer courseCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRateV2(dto, List.of(1)); + Integer denominatorCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRateV2(dto, List.of(0, 1)); + number.setCourseCompletionRate(getCompletionRate(courseCompletionRate, denominatorCompletionRate)); + // 总完课率 + Integer completionRate = ftbCultivateStatisticsMapper.courseCompletionRateV2(ftbCultivateStatisticsDTO, List.of(1)); + Integer denominatorOverallCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRateV2(ftbCultivateStatisticsDTO, List.of(0, 1)); + number.setOverallCourseCompletionRate(getCompletionRate(completionRate, denominatorOverallCompletionRate)); + return number; + } + + @Override + public List getPopularCourses(FtbCultivateStatisticsDTO dto) { + List userIds = dto.getUserIds(); + if (CollectionUtils.isEmpty(userIds)) { + return null; + } + return ftbCultivateStatisticsMapper.getPopularCourses(dto); + } + + @Override + public List numberOfStudentsLineGraph(FtbCultivateStatisticsDTO dto) { + List result = new ArrayList<>(); + if (dto.getStartTime() == null || dto.getEndTime() == null) { + return result; + } + // 月度,统计每一天的 + if (dto.getDateType() == 1) { + long betweenDay = DateUtil.betweenDay(dto.getStartTime(), dto.getEndTime(), true); + for (int i = 0; i <= betweenDay; i++) { + // 多线程查询 + Date offsetDay = DateUtil.offsetDay(dto.getStartTime(), i).toJdkDate(); + Date startTime = DateUtil.beginOfDay(offsetDay); + Date endTime = DateUtil.endOfDay(offsetDay); + doLearnDataProcessing(dto, startTime, endTime, offsetDay, result, "yyyy-MM-dd"); + } + } + // 季度,统计每个月的 + if (dto.getDateType() == 2) { + long betweenMonth = DateUtil.betweenMonth(dto.getStartTime(), dto.getEndTime(), true); + for (int i = 0; i <= betweenMonth; i++) { + Date offsetDay = DateUtil.offsetMonth(dto.getStartTime(), i).toJdkDate(); + Date startTime = DateUtil.beginOfMonth(offsetDay); + Date endTime = DateUtil.endOfMonth(offsetDay); + doLearnDataProcessing(dto, startTime, endTime, offsetDay, result, "yyyy-MM"); + } + } + // 年度,统计每个月的 + if (dto.getDateType() == 3) { + for (int i = 0; i <= 11; i++) { + Date offsetDay = DateUtil.offsetMonth(dto.getStartTime(), i).toJdkDate(); + Date startTime = DateUtil.beginOfMonth(offsetDay); + Date endTime = DateUtil.endOfMonth(offsetDay); + doLearnDataProcessing(dto, startTime, endTime, offsetDay, result, "yyyy-MM"); + } + } + return result; + } + + private void doLearnDataProcessing(FtbCultivateStatisticsDTO dto, Date startTime, Date endTime, Date offsetDay, + List result, String format) { + FtbCultivateStatisticsDTO ftbCultivateStatisticsDTO = new FtbCultivateStatisticsDTO(); + ftbCultivateStatisticsDTO.setUserIds(dto.getUserIds()); + ftbCultivateStatisticsDTO.setDateType(dto.getDateType()); + ftbCultivateStatisticsDTO.setOrgId(dto.getOrgId()); + ftbCultivateStatisticsDTO.setStartTime(startTime); + ftbCultivateStatisticsDTO.setEndTime(endTime); + FtbCultivateStatisticsVO.LineChartCumulativeLearners lineChartCumulativeLearners = new FtbCultivateStatisticsVO.LineChartCumulativeLearners(); + lineChartCumulativeLearners.setDate(DateUtil.format(offsetDay, format)); + if (CollectionUtils.isEmpty(dto.getUserIds())) { + // 学习人数 + lineChartCumulativeLearners.setNumberOfStudents(0); + // 学习时长 + lineChartCumulativeLearners.setStudyDuration(0); + } else { + V2CultivateLineNumDto numDto =ftbCultivateStatisticsMapper.queryStudyUserNumAndTimeV2(ftbCultivateStatisticsDTO); + // 学习人数 + lineChartCumulativeLearners.setNumberOfStudents(numDto.getNumberOfStudents()); + // 学习时长 + lineChartCumulativeLearners.setStudyDuration(numDto.getStudyDuration()); + } + result.add(lineChartCumulativeLearners); + } + + @Override + public List getTopMembers(FtbCultivateStatisticsDTO dto, Integer rankingType) { + if (CollectionUtils.isEmpty(dto.getUserIds())) { + return new ArrayList<>(); + } + if (rankingType == 1) { + List result = ftbCultivateStatisticsMapper.getTopMembers(dto); + Map userEntityMap = userApiV2Util.getUserNameAndCopyForUserIds( + result.stream() + .map(FtbCultivateStatisticsVO.TopMembers::getOrdinate) + .collect(Collectors.toList()) + ); + + result.forEach(item -> { + UserEntity userEntity = userEntityMap.get(item.getOrdinate()); + item.setOrdinate(userEntity != null ? userEntity.getRealName() : item.getOrdinate()); + }); + return result; + } else if (rankingType == 2) { + List rankingList = examUserService.statisticsExamRankingListNoPower(dto); + return rankingList.stream().map(item -> { + FtbCultivateStatisticsVO.TopMembers topMembers = new FtbCultivateStatisticsVO.TopMembers(); + topMembers.setAbscissa(item.getScore()); + topMembers.setOrdinate(item.getUserName()); + return topMembers; + }).collect(Collectors.toList()); + } else if (rankingType == 3) { + List identifyTop = applyService.getIdentifyTop(dto); + return identifyTop.stream().map(item -> { + FtbCultivateStatisticsVO.TopMembers topMembers = new FtbCultivateStatisticsVO.TopMembers(); + topMembers.setAbscissa(item.getPassRate().toString()); + topMembers.setOrdinate(item.getUserName()); + return topMembers; + }).collect(Collectors.toList()); + + } + return null; + } + + @Override + public NumberofAppSessions trainingStatisticsV2(FtbCultivateStatisticsDTO dto) { + NumberofAppSessions numberofAppSessions = new NumberofAppSessions(); + List organizeIdsAdnChildByUserId = processWithOrgIds(dto); + if (CollUtil.isEmpty(organizeIdsAdnChildByUserId)) { + return numberofAppSessions; + } + List listFeign = userApiV2Util.getUserListForOrgIds(organizeIdsAdnChildByUserId, null); + if (CollUtil.isEmpty(listFeign)) { + return numberofAppSessions; + } + + List userIds = listFeign.stream().map(UserPageListVO::getId).distinct().collect(Collectors.toList()); + dto.setUserIds(userIds); + + // 数据统计计算 + calculateStatistics(dto, numberofAppSessions); + + return numberofAppSessions; + } + + /** + * 处理有多个组织ID的情况 + */ + private List processWithOrgIds(FtbCultivateStatisticsDTO dto) { + List orgListForCurrUser = userApiV2Util.getOrgListForCurrUser( + List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT), null); + + if (CollUtil.isEmpty(orgListForCurrUser)) { + return Collections.emptyList(); + } + // 获取组织id + return orgListForCurrUser.stream() + .map(OrganizeGeneralDetailVO::getId).filter(vo -> dto.getOrgIds().contains(vo)) + .collect(Collectors.toList()); + } + + /** + * 计算各项统计数据 + */ + private void calculateStatistics(FtbCultivateStatisticsDTO dto, NumberofAppSessions numberofAppSessions) { + // 完课率 已学习/学习中+已学习 + Integer courseCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRate(dto, List.of(1)); + Integer denominatorCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRate(dto, List.of(0, 1, 2)); + + ExamStatisticsVo vo = examUserService.statisticsNew(dto); + // 考试整体总合格率 + String passRate = vo.getCurrPassRate(); + + IdentifyStatisticsForNewVo identifyStatistics = applyService.getHandsOnStatistics(dto); + + // 已学习 + numberofAppSessions.setNumberOfStudents(BigDecimal.valueOf(courseCompletionRate)); + // 学习中+已学习 + numberofAppSessions.setNumberOfCourse(BigDecimal.valueOf(denominatorCompletionRate)); + // 完课率 + numberofAppSessions.setCourseCompletionRate(getCompletionRate(courseCompletionRate, denominatorCompletionRate)); + + numberofAppSessions.setExamTotleRate(passRate); + // 考试总人数 + numberofAppSessions.setTotalNumberExam(vo.getCurrExamTotleNum()); + // 考试合格人数 + numberofAppSessions.setNumberOfExam(BigDecimal.valueOf(Long.parseLong(vo.getCurrExamPassNum()))); + + // 实操合格率 + numberofAppSessions.setIdentifyPassRate(identifyStatistics.getPassRate()); + // 实操合格人数 + numberofAppSessions.setNumberOfOperation(BigDecimal.valueOf(identifyStatistics.getPassNumber())); + // 鉴定总人数 + numberofAppSessions.setTotalNumberOfAppraisals(BigDecimal.valueOf(identifyStatistics.getTotalPeople())); + } + + @Override + public List trainingStatistics(FtbCultivateStatisticsDTO dto) { + List reultList = Collections.synchronizedList(new ArrayList()); + List organizeIdsAdnChildByUserId = new ArrayList<>(); + + Map orgNameMap = new HashMap<>(); + if (CollUtil.isNotEmpty(dto.getOrgIds())) { + List orgIds = dto.getOrgIds(); + if (StringUtil.isNotEmpty(dto.getOrgId())) { + orgIds.add(dto.getOrgId()); + } + organizeIdsAdnChildByUserId.addAll(orgIds); + List childrenOneLevelById = userApiV2Util.batchOrganizeNextLevel(orgIds, null); + if (CollUtil.isNotEmpty(childrenOneLevelById)) { + organizeIdsAdnChildByUserId.addAll(childrenOneLevelById.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + } + List orgListForCurrUser = userApiV2Util.getOrgListForCurrUser( + List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT), null); + + if (CollUtil.isEmpty(orgListForCurrUser)) { + return reultList; + } + + List intersection = UserApiV2Util.getIntersection(organizeIdsAdnChildByUserId, orgListForCurrUser.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + if (CollUtil.isEmpty(intersection)) { + return reultList; + } else { + organizeIdsAdnChildByUserId = intersection; + } + } else if (StringUtil.isEmpty(dto.getOrgId())) { + List orgListTreeeForCurrUser = userApiV2Util.getOrgListTreeForCurrUser( + List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT)); + organizeIdsAdnChildByUserId = userApiV2Util.getTopOrgList(orgListTreeeForCurrUser); + if (CollUtil.isEmpty(organizeIdsAdnChildByUserId)) { + return reultList; + } + } else { + List childrenOneLevelById = userApiV2Util.organizeNextLevel(dto.getOrgId(), null); + if (CollUtil.isNotEmpty(childrenOneLevelById)) { + organizeIdsAdnChildByUserId.addAll(childrenOneLevelById.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + } + List orgListForCurrUser = userApiV2Util.getOrgListForCurrUser( + List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT), null); + if (CollUtil.isEmpty(orgListForCurrUser)) { + return reultList; + } + List intersection = UserApiV2Util.getIntersection(organizeIdsAdnChildByUserId, orgListForCurrUser.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + if (CollUtil.isEmpty(intersection)) { + return reultList; + } else { + organizeIdsAdnChildByUserId = intersection; + } + + } + if (CollUtil.isNotEmpty(organizeIdsAdnChildByUserId)) { + List organizeByIds = userApiV2Util.organizesByOrganizeIds(organizeIdsAdnChildByUserId, null); + if (CollUtil.isNotEmpty(organizeByIds)) { + orgNameMap = + organizeByIds.stream() + .collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, OrganizeGeneralDetailVO::getName, (a, b) -> a)); + } + } + Map> orgUserMap = new HashMap<>(); + List allUserLists = userApiV2Util.getUserListForOrgIds(organizeIdsAdnChildByUserId, null); + if (CollUtil.isNotEmpty(allUserLists)) { + orgUserMap = allUserLists.stream().collect(Collectors.groupingBy(UserPageListVO::getOrganizeId)); + } + + String tenantId = UserProvider.getUser().getTenantId(); + List organizeManagerNodeVOS = userApiV2Util.queryAllOrgReturnTree(null); + Map orgTreeMap = new HashMap<>(); + UserApiV2Util.convertToOrgMap(organizeManagerNodeVOS, orgTreeMap); + final Map> orgUserMap1 = orgUserMap; + final Map orgNameMap1 = orgNameMap; + + organizeIdsAdnChildByUserId.forEach(str -> { + FtbCultivateStatisticsDTO newDto = BeanUtil.toBean(dto, FtbCultivateStatisticsDTO.class); + + // 查询当前组织下所有组织的信息 包含关系 + List selectUserIds = new ArrayList<>(); + List childrenById = new ArrayList<>(); + OrganizeManagerNodeVO organizeManagerNodeVO = orgTreeMap.get(str); + runGetCurrAndNextLevel(childrenById, organizeManagerNodeVO); + newDto.setOrgIds(childrenById); + if (CollUtil.isNotEmpty(childrenById)) { + + for (String s : childrenById) { + List listVOS = orgUserMap1.get(s); + if (CollUtil.isNotEmpty(listVOS)) { + selectUserIds.addAll(listVOS.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } + } + newDto.setUserIds(selectUserIds); + } + NumberofAppSessions numberofAppSessions = new NumberofAppSessions(); + numberofAppSessions.setOrgId(str); + numberofAppSessions.setOrgName(orgNameMap1.get(str)); + if (CollUtil.isEmpty(newDto.getUserIds())) { + reultList.add(numberofAppSessions); + return; + } +// try { +// TenantDataSourceUtil.switchTenant(tenantId);//静态方法切库 +// } catch (LoginException e) { +// throw new RuntimeException(e); +// } + // 完课率 已学习/学习中+已学习 + newDto.setOrgIds(new ArrayList<>()); + Integer courseCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRate(newDto, List.of(1)); + Integer denominatorCompletionRate = ftbCultivateStatisticsMapper.courseCompletionRate(newDto, List.of(0, 1, 2)); + // 已学习 + numberofAppSessions.setNumberOfStudents(BigDecimal.valueOf(courseCompletionRate)); + // 学习中+已学习 + numberofAppSessions.setNumberOfCourse(BigDecimal.valueOf(denominatorCompletionRate)); + // 完课率 + numberofAppSessions.setCourseCompletionRate(getCompletionRate(courseCompletionRate, denominatorCompletionRate)); + ExamStatisticsVo vo = examUserService.statisticsNew(newDto); + // 考试整体总合格率 + String passRate = vo.getCurrPassRate(); + numberofAppSessions.setExamTotleRate(passRate); + // 考试总人数 + numberofAppSessions.setTotalNumberExam(vo.getCurrExamTotleNum()); + // 考试合格人数 + numberofAppSessions.setNumberOfExam(BigDecimal.valueOf(Long.parseLong(vo.getCurrExamPassNum()))); + IdentifyStatisticsForNewVo identifyStatistics = applyService.getHandsOnStatistics(newDto); + // 实操合格率 + numberofAppSessions.setIdentifyPassRate(identifyStatistics.getPassRate()); + // 实操合格人数 + numberofAppSessions.setNumberOfOperation(BigDecimal.valueOf(identifyStatistics.getPassNumber())); + // 鉴定总人数 + numberofAppSessions.setTotalNumberOfAppraisals(BigDecimal.valueOf(identifyStatistics.getTotalPeople())); + reultList.add(numberofAppSessions); + }); + + return reultList; + } + + /** + * 获取组织tree下的所有 组织ids + * + * @param childrenById + * @param organizeManagerNodeVO 节点tree + */ + public void runGetCurrAndNextLevel(List childrenById, OrganizeManagerNodeVO organizeManagerNodeVO) { + if (organizeManagerNodeVO == null) { + return; + } + childrenById.add(organizeManagerNodeVO.getId()); + List children = organizeManagerNodeVO.getChildren(); + if (CollUtil.isNotEmpty(children)) { + for (OrganizeManagerNodeVO child : children) { + runGetCurrAndNextLevel(childrenById, child); + } + } + } + + + /** + * 获取用户列表 + * + * @param dto + */ + private void getUserList(FtbCultivateStatisticsDTO dto) { + List listFeign = userApiV2Util.getUserListForOrgIds(dto.getOrgIds(), null); + if (CollUtil.isNotEmpty(listFeign)) { + List userIds = listFeign.stream().map(UserPageListVO::getId).distinct().collect(Collectors.toList()); + dto.setUserIds(userIds); + } else { + log.info("课程学习统计,获取组织id:{},所对应的人员为空", dto.getOrgId()); + } + } + + /** + * 计算完成率 + * + * @param num 完成的数量 + * @param total 总数量 + * @return 完成率 + */ + private static BigDecimal getCompletionRate(Integer num, Integer total) { + if (total == 0) { + return BigDecimal.ZERO; + } + return new BigDecimal(num).divide(new BigDecimal(total), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStoreStatisticsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStoreStatisticsServiceImpl.java new file mode 100644 index 0000000..8d327b2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateStoreStatisticsServiceImpl.java @@ -0,0 +1,1054 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivateExamService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.service.FtbCultivateStoreStatisticService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.emnus.MiniAppEnum; +import jnpf.emnus.WorkStatusEnum; +import jnpf.emnus.WorkTypeEnum; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.dto.storestatistics.*; +import jnpf.model.cultivate.dto.storestatistics.dto.StoreCultivateTaskDto; +import jnpf.model.cultivate.dto.storestatistics.dto.StoreOfflineTrainDto; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.resp.AppExamListVo; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplyListAppReq; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyListAppVo; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.position.FtbCultivateStoreCountVO; +import jnpf.model.cultivate.vo.position.FtbCultivateWorkerCountVO; +import jnpf.model.cultivate.vo.position.FtbPersonTrainingStatisticsVO; +import jnpf.model.cultivate.vo.position.FtbStoreManagerTrainingStatisticsVO; +import jnpf.model.enums.CourseEnums; +import jnpf.model.thousandsfaces.TodayWorkVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbCultivateStoreStatisticsServiceImpl implements FtbCultivateStoreStatisticService { + private static final String IDENTITY_CODE = "startEvaluation";//鉴定权限编码 + private static final String EXAM_CODE = "startMarking";//批阅考试权限编码 + + @Autowired + CultivateIdentifyApplyTableBackupsMapper cultivateIdentifyApplyTableBackupsMapper; + @Autowired + FtbCultivatePromotionMemberNewMapper ftbCultivatePromotionMemberNewMapper; + @Autowired + FtbCultivatePromotionNewMapper ftbCultivatePromotionNewMapper; + @Autowired + private FtbCultivateLearnTaskAssignmentMapper assignmentMapper; + @Autowired + private CultivateIdentifyApplyMapper identifyApplyMapper; + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + @Autowired + private FtbCultivateOfflineTrainMapper ftbCultivateOfflineTrainMapper; + /** + * 考试服务 + */ + @Autowired + private FtbCultivateExamService examService; + @Autowired + private FtbCultivatePositionStatisticesMapper ftbCultivatePositionStatisticesMapper; + @Autowired + private UserApiV2Util userApiV2Util; + @Autowired + private FtbCultivatePromotionNewService promotionService; + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Autowired + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Autowired + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + + + @Override + public FtbCultivateStoreCountVO getCultivateCount(FtbCultivateStoreStatisticsReq req) { + + FtbCultivateStoreCountVO returnDto = new FtbCultivateStoreCountVO(); +// 获取当前登录的用户id + String userId = UserProvider.getLoginUserId(); + + UserInfo userInfo = UserProvider.getUser(); + String mainPositionId = ""; + String gradeId = ""; + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, userInfo.getTenantId()); + + // 判断用户绑定信息是否为null + if (userPrimaryBoundOne == null || StringUtils.isBlank(userPrimaryBoundOne.getPositionId())) { + returnDto.setPositionNotLearnedCount(0L); + returnDto.setPositionTotleLearnedCount(0L); + returnDto.setPositionCompleteLearnedCount(0L); + return returnDto; + } + + mainPositionId = userPrimaryBoundOne.getPositionId(); + gradeId = userPrimaryBoundOne.getGradeId(); + + // 根据V2岗位学习匹配规则查询课程 + selectCourseNumByV2Rule(returnDto, mainPositionId, gradeId, userId); + + return returnDto; + + } + + + /** + * 根据V2岗位学习匹配规则查询课程数量 + * 匹配规则: + * 1. 如果岗位未配置到职级(isConfiguredToGrade=0):查询该岗位下的所有课程 + * 2. 如果岗位配置到职级(isConfiguredToGrade=1): + * - 用户有职级:查询该岗位+该职级的课程 + * - 用户无职级:返回空结果 + * + * @param returnDto 返回对象 + * @param positionId 岗位ID + * @param gradeId 职级ID + * @param userId 用户ID + */ + private void selectCourseNumByV2Rule(FtbCultivateStoreCountVO returnDto, String positionId, String gradeId, String userId) { + // 1. 查询岗位学习配置 + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getPostId, positionId) + .eq(FtbCultivatePosition::getEnabledMark, 0) + .eq(FtbCultivatePosition::getIsGrounding, 1); // 只查询上架的岗位学习 + FtbCultivatePosition cultivatePosition = ftbCultivatePositionMapper.selectOne(positionWrapper); + + if (cultivatePosition == null) { + // 该岗位尚未配置岗位学习 + returnDto.setPositionNotLearnedCount(0L); + returnDto.setPositionTotleLearnedCount(0L); + returnDto.setPositionCompleteLearnedCount(0L); + return; + } + + // 2. 如果配置到职级但用户无职级,返回空结果 + if (cultivatePosition.getIsConfiguredToGrade() == 1 && StringUtils.isBlank(gradeId)) { + returnDto.setPositionNotLearnedCount(0L); + returnDto.setPositionTotleLearnedCount(0L); + returnDto.setPositionCompleteLearnedCount(0L); + return; + } + + // 3. 使用XML关联查询获取所有课程ID列表(直接JOIN课程表过滤) + List courseIds = ftbCultivatePositionCourseMapper.queryCourseIdsByPositionAndGrade( + cultivatePosition.getId(), + cultivatePosition.getIsConfiguredToGrade(), + gradeId + ); + + // 4. 统计课程总数 + Long courseTotleNum = CollUtil.isEmpty(courseIds) ? 0L : (long) courseIds.size(); + + if (courseTotleNum == 0) { + returnDto.setPositionNotLearnedCount(0L); + returnDto.setPositionTotleLearnedCount(0L); + returnDto.setPositionCompleteLearnedCount(0L); + return; + } + + // 5. 根据课程ID列表和用户ID统计已学习课程数(state=1表示已完成) + Long studyCourseTotleNum = ftbCultivatePositionCourceLearningMapper.countCompletedCoursesByUserAndCourseIds( + courseIds, + userId + ); + + if (studyCourseTotleNum == null) { + studyCourseTotleNum = 0L; + } + + Long noStudyCourseNum = courseTotleNum - studyCourseTotleNum; + + returnDto.setPositionNotLearnedCount(noStudyCourseNum); + returnDto.setPositionTotleLearnedCount(courseTotleNum); + returnDto.setPositionCompleteLearnedCount(studyCourseTotleNum); + } + + /** + * 根据V2岗位学习匹配规则查询个人培训统计的课程数量 + * 匹配规则: + * 1. 如果岗位未配置到职级(isConfiguredToGrade=0):查询该岗位下的所有课程 + * 2. 如果岗位配置到职级(isConfiguredToGrade=1): + * - 用户有职级:查询该岗位+该职级的课程 + * - 用户无职级:返回空结果 + * + * @param vo 个人培训统计VO + * @param positionId 岗位ID + * @param gradeId 职级ID + * @param userId 用户ID + */ + private void selectPersonCourseNumByV2Rule(FtbPersonTrainingStatisticsVO vo, String positionId, String gradeId, String userId) { + // 1. 查询岗位学习配置 + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getPostId, positionId) + .eq(FtbCultivatePosition::getEnabledMark, 0) + .eq(FtbCultivatePosition::getIsGrounding, 1); // 只查询上架的岗位学习 + FtbCultivatePosition cultivatePosition = ftbCultivatePositionMapper.selectOne(positionWrapper); + + if (cultivatePosition == null) { + // 该岗位尚未配置岗位学习 + vo.setCourseTotalCnt(0L); + vo.setCourseCompleteCnt(0L); + vo.setCourseStudyMinute("0"); + return; + } + + // 2. 如果配置到职级但用户无职级,返回空结果 + if (cultivatePosition.getIsConfiguredToGrade() == 1 && StringUtils.isBlank(gradeId)) { + vo.setCourseTotalCnt(0L); + vo.setCourseCompleteCnt(0L); + vo.setCourseStudyMinute("0"); + return; + } + + // 3. 使用XML关联查询获取所有课程ID列表(直接JOIN课程表过滤) + List courseIds = ftbCultivatePositionCourseMapper.queryCourseIdsByPositionAndGrade( + cultivatePosition.getId(), + cultivatePosition.getIsConfiguredToGrade(), + gradeId + ); + + // 4. 统计课程总数 + Long courseTotleNum = CollUtil.isEmpty(courseIds) ? 0L : (long) courseIds.size(); + + if (courseTotleNum == 0) { + vo.setCourseTotalCnt(0L); + vo.setCourseCompleteCnt(0L); + vo.setCourseStudyMinute("0"); + return; + } + + // 5. 根据课程ID列表和用户ID统计已学习课程数(state=1表示已完成) + Long studyCourseTotleNum = ftbCultivatePositionCourceLearningMapper.countCompletedCoursesByUserAndCourseIds( + courseIds, + userId + ); + + if (studyCourseTotleNum == null) { + studyCourseTotleNum = 0L; + } + + // 6. 设置课程总数和已完成数 + vo.setCourseTotalCnt(courseTotleNum); + vo.setCourseCompleteCnt(studyCourseTotleNum); + + // 7. 统计学习时长(分钟) + if (CollUtil.isNotEmpty(courseIds)) { + Long studyTime = ftbCultivatePositionCourceLearningMapper.queryStudyTime(courseIds, userId); + if (studyTime != null && studyTime > 0) { + Long result = studyTime / 60; + vo.setCourseStudyMinute(result.toString()); + } else { + vo.setCourseStudyMinute("0"); + } + } else { + vo.setCourseStudyMinute("0"); + } + } + + + /** + * 我的考试列表 + * + * @param req + * @return + */ + @Override + public List queryMyExamList(StoreStatisticsMyExamReq req) { + //今日工作-我的考试<已知的岗位考试及自定义考试>列表 + //待完成(待考试)/已逾期(已逾期)/已完成(已完成) + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + List ftbCultivateExamUsers = ftbCultivateExamUserMapper.storeMyExamList(loginUserId); + if (CollUtil.isEmpty(ftbCultivateExamUsers)) { + return new ArrayList<>(); + } + String today = DateUtil.format(new Date(), "yyyy-MM-dd"); + List returnList = new ArrayList<>(); + for (AppExamListVo entity : ftbCultivateExamUsers) { + + TodayWorkVo todayWorkVo = new TodayWorkVo(); + todayWorkVo.setWorkType(WorkTypeEnum.MY_EXAM); + todayWorkVo.setCreatorTime(entity.getCreateTime()); + Map paramMap = new HashMap<>(); + paramMap.put("examId", entity.getExamId()); + paramMap.put("userExamId", entity.getId()); +// 0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀 + if (entity.getStatus().equals(0)) { + todayWorkVo.setMiniApp(MiniAppEnum.EXAM_TODO_DETAIL); + if (entity.getExamType().equals(0)) { + todayWorkVo.setCreatorTime(entity.getCreateTime()); + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + } else { + if (entity.getStartTime() == null) { + todayWorkVo.setCreatorTime(entity.getCreateTime()); + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + } else if (entity.getStartTime().getTime() > new Date().getTime()) { + continue; + } else { + todayWorkVo.setCreatorTime(entity.getCreateTime()); + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + } + } + } else if (entity.getStatus().equals(1)) { + todayWorkVo.setMiniApp(MiniAppEnum.EXAM_TODO_DETAIL); + todayWorkVo.setCreatorTime(entity.getFinishTime()); + String completeTime = DateUtil.format(entity.getFinishTime(), "yyyy-MM-dd"); + if (today.equals(completeTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.FINISHED); + } else { + continue; + } + } else if (List.of(3, 4, 5).contains(entity.getStatus())) { + todayWorkVo.setMiniApp(MiniAppEnum.EXAM_RESULT_DETAIL); + todayWorkVo.setCreatorTime(entity.getFinishTime()); + String completeTime = DateUtil.format(entity.getFinishTime(), "yyyy-MM-dd"); + if (today.equals(completeTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.FINISHED); + } else { + continue; + } + } else if (entity.getStatus().equals(2)) { + todayWorkVo.setMiniApp(MiniAppEnum.EXAM_TODO_DETAIL); + todayWorkVo.setCreatorTime(entity.getLastModifyTime()); + String lastModifyTime = DateUtil.format(entity.getLastModifyTime(), "yyyy-MM-dd"); + if (today.equals(lastModifyTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.OVERDUE); + } else { + continue; + } + } +// + todayWorkVo.setTitle(entity.getExamName()); + todayWorkVo.setFinishDate(entity.getEndTime()); + todayWorkVo.setWorkId(entity.getId()); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + returnList.add(todayWorkVo); + } + return returnList; + } + + /** + * 待我批阅 + * + * @param req + * @return + */ + @Override + public List queryWaitMyReadOver(StoreStatisticsWaitMyCheckExamReq req) { + //待完成(待批阅)/已完成(已批阅) + //带我批阅 + String userId = UserProvider.getLoginUserId(); + String tenantId = UserProvider.getUser().getTenantId(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + + List btnList = userApiV2Util.queryUserBtnPermissionList(userPrimaryBoundOne); + if (!btnList.contains(EXAM_CODE)) { + return new ArrayList<>(); + } + List powerUserIdsForTrain = userApiV2Util.getPowerUserIdsForTrain(userId, tenantId); + //今日开始时间 + Date todayStartTime = DateUtil.beginOfDay(new Date()); + //今日结束时间 + Date todayEndTime = DateUtil.endOfDay(new Date()); + List returnList = new ArrayList<>(); + Set examIdList = new HashSet<>(); + Map examMap = new HashMap<>(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getFinishTime, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, FtbCultivateExamUser::getReviewerUserIds, FtbCultivateExamUser::getLastModifyTime) + .eq(FtbCultivateExamUser::getStatus, 1) //(0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + .eq(FtbCultivateExamUser::getIsJoinCount, 0) + .in(CollUtil.isNotEmpty(powerUserIdsForTrain), FtbCultivateExamUser::getUserId, powerUserIdsForTrain) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List ftbCultivateExamUsers = ftbCultivateExamUserMapper.selectList(wrapper); + + LambdaQueryWrapper wrapperTodayComplate = new LambdaQueryWrapper() + .select(FtbCultivateExamUser::getId, FtbCultivateExamUser::getStatus, FtbCultivateExamUser::getUserId, FtbCultivateExamUser::getFinishTime, FtbCultivateExamUser::getExamId, FtbCultivateExamUser::getExamSource, FtbCultivateExamUser::getReviewerUserIds, FtbCultivateExamUser::getLastModifyTime) + .in(FtbCultivateExamUser::getStatus, 3, 4, 5) //(0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀) + .eq(FtbCultivateExamUser::getIsJoinCount, 0) + .eq(FtbCultivateExamUser::getReviewerUserId, userId) + .between(FtbCultivateExamUser::getLastModifyTime, todayStartTime, todayEndTime) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List complateList = ftbCultivateExamUserMapper.selectList(wrapperTodayComplate); + + if (CollectionUtil.isNotEmpty(complateList)) { + for (FtbCultivateExamUser entity : complateList) { + examIdList.add(entity.getExamId()); + } + } + if (CollectionUtil.isNotEmpty(ftbCultivateExamUsers)) { + for (FtbCultivateExamUser entity : ftbCultivateExamUsers) { + examIdList.add(entity.getExamId()); + } + } + if (CollUtil.isNotEmpty(examIdList)) { + LambdaQueryWrapper wrapper1 = new LambdaQueryWrapper() + .select(FtbCultivateExam::getId, FtbCultivateExam::getExamId, FtbCultivateExam::getExamName, FtbCultivateExam::getReviewerAppoint, FtbCultivateExam::getReviewerAppointListConfig, FtbCultivateExam::getReviewer) + .in(FtbCultivateExam::getId, examIdList) + .eq(FtbCultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List ftbCultivateExams = examService.list(wrapper1); + if (CollectionUtil.isNotEmpty(ftbCultivateExams)) { + for (FtbCultivateExam exam : ftbCultivateExams) { + examMap.put(exam.getId(), exam); + } + } + } + + + if (CollectionUtil.isNotEmpty(ftbCultivateExamUsers)) { + + for (FtbCultivateExamUser entity : ftbCultivateExamUsers) { + FtbCultivateExam ftbCultivateExam = examMap.get(entity.getExamId()); + if (ftbCultivateExam != null) { + TodayWorkVo todayWorkVo = new TodayWorkVo(); + todayWorkVo.setWorkType(WorkTypeEnum.MARK_EXAM); + todayWorkVo.setCreatorTime(entity.getFinishTime()); + //0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀 + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + todayWorkVo.setTitle(ftbCultivateExam.getExamName()); + todayWorkVo.setFinishDate(entity.getEndTime()); + todayWorkVo.setWorkId(entity.getId()); + todayWorkVo.setMiniApp(MiniAppEnum.EXAM_CHECK_DETAIL); + Map paramMap = new HashMap<>(); + paramMap.put("examId", entity.getExamId()); + paramMap.put("id", entity.getId()); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + returnList.add(todayWorkVo); + } + + } + } + //今日完成批阅 + if (CollectionUtil.isNotEmpty(complateList)) { + + for (FtbCultivateExamUser entity : complateList) { + FtbCultivateExam ftbCultivateExam = examMap.get(entity.getExamId()); + if (ftbCultivateExam != null) { + TodayWorkVo todayWorkVo = new TodayWorkVo(); + todayWorkVo.setWorkType(WorkTypeEnum.TRAIN); + todayWorkVo.setCreatorTime(entity.getFinishTime()); + //0待考试,1待批阅,2已逾期,3合格,4不合格 5优秀 + todayWorkVo.setWorkStatus(WorkStatusEnum.FINISHED); + todayWorkVo.setTitle(ftbCultivateExam.getExamName()); + todayWorkVo.setFinishDate(entity.getEndTime()); + todayWorkVo.setWorkId(entity.getId()); + todayWorkVo.setMiniApp(MiniAppEnum.EXAM_DETAIL); + Map paramMap = new HashMap<>(); + paramMap.put("id", entity.getId()); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + returnList.add(todayWorkVo); + } + + } + } + return returnList; + } + + + @Override + public List queryOfflineTrainList(StoreOfflineTrainReq req) { + //今日工作-待参与及参与中的线下培训<负责人及参与人均属于相关人员>列表 + //待完成(未开始、进行中)/已完成(已结束) + List returnList = new ArrayList<>(); + String userId = UserProvider.getLoginUserId(); + //今日开始时间 + Date todayStartTime = DateUtil.beginOfDay(new Date()); + //今日结束时间 + Date todayEndTime = DateUtil.endOfDay(new Date()); + String today = DateUtil.format(new Date(), "yyyy-MM-dd"); + //查询参与人员 + List storeOfflineTrainDtos = ftbCultivateOfflineTrainMapper.queryMyOfflineTrainList(userId, todayStartTime, todayEndTime); + if (CollUtil.isNotEmpty(storeOfflineTrainDtos)) { + + for (StoreOfflineTrainDto entity : storeOfflineTrainDtos) { + TodayWorkVo todayWorkVo = new TodayWorkVo(); + todayWorkVo.setWorkType(WorkTypeEnum.OFFLINE_TRAIN); + + //0未开始,1已结束,2进行中 + if ((entity.getTrainStartTime().getTime() <= new Date().getTime()) && (entity.getTrainEndTime().getTime() >= new Date().getTime())) { + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + todayWorkVo.setCreatorTime(entity.getTrainStartTime()); + } + //未开始 + else if (entity.getTrainStartTime().getTime() > new Date().getTime()) { + continue; + } + //如果当前时间大于结束时间 一完成 + else if (entity.getTrainEndTime().getTime() < new Date().getTime()) { + todayWorkVo.setCreatorTime(entity.getTrainEndTime()); + String endTime = DateUtil.format(entity.getTrainEndTime(), "yyyy-MM-dd"); + if (today.equals(endTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.FINISHED); + } else { + continue; + } + } + todayWorkVo.setTitle(entity.getName()); + todayWorkVo.setFinishDate(entity.getTrainEndTime()); + todayWorkVo.setWorkId(entity.getId()); + todayWorkVo.setMiniApp(MiniAppEnum.TRAIN_TASK_OFFLINE_DETAIL); + Map paramMap = new HashMap<>(); + paramMap.put("id", entity.getId()); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + returnList.add(todayWorkVo); + } + } + //查询负责人 + List headerList = ftbCultivateOfflineTrainMapper.queryMyOfflineTrainingHeader(userId, todayStartTime, todayEndTime); + if (CollUtil.isNotEmpty(headerList)) { + + for (StoreOfflineTrainDto entity : headerList) { + TodayWorkVo todayWorkVo = new TodayWorkVo(); + todayWorkVo.setWorkType(WorkTypeEnum.OFFLINE_TRAIN); + + //0未开始,1已结束,2进行中,3已撤回 + //判断当前时间是否再开始和结束时间范围内 + if ((entity.getTrainStartTime().getTime() <= new Date().getTime()) && (entity.getTrainEndTime().getTime() >= new Date().getTime())) { + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + todayWorkVo.setCreatorTime(entity.getTrainStartTime()); + } + //未开始 + else if (entity.getTrainStartTime().getTime() > new Date().getTime()) { + continue; + } else if (entity.getTrainEndTime().getTime() < new Date().getTime()) { + todayWorkVo.setCreatorTime(entity.getTrainEndTime()); + String endTime = DateUtil.format(entity.getTrainEndTime(), "yyyy-MM-dd"); + if (today.equals(endTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.FINISHED); + } else { + continue; + } + } + + todayWorkVo.setTitle(entity.getName()); + todayWorkVo.setFinishDate(entity.getTrainEndTime()); + todayWorkVo.setWorkId(entity.getId()); + todayWorkVo.setMiniApp(MiniAppEnum.TRAIN_TASK_OFFLINE_DETAIL); + Map paramMap = new HashMap<>(); + paramMap.put("id", entity.getId()); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + returnList.add(todayWorkVo); + } + } + return returnList; + } + + @Override + public List storeMyTaskList(StoreOfflineTrainReq req) { + //今日工作-我的任务(当日开始的培训任务)列表 + //待完成(待开始)/已逾期(已逾期)/已完成(已完成) + String userId = UserProvider.getLoginUserId(); + List list = assignmentMapper.storeMyTaskList(userId); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + List returnList = new ArrayList<>(); + String today = DateUtil.format(new Date(), "yyyy-MM-dd"); + for (StoreCultivateTaskDto entity : list) { + TodayWorkVo todayWorkVo = new TodayWorkVo(); + todayWorkVo.setWorkType(WorkTypeEnum.TRAIN); + //学习状态 0未开始,1进行中,2已完成,3已逾期 + if (entity.getStudyStats().equals(1)) { + todayWorkVo.setCreatorTime(entity.getCreateTime()); + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + } else if (entity.getStudyStats().equals(0)) { + if (entity.getTaskType().equals(1)) { + if ((entity.getTimeLimitStartTime().getTime() <= new Date().getTime()) && (entity.getTimeLimitEndTime().getTime() >= new Date().getTime())) { + todayWorkVo.setCreatorTime(entity.getTimeLimitStartTime()); + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + } else { + continue; + } + } else { + todayWorkVo.setCreatorTime(entity.getCreateTime()); + todayWorkVo.setWorkStatus(WorkStatusEnum.TO_DO); + } + } else if (entity.getStudyStats().equals(2)) { + todayWorkVo.setCreatorTime(entity.getLearningEndTime()); + String learnEndTime = DateUtil.format(entity.getLearningEndTime(), "yyyy-MM-dd"); + if (learnEndTime != null && today.equals(learnEndTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.FINISHED); + } else { + continue; + } + } else if (entity.getStudyStats().equals(3)) { + todayWorkVo.setCreatorTime(entity.getTimeLimitEndTime()); + String limitEndTime = DateUtil.format(entity.getTimeLimitEndTime(), "yyyy-MM-dd"); + if (limitEndTime != null && today.equals(limitEndTime)) { + todayWorkVo.setWorkStatus(WorkStatusEnum.OVERDUE); + } else { + continue; + } + } + Map paramMap = new HashMap<>(); + paramMap.put("id", entity.getTaskId()); + todayWorkVo.setTitle(entity.getTaskName()); + todayWorkVo.setFinishDate(entity.getTimeLimitEndTime()); + todayWorkVo.setWorkId(entity.getId()); + todayWorkVo.setMiniApp(MiniAppEnum.TRAIN_TASK_DETAIL); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + returnList.add(todayWorkVo); + } + return returnList; + } + + /** + * 今日工作-我的鉴定列表 + * + * @param req + * @return + */ + @Override + public List storeMyIdentityList(StoreIdentityReq req) { + String loginUserId = UserProvider.getLoginUserId(); + List returnList = new ArrayList<>(); + + if (StringUtils.isEmpty(loginUserId)) { + return returnList; + } + List userIds = new ArrayList<>(); + //权限 + 鉴定人是自己的 + List powerUserIds = userApiV2Util.getPermissionUserIds(); + List myUserIds = this.identifyApplyMapper.queryMyIdentifyUserList(loginUserId); + + //待我鉴定的 + Page waitMyIdentifyPage = Page.of(1000, -1); + V2IdentifyApplyListAppReq waitMyIdentifyReq = new V2IdentifyApplyListAppReq(); + waitMyIdentifyPage = this.identifyApplyMapper.queryAppIdentifyApplyListWaiting(waitMyIdentifyPage, waitMyIdentifyReq, powerUserIds, myUserIds, loginUserId); + + + processCultivateIdentifyApply(waitMyIdentifyPage.getRecords(), WorkStatusEnum.TO_DO, MiniAppEnum.ACTUAL_IDENTIFY_TODO_DETAIL, WorkTypeEnum.ACTUAL_IDENTIFY, returnList); + //查询今日已完成的鉴定列表 + Page todayCompletePage = Page.of(1000, -1); + V2IdentifyApplyListAppReq todayCompleteReq = new V2IdentifyApplyListAppReq(); + // 设置今天的开始和结束时间 + todayCompleteReq.setStartTime(DateUtil.beginOfDay(new Date())); + todayCompleteReq.setEndTime(DateUtil.endOfDay(new Date())); + todayCompletePage = this.identifyApplyMapper.queryAppIdentifyApplyListComplete(todayCompletePage, todayCompleteReq, userIds, loginUserId); + processCultivateIdentifyApply(todayCompletePage.getRecords(), WorkStatusEnum.FINISHED, MiniAppEnum.ACTUAL_IDENTIFY_DETAIL, WorkTypeEnum.ACTUAL_IDENTIFY, returnList); + + + return returnList; + } + + /** + * 处理培育认定申请工作项 + * + * @param planList 培育认定申请列表 + * @param workStatus 工作状态枚举 + * @param miniAppEnum 小程序枚举 + * @param workTypeEnum 工作类型枚举 + * @param result 结果列表,用于存储处理后的今日工作项 + */ + private void processCultivateIdentifyApply(List planList, WorkStatusEnum + workStatus, MiniAppEnum miniAppEnum, WorkTypeEnum workTypeEnum, List result) { + if (CollUtil.isEmpty(planList)) { + return; + } + List todayWorkVos = new ArrayList<>(); + for (V2IdentifyApplyListAppVo identifyApply : planList) { + TodayWorkVo todayWorkVo = new TodayWorkVo(); + Map paramMap = new HashMap<>(); + paramMap.put("id", identifyApply.getId()); + if (WorkStatusEnum.FINISHED == workStatus) { + todayWorkVo.setCreatorTime(identifyApply.getCreatorTime()); + todayWorkVo.setFinishDate(identifyApply.getIdentifyTime()); + } else { + todayWorkVo.setCreatorTime(identifyApply.getCreatorTime()); + paramMap.put("flag", "0"); + } + todayWorkVo.setWorkType(workTypeEnum); + todayWorkVo.setWorkStatus(workStatus); + todayWorkVo.setTitle(identifyApply.getTableName()); + todayWorkVo.setWorkId(identifyApply.getId()); + todayWorkVo.setMiniApp(miniAppEnum); + todayWorkVo.setExtraJson(JSONUtil.toJsonStr(paramMap)); + todayWorkVos.add(todayWorkVo); + } + result.addAll(todayWorkVos); + } + + /** + * 获取员工学习统计 + * + * @param req + * @return + */ + public FtbCultivateWorkerCountVO getWorkerCultivateCount(FtbCultivateStoreStatisticsReq req) { + FtbCultivateWorkerCountVO returnDto = new FtbCultivateWorkerCountVO(); + String userId = UserProvider.getLoginUserId(); + //未完成的通用课程数量 + returnDto.setNoStudyCommonCourseNum(ftbCultivateCourseMapper.queryNoStudyCommonCourseNum(userId)); + //未完成的考试的数量 + returnDto.setNoCompleteExamNum(ftbCultivateExamUserMapper.queryMyWaitingExamNum(userId)); + return returnDto; + } + + /** + * 个人培训统计 + * + * @param req 统计参数 + * @return + */ + @Override + public FtbPersonTrainingStatisticsVO personTrainingStatistics(FtbCultivateStoreStatisticsReq req) { + String userId = UserProvider.getLoginUserId(); + String tenantId = UserProvider.getUser().getTenantId(); + FtbPersonTrainingStatisticsVO vo = new FtbPersonTrainingStatisticsVO(); + vo.initData(); + //1、学习任务 + Long taskCompleteCnt = assignmentMapper.queryTaskNum(userId, 2, req); + vo.setTaskCompleteCnt(taskCompleteCnt); + Long taskTotalCnt = assignmentMapper.queryTaskNum(userId, 0, req); + vo.setTaskTotalCnt(taskTotalCnt); + BigDecimal taskCompleteRate = BigDecimal.ZERO; + if (taskTotalCnt > 0) { + taskCompleteRate = new BigDecimal(taskCompleteCnt).divide(new BigDecimal(taskTotalCnt), 4, RoundingMode.HALF_UP); + } + vo.setTaskCompleteRate(UserApiV2Util.dealWithRate(taskCompleteRate, "")); + //2、岗位学习 + String mainPositionId = ""; + String gradeId = ""; + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne != null) { + mainPositionId = userPrimaryBoundOne.getPositionId(); + gradeId = userPrimaryBoundOne.getGradeId(); + } + + // 根据V2岗位学习匹配规则查询课程数量 + selectPersonCourseNumByV2Rule(vo, mainPositionId, gradeId, userId); + +// 3、考试 + List ftbCultivateExamUsers = ftbCultivateExamUserMapper.storeMyExamList(userId); + if (CollUtil.isNotEmpty(ftbCultivateExamUsers)) { + Long examPassCnt = calPassCnt(ftbCultivateExamUsers); + Long examTotalCnt = calTotalCnt(ftbCultivateExamUsers); + if (examTotalCnt > 0) { + vo.setExamPassRate(UserApiV2Util.dealWithRate(new BigDecimal(examPassCnt).divide(new BigDecimal(examTotalCnt), 4, RoundingMode.HALF_UP), "")); + } else { + vo.setExamPassRate(UserApiV2Util.dealWithRate(BigDecimal.ZERO, "")); + } + vo.setExamCnt(Long.valueOf(ftbCultivateExamUsers.size())); + } + + //鉴定 + LambdaQueryWrapper myWraper = Wrappers.lambdaQuery(); + myWraper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + myWraper.isNotNull(CultivateIdentifyApply::getTableId); + myWraper.eq(CultivateIdentifyApply::getIsVisible, 0); + myWraper.eq(CultivateIdentifyApply::getDeleteMark, 0); + List myList = identifyApplyMapper.selectList(myWraper); + if (CollUtil.isNotEmpty(myList)) { + Long identificationPassCnt = calIdentifyPassCnt(myList); + Long identificationTotalCnt = calIdentifyTotalCnt(myList); + if (identificationTotalCnt > 0) { + vo.setIdentificationPassRate(UserApiV2Util.dealWithRate(new BigDecimal(identificationPassCnt).divide(new BigDecimal(identificationTotalCnt), 4, RoundingMode.HALF_UP), "")); + } else { + vo.setIdentificationPassRate(UserApiV2Util.dealWithRate(BigDecimal.ZERO, "")); + } + vo.setIdentificationCnt((long) myList.size()); + } + return vo; + } + + /** + * 计算培育认定申请列表中状态为1的记录总数 + * + * @param myList 培育认定申请列表 + * @return 状态为1的培育认定申请记录总数 + */ + private Long calIdentifyTotalCnt(List myList) { + Long identificationTotalCnt = 0L; + for (CultivateIdentifyApply cultivateIdentifyApply : myList) { + if (cultivateIdentifyApply.getStatus() == 1) { + identificationTotalCnt++; + } + } + return identificationTotalCnt; + } + + /** + * 计算认证通过的数量 + * + * @param myList 认证申请列表 + * @return 认证通过的数量 + */ + private Long calIdentifyPassCnt(List myList) { + Long identificationPassCnt = 0L; + for (CultivateIdentifyApply my : myList) { + if (my.getStatus() == 1 && (my.getResult() == 1 || my.getResult() == 0)) { + identificationPassCnt++; + } + } + return identificationPassCnt; + } + + + /** + * 计算考试用户总数 + * + * @param ftbCultivateExamUsers 考试用户列表 + * @return 不包含状态为1和2的用户数量 + */ + private Long calTotalCnt(List ftbCultivateExamUsers) { + Long examTotalCnt = 0L; + for (AppExamListVo ftbCultivateExamUser : ftbCultivateExamUsers) { + if (ftbCultivateExamUser.getStatus() != 1 && ftbCultivateExamUser.getStatus() != 2) { + examTotalCnt++; + } + } + return examTotalCnt; + } + + /** + * 计算考试通过人数 + * + * @param ftbCultivateExamUsers 考试用户列表 + * @return 通过考试的人数 + */ + private Long calPassCnt(List ftbCultivateExamUsers) { + Long examPassCnt = 0L; + for (AppExamListVo ftbCultivateExamUser : ftbCultivateExamUsers) { + if (ftbCultivateExamUser.getStatus() == 3 || ftbCultivateExamUser.getStatus() == 5) { + examPassCnt++; + } + } + return examPassCnt; + } + + /** + * 工作台-店长界面统计 + * + * @param req 统计参数 + * @return 统计结果 + */ + @Override + public FtbStoreManagerTrainingStatisticsVO storeManagerTrainingStatistics(FtbCultivateStoreStatisticsReq req) { + + List powerUserIds = new ArrayList<>(); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + powerUserIds = innerPowerUserVO.getUserIds(); + } else if (innerPowerUserVO.getCode().equals(2)) { + powerUserIds.add("-1"); + } + + FtbStoreManagerTrainingStatisticsVO vo = new FtbStoreManagerTrainingStatisticsVO(); + String userId = UserProvider.getLoginUserId(); + String tenantId = UserProvider.getUser().getTenantId(); + vo.initData(); + //1、学习任务 + Long taskCompleteCnt = assignmentMapper.queryTaskNum(userId, 2, req); + Long taskTotalCnt = assignmentMapper.queryTaskNum(userId, 0, req); + BigDecimal taskCompleteRate = BigDecimal.ZERO; + if (taskTotalCnt > 0) { + taskCompleteRate = new BigDecimal(taskCompleteCnt).divide(new BigDecimal(taskTotalCnt), 4, RoundingMode.HALF_UP); + } + vo.setTaskCompleteRate(UserApiV2Util.dealWithRate(taskCompleteRate, "")); + //2、岗位学习 + String mainPositionId = ""; + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne != null) { + mainPositionId = userPrimaryBoundOne.getPositionId(); + } + String pomotionId = ftbCultivatePromotionMemberNewMapper.queryHasStudyMap(mainPositionId); + List courseIds = new ArrayList<>(); + if (StringUtils.isEmpty(pomotionId)) { + Long courseTotleNum = ftbCultivatePositionStatisticesMapper.courseTotleNum(List.of(mainPositionId)); + // 已学习课程数 + List studyCourseList = ftbCultivatePositionStatisticesMapper.queryComplateCourseLists(List.of(mainPositionId), userId); + if (CollUtil.isNotEmpty(studyCourseList)) { + courseIds = studyCourseList; + vo.setCourseCompleteCnt(Long.valueOf(courseIds.size())); + } + vo.setCourseTotalCnt(courseTotleNum); + } else { + Integer currentLearningStage = ftbCultivatePromotionMemberNewMapper.queryCurrLevel(userId, pomotionId); + if (currentLearningStage == null || currentLearningStage <= 1) { + Long courseTotleNum = ftbCultivatePositionStatisticesMapper.courseTotleNum(List.of(mainPositionId)); + // 已学习课程数 + List studyCourseList = ftbCultivatePositionStatisticesMapper.queryComplateCourseLists(List.of(mainPositionId), userId); + if (CollUtil.isNotEmpty(studyCourseList)) { + courseIds = studyCourseList; + vo.setCourseCompleteCnt(Long.valueOf(courseIds.size())); + } + vo.setCourseTotalCnt(courseTotleNum); + } else { + List promotionList = ftbCultivatePromotionNewMapper.selectAllPromoteList(pomotionId); + Map> stepMap = new HashMap<>(); + if (CollUtil.isNotEmpty(promotionList)) { + stepMap = promotionList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + } + if (CollUtil.isNotEmpty(stepMap)) { + Long completeNum = 0L; + Long totalNum = 0L; + //遍历stepMap + for (Map.Entry> entry : stepMap.entrySet()) { + Set positionSet = new HashSet<>(); + if (entry.getValue().size() <= 1) { + for (FtbCultivatePromotionPostNew promotionPostNew : entry.getValue()) { + positionSet.add(promotionPostNew.getPostId()); + } + } else { + List> maps = ftbCultivatePromotionNewMapper.queryUserChosesPostsWitId(userId, entry.getKey(), pomotionId); + if (CollUtil.isNotEmpty(maps)) { + for (Map map : maps) { + positionSet.add(map.get("postId").toString()); + } + } + } + if (CollUtil.isEmpty(positionSet)) { + positionSet.add("-1"); + } + Long courseTotalNum = ftbCultivatePositionStatisticesMapper.courseTotleNum(new ArrayList<>(positionSet)); + // 已学习课程数 + List studyCourseList = ftbCultivatePositionStatisticesMapper.queryComplateCourseLists(new ArrayList<>(positionSet), userId); + if (CollUtil.isNotEmpty(studyCourseList)) { + courseIds.addAll(studyCourseList); + completeNum += studyCourseList.size(); + } + + totalNum += courseTotalNum; + //累计就再这里 + if (currentLearningStage <= entry.getKey()) { + boolean isComplete = false; + Integer currLevelCompleteNum = 0; + for (FtbCultivatePromotionPostNew promotionPostNew : entry.getValue()) { + try { + String s = promotionService.innerCheckStudying(entry.getKey(), userId, promotionPostNew.getPostId(), pomotionId); + if (s == null) { + currLevelCompleteNum++; + if (currLevelCompleteNum >= promotionPostNew.getSelectCourseNumber()) { + isComplete = true; + break; + } + } + } catch (Exception e) { + log.error("统计已经学习的课程数,检查是否在学习中userid={},postion={},promotionId={}", userId, promotionPostNew.getPostId(), pomotionId); + isComplete = false; + } + } + if (!isComplete) { + break; + } + } + } + vo.setCourseCompleteCnt(completeNum); + vo.setCourseTotalCnt(totalNum); + } + } + } + //3、考试 + Long myAllReadOverCnt = ftbCultivateExamUserMapper.queryAllMyReadOver(userId); + Long myPassReadOverCnt = ftbCultivateExamUserMapper.queryPassMyReadOver(userId); + BigDecimal examPassRate = BigDecimal.ZERO; + if (myAllReadOverCnt > 0) { + examPassRate = new BigDecimal(myPassReadOverCnt).divide(new BigDecimal(myAllReadOverCnt), 4, RoundingMode.HALF_UP); + } + vo.setExamPassRate(UserApiV2Util.dealWithRate(examPassRate, "")); + //4、鉴定 + //查询一下已经完成的鉴定 + LambdaQueryWrapper identifyLambdaQueryWrapper = Wrappers.lambdaQuery(); + identifyLambdaQueryWrapper.eq(CultivateIdentifyApply::getIdentifyUserId, userId); + identifyLambdaQueryWrapper.isNotNull(CultivateIdentifyApply::getTableId); + identifyLambdaQueryWrapper.eq(CultivateIdentifyApply::getDeleteMark, 0); + identifyLambdaQueryWrapper.eq(CultivateIdentifyApply::getIsVisible, 0); + identifyLambdaQueryWrapper.eq(CultivateIdentifyApply::getStatus, 1); + Long completeIdentifyNum = identifyApplyMapper.selectCount(identifyLambdaQueryWrapper); + vo.setIdentificationPassCnt(completeIdentifyNum); + + Long waitAndOverIdentifyNum = 0L; + List btnList = userApiV2Util.queryUserBtnPermissionList(userPrimaryBoundOne); + if (btnList.contains(IDENTITY_CODE)) { + //根据权限查询待我鉴定和待我鉴定过期的 + completeIdentifyNum + LambdaQueryWrapper identifyWrapper = Wrappers.lambdaQuery(); + identifyWrapper.isNotNull(CultivateIdentifyApply::getTableId); + identifyWrapper.eq(CultivateIdentifyApply::getDeleteMark, 0); + identifyWrapper.eq(CultivateIdentifyApply::getIsVisible, 0); + identifyWrapper.ne(CultivateIdentifyApply::getStatus, 1); + if (CollUtil.isNotEmpty(powerUserIds)) { + identifyWrapper.in(CultivateIdentifyApply::getBeIdentifyUserId, powerUserIds); + } + waitAndOverIdentifyNum = identifyApplyMapper.selectCount(identifyWrapper); + } + vo.setIdentificationTotalCnt(completeIdentifyNum + waitAndOverIdentifyNum); + return vo; + } + + + private void processPositionCourse() { + // if (CollUtil.isNotEmpty(stepMap)) { +// Long completeNum = 0L; +// Long totalNum = 0L; +// for (Map.Entry> entry : stepMap.entrySet()) { +// Set positionSet = new HashSet<>(); +// Long level = Long.valueOf(entry.getKey()); +// if (level.equals(1L)) { +// for (FtbCultivatePromotionPostNew promotionPostNew : entry.getValue()) { +// positionSet.add(promotionPostNew.getPostId()); +// } +// } else { +// List> maps = ftbCultivatePromotionNewMapper.queryUserChosesPostsWitId(userId, entry.getKey(), pomotionId); +// if (CollUtil.isNotEmpty(maps)) { +// for (Map map : maps) { +// positionSet.add(map.get("postId").toString()); +// } +// } +// } +// Long courseTotalNum = ftbCultivatePositionStatisticesMapper.courseTotleNum(new ArrayList<>(positionSet)); +// // 已学习课程数 +// List studyCourseList = ftbCultivatePositionStatisticesMapper.queryComplateCourseLists(new ArrayList<>(positionSet), userId); +// if (CollUtil.isNotEmpty(studyCourseList)) { +// courseIds.addAll(studyCourseList); +// completeNum += studyCourseList.size(); +// } +// +// totalNum += courseTotalNum; +// //累计就再这里 +// if (level >= currentLearningStage) { +// break; +// } +// if(level.equals(1L)){ +// List needStudyCourse = ftbCultivatePositionStatisticesMapper.queryAllNeedStudyCourseForPost(new ArrayList<>(positionSet)); +// if(CollUtil.isNotEmpty(needStudyCourse)){ +// boolean result = containsAll(studyCourseList, needStudyCourse); // 返回true +// if(!result){ +// break; +// } +// } +// } +// } +// vo.setCourseCompleteCnt(completeNum); +// vo.setCourseTotalCnt(totalNum); +// } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperQuestionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperQuestionServiceImpl.java new file mode 100644 index 0000000..bca6642 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperQuestionServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.cultivate.service.impl; + + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateTestPaperQuestionMapper; +import jnpf.cultivate.service.FtbCultivateTestPaperQuestionService; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateTestPaperQuestionServiceImpl extends ServiceImpl implements FtbCultivateTestPaperQuestionService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperRuleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperRuleServiceImpl.java new file mode 100644 index 0000000..91258b9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperRuleServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateTestPaperRuleMapper; +import jnpf.cultivate.service.FtbCultivateTestPaperRuleService; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperRule; +import org.springframework.stereotype.Service; + +@Service +public class FtbCultivateTestPaperRuleServiceImpl extends ServiceImpl implements FtbCultivateTestPaperRuleService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperServiceImpl.java new file mode 100644 index 0000000..a08de00 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/FtbCultivateTestPaperServiceImpl.java @@ -0,0 +1,1254 @@ +package jnpf.cultivate.service.impl; + + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.FtbCultivateTestPaperMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.req.paper.QueryPaperReq; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.ExamUpdateStatus; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.util.SelfGrowthUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbCultivateTestPaperServiceImpl extends ServiceImpl implements FtbCultivateTestPaperService { + + /** + * 题目选项服务 + */ + @Autowired + private FtbCultivateQuestionOptionService questionOptionService; + /** + * 试卷和题目关联 服务 + */ + @Autowired + private FtbCultivateTestPaperQuestionService paperQuestionService; + + /** + * 题目服务 + */ + @Autowired + private FtbCultivateQuestionService questionService; + + /** + * 题库服务 + */ + @Autowired + private FtbCultivateQuestionBankService questionBankService; + + /** + * 考试服务 + */ + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private UserApiV2Util userApiV2Util; + + + /** + * 分页查询试卷列表 + * + * @param req + * @return + */ + @Override + public PageInfo getPageList(QueryPaperReq req) { + //构建分页 + if (StringUtils.isNotEmpty(req.getKeyword())) { + req.setKeyword(StringUtils.trim(req.getKeyword())); + } + + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + //构建查询 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .like(!StringUtils.isEmpty(req.getKeyword()), FtbCultivateTestPaper::getName, req.getKeyword()) + .like(!StringUtils.isEmpty(req.getQuestionBankId()), FtbCultivateTestPaper::getRelationQuestionBankId, req.getQuestionBankId()) + .eq(req.getStatus() != null, FtbCultivateTestPaper::getStatus, req.getStatus()) + .eq(req.getIsDraft() != null, FtbCultivateTestPaper::getIsDraft, req.getIsDraft()) + .eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByDesc(FtbCultivateTestPaper::getCreatorTime); + if (req.getNeedUpdate() != null) { + if (req.getNeedUpdate().equals(ExamUpdateStatus.NO_UPDATE.getCode()) || req.getNeedUpdate().equals(ExamUpdateStatus.NEED_UPDATE.getCode())) { + wrapper.eq(FtbCultivateTestPaper::getNeedUpdate, req.getNeedUpdate()); + } + } + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + records.forEach(entity -> { + PaperListVo vo = new PaperListVo(); + vo.setId(entity.getId()); + vo.setName(entity.getName()); + vo.setPaperId(entity.getPaperId()); + vo.setType(entity.getType()); + vo.setPaperType(entity.getPaperType()); + vo.setQuestionNumber(entity.getQuestionNumber()); + vo.setTotalScore(entity.getTotalScore()); + vo.setLastModifyTime(entity.getLastModifyTime()); + vo.setStatus(entity.getStatus()); + vo.setIsDraft(entity.getIsDraft()); + vo.setNeedUpdate(entity.getNeedUpdate()); + list.add(vo); + }); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 获取试卷详情 + * + * @param paperId 试卷id + * @return + */ + @Override + public PaperDetailVo getInfo(String paperId) { + //1、查询和检测试卷是否存在 + FtbCultivateTestPaper paper = getById(paperId); + if (paper == null) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + PaperDetailVo vo = convertPaperDetailVo(paper); + + //如果是随机回填一下题库的名称 + if (CourseEnums.PaperType.RANDOM.getCode().equals(vo.getType())) { + PaperConfigReq paperConfig = vo.getPaperConfig(); + if (null != paperConfig) { + Map> questionConfig = paperConfig.getQuestionConfig(); + if (MapUtil.isNotEmpty(questionConfig)) { + List questionBankIdList = questionConfig.keySet().stream().map(String::valueOf).collect(Collectors.toList()); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .in(FtbCultivateQuestionBank::getId, questionBankIdList) + .eq(FtbCultivateQuestionBank::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List questionBankList = questionBankService.list(wrapper); + if (CollectionUtil.isNotEmpty(questionBankList)) { + List questionBankVoList = BeanUtil.copyToList(questionBankList, QuestionBankVo.class); + vo.setQuestionBankMap(questionBankVoList.stream().collect(Collectors.toMap(QuestionBankVo::getId, Function.identity()))); + } + } + } + } + + + //查询试卷中的题目 + List questionList = queryQuestionList(paperId); + if (CollectionUtil.isNotEmpty(questionList)) { + vo.setQuestionList(questionList); + } + + return vo; + } + + /** + * 转换试卷详情 + * + * @param paper + * @return + */ + private PaperDetailVo convertPaperDetailVo(FtbCultivateTestPaper paper) { + PaperDetailVo vo = new PaperDetailVo(); + vo.setId(paper.getId()); + vo.setName(paper.getName()); + vo.setPaperId(paper.getPaperId()); + vo.setTotalScore(paper.getTotalScore()); + vo.setPaperType(paper.getPaperType()); + vo.setType(paper.getType()); + if (StringUtils.isNotEmpty(paper.getPaperConfig())) { + vo.setPaperConfig(JSONUtil.toBean(paper.getPaperConfig(), PaperConfigReq.class)); + } + + if (StringUtils.isNotEmpty(paper.getPostConfig())) { + vo.setPostAndPositionList(JSONUtil.toList(paper.getPostConfig(), PostAndPosition.class)); + } else { + vo.setPostAndPositionList(CollectionUtil.newArrayList()); + } + vo.setPostId(paper.getPostId()); + vo.setQuestionNumber(paper.getQuestionNumber()); + vo.setTotalScore(paper.getTotalScore()); + vo.setIsDraft(paper.getIsDraft()); + vo.setNeedUpdate(paper.getNeedUpdate()); + return vo; + } + + /** + * 新增试卷 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void insertData(SavePaperReq req) { + if (null == req.getIsDraft()) { + req.setIsDraft(0); + } + //1、检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateTestPaper::getName, req.getName()) + .eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("试卷名称已经存在"); + } + //2、添加试卷 + if (req.getPaperType() != 0 && req.getPaperType() != 1) { + throw new RuntimeException("试卷类型错误"); + } + FtbCultivateTestPaper paper = new FtbCultivateTestPaper(); + paper.setName(req.getName()); //试卷名称 + paper.setPaperType(req.getPaperType());//试卷类型 + paper.setType(req.getType()); //抽题类型 + paper.setPaperId(generatePaperId()); + paper.setStatus(CourseEnums.PaperStatus.ENABLED.getCode()); + paper.setCreatorTime(new Date()); + paper.setLastModifyTime(new Date()); + paper.setIsDraft(req.getIsDraft()); + if (req.getPaperConfig() != null) { + paper.setPaperConfig(JSONUtil.toJsonStr(req.getPaperConfig())); + } + if (CollectionUtil.isNotEmpty(req.getPostAndPositionList())) { + paper.setPostConfig(JSONUtil.toJsonStr(req.getPostAndPositionList())); + List postIdList = new ArrayList<>(); + for (PostAndPosition postAndPosition : req.getPostAndPositionList()) { + postIdList.add(postAndPosition.getId()); + } + paper.setPostId(postIdList.stream().collect(Collectors.joining(","))); + } + paper.setNeedUpdate(ExamUpdateStatus.NO_UPDATE.getCode()); + baseMapper.insert(paper); + //2、批量添加试卷关联题目 + List paperQuestionList = new ArrayList<>(); + //2.1 新增固定题目 + CourseEnums.PaperType paperType = CourseEnums.PaperType.fromCode(req.getType()); + switch (paperType) { + case FIX: + //固定试卷 + List questionItemList = req.getQuestionItemList(); + if (CollectionUtil.isNotEmpty(questionItemList)) { + //写入试卷题目关联表 + paperQuestionList = saveFixPaperQuestionRelation(paper.getId(), questionItemList); + } + break; + case RANDOM: + //随机试卷 + PaperConfigReq paperConfig = req.getPaperConfig(); + if (null == paperConfig) { + throw new RuntimeException("试卷配置信息为空"); + } + if (MapUtil.isEmpty(paperConfig.getQuestionConfig())) { + throw new RuntimeException("试卷题目配置信息为空"); + } + if (MapUtil.isEmpty(paperConfig.getScoreConfig())) { + throw new RuntimeException("试卷分数配置信息为空"); + } + paperQuestionList = saveRandomPaperQuestionRelation(paper.getId(), req.getPaperConfig()); + break; + } + + //3、修改题卷总分和总题目数量,冗余试卷关联的题库 + paper.setQuestionNumber(paperQuestionList.size()); + int totleScore = paperQuestionList.stream() + .mapToInt(FtbCultivateTestPaperQuestion::getScore) + .sum(); + paper.setTotalScore(totleScore); + paper.setLastModifyTime(new Date()); + Set paperIdSet = paperQuestionList.stream() + .map(FtbCultivateTestPaperQuestion::getBankId) + .collect(Collectors.toSet()); + paper.setRelationQuestionBankId(paperIdSet.stream() + .collect(Collectors.joining(","))); + baseMapper.updateById(paper); + } + + /** + * 保存随机试卷的题目关联 + * + * @param paperId 试卷id + * @param paperConfig 随机选题配置信息 + * @return + */ + private List saveRandomPaperQuestionRelation(String paperId, PaperConfigReq paperConfig) { + + //1、根据配置随机生成题目 + Map> questionConfig = paperConfig.getQuestionConfig(); + Map scoreConfig = paperConfig.getScoreConfig(); + + // 根据题库ID 查询题目列表 + List questionBankIdList = new ArrayList<>(questionConfig.keySet()); + List ftbCultivateQuestionBanks = questionBankService.listByIds(questionBankIdList); + Map ftbCultivateQuestionBanksMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(ftbCultivateQuestionBanks)) { + ftbCultivateQuestionBanksMap = ftbCultivateQuestionBanks.stream().collect(Collectors.toMap(FtbCultivateQuestionBank::getId, ftbCultivateQuestionBank -> ftbCultivateQuestionBank)); + } + Map> questionMap = BatchQueryQuestionForQuestionBankIds(questionBankIdList); + //检查所配置的题库中题目是否为0 + for (String outerKey : questionConfig.keySet()) { + //outerKey题库ID + Map innerMap = questionConfig.get(outerKey); + int needQuestionNum = 0; + for (String innerKey : innerMap.keySet()) { + //innerKey题型id + PaperConfigReq.QuestionNum questionNum = innerMap.get(innerKey); + needQuestionNum += questionNum.getSimpleNum() + questionNum.getGeneralNum() + questionNum.getHardNum(); + } + if (needQuestionNum > 0) { + List questionList = questionMap.get(outerKey); + if (CollectionUtil.isEmpty(questionList)) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题目数量为0"); + } + if (needQuestionNum > questionList.size()) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题目数量不足"); + } + + Map> questionTypeMap = questionList.stream() + .collect(Collectors.groupingBy(ftbCultivateQuestion -> String.valueOf(ftbCultivateQuestion.getType()))); + for (String innerKey : innerMap.keySet()) { + //innerKey题型id + int needQuestionType = 0; + PaperConfigReq.QuestionNum questionNum = innerMap.get(innerKey); + needQuestionType += questionNum.getSimpleNum() + questionNum.getGeneralNum() + questionNum.getHardNum(); + if (needQuestionType > 0) { + List questionListType = questionTypeMap.get(innerKey); + if (CollectionUtil.isEmpty(questionListType)) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(Integer.valueOf(innerKey)); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题型:" + questionType.getDesc() + "题目数量不足"); + } + if (needQuestionType > questionListType.size()) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(Integer.valueOf(innerKey)); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题型:" + questionType.getDesc() + "题目数量不足"); + } + Map> difficultyMap = questionListType.stream().collect(Collectors.groupingBy(FtbCultivateQuestion::getDifficulty)); + //1易,2中,3难 + if (questionNum.getSimpleNum() > 0) { + + List questionListDiffulty1 = difficultyMap.get(1); + if (CollectionUtil.isEmpty(questionListDiffulty1) || questionNum.getSimpleNum() > questionListDiffulty1.size()) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(Integer.valueOf(innerKey)); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题型:" + questionType.getDesc() + "难度为简单" + "题目数量不足"); + } + } + if (questionNum.getGeneralNum() > 0) { + + List questionListDiffulty2 = difficultyMap.get(2); + if (CollectionUtil.isEmpty(questionListDiffulty2) || questionNum.getGeneralNum() > questionListDiffulty2.size()) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(Integer.valueOf(innerKey)); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题型:" + questionType.getDesc() + "难度为中" + "题目数量不足"); + } + } + if (questionNum.getHardNum() > 0) { + List questionListDiffulty3 = difficultyMap.get(3); + if (CollectionUtil.isEmpty(questionListDiffulty3) || questionNum.getHardNum() > questionListDiffulty3.size()) { + FtbCultivateQuestionBank ftbCultivateQuestionBank = ftbCultivateQuestionBanksMap.get(outerKey); + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(Integer.valueOf(innerKey)); + throw new RuntimeException("抽题失败:" + ftbCultivateQuestionBank.getBankContent() + "题型:" + questionType.getDesc() + "难度为复杂" + "题目数量不足"); + } + } + } + + } + + } + } + + + List randomList = new ArrayList<>(); + questionMap.forEach((questionBankId, questionList) -> { + //获取该题库题型对应的题目列表 Map + Map> questionTypeMap = questionList.stream() + .collect(Collectors.groupingBy(ftbCultivateQuestion -> String.valueOf(ftbCultivateQuestion.getType()))); + //获取该题库随机选题的配置 + Map questionNumMap = questionConfig.get(questionBankId); + //随机选题 + questionNumMap.forEach((questionType, questionNum) -> { + //题型 的题目列表 + List tempList = questionTypeMap.get(questionType); + if (CollectionUtil.isEmpty(tempList)) { + return; + } + //题型难度列表 + Map> difficultyMap = tempList.stream().collect(Collectors.groupingBy(FtbCultivateQuestion::getDifficulty)); + //简单 + Integer simpleNum = questionNum.getSimpleNum(); + List simpleQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.EASY.getCode()); + //一般 + Integer generalNum = questionNum.getGeneralNum(); + List generalQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.MIDDLE.getCode()); + //难 + Integer hardNum = questionNum.getHardNum(); + List hardQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.MAX.getCode()); + randomList.addAll(randomGetQuestion(paperId, simpleQuestion, simpleNum, scoreConfig.get(questionType).getSimpleScore())); + randomList.addAll(randomGetQuestion(paperId, generalQuestion, generalNum, scoreConfig.get(questionType).getGeneralScore())); + randomList.addAll(randomGetQuestion(paperId, hardQuestion, hardNum, scoreConfig.get(questionType).getHardSore())); + }); + }); + + //2、批量保存 + paperQuestionService.saveBatch(randomList); + return randomList; + } + + /** + * 随机获取题目 + * + * @param paperId 试卷ID + * @param questionList 题目列表 + * @param randomSize 抽取数量 + * @param score 分数 + * @return + */ + private List randomGetQuestion(String paperId, List questionList, Integer randomSize, Integer score) { + if (CollectionUtil.isEmpty(questionList) || randomSize <= 0) { + return new ArrayList<>(); + } + List random = new ArrayList<>(); + if (questionList.size() <= randomSize) { + random = questionList; + } else { + // 打乱列表 + Collections.shuffle(questionList); + // 抽取样本 + random = questionList.subList(0, randomSize); + } + + return random.stream().map(question -> { + FtbCultivateTestPaperQuestion paperQuestion = new FtbCultivateTestPaperQuestion(); + paperQuestion.setPaperId(paperId); + paperQuestion.setQuestionId(question.getId()); + paperQuestion.setBankId(question.getClassifyId()); + paperQuestion.setScore(score); + paperQuestion.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + paperQuestion.setCreatorTime(new Date()); + return paperQuestion; + }).collect(Collectors.toList()); + } + + /** + * 批量查询题目 + * + * @param questionBankIdList 题库ID列表 + * @return 题库id->题目列表 + */ + + public Map> BatchQueryQuestionForQuestionBankIds(List questionBankIdList) { + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .in(FtbCultivateQuestion::getClassifyId, questionBankIdList) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List questionList = questionService.list(wrapper); + if (CollectionUtil.isEmpty(questionList)) { + return new HashMap<>(); + } + return questionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestion::getClassifyId)); + } + + + /** + * 保存固定试卷的题目关联 + * + * @param paperId 试卷ID + * @param questionItemList 题目配置列表,题目ID和分数 + * @return + */ + private List saveFixPaperQuestionRelation(String paperId, List questionItemList) { + List questionIdList = questionItemList.stream().map(SavePaperReq.PaperQuestionItem::getId).collect(Collectors.toList()); + //1、根据题目ID 查询题目信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbCultivateQuestion::getId, questionIdList) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List questionList = questionService.list(wrapper); + if (CollectionUtil.isEmpty(questionList)) { + return CollectionUtil.newArrayList(); + } + Map questionMap = questionList.stream() + .collect(Collectors.toMap(FtbCultivateQuestion::getId, question -> question)); + //2、批量写入 + List list = new ArrayList<>(); + for (SavePaperReq.PaperQuestionItem item : questionItemList) { + FtbCultivateQuestion question = questionMap.get(item.getId()); + if (question == null) { + continue; + } + FtbCultivateTestPaperQuestion testPaperQuestion = new FtbCultivateTestPaperQuestion(); + testPaperQuestion.setQuestionId(question.getId()); + testPaperQuestion.setPaperId(paperId); + testPaperQuestion.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + testPaperQuestion.setBankId(question.getClassifyId()); + testPaperQuestion.setScore(item.getScore()); + testPaperQuestion.setCreatorTime(new Date()); + list.add(testPaperQuestion); + } + paperQuestionService.saveBatch(list); + return list; + } + + /** + * 生成业务试卷ID + * + * @return + */ + private String generatePaperId() { + return SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.TEST_PAPER); + } + + /** + * 修改试卷 + * + * @param req + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void updateData(SavePaperReq req) { + //兼容老的试卷 + if (null == req.getIsDraft()) { + req.setIsDraft(0); + } + //1、检测名称是否重复 试卷是否存在 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateTestPaper::getName, req.getName()) + .ne(FtbCultivateTestPaper::getId, req.getId()) + .eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("试卷名称已经存在"); + } + + FtbCultivateTestPaper paper = getById(req.getId()); + if (null == paper) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + if (CourseEnums.DraftStatus.NO.getCode().equals(paper.getIsDraft())) { + throw new RuntimeException("试卷已经定稿,不能修改"); + } + if (req.getPaperType() != 0 && req.getPaperType() != 1) { + throw new RuntimeException("试卷类型错误"); + } + + //修改试卷 + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setId(paper.getId()); + updatePaper.setName(req.getName()); //试卷名称 + updatePaper.setPaperType(req.getPaperType());//试卷类型 +// updatePaper.setType(req.getType()); //抽题类型 + updatePaper.setLastModifyTime(new Date()); + updatePaper.setIsDraft(req.getIsDraft()); + + if (CollectionUtil.isNotEmpty(req.getPostAndPositionList())) { + updatePaper.setPostConfig(JSONUtil.toJsonStr(req.getPostAndPositionList())); + List postIdList = new ArrayList<>(); + for (PostAndPosition postAndPosition : req.getPostAndPositionList()) { + postIdList.add(postAndPosition.getId()); + } + updatePaper.setPostId(postIdList.stream().collect(Collectors.joining(","))); + } + //3、修改题卷总分和总题目数量,冗余试卷关联的题库 + updatePaper.setLastModifyTime(new Date()); + baseMapper.updateById(updatePaper); + + } + + /** + * 修改试卷 + * + * @param req + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void updatePaperAndQuestion(SavePaperReq req) { + //兼容老的试卷 + if (null == req.getIsDraft()) { + req.setIsDraft(0); + } + //1、检测名称是否重复 试卷是否存在 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateTestPaper::getName, req.getName()) + .ne(FtbCultivateTestPaper::getId, req.getId()) + .eq(FtbCultivateTestPaper::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("试卷名称已经存在"); + } + + FtbCultivateTestPaper paper = getById(req.getId()); + if (null == paper) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + if (paper.getNeedUpdate().equals(ExamUpdateStatus.NO_UPDATE.getCode())) { + if (CourseEnums.DraftStatus.NO.getCode().equals(paper.getIsDraft())) { + throw new RuntimeException("试卷已经定稿,不能修改"); + } + } + if (req.getPaperType() != 0 && req.getPaperType() != 1) { + throw new RuntimeException("试卷类型错误"); + } + + //删除试卷题目关系 + LambdaQueryWrapper paperQuestionWrapper = new LambdaQueryWrapper() + .eq(FtbCultivateTestPaperQuestion::getPaperId, paper.getId()); + paperQuestionService.remove(paperQuestionWrapper); + + //修改试卷 + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setId(paper.getId()); + updatePaper.setName(req.getName()); //试卷名称 + updatePaper.setPaperType(req.getPaperType());//试卷类型 + updatePaper.setType(req.getType()); //抽题类型 + updatePaper.setLastModifyTime(new Date()); + updatePaper.setIsDraft(req.getIsDraft()); + if (req.getPaperConfig() != null) { + updatePaper.setPaperConfig(JSONUtil.toJsonStr(req.getPaperConfig())); + } + if (CollectionUtil.isNotEmpty(req.getPostAndPositionList())) { + updatePaper.setPostConfig(JSONUtil.toJsonStr(req.getPostAndPositionList())); + List postIdList = new ArrayList<>(); + for (PostAndPosition postAndPosition : req.getPostAndPositionList()) { + postIdList.add(postAndPosition.getId()); + } + updatePaper.setPostId(postIdList.stream().collect(Collectors.joining(","))); + } + //2、批量添加试卷关联题目 + List paperQuestionList = new ArrayList<>(); + //2.1 新增固定题目 + CourseEnums.PaperType paperType = CourseEnums.PaperType.fromCode(req.getType()); + switch (paperType) { + case FIX: + //固定试卷 + List questionItemList = req.getQuestionItemList(); + if (CollectionUtil.isNotEmpty(questionItemList)) { + //写入试卷题目关联表 + paperQuestionList = saveFixPaperQuestionRelation(updatePaper.getId(), questionItemList); + } + break; + case RANDOM: + //随机试卷 + PaperConfigReq paperConfig = req.getPaperConfig(); + if (null == paperConfig) { + throw new RuntimeException("试卷配置信息为空"); + } + if (MapUtil.isEmpty(paperConfig.getQuestionConfig())) { + throw new RuntimeException("试卷题目配置信息为空"); + } + if (MapUtil.isEmpty(paperConfig.getScoreConfig())) { + throw new RuntimeException("试卷分数配置信息为空"); + } + paperQuestionList = saveRandomPaperQuestionRelation(updatePaper.getId(), req.getPaperConfig()); + break; + } + + //3、修改题卷总分和总题目数量,冗余试卷关联的题库 + updatePaper.setQuestionNumber(paperQuestionList.size()); + int totleScore = paperQuestionList.stream() + .mapToInt(FtbCultivateTestPaperQuestion::getScore) + .sum(); + updatePaper.setTotalScore(totleScore); + updatePaper.setLastModifyTime(new Date()); + Set paperIdSet = paperQuestionList.stream() + .map(FtbCultivateTestPaperQuestion::getBankId) + .collect(Collectors.toSet()); + updatePaper.setRelationQuestionBankId(paperIdSet.stream() + .collect(Collectors.joining(","))); + updatePaper.setNeedUpdate(ExamUpdateStatus.NO_UPDATE.getCode()); + baseMapper.updateById(updatePaper); + + } + + + /** + * 删除试卷 + * + * @param paperId 试卷id + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String paperId) { + //1、检测试卷 + FtbCultivateTestPaper paper = getById(paperId); + if (paper == null) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + return; + } + if (paper.getIsDraft() == 0) { + if (CourseEnums.PaperStatus.ENABLED.getCode().equals(paper.getStatus())) { + throw new RuntimeException("试卷启用中,请先禁用在删除"); + } + + //根据试卷ID查询是否有考试关联了该试卷 + List examVoList = examService.queryExamListByPaperId(paperId); + if (CollectionUtil.isNotEmpty(examVoList)) { + throw new RuntimeException("该试卷绑定了考试,请解除绑定关系后再进行删除"); + } + } + + //2、删除试卷 + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setId(paper.getId()); + updatePaper.setEnabledMark(CourseEnums.EnabledMarkType.INVALID.getCode()); + updatePaper.setDeleteTime(new Date()); + baseMapper.updateById(updatePaper); + //3、删除试卷题目关系 + LambdaUpdateWrapper paperQuestionWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateTestPaperQuestion::getEnabledMark, CourseEnums.EnabledMarkType.INVALID.getCode()) + .set(FtbCultivateTestPaperQuestion::getDeleteTime, new Date()) + .eq(FtbCultivateTestPaperQuestion::getPaperId, paperId) + .eq(FtbCultivateTestPaperQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + paperQuestionService.update(paperQuestionWrapper); + + //给考试打一个标记 + UpdateWrapper examWrapper = new UpdateWrapper<>(); + examWrapper.lambda() + .set(FtbCultivateExam::getNeedUpdate, ExamUpdateStatus.NEED_UPDATE.getCode()) + .eq(FtbCultivateExam::getPaperId, paperId); + examService.update(examWrapper); + } + + /** + * 检查试卷是否可以删除 + * + * @param paperId 试卷id + * @return + */ + @Override + public CanDeleteMsg checkPaperCanDelete(String paperId) { + FtbCultivateTestPaper paper = getById(paperId); + if (paper == null) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + if (paper.getIsDraft() == 0) { + if (CourseEnums.PaperStatus.ENABLED.getCode().equals(paper.getStatus())) { + throw new RuntimeException("试卷启用中,请先禁用在删除"); + } + + //根据试卷ID查询是否有考试关联了该试卷 + List examVoList = examService.queryExamListByPaperId(paperId); + if (CollectionUtil.isNotEmpty(examVoList)) { + List alertList = new ArrayList<>(); + examVoList.forEach(examVo -> { + DeleteAlertExamVo alertExamVo = new DeleteAlertExamVo(); + alertExamVo.setId(examVo.getId()); + alertExamVo.setExamId(examVo.getExamId()); + alertExamVo.setExamName(examVo.getExamName()); + alertExamVo.setExamType(examVo.getExamType()); + //根据岗位ID查询岗位名称 + if (StringUtils.isNotEmpty(examVo.getPostId())) { +// List entityList = positionApi.getPositionName(Arrays.asList(examVo.getPostId().split(","))); + List entityList = userApiV2Util.listPositionDetailInfoByIds(Arrays.asList(examVo.getPostId().split(",")), null); + if (CollectionUtil.isNotEmpty(entityList)) { + List collect = entityList.stream().map(PositionVO::getFullName).collect(Collectors.toList()); + alertExamVo.setPositionList(collect); + } + } + alertList.add(alertExamVo); + }); + return new CanDeleteMsg(false, "有考试和该试卷关联", alertList); + } + } + return new CanDeleteMsg(true, "没有考试和该试卷关联"); + + + } + + /** + * 切换试卷状态 + * + * @param paperId 试卷id + * @param status 状态 0-启用 1-禁用 + */ + + @Override + public void switchEnabledMark(String paperId, Integer status) { + FtbCultivateTestPaper paper = getById(paperId); + if (paper == null) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + if (!CourseEnums.PaperStatus.DISABLED.getCode().equals(status) + && !CourseEnums.PaperStatus.ENABLED.getCode().equals(status)) { + throw new RuntimeException("status参数异常,只能是[0-启用 1-禁用]"); + } + if (CourseEnums.PaperStatus.DISABLED.getCode().equals(status)) { + if (CourseEnums.PaperStatus.DISABLED.getCode().equals(paper.getStatus())) { + throw new RuntimeException("试卷已禁用,请不要重复操作"); + } + } + + if (CourseEnums.PaperStatus.ENABLED.getCode().equals(status)) { + if (CourseEnums.PaperStatus.ENABLED.getCode().equals(paper.getStatus())) { + throw new RuntimeException("试卷已经启用,请不要重复操作"); + } + } + + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setId(paper.getId()); + updatePaper.setStatus(status); + baseMapper.updateById(updatePaper); + + } + + /** + * 获取试卷信息和试题列表 + * + * @param paperId 试卷ID + * @return + */ + @Override + public PaperDetailVo queryPaperInfoAndQuestionList(String paperId) { + //1、查询试卷基本信息 + PaperDetailVo vo = getInfo(paperId); + //2、查询试卷中的题目列表 + List paperQuestionRelationList = queryPaperQuestionRelationList(paperId); + Map testQuestionMap = paperQuestionRelationList.stream().collect(Collectors.toMap(FtbCultivateTestPaperQuestion::getQuestionId, Function.identity())); + + //3、根据关联的试卷题目ID,查询题目列表 + if (CollectionUtil.isNotEmpty(paperQuestionRelationList)) { + List questionIdList = paperQuestionRelationList.stream().map(FtbCultivateTestPaperQuestion::getQuestionId).collect(Collectors.toList()); + List questionList = questionService.listByIds(questionIdList); + if (CollectionUtil.isNotEmpty(questionList)) { + List questionVoList = BeanUtil.copyToList(questionList, AppQuestionVo.class); + fillQuestionOption(questionVoList); + Map> map = new HashMap<>(); + for (AppQuestionVo questionVo : questionVoList) { + FtbCultivateTestPaperQuestion paperQuestion = testQuestionMap.get(questionVo.getId()); + questionVo.setQuestionScore(paperQuestion.getScore()); + String type = String.valueOf(questionVo.getType()); + List list = map.get(type); + if (CollectionUtil.isEmpty(list)) { + list = new ArrayList<>(); + } + list.add(questionVo); + map.put(type, list); + } + vo.setQuestionMap(map); + } + } + + return vo; + } + + /** + * 根据试卷id查询,试题列表 + * + * @param paperId 试卷ID + * @return + */ + + @Override + public List queryQuestionListForPaperId(String paperId) { + //1、查询试卷中的题目列表 + List paperQuestionRelationList = queryPaperQuestionRelationList(paperId); + //2、根据关联的试卷题目ID,查询题目列表 + if (CollectionUtil.isNotEmpty(paperQuestionRelationList)) { + List questionIdList = paperQuestionRelationList.stream().map(FtbCultivateTestPaperQuestion::getQuestionId).collect(Collectors.toList()); + List questionList = questionService.listByIds(questionIdList); + if (CollectionUtil.isNotEmpty(questionList)) { + return questionList; + } + } + return Collections.emptyList(); + } + + /** + * 查询试卷中配置的题目 + * @param paperId 试卷id + * @return 题目列表 + */ + @Override + public List queryConfigQuestionList(String paperId) { + //1、查询试卷中的题目列表 + List paperQuestionRelationList = queryPaperQuestionRelationList(paperId); + //2、根据关联的试卷题目ID,查询题目列表 + if (CollectionUtil.isEmpty(paperQuestionRelationList)) { + Collections.emptyList(); + } + return BeanUtil.copyToList(paperQuestionRelationList, ExamQuestionBakVo.class); + } + + /** + * 根据试卷id查询试卷完整的题目信息(题目内容 题目选项 题目分数) + * + * @param paperId 试卷id + * @return + */ + public List queryQuestionList(String paperId) { + List questionVoList = new ArrayList<>(); + //1、查询试卷中的题目列表 + List paperQuestionRelationList = queryPaperQuestionRelationList(paperId); + //2、根据关联的试卷题目ID,查询题目列表 + if (CollectionUtil.isNotEmpty(paperQuestionRelationList)) { + List questionIdList = paperQuestionRelationList.stream().map(FtbCultivateTestPaperQuestion::getQuestionId).collect(Collectors.toList()); + List questionList = questionService.listByIds(questionIdList); + if (CollectionUtil.isNotEmpty(questionList)) { + Map questionMap = questionList.stream().collect(Collectors.toMap(FtbCultivateQuestion::getId, Function.identity())); + for (FtbCultivateTestPaperQuestion testPaperQuestion : paperQuestionRelationList) { + FtbCultivateQuestion ftbCultivateQuestion = questionMap.get(testPaperQuestion.getQuestionId()); + if (null != ftbCultivateQuestion && ftbCultivateQuestion.getEnabledMark().equals(1)) { + AppQuestionVo vo = BeanUtil.copyProperties(ftbCultivateQuestion, AppQuestionVo.class); + vo.setQuestionId(ftbCultivateQuestion.getId()); + vo.setQuestionScore(testPaperQuestion.getScore()); + questionVoList.add(vo); + } + } + } + } + return questionVoList; + } + + /** + * 填充题目选项 + * + * @param questionVoList + */ + @Override + public void fillQuestionOption(List questionVoList) { + //根据题目ID 批量查询题目选项 + List questionIdList = questionVoList.stream().map(AppQuestionVo::getId).collect(Collectors.toList()); + List questionOptionList = questionOptionService.list(new LambdaQueryWrapper() + .in(FtbCultivateQuestionOption::getQuestionId, questionIdList) + .eq(FtbCultivateQuestionOption::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + if (CollectionUtil.isEmpty(questionOptionList)) { + return; + } + //转换问题选项 vo + List optionVoList = BeanUtil.copyToList(questionOptionList, QuestionOptionVo.class); + //安装题目ID分组 + Map> optionMap = optionVoList.stream() + .collect(Collectors.groupingBy(QuestionOptionVo::getQuestionId, Collectors.toList())); + + //根据题目ID回填选项 + for (AppQuestionVo questionVo : questionVoList) { + questionVo.setQuestionOptionVoList(optionMap.get(questionVo.getId())); + } + + } + + /** + * 根据试卷id查询试卷的题目列表(题目id 和 题目的分数) + * + * @param paperId 试卷ID + * @return + */ + @Override + public List queryPaperQuestionRelationList(String paperId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateTestPaperQuestion::getPaperId, paperId); + return paperQuestionService.list(wrapper); + } + + /** + * 设置为正式试卷 + * + * @param id 试卷id + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void setFormalPaper(String id) { + FtbCultivateTestPaper paper = getById(id); + if (null == paper) { + throw new RuntimeException("试卷不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(paper.getEnabledMark())) { + throw new RuntimeException("试卷已删除"); + } + if (CourseEnums.DraftStatus.NO.getCode().equals(paper.getIsDraft())) { + throw new RuntimeException("试卷已经定稿,不能修改为正式试卷"); + } + + if (paper.getNeedUpdate().equals(ExamUpdateStatus.NEED_UPDATE.getCode())) { + throw new RuntimeException("试卷异常,不能修改为正式试卷"); + } + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setId(paper.getId()); + updatePaper.setIsDraft(CourseEnums.DraftStatus.NO.getCode()); + updatePaper.setLastModifyTime(new Date()); + baseMapper.updateById(updatePaper); + + } + + /** + * 从试卷中删除题目 + * + * @param paperId 试卷id + * @param questionId 题目id + */ + @Override + @Transactional + public void deleteQuestionFormPaper(String paperId, String questionId) { + List examVoList = examService.queryExamListByPaperId(paperId); + if (CollectionUtil.isNotEmpty(examVoList)) { + //试卷关联考试 + return; + } + //试卷未关联考试 从试卷中提出题目 重新计算题目数量和分数 + LambdaQueryWrapper paperQuestionWrapper = new LambdaQueryWrapper() + .eq(FtbCultivateTestPaperQuestion::getQuestionId, questionId) + .eq(FtbCultivateTestPaperQuestion::getPaperId, paperId); + paperQuestionService.remove(paperQuestionWrapper); + + //查询试卷中的题目列表 + List paperQuestionRelationList = queryPaperQuestionRelationList(paperId); + Integer score = 0; + Integer sum = 0; + if (CollectionUtil.isNotEmpty(paperQuestionRelationList)) { + sum = paperQuestionRelationList.size(); + for (FtbCultivateTestPaperQuestion paperQuestion : paperQuestionRelationList) { + score += paperQuestion.getScore(); + } + } + //修改试卷题目数量和分数 + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setQuestionNumber(sum); + updatePaper.setTotalScore(score); + updatePaper.setId(paperId); + baseMapper.updateById(updatePaper); + //给考试打一个标记 + UpdateWrapper examWrapper = new UpdateWrapper<>(); + examWrapper.lambda() + .set(FtbCultivateExam::getNeedUpdate, ExamUpdateStatus.NEED_UPDATE.getCode()) + .eq(FtbCultivateExam::getPaperId, paperId); + examService.update(examWrapper); + } + + /** + * 批量删除题目 + * + * @param paperId 试卷ID + * @param questionIds 题目集合 + */ + @Override + @Transactional + public void batchDeleteQuestionFormPaper(String paperId, List questionIds) { + + //试卷未关联考试 从试卷中提出题目 重新计算题目数量和分数 + LambdaQueryWrapper paperQuestionWrapper = new LambdaQueryWrapper() + .in(FtbCultivateTestPaperQuestion::getQuestionId, questionIds) + .eq(FtbCultivateTestPaperQuestion::getPaperId, paperId); + paperQuestionService.remove(paperQuestionWrapper); + + //查询试卷中的题目列表 + List paperQuestionRelationList = queryPaperQuestionRelationList(paperId); + Integer score = 0; + Integer sum = 0; + if (CollectionUtil.isNotEmpty(paperQuestionRelationList)) { + sum = paperQuestionRelationList.size(); + for (FtbCultivateTestPaperQuestion paperQuestion : paperQuestionRelationList) { + score += paperQuestion.getScore(); + } + } + + //修改试卷题目数量和分数 + FtbCultivateTestPaper updatePaper = new FtbCultivateTestPaper(); + updatePaper.setQuestionNumber(sum); + updatePaper.setTotalScore(score); + updatePaper.setId(paperId); + //检测试卷是否异常 + updatePaper.setNeedUpdate(checkIsRandomNeedUpdate(paperId)); + baseMapper.updateById(updatePaper); + + List examVoList = examService.queryExamListByPaperId(paperId); + if (CollectionUtil.isEmpty(examVoList)) { + //试卷关联考试 + return; + } + //给考试打一个标记 + UpdateWrapper examWrapper = new UpdateWrapper<>(); + examWrapper.lambda() + .set(FtbCultivateExam::getNeedUpdate, ExamUpdateStatus.NEED_UPDATE.getCode()) + .eq(FtbCultivateExam::getPaperId, paperId); + examService.update(examWrapper); + } + + /** + * 检测试卷是否需要更新 + * + * @param paperId 试卷id + * @return + */ + private Integer checkIsRandomNeedUpdate(String paperId) { + FtbCultivateTestPaper paper = baseMapper.selectById(paperId); + if (paper == null) { + return ExamUpdateStatus.NO_UPDATE.getCode(); + } + if (paper.getType().equals(1)) { + return ExamUpdateStatus.NO_UPDATE.getCode(); + } + //只要删除了题目 就要异常 所有不需要管他是否能够抽题 + return ExamUpdateStatus.NEED_UPDATE.getCode(); + +// if (StringUtils.isEmpty(paper.getPaperConfig())) { +// return ExamUpdateStatus.NEED_UPDATE.getCode(); +// } +// PaperConfigReq paperConfig = JSONUtil.toBean(paper.getPaperConfig(), PaperConfigReq.class); +// List im = imitateRandom(paperConfig); +// if (CollectionUtil.isEmpty(im)) { +// return ExamUpdateStatus.NEED_UPDATE.getCode(); +// } +// +// //2、初始化返回值 +// Map imAnalyMap = QuestionAnalysisUtil.initAnalysQuestionCount(); +// +// QuestionAnalysisUtil.analysQuestionCount(imAnalyMap, im); +// +// +// Map oldAnalyMap = QuestionAnalysisUtil.initAnalysQuestionCount(); +// Map> questionConfig = paperConfig.getQuestionConfig(); +// //遍历 questionConfig +// for (Map.Entry> entry : questionConfig.entrySet()) { +// String questionType = entry.getKey();//题库id +// Map questionNumMap = entry.getValue(); +// //遍历 questionNumMap +// for (Map.Entry entry2 : questionNumMap.entrySet()) { +// String questionLevel = entry2.getKey();//题型 +// PaperConfigReq.QuestionNum questionNum = entry2.getValue(); +// PaperConfigReq.QuestionNum tmp = oldAnalyMap.get(questionLevel); +// tmp.setSimpleNum(questionNum.getSimpleNum() + tmp.getSimpleNum()); +// tmp.setHardNum(questionNum.getHardNum() + tmp.getHardNum()); +// tmp.setGeneralNum(questionNum.getGeneralNum() + tmp.getGeneralNum()); +// } +// } +// +// //遍历 oldAnalyMap +// for (Map.Entry entry : oldAnalyMap.entrySet()) { +// String questionLevel = entry.getKey();//题型 +// PaperConfigReq.QuestionNum old = entry.getValue(); +// PaperConfigReq.QuestionNum tmp = imAnalyMap.get(questionLevel); +// if (tmp == null) { +// return ExamUpdateStatus.NEED_UPDATE.getCode(); +// } +// if (!tmp.getSimpleNum().equals(old.getSimpleNum()) || !tmp.getGeneralNum().equals(old.getGeneralNum()) || !tmp.getHardNum().equals(old.getHardNum())) { +// return ExamUpdateStatus.NEED_UPDATE.getCode(); +// } +// } +// +// return ExamUpdateStatus.NO_UPDATE.getCode(); + + } + + + /** + * 模拟抽题 + * + * @param paperConfig 随机选题配置信息 + * @return + */ + private List imitateRandom(PaperConfigReq paperConfig) { + + //1、根据配置随机生成题目 + Map> questionConfig = paperConfig.getQuestionConfig(); + Map scoreConfig = paperConfig.getScoreConfig(); + + // 根据题库ID 查询题目列表 + List questionBankIdList = new ArrayList<>(questionConfig.keySet()); + Map> questionMap = BatchQueryQuestionForQuestionBankIds(questionBankIdList); + + List randomList = new ArrayList<>(); + questionMap.forEach((questionBankId, questionList) -> { + //获取该题库题型对应的题目列表 Map + Map> questionTypeMap = questionList.stream() + .collect(Collectors.groupingBy(ftbCultivateQuestion -> String.valueOf(ftbCultivateQuestion.getType()))); + //获取该题库随机选题的配置 + Map questionNumMap = questionConfig.get(questionBankId); + //随机选题 + questionNumMap.forEach((questionType, questionNum) -> { + //题型 的题目列表 + List tempList = questionTypeMap.get(questionType); + if (CollectionUtil.isEmpty(tempList)) { + return; + } + //题型难度列表 + Map> difficultyMap = tempList.stream().collect(Collectors.groupingBy(FtbCultivateQuestion::getDifficulty)); + //简单 + Integer simpleNum = questionNum.getSimpleNum(); + List simpleQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.EASY.getCode()); + //一般 + Integer generalNum = questionNum.getGeneralNum(); + List generalQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.MIDDLE.getCode()); + //难 + Integer hardNum = questionNum.getHardNum(); + List hardQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.MAX.getCode()); + randomList.addAll(imitateRandomGetQuestion(simpleQuestion, simpleNum, scoreConfig.get(questionType).getSimpleScore())); + randomList.addAll(imitateRandomGetQuestion(generalQuestion, generalNum, scoreConfig.get(questionType).getGeneralScore())); + randomList.addAll(imitateRandomGetQuestion(hardQuestion, hardNum, scoreConfig.get(questionType).getHardSore())); + }); + }); + return randomList; + } + + /** + * 模拟从题目列表中抽取一定数量的题目 + * @param questionList 题目列表 + * @param randomSize 抽取数量 + * @param score + * @return + */ + private List imitateRandomGetQuestion(List questionList, Integer randomSize, Integer score) { + if (CollectionUtil.isEmpty(questionList) || randomSize <= 0) { + return new ArrayList<>(); + } + if (questionList.size() <= randomSize) { + return questionList; + } + // 打乱列表 + Collections.shuffle(questionList); + // 抽取样本 + return questionList.subList(0, randomSize); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/PositionCultivateIdentifyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/PositionCultivateIdentifyServiceImpl.java new file mode 100644 index 0000000..6cf612d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/PositionCultivateIdentifyServiceImpl.java @@ -0,0 +1,94 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourseIdentityMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionExamIdentifyMapper; +import jnpf.cultivate.service.PositionCultivateIdentifyService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseIdentity; +import jnpf.model.cultivate.po.position.FtbCultivatePositionExamIdentify; +import jnpf.model.cultivate.vo.identify.PostAndCourseCorrelationVo; +import jnpf.permission.vo.v2.position.PositionVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Service +public class PositionCultivateIdentifyServiceImpl implements PositionCultivateIdentifyService { + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + @Autowired + private FtbCultivatePositionExamIdentifyMapper ftbCultivatePositionExamIdentifyMapper; + @Autowired + private FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + @SuppressWarnings("Duplicates") + public List selectCorrelationDataList(String tableId) { + List result = new ArrayList<>(); + // 岗位学习鉴定 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getIdentifyId, tableId); + queryWrapper.eq(FtbCultivatePositionExamIdentify::getEnabledMark, 0); + ftbCultivatePositionExamIdentifyMapper.selectList(queryWrapper).forEach(item -> { + PostAndCourseCorrelationVo vo = new PostAndCourseCorrelationVo(); + // 岗位学习主键ID + vo.setDataId(item.getPostLearnId()); +// PositionEntity positionInfoNewVO = positionApi.queryInfoById(item.getPostRankId()); + PositionVO positionInfoNewVO = userApiV2Util.infoPosition(item.getPostRankId(), null); + if (Objects.nonNull(positionInfoNewVO)) { + // 岗位名称 + vo.setPostName(positionInfoNewVO.getFullName()); + } + result.add(vo); + }); + // 岗位学习课程关联鉴定 + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePositionCourseIdentity::getIdentityId, tableId); + lambdaQuery.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + ftbCultivatePositionCourseIdentityMapper.selectList(lambdaQuery).forEach(item -> { + PostAndCourseCorrelationVo vo = new PostAndCourseCorrelationVo(); + // 岗位学习主键ID + vo.setDataId(item.getPostLearnId()); +// PositionEntity positionInfoNewVO = positionApi.queryInfoById(item.getPostRankId()); + PositionVO positionInfoNewVO = userApiV2Util.infoPosition(item.getPostRankId(), null); + if (Objects.nonNull(positionInfoNewVO)) { + // 岗位名称 + vo.setPostName(positionInfoNewVO.getFullName()); + } + // 课程名 + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(item.getCourseId()); + if (Objects.nonNull(ftbCultivateCourse)) { + vo.setCourseName(ftbCultivateCourse.getName()); + } + result.add(vo); + }); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchDeleteCorrelationRecord(List sourceIds) { + // 岗位学习鉴定 + LambdaUpdateWrapper identifyLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + identifyLambdaUpdateWrapper.set(FtbCultivatePositionExamIdentify::getEnabledMark, 1); + identifyLambdaUpdateWrapper.in(FtbCultivatePositionExamIdentify::getIdentifyId, sourceIds); + ftbCultivatePositionExamIdentifyMapper.update(new FtbCultivatePositionExamIdentify(), identifyLambdaUpdateWrapper); + // 岗位学习课程关联鉴定 + LambdaUpdateWrapper courseLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + courseLambdaUpdateWrapper.set(FtbCultivatePositionCourseIdentity::getEnabledMark, 1); + courseLambdaUpdateWrapper.in(FtbCultivatePositionCourseIdentity::getIdentityId, sourceIds); + ftbCultivatePositionCourseIdentityMapper.update(new FtbCultivatePositionCourseIdentity(), courseLambdaUpdateWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordPracticeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordPracticeServiceImpl.java new file mode 100644 index 0000000..5675cb5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordPracticeServiceImpl.java @@ -0,0 +1,565 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fantaibao.permission.handling.PermissionHandling; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.cultivate.mapper.CultivateUserViewMapper; +import jnpf.cultivate.mapper.TeachingRecordMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.v2.util.CultivateMqSendUtil; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.entity.cultivate.TeachingStudent; +import jnpf.enums.cultivate.TeachingRecordTypeEnum; +import jnpf.model.cultivate.dto.teaching.*; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.teaching.CultivateFile; +import jnpf.model.cultivate.po.teaching.CultivateUserView; +import jnpf.model.cultivate.v2.teaching.model.V2Attachment; +import jnpf.model.cultivate.vo.teaching.*; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.EasyExcelUtil; +import jnpf.util.FtbUtil; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +public class TeachingRecordPracticeServiceImpl extends ServiceImpl implements TeachingRecordPracticeService { + @Resource + private V2UserApi v2UserApi; + @Resource + private PermissionHandling permissionHandling; + @Resource + private TeachingSkillService teachingSkillService; + @Resource + private CultivateUserViewMapper cultivateUserViewMapper; + @Resource + private TeachingStudentService teachingStudentService; + @Resource + private TeachingRecordService teachingRecordService; + @Resource + private CultivateFileService cultivateFileService; + + @Resource + private CultivateMqSendUtil cultivateMqSendUtil; + + @Resource + private UserApi userApiV1; + + @Override + public List getStoreList() { + //获取练习记录 + List recordList = lambdaQuery() + .select(TeachingRecord::getStoreId, TeachingRecord::getStoreName) + .eq(TeachingRecord::getType, TeachingRecordTypeEnum.LX.getCode()) + .eq(TeachingRecord::getDeleteMark, 0) + .list(); + //返回门店下拉数据 + if (CollUtil.isNotEmpty(recordList)) { + Map storeListMap = recordList.stream().collect(Collectors.toMap(TeachingRecord::getStoreId, a -> a, (k1, k2) -> k1)); + return storeListMap.values().stream().map(record -> StoreInfoListVo.builder() + .storeId(record.getStoreId()) + .storeName(record.getStoreName()) + .build()).collect(Collectors.toList()); + } + return List.of(); + } + + @Override + public PageInfo summaryPageList(TeachingBaseFilter pageDto) { + pageDto.dateTimeFormat(); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + List pageListVoList = this.baseMapper.summaryPageList(page, pageDto); + List userIds = pageListVoList.stream().map(SummaryPageListVo::getUserId).collect(Collectors.toList()); + Map userMap = getUserMap(userIds); + setSummaryUserName(userMap, pageListVoList); + //获取筛选范围内总的技能点集合 + LinkedList skillList = this.baseMapper.summarySkillList(pageDto); + //处理skillList数据;每条数据的长度不一样,要处理成一样的,并且保证顺序一致 + processSkillPointData(skillList, pageListVoList); + PageInfo pageInfo = new PageInfo<>(pageListVoList); + pageInfo.setPageSize((int) pageDto.getPageSize()); + pageInfo.setPageNum((int) pageDto.getCurrentPage()); + pageInfo.setTotal(page.getTotal()); + return pageInfo; + } + + private void setSummaryUserName(Map userMap, List pageListVoList) { + pageListVoList.forEach(v -> { + UserBoundVO user = userMap.get(v.getUserId()); + if (null != user) { + v.setUserName(user.getUserName()); + v.setPostName(user.getPositionName()); + } else { + v.setPostName(null); + } + }); + } + + private Map getUserMap(List userIds) { + + List list; + if (userIds.isEmpty()) { + list = new ArrayList<>(); + } else { + list = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId()); + } + return list.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + + @Override + public void summaryExport(HttpServletResponse response, TeachingBaseFilter pageDto) throws Exception { + pageDto.dateTimeFormat(); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(100000L)); + List summaryVoList = this.baseMapper.summaryPageList(page, pageDto); + List userIds = summaryVoList.stream().map(SummaryPageListVo::getUserId).collect(Collectors.toList()); + Map userMap = getUserMap(userIds); + setSummaryUserName(userMap, summaryVoList); + //获取筛选范围内总的技能点集合 + LinkedList skillList = this.baseMapper.summarySkillList(pageDto); + //处理skillList数据;每条数据的长度不一样,要处理成一样的,并且保证顺序一致 + processSkillPointData(skillList, summaryVoList); + //获取动态表头 + List> list = new ArrayList<>(); + list.add(Collections.singletonList("门店名称")); + list.add(Collections.singletonList("学员")); + list.add(Collections.singletonList("学员岗位")); + list.add(Collections.singletonList("练习总次数")); + List> headers = EasyExcelUtil.getHeaders(summaryVoList, list); + //获取动态数据 + List> dataList = EasyExcelUtil.getDataList(summaryVoList); + EasyExcelUtil.rankingExportData(dataList, headers, "练习汇总数据", "练习汇总数据表", response); + } + + + @Override + public void recordExport(HttpServletResponse response, TeachingBaseFilter pageDto) throws Exception { + pageDto.dateTimeFormat(); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(100000L)); + // 技能点分类过滤 + List skillIds = teachingSkillService.querySkillIds(pageDto.getCategoryId()); + List pageListVoList; + if (skillIds == null) { + pageListVoList = new ArrayList<>(); + } else { + pageListVoList = this.baseMapper.recordPageList(page, pageDto, skillIds); + List exportListVoList = pageListVoList.stream().map(RecordPageListVo::convert).collect(Collectors.toList()); + EasyExcelUtil.export(response, "练习记录明细.xlsx", "练习记录明细表", RecordExportListVo.class, exportListVoList); + } + } + + @Override + public PageInfo recordPageList(TeachingBaseFilter pageDto) { + pageDto.dateTimeFormat(); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + // 技能点分类过滤 + List skillIds = teachingSkillService.querySkillIds(pageDto.getCategoryId()); + if (skillIds == null) { + return new PageInfo<>(); + } + List pageListVoList = this.baseMapper.recordPageList(page, pageDto, skillIds); + PageInfo pageInfo = new PageInfo<>(pageListVoList); + pageInfo.setPageSize((int) pageDto.getPageSize()); + pageInfo.setPageNum((int) pageDto.getCurrentPage()); + pageInfo.setTotal(page.getTotal()); + return pageInfo; + } + + @Override + public Boolean recordBatchDelete(RecordBatchDeleteDto deleteDto) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(TeachingRecord::getId, deleteDto.getIds()); + this.baseMapper.delete(queryWrapper); + return Boolean.TRUE; + } + + @Override + public PageInfo myRecordPageList(MyRecordPageListDto pageDto) { + pageDto.dateTimeFormat(); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + List pageListVoList = this.baseMapper.myRecordPageList(page, pageDto, UserProvider.getLoginUserId()); + PageInfo pageInfo = new PageInfo<>(pageListVoList); + pageInfo.setPageSize((int) pageDto.getPageSize()); + pageInfo.setPageNum((int) pageDto.getCurrentPage()); + pageInfo.setTotal(page.getTotal()); + if (!pageInfo.getList().isEmpty()) { + // 查询附件信息 + List recordIdList = pageInfo.getList().stream().map(MyRecordPageListVo::getId).collect(Collectors.toList()); + Map> fileMap = teachingRecordService.getFileMap(recordIdList); + // 查询是否浏览 + Set viewedSet = teachingRecordService.getViewedSet(recordIdList, UserProvider.getLoginUserId()); + pageInfo.getList().forEach(v -> { + List files = fileMap.get(v.getId()); + if (null != files && !files.isEmpty()) { + List attachments = new ArrayList<>(); + files.forEach(file -> { + V2Attachment v2Attachment = cultivateFileService.convertToV2Attachment(file); + attachments.add(v2Attachment); + }); + v.setAttachments(attachments); + } + if (viewedSet.contains(v.getId())) { + v.setViewed(true); + } + }); + } + return pageInfo; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean mySave(RecordSaveDto saveDto) throws Exception { + TeachingRecord teachingRecord = RecordSaveDto.convert(saveDto); + //查询用户主岗 + ActionResult actionResult = v2UserApi.infoAndCopy(UserProvider.getUser().getUserId()); + + Assert.isTrue(actionResult.getCode().equals(200), "查询登录用户主岗信息异常"); + UserBoundVO userInfo = actionResult.getData(); + if (null == userInfo) { + throw new Exception("未查询到登录用户的主岗信息"); + } + teachingRecord.setUserName(userInfo.getUserName()); + teachingRecord.setDeleteMark(0); + teachingRecord.setTenantId(UserProvider.getUser().getTenantId()); + teachingRecord.setPostId(userInfo.getPositionId()); + teachingRecord.setPostName(userInfo.getPositionName()); + teachingRecord.setId(FtbUtil.getId()); + // 新增附件信息 + if (null != saveDto.getFileList() && !saveDto.getFileList().isEmpty()) { + List fileList = teachingRecordService.changeToFileList(teachingRecord.getId(), saveDto.getFileList(), UserProvider.getLoginUserId(), FileEventDTO.FileType.PRACTICE); + cultivateFileService.saveBatch(fileList); + } + boolean save = this.save(teachingRecord); + // 注册事务同步器,在事务提交后发送MQ消息 + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + cultivateMqSendUtil.sendCompleteTeachingMessage(teachingRecord); + } + }); + } + return save; + } + + @Override + public RecordInfoVo myInfo(String id) { + TeachingRecord teachingRecord = this.getById(id); + Assert.notNull(teachingRecord, "未查询到该记录"); + CultivateUserView cultivateUserView = cultivateUserViewMapper.selectOne(new LambdaQueryWrapper().eq(CultivateUserView::getBizId, id)); + if (null == cultivateUserView) { + cultivateUserView = new CultivateUserView(FtbUtil.getId(), id, UserProvider.getLoginUserId()); + cultivateUserViewMapper.insert(cultivateUserView); + } + TeachingSkill teachingSkill = teachingSkillService.getById(teachingRecord.getSkillId()); + Assert.notNull(teachingSkill, "未查询到技能点信息"); + RecordInfoVo vo = RecordInfoVo.convert(teachingRecord, teachingSkill); + // 查询附件信息 + Map> fileMap = teachingRecordService.getFileMap(List.of(id)); + if (null != fileMap && !fileMap.isEmpty()) { + List files = fileMap.get(id); + List attachments = new ArrayList<>(); + files.forEach(file -> { + V2Attachment v2Attachment = cultivateFileService.convertToV2Attachment(file); + attachments.add(v2Attachment); + }); + vo.setAttachments(attachments); + } + return vo; + } + + @Override + public Boolean myUpdate(RecordUpdateDto updateDto) { + TeachingRecord teachingRecord = this.getById(updateDto.getId()); + Assert.notNull(teachingRecord, "未查询到该记录"); + RecordUpdateDto.convert(teachingRecord, updateDto); + // 更新附件信息 + teachingRecordService.updateFileInfo(updateDto.getId(), updateDto.getFileList(), FileEventDTO.FileType.PRACTICE); + return this.updateById(teachingRecord); + } + + @Override + public Boolean myDelete(String id) { + return this.removeById(id); + } + + @Override + public ViewDataVo myViewData(ViewDataDto viewDataDto) { + viewDataDto.dateTimeFormat(); + List skillTable = this.baseMapper.getMySkillTable(viewDataDto, UserProvider.getLoginUserId()); + skillTable.sort(Comparator.comparing(RecordViewDataModel::getCount).reversed()); + return ViewDataVo.builder() + .totalCount(CollUtil.isNotEmpty(skillTable) ? skillTable.stream().mapToInt(RecordViewDataModel::getCount).sum() : 0) + .skillTable(skillTable) + .build(); + } + + @Override + public PageInfo employeePageList(EmployeePageListDto pageDto) { + pageDto.dateTimeFormat(); + Page page = Page.of(Math.toIntExact(pageDto.getCurrentPage()), Math.toIntExact(pageDto.getPageSize())); + List pageListVoList = this.baseMapper.employeePageList(page, pageDto); + PageInfo pageInfo = new PageInfo<>(pageListVoList); + pageInfo.setPageSize((int) pageDto.getPageSize()); + pageInfo.setPageNum((int) pageDto.getCurrentPage()); + pageInfo.setTotal(page.getTotal()); + return pageInfo; + } + + @Override + public List employeeViewData(ViewDataDto viewDataDto) { + viewDataDto.dateTimeFormat(); + List employeeViewDataVoList = this.baseMapper.employeeViewData(viewDataDto); + //获取筛选范围内总的技能点集合 + LinkedList skillList = this.baseMapper.employeeSkillList(viewDataDto); + //处理skillList数据;每条数据的长度不一样,要处理成一样的,并且保证顺序一致 + processSkillPointData1(skillList, employeeViewDataVoList); + employeeViewDataVoList = BeanUtil.copyToList(employeeViewDataVoList, EmployeeViewDataVo.class).stream() + .sorted(Comparator.comparing(EmployeeViewDataVo::getTotalCount).reversed() + .thenComparing(dto -> Optional.ofNullable(dto.getUserName()).orElse(""))) + .collect(Collectors.toList()); + AtomicInteger rank = new AtomicInteger(1); + employeeViewDataVoList.forEach(vo -> vo.setRank(rank.getAndIncrement())); + return employeeViewDataVoList; + } + + @Override + public TodaySummaryDataVo getTodaySummary(String storeId) { + //获取有数组权限的用户集合 + List userIdList = permissionHandling.getUserIdsByUserId(UserProvider.getUser().getUserId()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(TeachingRecord::getStoreId, storeId); + if (CollUtil.isEmpty(userIdList)) { + return new TodaySummaryDataVo(); + } else { + queryWrapper.in(TeachingRecord::getUserId, userIdList); + } + queryWrapper.eq(TeachingRecord::getType, TeachingRecordTypeEnum.LX.getCode()); + queryWrapper.between(TeachingRecord::getDate, DateUtil.beginOfMonth(new Date()), DateUtil.endOfMonth(new Date())); + queryWrapper.eq(TeachingRecord::getDeleteMark, 0); + List teachingRecordList = this.baseMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(teachingRecordList)) { + return TodaySummaryDataVo.builder() + .totalCount(teachingRecordList.size()) + .totalUserCount(teachingRecordList.stream().map(TeachingRecord::getUserId).distinct().count()) + .build(); + } + return new TodaySummaryDataVo(); + } + + @Override + @Transactional + public Boolean createSimulatedData(String tenantId) { + List skillList = teachingSkillService.lambdaQuery().eq(TeachingSkill::getDeleteMark, 0).list(); + Assert.notEmpty(skillList, "未查询到技能点信息"); + // 查询用户数据 + ActionResult> usersBound = v2UserApi.getAllUserInfoBatch(List.of(), tenantId); + Assert.isTrue(usersBound.getCode().equals(200), "查询用户门店信息异常"); + List moreBoundInfoList = usersBound.getData(); + Assert.notEmpty(moreBoundInfoList, "未查询到用户组织门店信息"); + //排除掉没有门店信息的用户 + moreBoundInfoList.removeIf(item -> Objects.isNull(item.getOrganizeCategory()) || !item.getOrganizeCategory().equals(OrganizeCategoryEnums.STORE.getCode()) || StrUtil.isBlank(item.getPositionId())); + Assert.notEmpty(moreBoundInfoList, "过滤后暂无用户!"); + String videoPath = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/WebAnnexFile/20250903/20250903_153338__974453.mp4"; + BigDecimal videoDuration = BigDecimal.valueOf(71); + int videoActionType = 1; + Date currentDate = new Date(); + Collections.shuffle(skillList); + List skillList3 = skillList.stream().limit(3).collect(Collectors.toList()); + List saveStudentList = new ArrayList<>(); + List saveDataList = skillList3.stream() + .flatMap(item -> moreBoundInfoList.stream().flatMap(user -> { + // 为每个门店生成数据 + TeachingRecord lxRecord = TeachingRecord.builder() + .type(TeachingRecordTypeEnum.LX.getCode()) + .skillId(item.getId()) + .userId(user.getId()) + .userName(user.getUserName()) + .postId(user.getPositionId()) + .postName(user.getPositionName()) + .storeId(user.getOrganizeId()) + .storeName(user.getOrganizeName()) + .videoPath(videoPath) + .videoDuration(videoDuration) + .videoActionType(videoActionType) + .date(currentDate) + .build(); + return Stream.of(lxRecord); + })).collect(Collectors.toList()); + Map> storeIdMap = moreBoundInfoList.stream().collect(Collectors.groupingBy(UserBoundVO::getOrganizeId)); + saveDataList.addAll(skillList3.stream() + .flatMap(skill -> storeIdMap.entrySet().stream().flatMap(storeEntry -> { + String storeId = storeEntry.getKey(); + List storeUsers = storeEntry.getValue(); + return storeUsers.stream().map(currentUser -> { + // 为当前用户生成数据 + TeachingRecord djRecord = TeachingRecord.builder() + .type(TeachingRecordTypeEnum.DJ.getCode()) + .skillId(skill.getId()) + .userId(currentUser.getId()) + .userName(currentUser.getUserName()) + .postId(currentUser.getPositionId()) + .postName(currentUser.getPositionName()) + .storeId(storeId) + .storeName(currentUser.getOrganizeName()) + .videoPath(videoPath) + .videoDuration(videoDuration) + .videoActionType(videoActionType) + .date(currentDate) + .build(); + djRecord.setId(RandomUtil.uuId()); + // 获取同一门店下除自己外的其他用户(最多4个) + List otherStoreUsers = storeUsers.stream() + .filter(user -> !user.getId().equals(currentUser.getId())) + .limit(4).collect(Collectors.toList()); + // 为djRecord添加学生(不包含自己) + if (!otherStoreUsers.isEmpty()) { + List students = otherStoreUsers.stream() + .map(otherUser -> TeachingStudent.builder() + .recordId(djRecord.getId()) + .userId(otherUser.getId()) + .username(otherUser.getUserName()) + .postId(otherUser.getPositionId()) + .postName(otherUser.getPositionName()) + .creatorUserId(currentUser.getId()) + .creatorTime(currentDate) + .build()).collect(Collectors.toList()); + saveStudentList.addAll(students); + } + return djRecord; + }); + })).collect(Collectors.toList())); + if (CollUtil.isNotEmpty(saveStudentList)) { + this.teachingStudentService.saveBatch(saveStudentList); + } + if (CollUtil.isNotEmpty(saveDataList)) { + this.saveBatch(saveDataList); + } + return true; + } + + @Override + public MyPracticeSummaryVo getMyPracticeSummary(String storeId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(TeachingRecord::getStoreId, storeId); + queryWrapper.eq(TeachingRecord::getUserId, UserProvider.getLoginUserId()); + queryWrapper.eq(TeachingRecord::getType, 2); + queryWrapper.eq(TeachingRecord::getDeleteMark, 0); + List teachingRecordList = this.list(queryWrapper); + if (CollUtil.isEmpty(teachingRecordList)) { + return new MyPracticeSummaryVo(0, BigDecimal.ZERO); + } + return MyPracticeSummaryVo.builder() + .count(teachingRecordList.size()) + .duration(teachingRecordList.stream().map(TeachingRecord::getVideoDuration).reduce(BigDecimal.ZERO, + BigDecimal::add).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP)) + .build(); + } + + /** + * 处理skillList数据;每条数据的长度不一样,要处理成一样的,并且保证顺序一致 + * + * @param skillList 技能点集合 + * @param pageListVoList 待处理数据 + */ + private void processSkillPointData1(LinkedList skillList, List pageListVoList) { + //获取所有技能点,并且按照sort字段排序 + List sortedSkillList = skillList.stream() + .filter(item -> item.getDeleteMark().equals(0)) + .sorted(Comparator.comparing(SkillCountVo::getSort)) + .collect(Collectors.toList()); + //获取已删除的技能点 + List deleteSkillList = skillList.stream() + .filter(item -> item.getDeleteMark().equals(1)) + .sorted(Comparator.comparing(SkillCountVo::getSort)) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(deleteSkillList)) { + sortedSkillList.addAll(deleteSkillList); + } + LinkedHashMap skillCountVoMap = sortedSkillList.stream().collect(Collectors.toMap(SkillCountVo::getName, + skillCountVo -> skillCountVo, (k1, k2) -> k1, LinkedHashMap::new)); + Map skillCountMap; + for (EmployeeViewDataVo pageListVo : pageListVoList) { + skillCountMap = pageListVo.getSkillList().stream().collect(Collectors.toMap(SkillCountVo::getName, skillCountVo -> skillCountVo, (k1, k2) -> k1)); + Map finalSkillCountMap = skillCountMap; + LinkedList skillListData = new LinkedList<>(); + skillCountVoMap.forEach((name, skillCountVo) -> { + if (finalSkillCountMap.containsKey(name)) { + skillListData.add(finalSkillCountMap.get(name)); + } else { + skillListData.add(SkillCountVo.builder() + .name(skillCountVo.getName()) + .count(0) + .build()); + } + }); + pageListVo.setSkillList(skillListData); + } + } + + /** + * 处理skillList数据;每条数据的长度不一样,要处理成一样的,并且保证顺序一致 + * + * @param skillList 技能点集合 + * @param pageListVoList 待处理数据 + */ + private void processSkillPointData(LinkedList skillList, List pageListVoList) { + //获取所有技能点,并且按照sort字段排序 + List sortedSkillList = skillList.stream() + .filter(item -> item.getDeleteMark().equals(0)) + .sorted(Comparator.comparing(SkillCountVo::getSort)) + .collect(Collectors.toList()); + //获取已删除的技能点 + List deleteSkillList = skillList.stream() + .filter(item -> item.getDeleteMark().equals(1)) + .sorted(Comparator.comparing(SkillCountVo::getSort)) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(deleteSkillList)) { + sortedSkillList.addAll(deleteSkillList); + } + LinkedHashMap skillCountVoMap = sortedSkillList.stream().collect(Collectors.toMap(SkillCountVo::getName, skillCountVo -> skillCountVo, (k1, k2) -> k1, LinkedHashMap::new)); + Map skillCountMap; + for (SummaryPageListVo pageListVo : pageListVoList) { + skillCountMap = pageListVo.getSkillList().stream().collect(Collectors.toMap(SkillCountVo::getName, skillCountVo -> skillCountVo, (k1, k2) -> k1)); + Map finalSkillCountMap = skillCountMap; + LinkedList skillListData = new LinkedList<>(); + skillCountVoMap.forEach((name, skillCountVo) -> { + if (finalSkillCountMap.containsKey(name)) { + skillListData.add(finalSkillCountMap.get(name)); + } else { + skillListData.add(SkillCountVo.builder() + .name(skillCountVo.getName()) + .count(0) + .build()); + } + }); + pageListVo.setSkillList(skillListData); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordServiceImpl.java new file mode 100644 index 0000000..2bd92e2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingRecordServiceImpl.java @@ -0,0 +1,1388 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fantaibao.permission.handling.PermissionHandling; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.authority.FtbAuthorityApi; +import jnpf.authority.service.FtbPermissionUsersService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.consts.DeviceType; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.CultivateFileService; +import jnpf.cultivate.service.TeachingRecordService; +import jnpf.cultivate.service.TeachingSkillService; +import jnpf.cultivate.service.TeachingStudentService; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.entity.cultivate.TeachingStudent; +import jnpf.model.cultivate.dto.teaching.QueryTeachingRecordDto; +import jnpf.model.cultivate.dto.teaching.RecordQueryDto; +import jnpf.model.cultivate.dto.teaching.TeachingSaveDto; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.teaching.CultivateFile; +import jnpf.model.cultivate.po.teaching.CultivateUserView; +import jnpf.model.cultivate.po.teaching.TeachingApprove; +import jnpf.model.cultivate.v2.enums.ApproveEnum; +import jnpf.model.cultivate.v2.teaching.model.V2Attachment; +import jnpf.model.cultivate.v2.teaching.vo.*; +import jnpf.model.cultivate.vo.teaching.*; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.store.Store; +import jnpf.permission.PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.user.QueryUserDTO; +import jnpf.permission.dto.v2.AuthUserNodeDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.model.position.PositionInfoNewVO; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.model.user.UserInfoByIdsPost; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.store.service.StoreService; +import jnpf.util.*; +import jnpf.util.auth.V2AuthPermissionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class TeachingRecordServiceImpl extends ServiceImpl implements TeachingRecordService { + @Resource + private V2UserApi v2UserApi; + @Resource + private FtbPersonnelsRosterManagerApi ftbPersonnelsRosterManagerApi; + @Resource + private TeachingRecordMapper teachingRecordMapper; + @Resource + private TeachingStudentMapper teachingStudentMapper; + @Resource + private TeachingStudentService teachingStudentService; + @Resource + private StoreService storeService; + @Autowired + private V2UserQueryUtil v2UserQueryUtil; + @Resource + private PositionApi positionApi; + @Resource + private TeachingSkillMapper teachingSkillMapper; + @Resource + private TeachingSkillService teachingSkillService; + @Resource + private CultivateFileService cultivateFileService; + @Resource + private PermissionHandling permissionHandling; + @Resource + private TeachingApproveMapper teachingApproveMapper; + @Autowired + private FtbAuthorityApi ftbAuthorityApi; + @Resource + private RedisUtil redisUtil; + @Resource + private CultivateUserViewMapper cultivateUserViewMapper; + @Resource + private FtbPermissionUsersService permissionUsersService; + @Resource + private V2AuthPermissionUtils authPermissionUtils; + + /** + * 根据用户ID生成Redis键 + * + * @param userId 用户ID + * @return Redis键 + */ + public static String getUserPermissionKey(String userId) { + return "user:permission:user:" + userId; + } + + @Override + public List getPostUserList(String storeId) { + + List userBoundVOList = v2UserQueryUtil.getOrganizeChildUserList(storeId); + if (CollectionUtil.isEmpty(userBoundVOList)) { + return new ArrayList<>(); + } + //数据权限过滤 + /*if (!UserProvider.getUser().getIsAdministrator()) { + //非管理员加入数据权限筛选流程 + List permissionUserIds = permissionHandling.getUserIdsByUserId(UserProvider.getLoginUserId()); + if (CollUtil.isNotEmpty(permissionUserIds)) { + userBoundVOList = userBoundVOList.stream().filter(user -> permissionUserIds.contains(user.getId())).collect(Collectors.toList()); + } + }*/ + + Map> dataMap = userBoundVOList.stream().collect(Collectors.groupingBy(UserBoundVO::getPositionId)); + List positionUserVoList = new ArrayList<>(); + dataMap.forEach((k, v) -> { + PositionUserVo positionUserVo = new PositionUserVo(); + positionUserVo.setPostId(k); + positionUserVo.setFullName(v.get(0).getPositionName()); + List userList = v.stream().map(user -> { + StudentUserVo studentUserVo = new StudentUserVo(); + studentUserVo.setUserId(user.getId()); + studentUserVo.setUserName(user.getName()); + String headIcon = UploaderUtil.uploaderImg(user.getHeadIcon()); + studentUserVo.setHeadIcon(headIcon); + return studentUserVo; + }).collect(Collectors.toList()); + positionUserVo.setStudentUserVoList(userList); + positionUserVoList.add(positionUserVo); + }); + return positionUserVoList; + } + + @Override + public PageInfo page(QueryTeachingRecordDto queryTeachingRecordDto) { + String device = UserProvider.getDeviceForAgent().getDevice(); + + String userId = device.equals(DeviceType.APP.getDevice()) ? UserProvider.getLoginUserId() : null; + if (ObjectUtil.isNotNull(queryTeachingRecordDto.getFilterLogin()) && !queryTeachingRecordDto.getFilterLogin()) { + //是否过滤当前登录人 + userId = null; + } + // 技能点分类过滤 + List skillIds = teachingSkillService.querySkillIds(queryTeachingRecordDto.getCategoryId()); + if (skillIds == null) { + return new PageInfo<>(); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .eq(TeachingRecord::getType, 1) + .eq(StrUtil.isNotBlank(userId), TeachingRecord::getUserId, userId) + .eq(StrUtil.isNotBlank(queryTeachingRecordDto.getStoreId()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreId()) + .in(CollectionUtil.isNotEmpty(skillIds), TeachingRecord::getSkillId, skillIds) + // 兼容web端门店条件查询 + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreIdList()) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59"); + if (StrUtil.isNotBlank(queryTeachingRecordDto.getKeyword())) { + //技能点模糊查询数据 + QueryWrapper skillQueryWrapper = new QueryWrapper<>(); + skillQueryWrapper.lambda() + .like(TeachingSkill::getName, queryTeachingRecordDto.getKeyword()); + List teachingSkillList = teachingSkillMapper.selectList(skillQueryWrapper); + + List skillIdList = teachingSkillList.stream().map(TeachingSkill::getId).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(skillIdList)) { + skillIdList.add("-1"); + } + //学员模糊查询数据 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .like(TeachingStudent::getUsername, queryTeachingRecordDto.getKeyword()); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + List studentRecordIds = teachingStudents.stream().map(TeachingStudent::getRecordId).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(studentRecordIds)) { + studentRecordIds.add("-1"); + } + if (device.equals(DeviceType.APP.getDevice())) { + //app端模糊查询 + queryWrapper.lambda() + .and(q -> { + q.in(TeachingRecord::getSkillId, skillIdList) + .or() + .in(TeachingRecord::getId, studentRecordIds); + }); + } else { + // web端模糊查询 + queryWrapper.lambda() + .and(q -> { + q.like(TeachingRecord::getUserName, queryTeachingRecordDto.getKeyword()) + .or().like(TeachingRecord::getPostName, queryTeachingRecordDto.getKeyword()); + }); + } + + } + if (queryTeachingRecordDto.getIsDataPermission() && !UserProvider.getUser().getIsAdministrator()) { + UserInfo loginUser = UserProvider.getUser(); + String hashValue = getValidatedHashValue(UserProvider.getLoginUserId(), ServletUtil.getHeader("module")); + String scope = hashValue.split("#")[0]; + if ("3".equals(scope)) { + //权限为仅下属 就只看学员是“我的下属”的人 + List studentUserIds = loginUser.getIsAdministrator() ? null : permissionHandling.getUserIdsByUserId(UserProvider.getLoginUserId()); + if (studentUserIds != null && studentUserIds.isEmpty()) { + return new PageInfo<>(); + } + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .in(CollectionUtil.isNotEmpty(studentUserIds), TeachingStudent::getUserId, studentUserIds); + List studentList = teachingStudentMapper.selectList(studentQueryWrapper); + if (CollectionUtil.isEmpty(studentList)) { + return new PageInfo<>(); + } + List recordIdList = studentList.stream().map(TeachingStudent::getRecordId).collect(Collectors.toList()); + queryWrapper.lambda() + .in(TeachingRecord::getId, recordIdList); + } else { + List authStoreIdList = getAuthStoreIdList(); + List permissionStoreIdList = CollectionUtil.isEmpty(authStoreIdList) ? List.of("-1") : authStoreIdList; + //获取数据权限用户id集合 + + List queryStoreIdList = loginUser.getIsAdministrator() ? null : permissionStoreIdList; + queryWrapper.lambda() + .in(CollectionUtil.isNotEmpty(queryStoreIdList), TeachingRecord::getStoreId, queryStoreIdList); + } + + } + queryWrapper.lambda() + .orderByDesc(TeachingRecord::getDate) + .orderByDesc(TeachingRecord::getCreatorTime); + PageHelper.startPage((int) queryTeachingRecordDto.getCurrentPage(), (int) queryTeachingRecordDto.getPageSize()); + List teachingRecords = teachingRecordMapper.selectList(queryWrapper); + PageInfo pageInfo = new PageInfo<>(teachingRecords); + if (CollectionUtil.isEmpty(teachingRecords)) { + return new PageInfo<>(); + } + + List ids = teachingRecords.stream().map(TeachingRecord::getId).collect(Collectors.toList()); + + //查询记录学员 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .in(TeachingStudent::getRecordId, ids); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + Map> studentMap = teachingStudents.stream().collect(Collectors.groupingBy(TeachingStudent::getRecordId)); + List voList = JsonUtil.getJsonToList(teachingRecords, TeachingRecordVo.class); + // 技能点查询 + List skillIdList = voList.stream().map(TeachingRecordVo::getSkillId).collect(Collectors.toList()); + QueryWrapper skillQueryWrapper = new QueryWrapper<>(); + skillQueryWrapper.lambda() + .in(TeachingSkill::getId, skillIdList); + List teachingSkillList = teachingSkillMapper.selectList(skillQueryWrapper); + Map skillMap = teachingSkillList.stream().collect(Collectors.toMap(TeachingSkill::getId, Function.identity())); + for (TeachingRecordVo teachingRecordVo : voList) { + List recordStuList = studentMap.get(teachingRecordVo.getId()); + if (CollectionUtil.isNotEmpty(recordStuList)) { + List stuNameList = recordStuList.stream().map(TeachingStudent::getUsername).collect(Collectors.toList()); + String stus = CollectionUtil.join(stuNameList, "、"); + teachingRecordVo.setStudents(stus); + } + //拼接封面 + String cover = teachingRecordVo.getVideoPath() + ".0.jpg"; + teachingRecordVo.setVideoCover(cover); + TeachingSkill teachingSkill = skillMap.get(teachingRecordVo.getSkillId()); + teachingRecordVo.setSkillName(teachingSkill.getName()); + } + PageInfo voPageInfo = new PageInfo<>(); + BeanUtil.copyProperties(pageInfo, voPageInfo); + voPageInfo.setList(voList); + + return voPageInfo; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void add(TeachingSaveDto teachingSaveDto) { + TeachingRecord teachingRecord = JsonUtil.getJsonToBean(teachingSaveDto, TeachingRecord.class); + teachingRecord.setCreatorTime(new Date()); + String recordId = RandomUtil.uuId(); + teachingRecord.setId(recordId); + teachingRecord.setCreatorUserId(UserProvider.getLoginUserId()); + teachingRecord.setStoreId(teachingSaveDto.getStoreId()); + Store info = storeService.getInfo(teachingSaveDto.getStoreId()); + teachingRecord.setStoreName(info.getStoreName()); + String loginUserId = UserProvider.getLoginUserId(); + ActionResult actionResult = v2UserApi.infoAndCopy(UserProvider.getUser().getUserId()); + Assert.isTrue(actionResult.getCode().equals(200), "查询登录用户主岗信息异常"); + UserBoundVO userInfo = actionResult.getData(); + Assert.notNull(userInfo, "未查询到登录用户的主岗信息"); + teachingRecord.setUserId(loginUserId); + teachingRecord.setUserName(userInfo.getUserName()); + teachingRecord.setPostId(userInfo.getPositionId()); + teachingRecord.setDeleteMark(0); + teachingRecord.setPostName(ObjectUtil.isNull(userInfo.getPositionName()) ? null : userInfo.getPositionName()); + teachingRecordMapper.insert(teachingRecord); + //学员入库 + List studentUserIds = teachingSaveDto.getStudentUserIds(); + batchAddStuList(studentUserIds, recordId); + // 新增附件信息 + if (null != teachingSaveDto.getFileList() && !teachingSaveDto.getFileList().isEmpty()) { + List fileList = changeToFileList(recordId, teachingSaveDto.getFileList(), UserProvider.getLoginUserId(), FileEventDTO.FileType.TEACHING); + cultivateFileService.saveBatch(fileList); + } + // 新增审核 + if (teachingSaveDto.getType().equals(1)) { + TeachingApprove approve = new TeachingApprove(FtbUtil.getId(), teachingRecord.getId(), ApproveEnum.WAIT.getCode()); + teachingApproveMapper.insert(approve); + } + } + + @Override + public List changeToFileList(String recordId, List fileList, String userId, FileEventDTO.FileType fileType) { + + List list = new ArrayList<>(); + fileList.forEach(file -> { + CultivateFile f = new CultivateFile(); + f.setId(FtbUtil.getId()); + f.setBusinessId(recordId); + f.setFileName(file.getFileName()); + f.setUrl(file.getUrl()); + f.setSize(file.getSize()); + f.setExtension(file.getExtension()); + f.setType(fileType.getType()); + f.setVideoActionType(file.getVideoActionType()); + f.setCreatorUserId(userId); + f.setLastModifyUserId(userId); + list.add(f); + }); + return list; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void update(TeachingSaveDto teachingSaveDto) { + TeachingRecord teachingRecord = JsonUtil.getJsonToBean(teachingSaveDto, TeachingRecord.class); + teachingRecord.setStoreId(teachingSaveDto.getStoreId()); + Store info = storeService.getInfo(teachingSaveDto.getStoreId()); + teachingRecord.setStoreName(info.getStoreName()); + teachingRecord.setLastModifyTime(new Date()); + teachingRecord.setLastModifyUserId(UserProvider.getLoginUserId()); + teachingRecord.setDeleteMark(0); + teachingRecordMapper.updateById(teachingRecord); + //先删除已存在的学员 + UpdateWrapper deleteWrapper = new UpdateWrapper<>(); + deleteWrapper.lambda() + .eq(TeachingStudent::getRecordId, teachingRecord.getId()); + teachingStudentMapper.delete(deleteWrapper); + batchAddStuList(teachingSaveDto.getStudentUserIds(), teachingRecord.getId()); + // 更新附件信息 + updateFileInfo(teachingSaveDto.getId(), teachingSaveDto.getFileList(), FileEventDTO.FileType.TEACHING); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateFileInfo(String bizId, List fileList, FileEventDTO.FileType fileType) { + // 查询附件列表 + LambdaQueryWrapper fileQuery = new LambdaQueryWrapper() + .eq(CultivateFile::getBusinessId, bizId); + List list = cultivateFileService.list(fileQuery); + if (!list.isEmpty()) { + List collect = list.stream().map(CultivateFile::getId).collect(Collectors.toList()); + cultivateFileService.removeBatchByIds(collect); + } + // 新增附件信息 + if (null != fileList && !fileList.isEmpty()) { + List fList = changeToFileList(bizId, fileList, UserProvider.getLoginUserId(), fileType.TEACHING); + cultivateFileService.saveBatch(fList); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void delete(String id) { + teachingRecordMapper.deleteById(id); + //删除所有学员 + UpdateWrapper deleteWrapper = new UpdateWrapper<>(); + deleteWrapper.lambda() + .eq(TeachingStudent::getRecordId, id); + teachingStudentMapper.delete(deleteWrapper); + } + + @Override + public PageInfo queryMyTeachingRecordList(QueryTeachingRecordDto queryTeachingRecordDto) { + // 技能点模糊查询 + QueryWrapper skillQueryWrapper = new QueryWrapper<>(); + skillQueryWrapper.lambda() + .like(StrUtil.isNotBlank(queryTeachingRecordDto.getKeyword()), TeachingSkill::getName, queryTeachingRecordDto.getKeyword()); + List teachingSkills = teachingSkillMapper.selectList(skillQueryWrapper); + List skillIds = teachingSkills.stream().map(TeachingSkill::getId).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(skillIds)) { + skillIds.add("-1"); + } + // 查询学员记录 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .eq(TeachingStudent::getUserId, UserProvider.getLoginUserId()); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + if (CollectionUtil.isEmpty(teachingStudents)) { + return new PageInfo<>(); + } + List recordIdList = teachingStudents.stream().map(TeachingStudent::getRecordId).collect(Collectors.toList()); + // 查询带教记录 + String keyword = queryTeachingRecordDto.getKeyword(); + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreIdList()) + .in(TeachingRecord::getId, recordIdList) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59") + .and(StrUtil.isNotBlank(keyword), q -> { + q.like(TeachingRecord::getUserName, keyword) + .or() + .in(TeachingRecord::getSkillId, skillIds); + }).orderByDesc(TeachingRecord::getDate) + .orderByDesc(TeachingRecord::getCreatorTime); + PageHelper.startPage((int) queryTeachingRecordDto.getCurrentPage(), (int) queryTeachingRecordDto.getPageSize()); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + if (CollectionUtil.isEmpty(teachingRecords)) { + return new PageInfo<>(); + } + PageInfo pageInfo = new PageInfo<>(teachingRecords); + List teachingRecordVos = toRecordVo(teachingRecords); + // 查询附件信息 + List collect = teachingRecordVos.stream().map(TeachingRecordVo::getId).collect(Collectors.toList()); + Map> fileMap = cultivateFileService.getFileListByBizIds(collect); + // 查询是否浏览 + Set viewedSet = getViewedSet(collect, UserProvider.getLoginUserId()); + teachingRecordVos.forEach(v -> { + v.setFileList(fileMap.getOrDefault(v.getId(), List.of())); + if (viewedSet.contains(v.getId())) { + v.setViewed(true); + } + }); + PageInfo voPageInfo = new PageInfo<>(); + BeanUtil.copyProperties(pageInfo, voPageInfo); + voPageInfo.setList(teachingRecordVos); + return voPageInfo; + } + + @Override + public Set getViewedSet(List bizIds, String userId) { + + if (null == bizIds || bizIds.isEmpty()) { + return new HashSet<>(); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(CultivateUserView::getUserId, userId) + .eq(CultivateUserView::getDeleteMark, ConstantUtil.NUM_FALSE) + .in(CultivateUserView::getBizId, bizIds); + List list = cultivateUserViewMapper.selectList(queryWrapper); + if (list.isEmpty()) { + return new HashSet<>(); + } + return list.stream().map(CultivateUserView::getBizId).collect(Collectors.toSet()); + } + + @Override + public PageInfo teachingStudentDataCount(QueryTeachingRecordDto queryTeachingRecordDto) { + List storeIdList = queryTeachingRecordDto.getStoreIdList(); + List teachingUserIds = queryTeachingRecordDto.getTeachingUserIds(); + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getType, 1) + .eq(TeachingRecord::getDeleteMark, 0) + .in(CollectionUtil.isNotEmpty(storeIdList), TeachingRecord::getStoreId, storeIdList) + .in(CollectionUtil.isNotEmpty(teachingUserIds), TeachingRecord::getUserId, teachingUserIds) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59"); + + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + if (CollectionUtil.isEmpty(teachingRecords)) { + return new PageInfo<>(); + } + List recordIdList = teachingRecords.stream().map(TeachingRecord::getId).collect(Collectors.toList()); + PageHelper.startPage((int) queryTeachingRecordDto.getCurrentPage(), (int) queryTeachingRecordDto.getPageSize()); + queryTeachingRecordDto.setRecordIds(recordIdList); + List teachingDataListVos = teachingStudentMapper.queryTeachingDataList(queryTeachingRecordDto); + + List skillKeyVos = teachingStudentMapper.querySkillKeyList(queryTeachingRecordDto); + Map> skillKeyMap = skillKeyVos.stream().collect(Collectors.groupingBy(SkillKeyVo::getSkillKey)); + setSkillName(teachingDataListVos, skillKeyMap); + PageInfo voPageInfo = new PageInfo<>(teachingDataListVos); + if (CollectionUtil.isEmpty(teachingDataListVos)) { + return new PageInfo<>(); + } + + List allData = teachingStudentMapper.queryTeachingDataList(queryTeachingRecordDto); + dataVoSkillGroupCount(teachingDataListVos, allData, skillKeyVos, 2); + // Map lastPost = getLastStudentPost(recordIdList); + Set set = new HashSet<>(); + voPageInfo.getList().forEach(v -> { + set.add(v.getTeachingUserId()); + set.add(v.getUserId()); + }); + StaffRosterListReq staffRosterListReq = new StaffRosterListReq(); + staffRosterListReq.setPageSize(-1); + staffRosterListReq.setIsQueryAuth("0"); + staffRosterListReq.setUserIds(new ArrayList<>(set)); + ActionResult> pageListVOActionResult = ftbPersonnelsRosterManagerApi.postWithSalary(staffRosterListReq); + Map userMap; + if (null == pageListVOActionResult || !pageListVOActionResult.getCode().equals(200) + || pageListVOActionResult.getData() == null || CollUtil.isEmpty(pageListVOActionResult.getData().getList())) { + userMap = new HashMap<>(); + } else { + userMap = pageListVOActionResult.getData().getList().stream().collect(Collectors.toMap(FtbPersonnelsStaffRosterDto::getUserId, Function.identity())); + } + for (TeachingDataListVo vo : voPageInfo.getList()) { + FtbPersonnelsStaffRosterDto teachUser = userMap.get(vo.getTeachingUserId()); + FtbPersonnelsStaffRosterDto user = userMap.get(vo.getUserId()); + if (null == teachUser) { + vo.setTeachingUsername("--"); + } else { + vo.setTeachingUsername(teachUser.getName()); + } + if (null == user) { + vo.setUsername("--"); + vo.setStoreId(null); + vo.setStoreName(null); + vo.setPostName(null); + } else { + vo.setUsername(user.getName()); + vo.setStoreId(user.getCurrOrg()); + vo.setStoreName(user.getCurrOrgName()); + vo.setPostName(StringUtil.isEmpty(user.getCurrPositionName()) ? "--" : user.getCurrPositionName()); + } + } + setSkillName(allData, skillKeyMap); + return voPageInfo; + } + + private void dataVoSkillGroupCount(List teachingDataListVos, List allData, List skillKeyVos, Integer countType) { + if (CollectionUtil.isEmpty(teachingDataListVos) || CollectionUtil.isEmpty(allData)) { + return; + } + + List allSkillNameList = skillKeyVos.stream() + .map(SkillKeyVo::getSkillName) + .distinct().collect(Collectors.toList()); + + Map> skillCountGroupMap = skillKeyVos.stream().collect(Collectors.groupingBy(SkillKeyVo::getSkillKey)); + + for (TeachingDataListVo teachingDataListVo : teachingDataListVos) { + String userId = ""; + if (countType == 1) { + userId = teachingDataListVo.getTeachingUserId(); + } else { + userId = teachingDataListVo.getUserId(); + } + List skillCountList = skillCountGroupMap.get(userId + teachingDataListVo.getStoreId()); + if (ObjectUtil.isNull(skillCountList)) { + skillCountList = new ArrayList<>(); + } + Map> skillNameMap = skillCountList.stream().collect(Collectors.groupingBy(SkillKeyVo::getSkillName)); + List skillCountVoList = new ArrayList<>(); + //找到学员带教记录中对应的技能点 + for (String skillName : allSkillNameList) { + List skillVoList = skillNameMap.get(skillName); + SkillCountVo countVo = new SkillCountVo(); + countVo.setName(skillName); + countVo.setCount(CollectionUtil.size(skillVoList)); + skillCountVoList.add(countVo); + } + teachingDataListVo.setSkillList(skillCountVoList); + + } + } + + @Override + public List queryMyTeachingData(QueryTeachingRecordDto queryTeachingRecordDto) { + // 查询学员记录 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .eq(TeachingStudent::getUserId, UserProvider.getLoginUserId()); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + if (CollectionUtil.isEmpty(teachingStudents)) { + return new ArrayList<>(); + } + List recordIdList = teachingStudents.stream().map(TeachingStudent::getRecordId).collect(Collectors.toList()); + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .in(TeachingRecord::getId, recordIdList) + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreIdList()) + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getTeachingUserIds()), TeachingRecord::getUserId, queryTeachingRecordDto.getTeachingUserIds()) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59"); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + if (CollectionUtil.isEmpty(teachingRecords)) { + return new ArrayList<>(); + } + List recordIds = teachingRecords.stream().map(TeachingRecord::getId).collect(Collectors.toList()); + return teachingRecordMapper.skillCount(recordIds); + } + + @Override + public List teachingSelectList(QueryTeachingRecordDto queryTeachingRecordDto) { + // 查询权限下的门店 + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + if (null != targetAuthIdsVO && targetAuthIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.NONE)) { + return List.of(); + } + //取交集 + if (CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList())) { + if (null == targetAuthIdsVO) { + queryTeachingRecordDto.setStoreIdList(queryTeachingRecordDto.getStoreIdList()); + } else { + Collection intersection = CollectionUtil.intersection(targetAuthIdsVO.getIds(), queryTeachingRecordDto.getStoreIdList()); + queryTeachingRecordDto.setStoreIdList(new ArrayList<>(intersection)); + } + } else { + queryTeachingRecordDto.setStoreIdList(null == targetAuthIdsVO ? List.of() : targetAuthIdsVO.getIds()); + } + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .eq(TeachingRecord::getType, 1) + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreIdList()) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59") + .groupBy(TeachingRecord::getUserId) + .orderByDesc(TeachingRecord::getCreatorTime); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + teachingRecords = teachingRecords.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(TeachingRecord::getUserId))), ArrayList::new)); + List teachingUserVoList = JsonUtil.getJsonToList(teachingRecords, TeachingUserVo.class); + if (CollectionUtil.isEmpty(teachingUserVoList)) { + return teachingUserVoList; + } + List userIds = teachingUserVoList.stream().map(TeachingUserVo::getUserId).collect(Collectors.toList()); + Map userMap = getUserMap(userIds); + for (TeachingUserVo teachingUserVo : teachingUserVoList) { + //头像填充 + UserBoundVO user = userMap.get(teachingUserVo.getUserId()); + if (null != user) { + teachingUserVo.setHeadIcon(UploaderUtil.uploaderImg(user.getHeadIcon())); + teachingUserVo.setUserName(user.getUserName()); + teachingUserVo.setPostName(user.getPositionName()); + } + } + return teachingUserVoList; + } + + @Override + public List teachingSuperiorSelectList(QueryTeachingRecordDto queryTeachingRecordDto) { + //查询当前登录人的学员记录 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .eq(TeachingStudent::getUserId, UserProvider.getLoginUserId()); + List studentList = teachingStudentMapper.selectList(studentQueryWrapper); + if (CollectionUtil.isEmpty(studentList)) { + return new ArrayList<>(); + } + + List recordIds = studentList.stream().map(TeachingStudent::getRecordId).collect(Collectors.toList()); + //查询对应带教记录 + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getType, 1) + .eq(TeachingRecord::getDeleteMark, 0) + .in(TeachingRecord::getId, recordIds) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59"); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + //带教员userId去重 + teachingRecords = teachingRecords.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(TeachingRecord::getUserId))), ArrayList::new)); + + List teachingUserVoList = JsonUtil.getJsonToList(teachingRecords, TeachingUserVo.class); + if (CollectionUtil.isEmpty(teachingUserVoList)) { + return teachingUserVoList; + } + List userIds = teachingUserVoList.stream().map(TeachingUserVo::getUserId).collect(Collectors.toList()); + UserInfoByIdsPost userInfoByIdsPost = new UserInfoByIdsPost(); + userInfoByIdsPost.setUserIds(userIds); + userInfoByIdsPost.setTenantId(UserProvider.getUser().getTenantId()); +// List allUserList = userApi.getInfoByIdsManyAndCopyPost(userInfoByIdsPost); + List allUserList = v2UserQueryUtil.getUserListAndCopy(userIds, UserProvider.getUser().getTenantId()); + Map userMap = allUserList.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + + Map lastPost = getLastPost(queryTeachingRecordDto); + String storeId = queryTeachingRecordDto.getStoreIdList().get(0); + for (TeachingUserVo teachingUserVo : teachingUserVoList) { + //头像填充 + PartUserInfoVo partUserInfoVo = userMap.get(teachingUserVo.getUserId()); + teachingUserVo.setHeadIcon(ObjectUtil.isNull(partUserInfoVo) ? "" : UploaderUtil.uploaderImg(partUserInfoVo.getHeadIcon())); + String key = storeId + teachingUserVo.getUserId(); + teachingUserVo.setPostName(lastPost.get(key)); + } + return teachingUserVoList; + } + + /** + * 获取带教记录详情(修改时回显) + * @param id 带教记录id + * @return TeachingRecordDetailVo + */ + @Override + public TeachingRecordDetailVo getTeachingDetail(String id) { + TeachingRecord teachingRecord = teachingRecordMapper.selectById(id); + TeachingRecordDetailVo detailVo = JsonUtil.getJsonToBean(teachingRecord, TeachingRecordDetailVo.class); + TeachingSkill teachingSkill = teachingSkillMapper.selectById(detailVo.getSkillId()); + detailVo.setSkillName(teachingSkill.getName()); + detailVo.setSkillDesc(teachingSkill.getDescription()); + + //视频封面 + String videoCover = teachingRecord.getVideoPath() + ".0.jpg"; + detailVo.setVideoCover(videoCover); + + //查询所属学员 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .eq(TeachingStudent::getRecordId, id); + List studentList = teachingStudentMapper.selectList(studentQueryWrapper); + if (CollectionUtil.isEmpty(studentList)) { + return detailVo; + } + List studentIdList = studentList.stream().map(TeachingStudent::getUserId).collect(Collectors.toList()); + detailVo.setStudentIdList(studentIdList); + + List studentNames = studentList.stream().map(TeachingStudent::getUsername).collect(Collectors.toList()); + String studentName = CollectionUtil.join(studentNames, "、"); + detailVo.setStudents(studentName); + return detailVo; + } + + @Override + public List getTeachingStoreList() { + UserInfo loginUser = UserProvider.getUser(); + List permissionStoreIds = loginUser.getIsAdministrator() ? null : permissionHandling.getStoreIdsByUserId(UserProvider.getLoginUserId()); + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .eq(TeachingRecord::getType, 1) + .in(CollectionUtil.isNotEmpty(permissionStoreIds), TeachingRecord::getStoreId, permissionStoreIds) + .groupBy(TeachingRecord::getStoreId); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + return JsonUtil.getJsonToList(teachingRecords, TeachingStoreListVo.class); + } + + @Override + public PageInfo teachingSummaryPage(QueryTeachingRecordDto queryTeachingRecordDto) { + // 获取数据权限用户id + TargetAuthIdsVO targetAuthIdsVO = authPermissionUtils.processAuthIds(); + if (null != targetAuthIdsVO && targetAuthIdsVO.getTargetAuthEnums().equals(TargetAuthEnums.NONE)) { + return new PageInfo<>(); + } + //取交集 + if (CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList())) { + if (null == targetAuthIdsVO) { + queryTeachingRecordDto.setStoreIdList(queryTeachingRecordDto.getStoreIdList()); + } else { + Collection intersection = CollectionUtil.intersection(targetAuthIdsVO.getIds(), queryTeachingRecordDto.getStoreIdList()); + queryTeachingRecordDto.setStoreIdList(new ArrayList<>(intersection)); + } + } else { + queryTeachingRecordDto.setStoreIdList(null == targetAuthIdsVO ? List.of() : targetAuthIdsVO.getIds()); + } + // Map lastPostMap = getLastPost(queryTeachingRecordDto); + //先查询技能点 + List skillNameList = teachingRecordMapper.queryCountSkillList(queryTeachingRecordDto); + PageHelper.startPage((int) queryTeachingRecordDto.getCurrentPage(), (int) queryTeachingRecordDto.getPageSize()); + List teachingDataListVos = teachingRecordMapper.teachingSummaryQuery(queryTeachingRecordDto); + PageInfo pageInfo = new PageInfo<>(teachingDataListVos); + //查询所有技能点 + List allData = teachingRecordMapper.teachingSummaryQuery(queryTeachingRecordDto); + dataVoSkillGroupCount(teachingDataListVos, allData, skillNameList, 1); + List userIds = pageInfo.getList().stream().map(TeachingDataListVo::getTeachingUserId).collect(Collectors.toList()); + Map userMap = getUserMap(userIds); + //时间范围内最新岗位 + for (TeachingDataListVo teachingDataListVo : pageInfo.getList()) { + // String postName = lastPostMap.get(teachingDataListVo.getStoreId() + teachingDataListVo.getTeachingUserId()); + // teachingDataListVo.setPostName(postName); + UserBoundVO user = userMap.get(teachingDataListVo.getTeachingUserId()); + if (null != user) { + teachingDataListVo.setTeachingUsername(user.getUserName()); + teachingDataListVo.setPostName(StringUtil.isEmpty(user.getPositionName()) ? "--" : user.getPositionName()); + teachingDataListVo.setUsername(user.getUserName()); + } + } + return pageInfo; + } + + private Map getUserMap(List userIds) { + + List list; + if (userIds.isEmpty()) { + list = new ArrayList<>(); + } else { + list = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId()); + } + return list.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } + + @Override + public void summaryTeachExport(QueryTeachingRecordDto queryTeachingRecordDto) { + queryTeachingRecordDto.setCurrentPage(1); + queryTeachingRecordDto.setPageSize(10000); + PageInfo pageInfo = teachingSummaryPage(queryTeachingRecordDto); + List list = pageInfo.getList(); + String filename = "汇总-带教" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + ".xlsx"; + EasyExcelUtil.dynamicTableExport(getHeadList(list, List.of("门店名称", "带教员", "带教员岗位", "带教总次数")), getExportDataList(list), filename, "汇总带教"); + } + + @Override + public void studentDetailExport(QueryTeachingRecordDto queryTeachingRecordDto) { + queryTeachingRecordDto.setCurrentPage(1); + queryTeachingRecordDto.setPageSize(10000); + PageInfo voPageInfo = teachingStudentDataCount(queryTeachingRecordDto); + List> headList = getHeadList(voPageInfo.getList(), List.of("带教员", "学员", "学员岗位", "带教总次数")); + List> exportDataList = getSummaryDetailExportDataList(voPageInfo.getList()); + String filename = "汇总-带教" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + ".xlsx"; + EasyExcelUtil.dynamicTableExport(headList, exportDataList, filename, "汇总学员详情"); + } + + @Override + public PageInfo teachingItemPage(QueryTeachingRecordDto queryTeachingRecordDto) { + return page(queryTeachingRecordDto); + } + + @Override + public void detailItemTeachExport(QueryTeachingRecordDto queryTeachingRecordDto) { + queryTeachingRecordDto.setCurrentPage(1); + queryTeachingRecordDto.setPageSize(10000); + queryTeachingRecordDto.setIsDataPermission(true); + PageInfo page = page(queryTeachingRecordDto); + List list = page.getList(); + List> headList = getHeadList(null, List.of( + "门店名称", + "带教员", + "带教员岗位", + "学员", + "技能点", + "日期" + )); + List> teachingDetailItemExportData = getTeachingDetailItemExportData(list); + String filename = "汇总-明细" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + ".xlsx"; + EasyExcelUtil.dynamicTableExport(headList, teachingDetailItemExportData, filename, "带教明细"); + } + + /** + * 带教明细-批量删除 + * @param ids 记录id集合 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void batchDeleteRecord(List ids) { + UpdateWrapper recordDeleteWrapper = new UpdateWrapper<>(); + recordDeleteWrapper.lambda() + .in(TeachingRecord::getId, ids); + teachingRecordMapper.delete(recordDeleteWrapper); + + //删除对应学员 + UpdateWrapper stuDeleteWrapper = new UpdateWrapper<>(); + stuDeleteWrapper.lambda() + .in(TeachingStudent::getRecordId, ids); + teachingStudentMapper.delete(stuDeleteWrapper); + } + + /** + * 店长界面带教统计 + * @param storeId 门店id + * @return TeachingStoreCountVo + */ + @Override + public TeachingStoreCountVo storeTeachingCount(String storeId) { + String loginUserId = UserProvider.getLoginUserId(); + //查询我的带教记录 + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getStoreId, storeId) + .eq(TeachingRecord::getDeleteMark, 0) + .eq(TeachingRecord::getType, 1) + .eq(TeachingRecord::getUserId, loginUserId) + .apply("DATE_FORMAT(F_Date,'%Y-%m') = DATE_FORMAT(NOW(),'%Y-%m')"); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + TeachingStoreCountVo teachingStoreCountVo = new TeachingStoreCountVo(); + if (CollectionUtil.isEmpty(teachingRecords)) { + teachingStoreCountVo.setTeachingCount(0L); + teachingStoreCountVo.setStudentCount(0L); + return teachingStoreCountVo; + } + teachingStoreCountVo.setTeachingCount((long) teachingRecords.size()); + //查询学员 + List recordIds = teachingRecords.stream().map(TeachingRecord::getId).collect(Collectors.toList()); + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .in(TeachingStudent::getRecordId, recordIds); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + teachingStudents = teachingStudents.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(TeachingStudent::getUserId))), ArrayList::new)); + teachingStoreCountVo.setStudentCount(CollectionUtil.isEmpty(teachingStudents) ? 0 : (long) teachingStudents.size()); + return teachingStoreCountVo; + } + + @Override + public TeachingCountVo teachingCount(QueryTeachingRecordDto queryTeachingRecordDto) { + TeachingCountVo teachingCountVo = new TeachingCountVo(); + QueryWrapper recordQueryWrapper = new QueryWrapper<>(); + recordQueryWrapper.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .eq(TeachingRecord::getType, 1) + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getTeachingUserIds()), TeachingRecord::getUserId, queryTeachingRecordDto.getTeachingUserIds()) + .eq(StrUtil.isNotBlank(queryTeachingRecordDto.getStoreId()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreId()) + // 兼容web端门店条件查询 + .in(CollectionUtil.isNotEmpty(queryTeachingRecordDto.getStoreIdList()), TeachingRecord::getStoreId, queryTeachingRecordDto.getStoreIdList()) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59"); + List teachingRecords = teachingRecordMapper.selectList(recordQueryWrapper); + if (CollectionUtil.isEmpty(teachingRecords)) { + teachingCountVo.setTeachingCount(0L); + teachingCountVo.setTeachingPersonCount(0L); + return teachingCountVo; + } + //带教总次数 + long count = teachingRecords.size(); + teachingCountVo.setTeachingCount(count); + //带教总人数 + List recordIds = teachingRecords.stream().map(TeachingRecord::getId).collect(Collectors.toList()); + + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .in(TeachingStudent::getRecordId, recordIds) + .groupBy(TeachingStudent::getUserId); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + teachingCountVo.setTeachingPersonCount(CollectionUtil.isEmpty(teachingStudents) ? 0 : (long) teachingStudents.size()); + + return teachingCountVo; + } + + @Override + public SuperiorTeachingSummaryVo getSuperiorTeachingSummary(String storeId) { + return this.baseMapper.getSuperiorTeachingSummary(storeId, UserProvider.getLoginUserId()); + } + + @Override + public List getStoreRecordCount(RecordQueryDto queryDto) { + + // 如果关键字差查询不为空, 先过滤出满足条件的recordId + List recordIds = null; + if (StringUtil.isNotEmpty(queryDto.getKeyword())) { + recordIds = teachingStudentMapper.getRecordIdByKeyword(queryDto.getKeyword()); + if (recordIds == null) { + return List.of(); + } + } + return this.baseMapper.getStoreRecordCount(queryDto, recordIds); + } + + @Override + public PageInfo getStoreRecordPage(RecordQueryDto queryDto) { + + List recordIds = null; + if (StringUtil.isNotEmpty(queryDto.getKeyword())) { + recordIds = teachingStudentMapper.getRecordIdByKeyword(queryDto.getKeyword()); + if (recordIds == null) { + return new PageInfo<>(); + } + } + PageHelper.startPage((int) queryDto.getCurrentPage(), (int) queryDto.getPageSize()); + PageInfo page = new PageInfo<>(baseMapper.getStoreRecordList(queryDto, recordIds, UserProvider.getLoginUserId())); + if (page.getList().isEmpty()) { + return new PageInfo<>(); + } + // 查询学员信息 + List recordIdList = page.getList().stream().map(V2TeachingRecordVo::getId).collect(Collectors.toList()); + Map> stuMap = getStuMap(recordIdList); + // 查询附件信息 + Map> fileMap = getFileMap(recordIdList); + page.getList().forEach(v -> setStudentAndFile(v, stuMap, fileMap)); + return page; + } + + private void setStudentAndFile(V2TeachingRecordVo v, Map> stuMap, Map> fileMap) { + + List students = stuMap.get(v.getId()); + if (null != students && !students.isEmpty()) { + List studentList = JsonUtil.getJsonToList(students, StudentInfoVo.class); + v.setStudentList(studentList); + } + List files = fileMap.get(v.getId()); + if (null != files && !files.isEmpty()) { + List attachments = new ArrayList<>(); + files.forEach(file -> { + V2Attachment v2Attachment = cultivateFileService.convertToV2Attachment(file); + attachments.add(v2Attachment); + }); + v.setAttachments(attachments); + } + } + + @Override + public Map> getFileMap(List recordIds) { + + LambdaQueryWrapper fileQuery = new LambdaQueryWrapper() + .in(CultivateFile::getBusinessId, recordIds) + .orderByDesc(CultivateFile::getCreatorTime) + .orderByDesc(CultivateFile::getId); + List fileList = cultivateFileService.list(fileQuery); + return fileList.stream().collect(Collectors.groupingBy(CultivateFile::getBusinessId)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void removeAllOldFile() { + // 查询所有旧文件 + List list = this.list(new LambdaQueryWrapper() + .eq(TeachingRecord::getDeleteMark, ConstantUtil.NUM_FALSE) + .isNotNull(TeachingRecord::getVideoPath)); + if (list.isEmpty()) { + return; + } + List fileList = new ArrayList<>(); + list.forEach(v -> { + CultivateFile f = new CultivateFile(); + String fileName = v.getVideoPath().substring(v.getVideoPath().lastIndexOf("/") + 1); + String suffix = fileName.substring(fileName.lastIndexOf(".") + 1); + f.setId(FtbUtil.getId()); + f.setBusinessId(v.getId()); + f.setFileName(fileName); + f.setUrl(v.getVideoPath()); + f.setExtension(suffix); + f.setType(v.getType().equals(1) ? FileEventDTO.FileType.TEACHING.getType() : FileEventDTO.FileType.PRACTICE.getType()); + f.setVideoActionType(v.getVideoActionType()); + fileList.add(f); + }); + if (!fileList.isEmpty()) { + cultivateFileService.saveBatch(fileList); + } + } + + private Map> getStuMap(List recordIds) { + + LambdaQueryWrapper stuQuery = new LambdaQueryWrapper() + .eq(TeachingStudent::getDeleteMark, ConstantUtil.NUM_FALSE) + .in(TeachingStudent::getRecordId, recordIds) + .orderByDesc(TeachingStudent::getCreatorTime) + .orderByDesc(TeachingStudent::getId); + List stuList = teachingStudentMapper.selectList(stuQuery); + return stuList.stream().collect(Collectors.groupingBy(TeachingStudent::getRecordId)); + } + + @Override + public TeachingDetailVo getTeachingDetailById(String bizId) { + + TeachingDetailVo vo = baseMapper.getTeachingDetailById(bizId); + // 设置头像 + List list = v2UserApi.userListAndCopy(List.of(vo.getUserId()), null, UserProvider.getUser().getTenantId()); + if (null != list && !list.isEmpty()) { + vo.setHeadIcon(list.get(0).getHeadIcon()); + } + // 设置学员 + Map> stuMap = getStuMap(List.of(bizId)); + List students = stuMap.get(bizId); + if (null != students && !students.isEmpty()) { + List studentList = JsonUtil.getJsonToList(students, StudentInfoVo.class); + vo.setStudentList(studentList); + } + // 设置附件 + Map> fileMap = getFileMap(List.of(bizId)); + List files = fileMap.get(bizId); + if (null != files && !files.isEmpty()) { + List attachments = new ArrayList<>(); + files.forEach(file -> { + V2Attachment v2Attachment = cultivateFileService.convertToV2Attachment(file); + attachments.add(v2Attachment); + }); + vo.setFileList(attachments); + } + // 设置审批 + LambdaQueryWrapper approveQuery = new LambdaQueryWrapper() + .eq(TeachingApprove::getTeachId, bizId); + TeachingApprove teachingApprove = teachingApproveMapper.selectOne(approveQuery); + TeachingApproveVo approveVo = JsonUtil.getJsonToBean(teachingApprove, TeachingApproveVo.class); + List fileListApprove; + if (null == approveVo) { + fileListApprove = new ArrayList<>(); + } else { + Map> approveFileMap = getFileMap(List.of(approveVo.getId())); + fileListApprove = approveFileMap.getOrDefault(approveVo.getId(), List.of()); + } + // 查询审批附件 + if (!fileListApprove.isEmpty()) { + List attachments = new ArrayList<>(); + fileListApprove.forEach(file -> { + V2Attachment v2Attachment = cultivateFileService.convertToV2Attachment(file); + attachments.add(v2Attachment); + }); + approveVo.setFileList(attachments); + } + vo.setTeachingApprove(approveVo); + return vo; + } + + private Map getLastPost(QueryTeachingRecordDto queryTeachingRecordDto) { + QueryWrapper postRecordQuery = new QueryWrapper<>(); + postRecordQuery.lambda() + .eq(TeachingRecord::getDeleteMark, 0) + .eq(TeachingRecord::getType, 1) + .between(StrUtil.isNotBlank(queryTeachingRecordDto.getStartDate()) && StrUtil.isNotBlank(queryTeachingRecordDto.getEndDate()), TeachingRecord::getDate, + queryTeachingRecordDto.getStartDate() + " 00:00:00", queryTeachingRecordDto.getEndDate() + " 23:59:59"); + List teachingRecords = teachingRecordMapper.selectList(postRecordQuery); + if (CollectionUtil.isEmpty(teachingRecords)) { + return new HashMap<>(); + } + Map map = new HashMap<>(); + Map> groupMap = teachingRecords.stream().collect(Collectors.groupingBy( + teachingRecord -> teachingRecord.getStoreId() + teachingRecord.getUserId())); + groupMap.forEach((key, recordList) -> { + recordList.stream().max(Comparator.comparing(TeachingRecord::getCreatorTime)) + .ifPresent(teachingRecord -> map.put(key, teachingRecord.getPostName())); + }); + return map; + } + + private Map getLastStudentPost(List recordIds) { + QueryWrapper postRecordQuery = new QueryWrapper<>(); + postRecordQuery.lambda() + .eq(TeachingStudent::getDeleteMark, 0) + .in(TeachingStudent::getRecordId, recordIds); + List teachingRecords = teachingStudentMapper.selectList(postRecordQuery); + if (CollectionUtil.isEmpty(teachingRecords)) { + return new HashMap<>(); + } + Map map = new HashMap<>(); + Map> groupMap = teachingRecords.stream().collect(Collectors.groupingBy(new Function() { + @Override + public String apply(TeachingStudent teachingRecord) { + return teachingRecord.getUserId(); + } + })); + groupMap.forEach((key, recordList) -> { + TeachingStudent teachingStudent = recordList.stream().max(Comparator.comparing(TeachingStudent::getCreatorTime)).orElse(null); + if (teachingStudent == null) { + return; + } + map.put(key, teachingStudent.getPostName()); + }); + return map; + } + + /** + * 获取带教明细导出数据 + * @return List> 导出excel所使用的数据 + */ + private List> getTeachingDetailItemExportData(List list) { + List> dataList = new ArrayList<>(); + if (CollectionUtil.isEmpty(list)) { + return dataList; + } + for (TeachingRecordVo teachingRecordVo : list) { + List voDataList = new ArrayList<>(); + voDataList.add(teachingRecordVo.getStoreName()); + voDataList.add(teachingRecordVo.getUserName()); + voDataList.add(teachingRecordVo.getPostName()); + voDataList.add(teachingRecordVo.getStudents()); + voDataList.add(teachingRecordVo.getSkillName()); + voDataList.add(DateUtil.formatDate(teachingRecordVo.getDate())); + dataList.add(voDataList); + } + return dataList; + } + + public List> getHeadList(List list, List preFixHeadList) { + List> headList = new ArrayList<>(); + for (String head : preFixHeadList) { + headList.add(List.of(head)); + } + if (CollectionUtil.isEmpty(list)) { + return headList; + } + List> skillList = list.stream().map(TeachingDataListVo::getSkillList).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(skillList)) { + return headList; + } + List skillCountVoList = skillList.stream() + .max(Comparator.comparingInt(List::size)) + .orElse(Collections.emptyList()); + for (SkillCountVo skillCountVo : skillCountVoList) { + List head = new ArrayList<>(); + head.add(skillCountVo.getName()); + headList.add(head); + } + return headList; + } + + public List> getExportDataList(List list) { + List> dataList = new ArrayList<>(); + if (CollectionUtil.isEmpty(list)) { + return dataList; + } + List> skillList = list.stream() + .filter(Objects::nonNull) + .map(TeachingDataListVo::getSkillList) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List skillCountVoList = skillList.stream() + .max(Comparator.comparingInt(List::size)) + .orElse(Collections.emptyList()); + for (TeachingDataListVo teachingDataListVo : list) { + List voDataList = new ArrayList<>(); + voDataList.add(teachingDataListVo.getStoreName()); + voDataList.add(teachingDataListVo.getUsername()); + voDataList.add(teachingDataListVo.getPostName()); + voDataList.add(String.valueOf(teachingDataListVo.getTeachingCount())); + List voSkillList = teachingDataListVo.getSkillList(); + Map skillCountVoMap = voSkillList.stream().collect(Collectors.toMap(SkillCountVo::getName, Function.identity())); + for (SkillCountVo skillCountVo : skillCountVoList) { + SkillCountVo countVo = skillCountVoMap.get(skillCountVo.getName()); + if (ObjectUtil.isNull(countVo)) { + voDataList.add("0"); + } else { + voDataList.add(String.valueOf(countVo.getCount())); + } + } + dataList.add(voDataList); + } + return dataList; + } + + public List> getSummaryDetailExportDataList(List list) { + List> dataList = new ArrayList<>(); + if (CollectionUtil.isEmpty(list)) { + return dataList; + } + List> skillList = list.stream() + .filter(Objects::nonNull) + .map(TeachingDataListVo::getSkillList) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List skillCountVoList = skillList.stream() + .max(Comparator.comparingInt(List::size)) + .orElse(Collections.emptyList()); + for (TeachingDataListVo teachingDataListVo : list) { + List voDataList = new ArrayList<>(); + voDataList.add(teachingDataListVo.getTeachingUsername()); + voDataList.add(teachingDataListVo.getUsername()); + voDataList.add(teachingDataListVo.getPostName()); + voDataList.add(String.valueOf(teachingDataListVo.getTeachingCount())); + List voSkillList = teachingDataListVo.getSkillList(); + Map skillCountVoMap = voSkillList.stream().collect(Collectors.toMap(SkillCountVo::getName, Function.identity())); + for (SkillCountVo skillCountVo : skillCountVoList) { + SkillCountVo countVo = skillCountVoMap.get(skillCountVo.getName()); + if (ObjectUtil.isNull(countVo)) { + voDataList.add("0"); + } else { + voDataList.add(String.valueOf(countVo.getCount())); + } + } + dataList.add(voDataList); + } + return dataList; + } + + /** + * 批量新增学员 + * @param studentUserIds 学员id集合 + * @param recordId 记录id + */ + private void batchAddStuList(List studentUserIds, String recordId) { + if (CollectionUtil.isEmpty(studentUserIds)) { + return; + } + QueryUserDTO queryUserDTO = new QueryUserDTO(); + queryUserDTO.setUserIds(studentUserIds); + List infoByIdsPost = v2UserQueryUtil.getUserListToPartUserInfoVo(studentUserIds); + Map userInfoVoMap = infoByIdsPost.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + //查询用户岗位 + List postIdList = infoByIdsPost.stream().map(PartUserInfoVo::getPositionId).collect(Collectors.toList()); + ActionResult> positionListByIds = positionApi.getPositionListByIds(postIdList); + List postList = positionListByIds.getData(); + Map postMap = postList.stream().collect(Collectors.toMap(PositionInfoNewVO::getId, Function.identity())); + List addStudentList = new ArrayList<>(); + for (String studentUserId : studentUserIds) { + TeachingStudent teachingStudent = new TeachingStudent(); + teachingStudent.setId(RandomUtil.uuId()); + teachingStudent.setRecordId(recordId); + teachingStudent.setCreatorTime(new Date()); + teachingStudent.setDeleteMark(0); + teachingStudent.setCreatorUserId(UserProvider.getLoginUserId()); + teachingStudent.setUserId(studentUserId); + PartUserInfoVo partUserInfoVo = userInfoVoMap.get(studentUserId); + teachingStudent.setUsername(partUserInfoVo.getRealName()); + PositionInfoNewVO positionInfoNewVO = postMap.get(partUserInfoVo.getPositionId()); + teachingStudent.setPostId(partUserInfoVo.getPositionId()); + teachingStudent.setPostName(positionInfoNewVO.getFullName()); + addStudentList.add(teachingStudent); + } + teachingStudentService.saveBatch(addStudentList); + } + + private List toRecordVo(List teachingRecords) { + List ids = teachingRecords.stream().map(TeachingRecord::getId).collect(Collectors.toList()); + //查询记录学员 + QueryWrapper studentQueryWrapper = new QueryWrapper<>(); + studentQueryWrapper.lambda() + .in(TeachingStudent::getRecordId, ids); + List teachingStudents = teachingStudentMapper.selectList(studentQueryWrapper); + Map> studentMap = teachingStudents.stream().collect(Collectors.groupingBy(TeachingStudent::getRecordId)); + List voList = JsonUtil.getJsonToList(teachingRecords, TeachingRecordVo.class); + // 技能点 + List skillIds = teachingRecords.stream().map(TeachingRecord::getSkillId).collect(Collectors.toList()); + QueryWrapper skillQueryWrapper = new QueryWrapper<>(); + skillQueryWrapper.lambda() + .in(TeachingSkill::getId, skillIds); + List teachingSkills = teachingSkillMapper.selectList(skillQueryWrapper); + Map skillMap = teachingSkills.stream().collect(Collectors.toMap(TeachingSkill::getId, Function.identity())); + for (TeachingRecordVo teachingRecordVo : voList) { + List recordStuList = studentMap.get(teachingRecordVo.getId()); + List stuNameList = recordStuList.stream().map(TeachingStudent::getUsername).collect(Collectors.toList()); + String stus = CollectionUtil.join(stuNameList, "、"); + teachingRecordVo.setStudents(stus); + //拼接封面 + String cover = teachingRecordVo.getVideoPath() + ".0.jpg"; + teachingRecordVo.setVideoCover(cover); + //技能点名称 + TeachingSkill teachingSkill = skillMap.get(teachingRecordVo.getSkillId()); + teachingRecordVo.setSkillName(ObjectUtil.isNull(teachingSkill) ? null : teachingSkill.getName()); + } + return voList; + } + + /** + * 获取当前用户权限过滤后的门店id集合 + * @return 门店id + */ + private List getAuthStoreIdList() { + AuthUserNodeDTO authUserNodeDTO = new AuthUserNodeDTO(); + authUserNodeDTO.setHavaAuth(true); + authUserNodeDTO.setTenantId(UserProvider.getUser().getTenantId()); + authUserNodeDTO.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.STORE)); + ActionResult> listActionResult = ftbAuthorityApi.authOrganizesByUserBound(List.of(OrganizeCategoryEnums.STORE)); + List organizeGeneralDetailVOList = listActionResult.getData(); + return organizeGeneralDetailVOList.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } + + private Boolean isOnlyUnderling() { + String hashValue = getValidatedHashValue(UserProvider.getLoginUserId(), ServletUtil.getHeader("Module")); + return "3".equals(hashValue.split("#")[0]); + } + + private String getValidatedHashValue(String userId, String module) { + String hashValue = redisUtil.getHashValues(getUserPermissionKey(userId), module); + if (StrUtil.isBlank(hashValue)) { + throw new RuntimeException("当前用户暂未设置此模块数据权限适用范围"); + } + return hashValue; + } + + private void setSkillName(List allData, Map> skillKeyMap) { + + for (TeachingDataListVo allDatum : allData) { + String key = ""; + //查学员 + key = allDatum.getUserId() + allDatum.getStoreId(); + List skillKeyVos = skillKeyMap.get(key); + if (CollectionUtil.isNotEmpty(skillKeyVos)) { + List nameList = skillKeyVos.stream().map(SkillKeyVo::getSkillName).collect(Collectors.toList()); + allDatum.setSkillNameList(CollectionUtil.join(nameList, ",")); + } + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingSkillServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingSkillServiceImpl.java new file mode 100644 index 0000000..94ae31e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingSkillServiceImpl.java @@ -0,0 +1,354 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.FtbCultivateLabelMapper; +import jnpf.cultivate.mapper.TeachingSkillMapper; +import jnpf.cultivate.service.TeachingSkillService; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.enums.cultivate.BusinessScenarioTypeEnum; +import jnpf.model.cultivate.dto.teaching.TeachingSkillAddDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillPageDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillSortDto; +import jnpf.model.cultivate.dto.teaching.TeachingSkillUpdateDto; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.v2.exam.vo.ImportObjectVo; +import jnpf.model.cultivate.v2.exam.vo.SkillVo; +import jnpf.model.cultivate.vo.teaching.SkillInfoVo; +import jnpf.model.cultivate.vo.teaching.TeachingSkillVo; +import jnpf.util.*; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class TeachingSkillServiceImpl extends ServiceImpl implements TeachingSkillService { + @Autowired + private RedissonClient redissonClient; + @Resource + private FtbCultivateLabelMapper cultivateLabelMapper; + @Resource + private FtbCultivateLabelService cultivateLabelService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean addData(TeachingSkillAddDto addDto) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(TeachingSkill::getName, addDto.getName()); + queryWrapper.last("limit 1"); + TeachingSkill teachingSkill = baseMapper.selectOne(queryWrapper); + if (Objects.nonNull(teachingSkill) && teachingSkill.getDeleteMark().equals(0)) { + throw new RuntimeException("该技能点已存在!"); + } + //判断技能点数量是否达到上限 + LambdaQueryWrapper infoLambdaQueryWrapper = Wrappers.lambdaQuery(); + infoLambdaQueryWrapper.eq(TeachingSkill::getDeleteMark, 0); + if (baseMapper.selectCount(infoLambdaQueryWrapper) >= 100) { + throw new RuntimeException("技能点数量已达到上限,请删除后再添加"); + } + //使用分布式锁控制 + RLock lock = redissonClient.getLock(UserProvider.getUser().getTenantId() + BusinessScenarioTypeEnum.JND_PZ); + try { + if (!lock.tryLock(30, TimeUnit.SECONDS)) { + throw new RuntimeException("无法获取分布式锁,请稍后再试"); + } + //方法抽离,处理数据 + processingData(addDto, teachingSkill); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // 恢复中断状态 + throw new RuntimeException(e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return Boolean.TRUE; + } + + @Transactional(rollbackFor = Exception.class) + public void processingData(TeachingSkillAddDto addDto, TeachingSkill teachingSkill) { + //设置最大排序值 + LambdaQueryWrapper infoLambdaQueryWrapper = Wrappers.lambdaQuery(); + infoLambdaQueryWrapper.eq(TeachingSkill::getDeleteMark, 0); + infoLambdaQueryWrapper.last("limit 1"); + infoLambdaQueryWrapper.orderByDesc(TeachingSkill::getSort); + TeachingSkill teachingSkill1Max = baseMapper.selectOne(infoLambdaQueryWrapper); + Integer sortIndex = Objects.nonNull(teachingSkill1Max) ? teachingSkill1Max.getSort() + 1 : 1; + TeachingSkill saveSkill = Objects.nonNull(teachingSkill) ? teachingSkill : new TeachingSkill(); + saveSkill.setId(Objects.nonNull(teachingSkill) ? teachingSkill.getId() : RandomUtil.uuId()); + saveSkill.setCategoryId(addDto.getCategoryId()); + saveSkill.setSort(sortIndex); + saveSkill.setName(addDto.getName()); + saveSkill.setDescription(addDto.getDesc()); + saveSkill.setDeleteMark(0); + this.saveOrUpdate(saveSkill); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteData(String id) { + TeachingSkill teachingSkill = baseMapper.selectById(id); + Assert.notNull(teachingSkill, "未查询到该记录"); + teachingSkill.setDeleteMark(1); + baseMapper.updateById(teachingSkill); + //使用分布式锁控制数据重新排序 + RLock lock = redissonClient.getLock(UserProvider.getUser().getTenantId() + BusinessScenarioTypeEnum.JND_PZ); + try { + if (!lock.tryLock(30, TimeUnit.SECONDS)) { + throw new RuntimeException("无法获取分布式锁,请稍后再试"); + } + LambdaQueryWrapper updateWrapper = Wrappers.lambdaQuery(); + updateWrapper.eq(TeachingSkill::getDeleteMark, 0); + updateWrapper.orderByAsc(TeachingSkill::getSort); + List teachingSkillList = baseMapper.selectList(updateWrapper); + Optional.ofNullable(teachingSkillList).ifPresent(list -> { + AtomicInteger rank = new AtomicInteger(1); + teachingSkillList.forEach(vo -> vo.setSort(rank.getAndIncrement())); + this.updateBatchById(teachingSkillList); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return Boolean.TRUE; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateData(TeachingSkillUpdateDto updateDto) { + TeachingSkill teachingSkill = baseMapper.selectById(updateDto.getId()); + if (teachingSkill != null) { + teachingSkill.setName(updateDto.getName()); + teachingSkill.setCategoryId(updateDto.getCategoryId()); + teachingSkill.setDescription(updateDto.getDesc()); + teachingSkill.setLastModifyTime(new Date()); + teachingSkill.setLastModifyUserId(UserProvider.getUser().getUserId()); + } + return this.updateById(teachingSkill); + } + + @Override + public TeachingSkillVo getData(String id) { + TeachingSkill teachingSkill = baseMapper.selectById(id); + // 查询分类信息 + FtbCultivateLabel label = cultivateLabelMapper.selectById(teachingSkill.getCategoryId()); + return TeachingSkillVo.builder() + .id(teachingSkill.getId()) + .name(teachingSkill.getName()) + .categoryId(teachingSkill.getCategoryId()) + .categoryName(null == label ? "" : label.getName()) + .desc(teachingSkill.getDescription()) + .sort(teachingSkill.getSort()) + .build(); + } + + @Override + public PageInfo pageList(TeachingSkillPageDto pageDto) { + Page page = Page.of(pageDto.getCurrentPage(), pageDto.getPageSize()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (StringUtil.isNotEmpty(pageDto.getCategoryId())) { + if ("-1".equals(pageDto.getCategoryId())) { + queryWrapper.and(w -> w.isNull(TeachingSkill::getCategoryId) + .or() + .eq(TeachingSkill::getCategoryId, "")); + } else { + queryWrapper.eq(TeachingSkill::getCategoryId, pageDto.getCategoryId()); + } + } + queryWrapper + .like(StringUtils.isNotBlank(pageDto.getName()), TeachingSkill::getName, pageDto.getName()) + .eq(TeachingSkill::getDeleteMark, 0) + .orderByAsc(TeachingSkill::getSort); + Page queryPage = baseMapper.selectPage(page, queryWrapper); + List records = queryPage.getRecords(); + List categoryIds = records.stream().map(TeachingSkill::getCategoryId).collect(Collectors.toList()); + List categoryList; + if (categoryIds.isEmpty()) { + categoryList = List.of(); + } else { + categoryList = cultivateLabelMapper.selectBatchIds(categoryIds); + } + Map categoryMap = categoryList.stream().collect(Collectors.toMap(FtbCultivateLabel::getId, Function.identity())); + List dataList = records.stream().map(item -> { + TeachingSkillVo teachingSkillVo = JsonUtil.getJsonToBean(item, TeachingSkillVo.class); + teachingSkillVo.setDesc(item.getDescription()); + FtbCultivateLabel label = categoryMap.get(item.getCategoryId()); + if (null != label) { + teachingSkillVo.setCategoryName(label.getName()); + } + return teachingSkillVo; + }).collect(Collectors.toList()); + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(dataList); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public List selectDataList() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(TeachingSkill::getDeleteMark, 0) + .orderByAsc(TeachingSkill::getSort); + List skillList = this.list(queryWrapper); + if (CollUtil.isNotEmpty(skillList)) { + return skillList.stream().map(skill -> TeachingSkillVo.builder() + .id(skill.getId()) + .name(skill.getName()) + .desc(skill.getDescription()) + .sort(skill.getSort()) + .creatorTime(skill.getCreatorTime()) + .build()).collect(Collectors.toList()); + } + return CollUtil.newArrayList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean sort(TeachingSkillSortDto sortDto) { + List ids = sortDto.getDataList().stream().map(TeachingSkillVo::getId).distinct().collect(Collectors.toList()); + Map teachingSkillVoMap = sortDto.getDataList().stream().collect(Collectors.toMap(TeachingSkillVo::getId, a -> a, (k1, k2) -> k1)); + //使用分布式锁控制 + RLock lock = redissonClient.getLock(UserProvider.getUser().getTenantId() + BusinessScenarioTypeEnum.JND_PZ); + try { + if (!lock.tryLock(30, TimeUnit.SECONDS)) { + throw new RuntimeException("无法获取分布式锁,请稍后再试"); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(TeachingSkill::getId, ids); + queryWrapper.eq(TeachingSkill::getDeleteMark, 0); + List teachingSkillList = baseMapper.selectList(queryWrapper); + teachingSkillList.forEach(item -> { + Optional.ofNullable(teachingSkillVoMap.get(item.getId())).ifPresent(dishAppManagementInfoVo -> { + item.setLastModifyTime(!item.getSort().equals(dishAppManagementInfoVo.getSort()) ? new Date() : item.getLastModifyTime()); + item.setSort(dishAppManagementInfoVo.getSort()); + }); + }); + this.updateBatchById(teachingSkillList); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return Boolean.TRUE; + } + + @Override + public List querySkillIds(String categoryId) { + + List skillIds; + if (StringUtil.isNotEmpty(categoryId)) { + LambdaQueryWrapper skillQuery = new LambdaQueryWrapper() + .eq(!categoryId.equals("-1"), TeachingSkill::getCategoryId, categoryId) + .isNull(categoryId.equals("-1"), TeachingSkill::getCategoryId) + .eq(TeachingSkill::getDeleteMark, ConstantUtil.NUM_FALSE); + List skillList = this.list(skillQuery); + skillIds = skillList.stream().map(TeachingSkill::getId).collect(Collectors.toList()); + if (skillIds.isEmpty()) { + skillIds = null; + } + } else { + // 不过滤skillIds + skillIds = new ArrayList<>(); + } + return skillIds; + } + + @Override + public List getDistinctCountBatch(List list) { + + return baseMapper.getDistinctCountBatch(list); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void saveImportData(List list) { + + if (list.isEmpty()) { + return; + } + // 查询分类信息 + List categoryNameList = list.stream().map(ImportObjectVo::getCategoryName).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); + Map labelMap = cultivateLabelService.getLabelMapByName(categoryNameList); + // 查询最大sort + LambdaQueryWrapper sortQuery = new LambdaQueryWrapper() + .eq(TeachingSkill::getDeleteMark, ConstantUtil.NUM_FALSE) + .orderByDesc(TeachingSkill::getSort) + .last("LIMIT 1"); + TeachingSkill sk = baseMapper.selectOne(sortQuery); + int sort = null == sk ? 1 : sk.getSort() + 1; + List saveList = new ArrayList<>(); + for (ImportObjectVo vo : list) { + FtbCultivateLabel label = labelMap.get(vo.getCategoryName()); + TeachingSkill skill = new TeachingSkill(FtbUtil.getId(), null == label ? null : label.getId(), vo.getSkillName(), vo.getDescription(), sort++, UserProvider.getLoginUserId()); + saveList.add(skill); + } + this.saveBatch(saveList); + } + + @Override + public List getDownSkillList(String skillId, String categoryId) { + + return baseMapper.getDownSkillList(skillId, categoryId); + } + + @Override + public List getAllSkillList() { + + List allList = baseMapper.getAllSkillList(); + if (allList.isEmpty()) { + return List.of(); + } + List categoryList = allList.stream().filter(v -> v.getPId().equals("0")).collect(Collectors.toList()); + allList.removeAll(categoryList); + if (allList.isEmpty()) { + return List.of(); + } + Map> map = allList.stream().collect(Collectors.groupingBy(SkillInfoVo::getPId)); + List removeIds = new ArrayList<>(); + categoryList.forEach(category -> { + List childList = map.get(category.getId()); + if (childList == null || childList.isEmpty()) { + removeIds.add(category.getId()); + } else { + childList.sort(Comparator.comparing(SkillInfoVo::getSort)); + category.getChildren().addAll(childList); + } + }); + if (!removeIds.isEmpty()) { + categoryList.removeIf(v -> removeIds.contains(v.getId())); + } + categoryList.sort(Comparator.comparing(SkillInfoVo::getSort)); + return categoryList; + } + + @Override + public List getCategoryHasChild(List categoryIds) { + + return baseMapper.getCategoryHasChild(categoryIds); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingStudentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingStudentServiceImpl.java new file mode 100644 index 0000000..6afaecb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/TeachingStudentServiceImpl.java @@ -0,0 +1,11 @@ +package jnpf.cultivate.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.TeachingStudentMapper; +import jnpf.cultivate.service.TeachingStudentService; +import jnpf.entity.cultivate.TeachingStudent; +import org.springframework.stereotype.Service; + +@Service +public class TeachingStudentServiceImpl extends ServiceImpl implements TeachingStudentService { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/V2TeachingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/V2TeachingServiceImpl.java new file mode 100644 index 0000000..2bf340f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/service/impl/V2TeachingServiceImpl.java @@ -0,0 +1,146 @@ +package jnpf.cultivate.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.CultivateUserViewMapper; +import jnpf.cultivate.mapper.TeachingApproveMapper; +import jnpf.cultivate.service.CultivateFileService; +import jnpf.cultivate.service.TeachingRecordService; +import jnpf.cultivate.service.V2TeachingService; +import jnpf.cultivate.utils.CultivateImUtil; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.exception.HandleException; +import jnpf.model.cultivate.dto.teaching.RecordQueryDto; +import jnpf.model.cultivate.dto.teaching.TeachingApproveDto; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.teaching.CultivateFile; +import jnpf.model.cultivate.po.teaching.CultivateUserView; +import jnpf.model.cultivate.po.teaching.TeachingApprove; +import jnpf.model.cultivate.v2.teaching.vo.StoreTeachingVo; +import jnpf.model.cultivate.v2.teaching.vo.TeachingDetailVo; +import jnpf.model.cultivate.v2.teaching.vo.V2TeachingRecordVo; +import jnpf.permission.StoreApi; +import jnpf.permission.dto.store.QueryStoreListDTO; +import jnpf.permission.vo.store.StoreBaseListInfo; +import jnpf.util.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 带教/练习服务实现[v2] + * + * @author yanwenfu + * @create 2026-03-02 + */ +@Service +public class V2TeachingServiceImpl implements V2TeachingService { + + @Resource + private TeachingRecordService teachingRecordService; + @Resource + private StoreApi storeApi; + @Resource + private TeachingApproveMapper teachingApproveMapper; + @Resource + private CultivateUserViewMapper cultivateUserViewMapper; + @Resource + private CultivateFileService cultivateFileService; + @Resource + private CultivateImUtil cultivateImUtil; + + @Override + public List getPage(RecordQueryDto queryDto) { + + // 根据storeIds查询每个门店下的带教数量 + List list = teachingRecordService.getStoreRecordCount(queryDto); + // 查询store信息 + ActionResult> storeResult = storeApi.listInfo(new QueryStoreListDTO(queryDto.getStoreIdList(), null)); + List storeList; + if (null != storeResult && null != storeResult.getData()) { + storeList = storeResult.getData(); + } else { + storeList = new ArrayList<>(); + } + if (StringUtil.isEmpty(queryDto.getStoreId())) { + // 未传门店id, 默认展开第一个 + list.stream().max(Comparator.comparing(StoreTeachingVo::getRecordCount)).ifPresent(vo -> queryDto.setStoreId(vo.getStoreId())); + } + Map storeMap = storeList.stream().collect(Collectors.toMap(StoreBaseListInfo::getId, Function.identity())); + list.forEach(v -> { + StoreBaseListInfo store = storeMap.get(v.getStoreId()); + v.setStoreName(null == store ? "" : store.getStoreName()); + }); + // 根据storeId查询带教分页数据 + PageInfo page = teachingRecordService.getStoreRecordPage(queryDto); + if (page.getList() == null) { + page.setList(new ArrayList<>()); + } + List collect = page.getList().stream().map(V2TeachingRecordVo::getId).collect(Collectors.toList()); + // 查询是否浏览 + Set viewedSet = teachingRecordService.getViewedSet(collect, UserProvider.getLoginUserId()); + page.getList().forEach(v -> { + if (viewedSet.contains(v.getId())) { + v.setViewed(true); + } + }); + list.stream() + .filter(v -> v.getStoreId().equals(queryDto.getStoreId())) + .findFirst() + .ifPresent(v -> { + PageListVO vo = new PageListVO<>(); + vo.setList(page.getList()); + vo.setPagination(FtbUtil.getPagination(page)); + v.setRecordPage(vo); + }); + return list; + } + + @Override + public TeachingDetailVo getTeachingDetail(String bizId) { + + // 查询是否有已阅记录, 无则新增 + CultivateUserView cultivateUserView = cultivateUserViewMapper.selectOne(new LambdaQueryWrapper() + .eq(CultivateUserView::getBizId, bizId) + .eq(CultivateUserView::getUserId, UserProvider.getLoginUserId())); + if (null == cultivateUserView) { + cultivateUserView = new CultivateUserView(FtbUtil.getId(), bizId, UserProvider.getLoginUserId()); + cultivateUserViewMapper.insert(cultivateUserView); + } + return teachingRecordService.getTeachingDetailById(bizId); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void approveTeachingRecord(TeachingApproveDto approveDto) throws HandleException { + + TeachingApprove teachingApprove = teachingApproveMapper.selectById(approveDto.getId()); + if (null == teachingApprove) { + throw new HandleException("未找到审批记录"); + } + teachingApprove.setApproveStatus(approveDto.getApproveStatus()); + teachingApprove.setRemark(approveDto.getRemark()); + teachingApprove.setNeedFeedback(approveDto.getNeedFeedback()); + teachingApprove.setLastModifyUserId(UserProvider.getLoginUserId()); + teachingApprove.setLastModifyTime(new Date()); + // 新增附件信息 + if (null != approveDto.getFileList() && !approveDto.getFileList().isEmpty()) { + List fileList = teachingRecordService.changeToFileList(teachingApprove.getId(), approveDto.getFileList(), UserProvider.getLoginUserId(), FileEventDTO.FileType.TEACHING_APPROVE); + cultivateFileService.saveBatch(fileList); + } + teachingApproveMapper.updateById(teachingApprove); + if (teachingApprove.getApproveStatus().equals(ConstantUtil.PASS_FALSE) && null != approveDto.getNeedFeedback() && approveDto.getNeedFeedback().equals(ConstantUtil.NUM_TRUE)) { + // 需要反馈带教员 发送IM + TeachingRecord teachingRecord = teachingRecordService.getById(teachingApprove.getTeachId()); + cultivateImUtil.sendCultivateApproveMsg(List.of(teachingRecord.getUserId()), UserProvider.getUser().getTenantId(), + "带教审核结果提醒\n您有一条带教记录未通过,请点击查看详情进行查看!", teachingApprove.getTeachId()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/AsyncExamQuestionUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/AsyncExamQuestionUtils.java new file mode 100644 index 0000000..4aae408 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/AsyncExamQuestionUtils.java @@ -0,0 +1,130 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.google.common.collect.ImmutableMap; +import jnpf.base.UserInfo; +import jnpf.cultivate.service.FtbCultivateTestPaperQuestionService; +import jnpf.cultivate.service.FtbCultivateTestPaperService; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.enums.CourseEnums; +import jnpf.util.Constants; +import jnpf.util.UserProvider; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class AsyncExamQuestionUtils { + /** + * 试卷题目关联 + */ + @Lazy + @Autowired + private FtbCultivateTestPaperQuestionService paperQuestionService; + + /** + * 试卷 + */ + @Autowired + @Lazy + private FtbCultivateTestPaperService testPaperService; + + @Lazy + @Autowired + private V2CultivateExamService v2CultivateExamService; + + /** + * 异步处理试卷试题 + * @param tenantCode 租户ID + * @param questionIds 题目集合 + * @param headers + */ + @Async + public void asyncPaperQuestion(String tenantCode, List questionIds, Map headers) { + + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + + //查询题目收录的试卷列表 + QueryWrapper testPaperQuestionQueryWrapper = new QueryWrapper<>(); + testPaperQuestionQueryWrapper.lambda() + .in(FtbCultivateTestPaperQuestion::getQuestionId, questionIds) + .eq(FtbCultivateTestPaperQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List testPaperQuestionList = paperQuestionService.list(testPaperQuestionQueryWrapper); + if (CollectionUtil.isEmpty(testPaperQuestionList)) { + return; + } + //按照试卷id 把题目分组 + Map> map = new HashMap<>();//试卷ID ->题目列表 + testPaperQuestionList.forEach(paperQuestion -> { + List questionList = map.get(paperQuestion.getPaperId()); + if (CollectionUtil.isEmpty(questionList)) { + questionList =new ArrayList<>(); + } + questionList.add(paperQuestion.getQuestionId()); + map.put(paperQuestion.getPaperId(), questionList); + }); + for (Map.Entry> entry : map.entrySet()) { + String paperId = entry.getKey(); + List values = entry.getValue(); + testPaperService.batchDeleteQuestionFormPaper(paperId, values); + } + } catch (Exception e) { + log.error("异步处理试卷试题:tenantCode={},questionId={}", tenantCode, questionIds); + } finally { + FeignHolder.clear(); + } + } + + /** + * 异步处理考试题目 + * @param tenantCode 租户ID + * @param questionIds 题目集合 + * @param headers 请求头 + */ + @Async + public void asyncExamQuestion(String tenantCode, List questionIds, Map headers) { + + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + v2CultivateExamService.questionChangeDeal(questionIds); + } catch (Exception e) { + log.error("异步处理考试题目: tenantCode = {}, questionId = {}", tenantCode, questionIds); + } finally { + FeignHolder.clear(); + } + } + + /** + * 获取租户ID + * @return 租户ID + */ + public String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } + + public Map getHeadersForLogin() { + UserInfo userInfo = UserProvider.getUser(); + String token = userInfo.getToken(); + if (StringUtils.isEmpty(token)) { + token = ""; + } + return ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateDateTimeUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateDateTimeUtils.java new file mode 100644 index 0000000..c9ae69e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateDateTimeUtils.java @@ -0,0 +1,87 @@ +package jnpf.cultivate.utils; + +import jnpf.model.cultivate.vo.learn.info.TimeDifference; + +import java.text.DecimalFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +/** + * @Author: peng.hao + * @create: 2024/9/11:18:08 + */ +public class CultivateDateTimeUtils { + + /** + * 计算两个时间之间的剩余值。 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param type (0:天,1:小时) + * @return 剩余值 + */ + public static String calculateRemainingValue(Date startTime, Date endTime, Integer type) { + if (startTime == null || endTime == null) { + throw new IllegalArgumentException("Start time and end time cannot be null"); + } + LocalDateTime start = getLocalDateTime(startTime); + LocalDateTime end = getLocalDateTime(endTime); + if (start.isAfter(end)) { + return ""; + } + if (type == 0){ + long remainingDays = ChronoUnit.DAYS.between(start, end); + if (remainingDays >= 1) { + return String.valueOf(remainingDays); + } else { + // 计算剩余的毫秒数 + long between = ChronoUnit.HOURS.between(start, end); + double remainingHours = between % 24; // 计算剩余小时数 + DecimalFormat decimalFormat = new DecimalFormat("#.##"); + return decimalFormat.format(remainingHours / 24); + } + }else { + return String.valueOf(ChronoUnit.HOURS.between(start, end)); + } + } + /** + * 计算两个日期之间的差值,返回时分秒。 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 返回一个包含小时数、分钟数和秒数的对象 + */ + public static TimeDifference calculateDifference(Date startDate, Date endDate) { + long diffInMillies = endDate.getTime() - startDate.getTime(); + + long hours = diffInMillies / (60 * 60 * 1000); + diffInMillies %= (60 * 60 * 1000); + + long minutes = diffInMillies / (60 * 1000); + diffInMillies %= (60 * 1000); + + long seconds = diffInMillies / 1000; + + return new TimeDifference(hours, minutes, seconds); + } + + /** + * 转换时间 + * @param startTime + * @return + */ + private static LocalDateTime getLocalDateTime(Date startTime) { + // 获取 Instant 对象 + Instant instant = startTime.toInstant(); + // 指定时区,这里假设为中国标准时间 CST + ZoneId zoneId = ZoneId.systemDefault(); // 或者使用 ZoneId.of("Asia/Shanghai"); + // 将 Instant 与时区结合,创建 ZonedDateTime + ZonedDateTime zonedDateTime = instant.atZone(zoneId); + // 提取 LocalDateTime + return zonedDateTime.toLocalDateTime(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateIdentifyIMUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateIdentifyIMUtils.java new file mode 100644 index 0000000..b524ec5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateIdentifyIMUtils.java @@ -0,0 +1,95 @@ +package jnpf.cultivate.utils; + +import cn.hutool.json.JSONUtil; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.LinkedList; +import java.util.List; + +/** + * 培训鉴定提醒 + */ +@Component +@Slf4j +public class CultivateIdentifyIMUtils { + + @Autowired + private ImRobotApi imRobotApi; + private static final String NOTICE_APP_NAME = "培训"; + private static final String TITLE = "鉴定通知"; + private static final String NOTICE_APP_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/66457c71e4b05b749193397b.png"; + private static final String NOTICE_BUTTON_NAME = "点击查看详情"; + private static final String NOTICE_BUTTON_LINK = "/pages/qualificationManagement/authenticateInfo/index?menuId=461212890154671237&fullName=我的鉴定&id=";//跳转页面链接 + + private static final String CULTIVATE_MP_ID = "__UNI__74A05F6";//__UNI__74A05F6 培训管理 + + /** + * 发送消息 + * + * @param apply 鉴定信息 + * @param tenantId 租户ID + * @return + */ + public Boolean sendMsg(CultivateIdentifyApply apply, String tenantId) { + + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + //固定图片 + robotNoticeDataForm.setLogo(NOTICE_APP_LOGO); + robotNoticeDataForm.setAppName(NOTICE_APP_NAME); + + robotNoticeDataForm.setTitle(TITLE); + robotNoticeDataForm.setContent(buildNoticeContent(apply)); + + robotNoticeDataForm.setJumpUrlList(buildBtnUrl(apply)); + form.setRobotNoticeDataForm(robotNoticeDataForm); + form.setToUserIds(List.of(apply.getBeIdentifyUserId())); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + log.info("CultivateIdentifyIMUtils send msg ={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("CultivateIdentifyIMUtils,send msg req={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + return true; + } + + /** + * 构建消息内容 + * + * @param taskName 任务名称 + * @return + */ + private String buildNoticeContent(CultivateIdentifyApply apply) { + return "您有一条新的鉴定结果,请查看!"; + } + + /** + * 构建按钮链接 + * + * @param taskId 任务ID + * @return + */ + private LinkedList buildBtnUrl(CultivateIdentifyApply apply) { + LinkedList btn = new LinkedList<>(); + JumpUrlListModel urlModel = new JumpUrlListModel(); + urlModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + urlModel.setButtonName(NOTICE_BUTTON_NAME); + urlModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + urlModel.setType(1);//1-跳转链接 2-表单提交 + urlModel.setUrl(NOTICE_BUTTON_LINK + apply.getId()); + urlModel.setMpId(CULTIVATE_MP_ID); + btn.add(urlModel); + return btn; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateImUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateImUtil.java new file mode 100644 index 0000000..9723506 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateImUtil.java @@ -0,0 +1,139 @@ +package jnpf.cultivate.utils; + +import cn.hutool.json.JSONUtil; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.LinkedList; +import java.util.List; + +/** + * 培训im通知工具 + * @Author: peng.hao + * @create: 2024/7/24:15:50 + */ +@Component +@Slf4j +@SuppressWarnings("Duplicates") +public class CultivateImUtil { + + @Autowired + private ImRobotApi imRobotApi; + + // app名称 + private static final String APP_NAME = "培训"; + + private static final String CERTIFICATE_TITLE = "培训管理"; + // logo + public static final String LOGO_URL = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/66457c71e4b05b749193397b.png"; + + // 我的证书 + public static final String BASE_URL = "/pages/myCert/index?menuId=461212890154671237&fullName=我的证书"; + + public static final String BTN_NAME_ZS = "查看证书"; + + public static final String URL_CULTIVATE_APPROVE = "/pages/teaching/myTeaching/teachingDetails?bizId=%s"; + + public static final String BTN_NAME_DETAIL = "查看详情"; + + private static final String CULTIVATE_MP_ID = "__UNI__74A05F6";//__UNI__74A05F6 培训管理 + + /** + * 发送消息 + * + * @param userIds 发送消息 userIds + * @param tenantId 租户id + */ + public void sendMessage(List userIds, String tenantId,String contentMessage) { + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + form.setToUserIds(userIds); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(LOGO_URL);//固定图片 + robotNoticeDataForm.setAppName(APP_NAME); + robotNoticeDataForm.setTitle(CERTIFICATE_TITLE); + robotNoticeDataForm.setContent(contentMessage); + form.setRobotNoticeDataForm(robotNoticeDataForm); + // 跳转链接 + JumpUrlListModel jumpUrlListModel = createJumpUrlListModel(BASE_URL, BTN_NAME_ZS); + LinkedList objects = new LinkedList<>(); + objects.add(jumpUrlListModel); + robotNoticeDataForm.setJumpUrlList(objects); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("岗位学习证书发送消息: {}, {}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + } + + /** + * 发送培训带教审核IM消息 + * + * @param userIds 发送消息 userIds + * @param tenantId 租户id + */ + public void sendCultivateApproveMsg(List userIds, String tenantId, String contentMessage, String teachId) { + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + form.setToUserIds(userIds); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(LOGO_URL); + robotNoticeDataForm.setAppName(APP_NAME); + robotNoticeDataForm.setTitle(CERTIFICATE_TITLE); + robotNoticeDataForm.setContent(contentMessage); + form.setRobotNoticeDataForm(robotNoticeDataForm); + // 跳转链接 + JumpUrlListModel jumpUrlListModel = createJumpUrlListModel(String.format(URL_CULTIVATE_APPROVE, teachId), BTN_NAME_DETAIL); + LinkedList objects = new LinkedList<>(); + objects.add(jumpUrlListModel); + robotNoticeDataForm.setJumpUrlList(objects); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("发送消息失败 -> 带教审核结果提醒: {}, {}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + } + + /** + * 发送任务中止消息 + * + * @param userIds 发送消息 userIds + * @param tenantId 租户id + */ + public void sendTaskAbortMessage(List userIds, String tenantId, String contentMessage) { + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setMessageAttributionRobotMpId("__UNI__74A05F6"); + form.setTenantId(tenantId); + form.setToUserIds(userIds); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(LOGO_URL); + robotNoticeDataForm.setAppName(APP_NAME); + robotNoticeDataForm.setTitle(CERTIFICATE_TITLE); + robotNoticeDataForm.setContent(contentMessage); + // 无跳转链接 + form.setRobotNoticeDataForm(robotNoticeDataForm); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("培训任务中止发送消息", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + } + + private JumpUrlListModel createJumpUrlListModel(String url, String btnName) { + JumpUrlListModel jumpUrlListModel = new JumpUrlListModel(); + jumpUrlListModel.setType(1); + jumpUrlListModel.setUrl(url); + jumpUrlListModel.setButtonName(btnName); + jumpUrlListModel.setMpId(CULTIVATE_MP_ID); + jumpUrlListModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jumpUrlListModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + return jumpUrlListModel; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnTaskIMUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnTaskIMUtils.java new file mode 100644 index 0000000..3d23716 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnTaskIMUtils.java @@ -0,0 +1,99 @@ +package jnpf.cultivate.utils; + +import cn.hutool.json.JSONUtil; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import jnpf.model.cultivate.dto.learn.NeedAlertUserDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 任务提醒 + */ +@Component +@Slf4j +public class CultivateLearnTaskIMUtils { + + @Autowired + private ImRobotApi imRobotApi; + private static final String NOTICE_APP_NAME = "培训"; + private static final String CERTIFICATE_TITLE = "培训管理"; + private static final String NOTICE_APP_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/66457c71e4b05b749193397b.png"; + private static final String NOTICE_BUTTON_NAME = "去完成"; + private static final String NOTICE_BUTTON_LINK = "/pages/myTask/index?menuId=461212890154671237&fullName=我的培训任务";//跳转页面链接 + + private static final String CULTIVATE_MP_ID = "__UNI__74A05F6";//__UNI__74A05F6 培训管理 + /** + * 发送消息 + * + * @param alertUserList 提醒人员列表 + * @param tenantId 租户ID + * @return + */ + public Boolean sendMsg(List alertUserList, String tenantId) { + Map> batchMaps = alertUserList.stream() + .collect(Collectors.groupingBy(a -> a.getTaskId() + "#" + a.getTaskName())); + batchMaps.forEach((k, v) -> { + String[] split = k.split("#"); + List userIds = v.stream().map(NeedAlertUserDto::getUserId).collect(Collectors.toList()); + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + //固定图片 + robotNoticeDataForm.setLogo(NOTICE_APP_LOGO); + robotNoticeDataForm.setAppName(NOTICE_APP_NAME); + + robotNoticeDataForm.setTitle(CERTIFICATE_TITLE); + robotNoticeDataForm.setContent(buildNoticeContent(split[1])); + + robotNoticeDataForm.setJumpUrlList(buildBtnUrl(split[0])); + form.setRobotNoticeDataForm(robotNoticeDataForm); + form.setToUserIds(userIds); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + log.info("NeedAlertUserDto send msg ={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("NeedAlertUserDto,send msg req={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + }); + return true; + } + + /** + * 构建消息内容 + * @param taskName 任务名称 + * @return + */ + private String buildNoticeContent(String taskName) { + return "您有一项新的任务,可点击下方按钮前往\"培训任务\"进行学习!任务名称:" + taskName; + } + + /** + * 构建按钮链接 + * @param taskId 任务ID + * @return + */ + private LinkedList buildBtnUrl(String taskId) { + LinkedList btn = new LinkedList<>(); + JumpUrlListModel urlModel = new JumpUrlListModel(); + urlModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + urlModel.setButtonName(NOTICE_BUTTON_NAME); + urlModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + urlModel.setType(1);//1-跳转链接 2-表单提交 + urlModel.setUrl(NOTICE_BUTTON_LINK); + urlModel.setMpId(CULTIVATE_MP_ID); + btn.add(urlModel); + return btn; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnUtils.java new file mode 100644 index 0000000..71afb6e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateLearnUtils.java @@ -0,0 +1,1155 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskAssignmentMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.v2.service.FtbCultivatePromotionSettingService; +import jnpf.cultivate.v2.service.FtbCultivatePromotionUserService; +import jnpf.model.cultivate.dto.learn.NeedAlertUserDto; +import jnpf.model.cultivate.dto.learn.NeedPerDayAlertDto; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnCategories; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionSetting; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionUser; +import jnpf.model.cultivate.resp.TaskRelationCertificateVo; +import jnpf.model.cultivate.resp.TaskRelationExamVo; +import jnpf.model.cultivate.resp.TaskRelationIdentificationVo; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoCountListVO; +import jnpf.model.cultivate.vo.learn.FtbCultivateLearnTaskInfoListVO; +import jnpf.model.cultivate.vo.learn.FtbExportCultivateLearnTaskInfoCountListVO; +import jnpf.model.cultivate.vo.learn.FtbExportCultivateLearnTaskInfoListVO; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJobTenureDTO; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJoeInfo; +import jnpf.model.personnels.vo.roster.FtbPersonnlesJobTenureVO; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonneApi; +import jnpf.personnels.service.FtbPersonnelsMetaDataService; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalTime; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @Title: CultivateLearnUtils + * @Author: xuguilin + * @create: 2024/3/21 11:25 + */ +@Component +@Slf4j +public class CultivateLearnUtils { + + @Autowired + private FtbCultivateLearnCategoriesService cultivateLearnCategoriesService; + + + @Autowired + private FtbCultivateLearnTaskAssignmentService taskAssignmentService; + + @Autowired + private FtbCultivateLearnTaskCourseService taskCourseService; + + /** + * 考试用户服务 + */ + @Autowired + private FtbCultivateExamUserService examUserService; + + + @Autowired + private FtbCultivateLearnTaskExamService taskExamService; + + @Autowired + private FtbCultivateLearnTaskIdentificationService taskIdentificationService; + + @Autowired + private FtbCultivateLearnTaskCertificateService taskCertificateService; + + @Autowired + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskInfoMapper; + + @Autowired + private FtbPersonnelsMetaDataService metaDataService; + + @Autowired + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + private CultivateLearnTaskIMUtils cultivateLearnTaskIMUtils; + + @Autowired + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Autowired + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbCultivatePromotionSettingService ftbCultivatePromotionSettingService; + + @Autowired + private FtbCultivatePromotionUserService ftbCultivatePromotionUserService; + + @Autowired + private FtbPersonneApi ftbPersonneApi; + + /** + * 填充任务列表显示的统计数据 + * + * @param list 任务列表 + */ + public void fillTaskInfoList(List list) { + if (CollUtil.isEmpty(list)) { + return; + } + Set cateIdsSet = new HashSet<>();//分类id集合 + Set userIdsSet = new HashSet<>();//创建人id集合 + Set taskIdsSet = new HashSet<>();//任务id集合 + //填充任务分类名称 + for (FtbCultivateLearnTaskInfoListVO vo : list) { + cateIdsSet.add(vo.getTaskClass()); + userIdsSet.add(vo.getCreatorUserId()); + taskIdsSet.add(vo.getId()); + } + + Map categoriesMap = cultivateLearnCategoriesService.queryCateByIds(new ArrayList<>(cateIdsSet)); + Map userNameMap = userApiV2Util.getUserNameAndCopyForUserIds(new ArrayList<>(userIdsSet)); + + Map allTaskNumMap = taskAssignmentService.groupCountNum(new ArrayList<>(taskIdsSet), 0);//所有任务数量 + Map completeTaskNumMap = taskAssignmentService.groupCountNum(new ArrayList<>(taskIdsSet), 1); //已完成任务数量 + Date now = new Date(); + for (FtbCultivateLearnTaskInfoListVO vo : list) { + FtbCultivateLearnCategories cate = categoriesMap.get(vo.getTaskClass()); + if (cate != null) { + vo.setTaskClassName(cate.getName()); + } + UserEntity roster = userNameMap.get(vo.getCreatorUserId()); + if (roster != null) { + vo.setCreatorUserName(roster.getRealName()); + } + vo.setTotleNum(Objects.requireNonNullElse(allTaskNumMap.get(vo.getId()), 0)); + vo.setCompleteTotleNum(Objects.requireNonNullElse(completeTaskNumMap.get(vo.getId()), 0)); + //计算任务剩余天数 0,长期,1限时 + if (vo.getTaskType().equals(1)) { + if (now.before(vo.getTimeLimitStartTime())) { //未开始 + Long diff = DateUtil.betweenDay(vo.getTimeLimitStartTime(), vo.getTimeLimitStartTime(), false); + vo.setSurplusDay(diff); + } else if (now.before(vo.getTimeLimitEndTime())) { //进行中 + Long diff = DateUtil.betweenDay(vo.getTimeLimitStartTime(), now, false); + vo.setSurplusDay(diff); + } else { //结束 + vo.setSurplusDay(0L); + } + } + } + + } + + public static List convertFtbExportCultivateLearnTaskInfoListVO(List list) { + List retList = new ArrayList<>(); + if (CollUtil.isEmpty(list)) { + return retList; + } + for (FtbCultivateLearnTaskInfoListVO vo : list) { + FtbExportCultivateLearnTaskInfoListVO exportVo = new FtbExportCultivateLearnTaskInfoListVO(); + exportVo.setId(vo.getId()); + exportVo.setTaskName(vo.getTaskName()); + exportVo.setLearnTaskShowId(vo.getLearnTaskShowId()); + exportVo.setTaskClass(vo.getTaskClass()); + exportVo.setTaskClassName(vo.getTaskClassName()); + exportVo.setTotleNum(vo.getTotleNum()); + exportVo.setCompleteTotleNum(vo.getCompleteTotleNum()); + exportVo.setShowTaskNum(vo.getCompleteTotleNum() + "/" + vo.getTotleNum()); + exportVo.setStatus(vo.getStatus()); + exportVo.setStatusName(convertLearnStatusName(vo.getStatus())); + exportVo.setSurplusDay(vo.getSurplusDay()); + if (vo.getSurplusDay() == null) { + exportVo.setSurplusDayStr("-"); + } else { + exportVo.setSurplusDayStr(String.valueOf(vo.getSurplusDay())); + } + exportVo.setTaskType(vo.getTaskType()); + exportVo.setTaskTypeName(convertLearnTypeName(vo.getTaskType())); + exportVo.setTimeLimitStartTime(vo.getTimeLimitStartTime()); + exportVo.setTimeLimitEndTime(vo.getTimeLimitEndTime()); + if (vo.getTaskType().equals(1)) { + String timeStr = DateUtil.format(vo.getTimeLimitStartTime(), "yyyy-MM-dd HH:mm:ss") + "-" + DateUtil.format(vo.getTimeLimitEndTime(), "yyyy-MM-dd HH:mm:ss"); + exportVo.setTaskstartEndDateStr(timeStr); + } else { + exportVo.setTaskstartEndDateStr("-"); + } + exportVo.setCreatorUserName(vo.getCreatorUserName()); + exportVo.setCreatorTime(vo.getCreatorTime()); + exportVo.setCreatorTimeStr(DateUtil.format(vo.getCreatorTime(), "yyyy-MM-dd HH:mm:ss")); + retList.add(exportVo); + } + return retList; + } + + /** + * 转换学习类型名称 + * + * @param taskType 0长期,1限时 + * @return 学习类型名称 + */ + private static String convertLearnTypeName(Integer taskType) { + if (taskType == null) { + return ""; + } + + switch (taskType) { + case 0: + return "长期"; + case 1: + return "限时"; + default: + return ""; + } + } + + /** + * 转换学习状态名称 + * + * @param status 学习状态 0未发布,1未开始,2进行中,3已完成,4终止 + * @return 对应的状态名称 + */ + private static String convertLearnStatusName(Integer status) { + if (status == null) { + return ""; + } + + switch (status) { + case 0: + return "未发布"; + case 1: + return "未开始"; + case 2: + return "进行中"; + case 3: + return "已结束"; + case 4: + return "终止"; + default: + return ""; + } + } + + + /** + * 获取登录用户所属租户code + * + * @return 登录用户所属租户code + */ + public String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } + + /** + * 填充任务统计列表相关数据 + * + * @param list 任务统计列表 + */ + public void fillLearnCountList(List list) { + if (CollUtil.isEmpty(list)) { + return; + } + Set taskIdsSet = new HashSet<>();//任务id集合 + //填充任务分类名称 + for (FtbCultivateLearnTaskInfoCountListVO vo : list) { + taskIdsSet.add(vo.getId()); + } + Map certificateNameMap = queryCertificateName(new ArrayList<>(taskIdsSet)); + Map allIdentificationNameMap = queryIdentificationName(new ArrayList<>(taskIdsSet)); + Map allExamNameMap = queryExamMap(new ArrayList<>(taskIdsSet));//查询任务id对应->考试信息 + Map allExamUserNumMap = examUserService.groupAllCountNumV2(new ArrayList<>(taskIdsSet)); //所有的考试人数 + Map allPassExamUserNumMap = examUserService.groupPassCountNumV2(new ArrayList<>(taskIdsSet)); //所有通过的考试人数 + Map allCourseNumMap = taskCourseService.groupCountNum(new ArrayList<>(taskIdsSet)); //所有课程数量 + Map allTaskNumMap = taskAssignmentService.groupCountNum(new ArrayList<>(taskIdsSet), 0);//所有任务数量 + Map completeTaskNumMap = taskAssignmentService.groupCountNum(new ArrayList<>(taskIdsSet), 1); //已完成任务数量 + Map> allGroupUserListMap = taskAssignmentService.groupListAssignment(new ArrayList<>(taskIdsSet));//查询每个任务的用户列表 taskID->userlist + Map> allCourseListMap = taskCourseService.groupListTaskCourse(new ArrayList<>(taskIdsSet)); //所有课程列表 taskid->courselist + Map> completeLearnMap = queryCultivatePositionCourceLearning(allGroupUserListMap); //用户->已经完成的课程id列表 +// List tableIds = getAllTableId(allIdentificationNameMap); + Map userAllIdentifyMap = cultivateIdentifyApplyService.groupIdentifyCountNum(new ArrayList<>(taskIdsSet), 0); //鉴定所有的人数 + Map userPassIdentifyMap = cultivateIdentifyApplyService.groupIdentifyCountNum(new ArrayList<>(taskIdsSet), 1); //鉴定通过人数 + + for (FtbCultivateLearnTaskInfoCountListVO vo : list) { + + //应完成人数 任务完成人数 任务完成率 + Integer allNum = allTaskNumMap.get(vo.getId()); + if (allNum != null) { + vo.setTaskTotleNum(allNum); + } else { + vo.setTaskTotleNum(0); + } + Integer completeNum = completeTaskNumMap.get(vo.getId()); + if (completeNum != null) { + vo.setTaskCompleteTotleNum(completeNum); + } else { + vo.setTaskCompleteTotleNum(0); + } + vo.setTaskCompleteLv(convertLv(vo.getTaskCompleteTotleNum(), vo.getTaskTotleNum())); + //课程数量 完课率 + Integer courseNum = allCourseNumMap.get(vo.getId()); + vo.setCourseNum(courseNum); + //完课率 + List ftbCultivateLearnTaskAssignments = allGroupUserListMap.get(vo.getId()); + Integer completeCourse = calCompleteNum(allCourseListMap.get(vo.getId()), ftbCultivateLearnTaskAssignments, completeLearnMap); + vo.setCompleteCourseLv(convertLv(completeCourse, vo.getTaskTotleNum())); + + //考试名称 + TaskRelationExamVo examVo = allExamNameMap.get(vo.getId()); + if (examVo != null) { + vo.setExamName(examVo.getExamName()); + //参与考试人数 考试合格率 + Integer examNum = allExamUserNumMap.get(vo.getId()); + if (examNum != null) { + vo.setCompleteExamUserNum(examNum); + } else { + vo.setCompleteExamUserNum(0); + } + vo.setExamPassLv(convertLv(allPassExamUserNumMap.get(vo.getId()), allNum)); + } else { + vo.setExamName("-"); + vo.setExamPassLv("-"); + } + //鉴定名称 + TaskRelationIdentificationVo identificationVo = allIdentificationNameMap.get(vo.getId()); + if (identificationVo != null) { + vo.setIdentificationName(identificationVo.getIdentificationName()); + //鉴定人数 鉴定合格率 + Integer allIdentifyNum = userAllIdentifyMap.get(identificationVo.getTaskId()); + if (allIdentifyNum != null) { + vo.setCompleteIdentificationNum(allIdentifyNum); + } else { + vo.setCompleteIdentificationNum(0); + } + + vo.setIdentificationPassLv(convertLv(userPassIdentifyMap.get(identificationVo.getTaskId()), allNum)); + + } else { + vo.setIdentificationName("-"); + vo.setIdentificationPassLv("-"); + } + //证书名称 + TaskRelationCertificateVo certificateVo = certificateNameMap.get(vo.getId()); + if (certificateVo != null) { + vo.setCertificateName(certificateVo.getCertificateName()); + //已颁发证书 + vo.setSendCertificateNum(calCerTificateNum(ftbCultivateLearnTaskAssignments)); + + } else { + vo.setCertificateName("-"); + } + + } + + } + + /** + * 统计所有的鉴定表id + * + * @param allIdentificationNameMap + * @return + */ + private List getAllTableId(Map allIdentificationNameMap) { + List ret = new ArrayList<>(); + if (CollUtil.isEmpty(allIdentificationNameMap)) { + return ret; + } + //遍历 allIdentificationNameMap + for (Map.Entry entry : allIdentificationNameMap.entrySet()) { + String key = entry.getKey(); + TaskRelationIdentificationVo value = entry.getValue(); + if (value != null) { + ret.add(value.getTableId()); + } + } + return ret; + + } + + /** + * 计算整数数量 + * + * @param ftbCultivateLearnTaskAssignments 任务人员列表 + * @return 获取证书数量 + */ + private Integer calCerTificateNum(List ftbCultivateLearnTaskAssignments) { + Integer num = 0; + if (CollUtil.isEmpty(ftbCultivateLearnTaskAssignments)) { + return num; + } + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : ftbCultivateLearnTaskAssignments) { + if (ftbCultivateLearnTaskAssignment.getIssuedCertificate() > 0) { + num += ftbCultivateLearnTaskAssignment.getIssuedCertificate(); + } + } + return num; + } + + /** + * 计算完成的课程数量 + * + * @param courseList 课程列表 + * @param userAssignmentList 人员列表 + * @param completeLearnMap 已完成课程MAP + * @return + */ + private Integer calCompleteNum(List courseList, List userAssignmentList, Map> completeLearnMap) { + if (CollUtil.isEmpty(courseList)) { + return 0; + } + if (CollUtil.isEmpty(userAssignmentList)) { + return 0; + } + if (CollUtil.isEmpty(completeLearnMap)) { + return 0; + } + List allCoueseList = courseList.stream().map(FtbCultivateLearnTaskCourse::getCourseId).collect(Collectors.toList()); + int completeNum = 0; + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : userAssignmentList) { + List completeCourseList = completeLearnMap.get(ftbCultivateLearnTaskAssignment.getUserId()); + if (CollUtil.isEmpty(completeCourseList)) { + continue; + } + boolean isSubset = completeCourseList.containsAll(allCoueseList); + if (isSubset) { + completeNum++; + } + } + return completeNum; + } + + private Map> queryCultivatePositionCourceLearning(Map> allGroupUserListMap) { + Map> retMap = new HashMap<>(); + if (CollUtil.isEmpty(allGroupUserListMap)) { + return retMap; + } + Set userIdSet = new HashSet<>(); + for (Map.Entry> entry : allGroupUserListMap.entrySet()) { + String key = entry.getKey(); + List assignments = entry.getValue(); + for (FtbCultivateLearnTaskAssignment assignment : assignments) { + userIdSet.add(assignment.getUserId()); + } + } + if (CollUtil.isEmpty(userIdSet)) { + return retMap; + } + List ftbCultivatePositionCourceLearnings = ftbCultivatePositionCourceLearningMapper.listCompleteCourseForUserIds(new ArrayList<>(userIdSet)); + if (CollUtil.isEmpty(ftbCultivatePositionCourceLearnings)) { + return retMap; + } + + for (FtbCultivatePositionCourceLearning entity : ftbCultivatePositionCourceLearnings) { + List courseIdList = retMap.get(entity.getUserId()); + if (courseIdList == null) { + courseIdList = new ArrayList<>(); + } + courseIdList.add(entity.getCourceId()); + retMap.put(entity.getUserId(), courseIdList); + } + return retMap; + } + + /** + * 任务统计导出转换函数 + * + * @param list 任务统计列表 + * @return + */ + public static List convertFtbExportCultivateLearnTaskInfoCountListVO(List list) { + + List exportList = new ArrayList<>(); + if (CollUtil.isEmpty(list)) { + return exportList; + } + exportList = BeanUtil.copyToList(list, FtbExportCultivateLearnTaskInfoCountListVO.class); + for (FtbExportCultivateLearnTaskInfoCountListVO exportVo : exportList) { + String showTaskstartEndDateStr = ""; + if (exportVo.getTaskType().equals(1)) { + showTaskstartEndDateStr = DateUtil.format(exportVo.getTimeLimitStartTime(), "yyyy-MM-dd HH:mm:ss") + "-" + DateUtil.format(exportVo.getTimeLimitEndTime(), "yyyy-MM-dd HH:mm:ss"); + exportVo.setShowTaskstartEndDateStr(showTaskstartEndDateStr); + } else { + exportVo.setShowTaskstartEndDateStr("永久"); + } + if (exportVo.getCompleteExamUserNum() != null) { + exportVo.setCompleteExamUserNumStr(exportVo.getCompleteExamUserNum() + ""); + } else { + exportVo.setCompleteExamUserNumStr("-"); + } + if (exportVo.getCompleteIdentificationNum() != null) { + exportVo.setCompleteIdentificationNumStr(exportVo.getCompleteIdentificationNum() + ""); + } else { + exportVo.setCompleteIdentificationNumStr("-"); + } + if (exportVo.getSendCertificateNum() != null) { + exportVo.setSendCertificateNumStr(exportVo.getSendCertificateNum() + ""); + } else { + exportVo.setSendCertificateNumStr("-"); + } + + exportVo.setTaskTypeName(convertLearnTypeName(exportVo.getTaskType())); + exportVo.setStatusName(convertLearnStatusName(exportVo.getStatus())); + exportVo.setCreatorTimeStr(DateUtil.format(exportVo.getCreatorTime(), "yyyy-MM-dd HH:mm:ss")); + } + return exportList; + + } + + /** + * 批量查询证书名称 + * + * @param taskIds 任务ID集合 + * @return + */ + private Map queryCertificateName(ArrayList taskIds) { + Map retMap = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return retMap; + } + List list = taskCertificateService.queryCertificateName(taskIds); + if (CollUtil.isEmpty(list)) { + return retMap; + } + for (TaskRelationCertificateVo vo : list) { + retMap.put(vo.getTaskId(), vo); + } + return retMap; + } + + /** + * 批量查询鉴定名称 + * + * @param taskIds 任务ID集合 + * @return + */ + private Map queryIdentificationName(List taskIds) { + Map retMap = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return retMap; + } + List list = taskIdentificationService.queryIdentificationName(taskIds); + if (CollUtil.isEmpty(list)) { + return retMap; + } + for (TaskRelationIdentificationVo taskRelationIdentificationVo : list) { + retMap.put(taskRelationIdentificationVo.getTaskId(), taskRelationIdentificationVo); + } + return retMap; + } + + /** + * 批量查询任务考试信息 + * + * @param taskIds 任务ID集合 + * @return + */ + private Map queryExamMap(List taskIds) { + Map retMap = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return retMap; + } + List list = taskExamService.queryExamBaseInfo(taskIds); + if (CollUtil.isEmpty(list)) { + return retMap; + } + for (TaskRelationExamVo taskRelationExamVo : list) { + retMap.put(taskRelationExamVo.getTaskId(), taskRelationExamVo); + } + return retMap; + } + + /** + * 计算百分比 + * + * @param a + * @param b + * @return + */ + public static String convertLv(Integer a, Integer b) { + if (a == null || a.equals(0)) { + return "0%"; + } + if (b.equals(0)) { + return "0%"; + } + int round = Math.round((a / (float) b) * 100); + return round + "%"; + } + + public void organizeOrPositionChange(List userIds, String tenantCode) { + List userLists = userApiV2Util.getUserPrimaryBoundBatchReturnList(userIds, tenantCode); + if (CollUtil.isEmpty(userLists)) { + return; + } + //司令龄 +// Map companyAgeMap = metaDataService.queryCompanyAge(userLists.stream().map(UserBoundVO::getId).collect(Collectors.toList())); +// //岗位龄 +// Map postAgeMap = queryPostAge(userLists, tenantCode); + //查询满足条件的任务 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTask::getEnableMark, 0) + .eq(FtbCultivateLearnTask::getAssignmentRule, 2) + .notIn(FtbCultivateLearnTask::getStatus, 3, 4); + List taskList = ftbCultivateLearnTaskInfoMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskList)) { + return; + } + List taskIds = taskList.stream() + .map(FtbCultivateLearnTask::getId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + LambdaQueryWrapper assignWrapper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds); + List assignList = taskAssignmentService.list(assignWrapper); + Map> cultivateTaskMap = assignList.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskAssignment::getTaskId)); + List addAssignmentList = new ArrayList<>(); + for (FtbCultivateLearnTask task : taskList) { + + + Set existUserIds = new HashSet<>(); + List taskMapOrDefault = cultivateTaskMap.getOrDefault(task.getId(), new ArrayList<>()); + if (CollUtil.isNotEmpty(taskMapOrDefault)) { + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : taskMapOrDefault) { + existUserIds.add(ftbCultivateLearnTaskAssignment.getUserId()); + } + } + for (UserBoundVO newUser : userLists) { + if (!existUserIds.contains(newUser.getId()) + && StringUtils.isNotEmpty(newUser.getOrganizeId()) + && StringUtils.isNotEmpty(task.getAssociationIds()) + && task.getAssociationIds().contains(newUser.getOrganizeId()) + ) { + FtbCultivateLearnTaskAssignment add = new FtbCultivateLearnTaskAssignment(); + add.setTaskId(task.getId()); + add.setUserId(newUser.getId()); + add.setStudyStats(0); + add.setEnableMark(0); + addAssignmentList.add(add); + } + } + } + if (CollUtil.isNotEmpty(addAssignmentList)) { + addUserToTaskReal(addAssignmentList); + } + } + + + public void addNewPersonToTask(List userIds, String tenantCode) { + List userLists = userApiV2Util.getUserPrimaryBoundBatchReturnList(userIds, tenantCode); + if (CollUtil.isEmpty(userLists)) { + return; + } + //司令龄 + Map companyAgeMap = metaDataService.queryCompanyAge(userLists.stream().map(UserBoundVO::getId).collect(Collectors.toList())); + //岗位龄 + Map postAgeMap = queryPostAge(userLists, tenantCode); + //查询满足条件的任务 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTask::getEnableMark, 0) + .eq(FtbCultivateLearnTask::getAssignmentRule, 4) + .notIn(FtbCultivateLearnTask::getStatus, 3, 4); + List taskList = ftbCultivateLearnTaskInfoMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskList)) { + return; + } + List addAssignmentList = new ArrayList<>(); + for (FtbCultivateLearnTask task : taskList) { + + List realUserId = new ArrayList<>(); + Integer endEntryPeriod = task.getEndEntryPeriod(); + if (task.getCustomAssignType().equals(0)) { + for (UserBoundVO userEntity : userLists) { + Integer age = companyAgeMap.get(userEntity.getId()); + if (age == null) { + continue; + } + if (age <= endEntryPeriod) { + realUserId.add(userEntity.getId()); + } + + } + } else { + for (UserBoundVO userEntity : userLists) { + if (StringUtils.isNotEmpty(userEntity.getPositionId()) + && StringUtils.isNotEmpty(task.getCustomAssignPostIds()) + && task.getCustomAssignPostIds().contains(userEntity.getPositionId()) + ) { + Integer age = postAgeMap.get(userEntity.getId()); + if (age == null) { + continue; + } + if (age >= endEntryPeriod) { + realUserId.add(userEntity.getId()); + } + } + } + } + if (CollUtil.isEmpty(realUserId)) { + continue; + } + Set existUserIds = new HashSet<>(); + LambdaQueryWrapper assignWraper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .eq(FtbCultivateLearnTaskAssignment::getTaskId, task.getId()); + List assignList = taskAssignmentService.list(assignWraper); + if (CollUtil.isNotEmpty(assignList)) { + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : assignList) { + existUserIds.add(ftbCultivateLearnTaskAssignment.getUserId()); + } + } + for (String userId : realUserId) { + if (!existUserIds.contains(userId)) { + FtbCultivateLearnTaskAssignment add = new FtbCultivateLearnTaskAssignment(); + add.setTaskId(task.getId()); + add.setUserId(userId); + add.setStudyStats(0); + add.setEnableMark(0); + addAssignmentList.add(add); + } + } + } + if (CollUtil.isNotEmpty(addAssignmentList)) { + addUserToTaskReal(addAssignmentList); + } + } + + + private Map queryPostAge(List userBoundVOList, String tenantId) { + // 转换为用户任职信息列表 + List joeInfoList = userBoundVOList.stream() + .map(user -> { + FtbPersonnlesJoeInfo info = new FtbPersonnlesJoeInfo(); + info.setUserId(user.getId()); + info.setPositionId(user.getPositionId()); + return info; + }) + .collect(Collectors.toList()); + + // 查询岗龄信息 + FtbPersonnlesJobTenureDTO tenureDTO = new FtbPersonnlesJobTenureDTO(); + tenureDTO.setUserInfo(joeInfoList); + tenureDTO.setTenantId(tenantId); + + List tenureList = ftbPersonneApi.queryPositionTenureAge(tenureDTO); + if (CollUtil.isEmpty(tenureList)) { + return new HashMap<>(); + } + + return tenureList.stream() + .collect(Collectors.toMap(FtbPersonnlesJobTenureVO::getUserId, FtbPersonnlesJobTenureVO::getJobTenure)); + } + + + /** + * 真正添加人到任务列表 + * + * @param addAssignmentList 新增任务 + */ + private void addUserToTaskReal(List addAssignmentList) { + for (FtbCultivateLearnTaskAssignment vo : addAssignmentList) { + taskAssignmentService.save(vo); + } + } + + /** + * 计算员工司龄(根据当前日期和入职日期) + * + * @param userLists 用户列表 + * @return 用户ID -> 司龄(天)的映射 + */ + private Map runCompanyAge(List userLists) { + Map companyAgeMap = new HashMap<>(); + if (CollUtil.isEmpty(userLists)) { + return companyAgeMap; + } + + Date currentDate = new Date(); + for (UserBoundVO user : userLists) { + if (user.getEntryDate() == null) { + continue; + } + long between = DateUtil.between(user.getEntryDate(), currentDate, DateUnit.DAY); + companyAgeMap.put(user.getId(), (int) between); + } + + return companyAgeMap; + } + + /** + * 每天定时任务检测用户是否加入任务中 + */ + @Transactional + public void timingAddNewPersonToTask(String tenantId) { + //查询满足条件的任务 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTask::getEnableMark, 0) + .eq(FtbCultivateLearnTask::getAssignmentRule, 4) + .notIn(FtbCultivateLearnTask::getStatus, 3, 4); + List taskList = ftbCultivateLearnTaskInfoMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskList)) { + return; + } + List userLists = userApiV2Util.getAllUserList(tenantId); + if (CollUtil.isEmpty(userLists)) { + return; + } + //查询司令 + Map companyAgeMap = runCompanyAge(userLists); + //查询岗领 + Map postAgeMap = queryPostAge(userLists, tenantId); + + + List addAssignmentList = new ArrayList<>(); + for (FtbCultivateLearnTask task : taskList) { + + List realUserId = new ArrayList<>(); + Integer endEntryPeriod = task.getEndEntryPeriod(); + if (task.getCustomAssignType().equals(0)) { + for (UserBoundVO userEntity : userLists) { + Integer age = companyAgeMap.get(userEntity.getId()); + if (age == null) { + continue; + } + if (age <= endEntryPeriod) { + realUserId.add(userEntity.getId()); + } + + } + } else { + for (UserBoundVO userEntity : userLists) { + if (StringUtils.isNotEmpty(userEntity.getPositionId()) + && StringUtils.isNotEmpty(task.getCustomAssignPostIds()) + && task.getCustomAssignPostIds().contains(userEntity.getPositionId()) + ) { + Integer age = postAgeMap.get(userEntity.getId()); + if (age == null) { + continue; + } + if (age <= endEntryPeriod) { + realUserId.add(userEntity.getId()); + } + } + } + } + if (CollUtil.isEmpty(realUserId)) { + continue; + } + Set existUserIds = new HashSet<>(); + LambdaQueryWrapper assignWraper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .eq(FtbCultivateLearnTaskAssignment::getTaskId, task.getId()); + List assignList = taskAssignmentService.list(assignWraper); + if (CollUtil.isNotEmpty(assignList)) { + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : assignList) { + existUserIds.add(ftbCultivateLearnTaskAssignment.getUserId()); + } + } + for (String userId : realUserId) { + if (!existUserIds.contains(userId)) { + FtbCultivateLearnTaskAssignment add = new FtbCultivateLearnTaskAssignment(); + add.setTaskId(task.getId()); + add.setUserId(userId); + add.setStudyStats(0); + add.setEnableMark(0); + addAssignmentList.add(add); + } + } + } + if (CollUtil.isNotEmpty(addAssignmentList)) { + addUserToTaskReal(addAssignmentList); + } + } + + + /** + * 每天定时定点提醒 + * + * @param tenantId + */ + public void timingTaskLearningAlert(String tenantId) { + List taskList = ftbCultivateLearnTaskInfoMapper.queryNeedPerDayAlert(); + if (CollUtil.isEmpty(taskList)) { + return; + } + log.error("timingTaskLearningAlert={}", taskList); + List alertUserList = new ArrayList<>(); + for (NeedPerDayAlertDto dto : taskList) { + if (StringUtils.isEmpty(dto.getPerInterval())) { + continue; + } + // 解析时间 + String[] split = dto.getPerInterval().split(":"); + int alertHour = Integer.parseInt(split[0]); + LocalTime now = LocalTime.now(); + int currHour = now.getHour(); + if (currHour != alertHour) { + continue; + } + //需要提醒 查询未完成的人员列表 + LambdaQueryWrapper assignWraper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .in(FtbCultivateLearnTaskAssignment::getStudyStats, 0, 1) + .eq(FtbCultivateLearnTaskAssignment::getTaskId, dto.getTaskId()); + List assignList = taskAssignmentService.list(assignWraper); + if (CollUtil.isNotEmpty(assignList)) { + for (FtbCultivateLearnTaskAssignment assignment : assignList) { + NeedAlertUserDto userDto = new NeedAlertUserDto(); + userDto.setTaskId(dto.getTaskId()); + userDto.setTaskName(dto.getTaskName()); + userDto.setUserId(assignment.getUserId()); + alertUserList.add(userDto); + } + } + } + log.error("timingTaskLearningAlert user={}", alertUserList); + if (CollUtil.isNotEmpty(alertUserList)) { + cultivateLearnTaskIMUtils.sendMsg(alertUserList, tenantId); + } + + } + + /** + * 定时检查任务开始消息提醒 + * + * @param taskList 任务id + * @param tenantId 租户id + */ + public void sendStartTaskLearningAlert(List taskList, String tenantId) { + List alertUserList = new ArrayList<>(); + for (FtbCultivateLearnTask dto : taskList) { + //需要提醒 查询未完成的人员列表 + LambdaQueryWrapper assignWraper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .in(FtbCultivateLearnTaskAssignment::getStudyStats, 0, 1) + .eq(FtbCultivateLearnTaskAssignment::getTaskId, dto.getId()); + List assignList = taskAssignmentService.list(assignWraper); + if (CollUtil.isNotEmpty(assignList)) { + for (FtbCultivateLearnTaskAssignment assignment : assignList) { + NeedAlertUserDto userDto = new NeedAlertUserDto(); + userDto.setTaskId(dto.getId()); + userDto.setTaskName(dto.getTaskName()); + userDto.setUserId(assignment.getUserId()); + alertUserList.add(userDto); + } + } + } + log.error("timingTaskLearningAlert user={}", alertUserList); + if (CollUtil.isNotEmpty(alertUserList)) { + cultivateLearnTaskIMUtils.sendMsg(alertUserList, tenantId); + } + + } + + + /** + * 修改用户的任务状态 长期任务 + * + * @param tenantCode + */ + public void updateLongTaskUserTaskStatus(String tenantCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(FtbCultivateLearnTask::getId, + FtbCultivateLearnTask::getTimeLimitEndTime, + FtbCultivateLearnTask::getStudentTimeCompletion, + FtbCultivateLearnTask::getTimeCompletionUnit, + FtbCultivateLearnTask::getTimeCompletionDays + ).eq(FtbCultivateLearnTask::getStatus, 3) + .eq(FtbCultivateLearnTask::getTaskType, 1) + .eq(FtbCultivateLearnTask::getEnableMark, 0); + List taskList = ftbCultivateLearnTaskInfoMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(taskList)) { + //转换成map + Map taskMap = taskList.stream().collect(Collectors.toMap(FtbCultivateLearnTask::getId, Function.identity())); + //获取任务ids集合 + List taskIds = taskList.stream().map(FtbCultivateLearnTask::getId).collect(Collectors.toList()); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds); + lambdaQuery.in(FtbCultivateLearnTaskAssignment::getStudyStats, 0, 1); + lambdaQuery.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List taskAssignments = ftbCultivateLearnTaskAssignmentMapper.selectList(lambdaQuery); + if (CollUtil.isEmpty(taskAssignments)) return; + + List needUpdate = new ArrayList<>(); + for (FtbCultivateLearnTaskAssignment taskAssignment : taskAssignments) { + FtbCultivateLearnTask task = taskMap.get(taskAssignment.getTaskId()); + if (task == null) { + continue; + } + + //学习中 限时完成 学员限时完成(0,限时,1不限时) + if (taskAssignment.getStudyStats() == 1 || taskAssignment.getStudyStats() == 0) { + taskAssignment.setStudyStats(3); + needUpdate.add(taskAssignment); + } + } + if (CollUtil.isNotEmpty(needUpdate)) { + taskAssignmentService.updateBatchById(needUpdate, needUpdate.size()); + } + } + + } + + /** + * 添加地图人员 + * + * @param userIds 用户id + * @param tenantId 租户id + */ + public void addPromotionUser(List userIds, String tenantId) { + List userLists = userApiV2Util.getUserPrimaryBoundBatchReturnList(userIds, tenantId); + if (CollUtil.isEmpty(userLists)) { + return; + } + List positionIds = new ArrayList<>(); + Map> userMap = new HashMap<>(); + for (UserBoundVO user : userLists) { + if (StringUtils.isEmpty(user.getPositionId())) { + continue; + } + positionIds.add(user.getPositionId()); + List userList = userMap.get(user.getPositionId()); + if (userList == null) { + userList = new ArrayList<>(); + } + userList.add(user); + userMap.put(user.getPositionId(), userList); + } + + if (CollUtil.isEmpty(positionIds)) { + return; + } + LambdaQueryWrapper promotionSettingWrapper = Wrappers.lambdaQuery(); + promotionSettingWrapper.ne(FtbCultivatePromotionSetting::getScope, 3) + .eq(FtbCultivatePromotionSetting::getEnabledMark, 0) + .in(FtbCultivatePromotionSetting::getPostId, positionIds) + ; + List promotionSettingList = ftbCultivatePromotionSettingService.list(promotionSettingWrapper); + if (CollUtil.isEmpty(promotionSettingList)) { + return; + } + List ftbCultivatePromotionUsers = new ArrayList<>(); + for (FtbCultivatePromotionSetting ftbCultivatePromotionSetting : promotionSettingList) { + List userBoundVOS = userMap.get(ftbCultivatePromotionSetting.getPostId()); + if (CollUtil.isEmpty(userBoundVOS)) { + continue; + } + for (UserBoundVO userBoundVO : userBoundVOS) { + if (ftbCultivatePromotionSetting.getScope().equals(2) + && ftbCultivatePromotionSetting.getPostId().equals(userBoundVO.getPositionId()) + && ftbCultivatePromotionSetting.getScopeId().equals(userBoundVO.getGradeId()) + ) { + FtbCultivatePromotionUser ftbCultivatePromotionUser = new FtbCultivatePromotionUser(); + ftbCultivatePromotionUser.setPromotionId(ftbCultivatePromotionSetting.getPromotionId()); + ftbCultivatePromotionUser.setUserId(userBoundVO.getId()); + ftbCultivatePromotionUser.setEnabledMark(0); + ftbCultivatePromotionUsers.add(ftbCultivatePromotionUser); + } else if (ftbCultivatePromotionSetting.getScope().equals(1) + && ftbCultivatePromotionSetting.getPostId().equals(userBoundVO.getPositionId()) + ) { + FtbCultivatePromotionUser ftbCultivatePromotionUser = new FtbCultivatePromotionUser(); + ftbCultivatePromotionUser.setPromotionId(ftbCultivatePromotionSetting.getPromotionId()); + ftbCultivatePromotionUser.setUserId(userBoundVO.getId()); + ftbCultivatePromotionUser.setEnabledMark(0); + ftbCultivatePromotionUsers.add(ftbCultivatePromotionUser); + } + } + } + if (CollUtil.isNotEmpty(ftbCultivatePromotionUsers)) { + for (FtbCultivatePromotionUser user : ftbCultivatePromotionUsers) { + long count = ftbCultivatePromotionUserService.count( + Wrappers.lambdaQuery() + .eq(FtbCultivatePromotionUser::getUserId, user.getUserId()) + .eq(FtbCultivatePromotionUser::getPromotionId, user.getPromotionId()) + .eq(FtbCultivatePromotionUser::getEnabledMark, 0) + ); + if (count > 0) { + continue; + } + ftbCultivatePromotionUserService.save(user); + } + } + } + + public void dealPositionUpdateUser(List userIds, String tenantCode) { + List userLists = userApiV2Util.getUserPrimaryBoundBatchReturnList(userIds, tenantCode); + if (CollUtil.isEmpty(userLists)) { + return; + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTask::getEnableMark, 0) + .eq(FtbCultivateLearnTask::getAssignmentRule, 3) + .notIn(FtbCultivateLearnTask::getStatus, 3, 4); + List taskList = ftbCultivateLearnTaskInfoMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskList)) { + return; + } + List taskIds = taskList.stream() + .map(FtbCultivateLearnTask::getId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + LambdaQueryWrapper assignWrapper = new LambdaQueryWrapper() + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds); + List assignList = taskAssignmentService.list(assignWrapper); + Map> cultivateTaskMap = assignList.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskAssignment::getTaskId)); + List addAssignmentList = new ArrayList<>(); + for (FtbCultivateLearnTask task : taskList) { + + + Set existUserIds = new HashSet<>(); + List taskMapOrDefault = cultivateTaskMap.getOrDefault(task.getId(), new ArrayList<>()); + if (CollUtil.isNotEmpty(taskMapOrDefault)) { + for (FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment : taskMapOrDefault) { + existUserIds.add(ftbCultivateLearnTaskAssignment.getUserId()); + } + } + for (UserBoundVO newUser : userLists) { + if (!existUserIds.contains(newUser.getId()) + && StringUtils.isNotEmpty(newUser.getPositionId()) + && StringUtils.isNotEmpty(task.getAssociationIds()) + && task.getAssociationIds().contains(newUser.getPositionId()) + ) { + FtbCultivateLearnTaskAssignment add = new FtbCultivateLearnTaskAssignment(); + add.setTaskId(task.getId()); + add.setUserId(newUser.getId()); + add.setStudyStats(0); + add.setEnableMark(0); + addAssignmentList.add(add); + } + } + } + if (CollUtil.isNotEmpty(addAssignmentList)) { + addUserToTaskReal(addAssignmentList); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivatePerUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivatePerUtils.java new file mode 100644 index 0000000..ead6e9f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivatePerUtils.java @@ -0,0 +1,253 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import lombok.Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Title: CultivatePerUtils + * @Author: peng.hao + * @create: 2024/3/21 11:25 + */ +@Component +public class CultivatePerUtils { + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + FtbPersonnelsStaffRosterService staffRosterService; + + + /** + * 转换组织id集合 + * + * @param str 组织编码 + * @return 组织id集合 + */ + public List convertOrganizationalIdCollection(String str) { + String[] split = str.split(","); + // 组织id输入系统展示id + List list = Arrays.asList(split); + List organizeEntityList = userApiV2Util.organizesOrHaveChildByOrganizeEncodes(list, null); + return organizeEntityList.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } + + /** + * 转换岗位id集合 + * + * @param str + * @return + */ + public List convertPositionIdCollection(String str) { + String[] split = str.split(","); + // 岗位id输入系统展示id + List list = Arrays.asList(split); +// List positionEntities = positionApi.checkOrgPositionGradesBound(list); + List positionEntities = userApiV2Util.listPositionBaseInfoByEncodes(list, null); + return positionEntities.stream().map(PositionBaseInfoVO::getId).collect(Collectors.toList()); + } + + /** + * 转换员工id集合 + * + * @param str + * @return + */ + public List convertEmployeeIdCollection(String str) { + String[] split = str.split(","); + // 员工id输入系统展示id + List list = Arrays.asList(split); + List ftbPersonnelsStaffRosters = staffRosterService.queryUserInfoByWorkerId(list); + return ftbPersonnelsStaffRosters.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList()); + } + + /** + * 根据组织id查询展示组织id + * + * @param list + * @return + */ + public Map convertOrganizationalId(List list) { +// List organizeByIds = organizeApi.getOrganizeByIds(list); + List organizeByIds = userApiV2Util.organizesByOrganizeIds(list, null); + if (CollUtil.isEmpty(organizeByIds)) { + return new HashMap<>(); + } + Map map = new HashMap<>(); //组织id-》组织编码 + for (OrganizeGeneralDetailVO organizeById : organizeByIds) { + if (StringUtils.isEmpty(organizeById.getEnCode())) { + map.put(organizeById.getId(), ""); + } else { + map.put(organizeById.getId(), organizeById.getEnCode()); + } + } + return map; + } + + + /** + * 根据岗位id查询展示岗位id + * + * @param list + * @return + */ + public Map convertPostId(List list) { +// ActionResult> positionListByIds = positionApi.getPositionListByIds(list); + List positionListByIds = userApiV2Util.listPositionDetailInfoByIds(list, null); + if (CollUtil.isEmpty(positionListByIds)) { + return new HashMap<>(); + } + return positionListByIds.stream().map(item -> { + PostSysEntity postSysEntity = new PostSysEntity(); + postSysEntity.setId(item.getId()); + postSysEntity.setEncode(item.getEnCode()); + return postSysEntity; + }).collect(Collectors.toMap( + CultivatePerUtils.PostSysEntity::getId, + CultivatePerUtils.PostSysEntity::getEncode, + (a, b) -> a)); + } + + + /** + * 根据员工id查询展示员工id + * + * @param list + * @return + */ + public Map coverPersonalIds(List list) { + return staffRosterService.queryWorkerIdByUserIds(list).stream().map(item -> { + UserSysEntity userSysEntity = new UserSysEntity(); + userSysEntity.setId(item.getUserId()); + userSysEntity.setEncode(item.getSystemWokerId()); + return userSysEntity; + }).collect(Collectors.toMap(UserSysEntity::getId, UserSysEntity::getEncode, (a, b) -> a)); + } + + /** + * 获取当前组织下的所有组织 + * + * @param organizeId + * @return + */ + public List getAllOrganize(String organizeId) { +// List subsetsBasedOnParent = organizeApi.findSubsetsBasedOnParent(List.of(organizeId)); + List organizeGeneralDetailVOS = userApiV2Util.organizesOrHaveChildByOrganizeIds(List.of(organizeId), true, null); + if (ObjectUtil.isEmpty(organizeGeneralDetailVOS)) { + return new ArrayList<>(); + } + return organizeGeneralDetailVOS.stream().map(item -> item.getId()).collect(Collectors.toList()); + } + + /** + * 获取当前组织下的所有组织 + * + * @param orgIds 组织ID列表 + * @return 组织ID列表,包括当前组织及其所有子组织 + */ + public List batchAllOrganize(List orgIds) { + return getAllOrganizationInformation(orgIds); + } + + + /** + * 获取当前组织下的所有组织 + * + * @param orgIds 组织ID列表 + * @return 组织ID列表,包括当前组织及其所有子组织 + */ + public List getAllOrganizationInformation(List orgIds) { + List organizeGeneralDetailVOS = userApiV2Util.organizesOrHaveChildByOrganizeIds(orgIds, true, null); + if (ObjectUtil.isEmpty(organizeGeneralDetailVOS)) { + return new ArrayList<>(); + } + return organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } + + /** + * 根据用户id查询员工姓名 + * + * @param userIds + * @return + */ + public Map queryRosterNameBasedOnUserId(List userIds) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(FtbPersonnelsStaffRoster::getUserId, userIds); + List rosters = staffRosterService.list(wrapper); + if (CollUtil.isEmpty(rosters)) { + return new HashMap<>(); + } + return rosters.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getName, (a, b) -> a)); + } + + /** + * 根据组织id查询组织名称 + * + * @param ids + * @return + */ + public Map queryOrgNames(List ids) { +// List entityList = organizeApi.getOrganizeName(ids); + List entityList = userApiV2Util.organizesByOrganizeIds(ids, null); + if (CollUtil.isEmpty(entityList)) { + return new HashMap<>(); + } + return entityList.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, OrganizeGeneralDetailVO::getName, (a, b) -> a)); + } + + public static BigDecimal computeDivision(Integer a, Integer b) { + // a/b 除数为0 + if (b == 0) { + return BigDecimal.ZERO; + } + BigDecimal a0 = new BigDecimal(a); + BigDecimal b0 = new BigDecimal(b); + BigDecimal divide = a0.divide(b0, 2, RoundingMode.HALF_UP); + return divide.multiply(BigDecimal.valueOf(100)); + } + + public static BigDecimal computeDivision(BigDecimal a) { + // a/b 除数为0 + if (BigDecimal.ZERO.compareTo(a) == 0) { + return BigDecimal.ZERO; + } + BigDecimal a0 = a; + BigDecimal b0 = new BigDecimal(60 * 60); + BigDecimal divide = a0.divide(b0, 2, RoundingMode.HALF_UP); + return divide; + } + + @Data + public class OrganizeSysEntity { + private String id; + private String encode; + } + + @Data + public class PostSysEntity { + private String id; + private String encode; + } + + @Data + public class UserSysEntity { + private String id; + private String encode; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateTaskLeanAsyncDealUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateTaskLeanAsyncDealUtil.java new file mode 100644 index 0000000..994b962 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/CultivateTaskLeanAsyncDealUtil.java @@ -0,0 +1,44 @@ +package jnpf.cultivate.utils; +/** + * 任务异步处理 + * + * @Author: xuguilin + * @create: 2024/9/20:15:50 + */ + +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Slf4j +public class CultivateTaskLeanAsyncDealUtil { + + + @Autowired + @Lazy + private CultivateLearnUtils cultivateLearnUtils; + + /** + * 任务开始发送消息 + * + * @param taskList 任务id集合 + * @param tenantCode 租户id + */ + public void asyncSendStartTaskLearningAlert(List taskList, String tenantCode) { + cultivateLearnUtils.sendStartTaskLearningAlert(taskList, tenantCode); + } + + /** + * 异步处理 用户的任务状态 + * + * @param tenantCode + */ + public void asyncUpdateUserTaskStatus(String tenantCode) { + cultivateLearnUtils.updateLongTaskUserTaskStatus(tenantCode); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/HistoryPaperUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/HistoryPaperUtils.java new file mode 100644 index 0000000..c145811 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/HistoryPaperUtils.java @@ -0,0 +1,41 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamHistoryPaper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.resp.ExamQuestionBakVo; +import jnpf.util.QuestionAnalysisUtil; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class HistoryPaperUtils { + + public static FtbCultivateExam converToExam(FtbCultivateExamHistoryPaper ftbCultivateExamHistoryPaper) { + FtbCultivateExam exam = BeanUtil.copyProperties(ftbCultivateExamHistoryPaper, FtbCultivateExam.class); + exam.setId(ftbCultivateExamHistoryPaper.getPrimaryExamId()); + return exam; + } + + public static Map analysPaperQuestionCount(List questionVoList) { + //2、初始化返回值 + Map analyMap = QuestionAnalysisUtil.initAnalysQuestionCount(); + if (CollectionUtil.isNotEmpty(questionVoList)) { + QuestionAnalysisUtil.analysQuestionCount(analyMap, questionVoList); + } + return analyMap; + } + + public static List convertQuestionList(String str) { + if (StringUtils.isEmpty(str)) { + return new ArrayList<>(); + } + return JSONUtil.toList(str, ExamQuestionBakVo.class); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/QuestionExcelExportUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/QuestionExcelExportUtil.java new file mode 100644 index 0000000..b2f09e7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/QuestionExcelExportUtil.java @@ -0,0 +1,553 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jnpf.base.UserInfo; +import jnpf.model.cultivate.req.questionbank.AddQuestionReq; +import jnpf.model.cultivate.req.questionbank.EditQuestionReq; +import jnpf.model.cultivate.req.questionbank.QuestionOptionReq; +import jnpf.model.cultivate.resp.ExcelImportQuestionResultReq; +import jnpf.model.cultivate.resp.ExcelUserQuestionVo; +import jnpf.model.cultivate.resp.QuestionOptionVo; +import jnpf.model.cultivate.resp.UserQuestionVo; +import jnpf.model.enums.CourseEnums; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class QuestionExcelExportUtil { + + final static String FTB_QUESTION_IMPORT_SHEET_KEY = "qtn:import::%s:%s"; + + public static void questionExportExcel(HttpServletResponse response, String fileName, List list, Class tClass) throws IOException { + String fileRealName = URLEncoder.encode(fileName + System.currentTimeMillis() / 1000, "UTF-8").replaceAll("\\+", "%20"); + try (ServletOutputStream outputStream = response.getOutputStream()) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileRealName + ".xlsx"); + + + ExcelWriterBuilder write = EasyExcel.write(outputStream, tClass); + write.registerWriteHandler(new EasyExcelUtils.CustomCellWriteHandler()).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).inMemory(true); + write.sheet("sheet1").doWrite(list); + + } + } + + public static void questionExportExcel1(HttpServletResponse response, String fileName, List list, Class tClass) throws IOException { + String fileRealName = URLEncoder.encode(fileName + System.currentTimeMillis() / 1000, "UTF-8").replaceAll("\\+", "%20"); + try (ServletOutputStream outputStream = response.getOutputStream()) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileRealName + ".xlsx"); + String template = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/xgl/questionExportTemplate.xlsx"; + File localFile = downloadFile(template); + + + // 这里 会填充到第一个sheet, 然后文件流会自动关闭 + EasyExcel.write(outputStream).withTemplate(localFile).sheet("Sheet1").doFill(list); + + + } + } + + public static File downloadFile(String template) { + + // 本地保存的路径及文件名 + String savePath = "./" + System.currentTimeMillis() + "/questionExportTemplate.xlsx"; // 请根据实际情况修改 + + try { + // 下载文件 + File file = FileUtil.writeBytes(HttpUtil.downloadBytes(template), FileUtil.file(savePath)); + return file; + + } catch (Exception e) { + System.err.println("文件下载失败:" + e.getMessage()); + return null; + } + } + + + public static List convertQuestionList(List userQuestionVoList) { + List list = new ArrayList<>(); + for (UserQuestionVo userQuestionVo : userQuestionVoList) { + ExcelUserQuestionVo excelUserQuestionVo = toExcelUserQuestionVo(userQuestionVo); + list.add(excelUserQuestionVo); + } + return list; + } + + private static ExcelUserQuestionVo toExcelUserQuestionVo(UserQuestionVo userQuestionVo) { + ExcelUserQuestionVo vo = new ExcelUserQuestionVo(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.fromCode(userQuestionVo.getType()); + if (null != type) { + vo.setType(type.getDesc()); + } + + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.fromCode(userQuestionVo.getDifficulty()); + if (null != difficulty) { + vo.setDifficulty(difficulty.getDesc()); + } + List questionOptionVoList = new ArrayList<>(); + switch (type) { + case SINGLE://单选 + case JUDGE://判断 + case MULTI://多选 + case ONE_OR_MULTI://不定项 + questionOptionVoList = userQuestionVo.getQuestionOptionVoList(); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + List rightAnsters = new ArrayList<>(); + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setOptionValue(questionOptionVoList.get(i), vo, i); + if (StringUtils.isNotEmpty(right)) { + rightAnsters.add(right); + } + } + vo.setUserAnswer(StringUtils.join(rightAnsters, ",")); + } + break; + case FILL://填空题 + questionOptionVoList = userQuestionVo.getQuestionOptionVoList(); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + for (int i = 0; i < questionOptionVoList.size(); i++) { + setOptionForFill(i, questionOptionVoList, vo); + } + } + break; + + } + + vo.setContent(userQuestionVo.getContent()); + vo.setAnalysis(userQuestionVo.getAnalysis()); + + return vo; + } + + private static void setOptionForFill(int i, List questionOptionVoList, ExcelUserQuestionVo vo) { + + switch (i) { + case 0: + vo.setOptionA(questionOptionVoList.get(i).getRightAnswer()); + break; + case 1: + vo.setOptionB(questionOptionVoList.get(i).getRightAnswer()); + break; + case 2: + vo.setOptionC(questionOptionVoList.get(i).getRightAnswer()); + break; + case 3: + vo.setOptionD(questionOptionVoList.get(i).getRightAnswer()); + break; + case 4: + vo.setOptionE(questionOptionVoList.get(i).getRightAnswer()); + break; + case 5: + vo.setOptionF(questionOptionVoList.get(i).getRightAnswer()); + break; + case 6: + vo.setOptionG(questionOptionVoList.get(i).getRightAnswer()); + break; + case 7: + vo.setOptionH(questionOptionVoList.get(i).getRightAnswer()); + break; + default: + break; + } + } + + + private static String setOptionValue(QuestionOptionVo questionOptionVo, ExcelUserQuestionVo vo, Integer i) { + String optionValue = questionOptionVo.getContent(); + Integer isRightOption = questionOptionVo.getIsRightOption(); + return setRightAnswerAndFillOption(optionValue, isRightOption, vo, i); + } + + private static String setErrorOptionValue(QuestionOptionReq questionOptionVo, ExcelUserQuestionVo vo, Integer i) { + String optionValue = questionOptionVo.getContent(); + Integer isRightOption = questionOptionVo.getIsRightOption(); + return setRightAnswerAndFillOption(optionValue, isRightOption, vo, i); + } + + private static String setRightAnswerAndFillOption(String optionValue, Integer isRightOption, ExcelUserQuestionVo vo, Integer i) { + String right = ""; + switch (i) { + case 0: + vo.setOptionA(optionValue); + if (isRightOption == 1) { + right = "A"; + } + break; + case 1: + vo.setOptionB(optionValue); + if (isRightOption == 1) { + right = "B"; + } + break; + case 2: + vo.setOptionC(optionValue); + if (isRightOption == 1) { + right = "C"; + } + break; + case 3: + vo.setOptionD(optionValue); + if (isRightOption == 1) { + right = "D"; + } + break; + case 4: + vo.setOptionE(optionValue); + if (isRightOption == 1) { + right = "E"; + } + break; + case 5: + vo.setOptionF(optionValue); + if (isRightOption == 1) { + right = "F"; + } + break; + case 6: + vo.setOptionG(optionValue); + if (isRightOption == 1) { + right = "G"; + } + break; + case 7: + vo.setOptionH(optionValue); + if (isRightOption == 1) { + right = "H"; + } + break; + default: + break; + } + return right; + } + + + public static void dealFillQuestion(List allQuestionList) { + for (ExcelImportQuestionResultReq addQuestionReq : allQuestionList) { + checkOneQuestion(addQuestionReq); + } + } + + public static void checkOneQuestion(ExcelImportQuestionResultReq addQuestionReq) { + String content = addQuestionReq.getContent(); + if(StringUtils.isEmpty(content)){ + addQuestionReq.setMsg("题目内容不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if(content.length()>200){ + addQuestionReq.setMsg("题目内容长度不能大于200个字符"); + addQuestionReq.setSuccess(1); + return; + } + if(StringUtils.isNotEmpty(addQuestionReq.getAnalysis())){ + if(addQuestionReq.getAnalysis().length()>1000){ + addQuestionReq.setMsg("题目解析长度不能大于1000个字符"); + addQuestionReq.setSuccess(1); + return; + } + } + if (StringUtils.isNotEmpty(content) && CourseEnums.QuestionType.FILL.getCode().equals(addQuestionReq.getType())) { + int count = countUnderscoresConsideringAdjacentAsOne(content); + if (addQuestionReq.getOptionList().size() > count) { + addQuestionReq.setOptionList(addQuestionReq.getOptionList().subList(0, count)); + } else if (addQuestionReq.getOptionList().size() < count) { + long size = addQuestionReq.getOptionList().size(); + for (int i = 0; i < count - size; i++) { + QuestionOptionReq optionReq = new QuestionOptionReq(); + optionReq.setContent(""); + optionReq.setSortCode(size + i); + addQuestionReq.getOptionList().add(optionReq); + } + } + if (addQuestionReq.getOptionList().size() > 8) { + addQuestionReq.setMsg("选项不能大于8个"); + addQuestionReq.setSuccess(1); + return; + } + if (!addQuestionReq.getContent().contains("_")) { + addQuestionReq.setMsg("填空题题目必须包含下划线"); + addQuestionReq.setSuccess(1); + return; + } + } else if (CourseEnums.QuestionType.MULTI.getCode().equals(addQuestionReq.getType())) { + if (CollectionUtil.isEmpty(addQuestionReq.getOptionList())) { + addQuestionReq.setMsg("多选题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() < 2) { + addQuestionReq.setMsg("多选题选项不能少于2个"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() > 8) { + addQuestionReq.setMsg("选项不能大于8个"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() > 8) { + addQuestionReq.setMsg("选项不能大于8个"); + addQuestionReq.setSuccess(1); + return; + } + int num = 0; + for (QuestionOptionReq optionReq : addQuestionReq.getOptionList()) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num < 2) { + addQuestionReq.setMsg("多选题正确答案不能小于2个"); + addQuestionReq.setSuccess(1); + return; + } + + } else if (CourseEnums.QuestionType.ONE_OR_MULTI.getCode().equals(addQuestionReq.getType())) { + if (CollectionUtil.isEmpty(addQuestionReq.getOptionList())) { + addQuestionReq.setMsg("不定向选择题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() < 2) { + addQuestionReq.setMsg("不定向选择题选项不能少于2个"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() > 8) { + addQuestionReq.setMsg("选项不能大于8个"); + addQuestionReq.setSuccess(1); + return; + } + int num = 0; + for (QuestionOptionReq optionReq : addQuestionReq.getOptionList()) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num < 1) { + addQuestionReq.setMsg("不定项选择题正确答案不能小于1个"); + addQuestionReq.setSuccess(1); + return; + } + + } else if (CourseEnums.QuestionType.SINGLE.getCode().equals(addQuestionReq.getType())) { + if (CollectionUtil.isEmpty(addQuestionReq.getOptionList())) { + addQuestionReq.setMsg("单选题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() < 2) { + addQuestionReq.setMsg("单选题选项不能少于2个"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() > 8) { + addQuestionReq.setMsg("选项不能大于8个"); + addQuestionReq.setSuccess(1); + return; + } + int num = 0; + for (QuestionOptionReq optionReq : addQuestionReq.getOptionList()) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num != 1) { + addQuestionReq.setMsg("单选题正确答案应该只有1个"); + addQuestionReq.setSuccess(1); + return; + } + } + else if (CourseEnums.QuestionType.JUDGE.getCode().equals(addQuestionReq.getType())) { + if (CollectionUtil.isEmpty(addQuestionReq.getOptionList())) { + addQuestionReq.setMsg("判断题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (addQuestionReq.getOptionList().size() != 2) { + addQuestionReq.setMsg("判断题选项选项只能是2个"); + addQuestionReq.setSuccess(1); + return; + } + + int num = 0; + for (QuestionOptionReq optionReq : addQuestionReq.getOptionList()) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num >1) { + addQuestionReq.setMsg("判断题正确答案应该只有1个"); + addQuestionReq.setSuccess(1); + return; + } + if (num ==0) { + addQuestionReq.setMsg("判断题必须有1个正确答案"); + addQuestionReq.setSuccess(1); + return; + } + } + + + List optionList = addQuestionReq.getOptionList(); + if(CollectionUtil.isNotEmpty(optionList)){ + for (QuestionOptionReq optionReq : optionList) { + if(StringUtils.isNotEmpty(optionReq.getContent())){ + if(optionReq.getContent().length()>200){ + addQuestionReq.setMsg("题目选项的内容不能大于200个字符"); + addQuestionReq.setSuccess(1); + return; + } + } + } + } + } + + public static int countUnderscoresConsideringAdjacentAsOne(String str) { + // 使用正则表达式替换连续的下划线为单个下划线 + String normalizedStr = str.replaceAll("_+", "_"); + // 遍历处理后的字符串,计算下划线数量 + int count = 0; + for (int i = 0; i < normalizedStr.length(); i++) { + if (normalizedStr.charAt(i) == '_') { + count++; + } + } + return count; + } + + public static String qeneralQuestionImportKey() { + String loginUserId = UserProvider.getLoginUserId(); + String tenantId = getTenantId(); + return String.format(FTB_QUESTION_IMPORT_SHEET_KEY, tenantId, loginUserId); + } + + public static List getOrgIds(List workerGroupDataDtoList) { + if (CollectionUtil.isEmpty(workerGroupDataDtoList)) { + return new ArrayList<>(); + } + + List list = new ArrayList<>(); + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtoList) { + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedOrg())) { + list.add(workerGroupDataDto.getAffiliatedOrg()); + } + } + return list; + } + + public List uniqueStringList(List list) { + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream() + .filter(Objects::nonNull) // 过滤掉 null 值 + .filter(str -> !str.isEmpty()) // 过滤掉空字符串 + .distinct() // 去重 + .collect(Collectors.toList()); // 收集为 List + } + + public static List stringToList(String str, String split) { + if (StringUtils.isEmpty(str)) { + return new ArrayList<>(); + } + return Arrays.asList(str.split(split)); + } + + public static List convertErrorQuestionList(List errorList) { + List list = new ArrayList<>(); + for (ExcelImportQuestionResultReq userQuestionVo : errorList) { + ExcelUserQuestionVo excelUserQuestionVo = toErrorExcelUserQuestionVo(userQuestionVo); + list.add(excelUserQuestionVo); + } + return list; + } + + private static ExcelUserQuestionVo toErrorExcelUserQuestionVo(ExcelImportQuestionResultReq userQuestionVo) { + ExcelUserQuestionVo vo = new ExcelUserQuestionVo(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.fromCode(userQuestionVo.getType()); + if (null != type) { + vo.setType(type.getDesc()); + } + + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.fromCode(userQuestionVo.getDifficulty()); + if (null != difficulty) { + vo.setDifficulty(difficulty.getDesc()); + } + List questionOptionVoList = new ArrayList<>(); + switch (type) { + case SINGLE://单选 + case JUDGE://判断 + case MULTI://多选 + case ONE_OR_MULTI://不定项 + questionOptionVoList = userQuestionVo.getOptionList(); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + List rightAnsters = new ArrayList<>(); + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setErrorOptionValue(questionOptionVoList.get(i), vo, i); + if (StringUtils.isNotEmpty(right)) { + rightAnsters.add(right); + } + } + vo.setUserAnswer(StringUtils.join(rightAnsters, ",")); + } + break; + case FILL://填空题 + questionOptionVoList = userQuestionVo.getOptionList(); + if (CollectionUtil.isNotEmpty(questionOptionVoList)) { + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setErrorOptionValue(questionOptionVoList.get(i), vo, i); + } + } + break; + + } + + vo.setContent(userQuestionVo.getContent()); + vo.setAnalysis(userQuestionVo.getAnalysis()); + + return vo; + } + + public static void checkAddQuestionParam(AddQuestionReq req) { + ExcelImportQuestionResultReq excelImportQuestionResultReq = BeanUtil.copyProperties(req, ExcelImportQuestionResultReq.class); + checkOneQuestion(excelImportQuestionResultReq); + if (excelImportQuestionResultReq.getSuccess() == 1) { + throw new RuntimeException(excelImportQuestionResultReq.getMsg()); + } + } + + public static void checkEditQuestionParam(EditQuestionReq req) { + ExcelImportQuestionResultReq excelImportQuestionResultReq = BeanUtil.copyProperties(req, ExcelImportQuestionResultReq.class); + checkOneQuestion(excelImportQuestionResultReq); + if (excelImportQuestionResultReq.getSuccess() == 1) { + throw new RuntimeException(excelImportQuestionResultReq.getMsg()); + } + } + + public static String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserApiV2Util.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserApiV2Util.java new file mode 100644 index 0000000..8b95c4a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserApiV2Util.java @@ -0,0 +1,1960 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.fantaibao.permission.handling.PermissionHandling; +import jnpf.authority.mapper.FtbPermissionRoleMapper; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.vo.common.InnerPowerPositionVO; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.permission.*; +import jnpf.permission.dto.v2.grades.QueryGradeListDTO; +import jnpf.permission.dto.v2.organzie.AuthOrganizeUserNodeDTO; +import jnpf.permission.dto.v2.organzie.QueryOrganizeListTargetTypesDTO; +import jnpf.permission.dto.v2.position.QueryBasePositionBatchDTO; +import jnpf.permission.dto.v2.position.QueryPagePositionDTO; +import jnpf.permission.dto.v2.position.QueryPositionListDetailGradesDTO; +import jnpf.permission.dto.v2.position.QueryPositionUserListDTO; +import jnpf.permission.dto.v2.user.QueryListUserDTO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.dto.v2.user.QueryUserBoundDTO; +import jnpf.permission.entity.UserCopyEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.model.user.BatchGetUserCopyForm; +import jnpf.permission.model.user.UserInfoVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerFilterNodeVO; +import jnpf.permission.vo.v2.organzie.OrganizeManagerNodeVO; +import jnpf.permission.vo.v2.organzie.PositionIdBoundOrganizeInfoListVO; +import jnpf.permission.vo.v2.position.*; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.permission.vo.v2.user.UserRelationBaseVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +@Slf4j +@Component +public class UserApiV2Util { + @Autowired + private V2UserApi v2UserApi; + + @Autowired + private UserApi userApi; + + @Autowired + private V2PositionApi v2PositionApi; + + @Autowired + private V2GradesApi v2GradesApi; + + @Autowired + private V2OrganizeApi v2OrganizeApi; + + @Autowired + private PermissionHandling permissionHandling; + + @Autowired + @Lazy + private PermissionsUtils permissionsUtils; + + @Autowired + @Lazy + private FtbPermissionOrganizeService ftbPermissionOrganizeService; + + @Autowired + private FtbPersonnelsStaffRosterMapper rosterMapper; + + @Autowired + private FtbPermissionRoleMapper ftbPermissionRoleMapper; + + @Autowired + private ConfigValueUtil configValueUtil; + + /** + * 去掉时间的 小时 分钟秒数 00:00:00 + * + * @param time + * @return + */ + public static Date clearHourMinuteSecond(Date time) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(time); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } + + /** + * 添加时间 补齐 23:59:59 + * + * @param time + * @return + */ + public static Date addHourMinuteSecond(Date time) { + Calendar endCalendar = Calendar.getInstance(); + endCalendar.setTime(time); + endCalendar.set(Calendar.HOUR_OF_DAY, 23); + endCalendar.set(Calendar.MINUTE, 59); + endCalendar.set(Calendar.SECOND, 59); + endCalendar.set(Calendar.MILLISECOND, 999); + return endCalendar.getTime(); + } + + + /** + * 查询当前登录用户有权限的岗位列表 + * + * @param tenantId + * @return + */ + public InnerPowerPositionVO getPositionListForCurrUser(String tenantId) { + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + InnerPowerPositionVO vo = new InnerPowerPositionVO(); + InnerPowerUserVO innerPowerUserVO = getLoginManagerUserIdsForEncode(); + vo.setCode(innerPowerUserVO.getCode()); + if (innerPowerUserVO.getCode().equals(1)) { + + List userPrimaryBoundBatchReturnList = getUserPrimaryBoundBatchReturnList(innerPowerUserVO.getUserIds(), tenantId); + if (CollUtil.isEmpty(userPrimaryBoundBatchReturnList)) { + vo.setIds(new ArrayList<>()); + return vo; + } + + vo.setIds(uniqueStringList(userPrimaryBoundBatchReturnList.stream().map(UserBoundVO::getPositionId).collect(Collectors.toList()))); + + } else if (innerPowerUserVO.getCode().equals(2)) { + return vo; + } + return vo; + } + + /** + * 查询当前用户有权限的组织列表 + * + * @param organizeCategoryEnums + * @param tenantId + * @return + */ + public List getOrgListForCurrUser(List organizeCategoryEnums, String tenantId) { + return ftbPermissionOrganizeService.authOrganizesByUserBound(organizeCategoryEnums, null, false, false); + } + + public List getOrgListTreeForCurrUser(List organizeCategoryEnums) { + return ftbPermissionOrganizeService.listOrganizeTreeUsersFilterNodeJson(organizeCategoryEnums, new ArrayList<>(), false, false, false); + } + + /** + * 获取当前登录用户是否有全部权限 + * + * @return true表示有全部权限,false表示没有全部权限 + */ + public boolean hasAllPermission() { + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + return true; + } + return false; + } + + + public List getTopOrgList(List tree) { + List ret = new ArrayList<>(); + if (CollUtil.isEmpty(tree)) { + return ret; + } + for (OrganizeManagerFilterNodeVO organizeManagerFilterNodeVO : tree) { + List topOrg1 = getTopOrg1(organizeManagerFilterNodeVO); + if (CollUtil.isNotEmpty(topOrg1)) { + ret.addAll(topOrg1); + } + } + return ret; + } + + private List getTopOrg1(OrganizeManagerFilterNodeVO vo) { + List ids = new ArrayList<>(); + if (vo.getIsEnabled()) { + ids.add(vo.getId()); + return ids; + } + List children = vo.getChildren(); + if (CollUtil.isEmpty(children)) { + return null; + } + for (OrganizeManagerFilterNodeVO child : children) { + List topOrg = getTopOrg1(child); + if (CollUtil.isNotEmpty(topOrg)) { + ids.addAll(topOrg); + } + } + return ids; + + } + + /** + * 查询登录用户的数据权限,返回可以管理的用户列表 + * + * @return 返回有权限的用户信息 + */ + + public InnerPowerUserVO getLoginManagerUserIds() { + return getLoginManagerUserIdsForEncode(); + } + + /** + * 根据权限编码查询数据权限 + * + * @return 返回有权限的用户信息 + */ + public InnerPowerUserVO getLoginManagerUserIdsForEncode() { + InnerPowerUserVO vo = new InnerPowerUserVO(); + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + vo.setCode(0); + return vo; + } + try { + //往header中添加一个字段 + List userIds = permissionHandling.getUserIdsByUserId(userInfo.getUserId()); + // 权限适用范围为全部 + if (userIds == null) { + vo.setCode(0); + return vo; + } + if (userIds.isEmpty()) { + vo.setCode(2); + return vo; + } + vo.setCode(1); + vo.setUserIds(userIds); + } catch (Exception e) { + log.error("获取用户权限失败:", e); + vo.setCode(2); + return vo; + } + return vo; + } + + + /** + * 根据权限编码查询数据权限配置类型 header 找那个自动取module + * + * @return 0-全部 1-所在组织和下级组织员工 2-所在组织员工 3-仅下属 4-指定组织 -1-无效权限信息 + */ + public Integer getPermissionModuleType() { + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + return 0; + } + try { + PermissionsApplicableObject res = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userInfo.getUserId()); + return Integer.valueOf(res.getPermissionsApplicableEnums().getValue()); + } catch (Exception e) { + return -1; + } + } + + + /** + * 分页查询获取所有用户列表 + * + * @param tenantId + * @return + */ + public PageListVO listAllUserForPage(Long pageSize, Long currentPage, String keyWords, String tenantId) { + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setTenantId(tenantId); + if (pageSize == null || currentPage == 0) { + dto.setPageSize(10L); + } else { + dto.setPageSize(pageSize); + } + if (currentPage == null || currentPage <= 1) { + dto.setCurrentPage(1L); + } else { + dto.setCurrentPage(currentPage); + } + if (StringUtil.isNotEmpty(keyWords)) { + dto.setKeyword(keyWords); + } + + ActionResult> pageListVOActionResult = v2UserApi.pagePost(dto); + if (pageListVOActionResult == null) { + return null; + } + return pageListVOActionResult.getData(); + } + + + /** + * 获取当前登录的用户ID + * + * @return + */ + public String getCurrentLoginUserId() { + return UserProvider.getUser().getUserId(); + } + + /** + * 获取当前登录的用户ID + * + * @return 租户id + */ + public String getCurrentLoginTenantId() { + return UserProvider.getUser().getTenantId(); + } + + public PageListVO pageListUserForWhere(QueryPageUserDTO dto) { + + ActionResult> pageListVOActionResult = v2UserApi.pagePost(dto); + if (pageListVOActionResult == null) { + return null; + } + return pageListVOActionResult.getData(); + } + + /** + * 获取当前登录用户是否有全部权限 + * + * @return true表示继续,false表示不继续 + */ + public boolean checkIsContinue(List list) { + if (CollUtil.isEmpty(list)) { + return true; + } + if (list.size() == 1 && "-1".equals(list.get(0))) { + return false; + } + return true; + } + + /** + * 根据用户ids批量查询用户信息、组织、岗位、职级 + * 此方法适用于需要获取多个用户详细信息的场景,通过用户ID列表作为参数, + * 返回一个映射,其中每个用户ID对应一个包含用户详细信息的UserPrimaryBoundVO对象 + * + * @param userIds 用户ID列表,用于查询用户信息 + * @return 返回一个Map,键为用户ID,值为对应的UserPrimaryBoundVO对象 + */ + public Map getUserPrimaryBoundBatch(List userIds, String tenantId) { + Map map = new HashMap<>(); + if (CollUtil.isEmpty(userIds)) { + return map; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, tenantId); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return map; + } + for (UserBoundVO userPrimaryBoundVO : userPrimaryBoundBatch.getData()) { + map.put(userPrimaryBoundVO.getId(), userPrimaryBoundVO); + } + return map; + } + + public List getAllUserList(String tenantId) { + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryUserBatchDTO dto = new QueryUserBatchDTO(); + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2UserApi.getUserInfoBatch(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + public List getAllUserListPage(String tenantId) { + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryUserBatchDTO dto = new QueryUserBatchDTO(); + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2UserApi.getUserInfoBatch(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据用户名或者手机号查询用户 + * + * @param userName + * @param mobilePhone + * @param tenantId + * @return + */ + + public List getUserForNameOrPhone(String userName, String mobilePhone, String tenantId) { + QueryListUserDTO dto = new QueryListUserDTO(); + if (StringUtil.isEmpty(userName) && StringUtil.isEmpty(mobilePhone)) { + return new ArrayList<>(); + } + if (StringUtil.isNotEmpty(userName)) { + dto.setUserName(userName); + } + if (StringUtil.isNotEmpty(mobilePhone)) { + dto.setMobilePhone(mobilePhone); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2UserApi.listUser(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + public List searchUser(QueryListUserDTO dto) { + ActionResult> listActionResult = v2UserApi.listUser(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据用户ids批量查询用户信息、组织、岗位、职级 返回list + * 此方法适用于需要获取多个用户详细信息的场景,通过用户ID列表作为参数, + * 返回一个映射,其中每个用户ID对应一个包含用户详细信息的UserPrimaryBoundVO对象 + * + * @param userIds 用户ID列表,用于查询用户信息 + * @return 返回一个list + */ + public List getUserPrimaryBoundBatchReturnList(List userIds, String tenantId) { + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, tenantId); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return new ArrayList<>(); + } + return userPrimaryBoundBatch.getData(); + } + + /** + * 获取用户的主要绑定信息之一 + * 该方法用于调用API接口,获取指定用户的主要绑定信息,如果用户ID为空或API调用失败,将返回null + * + * @param userId 用户ID,用于查询用户的主要绑定信息 + * @return UserBoundVO 返回用户的绑定信息对象,如果没有找到或发生错误,则返回null + */ + public UserBoundVO getUserPrimaryBoundOne(String userId, String tenantId) { + + if (StringUtil.isEmpty(userId)) { + return null; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), tenantId); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return null; + } + return userPrimaryBoundBatch.getData().get(0); + } + + /** + * 根据用户ids批量查询用户信息、组织、岗位、职级 + * 与上一个方法不同的是,这个方法返回的映射中,每个用户ID对应的是一个UserPrimaryBoundVO对象列表 + * 这种设计是为了在将来可能的扩展性,比如一个用户可能属于多个组织或岗位的情况 + * + * @param userIds 用户ID列表,用于查询用户信息 + * @return 返回一个Map,键为用户ID,值为一个列表,包含对应的UserPrimaryBoundVO对象 + */ + public Map> getUserPrimaryBoundBatchCompatible(List userIds, String tenantId) { + Map> map = new HashMap<>(); + if (CollUtil.isEmpty(userIds)) { + return map; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, tenantId); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return map; + } + for (UserBoundVO userPrimaryBoundVO : userPrimaryBoundBatch.getData()) { + map.put(userPrimaryBoundVO.getId(), List.of(userPrimaryBoundVO)); + } + return map; + } + + + /** + * 根据用户ids批量查询用户信息、组织、岗位、职级 + * 与上一个方法不同的是,这个方法返回的映射中,每个用户ID对应的是一个UserPrimaryBoundVO对象列表 + * 这种设计是为了在将来可能的扩展性,比如一个用户可能属于多个组织或岗位的情况 + * + * @param userIds 用户ID列表,用于查询用户信息 + * @return 返回一个Map,键为用户ID,值为一个列表,包含对应的UserPrimaryBoundVO对象 + */ + public Map getUserPrimaryBoundBatchReturnMap(List userIds, String tenantId) { + Map map = new HashMap<>(); + if (CollUtil.isEmpty(userIds)) { + return map; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, tenantId); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return map; + } + for (UserBoundVO userPrimaryBoundVO : userPrimaryBoundBatch.getData()) { + map.put(userPrimaryBoundVO.getId(), userPrimaryBoundVO); + } + return map; + } + + /** + * 根据组织ID列表获取用户列表 + *

+ * 此方法旨在通过提供的组织ID列表,调用API接口以获取相关的用户列表 + * 它首先检查输入的组织ID列表是否为空,如果为空,则直接返回一个空的用户列表 + * 接着,它调用V2用户API中的方法来获取与指定组织ID相关的用户列表 + * 最后,检查API返回的结果是否为空,如果为空或不存在,则返回空用户列表; + * 否则,返回获取到的用户列表 + * + * @param orgIds 组织ID列表,用于查询用户信息 + * @return 用户列表,包含与指定组织ID相关的用户信息 + */ + public List getUserListForOrgIds(List orgIds, String tenantId) { + List userList = new ArrayList<>(); + if (CollUtil.isEmpty(orgIds)) { + return userList; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2UserApi.listTargetOrganizes(orgIds, tenantId, null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return userList; + } + return listActionResult.getData(); + } + + /** + * 查询所有组织并返回树结构 + * + * @param tenantId 租户ID + * @return 组织树结构 + */ + public List queryAllOrgReturnTree(String tenantId) { + AuthOrganizeUserNodeDTO dto = new AuthOrganizeUserNodeDTO(); + dto.setHavaAuth(false); + dto.setHaveUsers(false); + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + dto.setTenantId(tenantId); + String s = v2OrganizeApi.listOrganizeTreeUsersJson(dto); + + if (StringUtil.isEmpty(s)) { + return new ArrayList<>(); + } + try { + return JSON.parseArray(s, OrganizeManagerNodeVO.class); + } catch (Exception e) { + log.error("JSON.parseArray error", e); + } + return new ArrayList<>(); + } + + /** + * 根据组织ID列表获取用户列表,并按组织ID分组返回Map + * + * @param orgIds 组织ID列表 + * @param tenantId 租户ID,如果为空则使用当前用户所属租户 + * @return 以组织ID为key,该组织下用户列表为value的Map + */ + public Map> getUserListForOrgIdsReturnMap(List orgIds, String tenantId) { + + if (CollUtil.isEmpty(orgIds)) { + return new HashMap<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2UserApi.listTargetOrganizes(orgIds, tenantId, null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new HashMap<>(); + } + return listActionResult.getData().stream().collect(Collectors.groupingBy(UserPageListVO::getOrganizeId)); + } + + /** + * 将用户列表转换为组织ID到用户数量的映射 + * + * @param list 用户列表 + * @return 以组织ID为key,该组织下用户数量为value的Map + */ + public Map convertUserCount(List list) { + + // 如果用户列表为空,直接返回一个空的Map + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } + + // 使用 stream 直接分组并统计数量 + // 这里通过组织ID进行分组,并计算每个组的数量,结果是一个Map,键是组织ID,值是用户数量 + return list.stream().collect( + Collectors.groupingBy( + UserPageListVO::getOrganizeId, + HashMap::new, + Collectors.summingLong(v -> 1) + ) + ); + } + + /** + * 将用户列表转换为组织ID到用户ID列表的映射 + * + * @param userList 用户列表 + * @return 以组织ID为key,该组织下用户ID列表为value的Map + */ + public Map> convertUserIdList(List userList) { + Map> map = new HashMap<>(); + // 如果用户列表为空,直接返回一个空的Map + if (CollUtil.isEmpty(userList)) { + return map; + } + for (UserPageListVO userPageListVO : userList) { + List strings = map.get(userPageListVO.getOrganizeId()); + if (CollUtil.isEmpty(strings)) { + strings = new ArrayList<>(); + } + strings.add(userPageListVO.getId()); + map.put(userPageListVO.getOrganizeId(), strings); + } + return map; + } + + /** + * 根据组织id查询用户列表【是否查询下级组织的人员】 + * + * @param orgIds 组织ID列表 + * @param haveOrganizeChild true-查询该组织及下级组织所有用户, false-只查询直接组织的用户 + * @param tenantId 租户ID,如果为空则使用当前用户所属租户 + * @return 组织下的用户列表 + */ + public List listTargetOrganizesOrHaveChild(List orgIds, Boolean haveOrganizeChild, String tenantId) { + List userList = new ArrayList<>(); + if (CollUtil.isEmpty(orgIds)) { + return userList; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2UserApi.listTargetOrganizesOrHaveChild(orgIds, haveOrganizeChild, null, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return userList; + } + return listActionResult.getData(); + } + + + /** + * 获取下属目标用户列表 + *

+ * 该方法旨在查询与给定用户ID关联的所有下属用户信息这些用户被视为目标用户, + * 通常用于管理或数据展示场景中此方法首先检查输入的用户ID是否为空, + * 如果为空则直接返回一个空的用户列表如果用户ID有效,它将调用一个API来获取用户列表, + * 并在列表为空或获取失败时返回空列表 + * + * @param userId 用户ID,用于查询其下属目标用户 + * @return 包含下属目标用户的列表如果输入无效或查询无结果,则返回空列表 + */ + public List listUnderlingTargetUser(String userId, String tenantId) { + List userList = new ArrayList<>(); + if (StringUtil.isEmpty(userId)) { + return userList; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2UserApi.listUnderlingTargetUser(userId, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return userList; + } + return listActionResult.getData(); + } + + /** + * 根据用户id查询直属领导 + * + * @param userId 用户id + * @param tenantId 租户ID,如果为空则使用当前用户所属租户 + * @return 用户的直属领导信息,如果未找到则返回null + */ + public UserInfoVO getBossByUserId(String userId, String tenantId) { + if (StringUtil.isEmpty(userId)) { + return null; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return null; + } + UserBoundVO userBoundVO = listActionResult.getData().get(0); + ActionResult info = userApi.getInfo(userBoundVO.getLeaderId()); + if (info == null || info.getData() == null) { + return null; + } + + return info.getData(); + } + + /** + * 根据用户id查询用户绑定信息(包含直属领导信息) + * + * @param userId 用户id + * @param tenantId 租户ID,如果为空则使用当前用户所属租户 + * @return 用户绑定信息,包含直属领导信息,如果未找到则返回null + */ + public UserBoundVO getBossByUserIdSimple(String userId, String tenantId) { + if (StringUtil.isEmpty(userId)) { + return null; + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return null; + } + return listActionResult.getData().get(0); + + } + + /** + * 根据岗位id查询用户列表 + * 此方法用于批量查询与给定岗位ID列表相关的用户信息 + * 它首先检查岗位ID列表是否为空,如果为空,则直接返回一个空列表 + * 然后,它构造一个查询条件对象,包括岗位ID和租户ID,然后调用API获取用户信息 + * 如果API调用结果为空或不包含数据,则返回一个空列表,否则返回查询到的用户列表 + * + * @param positionIds 岗位ID列表,用于查询用户信息 + * @return 用户信息列表,如果查询失败或无数据,则返回空列表 + */ + public List getUserListForPositions(List positionIds, String tenantId) { + if (StringUtil.isEmpty(positionIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryUserBatchDTO dto = new QueryUserBatchDTO(); + dto.setPositionIds(positionIds); + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2UserApi.getUserInfoBatch(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据岗位名称查询用户列表 + * + * @param positionName 岗位名称 + * @return 用户信息列表,如果查询失败或无数据,则返回空列表 + */ + public List getUserListForPositionName(String positionName, String tenantId) { + if (StringUtil.isEmpty(positionName)) { + return new ArrayList<>(); + } + QueryUserBoundDTO dto = new QueryUserBoundDTO(); + dto.setPositionName(positionName); + ActionResult> listActionResult = v2UserApi.listUserBound(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织id 和 岗位id 查询用户列表 + * + * @param orgId 组织id + * @param positionId 岗位id + * @return 用户信息列表,如果查询失败或无数据,则返回空列表 + */ + public List getUserListForOrgIdAndPositionId(String orgId, String positionId, String tenantId) { + + QueryUserBoundDTO dto = new QueryUserBoundDTO(); + dto.setOrganizeId(orgId); + dto.setPositionId(positionId); + ActionResult> listActionResult = v2UserApi.listUserBound(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + + /** + * 根据岗位id查询岗位信息 + * + * @param positionId 岗位id + */ + public PositionVO infoPosition(String positionId, String tenantId) { + if (StringUtil.isEmpty(positionId)) { + return null; + } + ActionResult positionVOActionResult = v2PositionApi.infoPosition(positionId); + if (positionVOActionResult == null || positionVOActionResult.getData() == null) { + return null; + } + return positionVOActionResult.getData(); + } + + /** + * 根据组织id查询岗位信息列表 + * + * @param orgIds 组织id集合 + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 组织下的岗位信息列表 + */ + public List listPositionBaseInfoByIds(List orgIds, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryBasePositionBatchDTO dto = new QueryBasePositionBatchDTO(); + dto.setOrganizeIds(orgIds); + dto.setTenantId(tenantId); + + ActionResult> listActionResult = v2PositionApi.listPositionBaseInfo(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织id查询岗位信息列表 + * + * @param orgId 组织id + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 组织下的岗位信息列表 + */ + public List listPositionBaseInfoById(String orgId, String tenantId) { + if (StringUtil.isEmpty(orgId)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryBasePositionBatchDTO dto = new QueryBasePositionBatchDTO(); + dto.setOrganizeIds(List.of(orgId)); + dto.setTenantId(tenantId); + + ActionResult> listActionResult = v2PositionApi.listPositionBaseInfo(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织id查询岗位信息列表 返回map + * + * @param orgIds 组织id集合 + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 以组织ID为key,岗位信息列表为value的Map + */ + public Map> listPositionBaseInfoByIdsReturnMap(List orgIds, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new HashMap<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + + Map> map = new HashMap<>(); + for (String orgId : orgIds) { + List positionBaseInfoVOS = listPositionBaseInfoById(orgId, tenantId); + map.put(orgId, positionBaseInfoVOS); + } + return map; + } + + /** + * 根据组织id查询岗位信息列表 返回 key=组织id value-岗位id列表 + * + * @param orgIds 组织id集合 + * @param tenantId 租户id + * @return 返回组织id 为key,value 是岗位id列表 + */ + public Map> listPositionBaseInfoByIdsReturnMapSimple(List orgIds, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new HashMap<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + Map> stringListMap = batchQueryPositionForOrgIdsReturnMap(orgIds, tenantId); + if (stringListMap.isEmpty()) { + return new HashMap<>(); + } + Map> map = new HashMap<>(); + for (String s : stringListMap.keySet()) { + map.put(s, stringListMap.get(s).stream().map(PositionBoundOrganizeVO::getId).collect(Collectors.toList())); + } + return map; + } + + /** + * 根据岗位ids查询岗位信息 + * + * @param positionIds 岗位id集合 + */ + public List listPositionDetailInfoByIds(List positionIds, String tenantId) { + if (CollUtil.isEmpty(positionIds)) { + return new ArrayList<>(); + } + ActionResult> listActionResult = v2PositionApi.listPositionDetailInfo(positionIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据岗位ids查询岗位信息 + * + * @param positionIds 岗位id集合 + * @return map<岗位id , 岗位信息> + */ + public Map listPositionDetailInfoByIdsReturnMap(List positionIds, String tenantId) { + Map map = new HashMap<>(); + if (CollUtil.isEmpty(positionIds)) { + return map; + } + ActionResult> listActionResult = v2PositionApi.listPositionDetailInfo(positionIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return map; + } + return listActionResult.getData().stream().collect(Collectors.toMap(PositionVO::getId, item -> item)); + } + + /** + * 根据岗位名称模糊匹配岗位列表 + * + * @param positionName 岗位名称 + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 匹配的岗位及职级信息列表 + */ + public List likeQueryPositionForPositionName(String positionName, String tenantId) { + if (StringUtil.isEmpty(positionName)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryPositionListDetailGradesDTO dto = new QueryPositionListDetailGradesDTO(); + dto.setLikePositionName(positionName); + dto.setIsAll(false); + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2PositionApi.listPositionDetailGradesConcreteInfo(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 分页查询岗位列表 + * + * @param dto 查询岗位的分页参数 + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 岗位列表的分页结果 + */ + public ActionResult> pageIncludingRankPositions(QueryPagePositionDTO dto, String tenantId) { + return v2PositionApi.pageIncludingRankPositions(dto); + } + + /** + * 根据编码列表查询岗位信息列表 + * + * @param positionEncodeLists 岗位编码列表 + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 岗位信息列表 + */ + public List listPositionBaseInfoByEncodes(List positionEncodeLists, String tenantId) { + if (StringUtil.isEmpty(positionEncodeLists)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + + ActionResult> listActionResult = v2PositionApi.listPositionBaseInfoByEncodes(positionEncodeLists, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织编码列表查询组织信息列表 + * + * @param OrgEncodeLists 组织编码列表 + * @param tenantId 租户id,如果为空则使用当前用户所属租户 + * @return 组织信息列表 + */ + public List organizesOrHaveChildByOrganizeEncodes(List OrgEncodeLists, String tenantId) { + if (StringUtil.isEmpty(OrgEncodeLists)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeEncodes(OrgEncodeLists, false, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织ID列表批量查询岗位列表 + * + * @param orgIds 组织ID列表 + * @param tenantId 租户ID,如果为空则使用当前用户所属租户 + * @return 组织id为key,岗位绑定组织信息列表为value的Map + */ + public Map> batchQueryPositionForOrgIdsReturnMap(List orgIds, String tenantId) { + if (StringUtil.isEmpty(orgIds)) { + return new HashMap<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2PositionApi.listPositionBoundInfoOrganizeIds(orgIds, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new HashMap<>(); + } + return listActionResult.getData().stream().collect(Collectors.groupingBy(PositionBoundOrganizeVO::getOrganizeId)); + } + + /** + * 根据组织ID列表批量查询岗位绑定的组织信息 + * + * @param orgIds 组织ID列表 + * @param tenantId 租户ID,如果为空则使用当前用户所属租户 + * @return 岗位绑定组织信息列表 + */ + public List batchQueryPositionForOrgIdsReturnList(List orgIds, String tenantId) { + if (StringUtil.isEmpty(orgIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2PositionApi.listPositionBoundInfoOrganizeIds(orgIds, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据岗位id查询职级信息 + * + * @param gradeId 职级id + * @param tenantId 租户id + * @return 指定岗位下的职级列表 + */ + public GradeVO infoGrade(String gradeId, String tenantId) { + if (StringUtil.isEmpty(gradeId)) { + return null; + } + ActionResult result = v2GradesApi.infoGrade(gradeId); + if (result == null || result.getData() == null) { + return null; + } + return result.getData(); + } + + + /** + * 根据职级ids查询职级信息 + * + * @param gradeIds 职级ids集合 + * @param tenantId 租户id + * @return 职级信息列表 + */ + public List listGradeByIds(List gradeIds, String tenantId) { + if (CollUtil.isEmpty(gradeIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2GradesApi.listGradeByIds(gradeIds, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据职级ids查询职级信息返回map + * + * @param gradeIds 职级ids集合 + * @param tenantId 租户id + * @return 职级信息列表 + */ + public Map listGradeByIdsReturnMap(List gradeIds, String tenantId) { + List gradeVOS = listGradeByIds(gradeIds, tenantId); + return gradeVOS.stream().collect(Collectors.toMap(GradeVO::getId, gradeVO -> gradeVO)); + } + + /** + * 根据岗位名称查询所有岗位信息 + * + * @param tenantId 租户id + * @return 岗位信息列表 + */ + public List listAllPositionAndGradesByPositionName(String tenantId) { + + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryPositionListDetailGradesDTO dto = new QueryPositionListDetailGradesDTO(); + dto.setIsAll(true); + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2PositionApi.listPositionDetailGradesConcreteInfo(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据岗位ids 查询岗位 及岗位下的职级信息 + * + * @param tenantId 租户id + * @return 岗位信息列表 + */ + public List listPositionAndGradeByPositionIds(List positionIds, String tenantId) { + + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + QueryPositionListDetailGradesDTO dto = new QueryPositionListDetailGradesDTO(); + dto.setIsAll(false); + dto.setTenantId(tenantId); + dto.setPositionIds(positionIds); + ActionResult> listActionResult = v2PositionApi.listPositionDetailGradesConcreteInfo(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据岗位id查询岗位下的用户列表 + * + * @param positionIds 岗位id集合 + * @param tenantId 租户id + */ + public Map listPositionTreeUser(List positionIds, String tenantId) { + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + Map map = new HashMap<>(); //岗位id的map + QueryPositionUserListDTO dto = new QueryPositionUserListDTO(); + dto.setTenantId(tenantId); + dto.setPositionIds(positionIds); + ActionResult> listActionResult = v2PositionApi.listPositionTreeUser(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return map; + } + return listActionResult.getData().stream().collect(Collectors.toMap(PositionListUserVO::getId, item -> item)); + + } + + /** + * 根据组织id查询岗位和职级信息 + * + * @param orgId 组织id + * @param tenantId 租户id + * @return 岗位列表 + */ + public List listPositionAndGradesByPositionNameForOrgIds(String orgId, String tenantId) { + + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + List positionBaseInfoVOS = listPositionBaseInfoByIds(Collections.singletonList(orgId), tenantId); + if (CollUtil.isEmpty(positionBaseInfoVOS)) { + return new ArrayList<>(); + } + QueryPositionListDetailGradesDTO dto = new QueryPositionListDetailGradesDTO(); + dto.setPositionIds(positionBaseInfoVOS.stream().map(PositionBaseInfoVO::getId).collect(Collectors.toList())); + dto.setIsAll(false); + dto.setTenantId(tenantId); + ActionResult> listActionResult = v2PositionApi.listPositionDetailGradesConcreteInfo(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据岗位id查询职级信息 + * + * @param positionId 岗位id + * @param tenantId 租户id + * @return 岗位下的职级列表 + */ + public List listGrades(String positionId, String tenantId) { + if (StringUtil.isEmpty(positionId)) { + return new ArrayList<>(); + } + + QueryGradeListDTO req = new QueryGradeListDTO(); + req.setPositionId(positionId); + ActionResult> listActionResult = v2GradesApi.listGrades(req); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + + /** + * 根据组织id查询组织详细 + * + * @param orgId 组织id + * @param tenantId 租户id + * @return 组织详情信息 + */ + public OrganizeGeneralDetailVO organizeInfoById(String orgId, String tenantId) { + if (StringUtil.isEmpty(orgId)) { + return null; + } + ActionResult listActionResult = v2OrganizeApi.organizeInfoById(null, orgId); + if (listActionResult == null || listActionResult.getData() == null) { + return null; + } + return listActionResult.getData(); + } + + /** + * 根据组织id查询组织详细列表 + * + * @param orgIds 组织id集合 + * @param tenantId 租户id + * @return 组织详情列表 + */ + public List organizesByOrganizeIds(List orgIds, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new ArrayList<>(); + } + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(orgIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织id查询组织详细列表 + * + * @param orgIds 组织id集合 + * @param tenantId 租户id + * @return 以组织ID为键,组织详情为值的映射表 + */ + public Map organizesByOrganizeIdsReturenMap(List orgIds, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new HashMap<>(); + } + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(orgIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new HashMap<>(); + } + return listActionResult.getData().stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item)); + } + + /** + * 根据组织id查询组织详细列表 + * + * @param orgIds 组织id集合 + * @param haveOrganizeChild 是否包含下级组织 true-包含 false-不包含 + * @param tenantId 租户id + * @return 组织列表 + */ + public List organizesOrHaveChildByOrganizeIds(List orgIds, Boolean haveOrganizeChild, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(orgIds, haveOrganizeChild, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + /** + * 根据组织id查询直接下级组织列表 + * + * @param orgIds 组织id集合 + * @param haveOrganizeChild 是否包含下级组织 true-包含 false-不包含 + * @param tenantId 租户id + * @return 组织ids列表 + */ + public List queryChildOrgForOrgIdsReturnOrgIds(List orgIds, Boolean haveOrganizeChild, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new ArrayList<>(); + } + if (StringUtil.isEmpty(tenantId)) { + tenantId = getCurrentLoginTenantId(); + } + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(orgIds, haveOrganizeChild, tenantId); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData().stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } + + /** + * 根据组织id查询 直接下级组织列表 + * + * @param orgId 组织id + * @param tenantId 租户id + * @return 直接下级组织列表 + */ + public List organizeNextLevel(String orgId, String tenantId) { + if (StringUtil.isEmpty(orgId)) { + return new ArrayList<>(); + } + ActionResult> listActionResult = v2OrganizeApi.organizeNextLevel(List.of(orgId)); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + + } + + /** + * 根据组织id查询 直接下级组织列表 + * + * @param orgIds 组织id + * @param tenantId 租户id + * @return 直接下级组织列表 + */ + public List batchOrganizeNextLevel(List orgIds, String tenantId) { + if (CollUtil.isEmpty(orgIds)) { + return new ArrayList<>(); + } + ActionResult> listActionResult = v2OrganizeApi.organizeNextLevel(orgIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + + } + + + /** + * 根据条件获取组织列表 + * + * @param orgName 组织名称 + * @param orgIds 组织id + * @param tenantId 租户id + */ + public List getOrgListByWhere(String orgName, List orgIds, List organizeCategoryEnums, String tenantId) { + QueryOrganizeListTargetTypesDTO dto = new QueryOrganizeListTargetTypesDTO(); + + if (StringUtil.isNotEmpty(orgName)) { + dto.setOrganizeName(orgName); + } + if (CollUtil.isNotEmpty(orgIds)) { + dto.setOrganizeIds(orgIds); + } + if (CollUtil.isNotEmpty(organizeCategoryEnums)) { + dto.setOrganizeCategoryEnums(organizeCategoryEnums); + } else { + dto.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.TEAM)); + } + ActionResult> listActionResult = v2OrganizeApi.listOrganizeByTargetTypes(dto); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + + /** + * 根据岗位ids查询组织ids + * + * @param positionIds + * @param tenantId 返回组织列表 + */ + public List organizeInfoListByPositionIds(List positionIds, String tenantId) { + if (StringUtil.isEmpty(positionIds)) { + return new ArrayList<>(); + } + List listActionResult = v2OrganizeApi.organizeInfoListByPositionIds(positionIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult)) { + return new ArrayList<>(); + } + return listActionResult; + } + + /** + * 根据岗位ids查询组织信息,返回 Map<岗位id, List<组织信息>> + * + * @param positionIds + * @param tenantId + * @return Map<岗位id, List < 组织信息>> + */ + public Map> organizeInfoBoundListByPositionIds(List positionIds, String tenantId) { + if (StringUtil.isEmpty(positionIds)) { + return new HashMap<>(); + } + List listActionResult = v2OrganizeApi.organizeInfoBoundListByPositionIds(positionIds); + if (listActionResult == null || CollUtil.isEmpty(listActionResult)) { + return new HashMap<>(); + } + Map> map = new HashMap<>(); + for (PositionIdBoundOrganizeInfoListVO positionIdBoundOrganizeInfoListVO : listActionResult) { + map.put(positionIdBoundOrganizeInfoListVO.getPositionId(), positionIdBoundOrganizeInfoListVO.getOrganizeGeneralDetailVOList()); + } + return map; + } + + + /** + * 对字符串列表进行去重处理 + * + * @param list 待处理的字符串列表 + * @return 去除null值、空字符串和重复项后的新列表 + */ + public static List uniqueStringList(List list) { + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream() + .filter(Objects::nonNull) // 过滤掉 null 值 + .filter(str -> !str.isEmpty()) // 过滤掉空字符串 + .distinct() // 去重 + .collect(Collectors.toList()); // 收集为 List + } + + /** + * 获取两个List的交集 + * + * @param list1 第一个列表 + * @param list2 第二个列表 + * @return 交集列表 + */ + public static List getIntersection(List list1, List list2) { + if (CollUtil.isEmpty(list1) || CollUtil.isEmpty(list2)) { + return new ArrayList<>(); + } + + Set set2 = new HashSet<>(list2); + return list1.stream() + .filter(s -> s != null && set2.contains(s)) + .distinct() + .collect(Collectors.toList()); + } + + + /** + * 根据用户ID列表查询用户信息 + * + * @param userIds 用户ID列表 + * @return 以用户ID为键,用户实体为值的映射表 + */ + public Map getUserNameForUserIds(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + List list = userApi.getUserName(userIds); + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } + return list.stream().collect(Collectors.toMap(UserEntity::getId, Function.identity())); + + } + + + /** + * 根据用户ID列表查询用户姓名信息(离职+在职) + * + * @param userIds 用户Ids列表 + * @return 以用户ID为键,用户实体为值的映射表 + */ + public Map getUserNameAndCopyForUserIds(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + List list = userApi.getUserNameAndCopy(userIds); + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } + return list.stream().collect(Collectors.toMap(UserEntity::getId, Function.identity())); + + } + + /** + * 根据用户ID列表查询用户姓名信息(仅离职) + * + * @param userIds 用户Ids列表 + * @return 以用户ID为键,用户实体为值的映射表 + */ + public Map getUserNameOnlyCopy(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + BatchGetUserCopyForm form = new BatchGetUserCopyForm(); + form.setCopyUserIds(userIds); + List list = userApi.batchGetUserCopyByIds(form); + if (CollUtil.isEmpty(list)) { + return new HashMap<>(); + } + return list.stream().collect(Collectors.toMap(UserCopyEntity::getId, Function.identity())); + + } + + + /** + * 根据用户ID列表查询用户信息 + * + * @param userIds 用户ID列表 + * @return 以用户ID为键,用户实体为值的映射表 + */ + public List getUserNameForUserIdsReturnList(List userIds, String tenantId) { + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + List list = userApi.getUserListNoData(userIds, tenantId); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + + } + + + /** + * 批量查询用户姓名 返回列表 + * + * @param userIds 用户ID列表 + * @return 用户实体列表 + */ + public List getUserNameForUserIdsReturnList(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + List list = userApi.getUserName(userIds); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + + } + + /** + * 获取权限模块编码 + * 1、优先从参数获取 2、如果没有就从header中获取 + * + * @return 权限模块编码 + */ + public String getPermissionModuleId() { + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new RuntimeException("请传入权限菜单id"); + } + String module = attributes.getRequest().getHeader("module"); + if (StringUtil.isEmpty(module)) { + throw new RuntimeException("请传入权限菜单id"); + } + return module; + } + + /** + * 查询有权限的用户列表 + * + * @return 如果为 空数据组 就是有全部权限 + */ + public List getPermissionUserIds() { + List powerUserIds = new ArrayList<>(); + InnerPowerUserVO powerUserVO = getLoginManagerUserIdsForEncode(); + if (powerUserVO.getCode().equals(1)) { + powerUserIds = powerUserVO.getUserIds(); + } else if (powerUserVO.getCode().equals(2)) { + powerUserIds = new ArrayList<>(List.of("-1")); + } + return powerUserIds; + } + + /** + * 批量处理list + * + * @param list 数据 + * @param batchSize 批次大小 + * @param + * @return + */ + public static List> batchList(List list, int batchSize) { + if (list == null || list.isEmpty() || batchSize <= 0) { + return List.of(); + } + + return IntStream.range(0, (list.size() + batchSize - 1) / batchSize) + .mapToObj(i -> list.subList(i * batchSize, Math.min((i + 1) * batchSize, list.size()))) + .collect(Collectors.toList()); + } + + /** + * 查询当前用户有权限访问的组织列表 + * + * @return 当前用户有权限访问的组织ID列表 + */ + public List queryPowerOrgList() { + // 获取用户指定模块的权限 + List organizeGeneralDetailVOS = ftbPermissionOrganizeService.authOrganizesByUserBound(null, null, false, false); + if (CollUtil.isEmpty(organizeGeneralDetailVOS)) { + // 当没有的时候,这个代表着没有权限 + return new ArrayList<>(); + } + return organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } + + /** + * 根据关键字匹配用户名或系统ID + * + * @param keywords 关键字,用于匹配用户名、手机号或系统工作ID + * @return 匹配到的用户ID列表 + */ + public List matchUserNameOrSystemId(String keywords) { + QueryListUserDTO dto = new QueryListUserDTO(); + if (StringUtil.isEmpty(keywords)) { + return new ArrayList<>(); + } + if (StringUtil.isNotEmpty(keywords)) { + dto.setUserName(keywords); + dto.setSystemWorkerId(keywords); + dto.setMobilePhone(keywords); + } + dto.setTenantId(getCurrentLoginTenantId()); + List userForNameOrPhone = searchUser(dto); + List userIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(userForNameOrPhone)) { + userIds.addAll(userForNameOrPhone.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } + return userIds; + } + + public List matchSystemId(String keywords) { + QueryListUserDTO dto = new QueryListUserDTO(); + if (StringUtil.isEmpty(keywords)) { + return new ArrayList<>(); + } + if (StringUtil.isNotEmpty(keywords)) { + dto.setSystemWorkerId(keywords); + } + dto.setTenantId(getCurrentLoginTenantId()); + List userForNameOrPhone = searchUser(dto); + List userIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(userForNameOrPhone)) { + userIds.addAll(userForNameOrPhone.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } + return userIds; + } + + /** + * 格式化百分比 + * + * @param rate lv + * @param scale 保留几位小数 + * @return 返回百分比 + */ + public static String formatRate(BigDecimal rate, Integer scale) { + if (ObjectUtils.isEmpty(rate) || rate.compareTo(BigDecimal.ZERO) == 0) { + return "0%"; + } + BigDecimal result = rate.multiply(BigDecimal.valueOf(100)); + return result.setScale(scale, RoundingMode.HALF_UP).toString().concat("%"); + } + + public static String dealWithRate(BigDecimal rate, String suffix) { + if (ObjectUtils.isEmpty(rate) || rate.compareTo(BigDecimal.ZERO) == 0) { + return "0" + suffix; + } + BigDecimal result = rate.multiply(BigDecimal.valueOf(100)); + return result.setScale(2, RoundingMode.HALF_UP).toString().concat(suffix); + } + + + /** + * 根据userid查询培训模块有权限的人员列表 + * + * @param userId 用户id + * @param tenantId 租户id + * @return + */ + public List getPowerUserIdsForTrain(String userId, String tenantId) { + //判断是否是超级管理员 + List userListNoData = userApi.getUserListNoData(List.of(userId), tenantId); + if (CollUtil.isEmpty(userListNoData)) { + return List.of("-1"); + } + if (userListNoData.get(0).getIsAdministrator() == 1) { + return new ArrayList<>(); + } +// SystemEntity systemVO = systemApi.getSystemVONoToken(tenantId); +// log.info("getModule返回的systemVO:{}", JSONUtil.toJsonStr(systemVO)); +// String systemId = systemVO.getId(); +// ModuleEntity moduleEntity = moduleApi.getFullNameOrEnCode("培训管理", " xunlian", tenantId, "App", systemId); +// if (moduleEntity == null) { +// return List.of("-1"); +// } + String module = "461212890154671237"; + List powerOrgList = permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, module); + if (powerOrgList == null) { + return new ArrayList<>(); + } + if (CollUtil.isEmpty(powerOrgList)) { + return List.of("-1"); + } + return powerOrgList; + } + + /** + * 查询用户的按钮权限列表 + * + * @param userBoundVO 用户信息 + * @return 返回按钮权限列表 + */ + public List queryUserBtnPermissionList(UserBoundVO userBoundVO) { + if (userBoundVO == null) { + return new ArrayList<>(); + } + // 当前人员权限 + List permissionIdentifications = ftbPermissionRoleMapper.permissionIdentifications(userBoundVO.getId()); + if (CollUtil.isEmpty(permissionIdentifications)) { + permissionIdentifications = new ArrayList<>(); + } + // 获取人员岗位并获取岗位权限 + String positionId = userBoundVO.getPositionId(); + if (StringUtil.isNotEmpty(positionId)) { + List jobPermissions = ftbPermissionRoleMapper.queryJobPermissions(positionId); + if (CollUtil.isEmpty(jobPermissions)) { + jobPermissions = new ArrayList<>(); + } + permissionIdentifications = Stream.concat(permissionIdentifications.stream(), jobPermissions.stream()) + .distinct() + .collect(Collectors.toList()); + } + return permissionIdentifications; + } + + /** + * 判断container集合是否包含target集合中的所有元素 + * + * @param container 容器集合 + * @param target 目标集合 + * @return 如果container包含target中的所有元素返回true,否则返回false + */ + public static boolean containsAll(Collection container, Collection target) { + if (target == null || target.isEmpty()) { + return true; // 空集合被认为是被任何集合包含的 + } + + if (container == null || container.isEmpty()) { + return false; // 非空集合不能被空集合包含 + } + + return container.containsAll(target); + } + + /** + * 把组织树 转换成map 方便获取 + * + * @param organizeManagerNodeVOS 组织树 + * @param map 返回的map + */ + public static void convertToOrgMap(List organizeManagerNodeVOS, Map map) { + if (CollUtil.isEmpty(organizeManagerNodeVOS)) { + return; + } + for (OrganizeManagerNodeVO organizeManagerNodeVO : organizeManagerNodeVOS) { + map.put(organizeManagerNodeVO.getId(), organizeManagerNodeVO); + if (CollUtil.isNotEmpty(organizeManagerNodeVO.getChildren())) { + convertToOrgMap(organizeManagerNodeVO.getChildren(), map); + } + } + } + + /** + * 生成id + * + * @return id + */ + public static String generateId() { + return UUID.randomUUID().toString().replace("-", ""); + } + + + /** + * 手动构建分页数据 + * + * @param returnList 返回数据 + * @param pageSize 分页大小 + * @param currentPage 当前页 + * @return 分页数据 + */ + public static PageListVO buildPage(List returnList, long pageSize, long currentPage) { + //构建分页返回数据 + PageListVO pageInfo = new PageListVO<>(); + + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(currentPage); + pagination.setPageSize(pageSize); + pagination.setTotal(returnList.size()); + pageInfo.setPagination(pagination); + if (CollUtil.isEmpty(returnList)) { + pageInfo.setList(new ArrayList<>()); + } else { + List> batchList = batchList(returnList, (int) pageSize); + if (currentPage <= batchList.size()) { + pageInfo.setList(batchList.get((int) (currentPage - 1))); + } else { + pageInfo.setList(new ArrayList<>()); + } + } + return pageInfo; + } + + /** + * 判断两个列表是否相等 (数量和大小) + * + * @param list1 集合1 + * @param list2 集合2 + * @return true 表示两个列表相等,false 表示不相等 + */ + public static boolean areListsEqual(List list1, List list2) { + if (list1 == list2) { + return true; + } + if (list1 == null || list2 == null || list1.size() != list2.size()) { + return false; + } + + // 创建副本以避免修改原始列表 + List sortedList1 = new ArrayList<>(list1); + List sortedList2 = new ArrayList<>(list2); + + Collections.sort(sortedList1); + Collections.sort(sortedList2); + + return sortedList1.equals(sortedList2); + } + + /** + * 切换租户 + * + * @param tenantId 租户id + */ + public void checkOutTenant(String tenantId) { + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } + + + /** + * 返回一个空的分页结果列表(泛型版本) + * + * @param currentPage 当前页面 + * @param pageSize 每页调试 + * @param 分页列表中的数据类型 + * @return 空的分页结果列表 + */ + public static PageListVO returnEmptyListGeneric(Long currentPage, Long pageSize) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(currentPage); + pagination.setPageSize(pageSize); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserExamUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserExamUtil.java new file mode 100644 index 0000000..0cc2eac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/utils/UserExamUtil.java @@ -0,0 +1,541 @@ +package jnpf.cultivate.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.google.common.collect.ImmutableMap; +import jnpf.base.UserInfo; +import jnpf.cultivate.service.*; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.certificate.CertificateEventDTO; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamHistoryPaper; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskIdentification; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaperQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.enums.CourseEnums; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.PositionApi; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.Constants; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import java.util.*; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class UserExamUtil { + /** + * 考试服务(试卷) + */ + @Autowired + private FtbCultivateExamHistoryPaperService examHistoryPaperService; + + @Autowired + private FtbCultivateExamService examService; + + @Autowired + private FtbCultivateQuestionService questionService; + + @Autowired + private FtbPersonnelsStaffRosterService rosterService; + + + @Autowired + private FtbCultivateTestPaperService paperService; + + @Autowired + private FtbCultivateExamUserService examUserService; + + + @Autowired + private FtbCultivateTestPaperService testPaperService; + + @Autowired + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Autowired + private FtbCultivateLearnTaskIdentificationService taskIdentificationService; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * @param userIds 用户id集合 + * @return user->roster的map + */ + public Map queryRosterSimpleInfo(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + List rosterList = rosterService.queryWorkerIdByUserIds(userIds); + if (CollectionUtil.isEmpty(rosterList)) { + return new HashMap<>(); + } + return rosterList.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, roster -> roster)); + } + + /** + * 查询并检查考试信息 + * + * @param exam 考试信息 + * @param versionBatch 批次 + * @return + */ + public FtbCultivateExam checkAndUpdateExam(FtbCultivateExam exam, String versionBatch) { + if (versionBatch.equals(exam.getVersionBatch())) { + return exam; + } + FtbCultivateExamHistoryPaper ftbCultivateExamHistoryPaper = examHistoryPaperService.queryByEamIdAndVersionBatch(exam.getId(), versionBatch); + if (null == ftbCultivateExamHistoryPaper) { + return exam; + } + return HistoryPaperUtils.converToExam(ftbCultivateExamHistoryPaper); + } + + public void publishExam(String userId, String positionId, Integer issuanceType, String courseId) { + log.info("考试成功颁发证书,userId={},positionId={}", userId, positionId); + SpringContext.getApplicationContext() + .publishEvent(new JnpfApplicationEvent<>( + CertificateEventDTO.builder() + .source(1) + .postId(positionId) + .courseId(courseId) +// .issuanceType(1) + .issuanceType(issuanceType) + .userId(userId).build())); + } + + /** + * 考试完成回调 + * + * @param userId 用户ID + * @param positionId 岗位id + * @param status 用户考试合格状态 + * @return 0-不需要提醒 1-需要提醒 + */ + public Integer publishExamCallBack(String userId, String positionId, Integer status) { + log.info("考试完成回调,userId={},positionId={},status={}", userId, positionId, status); + //查询是否配置了考试完成才能鉴定 + Integer flag = examService.queryExamAndIdentifyConfig(positionId); + log.info("queryExamAndIdentifyConfig={},flag={}", positionId, flag); + if (flag == 1) { + if (status.equals(CourseEnums.ExamStatus.PASS.getCode()) || status.equals(CourseEnums.ExamStatus.VERY_PASS.getCode())) { + cultivateIdentifyApplyService.turnOnVisibility(positionId, userId); + } + } else { + cultivateIdentifyApplyService.turnOnVisibility(positionId, userId); + } + return flag; + + } + + /** + * 任务考试完成回调 + * + * @param userId 用户id + * @param taskId 任务id + * @param status 考试状态 + * @return + */ + public Integer publishTaskExamCallBack(String userId, String taskId, Integer status) { + log.error("task考试完成回调,userId={},taskId={},status={}", userId, taskId, status); + //查询是否配置了考试完成才能鉴定 + FtbCultivateLearnTaskIdentification ftbCultivateLearnTaskIdentification = taskIdentificationService.queryByTaskId(taskId); + Integer flag = 0; + if (ftbCultivateLearnTaskIdentification != null && ftbCultivateLearnTaskIdentification.getIdentificationRule().equals(1)) { + flag = 1; + } + if (flag.equals(1)) { + if (status.equals(CourseEnums.ExamStatus.PASS.getCode()) || status.equals(CourseEnums.ExamStatus.VERY_PASS.getCode())) { + cultivateIdentifyApplyService.turnOnVisibilityWithTaskId(taskId, userId, status); + } + } else { + cultivateIdentifyApplyService.turnOnVisibilityWithTaskId(taskId, userId, status); + } + return flag; + + } + + public Map getHeadersForLogin() { + UserInfo userInfo = UserProvider.getUser(); + String token = userInfo.getToken(); + if (StringUtils.isEmpty(token)) { + token = ""; + } + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + return headers; + } + + /** + * 获取租户id + * @return tenantId + */ + public String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } + + /** + * 对应字符串list去重复 + * + * @param list 字符串id集合 + * @return 去重复的id集合 + */ + public List uniqueStringList(List list) { + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream() + .filter(Objects::nonNull) // 过滤掉 null 值 + .filter(str -> !str.isEmpty()) // 过滤掉空字符串 + .distinct() // 去重 + .collect(Collectors.toList()); // 收集为 List + } + + public List queryHalfFullQuestionList(List questionList) { + if (CollectionUtil.isEmpty(questionList)) { + return new ArrayList<>(); + } + List questionIds = questionList.stream().map(ExamQuestionBakVo::getQuestionId).collect(Collectors.toList()); + List list = questionService.listByIds(questionIds); + return list; + } + + public Map> getUserOrgBoundInfoForUserList(List examUserList) { + Map map = userApiV2Util.getUserPrimaryBoundBatch(examUserList, null); + if (CollectionUtil.isEmpty(map)) { + return new HashMap<>(); + } + Map> retMap = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + String userId = entry.getKey(); // 用户ID + UserBoundVO vo = entry.getValue(); // 获取值列表 + List workerGroupDataDtoList = new ArrayList<>(); + if (vo !=null) { + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setIsDefault(true); + dto.setReportsTo(vo.getLeaderId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getGradeId()); + dto.setAffiliatedRankName(vo.getGradeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setIsDefaultOrganize(true); + dto.setIsDefaultPosition(true); + dto.setIsPrimaryPosition(true); + workerGroupDataDtoList.add(dto); + } + retMap.put(userId, workerGroupDataDtoList); + } + return retMap; + } + + /** + * 是否有直属主管 + * + * @param workerGroupDataDtoList + * @return true-有 false-没有 + */ + public boolean checkIsHasReporter(List workerGroupDataDtoList) { + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtoList) { + if (StringUtils.isNotEmpty(workerGroupDataDto.getReportsTo())) { + return true; + } + } + return false; + } + + public List convertWebReadOverExamAndPaperDetailVoForHistory(List historyList) { + if (CollectionUtil.isEmpty(historyList)) { + return new ArrayList<>(); + } + List retList = new ArrayList<>(); + for (FtbCultivateExamHistoryPaper exam : historyList) { + WebReadOverExamAndPaperDetailVo vo = new WebReadOverExamAndPaperDetailVo(); + vo.setId(exam.getPrimaryExamId()); + vo.setExamName(exam.getExamName()); + vo.setExamType(exam.getExamType()); + vo.setExamId(exam.getExamId()); + vo.setStartTime(exam.getStartTime()); + vo.setEndTime(exam.getEndTime()); + vo.setPaperId(exam.getPaperId()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setExamTime(exam.getExamTime()); + vo.setPassMark(exam.getPassMark()); + vo.setPassType(exam.getPassType()); + vo.setExcellentMark(exam.getExcellentMark()); + vo.setExcellentType(exam.getExcellentType()); + vo.setDescription(exam.getDescription()); + vo.setCurrQuestionNumber(exam.getCurrQuestionNumber()); + vo.setCurrTotalScore(exam.getCurrTotalScore()); + vo.setVersionBatch(exam.getVersionBatch()); + if (StringUtils.isNotEmpty(exam.getPaperInfo())) { + // + FtbCultivateTestPaper paper = JSONUtil.toBean(exam.getPaperInfo(), FtbCultivateTestPaper.class); + vo.setPaperVo(convertToPaperVo(paper)); + } else { + FtbCultivateTestPaper paper = paperService.getById(exam.getPaperId()); + vo.setPaperVo(convertToPaperVo(paper)); + } + + if (StringUtils.isNotEmpty(exam.getPostId())) { + // 将字符串数组转换为List + List postionIds = Arrays.asList(exam.getPostId().split(",")); + List positionEntities = userApiV2Util.listPositionDetailInfoByIds(postionIds, null); +// List positionEntities = personnelOrgUtils.queryPostionInfoForIds(postionIds); + vo.setPostAndPositionList(convertPostAndPositionList(positionEntities)); + } + retList.add(vo); + } + + return retList; + + } + + /** + * 数据转换 + * + * @param exam 考试 + * @return + */ + public WebReadOverExamAndPaperDetailVo convertToWebReadOverExamAndPaperDetailVo(FtbCultivateExam exam) { + WebReadOverExamAndPaperDetailVo vo = new WebReadOverExamAndPaperDetailVo(); + vo.setId(exam.getId()); + vo.setExamName(exam.getExamName()); + vo.setExamType(exam.getExamType()); + vo.setExamId(exam.getExamId()); + vo.setStartTime(exam.getStartTime()); + vo.setEndTime(exam.getEndTime()); + vo.setPaperId(exam.getPaperId()); + vo.setExamlimitation(exam.getExamlimitation()); + vo.setExamTime(exam.getExamTime()); + vo.setPassMark(exam.getPassMark()); + vo.setPassType(exam.getPassType()); + vo.setExcellentMark(exam.getExcellentMark()); + vo.setExcellentType(exam.getExcellentType()); + vo.setDescription(exam.getDescription()); + vo.setCurrQuestionNumber(exam.getCurrQuestionNumber()); + vo.setCurrTotalScore(exam.getCurrTotalScore()); + vo.setVersionBatch(exam.getVersionBatch()); + if (StringUtils.isNotEmpty(exam.getPaperInfo())) { + // + FtbCultivateTestPaper paper = JSONUtil.toBean(exam.getPaperInfo(), FtbCultivateTestPaper.class); + vo.setPaperVo(convertToPaperVo(paper)); + } else { + FtbCultivateTestPaper paper = paperService.getById(exam.getPaperId()); + vo.setPaperVo(convertToPaperVo(paper)); + } + + if (StringUtils.isNotEmpty(exam.getPostId())) { + // 将字符串数组转换为List + List postionIds = Arrays.asList(exam.getPostId().split(",")); +// List positionEntities = personnelOrgUtils.queryPostionInfoForIds(postionIds); + List positionEntities = userApiV2Util.listPositionDetailInfoByIds(postionIds, null); + vo.setPostAndPositionList(convertPostAndPositionList(positionEntities)); + } + return vo; + + } + + private PaperVo convertToPaperVo(FtbCultivateTestPaper paper) { + if (null == paper) { + return null; + } + return BeanUtil.copyProperties(paper, PaperVo.class); + } + + private List convertPostAndPositionList(List positionEntities) { + List list = new ArrayList<>(); + for (PositionVO positionEntity : positionEntities) { + PostAndPosition postAndPosition = new PostAndPosition(); + postAndPosition.setEnCode(positionEntity.getEnCode()); + postAndPosition.setId(positionEntity.getId()); + postAndPosition.setPostName(positionEntity.getFullName()); + + list.add(postAndPosition); + } + return list; + } + + + public FtbCultivateExam queryExamByIdAndBatch(String examId, String versionBatch) { + FtbCultivateExam exam = examService.getById(examId); + if (null != exam && exam.getVersionBatch().equals(versionBatch)) { + return exam; + } + FtbCultivateExamHistoryPaper ftbCultivateExamHistoryPaper = examHistoryPaperService.queryByEamIdAndVersionBatch(examId, versionBatch); + if (null == ftbCultivateExamHistoryPaper) { + return null; + } + return HistoryPaperUtils.converToExam(ftbCultivateExamHistoryPaper); + } + + public void updateUserExamNullify(String userExamId) { + FtbCultivateExamUser entity = new FtbCultivateExamUser(); + entity.setId(userExamId); + entity.setAppShow(1); + entity.setIsJoinCount(1); + examUserService.updateById(entity); + } + + public GeneralPaperQuestionVo randomQuestion(FtbCultivateExam exam) { + GeneralPaperQuestionVo vo = new GeneralPaperQuestionVo(); + List oldQuestion = HistoryPaperUtils.convertQuestionList(exam.getCurrQuestionList()); + if (StringUtils.isEmpty(exam.getPaperInfo())) { + return vo; + } + FtbCultivateTestPaper paper = JSONUtil.toBean(exam.getPaperInfo(), FtbCultivateTestPaper.class); + if (StringUtils.isEmpty(paper.getPaperConfig())) { + return vo; + } + + PaperConfigReq configReq = JSONUtil.toBean(paper.getPaperConfig(), PaperConfigReq.class); + final Map> questionConfig = configReq.getQuestionConfig(); + final Map scoreConfig = configReq.getScoreConfig(); + if (CollectionUtil.isEmpty(questionConfig) || CollectionUtil.isEmpty(scoreConfig)) { + return vo; + } + + // 根据题库ID 查询题目列表 + List questionBankIdList = new ArrayList<>(questionConfig.keySet()); + if (CollectionUtil.isEmpty(questionBankIdList)) { + return vo; + } + Map> questionMap = testPaperService.BatchQueryQuestionForQuestionBankIds(questionBankIdList); + if (CollectionUtil.isEmpty(questionMap)) { + return vo; + } + + List randomList = new ArrayList<>(); + for (Map.Entry> entry : questionMap.entrySet()) { + String questionBankId = entry.getKey(); + List questionList = entry.getValue(); + //获取该题库题型对应的题目列表 Map + Map> questionTypeMap = questionList.stream() + .collect(Collectors.groupingBy(ftbCultivateQuestion -> String.valueOf(ftbCultivateQuestion.getType()))); + //获取该题库随机选题的配置 + Map questionNumMap = questionConfig.get(questionBankId); + //随机选题 + questionNumMap.forEach((questionType, questionNum) -> { + //题型 的题目列表 + List tempList = questionTypeMap.get(questionType); + if (CollectionUtil.isEmpty(tempList)) { + return; + } + //题型难度列表 + Map> difficultyMap = tempList.stream().collect(Collectors.groupingBy(FtbCultivateQuestion::getDifficulty)); + //简单 + Integer simpleNum = questionNum.getSimpleNum(); + List simpleQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.EASY.getCode()); + //一般 + Integer generalNum = questionNum.getGeneralNum(); + List generalQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.MIDDLE.getCode()); + //难 + Integer hardNum = questionNum.getHardNum(); + List hardQuestion = difficultyMap.get(CourseEnums.QuestionDifficulty.MAX.getCode()); + randomList.addAll(randomGetQuestion(paper.getId(), simpleQuestion, simpleNum, scoreConfig.get(questionType).getSimpleScore())); + randomList.addAll(randomGetQuestion(paper.getId(), generalQuestion, generalNum, scoreConfig.get(questionType).getGeneralScore())); + randomList.addAll(randomGetQuestion(paper.getId(), hardQuestion, hardNum, scoreConfig.get(questionType).getHardSore())); + }); + } + + if (oldQuestion.size() == randomList.size()) { + vo.setSuccess(true); + vo.setRandomList(BeanUtil.copyToList(randomList, ExamQuestionBakVo.class)); + List questionIds = new ArrayList<>(); + for (FtbCultivateTestPaperQuestion paperQuestion : randomList) { + questionIds.add(paperQuestion.getQuestionId()); + } + List selectQuestionList = questionService.listByIds(questionIds); + vo.setQuestionList(selectQuestionList); + return vo; + } + return vo; + } + + private List randomGetQuestion(String paperId, List questionList, Integer randomSize, Integer score) { + if (CollectionUtil.isEmpty(questionList) || randomSize <= 0) { + return new ArrayList<>(); + } + List random = new ArrayList<>(); + if (questionList.size() < randomSize) { + random = questionList; + } else { + // 打乱列表 + Collections.shuffle(questionList); + // 抽取样本 + random = questionList.subList(0, randomSize); + } + + return random.stream().map(question -> { + FtbCultivateTestPaperQuestion paperQuestion = new FtbCultivateTestPaperQuestion(); + paperQuestion.setPaperId(paperId); + paperQuestion.setQuestionId(question.getId()); + paperQuestion.setBankId(question.getClassifyId()); + paperQuestion.setScore(score); + paperQuestion.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + paperQuestion.setCreatorTime(new Date()); + return paperQuestion; + }).collect(Collectors.toList()); + } + + public List getPostInfoList(List ids) { + if (CollUtil.isEmpty(ids)) { + return new ArrayList<>(); + } + ids = ids.stream().distinct().collect(Collectors.toList()); +// List positionEntities = positionApi.getPositionName(ids); + return userApiV2Util.listPositionDetailInfoByIds(ids, null); + } + + /** + * 获取当天的开始时间 + * + * @return + */ + public static Date getTodayStart() { + return DateUtil.beginOfDay(DateUtil.date()); + } + + /** + * 获取本周的开始时间 + * + * @return + */ + public static Date getWeekStart() { + return DateUtil.beginOfWeek(DateUtil.date()); + } + + /** + * 获取本月的开始时间 + * + * @return + */ + public static Date getMonthStart() { + return DateUtil.beginOfMonth(DateUtil.date()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/config/LocalDisableRocketMqListenerProcessor.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/config/LocalDisableRocketMqListenerProcessor.java new file mode 100644 index 0000000..038d722 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/config/LocalDisableRocketMqListenerProcessor.java @@ -0,0 +1,119 @@ +package jnpf.cultivate.v2.config; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author W + */ +@Slf4j +@Component +public class LocalDisableRocketMqListenerProcessor + implements BeanDefinitionRegistryPostProcessor, EnvironmentAware, PriorityOrdered { + + private static final String NACOS_REGISTER_ENABLED = + "spring.cloud.nacos.discovery.register-enabled"; + + private static final String LOCAL_LISTENER_REGISTER_ENABLED = + "local.listener.register-enabled"; + + private Environment environment; + + @Override + public void setEnvironment(@NotNull Environment environment) { + this.environment = environment; + } + + @Override + public void postProcessBeanDefinitionRegistry(@NotNull BeanDefinitionRegistry registry) throws BeansException { + if (!shouldDisableRocketMqListener()) { + return; + } + + List removeBeanNames = new ArrayList<>(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + for (String beanName : registry.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); + String beanClassName = beanDefinition.getBeanClassName(); + + if (!StringUtils.hasText(beanClassName)) { + continue; + } + + try { + Class beanClass = ClassUtils.forName(beanClassName, classLoader); + + RocketMQMessageListener listenerAnnotation = + AnnotationUtils.findAnnotation(beanClass, RocketMQMessageListener.class); + + if (listenerAnnotation != null) { + removeBeanNames.add(beanName); + + log.info( + "Local mode detected, remove RocketMQ listener bean. beanName={}, group={}, topic={}", + beanName, + listenerAnnotation.consumerGroup(), + listenerAnnotation.topic() + ); + } + } catch (Throwable ex) { + log.error("Check RocketMQ listener bean failed. beanName={}, className={}", + beanName, beanClassName, ex); + } + } + + for (String beanName : removeBeanNames) { + if (registry.containsBeanDefinition(beanName)) { + registry.removeBeanDefinition(beanName); + } + } + + if (!removeBeanNames.isEmpty()) { + log.info("RocketMQ listeners disabled because {}=false, removed listener count={}", + NACOS_REGISTER_ENABLED, + removeBeanNames.size()); + } + } + + @Override + public void postProcessBeanFactory(@NotNull ConfigurableListableBeanFactory beanFactory) throws BeansException { + // 不需要处理 + } + + @Override + public int getOrder() { + return HIGHEST_PRECEDENCE; + } + + private boolean shouldDisableRocketMqListener() { + //本地启用消费 + BindResult localListenerResult = Binder.get(environment) + .bind(LOCAL_LISTENER_REGISTER_ENABLED, Boolean.class); + if (localListenerResult.isBound() && Boolean.TRUE.equals(localListenerResult.get())) { + return false; + } + BindResult bindResult = Binder.get(environment) + .bind(NACOS_REGISTER_ENABLED, Boolean.class); + // 要求:配置存在,并且值为 false 时才禁用 + return bindResult.isBound() && Boolean.FALSE.equals(bindResult.get()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/course/V2CultivateCourseAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/course/V2CultivateCourseAppController.java new file mode 100644 index 0000000..ec26830 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/course/V2CultivateCourseAppController.java @@ -0,0 +1,250 @@ +package jnpf.cultivate.v2.controller.app.course; + +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.CultivateCourseMsgService; +import jnpf.cultivate.v2.service.FtbCultivateCourseSettingGlobalService; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.cultivate.v2.service.V2CultivateCourseAppService; +import jnpf.cultivate.v2.util.CultivateCourseStudyUtil; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.FtbCultivateCourseSettingGlobal; +import jnpf.model.cultivate.v2.course.vo.app.*; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.model.cultivate.v2.position.req.V2CultivateCommonCourseForAppReq; +import jnpf.model.cultivate.v2.position.req.V2MyCultivateCommonCourseForAppReq; +import jnpf.model.cultivate.vo.course.app.FtbCultivateCourseMsgForAppVO; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * app端课程模块 + * + * @author xgl + * @date 2026-01-27 14:04:01 + */ +@Slf4j +@RestController +@RequestMapping("/v2/app/course") +public class V2CultivateCourseAppController { + + + @Autowired + private V2CultivateCourseAppService v2CultivateCourseAppService; + + @Autowired + private FtbCultivateCourseSettingGlobalService ftbCultivateCourseSettingGlobalService; + + @Autowired + private FtbCultivateLabelService ftbCultivateLabelService; + + + @Autowired + private CultivateCourseStudyUtil cultivateCourseStudyUtil; + + @Autowired + private CultivateCourseMsgService cultivateCourseMsgService; + + + @Autowired + private RedisTemplate redisTemplate; + + + /** + * 我的课程详情(带学习进度) + * + * @param dto 请求参数 + * @return {@link ActionResult}<{@link V2CourseDetailsAppVO}> + */ + @GetMapping("/course-details") + public ActionResult courseDetails(@Validated V2CourseAppDto dto) { + V2CourseDetailsAppVO vo = v2CultivateCourseAppService.courseDetails(dto); + cultivateCourseStudyUtil.checkCourseComplete(dto, vo.getState()); + return ActionResult.success("", vo); + } + + /** + * 课程大纲(带学习进度) + * + * @param dto 请求参数 + * @return {@link ActionResult}<{@link V2CourseOutlineAppVo}> + */ + @GetMapping("/course-outline") + public ActionResult courseOutline(@Validated V2CourseAppDto dto) { + return ActionResult.success("", v2CultivateCourseAppService.courseOutline(dto.getCourseId(), dto.getUserId())); + } + + + /** + * App课程学习模块-章节详情 + * + * @param id 章节id,必填 + */ + @GetMapping("/chapter-details/{id}/{userId}") + public ActionResult chapterDetails(@PathVariable("id") String id, @PathVariable("userId") String userId) { + return ActionResult.success(v2CultivateCourseAppService.chapterDetails(id, userId)); + } + + + /** + * App课程学习模块-完成章节学习 + * + * @param chapterStudyDTO 章节学习参数 + * @return {@link ActionResult} + */ + @PostMapping("/chapter-study") + public ActionResult chapterStudy(@Validated @RequestBody V2ChapterStudyVo chapterStudyDTO) { + UserInfo userInfo = UserProvider.getUser(); + String lockKey = "chapterStudy:" + userInfo.getTenantId() + ":" + userInfo.getUserId() + ":" + chapterStudyDTO.getCourseId() + ":" + chapterStudyDTO.getChapterId(); + executeWithLock(lockKey, () -> v2CultivateCourseAppService.chapterStudy(chapterStudyDTO)); + return ActionResult.success(); + } + + /** + * App课程学习模块-时长记录 + * + * @param chapterStudyDTO 章节学习参数 + * @return {@link ActionResult} + */ + @PostMapping("/duration-record") + public ActionResult durationRecord(@Validated @RequestBody V2ChapterStudyVo chapterStudyDTO) { + if (chapterStudyDTO.getDuration() == null || chapterStudyDTO.getDuration() <= 0) { + return ActionResult.success(); + } + UserInfo userInfo = UserProvider.getUser(); + String lockKey = "duration-record:" + userInfo.getTenantId() + ":" + userInfo.getUserId() + ":" + chapterStudyDTO.getCourseId() + ":" + chapterStudyDTO.getChapterId(); + executeWithLock(lockKey, () -> v2CultivateCourseAppService.durationRecord(chapterStudyDTO)); + return ActionResult.success(); + } + + /** + * 使用分布式锁执行操作 + * + * @param lockKey 锁的key + * @param action 要执行的操作 + */ + private void executeWithLock(String lockKey, Runnable action) { + String lockValue = UUID.randomUUID().toString(); + Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(locked)) { + try { + action.run(); + } catch (Exception e) { + log.error("执行操作失败,lockKey: {}", lockKey, e); + throw e; + } finally { + // 安全的释放锁:只有当锁的值与当前值匹配时才删除 + Object currentValue = redisTemplate.opsForValue().get(lockKey); + if (lockValue.equals(currentValue)) { + redisTemplate.delete(lockKey); + } + } + } else { + log.warn("重复提交被拦截,lockKey: {}", lockKey); + throw new RuntimeException("请不要重复提交"); + } + } + + /** + * 获取课程规则全局配置 + * + * @return {@link ActionResult}<{@link FtbCultivateCourseSettingGlobal}> + */ + @GetMapping("/global-setting") + public ActionResult globalSetting() { + return ActionResult.success(ftbCultivateCourseSettingGlobalService.getInfo()); + } + + + /** + * App课程学习模块-增加当前课程浏览量 + * + * @param courseId 课程主键id,必填 + * @return {@link ActionResult} + */ + @PutMapping("/course-browsing/{id}") + public ActionResult courseBrowsing(@PathVariable("id") String courseId) { + v2CultivateCourseAppService.courseBrowsing(courseId); + return ActionResult.success(); + } + + + /** + * 我的课程-通用课程标签列表 + */ + @GetMapping("/common-course-label-list") + public ActionResult> commonCourseLabelList(V2CultivateCommonCourseForAppReq req) { + return ActionResult.success("成功", ftbCultivateLabelService.commonCourseLabelList(req)); + } + + + /** + * 我的课程-通用课程列表 + */ + @GetMapping("/common-course-list") + public ActionResult> commonCourseList(V2MyCultivateCommonCourseForAppReq dto, CultivatePage page) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivateCourseAppService.commonCourseList(dto, page))); + } + + + /** + * 我的课程-通用课程-(总课程/已经完成课程) + */ + @GetMapping("/common-course-count") + public ActionResult commonCourseCount(V2MyCultivateCommonCourseForAppReq dto) { + return ActionResult.success(v2CultivateCourseAppService.commonCourseCount(dto)); + } + + /** + * 最后一次学习的课程 + */ + @GetMapping("/query-last-study-course") + public ActionResult queryLastStudyCourse() { + LastStudyCourseVo vo = v2CultivateCourseAppService.queryLastStudyCourse(); + if (StringUtil.isEmpty(vo.getCourseId())) { + return ActionResult.success("成功", null); + } + if (StringUtil.isNotEmpty(vo.getCourseId())) { + V2CourseOutlineAppVo courseOutline = v2CultivateCourseAppService.courseOutline(vo.getCourseId(), UserProvider.getUser().getUserId()); + if (courseOutline != null) { + vo.setChapterTotalNumber(courseOutline.getChapterTotalNumber()); + vo.setChapterFinishedNumber(courseOutline.getChapterFinishedNumber()); + vo.setCoverUrl(courseOutline.getCoverUrl()); + vo.setCourseName(courseOutline.getCourseName()); + } + } + return ActionResult.success("成功", vo); + } + + /** + * App查询用户未读信息列表 + * + * @return {@link ActionResult}<{@link List}> + */ + @GetMapping("/queryUserUnreadMsg") + public ActionResult> queryUserUnreadMsg(@RequestParam("courseId") String courseId) { + return ActionResult.success(cultivateCourseMsgService.queryUserUnreadMsgV2(courseId)); + } + + /** + * App更新用户已读信息 + * + * @param courseId 课程id + * @return {@link ActionResult}<{@link Boolean}> + */ + @PutMapping("/updateInfoByMsgId/{courseId}") + public ActionResult updateInfoByMsgId(@PathVariable("courseId") String courseId) { + cultivateCourseMsgService.updateInfoByMsgIdV2(courseId); + return ActionResult.success("", true); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/ExamSubController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/ExamSubController.java new file mode 100644 index 0000000..f4a59df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/ExamSubController.java @@ -0,0 +1,62 @@ +package jnpf.cultivate.v2.controller.app.exam; + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.req.exam.SubExamQuestionReq; +import jnpf.model.cultivate.resp.SubExamVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * 我的考试[app] + * + * @author yanwenfu + * @create 2026-03-26 + */ +@RestController +@RequestMapping(value = "/v2/app/exam") +public class ExamSubController { + + @Resource + private FtbCultivateExamUserService examUserService; + @Resource + private V2CultivateExamService v2CultivateExamService; + @Autowired + private RedisTemplate redisTemplate; + + /** + * 提交试卷 + * @param userExamId 用户考试ID + * @param req 请求参数 + * @return jnpf.base.ActionResult + */ + @PostMapping("/sub-user-exam/{userExamId}") + public ActionResult subUserExam(@PathVariable("userExamId") String userExamId, @RequestBody @Valid SubExamQuestionReq req) { + + FtbCultivateExamUser examUser = examUserService.getById(userExamId); + if (null == examUser || examUser.getEnabledMark() == 0) { + return ActionResult.fail(1000, "考试记录已经删除"); + } + String lockKey = "subExam" + userExamId; + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(), 10, TimeUnit.SECONDS)) { + try { + SubExamVo subExamVo = v2CultivateExamService.subUserExam(userExamId, req); + return ActionResult.success(subExamVo); + } catch (Exception e) { + throw e; + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("考试提交中,请不要重复提交"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/V2CultivateUserExamAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/V2CultivateUserExamAppController.java new file mode 100644 index 0000000..bf4408c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/exam/V2CultivateUserExamAppController.java @@ -0,0 +1,150 @@ +package jnpf.cultivate.v2.controller.app.exam; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.model.cultivate.v2.exam.req.*; +import jnpf.model.cultivate.v2.exam.vo.*; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 我的考试[app] + * + * @author yanwenfu + */ +@RestController +@RequestMapping("/v2/app/exam") +public class V2CultivateUserExamAppController { + + @Resource + private V2CultivateExamService v2CultivateExamService; + + /** + * 我的考试[tab count] + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/tab-count") + public ActionResult getMyExamTabCount() { + + MyExamTabCountVo vo = v2CultivateExamService.getMyExamTabCount(); + return ActionResult.success(vo); + } + + /** + * 我的考试[分页] + * @param req 请求参数 + * @return jnpf.base.ActionResult> + */ + @GetMapping("/myExamList") + public ActionResult> myExamList(V2AppQueryExamListReq req) { + + PageInfo pageVo = v2CultivateExamService.myExamList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 重新考试[web] + * @param userExamId 用户考试id + * @return jnpf.base.ActionResult + */ + @PutMapping(value = "/restart/{userExamId}") + public ActionResult restartExam(@PathVariable("userExamId") String userExamId) throws Exception { + + String newUserExamId = v2CultivateExamService.restartExam(userExamId); + return ActionResult.success(MsgCode.SU000.get(), newUserExamId); + } + + /** + * 重新考试 - 带次数验证[app] + * @param userExamId 用户考试id + * @return jnpf.base.ActionResult + */ + @PutMapping(value = "/restartAndCheckNum/{userExamId}") + public ActionResult restartExamWithValid(@PathVariable("userExamId") String userExamId) throws Exception { + + String newUserExamId = v2CultivateExamService.restartExamWithValid(userExamId); + return ActionResult.success(MsgCode.SU000.get(), newUserExamId); + } + + + /** + * 验证是否可以重新考试[app&web] + * @param userExamId 用户考试id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/restart/check/{userExamId}") + public ActionResult checkRestartExam(@PathVariable("userExamId") String userExamId) { + + try { + Boolean b = v2CultivateExamService.checkRestartExam(userExamId); + return ActionResult.success(b); + } catch (Exception e) { + return ActionResult.success(Boolean.FALSE); + } + } + + /** + * 我的考试 - 详情 + * @param userExamId 用户考试id + * @return jnpf.base.ActionResult + */ + @GetMapping("/my-exam/detail/{userExamId}") + public ActionResult myExamDetail(@PathVariable("userExamId") String userExamId) { + + MyExamDetailVo vo = v2CultivateExamService.myExamDetail(userExamId); + return ActionResult.success(vo); + } + + /** + * 开始考试 - 查询试题 + * @param userExamId 用户考试id + * @return jnpf.base.ActionResult> + */ + @GetMapping("/exam-question/list/{userExamId}") + public ActionResult> queryQuestionListForExam(@PathVariable("userExamId") String userExamId) throws Exception { + + List questionList = v2CultivateExamService.getExamQuestionList(userExamId); + return ActionResult.success(questionList); + } + + /** + * 批阅考试[tab count] + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/read-over/tab-count") + public ActionResult getReadOverTabCount(@RequestBody QueryReadOverExamListReq req) { + + ReadOverTabCountVo vo = v2CultivateExamService.getReadOverTabCount(req.getOrganizeList()); + return ActionResult.success(vo); + } + + /** + * 批阅考试 - 考试列表[分页] + * @param req 请求参数 + * @return jnpf.base.ActionResult> + */ + @PostMapping("/read-over/exam/list") + public ActionResult> readOverExamList(@RequestBody QueryReadOverExamListReq req) { + + PageInfo pageVo = v2CultivateExamService.readOverExamList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 批阅考试 - 排行榜[分页] + * @param req 请求参数 + * @return jnpf.base.ActionResult> + */ + @PostMapping("/result-rank") + public ActionResult getExamRankList(@RequestBody ExamRankReq req) { + + UserExamRankVo rankVo = v2CultivateExamService.getExamRankList(req); + return ActionResult.success(rankVo); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/gained/V2CourseGainedCommentController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/gained/V2CourseGainedCommentController.java new file mode 100644 index 0000000..5e6e287 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/gained/V2CourseGainedCommentController.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.v2.controller.app.gained; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CourseGainedCommentService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.gained.req.V2AppCommentPageListReq; +import jnpf.model.cultivate.v2.gained.vo.V2AppCourseGainedCommentVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * app岗位学习课程-心得评论模块 + */ +@RestController +@RequestMapping("/v2/app/course_gained_comment") +public class V2CourseGainedCommentController { + + @Autowired + private V2CourseGainedCommentService ftbCourseGainedCommentService; + + + /** + * 一级评论列表 + * + * @param req + * @return + */ + @Operation(summary = "获取心得评论获取列表") + @GetMapping("/list") + public ActionResult> appList(V2AppCommentPageListReq req) { + return ActionResult.success(CultivatePage.coverPageList(ftbCourseGainedCommentService.getList(req))); + } + + /** + * 查看对应的心得评论回复 + * + * @param firstCommentId 一级评论id + * @return + */ + @Operation(summary = "查看对应的心得评论回复(移动端)") + @GetMapping("/listReply/{firstCommentId}") + public ActionResult> listReply(@PathVariable("firstCommentId") String firstCommentId) { + return ActionResult.success(ftbCourseGainedCommentService.listReply(firstCommentId)); + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateIdentifyManagerAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateIdentifyManagerAppController.java new file mode 100644 index 0000000..d3589df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateIdentifyManagerAppController.java @@ -0,0 +1,98 @@ +package jnpf.cultivate.v2.controller.app.identify; + + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.v2.service.V2CultivateIdentifyApplyService; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplyListAppReq; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplySaveReq; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplySubmitReq; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyAppBasicInfoVo; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyItemVo; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyListAppVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * app鉴定管理模块 + */ +@Slf4j +@RestController +@RequestMapping(value = "/v2/app/apply/manager") +public class V2CultivateIdentifyManagerAppController { + @Autowired + private V2CultivateIdentifyApplyService applyService; + + + /** + * 发起实操鉴定申请 + * + * @param req 实操鉴定申请保存请求对象,包含申请相关信息 + * @return 操作结果的ActionResult对象 + */ + @PostMapping(value = "/save") + public ActionResult applyDataSave(@Valid @RequestBody V2IdentifyApplySaveReq req) { + req.setSource(ApplySourceEnum.SDFQ.getCode()); + applyService.applyDataSaveBatch(req); + return ActionResult.success(MsgCode.SU018.get(),true); + } + + + /** + * 实操鉴定列表 + * + * @param req 帅选条件 + * @return 列表 + * @ + */ + @Operation(summary = "列表") + @PostMapping(value = "/list") + public ActionResult> queryAppIdentifyApplyList(@Valid @RequestBody V2IdentifyApplyListAppReq req) { + return ActionResult.success(CultivatePage.coverPageList(applyService.queryAppIdentifyApplyList(req))); + } + + + /** + * 获取实操鉴定申请的基本信息详情 + * + * @param id 鉴定申请ID + * @return 包含基本信息的ActionResult对象,数据为V2IdentifyApplyBasicInfoVo类型 + */ + @Operation(summary = "详情(基本信息)") + @GetMapping(value = "/basicInfo/{id}") + public ActionResult getApplyBasicInfo(@PathVariable("id") String id) { + return ActionResult.success(applyService.getAppApplyBasicInfo(id)); + } + + /** + * 获取鉴定项列表 + * + * @param id 鉴定申请ID + */ + @Operation(summary = "详情(鉴定详情)") + @GetMapping(value = "/item-list/{id}") + public ActionResult itemList(@PathVariable("id") String id) { + return ActionResult.success(applyService.itemList(id)); + } + + + /** + * 提交鉴定 + * + * @param req + */ + @Operation(summary = "提交鉴定") + @PostMapping(value = "/submit") + public ActionResult submit(@Valid @RequestBody V2IdentifyApplySubmitReq req) { + applyService.applyDataSubmit(req); + return ActionResult.success(MsgCode.SU005.get()); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateMyIdentifyAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateMyIdentifyAppController.java new file mode 100644 index 0000000..6e810e5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/identify/V2CultivateMyIdentifyAppController.java @@ -0,0 +1,70 @@ +package jnpf.cultivate.v2.controller.app.identify; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.v2.service.V2CultivateIdentifyApplyService; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.identify.IdentifyApplySubmitDto; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplySaveReq; +import jnpf.model.cultivate.v2.apply.req.V2MyIdentifyApplyListAppReq; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyListAppVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableListVo; +import jnpf.model.cultivate.vo.identify.IdentifyApplyListAppVo; +import jnpf.personnels.service.FtbPersonnelsPermissionsService; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * app我的鉴定模块 + */ +@Slf4j +@RestController +@RequestMapping(value = "/v2/app/apply/my") +public class V2CultivateMyIdentifyAppController { + @Autowired + private V2CultivateIdentifyApplyService applyService; + + + + /** + * 我的实操鉴定 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "列表") + @GetMapping(value = "/list") + public ActionResult> queryMyIdentifyApplyList(@Valid V2MyIdentifyApplyListAppReq req) { + return ActionResult.success(CultivatePage.coverPageList(applyService.queryMyIdentifyApplyList(req))); + } + + + /** + * 我申请鉴定 + * + * @param req 实操鉴定申请保存请求对象,包含申请相关信息 + * @return 操作结果的ActionResult对象 + */ + @PostMapping(value = "/submit-apply") + public ActionResult applyDataSave(@Valid @RequestBody V2IdentifyApplySaveReq req) { + req.setSource(ApplySourceEnum.APPLY_IN_PERSON.getCode()); + req.setBeIdentifyUserIds(List.of(UserProvider.getUser().getUserId())); + applyService.applyDataSaveBatch(req); + return ActionResult.success(MsgCode.SU018.get()); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/offline/V2CultivateOfflineTrainAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/offline/V2CultivateOfflineTrainAppController.java new file mode 100644 index 0000000..6454691 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/offline/V2CultivateOfflineTrainAppController.java @@ -0,0 +1,36 @@ +package jnpf.cultivate.v2.controller.app.offline; + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.V2CultivateOfflineTrainService; +import jnpf.model.cultivate.v2.offline.V2CultivateOfflineTrainUpdateDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * app线下培训模块 + */ +@RestController +@RequestMapping("/v2/app/cultivate/offline") +public class V2CultivateOfflineTrainAppController { + + @Autowired + private V2CultivateOfflineTrainService v2CultivateOfflineTrainService; + + + /** + * 修改培训结果 + * + * @param req 修改培训结果参数 + * @return {@link ActionResult} + */ + @PostMapping("/modify-training-results") + public ActionResult modifyTrainingResults(@RequestBody @Validated V2CultivateOfflineTrainUpdateDTO req) { + v2CultivateOfflineTrainService.updateTrainingResults(req); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2CultivatePositionAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2CultivatePositionAppController.java new file mode 100644 index 0000000..33740b0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2CultivatePositionAppController.java @@ -0,0 +1,111 @@ +package jnpf.cultivate.v2.controller.app.position; + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivatePositionService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.course.vo.app.AppCourseSimpleVo; +import jnpf.model.cultivate.v2.course.vo.app.OtherPositionCourseCountVo; +import jnpf.model.cultivate.v2.course.vo.app.PositionLearningCourseVo; +import jnpf.model.cultivate.v2.position.req.V2CultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.AppCultivatePositionDetailVo; +import jnpf.model.cultivate.v2.position.vo.CultivatePositionSimpleVo; +import jnpf.model.cultivate.v2.position.vo.CultivateSimpleUserInfoVo; +import jnpf.model.cultivate.v2.position.vo.V2CultivatePositionDetailForApp; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.UploaderUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +/** + * app岗位学习模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/app/position") +public class V2CultivatePositionAppController { + + @Autowired + private V2CultivatePositionService v2CultivatePositionService; + + @Autowired + private UserApiV2Util userApiV2Util; + + + /** + * 岗位学习本岗课程列表 + * + * @param req 请求参数 + * @return 岗位学习课程列表 + */ + @GetMapping("/position-course-list") + public ActionResult positionCourseLists(V2CultivatePositionCourseForAppReq req) { + return ActionResult.success(v2CultivatePositionService.positionCourseLists(req)); + } + + /** + * 他岗课程岗位列表 + */ + @GetMapping("/other-position-list") + public ActionResult> otherPositionList(V2OtherCultivatePositionCourseForAppReq req) { + return ActionResult.success("成功", v2CultivatePositionService.otherPositionList(req)); + } + + /** + * 他岗课程课程列表 + */ + @GetMapping("/other-position-course-list") + public ActionResult> otherPositionCourseList(V2OtherCultivatePositionCourseForAppReq dto, CultivatePage page) { + return ActionResult.success("成功", UserApiV2Util.buildPage(v2CultivatePositionService.otherPositionCourseList(dto, page), page.getPageSize(), page.getCurrentPage())); + } + + /** + * 他岗课程课程(总课程/已经完成课程) + */ + @GetMapping("/other-position-course-count") + public ActionResult otherPositionCourseCount(V2OtherCultivatePositionCourseForAppReq dto) { + return ActionResult.success("成功", v2CultivatePositionService.otherPositionCourseCount(dto)); + } + + + /** + * 查询岗位学习详情(userid + positionId) + */ + @GetMapping("/position-study-detail") + public ActionResult positionStudyDetail(V2CultivatePositionDetailForApp dto) { + return ActionResult.success("成功", v2CultivatePositionService.positionStudyDetail(dto)); + } + + + /** + * 根据用户id查询用户下属的成员列表 + * + * @param userId 用户id + * @return 成员列表 + */ + @GetMapping("/queryUnderlingUserList") + public ActionResult> queryUnderlingByUserId(@RequestParam("userId") String userId) { + List userPageList = userApiV2Util.listUnderlingTargetUser(userId, null); + List list = new ArrayList<>(); + if (CollUtil.isEmpty(userPageList)) { + return ActionResult.success(new ArrayList<>()); + } + for (UserPageListVO vo : userPageList) { + CultivateSimpleUserInfoVo newSub = new CultivateSimpleUserInfoVo(); + newSub.convert(vo); + newSub.setHeadIcon(UploaderUtil.uploaderImg(vo.getHeadIcon())); + list.add(newSub); + } + return ActionResult.success(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2NextUserCultivatePositionAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2NextUserCultivatePositionAppController.java new file mode 100644 index 0000000..06bcf29 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/position/V2NextUserCultivatePositionAppController.java @@ -0,0 +1,178 @@ +package jnpf.cultivate.v2.controller.app.position; + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivatePositionService; +import jnpf.cultivate.v2.service.V2CultivatePromotionService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.course.vo.app.AppCourseSimpleVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCultivateCourseExamVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCultivateCourseIdentityVo; +import jnpf.model.cultivate.v2.course.web.req.V2NextUserAllMapListReq; +import jnpf.model.cultivate.v2.course.web.req.V2NextUserCultivateCourseListReq; +import jnpf.model.cultivate.v2.position.vo.AppPracticeCountVo; +import jnpf.model.cultivate.v2.promotion.vo.MyCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.NextUserCultivatePromotionVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePostAndGradeVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UploaderUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * app下属的岗位学习模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/app/position/next-user") +public class V2NextUserCultivatePositionAppController { + /** + * 服务对象 + */ + @Autowired + private V2CultivatePromotionService promotionService; + + @Autowired + private V2CultivatePositionService v2CultivatePositionService; + @Autowired + private UserApiV2Util userApiV2Util; + + + /** + * 用户的地图列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/all-map-list") + public ActionResult allMapList(V2NextUserAllMapListReq req) { + NextUserCultivatePromotionVo vo = new NextUserCultivatePromotionVo(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userPrimaryBoundOne == null) { + return ActionResult.success("成功", vo); + } + vo.fillUserInfo(userPrimaryBoundOne); + vo.setUserAvatar(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + List myCultivatePromotionListVos = promotionService.queryMyAllPromotionList(req.getUserId()); + + if (CollUtil.isNotEmpty(myCultivatePromotionListVos)) { + List promotionIds = myCultivatePromotionListVos.stream().map(MyCultivatePromotionListVo::getId).collect(Collectors.toList()); + Map> map = promotionService.queryAllSelectPosition(req.getUserId(), promotionIds); + for (MyCultivatePromotionListVo myCultivatePromotionListVo : myCultivatePromotionListVos) { + myCultivatePromotionListVo.setSelectPositionList(map.get(myCultivatePromotionListVo.getId())); + } + } + + vo.setPromoteList(myCultivatePromotionListVos); + return ActionResult.success("成功", vo); + } + + /** + * 课程列表 + * req 岗位学习课程列表参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/all-complete-course-list") + public ActionResult> allCompleteCourseLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.allCompleteCourseLists(req))); + + } + + + /** + * 考试列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/all-complete-exam-list") + public ActionResult> allCompleteExamLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.allCompleteExamLists(req))); + } + + + /** + * 鉴定列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/all-complete-identity-list") + public ActionResult> allCompleteIdentityLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.allCompleteIdentityLists(req))); + } + + + /** + * 练习列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/all-complete-practice-list") + public ActionResult> allCompletePracticeLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(v2CultivatePositionService.allCompletePracticeLists(req)); + } + + + + + /** + * 本岗位-完成课程列表 + * req 岗位学习课程列表参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/self-position-all-complete-course-list") + public ActionResult> selfPositionAllCompleteCourseLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.selfPositionAllCompleteCourseLists(req))); + + } + + + /** + * 本岗位-考试列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/self-position-all-complete-exam-list") + public ActionResult> selfPositionAllCompleteExamLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.selfPositionAllCompleteExamLists(req))); + } + + + /** + * 本岗位-鉴定列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/self-position-all-complete-identity-list") + public ActionResult> selfPositionAllCompleteIdentityLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.selfPositionAllCompleteIdentityLists(req))); + } + + + /** + * 本岗位-练习列表 + * req 请求参数 + * + * @return {@link ActionResult} + */ + @GetMapping("/self-position-all-complete-practice-list") + public ActionResult> selfPositionAllCompletePracticeLists(V2NextUserCultivateCourseListReq req) { + return ActionResult.success(v2CultivatePositionService.selfPositionAllCompletePracticeLists(req)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/promotion/V2CultivatePromotionAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/promotion/V2CultivatePromotionAppController.java new file mode 100644 index 0000000..628349c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/promotion/V2CultivatePromotionAppController.java @@ -0,0 +1,121 @@ +package jnpf.cultivate.v2.controller.app.promotion; + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.V2CultivatePromotionService; +import jnpf.model.cultivate.v2.promotion.req.V2CultivatePromotionPhaseStateForApp; +import jnpf.model.cultivate.v2.promotion.req.V2CultivatePromotionPositionDetailForApp; +import jnpf.model.cultivate.v2.promotion.req.V2CultivatePromotionSelectPositionReq; +import jnpf.model.cultivate.v2.promotion.vo.AppCultivatePromotionPositionDetailVo; +import jnpf.model.cultivate.v2.promotion.vo.MyCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.PromotionPhaseVo; +import jnpf.model.cultivate.v2.promotion.vo.UserPromotionDetailVo; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + + +/** + * app学习地图模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/app/promotion") +public class V2CultivatePromotionAppController { + /** + * 服务对象 + */ + @Autowired + private V2CultivatePromotionService promotionService; + + + /** + * 获取我的所有学习地图列表 + * + * @return + */ + @GetMapping("/query-my-all-promotion-list") + public ActionResult> queryMyAllPromotionList() { + return ActionResult.success("成功", promotionService.queryMyAllPromotionList(UserProvider.getUser().getUserId())); + } + + + /** + * 选定一个学习地图 + * + * @return + */ + @PutMapping("/select-promotion/{promotionId}") + public ActionResult selectPromotion(@PathVariable("promotionId") String promotionId) { + promotionService.selectPromotion(UserProvider.getUser().getUserId(), promotionId); + return ActionResult.success("成功"); + } + + /** + * 学习地图的一个阶段,选定学习的岗位 + * + * @return + */ + @PostMapping("/record-select-position") + public ActionResult recordSelectPosition(@Validated @RequestBody V2CultivatePromotionSelectPositionReq req) { + promotionService.recordSelectPosition(req); + return ActionResult.success(); + } + + /** + * 查询学习地图阶段信息(userId+promotionId+level) + * + * @param userId 用户id + * @param promotionId 学习地图id + * @param level 阶段 + * @return 返回配置的岗位&职级信息 已经选择的岗位&职级信息 + */ + @GetMapping("/guery-promotion-info-for-level") + public ActionResult queryPromotionInfoForLevel(@RequestParam("userId") String userId, + @RequestParam("promotionId") String promotionId, + @RequestParam("level") Integer level) { + return ActionResult.success(promotionService.queryPromotionInfoForLevel(userId, promotionId, level)); + } + + + /** + * 查询地图岗位学习详情(userid + positionId + level) + * + * @param req 请求参数 + * @return 返回岗位学习详情 + */ + @GetMapping("/promotion-position-study-detail") + public ActionResult positionStudyDetail(V2CultivatePromotionPositionDetailForApp req) { + return ActionResult.success("成功", promotionService.getPositionStudyDetail(req)); + } + + + /** + * 查询地图阶段完成情况(promotionId + level + userId) + * + * @param req 请求参数 + * @return 状态 0-未完成 1-已完成 + */ + @GetMapping("/promotion-level-status") + public ActionResult promotionLevelStatus(V2CultivatePromotionPhaseStateForApp req) { + return ActionResult.success("成功", promotionService.promotionLevelStatus(req)); + } + + /** + * 根据用户ID和学习地图ID查询指定学习地图及分阶段岗位信息 + * + * @param userId 用户ID + * @param promotionId 学习地图ID + * @return 用户学习地图详情,包含该学习地图的各阶段岗位信息 + */ + @GetMapping("/user-promotion-detail") + public ActionResult getUserPromotionDetail(@RequestParam("userId") String userId, + @RequestParam("promotionId") String promotionId) { + return ActionResult.success("成功", promotionService.getUserPromotionDetail(userId, promotionId)); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/task/V2CultivateTaskAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/task/V2CultivateTaskAppController.java new file mode 100644 index 0000000..5137e91 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/task/V2CultivateTaskAppController.java @@ -0,0 +1,193 @@ +package jnpf.cultivate.v2.controller.app.task; + +import cn.hutool.core.collection.CollUtil; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivateTaskService; +import jnpf.cultivate.v2.util.CultivateCourseStudyUtil; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnAllocationDTO; +import jnpf.model.cultivate.v2.course.vo.app.V2CourseAppDto; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.task.req.CheckUserTaskAllPhasesCompleteReq; +import jnpf.model.cultivate.v2.task.req.V2CultivateLearnTaskListForManagerReq; +import jnpf.model.cultivate.v2.task.req.V2MyCultivateTaskListForReq; +import jnpf.model.cultivate.v2.task.vo.*; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Map; + +/** + * app 培训任务模块 + * + * @author xgl + */ + +@RestController +@Slf4j +@RequestMapping("/v2/app/task") +public class V2CultivateTaskAppController { + + @Autowired + private V2CultivateTaskService cultivateTaskService; + + @Autowired + private CultivateCourseStudyUtil cultivateCourseStudyUtil; + + + /** + * 任务管理-查询任务列表 + */ + @GetMapping("/query-task-list") + public ActionResult> queryTaskList(V2CultivateLearnTaskListForManagerReq req) { + return ActionResult.success(cultivateTaskService.queryTaskListForApp(req)); + } + + + /** + * 任务管理-tab栏小气泡数 + * noStart 未开始 + * doing 进行中 + * over 已经结束 + * + * @param keyWord 任务名称 + * @return Map + */ + @GetMapping("/query-task-count") + public ActionResult> queryTaskCount(@RequestParam(value = "keyWord", defaultValue = "", required = false) String keyWord) { + return ActionResult.success(cultivateTaskService.queryTaskCountForApp(keyWord)); + } + + + /** + * 任务管理-任务详情展示 + */ + @GetMapping("/query-task-info/{taskId}") + public ActionResult queryTaskInfo(@PathVariable("taskId") String taskId) { + return ActionResult.success(cultivateTaskService.get(taskId)); + } + + /** + * 任务管理-任务完成统计 + * + * @param taskId 任务id + * @return V2CultivateLearnTaskFinishStatisticsVo + */ + @GetMapping("/query-complete-statistics/{taskId}") + public ActionResult getCompletionStatistics(@PathVariable("taskId") String taskId) { + return ActionResult.success(cultivateTaskService.getCompletionStatisticsForManagerApp(taskId)); + } + + /** + * 任务管理-人员完成情况列表 + * + * @param taskId 任务id + * @return PageListVO + */ + @GetMapping("/query-task-user-list/{taskId}") + public ActionResult> queryTaskUserList(@PathVariable("taskId") String taskId, + CultivatePage page) { + return ActionResult.success(cultivateTaskService.queryTaskUserListForManagerApp(taskId, page)); + } + + + /** + * 分配任务给用户 + * + * @param req 任务分配数据传输对象,包含任务ID和用户分配信息 + * @return ActionResult 操作结果,仅表示操作是否成功 + */ + @PostMapping("/assign-user") + public ActionResult assignUser(@RequestBody @Validated FtbCultivateLearnAllocationDTO req) { + cultivateTaskService.allocation(req); + return ActionResult.success("成功"); + } + + + /** + * 我的培训任务列表 + */ + @GetMapping("/query-my-task-list") + public ActionResult> queryMyTaskList(V2MyCultivateTaskListForReq req) { + return ActionResult.success(cultivateTaskService.queryMyTaskListForApp(req)); + } + + + /** + * 我的培训任务列表tab小气泡 + * waitCompleteNum 待完成 + * completeNum 已完成 + * overNum 已逾期 + */ + @GetMapping("/query-my-task-count") + public ActionResult> queryMyTaskCount(@RequestParam(value = "keyWord", defaultValue = "", required = false) String keyWord) { + return ActionResult.success(cultivateTaskService.queryMyTaskCountForApp(keyWord)); + } + + + /** + * 根据任务id查询我的简单任务详情和阶段列表 + * + * @param myTaskId 我的任务id + */ + @GetMapping("/query-my-task-simple-info/{myTaskId}") + public ActionResult queryMyTaskSimpleInfo(@PathVariable("myTaskId") String myTaskId) { + return ActionResult.success("", cultivateTaskService.queryMyTaskSimpleInfoForApp(myTaskId)); + } + + + /** + * 根据任务id和阶段id查询我的任务一个阶段详情 + * + * @param myTaskId 任务id + * @param phase 阶段id + */ + @GetMapping("/query-my-task-info/{myTaskId}/{phase}") + public ActionResult queryMyTaskPhaseInfo(@PathVariable("myTaskId") String myTaskId, @PathVariable("phase") String phase) { + ActionResult success = ActionResult.success("", cultivateTaskService.queryMyTaskPhaseInfoForApp(myTaskId, phase)); + V2CultivateLearnTaskPhaseVo data = success.getData(); + if (data != null && CollUtil.isNotEmpty(data.getCourseList())) { + V2CourseAppDto dto = new V2CourseAppDto(); + dto.setCourseId(data.getCourseList().get(0).getCourseId()); + dto.setUserId(UserProvider.getUser().getUserId()); + dto.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + dto.setBusinessSourceId(data.getTaskId()); + cultivateCourseStudyUtil.checkCourseComplete(dto, data.getCourseList().get(0).getLearnState()); + } + return success; + } + + + /** + * 开始任务 + * + * @param myTaskId 我的任务id + */ + @PutMapping("/start-task/{myTaskId}") + public ActionResult startTask(@PathVariable("myTaskId") String myTaskId) { + //根据当前登录人设置任务开始 + cultivateTaskService.startTask(myTaskId, UserProvider.getUser().getUserId()); + return ActionResult.success(); + } + + + /** + * 检查用户任务所有阶段是否完成 + * + * @param req 检查请求参数 + * @return 用户任务所有阶段完成状态 + */ + @PostMapping("/check-all-phases-complete") + @Operation(summary = "检查用户任务所有阶段是否完成", description = "根据用户任务ID,返回该任务所有阶段的完成状态") + public ActionResult checkUserTaskAllPhasesComplete(@Valid @RequestBody CheckUserTaskAllPhasesCompleteReq req) { + CheckUserTaskAllPhasesCompleteVo result = cultivateTaskService.checkUserTaskAllPhasesComplete(req.getUserTaskId()); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/teaching/V2TeachingAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/teaching/V2TeachingAppController.java new file mode 100644 index 0000000..de63f49 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/app/teaching/V2TeachingAppController.java @@ -0,0 +1,61 @@ +package jnpf.cultivate.v2.controller.app.teaching; + +import jnpf.base.ActionResult; +import jnpf.cultivate.service.V2TeachingService; +import jnpf.model.cultivate.dto.teaching.RecordQueryDto; +import jnpf.model.cultivate.dto.teaching.TeachingApproveDto; +import jnpf.model.cultivate.v2.teaching.vo.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 带教管理[app] + */ +@Slf4j +@RestController +@RequestMapping("/v2/app/teachingRecord") +public class V2TeachingAppController { + + @Resource + private V2TeachingService v2TeachingService; + + /** + * 带教/练习记录[分页] + * @param queryDto 查询条件封装对象,包含分页参数和查询条件 + * @return 分页查询结果,包含带教记录列表和分页信息 + */ + @PostMapping("/page") + public ActionResult> page(@RequestBody RecordQueryDto queryDto) { + + List list = v2TeachingService.getPage(queryDto); + return ActionResult.success(list); + } + + /** + * 带教/练习详情 + * @param bizId 带教/练习id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/detail/{bizId}") + public ActionResult getTeachingDetail(@PathVariable(value = "bizId") String bizId) { + + TeachingDetailVo vo = v2TeachingService.getTeachingDetail(bizId); + return ActionResult.success(vo); + } + + /** + * 带教 - 审核 + * @param approveDto 审核dto + * @return java.lang.Object + */ + @PutMapping(value = "/approve") + public Object approveTeachingRecord(@RequestBody @Valid TeachingApproveDto approveDto) throws Exception { + + v2TeachingService.approveTeachingRecord(approveDto); + return ActionResult.success(); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/aihelper/V2CultivateAiHelperController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/aihelper/V2CultivateAiHelperController.java new file mode 100644 index 0000000..b1fb67b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/aihelper/V2CultivateAiHelperController.java @@ -0,0 +1,148 @@ +package jnpf.cultivate.v2.controller.web.aihelper; + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.V2CultivateCourseAppService; +import jnpf.cultivate.v2.service.V2CultivateCourseService; +import jnpf.cultivate.v2.service.V2CultivateExamStatisticsService; +import jnpf.cultivate.v2.service.V2CultivatePositionService; +import jnpf.model.cultivate.v2.course.vo.AiHelperCourseStatisticsVo; +import jnpf.model.cultivate.v2.course.vo.UserLearningStatusVo; +import jnpf.model.cultivate.v2.course.vo.app.LastStudyCourseVo; +import jnpf.model.cultivate.v2.exam.vo.AiHelperExamStatisticsVo; +import jnpf.model.cultivate.v2.position.vo.*; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +/** + * ai助手-培训接口 + * + * @author xgl + * @date 2026-01-27 14:04:01 + */ +@Slf4j +@RestController +@RequestMapping("/v2/ai-helper") +public class V2CultivateAiHelperController { + + + @Autowired + private V2CultivateCourseAppService v2CultivateCourseAppService; + + @Autowired + private V2CultivatePositionService v2CultivatePositionService; + + @Autowired + private V2CultivateExamStatisticsService v2CultivateExamStatisticsService; + + @Autowired + private V2CultivateCourseService v2CultivateCourseService; + + /** + * 最后一次学习完成的课程 + */ + @GetMapping("/course/query-last-study-course") + public ActionResult queryLastStudyCourse() { + LastStudyCourseVo vo = v2CultivateCourseAppService.queryLastStudyCourse(); + if (StringUtil.isEmpty(vo.getCourseId())) { + return ActionResult.success("成功", null); + } + if (vo.getState().equals(1)) { + return ActionResult.success("成功", null); + } + return ActionResult.success("成功", vo); + } + + /** + * 获取用户本岗位学习数据 + * + * @return + */ + @GetMapping("/self-position/position-study-detail") + public ActionResult positionStudyDetail() { + AppCultivatePositionDetailVo appCultivatePositionDetailVo = v2CultivatePositionService.positionStudyDetail(new V2CultivatePositionDetailForApp()); + if (CollUtil.isNotEmpty(appCultivatePositionDetailVo.getFtbCultivatePositionCourseList())) { + List ftbCultivatePositionCourseList = new ArrayList<>(); + for (AppCultivatePositionCourseVo item : appCultivatePositionDetailVo.getFtbCultivatePositionCourseList()) { + if (item.getLearnState().equals(1)) { + ftbCultivatePositionCourseList.add(item); + } + } + appCultivatePositionDetailVo.setFtbCultivatePositionCourseList(ftbCultivatePositionCourseList); + } + if (CollUtil.isNotEmpty(appCultivatePositionDetailVo.getExam())) { + List exam = new ArrayList<>(); + for (AppCultivatePositionCourseExamVo item : appCultivatePositionDetailVo.getExam()) { + if (item.getLearnState().equals(1)) { + exam.add(item); + } + } + appCultivatePositionDetailVo.setExam(exam); + } + if (CollUtil.isNotEmpty(appCultivatePositionDetailVo.getIdentity())) { + List identity = new ArrayList<>(); + for (AppCultivatePositionCourseIdentityVo item : appCultivatePositionDetailVo.getIdentity()) { + if (item.getIsCanIdentification().equals(1) && item.getIdentificationStatus().equals(1)) { + identity.add(item); + } + } + appCultivatePositionDetailVo.setIdentity(identity); + } + if (CollUtil.isNotEmpty(appCultivatePositionDetailVo.getPracticeList())) { + List practiceList = new ArrayList<>(); + for (AppCultivatePositionCoursePracticeVo item : appCultivatePositionDetailVo.getPracticeList()) { + if (item.getLearnState().equals(1)) { + practiceList.add(item); + } + } + appCultivatePositionDetailVo.setPracticeList(practiceList); + } + return ActionResult.success("成功", appCultivatePositionDetailVo); + + } + + /** + * 根据考试ID统计考试数据 + * + * @param examId 考试ID + * @return 考试统计结果 + */ + @GetMapping("/exam/statistics") + public ActionResult getExamStatistics(@RequestParam(value = "examId") String examId) { + AiHelperExamStatisticsVo statistics = v2CultivateExamStatisticsService.getExamStatistics(examId); + return ActionResult.success("成功", statistics); + } + + /** + * 根据课程ID统计课程学习数据 + * + * @param courseId 课程ID + * @return 课程学习统计结果 + */ + @GetMapping("/course/statistics") + public ActionResult getCourseStatistics(@RequestParam(value = "courseId") String courseId) { + AiHelperCourseStatisticsVo statistics = v2CultivateCourseService.getCourseStatistics(courseId); + return ActionResult.success("成功", statistics); + } + + /** + * 查询指定用户的学习情况 + * + * @param userId 用户ID + * @return 用户学习情况(包含已完成的课程、考试、鉴定列表,已去重) + */ + @GetMapping("/user/learning-status") + public ActionResult getUserLearningStatus(@RequestParam(value = "userId") String userId) { + UserLearningStatusVo learningStatus = v2CultivateCourseService.getUserLearningStatus(userId); + return ActionResult.success("成功", learningStatus); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/certificate/V2CertificateController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/certificate/V2CertificateController.java new file mode 100644 index 0000000..cee0cd3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/certificate/V2CertificateController.java @@ -0,0 +1,87 @@ +package jnpf.cultivate.v2.controller.web.certificate; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.cultivate.service.FtbCultivateCertificateImagesService; +import jnpf.cultivate.service.FtbCultivateCertificateService; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateImagesEntity; +import jnpf.model.cultivate.v2.certificate.req.DeleteCertificateImageReq; +import jnpf.model.cultivate.v2.certificate.req.V2SaveCertificateReq; +import jnpf.model.cultivate.v2.common.vo.V2BaseIdNameVo; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * web证书模块 + * + * @author shangyi + */ +@RestController +@RequestMapping("/v2/web/certificate") +public class V2CertificateController { + + private final FtbCultivateCertificateImagesService imagesService; + private final FtbCultivateCertificateService certificateService; + + public V2CertificateController(FtbCultivateCertificateImagesService imagesService, FtbCultivateCertificateService certificateService) { + this.imagesService = imagesService; + this.certificateService = certificateService; + } + + + /** + * 查询所有证书图片列表 + * + * @return ActionResult 包含所有证书图片实体列表的成功响应 + */ + @GetMapping("/style/list-all") + public ActionResult> listAll() { + return ActionResult.success(imagesService.listAll()); + } + + /** + * 添加新的证书样式 + * + * @param req 证书样式保存请求对象,包含样式名称、图片路径等信息 + * @return ActionResult 包含操作结果布尔值的成功响应 + */ + @PostMapping("/style/save") + public ActionResult save(@RequestBody @Valid V2SaveCertificateReq req) { + return ActionResult.success(imagesService.add(req)); + } + + /** + * 根据ID删除指定的证书样式 + * + * @param id 要删除的证书样式唯一标识符 + * @return ActionResult 包含删除操作结果布尔值的成功响应 + */ + @PostMapping("/style/del/{id}") + @Transactional(rollbackFor = Exception.class) + public ActionResult del(@PathVariable("id") String id, @Valid @RequestBody DeleteCertificateImageReq req) { + FtbCertificateImagesEntity imagesEntity = imagesService.getById(id); + if (imagesEntity == null) { + return ActionResult.fail("未找到该证书模版"); + } + certificateService.update(Wrappers.lambdaUpdate() + .eq(FtbCertificateEntity::getTemplate, imagesEntity.getPath()) + .set(FtbCertificateEntity::getTemplate, req.getPath()) + .set(FtbCertificateEntity::getPoints, req.getPoints())); + return ActionResult.success(imagesService.removeById(id)); + } + + /** + * 获取证书下拉选项列表 + * + * @return ActionResult 包含证书ID和名称对应关系列表的成功响应 + */ + @GetMapping("/select-options") + public ActionResult> selectOptions() { + return ActionResult.success(certificateService.getOption()); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CommonController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CommonController.java new file mode 100644 index 0000000..f8e22ab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CommonController.java @@ -0,0 +1,145 @@ +package jnpf.cultivate.v2.controller.web.common; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.CultivateCoverCategoryService; +import jnpf.cultivate.service.CultivateCoverInfoService; +import jnpf.cultivate.service.CultivateExamSettingService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.exam.FtbCultivateExamSettingEntity; +import jnpf.model.cultivate.v2.common.vo.V2BaseIdNameVo; +import jnpf.model.cultivate.v2.exam.req.V2ExamSettingReq; +import jnpf.model.cultivate.v2.exam.req.V2QueryCoverReq; +import jnpf.model.cultivate.v2.exam.req.V2SaveCoverCategoryReq; +import jnpf.model.cultivate.v2.exam.req.V2SaveCoverReq; +import jnpf.model.cultivate.v2.exam.vo.V2CoverVo; +import jnpf.model.cultivate.v2.exam.vo.V2TreeCoverVo; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * web通用模块 + * + * @author shangyi + */ +@RestController +@RequestMapping("/v2/web/common") +public class V2CommonController { + + @Resource + private CultivateCoverCategoryService coverCategoryService; + + @Resource + private CultivateCoverInfoService coverInfoService; + + @Resource + private CultivateExamSettingService examSettingService; + + + /** + * 封面分类列表 + * 封面分类类型: 0-自定义,1-系统内置 不传入-就查询所有 + * @return + */ + @PostMapping("/cover/category/list") + public ActionResult> categoryList(@RequestParam(value = "isSystem", required = false) Integer isSystem) { + return ActionResult.success(coverCategoryService.getCategoryList(isSystem)); + } + + /** + * 添加/编辑封面分类 + * + * @param req + * @return + */ + @PostMapping("/cover/category/save") + public ActionResult add(@RequestBody @Valid V2SaveCoverCategoryReq req) { + return ActionResult.success(coverCategoryService.saveCategory(req)); + } + + /** + * 删除封面分类 + * + * @param id + * @return + */ + @DeleteMapping("/cover/category/del/{id}") + public ActionResult coverCategoryDel(@PathVariable("id") String id) { + coverCategoryService.delCategory(id); + return ActionResult.success(); + } + + /** + * 添加封面 + * + * @param req + * @return + */ + @PostMapping("/cover/save") + public ActionResult add(@RequestBody @Valid V2SaveCoverReq req) { + coverInfoService.saveCover(req); + return ActionResult.success(); + } + + /** + * 删除封面 + * + * @param ids + * @return + */ + @DeleteMapping("/cover/del") + public ActionResult coverDel(@RequestBody List ids) { + coverInfoService.removeByIds(ids); + return ActionResult.success(); + } + + /** + * 获取封面列表 + * + * @param req + * @return + */ + @PostMapping("/cover/page") + public ActionResult> coverList(@RequestBody @Valid V2QueryCoverReq req) { + return ActionResult.success(CultivatePage.coverPageList(coverInfoService.getPageList(req))); + } + + + /** + * 获取所有封面,两级结构 + * + * @return + */ + @GetMapping("/cover/treeCover") + public ActionResult> coverTypeAll() { + return ActionResult.success(coverCategoryService.treeCover()); + } + + /** + * 增加/修改考试配置 + * + * @param req + * @return + */ + @PostMapping("/examSetting/save") + public ActionResult saveExamSetting(@RequestBody @Valid V2ExamSettingReq req) { + examSettingService.saveExamSetting(req); + return ActionResult.success(); + } + + /** + * 查询全局考试配置 + * + * @return 全局考试配置 + */ + @GetMapping("/examSetting/queryGlobalExamSetting") + public ActionResult queryGlobalExamSetting() { + return ActionResult.success(examSettingService.getById("1")); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CultivateCommonSettingGlobalController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CultivateCommonSettingGlobalController.java new file mode 100644 index 0000000..23b781f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/common/V2CultivateCommonSettingGlobalController.java @@ -0,0 +1,46 @@ +package jnpf.cultivate.v2.controller.web.common; + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.V2CultivateCommonSettingGlobalService; +import jnpf.model.cultivate.dto.FtbCultivateCommonSettingGlobalDTO; +import jnpf.model.cultivate.vo.FtbCultivateCommonSettingGlobalVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * web培训通用配置模块 + * + * @author xgl + * @date 2026/02/11 + */ +@RestController +@RequestMapping("/v2/web/cultivate/common/setting") +public class V2CultivateCommonSettingGlobalController { + + @Autowired + private V2CultivateCommonSettingGlobalService ftbCultivateCommonSettingGlobalService; + + /** + * 获取培训通用配置信息 + * + * @return {@link ActionResult}<{@link FtbCultivateCommonSettingGlobalVO}> + */ + @GetMapping("/info") + public ActionResult getInfo() { + return ActionResult.success(ftbCultivateCommonSettingGlobalService.getInfo()); + } + + /** + * 提交培训通用配置 + * + * @param req 配置信息 + * @return {@link ActionResult} + */ + @PostMapping("/update") + public ActionResult update(@RequestBody @Valid FtbCultivateCommonSettingGlobalDTO req) { + ftbCultivateCommonSettingGlobalService.update(req); + return ActionResult.success("提交成功"); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/FtbCultivateCourseSettingGlobalController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/FtbCultivateCourseSettingGlobalController.java new file mode 100644 index 0000000..05bad5d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/FtbCultivateCourseSettingGlobalController.java @@ -0,0 +1,46 @@ +package jnpf.cultivate.v2.controller.web.course; + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.FtbCultivateCourseSettingGlobalService; +import jnpf.model.cultivate.dto.FtbCultivateCourseSettingGlobalDTO; +import jnpf.model.cultivate.po.FtbCultivateCourseSettingGlobal; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + + +/** + * web课程全局配置模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/web/course/setting") +public class FtbCultivateCourseSettingGlobalController { + + @Autowired + private FtbCultivateCourseSettingGlobalService ftbCultivateCourseSettingGlobalService; + + /** + * 获取课程配置信息 + * + * @return {@link ActionResult}<{@link FtbCultivateCourseSettingGlobal}> + */ + @GetMapping("/info") + public ActionResult getInfo() { + return ActionResult.success(ftbCultivateCourseSettingGlobalService.getInfo()); + } + + /** + * 提交课程配置 + * + * @param req 课程配置信息 + * @return {@link ActionResult} + */ + @PostMapping("/update") + public ActionResult update(@RequestBody @Valid FtbCultivateCourseSettingGlobalDTO req) { + ftbCultivateCourseSettingGlobalService.update(req); + return ActionResult.success("提交成功"); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseStatisticesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseStatisticesController.java new file mode 100644 index 0000000..93d58b4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseStatisticesController.java @@ -0,0 +1,91 @@ +package jnpf.cultivate.v2.controller.web.course; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivateCourseStatisticesService; +import jnpf.model.cultivate.dto.course.FtbCultivateCourseOrgStatisticsDTO; +import jnpf.model.cultivate.dto.course.FtbCultivateCoursePersonStatisticesDTO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCourseOrgStatisticesVO; +import jnpf.model.cultivate.vo.course.web.FtbCultivateCoursePersonStatisticesVO; +import jnpf.util.EasyExcelUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * web培训数据看板 -课程统计 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/web/course/statistics") +public class V2CultivateCourseStatisticesController { + + @Autowired + private FtbCultivateCourseStatisticesService ftbCultivateCourseStatisticesService; + /** + * 组织维度统计 + *

+ * 根据传入的统计参数,查询并返回组织维度的统计数据。 + * + * @param statisticDTO 统计参数,包含查询条件和分页信息 + * @return 返回组织维度统计结果的分页列表视图对象 + */ + @PostMapping("/org-list-statistics") + public ActionResult> organizationListStatistics( + @RequestBody FtbCultivateCourseOrgStatisticsDTO statisticDTO) { + return ActionResult.success(ftbCultivateCourseStatisticesService.organizationListStatistics(statisticDTO)); + } + + /** + * 组织维度统计-导出 + *

+ * 根据传入的统计参数,查询组织维度统计数据,并将结果导出为Excel文件。 + * + * @param statisticDTO 统计参数,包含查询条件 + * @param response HTTP响应对象,用于输出Excel文件 + * @throws IOException 导出过程中可能抛出IO异常 + */ + @PostMapping("/org-list-statistics/export") + public void organizationListStatisticsExport(@RequestBody FtbCultivateCourseOrgStatisticsDTO statisticDTO, HttpServletResponse response) throws IOException { + statisticDTO.setPageSize(99999); + PageListVO listVO = ftbCultivateCourseStatisticesService.organizationListStatistics(statisticDTO); + EasyExcelUtil.simpleWrite(listVO.getList(), "组织维度统计", FtbCultivateCourseOrgStatisticesVO.class, response); + } + + /** + * 个人维度统计 + *

+ * 根据传入的统计参数,查询并返回个人维度的统计数据。 + * + * @param req 统计参数,包含查询条件和分页信息 + * @return 返回个人维度统计结果的分页列表视图对象 + */ + @PostMapping("/person-list-statistics") + public ActionResult> personListStatistics( + @RequestBody FtbCultivateCoursePersonStatisticesDTO req) { + return ActionResult.success(ftbCultivateCourseStatisticesService.personListStatistics(req)); + } + + /** + * 个人维度统计导出信息 + *

+ * 根据传入的统计参数,查询个人维度统计数据,并将结果导出为Excel文件。 + * + * @param req 统计参数,包含查询条件 + * @param response HTTP响应对象,用于输出Excel文件 + * @throws IOException 导出过程中可能抛出IO异常 + */ + @PostMapping("/person-list-statistics/export") + public void personListStatisticsExport(@RequestBody FtbCultivateCoursePersonStatisticesDTO req, HttpServletResponse response) throws IOException { + PageListVO listVO = ftbCultivateCourseStatisticesService.personListStatistics(req); + EasyExcelUtil.simpleWrite(listVO.getList(), "个人维度统计", FtbCultivateCoursePersonStatisticesVO.class, response); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseWebController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseWebController.java new file mode 100644 index 0000000..925c94c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/course/V2CultivateCourseWebController.java @@ -0,0 +1,100 @@ +package jnpf.cultivate.v2.controller.web.course; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivateCourseService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseListReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseSelectReq; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseDetailsVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCoursePageVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseSelectVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * web课程模块 + */ +@RestController +@RequestMapping("/v2/web/course") +public class V2CultivateCourseWebController { + @Autowired + private V2CultivateCourseService v2CultivateCourseService; + + /** + * 课程列表分页查询 + * + * @param req 条件参数 + * @return {@link ActionResult} + */ + @GetMapping("/query-list") + public ActionResult> list(V2CultivateCourseListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivateCourseService.webList(req))); + } + + + /** + * 添加课程 + * + * @param req 课程参数 + * @return String + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody V2CultivateCourseReq req) { + return ActionResult.success("成功", v2CultivateCourseService.add(req)); + } + + /** + * 修改课程信息 + * + * @param req 课程参数 + * @return {@link ActionResult} + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody V2CultivateCourseReq req) { + v2CultivateCourseService.update(req); + //处理是否重新学习 + v2CultivateCourseService.clearCourseLog(req.getId()); + return ActionResult.success(); + } + + + /** + * 删除课程 + * + * @param id 课程主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + v2CultivateCourseService.delete(id); + return ActionResult.success(); + } + + + /** + * 课程详情(根据课程id查询课程详情) + * + * @param id 课程主键id + * @return {@link ActionResult}<{@link FtbCultivateCourse}> + */ + @GetMapping("/details/{id}") + public ActionResult detail(@PathVariable("id") String id) { + return ActionResult.success(v2CultivateCourseService.detail(id)); + } + + + /** + * 添加岗位学习OR任务时,课程列表选择 + * + * @param req 分页请求对象 + * @return {@link ActionResult} + */ + @GetMapping("/learn-select-course-list") + public ActionResult> learnSelectCourseList(V2CultivateCourseSelectReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivateCourseService.learnSelectCourseList(req))); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamController.java new file mode 100644 index 0000000..6ee3e47 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamController.java @@ -0,0 +1,245 @@ +package jnpf.cultivate.v2.controller.web.exam; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.cultivate.v2.service.V2CultivatePositionService; +import jnpf.cultivate.v2.service.V2CultivateTaskService; +import jnpf.model.cultivate.req.exam.WebRestartExamReq; +import jnpf.model.cultivate.v2.exam.req.*; +import jnpf.model.cultivate.v2.exam.vo.*; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * 考试管理[web] + * + * @author yanwenfu + */ +@RestController +@RequestMapping("/v2/web/exam") +public class V2CultivateExamController { + + @Autowired + private V2CultivateExamService v2CultivateExamService; + + @Autowired + private V2CultivatePositionService v2CultivatePositionService; + + @Autowired + private V2CultivateTaskService cultivateTaskService; + + @PutMapping(value = "/test/del-question") + public void testDelQuestion(@RequestBody List questionIds) { + v2CultivateExamService.questionChangeDeal(questionIds); + } + + /** + * 考试列表[分页] + * + * @param req 查询参数 + * @return jnpf.base.ActionResult> + */ + @GetMapping("/list/page") + public ActionResult> getPage(V2QueryExamReq req) { + + PageInfo pageVo = v2CultivateExamService.getPage(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 我的考试列表[web](分页) + * + * @param queryDto 查询参数 + * @return jnpf.base.ActionResult> + */ + @GetMapping("/my-exam/page") + public ActionResult> getMyExamWebPage(MyExamWebQueryDto queryDto) { + + PageInfo pageVo = v2CultivateExamService.getMyExamWebPage(queryDto); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 新增岗位/自定义考试 + * + * @param req 考试参数 + * @return jnpf.base.ActionResult + */ + @PostMapping + public ActionResult addExam(@RequestBody @Valid V2SaveExamReq req) throws Exception { + + v2CultivateExamService.addExam(req); + return ActionResult.success(true); + } + + /** + * 编辑岗位/自定义考试 + * + * @param id 考试id + * @param req 考试参数 + * @return jnpf.base.ActionResult + */ + @PutMapping("/{id}") + public ActionResult updateExam(@PathVariable(value = "id") String id, @RequestBody @Valid V2SaveExamReq req) throws Exception { + + v2CultivateExamService.updateExam(id, req); + return ActionResult.success(true); + } + + /** + * 查询考试详情 + * + * @param id 考试id + * @param copy 是否复制(1: 是, 0: 否) + * @return jnpf.base.ActionResult + */ + @GetMapping("/detail/{id}") + public ActionResult getDetail(@PathVariable("id") String id, @RequestParam(value = "copy") Integer copy) { + + V2ExamVo vo = v2CultivateExamService.getDetail(id, copy); + return ActionResult.success(vo); + } + + /** + * 获取考试绑定状态- + * + * @param examId 考试id + * @return jnpf.base.ActionResult true-有绑定关系 false-无绑定关系 + */ + @GetMapping("/checkBindingStatus/{examId}") + public ActionResult checkBindingStatus(@PathVariable("examId") String examId) { + //查询是否有岗位学习绑定有考试 + boolean hasPositionBinding = v2CultivatePositionService.checkExamBinding(examId); + if (hasPositionBinding) { + return ActionResult.success(true); + } + + //查询是否有学习任务绑定有考试 + boolean hasTaskBinding = cultivateTaskService.checkExamBinding(examId); + if (hasTaskBinding) { + return ActionResult.success(true); + } + //只要有一个是绑定关系,则返回true + return ActionResult.success(false); + } + + /** + * 批阅详情 + * + * @param id 考试id + * @return jnpf.base.ActionResult + */ + @GetMapping("/mark-detail/{id}") + public ActionResult getMarkDetail(@PathVariable("id") String id) { + + V2ExamDetailVo v2ExamDetail = v2CultivateExamService.getMarkDetail(id); + return ActionResult.success(v2ExamDetail); + } + + /** + * 查看考卷详情 + * + * @param userExamId 用户的考试id + * @return jnpf.base.ActionResult + */ + @GetMapping("/paper-detail/{userExamId}") + public ActionResult getMarkPaperDetail(@PathVariable("userExamId") String userExamId) throws Exception { + + PaperDetailVo paperDetail = v2CultivateExamService.getMarkPaperDetail(userExamId); + return ActionResult.success(paperDetail); + } + + /** + * 删除用户的考试 + * @param userExamId 用户的考试id + * @return java.lang.Object + */ + @DeleteMapping(value = "/user-exam/{userExamId}") + public Object delUserExam(@PathVariable(value = "userExamId") String userExamId) throws Exception { + + v2CultivateExamService.delUserExam(userExamId); + return ActionResult.success(); + } + + /** + * 批阅详情 - 列表[分页] + * + * @param req 考试id + * @return jnpf.base.ActionResult + */ + @GetMapping("/mark-detail/page") + public ActionResult> getMarkDetailPage(MarkDetailQueryReq req) { + + PageInfo page = v2CultivateExamService.getMarkDetailPage(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 删除考试 + * + * @param id 考试id + * @return jnpf.base.ActionResult + */ + @DeleteMapping("/{id}") + public ActionResult delExam(@PathVariable("id") String id) throws Exception { + + v2CultivateExamService.delExam(id); + return ActionResult.success(true); + } + + /** + * 抽取题目数量及分值 + * + * @param bankIds 题库ids(英文逗号分隔) + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/bank-analysis") + public ActionResult getBankAnalysis(@RequestParam(value = "bankIds") String bankIds) throws Exception { + + if (StringUtil.isBlank(bankIds)) { + throw new Exception("请选择题库"); + } + BankAnalysisVo vo = v2CultivateExamService.getBankAnalysis(List.of(bankIds.split(","))); + return ActionResult.success(vo); + } + + /** + * 选择题目 + * + * @param req 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/question/list") + public ActionResult> getQuestionPage(QuestionReq req) { + + PageInfo page = v2CultivateExamService.getQuestionPage(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + + /** + * 设置重新考试 + * 用于批量重置用户的考试状态,允许用户重新参加考试 + * + * @param req 重新考试请求参数 + * - examId: 考试主键 ID(必填) + * - startTime: 常规考试开始时间,格式 yyyy-MM-dd HH:mm:ss + * - endTime: 常规考试结束时间,格式 yyyy-MM-dd HH:mm:ss + * - userExamIdList: 需要重考的用户考试 ID 集合 + * - versionBatch: 版本批次(必填) + * @return ActionResult 操作结果,成功返回 true + */ + @PostMapping("/setRestartExam") + public ActionResult setRestartExam(@Valid @RequestBody WebRestartExamReq req) { + v2CultivateExamService.webRestartExam(req); + return ActionResult.success("操作成功", true); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamStatisticsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamStatisticsController.java new file mode 100644 index 0000000..a295cac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamStatisticsController.java @@ -0,0 +1,90 @@ +package jnpf.cultivate.v2.controller.web.exam; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivateExamStatisticsService; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForOrgReq; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForPersonReq; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForOrgExcelVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForOrgVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForPersonExcelVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForPersonVo; +import jnpf.util.ConstantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * web数据统计考试关联 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/web/exam/statistics") +public class V2CultivateExamStatisticsController { + @Autowired + private V2CultivateExamStatisticsService statisticsService; + + + /** + * 考试统计,组织维度 + */ + @GetMapping("/queryExamStatisticsForOrg") + public ActionResult> queryExamStatisticsForOrg(V2ExamStatisticsForOrgReq dto) { + return ActionResult.success(statisticsService.queryExamStatisticsForOrg(dto)); + } + + /** + * 考试统计,组织维度,导出 + */ + @GetMapping("/queryExamStatisticsForOrg/export") + public void exportOrg(V2ExamStatisticsForOrgReq dto, HttpServletResponse response) throws IOException { + String fileName = "考试统计_组织维度_" + DateUtil.format(new Date(), "yyyyMMdd"); + dto.setPageSize(ConstantUtil.TEN_MILLION); + dto.setCurrentPage(1); + PageListVO pageVo = statisticsService.queryExamStatisticsForOrg(dto); + List excelList = BeanUtil.copyToList(pageVo.getList(), V2ExamStatisticsForOrgExcelVo.class); + EasyExcelUtils.exportExcel(response, fileName, excelList, V2ExamStatisticsForOrgExcelVo.class); + } + + /** + * 考试统计,个人维度 + */ + @PostMapping("/queryExamStatisticsForPerson") + public ActionResult> queryExamStatisticsForPerson(@RequestBody V2ExamStatisticsForPersonReq dto) { + PageInfo pageVo = statisticsService.queryExamStatisticsForPerson(dto); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 考试统计,个人维度,导出 + */ + @PostMapping("/queryExamStatisticsForPerson/export") + public void exportPerson(@RequestBody V2ExamStatisticsForPersonReq dto, HttpServletResponse response) throws IOException { + + PageInfo pageVo = statisticsService.queryExamStatisticsForPerson(dto); + List list = pageVo.getList(); + List excelVOList = new ArrayList<>(); + if (CollUtil.isNotEmpty(list)) { + for (V2ExamStatisticsForPersonVo vo : list) { + excelVOList.add(QuestionAnalysisUtil.convertToExcelPersonvoV2(vo)); + } + } + String fileName = "考试统计_个人维度_" + DateUtil.format(new Date(), "yyyyMMdd"); + EasyExcelUtils.exportExcel(response, fileName, excelVOList, V2ExamStatisticsForPersonExcelVo.class); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamUserController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamUserController.java new file mode 100644 index 0000000..3ab768f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/exam/V2CultivateExamUserController.java @@ -0,0 +1,81 @@ +package jnpf.cultivate.v2.controller.web.exam; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.v2.exam.req.V2QueryExamRecordReq; +import jnpf.model.cultivate.v2.exam.req.V2ReadOverExamReq; +import jnpf.model.cultivate.v2.exam.req.V2RestartExamReq; +import jnpf.model.cultivate.v2.exam.vo.V2ExamUserVo; +import jnpf.model.cultivate.v2.exam.vo.V2UserExamDetailVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +/** + * web用户考试管理模块 + * + * @author shangyi + */ +@RestController +@Slf4j +@RequestMapping("/v2/web/user-exam") +public class V2CultivateExamUserController { + + /** + * 批阅考试-用户考试列表 + * + * @param req + * @return + */ + @GetMapping("/record") + public ActionResult> record(V2QueryExamRecordReq req) { + return ActionResult.success(new PageListVO<>()); + } + + /** + * 批阅-详情 + * + * @param userExamId 考试用户ID + * @return + */ + @GetMapping("/get/{userExamId}") + public ActionResult get(@PathVariable("userExamId") String userExamId) { + return ActionResult.success(new V2UserExamDetailVo()); + } + + /** + * 批阅考试 + * + * @param req + * @return + */ + @PostMapping("/readOver") + public ActionResult readOver(@RequestBody V2ReadOverExamReq req) { + return ActionResult.success(true); + } + + + /** + * 发起重新考试 + * + * @param req 考试用户ID + * @return + */ + @PostMapping("/restartExam") + public ActionResult restartExam(@Valid @RequestBody V2RestartExamReq req) { + return ActionResult.success("操作成功", true); + } + + + /** + * 删除用户考试 + * @param id 用户考试ID + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + return ActionResult.success(true); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyApplyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyApplyController.java new file mode 100644 index 0000000..216cb4f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyApplyController.java @@ -0,0 +1,145 @@ +package jnpf.cultivate.v2.controller.web.identify; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.v2.service.V2CultivateIdentifyApplyService; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.enums.cultivate.ApplySourceEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplyListReq; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplySaveReq; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplySubmitReq; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyBasicInfoVo; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyInfoVo; +import jnpf.model.cultivate.v2.apply.vo.V2IdentifyApplyListVo; +import jnpf.model.cultivate.vo.identify.IdentifyApplyDetailsInfoVo; +import jnpf.util.ConstantUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * web实操鉴定用户申请模块 + * + * @author mouzeping + */ +@Slf4j +@RestController +@RequestMapping(value = "/v2/web/identify/apply") +public class V2CultivateIdentifyApplyController { + + @Autowired + private V2CultivateIdentifyApplyService applyService; + + /** + * 获取实操鉴定申请列表 + * + * @param req 查询条件对象,包含分页和筛选参数 + * @return 包含分页结果的ActionResult对象,数据为PageListVO类型 + */ + @Operation(summary = "列表") + @GetMapping(value = "/list") + public ActionResult> getWebPageApplyList(@Valid V2IdentifyApplyListReq req) { + return ActionResult.success(CultivatePage.coverPageList(applyService.getWebPageApplyList(req))); + } + + + /** + * 发起实操鉴定申请 + * + * @param req 实操鉴定申请保存请求对象,包含申请相关信息 + * @return 操作结果的ActionResult对象 + */ + @PostMapping(value = "/save") + public ActionResult applyDataSave(@Valid @RequestBody V2IdentifyApplySaveReq req) { + req.setSource(ApplySourceEnum.SDFQ.getCode()); + applyService.applyDataSaveBatch(req); + return ActionResult.success(MsgCode.SU018.get()); + } + + + /** + * 重新发起鉴定申请 + * + * @param id 需要重新鉴定的申请ID + * @return 操作结果的ActionResult对象 + */ + @Operation(summary = "重新鉴定") + @PutMapping(value = "/reIdentify/{id}") + public ActionResult applyDataReIdentify(@PathVariable("id") String id) { + applyService.applyDataReIdentify(id); + return ActionResult.success(MsgCode.SU005.get()); + } + + + /** + * 删除实操鉴定申请 + * + * @param id 需要删除的鉴定申请ID + * @return 操作结果的ActionResult对象 + */ + @Operation(summary = "删除") + @DeleteMapping(value = "/delete/{id}") + public ActionResult applyDataDelete(@PathVariable("id") String id) { + applyService.applyDataDelete(id, ConstantUtil.NUM_TRUE); + return ActionResult.success(MsgCode.SU003.get()); + } + + + /** + * 获取实操鉴定申请的基本信息详情 + * + * @param id 鉴定申请ID + * @return 包含基本信息的ActionResult对象,数据为V2IdentifyApplyBasicInfoVo类型 + */ + @Operation(summary = "详情(基本信息)") + @GetMapping(value = "/basicInfo/{id}") + public ActionResult getApplyBasicInfo(@PathVariable("id") String id) { + return ActionResult.success(applyService.getWebApplyBasicInfo(id)); + } + + /** + * 获取实操鉴定申请的详细信息 + * + * @param id 鉴定申请ID + * @return 包含详细信息的ActionResult对象,数据为V2IdentifyApplyInfoVo类型 + */ + @Operation(summary = "详情(鉴定详情)") + @GetMapping(value = "/info/{id}") + public ActionResult getApplyInfo(@PathVariable("id") String id) { + return ActionResult.success(applyService.getWebApplyInfo(id)); + } + + /** + * 重新提交鉴定记录 - 查询新鉴定项 + * + * @param id 鉴定申请ID + * @return 操作结果的ActionResult对象 + */ + @Operation(summary = "重新提交鉴定记录 - 查询新鉴定项") + @GetMapping(value = "/regenerate/{id}") + public ActionResult> regenerateIdentifyApply(@PathVariable("id") String id) { + List items = applyService.regenerateIdentifyApply(id); + return ActionResult.success(items); + } + + /** + * 重新提交鉴定记录 + * + * @param req 参数 + */ + @Operation(summary = "重新提交鉴定记录") + @PostMapping(value = "/re-submit") + public ActionResult reSubmit(@Valid @RequestBody V2IdentifyApplySubmitReq req) { + + applyService.reSubmit(req); + return ActionResult.success(); + } +} + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyCategoriesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyCategoriesController.java new file mode 100644 index 0000000..1a4bb74 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyCategoriesController.java @@ -0,0 +1,81 @@ +package jnpf.cultivate.v2.controller.web.identify; + + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyCategoriesService; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.v2.identify.req.QueryIdentifyCategoryListReq; +import jnpf.model.cultivate.v2.identify.req.V2SaveItemCategoryReq; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyCategoryTreeVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * web实操鉴项 分类模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/v2/web/identify/category") +public class V2CultivateIdentifyCategoriesController { + + + @Autowired + private FtbCultivateIdentifyCategoriesService ftbCultivateIdentifyCategoriesService; + + + /** + * 查询分类接口 + * + * @param req 查询参数 + * @return 查询结果 + */ + @GetMapping("/list") + public ActionResult> lists(QueryIdentifyCategoryListReq req) { + return ActionResult.success("成功", ftbCultivateIdentifyCategoriesService.getCategoryTree(req)); + } + + + /** + * 添加分类 + * + * @param req 添加参数 + * @return 添加结果 + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid V2SaveItemCategoryReq req) { + return ActionResult.success("添加成功", ftbCultivateIdentifyCategoriesService.insertData(req)); + } + + /** + * 修改分类 + * + * @param id 分类ID + * @param req 修改参数 + * @return 修改结果 + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid V2SaveItemCategoryReq req) { + return ActionResult.success("编辑成功", ftbCultivateIdentifyCategoriesService.updateData(id, req)); + } + + + /** + * 删除分类 + * + * @param id 分类ID + * @return 删除结果 + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + ftbCultivateIdentifyCategoriesService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyController.java new file mode 100644 index 0000000..cf118ba --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyController.java @@ -0,0 +1,236 @@ +package jnpf.cultivate.v2.controller.web.identify; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyCategoriesService; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyItemsPoolService; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.cultivate.v2.service.V2CultivateIdentifyTableService; +import jnpf.cultivate.v2.util.CultivateIdentityUtil; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableImportSaveReq; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableListReq; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableSaveReq; +import jnpf.model.cultivate.v2.identify.vo.*; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.util.RedisUtil; +import jnpf.util.excel.EasyExcelUtils; +import jnpf.util.excelv2.util.ExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * web实操鉴定表模块 + * + * @author xgl + */ +@Slf4j +@RestController +@RequestMapping(value = "/v2/web/identify/") +public class V2CultivateIdentifyController { + + + @Autowired + private V2CultivateIdentifyTableService v2CultivateIdentifyTableService; + + @Autowired + private FtbCultivateIdentifyCategoriesService ftbCultivateIdentifyCategoriesService; + + @Autowired + private FtbCultivateLabelService ftbCultivateLabelService; + + @Autowired + private FtbCultivateIdentifyItemsPoolService ftbCultivateIdentifyItemsPoolService; + + @Autowired + private RedisUtil redisUtil; + + + /** + * 鉴定表/列表 + * + * @param req 帅选条件 + * @return + * @ + */ + @Operation(summary = "列表") + @GetMapping(value = "/page-list") + public ActionResult> getPageList(@Valid V2IdentifyTableListReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivateIdentifyTableService.getPageList(req))); + } + + /** + * 鉴定表/详情 + * + * @return + * @ + */ + @Operation(summary = "详情") + @GetMapping(value = "/get/{tableId}") + public ActionResult getInfo(@PathVariable("tableId") String tableId) { + return ActionResult.success(v2CultivateIdentifyTableService.getInfo(tableId)); + } + + + /** + * 按分类获取鉴定表 + * + * @return + */ + @Operation(summary = "详情") + @GetMapping(value = "/getTableForCate") + public ActionResult> getTableForCate() { + return ActionResult.success("成功", v2CultivateIdentifyTableService.getTableForCate()); + + } + + /** + * 鉴定表/新增 + * + * @return + * @ + */ + @Operation(summary = "新增") + @PostMapping(value = "/add") + public ActionResult save(@Valid @RequestBody V2IdentifyTableSaveReq req) { + v2CultivateIdentifyTableService.saveData(req); + return ActionResult.success("操作成功", true); + } + + /** + * 鉴定表/修改 + * + * @return + */ + @Operation(summary = "修改") + @PutMapping(value = "/update/{tableId}") + public ActionResult update(@PathVariable("tableId") String tableId, @Valid @RequestBody V2IdentifyTableSaveReq req) { + v2CultivateIdentifyTableService.updateData(tableId, req); + return ActionResult.success(MsgCode.SU004.get()); + } + + + /** + * 鉴定表/删除 + * + * @return + * @ + */ + @Operation(summary = "删除") + @DeleteMapping(value = "/delete/{tableId}") + public ActionResult delete(@PathVariable("tableId") String tableId) { + v2CultivateIdentifyTableService.deleteData(tableId); + return ActionResult.success("删除成功", true); + } + + + /** + * 鉴定表导入 + * + * @param file + * @return + */ + @PostMapping("/table/import") + public ActionResult importData(@RequestPart("file") MultipartFile file) throws IOException { + + List itemCateList = ftbCultivateIdentifyCategoriesService.getAllList(); + List labelVoList = ftbCultivateLabelService.getList(2, 0); + if (CollUtil.isEmpty(itemCateList)) { + throw new RuntimeException("请先添加鉴定项分类"); + } + if (CollUtil.isEmpty(labelVoList)) { + throw new RuntimeException("请先添加鉴定表标签"); + } + InputStream inputStream = file.getInputStream(); + List> lists = CultivateIdentityUtil.readIrregularExcelRowByRow(inputStream); + long oldTableNum = 0L; + if (CollUtil.isNotEmpty(lists)) { + String tableName = lists.get(0).get(1); + if (StringUtils.isNotEmpty(tableName)) { + oldTableNum = v2CultivateIdentifyTableService.queryByTableName(tableName); + } + } + List poolList = ftbCultivateIdentifyItemsPoolService.listAll(); + V2IdentifyTableImportSaveReq v2IdentifyTableSaveReq = CultivateIdentityUtil.convertExcelData(lists, itemCateList, labelVoList, oldTableNum, poolList); + if (CollUtil.isNotEmpty(v2IdentifyTableSaveReq.getPreIdentifyImportVo().getItemList())) { + v2IdentifyTableSaveReq.getPreIdentifyImportVo().setIsSuccess(false); + redisUtil.insert(v2IdentifyTableSaveReq.getPreIdentifyImportVo().getUniqueId(), v2IdentifyTableSaveReq.getPreIdentifyImportVo(), 60 * 60); + } else { + v2CultivateIdentifyTableService.importData(v2IdentifyTableSaveReq); + } + + return ActionResult.success("成功", v2IdentifyTableSaveReq.getPreIdentifyImportVo()); + + } + + /** + * 鉴定表错误导出 + * + * @param key key + */ + @PostMapping("/table/down-error/{key}") + public void downError(@PathVariable(name = "key") String key, HttpServletResponse response) throws IOException { + if (!redisUtil.exists(key)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + V2PreIdentifyImportVo data = JSON.parseObject(redisUtil.getString(key).toString(), V2PreIdentifyImportVo.class); + if (data == null || CollUtil.isEmpty(data.getItemList())) { + throw new RuntimeException("无异常数据可以导出"); + } + ExcelUtils util = new ExcelUtils<>(V2PreIdentifyErrorVo.class); + util.exportExcel(response, "鉴定表导入异常数据.xlsx", "鉴定表导入异常数据", data.getItemList()); + } + + /** + * 下载导入模版 + * + * @param response 响应 + * @throws IOException 抛出异常 + */ + @GetMapping("/table/down-template") + public void importTemplate(HttpServletResponse response) throws IOException { + List allNameList = ftbCultivateIdentifyCategoriesService.getAllNameList(); + List labelVoList = ftbCultivateLabelService.getList(2, 0); + ExcelUtils.setResponseHeader(response, "鉴定表下载模版.xlsx"); + InputStream inputStream = EasyExcelUtils.checkExcelFile("https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/v2/face/xlsx/%E9%89%B4%E5%AE%9A%E8%A1%A8%E5%AF%BC%E5%85%A5%E6%A8%A1%E7%89%88.xlsx"); +// InputStream inputStream = EasyExcelUtils.checkExcelFile("http://127.0.0.1/1.xlsx"); + + Workbook workbook = WorkbookFactory.create(inputStream); + Sheet sheetAt = workbook.getSheetAt(0); + String[] itemCategoryNameList = new String[]{}; + if (CollUtil.isNotEmpty(allNameList)) { + itemCategoryNameList = allNameList.toArray(new String[0]); + } + ExcelUtils.setPromptOrValidation(sheetAt, itemCategoryNameList, "", 6, 1000, 2, 2); + String[] lableNameList = new String[]{}; + if (CollUtil.isNotEmpty(labelVoList)) { + ExcelUtils.setCellValue(sheetAt, 1, 1, labelVoList.get(0).getName()); + lableNameList = labelVoList.stream().map(FtbCultivateLabelVo::getName).toArray(String[]::new); + } + ExcelUtils.setPromptOrValidation(sheetAt, lableNameList, "", 1, 1, 1, 1); + + workbook.write(response.getOutputStream()); + + + } +} + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyItemsPoolController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyItemsPoolController.java new file mode 100644 index 0000000..b709bce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/identify/V2CultivateIdentifyItemsPoolController.java @@ -0,0 +1,380 @@ +package jnpf.cultivate.v2.controller.web.identify; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyCategoriesService; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyItemsPoolService; +import jnpf.cultivate.v2.util.CultivateIdentityUtil; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.common.req.V2BatchByPrimaryIdReq; +import jnpf.model.cultivate.v2.common.vo.V2PreImportVo; +import jnpf.model.cultivate.v2.item_pool.req.IdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.req.SaveCultivateIdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.vo.FtbCultivateIdentifyItemsPoolVo; +import jnpf.model.cultivate.v2.item_pool.vo.V2IdentifyItemExceptionVo; +import jnpf.model.cultivate.v2.item_pool.vo.V2IdentifyItemVo; +import jnpf.util.RedisUtil; +import jnpf.util.excelv2.util.ExcelUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.*; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.stream.Collectors; + +/** + * web实操鉴定项模块 + * + * @author lingma + * @since 2026-02-24 + */ +@Slf4j +@RestController +@RequestMapping("/v2/web/identify/items-pool") +@AllArgsConstructor +public class V2CultivateIdentifyItemsPoolController { + final private FtbCultivateIdentifyItemsPoolService itemsPoolService; + final private FtbCultivateIdentifyCategoriesService categoriesService; + final private RedisUtil redisUtil; + + + /** + * 查询鉴定项列表 + * + * @return 鉴定项池列表 + */ + @GetMapping("/list") + public ActionResult> list() { + return ActionResult.success("查询成功", itemsPoolService.listAll()); + } + + + /** + * 鉴定项列表(分页) + * + * @param req 查询参数 + * @return {@link ActionResult}<{@link PageListVO}<{@link FtbCultivateIdentifyItemsPoolVo}>> + */ + @GetMapping("/page-list") + public ActionResult> pageList(IdentifyItemsPoolReq req) { + return ActionResult.success(CultivatePage.coverPageList(itemsPoolService.webPageList(req))); + } + + /** + * 根据ID获取鉴定项详情 + * + * @param id 鉴定项池ID + * @return 鉴定项池详情 + */ + @GetMapping("/{id}") + public ActionResult getInfo(@PathVariable("id") String id) { + return ActionResult.success("查询成功", itemsPoolService.getInfo(id)); + } + + /** + * 添加鉴定项 + * + * @param req 鉴定项池信息 + * @return 添加结果 + */ + @PostMapping("/add") + public ActionResult add(@Valid @RequestBody SaveCultivateIdentifyItemsPoolReq req) { + req.preCheck(); + itemsPoolService.addData(req); + return ActionResult.success("添加成功", true); + } + + + /** + * 修改鉴定项 + * + * @param id 鉴定项池ID + * @param req 鉴定项池信息 + * @return 修改结果 + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Valid @RequestBody SaveCultivateIdentifyItemsPoolReq req) { + req.preCheck(); + itemsPoolService.updateData(id, req); + return ActionResult.success("编辑成功", true); + } + + /** + * 删除鉴定项 + * + * @param id 鉴定项池ID + * @return 删除结果 + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + itemsPoolService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + /** + * 批量删除鉴定项 + * + * @param req 批量删除参数 + * @return 删除结果 + */ + @DeleteMapping("/batch-del") + public ActionResult batchDel(@RequestBody @Validated V2BatchByPrimaryIdReq req) { + itemsPoolService.batchDel(req); + return ActionResult.success("删除成功", true); + } + + + /** + * 下载导入模版 + * + * @param response + * @throws IOException + */ + @GetMapping("/down-template") + public void importTemplate(HttpServletResponse response) throws IOException { + ExcelUtils util = new ExcelUtils<>(V2IdentifyItemVo.class); + + List allNameList = categoriesService.getAllNameList(); + if (CollUtil.isNotEmpty(allNameList)) { + List example = new ArrayList<>(); + V2IdentifyItemVo item = new V2IdentifyItemVo(); + item.setCateName(allNameList.get(0)); + item.setScore("0~100"); + item.setItemName("鉴定项内容"); + example.add(item); + util.exportExcel(response, "鉴定项导入模版.xlsx", "鉴定项导入模版", example); + } else { + util.exportExcel(response, "鉴定项导入模版.xlsx", "鉴定项导入模版", null); + } + + } + + /** + * 鉴定项导入 + * + * @param file + * @return + */ + @PostMapping("/import") + public ActionResult preImportData(@RequestPart("file") MultipartFile file) throws IOException { + // 将文件读取为字节数组,避免InputStream只能读取一次的问题 + byte[] fileBytes = file.getBytes(); + + //读取excel的第一行数据 判断表头 鉴定项内容 、鉴定项分类 、分值 + validateExcelHeader(new java.io.ByteArrayInputStream(fileBytes)); + + ExcelUtils util = new ExcelUtils<>(V2IdentifyItemVo.class); + List list = util.importExcel(new java.io.ByteArrayInputStream(fileBytes)); + if (CollUtil.isNotEmpty(list) && list.size() > 200) { + throw new RuntimeException("导入失败,请勿导入大于200条数据"); + } + V2PreImportVo v2PreImportVo = new V2PreImportVo(); + try { + v2PreImportVo = checkImportItem(list); + } catch (Exception e) { + + } + if (CollUtil.isEmpty(v2PreImportVo.getNormal()) && CollUtil.isEmpty(v2PreImportVo.getError())) { + throw new RuntimeException("导入失败,未检测到数据"); + } + if (CollUtil.isEmpty(v2PreImportVo.getError())) { + try { + itemsPoolService.batchAddData(v2PreImportVo.getNormal()); + return ActionResult.success("导入成功", v2PreImportVo); + } catch (Exception e) { + throw new RuntimeException("导入失败"); + } + } + redisUtil.insert(v2PreImportVo.getUniqueId(), v2PreImportVo, 60 * 60); + return ActionResult.success("成功", v2PreImportVo); + } + + /** + * 验证Excel表头是否正确 + * + * @param inputStream Excel文件输入流 + */ + private void validateExcelHeader(InputStream inputStream) { + try { + // 使用POI读取Excel文件(不关闭Workbook,避免关闭底层的InputStream) + Workbook workbook = WorkbookFactory.create(inputStream); + Sheet sheet = workbook.getSheetAt(0); + + if (sheet == null) { + throw new RuntimeException("导入模版不正确!"); + } + + // 获取第一行(表头行) + Row headerRow = sheet.getRow(0); + if (headerRow == null || headerRow.getPhysicalNumberOfCells() == 0) { + throw new RuntimeException("导入模版不正确!"); + } + + // 定义期望的表头(按照V2IdentifyItemVo中@Excel注解的顺序) + List expectedHeaders = Arrays.asList("鉴定项内容", "鉴定项分类", "分值"); + + // 提取实际表头值 + List actualHeaders = new ArrayList<>(); + for (int i = 0; i < headerRow.getPhysicalNumberOfCells(); i++) { + Cell cell = headerRow.getCell(i); + if (cell != null) { + String cellValue = getCellValue(cell); + if (StringUtils.isNotEmpty(cellValue)) { + actualHeaders.add(cellValue.trim()); + } + } + } + + // 将表头列表转换为字符串进行比较 + String expectedHeaderStr = String.join(",", expectedHeaders); + String actualHeaderStr = String.join(",", actualHeaders); + + // 验证表头是否正确 + if (!expectedHeaderStr.equals(actualHeaderStr)) { + throw new RuntimeException("导入模版不正确!"); + } + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + log.error("验证Excel表头失败", e); + throw new RuntimeException("导入失败,Excel文件格式错误"); + } + // 注意:不关闭Workbook,避免关闭底层的InputStream + // ByteArrayInputStream由调用方管理生命周期 + } + + /** + * 获取单元格的字符串值 + * + * @param cell 单元格 + * @return 单元格的字符串值 + */ + private String getCellValue(Cell cell) { + if (cell == null) { + return ""; + } + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + // 避免数字后面带.0 + double numericValue = cell.getNumericCellValue(); + if (numericValue == Math.floor(numericValue)) { + return String.valueOf((long) numericValue); + } else { + return String.valueOf(numericValue); + } + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + return cell.getCellFormula(); + default: + return ""; + } + } + + private V2PreImportVo checkImportItem(List list) { + List allCateList = categoriesService.getAllList(); + List dataList = itemsPoolService.listAll(); + List existNameList = new ArrayList<>(); + for (FtbCultivateIdentifyItemsPool itemsPool : dataList) { + String s = CultivateIdentityUtil.buildRepeatKey(itemsPool); + existNameList.add(s); + } + List cateNameList = new ArrayList<>(); + Map cateNameMap = new HashMap<>(); + for (FtbCultivateIdentifyCategories ftbCultivateIdentifyCategories : allCateList) { + cateNameMap.put(ftbCultivateIdentifyCategories.getName(), ftbCultivateIdentifyCategories.getId()); + cateNameList.add(ftbCultivateIdentifyCategories.getName()); + } + Set nameSet = new HashSet<>(); + for (V2IdentifyItemVo v2IdentifyItemVo : list) { + if (StringUtils.isEmpty(v2IdentifyItemVo.getItemName())) { + v2IdentifyItemVo.setMsg("鉴定项内容不能为空"); + v2IdentifyItemVo.setSuccess(1); + continue; + } + //判断鉴定项内容不能超过500个字 + if (v2IdentifyItemVo.getItemName().length() > 500) { + v2IdentifyItemVo.setMsg("鉴定项内容不能超过500个字"); + v2IdentifyItemVo.setSuccess(1); + continue; + } + + if (StringUtils.isEmpty(v2IdentifyItemVo.getCateName())) { + v2IdentifyItemVo.setMsg("鉴定项分类不能为空"); + v2IdentifyItemVo.setSuccess(1); + continue; + } + if (StringUtils.isEmpty(v2IdentifyItemVo.getScore())) { + v2IdentifyItemVo.setMsg("分值不能为空"); + } + CultivateIdentityUtil.parseAndValidateScore(v2IdentifyItemVo); + if (v2IdentifyItemVo.getSuccess().equals(1)) { + continue; + } + if (!cateNameList.contains(v2IdentifyItemVo.getCateName())) { + v2IdentifyItemVo.setMsg("鉴定项分类不存在"); + v2IdentifyItemVo.setSuccess(1); + } + v2IdentifyItemVo.setCateId(cateNameMap.get(v2IdentifyItemVo.getCateName())); + String repeat = CultivateIdentityUtil.buildRepeatKey(v2IdentifyItemVo); + if (nameSet.contains(repeat)) { + v2IdentifyItemVo.setMsg("鉴定项重复"); + } + if (existNameList.contains(repeat)) { + v2IdentifyItemVo.setMsg("鉴定项已存在"); + v2IdentifyItemVo.setSuccess(1); + } + nameSet.add(repeat); + } + V2PreImportVo v2PreImportVo = new V2PreImportVo(); + v2PreImportVo.setNormal(list.stream().filter(v2IdentifyItemVo -> v2IdentifyItemVo.getSuccess().equals(0)).collect(Collectors.toList())); + v2PreImportVo.setError(list.stream().filter(v2IdentifyItemVo -> v2IdentifyItemVo.getSuccess().equals(1)).collect(Collectors.toList())); + v2PreImportVo.setUniqueId(UUID.randomUUID().toString()); + v2PreImportVo.setNormalNumber(v2PreImportVo.getNormal().size()); + v2PreImportVo.setErrorNumber(v2PreImportVo.getError().size()); + return v2PreImportVo; + + } + + /** + * 鉴定项异常数据导出 + * + * @param key 预检测导入时返回的唯一标识(必传),用于导出异常数据 + */ + @GetMapping("/exception/{key}") + public void exceptionDataExport(@PathVariable(name = "key") String key, HttpServletResponse response) throws IOException { + + + if (!redisUtil.exists(key)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + V2PreImportVo data = JSON.parseObject(redisUtil.getString(key).toString(), V2PreImportVo.class); + if (data == null || CollUtil.isEmpty(data.getError())) { + throw new RuntimeException("无异常数据可以导出"); + } + ExcelUtils util = new ExcelUtils<>(V2IdentifyItemExceptionVo.class); + List v2IdentifyItemExceptionVos = BeanUtil.copyToList(data.getError(), V2IdentifyItemExceptionVo.class); + util.exportExcel(response, "鉴定项导入异常数据.xlsx", "鉴定项导入异常数据", v2IdentifyItemExceptionVos); + + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/label/FtbCultivateLabelController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/label/FtbCultivateLabelController.java new file mode 100644 index 0000000..ca29d54 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/label/FtbCultivateLabelController.java @@ -0,0 +1,92 @@ +package jnpf.cultivate.v2.controller.web.label; + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.model.cultivate.v2.label.dto.FtbCultivateLabelReq; +import jnpf.model.cultivate.v2.label.dto.FtbCultivateUpdateLabelReq; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * web课程标签模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/web/label") +public class FtbCultivateLabelController { + + @Autowired + private FtbCultivateLabelService ftbCultivateLabelService; + + /** + * 获取培训标签关联表列表 + * type 1-课程标签 2-鉴定表标签 3-带教技能点 + * source 类型:0-自定义,1-系统默认 ‘’-全部 + * + * @return 返回标签列表 + */ + @GetMapping("/list") + public ActionResult> getList(@RequestParam(value = "type") Integer type, @RequestParam(value = "source", defaultValue = "", required = false) String source) { + if (null == type) { + throw new RuntimeException("参数错误,请传入类型"); + } + Integer searchSource = null; + if (StringUtils.isNotEmpty(source)) { + searchSource = Integer.parseInt(source); + } + return ActionResult.success(ftbCultivateLabelService.getList(type, searchSource)); + } + + + /** + * 新增标签 + * + * @param req 请求参数 + * @return 返回添加的标签 + */ + @PostMapping + public ActionResult add(@RequestBody @Valid FtbCultivateLabelReq req) { + return ActionResult.success("新建成功", ftbCultivateLabelService.addData(req)); + } + + /** + * 修改培训标签 + * + * @param id 标签id + * @param req 请求参数 + * @return 返回修改的标签 + */ + @PutMapping("/{id}") + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid FtbCultivateUpdateLabelReq req) { + return ActionResult.success("修改成功", ftbCultivateLabelService.updateData(id, req)); + } + + + /** + * 获取培训标签信息 + * + * @param id 主键ID + * @return 培训标签信息 + */ + @GetMapping("/get/{id}") + public ActionResult getInfo(@PathVariable("id") String id) { + return ActionResult.success(ftbCultivateLabelService.getInfo(id)); + } + + /** + * 删除标签 + * + * @param id 主键ID + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbCultivateLabelService.deleteData(id); + return ActionResult.success("删除成功"); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/old/V2CultivateOldDealController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/old/V2CultivateOldDealController.java new file mode 100644 index 0000000..832eda2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/old/V2CultivateOldDealController.java @@ -0,0 +1,676 @@ +package jnpf.cultivate.v2.controller.web.old; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.cultivate.V2CultivateOldDealApi; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.CultivateIdentifyItemsService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyItemsPoolService; +import jnpf.cultivate.v2.service.V2CourseGainedCommentService; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.cultivate.v2.util.CultivateIdentityUtil; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.enums.AppraisalScoreTypeEnum; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionSetting; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionUser; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.entity.UserEntity; +import jnpf.util.CustomTenantUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.SelfGrowthUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 培训旧数据处理模块 + */ +@RestController +@Slf4j +@RequestMapping("/v2/cultivate/old") +public class V2CultivateOldDealController implements V2CultivateOldDealApi { + + @Autowired + private V2CourseGainedCommentService ftbCourseGainedCommentService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Resource + private V2CultivateExamService v2CultivateExamService; + @Resource + private CustomTenantUtil customTenantUtil; + + @Autowired + private FtbCultivateIdentifyItemsPoolService itemsPoolService; + + @Autowired + private CultivateIdentifyItemsService itemsService; + + @Resource + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + @Resource + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + @Resource + private FtbCultivateLearnTaskCourseMapper ftbCultivateLearnTaskCourseMapper; + + @Resource + private FtbCultivateLearnTaskExamMapper ftbCultivateLearnTaskExamMapper; + + @Resource + private FtbCultivateLearnTaskIdentificationMapper ftbCultivateLearnTaskIdentificationMapper; + + @Resource + private FtbCultivateLearnTaskCertificateMapper ftbCultivateLearnTaskCertificateMapper; + + @Resource + private FtbCultivateTaskLogMapper ftbCultivateTaskLogMapper; + + @Resource + private FtbCultivateCourseLearningLogMapper ftbCultivateCourseLearningLogMapper; + + @Resource + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Resource + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + @Resource + private CultivateIdentifyApplyMapper cultivateIdentifyApplyMapper; + + @Resource + private FtbCultivatePromotionMemberNewMapper ftbCultivatePromotionMemberNewMapper; + + @Resource + private FtbCultivatePromotionUserMapper ftbCultivatePromotionUserMapper; + + @Resource + private FtbCultivatePromotionSettingMapper ftbCultivatePromotionSettingMapper; + + + /** + * 课程心得评论旧数据处理 V2.0版本 上线处理 + * + * @param tenantId 租户ID + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/gained/comment") + @Override + public ActionResult dealOldData(@RequestParam("tenantId") String tenantId) { + userApiV2Util.checkOutTenant(tenantId); + Boolean result = ftbCourseGainedCommentService.dealOldData(tenantId); + return ActionResult.success(result); + } + + /** + * 鉴定项旧数据处理 + * + * @param tenantId 租户ID + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/identify/deal-old") + @Override + @Transactional + public ActionResult identifyDealOldData(String tenantId) { + + userApiV2Util.checkOutTenant(tenantId); + // 1. 查询所有未删除的鉴定项 + List allItems = itemsService.list( + Wrappers.lambdaQuery() + .eq(CultivateIdentifyItems::getPoolId, "") + .eq(CultivateIdentifyItems::getDeleteMark, 0) + ); + + if (allItems == null || allItems.isEmpty()) { + return ActionResult.success(true); + } + List ftbCultivateIdentifyItemsPools = itemsPoolService.listAll(); + Map oldItemsMap = new HashMap<>(); + Map oldBisinessIdMap = new HashMap<>(); + Map oldPoolMap = new HashMap<>(); + if (CollUtil.isNotEmpty(ftbCultivateIdentifyItemsPools)) { + for (FtbCultivateIdentifyItemsPool pool : ftbCultivateIdentifyItemsPools) { + oldItemsMap.put(CultivateIdentityUtil.buildRepeatKey(pool), pool.getId()); + oldPoolMap.put(CultivateIdentityUtil.buildRepeatKey(pool), pool); + } + } + List needAdd = new ArrayList<>(); + for (CultivateIdentifyItems item : allItems) { + String itemString = CultivateIdentityUtil.buildRepeatKey(item); + String poolId = oldItemsMap.get(itemString); + + if (StringUtils.isEmpty(poolId)) { + poolId = UserApiV2Util.generateId(); + oldItemsMap.put(itemString, poolId); + item.setBusinessId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.ITEM_POOL)); + oldBisinessIdMap.put(itemString, item.getBusinessId()); + needAdd.add(item); + } else { + item.setBusinessId(oldBisinessIdMap.get(itemString)); + } + item.setPoolId(poolId); + } + + itemsService.updateBatchById(allItems); + // 3. 为每组创建鉴定项池记录,并更新鉴定项的poolId + List poolList = new ArrayList<>(); + if (CollUtil.isNotEmpty(needAdd)) { + for (CultivateIdentifyItems identifyItems : needAdd) { + // 创建鉴定项池记录 + FtbCultivateIdentifyItemsPool pool = new FtbCultivateIdentifyItemsPool(); + pool.setId(identifyItems.getPoolId()); + pool.setName(identifyItems.getName()); + pool.setType(identifyItems.getType()); + pool.setCateId("2"); + if (identifyItems.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + pool.setTotalScore(identifyItems.getScore().intValue()); + } else { + pool.setMinScore(identifyItems.getMinScore()); + pool.setMaxScore(identifyItems.getMaxScore()); + } + pool.setBusinessId(identifyItems.getBusinessId()); + pool.setEnabledMark(0); + pool.setCreatorUserId("349057407209541"); + pool.setCreatorTime(new Date()); + pool.setLastModifyUserId("349057407209541"); + pool.setLastModifyTime(new Date()); + poolList.add(pool); + } + } + + if (CollUtil.isNotEmpty(poolList)) { + itemsPoolService.saveBatch(poolList); + } + return ActionResult.success(true); + } + + @NoDataSourceBind + @GetMapping(value = "/deal-data") + public ActionResult dealExamOldData(@RequestParam String tenantId) { + + customTenantUtil.checkOutTenant(tenantId); + Boolean b = v2CultivateExamService.dealExamOldData(); + return ActionResult.success(b); + } + + /** + * 迁移旧培训任务数据到新的任务日志表 + * 将旧培训任务中每个人的学习情况同步到新建立的中间表中 ftb_cultivate_task_log + * 默认第一阶段的阶段id就是用任务的主键id + * + * @param tenantId 租户ID + * @return 响应 + */ + @NoDataSourceBind + @GetMapping("/task/old-data") + @Transactional + @Override + public ActionResult migrateOldTaskData(@RequestParam("tenantId") String tenantId) { + userApiV2Util.checkOutTenant(tenantId); + + try { + log.info("开始迁移旧培训任务数据..."); + + // 1. 查询所有有效的旧培训任务 + List allTasks = ftbCultivateLearnTaskMapper.selectList( + Wrappers.lambdaQuery() + .eq(FtbCultivateLearnTask::getEnableMark, 0) + ); + + if (allTasks == null || allTasks.isEmpty()) { + log.info("没有找到有效的培训任务"); + return ActionResult.success("没有找到需要迁移的数据"); + } + + log.info("共找到 {} 个有效培训任务", allTasks.size()); + + // 预加载所有任务的课程、考试、鉴定、证书配置,避免N+1查询 + Map> taskCoursesMap = new HashMap<>(); + Map> taskExamsMap = new HashMap<>(); + Map> taskIdentificationsMap = new HashMap<>(); + Map> taskCertificatesMap = new HashMap<>(); + + // 提取所有任务ID + List taskIds = allTasks.stream() + .map(FtbCultivateLearnTask::getId) + .collect(Collectors.toList()); + + + // 批量查询所有任务关联的课程 + List allCourses = ftbCultivateLearnTaskCourseMapper.selectList( + Wrappers.lambdaQuery() + .in(FtbCultivateLearnTaskCourse::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskCourse::getEnableMark, 0) + ); + // 按任务ID分组 + taskCoursesMap = allCourses.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskCourse::getTaskId)); + + // 批量查询所有任务关联的考试 + List allExams = ftbCultivateLearnTaskExamMapper.selectList( + Wrappers.lambdaQuery() + .in(FtbCultivateLearnTaskExam::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskExam::getEnableMark, 0) + ); + // 按任务ID分组 + taskExamsMap = allExams.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskExam::getTaskId)); + + // 批量查询所有任务关联的鉴定 + List allIdentifications = ftbCultivateLearnTaskIdentificationMapper.selectList( + Wrappers.lambdaQuery() + .in(FtbCultivateLearnTaskIdentification::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0) + ); + // 按任务ID分组 + taskIdentificationsMap = allIdentifications.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskIdentification::getTaskId)); + + // 批量查询所有任务关联的证书 + List allCertificates = ftbCultivateLearnTaskCertificateMapper.selectList( + Wrappers.lambdaQuery() + .in(FtbCultivateLearnTaskCertificate::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskCertificate::getEnableMark, 0) + ); + // 按任务ID分组 + taskCertificatesMap = allCertificates.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskCertificate::getTaskId)); + + + log.info("完成预加载任务配置信息"); + + int totalMigrated = 0; + int processedTaskCount = 0; + Date now = new Date(); + + // 2. 遍历每个任务,处理其关联的用户学习情况 + for (FtbCultivateLearnTask task : allTasks) { + String taskId = task.getId(); + processedTaskCount++; + + if (processedTaskCount % 10 == 0) { + log.info("正在处理第 {}/{} 个任务: {}", processedTaskCount, allTasks.size(), task.getTaskName()); + } + + // 3. 查询该任务下所有分配的用户 + List assignments = ftbCultivateLearnTaskAssignmentMapper.selectList( + Wrappers.lambdaQuery() + .eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + ); + + if (assignments == null || assignments.isEmpty()) { + continue; + } + + // 获取该任务的配置信息 + List courses = taskCoursesMap.getOrDefault(taskId, new ArrayList<>()); + List exams = taskExamsMap.getOrDefault(taskId, new ArrayList<>()); + List identifications = taskIdentificationsMap.getOrDefault(taskId, new ArrayList<>()); + List certificates = taskCertificatesMap.getOrDefault(taskId, new ArrayList<>()); + + // 准备批量插入/更新的列表 + List toInsert = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + + // 4. 为每个用户创建或更新任务日志记录 + for (FtbCultivateLearnTaskAssignment assignment : assignments) { + String userId = assignment.getUserId(); + + // 使用任务ID作为第一阶段ID + String phaseId = taskId; + + // 检查是否已存在该任务-用户-阶段的日志记录 + FtbCultivateTaskLog existingLog = ftbCultivateTaskLogMapper.selectOne( + Wrappers.lambdaQuery() + .eq(FtbCultivateTaskLog::getTaskId, taskId) + .eq(FtbCultivateTaskLog::getUserId, userId) + .eq(FtbCultivateTaskLog::getPhaseId, phaseId) + .eq(FtbCultivateTaskLog::getEnabledMark, 0) + ); + + FtbCultivateTaskLog taskLog; + if (existingLog != null) { + // 如果已存在,则更新 + taskLog = existingLog; + } else { + // 如果不存在,则创建新记录 + taskLog = new FtbCultivateTaskLog(); + taskLog.setTaskId(taskId); + taskLog.setUserId(userId); + taskLog.setPhaseId(phaseId); + taskLog.setEnabledMark(0); + taskLog.setCreatorUserId(userId); + taskLog.setCreatorTime(now); + } + + // 设置基本状态信息 + // 根据用户的学习状态设置任务日志状态 + // 学习状态 0未开始,1进行中,2已完成,3已逾期 + Integer studyStats = assignment.getStudyStats(); + if (studyStats == 2) { + taskLog.setState(2); // 2-已完成 + } else { + taskLog.setState(1); // 默认未完成 + } + + if (CollUtil.isNotEmpty(certificates)) { + // 设置证书颁发状态 + taskLog.setIssuedCertificate(assignment.getIssuedCertificate()); + } else { + taskLog.setIssuedCertificate(-1); // 默认未配置证书 + } + + // 查询并设置课程状态:检查用户对任务中所有课程的完成情况 + if (!courses.isEmpty()) { + // 提取所有课程ID + List courseIds = new ArrayList<>(); + for (FtbCultivateLearnTaskCourse course : courses) { + courseIds.add(course.getCourseId()); + } + + // 查询用户对这些课程的完成情况(使用ftb_cultivate_position_cource_learning表) + Long completedCount = ftbCultivatePositionCourceLearningMapper.countCompletedCoursesByUserAndCourseIds( + courseIds, userId + ); + + // 判断是否所有课程都已完成 + boolean allCoursesCompleted = false; + if (completedCount != null && completedCount > 0) { + // 如果完成的课程数量等于任务配置的课程数量,则全部完成 + allCoursesCompleted = completedCount >= courseIds.size(); + } + + // 设置课程状态:1-已完成,0-未完成 + taskLog.setCourseState(allCoursesCompleted ? 1 : 0); + } else { + taskLog.setCourseState(-1); // 未配置课程 + } + + // 设置考试状态:查询用户对任务中所有考试的完成情况 + if (!exams.isEmpty()) { + // 提取所有考试ID + List examIds = new ArrayList<>(); + for (FtbCultivateLearnTaskExam exam : exams) { + examIds.add(exam.getExamId()); + } + + // 查询用户对这些考试的记录(使用ftb_cultivate_exam_user表) + List examUsers = ftbCultivateExamUserMapper.selectList( + Wrappers.lambdaQuery() + .in(FtbCultivateExamUser::getExamId, examIds) + .eq(FtbCultivateExamUser::getUserId, userId) + .eq(FtbCultivateExamUser::getExamSource, 4) + .eq(FtbCultivateExamUser::getRelationTaskId, taskId) + .orderByDesc(FtbCultivateExamUser::getCreatorTime) + .eq(FtbCultivateExamUser::getEnabledMark, 1) + ); + + // 判断是否有合格或优秀的考试记录 + boolean examCompleted = false; + if (CollUtil.isNotEmpty(examUsers)) { + // 检查是否有合格(3)或优秀(5)的记录 + if (examUsers.get(0).getStatus() == 3 || examUsers.get(0).getStatus() == 4 || examUsers.get(0).getStatus() == 5) { + examCompleted = true; + } + taskLog.setExamResult(examUsers.get(0).getStatus()); + taskLog.setUserExamId(examUsers.get(0).getId()); + taskLog.setExamState(examCompleted ? 1 : 0); + } else { + taskLog.setExamState(-1); // 未配置考试 + taskLog.setExamResult(-1); // 未配置考试 + } + + } + + // 设置鉴定状态:如果有鉴定配置则为0(未完成),否则为-1(未配置),再查询一下这个鉴定关联的数据 + if (!identifications.isEmpty()) { + // 提取所有鉴定ID + List identifyIds = new ArrayList<>(); + for (FtbCultivateLearnTaskIdentification identification : identifications) { + identifyIds.add(identification.getIdentificationId()); + } + + // 查询用户对这些鉴定的记录(使用ftb_cultivate_identify_apply表,关联ftb_cultivate_identify_apply_table_backups) + List identifyApplies = cultivateIdentifyApplyMapper.queryIdentifyApply( + userId, + identifyIds, + 4, // 4-任务鉴定 + taskId + ); + + // 判断是否有合格或优秀的鉴定记录 + + if (CollUtil.isNotEmpty(identifyApplies)) { + CultivateIdentifyApply latestApply = identifyApplies.get(0); + taskLog.setUserIdentifyId(latestApply.getId()); + if (latestApply.getStatus().equals(1)) { + // 检查是否有合格(0)或优秀(1)的记录 + taskLog.setIdentifyResult(latestApply.getResult()); + } else { + taskLog.setIdentifyState(0); + } + + } else { + taskLog.setIdentifyState(0); + } + + } else { + taskLog.setIdentifyState(-1); // 未配置鉴定 + taskLog.setIdentifyResult(-1); // 未配置鉴定 + } + + // 练习状态默认为-1(未配置),因为旧数据中没有练习相关表 + taskLog.setPracticeState(-1); + + + // 添加到对应的列表 + if (existingLog != null) { + taskLog.setLastModifyUserId(userId); + taskLog.setLastModifyTime(now); + toUpdate.add(taskLog); + } else { + taskLog.setLastModifyUserId(userId); + taskLog.setLastModifyTime(now); + toInsert.add(taskLog); + } + totalMigrated++; + } + + // 批量插入和更新 + if (!toInsert.isEmpty()) { + for (FtbCultivateTaskLog log : toInsert) { + ftbCultivateTaskLogMapper.insert(log); + } + } + if (!toUpdate.isEmpty()) { + for (FtbCultivateTaskLog log : toUpdate) { + ftbCultivateTaskLogMapper.updateById(log); + } + } + } + + log.info("旧培训任务数据迁移完成,共处理 {} 个任务,迁移 {} 条记录", processedTaskCount, totalMigrated); + return ActionResult.success("迁移完成,共处理 " + processedTaskCount + " 个任务," + totalMigrated + " 条用户任务记录"); + + } catch (Exception e) { + log.error("迁移旧培训任务数据时发生错误", e); + return ActionResult.fail("迁移失败: " + e.getMessage()); + } + } + + /** + * 旧晋升通道成员数据迁移接口 + * 将 ftb_cultivate_promotion_member_new 表中的用户数据迁移到 + * ftb_cultivate_promotion_user 和 ftb_cultivate_promotion_setting 表中 + * 同时校验用户是否为正常状态(enabledMark = 1) + * + * @param tenantId 租户ID + * @param promotionId 晋升通道ID(可选,如果传入则只迁移指定通道的数据) + * @return 响应结果 + */ + @NoDataSourceBind + @GetMapping("/promotion/migrate-old-data") + @Transactional(rollbackFor = Exception.class) + @Override + public ActionResult migratePromotionOldData(@RequestParam("tenantId") String tenantId, + @RequestParam(value = "promotionId", required = false) String promotionId) { + userApiV2Util.checkOutTenant(tenantId); + + try { + log.info("开始迁移旧晋升通道成员数据..."); + if (StringUtils.isNotEmpty(promotionId)) { + log.info("指定晋升通道ID: {}", promotionId); + } + + // 1. 查询所有有效的旧晋升通道成员数据(deleteMark=0表示未删除) + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); + + // 如果传入了promotionId,则只查询指定通道的数据 + if (StringUtils.isNotEmpty(promotionId)) { + queryWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId); + } + + List memberNewList = ftbCultivatePromotionMemberNewMapper.selectList(queryWrapper); + + if (memberNewList == null || memberNewList.isEmpty()) { + log.info("没有找到有效的旧晋升通道成员数据"); + return ActionResult.success("没有找到需要迁移的数据"); + } + + log.info("共找到 {} 条旧晋升通道成员数据", memberNewList.size()); + + // 2. 提取所有用户ID + Set userIds = memberNewList.stream() + .map(FtbCultivatePromotionMemberNew::getUserId) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toSet()); + + if (userIds.isEmpty()) { + log.warn("旧数据中没有有效的用户ID"); + return ActionResult.success("没有有效的用户数据需要迁移"); + } + + List userNameForUserIdsReturnList = userApiV2Util.getUserNameForUserIdsReturnList(new ArrayList<>(userIds), tenantId); + log.info("共提取到 {} 个唯一用户ID", userIds.size()); + if (CollUtil.isEmpty(userNameForUserIdsReturnList)) { + return ActionResult.success("没有有效的用户数据需要迁移"); + } + + // 3. 过滤出在用户列表中存在的成员(确保用户有效) + Set validUserIds = userNameForUserIdsReturnList.stream() + .map(UserEntity::getId) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toSet()); + + List validMembers = memberNewList.stream() + .filter(member -> validUserIds.contains(member.getUserId())) + .collect(Collectors.toList()); + + if (validMembers.isEmpty()) { + log.info("没有有效的用户数据需要迁移"); + return ActionResult.success("没有有效的用户数据需要迁移"); + } + + log.info("共有 {} 条用户数据需要迁移", validMembers.size()); + + // 4. 准备批量插入的数据 + Date now = new Date(); + String systemUserId = "349057407209541"; // 系统用户ID + + List promotionUsersToInsert = new ArrayList<>(); + List promotionSettingsToInsert = new ArrayList<>(); + + + int migratedCount = 0; + int skippedCount = 0; + + // 6. 处理每个有效的成员数据 + for (FtbCultivatePromotionMemberNew member : validMembers) { + String memberPromotionId = member.getPromotionId(); + String userId = member.getUserId(); + + + FtbCultivatePromotionUser promotionUser = new FtbCultivatePromotionUser(); + promotionUser.setId(UserApiV2Util.generateId()); + promotionUser.setPromotionId(memberPromotionId); + promotionUser.setUserId(userId); + promotionUser.setEnabledMark(0); + promotionUser.setUserSelect(0); + promotionUser.setIsComplete(0); + promotionUser.setCreatorUserId(systemUserId); + promotionUser.setCreatorTime(now); + promotionUser.setLastModifyUserId(systemUserId); + promotionUser.setLastModifyTime(now); + promotionUsersToInsert.add(promotionUser); + migratedCount++; + + + FtbCultivatePromotionSetting userSetting = new FtbCultivatePromotionSetting(); + userSetting.setId(UserApiV2Util.generateId()); + userSetting.setPromotionId(memberPromotionId); + userSetting.setScope(3); // 3-用户 + userSetting.setPostId(""); + userSetting.setScopeId(userId); + userSetting.setEnabledMark(0); + userSetting.setCreatorUserId(systemUserId); + userSetting.setCreatorTime(now); + userSetting.setLastModifyUserId(systemUserId); + userSetting.setLastModifyTime(now); + promotionSettingsToInsert.add(userSetting); + + } + + // 7. 批量插入数据 + if (!promotionUsersToInsert.isEmpty()) { + for (FtbCultivatePromotionUser user : promotionUsersToInsert) { + ftbCultivatePromotionUserMapper.insert(user); + } + log.info("成功插入 {} 条晋升用户记录", promotionUsersToInsert.size()); + } + + if (!promotionSettingsToInsert.isEmpty()) { + for (FtbCultivatePromotionSetting setting : promotionSettingsToInsert) { + ftbCultivatePromotionSettingMapper.insert(setting); + } + log.info("成功插入 {} 条晋升配置记录", promotionSettingsToInsert.size()); + } + + String resultMsg = String.format( + "迁移完成!%s共处理 %d 条数据,成功迁移 %d 条,跳过 %d 条重复记录,插入 %d 条用户记录,%d 条配置记录", + StringUtils.isNotEmpty(promotionId) ? "[通道ID: " + promotionId + "] " : "", + validMembers.size(), migratedCount, skippedCount, + promotionUsersToInsert.size(), promotionSettingsToInsert.size() + ); + log.info(resultMsg); + return ActionResult.success(resultMsg); + + } catch (Exception e) { + log.error("迁移旧晋升通道成员数据时发生错误", e); + return ActionResult.fail("迁移失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/position/V2CultivatePositionWebController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/position/V2CultivatePositionWebController.java new file mode 100644 index 0000000..9ba28e4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/position/V2CultivatePositionWebController.java @@ -0,0 +1,124 @@ +package jnpf.cultivate.v2.controller.web.position; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivatePositionService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbJobLearningPaginDTO; +import jnpf.model.cultivate.v2.position.req.FtbCultivatePositionSaveReq; +import jnpf.model.cultivate.v2.position.req.V2CultivateCoursePageReq; +import jnpf.model.cultivate.v2.position.vo.V2CultivateJobLearnCourseVo; +import jnpf.model.cultivate.v2.position.vo.WebCultivatePositionView; +import jnpf.model.cultivate.v2.position.vo.WebCultivatePositionVo; +import jnpf.model.cultivate.v2.position.vo.WebPositionLearningListVo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; + +/** + * web岗位学习模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/web/position") +@Validated +public class V2CultivatePositionWebController { + + @Autowired + private V2CultivatePositionService v2CultivatePositionService; + + /** + * 岗位学习列表(分页) + * + * @param req 分页参数 + * @return 岗位学习分页列表 + */ + @GetMapping("/page-list") + public ActionResult> pageList(@Valid FtbJobLearningPaginDTO req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.webPageList(req))); + } + + /** + * 添加岗位学习 + * + * @param req 岗位学习实体 + * @return 返回id + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid FtbCultivatePositionSaveReq req) { + String id = v2CultivatePositionService.add(req); + return ActionResult.success("岗位学习添加成功", id); + } + + /** + * 编辑岗位学习 + * + * @param req 岗位学习实体 + * @return 返回id + */ + @PutMapping("/update") + public ActionResult update(@RequestBody @Valid FtbCultivatePositionSaveReq req) { + String id = v2CultivatePositionService.update(req); + return ActionResult.success("岗位学习更新成功", id); + } + + /** + * 根据id删除岗位学习 + * + * @param id 岗位学习id + * @return 删除结果 + */ + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable @NotBlank(message = "岗位学习ID不能为空") String id) { + v2CultivatePositionService.delete(id); + return ActionResult.success("岗位学习删除成功", true); + } + + /** + * 根据id查询岗位学习详情 + * + * @param id 岗位学习id + * @return 岗位学习实体 + */ + @GetMapping("/{id}") + public ActionResult get(@PathVariable @NotBlank(message = "岗位学习ID不能为空") String id) { + return ActionResult.success(v2CultivatePositionService.get(id)); + } + + /** + * 根据id查询岗位学习配置总览 + * + * @param id 岗位学习id + * @return 岗位学习配置总览 + */ + @GetMapping("/view/{id}") + public ActionResult webView(@PathVariable @NotBlank(message = "岗位学习ID不能为空") String id) { + return ActionResult.success(v2CultivatePositionService.webView(id)); + } + + /** + * 添加岗位学习课程时,岗位学习课程列表 + * + * @param req 分页请求参数 + * @return 岗位学习课程列表 + */ + @GetMapping("/position-course-list") + public ActionResult> positionCourseList(@Valid V2CultivateCoursePageReq req) { + return ActionResult.success(CultivatePage.coverPageList(v2CultivatePositionService.positionCourseList(req))); + } + + /** + * 检查岗位学习是否存在 + * + * @param postId 岗位id + * @return true-存在 false-不存在 + */ + @GetMapping("/check-exist/{postId}") + public ActionResult checkExist(@PathVariable @NotBlank(message = "岗位ID不能为空") String postId) { + return ActionResult.success(v2CultivatePositionService.checkExist(postId)); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionStatisticController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionStatisticController.java new file mode 100644 index 0000000..d3808fd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionStatisticController.java @@ -0,0 +1,81 @@ +package jnpf.cultivate.v2.controller.web.promotion; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivatePromotionService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionOrgStatisticReq; +import jnpf.model.cultivate.v2.promotion.req.V2PromotionPersonStatisticReq; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionOrgStatisticVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionPersonStatisticVo; +import jnpf.util.EasyExcelUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + + +/** + * web学习地图统计模块 + * + * @author xgl + */ +@RestController +@RequestMapping("/v2/web/promotion/statistic") +public class V2CultivatePromotionStatisticController { + /** + * 服务对象 + */ + @Autowired + private V2CultivatePromotionService promotionService; + + /** + * 地图维度统计 + * + * @param req 请求参数 + * @return 学习地图分页列表 + */ + @PostMapping("/map-statistics") + public ActionResult> mapStatistics(@RequestBody V2PromotionOrgStatisticReq req) { + return ActionResult.success(CultivatePage.coverPageList(promotionService.organizationStatistics(req))); + } + + /** + * 地图维度统计 导出 + * + * @param req 请求参数 + */ + @PostMapping("/map-statistics/export") + public void mapStatisticsExport(@RequestBody V2PromotionOrgStatisticReq req, HttpServletResponse response) throws IOException { + Page listVO = promotionService.organizationStatistics(req); + EasyExcelUtil.simpleWrite(listVO.getRecords(), "组织维度统计", V2CultivatePromotionOrgStatisticVo.class, response); + } + + /** + * 个人维度统计 + * + * @param req 请求参数 + * @return 学习地图分页列表 + */ + @PostMapping("/person-statistics") + public ActionResult> personStatistics(@RequestBody V2PromotionPersonStatisticReq req) { + return ActionResult.success(promotionService.personStatistics(req)); + } + + /** + * 个人维度统计导出信息 + * + * @param req 请求参数 + * @param response 响应 + */ + @PostMapping("/person-statistics/export") + public void personStatisticsExport(@RequestBody V2PromotionPersonStatisticReq req, HttpServletResponse response) throws IOException { + PageListVO listVO = promotionService.personStatistics(req); + EasyExcelUtil.simpleWrite(listVO.getList(), "个人维度统计", V2CultivatePromotionPersonStatisticVo.class, response); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionWebController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionWebController.java new file mode 100644 index 0000000..43a544a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/promotion/V2CultivatePromotionWebController.java @@ -0,0 +1,182 @@ +package jnpf.cultivate.v2.controller.web.promotion; + +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivatePromotionService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.promotion.req.FtbCultivatePromotionReq; +import jnpf.model.cultivate.v2.promotion.req.V2CultivatePositionExcludeReq; +import jnpf.model.cultivate.v2.promotion.req.V2CultivatePromotionCreateReq; +import jnpf.model.cultivate.v2.promotion.req.V2WebCultivateStudyMemberListReq; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePositionVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionVo; +import jnpf.model.cultivate.v2.promotion.vo.V2WebCultivateStudyMemberListVo; +import jnpf.model.cultivate.v2.promotion.vo.WebCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.UserPromotionDetailVo; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + + +/** + * web学习地图模块 + * + * @author xgl + */ +@Slf4j +@RestController +@RequestMapping("/v2/web/promotion") +public class V2CultivatePromotionWebController { + /** + * 服务对象 + */ + @Autowired + private V2CultivatePromotionService promotionService; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * web获取学习地图列表 + * + * @param dto 查询条件参数 + * @param page 分页参数 + * @return 返回分页后的学习地图列表 + */ + @GetMapping("/list") + public ActionResult> webList(FtbCultivatePromotionReq dto, CultivatePage page) { + return ActionResult.success(promotionService.webList(page, dto)); + } + + /** + * 新增学习地图 + * + * @param req 学习地图创建请求参数,包含学习地图的基本信息 + * @return 返回操作结果,包含新增的学习地图ID + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody V2CultivatePromotionCreateReq req) { + String id = promotionService.add(req); + if (StringUtils.isNotEmpty(id)) { + promotionService.addMemberToPromotion(id, req); + } + return ActionResult.success("新增成功", id); + } + + /** + * 修改学习地图 + * + * @param req 学习地图更新请求参数,包含学习地图的更新信息 + * @return 返回操作结果,包含更新的学习地图ID + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody V2CultivatePromotionCreateReq req) { + String tenantId = UserProvider.getUser().getTenantId(); + String lockKey = "PromotionCreate:" + tenantId + ":" + req.getId(); + executeWithLock(lockKey, () -> { + String id = promotionService.update(req); + if (StringUtils.isNotEmpty(id)) { + promotionService.addMemberToPromotion(id, req); + } + }); + return ActionResult.success("修改成功", req.getId()); + } + + /** + * 使用分布式锁执行操作 + * + * @param lockKey 锁的key + * @param action 要执行的操作 + */ + private void executeWithLock(String lockKey, Runnable action) { + String lockValue = UUID.randomUUID().toString(); + Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 3, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(locked)) { + try { + action.run(); + } catch (Exception e) { + log.error("执行操作失败,lockKey: {}", lockKey, e); + throw e; + } finally { + // 安全的释放锁:只有当锁的值与当前值匹配时才删除 + Object currentValue = redisTemplate.opsForValue().get(lockKey); + if (lockValue.equals(currentValue)) { + redisTemplate.delete(lockKey); + } + } + } else { + log.warn("重复提交被拦截,lockKey: {}", lockKey); + throw new RuntimeException("请不要重复提交"); + } + } + + /** + * 获取学习地图详情 + * + * @param id 学习地图的唯一标识符 + * @return 返回指定学习地图的详细信息 + */ + @GetMapping("/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success("成功", promotionService.get(id)); + } + + /** + * 删除学习地图 + * + * @param id 学习地图的唯一标识符 + * @return 返回操作结果,表示是否删除成功 + */ + @Operation(summary = "删除学习地图") + @DeleteMapping("/{id}") + public ActionResult delete(@PathVariable("id") String id) { + promotionService.delete(id); + return ActionResult.success(); + } + + /** + * 根据地图主键ID获取地图成员信息 + * + * @param req 查询条件参数 + * @param page 分页参数 + * @return 返回指定学习地图的成员列表 + */ + @GetMapping("/query-promotion-member") + public ActionResult queryPromotionUserList(V2WebCultivateStudyMemberListReq req, CultivatePage page) { + return ActionResult.success("成功", promotionService.queryPromotionUserList(req, page)); + } + + /** + * 查询在岗位学习中完成的配置的岗位&职级 + * + * @param req 查询条件参数 + * @return 返回符合条件的岗位&职级列表 + */ + @GetMapping("/query-cultivate-position") + public ActionResult> queryCultivatePosition(V2CultivatePositionExcludeReq req) { + return ActionResult.success("成功", promotionService.queryCultivatePosition(req)); + } + + /** + * 根据用户ID和学习地图ID查询指定学习地图及分阶段岗位信息 + * + * @param userId 用户ID + * @param promotionId 学习地图ID + * @return 用户学习地图详情,包含该学习地图的各阶段岗位信息 + */ + @GetMapping("/user-promotion-detail") + public ActionResult getUserPromotionDetail(@RequestParam("userId") String userId, + @RequestParam("promotionId") String promotionId) { + return ActionResult.success("成功", promotionService.getUserPromotionDetail(userId, promotionId)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/question/V2CultivateQuestionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/question/V2CultivateQuestionController.java new file mode 100644 index 0000000..afcf256 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/question/V2CultivateQuestionController.java @@ -0,0 +1,250 @@ +package jnpf.cultivate.v2.controller.web.question; + +import cn.afterturn.easypoi.excel.ExcelImportUtil; +import cn.afterturn.easypoi.excel.entity.ImportParams; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.alibaba.fastjson.JSON; +import jnpf.base.ActionResult; +import jnpf.base.vo.DownloadVO; +import jnpf.cultivate.utils.QuestionExcelExportUtil; +import jnpf.cultivate.v2.service.V2CultivateQuestionService; +import jnpf.cultivate.v2.util.V2QuestionExcelExportUtil; +import jnpf.model.cultivate.req.paper.PreQuestionImportReq; +import jnpf.model.cultivate.req.questionbank.QuestionOptionReq; +import jnpf.model.cultivate.resp.ExcelUserQuestionVo; +import jnpf.model.cultivate.resp.PreQuestionImportVO; +import jnpf.model.cultivate.resp.QuestionVo; +import jnpf.model.cultivate.v2.question.req.*; +import jnpf.model.enums.CourseEnums; +import jnpf.util.RedisUtil; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * web题目模块 + * + * @author xgl + */ +@Slf4j +@RestController +@RequestMapping("/v2/web/question") +public class V2CultivateQuestionController { + + @Autowired + private V2CultivateQuestionService questionService; + + + @Autowired + private RedisUtil redisUtil; + + /** + * 批量添加问题的接口方法。 + * + * @param req 包含批量添加问题请求数据的对象,必须通过@Valid注解进行校验 + * @return 返回一个ActionResult对象,其中包含操作结果(true表示成功) + */ + @PostMapping("/batch-add") + public ActionResult batchAddQuestion(@RequestBody @Valid BatchAddQuestionReq req) throws Exception { + // 调用questionService的batchAddQuestion方法执行批量添加逻辑 + questionService.batchAddQuestion(req); + // 返回操作成功的ActionResult对象 + return ActionResult.success(true); + } + + /** + * 编辑题目 + * + * @param editQuestion + * @return + */ + @PostMapping("/edit") + public ActionResult editQuestion(@RequestBody @Valid V2EditQuestionReq editQuestion) throws Exception { + V2QuestionExcelExportUtil.checkEditQuestionParam(editQuestion); + questionService.updateData(editQuestion); + return ActionResult.success(true); + } + + + /** + * 查询题目详情,编辑时使用 + * + * @param id 题目ID + * @return + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success(questionService.getInfo(id)); + } + + + /** + * 预检测题目导入接口 + * + * @param preQuestionImportReq + * @param questionBankId 题库ID + * @return + */ + @PostMapping("/preImportData/{questionBankId}") + public ActionResult preImportData(@RequestBody @Validated PreQuestionImportReq preQuestionImportReq, @PathVariable(name = "questionBankId") String questionBankId) { + String rediskey = QuestionExcelExportUtil.qeneralQuestionImportKey(); + redisUtil.remove(rediskey); + ImportParams importParams = new ImportParams(); + importParams.setStartSheetIndex(1); // 读取第二个Sheet,因为索引是从0开始 + List allQuestionList = new ArrayList<>(); + try { + InputStream inputStream = EasyExcelUtils.checkExcelFile(preQuestionImportReq.getFileUrl()); + List> list = ExcelImportUtil.importExcel(inputStream, Map.class, importParams); + log.info("题库导入数据:{}", JSON.toJSONString(list)); + inputStream.close(); + + int line = 2; + for (Map o : list) { + V2ExcelImportQuestionResultReq addQuestionReq = new V2ExcelImportQuestionResultReq(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.getByDesc(String.valueOf(o.get("题型"))); + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.getByDesc(String.valueOf(o.get("难度"))); + addQuestionReq.setContent(ObjectUtils.isNotEmpty(o.get("题目")) ? String.valueOf(o.get("题目")) : ""); + addQuestionReq.setAnalysis(ObjectUtils.isNotEmpty(o.get("解析")) ? String.valueOf(o.get("解析")) : ""); + if (null == difficulty || null == type || StringUtils.isEmpty(addQuestionReq.getContent())) { + addQuestionReq.setMsg("题目内容为空"); + addQuestionReq.setSuccess(1); + continue; + } + addQuestionReq.setDifficulty(difficulty.getCode()); + addQuestionReq.setType(type.getCode()); + List optionFormList = new ArrayList<>(); + + Set keySet = o.keySet(); + long sortCode = 0; + String answer = ""; + for (String key : keySet) { + QuestionOptionReq optionForm = new QuestionOptionReq(); + if (key.contains("选项")) { + answer = String.valueOf(o.get("正确答案")); + String option = key.substring(2); + if (Objects.isNull(o.get(key))) { + continue; + } + optionForm.setContent(String.valueOf(o.get(key))); + if (!StringUtils.isEmpty(answer)) { + optionForm.setIsRightOption(answer.contains(option) ? 1 : 0); + } + optionForm.setSortCode(sortCode++); + optionFormList.add(optionForm); + } + if (CourseEnums.QuestionType.FILL.getCode().equals(addQuestionReq.getType())) { + optionForm.setRightAnswer(optionForm.getContent()); + } + } + addQuestionReq.setOptionList(optionFormList); + if (addQuestionReq.getType().equals(CourseEnums.QuestionType.INPUT.getCode())) { + addQuestionReq.setAnswer(answer); + } + allQuestionList.add(addQuestionReq); + line++; + } + + } catch (Exception e) { + e.printStackTrace(); + log.error("题目与导入失败"); + return ActionResult.fail("文件解析异常,导入失败!"); + } + // + if (CollUtil.isEmpty(allQuestionList)) { + throw new RuntimeException("导入题列表不能为空"); + } + V2QuestionExcelExportUtil.dealFillQuestion(allQuestionList); + V2PreQuestionImportVO vo = new V2PreQuestionImportVO(); + vo.setUniqueId(rediskey); + + List normal = new ArrayList<>(); + List error = new ArrayList<>(); + for (V2ExcelImportQuestionResultReq item : allQuestionList) { + if (item.getSuccess() == 1) {//fail + error.add(item); + } else {//success + normal.add(item); + } + } + vo.setNormal(normal); + vo.setError(error); + vo.setNormalNumber(normal.size()); + vo.setErrorNumber(error.size()); + redisUtil.insert(rediskey, vo, 60 * 60); + return ActionResult.success("成功", vo); + + } + + + /** + * 真正导入题目到题库 + * + * @param questionBankId 题库ID + * @param key 预检测导入时返回的唯一标识(必传),用于导出异常数据 + * @return + */ + @PostMapping("/realImportData/{questionBankId}/{key}") + public ActionResult realImportData(@PathVariable(name = "questionBankId") String questionBankId, @PathVariable(name = "key") String key) { + if (!redisUtil.exists(key)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + V2PreQuestionImportVO data = JSON.parseObject(redisUtil.getString(key).toString(), V2PreQuestionImportVO.class); + if (data == null || CollUtil.isEmpty(data.getNormal())) { + throw new RuntimeException("无正常题目可以导入"); + } + V2ImportQuestionResultVo importQuestionResultVo = questionService.realImportData(questionBankId, data.getNormal()); + if (CollUtil.isEmpty(importQuestionResultVo.getFailList())) { + String rediskey = V2QuestionExcelExportUtil.qeneralQuestionImportKey(); + redisUtil.remove(rediskey); + } + return ActionResult.success("导入成功", importQuestionResultVo); + } + + /** + * 异常数据导出 + * + * @param key 预检测导入时返回的唯一标识(必传),用于导出异常数据 + */ + @GetMapping("/exceptionDataExport/{key}") + public void exceptionDataExport(@PathVariable(name = "key") String key, HttpServletResponse response) throws IOException { + if (!redisUtil.exists(key)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + PreQuestionImportVO data = JSON.parseObject(redisUtil.getString(key).toString(), PreQuestionImportVO.class); + String fileName = "异常题目" + DateUtil.format(new Date(), "yyyyMMdd"); + if (data.getErrorNumber() <= 0) { + throw new RuntimeException("无异常数据"); + } + List list = QuestionExcelExportUtil.convertErrorQuestionList(data.getError()); + QuestionExcelExportUtil.questionExportExcel(response, fileName, list, ExcelUserQuestionVo.class); + } + + + /** + * 模版下载 + * + * @return 模版下载信息 + */ + @GetMapping("/download/template") + public ActionResult templateDownload() { + DownloadVO vo = DownloadVO.builder().build(); + try { + vo.setName("题目导入模板.xlsx"); + vo.setUrl("https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/v2/face/xlsx/%E9%A2%98%E7%9B%AE%E6%A8%A1%E6%9D%BFV2.xlsx"); + } catch (Exception e) { + log.error("下载模板错误:" + e.getMessage()); + } + return ActionResult.success(vo); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/task/V2CultivateTaskWebController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/task/V2CultivateTaskWebController.java new file mode 100644 index 0000000..07919e2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/task/V2CultivateTaskWebController.java @@ -0,0 +1,289 @@ +package jnpf.cultivate.v2.controller.web.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.v2.service.V2CultivateTaskCountService; +import jnpf.cultivate.v2.service.V2CultivateTaskService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnAllocationDTO; +import jnpf.model.cultivate.v2.position.req.V2CultivateCoursePageReq; +import jnpf.model.cultivate.v2.position.vo.V2CultivateJobLearnCourseVo; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskCountReq; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskSaveReq; +import jnpf.model.cultivate.v2.task.vo.V2CultivateLearnTaskPhaseVo; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskCountExportVo; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskCountVo; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskDetailsVo; +import jnpf.util.FtbUtil; +import jnpf.util.excel.EasyExcelUtils; +import jnpf.valid.ValidInsert; +import jnpf.valid.ValidUpdate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.groups.Default; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * web培训任务模块 + * + * @author xgl + */ + +@RestController +@Slf4j +@RequestMapping("/v2/web/task") +public class V2CultivateTaskWebController { + + @Autowired + private V2CultivateTaskService cultivateTaskService; + + @Autowired + private V2CultivateTaskCountService v2CultivateTaskCountService; + + /** + * 添加学习任务 + * + * @param req 学习任务保存请求对象,包含任务的基本信息和配置 + * @return ActionResult 操作结果,包含成功标识和任务ID + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Validated(value = {Default.class, ValidInsert.class}) V2CultivateTaskSaveReq req) { + return ActionResult.success("成功", cultivateTaskService.add(req)); + } + + /** + * 编辑学习任务 + * + * @param req 学习任务保存请求对象,包含需要更新的任务信息 + * @return ActionResult 操作结果,包含成功标识和任务ID + */ + @PostMapping("/update") + public ActionResult update(@RequestBody @Validated(value = {Default.class, ValidUpdate.class}) V2CultivateTaskSaveReq req) { + return ActionResult.success("成功", cultivateTaskService.update(req)); + } + + + /** + * 分配任务给用户 + * + * @param req 任务分配数据传输对象,包含任务ID和用户分配信息 + * @return ActionResult 操作结果,仅表示操作是否成功 + */ + @PostMapping("/assign-user") + public ActionResult assignUser(@RequestBody @Validated FtbCultivateLearnAllocationDTO req) { + cultivateTaskService.allocation(req); + return ActionResult.success("成功"); + } + + /** + * 中止指定的学习任务 + * + * @param id 任务主键ID(必传) + * @return {@link ActionResult } 操作结果,表示任务中止操作的状态 + */ + @PutMapping("/abort-task/{id}") + public ActionResult abortTask(@PathVariable("id") String id) { + cultivateTaskService.abort(id); + return ActionResult.success(); + } + + /** + * 删除指定的学习任务 + * + * @param id 任务主键ID(必传) + * @return {@link ActionResult } 操作结果,表示任务删除操作的状态 + */ + @DeleteMapping("/delete-task/{id}") + public ActionResult deleteTask(@PathVariable("id") String id) { + cultivateTaskService.deleteTask(id); + return ActionResult.success(); + } + + /** + * 根据任务ID获取任务详情,用于编辑页面的数据回显 + * + * @param id 任务主键ID(必传) + * @return {@link ActionResult} 包含任务详细信息的操作结果 + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success(cultivateTaskService.get(id)); + } + + + /** + * 添加岗位学习课程时,岗位学习课程列表 + * + * @param req + * @return {@link ActionResult} + */ + @GetMapping("/query-course-list") + public ActionResult> positionCourseList(V2CultivateCoursePageReq req) { + return ActionResult.success(CultivatePage.coverPageList(cultivateTaskService.positionCourseList(req))); + } + + /** + * 根据任务id查询我的简单任务详情和阶段列表 + * + * @param myTaskId 我的任务id + */ + @GetMapping("/query-user-task-info/{myTaskId}") + public ActionResult> queryUserTaskInfo(@PathVariable("myTaskId") String myTaskId) { + return ActionResult.success("", cultivateTaskService.queryUserTaskInfo(myTaskId)); + } + + /** + * V2查询任务统计列表 + * + * @param req 查询请求参数 + * @return 分页统计结果 + */ + @GetMapping("/query-count-list") + public ActionResult> queryCountList(@Validated V2CultivateTaskCountReq req) { + PageInfo pageVo = v2CultivateTaskCountService.queryTaskCountList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 导出培训任务统计数据 + * + * @param req 查询请求参数 + * @param response HTTP响应 + * @throws IOException IO异常 + */ + @GetMapping("/export-count-list") + public void exportCountList(@Validated V2CultivateTaskCountReq req, HttpServletResponse response) throws IOException { + // 设置不分页,查询所有数据 + req.setCurrentPage(1); + req.setPageSize(-1); + + // 直接调用统计服务获取数据 + PageInfo pageVo = v2CultivateTaskCountService.queryTaskCountList(req); + List list = pageVo.getList(); + + // 转换为导出VO + List exportList = convertToExportVo(list); + + // 使用EasyExcel导出 + String fileName = "培训任务统计-" + DateUtil.format(new Date(), "yyyyMMddHHmmss"); + EasyExcelUtils.exportExcel(response, fileName, exportList, V2CultivateTaskCountExportVo.class); + } + + /** + * 将统计VO转换为导出VO + * + * @param list 统计VO列表 + * @return 导出VO列表 + */ + private List convertToExportVo(List list) { + List exportList = new ArrayList<>(); + if (CollUtil.isEmpty(list)) { + return exportList; + } + + for (V2CultivateTaskCountVo vo : list) { + V2CultivateTaskCountExportVo exportVo = new V2CultivateTaskCountExportVo(); + + // 基本信息 + exportVo.setTaskName(vo.getTaskName()); + exportVo.setTaskTypeName(convertTaskTypeName(vo.getTaskType())); + exportVo.setStatusName(convertStatusName(vo.getStatus())); + + // 时间信息 + exportVo.setTimeLimitStartTime(vo.getTimeLimitStartTime() != null ? + DateUtil.format(vo.getTimeLimitStartTime(), "yyyy-MM-dd HH:mm:ss") : "-"); + exportVo.setTimeLimitEndTime(vo.getTimeLimitEndTime() != null ? + DateUtil.format(vo.getTimeLimitEndTime(), "yyyy-MM-dd HH:mm:ss") : "-"); + exportVo.setCreatorTime(vo.getCreatorTime() != null ? + DateUtil.format(vo.getCreatorTime(), "yyyy-MM-dd HH:mm:ss") : "-"); + + // 课程数量 + exportVo.setCourseNum(vo.getCourseNum() != null ? vo.getCourseNum() : 0); + + // 考试、鉴定、证书名称(列表转字符串) + exportVo.setExamNames(collToString(vo.getExamNames())); + exportVo.setIdentificationNames(collToString(vo.getIdentificationNames())); + exportVo.setCertificateNames(collToString(vo.getCertificateNames())); + + // 完成率相关 + exportVo.setCompleteCourseRate(vo.getCompleteCourseRate() != null ? vo.getCompleteCourseRate() : "0%"); + exportVo.setExamParticipantCount(vo.getExamParticipantCount() != null ? vo.getExamParticipantCount() : 0); + exportVo.setExamPassRate(vo.getExamPassRate() != null ? vo.getExamPassRate() : "0%"); + exportVo.setIdentifyParticipantCount(vo.getIdentifyParticipantCount() != null ? vo.getIdentifyParticipantCount() : 0); + exportVo.setIdentifyPassRate(vo.getIdentifyPassRate() != null ? vo.getIdentifyPassRate() : "0%"); + + // 任务完成情况 + exportVo.setAssignedUserCount(vo.getAssignedUserCount() != null ? vo.getAssignedUserCount() : 0); + exportVo.setCompletedUserCount(vo.getCompletedUserCount() != null ? vo.getCompletedUserCount() : 0); + exportVo.setTaskCompleteRate(vo.getTaskCompleteRate() != null ? vo.getTaskCompleteRate() : "0%"); + + // 证书 + exportVo.setIssuedCertificateCount(vo.getIssuedCertificateCount() != null ? vo.getIssuedCertificateCount() : 0); + + exportList.add(exportVo); + } + + return exportList; + } + + /** + * 转换任务类型名称 + */ + private String convertTaskTypeName(Integer taskType) { + if (taskType == null) { + return "-"; + } + switch (taskType) { + case 0: + return "长期"; + case 1: + return "限时"; + default: + return "-"; + } + } + + /** + * 转换任务状态名称 + */ + private String convertStatusName(Integer status) { + if (status == null) { + return "-"; + } + switch (status) { + case 0: + return "未发布"; + case 1: + return "未开始"; + case 2: + return "进行中"; + case 3: + return "已完成"; + case 4: + return "终止"; + default: + return "-"; + } + } + + /** + * 集合转字符串(用顿号分隔) + */ + private String collToString(List list) { + if (CollUtil.isEmpty(list)) { + return "-"; + } + return String.join("、", list); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingController.java new file mode 100644 index 0000000..a040324 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingController.java @@ -0,0 +1,30 @@ +package jnpf.cultivate.v2.controller.web.teaching; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.dto.teaching.TeachingBaseFilter; +import jnpf.model.cultivate.vo.teaching.TeachingDataListVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * web带教管理 + */ +@Slf4j +@RestController +@RequestMapping("/v2/teachingRecord") +public class V2TeachingController { + + /** + * 汇总数据-带教-分页列表 + * @param req + * @return ActionResult> + */ + @PostMapping("/summaryTeach") + public ActionResult> summaryTeach(@RequestBody TeachingBaseFilter req) { + return ActionResult.success(); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingRecordController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingRecordController.java new file mode 100644 index 0000000..bf1cffa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingRecordController.java @@ -0,0 +1,37 @@ +package jnpf.cultivate.v2.controller.web.teaching; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.TeachingRecordPracticeService; +import jnpf.model.cultivate.dto.teaching.TeachingBaseFilter; +import jnpf.model.cultivate.vo.teaching.SummaryPageListVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +/** + * web练习管理 + */ +@Slf4j +@RestController +@RequestMapping("/v2/teachingRecord") +public class V2TeachingRecordController { + @Autowired + private TeachingRecordPracticeService recordPracticeService; + + + /** + * 汇总数据-练习-分页列表 + * @param req + * @return ActionResult> + */ + @PostMapping("/v2/summaryPageList") + public ActionResult> summaryPageList(@Valid @RequestBody TeachingBaseFilter req) { + return ActionResult.success(); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingSkillController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingSkillController.java new file mode 100644 index 0000000..46b4fed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/teaching/V2TeachingSkillController.java @@ -0,0 +1,93 @@ +package jnpf.cultivate.v2.controller.web.teaching; + +import jnpf.base.ActionResult; +import jnpf.cultivate.v2.service.V2TeachingSkillService; +import jnpf.model.cultivate.v2.exam.req.SkillDto; +import jnpf.model.cultivate.v2.exam.vo.SkillUploadInfoVo; +import jnpf.model.cultivate.v2.exam.vo.SkillVo; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 技能点配置管理 + */ +@RestController +@RequestMapping("/v2/teaching-skill") +public class V2TeachingSkillController { + + @Resource + private V2TeachingSkillService v2TeachingSkillService; + + /** + * 技能点 - 模板下载 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/template-download") + public ActionResult templateDownload() throws Exception { + + String base64 = v2TeachingSkillService.templateDownload(); + return ActionResult.success(base64); + } + + /** + * 技能点 - 导入 + * @param file 文件 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/import") + public ActionResult teachingSkillImport(@RequestParam MultipartFile file) throws Exception { + + if (file == null || file.isEmpty()) { + return ActionResult.fail("请上传文件"); + } + // 校验文件后缀 + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || + !(originalFilename.endsWith(".xlsx") || originalFilename.endsWith(".xls"))) { + return ActionResult.fail("文件格式错误,仅支持 .xlsx 或 .xls"); + } + // 校验 Content-Type(防止随便改后缀) + String contentType = file.getContentType(); + if (!isExcelContentType(contentType)) { + return ActionResult.fail("文件类型非法,请上传 Excel 文件"); + } + SkillUploadInfoVo uploadInfo = v2TeachingSkillService.teachingSkillImport(file); + return ActionResult.success(uploadInfo); + } + + private boolean isExcelContentType(String contentType) { + if (contentType == null) { + return false; + } + return contentType.equals("application/vnd.ms-excel") + || contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + + /** + * 查询分类列表[下拉] + * @param searchSource 类型(0: 自定义, 1: 系统默认, null: 全部) + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/down-list/category") + public ActionResult> getDownCategoryList(@RequestParam(value = "searchSource", required = false) Integer searchSource) { + + List list = v2TeachingSkillService.getDownCategoryList(searchSource); + return ActionResult.success(list); + } + + /** + * 根据分类查询技能点[下拉] + * @param skillDto 技能点dto + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/down-list") + public ActionResult> getDownSkillList(SkillDto skillDto) { + + List list = v2TeachingSkillService.getDownSkillList(skillDto); + return ActionResult.success(list); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/timing/V2TimingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/timing/V2TimingController.java new file mode 100644 index 0000000..506d325 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/controller/web/timing/V2TimingController.java @@ -0,0 +1,121 @@ +package jnpf.cultivate.v2.controller.web.timing; + +import jnpf.base.ActionResult; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.cultivate.v2.service.V2CultivateTaskService; +import jnpf.exam.V2CultivateTimingApi; +import jnpf.util.NoDataSourceBind; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; + + +/** + * 培训定时任务 + */ + +@Slf4j +@RestController +@RequestMapping("/v2/cultivate/timing") +public class V2TimingController implements V2CultivateTimingApi { + + + @Autowired + private V2CultivateTaskService learnTaskListService; + + @Autowired + private CultivateLearnUtils cultivateLearnUtils; + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + @Autowired + private UserApiV2Util userApiV2Util; + + + @Autowired + private V2CultivateExamService v2CultivateExamService; + + + + + /** + * 每分钟执行 + * + * @param tenantId 租户ID + * @return 响应 + */ + @Override + @NoDataSourceBind + @GetMapping("/per-minute") + public ActionResult perMinute(@RequestParam("tenantId") String tenantId) { + + CompletableFuture.runAsync(() -> { + userApiV2Util.checkOutTenant(tenantId); + //修改学习任务的状态 + try { + learnTaskListService.checkAndUpdateTaskStatus(tenantId); + } catch (Exception e) { + log.error("任务学习定时任务执行异常: {}", e.getMessage()); + } + + try { + //1检测和修改用户的过期考试 + v2CultivateExamService.checkAndUpdateExpireUserExamStatus(); + //2检测和修改常规考试过期 + v2CultivateExamService.checkAndUpdateExamStatus(); + } catch (Exception e) { + log.error("考试定时任务执行异常: {}", e.getMessage()); + } + }, cultivateThreadPool); + + return ActionResult.success(""); + } + + /** + * 每个小时执行 + * + * @param tenantId 租户ID + * @return 响应 + */ + @Override + @NoDataSourceBind + @GetMapping("/per-hour") + public ActionResult perHour(@RequestParam("tenantId") String tenantId) { + log.error("start perHour.."); + CompletableFuture.runAsync(() -> { + userApiV2Util.checkOutTenant(tenantId); + //检查任务开始并发消息提醒 + try { + cultivateLearnUtils.timingTaskLearningAlert(tenantId); + } catch (Exception e) { + log.error("定时任务执行异常: {}", e.getMessage()); + } + }, cultivateThreadPool); + + + return ActionResult.success(""); + } + + /** + * 每半个小时执行 + * + * @param tenantId 租户ID + * @return 响应 + */ + @Override + @NoDataSourceBind + @GetMapping("/per-half-hour") + public ActionResult perHalfHour(@RequestParam("tenantId") String tenantId) { + return ActionResult.success(""); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/CultivateConsumerMQListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/CultivateConsumerMQListener.java new file mode 100644 index 0000000..7cacdec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/CultivateConsumerMQListener.java @@ -0,0 +1,154 @@ +package jnpf.cultivate.v2.mq; + +import jnpf.constants.MessageTopicConstants; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivatePostStudyService; +import jnpf.cultivate.v2.service.V2CultivateTaskStudyService; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.CULTIVATE_TOPIC, + consumerGroup = MessageTopicConstants.CULTIVATE_CONSUMER_GROUP_SELF, + replyTimeout = 600000, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class CultivateConsumerMQListener implements RocketMQListener { + + @Autowired + private V2CultivatePostStudyService v2CultivatePostStudyService; + + @Autowired + private V2CultivateTaskStudyService v2CultivateTaskStudyService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public void onMessage(CultivateMqDTO message) { + if (message == null) { + log.warn("接收到的培训信息为null"); + return; + } + + log.info("开始处理培训信息: type={}, businessSource={}, businessId={}, tenantId={}", + message.getType(), message.getBusinessSource(), message.getBusinessId(), message.getTenantId()); + + userApiV2Util.checkOutTenant(message.getTenantId()); + + try { + switch (message.getType()) { + case COURSE: + dealCourse(message); + break; + case EXAM: + dealExam(message); + break; + case IDENTITY: + dealIdentity(message); + break; + case PRACTICE: + dealPractice(message); + break; + default: + log.warn("不支持的培训类型: {}", message.getType()); + } + + log.info("培训信息处理成功: type={}, businessId={}", message.getType(), message.getBusinessId()); + + } catch (Exception exception) { + log.error("处理培训信息时发生异常: type={}, businessId={}, message={}", + message.getType(), message.getBusinessId(), message, exception); + } + } + + private void dealPractice(CultivateMqDTO message) { + if (message == null) { + log.warn("dealPractice方法接收到的message为null"); + return; + } + + try { + v2CultivatePostStudyService.dealPositionPracticeStudy(message); + } catch (Exception e) { + log.error("处理岗位学习信息练习时发生异常: message={}", message, e); + } + + try { + v2CultivateTaskStudyService.dealTaskPracticeStudy(message); + } catch (Exception e) { + log.error("处理任务学习信息练习时发生异常: message={}", message, e); + } + } + + private void dealIdentity(CultivateMqDTO message) { + if (message == null) { + log.warn("dealIdentity方法接收到的message为null"); + return; + } + // 根据业务来源判断处理哪个服务 + if (message.getBusinessSource() == PositionBusinessSourceEnum.POST_LEARNING) { + try { + v2CultivatePostStudyService.dealPositionIdentityStudy(message); + } catch (Exception e) { + log.error("处理岗位学习鉴定信息时发生异常: message={}", message, e); + } + } else if (message.getBusinessSource() == PositionBusinessSourceEnum.TASK_LEARNING) { + try { + v2CultivateTaskStudyService.dealTaskIdentityStudy(message); + } catch (Exception e) { + log.error("处理任务学习鉴定信息时发生异常: message={}", message, e); + } + } else { + log.warn("dealIdentity方法不支持的培训来源: {}", message.getBusinessSource()); + } + } + + private void dealExam(CultivateMqDTO message) { + if (message == null) { + log.warn("dealExam方法接收到的message为null"); + return; + } + + try { + v2CultivatePostStudyService.dealPositionExamStudy(message); + } catch (Exception e) { + log.error("处理岗位考试信息时发生异常: message={}", message, e); + } + + try { + v2CultivateTaskStudyService.dealTaskExamStudy(message); + } catch (Exception e) { + log.error("处理任务考试信息时发生异常: message={}", message, e); + } + } + + private void dealCourse(CultivateMqDTO message) { + if (message == null) { + log.warn("dealCourse方法接收到的message为null"); + return; + } + + switch (message.getBusinessSource()) { + case POST_LEARNING: + v2CultivatePostStudyService.dealPositionCourseStudy(message); + break; + case TASK_LEARNING: + v2CultivateTaskStudyService.dealTaskCourseStudy(message); + break; + case GENERAL_COURSE: + case OFFLINE_CLASS: + v2CultivatePostStudyService.dealCommonCourseStudy(message); + break; + default: + log.warn("dealCourse方法不支持的培训来源: {}", message.getBusinessSource()); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/PermissionConsumerMEListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/PermissionConsumerMEListener.java new file mode 100644 index 0000000..0ca7a03 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/mq/PermissionConsumerMEListener.java @@ -0,0 +1,366 @@ +package jnpf.cultivate.v2.mq; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.constants.MessageTopicConstants; +import jnpf.cultivate.mapper.CultivateIdentifyApplyMapper; +import jnpf.cultivate.mapper.FtbCultivateCertificateUserMapper; +import jnpf.cultivate.mapper.FtbCultivateChapterTestResultMapper; +import jnpf.cultivate.mapper.FtbCultivateExamUserMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskAssignmentMapper; +import jnpf.cultivate.mapper.FtbCultivateOfflineTrainMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceChapterLearningMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionMemberNewMapper; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.FtbCultivatePromotionUserService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.message.enums.PermissionMessageTagEnum; +import jnpf.message.enums.PermissionUserInfoEnum; +import jnpf.message.model.permission.PermissionRelationOrganizeUserListDTO; +import jnpf.message.model.permission.PermissionRelationUserPositionListDTO; +import jnpf.message.model.permission.UserInfoSaveMessageDTO; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionUser; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsReceive; +import jnpf.model.notice.domain.FtbNoticeUserGroupMembers; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeUserGroupMembersService; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.apache.rocketmq.spring.core.RocketMQListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * 培训请求信息 + */ +@Component +@RocketMQMessageListener( + topic = MessageTopicConstants.PERMISSION_TOPIC, + consumerGroup = MessageTopicConstants.CULTIVATE_CONSUMER_GROUP, + selectorExpression = "TAG_USER_INFO_SAVE || TAG_ORGANIZE_RELATION_USER || TAG_USER_RELATION_POSITION", + replyTimeout = 600000, + nameServer = "${spring.cloud.stream.rocketmq.binder.name-server}" +) +@Slf4j +public class PermissionConsumerMEListener implements RocketMQListener { + @Autowired + private FtbNoticeUserGroupMembersService ftbNoticeUserGroupMembersService; + + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + @Autowired + private FtbCultivatePromotionUserService ftbCultivatePromotionUserService; + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + @Autowired + private CultivateIdentifyApplyMapper cultivateIdentifyApplyMapper; + + @Autowired + private FtbCultivatePromotionMemberNewMapper ftbCultivatePromotionMemberNewMapper; + + @Autowired + private FtbCultivateCertificateUserMapper ftbCultivateCertificateUserMapper; + + @Autowired + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + private FtbCultivatePositionCourceChapterLearningMapper ftbCultivatePositionCourceChapterLearningMapper; + + @Autowired + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + @Autowired + private FtbCultivateChapterTestResultMapper ftbCultivateChapterTestResultMapper; + + @Autowired + private FtbCultivateOfflineTrainMapper ftbCultivateOfflineTrainMapper; + + @Autowired + @Lazy + private CultivateLearnUtils cultivateLearnUtils; + + @Autowired + @Lazy + private UserApiV2Util userApiV2Util; + + @Override + public void onMessage(MessageExt messageExt) { + String tag = messageExt.getTags(); + String message = new String(messageExt.getBody(), StandardCharsets.UTF_8); + if (StringUtil.isEmpty(message)) { + return; + } + log.error("接受组织下人员(批量入职/批量删除/预入职/离职)变更信息{}", message); + if (PermissionMessageTagEnum.USER_INFO_SAVE.getCode().equals(tag)) { + try { + List messageList = + JSON.parseArray(message, UserInfoSaveMessageDTO.class); + + if (CollUtil.isEmpty(messageList)) { + return; + } + + String tenantId = messageList.get(0).getUserInfo().getTenantId(); + userApiV2Util.checkOutTenant(tenantId); + + List deleteUserIds = new ArrayList<>(); + List addUserIds = new ArrayList<>(); + List updateUserIds = new ArrayList<>(); + + for (UserInfoSaveMessageDTO userInfoSaveMessageDTO : messageList) { + if (userInfoSaveMessageDTO.getOperateType() == PermissionUserInfoEnum.ADD) { + addUserIds.add(userInfoSaveMessageDTO.getUserInfo().getId()); + } else if (userInfoSaveMessageDTO.getOperateType() == PermissionUserInfoEnum.DELETE) { + deleteUserIds.add(userInfoSaveMessageDTO.getUserInfo().getId()); + } else if (userInfoSaveMessageDTO.getOperateType() == PermissionUserInfoEnum.UPDATE) { + updateUserIds.add(userInfoSaveMessageDTO.getUserInfo().getId()); + } + } + + dealDeleteUser(deleteUserIds, tenantId); + dealAddUser(addUserIds, tenantId); + dealUpdateUser(updateUserIds, tenantId); + + } catch (Exception e) { + log.error("消息解析异常: message={}", message, e); + } + + } else if (PermissionMessageTagEnum.ORGANIZE_RELATION_USER.getCode().equals(tag)) { + try { + List messageList = + JSON.parseArray(message, PermissionRelationOrganizeUserListDTO.class); + + if (CollUtil.isEmpty(messageList)) { + return; + } + PermissionRelationOrganizeUserListDTO dto = messageList.get(0); + String tenantId = dto.getTenantId(); + userApiV2Util.checkOutTenant(tenantId); + List updateUserIds = new ArrayList<>(); + for (PermissionRelationOrganizeUserListDTO permissionRelationOrganizeUserListDTO : messageList) { + updateUserIds.add(permissionRelationOrganizeUserListDTO.getUserId()); + } + dealOrganizeUpdateUser(updateUserIds, tenantId); + } catch (Exception e) { + log.error("组织用户关系消息解析异常: message={}", message, e); + } + + } else if (PermissionMessageTagEnum.USER_RELATION_POSITION.getCode().equals(tag)) { + try { + List messageList = + JSON.parseArray(message, PermissionRelationUserPositionListDTO.class); + + if (CollUtil.isEmpty(messageList)) { + return; + } + PermissionRelationUserPositionListDTO dto = messageList.get(0); + String tenantId = dto.getTenantId(); + userApiV2Util.checkOutTenant(tenantId); + List updateUserIds = new ArrayList<>(); + for (PermissionRelationUserPositionListDTO permissionRelationOrganizeUserListDTO : messageList) { + updateUserIds.add(permissionRelationOrganizeUserListDTO.getUserId()); + } + cultivateLearnUtils.dealPositionUpdateUser(updateUserIds, tenantId); + } catch (Exception e) { + log.error("组织用户关系消息解析异常: message={}", message, e); + } + + }else { + log.error("未知 RocketMQ tag: {}, msgId: {}, body: {}", + tag, messageExt.getMsgId(), message); + } + + } + + private void dealUpdateUser(List updateUserIds, String tenantId) { + if (CollUtil.isEmpty(updateUserIds)) { + return; + } + cultivateLearnUtils.addNewPersonToTask(updateUserIds, tenantId); + cultivateLearnUtils.organizeOrPositionChange(updateUserIds, tenantId); + cultivateLearnUtils.dealPositionUpdateUser(updateUserIds, tenantId); + } + + private void dealOrganizeUpdateUser(List updateUserIds, String tenantId) { + if (CollUtil.isEmpty(updateUserIds)) { + return; + } + cultivateLearnUtils.organizeOrPositionChange(updateUserIds, tenantId); + } + + private void dealAddUser(List addUserIds, String tenantId) { + if (CollUtil.isEmpty(addUserIds)) { + return; + } + recoverCultivateData(addUserIds, tenantId); + cultivateLearnUtils.addPromotionUser(addUserIds, tenantId); + cultivateLearnUtils.addNewPersonToTask(addUserIds, tenantId); + cultivateLearnUtils.organizeOrPositionChange(addUserIds, tenantId); + cultivateLearnUtils.dealPositionUpdateUser(addUserIds, tenantId); + } + + + /** + * 恢复数据 + * + * @param userIdList 用户集合 + * @param tenantId 租户id + */ + private void recoverCultivateData(List userIdList, String tenantId) { + + //标记恢复考试记录 + ftbCultivateExamUserMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivateExamUser::getUserId, userIdList) + .eq(FtbCultivateExamUser::getLeaveOut, 1) + .set(FtbCultivateExamUser::getLeaveOut, 0) + .set(FtbCultivateExamUser::getEnabledMark, 1)); + + //标记恢复鉴定记录 + cultivateIdentifyApplyMapper.update(null, new LambdaUpdateWrapper() + .in(CultivateIdentifyApply::getBeIdentifyUserId, userIdList) + .eq(CultivateIdentifyApply::getLeaveOut, 1) + .set(CultivateIdentifyApply::getLeaveOut, 0) + .set(CultivateIdentifyApply::getDeleteMark, 0)); + + + //标记恢复证书 + ftbCultivateCertificateUserMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCertificateUserEntity::getUserId, userIdList) + .eq(FtbCertificateUserEntity::getLeaveOut, 1) + .set(FtbCertificateUserEntity::getLeaveOut, 0) + .set(FtbCertificateUserEntity::getEnabledMark, 1)); + + //标记恢复课程学习记录 和章节学习 + ftbCultivatePositionCourceLearningMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivatePositionCourceLearning::getUserId, userIdList) + .eq(FtbCultivatePositionCourceLearning::getLeaveOut, 1) + .set(FtbCultivatePositionCourceLearning::getLeaveOut, 0) + .set(FtbCultivatePositionCourceLearning::getEnabledMark, 0)); + + ftbCultivatePositionCourceChapterLearningMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivatePositionCourceChapterLearning::getUserId, userIdList) + .eq(FtbCultivatePositionCourceChapterLearning::getLeaveOut, 1) + .set(FtbCultivatePositionCourceChapterLearning::getLeaveOut, 0) + .set(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0)); + + //处理顺堂测试 + ftbCultivateChapterTestResultMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivateChapterTestResult::getUserId, userIdList) + .eq(FtbCultivateChapterTestResult::getLeaveOut, 1) + .set(FtbCultivateChapterTestResult::getLeaveOut, 0) + .set(FtbCultivateChapterTestResult::getEnableMark, 0)); + } + + + /** + * 删除人员 + * + * @param userIdList 用户集合 + * @param tenantId 租户id + */ + public void dealDeleteUser(List userIdList, String tenantId) { + if (CollUtil.isEmpty(userIdList)) { + return; + } + //删除分组中的人 + ftbNoticeUserGroupMembersService.remove(new LambdaQueryWrapper() + .in(FtbNoticeUserGroupMembers::getUserId, userIdList)); + //删除未发送的公告接收人 + ftbNoticeAnnouncementsReceiveService.remove(new LambdaQueryWrapper() + .in(FtbNoticeAnnouncementsReceive::getUserId, userIdList) + .eq(FtbNoticeAnnouncementsReceive::getReadStatus, 1)); + + //已读的调整为标记删除 + ftbNoticeAnnouncementsReceiveService.update(new LambdaUpdateWrapper() + .set(FtbNoticeAnnouncementsReceive::getEnabledMark, 0) + .in(FtbNoticeAnnouncementsReceive::getUserId, userIdList) + .eq(FtbNoticeAnnouncementsReceive::getReadStatus, 2) + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, 1)) + ; + + //标记删除考试记录 + ftbCultivateExamUserMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivateExamUser::getUserId, userIdList) + .eq(FtbCultivateExamUser::getEnabledMark, 1) + .set(FtbCultivateExamUser::getLeaveOut, 1) + .set(FtbCultivateExamUser::getEnabledMark, 0)); + + //标记删除鉴定记录 + cultivateIdentifyApplyMapper.update(null, new LambdaUpdateWrapper() + .in(CultivateIdentifyApply::getBeIdentifyUserId, userIdList) + .eq(CultivateIdentifyApply::getDeleteMark, 0) + .set(CultivateIdentifyApply::getLeaveOut, 1) + .set(CultivateIdentifyApply::getDeleteMark, 1)); + + + //标记删除学习地图 + ftbCultivatePromotionMemberNewMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivatePromotionMemberNew::getUserId, userIdList) + .set(FtbCultivatePromotionMemberNew::getDeleteMark, 1)); + + //标记删除证书 + ftbCultivateCertificateUserMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCertificateUserEntity::getUserId, userIdList) + .eq(FtbCertificateUserEntity::getEnabledMark, 1) + .set(FtbCertificateUserEntity::getLeaveOut, 1) + .set(FtbCertificateUserEntity::getEnabledMark, 0)); + + //标记删除课程学习记录 和章节学习 + ftbCultivatePositionCourceLearningMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivatePositionCourceLearning::getUserId, userIdList) + .eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0) + .set(FtbCultivatePositionCourceLearning::getLeaveOut, 1) + .set(FtbCultivatePositionCourceLearning::getEnabledMark, 1)); + + ftbCultivatePositionCourceChapterLearningMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivatePositionCourceChapterLearning::getUserId, userIdList) + .eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0) + .set(FtbCultivatePositionCourceChapterLearning::getLeaveOut, 1) + .set(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 1)); + + //标记删除任务学习记录 + ftbCultivateLearnTaskAssignmentMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivateLearnTaskAssignment::getUserId, userIdList) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .set(FtbCultivateLearnTaskAssignment::getLeaveOut, 1) + .set(FtbCultivateLearnTaskAssignment::getEnableMark, 1)); + //处理顺堂测试 + ftbCultivateChapterTestResultMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivateChapterTestResult::getUserId, userIdList) + .eq(FtbCultivateChapterTestResult::getEnableMark, 0) + .set(FtbCultivateChapterTestResult::getLeaveOut, 1) + .set(FtbCultivateChapterTestResult::getEnableMark, 1)); + + //删除线下签到用户 + ftbCultivateOfflineTrainMapper.deleteLeaveUser(userIdList); + + //删除通道的成员 + LambdaUpdateWrapper promotionUserLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + promotionUserLambdaUpdateWrapper.in(FtbCultivatePromotionUser::getUserId, userIdList); + ftbCultivatePromotionUserService.remove(promotionUserLambdaUpdateWrapper); + + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivateExamDrawRuleService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivateExamDrawRuleService.java new file mode 100644 index 0000000..78d76b9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivateExamDrawRuleService.java @@ -0,0 +1,23 @@ +package jnpf.cultivate.v2.service; + +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.exam.vo.ConnectDrawRuleVo; + +import java.util.List; + +/** + * 抽题规则服务 + * + * @author yanwenfu + * @create 2026-03-05 + */ +public interface CultivateExamDrawRuleService extends SuperService { + + /** + * 查询关联题目的规则 + * @param questionIds 题目ids + * @return java.util.List + */ + List getConnectQuestionRule(List questionIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivatePositionCourseLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivatePositionCourseLogService.java new file mode 100644 index 0000000..ebd3d8b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/CultivatePositionCourseLogService.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivatePositionCourseLogEntity; + +import java.util.List; + +/** + * 岗位学习课程记录服务接口 + */ +public interface CultivatePositionCourseLogService extends IService { + + List queryMyAllCompletePositionCourse(String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseLearningLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseLearningLogService.java new file mode 100644 index 0000000..c1c6928 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseLearningLogService.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.FtbCultivateCourseLearningLogEntity; + +/** + * 学习课程日志表 Service + * + * @author JNPF + * @since 2026-04-08 + */ +public interface FtbCultivateCourseLearningLogService extends IService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseSettingGlobalService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseSettingGlobalService.java new file mode 100644 index 0000000..c198ae4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateCourseSettingGlobalService.java @@ -0,0 +1,26 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.FtbCultivateCourseSettingGlobalDTO; +import jnpf.model.cultivate.po.FtbCultivateCourseSettingGlobal; + +/** + * 课程配置表 服务接口 + */ +public interface FtbCultivateCourseSettingGlobalService extends IService { + + /** + * 获取课程配置表信息 + * + * @return 课程配置表信息 + */ + FtbCultivateCourseSettingGlobal getInfo(); + + /** + * 修改课程配置表 + * + * @param entity 实体对象 + */ + void update(FtbCultivateCourseSettingGlobalDTO entity); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyCategoriesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyCategoriesService.java new file mode 100644 index 0000000..6945a8a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyCategoriesService.java @@ -0,0 +1,57 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.v2.identify.req.QueryIdentifyCategoryListReq; +import jnpf.model.cultivate.v2.identify.req.V2SaveItemCategoryReq; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyCategoryTreeVo; + +import javax.validation.Valid; +import java.util.List; + +/** + * 鉴定项分类服务接口 + * + * @author lingma + * @since 2026-02-24 + */ +public interface FtbCultivateIdentifyCategoriesService extends IService { + + + /** + * 获取鉴定项分类树结构 + * + * @param req 查询条件请求对象 + * @return 鉴定项分类树结构列表 + */ + List getCategoryTree(QueryIdentifyCategoryListReq req); + + /** + * 新增鉴定项分类数据 + * + * @param req 保存鉴定项分类请求对象 + * @return 保存后的鉴定项分类实体 + */ + FtbCultivateIdentifyCategories insertData(@Valid V2SaveItemCategoryReq req); + + /** + * 更新鉴定项分类数据 + * + * @param id 鉴定项分类ID + * @param req 保存鉴定项分类请求对象 + * @return 更新后的鉴定项分类实体 + */ + FtbCultivateIdentifyCategories updateData(String id, @Valid V2SaveItemCategoryReq req); + + /** + * 删除鉴定项分类数据 + * + * @param id 鉴定项分类ID + */ + void deleteData(String id); + + + List getAllNameList(); + + List getAllList(); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyItemsPoolService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyItemsPoolService.java new file mode 100644 index 0000000..c37c28a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateIdentifyItemsPoolService.java @@ -0,0 +1,70 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.common.req.V2BatchByPrimaryIdReq; +import jnpf.model.cultivate.v2.item_pool.req.IdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.req.SaveCultivateIdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.vo.FtbCultivateIdentifyItemsPoolVo; +import jnpf.model.cultivate.v2.item_pool.vo.V2IdentifyItemVo; + +import java.util.List; + +/** + * 鉴定项池服务接口 + * + * @author lingma + * @since 2026-02-24 + */ +public interface FtbCultivateIdentifyItemsPoolService extends IService { + + /** + * 根据ID获取鉴定项池信息 + * + * @param id 鉴定项池ID + * @return 鉴定项池信息VO对象 + */ + FtbCultivateIdentifyItemsPoolVo getInfo(String id); + + /** + * 新增鉴定项池数据 + * + * @param req 保存鉴定项池请求对象 + */ + void addData(SaveCultivateIdentifyItemsPoolReq req); + + /** + * 更新鉴定项池数据 + * + * @param id 鉴定项池ID + * @param req 保存鉴定项池请求对象 + */ + void updateData(String id, SaveCultivateIdentifyItemsPoolReq req); + + /** + * 删除鉴定项池数据 + * + * @param id 鉴定项池ID + */ + void deleteData(String id); + + /** + * 分页查询鉴定项池列表 + * + * @param req 查询条件请求对象 + * @return 鉴定项池分页结果 + */ + Page webPageList(IdentifyItemsPoolReq req); + + /** + * 查询所有鉴定项池数据 + * + * @return 鉴定项池实体列表 + */ + List listAll(); + + void batchDel(V2BatchByPrimaryIdReq req); + + void batchAddData(List normal); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLabelService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLabelService.java new file mode 100644 index 0000000..57dd4d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLabelService.java @@ -0,0 +1,76 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.v2.label.dto.FtbCultivateLabelReq; +import jnpf.model.cultivate.v2.label.dto.FtbCultivateUpdateLabelReq; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.model.cultivate.v2.position.req.V2CultivateCommonCourseForAppReq; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * 培训标签关联表 服务接口 + */ +public interface FtbCultivateLabelService extends IService { + + + /** + * 根据类型获取培训标签列表 + * + * @param type 类型标识,用于筛选特定类型的培训标签 + * @param source 来源:0-自定义,1-系统默认 null 全部 + * @return 返回符合条件的培训标签列表 + */ + List getList(Integer type, Integer source); + + /** + * 获取培训标签关联表信息 + * + * @param id 主键ID,用于唯一标识一条培训标签记录 + * @return 返回对应的培训标签详细信息 + */ + FtbCultivateLabelVo getInfo(String id); + + /** + * 新增培训标签关联表 + * + * @param req 包含新增培训标签所需数据的请求对象 + * @return 返回新增成功的培训标签信息 + */ + FtbCultivateLabelVo addData(FtbCultivateLabelReq req); + + /** + * 更新培训标签关联表信息 + * + * @param id 主键ID,用于定位需要更新的培训标签记录 + * @param req 包含更新培训标签所需数据的请求对象,需通过@Valid注解进行校验 + * @return 返回更新后的培训标签信息 + */ + FtbCultivateLabelVo updateData(String id, @Valid FtbCultivateUpdateLabelReq req); + + /** + * 删除培训标签关联表信息 + * + * @param id 主键ID,用于定位需要删除的培训标签记录 + */ + void deleteData(String id); + + /** + * 获取通用课程标签列表 + * + * @param req 请求参数对象,包含查询通用课程标签所需的条件 + * @return 返回通用课程标签列表,每个标签封装为FtbCultivateLabelVo对象 + */ + List commonCourseLabelList(V2CultivateCommonCourseForAppReq req); + + /** + * 根据分类名称查询map数据 + * + * @param categoryNameList 分类名称 + * @return java.util.Map + */ + Map getLabelMapByName(List categoryNameList); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPhaseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPhaseService.java new file mode 100644 index 0000000..a0c963c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPhaseService.java @@ -0,0 +1,9 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPhase; + + +public interface FtbCultivateLearnTaskPhaseService extends IService { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPracticeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPracticeService.java new file mode 100644 index 0000000..c87019a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateLearnTaskPracticeService.java @@ -0,0 +1,25 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPractice; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskPracticeVo; + +import java.util.List; + +public interface FtbCultivateLearnTaskPracticeService extends IService { + + /** + * 根据任务ID查询所有有效练习 + * @param taskId 任务ID + * @return 练习列表 + */ + List listByTaskId(String taskId); + + /** + * 根据任务ID和阶段ID查询所有有效练习 + * @param taskId 任务ID + * @param phaseId 阶段ID + * @return 练习列表 + */ + List listByTaskIdAndPhaseId(String taskId, String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionLogService.java new file mode 100644 index 0000000..42bbba6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionLogService.java @@ -0,0 +1,78 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivatePositionCourseLogEntity; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.FtbCultivatePositionLog; + +import java.util.List; + +/** + * 岗位学习完成记录表服务接口 + */ +public interface FtbCultivatePositionLogService extends IService { + /** + * 根据条件保存或更新用户职位学习记录 + * 该方法会根据传入的用户ID、职位学习ID等条件来决定是保存新记录还是更新现有记录 + * + * @param userId 用户ID,用于标识操作的用户 + * @param positionLearnId 职位学习ID,用于标识具体的职位学习记录 + * @param postId 职位ID,关联到具体的职位信息 + * @param gradeId 年级ID,表示用户所在的年级信息 + * @param courseId 课程id,表示用户所在的年级信息 + */ + void completePositionCourse(String userId, String positionLearnId, String postId, String gradeId, String courseId); + + /** + * 查询用户已完成的职位学习记录 + * + * @param userId 用户ID,用于查询该用户已完成的职位学习记录 + * @return 返回一个包含已完成职位学习记录的列表 + */ + List queryMyCompletePosition(String userId); + + List batchQueryCompletePosition(List currUserIds); + + /** + * 职位学习完成 + * + * @param userId 用户ID + * @param positionLearnId 职位学习ID + * @param postId 职位ID + * @param gradeId 年级ID + * @param tenantId 租户ID + */ + void completePosition(String userId, String positionLearnId, String postId, String gradeId, String tenantId); + + /** + * 查询用户是否完成职位学习 + * + * @param userId 用户ID + * @param positionLearnId 职位学习ID + * @param postId 职位ID + * @param gradeId 年级ID + * @param tenantId 租户ID + * @return 返回一个布尔值,表示用户是否完成职位学习 + */ + boolean selectIsCompletePosition(String userId, String positionLearnId, String postId, String gradeId, String tenantId); + + CultivatePositionCourseLogEntity queryIsTriggerExam(String positionLearnId, String userId, String courseId, String gradeId); + + void recordUserExamId(String positionLearnId, String userId, String courseId,String gradeId, FtbCultivateExamUser ftbCultivateExamUser); + + void recodePositionCourseExam(String userId, String examId, String userExamId, Integer status); + + void recordUserIdentificationId(String positionLearnId, String userId, String courseId, String gradeId,CultivateIdentifyApply cultivateIdentifyApply); + + void recodePositionCourseIdentify(String userId, String identityId, String userIdentifyId, Integer status, Integer userIdentifyStatus); + + /** + * 根据用户ID和学习地图ID查询岗位学习课程记录 + * + * @param userId 用户ID + * @param postLearnId 学习地图ID(岗位学习ID) + * @return 岗位学习课程记录列表 + */ + List queryByUserIdAndPostLearnId(String userId, String postLearnId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionSettingService.java new file mode 100644 index 0000000..c14d2e2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePositionSettingService.java @@ -0,0 +1,26 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionSetting; + + +public interface FtbCultivatePositionSettingService extends IService { + + + /** + * 根据岗位学习id查询岗位学习设置 + * + * @param positionLearnId 岗位学习id + * @return 岗位学习设置 + */ + FtbCultivatePositionSetting listByPostLearnId(String positionLearnId); + + /** + * 根据岗位学习id和职级id查询岗位学习设置 + * + * @param positionLearnId 岗位学习id + * @param gradeId 职级id + * @return 岗位学习设置 + */ + FtbCultivatePositionSetting listByPostLearnIdAndGradeId(String positionLearnId, String gradeId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionLogService.java new file mode 100644 index 0000000..525c333 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionLogService.java @@ -0,0 +1,36 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionLog; + +import java.util.List; + +/** + * 学习地图完成记录 + * + * @author xgl + * @date 2026-01-27 14:04:01 + */ +public interface FtbCultivatePromotionLogService + extends IService { + + /** + * 完成晋升等级操作 + * + * @param userId 用户ID,标识执行晋升操作的用户 + * @param promotionId 晋升记录ID,用于标识具体的晋升活动或任务 + * @param level 晋升等级,表示用户要完成的具体等级数值 + * @param tenantId 租户ID,用于多租户环境下的数据隔离 + */ + void completePromotionLevel(String userId, String promotionId, Integer level, String tenantId); + + /** + * 查询用户的已完成晋升记录列表 + * + * @param userId 用户ID,用于查询指定用户的晋升日志记录 + * @param state 地图状态,1-未完成 2-已完成 null-全部 + * @return List 返回用户的所有已完成晋升记录列表 + */ + List queryMyCompletePromotion(String userId,Integer state); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionMemberNewService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionMemberNewService.java new file mode 100644 index 0000000..b8e95d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionMemberNewService.java @@ -0,0 +1,30 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; + +import java.util.List; + +/** + * 晋升通道成员服务类 + */ +public interface FtbCultivatePromotionMemberNewService extends IService { + /** + * 查询用户当前阶段的批次统计数据 + * + * @param userId 用户ID,用于标识查询的目标用户 + * @param promotionIds 推广活动ID列表,用于指定需要查询的推广活动范围 + * @return BatchCommonCountDto对象列表,包含用户在指定推广活动中各批次的统计信息 + */ + List queryMyCurrentPhase(String userId, List promotionIds); + + /** + * 查询用户所有已选择的职位信息 + * + * @param userId 用户ID,用于标识查询的目标用户 + * @return FtbCultivatePromotionMemberNew对象列表,包含用户所有已选择的职位相关信息 + */ + List queryMyAllSelectPosition(String userId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionNewMessageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionNewMessageService.java new file mode 100644 index 0000000..07f0623 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionNewMessageService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNewMessage; + +/** + * 晋升消息服务类 + */ +public interface FtbCultivatePromotionNewMessageService extends IService { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionPostNewService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionPostNewService.java new file mode 100644 index 0000000..1a51f71 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionPostNewService.java @@ -0,0 +1,11 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; + +/** + * 晋升通道岗位服务接口 + */ +public interface FtbCultivatePromotionPostNewService extends IService { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionSettingService.java new file mode 100644 index 0000000..914efcb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionSettingService.java @@ -0,0 +1,10 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionSetting; + +/** + * 学习地图人员和岗位配置表服务类 + */ +public interface FtbCultivatePromotionSettingService extends IService { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionUserService.java new file mode 100644 index 0000000..33aebb9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivatePromotionUserService.java @@ -0,0 +1,60 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionUser; +import jnpf.model.cultivate.v2.promotion.vo.MyCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionMemberVo; + +import java.util.List; + +/** + * 学习地图人员标 service + */ +public interface FtbCultivatePromotionUserService extends IService { + + + /** + * 分页查询用户信息 + * + * @param userPage 用户分页对象,包含分页参数和结果 + * @param queryUserIds 查询用户的ID列表 + * @param promotionId 推广活动ID + * @return 包含推广成员信息的分页结果 + */ + Page queryUserForPage(Page userPage, List queryUserIds, String promotionId); + + /** + * 查询用户的所有推广活动列表 + * + * @param userId 用户ID + * @return 用户参与的所有推广活动列表 + */ + List queryMyAllPromotionList(String userId); + + /** + * 查询用户的指定推广活动 + * + * @param userId 用户ID + * @param promotionId 推广活动ID + * @return 用户参与的指定推广活动信息,不存在则返回null + */ + MyCultivatePromotionListVo queryMyPromotionById(String userId, String promotionId); + + /** + * 完成地图 + * + * @param userId 用户ID,用于标识完成推广的用户 + * @param promotionId 推广活动ID,用于关联推广活动信息 + * @param tenantId 租户ID,用于指定租户下的推广活动 + */ + void completePromotion(String userId, String promotionId, String tenantId); + + /** + * 查询用户完成的推广活动列表 + * + * @param userId 用户ID + * @return 用户完成的推广活动列表 + */ + List queryMyCompletePromotion(String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateTaskLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateTaskLogService.java new file mode 100644 index 0000000..71c5ed0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/FtbCultivateTaskLogService.java @@ -0,0 +1,135 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; + +import java.util.List; + + +public interface FtbCultivateTaskLogService extends IService { + + + /** + * 根据任务ID、用户ID和阶段ID查询任务日志 + * + * @param taskId 任务ID + * @param userId 用户ID + * @param phaseId 阶段ID + * @return 任务日志 + */ + FtbCultivateTaskLog getByTaskIdAndUserIdAndPhaseId(String taskId, String userId, String phaseId); + + + /** + * 获取任务日志 + * + * @param taskId 任务ID + * @param userId 用户ID + * @return 任务日志列表 + */ + List queryCompleteTaskPhase(String taskId, String userId); + + /** + * 保存任务日志 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + */ + void saveOrUpdateByCondition(String userId, String taskId, String phaseId, Integer issuedCertificate); + + /** + * 获取任务日志 + * + * @param taskIds 任务ID + * @param userId 用户ID + * @return 任务日志列表 + */ + List batchQueryCompleteTaskPhase(List taskIds, String userId); + + /** + * 保存课程任务阶段状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param state 状态(-1-未配置 0-未完成 1-已完成) + */ + void saveCourseTaskPhase(String userId, String taskId, String phaseId, Integer state); + + /** + * 保存考试任务阶段状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param state 状态(-1-未配置 0-未完成 1-已完成) + */ + void saveExamTaskPhase(String userId, String taskId, String phaseId, Integer state, FtbCultivateExamUser examUser); + + /** + * 保存练习任务阶段状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param state 状态(-1-未配置 0-未完成 1-已完成) + */ + void savePracticeTaskPhase(String userId, String taskId, String phaseId, Integer state); + + /** + * 保存鉴定任务阶段状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param state 状态(-1-未配置 0-未完成 1-已完成) + */ + void saveIdentificationTaskPhase(String userId, String taskId, String phaseId, Integer state, CultivateIdentifyApply cultivateIdentifyApply); + + /** + * 保存考试结果状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param examResult 考试结果状态(-1-未配置 0-待考试 1-待批阅 2-已逾期 3-合格 4-不合格 5-优秀) + */ + void saveExamResultState(String userId, String taskId, String phaseId, Integer examResult); + + /** + * 保存鉴定结果状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param identifyResult 鉴定结果状态(-1-未配置 0-合格 1-优秀 2-不合格) + */ + void saveIdentifyResultState(String userId, String taskId, String phaseId, Integer identifyResult); + + /** + * 保存用户考试ID + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param userExamId 用户考试ID + */ + void saveUserExamId(String userId, String taskId, String phaseId, String userExamId); + + /** + * 保存用户鉴定ID + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param userIdentifyId 用户鉴定ID + */ + void saveUserIdentifyId(String userId, String taskId, String phaseId, String userIdentifyId); + + void checkAndSetTaskPhaseStart(String userId, String taskId, String phaseId); + + List queryTaskPhaseLogs(String userId, String taskId, String phaseId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CourseGainedCommentService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CourseGainedCommentService.java new file mode 100644 index 0000000..e94cd01 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CourseGainedCommentService.java @@ -0,0 +1,35 @@ +package jnpf.cultivate.v2.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.v2.gained.req.V2AppCommentPageListReq; +import jnpf.model.cultivate.v2.gained.vo.V2AppCourseGainedCommentVO; + +import java.util.List; + +public interface V2CourseGainedCommentService { + + /** + * 一级评论列表 + * + * @param req 请求 + * @return 评论列表 + */ + Page getList(V2AppCommentPageListReq req); + + /** + * 查看对应的心得评论回复 + * + * @param firstCommentId 一级评论id + * @return 评论列表 + */ + List listReply(String firstCommentId); + + /** + * 处理旧数据,为没有 F_Path 字段的评论生成路径 + * + * @param tenantId 租户 ID + * @return 处理结果 + */ + Boolean dealOldData(String tenantId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateBatchQueryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateBatchQueryService.java new file mode 100644 index 0000000..ff08c9e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateBatchQueryService.java @@ -0,0 +1,199 @@ +package jnpf.cultivate.v2.service; + +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterVo; +import jnpf.model.cultivate.v2.position.req.FtbCultivatePositionSaveReq; +import jnpf.model.cultivate.v2.promotion.vo.PositionProgressVo; +import jnpf.model.cultivate.v2.statistics.V2BatchQueryResult; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskSaveReq; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +/** + * V2培养模块批量查询服务接口 + * 提供根据ID列表批量查询课程、考试、鉴定、证书、练习技能信息的功能 + */ +public interface V2CultivateBatchQueryService { + + /** + * 岗位学习检查考试可用性 + * + * @param ftbCultivatePosition + */ + void positionCheckAvailable(FtbCultivatePositionSaveReq ftbCultivatePosition); + + /** + * 根据课程IDs批量查询课程信息 + * + * @param courseIds 课程ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 课程信息Map,key为ID,value为课程实体 + */ + Map batchQueryCoursesByIds(List courseIds, Integer isSelectNoDelete); + + /** + * 根据考试IDs批量查询考试信息 + * + * @param examIds 考试ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 考试信息Map,key为ID,value为考试实体 + */ + Map batchQueryExamsByIds(List examIds, Integer isSelectNoDelete); + + /** + * 根据鉴定IDs批量查询鉴定信息 + * + * @param identityIds 鉴定ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 鉴定信息Map,key为ID,value为鉴定实体 + */ + Map batchQueryIdentificationsByIds(List identityIds, Integer isSelectNoDelete); + + /** + * 根据证书IDs批量查询证书信息 + * + * @param certificateIds 证书ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 证书信息Map,key为ID,value为证书实体 + */ + Map batchQueryCertificatesByIds(List certificateIds, Integer isSelectNoDelete); + + /** + * 根据练习技能IDs批量查询技能信息 + * + * @param practiceSkillIds 练习技能ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 技能信息Map,key为ID,value为技能实体 + */ + Map batchQuerySkillsByIds(List practiceSkillIds, Integer isSelectNoDelete); + + /** + * 批量查询多种类型的信息 + * + * @param courseIds 课程ID列表 + * @param examIds 考试ID列表 + * @param identityIds 鉴定ID列表 + * @param certificateIds 证书ID列表 + * @param practiceSkillIds 练习技能ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 包含所有查询结果的Map集合 + */ + V2BatchQueryResult batchQueryAllByIds( + List courseIds, + List examIds, + List identityIds, + List certificateIds, + List practiceSkillIds, + Integer isSelectNoDelete + ); + + /** + * 根据用户ID和课程ID列表批量查询用户的课程学习情况 + * + * @param userId 用户ID列表 + * @param courseIds 课程ID列表 + * @return 课程学习情况Map,key为userId-courseId,value为学习情况实体 + */ + Map batchQueryUserCourseLearnStatus(String userId, List courseIds); + + /** + * 根据用户ID和考试ID列表批量查询用户的考试情况 + * + * @param userId 用户ID列表 + * @param examIds 考试ID列表 + * @return 考试情况Map,key为userId-examId,value为考试情况实体 + */ + Map batchQueryUserExamStatus(String userId, List examIds); + + /** + * 根据用户ID和鉴定ID列表批量查询用户的鉴定情况 + * + * @param userId 用户ID列表 + * @param identifyIds 鉴定ID列表 + * @return 鉴定情况Map,key为userId-identifyId,value为鉴定情况实体 + */ + Map batchQueryUserIdentificationStatus(String userId, List identifyIds); + + Map batchQueryUserIdentificationStatus(String userId, List identifyIds, Integer source, String sourceId); + + /** + * 根据用户ID和证书ID列表批量查询用户的证书获得情况 + * + * @param userId 用户ID列表 + * @param certificateIds 证书ID列表 + * @return 证书获得情况Map,key为userId-certificateId,value为证书情况实体 + */ + Map batchQueryUserCertificateStatus(String userId, List certificateIds); + + /** + * 根据用户ID和技能ID列表批量查询用户的技能完成情况 + * + * @param userId 用户ID列表 + * @param skillIds 技能ID列表 + * @return 技能完成情况Map,key为userId-skillId,value为练习情况实体 + */ + Map> batchQueryUserSkillStatus(String userId, List skillIds); + + + /** + * 根据课程ID列表批量查询每门课程的学习人数 + * + * @param courseIds 课程ID列表 + * @return 课程学习人数Map,key为课程ID,value为学习人数 + */ + Map batchQueryCourseLearningCount(List courseIds); + + /** + * 任务可用性检查 + * + * @param req 任务信息 + */ + void taskCheckAvailable(V2CultivateTaskSaveReq req); + + /** + * 根据章节ID列表批量查询每章的课程学习人数 + * + * @param chapterList 章节ID列表 + * @return 章节学习人数Map,key为章节ID,value为学习人数 + */ + Map batchQueryCourseChapterLearningState(String userId, List chapterList); + + /** + * 根据岗位ID列表批量查询每个岗位的课程、考试、鉴定、练习数量统计 + * + * @param postIds 岗位ID列表 + * @return 岗位进度统计Map,key为岗位ID,value为PositionProgressVo对象 + */ + Map batchQueryPositionProgress(List postIds, String userId); + + /** + * 根据用户ID和课程ID列表批量查询用户的课程学习情况(返回全部章节和章节的学习情况) + * + * @param userId 用户ID列表 + * @param courseIds 课程ID列表 + * @return 课程学习情况Map,key为课程id,value为章节列表 + */ + Map> batchQueryUserCourseChapterLearnStatus(String userId, List courseIds); + + Map batchQueryUserCourseLearnProgress(String userId, List courseIds); + + /** + * 根据用户ID查询所有已完成学习的课程ID列表 + * + * @param userId 用户ID + * @return 已完成课程ID列表 + */ + List getUserCompletedCourseIds(String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCertificateService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCertificateService.java new file mode 100644 index 0000000..f329f2c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCertificateService.java @@ -0,0 +1,8 @@ +package jnpf.cultivate.v2.service; + + +public interface V2CultivateCertificateService { + + + void triggerCertificate(String userId, String certificateId, String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCommonSettingGlobalService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCommonSettingGlobalService.java new file mode 100644 index 0000000..2b6a6ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCommonSettingGlobalService.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.cultivate.dto.FtbCultivateCommonSettingGlobalDTO; +import jnpf.model.cultivate.po.FtbCultivateCommonSettingGlobal; +import jnpf.model.cultivate.vo.FtbCultivateCommonSettingGlobalVO; + +/** + * 培训通用配置 服务接口 + * + * @author xgl + * @date 2026/02/11 + */ +public interface V2CultivateCommonSettingGlobalService extends IService { + + /** + * 获取培训通用配置信息 + * + * @return 培训通用配置信息 + */ + FtbCultivateCommonSettingGlobalVO getInfo(); + + /** + * 修改培训通用配置 + * + * @param dto 配置信息 + */ + void update(FtbCultivateCommonSettingGlobalDTO dto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseAppService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseAppService.java new file mode 100644 index 0000000..0ab0a10 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseAppService.java @@ -0,0 +1,69 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.v2.course.vo.app.*; +import jnpf.model.cultivate.v2.position.req.V2MyCultivateCommonCourseForAppReq; + +public interface V2CultivateCourseAppService { + + /** + * 获取课程详情 + * + * @param courseId 课程id + * @param userId 用户id + * @return 课程详情 + */ + V2CourseDetailsAppVO courseDetails(V2CourseAppDto dto); + + /** + * 获取课程大纲 + * + * @param courseId 课程id + * @param userId 用户id + * @return 课程详情 + */ + V2CourseOutlineAppVo courseOutline(String courseId, String userId); + + /** + * 获取章节详情 + * + * @param id 章节id + * @return 章节详情 + */ + V2ChapterAppDetails chapterDetails(String id, String userId); + + /** + * 章节学习 + * + * @param chapterStudyDTO 章节学习参数 + */ + void chapterStudy(V2ChapterStudyVo chapterStudyDTO); + + /** + * 持续时间记录 + * + * @param chapterStudyDTO 持续时间记录参数 + */ + void durationRecord(V2ChapterStudyVo chapterStudyDTO); + + /** + * 增加当前课程浏览量 + * + * @param courseId 课程id + */ + void courseBrowsing(String courseId); + + /** + * 通用课程列表 + * + * @param dto 参数 + * @param page 分页参数 + * @return 课程列表 + */ + Page commonCourseList(V2MyCultivateCommonCourseForAppReq dto, CultivatePage page); + + LastStudyCourseVo queryLastStudyCourse(); + + CommonCourseCountVo commonCourseCount(V2MyCultivateCommonCourseForAppReq dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseService.java new file mode 100644 index 0000000..90f1bd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateCourseService.java @@ -0,0 +1,82 @@ +package jnpf.cultivate.v2.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.v2.course.vo.AiHelperCourseStatisticsVo; +import jnpf.model.cultivate.v2.course.vo.UserLearningStatusVo; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseListReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseSelectReq; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseDetailsVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCoursePageVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseSelectVo; + +public interface V2CultivateCourseService { + + /** + * 课程列表分页查询 + * + * @param req 课程列表分页查询参数 + * @return 分页数据 + */ + Page webList(V2CultivateCourseListReq req); + + /** + * 添加课程 + * + * @param req 课程参数 + * @return 课程id + */ + String add(V2CultivateCourseReq req); + + /** + * 修改课程 + * + * @param req 课程参数 + */ + void update(V2CultivateCourseReq req); + + /** + * 删除课程 + * + * @param courseId 课程id + */ + void delete(String courseId); + + /** + * 课程详情 + * + * @param courseId 课程id + * @return 课程详情 + */ + V2CultivateCourseDetailsVo detail(String courseId); + + /** + * 添加岗位学习OR任务时,课程列表选择 + * + * @param req 分页请求参数 + * @return 分页数据 + */ + Page learnSelectCourseList(V2CultivateCourseSelectReq req); + + /** + * 清空课程学习记录 + * + * @param courseId 课程id + */ + void clearCourseLog(String courseId); + + /** + * 根据课程ID统计课程学习数据 + * @param courseId 课程ID + * @return 课程学习统计结果 + */ + AiHelperCourseStatisticsVo getCourseStatistics(String courseId); + + /** + * 查询指定用户的学习情况 + * @param userId 用户ID + * @return 用户学习情况(包含已完成的课程、考试、鉴定列表,已去重) + */ + UserLearningStatusVo getUserLearningStatus(String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamService.java new file mode 100644 index 0000000..f16a926 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamService.java @@ -0,0 +1,196 @@ +package jnpf.cultivate.v2.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.req.exam.SubExamQuestionReq; +import jnpf.model.cultivate.req.exam.WebRestartExamReq; +import jnpf.model.cultivate.resp.SubExamVo; +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.req.*; +import jnpf.model.cultivate.v2.exam.vo.*; + +import java.util.List; + +/** + * 考试服务v2 + * + * @author yanwenfu + * @create 2026-02-27 + */ +public interface V2CultivateExamService extends SuperService { + + /** + * 考试列表[分页] + * @param req 查询参数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getPage(V2QueryExamReq req); + + /** + * 新增岗位/自定义考试 + * @param req 考试参数 + */ + void addExam(V2SaveExamReq req) throws Exception; + + /** + * 编辑岗位/自定义考试 + * @param id 考试id + * @param req 考试参数 + */ + void updateExam(String id, V2SaveExamReq req) throws Exception; + + /** + * 考试详情 + * @param id 考试id + * @param copy 是否复制(1: 是, 0: 否) + */ + V2ExamVo getDetail(String id, Integer copy); + + /** + * 批阅详情 + * @param id 考试id + * @return jnpf.model.cultivate.v2.exam.vo.V2ExamDetailVo + */ + V2ExamDetailVo getMarkDetail(String id); + + /** + * 批阅详情 - 列表[分页] + * @param req 请求参数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getMarkDetailPage(MarkDetailQueryReq req); + + /** + * 删除考试 + * @param id 考试id + */ + void delExam(String id) throws Exception; + + /** + * 我的考试[分页] + * @param req 请求参数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo myExamList(V2AppQueryExamListReq req); + + /** + * 我的考试[tab count] + * @return jnpf.model.cultivate.v2.exam.vo.MyExamTabCountVo + */ + MyExamTabCountVo getMyExamTabCount(); + + /** + * 我的考试 - 详情 + * @param userExamId 用户考试id + * @return jnpf.model.cultivate.v2.exam.vo.MyExamDetailVo + */ + MyExamDetailVo myExamDetail(String userExamId); + + /** + * 开始考试 - 查询试题 + * @param userExamId 用户考试id + * @return java.util.List + */ + List getExamQuestionList(String userExamId) throws Exception; + + /** + * 批阅考试 - 考试列表[分页] + * @param req 请求参数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo readOverExamList(QueryReadOverExamListReq req); + + /** + * 批阅考试[tab count] + * @return jnpf.model.cultivate.v2.exam.vo.ReadOverTabCountVo + */ + ReadOverTabCountVo getReadOverTabCount(List organizeList); + + /** + * 批阅考试 - 排行榜[分页] + * @param req 请求参数 + * @return jnpf.model.cultivate.v2.exam.vo.UserExamRankVo + */ + UserExamRankVo getExamRankList(ExamRankReq req); + + /** + * 抽取题目数量及分值 + * @param bankIdList 题库ids + * @return jnpf.model.cultivate.v2.exam.vo.BankAnalysisVo + */ + BankAnalysisVo getBankAnalysis(List bankIdList); + + /** + * 选择题目 + * @param req 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getQuestionPage(QuestionReq req); + + /** + * 题目删除后考试处理为异常 + * @param questionIds 题目ids + */ + void questionChangeDeal(List questionIds); + + /** + * 重新考试 + * @param userExamId 用户考试id + * @return java.lang.String + */ + String restartExam(String userExamId) throws Exception; + + /** + * 重新考试 - 带次数验证 + * @param userExamId 用户考试id + * @return java.lang.String + */ + String restartExamWithValid(String userExamId) throws Exception; + + /** + * 验证是否可以重新考试[app&web] + * @param userExamId 用户考试id + * @return java.lang.Boolean + */ + Boolean checkRestartExam(String userExamId) throws Exception; + + void checkAndUpdateExpireUserExamStatus(); + + void checkAndUpdateExamStatus(); + + void webRestartExam(WebRestartExamReq req); + + /** + * 提交试卷 + * @param userExamId 用户考试id + * @param req 请求参数 + * @return jnpf.model.cultivate.resp.SubExamVo + */ + SubExamVo subUserExam(String userExamId, SubExamQuestionReq req); + + /** + * 培训1.0旧数据处理 + * @return java.lang.Boolean + */ + Boolean dealExamOldData(); + + /** + * 删除用户的考试 + * @param userExamId 用户的考试id + */ + void delUserExam(String userExamId) throws Exception; + + /** + * 查看考卷详情 + * @param userExamId 用户的考试id + * @return jnpf.model.cultivate.v2.exam.vo.PaperDetailVo + */ + PaperDetailVo getMarkPaperDetail(String userExamId) throws Exception; + + /** + * 我的考试列表[web](分页) + * @param queryDto 查询参数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getMyExamWebPage(MyExamWebQueryDto queryDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamStatisticsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamStatisticsService.java new file mode 100644 index 0000000..f2c5bcf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateExamStatisticsService.java @@ -0,0 +1,22 @@ +package jnpf.cultivate.v2.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForOrgReq; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForPersonReq; +import jnpf.model.cultivate.v2.exam.vo.AiHelperExamStatisticsVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForOrgVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForPersonVo; + +public interface V2CultivateExamStatisticsService { + PageListVO queryExamStatisticsForOrg(V2ExamStatisticsForOrgReq dto); + + PageInfo queryExamStatisticsForPerson(V2ExamStatisticsForPersonReq dto); + + /** + * 根据考试ID统计考试数据 + * @param examId 考试ID + * @return 考试统计结果 + */ + AiHelperExamStatisticsVo getExamStatistics(String examId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyApplyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyApplyService.java new file mode 100644 index 0000000..f41848a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyApplyService.java @@ -0,0 +1,90 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.model.cultivate.v2.apply.req.*; +import jnpf.model.cultivate.v2.apply.vo.*; +import jnpf.model.cultivate.vo.identify.IdentifyApplyDetailsInfoVo; + +import javax.validation.Valid; +import java.util.List; + + +public interface V2CultivateIdentifyApplyService { + + /** + * 获取Web端培养鉴定申请列表的分页数据 + * + * @param req 查询条件请求对象,包含分页信息和筛选条件 + * @return 分页结果对象,包含申请列表数据和分页信息 + */ + Page getWebPageApplyList(V2IdentifyApplyListReq req); + + /** + * 根据ID获取Web端培养鉴定申请的基本信息 + * + * @param id 申请记录的唯一标识符 + * @return 申请的基本信息对象 + */ + V2IdentifyApplyBasicInfoVo getWebApplyBasicInfo(String id); + + /** + * 根据ID获取Web端培养鉴定申请的详细信息 + * + * @param id 申请记录的唯一标识符 + * @return 申请的详细信息对象 + */ + V2IdentifyApplyInfoVo getWebApplyInfo(String id); + + /** + * 批量保存培养鉴定申请数据 + * + * @param req 保存请求对象,包含待保存的申请数据列表 + */ + void applyDataSaveBatch(V2IdentifyApplySaveReq req); + + /** + * 对指定ID的培养鉴定申请进行重新鉴定操作 + * + * @param id 需要重新鉴定的申请记录唯一标识符 + */ + void applyDataReIdentify(String id); + + /** + * 删除指定ID的培养鉴定申请记录 + * + * @param id 需要删除的申请记录唯一标识符 + * @param delMain 是否删除主表 + */ + void applyDataDelete(String id, Integer delMain); + + /** + * 查询当前用户的培养鉴定申请列表(移动端) + * + * @param req 查询条件请求对象,包含分页信息和筛选条件 + * @return 分页结果对象,包含申请列表数据和分页信息 + */ + Page queryMyIdentifyApplyList(V2MyIdentifyApplyListAppReq req); + + Page queryAppIdentifyApplyList(V2IdentifyApplyListAppReq req); + + V2IdentifyApplyAppBasicInfoVo getAppApplyBasicInfo(String id); + + V2IdentifyApplyItemVo itemList(String id); + + void applyDataSubmit(V2IdentifyApplySubmitReq req); + + /** + * 根据鉴定记录ID重新生成鉴定(保持创建时间和鉴定人不变) + * + * @param id 需要重新生成的鉴定申请记录唯一标识符 + */ + List regenerateIdentifyApply(String id); + + /** + * 重新提交鉴定记录 + * @param req 参数 + */ + void reSubmit(V2IdentifyApplySubmitReq req); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyTableService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyTableService.java new file mode 100644 index 0000000..3e8e177 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateIdentifyTableService.java @@ -0,0 +1,60 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableImportSaveReq; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableListReq; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableSaveReq; +import jnpf.model.cultivate.v2.identify.vo.V2CateIdentifyTableVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableInfoVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableListVo; + +import java.util.List; + + +public interface V2CultivateIdentifyTableService { + + /** + * 鉴定表/列表 + * + * @param req 查询条件参数对象,包含分页信息和筛选条件 + * @return 分页结果对象,包含鉴定表列表数据和分页信息 + */ + Page getPageList(V2IdentifyTableListReq req); + + /** + * 鉴定表/详情 + * + * @param tableId 鉴定表唯一标识ID + * @return 鉴定表详细信息对象 + */ + V2IdentifyTableInfoVo getInfo(String tableId); + + /** + * 鉴定表/新增 + * + * @param req 鉴定表保存请求参数对象,包含新增所需的所有字段信息 + */ + void saveData(V2IdentifyTableSaveReq req); + + /** + * 鉴定表/修改 + * + * @param tableId 鉴定表唯一标识ID + * @param req 鉴定表更新请求参数对象,包含需要修改的字段信息 + */ + void updateData(String tableId, V2IdentifyTableSaveReq req); + + /** + * 鉴定表/删除 + * + * @param tableId 鉴定表唯一标识ID + */ + void deleteData(String tableId); + + void importData(V2IdentifyTableImportSaveReq v2IdentifyTableSaveReq); + + long queryByTableName(String tableName); + + List getTableForCate(); +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateOfflineTrainService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateOfflineTrainService.java new file mode 100644 index 0000000..e7fc846 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateOfflineTrainService.java @@ -0,0 +1,14 @@ +package jnpf.cultivate.v2.service; + + +import jnpf.model.cultivate.v2.offline.V2CultivateOfflineTrainUpdateDTO; + +public interface V2CultivateOfflineTrainService { + + /** + * 修改培训结果 + * + * @param req 线下培训参数 + */ + void updateTrainingResults(V2CultivateOfflineTrainUpdateDTO req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePositionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePositionService.java new file mode 100644 index 0000000..d4a36f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePositionService.java @@ -0,0 +1,148 @@ +package jnpf.cultivate.v2.service; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbJobLearningPaginDTO; +import jnpf.model.cultivate.v2.course.vo.app.*; +import jnpf.model.cultivate.v2.course.web.req.V2NextUserCultivateCourseListReq; +import jnpf.model.cultivate.v2.position.req.FtbCultivatePositionSaveReq; +import jnpf.model.cultivate.v2.position.req.V2CultivateCoursePageReq; +import jnpf.model.cultivate.v2.position.req.V2CultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.req.V2OtherCultivatePositionCourseForAppReq; +import jnpf.model.cultivate.v2.position.vo.*; + +import java.util.List; + +public interface V2CultivatePositionService { + + /** + * 岗位学习管理-岗位学习分页列表 + * + * @param req 请求参数对象,包含分页和查询条件 + * @return {@link PageListVO}<{@link WebPositionLearningListVo}> + */ + Page webPageList(FtbJobLearningPaginDTO req); + + /** + * 添加岗位学习 + * + * @param req 岗位学习实体 + * @return 操作结果 + */ + String add(FtbCultivatePositionSaveReq req); + + /** + * 修改岗位学习 + * + * @param req 岗位学习实体 + * @return 操作结果 + */ + String update(FtbCultivatePositionSaveReq req); + + /** + * 删除岗位学习 + * + * @param id 岗位学习ID + * @return 操作结果 + */ + String delete(String id); + + /** + * 获取岗位学习 + * + * @param id 岗位学习ID + * @return 岗位学习实体 + */ + WebCultivatePositionVo get(String id); + + /** + * 获取岗位学习配置总览 + * + * @param id 岗位学习ID + * @return 岗位学习实体 + */ + WebCultivatePositionView webView(String id); + + /** + * 获取当前岗位的学习课程列表。 + * + * @param dto 请求参数对象,包含查询条件 + * @return 返回当前岗位的学习课程信息 + */ + PositionLearningCourseVo positionCourseLists(V2CultivatePositionCourseForAppReq dto); + + /** + * 获取其他岗位的岗位列表。 + * + * @param dto 请求参数对象,包含查询条件 + * @return 返回其他岗位的简要信息列表 + */ + List otherPositionList(V2OtherCultivatePositionCourseForAppReq dto); + + /** + * 获取其他岗位的课程数量统计信息。 + * + * @param dto 请求参数对象,包含查询条件 + * @return 返回其他岗位的课程数量统计信息 + */ + OtherPositionCourseCountVo otherPositionCourseCount(V2OtherCultivatePositionCourseForAppReq dto); + + /** + * 获取岗位学习的详细信息。 + * + * @param dto 请求参数对象,包含查询条件 + * @return 返回岗位学习的详细信息 + */ + AppCultivatePositionDetailVo positionStudyDetail(V2CultivatePositionDetailForApp dto); + + /** + * 分页查询岗位学习课程列表。 + * + * @param req 请求参数对象,包含分页和查询条件 + * @return 返回分页后的岗位学习课程列表 + */ + Page positionCourseList(V2CultivateCoursePageReq req); + + /** + * 检查指定岗位的学习记录是否存在。 + * + * @param postId 岗位ID + * @return 如果存在则返回true,否则返回false + */ + Boolean checkExist(String postId); + + /** + * 获取其他岗位的课程列表。 + * + * @param dto 请求参数对象,包含查询条件 + * @param page 分页参数对象 + * @return 返回其他岗位的课程简要信息列表 + */ + List otherPositionCourseList(V2OtherCultivatePositionCourseForAppReq dto, CultivatePage page); + + Page allCompleteCourseLists(V2NextUserCultivateCourseListReq req); + + Page allCompleteExamLists(V2NextUserCultivateCourseListReq req); + + Page allCompleteIdentityLists(V2NextUserCultivateCourseListReq req); + + PageListVO allCompletePracticeLists(V2NextUserCultivateCourseListReq req); + + Page selfPositionAllCompleteCourseLists(V2NextUserCultivateCourseListReq req); + + Page selfPositionAllCompleteExamLists(V2NextUserCultivateCourseListReq req); + + Page selfPositionAllCompleteIdentityLists(V2NextUserCultivateCourseListReq req); + + PageListVO selfPositionAllCompletePracticeLists(V2NextUserCultivateCourseListReq req); + + /** + * 检查考试是否被岗位学习绑定 + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + Boolean checkExamBinding(String examId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePostStudyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePostStudyService.java new file mode 100644 index 0000000..8b268e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePostStudyService.java @@ -0,0 +1,79 @@ +package jnpf.cultivate.v2.service; + + +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePostAndGradeVoExt; +import jnpf.permission.vo.v2.user.UserBoundVO; + +import javax.validation.constraints.NotBlank; +import java.util.List; +import java.util.Map; + +/** + * 岗位学习服务接口 + */ +public interface V2CultivatePostStudyService { + + + /** + * 处理岗位学习 + * + * @param message 培训信息 + */ + void dealPositionCourseStudy(CultivateMqDTO message); + + /** + * 处理岗位考试 + * + * @param message 培训信息 + */ + void dealPositionExamStudy(CultivateMqDTO message); + + /** + * 处理岗位 + * + * @param message 培训信息 + */ + void dealPositionReCheckStudy(CultivateMqDTO message); + + + /** + * 处理岗位鉴定 + * + * @param message 培训信息 + */ + void dealPositionIdentityStudy(CultivateMqDTO message); + + /** + * 处理岗位练习 + * + * @param message 培训信息 + */ + void dealPositionPracticeStudy(CultivateMqDTO message); + + + /** + * 处理通用课程学习 + * + * @param message 培训信息 + */ + void dealCommonCourseStudy(CultivateMqDTO message); + + void dealPositionExamStudyItem(UserBoundVO userBoundVO, String examId, V2CultivatePostAndGradeVoExt itemVo, String tenantId); + + void dealPositionReCheckStudyItem(UserBoundVO userBoundVO, V2CultivatePostAndGradeVoExt itemVo, String tenantId); + + void dealPositionIdentityStudyItem(UserBoundVO userBoundVO, String identityId, V2CultivatePostAndGradeVoExt itemVo, String tenantId); + + void dealPositionPracticeStudyItem(UserBoundVO userBoundVO, String practiceId, V2CultivatePostAndGradeVoExt itemVo, String tenantId); + + void prePositionCourseStudy(String userId, @NotBlank(message = "课程主键id不能为空") String courseId, String postLearnId,String gradeId, String tenantId); + + String checkExamStatusReturnUserExamId(String userId, String positionLearnId, List examIds, + Map userExamStatusMap, String courseId, String gradeId,String tenantId); + + String preCommonCourseStudy(String userId, String courseId, String tenantId); + + Boolean checkIsTriggerPositionCertificate(String userId, String certificateId, String positionLearnId, String postId, String gradeId, String courseId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePromotionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePromotionService.java new file mode 100644 index 0000000..c3cdd31 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivatePromotionService.java @@ -0,0 +1,166 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.promotion.req.*; +import jnpf.model.cultivate.v2.promotion.vo.*; + +import java.util.List; +import java.util.Map; + + +public interface V2CultivatePromotionService { + + /** + * web查询学习地图列表 + * + * @param page 分页查询参数对象,包含分页信息如页码、每页大小等 + * @param dto 包含查询条件和筛选参数 + * @return PageListVO 分页结果对象, + * 包含网页培育推广列表数据和分页相关信息 + */ + PageListVO webList(CultivatePage page, FtbCultivatePromotionReq dto); + + /** + * 新增学习地图 + * + * @param creatDto 新增参数对象,包含新增信息 + * @return String 新增结果信息,如成功返回"新增成功",失败返回错误信息 + */ + String add(V2CultivatePromotionCreateReq creatDto); + + /** + * 修改学习地图 + * + * @param creatDto 修改参数对象,包含修改信息 + * @return String 修改结果信息,如成功返回"修改成功",失败返回错误信息 + */ + String update(V2CultivatePromotionCreateReq creatDto); + + /** + * 删除学习地图 + * + * @param id 学习地图id + * @return String 删除结果信息,如成功返回"删除成功",失败返回错误信息 + */ + void delete(String id); + + /** + * 根据ID获取学习地图信息 + * + * @param id 学习地图id + * @return V2CultivatePromotionVo 查询结果对象,包含学习地图信息 + */ + V2CultivatePromotionVo get(String id); + + /** + * 根据地图主键id 获取地图成员信息 + * + * @param req 请求参数对象,包含查询条件 + * @param page 分页查询参数对象,包含分页信息如页码、每页大小等 + * @return V2WebCultivateStudyMemberListVo 获取结果对象,包含学习地图成员信息 + */ + V2WebCultivateStudyMemberListVo queryPromotionUserList(V2WebCultivateStudyMemberListReq req, CultivatePage page); + + /** + * 查询在岗位学习中完成的配置的岗位&职级 + * + * @param req 请求参数对象,包含排除条件 + * @return List 查询结果列表,包含岗位&职级信息 + */ + List queryCultivatePosition(V2CultivatePositionExcludeReq req); + + /** + * 获取我的所有学习地图列表 + * + * @param userId 用户ID + * @return List 查询结果列表,包含用户的学习地图信息 + */ + List queryMyAllPromotionList(String userId); + + /** + * 获取学习地图阶段信息(userId+promotionId+level) + * + * @param userId 用户id + * @param promotionId 学习地图id + * @param level 阶段 + * @return PromotionPhaseVo 查询结果对象,包含学习地图阶段信息 + */ + PromotionPhaseVo queryPromotionInfoForLevel(String userId, String promotionId, Integer level); + + /** + * 添加成员到学习地图 + * + * @param id 学习地图id + * @param creatDto 成员信息参数对象,包含新增成员的信息 + */ + void addMemberToPromotion(String id, V2CultivatePromotionCreateReq creatDto); + + /** + * 选定一个学习地图 + * + * @param userId 用户id + * @param promotionId 学习地图id + */ + void selectPromotion(String userId, String promotionId); + + /** + * 学习地图的一个阶段,选定学习的岗位 + * + * @param req 请求参数对象,包含岗位选择信息 + */ + void recordSelectPosition(V2CultivatePromotionSelectPositionReq req); + + /** + * 获取岗位学习详情 + * + * @param req 请求参数对象,包含查询条件 + * @return AppCultivatePromotionPositionDetailVo 岗位学习详情对象 + */ + AppCultivatePromotionPositionDetailVo getPositionStudyDetail(V2CultivatePromotionPositionDetailForApp req); + + /** + * 获取岗位学习详情 + * + * @param req 岗位学习详情请求参数对象 + * @return Integer 岗位学习状态码 + */ + Integer promotionLevelStatus(V2CultivatePromotionPhaseStateForApp req); + + /** + * 修改学习地图状态为错误 + * + * @param list 学习地图id列表 + */ + void updatePromotionError(List list); + + /** + * 个人维度统计 + * + * @param req 请求参数对象,包含统计条件 + * @return PageListVO 统计结果分页对象 + */ + PageListVO personStatistics(V2PromotionPersonStatisticReq req); + + /** + * 组织维度统计 + * + * @param req 请求参数对象,包含统计条件 + * @return PageListVO 统计结果分页对象 + */ + Page organizationStatistics(V2PromotionOrgStatisticReq req); + + Map> queryAllSelectPosition(String userId, List promotionIds); + + /** + * 根据用户ID和学习地图ID查询指定学习地图及分阶段岗位信息 + * + * @param userId 用户ID + * @param promotionId 学习地图ID + * @return UserPromotionDetailVo 用户学习地图详情,包含该学习地图的各阶段岗位信息 + */ + UserPromotionDetailVo getUserPromotionDetail(String userId, String promotionId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateQuestionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateQuestionService.java new file mode 100644 index 0000000..ff9299f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateQuestionService.java @@ -0,0 +1,25 @@ +package jnpf.cultivate.v2.service; + + +import jnpf.base.service.SuperService; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.resp.QuestionVo; +import jnpf.model.cultivate.v2.question.req.BatchAddQuestionReq; +import jnpf.model.cultivate.v2.question.req.V2EditQuestionReq; +import jnpf.model.cultivate.v2.question.req.V2ExcelImportQuestionResultReq; +import jnpf.model.cultivate.v2.question.req.V2ImportQuestionResultVo; + +import javax.validation.Valid; +import java.util.List; + +public interface V2CultivateQuestionService extends SuperService { + + + void batchAddQuestion(@Valid BatchAddQuestionReq req) throws Exception; + + void updateData(@Valid V2EditQuestionReq editQuestion) throws Exception; + + QuestionVo getInfo(String id); + + V2ImportQuestionResultVo realImportData(String questionBankId, List normal); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskCountService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskCountService.java new file mode 100644 index 0000000..bf4a1f5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskCountService.java @@ -0,0 +1,22 @@ +package jnpf.cultivate.v2.service; + +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskCountReq; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskCountVo; + +/** + * V2培训任务统计服务 + * + * @author system + * @create 2026-05-06 + */ +public interface V2CultivateTaskCountService { + + /** + * V2查询任务统计列表 + * + * @param req 查询请求参数 + * @return 分页统计结果 + */ + PageInfo queryTaskCountList(V2CultivateTaskCountReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskService.java new file mode 100644 index 0000000..29347ef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskService.java @@ -0,0 +1,166 @@ +package jnpf.cultivate.v2.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnAllocationDTO; +import jnpf.model.cultivate.v2.position.req.V2CultivateCoursePageReq; +import jnpf.model.cultivate.v2.position.vo.V2CultivateJobLearnCourseVo; +import jnpf.model.cultivate.v2.task.req.V2CultivateLearnTaskListForManagerReq; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskCountReq; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskSaveReq; +import jnpf.model.cultivate.v2.task.req.V2MyCultivateTaskListForReq; +import jnpf.model.cultivate.v2.task.vo.*; + +import java.util.List; +import java.util.Map; + + +public interface V2CultivateTaskService { + + + /** + * 添加学习任务 + * + * @param req 添加学习任务所需的参数 + * @return 添加成功后的任务ID + */ + String add(V2CultivateTaskSaveReq req); + + /** + * 更新学习任务 + * + * @param req 更新学习任务所需的参数 + * @return 更新成功后的任务ID + */ + String update(V2CultivateTaskSaveReq req); + + /** + * 分配任务 + * + * @param allocationDTO 分配数据传输对象,包含分配培养学习所需的各项数据 + */ + void allocation(FtbCultivateLearnAllocationDTO allocationDTO); + + /** + * 中止指定任务 + * + * @param taskId 任务ID,用于标识需要中止的任务 + */ + void abort(String taskId); + + /** + * 删除指定ID的任务 + * + * @param id 任务的唯一标识符 + */ + void deleteTask(String id); + + /** + * 获取指定ID的任务详情 + * + * @param id 任务的唯一标识符 + * @return 任务详情 + */ + V2CultivateTaskDetailsVo get(String id); + + + /** + * app任务管理查询任务列表 + * + * @return 任务列表 + */ + PageListVO queryTaskListForApp(V2CultivateLearnTaskListForManagerReq req); + + /** + * app任务管理查询任务列表小气泡数 + * + * @return 任务列表小气泡数 + */ + Map queryTaskCountForApp(String keyWord); + + /** + * app任务管理-任务完成统计 + * + * @param taskId 任务ID + * @return 任务完成统计信息 + */ + V2CultivateLearnTaskFinishStatisticsVo getCompletionStatisticsForManagerApp(String taskId); + + /** + * app任务管理-人员完成情况列表 + * + * @param taskId 任务id + * @return 人员列表 + */ + PageListVO queryTaskUserListForManagerApp(String taskId, CultivatePage page); + + /** + * app我的任务列表 + * + * @return 我的任务列表 + */ + PageListVO queryMyTaskListForApp(V2MyCultivateTaskListForReq req); + + /** + * app我的任务列表计数 + * + * @return 我的任务列表计数 + */ + Map queryMyTaskCountForApp(String keyWord); + + + /** + * 开始任务 + * + * @param myTaskId 我的任务id + * @param userId 用户id + */ + void startTask(String myTaskId, String userId); + + /** + * 定时检查任务状态(接定时任务) + * + * @param tenantId 租户id + */ + void checkAndUpdateTaskStatus(String tenantId); + + /** + * app我的任务列表-任务简单信息 + * + * @param myTaskId 我的任务 + * @return 任务简单信息 + */ + V2MyCultivateLearnTaskSimpleInfoVo queryMyTaskSimpleInfoForApp(String myTaskId); + + /** + * app我的任务详情-阶段详情 + * + * @param myTaskId 我的任务id + * @param phase 阶段ID + * @return 阶段详情 + */ + V2CultivateLearnTaskPhaseVo queryMyTaskPhaseInfoForApp(String myTaskId, String phase); + + + Page positionCourseList(V2CultivateCoursePageReq req); + + List queryUserTaskInfo(String myTaskId); + + /** + * 检查考试是否被学习任务绑定 + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + Boolean checkExamBinding(String examId); + + /** + * 检查用户任务所有阶段是否完成 + * + * @param userTaskId 用户任务ID(我的任务ID) + * @return 用户任务所有阶段完成状态 + */ + CheckUserTaskAllPhasesCompleteVo checkUserTaskAllPhasesComplete(String userTaskId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskStudyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskStudyService.java new file mode 100644 index 0000000..bfa550a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2CultivateTaskStudyService.java @@ -0,0 +1,55 @@ +package jnpf.cultivate.v2.service; + + +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPhase; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; + +import javax.validation.constraints.NotBlank; +import java.util.List; +import java.util.Map; + +/** + * 任务学习服务接口 + */ +public interface V2CultivateTaskStudyService { + + + /** + * 处理任务学习 + * + * @param message 培训信息 + */ + void dealTaskCourseStudy(CultivateMqDTO message); + + + /** + * 处理任务考试 + * + * @param message 培训信息 + */ + void dealTaskExamStudy(CultivateMqDTO message); + + /** + * 处理任务鉴定 + * + * @param message 培训信息 + */ + void dealTaskIdentityStudy(CultivateMqDTO message); + + + /** + * 处理任务练习 + * + * @param message 培训信息 + */ + void dealTaskPracticeStudy(CultivateMqDTO message); + + + void dealTaskStudyItem(UserBoundVO userPrimaryBoundOne, String taskId, String tenantId); + + void preTaskCourseStudy(String userId, @NotBlank(message = "课程主键id不能为空") String courseId, String taskId, String tenantId); + + FtbCultivateExamUser checkAndTriggerExam(String userId, String taskId, FtbCultivateLearnTaskPhase phaseEntity, List examIds, Map userExamStatusMap, String tenantId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2TeachingSkillService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2TeachingSkillService.java new file mode 100644 index 0000000..d86fa04 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/V2TeachingSkillService.java @@ -0,0 +1,45 @@ +package jnpf.cultivate.v2.service; + +import jnpf.model.cultivate.v2.exam.req.SkillDto; +import jnpf.model.cultivate.v2.exam.vo.SkillUploadInfoVo; +import jnpf.model.cultivate.v2.exam.vo.SkillVo; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 技能点服务[v2] + * + * @author yanwenfu + * @create 2026-02-28 + */ +public interface V2TeachingSkillService { + + /** + * 技能点 - 导入 + * @param file 导入文件 + * @return jnpf.model.cultivate.v2.exam.vo.SkillUploadInfoVo + */ + SkillUploadInfoVo teachingSkillImport(MultipartFile file) throws Exception; + + /** + * 技能点 - 模板下载 + * @return java.lang.String + */ + String templateDownload() throws Exception; + + /** + * 根据分类查询技能点[下拉] + * @param skillDto 技能点dto + * @return java.util.List + */ + List getDownSkillList(SkillDto skillDto); + + /** + * 查询分类列表[下拉] + * @param searchSource 类型(0: 自定义, 1: 系统默认, null: 全部) + * @return java.util.List + */ + List getDownCategoryList(Integer searchSource); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivateExamDrawRuleServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivateExamDrawRuleServiceImpl.java new file mode 100644 index 0000000..8f4a67a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivateExamDrawRuleServiceImpl.java @@ -0,0 +1,26 @@ +package jnpf.cultivate.v2.service.impl; + +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.CultivateExamDrawRuleMapper; +import jnpf.cultivate.v2.service.CultivateExamDrawRuleService; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.exam.vo.ConnectDrawRuleVo; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 抽题规则服务实现 + * + * @author yanwenfu + * @create 2026-03-05 + */ +@Service +public class CultivateExamDrawRuleServiceImpl extends SuperServiceImpl implements CultivateExamDrawRuleService { + + @Override + public List getConnectQuestionRule(List questionIds) { + + return baseMapper.getConnectQuestionRule(questionIds); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivatePositionCourseLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivatePositionCourseLogServiceImpl.java new file mode 100644 index 0000000..139001a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/CultivatePositionCourseLogServiceImpl.java @@ -0,0 +1,22 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivatePositionCourseLogMapper; +import jnpf.cultivate.v2.service.CultivatePositionCourseLogService; +import jnpf.entity.cultivate.CultivatePositionCourseLogEntity; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 岗位学习课程记录服务实现类 + */ +@Service +public class CultivatePositionCourseLogServiceImpl extends ServiceImpl implements CultivatePositionCourseLogService { + + + @Override + public List queryMyAllCompletePositionCourse(String userId) { + return baseMapper.queryMyAllCompletePositionCourse(userId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCommonSettingGlobalServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCommonSettingGlobalServiceImpl.java new file mode 100644 index 0000000..e4223f5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCommonSettingGlobalServiceImpl.java @@ -0,0 +1,59 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateCommonSettingGlobalMapper; +import jnpf.cultivate.v2.service.V2CultivateCommonSettingGlobalService; +import jnpf.model.cultivate.dto.FtbCultivateCommonSettingGlobalDTO; +import jnpf.model.cultivate.po.FtbCultivateCommonSettingGlobal; +import jnpf.model.cultivate.vo.FtbCultivateCommonSettingGlobalVO; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; + +/** + * 培训通用配置 服务实现类 + * + * @author xgl + * @date 2026/02/11 + */ +@Service +public class FtbCultivateCommonSettingGlobalServiceImpl + extends ServiceImpl + implements V2CultivateCommonSettingGlobalService { + + /** + * 获取培训通用配置信息 + * + * @return {@link FtbCultivateCommonSettingGlobalVO} + */ + @Override + public FtbCultivateCommonSettingGlobalVO getInfo() { + // 查询唯一的配置记录,如果没有则返回null + FtbCultivateCommonSettingGlobal entity = baseMapper.selectById("1"); + return convertFtbCultivateCommonSettingGlobalVO(entity); + } + + /** + * 转换为VO + * + * @param entity 实体 + * @return {@link FtbCultivateCommonSettingGlobalVO} + */ + private FtbCultivateCommonSettingGlobalVO convertFtbCultivateCommonSettingGlobalVO(FtbCultivateCommonSettingGlobal entity) { + FtbCultivateCommonSettingGlobalVO vo = new FtbCultivateCommonSettingGlobalVO(); + vo.setId(entity.getId()); + vo.setWatermark(entity.getWatermark()); + vo.setScreenshot(entity.getScreenshot()); + vo.setAttachmentsRule(entity.getAttachmentsRule()); + return vo; + } + + /** + * 提交培训通用配置 + * + * @param dto 配置信息 + */ + @Override + public void update(FtbCultivateCommonSettingGlobalDTO dto) { + this.updateById(dto.convert(UserProvider.getLoginUserId())); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourceLearningLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourceLearningLogServiceImpl.java new file mode 100644 index 0000000..eb8a56f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourceLearningLogServiceImpl.java @@ -0,0 +1,18 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateCourseLearningLogMapper; +import jnpf.cultivate.v2.service.FtbCultivateCourseLearningLogService; +import jnpf.entity.cultivate.FtbCultivateCourseLearningLogEntity; +import org.springframework.stereotype.Service; + +/** + * 学习课程日志表 ServiceImpl + * + * @author JNPF + * @since 2026-04-08 + */ +@Service +public class FtbCultivateCourceLearningLogServiceImpl extends ServiceImpl implements FtbCultivateCourseLearningLogService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourseSettingGlobalServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourseSettingGlobalServiceImpl.java new file mode 100644 index 0000000..4920134 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateCourseSettingGlobalServiceImpl.java @@ -0,0 +1,29 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateCourseSettingGlobalMapper; +import jnpf.cultivate.v2.service.FtbCultivateCourseSettingGlobalService; +import jnpf.model.cultivate.dto.FtbCultivateCourseSettingGlobalDTO; +import jnpf.model.cultivate.po.FtbCultivateCourseSettingGlobal; +import org.springframework.stereotype.Service; + +/** + * 课程配置表 服务实现类 + */ +@Service +public class FtbCultivateCourseSettingGlobalServiceImpl + extends ServiceImpl + implements FtbCultivateCourseSettingGlobalService { + + @Override + public FtbCultivateCourseSettingGlobal getInfo() { + // 查询唯一的配置记录,如果没有则返回默认值 + return baseMapper.selectById("1"); + } + + @Override + public void update(FtbCultivateCourseSettingGlobalDTO dto) { + this.updateById(dto.convert()); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyCategoriesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyCategoriesServiceImpl.java new file mode 100644 index 0000000..f38df4b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyCategoriesServiceImpl.java @@ -0,0 +1,283 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateIdentifyCategoriesMapper; +import jnpf.cultivate.mapper.FtbCultivateIdentifyItemsPoolMapper; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyCategoriesService; +import jnpf.enums.cultivate.CultivateIsSystemEnum; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.identify.req.QueryIdentifyCategoryListReq; +import jnpf.model.cultivate.v2.identify.req.V2SaveItemCategoryReq; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyCategoryTreeVo; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 鉴定项分类服务实现类 + * + * @author lingma + * @since 2026-02-24 + */ +@Slf4j +@Service +public class FtbCultivateIdentifyCategoriesServiceImpl extends ServiceImpl implements FtbCultivateIdentifyCategoriesService { + + + @Autowired + private FtbCultivateIdentifyItemsPoolMapper ftbCultivateIdentifyItemsPoolMapper; + + + /** + * 分类树 + * + * @param req 查询参数 + * @return 分类树 + */ + @Override + public List getCategoryTree(QueryIdentifyCategoryListReq req) { + List categories = this.list(new LambdaQueryWrapper().eq(FtbCultivateIdentifyCategories::getEnabledMark, 0).like(StringUtils.isNotBlank(req.getKeyWord()), FtbCultivateIdentifyCategories::getName, req.getKeyWord()).orderByAsc(FtbCultivateIdentifyCategories::getCreatorTime)); + + if (CollUtil.isEmpty(categories)) { + return new ArrayList<>(); + } + List ids = new ArrayList<>(); + for (FtbCultivateIdentifyCategories category : categories) { + ids.add(category.getId()); + if (!"0".equals(category.getParentId())) { + ids.add(category.getParentId()); + } + } + List allLists = this.listByIds(ids); + List list = allLists.stream().map(this::convertToVo).collect(Collectors.toList()); + //查询分类的数量 + + List groupedResult = ftbCultivateIdentifyItemsPoolMapper.countByCateId(); + Map countMap = new HashMap<>(); + if (CollUtil.isNotEmpty(groupedResult)) { + countMap = groupedResult.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, BatchCommonCountDto::getNum)); + } + return buildCateTree(list, countMap); + + } + + /** + * 新增分类 + * + * @param req 新增参数 + * @return 分类 + */ + @Override + public FtbCultivateIdentifyCategories insertData(V2SaveItemCategoryReq req) { + String userId = UserProvider.getUser().getUserId(); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateIdentifyCategories::getName, req.getName()) + .eq(FtbCultivateIdentifyCategories::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("名称已经存在"); + } + FtbCultivateIdentifyCategories entity = new FtbCultivateIdentifyCategories(); + entity.setName(req.getName()); + if (StringUtils.isEmpty(req.getParentId())) { + entity.setParentId("0"); + } else { + entity.setParentId(req.getParentId()); + } + entity.setEnabledMark(0); + entity.setLastModifyUserId(userId); + entity.setCreatorUserId(userId); + entity.setCreatorTime(new java.util.Date()); + entity.setLastModifyTime(new java.util.Date()); + entity.setIsSystem(CultivateIsSystemEnum.NO.getCode()); + + baseMapper.insert(entity); + return entity; + } + + /** + * 修改分类 + * + * @param id 分类id + * @param req 修改参数 + * @return 分类 + */ + @Override + public FtbCultivateIdentifyCategories updateData(String id, V2SaveItemCategoryReq req) { + queryAndCheckById(id); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateIdentifyCategories::getName, req.getName()) + .ne(FtbCultivateIdentifyCategories::getId, id) + .eq(FtbCultivateIdentifyCategories::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分类名称已经存在"); + } + if(StringUtils.isEmpty(req.getParentId())){ + req.setParentId("0"); + } + if (StringUtils.isNotEmpty(req.getParentId()) && !"0".equals(req.getParentId())) { + queryAndCheckParent(req.getParentId()); + } + if(id.equals(req.getParentId())){ + throw new RuntimeException("上级分类不能是自己"); + } + String userId = UserProvider.getUser().getUserId(); + //修改分类 + FtbCultivateIdentifyCategories categories = new FtbCultivateIdentifyCategories(); + categories.setName(req.getName()); + categories.setId(id); + categories.setEnabledMark(0); + categories.setLastModifyUserId(userId); + categories.setLastModifyTime(new java.util.Date()); + categories.setParentId(req.getParentId()); + baseMapper.updateById(categories); + return categories; + } + + /** + * 删除分类 + * + * @param id 分类id + */ + @Override + public void deleteData(String id) { + + //检测分类是否删除 + FtbCultivateIdentifyCategories identifyCategories = queryAndCheckById(id); + if(identifyCategories.getIsSystem().equals(CultivateIsSystemEnum.YES.getCode())){ + throw new RuntimeException("系统内置分类不能删除"); + } + + //检测分类下是否有下级分类 + List childCategoryIds = queryChildCategoryIds(id); + if (CollUtil.isNotEmpty(childCategoryIds)) { + throw new RuntimeException("请先删除该分类下的所有子分类"); + } + long count = ftbCultivateIdentifyItemsPoolMapper.selectCount(new LambdaQueryWrapper().eq(FtbCultivateIdentifyItemsPool::getCateId, id).eq(FtbCultivateIdentifyItemsPool::getEnabledMark, 0)); + if (count > 0) { + throw new RuntimeException("请先删除该分类下的鉴定项"); + } + + baseMapper.update(null, new LambdaUpdateWrapper() + .eq(FtbCultivateIdentifyCategories::getId, id) + .set(FtbCultivateIdentifyCategories::getEnabledMark, 1)); + } + + @Override + public List getAllNameList() { + List categoriesList = getAllList(); + List nameList = new ArrayList<>(); + if (CollUtil.isNotEmpty(categoriesList)) { + for (FtbCultivateIdentifyCategories ftbNoticeCategories : categoriesList) { + nameList.add(ftbNoticeCategories.getName()); + } + } + return nameList; + } + + @Override + public List getAllList() { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().ne(FtbCultivateIdentifyCategories::getParentId, '0') + .eq(FtbCultivateIdentifyCategories::getEnabledMark, 0); + return baseMapper.selectList(wrapper); + + } + + public List queryChildCategoryIds(String id) { + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbCultivateIdentifyCategories::getParentId, id).eq(FtbCultivateIdentifyCategories::getEnabledMark, 0); + List categoriesList = baseMapper.selectList(wrapper); + List childIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(categoriesList)) { + for (FtbCultivateIdentifyCategories ftbNoticeCategories : categoriesList) { + childIds.add(ftbNoticeCategories.getId()); + } + } + return childIds; + } + + + private void queryAndCheckParent(String parentId) { + FtbCultivateIdentifyCategories parent = baseMapper.selectById(parentId); + if (null == parent) { + throw new RuntimeException("父级分类不存在"); + } + if (parent.getEnabledMark().equals(1)) { + throw new RuntimeException("父级分类已被删除"); + } + if(StringUtils.isNotEmpty(parent.getParentId()) && !"0".equals(parent.getParentId())){ + throw new RuntimeException("父级分类不能有父级分类"); + } + } + + public FtbCultivateIdentifyCategories queryAndCheckById(String id) { + FtbCultivateIdentifyCategories entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("分类不存在"); + } + if (entity.getEnabledMark().equals(1)) { + throw new RuntimeException("分类已经删除"); + } + return entity; + } + + + private List buildCateTree(List list, Map countMap) { + List retList = new ArrayList<>(); + Map> map = new HashMap<>(); + for (V2IdentifyCategoryTreeVo dto : list) { + Integer count = countMap.get(dto.getId()); + dto.setCount(Objects.requireNonNullElse(count, 0)); + if ("0".equals(dto.getParentId())) { + retList.add(dto); + } else { + List tempList = map.get(dto.getParentId()); + if (CollUtil.isEmpty(tempList)) { + tempList = new ArrayList<>(); + } + tempList.add(dto); + map.put(dto.getParentId(), tempList); + } + } + + for (V2IdentifyCategoryTreeVo dto : list) { + List child = map.get(dto.getId()); + if (CollUtil.isNotEmpty(child)) { + dto.setChildren(child); + } + } + return retList; + } + + + /** + * 转换为VO对象 + * + * @param category 分类实体 + * @return VO对象 + */ + private V2IdentifyCategoryTreeVo convertToVo(FtbCultivateIdentifyCategories category) { + V2IdentifyCategoryTreeVo vo = new V2IdentifyCategoryTreeVo(); + BeanUtil.copyProperties(category, vo); + return vo; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyItemsPoolServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyItemsPoolServiceImpl.java new file mode 100644 index 0000000..2237323 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateIdentifyItemsPoolServiceImpl.java @@ -0,0 +1,330 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateIdentifyCategoriesMapper; +import jnpf.cultivate.mapper.FtbCultivateIdentifyItemsPoolMapper; +import jnpf.cultivate.service.CultivateIdentifyItemsService; +import jnpf.cultivate.service.CultivateIdentifyTableService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyItemsPoolService; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.enums.AppraisalScoreTypeEnum; +import jnpf.enums.cultivate.IdentifyItemExceptionEnum; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.common.req.V2BatchByPrimaryIdReq; +import jnpf.model.cultivate.v2.item_pool.req.IdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.req.SaveCultivateIdentifyItemsPoolReq; +import jnpf.model.cultivate.v2.item_pool.vo.FtbCultivateIdentifyItemsPoolVo; +import jnpf.model.cultivate.v2.item_pool.vo.V2IdentifyItemVo; +import jnpf.model.cultivate.v2.position.vo.WebPositionLearningListVo; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.entity.UserEntity; +import jnpf.util.ConstantUtil; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 鉴定项池服务实现类 + * + * @author lingma + * @since 2026-02-24 + */ +@Slf4j +@Service +public class FtbCultivateIdentifyItemsPoolServiceImpl extends ServiceImpl implements FtbCultivateIdentifyItemsPoolService { + + @Autowired + private FtbCultivateIdentifyCategoriesMapper categoriesMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private CultivateIdentifyTableService cultivateIdentifyTableService; + + @Autowired + private CultivateIdentifyItemsService itemsService; + + /** + * 获取鉴定项池信息 + * + * @param id 主键值 + * @return 实体对象 + */ + @Override + public FtbCultivateIdentifyItemsPoolVo getInfo(String id) { + FtbCultivateIdentifyItemsPool entity = baseMapper.selectById(id); + if (null == entity || entity.getEnabledMark().equals(1)) { + throw new RuntimeException("数据不存在"); + } + FtbCultivateIdentifyItemsPoolVo vo = new FtbCultivateIdentifyItemsPoolVo(); + BeanUtil.copyProperties(entity, vo); + if (StringUtils.isNotEmpty(vo.getCateId())) { + FtbCultivateIdentifyCategories ftbCultivateIdentifyCategories = categoriesMapper.selectById(vo.getCateId()); + if (ftbCultivateIdentifyCategories != null) { + vo.setCateName(ftbCultivateIdentifyCategories.getName()); + } + } + return vo; + } + + /** + * 新增鉴定项池 + * + * @param req 实体对象 + */ + @Override + public void addData(SaveCultivateIdentifyItemsPoolReq req) { + String userId = UserProvider.getUser().getUserId(); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateIdentifyItemsPool::getName, req.getName()) + .eq(FtbCultivateIdentifyItemsPool::getType, req.getType()) + .eq(FtbCultivateIdentifyItemsPool::getEnabledMark, 0); + if (req.getType().equals(AppraisalScoreTypeEnum.FLOATING.getCode())) { + wrapper.lambda() + .eq(FtbCultivateIdentifyItemsPool::getMinScore, req.getMinScore()) + .eq(FtbCultivateIdentifyItemsPool::getMaxScore, req.getMaxScore()); + } else { + wrapper.lambda() + .eq(FtbCultivateIdentifyItemsPool::getTotalScore, req.getTotalScore()); + } + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("鉴定项已经存在"); + } + FtbCultivateIdentifyItemsPool entity = new FtbCultivateIdentifyItemsPool(); + entity.setName(req.getName()); + entity.setCateId(req.getCateId()); + entity.setType(req.getType()); + entity.setTotalScore(req.getTotalScore()); + entity.setMinScore(req.getMinScore()); + entity.setMaxScore(req.getMaxScore()); + entity.setEnabledMark(0); + entity.setCreatorTime(new java.util.Date()); + entity.setCreatorUserId(userId); + entity.setLastModifyTime(new java.util.Date()); + entity.setLastModifyUserId(userId); + entity.setBusinessId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.ITEM_POOL)); + baseMapper.insert(entity); + + } + + /** + * 修改鉴定项池 + * + * @param id 主键值 + * @param req 实体对象 + */ + @Override + @Transactional + public void updateData(String id, SaveCultivateIdentifyItemsPoolReq req) { + FtbCultivateIdentifyItemsPool oldEntity = queryAndCheckById(id); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateIdentifyItemsPool::getName, req.getName()) + .ne(FtbCultivateIdentifyItemsPool::getId, id) + .eq(FtbCultivateIdentifyItemsPool::getType, req.getType()) + .eq(FtbCultivateIdentifyItemsPool::getEnabledMark, 0); + + if (req.getType().equals(AppraisalScoreTypeEnum.FLOATING.getCode())) { + wrapper.lambda() + .eq(FtbCultivateIdentifyItemsPool::getMinScore, req.getMinScore()) + .eq(FtbCultivateIdentifyItemsPool::getMaxScore, req.getMaxScore()); + } else { + wrapper.lambda() + .eq(FtbCultivateIdentifyItemsPool::getTotalScore, req.getTotalScore()); + } + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("鉴定项已经存在"); + } + FtbCultivateIdentifyItemsPool entity = req.convertData(id); + String userId = UserProvider.getUser().getUserId(); + //修改分类 + entity.setLastModifyUserId(userId); + baseMapper.updateById(entity); + + syncIdentifyTableForUpdate(oldEntity, entity); + } + + private void syncIdentifyTableForUpdate(FtbCultivateIdentifyItemsPool oldEntity, FtbCultivateIdentifyItemsPool entity) { + IdentifyItemExceptionEnum exceptionEnum = IdentifyItemExceptionEnum.NORMAL; + if (!oldEntity.getType().equals(entity.getType())) { + exceptionEnum = IdentifyItemExceptionEnum.MODIFY_EXCEPTION; + } else { + if (oldEntity.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + if (!oldEntity.getTotalScore().equals(entity.getTotalScore())) { + exceptionEnum = IdentifyItemExceptionEnum.MODIFY_EXCEPTION; + } + } else { + if (!oldEntity.getMinScore().equals(entity.getMinScore()) || !oldEntity.getMaxScore().equals(entity.getMaxScore())) { + exceptionEnum = IdentifyItemExceptionEnum.MODIFY_EXCEPTION; + } + } + } + if (exceptionEnum == IdentifyItemExceptionEnum.NORMAL && entity.getName().equals(oldEntity.getName())) { + return; + } + addAbnormalTag(entity, exceptionEnum); + + } + + private void addAbnormalTag(FtbCultivateIdentifyItemsPool entity, IdentifyItemExceptionEnum exceptionEnum) { + List itemList = itemsService.list(Wrappers.lambdaQuery() + .eq(CultivateIdentifyItems::getPoolId, entity.getId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE)); + if (CollUtil.isEmpty(itemList)) { + return; + } + Set tableIds = new HashSet<>(); + for (CultivateIdentifyItems item : itemList) { + item.setIsAbnormal(exceptionEnum.getCode()); + item.setType(entity.getType()); + if (entity.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + item.setScore(BigDecimal.valueOf(entity.getTotalScore())); + } else { + item.setMinScore(entity.getMinScore()); + item.setMaxScore(entity.getMaxScore()); + } + + item.setCateId(entity.getCateId()); + if (exceptionEnum == IdentifyItemExceptionEnum.NORMAL) { + item.setName(entity.getName()); + } + tableIds.add(item.getTableId()); + } + itemsService.updateBatchById(itemList); + + LambdaUpdateWrapper updateTableWrapper = new LambdaUpdateWrapper() + .in(CultivateIdentifyTable::getId, tableIds) + .eq(CultivateIdentifyTable::getIsAbnormal, ConstantUtil.NUM_FALSE) + .set(CultivateIdentifyTable::getIsAbnormal, IdentifyItemExceptionEnum.MODIFY_EXCEPTION.getCode()); + cultivateIdentifyTableService.update(updateTableWrapper); + } + + /** + * 删除鉴定项池 + * + * @param id 主键值 + */ + @Override + public void deleteData(String id) { + FtbCultivateIdentifyItemsPool entity = queryAndCheckById(id); + entity.setEnabledMark(1); + baseMapper.updateById(entity); + syncIdentifyTableForDelete(entity); + } + + private void syncIdentifyTableForDelete(FtbCultivateIdentifyItemsPool entity) { + IdentifyItemExceptionEnum exceptionEnum = IdentifyItemExceptionEnum.DELETE_EXCEPTION; + addAbnormalTag(entity, exceptionEnum); + } + + /** + * 获取鉴定项池列表 + * + * @return + */ + @Override + public Page webPageList(IdentifyItemsPoolReq req) { + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + + Page result = baseMapper.webPageList(page, req); + List records = result.getRecords(); + if (CollUtil.isEmpty(records)) { + return result; + } + List userIds = records.stream() + .map(FtbCultivateIdentifyItemsPoolVo::getUserId) + .collect(Collectors.toList()); + + Map userMap = userApiV2Util.getUserNameAndCopyForUserIds(userIds); + + records.forEach(item -> { + UserEntity userEntity = userMap.get(item.getUserId()); + if (userEntity != null && userEntity.getRealName() != null) { + item.setUserName(userEntity.getRealName()); + } + }); + return result; + } + + /** + * 获取鉴定项池列表 + * + * @return 鉴定项池列表 + */ + @Override + public List listAll() { + return baseMapper.selectList(new LambdaQueryWrapper() + .eq(FtbCultivateIdentifyItemsPool::getEnabledMark, 0)); + } + + @Override + @Transactional + public void batchDel(V2BatchByPrimaryIdReq req) { + List oldList = baseMapper.selectBatchIds(req.getIds()); + if (CollUtil.isNotEmpty(oldList)) { + for (FtbCultivateIdentifyItemsPool ftbCultivateIdentifyItemsPool : oldList) { + syncIdentifyTableForDelete(ftbCultivateIdentifyItemsPool); + } + } + baseMapper.update(null, new LambdaUpdateWrapper() + .in(FtbCultivateIdentifyItemsPool::getId, req.getIds()) + .set(FtbCultivateIdentifyItemsPool::getEnabledMark, 1)); + } + + @Override + public void batchAddData(List normal) { + if (CollUtil.isEmpty(normal)) { + return; + } + String userId = UserProvider.getUser().getUserId(); + List list = new ArrayList<>(); + for (V2IdentifyItemVo req : normal) { + FtbCultivateIdentifyItemsPool entity = req.convertData(userId); + entity.setBusinessId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.ITEM_POOL)); + list.add(entity); + } + this.saveBatch(list); + + } + + /** + * 根据id查询鉴定项 + * + * @param id 主键值 + * @return 鉴定项 + */ + public FtbCultivateIdentifyItemsPool queryAndCheckById(String id) { + FtbCultivateIdentifyItemsPool entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("鉴定项不存在"); + } + if (entity.getEnabledMark().equals(1)) { + throw new RuntimeException("鉴定项已经删除"); + } + return entity; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLabelServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLabelServiceImpl.java new file mode 100644 index 0000000..e69d25c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLabelServiceImpl.java @@ -0,0 +1,239 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivateIdentifyTableMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.mapper.FtbCultivateLabelMapper; +import jnpf.cultivate.mapper.TeachingSkillMapper; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.enums.cultivate.CultivateIsSystemEnum; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.v2.label.dto.FtbCultivateLabelReq; +import jnpf.model.cultivate.v2.label.dto.FtbCultivateUpdateLabelReq; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.model.cultivate.v2.position.req.V2CultivateCommonCourseForAppReq; +import jnpf.util.ConstantUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 培训标签关联表 服务实现类 + */ +@Service +public class FtbCultivateLabelServiceImpl extends ServiceImpl implements FtbCultivateLabelService { + + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Autowired + private CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Autowired + private TeachingSkillMapper teachingSkillMapper; + + /** + * 根据类型获取培训标签列表 + * + * @param type 类型标识,用于筛选特定类型的培训标签 + * @return 返回符合条件的培训标签列表 + */ + @Override + public List getList(Integer type, Integer source) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivateLabel::getType, type); + queryWrapper.eq(source != null, FtbCultivateLabel::getSource, source); + queryWrapper.eq(FtbCultivateLabel::getEnabledMark, 0); // 只查询未删除的记录 + queryWrapper.orderByDesc(FtbCultivateLabel::getSource); + queryWrapper.orderByAsc(FtbCultivateLabel::getCreatorTime); + List entityList = this.list(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return new ArrayList<>(); + } + // 将实体列表转换为 VO 列表 + return entityList.stream() + .map(this::entityToVo) + .collect(Collectors.toList()); + } + + /** + * 新增培训标签关联表 + * + * @param req 包含新增培训标签所需数据的请求对象 + * @return 返回新增成功的培训标签信息 + */ + @Override + public FtbCultivateLabelVo addData(FtbCultivateLabelReq req) { + // 检查标签名称是否重复 + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(FtbCultivateLabel::getName, req.getName()); + checkWrapper.eq(FtbCultivateLabel::getEnabledMark, 0); + checkWrapper.eq(FtbCultivateLabel::getType, req.getType()); + if (this.count(checkWrapper) > 0) { + throw new RuntimeException("标签名称已存在,请勿重复添加!"); + } + + // 创建实体对象 + FtbCultivateLabel entity = new FtbCultivateLabel(); + entity.setName(req.getName()); + entity.setSource(CultivateIsSystemEnum.NO.getCode()); // 使用常量替代硬编码 + entity.setType(req.getType()); + entity.setEnabledMark(0); // 设置为正常状态 + // 保存到数据库 + try { + this.save(entity); + } catch (Exception e) { + throw new RuntimeException("保存标签失败,请稍后重试!"); + } + // 返回 VO 对象 + return this.entityToVo(entity); + } + + + /** + * 更新培训标签关联表信息 + * + * @param id 主键ID,用于定位需要更新的培训标签记录 + * @param req 包含更新培训标签所需数据的请求对象,需通过@Valid注解进行校验 + * @return 返回更新后的培训标签信息 + */ + @Override + public FtbCultivateLabelVo updateData(String id, FtbCultivateUpdateLabelReq req) { + FtbCultivateLabel label = baseMapper.selectById(id); + if (label == null || label.getEnabledMark() == 1) { + throw new RuntimeException("标签不存在或已删除!"); + } + + // 检查标签名称是否重复 + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(FtbCultivateLabel::getName, req.getName()); + checkWrapper.eq(FtbCultivateLabel::getEnabledMark, 0); + checkWrapper.eq(FtbCultivateLabel::getType, label.getType()); + checkWrapper.ne(FtbCultivateLabel::getId, id); + if (this.count(checkWrapper) > 0) { + throw new RuntimeException("标签名称已存在!"); + } + label.setName(req.getName()); + baseMapper.updateById(label); + return this.entityToVo(label); + } + + /** + * 获取培训标签关联表信息 + * + * @param id 主键ID,用于唯一标识一条培训标签记录 + * @return 返回对应的培训标签详细信息 + */ + @Override + public FtbCultivateLabelVo getInfo(String id) { + FtbCultivateLabel label = baseMapper.selectById(id); + if (label == null || label.getEnabledMark() == 1) { + throw new RuntimeException("标签不存在或已删除!"); + } + return entityToVo(label); + } + + /** + * 删除培训标签关联表信息 + * + * @param id 主键ID,用于定位需要删除的培训标签记录 + */ + @Override + @Transactional + public void deleteData(String id) { + FtbCultivateLabel label = baseMapper.selectById(id); + if (label == null || label.getEnabledMark() == 1) { + throw new RuntimeException("标签不存在或已删除!"); + } + if (label.getSource().equals(CultivateIsSystemEnum.YES.getCode())) { + throw new RuntimeException("系统标签不能删除!"); + } + + if (label.getType().equals(1)) { + //检查课程是否已经关联改标签 + LambdaQueryWrapper courseWrapper = new LambdaQueryWrapper<>(); + courseWrapper.eq(FtbCultivateCourse::getTypeId, id); + courseWrapper.eq(FtbCultivateCourse::getEnableMark, 0); + Long l = ftbCultivateCourseMapper.selectCount(courseWrapper); + if (l > 0) { + throw new RuntimeException("该标签已关联课程,请先解除关联关系!"); + } + } else if (label.getType().equals(2)) { + //检查鉴定表是否已经关联改标签 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateIdentifyTable::getLabelId, id); + queryWrapper.eq(CultivateIdentifyTable::getDeleteMark, 0); // 只检查有效的课程 + long count = cultivateIdentifyTableMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("该标签已关联鉴定表,请先解除关联关系!"); + } + } else if (label.getType().equals(3)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(TeachingSkill::getCategoryId, id); + queryWrapper.eq(TeachingSkill::getDeleteMark, 0); // 只检查有效的课程 + long count = teachingSkillMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("该标签已关联技能点,请先解除关联关系!"); + } + teachingSkillMapper.update(null, new LambdaUpdateWrapper() + .eq(TeachingSkill::getCategoryId, id) + .set(TeachingSkill::getCategoryId, "")); + } + + label.setEnabledMark(1); + baseMapper.updateById(label); + } + + /** + * 获取常用课程标签列表 + * + * @param req 获取常用课程标签列表的请求参数 + * @return 返回常用课程标签列表 + */ + @Override + public List commonCourseLabelList(V2CultivateCommonCourseForAppReq req) { + return baseMapper.commonCourseLabelList(req); + } + + @Override + public Map getLabelMapByName(List categoryNameList) { + + if (categoryNameList.isEmpty()) { + return new HashMap<>(); + } + // 查询分类信息 + LambdaQueryWrapper labelQuery = new LambdaQueryWrapper() + .eq(FtbCultivateLabel::getType, 3) + .eq(FtbCultivateLabel::getEnabledMark, ConstantUtil.NUM_FALSE) + .in(FtbCultivateLabel::getName, categoryNameList); + List labelList = this.list(labelQuery); + return labelList.stream().collect(Collectors.toMap(FtbCultivateLabel::getName, Function.identity())); + } + + /** + * 将实体类转换为VO + */ + private FtbCultivateLabelVo entityToVo(FtbCultivateLabel entity) { + FtbCultivateLabelVo vo = new FtbCultivateLabelVo(); + vo.setId(entity.getId()); + vo.setName(entity.getName()); + vo.setSource(entity.getSource()); + vo.setType(entity.getType()); + return vo; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPhaseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPhaseServiceImpl.java new file mode 100644 index 0000000..499b76c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPhaseServiceImpl.java @@ -0,0 +1,12 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskPhaseMapper; +import jnpf.cultivate.v2.service.FtbCultivateLearnTaskPhaseService; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPhase; +import org.springframework.stereotype.Service; + + +@Service +public class FtbCultivateLearnTaskPhaseServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskPhaseService { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPracticeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPracticeServiceImpl.java new file mode 100644 index 0000000..b651bcb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateLearnTaskPracticeServiceImpl.java @@ -0,0 +1,30 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskPracticeMapper; +import jnpf.cultivate.v2.service.FtbCultivateLearnTaskPracticeService; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPractice; +import jnpf.model.cultivate.v2.task.vo.FtbCultivateLearnTaskPracticeVo; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 学习任务关联练习服务实现类(V2版本) + * + * @author admin + * @since 2025-01-21 + */ +@Service +public class FtbCultivateLearnTaskPracticeServiceImpl extends ServiceImpl implements FtbCultivateLearnTaskPracticeService { + + @Override + public List listByTaskId(String taskId) { + return baseMapper.listByTaskId(taskId); + } + + @Override + public List listByTaskIdAndPhaseId(String taskId, String phaseId) { + return baseMapper.listByTaskIdAndPhaseId(taskId, phaseId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionLogServiceImpl.java new file mode 100644 index 0000000..9d8efa8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionLogServiceImpl.java @@ -0,0 +1,302 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.CultivatePositionCourseLogMapper; +import jnpf.cultivate.mapper.FtbCultivatePositionLogMapper; +import jnpf.cultivate.v2.service.FtbCultivatePositionLogService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivatePositionCourseLogEntity; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.FtbCultivatePositionLog; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 岗位学习完成记录表服务实现类 + */ +@Service +public class FtbCultivatePositionLogServiceImpl extends ServiceImpl implements FtbCultivatePositionLogService { + + @Autowired + CultivatePositionCourseLogMapper cultivatePositionCourseLogMapper; + + /** + * 根据条件保存或更新用户职位学习记录 + * 该方法会根据传入的用户ID、职位学习ID等条件来决定是保存新记录还是更新现有记录 + * + * @param userId 用户ID,用于标识操作的用户 + * @param positionLearnId 职位学习ID,用于标识具体的职位学习记录 + * @param postId 职位ID,关联到具体的职位信息 + * @param gradeId 年级ID,表示用户所在的年级信息 + * @param courseId 课程id + */ + @Override + public void completePositionCourse(String userId, String positionLearnId, String postId, String gradeId, String courseId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0); + List logList = cultivatePositionCourseLogMapper.selectList(queryWrapper); + + if (CollUtil.isNotEmpty(logList)) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .set(CultivatePositionCourseLogEntity::getState, 2) + .set(CultivatePositionCourseLogEntity::getTenantId, "1")//todo 打一个标记 后面删除 + + ; + cultivatePositionCourseLogMapper.update(null, updateWrapper); + } else { + // 如果不存在,则新增 + CultivatePositionCourseLogEntity newLog = new CultivatePositionCourseLogEntity(); + newLog.setPostLearnId(positionLearnId); + newLog.setPostId(postId); + newLog.setGradeId(gradeId); + newLog.setCourseId(courseId); + newLog.setUserId(userId); + newLog.setState(2); + newLog.setEnabledMark(0); + newLog.setTenantId("2");//todo 打一个标记 后面删除 + cultivatePositionCourseLogMapper.insert(newLog); + } + } + + /** + * 查询用户已完成的职位学习记录 + * + * @param userId 用户ID + * @return 职位学习记录列表 + */ + @Override + public List queryMyCompletePosition(String userId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(FtbCultivatePositionLog::getState, 2) + .eq(FtbCultivatePositionLog::getUserId, userId) + .eq(FtbCultivatePositionLog::getEnabledMark, 0); + return this.list(queryWrapper); + + } + + @Override + public List batchQueryCompletePosition(List currUserIds) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(FtbCultivatePositionLog::getState, 2) + .in(FtbCultivatePositionLog::getUserId, currUserIds) + .eq(FtbCultivatePositionLog::getEnabledMark, 0); + return this.list(queryWrapper); + } + + /** + * 根据条件保存或更新用户职位学习记录 + * 该方法会根据传入的用户ID、职位学习ID等条件来决定是保存新记录还是更新现有记录 + * + * @param userId 用户ID,用于标识操作的用户 + * @param positionLearnId 职位学习ID,用于标识具体的职位学习记录 + * @param postId 职位ID,关联到具体的职位信息 + * @param gradeId 年级ID,表示用户所在的年级信息 + * @param tenantId 租户ID + */ + @Override + public void completePosition(String userId, String positionLearnId, String postId, String gradeId, String tenantId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePositionLog::getPostLearnId, positionLearnId) + .eq(FtbCultivatePositionLog::getPostId, postId) + .eq(StringUtils.isNotEmpty(gradeId), FtbCultivatePositionLog::getGradeId, gradeId) + .eq(FtbCultivatePositionLog::getUserId, userId) + .eq(FtbCultivatePositionLog::getEnabledMark, 0); + List logList = this.list(queryWrapper); + + if (CollUtil.isNotEmpty(logList)) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbCultivatePositionLog::getPostLearnId, positionLearnId) + .eq(FtbCultivatePositionLog::getPostId, postId) + .eq(StringUtils.isNotEmpty(gradeId), FtbCultivatePositionLog::getGradeId, gradeId) + .eq(FtbCultivatePositionLog::getUserId, userId) + .eq(FtbCultivatePositionLog::getEnabledMark, 0) + .set(FtbCultivatePositionLog::getState, 2) + ; + this.update(updateWrapper); + } else { + // 如果不存在,则新增 + FtbCultivatePositionLog newLog = new FtbCultivatePositionLog(); + newLog.setPostLearnId(positionLearnId); + newLog.setPostId(postId); + newLog.setGradeId(gradeId); + newLog.setUserId(userId); + newLog.setState(2); + newLog.setEnabledMark(0); + this.save(newLog); + } + } + + /** + * 查询用户是否完成职位学习 + * + * @param userId 用户ID + * @param positionLearnId 职位学习ID + * @param postId 职位ID + * @param gradeId 年级ID + * @param tenantId 租户ID + * @return true 表示用户已完成职位学习,false 表示用户未完成职位学习 + */ + @Override + public boolean selectIsCompletePosition(String userId, String positionLearnId, String postId, String gradeId, String tenantId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePositionLog::getPostLearnId, positionLearnId) + .eq(FtbCultivatePositionLog::getPostId, postId) + .eq(StringUtils.isNotEmpty(gradeId), FtbCultivatePositionLog::getGradeId, gradeId) + .eq(FtbCultivatePositionLog::getUserId, userId) + .eq(FtbCultivatePositionLog::getState, 2) + .eq(FtbCultivatePositionLog::getEnabledMark, 0); + List logList = this.list(queryWrapper); + if (CollUtil.isEmpty(logList)) { + return false; + } + return true; + } + + @Override + public CultivatePositionCourseLogEntity queryIsTriggerExam(String positionLearnId, String userId, String courseId, String gradeId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0).orderByAsc(CultivatePositionCourseLogEntity::getCreatorTime); + return cultivatePositionCourseLogMapper.selectOne(queryWrapper); + + } + + @Override + public void recordUserExamId(String positionLearnId, String userId, String courseId, String gradeId, FtbCultivateExamUser ftbCultivateExamUser) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0); + List logList = cultivatePositionCourseLogMapper.selectList(queryWrapper); + + if (CollUtil.isNotEmpty(logList)) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .set(ftbCultivateExamUser != null, CultivatePositionCourseLogEntity::getHasExam, 2) + .set(CultivatePositionCourseLogEntity::getExamId, ftbCultivateExamUser.getExamId()) + .set(CultivatePositionCourseLogEntity::getUserExamId, ftbCultivateExamUser.getId()) + .set(CultivatePositionCourseLogEntity::getUserExamStatus, ftbCultivateExamUser.getStatus()) + ; + cultivatePositionCourseLogMapper.update(null, updateWrapper); + } else { + // 如果不存在,则新增 + CultivatePositionCourseLogEntity newLog = new CultivatePositionCourseLogEntity(); + newLog.setPostLearnId(positionLearnId); + newLog.setCourseId(courseId); + newLog.setUserId(userId); + if (ftbCultivateExamUser != null) { + newLog.setHasExam(2); + } + newLog.setExamId(ftbCultivateExamUser.getExamId()); + newLog.setUserExamId(ftbCultivateExamUser.getId()); + newLog.setUserExamStatus(ftbCultivateExamUser.getStatus()); + newLog.setGradeId(gradeId); + cultivatePositionCourseLogMapper.insert(newLog); + } + } + + @Override + public void recodePositionCourseExam(String userId, String examId, String userExamId, Integer status) { + + cultivatePositionCourseLogMapper.update(null, new LambdaUpdateWrapper() + .eq(CultivatePositionCourseLogEntity::getUserExamId, userExamId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getExamId, examId) + .set(CultivatePositionCourseLogEntity::getUserExamStatus, status)); + } + + @Override + public void recordUserIdentificationId(String positionLearnId, String userId, String courseId, String gradeId, CultivateIdentifyApply apply) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0); + List logList = cultivatePositionCourseLogMapper.selectList(queryWrapper); + + if (CollUtil.isNotEmpty(logList)) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .set(CultivatePositionCourseLogEntity::getIdentifyId, apply.getOriginalTableId()) + .set(CultivatePositionCourseLogEntity::getUserIdentifyId, apply.getId()) + ; + if (apply.getStatus().equals(0)) { + updateWrapper.set(CultivatePositionCourseLogEntity::getUserIdentifyStatus, -1); + } else { + updateWrapper.set(CultivatePositionCourseLogEntity::getUserIdentifyStatus, apply.getResult()); + } + cultivatePositionCourseLogMapper.update(null, updateWrapper); + } else { + // 如果不存在,则新增 + CultivatePositionCourseLogEntity newLog = new CultivatePositionCourseLogEntity(); + newLog.setPostLearnId(positionLearnId); + newLog.setCourseId(courseId); + newLog.setUserId(userId); + newLog.setIdentifyId(apply.getOriginalTableId()); + newLog.setUserIdentifyId(apply.getId()); + newLog.setGradeId(gradeId); + if (apply.getStatus().equals(0)) { + newLog.setUserIdentifyStatus(-1); + } else { + newLog.setUserIdentifyStatus(apply.getResult()); + } + cultivatePositionCourseLogMapper.insert(newLog); + } + } + + @Override + public void recodePositionCourseIdentify(String userId, String identityId, String userIdentifyId, Integer status, Integer userIdentifyStatus) { + cultivatePositionCourseLogMapper.update(null, + new LambdaUpdateWrapper() + .eq(CultivatePositionCourseLogEntity::getUserIdentifyId, userIdentifyId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0) + .set(CultivatePositionCourseLogEntity::getUserIdentifyStatus, + status == 0 ? -1 : userIdentifyStatus) + ); + } + + /** + * 根据用户ID和学习地图ID查询岗位学习课程记录 + * + * @param userId 用户ID + * @param postLearnId 学习地图ID(岗位学习ID) + * @return 岗位学习课程记录列表 + */ + @Override + public List queryByUserIdAndPostLearnId(String userId, String postLearnId) { + return cultivatePositionCourseLogMapper.queryByUserIdAndPostLearnId(userId, postLearnId); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionSettingServiceImpl.java new file mode 100644 index 0000000..8fbd772 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePositionSettingServiceImpl.java @@ -0,0 +1,52 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePositionSettingMapper; +import jnpf.cultivate.v2.service.FtbCultivatePositionSettingService; +import jnpf.model.cultivate.po.position.FtbCultivatePositionSetting; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +public class FtbCultivatePositionSettingServiceImpl extends ServiceImpl implements FtbCultivatePositionSettingService { + + /** + * 根据岗位学习id获取岗位设置 + * + * @param positionLearnId 岗位学习id + * @return 岗位设置 + */ + @Override + public FtbCultivatePositionSetting listByPostLearnId(String positionLearnId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FtbCultivatePositionSetting::getPostLearnId, positionLearnId); + List list = list(wrapper); + if (CollUtil.isNotEmpty(list)) { + return list.get(0); + } + return null; + } + + /** + * 根据岗位学习id和职级id获取岗位设置 + * + * @param positionLearnId 岗位学习id + * @param gradeId 职级id + * @return 岗位设置 + */ + @Override + public FtbCultivatePositionSetting listByPostLearnIdAndGradeId(String positionLearnId, String gradeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FtbCultivatePositionSetting::getPostLearnId, positionLearnId); + wrapper.eq(FtbCultivatePositionSetting::getGradeId, gradeId); + List list = list(wrapper); + if (CollUtil.isNotEmpty(list)) { + return list.get(0); + } + return null; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionLogServiceImpl.java new file mode 100644 index 0000000..5248242 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionLogServiceImpl.java @@ -0,0 +1,73 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePromotionLogMapper; +import jnpf.cultivate.v2.service.FtbCultivatePromotionLogService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionLog; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +public class FtbCultivatePromotionLogServiceImpl + extends ServiceImpl + implements FtbCultivatePromotionLogService { + + /** + * 完成晋升等级操作 + * + * @param userId 用户ID,标识执行晋升操作的用户 + * @param promotionId 晋升记录ID,用于标识具体的晋升活动或任务 + * @param level 晋升等级,表示用户要完成的具体等级数值 + * @param tenantId 租户ID,用于多租户环境下的数据隔离 + */ + @Override + public void completePromotionLevel(String userId, String promotionId, Integer level, String tenantId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePromotionLog::getPromotionId, promotionId) + .eq(FtbCultivatePromotionLog::getLevel, level) + .eq(FtbCultivatePromotionLog::getUserId, userId) + .eq(FtbCultivatePromotionLog::getEnabledMark, 0); + List logList = this.list(queryWrapper); + + if (CollUtil.isNotEmpty(logList)) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbCultivatePromotionLog::getPromotionId, promotionId) + .eq(FtbCultivatePromotionLog::getLevel, level) + .eq(FtbCultivatePromotionLog::getUserId, userId) + .eq(FtbCultivatePromotionLog::getEnabledMark, 0) + .set(FtbCultivatePromotionLog::getState, 2); + this.update(updateWrapper); + } else { + // 如果不存在,则新增 + FtbCultivatePromotionLog newLog = new FtbCultivatePromotionLog(); + newLog.setPromotionId(promotionId); + newLog.setLevel(level); + newLog.setUserId(userId); + newLog.setState(2); + newLog.setEnabledMark(0); + this.save(newLog); + } + } + + /** + * 查询用户的已完成晋升记录列表 + * + * @param userId 用户ID,用于查询指定用户的晋升日志记录 + * @param state 地图状态,1-未完成 2-已完成 null-全部 + * @return List 返回用户的所有已完成晋升记录列表 + */ + @Override + public List queryMyCompletePromotion(String userId, Integer state) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(FtbCultivatePromotionLog::getUserId, userId) + .eq(state != null, FtbCultivatePromotionLog::getState, state) + .eq(FtbCultivatePromotionLog::getEnabledMark, 0).orderByAsc(FtbCultivatePromotionLog::getLevel); + return this.list(queryWrapper); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionMemberNewServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionMemberNewServiceImpl.java new file mode 100644 index 0000000..c124a82 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionMemberNewServiceImpl.java @@ -0,0 +1,41 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePromotionMemberNewMapper; +import jnpf.cultivate.v2.service.FtbCultivatePromotionMemberNewService; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 晋升通道成员服务实现类 + */ +@Service +public class FtbCultivatePromotionMemberNewServiceImpl extends ServiceImpl implements FtbCultivatePromotionMemberNewService { + + /** + * 查询用户当前阶段的批次通用计数信息 + * + * @param userId 用户ID,用于标识查询的目标用户 + * @param promotionIds 推广活动ID列表,用于指定需要查询的推广活动范围 + * @return 返回批次通用计数DTO列表,包含用户在指定推广活动中各阶段的计数信息 + */ + @Override + public List queryMyCurrentPhase(String userId, List promotionIds) { + return baseMapper.queryMyCurrentPhase(userId, promotionIds); + } + + /** + * 查询用户所有已选择职位的信息 + * + * @param userId 用户ID,用于标识查询的目标用户 + * @return 返回FTB培养推广成员新实体列表,包含用户所有已选择的职位信息 + */ + @Override + public List queryMyAllSelectPosition(String userId) { + return baseMapper.queryMyAllSelectPosition(userId); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionNewMessageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionNewMessageServiceImpl.java new file mode 100644 index 0000000..126be0c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionNewMessageServiceImpl.java @@ -0,0 +1,15 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePromotionNewMessageMapper; +import jnpf.cultivate.v2.service.FtbCultivatePromotionNewMessageService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNewMessage; +import org.springframework.stereotype.Service; + +/** + * 晋升消息服务实现类 + */ +@Service +public class FtbCultivatePromotionNewMessageServiceImpl extends ServiceImpl implements FtbCultivatePromotionNewMessageService { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionPostNewServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionPostNewServiceImpl.java new file mode 100644 index 0000000..7c7637c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionPostNewServiceImpl.java @@ -0,0 +1,19 @@ +package jnpf.cultivate.v2.service.impl; + +import jnpf.cultivate.mapper.FtbCultivatePromotionPostNewMapper; +import jnpf.cultivate.v2.service.FtbCultivatePromotionPostNewService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 晋升通道岗位服务实现类 + */ +@Service +public class FtbCultivatePromotionPostNewServiceImpl extends ServiceImpl implements FtbCultivatePromotionPostNewService { + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionSettingServiceImpl.java new file mode 100644 index 0000000..7985874 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionSettingServiceImpl.java @@ -0,0 +1,16 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePromotionSettingMapper; +import jnpf.cultivate.v2.service.FtbCultivatePromotionSettingService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionSetting; +import org.springframework.stereotype.Service; + +/** + * 学习地图人员和岗位配置表服务实现类 + */ +@Service +public class FtbCultivatePromotionSettingServiceImpl + extends ServiceImpl + implements FtbCultivatePromotionSettingService { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionUserServiceImpl.java new file mode 100644 index 0000000..67af432 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivatePromotionUserServiceImpl.java @@ -0,0 +1,96 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivatePromotionUserMapper; +import jnpf.cultivate.v2.service.FtbCultivatePromotionUserService; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionUser; +import jnpf.model.cultivate.v2.promotion.vo.MyCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionMemberVo; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbCultivatePromotionUserServiceImpl extends ServiceImpl implements FtbCultivatePromotionUserService { + + + /** + * 分页查询用户信息 + * + * @param userPage 分页对象,包含分页参数和结果数据 + * @param queryUserIds 用户ID列表,用于筛选特定用户 + * @param promotionId 推广活动ID,用于关联推广活动查询 + * @return Page 分页结果对象,包含符合条件的用户信息列表 + */ + @Override + public Page queryUserForPage(Page userPage, List queryUserIds, String promotionId) { + return baseMapper.queryUserForPage(userPage, queryUserIds, promotionId); + + } + + /** + * 查询用户的全部推广活动列表 + * + * @param userId 用户ID,用于查询指定用户参与的所有推广活动 + * @return List 用户参与的推广活动列表 + */ + @Override + public List queryMyAllPromotionList(String userId) { + return baseMapper.queryMyAllPromotionList(userId); + } + + /** + * 查询用户的指定推广活动 + * + * @param userId 用户ID,用于查询指定用户参与的推广活动 + * @param promotionId 推广活动ID,用于查询指定的推广活动 + * @return MyCultivatePromotionListVo 用户参与的指定推广活动信息,不存在则返回null + */ + @Override + public MyCultivatePromotionListVo queryMyPromotionById(String userId, String promotionId) { + return baseMapper.queryMyPromotionById(userId, promotionId); + } + + /** + * 完成地图 + * + * @param userId 用户ID,用于标识完成推广的用户 + * @param promotionId 推广活动ID,用于关联推广活动信息 + * @param tenantId 租户ID,用于指定租户下的推广活动 + */ + @Override + public void completePromotion(String userId, String promotionId, String tenantId) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbCultivatePromotionUser::getPromotionId, promotionId) + .eq(FtbCultivatePromotionUser::getUserId, userId) + .eq(FtbCultivatePromotionUser::getIsComplete, 0) + .set(FtbCultivatePromotionUser::getIsComplete, 1) + ; + this.update(updateWrapper); + } + + /** + * 查询用户的全部完成推广活动列表 + * + * @param userId 用户ID,用于查询指定用户完成的推广活动 + * @return List 用户完成的推广活动列表 + */ + @Override + public List queryMyCompletePromotion(String userId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePromotionUser::getUserId, userId) + .eq(FtbCultivatePromotionUser::getIsComplete, 1); + List list = this.list(queryWrapper); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream().map(FtbCultivatePromotionUser::getPromotionId).collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateTaskLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateTaskLogServiceImpl.java new file mode 100644 index 0000000..77cca1a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/FtbCultivateTaskLogServiceImpl.java @@ -0,0 +1,366 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateTaskLogMapper; +import jnpf.cultivate.v2.service.FtbCultivateTaskLogService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + + +@Service +public class FtbCultivateTaskLogServiceImpl extends ServiceImpl implements FtbCultivateTaskLogService { + + /** + * 根据任务ID、用户ID和阶段ID查询任务日志 + * + * @param taskId 任务ID + * @param userId 用户ID + * @param phaseId 阶段ID + * @return 任务日志 + */ + @Override + public FtbCultivateTaskLog getByTaskIdAndUserIdAndPhaseId(String taskId, String userId, String phaseId) { + LambdaQueryWrapper logWrapper = new LambdaQueryWrapper<>(); + logWrapper.eq(FtbCultivateTaskLog::getTaskId, taskId) + .eq(FtbCultivateTaskLog::getUserId, userId) + .eq(FtbCultivateTaskLog::getPhaseId, phaseId) + .eq(FtbCultivateTaskLog::getEnabledMark, 0); + return getOne(logWrapper); + } + + /** + * 查询用户已完成的阶段 + * + * @param taskId 任务ID + * @param userId 用户ID + * @return 已完成的阶段列表 + */ + @Override + public List queryCompleteTaskPhase(String taskId, String userId) { + LambdaQueryWrapper logWrapper = new LambdaQueryWrapper<>(); + logWrapper.eq(FtbCultivateTaskLog::getTaskId, taskId) + .eq(FtbCultivateTaskLog::getUserId, userId) + .eq(FtbCultivateTaskLog::getState, 2) + .eq(FtbCultivateTaskLog::getEnabledMark, 0); + return baseMapper.selectList(logWrapper); + } + + /** + * 保存或更新任务日志 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + */ + @Override + public void saveOrUpdateByCondition(String userId, String taskId, String phaseId, Integer issuedCertificate) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + if (ftbCultivateTaskLog.getState().equals(2)) { + return; + } + ftbCultivateTaskLog.setIssuedCertificate(issuedCertificate); + ftbCultivateTaskLog.setState(2); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setIssuedCertificate(issuedCertificate); + ftbCultivateTaskLog.setState(2); + baseMapper.insert(ftbCultivateTaskLog); + } + } + + /** + * 批量查询用户已完成的阶段 + * + * @param taskIds 任务ID列表 + * @param userId 用户ID + * @return 已完成的阶段列表 + */ + @Override + public List batchQueryCompleteTaskPhase(List taskIds, String userId) { + LambdaQueryWrapper logWrapper = new LambdaQueryWrapper<>(); + logWrapper.in(FtbCultivateTaskLog::getTaskId, taskIds) + .eq(FtbCultivateTaskLog::getUserId, userId) + .eq(FtbCultivateTaskLog::getState, 2) + .eq(FtbCultivateTaskLog::getEnabledMark, 0); + List ftbCultivateTaskLogs = baseMapper.selectList(logWrapper); + if (CollUtil.isEmpty(ftbCultivateTaskLogs)) { + return new ArrayList<>(); + } + return ftbCultivateTaskLogs; + } + + /** + * 保存课程任务阶段 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + * @param state 阶段状态 + */ + @Override + public void saveCourseTaskPhase(String userId, String taskId, String phaseId, Integer state) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + if (ftbCultivateTaskLog.getCourseState().equals(1)) { + return; + } + ftbCultivateTaskLog.setCourseState(state); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setCourseState(state); + baseMapper.insert(ftbCultivateTaskLog); + } + + } + + /** + * 保存考试任务阶段 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + * @param state 阶段状态 + */ + @Override + public void saveExamTaskPhase(String userId, String taskId, String phaseId, Integer state, FtbCultivateExamUser examUser) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + if (ftbCultivateTaskLog.getExamState().equals(1)) { + return; + } + ftbCultivateTaskLog.setExamState(state); + if (examUser != null) { + ftbCultivateTaskLog.setExamResult(examUser.getStatus()); + } + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setExamState(state); + if (examUser != null) { + + ftbCultivateTaskLog.setExamResult(examUser.getStatus()); + ftbCultivateTaskLog.setUserExamId(examUser.getId()); + } + baseMapper.insert(ftbCultivateTaskLog); + } + + } + + /** + * practice任务阶段 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + * @param state 阶段状态 + */ + @Override + public void savePracticeTaskPhase(String userId, String taskId, String phaseId, Integer state) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + if (ftbCultivateTaskLog.getPracticeState().equals(1)) { + return; + } + ftbCultivateTaskLog.setPracticeState(state); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setPracticeState(state); + baseMapper.insert(ftbCultivateTaskLog); + } + + } + + /** + * 身份证任务阶段 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + * @param state 阶段状态 + */ + @Override + public void saveIdentificationTaskPhase(String userId, String taskId, String phaseId, Integer state, CultivateIdentifyApply cultivateIdentifyApply) { + + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + if (ftbCultivateTaskLog.getIdentifyState().equals(1)) { + return; + } + ftbCultivateTaskLog.setIdentifyState(state); + if (cultivateIdentifyApply != null) { + ftbCultivateTaskLog.setIdentifyResult(cultivateIdentifyApply.getStatus()); + ftbCultivateTaskLog.setUserIdentifyId(cultivateIdentifyApply.getId()); + } + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setIdentifyState(state); + if (cultivateIdentifyApply != null) { + ftbCultivateTaskLog.setIdentifyResult(cultivateIdentifyApply.getStatus()); + ftbCultivateTaskLog.setUserIdentifyId(cultivateIdentifyApply.getId()); + } + baseMapper.insert(ftbCultivateTaskLog); + } + + } + + @Override + public void checkAndSetTaskPhaseStart(String userId, String taskId, String phaseId) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + if (ftbCultivateTaskLog.getState().equals(1)) { + return; + } + ftbCultivateTaskLog.setState(1); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setState(1); + baseMapper.insert(ftbCultivateTaskLog); + } + } + + /** + * 保存考试结果状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param examResult 考试结果状态(-1-未配置 0-待考试 1-待批阅 2-已逾期 3-合格 4-不合格 5-优秀) + */ + @Override + public void saveExamResultState(String userId, String taskId, String phaseId, Integer examResult) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + ftbCultivateTaskLog.setExamResult(examResult); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setExamResult(examResult); + baseMapper.insert(ftbCultivateTaskLog); + } + } + + /** + * 保存鉴定结果状态 + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param identifyResult 鉴定结果状态(-1-未配置 0-合格 1-优秀 2-不合格) + */ + @Override + public void saveIdentifyResultState(String userId, String taskId, String phaseId, Integer identifyResult) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + ftbCultivateTaskLog.setIdentifyResult(identifyResult); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setIdentifyResult(identifyResult); + baseMapper.insert(ftbCultivateTaskLog); + } + } + + /** + * 保存用户考试ID + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param userExamId 用户考试ID + */ + @Override + public void saveUserExamId(String userId, String taskId, String phaseId, String userExamId) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + ftbCultivateTaskLog.setUserExamId(userExamId); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setUserExamId(userExamId); + baseMapper.insert(ftbCultivateTaskLog); + } + } + + /** + * 保存用户鉴定ID + * + * @param userId 用户 ID + * @param taskId 任务 ID + * @param phaseId 阶段 ID + * @param userIdentifyId 用户鉴定ID + */ + @Override + public void saveUserIdentifyId(String userId, String taskId, String phaseId, String userIdentifyId) { + List taskLogList = queryTaskPhaseLogs(userId, taskId, phaseId); + if (CollUtil.isNotEmpty(taskLogList)) { + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogList.get(0); + ftbCultivateTaskLog.setUserIdentifyId(userIdentifyId); + this.updateById(ftbCultivateTaskLog); + } else { + FtbCultivateTaskLog ftbCultivateTaskLog = new FtbCultivateTaskLog(); + ftbCultivateTaskLog.setTaskId(taskId); + ftbCultivateTaskLog.setUserId(userId); + ftbCultivateTaskLog.setPhaseId(phaseId); + ftbCultivateTaskLog.setUserIdentifyId(userIdentifyId); + baseMapper.insert(ftbCultivateTaskLog); + } + } + + public List queryTaskPhaseLogs(String userId, String taskId, String phaseId) { + LambdaQueryWrapper logWrapper = new LambdaQueryWrapper<>(); + logWrapper.eq(FtbCultivateTaskLog::getTaskId, taskId) + .eq(FtbCultivateTaskLog::getUserId, userId) + .eq(FtbCultivateTaskLog::getPhaseId, phaseId) + .eq(FtbCultivateTaskLog::getEnabledMark, 0); + return baseMapper.selectList(logWrapper); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CourseGainedCommentServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CourseGainedCommentServiceImpl.java new file mode 100644 index 0000000..2f9623c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CourseGainedCommentServiceImpl.java @@ -0,0 +1,301 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.cultivate.mapper.FtbCourseGainedCommentMapper; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CourseGainedCommentService; +import jnpf.model.cultivate.po.gained.FtbCourseGainedCommentEntity; +import jnpf.model.cultivate.v2.gained.req.V2AppCommentPageListReq; +import jnpf.model.cultivate.v2.gained.vo.V2AppCourseGainedCommentVO; +import jnpf.model.cultivate.v2.gained.vo.V2SimpleCourseGainedCommentVO; +import jnpf.permission.entity.UserCopyEntity; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.StringUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +@Service +public class V2CourseGainedCommentServiceImpl implements V2CourseGainedCommentService { + + + @Autowired + private FtbCourseGainedCommentMapper ftbCourseGainedCommentMapper; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 获取列表 + * + * @param req 请求参数 + * @return 列表 + */ + @Override + public Page getList(V2AppCommentPageListReq req) { + String userId = UserProvider.getUser().getUserId(); + Page page = ftbCourseGainedCommentMapper.appCommentPage(Page.of(req.getCurrentPage(), req.getPageSize()), req); + if (CollUtil.isNotEmpty(page.getRecords())) { + List userIds = new ArrayList<>(); + for (V2AppCourseGainedCommentVO record : page.getRecords()) { + userIds.add(record.getCreatorUserId()); + } + + List simpleList = ftbCourseGainedCommentMapper.appSimpleComment(req.getGainedId()); + simpleList = buildTree(simpleList); + Map simpleMap = new HashMap<>(); + for (V2SimpleCourseGainedCommentVO record : simpleList) { + simpleMap.put(record.getId(), record); + } + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + Map userNameAndCopyForUserIds = userApiV2Util.getUserNameOnlyCopy(userIds); + for (V2AppCourseGainedCommentVO record : page.getRecords()) { + record.setReplyNumber(calNum(record.getId(), simpleMap)); + record.setReplyId(record.getId()); + record.setIsAuthor(userId.equals(record.getCreatorUserId()) ? 1 : 0); + UserBoundVO userBoundVO = userPrimaryBoundBatch.get(record.getCreatorUserId()); + if (userBoundVO != null) { + record.setOrganizeName(userBoundVO.getOrganizeName()); + record.setPositionName(userBoundVO.getPositionName()); + record.setUserName(userBoundVO.getUserName()); + record.setHeadIcon(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + } else { + UserCopyEntity userEntity = userNameAndCopyForUserIds.get(record.getCreatorUserId()); + if (userEntity != null) { + record.setUserName(userEntity.getRealName()); + if (StringUtil.isNotEmpty(userEntity.getHeadIcon())) { + record.setHeadIcon(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + } + } + } + } + } + return page; + } + + /** + * 获取评论列表 + * + * @param firstCommentId 一级评论 + * @return 列表 + */ + @Override + public List listReply(String firstCommentId) { + String userId = UserProvider.getUser().getUserId(); + FtbCourseGainedCommentEntity commentEntity = ftbCourseGainedCommentMapper.selectById(firstCommentId); + if (commentEntity == null) { + return new ArrayList<>(); + } + + List ftbCourseGainedCommentEntities = ftbCourseGainedCommentMapper.queryNextAllComment(firstCommentId); + if (CollUtil.isEmpty(ftbCourseGainedCommentEntities)) { + return new ArrayList<>(); + } + List userIds = ftbCourseGainedCommentEntities.stream().map(FtbCourseGainedCommentEntity::getCreatorUserId).collect(Collectors.toList()); + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + Map userNameAndCopyForUserIds = userApiV2Util.getUserNameOnlyCopy(userIds); + Map listMap = ftbCourseGainedCommentEntities.stream().collect(Collectors.toMap(FtbCourseGainedCommentEntity::getId, v -> v)); + FtbCourseGainedCommentEntity ftbCourseGainedCommentEntity1 = ftbCourseGainedCommentMapper.selectById(firstCommentId);//查一级主要显示二级的名称 + if(ftbCourseGainedCommentEntity1!=null){ + listMap.put(ftbCourseGainedCommentEntity1.getId(), ftbCourseGainedCommentEntity1); + } + List appCourseGainedCommentVOList = new ArrayList<>(); + for (FtbCourseGainedCommentEntity ftbCourseGainedCommentEntity : ftbCourseGainedCommentEntities) { + V2AppCourseGainedCommentVO appCourseGainedCommentVO = new V2AppCourseGainedCommentVO(); + appCourseGainedCommentVO.setId(ftbCourseGainedCommentEntity.getId()); + if (StringUtils.isNotEmpty(ftbCourseGainedCommentEntity.getParentId())) { + FtbCourseGainedCommentEntity parent = listMap.get(ftbCourseGainedCommentEntity.getParentId()); + if (parent != null) { + appCourseGainedCommentVO.setReplyUserId(parent.getCreatorUserId()); + appCourseGainedCommentVO.setReplyUserName(parent.getRealName()); + } + } else { + appCourseGainedCommentVO.setAccount(ftbCourseGainedCommentEntity.getAccount()); + appCourseGainedCommentVO.setRealName(ftbCourseGainedCommentEntity.getRealName()); + } + appCourseGainedCommentVO.setContent(ftbCourseGainedCommentEntity.getContent()); + appCourseGainedCommentVO.setCreatorUserId(ftbCourseGainedCommentEntity.getCreatorUserId()); + appCourseGainedCommentVO.setCreatorTime(ftbCourseGainedCommentEntity.getCreatorTime()); + appCourseGainedCommentVO.setIsAuthor(userId.equals(ftbCourseGainedCommentEntity.getCreatorUserId()) ? 1 : 0); + UserBoundVO userBoundVO = userPrimaryBoundBatch.get(ftbCourseGainedCommentEntity.getCreatorUserId()); + + if (userBoundVO != null) { + appCourseGainedCommentVO.setOrganizeName(userBoundVO.getOrganizeName()); + appCourseGainedCommentVO.setPositionName(userBoundVO.getPositionName()); + appCourseGainedCommentVO.setUserName(userBoundVO.getUserName()); + appCourseGainedCommentVO.setHeadIcon(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + } else { + UserCopyEntity userEntity = userNameAndCopyForUserIds.get(ftbCourseGainedCommentEntity.getCreatorUserId()); + if (userEntity != null) { + appCourseGainedCommentVO.setUserName(userEntity.getRealName()); + if (StringUtil.isNotEmpty(userEntity.getHeadIcon())) { + appCourseGainedCommentVO.setHeadIcon(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + } + } + } + + appCourseGainedCommentVOList.add(appCourseGainedCommentVO); + } + + + return appCourseGainedCommentVOList; + } + + @Override + public Boolean dealOldData(String tenantId) { + + // 查询该租户下所有评论数据 + List allComments = ftbCourseGainedCommentMapper.selectList(new QueryWrapper<>()); + + if (CollUtil.isEmpty(allComments)) { + return true; + } + + // 按 gainedId 分组处理 + Map> groupedByGainedId = allComments.stream() + .collect(Collectors.groupingBy(FtbCourseGainedCommentEntity::getGainedId)); + + for (Map.Entry> entry : groupedByGainedId.entrySet()) { + List comments = entry.getValue(); + + // 构建简单 VO 列表用于建树 + List simpleList = new ArrayList<>(); + for (FtbCourseGainedCommentEntity comment : comments) { + V2SimpleCourseGainedCommentVO vo = new V2SimpleCourseGainedCommentVO(); + vo.setId(comment.getId()); + vo.setParentId(comment.getParentId()); + simpleList.add(vo); + } + + // 构建树形结构 + List treeList = buildTree(simpleList); + + // 递归生成路径并更新 + generatePathAndUpdate(treeList, ""); + } + + return true; + } + + /** + * 递归生成路径并更新数据库 + * + * @param nodeList 节点列表 + * @param parentPath 父级路径 + */ + private void generatePathAndUpdate(List nodeList, String parentPath) { + if (CollUtil.isEmpty(nodeList)) { + return; + } + + for (V2SimpleCourseGainedCommentVO node : nodeList) { + String currentPath = ""; + if (StringUtil.isNotEmpty(node.getParentId())) { + currentPath = parentPath + node.getParentId() + "/"; + } + // 生成当前节点的路径:父路径 + 当前索引(从 1 开始) + + + // 更新数据库 + FtbCourseGainedCommentEntity updateEntity = new FtbCourseGainedCommentEntity(); + updateEntity.setId(node.getId()); + updateEntity.setPath(currentPath); + ftbCourseGainedCommentMapper.updateById(updateEntity); + + // 递归处理子节点 + if (CollUtil.isNotEmpty(node.getChildren())) { + generatePathAndUpdate(node.getChildren(), currentPath); + } + } + } + + private Long calNum(String id, Map simpleMap) { + Long num = 0L; + V2SimpleCourseGainedCommentVO vo = simpleMap.get(id); + if (vo == null) { + return 0L; + } + if (CollUtil.isEmpty(vo.getChildren())) { + return 0L; + } + return calRealNum(vo.getChildren()); + } + + private Long calRealNum(List vo) { + Long num = 0L; + if (CollUtil.isEmpty(vo)) { + return num; + } + for (V2SimpleCourseGainedCommentVO v2SimpleCourseGainedCommentVO : vo) { + num += 1; + if (CollUtil.isNotEmpty(v2SimpleCourseGainedCommentVO.getChildren())) { + num += calRealNum(v2SimpleCourseGainedCommentVO.getChildren()); + } + + } + return num; + } + + private void calRealIds(List vo, List ids) { + if (CollUtil.isEmpty(vo)) { + return; + } + for (V2SimpleCourseGainedCommentVO v2SimpleCourseGainedCommentVO : vo) { + ids.add(v2SimpleCourseGainedCommentVO.getId()); + if (CollUtil.isNotEmpty(v2SimpleCourseGainedCommentVO.getChildren())) { + calRealIds(v2SimpleCourseGainedCommentVO.getChildren(), ids); + } + } + } + + + /** + * 构建树形结构 + * + * @param list 扁平列表 + * @return 树形结构列表 + */ + private List buildTree(List list) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + + // 1. 创建 Map 用于快速查找节点 + Map nodeMap = new HashMap<>(); + for (V2SimpleCourseGainedCommentVO node : list) { + nodeMap.put(node.getId(), node); + } + + // 2. 构建树结构 + List rootNodes = new ArrayList<>(); + for (V2SimpleCourseGainedCommentVO node : list) { + String parentId = node.getParentId(); + if (parentId == null || "0".equals(parentId) || "-1".equals(parentId)) { + // 根节点直接加入结果集 + rootNodes.add(node); + } else { + // 查找父节点并添加当前节点为其子节点 + V2SimpleCourseGainedCommentVO parent = nodeMap.get(parentId); + if (parent != null) { + if (parent.getChildren() == null) { + parent.setChildren(new ArrayList<>()); + } + parent.getChildren().add(node); + } + } + } + + return rootNodes; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateBatchQueryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateBatchQueryServiceImpl.java new file mode 100644 index 0000000..c29f478 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateBatchQueryServiceImpl.java @@ -0,0 +1,1026 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.CultivateIdentifyTableService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourceChapterLearningService; +import jnpf.cultivate.v2.service.V2CultivateBatchQueryService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.*; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterVo; +import jnpf.model.cultivate.v2.position.req.FtbCultivatePositionCoursePracticeReq; +import jnpf.model.cultivate.v2.position.req.FtbCultivatePositionCourseReq; +import jnpf.model.cultivate.v2.position.req.FtbCultivatePositionSaveReq; +import jnpf.model.cultivate.v2.promotion.vo.PositionProgressVo; +import jnpf.model.cultivate.v2.statistics.V2BatchQueryResult; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskSaveReq; +import jnpf.model.cultivate.v2.task.vo.V2CultivateLearnTaskCourseVo; +import jnpf.model.cultivate.v2.task.vo.V2CultivateLearnTaskPhaseVo; +import jnpf.model.cultivate.v2.task.vo.V2CultivateLearnTaskPracticeVo; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +/** + * V2培养模块批量查询服务实现类 + * 提供根据ID列表批量查询课程、考试、鉴定、证书、练习技能信息的功能 + */ +@Service +public class V2CultivateBatchQueryServiceImpl implements V2CultivateBatchQueryService { + + + @Autowired + private CultivateIdentifyTableService cultivateIdentifyTableService; + + @Autowired + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Autowired + private CultivateIdentifyApplyMapper cultivateIdentifyApplyMapper; + + @Autowired + private FtbCultivatePositionCourceChapterLearningService chapterLearningService; + + @Autowired + private FtbCultivateCertificateUserMapper ftbCertificateUserMapper; + + @Autowired + private TeachingRecordMapper teachingRecordMapper; + + @Autowired + private FtbCultivatePositionCourseMapper ftbCultivatePositionCourseMapper; + + @Autowired + private FtbCultivatePositionCourseExamMapper ftbCultivatePositionCourseExamMapper; + + @Autowired + private FtbCultivatePositionCourseIdentityMapper ftbCultivatePositionCourseIdentityMapper; + + @Autowired + private FtbCultivatePositionCoursePracticeMapper ftbCultivatePositionCoursePracticeMapper; + + @Autowired + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Autowired + private TeachingSkillMapper teachingSkillMapper; + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + + @Autowired + private FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + + @Autowired + private FtbCultivateExamMapper ftbCultivateExamMapper; + + @Autowired + private FtbCultivateCertificateMapper ftbCultivateCertificateMapper; + + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + @Autowired + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + private FtbCultivatePositionCourceChapterLearningMapper ftbCultivatePositionCourceChapterLearningMapper; + + /** + * 根据课程ID列表批量查询课程信息 + * + * @param courseIds 课程ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 以课程ID为键,课程对象为值的映射表 + */ + @Override + public Map batchQueryCoursesByIds(List courseIds, Integer isSelectNoDelete) { + if (CollUtil.isEmpty(courseIds)) { + return new HashMap<>(); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbCultivateCourse::getId, courseIds) + .eq(FtbCultivateCourse::getIsGroundIng, 1) + .eq(isSelectNoDelete != null, FtbCultivateCourse::getEnableMark, isSelectNoDelete); // 只查询有效的课程 + + List courses = ftbCultivateCourseMapper.selectList(queryWrapper); + return courses.stream().collect(Collectors.toMap(FtbCultivateCourse::getId, item -> item)); + } + + + /** + * 根据考试ID列表批量查询考试信息 + * + * @param examIds 考试ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 以考试ID为键,考试对象为值的映射表 + */ + @Override + public Map batchQueryExamsByIds(List examIds, Integer isSelectNoDelete) { + if (CollUtil.isEmpty(examIds)) { + return new HashMap<>(); + } + if (isSelectNoDelete != null) {//考试和其他不一样 由于历史原因 + if (isSelectNoDelete == 0) { + isSelectNoDelete = 1; + } else { + isSelectNoDelete = 0; + } + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbCultivateExam::getId, examIds) + .eq(isSelectNoDelete != null, FtbCultivateExam::getEnabledMark, isSelectNoDelete); // 只查询有效的考试 + + List exams = ftbCultivateExamMapper.selectList(queryWrapper); + return exams.stream().collect(Collectors.toMap(FtbCultivateExam::getId, item -> item)); + } + + + /** + * 根据鉴定ID列表批量查询鉴定信息 + * + * @param identityIds 鉴定ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 以鉴定ID为键,鉴定对象为值的映射表 + */ + @Override + public Map batchQueryIdentificationsByIds(List identityIds, Integer isSelectNoDelete) { + if (CollUtil.isEmpty(identityIds)) { + return new HashMap<>(); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(CultivateIdentifyTable::getId, identityIds) + .eq(isSelectNoDelete != null, CultivateIdentifyTable::getDeleteMark, isSelectNoDelete);//0-有效 + + List identities = cultivateIdentifyTableService.list(queryWrapper); + return identities.stream().collect(Collectors.toMap(CultivateIdentifyTable::getId, item -> item)); + } + + + /** + * 根据证书ID列表批量查询证书信息 + * + * @param certificateIds 证书ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 以证书ID为键,证书对象为值的映射表 + */ + @Override + public Map batchQueryCertificatesByIds(List certificateIds, Integer isSelectNoDelete) { + if (CollUtil.isEmpty(certificateIds)) { + return new HashMap<>(); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbCertificateEntity::getId, certificateIds) + .eq(FtbCertificateEntity::getStatus, 1); + if (isSelectNoDelete != null) { + if (isSelectNoDelete == 0) { + queryWrapper.eq(FtbCertificateEntity::getEnabledMark, 1); // 只查询有效的证书 + } else { + queryWrapper.eq(FtbCertificateEntity::getEnabledMark, 0); // 只查询有效的证书 + } + } + + + List certificates = ftbCultivateCertificateMapper.selectList(queryWrapper); + return certificates.stream().collect(Collectors.toMap(FtbCertificateEntity::getId, item -> item)); + } + + + /** + * 根据技能ID列表批量查询技能信息 + * + * @param practiceSkillIds 技能ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 以技能ID为键,技能对象为值的映射表 + */ + @Override + public Map batchQuerySkillsByIds(List practiceSkillIds, Integer isSelectNoDelete) { + if (CollUtil.isEmpty(practiceSkillIds)) { + return new HashMap<>(); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(TeachingSkill::getId, practiceSkillIds) + .eq(isSelectNoDelete != null, TeachingSkill::getDeleteMark, isSelectNoDelete);//0-有效 1-删除 + + List skills = teachingSkillMapper.selectList(queryWrapper); + return skills.stream().collect(Collectors.toMap(TeachingSkill::getId, item -> item)); + } + + /** + * 批量查询所有信息 + * + * @param courseIds 课程ID列表 + * @param examIds 考试ID列表 + * @param identityIds 鉴定ID列表 + * @param certificateIds 证书ID列表 + * @param practiceSkillIds 技能ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @return 批量查询结果 + */ + @Override + public V2BatchQueryResult batchQueryAllByIds(List courseIds, List examIds, List identityIds, List certificateIds, List practiceSkillIds, Integer isSelectNoDelete) { + + // 并行执行批量查询,提高性能 + Map courseMap = batchQueryCoursesByIds(courseIds, isSelectNoDelete); + Map examMap = batchQueryExamsByIds(examIds, isSelectNoDelete); + Map identityMap = batchQueryIdentificationsByIds(identityIds, isSelectNoDelete); + Map certificateMap = batchQueryCertificatesByIds(certificateIds, isSelectNoDelete); + Map skillMap = batchQuerySkillsByIds(practiceSkillIds, isSelectNoDelete); + + return new V2BatchQueryResult(courseMap, examMap, identityMap, certificateMap, skillMap); + } + + + /** + * 批量查询用户在指定课程中的学习状态 + * + * @param userId 用户ID + * @param courseIds 课程ID列表 + * @return 以课程ID为键,用户学习记录为值的映射表 + */ + @Override + public Map batchQueryUserCourseLearnStatus(String userId, List courseIds) { + if (CollUtil.isEmpty(courseIds)) { + return new HashMap<>(); + } + + // 构建查询条件,查询用户在指定课程中的学习情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePositionCourceLearning::getId, + FtbCultivatePositionCourceLearning::getUserId, + FtbCultivatePositionCourceLearning::getCourceId, + FtbCultivatePositionCourceLearning::getState, + FtbCultivatePositionCourceLearning::getLearnTime) + .eq(FtbCultivatePositionCourceLearning::getUserId, userId) + .in(FtbCultivatePositionCourceLearning::getCourceId, courseIds) + .eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); // 有效标志为0表示有效 + + List courseLearnings = ftbCultivatePositionCourceLearningMapper.selectList(queryWrapper); + + // 构建key为userId-courseId的映射 + Map resultMap = new HashMap<>(); + for (FtbCultivatePositionCourceLearning learning : courseLearnings) { + resultMap.put(learning.getCourceId(), learning); + } + + return resultMap; + } + + + /** + * 批量查询用户在指定考试中的状态 + * + * @param userId 用户ID + * @param examIds 考试ID列表 + * @return 以考试ID为键,用户考试记录为值的映射表 + */ + @Override + public Map batchQueryUserExamStatus(String userId, List examIds) { + if (CollUtil.isEmpty(examIds)) { + return new HashMap<>(); + } + + // 构建查询条件,查询用户在指定考试中的情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivateExamUser::getId, + FtbCultivateExamUser::getExamId, + FtbCultivateExamUser::getStatus, + FtbCultivateExamUser::getScore, + FtbCultivateExamUser::getRanks, + FtbCultivateExamUser::getDuration, + FtbCultivateExamUser::getFinishTime) + .eq(FtbCultivateExamUser::getUserId, userId) + .in(FtbCultivateExamUser::getExamId, examIds) + .eq(FtbCultivateExamUser::getEnabledMark, 1); // 有效标志为1表示有效 + + List examUsers = ftbCultivateExamUserMapper.selectList(queryWrapper); + + // 构建key为userId-examId的映射 + Map resultMap = new HashMap<>(); + for (FtbCultivateExamUser examUser : examUsers) { + resultMap.put(examUser.getExamId(), examUser); + } + + return resultMap; + } + + + /** + * 批量查询用户在指定鉴定中的状态 + * + * @param userId 用户ID + * @param identifyIds 鉴定ID列表 + * @return 以鉴定ID为键,用户鉴定记录为值的映射表 + */ + @Override + public Map batchQueryUserIdentificationStatus(String userId, List identifyIds) { + if (CollUtil.isEmpty(identifyIds)) { + return new HashMap<>(); + } + + // 构建查询条件,查询用户在指定鉴定中的情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(CultivateIdentifyApply::getTableId, + CultivateIdentifyApply::getName, + CultivateIdentifyApply::getBeIdentifyUserId, + CultivateIdentifyApply::getIdentifyUserId, + CultivateIdentifyApply::getResult, + CultivateIdentifyApply::getStatus) + .eq(CultivateIdentifyApply::getBeIdentifyUserId, userId) + .in(CultivateIdentifyApply::getTableId, identifyIds) + .eq(CultivateIdentifyApply::getDeleteMark, 0); // 0是否删除标记 + + List identifies = cultivateIdentifyApplyService.list(queryWrapper); + + // 构建key为userId-identifyId的映射 + Map resultMap = new HashMap<>(); + for (CultivateIdentifyApply identify : identifies) { + resultMap.put(identify.getTableId(), identify); + } + + return resultMap; + } + + + @Override + public Map batchQueryUserIdentificationStatus(String userId, List identifyIds, Integer source, String sourceId) { + if (CollUtil.isEmpty(identifyIds)) { + return new HashMap<>(); + } + + // 构建查询条件,查询用户在指定鉴定中的情况 + List identifies = cultivateIdentifyApplyService.queryIdentifyApply(userId, identifyIds, source, sourceId); + // 构建key为userId-identifyId的映射 + Map resultMap = new HashMap<>(); + for (CultivateIdentifyApply identify : identifies) { + resultMap.put(identify.getTableId(), identify); + } + + return resultMap; + } + + + /** + * 批量查询用户在指定证书中的状态 + * + * @param userId 用户ID + * @param certificateIds 证书ID列表 + * @return 以证书ID为键,用户证书记录为值的映射表 + */ + @Override + public Map batchQueryUserCertificateStatus(String userId, List certificateIds) { + if (CollUtil.isEmpty(certificateIds)) { + return new HashMap<>(); + } + + // 构建查询条件,查询用户在指定证书中的情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCertificateUserEntity::getId, + FtbCertificateUserEntity::getCertificateId, + FtbCertificateUserEntity::getUserId, + FtbCertificateUserEntity::getStatus, + FtbCertificateUserEntity::getReason, + FtbCertificateUserEntity::getNumber, + FtbCertificateUserEntity::getExpireTime, + FtbCertificateUserEntity::getEffectTime) + .eq(FtbCertificateUserEntity::getUserId, userId) + .in(FtbCertificateUserEntity::getCertificateId, certificateIds) + .eq(FtbCertificateUserEntity::getEnabledMark, 1); // 有效标志为1表示有效 + + List certificates = ftbCertificateUserMapper.selectList(queryWrapper); + + // 构建key为certificateId的映射 + Map resultMap = new HashMap<>(); + for (FtbCertificateUserEntity certificate : certificates) { + resultMap.put(certificate.getCertificateId(), certificate); + } + + return resultMap; + } + + + /** + * 批量查询用户在指定技能中的状态(技能点数) + * + * @param userId 用户ID + * @param skillIds 技能ID列表 + * @return 以技能ID为键,用户技能点数为值的映射表 + */ + @Override + public Map> batchQueryUserSkillStatus(String userId, List skillIds) { + if (CollUtil.isEmpty(skillIds)) { + return new HashMap<>(); + } + // 构建查询条件,查询用户在指定技能中的情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(TeachingRecord::getId, + TeachingRecord::getSkillId, + TeachingRecord::getUserId, + TeachingRecord::getType) + .eq(TeachingRecord::getUserId, userId) + .in(TeachingRecord::getSkillId, skillIds) + .eq(TeachingRecord::getType, 2) + .eq(TeachingRecord::getDeleteMark, 0) + .orderByDesc(TeachingRecord::getCreatorTime); + + // 查询并直接分组处理 + return teachingRecordMapper.selectList(queryWrapper).stream().collect(Collectors.groupingBy(TeachingRecord::getSkillId, Collectors.mapping(TeachingRecord::getId, Collectors.toList()))); + } + + /** + * 检查一下 课程考试 鉴定 技能 证书是否存在且未删除 + * + * @param req + */ + @Override + public void taskCheckAvailable(V2CultivateTaskSaveReq req) { + if (CollUtil.isEmpty(req.getPhase())) { + return; + } + // 收集所有需要检查的ID + List courseIds = new ArrayList<>(); + List examIds = new ArrayList<>(); + List identityIds = new ArrayList<>(); + List practiceIds = new ArrayList<>(); + List certificateIds = new ArrayList<>(); + + for (V2CultivateLearnTaskPhaseVo phase : req.getPhase()) { + // 检查课程 + if (phase.getCourseList() != null) { + for (V2CultivateLearnTaskCourseVo course : phase.getCourseList()) { + if (course.getCourseId() != null) { + courseIds.add(course.getCourseId()); + } + } + } + + // 检查考试 + if (phase.getExam() != null && phase.getExam().getExamId() != null) { + examIds.add(phase.getExam().getExamId()); + } + + // 检查鉴定 + if (phase.getIdentification() != null && phase.getIdentification().getIdentityId() != null) { + identityIds.add(phase.getIdentification().getIdentityId()); + } + + // 检查练习/技能 + if (phase.getPracticeList() != null) { + for (V2CultivateLearnTaskPracticeVo practice : phase.getPracticeList()) { + if (practice.getBusinessId() != null) { + practiceIds.add(practice.getBusinessId()); + } + } + } + + // 检查证书 + if (phase.getCertificate() != null && phase.getCertificate().getCertificateId() != null) { + certificateIds.add(phase.getCertificate().getCertificateId()); + } + } + + // 批量查询各类实体并验证是否存在及有效性 + checkCourse(courseIds); + checkExam(examIds); + checkIdentity(identityIds); + checkPractice(practiceIds); + checkCertificate(certificateIds); + } + + private void checkPractice(List practiceIds) { + Map practiceMap; + if (CollUtil.isNotEmpty(practiceIds)) { + List practices = teachingSkillMapper.selectBatchIds(practiceIds); + practiceMap = practices.stream().collect(Collectors.toMap(TeachingSkill::getId, item -> item)); + for (String practiceId : practiceIds) { + TeachingSkill practice = practiceMap.get(practiceId); + if (practice == null) { + throw new RuntimeException("技能【" + practiceId + "】不存在或已被删除"); + } + } + } + } + + private void checkCertificate(List certificateIds) { + Map certificateMap; + if (CollUtil.isNotEmpty(certificateIds)) { + List certificates = ftbCultivateCertificateMapper.selectBatchIds(certificateIds); + certificateMap = certificates.stream().collect(Collectors.toMap(FtbCertificateEntity::getId, item -> item)); + for (String certificateId : certificateIds) { + FtbCertificateEntity certificate = certificateMap.get(certificateId); + if (certificate == null || certificate.getEnabledMark() == null || certificate.getEnabledMark() == 0) { + throw new RuntimeException("证书【" + (certificate != null ? certificate.getName() : certificateId) + "】不存在或已被删除"); + } + } + } + } + + private void checkIdentity(List identityIds) { + Map identityMap; + if (CollUtil.isNotEmpty(identityIds)) { + List identities = cultivateIdentifyTableService.listByIds(identityIds); + identityMap = identities.stream().collect(Collectors.toMap(CultivateIdentifyTable::getId, item -> item)); + for (String identityId : identityIds) { + CultivateIdentifyTable identity = identityMap.get(identityId); + if (identity == null) { + throw new RuntimeException("鉴定【" + identityId + "】不存在或已被删除"); + } + } + } + } + + private void checkExam(List examIds) { + Map examMap; + if (CollUtil.isNotEmpty(examIds)) { + List exams = ftbCultivateExamMapper.selectBatchIds(examIds); + examMap = exams.stream().collect(Collectors.toMap(FtbCultivateExam::getId, item -> item)); + for (String examId : examIds) { + FtbCultivateExam exam = examMap.get(examId); + if (exam == null || exam.getEnabledMark() == 0) { + throw new RuntimeException("考试【" + (exam != null ? exam.getExamName() : examId) + "】不存在或已被删除"); + } + } + } + } + + private void checkCourse(List courseIds) { + Map courseMap; + if (CollUtil.isNotEmpty(courseIds)) { + List courses = ftbCultivateCourseMapper.selectBatchIds(courseIds); + courseMap = courses.stream().collect(Collectors.toMap(FtbCultivateCourse::getId, item -> item)); + for (String courseId : courseIds) { + FtbCultivateCourse course = courseMap.get(courseId); + if (course == null || course.getEnableMark() == 1 || course.getIsGroundIng() == 0) { + throw new RuntimeException("课程【" + (course != null ? course.getName() : courseId) + "】不存在或已被删除/下架"); + } + } + } + } + + @Override + public Map batchQueryCourseChapterLearningState(String userId, List chapterList) { + + Map chapterLearningMap = new HashMap<>(); + LambdaQueryWrapper queryChapterWrapper = Wrappers.lambdaQuery(); + queryChapterWrapper.in(FtbCultivatePositionCourceChapterLearning::getChapterId, chapterList) + .eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId) + .eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List list = chapterLearningService.list(queryChapterWrapper); + if (CollUtil.isEmpty(list)) { + return chapterLearningMap; + } + return list.stream().collect(Collectors.toMap( + FtbCultivatePositionCourceChapterLearning::getChapterId, + item -> item, + (existing, replacement) -> replacement // 重复时直接覆盖,保留最后一条 + )); + } + + + /** + * 检查入参的 课程、考试、鉴定、实践/练习 是否已经删除和下架 + * + * @param ftbCultivatePosition 岗位学习对象 + */ + @Override + public void positionCheckAvailable(FtbCultivatePositionSaveReq ftbCultivatePosition) { + if (ftbCultivatePosition.getCourseList() == null || ftbCultivatePosition.getCourseList().isEmpty()) { + return; + } + // 收集所有需要检查的ID + List courseIds = new ArrayList<>(); + List examIds = new ArrayList<>(); + List identityIds = new ArrayList<>(); + List practiceIds = new ArrayList<>(); + List certificateIds = new ArrayList<>(); + + for (FtbCultivatePositionCourseReq courseReq : ftbCultivatePosition.getCourseList()) { + // 收集课程ID + if (courseReq.getCourseId() != null) { + courseIds.add(courseReq.getCourseId()); + } + + // 收集考试ID + if (courseReq.getExam() != null && courseReq.getExam().getExamId() != null) { + examIds.add(courseReq.getExam().getExamId()); + } + + // 收集鉴定ID + if (courseReq.getIdentity() != null && courseReq.getIdentity().getIdentityId() != null) { + identityIds.add(courseReq.getIdentity().getIdentityId()); + } + + // 收集练习/实践ID + if (courseReq.getPracticeList() != null) { + for (FtbCultivatePositionCoursePracticeReq practiceReq : courseReq.getPracticeList()) { + if (practiceReq.getBusinessId() != null) { + practiceIds.add(practiceReq.getBusinessId()); + } + } + } + + // 收集证书ID + if (courseReq.getCertificate() != null && courseReq.getCertificate().getCertificateId() != null) { + certificateIds.add(courseReq.getCertificate().getCertificateId()); + } + } + + // 批量查询各类实体 + Map courseMap; + checkCourse(courseIds); + + Map examMap; + checkExam(examIds); + + Map identityMap; + if (CollUtil.isNotEmpty(identityIds)) { + List identities = cultivateIdentifyTableService.listByIds(identityIds); + identityMap = identities.stream().collect(Collectors.toMap(CultivateIdentifyTable::getId, item -> item)); + for (String identityId : identityIds) { + CultivateIdentifyTable identity = identityMap.get(identityId); + if (identity == null) { + throw new RuntimeException("添加了已经删除的鉴定表"); + } + } + } + + Map practiceMap; + if (CollUtil.isNotEmpty(practiceIds)) { + List practices = teachingSkillMapper.selectBatchIds(practiceIds); + practiceMap = practices.stream().collect(Collectors.toMap(TeachingSkill::getId, item -> item)); + for (String practiceId : practiceIds) { + TeachingSkill practice = practiceMap.get(practiceId); + if (practice == null) { + throw new RuntimeException("添加了已经删除的技能"); + } + } + } + + Map certificateMap; + checkCertificate(certificateIds); + } + + /** + * 批量查询课程学习人数 + * + * @param courseIds 课程ID列表 + * @return 课程学习人数 + */ + @Override + public Map batchQueryCourseLearningCount(List courseIds) { + if (CollUtil.isEmpty(courseIds)) { + return new HashMap<>(); + } + + // 按课程ID分组并统计学习人数 + List groupedResult = ftbCultivatePositionCourceLearningMapper.countForCourseIds(courseIds); + + // 将结果转换为Map,按课程ID作为键,统计数量作为值 + Map courseLearningCountMap = new HashMap<>(); + if (CollUtil.isNotEmpty(groupedResult)) { + courseLearningCountMap = groupedResult.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, obj -> (long) obj.getNum(), (existing, replacement) -> existing // 如果有重复key,保留第一个值 + )); + } + + return courseLearningCountMap; + } + + /** + * 批量查询岗位学习进度 + * + * @param postList 岗位列表 + * @param userId 用户ID + * @return 岗位学习进度 + */ + @Override + public Map batchQueryPositionProgress(List postList, String userId) { + Set postIds = new HashSet<>(); + for (FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew : postList) { + postIds.add(ftbCultivatePromotionPostNew.getPostId()); + } + Map result = new HashMap<>(); + + if (CollUtil.isEmpty(postIds)) { + return result; + } + + // 1. 查询岗位学习信息,获取岗位学习ID列表 + LambdaQueryWrapper positionQueryWrapper = Wrappers.lambdaQuery(); + positionQueryWrapper.select(FtbCultivatePosition::getId, FtbCultivatePosition::getPostId) + .in(FtbCultivatePosition::getPostId, postIds) + .eq(FtbCultivatePosition::getEnabledMark, 0); // 只查询有效的岗位 + List positions = ftbCultivatePositionMapper.selectList(positionQueryWrapper); + + if (CollUtil.isEmpty(positions)) { + return result; + } + + // 获取岗位学习ID列表 + List positionLearnIds = positions.stream().map(FtbCultivatePosition::getId).collect(Collectors.toList()); + + LambdaQueryWrapper queryCourseWrapper = Wrappers.lambdaQuery(); + queryCourseWrapper.in(FtbCultivatePositionCourse::getPostLearnId, positionLearnIds); + queryCourseWrapper.eq(FtbCultivatePositionCourse::getCompulsory, 0); + queryCourseWrapper.eq(FtbCultivatePositionCourse::getEnabledMark, 0); + List courseList = ftbCultivatePositionCourseMapper.selectList(queryCourseWrapper); + // 按照 postRankId 和 gradeId 字符串连接一次分组,gradeId为null时用"NULL"标识 + Map> courseGroupMap = courseList.stream().collect(Collectors.groupingBy(item -> item.getPostRankId() + (item.getGradeId() == null ? "" : item.getGradeId()))); + + LambdaQueryWrapper queryCourseExamWrapper = Wrappers.lambdaQuery(); + queryCourseExamWrapper.in(FtbCultivatePositionCourseExam::getPostLearnId, positionLearnIds); + queryCourseExamWrapper.eq(FtbCultivatePositionCourseExam::getEnabledMark, 0); + List exams = ftbCultivatePositionCourseExamMapper.selectList(queryCourseExamWrapper); + // 按照 postRankId 和 gradeId 字符串连接一次分组,gradeId为null时用"NULL"标识 + Map> examGroupMap = exams.stream().collect(Collectors.groupingBy(item -> item.getPostRankId() + (item.getGradeId() == null ? "" : item.getGradeId()))); + + LambdaQueryWrapper queryCourseIdentityWrapper = Wrappers.lambdaQuery(); + queryCourseIdentityWrapper.in(FtbCultivatePositionCourseIdentity::getPostLearnId, positionLearnIds); + queryCourseIdentityWrapper.eq(FtbCultivatePositionCourseIdentity::getEnabledMark, 0); + List identities = ftbCultivatePositionCourseIdentityMapper.selectList(queryCourseIdentityWrapper); + // 按照 postRankId 和 gradeId 字符串连接一次分组,gradeId为null时用"NULL"标识 + Map> identityGroupMap = identities.stream().collect(Collectors.groupingBy(item -> item.getPostRankId() + (item.getGradeId() == null ? "" : item.getGradeId()))); + + LambdaQueryWrapper queryCoursePracticesWrapper = Wrappers.lambdaQuery(); + queryCoursePracticesWrapper.in(FtbCultivatePositionCoursePractice::getPostLearnId, positionLearnIds); + queryCoursePracticesWrapper.eq(FtbCultivatePositionCoursePractice::getEnabledMark, 0); + List practices = ftbCultivatePositionCoursePracticeMapper.selectList(queryCoursePracticesWrapper); + // 按照 postRankId 和 gradeId 字符串连接一次分组,gradeId为null时用"NULL"标识 + Map> practiceGroupMap = practices.stream().collect(Collectors.groupingBy(item -> item.getPostRankId() + (item.getGradeId() == null ? "" : item.getGradeId()))); + + for (FtbCultivatePromotionPostNew postNew : postList) { + PositionProgressVo vo = new PositionProgressVo(); + String key = postNew.getPostId() + (postNew.getGradeId() == null ? "" : postNew.getGradeId()); + + List itemCourseLists = courseGroupMap.get(key); + if (CollUtil.isEmpty(itemCourseLists)) { + itemCourseLists = new ArrayList<>(); + } + List itemCourseExamList = examGroupMap.get(key); + if (CollUtil.isEmpty(itemCourseExamList)) { + itemCourseExamList = new ArrayList<>(); + } + List itemCourseIdentityList = identityGroupMap.get(key); + if (CollUtil.isEmpty(itemCourseIdentityList)) { + itemCourseIdentityList = new ArrayList<>(); + } + List itemCoursePracticeList = practiceGroupMap.get(key); + if (CollUtil.isEmpty(itemCoursePracticeList)) { + itemCoursePracticeList = new ArrayList<>(); + } + Set courseIds = itemCourseLists.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toSet()); + Set courseExamIds = itemCourseExamList.stream().map(FtbCultivatePositionCourseExam::getExamId).collect(Collectors.toSet()); + Set courseIdentityIds = itemCourseIdentityList.stream().map(FtbCultivatePositionCourseIdentity::getIdentityId).collect(Collectors.toSet()); + Set coursePracticeIds = itemCoursePracticeList.stream().map(FtbCultivatePositionCoursePractice::getBusinessId).collect(Collectors.toSet()); + + vo.setTotal(courseIds.size() + courseExamIds.size() + courseIdentityIds.size() + coursePracticeIds.size()); + vo.setCourseIds(courseIds); + vo.setCourseExamIds(courseExamIds); + vo.setCourseIdentityIds(courseIdentityIds); + vo.setCoursePracticeIds(coursePracticeIds); + //查询课程完成数量 + long courseCompleteNum = 0; + if (CollUtil.isNotEmpty(courseIds)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId) + .eq(FtbCultivatePositionCourceLearning::getState, 1) + .in(FtbCultivatePositionCourceLearning::getCourceId, courseIds) + .eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); // 有效标志为0表示有效 + + courseCompleteNum = ftbCultivatePositionCourceLearningMapper.selectCount(queryWrapper); + } + // 构建查询条件,查询用户在指定考试中的情况 + long examCompleteNum = 0; + if (CollUtil.isNotEmpty(courseExamIds)) { + LambdaQueryWrapper userExamWrapper = Wrappers.lambdaQuery(); + userExamWrapper.eq(FtbCultivateExamUser::getUserId, userId) + .ne(FtbCultivateExamUser::getStatus, List.of(0, 2)) + .in(FtbCultivateExamUser::getExamId, courseExamIds) + .eq(FtbCultivateExamUser::getEnabledMark, 1); // 有效标志为1表示有效 + examCompleteNum = ftbCultivateExamUserMapper.selectCount(userExamWrapper); + } + //查询鉴定完成数量 + long identifyCompleteNum = 0; + if (CollUtil.isNotEmpty(courseIdentityIds)) { + // 使用 ftb_cultivate_identify_apply 和 ftb_cultivate_identify_apply_table_backups 关联查询 + identifyCompleteNum = cultivateIdentifyApplyMapper.countCompletedIdentifyByPost( + userId, + courseIdentityIds, + 2, // source: 岗位学习鉴定 + itemCourseLists.get(0).getPostLearnId() // sourceId: 岗位ID + ); + } + //查询练习完成数量 + long practiceNum = 0; + if (CollUtil.isNotEmpty(coursePracticeIds)) { + LambdaQueryWrapper queryTeachingWrapper = Wrappers.lambdaQuery(); + queryTeachingWrapper + .eq(TeachingRecord::getUserId, userId) + .in(TeachingRecord::getSkillId, coursePracticeIds) + .eq(TeachingRecord::getType, 2) + .eq(TeachingRecord::getDeleteMark, 0) + .orderByDesc(TeachingRecord::getCreatorTime); + practiceNum = teachingRecordMapper.selectCount(queryTeachingWrapper); + } + vo.setComplete((int) (courseCompleteNum + examCompleteNum + practiceNum + identifyCompleteNum)); + result.put(postNew.getPostId(), vo); + } + return result; + } + + private List batchQueryCourseChapters(List courseIds) { + LambdaQueryWrapper courseChapterWrapper = Wrappers.lambdaQuery(); + courseChapterWrapper.select(FtbCultivateCourseChapter::getId, + FtbCultivateCourseChapter::getCourseId, + FtbCultivateCourseChapter::getName, + FtbCultivateCourseChapter::getParentId, + FtbCultivateCourseChapter::getPath, + FtbCultivateCourseChapter::getSortCode) + .in(FtbCultivateCourseChapter::getCourseId, courseIds) + .eq(FtbCultivateCourseChapter::getEnableMark, 0); + List ftbCultivateCourseChapters = ftbCultivateCourseChapterMapper.selectList(courseChapterWrapper); + if (CollUtil.isEmpty(ftbCultivateCourseChapters)) { + return new ArrayList<>(); + } + return ftbCultivateCourseChapters; + } + + @Override + public Map> batchQueryUserCourseChapterLearnStatus(String userId, List courseIds) { + if (CollUtil.isEmpty(courseIds)) { + return new HashMap<>(); + } + List ftbCultivateCourseChapters = batchQueryCourseChapters(courseIds); + if (CollUtil.isEmpty(ftbCultivateCourseChapters)) { + return new HashMap<>(); + } + //按照课程分组 + Map> courseChapterMap = ftbCultivateCourseChapters.stream().collect(Collectors.groupingBy(FtbCultivateCourseChapter::getCourseId)); + + + List courseLearnings = batchQueryCourseChapterLearning(userId, courseIds); + Map> courseLearningsMap = courseLearnings.stream().collect(Collectors.groupingBy(FtbCultivatePositionCourceChapterLearning::getCourceId)); + + Map> ret = new HashMap<>(); + courseChapterMap.forEach((courseId, courseChapters) -> { + List chapterVos = new ArrayList<>(); + List chapterLearningList = courseLearningsMap.get(courseId); + //转换成 章节map + Map chapterLearningMap = new HashMap<>(); + if (CollUtil.isNotEmpty(chapterLearningList)) { + for (FtbCultivatePositionCourceChapterLearning chapterLearning : chapterLearningList) { + chapterLearningMap.put(chapterLearning.getChapterId(), chapterLearning); + } + } + for (FtbCultivateCourseChapter courseChapter : courseChapters) { + FtbCultivatePositionCourceChapterLearning chapterLearning = chapterLearningMap.get(courseChapter.getId()); + V2ChapterVo chapterVo = getV2ChapterVo(courseChapter, chapterLearning); + chapterVos.add(chapterVo); + } + ret.put(courseId, chapterVos); + }); + + return ret; + } + + + @Override + public Map batchQueryUserCourseLearnProgress(String userId, List courseIds) { + if (CollUtil.isEmpty(courseIds)) { + return new HashMap<>(); + } + List ftbCultivateCourseChapters = batchQueryCourseChapters(courseIds); + if (CollUtil.isEmpty(ftbCultivateCourseChapters)) { + return new HashMap<>(); + } + //按照课程分组 + Map> courseChapterMap = ftbCultivateCourseChapters.stream().collect(Collectors.groupingBy(FtbCultivateCourseChapter::getCourseId)); + + + List courseChapterLearnings = batchQueryCourseChapterLearning(userId, courseIds); + Map> courseLearningsMap = courseChapterLearnings.stream().collect(Collectors.groupingBy(FtbCultivatePositionCourceChapterLearning::getCourceId)); + Map courseLearnMap = batchQueryUserCourseLearnStatus(userId, courseIds); + Map ret = new HashMap<>(); + courseChapterMap.forEach((courseId, courseChapters) -> { + List chapterLearningList = courseLearningsMap.get(courseId); + if (CollUtil.isEmpty(chapterLearningList)) { + ret.put(courseId, BigDecimal.ZERO); + return; + } + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = courseLearnMap.get(courseId); + if(ftbCultivatePositionCourceLearning!=null && ftbCultivatePositionCourceLearning.getState()==1){ + ret.put(courseId, BigDecimal.ONE); + return; + } + // 将已学习记录转换为 Map,key为章节ID + Map learningMap = new HashMap<>(); + if (CollUtil.isNotEmpty(chapterLearningList)) { + for (FtbCultivatePositionCourceChapterLearning learning : chapterLearningList) { + learningMap.put(learning.getChapterId(), learning); + } + } + + // 计算进度:遍历所有需要学习的章节,检查是否在已学习记录中且状态为完成 + int totalChapters = courseChapters.size(); + long completedChapters = 0; + + completedChapters = courseChapters.stream() + .filter(chapter -> { + // 检查该章节是否有学习记录且状态为已完成(state=1) + FtbCultivatePositionCourceChapterLearning learning = learningMap.get(chapter.getId()); + return learning != null && learning.getState() != null && learning.getState() == 1; + }) + .count(); + + // 计算进度百分比 + BigDecimal progress = new BigDecimal(completedChapters) + .divide(new BigDecimal(totalChapters), 4, RoundingMode.HALF_UP) + .setScale(2, RoundingMode.HALF_UP); + ret.put(courseId, progress); + + }); + + return ret; + } + + + @NotNull + private static V2ChapterVo getV2ChapterVo(FtbCultivateCourseChapter courseChapter, FtbCultivatePositionCourceChapterLearning chapterLearning) { + V2ChapterVo chapterVo = new V2ChapterVo(); + chapterVo.setId(courseChapter.getId()); + chapterVo.setParentId(courseChapter.getParentId()); + chapterVo.setPath(courseChapter.getPath()); + chapterVo.setChapterName(courseChapter.getName()); + chapterVo.setSortCode(courseChapter.getSortCode()); + chapterVo.setState(chapterLearning == null ? 0 : chapterLearning.getState()); + chapterVo.setChapterTime(chapterLearning == null ? 0 : chapterLearning.getChapterTime()); + return chapterVo; + } + + private List batchQueryCourseChapterLearning(String userId, List courseIds) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePositionCourceChapterLearning::getId, + FtbCultivatePositionCourceChapterLearning::getChapterId, + FtbCultivatePositionCourceChapterLearning::getUserId, + FtbCultivatePositionCourceChapterLearning::getCourceId, + FtbCultivatePositionCourceChapterLearning::getState, + FtbCultivatePositionCourceChapterLearning::getChapterTime) + .eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId) + .in(FtbCultivatePositionCourceChapterLearning::getCourceId, courseIds) + .eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); // 有效标志为0表示有效 + List courseLearnings = ftbCultivatePositionCourceChapterLearningMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(courseLearnings)) { + return new ArrayList<>(); + } + return courseLearnings; + } + + /** + * 根据用户ID查询所有已完成学习的课程ID列表 + * + * @param userId 用户ID + * @return 已完成课程ID列表 + */ + @Override + public List getUserCompletedCourseIds(String userId) { + if (StringUtils.isEmpty(userId)) { + return Collections.emptyList(); + } + + // 构建查询条件,查询用户在指定课程中的学习情况 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePositionCourceLearning::getId, FtbCultivatePositionCourceLearning::getUserId, FtbCultivatePositionCourceLearning::getCourceId, FtbCultivatePositionCourceLearning::getState, FtbCultivatePositionCourceLearning::getLearnTime).eq(FtbCultivatePositionCourceLearning::getUserId, userId).eq(FtbCultivatePositionCourceLearning::getState, 1).eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); // 有效标志为0表示有效 + + List courseLearnings = ftbCultivatePositionCourceLearningMapper.selectList(queryWrapper); + + if (CollUtil.isEmpty(courseLearnings)) { + return Collections.emptyList(); + } + return courseLearnings.stream().map(FtbCultivatePositionCourceLearning::getCourceId).distinct().collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCertificateServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCertificateServiceImpl.java new file mode 100644 index 0000000..875b39f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCertificateServiceImpl.java @@ -0,0 +1,138 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.FtbCultivateCertificateMapper; +import jnpf.cultivate.mapper.FtbCultivateCertificateUserMapper; +import jnpf.cultivate.utils.CultivateImUtil; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivateCertificateService; +import jnpf.model.cultivate.po.certificate.FtbCertificateEntity; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.SelfGrowthUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; + + +@Service +@Slf4j +public class V2CultivateCertificateServiceImpl implements V2CultivateCertificateService { + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbCultivateCertificateUserMapper ftbCultivateCertificateUserMapper; + + @Autowired + private FtbCultivateCertificateMapper ftbCultivateCertificateMapper; + + @Autowired + private CultivateImUtil cultivateImUtil; + + + /** + * 触发证书 + * + * @param userId 用户id + * @param certificateId 证书id + * @param tenantId 租户id + */ + @Override + public void triggerCertificate(String userId, String certificateId, String tenantId) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (userPrimaryBoundOne == null) { + log.error("颁发证书 用户不存在 userId = {},certificateId={}", userId, certificateId); + return; + } + + // 证书是否下架及删除 + FtbCertificateEntity ftbCertificateEntity = ftbCultivateCertificateMapper.selectById(certificateId); + if (ftbCertificateEntity.getStatus() == 0 || CourseEnums.EnabledMarkType.INVALID.getCode().equals(ftbCertificateEntity.getEnabledMark())) { + log.error("证书已下架 userId = {},certificateId={}", userId, certificateId); + return; + } + + + FtbCertificateUserEntity certificateUserOldEntity = queryUserCertificate(userId, certificateId, ftbCultivateCertificateUserMapper); + + //说明证书存在但是是 1已失效,2已吊销 就删除重新颁发 + if (certificateUserOldEntity != null) { + if (ftbCertificateEntity.getExpireType().equals(CourseEnums.CertExpireType.NEVER.getCode()) && certificateUserOldEntity.getStatus().equals(0)) { + return; + } + certificateUserOldEntity.setEnabledMark(0); + ftbCultivateCertificateUserMapper.updateById(certificateUserOldEntity); + } + FtbCertificateUserEntity ftbCertificateUserNewEntity = new FtbCertificateUserEntity(); + ftbCertificateUserNewEntity.setUserId(userId); + ftbCertificateUserNewEntity.setUserName(userPrimaryBoundOne.getUserName()); + ftbCertificateUserNewEntity.setCertificateId(certificateId); + ftbCertificateUserNewEntity.setCertificateName(ftbCertificateEntity.getName()); + ftbCertificateUserNewEntity.setCompanyName(userPrimaryBoundOne.getOrganizeName()); + ftbCertificateUserNewEntity.setOrganizeId(userPrimaryBoundOne.getOrganizeId()); + ftbCertificateUserNewEntity.setPositionId(userPrimaryBoundOne.getPositionId()); + ftbCertificateUserNewEntity.setReason(""); + ftbCertificateUserNewEntity.setStatus(CourseEnums.CertUserStatus.USING.getCode()); + ftbCertificateUserNewEntity.setEnabledMark(CourseEnums.EnabledMarkType.VALID.getCode()); + Integer entityExpireTime = ftbCertificateEntity.getExpireTime(); + if (certificateUserOldEntity != null) { + ftbCertificateUserNewEntity.setNumber(certificateUserOldEntity.getNumber()); + ftbCertificateUserNewEntity.setEffectTime(LocalDateTime.now()); + ftbCertificateUserNewEntity.setCreatorTime(certificateUserOldEntity.getCreatorTime()); + ftbCertificateUserNewEntity.setCreatorUserId(certificateUserOldEntity.getCreatorUserId()); + + if (!CourseEnums.CertExpireType.NEVER.getCode().equals(ftbCertificateEntity.getExpireType())) { + //设置失效时间 ExpireTime=0时 null表示永久 + Date expireTime = certificateUserOldEntity.getExpireTime(); + if (CourseEnums.CertExpireType.DAY.getCode().equals(ftbCertificateEntity.getExpireType())) { + //添加天数 + expireTime = DateUtil.offsetDay(expireTime, ftbCertificateEntity.getExpireTime()); + } else if (CourseEnums.CertExpireType.MONTH.getCode().equals(ftbCertificateEntity.getExpireType())) { + expireTime = DateUtil.offsetMonth(expireTime, ftbCertificateEntity.getExpireTime()); + } + ftbCertificateUserNewEntity.setExpireTime(expireTime); + } + } else { + ftbCertificateUserNewEntity.setNumber(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.CERTIFICATE_ENCODING_PREFIX, 4)); + ftbCertificateUserNewEntity.setCreatorTime(new Date()); + ftbCertificateUserNewEntity.setCreatorUserId(userId); + ftbCertificateUserNewEntity.setEffectTime(LocalDateTime.now()); + if (!CourseEnums.CertExpireType.NEVER.getCode().equals(ftbCertificateEntity.getExpireType())) { + //设置失效时间 ExpireTime=0时 null表示永久 + LocalDateTime expireTime = LocalDateTime.now(); + if (CourseEnums.CertExpireType.DAY.getCode().equals(ftbCertificateEntity.getExpireType())) { + expireTime = expireTime.plusDays(entityExpireTime.longValue()); + } else if (CourseEnums.CertExpireType.MONTH.getCode().equals(ftbCertificateEntity.getExpireType())) { + expireTime = expireTime.plusMonths(entityExpireTime.longValue()); + } + Date date = Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant()); + ftbCertificateUserNewEntity.setExpireTime(date); + } + } + ftbCultivateCertificateUserMapper.insert(ftbCertificateUserNewEntity); + + String contentMessage = "恭喜获得" + ftbCertificateEntity.getName() + "证书。您可前往“我的证书”模块查看更多证书!"; + cultivateImUtil.sendMessage(List.of(userId), tenantId, contentMessage); + } + + public FtbCertificateUserEntity queryUserCertificate(String userId, String certificateId, FtbCultivateCertificateUserMapper ftbCultivateCertificateUserMapper) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCertificateUserEntity::getCertificateId, certificateId) + .eq(FtbCertificateUserEntity::getUserId, userId) + .eq(FtbCertificateUserEntity::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .last("limit 1"); + return ftbCultivateCertificateUserMapper.selectOne(queryWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseAppServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseAppServiceImpl.java new file mode 100644 index 0000000..4a6a89c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseAppServiceImpl.java @@ -0,0 +1,732 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.FtbCultivateChapterTestResultMapper; +import jnpf.cultivate.mapper.FtbCultivateCourseMapper; +import jnpf.cultivate.service.FtbCultivateCourseChapterService; +import jnpf.cultivate.service.FtbCultivateCourseSettingService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourceChapterLearningService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourceLearningService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.*; +import jnpf.cultivate.v2.util.CultivateMqSendUtil; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.entity.cultivate.FtbCultivateCourseLearningLogEntity; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.course.FtbCultivateChapterTestResult; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.course.FtbCultivateCourseChapter; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.course.vo.app.*; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.position.req.V2MyCultivateCommonCourseForAppReq; +import jnpf.model.cultivate.vo.chapter.FtbCultivateChapterTestResultVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +import static jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum.GENERAL_COURSE; + +@Slf4j +@Service +public class V2CultivateCourseAppServiceImpl implements V2CultivateCourseAppService { + @Autowired + private FtbCultivateCourseMapper cultivateCourseMapper; + + + @Autowired + private FtbCultivateChapterTestResultMapper ftbCultivateChapterTestResultMapper; + + @Autowired + private FtbCultivatePositionCourceLearningService courseLearningService; + @Autowired + private FtbCultivatePositionCourceChapterLearningService chapterLearningService; + + @Autowired + private FtbCultivateCourseChapterService courseChapterService; + + @Autowired + protected FtbCultivateCourseSettingService ftbCultivateCourseSettingService; + + @Autowired + private CultivateMqSendUtil cultivateMqSendUtil; + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + @Autowired + private FtbCultivateCourseLearningLogService ftbCultivateCourseLearningLogService; + + + @Autowired + private V2CultivatePostStudyService v2CultivatePostStudyService; + + @Autowired + private V2CultivateTaskStudyService v2CultivateTaskStudyService; + + /** + * 课程详情 + * + * @param courseId 课程id + * @param userId 用户id + * @return 课程详情 + */ + @Override + public V2CourseDetailsAppVO courseDetails(V2CourseAppDto dto) { + String courseId = dto.getCourseId(); + String userId = dto.getUserId(); + V2CourseDetailsAppVO retVo = new V2CourseDetailsAppVO(); + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectById(courseId); + if (Objects.isNull(ftbCultivateCourse)) { + throw new RuntimeException("课程不存在"); + } + + retVo.setCourseId(courseId); + retVo.setCourseName(ftbCultivateCourse.getName()); + retVo.setUpdateTime(ftbCultivateCourse.getLastModifyTime()); + retVo.setViews(ftbCultivateCourse.getViews()); + retVo.setSubtitle(ftbCultivateCourse.getSubtitle()); + retVo.setTypeId(ftbCultivateCourse.getTypeId()); + retVo.setCoverId(ftbCultivateCourse.getCoverId()); + retVo.setCoverUrl(ftbCultivateCourse.getCoverUrl()); + retVo.setHighLights(ftbCultivateCourse.getHighLights()); + retVo.setNeedExamAndIdentify(ftbCultivateCourse.getNeedExamAndIdentify()); + + // 学习总时长 + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId) + .eq(FtbCultivatePositionCourceLearning::getUserId, userId) + .last("limit 1"); + FtbCultivatePositionCourceLearning courseLearning = courseLearningService.getOne(lambdaQueryWrapper); + Optional.ofNullable(courseLearning) + .ifPresentOrElse( + cl -> { + retVo.setDuration(cl.getLearnTime()); + retVo.setState(cl.getState()); + }, + () -> { + retVo.setDuration(0); + retVo.setState(0); + } + ); + + + //课程学习 + if (ftbCultivateCourse.getNeedExamAndIdentify() != null && ftbCultivateCourse.getNeedExamAndIdentify().equals(1)) { + if (dto.getBusinessSource() != null && (dto.getBusinessSource().equals(PositionBusinessSourceEnum.GENERAL_COURSE) + || dto.getBusinessSource().equals(PositionBusinessSourceEnum.OFFLINE_CLASS))) { + if (StringUtils.isNotEmpty(ftbCultivateCourse.getExamId())) { + Map examBasicInfoMap = v2CultivateBatchQueryService.batchQueryExamsByIds(List.of(ftbCultivateCourse.getExamId()), 0); + FtbCultivateExam ftbCultivateExam = examBasicInfoMap.get(ftbCultivateCourse.getExamId()); + if (ftbCultivateExam != null) { + AppCultivateCourseExamVo exam = new AppCultivateCourseExamVo(); + exam.setExamId(ftbCultivateExam.getId()); + exam.setExamName(ftbCultivateExam.getExamName()); + + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, List.of(ftbCultivateExam.getId())); + FtbCultivateExamUser ftbCultivateExamUser = userExamStatusMap.get(ftbCultivateExam.getId()); + if (ftbCultivateExamUser != null) { + exam.setExamStatus(ftbCultivateExamUser.getStatus()); + exam.setExamCompleteDate(ftbCultivateExamUser.getFinishTime()); + exam.setUserExamId(ftbCultivateExamUser.getId()); + exam.setUserExamScore(ftbCultivateExamUser.getScore()); + exam.setExamTotalScore(ftbCultivateExamUser.getTotalScore()); + exam.setUserExamDuration(ftbCultivateExamUser.getDuration()); + } else { + exam.setExamStatus(0); + if (retVo.getState().equals(1) && dto.getBusinessSource() != null + && (dto.getBusinessSource() == PositionBusinessSourceEnum.GENERAL_COURSE || dto.getBusinessSource() == PositionBusinessSourceEnum.OFFLINE_CLASS)) { + String userExamId = v2CultivatePostStudyService.preCommonCourseStudy(userId, courseId, UserProvider.getUser().getTenantId()); + exam.setUserExamId(userExamId); + } else { + exam.setUserExamId(""); + } + } + retVo.setExam(exam); + } + } + if (StringUtils.isNotEmpty(ftbCultivateCourse.getIdentifyId())) { + Map identityBasicInfoMap = v2CultivateBatchQueryService.batchQueryIdentificationsByIds(List.of(ftbCultivateCourse.getIdentifyId()), 0); + CultivateIdentifyTable identityTable = identityBasicInfoMap.get(ftbCultivateCourse.getIdentifyId()); + if (identityTable != null) { + AppCultivateCourseIdentityVo identity = new AppCultivateCourseIdentityVo(); + identity.setIdentityId(identityTable.getId()); + identity.setIdentityName(identityTable.getName()); + + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, List.of(identityTable.getId()), 1, courseId); + + CultivateIdentifyApply identifyApply = userIdentificationStatusMap.get(identityTable.getId()); + if (identifyApply != null) { + identity.setIdentificationResult(identifyApply.getResult()); + identity.setIdentificationStatus(identifyApply.getStatus()); + identity.setUserIdentityId(identifyApply.getId()); + } else { + identity.setIdentificationStatus(0); + identity.setUserIdentityId(""); + } + + retVo.setIdentity(identity); + } + } + } + } + return retVo; + } + + /** + * 课程大纲 + * + * @param courseId 课程id + * @param userId 用户id + * @return 课程详情 + */ + @Override + public V2CourseOutlineAppVo courseOutline(String courseId, String userId) { + + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectById(courseId); + if (Objects.isNull(ftbCultivateCourse)) { + throw new RuntimeException("课程不存在"); + } + + V2CourseOutlineAppVo ftbCourseOutlineAppVO = new V2CourseOutlineAppVo(); + ftbCourseOutlineAppVO.initData(); + ftbCourseOutlineAppVO.setCourseId(courseId); + ftbCourseOutlineAppVO.setCourseName(ftbCultivateCourse.getName()); + ftbCourseOutlineAppVO.setCoverUrl(ftbCultivateCourse.getCoverUrl()); + ftbCourseOutlineAppVO.setHighLights(ftbCultivateCourse.getHighLights()); + // 查询课程章节信息 + List chapters = getCourseChapters(courseId); + // 批量查询课程章节学习信息 + if (CollUtil.isEmpty(chapters)) { + return ftbCourseOutlineAppVO; + } + + List chapterList = chapters.stream().map(FtbCultivateCourseChapter::getId).collect(Collectors.toList()); + Map chapterLearningMap = v2CultivateBatchQueryService.batchQueryCourseChapterLearningState(userId, chapterList); + List chapterTree = V2CourseOutlineAppVo.buildChapterVoTree(chapters, chapterLearningMap); + ftbCourseOutlineAppVO.setChapterVoList(chapterTree); + ftbCourseOutlineAppVO.setChapterTotalNumber(chapters.size()); + ftbCourseOutlineAppVO.setChapterFinishedNumber(V2CourseOutlineAppVo.calCompleteChapterNumber(chapterLearningMap)); + return ftbCourseOutlineAppVO; + } + + + /** + * 章节详情 + * + * @param id 章节id,必填 + * @return 课程章节详情 + */ + @Override + public V2ChapterAppDetails chapterDetails(String id, String userId) { + V2ChapterAppDetails retVo = courseChapterService.courseDetailsV2(id); + Map chapterLearningMap = v2CultivateBatchQueryService.batchQueryCourseChapterLearningState(userId, List.of(id)); + FtbCultivatePositionCourceChapterLearning chapterLearning = chapterLearningMap.get(id); + if (chapterLearning != null) { + retVo.setState(chapterLearning.getState()); + retVo.setChapterTime(chapterLearning.getChapterTime()); + retVo.setLastVideoStudyTime(chapterLearning.getLastVideoStudyTime()); + } else { + retVo.setState(0); + retVo.setChapterTime(0); + } + + + LambdaQueryWrapper testResultQuery = Wrappers.lambdaQuery(); + testResultQuery.eq(FtbCultivateChapterTestResult::getCourseId, retVo.getCourseId()); + testResultQuery.eq(FtbCultivateChapterTestResult::getCourseChapterId, id); + testResultQuery.eq(FtbCultivateChapterTestResult::getUserId, userId); + testResultQuery.eq(FtbCultivateChapterTestResult::getEnableMark, 0); + testResultQuery.orderByDesc(FtbCultivateChapterTestResult::getCreatorTime); + List ftbCultivateChapterTestResults = ftbCultivateChapterTestResultMapper.selectList(testResultQuery); + + if (CollUtil.isNotEmpty(ftbCultivateChapterTestResults)) { + FtbCultivateChapterTestResult testResult = ftbCultivateChapterTestResults.get(0); + if (StringUtils.isNotEmpty(testResult.getChapterTestOptions())) { + List chapterTestList = JSONObject.parseArray(testResult.getChapterTestOptions(), FtbCultivateChapterTestResultVO.class); + retVo.setUserTestResultList(chapterTestList); + } + } + + // 查询课程章节信息 + List chapters = getCourseChapters(retVo.getCourseId()); + // 批量查询课程章节学习信息 + if (CollUtil.isEmpty(chapters)) { + return retVo; + } + + List chapterTree = V2CourseOutlineAppVo.buildChapterVoTree(chapters, new HashMap<>()); + Map stringStringMap = V2ChapterAppDetails.lineVo(chapterTree, id); + retVo.setPreId(stringStringMap.get("preId")); + retVo.setNextId(stringStringMap.get("nextId")); + return retVo; + } + + /** + * 章节学习 + * + * @param req 章节学习参数 + */ + @Override + public void chapterStudy(V2ChapterStudyVo req) { + // 获取当前用户信息 + + UserInfo user = UserProvider.getUser(); + String userId = user.getUserId(); + String tenantId = user.getTenantId(); + + // 查询课程的所有章节并检查是否存在 + List chapterList = getCourseChapters(req.getCourseId()); + if (CollUtil.isEmpty(chapterList)) { + return; + } + + // 完成课程章节学习 + completeCourseChapterLearning(req, userId, tenantId); + + // 计算并更新课程整体学习状态 + List allChapterIds = chapterList.stream() + .map(FtbCultivateCourseChapter::getId) + .collect(Collectors.toList()); + Integer courseState = calculateCourseState(req.getCourseId(), userId, allChapterIds); + updateOrCreateCourseLearning(req, userId, tenantId, courseState); + // 如果课程已完成,发送消息通知 + if (Objects.equals(courseState, 1)) { + //记录学习记录 + ftbCultivateCourseLearningLogService.save(FtbCultivateCourseLearningLogEntity.builder() + .courseId(req.getCourseId()).learnTime(0).creatorUserId(userId).creatorTime(new Date()) + .tenantId(user.getTenantId()).enabledMark(0).status(1).build() + ); + if (req.getBusinessSource() != null) { + switch (req.getBusinessSource()) { + case POST_LEARNING: + v2CultivatePostStudyService.prePositionCourseStudy(userId, req.getCourseId(), req.getBusinessSourceId(), req.getGradeId(), tenantId); + break; + case TASK_LEARNING: + v2CultivateTaskStudyService.preTaskCourseStudy(userId, req.getCourseId(), req.getBusinessSourceId(), tenantId); + break; + default: + v2CultivatePostStudyService.preCommonCourseStudy(userId, req.getCourseId(), tenantId); + break; + } + } + cultivateMqSendUtil.sendCourseCompletionMessage(req, userId, tenantId); + } + } + + /** + * 获取课程所有章节 + * + * @param courseId 课程ID + * @return 章节列表 + */ + private List getCourseChapters(String courseId) { + LambdaQueryWrapper courseChapterWrapper = Wrappers.lambdaQuery(); + courseChapterWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId) + .eq(FtbCultivateCourseChapter::getEnableMark, 0) + .orderByAsc(FtbCultivateCourseChapter::getSortCode) + ; + return courseChapterService.list(courseChapterWrapper); + } + + /** + * 完成章节学习记录 + * + * @param req 章节学习参数 + * @param userId 用户ID + * @param tenantId 租户ID + */ + private void completeCourseChapterLearning(V2ChapterStudyVo req, String userId, String tenantId) { + String params = buildStudyParam(req, userId); + LambdaQueryWrapper queryChapterWrapper = Wrappers.lambdaQuery(); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getCourceId, req.getCourseId()); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getChapterId, req.getChapterId()); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List existingChapter = chapterLearningService.list(queryChapterWrapper); + + if (CollUtil.isNotEmpty(existingChapter)) { + // 更新现有章节学习记录 + LambdaUpdateWrapper updateChapterWrapper = Wrappers.lambdaUpdate(); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getCourceId, req.getCourseId()); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getChapterId, req.getChapterId()); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + updateChapterWrapper.set(FtbCultivatePositionCourceChapterLearning::getState, 1); + updateChapterWrapper.set(FtbCultivatePositionCourceChapterLearning::getLastModifyTime, new Date()); + updateChapterWrapper.set(FtbCultivatePositionCourceChapterLearning::getLastStudyParams, params); +// updateChapterWrapper.setSql("F_ChapterTime = F_ChapterTime + " + req.getDuration());//前端沟通完成后,取消学习时长累加 + chapterLearningService.update(new FtbCultivatePositionCourceChapterLearning(), updateChapterWrapper); + } else { + // 创建新的章节学习记录 + FtbCultivatePositionCourceChapterLearning chapterLearning = new FtbCultivatePositionCourceChapterLearning(); + chapterLearning.setCourceId(req.getCourseId()); + chapterLearning.setChapterId(req.getChapterId()); + chapterLearning.setUserId(userId); + chapterLearning.setEnabledMark(0); + chapterLearning.setState(1); +// chapterLearning.setChapterTime(req.getDuration()); + chapterLearning.setTenantId(tenantId); + chapterLearning.setLastModifyTime(new Date()); + chapterLearning.setLastStudyParams(params); + chapterLearningService.save(chapterLearning); + } + } + + /** + * 计算课程学习状态 + * + * @param courseId 课程ID + * @param userId 用户ID + * @param allChapterIds 课程所有章节ID列表 + * @return 课程学习状态:0-未学习,1-已学习,2-学习中 + */ + private Integer calculateCourseState(String courseId, String userId, List allChapterIds) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbCultivatePositionCourceChapterLearning::getCourceId, + FtbCultivatePositionCourceChapterLearning::getUserId, + FtbCultivatePositionCourceChapterLearning::getChapterId); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getCourceId, courseId); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getState, 1); + queryWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List courseLearningList = chapterLearningService.list(queryWrapper); + + if (CollUtil.isNotEmpty(courseLearningList)) { + List completedChapterIds = courseLearningList.stream() + .map(FtbCultivatePositionCourceChapterLearning::getChapterId) + .collect(Collectors.toList()); + // 如果已完成的章节ID列表包含所有章节ID,则课程已完成 + if (new HashSet<>(completedChapterIds).containsAll(allChapterIds)) { + return 1; // 已学习 + } + } + + return 2; // 学习中 + } + + /** + * 更新或创建课程学习记录 + * + * @param req 章节学习参数 + * @param userId 用户ID + * @param tenantId 租户ID + * @param courseState 课程学习状态 + */ + private void updateOrCreateCourseLearning(V2ChapterStudyVo req, String userId, String tenantId, Integer courseState) { + LambdaQueryWrapper courseLearningWrapper = Wrappers.lambdaQuery(); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, req.getCourseId()); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List courseLearningList = courseLearningService.list(courseLearningWrapper); + + if (CollUtil.isNotEmpty(courseLearningList)) { + // 更新现有课程学习记录 + LambdaUpdateWrapper updateCourseWrapper = Wrappers.lambdaUpdate(); + updateCourseWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, req.getCourseId()); + updateCourseWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + updateCourseWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + updateCourseWrapper.set(FtbCultivatePositionCourceLearning::getState, courseState); + updateCourseWrapper.set(FtbCultivatePositionCourceLearning::getLastModifyTime, new Date()); +// updateCourseWrapper.setSql("F_LearnTime = F_LearnTime + " + req.getDuration()); + courseLearningService.update(updateCourseWrapper); + } else { + // 创建新的课程学习记录 + FtbCultivatePositionCourceLearning courseLearning = new FtbCultivatePositionCourceLearning(); + courseLearning.setCourceId(req.getCourseId()); + courseLearning.setUserId(userId); +// courseLearning.setLearnTime(req.getDuration()); + courseLearning.setState(courseState); + courseLearning.setEnabledMark(0); + courseLearning.setTenantId(tenantId); + courseLearning.setLastModifyTime(new Date()); + courseLearning.setCreatorTime(new Date()); + courseLearningService.save(courseLearning); + } + } + + private String buildStudyParam(V2ChapterStudyVo req, String userId) { + LastStudyCourseVo lastStudyCourse = new LastStudyCourseVo(); + lastStudyCourse.setCourseId(req.getCourseId()); + lastStudyCourse.setChapterId(req.getChapterId()); + if (req.getBusinessSource() != null) { + lastStudyCourse.setBusinessSourceCode(req.getBusinessSource().getCode()); + lastStudyCourse.setBusinessSourceId(req.getBusinessSourceId()); + } else { + lastStudyCourse.setBusinessSourceCode(1); + lastStudyCourse.setBusinessSourceId(req.getCourseId()); + } + return JSONUtil.toJsonStr(lastStudyCourse); + } + + /** + * 处理随堂测试结果 + * + * @param req 章节学习参数 + * @param userId 用户ID + */ + private void handleOnsiteTestResults(V2ChapterStudyVo req, String userId) { + ftbCultivateCourseSettingService.onSiteTestResultStorageV2( + req.getFtbChapterTestDTOs(), + req.getCourseId(), + req.getChapterId(), + userId + ); + } + + + /** + * 课程学习时长记录 + * + * @param req 章节学习参数 + */ + @Override + public void durationRecord(V2ChapterStudyVo req) { + UserInfo user = UserProvider.getUser(); + String userId = user.getUserId(); + // 更新或创建章节学习时长 + recordChapterLearningTime(req, userId); + //记录课程学习时长 + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = recordCourseLearningTime(req, userId); + // 处理随堂测试结果 + handleOnsiteTestResults(req, userId); + + //记录学习记录 + ftbCultivateCourseLearningLogService.save(FtbCultivateCourseLearningLogEntity.builder() + .courseId(req.getCourseId()).learnTime(req.getDuration()).creatorUserId(userId).creatorTime(new Date()) + .tenantId(user.getTenantId()).enabledMark(0).status(ftbCultivatePositionCourceLearning.getState().equals(1) ? 1 : 0).build() + ); + + } + + /** + * 记录章节学习时长 + * + * @param req 章节学习参数 + * @param userId 用户ID + */ + private FtbCultivatePositionCourceLearning recordCourseLearningTime(V2ChapterStudyVo req, String userId) { + LambdaQueryWrapper courseLearningWrapper = Wrappers.lambdaQuery(); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, req.getCourseId()); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List courseLearningList = courseLearningService.list(courseLearningWrapper); + + if (CollUtil.isNotEmpty(courseLearningList)) { + // 更新现有课程学习记录 + LambdaUpdateWrapper updateCourseWrapper = Wrappers.lambdaUpdate(); + updateCourseWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, req.getCourseId()); + updateCourseWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + updateCourseWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + updateCourseWrapper.set(FtbCultivatePositionCourceLearning::getLastModifyTime, new Date()); + updateCourseWrapper.set(courseLearningList.get(0).getState() == 0, FtbCultivatePositionCourceLearning::getState, 2); + updateCourseWrapper.setSql("F_LearnTime = F_LearnTime + " + req.getDuration()); + courseLearningService.update(updateCourseWrapper); + return courseLearningList.get(0); + } else { + // 创建新的课程学习记录 + FtbCultivatePositionCourceLearning courseLearning = new FtbCultivatePositionCourceLearning(); + courseLearning.setCourceId(req.getCourseId()); + courseLearning.setUserId(userId); + courseLearning.setLearnTime(req.getDuration()); + courseLearning.setState(2); + courseLearning.setEnabledMark(0); + courseLearning.setLastModifyTime(new Date()); + courseLearningService.save(courseLearning); + return courseLearning; + } + } + + /** + * 记录课程章节学习时长 + * + * @param userId 用户id + */ + private void recordChapterLearningTime(V2ChapterStudyVo req, String userId) { + String params = buildStudyParam(req, userId); + LambdaQueryWrapper queryChapterWrapper = Wrappers.lambdaQuery(); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getCourceId, req.getCourseId()); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getChapterId, req.getChapterId()); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + List existingChapter = chapterLearningService.list(queryChapterWrapper); + + if (CollUtil.isNotEmpty(existingChapter)) { + // 更新现有章节学习记录 + LambdaUpdateWrapper updateChapterWrapper = Wrappers.lambdaUpdate(); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getCourceId, req.getCourseId()); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getChapterId, req.getChapterId()); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + updateChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + updateChapterWrapper.set(FtbCultivatePositionCourceChapterLearning::getLastStudyParams, params); + updateChapterWrapper.set(FtbCultivatePositionCourceChapterLearning::getLastModifyTime, new Date()); + if (req.getLastVideoStudyTime() != null) { + updateChapterWrapper.set(FtbCultivatePositionCourceChapterLearning::getLastVideoStudyTime, req.getLastVideoStudyTime()); + } + updateChapterWrapper.setSql("F_ChapterTime = F_ChapterTime + " + req.getDuration()); + chapterLearningService.update(new FtbCultivatePositionCourceChapterLearning(), updateChapterWrapper); + } else { + // 创建新的章节学习记录 + FtbCultivatePositionCourceChapterLearning chapterLearning = new FtbCultivatePositionCourceChapterLearning(); + chapterLearning.setCourceId(req.getCourseId()); + chapterLearning.setChapterId(req.getChapterId()); + chapterLearning.setUserId(userId); + chapterLearning.setEnabledMark(0); + chapterLearning.setState(0); + chapterLearning.setChapterTime(req.getDuration()); + chapterLearning.setLastModifyTime(new Date()); + chapterLearning.setLastStudyParams(params); + chapterLearning.setLastVideoStudyTime(req.getLastVideoStudyTime()); + chapterLearningService.save(chapterLearning); + } + } + + /** + * 课程浏览 + * + * @param courseId 课程id + */ + @Override + public void courseBrowsing(String courseId) { + // 直接执行原子更新操作,避免并发问题和多次数据库交互 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbCultivateCourse::getId, courseId) + .setSql("F_Views = IFNULL(F_Views, 0) + 1"); + cultivateCourseMapper.update(null, wrapper); + } + + /** + * 通用课程列表 + * + * @param dto 参数 + * @param page 分页参数 + * @return 课程列表 + */ + @Override + public Page commonCourseList(V2MyCultivateCommonCourseForAppReq dto, CultivatePage page) { + String userId = dto.getUserId(); + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (userBoundVO == null) { + throw new RuntimeException("用户信息不存在"); + } + Page pageResult = cultivateCourseMapper.queryCommonCourseApp(page.coverCultivatePage(), dto); + List records = pageResult.getRecords(); + if (CollUtil.isNotEmpty(records)) { + List courseIds = records.stream().map(AppCommonCourseSimpleVo::getCourseId).collect(Collectors.toList()); + Map studyNumMap = v2CultivateBatchQueryService.batchQueryCourseLearningCount(courseIds); + Map learningStateMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, courseIds); + for (AppCommonCourseSimpleVo record : records) { + record.setLearnTotalNumber(studyNumMap.getOrDefault(record.getCourseId(), 0L)); + FtbCultivatePositionCourceLearning state = learningStateMap.get(record.getCourseId()); + if (state == null) { + record.setLearnState(0); // 未学习 + } else { + record.setLearnState(state.getState()); // 0未学习,1已学习,2学习中 + } + record.setBusinessSource(GENERAL_COURSE); + record.setBusinessSourceId(record.getCourseId()); + } + } + return pageResult; + } + + @Override + public LastStudyCourseVo queryLastStudyCourse() { + LastStudyCourseVo vo = new LastStudyCourseVo(); + String userId = UserProvider.getUser().getUserId(); + LambdaQueryWrapper queryChapterWrapper = Wrappers.lambdaQuery(); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getUserId, userId); + queryChapterWrapper.eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0); + queryChapterWrapper.orderByDesc(FtbCultivatePositionCourceChapterLearning::getLastModifyTime); + queryChapterWrapper.last("LIMIT 1"); + List chapterLearningList = chapterLearningService.list(queryChapterWrapper); + if (CollUtil.isEmpty(chapterLearningList)) { + return vo; + } + FtbCultivatePositionCourceChapterLearning chapterLearning = chapterLearningList.get(0); + if (StringUtils.isEmpty(chapterLearning.getLastStudyParams())) { + return vo; + } + FtbCultivateCourse ftbCultivateCourse = cultivateCourseMapper.selectById(chapterLearning.getCourceId()); + if (ftbCultivateCourse == null || ftbCultivateCourse.getEnableMark().equals(1) || ftbCultivateCourse.getIsGroundIng().equals(0)) { + return vo; + } + if (chapterLearning.getState().equals(0)) { + LastStudyCourseVo lastStudyCourseVo = JSONUtil.toBean(chapterLearning.getLastStudyParams(), LastStudyCourseVo.class); + lastStudyCourseVo.setCourseId(chapterLearning.getCourceId()); + lastStudyCourseVo.setChapterId(chapterLearning.getChapterId()); + lastStudyCourseVo.setBusinessSource(PositionBusinessSourceEnum.getByCode(lastStudyCourseVo.getBusinessSourceCode())); + if (!lastStudyCourseVo.getBusinessSourceCode().equals(1)) { + lastStudyCourseVo.setBusinessSourceId(lastStudyCourseVo.getBusinessSourceId()); + } else { + lastStudyCourseVo.setBusinessSourceId(chapterLearning.getCourceId()); + } + lastStudyCourseVo.setState(2); + vo = lastStudyCourseVo; + } + String courseId = chapterLearning.getCourceId(); + LambdaQueryWrapper courseLearningWrapper = Wrappers.lambdaQuery(); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List courseLearningList = courseLearningService.list(courseLearningWrapper); + if (CollUtil.isEmpty(courseLearningList)) { + return vo; + } + FtbCultivatePositionCourceLearning courseLearning = courseLearningList.get(0); + vo.setState(courseLearning.getState()); + if (!courseLearning.getState().equals(1)) { + LastStudyCourseVo lastStudyCourseVo = JSONUtil.toBean(chapterLearning.getLastStudyParams(), LastStudyCourseVo.class); + lastStudyCourseVo.setCourseId(chapterLearning.getCourceId()); + lastStudyCourseVo.setChapterId(chapterLearning.getChapterId()); + lastStudyCourseVo.setBusinessSource(PositionBusinessSourceEnum.getByCode(lastStudyCourseVo.getBusinessSourceCode())); + return lastStudyCourseVo; + } + return vo; + } + + @Override + public CommonCourseCountVo commonCourseCount(V2MyCultivateCommonCourseForAppReq dto) { + String userId = dto.getUserId(); + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (userBoundVO == null) { + throw new RuntimeException("用户信息不存在"); + } + List courseIds = cultivateCourseMapper.commonCourseCount(dto); + CommonCourseCountVo vo = new CommonCourseCountVo(); + vo.init(); + if (CollUtil.isNotEmpty(courseIds)) { + vo.setTotalCourse(courseIds.size()); + Map learningStateMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, courseIds); + //统计已经完成的学习数量 + for (String courseId : courseIds) { + FtbCultivatePositionCourceLearning state = learningStateMap.get(courseId); + if (state != null && state.getState().equals(1)) { + vo.setAlreadyLearnedCourse(vo.getAlreadyLearnedCourse() + 1); + } + } + } + + return vo; + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseServiceImpl.java new file mode 100644 index 0000000..1f536ac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateCourseServiceImpl.java @@ -0,0 +1,705 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.CultivateCourseMsgService; +import jnpf.cultivate.service.FtbCultivateChapterTestOptionService; +import jnpf.cultivate.service.FtbCultivateChapterTestService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.FtbCultivateCourseSettingGlobalService; +import jnpf.cultivate.v2.service.V2CultivateCourseService; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.model.cultivate.dto.course.testoption.FtbCultivateChapterTestOptionDTO; +import jnpf.model.cultivate.po.FtbCultivateCourseSettingGlobal; +import jnpf.model.cultivate.po.course.*; +import jnpf.model.cultivate.po.course.app.CultivateCourseMsg; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceChapterLearning; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.course.vo.AiHelperCourseStatisticsVo; +import jnpf.model.cultivate.v2.course.vo.UserLearningStatusVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCultivateCourseExamVo; +import jnpf.model.cultivate.v2.course.vo.app.AppCultivateCourseIdentityVo; +import jnpf.model.cultivate.v2.course.vo.app.V2CultivateChapterTestAddDTO; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseChapterReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseListReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseReq; +import jnpf.model.cultivate.v2.course.web.req.V2CultivateCourseSelectReq; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseChapterVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseDetailsVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCoursePageVo; +import jnpf.model.cultivate.v2.course.web.vo.V2CultivateCourseSelectVo; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class V2CultivateCourseServiceImpl implements V2CultivateCourseService { + + @Autowired + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + private FtbCultivatePositionCourceChapterLearningMapper ftbCultivatePositionCourceChapterLearningMapper; + + @Autowired + private FtbCultivateChapterTestResultMapper ftbCultivateChapterTestResultMapper; + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Autowired + private FtbCultivateCourseChapterMapper ftbCultivateCourseChapterMapper; + + + @Autowired + private FtbCultivateLabelMapper ftbCultivateLabelMapper; + + @Autowired + private CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Autowired + private FtbCultivateChapterTestOptionService ftbCultivateChapterTestOptionService; + + @Autowired + private CultivateCourseMsgService cultivateCourseMsgService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + @Autowired + private FtbCultivateChapterTestService ftbCultivateChapterTestService; + + @Autowired + private FtbCultivateCourseSettingGlobalService ftbCultivateCourseSettingGlobalService; + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + @Autowired + private CultivateIdentifyApplyMapper cultivateIdentifyApplyMapper; + + /** + * 课程列表分页查询 + * + * @param req 课程列表分页查询参数 + * @return 分页数据 + */ + @Override + public Page webList(V2CultivateCourseListReq req) { + return ftbCultivateCourseMapper.webList(Page.of(req.getCurrentPage(), req.getPageSize()), req); + } + + /** + * 添加课程 + * + * @param req 添加课程参数 + * @return 课程ID + */ + @Override + @Transactional + public String add(V2CultivateCourseReq req) { + // 参数验证 + req.validateCourseParams(); + // 检查课程名称是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourse::getName, req.getName()) + .eq(FtbCultivateCourse::getEnableMark, 0); // 只检查有效的课程 + long count = ftbCultivateCourseMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("课程名称不能重复"); + } + + // 创建课程实体 + FtbCultivateCourse course = new FtbCultivateCourse(); + buildCultivateCourse(req, course); + course.setChapterNumber(calChapterNum(req.getChapterReqList())); // 初始章节数为0 + course.setEnableMark(0); // 设置为有效 + course.setHighLights(req.getHighLights()); + course.setCourseId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.COURSE)); + course.setLastModifyTime(new Date()); + course.setLastModifyUserId(UserProvider.getLoginUserId()); + // 插入课程 + ftbCultivateCourseMapper.insert(course); + String courseId = course.getId(); + + // 如果有章节信息,则处理章节及随堂测试 + updateAndAddCourseChapters(req.getChapterReqList(), courseId, null, ""); + return courseId; + } + + private void buildCultivateCourse(V2CultivateCourseReq req, FtbCultivateCourse course) { + course.setName(req.getName()); + course.setLabel(req.getLabel()); + course.setTypeId(req.getTypeId()); + course.setCoverId(req.getCoverId()); + course.setCoverUrl(req.getCoverUrl()); + course.setNeedExamAndIdentify(req.getNeedExamAndIdentify()); + course.setExamId(req.getExamId()); + course.setIdentifyId(req.getIdentifyId()); + } + + /** + * 计算章节数 + * + * @param chapterReqList 章节请求列表 + * @return 章节数量 + */ + private Integer calChapterNum(List chapterReqList) { + if (CollUtil.isEmpty(chapterReqList)) { + return 0; + } + + return chapterReqList.stream() + .mapToInt(chapter -> 1 + calChapterNum(chapter.getChildChapter())) + .sum(); + } + + + /** + * 处理章节随堂测试 + */ + private void handleChapterTests(List testList, String courseId, String chapterId) { + + LambdaUpdateWrapper testUpdateWrapper = Wrappers.lambdaUpdate(); + testUpdateWrapper.in(FtbCultivateChapterTest::getCourseChapterId, chapterId); + testUpdateWrapper.set(FtbCultivateChapterTest::getEnableMark, 1); // 逻辑删除 + ftbCultivateChapterTestService.update(new FtbCultivateChapterTest(), testUpdateWrapper); + + if (CollUtil.isEmpty(testList)) { + return; + } + for (V2CultivateChapterTestAddDTO test : testList) { + // 创建随堂测试实体 + FtbCultivateChapterTest chapterTest = new FtbCultivateChapterTest(); + chapterTest.setCourseId(courseId); + chapterTest.setCourseChapterId(chapterId); + chapterTest.setForcedStudy(test.getForcedStudy()); + chapterTest.setTopicName(test.getTopicName()); + chapterTest.setAnswerAnalysis(test.getAnswerAnalysis()); + chapterTest.setEnableMark(0); // 设置为有效 + + // 插入随堂测试 + ftbCultivateChapterTestService.save(chapterTest); + String testId = chapterTest.getId(); + + // 处理测试选项 + if (CollUtil.isNotEmpty(test.getFetchChapterTestOptions())) { + List optionList = new ArrayList<>(); + for (FtbCultivateChapterTestOptionDTO optionDTO : test.getFetchChapterTestOptions()) { + optionList.add(optionDTO.convert(testId)); + } + + // 批量插入测试选项 + if (CollUtil.isNotEmpty(optionList)) { + ftbCultivateChapterTestOptionService.saveBatch(optionList); + } + } + } + } + + + /** + * 更新课程 + * + * @param req 更新课程参数 + */ + @Override + @Transactional + public void update(V2CultivateCourseReq req) { + // 参数验证 + req.validateCourseParams(); + + // 获取原课程信息 + FtbCultivateCourse course = ftbCultivateCourseMapper.selectById(req.getId()); + if (course == null) { + throw new RuntimeException("课程不存在"); + } + + // 检查课程名称是否与其他课程重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourse::getName, req.getName()) + .ne(FtbCultivateCourse::getId, req.getId()) + .eq(FtbCultivateCourse::getEnableMark, 0); // 只检查有效的课程 + long count = ftbCultivateCourseMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("课程名称不能重复"); + } + + // 更新课程基本信息 + buildCultivateCourse(req, course); + course.setHighLights(req.getHighLights()); + + dealDeleteCourseChapter(req.getChapterReqList(), req.getId()); + // 处理章节更新 - 智能更新章节,保留章节ID + + updateAndAddCourseChapters(req.getChapterReqList(), req.getId(), null, ""); + + // 更新课程章节数 + course.setChapterNumber(calChapterNum(req.getChapterReqList())); + course.setLastModifyTime(new Date()); + course.setLastModifyUserId(UserProvider.getLoginUserId()); + ftbCultivateCourseMapper.updateById(course); + } + + /** + * 处理章节更新 - 智能更新章节,保留章节ID + */ + private void updateAndAddCourseChapters(List chapterList, String courseId, String parentId, String path) { + for (V2CultivateCourseChapterReq chapterReq : chapterList) { + // 创建章节实体 + FtbCultivateCourseChapter chapter = chapterReq.convertFtbCultivateCourseChapter(chapterReq); + chapter.setCourseId(courseId); + chapter.setEnableMark(0); // 设置为有效 + + // 计算路径 - 先保存章节以获取ID + if (StringUtils.isEmpty(parentId)) { + chapter.setParentId("0"); + chapter.setPath(""); + } else { + chapter.setParentId(parentId); + chapter.setPath(path); + } + if (StringUtils.isEmpty(chapterReq.getId())) { + ftbCultivateCourseChapterMapper.insert(chapter); + } else { + chapter.setId(chapterReq.getId()); + ftbCultivateCourseChapterMapper.updateById(chapter); + } + + String chapterId = chapter.getId(); + String newPath = ""; + if (StringUtils.isEmpty(parentId)) { + newPath = chapterId; + } else { + newPath = parentId + "-" + chapterId; + } + // 处理随堂测试 + + handleChapterTests(chapterReq.getCultivateChapterTest(), courseId, chapterId); + + + // 递归处理子章节 + if (CollUtil.isNotEmpty(chapterReq.getChildChapter())) { + updateAndAddCourseChapters(chapterReq.getChildChapter(), courseId, chapterId, newPath); + } + } + } + + /** + * 递归删除章节(包括子章节和随堂测试) + */ + private void dealDeleteCourseChapter(List chapterReqList, String courseId) { + + // 获取当前父级下的现有章节 + LambdaQueryWrapper existingQuery = Wrappers.lambdaQuery(); + existingQuery.eq(FtbCultivateCourseChapter::getCourseId, courseId); + existingQuery.eq(FtbCultivateCourseChapter::getEnableMark, 0); + List existingChapters = ftbCultivateCourseChapterMapper.selectList(existingQuery); + if (CollUtil.isEmpty(existingChapters)) { + return; + } + List existingChapterIds = existingChapters.stream().map(FtbCultivateCourseChapter::getId).collect(Collectors.toList()); + List chapterListIds = new ArrayList<>(); + getAllReqChapterIds(chapterReqList, chapterListIds); + List deleteChapterIds = new ArrayList<>(); + for (String existingChapterId : existingChapterIds) { + if (!chapterListIds.contains(existingChapterId)) { + deleteChapterIds.add(existingChapterId); + } + } + if (CollUtil.isEmpty(deleteChapterIds)) { + return; + } + + + // 删除随堂测试 + LambdaUpdateWrapper testUpdateWrapper = Wrappers.lambdaUpdate(); + testUpdateWrapper.in(FtbCultivateChapterTest::getCourseChapterId, deleteChapterIds); + testUpdateWrapper.set(FtbCultivateChapterTest::getEnableMark, 1); // 逻辑删除 + ftbCultivateChapterTestService.update(new FtbCultivateChapterTest(), testUpdateWrapper); + + + // 删除当前章节 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(FtbCultivateCourseChapter::getId, deleteChapterIds); + updateWrapper.set(FtbCultivateCourseChapter::getEnableMark, 1); // 逻辑删除 + ftbCultivateCourseChapterMapper.update(new FtbCultivateCourseChapter(), updateWrapper); + } + + private void getAllReqChapterIds(List chapterReqList, List chapterListIds) { + + for (V2CultivateCourseChapterReq chapterReq : chapterReqList) { + if (StringUtils.isNotEmpty(chapterReq.getId())) { + chapterListIds.add(chapterReq.getId()); + } + if (CollUtil.isNotEmpty(chapterReq.getChildChapter())) { + List chapterListIdsChild1 = new ArrayList<>(); + getAllReqChapterIds(chapterReq.getChildChapter(), chapterListIdsChild1); + chapterListIds.addAll(chapterListIdsChild1); + } + } + } + + + /** + * 删除课程 + * + * @param courseId 课程ID + */ + @Override + public void delete(String courseId) { + + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateCourse::getId, courseId); + updateWrapper.set(FtbCultivateCourse::getEnableMark, 1); + ftbCultivateCourseMapper.update(new FtbCultivateCourse(), updateWrapper); + + // 删除当前章节 + LambdaUpdateWrapper updateChapterWrapper = Wrappers.lambdaUpdate(); + updateChapterWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId); + updateChapterWrapper.set(FtbCultivateCourseChapter::getEnableMark, 1); // 逻辑删除 + ftbCultivateCourseChapterMapper.update(new FtbCultivateCourseChapter(), updateChapterWrapper); + + // 随堂测试 + LambdaUpdateWrapper chapterTestLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + chapterTestLambdaUpdateWrapper.eq(FtbCultivateChapterTest::getCourseId, courseId); + chapterTestLambdaUpdateWrapper.set(FtbCultivateChapterTest::getEnableMark, 1); + ftbCultivateChapterTestService.update(new FtbCultivateChapterTest(), chapterTestLambdaUpdateWrapper); + + } + + /** + * 查询课程详情 + * + * @param courseId 课程ID + * @return {@link V2CultivateCourseDetailsVo} + */ + @Override + public V2CultivateCourseDetailsVo detail(String courseId) { + // 1. 查询课程基本信息 + FtbCultivateCourse course = ftbCultivateCourseMapper.selectById(courseId); + if (course == null || course.getEnableMark() != 0) { + throw new RuntimeException("课程不存在或已被删除"); + } + + // 2. 构建返回VO + V2CultivateCourseDetailsVo courseDetailsVo = new V2CultivateCourseDetailsVo(); + courseDetailsVo.convert(course); + + // 3. 查询课程类型名称 + if (StringUtils.isNotEmpty(course.getTypeId())) { + FtbCultivateLabel courseType = ftbCultivateLabelMapper.selectById(course.getTypeId()); + if (courseType != null && courseType.getEnabledMark() == 0) { + courseDetailsVo.setTypeName(courseType.getName()); + } + } + + // 4. 查询课程章节信息 + List chapters = getCourseChapters(courseId); + + // 5. 构建章节树形结构 + List chapterTree = buildChapterTree(chapters); + courseDetailsVo.setChapterVoList(chapterTree); + + // 6. 查询考试信息 + if (course.getNeedExamAndIdentify() != null && course.getNeedExamAndIdentify().equals(1) && StringUtils.isNotEmpty(course.getExamId())) { + courseDetailsVo.setExam(getExamInfo(course.getExamId())); + } + + // 7. 查询鉴定信息 + if (course.getNeedExamAndIdentify() != null && course.getNeedExamAndIdentify().equals(1) && StringUtils.isNotEmpty(course.getIdentifyId())) { + courseDetailsVo.setIdentity(getIdentityInfo(course.getIdentifyId())); + } + + return courseDetailsVo; + } + + /** + * 查询课程章节列表 + */ + private List getCourseChapters(String courseId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivateCourseChapter::getCourseId, courseId); + queryWrapper.eq(FtbCultivateCourseChapter::getEnableMark, 0); + queryWrapper.orderByAsc(FtbCultivateCourseChapter::getSortCode); + return ftbCultivateCourseChapterMapper.selectList(queryWrapper); + } + + /** + * 构建章节树形结构 + */ + private List buildChapterTree(List chapters) { + if (CollUtil.isEmpty(chapters)) { + return new ArrayList<>(); + } + + // 转换为VO对象 + List chapterVos = chapters.stream().map(this::convertToChapterVo).collect(Collectors.toList()); + + // 构建父子关系映射 + Map> childrenMap = new HashMap<>(); + for (V2CultivateCourseChapterVo chapterVo : chapterVos) { + String parentId = "0".equals(chapterVo.getParentId()) ? "0" : chapterVo.getParentId(); + childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(chapterVo); + } + + // 设置子章节 + for (V2CultivateCourseChapterVo chapterVo : chapterVos) { + List children = childrenMap.get(chapterVo.getId()); + if (CollUtil.isNotEmpty(children)) { + chapterVo.setChildChapter(children); + } + } + + // 返回顶级章节 + return childrenMap.getOrDefault("0", new ArrayList<>()); + } + + /** + * 转换章节实体为VO + */ + private V2CultivateCourseChapterVo convertToChapterVo(FtbCultivateCourseChapter chapter) { + V2CultivateCourseChapterVo chapterVo = new V2CultivateCourseChapterVo(); + chapterVo.setId(chapter.getId()); + chapterVo.setCourseId(chapter.getCourseId()); + chapterVo.setName(chapter.getName()); + chapterVo.setSortCode(chapter.getSortCode()); + chapterVo.setAnnex(chapter.getAnnex()); + chapterVo.setAnnexName(chapter.getAnnexName()); + chapterVo.setDetailDesc(chapter.getDetailDesc()); + chapterVo.setDuration(chapter.getDuration()); + chapterVo.setWhetherTesting(chapter.getWhetherTesting()); + chapterVo.setForcedStudy(chapter.getForcedStudy()); + chapterVo.setChapterType(chapter.getChapterType()); + chapterVo.setParentId(chapter.getParentId()); + chapterVo.setSource(chapter.getSource()); + chapterVo.setPath(chapter.getPath()); + chapterVo.setVideoActionType(chapter.getVideoActionType()); + chapterVo.setSize(chapter.getSize()); + chapterVo.setLibraryId(chapter.getLibraryId()); + + // 查询随堂测试 + if (chapter.getWhetherTesting() != null && chapter.getWhetherTesting() == 0) { + List tests = getChapterTests(chapter.getId()); + chapterVo.setCultivateChapterTest(tests); + } + + return chapterVo; + } + + /** + * 查询章节随堂测试 + */ + private List getChapterTests(String chapterId) { + LambdaQueryWrapper testQuery = Wrappers.lambdaQuery(); + testQuery.eq(FtbCultivateChapterTest::getCourseChapterId, chapterId); + testQuery.eq(FtbCultivateChapterTest::getEnableMark, 0); + List tests = ftbCultivateChapterTestService.list(testQuery); + + if (CollUtil.isEmpty(tests)) { + return new ArrayList<>(); + } + + return tests.stream().map(test -> { + V2CultivateChapterTestAddDTO testDto = new V2CultivateChapterTestAddDTO(); + testDto.setTopicName(test.getTopicName()); + testDto.setAnswerAnalysis(test.getAnswerAnalysis()); + testDto.setForcedStudy(test.getForcedStudy()); + + // 查询测试选项 + List options = getTestOptions(test.getId()); + List optionDtos = options.stream().map(option -> { + FtbCultivateChapterTestOptionDTO optionDto = new FtbCultivateChapterTestOptionDTO(); + optionDto.setIsRightOption(option.getIsRightOption()); + optionDto.setContent(option.getContent()); + optionDto.setSortCode(option.getSortCode()); + return optionDto; + }).collect(Collectors.toList()); + testDto.setFetchChapterTestOptions(optionDtos); + + return testDto; + }).collect(Collectors.toList()); + } + + /** + * 查询测试选项 + */ + private List getTestOptions(String testId) { + LambdaQueryWrapper optionQuery = Wrappers.lambdaQuery(); + optionQuery.eq(FtbCultivateChapterTestOption::getChapterTestId, testId); + optionQuery.eq(FtbCultivateChapterTestOption::getEnableMark, 0); + optionQuery.orderByAsc(FtbCultivateChapterTestOption::getSortCode); + return ftbCultivateChapterTestOptionService.list(optionQuery); + } + + /** + * 查询考试信息 + */ + private AppCultivateCourseExamVo getExamInfo(String examId) { + // 这里需要调用考试服务查询考试信息 + // 由于没有具体的考试服务方法,暂时返回基础信息 + AppCultivateCourseExamVo examVo = new AppCultivateCourseExamVo(); + examVo.setExamId(examId); + // 可以通过FtbCultivateExamMapper查询考试名称等详细信息 + String examName = ftbCultivateCourseMapper.queryExamNameByExamId(examId); + examVo.setExamName(examName); + return examVo; + } + + /** + * 查询鉴定信息 + */ + private AppCultivateCourseIdentityVo getIdentityInfo(String identifyId) { + // 这里需要调用鉴定服务查询鉴定信息 + // 由于没有具体的鉴定服务方法,暂时返回基础信息 + AppCultivateCourseIdentityVo identityVo = new AppCultivateCourseIdentityVo(); + identityVo.setIdentityId(identifyId); + CultivateIdentifyTable table = cultivateIdentifyTableMapper.selectById(identifyId); + if (table != null) { + identityVo.setIdentityName(table.getName()); + } + // 鉴定名称可以通过其他方式查询 + return identityVo; + } + + /** + * 添加岗位学习OR任务时,课程列表选择 + * + * @param req + * @return {@link Page} + */ + @Override + public Page learnSelectCourseList(V2CultivateCourseSelectReq req) { + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + return ftbCultivateCourseMapper.jobLearningCourseList(page, req); + + } + + + /** + * 清空课程学习记录 + * + * @param courseId 课程ID + */ + @Override + public void clearCourseLog(String courseId) { + UserInfo user = UserProvider.getUser(); + String tenantId = user.getTenantId(); + CompletableFuture.runAsync(() -> { + userApiV2Util.checkOutTenant(tenantId); + FtbCultivateCourseSettingGlobal info = ftbCultivateCourseSettingGlobalService.getInfo(); + if (info == null || info.getCourseUpdate().equals(0)) { + ftbCultivatePositionCourceChapterLearningMapper.update(null, + new LambdaUpdateWrapper() + .eq(FtbCultivatePositionCourceChapterLearning::getCourceId, courseId) + .eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0) + .set(FtbCultivatePositionCourceChapterLearning::getLastVideoStudyTime, 0)); + return; + } + FtbCultivateCourse ftbCultivateCourse = ftbCultivateCourseMapper.selectById(courseId); + if (ftbCultivateCourse == null) { + return; + } + LambdaQueryWrapper queryCourseLearnWrapper = Wrappers.lambdaQuery(); + queryCourseLearnWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId) + .ne(FtbCultivatePositionCourceLearning::getState, 0) + .eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List allUserStudyList = ftbCultivatePositionCourceLearningMapper.selectList(queryCourseLearnWrapper); + + ftbCultivatePositionCourceLearningMapper.update(null, + new LambdaUpdateWrapper() + .eq(FtbCultivatePositionCourceLearning::getCourceId, courseId) + .eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0) + .set(FtbCultivatePositionCourceLearning::getState, 0)); + + ftbCultivatePositionCourceChapterLearningMapper.update(null, + new LambdaUpdateWrapper() + .eq(FtbCultivatePositionCourceChapterLearning::getCourceId, courseId) + .eq(FtbCultivatePositionCourceChapterLearning::getEnabledMark, 0) + .set(FtbCultivatePositionCourceChapterLearning::getLastVideoStudyTime, 0) + .set(FtbCultivatePositionCourceChapterLearning::getState, 0)); + ftbCultivateChapterTestResultMapper.update(null, + new LambdaUpdateWrapper() + .eq(FtbCultivateChapterTestResult::getCourseId, courseId) + .set(FtbCultivateChapterTestResult::getEnableMark, 1)); + if (CollUtil.isNotEmpty(allUserStudyList)) { + List cultivateCourseMsgLists = new ArrayList<>(); + for (FtbCultivatePositionCourceLearning item : allUserStudyList) { + CultivateCourseMsg cultivateCourseMsg = new CultivateCourseMsg(); + cultivateCourseMsg.setDesc("课程:" + ftbCultivateCourse.getName() + "章节发生变更,请根据要求重新学习"); + cultivateCourseMsg.setCourseId(ftbCultivateCourse.getId()); + cultivateCourseMsg.setState(0); + cultivateCourseMsg.setUserId(item.getUserId()); + cultivateCourseMsg.setStatus(0); + cultivateCourseMsgLists.add(cultivateCourseMsg); + } + cultivateCourseMsgService.saveBatch(cultivateCourseMsgLists); + } + }, cultivateThreadPool); + } + + @Override + public AiHelperCourseStatisticsVo getCourseStatistics(String courseId) { + if (StringUtils.isEmpty(courseId)) { + throw new RuntimeException("课程ID不能为空"); + } + AiHelperCourseStatisticsVo statistics = ftbCultivateCourseMapper.getCourseStatistics(courseId); + if (statistics == null) { + statistics = new AiHelperCourseStatisticsVo(); + statistics.setCompletedCount(0); + statistics.setLearningCount(0); + } + return statistics; + } + + /** + * 查询指定用户的学习情况 + * + * @param userId 用户ID + * @return 用户学习情况(包含已完成的课程、考试、鉴定列表,已去重) + */ + @Override + public UserLearningStatusVo getUserLearningStatus(String userId) { + if (StringUtils.isEmpty(userId)) { + throw new RuntimeException("用户ID不能为空"); + } + + UserLearningStatusVo result = new UserLearningStatusVo(); + result.setUserId(userId); + + // 1. 查询已完成的课程(使用关联查询,一次性获取课程信息) + List completedCourses = ftbCultivatePositionCourceLearningMapper.queryCompletedCoursesWithInfo(userId); + result.setCompletedCourses(completedCourses); + + // 2. 查询已完成的考试(使用关联查询,一次性获取考试信息,并按完成时间倒序) + List completedExams = ftbCultivateExamUserMapper.queryCompletedExamsWithInfo(userId); + result.setCompletedExams(completedExams); + + // 3. 查询已完成的鉴定(使用关联查询,一次性获取鉴定信息,并按鉴定时间倒序) + List completedIdentities = cultivateIdentifyApplyMapper.queryCompletedIdentitiesWithInfo(userId); + result.setCompletedIdentities(completedIdentities); + + return result; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamServiceImpl.java new file mode 100644 index 0000000..a788e02 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamServiceImpl.java @@ -0,0 +1,1567 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.authority.FtbAuthorityApi; +import jnpf.base.ActionResult; +import jnpf.base.service.SuperServiceImpl; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.utils.UserExamUtil; +import jnpf.cultivate.v2.service.CultivateExamDrawRuleService; +import jnpf.cultivate.v2.service.V2CultivateExamService; +import jnpf.cultivate.v2.util.CultivateMqSendUtil; +import jnpf.cultivate.v2.util.ExamChangeDetector; +import jnpf.cultivate.v2.util.ExamChangeResult; +import jnpf.culture.util.DateRangeUtil; +import jnpf.exception.HandleException; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamFrequencyLog; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourseExam; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionBank; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; +import jnpf.model.cultivate.po.teaching.TeachingApprove; +import jnpf.model.cultivate.req.exam.SubExamQuestionReq; +import jnpf.model.cultivate.req.exam.WebRestartExamReq; +import jnpf.model.cultivate.resp.AppQuestionVo; +import jnpf.model.cultivate.resp.ExamPaperVo; +import jnpf.model.cultivate.resp.PaperDrawRuleVo; +import jnpf.model.cultivate.resp.SubExamVo; +import jnpf.model.cultivate.v2.enums.ExamStatusEnum; +import jnpf.model.cultivate.v2.enums.FrequencyEnum; +import jnpf.model.cultivate.v2.enums.MyExamStatusEnum; +import jnpf.model.cultivate.v2.enums.QuestionTypeEnum; +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.exam.req.*; +import jnpf.model.cultivate.v2.exam.vo.*; +import jnpf.model.enums.CourseEnums; +import jnpf.model.enums.ExamUpdateStatus; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 考试服务v2实现 + * + * @author yanwenfu + * @create 2026-02-27 + */ +@Service +public class V2CultivateExamServiceImpl extends SuperServiceImpl implements V2CultivateExamService { + + @Resource + private CultivateExamDrawRuleService examDrawRuleService; + @Resource + private FtbCultivatePositionCourseExamMapper positionCourseExamMapper; + @Resource + private UserApiV2Util userApiV2Util; + @Resource + private FtbCultivateExamService examService; + @Resource + private FtbCultivateExamUserService examUserService; + @Resource + private FtbCultivateExamUserDetailService examUserDetailService; + @Resource + private FtbCultivateExamUserMapper examUserMapper; + @Resource + private TeachingRecordService teachingRecordService; + @Resource + private TeachingApproveMapper teachingApproveMapper; + @Resource + private FtbCultivateQuestionMapper questionMapper; + @Resource + private FtbCultivateQuestionOptionMapper questionOptionMapper; + @Resource + private FtbCultivateQuestionBankMapper questionBankMapper; + @Resource + private ExamFrequencyLogService frequencyLogService; + @Resource + private UserExamUtil userExamUtil; + @Resource + private CultivateMqSendUtil cultivateMqSendUtil; + @Autowired + private V2UserApi v2UserApi; + @Autowired + private FtbAuthorityApi authorityApi; + + @Override + public PageInfo getPage(V2QueryExamReq req) { + + return PageHelper.startPage(req.getCurrentPage(), req.getPageSize()) + .doSelectPageInfo(() -> baseMapper.selectListV2( + req.getExamName(), + req.getExamType(), + req.getNeedUpdate(), + req.getLimitedDate() + )); + } + + @Override + public PageInfo getMyExamWebPage(MyExamWebQueryDto queryDto) { + + return PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()) + .doSelectPageInfo(() -> baseMapper.getMyExamWebList( + queryDto.getUserId() + )); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addExam(V2SaveExamReq req) throws Exception { + + // req数据验证 + examReqCheck(req, null); + CultivateExam exam = JsonUtil.getJsonToBean(req, CultivateExam.class); + exam.setId(FtbUtil.getId()); + exam.setExamId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.EXAM)); + exam.setStatus(calExamStatus(req)); + exam.setEnabledMark(ConstantUtil.NUM_TRUE); + exam.setNeedUpdate(ConstantUtil.UPDATE_FALSE); + exam.setCreatorUserId(UserProvider.getLoginUserId()); + exam.setCreatorTime(new Date()); + exam.setLastModifyUserId(exam.getCreatorUserId()); + exam.setLastModifyTime(exam.getCreatorTime()); + // 抽题题库 + exam.setDrawDataBase(String.join(",", req.getDrawDataBaseList())); + // 抽题规则处理 + List ruleList = getExamDrawRuleList(req.getRandomDrawRule(), exam.getId(), req.getDrawMode(), exam.getCreatorUserId(), exam.getLastModifyUserId()); + List ruleVoList = getRuleListChange(ruleList, exam); + // 验证抽题是否可以满足 + List abnormalRuleList = getAbnormalRuleList(ruleVoList); + if (!abnormalRuleList.isEmpty()) { + List questionTypeList = abnormalRuleList.stream().map(ConnectDrawRuleVo::getQuestionType).collect(Collectors.toList()); + throw new HandleException("抽题规则异常: " + getQuestionTypeStr(questionTypeList) + " 数量异常"); + } + // 考试成员初始化exam_user数据 + if (exam.getExamType().equals(ConstantUtil.EXAM_TYPE_CUSTOM)) { + insertExamUserBatch(new ArrayList<>(normalize(exam.getExamMemberId())), exam, ruleList); + } + this.save(exam); + examDrawRuleService.saveBatch(ruleList); + } + + private Integer calExamStatus(V2SaveExamReq req) { + // 岗位考试始终为进行中状态 + if (req.getExamType().equals(ConstantUtil.EXAM_TYPE_POST)) { + return ExamStatusEnum.IN_PROGRESS.getCode(); + } + // 自定义考试 + Date now = new Date(); + // 无时间限制的自定义考试,默认为进行中 + if (req.getStartTime() == null && req.getEndTime() == null) { + return ExamStatusEnum.IN_PROGRESS.getCode(); + } + + // 有开始时间和结束时间的考试,根据当前时间判断状态 + if (req.getStartTime() != null && req.getEndTime() != null) { + // 开始时间在未来,未开始 + if (now.before(req.getStartTime())) { + return ExamStatusEnum.NOT_STARTED.getCode(); + } + // 在当前时间范围内,进行中 + if ((now.equals(req.getStartTime()) || now.after(req.getStartTime())) + && (now.equals(req.getEndTime()) || now.before(req.getEndTime()))) { + return ExamStatusEnum.IN_PROGRESS.getCode(); + } + // 结束时间已过,已结束 + if (now.after(req.getEndTime())) { + return ExamStatusEnum.FINISHED.getCode(); + } + } + + // 只有开始时间或只有结束时间的异常情况,默认未开始 + return ExamStatusEnum.NOT_STARTED.getCode(); + } + + private String getQuestionTypeStr(List questionTypeList) { + + StringBuilder sb = new StringBuilder(); + questionTypeList.forEach(v -> { + QuestionTypeEnum questionTypeEnum = QuestionTypeEnum.getByCode(v); + if (null != questionTypeEnum) { + sb.append(questionTypeEnum.getDescription()).append(","); + } + }); + return sb.length() != 0 ? sb.substring(0, sb.length() - 1) : ""; + } + + private List getRuleListChange(List ruleList, CultivateExam exam) { + + if (null == ruleList || ruleList.isEmpty()) { + return List.of(); + } + List list = new ArrayList<>(); + ruleList.forEach(v -> { + ExamDrawRuleVo bean = JsonUtil.getJsonToBean(v, ExamDrawRuleVo.class); + bean.setDrawDataBase(exam.getDrawDataBase()); + list.add(bean); + }); + return list; + } + + private void examReqCheck(V2SaveExamReq req, String id) throws HandleException { + // 考试名称不能重复 + long count = getRecordCountByName(req.getExamName(), id); + if (count > 0) { + throw new HandleException("考试名称已存在"); + } + // 开始时间和结束时间不能只有一个为空 + if (ConstantUtil.EXAM_TYPE_POST.equals(req.getExamType())) { + req.setStartTime(null); + req.setEndTime(null); + } else { + // 自定义考试时 + if ((req.getStartTime() != null && req.getEndTime() == null) || (req.getStartTime() == null && req.getEndTime() != null)) { + throw new HandleException("自定义考试开始时间或结束时间不能为空"); + } + // 需要选择考试成员 + if (StringUtil.isEmpty(req.getExamMemberId())) { + throw new HandleException("请选择参加自定义考试的成员"); + } + } + // 是否补考为是时, 需要设置补考频次 + if (req.getExamLimitation().equals(ConstantUtil.NUM_TRUE)) { + if (req.getReExamFrequencyType() == null) { + throw new HandleException("请选择补考频次"); + } + if (!req.getReExamFrequencyType().equals(FrequencyEnum.UNLIMITED.getCode()) && null == req.getReExamFrequencyNum()) { + throw new HandleException("请选择补考次数"); + } + } + } + + /** + * 根据考试名称查询记录数 + * + * @param examName 考试名称 + * @param id 考试id + * @return long + */ + private long getRecordCountByName(String examName, String id) { + + return this.count(new LambdaQueryWrapper() + .eq(CultivateExam::getExamName, examName) + .eq(CultivateExam::getEnabledMark, ConstantUtil.NUM_TRUE) + .ne(StringUtil.isNotEmpty(id), CultivateExam::getId, id)); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateExam(String id, V2SaveExamReq req) throws Exception { + + CultivateExam exam = this.getById(id); + if (null == exam) { + throw new HandleException("未找到该考试"); + } + if (!CourseEnums.ExamType.POSITION.getCode().equals(exam.getExamType()) + && null != exam.getStatus() && !exam.getStatus().equals(ExamStatusEnum.NOT_STARTED.getCode())) { + throw new HandleException("您只能编辑尚未开始的考试"); + } + // req数据验证 + examReqCheck(req, id); + List oldRuleList = examDrawRuleService.list(new LambdaQueryWrapper() + .eq(CultivateExamDrawRule::getExamId, exam.getId()) + .eq(CultivateExamDrawRule::getDeleteMark, ConstantUtil.NUM_FALSE) + ); + List ruleList = getExamDrawRuleList(req.getRandomDrawRule(), exam.getId(), req.getDrawMode(), exam.getCreatorUserId(), UserProvider.getLoginUserId()); + ExamChangeResult detect = ExamChangeDetector.detect(exam, req, oldRuleList, ruleList); + if (detect.isChanged()) { + exam.setVersionBatch(FtbUtil.getId()); + } + // 成员变更 + String oldMembers = exam.getExamMemberId(); + BeanUtils.copyProperties(req, exam); + // 设置其他字段的值 + exam.setLastModifyUserId(UserProvider.getLoginUserId()); + exam.setLastModifyTime(new Date()); + // 抽题题库 + exam.setDrawDataBase(String.join(",", req.getDrawDataBaseList())); + // 抽题规则处理 + List ruleVoList = getRuleListChange(ruleList, exam); + // 验证抽题是否可以满足 + List abnormalRuleList = getAbnormalRuleList(ruleVoList); + if (!abnormalRuleList.isEmpty()) { + List questionTypeList = abnormalRuleList.stream().map(ConnectDrawRuleVo::getQuestionType).collect(Collectors.toList()); + throw new HandleException("抽题规则异常: " + getQuestionTypeStr(questionTypeList) + " 数量异常"); + } else { + if (exam.getNeedUpdate().equals(ConstantUtil.UPDATE_TRUE)) { + exam.setNeedUpdate(ConstantUtil.UPDATE_FALSE); + } + } + Set oldSet = normalize(oldMembers); + Set newSet = normalize(req.getExamMemberId()); + // 删除的成员:old - new + Set removed = new HashSet<>(oldSet); + removed.removeAll(newSet); + // 待考试的考卷, 删除后生成新的版本 + Set stayed = new HashSet<>(oldSet); + stayed.removeAll(removed); + // 查询保持不变的中待考试的考卷 + List stayList; + if (stayed.isEmpty()) { + stayList = new ArrayList<>(); + } else { + stayList = examUserService.list(new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getExamId, exam.getId()) + .in(FtbCultivateExamUser::getUserId, stayed) + .eq(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_TRUE)); + } + Set update = stayList.stream().map(FtbCultivateExamUser::getUserId).collect(Collectors.toSet()); + // 变更的考试成员初始化exam_user数据, 删除后重新添加 + if (exam.getExamType().equals(ConstantUtil.EXAM_TYPE_CUSTOM)) { + // 新增的成员:new - old + Set added = new HashSet<>(newSet); + added.removeAll(oldSet); + // 岗位考试编辑考试信息时不会生成新的exam_user数据 + removed.addAll(update); + if (!removed.isEmpty()) { + removeExamUser(exam.getId(), removed); + } + added.addAll(update); + if (!added.isEmpty()) { + insertExamUserBatch(new ArrayList<>(added), exam, ruleList); + } + } else { + if (!update.isEmpty()) { + removeExamUser(exam.getId(), update); + insertExamUserBatch(new ArrayList<>(update), exam, ruleList); + } + } + exam.setStatus(calExamStatus(req)); + this.updateById(exam); + if (detect.isRuleChanged()) { + examDrawRuleService.remove(new LambdaQueryWrapper().eq(CultivateExamDrawRule::getExamId, id)); + examDrawRuleService.saveBatch(ruleList); + } + } + + private void removeExamUser(String id, Set removed) { + + examUserService.update(new LambdaUpdateWrapper() + .eq(FtbCultivateExamUser::getExamId, id) + .in(FtbCultivateExamUser::getUserId, removed) + .set(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_FALSE) + .set(FtbCultivateExamUser::getDeleteUserId, UserProvider.getLoginUserId()) + .set(FtbCultivateExamUser::getDeleteTime, new Date()) + ); + } + + private static Set normalize(String str) { + if (str == null || str.trim().isEmpty()) { + return Collections.emptySet(); + } + + return Arrays.stream(str.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet()); + } + + private void insertExamUserBatch(List userIds, CultivateExam exam, List ruleList) { + + List list = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId()); + if (!list.isEmpty()) { + List examUserList = new ArrayList<>(); + int questionNum = 0; + int totalScore = 0; + for (CultivateExamDrawRule drawRule : ruleList) { + questionNum += drawRule.getPickCount(); + totalScore += drawRule.getScore() * drawRule.getPickCount(); + } + // List logList = new ArrayList<>(); + for (UserBoundVO user : list) { + FtbCultivateExamUser examUser = new FtbCultivateExamUser(exam.getId(), exam.getExamTime(), + calculateScore(totalScore, exam.getPassType(), exam.getPassMark()), calculateScore(totalScore, exam.getExcellentType(), exam.getExcellentMark()), + user.getId(), exam.getStartTime(), exam.getEndTime(), + user.getOrganizeId(), totalScore, questionNum, JsonUtil.getObjectToString(ruleList)); + examUser.setVersionBatch(exam.getVersionBatch()); + examUserList.add(examUser); + // FtbCultivateExamFrequencyLog log = new FtbCultivateExamFrequencyLog(FtbUtil.getId(), exam.getId(), user.getId(), UserProvider.getLoginUserId()); + // logList.add(log); + } + examUserService.saveBatch(examUserList); + // frequencyLogService.saveBatch(logList); + } + } + + private BigDecimal calculateScore(int totalScore, Integer type, Integer mark) { + + if (type.equals(1)) { + return new BigDecimal(mark).setScale(2, RoundingMode.HALF_UP); + } + return BigDecimal.valueOf(totalScore) + .multiply(BigDecimal.valueOf(mark)) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } + + private List getExamDrawRuleList(List randomDrawRule, String examId, + Integer drawMode, String createUser, String updateUser) throws Exception { + + List ruleList = JsonUtil.getJsonToList(randomDrawRule, CultivateExamDrawRule.class); + for (CultivateExamDrawRule v : ruleList) { + v.setId(FtbUtil.getId()); + v.setExamId(examId); + if (drawMode.equals(ConstantUtil.DRAW_MODE_RANDOM)) { + if (null == v.getEasyCount()) { + v.setEasyCount(0); + } + if (null == v.getMediumCount()) { + v.setMediumCount(0); + } + if (null == v.getHardCount()) { + v.setHardCount(0); + } + int totalCount = v.getEasyCount() + v.getMediumCount() + v.getHardCount(); + if (totalCount != v.getPickCount()) { + throw new HandleException("抽题数量异常"); + } + } + v.setCreatorUserId(createUser); + v.setLastModifyUserId(updateUser); + } + return ruleList; + } + + @Override + public V2ExamVo getDetail(String id, Integer copy) { + + CultivateExam exam = this.getById(id); + V2ExamVo vo = JsonUtil.getJsonToBean(exam, V2ExamVo.class); + String[] split = exam.getDrawDataBase().split(","); + vo.setDrawDataBaseList(Arrays.asList(split)); + // 抽题规则查询 + List list = examDrawRuleService.list(new LambdaQueryWrapper().eq(CultivateExamDrawRule::getExamId, vo.getId())); + List ruleList = JsonUtil.getJsonToList(list, V2RandomDrawRuleReq.class); + if (!ruleList.isEmpty() && StringUtil.isNotEmpty(ruleList.get(0).getQuestionId())) { + List questionIds = ruleList.stream().map(V2RandomDrawRuleReq::getQuestionId).collect(Collectors.toList()); + List questionList = questionMapper.selectList(new LambdaQueryWrapper().in(FtbCultivateQuestion::getId, questionIds)); + Map questionMap = questionList.stream().collect(Collectors.toMap(FtbCultivateQuestion::getId, Function.identity())); + ruleList.forEach(v -> { + FtbCultivateQuestion question = questionMap.get(v.getQuestionId()); + if (null != question) { + v.setQuestionName(question.getContent()); + } + }); + } + vo.setRandomDrawRule(ruleList); + if (ConstantUtil.NUM_TRUE == copy) { + vo.setId(null); + vo.setExamId(null); + vo.setExamName(null); + } + return vo; + } + + @Override + public V2ExamDetailVo getMarkDetail(String id) { + + CultivateExam exam = this.getById(id); + V2ExamDetailVo vo = JsonUtil.getJsonToBean(exam, V2ExamDetailVo.class); + // 相关岗位查询 postNames + vo.setPostNames(getPostInfoList(id)); + // 查询抽题规则 获取总题目数、总分数 + List list = examDrawRuleService.list(new LambdaQueryWrapper().eq(CultivateExamDrawRule::getExamId, vo.getId())); + if (!list.isEmpty()) { + int totalQuestionNum = 0; + int totalScore = 0; + for (CultivateExamDrawRule rule : list) { + totalQuestionNum += rule.getPickCount(); + totalScore += rule.getScore() * rule.getPickCount(); + } + vo.setTotalQuestionNum(totalQuestionNum); + vo.setTotalScore(totalScore); + } + return vo; + } + + private List getPostInfoList(String id) { + + List postList = positionCourseExamMapper.selectList(new LambdaQueryWrapper() + .eq(FtbCultivatePositionCourseExam::getEnabledMark, ConstantUtil.NUM_TRUE) + .eq(FtbCultivatePositionCourseExam::getExamId, id)); + if (postList.isEmpty()) { + return List.of(); + } + Set postSet = new HashSet<>(); + Set gradeSet = new HashSet<>(); + postList.forEach(v -> { + if (StringUtil.isNotEmpty(v.getPostRankId())) { + postSet.add(v.getPostRankId()); + } + if (StringUtil.isNotEmpty(v.getGradeId())) { + gradeSet.add(v.getGradeId()); + } + }); + String tenantId = UserProvider.getUser().getTenantId(); + Map postMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(new ArrayList<>(postSet), tenantId); + Map gradeMap = userApiV2Util.listGradeByIdsReturnMap(new ArrayList<>(gradeSet), tenantId); + List postInfoList = new ArrayList<>(); + postList.forEach(v -> { + PositionVO post = postMap.get(v.getPostRankId()); + String postInfo = post.getFullName(); + if (!StringUtil.isEmpty(v.getGradeId())) { + GradeVO grade = gradeMap.get(v.getGradeId()); + if (null != grade) { + postInfo += "(" + grade.getFullName() + ")"; + } + } + postInfoList.add(postInfo); + }); + return postInfoList; + } + + @Override + public PageInfo getMarkDetailPage(MarkDetailQueryReq req) { + List userIds = new ArrayList<>(); + if (StringUtil.isNotEmpty(req.getKeyword())) { + List list = v2UserApi.userListAndCopyLikeName(req.getKeyword(), null, UserProvider.getUser().getTenantId()); + if (null == list || list.isEmpty()) { + return new PageInfo<>(); + } + userIds = list.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + // 获取用户权限 + List authUserList = getAuthUserList(); + // 查询考试版本号 + FtbCultivateExam exam = examService.getById(req.getId()); + PageHelper.startPage(req.getCurrentPage(), req.getPageSize()); + PageInfo page = new PageInfo<>(examUserMapper.getExamUserList(req.getId(), exam.getVersionBatch(), userIds, req.getScoreSort())); + if (CollUtil.isNotEmpty(page.getList())) { + userIds = page.getList().stream().map(V2TestPaperVo::getUserId).collect(Collectors.toList()); + authUserList.retainAll(new HashSet<>(userIds)); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + page.getList().forEach(v -> { + UserBoundVO user = userMap.get(v.getUserId()); + if (authUserList.contains(v.getUserId())) { + v.setHasMarkAuth(ConstantUtil.NUM_TRUE); + } else { + v.setHasMarkAuth(ConstantUtil.NUM_FALSE); + } + if (null != user) { + v.setUserName(user.getUserName()); + v.setPostId(user.getPositionId()); + v.setPostName(user.getPositionName()); + v.setOrganizeId(user.getOrganizeId()); + v.setOrganizeName(user.getOrganizeName()); + v.setGradeId(user.getGradeId()); + v.setGradeName(user.getGradeName()); + } + }); + } + return page; + } + + private List getAuthUserList() { + + List authUserList; + ActionResult> authList = authorityApi.authGetPermissionScopeUserIds(); + if (null == authList || CollUtil.isEmpty(authList.getData())) { + authUserList = new ArrayList<>(); + } else { + authUserList = authList.getData(); + } + return authUserList; + } + + @Override + public void delExam(String id) throws Exception { + + CultivateExam exam = this.getById(id); + if (null == exam) { + throw new HandleException("未找到该考试"); + } + if (!exam.getExamType().equals(CourseEnums.ExamType.POSITION.getCode()) && null != exam.getStatus() && !exam.getStatus().equals(ExamStatusEnum.NOT_STARTED.getCode()) + && !exam.getStatus().equals(ExamStatusEnum.REVOKE.getCode())) { + throw new HandleException("当前考试状态无法被删除"); + } + this.update(new LambdaUpdateWrapper() + .eq(CultivateExam::getId, id) + .set(CultivateExam::getEnabledMark, ConstantUtil.NUM_FALSE) + .set(CultivateExam::getDeleteTime, new Date()) + .set(CultivateExam::getDeleteUserId, UserProvider.getLoginUserId())); + // 删除还未考试, 或已逾期的exam_user + examUserService.update(new LambdaUpdateWrapper() + .eq(FtbCultivateExamUser::getExamId, id) + .in(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode(), CourseEnums.ExamStatus.OVERDUE.getCode()) + .set(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_FALSE) + .set(FtbCultivateExamUser::getDeleteTime, new Date()) + .set(FtbCultivateExamUser::getDeleteUserId, UserProvider.getLoginUserId())); + } + + @Override + public PageInfo myExamList(V2AppQueryExamListReq req) { + + return PageHelper.startPage(req.getCurrentPage(), req.getPageSize()) + .doSelectPageInfo(() -> examUserMapper.getMyExamList( + req.getStatus(), + UserProvider.getLoginUserId() + )); + } + + @Override + public MyExamTabCountVo getMyExamTabCount() { + + Integer waitExamNum = examUserMapper.getStatusCount(CourseEnums.ExamStatus.WAIT.getCode(), UserProvider.getLoginUserId()); + Integer passExamNum = examUserMapper.getStatusCount(CourseEnums.ExamStatus.PASS.getCode(), UserProvider.getLoginUserId()); + Integer failExamNum = examUserMapper.getStatusCount(CourseEnums.ExamStatus.NO_PASS.getCode(), UserProvider.getLoginUserId()); + return new MyExamTabCountVo(null == waitExamNum ? 0 : waitExamNum, + null == passExamNum ? 0 : passExamNum, + null == failExamNum ? 0 : failExamNum + ); + } + + @Override + public MyExamDetailVo myExamDetail(String userExamId) { + + // 考试信息 + MyExamDetailVo detail = examUserMapper.getMyExamDetail(userExamId); + if (detail == null) { + return null; + } + // 考试结果 + MyExamStatusEnum myStatus = MyExamStatusEnum.getByCode(detail.getMyExamStatus()); + if (null != myStatus) { + if (myStatus.equals(MyExamStatusEnum.QUALIFIED) || myStatus.equals(MyExamStatusEnum.UNQUALIFIED) || myStatus.equals(MyExamStatusEnum.EXCELLENT)) { + ExamResultVo examResult = examUserMapper.getExamResult(userExamId); + // 计算正确占比 + if (null != examResult.getCorrectCount()) { + BigDecimal correctRate; + if (examResult.getCorrectCount().equals(0)) { + correctRate = BigDecimal.ZERO; + } else { + correctRate = BigDecimal.valueOf(examResult.getCorrectCount()) + .divide(BigDecimal.valueOf(examResult.getQuestionNum()), 2, RoundingMode.HALF_UP) + .multiply(new BigDecimal(100)); + } + // 计算错误占比 + BigDecimal errorRate = BigDecimal.valueOf(100).subtract(correctRate); + examResult.setCorrectRate(correctRate); + examResult.setErrorRate(errorRate); + } + // 查询用户信息 + List list = v2UserApi.userListAndCopy(List.of(examResult.getUserId()), null, UserProvider.getUser().getTenantId()); + if (null != list && !list.isEmpty()) { + examResult.setUserName(list.get(0).getUserName()); + } + detail.setExamResult(examResult); + } + // 是否可以考试 + int couldExam = ConstantUtil.NUM_FALSE; + if (myStatus.equals(MyExamStatusEnum.WAIT)) { + if (null == detail.getStartTime() && null == detail.getEndTime()) { + couldExam = ConstantUtil.NUM_TRUE; + } else { + if (DateDetail.checkTimeBetween(new Date(), detail.getStartTime(), detail.getEndTime())) { + couldExam = ConstantUtil.NUM_TRUE; + } + } + } + detail.setCouldExam(couldExam); + } + // 抽题规则 + if (StringUtil.isNotEmpty(detail.getPickQuestionRule())) { + List ruleList = JsonUtil.getJsonToList(detail.getPickQuestionRule(), ExamDrawRuleVo.class); + List result = ruleList.stream() + .collect(Collectors.groupingBy(ExamDrawRuleVo::getQuestionType)) + .values() + .stream() + .map(list -> { + ExamDrawRuleVo vo = new ExamDrawRuleVo(); + vo.setQuestionType(list.get(0).getQuestionType()); + int totalCount = list.stream() + .mapToInt(ExamDrawRuleVo::getPickCount) + .sum(); + vo.setPickCount(totalCount); + return vo; + }) + .collect(Collectors.toList()); + detail.setDrawRuleList(result); + detail.setPickQuestionRule(null); + } + return detail; + } + + @Override + public List getExamQuestionList(String userExamId) throws Exception { + + MyExamDetailVo detail = examUserMapper.getMyExamDetail(userExamId); + if (detail == null) { + return List.of(); + } + CultivateExam exam = this.getById(detail.getExamId()); + if (null == exam) { + return List.of(); + } + if (exam.getNeedUpdate().equals(ConstantUtil.UPDATE_TRUE)) { + throw new HandleException("考试异常"); + } + // 抽题规则 + List ruleList = examDrawRuleService.list(new LambdaQueryWrapper().eq(CultivateExamDrawRule::getExamId, exam.getId())); + if (ruleList.isEmpty()) { + return List.of(); + } + List questionList; + if (exam.getDrawMode().equals(ConstantUtil.DRAW_MODE_RANDOM)) { + // 随机抽题 + questionList = questionMapper.getRandQuestionByRule(ruleList, exam.getDrawDataBase()); + if (questionList.isEmpty()) { + throw new HandleException("抽题异常"); + } + List questionIds = questionList.stream().map(V2AppQuestionVo::getQuestionId).collect(Collectors.toList()); + // ruleList为空, 因为sql里已经设置了score的值 + setOptionList(questionList, questionIds, null); + } else { + // 固定抽题 + List questionIds = ruleList.stream().map(CultivateExamDrawRule::getQuestionId).collect(Collectors.toList()); + questionList = questionMapper.selectListById(questionIds); + if (questionList.isEmpty() || questionList.size() != questionIds.size()) { + throw new HandleException("抽题异常"); + } + setOptionList(questionList, questionIds, ruleList); + } + return questionList; + } + + private void setOptionList(List questionList, List questionIds, List ruleList) { + + Map ruleMap; + if (null != ruleList) { + ruleMap = ruleList.stream().collect(Collectors.toMap(CultivateExamDrawRule::getQuestionId, Function.identity())); + } else { + ruleMap = new HashMap<>(); + } + // 查询选项 + List optionList = questionOptionMapper.selectList(new LambdaQueryWrapper() + .in(FtbCultivateQuestionOption::getQuestionId, questionIds) + .eq(FtbCultivateQuestionOption::getEnabledMark, ConstantUtil.NUM_TRUE) + .orderByDesc(FtbCultivateQuestionOption::getQuestionId) + .orderByAsc(FtbCultivateQuestionOption::getSortCode)); + Map> optionMap = optionList.stream().collect(Collectors.groupingBy(FtbCultivateQuestionOption::getQuestionId)); + questionList.forEach(question -> { + List oList = optionMap.get(question.getQuestionId()); + if (null != oList && !oList.isEmpty()) { + question.setQuestionOptionVoList(JsonUtil.getJsonToList(oList, V2QuestionOptionVo.class)); + } + CultivateExamDrawRule rule = ruleMap.get(question.getQuestionId()); + if (null != rule) { + question.setQuestionScore(rule.getScore()); + } + }); + } + + @Override + public PageInfo readOverExamList(QueryReadOverExamListReq req) { + + // 获取用户权限 + List authUserList = getAuthUserList(); + // 组织过滤 + MutablePair, Map> pair = getOrgQueryFilter(req.getOrganizeList()); + if (null == pair) { + return new PageInfo<>(); + } + authUserList.retainAll(pair.getLeft()); + if (authUserList.isEmpty()) { + return new PageInfo<>(); + } + pair.setLeft(authUserList); + PageInfo page = PageHelper.startPage(req.getCurrentPage(), req.getPageSize()) + .doSelectPageInfo(() -> examUserMapper.readOverExamList( + req.getKeyword(), + req.getStatus(), + pair.getLeft() + )); + if (!page.getList().isEmpty()) { + List userIdList = page.getList().stream().map(ReadOverExamVo::getUserId).collect(Collectors.toList()); + updateUserMap(pair, userIdList); + for (ReadOverExamVo vo : page.getList()) { + UserBoundVO user = pair.getRight().get(vo.getUserId()); + if (null != user) { + vo.setUserName(user.getUserName()); + } + } + } + return page; + } + + private void updateUserMap(MutablePair, Map> pair, List userIds) { + + if (null == pair.getRight()) { + List list = v2UserApi.userListAndCopy(userIds, null, UserProvider.getUser().getTenantId()); + if (null == list || list.isEmpty()) { + pair.setRight(new HashMap<>()); + } else { + pair.setRight( + list.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())) + ); + } + } + } + + private MutablePair, Map> getOrgQueryFilter(List organizeList) { + + List userIds; + Map userMap; + if (null != organizeList && !organizeList.isEmpty()) { + ActionResult> result = v2UserApi.listTargetOrganizesOrHaveChild(organizeList, false, null, UserProvider.getUser().getTenantId()); + if (null == result || null == result.getData() || result.getData().isEmpty()) { + return null; + } + userIds = result.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + userMap = result.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + } else { + userIds = new ArrayList<>(); + userMap = null; + } + return MutablePair.of(userIds, userMap); + } + + @Override + public ReadOverTabCountVo getReadOverTabCount(List organizeList) { + + // 获取用户权限 + List authUserList = getAuthUserList(); + MutablePair, Map> pair = getOrgQueryFilter(organizeList); + if (null == pair) { + return new ReadOverTabCountVo(0); + } + authUserList.retainAll(pair.getLeft()); + if (authUserList.isEmpty()) { + return new ReadOverTabCountVo(0); + } + pair.setLeft(authUserList); + Integer waitCheckExamNum = examUserMapper.getWaitCheckCount(CourseEnums.ExamStatus.WAIT_CHECK.getCode(), pair.getLeft()); + return new ReadOverTabCountVo(waitCheckExamNum); + } + + @Override + public UserExamRankVo getExamRankList(ExamRankReq req) { + + // 组织过滤 + MutablePair, Map> pair = getOrgQueryFilter(req.getOrganizeList()); + if (null == pair) { + return null; + } + UserExamRankVo rankVo = new UserExamRankVo(); + List allList = examUserMapper.getUserExamRankList(req.getExamId(), pair.getLeft()); + // 手动分页 + int pageNum = req.getCurrentPage(); + int pageSize = req.getPageSize(); + int start = (pageNum - 1) * pageSize; + int end = Math.min(start + pageSize, allList.size()); + List pageList = start >= allList.size() ? Collections.emptyList() : allList.subList(start, end); + // 构建 PageInfo + PageInfo page = new PageInfo<>(); + page.setList(pageList); + page.setTotal(allList.size()); + page.setPageNum(pageNum); + page.setPageSize(pageSize); + if (!page.getList().isEmpty()) { + // 填写用户信息 + List userIdList = page.getList().stream().map(V2AppExamRankingVo::getUserId).collect(Collectors.toList()); + updateUserMap(pair, userIdList); + for (V2AppExamRankingVo vo : page.getList()) { + UserBoundVO user = pair.getRight().get(vo.getUserId()); + setUserInfo(user, vo); + } + } + PageListVO returnPage = new PageListVO<>(); + returnPage.setList(page.getList()); + returnPage.setPagination(FtbUtil.getPagination(page)); + rankVo.setPage(returnPage); + V2AppExamRankingVo myRank = allList.stream().filter(v -> v.getUserId().equals(UserProvider.getLoginUserId())).findFirst().orElse(null); + if (null != myRank) { + List list = v2UserApi.userListAndCopy(List.of(myRank.getUserId()), null, UserProvider.getUser().getTenantId()); + if (null != list && !list.isEmpty()) { + setUserInfo(list.get(0), myRank); + } + rankVo.setMyRank(myRank); + } + return rankVo; + } + + private void setUserInfo(UserBoundVO user, V2AppExamRankingVo vo) { + + if (null != user) { + vo.setUserName(user.getUserName()); + vo.setHeadLogo(UploaderUtil.uploaderImg(StringUtil.isBlank(user.getHeadIcon()) ? "001.png" : user.getHeadIcon())); + V2PostAndPosition position = V2PostAndPosition.builder() + .organizeId(user.getOrganizeId()) + .organizeName(user.getOrganizeName()) + .postId(user.getPositionId()) + .postName(user.getPositionName()) + .gradeId(user.getGradeId()) + .gradeName(user.getGradeName()) + .build(); + vo.setPostAndPosition(position); + } + } + + @Override + public BankAnalysisVo getBankAnalysis(List bankIdList) { + + List bankList = questionBankMapper.selectList(new LambdaQueryWrapper() + .in(FtbCultivateQuestionBank::getId, bankIdList) + .eq(FtbCultivateQuestionBank::getEnabledMark, ConstantUtil.NUM_TRUE)); + if (bankList.isEmpty()) { + return null; + } + List nameList = new ArrayList<>(); + List idList = new ArrayList<>(); + bankList.forEach(item -> { + nameList.add(item.getBankContent()); + idList.add(item.getId()); + }); + // 题型统计 + List questionTypeAnalysisList = questionMapper.getTypeAnalysis(idList); + return new BankAnalysisVo(nameList, questionTypeAnalysisList); + } + + @Override + public PageInfo getQuestionPage(QuestionReq req) { + + List bankIds = StringUtil.isBlank(req.getBankIds()) ? List.of() : List.of(req.getBankIds().split(",")); + return PageHelper.startPage(req.getCurrentPage(), req.getPageSize()) + .doSelectPageInfo(() -> questionMapper.getQuestionList( + bankIds, + req.getKeyword() + )); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void questionChangeDeal(List questionIds) { + + // 自定义考试, 查询题目在哪些考试中 + List connectDrawRuleList = examDrawRuleService.getConnectQuestionRule(questionIds); + dealAbnormalData(connectDrawRuleList); + // 岗位考试, 查询删除的题目分别属于哪个题库, 哪个题型, 查询受影响的考试 + List questionList = questionMapper.selectList(new LambdaQueryWrapper().in(FtbCultivateQuestion::getId, questionIds)); + if (!questionList.isEmpty()) { + // 目前批量删除题目只会删除同一个题库的, 判定受影响的题库和题型 + String bankId = questionList.get(0).getClassifyId(); + List typeList = questionList.stream().map(FtbCultivateQuestion::getType).distinct().collect(Collectors.toList()); + List ruleList = baseMapper.getAffectedExamRule(bankId, typeList); + if (!ruleList.isEmpty()) { + List abnormalRuleList = getAbnormalRuleList(ruleList); + // 标记为异常 + dealAbnormalData(abnormalRuleList); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public String restartExam(String userExamId) throws Exception { + + MutablePair pair = baseValid(userExamId); + return restartGenerate(pair.getRight()); + } + + private MutablePair baseValid(String userExamId) throws Exception { + + // 不带重考次数验证, 可直接发起重考 + FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId); + if (null == examUser) { + throw new HandleException("未找到考试信息"); + } + // 判定考试状态是否正常 + CultivateExam exam = this.getById(examUser.getExamId()); + if (exam.getNeedUpdate().equals(ConstantUtil.UPDATE_TRUE)) { + throw new HandleException("考试异常,无法重新考试"); + } + return MutablePair.of(exam, examUser); + } + + /** + * 用户考试重新考试 ID不变 + * + * @param examUser 用户考试 + * @return 重新考试ID + */ + private String restartGenerate(FtbCultivateExamUser examUser) { + + // 更新用户考试详情为 删除 + LambdaUpdateWrapper updateDetailWrapper = new LambdaUpdateWrapper() + .eq(FtbCultivateExamUserDetail::getUserExamId, examUser.getId()) + .set(FtbCultivateExamUserDetail::getEnabledMark, ConstantUtil.NUM_FALSE) + .set(FtbCultivateExamUserDetail::getLastModifyUserId, UserProvider.getLoginUserId()) + .set(FtbCultivateExamUserDetail::getLastModifyTime, new Date()); + examUserDetailService.update(updateDetailWrapper); + // 用户考试复制 + FtbCultivateExamUser newExamUser = new FtbCultivateExamUser(examUser.getExamId(), examUser.getExamTime(), examUser.getPassScore(), examUser.getExcellentScore(), + examUser.getUserId(), examUser.getStartTime(), examUser.getEndTime(), + examUser.getUserOrgList(), examUser.getTotalScore(), examUser.getQuestionNumber(), examUser.getPickQuestionRule()); + newExamUser.setId(examUser.getId()); + newExamUser.setUserExamCount(examUser.getUserExamCount() + 1); + newExamUser.setLastModifyTime(new Date()); + newExamUser.setLastModifyUserId(UserProvider.getLoginUserId()); + newExamUser.setStatus(CourseEnums.ExamStatus.WAIT.getCode()); + newExamUser.setScore(0); + newExamUser.setAutoScore(0); + newExamUser.setReadOverScore(0); + newExamUser.setRemark(null); + examUserService.updateById(newExamUser); + // 新增补考记录 + FtbCultivateExamFrequencyLog log = new FtbCultivateExamFrequencyLog(FtbUtil.getId(), examUser.getExamId(), examUser.getUserId(), UserProvider.getLoginUserId()); + frequencyLogService.save(log); + return newExamUser.getId(); + } + + @Override + public String restartExamWithValid(String userExamId) throws Exception { + + // 查询重新考试配置 + MutablePair pair = baseValid(userExamId); + CultivateExam exam = pair.getLeft(); + FtbCultivateExamUser examUser = pair.getRight(); + hasTimesValid(exam, examUser); + return restartGenerate(examUser); + } + + private void hasTimesValid(CultivateExam exam, FtbCultivateExamUser examUser) throws Exception { + + if (exam.getExamLimitation().equals(ConstantUtil.NUM_FALSE)) { + throw new HandleException("暂不允许补考"); + } + FrequencyEnum frequencyEnum = FrequencyEnum.getByCode(exam.getReExamFrequencyType()); + assert frequencyEnum != null; + switch (frequencyEnum) { + case EVERY_DAY: + case EVERY_WEEK: + case EVERY_MONTH: + MutablePair range = DateRangeUtil.getRange(LocalDate.now().toString(), frequencyEnum.getDescription()); + // 查询范围内的考试次数 + Integer num = frequencyLogService.queryExamNumByDateStr(exam.getId(), examUser.getUserId(), range.getLeft(), range.getRight()); + if (num >= exam.getReExamFrequencyNum()) { + throw new HandleException("暂无补考次数"); + } + break; + case TOTAL: + Integer totalNum = frequencyLogService.queryExamNumByDateStr(exam.getId(), examUser.getUserId(), null, null); + totalNum -= 1; + if (totalNum >= exam.getReExamFrequencyNum()) { + throw new HandleException("暂无补考次数"); + } + break; + default: + break; + } + } + + @Override + public Boolean checkRestartExam(String userExamId) throws Exception { + + MutablePair pair = baseValid(userExamId); + hasTimesValid(pair.getLeft(), pair.getRight()); + return Boolean.TRUE; + } + + /** + * 检查并更新过期用户的考试状态为逾期 + * 筛选条件: + * 1. 结束时间小于当前时间 + * 2. 当前状态为待考 + * 3. 考试类型为基础考试 + * 4. 启用标记为有效 + */ + @Override + public void checkAndUpdateExpireUserExamStatus() { + // 获取当前时间,确保后续时间判断的一致性 + Date currentTime = new Date(); + + // 构建更新条件,将符合条件的用户考试记录状态更新为逾期 + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.OVERDUE.getCode()) + .set(FtbCultivateExamUser::getLastModifyTime, currentTime) + .lt(FtbCultivateExamUser::getEndTime, currentTime) + .eq(FtbCultivateExamUser::getStatus, CourseEnums.ExamStatus.WAIT.getCode()) + .eq(FtbCultivateExamUser::getExamType, CourseEnums.ExamType.BASE.getCode()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + + // 执行批量更新操作 + examUserMapper.update(null, wrapper); + } + + /** + * 检查并更新考试状态 + * 根据当前时间与考试的开始/结束时间对比,批量更新考试状态: + * 1. 未开始:开始时间大于等于当前时间 + * 2. 进行中:开始时间小于等于当前时间且结束时间大于等于当前时间 + * 3. 已结束:结束时间小于等于当前时间 + * 仅处理基础考试类型且有效的考试记录 + */ + @Override + public void checkAndUpdateExamStatus() { + // 获取当前时间,确保时间判断的一致性 + Date currentTime = new Date(); + + // 更新状态为未开始:开始时间在未来且当前状态不是未开始 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()) + .ge(CultivateExam::getStartTime, currentTime) + .eq(CultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode()) + .ne(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()) + .eq(CultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + + // 更新状态为进行中:在当前时间范围内且当前状态为未开始 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()) + .le(CultivateExam::getStartTime, currentTime) + .ge(CultivateExam::getEndTime, currentTime) + .eq(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.NOT_STARTED.getCode()) + .eq(CultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode()) + .eq(CultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + + // 更新状态为已结束:结束时间已过且当前状态为进行中,同时标记无需更新 + baseMapper.update(null, new LambdaUpdateWrapper() + .set(CultivateExam::getNeedUpdate, ExamUpdateStatus.NO_UPDATE.getCode()) + .set(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.DONE.getCode()) + .le(CultivateExam::getEndTime, currentTime) + .eq(CultivateExam::getStatus, CourseEnums.ExamTimeStatus.IN_PROGRESS.getCode()) + .eq(CultivateExam::getExamType, CourseEnums.ExamType.BASE.getCode()) + .eq(CultivateExam::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode())); + } + + + @Override + @Transactional + public void webRestartExam(WebRestartExamReq req) { + CultivateExam exam = this.getById(req.getExamId()); + checkWebRestartExam(exam, req); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getExamId, exam.getId()) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .in(FtbCultivateExamUser::getId, req.getUserExamIdList()) + .orderByDesc(FtbCultivateExamUser::getCreatorTime); + List userExamList = examUserMapper.selectList(wrapper); + + if (CollectionUtil.isEmpty(userExamList)) { + return; + } + for (FtbCultivateExamUser oldExamUser : userExamList) { + oldExamUser.setStartTime(req.getStartTime()); + oldExamUser.setEndTime(req.getEndTime()); + restartGenerate(oldExamUser); + //删除考试次数日志 + /*frequencyLogService.remove(new LambdaQueryWrapper() + .eq(FtbCultivateExamFrequencyLog::getUserId, oldExamUser.getUserId()) + .eq(FtbCultivateExamFrequencyLog::getExamId, exam.getId()) + );*/ + } + + } + + @Transactional(rollbackFor = Exception.class) + @Override + public SubExamVo subUserExam(String userExamId, SubExamQuestionReq req) { + + SubExamVo vo = new SubExamVo(); + if (CollectionUtil.isEmpty(req.getAnswerDetail())) { + throw new RuntimeException("提交的答案信息为空"); + } + if (null == req.getDuration()) { + throw new RuntimeException("考试时长为空"); + } + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbCultivateExamUser::getUserId, loginUserId) + .eq(FtbCultivateExamUser::getId, userExamId) + .eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + FtbCultivateExamUser examUser = examUserMapper.selectOne(wrapper); + if (null == examUser) { + throw new RuntimeException("用户考试信息不存在"); + } + if (CourseEnums.EnabledMarkType.INVALID.getCode().equals(examUser.getEnabledMark())) { + throw new RuntimeException("参加考试的人员考试取消"); + } + //查看考试信息 + FtbCultivateExam exam = examService.queryExamInfo(examUser.getExamId()); + if (exam.getStatus().equals(3)) { + userExamUtil.updateUserExamNullify(userExamId); + return vo; + } + //检测是否可以提交考试 + checkSubmitExam(examUser, exam); + //写入用户考试的题目和答案信息 + List userDetailList = writeUserQuestion(req, examUser, exam); + int autoScore = 0;//试卷自动阅卷得分 + int correctCount = 0;//正确数 + boolean isNeedReview = false;//是否需要批阅试卷 true:需要批阅 false:不需要批阅 + for (FtbCultivateExamUserDetail userDetail : userDetailList) { + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(userDetail.getType()); + assert questionType != null; + switch (questionType) { + case SINGLE://单选 + case JUDGE://判断 + case MULTI://多选 + case ONE_OR_MULTI://不定项 + if (Objects.equals(userDetail.getIsRight(), CourseEnums.IsRightAnswer.YES.getCode())) { + autoScore += userDetail.getQuestionScore(); + correctCount++; + } + break; + case FILL://填空题 + case INPUT://问答题 + isNeedReview = true; + break; + } + } + //修改考试题目信息 + Date now = new Date(); + FtbCultivateExamUser updateExamUser = new FtbCultivateExamUser(); + updateExamUser.setId(examUser.getId()); + updateExamUser.setUserId(examUser.getUserId()); + updateExamUser.setCorrectCount(correctCount); + updateExamUser.setExamId(examUser.getExamId()); + updateExamUser.setAutoScore(autoScore); + updateExamUser.setScore(autoScore); + updateExamUser.setFinishTime(now); + updateExamUser.setUserStartTime(new Date(now.getTime() - req.getDuration() * 1000)); + updateExamUser.setDuration(req.getDuration()); + updateExamUser.setStatus(CourseEnums.ExamStatus.WAIT_CHECK.getCode()); + updateExamUser.setTotalScore(examUser.getTotalScore()); + updateExamUser.setQuestionNumber(examUser.getQuestionNumber()); + // 修改用户考试 + if (!isNeedReview) { + //根据试卷ID查询试卷信息 + updateExamUser.setStatus(QuestionAnalysisUtil.calculateUserExamStatus(exam, updateExamUser.getTotalScore(), updateExamUser.getScore())); + BigDecimal score = BigDecimal.valueOf(updateExamUser.getScore()); + BigDecimal totalScore = BigDecimal.valueOf(updateExamUser.getTotalScore()); + BigDecimal divide = score.divide(totalScore, 2, RoundingMode.HALF_UP); + updateExamUser.setScoreRatio(divide); + } + examUserMapper.updateById(updateExamUser); + cultivateMqSendUtil.sendExamMessage(updateExamUser); + return vo; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Boolean dealExamOldData() { + + // 查询考试列表 + List examList = this.baseMapper.getOldExamList(); + if (examList.isEmpty()) { + return Boolean.TRUE; + } + // ftb_cultivate_exam 中 F_DrawDataBase, F_DrawMode, F_NeedUpdate 数据填写 + this.baseMapper.updateOldExamBatch(examList); + // 以前试卷的抽题规则需要维护到ftb_cultivate_exam_draw_rule表中 随机抽题生成规则方式待确认 + List paperIds = examList.stream().map(ExamPaperVo::getPaperId).distinct().collect(Collectors.toList()); + List paperDrawRuleList = questionMapper.getPaperDrawRuleList(paperIds); + Map> paperRuleMap = paperDrawRuleList.stream().collect(Collectors.groupingBy(PaperDrawRuleVo::getPaperId)); + List allList = new ArrayList<>(); + examList.forEach(exam -> { + List paperRuleList = paperRuleMap.get(exam.getPaperId()); + if (null != paperRuleList && !paperRuleList.isEmpty()) { + List ruleList = getRuleList(exam.getExamId(), exam.getDrawMode(), paperRuleList); + exam.setPickQuestionRule(JsonUtil.getObjectToString(ruleList)); + allList.addAll(ruleList); + } + }); + examDrawRuleService.saveBatch(allList); + // ftb_cultivate_exam_user表 F_ExamTime, F_PickQuestionRule, F_PassScore, F_ExcellentScore 数据填写 + examUserMapper.updateOldExamUserBatch(examList); + // 以前的带教/练习中绑定的视频文件 需要绑定到表 ftb_cultivate_file中 + teachingRecordService.removeAllOldFile(); + // 带教审批生成 + teachingApproveMapper.generateOldApproveData(); + return Boolean.TRUE; + } + + @Override + public void delUserExam(String userExamId) throws Exception { + + FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId); + if (null == examUser || examUser.getEnabledMark().equals(ConstantUtil.NUM_FALSE)) { + throw new Exception("未找到用户考试记录"); + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(FtbCultivateExamUser::getEnabledMark, ConstantUtil.NUM_FALSE) + .set(FtbCultivateExamUser::getDeleteTime, new Date()) + .set(FtbCultivateExamUser::getDeleteUserId, UserProvider.getLoginUserId()) + .eq(FtbCultivateExamUser::getId, userExamId); + examUserMapper.update(null, updateWrapper); + } + + @Override + public PaperDetailVo getMarkPaperDetail(String userExamId) throws Exception { + + FtbCultivateExamUser examUser = examUserMapper.selectById(userExamId); + if (examUser == null) { + throw new Exception("未找到记录"); + } + PaperDetailVo vo = JsonUtil.getJsonToBean(examUser, PaperDetailVo.class); + vo.setUserExamId(examUser.getId()); + FtbCultivateExam exam = examService.getById(vo.getExamId()); + vo.setExamName(null == exam ? "--" : exam.getExamName()); + // 查询用户信息 + List list = v2UserApi.userListAndCopy(List.of(vo.getUserId()), null, UserProvider.getUser().getTenantId()); + if (CollUtil.isNotEmpty(list)) { + UserBoundVO user = list.get(0); + vo.setUserName(user.getUserName()); + vo.setOrganizeName(user.getOrganizeName()); + vo.setPostName(user.getPositionName()); + vo.setGradeName(user.getGradeName()); + } + return vo; + } + + private List getRuleList(String examId, Integer drawMode, List paperRuleList) { + + List ruleList; + if (drawMode.equals(ConstantUtil.NUM_TRUE)) { + // 固定抽题 + ruleList = JsonUtil.getJsonToList(paperRuleList, CultivateExamDrawRule.class); + ruleList.forEach(v -> { + v.setId(FtbUtil.getId()); + v.setExamId(examId); + }); + } else { + // 随机抽题 + ruleList = paperRuleList.stream() + // 按题型分组 + .collect(Collectors.groupingBy(PaperDrawRuleVo::getQuestionType)) + .entrySet() + .stream() + .map(entry -> { + Integer questionType = entry.getKey(); + List list = entry.getValue(); + // 题目数量 + Map> difficultyMap = list.stream().collect(Collectors.groupingBy(PaperDrawRuleVo::getDifficulty)); + int pickCount = list.size(); + List easyList = difficultyMap.getOrDefault(ConstantUtil.EASY, List.of()); + List mediumList = difficultyMap.getOrDefault(ConstantUtil.MEDIUM, List.of()); + List hardList = difficultyMap.getOrDefault(ConstantUtil.HARD, List.of()); + // 总分 + int totalScore = list.stream() + .filter(v -> v.getScore() != null) + .mapToInt(PaperDrawRuleVo::getScore) + .sum(); + // 平均分(四舍五入) + int avgScore = (int) Math.round((double) totalScore / pickCount); + // 构建新规则 + CultivateExamDrawRule rule = new CultivateExamDrawRule(); + rule.setId(FtbUtil.getId()); + rule.setExamId(examId); + rule.setQuestionType(questionType); + rule.setPickCount(pickCount); + rule.setEasyCount(easyList.size()); + rule.setMediumCount(mediumList.size()); + rule.setHardCount(hardList.size()); + rule.setScore(avgScore); + rule.setQuestionId(null); + rule.setNormal(ConstantUtil.NUM_FALSE); + return rule; + }) + .collect(Collectors.toList()); + } + return ruleList; + } + + /** + * 检测是否可以提交考试 + */ + private void checkSubmitExam(FtbCultivateExamUser examUser, FtbCultivateExam exam) { + CourseEnums.ExamType examType = CourseEnums.ExamType.fromCode(exam.getExamType()); + assert examType != null; + switch (examType) { + case BASE://常规考试 + if (StringUtils.isEmpty(examUser.getRelationTaskId()) && examUser.getStartTime() != null && examUser.getEndTime() != null) { + //判断当前时间是否已经过了考试结束时间 + long time = new Date().getTime();//当前时间毫秒数 + //判断当前时间是否已经过了考试开始时间 + if (examUser.getStartTime().getTime() > time) { + throw new RuntimeException("考试未开始"); + } + } + break; + case POSITION://岗位学习考试 + break; + } + if (!CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) && !CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + throw new RuntimeException("考试已经提交,请不要重复提交"); + } + } + + private List writeUserQuestion(SubExamQuestionReq req, FtbCultivateExamUser examUser, FtbCultivateExam exam) { + + //获取当前用户信息 + String loginUserId = UserProvider.getLoginUserId(); + List answerDetailList = req.getAnswerDetail(); + if (CollectionUtil.isEmpty(answerDetailList)) { + CollectionUtil.newArrayList(); + } + List addList = new ArrayList<>(); + for (AppQuestionVo answerDetail : answerDetailList) { + String questionId = answerDetail.getQuestionId(); + String userAnswer = answerDetail.getUserAnswer(); + //查询题目分数 + Integer questionScore = answerDetail.getQuestionScore(); + FtbCultivateExamUserDetail user = FtbCultivateExamUserDetail.builder() + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .userExamId(examUser.getId()) + .examId(exam.getId()) + .questionId(questionId) + .questionOption(JSONUtil.toJsonStr(answerDetail.getQuestionOptionVoList())) + .classifyId(answerDetail.getClassifyId()) + .type(answerDetail.getType()) + .difficulty(answerDetail.getDifficulty()) + .userAnswer(userAnswer) + .content(answerDetail.getContent()) + .analysis(answerDetail.getAnalysis()) + .paperId(exam.getPaperId()) + .userId(loginUserId) + .answer(answerDetail.getAnswer()) + .questionScore(questionScore) + .batch(examUser.getBatch()).build(); + + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(answerDetail.getType()); + assert questionType != null; + switch (questionType) { + case SINGLE://单选 + case JUDGE://判断 + if (answerDetail.getAnswer().equals(userAnswer)) { + user.setUserScore(user.getQuestionScore()); + user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + break; + case MULTI://多选 + case ONE_OR_MULTI://不定项选择题 + if (QuestionAnalysisUtil.checkMultiRight(user.getAnswer(), user.getUserAnswer())) { + user.setUserScore(user.getQuestionScore()); + user.setIsRight(CourseEnums.IsRightAnswer.YES.getCode()); + } else { + user.setUserScore(0); + user.setIsRight(CourseEnums.IsRightAnswer.NO.getCode()); + } + break; + case FILL://填空题 + case INPUT://问答题 + user.setUserScore(0);//给一个默认分数 + break; + + } + addList.add(user); + } + examUserDetailService.saveBatch(addList); + return addList; + } + + private void checkWebRestartExam(CultivateExam exam, WebRestartExamReq req) { + if (CourseEnums.ExamType.BASE.getCode().equals(exam.getExamType())) { + if (null == req.getStartTime()) { + throw new RuntimeException("开始考试时间不能为空"); + } else if (null == req.getEndTime()) { + throw new RuntimeException("结束考试时间不能为空"); + } + if (req.getStartTime().after(req.getEndTime())) { + throw new RuntimeException("考试开始时间不能大于结束时间"); + } + } + if (CollectionUtil.isEmpty(req.getUserExamIdList())) { + throw new RuntimeException("请选择需要重考的用户考试Id"); + } + } + + private List getAbnormalRuleList(List ruleList) { + + // 查询受影响的考试还够不够抽题 + Set set = new HashSet<>(); + List bankPickList = ruleList.stream() + .flatMap(rule -> Arrays.stream(rule.getDrawDataBase().split(",")) + .map(b -> new BankPickVo(b, rule.getQuestionType()))) + .filter(v -> set.add(v.getBankId() + "_" + v.getQuestionType())) + .collect(Collectors.toList()); + // 查询bankPickList每个库每个题型有多少题目 + List countList = questionMapper.getBankQuestionTypeCount(bankPickList); + Map questionCountMap = countList.stream() + .collect(Collectors.toMap( + v -> v.getBankId() + "_" + v.getQuestionType(), + Function.identity() + )); + List abnormalRules = new ArrayList<>(); + // 4. 校验每条规则 + for (ExamDrawRuleVo rule : ruleList) { + int needEasy = Optional.ofNullable(rule.getEasyCount()).orElse(0); + int needMedium = Optional.ofNullable(rule.getMediumCount()).orElse(0); + int needHard = Optional.ofNullable(rule.getHardCount()).orElse(0); + int easyTotal = 0; + int mediumTotal = 0; + int hardTotal = 0; + for (String bId : rule.getDrawDataBase().split(",")) { + String key = bId + "_" + rule.getQuestionType(); + BankPickVo vo = questionCountMap.get(key); + if (vo != null) { + easyTotal += Optional.ofNullable(vo.getEasyCount()).orElse(0); + mediumTotal += Optional.ofNullable(vo.getMediumCount()).orElse(0); + hardTotal += Optional.ofNullable(vo.getHardCount()).orElse(0); + } + } + // 5. 只要任意一个难度不满足,就算异常 + if (easyTotal < needEasy + || mediumTotal < needMedium + || hardTotal < needHard) { + abnormalRules.add(new ConnectDrawRuleVo( + rule.getId(), + rule.getExamId(), + rule.getQuestionType() + )); + } + } + return abnormalRules; + } + + private void dealAbnormalData(List connectDrawRuleList) { + if (null != connectDrawRuleList && !connectDrawRuleList.isEmpty()) { + // 异常考试 + List examIds = connectDrawRuleList.stream().map(ConnectDrawRuleVo::getExamId).distinct().collect(Collectors.toList()); + // 异常固定题目 + List ruleIds = connectDrawRuleList.stream().map(ConnectDrawRuleVo::getRuleId).collect(Collectors.toList()); + this.update(new LambdaUpdateWrapper() + .in(CultivateExam::getId, examIds) + .set(CultivateExam::getNeedUpdate, ConstantUtil.UPDATE_TRUE) + .set(CultivateExam::getLastModifyTime, new Date()) + .set(CultivateExam::getLastModifyUserId, UserProvider.getLoginUserId())); + examDrawRuleService.update(new LambdaUpdateWrapper() + .in(CultivateExamDrawRule::getId, ruleIds) + .set(CultivateExamDrawRule::getNormal, ConstantUtil.NUM_FALSE) + .set(CultivateExamDrawRule::getLastModifyTime, new Date()) + .set(CultivateExamDrawRule::getLastModifyUserId, UserProvider.getLoginUserId())); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamStatisticsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamStatisticsServiceImpl.java new file mode 100644 index 0000000..078057e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateExamStatisticsServiceImpl.java @@ -0,0 +1,329 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.FtbCultivateExamUserMapper; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivateExamStatisticsService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.resp.AppExamListVo; +import jnpf.model.cultivate.resp.StatisticsResultDto; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForOrgReq; +import jnpf.model.cultivate.v2.exam.req.V2ExamStatisticsForPersonReq; +import jnpf.model.cultivate.v2.exam.vo.AiHelperExamStatisticsVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForOrgVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForPersonVo; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.QuestionAnalysisUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class V2CultivateExamStatisticsServiceImpl implements V2CultivateExamStatisticsService { + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + + @Override + public PageListVO queryExamStatisticsForOrg(V2ExamStatisticsForOrgReq dto) { + Page page = dto.coverCultivatePage(); + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return CultivatePage.coverPageList(page); + } + if(CollUtil.isNotEmpty(dto.getOrgIds())){ + List intersection = UserApiV2Util.getIntersection(powerOrgList, dto.getOrgIds()); + if (CollUtil.isEmpty(intersection)) { + return CultivatePage.coverPageList(page); + } + dto.setOrgIds(intersection); + }else{ + dto.setOrgIds(powerOrgList); + } + if (CollUtil.isEmpty(dto.getOrgIds())) { + return CultivatePage.coverPageList(page); + } + List selectOrgList = dto.getOrgIds(); + if (dto.getIsNext() == 1) { + List organizeGeneralDetailVOS = userApiV2Util.organizesOrHaveChildByOrganizeIds(selectOrgList, true, null); + if (CollUtil.isNotEmpty(organizeGeneralDetailVOS)) { + List collect = organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + selectOrgList.addAll(collect); + } + } + List intersection = UserApiV2Util.getIntersection(powerOrgList, selectOrgList); + if (CollUtil.isEmpty(intersection)) { + return CultivatePage.coverPageList(page); + } + selectOrgList = intersection; + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(selectOrgList, null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return CultivatePage.coverPageList(page); + } + Map> userOrgGroup = userListForOrgIds.stream().collect(Collectors.groupingBy(UserPageListVO::getOrganizeId)); + List queryList = ftbCultivateExamUserMapper.countForOrgV2(dto, userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + + if (CollUtil.isEmpty(queryList)) { + return CultivatePage.coverPageList(page); + } + //queryList 转换成用户的map + Map> userExamMap = queryList.stream().collect(Collectors.groupingBy(AppExamListVo::getUserId)); + //查询组织 + Map organizeEntityMap = userApiV2Util.organizesByOrganizeIdsReturenMap(selectOrgList, null); + List retList = new ArrayList<>(); + for (String orgId : selectOrgList) { + List listVOS = userOrgGroup.get(orgId); + if (CollUtil.isEmpty(listVOS)) { + continue; + } + List userIds = listVOS.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + List examUserList = new ArrayList<>(); + for (String userId : userIds) { + List appExamListVo = userExamMap.get(userId); + if (appExamListVo != null) { + examUserList.addAll(appExamListVo); + } + } + if (CollUtil.isEmpty(examUserList)) { + continue; + } + //按照考试分组 + Map> examGroupUserMap = examUserList.stream() + .collect(Collectors.groupingBy(AppExamListVo::getExamId)); + for (Map.Entry> examGroupEntity : examGroupUserMap.entrySet()) { + String examId = examGroupEntity.getKey(); // 当前键 + String examName = ""; + Integer examType = null; + String examTypeName = ""; + List examGroupList = examGroupEntity.getValue(); // 当前值(List) + if (CollUtil.isNotEmpty(examGroupList)) { + examName = examGroupList.get(0).getExamName(); + examType = examGroupList.get(0).getExamType(); + if (examGroupList.get(0).getExamType() == 1) { + examTypeName = "岗位学习考试"; + } else { + examTypeName = "自定义考试"; + } + } + StatisticsResultDto statisticsResultDto = QuestionAnalysisUtil.statisticeLvForAppExam(examGroupList); + V2ExamStatisticsForOrgVo vo = new V2ExamStatisticsForOrgVo(); + vo.setOrgId(orgId); + OrganizeGeneralDetailVO organizeEntity = organizeEntityMap.get(orgId); + if (null != organizeEntity) { + vo.setOrgName(organizeEntity.getName()); + vo.setOrgCode(organizeEntity.getEnCode()); + } + vo.setExamId(examId); + vo.setExamName(examName); + vo.setExamType(examType); + vo.setExamTypeName(examTypeName); + + vo.setTotleNum(String.valueOf(statisticsResultDto.getTotle())); + vo.setExcellentNum(String.valueOf(statisticsResultDto.getExcellent())); + vo.setPassNum(String.valueOf(statisticsResultDto.getPass())); + vo.setNoPassNum(String.valueOf(statisticsResultDto.getNoPass())); + + vo.setNoPassRate(String.valueOf(statisticsResultDto.getNoPassLv())); + vo.setPassRate(String.valueOf(statisticsResultDto.getPassLv())); + vo.setExcellentRate(String.valueOf(statisticsResultDto.getExcellentLv())); + retList.add(vo); + } + } + return UserApiV2Util.buildPage(retList, dto.getPageSize(), dto.getCurrentPage()); + } + + private List buildExamStatus(V2ExamStatisticsForPersonReq dto) { + List innerStatus = new ArrayList<>(); + if (dto.getExamStatus().equals(0)) { + if (dto.getExamResult() == 0) { + innerStatus.add(0); + innerStatus.add(1); + innerStatus.add(2); + innerStatus.add(3); + innerStatus.add(4); + innerStatus.add(5); + } else if (dto.getExamResult() == 1) { + innerStatus.add(5); + } else if (dto.getExamResult() == 2) { + innerStatus.add(3); +// innerStatus.add(5); + } else if (dto.getExamResult() == 3) { + innerStatus.add(4); + } + } else if (dto.getExamStatus().equals(1)) { + innerStatus.add(0); + innerStatus.add(2); + } else if (dto.getExamStatus().equals(2)) { + if (dto.getExamResult() == 0) { + innerStatus.add(1); + innerStatus.add(3); + innerStatus.add(4); + innerStatus.add(5); + } else if (dto.getExamResult() == 1) { + innerStatus.add(5); + } else if (dto.getExamResult() == 2) { + innerStatus.add(3); +// innerStatus.add(5); + } else if (dto.getExamResult() == 3) { + innerStatus.add(4); + } + } + return innerStatus; + } + + private List queryUserIdsForPositionIds(List positionIds) { + List voList = userApiV2Util.getUserListForPositions(positionIds, null); + if (CollUtil.isEmpty(voList)) { + return new ArrayList<>(); + } + return voList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + + @Override + public PageInfo queryExamStatisticsForPerson(V2ExamStatisticsForPersonReq dto) { + if (null == dto.getCurrentPage() || null == dto.getPageSize()) { + throw new RuntimeException("请传入分页信息"); + } + + if (dto.getExamStatus().equals(1) && !dto.getExamResult().equals(0)) { + return PageInfo.emptyPageInfo(); + } + + + //0待考试,1待批阅,2已逾期,3合格,4不合格 5、优秀 + List innerStatus = buildExamStatus(dto); + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize()); + List innerUserIds = new ArrayList<>(); + if (StringUtils.isNotEmpty(dto.getPersonPositionId()) && CollUtil.isNotEmpty(dto.getUserIds())) { + List positionUserIds = queryUserIdsForPositionIds(List.of(dto.getPersonPositionId())); + if (CollUtil.isEmpty(positionUserIds)) { + return PageInfo.emptyPageInfo(); + } + List intersection = UserApiV2Util.getIntersection(positionUserIds, dto.getUserIds()); + if (CollUtil.isEmpty(intersection)) { + return PageInfo.emptyPageInfo(); + } + innerUserIds = intersection; + } else if (CollUtil.isNotEmpty(dto.getUserIds())) { + innerUserIds.addAll(dto.getUserIds()); + } else if (StringUtils.isNotEmpty(dto.getPersonPositionId())) { + List positionUserIds = queryUserIdsForPositionIds(List.of(dto.getPersonPositionId())); + if (CollUtil.isEmpty(positionUserIds)) { + return PageInfo.emptyPageInfo(); + } + innerUserIds = positionUserIds; + } + if (StringUtils.isNotEmpty(dto.getOrgId())) { + String[] split = dto.getOrgId().split(","); + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(Arrays.asList(split), null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return PageInfo.emptyPageInfo(); + } + if (CollUtil.isEmpty(innerUserIds)) { + innerUserIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + } else { + List intersection = UserApiV2Util.getIntersection(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()), innerUserIds); + if (CollUtil.isEmpty(intersection)) { + return PageInfo.emptyPageInfo(); + } + innerUserIds = intersection; + } + } + + InnerPowerUserVO powerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (powerUserVO.getCode() == 1) { + if (CollUtil.isNotEmpty(innerUserIds)) { + List intersection = UserApiV2Util.getIntersection(powerUserVO.getUserIds(), innerUserIds); + if (CollUtil.isEmpty(intersection)) { + return PageInfo.emptyPageInfo(); + } + innerUserIds = intersection; + } else { + innerUserIds = powerUserVO.getUserIds(); + } + } else if (powerUserVO.getCode() == 2) { + return PageInfo.emptyPageInfo(); + } + + Page queryPage = ftbCultivateExamUserMapper.queryExamStatisticsForPersonV2(page, dto, innerUserIds, innerStatus); + List list = queryPage.getRecords(); + if (CollUtil.isNotEmpty(list)) { + List tempUserId = new ArrayList<>(); + List tempPostionId = new ArrayList<>(); + for (V2ExamStatisticsForPersonVo examStatisticsForPersonVo : list) { + tempUserId.add(examStatisticsForPersonVo.getUserId()); + if (examStatisticsForPersonVo.getExamSource() == 1 || examStatisticsForPersonVo.getExamSource() == 2) { + tempPostionId.add(examStatisticsForPersonVo.getRelationPositionId()); + } + } + + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(tempUserId, null); + Map tempPositionMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(tempPostionId, null); + for (V2ExamStatisticsForPersonVo examStatisticsForPersonVo : list) { + if (examStatisticsForPersonVo.getExamSource() == 1 || examStatisticsForPersonVo.getExamSource() == 2) { + PositionVO positionEntity = tempPositionMap.get(examStatisticsForPersonVo.getRelationPositionId()); + if (null != positionEntity) { + examStatisticsForPersonVo.setStudyPositionName(positionEntity.getFullName()); + } + } + UserBoundVO userPrimaryBoundOne = userPrimaryBoundBatch.get(examStatisticsForPersonVo.getUserId()); + if (userPrimaryBoundOne != null) { + examStatisticsForPersonVo.setOrganizeId(userPrimaryBoundOne.getOrganizeId()); + examStatisticsForPersonVo.setOrganizeName(userPrimaryBoundOne.getOrganizeName()); + examStatisticsForPersonVo.setOrganizeEnCode(userPrimaryBoundOne.getOrganizeEnCode()); + examStatisticsForPersonVo.setPositionId(userPrimaryBoundOne.getPositionId()); + examStatisticsForPersonVo.setPositionName(userPrimaryBoundOne.getPositionName()); + examStatisticsForPersonVo.setPositionEnCode(userPrimaryBoundOne.getPositionEnCode()); + examStatisticsForPersonVo.setGradeId(userPrimaryBoundOne.getGradeId()); + examStatisticsForPersonVo.setGradeName(userPrimaryBoundOne.getGradeName()); + } + if (null != examStatisticsForPersonVo.getExamTime()) { + examStatisticsForPersonVo.setExamTime(examStatisticsForPersonVo.getExamTime() / 60); + } + } + } + + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public AiHelperExamStatisticsVo getExamStatistics(String examId) { + if (StringUtils.isEmpty(examId)) { + throw new RuntimeException("考试ID不能为空"); + } + AiHelperExamStatisticsVo statistics = ftbCultivateExamUserMapper.getExamStatistics(examId); + if (statistics == null) { + statistics = new AiHelperExamStatisticsVo(); + statistics.setTotalParticipants(0); + statistics.setQualifiedCount(0); + statistics.setPassRate(java.math.BigDecimal.ZERO); + statistics.setPendingReviewCount(0); + } + return statistics; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyApplyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyApplyServiceImpl.java new file mode 100644 index 0000000..09267d5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyApplyServiceImpl.java @@ -0,0 +1,1115 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.cultivate.mapper.CultivateIdentifyApplyDetailsMapper; +import jnpf.cultivate.mapper.CultivateIdentifyApplyMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.service.impl.FtbCultivateFileService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.cultivate.v2.service.V2CultivateCertificateService; +import jnpf.cultivate.v2.service.V2CultivateIdentifyApplyService; +import jnpf.cultivate.v2.util.CultivateIdentityUtil; +import jnpf.cultivate.v2.util.CultivateMqSendUtil; +import jnpf.entity.cultivate.*; +import jnpf.enums.cultivate.ApplyResultEnum; +import jnpf.enums.cultivate.ApplyStatusEnum; +import jnpf.enums.cultivate.IdentifyItemExceptionEnum; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.v2.apply.req.*; +import jnpf.model.cultivate.v2.apply.vo.*; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableScoreConfigVo; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyDetailsInfoVo; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.*; +import jnpf.util.context.SpringContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + + +@Slf4j +@Service +public class V2CultivateIdentifyApplyServiceImpl implements V2CultivateIdentifyApplyService { + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private CultivateIdentifyTableService identifyTableService; + @Autowired + private CultivateIdentifyItemsService identifyItemsService; + @Autowired + private CultivateIdentifyApplyDetailsMapper applyDetailsMapper; + @Autowired + private CultivateIdentifyApplyDetailsService applyDetailsService; + + @Autowired + private CultivateIdentifyApplyTableBackupsService applyTableBackupsService; + @Autowired + private CultivateIdentifyApplyDetailsBackupsService applyDetailsBackupsService; + + @Autowired + private FtbCultivateFileService ftbCultivateFileService; + + + @Autowired + private CultivateIdentifyApplyMapper baseMapper; + + @Autowired + private CultivateMqSendUtil cultivateMqSendUtil; + + @Autowired + private V2CultivateCertificateService v2CultivateCertificateService; + + @Autowired + private FtbCultivateLabelService ftbCultivateLabelService; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + /** + * 获取Web端培养鉴定申请列表的分页数据 + * + * @param req 查询条件请求对象,包含分页信息和筛选条件 + * @return 分页结果对象,包含申请列表数据和分页信息 + */ + @Override + public Page getWebPageApplyList(V2IdentifyApplyListReq req) { + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode() == 1) { + req.setInnerPowerUserIds(innerPowerUserVO.getUserIds()); + } else if (innerPowerUserVO.getCode() == 0) { + req.setInnerPowerUserIds(new ArrayList<>()); + } else { + return page; + } + if (StringUtil.isNotEmpty(req.getOrgId())) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(List.of(req.getOrgId()), null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return page; + } + if (CollUtil.isNotEmpty(req.getInnerPowerUserIds())) { + List intersection = UserApiV2Util.getIntersection(req.getInnerPowerUserIds(), userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + if (CollUtil.isEmpty(intersection)) { + return page; + } + req.setInnerPowerUserIds(intersection); + } else { + req.setInnerPowerUserIds(userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } + } + if (StrUtil.isNotBlank(req.getKeyword())){ + //查询用户信息 通过用户名称模糊搜索出用户ID集合 + StaffRosterListReq rosterListReq = new StaffRosterListReq(); + rosterListReq.setIsQueryAuth("0"); + rosterListReq.setLikeUserName(req.getKeyword()); + List rosterDtos = staffRosterService.postWithSalaryNoPage(rosterListReq); + req.setUserIdsKeyWord(rosterDtos.stream() + .map(FtbPersonnelsStaffRosterDto::getUserId) + .filter(StrUtil::isNotEmpty) + .collect(Collectors.toList())); + } + page = this.baseMapper.getPageListV2(page, req); + if (CollUtil.isNotEmpty(page.getRecords())) { + //查询到人员名称-服务调用 + List userIdList = new ArrayList<>(); + for (V2IdentifyApplyListVo record : page.getRecords()) { + userIdList.add(record.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(record.getIdentifyUserId())) { + userIdList.addAll(List.of(record.getIdentifyUserId().split(","))); + } + } + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIdList, null); + + for (V2IdentifyApplyListVo item : page.getRecords()) { + UserBoundVO beUserInfoVo = userMap.get(item.getBeIdentifyUserId()); + // 获取用户头像 + if (Objects.nonNull(beUserInfoVo) && !StringUtil.isEmpty(beUserInfoVo.getHeadIcon())) { + item.setBeIdentifyUserInfo(beUserInfoVo); + } + if (StringUtil.isNotEmpty(item.getIdentifyUserId())) { + List identifyUserInfo = new ArrayList<>(); + for (String tempId : item.getIdentifyUserId().split(",")) { + //鉴定用户信息 + UserBoundVO userInfoVo = userMap.get(tempId); + // 获取用户头像 + if (Objects.nonNull(userInfoVo)) { + identifyUserInfo.add(userInfoVo); + } + } + item.setIdentifyUserInfo(identifyUserInfo); + } + } + } + return page; + } + + /** + * 根据ID获取Web端培养鉴定申请的基本信息 + * + * @param id 申请记录的唯一标识符 + * @return 申请的基本信息对象 + */ + @Override + public V2IdentifyApplyBasicInfoVo getWebApplyBasicInfo(String id) { + CultivateIdentifyApply apply = queryAndCheckApply(id); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + V2IdentifyApplyBasicInfoVo applyBasicInfoVo = new V2IdentifyApplyBasicInfoVo(); + BeanUtils.copyProperties(apply, applyBasicInfoVo); + + applyBasicInfoVo.setId(apply.getId()); + applyBasicInfoVo.setTableId(table.getTableId()); + applyBasicInfoVo.setTableName(table.getName()); + applyBasicInfoVo.setRuleId(table.getRuleId()); + applyBasicInfoVo.setLabelId(table.getLabelId()); + if (StringUtil.isNotEmpty(table.getLabelId())) { + FtbCultivateLabel label = ftbCultivateLabelService.getById(table.getLabelId()); + if (label != null) { + applyBasicInfoVo.setLabelName(label.getName()); + } + } else { + applyBasicInfoVo.setLabelName("未设置分类"); + } + String beIdentifyUserId = apply.getBeIdentifyUserId(); + + List userIds = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + userIds.addAll(List.of(apply.getIdentifyUserId().split(","))); + } + userIds.add(beIdentifyUserId); + + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + applyBasicInfoVo.setBeIdentifyUserInfo(userMap.get(beIdentifyUserId)); + + + //鉴定用户信息 + List identifyUserInfo = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + for (String s : apply.getIdentifyUserId().split(",")) { + UserBoundVO userBoundVO = userMap.get(s); + if (userBoundVO != null) { + identifyUserInfo.add(userBoundVO); + } + } + } + + applyBasicInfoVo.setIdentifyUserInfo(identifyUserInfo); + return applyBasicInfoVo; + } + + private CultivateIdentifyApply queryAndCheckApply(String id) { + CultivateIdentifyApply apply = this.baseMapper.selectById(id); + if (apply == null || apply.getDeleteMark().equals(1)) { + throw new ServiceException("鉴定不存在或者已经删除"); + } + return apply; + } + + @Override + public V2IdentifyApplyAppBasicInfoVo getAppApplyBasicInfo(String id) { + CultivateIdentifyApply apply = queryAndCheckApply(id); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + V2IdentifyApplyAppBasicInfoVo applyBasicInfoVo = new V2IdentifyApplyAppBasicInfoVo(); + BeanUtils.copyProperties(apply, applyBasicInfoVo); + + applyBasicInfoVo.setId(apply.getId()); + applyBasicInfoVo.setTableId(table.getTableId()); + applyBasicInfoVo.setTableName(table.getName()); + applyBasicInfoVo.setRuleId(table.getRuleId()); + applyBasicInfoVo.setCoverUrl(table.getCoverUrl()); + applyBasicInfoVo.setScoreType(table.getScoreType()); + applyBasicInfoVo.setRuleDesc(table.getRuleDesc()); + applyBasicInfoVo.setPassType(table.getPassType()); + applyBasicInfoVo.setPassScore(table.getPassScore()); + applyBasicInfoVo.setExcellentType(table.getExcellentType()); + applyBasicInfoVo.setExcellentScore(table.getExcellentScore()); + applyBasicInfoVo.setIdentifyTime(apply.getIdentifyTime()); + if (StringUtil.isNotEmpty(table.getScoreConfig())) { + applyBasicInfoVo.setScoreConfig(JSON.parseArray(table.getScoreConfig(), V2IdentifyTableScoreConfigVo.class)); + } + applyBasicInfoVo.setCertificateId(table.getCertificateId()); + String beIdentifyUserId = apply.getBeIdentifyUserId(); + + List userIds = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + userIds.addAll(List.of(apply.getIdentifyUserId().split(","))); + } + userIds.add(beIdentifyUserId); + + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + applyBasicInfoVo.setBeIdentifyUserInfo(userMap.get(beIdentifyUserId)); + + + //鉴定用户信息 + List identifyUserInfo = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + for (String s : apply.getIdentifyUserId().split(",")) { + UserBoundVO userBoundVO = userMap.get(s); + if (userBoundVO != null) { + identifyUserInfo.add(userBoundVO); + } + } + } + applyBasicInfoVo.setIdentifyUserInfo(identifyUserInfo); + + LambdaQueryWrapper itemsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, table.getId()) + .eq(CultivateIdentifyApplyDetailsBackups::getDeleteMark, ConstantUtil.NUM_FALSE); + List identifyItemsList = this.applyDetailsBackupsService.list(itemsLambdaQueryWrapper); + BigDecimal totalScore = CultivateIdentityUtil.calIdentityTotalScore(identifyItemsList); + applyBasicInfoVo.setItemNum(identifyItemsList.size()); + applyBasicInfoVo.setShowScoreConfig(CultivateIdentityUtil.calPassAndExcellentResult(totalScore, table)); + + return applyBasicInfoVo; + } + + @Override + public V2IdentifyApplyItemVo itemList(String id) { + CultivateIdentifyApply apply = queryAndCheckApply(id); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + V2IdentifyApplyItemVo applyBasicInfoVo = new V2IdentifyApplyItemVo(); + BeanUtils.copyProperties(apply, applyBasicInfoVo); + + applyBasicInfoVo.setId(apply.getId()); + applyBasicInfoVo.setTableId(table.getTableId()); + applyBasicInfoVo.setTableName(table.getName()); + applyBasicInfoVo.setRuleId(table.getRuleId()); + applyBasicInfoVo.setCoverUrl(table.getCoverUrl()); + applyBasicInfoVo.setScoreType(table.getScoreType()); + applyBasicInfoVo.setRuleDesc(table.getRuleDesc()); + applyBasicInfoVo.setPassType(table.getPassType()); + applyBasicInfoVo.setPassScore(table.getPassScore()); + applyBasicInfoVo.setExcellentType(table.getExcellentType()); + applyBasicInfoVo.setExcellentScore(table.getExcellentScore()); + if (StringUtil.isNotEmpty(table.getScoreConfig())) { + applyBasicInfoVo.setScoreConfig(JSON.parseArray(table.getScoreConfig(), V2IdentifyTableScoreConfigVo.class)); + } + applyBasicInfoVo.setCertificateId(table.getCertificateId()); + String beIdentifyUserId = apply.getBeIdentifyUserId(); + + List userIds = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + userIds.addAll(List.of(apply.getIdentifyUserId().split(","))); + } + userIds.add(beIdentifyUserId); + + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + applyBasicInfoVo.setBeIdentifyUserInfo(userMap.get(beIdentifyUserId)); + + + //鉴定用户信息 + List identifyUserInfo = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + for (String s : apply.getIdentifyUserId().split(",")) { + UserBoundVO userBoundVO = userMap.get(s); + if (userBoundVO != null) { + identifyUserInfo.add(userBoundVO); + } + } + } + applyBasicInfoVo.setIdentifyUserInfo(identifyUserInfo); + + + //设置鉴定项得分集合 + List detailsInfoVos = this.applyDetailsMapper.getIdentifyItemList(apply.getId()); + if (CollUtil.isNotEmpty(detailsInfoVos)) { + List tempIds = new ArrayList<>(); + BigDecimal totalScore = new BigDecimal(0); + for (IdentifyApplyDetailsInfoVo detailsInfoVo : detailsInfoVos) { + tempIds.add(detailsInfoVo.getId()); + if (detailsInfoVo.getType().equals(0)) { + totalScore = totalScore.add(detailsInfoVo.getTotalScore()); + } else { + totalScore = totalScore.add(new BigDecimal(detailsInfoVo.getMaxScore())); + } + } + applyBasicInfoVo.setIdentifyItemList(detailsInfoVos); + List fileItemLists = queryFilesLists(tempIds); + if (CollUtil.isNotEmpty(fileItemLists)) { + //按照businessId 分组 + Map> fileItemMap = fileItemLists.stream().collect(Collectors.groupingBy(FtbCultivateFile::getBusinessId)); + for (IdentifyApplyDetailsInfoVo detailsInfoVo : detailsInfoVos) { + List fileItemList = fileItemMap.get(detailsInfoVo.getId()); + if (CollUtil.isNotEmpty(fileItemList)) { + detailsInfoVo.setFiles(fileItemList.stream().map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList())); + } + } + } + + applyBasicInfoVo.setShowScoreConfig(CultivateIdentityUtil.calPassAndExcellentResult(totalScore, table)); + + + } else { + applyBasicInfoVo.setIdentifyItemList(CollUtil.newArrayList()); + applyBasicInfoVo.setTotalScore(null); + applyBasicInfoVo.setUserTime(null); + applyBasicInfoVo.setResult(null); + } + List applyFileIds = new ArrayList<>(); + applyFileIds.add(apply.getId()); + applyFileIds.add(apply.getId() + apply.getBeIdentifyUserId()); + + List applyFileList = queryFilesLists(applyFileIds); + + applyBasicInfoVo.setFiles(applyFileList.stream().map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList())); + + + return applyBasicInfoVo; + } + + @Override + @Transactional + public void applyDataSubmit(V2IdentifyApplySubmitReq req) { + + String userId = UserProvider.getLoginUserId(); + String tenantId = UserProvider.getUser().getTenantId(); + CultivateIdentifyApply apply = this.baseMapper.selectById(req.getId()); + if (apply == null || apply.getDeleteMark().equals(1)) { + throw new ServiceException("鉴定不存在或者已经删除"); + } + if (userId.equals(apply.getBeIdentifyUserId())) { + throw new ServiceException("对不起,不能够自己给自己鉴定"); + } + if (apply.getStatus().equals(1)) { + throw new ServiceException("该鉴定已完成"); + } + + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + + LambdaQueryWrapper itemsLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, table.getId()) + .eq(CultivateIdentifyApplyDetailsBackups::getDeleteMark, ConstantUtil.NUM_FALSE); + List identifyItemsList = this.applyDetailsBackupsService.list(itemsLambdaQueryWrapper); + ServiceException.notNull(identifyItemsList, "未查询到实操鉴定项记录"); + + //计算鉴定总分 + BigDecimal totalScore = CultivateIdentityUtil.calIdentityTotalScore(identifyItemsList); + //计算总得分 + BigDecimal userTotalScore = CultivateIdentityUtil.calUserTotalScore(req.getIdentifyItemList()); +// 0-合格,1-优秀,2-不合格 + MutablePair pair = CultivateIdentityUtil.calUserIdentifyResult(table, totalScore, userTotalScore); + + apply.setResult(pair.left); + apply.setGradeName(pair.right); + apply.setTotalScore(totalScore); + apply.setUserTotalScore(userTotalScore); + //变更为已鉴定 + apply.setStatus(ApplyStatusEnum.YJD.getCode()); + apply.setIdentifyTime(new Date()); + apply.setUseTime((int) DateUtil.between(new Date(), apply.getCreatorTime(), DateUnit.MINUTE)); + apply.setIdentifyUserId(userId); + this.baseMapper.updateById(apply); + + List applyDetailsList = new ArrayList<>(); + if (CollUtil.isNotEmpty(req.getIdentifyItemList())) { + for (V2IdentifyApplyItemReq v2IdentifyApplyItemReq : req.getIdentifyItemList()) { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setId(v2IdentifyApplyItemReq.getId()); + applyDetails.setScore(v2IdentifyApplyItemReq.getIdentifyScore()); + applyDetails.setDeleteMark(0); + applyDetails.setRemark(v2IdentifyApplyItemReq.getRemark()); + applyDetailsList.add(applyDetails); + } + this.applyDetailsService.updateBatchById(applyDetailsList); + } + + if (CollUtil.isNotEmpty(applyDetailsList)) { + for (V2IdentifyApplyItemReq v2IdentifyApplyItemReq : req.getIdentifyItemList()) { + if (CollUtil.isNotEmpty(v2IdentifyApplyItemReq.getFiles())) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(v2IdentifyApplyItemReq.getFiles()) + .businessTypeID(v2IdentifyApplyItemReq.getId()) + .type(FileEventDTO.FileType.PRACTICAL_APPRAISAL_ITEM).build())); + } + } + } + if (StringUtil.isNotEmpty(table.getCertificateId())) { + if (apply.getResult().equals(0) || apply.getResult().equals(1)) { + if (apply.getSource().equals(0) || apply.getSource().equals(1) || apply.getSource().equals(3)) { + v2CultivateCertificateService.triggerCertificate(apply.getBeIdentifyUserId(), table.getCertificateId(), tenantId); + } + } + } + cultivateMqSendUtil.sendIdentityCompletionMessage(apply, table.getTableId()); + } + + + public List queryFilesLists(List businessIds) { + LambdaQueryWrapper wrapperItemFile = Wrappers.lambdaQuery(); + wrapperItemFile.in(FtbCultivateFile::getBusinessId, businessIds); + wrapperItemFile.eq(FtbCultivateFile::getEnabledMark, 0); + wrapperItemFile.orderByAsc(FtbCultivateFile::getCreatorTime); + List list = ftbCultivateFileService.list(wrapperItemFile); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + /** + * 根据ID获取Web端培养鉴定申请的详细信息 + * + * @param id 申请记录的唯一标识符 + * @return 申请的详细信息对象 + */ + @Override + public V2IdentifyApplyInfoVo getWebApplyInfo(String id) { + CultivateIdentifyApply apply = queryAndCheckApply(id); + CultivateIdentifyApplyTableBackups table = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + V2IdentifyApplyInfoVo applyInfoVo = new V2IdentifyApplyInfoVo(); + BeanUtils.copyProperties(apply, applyInfoVo); + applyInfoVo.setUserTime(apply.getUseTime()); + applyInfoVo.setTableName(table.getName()); + applyInfoVo.setRuleDesc(table.getRuleDesc()); + + String beIdentifyUserId = apply.getBeIdentifyUserId(); + List userIds = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + userIds.addAll(List.of(apply.getIdentifyUserId().split(","))); + } + userIds.add(beIdentifyUserId); + + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + + applyInfoVo.setBeIdentifyUserInfo(userMap.get(beIdentifyUserId)); + //鉴定用户信息 + List identifyUserInfo = new ArrayList<>(); + if (StringUtil.isNotEmpty(apply.getIdentifyUserId())) { + for (String s : apply.getIdentifyUserId().split(",")) { + UserBoundVO userBoundVO = userMap.get(s); + if (userBoundVO != null) { + identifyUserInfo.add(userBoundVO); + } + } + } + applyInfoVo.setIdentifyUserInfo(identifyUserInfo); + + //设置鉴定项得分集合 + BigDecimal totalScore = BigDecimal.ZERO; + BigDecimal userTotalScore = BigDecimal.ZERO; + List detailsInfoVos = this.applyDetailsMapper.getIdentifyItemList(apply.getId()); + if (CollUtil.isNotEmpty(detailsInfoVos)) { + List tempIds = new ArrayList<>(); + for (IdentifyApplyDetailsInfoVo detailsInfoVo : detailsInfoVos) { + if (detailsInfoVo.getTotalScore() != null) { + totalScore = totalScore.add(detailsInfoVo.getTotalScore()); + } + if (detailsInfoVo.getMaxScore() != null) { + totalScore = totalScore.add(new BigDecimal(detailsInfoVo.getMaxScore())); + } + if (detailsInfoVo.getScore() != null) { + userTotalScore = userTotalScore.add(detailsInfoVo.getScore()); + } + tempIds.add(detailsInfoVo.getId()); + } + applyInfoVo.setTotalScore(totalScore); + applyInfoVo.setUserTotalScore(userTotalScore); + applyInfoVo.setIdentifyItemList(detailsInfoVos); +// LambdaQueryWrapper wrapperItemFile = Wrappers.lambdaQuery(); +// wrapperItemFile.in(FtbCultivateFile::getBusinessId, tempIds); +// wrapperItemFile.orderByAsc(FtbCultivateFile::getCreatorTime); + List fileItemLists = queryFilesLists(tempIds); + if (CollUtil.isNotEmpty(fileItemLists)) { + //按照businessId 分组 + Map> fileItemMap = fileItemLists.stream().collect(Collectors.groupingBy(FtbCultivateFile::getBusinessId)); + for (IdentifyApplyDetailsInfoVo detailsInfoVo : detailsInfoVos) { + List fileItemList = fileItemMap.get(detailsInfoVo.getId()); + if (CollUtil.isNotEmpty(fileItemList)) { + detailsInfoVo.setFiles(fileItemList.stream().map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList())); + } + } + + } + } else { + applyInfoVo.setIdentifyItemList(CollUtil.newArrayList()); + applyInfoVo.setTotalScore(null); + applyInfoVo.setUserTime(null); + applyInfoVo.setResult(null); + } + + List applyFileIds = new ArrayList<>(); + applyFileIds.add(apply.getId()); + applyFileIds.add(apply.getId() + apply.getBeIdentifyUserId()); + List applyFileList = queryFilesLists(applyFileIds); + applyInfoVo.setFiles(applyFileList.stream().map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList())); + return applyInfoVo; + } + + + private V2IdentifyApplySaveDto convertToIdentifyApplySaveDto(V2IdentifyApplySaveReq req) { + V2IdentifyApplySaveDto identifyApplySaveDto = new V2IdentifyApplySaveDto(); + identifyApplySaveDto.setTableId(req.getTableId()); + identifyApplySaveDto.setAppraisalResults(req.getAppraisalResults()); + identifyApplySaveDto.setSource(req.getSource()); + if (CollUtil.isEmpty(req.getIdentifyUserIds())) { + identifyApplySaveDto.setIdentifyUserId(""); + } else { + identifyApplySaveDto.setIdentifyUserId(String.join(",", req.getIdentifyUserIds())); + } + identifyApplySaveDto.setFiles(req.getFiles()); + return identifyApplySaveDto; + } + + private void checkBatchParam(V2IdentifyApplySaveReq req) { + if (CollUtil.isEmpty(req.getBeIdentifyUserIds())) { + throw new RuntimeException("被鉴定人信息不能为空"); + } + } + + /** + * 批量保存培养鉴定申请数据 + * + * @param req 保存请求对象,包含待保存的申请数据列表 + */ + @Override + @Transactional + public void applyDataSaveBatch(V2IdentifyApplySaveReq req) { + checkBatchParam(req); + CultivateIdentifyTable table = this.identifyTableService.getById(req.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + if (!table.getIsAbnormal().equals(IdentifyItemExceptionEnum.NORMAL.getCode())) { + throw new RuntimeException("该鉴定表存在异常,不能发起鉴定申请"); + } + List applyList = CollUtil.newArrayList(); + + List identifyItemsList = this.identifyItemsService.lambdaQuery().eq(CultivateIdentifyItems::getTableId, req.getTableId()).eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE).list(); + ServiceException.notNull(identifyItemsList, "未查询到鉴定项数据"); + + List bIdentifyUserIds = req.getBeIdentifyUserIds(); + List allUserIds = new ArrayList<>(bIdentifyUserIds); + if (CollUtil.isNotEmpty(req.getIdentifyUserIds())) { + allUserIds.addAll(req.getIdentifyUserIds()); + } + Map userMap = userApiV2Util.getUserPrimaryBoundBatchReturnMap(allUserIds, null); + for (String userId : bIdentifyUserIds) { + UserBoundVO userBoundVO = userMap.get(userId); + if (userBoundVO != null) { + if (CollUtil.isNotEmpty(req.getIdentifyUserIds())) { + if (req.getIdentifyUserIds().contains(userId)) { + throw new RuntimeException("被鉴定人不能同时是鉴定人[" + userBoundVO.getUserName() + "]"); + } + } + } + } + + for (String userId : bIdentifyUserIds) { + UserBoundVO userBoundVO = userMap.get(userId); + if (userBoundVO == null) { + continue; + } + V2IdentifyApplySaveDto identifyApplySaveDto = this.convertToIdentifyApplySaveDto(req); + applyList.add(this.applyDataSaveNew(identifyApplySaveDto, table, identifyItemsList, userBoundVO)); + } + } + + public CultivateIdentifyApply applyDataSaveNew(V2IdentifyApplySaveDto req, CultivateIdentifyTable table, List identifyItemsList, UserBoundVO userBoundVO) throws RuntimeException { + CultivateIdentifyApply apply = new CultivateIdentifyApply(); + apply.setId(RandomUtil.uuId()); + apply.setName(table.getName()); + BeanUtils.copyProperties(req, apply); + apply.setStatus(ApplyStatusEnum.DJD.getCode()); + apply.setIsReIdentify(ConstantUtil.NUM_FALSE); + apply.setBeIdentifyUserId(userBoundVO.getId()); + apply.setIdentifyUserId(req.getIdentifyUserId()); + apply.setOriginalTableId(table.getId()); + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + apply.setTableId(applyTableBackups.getId()); + apply.setSource(req.getSource()); + + //生成鉴定项数据 + List applyDetailsBackupsList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setDeleteMark(0); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List applyDetailsList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(applyDetailsBackupsList)) { + applyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(apply.getId()); + applyDetails.setItemsId(item.getId()); + applyDetails.setDeleteMark(0); + return applyDetails; + }).collect(Collectors.toList())); + } else { + ServiceException.notNull(apply, "未查询到鉴定项数据"); + } + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + //保存鉴定数据 + baseMapper.insert(apply); + if (CollUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.saveBatch(applyDetailsList); + } + + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(req.getFiles()) + .businessTypeID(apply.getId() + apply.getBeIdentifyUserId()) + .type(FileEventDTO.FileType.PRACTICAL_APPRAISAL).build())); + return apply; + } + + public IdentifyDataVo reAddIdentifyApply(CultivateIdentifyTable table) throws RuntimeException { + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + // 生成鉴定项数据 + List identifyItemsList = this.identifyItemsService.lambdaQuery() + .eq(CultivateIdentifyItems::getTableId, table.getId()).eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE).list(); + List applyDetailsBackupsList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setDeleteMark(0); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + return new IdentifyDataVo(applyTableBackups, applyDetailsBackupsList); + } + + /** + * 对指定ID的培养鉴定申请进行重新鉴定操作 + * + * @param id 需要重新鉴定的申请记录唯一标识符 + */ + @Override + @Transactional + public void applyDataReIdentify(String id) { + CultivateIdentifyApply apply = baseMapper.selectById(id); + if (apply == null || apply.getDeleteMark().equals(1)) { + throw new ServiceException("未查询到实操鉴定数据"); + } + if (!apply.getResult().equals(ApplyResultEnum.BHG.getCode())) { + throw new ServiceException("鉴定不合格的数据才能重新鉴定"); + } + + + CultivateIdentifyApplyTableBackups applyTableBackupsOld = this.applyTableBackupsService.getById(apply.getTableId()); + ServiceException.notNull(applyTableBackupsOld, "未查询到实操鉴定备份表记录"); + //重新鉴定需拉取最新的鉴定表数据 + CultivateIdentifyTable table = this.identifyTableService.getById(applyTableBackupsOld.getTableId()); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + //生成鉴定申请 + CultivateIdentifyApply reIdentifyApply = JsonUtil.getJsonToBean(apply, CultivateIdentifyApply.class); + reIdentifyApply.setOriginalTableId(table.getId()); + reIdentifyApply.setPlanIdentifyTime(null); + reIdentifyApply.setIdentifyTime(null); + reIdentifyApply.setUseTime(null); + reIdentifyApply.setStatus(ApplyStatusEnum.DJD.getCode()); + reIdentifyApply.setResult(null); + reIdentifyApply.setIsReIdentify(ConstantUtil.NUM_TRUE); + + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + reIdentifyApply.setTableId(applyTableBackups.getId()); + + List identifyItemsList = this.identifyItemsService.lambdaQuery().eq(CultivateIdentifyItems::getTableId, table.getId()).eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE).list(); + + List applyDetailsBackupsList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List reIdentifyApplyDetailsList = CollUtil.newArrayList(); + reIdentifyApplyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(reIdentifyApply.getId()); + applyDetails.setItemsId(item.getId()); + return applyDetails; + }).collect(Collectors.toList())); + + //保存备份数据 + this.applyTableBackupsService.save(applyTableBackups); + if (CollUtil.isNotEmpty(applyDetailsBackupsList)) { + this.applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + //保存鉴定数据 + apply.setDeleteMark(ConstantUtil.NUM_TRUE); + baseMapper.updateById(apply); + baseMapper.insert(reIdentifyApply); + if (CollUtil.isNotEmpty(reIdentifyApplyDetailsList)) { + this.applyDetailsService.saveBatch(reIdentifyApplyDetailsList); + } + } + + /** + * 删除指定ID的培养鉴定申请记录 + * + * @param id 需要删除的申请记录唯一标识符 + */ + @Override + @Transactional + public void applyDataDelete(String id, Integer delMain) { + + CultivateIdentifyApply apply = queryAndCheckApply(id); + // 1. 删除关联的文件(实操鉴定附件) + LambdaUpdateWrapper fileUpdateWrapper = Wrappers.lambdaUpdate(); + fileUpdateWrapper.eq(FtbCultivateFile::getBusinessId, apply.getId()); + fileUpdateWrapper.eq(FtbCultivateFile::getType, FileEventDTO.FileType.PRACTICAL_APPRAISAL.getType()); + fileUpdateWrapper.set(FtbCultivateFile::getEnabledMark, ConstantUtil.NUM_TRUE); + ftbCultivateFileService.update(fileUpdateWrapper); + + // 2. 删除实操鉴定申请详情记录 + this.applyDetailsService.update(new LambdaUpdateWrapper() + .eq(CultivateIdentifyApplyDetails::getApplyId, id) + .eq(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(CultivateIdentifyApplyDetails::getDeleteMark, ConstantUtil.NUM_TRUE)); + + // 3. 删除鉴定表备份记录 + this.applyTableBackupsService.update(new LambdaUpdateWrapper() + .eq(CultivateIdentifyApplyTableBackups::getId, apply.getTableId()) + .eq(CultivateIdentifyApplyTableBackups::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(CultivateIdentifyApplyTableBackups::getDeleteMark, ConstantUtil.NUM_TRUE)); + + // 4. 删除鉴定项备份记录 + this.applyDetailsBackupsService.update(new LambdaUpdateWrapper() + .eq(CultivateIdentifyApplyDetailsBackups::getTableId, apply.getTableId()) + .eq(CultivateIdentifyApplyDetailsBackups::getDeleteMark, ConstantUtil.NUM_FALSE) + .set(CultivateIdentifyApplyDetailsBackups::getDeleteMark, ConstantUtil.NUM_TRUE)); + + // 5. 删除鉴定申请主记录 + if (ConstantUtil.NUM_TRUE == delMain) { + apply.setDeleteMark(ConstantUtil.NUM_TRUE); + baseMapper.updateById(apply); + } + } + + /** + * 查询当前用户的培养鉴定申请列表(移动端) + * + * @param req 查询条件请求对象,包含分页信息和筛选条件 + * @return 分页结果对象,包含申请列表数据和分页信息 + */ + @Override + public Page queryMyIdentifyApplyList(V2MyIdentifyApplyListAppReq req) { + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + String userId = UserProvider.getUser().getUserId(); + + page = this.baseMapper.queryMyIdentifyApplyList(page, req, userId); + if (CollUtil.isEmpty(page.getRecords())) { + return page; + } + + // 收集所有需要查询的用户ID(被鉴定人 + 鉴定人) + List userIdList = new ArrayList<>(); + for (V2IdentifyApplyListAppVo record : page.getRecords()) { + userIdList.add(record.getBeIdentifyUserId()); + if (StringUtil.isNotEmpty(record.getIdentifyUserId())) { + userIdList.addAll(List.of(record.getIdentifyUserId().split(","))); + } + } + + // 批量查询用户信息 + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIdList, null); + + // 填充被鉴定人和鉴定人信息 + for (V2IdentifyApplyListAppVo record : page.getRecords()) { + // 设置被鉴定人信息 + UserBoundVO beUserInfoVo = userMap.get(record.getBeIdentifyUserId()); + if (Objects.nonNull(beUserInfoVo)) { + record.setBeIdentifyUserInfo(beUserInfoVo); + } + + // 设置鉴定人信息(取第一个鉴定人) + if (StringUtil.isNotEmpty(record.getIdentifyUserId())) { + if (record.getStatus().equals(1)) { + { + UserBoundVO identifyUserInfoVo = userMap.get(record.getIdentifyUserId()); + if (Objects.nonNull(identifyUserInfoVo)) { + record.setIdentifyUserInfo(identifyUserInfoVo); + } + } + } + } + } + return page; + } + + @Override + public Page queryAppIdentifyApplyList(V2IdentifyApplyListAppReq req) { + String loginUserId = UserProvider.getUser().getUserId(); + List orgUserIds = new ArrayList<>(); + List myUserIds = this.baseMapper.queryMyIdentifyUserList(loginUserId); + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + if (CollUtil.isNotEmpty(req.getOrgIds())) { + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(req.getOrgIds(), null); + if (CollUtil.isEmpty(userListForOrgIds)) { + orgUserIds = new ArrayList<>(List.of("-1")); + } else { + orgUserIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + } + } + + if (req.getStatus().equals(0)) { + //权限 + 鉴定人是自己的 + List powerUserIds = userApiV2Util.getPermissionUserIds(); + if (CollUtil.isNotEmpty(powerUserIds)) { + + if (CollUtil.isNotEmpty(orgUserIds)) { + List intersection = UserApiV2Util.getIntersection(powerUserIds, orgUserIds); + if (CollUtil.isEmpty(intersection)) { + powerUserIds = new ArrayList<>(List.of("-1")); + } else { + powerUserIds = intersection; + } + } + } else { + powerUserIds = orgUserIds; + } + + page = this.baseMapper.queryAppIdentifyApplyListWaiting(page, req, powerUserIds, myUserIds, loginUserId); + if (CollUtil.isEmpty(page.getRecords())) { + return page; + } + } else if (req.getStatus().equals(1)) { + page = this.baseMapper.queryAppIdentifyApplyListComplete(page, req, orgUserIds, loginUserId); + if (CollUtil.isEmpty(page.getRecords())) { + return page; + } + } else { + List powerUserIds = userApiV2Util.getPermissionUserIds(); + if (CollUtil.isNotEmpty(powerUserIds)) { + if (CollUtil.isNotEmpty(orgUserIds)) { + List intersection = UserApiV2Util.getIntersection(powerUserIds, orgUserIds); + if (CollUtil.isEmpty(intersection)) { + powerUserIds = new ArrayList<>(List.of("-1")); + } else { + powerUserIds = intersection; + } + } + }else{ + powerUserIds = orgUserIds; + } + page = this.baseMapper.queryAppIdentifyApplyList(page, req,orgUserIds, powerUserIds, myUserIds, loginUserId); + if (CollUtil.isEmpty(page.getRecords())) { + return page; + } + } + + Map userMap = userApiV2Util.getUserPrimaryBoundBatchReturnMap(page.getRecords().stream().map(V2IdentifyApplyListAppVo::getBeIdentifyUserId).collect(Collectors.toList()), null); + for (V2IdentifyApplyListAppVo record : page.getRecords()) { + record.setBeIdentifyUserInfo(userMap.get(record.getBeIdentifyUserId())); + } + return page; + } + + /** + * 根据鉴定记录ID重新发起鉴定 + * + * @param id 需要重新发起的鉴定申请记录唯一标识符 + */ + @Override + @Transactional + public List regenerateIdentifyApply(String id) { + // 1. 查询原鉴定记录,获取基本信息 + CultivateIdentifyApply oldApply = queryAndCheckApply(id); + if (oldApply.getStatus().equals(1) && (oldApply.getResult().equals(0) || oldApply.getResult().equals(1))) { + throw new RuntimeException("鉴定已经合格,不能重新鉴定"); + } + CultivateIdentifyTable table = getIdentifyTable(oldApply.getTableId()); + if (!table.getIsAbnormal().equals(IdentifyItemExceptionEnum.NORMAL.getCode())) { + throw new RuntimeException("鉴定表存在异常,不能重新起鉴定申请"); + } + List list = this.identifyItemsService.lambdaQuery().eq(CultivateIdentifyItems::getTableId, table.getId()) + .eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE).orderByAsc(CultivateIdentifyItems::getSortCode) + .list(); + if (CollUtil.isNotEmpty(list)) { + return convertToIdentifyApplyDetailsInfoVo(list); + } + return new ArrayList<>(); + } + + /** + * 将CultivateIdentifyItems列表转换为IdentifyApplyDetailsInfoVo列表 + * + * @param itemsList 鉴定项列表 + * @return 转换后的VO列表 + */ + private List convertToIdentifyApplyDetailsInfoVo(List itemsList) { + List result = new ArrayList<>(); + for (CultivateIdentifyItems item : itemsList) { + IdentifyApplyDetailsInfoVo vo = new IdentifyApplyDetailsInfoVo(); + vo.setId(item.getId()); + vo.setName(item.getName()); + // CultivateIdentifyItems中的score对应数据库的F_TotalScore字段,即鉴定项总分值 + vo.setTotalScore(item.getScore()); + vo.setType(item.getType()); + // 在重新发起鉴定时,初始得分为null或0,因为还没有进行评分 + vo.setScore(null); + vo.setMinScore(item.getMinScore()); + vo.setMaxScore(item.getMaxScore()); + vo.setSortCode(item.getSortCode()); + // 注意:CultivateIdentifyItems中没有remark字段,所以这里设置为null + // 如果需要从其他地方获取remark,可以在这里添加相应逻辑 + vo.setRemark(null); + vo.setFiles(new ArrayList<>()); // 初始化附件列表为空 + result.add(vo); + } + return result; + } + + private CultivateIdentifyTable getIdentifyTable(String tableId) { + + // 查询备份表,获取原始鉴定表ID + CultivateIdentifyApplyTableBackups tableBackups = this.applyTableBackupsService.getById(tableId); + ServiceException.notNull(tableBackups, "未查询到实操鉴定备份表记录"); + String originalTableId = tableBackups.getTableId(); + ServiceException.notNull(originalTableId, "未查询到原始鉴定表ID"); + // 校验鉴定表是否正常 + CultivateIdentifyTable table = this.identifyTableService.getById(originalTableId); + ServiceException.notNull(table, "未查询到实操鉴定表记录"); + return table; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void reSubmit(V2IdentifyApplySubmitReq req) { + + String applyId = req.getId(); + CultivateIdentifyApply oldApply = queryAndCheckApply(applyId); + CultivateIdentifyTable identifyTable = getIdentifyTable(oldApply.getTableId()); + // 移除旧鉴定项数据 + applyDataDelete(applyId, ConstantUtil.NUM_FALSE); + // 生成新鉴定项数据 + IdentifyDataVo identifyData = reAddIdentifyApply(identifyTable); + // 鉴定表重置 使用 LambdaUpdateWrapper 重置鉴定记录状态 + + // 开始鉴定 + UserInfo user = UserProvider.getUser(); + if (user.getUserId().equals(oldApply.getBeIdentifyUserId())) { + throw new ServiceException("对不起,不能够自己给自己鉴定"); + } + CultivateIdentifyApplyTableBackups table = identifyData.getApplyTableBackups(); + List identifyItemsList = identifyData.getApplyDetailsBackupsList(); + Map itemMap = new HashMap<>(); + for (CultivateIdentifyApplyDetailsBackups item : identifyItemsList) { + itemMap.put(item.getItemsId(), item.getId()); + } + // 计算鉴定总分 + BigDecimal totalScore = CultivateIdentityUtil.calIdentityTotalScore(identifyItemsList); + // 计算总得分 + BigDecimal userTotalScore = CultivateIdentityUtil.calUserTotalScore(req.getIdentifyItemList()); + // 0-合格,1-优秀,2-不合格 + MutablePair pair = CultivateIdentityUtil.calUserIdentifyResult(table, totalScore, userTotalScore); + + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(CultivateIdentifyApply::getId, applyId); + updateWrapper.set(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + updateWrapper.set(CultivateIdentifyApply::getResult, pair.left); // 清空鉴定结果 + updateWrapper.set(CultivateIdentifyApply::getStatus, ApplyStatusEnum.YJD.getCode()); // 设置为待鉴定状态 + updateWrapper.set(CultivateIdentifyApply::getIdentifyTime, new Date()); // 清空鉴定时间 + updateWrapper.set(CultivateIdentifyApply::getUseTime, (int) DateUtil.between(new Date(), oldApply.getCreatorTime(), DateUnit.MINUTE)); // 清空用时 + updateWrapper.set(CultivateIdentifyApply::getPlanIdentifyTime, null); // 清空计划鉴定时间 + updateWrapper.set(CultivateIdentifyApply::getUserTotalScore, userTotalScore); + updateWrapper.set(CultivateIdentifyApply::getIsReIdentify, ConstantUtil.NUM_TRUE); + updateWrapper.set(CultivateIdentifyApply::getTableId, identifyData.getApplyTableBackups().getId()); + updateWrapper.set(CultivateIdentifyApply::getIdentifyUserId, user.getUserId()); + updateWrapper.set(CultivateIdentifyApply::getTotalScore, totalScore); + updateWrapper.set(CultivateIdentifyApply::getGradeName, pair.right); + baseMapper.update(new CultivateIdentifyApply(), updateWrapper); + List applyDetailsList = new ArrayList<>(); + if (CollUtil.isNotEmpty(req.getIdentifyItemList())) { + for (V2IdentifyApplyItemReq v2IdentifyApplyItemReq : req.getIdentifyItemList()) { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setId(FtbUtil.getId()); + applyDetails.setApplyId(applyId); + applyDetails.setItemsId(itemMap.get(v2IdentifyApplyItemReq.getId())); + applyDetails.setScore(v2IdentifyApplyItemReq.getIdentifyScore()); + applyDetails.setDeleteMark(0); + applyDetails.setRemark(v2IdentifyApplyItemReq.getRemark()); + applyDetailsList.add(applyDetails); + } + this.applyDetailsService.saveBatch(applyDetailsList); + } + if (CollUtil.isNotEmpty(applyDetailsList)) { + for (V2IdentifyApplyItemReq v2IdentifyApplyItemReq : req.getIdentifyItemList()) { + if (CollUtil.isNotEmpty(v2IdentifyApplyItemReq.getFiles())) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(v2IdentifyApplyItemReq.getFiles()) + .businessTypeID(itemMap.get(v2IdentifyApplyItemReq.getId())) + .type(FileEventDTO.FileType.PRACTICAL_APPRAISAL_ITEM).build())); + } + } + } + if (StringUtil.isNotEmpty(table.getCertificateId())) { + if (oldApply.getResult().equals(0) || oldApply.getResult().equals(1)) { + if (oldApply.getSource().equals(0) || oldApply.getSource().equals(1) || oldApply.getSource().equals(3)) { + v2CultivateCertificateService.triggerCertificate(oldApply.getBeIdentifyUserId(), table.getCertificateId(), user.getTenantId()); + } + } + } + cultivateMqSendUtil.sendIdentityCompletionMessage(oldApply, table.getTableId()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyTableServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyTableServiceImpl.java new file mode 100644 index 0000000..03f0649 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateIdentifyTableServiceImpl.java @@ -0,0 +1,514 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.cultivate.mapper.CultivateCoverInfoMapper; +import jnpf.cultivate.mapper.CultivateIdentifyTableMapper; +import jnpf.cultivate.service.CultivateIdentifyItemsService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.FtbCultivateIdentifyItemsPoolService; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.cultivate.v2.service.V2CultivateIdentifyTableService; +import jnpf.cultivate.v2.util.CultivateIdentityUtil; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.enums.AppraisalScoreTypeEnum; +import jnpf.enums.cultivate.IdentifyItemExceptionEnum; +import jnpf.enums.cultivate.IdentifyScoreTypeEnum; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.po.common.FtbCultivateCoverInfoEntity; +import jnpf.model.cultivate.v2.identify.req.*; +import jnpf.model.cultivate.v2.identify.vo.V2CateIdentifyTableVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableInfoVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableListVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableScoreConfigVo; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + + +@Slf4j +@Service +public class V2CultivateIdentifyTableServiceImpl implements V2CultivateIdentifyTableService { + + @Autowired + private CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Autowired + private CultivateIdentifyItemsService itemsService; + + @Autowired + private FtbCultivateIdentifyItemsPoolService ftbCultivateIdentifyItemsPoolService; + + @Autowired + private CultivateCoverInfoMapper cultivateCoverInfoMapper; + + + @Autowired + private FtbCultivateLabelService ftbCultivateLabelService; + + + /** + * 鉴定表/列表 + * + * @param req 帅选条件 + * @return Page + */ + @Override + public Page getPageList(V2IdentifyTableListReq req) { + + Page page = this.cultivateIdentifyTableMapper.getPageListV2(Page.of(req.getCurrentPage(), req.getPageSize()), req); + if (CollUtil.isNotEmpty(page.getRecords())) { + List tableIdList = page.getRecords().stream().map(V2IdentifyTableListVo::getId).collect(Collectors.toList()); + List countDtoList = itemsService.countForTableId(tableIdList); + Map countMap = new HashMap<>(); + if (CollUtil.isNotEmpty(countDtoList)) { + countMap = countDtoList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, BatchCommonCountDto::getNum)); + } + for (V2IdentifyTableListVo record : page.getRecords()) { + Integer cnt = countMap.get(record.getId()); + record.setIdentifyCount(cnt == null ? 0 : cnt); + } + } + return page; + } + + /** + * 鉴定表/详情 + * + * @param tableId 帅选条件 + * @return V2IdentifyTableInfoVo + */ + @Override + public V2IdentifyTableInfoVo getInfo(String tableId) { + CultivateIdentifyTable table = this.cultivateIdentifyTableMapper.selectById(tableId); + if (table == null || table.getDeleteMark() == 1) { + throw new RuntimeException("鉴定表不存在或者已经被删除"); + } + V2IdentifyTableInfoVo tableInfoVo = new V2IdentifyTableInfoVo(); + tableInfoVo.setId(table.getId()); + tableInfoVo.setName(table.getName()); + tableInfoVo.setRuleDesc(table.getRuleDesc()); + tableInfoVo.setPassType(table.getPassType()); + tableInfoVo.setPassScore(table.getPassScore()); + tableInfoVo.setExcellentType(table.getExcellentType()); + tableInfoVo.setExcellentScore(table.getExcellentScore()); + tableInfoVo.setLabelId(table.getLabelId()); + tableInfoVo.setCoverId(table.getCoverId()); + tableInfoVo.setCoverUrl(table.getCoverUrl()); + tableInfoVo.setScoreType(table.getScoreType()); + tableInfoVo.setCertificateId(table.getCertificateId()); + if (StringUtils.isNotEmpty(table.getScoreConfig())) { + tableInfoVo.setScoreConfig(JSON.parseArray(table.getScoreConfig(), V2IdentifyTableScoreConfigVo.class)); + } + + + tableInfoVo.setIdentifyItemList(itemsService.listWithCategory(tableId)); + + + return tableInfoVo; + } + + /** + * 鉴定表/新增 + * + * @param req 帅选条件 + */ + + @Override + @Transactional + public void saveData(V2IdentifyTableSaveReq req) { + preCheck(req); + String userId = UserProvider.getUser().getUserId(); + // 检查课程名称是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateIdentifyTable::getName, req.getName()); + queryWrapper.eq(CultivateIdentifyTable::getDeleteMark, 0); // 只检查有效的课程 + long count = cultivateIdentifyTableMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("鉴定表名称已经存在"); + } + CultivateIdentifyTable tableSave = CultivateIdentifyTable.builder() + .name(req.getName()) + .ruleId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.IDENTIFY)) + .ruleDesc(req.getRuleDesc()) + .passType(req.getPassType()) + .passScore(req.getPassScore()) + .excellentType(req.getExcellentType()) + .excellentScore(req.getExcellentScore()) + .coverId(req.getCoverId()) + .coverUrl(req.getCoverUrl()) + .labelId(req.getLabelId()) + .scoreType(req.getScoreType()) + .certificateId(req.getCertificateId()) + .scoreConfig(CollUtil.isNotEmpty(req.getScoreConfig()) ? JSON.toJSONString(req.getScoreConfig()) : "") + .build(); + tableSave.setCreatorTime(new Date()); + tableSave.setLastModifyTime(new Date()); + tableSave.setCreatorUserId(userId); + tableSave.setLastModifyUserId(userId); + tableSave.setTenantId(UserProvider.getUser().getTenantId()); + tableSave.setDeleteMark(0); + cultivateIdentifyTableMapper.insert(tableSave); + //批量新增鉴定项 + + addIdentifyItemsData(req, tableSave.getId()); + } + + private void addIdentifyItemsData(V2IdentifyTableSaveReq req, String tableId) { + List batchSaveItemsList = new ArrayList<>(); + for (V2IdentifyItemsSaveReq items : req.getIdentifyItemList()) { + CultivateIdentifyItems.CultivateIdentifyItemsBuilder cultivateIdentifyItemsBuilder = CultivateIdentifyItems.builder() + .tableId(tableId) + .cateId(items.getCateId()) + .name(items.getName()) + .score(items.getScore()) + .type(items.getType()) + .minScore(items.getMinScore()) + .maxScore(items.getMaxScore()) + .poolId(items.getPoolId()) + .businessId(items.getBusinessId()) + .isAbnormal(IdentifyItemExceptionEnum.NORMAL.getCode()); + batchSaveItemsList.add(cultivateIdentifyItemsBuilder.build()); + } + if (CollUtil.isNotEmpty(batchSaveItemsList)) { + this.itemsService.saveBatch(batchSaveItemsList); + } + } + + private void preCheck(V2IdentifyTableSaveReq req) { + if (req.getScoreType().equals(IdentifyScoreTypeEnum.LEVEL_SYSTEM.getCode())) { + + if (CollUtil.isEmpty(req.getScoreConfig())) { + throw new RuntimeException("请选择等级制配置"); + } + if (req.getScoreConfig().size() < 2) { + throw new RuntimeException("等级制配置至少需要两个"); + } + V2IdentifyTableScoreConfigVo v2IdentifyTableScoreConfigVo = req.getScoreConfig().get(0); + if (v2IdentifyTableScoreConfigVo.getMin() == null) { + throw new RuntimeException("第1项得分配置不正确"); + } + if (v2IdentifyTableScoreConfigVo.getResult() == null) { + throw new RuntimeException("第1项结果配置不正确"); + } + V2IdentifyTableScoreConfigVo end = req.getScoreConfig().get(req.getScoreConfig().size() - 1); + if (end.getMax() == null) { + throw new RuntimeException("最后一项得分配置不正确"); + } + if (end.getResult() == null) { + throw new RuntimeException("最后一项结果配置不正确"); + } + for (int i = 0; i < req.getScoreConfig().size(); i++) { + V2IdentifyTableScoreConfigVo vo = req.getScoreConfig().get(i); + if (StringUtils.isEmpty(vo.getLevelName())) { + throw new RuntimeException("第" + (i + 1) + "项评级名称不能为空"); + } + if (vo.getLevelName().length() > 5) { + throw new RuntimeException("第" + (i + 1) + "项评级名称不能超过5个字符"); + } + } + } + if (CollUtil.isEmpty(req.getIdentifyItemList())) { + throw new RuntimeException("鉴定项不能为空"); + } + for (V2IdentifyItemsSaveReq items : req.getIdentifyItemList()) { + if (StringUtils.isEmpty(items.getName())) { + throw new RuntimeException("鉴定项名称不能为空"); + } + if (StringUtils.isEmpty(items.getCateId())) { + throw new RuntimeException("鉴定项分类不能是空"); + } + if (StringUtils.isEmpty(items.getPoolId())) { + throw new RuntimeException("鉴定项poolId不能是空"); + } + } + + // 校验鉴定项是否有重复(除id、poolId、sortCode外全部相同) + Set itemKeys = new HashSet<>(); + for (int i = 0; i < req.getIdentifyItemList().size(); i++) { + V2IdentifyItemsSaveReq items = req.getIdentifyItemList().get(i); + // 构建唯一键:name + cateId + type + score + minScore + maxScore + String key = buildItemUniqueKey(items); + if (itemKeys.contains(key)) { + throw new RuntimeException("第" + (i + 1) + "项鉴定项与前面的鉴定项重复(名称、分类、类型、分值等全部相同)"); + } + itemKeys.add(key); + } + + } + + + @Override + @Transactional + public void updateData(String tableId, V2IdentifyTableSaveReq req) { + preCheck(req); + String userId = UserProvider.getUser().getUserId(); + CultivateIdentifyTable table = this.cultivateIdentifyTableMapper.selectById(tableId); + if (table == null || table.getDeleteMark() == 1) { + throw new RuntimeException("鉴定表不存在或者已经被删除"); + } + // 检查课程名称是否与其他课程重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateIdentifyTable::getName, req.getName()); + queryWrapper.ne(CultivateIdentifyTable::getId, tableId); // 排除当前课程 + queryWrapper.eq(CultivateIdentifyTable::getDeleteMark, 0); // 只检查有效的课程 + long count = cultivateIdentifyTableMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("名称不能重复"); + } + + CultivateIdentifyTable tableSave = CultivateIdentifyTable.builder() + .name(req.getName()) + .ruleDesc(req.getRuleDesc()) + .passType(req.getPassType()) + .passScore(req.getPassScore()) + .excellentType(req.getExcellentType()) + .excellentScore(req.getExcellentScore()) + .coverId(req.getCoverId()) + .coverUrl(req.getCoverUrl()) + .labelId(req.getLabelId()) + .scoreType(req.getScoreType()) + .certificateId(req.getCertificateId()) + .isAbnormal(IdentifyItemExceptionEnum.NORMAL.getCode()) + .scoreConfig(CollUtil.isNotEmpty(req.getScoreConfig()) ? JSON.toJSONString(req.getScoreConfig()) : "").build(); + tableSave.setLastModifyTime(new Date()); + tableSave.setLastModifyUserId(userId); + tableSave.setTenantId(UserProvider.getUser().getTenantId()); + tableSave.setDeleteMark(0); + tableSave.setId(tableId); + cultivateIdentifyTableMapper.updateById(tableSave); + itemsService.remove(new LambdaUpdateWrapper().eq(CultivateIdentifyItems::getTableId, tableId)); + //批量新增鉴定项 + addIdentifyItemsData(req, tableSave.getId()); + } + + /** + * 鉴定表/删除 + * + * @param tableId 帅选条件 + */ + @Override + @Transactional + public void deleteData(String tableId) { + CultivateIdentifyTable table = this.cultivateIdentifyTableMapper.selectById(tableId); + if (table == null || table.getDeleteMark() == 1) { + throw new RuntimeException("鉴定表不存在或者已经被删除"); + } + table.setDeleteMark(1); + cultivateIdentifyTableMapper.updateById(table); + + this.itemsService.update(new CultivateIdentifyItems(), new LambdaUpdateWrapper() + .eq(CultivateIdentifyItems::getTableId, tableId) + .eq(CultivateIdentifyItems::getDeleteMark, 1) + ); + } + + @Override + @Transactional + public void importData(V2IdentifyTableImportSaveReq req) { + String userId = UserProvider.getUser().getUserId(); + FtbCultivateCoverInfoEntity ftbCultivateCoverInfoEntity = cultivateCoverInfoMapper.selectById("1"); + if (ftbCultivateCoverInfoEntity == null) { + ftbCultivateCoverInfoEntity = new FtbCultivateCoverInfoEntity(); + } + // 检查课程名称是否重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateIdentifyTable::getName, req.getName()) + .eq(CultivateIdentifyTable::getDeleteMark, 0); // 只检查有效的课程 + long count = cultivateIdentifyTableMapper.selectCount(queryWrapper); + if (count > 0) { + throw new RuntimeException("鉴定表名称已经存在"); + } + + + CultivateIdentifyTable tableSave = CultivateIdentifyTable.builder() + .name(req.getName()) + .ruleId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.IDENTIFY)) + .ruleDesc(req.getRuleDesc()) + .passType(req.getPassType()) + .passScore(req.getPassScore()) + .excellentType(req.getExcellentType()) + .excellentScore(req.getExcellentScore()) + .coverId(ftbCultivateCoverInfoEntity.getId()) + .coverUrl(ftbCultivateCoverInfoEntity.getPath()) + .labelId(req.getLabelId()) + .scoreType(req.getScoreType()) + .scoreConfig("").build(); + tableSave.setCreatorTime(new Date()); + tableSave.setLastModifyTime(new Date()); + tableSave.setCreatorUserId(userId); + tableSave.setLastModifyUserId(userId); + tableSave.setTenantId(UserProvider.getUser().getTenantId()); + tableSave.setDeleteMark(0); + cultivateIdentifyTableMapper.insert(tableSave); + //批量新增鉴定项 + List batchSaveItemsList = new ArrayList<>(); + List list = new ArrayList<>(); + List dataList = ftbCultivateIdentifyItemsPoolService.listAll(); + Set existNameList = new HashSet<>(); + Map map = new HashMap<>(); + Map oldItemMap = new HashMap<>(); + for (FtbCultivateIdentifyItemsPool identifyItemsPool : dataList) { + String s = CultivateIdentityUtil.buildRepeatKey(identifyItemsPool); + existNameList.add(s); + map.put(s, identifyItemsPool.getId()); + oldItemMap.put(s, identifyItemsPool); + } + for (V2IdentifyItemsImportSaveReq items : req.getIdentifyItemList()) { + String itemCombine = CultivateIdentityUtil.buildRepeatKey(items); + CultivateIdentifyItems identifyItems = CultivateIdentifyItems.builder() + .tableId(tableSave.getId()) + .cateId(items.getCateId()) + .name(items.getName()) + .score(items.getScore()) + .type(items.getType()) + .minScore(items.getMinScore()) + .maxScore(items.getMaxScore()).build(); + String poolId = map.get(itemCombine); + if (StringUtils.isEmpty(poolId)) { + poolId = UserApiV2Util.generateId(); + identifyItems.setBusinessId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.ITEM_POOL)); + } else { + FtbCultivateIdentifyItemsPool ftbCultivateIdentifyItemsPool = oldItemMap.get(itemCombine); + if (ftbCultivateIdentifyItemsPool != null) { + identifyItems.setBusinessId(ftbCultivateIdentifyItemsPool.getBusinessId()); + } + } + identifyItems.setPoolId(poolId); + batchSaveItemsList.add(identifyItems); + if (existNameList.contains(itemCombine)) { + continue; + } + list.add(buildFtbCultivateIdentifyItemsPool(items, userId, identifyItems)); + } + if (CollUtil.isNotEmpty(batchSaveItemsList)) { + this.itemsService.saveBatch(batchSaveItemsList); + } + + if (CollUtil.isNotEmpty(list)) { + this.ftbCultivateIdentifyItemsPoolService.saveBatch(list); + } + + } + + @Override + public long queryByTableName(String tableName) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(CultivateIdentifyTable::getName, tableName) + .eq(CultivateIdentifyTable::getDeleteMark, 0); // 只检查有效的课程 + return cultivateIdentifyTableMapper.selectCount(queryWrapper); + } + + @Override + public List getTableForCate() { + List ret = new ArrayList<>(); + List labelVoList = ftbCultivateLabelService.getList(2, 0); + + ret.add(V2CateIdentifyTableVo.builder().id("1").name("未设置分类").build()); + buildRet(ret, labelVoList); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper + .select(CultivateIdentifyTable::getId, CultivateIdentifyTable::getName, CultivateIdentifyTable::getLabelId) + .eq(CultivateIdentifyTable::getIsAbnormal, IdentifyItemExceptionEnum.NORMAL.getCode()) + .eq(CultivateIdentifyTable::getDeleteMark, 0); // 只检查有效的课程 + List tables = cultivateIdentifyTableMapper.selectList(queryWrapper); + Map> map = new HashMap<>(); + if (CollUtil.isNotEmpty(tables)) { + for (CultivateIdentifyTable table : tables) { + String key = table.getLabelId(); + if (StringUtils.isEmpty(table.getLabelId())) { + key = "1"; + } + List list = map.get(key); + if (CollUtil.isEmpty(list)) { + list = new ArrayList<>(); + map.put(key, list); + } + list.add(table); + } + } + for (V2CateIdentifyTableVo v2CateIdentifyTableVo : ret) { + List list = map.get(v2CateIdentifyTableVo.getId()); + v2CateIdentifyTableVo.setTableList(list); + } + return ret; + + + } + + private void buildRet(List ret, List labelVoList) { + if (CollUtil.isEmpty(labelVoList)) { + return; + } + for (FtbCultivateLabelVo labelVo : labelVoList) { + ret.add(V2CateIdentifyTableVo.builder().id(labelVo.getId()).name(labelVo.getName()).build()); + } + } + + @NotNull + private static FtbCultivateIdentifyItemsPool buildFtbCultivateIdentifyItemsPool(V2IdentifyItemsImportSaveReq items, String userId, CultivateIdentifyItems identifyItems) { + FtbCultivateIdentifyItemsPool entity = new FtbCultivateIdentifyItemsPool(); + entity.setName(items.getName()); + entity.setCateId(items.getCateId()); + entity.setType(items.getType()); + if (items.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + entity.setTotalScore(items.getScore().intValue()); + } else { + entity.setMinScore(items.getMinScore()); + entity.setMaxScore(items.getMaxScore()); + } + entity.setEnabledMark(0); + entity.setCreatorTime(new Date()); + entity.setCreatorUserId(userId); + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(userId); + entity.setId(identifyItems.getPoolId()); + entity.setBusinessId(identifyItems.getBusinessId()); + return entity; + } + + /** + * 构建鉴定项唯一键(用于检测重复) + * + * @param items 鉴定项 + * @return 唯一键字符串 + */ + private String buildItemUniqueKey(V2IdentifyItemsSaveReq items) { + // 将所有字段拼接成唯一键,使用特殊分隔符避免冲突 + // 排除 poolId 和 sortCode + // 根据分值类型决定比较字段:0-固定分比较score,1-浮动分比较minScore和maxScore + StringBuilder sb = new StringBuilder(); + sb.append(items.getName() != null ? items.getName() : "") + .append("|") + .append(items.getType() != null ? items.getType() : ""); + + // 根据分值类型添加对应的分值字段 + if (items.getType() != null && items.getType() == 0) { + // 固定分:只比较score + sb.append("|").append(items.getScore() != null ? items.getScore().toString() : ""); + } else { + // 浮动分:比较minScore和maxScore + sb.append("|").append(items.getMinScore() != null ? items.getMinScore().toString() : "") + .append("|").append(items.getMaxScore() != null ? items.getMaxScore().toString() : ""); + } + + return sb.toString(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateOfflineTrainServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateOfflineTrainServiceImpl.java new file mode 100644 index 0000000..d901420 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateOfflineTrainServiceImpl.java @@ -0,0 +1,64 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.mapper.FtbCultivateOfflineTrainMapper; +import jnpf.cultivate.service.impl.FtbCultivateFileService; +import jnpf.cultivate.v2.service.V2CultivateOfflineTrainService; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.offline.FtbCultivateOfflineTrain; +import jnpf.model.cultivate.v2.offline.V2CultivateOfflineTrainUpdateDTO; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +public class V2CultivateOfflineTrainServiceImpl implements V2CultivateOfflineTrainService { + + @Autowired + private FtbCultivateOfflineTrainMapper ftbCultivateOfflineTrainMapper; + + @Autowired + private FtbCultivateFileService ftbCultivateFileService; + + /** + * 修改培训结果 + * + * @param req + */ + @Override + public void updateTrainingResults(V2CultivateOfflineTrainUpdateDTO req) { + //修改线下培训反馈结果 + String userId = UserProvider.getUser().getUserId(); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, req.getId()) + .set(FtbCultivateOfflineTrain::getFeedHeadUserId, userId) + .set(FtbCultivateOfflineTrain::getHeadContent, req.getHeadContent()) + .set(FtbCultivateOfflineTrain::getFeedBackTime, new Date()); + ftbCultivateOfflineTrainMapper.update(new FtbCultivateOfflineTrain(), updateWrapper); + + //删除就得反馈文件 + LambdaQueryWrapper fileRemoveWrapper = Wrappers.lambdaQuery(); + fileRemoveWrapper + .eq(FtbCultivateFile::getBusinessId, req.getId()) + .eq(FtbCultivateFile::getType, FileEventDTO.FileType.OFFLINE_TRAINING_FEEDBACK.getType()); + ftbCultivateFileService.remove(fileRemoveWrapper); + + // 附件文件 + if (CollUtil.isNotEmpty(req.getFiles())) { + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(req.getFiles()) + .businessTypeID(req.getId()) + .type(FileEventDTO.FileType.OFFLINE_TRAINING_FEEDBACK) + .build())); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePositionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePositionServiceImpl.java new file mode 100644 index 0000000..2f8157f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePositionServiceImpl.java @@ -0,0 +1,2245 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivatePositionCourseCertificateService; +import jnpf.cultivate.service.FtbCultivatePositionCoursePracticeService; +import jnpf.cultivate.service.FtbCultivatePositionCourseService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourceLearningService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourseExamService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourseIdentityService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.*; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyTable; +import jnpf.entity.cultivate.CultivatePositionCourseLogEntity; +import jnpf.enums.cultivate.v2.mq.CultivateTypeEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.FtbJobLearningPaginDTO; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.*; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.course.vo.app.*; +import jnpf.model.cultivate.v2.course.web.req.V2NextUserCultivateCourseListReq; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.position.req.*; +import jnpf.model.cultivate.v2.position.vo.*; +import jnpf.model.cultivate.v2.promotion.vo.PromotionAndPostVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePostAndGradeVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePromotionErrorVo; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class V2CultivatePositionServiceImpl implements V2CultivatePositionService { + @Autowired + private UserApiV2Util userApiV2Util; + @Autowired + private FtbCultivatePositionCourseService ftbCultivatePositionCourseService; + @Autowired + private FtbCultivatePositionCourseExamService ftbCultivatePositionCourseExamService; + @Autowired + private FtbCultivatePositionCourseIdentityService ftbCultivatePositionCourseIdentityService; + @Autowired + private FtbCultivatePositionCoursePracticeService ftbCultivatePositionCoursePracticeService; + @Autowired + private FtbCultivatePositionCourseCertificateService ftbCultivatePositionCourseCertificateService; + @Autowired + private FtbCultivatePositionSettingService ftbCultivatePositionSettingService; + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + @Autowired + private FtbCultivatePositionCourceLearningService ftbCultivatePositionCourceLearningService; + + @Autowired + private FtbCultivatePositionLogService ftbCultivatePositionLogService; + + @Autowired + private CultivatePositionCourseLogService cultivatePositionCourseLogService; + + @Autowired + private FtbCultivatePromotionNewMapper ftbCultivatePromotionNewMapper; + + @Autowired + private FtbCultivatePromotionMemberNewMapper ftbCultivatePromotionMemberNewMapper; + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Autowired + private FtbCultivatePositionMapper ftbCultivatePositionMapper; + + @Autowired + private FtbCultivatePositionExamMapper ftbCultivatePositionExamMapper; + + @Autowired + private V2CultivatePostStudyService v2CultivatePostStudyService; + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + + /** + * 分页查询岗位学习列表 + * + * @param req 分页查询请求参数,包含当前页码和页面大小等信息 + * @return 返回岗位学习列表的分页数据,每页包含课程、考试、鉴定、学习地图的数量统计 + */ + public Page webPageList(FtbJobLearningPaginDTO req) { + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + + Page result = ftbCultivatePositionMapper.webPageList(page, req); + List records = result.getRecords(); + if (CollUtil.isNotEmpty(records)) { + // 获取所有岗位学习ID列表 + List positionLearnIds = records.stream() + .map(WebPositionLearningListVo::getId) + .collect(Collectors.toList()); + + // 根据岗位学习ids 统计出课程数量 map + Map courseCountMap = ftbCultivatePositionCourseService.countByPositionLearnIds(positionLearnIds); + + // 根据岗位学习ids 统计出考试数量 map + Map examCountMap = ftbCultivatePositionCourseExamService.countByPositionLearnIds(positionLearnIds); + + // 根据岗位学习ids 统计出鉴定数量 map + Map identityCountMap = ftbCultivatePositionCourseIdentityService.countByPositionLearnIds(positionLearnIds); + + //查询学习地图 + List positionIds = records.stream() + .map(WebPositionLearningListVo::getPostId) + .collect(Collectors.toList()); + List listCount = ftbCultivatePromotionNewMapper.countByPositionIds(positionIds); + Map promotionCountMap = buildPromotionNumMap(listCount); + + //批量查询岗位名称 + Map positionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(positionIds, null); + // 回填每个岗位学习的课程 考试 鉴定 实践/练习的数量 + for (WebPositionLearningListVo record : records) { + record.setCourseNum(courseCountMap.getOrDefault(record.getId(), 0L)); + record.setExamNum(Math.toIntExact(examCountMap.getOrDefault(record.getId(), 0L))); + record.setIdentificationNum(identityCountMap.getOrDefault(record.getId(), 0L)); + record.setMapNum(promotionCountMap.getOrDefault(record.getPostId(), 0L)); + PositionVO positionVO = positionVOMap.get(record.getPostId()); + if (positionVO != null) { + record.setPostName(positionVO.getFullName()); + record.setPostCustomId(positionVO.getEnCode()); + } + } + } + + return result; + } + + /** + * 构建促销数量映射表 + * 根据帖子ID对促销数据进行分组统计,返回每个帖子ID对应的促销数量 + * + * @param listCount 促销与帖子视图对象列表,包含帖子ID等信息 + * @return Map 以帖子ID为键、对应促销数量为值的映射表 + */ + private Map buildPromotionNumMap(List listCount) { + if (CollUtil.isEmpty(listCount)) { + return new HashMap<>(); + } + return listCount.stream() + .collect(Collectors.groupingBy( + PromotionAndPostVo::getPostId, + Collectors.counting() + )); + } + + + /** + * 添加岗位学习 + * + * @param ftbCultivatePosition 岗位学习实体 + * @return 操作结果 + */ + @Override + @Transactional + public String add(FtbCultivatePositionSaveReq ftbCultivatePosition) { + ftbCultivatePosition.preCheckParams(); + v2CultivateBatchQueryService.positionCheckAvailable(ftbCultivatePosition); + + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getPostId, ftbCultivatePosition.getPostId()) + .eq(FtbCultivatePosition::getEnabledMark, 0); + + FtbCultivatePosition existingPosition = ftbCultivatePositionMapper.selectOne(positionWrapper); + + if (existingPosition != null) { + throw new RuntimeException("该岗位已经配置了岗位学习,请不要重复操作"); + } + + FtbCultivatePosition newPosition = new FtbCultivatePosition(); + newPosition.setPostId(ftbCultivatePosition.getPostId()); + newPosition.setIsConfiguredToGrade(ftbCultivatePosition.getIsConfiguredToGrade()); + newPosition.setIsGrounding(PositionConstants.IS_GROUNDING_ONLINE); + newPosition.setLastModifyTime(new Date()); + newPosition.setLastModifyUserId(UserProvider.getLoginUserId()); + ftbCultivatePositionMapper.insert(newPosition); + + realDealPosition(ftbCultivatePosition, newPosition.getId()); + + return newPosition.getId(); + } + + /** + * 处理岗位学习数据 + * + * @param ftbCultivatePosition 岗位学习实体 + * @param cultivatePositionId 岗位学习ID + */ + private void realDealPosition(FtbCultivatePositionSaveReq ftbCultivatePosition, String cultivatePositionId) { + + if (CollUtil.isEmpty(ftbCultivatePosition.getCourseList())) { + return; + } + // 准备批量插入的数据集合 + List positionCourses = new ArrayList<>(); + List positionCourseExams = new ArrayList<>(); + List positionCourseIdentitys = new ArrayList<>(); + List positionCoursePractices = new ArrayList<>(); + List positionCourseCertificates = new ArrayList<>(); + + for (FtbCultivatePositionCourseReq courseReq : ftbCultivatePosition.getCourseList()) { + // 添加岗位学习课程表记录 + FtbCultivatePositionCourse positionCourse = new FtbCultivatePositionCourse(); + positionCourse.setCourseId(courseReq.getCourseId()); + positionCourse.setPostLearnId(cultivatePositionId); + positionCourse.setPostRankId(ftbCultivatePosition.getPostId()); + positionCourse.setCompulsory(courseReq.getCompulsory()); + + positionCourse.setGradeId(courseReq.getGradeId()); + positionCourse.setSortCode(courseReq.getSortCode()); + + positionCourse.setEnabledMark(0); + positionCourses.add(positionCourse); + + // 添加岗位学习课程考试表记录 + if (courseReq.getExam() != null) { + positionCourseExams.add(getFtbCultivatePositionCourseExam(ftbCultivatePosition, cultivatePositionId, courseReq)); + } + + // 添加岗位学习课程鉴定表记录 + if (courseReq.getIdentity() != null) { + positionCourseIdentitys.add(getFtbCultivatePositionCourseIdentity(ftbCultivatePosition, cultivatePositionId, courseReq)); + } + + // 添加岗位学习课程练习表记录 + if (CollUtil.isNotEmpty(courseReq.getPracticeList())) { + for (FtbCultivatePositionCoursePracticeReq practiceReq : courseReq.getPracticeList()) { + positionCoursePractices.add(buildFtbCultivatePositionCoursePractice(ftbCultivatePosition, cultivatePositionId, courseReq, practiceReq)); + } + } + + // 添加岗位学习课程证书表记录 + if (courseReq.getCertificate() != null) { + positionCourseCertificates.add(getFtbCultivatePositionCourseCertificate(ftbCultivatePosition, cultivatePositionId, courseReq)); + } + } + //添加配置数据 + List setting = ftbCultivatePosition.getSetting(); + if (CollUtil.isNotEmpty(setting)) { + ftbCultivatePositionSettingService.saveBatch(getFtbCultivatePositionSettings(ftbCultivatePosition, cultivatePositionId, setting)); + } + // 批量保存数据 + if (CollUtil.isNotEmpty(positionCourses)) { + ftbCultivatePositionCourseService.saveBatch(positionCourses); + } + if (CollUtil.isNotEmpty(positionCourseExams)) { + ftbCultivatePositionCourseExamService.saveBatch(positionCourseExams); + } + if (CollUtil.isNotEmpty(positionCourseIdentitys)) { + ftbCultivatePositionCourseIdentityService.saveBatch(positionCourseIdentitys); + } + if (CollUtil.isNotEmpty(positionCoursePractices)) { + ftbCultivatePositionCoursePracticeService.saveBatch(positionCoursePractices); + } + if (CollUtil.isNotEmpty(positionCourseCertificates)) { + ftbCultivatePositionCourseCertificateService.saveBatch(positionCourseCertificates); + } + + } + + @NotNull + private static FtbCultivatePositionCoursePractice buildFtbCultivatePositionCoursePractice(FtbCultivatePositionSaveReq ftbCultivatePosition, String cultivatePositionId, FtbCultivatePositionCourseReq courseReq, FtbCultivatePositionCoursePracticeReq practiceReq) { + FtbCultivatePositionCoursePractice positionCoursePractice = new FtbCultivatePositionCoursePractice(); + positionCoursePractice.setCourseId(courseReq.getCourseId()); + positionCoursePractice.setPostLearnId(cultivatePositionId); + positionCoursePractice.setBusinessId(practiceReq.getBusinessId()); + positionCoursePractice.setPostRankId(ftbCultivatePosition.getPostId()); + positionCoursePractice.setNum(practiceReq.getNum()); + positionCoursePractice.setGradeId(courseReq.getGradeId()); + positionCoursePractice.setEnabledMark(0); + return positionCoursePractice; + } + + @NotNull + private static List getFtbCultivatePositionSettings(FtbCultivatePositionSaveReq ftbCultivatePosition, String cultivatePositionId, List setting) { + List ftbCultivatePositionSettings = new ArrayList<>(); + for (FtbCultivatePositionSettingReq ftbCultivatePositionSettingReq : setting) { + FtbCultivatePositionSetting ftbCultivatePositionSetting = new FtbCultivatePositionSetting(); + ftbCultivatePositionSetting.setPostLearnId(cultivatePositionId); + ftbCultivatePositionSetting.setPostRankId(ftbCultivatePosition.getPostId()); + ftbCultivatePositionSetting.setQualified(ftbCultivatePositionSettingReq.getQualified()); + ftbCultivatePositionSetting.setGradeId(ftbCultivatePositionSettingReq.getGradeId()); + ftbCultivatePositionSettings.add(ftbCultivatePositionSetting); + } + return ftbCultivatePositionSettings; + } + + @NotNull + private static FtbCultivatePositionCourseCertificate getFtbCultivatePositionCourseCertificate(FtbCultivatePositionSaveReq ftbCultivatePosition, String cultivatePositionId, FtbCultivatePositionCourseReq courseReq) { + FtbCultivatePositionCourseCertificate positionCourseCertificate = new FtbCultivatePositionCourseCertificate(); + positionCourseCertificate.setCourseId(courseReq.getCourseId()); + positionCourseCertificate.setPostLearnId(cultivatePositionId); + positionCourseCertificate.setPostRankId(ftbCultivatePosition.getPostId()); + positionCourseCertificate.setCertificateId(courseReq.getCertificate().getCertificateId()); + positionCourseCertificate.setGradeId(courseReq.getGradeId()); + positionCourseCertificate.setEnabledMark(0); + return positionCourseCertificate; + } + + @NotNull + private static FtbCultivatePositionCourseIdentity getFtbCultivatePositionCourseIdentity(FtbCultivatePositionSaveReq ftbCultivatePosition, String cultivatePositionId, FtbCultivatePositionCourseReq courseReq) { + FtbCultivatePositionCourseIdentity positionCourseIdentity = new FtbCultivatePositionCourseIdentity(); + positionCourseIdentity.setCourseId(courseReq.getCourseId()); + positionCourseIdentity.setPostLearnId(cultivatePositionId); + positionCourseIdentity.setPostRankId(ftbCultivatePosition.getPostId()); + positionCourseIdentity.setIdentityId(courseReq.getIdentity().getIdentityId()); + positionCourseIdentity.setGradeId(courseReq.getGradeId()); + positionCourseIdentity.setEnabledMark(0); + return positionCourseIdentity; + } + + @NotNull + private static FtbCultivatePositionCourseExam getFtbCultivatePositionCourseExam(FtbCultivatePositionSaveReq ftbCultivatePosition, String cultivatePositionId, FtbCultivatePositionCourseReq courseReq) { + FtbCultivatePositionCourseExam positionCourseExam = new FtbCultivatePositionCourseExam(); + positionCourseExam.setCourseId(courseReq.getCourseId()); + positionCourseExam.setPostLearnId(cultivatePositionId); + positionCourseExam.setPostRankId(ftbCultivatePosition.getPostId()); + positionCourseExam.setExamId(courseReq.getExam().getExamId()); + positionCourseExam.setGradeId(courseReq.getGradeId()); + positionCourseExam.setEnabledMark(0); + return positionCourseExam; + } + + /** + * 修改岗位学习 + * + * @param ftbCultivatePosition 岗位学习实体 + * @return 操作结果 + */ + @Override + @Transactional + public String update(FtbCultivatePositionSaveReq ftbCultivatePosition) { + //入参检查 + if (ftbCultivatePosition == null || StringUtils.isEmpty(ftbCultivatePosition.getId())) { + throw new RuntimeException("岗位学习信息不能为空"); + } + + //入参检查 + ftbCultivatePosition.preCheckParams(); + v2CultivateBatchQueryService.positionCheckAvailable(ftbCultivatePosition); + //查询原有岗位学习主表信息 + FtbCultivatePosition existingPosition = ftbCultivatePositionMapper.selectById(ftbCultivatePosition.getId()); + if (existingPosition == null) { + throw new RuntimeException("未找到对应的岗位学习信息"); + } + if (existingPosition.getEnabledMark().equals(1)) { + throw new RuntimeException("岗位学习信息不存在"); + } + + + // 检查是否已存在相同的岗位ID,如果存在则更新 + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getPostId, ftbCultivatePosition.getPostId()) + .eq(FtbCultivatePosition::getEnabledMark, 0); + FtbCultivatePosition existingPositionByPostId = ftbCultivatePositionMapper.selectOne(positionWrapper); + + if (existingPositionByPostId != null && !existingPositionByPostId.getId().equals(existingPosition.getId())) { + throw new RuntimeException("该岗位已经配置了岗位学习,请不要重复操作"); + } + + //修改岗位学习主表 + existingPosition.setPostId(ftbCultivatePosition.getPostId()); + existingPosition.setIsConfiguredToGrade(ftbCultivatePosition.getIsConfiguredToGrade()); + existingPosition.setIsGrounding(1); + existingPosition.setLastModifyTime(new Date()); + existingPosition.setLastModifyUserId(UserProvider.getLoginUserId()); + ftbCultivatePositionMapper.updateById(existingPosition); + deleteCultivateRelation(existingPosition.getId()); + realDealPosition(ftbCultivatePosition, existingPosition.getId()); + return existingPosition.getId(); + } + + /** + * 删除岗位学习 + * + * @param id 岗位学习ID + */ + @Transactional(rollbackFor = Exception.class) + public void deleteCultivateRelation(String id) { + log.info("开始删除岗位学习关联数据, postLearnId={}", id); + try { + LambdaUpdateWrapper courseWrapper = Wrappers.lambdaUpdate(); + courseWrapper.eq(FtbCultivatePositionCourse::getPostLearnId, id); + courseWrapper.set(FtbCultivatePositionCourse::getEnabledMark, PositionConstants.ENABLED_MARK_DELETED); + ftbCultivatePositionCourseService.update(courseWrapper); + + LambdaUpdateWrapper examWrapper = Wrappers.lambdaUpdate(); + examWrapper.eq(FtbCultivatePositionCourseExam::getPostLearnId, id); + examWrapper.set(FtbCultivatePositionCourseExam::getEnabledMark, PositionConstants.ENABLED_MARK_DELETED); + ftbCultivatePositionCourseExamService.update(examWrapper); + + LambdaUpdateWrapper identityWrapper = Wrappers.lambdaUpdate(); + identityWrapper.eq(FtbCultivatePositionCourseIdentity::getPostLearnId, id); + identityWrapper.set(FtbCultivatePositionCourseIdentity::getEnabledMark, PositionConstants.ENABLED_MARK_DELETED); + ftbCultivatePositionCourseIdentityService.update(identityWrapper); + + LambdaUpdateWrapper practiceWrapper = Wrappers.lambdaUpdate(); + practiceWrapper.eq(FtbCultivatePositionCoursePractice::getPostLearnId, id); + practiceWrapper.set(FtbCultivatePositionCoursePractice::getEnabledMark, PositionConstants.ENABLED_MARK_DELETED); + ftbCultivatePositionCoursePracticeService.update(practiceWrapper); + + LambdaUpdateWrapper certificateWrapper = Wrappers.lambdaUpdate(); + certificateWrapper.eq(FtbCultivatePositionCourseCertificate::getPostLearnId, id); + certificateWrapper.set(FtbCultivatePositionCourseCertificate::getEnabledMark, PositionConstants.ENABLED_MARK_DELETED); + ftbCultivatePositionCourseCertificateService.update(certificateWrapper); + + LambdaQueryWrapper settingWrapper = Wrappers.lambdaQuery(); + settingWrapper.eq(FtbCultivatePositionSetting::getPostLearnId, id); + ftbCultivatePositionSettingService.remove(settingWrapper); + + LambdaUpdateWrapper logWrapper = Wrappers.lambdaUpdate(); + logWrapper.eq(FtbCultivatePositionLog::getPostLearnId, id); + logWrapper.set(FtbCultivatePositionLog::getEnabledMark, PositionConstants.ENABLED_MARK_DELETED); + ftbCultivatePositionLogService.update(logWrapper); + + //删除岗位学习ftb_cultivate_position_course_log中数据 F_EnabledMark=1 + LambdaUpdateWrapper courseLogWrapper = Wrappers.lambdaUpdate(); + courseLogWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, id); + cultivatePositionCourseLogService.remove(courseLogWrapper); + + + log.info("岗位学习关联数据删除完成, postLearnId={}", id); + } catch (Exception e) { + log.error("删除岗位学习关联数据失败, postLearnId={}", id, e); + throw new RuntimeException("删除岗位学习关联数据失败", e); + } + } + + /** + * 删除岗位学习 + * + * @param id 岗位学习id + * @return 操作结果 + */ + @Override + @Transactional + public String delete(String id) { + validateId(id, "岗位学习"); + + FtbCultivatePosition existingPosition = ftbCultivatePositionMapper.selectById(id); + if (existingPosition == null) { + throw new RuntimeException("未找到对应的岗位学习信息"); + } + if (existingPosition.getEnabledMark().equals(1)) { + throw new RuntimeException("岗位学习信息不存在"); + } + existingPosition.setEnabledMark(1); + ftbCultivatePositionMapper.updateById(existingPosition); + + deleteCultivateRelation(id); + //学习地图异常 + List promotionPostNewList = ftbCultivatePromotionMemberNewMapper.queryRelationMap(existingPosition.getPostId()); + if (CollUtil.isNotEmpty(promotionPostNewList)) { + // 按学习地图ID进行分组 + Map> promotionIdMap = + promotionPostNewList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getPromotionId)); + + for (Map.Entry> entry : promotionIdMap.entrySet()) { + String promotionId = entry.getKey(); + List promotionPosts = entry.getValue(); + + // 转换异常岗位信息 + List jsonList = new ArrayList<>(); + if (CollUtil.isNotEmpty(promotionPosts)) { + for (FtbCultivatePromotionPostNew promotionPost : promotionPosts) { + V2CultivatePostAndGradeVo json = new V2CultivatePostAndGradeVo(); + json.setPostId(promotionPost.getPostId()); + json.setPostName(promotionPost.getPostName()); + json.setGradeId(promotionPost.getGradeId()); + json.setGradeName(promotionPost.getGradeName()); + jsonList.add(json); + } + } + + // 创建异常信息VO + V2CultivatePromotionErrorVo vo = new V2CultivatePromotionErrorVo(); + vo.setPositionLearnDel(jsonList); + // 更新学习地图状态 + ftbCultivatePromotionNewMapper.update(new FtbCultivatePromotionNew(), + new UpdateWrapper().lambda() + .eq(FtbCultivatePromotionNew::getId, promotionId) + .set(FtbCultivatePromotionNew::getIsError, 1) + .set(FtbCultivatePromotionNew::getErrorMsg, JSONUtil.toJsonStr(vo)) + ); + } + } + return "success"; + } + + /** + * 获取岗位学习信息 + * + * @param id 岗位学习ID + * @return 岗位学习信息 + */ + @Override + public WebCultivatePositionVo get(String id) { + validateId(id, "岗位学习"); + + FtbCultivatePosition position = ftbCultivatePositionMapper.selectById(id); + if (position == null) { + throw new RuntimeException("未找到对应的岗位学习信息"); + } + + WebCultivatePositionVo vo = new WebCultivatePositionVo(); + vo.convert(position); + enrichWithPostInfo(vo, position.getPostId()); + enrichWithCourseListData(vo, id, position); + + return vo; + } + + private void validateId(String id, String entityName) { + if (StringUtils.isEmpty(id)) { + throw new RuntimeException(entityName + "ID不能为空"); + } + } + + private void enrichWithPostInfo(WebCultivatePositionVo vo, String postId) { + PositionVO positionVO = userApiV2Util.infoPosition(postId, null); + if (positionVO != null) { + vo.setPostName(positionVO.getFullName()); + vo.setPostEncode(positionVO.getEnCode()); + } + vo.setGradeVOList(userApiV2Util.listGrades(postId, null)); + } + + private void enrichWithCourseListData(WebCultivatePositionVo vo, String postLearnId, FtbCultivatePosition position) { + List courses = ftbCultivatePositionCourseService.listByPostLearnIdWithName(postLearnId, ""); + + if (CollUtil.isEmpty(courses)) { + vo.setFtbCultivatePositionCourseList(new ArrayList<>()); + return; + } + + List courseIds = extractCourseIds(courses); + + CourseRelatedData relatedData = batchQueryRelatedData(postLearnId, courseIds); + + List courseVos = buildCourseVoList(courses, relatedData); + List positionLearnIds = courseVos.stream() + .map(FtbCultivatePositionCourseVo::getPostLearnId) + .filter(StrUtil::isNotBlank) + .distinct().collect(Collectors.toList()); + if (CollUtil.isEmpty(positionLearnIds)) { + vo.setFtbCultivatePositionCourseList(courseVos); + return; + } + List settings = ftbCultivatePositionSettingService + .list(Wrappers.lambdaQuery() + .in(FtbCultivatePositionSetting::getPostLearnId, positionLearnIds)); + List settingVoList = new ArrayList<>(); + for (FtbCultivatePositionSetting setting : settings) { + FtbCultivatePositionSettingVo settingVo = new FtbCultivatePositionSettingVo(); + settingVo.setId(setting.getId()); + settingVo.setGradeId(setting.getGradeId()); + settingVo.setQualified(setting.getQualified()); + settingVoList.add(settingVo); + } + vo.setFtbCultivatePositionCourseList(courseVos); + vo.setFtbCultivatePositionGradeList(settingVoList); + } + + private List extractCourseIds(List courses) { + return courses.stream() + .map(FtbCultivatePositionCourseWithNameVo::getCourseId) + .distinct() + .collect(Collectors.toList()); + } + + private CourseRelatedData batchQueryRelatedData(String postLearnId, List courseIds) { + List exams = ftbCultivatePositionCourseExamService.listByPostLearnIdWithName(postLearnId); + List identities = ftbCultivatePositionCourseIdentityService.listByPostLearnIdWithName(postLearnId); + List practices = ftbCultivatePositionCoursePracticeService.listByPostLearnIdWithName(postLearnId); + List certificates = ftbCultivatePositionCourseCertificateService.listByPostLearnIdWithName(postLearnId); + + return new CourseRelatedData(exams, identities, practices, certificates); + } + + private List buildCourseVoList(List courses, CourseRelatedData relatedData) { + List courseVos = new ArrayList<>(); + for (FtbCultivatePositionCourseWithNameVo course : courses) { + FtbCultivatePositionCourseVo courseVo = convertToCourseVo(course); + courseVo.setExam(findExamForCourse(course, relatedData.exams)); + courseVo.setIdentity(findIdentityForCourse(course, relatedData.identities)); + courseVo.setPracticeList(findPracticesForCourse(course, relatedData.practices)); + courseVo.setCertificate(findCertificateForCourse(course, relatedData.certificates)); + courseVos.add(courseVo); + } + return courseVos; + } + + private FtbCultivatePositionCourseVo convertToCourseVo(FtbCultivatePositionCourseWithNameVo course) { + FtbCultivatePositionCourseVo courseVo = new FtbCultivatePositionCourseVo(); + courseVo.setId(course.getId()); + courseVo.setCourseId(course.getCourseId()); + courseVo.setCourseName(course.getCourseName()); + courseVo.setCourseIdShow(course.getShowCourseId()); + courseVo.setPostRankId(course.getPostRankId()); + courseVo.setGradeId(course.getGradeId()); + courseVo.setPostLearnId(course.getPostLearnId()); + courseVo.setCompulsory(course.getCompulsory()); + courseVo.setSortCode(course.getSortCode() != null ? course.getSortCode().longValue() : PositionConstants.DEFAULT_SORT_CODE); + return courseVo; + } + + private FtbCultivatePositionCourseExamVo findExamForCourse(FtbCultivatePositionCourseWithNameVo course, List exams) { + return exams.stream() + .filter(e -> e.getCourseId().equals(course.getCourseId()) && isGradeMatch(e.getGradeId(), course.getGradeId())) + .findFirst() + .map(this::convertToExamVo) + .orElse(null); + } + + private FtbCultivatePositionCourseIdentityVo findIdentityForCourse(FtbCultivatePositionCourseWithNameVo course, List identities) { + return identities.stream() + .filter(i -> i.getCourseId().equals(course.getCourseId()) && isGradeMatch(i.getGradeId(), course.getGradeId())) + .findFirst() + .map(this::convertToIdentityVo) + .orElse(null); + } + + private List findPracticesForCourse(FtbCultivatePositionCourseWithNameVo course, List practices) { + return practices.stream() + .filter(p -> p.getCourseId().equals(course.getCourseId()) && isGradeMatch(p.getGradeId(), course.getGradeId())) + .map(this::convertToPracticeVo) + .collect(Collectors.toList()); + } + + private FtbCultivatePositionCourseCertificateVo findCertificateForCourse(FtbCultivatePositionCourseWithNameVo course, List certificates) { + return certificates.stream() + .filter(c -> c.getCourseId().equals(course.getCourseId()) && isGradeMatch(c.getGradeId(), course.getGradeId())) + .findFirst() + .map(this::convertToCertificateVo) + .orElse(null); + } + + private boolean isGradeMatch(String relatedGradeId, String courseGradeId) { + return relatedGradeId == null || relatedGradeId.equals(courseGradeId); + } + + private FtbCultivatePositionCourseExamVo convertToExamVo(FtbCultivatePositionCourseExamWithNameVo exam) { + FtbCultivatePositionCourseExamVo examVo = new FtbCultivatePositionCourseExamVo(); + examVo.setId(exam.getId()); + examVo.setExamId(exam.getExamId()); + examVo.setExamName(exam.getExamName()); + examVo.setPostRankId(exam.getPostRankId()); + examVo.setGradeId(exam.getGradeId()); + return examVo; + } + + private FtbCultivatePositionCourseIdentityVo convertToIdentityVo(FtbCultivatePositionCourseIdentityWithNameVo identity) { + FtbCultivatePositionCourseIdentityVo identityVo = new FtbCultivatePositionCourseIdentityVo(); + identityVo.setId(identity.getId()); + identityVo.setIdentityId(identity.getIdentityId()); + identityVo.setIdentityName(identity.getIdentityName()); + identityVo.setPostRankId(identity.getPostRankId()); + identityVo.setGradeId(identity.getGradeId()); + return identityVo; + } + + private FtbCultivatePositionCoursePracticeVo convertToPracticeVo(FtbCultivatePositionCoursePracticeWithNameVo practice) { + FtbCultivatePositionCoursePracticeVo practiceVo = new FtbCultivatePositionCoursePracticeVo(); + practiceVo.setId(practice.getId()); + practiceVo.setNum(practice.getNum()); + practiceVo.setBusinessId(practice.getBusinessId()); + practiceVo.setBusinessName(practice.getBusinessName()); + practiceVo.setPostRankId(practice.getPostRankId()); + practiceVo.setGradeId(practice.getGradeId()); + return practiceVo; + } + + private FtbCultivatePositionCourseCertificateVo convertToCertificateVo(FtbCultivatePositionCourseCertificateWithNameVo certificate) { + FtbCultivatePositionCourseCertificateVo certificateVo = new FtbCultivatePositionCourseCertificateVo(); + certificateVo.setId(certificate.getId()); + certificateVo.setCertificateId(certificate.getCertificateId()); + certificateVo.setCertificateName(certificate.getCertificateName()); + certificateVo.setPostRankId(certificate.getPostRankId()); + certificateVo.setPostLearnId(certificate.getPostLearnId()); + certificateVo.setCourseId(certificate.getCourseId()); + certificateVo.setGradeId(certificate.getGradeId()); + return certificateVo; + } + + private static class CourseRelatedData { + final List exams; + final List identities; + final List practices; + final List certificates; + + CourseRelatedData(List exams, + List identities, + List practices, + List certificates) { + this.exams = exams; + this.identities = identities; + this.practices = practices; + this.certificates = certificates; + } + } + + /** + * 根据id查询岗位学习配置总览 + * + * @param id 岗位学习id + * @return 岗位学习配置总览 + */ + @Override + public WebCultivatePositionView webView(String id) { + validateId(id, "岗位学习"); + + FtbCultivatePosition position = ftbCultivatePositionMapper.selectById(id); + if (position == null) { + throw new RuntimeException("未找到对应的岗位学习信息"); + } + + WebCultivatePositionView view = new WebCultivatePositionView(); + view.setId(position.getId()); + view.setPostId(position.getPostId()); + + setPostInfo(view, position.getPostId()); + setCourseList(view, id); + setExamInfo(view, id); + setIdentityInfo(view, id); + setPromotionInfo(view, position.getPostId()); + + return view; + } + + /** + * 设置岗位信息 + */ + private void setPostInfo(WebCultivatePositionView view, String postId) { + PositionVO positionVO = userApiV2Util.infoPosition(postId, null); + if (positionVO != null) { + view.setPostName(positionVO.getFullName()); + view.setPostCode(positionVO.getEnCode()); + } + } + + /** + * 设置课程列表 + */ + private void setCourseList(WebCultivatePositionView view, String id) { + List courses = ftbCultivatePositionCourseService.listByPostLearnId(id, ""); + if (CollUtil.isEmpty(courses)) { + view.setCourseList(new ArrayList<>()); + return; + } + + List courseIds = courses.stream() + .map(FtbCultivatePositionCourseWithNameVo::getCourseId) + .distinct() + .collect(Collectors.toList()); + + // 批量查询课程基础信息 + Map courseBasicInfoMap = v2CultivateBatchQueryService.batchQueryCoursesByIds(courseIds, null); + + // 构建课程列表VO + List courseSimpleVos = courses.stream() + .map(course -> buildCourseSimpleVo(course, courseBasicInfoMap)) + .collect(Collectors.toList()); + view.setCourseList(courseSimpleVos); + } + + /** + * 构建课程简单VO + */ + private CourseSimpleVo buildCourseSimpleVo(FtbCultivatePositionCourseWithNameVo course, Map courseBasicInfoMap) { + CourseSimpleVo courseSimpleVo = new CourseSimpleVo(); + courseSimpleVo.setId(course.getCourseId()); + FtbCultivateCourse courseBasicInfo = courseBasicInfoMap.get(course.getCourseId()); + if (courseBasicInfo != null) { + courseSimpleVo.setName(courseBasicInfo.getName()); + courseSimpleVo.setIsGroundIng(courseBasicInfo.getIsGroundIng()); + courseSimpleVo.setCourseId(courseBasicInfo.getCourseId()); + } + return courseSimpleVo; + } + + /** + * 设置考试信息 + */ + private void setExamInfo(WebCultivatePositionView view, String id) { + List exams = ftbCultivatePositionCourseExamService.listByPostLearnId(id); + if (CollUtil.isEmpty(exams)) { + view.setExamVoList(new ArrayList<>()); + return; + } + + // 获取考试ID列表并批量查询考试基础信息 + List examIds = exams.stream() + .map(FtbCultivatePositionCourseExam::getExamId) + .distinct() + .collect(Collectors.toList()); + + + Map examBasicInfoMap = v2CultivateBatchQueryService.batchQueryExamsByIds(examIds, null); + + // 构建考试VO列表 + List examVoList = exams.stream() + .map(exam -> { + FtbCultivatePositionCourseExamVo examVo = new FtbCultivatePositionCourseExamVo(); + examVo.convert(exam); + FtbCultivateExam examBasicInfo = examBasicInfoMap.get(exam.getExamId()); + if (examBasicInfo != null) { + examVo.setExamName(examBasicInfo.getExamName()); + } + return examVo; + }) + .collect(Collectors.toList()); + + view.setExamVoList(examVoList); + } + + /** + * 设置鉴定信息 + */ + private void setIdentityInfo(WebCultivatePositionView view, String id) { + List identities = ftbCultivatePositionCourseIdentityService.listByPostLearnId(id); + if (CollUtil.isEmpty(identities)) { + view.setIdentityVoList(new ArrayList<>()); + return; + } + + // 获取鉴定ID列表并批量查询鉴定基础信息 + List identityIds = identities.stream() + .map(FtbCultivatePositionCourseIdentity::getIdentityId) + .distinct() + .collect(Collectors.toList()); + + Map identityBasicInfoMap = v2CultivateBatchQueryService.batchQueryIdentificationsByIds(identityIds, null); + + // 构建鉴定VO列表 + List identityVoList = identities.stream() + .map(identity -> { + FtbCultivatePositionCourseIdentityVo identityVo = new FtbCultivatePositionCourseIdentityVo(); + identityVo.convert(identity); + CultivateIdentifyTable identityBasicInfo = identityBasicInfoMap.get(identity.getIdentityId()); + if (identityBasicInfo != null) { + identityVo.setIdentityName(identityBasicInfo.getName()); + } + return identityVo; + }) + .collect(Collectors.toList()); + + view.setIdentityVoList(identityVoList); + } + + /** + * 设置学习地图信息 + */ + private void setPromotionInfo(WebCultivatePositionView view, String postId) { + List positionIds = List.of(postId); + List promotionList = ftbCultivatePromotionNewMapper.countByPositionIds(positionIds); + + if (CollUtil.isEmpty(promotionList)) { + return; + } + List voList = new ArrayList<>(); + for (PromotionAndPostVo promotionAndPostVo : promotionList) { + voList.add(promotionAndPostVo.convert()); + } + view.setPromotionVOList(voList); + } + + /** + * app岗位学习-本岗位学习课程列表 + * + * @param req 岗位学习课程列表请求参数 + * @return 岗位学习课程列表 + */ + @Override + public PositionLearningCourseVo positionCourseLists(V2CultivatePositionCourseForAppReq req) { + // 1. 获取当前登录用户信息 + UserBoundVO userBoundVO = getUserBoundVO(req); + // 2. 查询用户岗位ID + String postId = userBoundVO.getPositionId(); + //用户的职级id + String gradeId = userBoundVO.getGradeId(); + // 3. 查询岗位学习是否存在 + FtbCultivatePosition cultivatePosition = getFtbCultivatePosition(postId); + if (cultivatePosition == null) { + throw new RuntimeException("该岗位尚未配置岗位学习"); + } + + V2InnerPositionDto v2InnerPositionDto = new V2InnerPositionDto(); + v2InnerPositionDto.setKeyWord(req.getKeyWord()); + v2InnerPositionDto.setUserId(userBoundVO.getId()); + PositionLearningCourseVo positionLearningCourseVo = null; + if (cultivatePosition.getIsConfiguredToGrade().equals(0))//未分配到职级 + { + //根据岗位id查询课程列表 + positionLearningCourseVo = realQueryCourseListForPositionId(cultivatePosition, v2InnerPositionDto); + } else { + if (StringUtils.isNotEmpty(gradeId)) { + //根据岗位id和职级id查询课程列表 + positionLearningCourseVo = realQueryCourseListForPositionIdAndGradeId(cultivatePosition, v2InnerPositionDto, gradeId); + } + } + // 如果岗位配置到职级但用户没有职级,则返回空结果 + if (positionLearningCourseVo == null) { + return PositionLearningCourseVo.empty(); + } + List courseLists = positionLearningCourseVo.getCourseLists(); + if (CollUtil.isNotEmpty(courseLists)) { + Map studyNumMap = v2CultivateBatchQueryService.batchQueryCourseLearningCount(courseLists.stream().map(AppCourseSimpleVo::getCourseId).collect(Collectors.toList())); + for (AppCourseSimpleVo vo : courseLists) { + vo.setLearnTotalNumber(studyNumMap.getOrDefault(vo.getCourseId(), 0L)); + } + } + return positionLearningCourseVo; + } + + @NotNull + private UserBoundVO getUserBoundVO(V2CultivatePositionCourseForAppReq req) { + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userBoundVO == null) { + throw new RuntimeException("用户信息不存在"); + } + // 2. 查询用户岗位ID + String postId = userBoundVO.getPositionId(); + if (StringUtils.isEmpty(postId)) { + throw new RuntimeException("用户未分配岗位"); + } + return userBoundVO; + } + + /** + * 通用方法:查询课程列表核心逻辑 + */ + private PositionLearningCourseVo queryCourseListCore(FtbCultivatePosition cultivatePosition, + V2InnerPositionDto req, + List positionCourses) { + if (CollUtil.isEmpty(positionCourses)) { + return PositionLearningCourseVo.empty(); + } + + // 获取课程ID列表,用于批量查询课程基础信息 + List courseIds = positionCourses.stream() + .map(FtbCultivatePositionCourseWithNameVo::getCourseId) + .distinct() + .collect(Collectors.toList()); + + + // 批量查询课程基础信息 + List courseList = ftbCultivateCourseMapper.selectList(Wrappers.lambdaQuery() + .in(FtbCultivateCourse::getId, courseIds) + .like(StringUtils.isNotEmpty(req.getKeyWord()), FtbCultivateCourse::getName, req.getKeyWord()) + .eq(FtbCultivateCourse::getIsGroundIng, 1) + .eq(FtbCultivateCourse::getEnableMark, 0)); + if (CollUtil.isEmpty(courseList)) { + return PositionLearningCourseVo.empty(); + } + Map studyNumMap = v2CultivateBatchQueryService.batchQueryCourseLearningCount(courseIds); + PositionLearningCourseVo result = new PositionLearningCourseVo(); + // 批量查询用户的学习记录 + List learningRecords = ftbCultivatePositionCourceLearningService.list( + Wrappers.lambdaQuery() + .select(FtbCultivatePositionCourceLearning::getCourceId, FtbCultivatePositionCourceLearning::getState) + .eq(FtbCultivatePositionCourceLearning::getUserId, req.getUserId()) + .in(FtbCultivatePositionCourceLearning::getCourceId, courseIds) + .eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0) + ); + + // 构建学习记录映射 + Integer completedCount = 0; + Map learningStateMap = new HashMap<>(); + if (CollUtil.isNotEmpty(learningRecords)) { + for (FtbCultivatePositionCourceLearning learningRecord : learningRecords) { + completedCount += learningRecord.getState() == 1 ? 1 : 0; + learningStateMap.put(learningRecord.getCourceId(), learningRecord.getState()); + } + } + // 7. 构建返回结果 + result.setAlreadyLearnedCourse(completedCount); + result.setTotalCourse(courseList.size()); + + List appCourseSimpleVoList = new ArrayList<>(); + for (FtbCultivatePositionCourseWithNameVo positionCourse : positionCourses) { + + AppCourseSimpleVo courseVo = new AppCourseSimpleVo(); + courseVo.setBusinessSourceId(cultivatePosition.getId()); + courseVo.setBusinessSource(PositionBusinessSourceEnum.POST_LEARNING); + courseVo.setCourseId(positionCourse.getCourseId()); + courseVo.setCourseName(positionCourse.getCourseName()); + courseVo.setCoverUrl(positionCourse.getCoverUrl()); + courseVo.setTypeId(positionCourse.getTypeId()); + courseVo.setPostId(cultivatePosition.getPostId()); // 使用参数传递的岗位ID + + // 设置学习状态 + Integer state = learningStateMap.get(positionCourse.getCourseId()); + if (state == null) { + courseVo.setLearnState(0); // 未学习 + } else { + courseVo.setLearnState(state); // 0未学习,1已学习,2学习中 + } + + // 设置学习总人数(暂时设为0,实际可从统计数据中获取) + courseVo.setLearnTotalNumber(studyNumMap.get(positionCourse.getId())); + appCourseSimpleVoList.add(courseVo); + } + + result.setCourseLists(appCourseSimpleVoList); + return result; + } + + + private PositionLearningCourseVo realQueryCourseListForPositionIdAndGradeId(FtbCultivatePosition cultivatePosition, V2InnerPositionDto req, String gradeId) { + // 4. 根据岗位学习ID查询岗位学习课程列表 + List positionCourses = ftbCultivatePositionCourseService.listByPostLearnIdAndGradeId(cultivatePosition.getId(), gradeId, ""); + return queryCourseListCore(cultivatePosition, req, positionCourses); + } + + private PositionLearningCourseVo realQueryCourseListForPositionId(FtbCultivatePosition cultivatePosition, V2InnerPositionDto req) { + // 根据岗位学习ID查询岗位学习课程列表 + List positionCourses = ftbCultivatePositionCourseService.listByPostLearnId(cultivatePosition.getId(), ""); + return queryCourseListCore(cultivatePosition, req, positionCourses); + } + + /** + * app岗位学习-他岗位课程列表 + * + * @param req 岗位学习课程列表请求参数 + * @return 岗位学习课程列表 + */ + @Override + public List otherPositionList(V2OtherCultivatePositionCourseForAppReq req) { + // 1. 获取当前登录用户信息 + UserBoundVO userBoundVO = getUserBoundVO(req); + + // 2. 查询用户岗位ID + String postId = userBoundVO.getPositionId(); + //用户的职级id + String gradeId = userBoundVO.getGradeId(); + List cultivatePosition = ftbCultivatePositionCourseService.queryAllPositionLearn(req.getKeyWord()); + if (CollUtil.isEmpty(cultivatePosition)) { + return new ArrayList<>(); + } + cultivatePosition = filterOtherPositionList(cultivatePosition, postId, gradeId); + if (CollUtil.isEmpty(cultivatePosition)) { + return new ArrayList<>(); + } + //获取岗位ids 和 gradeIds + List postIds = new ArrayList<>(); + List gradeIds = new ArrayList<>(); + for (CultivatePositionSimpleVo vo : cultivatePosition) { + postIds.add(vo.getPostId()); + if (StringUtils.isNotEmpty(vo.getGradeId())) { + gradeIds.add(vo.getGradeId()); + } + } + //批量查询岗位名称 + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + Map stringGradeVOMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + for (CultivatePositionSimpleVo vo : cultivatePosition) { + PositionVO positionVO = stringPositionVOMap.get(vo.getPostId()); + if (positionVO != null) { + vo.setPostName(positionVO.getFullName()); + } + if (StringUtils.isNotEmpty(vo.getGradeId())) { + GradeVO gradeVO = stringGradeVOMap.get(vo.getGradeId()); + if (gradeVO != null) { + vo.setGradeName(gradeVO.getFullName()); + } + } + } + return cultivatePosition; + } + + /** + * 过滤其他岗位列表 + * + * @param lists 岗位列表 + * @param postId 当前用户岗位ID + * @param gradeId 当前用户职级ID + * @param 继承自 CultivatePositionSimpleVo 的类型 + * @return 过滤后的岗位列表 + */ + private List filterOtherPositionList(List lists, String postId, String gradeId) { + List returnList = new ArrayList<>(); + for (T vo : lists) { + if (StringUtils.isEmpty(vo.getGradeId())) { + if (!vo.getPostId().equals(postId)) { + returnList.add(vo); + } + } else { + if (!vo.getPostId().equals(postId) || !vo.getGradeId().equals(gradeId)) { + returnList.add(vo); + } + } + } + return returnList; + } + + + /** + * app岗位学习-他岗位课程(总课程/已经完成课程) + * + * @param req 岗位学习课程列表请求参数 + * @return 岗位学习课程列表 + */ + @Override + public OtherPositionCourseCountVo otherPositionCourseCount(V2OtherCultivatePositionCourseForAppReq req) { + + UserBoundVO userBoundVO = getUserBoundVO(req); + + // 2. 查询用户岗位ID + String postId = userBoundVO.getPositionId(); + //用户的职级id + String gradeId = userBoundVO.getGradeId(); + OtherPositionCourseCountVo returnVo = new OtherPositionCourseCountVo(); + returnVo.init(); + List list = ftbCultivatePositionCourseService.queryAllPositionLearnCourseLists(req); + if (CollUtil.isEmpty(list)) { + return returnVo; + } + list = filterOtherPositionList(list, postId, gradeId); + Set courseIds = new HashSet<>(); + for (AppCourseSimpleVo vo : list) { + courseIds.add(vo.getCourseId()); + } + returnVo.setTotalCourse(courseIds.size()); + + Map stringFtbCultivatePositionCourceLearningMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userBoundVO.getId(), new ArrayList<>(courseIds)); + Long alreadyLearnedCourse = 0L; + for (String courseId : courseIds) { + FtbCultivatePositionCourceLearning ftbCultivatePositionCourceLearning = stringFtbCultivatePositionCourceLearningMap.get(courseId); + if (ftbCultivatePositionCourceLearning != null && ftbCultivatePositionCourceLearning.getState() == 1) { + alreadyLearnedCourse++; + } + } + returnVo.setAlreadyLearnedCourse(alreadyLearnedCourse); + return returnVo; + } + + @Override + public AppCultivatePositionDetailVo positionStudyDetail(V2CultivatePositionDetailForApp req) { + // 初始化返回对象并验证请求参数 + AppCultivatePositionDetailVo returnVo = initializeAndValidateRequest(req); + + // 查询岗位学习配置信息 + FtbCultivatePosition existingPosition = queryPositionConfiguration(req); + populatePositionInfo(returnVo, existingPosition); + populatePositionState(returnVo, existingPosition, req); + // 查询岗位学习设置 + populatePositionSettings(returnVo, existingPosition, req); + + // 查询岗位和职级信息 + populatePostAndGradeInfo(returnVo, req); + existingPosition.setQualified(returnVo.getQualified()); + + // 查询课程及关联的学习记录、考试、鉴定、练习、证书等信息 + List courseVoList = buildCourseListWithRelatedData(existingPosition, req); + returnVo.setFtbCultivatePositionCourseList(courseVoList); + for (AppCultivatePositionCourseVo appCultivatePositionCourseVo : courseVoList) { + if (CollUtil.isNotEmpty(appCultivatePositionCourseVo.getExam())) { + returnVo.getExam().addAll(appCultivatePositionCourseVo.getExam()); + } + if (CollUtil.isNotEmpty(appCultivatePositionCourseVo.getIdentity())) { + returnVo.getIdentity().addAll(appCultivatePositionCourseVo.getIdentity()); + } + if (CollUtil.isNotEmpty(appCultivatePositionCourseVo.getPracticeList())) { + returnVo.getPracticeList().addAll(appCultivatePositionCourseVo.getPracticeList()); + } + if (CollUtil.isNotEmpty(appCultivatePositionCourseVo.getCertificate())) { + returnVo.getCertificate().addAll(appCultivatePositionCourseVo.getCertificate()); + } + } + String tenantId = UserProvider.getUser().getTenantId(); + CompletableFuture.runAsync(() -> { + userApiV2Util.checkOutTenant(tenantId); + try { + Integer hasExam = CollUtil.isEmpty(returnVo.getExam()) ? 1 : 2; + Integer hasIdentify = CollUtil.isEmpty(returnVo.getIdentity()) ? 1 : 2; + initPositionCourseLogs(existingPosition.getId(), existingPosition.getPostId(), req.getGradeId(), req.getUserId(), returnVo.getFtbCultivatePositionCourseList(), hasExam, hasIdentify); + } catch (Exception e) { + log.error("考试定时任务执行异常: {}", e.getMessage()); + } + }, cultivateThreadPool); + + return returnVo; + } + + private void populatePositionState(AppCultivatePositionDetailVo returnVo, FtbCultivatePosition existingPosition, V2CultivatePositionDetailForApp req) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper + .eq(FtbCultivatePositionLog::getUserId, req.getUserId()) + .eq(FtbCultivatePositionLog::getPostLearnId, existingPosition.getId()) + .eq(existingPosition.getIsConfiguredToGrade().equals(1), FtbCultivatePositionLog::getGradeId, req.getGradeId()) + .eq(FtbCultivatePositionLog::getEnabledMark, 0); + List list = ftbCultivatePositionLogService.list(queryWrapper); + if (CollUtil.isEmpty(list)) { + returnVo.setPositionState(0); + return; + } + returnVo.setPositionState(list.get(0).getState()); + } + + private void initPositionCourseLogs(String positionLearnId, String postId, String gradeId, String userId, List courseVoList, Integer hasExam, Integer hasIdentify) { + if (CollUtil.isEmpty(courseVoList)) { + return; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0); + List logList = cultivatePositionCourseLogService.list(queryWrapper); + if (CollUtil.isEmpty(logList)) { + List addList = new ArrayList<>(); + for (AppCultivatePositionCourseVo appCultivatePositionCourseVo : courseVoList) { + CultivatePositionCourseLogEntity log = new CultivatePositionCourseLogEntity(); + log.setPostLearnId(positionLearnId); + log.setPostId(postId); + log.setGradeId(gradeId); + log.setCourseId(appCultivatePositionCourseVo.getCourseId()); + log.setUserId(userId); + log.setState(1); + log.setHasExam(hasExam); + log.setHasIdentity(hasIdentify); + addList.add(log); + } + cultivatePositionCourseLogService.saveBatch(addList); + } + } + + /** + * 初始化请求参数并进行验证 + */ + private AppCultivatePositionDetailVo initializeAndValidateRequest(V2CultivatePositionDetailForApp req) { + AppCultivatePositionDetailVo returnVo = new AppCultivatePositionDetailVo(); + returnVo.setExam(new ArrayList<>()); + returnVo.setIdentity(new ArrayList<>()); + returnVo.setPracticeList(new ArrayList<>()); + returnVo.setCertificate(new ArrayList<>()); + String userId = UserProvider.getUser().getUserId(); + if (StringUtils.isEmpty(req.getUserId())) { + req.setUserId(userId); + } + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (StringUtils.isEmpty(req.getPostId())) { + req.setPostId(userBoundVO.getPositionId()); + req.setGradeId(userBoundVO.getGradeId()); + } + return returnVo; + } + + /** + * 查询岗位学习配置信息 + */ + private FtbCultivatePosition queryPositionConfiguration(V2CultivatePositionDetailForApp req) { + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getPostId, req.getPostId()); + positionWrapper.eq(FtbCultivatePosition::getIsGrounding, 1); + positionWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + + FtbCultivatePosition existingPosition = ftbCultivatePositionMapper.selectOne(positionWrapper); + + if (existingPosition == null) { + throw new RuntimeException("该岗位未配置岗位学习"); + } + return existingPosition; + } + + /** + * 填充岗位基本信息到返回对象 + */ + private void populatePositionInfo(AppCultivatePositionDetailVo returnVo, FtbCultivatePosition existingPosition) { + returnVo.setId(existingPosition.getId()); + returnVo.setPostId(existingPosition.getPostId()); + returnVo.setIsConfiguredToGrade(existingPosition.getIsConfiguredToGrade()); + } + + /** + * 填充岗位学习设置信息 + */ + private void populatePositionSettings(AppCultivatePositionDetailVo returnVo, FtbCultivatePosition existingPosition, V2CultivatePositionDetailForApp req) { + LambdaQueryWrapper positionSettingWrapper = Wrappers.lambdaQuery(); + positionSettingWrapper.eq(FtbCultivatePositionSetting::getPostLearnId, existingPosition.getId()); + if (existingPosition.getIsConfiguredToGrade().equals(1)) { + if (StringUtils.isEmpty(req.getGradeId())) { + throw new RuntimeException("请传入用户的职级"); + } + positionSettingWrapper.eq(FtbCultivatePositionSetting::getGradeId, req.getGradeId()); + } + List ftbCultivatePositionSettings = ftbCultivatePositionSettingService.list(positionSettingWrapper); + if (CollUtil.isNotEmpty(ftbCultivatePositionSettings)) { + returnVo.setQualified(ftbCultivatePositionSettings.get(0).getQualified()); + } else { + returnVo.setQualified(0); + } + } + + /** + * 填充岗位和职级信息 + */ + private void populatePostAndGradeInfo(AppCultivatePositionDetailVo returnVo, V2CultivatePositionDetailForApp req) { + PositionVO positionVO = userApiV2Util.infoPosition(req.getPostId(), null); + if (positionVO != null) { + returnVo.setPostName(positionVO.getFullName()); + returnVo.setPostEncode(positionVO.getEnCode()); + } + if (StringUtils.isNotEmpty(req.getGradeId())) { + GradeVO gradeVO = userApiV2Util.infoGrade(req.getGradeId(), null); + if (gradeVO != null) { + returnVo.setGradeName(gradeVO.getName()); + returnVo.setGradeId(gradeVO.getId()); + } + } + } + + /** + * 构建课程列表及其相关数据(考试、鉴定、练习、证书等) + */ + private List buildCourseListWithRelatedData(FtbCultivatePosition existingPosition, V2CultivatePositionDetailForApp req) { + // 1. 查询岗位学习课程列表 + List positionCourses = queryPositionCourses(existingPosition, req); + + if (CollUtil.isEmpty(positionCourses)) { + return new ArrayList<>(); + } + + // 获取课程ID列表,用于批量查询 + List courseIds = positionCourses.stream() + .map(FtbCultivatePositionCourseWithNameVo::getCourseId) + .distinct() + .collect(Collectors.toList()); + + + // 批量查询用户的学习记录 + Map learningMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(req.getUserId(), courseIds); + Map> learningChapterMap = v2CultivateBatchQueryService.batchQueryUserCourseChapterLearnStatus(req.getUserId(), courseIds); + + // 查询课程关联的考试、鉴定、练习、证书信息 + RelatedDataBundle relatedData = queryRelatedDataBundle(existingPosition.getId(), courseIds, req.getUserId()); + + // 构建课程列表VO + return positionCourses.stream() + .map(positionCourse -> buildCourseVo(existingPosition, positionCourse, learningMap, learningChapterMap, relatedData, req)) + .collect(Collectors.toList()); + } + + /** + * 查询岗位学习课程列表 + */ + private List queryPositionCourses(FtbCultivatePosition existingPosition, V2CultivatePositionDetailForApp req) { + return ftbCultivatePositionCourseService.listPositionCourseList(existingPosition, req); + } + + /** + * 查询课程关联的所有相关数据 + */ + private RelatedDataBundle queryRelatedDataBundle(String postLearnId, List courseIds, String userId) { + // 查询课程关联的考试、鉴定、练习、证书信息 - 使用JOIN一次性获取基础信息 + List positionCourseExams = ftbCultivatePositionCourseExamService.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + List positionCourseIdentities = ftbCultivatePositionCourseIdentityService.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + List positionCoursePractices = ftbCultivatePositionCoursePracticeService.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + List positionCourseCertificates = ftbCultivatePositionCourseCertificateService.listByPostLearnIdAndCourseIdsWithName(postLearnId, courseIds); + + // 收集考试、鉴定、练习、证书ID用于批量查询用户状态 + List examIds = positionCourseExams.stream() + .map(FtbCultivatePositionCourseExamWithNameVo::getExamId) + .distinct() + .collect(Collectors.toList()); + + List identityIds = positionCourseIdentities.stream() + .map(FtbCultivatePositionCourseIdentityWithNameVo::getIdentityId) + .distinct() + .collect(Collectors.toList()); + + List practiceIds = positionCoursePractices.stream() + .map(FtbCultivatePositionCoursePracticeWithNameVo::getBusinessId) + .distinct() + .collect(Collectors.toList()); + + List certificateIds = positionCourseCertificates.stream() + .map(FtbCultivatePositionCourseCertificateWithNameVo::getCertificateId) + .distinct() + .collect(Collectors.toList()); + + // 批量查询用户的考试、鉴定、证书、技能完成情况 + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, identityIds, 2, postLearnId); + Map userCertificateStatusMap = v2CultivateBatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillStatusMap = v2CultivateBatchQueryService.batchQueryUserSkillStatus(userId, practiceIds); + + return new RelatedDataBundle( + positionCourseExams, positionCourseIdentities, positionCoursePractices, positionCourseCertificates, + userExamStatusMap, userIdentificationStatusMap, userCertificateStatusMap, userSkillStatusMap, userId + ); + } + + /** + * 构建单个课程VO对象 + */ + private AppCultivatePositionCourseVo buildCourseVo( + FtbCultivatePosition existingPosition, + FtbCultivatePositionCourseWithNameVo positionCourse, + Map learningMap, + Map> learningChapterMap, + RelatedDataBundle relatedData, + V2CultivatePositionDetailForApp req) { + AppCultivatePositionCourseVo courseVo = new AppCultivatePositionCourseVo(); + courseVo.convert(positionCourse); + + // 设置课程基础信息 + + courseVo.setCourseName(positionCourse.getCourseName()); + courseVo.setCourseIdShow(positionCourse.getShowCourseId()); + + + // 设置学习状态和进度 + FtbCultivatePositionCourceLearning learningRecord = learningMap.get(positionCourse.getCourseId()); + if (learningRecord != null) { + courseVo.setLearnState(learningRecord.getState()); + // 对于学习进度,我们暂时设为固定值,因为课程实体中没有总时长字段 + if (learningRecord.getState().equals(1)) { + courseVo.setLearnProgress("100%"); // 在实际应用中,这里可能需要从其他地方获取进度信息 + } else if (learningRecord.getState().equals(0)) { + courseVo.setLearnProgress("0%"); // 在实际应用中,这里可能 + } else { + //根据已经学习的章节 和总章节计算 + List chapterLearningList = learningChapterMap.get(positionCourse.getCourseId()); + if (CollUtil.isEmpty(chapterLearningList)) { + courseVo.setLearnProgress("0%"); + } else { + int total = chapterLearningList.size(); + int completed = (int) chapterLearningList.stream().filter(chapterLearning -> chapterLearning.getState().equals(1)).count(); + courseVo.setLearnProgress(String.format("%d%%", completed * 100 / total)); + } + + } + } else { + courseVo.setLearnState(0); // 未学习 + courseVo.setLearnProgress("0%"); + } + + + // 设置关联的考试信息 + List examVoList = buildExamVoList(positionCourse, relatedData, courseVo); + courseVo.setExam(examVoList); + + // 设置关联的练习信息 + List practiceVoList = buildPracticeVoList(positionCourse, relatedData, req, courseVo); + courseVo.setPracticeList(practiceVoList); + Integer isCanIdentification = runCanIdentification(existingPosition, courseVo, examVoList, practiceVoList); + + //todo 查询鉴定id + // 设置关联的鉴定信息 + List identityVoList = buildIdentityVoList(positionCourse, relatedData, isCanIdentification); + courseVo.setIdentity(identityVoList); + + + // 设置关联的证书信息 + List certificateVoList = buildCertificateVoList(positionCourse, relatedData); + courseVo.setCertificate(certificateVoList); + + return courseVo; + } + + + private Integer runCanIdentification(FtbCultivatePosition existingPosition, + AppCultivatePositionCourseVo courseVo, + List examVoList, + List practiceVoList) { + if (!courseVo.getLearnState().equals(1)) { + return 0; + } + if (existingPosition.getQualified() == 1) { + + if (CollUtil.isNotEmpty(practiceVoList)) { + for (AppCultivatePositionCoursePracticeVo appCultivatePositionCoursePracticeVo : practiceVoList) { + if (appCultivatePositionCoursePracticeVo.getStatus().equals(0)) { + return 0; + } + } + } + if (CollUtil.isNotEmpty(examVoList)) { + if (!(examVoList.get(0).getExamStatus().equals(3) + || examVoList.get(0).getExamStatus().equals(5))) { + return 0; + } + } + } + return 1; + } + + /** + * 构建考试VO列表 + */ + private List buildExamVoList(FtbCultivatePositionCourseWithNameVo positionCourse, RelatedDataBundle relatedData, AppCultivatePositionCourseVo courseVo) { + List courseExams = relatedData.positionCourseExams.stream() + .filter(exam -> exam.getCourseId().equals(positionCourse.getCourseId())) + .filter(exam -> StringUtils.isEmpty(positionCourse.getGradeId()) || (positionCourse.getGradeId().equals(exam.getGradeId()))) + .collect(Collectors.toList()); + + return courseExams.stream() + .map(exam -> { + AppCultivatePositionCourseExamVo examVo = new AppCultivatePositionCourseExamVo(); + examVo.setExamId(exam.getExamId()); + // 直接从VO中获取考试名称,无需额外查询 + examVo.setExamName(exam.getExamName()); + FtbCultivateExamUser ftbCultivateExamUser = relatedData.userExamStatusMap.get(exam.getExamId()); + if (ftbCultivateExamUser != null) { + examVo.setExamStatus(ftbCultivateExamUser.getStatus()); + examVo.setUserExamId(ftbCultivateExamUser.getId()); + examVo.setUserExamScore(ftbCultivateExamUser.getScore()); + examVo.setExamTotalScore(ftbCultivateExamUser.getTotalScore()); + examVo.setExamCompleteDate(ftbCultivateExamUser.getFinishTime()); + examVo.setUserExamDuration(ftbCultivateExamUser.getDuration()); + } else { + examVo.setExamStatus(0); + if (courseVo.getLearnState().equals(1)) { + String userExamId = v2CultivatePostStudyService.checkExamStatusReturnUserExamId(relatedData.userId, exam.getPostLearnId(), List.of(exam.getExamId()), relatedData.userExamStatusMap, courseVo.getCourseId(), positionCourse.getGradeId(), UserProvider.getUser().getTenantId()); + examVo.setUserExamId(userExamId); + } else { + examVo.setUserExamId(null); + } + } + examVo.setLearnState(courseVo.getLearnState()); + examVo.setCourseId(courseVo.getCourseId()); + return examVo; + }) + .filter(Objects::nonNull) // 过滤掉 null 值 + .collect(Collectors.toList()); + } + + /** + * 构建鉴定VO列表 + */ + private List buildIdentityVoList(FtbCultivatePositionCourseWithNameVo positionCourse, RelatedDataBundle relatedData, Integer isCanIdentification) { + List courseIdentities = relatedData.positionCourseIdentities.stream() + .filter(identity -> identity.getCourseId().equals(positionCourse.getCourseId())) + .filter(identity -> positionCourse.getGradeId() == null || (positionCourse.getGradeId().equals(identity.getGradeId()))) + .collect(Collectors.toList()); + + return courseIdentities.stream() + .map(identity -> { + AppCultivatePositionCourseIdentityVo identityVo = new AppCultivatePositionCourseIdentityVo(); + identityVo.setIdentityId(identity.getIdentityId()); + // 直接从VO中获取鉴定名称,无需额外查询 + identityVo.setIdentityName(identity.getIdentityName()); + identityVo.setBusinessSource(PositionBusinessSourceEnum.POST_LEARNING); + identityVo.setBusinessSourceId(identity.getPostLearnId()); + identityVo.setIsCanIdentification(isCanIdentification); + CultivateIdentifyApply cultivateIdentifyApply = relatedData.userIdentificationStatusMap.get(identity.getIdentityId()); + if (cultivateIdentifyApply != null) { + identityVo.setIdentificationResult(cultivateIdentifyApply.getResult()); + identityVo.setIdentificationStatus(cultivateIdentifyApply.getStatus()); + identityVo.setUserIdentityId(cultivateIdentifyApply.getId()); + } else { + identityVo.setIdentificationResult(0); + identityVo.setIdentificationStatus(0); + identityVo.setUserIdentityId(""); + } + return identityVo; + }) + .collect(Collectors.toList()); + } + + /** + * 构建练习VO列表 + */ + private List buildPracticeVoList(FtbCultivatePositionCourseWithNameVo positionCourse, + RelatedDataBundle relatedData, + V2CultivatePositionDetailForApp req, + AppCultivatePositionCourseVo courseVo + ) { + List coursePractices = relatedData.positionCoursePractices.stream() + .filter(practice -> practice.getCourseId().equals(positionCourse.getCourseId())) + .filter(practice -> positionCourse.getGradeId() == null || (positionCourse.getGradeId().equals(practice.getGradeId()))) + .collect(Collectors.toList()); + + return coursePractices.stream() + .map(practice -> { + AppCultivatePositionCoursePracticeVo practiceVo = new AppCultivatePositionCoursePracticeVo(); + practiceVo.setBusinessId(practice.getBusinessId()); + practiceVo.setNum(practice.getNum()); + // 直接从VO中获取技能点名称,无需额外查询 + practiceVo.setBusinessName(practice.getBusinessName()); + // 设置练习完成情况 + List completeNum = relatedData.userSkillStatusMap.get(practiceVo.getBusinessId()); + practiceVo.setCompleteNum(completeNum != null ? completeNum.size() : 0); + if (practiceVo.getCompleteNum() >= practiceVo.getNum()) { + practiceVo.setStatus(1); + practiceVo.setUserPracticeIds(completeNum); + } else { + practiceVo.setStatus(0); + } + + practiceVo.setLearnState(courseVo.getLearnState()); + practiceVo.setCourseId(courseVo.getCourseId()); + return practiceVo; + }) + .collect(Collectors.toList()); + } + + /** + * 构建证书VO列表 + */ + private List buildCertificateVoList(FtbCultivatePositionCourseWithNameVo positionCourse, RelatedDataBundle relatedData) { + List courseCertificates = relatedData.positionCourseCertificates.stream() + .filter(certificate -> certificate.getCourseId().equals(positionCourse.getCourseId())) + .filter(certificate -> positionCourse.getGradeId() == null || (positionCourse.getGradeId().equals(certificate.getGradeId()))) + .collect(Collectors.toList()); + + + return courseCertificates.stream() + .map(certificate -> { + AppCultivatePositionCourseCertificateVo certificateVo = new AppCultivatePositionCourseCertificateVo(); + certificateVo.setCertificateId(certificate.getCertificateId()); + // 直接从VO中获取证书名称,无需额外查询 + certificateVo.setCertificateName(certificate.getCertificateName()); + Boolean isGetCertificate = v2CultivatePostStudyService.checkIsTriggerPositionCertificate(relatedData.userId, certificate.getCertificateId(), certificate.getPostLearnId(), certificate.getPostRankId(), StringUtils.isEmpty(certificate.getGradeId()) ? "" : certificate.getGradeId(), positionCourse.getCourseId()); + certificateVo.setIsGet(isGetCertificate); + FtbCertificateUserEntity ftbCertificateUserEntity = relatedData.userCertificateStatusMap.get(certificate.getCertificateId()); + if (ftbCertificateUserEntity != null) { + certificateVo.setUserCertificateId(ftbCertificateUserEntity.getId()); + certificateVo.setUserCertificateStatus(ftbCertificateUserEntity.getStatus()); + } + return certificateVo; + }) + .collect(Collectors.toList()); + } + + /** + * 内部类:封装课程关联的所有相关数据 + */ + private static class RelatedDataBundle { + List positionCourseExams; + List positionCourseIdentities; + List positionCoursePractices; + List positionCourseCertificates; + + // 基础信息已通过JOIN查询包含在上面的VO中,不再需要单独的Map + + Map userExamStatusMap; + Map userIdentificationStatusMap; + Map userCertificateStatusMap; + Map> userSkillStatusMap; + String userId; + + public RelatedDataBundle( + List positionCourseExams, + List positionCourseIdentities, + List positionCoursePractices, + List positionCourseCertificates, + Map userExamStatusMap, + Map userIdentificationStatusMap, + Map userCertificateStatusMap, + Map> userSkillStatusMap, + String userId) { + this.positionCourseExams = positionCourseExams; + this.positionCourseIdentities = positionCourseIdentities; + this.positionCoursePractices = positionCoursePractices; + this.positionCourseCertificates = positionCourseCertificates; + this.userExamStatusMap = userExamStatusMap; + this.userIdentificationStatusMap = userIdentificationStatusMap; + this.userCertificateStatusMap = userCertificateStatusMap; + this.userSkillStatusMap = userSkillStatusMap; + this.userId = userId; + } + } + + @Override + public Page positionCourseList(V2CultivateCoursePageReq req) { + + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + return ftbCultivateCourseMapper.queryBindingCourses(page, req); + } + + + @Override + public Boolean checkExist(String postId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbCultivatePosition::getPostId, postId); + queryWrapper.eq(FtbCultivatePosition::getIsGrounding, 1); + queryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); // 查询未被删除的记录 + long count = ftbCultivatePositionMapper.selectCount(queryWrapper); + return count > 0; + } + + @Override + public List otherPositionCourseList(V2OtherCultivatePositionCourseForAppReq req, CultivatePage page) { + UserBoundVO userBoundVO = getUserBoundVO(req); + + // 2. 查询用户岗位ID + String postId = userBoundVO.getPositionId(); + //用户的职级id + String gradeId = userBoundVO.getGradeId(); + + List list = ftbCultivatePositionCourseService.queryAllPositionLearnCourseLists(req); + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + list = filterOtherPositionList(list, postId, gradeId); + if (StringUtils.isNotEmpty(req.getPostId())) { + list = filterSearchPostAndGrade(list, req.getPostId(), req.getGradeId()); + } + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + //获取岗位ids 和 gradeIds + List postIds = new ArrayList<>(); + List gradeIds = new ArrayList<>(); + List courseIds = new ArrayList<>(); + for (AppCourseSimpleVo vo : list) { + postIds.add(vo.getPostId()); + if (StringUtils.isNotEmpty(vo.getGradeId())) { + gradeIds.add(vo.getGradeId()); + } + courseIds.add(vo.getCourseId()); + } + //批量查询岗位名称 + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + Map stringGradeVOMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + + Map studyNumMap = v2CultivateBatchQueryService.batchQueryCourseLearningCount(courseIds); + Map learningStateMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userBoundVO.getId(), courseIds); + + + for (AppCourseSimpleVo vo : list) { + vo.setLearnTotalNumber(studyNumMap.getOrDefault(vo.getCourseId(), 0L)); + PositionVO positionVO = stringPositionVOMap.get(vo.getPostId()); + if (positionVO != null) { + vo.setPostName(positionVO.getFullName()); + } + if (StringUtils.isNotEmpty(vo.getGradeId())) { + GradeVO gradeVO = stringGradeVOMap.get(vo.getGradeId()); + if (gradeVO != null) { + vo.setGradeName(gradeVO.getFullName()); + } + } + FtbCultivatePositionCourceLearning state = learningStateMap.get(vo.getCourseId()); + if (state == null) { + vo.setLearnState(0); // 未学习 + } else { + vo.setLearnState(state.getState()); // 0未学习,1已学习,2学习中 + } + } + return list; + + } + + @Override + public Page allCompleteCourseLists(V2NextUserCultivateCourseListReq req) { + + List courseIds = queryAllCourseIdsForPromotion(req.getUserId(), req.getPromotionId()); + if (CollUtil.isEmpty(courseIds)) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + return ftbCultivatePositionMapper.allCompleteCourseLists(Page.of(req.getCurrentPage(), req.getPageSize()), req, courseIds); + } + + @Override + public Page allCompleteExamLists(V2NextUserCultivateCourseListReq req) { + + List examIds = queryAllExamIdsForPromotion(req.getUserId(), req.getPromotionId()); + if (CollUtil.isEmpty(examIds)) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + return ftbCultivatePositionMapper.allCompleteExamLists(Page.of(req.getCurrentPage(), req.getPageSize()), req, examIds); + } + + @Override + public Page allCompleteIdentityLists(V2NextUserCultivateCourseListReq req) { + + NextUserAppIdentityTmpVo vo = queryAllIdentityIdsForPromotion(req.getUserId(), req.getPromotionId()); + if (CollUtil.isEmpty(vo.getIdentityIds()) || CollUtil.isEmpty(vo.getPositionLearnIds())) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + return ftbCultivatePositionMapper.allCompleteIdentityLists(Page.of(req.getCurrentPage(), req.getPageSize()), req, vo.getIdentityIds(), vo.getPositionLearnIds()); + + } + + @Override + public PageListVO allCompletePracticeLists(V2NextUserCultivateCourseListReq req) { + List practiceIds = queryAllPracticeIdsForPromotion(req.getUserId(), req.getPromotionId()); + if (CollUtil.isEmpty(practiceIds)) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + Map practiceCountMap = new HashMap<>(); + for (String practiceId : practiceIds) { + String[] split = practiceId.split("_"); + practiceCountMap.put(split[0], Integer.parseInt(split[1])); + } + List appPracticeCountVos = ftbCultivatePositionMapper.allCompletePracticeLists(req, new ArrayList<>(practiceCountMap.keySet())); + if (CollUtil.isEmpty(appPracticeCountVos)) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + List ret = new ArrayList<>(); + for (AppPracticeCountVo vo : appPracticeCountVos) { + Integer i = practiceCountMap.get(vo.getId()); + if (vo.getNum() >= i) { + vo.setStatus(1); + ret.add(vo); + } + } + return UserApiV2Util.buildPage(ret, req.getPageSize(), req.getCurrentPage()); + + } + + private List queryAllPracticeIdsForPromotion(String userId, String promotionId) { + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId) + .eq(FtbCultivatePromotionMemberNew::getUserId, userId) + .eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); // 只查询未删除的记录 + List memberNewList = ftbCultivatePromotionMemberNewMapper.selectList(memberWrapper); + if (CollUtil.isEmpty(memberNewList)) { + return new ArrayList<>(); + } + + //查询所有的岗位学习课程列表 + List AllPracticeList = ftbCultivatePositionCoursePracticeService.queryAllPositionLearnParacticeLists(new V2OtherCultivatePositionCourseForAppReq()); + if (CollUtil.isEmpty(AllPracticeList)) { + return new ArrayList<>(); + } + Map> positionToPractice = new HashMap<>(); + for (FtbCultivatePositionCoursePractice vo : AllPracticeList) { + String key = vo.getPostRankId(); + if (StringUtils.isNotEmpty(vo.getGradeId())) { + key = key + "_" + vo.getGradeId(); + } + if (positionToPractice.containsKey(key)) { + positionToPractice.get(key).add(vo.getBusinessId() + "_" + vo.getNum()); + } else { + List ids = new ArrayList<>(); + ids.add(vo.getBusinessId() + "_" + vo.getNum()); + positionToPractice.put(key, ids); + } + } + + List allPracticeIds = new ArrayList<>(); + for (FtbCultivatePromotionMemberNew memberNew : memberNewList) { + String key = memberNew.getPositionId(); + if (StringUtils.isNotEmpty(memberNew.getGradeId())) { + key = key + "_" + memberNew.getGradeId(); + } + List ids = positionToPractice.get(key); + if (CollUtil.isNotEmpty(ids)) { + allPracticeIds.addAll(ids); + } + } + return allPracticeIds; + } + + private NextUserAppIdentityTmpVo queryAllIdentityIdsForPromotion(String userId, String promotionId) { + NextUserAppIdentityTmpVo tmpVo = new NextUserAppIdentityTmpVo(); + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId) + .eq(FtbCultivatePromotionMemberNew::getUserId, userId) + .eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); // 只查询未删除的记录 + List memberNewList = ftbCultivatePromotionMemberNewMapper.selectList(memberWrapper); + if (CollUtil.isEmpty(memberNewList)) { + return tmpVo; + } + + //查询所有的岗位学习课程列表 + List allIdentityList = ftbCultivatePositionCourseIdentityService.queryAllPositionLearnIdentityLists(); + if (CollUtil.isEmpty(allIdentityList)) { + return tmpVo; + } + Map> positionToIdentity = new HashMap<>(); + for (FtbCultivatePositionCourseIdentity vo : allIdentityList) { + String key = vo.getPostRankId(); + if (StringUtils.isNotEmpty(vo.getGradeId())) { + key = key + "_" + vo.getGradeId(); + } + if (positionToIdentity.containsKey(key)) { + positionToIdentity.get(key).add(vo.getIdentityId()); + } else { + List identityIds = new ArrayList<>(); + identityIds.add(vo.getIdentityId()); + positionToIdentity.put(key, identityIds); + } + } + + List allIdentityIds = new ArrayList<>(); + for (FtbCultivatePromotionMemberNew memberNew : memberNewList) { + String key = memberNew.getPositionId(); + if (StringUtils.isNotEmpty(memberNew.getGradeId())) { + key = key + "_" + memberNew.getGradeId(); + } + List identityIds = positionToIdentity.get(key); + if (CollUtil.isNotEmpty(identityIds)) { + allIdentityIds.addAll(identityIds); + } + } + Set positionIds = memberNewList.stream().map(FtbCultivatePromotionMemberNew::getPositionId).collect(Collectors.toSet()); + tmpVo.setPositionLearnIds(convertLearnIngId(positionIds)); + tmpVo.setIdentityIds(allIdentityIds); + return tmpVo; + } + + private List convertLearnIngId(Set positionIds) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbCultivatePosition::getPostId, positionIds); + queryWrapper.eq(FtbCultivatePosition::getIsGrounding, 1); + queryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); // 查询未被删除的记录 + List ftbCultivatePositions = ftbCultivatePositionMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(ftbCultivatePositions)) { + return new ArrayList<>(); + } + return ftbCultivatePositions.stream().map(FtbCultivatePosition::getId).collect(Collectors.toList()); + } + + private List queryAllExamIdsForPromotion(String userId, String promotionId) { + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId) + .eq(FtbCultivatePromotionMemberNew::getUserId, userId) + .eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); // 只查询未删除的记录 + List memberNewList = ftbCultivatePromotionMemberNewMapper.selectList(memberWrapper); + if (CollUtil.isEmpty(memberNewList)) { + return new ArrayList<>(); + } + + //查询所有的岗位学习课程列表 + List AllExamList = ftbCultivatePositionCourseExamService.queryAllPositionLearnExamLists(new V2OtherCultivatePositionCourseForAppReq()); + if (CollUtil.isEmpty(AllExamList)) { + return new ArrayList<>(); + } + Map> positionToExam = new HashMap<>(); + for (FtbCultivatePositionCourseExam vo : AllExamList) { + String key = vo.getPostRankId(); + if (StringUtils.isNotEmpty(vo.getGradeId())) { + key = key + "_" + vo.getGradeId(); + } + if (positionToExam.containsKey(key)) { + positionToExam.get(key).add(vo.getExamId()); + } else { + List examIds = new ArrayList<>(); + examIds.add(vo.getExamId()); + positionToExam.put(key, examIds); + } + } + + List allExamIds = new ArrayList<>(); + for (FtbCultivatePromotionMemberNew memberNew : memberNewList) { + String key = memberNew.getPositionId(); + if (StringUtils.isNotEmpty(memberNew.getGradeId())) { + key = key + "_" + memberNew.getGradeId(); + } + List examIds = positionToExam.get(key); + if (CollUtil.isNotEmpty(examIds)) { + allExamIds.addAll(examIds); + } + } + return allExamIds; + } + + private List queryAllCourseIdsForPromotion(String userId, String promotionId) { + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId) + .eq(FtbCultivatePromotionMemberNew::getUserId, userId) + .eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); // 只查询未删除的记录 + List memberNewList = ftbCultivatePromotionMemberNewMapper.selectList(memberWrapper); + if (CollUtil.isEmpty(memberNewList)) { + return new ArrayList<>(); + } + + //查询所有的岗位学习课程列表 + List AllCourseList = ftbCultivatePositionCourseService.queryAllPositionLearnCourseLists(new V2OtherCultivatePositionCourseForAppReq()); + if (CollUtil.isEmpty(AllCourseList)) { + return new ArrayList<>(); + } + Map> positionToCourse = new HashMap<>(); + for (AppCourseSimpleVo vo : AllCourseList) { + String key = vo.getPostId(); + if (StringUtils.isNotEmpty(vo.getGradeId())) { + key = key + "_" + vo.getGradeId(); + } + if (positionToCourse.containsKey(key)) { + positionToCourse.get(key).add(vo.getCourseId()); + } else { + List courseIds = new ArrayList<>(); + courseIds.add(vo.getCourseId()); + positionToCourse.put(key, courseIds); + } + } + + List allCourseIds = new ArrayList<>(); + for (FtbCultivatePromotionMemberNew memberNew : memberNewList) { + String key = memberNew.getPositionId(); + if (StringUtils.isNotEmpty(memberNew.getGradeId())) { + key = key + "_" + memberNew.getGradeId(); + } + List courseIds = positionToCourse.get(key); + if (CollUtil.isNotEmpty(courseIds)) { + allCourseIds.addAll(courseIds); + } + } + return allCourseIds; + } + + + private List filterSearchPostAndGrade(List lists, String postId, String gradeId) { + List returnList = new ArrayList<>(); + for (AppCourseSimpleVo vo : lists) { + if (StringUtils.isEmpty(gradeId)) { + if (vo.getPostId().equals(postId)) { + returnList.add(vo); + } + } else { + if (vo.getPostId().equals(postId) && vo.getGradeId().equals(gradeId)) { + returnList.add(vo); + } + } + } + return returnList; + } + + + @Override + public Page selfPositionAllCompleteCourseLists(V2NextUserCultivateCourseListReq req) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userPrimaryBoundOne == null || StringUtils.isEmpty(userPrimaryBoundOne.getPositionId())) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + String positionId = userPrimaryBoundOne.getPositionId(); + String gradeId = userPrimaryBoundOne.getGradeId(); + // 3. 查询岗位学习是否存在 + FtbCultivatePosition cultivatePosition = getFtbCultivatePosition(positionId); + + if (cultivatePosition == null) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + List ids = new ArrayList<>(); + if (cultivatePosition.getIsConfiguredToGrade().equals(0))//未分配到职级 + { + //根据岗位id查询课程列表 + ids = queryPositionBindItem(cultivatePosition, CultivateTypeEnum.COURSE, ""); + } else { + if (StringUtils.isNotEmpty(gradeId)) { + //根据岗位id和职级id查询课程列表 + ids = queryPositionBindItem(cultivatePosition, CultivateTypeEnum.COURSE, gradeId); + } + } + + if (CollUtil.isEmpty(ids)) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + return ftbCultivatePositionMapper.allCompleteCourseLists(Page.of(req.getCurrentPage(), req.getPageSize()), req, ids); + } + + private FtbCultivatePosition getFtbCultivatePosition(String positionId) { + return ftbCultivatePositionMapper.selectOne( + Wrappers.lambdaQuery() + .eq(FtbCultivatePosition::getPostId, positionId) + .eq(FtbCultivatePosition::getIsGrounding, 1) + .eq(FtbCultivatePosition::getEnabledMark, 0) + ); + } + + @Override + public Page selfPositionAllCompleteExamLists(V2NextUserCultivateCourseListReq req) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userPrimaryBoundOne == null || StringUtils.isEmpty(userPrimaryBoundOne.getPositionId())) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + String positionId = userPrimaryBoundOne.getPositionId(); + String gradeId = userPrimaryBoundOne.getGradeId(); + // 3. 查询岗位学习是否存在 + FtbCultivatePosition cultivatePosition = getFtbCultivatePosition(positionId); + + if (cultivatePosition == null) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + List ids = new ArrayList<>(); + if (cultivatePosition.getIsConfiguredToGrade().equals(0))//未分配到职级 + { + //根据岗位id查询课程列表 + ids = queryPositionBindItem(cultivatePosition, CultivateTypeEnum.EXAM, ""); + } else { + if (StringUtils.isNotEmpty(gradeId)) { + //根据岗位id和职级id查询课程列表 + ids = queryPositionBindItem(cultivatePosition, CultivateTypeEnum.EXAM, gradeId); + } + } + + if (CollUtil.isEmpty(ids)) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + + return ftbCultivatePositionMapper.allCompleteExamLists(Page.of(req.getCurrentPage(), req.getPageSize()), req, ids); + } + + @Override + public Page selfPositionAllCompleteIdentityLists(V2NextUserCultivateCourseListReq req) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userPrimaryBoundOne == null || StringUtils.isEmpty(userPrimaryBoundOne.getPositionId())) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + String positionId = userPrimaryBoundOne.getPositionId(); + String gradeId = userPrimaryBoundOne.getGradeId(); + // 3. 查询岗位学习是否存在 + FtbCultivatePosition cultivatePosition = getFtbCultivatePosition(positionId); + + if (cultivatePosition == null) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + List ids = new ArrayList<>(); + if (cultivatePosition.getIsConfiguredToGrade().equals(0))//未分配到职级 + { + //根据岗位id查询课程列表 + ids = queryPositionBindItem(cultivatePosition, CultivateTypeEnum.IDENTITY, ""); + } else { + if (StringUtils.isNotEmpty(gradeId)) { + //根据岗位id和职级id查询课程列表 + ids = queryPositionBindItem(cultivatePosition, CultivateTypeEnum.IDENTITY, gradeId); + } + } + + if (CollUtil.isEmpty(ids)) { + return Page.of(req.getCurrentPage(), req.getPageSize()); + } + return ftbCultivatePositionMapper.allCompleteIdentityLists(Page.of(req.getCurrentPage(), req.getPageSize()), req, ids, List.of(positionId)); + } + + @Override + public PageListVO selfPositionAllCompletePracticeLists(V2NextUserCultivateCourseListReq req) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(req.getUserId(), null); + if (userPrimaryBoundOne == null || StringUtils.isEmpty(userPrimaryBoundOne.getPositionId())) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + String positionId = userPrimaryBoundOne.getPositionId(); + String gradeId = userPrimaryBoundOne.getGradeId(); + // 3. 查询岗位学习是否存在 + FtbCultivatePosition cultivatePosition = getFtbCultivatePosition(positionId); + + if (cultivatePosition == null) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + + List positionCoursePractices = new ArrayList<>(); + if (cultivatePosition.getIsConfiguredToGrade().equals(0))//未分配到职级 + { + //根据岗位id查询课程列表 + positionCoursePractices = ftbCultivatePositionCoursePracticeService.queryPositionBindItem(cultivatePosition, ""); + } else { + if (StringUtils.isNotEmpty(gradeId)) { + //根据岗位id和职级id查询课程列表 + positionCoursePractices = ftbCultivatePositionCoursePracticeService.queryPositionBindItem(cultivatePosition, gradeId); + } + } + + if (CollUtil.isEmpty(positionCoursePractices)) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + + + List appPracticeCountVos = ftbCultivatePositionMapper.allCompletePracticeLists(req, positionCoursePractices.stream().map(FtbCultivatePositionCoursePractice::getBusinessId).collect(Collectors.toList())); + if (CollUtil.isEmpty(appPracticeCountVos)) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + Map map = new HashMap<>(); + for (FtbCultivatePositionCoursePractice positionCoursePractice : positionCoursePractices) { + map.put(positionCoursePractice.getBusinessId(), positionCoursePractice); + } + appPracticeCountVos = appPracticeCountVos.stream().filter(vo -> vo.getNum() > 0).collect(Collectors.toList()); + List ret = new ArrayList<>(); + for (AppPracticeCountVo vo : appPracticeCountVos) { + FtbCultivatePositionCoursePractice practice = map.get(vo.getId()); + if (vo.getNum() >= practice.getNum()) { + vo.setStatus(1); + ret.add(vo); + } + } + return UserApiV2Util.buildPage(ret, req.getPageSize(), req.getCurrentPage()); + } + + private List queryPositionBindItem(FtbCultivatePosition cultivatePosition, CultivateTypeEnum typeEnum, String gradeId) { + switch (typeEnum) { + case COURSE: + return ftbCultivatePositionCourseService.queryPositionBindItem(cultivatePosition, gradeId); + case EXAM: + return ftbCultivatePositionCourseExamService.queryPositionBindItem(cultivatePosition, gradeId); + case IDENTITY: + return ftbCultivatePositionCourseIdentityService.queryPositionBindItem(cultivatePosition, gradeId); + default: + return new ArrayList<>(); + } + } + + /** + * 检查考试是否被岗位学习绑定 + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + @Override + public Boolean checkExamBinding(String examId) { + return ftbCultivatePositionCourseExamService.checkExamBinding(examId); + } + + public static final class PositionConstants { + public static final int ENABLED_MARK_VALID = 0; + public static final int ENABLED_MARK_DELETED = 1; + public static final int IS_GROUNDING_ONLINE = 1; + public static final int IS_GROUNDING_OFFLINE = 0; + public static final long DEFAULT_COUNT = 0L; + public static final int LEARN_STATE_NOT_STARTED = 0; + public static final int LEARN_STATE_COMPLETED = 1; + public static final int LEARN_STATE_IN_PROGRESS = 2; + public static final int DEFAULT_SORT_CODE = 0; + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePostStudyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePostStudyServiceImpl.java new file mode 100644 index 0000000..ce21205 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePostStudyServiceImpl.java @@ -0,0 +1,1833 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.CultivateExamMapper; +import jnpf.cultivate.mapper.FtbCultivateCertificateResultMapper; +import jnpf.cultivate.mapper.FtbCultivateExamUserMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourseExamService; +import jnpf.cultivate.service.impl.FtbCultivatePositionCourseIdentityService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.*; +import jnpf.entity.cultivate.*; +import jnpf.enums.cultivate.ApplyStatusEnum; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.po.FtbCultivatePositionCourseCertificate; +import jnpf.model.cultivate.po.course.FtbCultivateCertificateResult; +import jnpf.model.cultivate.po.course.FtbCultivateCourse; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.*; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionLog; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionMemberNew; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import jnpf.model.cultivate.v2.position.vo.CheckPostAndGradeResult; +import jnpf.model.cultivate.v2.position.vo.FtbCultivatePositionCourseWithNameVo; +import jnpf.model.cultivate.v2.position.vo.PositionStatusResultVo; +import jnpf.model.cultivate.v2.position.vo.V2AllCultivatePositionCourseExam; +import jnpf.model.cultivate.v2.promotion.vo.MyCultivatePromotionListVo; +import jnpf.model.cultivate.v2.promotion.vo.V2CultivatePostAndGradeVoExt; +import jnpf.model.cultivate.vo.identify.UserOrgInfoAll1Vo; +import jnpf.model.enums.CourseEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.ConstantUtil; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class V2CultivatePostStudyServiceImpl implements V2CultivatePostStudyService { + + + @Autowired + private FtbCultivatePositionService ftbCultivatePositionService; + + @Autowired + private FtbCultivatePositionCourseService ftbCultivatePositionCourseService; + + @Autowired + private FtbCultivatePositionCourseExamService ftbCultivatePositionCourseExamService; + + @Autowired + private FtbCultivatePositionCourseIdentityService ftbCultivatePositionCourseIdentityService; + + @Autowired + private FtbCultivatePositionCoursePracticeService ftbCultivatePositionCoursePracticeService; + + @Autowired + private FtbCultivatePositionCourseCertificateService ftbCultivatePositionCourseCertificateService; + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + @Autowired + private FtbCultivatePositionSettingService ftbCultivatePositionSettingService; + + + @Autowired + private FtbCultivatePositionLogService ftbCultivatePositionLogService; + + + @Autowired + private FtbCultivateCourseService ftbCultivateCourseService; + + @Autowired + private CultivateIdentifyTableService identifyTableService; + + @Autowired + private CultivateIdentifyItemsService identifyItemsService; + + @Autowired + private CultivateIdentifyApplyDetailsService applyDetailsService; + + @Autowired + private CultivateIdentifyApplyService identifyApplyService; + + @Autowired + private CultivateIdentifyApplyTableBackupsService applyTableBackupsService; + + @Autowired + private CultivateIdentifyApplyDetailsBackupsService applyDetailsBackupsService; + + @Autowired + private FtbCultivatePromotionLogService ftbCultivatePromotionLogService; + + @Autowired + private V2CultivateCertificateService v2CultivateCertificateService; + + + @Autowired + private CultivateExamDrawRuleService examDrawRuleService; + + @Autowired + private FtbCultivatePromotionUserService ftbCultivatePromotionUserService; + + @Autowired + private FtbCultivatePromotionMemberNewService ftbCultivatePromotionMemberNewService; + + @Autowired + private FtbCultivatePromotionPostNewService ftbCultivatePromotionPostNewService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + + @Autowired + private FtbCultivateCertificateResultMapper ftbCultivateCertificateResultMapper; + + + @Autowired + private CultivatePositionCourseLogService cultivatePositionCourseLogService; + + + @Autowired + private CultivateExamMapper v2CultivateExamMapper; + + + @Autowired + private ApplicationContext applicationContext; + + private V2CultivatePostStudyService selfProxy; + + @PostConstruct + public void init() { + this.selfProxy = applicationContext.getBean(V2CultivatePostStudyService.class); + } + + /** + * 岗位学习 + * + * @param message 培训信息 + */ + @Override + @Transactional + public void dealPositionCourseStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String courseId = message.getBusinessId(); + String positionLearnId = message.getBusinessSourceId();//岗位学习id + String tenantId = message.getTenantId(); + + if (StringUtils.isEmpty(positionLearnId) || StringUtils.isEmpty(courseId) || StringUtils.isEmpty(userId)) { + return; + } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,courseId={}, userId={}", courseId, userId); + return; + } + //查询岗位学习的基本信息 + FtbCultivatePosition position = ftbCultivatePositionService.getById(positionLearnId); + if (position == null) { + log.error("dealPositionCourse positionLearnId={},未查询到岗位学习", positionLearnId); + return; + } + + + String gradeId = ""; + String postId = position.getPostId(); + // 查询岗位学习课程表 - 使用新封装的方法 + FtbCultivatePositionSetting positionSetting; + List courseList; + + if (position.getIsConfiguredToGrade().equals(0)) { + courseList = ftbCultivatePositionCourseService.listByPostLearnId(positionLearnId, courseId); + positionSetting = ftbCultivatePositionSettingService.listByPostLearnId(positionLearnId); + } else { + if (message.getParams() == null || message.getParams().get(CultivateMqDTO.GRADE_ID) == null) { + log.error("dealPositionCourse positionLearnId={},分配到职级了,但是未指定职级 params={}", positionLearnId, message.getParams()); + return; + } + gradeId = (String) message.getParams().get(CultivateMqDTO.GRADE_ID); + courseList = ftbCultivatePositionCourseService.listByPostLearnIdAndGradeId(positionLearnId, gradeId, courseId); + positionSetting = ftbCultivatePositionSettingService.listByPostLearnIdAndGradeId(positionLearnId, gradeId); + } + if (CollUtil.isEmpty(courseList)) { + log.error("dealPositionCourse positionLearnId={},courseId={},未查询到课程", positionLearnId, courseId); + return; + } + List logList = queryCompletePositionList(userId, positionLearnId, postId, gradeId, position); + if (CollUtil.isNotEmpty(logList)) { + log.error("dealPositionCourse positionLearnId={},courseId={},userId={},postId={},gradeId={},用户已学习完成", + positionLearnId, courseId, userId, postId, gradeId); + return; + } + //检查课程是否学习完成 + if (!checkCourseIsComplete(courseList.stream().map(FtbCultivatePositionCourseWithNameVo::getCourseId).collect(Collectors.toList()), userId)) { + return; + } + + // 查询考试信息 - 使用新封装的方法 + List exams = ftbCultivatePositionCourseExamService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List examIds = exams.stream().map(FtbCultivatePositionCourseExam::getExamId).distinct().collect(Collectors.toList()); + + // 查询鉴定信息 - 使用新封装的方法 + List identities = ftbCultivatePositionCourseIdentityService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List identityIds = identities.stream().map(FtbCultivatePositionCourseIdentity::getIdentityId).distinct().collect(Collectors.toList()); + + // 查询练习信息 - 使用新封装的方法 + List practices = ftbCultivatePositionCoursePracticeService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List practiceIds = practices.stream().map(FtbCultivatePositionCoursePractice::getBusinessId).distinct().collect(Collectors.toList()); + Map practiceMap = new HashMap<>(); + if (CollUtil.isNotEmpty(practices)) { + for (FtbCultivatePositionCoursePractice practice : practices) { + practiceMap.put(practice.getBusinessId(), practice); + } + } + // 查询证书信息 - 使用新封装的方法 + List certificates = ftbCultivatePositionCourseCertificateService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List certificateIds = certificates.stream().map(FtbCultivatePositionCourseCertificate::getCertificateId).distinct().collect(Collectors.toList()); + + // 批量查询用户的考试、鉴定、证书、技能完成情况 + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, identityIds, 2, positionLearnId); +// Map userCertificateStatusMap = v2CultivateBatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillStatusMap = v2CultivateBatchQueryService.batchQueryUserSkillStatus(userId, practiceIds); + + Integer qualified = 0; //试合格和练习满足才能完成鉴定【0-否 1-是】 + if (positionSetting != null) { + qualified = positionSetting.getQualified(); + } + + // 检查考试、技能和鉴定状态 + PositionStatusResultVo positionStatusResult = evaluatePositionStatus(userId, positionLearnId, examIds, practiceIds, identityIds, + qualified, userExamStatusMap, userIdentificationStatusMap, practiceMap, userSkillStatusMap, courseId, gradeId, tenantId); + + handlePositionCourseCompletion(userId, positionLearnId, postId, gradeId, courseId, certificateIds, tenantId, + positionStatusResult); + + checkAndCompletePosition(userId, positionLearnId, postId, gradeId, tenantId); + + // 查询用户所在的学习地图 + List list = ftbCultivatePromotionUserService.queryMyAllPromotionList(userId); + if (CollUtil.isNotEmpty(list)) { + List promotionIds = list.stream().map(MyCultivatePromotionListVo::getId).collect(Collectors.toList()); + Map> promotionPostMap = batchQueryPositionsByPromotionIds(promotionIds); + checkPromotionComplete(list, promotionPostMap, userId, tenantId); + } + } + + /** + * 查询用户对岗位学习是否完成 + * + * @param userId 用户ID + * @param positionLearnId 岗位学习ID + * @param postId 岗位id + * @param gradeId 职级id + */ + private void checkAndCompletePosition(String userId, String positionLearnId, String postId, String gradeId, String tenantId) { + + boolean isComplete = ftbCultivatePositionLogService.selectIsCompletePosition(userId, positionLearnId, postId, gradeId, tenantId); + if (isComplete) { + return; + } + //查询用户已经完成的所有课程id + List completeCourseIds = v2CultivateBatchQueryService.getUserCompletedCourseIds(userId); + if (CollUtil.isEmpty(completeCourseIds)) { + return; + } + + List courseList = ftbCultivatePositionCourseService.listAllCourseByPostLearnId(positionLearnId, postId, gradeId, 0); + if (CollUtil.isEmpty(courseList)) { + return; + } + + List courseIds = courseList.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList()); + List positionCourseLogEntityList = ftbCultivatePositionLogService.queryByUserIdAndPostLearnId(userId, positionLearnId); + if (CollUtil.isEmpty(positionCourseLogEntityList)) { + return; + } + Map positionCourseLogEntityMap = new HashMap<>(); + for (CultivatePositionCourseLogEntity positionCourseLogEntity : positionCourseLogEntityList) { + positionCourseLogEntityMap.put(positionCourseLogEntity.getCourseId(), positionCourseLogEntity); + } + // 判断需要完成的课程是否已经全部完成(已完成课程包含所有应完成课程) + if (CollUtil.isNotEmpty(courseIds) && new HashSet<>(completeCourseIds).containsAll(courseIds)) { + for (String courseId : courseIds) { + CultivatePositionCourseLogEntity courseLogEntity = positionCourseLogEntityMap.get(courseId); + if (courseLogEntity == null) { + return; + } + if (!courseLogEntity.getState().equals(2)) { + return; + } + // 检查考试状态:如果有考试(hasExam=2),则考试状态不能是待考试(0)、待批阅(1)或已逾期(2) + if (courseLogEntity.getHasExam().equals(2)) { + Integer examStatus = courseLogEntity.getUserExamStatus(); + if (examStatus == null || examStatus.equals(0) || examStatus.equals(1) || examStatus.equals(2)) { + return; + } + } + // 检查鉴定状态:如果有鉴定(hasIdentity=2),则鉴定状态不能是待鉴定(-1) + if (courseLogEntity.getHasIdentity().equals(2)) { + Integer identifyStatus = courseLogEntity.getUserIdentifyStatus(); + if (identifyStatus == null || identifyStatus.equals(-1)) { + return; + } + } + } + + + ftbCultivatePositionLogService.completePosition(userId, positionLearnId, postId, gradeId, tenantId); + } + } + + /** + * 评估岗位学习状态 + */ + public PositionStatusResultVo evaluatePositionStatus(String userId, String positionLearnId, List examIds, List practiceIds, + List identityIds, + Integer qualified, + Map userExamStatusMap, + Map userIdentificationStatusMap, + Map practiceMap, + Map> userSkillStatusMap, + String courseId, + String gradeId, + String tenantId + ) { + boolean isExam = true;//false-未完成 true-已完成 + boolean isPractice = true;//false-未完成 true-已完成 + PositionStatusResultVo resultVo = new PositionStatusResultVo(); + + // 根据是否有考试设置hasExam字段 + Integer hasExam = CollUtil.isNotEmpty(examIds) ? 2 : 1; + + // 根据是否有鉴定设置hasIdentity字段 + Integer hasIdentity = CollUtil.isNotEmpty(identityIds) ? 2 : 1; + + // 检查考试状态 + if (CollUtil.isNotEmpty(examIds)) { + isExam = checkExamStatus(userId, positionLearnId, examIds, qualified, userExamStatusMap, courseId, gradeId, hasIdentity, tenantId); + } + + // 检查技能状态 + if (CollUtil.isNotEmpty(practiceIds)) { + isPractice = checkPracticeStatus(practiceIds, practiceMap, userSkillStatusMap, qualified); + } + + // 检查鉴定状态 + if (CollUtil.isNotEmpty(identityIds)) { + resultVo = checkIdentificationStatus(userId, positionLearnId, identityIds, qualified, + isExam, isPractice, userIdentificationStatusMap, courseId, gradeId, tenantId); + } else { + resultVo.setIsIdentification(true); + resultVo.setFinishIdentification(true); + } + + // 更新hasExam和hasIdentity字段 + updateHasExamAndIdentity(positionLearnId, userId, courseId, hasExam, hasIdentity, gradeId); + return new PositionStatusResultVo(isExam, isPractice, resultVo.getIsIdentification(), resultVo.getFinishIdentification()); + } + + /** + * 检查考试状态 + */ + public boolean checkExamStatus(String userId, String positionLearnId, List examIds, Integer qualified, + Map userExamStatusMap, String courseId, String gradeId, Integer hasIdentity, String tenantId) { + CultivatePositionCourseLogEntity courseLogEntity = ftbCultivatePositionLogService.queryIsTriggerExam(positionLearnId, userId, courseId, gradeId); + if (courseLogEntity != null && StringUtils.isNotEmpty(courseLogEntity.getUserExamId())) { + // 有考试记录时,检查考试状态 + Integer examStatus = courseLogEntity.getUserExamStatus(); + + // 如果有待批阅(1)、待考试(0)或已逾期(2)状态,则认为考试未完成 + if (examStatus.equals(0) || examStatus.equals(1) || examStatus.equals(2)) { + return false; + } + + // 试合格和练习满足才能完成鉴定【0-否 1-是】 + if (qualified == 1 && hasIdentity == 2) { + // 需要考试合格或优秀才能触发鉴定 + return examStatus.equals(3) || examStatus.equals(5); + } else { + // 不需要鉴定或不需要合格,只要考试有结果即可(合格、不合格、优秀) + return examStatus.equals(3) || examStatus.equals(4) || examStatus.equals(5); + } + } + + // 没有考试记录,查询或创建考试 + FtbCultivateExamUser ftbCultivateExamUser = userExamStatusMap.get(examIds.get(0)); + if (ftbCultivateExamUser != null) { + ftbCultivatePositionLogService.recordUserExamId(positionLearnId, userId, courseId, gradeId, ftbCultivateExamUser); + Integer status = ftbCultivateExamUser.getStatus(); + + // 如果有待批阅(1)、待考试(0)或已逾期(2)状态,则认为考试未完成 + if (status.equals(0) || status.equals(1) || status.equals(2)) { + return false; + } + + if (hasIdentity == 2) { + // 需要鉴定时,必须考试合格或优秀 + return status.equals(3) || status.equals(5); + } else { + // 不需要鉴定时,合格、不合格、优秀都可以 + return status.equals(3) || status.equals(4) || status.equals(5); + } + } + + // 触发考试 + ftbCultivateExamUser = triggerExam(userId, examIds.get(0), 2, positionLearnId, tenantId); + ftbCultivatePositionLogService.recordUserExamId(positionLearnId, userId, courseId, gradeId, ftbCultivateExamUser); + return false; + } + + public String checkExamStatusReturnUserExamId(String userId, String positionLearnId, List examIds, + Map userExamStatusMap, String courseId, String gradeId, String tenantId) { + CultivatePositionCourseLogEntity courseLogEntity = ftbCultivatePositionLogService.queryIsTriggerExam(positionLearnId, userId, courseId, gradeId); + if (courseLogEntity != null && StringUtils.isNotEmpty(courseLogEntity.getUserExamId())) { + return courseLogEntity.getUserExamId(); + } + FtbCultivateExamUser ftbCultivateExamUser = userExamStatusMap.get(examIds.get(0)); + if (ftbCultivateExamUser != null) { + ftbCultivatePositionLogService.recordUserExamId(positionLearnId, userId, courseId, gradeId, ftbCultivateExamUser); + return ftbCultivateExamUser.getId(); + } + ftbCultivateExamUser = triggerExam(userId, examIds.get(0), 2, positionLearnId, tenantId); + ftbCultivatePositionLogService.recordUserExamId(positionLearnId, userId, courseId, gradeId, ftbCultivateExamUser); + return ftbCultivateExamUser.getId(); + } + + /** + * 检查技能状态 + */ + private boolean checkPracticeStatus(List practiceIds, + Map practiceMap, + Map> userSkillStatusMap, + Integer qualified + ) { + for (String practiceId : practiceIds) { + FtbCultivatePositionCoursePractice ftbCultivatePositionCoursePractice = practiceMap.get(practiceId); + List count = userSkillStatusMap.get(practiceId); + if (qualified == 1 && (ftbCultivatePositionCoursePractice == null || count == null || count.size() < ftbCultivatePositionCoursePractice.getNum())) { + return false; + } + } + return true; + } + + /** + * 检查鉴定状态颁发证书 + */ + private PositionStatusResultVo checkIdentificationStatus(String userId, String positionLearnId, List identityIds, + Integer qualified, boolean isExam, boolean isPractice, + Map userIdentificationStatusMap, + String courseId, String gradeId, + String tenantId + ) { + //试合格和练习满足才能完成鉴定【0-否 1-是】 + if (qualified.equals(1)) { + // 需要考试和练习都完成才能进行鉴定 + if (isExam && isPractice) { + return checkIdentificationWithQualification(userId, positionLearnId, identityIds, userIdentificationStatusMap, courseId, gradeId, qualified, tenantId); + } + return new PositionStatusResultVo() {{ + setIsIdentification(false); + setFinishIdentification(false); + }}; + } + // 不需要考试和练习完成即可鉴定 + return checkIdentificationWithQualification(userId, positionLearnId, identityIds, userIdentificationStatusMap, courseId, gradeId, qualified, tenantId); + + } + + /** + * 检查鉴定状态(需要考试和练习完成) + */ + private PositionStatusResultVo checkIdentificationWithQualification(String userId, String positionLearnId, List identityIds, + Map userIdentificationStatusMap, + String courseId, String gradeId, Integer qualified, String tenantId) { + PositionStatusResultVo resultVo = new PositionStatusResultVo(); + CultivatePositionCourseLogEntity courseLogEntity = ftbCultivatePositionLogService.queryIsTriggerExam(positionLearnId, userId, courseId, gradeId); + if (courseLogEntity != null && StringUtils.isNotEmpty(courseLogEntity.getUserIdentifyId())) { + // 有鉴定记录时,检查鉴定状态 + Integer identifyStatus = courseLogEntity.getUserIdentifyStatus(); + + // -1:待鉴定,认为鉴定未完成 + if (identifyStatus == null || identifyStatus.equals(-1)) { + resultVo.setIsIdentification(false); + resultVo.setFinishIdentification(false); + } else { + resultVo.setFinishIdentification(true); + // 鉴定结果为 0-合格,1-优秀,则认为鉴定完成;2-不合格,也认为有结果了 + if (qualified == 0) { + resultVo.setIsIdentification(true); + } else { + resultVo.setIsIdentification(!identifyStatus.equals(2)); + } + } + + return resultVo; + } + + // 没有鉴定记录,查询或创建鉴定 + CultivateIdentifyApply cultivateIdentifyApply = userIdentificationStatusMap.get(identityIds.get(0)); + if (cultivateIdentifyApply == null) { + cultivateIdentifyApply = triggerIdentification(userId, identityIds.get(0), 2, positionLearnId, tenantId); + ftbCultivatePositionLogService.recordUserIdentificationId(positionLearnId, userId, courseId, gradeId, cultivateIdentifyApply); + resultVo.setIsIdentification(false); + resultVo.setFinishIdentification(false); + return resultVo; + } + + // status: 0-待鉴定, 1-已完成, 2-已撤销 + // result: 0-合格, 1-优秀, 2-不合格 + if (cultivateIdentifyApply.getStatus().equals(0) || cultivateIdentifyApply.getStatus().equals(2)) { + // 待鉴定或已撤销,认为鉴定未完成 + resultVo.setIsIdentification(false); + resultVo.setFinishIdentification(false); + return resultVo; + } + + // 已完成且结果不是不合格,则认为鉴定完成 + if (qualified == 0) { + resultVo.setIsIdentification(true); + } else { + resultVo.setIsIdentification(!cultivateIdentifyApply.getResult().equals(2)); + } + resultVo.setFinishIdentification(true); + return resultVo; + } + + /** + * 更新是否有考试和是否有鉴定字段 + * + * @param positionLearnId 岗位学习ID + * @param userId 用户ID + * @param courseId 课程ID + * @param hasExam 是否有考试 1-没有 2-有 + * @param hasIdentity 是否有鉴定 1-没有 2-有 + */ + private void updateHasExamAndIdentity(String positionLearnId, String userId, String courseId, Integer hasExam, Integer hasIdentity, String gradeId) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CultivatePositionCourseLogEntity::getPostLearnId, positionLearnId) + .eq(CultivatePositionCourseLogEntity::getUserId, userId) + .eq(CultivatePositionCourseLogEntity::getCourseId, courseId) + .eq(StringUtils.isNotEmpty(gradeId), CultivatePositionCourseLogEntity::getGradeId, gradeId) + .eq(CultivatePositionCourseLogEntity::getEnabledMark, 0) + .set(CultivatePositionCourseLogEntity::getHasExam, hasExam) + .set(CultivatePositionCourseLogEntity::getHasIdentity, hasIdentity); + cultivatePositionCourseLogService.update(null, updateWrapper); + } + + /** + * 处理岗位学习完成逻辑(证书和日志记录) + */ + private void handlePositionCourseCompletion(String userId, String positionLearnId, String postId, String gradeId, String courseId, + List certificateIds, String tenantId, + PositionStatusResultVo positionStatusResult) { + //颁发证书 + if (positionStatusResult.getIsExam() && positionStatusResult.getIsPractice() && positionStatusResult.getIsIdentification()) { + if (CollUtil.isNotEmpty(certificateIds)) { + if (!checkIsTriggerPositionCertificate(userId, certificateIds.get(0), positionLearnId, postId, gradeId, courseId)) { + triggerCertificate(userId, certificateIds.get(0), tenantId); + recordTriggerPositionCertificate(userId, certificateIds.get(0), positionLearnId, postId, gradeId, courseId); + } + } + } + //鉴定完成 进入下一阶段 + if (positionStatusResult.getIsExam() && positionStatusResult.getIsPractice() && positionStatusResult.getFinishIdentification()) { + ftbCultivatePositionLogService.completePositionCourse(userId, positionLearnId, postId, gradeId, courseId); + } + } + + + /** + * 通用课程学习 + * + * @param message 培训信息 + */ + @Override + @Transactional + public void dealCommonCourseStudy(CultivateMqDTO message) { + + String userId = message.getUserId(); + String courseId = message.getBusinessId(); + String tenantId = message.getTenantId(); + + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,examId={}, userId={}", userId); + return; + } + + FtbCultivateCourse course = ftbCultivateCourseService.getById(courseId); + if (course == null || course.getEnableMark() == 1 || course.getIsGroundIng().equals(0)) { + log.warn("dealCommonCourseStudy方法接收到的message为null"); + return; + } + if (course.getNeedExamAndIdentify().equals(0)) { + return; + } + if (StringUtils.isNotEmpty(course.getExamId())) { + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, List.of(course.getExamId())); + //触发考试 + FtbCultivateExamUser ftbCultivateExamUser = userExamStatusMap.get(course.getExamId()); + if (ftbCultivateExamUser == null) { + //触发考试 + triggerExam(userId, course.getExamId(), 1, courseId, tenantId); + } + } + if (StringUtils.isNotEmpty(course.getIdentifyId())) { + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, List.of(course.getIdentifyId()), 1, courseId); + CultivateIdentifyApply cultivateIdentifyApply = userIdentificationStatusMap.get(course.getIdentifyId()); + if (cultivateIdentifyApply == null) { + //触发鉴定 userid + identifyId + positionId + triggerIdentification(userId, course.getIdentifyId(), 1, courseId, tenantId); + } + } + + } + + /** + * 岗位考试学习 + * + * @param message 培训信息 + */ + @Override + public void dealPositionExamStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String examId = message.getBusinessId(); + String tenantId = message.getTenantId(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未 null,examId={}, userId={}", examId, userId); + return; + } + if (StringUtils.isEmpty(examId) || StringUtils.isEmpty(userId)) { + log.error("dealPositionExamStudy 参数不完整,examId={}, userId={}", examId, userId); + return; + } + recodePositionCourseExam(userId, examId, message.getParams(), message.getStatus()); + + // 获取需要检查的岗位和职级列表 + CheckPostAndGradeResult result = buildCheckPostAndGradeList(userPrimaryBoundOne, userId); + if (CollUtil.isEmpty(result.getCheckPostAndGrade())) { + return; + } + + for (V2CultivatePostAndGradeVoExt itemVo : result.getCheckPostAndGrade()) { + this.selfProxy.dealPositionExamStudyItem(userPrimaryBoundOne, examId, itemVo, tenantId); + } + + //判断学习地图是否完成并记录 + checkPromotionComplete(result.getList(), result.getPromotionPostMap(), userId, tenantId); + } + + + public String preCommonCourseStudy(String userId, String courseId, String tenantId) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,examId={}, userId={}", userId); + return ""; + } + FtbCultivateCourse course = ftbCultivateCourseService.getById(courseId); + if (course == null || course.getEnableMark() == 1 || course.getIsGroundIng().equals(0)) { + log.warn("dealCommonCourseStudy方法接收到的message为null"); + return ""; + } + if (course.getNeedExamAndIdentify().equals(0)) { + return ""; + } + if (StringUtils.isNotEmpty(course.getExamId())) { + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, List.of(course.getExamId())); + //触发考试 + FtbCultivateExamUser ftbCultivateExamUser = userExamStatusMap.get(course.getExamId()); + if (ftbCultivateExamUser == null) { + //触发考试 + ftbCultivateExamUser = triggerExam(userId, course.getExamId(), 1, courseId, tenantId); + } + if (ftbCultivateExamUser != null) { + return ftbCultivateExamUser.getId(); + } + + } + return ""; + } + + private void recodePositionCourseExam(String userId, String examId, Map params, Integer status) { + if (params == null) { + return; + } + if (params.get("userExamId") == null || params.get("userExamId").equals("")) { + return; + } + String userExamId = (String) params.get("userExamId"); + ftbCultivatePositionLogService.recodePositionCourseExam(userId, examId, userExamId, status); + } + + + /** + * 岗位学习 + * + * @param message 培训信息 + */ + @Override + public void dealPositionReCheckStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String tenantId = message.getTenantId(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("dealPositionReCheckStudy 用户查询未 null,userId={},tenantId={}", userId, tenantId); + return; + } + if (StringUtils.isEmpty(tenantId) || StringUtils.isEmpty(userId)) { + log.error("dealPositionReCheckStudy 参数补全,userId={},tenantId={}", userId, tenantId); + return; + } + + // 获取需要检查的岗位和职级列表 + CheckPostAndGradeResult result = buildCheckPostAndGradeList(userPrimaryBoundOne, userId); + if (CollUtil.isEmpty(result.getCheckPostAndGrade())) { + return; + } + + for (V2CultivatePostAndGradeVoExt itemVo : result.getCheckPostAndGrade()) { + this.selfProxy.dealPositionReCheckStudyItem(userPrimaryBoundOne, itemVo, tenantId); + } + + //判断学习地图是否完成并记录 + checkPromotionComplete(result.getList(), result.getPromotionPostMap(), userId, tenantId); + + + } + + + /** + * 岗位鉴定学习 + * + * @param message 培训信息 + */ + @Override + public void dealPositionIdentityStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String identityId = message.getBusinessId(); + String tenantId = message.getTenantId(); + + if (StringUtils.isEmpty(identityId) || StringUtils.isEmpty(userId)) { + log.error("dealPositionIdentityStudy参数不完整,identityId={}, userId={}", identityId, userId); + return; + } + + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,identityId={}, userId={}", identityId, userId); + return; + } + recodePositionIdentityLog(userId, identityId, message); + + // 查询需要鉴定的岗位和职级 + CheckPostAndGradeResult result = buildCheckPostAndGradeList(userPrimaryBoundOne, userId); + if (CollUtil.isEmpty(result.getCheckPostAndGrade())) { + return; + } + for (V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVoExt : result.getCheckPostAndGrade()) { + this.selfProxy.dealPositionIdentityStudyItem(userPrimaryBoundOne, identityId, v2CultivatePostAndGradeVoExt, tenantId); + } + + + //判断学习地图是否完成并记录 + checkPromotionComplete(result.getList(), result.getPromotionPostMap(), userId, tenantId); + } + + private void recodePositionIdentityLog(String userId, String identityId, CultivateMqDTO message) { + + Map params = message.getParams(); + if (params == null) { + return; + } + String userIdentifyId = (String) params.get("userIdentifyId"); + Integer userIdentifyStatus = (Integer) params.get("userIdentifyStatus"); + Integer status = message.getStatus(); + ftbCultivatePositionLogService.recodePositionCourseIdentify(userId, identityId, userIdentifyId, status, userIdentifyStatus); + } + + /** + * 岗位技能学习 + * + * @param message 培训信息 + */ + @Override + public void dealPositionPracticeStudy(CultivateMqDTO message) { + + String userId = message.getUserId(); + String practiceId = message.getBusinessId(); + String tenantId = message.getTenantId(); + + if (StringUtils.isEmpty(practiceId) || StringUtils.isEmpty(userId)) { + log.error("dealPositionExamStudy参数不完整,practiceId={}, userId={}", practiceId, userId); + return; + } + + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,practiceId={}, userId={}", practiceId, userId); + return; + } + + // 查询需要练习的岗位和职级 + CheckPostAndGradeResult result = buildCheckPostAndGradeList(userPrimaryBoundOne, userId); + if (CollUtil.isEmpty(result.getCheckPostAndGrade())) { + return; + } + for (V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVoExt : result.getCheckPostAndGrade()) { + this.selfProxy.dealPositionPracticeStudyItem(userPrimaryBoundOne, practiceId, v2CultivatePostAndGradeVoExt, tenantId); + } + + //判断学习地图是否完成并记录 + checkPromotionComplete(result.getList(), result.getPromotionPostMap(), userId, tenantId); + } + + /** + * 构建需要检查的岗位和职级列表 + * + * @param userPrimaryBoundOne 用户主要绑定信息 + * @param userId 用户 ID + * @return 检查结果,包含岗位职级列表、学习地图和岗位映射 + */ + private CheckPostAndGradeResult buildCheckPostAndGradeList(UserBoundVO userPrimaryBoundOne, String userId) { + // 查询所有的岗位学习 + List v2CultivatePostAndGradeVos = queryAllPositionAndGrade(); + if (CollUtil.isEmpty(v2CultivatePostAndGradeVos)) { + return new CheckPostAndGradeResult(new ArrayList<>(), null, null); + } + Map v2CultivatePostAndGradeVosMap = convertV2CultivatePostAndGradeVoMap(v2CultivatePostAndGradeVos); + List checkPostAndGrade = new ArrayList<>(); + + // 查询用户所在的学习地图 + List list = ftbCultivatePromotionUserService.queryMyAllPromotionList(userId); + Map> promotionPostMap = null; + + if (CollUtil.isNotEmpty(list)) { + List promotionIds = list.stream().map(MyCultivatePromotionListVo::getId).collect(Collectors.toList()); + + // 用户所在阶段 + List currentPhaseList = ftbCultivatePromotionMemberNewService.queryMyCurrentPhase(userId, promotionIds); + Map currentPhaseMap = new HashMap<>(); + if (CollUtil.isNotEmpty(currentPhaseList)) { + currentPhaseMap = currentPhaseList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + } + promotionPostMap = batchQueryPositionsByPromotionIds(promotionIds); + + for (MyCultivatePromotionListVo item : list) { + List promotionPostNews = promotionPostMap.get(item.getId()); + if (CollUtil.isEmpty(promotionPostNews)) { + continue; + } + + Integer currLevel; + BatchCommonCountDto currentPhase = currentPhaseMap.get(item.getId()); + if (currentPhase != null) { + currLevel = currentPhase.getNum(); + } else { + currLevel = 1; + } + + Map> currentPhasePostMap = groupAndSortByLevel(promotionPostNews); + + for (Map.Entry> entry : currentPhasePostMap.entrySet()) { + if (currLevel > entry.getKey()) { + break; + } + List postNewList = entry.getValue(); + + for (FtbCultivatePromotionPostNew postNew : postNewList) { + V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVo = v2CultivatePostAndGradeVosMap.get(postNew.getPostId() + "-" + postNew.getGradeId()); + if (v2CultivatePostAndGradeVo == null) { + continue; + } + checkPostAndGrade.add(v2CultivatePostAndGradeVo); + } + } + } + } + + // 添加用户当前岗位 + V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVo = queryUserSelfPosition(userPrimaryBoundOne); + if (v2CultivatePostAndGradeVo != null) { + checkPostAndGrade.add(v2CultivatePostAndGradeVo); + } + + return new CheckPostAndGradeResult(checkPostAndGrade, list, promotionPostMap); + } + + + private void checkPromotionComplete(List list, Map> promotionPostMap, String userId, String tenantId) { + if (CollUtil.isEmpty(list) || CollUtil.isEmpty(promotionPostMap)) { + return; + } + //查找已经完成的岗位学习 + List myCompletePositionList = ftbCultivatePositionLogService.queryMyCompletePosition(userId); + List memberList = ftbCultivatePromotionMemberNewService.queryMyAllSelectPosition(userId); + List completePromotionList = ftbCultivatePromotionLogService.queryMyCompletePromotion(userId, 2); + List completePromotionUserList = ftbCultivatePromotionUserService.queryMyCompletePromotion(userId); + List positionCourseLogEntityList = cultivatePositionCourseLogService.queryMyAllCompletePositionCourse(userId); + Map positionCourseLogEntityMap = new HashMap<>(); + if (CollUtil.isNotEmpty(positionCourseLogEntityList)) { + for (CultivatePositionCourseLogEntity courseLogEntity : positionCourseLogEntityList) { + if (StringUtils.isNotEmpty(courseLogEntity.getGradeId())) { + positionCourseLogEntityMap.put(courseLogEntity.getPostId() + "-" + courseLogEntity.getGradeId(), courseLogEntity); + } else { + positionCourseLogEntityMap.put(courseLogEntity.getPostId(), courseLogEntity); + } + } + } + if (CollUtil.isEmpty(memberList)) { + return; + } + List completePromotion = new ArrayList<>(); + for (FtbCultivatePromotionLog ftbCultivatePromotionLog : completePromotionList) { + completePromotion.add(ftbCultivatePromotionLog.getPromotionId() + "-" + ftbCultivatePromotionLog.getLevel()); + } + //按照 promotionId 分组 + Map> memberMap = memberList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionMemberNew::getPromotionId)); +// 按照岗位分组 + List completePostAndGrade = new ArrayList<>(); + for (FtbCultivatePositionLog ftbCultivatePositionLog : myCompletePositionList) { + if (StringUtils.isEmpty(ftbCultivatePositionLog.getGradeId())) { + completePostAndGrade.add(ftbCultivatePositionLog.getPostId()); + } else { + completePostAndGrade.add(ftbCultivatePositionLog.getPostId() + "-" + ftbCultivatePositionLog.getGradeId()); + } + + } + for (MyCultivatePromotionListVo promotionVo : list) { + if (completePromotionUserList.contains(promotionVo.getId())) { + continue; + } + List promotionPostNews = promotionPostMap.get(promotionVo.getId()); + if (CollUtil.isEmpty(promotionPostNews)) { + continue; + } + Map> currentPhasePostMap = groupAndSortByLevel(promotionPostNews); + Integer maxCompleteLevel = 0; + for (Map.Entry> entry : currentPhasePostMap.entrySet()) { + Integer level = entry.getKey(); + List postNewList = entry.getValue(); + if (CollUtil.isEmpty(postNewList)) { + continue; + } + List memberNewList = memberMap.get(promotionVo.getId()); + if (CollUtil.isEmpty(memberNewList)) { + continue; + } + if (completePromotion.contains(promotionVo.getId() + "-" + level)) { + maxCompleteLevel = level; + continue; + } + //memberNewList按照level分组 + Map> memberNewMap = memberNewList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionMemberNew::getCurrentLearningStage)); + + List currMemberLists = memberNewMap.get(level); + if (CollUtil.isEmpty(currMemberLists)) { + continue; + } + Map promotionPostNewMap = postNewList.stream().collect(Collectors.toMap(FtbCultivatePromotionPostNew::getId, v -> v)); + + boolean isCompleteLevel = true; + for (FtbCultivatePromotionMemberNew memberNew : currMemberLists) { + FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew = promotionPostNewMap.get(memberNew.getPromotionPostId()); + if (ftbCultivatePromotionPostNew == null) { + isCompleteLevel = false; + break; + } + String key = ftbCultivatePromotionPostNew.getPostId(); + if (StringUtils.isNotEmpty(ftbCultivatePromotionPostNew.getGradeId())) { + key = ftbCultivatePromotionPostNew.getPostId() + "-" + ftbCultivatePromotionPostNew.getGradeId(); + } + if (!completePostAndGrade.contains(key)) { + isCompleteLevel = false; + break; + } + if (promotionVo.getLearningMapSetting().equals(1)) { + CultivatePositionCourseLogEntity courseLogEntity = positionCourseLogEntityMap.get(key); + if (courseLogEntity != null) { + if (courseLogEntity.getHasExam().equals(2) && (courseLogEntity.getUserExamStatus() != 3 && courseLogEntity.getUserExamStatus() != 5)) { + isCompleteLevel = false; + break; + } + + if (courseLogEntity.getHasIdentity().equals(2) && (courseLogEntity.getUserIdentifyStatus() != 0 && courseLogEntity.getUserIdentifyStatus() != 1)) { + isCompleteLevel = false; + break; + } + } + + } else { + CultivatePositionCourseLogEntity courseLogEntity = positionCourseLogEntityMap.get(key); + if (courseLogEntity != null) { + if (courseLogEntity.getHasExam().equals(2) && (courseLogEntity.getUserExamStatus() != 3 && courseLogEntity.getUserExamStatus() != 4 && courseLogEntity.getUserExamStatus() != 5)) { + isCompleteLevel = false; + break; + } + + if (courseLogEntity.getHasIdentity().equals(2) && (courseLogEntity.getUserIdentifyStatus() == -1)) { + isCompleteLevel = false; + break; + } + } + + } + } + if (isCompleteLevel) { + //完成学习 + ftbCultivatePromotionLogService.completePromotionLevel(userId, promotionVo.getId(), level, tenantId); + maxCompleteLevel = level; + } + } + Set levels = currentPhasePostMap.keySet(); + if (maxCompleteLevel >= Collections.max(levels)) { + ftbCultivatePromotionUserService.completePromotion(userId, promotionVo.getId(), tenantId); + } + } + } + + + @Override + public void dealPositionIdentityStudyItem(UserBoundVO userBoundVO, String identityId, V2CultivatePostAndGradeVoExt itemVo, String tenantId) { + String userId = userBoundVO.getId(); + String positionLearnId = itemVo.getId(); + String postId = itemVo.getPostId(); + String gradeId = itemVo.getGradeId(); + FtbCultivatePositionSetting positionSetting = itemVo.getSetting(); + List courseList = ftbCultivatePositionCourseService.listByPostLearnIdAndIdentityId(positionLearnId, gradeId, identityId); + if (CollUtil.isEmpty(courseList)) { + log.error("dealPositionPracticeStudyItem,positionLearnId={},gradeId={}, identityId={}, userId={}", + positionLearnId, gradeId, identityId, userId); + return; + } + FtbCultivatePosition position = ftbCultivatePositionService.getById(positionLearnId); + if (position == null || position.getEnabledMark() == 1) { + log.error("dealRealPositionExamStudy未查询到岗位信息,positionLearnId={},postId={},userId={},", positionLearnId, postId, userId); + return; + } + List logList = queryCompletePositionList(userId, positionLearnId, postId, gradeId, position); + if (CollUtil.isNotEmpty(logList)) { + log.error("dealPositionIdentityStudyItem positionLearnId={},identityId={},userId={},postId={},gradeId={},用户已学习完成", + positionLearnId, identityId, userId, postId, gradeId); + return; + } + Map courseLearningMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, + courseList.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList())); + + for (FtbCultivatePositionCourse ftbCultivatePositionCourse : courseList) { + String courseId = ftbCultivatePositionCourse.getCourseId(); + if (!checkItemCourseIsComplete(courseLearningMap, courseId)) { + continue; + } + + // 查询考试信息 - 使用新封装的方法 + List exams = ftbCultivatePositionCourseExamService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List examIds = exams.stream().map(FtbCultivatePositionCourseExam::getExamId).distinct().collect(Collectors.toList()); + + // 查询鉴定信息 - 使用新封装的方法 + List identities = ftbCultivatePositionCourseIdentityService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List identityIds = identities.stream().map(FtbCultivatePositionCourseIdentity::getIdentityId).distinct().collect(Collectors.toList()); + + // 查询练习信息 - 使用新封装的方法 + List practices = ftbCultivatePositionCoursePracticeService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List practiceIds = practices.stream().map(FtbCultivatePositionCoursePractice::getBusinessId).distinct().collect(Collectors.toList()); + Map practiceMap = new HashMap<>(); + if (CollUtil.isNotEmpty(practices)) { + for (FtbCultivatePositionCoursePractice practice : practices) { + practiceMap.put(practice.getBusinessId(), practice); + } + } + // 查询证书信息 - 使用新封装的方法 + List certificates = ftbCultivatePositionCourseCertificateService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List certificateIds = certificates.stream().map(FtbCultivatePositionCourseCertificate::getCertificateId).distinct().collect(Collectors.toList()); + + // 批量查询用户的考试、鉴定、证书、技能完成情况 + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, identityIds, 2, positionLearnId); +// Map userCertificateStatusMap = v2CultivateBatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillStatusMap = v2CultivateBatchQueryService.batchQueryUserSkillStatus(userId, practiceIds); + + Integer qualified = 0; //试合格和练习满足才能完成鉴定【0-否 1-是】 + if (positionSetting != null) { + qualified = positionSetting.getQualified(); + } + + // 检查考试、技能和鉴定状态 + PositionStatusResultVo positionStatusResult = evaluatePositionStatus(userId, positionLearnId, examIds, practiceIds, identityIds, + qualified, userExamStatusMap, userIdentificationStatusMap, practiceMap, userSkillStatusMap, courseId, ftbCultivatePositionCourse.getGradeId(), tenantId); + boolean isExam = positionStatusResult.getIsExam(); + boolean isPractice = positionStatusResult.getIsPractice(); + boolean isIdentification = positionStatusResult.getIsIdentification(); + + handlePositionCourseCompletion(userId, positionLearnId, postId, gradeId, courseId, certificateIds, tenantId, + positionStatusResult); + } + + checkAndCompletePosition(userId, positionLearnId, postId, gradeId, tenantId); + + } + + @Override + public void dealPositionPracticeStudyItem(UserBoundVO userBoundVO, String practiceId, V2CultivatePostAndGradeVoExt itemVo, String tenantId) { + String userId = userBoundVO.getId(); + String positionLearnId = itemVo.getId(); + String postId = itemVo.getPostId(); + String gradeId = itemVo.getGradeId(); + FtbCultivatePositionSetting positionSetting = itemVo.getSetting(); + List courseList = ftbCultivatePositionCourseService.listByPostLearnIdAndPracticeId(positionLearnId, gradeId, practiceId); + if (CollUtil.isEmpty(courseList)) { + log.error("dealPositionPracticeStudyItem,positionLearnId={},gradeId={}, practiceId={}, userId={}", + positionLearnId, gradeId, practiceId, userId); + return; + } + FtbCultivatePosition position = ftbCultivatePositionService.getById(positionLearnId); + if (position == null || position.getEnabledMark() == 1) { + log.error("dealPositionPracticeStudyItem未查询到岗位信息,positionLearnId={},postId={},userId={},", positionLearnId, postId, userId); + return; + } + + List logList = queryCompletePositionList(userId, positionLearnId, postId, gradeId, position); + if (CollUtil.isNotEmpty(logList)) { + log.error("dealPositionPracticeStudyItem positionLearnId={},practiceId={},userId={},postId={},gradeId={},用户已学习完成", + positionLearnId, practiceId, userId, postId, gradeId); + return; + } + Map courseLearningMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, + courseList.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList())); + + checkAndDoPositionStudy(tenantId, userId, positionLearnId, postId, gradeId, positionSetting, courseList, courseLearningMap); + + } + + @Override + public void prePositionCourseStudy(String userId, String courseId, String postLearnId, String gradeId, String tenantId) { + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未 null,courseId={}, userId={}", courseId, userId); + return; + } + FtbCultivatePosition position = ftbCultivatePositionService.getById(postLearnId); + if (position == null) { + return; + } + FtbCultivatePositionSetting positionSetting; + + if (position.getIsConfiguredToGrade().equals(0)) { + positionSetting = ftbCultivatePositionSettingService.listByPostLearnId(postLearnId); + } else { + if (StringUtils.isEmpty(gradeId)) { + return; + } + positionSetting = ftbCultivatePositionSettingService.listByPostLearnIdAndGradeId(postLearnId, gradeId); + } + + List examList = ftbCultivatePositionCourseExamService.queryAllConfigExamId(List.of(postLearnId), courseId, gradeId); + if (CollUtil.isEmpty(examList)) { + return; + } + Integer qualified = 0; //试合格和练习满足才能完成鉴定【0-否 1-是】 + if (positionSetting != null) { + qualified = positionSetting.getQualified(); + } + + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examList.stream().map(V2AllCultivatePositionCourseExam::getExamId).collect(Collectors.toList())); + for (V2AllCultivatePositionCourseExam exam : examList) { + checkExamStatus(userId, exam.getPostLearnId(), List.of(exam.getExamId()), qualified, userExamStatusMap, courseId, exam.getGradeId(), 1, tenantId); + } + + } + + private void checkAndDoPositionStudy(String tenantId, String userId, String positionLearnId, String postId, String gradeId, FtbCultivatePositionSetting positionSetting, List courseList, Map courseLearningMap) { + for (FtbCultivatePositionCourse ftbCultivatePositionCourse : courseList) { + String courseId = ftbCultivatePositionCourse.getCourseId(); + if (!checkItemCourseIsComplete(courseLearningMap, courseId)) { + continue; + } + + // 查询考试信息 - 使用新封装的方法 + List exams = ftbCultivatePositionCourseExamService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List examIds = exams.stream().map(FtbCultivatePositionCourseExam::getExamId).distinct().collect(Collectors.toList()); + + // 查询鉴定信息 - 使用新封装的方法 + List identities = ftbCultivatePositionCourseIdentityService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List identityIds = identities.stream().map(FtbCultivatePositionCourseIdentity::getIdentityId).distinct().collect(Collectors.toList()); + + // 查询练习信息 - 使用新封装的方法 + List practices = ftbCultivatePositionCoursePracticeService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List practiceIds = practices.stream().map(FtbCultivatePositionCoursePractice::getBusinessId).distinct().collect(Collectors.toList()); + Map practiceMap = new HashMap<>(); + if (CollUtil.isNotEmpty(practices)) { + for (FtbCultivatePositionCoursePractice practice : practices) { + practiceMap.put(practice.getBusinessId(), practice); + } + } + // 查询证书信息 - 使用新封装的方法 + List certificates = ftbCultivatePositionCourseCertificateService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List certificateIds = certificates.stream().map(FtbCultivatePositionCourseCertificate::getCertificateId).distinct().collect(Collectors.toList()); + + // 批量查询用户的考试、鉴定、证书、技能完成情况 + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, identityIds, 2, ftbCultivatePositionCourse.getPostLearnId()); +// Map userCertificateStatusMap = v2CultivateBatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillStatusMap = v2CultivateBatchQueryService.batchQueryUserSkillStatus(userId, practiceIds); + + Integer qualified = 0; //试合格和练习满足才能完成鉴定【0-否 1-是】 + if (positionSetting != null) { + qualified = positionSetting.getQualified(); + } + + // 检查考试、技能和鉴定状态 + PositionStatusResultVo positionStatusResult = evaluatePositionStatus(userId, positionLearnId, examIds, practiceIds, identityIds, + qualified, userExamStatusMap, userIdentificationStatusMap, practiceMap, userSkillStatusMap, courseId, gradeId, tenantId); + + handlePositionCourseCompletion(userId, positionLearnId, postId, gradeId, courseId, certificateIds, tenantId, + positionStatusResult); + } + checkAndCompletePosition(userId, positionLearnId, postId, gradeId, tenantId); + } + + + @Transactional + @Override + public void dealPositionExamStudyItem(UserBoundVO userBoundVO, String examId, V2CultivatePostAndGradeVoExt itemVo, String tenantId) { + String userId = userBoundVO.getId(); + String positionLearnId = itemVo.getId(); + String postId = itemVo.getPostId(); + String gradeId = itemVo.getGradeId(); + FtbCultivatePositionSetting positionSetting = itemVo.getSetting(); + List courseList = ftbCultivatePositionCourseService.listByPostLearnIdAndExamId(positionLearnId, gradeId, examId); + if (CollUtil.isEmpty(courseList)) { + log.error("dealRealPositionExamStudy未查询到课程信息,positionLearnId={},gradeId={}, examId={}, userId={}", + positionLearnId, gradeId, examId, userId); + return; + } + FtbCultivatePosition position = ftbCultivatePositionService.getById(positionLearnId); + if (position == null || position.getEnabledMark() == 1) { + log.error("dealRealPositionExamStudy未查询到岗位信息,positionLearnId={},postId={},userId={},", positionLearnId, postId, userId); + return; + } + + List logList = queryCompletePositionList(userId, positionLearnId, postId, gradeId, position); + if (CollUtil.isNotEmpty(logList)) { + log.error("dealPositionExamStudyItem positionLearnId={},examId={},userId={},postId={},gradeId={},用户已学习完成", + positionLearnId, examId, userId, postId, gradeId); + return; + } + Map courseLearningMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, + courseList.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList())); + + checkAndDoPositionStudy(tenantId, userId, positionLearnId, postId, gradeId, positionSetting, courseList, courseLearningMap); + + } + + private List queryCompletePositionList(String userId, String positionLearnId, String postId, String gradeId, FtbCultivatePosition position) { + // 构建查询条件 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePositionLog::getPostLearnId, positionLearnId) + .eq(FtbCultivatePositionLog::getPostId, postId) + .eq(Objects.equals(position.getIsConfiguredToGrade(), 1), FtbCultivatePositionLog::getGradeId, gradeId) + .eq(FtbCultivatePositionLog::getUserId, userId) + .eq(FtbCultivatePositionLog::getEnabledMark, 0); // 0-正常 1-删除 + + List list = ftbCultivatePositionLogService.list(queryWrapper); + + // 如果没有找到记录,则创建一条新记录(状态:0-未完成) + if (CollUtil.isEmpty(list)) { + FtbCultivatePositionLog log = new FtbCultivatePositionLog(); + log.setPostLearnId(positionLearnId); + log.setPostId(postId); + if (Objects.equals(position.getIsConfiguredToGrade(), 1)) { + log.setGradeId(gradeId); + } + log.setUserId(userId); + log.setState(0); // 0-未完成 1-进行中 2-已完成 + log.setEnabledMark(0); // 0-正常 + ftbCultivatePositionLogService.save(log); + return null; + } else { + // 如果第一条记录状态为已完成(2),则返回列表 + if (list.get(0).getState().equals(2)) { + return list; + } + } + + return null; + } + + + @Transactional + @Override + public void dealPositionReCheckStudyItem(UserBoundVO userBoundVO, V2CultivatePostAndGradeVoExt itemVo, String tenantId) { + String userId = userBoundVO.getId(); + String positionLearnId = itemVo.getId(); + String postId = itemVo.getPostId(); + String gradeId = itemVo.getGradeId(); + FtbCultivatePositionSetting positionSetting = itemVo.getSetting(); + List courseList = ftbCultivatePositionCourseService.queryAllPositionCourseAndLearnIdAndGradeId(positionLearnId, gradeId); + if (CollUtil.isEmpty(courseList)) { + log.error("dealPositionReCheckStudyItem未查询到课程信息,positionLearnId={},gradeId={}, userId={}", + positionLearnId, gradeId, userId); + return; + } + Map courseLearningMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, + courseList.stream().map(FtbCultivatePositionCourse::getCourseId).collect(Collectors.toList())); + for (FtbCultivatePositionCourse ftbCultivatePositionCourse : courseList) { + String courseId = ftbCultivatePositionCourse.getCourseId(); + //课程是否完成 + if (!checkItemCourseIsComplete(courseLearningMap, courseId)) { + continue; + } + // 查询考试信息 - 使用新封装的方法 + List exams = ftbCultivatePositionCourseExamService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List examIds = exams.stream().map(FtbCultivatePositionCourseExam::getExamId).distinct().collect(Collectors.toList()); + + // 查询鉴定信息 - 使用新封装的方法 + List identities = ftbCultivatePositionCourseIdentityService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId), gradeId); + List identityIds = identities.stream().map(FtbCultivatePositionCourseIdentity::getIdentityId).distinct().collect(Collectors.toList()); + + // 查询练习信息 - 使用新封装的方法 + List practices = ftbCultivatePositionCoursePracticeService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List practiceIds = practices.stream().map(FtbCultivatePositionCoursePractice::getBusinessId).distinct().collect(Collectors.toList()); + Map practiceMap = new HashMap<>(); + if (CollUtil.isNotEmpty(practices)) { + for (FtbCultivatePositionCoursePractice practice : practices) { + practiceMap.put(practice.getBusinessId(), practice); + } + } + // 查询证书信息 - 使用新封装的方法 + List certificates = ftbCultivatePositionCourseCertificateService.listByPostLearnIdAndCourseIds(positionLearnId, List.of(courseId)); + List certificateIds = certificates.stream().map(FtbCultivatePositionCourseCertificate::getCertificateId).distinct().collect(Collectors.toList()); + + // 批量查询用户的考试、鉴定、证书、技能完成情况 + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, identityIds, 2, ftbCultivatePositionCourse.getPostLearnId()); +// Map userCertificateStatusMap = v2CultivateBatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillStatusMap = v2CultivateBatchQueryService.batchQueryUserSkillStatus(userId, practiceIds); + + Integer qualified = 0; //试合格和练习满足才能完成鉴定【0-否 1-是】 + if (positionSetting != null) { + qualified = positionSetting.getQualified(); + } + + // 检查考试、技能和鉴定状态 + PositionStatusResultVo positionStatusResult = evaluatePositionStatus(userId, positionLearnId, examIds, practiceIds, identityIds, + qualified, userExamStatusMap, userIdentificationStatusMap, practiceMap, userSkillStatusMap, courseId, ftbCultivatePositionCourse.getGradeId(), tenantId); + + handlePositionCourseCompletion(userId, positionLearnId, postId, gradeId, courseId, certificateIds, tenantId, + positionStatusResult); + } + + } + + private V2CultivatePostAndGradeVoExt queryUserSelfPosition(UserBoundVO userPrimaryBoundOne) { + List cultivatePositionList = ftbCultivatePositionService.list( + Wrappers.lambdaQuery() + .eq(FtbCultivatePosition::getPostId, userPrimaryBoundOne.getPositionId()) + .eq(FtbCultivatePosition::getIsGrounding, 1) + .eq(FtbCultivatePosition::getEnabledMark, 0)); + + if (CollUtil.isEmpty(cultivatePositionList)) { + return null; + } + + + FtbCultivatePosition cultivatePosition = cultivatePositionList.get(0); + + LambdaQueryWrapper settingWrapper = Wrappers.lambdaQuery(); + settingWrapper.eq(FtbCultivatePositionSetting::getPostLearnId, cultivatePosition.getId()); + List settingList = ftbCultivatePositionSettingService.list(settingWrapper); + + if (cultivatePosition.getIsConfiguredToGrade().equals(0))//未分配到职级 + { + V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVo = new V2CultivatePostAndGradeVoExt(); + v2CultivatePostAndGradeVo.setPostId(cultivatePosition.getPostId()); + v2CultivatePostAndGradeVo.setId(cultivatePosition.getId()); + v2CultivatePostAndGradeVo.setGradeId(""); + if (CollUtil.isNotEmpty(settingList)) { + v2CultivatePostAndGradeVo.setSetting(settingList.get(0)); + } + return v2CultivatePostAndGradeVo; + } else { + + if (StringUtils.isEmpty(userPrimaryBoundOne.getGradeId())) { + return null; + } + for (FtbCultivatePositionSetting ftbCultivatePositionSetting : settingList) { + if (ftbCultivatePositionSetting.getGradeId().equals(userPrimaryBoundOne.getGradeId())) { + V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVo = new V2CultivatePostAndGradeVoExt(); + v2CultivatePostAndGradeVo.setPostId(cultivatePosition.getPostId()); + v2CultivatePostAndGradeVo.setId(cultivatePosition.getId()); + v2CultivatePostAndGradeVo.setGradeId(ftbCultivatePositionSetting.getGradeId()); + v2CultivatePostAndGradeVo.setSetting(ftbCultivatePositionSetting); + return v2CultivatePostAndGradeVo; + } + } + return null; + } + } + + private Map convertV2CultivatePostAndGradeVoMap(List v2CultivatePostAndGradeVos) { + if (CollUtil.isEmpty(v2CultivatePostAndGradeVos)) { + return new HashMap<>(); + } + Map ret = new HashMap<>(); + for (V2CultivatePostAndGradeVoExt v2CultivatePostAndGradeVo : v2CultivatePostAndGradeVos) { + ret.put(v2CultivatePostAndGradeVo.getPostId() + "-" + v2CultivatePostAndGradeVo.getGradeId(), v2CultivatePostAndGradeVo); + } + return ret; + } + + private List queryAllPositionAndGrade() { + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getIsGrounding, 1); + positionWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); // 仅查询有效的岗位 + List positionList = ftbCultivatePositionService.list(positionWrapper); + + if (CollUtil.isEmpty(positionList)) { + return List.of(); + } + + // 提取ID列表并在一次遍历中完成 + List learnIds = new ArrayList<>(positionList.size()); + for (FtbCultivatePosition ftbCultivatePosition : positionList) { + learnIds.add(ftbCultivatePosition.getId()); + } + + // 获取所有岗位对应的职级配置 + LambdaQueryWrapper settingWrapper = Wrappers.lambdaQuery(); + settingWrapper.in(FtbCultivatePositionSetting::getPostLearnId, learnIds); + List settingList = ftbCultivatePositionSettingService.list(settingWrapper); + + // 构建岗位学习ID到职级列表的映射 + Map> positionSettingMap = settingList.stream() + .collect(Collectors.groupingBy( + FtbCultivatePositionSetting::getPostLearnId, + Collectors.toList() + )); + List ret = new ArrayList<>(); + for (FtbCultivatePosition ftbCultivatePosition : positionList) { + List positionSettings = positionSettingMap.get(ftbCultivatePosition.getId()); + if (CollUtil.isEmpty(positionSettings)) { + V2CultivatePostAndGradeVoExt postAndGradeVo = new V2CultivatePostAndGradeVoExt(); + postAndGradeVo.setId(ftbCultivatePosition.getId()); + postAndGradeVo.setPostId(ftbCultivatePosition.getPostId()); + postAndGradeVo.setGradeId(""); + postAndGradeVo.setSetting(null); + ret.add(postAndGradeVo); + } else { + for (FtbCultivatePositionSetting positionSetting : positionSettings) { + V2CultivatePostAndGradeVoExt postAndGradeVo = new V2CultivatePostAndGradeVoExt(); + postAndGradeVo.setId(ftbCultivatePosition.getId()); + postAndGradeVo.setPostId(ftbCultivatePosition.getPostId()); + postAndGradeVo.setGradeId(positionSetting.getGradeId()); + postAndGradeVo.setSetting(positionSetting); + ret.add(postAndGradeVo); + } + } + } + return ret; + } + + + /** + * 按照level字段对List进行分组并分别排序(升序) + * + * @param list 待处理的FtbCultivatePromotionPostNew列表 + * @return 按level分组后每组内按升序排列的Map + */ + public Map> groupAndSortByLevel(List list) { + if (list == null || list.isEmpty()) { + return new HashMap<>(); + } + + return list.stream() + .collect(Collectors.groupingBy( + FtbCultivatePromotionPostNew::getLevel, + Collectors.collectingAndThen( + Collectors.toList(), + subList -> { + subList.sort(Comparator.comparingInt(FtbCultivatePromotionPostNew::getLevel)); + return subList; + } + ) + )); + } + + + /** + * 根据学习地图id批量查询地图中配置的岗位并按照学习地图id分组 + * + * @param promotionIds 学习地图id列表 + * @return 以学习地图id为key,岗位列表为value的Map + */ + public Map> batchQueryPositionsByPromotionIds(List promotionIds) { + if (CollUtil.isEmpty(promotionIds)) { + return new HashMap<>(); + } + + // 构建查询条件,查询属于这些学习地图的所有岗位 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbCultivatePromotionPostNew::getPromotionId, promotionIds) + .eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); + + List positionList = ftbCultivatePromotionPostNewService.list(queryWrapper); + + // 按照学习地图id进行分组 + return positionList.stream() + .collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getPromotionId)); + } + + + /** + * 删除申请详情 + * + * @param appId 鉴定id + */ + private void deleteApplyDetails(String appId) { + applyDetailsService.update(new LambdaUpdateWrapper().set(CultivateIdentifyApplyDetails::getDeleteMark, 1).eq(CultivateIdentifyApplyDetails::getApplyId, appId).eq(CultivateIdentifyApplyDetails::getDeleteMark, 0)); + } + + /** + * 触发考试 + * + * @param userId 用户id + * @param examId 考试id + * @param source 来源 + * @param sourceId 来源id + */ + public FtbCultivateExamUser triggerExam(String userId, String examId, Integer source, String sourceId, String tenantId) { + log.error("triggerExam userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + CultivateExam ftbCultivateExam = v2CultivateExamMapper.selectById(examId); + if (ftbCultivateExam == null) { + log.error("triggerExam 考试不存在在 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + return null; + } + + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper().eq(FtbCultivateExamUser::getUserId, userId).eq(FtbCultivateExamUser::getExamId, examId).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()).orderByDesc(FtbCultivateExamUser::getCreatorTime); + List examUserList = ftbCultivateExamUserMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(examUserList)) { + log.error("triggerExam 考试已经完成或存在 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + return examUserList.get(0); + } + +// LambdaQueryWrapper wrapperNoComplete = new LambdaQueryWrapper().eq(FtbCultivateExamUser::getUserId, userId).eq(FtbCultivateExamUser::getExamId, examId).in(FtbCultivateExamUser::getStatus, 0, 2).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); +// List examUserListComplete = ftbCultivateExamUserMapper.selectList(wrapperNoComplete); +// if (CollUtil.isNotEmpty(examUserListComplete)) { +// log.error("triggerExam 考试已经存在 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); +// return; +// } + + + List ruleList = examDrawRuleService.list(new LambdaQueryWrapper().eq(CultivateExamDrawRule::getExamId, ftbCultivateExam.getId())); + + + //2、没有就写入一条记录 + FtbCultivateExamUser user = FtbCultivateExamUser.builder() + .examId(examId) + .examType(ftbCultivateExam.getExamType()) + .userId(userId) + .status(CourseEnums.ExamStatus.WAIT.getCode()) + .examSource(source) + .enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()) + .batch(UUID.randomUUID().toString().replaceAll("-", "")) + .userExamCount(1).build(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne != null && userPrimaryBoundOne.getOrganizeId() != null) { + user.setUserOrgList(userPrimaryBoundOne.getOrganizeId()); + } + if (CollUtil.isNotEmpty(ruleList)) { + user.setPickQuestionRule(JSON.toJSONString(ruleList)); + int totalScore = 0; + int questionNumber = 0; + for (CultivateExamDrawRule cultivateExamDrawRule : ruleList) { + totalScore += cultivateExamDrawRule.getScore() * cultivateExamDrawRule.getPickCount(); + questionNumber += cultivateExamDrawRule.getPickCount(); + } + user.setTotalScore(totalScore); + user.setQuestionNumber(questionNumber); + user.setPassScore(QuestionAnalysisUtil.calPassScore(totalScore, ftbCultivateExam)); + user.setExcellentScore(QuestionAnalysisUtil.calExcellentScore(totalScore, ftbCultivateExam)); + user.setExamTime(ftbCultivateExam.getExamTime()); + } + if (source.equals(2)) { + user.setRelationPositionId(sourceId); + } else if (source.equals(4)) { + user.setRelationTaskId(sourceId); + } + ftbCultivateExamUserMapper.insert(user); + log.error("triggerExam 生成考试记录 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + return user; + } + + /** + * 触发鉴定 + * + * @param userId 用户id + * @param identifyId 鉴定id + * @param source 鉴定来源(0手动发起,1课程学习鉴定,2岗位学习鉴定,3本人申请,4任务鉴定) + * @param sourceId 鉴定来源id + */ + public CultivateIdentifyApply triggerIdentification(String userId, String identifyId, Integer source, String sourceId, String tenantId) { + log.info("触发鉴定 userId={}, identifyId={}, source={}, sourceId ={}", userId, identifyId, source, sourceId); + CultivateIdentifyTable table = identifyTableService.getById(identifyId); + if (table == null) { + log.error("鉴定表不存在 userId={}, identifyId={}, source={}, sourceId ={}", userId, identifyId, source, sourceId); + return null; + } + + List identifyItemsList = this.identifyItemsService.lambdaQuery().eq(CultivateIdentifyItems::getTableId, table.getId()).eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE).list(); + if (CollUtil.isEmpty(identifyItemsList)) { + log.error("鉴定表项不存在 userId={}, identifyId={}, source={}, sourceId ={}", userId, identifyId, source, sourceId); + } + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userBoundVO == null) { + return null; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CultivateIdentifyApply::getSourceId, sourceId); + wrapper.eq(CultivateIdentifyApply::getSource, source); + wrapper.eq(CultivateIdentifyApply::getOriginalTableId, table.getId()); + wrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + wrapper.eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + wrapper.orderByDesc(CultivateIdentifyApply::getCreatorTime); + List oldApplyLists = identifyApplyService.list(wrapper); + + if (CollUtil.isNotEmpty(oldApplyLists)) { + CultivateIdentifyApply oldApply = oldApplyLists.get(0); + if (oldApply.getStatus().equals(1) || oldApply.getStatus().equals(0)) { + return oldApply; + } + oldApply.setDeleteMark(1); + deleteApplyDetails(oldApply.getId()); + identifyApplyService.updateById(oldApply); + } + CultivateIdentifyApply apply = new CultivateIdentifyApply(); + apply.setId(UUID.randomUUID().toString()); + apply.setIdentifyUserId(""); + apply.setIdentifyOrgList(""); + apply.setSourceId(sourceId); + apply.setSource(source); + apply.setTableId(table.getId()); + apply.setOriginalTableId(table.getId()); + apply.setName(table.getName()); + apply.setBeIdentifyUserId(userId); + apply.setStatus(ApplyStatusEnum.DJD.getCode()); + apply.setAppraisalResults(ConstantUtil.NUM_FALSE); + apply.setUseTime(0); + apply.setIsVisible(0); + + //设置被鉴定人信息 + apply.setIsChoose(ConstantUtil.NUM_FALSE); + apply.setStudyFinishTime(new Date()); + apply.setIsReIdentify(ConstantUtil.NUM_FALSE); + apply.setDeleteMark(0); + // 岗位学习和课程实操鉴定,不进行直属主管判定,因为直属主管需要组织、岗位、职等进行确定 + + //被鉴定人组织信息 + List beIdentifyUserInfoJsonList = CollUtil.newArrayList(); + UserOrgInfoAll1Vo all1Vo = new UserOrgInfoAll1Vo(); + all1Vo.setUserOrgCode(userBoundVO.getOrganizeEnCode()); + all1Vo.setUserOrgName(userBoundVO.getOrganizeName()); + all1Vo.setUserPostCode(userBoundVO.getPositionEnCode()); + all1Vo.setUserPostName(userBoundVO.getPositionName()); + all1Vo.setUserOfficialRankCode(userBoundVO.getGradeId()); + all1Vo.setUserOfficialRankName(userBoundVO.getGradeName()); + beIdentifyUserInfoJsonList.add(all1Vo); + + apply.setBeIdentifyOrgList(userBoundVO.getOrganizeId()); + apply.setBeOfficialRankInfoJson(JSON.toJSONString(beIdentifyUserInfoJsonList)); + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + apply.setTableId(applyTableBackups.getId()); + + List applyDetailsBackupsList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + applyDetailsBackups.setDeleteMark(0); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List applyDetailsList = CollUtil.newArrayList(); + + applyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(apply.getId()); + applyDetails.setItemsId(item.getId()); + applyDetails.setDeleteMark(0); + return applyDetails; + }).collect(Collectors.toList())); + //保存备份数据 + applyTableBackupsService.save(applyTableBackups); + if (CollUtil.isNotEmpty(applyDetailsBackupsList)) { + applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + + if (CollUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.saveBatch(applyDetailsList); + } + identifyApplyService.save(apply); + return apply; + } + + /** + * 鉴定表项是否触发证书颁发 + * + * @param userId 用户id + * @param certificateId 证书id + * @param positionLearnId 岗位学习id + * @param postId 岗位id + * @param gradeId 职级id + * @param courseId 课程id + * @return true-已经触发过了 false-未触发 + */ + public Boolean checkIsTriggerPositionCertificate(String userId, String certificateId, String positionLearnId, String postId, String gradeId, String courseId) { + //查询是否颁发 + LambdaQueryWrapper certificateResultLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getUserId, userId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCertificateId, certificateId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getPositionLearnId, positionLearnId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getPostRankId, postId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getGradeId, gradeId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCourseId, courseId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getType, 1);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + return ftbCultivateCertificateResultMapper.selectCount(certificateResultLambdaQueryWrapper) > 0; + + } + + /** + * 记录触发的证书 + * + * @param userId 用户id + * @param certificateId 证书id + * @param positionLearnId 岗位学习id + * @param postId 岗位id + * @param gradeId 职级id + * @param courseId 课程id + */ + private void recordTriggerPositionCertificate(String userId, String certificateId, String positionLearnId, String postId, String gradeId, String courseId) { + FtbCultivateCertificateResult cultivateCertificateResult = new FtbCultivateCertificateResult(); + cultivateCertificateResult.setUserId(userId); + cultivateCertificateResult.setCertificateId(certificateId); + cultivateCertificateResult.setPositionLearnId(positionLearnId); + cultivateCertificateResult.setPostRankId(postId); + cultivateCertificateResult.setGradeId(gradeId); + cultivateCertificateResult.setCourseId(courseId); + cultivateCertificateResult.setType(1);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + ftbCultivateCertificateResultMapper.insert(cultivateCertificateResult); + } + + /** + * 触发证书 + * + * @param userId 用户id + * @param certificateId 证书id + * @param tenantId 租户id + */ + public void triggerCertificate(String userId, String certificateId, String tenantId) { + v2CultivateCertificateService.triggerCertificate(userId, certificateId, tenantId); + } + + + /** + * 检查课程是否完成 + * + * @param courseList 课程列表 + * @param userId 用户ID + * @return false-未完成 true-完成 + */ + private Boolean checkCourseIsComplete(List courseList, String userId) { + Map map = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, courseList); + if (CollUtil.isEmpty(map)) { + return false; + } + for (String courseId : courseList) { + jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning courceLearning = map.get(courseId); + if (courceLearning == null || courceLearning.getState() != 1) { + return false; + } + } + //记录 + return true; + } + + /** + * 检查单个课程是否完成 + * + * @param courseLearningMap 学习列表 + * @param courseId 课程id + * @return false-未完成 true-完成 + */ + private Boolean checkItemCourseIsComplete(Map courseLearningMap, String courseId) { + jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning courseLearning = courseLearningMap.get(courseId); + if (courseLearning == null || courseLearning.getState() == null) { + return false; + } + return courseLearning.getState() == 1; + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePromotionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePromotionServiceImpl.java new file mode 100644 index 0000000..8ae33cf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivatePromotionServiceImpl.java @@ -0,0 +1,1548 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.service.FtbCultivatePositionService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.*; +import jnpf.cultivate.v2.util.CultivateCourseStudyUtil; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.entiy.BaseEntity; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.position.FtbCultivatePositionLog; +import jnpf.model.cultivate.po.position.FtbCultivatePositionSetting; +import jnpf.model.cultivate.po.promotion.*; +import jnpf.model.cultivate.v2.position.vo.AppCultivatePositionDetailVo; +import jnpf.model.cultivate.v2.position.vo.V2CultivatePositionDetailForApp; +import jnpf.model.cultivate.v2.promotion.dto.PersonStatisticsDataDto; +import jnpf.model.cultivate.v2.promotion.req.*; +import jnpf.model.cultivate.v2.promotion.vo.*; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.ThreadContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class V2CultivatePromotionServiceImpl implements V2CultivatePromotionService { + + @Autowired + private FtbCultivatePromotionPostNewService ftbCultivatePromotionPostNewService; + + @Autowired + private FtbCultivatePromotionMemberNewService ftbCultivatePromotionMemberNewService; + + @Autowired + private FtbCultivatePromotionNewService ftbCultivatePromotionNewService; + + @Autowired + private FtbCultivatePromotionSettingService ftbCultivatePromotionSettingService; + + + @Autowired + private FtbCultivatePromotionNewMessageService ftbCultivatePromotionNewMessageService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + @Autowired + private FtbCultivatePromotionUserService ftbCultivatePromotionUserService; + + @Autowired + private FtbCultivatePositionService ftbCultivatePositionService; + + @Autowired + private FtbCultivatePositionSettingService ftbCultivatePositionSettingService; + + @Autowired + private V2CultivatePositionService v2CultivatePositionService; + + @Autowired + private FtbCultivatePromotionLogService ftbCultivatePromotionLogService; + + @Autowired + private FtbCultivatePositionLogService ftbCultivatePositionLogService; + + @Autowired + private CultivateCourseStudyUtil courseStudyUtil; + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + /** + * 查询学习地图列表 + * + * @param cultivatePage 分页参数对象,用于封装分页信息 + * @param dto 查询条件对象,用于传递查询参数 + * @return 返回分页结果对象,包含学习地图列表数据 + */ + @Override + public PageListVO webList(CultivatePage cultivatePage, FtbCultivatePromotionReq dto) { + // 将分页参数转换为Page对象 + Page page = cultivatePage.coverCultivatePage(); + // 调用服务层方法查询学习地图列表 + page = ftbCultivatePromotionNewService.webList(page, dto); + // 将Page对象转换为PageListVO对象并返回 + return CultivatePage.coverPageList(page); + } + + /** + * 新增学习地图 + * + * @param req 创建学习地图的请求参数对象 + * @return 返回操作结果字符串,通常为成功或失败信息 + */ + @Override + @Transactional + public String add(V2CultivatePromotionCreateReq req) { + // 检查标签名称是否重复 + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(FtbCultivatePromotionNew::getPromotion, req.getPromotion()); + checkWrapper.eq(FtbCultivatePromotionNew::getEnableMark, 0); + if (ftbCultivatePromotionNewService.count(checkWrapper) > 0) { + throw new RuntimeException("地图已存在,请勿重复添加!"); + } + // 对传入参数进行前置校验 + req.preParamCheck(); + // 执行实际的学习地图创建逻辑 + return realDealPromotion(req); + } + + + /** + * 实际执行学习地图创建逻辑 + * + * @param req 创建学习地图的请求参数对象 + * @return 创建结果字符串,通常为成功或失败信息 + */ + private String realDealPromotion(V2CultivatePromotionCreateReq req) { + // 添加学习地图主表 + FtbCultivatePromotionNew promotionNew = V2CultivatePromotionCreateReq.coverFtbCultivatePromotionCreatDto(req, UserProvider.getLoginUserId()); + // 设置默认启用状态 + if (promotionNew.getEnableMark() == null) { + promotionNew.setEnableMark(0); // 0表示启用,1表示禁用 + } + if (StringUtils.isNotEmpty(req.getId())) { + ftbCultivatePromotionNewService.updateById(promotionNew); + } else { + promotionNew.setPromotionBusinessId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.PROMOTION)); + ftbCultivatePromotionNewService.save(promotionNew); + } + + + String promotionId = promotionNew.getId(); // 获取插入后的主键ID + // 添加学习地图通道 + List promotionPostNewList = new java.util.ArrayList<>(); + for (V2CultivatePromotionPostReq postReq : req.getPhaseChannel()) { + if (postReq.getPostAndGradeVoList() != null && !postReq.getPostAndGradeVoList().isEmpty()) { + for (V2CultivatePostAndGradeVo postGradeVo : postReq.getPostAndGradeVoList()) { + promotionPostNewList.add(buildFtbCultivatePromotionPostNew(postReq, postGradeVo, promotionId)); + } + } + } + ftbCultivatePromotionPostNewService.saveBatch(promotionPostNewList); + // 添加参加学习地图的人员和岗位 + List settingList = new ArrayList<>(); + for (V2CultivatePromotionScopeReq v2CultivatePromotionScopeReq : req.getScope()) { + settingList.add(buildFtbCultivatePromotionSetting(v2CultivatePromotionScopeReq, promotionId)); + } + ftbCultivatePromotionSettingService.saveBatch(settingList); + return promotionId; + } + + @NotNull + private static FtbCultivatePromotionSetting buildFtbCultivatePromotionSetting(V2CultivatePromotionScopeReq v2CultivatePromotionScopeReq, String promotionId) { + FtbCultivatePromotionSetting setting = new FtbCultivatePromotionSetting(); + setting.setPromotionId(promotionId); + setting.setScope(v2CultivatePromotionScopeReq.getScope()); + setting.setPostId(v2CultivatePromotionScopeReq.getPostId()); + setting.setScopeId(v2CultivatePromotionScopeReq.getScopeId()); + return setting; + } + + @NotNull + private static FtbCultivatePromotionPostNew buildFtbCultivatePromotionPostNew(V2CultivatePromotionPostReq postReq, V2CultivatePostAndGradeVo postGradeVo, String promotionId) { + FtbCultivatePromotionPostNew promotionPostNew = new FtbCultivatePromotionPostNew(); + promotionPostNew.setPromotionId(promotionId); + promotionPostNew.setLevel(postReq.getChannelLevel()); // 使用通道级别作为level + promotionPostNew.setSelectCourseNumber(postReq.getSelectCourseNumber()); + promotionPostNew.setPostId(postGradeVo.getPostId()); + promotionPostNew.setPostName(postGradeVo.getPostName()); + promotionPostNew.setGradeId(postGradeVo.getGradeId()); + promotionPostNew.setGradeName(postGradeVo.getGradeName()); + promotionPostNew.setDeleteMark(0); + return promotionPostNew; + } + + + /** + * 修改学习地图 + * + * @param creatDto 修改学习地图的请求参数对象 + * @return 创建结果字符串,通常为成功或失败信息 + */ + @Override + @Transactional + public String update(V2CultivatePromotionCreateReq creatDto) { + FtbCultivatePromotionNew promotionNewEntity = ftbCultivatePromotionNewService.getById(creatDto.getId()); + if (promotionNewEntity == null || promotionNewEntity.getEnableMark() == 1) { + throw new RuntimeException("该学习地图不存在"); + } + + // 检查标签名称是否重复 + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(FtbCultivatePromotionNew::getPromotion, creatDto.getPromotion()).eq(FtbCultivatePromotionNew::getEnableMark, 0).ne(FtbCultivatePromotionNew::getId, creatDto.getId()); + if (ftbCultivatePromotionNewService.count(checkWrapper) > 0) { + throw new RuntimeException("地图名称已存在!"); + } + + // 参数检查 + creatDto.preParamCheck(); + String promotionId = creatDto.getId(); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId); + List memberNews = ftbCultivatePromotionMemberNewService.list(lambdaQuery); + if (CollUtil.isNotEmpty(memberNews)) { + List collect = memberNews.stream().map(item -> { + FtbCultivatePromotionNewMessage message = new FtbCultivatePromotionNewMessage(); + message.setPromotionId(promotionId); + message.setUserId(item.getUserId()); + return message; + }).collect(Collectors.toList()); + ftbCultivatePromotionNewMessageService.saveBatch(collect); + } + realDeletePromotion(promotionId); + return realDealPromotion(creatDto); + } + + /** + * 删除学习地图 + * + * @param id 要删除的学学习地图的ID + */ + @Override + @Transactional + public void delete(String id) { + realDeletePromotion(id); + } + + /** + * 根据ID获取学习地图信息 + * + * @param id 要获取的学学习地图的ID + * @return 返回一个包含学习地图信息的对象 + */ + private void realDeletePromotion(String id) { + if (id == null || id.trim().isEmpty()) { + throw new IllegalArgumentException("学习地图ID不能为空"); + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(FtbCultivatePromotionNew::getEnableMark, "1"); + updateWrapper.eq(BaseEntity::getId, id); + ftbCultivatePromotionNewService.update(new FtbCultivatePromotionNew(), updateWrapper); + + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + // 移除晋升通道岗位信息 + wrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, id); + ftbCultivatePromotionPostNewService.remove(wrapper); + // 移除关联数据 地图成员管理关联数据 + LambdaUpdateWrapper memberLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + memberLambdaUpdateWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, id); + ftbCultivatePromotionMemberNewService.remove(memberLambdaUpdateWrapper); + + //删除配置 + LambdaUpdateWrapper settingLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + settingLambdaUpdateWrapper.eq(FtbCultivatePromotionSetting::getPromotionId, id); + ftbCultivatePromotionSettingService.remove(settingLambdaUpdateWrapper); + + //删除通道的成员 + LambdaUpdateWrapper promotionUserLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + promotionUserLambdaUpdateWrapper.eq(FtbCultivatePromotionUser::getPromotionId, id); + ftbCultivatePromotionUserService.remove(promotionUserLambdaUpdateWrapper); + } + + /** + * 获取学习地图详情 + * + * @param id 学习地图id + * @return 学习地图信息 + */ + @Override + public V2CultivatePromotionVo get(String id) { + // 查询学习地图信息 ftb_cultivate_promotion_new + FtbCultivatePromotionNew promotionNew = ftbCultivatePromotionNewService.getById(id); + if (promotionNew == null || promotionNew.getEnableMark() == 1) { + throw new RuntimeException("该学习地图不存在"); + } + + // 将基础信息转换为VO + V2CultivatePromotionVo vo = V2CultivatePromotionVo.coverFtbCultivateStudyMemberVO(promotionNew); + + // 查询学习地图岗位信息 ftb_cultivate_promotion_post_new + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, id) + .eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); // 只查询未删除的记录 + List postList = ftbCultivatePromotionPostNewService.list(postWrapper); + + List allPostIds = new ArrayList<>(); + Map allPostInfoMap = new HashMap<>(); + if (CollUtil.isNotEmpty(postList)) { + allPostIds = postList.stream().map(FtbCultivatePromotionPostNew::getPostId).collect(Collectors.toList()); + List positionAndGradesVOS = userApiV2Util.listPositionAndGradeByPositionIds(allPostIds, null); + if (CollUtil.isNotEmpty(positionAndGradesVOS)) { + allPostInfoMap = positionAndGradesVOS.stream().collect(Collectors.toMap(PositionAndGradesVO::getId, item -> item)); + } + } + // 按level分组并组织岗位信息 + Map> postGroupByLevel = postList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + + List levelList = new ArrayList<>(); + for (Map.Entry> entry : postGroupByLevel.entrySet()) { + Integer level = entry.getKey(); + List levelPosts = entry.getValue(); + + V2CultivatePromotionLevel promotionLevel = new V2CultivatePromotionLevel(); + promotionLevel.setLevel(level); + + // 使用第一个岗位的选学数量作为该级别的选学数量(假设同一级别的岗位选学数量相同) + if (!levelPosts.isEmpty()) { + promotionLevel.setSelectCourseNumber(levelPosts.get(0).getSelectCourseNumber()); + } + List postVOList = new ArrayList<>(); + for (FtbCultivatePromotionPostNew post : levelPosts) { + + PositionAndGradesVO positionAndGradesVO = allPostInfoMap.get(post.getPostId()); + + // 设置状态:如果岗位信息不存在或职级不匹配则设置为1(无效),否则为0(有效) + int status = determineStatus(positionAndGradesVO, post.getGradeId()); + postVOList.add(buildV2CultivatePromotionPostNewVO(post, status, positionAndGradesVO)); + } + promotionLevel.setPostList(postVOList); + levelList.add(promotionLevel); + } + + // 按level排序 + levelList.sort(Comparator.comparing(V2CultivatePromotionLevel::getLevel)); + vo.setLevelList(levelList); + + // 查询学习地图配置 ftb_cultivate_promotion_setting + LambdaQueryWrapper settingWrapper = Wrappers.lambdaQuery(); + settingWrapper.eq(FtbCultivatePromotionSetting::getPromotionId, id); + List settingList = ftbCultivatePromotionSettingService.list(settingWrapper); + + // 转换配置信息 + List scopeVoList = settingList.stream().map(setting -> { + V2CultivatePromotionScopeVo scopeVo = new V2CultivatePromotionScopeVo(); + scopeVo.setId(setting.getId()); + scopeVo.setScope(setting.getScope()); + scopeVo.setPostId(setting.getPostId()); + scopeVo.setScopeId(setting.getScopeId()); + return scopeVo; + }).collect(Collectors.toList()); + + vo.setScopeVoList(scopeVoList); + + return vo; + } + + @NotNull + private static V2CultivatePromotionPostNewVO buildV2CultivatePromotionPostNewVO(FtbCultivatePromotionPostNew post, int status, PositionAndGradesVO positionAndGradesVO) { + V2CultivatePromotionPostNewVO postVO = new V2CultivatePromotionPostNewVO(); + postVO.setStatus(status); + postVO.setPromotionId(post.getPromotionId()); + postVO.setPostId(post.getPostId()); + postVO.setPostName(post.getPostName()); + postVO.setChannelLevel(post.getLevel()); + postVO.setGradeId(post.getGradeId()); + postVO.setGradeName(post.getGradeName()); + postVO.setSelectCourseNumber(post.getSelectCourseNumber()); + if (status == 0) { + postVO.fillPositionAndGradesName(positionAndGradesVO); + } + return postVO; + } + + /** + * 确定岗位职级状态 + * + * @param positionAndGradesVO 岗位及职级信息 + * @param gradeId 职级ID + * @return 状态码:0-有效,1-无效 + */ + private int determineStatus(PositionAndGradesVO positionAndGradesVO, String gradeId) { + // 如果岗位信息不存在,则状态为无效(1) + if (positionAndGradesVO == null) { + return 1; + } + + // 如果没有指定职级ID,则认为是有效的 + if (StringUtils.isEmpty(gradeId)) { + return 0; // 或根据业务需求决定默认值 + } + + // 如果岗位存在但没有关联的职级列表,则状态为无效(1) + if (CollUtil.isEmpty(positionAndGradesVO.getList())) { + return 1; + } + + // 检查指定的职级ID是否存在于岗位的职级列表中 + boolean gradeExists = positionAndGradesVO.getList().stream().anyMatch(item -> item.getId().equals(gradeId)); + + // 如果职级存在则返回0(有效),否则返回1(无效) + return gradeExists ? 0 : 1; + } + + /** + * 根据地图主键id 获取地图成员信息 + * + * @param req 请求参数对象,包含查询条件 + * @param reqPage 分页查询参数对象,包含分页信息如页码、每页大小等 + * @return V2WebCultivateStudyMemberListVo 获取结果对象,包含学习地图成员信息 + */ + @Override + public V2WebCultivateStudyMemberListVo queryPromotionUserList(V2WebCultivateStudyMemberListReq req, CultivatePage reqPage) { + Page page = reqPage.coverCultivatePage(); + V2WebCultivateStudyMemberListVo returnVo = new V2WebCultivateStudyMemberListVo(); + + // 查询学习地图基本信息 + FtbCultivatePromotionNew promotionNew = ftbCultivatePromotionNewService.getById(req.getPromotionId()); + if (promotionNew == null || promotionNew.getEnableMark() == 1) { + return returnVo; + } + + // 设置返回的头部信息 + returnVo.setPromotion(promotionNew.getPromotion()); + returnVo.setPromotionBusinessId(promotionNew.getPromotionBusinessId()); + + // 查询学习地图岗位职级信息 + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, req.getPromotionId()); + postWrapper.eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); + List postList = ftbCultivatePromotionPostNewService.list(postWrapper); + if (CollUtil.isEmpty(postList)) { + return returnVo; + } + + + Map postMap = new HashMap<>(); + List postIds = new ArrayList<>(); + List gradeIds = new ArrayList<>(); + int maxPhses = 0; + for (FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew : postList) { + postIds.add(ftbCultivatePromotionPostNew.getPostId()); + if (StringUtils.isNotEmpty(ftbCultivatePromotionPostNew.getGradeId())) { + gradeIds.add(ftbCultivatePromotionPostNew.getGradeId()); + } + postMap.put(ftbCultivatePromotionPostNew.getId(), ftbCultivatePromotionPostNew); + maxPhses = Math.max(maxPhses, ftbCultivatePromotionPostNew.getLevel()); + } + returnVo.setLearningPhase(maxPhses); + returnVo.setLearningScope(postList.size()); + + + Map stringGradeVOMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + + // 如果传入有关键词【用户姓名】 先查询V2UserApi的接口过滤用户id 再在ftb_cultivate_promotion_user这个表中查 + List queryUserIds = new ArrayList<>(); + if (StringUtils.isNotEmpty(req.getKeyWord())) { + List filteredUsers = userApiV2Util.getUserForNameOrPhone(req.getKeyWord(), null, null); + if (CollUtil.isNotEmpty(filteredUsers)) { + queryUserIds = filteredUsers.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + + } else { + // 如果关键词搜索结果为空,返回空结果 + return getV2WebCultivateStudyMemberListVo(page, returnVo); + } + } + + + // 使用分页查询获取当前页的用户 + Page pagedUserList = ftbCultivatePromotionUserService.queryUserForPage(page, queryUserIds, req.getPromotionId()); + + List userList = pagedUserList.getRecords(); + List userIds = userList.stream().map(V2CultivatePromotionMemberVo::getUserId).collect(Collectors.toList()); + + if (CollUtil.isEmpty(userIds)) { + // 如果没有用户,返回空结果 + return getV2WebCultivateStudyMemberListVo(page, returnVo); + } + + // 查询每个人对这个学习地图每个阶段选择的岗位 + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.select(FtbCultivatePromotionMemberNew::getUserId, FtbCultivatePromotionMemberNew::getPromotionId, FtbCultivatePromotionMemberNew::getCurrentLearningStage, FtbCultivatePromotionMemberNew::getPromotionPostId); + memberWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, req.getPromotionId()); + memberWrapper.in(FtbCultivatePromotionMemberNew::getUserId, userIds); + List memberList = ftbCultivatePromotionMemberNewService.list(memberWrapper); + + // 按用户ID分组成员信息 + Map> memberGroupByUser = memberList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionMemberNew::getUserId)); + + // 获取用户详细信息 + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + + // 构建VO对象 + for (V2CultivatePromotionMemberVo vo : userList) { // 遍历分页后的用户列表,保持正确的顺序 + String userId = vo.getUserId(); + UserBoundVO userBound = userMap.get(userId); + if (userBound == null) continue; + vo.setUserId(userBound.getId()); + vo.setUserName(userBound.getUserName()); + vo.setOrgId(userBound.getOrganizeId()); + vo.setOrgName(userBound.getOrganizeName()); + vo.setPositionId(userBound.getPositionId()); + vo.setPositionName(userBound.getPositionName()); + vo.setGradeId(userBound.getGradeId()); + vo.setGradeName(userBound.getGradeName()); + + // 查找该用户在该学习地图中的记录 + List userMembers = memberGroupByUser.get(userId); + + if (CollUtil.isNotEmpty(userMembers)) { + Integer maxStage = 1; + for (FtbCultivatePromotionMemberNew userMember : userMembers) { + if (userMember.getCurrentLearningStage() > maxStage) { + maxStage = userMember.getCurrentLearningStage(); + } + } + vo.setCurrentLearningStage(maxStage); + + // 获取已选学岗位 + List selectedStudyPositions = new ArrayList<>(); + for (FtbCultivatePromotionMemberNew userMember : userMembers) { + if (userMember.getCurrentLearningStage().equals(maxStage)) { + FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew = postMap.get(userMember.getPromotionPostId()); + if (ftbCultivatePromotionPostNew != null) { + selectedStudyPositions.add(ftbCultivatePromotionPostNew); + } + } + } + vo.setSelectedStudyPosition(convertToPositionAndGrade(selectedStudyPositions, stringGradeVOMap, stringPositionVOMap)); + // 当前学习岗位 todo 暂时不知道怎么获取 + vo.setCurrStudyPosition(convertToPositionAndGrade(selectedStudyPositions, stringGradeVOMap, stringPositionVOMap)); + } + } + + returnVo.setMemberList(CultivatePage.coverPageList(pagedUserList)); + return returnVo; + } + + @NotNull + private V2WebCultivateStudyMemberListVo getV2WebCultivateStudyMemberListVo(Page page, V2WebCultivateStudyMemberListVo returnVo) { + PageListVO emptyResult = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(page.getCurrent()); + pagination.setPageSize(page.getSize()); + pagination.setTotal(0); + emptyResult.setPagination(pagination); + emptyResult.setList(new ArrayList<>()); + returnVo.setMemberList(emptyResult); + return returnVo; + } + + private List convertToPositionAndGrade(List selectedStudyPositions, Map stringGradeVOMap, Map stringPositionVOMap) { + if (CollUtil.isEmpty(selectedStudyPositions)) { + return new ArrayList<>(); + } + List ret = new ArrayList<>(); + for (FtbCultivatePromotionPostNew selectedStudyPosition : selectedStudyPositions) { + + + V2CultivatePostAndGradeVo vo = new V2CultivatePostAndGradeVo(); + if (StringUtils.isNotEmpty(selectedStudyPosition.getGradeId())) { + GradeVO gradeVO = stringGradeVOMap.get(selectedStudyPosition.getGradeId()); + if (gradeVO != null) { + vo.setGradeId(gradeVO.getId()); + vo.setGradeName(gradeVO.getFullName()); + } + } + PositionVO positionVO = stringPositionVOMap.get(selectedStudyPosition.getPostId()); + if (positionVO != null) { + vo.setPostId(positionVO.getId()); + vo.setPostName(positionVO.getFullName()); + } + ret.add(vo); + } + return ret; + } + + /** + * 查询在岗位学习中完成的配置的岗位&职级 + * + * @param req 请求参数对象,包含排除条件 + * @return List 查询结果列表,包含岗位&职级信息 + */ + @Override + public List queryCultivatePosition(V2CultivatePositionExcludeReq req) { + // 获取所有有效的岗位配置 + LambdaQueryWrapper positionWrapper = Wrappers.lambdaQuery(); + positionWrapper.eq(FtbCultivatePosition::getIsGrounding, 1); + positionWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); // 仅查询有效的岗位 + List positionList = ftbCultivatePositionService.list(positionWrapper); + + if (CollUtil.isEmpty(positionList)) { + return List.of(); + } + + // 提取ID列表并在一次遍历中完成 + List learnIds = new ArrayList<>(positionList.size()); + List postIds = new ArrayList<>(positionList.size()); + for (FtbCultivatePosition ftbCultivatePosition : positionList) { + postIds.add(ftbCultivatePosition.getPostId()); + learnIds.add(ftbCultivatePosition.getId()); + } + + // 获取所有岗位对应的职级配置 + LambdaQueryWrapper settingWrapper = Wrappers.lambdaQuery(); + settingWrapper.in(FtbCultivatePositionSetting::getPostLearnId, learnIds); + List settingList = ftbCultivatePositionSettingService.list(settingWrapper); + + // 构建岗位学习ID到职级列表的映射 + Map> positionSettingMap = settingList.stream().collect(Collectors.groupingBy(FtbCultivatePositionSetting::getPostLearnId, Collectors.toList())); + + // 提取职级ID列表 + List gradeIds = settingList.stream().map(FtbCultivatePositionSetting::getGradeId).distinct().collect(Collectors.toList()); + + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + Map stringGradeVOMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + + // 构建最终结果 + List result = new ArrayList<>(); + List excludes = req.getPostAndGradeVoList(); + + // 预处理排除项,建立postId到排除职级ID集合的映射,用于快速查找 + Set excludedPostIds = new HashSet<>(); + Map> excludedGradeIdsByPost = new HashMap<>(); + if (CollUtil.isNotEmpty(excludes)) { + for (V2CultivatePostAndGradeVo exclude : excludes) { + if (StringUtils.isEmpty(exclude.getGradeId())) { + excludedPostIds.add(exclude.getPostId()); + } else { + excludedGradeIdsByPost.computeIfAbsent(exclude.getPostId(), k -> new HashSet<>()).add(exclude.getGradeId()); + } + } + } + + for (FtbCultivatePosition position : positionList) { + // 检查岗位是否被完全排除 + if (excludedPostIds.contains(position.getPostId())) { + continue; + } + + V2CultivatePositionVo positionVo = new V2CultivatePositionVo(); + result.add(positionVo); + positionVo.setPostId(position.getPostId()); + PositionVO positionVO = stringPositionVOMap.get(position.getPostId()); + if (positionVO != null) { + positionVo.setPostName(positionVO.getFullName()); + } + + // 获取该岗位下的职级配置 + List gradeSettings = positionSettingMap.get(position.getId()); + List gradeList = new ArrayList<>(); + + if (CollUtil.isNotEmpty(gradeSettings)) { + Set excludedGradesForCurrentPost = excludedGradeIdsByPost.get(position.getPostId()); + for (FtbCultivatePositionSetting setting : gradeSettings) { + // 检查当前职级是否被排除 + if (excludedGradesForCurrentPost != null && excludedGradesForCurrentPost.contains(setting.getGradeId())) { + continue; + } + + V2CultivatePositionGradeVo gradeVo = new V2CultivatePositionGradeVo(); + GradeVO gradeVO = stringGradeVOMap.get(setting.getGradeId()); + if (gradeVO != null) { // 只有当职级名称存在时才添加 + gradeVo.setGradeId(gradeVO.getId()); + gradeVo.setGradeName(gradeVO.getName()); + gradeList.add(gradeVo); + } + } + } + positionVo.setGradeList(gradeList); + } + + return result; + } + + /** + * 获取我的所有学习地图列表 + * + * @param userId 用户ID + * @return List 查询结果列表,包含用户的学习地图信息 + */ + @Override + public List queryMyAllPromotionList(String userId) { + List list = ftbCultivatePromotionUserService.queryMyAllPromotionList(userId); + if (CollUtil.isNotEmpty(list)) { + List promotionIds = list.stream().map(MyCultivatePromotionListVo::getId).collect(Collectors.toList()); + //查询最大阶段 + List maxList = ftbCultivatePromotionNewService.queryMaxLevel(promotionIds); + Map promotionIdMap = new HashMap<>(); + if (CollUtil.isNotEmpty(maxList)) { + promotionIdMap = maxList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + } + List completePromotionList = ftbCultivatePromotionLogService.queryMyCompletePromotion(userId, 2); + //按照推广ID进行分组 + Map> completePromotionMap = new HashMap<>(); + if (CollUtil.isNotEmpty(completePromotionList)) { + completePromotionMap = completePromotionList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionLog::getPromotionId)); + } + //按照 + //用户所在阶段 + List currentPhaseList = ftbCultivatePromotionMemberNewService.queryMyCurrentPhase(userId, promotionIds); + Map currentPhaseMap = new HashMap<>(); + if (CollUtil.isNotEmpty(currentPhaseList)) { + currentPhaseMap = currentPhaseList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + } + for (MyCultivatePromotionListVo item : list) { + BatchCommonCountDto max = promotionIdMap.get(item.getId()); + if (max != null) { + item.setPhaseNumber(max.getNum()); + } else { + item.setPhaseNumber(0); + } + BatchCommonCountDto currentPhase = currentPhaseMap.get(item.getId()); + if (currentPhase != null) { + item.setDoingPhaseNumber(currentPhase.getNum()); + } else { + item.setDoingPhaseNumber(1); + } + List ftbCultivatePromotionLogs = completePromotionMap.get(item.getId()); + if (CollUtil.isNotEmpty(ftbCultivatePromotionLogs)) { + item.setCompletePhaseList(ftbCultivatePromotionLogs.stream().map(FtbCultivatePromotionLog::getLevel).collect(Collectors.toList())); + } + } + } + return list; + } + + /** + * 查询学习地图的阶段信息 + * + * @param userId 用户ID + * @param promotionId 学习地图ID + * @param level 阶段编号 + * @return PromotionPhaseVo 阶段信息 + */ + @Override + public PromotionPhaseVo queryPromotionInfoForLevel(String userId, String promotionId, Integer level) { + + PromotionPhaseVo resultVo = new PromotionPhaseVo(); + LambdaQueryWrapper resetWrapper = Wrappers.lambdaQuery(); + resetWrapper.eq(FtbCultivatePromotionUser::getUserId, userId).eq(FtbCultivatePromotionUser::getPromotionId, promotionId); + List userList = ftbCultivatePromotionUserService.list(resetWrapper); + if (CollUtil.isEmpty(userList)) { + throw new RuntimeException("您没有加入该学习地图"); + } + + // 查询学习地图信息 ftb_cultivate_promotion_new + FtbCultivatePromotionNew promotionNew = ftbCultivatePromotionNewService.getById(promotionId); + if (promotionNew == null || promotionNew.getEnableMark() == 1) { + throw new RuntimeException("该学习地图不存在"); + } + if (promotionNew.getIsError().equals(1)) { + throw new RuntimeException("该学习地图异常"); + } + + resultVo.setLevel(level); + + + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + postWrapper.eq(FtbCultivatePromotionPostNew::getLevel, level); + postWrapper.eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); // 只查询未删除的记录 + List postList = ftbCultivatePromotionPostNewService.list(postWrapper); + if (CollUtil.isEmpty(postList)) { + return resultVo; + } + + Map progressVoMap = v2CultivateBatchQueryService.batchQueryPositionProgress(postList, userId); + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.eq(FtbCultivatePromotionMemberNew::getPromotionId, promotionId).eq(FtbCultivatePromotionMemberNew::getCurrentLearningStage, level) + .eq(FtbCultivatePromotionMemberNew::getUserId, userId).eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); // 只查询未删除的记录 + List memberNewList = ftbCultivatePromotionMemberNewService.list(memberWrapper); + + + List postIds = new ArrayList<>(); + List gradeIds = new ArrayList<>(); + Map postIdMap = new HashMap<>(); + for (FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew : postList) { + postIdMap.put(ftbCultivatePromotionPostNew.getId(), ftbCultivatePromotionPostNew); + postIds.add(ftbCultivatePromotionPostNew.getPostId()); + if (StringUtils.isNotEmpty(ftbCultivatePromotionPostNew.getGradeId())) { + gradeIds.add(ftbCultivatePromotionPostNew.getGradeId()); + } + } + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + Map stringGradeVOMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + //设置配置的岗位学习 + resultVo.setPostAndGradeList(PromotionLevelInfoVo.convertToPositionAndGrade(postList, stringGradeVOMap, stringPositionVOMap, memberNewList, progressVoMap)); + + resultVo.setSelectCourseNumber(postList.get(0).getSelectCourseNumber()); + + + if (CollUtil.isNotEmpty(memberNewList)) { + resultVo.setUserSelect(1); + } else { + resultVo.setUserSelect(0); + } + courseStudyUtil.checkCultivateLearning(userId, UserProvider.getUser().getTenantId()); + return resultVo; + } + + /** + * 添加岗位到学习地图 + * + * @param id 学习地图ID + * @param creatDto 添加岗位信息 + */ + @Override + public void addMemberToPromotion(String id, V2CultivatePromotionCreateReq creatDto) { + UserInfo user = UserProvider.getUser(); + String tenantId = user.getTenantId(); + + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + //检查任务开始并发消息提醒 + List scope = creatDto.getScope(); + if (CollUtil.isEmpty(scope)) { + return; + } + List postIds = new ArrayList<>(); + List userIds = new ArrayList<>(); + List postScope = new ArrayList<>(); + for (V2CultivatePromotionScopeReq scopeReq : scope) { + if (scopeReq.getScope().equals(3)) { + userIds.add(scopeReq.getScopeId()); + } else { + postIds.add(scopeReq.getPostId()); + postScope.add(scopeReq); + } + } + if (CollUtil.isNotEmpty(postIds)) { + List userListForPositions = userApiV2Util.getUserListForPositions(postIds, tenantId); + List positUserIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(userListForPositions)) { + for (UserBoundVO userBoundVO : userListForPositions) { + for (V2CultivatePromotionScopeReq scopeReq : postScope) { + if (scopeReq.getScope().equals(1) && userBoundVO.getPositionId().equals(scopeReq.getPostId())) { + positUserIds.add(userBoundVO.getId()); + break; + } else if (scopeReq.getScope().equals(2) && userBoundVO.getPositionId().equals(scopeReq.getPostId()) && userBoundVO.getGradeId().equals(scopeReq.getScopeId())) { + positUserIds.add(userBoundVO.getId()); + break; + } + } + } + } + if (CollUtil.isNotEmpty(positUserIds)) { + userIds.addAll(positUserIds); + } + } + //加入学习地图 + if (CollUtil.isNotEmpty(userIds)) { + userApiV2Util.checkOutTenant(tenantId); + List userList = new ArrayList<>(); + for (String userId : userIds) { + FtbCultivatePromotionUser promotionUser = new FtbCultivatePromotionUser(); + promotionUser.setPromotionId(id); + promotionUser.setUserId(userId); + promotionUser.setEnabledMark(0); + userList.add(promotionUser); + } + ftbCultivatePromotionUserService.saveBatch(userList); + } + + }), cultivateThreadPool); + } + + /** + * 选择学习地图 + * + * @param userId 用户id + * @param promotionId 学习地图id + */ + @Override + @Transactional + public void selectPromotion(String userId, String promotionId) { + // 首先将用户的所有学习地图选择标记设为0 + LambdaUpdateWrapper resetWrapper = Wrappers.lambdaUpdate(); + resetWrapper.eq(FtbCultivatePromotionUser::getUserId, userId).set(FtbCultivatePromotionUser::getUserSelect, 0); + ftbCultivatePromotionUserService.update(resetWrapper); + + // 然后将指定的学习地图选择标记设为1 + LambdaUpdateWrapper selectWrapper = Wrappers.lambdaUpdate(); + selectWrapper.eq(FtbCultivatePromotionUser::getUserId, userId).eq(FtbCultivatePromotionUser::getPromotionId, promotionId).set(FtbCultivatePromotionUser::getUserSelect, 1); + ftbCultivatePromotionUserService.update(selectWrapper); + } + + /** + * 学习地图的一个阶段,选定学习的岗位 + * + * @param req 请求参数对象,包含岗位选择信息 + */ + @Override + @Transactional + public void recordSelectPosition(V2CultivatePromotionSelectPositionReq req) { + + if (CollUtil.isEmpty(req.getPostAndGradeList())) { + throw new RuntimeException("请选择需要学习的岗位"); + } + String userId = UserProvider.getUser().getUserId(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (userPrimaryBoundOne == null) { + log.error("查询用户信息失败:{}", userId); + return; + } + // 查询学习地图信息 ftb_cultivate_promotion_new + FtbCultivatePromotionNew promotionNew = ftbCultivatePromotionNewService.getById(req.getPromotionId()); + if (promotionNew == null || promotionNew.getEnableMark() == 1) { + throw new RuntimeException("该学习地图不存在"); + } + if (promotionNew.getIsError().equals(1)) { + throw new RuntimeException("该学习地图已异常"); + } + + // 查询学习地图岗位信息 ftb_cultivate_promotion_post_new + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, req.getPromotionId()).eq(FtbCultivatePromotionPostNew::getLevel, req.getLevel()).eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); // 只查询未删除的记录 + List postList = ftbCultivatePromotionPostNewService.list(postWrapper); + if (CollUtil.isEmpty(postList)) { + throw new RuntimeException("本学习地图该阶段没有可以选择的岗位"); + } + + Map promotionPostNewMap = new HashMap<>(); + for (FtbCultivatePromotionPostNew postNew : postList) { + promotionPostNewMap.put(postNew.getPostId() + "_" + postNew.getGradeId(), postNew); + } + + LambdaQueryWrapper resetWrapper = Wrappers.lambdaQuery(); + resetWrapper.eq(FtbCultivatePromotionUser::getUserId, userId).eq(FtbCultivatePromotionUser::getPromotionId, req.getPromotionId()); + List userList = ftbCultivatePromotionUserService.list(resetWrapper); + if (CollUtil.isEmpty(userList)) { + throw new RuntimeException("该用户没有加入该学习地图"); + } + + List promotionMemberNewList = new ArrayList<>(); + for (V2CultivatePostAndGradeVo postAndGrade : req.getPostAndGradeList()) { + FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew = promotionPostNewMap.get(postAndGrade.getPostId() + "_" + postAndGrade.getGradeId()); + if (ftbCultivatePromotionPostNew == null) { + continue; + } + FtbCultivatePromotionMemberNew promotionMemberNew = new FtbCultivatePromotionMemberNew(); + promotionMemberNew.setPromotionId(req.getPromotionId()); + promotionMemberNew.setPromotionPostId(ftbCultivatePromotionPostNew.getId()); + promotionMemberNew.setUserId(userId); + promotionMemberNew.setCurrentLearningStage(req.getLevel()); + promotionMemberNew.setDeleteMark(0); + promotionMemberNew.setCreatorTime(new Date()); + promotionMemberNew.setCreatorUserId(userId); + promotionMemberNew.setOrgName(userPrimaryBoundOne.getOrganizeName()); + promotionMemberNew.setOrgId(userPrimaryBoundOne.getOrganizeId()); + promotionMemberNew.setPositionName(ftbCultivatePromotionPostNew.getPostName()); + promotionMemberNew.setPositionId(ftbCultivatePromotionPostNew.getPostId()); + promotionMemberNew.setGradeName(ftbCultivatePromotionPostNew.getGradeName()); + promotionMemberNew.setGradeId(ftbCultivatePromotionPostNew.getGradeId()); + promotionMemberNewList.add(promotionMemberNew); + } + if (CollUtil.isNotEmpty(promotionMemberNewList)) { + ftbCultivatePromotionMemberNewService.saveBatch(promotionMemberNewList); + } + + // 初始化学习地图阶段日志表 FtbCultivatePromotionLog + initializePromotionLogIfNeeded(userId, req.getPromotionId(), req.getLevel()); + + } + + /** + * 初始化学习地图阶段日志 + * + * @param userId 用户ID + * @param promotionId 学习地图ID + * @param level 阶段编号 + */ + private void initializePromotionLogIfNeeded(String userId, String promotionId, Integer level) { + // 检查该用户在该学习地图的该阶段是否已经有日志记录 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbCultivatePromotionLog::getUserId, userId) + .eq(FtbCultivatePromotionLog::getPromotionId, promotionId) + .eq(FtbCultivatePromotionLog::getLevel, level) + .eq(FtbCultivatePromotionLog::getEnabledMark, 0); + + long count = ftbCultivatePromotionLogService.count(queryWrapper); + + // 如果不存在日志记录,则创建新的日志记录(状态为未完成) + if (count == 0) { + FtbCultivatePromotionLog promotionLog = new FtbCultivatePromotionLog(); + promotionLog.setUserId(userId); + promotionLog.setPromotionId(promotionId); + promotionLog.setLevel(level); + promotionLog.setState(1); // 1-未完成 + promotionLog.setEnabledMark(0); // 0-有效 + ftbCultivatePromotionLogService.save(promotionLog); + } + } + + /** + * 获取岗位学习详情 + * + * @param req 请求参数 + * @return 岗位学习详情 + */ + @Override + public AppCultivatePromotionPositionDetailVo getPositionStudyDetail(V2CultivatePromotionPositionDetailForApp req) { + req.checkParams(); + AppCultivatePromotionPositionDetailVo resultVo = new AppCultivatePromotionPositionDetailVo(); + LambdaQueryWrapper resetWrapper = Wrappers.lambdaQuery(); + resetWrapper.eq(FtbCultivatePromotionUser::getUserId, req.getUserId()).eq(FtbCultivatePromotionUser::getPromotionId, req.getPromotionId()); + List userList = ftbCultivatePromotionUserService.list(resetWrapper); + if (CollUtil.isEmpty(userList)) { + throw new RuntimeException("您没有加入该学习地图"); + } + + // 查询学习地图信息 ftb_cultivate_promotion_new + FtbCultivatePromotionNew promotionNew = ftbCultivatePromotionNewService.getById(req.getPromotionId()); + if (promotionNew == null || promotionNew.getEnableMark() == 1) { + throw new RuntimeException("该学习地图不存在"); + } + if (promotionNew.getIsError().equals(1)) { + throw new RuntimeException("该学习地图异常"); + } + + + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, req.getPromotionId()); + postWrapper.eq(FtbCultivatePromotionPostNew::getPostId, req.getPostId()); + postWrapper.eq(FtbCultivatePromotionPostNew::getLevel, req.getLevel()); + postWrapper.eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); // 只查询未删除的记录 + List postList = ftbCultivatePromotionPostNewService.list(postWrapper); + if (CollUtil.isEmpty(postList)) { + throw new RuntimeException("该学习地图,第+" + req.getLevel() + "阶段没有该岗位"); + } + + //查询岗位学习的基本详情 + V2CultivatePositionDetailForApp v2CultivatePositionDetailForAppReq = new V2CultivatePositionDetailForApp(); + v2CultivatePositionDetailForAppReq.setPostId(req.getPostId()); + v2CultivatePositionDetailForAppReq.setGradeId(req.getGradeId()); + v2CultivatePositionDetailForAppReq.setUserId(req.getUserId()); + v2CultivatePositionDetailForAppReq.setCompulsory(req.getCompulsory()); + AppCultivatePositionDetailVo appCultivatePositionDetailVo = v2CultivatePositionService.positionStudyDetail(v2CultivatePositionDetailForAppReq); + resultVo.setPositionDetail(appCultivatePositionDetailVo); + //查询该阶段是否完成 + V2CultivatePromotionPhaseStateForApp promotionPhaseStateForAppReq = new V2CultivatePromotionPhaseStateForApp(); + promotionPhaseStateForAppReq.setPromotionId(req.getPromotionId()); + promotionPhaseStateForAppReq.setLevel(req.getLevel()); + promotionPhaseStateForAppReq.setUserId(req.getUserId()); + resultVo.setPhaseStats(promotionLevelStatus(promotionPhaseStateForAppReq)); + return resultVo; + } + + /** + * 获取学习地图阶段状态 + * + * @param req 请求参数 + * @return 阶段状态 状态 0-未完成 1-已完成 + */ + @Override + public Integer promotionLevelStatus(V2CultivatePromotionPhaseStateForApp req) { + LambdaQueryWrapper logWrapper = Wrappers.lambdaQuery(); + logWrapper.eq(FtbCultivatePromotionLog::getPromotionId, req.getPromotionId()) + .eq(FtbCultivatePromotionLog::getLevel, req.getLevel()) + .eq(FtbCultivatePromotionLog::getUserId, req.getUserId()) + .eq(FtbCultivatePromotionLog::getEnabledMark, 0); + List list = ftbCultivatePromotionLogService.list(logWrapper); + if (CollUtil.isNotEmpty(list)) { + return list.get(0).getState().equals(2) ? 1 : 0; + } else { + initializePromotionLogIfNeeded(req.getUserId(), req.getPromotionId(), req.getLevel()); + return 0; + } + } + + /** + * 修改学习地图异常信息 + * + * @param list 异常信息 + */ + @Override + public void updatePromotionError(List list) { + // 按学习地图ID进行分组 + Map> promotionIdMap = list.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getPromotionId)); + + for (Map.Entry> entry : promotionIdMap.entrySet()) { + String promotionId = entry.getKey(); + List promotionPosts = entry.getValue(); + + // 转换异常岗位信息 + List jsonList = new ArrayList<>(); + if (CollUtil.isNotEmpty(promotionPosts)) { + for (FtbCultivatePromotionPostNew promotionPost : promotionPosts) { + V2CultivatePostAndGradeVo json = new V2CultivatePostAndGradeVo(); + json.setPostId(promotionPost.getPostId()); + json.setPostName(promotionPost.getPostName()); + json.setGradeId(promotionPost.getGradeId()); + json.setGradeName(promotionPost.getGradeName()); + jsonList.add(json); + } + } + + // 创建异常信息VO + V2CultivatePromotionErrorVo vo = new V2CultivatePromotionErrorVo(); + vo.setPositionDel(jsonList); + + // 更新学习地图状态 + ftbCultivatePromotionNewService.update(new UpdateWrapper().lambda().eq(FtbCultivatePromotionNew::getId, promotionId).set(FtbCultivatePromotionNew::getIsError, 1).set(FtbCultivatePromotionNew::getErrorMsg, JSONUtil.toJsonStr(vo))); + } + } + + + /** + * 获取学习地图人员统计信息 + * + * @param req 统计信息请求参数 + * @return 学习地图人员统计信息 + */ + @Override + public PageListVO personStatistics(V2PromotionPersonStatisticReq req) { + // 1. 获取符合条件的用户ID列表 + List userIds = getUserIdsForStatistics(req); + if (userIds == null) { + return UserApiV2Util.returnEmptyListGeneric(req.getCurrentPage(), req.getPageSize()); + } + + // 2. 执行分页查询 + Page page = req.coverCultivatePage(); + page = ftbCultivatePromotionNewService.personStatisticsV2(req, userIds, page); + List records = page.getRecords(); + + if (CollUtil.isEmpty(records)) { + return CultivatePage.coverPageList(page); + } + + // 3. 批量处理统计数据 + enrichPersonStatisticsData(records); + + return CultivatePage.coverPageList(page); + } + + /** + * 获取当前组织下的所有组织 + * + * @param orgIds 组织ID列表 + * @return 组织ID列表,包括当前组织及其所有子组织 + */ + public List queryChildOrg(List orgIds) { + List organizeGeneralDetailVOS = userApiV2Util.organizesOrHaveChildByOrganizeIds(orgIds, true, null); + if (ObjectUtil.isEmpty(organizeGeneralDetailVOS)) { + return new ArrayList<>(); + } + return organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } + + /** + * 获取学习地图组织统计信息 + * + * @param req 统计信息请求参数 + * @return 学习地图组织统计信息 + */ + @Override + public Page organizationStatistics(V2PromotionOrgStatisticReq req) { + Page page = req.coverCultivatePage(); + page = ftbCultivatePromotionNewService.mapStatisticPageV2(page, req, new ArrayList<>()); + if (CollUtil.isNotEmpty(page.getRecords())) { + List maxList = ftbCultivatePromotionNewService.queryMaxLevel(page.getRecords().stream().map(V2CultivatePromotionOrgStatisticVo::getPromotionId).collect(Collectors.toList())); + Map promotionIdMap = maxList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + + List positionList = ftbCultivatePromotionNewService.queryPositionNumForMap(page.getRecords().stream().map(V2CultivatePromotionOrgStatisticVo::getPromotionId).collect(Collectors.toList())); + Map positionNumMap = positionList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + + for (V2CultivatePromotionOrgStatisticVo record : page.getRecords()) { + record.setPhaseNum(promotionIdMap.get(record.getPromotionId()) == null ? 0 : promotionIdMap.get(record.getPromotionId()).getNum()); + record.setNumberOfMapPositions(positionNumMap.get(record.getPromotionId()) == null ? 0 : positionNumMap.get(record.getPromotionId()).getNum()); + } + } + + List orgIds = new ArrayList<>(); + List powerOrgList = userApiV2Util.queryPowerOrgList(); + if (CollUtil.isEmpty(powerOrgList)) { + return page; + } + + if ("1".equals(req.getSelectLogo()) && CollUtil.isNotEmpty(req.getOrgIds())) { + List allOrganize = queryChildOrg(req.getOrgIds()); + List intersection = UserApiV2Util.getIntersection(powerOrgList, allOrganize); + if (CollUtil.isEmpty(intersection)) { + return page; + } + orgIds = intersection; + } else if (CollUtil.isNotEmpty(req.getOrgIds())) { + List intersection = UserApiV2Util.getIntersection(powerOrgList, req.getOrgIds()); + if (CollUtil.isEmpty(intersection)) { + return page; + } + orgIds = intersection; + } else if (CollUtil.isEmpty(req.getOrgIds())) { + orgIds = powerOrgList; + } + + List userListForOrgIds = userApiV2Util.getUserListForOrgIds(orgIds, null); + if (CollUtil.isEmpty(userListForOrgIds)) { + return page; + } + List userIds = userListForOrgIds.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + + + List userCount = ftbCultivatePromotionNewService.userCountForPromotion(userIds); + Map userNumMap = userCount.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + + + for (V2CultivatePromotionOrgStatisticVo record : page.getRecords()) { + record.setStudyUserNum(userNumMap.get(record.getPromotionId()) == null ? 0 : userNumMap.get(record.getPromotionId()).getNum()); + record.setStudyUserIds(userNumMap.get(record.getPromotionId()) == null ? new ArrayList<>() : List.of(userNumMap.get(record.getPromotionId()).getUserIds().split(","))); + } + return page; + } + + @Override + public Map> queryAllSelectPosition(String userId, List promotionIds) { + + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.in(FtbCultivatePromotionPostNew::getPromotionId, promotionIds); + postWrapper.eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); // 只查询未删除的记录 + List postList = ftbCultivatePromotionPostNewService.list(postWrapper); + if (CollUtil.isEmpty(postList)) { + return new HashMap<>(); + } + + LambdaQueryWrapper memberWrapper = Wrappers.lambdaQuery(); + memberWrapper.in(FtbCultivatePromotionMemberNew::getPromotionId, promotionIds).eq(FtbCultivatePromotionMemberNew::getUserId, userId).eq(FtbCultivatePromotionMemberNew::getDeleteMark, 0); // 只查询未删除的记录 + List memberNewList = ftbCultivatePromotionMemberNewService.list(memberWrapper); + if (CollUtil.isEmpty(memberNewList)) { + return new HashMap<>(); + } + + + List postIds = new ArrayList<>(); + List gradeIds = new ArrayList<>(); + Map postIdMap = new HashMap<>(); + for (FtbCultivatePromotionPostNew ftbCultivatePromotionPostNew : postList) { + postIdMap.put(ftbCultivatePromotionPostNew.getId(), ftbCultivatePromotionPostNew); + postIds.add(ftbCultivatePromotionPostNew.getPostId()); + if (StringUtils.isNotEmpty(ftbCultivatePromotionPostNew.getGradeId())) { + gradeIds.add(ftbCultivatePromotionPostNew.getGradeId()); + } + } + Map stringPositionVOMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + Map stringGradeVOMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + //设置配置的岗位学习 + return PromotionLevelInfoVo.convertToPositionAndGradeNextUser(stringGradeVOMap, stringPositionVOMap, memberNewList); + + } + + + private List getUserIdsForStatistics(V2PromotionPersonStatisticReq req) { + List userIds = new ArrayList<>(); + + // 处理组织机构筛选 + if (CollUtil.isNotEmpty(req.getOrgIds())) { + List orgUsers = userApiV2Util.getUserListForOrgIds(req.getOrgIds(), null); + if (CollUtil.isEmpty(orgUsers)) { + return null; + } + + List orgUserIds = orgUsers.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(req.getUserIds())) { + userIds = orgUserIds; + } else { + // 取交集:同时满足组织筛选和用户筛选 + List intersection = UserApiV2Util.getIntersection(req.getUserIds(), orgUserIds); + if (CollUtil.isEmpty(intersection)) { + return null; + } + userIds = intersection; + } + } else if (CollUtil.isNotEmpty(req.getUserIds())) { + // 如果没有指定组织但指定了用户,直接使用指定的用户ID(去重) + userIds = req.getUserIds().stream().distinct().collect(Collectors.toList()); + } + + // 处理权限控制 + InnerPowerUserVO powerUser = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (powerUser.getCode() == 2) { + // 无权限,返回null + return null; + } + + if (powerUser.getCode() == 1) { + // 有部分权限,需要与权限用户取交集 + if (CollUtil.isNotEmpty(userIds)) { + List intersection = UserApiV2Util.getIntersection(powerUser.getUserIds(), userIds); + return CollUtil.isNotEmpty(intersection) ? intersection : null; + } else { + // 未指定任何筛选条件,返回所有有权限的用户 + userIds = powerUser.getUserIds(); + } + } + // powerUser.getCode() == 0 表示完全权限,直接返回已筛选的userIds(可能为空列表) + + return userIds; + } + + /** + * 批量丰富人员统计数据 + */ + private void enrichPersonStatisticsData(List records) { + // 提取用户ID和学习地图ID + Set userIds = records.stream().map(V2CultivatePromotionPersonStatisticVo::getUserId).collect(Collectors.toSet()); + Set promotionIds = records.stream().map(V2CultivatePromotionPersonStatisticVo::getPromotionId).collect(Collectors.toSet()); + + List maxList = ftbCultivatePromotionNewService.queryMaxLevel(new ArrayList<>(promotionIds)); + Map promotionIdMap = maxList.stream().collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, v -> v)); + + + // 并行获取所有需要的数据 + PersonStatisticsDataDto data = loadPersonStatisticsData(userIds, promotionIds); + + // 填充统计数据 + for (V2CultivatePromotionPersonStatisticVo record : records) { + enrichSingleRecord(record, data); + BatchCommonCountDto max = promotionIdMap.get(record.getPromotionId()); + if (max != null) { + record.setPhaseNum(max.getNum()); + } else { + record.setPhaseNum(0); + } + } + } + + /** + * 加载人员统计所需的所有数据 + */ + private PersonStatisticsDataDto loadPersonStatisticsData(Set userIds, Set promotionIds) { + PersonStatisticsDataDto data = new PersonStatisticsDataDto(); + + // 1. 获取学习地图岗位职级信息 + List postList = getPromotionPosts(promotionIds); + data.postMap = postList.stream().collect(Collectors.toMap(FtbCultivatePromotionPostNew::getId, p -> p)); + data.postGroupMap = postList.stream().collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getPromotionId)); + + List postIds = postList.stream().map(FtbCultivatePromotionPostNew::getPostId).distinct().collect(Collectors.toList()); + List gradeIds = postList.stream().map(FtbCultivatePromotionPostNew::getGradeId).filter(StringUtils::isNotEmpty).distinct().collect(Collectors.toList()); + + // 2. 批量获取基础数据 + data.gradeMap = userApiV2Util.listGradeByIdsReturnMap(gradeIds, null); + data.positionMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(postIds, null); + data.userBoundMap = userApiV2Util.getUserPrimaryBoundBatch(new ArrayList<>(userIds), null); + + // 3. 获取已完成的岗位记录 + List completePositions = ftbCultivatePositionLogService.batchQueryCompletePosition(new ArrayList<>(userIds)); + data.completePositionMap = completePositions.stream().collect(Collectors.groupingBy(FtbCultivatePositionLog::getUserId)); + + // 4. 获取成员学习信息 + data.memberMap = getPromotionMembers(promotionIds, userIds); + + return data; + } + + private List getPromotionPosts(Set promotionIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivatePromotionPostNew::getPromotionId, promotionIds); + wrapper.eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); + return ftbCultivatePromotionPostNewService.list(wrapper); + } + + private Map> getPromotionMembers(Set promotionIds, Set userIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.select(FtbCultivatePromotionMemberNew::getUserId, FtbCultivatePromotionMemberNew::getPromotionId, FtbCultivatePromotionMemberNew::getCurrentLearningStage, FtbCultivatePromotionMemberNew::getPromotionPostId); + wrapper.in(FtbCultivatePromotionMemberNew::getPromotionId, promotionIds); + wrapper.in(FtbCultivatePromotionMemberNew::getUserId, userIds); + List members = ftbCultivatePromotionMemberNewService.list(wrapper); + return members.stream().collect(Collectors.groupingBy(m -> m.getUserId() + "-" + m.getPromotionId())); + } + + private void enrichSingleRecord(V2CultivatePromotionPersonStatisticVo record, PersonStatisticsDataDto data) { + UserBoundVO userBound = data.userBoundMap.get(record.getUserId()); + if (userBound == null) return; + + record.setFullName(userBound.getUserName()); + record.setOrganizeName(userBound.getOrganizeName()); + record.setOrganizeId(userBound.getOrganizeId()); + record.setSysEmployeeID(userBound.getSystemWorkerId()); + record.setEmployeeSCurrentPosition(buildCurrentPosition(userBound)); + + List posts = data.postGroupMap.get(record.getPromotionId()); + if (CollUtil.isNotEmpty(posts)) { + record.setNumberOfMapPositions(posts.size()); + } + + List members = data.memberMap.get(record.getUserId() + "-" + record.getPromotionId()); + List completePositions = data.completePositionMap.get(record.getUserId()); + + if (CollUtil.isNotEmpty(members) && CollUtil.isNotEmpty(completePositions)) { + record.setPositionLearned(buildLearnedPositions(members, completePositions, data)); + } + } + + private String buildCurrentPosition(UserBoundVO userBound) { + StringBuilder sb = new StringBuilder(); + if (StringUtils.isNotEmpty(userBound.getPositionName())) { + sb.append(userBound.getPositionName()); + } + if (StringUtils.isNotEmpty(userBound.getGradeName())) { + if (sb.length() > 0) sb.append("_"); + sb.append(userBound.getGradeName()); + } + return sb.toString(); + } + + private String buildLearnedPositions(List members, List completePositions, PersonStatisticsDataDto data) { + StringBuilder learnedPositions = new StringBuilder(); + Set completedKeys = completePositions.stream().map(log -> StringUtils.isEmpty(log.getGradeId()) ? log.getPostId() : log.getPostId() + "-" + log.getGradeId()).collect(Collectors.toSet()); + + for (FtbCultivatePromotionMemberNew member : members) { + FtbCultivatePromotionPostNew post = data.postMap.get(member.getPromotionPostId()); + if (post == null) continue; + + String postKey = StringUtils.isEmpty(post.getGradeId()) ? post.getPostId() : post.getPostId() + "-" + post.getGradeId(); + if (completedKeys.contains(postKey)) { + PositionVO position = data.positionMap.get(post.getPostId()); + if (position != null) { + StringBuilder sb = new StringBuilder(position.getFullName()); + if (StringUtils.isNotEmpty(post.getGradeId())) { + GradeVO grade = data.gradeMap.get(post.getGradeId()); + if (grade != null) { + sb.append("|").append(grade.getFullName()); + } + } + learnedPositions.append(sb).append(" "); + } + } + } + return learnedPositions.toString().trim(); + } + + /** + * 根据用户ID查询用户的所有学习地图及分阶段岗位信息 + * + * @param userId 用户ID + * @return List 用户学习地图详情列表 + */ + @Override + public UserPromotionDetailVo getUserPromotionDetail(String userId, String promotionId) { + // 1. 获取指定学习地图信息 + MyCultivatePromotionListVo promotion = ftbCultivatePromotionUserService.queryMyPromotionById(userId, promotionId); + if (promotion == null) { + return null; + } + + // 2. 查询该学习地图的岗位配置信息 + LambdaQueryWrapper postWrapper = Wrappers.lambdaQuery(); + postWrapper.eq(FtbCultivatePromotionPostNew::getPromotionId, promotionId); + postWrapper.eq(FtbCultivatePromotionPostNew::getDeleteMark, 0); + List allPostList = ftbCultivatePromotionPostNewService.list(postWrapper); + + // 3. 按level分组岗位配置 + Map> levelPostMap = new HashMap<>(); + if (CollUtil.isNotEmpty(allPostList)) { + levelPostMap = allPostList.stream() + .collect(Collectors.groupingBy(FtbCultivatePromotionPostNew::getLevel)); + } + + // 4. 收集所有需要的岗位ID和职级ID + Set allPostIds = new HashSet<>(); + Set allGradeIds = new HashSet<>(); + for (FtbCultivatePromotionPostNew post : allPostList) { + allPostIds.add(post.getPostId()); + if (StringUtils.isNotEmpty(post.getGradeId())) { + allGradeIds.add(post.getGradeId()); + } + } + + // 5. 批量查询岗位和职级信息 + Map positionMap = userApiV2Util.listPositionDetailInfoByIdsReturnMap(new ArrayList<>(allPostIds), null); + Map gradeMap = userApiV2Util.listGradeByIdsReturnMap(new ArrayList<>(allGradeIds), null); + + // 6. 组装返回结果 + UserPromotionDetailVo detailVo = new UserPromotionDetailVo(); + detailVo.setId(promotion.getId()); + detailVo.setPromotion(promotion.getPromotion()); + detailVo.setPromotionBusinessId(promotion.getPromotionBusinessId()); + + // 7. 组装该学习地图的分阶段岗位信息 + List phaseList = new ArrayList<>(); + + if (CollUtil.isNotEmpty(levelPostMap)) { + // 按阶段编号排序 + List sortedLevels = levelPostMap.keySet().stream() + .sorted() + .collect(Collectors.toList()); + + for (Integer level : sortedLevels) { + PromotionPhasePositionVo phaseVo = new PromotionPhasePositionVo(); + phaseVo.setLevel(level); + + List postList = levelPostMap.get(level); + List positionVoList = new ArrayList<>(); + + for (FtbCultivatePromotionPostNew post : postList) { + V2CultivatePostAndGradeVo vo = new V2CultivatePostAndGradeVo(); + vo.setId(post.getId()); + vo.setPostId(post.getPostId()); + + PositionVO positionVO = positionMap.get(post.getPostId()); + if (positionVO != null) { + vo.setPostName(positionVO.getFullName()); + } + + if (StringUtils.isNotEmpty(post.getGradeId())) { + vo.setGradeId(post.getGradeId()); + GradeVO gradeVO = gradeMap.get(post.getGradeId()); + if (gradeVO != null) { + vo.setGradeName(gradeVO.getFullName()); + } + } + + positionVoList.add(vo); + } + + phaseVo.setPositionList(positionVoList); + phaseList.add(phaseVo); + } + } + + detailVo.setPhaseList(phaseList); + return detailVo; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateQuestionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateQuestionServiceImpl.java new file mode 100644 index 0000000..e92a423 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateQuestionServiceImpl.java @@ -0,0 +1,252 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.mapper.FtbCultivateQuestionMapper; +import jnpf.cultivate.service.FtbCultivateQuestionOptionService; +import jnpf.cultivate.v2.service.V2CultivateQuestionService; +import jnpf.cultivate.v2.util.V2QuestionExcelExportUtil; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.po.question.FtbCultivateQuestionOption; +import jnpf.model.cultivate.req.questionbank.QuestionOptionReq; +import jnpf.model.cultivate.resp.QuestionOptionVo; +import jnpf.model.cultivate.resp.QuestionVo; +import jnpf.model.cultivate.v2.question.req.*; +import jnpf.model.enums.CourseEnums; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class V2CultivateQuestionServiceImpl extends SuperServiceImpl implements V2CultivateQuestionService { + + /** + * 题目选项服务 + */ + @Autowired + private FtbCultivateQuestionOptionService questionOptionService; + + @Override + @Transactional + public void batchAddQuestion(BatchAddQuestionReq batchAddQuestionReq) throws Exception { + preCheckParam(batchAddQuestionReq); + String questionBankId = batchAddQuestionReq.getQuestionBankId(); + List questionList = new ArrayList<>(); + List questionOptionList = new ArrayList<>(); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbCultivateQuestion::getContent) + .eq(FtbCultivateQuestion::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); + List existQuestionList = baseMapper.selectList(wrapper); + + List questionContentList = new ArrayList<>(); + if(CollUtil.isNotEmpty(existQuestionList)) { + for (FtbCultivateQuestion question : existQuestionList) { + questionContentList.add(question.getContent()); + } + } + //检查题目是否重复 + for (V2AddQuestionReq req : batchAddQuestionReq.getQuestionList()) { + if(questionContentList.contains(req.getContent())){ + throw new RuntimeException("题目已经存在"); + } + //1、添加题目信息 + FtbCultivateQuestion question = req.convertData(questionBankId); + questionList.add(question); + // ftbCultivateQuestionMapper.insert(question); + //2、添加题目选项信息 + if (CourseEnums.QuestionType.INPUT.getCode().equals(question.getType()) || CourseEnums.QuestionType.COMBINE.getCode().equals(question.getType())) { + continue; + } + List optionList = req.getOptionList(); + if (CollUtil.isEmpty(optionList)) { + continue; + } + for (QuestionOptionReq option : optionList) { + questionOptionList.add(option.convertData(question.getId())); + } + // questionOptionService.saveBatch(questionOptionList); + log.info(questionOptionList.toString()); + //3、写入正确答案 + FtbCultivateQuestion questionHasAnswer = writeRightAnswer(question, questionOptionList); + question.setAnswer(questionHasAnswer.getAnswer()); + + } + if (questionList.isEmpty()) { + return; + } + this.saveBatch(questionList); + if (!questionOptionList.isEmpty()) { + questionOptionService.saveBatch(questionOptionList); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateData(V2EditQuestionReq req) throws Exception { + //1、检测题目信息 + FtbCultivateQuestion question = this.baseMapper.selectById(req.getId()); + if (null == question) { + throw new RuntimeException("题目不存在"); + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("题目已经被删除,不能操作"); + } + //1、修改题目信息 + question.setType(req.getType()); + question.setDifficulty(req.getDifficulty()); + question.setContent(req.getContent()); + question.setAnalysis(req.getAnalysis()); + question.setAnswer(req.getAnswer()); + questionOptionService.remove(new QueryWrapper().eq("F_QuestionId", question.getId())); + //2、添加题目选项信息 + if (!CourseEnums.QuestionType.INPUT.getCode().equals(question.getType()) && !CourseEnums.QuestionType.COMBINE.getCode().equals(question.getType())) { + List optionList = req.getOptionList(); + if (CollUtil.isNotEmpty(optionList)) { + List questionOptionList = new ArrayList<>(); + for (QuestionOptionReq option : optionList) { + questionOptionList.add(option.convertData(question.getId())); + } + questionOptionService.saveBatch(questionOptionList); + //3、写入正确答案 + FtbCultivateQuestion questionHasAnswer = writeRightAnswer(question, questionOptionList); + question.setAnswer(questionHasAnswer.getAnswer()); + } + } + this.baseMapper.updateById(question); + } + + @Override + public QuestionVo getInfo(String id) { + //1、检测题目信息 + FtbCultivateQuestion question = this.baseMapper.selectById(id); + if (null == question) { + throw new RuntimeException("题目不存在"); + } + if (question.getEnabledMark().equals(CourseEnums.EnabledMarkType.INVALID.getCode())) { + throw new RuntimeException("题目已经被删除,不能操作"); + } + //2、题目po转vo + QuestionVo vo = BeanUtil.copyProperties(question, QuestionVo.class); + //3、查询题目的选项信息 + if (!CourseEnums.QuestionType.INPUT.getCode().equals(question.getType()) + && !CourseEnums.QuestionType.COMBINE.getCode().equals(question.getType())) { + List questionOptionList = queryOptionListForQuestionId(id); + if (CollUtil.isNotEmpty(questionOptionList)) { + vo.setQuestionOptionVoList(BeanUtil.copyToList(questionOptionList, QuestionOptionVo.class)); + } + } + return vo; + } + + @Override + public V2ImportQuestionResultVo realImportData(String questionBankId, List allQuestionList) { + List failList = new ArrayList<>(); + List successList = new ArrayList<>(); + allQuestionList.forEach(req -> { + V2AddQuestionReq addQuestionReq = BeanUtil.copyProperties(req, V2AddQuestionReq.class); + try { + insertData(questionBankId, addQuestionReq); + successList.add(addQuestionReq); + } catch (Exception e) { + log.error("导入题目失败", e); + failList.add(addQuestionReq); + } + }); + return V2ImportQuestionResultVo.builder().successList(successList).failList(failList).build(); + } + + public void insertData(String questionBankId, V2AddQuestionReq req) throws Exception { + //1、添加题目信息 + FtbCultivateQuestion question = req.convertData(questionBankId); + this.baseMapper.insert(question); + //2、添加题目选项信息 + if (CourseEnums.QuestionType.INPUT.getCode().equals(question.getType()) || CourseEnums.QuestionType.COMBINE.getCode().equals(question.getType())) { + return; + } + List optionList = req.getOptionList(); + if (CollUtil.isEmpty(optionList)) { + return; + } + List questionOptionList = new ArrayList<>(); + for (QuestionOptionReq option : optionList) { + questionOptionList.add(option.convertData(question.getId())); + } + questionOptionService.saveBatch(questionOptionList); + log.info(questionOptionList.toString()); + //3、写入正确答案 + FtbCultivateQuestion questionHasAnswer = writeRightAnswer(question, questionOptionList); + question.setAnswer(questionHasAnswer.getAnswer()); + this.baseMapper.updateById(question); + } + + + /** + * 根据题目ID查 题目选项列表 + * + * @param questionId 题目ID + */ + private List queryOptionListForQuestionId(String questionId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbCultivateQuestionOption::getQuestionId, questionId) + .eq(FtbCultivateQuestionOption::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()) + .orderByAsc(FtbCultivateQuestionOption::getSortCode); + return questionOptionService.list(wrapper); + } + + /** + * 写入正确答案 + * + * @param question 题目信息 + * @param questionOptionList 题目选项信息 + */ + private FtbCultivateQuestion writeRightAnswer(FtbCultivateQuestion question, List questionOptionList) throws Exception { + List rightAnswerList = new ArrayList<>(); + if (CourseEnums.QuestionType.SINGLE.getCode().equals(question.getType()) + || CourseEnums.QuestionType.MULTI.getCode().equals(question.getType()) + || CourseEnums.QuestionType.JUDGE.getCode().equals(question.getType()) + || CourseEnums.QuestionType.ONE_OR_MULTI.getCode().equals(question.getType()) + ) { + for (FtbCultivateQuestionOption questionOption : questionOptionList) { + if (CourseEnums.IsRightAnswer.YES.getCode().equals(questionOption.getIsRightOption())) { + rightAnswerList.add(questionOption.getId()); + } + } + } + if (CourseEnums.QuestionType.FILL.getCode().equals(question.getType())) { + for (FtbCultivateQuestionOption questionOption : questionOptionList) { + rightAnswerList.add(questionOption.getId()); + } + } + if (rightAnswerList.isEmpty()) { + throw new Exception("请设置正确答案"); + } + FtbCultivateQuestion updateQuestion = new FtbCultivateQuestion(); + updateQuestion.setAnswer(String.join(",", rightAnswerList)); + updateQuestion.setId(question.getId()); + // ftbCultivateQuestionMapper.updateById(updateQuestion); + return updateQuestion; + } + + private void preCheckParam(BatchAddQuestionReq req) { + if (req.getQuestionBankId() == null) { + throw new RuntimeException("题库ID不能为空"); + } + if (CollUtil.isEmpty(req.getQuestionList())) { + throw new RuntimeException("题目列表不能为空"); + } + for (V2AddQuestionReq question : req.getQuestionList()) { + V2QuestionExcelExportUtil.checkAddQuestionParam(question); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskCountServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskCountServiceImpl.java new file mode 100644 index 0000000..03dbd12 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskCountServiceImpl.java @@ -0,0 +1,634 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivateLearnTaskAssignmentService; +import jnpf.cultivate.service.FtbCultivateLearnTaskCourseService; +import jnpf.cultivate.v2.service.V2CultivateTaskCountService; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskCountReq; +import jnpf.model.cultivate.v2.task.vo.V2CultivateTaskCountVo; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * V2培训任务统计服务实现 + * + * @author system + * @create 2026-05-06 + */ +@Slf4j +@Service +public class V2CultivateTaskCountServiceImpl implements V2CultivateTaskCountService { + + @Autowired + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + @Autowired + private FtbCultivateLearnTaskCourseMapper ftbCultivateLearnTaskCourseMapper; + + @Autowired + private FtbCultivateLearnTaskExamMapper ftbCultivateLearnTaskExamMapper; + + @Autowired + private FtbCultivateLearnTaskIdentificationMapper ftbCultivateLearnTaskIdentificationMapper; + + @Autowired + private FtbCultivateLearnTaskCertificateMapper ftbCultivateLearnTaskCertificateMapper; + + @Autowired + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + @Autowired + private FtbCultivateTaskLogMapper ftbCultivateTaskLogMapper; + + @Autowired + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Autowired + private FtbCultivateLearnTaskCourseService ftbCultivateLearnTaskCourseService; + + @Autowired + private FtbCultivateLearnTaskAssignmentService ftbCultivateLearnTaskAssignmentService; + + @Autowired + private jnpf.cultivate.mapper.FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Autowired + private jnpf.cultivate.mapper.FtbCultivateExamMapper ftbCultivateExamMapper; + + @Autowired + private jnpf.cultivate.mapper.CultivateIdentifyTableMapper cultivateIdentifyTableMapper; + + @Autowired + private jnpf.cultivate.mapper.FtbCultivateCertificateMapper ftbCultivateCertificateMapper; + + @Override + public PageInfo queryTaskCountList(V2CultivateTaskCountReq req) { + // 1. 查询任务列表 + Page page = ftbCultivateLearnTaskMapper.queryV2TaskCountList( + Page.of(req.getCurrentPage(), req.getPageSize()), + req.getTaskName(), + req.getStatus(), + req.getCreatorTimeStart(), + req.getCreatorTimeEnd() + ); + + List records = page.getRecords(); + if (CollUtil.isEmpty(records)) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPageSize((int) page.getSize()); + pageInfo.setPageNum((int) page.getCurrent()); + pageInfo.setTotal((int) page.getTotal()); + return pageInfo; + } + + // 2. 提取任务ID列表 + List taskIds = records.stream() + .map(V2CultivateTaskCountVo::getId) + .collect(Collectors.toList()); + + // 3. 批量查询各项统计数据 + Map courseNumMap = getCourseNumMap(taskIds); + Map> examNamesMap = getExamNamesMap(taskIds); + Map> identificationNamesMap = getIdentificationNamesMap(taskIds); + Map> certificateNamesMap = getCertificateNamesMap(taskIds); + Map assignedUserCountMap = getAssignedUserCountMap(taskIds); + Map completedUserCountMap = getCompletedUserCountMap(taskIds); + Map issuedCertificateCountMap = getIssuedCertificateCountMap(taskIds); + + // 4. 从任务日志中计算完课率、考试和鉴定相关指标 + Map taskLogStatsMap = getTaskLogStatisticsMap(taskIds); + + // 5. 填充统计数据 + for (V2CultivateTaskCountVo vo : records) { + String taskId = vo.getId(); + + // 课程数量 + vo.setCourseNum(courseNumMap.getOrDefault(taskId, 0)); + + // 考试名称列表 + vo.setExamNames(examNamesMap.getOrDefault(taskId, new ArrayList<>())); + + // 鉴定表名称列表 + vo.setIdentificationNames(identificationNamesMap.getOrDefault(taskId, new ArrayList<>())); + + // 证书名称列表 + vo.setCertificateNames(certificateNamesMap.getOrDefault(taskId, new ArrayList<>())); + + // 应完成人数 + vo.setAssignedUserCount(assignedUserCountMap.getOrDefault(taskId, 0)); + + // 任务完成人数 + vo.setCompletedUserCount(completedUserCountMap.getOrDefault(taskId, 0)); + + // 任务完成率 + Integer assignedCount = vo.getAssignedUserCount(); + Integer completedCount = vo.getCompletedUserCount(); + vo.setTaskCompleteRate(calculateRate(completedCount, assignedCount)); + + // 已颁发证书数量 + vo.setIssuedCertificateCount(issuedCertificateCountMap.getOrDefault(taskId, 0)); + + // 从任务日志中获取统计数据 + V2TaskLogStatistics stats = taskLogStatsMap.get(taskId); + if (stats != null) { + // 完课率 + vo.setCompleteCourseRate(calculateRate(stats.getCompleteCourseUserCount(), assignedCount)); + + // 参与考试人数 + vo.setExamParticipantCount(stats.getExamParticipantCount()); + + // 考试合格率 + vo.setExamPassRate(calculateRate(stats.getExamPassCount(), stats.getExamParticipantCount())); + + // 鉴定人数 + vo.setIdentifyParticipantCount(stats.getIdentifyParticipantCount()); + + // 鉴定合格率 + vo.setIdentifyPassRate(calculateRate(stats.getIdentifyPassCount(), stats.getIdentifyParticipantCount())); + } else { + vo.setCompleteCourseRate("0%"); + vo.setExamParticipantCount(0); + vo.setExamPassRate("0%"); + vo.setIdentifyParticipantCount(0); + vo.setIdentifyPassRate("0%"); + } + } + + // 6. 构建分页结果 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) page.getSize()); + pageInfo.setPageNum((int) page.getCurrent()); + pageInfo.setTotal((int) page.getTotal()); + + return pageInfo; + } + + /** + * 获取每个任务的课程数量(未删除、上架状态) + */ + private Map getCourseNumMap(List taskIds) { + Map result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + // 1. 查询任务关联的课程(未删除) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateLearnTaskCourse::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskCourse::getEnableMark, 0); + + List taskCourses = ftbCultivateLearnTaskCourseMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskCourses)) { + return result; + } + + // 2. 提取所有课程ID + List courseIds = taskCourses.stream() + .map(FtbCultivateLearnTaskCourse::getCourseId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(courseIds)) { + return result; + } + + // 3. 查询课程表,过滤出上架且未删除的课程 + LambdaQueryWrapper courseWrapper = Wrappers.lambdaQuery(); + courseWrapper.in(jnpf.model.cultivate.po.course.FtbCultivateCourse::getId, courseIds) + .eq(jnpf.model.cultivate.po.course.FtbCultivateCourse::getIsGroundIng, 1) // 1-上架 + .eq(jnpf.model.cultivate.po.course.FtbCultivateCourse::getEnableMark, 0); // 0-有效 + + List validCourses = ftbCultivateCourseMapper.selectList(courseWrapper); + Set validCourseIds = validCourses.stream() + .map(jnpf.model.cultivate.po.course.FtbCultivateCourse::getId) + .collect(Collectors.toSet()); + + // 4. 统计每个任务的有效课程数量 + result = taskCourses.stream() + .filter(tc -> validCourseIds.contains(tc.getCourseId())) + .collect(Collectors.groupingBy( + FtbCultivateLearnTaskCourse::getTaskId, + Collectors.collectingAndThen(Collectors.counting(), Long::intValue) + )); + + return result; + } + + /** + * 获取每个任务的考试名称列表(去重) + */ + private Map> getExamNamesMap(List taskIds) { + Map> result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + // 1. 查询任务关联的考试(未删除) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateLearnTaskExam::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskExam::getEnableMark, 0); + + List taskExams = ftbCultivateLearnTaskExamMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskExams)) { + return result; + } + + // 2. 提取所有考试ID(去重) + List examIds = taskExams.stream() + .map(FtbCultivateLearnTaskExam::getExamId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(examIds)) { + return result; + } + + // 3. 查询考试表,获取考试名称 + LambdaQueryWrapper examWrapper = Wrappers.lambdaQuery(); + examWrapper.in(jnpf.model.cultivate.po.exam.FtbCultivateExam::getId, examIds) + .eq(jnpf.model.cultivate.po.exam.FtbCultivateExam::getEnabledMark, 1); // 1-有效 + + List exams = ftbCultivateExamMapper.selectList(examWrapper); + Map examNameMap = exams.stream() + .filter(e -> StringUtils.isNotBlank(e.getExamName())) + .collect(Collectors.toMap( + jnpf.model.cultivate.po.exam.FtbCultivateExam::getId, + jnpf.model.cultivate.po.exam.FtbCultivateExam::getExamName, + (v1, v2) -> v1 // 如果有重复,保留第一个 + )); + + // 4. 按任务ID分组,收集考试名称 + Map> taskExamMap = new HashMap<>(); + for (FtbCultivateLearnTaskExam taskExam : taskExams) { + String examName = examNameMap.get(taskExam.getExamId()); + if (StringUtils.isNotBlank(examName)) { + taskExamMap.computeIfAbsent(taskExam.getTaskId(), k -> new ArrayList<>()) + .add(examName); + } + } + + // 5. 对每个任务的考试名称去重 + taskExamMap.forEach((taskId, names) -> { + List distinctNames = names.stream().distinct().collect(Collectors.toList()); + result.put(taskId, distinctNames); + }); + + return result; + } + + /** + * 获取每个任务的鉴定表名称列表(去重) + */ + private Map> getIdentificationNamesMap(List taskIds) { + Map> result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + // 1. 查询任务关联的鉴定(未删除) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateLearnTaskIdentification::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskIdentification::getEnableMark, 0); + + List taskIdentifications = ftbCultivateLearnTaskIdentificationMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskIdentifications)) { + return result; + } + + // 2. 提取所有鉴定ID(去重) + List identifyIds = taskIdentifications.stream() + .map(FtbCultivateLearnTaskIdentification::getIdentificationId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(identifyIds)) { + return result; + } + + // 3. 查询鉴定表,获取鉴定名称 + LambdaQueryWrapper identifyWrapper = Wrappers.lambdaQuery(); + identifyWrapper.in(jnpf.entity.cultivate.CultivateIdentifyTable::getId, identifyIds) + .eq(jnpf.entity.cultivate.CultivateIdentifyTable::getDeleteMark, 0); // 0-未删除 + + List identifies = cultivateIdentifyTableMapper.selectList(identifyWrapper); + Map identifyNameMap = identifies.stream() + .filter(i -> StringUtils.isNotBlank(i.getName())) + .collect(Collectors.toMap( + jnpf.entity.cultivate.CultivateIdentifyTable::getId, + jnpf.entity.cultivate.CultivateIdentifyTable::getName, + (v1, v2) -> v1 + )); + + // 4. 按任务ID分组,收集鉴定表名称 + Map> taskIdentifyMap = new HashMap<>(); + for (FtbCultivateLearnTaskIdentification taskIdentify : taskIdentifications) { + String identifyName = identifyNameMap.get(taskIdentify.getIdentificationId()); + if (StringUtils.isNotBlank(identifyName)) { + taskIdentifyMap.computeIfAbsent(taskIdentify.getTaskId(), k -> new ArrayList<>()) + .add(identifyName); + } + } + + // 5. 对每个任务的鉴定表名称去重 + taskIdentifyMap.forEach((taskId, names) -> { + List distinctNames = names.stream().distinct().collect(Collectors.toList()); + result.put(taskId, distinctNames); + }); + + return result; + } + + /** + * 获取每个任务的证书名称列表(去重) + */ + private Map> getCertificateNamesMap(List taskIds) { + Map> result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + // 1. 查询任务关联的证书(未删除) + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateLearnTaskCertificate::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskCertificate::getEnableMark, 0); + + List taskCertificates = ftbCultivateLearnTaskCertificateMapper.selectList(wrapper); + if (CollUtil.isEmpty(taskCertificates)) { + return result; + } + + // 2. 提取所有证书ID(去重) + List certificateIds = taskCertificates.stream() + .map(FtbCultivateLearnTaskCertificate::getCertificateId) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (CollUtil.isEmpty(certificateIds)) { + return result; + } + + // 3. 查询证书表,获取证书名称 + LambdaQueryWrapper certWrapper = Wrappers.lambdaQuery(); + certWrapper.in(jnpf.model.cultivate.po.certificate.FtbCertificateEntity::getId, certificateIds) + .eq(jnpf.model.cultivate.po.certificate.FtbCertificateEntity::getEnabledMark, 1); // 1-有效 + + List certificates = ftbCultivateCertificateMapper.selectList(certWrapper); + Map certNameMap = certificates.stream() + .filter(c -> StringUtils.isNotBlank(c.getName())) + .collect(Collectors.toMap( + jnpf.model.cultivate.po.certificate.FtbCertificateEntity::getId, + jnpf.model.cultivate.po.certificate.FtbCertificateEntity::getName, + (v1, v2) -> v1 + )); + + // 4. 按任务ID分组,收集证书名称 + Map> taskCertMap = new HashMap<>(); + for (FtbCultivateLearnTaskCertificate taskCert : taskCertificates) { + String certName = certNameMap.get(taskCert.getCertificateId()); + if (StringUtils.isNotBlank(certName)) { + taskCertMap.computeIfAbsent(taskCert.getTaskId(), k -> new ArrayList<>()) + .add(certName); + } + } + + // 5. 对每个任务的证书名称去重 + taskCertMap.forEach((taskId, names) -> { + List distinctNames = names.stream().distinct().collect(Collectors.toList()); + result.put(taskId, distinctNames); + }); + + return result; + } + + /** + * 获取每个任务的分配用户数 + */ + private Map getAssignedUserCountMap(List taskIds) { + Map result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + + List assignments = ftbCultivateLearnTaskAssignmentMapper.selectList(wrapper); + + if (CollUtil.isNotEmpty(assignments)) { + result = assignments.stream() + .collect(Collectors.groupingBy( + FtbCultivateLearnTaskAssignment::getTaskId, + Collectors.collectingAndThen(Collectors.counting(), Long::intValue) + )); + } + + return result; + } + + /** + * 获取每个任务的完成用户数 + */ + private Map getCompletedUserCountMap(List taskIds) { + Map result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds) + .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) + .eq(FtbCultivateLearnTaskAssignment::getStudyStats, 2); // 2-已完成 + + List assignments = ftbCultivateLearnTaskAssignmentMapper.selectList(wrapper); + + if (CollUtil.isNotEmpty(assignments)) { + result = assignments.stream() + .collect(Collectors.groupingBy( + FtbCultivateLearnTaskAssignment::getTaskId, + Collectors.collectingAndThen(Collectors.counting(), Long::intValue) + )); + } + + return result; + } + + /** + * 获取每个任务的已颁发证书数量 + */ + private Map getIssuedCertificateCountMap(List taskIds) { + Map result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateTaskLog::getTaskId, taskIds) + .eq(FtbCultivateTaskLog::getEnabledMark, 0) + .eq(FtbCultivateTaskLog::getIssuedCertificate, 1); // 1-已颁发 + + List logs = ftbCultivateTaskLogMapper.selectList(wrapper); + + if (CollUtil.isNotEmpty(logs)) { + result = logs.stream() + .collect(Collectors.groupingBy( + FtbCultivateTaskLog::getTaskId, + Collectors.collectingAndThen(Collectors.counting(), Long::intValue) + )); + } + + return result; + } + + /** + * 从任务日志中获取统计信息 + */ + private Map getTaskLogStatisticsMap(List taskIds) { + Map result = new HashMap<>(); + if (CollUtil.isEmpty(taskIds)) { + return result; + } + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbCultivateTaskLog::getTaskId, taskIds) + .eq(FtbCultivateTaskLog::getEnabledMark, 0); + + List logs = ftbCultivateTaskLogMapper.selectList(wrapper); + + if (CollUtil.isEmpty(logs)) { + return result; + } + + // 按任务ID分组统计 + Map> taskLogMap = logs.stream() + .collect(Collectors.groupingBy(FtbCultivateTaskLog::getTaskId)); + + for (Map.Entry> entry : taskLogMap.entrySet()) { + String taskId = entry.getKey(); + List taskLogs = entry.getValue(); + + V2TaskLogStatistics stats = new V2TaskLogStatistics(); + + // 统计必修课程完成情况 + // 注意:这里需要从任务课程表中获取必修课程列表,然后判断哪些用户完成了所有必修课程 + // 简化处理:统计courseState=1的记录数 + int completeCourseUsers = (int) taskLogs.stream() + .filter(log -> log.getCourseState() != null && log.getCourseState() == 1) + .map(FtbCultivateTaskLog::getUserId) + .distinct() + .count(); + stats.setCompleteCourseUserCount(completeCourseUsers); + + // 统计参与考试人次(examState != -1 表示已触发考试) + int examParticipants = (int) taskLogs.stream() + .filter(log -> log.getExamState() != null && log.getExamState() != -1) + .count(); + stats.setExamParticipantCount(examParticipants); + + // 统计考试合格人次(examResult = 3合格 或 5优秀) + int examPassCount = (int) taskLogs.stream() + .filter(log -> log.getExamResult() != null && + (log.getExamResult() == 3 || log.getExamResult() == 5)) + .count(); + stats.setExamPassCount(examPassCount); + + // 统计参与鉴定人次(identifyState != -1 表示已触发鉴定) + int identifyParticipants = (int) taskLogs.stream() + .filter(log -> log.getIdentifyState() != null && log.getIdentifyState() != -1) + .count(); + stats.setIdentifyParticipantCount(identifyParticipants); + + // 统计鉴定合格人次(identifyResult = 0合格 或 1优秀) + int identifyPassCount = (int) taskLogs.stream() + .filter(log -> log.getIdentifyResult() != null && + (log.getIdentifyResult() == 0 || log.getIdentifyResult() == 1)) + .count(); + stats.setIdentifyPassCount(identifyPassCount); + + result.put(taskId, stats); + } + + return result; + } + + /** + * 计算百分比 + */ + private String calculateRate(Integer numerator, Integer denominator) { + if (numerator == null || numerator == 0 || denominator == null || denominator == 0) { + return "0%"; + } + int rate = Math.round((numerator / (float) denominator) * 100); + return rate + "%"; + } + + /** + * 任务日志统计数据内部类 + */ + private static class V2TaskLogStatistics { + private int completeCourseUserCount = 0; + private int examParticipantCount = 0; + private int examPassCount = 0; + private int identifyParticipantCount = 0; + private int identifyPassCount = 0; + + public int getCompleteCourseUserCount() { + return completeCourseUserCount; + } + + public void setCompleteCourseUserCount(int completeCourseUserCount) { + this.completeCourseUserCount = completeCourseUserCount; + } + + public int getExamParticipantCount() { + return examParticipantCount; + } + + public void setExamParticipantCount(int examParticipantCount) { + this.examParticipantCount = examParticipantCount; + } + + public int getExamPassCount() { + return examPassCount; + } + + public void setExamPassCount(int examPassCount) { + this.examPassCount = examPassCount; + } + + public int getIdentifyParticipantCount() { + return identifyParticipantCount; + } + + public void setIdentifyParticipantCount(int identifyParticipantCount) { + this.identifyParticipantCount = identifyParticipantCount; + } + + public int getIdentifyPassCount() { + return identifyPassCount; + } + + public void setIdentifyPassCount(int identifyPassCount) { + this.identifyPassCount = identifyPassCount; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskServiceImpl.java new file mode 100644 index 0000000..64df372 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskServiceImpl.java @@ -0,0 +1,2190 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.*; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.*; +import jnpf.cultivate.v2.service.*; +import jnpf.cultivate.v2.util.CultivateBatchQueryUtil; +import jnpf.cultivate.v2.util.CultivateCourseStudyUtil; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetails; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.enums.cultivate.StudyStatsEnum; +import jnpf.enums.cultivate.task.TaskAssignmentRuleEnum; +import jnpf.enums.cultivate.v2.LearnTaskStatusEnum; +import jnpf.exception.DataException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.dto.learn.FtbCultivateLearnAllocationDTO; +import jnpf.model.cultivate.dto.learn.NeedAlertUserDto; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.common.FtbCultivateCoverInfoEntity; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.*; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPhase; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPractice; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterVo; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.position.req.V2CultivateCoursePageReq; +import jnpf.model.cultivate.v2.position.vo.V2CultivateJobLearnCourseVo; +import jnpf.model.cultivate.v2.statistics.V2UserLearningStatusResult; +import jnpf.model.cultivate.v2.task.req.V2CultivateLearnTaskListForManagerReq; +import jnpf.model.cultivate.v2.task.req.V2CultivateTaskSaveReq; +import jnpf.model.cultivate.v2.task.req.V2MyCultivateTaskListForReq; +import jnpf.model.cultivate.v2.task.vo.*; +import jnpf.model.cultivate.vo.learn.info.TimeDifference; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJobTenureDTO; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJoeInfo; +import jnpf.model.personnels.vo.roster.FtbPersonnlesJobTenureVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.FtbPersonneApi; +import jnpf.personnels.service.FtbPersonnelsMetaDataService; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class V2CultivateTaskServiceImpl implements V2CultivateTaskService { + + @Autowired + @Lazy + private UserApiV2Util userApiV2Util; + + @Autowired + @Lazy + private CultivateImUtil cultivateImUtil; + + @Autowired + @Lazy + private CultivateLearnTaskIMUtils captchaImUtil; + + @Autowired + private FtbCultivateLearnTaskService ftbCultivateLearnTaskService; + + @Autowired + private FtbCultivateLearnTaskAssignmentMapper ftbCultivateLearnTaskAssignmentMapper; + + + @Autowired + private FtbPersonnelsMetaDataService ftbPersonnelsMetaDataService; + + @Autowired + private FtbCultivateLearnTaskReminderRuleMapper ftbCultivateLearnTaskReminderRuleMapper; + + + @Autowired + private FtbCultivateLearnTaskPhaseMapper ftbCultivateLearnTaskPhaseMapper; + + + @Autowired + private CultivateIdentifyApplyMapper identifyApplyMapper; + + @Autowired + private CultivateIdentifyApplyDetailsMapper identifyApplyDetailsMapper; + + @Autowired + private FtbCultivateLearnTaskAssignmentService ftbCultivateLearnTaskAssignmentService; + + @Autowired + private FtbCultivateLearnTaskPhaseService ftbCultivateLearnTaskPhaseService; + + @Autowired + private FtbCultivateLearnTaskCourseService ftbCultivateLearnTaskCourseService; + + @Autowired + private FtbCultivateLearnTaskExamService ftbCultivateLearnTaskExamService; + + @Autowired + private FtbCultivateLearnTaskIdentificationService ftbCultivateLearnTaskIdentificationService; + + @Autowired + private FtbCultivateLearnTaskCertificateService ftbCultivateLearnTaskCertificateService; + + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + @Autowired + private FtbCultivateTaskLogService ftbCultivateTaskLogService; + + @Autowired + private FtbCultivateLearnTaskPracticeService ftbCultivateLearnTaskPracticeService; + + @Autowired + private FtbCultivateLearnTaskMapper ftbCultivateLearnTaskMapper; + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + @Autowired + private CultivateTaskLeanAsyncDealUtil cultivateTaskLeanAsyncDealUtil; + + @Autowired + private CultivateCoverInfoMapper cultivateCoverInfoMapper; + + @Autowired + private FtbCultivateCourseMapper ftbCultivateCourseMapper; + + @Autowired + private FtbCultivateLearnCategoriesMapper ftbCultivateLearnCategoriesMapper; + + @Autowired + private CultivateCourseStudyUtil courseStudyUtil; + + @Autowired + private FtbPersonneApi ftbPersonneApi; + + @Autowired + private V2CultivateTaskStudyService v2CultivateTaskStudyService; + + /** + * 根据岗位和司龄筛选用户 + * + * @param positionIds 岗位 ID 列表(逗号分隔) + * @param minTenure 最小司龄要求 + * @return 符合条件的用户 ID 列表 + */ + private List filterUsersByPositionAndTenure(String positionIds, Integer minTenure) { + // 获取岗位下的所有用户 + List userBoundVOList = userApiV2Util.getUserListForPositions( + List.of(positionIds.split(",")), + null + ); + + if (CollUtil.isEmpty(userBoundVOList)) { + return new ArrayList<>(); + } + + // 转换为用户任职信息列表 + List joeInfoList = userBoundVOList.stream() + .map(user -> { + FtbPersonnlesJoeInfo info = new FtbPersonnlesJoeInfo(); + info.setUserId(user.getId()); + info.setPositionId(user.getPositionId()); + return info; + }) + .collect(Collectors.toList()); + + // 查询岗龄信息 + FtbPersonnlesJobTenureDTO tenureDTO = new FtbPersonnlesJobTenureDTO(); + tenureDTO.setUserInfo(joeInfoList); + tenureDTO.setTenantId(UserProvider.getUser().getTenantId()); + String objectToString = JsonUtil.getObjectToString( + tenureDTO + ); + log.error("查询岗位司龄信息:{}", objectToString); + + List tenureList = ftbPersonneApi.queryPositionTenureAge(tenureDTO); + + // 筛选满足司龄要求的用户 + if (CollUtil.isEmpty(tenureList)) { + return new ArrayList<>(); + } + + return tenureList.stream() + .filter(vo -> vo.getJobTenure() != null && vo.getJobTenure() >= minTenure) + .map(FtbPersonnlesJobTenureVO::getUserId) + .collect(Collectors.toList()); + } + + + /** + * 添加学习任务 + * + * @param req 添加学习任务所需的参数 + * @return 添加成功后的任务ID + */ + @Override + @Transactional + public String add(V2CultivateTaskSaveReq req) { + //参数校验 + req.beforeCheck(); + v2CultivateBatchQueryService.taskCheckAvailable(req); + // 任务名称不能重复 + if (queryTaskNumByName(req.getTaskName()) > 0) { + throw new RuntimeException("任务名称不能重复"); + } + return realDealTaskData(req); + } + + /** + * 学习任务数据 + * + * @param taskInfoDto 任务信息 + */ + private String realDealTaskData(V2CultivateTaskSaveReq taskInfoDto) { + // 任务分配规则(1,指定成员,2指定组织,3指定岗位,4自定义) + List userIds = new ArrayList<>(); + TaskAssignmentRuleEnum taskAssignmentRuleEnum = TaskAssignmentRuleEnum.getByCode(taskInfoDto.getAssignmentRule()); + switch (taskAssignmentRuleEnum) { + case SPECIFIED_MEMBER: + // 指定成员 + userIds = taskInfoDto.getUserIds(); + break; + case SPECIFIED_ORGANIZATION: + // 指定组织下所有用户 + if (StringUtil.isNotEmpty(taskInfoDto.getAssociationIds())) { + List organizeIds = List.of(taskInfoDto.getAssociationIds().split(StringPool.COMMA)); + List userBaseInfoByOrganizes = userApiV2Util.getUserListForOrgIds(organizeIds, null); + if (CollUtil.isNotEmpty(userBaseInfoByOrganizes)) { + userIds = userBaseInfoByOrganizes.stream() + .map(UserPageListVO::getId) + .distinct() + .collect(Collectors.toList()); + } + } + break; + case SPECIFIED_POSITION: + // 指定岗位下所有用户 + if (StringUtil.isNotEmpty(taskInfoDto.getAssociationIds())) { + List positionIds = List.of(taskInfoDto.getAssociationIds().split(StringPool.COMMA)); + List userBaseInfoByPositions = userApiV2Util.getUserListForPositions(positionIds, null); + if (CollUtil.isNotEmpty(userBaseInfoByPositions)) { + userIds = userBaseInfoByPositions.stream() + .map(UserBoundVO::getId) + .distinct() + .collect(Collectors.toList()); + } + } + break; + case CUSTOM: + // 自定义分配 + if (taskInfoDto.getCustomAssignType() != null) { + if (taskInfoDto.getCustomAssignType().equals(0)) { + userIds = ftbPersonnelsMetaDataService.queryUserIdsForCompanyAge( + taskInfoDto.getStartEntryPeriod(), + taskInfoDto.getEndEntryPeriod() + ); + } else if (taskInfoDto.getCustomAssignType().equals(1) + && StringUtils.isNotEmpty(taskInfoDto.getCustomAssignPostIds())) { + // 根据岗位和司龄筛选用户 + userIds = filterUsersByPositionAndTenure( + taskInfoDto.getCustomAssignPostIds(), + taskInfoDto.getEndEntryPeriod() + ); + } + } + break; + default: + // 默认情况或未知规则,保持 userIds 为空列表 + break; + } + //根据封面id查询封面信息 + FtbCultivateCoverInfoEntity coverInfo = cultivateCoverInfoMapper.selectById(taskInfoDto.getCoverId()); + + // 任务主表 + FtbCultivateLearnTask ftbCultivateLearnTask = taskInfoDto.convertFtbCultivateLearnTask(taskInfoDto); + if (coverInfo != null) { + ftbCultivateLearnTask.setCoverUrl(coverInfo.getPath()); + } + + // 任务展示Id,更新是之前的,新增则为新的 + if (StringUtil.isNotEmpty(taskInfoDto.getId())) { + FtbCultivateLearnTask oldTaskInfo = ftbCultivateLearnTaskService.getById(ftbCultivateLearnTask.getId()); + if (oldTaskInfo == null || oldTaskInfo.getEnableMark() == 1) { + throw new RuntimeException("修改任务不存在或已经删除"); + } + deleteOldTask(taskInfoDto.getId()); + ftbCultivateLearnTask.setId(oldTaskInfo.getId()); + ftbCultivateLearnTask.setLearnTaskShowId(oldTaskInfo.getLearnTaskShowId()); + ftbCultivateLearnTask.setCreatorTime(oldTaskInfo.getCreatorTime()); + ftbCultivateLearnTask.setCreatorUserId(oldTaskInfo.getCreatorUserId()); + ftbCultivateLearnTaskService.updateById(ftbCultivateLearnTask); + } else { + ftbCultivateLearnTask.setLearnTaskShowId(SelfGrowthUtil.provideACustomIDBasedOnTheModule(SelfrowingEnum.LEARN_TASK)); + ftbCultivateLearnTaskService.save(ftbCultivateLearnTask); + } + + // 处理各阶段数据 + List taskCourseList = new ArrayList<>(); + List taskExamList = new ArrayList<>(); + List taskIdentificationList = new ArrayList<>(); + List taskPracticeList = new ArrayList<>(); + List taskCertificateList = new ArrayList<>(); + List taskPhaseList = new ArrayList<>(); + + taskInfoDto.getPhase().forEach(phaseVo -> { + // 创建阶段 + FtbCultivateLearnTaskPhase phase = new FtbCultivateLearnTaskPhase(); + phase.setId(FtbUtil.getId()); + phase.setTaskId(ftbCultivateLearnTask.getId()); + phase.setQualified(phaseVo.getQualified()); + phase.setCertificateExam(phaseVo.getCertificateExam()); + phase.setCertificateIdentification(phaseVo.getCertificateIdentification()); + phase.setIsPassComplete(phaseVo.getIsPassComplete()); + //设置创建时间 创建人 + phase.setCreatorTime(DateUtil.date()); + phase.setCreatorUserId(UserProvider.getLoginUserId()); + phase.setLastModifyTime(DateUtil.date()); + phase.setLastModifyUserId(UserProvider.getLoginUserId()); + taskPhaseList.add(phase); + + // 处理阶段内的课程 + if (CollUtil.isNotEmpty(phaseVo.getCourseList())) { + phaseVo.getCourseList().forEach(courseInfo -> { + FtbCultivateLearnTaskCourse course = new FtbCultivateLearnTaskCourse(); + course.setTaskId(ftbCultivateLearnTask.getId()); + course.setCourseId(courseInfo.getCourseId()); + course.setIsRequired(courseInfo.getIsRequired()); + course.setPhaseId(phase.getId()); // 关联阶段ID + + course.setCreatorTime(DateUtil.date()); + course.setCreatorUserId(UserProvider.getLoginUserId()); + course.setLastModifyTime(DateUtil.date()); + course.setLastModifyUserId(UserProvider.getLoginUserId()); + taskCourseList.add(course); + }); + } + + // 处理阶段内的考试 + if (phaseVo.getExam() != null) { + FtbCultivateLearnTaskExam exam = new FtbCultivateLearnTaskExam(); + exam.setTaskId(ftbCultivateLearnTask.getId()); + exam.setExamId(phaseVo.getExam().getExamId()); + exam.setPhaseId(phase.getId()); // 关联阶段ID + exam.setCreatorTime(DateUtil.date()); + exam.setCreatorUserId(UserProvider.getLoginUserId()); + exam.setLastModifyTime(DateUtil.date()); + exam.setLastModifyUserId(UserProvider.getLoginUserId()); + taskExamList.add(exam); + } + + // 处理阶段内的鉴定 + if (phaseVo.getIdentification() != null && phaseVo.getIdentification() != null) { + FtbCultivateLearnTaskIdentification identification = new FtbCultivateLearnTaskIdentification(); + identification.setTaskId(ftbCultivateLearnTask.getId()); + identification.setIdentificationId(phaseVo.getIdentification().getIdentityId()); + identification.setPhaseId(phase.getId()); // 关联阶段ID + identification.setCreatorTime(DateUtil.date()); + identification.setCreatorUserId(UserProvider.getLoginUserId()); + identification.setLastModifyTime(DateUtil.date()); + identification.setLastModifyUserId(UserProvider.getLoginUserId()); + taskIdentificationList.add(identification); + } + + // 处理阶段内的练习 + if (CollUtil.isNotEmpty(phaseVo.getPracticeList())) { + phaseVo.getPracticeList().forEach(practiceInfo -> { + FtbCultivateLearnTaskPractice practice = new FtbCultivateLearnTaskPractice(); + practice.setTaskId(ftbCultivateLearnTask.getId()); + practice.setBusinessId(practiceInfo.getBusinessId()); + practice.setNum(practiceInfo.getNum()); + practice.setPhaseId(phase.getId()); // 关联阶段ID + practice.setCreatorTime(DateUtil.date()); + practice.setCreatorUserId(UserProvider.getLoginUserId()); + practice.setLastModifyTime(DateUtil.date()); + practice.setLastModifyUserId(UserProvider.getLoginUserId()); + taskPracticeList.add(practice); + }); + } + + // 处理阶段内的证书 + if (phaseVo.getCertificateIdentification() != null && phaseVo.getCertificate() != null) { + FtbCultivateLearnTaskCertificate certificate = new FtbCultivateLearnTaskCertificate(); + certificate.setTaskId(ftbCultivateLearnTask.getId()); + certificate.setCertificateId(phaseVo.getCertificate().getCertificateId()); + certificate.setPhaseId(phase.getId()); // 关联阶段ID + certificate.setCreatorTime(DateUtil.date()); + certificate.setCreatorUserId(UserProvider.getLoginUserId()); + certificate.setLastModifyTime(DateUtil.date()); + certificate.setLastModifyUserId(UserProvider.getLoginUserId()); + taskCertificateList.add(certificate); + } + }); + + // 批量插入各阶段数据 + if (CollUtil.isNotEmpty(taskPhaseList)) { + ftbCultivateLearnTaskPhaseService.saveBatch(taskPhaseList); + } + + if (CollUtil.isNotEmpty(taskCourseList)) { + ftbCultivateLearnTaskCourseService.saveBatch(taskCourseList); + } + + if (CollUtil.isNotEmpty(taskExamList)) { + ftbCultivateLearnTaskExamService.saveBatch(taskExamList); + } + + if (CollUtil.isNotEmpty(taskIdentificationList)) { + ftbCultivateLearnTaskIdentificationService.saveBatch(taskIdentificationList); + } + + if (CollUtil.isNotEmpty(taskPracticeList)) { + ftbCultivateLearnTaskPracticeService.saveBatch(taskPracticeList); + } + + if (CollUtil.isNotEmpty(taskCertificateList)) { + ftbCultivateLearnTaskCertificateService.saveBatch(taskCertificateList); + } + + // 任务学习关联规则提醒 + FtbCultivateLearnTaskReminderRule ftbCultivateLearnTaskReminderRule = taskInfoDto.convertTaskReminderRule(taskInfoDto); + ftbCultivateLearnTaskReminderRule.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskReminderRuleMapper.insert(ftbCultivateLearnTaskReminderRule); + // 学习任务关联人 + List ftbCultivateLearnTaskAssignments = userIds.stream().map(a -> { + FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment = new FtbCultivateLearnTaskAssignment(); + ftbCultivateLearnTaskAssignment.setTaskId(ftbCultivateLearnTask.getId()); + ftbCultivateLearnTaskAssignment.setStudyStats(0); + ftbCultivateLearnTaskAssignment.setUserId(a); + return ftbCultivateLearnTaskAssignment; + }).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(ftbCultivateLearnTaskAssignments)) { + ftbCultivateLearnTaskAssignmentService.saveBatch(ftbCultivateLearnTaskAssignments); + } + + + // 任务进行中,启用提醒后,任务开始时将提醒员工 + if (ftbCultivateLearnTask.getStatus() == 2 + && ftbCultivateLearnTaskReminderRule.getTaskStartReminder() == 1 + && CollUtil.isNotEmpty(userIds)) { + List alertUserList = userIds.stream().map(a -> { + NeedAlertUserDto needAlertUserDto = new NeedAlertUserDto(); + needAlertUserDto.setUserId(a); + needAlertUserDto.setTaskId(ftbCultivateLearnTask.getId()); + needAlertUserDto.setTaskName(ftbCultivateLearnTask.getTaskName()); + return needAlertUserDto; + }).collect(Collectors.toList()); + String tenantId = UserProvider.getUser().getTenantId(); + CompletableFuture.runAsync(() -> { + captchaImUtil.sendMsg(alertUserList, tenantId); + }, cultivateThreadPool); + } + if (ftbCultivateLearnTask.getStatus().equals(2)) { + List courseIds = taskCourseList.stream().map(FtbCultivateLearnTaskCourse::getCourseId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(courseIds) && CollUtil.isNotEmpty(userIds)) { + userIds = UserApiV2Util.uniqueStringList(userIds); + courseIds = UserApiV2Util.uniqueStringList(courseIds); + courseStudyUtil.batchInitCourseStudyRecord(userIds, courseIds, UserProvider.getUser().getTenantId()); + } + } + + return ftbCultivateLearnTask.getId(); + } + + + public void deleteOldTask(String id) { + // 任务删除 + LambdaUpdateWrapper deleteLearnTaskWrapper = Wrappers.lambdaUpdate(); + deleteLearnTaskWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + deleteLearnTaskWrapper.set(FtbCultivateLearnTask::getEnableMark, 1); // 逻辑删除 + ftbCultivateLearnTaskService.update(deleteLearnTaskWrapper); + // 任务人员 + LambdaUpdateWrapper deleteLearnTaskAssignmentWrapper = Wrappers.lambdaUpdate(); + deleteLearnTaskAssignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, id); + ftbCultivateLearnTaskAssignmentService.remove(deleteLearnTaskAssignmentWrapper); + // 任务课程 + LambdaUpdateWrapper deleteCourseWrapper = Wrappers.lambdaUpdate(); + deleteCourseWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, id); + ftbCultivateLearnTaskCourseService.remove(deleteCourseWrapper); + // 任务考试 + LambdaUpdateWrapper deleteExamWrapper = Wrappers.lambdaUpdate(); + deleteExamWrapper.eq(FtbCultivateLearnTaskExam::getTaskId, id); + ftbCultivateLearnTaskExamService.remove(deleteExamWrapper); + // 任务鉴定 + LambdaUpdateWrapper deleteIdentificationWrapper = Wrappers.lambdaUpdate(); + deleteIdentificationWrapper.eq(FtbCultivateLearnTaskIdentification::getTaskId, id); + ftbCultivateLearnTaskIdentificationService.remove(deleteIdentificationWrapper); + + // 任务证书 + LambdaUpdateWrapper deleteCertificateWrapper = Wrappers.lambdaUpdate(); + deleteCertificateWrapper.eq(FtbCultivateLearnTaskCertificate::getTaskId, id); + ftbCultivateLearnTaskCertificateService.remove(deleteCertificateWrapper); + + // 练习 + LambdaUpdateWrapper practiceWrapper = Wrappers.lambdaUpdate(); + practiceWrapper.eq(FtbCultivateLearnTaskPractice::getTaskId, id); + ftbCultivateLearnTaskPracticeService.remove(practiceWrapper); + + //阶段 + LambdaUpdateWrapper phaseWrapper = Wrappers.lambdaUpdate(); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, id); + ftbCultivateLearnTaskPhaseService.remove(phaseWrapper); + + // 任务提醒鉴定 + LambdaUpdateWrapper deleteReminderWrapper = Wrappers.lambdaUpdate(); + deleteReminderWrapper.eq(FtbCultivateLearnTaskReminderRule::getTaskId, id); + deleteReminderWrapper.eq(FtbCultivateLearnTaskReminderRule::getEnableMark, 0); + ftbCultivateLearnTaskReminderRuleMapper.delete(deleteReminderWrapper); + + //删除任务阶段学习 + LambdaQueryWrapper taskLogWrapper = Wrappers.lambdaQuery(); + taskLogWrapper.eq(FtbCultivateTaskLog::getTaskId, id); + ftbCultivateTaskLogService.remove(taskLogWrapper); + + // 删除任务未完成的鉴定 + LambdaQueryWrapper identifyApplyLambdaQueryWrapper = Wrappers.lambdaQuery(); + identifyApplyLambdaQueryWrapper.select(CultivateIdentifyApply::getId); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getSourceId, id); + identifyApplyLambdaQueryWrapper.eq(CultivateIdentifyApply::getSource, 4); + List cultivateIdentifyAppliesList = identifyApplyMapper.selectList(identifyApplyLambdaQueryWrapper); + if (CollUtil.isNotEmpty(cultivateIdentifyAppliesList)) { + List identifyApplyIds = cultivateIdentifyAppliesList.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + identifyApplyMapper.deleteBatchIds(identifyApplyIds); + + LambdaQueryWrapper deleteApplyDetailsWrapper = Wrappers.lambdaQuery(); + deleteApplyDetailsWrapper.in(CultivateIdentifyApplyDetails::getApplyId, identifyApplyIds); + identifyApplyDetailsMapper.delete(deleteApplyDetailsWrapper); + } + + // 删除任务关联考试 + //todo v2 怎么删除考试 + } + + + /** + * 修改学习任务 + * + * @param req 修改学习任务所需的参数 + * @return 修改成功后的任务ID + */ + @Override + @Transactional + public String update(V2CultivateTaskSaveReq req) { + + req.beforeCheck(); + FtbCultivateLearnTask repeatNameTask = queryTaskInfoByName(req.getTaskName()); + if (repeatNameTask != null && !repeatNameTask.getId().equals(req.getId())) { + throw new RuntimeException("任务名称重复"); + } + return realDealTaskData(req); + } + + /** + * 分配任务(给用户分配任务) + * + * @param req 分配数据传输对象,包含分配培养学习所需的各项数据 + */ + @Override + public void allocation(FtbCultivateLearnAllocationDTO req) { + FtbCultivateLearnTask entity = ftbCultivateLearnTaskService.getById(req.getTaskId()); + if (entity == null || entity.getEnableMark() == 1) { + throw new RuntimeException("任务不存在"); + } + LambdaQueryWrapper taskAssignmentLambdaQueryWrapper = Wrappers.lambdaQuery(); + taskAssignmentLambdaQueryWrapper.select(FtbCultivateLearnTaskAssignment::getUserId); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, req.getTaskId()); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List oldLearnTaskAssignments = ftbCultivateLearnTaskAssignmentService.list(taskAssignmentLambdaQueryWrapper); + List oldUserIds = oldLearnTaskAssignments.stream() + .map(FtbCultivateLearnTaskAssignment::getUserId) + .collect(Collectors.toList()); + + // 计算需要移除的用户ID(在旧列表中但不在新列表中的用户) + List removeUserIds = oldUserIds.stream() + .filter(userId -> !req.getUserIds().contains(userId)) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(removeUserIds)) { + // 移除不在新列表中的用户 + LambdaQueryWrapper removeWrapper = Wrappers.lambdaQuery(); + removeWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, req.getTaskId()); + removeWrapper.in(FtbCultivateLearnTaskAssignment::getUserId, removeUserIds); + ftbCultivateLearnTaskAssignmentService.remove(removeWrapper); + } + + // 计算需要新增的用户ID + List newAddedUserIds = req.getUserIds() + .stream() + .filter(userId -> !oldUserIds.contains(userId)) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(newAddedUserIds)) { + // 新人员绑定 + List ftbCultivateLearnTaskAssignments = newAddedUserIds.stream().map(a -> { + FtbCultivateLearnTaskAssignment ftbCultivateLearnTaskAssignment = new FtbCultivateLearnTaskAssignment(); + ftbCultivateLearnTaskAssignment.setTaskId(req.getTaskId()); + ftbCultivateLearnTaskAssignment.setUserId(a); + ftbCultivateLearnTaskAssignment.setStudyStats(0); + return ftbCultivateLearnTaskAssignment; + }).collect(Collectors.toList()); + ftbCultivateLearnTaskAssignmentService.saveBatch(ftbCultivateLearnTaskAssignments); + } + + } + + /** + * 中止指定任务 + * + * @param taskId 任务ID,用于标识需要中止的任务 + */ + @Override + public void abort(String taskId) { + FtbCultivateLearnTask entity = ftbCultivateLearnTaskService.getById(taskId); + if (entity == null || entity.getEnableMark() == 1) { + throw new RuntimeException("任务不存在"); + } + + // 任务中止 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateLearnTask::getId, taskId); + updateWrapper.set(FtbCultivateLearnTask::getStatus, 4); + ftbCultivateLearnTaskService.update(new FtbCultivateLearnTask(), updateWrapper); + List userIds = queryUserListForTaskNoComplete(taskId); + //终止用户 + +// LambdaUpdateWrapper assignUpdateWrapper = Wrappers.lambdaUpdate(); +// assignUpdateWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId) +// .ne(FtbCultivateLearnTaskAssignment::getStudyStats, 2) +// .eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0) +// .set(FtbCultivateLearnTaskAssignment::getStudyStats, 4); +// ftbCultivateLearnTaskAssignmentService.update(assignUpdateWrapper); + + if (CollUtil.isNotEmpty(userIds)) { + String tenantId = UserProvider.getUser().getTenantId(); + String pushMessageContent = "您的任务【" + entity.getTaskName() + "】已终止,无法继续进行任务!"; + cultivateImUtil.sendTaskAbortMessage(userIds, tenantId, pushMessageContent); + } + } + + /** + * 查询未完成的学员 + * + * @param taskId 任务id + * @return + */ + public List queryUserListForTaskNoComplete(String taskId) { + LambdaQueryWrapper assignmentQueryWrapper = Wrappers.lambdaQuery(); + assignmentQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + assignmentQueryWrapper.ne(FtbCultivateLearnTaskAssignment::getStudyStats, 2); + assignmentQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List list = ftbCultivateLearnTaskAssignmentService.list(assignmentQueryWrapper); + if (CollUtil.isNotEmpty(list)) { + return list.stream().map(FtbCultivateLearnTaskAssignment::getUserId).collect(Collectors.toList()); + } + return CollUtil.newArrayList(); + } + + /** + * 删除指定ID的任务 + * + * @param id 任务的唯一标识符 + */ + @Override + @Transactional + public void deleteTask(String id) { + deleteOldTask(id); + } + + + /** + * 获取指定ID的任务详情 + * + * @param id 任务ID + * @return 任务详情 + */ + @Override + public V2CultivateTaskDetailsVo get(String id) { + // 查询任务主表 + FtbCultivateLearnTask task = ftbCultivateLearnTaskService.getById(id); + if (task == null) { + return null; + } + + // 构建返回对象 + V2CultivateTaskDetailsVo vo = new V2CultivateTaskDetailsVo(); + vo.convert(task); + if (StringUtils.isNotEmpty(task.getTaskClass())) { + FtbCultivateLearnCategories categories = ftbCultivateLearnCategoriesMapper.selectById(task.getTaskClass()); + if (categories != null) { + vo.setTaskClassName(categories.getName()); + } + } + + // 批量查询任务关联的所有数据 + // 查询任务阶段表 + LambdaQueryWrapper phaseWrapper = Wrappers.lambdaQuery(); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, id); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getEnableMark, 0); // 只查询有效的 + List phaseList = ftbCultivateLearnTaskPhaseService.list(phaseWrapper); + + // 如果没有阶段,则直接返回 + if (CollUtil.isEmpty(phaseList)) { + vo.setPhase(new ArrayList<>()); + return vo; + } + + // 批量查询所有阶段的课程、考试、鉴定、证书和练习 - 使用新封装的方法 + List courseEntities = ftbCultivateLearnTaskCourseService.listByTaskId(id, null); + List examEntities = ftbCultivateLearnTaskExamService.listByTaskId(id); + List identificationEntities = ftbCultivateLearnTaskIdentificationService.listByTaskId(id); + List certificateEntities = ftbCultivateLearnTaskCertificateService.listByTaskId(id); + List practiceEntities = ftbCultivateLearnTaskPracticeService.listByTaskId(id); + + // 按阶段分组数据 + Map> courseGroupByPhase = courseEntities.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskCourseVo::getPhaseId)); + Map> examGroupByPhase = examEntities.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskExamVo::getPhaseId)); + Map> identificationGroupByPhase = identificationEntities.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskIdentificationVo::getPhaseId)); + Map> certificateGroupByPhase = certificateEntities.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskCertificateVo::getPhaseId)); + Map> practiceGroupByPhase = practiceEntities.stream() + .collect(Collectors.groupingBy(FtbCultivateLearnTaskPracticeVo::getPhaseId)); + + // 按照阶段分组并组装数据 + List phaseVoList = new ArrayList<>(); + for (FtbCultivateLearnTaskPhase phase : phaseList) { + V2CultivateLearnTaskPhaseVo phaseVo = new V2CultivateLearnTaskPhaseVo(); + phaseVo.convert(phase); + String phaseId = phase.getId(); + // 处理课程列表 - 按照sortCode升序排序 + List courseInPhase = courseGroupByPhase.getOrDefault(phaseId, new ArrayList<>()); + // 按排序字段升序排列 + courseInPhase.sort((a, b) -> { + Integer sortA = a.getSortCode() != null ? a.getSortCode() : 0; + Integer sortB = b.getSortCode() != null ? b.getSortCode() : 0; + return sortA.compareTo(sortB); + }); + + List courseList = new ArrayList<>(); + for (FtbCultivateLearnTaskCourseVo course : courseInPhase) { + // 设置课程名称 + V2CultivateLearnTaskCourseVo courseVo = new V2CultivateLearnTaskCourseVo(); + courseVo.convert(course); + courseVo.setCourseName(course.getCourseName()); + courseVo.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + courseVo.setBusinessSourceId(task.getId()); + courseList.add(courseVo); + } + phaseVo.setCourseList(courseList); + + // 处理考试信息 + List examInPhase = examGroupByPhase.getOrDefault(phaseId, new ArrayList<>()); + if (CollUtil.isNotEmpty(examInPhase)) { + FtbCultivateLearnTaskExamVo examEntity = examInPhase.get(0); // 每个阶段最多一个考试 + // 设置考试名称 + V2CultivateLearnTaskExamVo examVo = new V2CultivateLearnTaskExamVo(); + examVo.convert(examEntity); + examVo.setExamName(examEntity.getExamName()); + phaseVo.setExam(examVo); + + } + + // 处理鉴定信息 + List identificationInPhase = identificationGroupByPhase.getOrDefault(phaseId, new ArrayList<>()); + if (CollUtil.isNotEmpty(identificationInPhase)) { + FtbCultivateLearnTaskIdentificationVo identificationEntity = identificationInPhase.get(0); // 每个阶段最多一个鉴定 + V2CultivateLearnTaskIdentificationVo identificationVo = new V2CultivateLearnTaskIdentificationVo(); + identificationVo.convert(identificationEntity); + identificationVo.setIdentityName(identificationEntity.getIdentificationName()); // 使用identityId作为名称,可能需要根据实际需求调整 + identificationVo.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + identificationVo.setBusinessSourceId(task.getId()); + phaseVo.setIdentification(identificationVo); + } + + // 处理证书信息 + List certificateInPhase = certificateGroupByPhase.getOrDefault(phaseId, new ArrayList<>()); + if (CollUtil.isNotEmpty(certificateInPhase)) { + FtbCultivateLearnTaskCertificateVo certificateEntity = certificateInPhase.get(0); // 每个阶段最多一个证书 + + V2CultivateLearnTaskCertificateVo certificateVo = new V2CultivateLearnTaskCertificateVo(); + certificateVo.convert(certificateEntity); + certificateVo.setCertificateName(certificateEntity.getCertName()); + phaseVo.setCertificate(certificateVo); + } + + // 处理练习列表 + List practiceInPhase = practiceGroupByPhase.getOrDefault(phaseId, new ArrayList<>()); + + List practiceList = new ArrayList<>(); + for (FtbCultivateLearnTaskPracticeVo practice : practiceInPhase) { + // 设置技能名称,从教学技能映射中获取 + V2CultivateLearnTaskPracticeVo practiceVo = new V2CultivateLearnTaskPracticeVo(); + practiceVo.convert(practice); + practiceVo.setBusinessName(practice.getBusinessName()); + practiceVo.setCompleteNum(0); // 暂时设为0,需要根据实际情况查询 + practiceVo.setStatus(0); // 暂时设为0,需要根据实际情况查询 + practiceList.add(practiceVo); + } + phaseVo.setPracticeList(practiceList); + phaseVoList.add(phaseVo); + } + vo.setPhase(phaseVoList); + + // 查询任务提醒规则 + LambdaQueryWrapper reminderRuleWrapper = Wrappers.lambdaQuery(); + reminderRuleWrapper.eq(FtbCultivateLearnTaskReminderRule::getTaskId, id) + .eq(FtbCultivateLearnTaskReminderRule::getEnableMark, 0); // 只查询有效的 + FtbCultivateLearnTaskReminderRule reminderRule = ftbCultivateLearnTaskReminderRuleMapper.selectOne(reminderRuleWrapper); + + if (reminderRule != null) { + vo.setTaskStartReminder(reminderRule.getTaskStartReminder()); + vo.setTaskEndReminder(reminderRule.getTaskEndReminder()); + vo.setPerInterval(reminderRule.getPerInterval()); + } + + return vo; + } + + /** + * App查询任务列表 + *

+ * vo.setTaskStartReminder(reminderRule.getTaskStartReminder()); + * vo.setTaskEndReminder(reminderRule.getTaskEndReminder()); + * vo.setPerInterval(reminderRule.getPerInterval()); + * } + *

+ * return vo; + * } + *

+ * /** + * App查询任务列表 + * + * @param taskListDto 查询任务列表所需的参数 + * @return 任务列表 + */ + @Override + public PageListVO queryTaskListForApp(V2CultivateLearnTaskListForManagerReq taskListDto) { + Page page = taskListDto.coverCultivatePage(); + page = ftbCultivateLearnTaskMapper.queryTaskListForApp( + page, + taskListDto.getKeyWords(), + taskListDto.getStatus() + ); + List records = page.getRecords(); + if (CollUtil.isNotEmpty(records)) { + //获取任务IDs + List taskIds = records.stream().map(V2CultivateTaskListForManagerVo::getId).collect(Collectors.toList()); + // 查询任务的总人数 map + List totalCountList = ftbCultivateLearnTaskAssignmentMapper.groupCountNum(taskIds, 0); + Map totalNumMap = totalCountList.stream() + .collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, item -> item.getNum().longValue())); + + // 查询任务的完成人数 map + List completeCountList = ftbCultivateLearnTaskAssignmentMapper.groupCountNum(taskIds, 1); + Map completeNumMap = completeCountList.stream() + .collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, item -> item.getNum().longValue())); + + // 查询任务的阶段数 map + List phaseCountList = ftbCultivateLearnTaskPhaseMapper.groupCountNum(taskIds); + Map phaseNumMap = phaseCountList.stream() + .collect(Collectors.toMap(BatchCommonCountDto::getSelectKey, item -> item.getNum().longValue())); + // 目前使用现有字段taskNumber, taskCompleteNumber, phaseNum + for (V2CultivateTaskListForManagerVo record : records) { + record.setTaskNumber(Math.toIntExact(totalNumMap.getOrDefault(record.getId(), 0L))); + record.setTaskCompleteNumber(Math.toIntExact(completeNumMap.getOrDefault(record.getId(), 0L))); + record.setPhaseNum(phaseNumMap.getOrDefault(record.getId(), 0L)); + } + } + + return CultivatePage.coverPageList(page); + } + + /** + * App查询任务数量 + * + * @param keyWord 关键字 + * @return 任务数量 + */ + @Override + public Map queryTaskCountForApp(String keyWord) { + // 查询各个状态的任务数量,添加空值检查 + Long count1 = ftbCultivateLearnTaskMapper.queryTaskCountForApp(keyWord, LearnTaskStatusEnum.NOT_STARTED.getCode()); + Long count2 = ftbCultivateLearnTaskMapper.queryTaskCountForApp(keyWord, LearnTaskStatusEnum.IN_PROGRESS.getCode()); + Long count3 = ftbCultivateLearnTaskMapper.queryTaskCountForApp(keyWord, LearnTaskStatusEnum.FINISHED.getCode()); + + // 对可能为null的值进行安全转换 + return Map.of( + "noStart", count1 != null ? count1.intValue() : 0, + "doing", count2 != null ? count2.intValue() : 0, + "over", count3 != null ? count3.intValue() : 0 + ); + } + + /** + * App查询任务完成统计 + * + * @param taskId 任务ID + * @return 任务完成统计 + */ + @Override + public V2CultivateLearnTaskFinishStatisticsVo getCompletionStatisticsForManagerApp(String taskId) { + // 参数验证 + if (taskId == null) { + return new V2CultivateLearnTaskFinishStatisticsVo(); + } + + // 根据任务统计该任务下人员完成情况 + V2CultivateLearnTaskFinishStatisticsVo statisticsVO = new V2CultivateLearnTaskFinishStatisticsVo(); + LambdaQueryWrapper taskAssignmentLambdaQueryWrapper = Wrappers.lambdaQuery(); + taskAssignmentLambdaQueryWrapper.select(FtbCultivateLearnTaskAssignment::getUserId, + FtbCultivateLearnTaskAssignment::getStudyStats); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + taskAssignmentLambdaQueryWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List taskAssignmentList = ftbCultivateLearnTaskAssignmentMapper.selectList(taskAssignmentLambdaQueryWrapper); + Map> listMap = taskAssignmentList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskAssignment::getStudyStats, Collectors.toList())); + // 应完成人数 + // 0未开始,1进行中,2已完成,3已逾期 + statisticsVO.setBeComplete(taskAssignmentList.size()); + // 0未开始 + statisticsVO.setNoStart(listMap.getOrDefault(0, Collections.emptyList()).size()); + // 1进行中 + statisticsVO.setDoing(listMap.getOrDefault(1, Collections.emptyList()).size()); + // 2已完成 + statisticsVO.setComplete(listMap.getOrDefault(2, Collections.emptyList()).size()); + // 3已逾期 + statisticsVO.setOverdue(listMap.getOrDefault(3, Collections.emptyList()).size()); + return statisticsVO; + } + + /** + * App查询任务完成情况用户列表 + * + * @param taskId 课程ID + * @param page 分页参数 + * @return 任务完成用户列表 + */ + @Override + public PageListVO queryTaskUserListForManagerApp(String taskId, CultivatePage page) { + Page searchPage = page.coverCultivatePage(); + searchPage = ftbCultivateLearnTaskAssignmentMapper.queryTaskUserListForManagerApp(searchPage, taskId); + List records = searchPage.getRecords(); + if (CollUtil.isNotEmpty(records)) { + List userIds = records.stream().map(V2CultivateTaskFinishUserListVo::getUserId).collect(Collectors.toList()); + Map userBoundVOMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + records.forEach(item -> { + UserBoundVO userBoundVO = userBoundVOMap.get(item.getUserId()); + if (userBoundVO != null) { + item.setUserName(userBoundVO.getUserName()); + item.setOrgName(userBoundVO.getOrganizeName()); + item.setPostName(userBoundVO.getPositionName()); + item.setGradeName(userBoundVO.getGradeName()); + item.setEmployeeID(userBoundVO.getSystemWorkerId()); + } + + if (item.getTaskStartTime() != null && item.getTaskEndTime() != null) { + // 计算任务时长 + TimeDifference timeDifference = CultivateDateTimeUtils.calculateDifference(item.getTaskStartTime(), item.getTaskEndTime()); + item.setTaskDuration(timeDifference.toString()); + } + }); + } + return CultivatePage.coverPageList(searchPage); + } + + + /** + * App查询我的任务列表 + * + * @param taskListDto 查询任务列表所需的参数 + * @return 我的任务列表 + */ + @Override + public PageListVO queryMyTaskListForApp(V2MyCultivateTaskListForReq taskListDto) { + String userId = UserProvider.getUser().getUserId(); + + Page tPage = taskListDto.coverCultivatePage(); + Page taskPage = ftbCultivateLearnTaskMapper.queryMyTaskListForApp(tPage, taskListDto, userId); + if (CollUtil.isNotEmpty(taskPage.getRecords())) { + List taskIds = new ArrayList<>(); + taskPage.getRecords().forEach(item -> { + if (item.getTaskType() == 1) { + Date learningEndTime = item.getTimeLimitEndTime(); + String remainingValue = CultivateDateTimeUtils.calculateRemainingValue(new Date(), learningEndTime, 0); + item.setDaysRemaining(remainingValue); + } + taskIds.add(item.getTaskId()); + }); + + LambdaQueryWrapper phaseWrapper = Wrappers.lambdaQuery(); + phaseWrapper.in(FtbCultivateLearnTaskPhase::getTaskId, taskIds); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getEnableMark, 0); // 只查询有效的 + phaseWrapper.orderByAsc(FtbCultivateLearnTaskPhase::getCreatorTime); + List phaseList = ftbCultivateLearnTaskPhaseService.list(phaseWrapper); + //按照任务id分组 + Map> phaseMap = phaseList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskPhase::getTaskId)); + List completePhaseList = ftbCultivateTaskLogService.batchQueryCompleteTaskPhase(taskIds, userId); + Map> completePhaseMap = completePhaseList.stream().collect(Collectors.groupingBy(FtbCultivateTaskLog::getTaskId)); + for (V2MyCultivateLearnTaskListVo record : taskPage.getRecords()) { + List phases = phaseMap.get(record.getTaskId()); + record.setPhaseNum((long) phases.size()); + List ftbCultivateTaskLogs = completePhaseMap.get(record.getTaskId()); + if (CollUtil.isNotEmpty(ftbCultivateTaskLogs)) { + record.setCompletePhaseNum((long) ftbCultivateTaskLogs.size()); + } else { + record.setCompletePhaseNum(0L); + } + } + +// List completePhaseIds = completePhaseList.stream().map(FtbCultivateTaskLog::getPhaseId).collect(Collectors.toList()); +// List phaseIds = phaseList.stream().map(FtbCultivateLearnTaskPhase::getId).collect(Collectors.toList()); +// boolean b = UserApiV2Util.areListsEqual(completePhaseIds, phaseIds); + } + + return CultivatePage.coverPageList(taskPage); + } + + /** + * App查询我的任务数量 + * + * @param keyWord 搜索关键字 + * @return 我的任务数量 + */ + @Override + public Map queryMyTaskCountForApp(String keyWord) { +// 1-待完成,2-已完成,3-已逾期 + String userId = UserProvider.getUser().getUserId(); + Integer waitCompleteNum = ftbCultivateLearnTaskMapper.queryMyTaskCountForApp(keyWord, userId, StudyStatsEnum.IN_PROGRESS.getCode()); + Integer completeNum = ftbCultivateLearnTaskMapper.queryMyTaskCountForApp(keyWord, userId, StudyStatsEnum.COMPLETED.getCode()); + Integer overNum = ftbCultivateLearnTaskMapper.queryMyTaskCountForApp(keyWord, userId, StudyStatsEnum.OVERDUE.getCode()); + Map ret = new HashMap<>(); + ret.put("waitCompleteNum", waitCompleteNum); + ret.put("completeNum", completeNum); + ret.put("overNum", overNum); + return ret; + } + + + /** + * App查询我的任务阶段信息 + * + * @param myTaskId 我的任务id + * @param phaseId 阶段ID + * @return 我的任务阶段信息 + */ + @Override + public V2CultivateLearnTaskPhaseVo queryMyTaskPhaseInfoForApp(String myTaskId, String phaseId) { + String userId = UserProvider.getUser().getUserId(); + + FtbCultivateLearnTaskAssignment taskAssignment = ftbCultivateLearnTaskAssignmentService.getById(myTaskId); + + if (taskAssignment == null || taskAssignment.getEnableMark() == 1) { + throw new RuntimeException("任务不存在"); + } + + String taskId = taskAssignment.getTaskId(); + + // 根据任务id查询任务信息 + FtbCultivateLearnTask task = ftbCultivateLearnTaskService.getById(taskId); + if (task == null || task.getEnableMark() == 1) { + throw new RuntimeException("任务不存在或已被删除"); + } + + // 查询任务的阶段信息 + FtbCultivateLearnTaskPhase phase = ftbCultivateLearnTaskPhaseService.getById(phaseId); + if (phase == null || phase.getEnableMark() == 1) { + throw new RuntimeException("任务阶段不存在或已被删除"); + } + + // 构建返回对象 + V2CultivateLearnTaskPhaseVo phaseVo = new V2CultivateLearnTaskPhaseVo(); + phaseVo.setId(phase.getId()); + phaseVo.setTaskId(phase.getTaskId()); + phaseVo.setCertificateExam(phase.getCertificateExam()); + phaseVo.setCertificateIdentification(phase.getCertificateIdentification()); + phaseVo.setQualified(phase.getQualified()); + phaseVo.setIsPassComplete(phase.getIsPassComplete()); + phaseVo.setCreateTime(phase.getCreatorTime()); + + // 一次性批量查询所有阶段相关的信息 + // 查询阶段的课程、考试、鉴定、证书、练习信息 + List courseList = ftbCultivateLearnTaskCourseService.listByTaskIdAndPhaseId(taskId, phaseId); + + List examList = ftbCultivateLearnTaskExamService.listByTaskIdAndPhaseId(taskId, phaseId); + FtbCultivateLearnTaskExamVo exam = CollUtil.isNotEmpty(examList) ? examList.get(0) : null; + + List identificationList = ftbCultivateLearnTaskIdentificationService.listByTaskIdAndPhaseId(taskId, phaseId); + FtbCultivateLearnTaskIdentificationVo identification = CollUtil.isNotEmpty(identificationList) ? identificationList.get(0) : null; + + List certificateList = ftbCultivateLearnTaskCertificateService.listByTaskIdAndPhaseId(taskId, phaseId); + FtbCultivateLearnTaskCertificateVo certificate = CollUtil.isNotEmpty(certificateList) ? certificateList.get(0) : null; + + List practiceList = ftbCultivateLearnTaskPracticeService.listByTaskIdAndPhaseId(taskId, phaseId); + + // 调用封装的批量查询方法 + List courseIds = CollUtil.isNotEmpty(courseList) ? courseList.stream().map(FtbCultivateLearnTaskCourseVo::getCourseId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + List examIds = exam != null ? Collections.singletonList(exam.getExamId()) : new ArrayList<>(); + List identityIds = identification != null ? Collections.singletonList(identification.getIdentificationId()) : new ArrayList<>(); + List certificateIds = certificate != null ? Collections.singletonList(certificate.getCertificateId()) : new ArrayList<>(); + List practiceBusinessIds = CollUtil.isNotEmpty(practiceList) ? practiceList.stream().map(FtbCultivateLearnTaskPracticeVo::getBusinessId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + + Map taskLogsMap = convertTaskLogMap(ftbCultivateTaskLogService.queryCompleteTaskPhase(taskId, userId)); + + // 获取用户学习状态 + V2UserLearningStatusResult userStatusResult = CultivateBatchQueryUtil.batchQueryUserLearningStatusWithSpecificIds( + userId, courseIds, examIds, identityIds, certificateIds, practiceBusinessIds, v2CultivateBatchQueryService, 4, phaseVo.getTaskId()); + Map> learningChapterMap = v2CultivateBatchQueryService.batchQueryUserCourseChapterLearnStatus(userId, courseIds); + // 封装课程信息 + Boolean isCourseComplete = true; + if (CollUtil.isNotEmpty(courseList)) { + List courseVoList = new ArrayList<>(); + Boolean hasNeedCourse = false;//false-没有必须课 true-有必须课 + for (FtbCultivateLearnTaskCourseVo course : courseList) { + V2CultivateLearnTaskCourseVo courseVo = new V2CultivateLearnTaskCourseVo(); + courseVo.setBusinessSourceId(course.getTaskId()); + courseVo.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + courseVo.setId(course.getId()); + courseVo.setCourseId(course.getCourseId()); + courseVo.setIsRequired(course.getIsRequired()); + courseVo.setSortCode(course.getSortCode()); + courseVo.setCourseName(course.getCourseName()); + // 查询课程学习状态 (从批量查询结果中获取) + FtbCultivatePositionCourceLearning userCourseStatus = userStatusResult.getUserCourseMap().get(course.getCourseId()); + if (userCourseStatus != null) { + // 这里可以设置更详细的课程学习状态,比如完成度、是否已学等 + // 示例:根据实际业务逻辑设置学习状态 + courseVo.setLearnState(getCourseLearnState(userCourseStatus)); // 根据实际业务逻辑获取学习状态 + } else { + courseVo.setLearnState(0); // 默认未学习 + } + if (courseVo.getIsRequired().equals(0)) { + hasNeedCourse = true; + } + if (courseVo.getIsRequired().equals(0) && (courseVo.getLearnState().equals(0) || courseVo.getLearnState().equals(2))) { + isCourseComplete = false; + } + if (courseVo.getLearnState().equals(0)) { + courseVo.setLearnProgress("0%"); + } else if (courseVo.getLearnState().equals(1)) { + courseVo.setLearnProgress("100%"); + } else { + //根据已经学习的章节 和总章节计算 + List chapterLearningList = learningChapterMap.get(courseVo.getCourseId()); + if (CollUtil.isEmpty(chapterLearningList)) { + courseVo.setLearnProgress("0%"); + } else { + int total = chapterLearningList.size(); + int completed = (int) chapterLearningList.stream().filter(chapterLearning -> chapterLearning.getState().equals(1)).count(); + courseVo.setLearnProgress(String.format("%d%%", completed * 100 / total)); + } + } + courseVoList.add(courseVo); + } + if (!hasNeedCourse) { + isCourseComplete = false; + } + phaseVo.setCourseList(courseVoList); + } else { + // 如果没有课程列表,说明没有必修课,默认为已完成 + phaseVo.setCourseList(new ArrayList<>()); + isCourseComplete = false; + } + + // 封装考试信息 + if (exam != null) { + V2CultivateLearnTaskExamVo examVo = new V2CultivateLearnTaskExamVo(); + examVo.setId(exam.getId()); + examVo.setExamId(exam.getExamId()); + examVo.setExamName(exam.getExamName()); + examVo.setCreatorTime(exam.getCreatorTime()); + examVo.setLastModifyTime(exam.getLastModifyTime()); + examVo.setIsCourseComplete(isCourseComplete); + // 查询考试状态 (从批量查询结果中获取) + FtbCultivateExamUser userExamStatus = userStatusResult.getUserExamMap().get(exam.getExamId()); + if (userExamStatus != null) { + // 这里可以设置更详细的考试状态 + examVo.setExamStatus(getExamStatus(userExamStatus)); // 根据实际业务逻辑获取考试状态 + examVo.setUserExamId(userExamStatus.getId()); + } else { + examVo.setExamStatus(0); // 默认待考试 + if (isCourseComplete) { + userExamStatus = v2CultivateTaskStudyService.checkAndTriggerExam(userId, phaseVo.getTaskId(), phase, Collections.singletonList(exam.getExamId()), userStatusResult.getUserExamMap(),UserProvider.getUser().getTenantId()); + if (userExamStatus != null) { + examVo.setUserExamId(userExamStatus.getId()); + } + } else { + examVo.setUserExamId(null); + } + } + phaseVo.setExam(examVo); + + } + + // 封装鉴定信息 + if (identification != null) { + V2CultivateLearnTaskIdentificationVo identificationVo = new V2CultivateLearnTaskIdentificationVo(); + // 获取鉴定信息 + identificationVo.setBusinessSourceId(identification.getTaskId()); + identificationVo.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + identificationVo.setId(identification.getId()); + identificationVo.setIdentityId(identification.getIdentificationId()); + identificationVo.setIdentityName(identification.getIdentificationName()); + identificationVo.setCreatorTime(identification.getCreatorTime()); + identificationVo.setLastModifyTime(identification.getLastModifyTime()); + identificationVo.setIsCourseComplete(isCourseComplete); + + // 查询鉴定状态 (从批量查询结果中获取) + CultivateIdentifyApply userIdentificationStatus = userStatusResult.getUserIdentificationMap().get(identification.getIdentificationId()); + identificationVo.setIsCanIdentification(runCanIdentification(phaseVo, phaseVo.getExam())); + if (userIdentificationStatus != null) { + identificationVo.setIdentificationStatus(userIdentificationStatus.getStatus()); + // 这里可以设置更详细的鉴定状态 + identificationVo.setIdentificationResult(getIdentificationStatus(userIdentificationStatus)); // 根据实际业务逻辑获取鉴定状态 + identificationVo.setUserIdentityId(userIdentificationStatus.getId()); + } else { + identificationVo.setIdentificationStatus(0); // 默认待鉴定 + } + phaseVo.setIdentification(identificationVo); + + } + + // 封装证书信息 + if (certificate != null) { + V2CultivateLearnTaskCertificateVo certificateVo = new V2CultivateLearnTaskCertificateVo(); + certificateVo.setId(certificate.getId()); + certificateVo.setCertificateId(certificate.getCertificateId()); + certificateVo.setCertificateName(certificate.getCertName()); + // 查询证书状态 (从批量查询结果中获取) + FtbCertificateUserEntity userCertificateStatus = userStatusResult.getUserCertificateMap().get(certificate.getCertificateId()); + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogsMap.get(certificate.getPhaseId()); + certificateVo.setIsGet(calIsGetCert(userCertificateStatus, ftbCultivateTaskLog)); + phaseVo.setCertificate(certificateVo); + + } + + // 封装练习信息 + if (CollUtil.isNotEmpty(practiceList)) { + List practiceVoList = new ArrayList<>(); + for (FtbCultivateLearnTaskPracticeVo practice : practiceList) { + V2CultivateLearnTaskPracticeVo practiceVo = new V2CultivateLearnTaskPracticeVo(); + practiceVo.setId(practice.getId()); + practiceVo.setBusinessId(practice.getBusinessId()); + practiceVo.setBusinessName(practice.getBusinessName()); + practiceVo.setNum(practice.getNum()); + practiceVo.setIsCourseComplete(isCourseComplete); + + // 查询技能完成状态 (从批量查询结果中获取) + List completeBussinessNum = userStatusResult.getUserSkillMap().get(practice.getBusinessId()); + if (completeBussinessNum != null) { + if (completeBussinessNum.size() >= practice.getNum()) { + // 这里可以设置更详细的技能完成状态 + practiceVo.setCompleteNum(practice.getNum()); // 根据实际业务逻辑获取完成数 + practiceVo.setStatus(1); // 根据实际业务逻辑获取状态 + } else { + practiceVo.setCompleteNum(completeBussinessNum.size()); + practiceVo.setStatus(0); + } + } else { + practiceVo.setCompleteNum(0); // 实际需要查询完成情况 + practiceVo.setStatus(0); // 实际需要查询完成状态 + } + practiceVoList.add(practiceVo); + + } + phaseVo.setPracticeList(practiceVoList); + } else { + phaseVo.setPracticeList(new ArrayList<>()); + } + + // 查询阶段状态 (通过日志记录确定) - 使用封装的服务方法 + FtbCultivateTaskLog taskLog = ftbCultivateTaskLogService.getByTaskIdAndUserIdAndPhaseId(taskId, userId, phaseId); + + if (taskLog != null) { + phaseVo.setPhaseStatus(taskLog.getState()); + } else { + // 如果没有日志记录,默认为未开始 + phaseVo.setPhaseStatus(0); + } + + return phaseVo; + } + + private Integer runCanIdentification(V2CultivateLearnTaskPhaseVo phaseVo, + V2CultivateLearnTaskExamVo examVo) { + if (phaseVo.getQualified() == 1) { + if (examVo != null) { + if (!(examVo.getExamStatus().equals(3) + || examVo.getExamStatus().equals(5))) { + return 0; + } + } + } + return 1; + } + + private Boolean calIsGetCert(FtbCertificateUserEntity userCertificateStatus, FtbCultivateTaskLog ftbCultivateTaskLog) { + if (ftbCultivateTaskLog == null || ftbCultivateTaskLog.getIssuedCertificate().equals(0)) { + return false; + } + if (ftbCultivateTaskLog.getIssuedCertificate().equals(1)) { + return true; + } + return false; + } + + private Map convertTaskLogMap(List ftbCultivateTaskLogs) { + Map taskLogMap = new HashMap<>(); + if (CollUtil.isEmpty(ftbCultivateTaskLogs)) { + return taskLogMap; + } + for (FtbCultivateTaskLog taskLog : ftbCultivateTaskLogs) { + taskLogMap.put(taskLog.getPhaseId(), taskLog); + } + return taskLogMap; + } + + @Override + public Page positionCourseList(V2CultivateCoursePageReq req) { + Page page = Page.of(req.getCurrentPage(), req.getPageSize()); + return ftbCultivateCourseMapper.queryBindingCourses(page, req); + } + + // 辅助方法:根据课程学习记录获取学习状态 + private int getCourseLearnState(FtbCultivatePositionCourceLearning courseLearning) { + // 根据实际业务逻辑设置学习状态 + // 例如:0-未开始,1-学习中,2-已完成 + if (courseLearning == null) { + return 0; // 未学习 + } + return courseLearning.getState() != null ? courseLearning.getState() : 0; // 使用实体中的状态 + } + + // 辅助方法:根据考试记录获取考试状态 + private int getExamStatus(FtbCultivateExamUser examUser) { + // 根据实际业务逻辑设置考试状态 + // 例如:0-未考试,1-考试中,2-已通过,3-未通过 + if (examUser == null) { + return 0; // 待考试 + } + return examUser.getStatus() != null ? examUser.getStatus() : 0; // 使用实体中的状态 + } + + // 辅助方法:根据鉴定记录获取鉴定状态 + private int getIdentificationStatus(CultivateIdentifyApply identification) { + // 根据实际业务逻辑设置鉴定状态 +// 鉴定结果(0合格,1优秀,2不合格) + if (identification == null) { + return 2; // 待鉴定 + } + return identification.getResult() != null ? identification.getResult() : 2; // 使用实体中的状态 + } + + // 辅助方法:根据技能记录获取完成数量 + private int getSkillCompleteNum(TeachingRecord skillRecord) { + // 根据实际业务逻辑设置完成数量 + // 这里可以根据记录存在与否来判断是否完成 + return skillRecord != null ? 1 : 0; // 如果有记录则认为已完成 + } + + // 辅助方法:根据技能记录获取技能状态 + private int getSkillStatus(TeachingRecord skillRecord) { + // 根据实际业务逻辑设置技能状态 + // 例如:0-未开始,1-进行中,2-已完成 + if (skillRecord == null) { + return 0; // 未开始 + } + return 2; // 假设有记录即为已完成 + } + + /** + * App查询任务阶段信息 + * + * @param myTaskId 我的任务ID + * @param userId 用户ID + * @return + */ + @Override + @Transactional + public void startTask(String myTaskId, String userId) { + FtbCultivateLearnTaskAssignment task = ftbCultivateLearnTaskAssignmentMapper.selectById(myTaskId); + if (task == null || task.getEnableMark() == 1) { + throw new DataException("任务不存在"); + } + if (!task.getUserId().equals(userId)) { + throw new DataException("用户无权开启这个任务"); + } + if (task.getStudyStats() == 1) { + throw new DataException("任务已开始"); + } + if (task.getStudyStats() == 2) { + return; + } + if (task.getStudyStats() == 3) { + throw new DataException("任务已逾期"); + } + + String taskId = task.getTaskId(); + FtbCultivateLearnTask ftbCultivateLearnTask = ftbCultivateLearnTaskMapper.selectById(taskId); + if (ftbCultivateLearnTask == null || ftbCultivateLearnTask.getEnableMark() == 1) { + throw new DataException("任务不存在,或被删除"); + } + if (ftbCultivateLearnTask.getStatus().equals(4)) { + throw new DataException("任务已终止"); + } + if (ftbCultivateLearnTask.getStatus().equals(3)) { + throw new DataException("任务已结束"); + } + if (ftbCultivateLearnTask.getTaskType().equals(1)) { + //帮我判断任务是否开始 和结束 + if (ftbCultivateLearnTask.getTimeLimitStartTime().after(new Date())) { + throw new DataException("任务还未到开始时间"); + } + if (ftbCultivateLearnTask.getTimeLimitEndTime().before(new Date())) { + task.setStudyStats(3); + } + } + task.setLearningStartTime(new Date()); + task.setStudyStats(1); + ftbCultivateLearnTaskAssignmentMapper.updateById(task); + //开始记录第一阶段正在学习中 + initUserPhaseLog(taskId, userId); + } + + /** + * App修改任务状态 + * + * @param tenantId 租户ID + */ + @Override + public void checkAndUpdateTaskStatus(String tenantId) { + try { + Date now = new Date(); + //修改进行中 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.le(FtbCultivateLearnTask::getTimeLimitStartTime, now) + .ge(FtbCultivateLearnTask::getTimeLimitEndTime, now) + .eq(FtbCultivateLearnTask::getStatus, 1) + .eq(FtbCultivateLearnTask::getTaskType, 1) + .eq(FtbCultivateLearnTask::getEnableMark, 0); + List taskList = ftbCultivateLearnTaskMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(taskList)) { + //获取任务ids集合 + List taskIds = taskList.stream().map(FtbCultivateLearnTask::getId).collect(Collectors.toList()); + LambdaUpdateWrapper taskLambdaUpdateWrapperStart = new LambdaUpdateWrapper() + .set(FtbCultivateLearnTask::getStatus, 2) + .in(FtbCultivateLearnTask::getId, taskIds) + .eq(FtbCultivateLearnTask::getEnableMark, 0); + ftbCultivateLearnTaskMapper.update(null, taskLambdaUpdateWrapperStart); + cultivateTaskLeanAsyncDealUtil.asyncSendStartTaskLearningAlert(taskList, tenantId); + courseStudyUtil.addTaskCourseStudyRecord(taskIds, tenantId); + } + //修改结束 + LambdaUpdateWrapper taskLambdaUpdateWrapperOver = new LambdaUpdateWrapper() + .set(FtbCultivateLearnTask::getStatus, 3) + .le(FtbCultivateLearnTask::getTimeLimitEndTime, now) + .in(FtbCultivateLearnTask::getStatus, 1, 2) + .eq(FtbCultivateLearnTask::getTaskType, 1) + .eq(FtbCultivateLearnTask::getEnableMark, 0); + ftbCultivateLearnTaskMapper.update(null, taskLambdaUpdateWrapperOver); + //异步处理所有用户任务的状态 + cultivateTaskLeanAsyncDealUtil.asyncUpdateUserTaskStatus(tenantId); + //如果处理长期任务 显示 + checkAndUpdateOverdueUserTasksForLong(tenantId); + //处理限时任务 再限时 用户开始后 根据用户开始任务时间,把限时改成已逾期 + checkAndUpdateOverdueUserTasksForTimeLimited(tenantId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public V2MyCultivateLearnTaskSimpleInfoVo queryMyTaskSimpleInfoForApp(String myTaskId) { + String userId = UserProvider.getUser().getUserId(); + V2MyCultivateLearnTaskSimpleInfoVo taskSimpleInfo = new V2MyCultivateLearnTaskSimpleInfoVo(); + + LambdaQueryWrapper assignmentWrapper = Wrappers.lambdaQuery(); + assignmentWrapper.select(FtbCultivateLearnTaskAssignment::getId, + FtbCultivateLearnTaskAssignment::getTaskId, + FtbCultivateLearnTaskAssignment::getUserId, + FtbCultivateLearnTaskAssignment::getStudyStats, + FtbCultivateLearnTaskAssignment::getLearningStartTime, + FtbCultivateLearnTaskAssignment::getLearningEndTime); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getId, myTaskId); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getUserId, userId); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + FtbCultivateLearnTaskAssignment taskAssignment = ftbCultivateLearnTaskAssignmentService.getOne(assignmentWrapper); + if (taskAssignment == null) { + throw new RuntimeException("对不起,你的任务已经不存在了"); + } + // 查询任务详情 + String taskId = taskAssignment.getTaskId(); + FtbCultivateLearnTask task = ftbCultivateLearnTaskService.getById(taskId); + if (task == null || task.getEnableMark() == 1) { + throw new RuntimeException("对不起,任务删除了"); + } + + // 构建任务基础信息 + taskSimpleInfo.setId(taskAssignment.getId()); + taskSimpleInfo.setTaskId(task.getId()); + taskSimpleInfo.setStudyStats(taskAssignment.getStudyStats()); + taskSimpleInfo.setTaskClass(task.getTaskClass()); + taskSimpleInfo.setTaskName(task.getTaskName()); + taskSimpleInfo.setTaskType(task.getTaskType()); + taskSimpleInfo.setTimeLimitStartTime(task.getTimeLimitStartTime()); + taskSimpleInfo.setTimeLimitEndTime(task.getTimeLimitEndTime()); + taskSimpleInfo.setStudentTimeCompletion(task.getStudentTimeCompletion()); + taskSimpleInfo.setTimeCompletionUnit(task.getTimeCompletionUnit()); + taskSimpleInfo.setTimeCompletionDays(task.getTimeCompletionDays()); + taskSimpleInfo.setAssignmentRule(task.getAssignmentRule()); + taskSimpleInfo.setAssociationIds(task.getAssociationIds()); + taskSimpleInfo.setStartEntryPeriod(task.getStartEntryPeriod()); + taskSimpleInfo.setEndEntryPeriod(task.getEndEntryPeriod()); + taskSimpleInfo.setCustomAssignType(task.getCustomAssignType()); + taskSimpleInfo.setCustomAssignPostIds(task.getCustomAssignPostIds()); + taskSimpleInfo.setDescription(task.getDescription()); + taskSimpleInfo.setCoverId(task.getCoverId()); + taskSimpleInfo.setCoverUrl(task.getCoverUrl()); + taskSimpleInfo.setStatus(task.getStatus()); + + + // 查询任务阶段列表 + LambdaQueryWrapper phaseWrapper = Wrappers.lambdaQuery(); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, taskId); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getEnableMark, 0); + List phaseList = ftbCultivateLearnTaskPhaseService.list(phaseWrapper); + + // 将数据库实体转换为VO,并确定每个阶段的状态 + List phaseVoList = new ArrayList<>(); + + // 一次性查询所有阶段的日志,提高性能 + LambdaQueryWrapper logWrapper = Wrappers.lambdaQuery(); + logWrapper.eq(FtbCultivateTaskLog::getTaskId, taskId); + logWrapper.eq(FtbCultivateTaskLog::getUserId, userId); + logWrapper.eq(FtbCultivateTaskLog::getEnabledMark, 0); + List allLogs = ftbCultivateTaskLogService.list(logWrapper); + + // allLogs 转换成key是id的map + Map logsMap = allLogs.stream().collect(Collectors.toMap(FtbCultivateTaskLog::getPhaseId, log -> log)); + + String currStudyPhase = phaseList.get(0).getId(); + for (FtbCultivateLearnTaskPhase phase : phaseList) { + V2CultivateLearnTaskSimplePhaseVo phaseVo = new V2CultivateLearnTaskSimplePhaseVo(); + phaseVo.convert(phase); + FtbCultivateTaskLog ftbCultivateTaskLog = logsMap.get(phase.getId()); + if (ftbCultivateTaskLog != null) { + phaseVo.setPhaseStatus(ftbCultivateTaskLog.getState()); + currStudyPhase = phase.getId(); + } else { + phaseVo.setPhaseStatus(0); + } + phaseVoList.add(phaseVo); + } + + taskSimpleInfo.setPhase(phaseVoList); + taskSimpleInfo.setCurrStudyPhase(currStudyPhase); + + return taskSimpleInfo; + } + + @Override + public List queryUserTaskInfo(String myTaskId) { + FtbCultivateLearnTaskAssignment taskAssignment = ftbCultivateLearnTaskAssignmentService.getById(myTaskId); + if (taskAssignment == null || taskAssignment.getEnableMark().equals(1)) { + throw new RuntimeException("该数据已经不存在"); + } + String userId = taskAssignment.getUserId(); + // 查询任务详情 + String taskId = taskAssignment.getTaskId(); + FtbCultivateLearnTask task = ftbCultivateLearnTaskService.getById(taskId); + if (task == null || task.getEnableMark().equals(1)) { + throw new RuntimeException("对不起,任务删除了"); + } + List returnList = new ArrayList<>(); + // 查询任务阶段列表 + LambdaQueryWrapper phaseWrapper = Wrappers.lambdaQuery(); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, taskId) + .eq(FtbCultivateLearnTaskPhase::getEnableMark, 0); + List phaseList = ftbCultivateLearnTaskPhaseService.list(phaseWrapper); + Map phaseMap = new HashMap<>(); + // 转出 V2CultivateLearnTaskPhaseVo + List phaseVoList = phaseList.stream().map(phase -> { + phaseMap.put(phase.getId(), phase); + V2CultivateLearnTaskPhaseVo phaseVo = new V2CultivateLearnTaskPhaseVo(); + phaseVo.convert(phase); + return phaseVo; + }).collect(Collectors.toList()); + + // 一次性批量查询所有阶段相关的信息 + // 查询阶段的课程、考试、鉴定、证书、练习信息 + List courseList = ftbCultivateLearnTaskCourseService.listByTaskId(taskId, null); + + List examList = ftbCultivateLearnTaskExamService.listByTaskId(taskId); + + List identificationList = ftbCultivateLearnTaskIdentificationService.listByTaskId(taskId); + + List certificateList = ftbCultivateLearnTaskCertificateService.listByTaskId(taskId); + + List practiceList = ftbCultivateLearnTaskPracticeService.listByTaskId(taskId); + + + //按阶段分组 对课程 考试 鉴定 证书 练习 + Map> courseMap = courseList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskCourseVo::getPhaseId)); + Map> examMap = examList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskExamVo::getPhaseId)); + Map> identificationMap = identificationList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskIdentificationVo::getPhaseId)); + Map> certificateMap = certificateList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskCertificateVo::getPhaseId)); + Map> practiceMap = practiceList.stream().collect(Collectors.groupingBy(FtbCultivateLearnTaskPracticeVo::getPhaseId)); + + Map taskLogsMap = convertTaskLogMap(ftbCultivateTaskLogService.queryCompleteTaskPhase(taskId, userId)); + Map> learningChapterMap = v2CultivateBatchQueryService.batchQueryUserCourseChapterLearnStatus(userId, courseList.stream().map(FtbCultivateLearnTaskCourseVo::getCourseId).collect(Collectors.toList())); + + for (V2CultivateLearnTaskPhaseVo phaseVo : phaseVoList) { + String phaseId = phaseVo.getId(); + List taskCourseList = courseMap.get(phaseId); + List ftbCultivateLearnTaskExams = examMap.get(phaseId); + List ftbCultivateLearnTaskIdentifications = identificationMap.get(phaseId); + List ftbCultivateLearnTaskCertificates = certificateMap.get(phaseId); + List ftbCultivateLearnTaskPractices = practiceMap.get(phaseId); + + List courseIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(taskCourseList)) { + courseIds = taskCourseList.stream().map(FtbCultivateLearnTaskCourseVo::getCourseId).collect(Collectors.toList()); + } + + List examIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(ftbCultivateLearnTaskExams)) { + examIds = ftbCultivateLearnTaskExams.stream().map(FtbCultivateLearnTaskExamVo::getExamId).collect(Collectors.toList()); + } + List identityIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(ftbCultivateLearnTaskIdentifications)) { + identityIds = ftbCultivateLearnTaskIdentifications.stream().map(FtbCultivateLearnTaskIdentificationVo::getIdentificationId).collect(Collectors.toList()); + } + List certificateIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(ftbCultivateLearnTaskCertificates)) { + certificateIds = ftbCultivateLearnTaskCertificates.stream().map(FtbCultivateLearnTaskCertificateVo::getCertificateId).collect(Collectors.toList()); + } + List practiceBusinessIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(ftbCultivateLearnTaskPractices)) { + practiceBusinessIds = ftbCultivateLearnTaskPractices.stream().map(FtbCultivateLearnTaskPracticeVo::getBusinessId).collect(Collectors.toList()); + } + // 获取用户学习状态 + V2UserLearningStatusResult userStatusResult = CultivateBatchQueryUtil.batchQueryUserLearningStatusWithSpecificIds( + userId, courseIds, examIds, identityIds, certificateIds, practiceBusinessIds, v2CultivateBatchQueryService, 4, phaseVo.getTaskId()); + + // 封装课程信息 + if (CollUtil.isNotEmpty(taskCourseList)) { + List courseVoList = new ArrayList<>(); + for (FtbCultivateLearnTaskCourseVo course : taskCourseList) { + V2CultivateLearnTaskCourseVo courseVo = new V2CultivateLearnTaskCourseVo(); + courseVo.setBusinessSourceId(course.getTaskId()); + courseVo.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + courseVo.setId(course.getId()); + courseVo.setCourseId(course.getCourseId()); + courseVo.setIsRequired(course.getIsRequired()); + courseVo.setSortCode(course.getSortCode()); + courseVo.setCourseName(course.getCourseName()); + // 查询课程学习状态 (从批量查询结果中获取) + FtbCultivatePositionCourceLearning userCourseStatus = userStatusResult.getUserCourseMap().get(course.getCourseId()); + if (userCourseStatus != null) { + // 这里可以设置更详细的课程学习状态,比如完成度、是否已学等 + // 示例:根据实际业务逻辑设置学习状态 + courseVo.setLearnState(getCourseLearnState(userCourseStatus)); // 根据实际业务逻辑获取学习状态 + courseVo.setLearnTime(userCourseStatus.getLearnTime()); + } else { + courseVo.setLearnState(0); // 默认未学习 + courseVo.setLearnTime(0); + } + if (courseVo.getLearnState().equals(0)) { + courseVo.setLearnProgress("0%"); + } else if (courseVo.getLearnState().equals(1)) { + courseVo.setLearnProgress("100%"); + } else { + //根据已经学习的章节 和总章节计算 + List chapterLearningList = learningChapterMap.get(courseVo.getCourseId()); + if (CollUtil.isEmpty(chapterLearningList)) { + courseVo.setLearnProgress("0%"); + } else { + int total = chapterLearningList.size(); + int completed = (int) chapterLearningList.stream().filter(chapterLearning -> chapterLearning.getState().equals(1)).count(); + courseVo.setLearnProgress(String.format("%d%%", completed * 100 / total)); + } + } + courseVoList.add(courseVo); + } + phaseVo.setCourseList(courseVoList); + } + + + // 封装考试信息 + if (ftbCultivateLearnTaskExams != null) { + V2CultivateLearnTaskExamVo examVo = new V2CultivateLearnTaskExamVo(); + FtbCultivateLearnTaskExamVo ftbCultivateLearnTaskExam = ftbCultivateLearnTaskExams.get(0); + // 获取考试信息 + examVo.setId(ftbCultivateLearnTaskExam.getId()); + examVo.setExamId(ftbCultivateLearnTaskExam.getExamId()); + examVo.setExamName(ftbCultivateLearnTaskExam.getExamName()); + // 查询考试状态 (从批量查询结果中获取) + FtbCultivateExamUser userExamStatus = userStatusResult.getUserExamMap().get(ftbCultivateLearnTaskExam.getExamId()); + if (userExamStatus != null) { + // 这里可以设置更详细的考试状态 + examVo.setExamStatus(getExamStatus(userExamStatus)); // 根据实际业务逻辑获取考试状态 + examVo.setUserExamId(userExamStatus.getId()); + examVo.setUserExamDuration(userExamStatus.getDuration()); + } else { + examVo.setExamStatus(0); // 默认待考试 + examVo.setUserExamDuration(0L); + examVo.setUserExamId(null); + } + phaseVo.setExam(examVo); + + } + + // 封装鉴定信息 + if (ftbCultivateLearnTaskIdentifications != null) { + V2CultivateLearnTaskIdentificationVo identificationVo = new V2CultivateLearnTaskIdentificationVo(); + // 获取鉴定信息 + FtbCultivateLearnTaskIdentificationVo ftbCultivateLearnTaskIdentification = ftbCultivateLearnTaskIdentifications.get(0); + + identificationVo.setBusinessSourceId(ftbCultivateLearnTaskIdentification.getTaskId()); + identificationVo.setBusinessSource(PositionBusinessSourceEnum.TASK_LEARNING); + identificationVo.setId(ftbCultivateLearnTaskIdentification.getId()); + identificationVo.setIdentityId(ftbCultivateLearnTaskIdentification.getIdentificationId()); + identificationVo.setIdentityName(ftbCultivateLearnTaskIdentification.getIdentificationName()); + // 查询鉴定状态 (从批量查询结果中获取) + CultivateIdentifyApply userIdentificationStatus = userStatusResult.getUserIdentificationMap().get(ftbCultivateLearnTaskIdentification.getIdentificationId()); + if (userIdentificationStatus != null) { + identificationVo.setIdentificationStatus(userIdentificationStatus.getStatus()); + // 这里可以设置更详细的鉴定状态 + identificationVo.setIdentificationResult(getIdentificationStatus(userIdentificationStatus)); // 根据实际业务逻辑获取鉴定状态 + identificationVo.setUserIdentityId(userIdentificationStatus.getId()); + identificationVo.setUseTime(userIdentificationStatus.getUseTime()); + } else { + identificationVo.setIdentificationStatus(0); // 默认待鉴定 + identificationVo.setUserIdentityId(null); + identificationVo.setUseTime(0); + } + + phaseVo.setIdentification(identificationVo); + + } + + // 封装证书信息 + if (ftbCultivateLearnTaskCertificates != null) { + V2CultivateLearnTaskCertificateVo certificateVo = new V2CultivateLearnTaskCertificateVo(); + FtbCultivateLearnTaskCertificateVo ftbCultivateLearnTaskCertificate = ftbCultivateLearnTaskCertificates.get(0); + + certificateVo.setId(ftbCultivateLearnTaskCertificate.getId()); + certificateVo.setCertificateId(ftbCultivateLearnTaskCertificate.getCertificateId()); + certificateVo.setCertificateName(ftbCultivateLearnTaskCertificate.getCertName()); + // 查询证书状态 (从批量查询结果中获取) + FtbCertificateUserEntity userCertificateStatus = userStatusResult.getUserCertificateMap().get(ftbCultivateLearnTaskCertificate.getCertificateId()); + FtbCultivateTaskLog ftbCultivateTaskLog = taskLogsMap.get(phaseId); + certificateVo.setIsGet(calIsGetCert(userCertificateStatus, ftbCultivateTaskLog)); + phaseVo.setCertificate(certificateVo); + + } + + // 封装练习信息 + if (CollUtil.isNotEmpty(ftbCultivateLearnTaskPractices)) { + List practiceVoList = new ArrayList<>(); + for (FtbCultivateLearnTaskPracticeVo practice : ftbCultivateLearnTaskPractices) { + V2CultivateLearnTaskPracticeVo practiceVo = new V2CultivateLearnTaskPracticeVo(); + // 从缓存中获取技能名称 + + practiceVo.setId(practice.getId()); + practiceVo.setBusinessId(practice.getBusinessId()); + practiceVo.setBusinessName(practice.getBusinessName()); + practiceVo.setNum(practice.getNum()); + + // 查询技能完成状态 (从批量查询结果中获取) + List completeBussinessNum = userStatusResult.getUserSkillMap().get(practice.getBusinessId()); + if (completeBussinessNum != null) { + if (completeBussinessNum.size() >= practice.getNum()) { + // 这里可以设置更详细的技能完成状态 + practiceVo.setCompleteNum(practice.getNum()); // 根据实际业务逻辑获取完成数 + practiceVo.setStatus(1); // 根据实际业务逻辑获取状态 + } else { + practiceVo.setCompleteNum(completeBussinessNum.size()); + practiceVo.setStatus(0); + } + } else { + practiceVo.setCompleteNum(0); // 实际需要查询完成情况 + practiceVo.setStatus(0); // 实际需要查询完成状态 + } + practiceVoList.add(practiceVo); + + } + phaseVo.setPracticeList(practiceVoList); + } else { + phaseVo.setPracticeList(new ArrayList<>()); + } + + // 查询阶段状态 (通过日志记录确定) - 使用封装的服务方法 + FtbCultivateTaskLog taskLog = ftbCultivateTaskLogService.getByTaskIdAndUserIdAndPhaseId(taskId, userId, phaseId); + + if (taskLog != null) { + phaseVo.setPhaseStatus(taskLog.getState()); + } else { + // 如果没有日志记录,默认为未开始 + phaseVo.setPhaseStatus(0); + } + returnList.add(phaseVo); + } + return returnList; + } + + + /** + * 根据任务名称查询任务数量 + * + * @param taskName 任务名称 + * @return 任务数量 + */ + public Long queryTaskNumByName(String taskName) { + LambdaQueryWrapper taskQueryWrapper = Wrappers.lambdaQuery(); + taskQueryWrapper.eq(FtbCultivateLearnTask::getTaskName, taskName); + taskQueryWrapper.eq(FtbCultivateLearnTask::getEnableMark, 0); + return ftbCultivateLearnTaskService.count(taskQueryWrapper); + } + + public FtbCultivateLearnTask queryTaskInfoByName(String taskName) { + LambdaQueryWrapper taskNameQueryWrapper = Wrappers.lambdaQuery(); + taskNameQueryWrapper.eq(FtbCultivateLearnTask::getTaskName, taskName); + taskNameQueryWrapper.eq(FtbCultivateLearnTask::getEnableMark, 0); + List list = ftbCultivateLearnTaskService.list(taskNameQueryWrapper); + if (CollUtil.isNotEmpty(list)) { + return list.get(0); + } + return null; + + } + + /** + * 初始化用户任务的阶段 + * 当用户开始任务时,自动标记第一阶段为进行中状态 + * + * @param taskId 任务ID + * @param userId 用户ID + */ + private void initUserPhaseLog(String taskId, String userId) { + // 查询任务的第一个阶段 + LambdaQueryWrapper phaseQueryWrapper = Wrappers.lambdaQuery(); + phaseQueryWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, taskId) + .eq(FtbCultivateLearnTaskPhase::getEnableMark, 0) + .orderByAsc(FtbCultivateLearnTaskPhase::getCreatorTime); // 按创建时间排序,获取第一个阶段 + List phases = ftbCultivateLearnTaskPhaseService.list(phaseQueryWrapper); + if (CollUtil.isNotEmpty(phases)) { + // 检查是否已存在该阶段的日志记录 + LambdaQueryWrapper logQueryWrapper = Wrappers.lambdaQuery(); + logQueryWrapper.eq(FtbCultivateTaskLog::getTaskId, taskId) + .eq(FtbCultivateTaskLog::getUserId, userId) + .eq(FtbCultivateTaskLog::getEnabledMark, 0); + ftbCultivateTaskLogService.remove(logQueryWrapper); + FtbCultivateLearnTaskPhase phase = phases.get(0); + FtbCultivateTaskLog taskLog = new FtbCultivateTaskLog(); + taskLog.setTaskId(taskId); + taskLog.setPhaseId(phase.getId()); + taskLog.setUserId(userId); + taskLog.setState(1); // 1表示正在进行中 + ftbCultivateTaskLogService.save(taskLog); + } + } + + /** + * 检查考试是否被学习任务绑定 + * + * @param examId 考试ID + * @return true-已绑定 false-未绑定 + */ + @Override + public Boolean checkExamBinding(String examId) { + return ftbCultivateLearnTaskExamService.checkExamBinding(examId); + } + + + @Override + public CheckUserTaskAllPhasesCompleteVo checkUserTaskAllPhasesComplete(String userTaskId) { + // 1. 查询用户任务分配信息,获取用户ID和任务ID + FtbCultivateLearnTaskAssignment assignment = ftbCultivateLearnTaskAssignmentMapper.selectById(userTaskId); + if (assignment == null) { + throw new DataException("用户任务不存在"); + } + + String userId = assignment.getUserId(); + String taskId = assignment.getTaskId(); + + // 2. 查询该任务的所有阶段 + LambdaQueryWrapper phaseWrapper = Wrappers.lambdaQuery(); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, taskId) + .eq(FtbCultivateLearnTaskPhase::getEnableMark, 0) + .orderByAsc(FtbCultivateLearnTaskPhase::getCreatorTime); + List phases = ftbCultivateLearnTaskPhaseMapper.selectList(phaseWrapper); + + // 3. 批量查询所有阶段的日志记录(优化:循环外查询) + Map taskLogMap = new HashMap<>(); + if (CollUtil.isNotEmpty(phases)) { + List phaseIds = phases.stream() + .map(FtbCultivateLearnTaskPhase::getId) + .collect(Collectors.toList()); + + // 批量查询该用户在该任务下所有阶段的日志 + LambdaQueryWrapper logWrapper = Wrappers.lambdaQuery(); + logWrapper.eq(FtbCultivateTaskLog::getTaskId, taskId) + .eq(FtbCultivateTaskLog::getUserId, userId) + .in(FtbCultivateTaskLog::getPhaseId, phaseIds); + List taskLogs = ftbCultivateTaskLogService.list(logWrapper); + + // 将日志列表转换为 Map,key 为 phaseId + taskLogMap = taskLogs.stream() + .collect(Collectors.toMap(FtbCultivateTaskLog::getPhaseId, log -> log, (k1, k2) -> k1)); + } + + // 4. 构建返回结果 + List phaseCompleteInfos = new ArrayList<>(); + + for (FtbCultivateLearnTaskPhase phase : phases) { + // 从 Map 中获取该阶段的日志记录 + FtbCultivateTaskLog taskLog = taskLogMap.get(phase.getId()); + + CheckUserTaskAllPhasesCompleteVo.PhaseCompleteInfo phaseInfo; + if (taskLog != null) { + // 阶段状态:根据state字段判断(1-已完成) + Integer state = taskLog.getState(); + boolean isComplete = state != null && state == 2; + + phaseInfo = CheckUserTaskAllPhasesCompleteVo.PhaseCompleteInfo.builder() + .phaseId(phase.getId()) + .isComplete(isComplete) + .phaseStatus(state) + .courseState(taskLog.getCourseState()) + .examState(taskLog.getExamState()) + .practiceState(taskLog.getPracticeState()) + .identifyState(taskLog.getIdentifyState()) + .issuedCertificate(taskLog.getIssuedCertificate()) + .build(); + } else { + // 如果没有日志记录,说明阶段还未开始 + phaseInfo = CheckUserTaskAllPhasesCompleteVo.PhaseCompleteInfo.builder() + .phaseId(phase.getId()) + .isComplete(false) + .phaseStatus(0) // 0-未开始 + .courseState(-1) // -1-未配置 + .examState(-1) + .practiceState(-1) + .identifyState(-1) + .issuedCertificate(0) + .build(); + } + + phaseCompleteInfos.add(phaseInfo); + } + + return CheckUserTaskAllPhasesCompleteVo.builder() + .userTaskId(userTaskId) + .phases(phaseCompleteInfos) + .build(); + } + + /** + * 检查并更新限时任务的用户学习状态为已逾期 + * 针对长期任务 显示的情况 + * 将已经超时的用户任务状态更新为已逾期(studyStats=3) + * + * @param tenantId 租户ID + */ + private void checkAndUpdateOverdueUserTasksForLong(String tenantId) { + try { + Date now = new Date(); + + // 查询所有限时任务(studentTimeCompletion=0)且状态为进行中的任务 + LambdaQueryWrapper taskWrapper = Wrappers.lambdaQuery(); + taskWrapper.eq(FtbCultivateLearnTask::getTaskType, 0); + taskWrapper.eq(FtbCultivateLearnTask::getStudentTimeCompletion, 0); // 0-限时 + taskWrapper.eq(FtbCultivateLearnTask::getEnableMark, 0); // 有效 + taskWrapper.in(FtbCultivateLearnTask::getStatus, 2); // 2-进行中 + List timeLimitedTasks = ftbCultivateLearnTaskService.list(taskWrapper); + + if (CollUtil.isEmpty(timeLimitedTasks)) { + return; + } + + // 获取这些任务的ID列表 + List taskIds = timeLimitedTasks.stream() + .map(FtbCultivateLearnTask::getId) + .collect(Collectors.toList()); + + // 批量查询这些任务下的所有用户任务分配记录 + LambdaQueryWrapper assignmentWrapper = Wrappers.lambdaQuery(); + assignmentWrapper.in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); // 有效 + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getStudyStats, 1); // 0-未开始, 1-进行中 + assignmentWrapper.isNotNull(FtbCultivateLearnTaskAssignment::getLearningStartTime); // 有开始时间 + List assignments = ftbCultivateLearnTaskAssignmentService.list(assignmentWrapper); + + if (CollUtil.isEmpty(assignments)) { + return; + } + + // 构建taskId到Task的映射 + Map taskMap = timeLimitedTasks.stream() + .collect(Collectors.toMap(FtbCultivateLearnTask::getId, task -> task)); + + // 需要更新为逾期的用户任务ID列表 + List overdueAssignmentIds = new ArrayList<>(); + + + for (FtbCultivateLearnTaskAssignment assignment : assignments) { + // 计算用户应该完成的时间 = 开始学习时间 + 限时时长 + Date learningStartTime = assignment.getLearningStartTime(); + if (learningStartTime == null) { + continue; + } + FtbCultivateLearnTask task = taskMap.get(assignment.getTaskId()); + if (task == null) { + continue; + } + if (task.getTimeCompletionUnit() == null || task.getTimeCompletionUnit() < 0) { + continue; + } + if (task.getTimeCompletionDays() == null || task.getTimeCompletionDays() <= 0) { + continue; + } + + + // 计算任务的限时时长(毫秒) + long timeLimitMillis = calculateTimeLimitMillis(task); + if (timeLimitMillis <= 0) { + continue; // 无法计算有效限时时长,跳过 + } + + + long deadlineMillis = learningStartTime.getTime() + timeLimitMillis; + Date deadline = new Date(deadlineMillis); + + // 如果当前时间已经超过截止时间,则标记为逾期 + if (now.after(deadline)) { + overdueAssignmentIds.add(assignment.getId()); + } + } + + // 批量更新逾期状态 + if (CollUtil.isNotEmpty(overdueAssignmentIds)) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateLearnTaskAssignment::getStudyStats, StudyStatsEnum.OVERDUE.getCode()); // 3-已逾期 + updateWrapper.in(FtbCultivateLearnTaskAssignment::getId, overdueAssignmentIds); + ftbCultivateLearnTaskAssignmentService.update(updateWrapper); + + log.info("租户[{}]限时任务逾期检查完成,共更新{}条用户任务为已逾期状态", tenantId, overdueAssignmentIds.size()); + } + + } catch (Exception e) { + log.error("检查并更新限时任务用户逾期状态失败,租户ID: {}", tenantId, e); + } + } + + /** + * 计算任务的限时时长(毫秒) + * + * @param task 学习任务 + * @return 限时时长(毫秒) + */ + private long calculateTimeLimitMillis(FtbCultivateLearnTask task) { + Integer timeCompletionUnit = task.getTimeCompletionUnit(); + Integer timeCompletionDays = task.getTimeCompletionDays(); + + + if (timeCompletionUnit == 1) { + // 小时单位:timeCompletionDays 实际表示小时数 + return timeCompletionDays.longValue() * 60 * 60 * 1000L; + } else { + // 天单位(默认):timeCompletionDays 表示天数 + return timeCompletionDays.longValue() * 24 * 60 * 60 * 1000L; + } + } + + /** + * 检查并更新限时任务(taskType=1)的用户逾期状态 + * 根据用户开始任务时间和任务配置的限时时长,判断是否已逾期 + * + * @param tenantId 租户ID + */ + private void checkAndUpdateOverdueUserTasksForTimeLimited(String tenantId) { + try { + Date now = new Date(); + + // 查询所有限时任务(taskType=1)且状态为进行中的任务 + LambdaQueryWrapper taskWrapper = Wrappers.lambdaQuery(); + taskWrapper.eq(FtbCultivateLearnTask::getTaskType, 1); + taskWrapper.eq(FtbCultivateLearnTask::getStudentTimeCompletion, 0); // 0-限时 + taskWrapper.eq(FtbCultivateLearnTask::getEnableMark, 0); // 有效 + taskWrapper.in(FtbCultivateLearnTask::getStatus, 2); // 2-进行中 + List timeLimitedTasks = ftbCultivateLearnTaskService.list(taskWrapper); + + if (CollUtil.isEmpty(timeLimitedTasks)) { + return; + } + + // 获取这些任务的ID列表 + List taskIds = timeLimitedTasks.stream() + .map(FtbCultivateLearnTask::getId) + .collect(Collectors.toList()); + + // 批量查询这些任务下的所有用户任务分配记录 + LambdaQueryWrapper assignmentWrapper = Wrappers.lambdaQuery(); + assignmentWrapper.in(FtbCultivateLearnTaskAssignment::getTaskId, taskIds); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); // 有效 + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getStudyStats, 1); // 0-未开始, 1-进行中 + assignmentWrapper.isNotNull(FtbCultivateLearnTaskAssignment::getLearningStartTime); // 有开始时间 + List assignments = ftbCultivateLearnTaskAssignmentService.list(assignmentWrapper); + + if (CollUtil.isEmpty(assignments)) { + return; + } + + // 构建taskId到Task的映射 + Map taskMap = timeLimitedTasks.stream() + .collect(Collectors.toMap(FtbCultivateLearnTask::getId, task -> task)); + + // 需要更新为逾期的用户任务ID列表 + List overdueAssignmentIds = new ArrayList<>(); + + for (FtbCultivateLearnTaskAssignment assignment : assignments) { + // 计算用户应该完成的时间 = 开始学习时间 + 限时时长 + Date learningStartTime = assignment.getLearningStartTime(); + if (learningStartTime == null) { + continue; + } + FtbCultivateLearnTask task = taskMap.get(assignment.getTaskId()); + if (task == null) { + continue; + } + if (task.getTimeCompletionUnit() == null || task.getTimeCompletionUnit() < 0) { + continue; + } + if (task.getTimeCompletionDays() == null || task.getTimeCompletionDays() <= 0) { + continue; + } + + // 计算任务的限时时长(毫秒) + long timeLimitMillis = calculateTimeLimitMillis(task); + if (timeLimitMillis <= 0) { + continue; // 无法计算有效限时时长,跳过 + } + + long deadlineMillis = learningStartTime.getTime() + timeLimitMillis; + Date deadline = new Date(deadlineMillis); + + // 如果当前时间已经超过截止时间,则标记为逾期 + if (now.after(deadline)) { + overdueAssignmentIds.add(assignment.getId()); + } + } + + // 批量更新逾期状态 + if (CollUtil.isNotEmpty(overdueAssignmentIds)) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateLearnTaskAssignment::getStudyStats, StudyStatsEnum.OVERDUE.getCode()); // 3-已逾期 + updateWrapper.in(FtbCultivateLearnTaskAssignment::getId, overdueAssignmentIds); + ftbCultivateLearnTaskAssignmentService.update(updateWrapper); + + log.info("租户[{}]限时任务(taskType=1)逾期检查完成,共更新{}条用户任务为已逾期状态", tenantId, overdueAssignmentIds.size()); + } + + } catch (Exception e) { + log.error("检查并更新限时任务(taskType=1)用户逾期状态失败,租户ID: {}", tenantId, e); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskStudyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskStudyServiceImpl.java new file mode 100644 index 0000000..fb05899 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2CultivateTaskStudyServiceImpl.java @@ -0,0 +1,1087 @@ +package jnpf.cultivate.v2.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.CultivateExamMapper; +import jnpf.cultivate.mapper.FtbCultivateCertificateResultMapper; +import jnpf.cultivate.mapper.FtbCultivateExamUserMapper; +import jnpf.cultivate.mapper.FtbCultivateLearnTaskAssignmentMapper; +import jnpf.cultivate.service.*; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.*; +import jnpf.entity.cultivate.*; +import jnpf.enums.cultivate.ApplyStatusEnum; +import jnpf.enums.cultivate.task.TaskStateEnum; +import jnpf.model.cultivate.po.course.FtbCultivateCertificateResult; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTask; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.po.task.FtbCultivateLearnTaskPhase; +import jnpf.model.cultivate.po.task.FtbCultivateTaskLog; +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import jnpf.model.cultivate.v2.task.vo.*; +import jnpf.model.cultivate.vo.identify.UserOrgInfoAll1Vo; +import jnpf.model.enums.CourseEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.ConstantUtil; +import jnpf.util.QuestionAnalysisUtil; +import jnpf.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class V2CultivateTaskStudyServiceImpl implements V2CultivateTaskStudyService { + + + @Autowired + private V2CultivateBatchQueryService v2CultivateBatchQueryService; + + @Autowired + private FtbCultivateLearnTaskService ftbCultivateLearnTaskService; + + @Autowired + private FtbCultivateLearnTaskPhaseService ftbCultivateLearnTaskPhaseService; + + @Autowired + private FtbCultivateLearnTaskCourseService ftbCultivateLearnTaskCourseService; + + @Autowired + private FtbCultivateLearnTaskExamService ftbCultivateLearnTaskExamService; + + @Autowired + private FtbCultivateLearnTaskIdentificationService ftbCultivateLearnTaskIdentificationService; + + @Autowired + private FtbCultivateLearnTaskCertificateService ftbCultivateLearnTaskCertificateService; + + @Autowired + private FtbCultivateLearnTaskPracticeService ftbCultivateLearnTaskPracticeService; + + + @Autowired + private FtbCultivateTaskLogService ftbCultivateTaskLogService; + + + @Autowired + private CultivateIdentifyTableService identifyTableService; + + @Autowired + private CultivateIdentifyItemsService identifyItemsService; + + @Autowired + private CultivateIdentifyApplyDetailsService applyDetailsService; + + @Autowired + private CultivateIdentifyApplyService identifyApplyService; + + @Autowired + private CultivateIdentifyApplyTableBackupsService applyTableBackupsService; + + @Autowired + private CultivateIdentifyApplyDetailsBackupsService applyDetailsBackupsService; + + + @Autowired + private V2CultivateCertificateService v2CultivateCertificateService; + + + @Autowired + private CultivateExamDrawRuleService examDrawRuleService; + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private FtbCultivateExamUserMapper ftbCultivateExamUserMapper; + + @Autowired + private FtbCultivateLearnTaskAssignmentMapper learnTaskAssignmentMapper; + + @Autowired + private FtbCultivateCertificateResultMapper ftbCultivateCertificateResultMapper; + + + @Autowired + private CultivateExamMapper v2CultivateExamMapper; + + + @Autowired + private ApplicationContext applicationContext; + + private V2CultivateTaskStudyService selfProxy; + + @PostConstruct + public void init() { + this.selfProxy = applicationContext.getBean(V2CultivateTaskStudyService.class); + } + + + /** + * 任务学习 + * + * @param message 培训信息 + */ + @Override + public void dealTaskCourseStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String courseId = message.getBusinessId(); + String taskId = message.getBusinessSourceId();//岗位学习id + String tenantId = message.getTenantId(); + if (StringUtils.isEmpty(taskId) || StringUtils.isEmpty(courseId) || StringUtils.isEmpty(userId)) { + return; + } + + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,examId={}, userId={}", userId); + return; + } + this.selfProxy.dealTaskStudyItem(userPrimaryBoundOne, taskId, tenantId); + } + + + /** + * 检查用户课程学习完成状态 + * + * @param userId 用户ID + * @param courseIds 课程ID列表 + * @return 课程是否全部完成学习(true - 全部完成 , false - 未全部完成) + */ + private boolean checkUserCourseCompleteStatus(String userId, List courseIds, String taskId, String phaseId) { + if (CollUtil.isEmpty(courseIds)) { + ftbCultivateTaskLogService.saveCourseTaskPhase(userId, taskId, phaseId, TaskStateEnum.NOT_CONFIGURED.getCode()); + return true; // 如果没有课程,认为已完成 + } + + boolean phaseCourseCompleteStatus = true; + Map courseLearnStatusMap = v2CultivateBatchQueryService.batchQueryUserCourseLearnStatus(userId, courseIds); + for (String id : courseIds) { + FtbCultivatePositionCourceLearning courseLearnStatus = courseLearnStatusMap.get(id); + if (courseLearnStatus == null) { + phaseCourseCompleteStatus = false; + break; + } + if (courseLearnStatus.getState() != 1) { + phaseCourseCompleteStatus = false; + break; + } + } + if (phaseCourseCompleteStatus) { + ftbCultivateTaskLogService.saveCourseTaskPhase(userId, taskId, phaseId, TaskStateEnum.COMPLETED.getCode()); + } + return phaseCourseCompleteStatus; + } + + /** + * 评估阶段的考试、技能和鉴定状态 + */ + private PhaseStatusResultVo evaluatePhaseStatus(String userId, String taskId, String tenantId, List examIds, List practiceIds, List identityIds, FtbCultivateLearnTaskPhase phaseEntity, Map userExamStatusMap, Map userIdentificationStatusMap, Map practiceTaskMap, Map> userSkillStatusMap) { + boolean isExam = true;//false-未完成 true-已完成 + boolean isPractice = true;//false-未完成 true-已完成 + boolean isIdentification = true; //false-未完成 true-已完成 + + boolean isExamDone = true;//考试是否做了 false-未做 true-已做 + boolean isIdentificationDone = true; //鉴定是否做了 false-未做 true-已做 + + Integer examStatus = 3;//3-优秀 2-合格 1-不合格 0-未考试 + Integer identificationStatus = 3;//3-优秀 2-合格 1-不合格 0-未鉴定 + String userExamId = ""; + String userIdentifyId = ""; + + // 处理考试状态 + if (CollUtil.isNotEmpty(examIds)) { + PhaseExamStatusVo examStatusResult = evaluateExamStatus(userId, taskId, examIds, userExamStatusMap, tenantId); + isExam = examStatusResult.isCompleted(); + isExamDone = examStatusResult.isDone(); + examStatus = examStatusResult.getStatus(); + if (examStatusResult.getExamUser() != null) { + userExamId = examStatusResult.getExamUser().getId(); + } + ftbCultivateTaskLogService.saveExamTaskPhase(userId, taskId, phaseEntity.getId(), isExam ? 1 : 0, examStatusResult.getExamUser()); + + } else { + ftbCultivateTaskLogService.saveExamTaskPhase(userId, taskId, phaseEntity.getId(), TaskStateEnum.NOT_CONFIGURED.getCode(), null); + } + + // 处理技能状态 + if (CollUtil.isNotEmpty(practiceIds)) { + isPractice = evaluatePracticeStatus(practiceIds, practiceTaskMap, userSkillStatusMap); + if (isPractice) { + ftbCultivateTaskLogService.savePracticeTaskPhase(userId, taskId, phaseEntity.getId(), TaskStateEnum.COMPLETED.getCode()); + } + } else { + ftbCultivateTaskLogService.savePracticeTaskPhase(userId, taskId, phaseEntity.getId(), TaskStateEnum.NOT_CONFIGURED.getCode()); + } + + // 处理鉴定状态 + if (CollUtil.isNotEmpty(identityIds)) { + PhaseIdentificationStatusVo identificationStatusResult = evaluateIdentificationStatus(userId, taskId, identityIds, phaseEntity, userIdentificationStatusMap, isExam, isPractice, tenantId); + isIdentification = identificationStatusResult.isCompleted(); + isIdentificationDone = identificationStatusResult.isDone(); + identificationStatus = identificationStatusResult.getStatus(); + ftbCultivateTaskLogService.saveIdentificationTaskPhase(userId, taskId, phaseEntity.getId(), isIdentificationDone ? 1 : 0, identificationStatusResult.getCultivateIdentifyApply()); + } else { + ftbCultivateTaskLogService.saveIdentificationTaskPhase(userId, taskId, phaseEntity.getId(), TaskStateEnum.NOT_CONFIGURED.getCode(), null); + } + + return new PhaseStatusResultVo(isExam, isPractice, isIdentification, isExamDone, isIdentificationDone, examStatus, identificationStatus, userExamId, userIdentifyId); + } + + /** + * 评估考试状态 + */ + private PhaseExamStatusVo evaluateExamStatus(String userId, String taskId, List examIds, Map userExamStatusMap, String tenantId) { + //false-未完成 true-已完成 + boolean isExam = true; + //考试是否做了 false-未做 true-已做 + boolean isExamDone = true;//必须有结果 + //3-优秀 2-合格 1-不合格 0-未考试 + int examStatus = 3; + + FtbCultivateExamUser ftbCultivateExamUser = userExamStatusMap.get(examIds.get(0)); + if (ftbCultivateExamUser == null) { + ftbCultivateExamUser = triggerExam(userId, examIds.get(0), 4, taskId, tenantId); + if (ftbCultivateExamUser == null) { + isExamDone = false; + isExam = false; + } + } + if (ftbCultivateExamUser != null) { + Integer status = ftbCultivateExamUser.getStatus(); + if (status.equals(0) || status.equals(1) || status.equals(2) || status.equals(4)) { + isExam = false; + examStatus = 1; + } + if (status.equals(0) || status.equals(1) || status.equals(2)) { + isExamDone = false; + } + if (status.equals(3)) { + examStatus = 2; + } + } + + return new PhaseExamStatusVo(isExam, isExamDone, examStatus, ftbCultivateExamUser); + } + + /** + * 评估技能状态 + */ + private boolean evaluatePracticeStatus(List practiceIds, Map practiceTaskMap, Map> userSkillStatusMap) { + for (String practiceId : practiceIds) { + FtbCultivateLearnTaskPracticeVo ftbCultivateLearnTaskPractice = practiceTaskMap.get(practiceId); + List count = userSkillStatusMap.get(practiceId); + if (ftbCultivateLearnTaskPractice == null || count == null || count.size() < ftbCultivateLearnTaskPractice.getNum()) { + return false; + } + } + return true; + } + + /** + * 评估鉴定状态 + */ + private PhaseIdentificationStatusVo evaluateIdentificationStatus(String userId, String taskId, List identityIds, FtbCultivateLearnTaskPhase phaseEntity, Map userIdentificationStatusMap, boolean isExam, boolean isPractice, String tenantId) { + boolean isIdentification = true; //false-未完成 true-已完成 + boolean isIdentificationDone = true; //鉴定是否做了 false-未做 true-已做 + int identificationStatus = 3;//3-优秀 2-合格 1-不合格 0-未鉴定 + + + Integer qualified = phaseEntity.getQualified(); //考试合格和练习满足才能完成鉴定【0-否 1-是】 + + boolean shouldCheckIdentification = true; + CultivateIdentifyApply cultivateIdentifyApply = null; + if (qualified.equals(1)) { // 需要考试和练习都完成才能鉴定 + shouldCheckIdentification = isExam; + } + + if (shouldCheckIdentification) { + cultivateIdentifyApply = userIdentificationStatusMap.get(identityIds.get(0)); + if (cultivateIdentifyApply == null) { + cultivateIdentifyApply = triggerIdentification(userId, identityIds.get(0), 4, taskId, phaseEntity.getId(), tenantId); + isIdentification = false; + isIdentificationDone = false; + identificationStatus = 0; + } else { + + if (cultivateIdentifyApply.getStatus().equals(0) || cultivateIdentifyApply.getStatus().equals(2)) { + isIdentification = false; + isIdentificationDone = false; + identificationStatus = 0; + } else { + if (cultivateIdentifyApply.getResult().equals(2)) { + isIdentification = false; + identificationStatus = 1; + } else if (cultivateIdentifyApply.getResult().equals(0)) { + identificationStatus = 2; + } else if (cultivateIdentifyApply.getResult().equals(1)) { + identificationStatus = 3; + } + } + } + } else { + isIdentification = false; + isIdentificationDone = false; + identificationStatus = 0; + } + + return new PhaseIdentificationStatusVo(isIdentification, isIdentificationDone, identificationStatus, cultivateIdentifyApply); + } + + /** + * 处理阶段完成逻辑(证书和日志记录) + */ + private void handlePhaseCompletion(String userId, String taskId, String tenantId, FtbCultivateLearnTaskPhase phaseEntity, List certificateIds, PhaseStatusResultVo phaseStatusResult) { + boolean isExam = phaseStatusResult.isExam(); + boolean isPractice = phaseStatusResult.isPractice(); + boolean isIdentification = phaseStatusResult.isIdentification(); + boolean isExamDone = phaseStatusResult.isExamDone(); + boolean isIdentificationDone = phaseStatusResult.isIdentificationDone(); + Integer examStatus = phaseStatusResult.getExamStatus(); + Integer identificationStatus = phaseStatusResult.getIdentificationStatus(); + String userExamId = phaseStatusResult.getUserExamId(); + String userIdentifyId = phaseStatusResult.getUserIdentifyId(); + + // 是否有证书 + if (phaseEntity.getIsPassComplete().equals(1)) { + if (isExam && isPractice && isIdentification) { + Integer issuedCertificate = processCertificateIssuance(certificateIds, phaseEntity, examStatus, identificationStatus, userId, tenantId); + ftbCultivateTaskLogService.saveOrUpdateByCondition(userId, taskId, phaseEntity.getId(), issuedCertificate); + // 保存考试结果状态、鉴定结果状态、用户考试ID和用户鉴定ID + saveExamAndIdentifyResultState(userId, taskId, phaseEntity.getId(), examStatus, identificationStatus, userExamId, userIdentifyId); + } + } else { + if (isExamDone && isIdentificationDone) { + Integer issuedCertificate = processCertificateIssuance(certificateIds, phaseEntity, examStatus, identificationStatus, userId, tenantId); + ftbCultivateTaskLogService.saveOrUpdateByCondition(userId, taskId, phaseEntity.getId(), issuedCertificate); + // 保存考试结果状态、鉴定结果状态、用户考试ID和用户鉴定ID + saveExamAndIdentifyResultState(userId, taskId, phaseEntity.getId(), examStatus, identificationStatus, userExamId, userIdentifyId); + } + } + } + + /** + * 保存考试结果状态、鉴定结果状态、用户考试ID和用户鉴定ID + * + * @param userId 用户ID + * @param taskId 任务ID + * @param phaseId 阶段ID + * @param examStatus 考试状态(0待考试,1待批阅,2已逾期,3合格,4不合格,5优秀) + * @param identificationStatus 鉴定状态(0-合格,1-优秀,2-不合格) + * @param userExamId 用户考试ID + * @param userIdentifyId 用户鉴定ID + */ + private void saveExamAndIdentifyResultState(String userId, String taskId, String phaseId, Integer examStatus, Integer identificationStatus, String userExamId, String userIdentifyId) { + // 转换考试状态:3-合格 -> 3, 4-不合格 -> 4, 5-优秀 -> 5 + if (examStatus != null && examStatus >= 3) { + ftbCultivateTaskLogService.saveExamResultState(userId, taskId, phaseId, examStatus); + } + + // 转换鉴定状态:2-合格 -> 0, 3-优秀 -> 1, 1-不合格 -> 2 + if (identificationStatus != null && identificationStatus >= 1) { + Integer identifyResult; + if (identificationStatus == 2) { + identifyResult = 0; // 合格 + } else if (identificationStatus == 3) { + identifyResult = 1; // 优秀 + } else if (identificationStatus == 1) { + identifyResult = 2; // 不合格 + } else { + identifyResult = -1; // 未配置 + } + ftbCultivateTaskLogService.saveIdentifyResultState(userId, taskId, phaseId, identifyResult); + } + + // 保存用户考试ID + if (userExamId != null && !userExamId.isEmpty()) { + ftbCultivateTaskLogService.saveUserExamId(userId, taskId, phaseId, userExamId); + } + + // 保存用户鉴定ID + if (userIdentifyId != null && !userIdentifyId.isEmpty()) { + ftbCultivateTaskLogService.saveUserIdentifyId(userId, taskId, phaseId, userIdentifyId); + } + } + + /** + * 处理证书颁发逻辑 + */ + private Integer processCertificateIssuance(List certificateIds, FtbCultivateLearnTaskPhase phaseEntity, Integer examStatus, Integer identificationStatus, String userId, String tenantId) { + if (CollUtil.isEmpty(certificateIds)) { + return TaskStateEnum.NOT_CONFIGURED.getCode(); + } + boolean isCanIssueCertificate = isCanIssueCertificate(phaseEntity, examStatus, identificationStatus); + if (isCanIssueCertificate) { + if (!checkIsTriggerTaskCertificate(userId, certificateIds.get(0), phaseEntity.getTaskId(), phaseEntity.id)) { + triggerCertificate(userId, certificateIds.get(0), tenantId); + recordTriggerTaskCertificate(userId, certificateIds.get(0), phaseEntity.getTaskId(), phaseEntity.id); + return TaskStateEnum.COMPLETED.getCode(); + } else { + return TaskStateEnum.COMPLETED.getCode(); + } + + } + return TaskStateEnum.INCOMPLETE.getCode(); + } + + + private boolean isCanIssueCertificate(FtbCultivateLearnTaskPhase phaseEntity, Integer examStatus, Integer identificationStatus) { +// Integer examStatus;//3-优秀 2-合格 1-不合格 0-未考试 +// Integer identificationStatus;//3-优秀 2-合格 1-不合格 0-未鉴定 + boolean isExam = false; + if (phaseEntity.getCertificateExam() == null || phaseEntity.getCertificateExam().equals(0)) { + isExam = true; + } else if (phaseEntity.getCertificateExam().equals(1)) { + if (examStatus >= 2) { + isExam = true; + } + } else if (phaseEntity.getCertificateExam().equals(2)) { + if (examStatus >= 3) { + isExam = true; + } + } + + boolean isIdentification = false; + if (phaseEntity.getCertificateIdentification() == null || phaseEntity.getCertificateIdentification().equals(0)) { + isIdentification = true; + } else if (phaseEntity.getCertificateIdentification().equals(1)) { + if (identificationStatus >= 2) { + isIdentification = true; + } + } else if (phaseEntity.getCertificateIdentification().equals(2)) { + if (identificationStatus >= 3) { + isIdentification = true; + } + } + return isExam && isIdentification; + } + + private void checkTaskComplete(String taskId, String userId) { + + List phaseList = getFtbCultivateLearnTaskPhases(taskId); + if (CollUtil.isEmpty(phaseList)) { + return; + } + List completePhaseList = ftbCultivateTaskLogService.queryCompleteTaskPhase(taskId, userId); + if (CollUtil.isEmpty(completePhaseList)) { + return; + } + List completePhaseIds = new ArrayList<>(); + boolean isGetCert = false; + for (FtbCultivateTaskLog ftbCultivateTaskLog : completePhaseList) { + completePhaseIds.add(ftbCultivateTaskLog.getPhaseId()); + if (ftbCultivateTaskLog.getIssuedCertificate().equals(1)) { + isGetCert = true; + } + } + List phaseIds = phaseList.stream().map(FtbCultivateLearnTaskPhase::getId).collect(Collectors.toList()); + if (UserApiV2Util.areListsEqual(completePhaseIds, phaseIds)) { + //任务完成 + recordTaskComplete(userId, taskId, isGetCert); + } + } + + + /** + * 记录任务完成 + * + * @param userId 用户ID + * @param taskId 任务ID + * @param isGetCertificate 是否获取证书 + */ + private void recordTaskComplete(String userId, String taskId, boolean isGetCertificate) { + // 任务记录证书颁发 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbCultivateLearnTaskAssignment::getStudyStats, 2).set(FtbCultivateLearnTaskAssignment::getLearningEndTime, new Date()).eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0).eq(FtbCultivateLearnTaskAssignment::getUserId, userId).eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + if (isGetCertificate) { + updateWrapper.set(FtbCultivateLearnTaskAssignment::getIssuedCertificate, 1); + } else { + updateWrapper.set(FtbCultivateLearnTaskAssignment::getIssuedCertificate, 0); + } + learnTaskAssignmentMapper.update(null, updateWrapper); + } + + + /** + * 任务考试学习 + * + * @param message 培训信息 + */ + @Override + public void dealTaskExamStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String examId = message.getBusinessId(); + String tenantId = message.getTenantId(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,examId={}, userId={}", examId, userId); + return; + } + if (StringUtils.isEmpty(examId) || StringUtils.isEmpty(userId)) { + log.error("dealTaskExamStudy参数不完整,examId={}, userId={}", examId, userId); + return; + } + List taskListVoList = ftbCultivateLearnTaskService.queryMyNoCompleteTaskListForUserId(userId); + if (CollUtil.isEmpty(taskListVoList)) { + return; + } + List taskIds = taskListVoList.stream().map(V2MyCultivateLearnTaskListVo::getTaskId).collect(Collectors.toList()); + //过滤有改考试的任务 + taskIds = ftbCultivateLearnTaskService.queryTaskHasExam(taskIds, examId); + if (CollUtil.isEmpty(taskIds)) { + return; + } + for (String taskId : taskIds) { + // 查询任务阶段表 + this.selfProxy.dealTaskStudyItem(userPrimaryBoundOne, taskId, tenantId); + } + + } + + @Transactional + @Override + public void dealTaskStudyItem(UserBoundVO userPrimaryBoundOne, String taskId, String tenantId) { + String userId = userPrimaryBoundOne.getId(); + + // 验证任务有效性 + if (!validateTask(taskId, userId)) { + return; + } + + List phaseList = getFtbCultivateLearnTaskPhases(taskId); + // 如果没有阶段,则直接返回 + if (CollUtil.isEmpty(phaseList)) { + return; + } + //查询已经完成的阶段 + List completePhaseIds = new ArrayList<>(); + List completePhaseList = ftbCultivateTaskLogService.queryCompleteTaskPhase(taskId, userId); + if (CollUtil.isNotEmpty(completePhaseList)) { + completePhaseIds = completePhaseList.stream().map(FtbCultivateTaskLog::getPhaseId).distinct().collect(Collectors.toList()); + } + + List courseEntities = ftbCultivateLearnTaskCourseService.listByTaskId(taskId, 0); + List examEntities = ftbCultivateLearnTaskExamService.listByTaskId(taskId); + List identificationEntities = ftbCultivateLearnTaskIdentificationService.listByTaskId(taskId); + List certificateEntities = ftbCultivateLearnTaskCertificateService.listByTaskId(taskId); + List practiceEntities = ftbCultivateLearnTaskPracticeService.listByTaskId(taskId); + + //课程按照阶段分组 + Map> phaseCourseMap = courseEntities.stream().filter(course -> course.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskCourseVo::getPhaseId)); + //考试按阶段分组 + Map> phaseExamMap = examEntities.stream().filter(exam -> exam.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskExamVo::getPhaseId)); + //鉴定按阶段分组 + Map> phaseIdentificationMap = identificationEntities.stream().filter(identification -> identification.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskIdentificationVo::getPhaseId)); + //证书按阶段分组 + Map> phaseCertificateMap = certificateEntities.stream().filter(certificate -> certificate.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskCertificateVo::getPhaseId)); + //练习按阶段分组 + Map> phasePracticeMap = practiceEntities.stream().filter(practice -> practice.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskPracticeVo::getPhaseId)); + + //确定是那个阶段 + for (FtbCultivateLearnTaskPhase phaseEntity : phaseList) { + + if (completePhaseIds.contains(phaseEntity.getId())) { + continue; + } + List taskCourseList = phaseCourseMap.get(phaseEntity.getId()); + if (CollUtil.isEmpty(taskCourseList)) { + break; + } + //设置为开始,如果没有开始就设置为开始 + checkAndSetTaskPhaseStart(userId, taskId, phaseEntity.getId()); + List courseIds = CollUtil.isNotEmpty(taskCourseList) ? taskCourseList.stream().map(FtbCultivateLearnTaskCourseVo::getCourseId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + boolean phaseCourseCompleteStatus = checkUserCourseCompleteStatus(userId, courseIds, phaseEntity.getTaskId(), phaseEntity.getId()); + if (!phaseCourseCompleteStatus) { + break; + } + List taskExamList = phaseExamMap.get(phaseEntity.getId()); + List taskIdentificationList = phaseIdentificationMap.get(phaseEntity.getId()); + List taskCertificateList = phaseCertificateMap.get(phaseEntity.getId()); + List taskPracticeList = phasePracticeMap.get(phaseEntity.getId()); + Map practiceTaskMap = new HashMap<>(); + if (CollUtil.isNotEmpty(taskPracticeList)) { + for (FtbCultivateLearnTaskPracticeVo practice : taskPracticeList) { + practiceTaskMap.put(practice.getBusinessId(), practice); + } + } + + List examIds = CollUtil.isNotEmpty(taskExamList) ? taskExamList.stream().map(FtbCultivateLearnTaskExamVo::getExamId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + List identityIds = CollUtil.isNotEmpty(taskIdentificationList) ? taskIdentificationList.stream().map(FtbCultivateLearnTaskIdentificationVo::getIdentificationId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + List certificateIds = CollUtil.isNotEmpty(taskCertificateList) ? taskCertificateList.stream().map(FtbCultivateLearnTaskCertificateVo::getCertificateId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + List practiceIds = CollUtil.isNotEmpty(taskPracticeList) ? taskPracticeList.stream().map(FtbCultivateLearnTaskPracticeVo::getBusinessId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + + + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationStatusMap = v2CultivateBatchQueryService.batchQueryUserIdentificationStatus(userId, identityIds, 4, phaseEntity.getTaskId()); +// Map userCertificateStatusMap = v2CultivateBatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillStatusMap = v2CultivateBatchQueryService.batchQueryUserSkillStatus(userId, practiceIds); + + // 检查考试、技能和鉴定状态 + PhaseStatusResultVo phaseStatusResult = evaluatePhaseStatus(userId, taskId, tenantId, examIds, practiceIds, identityIds, phaseEntity, userExamStatusMap, userIdentificationStatusMap, practiceTaskMap, userSkillStatusMap); + + // 根据阶段完成条件处理证书和日志记录 + handlePhaseCompletion(userId, taskId, tenantId, phaseEntity, certificateIds, phaseStatusResult); + List taskLogList = ftbCultivateTaskLogService.queryTaskPhaseLogs(userId, taskId, phaseEntity.getId()); + if (CollUtil.isNotEmpty(taskLogList)) { + if (!taskLogList.get(0).getState().equals(2)) { + break; + } + } + } + //检查任务是否完成 + checkTaskComplete(taskId, userId); + } + + private void checkAndSetTaskPhaseStart(String userId, String taskId, String phaseId) { + ftbCultivateTaskLogService.checkAndSetTaskPhaseStart(userId, taskId, phaseId); + } + + @Override + public void preTaskCourseStudy(String userId, String courseId, String currTaskId, String tenantId) { + List taskIds = List.of(currTaskId); + for (String taskId : taskIds) { + // 验证任务有效性 + if (!validateTask(taskId, userId)) { + return; + } + List phaseList = getFtbCultivateLearnTaskPhases(taskId); + // 如果没有阶段,则直接返回 + if (CollUtil.isEmpty(phaseList)) { + return; + } + //查询已经完成的阶段 + List completePhaseIds = new ArrayList<>(); + List completePhaseList = ftbCultivateTaskLogService.queryCompleteTaskPhase(taskId, userId); + if (CollUtil.isNotEmpty(completePhaseList)) { + completePhaseIds = completePhaseList.stream().map(FtbCultivateTaskLog::getPhaseId).distinct().collect(Collectors.toList()); + } + + List courseEntities = ftbCultivateLearnTaskCourseService.listByTaskId(taskId, 0); + List examEntities = ftbCultivateLearnTaskExamService.listByTaskId(taskId); + //课程按照阶段分组 + Map> phaseCourseMap = courseEntities.stream().filter(course -> course.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskCourseVo::getPhaseId)); + //考试按阶段分组 + Map> phaseExamMap = examEntities.stream().filter(exam -> exam.getPhaseId() != null).collect(Collectors.groupingBy(FtbCultivateLearnTaskExamVo::getPhaseId)); + + //确定是那个阶段 + for (FtbCultivateLearnTaskPhase phaseEntity : phaseList) { + + if (completePhaseIds.contains(phaseEntity.getId())) { + continue; + } + List taskCourseList = phaseCourseMap.get(phaseEntity.getId()); + if (CollUtil.isEmpty(taskCourseList)) { + break; + } + List courseIds = CollUtil.isNotEmpty(taskCourseList) ? taskCourseList.stream().map(FtbCultivateLearnTaskCourseVo::getCourseId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + boolean phaseCourseCompleteStatus = checkUserCourseCompleteStatus(userId, courseIds, phaseEntity.getTaskId(), phaseEntity.getId()); + if (!phaseCourseCompleteStatus) { + break; + } + List taskExamList = phaseExamMap.get(phaseEntity.getId()); + List examIds = CollUtil.isNotEmpty(taskExamList) ? taskExamList.stream().map(FtbCultivateLearnTaskExamVo::getExamId).distinct().collect(Collectors.toList()) : new ArrayList<>(); + Map userExamStatusMap = v2CultivateBatchQueryService.batchQueryUserExamStatus(userId, examIds); + // 处理考试状态 + if (CollUtil.isNotEmpty(examIds)) { + checkAndTriggerExam(userId, taskId, phaseEntity, examIds, userExamStatusMap, tenantId); + } else { + ftbCultivateTaskLogService.saveExamTaskPhase(userId, taskId, phaseEntity.getId(), TaskStateEnum.NOT_CONFIGURED.getCode(), null); + } + } + } + } + + + + + @Override + public FtbCultivateExamUser checkAndTriggerExam(String userId, String taskId, FtbCultivateLearnTaskPhase phaseEntity, List examIds, Map userExamStatusMap, String tenantId) { + PhaseExamStatusVo examStatusResult = evaluateExamStatus(userId, taskId, examIds, userExamStatusMap, tenantId); + ftbCultivateTaskLogService.saveExamTaskPhase(userId, taskId, phaseEntity.getId(), examStatusResult.isCompleted() ? 1 : 0, examStatusResult.getExamUser()); + + return examStatusResult.getExamUser(); + } + + private List getFtbCultivateLearnTaskPhases(String taskId) { + LambdaQueryWrapper phaseWrapper = Wrappers.lambdaQuery(); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getTaskId, taskId); + phaseWrapper.eq(FtbCultivateLearnTaskPhase::getEnableMark, 0); // 只查询有效的 + phaseWrapper.orderByAsc(FtbCultivateLearnTaskPhase::getCreatorTime); + return ftbCultivateLearnTaskPhaseService.list(phaseWrapper); + } + + /** + * 验证任务有效性 + * + * @param taskId 任务 ID + * @param userId 用户 ID + * @return 任务有效返回 true,否则返回 false + */ + private boolean validateTask(String taskId, String userId) { + FtbCultivateLearnTask task = ftbCultivateLearnTaskService.getById(taskId); + if (task == null) { + return false; + } + if (task.getEnableMark().equals(1)) { + log.error("任务已删除,taskId={}, userId={}", taskId, userId); + return false; + } + if (task.getTaskType().equals(1) && task.getStatus().equals(3)) { + log.error("任务已结束,taskId={}, userId={}", taskId, userId); + return false; + } + if (task.getStatus().equals(4)) { + log.error("任务已终止,taskId={}, userId={}", taskId, userId); + return false; + } + return true; + } + + /** + * 岗位鉴定学习 + * + * @param message 培训信息 + */ + @Override + public void dealTaskIdentityStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String identityId = message.getBusinessId(); + String tenantId = message.getTenantId(); + if (StringUtils.isEmpty(identityId) || StringUtils.isEmpty(userId)) { + log.error("dealTaskIdentityStudy参数异常,identityId={}, userId={}", identityId, userId); + return; + } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,identityId={}, userId={}", identityId, userId); + return; + } + List taskListVoList = ftbCultivateLearnTaskService.queryMyNoCompleteTaskListForUserId(userId); + if (CollUtil.isEmpty(taskListVoList)) { + return; + } + List taskIds = taskListVoList.stream().map(V2MyCultivateLearnTaskListVo::getTaskId).collect(Collectors.toList()); + //过滤有改考试的任务 + taskIds = ftbCultivateLearnTaskService.queryTaskHasIdentityId(taskIds, identityId); + if (CollUtil.isEmpty(taskIds)) { + return; + } + for (String taskId : taskIds) { + // 查询任务阶段表 + this.selfProxy.dealTaskStudyItem(userPrimaryBoundOne, taskId, tenantId); + } + + } + + + /** + * 任务技能学习 + * + * @param message 培训信息 + */ + @Override + public void dealTaskPracticeStudy(CultivateMqDTO message) { + String userId = message.getUserId(); + String practiceId = message.getBusinessId(); + String tenantId = message.getTenantId(); + if (StringUtils.isEmpty(practiceId) || StringUtils.isEmpty(userId)) { + log.error("dealPositionExamStudy参数不完整,practiceId={}, userId={}", practiceId, userId); + return; + } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne == null) { + log.error("用户查询未null,practiceId={}, userId={}", practiceId, userId); + return; + } + List taskListVoList = ftbCultivateLearnTaskService.queryMyNoCompleteTaskListForUserId(userId); + if (CollUtil.isEmpty(taskListVoList)) { + return; + } + List taskIds = taskListVoList.stream().map(V2MyCultivateLearnTaskListVo::getTaskId).collect(Collectors.toList()); + //过滤有改考试的任务 + taskIds = ftbCultivateLearnTaskService.queryTaskHasPracticeId(taskIds, practiceId); + if (CollUtil.isEmpty(taskIds)) { + return; + } + for (String taskId : taskIds) { + // 查询任务阶段表 + this.selfProxy.dealTaskStudyItem(userPrimaryBoundOne, taskId, tenantId); + } + + } + + + /** + * 删除申请详情 + * + * @param appId 鉴定id + */ + private void deleteApplyDetails(String appId) { + applyDetailsService.update(new LambdaUpdateWrapper().set(CultivateIdentifyApplyDetails::getDeleteMark, 1).eq(CultivateIdentifyApplyDetails::getApplyId, appId).eq(CultivateIdentifyApplyDetails::getDeleteMark, 0)); + } + + /** + * 触发考试 + * + * @param userId 用户id + * @param examId 考试id + * @param source 来源 + * @param sourceId 来源id + */ + public FtbCultivateExamUser triggerExam(String userId, String examId, Integer source, String sourceId, String tenantId) { + log.error("triggerExam userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + CultivateExam ftbCultivateExam = v2CultivateExamMapper.selectById(examId); + if (ftbCultivateExam == null) { + log.error("triggerExam 考试不存在在 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + return null; + } + if (ftbCultivateExam.getEnabledMark().equals(0)) { + log.error("triggerExam 考试已删除 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + return null; + } + //异常正常触发 用户考试的时候提示 +// if (ftbCultivateExam.getNeedUpdate().equals(2)) { +// log.error("triggerExam 考试已结束 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); +// return null; +// } + + //查看是否有记录 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper().eq(FtbCultivateExamUser::getUserId, userId).eq(FtbCultivateExamUser::getExamId, examId).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()).orderByDesc(FtbCultivateExamUser::getCreatorTime); + List examUserList = ftbCultivateExamUserMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(examUserList)) { + log.error("triggerExam 考试已经完成或存在 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + + return examUserList.get(0); + } + +// LambdaQueryWrapper wrapperNoComplete = new LambdaQueryWrapper().eq(FtbCultivateExamUser::getUserId, userId).eq(FtbCultivateExamUser::getExamId, examId).in(FtbCultivateExamUser::getStatus, 0, 2).eq(FtbCultivateExamUser::getEnabledMark, CourseEnums.EnabledMarkType.VALID.getCode()); +// List examUserListComplete = ftbCultivateExamUserMapper.selectList(wrapperNoComplete); +// if (CollUtil.isNotEmpty(examUserListComplete)) { +// log.error("triggerExam 考试已经存在 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); +// return; +// } + + + List ruleList = examDrawRuleService.list(new LambdaQueryWrapper().eq(CultivateExamDrawRule::getExamId, ftbCultivateExam.getId())); + + + //2、没有就写入一条记录 + FtbCultivateExamUser user = FtbCultivateExamUser.builder().examId(examId).examType(ftbCultivateExam.getExamType()).userId(userId).status(CourseEnums.ExamStatus.WAIT.getCode()).examSource(source).enabledMark(CourseEnums.EnabledMarkType.VALID.getCode()).batch(UUID.randomUUID().toString().replaceAll("-", "")).userExamCount(1).build(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userPrimaryBoundOne != null && userPrimaryBoundOne.getOrganizeId() != null) { + user.setUserOrgList(userPrimaryBoundOne.getOrganizeId()); + } + if (CollUtil.isNotEmpty(ruleList)) { + user.setPickQuestionRule(JSON.toJSONString(ruleList)); + int totalScore = 0; + int questionNumber = 0; + for (CultivateExamDrawRule cultivateExamDrawRule : ruleList) { + totalScore += cultivateExamDrawRule.getScore() * cultivateExamDrawRule.getPickCount(); + questionNumber += cultivateExamDrawRule.getPickCount(); + } + user.setTotalScore(totalScore); + user.setQuestionNumber(questionNumber); + user.setPassScore(QuestionAnalysisUtil.calPassScore(totalScore, ftbCultivateExam)); + user.setExcellentScore(QuestionAnalysisUtil.calExcellentScore(totalScore, ftbCultivateExam)); + user.setExamTime(ftbCultivateExam.getExamTime()); + } + if (source.equals(2)) { + user.setRelationPositionId(sourceId); + } else if (source.equals(4)) { + user.setRelationTaskId(sourceId); + } + ftbCultivateExamUserMapper.insert(user); + log.error("triggerExam 生成考试记录 userId={},examId={},source={},sourceId={}", userId, examId, source, sourceId); + return user; + } + + /** + * 触发鉴定 + * + * @param userId 用户id + * @param identifyId 鉴定id + * @param source 鉴定来源(0手动发起,1课程学习鉴定,2岗位学习鉴定,3本人申请,4任务鉴定) + * @param sourceId 鉴定来源id + * @param phaseId 阶段 + */ + public CultivateIdentifyApply triggerIdentification(String userId, String identifyId, Integer source, String sourceId, String phaseId, String tenantId) { + log.info("触发鉴定 userId={}, identifyId={}, source={}, sourceId ={}", userId, identifyId, source, sourceId); + CultivateIdentifyTable table = identifyTableService.getById(identifyId); + if (table == null) { + log.error("鉴定表不存在 userId={}, identifyId={}, source={}, sourceId ={}", userId, identifyId, source, sourceId); + return null; + } + + List identifyItemsList = this.identifyItemsService.lambdaQuery().eq(CultivateIdentifyItems::getTableId, table.getId()).eq(CultivateIdentifyItems::getDeleteMark, ConstantUtil.NUM_FALSE).list(); + if (CollUtil.isEmpty(identifyItemsList)) { + log.error("鉴定表项不存在 userId={}, identifyId={}, source={}, sourceId ={}", userId, identifyId, source, sourceId); + } + UserBoundVO userBoundVO = userApiV2Util.getUserPrimaryBoundOne(userId, tenantId); + if (userBoundVO == null) { + return null; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(CultivateIdentifyApply::getSourceId, sourceId); + wrapper.eq(CultivateIdentifyApply::getSource, source); + wrapper.eq(CultivateIdentifyApply::getOriginalTableId, table.getId()); + wrapper.eq(CultivateIdentifyApply::getBeIdentifyUserId, userId); + wrapper.eq(CultivateIdentifyApply::getPhaseId, phaseId); + wrapper.eq(CultivateIdentifyApply::getDeleteMark, ConstantUtil.NUM_FALSE); + wrapper.orderByDesc(CultivateIdentifyApply::getCreatorTime); + List oldApplyLists = identifyApplyService.list(wrapper); + + if (CollUtil.isNotEmpty(oldApplyLists)) { + CultivateIdentifyApply oldApply = oldApplyLists.get(0); + if (oldApply.getStatus().equals(1) || oldApply.getStatus().equals(0)) { + return oldApply; + } + oldApply.setDeleteMark(1); + deleteApplyDetails(oldApply.getId()); + identifyApplyService.updateById(oldApply); + } + CultivateIdentifyApply apply = new CultivateIdentifyApply(); + apply.setId(UUID.randomUUID().toString()); + apply.setIdentifyUserId(""); + apply.setIdentifyOrgList(""); + apply.setSourceId(sourceId); + apply.setSource(source); + apply.setTableId(table.getId()); + apply.setOriginalTableId(table.getId()); + apply.setName(table.getName()); + apply.setBeIdentifyUserId(userId); + apply.setStatus(ApplyStatusEnum.DJD.getCode()); + apply.setAppraisalResults(ConstantUtil.NUM_FALSE); + apply.setUseTime(0); + apply.setIsVisible(0); + apply.setPhaseId(phaseId); + + //设置被鉴定人信息 + apply.setIsChoose(ConstantUtil.NUM_FALSE); + apply.setStudyFinishTime(new Date()); + apply.setIsReIdentify(ConstantUtil.NUM_FALSE); + apply.setDeleteMark(0); + // 岗位学习和课程实操鉴定,不进行直属主管判定,因为直属主管需要组织、岗位、职等进行确定 + + //被鉴定人组织信息 + List beIdentifyUserInfoJsonList = CollUtil.newArrayList(); + UserOrgInfoAll1Vo all1Vo = new UserOrgInfoAll1Vo(); + all1Vo.setUserOrgCode(userBoundVO.getOrganizeEnCode()); + all1Vo.setUserOrgName(userBoundVO.getOrganizeName()); + all1Vo.setUserPostCode(userBoundVO.getPositionEnCode()); + all1Vo.setUserPostName(userBoundVO.getPositionName()); + all1Vo.setUserOfficialRankCode(userBoundVO.getGradeId()); + all1Vo.setUserOfficialRankName(userBoundVO.getGradeName()); + beIdentifyUserInfoJsonList.add(all1Vo); + + apply.setBeIdentifyOrgList(userBoundVO.getOrganizeId()); + apply.setBeOfficialRankInfoJson(JSON.toJSONString(beIdentifyUserInfoJsonList)); + + //生成鉴定表、鉴定项备份数据 + CultivateIdentifyApplyTableBackups applyTableBackups = new CultivateIdentifyApplyTableBackups(); + BeanUtils.copyProperties(table, applyTableBackups); + applyTableBackups.setId(RandomUtil.uuId()); + applyTableBackups.setTableId(table.getId()); + apply.setTableId(applyTableBackups.getId()); + + List applyDetailsBackupsList = CollUtil.newArrayList(); + if (CollUtil.isNotEmpty(identifyItemsList)) { + applyDetailsBackupsList.addAll(identifyItemsList.stream().map(item -> { + CultivateIdentifyApplyDetailsBackups applyDetailsBackups = new CultivateIdentifyApplyDetailsBackups(); + BeanUtils.copyProperties(item, applyDetailsBackups); + applyDetailsBackups.setId(RandomUtil.uuId()); + applyDetailsBackups.setItemsId(item.getId()); + applyDetailsBackups.setTableId(applyTableBackups.getId()); + applyDetailsBackups.setDeleteMark(0); + return applyDetailsBackups; + }).collect(Collectors.toList())); + } + //生成鉴定项数据 + List applyDetailsList = CollUtil.newArrayList(); + + applyDetailsList.addAll(applyDetailsBackupsList.stream().map(item -> { + CultivateIdentifyApplyDetails applyDetails = new CultivateIdentifyApplyDetails(); + applyDetails.setApplyId(apply.getId()); + applyDetails.setItemsId(item.getId()); + applyDetails.setDeleteMark(0); + return applyDetails; + }).collect(Collectors.toList())); + //保存备份数据 + applyTableBackupsService.save(applyTableBackups); + if (CollUtil.isNotEmpty(applyDetailsBackupsList)) { + applyDetailsBackupsService.saveBatch(applyDetailsBackupsList); + } + + if (CollUtil.isNotEmpty(applyDetailsList)) { + this.applyDetailsService.saveBatch(applyDetailsList); + } + identifyApplyService.save(apply); + return apply; + } + + /** + * 鉴定表项是否触发任务证书颁发 + * + * @param userId 用户id + * @param certificateId 证书id + * @param taskId 任务id + * @param phaseId 阶段id + * @return true-已经触发过了 false-未触发 + */ + private Boolean checkIsTriggerTaskCertificate(String userId, String certificateId, String taskId, String phaseId) { + //查询是否颁发 + LambdaQueryWrapper certificateResultLambdaQueryWrapper = Wrappers.lambdaQuery(); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getUserId, userId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getCertificateId, certificateId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getTaskId, taskId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getPhaseId, phaseId); + certificateResultLambdaQueryWrapper.eq(FtbCultivateCertificateResult::getType, 3);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + return ftbCultivateCertificateResultMapper.selectCount(certificateResultLambdaQueryWrapper) > 0; + } + + /** + * 记录触发的任务证书 + * + * @param userId 用户id + * @param certificateId 证书id + * @param taskId 任务id + * @param phaseId 阶段id + */ + private void recordTriggerTaskCertificate(String userId, String certificateId, String taskId, String phaseId) { + FtbCultivateCertificateResult cultivateCertificateResult = new FtbCultivateCertificateResult(); + cultivateCertificateResult.setUserId(userId); + cultivateCertificateResult.setCertificateId(certificateId); + cultivateCertificateResult.setTaskId(taskId); + cultivateCertificateResult.setPhaseId(phaseId); + cultivateCertificateResult.setType(3);//来源,1:岗位学习证书,2:课程证书 3: 任务证书 + ftbCultivateCertificateResultMapper.insert(cultivateCertificateResult); + } + + /** + * 触发证书 + * + * @param userId 用户id + * @param certificateId 证书id + * @param tenantId 租户id + */ + public void triggerCertificate(String userId, String certificateId, String tenantId) { + v2CultivateCertificateService.triggerCertificate(userId, certificateId, tenantId); + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2TeachingSkillServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2TeachingSkillServiceImpl.java new file mode 100644 index 0000000..d0edbf7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/service/impl/V2TeachingSkillServiceImpl.java @@ -0,0 +1,365 @@ +package jnpf.cultivate.v2.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.cultivate.service.TeachingRecordService; +import jnpf.cultivate.service.TeachingSkillService; +import jnpf.cultivate.v2.service.FtbCultivateLabelService; +import jnpf.cultivate.v2.service.V2TeachingSkillService; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.entity.cultivate.TeachingSkill; +import jnpf.exception.HandleException; +import jnpf.model.cultivate.po.label.FtbCultivateLabel; +import jnpf.model.cultivate.v2.exam.req.SkillDto; +import jnpf.model.cultivate.v2.exam.vo.ErrorObjectVo; +import jnpf.model.cultivate.v2.exam.vo.ImportObjectVo; +import jnpf.model.cultivate.v2.exam.vo.SkillUploadInfoVo; +import jnpf.model.cultivate.v2.exam.vo.SkillVo; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.util.*; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 技能点服务实现[v2] + * + * @author yanwenfu + * @create 2026-02-28 + */ +@Service +public class V2TeachingSkillServiceImpl implements V2TeachingSkillService { + + @Resource + private TeachingSkillService teachingSkillService; + @Resource + private FtbCultivateLabelService cultivateLabelService; + @Resource + private TeachingRecordService teachingRecordService; + + private static final Pattern EMOJI_PATTERN = Pattern.compile( + "[\uD83C\uDF00-\uD83D\uDDFF]|" + + "[\uD83D\uDE00-\uD83D\uDE4F]|" + + "[\uD83D\uDE80-\uD83D\uDEFF]|" + + "[\u2600-\u26FF]|" + + "[\u2700-\u27BF]", + Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE + ); + + @Override + public SkillUploadInfoVo teachingSkillImport(MultipartFile file) throws Exception { + + SkillUploadInfoVo uploadInfo = new SkillUploadInfoVo(); + + try (InputStream is = file.getInputStream(); + XSSFWorkbook workbook = new XSSFWorkbook(is)) { + // 获取数据 + XSSFSheet sheet = workbook.getSheetAt(0); + List dataFromSheet = TemplateExcelUtils.getDataFromSheetWithNullRow(sheet); + if (dataFromSheet.isEmpty() || dataFromSheet.size() == 1) { + throw new HandleException("未获取到工作簿数据"); + } + List list = new ArrayList<>(); + Set set = new HashSet<>(); + String[] row1 = dataFromSheet.get(0); + if (row1.length < 3 || !row1[0].equals("*技能点名称") || !row1[1].equals("分类") || !row1[2].equals("技能点描述")) { + throw new HandleException("当前模板非技能点导入模板,请上传正确模板及内容"); + } + for (int i = 1; i < dataFromSheet.size(); i++) { + List array = List.of(dataFromSheet.get(i)); + if (StringUtil.isEmpty(array.get(0)) && StringUtil.isEmpty(array.get(1)) && StringUtil.isEmpty(array.get(2))) { + continue; + } + ImportObjectVo importObj = new ImportObjectVo(); + importObj.setIndex(i); + String errorMsg = ""; + for (int j = 0; j < array.size(); j++) { + String str = array.get(j); + if (j == 0) { + // 技能点名称 + String msg = checkStr(i, j, str, 20, errorMsg); + if (StringUtil.isNotEmpty(msg)) { + errorMsg += msg; + } + importObj.setSkillName(str); + } + if (j == 1) { + // 技能点分类 + if (StringUtil.isNotEmpty(str)) { + importObj.setCategoryName(str); + } + } + if (j == 2) { + // 技能点描述 + String msg = checkStr(i, j, str, 500, errorMsg); + if (StringUtil.isNotEmpty(msg)) { + errorMsg += StringUtil.isNotEmpty(errorMsg) ? "," + msg : msg; + } + importObj.setDescription(str); + } + } + // 报错了 + if (StringUtil.isNotEmpty(errorMsg)) { + ErrorObjectVo errorObj = JsonUtil.getJsonToBean(importObj, ErrorObjectVo.class); + errorObj.setErrorReason(errorMsg); + uploadInfo.getFailList().add(errorObj); + uploadInfo.setFailNum(uploadInfo.getFailNum() + 1); + continue; + } + // 说明内容未重复 + if (set.add(importObj.getSkillName())) { + list.add(importObj); + } else { + ErrorObjectVo errorObj = JsonUtil.getJsonToBean(importObj, ErrorObjectVo.class); + errorObj.setErrorReason("第" + (i + 1) + "行 技能点名称重复"); + uploadInfo.getFailList().add(errorObj); + uploadInfo.setFailNum(uploadInfo.getFailNum() + 1); + } + } + if (!list.isEmpty()) { + // 判断list size + db size是否大于100 舍弃大于100的 + Long dbCount = getDbCount(); + // 还能导入的条数 + int insertCount = 100 - dbCount.intValue(); + if (insertCount <= 0) { + // 数据库配置数已满, 无法导入 + // -1 是因为第一条是表头 + uploadInfo.setFailNum(uploadInfo.getFailNum() + list.size()); + list.forEach(v -> { + ErrorObjectVo errorObj = JsonUtil.getJsonToBean(v, ErrorObjectVo.class); + errorObj.setErrorReason("已超过技能点配置最大条数(100)限制"); + uploadInfo.getFailList().add(errorObj); + }); + uploadInfo.getFailList().sort(Comparator.comparing(ErrorObjectVo::getIndex)); + // 生成失败excel + String failBase64 = generateFailExcelBase64(uploadInfo.getFailList()); + uploadInfo.setErrorExcelBase64(failBase64); + return uploadInfo; + } + // 判断list中有没有与数据库重复的 + List exceptIndex = teachingSkillService.getDistinctCountBatch(list); + // 去除重复的 + if (!exceptIndex.isEmpty()) { + addFailDataToList(list, uploadInfo, exceptIndex, "技能点名称重复"); + } + // 验证category + List categoryNameList = list.stream().map(ImportObjectVo::getCategoryName).filter(StringUtil::isNotEmpty).distinct().collect(Collectors.toList()); + Map labelMap = cultivateLabelService.getLabelMapByName(categoryNameList); + List categoryErrorIndex = new ArrayList<>(); + list.forEach(v -> { + if (StringUtil.isNotEmpty(v.getCategoryName())) { + FtbCultivateLabel label = labelMap.get(v.getCategoryName()); + if (null == label) { + categoryErrorIndex.add(v.getIndex()); + } + } + }); + if (!categoryErrorIndex.isEmpty()) { + addFailDataToList(list, uploadInfo, categoryErrorIndex, "分类异常"); + } + // 不可导入的条数 + int overflowCount = insertCount - list.size(); + if (overflowCount < 0) { + overflowCount = Math.abs(overflowCount); + // 被截掉的部分,取最后 exceptCount 条 + List removedIndexes = list.subList(list.size() - overflowCount, list.size()) + .stream() + .map(ImportObjectVo::getIndex) + .collect(Collectors.toList()); + addFailDataToList(list, uploadInfo, removedIndexes, "已超过文字配置最大条数(100)限制"); + } + } + if (uploadInfo.getFailNum() > 0) { + uploadInfo.getFailList().sort(Comparator.comparing(ErrorObjectVo::getIndex)); + // 生成失败excel + String failBase64 = generateFailExcelBase64(uploadInfo.getFailList()); + uploadInfo.setErrorExcelBase64(failBase64); + } + if (!list.isEmpty()) { + uploadInfo.setSuccessNum(list.size()); + // 导入成功的数据 + teachingSkillService.saveImportData(list); + } + } catch (Exception e) { + if (e instanceof HandleException) { + throw new HandleException(e.getMessage()); + } else { + throw new HandleException("导入转换工作簿异常"); + } + } + return uploadInfo; + } + + @Override + public String templateDownload() throws Exception { + + // 生成工作簿 + TemplateWorkSheet workSheet = new TemplateWorkSheet(); + workSheet.setSheetName(ConstantUtil.NAME_SKILL_IMPORT); + // 生成表头 + workSheet.setRowNames(ConstantUtil.TITLE_SKILL_IMPORT); + workSheet.setTitleNum(1); + // 生成数据 + List dataList = new ArrayList<>(); + // 查询分类列表 + LambdaQueryWrapper labelList = new LambdaQueryWrapper() + .eq(FtbCultivateLabel::getEnabledMark, ConstantUtil.NUM_FALSE) + .eq(FtbCultivateLabel::getType, 3) + .orderByDesc(FtbCultivateLabel::getCreatorTime); + List list = cultivateLabelService.list(labelList); + if (!list.isEmpty()) { + String[] names = list.stream().map(FtbCultivateLabel::getName).toArray(String[]::new); + int[] coordinate = new int[]{1, 100, 1, 1}; + workSheet.setDropDownListMap(Map.of(names, coordinate)); + } + workSheet.setDataList(dataList); + return excelBase64(List.of(workSheet)); + } + + @Override + public List getDownSkillList(SkillDto skillDto) { + + String skillId = null; + if (!StringUtil.isBlank(skillDto.getBizId())) { + // 查询bizId关联的技能点 + TeachingRecord record = teachingRecordService.getOne(new LambdaQueryWrapper().eq(TeachingRecord::getId, skillDto.getBizId())); + if (null != record) { + TeachingSkill skill = teachingSkillService.getOne(new LambdaQueryWrapper().eq(TeachingSkill::getId, record.getSkillId())); + if (StringUtil.isBlank(skillDto.getCategoryId())) { + skillId = record.getSkillId(); + } else { + if (("-1".equals(skillDto.getCategoryId()) && StringUtil.isBlank(skill.getCategoryId())) + || (StringUtil.isNotBlank(skill.getCategoryId()) && skill.getCategoryId().equals(skillDto.getCategoryId()))) { + skillId = record.getSkillId(); + } + } + } + } + return teachingSkillService.getDownSkillList(skillId, skillDto.getCategoryId()); + } + + @Override + public List getDownCategoryList(Integer searchSource) { + + List list = cultivateLabelService.getList(ConstantUtil.LABEL_SKILL, searchSource); + if (null == list || list.isEmpty()) { + return List.of(new FtbCultivateLabelVo(ConstantUtil.DEFAULT_ID)); + } + List categoryIds = list.stream().map(FtbCultivateLabelVo::getId).collect(Collectors.toList()); + List collect = teachingSkillService.getCategoryHasChild(categoryIds); + list.removeIf(v -> !collect.contains(v.getId())); + list.add(new FtbCultivateLabelVo(ConstantUtil.DEFAULT_ID)); + return list; + } + + private String excelBase64(List workSheetList) throws Exception { + + Workbook workbook = TemplateExcelUtils.createWorkBook(workSheetList, false); + if (workbook == null) { + return null; + } + try (workbook; ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + workbook.write(bos); + return Base64.getEncoder().encodeToString(bos.toByteArray()); + } + } + + private String generateFailExcelBase64(List failList) throws Exception { + + if (null == failList || failList.isEmpty()) { + return null; + } + // 生成工作簿 + TemplateWorkSheet workSheet = new TemplateWorkSheet(); + workSheet.setSheetName(ConstantUtil.NAME_SKILL_ERROR); + // 生成表头 + workSheet.setRowNames(ConstantUtil.TITLE_SKILL_ERROR); + workSheet.setTitleNum(1); + // 生成数据 + List dataList = new ArrayList<>(); + // 数据源 + failList.forEach(fail -> { + Object[] objArray = new Object[]{fail.getSkillName(), fail.getCategoryName(), fail.getDescription(), fail.getErrorReason()}; + dataList.add(objArray); + }); + workSheet.setDataList(dataList); + return excelBase64(List.of(workSheet)); + } + + private void addFailDataToList(List list, SkillUploadInfoVo uploadInfo, List exceptIndex, String reason) { + List removeList = list.stream().filter(v -> exceptIndex.contains(v.getIndex())) + .sorted(Comparator.comparing(ImportObjectVo::getIndex)) + .collect(Collectors.toList()); + removeList.forEach(v -> { + ErrorObjectVo errorObj = JsonUtil.getJsonToBean(v, ErrorObjectVo.class); + errorObj.setErrorReason("第" + (v.getIndex() + 1) + "行 " + reason); + uploadInfo.getFailList().add(errorObj); + }); + list.removeAll(removeList); + uploadInfo.setFailNum(uploadInfo.getFailNum() + exceptIndex.size()); + } + + private String getKey() { + + return UserProvider.getLoginUserId() + "-" + FtbUtil.getId(); + } + + private Long getDbCount() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(TeachingSkill::getDeleteMark, ConstantUtil.NUM_FALSE); + return teachingSkillService.count(queryWrapper); + } + + /** + * 检查内容是否符合规范 + * @param row 行 + * @param col 列(0: 标题, 1: 内容) + * @param str 内容 + * @param wordLimit 允许字数 + * @return java.lang.String + */ + private String checkStr(int row, int col, String str, int wordLimit, String errorMsg) { + + String msg = ""; + int realLength; + // 判断内容是否为空 + if (StringUtil.isEmpty(str)) { + msg += col == 0 ? "技能点名称为空" : ""; + realLength = 0; + } else { + realLength = str.codePointCount(0, str.length()); + } + // 判断字数是否超过限制 + if (realLength > wordLimit) { + if (col == 0) { + msg += "技能点名称超过字数(" + wordLimit + ")上限"; + } + if (col == 2) { + msg += "技能点描述超过字数(" + wordLimit + ")上限"; + } + } + // 判断是否包含emoji表情 + if (containsEmoji(str)) { + msg += col == 0 ? "技能点名称中包含emoji表情" : ""; + } + if (StringUtil.isEmpty(errorMsg) && StringUtil.isNotEmpty(msg)) { + msg = "第" + (row + 1) + "行 " + msg; + } + return msg; + } + + public boolean containsEmoji(String text) { + if (text == null) { + return false; + } + return EMOJI_PATTERN.matcher(text).find(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateBatchQueryUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateBatchQueryUtil.java new file mode 100644 index 0000000..b94eaa2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateBatchQueryUtil.java @@ -0,0 +1,82 @@ +package jnpf.cultivate.v2.util; + +import jnpf.cultivate.v2.service.V2CultivateBatchQueryService; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.model.cultivate.po.certificate.FtbCertificateUserEntity; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.statistics.V2BatchQueryResult; +import jnpf.model.cultivate.v2.statistics.V2UserLearningStatusResult; + +import java.util.List; +import java.util.Map; + +/** + * 培养模块批量查询工具类 + * 提供通用的批量查询方法,适用于任务和岗位学习等多个场景 + */ +public class CultivateBatchQueryUtil { + + + /** + * 使用V2批量查询服务进行批量查询 + * 适用于需要使用统一服务接口的场景 + * + * @param courseIds 课程ID列表 + * @param examIds 考试ID列表 + * @param identityIds 鉴定ID列表 + * @param certificateIds 证书ID列表 + * @param practiceBusinessIds 练习业务ID列表 + * @param isSelectNoDelete 0-查询有效的 1-查询删除的 null 查询全部 + * @param v2BatchQueryService V2批量查询服务 + * @return 批量查询结果 + */ + public static V2BatchQueryResult batchQueryBasicInfoWithV2Service( + List courseIds, + List examIds, + List identityIds, + List certificateIds, + List practiceBusinessIds, + Integer isSelectNoDelete, + V2CultivateBatchQueryService v2BatchQueryService) { + + // 使用V2批量查询服务 + return v2BatchQueryService.batchQueryAllByIds( + courseIds, examIds, identityIds, certificateIds, practiceBusinessIds, isSelectNoDelete); + } + + + /** + * 使用V2批量查询服务查询用户学习情况(按具体ID列表) + * + * @param userId 用户ID列表 + * @param courseIds 课程ID列表 + * @param examIds 考试ID列表 + * @param identifyIds 鉴定ID列表 + * @param certificateIds 证书ID列表 + * @param skillIds 技能ID列表 + * @param v2BatchQueryService V2批量查询服务 + * @return 用户学习情况批量查询结果 + */ + public static V2UserLearningStatusResult batchQueryUserLearningStatusWithSpecificIds( + String userId, + List courseIds, + List examIds, + List identifyIds, + List certificateIds, + List skillIds, + V2CultivateBatchQueryService v2BatchQueryService, + Integer source, + String sourceId + ) { + + // 使用V2批量查询服务查询用户学习情况 + Map userCourseMap = v2BatchQueryService.batchQueryUserCourseLearnStatus(userId, courseIds); + Map userExamMap = v2BatchQueryService.batchQueryUserExamStatus(userId, examIds); + Map userIdentificationMap = v2BatchQueryService.batchQueryUserIdentificationStatus(userId, identifyIds,source, sourceId); + Map userCertificateMap = v2BatchQueryService.batchQueryUserCertificateStatus(userId, certificateIds); + Map> userSkillMap = v2BatchQueryService.batchQueryUserSkillStatus(userId, skillIds); + + return new V2UserLearningStatusResult(userCourseMap, userExamMap, userIdentificationMap, userCertificateMap, userSkillMap); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateCourseStudyUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateCourseStudyUtil.java new file mode 100644 index 0000000..e06af39 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateCourseStudyUtil.java @@ -0,0 +1,261 @@ +package jnpf.cultivate.v2.util; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.cultivate.mapper.FtbCultivatePositionCourceLearningMapper; +import jnpf.cultivate.service.FtbCultivateLearnTaskAssignmentService; +import jnpf.cultivate.service.FtbCultivateLearnTaskCourseService; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.cultivate.v2.service.V2CultivatePostStudyService; +import jnpf.cultivate.v2.service.V2CultivateTaskStudyService; +import jnpf.enums.cultivate.v2.mq.CultivateTypeEnum; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskAssignment; +import jnpf.model.cultivate.po.learn.FtbCultivateLearnTaskCourse; +import jnpf.model.cultivate.po.position.FtbCultivatePositionCourceLearning; +import jnpf.model.cultivate.v2.course.vo.app.V2CourseAppDto; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import jnpf.util.UserProvider; +import jnpf.util.context.ThreadContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.stream.Collectors; + +/** + * 培养模块批量查询工具类 + * 提供通用的批量查询方法,适用于任务和岗位学习等多个场景 + */ +@Component +public class CultivateCourseStudyUtil { + + @Autowired + @Lazy + private FtbCultivatePositionCourceLearningMapper ftbCultivatePositionCourceLearningMapper; + + @Autowired + @Lazy + private FtbCultivateLearnTaskAssignmentService ftbCultivateLearnTaskAssignmentService; + + @Autowired + @Lazy + private FtbCultivateLearnTaskCourseService ftbCultivateLearnTaskCourseService; + + @Autowired + @Lazy + private V2CultivatePostStudyService v2CultivatePostStudyService; + + @Autowired + @Lazy + private V2CultivateTaskStudyService v2CultivateTaskStudyService; + + @Resource + private ThreadPoolExecutor cultivateThreadPool; + + @Autowired + private UserApiV2Util userApiV2Util; + + + /** + * 初始化课程学习记录 + * + * @param userId 用户id + * @param courseId 课程id + */ + public void initCourseStudyRecord(String userId, String courseId, String tenantId) { + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + userApiV2Util.checkOutTenant(tenantId); + LambdaQueryWrapper courseLearningWrapper = Wrappers.lambdaQuery(); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getCourceId, courseId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List courseLearningList = ftbCultivatePositionCourceLearningMapper.selectList(courseLearningWrapper); + if (CollUtil.isNotEmpty(courseLearningList)) { + return; + } + // 创建新的课程学习记录 + FtbCultivatePositionCourceLearning courseLearning = new FtbCultivatePositionCourceLearning(); + courseLearning.setCourceId(courseId); + courseLearning.setUserId(userId); + courseLearning.setLearnTime(0); + courseLearning.setState(0); + courseLearning.setEnabledMark(0); + ftbCultivatePositionCourceLearningMapper.insert(courseLearning); + }), cultivateThreadPool); + + } + + /** + * 批量初始化课程学习记录 + * + * @param userIds 用户id + * @param courseIds 课程id + */ + public void batchInitCourseStudyRecord(List userIds, List courseIds, String tenantId) { + + + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + userApiV2Util.checkOutTenant(tenantId); + for (String userId : userIds) { + LambdaQueryWrapper courseLearningWrapper = Wrappers.lambdaQuery(); + courseLearningWrapper.select(FtbCultivatePositionCourceLearning::getCourceId); + courseLearningWrapper.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List courseLearningList = ftbCultivatePositionCourceLearningMapper.selectList(courseLearningWrapper); + List existCourseIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(courseLearningList)) { + existCourseIds = courseLearningList.stream().map(FtbCultivatePositionCourceLearning::getCourceId).collect(Collectors.toList()); + } + for (String courseId : courseIds) { + if (existCourseIds.contains(courseId)) { + continue; + } + // 创建新的课程学习记录 + FtbCultivatePositionCourceLearning courseLearning = new FtbCultivatePositionCourceLearning(); + courseLearning.setCourceId(courseId); + courseLearning.setUserId(userId); + courseLearning.setLearnTime(0); + courseLearning.setState(0); + courseLearning.setEnabledMark(0); + ftbCultivatePositionCourceLearningMapper.insert(courseLearning); + } + } + + }), cultivateThreadPool); + } + + /** + * 真实初始化课程学习记录 + * + * @param userIds 用户id + * @param courseIds 课程id + */ + public void realInitCourseStudyRecord(List userIds, List courseIds) { + for (String userId : userIds) { + LambdaQueryWrapper courseLearningWrapper = Wrappers.lambdaQuery(); + courseLearningWrapper.in(FtbCultivatePositionCourceLearning::getCourceId, courseIds); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getUserId, userId); + courseLearningWrapper.eq(FtbCultivatePositionCourceLearning::getEnabledMark, 0); + List courseLearningList = ftbCultivatePositionCourceLearningMapper.selectList(courseLearningWrapper); + List existCourseIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(courseLearningList)) { + existCourseIds = courseLearningList.stream().map(FtbCultivatePositionCourceLearning::getCourceId).collect(Collectors.toList()); + } + for (String courseId : courseIds) { + if (existCourseIds.contains(courseId)) { + continue; + } + // 创建新的课程学习记录 + FtbCultivatePositionCourceLearning courseLearning = new FtbCultivatePositionCourceLearning(); + courseLearning.setCourceId(courseId); + courseLearning.setUserId(userId); + courseLearning.setLearnTime(0); + courseLearning.setState(0); + courseLearning.setEnabledMark(0); + ftbCultivatePositionCourceLearningMapper.insert(courseLearning); + } + } + } + + /** + * 添加任务课程学习记录 + * + * @param taskIds 任务id + */ + public void addTaskCourseStudyRecord(List taskIds, String tenantId) { + + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + userApiV2Util.checkOutTenant(tenantId); + for (String taskId : taskIds) { + + LambdaQueryWrapper assignmentWrapper = Wrappers.lambdaQuery(); + assignmentWrapper.select( + FtbCultivateLearnTaskAssignment::getId, + FtbCultivateLearnTaskAssignment::getTaskId, + FtbCultivateLearnTaskAssignment::getUserId + ); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getTaskId, taskId); + assignmentWrapper.eq(FtbCultivateLearnTaskAssignment::getEnableMark, 0); + List taskAssignment = ftbCultivateLearnTaskAssignmentService.list(assignmentWrapper); + if (CollUtil.isEmpty(taskAssignment)) { + continue; + } + + List userIds = taskAssignment.stream().map(FtbCultivateLearnTaskAssignment::getUserId).collect(Collectors.toList()); + LambdaQueryWrapper courseWrapper = Wrappers.lambdaQuery(); + courseWrapper.select( + FtbCultivateLearnTaskCourse::getId, + FtbCultivateLearnTaskCourse::getTaskId, + FtbCultivateLearnTaskCourse::getCourseId + ); + courseWrapper.eq(FtbCultivateLearnTaskCourse::getTaskId, taskId); + courseWrapper.eq(FtbCultivateLearnTaskCourse::getEnableMark, 0); + List courseList = ftbCultivateLearnTaskCourseService.list(courseWrapper); + if (CollUtil.isEmpty(courseList)) { + continue; + } + List courseIds = courseList.stream().map(FtbCultivateLearnTaskCourse::getCourseId).collect(Collectors.toList()); + realInitCourseStudyRecord(userIds, courseIds); + } + }), cultivateThreadPool); + } + + /** + * 检查岗位学习 + * + * @param userId 用户id + * @param tenantId 租户id + */ + public void checkCultivateLearning(String userId, String tenantId) { + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + userApiV2Util.checkOutTenant(tenantId); + v2CultivatePostStudyService.dealPositionReCheckStudy(CultivateMqDTO.builder().userId(userId).tenantId(tenantId).build()); + }), cultivateThreadPool); + } + + + /** + * 检查课程完成 + * + * @param dto 课程信息 + * @param state 学习状态 1已学习,0未学习,2学习中 + */ + public void checkCourseComplete(V2CourseAppDto dto, Integer state) { + String tenantId = UserProvider.getUser().getTenantId(); + String userId = dto.getUserId(); + String courseId = dto.getCourseId(); + if (state == null || state == 0 || state == 2) { + return; + } + + CompletableFuture.runAsync(ThreadContext.wrap(() -> { + userApiV2Util.checkOutTenant(tenantId); + //备注一下就是 岗位学习 、 学习任务 、通用课程 都不能选择限时的 考试是吧 + CultivateMqDTO message = CultivateMqDTO.builder().userId(userId) + .tenantId(tenantId).businessId(courseId) + .businessSource(dto.getBusinessSource()) + .businessSourceId(dto.getBusinessSourceId()) + .type(CultivateTypeEnum.COURSE).build(); + switch (dto.getBusinessSource()) { + case POST_LEARNING: + v2CultivatePostStudyService.dealPositionCourseStudy(message); + break; + case TASK_LEARNING: + v2CultivateTaskStudyService.dealTaskCourseStudy(message); + break; + case GENERAL_COURSE: + case OFFLINE_CLASS: + v2CultivatePostStudyService.dealCommonCourseStudy(message); + break; + default: + } + }), cultivateThreadPool); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateIdentityUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateIdentityUtil.java new file mode 100644 index 0000000..72437b8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateIdentityUtil.java @@ -0,0 +1,578 @@ +package jnpf.cultivate.v2.util; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import jnpf.entity.cultivate.CultivateIdentifyApplyDetailsBackups; +import jnpf.entity.cultivate.CultivateIdentifyApplyTableBackups; +import jnpf.entity.cultivate.CultivateIdentifyItems; +import jnpf.enums.AppraisalScoreTypeEnum; +import jnpf.enums.cultivate.ApplyResultEnum; +import jnpf.enums.cultivate.ApplyTypeEnum; +import jnpf.enums.cultivate.IdentifyScoreTypeEnum; +import jnpf.model.cultivate.po.FtbCultivateIdentifyCategories; +import jnpf.model.cultivate.po.FtbCultivateIdentifyItemsPool; +import jnpf.model.cultivate.v2.apply.req.V2IdentifyApplyItemReq; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyItemsImportSaveReq; +import jnpf.model.cultivate.v2.identify.req.V2IdentifyTableImportSaveReq; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyScoreConfigVo; +import jnpf.model.cultivate.v2.identify.vo.V2IdentifyTableScoreConfigVo; +import jnpf.model.cultivate.v2.identify.vo.V2PreIdentifyErrorVo; +import jnpf.model.cultivate.v2.item_pool.vo.V2IdentifyItemVo; +import jnpf.model.cultivate.v2.label.vo.FtbCultivateLabelVo; +import jnpf.util.StringUtil; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CultivateIdentityUtil { + + public static MutablePair calUserIdentifyResult(CultivateIdentifyApplyTableBackups table, BigDecimal totalScore, BigDecimal userTotalScore) { + + if (table.getScoreType().equals(IdentifyScoreTypeEnum.GRADE_SYSTEM.getCode())) { + //计算合格分数 + BigDecimal qualifiedScore; + if (table.getPassType().equals(ApplyTypeEnum.GDF.getCode())) { + qualifiedScore = table.getPassScore(); + } else { + qualifiedScore = totalScore.multiply(table.getPassScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + //计算优秀分数 + BigDecimal excellentScore; + if (table.getExcellentType().equals(ApplyTypeEnum.GDF.getCode())) { + excellentScore = table.getExcellentScore(); + } else { + excellentScore = totalScore.multiply(table.getExcellentScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + if (userTotalScore.compareTo(qualifiedScore) < 0) { + return MutablePair.of(ApplyResultEnum.BHG.getCode(), ""); + } else if (userTotalScore.compareTo(qualifiedScore) >= 0 && userTotalScore.compareTo(excellentScore) < 0) { + return MutablePair.of(ApplyResultEnum.HG.getCode(), ""); + } else if (userTotalScore.compareTo(excellentScore) >= 0) { + return MutablePair.of(ApplyResultEnum.YX.getCode(), ""); + } + } else { + String scoreConfig = table.getScoreConfig(); + if (StringUtil.isEmpty(scoreConfig)) { + return MutablePair.of(ApplyResultEnum.BHG.getCode(), ""); + } + List configVoList = JSON.parseArray(table.getScoreConfig(), V2IdentifyTableScoreConfigVo.class); + if (CollUtil.isEmpty(configVoList)) { + return MutablePair.of(ApplyResultEnum.BHG.getCode(), ""); + } + int maxIndex = configVoList.size() - 1; + for (int i = 0; i < configVoList.size(); i++) { + V2IdentifyTableScoreConfigVo configVo = configVoList.get(i); + if (i == 0) { + if (userTotalScore.compareTo(new BigDecimal(configVo.getMin())) >= 0) { + return MutablePair.of(configVo.getResult(), configVo.getLevelName()); + } + } else if (maxIndex == i) { + if (userTotalScore.compareTo(new BigDecimal(configVo.getMax())) <= 0) { + return MutablePair.of(configVo.getResult(), configVo.getLevelName()); + } + } else { + if (userTotalScore.compareTo(new BigDecimal(configVo.getMin())) >= 0 && userTotalScore.compareTo(new BigDecimal(configVo.getMax())) <= 0) { + return MutablePair.of(configVo.getResult(), configVo.getLevelName()); + } + } + } + return MutablePair.of(ApplyResultEnum.BHG.getCode(), ""); + } + return MutablePair.of(ApplyResultEnum.BHG.getCode(), ""); + } + + public static BigDecimal calUserTotalScore(List identifyItemList) { + BigDecimal totalScore = new BigDecimal(0); + for (V2IdentifyApplyItemReq applyItem : identifyItemList) { + totalScore = totalScore.add(applyItem.getIdentifyScore()); + } + return totalScore; + } + + public static BigDecimal calIdentityTotalScore(List identifyItemsList) { + //计算鉴定总分 + BigDecimal totalScore = new BigDecimal(0); + for (CultivateIdentifyApplyDetailsBackups applyDetails : identifyItemsList) { + if (applyDetails.getType().equals(0)) { + totalScore = totalScore.add(applyDetails.getScore()); + } else { + totalScore = totalScore.add(new BigDecimal(applyDetails.getMaxScore())); + } + } + return totalScore; + } + + /** + * 构建鉴定项重复判断的唯一标识 + * + * @param item 鉴定项信息 + * @return 重复判断的唯一标识字符串 + */ + public static String buildRepeatKey(FtbCultivateIdentifyItemsPool item) { + StringBuilder sb = new StringBuilder(); + sb.append(item.getName()) + .append(item.getType()); + + if (item.getType().equals(AppraisalScoreTypeEnum.FLOATING.getCode())) { + sb.append(item.getMinScore()) + .append(item.getMaxScore()); + } else if (item.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + sb.append(item.getTotalScore()); + } + + return sb.toString(); + } + + public static String buildRepeatKey(V2IdentifyItemsImportSaveReq item) { + StringBuilder sb = new StringBuilder(); + sb.append(item.getName()) + .append(item.getType()); + + if (item.getType().equals(AppraisalScoreTypeEnum.FLOATING.getCode())) { + sb.append(item.getMinScore()) + .append(item.getMaxScore()); + } else if (item.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + sb.append(item.getScore()); + } + + return sb.toString(); + } + + /** + * 构建鉴定项重复判断的唯一标识 + * + * @param item 鉴定项信息 + * @return 重复判断的唯一标识字符串 + */ + public static String buildRepeatKey(V2IdentifyItemVo item) { + StringBuilder sb = new StringBuilder(); + sb.append(item.getItemName()) + .append(item.getType()); + + if (item.getType().equals(AppraisalScoreTypeEnum.FLOATING.getCode())) { + sb.append(item.getMinScore()) + .append(item.getMaxScore()); + } else if (item.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + sb.append(item.getTotalScore()); + } + + return sb.toString(); + } + + public static void parseAndValidateScore(V2IdentifyItemVo itemVo) { + String scoreStr = itemVo.getScore().trim(); + + // 检查是否包含 ~ 或 - 分隔符 + if (scoreStr.contains("~") || scoreStr.contains("-")) { + // 使用相应的分隔符分割 + String[] parts = scoreStr.contains("~") ? + scoreStr.split("~") : scoreStr.split("-"); + + if (parts.length != 2) { + itemVo.setMsg("分值格式错误,请使用'最小值~最大值'或'最小值-最大值'格式"); + itemVo.setSuccess(1); + return; + } + + try { + String minStr = parts[0].trim(); + String maxStr = parts[1].trim(); + + // 检查是否为数字 + if (!isNumeric(minStr) || !isNumeric(maxStr)) { + itemVo.setMsg("分值必须为数字"); + itemVo.setSuccess(1); + return; + } + + int minScore = Integer.parseInt(minStr); + int maxScore = Integer.parseInt(maxStr); + + // 验证数值范围 + if (minScore < 0 || minScore > 100 || maxScore < 0 || maxScore > 100) { + itemVo.setMsg("分值应在0-100范围内"); + itemVo.setSuccess(1); + return; + } + + if (minScore >= maxScore) { + itemVo.setMsg("浮动分前值应小于后值"); + itemVo.setSuccess(1); + return; + } + + // 设置浮动分值 + itemVo.setType(AppraisalScoreTypeEnum.FLOATING.getCode()); + itemVo.setMinScore(minScore); + itemVo.setMaxScore(maxScore); + itemVo.setSuccess(0); + itemVo.setMsg(null); + + } catch (NumberFormatException e) { + itemVo.setMsg("分值格式不正确"); + itemVo.setSuccess(1); + } + } else { + // 单个分值 + try { + if (!isNumeric(scoreStr)) { + itemVo.setMsg("分值必须为数字"); + itemVo.setSuccess(1); + return; + } + + int totalScore = Integer.parseInt(scoreStr); + + if (totalScore < 0 || totalScore > 100) { + itemVo.setMsg("分值应在0-100范围内"); + itemVo.setSuccess(1); + return; + } + + // 设置固定分值 + itemVo.setType(AppraisalScoreTypeEnum.FIXED.getCode()); + itemVo.setTotalScore(totalScore); + itemVo.setSuccess(0); + itemVo.setMsg(null); + + } catch (NumberFormatException e) { + itemVo.setMsg("分值格式不正确"); + itemVo.setSuccess(1); + } + } + } + + /** + * 判断字符串是否为数字(包括整数和小数) + * + * @param str 待判断的字符串 + * @return 是否为数字 true-是、false-否 + */ + public static boolean isNumeric(String str) { + if (str == null || str.trim().isEmpty()) { + return false; + } + return str.matches("-?\\d+(\\.\\d+)?"); + } + + + /** + * 逐行读取不规则表格数据 + * + * @param inputStream + * @return 包含所有行数据的列表,每行是一个字符串列表 + */ + public static List> readIrregularExcelRowByRow(InputStream inputStream) throws IOException { + List> allRows = new ArrayList<>(); + + try (Workbook workbook = new XSSFWorkbook(inputStream)) { + Sheet sheet = workbook.getSheetAt(0); // 获取第一个工作表 + for (Row row : sheet) { + List rowData = new ArrayList<>(); + // 处理每一行的单元格 + for (Cell cell : row) { + String cellValue = getCellValueAsString(cell); + rowData.add(cellValue.trim()); + } + // 只添加非空行(可根据业务需求调整) + if (!isRowEmpty(rowData)) { + allRows.add(rowData); + } + } + } + + return allRows; + } + + /** + * 将单元格值转换为字符串 + */ + public static String getCellValueAsString(Cell cell) { + if (cell == null) { + return ""; + } + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + // 处理数字,避免科学计数法 + return String.valueOf((long) cell.getNumericCellValue()); + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + return cell.getCellFormula(); + case BLANK: + return ""; + default: + return ""; + } + } + + /** + * 检查行是否为空(所有单元格都为空) + */ + public static boolean isRowEmpty(List rowData) { + for (String cellValue : rowData) { + if (cellValue != null && !cellValue.trim().isEmpty()) { + return false; + } + } + return true; + } + + + public static V2IdentifyTableImportSaveReq convertExcelData(List> lists, + List itemCateList, + List labelVoList, + long oldTableNum, + List poolList + ) { + + Map itemCateMap = itemCateList.stream().collect(Collectors.toMap(FtbCultivateIdentifyCategories::getName, item -> item)); + Map labelMap = labelVoList.stream().collect(Collectors.toMap(FtbCultivateLabelVo::getName, item -> item)); + checkParams(lists); + V2IdentifyTableImportSaveReq req = new V2IdentifyTableImportSaveReq(); + List errorVoList = req.getPreIdentifyImportVo().getItemList(); + String tableName = lists.get(0).get(1); + if (StringUtil.isEmpty(tableName)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第1行").msg("鉴定表名称不能为空").build()); + } + //增加鉴定表名称长度不超哥20个字 + if (tableName.length() > 20) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第1行").msg("鉴定表名称不能超过20个字").build()); + } + if (oldTableNum > 0) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第1行").msg("鉴定表名称已经在系统中存在").build()); + } + + req.setName(tableName); + String tableLabelId = lists.get(1).get(1); + if (StringUtil.isEmpty(tableLabelId)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第2行").msg("鉴定表标签不能为空").build()); + } else { + FtbCultivateLabelVo ftbCultivateLabelVo = labelMap.get(tableLabelId); + if (ftbCultivateLabelVo == null) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第2行").msg("鉴定表标签不存在").build()); + } else { + req.setLabelId(ftbCultivateLabelVo.getId()); + } + } + req.setScoreType(IdentifyScoreTypeEnum.GRADE_SYSTEM.getCode()); + String passScoreStr = lists.get(3).get(1); + if (StringUtil.isEmpty(passScoreStr)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第4行").msg("鉴定合格分数不能为空").build()); + } else { + if (!CultivateIdentityUtil.isNumeric(passScoreStr)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第4行").msg("鉴定合格分数必须是数字").build()); + } else { + req.setPassScore(new BigDecimal(passScoreStr)); + req.setPassType(1); + } + } + String excellentScoreStr = lists.get(4).get(1); + if (StringUtil.isEmpty(excellentScoreStr)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第5行").msg("鉴定优秀分数不能为空").build()); + } else { + if (!CultivateIdentityUtil.isNumeric(excellentScoreStr)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第5行").msg("鉴定优秀分数必须是数字").build()); + } else { + req.setExcellentScore(new BigDecimal(excellentScoreStr)); + req.setExcellentType(1); + } + } + Map dbContentCateIdMap = new HashMap<>(); + for (FtbCultivateIdentifyItemsPool itemsPool : poolList) { + String s = CultivateIdentityUtil.buildRepeatKey(itemsPool); + dbContentCateIdMap.put(s, itemsPool.getCateId()); + } + + List identifyItemList = new ArrayList<>(); + Integer totalScore = 0; + List itemNameList = new ArrayList<>(); + for (int i = 6; i < lists.size(); i++) { + V2IdentifyItemsImportSaveReq item = new V2IdentifyItemsImportSaveReq(); + List rowData = lists.get(i); + if (isRowEmpty(rowData)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项数据不能为空").build()); + continue; + } + String numStr = rowData.get(0); + if (StringUtil.isEmpty(numStr)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项编号不能为空").build()); + continue; + } + try { + item.setSortCode(Integer.parseInt(numStr)); + } catch (Exception e) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项编号必须是数字").build()); + continue; + } + String name = rowData.get(1); + if (StringUtil.isEmpty(name)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项内容不能为空").build()); + continue; + } + // 鉴定项内容长度不能超过500个字 + if (name.length() > 500) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项内容长度不能超过500个字").build()); + continue; + } + item.setName(name); + + String itemCateId = rowData.get(2); + if (StringUtil.isEmpty(itemCateId)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项分类不能为空").build()); + continue; + } + FtbCultivateIdentifyCategories itemCate = itemCateMap.get(itemCateId); + if (itemCate == null) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项分类不存在").build()); + continue; + } + item.setCateId(itemCate.getId()); + + V2IdentifyItemVo v2IdentifyItemVo = new V2IdentifyItemVo(); + v2IdentifyItemVo.setScore(rowData.get(3)); + + parseAndValidateScore(v2IdentifyItemVo); + if (v2IdentifyItemVo.getSuccess().equals(1)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg(v2IdentifyItemVo.getMsg()).build()); + continue; + } + item.setType(v2IdentifyItemVo.getType()); + if (item.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + item.setScore(new BigDecimal(v2IdentifyItemVo.getTotalScore())); + totalScore += item.getScore().intValue(); + } else { + item.setMinScore(v2IdentifyItemVo.getMinScore()); + item.setMaxScore(v2IdentifyItemVo.getMaxScore()); + totalScore += item.getMaxScore(); + } + + String repeat = CultivateIdentityUtil.buildRepeatKey(item); + if (itemNameList.contains(repeat)) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项内容和分数重复").build()); + } + String s = dbContentCateIdMap.get(repeat); + if (s != null && !s.equals(item.getCateId())) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第" + (i + 1) + "行").msg("鉴定项中的分类和数据库中存在的分类不一致").build()); + } + itemNameList.add(repeat); + identifyItemList.add(item); + } + if (req.getPassScore() != null && req.getExcellentScore() != null) { + if (req.getPassScore().intValue() >= req.getExcellentScore().intValue()) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第5行").msg("鉴定合格分数不能大于等于鉴定优秀分数").build()); + } + } + if (req.getPassScore() != null) { + if (req.getPassScore().intValue() >= totalScore) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第4行").msg("鉴定合格分数不能大于等于鉴定总分").build()); + } + } + if (req.getExcellentScore() != null) { + if (req.getExcellentScore().intValue() > totalScore) { + errorVoList.add(V2PreIdentifyErrorVo.builder().alt("第5行").msg("鉴定优秀分数不能大于鉴定总分").build()); + } + } + + req.setIdentifyItemList(identifyItemList); + return req; + } + + private static void checkParams(List> lists) { + if (lists == null || lists.isEmpty()) { + throw new RuntimeException("请上传正确的模版数据"); + } + if (lists.size() < 6) { + throw new RuntimeException("请上传正确模版数据"); + } + if (!lists.get(0).get(0).equals("鉴定表名称")) { + throw new RuntimeException("上传的模版不正确"); + } + if (!lists.get(1).get(0).equals("鉴定表标签")) { + throw new RuntimeException("上传的模版不正确"); + } + if (!lists.get(2).get(0).equals("鉴定总分")) { + throw new RuntimeException("上传的模版不正确"); + } + if (!lists.get(3).get(0).equals("鉴定合格分数")) { + throw new RuntimeException("上传的模版不正确"); + } + if (!lists.get(4).get(0).equals("鉴定优秀分数")) { + throw new RuntimeException("上传的模版不正确"); + } + if (!lists.get(5).get(0).equals("鉴定项序号") || + !lists.get(5).get(1).equals("鉴定项内容") || + !lists.get(5).get(2).equals("鉴定项分类") || + !lists.get(5).get(3).equals("分值")) { + throw new RuntimeException("上传的模版不正确"); + } + } + + public static V2IdentifyScoreConfigVo calPassAndExcellentResult(BigDecimal totalScore, CultivateIdentifyApplyTableBackups table) { + V2IdentifyScoreConfigVo vo = new V2IdentifyScoreConfigVo(); + vo.setCalTotalScore(totalScore); + vo.setScoreType(table.getScoreType()); + if (table.getScoreType().equals(IdentifyScoreTypeEnum.GRADE_SYSTEM.getCode())) { + //计算合格分数 + BigDecimal qualifiedScore; + if (table.getPassType().equals(ApplyTypeEnum.GDF.getCode())) { + qualifiedScore = table.getPassScore(); + } else { + qualifiedScore = totalScore.multiply(table.getPassScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + //计算优秀分数 + BigDecimal excellentScore; + if (table.getExcellentType().equals(ApplyTypeEnum.GDF.getCode())) { + excellentScore = table.getExcellentScore(); + } else { + excellentScore = totalScore.multiply(table.getExcellentScore().divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + } + + vo.setCalPassScore(qualifiedScore); + vo.setCalExcellentScore(excellentScore); + + } else { + List configVoList = JSON.parseArray(table.getScoreConfig(), V2IdentifyTableScoreConfigVo.class); + vo.setConfigVoList(configVoList); + } + return vo; + } + + /** + * 鉴定项内容重复 + * + * @param item 鉴定表中的鉴定项 + * @return 重复的key + */ + public static String buildRepeatKey(CultivateIdentifyItems item) { + StringBuilder sb = new StringBuilder(); + sb.append(item.getName()) + .append(item.getType()); + + if (item.getType().equals(AppraisalScoreTypeEnum.FLOATING.getCode())) { + sb.append(item.getMinScore()) + .append(item.getMaxScore()); + } else if (item.getType().equals(AppraisalScoreTypeEnum.FIXED.getCode())) { + sb.append(item.getScore()); + } + + return sb.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateMqSendUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateMqSendUtil.java new file mode 100644 index 0000000..de56ec3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/CultivateMqSendUtil.java @@ -0,0 +1,175 @@ +package jnpf.cultivate.v2.util; + +import jnpf.constants.MessageTopicConstants; +import jnpf.cultivate.utils.CultivateIdentifyIMUtils; +import jnpf.entity.cultivate.CultivateIdentifyApply; +import jnpf.entity.cultivate.TeachingRecord; +import jnpf.enums.cultivate.v2.mq.CultivateTypeEnum; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.v2.course.vo.app.V2ChapterStudyVo; +import jnpf.model.cultivate.v2.enums.PositionBusinessSourceEnum; +import jnpf.model.cultivate.v2.mq.CultivateMqDTO; +import jnpf.util.UserProvider; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 培训模块发送mq工具类 + */ +@Component +public class CultivateMqSendUtil { + + @Autowired + private RocketMQTemplate rocketMqTemplate; + + @Autowired + private CultivateIdentifyIMUtils cultivateIdentifyIMUtils; + + /** + * 发送课程完成消息 + * + * @param req 章节学习参数 + * @param userId 用户 ID + * @param tenantId 租户 ID + */ + public void sendCourseCompletionMessage(V2ChapterStudyVo req, String userId, String tenantId) { + Map params = new HashMap<>(); + params.put(CultivateMqDTO.POST_ID, req.getPostId()); + params.put(CultivateMqDTO.GRADE_ID, req.getGradeId()); + params.put("chapterId", req.getChapterId()); + CultivateMqDTO courseEventDTO = CultivateMqDTO.builder() + .tenantId(tenantId) + .userId(userId) + .businessSource(req.getBusinessSource()) + .businessSourceId(req.getBusinessSourceId()) + .type(CultivateTypeEnum.COURSE) + .businessId(req.getCourseId()) + .status(1) + .params(params) + .build(); + + // 构建带 Tag 和 Key 的消息 + Message message = MessageBuilder + .withPayload(courseEventDTO) + .setHeader("TAGS", tenantId) // 课程完成 Tag + .setHeader("KEYS", tenantId + "_" + userId) // 唯一 Key: userId_courseId + .build(); + + rocketMqTemplate.syncSend(MessageTopicConstants.CULTIVATE_TOPIC, message); + } + + /** + * 发送课程完成消息 + * + * @param apply 章节学习参数 + */ + public void sendIdentityCompletionMessage(CultivateIdentifyApply apply, String identityId) { + // 判断鉴定结果是否告知被鉴定人(0-告知,1-不告知) + if (apply.getAppraisalResults() != null && apply.getAppraisalResults().equals(0)) { + noticeAppMessage(apply); + } + //鉴定来源 (0-手动发起,1-课程学习鉴定,2-岗位学习鉴定,3-本人申请,4-任务鉴定) + if (apply.getSource().equals(0) || apply.getSource().equals(1) || apply.getSource().equals(3)) { + return; + } + + + String tenantId = UserProvider.getUser().getTenantId(); + Map params = new HashMap<>(); + params.put("userIdentifyId", apply.getId()); + if (!apply.getStatus().equals(0)) { + params.put("userIdentifyStatus", apply.getResult()); + } + PositionBusinessSourceEnum businessSource = null; + if (apply.getSource().equals(2)) { + businessSource = PositionBusinessSourceEnum.POST_LEARNING; + } else if (apply.getSource().equals(4)) { + businessSource = PositionBusinessSourceEnum.TASK_LEARNING; + } + CultivateMqDTO courseEventDTO = CultivateMqDTO.builder() + .tenantId(tenantId) + .userId(apply.getBeIdentifyUserId()) + .businessSource(businessSource) + .businessSourceId(apply.getSourceId()) + .type(CultivateTypeEnum.IDENTITY) + .businessId(identityId) + .status(apply.getStatus()) + .params(params) + .build(); + + // 构建带 Tag 和 Key 的消息 + Message message = MessageBuilder + .withPayload(courseEventDTO) + .setHeader("TAGS", tenantId) // 鉴定完成 Tag + .setHeader("KEYS", tenantId + "_" + apply.getBeIdentifyUserId()) // 唯一 Key + .build(); + + rocketMqTemplate.syncSend(MessageTopicConstants.CULTIVATE_TOPIC, message); + } + + private void noticeAppMessage(CultivateIdentifyApply apply) { + cultivateIdentifyIMUtils.sendMsg(apply,UserProvider.getUser().getTenantId()); + } + + public void sendCompleteTeachingMessage(TeachingRecord teachingRecord) { + String tenantId = UserProvider.getUser().getTenantId(); + Map params = new HashMap<>(); + PositionBusinessSourceEnum businessSource = null; + + CultivateMqDTO courseEventDTO = CultivateMqDTO.builder() + .tenantId(tenantId) + .userId(teachingRecord.getUserId()) + .businessSource(businessSource) + .businessSourceId("") + .type(CultivateTypeEnum.PRACTICE) + .businessId(teachingRecord.getSkillId()) + .status(1) + .params(params) + .build(); + + // 构建带 Tag 和 Key 的消息 + Message message = MessageBuilder + .withPayload(courseEventDTO) + .setHeader("TAGS", tenantId) // 实训完成 Tag + .setHeader("KEYS", tenantId + "_" + teachingRecord.getUserId()) // 唯一 Key + .build(); + + rocketMqTemplate.syncSend(MessageTopicConstants.CULTIVATE_TOPIC, message); + } + + public void sendExamMessage(FtbCultivateExamUser examUser) { + String tenantId = UserProvider.getUser().getTenantId(); + Map params = new HashMap<>(); + params.put("userExamId", examUser.getId()); + params.put("userExamStatus", examUser.getStatus()); + PositionBusinessSourceEnum businessSource = null; + + CultivateMqDTO courseEventDTO = CultivateMqDTO.builder() + .tenantId(tenantId) + .userId(examUser.getUserId()) + .businessSource(businessSource) + .businessSourceId("") + .type(CultivateTypeEnum.EXAM) + .businessId(examUser.getExamId()) + .status(examUser.getStatus()) + .params(params) + .build(); + + // 构建带 Tag 和 Key 的消息 + Message message = MessageBuilder + .withPayload(courseEventDTO) + .setHeader("TAGS", tenantId) // 考试完成 Tag + .setHeader("KEYS", tenantId + "_" + examUser.getUserId()) // 唯一 Key + .build(); + + rocketMqTemplate.syncSend(MessageTopicConstants.CULTIVATE_TOPIC, message); + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeDetector.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeDetector.java new file mode 100644 index 0000000..2ecadae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeDetector.java @@ -0,0 +1,227 @@ +package jnpf.cultivate.v2.util; + +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.po.CultivateExamDrawRule; +import jnpf.model.cultivate.v2.exam.req.V2SaveExamReq; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 考试变更判定 + * + * @author yanwenfu + * @create 2026-04-22 + */ +public class ExamChangeDetector { + + public static ExamChangeResult detect( + CultivateExam oldExam, + V2SaveExamReq newExam, + List oldRules, + List newRules) { + + ExamChangeResult result = new ExamChangeResult(); + result.setChangedFields(new ArrayList<>()); + + // ===== 1. exam字段对比 ===== + boolean examChanged = compareExam(oldExam, newExam, result); + + // ===== 2. 抽题规则对比 ===== + boolean ruleChanged = hasRuleChanged(oldRules, newRules); + + if (ruleChanged) { + result.markChanged("drawRule"); + } + + result.setExamChanged(examChanged); + result.setRuleChanged(ruleChanged); + result.setChanged(examChanged || ruleChanged); + + return result; + } + + private static boolean compareExam(CultivateExam oldExam, + V2SaveExamReq newExam, + ExamChangeResult result) { + + if (oldExam == null && newExam == null) return false; + if (oldExam == null || newExam == null) { + result.markChanged("exam"); + return true; + } + + boolean changed = false; + + if (!Objects.equals(oldExam.getExamType(), newExam.getExamType())) { + result.markChanged("examType"); + changed = true; + } + + if (!Objects.equals(oldExam.getStartTime(), newExam.getStartTime())) { + result.markChanged("startTime"); + changed = true; + } + + if (!Objects.equals(oldExam.getEndTime(), newExam.getEndTime())) { + result.markChanged("endTime"); + changed = true; + } + + if (!Objects.equals(oldExam.getExamLimitation(), newExam.getExamLimitation())) { + result.markChanged("examLimitation"); + changed = true; + } + + if (!Objects.equals(oldExam.getExamTime(), newExam.getExamTime())) { + result.markChanged("examTime"); + changed = true; + } + + if (!Objects.equals(oldExam.getDrawMode(), newExam.getDrawMode())) { + result.markChanged("drawMode"); + changed = true; + } + + if (!Objects.equals(oldExam.getDrawDataBase(), String.join(",", newExam.getDrawDataBaseList()))) { + result.markChanged("drawDataBase"); + changed = true; + } + + if (!Objects.equals(oldExam.getReExamFrequencyType(), newExam.getReExamFrequencyType())) { + result.markChanged("reExamFrequencyType"); + changed = true; + } + + if (!Objects.equals(oldExam.getReExamFrequencyNum(), newExam.getReExamFrequencyNum())) { + result.markChanged("reExamFrequencyNum"); + changed = true; + } + + if (!Objects.equals(oldExam.getPassType(), newExam.getPassType())) { + result.markChanged("passType"); + changed = true; + } + + if (!Objects.equals(oldExam.getPassMark(), newExam.getPassMark())) { + result.markChanged("passMark"); + changed = true; + } + + if (!Objects.equals(oldExam.getExcellentType(), newExam.getExcellentType())) { + result.markChanged("excellentType"); + changed = true; + } + + if (!Objects.equals(oldExam.getExcellentMark(), newExam.getExcellentMark())) { + result.markChanged("excellentMark"); + changed = true; + } + + return changed; + } + + private static boolean isSameMember(String a, String b) { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + + return normalize(a).equals(normalize(b)); + } + + private static List normalize(String str) { + return Arrays.stream(str.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .sorted() + .collect(Collectors.toList()); + } + + // --------------------------------------------- 规则变更 ------------------------------------------- + public static boolean hasRuleChanged(List oldList, + List newList) { + + // 都为空 → 无变更 + if (oldList == null && newList == null) return false; + // 一个为空 → 有变更 + if (oldList == null || newList == null) return true; + + if (oldList.size() != newList.size()) return true; + if (oldList.isEmpty()) return false; + + // 判断模式(看有没有 questionId) + boolean isFixed = isFixedMode(oldList); + + if (isFixed != isFixedMode(newList)) { + // 模式变了(随机 → 固定 / 固定 → 随机) + return true; + } + + return isFixed ? hasChangedFixed(oldList, newList) : hasChangedRandom(oldList, newList); + } + + /** + * 判断是否固定抽题 + */ + private static boolean isFixedMode(List list) { + return list.stream().anyMatch(r -> r.getQuestionId() != null && !r.getQuestionId().isEmpty()); + } + + /** + * 固定抽题:比 questionId + score + questionType + */ + private static boolean hasChangedFixed(List oldList, + List newList) { + + Set oldSet = oldList.stream() + .map(ExamChangeDetector::buildFixedKey) + .collect(Collectors.toSet()); + + Set newSet = newList.stream() + .map(ExamChangeDetector::buildFixedKey) + .collect(Collectors.toSet()); + + return !oldSet.equals(newSet); + } + + private static String buildFixedKey(CultivateExamDrawRule r) { + return r.getQuestionType() + "_" + + r.getQuestionId() + "_" + + r.getScore(); + } + + /** + * 随机抽题:按 questionType 唯一比配置 + */ + private static boolean hasChangedRandom(List oldList, + List newList) { + + Map oldMap = oldList.stream() + .collect(Collectors.toMap( + CultivateExamDrawRule::getQuestionType, + r -> r + )); + + Map newMap = newList.stream() + .collect(Collectors.toMap( + CultivateExamDrawRule::getQuestionType, + r -> r + )); + + if (!oldMap.keySet().equals(newMap.keySet())) { + return true; + } + + for (Integer type : oldMap.keySet()) { + CultivateExamDrawRule o = oldMap.get(type); + CultivateExamDrawRule n = newMap.get(type); + + if (!Objects.equals(o.getPickCount(), n.getPickCount())) return true; + if (!Objects.equals(o.getEasyCount(), n.getEasyCount())) return true; + if (!Objects.equals(o.getMediumCount(), n.getMediumCount())) return true; + if (!Objects.equals(o.getHardCount(), n.getHardCount())) return true; + if (!Objects.equals(o.getScore(), n.getScore())) return true; + } + + return false; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeResult.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeResult.java new file mode 100644 index 0000000..e47000a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/ExamChangeResult.java @@ -0,0 +1,42 @@ +package jnpf.cultivate.v2.util; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Collections; +import java.util.List; + +/** + * 考试变更结果 + * + * @author yanwenfu + * @create 2026-04-22 + */ +@Getter +@Setter +public class ExamChangeResult { + + /** 是否有任何变更 */ + private boolean changed; + + /** 基础信息是否变更 */ + private boolean examChanged; + + /** 抽题规则是否变更 */ + private boolean ruleChanged; + + /** 具体变更字段 */ + private List changedFields; + + public static ExamChangeResult noChange() { + ExamChangeResult r = new ExamChangeResult(); + r.changed = false; + r.changedFields = Collections.emptyList(); + return r; + } + + public void markChanged(String field) { + this.changed = true; + this.changedFields.add(field); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/V2QuestionExcelExportUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/V2QuestionExcelExportUtil.java new file mode 100644 index 0000000..123c59d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/cultivate/v2/util/V2QuestionExcelExportUtil.java @@ -0,0 +1,550 @@ +package jnpf.cultivate.v2.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.builder.ExcelWriterBuilder; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jnpf.base.UserInfo; +import jnpf.model.cultivate.req.questionbank.QuestionOptionReq; +import jnpf.model.cultivate.resp.ExcelImportQuestionResultReq; +import jnpf.model.cultivate.resp.ExcelUserQuestionVo; +import jnpf.model.cultivate.resp.QuestionOptionVo; +import jnpf.model.cultivate.resp.UserQuestionVo; +import jnpf.model.cultivate.v2.question.req.V2AddQuestionReq; +import jnpf.model.cultivate.v2.question.req.V2EditQuestionReq; +import jnpf.model.cultivate.v2.question.req.V2ExcelImportQuestionResultReq; +import jnpf.model.enums.CourseEnums; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +public class V2QuestionExcelExportUtil { + + final static String FTB_QUESTION_IMPORT_SHEET_KEY = "qtn:import::%s:%s"; + + public static void questionExportExcel(HttpServletResponse response, String fileName, List list, Class tClass) throws IOException { + String fileRealName = URLEncoder.encode(fileName + System.currentTimeMillis() / 1000, "UTF-8").replaceAll("\\+", "%20"); + try (ServletOutputStream outputStream = response.getOutputStream()) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileRealName + ".xlsx"); + + + ExcelWriterBuilder write = EasyExcel.write(outputStream, tClass); + write.registerWriteHandler(new EasyExcelUtils.CustomCellWriteHandler()).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).inMemory(true); + write.sheet("sheet1").doWrite(list); + + } + } + + public static void questionExportExcel1(HttpServletResponse response, String fileName, List list, Class tClass) throws IOException { + String fileRealName = URLEncoder.encode(fileName + System.currentTimeMillis() / 1000, "UTF-8").replaceAll("\\+", "%20"); + try (ServletOutputStream outputStream = response.getOutputStream()) { + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileRealName + ".xlsx"); + String template = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/xgl/questionExportTemplate.xlsx"; + File localFile = downloadFile(template); + + + // 这里 会填充到第一个sheet, 然后文件流会自动关闭 + EasyExcel.write(outputStream).withTemplate(localFile).sheet("Sheet1").doFill(list); + + + } + } + + public static File downloadFile(String template) { + + // 本地保存的路径及文件名 + String savePath = "./" + System.currentTimeMillis() + "/questionExportTemplate.xlsx"; // 请根据实际情况修改 + + try { + // 下载文件 + File file = FileUtil.writeBytes(HttpUtil.downloadBytes(template), FileUtil.file(savePath)); + return file; + + } catch (Exception e) { + System.err.println("文件下载失败:" + e.getMessage()); + return null; + } + } + + + public static List convertQuestionList(List userQuestionVoList) { + List list = new ArrayList<>(); + for (UserQuestionVo userQuestionVo : userQuestionVoList) { + ExcelUserQuestionVo excelUserQuestionVo = toExcelUserQuestionVo(userQuestionVo); + list.add(excelUserQuestionVo); + } + return list; + } + + private static ExcelUserQuestionVo toExcelUserQuestionVo(UserQuestionVo userQuestionVo) { + ExcelUserQuestionVo vo = new ExcelUserQuestionVo(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.fromCode(userQuestionVo.getType()); + if (null != type) { + vo.setType(type.getDesc()); + } + + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.fromCode(userQuestionVo.getDifficulty()); + if (null != difficulty) { + vo.setDifficulty(difficulty.getDesc()); + } + List questionOptionVoList = new ArrayList<>(); + switch (type) { + case SINGLE://单选 + case JUDGE://判断 + case MULTI://多选 + case ONE_OR_MULTI://不定项 + questionOptionVoList = userQuestionVo.getQuestionOptionVoList(); + if (CollUtil.isNotEmpty(questionOptionVoList)) { + List rightAnsters = new ArrayList<>(); + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setOptionValue(questionOptionVoList.get(i), vo, i); + if (StringUtils.isNotEmpty(right)) { + rightAnsters.add(right); + } + } + vo.setUserAnswer(StringUtils.join(rightAnsters, ",")); + } + break; + case FILL://填空题 + questionOptionVoList = userQuestionVo.getQuestionOptionVoList(); + if (CollUtil.isNotEmpty(questionOptionVoList)) { + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setOptionValue(questionOptionVoList.get(i), vo, i); + } + } + break; + + } + + vo.setContent(userQuestionVo.getContent()); + vo.setAnalysis(userQuestionVo.getAnalysis()); + + return vo; + } + + + private static String setOptionValue(QuestionOptionVo questionOptionVo, ExcelUserQuestionVo vo, Integer i) { + String optionValue = questionOptionVo.getContent(); + Integer isRightOption = questionOptionVo.getIsRightOption(); + return setRightAnswerAndFillOption(optionValue, isRightOption, vo, i); + } + + private static String setErrorOptionValue(QuestionOptionReq questionOptionVo, ExcelUserQuestionVo vo, Integer i) { + String optionValue = questionOptionVo.getContent(); + Integer isRightOption = questionOptionVo.getIsRightOption(); + return setRightAnswerAndFillOption(optionValue, isRightOption, vo, i); + } + + private static String setRightAnswerAndFillOption(String optionValue, Integer isRightOption, ExcelUserQuestionVo vo, Integer i) { + String right = ""; + switch (i) { + case 0: + vo.setOptionA(optionValue); + if (isRightOption == 1) { + right = "A"; + } + break; + case 1: + vo.setOptionB(optionValue); + if (isRightOption == 1) { + right = "B"; + } + break; + case 2: + vo.setOptionC(optionValue); + if (isRightOption == 1) { + right = "C"; + } + break; + case 3: + vo.setOptionD(optionValue); + if (isRightOption == 1) { + right = "D"; + } + break; + case 4: + vo.setOptionE(optionValue); + if (isRightOption == 1) { + right = "E"; + } + break; + case 5: + vo.setOptionF(optionValue); + if (isRightOption == 1) { + right = "F"; + } + break; + case 6: + vo.setOptionG(optionValue); + if (isRightOption == 1) { + right = "G"; + } + break; + case 7: + vo.setOptionH(optionValue); + if (isRightOption == 1) { + right = "H"; + } + break; + default: + break; + } + return right; + } + + + public static void dealFillQuestion(List allQuestionList) { + for (V2ExcelImportQuestionResultReq addQuestionReq : allQuestionList) { + checkOneQuestion(addQuestionReq); + } + } + + public static void checkOneQuestion(V2ExcelImportQuestionResultReq addQuestionReq) { + String content = addQuestionReq.getContent(); + if (StringUtils.isEmpty(content)) { + addQuestionReq.setMsg("题目内容不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (content.length() > 200) { + addQuestionReq.setMsg("题目内容长度不能大于200个字符"); + addQuestionReq.setSuccess(1); + return; + } + if (StringUtils.isNotEmpty(addQuestionReq.getAnalysis())) { + if (addQuestionReq.getAnalysis().length() > 1000) { + addQuestionReq.setMsg("题目解析长度不能大于1000个字符"); + addQuestionReq.setSuccess(1); + return; + } + } + + List optionList = addQuestionReq.getOptionList(); + if (CollUtil.isNotEmpty(optionList)) { + if (optionList.size() > 8) { + addQuestionReq.setMsg("选项不能大于 8 个"); + addQuestionReq.setSuccess(1); + return; + } + for (QuestionOptionReq optionReq : optionList) { + if (StringUtils.isNotEmpty(optionReq.getContent())) { + if (optionReq.getContent().length() > 200) { + addQuestionReq.setMsg("题目选项的内容不能大于200个字符"); + addQuestionReq.setSuccess(1); + return; + } + } + } + } + // 根据题型进行验证 + CourseEnums.QuestionType questionType = CourseEnums.QuestionType.fromCode(addQuestionReq.getType()); + if (questionType == null) { + addQuestionReq.setMsg("题目类型无效"); + addQuestionReq.setSuccess(1); + return; + } + + switch (questionType) { + case FILL: { + checkFill(addQuestionReq, content, optionList); + break; + } + case MULTI: { + checkMulti(addQuestionReq, optionList); + break; + } + case ONE_OR_MULTI: { + checkOneOrMulti(addQuestionReq, optionList); + break; + } + case SINGLE: { + checkSingle(addQuestionReq, optionList); + break; + } + case JUDGE: { + checkJudge(addQuestionReq, optionList); + break; + } + case INPUT: { + break; + } + default: { + } + } + } + + private static void checkJudge(V2ExcelImportQuestionResultReq addQuestionReq, List optionList) { + if (CollUtil.isEmpty(optionList)) { + addQuestionReq.setMsg("判断题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (optionList.size() != 2) { + addQuestionReq.setMsg("判断题选项选项只能是 2 个"); + addQuestionReq.setSuccess(1); + return; + } + + int num = 0; + for (QuestionOptionReq optionReq : optionList) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num > 1) { + addQuestionReq.setMsg("判断题正确答案应该只有 1 个"); + addQuestionReq.setSuccess(1); + return; + } + if (num == 0) { + addQuestionReq.setMsg("判断题必须有 1 个正确答案"); + addQuestionReq.setSuccess(1); + return; + } + } + + private static void checkSingle(V2ExcelImportQuestionResultReq addQuestionReq, List optionList) { + if (CollUtil.isEmpty(optionList)) { + addQuestionReq.setMsg("单选题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (optionList.size() < 2) { + addQuestionReq.setMsg("单选题选项不能少于 2 个"); + addQuestionReq.setSuccess(1); + return; + } + + int num = 0; + for (QuestionOptionReq optionReq : optionList) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num != 1) { + addQuestionReq.setMsg("单选题正确答案应该只有 1 个"); + addQuestionReq.setSuccess(1); + return; + } + } + + private static void checkOneOrMulti(V2ExcelImportQuestionResultReq addQuestionReq, List optionList) { + if (CollUtil.isEmpty(optionList)) { + addQuestionReq.setMsg("不定向选择题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (optionList.size() < 2) { + addQuestionReq.setMsg("不定向选择题选项不能少于 2 个"); + addQuestionReq.setSuccess(1); + return; + } + + int num = 0; + for (QuestionOptionReq optionReq : optionList) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num < 1) { + addQuestionReq.setMsg("不定项选择题正确答案不能小于 1 个"); + addQuestionReq.setSuccess(1); + return; + } + } + + private static void checkMulti(V2ExcelImportQuestionResultReq addQuestionReq, List optionList) { + if (CollUtil.isEmpty(optionList)) { + addQuestionReq.setMsg("多选题选项不能为空"); + addQuestionReq.setSuccess(1); + return; + } + if (optionList.size() < 2) { + addQuestionReq.setMsg("多选题选项不能少于 2 个"); + addQuestionReq.setSuccess(1); + return; + } + + int num = 0; + for (QuestionOptionReq optionReq : optionList) { + if (optionReq.getIsRightOption() != null && optionReq.getIsRightOption() == 1) { + num++; + } + } + if (num < 2) { + addQuestionReq.setMsg("多选题正确答案不能小于 2 个"); + addQuestionReq.setSuccess(1); + return; + } + } + + private static void checkFill(V2ExcelImportQuestionResultReq addQuestionReq, String content, List optionList) { + int count = countUnderscoresConsideringAdjacentAsOne(content); + if (optionList.size() > count) { + addQuestionReq.setOptionList(optionList.subList(0, count)); + } else if (optionList.size() < count) { + long size = optionList.size(); + for (int i = 0; i < count - size; i++) { + QuestionOptionReq optionReq = new QuestionOptionReq(); + optionReq.setContent(""); + optionReq.setSortCode(size + i); + addQuestionReq.getOptionList().add(optionReq); + } + } + if (!addQuestionReq.getContent().contains("()") && !addQuestionReq.getContent().contains("( )")) { + addQuestionReq.setMsg("填空题题目必须包含括号"); + addQuestionReq.setSuccess(1); + return; + } + } + + public static int countUnderscoresConsideringAdjacentAsOne(String str) { + // 使用正则表达式替换连续的下划线为单个下划线 + String normalizedStr = str.replaceAll("\\(+", "("); + normalizedStr = normalizedStr.replaceAll("\\)+", ")"); + normalizedStr = normalizedStr.replaceAll("\\(+", "("); + normalizedStr = normalizedStr.replaceAll("\\)+", ")"); + // 遍历处理后的字符串,计算下划线数量 + int count = 0; + for (int i = 0; i < normalizedStr.length(); i++) { + if (normalizedStr.charAt(i) == '(') { + count++; + } + } + return count; + } + + public static String qeneralQuestionImportKey() { + String loginUserId = UserProvider.getLoginUserId(); + String tenantId = getTenantId(); + return String.format(FTB_QUESTION_IMPORT_SHEET_KEY, tenantId, loginUserId); + } + + public static List getOrgIds(List workerGroupDataDtoList) { + if (CollUtil.isEmpty(workerGroupDataDtoList)) { + return new ArrayList<>(); + } + + List list = new ArrayList<>(); + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtoList) { + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedOrg())) { + list.add(workerGroupDataDto.getAffiliatedOrg()); + } + } + return list; + } + + public List uniqueStringList(List list) { + if (CollUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream().filter(Objects::nonNull) // 过滤掉 null 值 + .filter(str -> !str.isEmpty()) // 过滤掉空字符串 + .distinct() // 去重 + .collect(Collectors.toList()); // 收集为 List + } + + public static List stringToList(String str, String split) { + if (StringUtils.isEmpty(str)) { + return new ArrayList<>(); + } + return Arrays.asList(str.split(split)); + } + + public static List convertErrorQuestionList(List errorList) { + List list = new ArrayList<>(); + for (ExcelImportQuestionResultReq userQuestionVo : errorList) { + ExcelUserQuestionVo excelUserQuestionVo = toErrorExcelUserQuestionVo(userQuestionVo); + list.add(excelUserQuestionVo); + } + return list; + } + + private static ExcelUserQuestionVo toErrorExcelUserQuestionVo(ExcelImportQuestionResultReq userQuestionVo) { + ExcelUserQuestionVo vo = new ExcelUserQuestionVo(); + CourseEnums.QuestionType type = CourseEnums.QuestionType.fromCode(userQuestionVo.getType()); + if (null != type) { + vo.setType(type.getDesc()); + } + + CourseEnums.QuestionDifficulty difficulty = CourseEnums.QuestionDifficulty.fromCode(userQuestionVo.getDifficulty()); + if (null != difficulty) { + vo.setDifficulty(difficulty.getDesc()); + } + List questionOptionVoList = new ArrayList<>(); + switch (type) { + case SINGLE://单选 + case JUDGE://判断 + case MULTI://多选 + case ONE_OR_MULTI://不定项 + questionOptionVoList = userQuestionVo.getOptionList(); + if (CollUtil.isNotEmpty(questionOptionVoList)) { + List rightAnsters = new ArrayList<>(); + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setErrorOptionValue(questionOptionVoList.get(i), vo, i); + if (StringUtils.isNotEmpty(right)) { + rightAnsters.add(right); + } + } + vo.setUserAnswer(StringUtils.join(rightAnsters, ",")); + } + break; + case FILL://填空题 + questionOptionVoList = userQuestionVo.getOptionList(); + if (CollUtil.isNotEmpty(questionOptionVoList)) { + for (int i = 0; i < questionOptionVoList.size(); i++) { + String right = setErrorOptionValue(questionOptionVoList.get(i), vo, i); + } + } + break; + + } + + vo.setContent(userQuestionVo.getContent()); + vo.setAnalysis(userQuestionVo.getAnalysis()); + + return vo; + } + + public static void checkAddQuestionParam(V2AddQuestionReq req) { + V2ExcelImportQuestionResultReq excelImportQuestionResultReq = BeanUtil.copyProperties(req, V2ExcelImportQuestionResultReq.class); + checkOneQuestion(excelImportQuestionResultReq); + if (excelImportQuestionResultReq.getSuccess() == 1) { + throw new RuntimeException(excelImportQuestionResultReq.getMsg()); + } + } + + public static void checkEditQuestionParam(V2EditQuestionReq req) { + V2ExcelImportQuestionResultReq excelImportQuestionResultReq = BeanUtil.copyProperties(req, V2ExcelImportQuestionResultReq.class); + checkOneQuestion(excelImportQuestionResultReq); + if (excelImportQuestionResultReq.getSuccess() == 1) { + throw new RuntimeException(excelImportQuestionResultReq.getMsg()); + } + } + + public static String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } +} diff --git a/ftb/src/main/java/ftb/test/controller/CultureClockInController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureClockInController.java similarity index 76% rename from ftb/src/main/java/ftb/test/controller/CultureClockInController.java rename to jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureClockInController.java index cfcae28..153ee0d 100644 --- a/ftb/src/main/java/ftb/test/controller/CultureClockInController.java +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureClockInController.java @@ -45,8 +45,8 @@ public class CultureClockInController { * @param lastCombo 上次组合 * @param response HttpServletResponse */ - @PostMapping(value = "/random-preview1") - public void getRandomPicPreview(@RequestParam(value = "lastCombo1", required = false) String lastCombo1, HttpServletRequest request, HttpServletResponse response) throws Exception { + @GetMapping(value = "/random-preview") + public void getRandomPicPreview(@RequestParam(value = "lastCombo", required = false) String lastCombo, HttpServletRequest request, HttpServletResponse response) throws Exception { MutablePair pair = cultureClockInService.getRandomPicPreview(lastCombo, requestUrl); if (pair == null || pair.getRight() == null) { @@ -69,30 +69,7 @@ public class CultureClockInController { * @param lastCombo 上次组合 * @param response HttpServletResponse */ -// @GetMapping(value = "/random-preview/base641") - @PutMapping(value = "/random-preview/base64") - public ActionResult getRandomPicPreviewBase64(@RequestParam(value = "lastCombo1", required = false) String lastCombo, HttpServletRequest request, HttpServletResponse response) throws Exception { - - MutablePair pair = cultureClockInService.getRandomPicPreview(lastCombo, requestUrl); - if (pair == null || pair.getRight() == null) { - throw new Exception("获取图片失败,请重试"); - } - byte[] bytes; - try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { - ImageIO.write(pair.getRight(), "png", output); - bytes = output.toByteArray(); - } - String base64 = Base64.getEncoder().encodeToString(bytes); - String base64Img = "data:image/png;base64," + base64; - return ActionResult.success(new Base64ImageVo(pair.getLeft(), base64Img)); - } - - /** - * 打卡分享 - 获取随机图片[base64] - * @param lastCombo 上次组合 - * @param response HttpServletResponse - */ - @DeleteMapping(value = "/random-preview/delete") + @GetMapping(value = "/random-preview/base64") public ActionResult getRandomPicPreviewBase64(@RequestParam(value = "lastCombo", required = false) String lastCombo, HttpServletRequest request, HttpServletResponse response) throws Exception { MutablePair pair = cultureClockInService.getRandomPicPreview(lastCombo, requestUrl); @@ -109,7 +86,6 @@ public class CultureClockInController { return ActionResult.success(new Base64ImageVo(pair.getLeft(), base64Img)); } - /** * 打卡分享 - 打卡 * @param currentCombo 当前组合 @@ -131,9 +107,9 @@ public class CultureClockInController { * @param limitNum 返回有数据的天数 * @return jnpf.base.ActionResult */ - @GetMapping(value = "/dynamic1") - public ActionResult getRecordList(@RequestParam(value = "cursorDate", required = false) Boolean cursorDate, - @RequestParam(value = "limitNum", required = false, defaultValue = "10") String limitNum) { + @GetMapping(value = "/dynamic") + public ActionResult getRecordList(@RequestParam(value = "cursorDate", required = false) String cursorDate, + @RequestParam(value = "limitNum", required = false, defaultValue = "10") Integer limitNum) { limitNum = Math.max(10, Math.min(limitNum, 30)); RecordListVo recordList = cultureClockInService.getRecordList(cursorDate, limitNum); diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CulturePicSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CulturePicSettingController.java new file mode 100644 index 0000000..beccb68 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CulturePicSettingController.java @@ -0,0 +1,93 @@ +package jnpf.culture.controller; + +import jnpf.base.ActionResult; +import jnpf.culture.service.CulturePicSettingService; +import jnpf.model.culture.dto.CulturePicSettingDto; +import jnpf.model.culture.dto.SettingQueryDto; +import jnpf.model.culture.dto.UploadDto; +import jnpf.model.culture.vo.CulturePicSettingVo; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 文化打卡 - 图片配置 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@RestController +@RequestMapping(value = "/culture/pic-setting") +public class CulturePicSettingController { + + @Resource + private CulturePicSettingService culturePicSettingService; + + /** + * 查询图片配置数量 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/count") + public ActionResult getCulturePicSettingCount() { + + Integer num = culturePicSettingService.getCulturePicSettingCount(); + return ActionResult.success(num); + } + + /** + * 图片配置 - 列表 + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/list") + public ActionResult> getPicSettingList(SettingQueryDto queryDto) { + + List list = culturePicSettingService.getPicSettingList(queryDto); + return ActionResult.success(list); + } + + /** + * 图片配置 - 改名 + * @param id 图片配置id + * @param picSettingDto 配置dto + * @return jnpf.base.ActionResult + */ + @PutMapping(value = "/pic-name/{id}") + public ActionResult updatePicName(@PathVariable(value = "id") String id, @RequestBody @Valid CulturePicSettingDto picSettingDto) { + + culturePicSettingService.updatePicName(id, picSettingDto); + return ActionResult.success(); + } + + /** + * 图片配置 - 批量删除 + * @param ids 图片ids(英文逗号分隔) + * @return jnpf.base.ActionResult + */ + @DeleteMapping(value = "/batch") + public ActionResult deletePicSettingBatch(@RequestParam(value = "ids") String ids) throws Exception { + + String[] split; + try { + split = ids.split(","); + } catch (Exception e) { + return ActionResult.fail("请选择要删除的图片配置"); + } + culturePicSettingService.deletePicSettingBatch(List.of(split)); + return ActionResult.success(); + } + + /** + * 图片配置 - 上传图片 + * @param uploadDto 上传dto + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/upload") + public ActionResult uploadPicSetting(@RequestBody @Valid UploadDto uploadDto) throws Exception { + + culturePicSettingService.uploadPicSetting(uploadDto); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureStatController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureStatController.java new file mode 100644 index 0000000..440d224 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureStatController.java @@ -0,0 +1,60 @@ +package jnpf.culture.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.culture.service.CultureStatService; +import jnpf.model.culture.dto.StatQueryDto; +import jnpf.model.culture.vo.CultureStatVo; +import jnpf.util.*; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.rmi.server.ExportException; + +/** + * 文化打卡 - 统计 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@RestController +@RequestMapping(value = "/culture/stat") +public class CultureStatController { + + @Resource + private CultureStatService cultureStatService; + + @Resource + private ExportUtil exportUtil; + + /** + * 打卡统计 - 列表(分页) + * @param queryDto 查询dto + * @return jnpf.base.ActionResult> + */ + @PostMapping(value = "/page") + public ActionResult> getStatPage(@RequestBody StatQueryDto queryDto) { + + PageInfo page = cultureStatService.getStatPage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 打卡统计 - 导出 + * @param queryDto 查询条件 + * @param response HttpServletResponse + */ + @PostMapping(value = "/export") + public void getStatExport(@RequestBody StatQueryDto queryDto, HttpServletResponse response) throws Exception { + + Workbook workbook = cultureStatService.getStatWorkbook(queryDto); + if (null != workbook) { + exportUtil.exportTemplateExcel(workbook, response, ConstantUtil.CULTURE_CLOCK_IN_FORM); + } else { + throw new ExportException("导出" + ConstantUtil.CULTURE_CLOCK_IN_FORM + "失败"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureTextSettingController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureTextSettingController.java new file mode 100644 index 0000000..81af2aa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/controller/CultureTextSettingController.java @@ -0,0 +1,149 @@ +package jnpf.culture.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.DownloadVO; +import jnpf.base.vo.PageListVO; +import jnpf.culture.service.CultureTextSettingService; +import jnpf.model.culture.dto.CultureTextSettingDto; +import jnpf.model.culture.dto.SettingQueryDto; +import jnpf.model.culture.vo.CultureTextSettingVo; +import jnpf.model.culture.vo.UploadInfoVo; +import jnpf.util.FtbUtil; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 文化打卡 - 文字配置 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@RestController +@RequestMapping(value = "/culture/text-setting") +public class CultureTextSettingController { + + @Resource + private CultureTextSettingService cultureTextSettingService; + + /** + * 查询文字配置数量 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/count") + public ActionResult getCultureTextSettingCount() { + + Integer num = cultureTextSettingService.getCultureTextSettingCount(); + return ActionResult.success(num); + } + + /** + * 文字配置 - 列表(分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/page") + public ActionResult> getCultureTextSettingList(SettingQueryDto queryDto) { + + PageInfo page = cultureTextSettingService.getCultureTextSettingList(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 文字配置 - 新增 + * @param textSettingDto 文案设置dto + * @return jnpf.base.ActionResult + */ + @PostMapping + public ActionResult addTextSetting(@RequestBody @Valid CultureTextSettingDto textSettingDto) throws Exception { + + cultureTextSettingService.addTextSetting(textSettingDto); + return ActionResult.success(); + } + + /** + * 文字配置 - 编辑 + * @param textSettingDto 文案设置dto + * @return jnpf.base.ActionResult + */ + @PutMapping(value = "/{id}") + public ActionResult updateTextSetting(@PathVariable(value = "id") String id, @RequestBody @Valid CultureTextSettingDto textSettingDto) throws Exception { + + cultureTextSettingService.updateTextSetting(id, textSettingDto); + return ActionResult.success(); + } + + /** + * 文字配置 - 批量删除 + * @param ids 文案ids(英文逗号分隔) + * @return jnpf.base.ActionResult + */ + @DeleteMapping(value = "/batch") + public ActionResult deleteTextSettingBatch(@RequestParam(value = "ids") String ids) throws Exception { + + String[] split; + try { + split = ids.split(","); + } catch (Exception e) { + return ActionResult.fail("请选择要删除的文字配置"); + } + cultureTextSettingService.deleteTextSettingBatch(List.of(split)); + return ActionResult.success(); + } + + /** + * 文字配置 - 模板下载 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/template-download") + public ActionResult templateDownload() { + DownloadVO vo = DownloadVO.builder().build(); + try { + vo.setName("文化打卡导入模板" + System.currentTimeMillis() + ".xlsx"); + vo.setUrl("https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/TemplateFile/文化打卡导入模板.xlsx"); + } catch (Exception e) { + return ActionResult.fail("下载模板失败..."); + } + return ActionResult.success(vo); + } + + /** + * 文字配置 - 导入 + * @param file 文件 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/import") + public ActionResult textSettingImport(@RequestParam MultipartFile file) throws Exception { + + if (file == null || file.isEmpty()) { + return ActionResult.fail("请上传文件"); + } + + // 校验文件后缀 + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || + !(originalFilename.endsWith(".xlsx") || originalFilename.endsWith(".xls"))) { + return ActionResult.fail("文件格式错误,仅支持 .xlsx 或 .xls"); + } + + // 校验 Content-Type(防止随便改后缀) + String contentType = file.getContentType(); + if (!isExcelContentType(contentType)) { + return ActionResult.fail("文件类型非法,请上传 Excel 文件"); + } + UploadInfoVo uploadInfo = cultureTextSettingService.textSettingImport(file); + return ActionResult.success(uploadInfo); + } + + private boolean isExcelContentType(String contentType) { + if (contentType == null) { + return false; + } + return contentType.equals("application/vnd.ms-excel") + || contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInMapper.java new file mode 100644 index 0000000..884c992 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInMapper.java @@ -0,0 +1,52 @@ +package jnpf.culture.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.culture.CultureClockIn; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDate; +import java.util.List; + +/** + * 文化打卡记录mapper + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureClockInMapper extends SuperMapper { + + /** + * 查询打卡记录列表 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @param userIds 用户ids + * @return java.util.List + */ + List getClockInList(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("list") List userIds); + + /** + * 查询一年的打卡日期 + * @param userId 用户id + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return java.util.List + */ + List getClockInDateByYear(@Param("userId") String userId, @Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate); + + /** + * 查询日期游标列表 + * @param userId 用户id + * @param cursorDate 游标日期 + * @param limitPlusOne 分页数量 + 1 + * @return java.util.List + */ + List getClockInDateList(@Param("userId") String userId, @Param("cursorDate") String cursorDate, @Param("limitPlusOne") Integer limitPlusOne); + + /** + * 查询这些日期下的打卡记录 + * @param userId 用户id + * @param pageDates 日期列表 + * @return java.util.List + */ + List getClockInListByDates(@Param("userId") String userId, @Param("list") List pageDates); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInStatMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInStatMapper.java new file mode 100644 index 0000000..e968e63 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureClockInStatMapper.java @@ -0,0 +1,24 @@ +package jnpf.culture.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.culture.CultureClockInStat; +import jnpf.model.culture.dto.StatQueryDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 文化打卡统计mapper + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureClockInStatMapper extends SuperMapper { + + /** + * 打卡统计列表 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getStatList(@Param("queryDto") StatQueryDto queryDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicSettingMapper.java new file mode 100644 index 0000000..ea06ae6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicSettingMapper.java @@ -0,0 +1,28 @@ +package jnpf.culture.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.culture.CulturePicSetting; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 文化打卡 - 图片配置 + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CulturePicSettingMapper extends SuperMapper { + + /** 查询所有可用的id */ + List selectAllIds(); + + /** + * 查询时段内被删除的图片id + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.util.List + */ + List getDelIdsByDate(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicTempMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicTempMapper.java new file mode 100644 index 0000000..3da518a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CulturePicTempMapper.java @@ -0,0 +1,13 @@ +package jnpf.culture.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.culture.CulturePicTemp; + +/** + * 文化打卡 - 组合缓存 + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CulturePicTempMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureTextSettingMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureTextSettingMapper.java new file mode 100644 index 0000000..f8261ae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/mapper/CultureTextSettingMapper.java @@ -0,0 +1,51 @@ +package jnpf.culture.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.culture.CultureTextSetting; +import jnpf.model.culture.vo.CultureTextSettingVo; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 文化打卡 - 文案配置 + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureTextSettingMapper extends SuperMapper { + + /** + * 判定标题+内容是否重复 + * @param cultureTitle 标题 + * @param cultureContent 内容 + * @return int + */ + int getDistinctCount(@Param("cultureTitle") String cultureTitle, @Param("cultureContent") String cultureContent, @Param("id") String id); + + /** + * 查询文字配置列表 + * @param keyword 文化标题/文化内容 + * @return java.util.List + */ + List getCultureTextSettingList(@Param("keyword") String keyword); + + /** + * 查询数据库存在的配置的index + * @param list 配置列表 + * @return java.util.List + */ + List getDistinctCountBatch(@Param("list") List list); + + /** 查询所有可用的id */ + List selectAllIds(); + + /** + * 查询时段内被删除的ids + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return java.util.List + */ + List getDelIdsByDate(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureAppService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureAppService.java new file mode 100644 index 0000000..35217e9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureAppService.java @@ -0,0 +1,10 @@ +package jnpf.culture.service; + +/** + * 文化打卡服务 - app + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureAppService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureClockInService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureClockInService.java new file mode 100644 index 0000000..673a67e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureClockInService.java @@ -0,0 +1,50 @@ +package jnpf.culture.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.culture.CultureClockIn; +import jnpf.model.culture.vo.CultureClockInVo; +import jnpf.model.culture.vo.RecordListVo; +import jnpf.model.culture.vo.YearDataVo; +import org.apache.commons.lang3.tuple.MutablePair; + +import java.awt.image.BufferedImage; + +/** + * 文化打卡服务 + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureClockInService extends SuperService { + + /** + * 打卡分享 - 获取随机图片 + * @param lastCombo 上次组合 + * @param requestUrl 请求地址 + * @return org.apache.commons.lang3.tuple.MutablePair + */ + MutablePair getRandomPicPreview(String lastCombo, String requestUrl) throws Exception; + + /** + * 打卡分享 - 打卡 + * @param currentCombo 当前组合 + * @param requestUrl 请求地址 + * @return jnpf.model.culture.vo.CultureClockInVo + */ + CultureClockInVo getCultureClockInPic(String currentCombo, String requestUrl) throws Exception; + + /** + * 打卡动态 - 打卡记录列表 + * @param cursorDate 游标日期(yyyy-MM-dd)[首次不传] + * @param limitNum 返回有数据的天数 + * @return jnpf.model.culture.vo.RecordListVo + */ + RecordListVo getRecordList(String cursorDate, Integer limitNum); + + /** + * 打卡日历 + * @param year 年 + * @return jnpf.model.culture.vo.YearDataVo + */ + YearDataVo getYearData(Integer year); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CulturePicSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CulturePicSettingService.java new file mode 100644 index 0000000..1042ca9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CulturePicSettingService.java @@ -0,0 +1,51 @@ +package jnpf.culture.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.culture.CulturePicSetting; +import jnpf.model.culture.dto.CulturePicSettingDto; +import jnpf.model.culture.dto.SettingQueryDto; +import jnpf.model.culture.dto.UploadDto; +import jnpf.model.culture.vo.CulturePicSettingVo; + +import java.util.List; + +/** + * 文化打卡 - 图片配置 + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CulturePicSettingService extends SuperService { + + /** + * 图片配置 - 列表 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getPicSettingList(SettingQueryDto queryDto); + + /** + * 图片配置 - 改名 + * @param id 图片配置id + * @param picSettingDto 配置dto + */ + void updatePicName(String id, CulturePicSettingDto picSettingDto); + + /** + * 图片配置 - 批量删除 + * @param ids 图片配置ids + */ + void deletePicSettingBatch(List ids) throws Exception; + + /** + * 图片配置 - 上传图片 + * @param uploadDto 上传dto + */ + void uploadPicSetting(UploadDto uploadDto) throws Exception; + + /** + * 查询图片配置数量 + * @return java.lang.Integer + */ + Integer getCulturePicSettingCount(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureStatService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureStatService.java new file mode 100644 index 0000000..08e04e8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureStatService.java @@ -0,0 +1,29 @@ +package jnpf.culture.service; + +import com.github.pagehelper.PageInfo; +import jnpf.model.culture.dto.StatQueryDto; +import jnpf.model.culture.vo.CultureStatVo; +import org.apache.poi.ss.usermodel.Workbook; + +/** + * 文化打卡 - 统计 服务 + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureStatService { + + /** + * 打卡统计 - 列表(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getStatPage(StatQueryDto queryDto); + + /** + * 打卡统计 - 导出 + * @param queryDto 查询条件 + * @return org.apache.poi.ss.usermodel.Workbook + */ + Workbook getStatWorkbook(StatQueryDto queryDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureTextSettingService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureTextSettingService.java new file mode 100644 index 0000000..35d44e4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/CultureTextSettingService.java @@ -0,0 +1,61 @@ +package jnpf.culture.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.culture.CultureTextSetting; +import jnpf.model.culture.dto.CultureTextSettingDto; +import jnpf.model.culture.dto.SettingQueryDto; +import jnpf.model.culture.vo.CultureTextSettingVo; +import jnpf.model.culture.vo.UploadInfoVo; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 文化打卡服务 - web + * + * @author yanwenfu + * @create 2025-12-23 + */ +public interface CultureTextSettingService extends SuperService { + + /** + * 文字配置 - 列表(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getCultureTextSettingList(SettingQueryDto queryDto); + + /** + * 文字配置 - 新增 + * @param textSettingDto 文案设置dto + */ + void addTextSetting(CultureTextSettingDto textSettingDto) throws Exception; + + /** + * 文字配置 - 编辑 + * @param id 文案id + * @param textSettingDto 文案设置dto + */ + void updateTextSetting(String id, CultureTextSettingDto textSettingDto) throws Exception; + + /** + * 文字配置 - 批量删除 + * @param ids 文案ids + */ + void deleteTextSettingBatch(List ids) throws Exception; + + /** + * 文字配置 - 导入 + * @param file 文件 + * @throws Exception 导入异常 + * @return jnpf.model.culture.vo.UploadInfoVo + */ + UploadInfoVo textSettingImport(MultipartFile file) throws Exception; + + /** + * 查询文字配置数量 + * @return java.lang.Integer + */ + Integer getCultureTextSettingCount(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureAppServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureAppServiceImpl.java new file mode 100644 index 0000000..e8e0986 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureAppServiceImpl.java @@ -0,0 +1,14 @@ +package jnpf.culture.service.impl; + +import jnpf.culture.service.CultureAppService; +import org.springframework.stereotype.Service; + +/** + * 文化打卡服务实现 - app + * + * @author yanwenfu + * @create 2025-12-23 + */ +@Service +public class CultureAppServiceImpl implements CultureAppService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureClockInServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureClockInServiceImpl.java new file mode 100644 index 0000000..ab71962 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureClockInServiceImpl.java @@ -0,0 +1,334 @@ +package jnpf.culture.service.impl; + +import cn.xuyanwu.spring.file.storage.FileInfo; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.base.SysConfigApi; +import jnpf.base.UserInfo; +import jnpf.base.model.systemconfig.SysConfigModel; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constant.FileTypeConstant; +import jnpf.culture.mapper.*; +import jnpf.culture.service.CultureClockInService; +import jnpf.culture.util.ImageComboUtil; +import jnpf.entity.culture.*; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.file.FileApi; +import jnpf.file.FileUploadApi; +import jnpf.model.culture.vo.CultureClockInVo; +import jnpf.model.culture.vo.RecordDataVo; +import jnpf.model.culture.vo.RecordListVo; +import jnpf.model.culture.vo.YearDataVo; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.awt.image.BufferedImage; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 文化打卡服务实现 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@Service +public class CultureClockInServiceImpl extends SuperServiceImpl implements CultureClockInService { + + @Resource + private CultureClockInStatMapper cultureClockInStatMapper; + @Resource + private CultureClockInMapper cultureClockInMapper; + @Resource + private CultureTextSettingMapper cultureTextSettingMapper; + @Resource + private CulturePicSettingMapper culturePicSettingMapper; + @Resource + private CulturePicTempMapper culturePicTempMapper; + @Autowired + private FileUploadApi fileUploadApi; + @Autowired + private FileApi fileApi; + @Autowired + private SysConfigApi sysConfigApi; + @Autowired + private ImageComboUtil imageComboUtil; + @Autowired + private V2UserApi v2UserApi; + + @Transactional(rollbackFor = Exception.class) + @Override + public MutablePair getRandomPicPreview(String lastCombo, String requestUrl) throws Exception { + + UserInfo user = UserProvider.getUser(); + // 查询最近2天被删除的图片id和文字id + LocalDate today = LocalDate.now(); + LocalDateTime startTime = today.minusDays(1).atStartOfDay(); + LocalDateTime endTime = today.plusDays(1).atStartOfDay(); + List delPicIds = culturePicSettingMapper.getDelIdsByDate(startTime, endTime); + List delTextIds = cultureTextSettingMapper.getDelIdsByDate(startTime, endTime); + if (imageComboUtil.checkNeedGenerate()) { + // 生成新的组合池 + List allPicIds = culturePicSettingMapper.selectAllIds(); + List allTextIds = cultureTextSettingMapper.selectAllIds(); + imageComboUtil.generateComboPool(allPicIds, allTextIds, delPicIds, delTextIds); + } + String randomCombo = imageComboUtil.getRandomCombo(lastCombo, user.getUserId(), delPicIds, delTextIds); + if (null == randomCombo) { + List allPicIds = culturePicSettingMapper.selectAllIds(); + List allTextIds = cultureTextSettingMapper.selectAllIds(); + imageComboUtil.generateComboPool(allPicIds, allTextIds, delPicIds, delTextIds); + randomCombo = imageComboUtil.getRandomCombo(lastCombo, user.getUserId(), delPicIds, delTextIds); + if (null == randomCombo) { + throw new QueryException("获取图片失败,请重试"); + } + } + BufferedImage finalImage = generateClockImage(randomCombo, requestUrl, user, ConstantUtil.NUM_FALSE); + return MutablePair.of(randomCombo, finalImage); + } + + /** + * 生成打卡图片 + * @param randomCombo 随机组合 + * @param requestUrl 请求地址 + * @param user 用户信息 + * @param clockType 0: 预览, 1: 打卡 + * @return java.awt.image.BufferedImage + */ + private BufferedImage generateClockImage(String randomCombo, String requestUrl, UserInfo user, Integer clockType) throws Exception { + + // 查询图片缓存中是否有当前组合 + String[] split = randomCombo.split("_"); + LambdaQueryWrapper tempQuery = new LambdaQueryWrapper() + .eq(CulturePicTemp::getPicId, split[0]) + .eq(CulturePicTemp::getContentId, split[1]); + CulturePicTemp tempPic = culturePicTempMapper.selectOne(tempQuery); + String tempUrl; + if (null == tempPic) { + // 查询图片、文案原始数据 + CulturePicSetting pic = culturePicSettingMapper.selectById(split[0]); + CultureTextSetting text = cultureTextSettingMapper.selectById(split[1]); + // 生成图片并上传到oss + // 查询企业头像 + String companyIconUrl = getCompanyIconUrl(requestUrl); + BufferedImage baseImage = imageComboUtil.generateBaseImage(requestUrl + pic.getPicUrl(), companyIconUrl, text.getCultureTitle(), text.getCultureContent()); + MultipartFile multiFile = imageComboUtil.bufferedImageToMultipartFile(baseImage); + String path = fileApi.getPath(FileTypeConstant.TEMPORARY); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multiFile, path, multiFile.getOriginalFilename()); + tempUrl = ConstantUtil.URL_HEAD + fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getOriginalFilename(); + // 1.3 存入临时表 + CulturePicTemp newTemp = new CulturePicTemp(); + newTemp.setId(FtbUtil.getId()); + newTemp.setPicId(split[0]); + newTemp.setContentId(split[1]); + newTemp.setTempUrl(tempUrl); + culturePicTempMapper.insert(newTemp); + } else { + tempUrl = tempPic.getTempUrl(); + } + // 打卡图片并添加 头像 姓名 已累计打卡xx天 打卡时间 + LambdaQueryWrapper statQuery = new LambdaQueryWrapper() + .eq(CultureClockInStat::getUserId, user.getUserId()) + .eq(CultureClockInStat::getDeleteMark, ConstantUtil.NUM_FALSE); + CultureClockInStat stat = cultureClockInStatMapper.selectOne(statQuery); + String str; + if (null == stat) { + str = ConstantUtil.NUM_FALSE == clockType ? "0" : "1"; + } else { + if (ConstantUtil.NUM_FALSE == clockType || stat.getLastClockInDate().equals(LocalDate.now())) { + str = stat.getTotalDays().toString(); + } else { + str = String.valueOf(stat.getTotalDays() + 1); + } + } + MutablePair pair = getUserIconUrl(user.getUserId(), user.getTenantId(), requestUrl); + return imageComboUtil.generateFinalImage(tempUrl, pair.getRight(), pair.getLeft(), str); + } + + private String getCompanyIconUrl(String requestUrl) { + + SysConfigModel configModel = sysConfigApi.getSysConfigModel(); + String companyIconUrl; + if (null == configModel || StringUtil.isEmpty(configModel.getAppIcon())) { + companyIconUrl = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/SystemFile/company-default-logo.png"; + } else { + companyIconUrl = requestUrl + configModel.getAppIcon(); + } + return companyIconUrl; + } + + private MutablePair getUserIconUrl(String userId, String tenantId, String requestUrl) { + + List list = v2UserApi.userListAndCopy(List.of(userId), null, tenantId); + String userIconUrl = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/SystemFile/user-default-icon.png"; + String userName = "未知"; + if (null != list && !list.isEmpty()) { + UserBoundVO user = list.get(0); + if (StringUtil.isNotEmpty(user.getHeadIcon())) { + userIconUrl = requestUrl + UploaderUtil.uploaderImg(list.get(0).getHeadIcon()); + } + if (StringUtil.isNotEmpty(user.getUserName())) { + userName = user.getUserName(); + } + } + return MutablePair.of(userName, userIconUrl); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public CultureClockInVo getCultureClockInPic(String currentCombo, String requestUrl) throws Exception { + + UserInfo user = UserProvider.getUser(); + // 查询用户今日打卡次数 + LambdaQueryWrapper clockQuery = new LambdaQueryWrapper() + .eq(CultureClockIn::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(CultureClockIn::getUserId, user.getUserId()) + .eq(CultureClockIn::getClockInDate, LocalDate.now()); + Long count = cultureClockInMapper.selectCount(clockQuery); + if (count.intValue() >= 50) { + throw new HandleException("今日已打卡50次了~先休息一下明天继续吧"); + } + // 生成图片 + BufferedImage bufferedImage = generateClockImage(currentCombo, requestUrl, user, ConstantUtil.NUM_TRUE); + MultipartFile multiFile = imageComboUtil.bufferedImageToMultipartFile(bufferedImage); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multiFile, fileApi.getPath(FileTypeConstant.TEMPORARY), multiFile.getOriginalFilename()); + String url = ConstantUtil.URL_HEAD + fileInfo.getBasePath() + fileInfo.getPath() + fileInfo.getOriginalFilename(); + // String url = fileInfo.getUrl().replace(ConstantUtil.EXTRA_PATH, ""); + // 生成打卡记录 + CultureClockIn cultureClockIn = new CultureClockIn(); + cultureClockIn.setId(FtbUtil.getId()); + cultureClockIn.setUserId(user.getUserId()); + cultureClockIn.setClockInDate(LocalDate.now()); + cultureClockIn.setClockInTime(new Date()); + String[] split = currentCombo.split("_"); + cultureClockIn.setPicId(split[0]); + cultureClockIn.setContentId(split[1]); + cultureClockIn.setPicUrl(url); + cultureClockIn.setCreatorUserId(user.getUserId()); + cultureClockIn.setLastModifyUserId(user.getUserId()); + cultureClockIn.setDeleteMark(ConstantUtil.NUM_FALSE); + cultureClockInMapper.insert(cultureClockIn); + CultureClockInVo vo = JsonUtil.getJsonToBean(cultureClockIn, CultureClockInVo.class); + // 统计数据更新 + LambdaQueryWrapper statQuery = new LambdaQueryWrapper() + .eq(CultureClockInStat::getUserId, user.getUserId()) + .eq(CultureClockInStat::getDeleteMark, ConstantUtil.NUM_FALSE); + CultureClockInStat stat = cultureClockInStatMapper.selectOne(statQuery); + if (null == stat) { + stat = new CultureClockInStat(); + stat.setUserId(user.getUserId()); + stat.setTotalDays(1); + stat.setCurrentContinuousDays(1); + stat.setMaxContinuousDays(1); + stat.setMaxMissDays(0); + stat.setLastClockInDate(LocalDate.now()); + stat.setCreatorUserId(user.getUserId()); + stat.setLastModifyUserId(user.getUserId()); + stat.setDeleteMark(ConstantUtil.NUM_FALSE); + vo.setFirstClick(Boolean.TRUE); + vo.setContinueTimes(stat.getCurrentContinuousDays()); + } + if (null == stat.getId()) { + stat.setId(FtbUtil.getId()); + cultureClockInStatMapper.insert(stat); + } else { + // 计算统计值并更新 + if (!stat.getLastClockInDate().equals(LocalDate.now())) { + stat.setTotalDays(stat.getTotalDays() + 1); + vo.setFirstClick(Boolean.TRUE); + } + // 当前打卡日期如果等于今天, 今天已经打过了不增加, 如果等于昨天+1, 小于昨天归1 + LocalDate yesterday = LocalDate.now().minusDays(1); + if (stat.getLastClockInDate().isBefore(yesterday)) { + stat.setCurrentContinuousDays(1); + } else if (stat.getLastClockInDate().isEqual(yesterday)) { + stat.setCurrentContinuousDays(stat.getCurrentContinuousDays() + 1); + } + // 如果当前连续打卡天数大于历史连续打卡天数 更新 + if (stat.getCurrentContinuousDays() > stat.getMaxContinuousDays()) { + stat.setMaxContinuousDays(stat.getCurrentContinuousDays()); + } + // 当前打卡时间 - 上次打卡时间 如果大于历史最大缺卡天数则更新 + int currentMissDays = DateDetail.calculateDayDiff(stat.getLastClockInDate(), LocalDate.now()) - 1; + stat.setMaxMissDays(Math.max(currentMissDays, stat.getMaxMissDays())); + stat.setLastClockInDate(LocalDate.now()); + stat.setLastModifyTime(new Date()); + stat.setLastModifyUserId(user.getUserId()); + cultureClockInStatMapper.updateById(stat); + vo.setContinueTimes(stat.getCurrentContinuousDays()); + } + return vo; + } + + @Override + public RecordListVo getRecordList(String cursorDate, Integer limitNum) { + + RecordListVo recordList = new RecordListVo(); + UserInfo user = UserProvider.getUser(); + // 1. 查询日期游标列表 + List dateList = cultureClockInMapper.getClockInDateList(user.getUserId(), cursorDate, limitNum + 1); + if (dateList.isEmpty()) { + return recordList; + } + // 实际用于返回的日期 + List pageDates; + if (dateList.size() > limitNum) { + recordList.setHasMore(true); + // 取前 limitNum 条作为本页 + pageDates = dateList.subList(0, limitNum); + // 第 limitNum 条(下标 limitNum - 1)作为下一页游标 + recordList.setNextCursor(pageDates.get(pageDates.size() - 1).toString()); + } else { + pageDates = dateList; + } + // 2. 查询这些日期下的打卡记录 + List records = cultureClockInMapper.getClockInListByDates(user.getUserId(), pageDates); + // 3. 按日期分组 + Map> groupMap = records.stream().collect(Collectors.groupingBy(CultureClockIn::getClockInDate)); + // 4. 组装数据返回 + pageDates.forEach(day -> { + List returnList = groupMap.get(day); + List picList = returnList.stream().map(CultureClockIn::getPicUrl).collect(Collectors.toList()); + long epochMilli = day + .atStartOfDay(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + recordList.getDataList().add(new RecordDataVo(epochMilli, picList.size(), picList)); + }); + return recordList; + } + + @Override + public YearDataVo getYearData(Integer year) { + + YearDataVo yearData = new YearDataVo(); + UserInfo user = UserProvider.getUser(); + List list = v2UserApi.userListAndCopy(List.of(user.getUserId()), null, user.getTenantId()); + if (null != list && !list.isEmpty()) { + yearData.setUserName(list.get(0).getUserName()); + yearData.setHeadIcon(list.get(0).getHeadIcon()); + } + // 查询累计打卡次数 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(CultureClockInStat::getUserId, user.getUserId()) + .eq(CultureClockInStat::getDeleteMark, ConstantUtil.NUM_FALSE); + CultureClockInStat stat = cultureClockInStatMapper.selectOne(queryWrapper); + yearData.setTotalDays(null == stat ? 0 : stat.getTotalDays()); + // 查询打卡日期 + LocalDate startDate = LocalDate.of(year, 1, 1); + LocalDate endDate = LocalDate.of(year + 1, 1, 1); + List dateList = cultureClockInMapper.getClockInDateByYear(user.getUserId(), startDate, endDate); + yearData.setDataDays(dateList); + return yearData; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CulturePicSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CulturePicSettingServiceImpl.java new file mode 100644 index 0000000..ed8b83d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CulturePicSettingServiceImpl.java @@ -0,0 +1,110 @@ +package jnpf.culture.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.base.service.SuperServiceImpl; +import jnpf.culture.mapper.CulturePicSettingMapper; +import jnpf.culture.mapper.CulturePicTempMapper; +import jnpf.culture.service.CulturePicSettingService; +import jnpf.entity.culture.CulturePicSetting; +import jnpf.entity.culture.CulturePicTemp; +import jnpf.exception.HandleException; +import jnpf.model.culture.dto.CulturePicSettingDto; +import jnpf.model.culture.dto.SettingQueryDto; +import jnpf.model.culture.dto.UploadDto; +import jnpf.model.culture.vo.CulturePicSettingVo; +import jnpf.util.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * 文化打卡 - 图片配置 服务实现 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@Service +public class CulturePicSettingServiceImpl extends SuperServiceImpl implements CulturePicSettingService { + + @Resource + private CulturePicTempMapper culturePicTempMapper; + + @Override + public List getPicSettingList(SettingQueryDto queryDto) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(CulturePicSetting::getDeleteMark, ConstantUtil.NUM_FALSE) + .like(StringUtil.isNotEmpty(queryDto.getKeyword()), CulturePicSetting::getPicName, queryDto.getKeyword()) + .orderByDesc(CulturePicSetting::getCreatorTime); + List list = this.list(queryWrapper); + if (list.isEmpty()) { + return List.of(); + } + return JsonUtil.getJsonToList(list, CulturePicSettingVo.class); + } + + @Override + public void updatePicName(String id, CulturePicSettingDto picSettingDto) { + + this.update(new LambdaUpdateWrapper() + .eq(CulturePicSetting::getId, id) + .set(CulturePicSetting::getPicName, picSettingDto.getPicName()) + .set(CulturePicSetting::getLastModifyTime, new Date()) + .set(CulturePicSetting::getLastModifyUserId, UserProvider.getLoginUserId())); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deletePicSettingBatch(List ids) throws Exception { + + Long dbCount = getDbCount(); + if (dbCount.intValue() - ids.size() < 1) { + throw new HandleException("最少需保留一张图片,不可全部删除"); + } + // 删除图片配置 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .in(CulturePicSetting::getId, ids) + .set(CulturePicSetting::getDeleteMark, ConstantUtil.NUM_TRUE) + .set(CulturePicSetting::getDeleteUserId, UserProvider.getLoginUserId()) + .set(CulturePicSetting::getDeleteTime, new Date()); + this.update(null, updateWrapper); + // 删除图片配置关联的临时图片 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(CulturePicTemp::getPicId, ids); + culturePicTempMapper.delete(queryWrapper); + } + + private Long getDbCount() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(CulturePicSetting::getDeleteMark, ConstantUtil.NUM_FALSE); + return this.count(queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void uploadPicSetting(UploadDto uploadDto) throws Exception { + + Long dbCount = getDbCount(); + if (dbCount + uploadDto.getUploadList().size() > 100) { + throw new HandleException("新增图片配置失败,新增数量超出(100)限制"); + } + List list = JsonUtil.getJsonToList(uploadDto.getUploadList(), CulturePicSetting.class); + list.forEach(v -> { + v.setId(FtbUtil.getId()); + v.setDeleteMark(ConstantUtil.NUM_FALSE); + v.setCreatorUserId(UserProvider.getLoginUserId()); + v.setLastModifyUserId(UserProvider.getLoginUserId()); + }); + this.saveBatch(list); + } + + @Override + public Integer getCulturePicSettingCount() { + + return getDbCount().intValue(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureStatServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureStatServiceImpl.java new file mode 100644 index 0000000..e299766 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureStatServiceImpl.java @@ -0,0 +1,239 @@ +package jnpf.culture.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.authority.service.FtbPermissionOrganizeService; +import jnpf.authority.service.FtbPermissionUsersService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.culture.mapper.CultureClockInMapper; +import jnpf.culture.mapper.CultureClockInStatMapper; +import jnpf.culture.service.CultureStatService; +import jnpf.entity.culture.CultureClockIn; +import jnpf.entity.culture.CultureClockInStat; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.model.culture.dto.StatQueryDto; +import jnpf.model.culture.vo.CultureStatVo; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterReq; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.*; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 文化打卡 - 统计 服务实现 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@Service +public class CultureStatServiceImpl implements CultureStatService { + + @Resource + private CultureClockInStatMapper cultureClockInStatMapper; + @Resource + private CultureClockInMapper cultureClockInMapper; + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + @Autowired + private V2OrganizeApi v2OrganizeApi; + @Autowired + private FtbPermissionOrganizeService ftbPermissionOrganizeService; + @Autowired + private FtbPermissionUsersService ftbPermissionUsersService; + + @Override + public PageInfo getStatPage(StatQueryDto queryDto) { + + List orgIds = getOrgAndChildren(queryDto.getOrgIds()); + List data = null; + List userIdList = new ArrayList<>(); + // 查询用户权限 + List list = ftbPermissionUsersService.authGetAllUserInfoBatch(); + if (null == list || list.isEmpty()) { + PageInfo page = new PageInfo<>(); + page.setPageSize(queryDto.getPageSize()); + page.setPageNum(queryDto.getCurrentPage()); + return page; + } + List authUserIds = list.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (StringUtil.isNotEmpty(queryDto.getUserName()) || (null != orgIds && !orgIds.isEmpty())) { + StaffRosterReq req = new StaffRosterReq(); + req.setKeyWords(queryDto.getUserName()); + req.setOrgIds(orgIds); + req.setUserIds(authUserIds); + data = staffRosterService.queryWithUserIdsPost(req); + if (null == data || data.isEmpty()) { + PageInfo page = new PageInfo<>(); + page.setPageSize(queryDto.getPageSize()); + page.setPageNum(queryDto.getCurrentPage()); + return page; + } + List collect = data.stream().map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + userIdList.addAll(collect); + } else { + userIdList.addAll(authUserIds); + } + // 查询每个员工的最近打卡 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(cultureClockInMapper.getClockInList(queryDto.getStartDate(), queryDto.getEndDate(), userIdList)); + if (page.getList().isEmpty()) { + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setPageSize(queryDto.getPageSize()); + pageInfo.setPageNum(queryDto.getCurrentPage()); + return pageInfo; + } + // 转换分页对象 + PageInfo returnPage = new PageInfo<>(); + List returnList = new ArrayList<>(); + page.getList().forEach(v -> returnList.add(new CultureStatVo(v.getUserId(), v.getClockInTime()))); + returnPage.setList(returnList); + returnPage.setTotal(page.getTotal()); + returnPage.setPageNum(page.getPageNum()); + returnPage.setPageSize(page.getPageSize()); + // 填充用户信息 + List userIds = page.getList().stream().map(CultureClockIn::getUserId).collect(Collectors.toList()); + if (data == null) { + StaffRosterReq req = new StaffRosterReq(); + req.setUserIds(userIds); + data = staffRosterService.queryWithUserIdsPost(req); + } + if (null != data && !data.isEmpty()) { + Map userMap = data.stream().collect(Collectors.toMap(FtbPersonnelsStaffRosterDto::getUserId, Function.identity())); + returnPage.getList().forEach(v -> { + FtbPersonnelsStaffRosterDto user = userMap.get(v.getUserId()); + if (null != user) { + v.setUserName(user.getName()); + v.setOrgName(user.getCurrOrgName()); + if (user.getEnabledMark() == ConstantUtil.NUM_TRUE) { + v.setUserName(v.getUserName() + "(已删除)"); + } else { + if (user.getWorkerStatus().equals(StatisticsEnumUtil.WorkStatusEnum.LZ.getCode())) { + v.setUserName(v.getUserName() + "(已离职)"); + } + } + } + }); + } + // 填充统计信息 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(CultureClockInStat::getUserId, userIds) + .eq(CultureClockInStat::getDeleteMark, ConstantUtil.NUM_FALSE); + List statList = cultureClockInStatMapper.selectList(queryWrapper); + Map map = statList.stream().collect(Collectors.toMap(CultureClockInStat::getUserId, Function.identity())); + returnPage.getList().forEach(v -> { + CultureClockInStat stat = map.get(v.getUserId()); + if (null == stat) { + v.setTotalDays(0); + v.setMaxContinuousDays(0); + v.setMaxMissDays(0); + } else { + v.setTotalDays(stat.getTotalDays()); + v.setMaxContinuousDays(stat.getMaxContinuousDays()); + int realMaxMissDays = stat.getMaxMissDays(); + LocalDate yesterday = LocalDate.now().minusDays(1); + if (stat.getLastClockInDate() != null && !stat.getLastClockInDate().equals(yesterday)) { + int currentMissDays = DateDetail.calculateDayDiff(stat.getLastClockInDate(), LocalDate.now()) - 1; + realMaxMissDays = Math.max(currentMissDays, realMaxMissDays); + } + v.setMaxMissDays(realMaxMissDays); + } + }); + return returnPage; + } + + private List getOrgAndChildren(List orgIds) { + + // 查询用户权限 + List orgList = ftbPermissionOrganizeService.authOrganizesByUserBound(List.of(), null, false, false); + if (null == orgList || orgList.isEmpty()) { + return null; + } + // 用户所有可查看的组织 + List orgIdList = orgList.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + List intersection = getAuthOrgList(orgIdList, orgIds); + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(intersection, true, UserProvider.getUser().getTenantId()); + if (null != listActionResult && 200 == listActionResult.getCode()) { + List collect = listActionResult.getData().stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + return getAuthOrgList(orgIdList, collect); + } + return null; + } + + private List getAuthOrgList(List authList, List orgIds) { + + List intersection; + if (null != orgIds && !orgIds.isEmpty()) { + Set setB = new HashSet<>(orgIds); + intersection = authList.stream().filter(setB::contains).collect(Collectors.toList()); + if (intersection.isEmpty()) { + return null; + } + } else { + intersection = authList; + } + return intersection; + } + + @Override + public Workbook getStatWorkbook(StatQueryDto queryDto) { + + queryDto.setCurrentPage(1); + queryDto.setPageSize(999999); + PageInfo statPage = getStatPage(queryDto); + // 生成工作簿 + TemplateWorkSheet workSheet = new TemplateWorkSheet(); + workSheet.setSheetName(ConstantUtil.CULTURE_CLOCK_IN_FORM); + // 生成表头 + workSheet.setRowNames(ConstantUtil.TITLE_CULTURE_CLOCK_IN_TABLE); + workSheet.setTitleNum(1); + int len = workSheet.getRowNames().length; + // 生成数据 + List dataList = new ArrayList<>(); + if (!statPage.getList().isEmpty()) { + for (int i = 0; i < statPage.getList().size(); i++) { + Object[] array = new Object[len]; + CultureStatVo stat = statPage.getList().get(i); + for (int j = 0; j < len; j++) { + switch (j) { + case 0: + array[j] = stat.getUserName(); + break; + case 1: + array[j] = stat.getOrgName(); + break; + case 2: + array[j] = null == stat.getLastClockInTime() ? null : DateDetail.getDate2Str(stat.getLastClockInTime(), DateDetail.DF4); + break; + case 3: + array[j] = stat.getTotalDays(); + break; + case 4: + array[j] = stat.getMaxContinuousDays(); + break; + case 5: + array[j] = stat.getMaxMissDays(); + break; + default: + break; + } + } + dataList.add(array); + } + } + workSheet.setDataList(dataList); + return TemplateExcelUtils.createWorkBook(List.of(workSheet), true); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureTextSettingServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureTextSettingServiceImpl.java new file mode 100644 index 0000000..9532175 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/service/impl/CultureTextSettingServiceImpl.java @@ -0,0 +1,331 @@ +package jnpf.culture.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.culture.mapper.CulturePicTempMapper; +import jnpf.culture.mapper.CultureTextSettingMapper; +import jnpf.culture.service.CultureTextSettingService; +import jnpf.entity.culture.CulturePicTemp; +import jnpf.entity.culture.CultureTextSetting; +import jnpf.enums.attendance.StatisticsEnumUtil; +import jnpf.exception.HandleException; +import jnpf.model.culture.dto.CultureTextSettingDto; +import jnpf.model.culture.dto.SettingQueryDto; +import jnpf.model.culture.vo.CultureStatVo; +import jnpf.model.culture.vo.CultureTextSettingVo; +import jnpf.model.culture.vo.UploadInfoVo; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.req.roster.StaffRosterReq; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.*; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * 文化打卡 - 文字配置 服务实现 + * + * @author yanwenfu + * @create 2025-12-23 + */ +@Service +public class CultureTextSettingServiceImpl extends SuperServiceImpl implements CultureTextSettingService { + + @Resource + private CultureTextSettingMapper cultureTextSettingMapper; + @Resource + private CulturePicTempMapper culturePicTempMapper; + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + private static final Pattern EMOJI_PATTERN = Pattern.compile( + "[\uD83C\uDF00-\uD83D\uDDFF]|" + + "[\uD83D\uDE00-\uD83D\uDE4F]|" + + "[\uD83D\uDE80-\uD83D\uDEFF]|" + + "[\u2600-\u26FF]|" + + "[\u2700-\u27BF]", + Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE + ); + + @Override + public PageInfo getCultureTextSettingList(SettingQueryDto queryDto) { + + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(cultureTextSettingMapper.getCultureTextSettingList(queryDto.getKeyword())); + if (page.getList().isEmpty()) { + page.setPageSize(queryDto.getPageSize()); + page.setPageNum(queryDto.getCurrentPage()); + return page; + } + // 设置人员信息 + List userIds = page.getList().stream().map(CultureTextSettingVo::getLastModifyUserId).distinct().collect(Collectors.toList()); + StaffRosterReq req = new StaffRosterReq(); + req.setUserIds(userIds); + List data = staffRosterService.queryWithUserIdsPost(req); + if (null != data && !data.isEmpty()) { + Map userMap = data.stream().collect(Collectors.toMap(FtbPersonnelsStaffRosterDto::getUserId, Function.identity())); + page.getList().forEach(v -> { + FtbPersonnelsStaffRosterDto user = userMap.get(v.getLastModifyUserId()); + if (null != user) { + v.setLastModifyUserName(user.getName()); + if (user.getEnabledMark() == ConstantUtil.NUM_TRUE) { + v.setLastModifyUserName(v.getLastModifyUserName() + "(已删除)"); + } else { + if (user.getWorkerStatus().equals(StatisticsEnumUtil.WorkStatusEnum.LZ.getCode())) { + v.setLastModifyUserName(v.getLastModifyUserName() + "(已离职)"); + } + } + } + }); + } + return page; + } + + @Override + public void addTextSetting(CultureTextSettingDto textSettingDto) throws Exception { + + // 判定是否超过最大配置数量 100 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(CultureTextSetting::getDeleteMark, ConstantUtil.NUM_FALSE); + Long total = cultureTextSettingMapper.selectCount(queryWrapper); + if (total.intValue() >= 100) { + throw new HandleException("您已超过最大配置数量"); + } + // 查询文案是否已经存在 判定方式为 标题 + 文字 是否重复 + int count = cultureTextSettingMapper.getDistinctCount(textSettingDto.getCultureTitle(), textSettingDto.getCultureContent(), null); + if (count > 0) { + throw new HandleException("新增内容已存在,无法重复添加"); + } + CultureTextSetting setting = new CultureTextSetting(); + setting.setId(FtbUtil.getId()); + setting.setCultureTitle(textSettingDto.getCultureTitle()); + setting.setCultureContent(textSettingDto.getCultureContent()); + setting.setDeleteMark(ConstantUtil.NUM_FALSE); + setting.setCreatorUserId(UserProvider.getLoginUserId()); + setting.setLastModifyUserId(UserProvider.getLoginUserId()); + cultureTextSettingMapper.insert(setting); + } + + @Override + public void updateTextSetting(String id, CultureTextSettingDto textSettingDto) throws Exception { + + CultureTextSetting setting = cultureTextSettingMapper.selectById(id); + if (null == setting) { + throw new HandleException("未找到配置"); + } + int count = cultureTextSettingMapper.getDistinctCount(textSettingDto.getCultureTitle(), textSettingDto.getCultureContent(), id); + if (count > 0) { + throw new HandleException("内容已存在,无法重复添加"); + } + setting.setCultureTitle(textSettingDto.getCultureTitle()); + setting.setCultureContent(textSettingDto.getCultureContent()); + setting.setLastModifyUserId(UserProvider.getLoginUserId()); + setting.setLastModifyTime(new Date()); + cultureTextSettingMapper.updateById(setting); + // 删除文字配置关联的临时图片 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(CulturePicTemp::getContentId, id); + culturePicTempMapper.delete(queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteTextSettingBatch(List ids) throws Exception { + + Long dbCount = getDbCount(); + if (dbCount.intValue() - ids.size() < 1) { + throw new HandleException("至少需保留一条文化内容,不可全部删除"); + } + // 删除文字配置 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .in(CultureTextSetting::getId, ids) + .set(CultureTextSetting::getDeleteMark, ConstantUtil.NUM_TRUE) + .set(CultureTextSetting::getDeleteUserId, UserProvider.getLoginUserId()) + .set(CultureTextSetting::getDeleteTime, new Date()); + cultureTextSettingMapper.update(null, updateWrapper); + // 删除文字配置关联的临时图片 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(CulturePicTemp::getContentId, ids); + culturePicTempMapper.delete(queryWrapper); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public UploadInfoVo textSettingImport(MultipartFile file) throws Exception { + + UploadInfoVo uploadInfo = new UploadInfoVo(); + XSSFWorkbook workbook; + try { + workbook = new XSSFWorkbook(file.getInputStream()); + } catch (Exception e) { + throw new HandleException("导入转换工作簿异常"); + } + // 获取数据 + XSSFSheet sheet = workbook.getSheetAt(0); + List dataFromSheet = TemplateExcelUtils.getDataFromSheetWithNullRow(sheet); + if (dataFromSheet.isEmpty() || dataFromSheet.size() == 1) { + throw new HandleException("未获取到工作簿数据"); + } + List list = new ArrayList<>(); + Set set = new HashSet<>(); + String[] row1 = dataFromSheet.get(0); + if (row1.length < 2 || !row1[0].equals("文化标题") || !row1[1].equals("文化内容")) { + throw new HandleException("当前模板非文化内容模板,请上传正确模板及内容"); + } + LocalDateTime now = LocalDateTime.now(); + for (int i = 1; i < dataFromSheet.size(); i++) { + List array = List.of(dataFromSheet.get(i)); + if (StringUtil.isEmpty(array.get(0)) && StringUtil.isEmpty(array.get(1))) { + continue; + } + CultureTextSetting setting = new CultureTextSetting(); + setting.setId(FtbUtil.getId()); + String titleAndContent = ""; + String errorMsg = ""; + for (int j = 0; j < array.size(); j++) { + String str = array.get(j); + if (j == 0) { + // 标题 + titleAndContent += str; + String msg = checkStr(i, j, str, 50, errorMsg); + if (StringUtil.isNotEmpty(msg)) { + errorMsg += msg; + } + } + if (j == 1) { + // 内容 + titleAndContent += "\u0001" + str; + String msg = checkStr(i, j, str, 200, errorMsg); + if (StringUtil.isNotEmpty(msg)) { + errorMsg += StringUtil.isNotEmpty(errorMsg) ? "," + msg : msg; + } + } + } + // 报错了 + if (StringUtil.isNotEmpty(errorMsg)) { + uploadInfo.getFailMsgList().add(errorMsg); + uploadInfo.setFailNum(uploadInfo.getFailNum() + 1); + continue; + } + if (StringUtil.isEmpty(titleAndContent)) { + continue; + } + // 说明内容未重复 + if (set.add(titleAndContent)) { + String[] split = titleAndContent.split("\u0001"); + setting.setIndex(i); + setting.setCultureTitle(split[0]); + setting.setCultureContent(split[1]); + setting.setDeleteMark(ConstantUtil.NUM_FALSE); + setting.setCreatorUserId(UserProvider.getLoginUserId()); + now = now.plusSeconds(1); + setting.setCreatorTime(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())); + setting.setLastModifyUserId(UserProvider.getLoginUserId()); + setting.setLastModifyTime(setting.getCreatorTime()); + list.add(setting); + } else { + uploadInfo.getFailMsgList().add("第" + (i + 1) + "行 文化标题文本信息已存在"); + uploadInfo.setFailNum(uploadInfo.getFailNum() + 1); + } + } + if (!list.isEmpty()) { + // 判断list size + db size是否大于100 舍弃大于100的 + Long dbCount = getDbCount(); + // 还能导入的条数 + int insertCount = 100 - dbCount.intValue(); + if (insertCount <= 0) { + // 数据库配置数已满, 无法导入 + // -1 是因为第一条是表头 + uploadInfo.setFailNum(uploadInfo.getFailNum() + list.size()); + uploadInfo.getFailMsgList().add("无可导入的内容,已超过文字配置最大条数(100)限制"); + return uploadInfo; + } + // 判断list中有没有与数据库重复的 + List exceptIndex = cultureTextSettingMapper.getDistinctCountBatch(list); + // 去除重复的 + if (!exceptIndex.isEmpty()) { + list.removeIf(v -> exceptIndex.contains(v.getIndex())); + uploadInfo.getFailMsgList().add("第" + exceptIndex.stream().sorted().map(v -> String.valueOf(v + 1)).collect(Collectors.joining(",")) + "行 无法导入,文化内容已存在"); + uploadInfo.setFailNum(uploadInfo.getFailNum() + exceptIndex.size()); + } + // 不可导入的条数 + int overflowCount = insertCount - list.size(); + if (overflowCount < 0) { + overflowCount = Math.abs(overflowCount); + uploadInfo.setFailNum(uploadInfo.getFailNum() + overflowCount); + // 被截掉的部分,取最后 exceptCount 条 + List removedIndexes = list.subList(list.size() - overflowCount, list.size()) + .stream() + .map(CultureTextSetting::getIndex) + .collect(Collectors.toList()); + list = new ArrayList<>(list.subList(0, list.size() - overflowCount)); + uploadInfo.getFailMsgList().add("第" + removedIndexes.stream().sorted().map(v -> String.valueOf(v + 1)).collect(Collectors.joining(",")) + "行无法导入,已超过文字配置最大条数(100)限制"); + } + this.saveBatch(list); + uploadInfo.setSuccessNum(list.size()); + } + return uploadInfo; + } + + @Override + public Integer getCultureTextSettingCount() { + + return getDbCount().intValue(); + } + + private Long getDbCount() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(CultureTextSetting::getDeleteMark, ConstantUtil.NUM_FALSE); + return cultureTextSettingMapper.selectCount(queryWrapper); + } + + /** + * 检查内容是否符合规范 + * @param row 行 + * @param col 列(0: 标题, 1: 内容) + * @param str 内容 + * @param wordLimit 允许字数 + * @return java.lang.String + */ + private String checkStr(int row, int col, String str, int wordLimit, String errorMsg) { + + String msg = ""; + // 判断内容是否为空 + if (StringUtil.isEmpty(str)) { + msg += col == 0 ? "无文化标题" : "无文化内容 "; + } + // 判断字数是否超过限制 + int realLength = str.codePointCount(0, str.length()); + if (realLength > wordLimit) { + msg += col == 0 ? "文化标题超过字数(" + wordLimit + ")上限" : "文化内容超过字数(" + wordLimit + ")上限 "; + } + // 判断是否包含emoji表情 + if (containsEmoji(str)) { + msg += col == 0 ? "文化标题中包含emoji表情" : "文化内容中包含emoji表情 "; + } + if (StringUtil.isEmpty(errorMsg) && StringUtil.isNotEmpty(msg)) { + msg = "第" + (row + 1) + "行 " + msg; + } + return msg; + } + + public boolean containsEmoji(String text) { + if (text == null) { + return false; + } + return EMOJI_PATTERN.matcher(text).find(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/DateRangeUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/DateRangeUtil.java new file mode 100644 index 0000000..6a273f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/DateRangeUtil.java @@ -0,0 +1,55 @@ +package jnpf.culture.util; + +import org.apache.commons.lang3.tuple.MutablePair; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAdjusters; + +/** + * 日期工具 + * + * @author yanwenfu + * @create 2026-03-23 + */ +public class DateRangeUtil { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public static final String QUERY_DAY = "day"; + public static final String QUERY_WEEK = "week"; + public static final String QUERY_MONTH = "month"; + + /** + * @param dateStr 日期字符串 yyyy-MM-dd + * @param queryType day / week / month + * @return MutablePair<开始日期, 结束日期> + */ + public static MutablePair getRange(String dateStr, String queryType) { + + LocalDate date = LocalDate.parse(dateStr, FORMATTER); + switch (queryType.toLowerCase()) { + + case QUERY_DAY: + return MutablePair.of(dateStr, dateStr); + case QUERY_WEEK: + // 周一为一周开始,周日为结束 + LocalDate weekStart = date.with(DayOfWeek.MONDAY); + LocalDate weekEnd = date.with(DayOfWeek.SUNDAY); + return MutablePair.of( + weekStart.format(FORMATTER), + weekEnd.format(FORMATTER) + ); + case QUERY_MONTH: + LocalDate monthStart = date.with(TemporalAdjusters.firstDayOfMonth()); + LocalDate monthEnd = date.with(TemporalAdjusters.lastDayOfMonth()); + return MutablePair.of( + monthStart.format(FORMATTER), + monthEnd.format(FORMATTER) + ); + default: + throw new IllegalArgumentException("queryType 只能是 day/week/month"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/FontUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/FontUtil.java new file mode 100644 index 0000000..27de25c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/FontUtil.java @@ -0,0 +1,50 @@ +package jnpf.culture.util; + +import java.awt.*; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 字体缓存工具类 + * + * @author yanwenfu + * @create 2025-12-31 + */ +public class FontUtil { + + // 缓存字体: key = 路径 + size + private static final Map FONT_CACHE = new ConcurrentHashMap<>(); + + /** + * 加载字体并缓存 + * @param path 字体文件路径,相对于 classpath,例如 "/fonts/MiSans-Semibold.ttf" + * @param size 字体大小(pt) + * @return Font 对象 + */ + public static Font loadFont(String path, float size) { + String cacheKey = path + "_" + size; + if (FONT_CACHE.containsKey(cacheKey)) { + return FONT_CACHE.get(cacheKey); + } + + try (InputStream is = FontUtil.class.getResourceAsStream(path)) { + if (is == null) { + throw new RuntimeException("字体文件未找到: " + path); + } + Font baseFont = Font.createFont(Font.TRUETYPE_FONT, is); + Font font = baseFont.deriveFont(Font.PLAIN, size); + FONT_CACHE.put(cacheKey, font); + return font; + } catch (Exception e) { + throw new RuntimeException("加载字体失败: " + path, e); + } + } + + /** + * 清空字体缓存(可选) + */ + public static void clearCache() { + FONT_CACHE.clear(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/ImageComboUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/ImageComboUtil.java new file mode 100644 index 0000000..f425b1b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/culture/util/ImageComboUtil.java @@ -0,0 +1,452 @@ +package jnpf.culture.util; + +import cn.xuyanwu.spring.file.storage.MockMultipartFile; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 图片组合工具类 + * + * @author yanwenfu + * @create 2025-12-26 + */ +@Slf4j +@Component +public class ImageComboUtil { + + @Autowired + private RedisUtil redisUtil; + @Autowired + private RedissonClient redissonClient; + + public final int MAX_ROUND_SIZE = 33; + public final String COMBO_POOL_KEY = "culture:combo:pool:"; + public final String PIC_USED_KEY = "culture:pic:used:"; + public final String TEXT_USED_KEY = "culture:text:used:"; + public final String TYPE_PIC = "pic"; + public final String TYPE_TEXT = "text"; + + public boolean checkNeedGenerate() { + return redisUtil.getListSize(getRedisKeyWithTenant(COMBO_POOL_KEY)) == 0L; + } + + public String getRedisKeyWithTenant(String key) { + return key + UserProvider.getUser().getTenantId(); + } + + public void generateComboPool(List allPicIds, List allTextIds, List delPicIds, List delTextIds) throws Exception { + + if (allPicIds.isEmpty() || allTextIds.isEmpty()) { + return; + } + String lockKey = "combo:pool:generate:lock"; + // 尝试加锁 + RLock lock = redissonClient.getLock(lockKey); + boolean locked = false; + try { + // 显式 leaseTime + locked = lock.tryLock(5, 30, TimeUnit.SECONDS); + if (!locked) { + return; + } + // 二次检查:奖池已有数据,直接返回 + if (redisUtil.getListSize(getRedisKeyWithTenant(COMBO_POOL_KEY)) > 0) { + return; + } + // 1. Redis 中已参与过的 + Set usedPics = redisUtil.getSet(getRedisKeyWithTenant(PIC_USED_KEY)); + Set usedTexts = redisUtil.getSet(getRedisKeyWithTenant(TEXT_USED_KEY)); + if (usedPics == null) usedPics = new HashSet<>(); + usedPics.removeIf(v -> delPicIds.contains(v.toString())); + if (usedTexts == null) usedTexts = new HashSet<>(); + usedTexts.removeIf(v -> delTextIds.contains(v.toString())); + // 2. 本轮参与数量 + int picLimit = Math.min(MAX_ROUND_SIZE, allPicIds.size()); + int textLimit = Math.min(MAX_ROUND_SIZE, allTextIds.size()); + // 3. 选参与素材 + List roundPics = pickIds(TYPE_PIC, allPicIds, usedPics, picLimit); + List roundTexts = pickIds(TYPE_TEXT, allTextIds, usedTexts, textLimit); + // 4. 生成 combo + List combos = new ArrayList<>(roundPics.size() * roundTexts.size()); + for (String picId : roundPics) { + for (String textId : roundTexts) { + combos.add(picId + "_" + textId); + } + } + Collections.shuffle(combos); + // 5. 写 Redis(先清再写,防并发残留) + redisUtil.remove(getRedisKeyWithTenant(COMBO_POOL_KEY)); + redisUtil.insert(getRedisKeyWithTenant(COMBO_POOL_KEY), combos); + // 6. 标记“参与过生成” + redisUtil.insert(getRedisKeyWithTenant(PIC_USED_KEY), new HashSet<>(roundPics)); + redisUtil.insert(getRedisKeyWithTenant(TEXT_USED_KEY), new HashSet<>(roundTexts)); + } finally { + if (locked && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + /** + * 获取指定数量的id(未使用优先) + * @param pickType 类型(图片, 文案) + * @param allIds 所有的id + * @param usedIds 使用过的id + * @param limit 最大取值 + * @return java.util.List + */ + private List pickIds(String pickType, List allIds, Set usedIds, int limit) { + + if (allIds.isEmpty() || limit <= 0) { + return new ArrayList<>(); + } + Set usedIdSet = usedIds.stream().map(String::valueOf).collect(Collectors.toSet()); + // unused = allIds - usedIds + List unused = allIds.stream().filter(id -> !usedIdSet.contains(id)).collect(Collectors.toList()); + Collections.shuffle(unused); + // 用 LinkedHashSet 保证唯一和顺序 + Set resultSet = new LinkedHashSet<>(); + // 优先添加 unused + for (String id : unused) { + resultSet.add(id); + if (resultSet.size() >= limit) { + break; + } + } + // 够,用 usedIds 补充 + if (resultSet.size() < limit && !usedIdSet.isEmpty()) { + for (String id : usedIdSet) { + // LinkedHashSet 自动去重 + resultSet.add(id); + if (resultSet.size() >= limit) { + break; + } + } + } + if (unused.size() < limit) { + redisUtil.remove(TYPE_PIC.equals(pickType) ? getRedisKeyWithTenant(PIC_USED_KEY) : getRedisKeyWithTenant(TEXT_USED_KEY)); + } + // 转回 List 返回 + List finalList = new ArrayList<>(resultSet); + Collections.shuffle(finalList); + return finalList; + } + + public String getRandomCombo(String lastCombo, String userId, List delPicIds, List delTextIds) { + + String todayKey = "culture:combo:" + userId + "_used:" + LocalDate.now(); + String secondBest = null; + String fallback = null; + for (int i = 0; i < 3; i++) { + Object obj = redisUtil.lPop(getRedisKeyWithTenant(COMBO_POOL_KEY)); + if (obj == null) { + break; + } + String combo = obj.toString(); + // 判断combo中图片或文字有没有被删除 + if (!checkComboCouldUse(combo, delPicIds, delTextIds)) { + i -= 1; + continue; + } + // 最优 + if (!combo.equals(lastCombo) && !redisUtil.sIsMember(todayKey, combo)) { + return combo; + } + // 次优:今天用过,但不等于 last + if (!combo.equals(lastCombo) && secondBest == null) { + secondBest = combo; + } + // 兜底 + if (fallback == null) { + fallback = combo; + } + redisUtil.rPush(getRedisKeyWithTenant(COMBO_POOL_KEY), combo); + } + if (secondBest != null) { + return secondBest; + } + return fallback; + } + + private boolean checkComboCouldUse(String combo, List delPicIds, List delTextIds) { + + int idx = combo.indexOf('_'); + if (idx <= 0 || idx >= combo.length() - 1) { + // 格式非法:没有 _ 或在首尾 + return false; + } + String picId = combo.substring(0, idx); + String contentId = combo.substring(idx + 1); + // 命中已删除 pic + if (delPicIds != null && delPicIds.contains(picId)) { + return false; + } + // 命中已删除 text + if (delTextIds != null && delTextIds.contains(contentId)) { + return false; + } + return true; + } + + /** + * 生成基础图片 + * @param picUrl 背景图 + * @param companyIconUrl 企业logo + * @param title 标题 + * @param content 内容 + * @return java.awt.image.BufferedImage + */ + public BufferedImage generateBaseImage(String picUrl, String companyIconUrl, String title, String content) throws Exception { + + final int CANVAS_WIDTH = 900; + final int CANVAS_HEIGHT = 1600; + // 读取背景图 + log.error("图片地址:{}", picUrl); + BufferedImage bgImage = ImageIO.read(new URL(picUrl)); + // 固定画布 900 * 1600 + BufferedImage combined = new BufferedImage(CANVAS_WIDTH, CANVAS_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = combined.createGraphics(); + // 抗锯齿 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + // 计算 cover 缩放比例 + double scale = Math.max( + (double) CANVAS_WIDTH / bgImage.getWidth(), + (double) CANVAS_HEIGHT / bgImage.getHeight() + ); + int drawWidth = (int) Math.round(bgImage.getWidth() * scale); + int drawHeight = (int) Math.round(bgImage.getHeight() * scale); + // 居中裁剪 + int x = (CANVAS_WIDTH - drawWidth) / 2; + int y = (CANVAS_HEIGHT - drawHeight) / 2; + // 画背景(铺满) + g2d.drawImage(bgImage, x, y, drawWidth, drawHeight, null); + // 画黑色透明遮罩 50%透明度 + Composite oldComposite = g2d.getComposite(); + g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)); + g2d.setColor(Color.BLACK); + g2d.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + // 恢复原始透明度 + g2d.setComposite(oldComposite); + int margin = 40; + int marginY = 48; + // 绘制企业 logo + if (companyIconUrl != null && !companyIconUrl.isEmpty()) { + BufferedImage logo = ImageIO.read(new URL(companyIconUrl)); + int logoHeight = 120; + int logoWidth = logo.getWidth() * logoHeight / logo.getHeight(); + g2d.drawImage(logo, margin, marginY, logoWidth, logoHeight, null); + } + // 绘制标题 + int titleY = 250; + g2d.setColor(Color.WHITE); + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Semibold.ttf", 50f)); + int titleEndY = drawStringMultiLine(g2d, title, margin, titleY, CANVAS_WIDTH - 2 * margin, 60, 5); + // 分割线 + int lineY = titleEndY + 40; + g2d.fillRect(margin, lineY, CANVAS_WIDTH - 2 * margin, 5); + // 内容 + int contentY = lineY + 60; + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Normal.ttf", 35f)); + drawStringMultiLine(g2d, content, margin, contentY, CANVAS_WIDTH - 2 * margin, 45, 2); + g2d.dispose(); + return combined; + } + + /** + * 绘制多行文本,自动换行 + */ + private int drawStringMultiLine(Graphics2D g2d, String text, int x, int y, int maxWidth, int lineHeight, int charSpacing) { + FontMetrics fm = g2d.getFontMetrics(); + int curX = x; + int curY = y; + for (int i = 0; i < text.length(); i++) { + String ch = String.valueOf(text.charAt(i)); + int charWidth = fm.stringWidth(ch); + // 自动换行 + if (curX + charWidth > x + maxWidth) { + curX = x; + curY += lineHeight; + } + g2d.drawString(ch, curX, curY); + curX += charWidth + charSpacing; + } + return curY; + } + + /** + * 生成最终图片 + * @param tempUrl 基础图片 + * @param userIconUrl 用户头像 + * @param userName 用户名称 + * @param days 累计签到天数 + * @return java.awt.image.BufferedImage + */ + public BufferedImage generateFinalImage(String tempUrl, String userIconUrl, String userName, String days) throws Exception { + + // 读取基础图片 + BufferedImage baseImage = ImageIO.read(new URL(tempUrl)); + int width = baseImage.getWidth(); + int height = baseImage.getHeight(); + BufferedImage combined = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = combined.createGraphics(); + // 高质量绘制 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + // 先画基础图 + g2d.drawImage(baseImage, 0, 0, null); + // ===== 下半部分布局参数 ===== + int avatarSize = 120; + // 圆角程度(方形带点圆角) + int avatarRadius = 32; + int marginLeft = 40; + int footerTopY = height - 160; + // ===== 用户头像(圆角裁剪)===== + if (userIconUrl != null && !userIconUrl.isEmpty()) { + BufferedImage avatar = ImageIO.read(new URL(userIconUrl)); + BufferedImage avatarClip = new BufferedImage(avatarSize, avatarSize, BufferedImage.TYPE_INT_ARGB); + Graphics2D ag = avatarClip.createGraphics(); + ag.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + // ===== 1px 白色边框 ===== + ag.setColor(Color.WHITE); + ag.setStroke(new BasicStroke(1f)); + RoundRectangle2D border = new RoundRectangle2D.Float( + 0.5f, 0.5f, + avatarSize - 1f, avatarSize - 1f, + avatarRadius, avatarRadius + ); + ag.draw(border); + // ===== 裁剪区域(向内缩 1px,避免盖住边框)===== + RoundRectangle2D clip = new RoundRectangle2D.Float( + 1f, 1f, + avatarSize - 2f, avatarSize - 2f, + avatarRadius - 1f, avatarRadius - 1f + ); + ag.setClip(clip); + // cover 填充头像 + double scale = Math.max( + (double) (avatarSize - 2) / avatar.getWidth(), + (double) (avatarSize - 2) / avatar.getHeight() + ); + int drawW = (int) (avatar.getWidth() * scale); + int drawH = (int) (avatar.getHeight() * scale); + int dx = (avatarSize - drawW) / 2; + int dy = (avatarSize - drawH) / 2; + ag.drawImage(avatar, dx, dy, drawW, drawH, null); + ag.dispose(); + g2d.drawImage(avatarClip, marginLeft, footerTopY, null); + } + // ===== 用户名称 ===== + int textStartX = marginLeft + avatarSize + 30; + int nameY = footerTopY + 45; + g2d.setColor(Color.WHITE); + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Semibold.ttf", 36f)); + g2d.drawString(userName, textStartX, nameY); + // ===== 累计签到天数 ===== + int countY = nameY + 70; + String prefix = "已累计打卡"; + String suffix = "天"; + // 前缀 + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Normal.ttf", 30f)); + FontMetrics fmPrefix = g2d.getFontMetrics(); + g2d.drawString(prefix, textStartX, countY); + int xOffset = textStartX + fmPrefix.stringWidth(prefix) + 6; + // 数字(放大) + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Semibold.ttf", 48f)); + FontMetrics fmDays = g2d.getFontMetrics(); + g2d.drawString(days, xOffset, countY); + xOffset += fmDays.stringWidth(days) + 6; + // 后缀 + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Normal.ttf", 30f)); + g2d.drawString(suffix, xOffset, countY); + countY -= 10; + // 时间图戳 + BufferedImage bottomIcon; + try (InputStream is = getClass() + .getClassLoader() + .getResourceAsStream("img/icon-bottom.png")) { + assert is != null; + bottomIcon = ImageIO.read(is); + } + // 缩放 + int newWidth = bottomIcon.getWidth() / 10 * 7; + int newHeight = bottomIcon.getHeight() / 10 * 7; + Image scaledIcon = bottomIcon.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); + // 计算绘制位置 + int x = width - newWidth; + int y = height - newHeight; + // 绘制缩放后的图 + g2d.drawImage(scaledIcon, x, y, null); + + LocalDateTime now = LocalDateTime.now(); + int year = now.getYear(); + int month = now.getMonth().getValue(); + int day = now.getDayOfMonth(); + int hour = now.getHour(); + int minute = now.getMinute(); + String topLText = String.format("%02d", day); + String topRText = "日"; + String midText = year + "/" + String.format("%02d", month); + String bottomText = String.format("%02d", hour) + ":" + String.format("%02d", minute); + + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Semibold.ttf", 66f)); + g2d.setColor(Color.WHITE); + int textX = width - 252; + countY = countY - 125; + g2d.drawString(topLText, textX, countY); + + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Normal.ttf", 34f)); + g2d.setColor(new Color(255, 255, 255, 180)); + textX = textX + 90; + g2d.drawString(topRText, textX, countY); + + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Normal.ttf", 38f)); + g2d.setColor(Color.WHITE); + textX = textX - 103; + countY = countY + 55; + g2d.drawString(midText, textX, countY); + + g2d.setFont(FontUtil.loadFont("/fonts/MiSans-Normal.ttf", 34f)); + g2d.setColor(new Color(255, 255, 255, 180)); + textX = textX + 28; + countY = countY + 55; + g2d.drawString(bottomText, textX, countY); + g2d.dispose(); + return combined; + } + + public MultipartFile bufferedImageToMultipartFile(BufferedImage image) throws Exception { + + String suffix = "png"; + String fileName = UUID.randomUUID() + "." + suffix; + // 自动关闭流 + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + ImageIO.write(image, suffix, os); + byte[] bytes = os.toByteArray(); + return new MockMultipartFile(fileName, fileName, "image/" + suffix, bytes); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DataReportController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DataReportController.java new file mode 100644 index 0000000..747b643 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DataReportController.java @@ -0,0 +1,32 @@ +package jnpf.doclibrary.controller; + +import cn.hutool.json.JSONObject; +import jnpf.base.ActionResult; +import jnpf.doclibrary.service.DataReportService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 数据报表控制器 + * + * @author yanwenfu + * @create 2024-07-19 + */ +@RestController +@RequestMapping(value = "/dataReport") +public class DataReportController { + + @Resource + private DataReportService dataReportService; + + @GetMapping(value = "/url/config") + public Object getDataReportUrlConfig() { + + JSONObject json = dataReportService.getDataReportUrlConfig(); + return ActionResult.success(json); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DocLibraryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DocLibraryController.java new file mode 100644 index 0000000..f5fe0ab --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/DocLibraryController.java @@ -0,0 +1,315 @@ +package jnpf.doclibrary.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PaginationVO; +import jnpf.doclibrary.service.DocLibraryService; +import jnpf.model.doclibrary.dto.FileQueryDto; +import jnpf.model.doclibrary.dto.SpaceDto; +import jnpf.model.doclibrary.vo.InformationAuthorityVo; +import jnpf.model.doclibrary.vo.InformationVo; +import jnpf.util.UserProvider; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 资料库控制器 + * + * @author yanwenfu + * @create 2023-07-20 + */ +@RestController +@RequestMapping(value = "/doclibrary") +public class DocLibraryController { + + @Resource + private DocLibraryService docLibraryService; + @Resource + private UserProvider userProvider; + + /** + * 获取-与我相关的文件列表 + * @param fileQueryDto 搜索参数 + * @return 返回值 + */ + @GetMapping("/relatedToMeFile") + public ActionResult relatedToMeFile(FileQueryDto fileQueryDto) { + fileQueryDto.setUserId(userProvider.get().getUserId()); + List vo = docLibraryService.relatedToMeFile(fileQueryDto); + return ActionResult.success(vo); + } + + /** + * 获取-与我相关的文件分页列表 + * @param fileQueryDto 搜索参数 + * @return 返回值 + */ + @GetMapping("/relatedToMeFile/page") + public ActionResult relatedToMeFilePage(FileQueryDto fileQueryDto) { + fileQueryDto.setUserId(userProvider.get().getUserId()); + PageInfo page = docLibraryService.relatedToMeFilePage(fileQueryDto); + PaginationVO paginationVO = new PaginationVO(); + paginationVO.setTotal((int) page.getTotal()); + paginationVO.setPageSize((long) page.getPageSize()); + paginationVO.setCurrentPage((long) page.getPageNum()); + return ActionResult.page(page.getList(), paginationVO); + } + + /** + * 获取--最近查看 + * @param fileQueryDto 搜索参数 + * @return 返回值 + */ + @GetMapping("/recentlyViewed") + public ActionResult recentlyViewed(FileQueryDto fileQueryDto) { + fileQueryDto.setUserId(userProvider.get().getUserId()); + List vo = docLibraryService.recentlyViewed(fileQueryDto); + return ActionResult.success(vo); + } + + /** + * 获取--最近查看分页 + * @param fileQueryDto 搜索参数 + * @return 返回值 + */ + @GetMapping("/recentlyViewed/page") + public ActionResult recentlyViewedPage(FileQueryDto fileQueryDto) { + fileQueryDto.setUserId(userProvider.get().getUserId()); + PageInfo page = docLibraryService.recentlyViewedPage(fileQueryDto); + PaginationVO paginationVO = new PaginationVO(); + paginationVO.setTotal((int) page.getTotal()); + paginationVO.setPageSize((long) page.getPageSize()); + paginationVO.setCurrentPage((long) page.getPageNum()); + return ActionResult.page(page.getList(), paginationVO); + } + /** + * 重命名 + * @param id 空间/文件夹/文件id + * @param fileQueryDto 修改后参数 + * @return 返回值 + */ + @PutMapping("/rename/{id}") + public ActionResult rename(@PathVariable("id" ) String id,@RequestBody FileQueryDto fileQueryDto) { + UserInfo userInfo = userProvider.get(); + fileQueryDto.setUserId(userInfo.getUserId()); + fileQueryDto.setUserName(userInfo.getUserName()); + int i = docLibraryService.rename(id,fileQueryDto); + if (i == -1){ + return ActionResult.fail("您没有权限!"); + } + return ActionResult.success(); + } + + /** + * 获取空间/文件夹/文件的权限 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + @GetMapping("/userPermissions/{id}") + public ActionResult userPermissions(@PathVariable("id" ) String id) { + int i = docLibraryService.userPermissions(id); + if (i == -1){ + return ActionResult.fail("您没有权限!"); + } + return ActionResult.success(i); + } + + /** + * 删除空间/文件夹/文件 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + @DeleteMapping("/deleteInformation/{id}") + public ActionResult deleteInformation(@PathVariable("id" ) String id) { + int i = docLibraryService.deleteInformation(id); + if (i == -1){ + return ActionResult.fail("您没有权限!"); + } + return ActionResult.success(); + } + + + /** + * 空间/文件夹/文件下成员列表 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + @GetMapping("/userList/{id}") + public ActionResult userList(@PathVariable("id" ) String id) { + //id如果是空间id 那么只用验证是不是空间的成员 + if(!docLibraryService.checkSpaceMember(id)){ + int i = docLibraryService.userPermissions(id); + if (i < 0 || i >= 2){ + return ActionResult.fail("您没有权限!"); + } + } + return ActionResult.success(docLibraryService.userList(id)); + } + + /** + * 转移所有权 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + @PutMapping("/transferOwnership/{id}") + public ActionResult transferOwnership(@PathVariable("id" ) String id,@RequestBody FileQueryDto fileQueryDto) { + int i = docLibraryService.userPermissions(id); + if (i != 0 ){ + return ActionResult.fail("您没有权限!"); + } + docLibraryService.transferOwnership(id,fileQueryDto); + return ActionResult.success(); + } + + /** + * 成员权限编辑 + * @param id 空间/文件夹/文件id + * @param informationAuthorityVo 权限信息 + * @return 返回值 + */ + @PutMapping("/editPermissions/{id}") + public ActionResult editPermissions(@PathVariable("id" ) String id,@RequestBody InformationAuthorityVo informationAuthorityVo) { + int i = docLibraryService.userPermissions(id); + if (i != 0 && i != 1){ + return ActionResult.fail("您没有权限!"); + } + if(null == informationAuthorityVo.getLimitsOfAuthority() || informationAuthorityVo.getLimitsOfAuthority() < 1 || informationAuthorityVo.getLimitsOfAuthority() >5){ + return ActionResult.fail("非法参数!"); + } + docLibraryService.editPermissions(id,informationAuthorityVo); + return ActionResult.success(); + } + + /** + * 批量成员权限编辑 + * @param id 空间/文件夹/文件id + * @param fileQueryDto 权限信息 + * @return 返回值 + */ + @PutMapping("/editPermissionsBatch/{id}") + public ActionResult editPermissionsBatch(@PathVariable("id" ) String id,@RequestBody FileQueryDto fileQueryDto) { + int i = docLibraryService.userPermissions(id); + if (i != 0 && i != 1){ + return ActionResult.fail("您没有权限!"); + } + if (null != fileQueryDto.getInformationAuthorityVos() && fileQueryDto.getInformationAuthorityVos().size() >0) { + for (InformationAuthorityVo informationAuthorityVo : fileQueryDto.getInformationAuthorityVos()) { + if (null == informationAuthorityVo.getLimitsOfAuthority() || informationAuthorityVo.getLimitsOfAuthority() < 1 || informationAuthorityVo.getLimitsOfAuthority() > 5) { + return ActionResult.fail("非法参数!"); + } + } + docLibraryService.editPermissionsBatch(id, fileQueryDto); + } + return ActionResult.success(); + } + /** + * 移除成员 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + @DeleteMapping("/removeUser/{id}") + public ActionResult removeUser(@PathVariable("id" ) String id, @RequestBody InformationAuthorityVo informationAuthorityVo) { + int i = docLibraryService.userPermissions(id); + if (i != 0 && i != 1){ + return ActionResult.fail("您没有权限!"); + } + docLibraryService.removeUser(informationAuthorityVo.getId()); + return ActionResult.success(); + } + + /** + * 批量移除成员 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + @DeleteMapping("/removeUserBatch/{id}") + public ActionResult removeUserBatch(@PathVariable("id" ) String id,@RequestBody FileQueryDto fileQueryDto) { + int i = docLibraryService.userPermissions(id); + if (i != 0 && i != 1){ + return ActionResult.fail("您没有权限!"); + } + docLibraryService.removeUserBatch(fileQueryDto.getInformationAuthorityVos()); + return ActionResult.success(); + } + + /** + * 移动文件 + * @param id 被移动的文件id + * @param fileQueryDto 信息 + * @return 返回值 + */ + @PutMapping("/move/{id}") + public ActionResult move(@PathVariable("id" ) String id,@RequestBody FileQueryDto fileQueryDto) { + int i = docLibraryService.userPermissions(id); + if (i != 0 && i != 1 && i != 2){ + return ActionResult.fail("您没有权限!"); + } + docLibraryService.move(id,fileQueryDto); + return ActionResult.success(); + } + + /** + * 新增共享空间 + * @param spaceDto 空间信息 + * @return 返回值 + */ + @PostMapping("/space") + public ActionResult space(@RequestBody SpaceDto spaceDto) { + docLibraryService.space(spaceDto); + return ActionResult.success(); + } + /** + * 批量新增成员 + * @param spaceDto 成员信息 + * @return 返回值 + */ + @PostMapping("/membersBatch") + public ActionResult membersBatch(@RequestBody SpaceDto spaceDto) { + int i = docLibraryService.userPermissions(spaceDto.getId()); + if (i != 0 && i != 1){ + return ActionResult.fail("您没有权限!"); + } + docLibraryService.membersBatch(spaceDto); + return ActionResult.success(); + } + + + /** + * 新增文件夹 + * @param spaceDto 空间信息 + * @return 返回值 + */ + @PostMapping("/folder") + public ActionResult folder(@RequestBody SpaceDto spaceDto) { + docLibraryService.folder(spaceDto); + return ActionResult.success(); + } + + /** + * 退出空间 + * @param id 空间id + * @return 返回值 + */ + @PutMapping("/exitSpace/{id}") + public ActionResult exitSpace(@PathVariable("id" ) String id) { + Integer integer = docLibraryService.exitSpace(id); + if (integer < 0){ + return ActionResult.fail("空间拥有者不能退出!"); + } + return ActionResult.success(); + } + + /** + * 退出空间按钮判断 + * @param id 空间id + * @return 返回值 + */ + @GetMapping("/exitSpaceButton/{id}") + public ActionResult exitSpaceButton(@PathVariable("id" ) String id) { + + return ActionResult.success(docLibraryService.exitSpaceButton(id)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/SpaceController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/SpaceController.java new file mode 100644 index 0000000..5291cec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/controller/SpaceController.java @@ -0,0 +1,284 @@ +package jnpf.doclibrary.controller; + +import cn.xuyanwu.spring.file.storage.FileInfo; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.doclibrary.service.SpaceService; +import jnpf.entity.Information; +import jnpf.exception.HandleException; +import jnpf.model.doclibrary.dto.*; +import jnpf.model.doclibrary.vo.*; +import jnpf.util.CustomTenantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * 共享空间控制器 + * + * @author yanwenfu + * @create 2023-07-20 + */ +@RestController +@RequestMapping(value = "/doclibrary") +public class SpaceController { + + @Resource + private SpaceService spaceService; + @Resource + private CustomTenantUtil customTenantUtil; + + /** + * 点击共享空间/空间/文件夹查询下面的所有内容 + * @param pid 父id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/fileList/{pid}") + public ActionResult> getMyResourceList(@PathVariable(value = "pid") String pid) { + + List list = spaceService.getMyResourceList(pid); + return ActionResult.success(list); + } + + /** + * 点击共享空间/空间/文件夹查询下面的所有内容(分页) + * @param pid 父id + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/filePage/{pid}") + public ActionResult> getMyResourcePage(@PathVariable(value = "pid") String pid, InformationQueryDto queryDto) { + + PageInfo page = spaceService.getMyResourcePage(pid, queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 查询空间/文件夹/文件id占用空间的大小 + * @param id 空间/文件夹/文件id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/information/space/{id}") + public ActionResult getInformationSpace(@PathVariable(value = "id") String id) { + + SpaceVo space = spaceService.getInformationSpace(id); + return ActionResult.success(space); + } + + /** + * 查询空间/文件夹/文件关联的成员 + * @param id 空间/文件夹/文件id + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/information/member/list/{id}") + public ActionResult> getInformationMemberList(@PathVariable(value = "id") String id) { + + List list = spaceService.getInformationMemberList(id); + return ActionResult.success(list); + } + + /** + * 查看空间/文件夹/文件信息 + * @param id 空间/文件夹/文件id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/information/{id}") + public ActionResult getInformation(@PathVariable(value = "id") String id) throws Exception { + + InformationDetailVo detail = spaceService.getInformation(id); + return ActionResult.success(detail); + } + + /** + * 新增/更新浏览记录 + * @param viewDto 查看记录dto + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/viewRecord") + public Object addViewRecord(@RequestBody @Valid ViewDto viewDto) { + + spaceService.addViewRecord(viewDto); + return ActionResult.success(); + } + + /** + * 回收站 - 列表 + * @param queryType 查询类型(1: 文件夹/2: 空间) + * @param queryCondition 查询条件(文件名) + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/rubbish/list") + public ActionResult> getRubbishList(@RequestParam(value = "queryType") Integer queryType, @RequestParam(value = "queryCondition", required = false) String queryCondition) { + + List list = spaceService.getRubbishList(queryType, queryCondition); + return ActionResult.success(list); + } + + /** + * 回收站 - 列表(分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/rubbish/page") + public ActionResult> getRubbishPage(@Valid RubbishQueryDto queryDto) { + + PageInfo page = spaceService.getRubbishPage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 回收站 - 彻底删除 + * @param rubbishId 垃圾id + * @return jnpf.base.ActionResult + */ + @DeleteMapping(value = "/rubbish/{rubbishId}") + public Object deleteRubbish(@PathVariable(value = "rubbishId") String rubbishId) { + + spaceService.deleteRubbish(rubbishId); + return ActionResult.success(); + } + + /** + * 回收站 - 恢复(空间/文件夹/文件) + * @param undeleteDto 撤销删除dto + * @return jnpf.base.ActionResult + */ + @PutMapping(value = "/rubbish/undelete") + public Object updateToUndelete(@RequestBody @Valid UndeleteDto undeleteDto) throws HandleException { + + spaceService.updateToUndelete(undeleteDto); + return ActionResult.success(); + } + + /** + * 回收站 - 恢复资源验证 + * @param rubbishId 垃圾id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/rubbish/undelete/{rubbishId}") + public ActionResult checkUndelete(@PathVariable(value = "rubbishId") String rubbishId) { + + CheckRubbishVo checkRubbish = spaceService.checkUndelete(rubbishId); + return ActionResult.success(checkRubbish); + } + + /** + * 新增文档/表格/PPT + * @param uploadDto 上传dto + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/information/file") + public ActionResult addInformationFile(@RequestBody @Valid UploadDto uploadDto) throws Exception { + + Information info = spaceService.addInformationFile(uploadDto); + return ActionResult.success(info); + } + + /** + * 批量新增资源库数据(图片/视频/文件) + * @param list 信息列表 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/information/details") + public Object addInformationDetails(@RequestBody @Valid List list) { + + spaceService.addInformationDetails(list); + return ActionResult.success(); + } + + /** + * 上传文件 + * @param multipartFile 文件 + * @param fileName 文件名 + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ActionResult uploadFile(@RequestPart("multipartFile") MultipartFile multipartFile, @RequestParam(value = "fileName", required = false) String fileName) { + + FileInfo info = spaceService.uploadFile(multipartFile, fileName); + return ActionResult.success(info); + } + + /** + * 畅写回调 + * @param request HttpServletRequest + * @param response HttpServletResponse + */ + @NoDataSourceBind + @PostMapping("/changxie/callback") + public void singleFileUploads(HttpServletRequest request, HttpServletResponse response) throws Exception { + + customTenantUtil.checkOutTenant(request.getParameter("tenantId")); + spaceService.singleFileUploads(request, response); + } + + @NoDataSourceBind + @GetMapping(value = "/file") + public void getFile(@RequestParam String url, @RequestParam String fileName, HttpServletResponse response) throws Exception { + + URL fileUrl = new URL(url); + HttpURLConnection connection = (HttpURLConnection) fileUrl.openConnection(); + InputStream in = connection.getInputStream(); + if (null == in) { + throw new Exception("下载失败..."); + } + String subStr = url.substring(url.lastIndexOf(".")); + response.setCharacterEncoding("utf-8"); + response.setContentType("application/octet-stream;charset=utf-8"); + // 6. 禁止图像缓存。 + response.setHeader("Pragma", "no-cache"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName + subStr, StandardCharsets.UTF_8)); + response.setDateHeader("Expires", 0); + int count; + byte[] by = new byte[1024]; + //通过response对象获取OutputStream流 + OutputStream out = response.getOutputStream(); + while ((count = in.read(by)) != -1) { + //将缓冲区的数据输出到浏览器 + out.write(by, 0, count); + } + in.close(); + out.flush(); + out.close(); + } + + + /** + * 查询我的所有文件夹(返回tree) + * + * @param keyWord 文件夹名称 + * @return 返回一颗tree + */ + @GetMapping(value = "/query-my-folder") + public ActionResult> queryMyResourceFolder(@RequestParam("keyWord") String keyWord) { + return ActionResult.success("成功", spaceService.queryMyResourceFolder(keyWord)); + } + + /** + * 查询文件夹下的所有文件 + * + * @param req 请求参数 + * @return 文件列表 + */ + @GetMapping(value = "/query-my-files") + public ActionResult> queryMyFilesForFolderId(QueryMyFilesReq req) { + PageInfo page =spaceService.queryMyFilesForFolderId(req); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/crontask/DocLibraryCronTask.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/crontask/DocLibraryCronTask.java new file mode 100644 index 0000000..7bd178c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/crontask/DocLibraryCronTask.java @@ -0,0 +1,34 @@ +package jnpf.doclibrary.crontask; + +import jnpf.doclibrary.service.SpaceService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 资料库定时任务 + * + * @author yanwenfu + * @create 2023-07-26 + */ +@Slf4j +@Component +public class DocLibraryCronTask { + + @Resource + private SpaceService spaceService; + + @Scheduled(cron="0 1 0 * * ?") + public void deleteOverdueViewRecord() { + log.info("删除过期的浏览记录..."); + // spaceService.deleteOverdueViewRecord(); + } + + @Scheduled(cron="0 1 0 * * ?") + public void deleteOverdueRubbish() { + log.info("删除过期的回收站资料..."); + // spaceService.deleteOverdueRubbish(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/CommonConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/CommonConfigMapper.java new file mode 100644 index 0000000..753b36a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/CommonConfigMapper.java @@ -0,0 +1,11 @@ +package jnpf.doclibrary.mapper; + +public interface CommonConfigMapper { + + /** + * 根据key查询数据报表url配置 + * @param key 键 + * @return java.lang.String + */ + String getDataReportUrlConfig(String key); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationAuthorityMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationAuthorityMapper.java new file mode 100644 index 0000000..3bbe483 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationAuthorityMapper.java @@ -0,0 +1,119 @@ +package jnpf.doclibrary.mapper; + +import jnpf.model.doclibrary.dto.InformationAuthorityDto; +import jnpf.model.doclibrary.vo.InformationAuthorityVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface InformationAuthorityMapper { + + /** + * 获取文件权限 + * @param informationId 空间/文件夹/文件/文档id + * @param userId 用户id + * @return 返回值 + */ + Integer getInformationAuthority(@Param("informationId") String informationId, @Param("userId") String userId); + + /** + * 向上查看文件有无权限 + * @param informationId 文件id + * @param userId 用户id + * @return 返回值 + */ + List getUpPathAuthority(@Param("informationId") String informationId, @Param("userId") String userId); + + /** + * 向下查看文件有无权限 + * @param informationId 文件id + * @param userId 用户id + * @return 返回值 + */ + Integer getDownPathAuthority(@Param("informationId") String informationId, @Param("userId") String userId); + + /** + * 空间/文件夹/文件下成员列表 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + List getUserList(@Param("id") String id); + + /** + * 修改用户权限 + * @param userId 用户id + * @param id 文件id + * @param limitsOfAuthority 权限管理(0.超级管理员1..管理员 2.可编辑 3.可上传下载 4.可下载 5.仅预览) + * @return 返回值 + */ + void updateUserAuthority(@Param("userId") String userId, @Param("id") String id , @Param("limitsOfAuthority") Integer limitsOfAuthority); + + /** + * 查看用户是不是指定文件的成员 + * @param id 空间/文件夹/文件id + * @param userId 用户id + * @return 返回值 + */ + String getIdByUserId(@Param("id") String id, @Param("userId") String userId); + + /** + * 新增用户权限 + * @param userAuthorityId 用户与权限关联的id + * @param userId 修改人id + * @param id 文件id + * @param limitsOfAuthority 权限管理(0.超级管理员1..管理员 2.可编辑 3.可上传下载 4.可下载 5.仅预览) + * @param toUserId 用户id + * @return 返回值 + */ + void addUserAuthority(@Param("userAuthorityId") String userAuthorityId, @Param("userId") String userId, @Param("id") String id, @Param("limitsOfAuthority") Integer limitsOfAuthority, @Param("toUserId") String toUserId); + + /** + * 修改指定的权限 + * @param idByUserId 用户权限关联id + * @param limitsOfAuthority 权限管理(0.超级管理员1..管理员 2.可编辑 3.可上传下载 4.可下载 5.仅预览) + */ + void updateById(@Param("idByUserId") String idByUserId, @Param("limitsOfAuthority") Integer limitsOfAuthority, @Param("updateUserId") String updateUserId); + + + /** + * 通过用户与权限关联id删除信息 + * @param id 用户权限关联id + */ + void deleteById(@Param("id") String id); + + + /** + * 批量新增用户权限信息 + * @param id 空间/文件夹id + * @param userId 新增人id + * @param userList 被新增用户的信息 + * @return 返回值 + */ + void addUserAuthorityList(@Param("id") String id, @Param("userId") String userId, @Param("userList") List userList); + + /** + * 退出空间 + * @param id 空间id + * @param userId 用户id + * @return 返回值 + */ + void deleteByUserAndId(@Param("id") String id, @Param("userId") String userId); + + /** + * 验证用户是不是空间及齐下的成员 + * @param id 空间id + * @param userId 用户id + * @return 返回值 + */ + Integer checkSpaceMember(@Param("id") String id, @Param("userId") String userId); + + /** + * 验证用户是不是空间的成员 + * @param id 空间id + * @param userId 用户id + * @return 返回值 + */ + InformationAuthorityVo spaceMember(@Param("id") String id, @Param("userId") String userId); + + void deleteByUsersAndId(@Param("id") String id, @Param("ids") List ids); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationLogMapper.java new file mode 100644 index 0000000..c28a7ed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationLogMapper.java @@ -0,0 +1,16 @@ +package jnpf.doclibrary.mapper; + +import org.apache.ibatis.annotations.Param; + +public interface InformationLogMapper { + + /** + * 新增日志 + * @param informationId 文档/文件id + * @param userId 用户信息 + * @param action 动作 新增 /修改/... + * @param content 日志内容 + * @return 返回值 + */ + void addInformationLog(@Param("informationId") String informationId, @Param("userId") String userId, @Param("action") String action, @Param("content") String content, @Param("logId") String logId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationMapper.java new file mode 100644 index 0000000..1daf206 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationMapper.java @@ -0,0 +1,103 @@ +package jnpf.doclibrary.mapper; + +import jnpf.model.doclibrary.dto.FileQueryDto; +import jnpf.model.doclibrary.dto.SpaceDto; +import jnpf.model.doclibrary.vo.InformationVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface InformationMapper { + + /** + * 获取-与我相关的文件列表 + * @param fileQueryDto 文件搜索信息 + * @return 返回值 + */ + List relatedToMeFile(@Param("fileQueryDto") FileQueryDto fileQueryDto, @Param("enabledMarkIds") List enabledMarkIds); + + /** + * 获取最近查看列表 + * @param fileQueryDto 文件搜索信息 + * @return 返回值 + */ + List recentlyViewed(@Param("fileQueryDto") FileQueryDto fileQueryDto, @Param("enabledMarkIds") List enabledMarkIds); + + /** + * 重命名 + * @param id 文件id + * @param fileQueryDto 修改后参数 + * @return 返回值 + */ + void rename(@Param("id") String id, @Param("fileQueryDto") FileQueryDto fileQueryDto); + + /** + * 通过文件id获取文件信息 + * @param id 文件id + * @return 返回值 + */ + InformationVo getInformationById(@Param("id") String id); + + /** + * 删除空间/文件夹/文件 + * @param id 删除空间/文件夹/文件id + * @param userId 用户id + * @return 返回值 + */ + void delete(@Param("id") String id, @Param("userId") String userId); + + /** + * 修改文件拥有者 + * @param id 删除空间/文件夹/文件id + * @param userId 用户id + * @param updateUserId 更新人id + * @return 返回值 + */ + void updateOwner(@Param("id") String id, @Param("userId") String userId, @Param("updateUserId") String updateUserId); + + /** + * 新增空间/文件夹 + * @param spaceDto 空间/文件夹信息 + * @param userId 用户id + * @return 返回值 + */ + void addInformation(@Param("spaceDto") SpaceDto spaceDto, @Param("userId") String userId); + + /** + * 获取文件夹及其下级路径信息 + * @param id 文件夹/文件id + * @return 返回值 + */ + List getSubordinate(@Param("id") String id); + + /** + * 批量修改文件路径 + * @param id 文件id + * @param path 修改后路径 + * @param userId 用户信息 + */ + void updateFilePath(@Param("id") String id , @Param("path") String path, @Param("userId") String userId); + + /** + * 修改指定文件的父级id + * @param id 文件id + * @param toInformationId 父级id + */ + void updateFilePId(@Param("id") String id, @Param("toInformationId") String toInformationId , @Param("userId") String userId); + + List recentlyViewedMember(@Param("fileQueryDto") FileQueryDto fileQueryDto, @Param("enabledMarkIds") List enabledMarkIds); + + /** + * 得到企业下所有被标记为删除的id集合 + * @return 返回值 + */ + List getEnabledMarkInformation(); + + + /** + * 获取所有被删除的文件 + * @param enabledMarkIds 标记为删除的ids + * @return 返回值 + */ + List getDeleteIds(List enabledMarkIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationRubbishMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationRubbishMapper.java new file mode 100644 index 0000000..9be2c10 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InformationRubbishMapper.java @@ -0,0 +1,14 @@ +package jnpf.doclibrary.mapper; + +import org.apache.ibatis.annotations.Param; + +public interface InformationRubbishMapper { + + /** + * 新增垃圾箱 + * @param id 删除空间/文件夹/文件id + * @return 返回值 + */ + void addRubbish(@Param("id") String id, @Param("rubbishId") String rubbishId, @Param("userId") String userId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InfornationRecentlyViewedMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InfornationRecentlyViewedMapper.java new file mode 100644 index 0000000..420b6ed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/InfornationRecentlyViewedMapper.java @@ -0,0 +1,5 @@ +package jnpf.doclibrary.mapper; + +public interface InfornationRecentlyViewedMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/SpaceMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/SpaceMapper.java new file mode 100644 index 0000000..3429c1c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/mapper/SpaceMapper.java @@ -0,0 +1,306 @@ +package jnpf.doclibrary.mapper; + +import jnpf.entity.Information; +import jnpf.entity.InformationRubbish; +import jnpf.model.doclibrary.dto.QueryMyFilesReq; +import jnpf.model.doclibrary.vo.*; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 共享空间mapper + * + * @author yanwenfu + * @create 2023-07-20 + */ +public interface SpaceMapper { + + /** + * 查询资源中我拥有权限的 + * @param userId 用户id + * @param infoIds 资源ids + * @return java.util.List + */ + List getMyInfoIdByPath(@Param("userId") String userId, @Param("list") List infoIds); + + /** + * 查询资源列表 + * @param ids 资源ids + * @param queryCondition 查询条件(名称) + * @param queryType 查询类型 + * @return java.util.List + */ + List getInformationList(@Param("list") List ids, @Param("queryCondition") String queryCondition, @Param("queryType") String queryType); + + /** + * 根据关键字查询配置信息 + * @param key 关键字 + * @return java.lang.Integer + */ + String getCommonConfigByKey(String key); + + /** + * 查询已使用空间 + * @param id 空间/文件夹/文件id + * @param delIds 被删除的 + * @return java.lang.Double + */ + Double getUsedSpaceSize(@Param("id") String id, @Param("list") List delIds); + + /** + * 查询被删除的部分 + * @param id 空间/文件夹/文件id + * @return java.util.List + */ + List getDeletedFileIds(String id); + + /** + * 查询资源关联的成员 + * @param id 空间/文件夹/文件id + * @return java.util.List + */ + List getInformationMemberList(String id); + + /** + * 查看空间/文件夹/文件信息 + * @param id 空间/文件夹/文件id + * @return jnpf.model.doclibrary.vo.InformationDetailVo + */ + InformationDetailVo getInformation(String id); + + /** + * 查看最近浏览记录 + * @param id 空间/文件夹/文件id + * @return jnpf.model.doclibrary.vo.RecentlyViewedVo + */ + RecentlyViewedVo getRecentlyViewInfo(String id); + + /** + * 查看用户最近浏览记录 + * @param userId 用户id + * @param informationId 资料id + * @return jnpf.model.doclibrary.vo.RecentlyViewedVo + */ + RecentlyViewedVo getRecentlyViewByUser(@Param("userId") String userId, @Param("informationId") String informationId); + + /** + * 更新用户浏览记录 + * @param id 记录id + * @return int + */ + int updateRecentlyViewInfo(String id); + + /** + * 新增用户浏览记录 + * @param id 记录id + * @param informationId 记录id + * @param userId 用户id + * @return int + */ + int addRecentlyViewRecord(@Param("id") String id, @Param("informationId") String informationId, @Param("userId") String userId); + + /** + * 回收站 - 列表 + * @param queryType 查询类型(1: 文件夹/2: 空间) + * @param queryCondition 查询条件(文件名) + * @param userId 用户id + * @return java.util.List + */ + List getRubbishList(@Param("queryType") Integer queryType, @Param("queryCondition") String queryCondition, @Param("userId") String userId); + + /** + * 通过ids查询回收站记录 + * @param rubbishIds 垃圾ids + * @return java.util.List + */ + List getRubbishListByIds(@Param("list") List rubbishIds); + + /** + * 恢复为未删除 + * @param ids 资料ids + * @param userId 操作人 + * @return int + */ + int updateToUndelete(@Param("list") List ids, @Param("userId") String userId); + + /** + * 恢复为未删除 + * @param informationId 资料id + * @param userId 操作人 + * @return int + */ + int updateToUndeleteById(@Param("informationId") String informationId, @Param("userId") String userId); + + /** + * 删除回收站记录 + * @param rubbishIds 垃圾ids + * @return int + */ + int deleteRubbishRecord(@Param("list") List rubbishIds); + + /** + * 查询回收站记录 + * @param rubbishId 垃圾id + * @return jnpf.model.doclibrary.vo.RubbishInfoVo + */ + RubbishInfoVo getRubbishById(String rubbishId); + + /** + * 根据path查询资源 + * @param informationId 资源id + * @return java.util.List + */ + List getInformationByPath(String informationId); + + /** + * 根据ids的path查询资源 + * @param informationIds 资源ids + * @return java.util.List + */ + List getInformationByIdsPath(@Param("list") List informationIds); + + /** + * 根据资料id删除回收站记录 + * @param infoIds 资料ids + * @return int + */ + int deleteRubbishByInfoIds(@Param("list") List infoIds); + + /** + * 根据资料id删除浏览记录 + * @param infoIds 资料ids + * @return int + */ + int deleteViewByInfoIds(@Param("list") List infoIds); + + /** + * 根据资料id删除资料相关权限 + * @param infoIds 资料ids + * @return int + */ + int deleteAuthorityByInfoIds(@Param("list") List infoIds); + + /** + * 根据资料id删除资料记录 + * @param infoIds 资料ids + * @return int + */ + int deleteInformationByInfoIds(@Param("list") List infoIds); + + /** + * 查询资源path + * @param informationId 资源id + * @return java.lang.String + */ + String getInformationPath(String informationId); + + /** + * 根据文件路径查询路径上的删除情况 + * @param filePath 文件路径 + * @return java.util.List + */ + List getDeleteInfoByPath(String filePath); + + /** + * 恢复文件到指定父级 + * @param informationId 资源id + * @param pid 父id + * @param path 路径 + * @param userId 操作人 + * @return int + */ + int updateUndeleteToSpace(@Param("informationId") String informationId, @Param("pid") String pid, @Param("path") String path, @Param("userId") String userId); + + /** + * 查询空间信息 + * @param ids 空间ids + * @return java.util.List + */ + List getSpaceNameList(@Param("list") List ids); + + /** + * 新增资源 + * @param information 资源 + * @return int + */ + int addInformation(Information information); + + /** + * 删除过期的浏览记录 + * @return int + */ + int deleteOverdueViewRecord(); + + /** + * 查询过期的垃圾文件 + * @param keepDay 有效期 + * @return java.util.List + */ + List getExpireRubbish(@Param("keepDay") String keepDay, @Param("userId") String userId); + + /** + * 查出完整路径中包含infoIds的并且我拥有权限的文件/文件夹/空间 + * @param infoIds 资源ids + * @return java.util.List + */ + List getMyResourcePath(@Param("list") List infoIds); + + /** + * 查询所有资源中我拥有权限的 + * @param userId 用户id + * @return java.util.List + */ + List getMyRootInfoIdByPath(String userId); + + /** + * 更新资源大小 + * @param informationId 资源id + * @param userId 操作人 + * @return int + */ + int updateFileSize(@Param("informationId") String informationId, @Param("fileSize") Long fileSize, @Param("userId") String userId); + + /** + * 查询本级和子级资源 + * @param informationId 资源id + * @return java.util.List + */ + List getChildrenInfoList(String informationId); + + /** + * 查询所有子级 + * @param pid 父id + * @return java.util.List + */ + List getAllByPid(String pid); + + /** + * 查询回收站资源占用空间 + * @param informationId 资源id + * @return java.lang.Double + */ + Double getRubbishUsedSpace(String informationId); + + /** + * 查询我的文件 + * @param folderName 文件夹名称 + * @param ids 文件夹ids + * @return java.util.List + */ + List queryMyResourceFolder(@Param("folderName") String folderName, @Param("ids") List ids); + + /** + * 查询所有有权限的资料id + * @param userId 用户id + * @return java.util.List + */ + List queryAllPowerInfoIds(@Param("userId") String userId); + + /** + * 查询我的文件 + * @param params 参数 + * @return java.util.List + */ + List queryMyFilesForFolderId(@Param("params") QueryMyFilesReq params); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DataReportService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DataReportService.java new file mode 100644 index 0000000..9fa920f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DataReportService.java @@ -0,0 +1,18 @@ +package jnpf.doclibrary.service; + +import cn.hutool.json.JSONObject; + +/** + * 数据报表服务 + * + * @author yanwenfu + * @create 2024-07-19 + */ +public interface DataReportService { + + /** + * 根据key查询数据报表url + * @return cn.hutool.json.JSONObject + */ + JSONObject getDataReportUrlConfig(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DocLibraryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DocLibraryService.java new file mode 100644 index 0000000..a348ff1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/DocLibraryService.java @@ -0,0 +1,146 @@ +package jnpf.doclibrary.service; + +import com.github.pagehelper.PageInfo; +import jnpf.model.doclibrary.dto.FileQueryDto; +import jnpf.model.doclibrary.dto.SpaceDto; +import jnpf.model.doclibrary.vo.InformationAuthorityVo; +import jnpf.model.doclibrary.vo.InformationVo; + +import java.util.List; + +/** + * 资料库服务 + * + * @author yanwenfu + * @create 2023-07-20 + */ +public interface DocLibraryService { + + /** + * 获取-与我相关的文件列表 + * @param fileQueryDto 筛选参数 + * @return 返回值 + */ + List relatedToMeFile(FileQueryDto fileQueryDto); + + /** + * 获取最近查看列表 + * @param fileQueryDto 参数 + * @return 返回值 + */ + + List recentlyViewed(FileQueryDto fileQueryDto); + + /** + * 重命名 + * @param id 空间/文件夹/文件id + * @param fileQueryDto 文件信息 + * @return 返回值 + */ + int rename(String id, FileQueryDto fileQueryDto); + + /** + * 空间/文件夹/文件权限 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + Integer userPermissions(String id); + + /** + * 删除空间/文件夹/文件 + * @param id 删除空间/文件夹/文件id + * @return 返回值 + */ + int deleteInformation(String id); + + /** + * 空间/文件夹/文件下成员列表 + * @param id 空间/文件夹/文件id + * @return 返回值 + */ + List userList(String id); + + /** + * 转移所有权 + * @param id 空间/文件夹/文件id + */ + void transferOwnership(String id , FileQueryDto fileQueryDto); + + /** + * 修改用户权限 + * @param id 用户文件权限关联id + * @param informationAuthorityVo 参数 + */ + void editPermissions(String id, InformationAuthorityVo informationAuthorityVo); + + /** + * 移除成员 + * @param id 用户文件权限关联id + */ + void removeUser(String id); + + /** + * 新增空间 + * @param spaceDto 空间信息 + */ + void space(SpaceDto spaceDto); + + /** + * 新增文件夹 + * @param spaceDto 文件夹信息 + */ + void folder(SpaceDto spaceDto); + + /** + * 移动文件夹/文件 + * @param id 文件夹/文件id + * @param fileQueryDto 移动到.. + */ + void move(String id,FileQueryDto fileQueryDto); + + /** + * 与我相关的文件分页列表 + * @param fileQueryDto 搜索参数 + * @return 返回值 + */ + PageInfo relatedToMeFilePage(FileQueryDto fileQueryDto); + + /** + * 获取--与我相关分页 + * @param fileQueryDto 搜索参数 + * @return 返回值 + */ + PageInfo recentlyViewedPage(FileQueryDto fileQueryDto); + + /** + * 退出空间 + * @param id 空间id + */ + Integer exitSpace(String id); + + /** + * 批量编辑成员权限 + * @param id 空间/文件夹/文件id + * @param fileQueryDto 操作信息 + */ + void editPermissionsBatch(String id, FileQueryDto fileQueryDto); + + /** + * 批量移除成员 + * @param informationAuthorityVos 需要移除的成员信息 + * @return 返回值 + */ + void removeUserBatch(List informationAuthorityVos); + + /** + * 批量新增成员权限 + * @param spaceDto 权限信息 + */ + void membersBatch(SpaceDto spaceDto); + + void addInformationLog(String informationId, Integer classification, String userId, String userName, String action, List userNameList); + /** 验证是否是空间成员 */ + boolean checkSpaceMember(String id); + + Integer exitSpaceButton(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/SpaceService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/SpaceService.java new file mode 100644 index 0000000..29774b0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/SpaceService.java @@ -0,0 +1,146 @@ +package jnpf.doclibrary.service; + +import cn.xuyanwu.spring.file.storage.FileInfo; +import com.github.pagehelper.PageInfo; +import jnpf.entity.Information; +import jnpf.exception.HandleException; +import jnpf.model.doclibrary.dto.*; +import jnpf.model.doclibrary.vo.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 共享空间服务 + * + * @author yanwenfu + * @create 2023-07-20 + */ +public interface SpaceService { + + /** + * 点击共享空间/空间/文件夹查询下面的所有内容 + * @param pid 父id + * @return java.util.List + */ + List getMyResourceList(String pid); + + /** + * 点击共享空间/空间/文件夹查询下面的所有内容(分页) + * @param pid 父id + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getMyResourcePage(String pid, InformationQueryDto queryDto); + + /** + * 查询空间/文件夹/文件id占用空间大小 + * @param id 空间/文件夹/文件id + * @return jnpf.model.doclibrary.vo.SpaceVo + */ + SpaceVo getInformationSpace(String id); + + /** + * 查询空间/文件夹/文件关联的成员 + * @param id 空间/文件夹/文件id + * @return java.util.List + */ + List getInformationMemberList(String id); + + /* + * 查看空间/文件夹/文件信息 + * @param id 空间/文件夹/文件id + * @return jnpf.model.doclibrary.vo.InformationDetailVo + */ + InformationDetailVo getInformation(String id) throws Exception; + + /** + * 新增/更新浏览记录 + * @param viewDto 浏览记录dto + */ + void addViewRecord(ViewDto viewDto); + + /** + * 回收站 - 列表 + * @param queryType 查询类型(1: 文件夹/2: 空间) + * @param queryCondition 查询条件(文件名) + * @return java.util.List + */ + List getRubbishList(Integer queryType, String queryCondition); + + /** + * 回收站 - 列表(分页) + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getRubbishPage(RubbishQueryDto queryDto); + + /** + * 回收站 - 彻底删除 + * @param rubbishId 垃圾id + */ + void deleteRubbish(String rubbishId); + + /** + * 回收站 - 恢复(空间/文件夹/文件) + * @param undeleteDto 撤销删除dto + * @throws HandleException 操作异常 + */ + void updateToUndelete(UndeleteDto undeleteDto) throws HandleException; + + /** + * 回收站 - 恢复资源验证 + * @param rubbishId 垃圾id + * @return jnpf.model.doclibrary.vo.CheckRubbishVo + */ + CheckRubbishVo checkUndelete(String rubbishId); + + /** + * 新增文档/表格/PPT + * @param uploadDto 上传dto + * @return jnpf.entity.Information + */ + Information addInformationFile(UploadDto uploadDto) throws Exception; + + /** + * 批量新增资源库数据 + * @param list 列表 + */ + void addInformationDetails(List list); + + /** + * 删除过期的回收站资料 + */ + void deleteOverdueRubbish(String userId); + + /** + * 删除过期的浏览记录 + */ + void deleteOverdueViewRecord(); + + void singleFileUploads(HttpServletRequest request, HttpServletResponse response) throws Exception; + + /** + * 上传文件 + * @param multipartFile 文件 + * @param fileName 文件名 + * @return cn.xuyanwu.spring.file.storage.FileInfo + */ + FileInfo uploadFile(MultipartFile multipartFile, String fileName); + + /** + * 查询我的资源文件夹 + * @param keyWord 关键字 + * @return java.util.List + */ + List queryMyResourceFolder(String keyWord); + + /** + * 查询我的文件 + * @param req 请求参数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo queryMyFilesForFolderId(QueryMyFilesReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DataReportServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DataReportServiceImpl.java new file mode 100644 index 0000000..602b924 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DataReportServiceImpl.java @@ -0,0 +1,34 @@ +package jnpf.doclibrary.service.impl; + +import cn.hutool.json.JSONObject; +import jnpf.doclibrary.mapper.CommonConfigMapper; +import jnpf.doclibrary.service.DataReportService; +import jnpf.entity.CommonConfig; +import jnpf.util.ConstantUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 数据报表服务实现 + * + * @author yanwenfu + * @create 2024-07-19 + */ +@Service +public class DataReportServiceImpl implements DataReportService { + + @Resource + private CommonConfigMapper commonConfigMapper; + + @Override + public JSONObject getDataReportUrlConfig() { + + String jsonStr = commonConfigMapper.getDataReportUrlConfig(ConstantUtil.CONFIG_DATA_REPORT); + if (StringUtils.isNotEmpty(jsonStr)) { + return new JSONObject(jsonStr); + } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DocLibraryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DocLibraryServiceImpl.java new file mode 100644 index 0000000..310cac6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/DocLibraryServiceImpl.java @@ -0,0 +1,524 @@ +package jnpf.doclibrary.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.doclibrary.mapper.InformationAuthorityMapper; +import jnpf.doclibrary.mapper.InformationLogMapper; +import jnpf.doclibrary.mapper.InformationMapper; +import jnpf.doclibrary.mapper.InformationRubbishMapper; +import jnpf.doclibrary.service.DocLibraryService; +import jnpf.doclibrary.service.SpaceService; +import jnpf.model.doclibrary.dto.FileQueryDto; +import jnpf.model.doclibrary.dto.InformationAuthorityDto; +import jnpf.model.doclibrary.dto.SpaceDto; +import jnpf.model.doclibrary.vo.InformationAuthorityVo; +import jnpf.model.doclibrary.vo.InformationVo; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.FtbUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 资料库服务 + * + * @author yanwenfu + * @create 2023-07-20 + */ +@Service +public class DocLibraryServiceImpl implements DocLibraryService { + + @Resource + private InformationMapper informationMapper; + @Resource + private InformationLogMapper informationLogMapper; + + @Resource + private InformationAuthorityMapper informationAuthorityMapper; + + @Resource + private InformationRubbishMapper informationRubbishMapper; + + @Autowired + private UserApi userApi; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + private SpaceService spaceService; + + @Resource + private UserProvider userProvider; + + @Override + public List relatedToMeFile(FileQueryDto fileQueryDto) { + //得到企业下所有被标记为删除的id集合 + List enabledMarkIds = informationMapper.getEnabledMarkInformation(); + List informationVos = informationMapper.relatedToMeFile(fileQueryDto,enabledMarkIds); + setOwnerNames(informationVos); + return informationVos; + } + + @Override + public List recentlyViewed(FileQueryDto fileQueryDto) { + spaceService.deleteOverdueViewRecord(); + UserInfo userInfo = userProvider.get(); + //验证是否是超级管理员 +// Integer userModuleDataAuthorizesChem = userApi.getUserModuleDataAuthorizesChem("resource", userInfo.getUserId(), "information_admin"); + Boolean isAdministrator = userInfo.getIsAdministrator(); + //得到企业下所有被标记为删除的id集合 + List enabledMarkIds = informationMapper.getEnabledMarkInformation(); + if (isAdministrator){ + //超级管理员拥有最高权限 跳过验证 看所有的不管是不是成员 + List informationVos = informationMapper.recentlyViewed(fileQueryDto,enabledMarkIds); + setOwnerNames(informationVos); + return informationVos; + } + List informationVos = informationMapper.recentlyViewedMember(fileQueryDto,enabledMarkIds); + setOwnerNames(informationVos); + return informationVos; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int rename(String id, FileQueryDto fileQueryDto) { + //验证权限 + Integer integer = userPermissions(id); + if (integer < 0 || integer >= 3){ + return -1; + } + //修改文件名称 + informationMapper.rename(id,fileQueryDto); + InformationVo informationVo = informationMapper.getInformationById(id); + addInformationLog(id,informationVo.getClassification(),fileQueryDto.getUserId(),fileQueryDto.getUserName(),"更新",null); + return 0; + } + + /** + * 新增日志 + * @param informationId 文档/文件id + * @param classification 文件类型 1.空间 2.文件夹 3.文档 4.文件 + * @param userId 用户id + * @param userName 用户名称 + * @param action 动作 + * @param userNameList 被邀请人的名字列表 + */ + @Override + public void addInformationLog(String informationId ,Integer classification ,String userId,String userName,String action,List userNameList) { + + String typeName = null; + if (3 == classification){ + typeName = "文档"; + } + if (4 == classification){ + typeName = "文件"; + } + if (null != typeName) { + //张三创建了文档 + //张三更新了文档/文件 + //张三邀请赵利、王三、李四、...加入了此文件/文档 + //张三上传了文件/文档 + //新增日志 + String content; + if ("邀请".equals(action)){ + StringBuilder userNames = null; + for (String name : userNameList) { + if (null == userNames){ + userNames = new StringBuilder(name); + }else { + userNames.append("、").append(name); + } + } + content = userName + action + userNames + "加入了此" + typeName; + }else { + content = userName + action + "了" + typeName; + } + informationLogMapper.addInformationLog(informationId, userId, action, content, FtbUtil.getId()); + } + } + + @Override + public boolean checkSpaceMember(String id) { + UserInfo userInfo = userProvider.get(); + InformationVo informationById = informationMapper.getInformationById(id); + if (null != informationById && informationById.getClassification() == 1) { + //验证该id 是不是空间 + Integer integer = informationAuthorityMapper.checkSpaceMember(id, userInfo.getUserId()); + if (0 < integer) { + return true; + } + } + return false; + } + + @Override + public Integer exitSpaceButton(String id) { + UserInfo userInfo = userProvider.get(); + //验证是否是超级管理员 +// Integer userModuleDataAuthorizesChem = userApi.getUserModuleDataAuthorizesChem("resource", userInfo.getUserId(), "information_admin"); + if (userInfo.getIsAdministrator()){ + //超级管理员拥有最高权限 跳过验证 + return 1; + } + InformationVo informationById = informationMapper.getInformationById(id); + if (null != informationById && informationById.getClassification() == 1) { + //验证该id 是不是空间 + InformationAuthorityVo informationAuthorityVo = informationAuthorityMapper.spaceMember(id, userInfo.getUserId()); + if (null != informationAuthorityVo) { + if (0 == informationAuthorityVo.getLimitsOfAuthority()){ + //是空间拥有者 展示解散空间按钮按 + return 1; + } + // 是成员展示退出空间按钮 + return 2; + } + } + //不是成员不展示 + return 0; + } + + /** + * 用户权限 + * @param informationId 空间/文件夹/文件id + * @return 权限管理(0.超级管理员 1.管理员 2.可编辑 3.可上传下载 4.可下载 5.仅预览) + */ + @Override + public Integer userPermissions(String informationId ) { + UserInfo userInfo = userProvider.get(); + //验证是否是超级管理员 +// Integer userModuleDataAuthorizesChem = userApi.getUserModuleDataAuthorizesChem("resource", userInfo.getUserId(), "information_admin"); + if (userInfo.getIsAdministrator()){ + //超级管理员拥有最高权限 跳过验证 + return 0; + } + + InformationVo informationById = informationMapper.getInformationById(informationId); + //获取当前文件的权限 + Integer informationAuthority = informationAuthorityMapper.getInformationAuthority(informationId,userInfo.getUserId()); + if (null == informationAuthority){ + //当本文件没有权限时 向上查找路径上的第一个有文件的权限 拥有该文件权限 + List informationAuthorityVoList = informationAuthorityMapper.getUpPathAuthority(informationId,userInfo.getUserId()); + if (null == informationAuthorityVoList || informationAuthorityVoList.isEmpty()){ + //所有上级都不是成员 向下找看看有没有有文件夹/文件是成员的 + Integer num = informationAuthorityMapper.getDownPathAuthority(informationId,userInfo.getUserId()); + //有 赋值为查看权 + if (0 < num){ + return 5; + } + //没有 那么就没有权限 + return -1; + } + //找到最大的id + List list = new ArrayList<>(Arrays.asList(informationById.getFilePath().split(","))); + Collections.reverse(list); + outer : for (String s : list) { + for (InformationAuthorityVo informationAuthorityVo : informationAuthorityVoList) { + if (s.equals(informationAuthorityVo.getInformationId())) { + informationAuthority = informationAuthorityVo.getLimitsOfAuthority(); + break outer; + } + } + } + } + return informationAuthority; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteInformation(String id) { + //验证权限 + Integer integer = userPermissions(id); + if (integer < 0 || integer >= 3){ + return -1; + } + String userId = userProvider.get().getUserId(); + //删除文件 + informationMapper.delete(id,userId); + //加入垃圾箱 + informationRubbishMapper.addRubbish(id,FtbUtil.getId(),userId); + return 0; + } + + @Override + public List userList(String id) { + List informationAuthorityVos = informationAuthorityMapper.getUserList(id); + //赋值用户名称及头像 + List userIds = informationAuthorityVos.stream().map(InformationAuthorityVo::getUserId).collect(Collectors.toList()); + // 查询成员信息 +// List list = userApi.getInfoByIds(userIds); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, null); + Map map = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + } +// Map map = list.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + List ids = new ArrayList<>(); + Map finalMap = map; + informationAuthorityVos.forEach(v -> { +// PartUserInfoVo user = map.get(v.getUserId()); + UserBoundVO user = finalMap.get(v.getUserId()); + if (null != user) { + v.setUserName(user.getUserName()); + v.setHeadIcon(UploaderUtil.uploaderImg(user.getHeadIcon())); + }else { + ids.add(v.getUserId()); + } + }); + if (!ids.isEmpty()) { + // 移除已删除用户 + informationAuthorityMapper.deleteByUsersAndId(id, ids); + removeUser(informationAuthorityVos, ids); + } + return informationAuthorityVos; + } + + public static List removeUser(List users, List userIds) { + userIds.forEach(v->{ + for (InformationAuthorityVo user : users) { + if (Objects.equals(v, user.getUserId())) { + users.remove(user); + break; + } + } + }); + return users; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void transferOwnership(String id, FileQueryDto fileQueryDto) { + //转移空间/文件夹/文件所有权 + UserInfo userInfo = userProvider.get(); + InformationVo informationById = informationMapper.getInformationById(id); + //先将原来的文件拥有者的权限改为管理员 + informationAuthorityMapper.updateUserAuthority(informationById.getOwner(),id,2); + //将information中的拥有者改为新的用户 + informationMapper.updateOwner(id , fileQueryDto.getUserId(),userInfo.getUserId()); + //查看新的用户是否属于权限表中 没有新增 + String idByUserId = informationAuthorityMapper.getIdByUserId(id , fileQueryDto.getUserId()); + if (null != idByUserId){ + //有修改该用户的权限 + informationAuthorityMapper.updateById(idByUserId,0,userInfo.getUserId()); + }else { + informationAuthorityMapper.addUserAuthority(FtbUtil.getId(),userInfo.getUserId(),id,0,fileQueryDto.getUserId()); + } + } + + @Override + public void editPermissions(String id, InformationAuthorityVo informationAuthorityVo) { + String userId = userProvider.get().getUserId(); + informationAuthorityMapper.updateById(informationAuthorityVo.getId(),informationAuthorityVo.getLimitsOfAuthority(),userId); + } + + @Override + public void removeUser(String id) { + informationAuthorityMapper.deleteById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void space(SpaceDto spaceDto) { + UserInfo userInfo = userProvider.get(); + //空间赋值id + String id = FtbUtil.getId(); + spaceDto.setId(id); + //赋值路径 + spaceDto.setFilePath("0,"+id+","); + spaceDto.setClassification(1); + informationMapper.addInformation(spaceDto,userInfo.getUserId()); + //绑定空间成员 + spaceDto.getUserList().forEach(v->v.setId(FtbUtil.getId())); + informationAuthorityMapper.addUserAuthorityList(id,userInfo.getUserId(),spaceDto.getUserList()); + + } + + @Override + public void folder(SpaceDto spaceDto) { + InformationVo informationById = informationMapper.getInformationById(spaceDto.getFilePId()); + UserInfo userInfo = userProvider.get(); + //文件夹赋值id + String id = FtbUtil.getId(); + spaceDto.setId(id); + //赋值路径 + spaceDto.setFilePath(informationById.getFilePath()+id+","); + spaceDto.setClassification(2); + informationMapper.addInformation(spaceDto,userInfo.getUserId()); + //将创建人写入权限表 + List userList = new ArrayList<>(); + InformationAuthorityDto informationAuthorityDto = new InformationAuthorityDto(); + informationAuthorityDto.setId(FtbUtil.getId()); + informationAuthorityDto.setUserId(userInfo.getUserId()); + informationAuthorityDto.setLimitsOfAuthority(0); + userList.add(informationAuthorityDto); + informationAuthorityMapper.addUserAuthorityList(id,userInfo.getUserId(),userList); + } + + @Override + public void move(String id, FileQueryDto fileQueryDto) { + //通过移动后的空间/文件id查出信息 + InformationVo informationById = informationMapper.getInformationById(fileQueryDto.getToInformationId()); + //拿到当前文件夹和文件夹下数据信息 + List informationVos = informationMapper.getSubordinate(id); + UserInfo userInfo = userProvider.get(); + //替换路径 + informationVos.forEach(v->{ + String[] split = v.getFilePath().split(id + ","); + String s=""; + if (split.length > 1){ + s = split[1]; + } + String filepath = informationById.getFilePath()+id+","+s; + //修改文件路径 + informationMapper.updateFilePath(v.getId(),filepath,userInfo.getUserId()); + }); + //修改文件父级 + informationMapper.updateFilePId(id,fileQueryDto.getToInformationId(),userInfo.getUserId()); + } + + @Override + public PageInfo relatedToMeFilePage(FileQueryDto fileQueryDto) { + //参数处理 如果是全部处理为空 + if("全部".equals(fileQueryDto.getFileType())){fileQueryDto.setFileType(null);} + //得到企业下所有被标记为删除的id集合 + List enabledMarkIds = informationMapper.getEnabledMarkInformation(); + PageHelper.startPage(fileQueryDto.getCurrentPage(), fileQueryDto.getPageSize()); + PageInfo info = new PageInfo<>(informationMapper.relatedToMeFile(fileQueryDto,enabledMarkIds)); + setOwnerName(info); + return info; + } + + @Override + public PageInfo recentlyViewedPage(FileQueryDto fileQueryDto) { + spaceService.deleteOverdueViewRecord(); + //参数处理 如果是全部处理为空 + if("全部".equals(fileQueryDto.getFileType())){fileQueryDto.setFileType(null);} + UserInfo userInfo = userProvider.get(); + //验证是否是超级管理员 +// Integer userModuleDataAuthorizesChem = userApi.getUserModuleDataAuthorizesChem("resource", userInfo.getUserId(), "information_admin"); + //得到企业下所有被标记为删除的id集合 + List enabledMarkIds = informationMapper.getEnabledMarkInformation(); + if (userInfo.getIsAdministrator()){ + //超级管理员拥有最高权限 跳过验证 看所有的不管是不是成员 + PageHelper.startPage(fileQueryDto.getCurrentPage(), fileQueryDto.getPageSize()); + PageInfo info = new PageInfo<>(informationMapper.recentlyViewed(fileQueryDto,enabledMarkIds)); + setOwnerName(info); + return info; + } + PageHelper.startPage(fileQueryDto.getCurrentPage(), fileQueryDto.getPageSize()); + PageInfo info = new PageInfo<>(informationMapper.recentlyViewedMember(fileQueryDto,enabledMarkIds)); + setOwnerName(info); + return info; + + } + + private void setOwnerName(PageInfo info) { + if(null != info.getList() && info.getList().size() > 0){ + //赋值拥有者名称 + List userIds = info.getList().stream().map(InformationVo::getCreatorUserId).collect(Collectors.toList()); + // 查询成员信息 +// List list = userApi.getInfoByIds(userIds); +// Map map = list.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, null); + Map map = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + } + Map finalMap = map; + info.getList().forEach(v->{ +// PartUserInfoVo partUserInfoVo = map.get(v.getCreatorUserId()); + UserBoundVO partUserInfoVo = finalMap.get(v.getCreatorUserId()); + if (null != partUserInfoVo){ + v.setCreatorName(partUserInfoVo.getUserName()); + } + }); + } + } + + private void setOwnerNames(List informationVos) { + if(null != informationVos && informationVos.size() > 0){ + //赋值拥有者名称 + List userIds = informationVos.stream().map(InformationVo::getCreatorUserId).collect(Collectors.toList()); + // 查询成员信息 +// List list = userApi.getInfoByIds(userIds); +// Map map = list.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, Function.identity())); + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, null); + Map map = new HashMap<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + map = userList.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, a -> a, (k1, k2) -> k1)); + } + Map finalMap = map; + informationVos.forEach(v->{ +// PartUserInfoVo partUserInfoVo = map.get(v.getCreatorUserId()); + UserBoundVO partUserInfoVo = finalMap.get(v.getCreatorUserId()); + if (null != partUserInfoVo){ + v.setCreatorName(partUserInfoVo.getUserName()); + } + }); + } + } + + @Override + public Integer exitSpace(String id) { + //如果我是空间拥有者 不能退出 + UserInfo userInfo = userProvider.get(); + InformationVo informationById = informationMapper.getInformationById(id); + if (informationById.getOwner().equals(userInfo.getUserId())){ + return -1; + } + informationAuthorityMapper.deleteByUserAndId(id,userInfo.getUserId()); + return 0; + } + + @Override + public void editPermissionsBatch(String id, FileQueryDto fileQueryDto) { + String userId = userProvider.get().getUserId(); + fileQueryDto.getInformationAuthorityVos().forEach(v->{ + informationAuthorityMapper.updateById(v.getId(),v.getLimitsOfAuthority(),userId); + }); + } + + @Override + public void removeUserBatch(List informationAuthorityVos) { + if (null != informationAuthorityVos && informationAuthorityVos.size() >0){ + informationAuthorityVos.forEach(v->{ + informationAuthorityMapper.deleteById(v.getId()); + }); + } + } + + @Override + public void membersBatch(SpaceDto spaceDto) { + UserInfo userInfo = userProvider.get(); + //绑定空间成员 + spaceDto.getUserList().forEach(v->v.setId(FtbUtil.getId())); + informationAuthorityMapper.addUserAuthorityList(spaceDto.getId(),userInfo.getUserId(),spaceDto.getUserList()); + //新增日志 + InformationVo informationVo = informationMapper.getInformationById(spaceDto.getId()); + // + List userIds = spaceDto.getUserList().stream().map(InformationAuthorityDto::getUserId).collect(Collectors.toList()); + // 查询成员信息 +// List list = userApi.getInfoByIds(userIds); +// List userNames = list.stream().map(PartUserInfoVo::getRealName).collect(Collectors.toList()); + + ActionResult> userList = v2UserApi.getAllUserInfoBatch(userIds, null); + List userNames = new ArrayList<>(); + if (200 == userList.getCode() && CollectionUtil.isNotEmpty(userList.getData())) { + userNames = userList.getData().stream().map(UserBoundVO::getUserName).collect(Collectors.toList()); + } + addInformationLog(spaceDto.getId(),informationVo.getClassification(),userInfo.getUserId(),userInfo.getUserName(),"邀请",userNames); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/SpaceServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/SpaceServiceImpl.java new file mode 100644 index 0000000..607c5d1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/doclibrary/service/impl/SpaceServiceImpl.java @@ -0,0 +1,775 @@ +package jnpf.doclibrary.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.xuyanwu.spring.file.storage.FileInfo; +import cn.xuyanwu.spring.file.storage.MockMultipartFile; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.doclibrary.mapper.InformationAuthorityMapper; +import jnpf.doclibrary.mapper.SpaceMapper; +import jnpf.doclibrary.service.DocLibraryService; +import jnpf.doclibrary.service.SpaceService; +import jnpf.entity.Information; +import jnpf.entity.InformationRubbish; +import jnpf.exception.HandleException; +import jnpf.file.FileUploadApi; +import jnpf.model.doclibrary.dto.*; +import jnpf.model.doclibrary.vo.*; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 共享空间服务实现 + * + * @author yanwenfu + * @create 2023-07-20 + */ +@Slf4j +@Service +public class SpaceServiceImpl implements SpaceService { + + @Resource + private SpaceMapper spaceMapper; + + @Resource + private InformationAuthorityMapper informationAuthorityMapper; + + @Resource + private DocLibraryService docLibraryService; + + @Resource + private UserProvider userProvider; + + @Autowired + private UserApi userApi; + @Autowired + private V2UserApi v2UserApi; + + @Autowired + private FileUploadApi fileUploadApi; + + @Override + public List getMyResourceList(String pid) { + + List ids = getIds(pid); + // 根据ids查询资源信息 + if (ids.isEmpty()) { + return new ArrayList<>(); + } + List list = spaceMapper.getInformationList(new ArrayList<>(ids), "", ""); + if (!list.isEmpty()) { + setCreateUserName(list); + setInformationSize(list); + } + return list; + } + + private List getIds(String pid) { + + String userId = userProvider.get().getUserId(); +// Integer authorize = userApi.getUserModuleDataAuthorizesChem("resource", userId, "information_admin"); + List ids; + if (UserProvider.getUser().getIsAdministrator()) { + // 超级管理员查看全部 + ids = spaceMapper.getAllByPid(pid); + } else { + ids = getMyResourceCommon(pid); + } + return ids; + } + + private List getMyResourceCommon(String pid) { + + // 1.本身拥有权限的空间/文件夹/文件,本身的权限优先级最高 + // 2.拥有权限的层级 向下覆盖 所有不是成员的文件/文件夹的权限 + // 3.拥有权限的层级 向上查看 查看完整路径有无自己有权限的,有则向下覆盖 + // 4.拥有权限的层级 向上查看 查看完整路径有无自己有权限的,无则只能查看 + + String loginUserId = userProvider.get().getUserId(); + List infoIds; + if (pid.equals("0")) { + infoIds = spaceMapper.getMyRootInfoIdByPath(loginUserId); + } else { + // 父级id + String infoPath = spaceMapper.getInformationPath(pid); + String[] infoIdArray = infoPath.split(","); + // 子级id + List ids = spaceMapper.getChildrenInfoList(pid); + ids.addAll(Arrays.asList(infoIdArray)); + // 查询存在pid的所有path中我拥有权限的informationId + infoIds = spaceMapper.getMyInfoIdByPath(loginUserId, ids); + } + if (infoIds.isEmpty()) { + return new ArrayList<>(); + } + // 查出完整路径中包含infoIds的并且我拥有权限的文件/文件夹/空间 + List pathList = spaceMapper.getMyResourcePath(infoIds); + // 根据查出来的路径, 找到路径中pid的下一级 + Set ids = new HashSet<>(); + pathList.forEach(path -> { + String[] split = path.split(","); + List strList = new ArrayList<>(Arrays.asList(split)); + int index = strList.indexOf(pid); + if (index != strList.size() - 1) { + ids.add(strList.get(index + 1)); + } + }); + return new ArrayList<>(ids); + } + + @Override + public PageInfo getMyResourcePage(String pid, InformationQueryDto queryDto) { + + List ids = getIds(pid); + // 根据ids查询资源信息 + if (ids.isEmpty()) { + return new PageInfo<>(); + } + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(spaceMapper.getInformationList(new ArrayList<>(ids), queryDto.getQueryCondition(), queryDto.getQueryType())); + if (page.getTotal() > 0) { + // 查询用户信息 + setCreateUserName(page.getList()); + setInformationSize(page.getList()); + } + return page; + } + + private void setInformationSize(List list) { + + list.forEach(v -> { + if (v.getClassification().equals(ConstantUtil.FILE_SPACE) || v.getClassification().equals(ConstantUtil.FILE_FOLDER)) { + // 查询路径中被删除的部分 + List delIds = spaceMapper.getDeletedFileIds(v.getId()); + // 查询空间/文件夹/文件占用空间 + Double usedSpaceSize = spaceMapper.getUsedSpaceSize(v.getId(), delIds); + v.setFileSize(usedSpaceSize); + } + }); + } + + private void setCreateUserName(List list) { + + List userIds = list.stream().map(MiniInformationVo::getCreatorUserId).distinct().collect(Collectors.toList()); +// List userList = userApi.getInfoByIds(userIds); + ActionResult> userListResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + List userList = userListResult.getData(); + Map map = userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + list.forEach(v -> { + UserBoundVO user = map.get(v.getCreatorUserId()); + if (null != user) { + v.setCreatorUserName(user.getUserName()); + } + }); + } + + @Override + public SpaceVo getInformationSpace(String id) { + + // 查询企业空间总容量 + String jsonStr = spaceMapper.getCommonConfigByKey(ConstantUtil.CONFIG_SPACE); + SpaceVo space = new SpaceVo(); + space.setTotalSize(StringUtils.isEmpty(jsonStr) ? 0 : Integer.parseInt(jsonStr)); + // 查询路径中被删除的部分 + List delIds = spaceMapper.getDeletedFileIds(id); + // 查询空间/文件夹/文件占用空间 + Double usedSpaceSize = spaceMapper.getUsedSpaceSize(id, delIds); + space.setUsedSpaceSize(usedSpaceSize); + return space; + } + + @Override + public List getInformationMemberList(String id) { + + // 查询关联的成员 + List infoList = spaceMapper.getInformationMemberList(id); + if (infoList.isEmpty()) { + return new ArrayList<>(); + } + List userIds = infoList.stream().map(UserInfoVo::getUserId).collect(Collectors.toList()); + // 查询成员信息 +// List list = userApi.getInfoByIds(userIds); + ActionResult> userListResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + List list = userListResult.getData(); + Map map = list.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + infoList.forEach(v -> { + UserBoundVO user = map.get(v.getUserId()); + if (null != user) { + BeanUtils.copyProperties(user, v); + v.setHeadIcon(UploaderUtil.uploaderImg(v.getHeadIcon())); + } + }); + return infoList; + } + + @Override + public InformationDetailVo getInformation(String id) throws Exception { + + Set userIds = new HashSet<>(); + InformationDetailVo detail = spaceMapper.getInformation(id); + if (null == detail) { + throw new Exception("未找到记录"); + } + userIds.add(detail.getOwner()); + userIds.add(detail.getCreatorUserId()); + if (null != detail.getLastModifyUserId()) { + userIds.add(detail.getLastModifyUserId()); + } + // 查看占用空间 + SpaceVo space = getInformationSpace(id); + detail.setFileSize(space.getUsedSpaceSize()); + // 查询最近查看记录 + RecentlyViewedVo viewedVo = spaceMapper.getRecentlyViewInfo(id); + if (null != viewedVo) { + userIds.add(viewedVo.getUserId()); + detail.setLastViewUserId(viewedVo.getUserId()); + detail.setLastViewTime(viewedVo.getLastModifyTime()); + } +// List userList = userApi.getInfoByIds(new ArrayList<>(userIds)); + ActionResult> userListResult = v2UserApi.getUserPrimaryBoundBatch(new ArrayList<>(userIds), UserProvider.getUser().getTenantId()); + List userList = userListResult.getData(); + Map map = userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + UserBoundVO user = map.get(detail.getCreatorUserId()); + if (null != user) { + detail.setCreatorUserName(user.getUserName()); + } + UserBoundVO owner = map.get(detail.getOwner()); + if (null != owner) { + detail.setOwnerName(owner.getUserName()); + } + UserBoundVO updateUser = map.get(detail.getLastModifyUserId()); + if (null != detail.getLastModifyUserId() && null != updateUser) { + detail.setLastModifyUserName(updateUser.getUserName()); + } + if (null != viewedVo) { + UserBoundVO viewUser = map.get(viewedVo.getUserId()); + if (null != viewUser) { + detail.setLastViewUserName(viewUser.getUserName()); + } + } + return detail; + } + + @Override + public void addViewRecord(ViewDto viewDto) { + + UserInfo userInfo = userProvider.get(); + // 查看浏览记录是否存在 + RecentlyViewedVo viewedVo = spaceMapper.getRecentlyViewByUser(userInfo.getUserId(), viewDto.getInformationId()); + if (null != viewedVo) { + // 存在 -> 更新 + spaceMapper.updateRecentlyViewInfo(viewedVo.getId()); + } else { + // 不存在 -> 新增 + spaceMapper.addRecentlyViewRecord(FtbUtil.getId(), viewDto.getInformationId(), userInfo.getUserId()); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public List getRubbishList(Integer queryType, String queryCondition) { + + String userId = getUserId(); + // 删除过期的回收站数据 + deleteOverdueRubbish(userId); + List list = spaceMapper.getRubbishList(queryType, queryCondition, userId); + if (list.isEmpty()) { + return new ArrayList<>(); + } + setDaysRemaining(list); + setSpaceName(list); + return list; + } + + private String getUserId() { + UserInfo userInfo = userProvider.get(); +// Integer authorize = userApi.getUserModuleDataAuthorizesChem("resource", userInfo.getUserId(), "information_admin"); +// return authorize > 0 ? null : userInfo.getUserId(); + return userInfo.getIsAdministrator() ? null : userInfo.getUserId(); + } + + private void setSpaceName(List list) { + + Set set = new HashSet<>(); + // 所有的空间id + list.forEach(v -> { + String spaceId = v.getFilePath().split(",")[1]; + v.setSpaceId(spaceId); + set.add(spaceId); + }); + // 查询空间名称 + List spaceList = spaceMapper.getSpaceNameList(new ArrayList<>(set)); + Map map = spaceList.stream().collect(Collectors.toMap(SpaceInfoVo::getId, Function.identity())); + list.forEach(v -> { + SpaceInfoVo info = map.get(v.getSpaceId()); + if (null != info) { + v.setSpaceName(info.getSpaceName()); + } + }); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public PageInfo getRubbishPage(RubbishQueryDto queryDto) { + + String userId = getUserId(); + // 删除过期的回收站数据 + deleteOverdueRubbish(userId); + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(spaceMapper.getRubbishList(queryDto.getQueryType(), queryDto.getQueryCondition(), userId)); + if (page.getList().isEmpty()) { + return page; + } + setDaysRemaining(page.getList()); + setSpaceName(page.getList()); + return page; + } + + private void setDaysRemaining(List list) { + + // 计算在回收站中的过期时间 + String json = spaceMapper.getCommonConfigByKey(ConstantUtil.CONFIG_RUBBISH_EXPIRE); + if (StringUtils.isNotEmpty(json)) { + int expireDay = Integer.parseInt(json); + Date today = new Date(); + list.forEach(v -> { + int diff = FtbUtil.dateDiff(v.getCreatorTime(), today); + v.setDaysRemaining(expireDay - diff); + }); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteRubbish(String rubbishId) { + + RubbishInfoVo rubbish = spaceMapper.getRubbishById(rubbishId); + // 根据path查询所有要删除的项 + List infoIds = spaceMapper.getInformationByPath(rubbish.getInformationId()); + // 删除ftb_information_rubbish记录 + spaceMapper.deleteRubbishByInfoIds(infoIds); + // 删除ftb_information_recently_viewed + spaceMapper.deleteViewByInfoIds(infoIds); + // 删除ftb_information_authority + spaceMapper.deleteAuthorityByInfoIds(infoIds); + // 删除ftb_information记录 + spaceMapper.deleteInformationByInfoIds(infoIds); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void updateToUndelete(UndeleteDto undeleteDto) throws HandleException { + + // 查询回收站记录 + RubbishInfoVo rubbish = spaceMapper.getRubbishById(undeleteDto.getRubbishId()); + String userId = userProvider.get().getUserId(); + // 判断是否可以直接恢复, 不可以直接恢复, 恢复到空间下, 空间被删除, 无法恢复 + CheckRubbishVo vo = checkUndelete(rubbish.getRubbishId()); + if (vo.isResult() && StringUtils.isEmpty(vo.getMessage())) { + // 直接恢复 + spaceMapper.updateToUndeleteById(rubbish.getInformationId(), userId); + } else if (vo.isResult() && StringUtils.isNotEmpty(vo.getMessage())) { + // 查询空间id + InformationDetailVo information = spaceMapper.getInformation(rubbish.getInformationId()); + String spaceId = information.getFilePath().split(",")[1]; + // 生成新的path + InformationDetailVo spaceInfo = spaceMapper.getInformation(spaceId); + String newPath = spaceInfo.getFilePath() + information.getId() + ","; + // 恢复到空间下 + spaceMapper.updateUndeleteToSpace(information.getId(), spaceId, newPath, userId); + } else { + // 无法恢复 + throw new HandleException(vo.getMessage()); + } + // 删除回收站记录 + spaceMapper.deleteRubbishRecord(Stream.of(undeleteDto.getRubbishId()).collect(Collectors.toList())); + } + + @Override + public CheckRubbishVo checkUndelete(String rubbishId) { + + // 查询回收站信息 + RubbishInfoVo rubbish = spaceMapper.getRubbishById(rubbishId); + // 查询空间已用大小 + SpaceVo spaceUse = getInformationSpace("0"); + // 查询回收站资源已用大小 + Double rubbishUse = spaceMapper.getRubbishUsedSpace(rubbish.getInformationId()); + if (FtbUtil.checkFileSize(rubbishUse + spaceUse.getUsedSpaceSize(), spaceUse.getTotalSize(), ConstantUtil.UNIT_G)) { + return new CheckRubbishVo(false, "空间已满,无法恢复。如需要恢复,请删除部分文档/文件"); + } + // 查询资源信息 + InformationDetailVo information = spaceMapper.getInformation(rubbish.getInformationId()); + if (information.getClassification().equals(ConstantUtil.FILE_SPACE)) { + return new CheckRubbishVo(true, ""); + } + // 查看资源path删除情况 + List list = spaceMapper.getDeleteInfoByPath(information.getFilePath()); + DeleteInfoVo info = list.stream() + .filter(v -> v.getEnabledMark().equals(ConstantUtil.NUM_TRUE) && !v.getInformationId().equals(rubbish.getInformationId())) + .findFirst().orElse(null); + if (null == info) { + // 路径上没有其他节点被删除, 直接恢复 + return new CheckRubbishVo(true, ""); + } else { + if (list.get(0).getEnabledMark().equals(ConstantUtil.NUM_TRUE)) { + // 路径上有被删除的, 且空间被删除, 无法恢复 + return new CheckRubbishVo(false, "原所在空间已被删除,无法恢复"); + } else { + // 路径上有被删除的, 空间没被删除, 恢复到空间下 + return new CheckRubbishVo(true, "原所在文件夹已被删除,将恢复至 所在共享空间 的根目录"); + } + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Information addInformationFile(UploadDto uploadDto) throws Exception { + + String filePath; + switch (uploadDto.getFileType()) { + case ConstantUtil.FILE_TYPE_DOC: + filePath = "/jnpf/doclibrary/document/空白文档.docx"; + break; + case ConstantUtil.FILE_TYPE_XLS: + filePath = "/jnpf/doclibrary/document/空白表格.xlsx"; + break; + case ConstantUtil.FILE_TYPE_PPT: + filePath = "/jnpf/doclibrary/document/空白PPT.pptx"; + break; + default: + throw new HandleException("文件类型错误"); + } + String tmpName = filePath.substring(filePath.lastIndexOf("/") + 1, filePath.lastIndexOf(".")); + String tmpPath = System.getProperty("java.io.tmpdir") + filePath.substring(1); + String uuid = RandomUtil.uuId(); + String suffix = filePath.substring(filePath.lastIndexOf(".") + 1); + String fileName = uuid + "." + suffix; + tmpPath = tmpPath.replace(tmpName, uuid); + File file = new File(tmpPath); + InputStream inputStream = this.getClass().getResourceAsStream(filePath); + if (null == inputStream) { + throw new HandleException("文件读取失败"); + } + FileUtils.copyInputStreamToFile(inputStream, file); + inputStream.close(); + MultipartFile multiFile; + try { + multiFile = new MockMultipartFile(uuid, file.getName(), MediaType.MULTIPART_FORM_DATA_VALUE, new FileInputStream(file)); + } catch (FileNotFoundException e) { + throw new HandleException("文件未找到"); + } + String userId = userProvider.get().getUserId(); + String uploadPath = String.format(ConstantUtil.UPLOAD_FOLDER, userId); + FileInfo fileInfo = fileUploadApi.uploadFile(multiFile, uploadPath, fileName); + fileInfo.setUrl(fileInfo.getUrl().replace("/jnpf-resource-1304460613/", "")); + FileUtil.deleteFile(tmpPath); + // 生成ftb_information数据 + Information information = getInformationData(fileInfo, userId, suffix, tmpName, uploadDto); + executeSql(information, userId); + return information; + } + + private void executeSql(Information information, String userId) { + + // 新增资料库记录 + spaceMapper.addInformation(information); + // 新增权限记录 + informationAuthorityMapper.addUserAuthority(FtbUtil.getId(), userId, information.getId(), 0, userId); + // 新增日志 + docLibraryService.addInformationLog(information.getId(), information.getClassification(), userId, userProvider.get().getUserName(), "上传", null); + } + + @Override + public void singleFileUploads(HttpServletRequest request, HttpServletResponse response) throws Exception { + + log.info("进入回调..."); + PrintWriter writer = response.getWriter(); + + Scanner scanner = new Scanner(request.getInputStream(), "GBK").useDelimiter("\\A"); + String body = scanner.hasNext() ? scanner.next() : ""; + JSONObject jsonObj = new JSONObject(body); + log.error("body:{}", jsonObj); + if (Integer.parseInt(jsonObj.get("status").toString()) == 2 || Integer.parseInt(jsonObj.get("status").toString()) == 6) { + + String downloadUri = (String) jsonObj.get("url"); + URL url = new URL(downloadUri); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + InputStream inputStream = connection.getInputStream(); + // 前端加了"_"以获取最新数据 + String id = request.getParameter("id"); + if (StringUtil.isEmpty(id)) { + try { + String key = jsonObj.get("key").toString(); + id = key.split("_")[0]; + } catch (Exception e) { + throw new HandleException("获取资料库id失败..."); + } + } + if (StringUtil.isEmpty(id)) { + throw new HandleException("获取资料库id失败..."); + } + InformationDetailVo information = spaceMapper.getInformation(id); + if (null == information) { + throw new HandleException("查询资料库数据失败..."); + } + String dbUrl = information.getFileUrl(); + log.info("数据库:{}", dbUrl); + // 上传到腾讯云 路径覆盖 + String fileName = dbUrl.substring(dbUrl.lastIndexOf("/") + 1); + MultipartFile multiFile = new MockMultipartFile(fileName.substring(0, fileName.lastIndexOf(".")), fileName, MediaType.MULTIPART_FORM_DATA_VALUE, inputStream); + inputStream.close(); + connection.disconnect(); + log.info("multiFile: {}, fileName: {}", multiFile.getName(), fileName); + // 判断文件大小 + SpaceVo space = getInformationSpace("0"); + if (FtbUtil.checkFileSize((double) multiFile.getSize() + space.getUsedSpaceSize(), space.getTotalSize(), ConstantUtil.UNIT_G)) { + log.error("空间已满,编辑失败。如需要编辑,请删除部分文档/文件"); + writer.write("{\"error\":-1, \"message\":\"空间已满,编辑失败。如需要编辑,请删除部分文档/文件\"}"); + return; + } + String[] split = dbUrl.split("/"); + StringBuilder sb = new StringBuilder(); + for (int i = split.length - 1; i > 0; i--) { + if (!split[i].equals("jnpf-resources")) { + sb.insert(0, split[i]).insert(0, "/"); + } else { + sb.replace(0, 1, ""); + break; + } + } + log.info(sb.toString()); + String uploadPath = sb.toString().replaceAll(fileName, ""); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multiFile, uploadPath, fileName); + if (null == fileInfo) { + log.info("上传文件失败..."); + writer.write("{\"error\":-1}"); + return; + } + log.info("返回路径: {}", fileInfo.getUrl()); + // 生成修改日志 + Object users = jsonObj.get("users"); + JSONArray array = new JSONArray(users); + List userIds = array.toList(String.class); + // 修改文件大小 + spaceMapper.updateFileSize(information.getId(), fileInfo.getSize(), userIds.get(0)); + String tenantId = request.getParameter("tenantId"); + log.error(tenantId); + ActionResult> userResult = v2UserApi.getUserPrimaryBoundBatch(userIds, tenantId); + List userList = userResult.getData(); + if (null != userList && !userList.isEmpty()) { + for (UserBoundVO user : userList) { + docLibraryService.addInformationLog(information.getId(), information.getClassification(), user.getId(), user.getUserName(), "编辑", null); + } + } + jsonObj.clear(); + writer.write("{\"error\":0}"); + } else if (Integer.parseInt(jsonObj.get("status").toString()) == 3 || Integer.parseInt(jsonObj.get("status").toString()) == 7) { + jsonObj.clear(); + writer.write("{\"error\":-1}"); + } else { + writer.write("{\"error\":0}"); + } + } + + @Override + public FileInfo uploadFile(MultipartFile multipartFile, String fileName) { + + String userId = userProvider.get().getUserId(); + String uploadPath = String.format(ConstantUtil.UPLOAD_FOLDER, userId); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multipartFile, uploadPath, StringUtils.isEmpty(fileName) ? multipartFile.getName() : fileName); + fileInfo.setUrl(fileInfo.getUrl().replace("/jnpf-resource-1304460613/", "")); + fileInfo.setFilename(fileInfo.getFilename().substring(0, fileInfo.getFilename().lastIndexOf("."))); + return fileInfo; + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addInformationDetails(List list) { + + UserInfo userInfo = userProvider.get(); + list.forEach(info -> { + Information information = getInfoByDetailDto(info, userInfo); + executeSql(information, userInfo.getUserId()); + }); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void deleteOverdueRubbish(String userId) { + + // 查询配置的有效天数 + String keepDay = spaceMapper.getCommonConfigByKey(ConstantUtil.CONFIG_RUBBISH_EXPIRE); + // 查询过期的垃圾文件 + List list = spaceMapper.getExpireRubbish(keepDay, userId); + if (list.isEmpty()) { + return; + } + List collect = list.stream().map(InformationRubbish::getInformationId).collect(Collectors.toList()); + // 根据path查询所有要删除的项 + List infoIds = spaceMapper.getInformationByIdsPath(collect); + // 删除ftb_information_rubbish记录 + spaceMapper.deleteRubbishByInfoIds(infoIds); + // 删除ftb_information_recently_viewed + spaceMapper.deleteViewByInfoIds(infoIds); + // 删除ftb_information_authority + spaceMapper.deleteAuthorityByInfoIds(infoIds); + // 删除ftb_information记录 + spaceMapper.deleteInformationByInfoIds(infoIds); + } + + @Override + public void deleteOverdueViewRecord() { + + int i = spaceMapper.deleteOverdueViewRecord(); + log.info("删除了{}条记录", i); + } + + private Information getInfoByDetailDto(InfoDetailDto info, UserInfo userInfo) { + + Information information = new Information(); + information.setId(FtbUtil.getId()); + information.setFileName(info.getFileName()); + information.setFilesize(Double.longBitsToDouble(Double.doubleToLongBits(info.getFileSize()))); + information.setFileUrl(info.getFileUrl()); + information.setOwner(userInfo.getUserId()); + information.setFileSuffix(info.getFileSuffix()); + information.setClassification(info.getClassification()); + information.setFileType(info.getFileType()); + String path = getFIlePath(info.getFilePid(), information.getId()); + information.setFilePath(path); + information.setFilePid(info.getFilePid()); + information.setCreatorUserId(userInfo.getUserId()); + information.setLastModifyUserId(userInfo.getUserId()); + return information; + } + + private Information getInformationData(FileInfo fileInfo, String userId, String suffix, String fName, UploadDto uploadDto) { + + Information info = new Information(); + info.setId(FtbUtil.getId()); + info.setFileName(fName); + info.setFilesize(FtbUtil.getFileSize(fileInfo.getSize())); + info.setFileUrl(fileInfo.getUrl()); + info.setOwner(userId); + info.setFileSuffix(suffix); + info.setClassification(3); + info.setFileType(uploadDto.getFileType()); + String pid = uploadDto.getPid(); + String path = getFIlePath(pid, info.getId()); + info.setFilePath(path); + info.setFilePid(pid); + info.setCreatorUserId(userId); + info.setLastModifyUserId(userId); + return info; + } + + private String getFIlePath(String pid, String id) { + + String path; + if (pid.equals("0")) { + path = "0," + id + ","; + } else { + InformationDetailVo information = spaceMapper.getInformation(pid); + path = information.getFilePath() + id + ","; + } + return path; + } + + + /** + * 查询我的资源文件夹 + * + * @param folderName 文件夹名称 + * @return 文件夹列表 + */ + @Override + public List queryMyResourceFolder(String folderName) { + List powerInfoIds = new ArrayList<>(); + UserInfo user = UserProvider.getUser(); + if (!user.getIsAdministrator()) { + powerInfoIds = spaceMapper.queryAllPowerInfoIds(user.getUserId()); + if (powerInfoIds.isEmpty()) { + return new ArrayList<>(); + } + } + List list = spaceMapper.queryMyResourceFolder(folderName, powerInfoIds); + if (CollectionUtil.isNotEmpty(list)) { + return buildTree(list); + } + return list; + } + + /** + * 查询我的文件 + * + * @param req 参数 + * @return 分页返回文件列表 + */ + @Override + public PageInfo queryMyFilesForFolderId(QueryMyFilesReq req) { + + PageHelper.startPage(req.getCurrentPage(), req.getPageSize()); + return new PageInfo<>(spaceMapper.queryMyFilesForFolderId(req)); + + } + + public static List buildTree(List list) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + + // 1. 创建 Map 用于快速查找节点 + Map nodeMap = new HashMap<>(); + for (MiniInformationTreeVo node : list) { + nodeMap.put(node.getId(), node); + } + + // 2. 构建树结构 + List rootNodes = new ArrayList<>(); + for (MiniInformationTreeVo node : list) { + String parentId = node.getFilePid(); + if ("0".equals(parentId) || parentId == null) { + // 根节点直接加入结果集 + rootNodes.add(node); + } else { + // 查找父节点并添加当前节点为其子节点 + MiniInformationTreeVo parent = nodeMap.get(parentId); + if (parent != null) { + if (parent.getChildren() == null) { + parent.setChildren(new ArrayList<>()); + } + parent.getChildren().add(node); + } + } + } + + return rootNodes; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ApproveException.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ApproveException.java new file mode 100644 index 0000000..8e808f1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ApproveException.java @@ -0,0 +1,16 @@ +package jnpf.exception; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/12/28 + */ +public class ApproveException extends Exception { + public ApproveException() { + } + + public ApproveException(String message) { + super(message); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/HandleException.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/HandleException.java new file mode 100644 index 0000000..07d571c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/HandleException.java @@ -0,0 +1,17 @@ +package jnpf.exception; + +/** + * 操作异常 + * + * @author yanwenfu + * @create 2021-04-26 + */ +public class HandleException extends Exception { + + public HandleException() { + } + + public HandleException(String message) { + super(message); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/QueryException.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/QueryException.java new file mode 100644 index 0000000..a6c2a8c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/QueryException.java @@ -0,0 +1,17 @@ +package jnpf.exception; + +/** + * 查询异常 + * + * @author yanwenfu + * @create 2021-10-19 + */ +public class QueryException extends Exception { + + public QueryException() { + } + + public QueryException(String message) { + super(message); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ResultException.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ResultException.java new file mode 100644 index 0000000..6267c33 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/exception/ResultException.java @@ -0,0 +1,94 @@ +package jnpf.exception; + +import cn.dev33.satoken.exception.IdTokenInvalidException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; +import jnpf.base.ActionResult; +import jnpf.base.ActionResultCode; +import jnpf.util.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RestControllerAdvice +public class ResultException { + @ResponseBody + @ExceptionHandler(value = LoginException.class) + public ActionResult loginException(LoginException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getMessage()); + } + + @ResponseBody + @ExceptionHandler(value = ImportException.class) + public ActionResult loginException(ImportException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getMessage()); + } + + @ResponseBody + @ExceptionHandler(value = DataException.class) + public ActionResult dataException(DataException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getMessage()); + } + + @ResponseBody + @ExceptionHandler(value = RuntimeException.class) + public ActionResult dataException(RuntimeException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getMessage()); + } + + @ResponseBody + @ExceptionHandler(value = IllegalArgumentException.class) + public ActionResult dataException(IllegalArgumentException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), e.getMessage()); + } + + + @ResponseBody + @ExceptionHandler(value = WorkFlowException.class) + public ActionResult workFlowException(WorkFlowException e) { + if (e.getCode() == 200) { + List> list = JsonUtil.getJsonToListMap(e.getMessage()); + return ActionResult.success(list); + } else { + return ActionResult.fail(e.getMessage()); + } + } + + @ResponseBody + @ExceptionHandler(value = WxErrorException.class) + public ActionResult wxErrorException(WxErrorException e) { + return ActionResult.fail(e.getError().getErrorCode(), "操作过于频繁"); + } + + @ResponseBody + @ExceptionHandler(NotPermissionException.class) + public ActionResult handleNotPermissionException(NotPermissionException e) { + return ActionResult.fail(ActionResultCode.Fail.getCode(), "没有访问权限,请联系管理员授权"); + } + + @ResponseBody + @ExceptionHandler(NotRoleException.class) + public ActionResult handleNotRoleException(NotRoleException e) { + return ActionResult.fail(ActionResultCode.ValidateError.getCode(), "没有访问权限,请联系管理员授权"); + } + + @ResponseBody + @ExceptionHandler(NotLoginException.class) + public ActionResult handleNotLoginException(NotLoginException e) { + return ActionResult.fail(ActionResultCode.SessionOverdue.getCode(), "认证失败,无法访问系统资源"); + } + + @ResponseBody + @ExceptionHandler(IdTokenInvalidException.class) + public ActionResult handleIdTokenInvalidException(IdTokenInvalidException e) { + return ActionResult.fail(ActionResultCode.SessionOverdue.getCode(), "无效内部认证,无法访问系统资源"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeConsumerSource.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeConsumerSource.java new file mode 100644 index 0000000..a4a618d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeConsumerSource.java @@ -0,0 +1,17 @@ +package jnpf.franchisee.consumer; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +/** + * 加盟商消息通道定义 + */ +public interface FranchiseeConsumerSource { + /** + * 消费通道 + */ + String INPUT = "permission-franchisee-input"; + + @Input(INPUT) + SubscribableChannel input(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeStoreNumConsumer.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeStoreNumConsumer.java new file mode 100644 index 0000000..7313387 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/consumer/FranchiseeStoreNumConsumer.java @@ -0,0 +1,239 @@ +package jnpf.franchisee.consumer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.franchisee.mapper.FranchiseeMapper; +import jnpf.message.enums.permission.v2.OperationTypeMessageEnums; +import jnpf.message.enums.permission.v2.OrganizeCategoryMessageEnums; +import jnpf.message.model.permission.v2.OrganizeUpdateMessageDTO; +import jnpf.model.franchisee.po.FranchiseeEntity; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 加盟商门店数量写扩散消费者 + */ +@Slf4j +@Component +@EnableBinding(FranchiseeConsumerSource.class) +public class FranchiseeStoreNumConsumer { + + private static final String IDEMPOTENT_KEY_PREFIX = "franchisee:storeNum:consume"; + private static final String IDEMPOTENT_VALUE = "1"; + private static final long IDEMPOTENT_EXPIRE_DAYS = 1L; + + @Resource + private FranchiseeMapper franchiseeMapper; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + @Lazy + private ConfigValueUtil configValueUtil; + + /** + * 监听组织消息中的门店更新事件,执行加盟商门店数量写扩散。 + *

+ * 幂等与并发控制在上层消费入口处理:使用消息头 ROCKET_KEYS 作为 setnx 键,过期时间 1 天。 + * 如果缓存过期后发生重复消费导致数量偏差,会在“查询加盟商门店数量接口”按门店表实时修正。 + * + * @param message 消息体 + */ + @StreamListener(target = FranchiseeConsumerSource.INPUT, condition = "headers['ROCKET_TAGS'] == 'TAG_ORGANIZE'") + public void receiveStoreFranchiseeChange(Message message) { + String payload = message.getPayload(); + if (StrUtil.isBlank(payload)) { + return; + } + + MessageHeaders headers = message.getHeaders(); + String rocketKeys = headers.get("ROCKET_KEYS", String.class); + if (StrUtil.isBlank(rocketKeys)) { + log.warn("加盟商门店数量写扩散消息缺少ROCKET_KEYS,跳过处理。payload={}", payload); + return; + } + + String idempotentKey = IDEMPOTENT_KEY_PREFIX + ":" + rocketKeys; + Boolean firstConsume = stringRedisTemplate.opsForValue() + .setIfAbsent(idempotentKey, IDEMPOTENT_VALUE, IDEMPOTENT_EXPIRE_DAYS, TimeUnit.DAYS); + if (!Boolean.TRUE.equals(firstConsume)) { + return; + } + + List messageList; + try { + messageList = JSONUtil.toList(JSONUtil.parseArray(payload), OrganizeUpdateMessageDTO.class); + } catch (Exception e) { + log.error("加盟商门店数量写扩散消息解析失败,payload={}", payload, e); + return; + } + if (CollUtil.isEmpty(messageList)) { + return; + } + + for (OrganizeUpdateMessageDTO item : messageList) { + try { + processStoreUpdate(item); + } catch (Exception e) { + log.error("处理加盟商门店数量写扩散消息失败,item={}", JSONUtil.toJsonStr(item), e); + } + } + } + + /** + * 处理单条门店更新消息 + * + * @param item 消息项 + */ + private void processStoreUpdate(OrganizeUpdateMessageDTO item) { + if (item == null) { + return; + } + if (!OrganizeCategoryMessageEnums.STORE.equals(item.getOrganizeCategoryEnum())) { + return; + } + log.error("处理加盟商门店数量写扩散消息,item={}", JSONUtil.toJsonStr(item)); + OperationTypeMessageEnums operationType = item.getOperationTypeEnum(); + if (operationType == null) { + return; + } + + String tenantId = StrUtil.trim(item.getTenantId()); + if (StrUtil.isBlank(tenantId)) { + return; + } + String oldFranchiseeId = StrUtil.trim(item.getOldFranchiseeId()); + JSONObject jsonObject = parseJsonEntity(item.getJsonEntity()); + String newFranchiseeId = jsonObject.getStr("franchiseeId"); + Date lastModifyTime = jsonObject.getDate("lastModifyTime");//使用门店表修改时间,作为事件水位线。 + if(Objects.isNull(lastModifyTime)){ + log.error("processStoreUpdate lastModifyTime is null.item:{}",JSONUtil.toJsonStr(item)); + return; + } + long updateStoreNumTimestamp = lastModifyTime.getTime(); + if (OperationTypeMessageEnums.ADD.equals(operationType)) { + if (StrUtil.isBlank(newFranchiseeId)) { + return; + } + switchTenant(tenantId); + updateStoreNum(newFranchiseeId, updateStoreNumTimestamp,1); + return; + } + + if (OperationTypeMessageEnums.DELETE.equals(operationType)) { + String deleteFranchiseeId = StrUtil.isNotBlank(oldFranchiseeId) ? oldFranchiseeId : newFranchiseeId; + if (StrUtil.isBlank(deleteFranchiseeId)) { + return; + } + switchTenant(tenantId); + updateStoreNum(deleteFranchiseeId, updateStoreNumTimestamp,-1); + return; + } + + if (OperationTypeMessageEnums.UPDATE.equals(operationType)) { + if (StrUtil.equals(oldFranchiseeId, newFranchiseeId)) { + return; + } + switchTenant(tenantId); + if (StrUtil.isNotBlank(newFranchiseeId)) { + updateStoreNum(newFranchiseeId, updateStoreNumTimestamp,1); + } + if (StrUtil.isNotBlank(oldFranchiseeId)) { + updateStoreNum(oldFranchiseeId, updateStoreNumTimestamp,-1); + } + } + } + + /** + * 解析消息中的新加盟商ID + * + * @param jsonEntity 门店JSON实体 + * @return 新加盟商ID + */ + private String parseNewFranchiseeId(String jsonEntity) { + if (StrUtil.isBlank(jsonEntity) || !JSONUtil.isTypeJSONObject(jsonEntity)) { + return null; + } + try { + JSONObject jsonObject = JSONUtil.parseObj(jsonEntity); + return StrUtil.trim(jsonObject.getStr("franchiseeId")); + } catch (Exception e) { + return null; + } + } + + private JSONObject parseJsonEntity(String jsonEntity) { + if (StrUtil.isBlank(jsonEntity) || !JSONUtil.isTypeJSONObject(jsonEntity)) { + return new JSONObject(); + } + try { + return JSONUtil.parseObj(jsonEntity); + } catch (Exception e) { + return new JSONObject(); + } + } + + /** + * 写扩散更新加盟商门店数量 + * + * @param franchiseeId 加盟商ID + * @param updateStoreNumTimestamp 更新水位 + * @param delta 变更值(+1 或 -1) + */ + private void updateStoreNum(String franchiseeId,long updateStoreNumTimestamp, int delta) { + if (StrUtil.isBlank(franchiseeId) || delta == 0) { + return; + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FranchiseeEntity::getId, franchiseeId); + updateWrapper.eq(FranchiseeEntity::getEnabledMark, 0); + updateWrapper.lt(FranchiseeEntity::getUpdateStoreNumTimestamp, updateStoreNumTimestamp);//作为事件水位线。 + if (delta > 0) { + updateWrapper.setSql("F_StoreNum = IFNULL(F_StoreNum, 0) + " + delta); + } else { + int abs = Math.abs(delta); + updateWrapper.setSql("F_StoreNum = CASE WHEN IFNULL(F_StoreNum, 0) >= " + + abs + " THEN IFNULL(F_StoreNum, 0) - " + abs + " ELSE 0 END"); + } + franchiseeMapper.update(null, updateWrapper); + } + + /** + * 切换租户数据源 + * + * @param tenantId 租户ID + */ + private void switchTenant(String tenantId) { + // 判断是否为多租户 + if (!configValueUtil.isMultiTenancy()) { + log.info("加盟商门店数量写扩散消息,配置为非多租户,跳过处理。tenantId={}", tenantId); + return; + } + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (Exception e) { + throw new RuntimeException("切换租户失败", e); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/FranchiseeApiController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/FranchiseeApiController.java new file mode 100644 index 0000000..bcb6daf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/FranchiseeApiController.java @@ -0,0 +1,86 @@ +package jnpf.franchisee.controller; + +import jnpf.franchisee.FranchiseeApi; +import jnpf.franchisee.service.FranchiseeService; +import jnpf.model.franchisee.vo.FranchiseeIdName; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.List; + + +@Slf4j +@Validated +@RestController +@RequestMapping("/web/franchise-api") +public class FranchiseeApiController implements FranchiseeApi { + + @Autowired + private FranchiseeService franchiseeService; + + /** + * 获取所有加盟商信息 + * @return + */ + @Override + @GetMapping("/getFranchiseeIdNameList") + public List getFranchiseeIdNameList() { + return franchiseeService.queryIdListByPrefix(null); + } + + /** + * 根据ID列表获取加盟商信息 + * @param ids + * @return + */ + @Override + public List getFranchiseeIdNameListByIds(Collection ids) { + return franchiseeService.getFranchiseeIdNameListByIds(ids); + } + + /** + * 根据ID获取加盟商信息 + * @param id + * @return + */ + @Override + public FranchiseeIdName getFranchiseeIdNameListById(String id) { + return franchiseeService.getFranchiseeIdNameListById(id); + } + + /** + * 根据名称获取加盟商信息 + * @param name + * @return + */ + @Override + public List getFranchiseeIdNameListByName(String name) { + return franchiseeService.getFranchiseeIdNameListByName(name); + } + + /** + * 根据名称列表获取加盟商信息 + * @param names + * @return + */ + @Override + public List getFranchiseeIdNameListByNames(Collection names) { + return franchiseeService.getFranchiseeIdNameListByNames(names); + } + + @Override + public FranchiseeIdName getFranchiseeIdNameListByCode(String code) { + return franchiseeService.getFranchiseeIdNameListByCode(code); + } + + @Override + public List getFranchiseeIdNameListByCodes(Collection codes) { + return franchiseeService.getFranchiseeIdNameListByCodes(codes); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/app/FranchiseeAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/app/FranchiseeAppController.java new file mode 100644 index 0000000..1df051d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/app/FranchiseeAppController.java @@ -0,0 +1,151 @@ +package jnpf.franchisee.controller.app; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.franchisee.service.FranchiseeService; +import jnpf.model.franchisee.req.FranchiseeAddReq; +import jnpf.model.franchisee.req.FranchiseeQueryReq; +import jnpf.model.franchisee.req.FranchiseeSimpleAddReq; +import jnpf.model.franchisee.req.FranchiseeUpdateReq; +import jnpf.model.franchisee.vo.FranchiseeIdName; +import jnpf.model.franchisee.vo.FranchiseePageVO; +import jnpf.model.franchisee.vo.FranchiseeStoreVO; +import jnpf.model.franchisee.vo.FranchiseeVO; +import jnpf.parameter.service.ParamService; +import jnpf.util.FtbUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * APP加盟商控制器 + */ +@RestController +@Validated +@RequestMapping("/app/franchisee") +public class FranchiseeAppController { + + private static final String FRANCHISEE_CODE_PREFIX = "JSM"; + private static final String FRANCHISEE_CODE_SEQ_KEY = "franchiseeCodeSeq"; + private static final DateTimeFormatter FRANCHISEE_CODE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Autowired + private FranchiseeService franchiseeService; + @Autowired + private ParamService paramService; + + /** + * 简单新增加盟商 + * + * @param req 简单新增请求参数 + * @return 操作结果 + */ + @PostMapping("/simple-add") + public ActionResult simpleAdd(@Validated @RequestBody FranchiseeSimpleAddReq req) { + franchiseeService.add(req.convert()); + return ActionResult.success(); + } + + /** + * 自动生成加盟商编码 + * + * 规则:JSM + yyyyMMdd + 递增序号。 + * 递增序号通过固定key在数据库中维护,不按天重置。 + * + * @return 加盟商编码 + */ + @GetMapping("/generate-code") + public ActionResult generateCode() { + return ActionResult.success("success",franchiseeService.generateCode()); + } + + /** + * 新增加盟商 + * + * @param req 新增请求参数 + * @return 操作结果 + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FranchiseeAddReq req) { + franchiseeService.add(req); + return ActionResult.success(); + } + + /** + * 编辑加盟商 + * + * @param req 编辑请求参数 + * @return 操作结果 + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FranchiseeUpdateReq req) { + franchiseeService.update(req); + return ActionResult.success(); + } + + /** + * 删除加盟商 + * + * @param id 主键ID + * @return 操作结果 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") @NotBlank(message = "主键ID不能为空") String id) { + franchiseeService.delete(id); + return ActionResult.success(); + } + + /** + * 查询加盟商详情 + * + * @param id 主键ID + * @return 加盟商详情 + */ + @GetMapping("/query-info/{id}") + public ActionResult queryInfo(@PathVariable("id") @NotBlank(message = "主键ID不能为空") String id) { + return ActionResult.success(franchiseeService.queryInfo(id)); + } + + /** + * 分页查询加盟商列表 + * + * @param req 查询参数 + * @return 分页结果 + */ + @GetMapping("/query-page") + public ActionResult> queryPage(@Valid FranchiseeQueryReq req) { + PageInfo pageInfo = franchiseeService.queryPage(req,true); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 根据关键字查询加盟商ID列表(模糊匹配加盟商名称) + * + * @param keyword 关键字(加盟商名称) + * @return 加盟商ID列表 + */ + @GetMapping("/query-id-list") + public ActionResult> queryIdListByPrefix(@RequestParam(value = "keyword", required = false) String keyword) { + return ActionResult.success(franchiseeService.queryIdListByPrefix(keyword)); + } + + /** + * 根据加盟商ID查询其下门店列表 + * + * @param franchiseeId 加盟商ID + * @return 门店列表 + */ + @GetMapping("/query-store-list/{franchiseeId}") + public ActionResult> queryStoreList(@PathVariable("franchiseeId") @NotBlank(message = "加盟商ID不能为空") String franchiseeId) { + return ActionResult.success(franchiseeService.queryStoreList(franchiseeId)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeController.java new file mode 100644 index 0000000..a03e872 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeController.java @@ -0,0 +1,149 @@ +package jnpf.franchisee.controller.web; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.franchisee.service.FranchiseeService; +import jnpf.model.franchisee.req.FranchiseeAddReq; +import jnpf.model.franchisee.req.FranchiseeQueryReq; +import jnpf.model.franchisee.req.FranchiseeSimpleAddReq; +import jnpf.model.franchisee.req.FranchiseeUpdateReq; +import jnpf.model.franchisee.vo.FranchiseeIdName; +import jnpf.model.franchisee.vo.FranchiseePageVO; +import jnpf.model.franchisee.vo.FranchiseeStoreVO; +import jnpf.model.franchisee.vo.FranchiseeVO; +import jnpf.util.FtbUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * Web加盟商控制器 + */ +@RestController +@Validated +@RequestMapping("/web/franchisee") +public class FranchiseeController { + + @Autowired + private FranchiseeService franchiseeService; + + /** + * 简单新增加盟商 + * + * @param req 简单新增请求参数 + * @return 操作结果 + */ + @PostMapping("/simple-add") + public ActionResult simpleAdd(@Validated @RequestBody FranchiseeSimpleAddReq req) { + franchiseeService.add(req.convert()); + return ActionResult.success(); + } + + /** + * 自动生成加盟商编码 + * + * 规则:JMS + yyyyMMdd + 递增序号。 + * 递增序号通过固定key在数据库中维护,不按天重置。 + * + * @return 加盟商编码 + */ + @GetMapping("/generate-code") + public ActionResult generateCode() { + return ActionResult.success("succeed",franchiseeService.generateCode()); + } + + /** + * 新增加盟商 + * + * @param req 新增请求参数 + * @return 操作结果 + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FranchiseeAddReq req) { + franchiseeService.add(req); + return ActionResult.success(); + } + + /** + * 编辑加盟商 + * + * @param req 编辑请求参数 + * @return 操作结果 + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FranchiseeUpdateReq req) { + franchiseeService.update(req); + return ActionResult.success(); + } + + /** + * 删除加盟商 + * + * @param id 主键ID + * @return 操作结果 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") @NotBlank(message = "主键ID不能为空") String id) { + franchiseeService.delete(id); + return ActionResult.success(); + } + + /** + * 查询加盟商详情 + * + * @param id 主键ID + * @return 加盟商详情 + */ + @GetMapping("/query-info/{id}") + public ActionResult queryInfo(@PathVariable("id") @NotBlank(message = "主键ID不能为空") String id) { + return ActionResult.success(franchiseeService.queryInfo(id)); + } + + /** + * 分页查询加盟商列表 + * + * @param req 查询参数 + * @return 分页结果 + */ + @GetMapping("/query-page") + public ActionResult> queryPage(@Valid FranchiseeQueryReq req) { + PageInfo pageInfo = franchiseeService.queryPage(req,false); + return ActionResult.page(pageInfo.getList(), FtbUtil.getPagination(pageInfo)); + } + + /** + * 根据关键字查询加盟商ID列表(模糊匹配加盟商名称) + * + * @param keyword 关键字(加盟商名称) + * @return 加盟商ID列表 + */ + @GetMapping("/query-id-list") + public ActionResult> queryIdListByPrefix(@RequestParam(value = "keyword", required = false) String keyword) { + return ActionResult.success(franchiseeService.queryIdListByPrefix(keyword)); + } + + /** + * 根据加盟商ID查询其下门店列表 + * + * @param franchiseeId 加盟商ID + * @return 门店列表 + */ + @GetMapping("/query-store-list/{franchiseeId}") + public ActionResult> queryStoreList(@PathVariable("franchiseeId") @NotBlank(message = "加盟商ID不能为空") String franchiseeId) { + return ActionResult.success(franchiseeService.queryStoreList(franchiseeId)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeCustomFieldConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeCustomFieldConfigController.java new file mode 100644 index 0000000..f5ded38 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/controller/web/FranchiseeCustomFieldConfigController.java @@ -0,0 +1,114 @@ +package jnpf.franchisee.controller.web; + +import jnpf.base.ActionResult; +import jnpf.franchisee.service.FranchiseeCustomFieldConfigService; +import jnpf.model.franchisee.req.FranchiseeCustomFieldConfigReq; +import jnpf.model.franchisee.req.FranchiseeCustomNameUpdateReq; +import jnpf.model.franchisee.vo.FranchiseeCustomFieldConfigVO; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.constraints.NotBlank; +import java.util.List; + +/** + * web加盟商自定义字段配置控制器 + */ +@RestController +@Validated +@RequestMapping("/web/franchisee/custom-field-config") +public class FranchiseeCustomFieldConfigController { + + @Autowired + private FranchiseeCustomFieldConfigService franchiseeCustomFieldConfigService; + + /** + * 新增自定义字段配置 + * + * @param req 请求参数 + * @return 配置key + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody FranchiseeCustomFieldConfigReq req) { + return ActionResult.success("succeed",franchiseeCustomFieldConfigService.add(req)); + } + + /** + * 更新自定义字段配置 + * + * @param req 请求参数 + * @return 配置key + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody FranchiseeCustomFieldConfigReq req) { + return ActionResult.success("succeed",franchiseeCustomFieldConfigService.update(req)); + } + + /** + * 查询所有自定义字段配置 + * + * @return 配置列表 + */ + @GetMapping("/query-list") + public ActionResult> queryList() { + return ActionResult.success(franchiseeCustomFieldConfigService.queryList()); + } + + /** + * 查询自定义字段配置详情 + * + * @return 配置列表 + */ + @GetMapping("/query") + public ActionResult query(@RequestParam("key") String key) { + return ActionResult.success(franchiseeCustomFieldConfigService.query(key)); + } + + /** + * 查询自定义名称 + * + * @return 自定义名称 + */ + @GetMapping("/query-custom-name") + public ActionResult queryCustomName() { + return ActionResult.success("succeed",franchiseeCustomFieldConfigService.queryCustomName()); + } + + /** + * 更新自定义名称 + * + * @param req 更新请求参数 + * @return 更新后的自定义名称 + */ + @PutMapping("/update-custom-name") + public ActionResult updateCustomName(@Validated @RequestBody FranchiseeCustomNameUpdateReq req) { + return ActionResult.success("succeed",franchiseeCustomFieldConfigService.updateCustomName(req.getCustomName())); + } + + /** + * 删除自定义字段配置 + * + * @param key 配置key + * @return 操作结果 + */ + @DeleteMapping("/delete/{key}") + public ActionResult delete(@PathVariable("key") @NotBlank(message = "key不能为空") String key) { + franchiseeCustomFieldConfigService.delete(key); + return ActionResult.success(); + } + + /** + * 调整自定义字段排序 + * + * @param key 按目标顺序传入的字段key列表 + * @return 操作结果 + */ + @PutMapping("/adjust-sort") + public ActionResult adjustSort(@RequestBody List key) { + franchiseeCustomFieldConfigService.adjustSort(key); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeCustomFieldMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeCustomFieldMapper.java new file mode 100644 index 0000000..5ba55f4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeCustomFieldMapper.java @@ -0,0 +1,21 @@ +package jnpf.franchisee.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.franchisee.po.FranchiseeCustomFieldEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 加盟商自定义字段值Mapper + */ +public interface FranchiseeCustomFieldMapper extends SuperMapper { + /** + * 批量插入 + * + * @param entities 实体列表 + * @return 插入的数量 + */ + int batchInsert(@Param("entities") List entities); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeExperienceMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeExperienceMapper.java new file mode 100644 index 0000000..2637894 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeExperienceMapper.java @@ -0,0 +1,21 @@ +package jnpf.franchisee.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.franchisee.po.FranchiseeExperienceEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 加盟商从业经历Mapper + */ +public interface FranchiseeExperienceMapper extends SuperMapper { + /** + * 批量插入 + * + * @param entities + * @return 插入的数量 + */ + int batchInsert(@Param("entities") List entities); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeJoinRegionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeJoinRegionMapper.java new file mode 100644 index 0000000..2c6efcf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeJoinRegionMapper.java @@ -0,0 +1,21 @@ +package jnpf.franchisee.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.franchisee.po.FranchiseeJoinRegionEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 加盟商加盟地区冗余表Mapper + */ +public interface FranchiseeJoinRegionMapper extends SuperMapper { + /** + * 批量插入 + * + * @param entities 实体列表 + * @return 影响行数 + */ + int batchInsert(@Param("entities") List entities); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeMapper.java new file mode 100644 index 0000000..993b9da --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeMapper.java @@ -0,0 +1,29 @@ +package jnpf.franchisee.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.franchisee.po.FranchiseeEntity; +import jnpf.model.franchisee.req.FranchiseeJoinRegionReq; +import jnpf.model.franchisee.req.FranchiseeQueryReq; +import jnpf.model.franchisee.vo.FranchiseePageVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 加盟商主表 Mapper + */ +public interface FranchiseeMapper extends SuperMapper { + + /** + * 分页查询加盟商列表 + * + * @param req 查询参数 + * @param provinceCityJoinRegion 区为空时的省市集合 + * @param provinceList 市为空时的省集合 + * @return 列表数据 + */ + List queryPageList(@Param("req") FranchiseeQueryReq req, + @Param("provinceCityJoinRegion") List provinceCityJoinRegion, + @Param("provinceList") List provinceList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeStoreMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeStoreMapper.java new file mode 100644 index 0000000..0b44d3a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/mapper/FranchiseeStoreMapper.java @@ -0,0 +1,11 @@ +package jnpf.franchisee.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.permission.entity.StoreEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 加盟商门店Mapper。 + */ +public interface FranchiseeStoreMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeCustomFieldConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeCustomFieldConfigService.java new file mode 100644 index 0000000..412f6e2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeCustomFieldConfigService.java @@ -0,0 +1,71 @@ +package jnpf.franchisee.service; + +import jnpf.model.franchisee.req.FranchiseeCustomFieldConfigReq; +import jnpf.model.franchisee.vo.FranchiseeCustomFieldConfigVO; + +import java.util.List; + +/** + * 加盟商自定义字段配置服务 + */ +public interface FranchiseeCustomFieldConfigService { + + /** + * 新增自定义字段配置 + * + * @param req 请求参数 + * @return 配置key + */ + String add(FranchiseeCustomFieldConfigReq req); + + /** + * 更新自定义字段配置 + * + * @param req 请求参数 + * @return 配置key + */ + String update(FranchiseeCustomFieldConfigReq req); + + /** + * 查询所有自定义字段配置 + * + * @return 配置列表 + */ + List queryList(); + + /** + * 查询自定义字段配置详情 + * + * @return 配置 + */ + FranchiseeCustomFieldConfigVO query(String key); + + /** + * 删除自定义字段配置 + * + * @param key 配置key + */ + void delete(String key); + + /** + * 调整自定义字段排序 + * + * @param key 按目标顺序传入的字段key列表 + */ + void adjustSort(List key); + + /** + * 查询自定义名称。 + * + * @return 自定义名称 + */ + String queryCustomName(); + + /** + * 更新自定义名称。 + * + * @param customName 自定义名称 + * @return 更新后的自定义名称 + */ + String updateCustomName(String customName); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeService.java new file mode 100644 index 0000000..f9d5da1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/FranchiseeService.java @@ -0,0 +1,149 @@ +package jnpf.franchisee.service; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.franchisee.po.FranchiseeEntity; +import jnpf.model.franchisee.req.FranchiseeAddReq; +import jnpf.model.franchisee.req.FranchiseeQueryReq; +import jnpf.model.franchisee.req.FranchiseeUpdateReq; +import jnpf.model.franchisee.vo.FranchiseeIdName; +import jnpf.model.franchisee.vo.FranchiseePageVO; +import jnpf.model.franchisee.vo.FranchiseeVO; +import jnpf.model.franchisee.vo.FranchiseeStoreVO; +import jnpf.permission.vo.store.StoreInfoDetailVO; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.Collection; +import java.util.List; + +/** + * 加盟商服务 + */ +public interface FranchiseeService extends IService { + + /** + * 新增加盟商 + * + * @param req 新增请求参数 + */ + void add(FranchiseeAddReq req); + + /** + * 编辑加盟商 + * + * @param req 编辑请求参数 + */ + void update(FranchiseeUpdateReq req); + + /** + * 删除加盟商 + * + * @param id 主键ID + */ + void delete(String id); + + /** + * 查询加盟商详情 + * + * @param id 主键ID + * @return 加盟商详情 + */ + FranchiseeVO queryInfo(String id); + + /** + * 分页查询加盟商列表 + * + * @param req 查询参数 + * @return 分页结果 + */ + PageInfo queryPage(FranchiseeQueryReq req,boolean includeJoinRegion); + + /** + * 根据关键字查询加盟商ID列表(模糊匹配加盟商编码/名称/手机号) + * + * @param keyword 关键字(加盟商编码/加盟商名称/手机号) + * @return 加盟商ID,Name列表 + */ + List queryIdListByPrefix(String keyword); + + /** + * 根据加盟商ID查询其下门店列表。 + * + * @param franchiseeId 加盟商ID + * @return 门店列表 + */ + List queryStoreList(String franchiseeId); + + /** + * 查询加盟商门店数量,并修正加盟商门店数量。返回查询到的门店信息 + * @param franchiseeId + * @return + */ + List queryFranchiseeStoreNumAndUpdate(String franchiseeId); + + /** + * 生成加盟商编码 + * + * @return 加盟商编码 + */ + String generateCode(); + + /** + * 根据ID列表获取加盟商信息 + * @param ids + * @return + */ + List getFranchiseeIdNameListByIds(Collection ids); + + /** + * 根据ID获取加盟商信息 + * @param id + * @return + */ + default FranchiseeIdName getFranchiseeIdNameListById(String id){ + List franchiseeIdNames = getFranchiseeIdNameListByIds(List.of(id)); + if(CollectionUtil.isEmpty(franchiseeIdNames)){ + return null; + } + return franchiseeIdNames.get(0); + } + + /** + * 根据名称获取加盟商信息 + * @param name + * @return + */ + default List getFranchiseeIdNameListByName(String name){ + return getFranchiseeIdNameListByNames(List.of(name)); + } + + /** + * 根据名称列表获取加盟商信息 + * @param names + * @return + */ + List getFranchiseeIdNameListByNames(Collection names); + + /** + * 根据编码获取加盟商信息 + * @param code + * @return + */ + default FranchiseeIdName getFranchiseeIdNameListByCode(String code){ + List franchiseeIdNames = getFranchiseeIdNameListByCodes(List.of(code)); + if(CollectionUtil.isEmpty(franchiseeIdNames)){ + return null; + } + return franchiseeIdNames.get(0); + } + + /** + * 根据编码列表获取加盟商信息 + * @param codes + * @return + */ + List getFranchiseeIdNameListByCodes(Collection codes); + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeCustomFieldConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeCustomFieldConfigServiceImpl.java new file mode 100644 index 0000000..7226a5c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeCustomFieldConfigServiceImpl.java @@ -0,0 +1,409 @@ +package jnpf.franchisee.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.franchisee.service.FranchiseeCustomFieldConfigService; +import jnpf.model.franchisee.enums.FranchiseeCustomFieldTypeEnum; +import jnpf.model.franchisee.req.FranchiseeCustomFieldConfigOptionReq; +import jnpf.model.franchisee.req.FranchiseeCustomFieldConfigReq; +import jnpf.model.franchisee.vo.FranchiseeCustomFieldConfigOptionVO; +import jnpf.model.franchisee.vo.FranchiseeCustomFieldConfigVO; +import jnpf.model.warningnotice.po.FtbParamEntity; +import jnpf.storecertificatephoto.mapper.BaseParamMapper; +import jnpf.util.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 加盟商自定义字段配置服务实现 + */ +@Service +public class FranchiseeCustomFieldConfigServiceImpl implements FranchiseeCustomFieldConfigService { + + /** + * 自定义字段配置类型 + */ + private static final String customerFieldType = "customerFieldType"; + /** + * 自定义名称配置:固定key,同时type也固定为该值。 + */ + private static final String customerNameType = "franchiseeCustomerNameType"; + private static final String lockPrefix = "franchisee:customer:field:"; + + @Autowired + private BaseParamMapper baseParamMapper; + @Autowired + private RedissonClient redissonClient; + + /** + * 新增自定义字段配置 + * + * @param req 请求参数 + * @return 配置key + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String add(FranchiseeCustomFieldConfigReq req) { + return saveOrUpdate(req); + } + + /** + * 更新自定义字段配置 + * + * @param req 请求参数 + * @return 配置key + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String update(FranchiseeCustomFieldConfigReq req) { + String key = req.getKey(); + if(StringUtil.isBlank(key)){ + throw new ServiceException("key不能为空"); + } + return saveOrUpdate(req); + } + + /** + * 查询所有自定义字段配置 + * + * @return 配置列表 + */ + @Override + public List queryList() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbParamEntity::getType, customerFieldType); + queryWrapper.orderByAsc(FtbParamEntity::getSort); + List entityList = baseParamMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + for (FtbParamEntity entity : entityList) { + FranchiseeCustomFieldConfigVO vo = buildFranchiseeCustomerFieldVo(entity); + if (vo == null) { + continue; + } + result.add(vo); + } + return result; + } + + @Override + public FranchiseeCustomFieldConfigVO query(String key) { + FtbParamEntity ftbParamEntity = baseParamMapper.selectByKey(key); + if(Objects.isNull(ftbParamEntity)){ + throw new ServiceException("找不到该配置!",404); + } + FranchiseeCustomFieldConfigVO franchiseeCustomFieldConfigVO = buildFranchiseeCustomerFieldVo(ftbParamEntity); + if(Objects.isNull(franchiseeCustomFieldConfigVO)){ + throw new ServiceException("该配置不正确!",404); + } + return franchiseeCustomFieldConfigVO; + } + + private FranchiseeCustomFieldConfigVO buildFranchiseeCustomerFieldVo(FtbParamEntity entity) { + FranchiseeCustomFieldConfigVO vo = JsonUtil.getJsonToBean(entity.getValue(), FranchiseeCustomFieldConfigVO.class); + if (vo == null) { + return null; + } + vo.setKey(entity.getKey()); + vo.setFieldCode(entity.getKey()); + vo.setSort(entity.getSort()); + vo.setFieldName(StrUtil.trim(vo.getFieldName())); + if (vo.getOptionList() == null) { + vo.setOptionList(new ArrayList<>()); + } + if(vo.getImageList() == null){ + vo.setImageList(new ArrayList<>()); + } + return vo; + } + + /** + * 删除自定义字段配置 + * + * @param key 配置key + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(String key) { + String trimKey = StrUtil.trim(key); + ServiceException.isTrue(StrUtil.isNotBlank(trimKey), "key不能为空"); + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(FtbParamEntity::getType, customerFieldType); + deleteWrapper.eq(FtbParamEntity::getKey, trimKey); + int deleteCount = baseParamMapper.delete(deleteWrapper); + ServiceException.isTrue(deleteCount > 0, "自定义字段配置不存在"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void adjustSort(List key) { + ServiceException.isTrue(CollUtil.isNotEmpty(key), "key list cannot be empty"); + List sortedKeyList = new ArrayList<>(); + Set keySet = new HashSet<>(); + for (String item : key) { + String trimKey = StrUtil.trim(item); + ServiceException.isTrue(StrUtil.isNotBlank(trimKey), "key cannot be blank"); + ServiceException.isTrue(keySet.add(trimKey), "key list contains duplicate key"); + sortedKeyList.add(trimKey); + } + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbParamEntity::getType, customerFieldType); + List entityList = baseParamMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return; + } + + Map entityMap = entityList.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(item -> StrUtil.trim(item.getKey()), item -> item, (a, b) -> a)); + for (String sortKey : sortedKeyList) { + ServiceException.isTrue(entityMap.containsKey(sortKey), "自定义字段配置不存在"); + } + + List remainingEntityList = entityList.stream() + .filter(item -> !keySet.contains(StrUtil.trim(item.getKey()))) + .sorted((a, b) -> { + Long sortA = a.getSort() == null ? Long.MAX_VALUE : a.getSort(); + Long sortB = b.getSort() == null ? Long.MAX_VALUE : b.getSort(); + int sortCompare = sortA.compareTo(sortB); + if (sortCompare != 0) { + return sortCompare; + } + return StrUtil.nullToDefault(a.getKey(), "").compareTo(StrUtil.nullToDefault(b.getKey(), "")); + }) + .collect(Collectors.toList()); + for (FtbParamEntity entity : remainingEntityList) { + sortedKeyList.add(StrUtil.trim(entity.getKey())); + } + + long targetSort = 1L; + for (String sortKey : sortedKeyList) { + FtbParamEntity entity = entityMap.get(sortKey); + if (entity == null) { + continue; + } + if (!Objects.equals(entity.getSort(), targetSort)) { + entity.setSort(targetSort); + baseParamMapper.updateByKey(sortKey, entity); + } + targetSort++; + } + } + + /** + * 按key新增或更新 + * + * 规则: + * 1. key为空,新增; + * 2. key不为空,按key更新。 + * + * @param req 请求参数 + * @return 配置key + */ + /** + * 查询自定义名称。 + * + * 读取 base_param 表中 key=customerNameType 且 type=customerNameType 的记录。 + * + * @return 自定义名称;未配置时返回空字符串 + */ + @Override + public String queryCustomName() { + FtbParamEntity entity = baseParamMapper.selectByKey(customerNameType); + if (entity == null || !StrUtil.equals(customerNameType, StrUtil.trim(entity.getType()))) { + return ""; + } + return StrUtil.blankToDefault(StrUtil.trim(entity.getValue()), ""); + } + + /** + * 更新自定义名称。 + * + * 保存逻辑:先按固定key更新,若不存在则插入;type 固定为 customerNameType。 + * + * @param customName 自定义名称 + * @return 更新后的自定义名称 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String updateCustomName(String customName) { + String name = StrUtil.trim(customName); + ServiceException.isTrue(StrUtil.isNotBlank(name), "customName cannot be blank"); + + FtbParamEntity entity = new FtbParamEntity(); + entity.setId(FtbUtil.getId()); + entity.setKey(customerNameType); + entity.setType(customerNameType); + entity.setValue(name); + entity.setSort(0L); + + int updateCount = baseParamMapper.updateByKey(customerNameType, entity); + if (updateCount > 0) { + return name; + } + + try { + baseParamMapper.insertParam(entity); + } catch (DuplicateKeyException e) { + int retryCount = baseParamMapper.updateByKey(customerNameType, entity); + ServiceException.isTrue(retryCount > 0, "update customName failed"); + } + return name; + } + + private String saveOrUpdate(FranchiseeCustomFieldConfigReq req) { + validateReq(req); + String key = StrUtil.trim(req.getKey()); + if (StrUtil.isBlank(key)) { + key = FtbUtil.getId(); + FtbParamEntity entity = buildEntity(key, req); + entity.setId(FtbUtil.getId()); + insertCustomerField(entity); + return key; + } + + FtbParamEntity source = baseParamMapper.selectByKey(key); + ServiceException.notNull(source, "自定义字段配置不存在"); + ServiceException.isTrue(customerFieldType.equals(source.getType()), "自定义字段配置不存在"); + int updateCount = baseParamMapper.updateByKey(key, buildEntity(key, req)); + ServiceException.isTrue(updateCount > 0, "更新失败"); + return key; + } + + private void insertCustomerField(FtbParamEntity entity) { + String lockKey = buildFranchiseeCustomerFieldAddLock(); + RLock rLock = redissonClient.getLock(lockKey); + try { + rLock.lock(10, TimeUnit.SECONDS); + Long count = baseParamMapper.selectCount(new LambdaQueryWrapper() + .eq(FtbParamEntity::getType, customerFieldType)); + if(count >= 100){ + throw new ServiceException("自定义字段配置已满100"); + } + baseParamMapper.insertParam(entity); + }finally { + rLock.unlock(); + } + } + + /** + * 构建并序列化配置实体 + * + * @param key 配置key + * @param req 请求参数 + * @return BaseParam实体 + */ + private FtbParamEntity buildEntity(String key, FranchiseeCustomFieldConfigReq req) { + FranchiseeCustomFieldConfigValueModel saveData = buildSaveData(key, req); + FtbParamEntity entity = new FtbParamEntity(); + entity.setKey(key); + entity.setType(customerFieldType); + entity.setSort(req.getSort() == null ? 0L : req.getSort()); + entity.setValue(JsonUtil.getObjectToString(saveData)); + return entity; + } + + /** + * 构建保存数据对象 + * + * @param key 配置key + * @param req 请求参数 + * @return 保存对象 + */ + private FranchiseeCustomFieldConfigValueModel buildSaveData(String key, FranchiseeCustomFieldConfigReq req) { + FranchiseeCustomFieldConfigValueModel vo = new FranchiseeCustomFieldConfigValueModel(); + vo.setFieldCode(key); + vo.setFieldName(StrUtil.trim(req.getFieldName())); + vo.setFieldType(req.getFieldType()); + vo.setRequiredMark(req.getRequiredMark() != null && req.getRequiredMark()); + vo.setSort(req.getSort() == null ? 0L : req.getSort()); + List optionList = req.getOptionList().stream() + .filter(Objects::nonNull) + .map(item -> { + FranchiseeCustomFieldConfigOptionVO option = new FranchiseeCustomFieldConfigOptionVO(); + option.setOptionName(StrUtil.trim(item.getOptionName())); + return option; + }) + .filter(item -> StrUtil.isNotBlank(item.getOptionName())) + .collect(Collectors.toList()); + vo.setOptionList(optionList); + + List imageList = req.getImageList().stream() + .filter(Objects::nonNull) + .map(item -> { + FranchiseeCustomFieldConfigOptionVO option = new FranchiseeCustomFieldConfigOptionVO(); + option.setOptionName(StrUtil.trim(item.getOptionName())); + return option; + }) + .filter(item -> StrUtil.isNotBlank(item.getOptionName())) + .collect(Collectors.toList()); + vo.setImageList(imageList); + vo.setTextTips(req.getTextTips()); + return vo; + } + + /** + * 校验请求参数 + * + * @param req 请求参数 + */ + private void validateReq(FranchiseeCustomFieldConfigReq req) { + ServiceException.notNull(req, "请求参数不能为空"); + ServiceException.isTrue(StrUtil.isNotBlank(req.getFieldName()), "字段名称不能为空"); + ServiceException.notNull(req.getFieldType(), "字段类型不能为空"); + if (req.getOptionList() == null) { + req.setOptionList(new ArrayList<>()); + } + if (FranchiseeCustomFieldTypeEnum.MULTI_OPTION.equals(req.getFieldType())) { + ServiceException.isTrue(CollUtil.isNotEmpty(req.getOptionList()), "选项不能为空"); + } + for (FranchiseeCustomFieldConfigOptionReq option : req.getOptionList()) { + ServiceException.notNull(option, "选项不能为空"); + ServiceException.isTrue(StrUtil.isNotBlank(option.getOptionName()), "选项名称不能为空"); + } + } + + private String buildFranchiseeCustomerFieldAddLock() { + return lockPrefix+ UserProvider.getUser().getTenantId(); + } + + /** + * 保存到BaseParamEntity.value的模型(不包含key) + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + private static class FranchiseeCustomFieldConfigValueModel { + private String fieldCode; + private String fieldName; + private FranchiseeCustomFieldTypeEnum fieldType; + private List optionList = new ArrayList<>(); + private List imageList = new ArrayList<>(); + private Boolean requiredMark; + private Long sort; + private String textTips; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeServiceImpl.java new file mode 100644 index 0000000..e62fbbb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/franchisee/service/impl/FranchiseeServiceImpl.java @@ -0,0 +1,1131 @@ +package jnpf.franchisee.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.certificate.helper.OrganizationHelper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.certificate.util.CertificateStatusUtils; +import jnpf.franchisee.mapper.FranchiseeCustomFieldMapper; +import jnpf.franchisee.mapper.FranchiseeExperienceMapper; +import jnpf.franchisee.mapper.FranchiseeJoinRegionMapper; +import jnpf.franchisee.mapper.FranchiseeMapper; +import jnpf.franchisee.service.FranchiseeService; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.franchisee.po.FranchiseeCustomFieldEntity; +import jnpf.model.franchisee.po.FranchiseeEntity; +import jnpf.model.franchisee.po.FranchiseeExperienceEntity; +import jnpf.model.franchisee.po.FranchiseeJoinRegionEntity; +import jnpf.model.franchisee.req.FranchiseeAddReq; +import jnpf.model.franchisee.req.FranchiseeCustomFieldReq; +import jnpf.model.franchisee.req.FranchiseeExperienceReq; +import jnpf.model.franchisee.req.FranchiseeJoinRegionReq; +import jnpf.model.franchisee.req.FranchiseeQueryReq; +import jnpf.model.franchisee.req.FranchiseeUpdateReq; +import jnpf.model.franchisee.vo.*; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.po.FtbParamEntity; +import jnpf.parameter.hepler.MysqlSequenceHelper; +import jnpf.permission.StoreApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.store.StoreInfoDetailVO; +import jnpf.permission.vo.store.StoreRegionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.storecertificatephoto.mapper.BaseParamMapper; +import jnpf.util.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 加盟商服务实现 + */ +@Service +public class FranchiseeServiceImpl extends ServiceImpl implements FranchiseeService { + + private static final String EMPTY_JSON_ARRAY = "[]"; + + private static final int MAX_EXPERIENCE_COUNT = 5; + + private static final String FRANCHISEE_CODE_PREFIX = "JMS"; + + private static final String FRANCHISEE_CODE_SEQ_KEY_PREFIX = "franchiseeCodeSeq_"; + + private static final DateTimeFormatter FRANCHISEE_CODE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final Long FRANCHISEE_CODE_MAX_SEQUENCE = 10000L; + + @Autowired + private FranchiseeCustomFieldMapper franchiseeCustomFieldMapper; + @Autowired + private FranchiseeExperienceMapper franchiseeExperienceMapper; + @Autowired + private FranchiseeJoinRegionMapper franchiseeJoinRegionMapper; + @Autowired + private BaseParamMapper baseParamMapper; + @Autowired + private MysqlSequenceHelper mysqlSequenceHelper; + @Autowired + private StoreApi storeApi; + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private OrganizationHelper organizationHelper; + @Autowired + private V2UserApi v2UserApi; + + /** + * 新增加盟商 + * + * @param req 新增参数 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void add(FranchiseeAddReq req) { + List customFieldReqList = validateAddReq(req); + List joinRegionReqs = normalizeJoinRegionList(req.getJoinRegion()); + FranchiseeEntity entity = req.convert(); + + insertFranchiseeEntity(entity); + saveCustomField(entity.getId(), customFieldReqList); + saveExperience(entity.getId(), req.getExperienceList()); + saveJoinRegion(entity.getId(), joinRegionReqs); + } + + /** + * 编辑加盟商 + * + * @param req 编辑参数 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(FranchiseeUpdateReq req) { + List customFieldReqList = validateAddReq(req); + List joinRegionReqs = normalizeJoinRegionList(req.getJoinRegion()); + String id = StrUtil.trim(req.getId()); + if (StrUtil.isBlank(id)) { + throw new RuntimeException("主键ID不能为空"); + } + + FranchiseeEntity dbEntity = getEntityById(id); + FranchiseeEntity entity = req.convert(); + entity.setLastModifyTime(new Date()); + entity.setLastModifyUserId(UserProvider.getLoginUserId()); + entity.setId(dbEntity.getId()); + // 门店数量由门店变更事件维护,编辑时保持原值避免被覆盖 +// entity.setStoreNum(dbEntity.getStoreNum()); + entity.setEnabledMark(dbEntity.getEnabledMark()); + updateFranchiseeEntity(entity); + + deleteCustomFieldByFranchiseeId(id); + saveCustomField(id, customFieldReqList); + + deleteExperienceByFranchiseeId(id); + saveExperience(id, req.getExperienceList()); + + deleteJoinRegionByFranchiseeId(id); + saveJoinRegion(id, joinRegionReqs); + } + + /** + * 删除加盟商 + * + * @param id 主键ID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(String id) { + String franchiseeId = StrUtil.trim(id); + if (StrUtil.isBlank(franchiseeId)) { + throw new RuntimeException("主键ID不能为空"); + } + + FranchiseeEntity franchiseeEntity = getEntityById(franchiseeId); + if (queryRealStoreNum(franchiseeId) > 0) { + throw new RuntimeException("加盟商已关联门店,不能删除"); + } + baseMapper.update(null, Wrappers.lambdaUpdate() + .eq(FranchiseeEntity::getId, franchiseeId) + .set(FranchiseeEntity::getLastModifyTime,new Date()) + .set(FranchiseeEntity::getLastModifyUserId, UserProvider.getLoginUserId()) + .set(FranchiseeEntity::getEnabledMark, 1) + //唯一索引,更新为墓碑值 + .set(FranchiseeEntity::getFranchiseeName,id) + .set(FranchiseeEntity::getFranchiseeCode,id) + .set(FranchiseeEntity::getTenantId,franchiseeEntity.getFranchiseeName()+","+franchiseeEntity.getFranchiseeCode())); + } + + /** + * 根据ID查询加盟商详情 + * + * @param id 主键ID + * @return 加盟商详情 + */ + @Override + public FranchiseeVO queryInfo(String id) { + String franchiseeId = StrUtil.trim(id); + if (StrUtil.isBlank(franchiseeId)) { + throw new RuntimeException("主键ID不能为空"); + } + + FranchiseeEntity entity = getEntityById(franchiseeId); + FranchiseeVO vo = FranchiseeVO.convert(entity); + vo.setCustomFieldList(queryCustomFieldList(franchiseeId)); + vo.setExperienceList(queryExperienceList(franchiseeId)); + vo.setJoinRegion(queryJoinRegionList(franchiseeId)); + return vo; + } + + private String queryJoinRegionList(String franchiseeId) { + List joinRegionEntities = franchiseeJoinRegionMapper.selectList(new LambdaQueryWrapper() + .eq(FranchiseeJoinRegionEntity::getFranchiseeId,franchiseeId)); + if(CollectionUtil.isEmpty(joinRegionEntities)){ + return "[]"; + } + return JSONUtil.toJsonStr(joinRegionEntities); + } + + /** + * 分页查询加盟商列表 + * + * @param req 查询参数 + * @return 分页数据 + */ + @Override + public PageInfo queryPage(FranchiseeQueryReq req,boolean includeJoinRegion) { + FranchiseeQueryReq queryReq = req == null ? new FranchiseeQueryReq() : req; + validateQueryReq(queryReq); + List normalizedJoinRegionReqs = normalizeJoinRegionList(queryReq.getJoinRegion()); + List provinceCityJoinRegion = extractProvinceCityJoinRegion(normalizedJoinRegionReqs); + List provinceList = extractProvinceList(normalizedJoinRegionReqs); + queryReq.setJoinRegion(expandQueryJoinRegionList(normalizedJoinRegionReqs)); + + int currentPage = queryReq.getCurrentPage() == null || queryReq.getCurrentPage() < 1 ? 1 : queryReq.getCurrentPage(); + int pageSize = queryReq.getPageSize() == null || queryReq.getPageSize() < 1 ? 10 : queryReq.getPageSize(); + queryReq.setCurrentPage(currentPage); + queryReq.setPageSize(pageSize); + + PageHelper.startPage(currentPage, pageSize); + List result = baseMapper.queryPageList(queryReq, provinceCityJoinRegion, provinceList); + if(includeJoinRegion){ + buildJoinRegions(result); + } + return new PageInfo<>(result); + } + + private void buildJoinRegions(Collection result) { + if(CollUtil.isEmpty(result)){ + return; + } + Map> joinRegionsMap = buildJoinRegionsMap(result.stream().map(FranchiseePageVO::getId).collect(Collectors.toSet())); + for (FranchiseePageVO franchiseePageVO:result){ + List joinRegionEntities = joinRegionsMap.get(franchiseePageVO.getId()); + if(CollUtil.isEmpty(joinRegionEntities)){ + franchiseePageVO.setJoinRegion("[]"); + continue; + } + franchiseePageVO.setJoinRegion(JSONUtil.toJsonStr(joinRegionEntities)); + } + } + + private Map> buildJoinRegionsMap(Collection franchiseeIds){ + List joinRegionEntities = franchiseeJoinRegionMapper.selectList(new LambdaQueryWrapper() + .in(FranchiseeJoinRegionEntity::getFranchiseeId,franchiseeIds)); + Map> joinRegions = new HashMap<>(); + for (FranchiseeJoinRegionEntity franchiseeJoinRegionEntity:joinRegionEntities){ + String franchiseeId = franchiseeJoinRegionEntity.getFranchiseeId(); + List franchiseeJoinRegionEntities = joinRegions.computeIfAbsent(franchiseeId,v->new ArrayList<>()); + franchiseeJoinRegionEntities.add(franchiseeJoinRegionEntity); + } + return joinRegions; + } + + /** + * 根据关键字查询加盟商ID列表(模糊匹配加盟商名称) + * + * @param keyword 关键字(可匹配加盟商名称) + * @return 加盟商ID列表 + */ + @Override + public List queryIdListByPrefix(String keyword) { + String searchKeyword = StrUtil.trim(keyword); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FranchiseeEntity::getId, + FranchiseeEntity::getFranchiseeName, + FranchiseeEntity::getMobilePhone, + FranchiseeEntity::getFranchiseeCode, + FranchiseeEntity::getCreatorTime); + queryWrapper.eq(FranchiseeEntity::getEnabledMark, 0); + queryWrapper.like(StringUtil.isNotBlank(searchKeyword),FranchiseeEntity::getFranchiseeCode, searchKeyword); + queryWrapper.orderByDesc(FranchiseeEntity::getCreatorTime); + + List entityList = baseMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return Collections.emptyList(); + } + return entityList.stream() + .map(e->new FranchiseeIdName(e.getId(),e.getFranchiseeName(),e.getMobilePhone(),e.getFranchiseeCode())) + .collect(Collectors.toList()); + } + + /** + * 根据加盟商ID查询其下门店列表。 + * + * @param franchiseeId 加盟商ID + * @return 门店列表 + */ + @Override + public List queryStoreList(String franchiseeId) { + String targetFranchiseeId = StrUtil.trim(franchiseeId); + if (StrUtil.isBlank(targetFranchiseeId)) { + throw new RuntimeException("加盟商ID不能为空"); + } + + FranchiseeEntity franchiseeEntity = getEntityById(targetFranchiseeId); + Integer dbStoreNum = franchiseeEntity.getStoreNum(); + + Map> storeBaseListInfos = organizationHelper.buildStoreInfoDetailsByFranchiseeId(targetFranchiseeId); + if(CollectionUtil.isEmpty(storeBaseListInfos)){ + return Collections.emptyList(); + } + List storeInfoDetailVOS = storeBaseListInfos.get(targetFranchiseeId); + if(CollectionUtil.isEmpty(storeInfoDetailVOS)){ + return Collections.emptyList(); + } + //数据不一致 fixme看是否需要增加兜底逻辑,会增加并发处理逻辑更复杂 + if(Objects.nonNull(dbStoreNum) && dbStoreNum != storeInfoDetailVOS.size()){ + storeInfoDetailVOS = updateFranchiseeStoreNum(franchiseeEntity,storeInfoDetailVOS); + } + Set userIds = new HashSet<>(); + Set storeIds = new HashSet<>(); + for (StoreInfoDetailVO storeBaseListInfo : storeInfoDetailVOS){ + String storeHeadUserId = storeBaseListInfo.getStoreHeadUserId(); + if(StringUtil.isNotBlank(storeHeadUserId)){ + userIds.add(storeHeadUserId); + } + String storeId = storeBaseListInfo.getId(); + if(StringUtil.isNotBlank(storeId)){ + storeIds.add(storeId); + } + } + + Map userBoundVOMap = organizationHelper.getUserPrimaryBoundBatch(userIds); + + Map> certificateMap = certificateInstanceMapper.selectList(new LambdaQueryWrapper() + .in(CertificateInstanceEntity::getSubjectId,storeIds) + .in(CertificateInstanceEntity::getSubjectType,3) + .eq(CertificateInstanceEntity::getEnabledMark,0)) + .stream() + .collect(Collectors.groupingBy(CertificateInstanceEntity::getSubjectId));//门店 + return storeInfoDetailVOS.stream() + .map(s->convertStoreVO(s,userBoundVOMap,certificateMap)) + .collect(Collectors.toList()); + } + + private List updateFranchiseeStoreNum(FranchiseeEntity franchiseeEntity, List storeInfoDetailVOS) { + String franchiseeId = franchiseeEntity.getId(); + Integer dbStoreNum = franchiseeEntity.getStoreNum(); + Integer currentStoreNum = storeInfoDetailVOS.size(); + if(Objects.equals(dbStoreNum, currentStoreNum)){//已经相等了,退出 + return storeInfoDetailVOS; + } + int result = baseMapper.update(null,new LambdaUpdateWrapper() + .set(FranchiseeEntity::getStoreNum,currentStoreNum) + .set(FranchiseeEntity::getUpdateStoreNumTimestamp,System.currentTimeMillis())//事件水位线。需要和mq消费事件水位获取来源一致。目前来看,他们是用得数据库的当前时间戳。所以,需要数据库和机器的时间要一致。 + .eq(FranchiseeEntity::getId,franchiseeId) + .eq(FranchiseeEntity::getStoreNum,dbStoreNum));//乐观锁更新。 + if(result <= 0){//更新失败,说明数据已经被修改了,重新查询一次,在次更新,直到数据没有被别的线程修改(最新数据)。 + return queryFranchiseeStoreNumAndUpdate(franchiseeId); + } + return storeInfoDetailVOS; + } + + @Override + public List queryFranchiseeStoreNumAndUpdate(String franchiseeId) { + FranchiseeEntity franchiseeEntity = getEntityById(franchiseeId); + Map> storeBaseListInfos = organizationHelper.buildStoreInfoDetailsByFranchiseeId(franchiseeId); + ServiceException.isTrue(CollectionUtil.isNotEmpty(storeBaseListInfos),"查询异常,请稍后再试!"); + List storeInfoDetailVOS = storeBaseListInfos.get(franchiseeId); + return updateFranchiseeStoreNum(franchiseeEntity,storeInfoDetailVOS); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String generateCode() { + LocalDate today = LocalDate.now(); + String datePart = FRANCHISEE_CODE_DATE_FORMATTER.format(today); + String todaySeqKey = buildFranchiseeCodeSeqKey(today); + + Long sequence = mysqlSequenceHelper.increment(todaySeqKey,FRANCHISEE_CODE_SEQ_KEY_PREFIX); + if(sequence >= FRANCHISEE_CODE_MAX_SEQUENCE){ + throw new RuntimeException("加盟商编号已超出最大值!"); + } + long serialValue = sequence == null || sequence < 1 ? 1L : sequence; + if (serialValue == 1L) { + baseParamMapper.deleteByTypeNotKey(FRANCHISEE_CODE_SEQ_KEY_PREFIX,todaySeqKey); + } + + String serialPart = serialValue <= 999 ? String.format("%03d", serialValue) : String.valueOf(serialValue); + return FRANCHISEE_CODE_PREFIX + datePart + serialPart; + } + + @Override + public List getFranchiseeIdNameListByIds(Collection ids) { + return baseMapper.selectList(new LambdaQueryWrapper() + .eq(FranchiseeEntity::getEnabledMark, 0) + .in(FranchiseeEntity::getId,ids)) + .stream() + .map(e->new FranchiseeIdName(e.getId(),e.getFranchiseeName(),e.getMobilePhone(),e.getFranchiseeCode())) + .collect(Collectors.toList()); + } + + @Override + public List getFranchiseeIdNameListByNames(Collection names) { + return baseMapper.selectList(new LambdaQueryWrapper() + .eq(FranchiseeEntity::getEnabledMark, 0) + .in(FranchiseeEntity::getFranchiseeName,names)) + .stream() + .map(e->new FranchiseeIdName(e.getId(),e.getFranchiseeName(),e.getMobilePhone(),e.getFranchiseeCode())) + .collect(Collectors.toList()); + } + + @Override + public List getFranchiseeIdNameListByCodes(Collection codes) { + return baseMapper.selectList(new LambdaQueryWrapper() + .eq(FranchiseeEntity::getEnabledMark, 0) + .in(FranchiseeEntity::getFranchiseeCode,codes)) + .stream() + .map(e->new FranchiseeIdName(e.getId(),e.getFranchiseeName(),e.getMobilePhone(),e.getFranchiseeCode())) + .collect(Collectors.toList()); + } + + /** + * 校验新增/编辑参数,并返回规范化后的自定义字段列表 + * + * @param req 请求参数 + * @return 规范化后的自定义字段列表 + */ + private List validateAddReq(FranchiseeAddReq req) { + if (req == null) { + throw new RuntimeException("请求参数不能为空"); + } + if (StrUtil.isBlank(req.getFranchiseeCode())) { + throw new RuntimeException("加盟商编号不能为空"); + } + if (StrUtil.isBlank(req.getFranchiseeName())) { + throw new RuntimeException("加盟商名称不能为空"); + } + if (StrUtil.isBlank(req.getMobilePhone())) { + throw new RuntimeException("手机号不能为空"); + } + if (req.getAge() != null && req.getAge() < 0) { + throw new RuntimeException("年龄不能小于0"); + } + if (req.getInvestStoreCount() != null && req.getInvestStoreCount() < 0) { + throw new RuntimeException("投资门店数不能小于0"); + } + if (req.getExpectedStaffCount() != null && req.getExpectedStaffCount() < 0) { + throw new RuntimeException("预计员工数不能小于0"); + } + + List customFieldList = normalizeAndValidateCustomFieldList(req.getCustomFieldList()); + validateExperience(req.getExperienceList()); + return customFieldList; + } + + /** + * 校验分页查询参数 + * + * @param req 查询参数 + */ + private void validateQueryReq(FranchiseeQueryReq req) { + if (req.getMinStoreNum() != null && req.getMaxStoreNum() != null + && req.getMinStoreNum() > req.getMaxStoreNum()) { + throw new RuntimeException("最小门店数不能大于最大门店数"); + } + if (req.getMinInvestAmount() != null && req.getMaxInvestAmount() != null + && req.getMinInvestAmount().compareTo(req.getMaxInvestAmount()) > 0) { + throw new RuntimeException("最小投资金额不能大于最大投资金额"); + } + if (req.getJoinDateStart() != null && req.getJoinDateEnd() != null + && req.getJoinDateStart().after(req.getJoinDateEnd())) { + throw new RuntimeException("加盟日期开始时间不能晚于结束时间"); + } + + Integer joinDurationStart = req.getJoinDurationStart(); + Integer joinDurationEnd = req.getJoinDurationEnd(); + if (joinDurationStart != null && joinDurationEnd != null + && joinDurationStart > joinDurationEnd) { + throw new RuntimeException("加盟时长开始不能大于结束"); + } + if(joinDurationStart != null && joinDurationStart < 0){ + throw new RuntimeException("加盟时长开始不能小于0"); + } + if(joinDurationEnd != null && joinDurationEnd < 0){ + throw new RuntimeException("加盟时长结束不能小于0"); + } + boolean joinDateQuery = req.getJoinDateStart() != null || req.getJoinDateEnd() != null; + if(joinDurationStart != null && !joinDateQuery){ + Date joinDateEnd = DateUtil.dateAddMonths(cn.hutool.core.date.DateUtil.beginOfDay(new Date()), -joinDurationStart); + req.setJoinDateEnd(joinDateEnd); + } + if(joinDurationEnd != null && !joinDateQuery){ + Date joinDateStart = DateUtil.dateAddMonths(cn.hutool.core.date.DateUtil.beginOfDay(new Date()), -joinDurationEnd); + req.setJoinDateStart(joinDateStart); + } + } + + /** + * 校验并规范化自定义字段列表 + * + * @param customFieldList 自定义字段列表 + * @return 规范化后的列表 + */ + private List validateCustomField(List customFieldList) { + if (CollUtil.isEmpty(customFieldList)) { + return Collections.emptyList(); + } + + Set nameSet = new LinkedHashSet<>(); + List result = new ArrayList<>(); + for (FranchiseeCustomFieldReq item : customFieldList) { + if (item == null) { + continue; + } + String fieldName = StrUtil.trim(item.getFieldName()); + if (StrUtil.isBlank(fieldName)) { + throw new RuntimeException("自定义字段名称不能为空"); + } + if (!nameSet.add(fieldName)) { + throw new RuntimeException("自定义字段名称不能重复"); + } + + FranchiseeCustomFieldReq normalizedItem = new FranchiseeCustomFieldReq(); + normalizedItem.setFieldName(fieldName); + normalizedItem.setFieldValue(StrUtil.trim(item.getFieldValue())); + result.add(normalizedItem); + } + return result; + } + + /** + * 解析并校验自定义字段 JSON 字符串 + * + * @param customFieldList 自定义字段 JSON 字符串 + * @return 规范化后的自定义字段列表 + */ + private List normalizeAndValidateCustomFieldList(String customFieldList) { + String json = StrUtil.blankToDefault(customFieldList, EMPTY_JSON_ARRAY).trim(); + if (!JSONUtil.isTypeJSONArray(json)) { + throw new RuntimeException("自定义字段列表必须为JSON数组"); + } + try { + List list = JSONUtil.toList(JSONUtil.parseArray(json), FranchiseeCustomFieldReq.class); + return validateCustomField(list); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("自定义字段列表必须为JSON数组"); + } + } + + /** + * 校验从业经历 + * + * @param experienceList 从业经历列表 + */ + private void validateExperience(List experienceList) { + if (CollUtil.isEmpty(experienceList)) { + return; + } + if (experienceList.size() > MAX_EXPERIENCE_COUNT) { + throw new RuntimeException("从业经历最多填写5条"); + } + + for (FranchiseeExperienceReq item : experienceList) { + if (item == null) { + continue; + } + Date startTime = item.getStartTime(); + Date endTime = item.getEndTime(); + if (startTime != null && endTime != null && startTime.after(endTime)) { + throw new RuntimeException("从业经历开始时间不能晚于结束时间"); + } + } + } + + /** + * 根据ID查询有效加盟商 + * + * @param id 主键ID + * @return 加盟商实体 + */ + private FranchiseeEntity getEntityById(String id) { + FranchiseeEntity entity = baseMapper.selectById(id); + if (entity == null || !Integer.valueOf(0).equals(entity.getEnabledMark())) { + throw new RuntimeException("加盟商不存在"); + } + return entity; + } + + /** + * 根据加盟商ID统计门店真实数量 + * + * @param franchiseeId 加盟商ID + * @return 门店数量 + */ + private int queryRealStoreNum(String franchiseeId) { + Map> storeInfoDetailsByFranchiseeId = organizationHelper.buildStoreInfoDetailsByFranchiseeId(franchiseeId); + if(Objects.isNull(storeInfoDetailsByFranchiseeId)){ + throw new RuntimeException("组织架构服务异常!"); + } + return storeInfoDetailsByFranchiseeId.size(); + } + + /** + * 删除自定义字段子表数据 + * + * @param franchiseeId 加盟商ID + */ + private void deleteCustomFieldByFranchiseeId(String franchiseeId) { + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(FranchiseeCustomFieldEntity::getFranchiseeId, franchiseeId); + franchiseeCustomFieldMapper.delete(deleteWrapper); + } + + /** + * 保存自定义字段子表数据 + * + * @param franchiseeId 加盟商ID + * @param customFieldReqList 自定义字段列表 + */ + private void saveCustomField(String franchiseeId, List customFieldReqList) { + if (CollUtil.isEmpty(customFieldReqList)) { + return; + } + + List entities = new ArrayList<>(customFieldReqList.size()); + long sorts = 1L; + for (FranchiseeCustomFieldReq item : customFieldReqList) { + if (item == null) { + continue; + } + FranchiseeCustomFieldEntity entity = new FranchiseeCustomFieldEntity(); + entity.setId(FtbUtil.getId()); + entity.setFranchiseeId(franchiseeId); + entity.setFieldName(StrUtil.trim(item.getFieldName())); + entity.setFieldValue(StrUtil.trim(item.getFieldValue())); + entity.setSorts(sorts++); + entity.setEnabledMark(0); + entities.add(entity); + } + if (CollUtil.isNotEmpty(entities)) { + franchiseeCustomFieldMapper.batchInsert(entities); + } + } + + /** + * 查询自定义字段值列表 + * + * @param franchiseeId 加盟商ID + * @return 自定义字段值列表 + */ + private List queryCustomFieldList(String franchiseeId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FranchiseeCustomFieldEntity::getFranchiseeId, franchiseeId); + queryWrapper.eq(FranchiseeCustomFieldEntity::getEnabledMark, 0); + queryWrapper.orderByAsc(FranchiseeCustomFieldEntity::getSorts); + + List entityList = franchiseeCustomFieldMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return Collections.emptyList(); + } + return entityList.stream().map(FranchiseeCustomFieldVO::convert).collect(Collectors.toList()); + } + + /** + * 删除从业经历子表数据 + * + * @param franchiseeId 加盟商ID + */ + private void deleteExperienceByFranchiseeId(String franchiseeId) { + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(FranchiseeExperienceEntity::getFranchiseeId, franchiseeId); + franchiseeExperienceMapper.delete(deleteWrapper); + } + + /** + * 删除加盟地区冗余数据。 + * + * @param franchiseeId 加盟商ID + */ + private void deleteJoinRegionByFranchiseeId(String franchiseeId) { + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.eq(FranchiseeJoinRegionEntity::getFranchiseeId, franchiseeId); + franchiseeJoinRegionMapper.delete(deleteWrapper); + } + + /** + * 保存加盟地区冗余数据。 + * + * @param franchiseeId 加盟商ID + * @param joinRegionReqs 加盟地区列表 + */ + private void saveJoinRegion(String franchiseeId, List joinRegionReqs) { + if (CollUtil.isEmpty(joinRegionReqs)) { + return; + } + List entities = new ArrayList<>(joinRegionReqs.size()); + for (FranchiseeJoinRegionReq item : joinRegionReqs) { + if (item == null) { + continue; + } + FranchiseeJoinRegionEntity entity = new FranchiseeJoinRegionEntity(); + entity.setId(FtbUtil.getId()); + entity.setFranchiseeId(franchiseeId); + entity.setProvince(item.getProvince()); + entity.setCity(item.getCity()); + entity.setDistrict(item.getDistrict()); + entity.setCounty(item.getCounty()); + entities.add(entity); + } + if (CollUtil.isNotEmpty(entities)) { + franchiseeJoinRegionMapper.batchInsert(entities); + } + } + + /** + * 保存从业经历 + * + * @param franchiseeId 加盟商ID + * @param experienceReqs 从业经历列表 + */ + private void saveExperience(String franchiseeId, List experienceReqs) { + if (CollUtil.isEmpty(experienceReqs)) { + return; + } + + List entities = new ArrayList<>(experienceReqs.size()); + long sorts = 1L; + for (FranchiseeExperienceReq item : experienceReqs) { + if (item == null || isEmptyExperienceItem(item)) { + continue; + } + + FranchiseeExperienceEntity entity = new FranchiseeExperienceEntity(); + entity.setId(FtbUtil.getId()); + entity.setFranchiseeId(franchiseeId); + entity.setCompanyName(StrUtil.trim(item.getCompanyName())); + entity.setPositionName(StrUtil.trim(item.getPositionName())); + entity.setStartTime(item.getStartTime()); + entity.setEndTime(item.getEndTime()); + entity.setIndustry(StrUtil.trim(item.getIndustry())); + entity.setWorkDescription(StrUtil.trim(item.getWorkDescription())); + entity.setSorts(sorts++); + entity.setEnabledMark(0); + entities.add(entity); + } + if (CollUtil.isNotEmpty(entities)) { + franchiseeExperienceMapper.batchInsert(entities); + } + } + + /** + * 判断从业经历是否为空行 + * + * @param item 从业经历 + * @return true-空行,false-非空行 + */ + private boolean isEmptyExperienceItem(FranchiseeExperienceReq item) { + return StrUtil.isBlank(item.getCompanyName()) + && StrUtil.isBlank(item.getPositionName()) + && item.getStartTime() == null + && item.getEndTime() == null + && StrUtil.isBlank(item.getIndustry()) + && StrUtil.isBlank(item.getWorkDescription()); + } + + /** + * 查询从业经历列表 + * + * @param franchiseeId 加盟商ID + * @return 从业经历返回列表 + */ + private List queryExperienceList(String franchiseeId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FranchiseeExperienceEntity::getFranchiseeId, franchiseeId); + queryWrapper.eq(FranchiseeExperienceEntity::getEnabledMark, 0); + queryWrapper.orderByAsc(FranchiseeExperienceEntity::getSorts); + + List entityList = franchiseeExperienceMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return Collections.emptyList(); + } + return entityList.stream().map(FranchiseeExperienceVO::convert).collect(Collectors.toList()); + } + + /** + * 新增加盟商主表 + * + * @param entity 加盟商主表实体 + */ + private void insertFranchiseeEntity(FranchiseeEntity entity) { + try { + baseMapper.insert(entity); + } catch (DuplicateKeyException e) { + if(containsDuplicateKey(e,"uk_franchisee_code")){ + throw new RuntimeException("加盟商编号已存在"); + } + if(containsDuplicateKey(e,"uk_franchisee_name")){ + throw new RuntimeException("加盟商名称已存在"); + } + throw new RuntimeException("加盟商编号或者加盟商名称已存在"); + } + } + + private boolean containsDuplicateKey(DuplicateKeyException e, String ukIndexName) { + if(Objects.isNull(e)){ + return false; + } + if(StringUtil.isBlank(ukIndexName)){ + return false; + } + String message = e.getMessage(); + if(StringUtil.isBlank(message)){ + return false; + } + return message.contains(ukIndexName); + } + + /** + * 更新加盟商主表 + * + * @param entity 加盟商主表实体 + */ + private void updateFranchiseeEntity(FranchiseeEntity entity) { + try { + baseMapper.updateById(entity); + } catch (DuplicateKeyException e) { + if(containsDuplicateKey(e,"uk_franchisee_code")){ + throw new RuntimeException("加盟商编号已存在"); + } + if(containsDuplicateKey(e,"uk_franchisee_name")){ + throw new RuntimeException("加盟商名称已存在"); + } + throw new RuntimeException("加盟商编号或者加盟商名称已存在"); + } + } + + /** + * 规范化加盟地区列表。 + * + * @param joinRegionReqs 加盟地区列表 + * @return 规范化后的加盟地区列表 + */ + private List normalizeJoinRegionList(List joinRegionReqs) { + if (CollUtil.isEmpty(joinRegionReqs)) { + return Collections.emptyList(); + } + Set uniqueKeys = new LinkedHashSet<>(); + List result = new ArrayList<>(); + for (FranchiseeJoinRegionReq item : joinRegionReqs) { + FranchiseeJoinRegionReq normalized = normalizeJoinRegionItem(item); + if (normalized == null) { + continue; + } + String uniqueKey = buildJoinRegionUniqueKey(normalized); + if (!uniqueKeys.add(uniqueKey)) { + continue; + } + result.add(normalized); + } + return result; + } + + /** + * 查询条件扩展:县补省市区/省市/省,区补省市/省,市补省。 + * + * @param joinRegionReqs 标准化后的加盟地区列表 + * @return 扩展后的加盟地区列表 + */ + private List expandQueryJoinRegionList(List joinRegionReqs) { + if (CollUtil.isEmpty(joinRegionReqs)) { + return Collections.emptyList(); + } + + Set uniqueKeys = new LinkedHashSet<>(); + List result = new ArrayList<>(); + for (FranchiseeJoinRegionReq item : joinRegionReqs) { + appendJoinRegion(result, uniqueKeys, item.getProvince(), item.getCity(), item.getDistrict(), item.getCounty()); + if (StrUtil.isNotBlank(item.getCounty())) { + appendJoinRegion(result, uniqueKeys, item.getProvince(), item.getCity(), item.getDistrict(), null); + appendJoinRegion(result, uniqueKeys, item.getProvince(), item.getCity(), null, null); + appendJoinRegion(result, uniqueKeys, item.getProvince(), null, null, null); + continue; + } + if (StrUtil.isNotBlank(item.getDistrict())) { + appendJoinRegion(result, uniqueKeys, item.getProvince(), item.getCity(), null, null); + appendJoinRegion(result, uniqueKeys, item.getProvince(), null, null, null); + continue; + } + if (StrUtil.isNotBlank(item.getCity())) { + appendJoinRegion(result, uniqueKeys, item.getProvince(), null, null, null); + } + } + return result; + } + + /** + * 添加加盟地区到结果集合并去重。 + * + * @param result 查询地区结果 + * @param uniqueKeys 去重Key + * @param province 省 + * @param city 市 + * @param district 区 + * @param county 县 + */ + private void appendJoinRegion(List result, Set uniqueKeys, + String province, String city, String district, String county) { + FranchiseeJoinRegionReq source = new FranchiseeJoinRegionReq(); + source.setProvince(province); + source.setCity(city); + source.setDistrict(district); + source.setCounty(county); + + FranchiseeJoinRegionReq normalized = normalizeJoinRegionItem(source); + if (normalized == null) { + return; + } + String uniqueKey = buildJoinRegionUniqueKey(normalized); + if (!uniqueKeys.add(uniqueKey)) { + return; + } + result.add(normalized); + } + + /** + * 标准化加盟地区单条记录。 + * + * @param item 加盟地区 + * @return 标准化后的记录 + */ + private FranchiseeJoinRegionReq normalizeJoinRegionItem(FranchiseeJoinRegionReq item) { + if (item == null) { + return null; + } + + String province = StrUtil.trim(item.getProvince()); + String city = StrUtil.trim(item.getCity()); + String district = StrUtil.trim(item.getDistrict()); + String county = StrUtil.trim(item.getCounty()); + if (StrUtil.isBlank(province)) { + return null; + } + + if (StrUtil.isBlank(city)) { + city = null; + district = null; + county = null; + } else if (StrUtil.isBlank(district)) { + district = null; + county = null; + } else if (StrUtil.isBlank(county)) { + county = null; + } + + FranchiseeJoinRegionReq normalized = new FranchiseeJoinRegionReq(); + normalized.setProvince(province); + normalized.setCity(city); + normalized.setDistrict(district); + normalized.setCounty(county); + return normalized; + } + + /** + * 加盟地区去重Key。 + * + * @param item 加盟地区 + * @return 去重Key + */ + private String buildJoinRegionUniqueKey(FranchiseeJoinRegionReq item) { + return item.getProvince() + + "#" + + StrUtil.blankToDefault(item.getCity(), "") + + "#" + + StrUtil.blankToDefault(item.getDistrict(), "") + + "#" + + StrUtil.blankToDefault(item.getCounty(), ""); + } + + /** + * 提取“区为空”时的省市集合。 + * + * @param joinRegionReqs 标准化后的加盟地区 + * @return 省市集合 + */ + private List extractProvinceCityJoinRegion(List joinRegionReqs) { + if (CollUtil.isEmpty(joinRegionReqs)) { + return Collections.emptyList(); + } + + Set uniqueKeys = new LinkedHashSet<>(); + List result = new ArrayList<>(); + for (FranchiseeJoinRegionReq item : joinRegionReqs) { + if (StrUtil.isBlank(item.getDistrict()) && StrUtil.isNotBlank(item.getCity())) { + String key = item.getProvince() + "#" + item.getCity(); + if (!uniqueKeys.add(key)) { + continue; + } + FranchiseeJoinRegionReq provinceCity = new FranchiseeJoinRegionReq(); + provinceCity.setProvince(item.getProvince()); + provinceCity.setCity(item.getCity()); + result.add(provinceCity); + } + } + return result; + } + + /** + * 提取“市为空”时的省集合。 + * + * @param joinRegionReqs 标准化后的加盟地区 + * @return 省集合 + */ + private List extractProvinceList(List joinRegionReqs) { + if (CollUtil.isEmpty(joinRegionReqs)) { + return Collections.emptyList(); + } + + Set provinceSet = new LinkedHashSet<>(); + for (FranchiseeJoinRegionReq item : joinRegionReqs) { + if (StrUtil.isBlank(item.getCity())) { + provinceSet.add(item.getProvince()); + } + } + return new ArrayList<>(provinceSet); + } + + /** + * 生成加盟商编码的key + * @param date + * @return + */ + private String buildFranchiseeCodeSeqKey(LocalDate date) { + return FRANCHISEE_CODE_SEQ_KEY_PREFIX + FRANCHISEE_CODE_DATE_FORMATTER.format(date); + } + + private Long createDailySequenceWhenAbsent(String key) { + FtbParamEntity entity = new FtbParamEntity(); + entity.setId(RandomUtil.uuId()); + entity.setKey(key); + entity.setType(key); + entity.setValue("1"); + entity.setSort(1L); + + try { + baseParamMapper.insertParam(entity); + return 1L; + } catch (DuplicateKeyException e) { + return incrementDailySequence(key); + } + } + + private Long incrementDailySequence(String key) { + int updateCount = baseParamMapper.incrementValueByKey(key, 1L); + if (updateCount == 0) { + return createDailySequenceWhenAbsent(key); + } + + FtbParamEntity entity = baseParamMapper.selectByKey(key); + if (entity == null || StrUtil.isBlank(entity.getValue())) { + return 1L; + } + try { + return Long.parseLong(StrUtil.trim(entity.getValue())); + } catch (NumberFormatException e) { + return 1L; + } + } + + private FranchiseeStoreVO convertStoreVO(StoreInfoDetailVO storeBaseInfo, + Map userBoundVOMap, + Map> certificateMap) { + FranchiseeStoreVO vo = new FranchiseeStoreVO(); + String storeId = storeBaseInfo.getId(); + vo.setStoreId(storeId); + vo.setStoreName(StrUtil.trim(storeBaseInfo.getStoreName())); + vo.setDisabled(storeBaseInfo.getDisabled()); + vo.setAreaNum(storeBaseInfo.getAreaNum()); + vo.setLongitude(StrUtil.trim(storeBaseInfo.getLongitude())); + vo.setLatitude(StrUtil.trim(storeBaseInfo.getLatitude())); + vo.setAddress(StrUtil.trim(storeBaseInfo.getAddress())); + vo.setUserNum(storeBaseInfo.getUserNum()); + vo.setOrganizeTreeName(storeBaseInfo.getOrganizeName()); + + String storeHeadUserId = storeBaseInfo.getStoreHeadUserId(); + UserBoundVO userBoundVO = userBoundVOMap.get(storeHeadUserId); + if(Objects.nonNull(userBoundVO)){ + vo.setStoreHeadUserName(userBoundVO.getName()); + vo.setStoreHeadUserId(storeHeadUserId); + } + + String longitude = StrUtil.blankToDefault(storeBaseInfo.getLongitude(), ""); + String latitude = StrUtil.blankToDefault(storeBaseInfo.getLatitude(), ""); + if (StrUtil.isAllBlank(longitude, latitude)) { + vo.setCoordinate(null); + } else { + vo.setCoordinate(longitude + "," + latitude); + } + + List regionList = storeBaseInfo.getRegionList(); + if(CollectionUtil.isNotEmpty(regionList)){ + StoreRegionVO storeRegionVO = regionList.get(0); + vo.setDeskCount(storeRegionVO.getDeskCount()); + vo.setSeatCount(storeRegionVO.getSeatCount()); + } + + List certificateInstanceEntities = certificateMap.get(storeId); + vo.setBusinessLicenseStatus(1); + vo.setBusinessLicenseStatusName(CertificateStatusUtils.buildStatusName(1)); + vo.setHygieneLicenseStatus(1); + vo.setHygieneLicenseStatusName(CertificateStatusUtils.buildStatusName(1)); + + if(CollectionUtil.isEmpty(certificateInstanceEntities)){ + return vo; + } + CertificateInstanceEntity businessLicense = null; + CertificateInstanceEntity hygieneLicense = null; + for (CertificateInstanceEntity certificateInstanceEntity : certificateInstanceEntities){ + String certificateType = certificateInstanceEntity.getCertificateType(); + if(CertificateTypeEnum.BUSINESS_LICENSE.getType().equals(certificateType)){ + businessLicense = certificateInstanceEntity; + } else if (CertificateTypeEnum.HYGIENE_LICENSE.getType().equals(certificateType)) { + hygieneLicense = certificateInstanceEntity; + } + } + if(Objects.nonNull(businessLicense)){ + Integer status = businessLicense.getStatus(); + vo.setBusinessLicenseStatus(status); + vo.setBusinessLicenseStatusName(CertificateStatusUtils.buildStatusName(status)); + } + if(Objects.nonNull(hygieneLicense)){ + Integer status = hygieneLicense.getStatus(); + vo.setHygieneLicenseStatus(status); + vo.setHygieneLicenseStatusName(CertificateStatusUtils.buildStatusName(status)); + } + return vo; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellStyleHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellStyleHandler.java new file mode 100644 index 0000000..5a66cb0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellStyleHandler.java @@ -0,0 +1,40 @@ +package jnpf.handler; + +import com.alibaba.excel.metadata.Head; +import com.alibaba.excel.write.handler.CellWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteTableHolder; +import org.apache.poi.ss.usermodel.*; + +public class CustomCellStyleHandler implements CellWriteHandler { + private CellStyle headerStyle; + private CellStyle contentStyle; + + @Override + public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { + Workbook workbook = writeSheetHolder.getSheet().getWorkbook(); + // 初始化表头样式(只创建一次) + if (headerStyle == null) { + headerStyle = workbook.createCellStyle(); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerFont.setFontName("黑体"); + headerFont.setFontHeightInPoints((short) 12); + headerStyle.setFont(headerFont); + } + // 初始化内容样式(只创建一次) + if (contentStyle == null) { + contentStyle = workbook.createCellStyle(); + contentStyle.setAlignment(HorizontalAlignment.CENTER); + contentStyle.setVerticalAlignment(VerticalAlignment.CENTER); + } + // 根据是否为表头应用对应的样式 + if (isHead) { + cell.setCellStyle(headerStyle); + } else { + cell.setCellStyle(contentStyle); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellWriteHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellWriteHandler.java new file mode 100644 index 0000000..21bbe7d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomCellWriteHandler.java @@ -0,0 +1,60 @@ +package jnpf.handler; + +import com.alibaba.excel.write.handler.CellWriteHandler; +import com.alibaba.excel.write.handler.context.CellWriteHandlerContext; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Sheet; + +import java.util.HashMap; +import java.util.Map; + +public class CustomCellWriteHandler implements CellWriteHandler { + // 记录每列的最大宽度(列索引 -> 最大宽度) + private final Map maxColumnWidthMap = new HashMap<>(); + + @Override + public void afterCellDispose(CellWriteHandlerContext context) { + // 设置列宽的逻辑 + Sheet sheet = context.getWriteWorkbookHolder().getWorkbook().getSheetAt(0); + int columnIndex = context.getCell().getColumnIndex(); + // 示例1:固定列宽 + sheet.setColumnWidth(columnIndex, 20 * 256); + // 示例2:根据内容动态调整列宽(简单逻辑) + String content = getCellValueAsString(context.getCell()); + // 计算单元格内容宽度(考虑中文) + int width = calculateColumnWidth(content); + // 更新最大列宽 + if (width > maxColumnWidthMap.getOrDefault(columnIndex, 0)) { + maxColumnWidthMap.put(columnIndex, width); + sheet.setColumnWidth(columnIndex, width * 256); + } + } + + // 动态计算列宽(中文字符按 2 倍宽度计算) + private int calculateColumnWidth(String text) { + int chineseCharCount = countChineseCharacters(text); + int otherCharCount = text.length() - chineseCharCount; + return (int) (chineseCharCount * 2 * 1.2 + otherCharCount * 1.0); // 额外增加 20% 缓冲 + } + + // 统计中文字符数量 + private int countChineseCharacters(String text) { + return (int) text.chars().filter(c -> c >= 0x4E00 && c <= 0x9FA5).count(); + } + + private String getCellValueAsString(Cell cell) { + String result = null; + switch (cell.getCellType()) { + case STRING: + result = cell.getStringCellValue(); + break; + case NUMERIC: + result = String.valueOf(cell.getNumericCellValue()); + break; + case BOOLEAN: + result = String.valueOf(cell.getBooleanCellValue()); + break; + } + return result; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomRowHeightHandler.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomRowHeightHandler.java new file mode 100644 index 0000000..3d0dcac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/handler/CustomRowHeightHandler.java @@ -0,0 +1,25 @@ +package jnpf.handler; + +import com.alibaba.excel.write.handler.RowWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteTableHolder; +import org.apache.poi.ss.usermodel.Row; + +public class CustomRowHeightHandler implements RowWriteHandler { + + // 表头行高(单位:点) + private static final float HEAD_ROW_HEIGHT = 20f; + // 内容行高(单位:点) + private static final float CONTENT_ROW_HEIGHT = 17f; + + @Override + public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) { + if (isHead) { + // 设置表头行高 + row.setHeightInPoints(HEAD_ROW_HEIGHT); + } else { + // 设置内容行高 + row.setHeightInPoints(CONTENT_ROW_HEIGHT); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/controller/HelpAiLogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/controller/HelpAiLogController.java new file mode 100644 index 0000000..5c491ef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/controller/HelpAiLogController.java @@ -0,0 +1,62 @@ +package jnpf.helpailog.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.helpailog.service.FtbHelpAiChatLogService; +import jnpf.model.helpailog.dto.HelpAiLogDto; +import jnpf.model.helpailog.dto.HelpAiLogReq; +import jnpf.model.helpailog.po.FtbHelpAiChatLog; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * 帮助ai 聊天记录 + * + * @author xgl + */ +@RestController +@Slf4j +@RequestMapping("/help-ai-chat-log") +public class HelpAiLogController { + + + @Autowired + private UserProvider userProvider; + + @Autowired + private FtbHelpAiChatLogService ftbHelpAiChatLogService; + + /** + * @param dto + * @return + */ + @PostMapping("/record") + public ActionResult record(@Validated @RequestBody HelpAiLogDto dto) { + UserInfo userInfo = userProvider.get(); + String userId = userInfo.getUserId(); + String tenantId = userInfo.getTenantId(); + String userName = userInfo.getUserName(); + ftbHelpAiChatLogService.record(dto.convertToEntity(userId, userName, tenantId)); + return ActionResult.success(); + } + + + /** + * 分页查询聊天 + * + * @param req + * @return + */ + @GetMapping("/list") + public ActionResult> pageLists(HelpAiLogReq req) { + PageInfo pageVo = ftbHelpAiChatLogService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/mapper/FtbHelpAiChatLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/mapper/FtbHelpAiChatLogMapper.java new file mode 100644 index 0000000..82fc6ce --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/mapper/FtbHelpAiChatLogMapper.java @@ -0,0 +1,15 @@ +package jnpf.helpailog.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.helpailog.po.FtbHelpAiChatLog; + +/** + * 帮助聊天日志 + * + * @author xgl + * @create 2024-09-29 + */ +public interface FtbHelpAiChatLogMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/FtbHelpAiChatLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/FtbHelpAiChatLogService.java new file mode 100644 index 0000000..0bdb31c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/FtbHelpAiChatLogService.java @@ -0,0 +1,27 @@ +package jnpf.helpailog.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.helpailog.dto.HelpAiLogReq; +import jnpf.model.helpailog.po.FtbHelpAiChatLog; + +/** + * ai聊天日志服务接口 + */ +public interface FtbHelpAiChatLogService extends IService { + /** + * 记录聊天日志 + * + * @param entity 实体 + */ + void record(FtbHelpAiChatLog entity); + + /** + * 分页查询 + * + * @param req + * @return + */ + + PageInfo getPageList(HelpAiLogReq req); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/impl/FtbHelpAiChatLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/impl/FtbHelpAiChatLogServiceImpl.java new file mode 100644 index 0000000..232dd09 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/helpailog/service/impl/FtbHelpAiChatLogServiceImpl.java @@ -0,0 +1,59 @@ +package jnpf.helpailog.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.helpailog.mapper.FtbHelpAiChatLogMapper; +import jnpf.helpailog.service.FtbHelpAiChatLogService; +import jnpf.model.helpailog.dto.HelpAiLogReq; +import jnpf.model.helpailog.po.FtbHelpAiChatLog; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.List; + + +@Service +public class FtbHelpAiChatLogServiceImpl extends ServiceImpl implements FtbHelpAiChatLogService { + /** + * 添加 + * + * @param entity 实体 + */ + @Override + public void record(FtbHelpAiChatLog entity) { + baseMapper.insert(entity); + } + + /** + * 分页查询 + * + * @param req + * @return + */ + @Override + public PageInfo getPageList(HelpAiLogReq req) { + if (StringUtils.isNotEmpty(req.getSessionId())) { + req.setSessionId(StringUtils.trim(req.getSessionId())); + } + //构建分页 + Page page = new Page(req.getCurrentPage(), req.getPageSize()); + //构建查询 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(!StringUtils.isEmpty(req.getSessionId()), FtbHelpAiChatLog::getSessionId, req.getSessionId()) + .orderByDesc(FtbHelpAiChatLog::getCreatorTime); + Page queryPage = baseMapper.selectPage(page, wrapper); + List records = queryPage.getRecords(); + + + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/LogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/LogController.java new file mode 100644 index 0000000..dcc507f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/LogController.java @@ -0,0 +1,150 @@ +package jnpf.memberLog.controller; + +import jnpf.base.ActionResult; +import jnpf.memberLog.service.LogService; +import jnpf.model.memberLog.dto.LogAgentAlertDto; +import jnpf.model.memberLog.dto.LogMemberLogDto; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/08/07 + */ +@RestController +@RequestMapping(value = "/log") +public class LogController { + @Resource + private LogService logService; + + + /** + * 获取模板列表 + */ + @GetMapping("/template/list") + public ActionResult getTemplateList() { + return ActionResult.success(logService.getTemplateList()); + } + + /** + * web--日志类型的下拉框筛选 + */ + @GetMapping("/getTemplateList") + public ActionResult getTemplateNameList() { + return ActionResult.success(logService.getTemplateNameList()); + } + /** + * 新增日志 + * @param logMemberLogDto 日志信息 + * @return 返回值 + */ + @PostMapping() + public ActionResult addLog(@RequestBody LogMemberLogDto logMemberLogDto) { + logService.addLog(logMemberLogDto); + return ActionResult.success(logMemberLogDto.getId()); + } + + /** + * 给工作日志相关人员发im消息 + * @param dto + * @return + */ + @PostMapping("/agentAlert") + public ActionResult agentAlert(@RequestBody LogAgentAlertDto dto) { + logService.agentAlert(dto); + return ActionResult.success(); + } + + /** + * 获取日志详情 + * @param logId 日志id + * @return 返回值 + */ + @GetMapping("/detail/{logId}") + public ActionResult getLogDetail(@PathVariable("logId") String logId) { + return ActionResult.success(logService.getLogDetail(logId)); + } + /** + * 获取日志详情 + * @param logId 日志id + * @return 返回值 + */ + @GetMapping("/webDetail/{logId}") + public ActionResult getWebLogDetail(@PathVariable("logId") String logId) { + return ActionResult.success(logService.getLogDetail(logId)); + } + + /** + * 点击日志接收人出现的列表--接收范围 + * @param logId 日志id + * @return 返回值 + */ + @GetMapping("/receiveRange/{logId}") + public ActionResult getReceiveRange(@PathVariable("logId") String logId) { + return ActionResult.success(logService.getReceiveRange(logId)); + } + + /** + * 点击日志接收人出现的列表--已读 + * @param logId 日志id + * @return 返回值 + */ + @GetMapping("/read/{logId}") + public ActionResult getReadList(@PathVariable("logId") String logId) { + return ActionResult.success(logService.getReadList(logId)); + } + + /** + * 点击日志接收人出现的列表--未读 + * @param logId 日志id + * @return 返回值 + */ + @GetMapping("/unread/{logId}") + public ActionResult getUnReadList(@PathVariable("logId") String logId) { + return ActionResult.success(logService.getUnReadList(logId)); + } + + + /** + * 点击日志接收人出现的列表--点赞列表 + * @param logId 日志id + * @return 返回值 + */ + @GetMapping("/like/{logId}") + public ActionResult getLikeList(@PathVariable("logId") String logId) { + return ActionResult.success(logService.getLikeList(logId)); + } + + /** + * 修改日志 + * @param logId 日志id + * @param logMemberLogDto 日志信息 + * @return 返回值 + */ + @PutMapping("/{logId}") + public ActionResult updateLog(@PathVariable("logId") String logId,@RequestBody LogMemberLogDto logMemberLogDto) { + logMemberLogDto.setId(logId); + logService.updateLog(logMemberLogDto); + return ActionResult.success(); + } + + /** + * 删除指定日志 + * @param logId 日志id + */ + @DeleteMapping("/{logId}") + public ActionResult deleteLog(@PathVariable("logId") String logId) { + Integer i = logService.deleteLog(logId); + if (i == -1){ + return ActionResult.fail("未找到该数据!"); + } + if (i == -2){ + return ActionResult.fail("不能删除其他人的日志!"); + } + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/WorkLogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/WorkLogController.java new file mode 100644 index 0000000..23b3e56 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/controller/WorkLogController.java @@ -0,0 +1,223 @@ +package jnpf.memberLog.controller; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.exception.QueryException; +import jnpf.memberLog.service.WorkLogService; +import jnpf.model.memberLog.dto.*; +import jnpf.model.memberLog.vo.*; +import jnpf.util.*; +import jnpf.util.data.DataSourceContextHolder; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.rmi.server.ExportException; +import java.util.List; + +/** + * 工作日志控制器 + * + * @author yanwenfu + * @create 2023-08-08 + */ +@RestController +@RequestMapping(value = "/log") +public class WorkLogController { + + @Resource + private WorkLogService workLogService; + + @Resource + private ExportUtil exportUtil; + + @Autowired + private ConfigValueUtil configValueUtil; + + /** + * 评论列表 + * @param logId 日志id + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/comment/list/{logId}") + public ActionResult> getCommentList(@PathVariable(value = "logId") String logId, @Valid CommentQueryDto queryDto) { + + PageInfo page = workLogService.getCommentList(logId, queryDto); + CommentPaginationVo pagination = new CommentPaginationVo(FtbUtil.getPagination(page)); + pagination.setCommentTotalNum(workLogService.getCommentTotalNum(logId)); + return ActionResult.page(page.getList(), pagination); + } + + /** + * 日志详情 - 评论/回复 + * @param logId 日志id + * @param addReplyDto 新增评论dto + * @return java.lang.Object + */ + @PostMapping(value = "/comment/reply/{logId}") + public ActionResult addCommentReply(@PathVariable(value = "logId") String logId, @RequestBody @Valid AddReplyDto addReplyDto) { + + LogCommentVo comment = workLogService.addCommentReply(logId, addReplyDto); + return ActionResult.success(comment); + } + + /** + * 删除评论/回复 + * @param commentId 评论id + * @return java.lang.Object + */ + @DeleteMapping(value = "/comment/reply/{commentId}") + public Object deleteCommentReply(@PathVariable(value = "commentId") String commentId) { + + workLogService.deleteCommentReply(commentId); + return ActionResult.success(); + } + + /** + * 新增/取消点赞 + * @param logId 日志id + * @return java.lang.Object + */ + @PostMapping(value = "/like/{logId}") + public Object addOrCancelLike(@PathVariable(value = "logId") String logId) { + + workLogService.addOrCancelLike(logId); + return ActionResult.success(); + } + + /** + * 新增转发信息 + * @param logId 日志id + * @param list 转发信息 + * @return java.lang.Object + */ + @PostMapping(value = "/forward/{logId}") + public Object addForwardRecord(@PathVariable(value = "logId") String logId, @RequestBody @Valid List list) { + + workLogService.addForwardRecord(logId, list); + return ActionResult.success(); + } + + /** + * 日志统计 + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/dataStat") + public ActionResult getLogDataStat(@Valid StatQueryDto queryDto) throws Exception { + + LogStatVo logStat = workLogService.getLogDataStat(queryDto); + return ActionResult.success(logStat); + } + + /** + * 日志统计(按人分页) + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/dataStat/page") + public ActionResult getLogDataStatPage(@Valid StatQueryDto queryDto) throws Exception { + + LogStatVo logStat = workLogService.getLogDataStatPage(queryDto); + return ActionResult.success(logStat); + } + + /** + * 日志统计 - 导出 + * @param queryDto 查询条件 + * @param response HttpServletResponse + */ + @NoDataSourceBind + @GetMapping(value = "/dataStat/export") + public void getLogDataStatExport(@Valid StatQueryDto queryDto, HttpServletResponse response) throws Exception { + if(StringUtil.isNotEmpty(queryDto.getTenantId())){ + switchTenant(queryDto.getTenantId()); + } + Workbook workbook = workLogService.getLogDataStatWorkbook(queryDto); + if (null != workbook) { + exportUtil.exportTemplateExcel(workbook, response, ConstantUtil.LOG_DATA_FORM); + } else { + throw new ExportException("导出" + ConstantUtil.LOG_DATA_FORM + "失败"); + } + } + + /** + * 切换租户 + * @param tenantId 租户id + */ + public void switchTenant(String tenantId){ + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } + /** + * 日志列表 - app + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/list") + public ActionResult> getAppLogPage(@Valid LogQueryDto queryDto) { + + PageInfo page = workLogService.getAppLogPage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 日志列表 - 条件筛选 - app + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/list/byCondition") + public ActionResult> getAppLogPageByCondition(ConditionLogQueryDto queryDto) throws QueryException { + + PageInfo page = workLogService.getAppLogPageByCondition(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 日志列表 - 指定人员查询 - app + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/list/byUser") + public ActionResult> getAppLogPageByUser(ConditionLogQueryDto queryDto) throws QueryException { + + PageInfo page = workLogService.getAppLogPageByUser(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 日志列表 - web + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/list/webPage") + public ActionResult> getWebLogPage(@Valid LogWebQueryDto queryDto) throws QueryException { + + PageInfo page = workLogService.getWebLogPage(queryDto); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogCommentMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogCommentMapper.java new file mode 100644 index 0000000..41d6024 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogCommentMapper.java @@ -0,0 +1,7 @@ +package jnpf.memberLog.mapper; + + + +public interface LogCommentMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberAssociationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberAssociationMapper.java new file mode 100644 index 0000000..1edcba8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberAssociationMapper.java @@ -0,0 +1,67 @@ +package jnpf.memberLog.mapper; + + +import jnpf.model.memberLog.dto.LogMemberAssociationDto; +import jnpf.model.memberLog.po.LogMemberAssociation; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface LogMemberAssociationMapper { + + /** + * 批量新增成员信息 + * @param logMemberAssociationList 成员信息 + * @param id 日志主表id + */ + void addMemberAssociation(@Param("logMemberAssociationList") List logMemberAssociationList, @Param("id") String id); + + /** + * 通过日志id获取关联用户信息 + * @param logId 日志id + * @return 返回值 + */ + List getReceiveRange(@Param("logId") String logId); + + /** + * 日志阅读相关查询 + * @param logId 日志id + * @return 返回值 + */ + List getReadDependence(@Param("logId") String logId, @Param("read") Integer read); + + /** + * 获取日志点赞列表 + * @param logId 日志id + * @return 返回值 + */ + List getLikeList(@Param("logId") String logId); + + /** + * 修改指定日志下的所有人为未读 + * @param id 日志id + */ + void updateUnRead(@Param("id") String id); + + /** + * 当前用户点赞状态 + * @param logId 日志id + * @param userId 用户id + * @return 返回值 + */ + String getMeLike(@Param("logId") String logId, @Param("userId") String userId); + + /** + * 获取当前用户在该日志中汇报和抄送中未读的记录 + * @param userId 用户id + * @param logId 日志id + * @return 返回值 + */ + List getMeUnRead(@Param("userId") String userId, @Param("logId") String logId); + + /** + * 阅读指定信息 + * @param ids ids + */ + void readByIds(@Param("ids") List ids); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberFieldMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberFieldMapper.java new file mode 100644 index 0000000..ba00355 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberFieldMapper.java @@ -0,0 +1,34 @@ +package jnpf.memberLog.mapper; + + +import jnpf.model.memberLog.dto.LogMemberFieldDto; +import jnpf.model.memberLog.vo.LogMemberFieldVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface LogMemberFieldMapper { + + + /** + * 批量新增日志字段 + * @param logMemberFieldList 字段信息 + * @param id 日志主表id + */ + void addField(@Param("logMemberFieldList") List logMemberFieldList, @Param("id") String id); + + /** + * 通过日志id获取字段 + * @param logId 日志id + * @return 返回值 + */ + List getFieldById(@Param("logId") String logId); + + /** + * 通过日志id删除下面的字段 + * @param id 日志id + */ + void deleteByLogId(@Param("id") String id); + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberLogMapper.java new file mode 100644 index 0000000..57e46c5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogMemberLogMapper.java @@ -0,0 +1,44 @@ +package jnpf.memberLog.mapper; + +import jnpf.model.memberLog.dto.LogMemberLogDto; +import jnpf.model.memberLog.vo.LogMemberLogVo; +import org.apache.ibatis.annotations.Param; + +public interface LogMemberLogMapper { + + /** + * 新增成员日志 + * @param logMemberLogDto 日志信息 + * @return 返回值 + */ + void addLog(@Param("logMemberLogDto") LogMemberLogDto logMemberLogDto, @Param("userId") String userId); + + /** + * 通过日志id获取日志详情 + * @param logId 日志id + * @return 返回值 + */ + LogMemberLogVo getLogDetail(@Param("logId") String logId); + + /** + * 修改日志主表 + * @param logMemberLogDto 日志信息 + * @param userId 修改人 + * @return 返回值 + */ + void updateLog(@Param("logMemberLogDto") LogMemberLogDto logMemberLogDto, @Param("userId") String userId); + + /** + * 删除指定日志 + * @param logId 日志id + * @param userId 用户id + */ + void deleteLog(@Param("logId") String logId, @Param("userId") String userId); + + /** + * 查询日志相关信息 + * @param logId 日志id + * @return + */ + LogMemberLogVo selectById(@Param("logId") String logId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateFieldMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateFieldMapper.java new file mode 100644 index 0000000..20891b5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateFieldMapper.java @@ -0,0 +1,16 @@ +package jnpf.memberLog.mapper; + +import jnpf.model.memberLog.vo.LogTemplateFieldVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface LogTemplateFieldMapper { + + /** + * 通过模板id获取下面的字段 + * @param id 模板id + * @return 返回值 + */ + List getFieldById(@Param("id") String id); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateMapper.java new file mode 100644 index 0000000..36f7a0e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/LogTemplateMapper.java @@ -0,0 +1,14 @@ +package jnpf.memberLog.mapper; + +import jnpf.model.memberLog.vo.LogTemplateVo; + +import java.util.List; + +public interface LogTemplateMapper { + + /** + * 获取启用中的模板列表 + */ + List getTemplateList(); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/WorkLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/WorkLogMapper.java new file mode 100644 index 0000000..d536efb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/mapper/WorkLogMapper.java @@ -0,0 +1,190 @@ +package jnpf.memberLog.mapper; + +import jnpf.model.memberLog.dto.CommentQueryDto; +import jnpf.model.memberLog.dto.LogQueryDto; +import jnpf.model.memberLog.dto.StatQueryDto; +import jnpf.model.memberLog.po.LogComment; +import jnpf.model.memberLog.vo.*; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 工作日志mapper + * + * @author yanwenfu + * @create 2023-08-08 + */ +public interface WorkLogMapper { + + /** + * 查询日志主表 + * @param logId 日志id + * @return jnpf.model.memberLog.vo.MiniMemberLogVo + */ + MiniMemberLogVo getLog(String logId); + + /** + * 查询评论列表 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getCommentList(CommentQueryDto queryDto); + + /** + * 查询日志评论 + * @param id 评论id + * @return jnpf.model.memberLog.po.LogComment + */ + LogComment getLogComment(String id); + + /** + * 新增日志评论 + * @param logComment 评论 + * @return int + */ + int addCommentReply(LogComment logComment); + + /** + * 查询成员日志信息关联 + * @param logId 日志id + * @param userId 用户id + * @param type 类型(1: 汇报, 2: 抄送, 3: 转发, 4: 点赞) + * @return jnpf.model.memberLog.vo.LogAssociationVo + */ + LogAssociationVo getLogAssociation(@Param("logId") String logId, @Param("userId") String userId, @Param("type") int type); + + /** + * 删除点赞记录 + * @param associationId 关联id + * @return int + */ + int deleteLikeRecord(String associationId); + + /** + * 新增点赞记录 + * @param association 点赞信息 + * @return int + */ + int addAssociationRecord(LogAssociationVo association); + + /** + * 根据日期统计用户写日志数量 + * @param queryDto 查询条件 + * @param userIds 筛选成员 + * @return java.util.List + */ + List getLogDataStat(@Param("queryDto") StatQueryDto queryDto, @Param("userIds") List userIds); + + /** + * 查询与我相关的全部日志列表 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getAllLogList(LogQueryDto queryDto); + + /** + * 我发布的日志列表 + * @param queryDto 查询条件 + * @return java.util.List + */ + List getMyLogList(LogQueryDto queryDto); + + /** + * 查询与我部门相关的日志列表 + * @param userId 用户id + * @param orgUserIds 部门用户ids + * @param beginDate 开始时间 + * @param endDate 结束时间 + * @return java.util.List + */ + List getMyOrganizeLogList(@Param("userId") String userId, @Param("orgUserIds") List orgUserIds, @Param("beginDate") String beginDate, @Param("endDate") String endDate); + + /** + * 可查看的字段列表 + * @param logIds 日志ids + * @return java.util.List + */ + List getViewableFieldList(@Param("logIds") List logIds); + + /** + * 查询日志列表 + * @param queryType 类型(日志, 日报, 周报, 月报, 拜访记录, 业绩日报) + * @param beginDate 开始时间(yyyy-MM-dd) + * @param endDate 结束时间(yyyy-MM-dd) + * @param content 模糊查询(内容) + * @param userIds 用户ids + * @return java.util.List + */ + List getWebLogList(@Param("queryType") String queryType, @Param("beginDate") String beginDate, @Param("endDate") String endDate, + @Param("content") String content, @Param("userIds") List userIds); + + /** + * 查询我创建的, 别人抄送/汇报给我的 + * @param queryType 类型(日志, 日报, 周报, 月报, 拜访记录, 业绩日报) + * @param beginDate 开始时间(yyyy-MM-dd) + * @param endDate 结束时间(yyyy-MM-dd) + * @param content 模糊查询(内容) + * @param userIds 用户ids + * @param userId 当前登录用户id + * @param selfQuery 是否查询自己创建的(1: 是, 0: 否) + * @return java.util.List + */ + List getWebLogListByAuth(@Param("queryType") String queryType, @Param("beginDate") String beginDate, @Param("endDate") String endDate, + @Param("content") String content, @Param("userIds") List userIds, @Param("userId") String userId, @Param("selfQuery") String selfQuery); + + /** + * 根据条件查询日志列表 + * @param userId 当前登录人 + * @param content 内容 + * @param userIds 用户ids + * @return java.util.List + */ + List getLogListByCondition(@Param("userId") String userId, @Param("content") String content, @Param("userIds") List userIds); + + /** + * 日志列表 - 指定人员查询 + * @param userId 用户id + * @param beginDate 开始时间 + * @param endDate 结束时间 + * @return java.util.List + */ + List getLogListByUser(@Param("userId") String userId, @Param("beginDate") String beginDate, @Param("endDate") String endDate); + + /** + * 查询评论/回复详情 + * @param id 评论/回复id + * @return jnpf.model.memberLog.vo.LogCommentVo + */ + LogCommentVo getLogCommentVo(String id); + + /** + * 删除回复/评论 + * @param commentId 评论id + * @param userId 删除人id + * @return int + */ + int deleteLogCommentByPath(@Param("commentId") String commentId, @Param("userId") String userId); + + /** + * 批量新增转发信息 + * @param list 转发信息 + * @return int + */ + int addAssociationRecordBatch(@Param("list") List list); + + /** + * 评论总条数 + * @param logId 日志id + * @return java.lang.Integer + */ + Integer getCommentTotalNum(String logId); + + /** + * 删除评论 + * @param commentId 评论id + * @param userId 用户id + * @return int + */ + int deleteLogCommentById(@Param("commentId") String commentId, @Param("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/LogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/LogService.java new file mode 100644 index 0000000..875ff7a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/LogService.java @@ -0,0 +1,89 @@ +package jnpf.memberLog.service; + +import jnpf.model.memberLog.dto.LogAgentAlertDto; +import jnpf.model.memberLog.dto.LogMemberLogDto; +import jnpf.model.memberLog.po.LogMemberAssociation; +import jnpf.model.memberLog.vo.LogMemberLogVo; +import jnpf.model.memberLog.vo.LogTemplateVo; +import jnpf.model.memberLog.vo.ReceiveRangeVo; + +import java.util.List; + +/** + * 日志服务 + * + * @author HuangLinPan + * @date 2023/08/07 + */ + +public interface LogService { + /** + * 获取模板列表 + * @return jnpf.model.memberLog.vo.LogTemplateVo + */ + List getTemplateList(); + + /** + * 新增日志信息 + * @param logMemberLogDto 日志信息 + * @return 返回值 + */ + void addLog(LogMemberLogDto logMemberLogDto); + + /** + * 获取日志详情 + * @param logId 日志id + * @return 返回值 + */ + LogMemberLogVo getLogDetail(String logId); + + /** + * 点击日志接收人出现的列表--接收范围 + * @param logId 日志id + * @return 返回值 + */ + ReceiveRangeVo getReceiveRange(String logId); + + /** + * 点击日志接收人出现的列表--已读 + * @param logId 日志id + * @return 返回值 + */ + List getReadList(String logId); + + /** + * 点击日志接收人出现的列表--未读 + * @param logId 日志id + * @return 返回值 + */ + List getUnReadList(String logId); + + /** + * 获取日志点赞列表 + * @param logId 日志id + * @return 返回值 + */ + List getLikeList(String logId); + + /** + * 修改日志 + * @param logMemberLogDto 日志信息 + * @return 返回值 + */ + void updateLog(LogMemberLogDto logMemberLogDto); + + List getTemplateNameList(); + + /** + * 删除指定日志 + * @param logId 日志id + * @return 状态 + */ + Integer deleteLog(String logId); + + /** + * 给工作日志相关人员发im消息 + * @param dto + */ + void agentAlert(LogAgentAlertDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/WorkLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/WorkLogService.java new file mode 100644 index 0000000..786f34e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/WorkLogService.java @@ -0,0 +1,112 @@ +package jnpf.memberLog.service; + +import com.github.pagehelper.PageInfo; +import jnpf.exception.QueryException; +import jnpf.model.memberLog.dto.*; +import jnpf.model.memberLog.vo.LogCommentVo; +import jnpf.model.memberLog.vo.LogStatVo; +import jnpf.model.memberLog.vo.MiniLogVo; +import jnpf.model.memberLog.vo.WebLogVo; +import org.apache.poi.ss.usermodel.Workbook; + +import java.util.List; + +/** + * 工作日志服务 + * + * @author yanwenfu + * @create 2023-08-08 + */ +public interface WorkLogService { + + /** + * 评论列表 + * @param logId 日志id + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getCommentList(String logId, CommentQueryDto queryDto); + + /** + * 日志详情 - 评论/回复 + * @param logId 日志id + * @param addReplyDto 新增评论dto + * @return jnpf.model.memberLog.vo.LogCommentVo + */ + LogCommentVo addCommentReply(String logId, AddReplyDto addReplyDto); + + /** + * 新增/取消点赞 + * @param logId 日志id + */ + void addOrCancelLike(String logId); + + /** + * 新增转发信息 + * @param logId 日志id + * @param list 转发信息 + */ + void addForwardRecord(String logId, List list); + + /** + * 日志统计 + * @param queryDto 查询条件 + * @return jnpf.model.memberLog.vo.LogStatVo + */ + LogStatVo getLogDataStat(StatQueryDto queryDto) throws Exception; + + /** + * 日志统计 - 导出 + * @param queryDto 查询条件 + * @return org.apache.poi.ss.usermodel.Workbook + */ + Workbook getLogDataStatWorkbook(StatQueryDto queryDto) throws Exception; + + /** + * 日志列表 + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getAppLogPage(LogQueryDto queryDto); + + /** + * 日志列表 - web + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getWebLogPage(LogWebQueryDto queryDto) throws QueryException; + + /** + * 日志列表 - 条件筛选 - app + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getAppLogPageByCondition(ConditionLogQueryDto queryDto) throws QueryException; + + /** + * 日志列表 - 指定人员查询 - app + * @param queryDto 查询条件 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getAppLogPageByUser(ConditionLogQueryDto queryDto); + + /** + * 删除评论/回复 + * @param commentId 评论id + */ + void deleteCommentReply(String commentId); + + /** + * 评论总条数 + * @param logId 日志id + * @return java.lang.Integer + */ + Integer getCommentTotalNum(String logId); + + /** + * 日志统计(按人分页) + * @param queryDto 查询条件 + * @return jnpf.model.memberLog.vo.LogStatVo + */ + LogStatVo getLogDataStatPage(StatQueryDto queryDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/LogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/LogServiceImpl.java new file mode 100644 index 0000000..9bbfec3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/LogServiceImpl.java @@ -0,0 +1,598 @@ +package jnpf.memberLog.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.enums.ConstantStatusEnum; +import jnpf.enums.OptTypeEnum; +import jnpf.enums.memberLog.MemberLogContentEnum; +import jnpf.enums.memberLog.MemberLogImEnum; +import jnpf.enums.memberLog.MemberLogImJumpEnum; +import jnpf.from.*; +import jnpf.memberLog.mapper.*; +import jnpf.memberLog.service.LogService; +import jnpf.model.memberLog.dto.LogAgentAlertDto; +import jnpf.model.memberLog.dto.LogMemberAssociationDto; +import jnpf.model.memberLog.dto.LogMemberFieldDto; +import jnpf.model.memberLog.dto.LogMemberLogDto; +import jnpf.model.memberLog.po.LogMemberAssociation; +import jnpf.model.memberLog.vo.*; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.FtbUtil; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +/** + * describe + * 日志实现 + * + * @author HuangLinPan + * @date 2023/08/07 + */ +@Slf4j +@Service +public class LogServiceImpl implements LogService { + + @Resource + private LogCommentMapper logCommentMapper; + @Resource + private LogMemberAssociationMapper logMemberAssociationMapper; + @Resource + private LogMemberFieldMapper logMemberFieldMapper; + @Resource + private LogMemberLogMapper logMemberLogMapper; + @Resource + private LogTemplateMapper logTemplateMapper; + @Resource + private LogTemplateFieldMapper logTemplateFieldMapper; + @Resource + private UserProvider userProvider; + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private ImRobotApi imRobotApi; + + + @Override + public List getTemplateList() { + List logTemplateVos = logTemplateMapper.getTemplateList(); + if (null != logTemplateVos && logTemplateVos.size() > 0) { + //赋值下面字段 + logTemplateVos.forEach(v -> { + List fieldVos = logTemplateFieldMapper.getFieldById(v.getId()); + if (null != fieldVos) { + v.setLogTemplateFieldVoList(fieldVos); + } + }); + } + return logTemplateVos; + } + + @Override + public void addLog(LogMemberLogDto logMemberLogDto) { + //新增日志主表 + String id = FtbUtil.getId(); + logMemberLogDto.setId(id); + String userId = userProvider.get().getUserId(); + setLogParam(logMemberLogDto, userId); + logMemberLogMapper.addLog(logMemberLogDto, userId); + if (null != logMemberLogDto.getLogMemberFieldList() && logMemberLogDto.getLogMemberFieldList().size() > 0) { + logMemberLogDto.getLogMemberFieldList().forEach(v -> v.setId(FtbUtil.getId())); + //新增字段信息 + logMemberFieldMapper.addField(logMemberLogDto.getLogMemberFieldList(), logMemberLogDto.getId()); + } + //新增成员信息 + if (null != logMemberLogDto.getLogMemberAssociationList() && logMemberLogDto.getLogMemberAssociationList().size() > 0) { + logMemberLogDto.getLogMemberAssociationList().forEach(v -> v.setId(FtbUtil.getId())); + //新增字段信息 + logMemberAssociationMapper.addMemberAssociation(logMemberLogDto.getLogMemberAssociationList(), logMemberLogDto.getId()); + } + + //消息推送 + List logMemberAssociationList = logMemberLogDto.getLogMemberAssociationList(); + if (CollectionUtil.isEmpty(logMemberAssociationList)) { + return; + } + + List logMemberFieldList = logMemberLogDto.getLogMemberFieldList(); + List titleDtoList = logMemberFieldList.stream().filter(log -> { + return log.getCode().equals("TITLE") || log.getCode().equals("title"); + }).collect(Collectors.toList()); + + String title = ""; + //日志/日报标题 + if (CollectionUtil.isNotEmpty(titleDtoList)) { + //日志标题 + title = titleDtoList.get(0).getContent(); + } else { + //日报标题需要拼接 + String userName = UserProvider.getUser().getUserName(); + title = userName + "的"+logMemberLogDto.getLogType(); + } + + + //获取日报/日志内容 + String logContent = getLogContentTemplate(logMemberFieldList); + + UserInfo loginUser = UserProvider.getUser(); +// List toUserIds = logMemberAssociationList.stream().map(LogMemberAssociationDto::getUserId).collect(Collectors.toList()); + + List toUserIds = logMemberAssociationList.stream().filter(logToUser -> { + //推送到用户id + return logToUser.getUserType().equals(1); + }).map(logToUser -> { + String toUserId = logToUser.getUserId(); + if (toUserId.contains(":")) { + return toUserId.split(":")[1]; + } + return toUserId; + }).collect(Collectors.toList()); + + List groupIds = logMemberAssociationList.stream().filter(logToUser -> { + return logToUser.getUserType().equals(2); + }).map(LogMemberAssociationDto::getUserId).collect(Collectors.toList()); + //推送到个人 + if (CollectionUtil.isNotEmpty(toUserIds)) { + msgSendToSomePeople(title, logContent, toUserIds, id, loginUser.getTenantId(), logMemberLogDto.getLogType()); + } + + if (CollectionUtil.isNotEmpty(groupIds)) { + for (String groupId : groupIds) { + if (StrUtil.isBlank(groupId)) { + continue; + } + //推送到群 + msgSendToGroup(title, logContent, groupId, loginUser.getTenantId(), id, logMemberLogDto.getLogType()); + } + } + } + + private void setLogParam(LogMemberLogDto logMemberLogDto, String userId) { + //赋值最后一个部门/岗位 + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (null != userPrimaryBoundOne) { + logMemberLogDto.setOrganizeId(userPrimaryBoundOne.getOrganizeId()); + logMemberLogDto.setOrganizeName(userPrimaryBoundOne.getOrganizeName()); + logMemberLogDto.setPositionId(userPrimaryBoundOne.getPositionId()); + logMemberLogDto.setPositionName(userPrimaryBoundOne.getPositionName()); + } + } + + @Override + public LogMemberLogVo getLogDetail(String logId) { + String userId = userProvider.get().getUserId(); + //查询日志主表信息并统计点赞/已读数 + LogMemberLogVo logDetail = logMemberLogMapper.getLogDetail(logId); + if (null != logDetail) { + //查看当前用户的点赞状态 + String meLike = logMemberAssociationMapper.getMeLike(logId, userId); + if (null != meLike) { + logDetail.setLikeType(1); + } else { + logDetail.setLikeType(0); + } + //赋值创建用户信息 + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(logDetail.getCreatorUserId(), null); + + if (null != userPrimaryBoundOne) { + logDetail.setCreatorUserName(userPrimaryBoundOne.getUserName()); + logDetail.setCreatorUserHeadIcon(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + } + //查询日志字段信息 + List logMemberFieldVos = logMemberFieldMapper.getFieldById(logId); + logDetail.setLogMemberFieldVos(logMemberFieldVos); + //判断当前登录人是否是日志的抄送人或汇报人 + List ids = logMemberAssociationMapper.getMeUnRead(userId, logId); + if (null != ids && ids.size() > 0) { + //变为已读 + logMemberAssociationMapper.readByIds(ids); + } + } + return logDetail; + } + + @Override + public ReceiveRangeVo getReceiveRange(String logId) { + ReceiveRangeVo receiveRangeVo = new ReceiveRangeVo(); + List logMemberAssociations = logMemberAssociationMapper.getReceiveRange(logId); + if (null != logMemberAssociations && logMemberAssociations.size() > 0) { + Map> collect = logMemberAssociations.stream().collect(Collectors.groupingBy(LogMemberAssociation::getType)); + //找到汇报 + List reportList = collect.get(1); + if (null != reportList) { + setUserParam(reportList); + //排序 + receiveRangeVo.setReportList(reportList.stream().sorted(Comparator.comparing(LogMemberAssociation::getUserType)).collect(Collectors.toList())); + } + //找到抄送 + List chaosongList = collect.get(2); + if (null != chaosongList) { + setUserParam(chaosongList); + //排序 + receiveRangeVo.setChaosongList(chaosongList.stream().sorted(Comparator.comparing(LogMemberAssociation::getUserType)).collect(Collectors.toList())); + } + //找到转发 + List forwardList = collect.get(3); + if (null != forwardList) { + List list = new ArrayList<>(); + List idList = new ArrayList<>(); + //去重 + for (LogMemberAssociation logMemberAssociation : forwardList) { + if (!idList.contains(logMemberAssociation.getUserId())) { + list.add(logMemberAssociation); + idList.add(logMemberAssociation.getUserId()); + } + } + setUserParam(list); + //排序 + receiveRangeVo.setForwardList(list.stream().sorted(Comparator.comparing(LogMemberAssociation::getUserType)).collect(Collectors.toList())); + } + } + return receiveRangeVo; + } + + @Override + public List getReadList(String logId) { + List readDependence = logMemberAssociationMapper.getReadDependence(logId, 1); + setUserParam(readDependence); + return readDependence; + } + + @Override + public List getUnReadList(String logId) { + List readDependence = logMemberAssociationMapper.getReadDependence(logId, 0); + setUserParam(readDependence); + return readDependence; + } + + @Override + public List getLikeList(String logId) { + List readDependence = logMemberAssociationMapper.getLikeList(logId); + setUserParam(readDependence); + return readDependence; + } + + @Override + public void updateLog(LogMemberLogDto logMemberLogDto) { + //修改日志主表 + String userId = userProvider.get().getUserId(); + setLogParam(logMemberLogDto, userId); + logMemberLogMapper.updateLog(logMemberLogDto, userId); + if (null != logMemberLogDto.getLogMemberFieldList() && logMemberLogDto.getLogMemberFieldList().size() > 0) { + //先删除原来的字段 + logMemberFieldMapper.deleteByLogId(logMemberLogDto.getId()); + logMemberLogDto.getLogMemberFieldList().forEach(v -> v.setId(FtbUtil.getId())); + //新增字段信息 + logMemberFieldMapper.addField(logMemberLogDto.getLogMemberFieldList(), logMemberLogDto.getId()); + } + if (1 == logMemberLogDto.getUpdateType()) { + //修改并通知 清除阅读状态 + logMemberAssociationMapper.updateUnRead(logMemberLogDto.getId()); + + //日志/日报标题 + String title = ""; + + //消息推送 + List titleDtoList = logMemberLogDto.getLogMemberFieldList().stream().filter(log -> { + return log.getCode().equals("TITLE"); + }).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(titleDtoList)) { + title = titleDtoList.get(0).getContent(); + } else { + String userName = UserProvider.getUser().getUserName(); + title = userName + "的日报"; + } + + if (CollectionUtil.isEmpty(logMemberLogDto.getLogMemberAssociationList())) { + return; + } + + if (CollectionUtil.isEmpty(logMemberLogDto.getLogMemberFieldList())) { + return; + } + + List logMemberFieldList = logMemberLogDto.getLogMemberFieldList(); + String logMsg = title; + + String content = getLogContentTemplate(logMemberFieldList); + UserInfo loginUser = UserProvider.getUser(); +// List toUserIds = logMemberLogDto.getLogMemberAssociationList().stream().map(LogMemberAssociationDto::getUserId).collect(Collectors.toList()); + List logMemberAssociationList = logMemberLogDto.getLogMemberAssociationList(); + List toUserIds = logMemberAssociationList.stream().filter(logToUser -> { + //推送到用户id + return logToUser.getUserType().equals(1); + }).map(logToUser -> { + String toUserId = logToUser.getUserId(); + if (toUserId.contains(":")) { + return toUserId.split(":")[1]; + } + return toUserId; + }).collect(Collectors.toList()); + + List groupIds = logMemberAssociationList.stream().filter(logToUser -> { + return logToUser.getUserType().equals(2); + }).map(LogMemberAssociationDto::getUserId).collect(Collectors.toList()); + + if (CollectionUtil.isNotEmpty(toUserIds)) { + //推送到个人 + msgSendToSomePeople(logMsg, content, toUserIds, logMemberLogDto.getId(), loginUser.getTenantId(), logMemberLogDto.getLogType()); + } + if (CollectionUtil.isNotEmpty(groupIds)) { + //推送到群 + for (String groupId : groupIds) { + if (StrUtil.isBlank(groupId)) { + continue; + } + msgSendToGroup(logMsg, content, groupId, loginUser.getTenantId(), logMemberLogDto.getId(), logMemberLogDto.getLogType()); + } + } + + } + } + + @Override + public List getTemplateNameList() { + return logTemplateMapper.getTemplateList(); + } + + @Override + public Integer deleteLog(String logId) { + String userId = userProvider.get().getUserId(); + LogMemberLogVo logDetail = logMemberLogMapper.getLogDetail(logId); + if (null == logDetail) { + return -1; + } + if (!userId.equals(logDetail.getCreatorUserId())) { + return -2; + } + logMemberLogMapper.deleteLog(logId, userId); + return 1; + } + + /** + * 给工作日志相关人员发im消息 + * + * @param dto 请求对象 + */ + @Override + public void agentAlert(LogAgentAlertDto dto) { + if (CollectionUtil.isEmpty(dto.getUserIds())) { + throw new RuntimeException("提醒人员不能为空"); + } + if (StrUtil.isEmpty(dto.getLogId())) { + throw new RuntimeException("日志id不能为空"); + } + String logId = dto.getLogId(); + +// //未读列表 +// List readDependence = logMemberAssociationMapper.getReadDependence(logId,0); +// + LogMemberLogVo logMemberLogDto = logMemberLogMapper.selectById(logId); + //查询字段 + List field = logMemberFieldMapper.getFieldById(logId); + if (CollectionUtil.isEmpty(field)) { + return; + } + + String title = ""; + //日志/日报标题 + //获取日报/日志内容 + StringBuilder logContentBuilder = new StringBuilder(); + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(logMemberLogDto.getCreatorUserId(), null); + if (userPrimaryBoundOne != null) { + title = userPrimaryBoundOne.getName() + "的" + logMemberLogDto.getLogType(); + } else { + title = logMemberLogDto.getLogType(); + } + + if (logMemberLogDto.getLogType().equals("日志")) { + for (LogMemberFieldVo logMemberFieldVo : field) { + if (logMemberFieldVo.getCode().equals("TITLE")) { + title = logMemberFieldVo.getContent(); + } else if (logMemberFieldVo.getCode().equals("CONTENT")) { + logContentBuilder.append(logMemberFieldVo.getContent()); + } + } + } else { + for (LogMemberFieldVo logMemberFieldVo : field) { + if (StrUtil.isNotEmpty(logMemberFieldVo.getContent())) { + logContentBuilder.append(logMemberFieldVo.getField()) + .append(":
") + .append(logMemberFieldVo.getContent()) + .append("
"); + } + } + } + + String logContent = logContentBuilder.toString(); + if (StrUtil.isEmpty(title) || StrUtil.isEmpty(logContent)) { + return; + } + + // 去掉 logContent 最后一个br + if (logContent.endsWith("
")) { + logContent = logContent.substring(0, logContent.lastIndexOf("
")); + } + + UserInfo loginUser = UserProvider.getUser(); + //推送到个人 + msgSendToSomePeople(title, logContent, dto.getUserIds(), logMemberLogDto.getId(), loginUser.getTenantId(), logMemberLogDto.getLogType()); + } + + + private void setUserParam(List reportList) { + if (null != reportList && reportList.size() > 0) { + for (LogMemberAssociation logMemberAssociation : reportList) { + String userId = logMemberAssociation.getUserId(); + if (StrUtil.isNotBlank(userId) && userId.contains(":")) { + // 查询数据库发现有类似于yawen:1234之类的用户id,这些数据作为用户id是查询不到数据的 + String uid = userId.split(":")[1]; + logMemberAssociation.setUserId(uid); + } + } + + Map> userTypeMap = reportList.stream().collect(Collectors.groupingBy(LogMemberAssociation::getUserType)); + //人 赋值人员信息 + List logMemberMap = userTypeMap.get(1); + if (null != logMemberMap && logMemberMap.size() > 0) { + List userIds = logMemberMap.stream().distinct().map(LogMemberAssociation::getUserId).collect(Collectors.toList()); + // 查询成员信息 + Map map = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + logMemberMap.forEach(v -> { + UserBoundVO userInfoVo = map.get(v.getUserId()); + if (null != userInfoVo) { + v.setHeadIcon(UploaderUtil.uploaderImg(userInfoVo.getHeadIcon())); + v.setUserName(userInfoVo.getUserName()); + v.setOrganizeId(userInfoVo.getOrganizeId()); + v.setOrganizeName(userInfoVo.getOrganizeName()); + v.setPositionId(userInfoVo.getPositionId()); + v.setPositionName(userInfoVo.getPositionName()); + } + }); + } + } + } + + /** + * 消息推送到一些人 + * + * @param content 内容 + * @param toUserIds 推送到用户id + * @param logId 日志id + * @param tenantId 租户id + */ + private void msgSendToSomePeople(String title, String content, List toUserIds, String logId, String tenantId, String logType) { + //通知 + SingleSendRobotNoticeForm sendRobotNoticeForm = new SingleSendRobotNoticeForm(); +// List toUserIds = logMemberLogDto.getLogMemberAssociationList().stream().map(LogMemberAssociationDto::getUserId).collect(Collectors.toList()); + sendRobotNoticeForm.setToUserIds(toUserIds); + sendRobotNoticeForm.setTenantId(tenantId); +// ImRobotTypeEnum imRobotTypeEnum = ImRobotTypeEnum.getByUserId(loginUser.getUserId()); +// sendRobotNoticeForm.setRobotTypeEnum(imRobotTypeEnum); + sendRobotNoticeForm.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + + //消息体 + SendRobotNoticeDataForm msgBody = new SendRobotNoticeDataForm(); + + + msgBody.setTitle(title); + msgBody.setContent(content); + msgBody.setScene(MemberLogImEnum.UPDATE_LOG.getScene()); + msgBody.setContentSummary(content); + msgBody.setAppName(MemberLogImEnum.UPDATE_LOG.getTitle()); + msgBody.setLogo(MemberLogImEnum.UPDATE_LOG.getLogoUrl()); + + + //按钮 + LinkedList jumpList = new LinkedList<>(); + JumpUrlListModel jump = new JumpUrlListModel(); + jump.setButtonName(MemberLogImJumpEnum.MEMBER_LOG.getButtonName()); + jump.setType(1); + jump.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jump.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jump.setMpId(MemberLogImJumpEnum.MEMBER_LOG.getMpId()); + String jumpUrl = String.format(MemberLogImJumpEnum.MEMBER_LOG.getUrl(), logId, UserProvider.getLoginUserId(), logType); + jump.setUrl(jumpUrl); + jumpList.add(jump); + msgBody.setJumpUrlList(jumpList); + + sendRobotNoticeForm.setRobotNoticeDataForm(msgBody); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(sendRobotNoticeForm); + if (!ActionResult.success().getCode().equals(actionResult.getCode())) { + log.error(actionResult.getMsg()); + } + } + + /** + * 消息推送到群 + * + * @param content 内容 + * @param groupId 群id + * @param tenantId 租户id + */ + private void msgSendToGroup(String title, String content, String groupId, String tenantId, String logId, String logType) { + AddOrDeleteRobotInGroupForm addForm = new AddOrDeleteRobotInGroupForm(); + addForm.setGroupId(groupId); + addForm.setOptTypeEnum(OptTypeEnum.ADD); + addForm.setIsNotice(ConstantStatusEnum.NUM_FALSE); + addForm.setTenantId(tenantId); + addForm.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + addForm.setSource("日志"); + ActionResult addImResult = imRobotApi.addOrDeleteRobotInGroup(addForm); + if (!ActionResult.success().getCode().equals(addImResult.getCode())) { + log.error("添加im机器人异常:{}", addImResult.getMsg()); + } + //通知 + GroupSendRobotNoticeForm sendRobotNoticeForm = new GroupSendRobotNoticeForm(); + sendRobotNoticeForm.setGroupId(groupId); + sendRobotNoticeForm.setTenantId(tenantId); + + sendRobotNoticeForm.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + + //消息体 + SendRobotNoticeDataForm msgBody = new SendRobotNoticeDataForm(); + + + msgBody.setTitle(title); + msgBody.setContent(content); + msgBody.setScene(MemberLogImEnum.UPDATE_LOG.getScene()); + msgBody.setContentSummary(content); + msgBody.setAppName(MemberLogImEnum.UPDATE_LOG.getTitle()); + msgBody.setLogo(MemberLogImEnum.UPDATE_LOG.getLogoUrl()); + + //按钮 + LinkedList jumpList = new LinkedList<>(); + JumpUrlListModel jump = new JumpUrlListModel(); + jump.setButtonName(MemberLogImJumpEnum.MEMBER_LOG.getButtonName()); + jump.setType(1); + jump.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jump.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jump.setMpId(MemberLogImJumpEnum.MEMBER_LOG.getMpId()); + String jumpUrl = String.format(MemberLogImJumpEnum.MEMBER_LOG.getUrl(), logId, UserProvider.getLoginUserId(), logType); + jump.setUrl(jumpUrl); + jumpList.add(jump); + msgBody.setJumpUrlList(jumpList); + + sendRobotNoticeForm.setRobotNoticeDataForm(msgBody); + + ActionResult actionResult = imRobotApi.sendGroupRobotNotice(sendRobotNoticeForm); + if (!ActionResult.success().getCode().equals(actionResult.getCode())) { + log.error(actionResult.getMsg()); + } + } + + private String getLogContentTemplate(List logMemberFieldList) { + String content = ""; + for (int i = 0; i < logMemberFieldList.size(); i++) { + LogMemberFieldDto logMemberFieldDto = logMemberFieldList.get(i); + if (logMemberFieldDto.getCode().equals("TITLE")) { + continue; + } + String contentFormat = MemberLogContentEnum.getContentFormat(logMemberFieldDto.getCode()); + if (StrUtil.isBlank(contentFormat)) { + continue; + } + if(StrUtil.isEmpty(logMemberFieldDto.getContent())){ + continue; + } + if (i == (logMemberFieldList.size() - 1)) { + content += String.format(contentFormat, logMemberFieldDto.getContent()); + } else { + content += String.format(contentFormat, logMemberFieldDto.getContent()) + "
"; + } + + } + return content; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/WorkLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/WorkLogServiceImpl.java new file mode 100644 index 0000000..4d91e7a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/memberLog/service/impl/WorkLogServiceImpl.java @@ -0,0 +1,722 @@ +package jnpf.memberLog.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.StrUtil; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.enums.memberLog.MemberLogImEnum; +import jnpf.enums.memberLog.MemberLogImJumpEnum; +import jnpf.exception.QueryException; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import jnpf.memberLog.mapper.WorkLogMapper; +import jnpf.memberLog.service.WorkLogService; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.memberLog.dto.*; +import jnpf.model.memberLog.po.LogComment; +import jnpf.model.memberLog.vo.*; +import jnpf.permission.UserApi; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.user.MiniUserVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.stereotype.Service; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 工作日志服务实现 + * + * @author yanwenfu + * @create 2023-08-08 + */ +@Slf4j +@Service +public class WorkLogServiceImpl implements WorkLogService { + private static final List EMPTY_LIST = List.of("-1"); + + + @Resource + private WorkLogMapper workLogMapper; + + @Resource + private UserProvider userProvider; + + @Resource + private DataSourceTransactionManager transactionManager; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private UserApi userApi; + @Autowired + private ImRobotApi imRobotApi; + + @Override + public PageInfo getCommentList(String logId, CommentQueryDto queryDto) { + + queryDto.setLogId(logId); + MiniMemberLogVo memberLog = workLogMapper.getLog(logId); + // 分页查询数据 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(workLogMapper.getCommentList(queryDto)); + if (page.getSize() > 0) { + Set collect1 = page.getList().stream().map(LogCommentVo::getCreatorUserId).collect(Collectors.toSet()); + Set collect2 = page.getList().stream().map(LogCommentVo::getReplyUserId).collect(Collectors.toSet()); + collect1.addAll(collect2); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(new ArrayList<>(collect1), null); + + page.getList().forEach(v -> { + setReturnCommentValue(v, memberLog, userMap); + }); + } + return page; + } + + @Override + public LogCommentVo addCommentReply(String logId, AddReplyDto addReplyDto) { + + String userId = userProvider.get().getUserId(); + // 查询父级信息 + LogComment comment = workLogMapper.getLogComment(addReplyDto.getPid()); + LogComment logComment = new LogComment(); + logComment.setId(FtbUtil.getId()); + logComment.setMemberLogId(logId); + logComment.setContent(addReplyDto.getContent()); + logComment.setPic(addReplyDto.getPic()); + logComment.setFile(addReplyDto.getFile()); + logComment.setPid(addReplyDto.getPid()); + String path; + String rootId = "0"; + if (null != comment) { + path = comment.getPath() + logComment.getId() + ","; + if (comment.getRootId().equals(rootId)) { + rootId = comment.getId(); + } else { + rootId = comment.getRootId(); + } + } else { + path = addReplyDto.getPid() + "," + logComment.getId() + ","; + } + logComment.setPath(path); + logComment.setRootId(rootId); + logComment.setCreatorUserId(userId); + logComment.setLastModifyUserId(userId); + // 获取事务定义 + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + // 设置事务隔离级别,开启新事务 + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + // 获得事务状态,相当于开启事物 + TransactionStatus transactionStatus = transactionManager.getTransaction(def); + workLogMapper.addCommentReply(logComment); + transactionManager.commit(transactionStatus); + // 查询LogCommentVo + LogCommentVo returnComment = workLogMapper.getLogCommentVo(logComment.getId()); + MiniMemberLogVo memberLog = workLogMapper.getLog(logId); + Set userIdSet = Stream.of(returnComment.getCreatorUserId(), returnComment.getReplyUserId()).collect(Collectors.toSet()); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(new ArrayList<>(userIdSet), null); + setReturnCommentValue(returnComment, memberLog, userMap); + + //消息推送 + String replyUserName = returnComment.getReplyUserName(); + String replyUserHeadIcon = returnComment.getReplyUserHeadIcon(); + List userEntityList = userApiV2Util.getUserNameForUserIdsReturnList(List.of(userId)); + String userName = ""; + if(CollectionUtil.isNotEmpty(userEntityList)){ + userName = userEntityList.get(0).getRealName(); + } + String title = userName+"评论了你的" + memberLog.getLogType(); + List toUserIds = ListUtil.of(memberLog.getCreatorUserId()); + UserInfo loginUser = UserProvider.getUser(); + + msgSendToSomePeople(title, addReplyDto.getContent(), toUserIds, logId, loginUser.getTenantId(), memberLog.getLogType()); + return returnComment; + } + + /** + * 消息推送到一些人 + * + * @param content 内容 + * @param toUserIds 推送到用户id + * @param logId 日志id + * @param tenantId 租户id + */ + private void msgSendToSomePeople(String title, String content, List toUserIds, String logId, String tenantId, String logType) { + //通知 + + SingleSendRobotNoticeForm sendRobotNoticeForm = new SingleSendRobotNoticeForm(); + sendRobotNoticeForm.setToUserIds(toUserIds); + sendRobotNoticeForm.setTenantId(tenantId); + sendRobotNoticeForm.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + + //消息体 + SendRobotNoticeDataForm msgBody = new SendRobotNoticeDataForm(); + + + msgBody.setTitle(title); + msgBody.setContent(content); + msgBody.setScene(MemberLogImEnum.UPDATE_LOG.getScene()); + msgBody.setContentSummary(content); + msgBody.setAppName(MemberLogImEnum.UPDATE_LOG.getTitle()); + msgBody.setLogo(MemberLogImEnum.UPDATE_LOG.getLogoUrl()); + + //按钮 + LinkedList jumpList = new LinkedList<>(); + JumpUrlListModel jump = new JumpUrlListModel(); + jump.setButtonName(MemberLogImJumpEnum.MEMBER_LOG.getButtonName()); + jump.setType(1); + jump.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jump.setMpId(MemberLogImJumpEnum.MEMBER_LOG.getMpId()); + String jumpUrl = String.format(MemberLogImJumpEnum.MEMBER_LOG.getUrl(), logId, UserProvider.getLoginUserId(), logType); + jump.setUrl(jumpUrl); + jumpList.add(jump); + msgBody.setJumpUrlList(jumpList); + + sendRobotNoticeForm.setRobotNoticeDataForm(msgBody); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(sendRobotNoticeForm); + if (!ActionResult.success().getCode().equals(actionResult.getCode())) { + log.error(actionResult.getMsg()); + } + } + + private void setReturnCommentValue(LogCommentVo v, MiniMemberLogVo memberLog, Map userMap) { + // 查询日志创建人, 判断评论是否作者 + if (v.getCreatorUserId().equals(memberLog.getCreatorUserId())) { + v.setCheckAuthor(ConstantUtil.NUM_TRUE); + } + // 处理回复人信息 + if (StringUtils.isNotEmpty(v.getReplyUserId())) { + UserBoundVO user = userMap.get(v.getReplyUserId()); + if (null != user) { + v.setReplyUserName(user.getUserName()); + v.setReplyUserHeadIcon(user.getHeadIcon()); + } + } + // 查询用户基础信息 + UserBoundVO user = userMap.get(v.getCreatorUserId()); + if (user != null) { + v.setCreatorUserName(user.getUserName()); + v.setOrganizeId(user.getOrganizeId()); + v.setOrganizeName(user.getOrganizeName()); + v.setPositionId(user.getPositionId()); + v.setPositionName(user.getPositionName()); + v.setHeadIcon(UploaderUtil.uploaderImg(user.getHeadIcon())); + } + } + + @Override + public void addOrCancelLike(String logId) { + + String userId = userProvider.get().getUserId(); + // 查询用户是否有点赞记录 + LogAssociationVo association = workLogMapper.getLogAssociation(logId, userId, ConstantUtil.ASSOCIATION_LIKE); + if (null == association) { + // 无, 新增 + LogAssociationVo addVo = new LogAssociationVo(FtbUtil.getId(), logId, userId, 1, ConstantUtil.ASSOCIATION_LIKE, null); + workLogMapper.addAssociationRecord(addVo); + } else { + // 有, 取消 + workLogMapper.deleteLikeRecord(association.getAssociationId()); + } + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void addForwardRecord(String logId, List list) { + + List addList = new ArrayList<>(); + list.forEach(addForwardDto -> { + LogAssociationVo addVo = new LogAssociationVo(FtbUtil.getId(), logId, addForwardDto.getUserId(), addForwardDto.getUserType(), ConstantUtil.ASSOCIATION_FORWARD, null); + addList.add(addVo); + }); + workLogMapper.addAssociationRecordBatch(addList); + } + + @Override + public LogStatVo getLogDataStat(StatQueryDto queryDto) throws Exception { + + String tenantId = StringUtils.isEmpty(queryDto.getTenantId()) ? userProvider.get().getTenantId() : queryDto.getTenantId(); + LogStatVo logStat = new LogStatVo(); + String[] split = queryDto.getBeginDate().split("-"); + logStat.setStatDate(split[0] + "年" + split[1] + "月"); + // 获取时间段 + DateStrVo dateStr = FtbUtil.getDateStr(queryDto.getBeginDate(), queryDto.getEndDate()); + logStat.getDateList().addAll(dateStr.getShowList()); + // 查询企业所有成员 + List userList = getUserListByCondition(queryDto.getOrganizeId(), queryDto.getPositionId(), queryDto.getUserName(), tenantId); + if (null == userList || userList.isEmpty()) { + return logStat; + } + Map allUserMap = userList.stream().collect(Collectors.toMap(MiniUserVo::getId, v -> v)); + List userIds = userList.stream().map(MiniUserVo::getId).collect(Collectors.toList()); + + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + List managerUserIds = innerPowerUserVO.getUserIds(); + List intersection = UserApiV2Util.getIntersection(managerUserIds, userIds); + if (CollectionUtil.isNotEmpty(intersection)) { + userIds = intersection; + } else { + return logStat; + } + } else if (innerPowerUserVO.getCode().equals(2)) { + return logStat; + } + // 中文排序 + /*userList = userList.stream().filter(FtbUtil.distinctByKey1(MiniUserVo::getId)) + .sorted((o1, o2) -> Collator.getInstance(Locale.TRADITIONAL_CHINESE).compare(o1.getUserName(), o2.getUserName())) + .collect(Collectors.toList());*/ + // 根据条件查询数据源 + List list = workLogMapper.getLogDataStat(queryDto, userIds); + ConcurrentMap> logMap; + if (list.isEmpty()) { + logMap = new ConcurrentHashMap<>(); + } else { + logMap = list.stream().collect(Collectors.groupingByConcurrent(LogDataVo::getCreatorUserId)); + } + // 根据用户, 时间进行统计 + userIds.forEach(userId -> { + MiniUserVo user = allUserMap.get(userId); + // 筛选用户所有日期的数据 + List logList = logMap.get(user.getId()); + String[] array = new String[dateStr.getUseList().size()]; + Arrays.fill(array, ""); + if (null != logList && !logList.isEmpty()) { + Map dateMap = logList.stream().collect(Collectors.toMap(LogDataVo::getCreatorTime, Function.identity())); + for (int i = 0; i < dateStr.getUseList().size(); i++) { + String date = dateStr.getUseList().get(i); + LogDataVo vo = dateMap.get(date); + if (null != vo) { + array[i] = vo.getNum().toString(); + } + } + } + logStat.getUserList().add(user.getUserName()); + logStat.getUserIdList().add(user.getId()); + logStat.getDataList().add(array); + }); + return logStat; + } + + private List getUserListByCondition(String organizeId, String positionId, String userName, String tenantId) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setTenantId(userProvider.get().getTenantId()); + dto.setPageSize(100000L); + dto.setCurrentPage(1L); + dto.setOrganizeId(organizeId); + dto.setPositionId(positionId); + dto.setKeyword(userName); + PageListVO userBoundVOPageListVO = userApiV2Util.pageListUserForWhere(dto); + if (userBoundVOPageListVO != null && CollectionUtil.isNotEmpty(userBoundVOPageListVO.getList())) { + return convertMiniUserVo(userBoundVOPageListVO.getList()); + } + return new ArrayList<>(); + } + + private List convertMiniUserVo(List list) { + List ret = new ArrayList<>(); + for (UserBoundVO user : list) { + MiniUserVo v = new MiniUserVo(); + v.setId(user.getId()); + v.setUserName(user.getUserName()); + v.setOrganizeId(user.getOrganizeId()); + v.setOrganizeName(user.getOrganizeName()); + v.setPositionId(user.getPositionId()); + v.setPositionName(user.getPositionName()); + v.setHeadIcon(UploaderUtil.uploaderImg(user.getHeadIcon())); + ret.add(v); + } + return ret; + } + + @Override + public Workbook getLogDataStatWorkbook(StatQueryDto queryDto) throws Exception { + + LogStatVo logStat = getLogDataStat(queryDto); + // 生成工作簿 + TemplateWorkSheet workSheet = new TemplateWorkSheet(); + workSheet.setSheetName(ConstantUtil.LOG_DATA_FORM); + // 生成表头 + workSheet.setComplexRowNames(getRowNames(logStat, queryDto)); + workSheet.setTitleNum(2); + int len = workSheet.getComplexRowNames()[0].length; + // 合并单元格 + List mergeCells = new ArrayList<>(); + int[] mergeCell = new int[]{0, 0, 0, len - 1}; + mergeCells.add(mergeCell); + workSheet.setMergeCells(mergeCells); + // 生成数据 + List dataList = new ArrayList<>(); + if (!logStat.getUserList().isEmpty()) { + for (int i = 0; i < logStat.getUserList().size(); i++) { + Object[] array = new Object[len]; + String[] dataArray = logStat.getDataList().get(i); + for (int j = 0; j < len; j++) { + if (j == 0) { + array[j] = logStat.getUserList().get(i); + } else { + array[j] = dataArray[j - 1]; + } + } + dataList.add(array); + } + } + workSheet.setDataList(dataList); + return TemplateExcelUtils.createWorkBook(Stream.of(workSheet).collect(Collectors.toList()), true); + } + + @Override + public PageInfo getAppLogPage(LogQueryDto queryDto) { + + PageInfo page; + String userId = userProvider.get().getUserId(); + queryDto.setUserId(userId); + switch (queryDto.getQueryType()) { + case 0: + // 全部 -> 我创建的, 别人抄送/汇报给我的 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + page = new PageInfo<>(workLogMapper.getAllLogList(queryDto)); + break; + case 1: + // 我发出的 -> 我创建的 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + page = new PageInfo<>(workLogMapper.getMyLogList(queryDto)); + break; + case 2: + // 我的部门 -> 我部门的人抄送/汇报给我, 我汇报/抄送给我部门的人 + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (null == userPrimaryBoundOne) { + page = new PageInfo<>(); + break; + } + // 查询我的部门下的所有成员 + List organizeIds = List.of(userPrimaryBoundOne.getOrganizeId()); + List orgUserList = userApiV2Util.getUserListForOrgIds(organizeIds, null); + if (orgUserList.isEmpty()) { + page = new PageInfo<>(); + break; + } + List orgUserIds = orgUserList.stream().map(UserPageListVO::getId).distinct().collect(Collectors.toList()); + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + page = new PageInfo<>(workLogMapper.getMyOrganizeLogList(userId, orgUserIds, queryDto.getBeginDate(), queryDto.getEndDate())); + break; + default: + page = new PageInfo<>(); + break; + } + if (page.getSize() > 0) { + completePageInfo(page.getList()); + } + return page; + } + + private void completePageInfo(List list) { + if (CollectionUtil.isEmpty(list)) { + return; + } + for (MiniLogVo miniLogVo : list) { + String userId = miniLogVo.getUserId(); + if (StrUtil.isNotBlank(userId) && userId.contains(":")) { + miniLogVo.setUserId(userId.split(":")[1]); + } + } + + // 查询每个日志下可以查看的内容 + ConcurrentMap> fieldMap = getLogFieldMap(list); + // 设置用户信息 + List userIds = list.stream().map(MiniLogVo::getUserId).collect(Collectors.toList()); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + + list.forEach(v -> { + UserBoundVO user = userMap.get(v.getUserId()); + if (null != user) { + v.setUserName(user.getUserName()); + v.setHeadIcon(UploaderUtil.uploaderImg(user.getHeadIcon())); + } + // 设置可显示字段 + setFieldList(fieldMap, v); + }); + } + + private void setFieldList(ConcurrentMap> fieldMap, T v) { + List logFieldList = fieldMap.get(v.getLogId()); + if (null != logFieldList && !logFieldList.isEmpty()) { + v.getFieldList().addAll(logFieldList); + } + } + + private ConcurrentMap> getLogFieldMap(List list) { + + List logIds = list.stream().map(LogInfoVo::getLogId).collect(Collectors.toList()); + List fieldList = workLogMapper.getViewableFieldList(logIds); + return fieldList.stream().collect(Collectors.groupingByConcurrent(LogMemberFieldVo::getMemberLogId)); + } + + @Override + public PageInfo getWebLogPage(LogWebQueryDto queryDto) throws QueryException { + String loginUserId = UserProvider.getLoginUserId(); + List managerUserIds = new ArrayList<>(); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + managerUserIds = innerPowerUserVO.getUserIds(); + if (StringUtils.isNotEmpty(queryDto.getUserId())) { + if (managerUserIds.contains(queryDto.getUserId())) { + managerUserIds = List.of(queryDto.getUserId()); + } else { + managerUserIds=EMPTY_LIST; + } + } + } else if (innerPowerUserVO.getCode().equals(2)) { + managerUserIds = EMPTY_LIST; + } + if (!(innerPowerUserVO.getCode().equals(0) + && StringUtils.isEmpty(queryDto.getUserId()) + && StringUtils.isEmpty(queryDto.getOrganizeId()) + && StringUtils.isEmpty(queryDto.getPositionId()) + && StringUtils.isEmpty(queryDto.getUserName()) + ) && userApiV2Util.checkIsContinue(managerUserIds)) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setTenantId(userProvider.get().getTenantId()); + + dto.setPageSize(100000L); + dto.setCurrentPage(1L); + dto.setOrganizeId(queryDto.getOrganizeId()); + dto.setPositionId(queryDto.getPositionId()); + dto.setKeyword(queryDto.getUserName()); + PageListVO userBoundVOPageListVO = userApiV2Util.pageListUserForWhere(dto); + + if (userBoundVOPageListVO != null && CollectionUtil.isNotEmpty(userBoundVOPageListVO.getList())) { + List stringList = userBoundVOPageListVO.getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(managerUserIds)) { + List intersection = UserApiV2Util.getIntersection(stringList, managerUserIds); + if (CollectionUtil.isNotEmpty(intersection)) { + managerUserIds = intersection; + } else { + managerUserIds=EMPTY_LIST; + } + } else { + managerUserIds = stringList; + } + } else { + managerUserIds=EMPTY_LIST; + } + } + + + // 查询日志列表 + PageInfo page; + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + page = new PageInfo<>(workLogMapper.getWebLogListByAuth(queryDto.getQueryType(), queryDto.getBeginDate(), queryDto.getEndDate(), queryDto.getContent(), managerUserIds, loginUserId, "1")); + + + if (page.getSize() > 0 && CollectionUtil.isNotEmpty(page.getList())) { + List recordList = page.getList(); + List userIds = recordList.stream().map(WebLogVo::getCreatorUserId).collect(Collectors.toList()); + Map userMap = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + ConcurrentMap> fieldMap = getLogFieldMap(recordList); + recordList.forEach(v -> { + // 设置用户信息 + UserBoundVO user = userMap.get(v.getCreatorUserId()); + if (null != user) { + v.setCreatorUserName(user.getUserName()); + v.setPositionName(user.getPositionName()); + v.setOrganizeName(user.getOrganizeName()); + } + // 设置可显示字段 + setFieldList(fieldMap, v); + }); + } + return page; + } + + private MiniUserVo changeToMiniUserVo(PartUserInfoVo user) { + + return new MiniUserVo(user); + } + + @Override + public PageInfo getAppLogPageByCondition(ConditionLogQueryDto queryDto) throws QueryException { + + if (StringUtils.isEmpty(queryDto.getContent()) && StringUtils.isEmpty(queryDto.getOrganizeName()) + && StringUtils.isEmpty(queryDto.getPositionName()) && StringUtils.isEmpty(queryDto.getCreatorUserName())) { + throw new QueryException("请输入查询内容"); + } + // 查询符合条件的人 + List userList = userApi.getUserByName(queryDto.getPositionName(), queryDto.getOrganizeName(), queryDto.getCreatorUserName()); + List userIds = userList.stream().map(MiniUserVo::getId).collect(Collectors.toList()); + if (StringUtils.isEmpty(queryDto.getContent()) && userIds.isEmpty()) { + return new PageInfo<>(); + } + // 查询数据 + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(workLogMapper.getLogListByCondition(userProvider.get().getUserId(), queryDto.getContent(), userIds)); + if (page.getSize() > 0) { + completePageInfo(page.getList()); + } + return page; + } + + @Override + public PageInfo getAppLogPageByUser(ConditionLogQueryDto queryDto) { + + PageHelper.startPage(queryDto.getCurrentPage(), queryDto.getPageSize()); + PageInfo page = new PageInfo<>(workLogMapper.getLogListByUser(queryDto.getUserId(), queryDto.getBeginDate(), queryDto.getEndDate())); + if (page.getSize() > 0) { + completePageInfo(page.getList()); + } + return page; + } + + @Override + public void deleteCommentReply(String commentId) { + + // 查询评论 + LogComment logComment = workLogMapper.getLogComment(commentId); + if (logComment.getPid().equals("0")) { + // 一级评论删除, 下级全删 + workLogMapper.deleteLogCommentByPath(commentId, userProvider.get().getUserId()); + } else { + // 其他评论删除, 下级不删 + workLogMapper.deleteLogCommentById(commentId, userProvider.get().getUserId()); + } + } + + @Override + public Integer getCommentTotalNum(String logId) { + + return workLogMapper.getCommentTotalNum(logId); + } + + @Override + public LogStatVo getLogDataStatPage(StatQueryDto queryDto) { + + String tenantId = StringUtils.isEmpty(queryDto.getTenantId()) ? userProvider.get().getTenantId() : queryDto.getTenantId(); + LogStatVo logStat = new LogStatVo(queryDto.getPageSize()); + String[] split = queryDto.getBeginDate().split("-"); + logStat.setStatDate(split[0] + "年" + split[1] + "月"); + // 获取时间段 + DateStrVo dateStr = FtbUtil.getDateStr(queryDto.getBeginDate(), queryDto.getEndDate()); + logStat.getDateList().addAll(dateStr.getShowList()); + // 查询企业所有成员 + List userList = getUserListByCondition(queryDto.getOrganizeId(), queryDto.getPositionId(), queryDto.getUserName(), tenantId); + if (null == userList || userList.isEmpty()) { + return logStat; + } + List userIds = userList.stream().map(MiniUserVo::getId).collect(Collectors.toList()); + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(1)) { + List managerUserIds = innerPowerUserVO.getUserIds(); + List intersection = UserApiV2Util.getIntersection(managerUserIds, userIds); + if (CollectionUtil.isNotEmpty(intersection)) { + userIds = intersection; + } else { + return logStat; + } + } else if (innerPowerUserVO.getCode().equals(2)) { + return logStat; + } + logStat.getPaginationVO().setTotal(userList.size()); + logStat.getPaginationVO().setCurrentPage((long) queryDto.getCurrentPage()); + + userIds = userIds.stream() + .skip(((long) queryDto.getCurrentPage() - 1) * queryDto.getPageSize()) + .limit(queryDto.getPageSize()) + .collect(Collectors.toList()); + // 根据条件查询数据源 + List list = workLogMapper.getLogDataStat(queryDto, userIds); + ConcurrentMap> logMap; + if (list.isEmpty()) { + logMap = new ConcurrentHashMap<>(); + } else { + logMap = list.stream().collect(Collectors.groupingByConcurrent(LogDataVo::getCreatorUserId)); + } + Map userMap = userList.stream().collect(Collectors.toMap(MiniUserVo::getId, Function.identity())); + // 根据用户, 时间进行统计 + userIds.forEach(userId -> { + MiniUserVo user = userMap.get(userId); + // 筛选用户所有日期的数据 + List logList = logMap.get(userId); + String[] array = new String[dateStr.getUseList().size()]; + Arrays.fill(array, ""); + if (null != logList && !logList.isEmpty()) { + Map dateMap = logList.stream().collect(Collectors.toMap(LogDataVo::getCreatorTime, Function.identity())); + for (int i = 0; i < dateStr.getUseList().size(); i++) { + String date = dateStr.getUseList().get(i); + LogDataVo vo = dateMap.get(date); + if (null != vo) { + array[i] = vo.getNum().toString(); + } + } + } + logStat.getUserList().add(user.getUserName()); + logStat.getUserIdList().add(user.getId()); + logStat.getDataList().add(array); + }); + return logStat; + } + + private String[][] getRowNames(LogStatVo logStat, StatQueryDto queryDto) { + OrganizeGeneralDetailVO organizeGeneralDetailVO = userApiV2Util.organizeInfoById(queryDto.getOrganizeId(),null); + String organizeName = "全部"; + if(organizeGeneralDetailVO!=null){ + organizeName = organizeGeneralDetailVO.getOrganizeTreeName(); + } + if(StringUtils.isNotEmpty(queryDto.getOrganizeName())){ + organizeName = queryDto.getOrganizeName(); + } + String[][] rowNames = new String[2][logStat.getDateList().size() + 1]; + // 第一行数据 + String title = ""; + if (StringUtils.isNotEmpty(queryDto.getUserName())) { + title += "姓名:" + queryDto.getUserName() + " "; + } + title += "部门:" + organizeName + " 岗位:" + queryDto.getPositionName() + " 类型:" + queryDto.getQueryType(); + rowNames[0][0] = title; + for (int i = 0; i < rowNames.length; i++) { + for (int j = 0; j < rowNames[i].length; j++) { + if (i == 0) { + if (j == 0) { + rowNames[i][j] = title; + } else { + rowNames[i][j] = ""; + } + } else { + if (j == 0) { + rowNames[i][j] = logStat.getStatDate(); + } else { + rowNames[i][j] = logStat.getDateList().get(j - 1); + } + } + } + } + return rowNames; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppMyNoticeAnnouncementsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppMyNoticeAnnouncementsController.java new file mode 100644 index 0000000..67c02b3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppMyNoticeAnnouncementsController.java @@ -0,0 +1,143 @@ +package jnpf.notice.controller.app; + + +import cn.hutool.core.collection.CollectionUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsDetailDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsMsgDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.AppQueryMyAnnouncementListReq; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import jnpf.model.personnels.req.employment.BatchByPrimaryIdReq; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * app 我的公告模块 + * + * @author xgl + * @since 2024-05-08 09:51:26 + */ +@RestController +@RequestMapping("/app/myNoticeAnnouncements") +public class AppMyNoticeAnnouncementsController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeAnnouncementsService ftbNoticeAnnouncementsService; + + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + /** + * App查询最新的N条公告 + * + * @param num 最新公告数据量 + * @return {@link ActionResult}<{@link MyNoticeAnnouncementsDto}> + */ + @GetMapping("/queryLatest/{num}") + public ActionResult> queryLatest(@PathVariable("num") Integer num) { + return ActionResult.success("查询成功", ftbNoticeAnnouncementsService.queryMyLatest(UserProvider.getLoginUserId(), num)); + } + + /** + * 查询我的公告列表 + * + * @param req + * @return {@link ActionResult}<{@link MyNoticeAnnouncementsDto}> + */ + @GetMapping("/queryList") + public ActionResult> pageLists(@Validated AppQueryMyAnnouncementListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsService.getAppMyPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 查询公告详情 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @GetMapping("/get/{announcementId}") + public ActionResult get(@PathVariable("announcementId") String announcementId) { + return ActionResult.success("成功", ftbNoticeAnnouncementsService.getMyAnnouncementInfo(announcementId, UserProvider.getLoginUserId())); + } + + /** + * 查询公告中已读/未读人员列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsReceiveDto}> + */ + @GetMapping("/queryUser") + public ActionResult> queryUserList(@Validated QueryUserListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsReceiveService.queryUserListForReadStatus(req, false); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 轮训我的未读公告 + * + * @param num 公告数量,0查询所有 1:查询1条 2 查询2条 + * @return {@link ActionResult} + */ + @GetMapping("/queryAllNoRead/{num}") + public ActionResult> queryAllNoRead(@PathVariable("num") Integer num) { + //0:全部 1:非必读,2:必读 + return ActionResult.success("成功", ftbNoticeAnnouncementsService.queryAllMyNoRead(UserProvider.getLoginUserId(), num, NoticeEnums.NeedReadStatus.REQUIRED.getCode())); + } + + /** + * 轮训我的未读公告 + * + * @param num 公告数量,0查询所有 1:查询1条 2 查询2条 + * @param status 0-全部 1-非必读 2-必读 + * @return {@link ActionResult} + */ + @GetMapping("/queryAllNoRead/{num}/{status}") + public ActionResult> queryAllNoReadAndStatus(@PathVariable("num") Integer num, @PathVariable("status") Integer status) { + //0:全部 1:非必读,2:必读 + return ActionResult.success("成功", ftbNoticeAnnouncementsService.queryAllMyNoRead(UserProvider.getLoginUserId(), num, status)); + } + + /** + * 公告完成阅读接口 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @GetMapping("/completeRead/{announcementId}") + public ActionResult completeRead(@PathVariable("announcementId") String announcementId) { + ftbNoticeAnnouncementsService.completeRead(announcementId, UserProvider.getLoginUserId()); + return ActionResult.success("成功", true); + } + + /** + * 检测公告是否已读,返回未读列表 + * + * @param req + * @return + */ + @PostMapping("/queryReadStatusForAnnouncementIds") + public ActionResult> queryReadStatusForAnnouncementIds(@RequestBody @Validated BatchByPrimaryIdReq req) { + if (CollectionUtil.isEmpty(req.getIds())) { + throw new IllegalArgumentException("参数错误"); + } + return ActionResult.success("成功", ftbNoticeAnnouncementsReceiveService.checkIsRead(UserProvider.getLoginUserId(), req.getIds())); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeAnnouncementsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeAnnouncementsController.java new file mode 100644 index 0000000..1952bf7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeAnnouncementsController.java @@ -0,0 +1,240 @@ +package jnpf.notice.controller.app; + + +import cn.hutool.core.bean.BeanUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.domain.FtbNoticeCategories; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDetailDto; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDto; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.AddNoticeAnnouncementsReq; +import jnpf.model.notice.req.announcement.AppQueryAnnouncementListReq; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import jnpf.model.notice.req.announcement.UpdateNoticeAnnouncementsReq; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.notice.service.FtbNoticeCategoriesService; +import jnpf.notice.utils.NoticeUtils; +import jnpf.util.FtbUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * app公告模块 + * + * @author xgl + * @since 2024-05-08 09:51:26 + */ +@Slf4j +@RestController +@RequestMapping("/app/noticeAnnouncements") +public class AppNoticeAnnouncementsController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeAnnouncementsService ftbNoticeAnnouncementsService; + + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private FtbNoticeCategoriesService ftbNoticeCategoriesService; + + /** + * 查询公告列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsDto}> + */ + @GetMapping("/queryList") + public ActionResult> pageLists(@Validated AppQueryAnnouncementListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsService.getAppPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 添加公告 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody AddNoticeAnnouncementsReq req) { + FtbNoticeAnnouncements entity = ftbNoticeAnnouncementsService.insertData(req); + FtbNoticeAnnouncementsDetailDto dto = BeanUtil.copyProperties(entity, FtbNoticeAnnouncementsDetailDto.class); + FtbNoticeCategories categories = ftbNoticeCategoriesService.getById(req.getCategoryId()); + if(null !=categories){ + dto.setCategoryName(categories.getName()); + } + if (req.getOptionBtn().equals(NoticeEnums.OptionButtonType.DRAFT.getCode())) { + return ActionResult.success("保存成功", dto); + } else { + return ActionResult.success("发布成功", dto); + } + } + + + /** + * 编辑公告 + * + * @param id 公告主键ID,必传 + * @param req + * @return {@link ActionResult} + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Validated @RequestBody UpdateNoticeAnnouncementsReq req) { + String lockKey = String.format(NoticeUtils.NOTICE_UPDATE_KEY, id); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(), NoticeUtils.NOTICE_UPDATE_MAX_TIME, TimeUnit.SECONDS)) { + try { + FtbNoticeAnnouncements entity = ftbNoticeAnnouncementsService.updateData(id, req); + FtbNoticeAnnouncementsDetailDto dto = BeanUtil.copyProperties(entity, FtbNoticeAnnouncementsDetailDto.class); + FtbNoticeCategories categories = ftbNoticeCategoriesService.getById(req.getCategoryId()); + if(null !=categories){ + dto.setCategoryName(categories.getName()); + } + if (req.getOptionBtn().equals(NoticeEnums.OptionButtonType.DRAFT.getCode())) { + return ActionResult.success("保存成功", dto); + } else { + return ActionResult.success("发布成功", dto); + } + } catch (Exception e) { + log.error("编辑公告失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + + /** + * 查询详情 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success("成功", ftbNoticeAnnouncementsService.getInfo(id, "app")); + } + + /** + * 删除公告 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbNoticeAnnouncementsService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + + /** + * 发布接口 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @PostMapping("/publish/{id}") + public ActionResult publish(@PathVariable("id") String id) { + String lockKey = String.format(NoticeUtils.NOTICE_UPDATE_KEY, id); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(), NoticeUtils.NOTICE_UPDATE_MAX_TIME, TimeUnit.SECONDS)) { + try { + ftbNoticeAnnouncementsService.publish(id); + return ActionResult.success("发布成功", true); + } catch (Exception e) { + log.error("公告发布失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + /** + * 撤销接口 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @PostMapping("/cancelPublish/{id}") + public ActionResult cancelPublish(@PathVariable("id") String id) { + ftbNoticeAnnouncementsService.cancelPublish(id); + return ActionResult.success("撤回成功", true); + } + + /** + * 根据公告ID提醒未读人员接口 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @PostMapping("/alertAll/{announcementId}") + public ActionResult alertAll(@PathVariable("announcementId") String announcementId) { + + + String lockKey = String.format(NoticeUtils.NOTICE_UPDATE_KEY, announcementId); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(), NoticeUtils.NOTICE_UPDATE_MAX_TIME, TimeUnit.SECONDS)) { + try { + ftbNoticeAnnouncementsService.alertAll(announcementId); + return ActionResult.success(); + } catch (Exception e) { + log.error("公告发布失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + /** + * 提醒公告中一人未读接口 + * + * @param announcementId 公告ID + * @param userId 用户ID + * @return {@link ActionResult} + */ + @PostMapping("/alertOne/{announcementId}/{userId}") + public ActionResult alertOne(@PathVariable("announcementId") String announcementId, @PathVariable("userId") String userId) { + ftbNoticeAnnouncementsService.alertOne(announcementId, userId); + return ActionResult.success(); + } + + + /** + * 查询公告中已读/未读人员列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsReceiveDto}> + */ + @GetMapping("/queryUser") + public ActionResult> queryUserList(@Validated QueryUserListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsReceiveService.queryUserListForReadStatus(req, false); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeCategoriesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeCategoriesController.java new file mode 100644 index 0000000..daaf472 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeCategoriesController.java @@ -0,0 +1,93 @@ +package jnpf.notice.controller.app; + + +import jnpf.base.ActionResult; +import jnpf.model.notice.domain.FtbNoticeCategories; +import jnpf.model.notice.dto.AppNoticeCategoriesDto; +import jnpf.model.notice.dto.FtbNoticeCategoriesDto; +import jnpf.model.notice.req.category.AddCategoryReq; +import jnpf.model.notice.req.category.QueryCategoryListReq; +import jnpf.model.notice.req.category.QueryNextCategoryListReq; +import jnpf.model.notice.req.category.UpdateCategoryReq; +import jnpf.notice.service.FtbNoticeCategoriesService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * app公告分类模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/app/noticeCategories") +public class AppNoticeCategoriesController { + + @Autowired + private FtbNoticeCategoriesService ftbNoticeCategoriesService; + + + /** + * 根据分类ID查询直接下级分类接口 + * + * @param req + * @return + */ + @GetMapping("/nextCatelists") + public ActionResult> nextCatelists(QueryNextCategoryListReq req) { + return ActionResult.success("成功", ftbNoticeCategoriesService.fillHasChild(ftbNoticeCategoriesService.nextCatelists(req))); + } + + /** + * 查询公告分类接口 + * + * @param req + * @return + */ + @GetMapping("/lists") + public ActionResult> lists(QueryCategoryListReq req) { + return ActionResult.success("成功", ftbNoticeCategoriesService.listCategory(req)); + } + + + /** + * 添加公告分类 + * + * @param req + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid AddCategoryReq req) { + return ActionResult.success("添加成功", ftbNoticeCategoriesService.insertData(req)); + } + + /** + * 修改公告分类 + * + * @param id 分类ID + * @param req + * @return + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid UpdateCategoryReq req) { + return ActionResult.success("编辑成功", ftbNoticeCategoriesService.updateData(id, req)); + } + + /** + * 删除公告分类 + * + * @param id 分类ID + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + ftbNoticeCategoriesService.deleteData(id); + return ActionResult.success(true); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeManagerController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeManagerController.java new file mode 100644 index 0000000..eff0660 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeManagerController.java @@ -0,0 +1,64 @@ +package jnpf.notice.controller.app; + + +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.model.notice.domain.FtbNoticeManager; +import jnpf.model.notice.dto.NoticeManagerAuthorityDto; +import jnpf.notice.service.FtbNoticeManagerService; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * app公告管理员模块 + * + * @author makejava + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/app/noticeManager") +public class AppNoticeManagerController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeManagerService ftbNoticeManagerService; + + /** + * 查询当前用户是否是通知公告管理员 + * + * @return {@link ActionResult} + */ + @GetMapping("/queryMyNoticeManagerAuthority") + public ActionResult queryMyNoticeManagerAuthority() { + UserInfo userInfo = UserProvider.getUser(); + NoticeManagerAuthorityDto noticeManagerAuthorityDto = new NoticeManagerAuthorityDto(); + if (StringUtil.isEmpty(userInfo.getUserId())) { + noticeManagerAuthorityDto.setIsNoticeManager(false); + return ActionResult.success("成功", noticeManagerAuthorityDto); + } + if (userInfo.getIsAdministrator()) { + noticeManagerAuthorityDto.setIsNoticeManager(true); + return ActionResult.success("成功", noticeManagerAuthorityDto); + } + + + //非系统管理员 判断是否添加了通知公告权限 + FtbNoticeManager ftbNoticeManager = ftbNoticeManagerService.queryByUserId(userInfo.getUserId()); + if (null != ftbNoticeManager) { + noticeManagerAuthorityDto.setIsNoticeManager(true); + return ActionResult.success("成功", noticeManagerAuthorityDto); + } + + noticeManagerAuthorityDto.setIsNoticeManager(false); + return ActionResult.success("成功", noticeManagerAuthorityDto); + + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeUserGroupsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeUserGroupsController.java new file mode 100644 index 0000000..ab78000 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/AppNoticeUserGroupsController.java @@ -0,0 +1,138 @@ +package jnpf.notice.controller.app; + + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.common.PageDto; +import jnpf.model.notice.dto.FtbNoticeUserGroupMembersDto; +import jnpf.model.notice.dto.FtbNoticeUserGroupsDto; +import jnpf.model.notice.req.group.AddUserGroupReq; +import jnpf.model.notice.req.group.ListGroupUserMemberReq; +import jnpf.model.notice.req.group.UpdateUserGroupReq; +import jnpf.notice.service.FtbNoticeUserGroupMembersService; +import jnpf.notice.service.FtbNoticeUserGroupsService; +import jnpf.util.FtbUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * app用户分组表模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/app/noticeUserGroup") +public class AppNoticeUserGroupsController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeUserGroupsService ftbNoticeUserGroupsService; + + @Autowired + private FtbNoticeUserGroupMembersService ftbNoticeUserGroupMembersService; + + @Autowired + private UserApiV2Util userApiV2Util; + + + /** + * 分页查询分组列表 + * + * @return {@link ActionResult}<{@link FtbNoticeUserGroupsDto}> + */ + @GetMapping("/queryPage") + public ActionResult> queryAppPage(@Validated PageDto req) { + PageInfo pageVo = ftbNoticeUserGroupsService.queryAppPage(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 分页查询一个分组的 人员列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeUserGroupMembersDto}> + */ + @GetMapping("/queryList/{groupId}") + public ActionResult> pageLists(@PathVariable("groupId") String groupId, @Validated PageDto req) { + ListGroupUserMemberReq selectReq = new ListGroupUserMemberReq(); + selectReq.setCurrentPage(req.getCurrentPage()); + selectReq.setPageSize(req.getPageSize()); + selectReq.setSelectFlowerAndHeadLogo(true); + //查询离职人员 +// List leaveUserIds = userApiV2Util.queryLeaveUserId(); + List powerUserIds = userApiV2Util.getPermissionUserIds(); + PageInfo pageVo = ftbNoticeUserGroupMembersService.getPageList(powerUserIds, groupId, selectReq); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 添加分组 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody AddUserGroupReq req) { + return ActionResult.success("成功", ftbNoticeUserGroupsService.insertData(req)); + } + + + /** + * 编辑分组 + * + * @param id 分组主键ID + * @param req + * @return {@link ActionResult} + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Validated @RequestBody UpdateUserGroupReq req) { + ftbNoticeUserGroupsService.updateData(id, req); + return ActionResult.success(); + } + + + /** + * 查询分组详情及分组的全部人员 + * + * @param id 分组主键ID + * @return {@link ActionResult} + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success("成功", ftbNoticeUserGroupsService.getInfo(id)); + } + + /** + * 批量查询分组的人员列表 + * + * @param groupIds 分组主键ID,逗号分开 + * @return {@link ActionResult} + */ + @GetMapping("/queryBatchGroupUser/{groupIds}") + public ActionResult> queryBatchGroupUser(@PathVariable("groupIds") String groupIds) { + return ActionResult.success("成功", ftbNoticeUserGroupsService.queryBatchGroupUserApp(groupIds)); + } + + /** + * 删除分组 + * + * @param id 分组主键ID,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbNoticeUserGroupsService.deleteData(id); + return ActionResult.success(); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/ImController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/ImController.java new file mode 100644 index 0000000..466d302 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/app/ImController.java @@ -0,0 +1,45 @@ +package jnpf.im.controller; + +import com.tencentyun.TLSSigAPIv2; +import jnpf.base.ActionResult; +import jnpf.util.RedisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/08/16 + */ +@Deprecated +@Slf4j +@RestController +@RequestMapping(value = "/im") +public class ImController { + @Autowired + private RedisUtil redisUtil; + @Value("${config.SDKAPPID}") + private long SDKAPPID; + @Value("${config.KEY}") + private String KEY; + + @GetMapping("/getUserSig/{userId}") + public ActionResult getUserSig(@PathVariable("userId") String userId) { + String userSig = null; + Object type = redisUtil.getString("userSig"+userId); + if (null == type) { + TLSSigAPIv2 tlsSigAPIv2 = new TLSSigAPIv2(SDKAPPID, KEY); + userSig = tlsSigAPIv2.genUserSig(userId, 60 * 60 * 24 * 60); + redisUtil.insert("userSig" + userId, userSig, 60 * 60 * 24 * 60); + }else { + userSig = type.toString(); + } + return ActionResult.success(userSig); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbMyNoticeAnnouncementsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbMyNoticeAnnouncementsController.java new file mode 100644 index 0000000..df705ca --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbMyNoticeAnnouncementsController.java @@ -0,0 +1,117 @@ +package jnpf.notice.controller.web; + + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsDetailDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsMsgDto; +import jnpf.model.notice.req.announcement.QueryMyWebAnnouncementListReq; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * web 我的公告模块 + * + * @author xgl + * @since 2024-05-08 09:51:26 + */ +@RestController +@RequestMapping("/web/myNoticeAnnouncements") +public class FtbMyNoticeAnnouncementsController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeAnnouncementsService ftbNoticeAnnouncementsService; + + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + /** + * 查询我的公告列表 + * + * @param req + * @return {@link ActionResult}<{@link MyNoticeAnnouncementsDto}> + */ + @GetMapping("/queryList") + public ActionResult> pageLists(@Validated QueryMyWebAnnouncementListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsService.getMyWebPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 查询公告详情 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @GetMapping("/get/{announcementId}") + public ActionResult get(@PathVariable("announcementId") String announcementId) { + return ActionResult.success("成功", ftbNoticeAnnouncementsService.getMyAnnouncementInfo(announcementId, UserProvider.getLoginUserId())); + } + + /** + * 公告完成阅读接口 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @GetMapping("/completeRead/{announcementId}") + public ActionResult completeRead(@PathVariable("announcementId") String announcementId) { + ftbNoticeAnnouncementsService.completeRead(announcementId, UserProvider.getLoginUserId()); + return ActionResult.success("成功", true); + } + + /** + * 查询公告中已读/未读人员列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsReceiveDto}> + */ + @GetMapping("/queryUser") + public ActionResult> queryUserList(@Validated QueryUserListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsReceiveService.queryUserListForReadStatus(req, true); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 轮训我的未读公告 + * + * @return {@link ActionResult} + */ + @GetMapping("/queryAllNoRead") + public ActionResult> queryAllNoRead() { + //needRead 0:全部 1:非必读,2:必读 + return ActionResult.success("成功", ftbNoticeAnnouncementsService.queryAllWebMyNoRead(UserProvider.getLoginUserId(), 0)); + } + + /** + * web轮训用户X掉轮训不显示 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @GetMapping("/cancelWebAlert/{announcementId}") + public ActionResult cancelWebAlert(@PathVariable("announcementId") String announcementId) { + ftbNoticeAnnouncementsReceiveService.cancelWebAlert(announcementId, UserProvider.getLoginUserId()); + return ActionResult.success("成功", true); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsController.java new file mode 100644 index 0000000..7a8c266 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsController.java @@ -0,0 +1,275 @@ +package jnpf.notice.controller.web; + + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDetailDto; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDto; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.AddNoticeAnnouncementsReq; +import jnpf.model.notice.req.announcement.QueryAnnouncementListReq; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import jnpf.model.notice.req.announcement.UpdateNoticeAnnouncementsReq; +import jnpf.notice.FtbNoticeApi; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.notice.utils.NoticeUtils; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * web公告模块 + * + * @author xgl + * @since 2024-05-08 09:51:26 + */ +@Slf4j +@RestController +@RequestMapping("/web/noticeAnnouncements") +public class FtbNoticeAnnouncementsController implements FtbNoticeApi { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeAnnouncementsService ftbNoticeAnnouncementsService; + + + @Autowired + private ConfigValueUtil configValueUtil; + + + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + @Autowired + private RedisTemplate redisTemplate; + + + /** + * 查询公告列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsDto}> + */ + @GetMapping("/queryList") + public ActionResult> pageLists(@Validated QueryAnnouncementListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsService.getWebPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 添加公告 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody AddNoticeAnnouncementsReq req) { + FtbNoticeAnnouncements entity = ftbNoticeAnnouncementsService.insertData(req); + if (req.getOptionBtn().equals(NoticeEnums.OptionButtonType.DRAFT.getCode())) { + return ActionResult.success("保存成功", entity); + } else { + return ActionResult.success("发布成功", entity); + } + } + + + /** + * 编辑公告 + * + * @param id 主键ID + * @param req + * @return {@link ActionResult} + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Validated @RequestBody UpdateNoticeAnnouncementsReq req) { + String lockKey = String.format(NoticeUtils.NOTICE_UPDATE_KEY, id); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", NoticeUtils.NOTICE_UPDATE_MAX_TIME, TimeUnit.SECONDS)) { + try { + FtbNoticeAnnouncements entity = ftbNoticeAnnouncementsService.updateData(id, req); + if (req.getOptionBtn().equals(NoticeEnums.OptionButtonType.DRAFT.getCode())) { + return ActionResult.success("保存成功", entity); + } else { + return ActionResult.success("发布成功", entity); + } + } catch (Exception e) { + log.error("编辑公告失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + + } + + + /** + * 查询详情 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success("成功", ftbNoticeAnnouncementsService.getInfo(id, "web")); + } + + /** + * 删除公告 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbNoticeAnnouncementsService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + + /** + * 发布接口 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @PostMapping("/publish/{id}") + public ActionResult publish(@PathVariable("id") String id) { + + String lockKey = String.format(NoticeUtils.NOTICE_UPDATE_KEY, id); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", NoticeUtils.NOTICE_UPDATE_MAX_TIME, TimeUnit.SECONDS)) { + try { + ftbNoticeAnnouncementsService.publish(id); + return ActionResult.success("发布成功", true); + } catch (Exception e) { + log.error("公告发布失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + /** + * 撤销接口 + * + * @param id 公告主键ID,必传 + * @return {@link ActionResult} + */ + @PostMapping("/cancelPublish/{id}") + public ActionResult cancelPublish(@PathVariable("id") String id) { + ftbNoticeAnnouncementsService.cancelPublish(id); + return ActionResult.success("撤回成功", true); + } + + /** + * 根据公告ID提醒未读人员接口 + * + * @param announcementId 公告ID + * @return {@link ActionResult} + */ + @PostMapping("/alertAll/{announcementId}") + public ActionResult alertAll(@PathVariable("announcementId") String announcementId) { + String lockKey = String.format(NoticeUtils.NOTICE_UPDATE_KEY, announcementId); + String lockValue = UUID.randomUUID().toString(); + + if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, NoticeUtils.NOTICE_UPDATE_MAX_TIME, TimeUnit.SECONDS)) { + try { + ftbNoticeAnnouncementsService.alertAll(announcementId); + return ActionResult.success("提醒成功"); + } catch (Exception e) { + log.error("公告发布失败,announcementId: {}", announcementId, e); + return ActionResult.fail(e.getMessage()); + } finally { + // 安全释放锁:只删除自己持有的锁 + String currentValue = (String) redisTemplate.opsForValue().get(lockKey); + if (lockValue.equals(currentValue)) { + redisTemplate.delete(lockKey); + } + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + /** + * 提醒公告中一人未读接口 + * + * @param announcementId 公告ID + * @param userId 用户ID + * @return {@link ActionResult} + */ + @PostMapping("/alertOne/{announcementId}/{userId}") + public ActionResult alertOne(@PathVariable("announcementId") String announcementId, @PathVariable("userId") String userId) { + ftbNoticeAnnouncementsService.alertOne(announcementId, userId); + return ActionResult.success("提醒成功"); + } + + + /** + * 查询公告中已读/未读人员列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsReceiveDto}> + */ + @GetMapping("/queryUser") + public ActionResult> queryUserList(@Validated QueryUserListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsReceiveService.queryUserListForReadStatus(req, true); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 每半个小时检测一下是否有发布的通知公告 + * + * @param tenantId 租户ID + * @return + */ + @NoDataSourceBind + @GetMapping("/checkPublishNotice") + public ActionResult checkPublishNotice(@RequestParam("tenantId") String tenantId) { + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + ftbNoticeAnnouncementsService.checkPublishNotice(tenantId); + return ActionResult.success("成功", Boolean.TRUE); + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsLogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsLogController.java new file mode 100644 index 0000000..c642f53 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeAnnouncementsLogController.java @@ -0,0 +1,44 @@ +package jnpf.notice.controller.web; + + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsLogDto; +import jnpf.model.notice.req.oplog.QueryLogListReq; +import jnpf.notice.service.FtbNoticeAnnouncementsLogService; +import jnpf.util.FtbUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * web公告操作日志模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/web/noticeLog") +public class FtbNoticeAnnouncementsLogController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeAnnouncementsLogService ftbNoticeAnnouncementsLogService; + + /** + * 查询操作日志 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeAnnouncementsLogDto}> + */ + @GetMapping("/queryList") + public ActionResult> queryList(QueryLogListReq req) { + PageInfo pageVo = ftbNoticeAnnouncementsLogService.queryList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeCategoriesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeCategoriesController.java new file mode 100644 index 0000000..21b66a1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeCategoriesController.java @@ -0,0 +1,95 @@ +package jnpf.notice.controller.web; + + +import jnpf.base.ActionResult; +import jnpf.model.notice.domain.FtbNoticeCategories; +import jnpf.model.notice.dto.FtbNoticeCategoriesDto; +import jnpf.model.notice.req.category.AddCategoryReq; +import jnpf.model.notice.req.category.QueryCategoryListReq; +import jnpf.model.notice.req.category.QueryNextCategoryListReq; +import jnpf.model.notice.req.category.UpdateCategoryReq; +import jnpf.notice.service.FtbNoticeCategoriesService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * web公告分类模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/web/noticeCategories") +public class FtbNoticeCategoriesController { + + @Autowired + private FtbNoticeCategoriesService ftbNoticeCategoriesService; + + + /** + * 查询公告分类接口 + * + * @param req + * @return + */ + @GetMapping("/lists") + public ActionResult> lists(QueryCategoryListReq req) { + + return ActionResult.success("成功", ftbNoticeCategoriesService.listCategory(req)); + } + + /** + * 根据分类ID查询直接下级分类接口 + * + * @param req + * @return + */ + @GetMapping("/nextCatelists") + public ActionResult> nextCatelists(QueryNextCategoryListReq req) { + return ActionResult.success("成功", ftbNoticeCategoriesService.nextCatelists(req)); + } + + + /** + * 添加公告分类 + * + * @param req + * @return + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Valid AddCategoryReq req) { + return ActionResult.success("添加成功", ftbNoticeCategoriesService.insertData(req)); + } + + /** + * 修改公告分类 + * + * @param id 分类ID + * @param req + * @return + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid UpdateCategoryReq req) { + + + return ActionResult.success("编辑成功", ftbNoticeCategoriesService.updateData(id, req)); + } + + /** + * 删除公告分类 + * + * @param id 分类ID + * @return + */ + @DeleteMapping("/del/{id}") + public ActionResult del(@PathVariable("id") String id) { + ftbNoticeCategoriesService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeManagerController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeManagerController.java new file mode 100644 index 0000000..c93bbf9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeManagerController.java @@ -0,0 +1,109 @@ +package jnpf.notice.controller.web; + + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.notice.domain.FtbNoticeManager; +import jnpf.model.notice.dto.FtbNoticeManagerDto; +import jnpf.model.notice.dto.NoticeManagerAuthorityDto; +import jnpf.model.notice.req.manager.AddNoticeManagerReq; +import jnpf.model.notice.req.manager.QueryNoticeManagerReq; +import jnpf.notice.service.FtbNoticeManagerService; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/** + * web公告管理员模块 + * + * @author makejava + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/web/noticeManager") +public class FtbNoticeManagerController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeManagerService ftbNoticeManagerService; + + + /** + * 查询管理员列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbNoticeManagerDto}> + */ + @GetMapping("/queryList") + public ActionResult> pageLists(@Validated QueryNoticeManagerReq req) { + PageInfo pageVo = ftbNoticeManagerService.getPageLists(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 添加管理员 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody AddNoticeManagerReq req) { + ftbNoticeManagerService.insertData(req); + return ActionResult.success("添加成功"); + } + + + /** + * 删除管理员 + * + * @param id 管理员表主键id + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbNoticeManagerService.deleteData(id); + return ActionResult.success("删除成功", true); + } + + + /** + * 查询当前用户是否是通知公告管理员 + * + * @return {@link ActionResult} + */ + @GetMapping("/queryMyNoticeManagerAuthority") + public ActionResult queryMyNoticeManagerAuthority() { + UserInfo userInfo = UserProvider.getUser(); + NoticeManagerAuthorityDto noticeManagerAuthorityDto = new NoticeManagerAuthorityDto(); + if (StringUtil.isEmpty(userInfo.getUserId())) { + noticeManagerAuthorityDto.setIsNoticeManager(false); + return ActionResult.success("成功", noticeManagerAuthorityDto); + } + if (userInfo.getIsAdministrator()) { + noticeManagerAuthorityDto.setIsNoticeManager(true); + return ActionResult.success("成功", noticeManagerAuthorityDto); + } + + + //非系统管理员 判断是否添加了通知公告权限 + FtbNoticeManager ftbNoticeManager = ftbNoticeManagerService.queryByUserId(userInfo.getUserId()); + if (null != ftbNoticeManager) { + noticeManagerAuthorityDto.setIsNoticeManager(true); + return ActionResult.success("成功", noticeManagerAuthorityDto); + } + + noticeManagerAuthorityDto.setIsNoticeManager(false); + return ActionResult.success("成功", noticeManagerAuthorityDto); + + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeUserGroupsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeUserGroupsController.java new file mode 100644 index 0000000..ae28cdc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/controller/web/FtbNoticeUserGroupsController.java @@ -0,0 +1,104 @@ +package jnpf.notice.controller.web; + + +import jnpf.base.ActionResult; +import jnpf.model.notice.dto.FtbNoticeUserGroupsDto; +import jnpf.model.notice.req.group.AddUserGroupReq; +import jnpf.model.notice.req.group.UpdateUserGroupReq; +import jnpf.notice.service.FtbNoticeUserGroupsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * web用户分组表模块 + * + * @author xgl + * @since 2024-05-08 09:51:36 + */ +@RestController +@RequestMapping("/web/noticeUserGroup") +public class FtbNoticeUserGroupsController { + /** + * 服务对象 + */ + @Autowired + private FtbNoticeUserGroupsService ftbNoticeUserGroupsService; + + + /** + * 查询分组列表 + * + * @return {@link ActionResult}<{@link FtbNoticeUserGroupsDto}> + */ + @GetMapping("/queryAllGroup") + public ActionResult> queryAllGroup() { + return ActionResult.success("成功", ftbNoticeUserGroupsService.queryWebAllGroup()); + } + + + /** + * 添加分组 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody AddUserGroupReq req) { + return ActionResult.success("操作成功", ftbNoticeUserGroupsService.insertData(req)); + } + + + /** + * 编辑分组 + * + * @param id 分组主键ID + * @param req + * @return {@link ActionResult} + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Validated @RequestBody UpdateUserGroupReq req) { + ftbNoticeUserGroupsService.updateData(id, req); + return ActionResult.success("操作成功"); + } + + + /** + * 查询分组详情 + * + * @param id 分组主键ID + * @return {@link ActionResult} + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + return ActionResult.success("成功", ftbNoticeUserGroupsService.getInfo(id)); + } + + /** + * 批量查询分组的人员列表 + * + * @param groupIds 分组主键ID,逗号分开 + * @return {@link ActionResult} + */ + @GetMapping("/queryBatchGroupUser/{groupIds}") + public ActionResult> queryBatchGroupUser(@PathVariable("groupIds") String groupIds) { + return ActionResult.success("成功", ftbNoticeUserGroupsService.queryBatchGroupUser(groupIds)); + } + + /** + * 删除分组 + * + * @param id 分组主键ID,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbNoticeUserGroupsService.deleteData(id); + return ActionResult.success(); + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsLogMapper.java new file mode 100644 index 0000000..d526d95 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsLogMapper.java @@ -0,0 +1,29 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsLog; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsLogDto; +import jnpf.model.notice.req.oplog.QueryLogListReq; +import org.apache.ibatis.annotations.Param; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements_log(公告操作日志表)】的数据库操作Mapper + * @createDate 2024-05-08 09:42:09 + * @Entity jnpf.notice.domain.FtbNoticeAnnouncementsLog + */ +public interface FtbNoticeAnnouncementsLogMapper extends BaseMapper { + + /** + * 分页查询通知公告日志 + * @param page + * @param params + * @return + */ + Page pagingQuery(@Param("page") Page page, @Param("params") QueryLogListReq params); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsMapper.java new file mode 100644 index 0000000..c374e91 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsMapper.java @@ -0,0 +1,85 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsDto; +import jnpf.model.notice.dto.MyNoticeAnnouncementsMsgDto; +import jnpf.model.notice.req.announcement.InnerAppQueryAnnouncementListReq; +import jnpf.model.notice.req.announcement.InnerAppQueryMyAnnouncementListReq; +import jnpf.model.notice.req.announcement.InnerQueryMyWebAnnouncementListReq; +import jnpf.model.notice.req.announcement.QueryAnnouncementListReq; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements(公告表)】的数据库操作Mapper + * @createDate 2024-05-08 09:42:09 + * @Entity jnpf.notice.domain.FtbNoticeAnnouncements + */ +public interface FtbNoticeAnnouncementsMapper extends BaseMapper { + + /** + * web端分页查询公告信息 + * @param page + * @param params + * @return + */ + Page getWebPageList(@Param("page") Page page, @Param("params") QueryAnnouncementListReq params); + + /** + * App端分页查询公告信息 + * @param page + * @param params + * @return + */ + Page getAppPageList(@Param("page") Page page, @Param("params") InnerAppQueryAnnouncementListReq params); + + /** + * 分页查询我的公告信息 + * @param page + * @param params + * @return + */ + Page getMyWebPageList(@Param("page") Page page, @Param("params") InnerQueryMyWebAnnouncementListReq params); + /** + * 获取当前用户公告列表的分页信息 + * + * @param page 分页信息 + * @param params 查询当前用户公告列表的请求参数,包括分页信息和查询条件等 + * @return 返回包含当前用户公告列表的分页信息对象,其中包含公告数据和分页元数据 + */ + Page getAppMyPageList(@Param("page") Page page, @Param("params") InnerAppQueryMyAnnouncementListReq params); + /** + * 查询用户最新的3条记录 + * + * @param userId 用户ID + * @param num 数量 + * @return + */ + List getAppMyList(@Param("userId") String userId, @Param("num") Integer num); + + /** + * 查询所有未读消息 + * + * @param userId 用户ID + * @param needRead 0:全部 1:非必读,2:必读 + * @return + */ + List queryAllMyNoRead(@Param("userId") String userId, @Param("num") Integer num, @Param("needRead") Integer needRead); + /** + * web查询所有未读消息 + * + * @param userId 用户ID + * @param needRead 0:全部 1:非必读,2:必读 + * @return + */ + List queryAllWebMyNoRead(@Param("userId") String userId, @Param("needRead") Integer needRead); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsReceiveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsReceiveMapper.java new file mode 100644 index 0000000..517fc3a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeAnnouncementsReceiveMapper.java @@ -0,0 +1,46 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsReceive; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements_receive(公告用户接收表)】的数据库操作Mapper + * @createDate 2024-05-08 09:42:09 + * @Entity jnpf.notice.domain.FtbNoticeAnnouncementsReceive + */ +public interface FtbNoticeAnnouncementsReceiveMapper extends BaseMapper { + /** + * 根据阅读状态查询用户列表 + * @param page + * @param params + * @return + */ + Page queryUserListForReadStatus(@Param("page") Page page, + @Param("params") QueryUserListReq params); + + /** + * 根据公告id查询用户数量 + * @param adList 公告id + * @return 公告数量 + */ + List queryCountAcceptUserNum(@Param("adList") List adList); + + /** + * 批量根据公告id查询用户未提醒数量 + * @param adList 公告id + * @return 公告数量 + */ + List batchQueryNoAlertUserNum(@Param("adList") List adList); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeCategoriesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeCategoriesMapper.java new file mode 100644 index 0000000..711b175 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeCategoriesMapper.java @@ -0,0 +1,18 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.notice.domain.FtbNoticeCategories; + +/** +* @author 许贵林 +* @description 针对表【ftb_notice_categories(公告分类表)】的数据库操作Mapper +* @createDate 2024-05-08 09:42:09 +* @Entity jnpf.notice.domain.FtbNoticeCategories +*/ +public interface FtbNoticeCategoriesMapper extends BaseMapper { + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeFilesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeFilesMapper.java new file mode 100644 index 0000000..03dec66 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeFilesMapper.java @@ -0,0 +1,18 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.notice.domain.FtbNoticeFiles; + +/** +* @author 许贵林 +* @description 针对表【ftb_notice_files(公告附件表)】的数据库操作Mapper +* @createDate 2024-05-08 09:42:09 +* @Entity jnpf.notice.domain.FtbNoticeFiles +*/ +public interface FtbNoticeFilesMapper extends BaseMapper { + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeManagerMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeManagerMapper.java new file mode 100644 index 0000000..6f1b3c9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeManagerMapper.java @@ -0,0 +1,29 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.notice.domain.FtbNoticeManager; +import jnpf.model.notice.dto.FtbNoticeManagerDto; +import jnpf.model.notice.req.manager.QueryNoticeManagerReq; +import org.apache.ibatis.annotations.Param; + +/** +* @author 许贵林 +* @description 针对表【ftb_notice_manager(公告管理员表)】的数据库操作Mapper +* @createDate 2024-05-08 09:42:09 +* @Entity jnpf.notice.domain.FtbNoticeManager +*/ +public interface FtbNoticeManagerMapper extends BaseMapper { + + /** + * 分页查询通知公告管理员列表 + * @param page + * @param params + * @return + */ + Page pagingQuery(@Param("page") Page page, @Param("params") QueryNoticeManagerReq params); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupMembersMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupMembersMapper.java new file mode 100644 index 0000000..9fc301a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupMembersMapper.java @@ -0,0 +1,43 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.notice.domain.FtbNoticeUserGroupMembers; +import jnpf.model.notice.dto.FtbNoticeUserGroupMembersDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** +* @author 许贵林 +* @description 针对表【ftb_notice_user_group_members(用户分组成员表)】的数据库操作Mapper +* @createDate 2024-05-08 09:42:09 +* @Entity jnpf.notice.domain.FtbNoticeUserGroupMembers +*/ +public interface FtbNoticeUserGroupMembersMapper extends BaseMapper { + /** + * 分页查询用户分组列表 + * @param page + * @param groupId 分组id + * @return + */ + Page pagingQuery(@Param("page") Page page, @Param("groupId") String groupId, @Param("powerUserIds") List powerUserIds); + + /** + * 查询所有的用户分组列表 + * @param groupId 分组id + * @return + */ + List queryAllList(@Param("groupId") String groupId, @Param("powerUserIds") List powerUserIds); + + /** + * 根据分组id查询所有的用户分组列表 + * @param groupIds 分组id集合 + * @return + */ + List queryAllListForGroupIds(@Param("groupIds") List groupIds, @Param("powerUserIds") List powerUserIds); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupsMapper.java new file mode 100644 index 0000000..5a8151d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/mapper/FtbNoticeUserGroupsMapper.java @@ -0,0 +1,27 @@ +package jnpf.notice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.notice.domain.FtbNoticeUserGroups; +import jnpf.model.notice.dto.FtbNoticeUserGroupsDto; +import org.apache.ibatis.annotations.Param; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_user_groups(用户分组表)】的数据库操作Mapper + * @createDate 2024-05-08 09:42:09 + * @Entity jnpf.notice.domain.FtbNoticeUserGroups + */ +public interface FtbNoticeUserGroupsMapper extends BaseMapper { + + /** + * 分页查询用户组信息 + * @param page 分页信息 + * @return + */ + Page pagingQuery(@Param("page") Page page, @Param("loginUserId") String loginUserId); +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsLogService.java new file mode 100644 index 0000000..6a0886b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsLogService.java @@ -0,0 +1,30 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsLog; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsLogDto; +import jnpf.model.notice.req.oplog.QueryLogListReq; + +/** +* @author 许贵林 +* @description 针对表【ftb_notice_announcements_log(公告操作日志表)】的数据库操作Service +* @createDate 2024-05-08 09:42:09 +*/ +public interface FtbNoticeAnnouncementsLogService extends IService { + /** + * 查询公告通知日志列表 + * + * @param req 查询条件对象,包含查询列表所需的各种过滤条件 + * @return 返回一个分页对象,包含查询到的公告通知日志列表和相关分页信息 + */ + PageInfo queryList(QueryLogListReq req); + + /** + * 记录公告通知日志 + * + * @param log 日志对象,包含需要记录的日志详细信息 + */ + void recordLog(FtbNoticeAnnouncementsLog log); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsReceiveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsReceiveService.java new file mode 100644 index 0000000..32c886f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsReceiveService.java @@ -0,0 +1,184 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsReceive; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.QueryUserListReq; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements_receive(公告用户接收表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeAnnouncementsReceiveService extends IService { + + /** + * 根据公告ID查询用户 + * + * @param announcementId 公告ID + */ + List queryUserList(String announcementId); + + /** + * 根据阅读状态 查询公告的用户 + * + * @param req + * @param selectUserOrgAndPosition 是否查询用户组织及职位 true 查询 false 不查询 + */ + PageInfo queryUserListForReadStatus(QueryUserListReq req, Boolean selectUserOrgAndPosition); + + + /** + * 根据提醒状态 查询公告的用户 + * + * @param announcementId 公告ID + * @param alertStatus 提醒状态 1未提醒 2已提醒 + * @return + */ + List queryUserListForAlertStatus(String announcementId, Integer alertStatus); + + /** + * 添加公告接收用户 + * + * @param announcementsId 公告ID + * @param userList 接收用户列表 + */ + List addReceiveUser(String announcementsId, List userList); + + /** + * 删除公告接收用户 + * + * @param announcementsId 公告ID + */ + void deleteReceiveUser(String announcementsId); + + /** + * 取消接收用户通知 + * + * @param announcementId 通知的唯一标识符,用于标识特定的通知 + */ + void cancelReceiveUser(String announcementId); + + /** + * 重置通知的已读、提醒和网页取消状态 + * + * @param announcementId 通知的唯一标识符,用于标识特定的通知 + */ + void resetReadAndAlertAndWebCancel(String announcementId); + + + /** + * 编辑公告时,处理公告接收用户 + * + * @param announcementId 公告ID + * @param noticeStatus + */ + void dealAnnouncementUserWhenEdit(String announcementId, List userList, NoticeEnums.NoticeStatus noticeStatus); + + /** + * 完成提醒 + * + * @param announcementId + */ + void updateCompleteAlertAll(String announcementId); + + /** + * 根据公告ID和用户ID查询用户 + * + * 此方法旨在处理特定公告与用户的关联查询,用于获取特定公告的用户信息 + * + * @param announcementId 公告ID,用于标识特定的公告 + * @param userId 用户ID,用于标识特定的用户 + * @return 返回包含公告和用户信息的对象,便于进一步处理和展示 + */ + FtbNoticeAnnouncementsReceive queryUserForUserId(String announcementId, String userId); + + + /** + * 修改一个用户已经完成提醒 + * + * @param announcementId + * @param userId + */ + void updateCompleteAlert(String announcementId, String userId); + + /** + * 完成阅读 + * + * @param announcementId + * @param userId + */ + void updateCompleteRead(String announcementId, String userId); + + /** + * 根据用户ID查询用户通知公告记录(忽略删除状态) + * 此方法用于获取特定用户的通知公告记录,即使该记录被标记为已删除也能查询到 + * 主要用于后台管理或数据统计等场景,需要管理员权限 + * + * @param announcementId 公告ID,用于标识特定的通知公告 + * @param userId 用户ID,用于标识接收通知公告的用户 + * @return 返回匹配指定公告ID和用户ID的通知公告记录,包括未删除和已删除的记录 + */ + FtbNoticeAnnouncementsReceive queryUserForUserIdIngoreDelete(String announcementId, String userId); + + + /** + * 检查查询公告 是否已读,返回未读列表 + * + * @param userId + * @param ids + * @return + */ + List checkIsRead(String userId, List ids); + + /** + * 查询公告阅读状态的数量 + * + * @param announcementId + * @param readStatus 1未读,2已读 + * @return + */ + Long queryReadStatusCount(String announcementId, Integer readStatus); + + /** + * 查询未提醒用户数量 + * + * @param announcementId + * @return + */ + Long queryNoAlertUserNum(String announcementId); + /** + * 取消网页公告提醒 + * + * @param announcementId 公告的唯一标识符 + * @param userId 用户的唯一标识符 + */ + void cancelWebAlert(String announcementId, String userId); + + /** + * 查询需要提醒的用户列表 + * + * @param announcementId 公告的唯一标识符 + * @return 返回需要提醒的用户列表 + */ + List queryNeedAlertUserList(String announcementId); + + /** + * 根据公告id查询用户数量 + * @param adList 公告id + * @return 公告数量 + */ + List queryCountAcceptUserNum(List adList); + + /** + * 批量根据公告id查询用户未提醒数量 + * @param adList 公告id + * @return 公告数量 + */ + List batchQueryNoAlertUserNum(List adList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsService.java new file mode 100644 index 0000000..f069e04 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeAnnouncementsService.java @@ -0,0 +1,156 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.dto.*; +import jnpf.model.notice.req.announcement.*; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements(公告表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeAnnouncementsService extends IService { + + /** + * Web端管理员查询公告列表 + * + * @param req + * @return + */ + PageInfo getWebPageList(QueryAnnouncementListReq req); + + /** + * 新增公告 + * @param req + * @return + */ + FtbNoticeAnnouncements insertData(AddNoticeAnnouncementsReq req); + + /** + * 修改公告 + * @param id 公告id + * @param req + * @return + */ + FtbNoticeAnnouncements updateData(String id, UpdateNoticeAnnouncementsReq req); + + /** + * 查询公告基本信息 + * @param id 公告id + * @param terminal 终端【web or app】 + * @return + */ + FtbNoticeAnnouncementsDetailDto getInfo(String id,String terminal); + + /** + * 发布公告 + * @param id 公告id + */ + void publish(String id); + + /** + * 删除公告 + * @param id 公告id + */ + void deleteData(String id); + + /** + * 提醒公告 + * @param announcementId 公告id + * @param userId 用户id + */ + void alertOne(String announcementId, String userId); + + /** + * 提醒公告相关的所有用户 + * @param announcementId 公告id + */ + void alertAll(String announcementId); + + /** + * 取消发布 + * @param id 公告id + */ + void cancelPublish(String id); + + /** + * 分页查询当前用户的公告列表 + * @param req + * @return + */ + PageInfo getMyWebPageList(QueryMyWebAnnouncementListReq req); + /** + * 获取个人公告详情 + * + * @param announcementId 公告ID,用于标识特定的公告 + * @param userId 用户ID,用于标识请求公告信息的用户 + * @return 返回一个MyNoticeAnnouncementsDetailDto对象,包含公告的详细信息 + */ + MyNoticeAnnouncementsDetailDto getMyAnnouncementInfo(String announcementId, String userId); + + /** + * 查询所有未读消息 + * + * @param userId 用户ID + * @param needRead 0:全部 1:非必读,2:必读 + * @return + */ + List queryAllMyNoRead(String userId, Integer num, Integer needRead); + + /** + * 查询用户的最近的几条公告 + * @param userId 用户ID + * @param num 数量 + * @return + */ + List queryMyLatest(String userId, Integer num); + + /** + * 获取当前用户公告列表的分页信息 + * + * @param req 查询当前用户公告列表的请求参数,包括分页信息和查询条件等 + * @return 返回包含当前用户公告列表的分页信息对象,其中包含公告数据和分页元数据 + */ + PageInfo getAppMyPageList(AppQueryMyAnnouncementListReq req); + + /** + * 获取应用公告分页列表 + * + * @param req 查询应用公告列表的请求对象,包含查询条件和分页信息 + * @return 返回包含公告列表的分页信息的Dto(数据传输对象)对象 + */ + PageInfo getAppPageList(AppQueryAnnouncementListReq req); + + /** + * 定时检测定时发布的公告,到时间点就发布公告 + * @param tenantId 租户id + */ + void checkPublishNotice(String tenantId); + + /** + * 完成阅读 + * + * @param announcementId 公告ID + * @param userId 用户ID + */ + void completeRead(String announcementId, String userId); + /** + * 查询并检测公告是否存在 + * @param id 公告id + * @return + */ + FtbNoticeAnnouncements queryAndCheckById(String id); + + /** + * web查询所有未读消息 + * + * @param userId 用户ID + * @param needRead 0:全部 1:非必读,2:必读 + * @return + */ + List queryAllWebMyNoRead(String userId, Integer needRead); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeCategoriesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeCategoriesService.java new file mode 100644 index 0000000..cfe5482 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeCategoriesService.java @@ -0,0 +1,95 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.notice.domain.FtbNoticeCategories; +import jnpf.model.notice.dto.AppNoticeCategoriesDto; +import jnpf.model.notice.dto.FtbNoticeCategoriesDto; +import jnpf.model.notice.req.category.AddCategoryReq; +import jnpf.model.notice.req.category.QueryCategoryListReq; +import jnpf.model.notice.req.category.QueryNextCategoryListReq; +import jnpf.model.notice.req.category.UpdateCategoryReq; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_categories(公告分类表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeCategoriesService extends IService { + /** + * 查询所有的通知公告分类 + * @param req + * @return + */ + List listCategory(QueryCategoryListReq req); + + /** + * 添加分类 + * @param req + * @return + */ + FtbNoticeCategories insertData(AddCategoryReq req); + + /** + * 根据分类id删除分类 + * @param id 分类id + */ + void deleteData(String id); + + /** + * 编辑分类 + * @param id 分类id + * @param req + * @return + */ + FtbNoticeCategories updateData(String id, UpdateCategoryReq req); + + /** + * 查询子分类 + * @param parentId 父分类id + * @return + */ + List queryChildCategory(String parentId); + + /** + * 查询并检查分类 + * @param id 分类id + * @return + */ + FtbNoticeCategories queryAndCheckById(String id); + + /** + * 查询所有分类并返回分类树 + * @return + */ + List listAll(); + + /** + * 查询子分类 + * @param currCategory + * @return + */ + List queryChildCategoryIds(FtbNoticeCategories currCategory); + + /** + * 查询下级分类 + * @param req + * @return + */ + List nextCatelists(QueryNextCategoryListReq req); + + /** + * 查询是否有子分类 + * @param list + * @return + */ + List fillHasChild(List list); + + /** + * 查询子分类数量 + * @param parentId 分类id + * @return + */ + Long queryChildCategoryNum(String parentId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeFilesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeFilesService.java new file mode 100644 index 0000000..269e0b3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeFilesService.java @@ -0,0 +1,38 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.notice.domain.FtbNoticeFiles; +import jnpf.model.notice.dto.FtbNoticeFilesDto; +import jnpf.model.notice.req.announcement.NoticeFilesVo; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_files(公告附件表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeFilesService extends IService { + + /** + * 添加附件 + * + * @param announcementId 公告id + * @param fileList 附件列表 + */ + void addFiles(String announcementId, List fileList); + + /** + * 删除公告 附件 + * + * @param announcementId 公告id + */ + void deleteFiles(String announcementId); + + /** + * 查询公告附件 + * @param announcementId 公告id + * @return + */ + List getFiles(String announcementId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeManagerService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeManagerService.java new file mode 100644 index 0000000..a4c5036 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeManagerService.java @@ -0,0 +1,47 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.notice.domain.FtbNoticeManager; +import jnpf.model.notice.dto.FtbNoticeManagerDto; +import jnpf.model.notice.req.manager.AddNoticeManagerReq; +import jnpf.model.notice.req.manager.QueryNoticeManagerReq; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_manager(公告管理员表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeManagerService extends IService { + + /** + * 查询公告分页列表 + * + * @param req 查询条件对象,包含查询公告所需的信息 + * @return 返回包含公告管理器DTO的分页信息对象 + */ + PageInfo getPageLists(QueryNoticeManagerReq req); + + /** + * 插入公告数据 + * + * @param req 插入公告请求对象,包含需要插入的公告信息 + */ + void insertData(AddNoticeManagerReq req); + + /** + * 删除公告数据 + * + * @param id 公告的唯一标识符,用于标识需要删除的公告 + */ + void deleteData(String id); + + /** + * 根据用户ID查询公告 + * + * @param userId 用户ID,用于查找该用户相关的公告 + * @return 返回与指定用户ID相关的公告管理器对象 + */ + FtbNoticeManager queryByUserId(String userId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupMembersService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupMembersService.java new file mode 100644 index 0000000..186e67e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupMembersService.java @@ -0,0 +1,66 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.notice.domain.FtbNoticeUserGroupMembers; +import jnpf.model.notice.dto.FtbNoticeUserGroupMembersDto; +import jnpf.model.notice.req.group.ListGroupUserMemberReq; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_user_group_members(用户分组成员表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeUserGroupMembersService extends IService { + /** + * 分页获取用户组成员列表 + * + * @param groupId 用户组ID,用于定位特定的用户组 + * @param req 包含分页信息和查询条件的请求对象 + * @return 返回分页的用户组成员列表 + */ + PageInfo getPageList(List powerUserIds, String groupId, ListGroupUserMemberReq req); + + /** + * 获取用户组的所有成员信息 + * + * @param groupId 用户组ID,用于定位特定的用户组 + * @param selectFlowerAndHeadLogo 指示是否需要同时查询用户的鲜花数和头像的布尔值 + * @return 返回用户组的所有成员信息列表 + */ + List getAllList(String groupId, Boolean selectFlowerAndHeadLogo); + + /** + * 批量添加用户到分组 + * + * @param groupId 分组ID + * @param userList 人员列表 + */ + void batchAddGroupUser(String groupId, List userList); + + /** + * 批量删除用户从分组 + * + * @param groupId 分组ID + */ + void batchDeleteGroupUser(String groupId); + /** + * 根据组ID列表获取所有列表 + * 该方法用于根据提供的组ID列表返回所有相关的列表 + * + * @param groupIds 组ID列表,用于识别和查询相关的列表 + * @return 返回一个字符串列表,包含所有与提供的组ID相关的项目 + */ + List getAllListByGroupIds(List groupIds, List leaveUserIds); + + /** + * 根据组ID列表查询批次组用户应用情况 + * 该方法旨在通过提供的组ID列表查询并返回组内的用户应用情况 + * + * @param groupIdList 组ID列表,用于识别和查询组内用户的应用情况 + * @return 返回一个包含组用户应用情况的列表,每项代表一个组的用户应用详情 + */ + List queryBatchGroupUserApp(List groupIdList, List powerUserIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupsService.java new file mode 100644 index 0000000..fdd8769 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/FtbNoticeUserGroupsService.java @@ -0,0 +1,82 @@ +package jnpf.notice.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.common.PageDto; +import jnpf.model.notice.domain.FtbNoticeUserGroups; +import jnpf.model.notice.dto.FtbNoticeUserGroupMembersDto; +import jnpf.model.notice.dto.FtbNoticeUserGroupsDto; +import jnpf.model.notice.req.group.AddUserGroupReq; +import jnpf.model.notice.req.group.UpdateUserGroupReq; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_user_groups(用户分组表)】的数据库操作Service + * @createDate 2024-05-08 09:42:09 + */ +public interface FtbNoticeUserGroupsService extends IService { + /** + * 查询网站所有用户组 + * + * @return 包含所有用户组的列表 + */ + List queryWebAllGroup(); + + /** + * 插入用户组数据 + * + * @param req 包含要插入用户组信息的请求对象 + * @return 插入操作的结果信息 + */ + String insertData(AddUserGroupReq req); + + /** + * 更新用户组数据 + * + * @param id 用户组的唯一标识 + * @param req 包含要更新用户组信息的请求对象 + */ + void updateData(String id, UpdateUserGroupReq req); + + /** + * 获取用户组详细信息 + * + * @param id 用户组的唯一标识 + * @return 包含用户组详细信息的数据传输对象 + */ + FtbNoticeUserGroupsDto getInfo(String id); + + /** + * 删除用户组数据 + * + * @param id 用户组的唯一标识 + */ + void deleteData(String id); + + /** + * 查询用户组分页信息 + * + * @param req 分页查询请求对象 + * @return 包含分页用户组信息的分页对象 + */ + PageInfo queryAppPage(PageDto req); + + /** + * 批量查询用户组用户 + * + * @param groupIds 用户组的标识列表 + * @return 包含用户信息的列表 + */ + List queryBatchGroupUser(String groupIds); + + /** + * 批量查询用户组用户(适用于APP) + * + * @param groupIds 用户组的标识列表 + * @return 包含用户组成员信息的列表 + */ + List queryBatchGroupUserApp(String groupIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsLogServiceImpl.java new file mode 100644 index 0000000..aad0db7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsLogServiceImpl.java @@ -0,0 +1,56 @@ +package jnpf.notice.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsLog; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsLogDto; +import jnpf.model.notice.req.oplog.QueryLogListReq; +import jnpf.notice.mapper.FtbNoticeAnnouncementsLogMapper; +import jnpf.notice.service.FtbNoticeAnnouncementsLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements_log(公告操作日志表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeAnnouncementsLogServiceImpl extends ServiceImpl + implements FtbNoticeAnnouncementsLogService { + + /** + * 查询公告操作日志列表 + * + * @param req 查询条件对象,包含查询列表所需的各种过滤条件 + * @return + */ + @Override + public PageInfo queryList(QueryLogListReq req) { + Page queryPage = baseMapper.pagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 记录日志 + * + * @param log 日志对象,包含需要记录的日志详细信息 + */ + @Override + public void recordLog(FtbNoticeAnnouncementsLog log) { + baseMapper.insert(log); + } +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsReceiveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsReceiveServiceImpl.java new file mode 100644 index 0000000..e881ac9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsReceiveServiceImpl.java @@ -0,0 +1,486 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsReceive; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import jnpf.notice.mapper.FtbNoticeAnnouncementsReceiveMapper; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.notice.utils.NoticeUtils; +import jnpf.permission.entity.UserEntity; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements_receive(公告用户接收表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeAnnouncementsReceiveServiceImpl extends ServiceImpl + implements FtbNoticeAnnouncementsReceiveService { + + + @Autowired + private NoticeUtils noticeUtils; + + @Autowired + private FtbNoticeAnnouncementsService ftbNoticeAnnouncementsService; + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 根据公告ID查询用户 + * + * @param announcementId 公告ID + */ + @Override + public List queryUserList(String announcementId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode())// 是否撤销,0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List list = baseMapper.selectList(wrapper); + return convertFtbNoticeAnnouncementsReceiveDto(list); + } + + + /** + * 查询用户列表 + * + * @param req + * @param selectUserOrgAndPosition 是否查询用户组织及职位 true 查询 false 不查询 + * @return + */ + @Override + public PageInfo queryUserListForReadStatus(QueryUserListReq req, Boolean selectUserOrgAndPosition) { + //判断如果是全部用户 且 草稿 待发布 撤销状态 直接查询所有用户 + FtbNoticeAnnouncements noticeAnnouncements = ftbNoticeAnnouncementsService.queryAndCheckById(req.getAnnouncementId()); + NoticeEnums.ReceiveUserTypeEnum receiveUserTypeEnum = NoticeEnums.ReceiveUserTypeEnum.getByCode(noticeAnnouncements.getReceiveUserType()); + NoticeEnums.NoticeStatus oldNoticeStaus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.ALL + && oldNoticeStaus != NoticeEnums.NoticeStatus.PUBLISHED) { + if (req.getType().equals(1)) { + return queryAllAcceptUser(req); + } else { + return PageInfo.EMPTY; + } + } + List powerUserIds = new ArrayList<>(); + if(NoticeEnums.NoticeStatus.PUBLISHED!=oldNoticeStaus){ + //过滤权限 + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if(innerPowerUserVO.getCode().equals(2)){ + powerUserIds = List.of("-1"); + } else if (innerPowerUserVO.getCode().equals(1)) { + powerUserIds.addAll(innerPowerUserVO.getUserIds()); + } + } + req.setPowerUserIds(powerUserIds); + + Page queryPage = baseMapper.queryUserListForReadStatus(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + + if (CollectionUtil.isNotEmpty(records)) { + noticeUtils.fillFlowerAndHeadLog(records); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 查询全部接受用户 + * @param req + * @return + */ + private PageInfo queryAllAcceptUser(QueryUserListReq req) { + return noticeUtils.queryUserByPage(req); + + } + + /** + * 根据提醒状态 查询公告的用户 + * + * @param announcementId 公告ID + * @param alertStatus 提醒状态 1未提醒 2已提醒 + * @return + */ + @Override + public List queryUserListForAlertStatus(String announcementId, Integer alertStatus) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getAlertStatus, alertStatus) //1未提醒 2已提醒 + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List list = baseMapper.selectList(wrapper); + return convertFtbNoticeAnnouncementsReceiveDto(list); + } + + /** + * 查询需要提醒的用户列表 + * @param announcementId 公告的唯一标识符 + * @return + */ + @Override + public List queryNeedAlertUserList(String announcementId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.UNNOTIFIED.getCode()) //1未提醒 2已提醒 + .eq(FtbNoticeAnnouncementsReceive::getReadStatus, NoticeEnums.ReadStatus.UNREAD.getCode()) //是否已读,1未读,2已读 + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List list = baseMapper.selectList(wrapper); + return convertFtbNoticeAnnouncementsReceiveDto(list); + } + + /** + * 完成提醒 + * + * @param announcementId + */ + @Override + public void updateCompleteAlertAll(String announcementId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.UNNOTIFIED.getCode()) //1未提醒 2已提醒 + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .set(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.NOTIFIED.getCode()) + .set(FtbNoticeAnnouncementsReceive::getAlertTime, new Date()); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + /** + * 修改一个用户已经完成提醒 + * + * @param announcementId + * @param userId + */ + @Override + public void updateCompleteAlert(String announcementId, String userId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.UNNOTIFIED.getCode()) //1未提醒 2已提醒 + .eq(FtbNoticeAnnouncementsReceive::getUserId, userId) + .set(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.NOTIFIED.getCode()) + .set(FtbNoticeAnnouncementsReceive::getAlertTime, new Date()); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + /** + * 完成阅读 + * + * @param announcementId + * @param userId + */ + @Override + public void updateCompleteRead(String announcementId, String userId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getReadStatus, NoticeEnums.ReadStatus.UNREAD.getCode()) //1未读,2已读 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .eq(FtbNoticeAnnouncementsReceive::getUserId, userId) + .set(FtbNoticeAnnouncementsReceive::getReadStatus, NoticeEnums.ReadStatus.READ.getCode()) + .set(FtbNoticeAnnouncementsReceive::getReadTime, new Date()); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + + + /** + * 根据公告ID和用户ID查询用户 + * + * 此方法旨在处理特定公告与用户的关联查询,用于获取特定公告的用户信息 + * + * @param announcementId 公告ID,用于标识特定的公告 + * @param userId 用户ID,用于标识特定的用户 + * @return 返回包含公告和用户信息的对象,便于进一步处理和展示 + */ + @Override + public FtbNoticeAnnouncementsReceive queryUserForUserId(String announcementId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getUserId, userId) + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return null; + } + return list.get(0); + + } + /** + * 根据用户ID查询用户通知公告记录(忽略删除状态) + * 此方法用于获取特定用户的通知公告记录,即使该记录被标记为已删除也能查询到 + * 主要用于后台管理或数据统计等场景,需要管理员权限 + * + * @param announcementId 公告ID,用于标识特定的通知公告 + * @param userId 用户ID,用于标识接收通知公告的用户 + * @return 返回匹配指定公告ID和用户ID的通知公告记录,包括未删除和已删除的记录 + */ + @Override + public FtbNoticeAnnouncementsReceive queryUserForUserIdIngoreDelete(String announcementId, String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getUserId, userId); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return null; + } + return list.get(0); + + } + /** + * 检查查询公告 是否已读,返回未读列表 + * + * @param userId + * @param ids + * @return + */ + @Override + public List checkIsRead(String userId, List ids) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbNoticeAnnouncementsReceive::getAnnouncementId) + .in(FtbNoticeAnnouncementsReceive::getAnnouncementId, ids) + .eq(FtbNoticeAnnouncementsReceive::getUserId, userId) + .eq(FtbNoticeAnnouncementsReceive::getReadStatus, NoticeEnums.ReadStatus.UNREAD.getCode()) + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()).orderByDesc(FtbNoticeAnnouncementsReceive::getCreatorTime); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + List noReadLit = new ArrayList<>(); + for (FtbNoticeAnnouncementsReceive ftbNoticeAnnouncementsReceive : list) { + noReadLit.add(ftbNoticeAnnouncementsReceive.getAnnouncementId()); + } + return noReadLit; + } + /** + * 查询公告阅读状态的数量 + * + * @param announcementId + * @param readStatus 1未读,2已读 + * @return + */ + @Override + public Long queryReadStatusCount(String announcementId, Integer readStatus) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbNoticeAnnouncementsReceive::getUserId) + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getReadStatus, readStatus) + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()).orderByDesc(FtbNoticeAnnouncementsReceive::getCreatorTime); + List list= baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty( list)) { + return 0L; + } + List userIds = list.stream().map(FtbNoticeAnnouncementsReceive::getUserId).collect(Collectors.toList()); + List userNameForUserIdsReturnList = userApiV2Util.getUserNameForUserIdsReturnList(userIds); + return Long.valueOf(userNameForUserIdsReturnList.size()); + + } + /** + * 查询未提醒用户数量 + * + * @param announcementId + * @return + */ + @Override + public Long queryNoAlertUserNum(String announcementId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.UNNOTIFIED.getCode()) + .eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()) //0未撤销 1已撤销 + .eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()).orderByDesc(FtbNoticeAnnouncementsReceive::getCreatorTime); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return 0L; + } + List userIds = list.stream().map(FtbNoticeAnnouncementsReceive::getUserId).collect(Collectors.toList()); + List userNameForUserIdsReturnList = userApiV2Util.getUserNameForUserIdsReturnList(userIds); + return Long.valueOf(userNameForUserIdsReturnList.size()); + } + /** + * 取消网页公告提醒 + * + * @param announcementId 公告的唯一标识符 + * @param userId 用户的唯一标识符 + */ + @Override + public void cancelWebAlert(String announcementId, String userId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId) + .eq(FtbNoticeAnnouncementsReceive::getUserId, userId) + .set(FtbNoticeAnnouncementsReceive::getWebCancel, 1); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + + /** + * 添加公告接收用户 + * + * @param announcementsId 公告ID + * @param userList 接收用户列表 + */ + @Override + public List addReceiveUser(String announcementsId, List userList) { + if (CollectionUtil.isEmpty(userList) || StringUtils.isEmpty(announcementsId)) { + return new ArrayList<>(); + } + //兼容 重复撤销和发布 + Map map = new HashMap<>(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbNoticeAnnouncementsReceive::getId, FtbNoticeAnnouncementsReceive::getUserId) + .eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementsId); + List oldList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(oldList)) { + for (FtbNoticeAnnouncementsReceive ftbNoticeAnnouncementsReceive : oldList) { + map.put(ftbNoticeAnnouncementsReceive.getUserId(), ftbNoticeAnnouncementsReceive); + } + } + + List addList = new ArrayList<>(); + List updateList = new ArrayList<>(); + for (String userId : userList) { + FtbNoticeAnnouncementsReceive receive = new FtbNoticeAnnouncementsReceive(); + FtbNoticeAnnouncementsReceive old = map.get(userId); + + receive.setAnnouncementId(announcementsId); + receive.setUserId(userId); + receive.setIsCancel(NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()); + receive.setAlertStatus(NoticeEnums.AlertStatus.UNNOTIFIED.getCode()); + receive.setReadStatus(NoticeEnums.ReadStatus.UNREAD.getCode()); + receive.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + receive.setWebCancel(0); + receive.setCreatorTime(new Date()); + if (null != old) { + receive.setId(old.getId()); + updateList.add(receive); + } else { + addList.add(receive); + } + + } + if (CollectionUtil.isNotEmpty(updateList)) { + updateBatchById(updateList); + } + if (CollectionUtil.isNotEmpty(addList)) { + saveBatch(addList); + } + addList.addAll(updateList); + return addList; + } + /** + * 删除公告接收用户 + * + * @param announcementsId 公告ID + */ + @Override + public void deleteReceiveUser(String announcementsId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementsId); + updateWrapper.set(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.INVALID.getCode()); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + /** + * 取消接收用户通知 + * + * @param announcementId 通知的唯一标识符,用于标识特定的通知 + */ + @Override + public void cancelReceiveUser(String announcementId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId); + updateWrapper.set(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.CANCEL.getCode()); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + /** + * 重置通知的已读、提醒和网页取消状态 + * + * @param announcementId 通知的唯一标识符,用于标识特定的通知 + */ + @Override + public void resetReadAndAlertAndWebCancel(String announcementId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.NOT_CANCEL.getCode()); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + updateWrapper.set(FtbNoticeAnnouncementsReceive::getAlertStatus, NoticeEnums.AlertStatus.UNNOTIFIED.getCode()); + updateWrapper.set(FtbNoticeAnnouncementsReceive::getReadStatus, NoticeEnums.ReadStatus.UNREAD.getCode()); + updateWrapper.set(FtbNoticeAnnouncementsReceive::getWebCancel, 0); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + } + /** + * 编辑公告时,处理公告接收用户 + * + * @param announcementId 公告ID + * @param noticeStatus + */ + @Override + public void dealAnnouncementUserWhenEdit(String announcementId, List userList, NoticeEnums.NoticeStatus noticeStatus) { + + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeAnnouncementsReceive::getAnnouncementId, announcementId); + updateWrapper.set(FtbNoticeAnnouncementsReceive::getIsCancel, NoticeEnums.UserNoticeCancelStatus.CANCEL.getCode()); + baseMapper.update(new FtbNoticeAnnouncementsReceive(), updateWrapper); + + + } + + /** + * 对象转换 + * @param list + * @return + */ + private List convertFtbNoticeAnnouncementsReceiveDto(List list) { + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return BeanUtil.copyToList(list, FtbNoticeAnnouncementsReceiveDto.class); + } + + + /** + * 根据公告id查询用户数量 + * @param adList 公告id + * @return 公告数量 + */ + @Override + public List queryCountAcceptUserNum(List adList) { + return baseMapper.queryCountAcceptUserNum(adList); + } + + /** + * 批量根据公告id查询用户未提醒数量 + * @param adList 公告id + * @return 公告数量 + */ + @Override + public List batchQueryNoAlertUserNum(List adList) { + return baseMapper.batchQueryNoAlertUserNum(adList); + } +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsServiceImpl.java new file mode 100644 index 0000000..cc0ba95 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeAnnouncementsServiceImpl.java @@ -0,0 +1,1354 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.base.UserInfo; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.dto.learn.BatchCommonCountDto; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsLog; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsReceive; +import jnpf.model.notice.domain.FtbNoticeCategories; +import jnpf.model.notice.dto.*; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.*; +import jnpf.notice.mapper.FtbNoticeAnnouncementsMapper; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.notice.service.FtbNoticeCategoriesService; +import jnpf.notice.service.FtbNoticeFilesService; +import jnpf.notice.utils.NoticeAsyncDealUtil; +import jnpf.notice.utils.NoticeUtils; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_announcements(公告表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +@Slf4j +public class FtbNoticeAnnouncementsServiceImpl extends ServiceImpl implements FtbNoticeAnnouncementsService { + private final static Integer CONTENT_CUT_MAX_LENGTH = 250;//公告内容shortContent最大长度 + + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + + @Autowired + private FtbNoticeFilesService ftbNoticeFilesService; + + @Autowired + private NoticeUtils noticeUtils; + + @Autowired + private NoticeAsyncDealUtil noticeAsyncDealUtil; + + @Autowired + private FtbNoticeCategoriesService categoriesService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 查询公告列表 + * + * @param req + * @return + */ + @Override + public PageInfo getWebPageList(QueryAnnouncementListReq req) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(req.getKeyWord().trim()); + } + String loginUserId = UserProvider.getLoginUserId(); + //只有超级管理员才查所有,其他查询自己 + if (userApiV2Util.hasAllPermission()) { + req.setInnerPowerUserId(""); + } else { + req.setInnerPowerUserId(loginUserId); + } + Page queryPage = baseMapper.getWebPageList(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + //由于要显示一级和二级分类 + fillCategoryInfo(records); + //发布人信息 + fillUserInfo(records, 1); + //查询未提醒人员数量 + addNoAlertUserNum(records); + addAcceptUserNum(records); + + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + private void addAcceptUserNum(List records) { + if (CollectionUtil.isEmpty(records)) { + return; + } + List adList = new ArrayList<>(); + for (FtbNoticeAnnouncementsDto record : records) { + if (record.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + adList.add(record.getId()); + record.setUserCount(0L); + } + } + if (CollectionUtil.isEmpty(adList)) { + return; + } + List list = ftbNoticeAnnouncementsReceiveService.queryCountAcceptUserNum(adList); + Map ret = new HashMap<>(); + if (CollectionUtil.isNotEmpty(list)) { + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + } + for (FtbNoticeAnnouncementsDto record : records) { + if (record.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + Integer i = ret.get(record.getId()); + if (i != null) { + record.setUserCount(Long.valueOf(i)); + } + } + + } + + } + + /** + * 填充公告类别信息 + * 该方法根据提供的公告类别DTO记录列表,补充每个记录的类别名称列表信息 + * 它首先获取所有公告类别的信息,然后建立类别ID到类别的映射, + * 以便快速查找每个记录所属类别的详细信息,并处理类别的层级关系 + * + * @param records 公告类别DTO记录列表,泛型限定为AnnouncementsCategoryDto的子类 + */ + private void fillCategoryInfo(List records) { + + if (CollectionUtil.isEmpty(records)) { + return; + } + List allCate = categoriesService.listAll(); + if (CollectionUtil.isEmpty(allCate)) { + return; + } + Map cateMap = new HashMap(); + for (FtbNoticeCategories ftbNoticeCategories : allCate) { + cateMap.put(ftbNoticeCategories.getId(), ftbNoticeCategories); + } + for (AnnouncementsCategoryDto record : records) { + FtbNoticeCategories ftbNoticeCategories = cateMap.get(record.getCategoryId()); + if (null == ftbNoticeCategories) { + continue; + } + List categoryNameList = new ArrayList<>(); + if (StringUtils.isEmpty(ftbNoticeCategories.getPath())) { + categoryNameList.add(ftbNoticeCategories.getName()); + } else { + List allPreList = Arrays.asList(ftbNoticeCategories.getPath().split("-")); + for (String s : allPreList) { + FtbNoticeCategories parent = cateMap.get(s); + if (null != parent) { + categoryNameList.add(parent.getName()); + } + } + categoryNameList.add(ftbNoticeCategories.getName()); + } + record.setCategoryNameList(categoryNameList); + } + } + + /** + * 为公告记录添加未提醒用户数量 + *

+ * 此方法遍历给定的公告记录列表,对于每个已发布的公告, + * 查询并设置未提醒用户数量该方法旨在更新公告对象,使其包含 + * 未被提醒的用户数量信息 + * + * @param records 公告记录列表,不能为空 + */ + private void addNoAlertUserNum(List records) { + if (CollectionUtil.isEmpty(records)) { + return; + } + List adList = new ArrayList<>(); + for (FtbNoticeAnnouncementsDto record : records) { + if (record.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + adList.add(record.getId()); + record.setNoAlertUserCount(0L); + } + } + if (CollectionUtil.isEmpty(adList)) { + return; + } + List list = ftbNoticeAnnouncementsReceiveService.batchQueryNoAlertUserNum(adList); + Map ret = new HashMap<>(); + if (CollectionUtil.isNotEmpty(list)) { + for (BatchCommonCountDto batchCommonCountDto : list) { + ret.put(batchCommonCountDto.getSelectKey(), batchCommonCountDto.getNum()); + } + } + for (FtbNoticeAnnouncementsDto record : records) { + if (record.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + Integer i = ret.get(record.getId()); + if (i != null) { + record.setNoAlertUserCount(Long.valueOf(i)); + } + } + + } + } + + /** + * 查询用户信息 + * + * @param records + * @param type 1 创建人信息 2 发布人信息 + */ + + private void fillUserInfo(List records, Integer type) { + if (CollectionUtil.isEmpty(records)) { + return; + } + List userIds = new ArrayList<>(); + for (FtbNoticeAnnouncementsDto dto : records) { + if (type == 1) { + userIds.add(dto.getCreatorUserId()); + } else if (type == 2) { + userIds.add(dto.getPublishUserId()); + } + } + + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + for (FtbNoticeAnnouncementsDto dto : records) { + if (type == 1) { + UserBoundVO userPrimaryBoundOne = userPrimaryBoundBatch.get(dto.getCreatorUserId()); + if (userPrimaryBoundOne != null) { + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserId(userPrimaryBoundOne.getId()); + noticeUserDto.setUserName(userPrimaryBoundOne.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + noticeUserDto.setFlowerName(userPrimaryBoundOne.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = NoticeUtils.getUserOrgAndPositionDto(userPrimaryBoundOne); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + dto.setCreateUserInfo(noticeUserDto); + }else{ + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserName(dto.getCreateUserName()); + noticeUserDto.setUserId(dto.getCreatorUserId()); + dto.setCreateUserInfo(noticeUserDto); + } + } else if (type == 2) { + UserBoundVO userPrimaryBoundOne = userPrimaryBoundBatch.get(dto.getPublishUserId()); + if (userPrimaryBoundOne != null) { + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserId(userPrimaryBoundOne.getId()); + noticeUserDto.setUserName(userPrimaryBoundOne.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + noticeUserDto.setFlowerName(userPrimaryBoundOne.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = NoticeUtils.getUserOrgAndPositionDto(userPrimaryBoundOne); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + dto.setPublishUserInfo(noticeUserDto); + }else{ + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserName(dto.getPublicUserName()); + noticeUserDto.setUserId(dto.getPublishUserId()); + dto.setPublishUserInfo(noticeUserDto); + } + } + } + } + + /** + * 获取应用公告分页列表 + * + * @param req 查询应用公告列表的请求对象,包含查询条件和分页信息 + * @return 返回包含公告列表的分页信息的Dto(数据传输对象)对象 + */ + @Override + public PageInfo getAppPageList(AppQueryAnnouncementListReq req) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(req.getKeyWord().trim()); + } + InnerAppQueryAnnouncementListReq innerReq = BeanUtil.copyProperties(req, InnerAppQueryAnnouncementListReq.class); + if (StringUtils.isNotEmpty(req.getCategoryId())) { + List innerCate = getInnerCategory(innerReq.getCategoryId()); + innerReq.setInnerCategoryList(innerCate); + } + + String loginUserId = UserProvider.getLoginUserId(); + //只有超级管理员才查所有,其他查询自己 + if (userApiV2Util.hasAllPermission()) { + innerReq.setInnerPowerUserId(""); + } else { + innerReq.setInnerPowerUserId(loginUserId); + } + + Page queryPage = baseMapper.getAppPageList(Page.of(req.getCurrentPage(), req.getPageSize()), innerReq); + List records = queryPage.getRecords(); + addAcceptUserNum(records); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 新增公告 + * + * @param req + * @return + */ + + @Override + @Transactional + public FtbNoticeAnnouncements insertData(AddNoticeAnnouncementsReq req) { + //检测参数 + Date now = new Date(); + checkAnnouncementsParam(req); + NoticeEnums.SendSchedule sendSchedule = NoticeEnums.SendSchedule.fromCode(req.getIsScheduled()); + NoticeEnums.OptionButtonType opBtn = NoticeEnums.OptionButtonType.getByCode(req.getOptionBtn()); + + categoriesService.queryAndCheckById(req.getCategoryId()); + //添加公告 + String content = noticeUtils.replaceAllScript(req.getContent()); + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setCategoryId(req.getCategoryId()); + entity.setTitle(req.getTitle()); + entity.setShortContent(noticeUtils.clearHtmlTag(content, CONTENT_CUT_MAX_LENGTH)); + entity.setContent(content); + entity.setNeedRead(req.getNeedRead()); + entity.setIsScheduled(req.getIsScheduled()); + entity.setCreateUserName(UserProvider.getUser().getUserName()); + if (sendSchedule == NoticeEnums.SendSchedule.SCHEDULED) { + entity.setScheduledTime(req.getScheduledTime()); + } + if (opBtn == NoticeEnums.OptionButtonType.DRAFT) { + entity.setStatus(NoticeEnums.NoticeStatus.DRAFT.getCode());//草稿 + } else if (opBtn == NoticeEnums.OptionButtonType.PUBLISH) { + if (sendSchedule == NoticeEnums.SendSchedule.IMMEDIATELY) { + entity.setStatus(NoticeEnums.NoticeStatus.PUBLISHED.getCode());//已发布 + entity.setPublishUserId(UserProvider.getUser().getUserId()); + entity.setPublishTime(now); + } else { + entity.setStatus(NoticeEnums.NoticeStatus.PENDING.getCode()); //待发布 + } + } + + + entity.setReceiveUserType(req.getReceiveUserType()); + entity.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + NoticeEnums.NoticeStatus noticeStatus = NoticeEnums.NoticeStatus.fromCode(entity.getStatus()); + List userList = getAnnouncementUserList(req, noticeStatus); + entity.setUserCount(Long.valueOf(userList.size())); + String moduleId = userApiV2Util.getPermissionModuleId(); + entity.setModuleId(moduleId); + baseMapper.insert(entity); + //处理附件 + if (CollectionUtil.isNotEmpty(req.getFiles())) { + ftbNoticeFilesService.addFiles(entity.getId(), req.getFiles()); + } + //添加用户 + if (CollectionUtil.isNotEmpty(userList)) { + batchAddAnnouncementUser(entity.getId(), userList); + } + + //发送推送消息 + if (sendSchedule == NoticeEnums.SendSchedule.IMMEDIATELY) { + sendIM(userList, BeanUtil.copyProperties(entity, FtbNoticeAnnouncementsDto.class)); + } + + //如果是定时发送,将scheduledTime写入Redis,记录最大的发布时间 + if (sendSchedule == NoticeEnums.SendSchedule.SCHEDULED && req.getScheduledTime() != null) { + updateMaxScheduledTime(entity.getId(),req.getScheduledTime()); + } + + //记录日志 + FtbNoticeAnnouncements copy = null; + if (StringUtils.isNotEmpty(req.getCopyFrom())) { + copy = baseMapper.selectById(req.getCopyFrom()); + } + if (null != copy) { + asyncRecordAnnouncementLogs(entity.getId(), entity.getTitle(), NoticeEnums.NoticeOperationLogType.COPY); + } else { + asyncRecordAnnouncementLogs(entity.getId(), entity.getTitle(), NoticeEnums.NoticeOperationLogType.ADD); + } + + return entity; + + } + + /** + * 修改公告 + * + * @param id 公告id + * @param req + * @return + */ + @Override + @Transactional + public FtbNoticeAnnouncements updateData(String id, UpdateNoticeAnnouncementsReq req) { + //检测参数 + Date now = new Date(); + checkAnnouncementsParam(req); + //检测公告信息 + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(id); + NoticeEnums.NoticeStatus oldNoticeStaus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (oldNoticeStaus == NoticeEnums.NoticeStatus.PUBLISHED) { + throw new RuntimeException("公告已经发布不能修改"); + } else if (oldNoticeStaus == NoticeEnums.NoticeStatus.PENDING) { + Date scheduledTime = noticeAnnouncements.getScheduledTime(); + // 判断发布日期不能超过当前日期 + if (now.after(scheduledTime)) { + throw new RuntimeException("公告正在发布中,不能修改"); + } + } + categoriesService.queryAndCheckById(req.getCategoryId()); + + NoticeEnums.SendSchedule sendSchedule = NoticeEnums.SendSchedule.fromCode(req.getIsScheduled()); + NoticeEnums.OptionButtonType opBtn = NoticeEnums.OptionButtonType.getByCode(req.getOptionBtn()); + + //修改 + String content = noticeUtils.replaceAllScript(req.getContent()); + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setId(id); + entity.setCategoryId(req.getCategoryId()); + entity.setTitle(req.getTitle()); + entity.setShortContent(noticeUtils.clearHtmlTag(content, CONTENT_CUT_MAX_LENGTH)); + entity.setContent(content); + entity.setNeedRead(req.getNeedRead()); + entity.setIsScheduled(req.getIsScheduled()); + if (sendSchedule == NoticeEnums.SendSchedule.SCHEDULED) { + entity.setScheduledTime(req.getScheduledTime()); + } + if (opBtn == NoticeEnums.OptionButtonType.DRAFT) { + entity.setStatus(NoticeEnums.NoticeStatus.DRAFT.getCode());//草稿 + } else if (opBtn == NoticeEnums.OptionButtonType.PUBLISH) { + if (sendSchedule == NoticeEnums.SendSchedule.IMMEDIATELY) { + entity.setStatus(NoticeEnums.NoticeStatus.PUBLISHED.getCode());//已发布 + entity.setPublishUserId(UserProvider.getUser().getUserId()); + entity.setPublishTime(now); + } else { + entity.setStatus(NoticeEnums.NoticeStatus.PENDING.getCode()); //待发布 + } + } + + entity.setReceiveUserType(req.getReceiveUserType()); + entity.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + NoticeEnums.NoticeStatus noticeStatus = NoticeEnums.NoticeStatus.fromCode(entity.getStatus()); + List userList = getAnnouncementUserList(req, noticeStatus); + entity.setUserCount(Long.valueOf(userList.size())); + String moduleId = userApiV2Util.getPermissionModuleId(); + entity.setModuleId(moduleId); + baseMapper.updateById(entity); + //处理附件 + ftbNoticeFilesService.deleteFiles(id); + if (CollectionUtil.isNotEmpty(req.getFiles())) { + ftbNoticeFilesService.addFiles(entity.getId(), req.getFiles()); + } + //处理老的接收用户 + ftbNoticeAnnouncementsReceiveService.dealAnnouncementUserWhenEdit(entity.getId(), userList, noticeStatus); + + //添加用户 + if (CollectionUtil.isNotEmpty(userList)) { + batchAddAnnouncementUser(entity.getId(), userList); + } + //发送推送消息 + if (sendSchedule == NoticeEnums.SendSchedule.IMMEDIATELY) { + sendIM(userList, BeanUtil.copyProperties(entity, FtbNoticeAnnouncementsDto.class)); + } + + //如果是定时发送,将scheduledTime写入Redis,记录最大的发布时间 + if (sendSchedule == NoticeEnums.SendSchedule.SCHEDULED && req.getScheduledTime() != null) { + updateMaxScheduledTime(entity.getId(),req.getScheduledTime()); + } + + //记录日志 + asyncRecordAnnouncementLogs(entity.getId(), entity.getTitle(), NoticeEnums.NoticeOperationLogType.MODIFY); + return entity; + } + + /** + * 确定公告用户列表 + * + * @param req + * @param noticeStatus + * @return + */ + private List getAnnouncementUserList(AddNoticeAnnouncementsReq req, NoticeEnums.NoticeStatus noticeStatus) { + NoticeEnums.ReceiveUserTypeEnum receiveUserTypeEnum = NoticeEnums.ReceiveUserTypeEnum.getByCode(req.getReceiveUserType()); + List userList = new ArrayList<>(); + switch (noticeStatus) { + case DRAFT://草稿 + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.SPECIFIC) { + //保存指定用户 + userList = noticeUtils.uniqueStringList(req.getUserList()); + userList = filterInvalidUser(userList); + } + break; + case PENDING: //待发布 + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.SPECIFIC) { + //保存指定用户 + userList = noticeUtils.uniqueStringList(req.getUserList()); + userList = filterInvalidUser(userList); + } + break; + case PUBLISHED://已发布 + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.SPECIFIC) { + //保存指定用户 + userList = noticeUtils.uniqueStringList(req.getUserList()); + userList = filterInvalidUser(userList); + } else { + //保存全部用户 + userList = noticeUtils.queryAllUserId(); + } + break; + } + return userList; + } + + //过滤已经删除和离职用户 + private List filterInvalidUser(List userList) { + return noticeUtils.filterInvalidUser(userList); + } + + /** + * 公告参数校验 + * + * @param req + */ + private void checkAnnouncementsParam(AddNoticeAnnouncementsReq req) { + Date now = new Date(); + NoticeEnums.NeedReadStatus needReadStatus = NoticeEnums.NeedReadStatus.fromCode(req.getNeedRead()); + if (null == needReadStatus) { + throw new RuntimeException("必读状态参数错误"); + } + NoticeEnums.SendSchedule sendSchedule = NoticeEnums.SendSchedule.fromCode(req.getIsScheduled()); + if (null == sendSchedule) { + throw new RuntimeException("是否定时发送参数错误"); + } + if (sendSchedule == NoticeEnums.SendSchedule.SCHEDULED) { + if (null == req.getScheduledTime()) { + throw new RuntimeException("定时发送日期不能为空"); + } + // 判断过去日期是否超过当前日期 + if (req.getScheduledTime().before(now)) { + throw new RuntimeException("定时发送日期必须大于当前日期"); + } + } + NoticeEnums.ReceiveUserTypeEnum receiveUserTypeEnum = NoticeEnums.ReceiveUserTypeEnum.getByCode(req.getReceiveUserType()); + if (null == receiveUserTypeEnum) { + throw new RuntimeException("接收人员类型参数错误"); + } + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.SPECIFIC) { + req.setUserList(noticeUtils.uniqueStringList(req.getUserList())); + if (CollectionUtil.isEmpty(req.getUserList())) { + throw new RuntimeException("公告接收人员不能为空"); + } + } + NoticeEnums.OptionButtonType opBtn = NoticeEnums.OptionButtonType.getByCode(req.getOptionBtn()); + if (null == opBtn) { + throw new RuntimeException("操作按钮类型参数错误"); + } + } + + /** + * 查询并检测公告是否存在 + * + * @param id 公告id + * @return + */ + @Override + public FtbNoticeAnnouncements queryAndCheckById(String id) { + FtbNoticeAnnouncements entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("公告不存在"); + } + if (entity.getEnabledMark().equals(NoticeEnums.DeleteType.INVALID.getCode())) { + throw new RuntimeException("公告已经删除"); + } + return entity; + } + + /** + * 查询公告基本信息 + * + * @param id 公告id + * @param terminal 终端【web or app】 + * @return + */ + @Override + public FtbNoticeAnnouncementsDetailDto getInfo(String id, String terminal) { + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(id); + FtbNoticeAnnouncementsDetailDto dto = BeanUtil.copyProperties(noticeAnnouncements, FtbNoticeAnnouncementsDetailDto.class); + //查询分类名称 + if (StringUtils.isNotEmpty(noticeAnnouncements.getCategoryId())) { + FtbNoticeCategories category = categoriesService.getById(noticeAnnouncements.getCategoryId()); + if (null != category) { + dto.setCategoryName(category.getName()); + List categoryIdList = new ArrayList<>(); + if (StringUtils.isEmpty(category.getPath())) { + categoryIdList.add(category.getId()); + } else { + categoryIdList.addAll(Arrays.asList(category.getPath().split("-"))); + categoryIdList.add(category.getId()); + } + dto.setCategoryIdList(categoryIdList); + } + } + + //查询用户接收人,只有指定才有 + if (noticeAnnouncements.getReceiveUserType().equals(NoticeEnums.ReceiveUserTypeEnum.SPECIFIC.getCode())) { + List receiveDtoList = ftbNoticeAnnouncementsReceiveService.queryUserList(dto.getId()); + List userIdList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(receiveDtoList)) { + for (FtbNoticeAnnouncementsReceiveDto receiveDto : receiveDtoList) { + userIdList.add(receiveDto.getUserId()); + } + } + if(!noticeAnnouncements.getStatus().equals(3)){ + //过滤权限 + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if(innerPowerUserVO.getCode().equals(2)){ + userIdList = new ArrayList<>(); + } else if (innerPowerUserVO.getCode().equals(1)) { + userIdList =UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), userIdList); + } + } + if ("web".equals(terminal)) { + dto.setUserIdList(noticeUtils.queryEffectiveUserIds(userIdList, false)); + } else if ("app".equals(terminal)) { + dto.setUserList(noticeUtils.FillUserInfo(userIdList, false)); + } + } + //查询附件信息 + dto.setFilesList(ftbNoticeFilesService.getFiles(dto.getId())); + dto.setCreateUserInfo(noticeUtils.queryUserInfoForUserId(dto.getCreatorUserId())); + if (dto.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + if (dto.getCreatorUserId().equals(dto.getPublishUserId())) { + dto.setPublishUserInfo(dto.getCreateUserInfo()); + } else { + dto.setPublishUserInfo(noticeUtils.queryUserInfoForUserId(dto.getPublishUserId())); + } + dto.setNoReadCount(ftbNoticeAnnouncementsReceiveService.queryReadStatusCount(noticeAnnouncements.getId(), NoticeEnums.ReadStatus.UNREAD.getCode())); + dto.setReadCount(ftbNoticeAnnouncementsReceiveService.queryReadStatusCount(noticeAnnouncements.getId(), NoticeEnums.ReadStatus.READ.getCode())); + } else { + dto.setReadCount(0L); + dto.setNoReadCount(0L); + } + return dto; + } + + /** + * 发布公告 + * + * @param id 公告id + */ + @Override + @Transactional + public void publish(String id) { + Date now = new Date(); + //检测公告信息 + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(id); + NoticeEnums.NoticeStatus oldNoticeStaus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (oldNoticeStaus == NoticeEnums.NoticeStatus.PUBLISHED) { + throw new RuntimeException("公告已经发布,请不要重复操作"); + } + NoticeEnums.SendSchedule schedule = NoticeEnums.SendSchedule.fromCode(noticeAnnouncements.getIsScheduled()); + if (schedule == NoticeEnums.SendSchedule.SCHEDULED) { + //修改发布状态 + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setId(id); + entity.setPublishUserId(UserProvider.getUser().getUserId()); + entity.setPublicUserName(UserProvider.getUser().getUserName()); + entity.setStatus(NoticeEnums.NoticeStatus.PENDING.getCode()); //待发布 + baseMapper.updateById(entity); + return; + } + //获取用户ID列表 + NoticeEnums.ReceiveUserTypeEnum receiveUserTypeEnum = NoticeEnums.ReceiveUserTypeEnum.getByCode(noticeAnnouncements.getReceiveUserType()); + List userList = getAnnouncementUserListForEntity(noticeAnnouncements, receiveUserTypeEnum); + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.SPECIFIC) { + userList = noticeUtils.filterInvalidUser(userList); + //过滤权限 + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if(innerPowerUserVO.getCode().equals(2)){ + userList = new ArrayList<>(); + } else if (innerPowerUserVO.getCode().equals(1)) { + userList =UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), userList); + } + } + + //修改发布状态 + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setId(id); + entity.setPublishTime(now); + entity.setPublishUserId(UserProvider.getUser().getUserId()); + entity.setStatus(NoticeEnums.NoticeStatus.PUBLISHED.getCode()); //待发布 + entity.setPublicUserName(UserProvider.getUser().getUserName()); + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.ALL) { + entity.setUserCount(Long.valueOf(userList.size())); + } + baseMapper.updateById(entity); + //如果是全部用户 就需要添加到用户接收表中 + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.ALL) { + //添加到用户接收到表中 + ftbNoticeAnnouncementsReceiveService.cancelReceiveUser(noticeAnnouncements.getId());//先取消 + if (CollectionUtil.isNotEmpty(userList)) { + batchAddAnnouncementUser(entity.getId(), userList); + } + } + //发送推送消息 + sendIM(userList, BeanUtil.copyProperties(noticeAnnouncements, FtbNoticeAnnouncementsDto.class)); + + //记录日志 + asyncRecordAnnouncementLogs(noticeAnnouncements.getId(), noticeAnnouncements.getTitle(), NoticeEnums.NoticeOperationLogType.PUBLISH); + } + + /** + * 取消发布 + * + * @param id 公告id + */ + @Override + @Transactional + public void cancelPublish(String id) { + //检测公告信息 + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(id); + NoticeEnums.NoticeStatus oldNoticeStaus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (oldNoticeStaus != NoticeEnums.NoticeStatus.PUBLISHED) { + throw new RuntimeException("公告未发布,不需要撤销"); + } + + //修改发布状态 撤销 + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setId(id); + entity.setStatus(NoticeEnums.NoticeStatus.WITHDRAWN.getCode()); //待发布 + baseMapper.updateById(entity); + + batchResetReadAndAlertAndWebCancel(entity.getId()); + //记录日志 + asyncRecordAnnouncementLogs(noticeAnnouncements.getId(), noticeAnnouncements.getTitle(), NoticeEnums.NoticeOperationLogType.REVOKE); + } + + /** + * 根据通知公告和接收用户类型获取接收用户列表 + * + * @param noticeAnnouncements 通知公告对象,包含公告信息 + * @param receiveUserTypeEnum 用户接收类型枚举,确定公告发送给所有用户还是特定用户 + * @return 返回接收用户的ID列表 + */ + private List getAnnouncementUserListForEntity(FtbNoticeAnnouncements noticeAnnouncements, NoticeEnums.ReceiveUserTypeEnum receiveUserTypeEnum) { + + List userList = new ArrayList<>(); + switch (receiveUserTypeEnum) { + case ALL: + userList = noticeUtils.queryAllUserId(); + break; + case SPECIFIC: + List receiveDtoList = ftbNoticeAnnouncementsReceiveService.queryUserList(noticeAnnouncements.getId()); + if (CollectionUtil.isNotEmpty(receiveDtoList)) { + for (FtbNoticeAnnouncementsReceiveDto receiveDto : receiveDtoList) { + userList.add(receiveDto.getUserId()); + } + } + //过滤权限 + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if(innerPowerUserVO.getCode().equals(2)){ + userList = new ArrayList<>(); + } else if (innerPowerUserVO.getCode().equals(1)) { + userList =UserApiV2Util.getIntersection(innerPowerUserVO.getUserIds(), userList); + } + break; + } + return userList; + } + + /** + * 删除公告 + * + * @param id 公告id + */ + @Override + @Transactional + public void deleteData(String id) { + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(id); + NoticeEnums.NoticeStatus noticeStatus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (noticeStatus == NoticeEnums.NoticeStatus.PUBLISHED) { + throw new RuntimeException("公告已经发布不能删除"); + } + //删除公告 + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setId(id); + entity.setEnabledMark(NoticeEnums.DeleteType.INVALID.getCode()); + baseMapper.updateById(entity); + //删除公告接收人 + batchDeleteAnnouncementUser(entity.getId()); + //删除公告的附件 + batchDeleteAnnouncementFiles(entity.getId()); + //记录日志 + asyncRecordAnnouncementLogs(noticeAnnouncements.getId(), noticeAnnouncements.getTitle(), NoticeEnums.NoticeOperationLogType.DELETE); + } + + /** + * 提醒公告的一个用户 + * + * @param announcementId 公告id + * @param userId 用户id + */ + @Override + public void alertOne(String announcementId, String userId) { + + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(announcementId); + NoticeEnums.NoticeStatus oldNoticeStaus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (oldNoticeStaus != NoticeEnums.NoticeStatus.PUBLISHED) { + throw new RuntimeException("对不起,公告未发布"); + } + + FtbNoticeAnnouncementsReceive ftbNoticeAnnouncementsReceive = ftbNoticeAnnouncementsReceiveService.queryUserForUserId(announcementId, userId); + if (null == ftbNoticeAnnouncementsReceive) { + return; + } + if (ftbNoticeAnnouncementsReceive.getReadStatus().equals(NoticeEnums.ReadStatus.READ.getCode())) { + throw new RuntimeException("该用户已经阅读,无需提醒"); + } + if (ftbNoticeAnnouncementsReceive.getAlertStatus().equals(NoticeEnums.AlertStatus.NOTIFIED.getCode())) { + throw new RuntimeException("该用户已经提醒,无需重复操作"); + } + //修改提醒状态 + ftbNoticeAnnouncementsReceiveService.updateCompleteAlert(announcementId, ftbNoticeAnnouncementsReceive.getUserId()); + //发送消息 + FtbNoticeAnnouncementsDto ftbNoticeAnnouncementsDto = BeanUtil.copyProperties(noticeAnnouncements, FtbNoticeAnnouncementsDto.class); + ftbNoticeAnnouncementsDto.setInnerAlert(1); + sendIM(List.of(ftbNoticeAnnouncementsReceive.getUserId()), ftbNoticeAnnouncementsDto); + //记录日志 + asyncRecordAnnouncementLogs(noticeAnnouncements.getId(), noticeAnnouncements.getTitle(), NoticeEnums.NoticeOperationLogType.REMIND); + } + + /** + * 提醒公告相关的所有用户 + * + * @param announcementId 公告id + */ + @Override + public void alertAll(String announcementId) { + FtbNoticeAnnouncements noticeAnnouncements = queryAndCheckById(announcementId); + NoticeEnums.NoticeStatus oldNoticeStaus = NoticeEnums.NoticeStatus.fromCode(noticeAnnouncements.getStatus()); + if (oldNoticeStaus != NoticeEnums.NoticeStatus.PUBLISHED) { + throw new RuntimeException("对不起,公告未发布"); + } + List userList = new ArrayList<>(); + List receiveDtoList = ftbNoticeAnnouncementsReceiveService.queryNeedAlertUserList(announcementId); + if (CollectionUtil.isNotEmpty(receiveDtoList)) { + for (FtbNoticeAnnouncementsReceiveDto receiveDto : receiveDtoList) { + userList.add(receiveDto.getUserId()); + } + } + ftbNoticeAnnouncementsReceiveService.updateCompleteAlertAll(announcementId); + if (CollectionUtil.isNotEmpty(userList)) { + FtbNoticeAnnouncementsDto ftbNoticeAnnouncementsDto = BeanUtil.copyProperties(noticeAnnouncements, FtbNoticeAnnouncementsDto.class); + ftbNoticeAnnouncementsDto.setInnerAlert(1); + sendIM(userList, ftbNoticeAnnouncementsDto); + } + + //记录日志 + asyncRecordAnnouncementLogs(noticeAnnouncements.getId(), noticeAnnouncements.getTitle(), NoticeEnums.NoticeOperationLogType.REMIND); + } + + /** + * 定时检测定时发布的公告,到时间点就发布公告 + * + * @param tenantId 租户id + */ + @Override + @Transactional + public void checkPublishNotice(String tenantId) { + // 先从 Redis 获取最大的定时发布时间 + String redisKey = String.format(NoticeUtils.NOTICE_SCHEDULED_MAX_TIME_KEY, tenantId); + + List expiredIds = new ArrayList<>(); + Boolean hasKey = redisTemplate.hasKey(redisKey); + if (Boolean.TRUE.equals(hasKey)) { + // Redis key存在,查询缓存中所有数据 + Map entries = redisTemplate.opsForHash().entries(redisKey); + if (CollectionUtil.isEmpty(entries)) { + return; + } + long currentTime = System.currentTimeMillis(); + // 遍历所有数据,判断当前时间是否大于缓存中的时间 + for (Map.Entry entry : entries.entrySet()) { + String id = (String) entry.getKey(); + Long scheduledTime = (Long) entry.getValue(); + // 如果当前时间大于缓存中的时间,说明该公告应该发布 + if (currentTime >= scheduledTime) { + expiredIds.add(id); + } + } + if(CollectionUtil.isEmpty(expiredIds)){ + return; + } + // 删除缓存中对应的key + if (CollectionUtil.isNotEmpty(expiredIds)) { + for (String id : expiredIds) { + redisTemplate.opsForHash().delete(redisKey, id); + } + } + } + + Date now = new Date(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeAnnouncements::getIsScheduled, NoticeEnums.SendSchedule.SCHEDULED.getCode()) + .eq(FtbNoticeAnnouncements::getStatus, NoticeEnums.NoticeStatus.PENDING.getCode()) + .lt(FtbNoticeAnnouncements::getScheduledTime, now) + .eq(FtbNoticeAnnouncements::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return; + } + for (FtbNoticeAnnouncements announcements : list) { + //查询用户 + List userList = new ArrayList<>(); + NoticeEnums.ReceiveUserTypeEnum receiveUserTypeEnum = NoticeEnums.ReceiveUserTypeEnum.getByCode(announcements.getReceiveUserType()); + if (receiveUserTypeEnum == NoticeEnums.ReceiveUserTypeEnum.SPECIFIC) {//指定用户 + List ftbNoticeAnnouncementsReceiveDtos = ftbNoticeAnnouncementsReceiveService.queryUserList(announcements.getId()); + if (CollectionUtil.isNotEmpty(ftbNoticeAnnouncementsReceiveDtos)) { + for (FtbNoticeAnnouncementsReceiveDto ftbNoticeAnnouncementsReceiveDto : ftbNoticeAnnouncementsReceiveDtos) { + userList.add(ftbNoticeAnnouncementsReceiveDto.getUserId()); + } + } + userList = noticeUtils.filterInvalidUser(userList); + if (StringUtils.isNotEmpty(announcements.getModuleId())) { + //过滤权限 + List powerUserList = noticeUtils.queryPermissionForUserId(announcements.getCreatorUserId(), announcements.getModuleId(), tenantId); + if (CollectionUtil.isEmpty(powerUserList)) { + userList = new ArrayList<>(); + } else { + userList = UserApiV2Util.getIntersection(powerUserList, userList); + } + } + } else { + if (StringUtils.isNotEmpty(announcements.getModuleId())) { + try { + //查询全部用户 + userList = noticeUtils.queryPermissionForUserId(announcements.getCreatorUserId(), announcements.getModuleId(), tenantId); + //添加到用户接收到表中 + ftbNoticeAnnouncementsReceiveService.addReceiveUser(announcements.getId(), userList); + } catch (Exception e) { + log.error("查询用户权限范围内的用户失败", e); + } + } else { + //兼容老数据没有模块id的情况 + List allUserList = userApiV2Util.getAllUserList(tenantId); + if (CollectionUtil.isNotEmpty(allUserList)) { + userList = allUserList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + ftbNoticeAnnouncementsReceiveService.addReceiveUser(announcements.getId(), userList); + } + } + } + //修改公告为已经发布 + FtbNoticeAnnouncements entity = new FtbNoticeAnnouncements(); + entity.setId(announcements.getId()); + entity.setPublishTime(now); + String publishUserId = announcements.getLastModifyUserId(); + if (StringUtils.isEmpty(publishUserId)) { + publishUserId = announcements.getCreatorUserId(); + } + entity.setPublishUserId(publishUserId); + entity.setStatus(NoticeEnums.NoticeStatus.PUBLISHED.getCode()); + entity.setUserCount(Long.valueOf(userList.size())); + baseMapper.updateById(entity); + //发送推送消息 + sendIM(userList, BeanUtil.copyProperties(announcements, FtbNoticeAnnouncementsDto.class), tenantId); + } + + } + + /** + * 完成阅读 + * + * @param announcementId 公告ID + * @param userId 用户ID + */ + @Override + public void completeRead(String announcementId, String userId) { + ftbNoticeAnnouncementsReceiveService.updateCompleteRead(announcementId, userId); + } + + /** + * 分页查询当前用户的公告列表 + * + * @param req + * @return + */ + @Override + public PageInfo getMyWebPageList(QueryMyWebAnnouncementListReq req) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(req.getKeyWord().trim()); + } + InnerQueryMyWebAnnouncementListReq innerReq = BeanUtil.copyProperties(req, InnerQueryMyWebAnnouncementListReq.class); + String userId = UserProvider.getLoginUserId(); + innerReq.setUserId(userId); + Page queryPage = baseMapper.getMyWebPageList(Page.of(req.getCurrentPage(), req.getPageSize()), innerReq); + List records = queryPage.getRecords(); + fillCategoryInfo(records); + //发布人信息 + fillUserInfoForMyNoticeAnnouncementsDto(records, 2); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 填充用户信息 + * + * @param records + * @param type 1:创建人 2:发布人 + */ + private void fillUserInfoForMyNoticeAnnouncementsDto(List records, Integer type) { + if (CollectionUtil.isEmpty(records)) { + return; + } + List userIds = new ArrayList<>(); + for (MyNoticeAnnouncementsDto dto : records) { + if (type == 1) { + userIds.add(dto.getCreatorUserId()); + } else if (type == 2) { + userIds.add(dto.getPublishUserId()); + } + } + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + for (MyNoticeAnnouncementsDto dto : records) { + if (type == 1) { + UserBoundVO userPrimaryBoundOne = userPrimaryBoundBatch.get(dto.getCreatorUserId()); + if (userPrimaryBoundOne != null) { + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserId(userPrimaryBoundOne.getId()); + noticeUserDto.setUserName(userPrimaryBoundOne.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + noticeUserDto.setFlowerName(userPrimaryBoundOne.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = NoticeUtils.getUserOrgAndPositionDto(userPrimaryBoundOne); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + dto.setCreateUserInfo(noticeUserDto); + }else{ + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserName(dto.getCreateUserName()); + noticeUserDto.setUserId(dto.getCreatorUserId()); + dto.setCreateUserInfo(noticeUserDto); + } + } else if (type == 2) { + UserBoundVO userPrimaryBoundOne = userPrimaryBoundBatch.get(dto.getPublishUserId()); + if (userPrimaryBoundOne != null) { + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserId(userPrimaryBoundOne.getId()); + noticeUserDto.setUserName(userPrimaryBoundOne.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + noticeUserDto.setFlowerName(userPrimaryBoundOne.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = NoticeUtils.getUserOrgAndPositionDto(userPrimaryBoundOne); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + dto.setPublishUserInfo(noticeUserDto); + }else{ + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserName(dto.getPublicUserName()); + noticeUserDto.setUserId(dto.getPublishUserId()); + dto.setPublishUserInfo(noticeUserDto); + } + } + } + } + + /** + * 获取当前用户公告列表的分页信息 + * + * @param req 查询当前用户公告列表的请求参数,包括分页信息和查询条件等 + * @return 返回包含当前用户公告列表的分页信息对象,其中包含公告数据和分页元数据 + */ + @Override + public PageInfo getAppMyPageList(AppQueryMyAnnouncementListReq req) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(req.getKeyWord().trim()); + } + InnerAppQueryMyAnnouncementListReq innerReq = BeanUtil.copyProperties(req, InnerAppQueryMyAnnouncementListReq.class); + String userId = UserProvider.getLoginUserId(); + innerReq.setUserId(userId); + if (StringUtils.isNotEmpty(req.getCategoryId())) { + List innerCate = getInnerCategory(innerReq.getCategoryId()); + innerReq.setInnerCategoryList(innerCate); + } + Page queryPage = baseMapper.getAppMyPageList(Page.of(req.getCurrentPage(), req.getPageSize()), innerReq); + List records = queryPage.getRecords(); + + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 查询我的公告详细 + * + * @param announcementId 公告ID + * @param userId 用户ID + * @return + */ + @Override + public MyNoticeAnnouncementsDetailDto getMyAnnouncementInfo(String announcementId, String userId) { + FtbNoticeAnnouncements noticeAnnouncements = baseMapper.selectById(announcementId); + if (null == noticeAnnouncements) { + throw new RuntimeException("公告不存在"); + } + + //查询我的公告表 + FtbNoticeAnnouncementsReceive receive = ftbNoticeAnnouncementsReceiveService.queryUserForUserIdIngoreDelete(announcementId, userId); + if (null == receive) { + throw new RuntimeException("对不起,公告不存在"); + } + MyNoticeAnnouncementsDetailDto dto = BeanUtil.copyProperties(noticeAnnouncements, MyNoticeAnnouncementsDetailDto.class); + dto.setAlertStatus(receive.getAlertStatus()); + dto.setReadStatus(receive.getReadStatus()); + dto.setReadTime(receive.getReadTime()); + if (!noticeAnnouncements.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + dto.setIsCancel(1); // 0未撤销 1已撤销 + } else { + dto.setIsCancel(receive.getIsCancel()); + } + //查询分类名称 + if (StringUtils.isNotEmpty(noticeAnnouncements.getCategoryId())) { + FtbNoticeCategories category = categoriesService.getById(noticeAnnouncements.getCategoryId()); + if (null != category) { + dto.setCategoryName(category.getName()); + } + } + //查询附件信息 + dto.setFilesList(ftbNoticeFilesService.getFiles(dto.getId())); + + dto.setNoReadCount(ftbNoticeAnnouncementsReceiveService.queryReadStatusCount(announcementId, NoticeEnums.ReadStatus.UNREAD.getCode())); + dto.setReadCount(ftbNoticeAnnouncementsReceiveService.queryReadStatusCount(announcementId, NoticeEnums.ReadStatus.READ.getCode())); + dto.setCreateUserInfo(noticeUtils.queryUserInfoForUserId(dto.getCreatorUserId())); + if (dto.getStatus().equals(NoticeEnums.NoticeStatus.PUBLISHED.getCode())) { + if (dto.getCreatorUserId().equals(dto.getPublishUserId())) { + dto.setPublishUserInfo(dto.getCreateUserInfo()); + } else { + dto.setPublishUserInfo(noticeUtils.queryUserInfoForUserId(dto.getPublishUserId())); + } + } + return dto; + } + + /** + * 查询用户未读消息 + * + * @param userId 用户ID + * @param num 数量 + * @param needRead 0:全部 1:非必读,2:必读 + * @return + */ + @Override + public List queryAllMyNoRead(String userId, Integer num, Integer needRead) { + List list = baseMapper.queryAllMyNoRead(userId, num, needRead); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + /** + * web查询所有未读消息 + * + * @param userId 用户ID + * @param needRead 0:全部 1:非必读,2:必读 + * @return + */ + @Override + public List queryAllWebMyNoRead(String userId, Integer needRead) { + List list = baseMapper.queryAllWebMyNoRead(userId, needRead); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + + /** + * 查询用户最新的3条记录 + * + * @param userId 用户ID + * @param num 数量 + * @return + */ + @Override + public List queryMyLatest(String userId, Integer num) { + List list = baseMapper.getAppMyList(userId, num); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + /** + * 获取指定类别的内部子类别列表 + * 该方法首先将当前类别ID添加到内部类别列表中,然后查询并添加其所有子类别的ID + * + * @param categoryId 类别ID + * @return 包含该类别及其所有子类别ID的列表 + */ + private List getInnerCategory(String categoryId) { + List innerCategoryList = new ArrayList<>(); + FtbNoticeCategories currCate = categoriesService.getById(categoryId); + innerCategoryList.add(categoryId); + List categoryIds = categoriesService.queryChildCategoryIds(currCate); + if (CollectionUtil.isNotEmpty(categoryIds)) { + innerCategoryList.addAll(categoryIds); + } + return innerCategoryList; + } + + /** + * 发送即时消息 + * 此方法负责向指定的用户列表发送即时消息通知 + * 它会根据提供的类别ID设置通知的类别名称,如果类别ID有效,则异步发送即时消息 + * + * @param userList 用户列表,用于接收即时消息通知 + * @param dto 通知公告数据传输对象,包含发送即时消息所需的信息 + * @param tenantId 租户ID,用于区分不同租户的消息发送环境 + */ + private void sendIM(List userList, FtbNoticeAnnouncementsDto dto, String tenantId) { + FtbNoticeCategories categories = categoriesService.getById(dto.getCategoryId()); + if (null != categories) { + dto.setCategoryName(categories.getName()); + } else { + dto.setCategoryName(""); + } + noticeAsyncDealUtil.asyncSendIm(tenantId, userList, dto, noticeUtils.getHeadersForLogin()); + + } + + /** + * 异步IM通知提醒 + * + * @param userList + * @param dto + */ + private void sendIM(List userList, FtbNoticeAnnouncementsDto dto) { + String tenantId = noticeUtils.getTenantId(); + sendIM(userList, dto, tenantId); + } + + /** + * 删除公告接受用户 + * + * @param announcementId 公告id + */ + + private void batchDeleteAnnouncementUser(String announcementId) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.asyncDeleteAnnouncementMember(tenantId, announcementId, headers); + } + + /** + * 删除公告附件 + * + * @param announcementId 公告id + */ + private void batchDeleteAnnouncementFiles(String announcementId) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.asyncDeleteAnnouncementFiles(tenantId, announcementId, headers); + } + + /** + * 批量撤销接收人用户 + * + * @param announcementId + */ + private void batchResetReadAndAlertAndWebCancel(String announcementId) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.resetReadAndAlertAndWebCancel(tenantId, announcementId, headers); + } + + /** + * 批量添加接收人用户 + * + * @param announcementId + * @param userList + */ + private void batchAddAnnouncementUser(String announcementId, List userList) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.asyncAddAnnouncementMember(tenantId, announcementId, userList, headers); + } + + + /** + * 异步记录通知公告操作日志 + * + * @param announcementId + */ + private void asyncRecordAnnouncementLogs(String announcementId, String title, NoticeEnums.NoticeOperationLogType opType) { + UserInfo userInfo = UserProvider.getUser(); + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + FtbNoticeAnnouncementsLog log = new FtbNoticeAnnouncementsLog(); + log.setUserId(userInfo.getUserId()); + log.setUserName(userInfo.getUserName()); + log.setType(opType.getCode()); + log.setContent(opType.getDescription() + "公告:[" + title + "]"); + log.setBusinessId(announcementId); + log.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + noticeAsyncDealUtil.asyncRecordAnnouncementLogs(tenantId, log, headers); + } + + /** + * 更新Redis中最大的定时发布时间 + * + * @param scheduledTime 定时发布时间 + */ + private void updateMaxScheduledTime(String id,Date scheduledTime) { + + String redisKey = String.format(NoticeUtils.NOTICE_SCHEDULED_MAX_TIME_KEY, noticeUtils.getTenantId()); + // 将ID和时间写入Hash,key是id,value是时间戳 + redisTemplate.opsForHash().put(redisKey, id, scheduledTime.getTime()); + + } + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeCategoriesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeCategoriesServiceImpl.java new file mode 100644 index 0000000..617f108 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeCategoriesServiceImpl.java @@ -0,0 +1,467 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.net.URLDecoder; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.notice.domain.FtbNoticeAnnouncements; +import jnpf.model.notice.domain.FtbNoticeCategories; +import jnpf.model.notice.dto.AppNoticeCategoriesDto; +import jnpf.model.notice.dto.FtbNoticeCategoriesDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.category.AddCategoryReq; +import jnpf.model.notice.req.category.QueryCategoryListReq; +import jnpf.model.notice.req.category.QueryNextCategoryListReq; +import jnpf.model.notice.req.category.UpdateCategoryReq; +import jnpf.notice.mapper.FtbNoticeCategoriesMapper; +import jnpf.notice.service.FtbNoticeAnnouncementsService; +import jnpf.notice.service.FtbNoticeCategoriesService; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_categories(公告分类表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeCategoriesServiceImpl extends ServiceImpl + implements FtbNoticeCategoriesService { + + @Autowired + private FtbNoticeAnnouncementsService ftbNoticeAnnouncementsService; + + /** + * 查询所有 返回tree + * + * @param req + * @return + */ + @Override + public List listCategory(QueryCategoryListReq req) { + if (req.getType() == null) {//默认查询所有 + req.setType(0); + } + if (!req.getType().equals(0) && !req.getType().equals(1)) { + throw new RuntimeException("type参数错误"); + } + //type 0 查询所有分类(默认) 1 查询一级分类 + if(StringUtils.isEmpty(req.getKeyWord())) { + return searchNoKeyWord(req); + } + return searchHasKeyWord(req); + } + + private List searchHasKeyWord(QueryCategoryListReq req) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(req.getType().equals(1), FtbNoticeCategories::getParentId, "") + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .like(StringUtils.isNotEmpty(req.getKeyWord()), FtbNoticeCategories::getName, URLDecoder.decode(req.getKeyWord(), StandardCharsets.UTF_8)) + .orderByAsc(FtbNoticeCategories::getCreatorTime); + List categoriesList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(categoriesList)) { + return new ArrayList<>(); + } + Set cateIds = new HashSet<>(); + for (FtbNoticeCategories ftbNoticeCategories : categoriesList) { + cateIds.add(ftbNoticeCategories.getId()); + if(StringUtils.isNotEmpty(ftbNoticeCategories.getPath())){ + String[] split = ftbNoticeCategories.getPath().split(","); + cateIds.addAll(Arrays.asList(split)); + } + } + List categoriesListAll = baseMapper.selectBatchIds(cateIds); + if (req.getType().equals(1)) { + //一级分类不需要够级分类树 + return BeanUtil.copyToList(categoriesListAll, FtbNoticeCategoriesDto.class); + } else { + return buildCateTree(categoriesListAll); + } + } + + private List searchNoKeyWord(QueryCategoryListReq req) { + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(req.getType().equals(1), FtbNoticeCategories::getParentId, "") + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .orderByAsc(FtbNoticeCategories::getCreatorTime); + List categoriesList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(categoriesList)) { + return new ArrayList<>(); + } + + if (req.getType().equals(1)) { + //一级分类不需要够级分类树 + return BeanUtil.copyToList(categoriesList, FtbNoticeCategoriesDto.class); + } else { + return buildCateTree(categoriesList); + } + } + + /** + * 查询下级分类 + * + * @param req + * @return + */ + @Override + public List nextCatelists(QueryNextCategoryListReq req) { + List categoriesList = queryChildCategory(req.getId()); + if (CollectionUtil.isEmpty(categoriesList)) { + return Collections.emptyList(); + } + List ftbNoticeCategoriesDtos = BeanUtil.copyToList(categoriesList, FtbNoticeCategoriesDto.class); + for (FtbNoticeCategoriesDto ftbNoticeCategoriesDto : ftbNoticeCategoriesDtos) { + fillCategoryLevel(ftbNoticeCategoriesDto); + } + return ftbNoticeCategoriesDtos; + } + + /** + * 查询是否有子分类 + * + * @param list + * @return + */ + @Override + public List fillHasChild(List list) { + if (CollectionUtil.isEmpty(list)) { + return CollectionUtil.newArrayList(); + } + List appNoticeCategoriesList = BeanUtil.copyToList(list, AppNoticeCategoriesDto.class); + for (AppNoticeCategoriesDto dto : appNoticeCategoriesList) { + Long aLong = queryChildCategoryNum(dto.getId()); + if (aLong > 0) { + dto.setHasChild(true); + } else { + dto.setHasChild(false); + } + } + return appNoticeCategoriesList; + } + + /** + * 查询所有分类并返回分类树 + * + * @return + */ + @Override + public List listAll() { + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .orderByAsc(FtbNoticeCategories::getCreatorTime); + List categoriesList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(categoriesList)) { + return new ArrayList<>(); + } + return categoriesList; + } + + /** + * 添加分类 + * + * @param req + * @return + */ + @Override + public FtbNoticeCategories insertData(AddCategoryReq req) { + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeCategories::getName, req.getName()) + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分类名称已经存在"); + } + //查询检测父级分类是否存在 + FtbNoticeCategories parent = null; + if (StringUtils.isNotEmpty(req.getParentId())) { + parent = queryAndCheckParent(req.getParentId()); + } + + //添加分类 + FtbNoticeCategories categories = new FtbNoticeCategories(); + categories.setName(req.getName()); + categories.setParentId(req.getParentId()); + String path = ""; + if (null != parent) { + //检查是否超过5级 + if (StringUtils.isNotEmpty(parent.getPath())) { + if (parent.getPath().split("-").length > 5) { + throw new RuntimeException("分类最多支持5级"); + } + } + + if (StringUtils.isNotEmpty(parent.getPath())) { + path = parent.getPath() + "-" + parent.getId(); + } else { + path = parent.getId(); + } + } + categories.setPath(path); + categories.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + baseMapper.insert(categories); + return categories; + } + + /** + * 修改分类 + * + * @param id 分类id + * @param req + * @return + */ + @Override + public FtbNoticeCategories updateData(String id, UpdateCategoryReq req) { + //检测分类是否删除 + queryAndCheckById(id); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeCategories::getName, req.getName()) + .ne(FtbNoticeCategories::getId, id) + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分类名称已经存在"); + } + + FtbNoticeCategories parent = null; + if (StringUtils.isNotEmpty(req.getParentId())) { + parent = queryAndCheckParent(req.getParentId()); + } + + //修改分类 + FtbNoticeCategories categories = new FtbNoticeCategories(); + categories.setName(req.getName()); +// categories.setParentId(req.getParentId()); +// String path = ""; +// if (null != parent) { +// if (id.equals(parent.getId())) { +// throw new RuntimeException("对不起,自己不能是自己的父级分类"); +// } +// if (StringUtils.isNotEmpty(parent.getPath())) { +// path = parent.getPath() + "-" + parent.getId(); +// } else { +// path = parent.getId(); +// } +// } +// categories.setPath(path); + + categories.setId(id); + categories.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + baseMapper.updateById(categories); + return categories; + + } + + /** + * 查询并验证父分类是否存在且有效 + * 该方法用于查询给定ID的父分类,并确保该父分类存在且未被标记为删除 + * 如果父分类不存在或已被标记为删除,则抛出运行时异常 + * + * @param parentId 父分类的ID + * @return 返回查询到的父分类对象 + * @throws RuntimeException 如果父分类不存在或已被标记为删除,则抛出此异常 + */ + private FtbNoticeCategories queryAndCheckParent(String parentId) { + FtbNoticeCategories parent = baseMapper.selectById(parentId); + if (null == parent) { + throw new RuntimeException("父级分类不存在"); + } + if (parent.getEnabledMark().equals(NoticeEnums.DeleteType.INVALID.getCode())) { + throw new RuntimeException("父级分类已被删除"); + } + return parent; + } + + /** + * 查询分类并检查分类是否存在 + * + * @param id 分类id + * @return + */ + @Override + public FtbNoticeCategories queryAndCheckById(String id) { + FtbNoticeCategories entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("分类不存在"); + } + if (entity.getEnabledMark().equals(NoticeEnums.DeleteType.INVALID.getCode())) { + throw new RuntimeException("分类已经删除"); + } + return entity; + } + + /** + * 删除分类 + * + * @param id 分类id + */ + @Override + public void deleteData(String id) { + String loginUserId = UserProvider.getLoginUserId(); + List deleteIds = new ArrayList<>(); + deleteIds.add(id); + //检测分类是否删除 + FtbNoticeCategories currCategory = queryAndCheckById(id); + + //检测分类下是否有下级分类 + List childCategoryIds = queryChildCategoryIds(currCategory); + if (CollectionUtil.isNotEmpty(childCategoryIds)) { + deleteIds.addAll(childCategoryIds); + } + + //检测分类是否有公告 + QueryWrapper announcementsQueryWrapper = new QueryWrapper<>(); + announcementsQueryWrapper.lambda() + .select(FtbNoticeAnnouncements::getId, FtbNoticeAnnouncements::getTitle,FtbNoticeAnnouncements::getCreatorUserId) + .in(FtbNoticeAnnouncements::getCategoryId, deleteIds) + .eq(FtbNoticeAnnouncements::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List announcementsList = ftbNoticeAnnouncementsService.list(announcementsQueryWrapper); + if (CollectionUtil.isNotEmpty(announcementsList)) { + boolean myIsHas = false; + for (FtbNoticeAnnouncements ftbNoticeAnnouncements : announcementsList) { + if (ftbNoticeAnnouncements.getCreatorUserId().equals(loginUserId)) { + myIsHas = true; + } + } + if(myIsHas) { + throw new RuntimeException("分类下存在公告,请先删除公告"); + }else{ + throw new RuntimeException("分类下存在其他员工建立的公告,请先删除公告"); + } + } + + //删除分类 + List updateList = new ArrayList<>(); + for (String deleteId : deleteIds) { + FtbNoticeCategories categories = new FtbNoticeCategories(); + categories.setId(deleteId); + categories.setEnabledMark(NoticeEnums.DeleteType.INVALID.getCode()); + updateList.add(categories); + } + + updateBatchById(updateList); + } + + /** + * 查询子分类 + * + * @param parentId 父分类id + * @return + */ + @Override + public List queryChildCategory(String parentId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeCategories::getParentId, parentId) + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + return baseMapper.selectList(wrapper); + } + + /** + * 查询子分类数量 + * + * @param parentId 分类id + * @return + */ + @Override + public Long queryChildCategoryNum(String parentId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeCategories::getParentId, parentId) + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + return baseMapper.selectCount(wrapper); + } + + /** + * 查询子分类id + * + * @param currCategory + * @return + */ + @Override + public List queryChildCategoryIds(FtbNoticeCategories currCategory) { + String path = ""; + if (currCategory == null) { + return new ArrayList<>(); + } + if (StringUtils.isNotEmpty(currCategory.getPath())) { + path = currCategory.getPath() + "-" + currCategory.getId(); + } else { + path = currCategory.getId(); + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .likeRight(FtbNoticeCategories::getPath, path) + .eq(FtbNoticeCategories::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List categoriesList = baseMapper.selectList(wrapper); + List childIds = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(categoriesList)) { + for (FtbNoticeCategories ftbNoticeCategories : categoriesList) { + childIds.add(ftbNoticeCategories.getId()); + } + } + return childIds; + } + + /** + * 构建分类树 + * + * @param categoriesList + * @return + */ + private List buildCateTree(List categoriesList) { + List categoriesDtoList = BeanUtil.copyToList(categoriesList, FtbNoticeCategoriesDto.class); + List retList = new ArrayList<>(); + Map> map = new HashMap<>(); + for (FtbNoticeCategoriesDto dto : categoriesDtoList) { + fillCategoryLevel(dto); + if (StringUtils.isEmpty(dto.getParentId())) { + retList.add(dto); + } else { + List tempList = map.get(dto.getParentId()); + if (CollectionUtil.isEmpty(tempList)) { + tempList = new ArrayList<>(); + } + tempList.add(dto); + map.put(dto.getParentId(), tempList); + } + } + + for (FtbNoticeCategoriesDto dto : categoriesDtoList) { + List child = map.get(dto.getId()); + if (CollectionUtil.isNotEmpty(child)) { + dto.setChildren(child); + } + } + return retList; + } + + private void fillCategoryLevel(FtbNoticeCategoriesDto dto) { + if (StringUtils.isEmpty(dto.getPath())) { + dto.setLevel(0); + return; + } + String[] split = dto.getPath().split("-"); + dto.setLevel(split.length - 1); + } + + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeFilesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeFilesServiceImpl.java new file mode 100644 index 0000000..7e8d5b7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeFilesServiceImpl.java @@ -0,0 +1,138 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.notice.domain.FtbNoticeFiles; +import jnpf.model.notice.dto.FtbNoticeFilesDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.NoticeFilesVo; +import jnpf.notice.mapper.FtbNoticeFilesMapper; +import jnpf.notice.service.FtbNoticeFilesService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_files(公告附件表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeFilesServiceImpl extends ServiceImpl + implements FtbNoticeFilesService { + + /** + * 添加附件 + * @param announcementId 公告id + * @param fileList 附件列表 + */ + @Override + public void addFiles(String announcementId, List fileList) { + if (StringUtils.isEmpty(announcementId) || CollectionUtil.isEmpty(fileList)) { + return; + } + Map oldMap = new HashMap<>(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeFiles::getBusinessId, announcementId); + List oldFiles = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(oldFiles)) { + for (FtbNoticeFiles file : oldFiles) { + StringBuilder sb = new StringBuilder(); + sb.append(file.getFileName()); + sb.append(file.getUrl()); + sb.append(file.getExtension()); + sb.append(file.getSize()); + sb.append(file.getPreUrl()); + String key = sb.toString(); + oldMap.put(key, file); + } + } + + + List addList = new ArrayList<>(); + List updateList = new ArrayList<>(); + + for (NoticeFilesVo item : fileList) { + FtbNoticeFiles file = new FtbNoticeFiles(); + file.setBusinessId(announcementId); + file.setFileName(item.getFileName()); + file.setUrl(item.getFileUrl()); + file.setSize(item.getFilesize()); + file.setExtension(item.getFileSuffix()); + file.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + file.setCreatorTime(new Date()); + file.setPreUrl(item.getPreUrl()); + + + StringBuilder sb = new StringBuilder(); + sb.append(file.getFileName()); + sb.append(file.getUrl()); + sb.append(file.getExtension()); + sb.append(file.getSize()); + sb.append(file.getPreUrl()); + String key = sb.toString(); + FtbNoticeFiles ftbNoticeFiles = oldMap.get(key); + if (null == ftbNoticeFiles) { + addList.add(file); + } else { + file.setId(ftbNoticeFiles.getId()); + updateList.add(file); + } + + } + if (CollectionUtil.isNotEmpty(updateList)) { + updateBatchById(updateList); + } + if (CollectionUtil.isNotEmpty(addList)) { + saveBatch(addList); + } + } + + /** + * 删除公告附件 + * @param announcementId 公告id + */ + @Override + public void deleteFiles(String announcementId) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbNoticeFiles::getBusinessId, announcementId); + updateWrapper.set(FtbNoticeFiles::getEnabledMark, NoticeEnums.DeleteType.INVALID.getCode()); + baseMapper.update(new FtbNoticeFiles(), updateWrapper); + } + + /** + * 获取公告附件列表 + * @param announcementId 公告id + * @return + */ + @Override + public List getFiles(String announcementId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeFiles::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .eq(FtbNoticeFiles::getBusinessId, announcementId).orderByAsc(FtbNoticeFiles::getCreatorTime); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + List retList = new ArrayList<>(); + for (FtbNoticeFiles ftbNoticeFiles : list) { + FtbNoticeFilesDto dto = new FtbNoticeFilesDto(); + dto.setId(ftbNoticeFiles.getId()); + dto.setFileName(ftbNoticeFiles.getFileName()); + dto.setFileUrl(ftbNoticeFiles.getUrl()); + dto.setFileSuffix(ftbNoticeFiles.getExtension()); + dto.setFilesize(ftbNoticeFiles.getSize()); + dto.setPreUrl(ftbNoticeFiles.getPreUrl()); + retList.add(dto); + } + return retList; + } +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeManagerServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeManagerServiceImpl.java new file mode 100644 index 0000000..8d7b52c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeManagerServiceImpl.java @@ -0,0 +1,154 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.model.notice.domain.FtbNoticeManager; +import jnpf.model.notice.dto.FtbNoticeManagerDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.manager.AddNoticeManagerReq; +import jnpf.model.notice.req.manager.QueryNoticeManagerReq; +import jnpf.notice.mapper.FtbNoticeManagerMapper; +import jnpf.notice.service.FtbNoticeManagerService; +import jnpf.notice.utils.NoticeUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_manager(公告管理员表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeManagerServiceImpl extends ServiceImpl + implements FtbNoticeManagerService { + + @Autowired + private NoticeUtils noticeUtils; + + /** + * 分页查询公告管理员列表 + * + * @param req 查询条件对象,包含查询公告所需的信息 + * @return + */ + @Override + public PageInfo getPageLists(QueryNoticeManagerReq req) { + Page queryPage = baseMapper.pagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + noticeUtils.fillFlowerAndHeadLog(records); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 插入公告管理员 + * + * @param req 插入公告请求对象,包含需要插入的公告信息 + */ + @Override + @Transactional + public void insertData(AddNoticeManagerReq req) { + List userList = noticeUtils.uniqueStringList(req.getUserList()); + if (CollectionUtil.isEmpty(userList)) { + throw new RuntimeException("请选择管理员"); + } + + List realAddUserList = new ArrayList<>(); + + //查询已经是的管理员 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .in(FtbNoticeManager::getUserId, userList) + .eq(FtbNoticeManager::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List oldList = baseMapper.selectList(queryWrapper); + if (CollectionUtil.isNotEmpty(oldList)) { + List exiestUser = oldList.stream() + .map(FtbNoticeManager::getUserId) + .collect(Collectors.toList()); + for (String userId : userList) { + if (!exiestUser.contains(userId)) { + realAddUserList.add(userId); + } + } + } else { + realAddUserList = userList; + } + if (CollectionUtil.isEmpty(realAddUserList)) { + //选择的用户已经全部是管理员 无需操作 返回 + throw new RuntimeException("添加的用户已经是管理员"); + } + //添加管理员 + List managerList = new ArrayList<>(); + for (String userId : realAddUserList) { + FtbNoticeManager manager = new FtbNoticeManager(); + manager.setUserId(userId); + manager.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + managerList.add(manager); + } + saveBatch(managerList); + } + + /** + * 查询并检查公告是否存在 + * + * @param id + * @return + */ + private FtbNoticeManager queryAndCheckById(String id) { + FtbNoticeManager entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("数据不存在"); + } + return entity; + } + + /** + * 删除公告 + * + * @param id 公告的唯一标识符,用于标识需要删除的公告 + */ + @Override + public void deleteData(String id) { + queryAndCheckById(id); + baseMapper.deleteById(id); + } + + /** + * 根据用户ID查询公告 + * + * @param userId 用户ID,用于查找该用户相关的公告 + * @return 返回与指定用户ID相关的公告管理器对象 + */ + @Override + public FtbNoticeManager queryByUserId(String userId) { + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeManager::getUserId, userId) + .eq(FtbNoticeManager::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + List managerList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(managerList)) { + return null; + } + return managerList.get(0); + } +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupMembersServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupMembersServiceImpl.java new file mode 100644 index 0000000..222b100 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupMembersServiceImpl.java @@ -0,0 +1,208 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.notice.domain.FtbNoticeUserGroupMembers; +import jnpf.model.notice.dto.FtbNoticeUserGroupMembersDto; +import jnpf.model.notice.dto.UserOrgAndPositionDto; +import jnpf.model.notice.req.group.ListGroupUserMemberReq; +import jnpf.notice.mapper.FtbNoticeUserGroupMembersMapper; +import jnpf.notice.service.FtbNoticeUserGroupMembersService; +import jnpf.notice.utils.NoticeUtils; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.UploaderUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_user_group_members(用户分组成员表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeUserGroupMembersServiceImpl extends ServiceImpl + implements FtbNoticeUserGroupMembersService { + + @Autowired + private NoticeUtils noticeUtils; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 分页获取用户组成员列表 内存分页要过滤已经不存在的用户 + * + * @param groupId 用户组ID,用于定位特定的用户组 + * @param req 包含分页信息和查询条件的请求对象 + * @return 返回分页的用户组成员列表 + */ + @Override + public PageInfo getPageList(List powerUserIds, String groupId, ListGroupUserMemberReq req) { + List returnList = new ArrayList<>(); + int currentPage = (req.getCurrentPage() != null && req.getCurrentPage() > 0) ? req.getCurrentPage() : 1; + int pageSize = (req.getPageSize() != null && req.getPageSize() > 0) ? req.getPageSize() : 20; + + Page queryPage = baseMapper.pagingQuery(Page.of(1, -1), groupId, powerUserIds); + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + List userIds = records.stream().map(FtbNoticeUserGroupMembersDto::getUserId).collect(Collectors.toList()); + Map map = userApiV2Util.getUserPrimaryBoundBatch(new ArrayList<>(userIds), null); + for (FtbNoticeUserGroupMembersDto noticeUserDto : records) { + UserBoundVO userBoundVO = map.get(noticeUserDto.getUserId()); + if (userBoundVO != null) { + noticeUserDto.setUserId(userBoundVO.getId()); + noticeUserDto.setUserName(userBoundVO.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + noticeUserDto.setFlowerName(userBoundVO.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = NoticeUtils.getUserOrgAndPositionDto(userBoundVO); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + returnList.add(noticeUserDto); + } + } + } + + + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setTotal(returnList.size()); + pageInfo.setPageSize(pageSize); + pageInfo.setPageNum(currentPage); + if (CollectionUtil.isEmpty(returnList)) { + pageInfo.setList(returnList); + } else { + List> batchList = UserApiV2Util.batchList(returnList, pageSize); + if (currentPage <= batchList.size()) { + pageInfo.setList(batchList.get(currentPage - 1)); + } else { + pageInfo.setList(new ArrayList<>()); + } + } + return pageInfo; + } + + + /** + * 获取用户组的所有成员信息 + * + * @param groupId 用户组ID,用于定位特定的用户组 + * @param selectFlowerAndHeadLogo 指示是否需要同时查询用户的鲜花数和头像的布尔值 + * @return 返回用户组的所有成员信息列表 + */ + @Override + public List getAllList(String groupId, Boolean selectFlowerAndHeadLogo) { + List returnList = new ArrayList<>(); + List powerUserIds = userApiV2Util.getPermissionUserIds(); + List list = baseMapper.queryAllList(groupId, powerUserIds); + if (CollectionUtil.isNotEmpty(list)) { + List userIds = list.stream().map(FtbNoticeUserGroupMembersDto::getUserId).collect(Collectors.toList()); + Map map = userApiV2Util.getUserPrimaryBoundBatch(new ArrayList<>(userIds), null); + for (FtbNoticeUserGroupMembersDto noticeUserDto : list) { + UserBoundVO userBoundVO = map.get(noticeUserDto.getUserId()); + if (userBoundVO != null) { + noticeUserDto.setUserId(userBoundVO.getId()); + noticeUserDto.setUserName(userBoundVO.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + noticeUserDto.setFlowerName(userBoundVO.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = NoticeUtils.getUserOrgAndPositionDto(userBoundVO); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + returnList.add(noticeUserDto); + } + } + + } + return returnList; + } + + /** + * 批量添加用户到分组 + * + * @param groupId 分组ID + * @param userList 人员列表 + */ + @Override + public void batchAddGroupUser(String groupId, List userList) { + if (StringUtils.isEmpty(groupId) || CollectionUtil.isEmpty(userList)) { + return; + } + List list = new ArrayList<>(); + for (String userId : userList) { + FtbNoticeUserGroupMembers member = new FtbNoticeUserGroupMembers(); + member.setUserId(userId); + member.setGroupId(groupId); + list.add(member); + } + saveBatch(list); + } + + /** + * 批量删除用户从分组 + * + * @param groupId 分组ID + */ + @Override + public void batchDeleteGroupUser(String groupId) { + if (StringUtils.isEmpty(groupId)) { + return; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbNoticeUserGroupMembers::getGroupId, groupId); + remove(wrapper); + } + + /** + * 根据组ID列表获取所有列表 + * 该方法用于根据提供的组ID列表返回所有相关的列表 + * + * @param groupIds 组ID列表,用于识别和查询相关的列表 + * @return 返回一个字符串列表,包含所有与提供的组ID相关的项目 + */ + @Override + public List getAllListByGroupIds(List groupIds, List powerUserIds) { + List list = baseMapper.queryAllListForGroupIds(groupIds, powerUserIds); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + Set userIdSet = new HashSet<>(); + for (FtbNoticeUserGroupMembersDto ftbNoticeUserGroupMembersDto : list) { + userIdSet.add(ftbNoticeUserGroupMembersDto.getUserId()); + } + List userPrimaryBoundBatchReturnList = userApiV2Util.getUserPrimaryBoundBatchReturnList(new ArrayList<>(userIdSet), null); + if(CollectionUtil.isEmpty(userPrimaryBoundBatchReturnList)){ + return new ArrayList<>(); + } + return userPrimaryBoundBatchReturnList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + + } + + /** + * 批量查询用户组内用户的应用情况 + * + * @param groupIdList 组ID列表,用于识别和查询组内用户的应用情况 + * @return + */ + @Override + public List queryBatchGroupUserApp(List groupIdList, List powerUserIds) { + List list = baseMapper.queryAllListForGroupIds(groupIdList, powerUserIds); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + if (CollectionUtil.isNotEmpty(list)) { + //补充人员的花名和 头像 + noticeUtils.fillFlowerAndHeadLog(list); + } + return list; + } + +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupsServiceImpl.java new file mode 100644 index 0000000..fc9dc7a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/service/impl/FtbNoticeUserGroupsServiceImpl.java @@ -0,0 +1,338 @@ +package jnpf.notice.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.common.PageDto; +import jnpf.model.notice.domain.FtbNoticeUserGroups; +import jnpf.model.notice.dto.FtbNoticeUserGroupMembersDto; +import jnpf.model.notice.dto.FtbNoticeUserGroupsDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.group.AddUserGroupReq; +import jnpf.model.notice.req.group.ListGroupUserMemberReq; +import jnpf.model.notice.req.group.UpdateUserGroupReq; +import jnpf.notice.mapper.FtbNoticeUserGroupsMapper; +import jnpf.notice.service.FtbNoticeUserGroupMembersService; +import jnpf.notice.service.FtbNoticeUserGroupsService; +import jnpf.notice.utils.NoticeAsyncDealUtil; +import jnpf.notice.utils.NoticeUtils; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @author 许贵林 + * @description 针对表【ftb_notice_user_groups(用户分组表)】的数据库操作Service实现 + * @createDate 2024-05-08 09:42:09 + */ +@Service +public class FtbNoticeUserGroupsServiceImpl extends ServiceImpl + implements FtbNoticeUserGroupsService { + + @Autowired + private FtbNoticeUserGroupMembersService ftbNoticeUserGroupMembersService; + + @Autowired + private NoticeUtils noticeUtils; + + @Autowired + private NoticeAsyncDealUtil noticeAsyncDealUtil; + + @Autowired + private UserApiV2Util userApiV2Util; + + /** + * 查询所有分组信息 + * @return + */ + @Override + public List queryWebAllGroup() { + String loginUserId = UserProvider.getLoginUserId(); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeUserGroups::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()) + .eq(FtbNoticeUserGroups::getCreatorUserId, loginUserId) + .orderByAsc(FtbNoticeUserGroups::getCreatorTime); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + //查询离职人员 + List powerUserIds = userApiV2Util.getPermissionUserIds(); + List dtoList = BeanUtil.copyToList(list, FtbNoticeUserGroupsDto.class); + //循环查询每个分组的人员数量和第一个用户 + ListGroupUserMemberReq req = new ListGroupUserMemberReq(); + req.setPageSize(1); + req.setCurrentPage(1); + req.setSelectFlowerAndHeadLogo(true); + for (FtbNoticeUserGroupsDto dto : dtoList) { + PageInfo page = pageSelectGroupUser(powerUserIds, dto.getId(), req); + dto.setUserCount(page.getTotal()); + dto.setUserList(page.getList()); + } + return dtoList; + } + + /** + * 分页查询分组信息 + * @param req 分页查询请求对象 + * @return + */ + @Override + public PageInfo queryAppPage(PageDto req) { + String loginUserId = UserProvider.getLoginUserId(); + Page queryPage = baseMapper.pagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()),loginUserId); + List records = queryPage.getRecords(); + + if (CollectionUtil.isNotEmpty(records)) { + //查询离职人员 + ListGroupUserMemberReq memberReq = new ListGroupUserMemberReq(); + memberReq.setPageSize(5); + memberReq.setCurrentPage(1); + memberReq.setSelectFlowerAndHeadLogo(true); + List powerUserIds = userApiV2Util.getPermissionUserIds(); + for (FtbNoticeUserGroupsDto dto : records) { + PageInfo page = pageSelectGroupUser(powerUserIds, dto.getId(), memberReq); + dto.setUserCount(page.getTotal()); + dto.setUserList(page.getList()); + } + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 批量查询用户组人员信息 + * @param groupIds 用户组的标识列表 + * @return + */ + @Override + public List queryBatchGroupUser(String groupIds) { + if (StringUtils.isEmpty(groupIds)) { + return new ArrayList<>(); + } + // 将数组转换为 List + List groupIdList = Arrays.asList(groupIds.split(",")); + if (CollectionUtil.isEmpty(groupIdList)) { + return new ArrayList<>(); + } + List powerUserIds = userApiV2Util.getPermissionUserIds(); + return ftbNoticeUserGroupMembersService.getAllListByGroupIds(groupIdList, powerUserIds); + + } + + /** + * 批量查询用户组人员信息 + * @param groupIds 用户组的标识列表 + * @return + */ + @Override + public List queryBatchGroupUserApp(String groupIds) { + if (StringUtils.isEmpty(groupIds)) { + return new ArrayList<>(); + } + // 将数组转换为 List + List groupIdList = Arrays.asList(groupIds.split(",")); + if (CollectionUtil.isEmpty(groupIdList)) { + return new ArrayList<>(); + } + List powerUserIds = userApiV2Util.getPermissionUserIds(); + return ftbNoticeUserGroupMembersService.queryBatchGroupUserApp(groupIdList, powerUserIds); + } + + public PageInfo pageSelectGroupUser(List powerUserIds, String groupId, ListGroupUserMemberReq req) { + return ftbNoticeUserGroupMembersService.getPageList(powerUserIds, groupId, req); + } + + /** + * 新增用户组 + * @param req 包含要插入用户组信息的请求对象 + * @return + */ + @Override + @Transactional + public String insertData(AddUserGroupReq req) { + String loginUserId = UserProvider.getLoginUserId(); + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeUserGroups::getGroupName, req.getGroupName()) + .eq(FtbNoticeUserGroups::getCreatorUserId, loginUserId) + .eq(FtbNoticeUserGroups::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分组名称已经存在"); + } + + + if (CollectionUtil.isEmpty(req.getUserList())) { + throw new RuntimeException("分组人员不能为空"); + } + + + //添加分组 + FtbNoticeUserGroups entity = new FtbNoticeUserGroups(); + entity.setGroupName(req.getGroupName()); + entity.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + entity.setCreatorUserId(loginUserId); + baseMapper.insert(entity); + + //异步添加用户到分组成员列表 + batchAddGroupUser(entity.getId(), noticeUtils.uniqueStringList(req.getUserList())); + + return entity.getId(); + } + + /** + * 修改用户组信息 + * @param id 用户组的唯一标识 + * @param req 包含要更新用户组信息的请求对象 + */ + @Override + @Transactional + public void updateData(String id, UpdateUserGroupReq req) { + String loginUserId = UserProvider.getLoginUserId(); + //检测是否删除 + FtbNoticeUserGroups ftbNoticeUserGroups = queryAndCheckById(id); + if(!ftbNoticeUserGroups.getCreatorUserId().equals(loginUserId)){ + throw new RuntimeException("您没有权限修改此分组信息"); + } + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbNoticeUserGroups::getGroupName, req.getGroupName()) + .ne(FtbNoticeUserGroups::getId, id) + .eq(FtbNoticeUserGroups::getCreatorUserId, loginUserId) + .eq(FtbNoticeUserGroups::getEnabledMark, NoticeEnums.DeleteType.VALID.getCode()); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("分组名称已经存在"); + } + + if (CollectionUtil.isEmpty(req.getUserList())) { + throw new RuntimeException("分组人员不能为空"); + } + + + //修改 + FtbNoticeUserGroups entity = new FtbNoticeUserGroups(); + entity.setId(id); + entity.setEnabledMark(NoticeEnums.DeleteType.VALID.getCode()); + entity.setGroupName(req.getGroupName()); + baseMapper.updateById(entity); + //异步调整分组人员 + batchUpdateGroupUser(entity.getId(), noticeUtils.uniqueStringList(req.getUserList())); + + + } + + /** + * 查询用户组信息 + * @param id 用户组的唯一标识 + * @return + */ + @Override + public FtbNoticeUserGroupsDto getInfo(String id) { + //检测是否删除 + FtbNoticeUserGroups oldGroup = queryAndCheckById(id); + FtbNoticeUserGroupsDto ftbNoticeUserGroupsDto = BeanUtil.copyProperties(oldGroup, FtbNoticeUserGroupsDto.class); + + //查询分组的全部人员 + List userList = ftbNoticeUserGroupMembersService.getAllList(id, true); + ftbNoticeUserGroupsDto.setUserList(userList); + ftbNoticeUserGroupsDto.setUserCount((long) userList.size()); + return ftbNoticeUserGroupsDto; + } + + /** + * 删除用户组 + * @param id 用户组的唯一标识 + */ + @Override + @Transactional + public void deleteData(String id) { + String loginUserId = UserProvider.getLoginUserId(); + //检测是否删除 + FtbNoticeUserGroups ftbNoticeUserGroups = queryAndCheckById(id); + if(!ftbNoticeUserGroups.getCreatorUserId().equals(loginUserId)){ + throw new RuntimeException("您没有权限删除此分组信息"); + } + + //删除分组 + FtbNoticeUserGroups entity = new FtbNoticeUserGroups(); + entity.setId(id); + entity.setEnabledMark(NoticeEnums.DeleteType.INVALID.getCode()); + baseMapper.updateById(entity); + + //删除分组人员 + batchDeleteGroupUser(id); + } + + /** + * 查询并检查用户组是否存在 + * @param id + * @return + */ + private FtbNoticeUserGroups queryAndCheckById(String id) { + FtbNoticeUserGroups entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("分组不存在"); + } + if (entity.getEnabledMark().equals(NoticeEnums.DeleteType.INVALID.getCode())) { + throw new RuntimeException("分组已经删除"); + } + return entity; + } + + /** + * 异步添加用户到分组成员列表 + * @param groupId 分组唯一标识 + * @param userList 用户列表集合 + */ + private void batchAddGroupUser(String groupId, List userList) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.asyncAddUserGroupMember(tenantId, groupId, userList, headers); + } + + /** + * 异步修改分组人员 + * @param groupId 分组唯一标识 + * @param userList 用户集合 + */ + private void batchUpdateGroupUser(String groupId, List userList) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.asyncUpdateUserGroupMember(tenantId, groupId, userList, headers); + } + + /** + * 异步删除分组人员 + * @param groupId 分组唯一标识 + */ + private void batchDeleteGroupUser(String groupId) { + String tenantId = noticeUtils.getTenantId(); + Map headers = noticeUtils.getHeadersForLogin(); + noticeAsyncDealUtil.asyncDeleteUserGroupMember(tenantId, groupId, headers); + } +} + + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeAsyncDealUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeAsyncDealUtil.java new file mode 100644 index 0000000..5222e5e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeAsyncDealUtil.java @@ -0,0 +1,229 @@ +package jnpf.notice.utils; + +import cn.hutool.json.JSONUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.notice.domain.FtbNoticeAnnouncementsLog; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDto; +import jnpf.notice.service.FtbNoticeAnnouncementsLogService; +import jnpf.notice.service.FtbNoticeAnnouncementsReceiveService; +import jnpf.notice.service.FtbNoticeFilesService; +import jnpf.notice.service.FtbNoticeUserGroupMembersService; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class NoticeAsyncDealUtil { + @Autowired + @Lazy + private FtbNoticeUserGroupMembersService ftbNoticeUserGroupMembersService; + + @Lazy + @Autowired + private FtbNoticeAnnouncementsReceiveService ftbNoticeAnnouncementsReceiveService; + + @Lazy + @Autowired + private FtbNoticeFilesService ftbNoticeFilesService; + + @Lazy + @Autowired + private FtbNoticeAnnouncementsLogService ftbNoticeAnnouncementsLogService; + + @Lazy + @Autowired + private NoticeIMUtils noticeIMUtils; + + /** + * 异步添加用户分组成员数据 + * + * @param tenantCode 租户 + * @param groupId 分组id + * @param userList 人员列表 + */ + @Async + public void asyncAddUserGroupMember(String tenantCode, String groupId, List userList, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeUserGroupMembersService.batchAddGroupUser(groupId, userList); + } catch (Exception e) { + log.error("异步添加用户分组成员异常:tenantCode={},groupId={},data={}", tenantCode, groupId, JSONUtil.toJsonStr(userList)); + } finally { + FeignHolder.clear(); + } + } + + /** + * 异步修改用户分组成员数据 + * + * @param tenantCode 租户 + * @param groupId 分组ID + * @param userList 人员列表 + */ + @Async + public void asyncUpdateUserGroupMember(String tenantCode, String groupId, List userList, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeUserGroupMembersService.batchDeleteGroupUser(groupId); + ftbNoticeUserGroupMembersService.batchAddGroupUser(groupId, userList); + } catch (Exception e) { + log.error("异步添加用户分组成员异常:tenantCode={},groupId={},data={}", tenantCode, groupId, JSONUtil.toJsonStr(userList)); + } finally { + FeignHolder.clear(); + } + } + + + /** + * 给通知公告异步添加接收用户 + * + * @param tenantCode 租户 + * @param announcementId 公告ID + * @param userList 接收用户 + * @param headers + */ + @Async + public void asyncAddAnnouncementMember(String tenantCode, String announcementId, List userList, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeAnnouncementsReceiveService.addReceiveUser(announcementId, userList); + } catch (Exception e) { + log.error("异步添加公告接收人异常:tenantCode={},announcementId={},data={}", tenantCode, announcementId, JSONUtil.toJsonStr(userList)); + } finally { + FeignHolder.clear(); + } + } + + /** + * 撤销公告用户接收用户 + * + * @param tenantCode 租户 + * @param announcementId 公告ID + * @param headers + */ + @Async + public void resetReadAndAlertAndWebCancel(String tenantCode, String announcementId, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeAnnouncementsReceiveService.resetReadAndAlertAndWebCancel(announcementId); + } catch (Exception e) { + log.error("异步撤销公告接收人异常:tenantCode={},announcementId={},data={}", tenantCode, announcementId); + } finally { + FeignHolder.clear(); + } + } + + /** + * 异步删除公告接收用户 + * + * @param tenantCode 租户 + * @param announcementId 公告ID + * @param headers + */ + @Async + public void asyncDeleteAnnouncementMember(String tenantCode, String announcementId, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeAnnouncementsReceiveService.deleteReceiveUser(announcementId); + } catch (Exception e) { + log.error("异步删除公告接收人异常:tenantCode={},announcementId={},data={}", tenantCode, announcementId); + } finally { + FeignHolder.clear(); + } + } + + /** + * 异步删除公告附件 + * + * @param tenantCode 租户 + * @param announcementId 公告ID + * @param headers + */ + @Async + public void asyncDeleteAnnouncementFiles(String tenantCode, String announcementId, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeFilesService.deleteFiles(announcementId); + } catch (Exception e) { + log.error("异步删除公告附件异常:tenantCode={},announcementId={},data={}", tenantCode, announcementId); + } finally { + FeignHolder.clear(); + } + } + + + /** + * 异步记录通知公告操作日志 + * + * @param tenantCode 租户 + * @param recordLog 操作日志 + * @param headers + */ + @Async + public void asyncRecordAnnouncementLogs(String tenantCode, FtbNoticeAnnouncementsLog recordLog, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeAnnouncementsLogService.recordLog(recordLog); + } catch (Exception e) { + log.error("异步添加通知公告日志失败:tenantCode={},data={}", tenantCode, JSONUtil.toJsonStr(recordLog)); + } finally { + FeignHolder.clear(); + } + } + + /** + * 发送IM + * + * @param tenantCode 租户 + * @param userList 用户列表 + * @param dto 公告信息 + * @param headers + */ + @Async + public void asyncSendIm(String tenantCode, List userList, FtbNoticeAnnouncementsDto dto, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + noticeIMUtils.sendMsg(tenantCode, userList, dto); + } catch (Exception e) { + log.error("异步发送IM失败:tenantCode={},data={}", tenantCode, JSONUtil.toJsonStr(dto)); + } finally { + FeignHolder.clear(); + } + } + + /** + * 异步删除分组中的人员 + * + * @param tenantCode + * @param groupId + * @param headers + */ + @Async + public void asyncDeleteUserGroupMember(String tenantCode, String groupId, Map headers) { + + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbNoticeUserGroupMembersService.batchDeleteGroupUser(groupId); + } catch (Exception e) { + log.error("异步删除用户分组成员异常:tenantCode={},groupId={}", tenantCode, groupId); + } finally { + FeignHolder.clear(); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeIMUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeIMUtils.java new file mode 100644 index 0000000..c0ab359 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeIMUtils.java @@ -0,0 +1,104 @@ +package jnpf.notice.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDto; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.LinkedList; +import java.util.List; + +@Component +@Slf4j +public class NoticeIMUtils { + + @Autowired + private ImRobotApi imRobotApi; + private static final String NOTICE_APP_NAME = "通知公告"; + private static final String NOTICE_APP_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/66457c71e4b05b749193397b.png"; + private static final String NOTICE_BUTTON_NAME = "查看全文 >"; + private static final String NOTICE_BUTTON_LINK = "/pages/announcement/components/info?id=%s";//跳转页面链接 + + + /** + * 发送消息 + * + * @param tenantId 租户 + * @param toUserIds 接收人 + * @param dto 公告 + * @return + */ + public Boolean sendMsg(String tenantId, List toUserIds, FtbNoticeAnnouncementsDto dto) { + if (CollectionUtil.isEmpty(toUserIds)) { + return false; + } + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(NOTICE_APP_LOGO);//固定图片 + robotNoticeDataForm.setAppName(NOTICE_APP_NAME); + if(null==dto.getInnerAlert() || dto.getInnerAlert()==0) { + robotNoticeDataForm.setTitle(buildNoticeTitle(dto)); + robotNoticeDataForm.setContent(dto.getShortContent()); + }else{ + robotNoticeDataForm.setTitle("您有一条未读公告"); + robotNoticeDataForm.setContent(buildNoticeTitle(dto)); + } + robotNoticeDataForm.setExpand(buildExpand(dto)); + robotNoticeDataForm.setJumpUrlList(buildBtnUrl(dto.getId())); + form.setRobotNoticeDataForm(robotNoticeDataForm); + List> batches = Lists.partition(toUserIds, 450); + for (List batch : batches) { + form.setToUserIds(batch); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("通知公告im发送失败,send msg req={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + } + return true; + } + + private String buildNoticeTitle(FtbNoticeAnnouncementsDto dto) { + StringBuilder sb = new StringBuilder(); + if (StringUtils.isNotEmpty(dto.getCategoryName())) { + sb.append("【"); + sb.append(dto.getCategoryName()); + sb.append("】"); + } + sb.append(dto.getTitle()); + return sb.toString(); + } + + private String buildExpand(FtbNoticeAnnouncementsDto dto) { + JSONObject expand = new JSONObject(); + expand.put("announcementId", dto.getId()); + expand.put("needRead", dto.getNeedRead()); + return expand.toJSONString(); + } + + private LinkedList buildBtnUrl(String announcementId) { + LinkedList btn = new LinkedList<>(); + JumpUrlListModel urlModel = new JumpUrlListModel(); + urlModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + urlModel.setButtonName(NOTICE_BUTTON_NAME); + urlModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + urlModel.setType(1);//1-跳转链接 2-表单提交 + urlModel.setUrl(String.format(NOTICE_BUTTON_LINK, announcementId)); + urlModel.setMpId("__UNI__EA6FB1C"); + btn.add(urlModel); + return btn; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeUtils.java new file mode 100644 index 0000000..562ec2c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/notice/utils/NoticeUtils.java @@ -0,0 +1,448 @@ +package jnpf.notice.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HtmlUtil; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.ImmutableMap; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.UserInfo; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.model.cultivate.vo.common.InnerPowerUserVO; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsReceiveDto; +import jnpf.model.notice.dto.NoticeUserDto; +import jnpf.model.notice.dto.UserOrgAndPositionDto; +import jnpf.model.notice.enums.NoticeEnums; +import jnpf.model.notice.req.announcement.QueryUserListReq; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.util.Constants; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Component +public class NoticeUtils { + + + @Autowired + private UserApiV2Util userApiV2Util; + + @Autowired + private PermissionsUtils permissionsUtils; + + @Autowired + private UserApi userApi; + + public static final String NOTICE_UPDATE_KEY = "notice_upd_pub_alt:%s"; + public static final Integer NOTICE_UPDATE_MAX_TIME = 5;//最大执行时间,key过期 + + /** + * Redis key: 定时发布公告的最大发布时间 + */ + public static final String NOTICE_SCHEDULED_MAX_TIME_KEY = "notice:scheduled:notice_time:%s"; + + /** + * 补充用户信息 + * + * @param userIds 用户列表 + * @param filterOut 是否过滤离职员工 true 过滤 false 不过滤 + * @return + */ + public List FillUserInfo(List userIds, Boolean filterOut) { + if (CollectionUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + + if (userPrimaryBoundBatch.isEmpty()) { + return new ArrayList<>(); + } + List userDtoList = new ArrayList<>(); + for (Map.Entry entry : userPrimaryBoundBatch.entrySet()) { + UserBoundVO userBoundVO = entry.getValue(); + // 处理逻辑 + NoticeUserDto noticeUserDto = new NoticeUserDto(); + noticeUserDto.setUserId(userBoundVO.getId()); + noticeUserDto.setUserName(userBoundVO.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + noticeUserDto.setFlowerName(userBoundVO.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = getUserOrgAndPositionDto(userBoundVO); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + userDtoList.add(noticeUserDto); + } + return userDtoList; + } + + + public List queryEffectiveUserIds(List userIds, Boolean filterOut) { + if (CollectionUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + List userPrimaryBoundBatchReturnList = userApiV2Util.getUserPrimaryBoundBatchReturnList(userIds, null); + + List userIdList = new ArrayList<>(); + for (UserBoundVO roster : userPrimaryBoundBatchReturnList) { + userIdList.add(roster.getId()); + } + return userIdList; + } + + /** + * 查询所有的正常用户 + * + * @return 用户列表 + */ + public List queryAllUserId() { + InnerPowerUserVO innerPowerUserVO = userApiV2Util.getLoginManagerUserIdsForEncode(); + if (innerPowerUserVO.getCode().equals(0)) { + return queryAllValidUserId(); + } else if (innerPowerUserVO.getCode().equals(1)) { + return innerPowerUserVO.getUserIds(); + } else { + return new ArrayList<>(); + } + } + + public List queryAllValidUserId() { + List allUserList = userApiV2Util.getAllUserList(null); + if (CollectionUtil.isEmpty(allUserList)) { + return new ArrayList<>(); + } + List userIdList = new ArrayList<>(); + for (UserBoundVO userBoundVO : allUserList) { + userIdList.add(userBoundVO.getId()); + } + return userIdList; + } + + + /** + * 根据用户id查询权限(非登录用户) + * @param userId 用户id + * @param moduleId 模块id + * @return 权限列表 + */ + public List queryPermissionForUserId(String userId, String moduleId,String tenantId) { + //判断是否是超级管理员 + List userListNoData = userApi.getUserListNoData(List.of(userId), tenantId); + if(CollectionUtil.isEmpty(userListNoData)){ + return new ArrayList<>(); + } + //超级管理员自己获取 + if(userListNoData.get(0).getIsAdministrator() == 1){ + List allUserList = userApiV2Util.getAllUserList(tenantId); + if (CollectionUtil.isEmpty(allUserList)) { + return new ArrayList<>(); + } + List userIdList = new ArrayList<>(); + for (UserBoundVO userBoundVO : allUserList) { + userIdList.add(userBoundVO.getId()); + } + return userIdList; + } + return permissionsUtils.obtainPersonnelUserIdDataPermissions(userId, moduleId); + } + /** + * 查询用户列表 + * + * @param req 查询参数 + * @return 用户列表 + */ + + public PageInfo queryUserByPage(QueryUserListReq req) { + List allUserList = userApiV2Util.getAllUserList(null); + int pageNum = 1; + if (req.getCurrentPage() != null && req.getCurrentPage() > 0) { + pageNum = req.getCurrentPage(); + } + + int pageSize = 10; // 每页数量 + if (req.getPageSize() != null && req.getPageSize() > 0) { + pageSize = req.getPageSize(); + } + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, allUserList.size()); + + List records = allUserList.subList(fromIndex, toIndex); + //转换查询对象 + List list = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(records)) { + for (UserBoundVO record : records) { + FtbNoticeAnnouncementsReceiveDto dto = new FtbNoticeAnnouncementsReceiveDto(); + dto.setUserId(record.getId()); + dto.setUserName(record.getName()); + dto.setRosterId(record.getId()); + dto.setReadStatus(NoticeEnums.ReadStatus.UNREAD.getCode()); + list.add(dto); + } + fillFlowerAndHeadLog(list); + } + + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) req.getPageSize()); + pageInfo.setPageNum((int) req.getCurrentPage()); + pageInfo.setTotal((int) allUserList.size()); + return pageInfo; + } + + /** + * 查询用户的岗位信息 + * + * @param userId 用户id + * @return 岗位信息 + */ + public List getUserOrgBoundInfo(String userId) { + List list = userApiV2Util.getUserPrimaryBoundBatchReturnList(List.of(userId), null); + if (CollectionUtil.isEmpty(list)) { + return CollectionUtil.newArrayList(); + } + List retList = new ArrayList<>(); + for (UserBoundVO vo : list) { + UserOrgAndPositionDto dto = new UserOrgAndPositionDto(); + dto.setOrgId(vo.getOrganizeId()); + dto.setOrgName(vo.getOrganizeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + + dto.setPositionId(vo.getPositionId()); + dto.setPositionName(vo.getPositionName()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setRankId(vo.getGradeId()); + dto.setRankName(vo.getGradeName()); + retList.add(dto); + } + return retList; + } + + + /** + * 对应字符串list去重复 + * + * @param list 字符串集合 + * @return 去重后的字符串集合 + */ + public List uniqueStringList(List list) { + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list.stream() + .filter(Objects::nonNull) // 过滤掉 null 值 + .filter(str -> !str.isEmpty()) // 过滤掉空字符串 + .distinct() // 去重 + .collect(Collectors.toList()); // 收集为 List + } + + /** + * 获取随机数 + * + * @return 随机数 + */ + private Integer getRandom() { + Random random = new Random(); + return random.nextInt(60); + } + + /** + * 填充用户的花名和头像 + * + * @param list 用户列表 + */ + public void fillFlowerAndHeadLog(List list) { + List userIds = new ArrayList<>(); + for (NoticeUserDto noticeUserDto : list) { + userIds.add(noticeUserDto.getUserId()); + } + Map userPrimaryBoundBatch = userApiV2Util.getUserPrimaryBoundBatch(userIds, null); + for (NoticeUserDto noticeUserDto : list) { + UserBoundVO userBoundVO = userPrimaryBoundBatch.get(noticeUserDto.getUserId()); + if (userBoundVO != null) { + noticeUserDto.setUserId(userBoundVO.getId()); + noticeUserDto.setUserName(userBoundVO.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userBoundVO.getHeadIcon())); + noticeUserDto.setFlowerName(userBoundVO.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = getUserOrgAndPositionDto(userBoundVO); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + } + } + } + + + @NotNull + public static UserOrgAndPositionDto getUserOrgAndPositionDto(UserBoundVO userBoundVO) { + UserOrgAndPositionDto userOrgAndPositionDto = new UserOrgAndPositionDto(); + userOrgAndPositionDto.setOrgId(userBoundVO.getOrganizeId()); + userOrgAndPositionDto.setOrgEncode(userBoundVO.getOrganizeEnCode()); + userOrgAndPositionDto.setOrgName(userBoundVO.getOrganizeName()); + userOrgAndPositionDto.setPositionId(userBoundVO.getPositionId()); + userOrgAndPositionDto.setPositionEncode(userBoundVO.getPositionEnCode()); + userOrgAndPositionDto.setPositionName(userBoundVO.getPositionName()); + userOrgAndPositionDto.setRankId(userBoundVO.getGradeId()); + userOrgAndPositionDto.setRankName(userBoundVO.getGradeName()); + return userOrgAndPositionDto; + } + + /** + * 去掉富文本中的script 脚本 + * + * @param htmlString + * @return 去掉的富文本 + */ + + public String replaceAllScript(String htmlString) { + if (StringUtils.isEmpty(htmlString)) { + return ""; + } + return htmlString.replaceAll("]*>", "").replaceAll("]*>", "") + .replaceAll("", "").replaceAll("", ""); + } + + /** + * 去除html标签 并且截取前maxNum个字符 + * + * @param htmlString html字符串 + * @param maxNum 截取最大长度 + * @return 截取后的字符串 + */ + public String clearHtmlTag(String htmlString, Integer maxNum) { + if (StringUtils.isEmpty(htmlString)) { + return ""; + } + htmlString = replaceImgTagsWithText(htmlString); + String cleanedHtml = HtmlUtil.cleanHtmlTag(htmlString); + return StrUtil.sub(cleanedHtml, 0, maxNum); // 从第0个字符开始截取,长度为10 + } + + /** + * 将HTML字符串中的所有, ", "").replaceAll("", "") + .replaceAll("", "").replaceAll("", ""); + } + + /** + * 获取租户编码 + * + * @return String + */ + public String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } + + /** + * 根据用户ID 查询用户的基本信息 姓名 头像 花名 + * + * @param userId 用户ID + * @return NoticeUserDto + */ + public NoticeUserDto queryUserInfoForUserId(String userId) { + NoticeUserDto noticeUserDto = new NoticeUserDto(); + if (StringUtils.isEmpty(userId)) { + return noticeUserDto; + } + UserBoundVO userPrimaryBoundOne = userApiV2Util.getUserPrimaryBoundOne(userId, null); + if (userPrimaryBoundOne != null) { + noticeUserDto.setUserId(userPrimaryBoundOne.getId()); + noticeUserDto.setUserName(userPrimaryBoundOne.getName()); + noticeUserDto.setHeadLogo(UploaderUtil.uploaderImg(userPrimaryBoundOne.getHeadIcon())); + noticeUserDto.setFlowerName(userPrimaryBoundOne.getNickname()); + UserOrgAndPositionDto userOrgAndPositionDto = getUserOrgAndPositionDto(userPrimaryBoundOne); + noticeUserDto.setOrgList(List.of(userOrgAndPositionDto)); + } + + + return noticeUserDto; + } + + /** + * 获取登录头信息 + * + * @return Map + */ + public Map getHeadersForLogin() { + UserInfo userInfo = UserProvider.getUser(); + String token = userInfo.getToken(); + if (StringUtils.isEmpty(token)) { + token = ""; + } + return ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + } + + /** + * 过滤无效用户 + * + * @param userList 用户id列表 + * @return 过滤后的用户id列表 + */ + public List filterInvalidUser(List userList) { + if (CollectionUtil.isEmpty(userList)) { + return Collections.emptyList(); + } + + // 查询所有有效用户ID + List validUsers = queryAllValidUserId(); + if (CollectionUtil.isEmpty(validUsers)) { + return Collections.emptyList(); + } + try { + return getIntersection(userList, validUsers); + } catch (Exception e) { + // 根据实际需求决定是否记录日志或抛出异常 + // 示例:log.warn("过滤无效用户时发生异常", e); + return Collections.emptyList(); + } + } + + /** + * 获取两个List的交集 + * + * @param list1 第一个列表 + * @param list2 第二个列表 + * @return 交集列表 + */ + public static List getIntersection(List list1, List list2) { + if (CollectionUtil.isEmpty(list1) || CollectionUtil.isEmpty(list2)) { + return new ArrayList<>(); + } + + Set set2 = new HashSet<>(list2); + return list1.stream() + .filter(s -> s != null && set2.contains(s)) + .distinct() + .collect(Collectors.toList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/hepler/MysqlSequenceHelper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/hepler/MysqlSequenceHelper.java new file mode 100644 index 0000000..a06bf7c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/hepler/MysqlSequenceHelper.java @@ -0,0 +1,55 @@ +package jnpf.parameter.hepler; + +import jnpf.parameter.service.ParamService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MysqlSequenceHelper { + private final ParamService paramService; + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @param number 递增参数(兼容调用方传参) + * @return 最新数值 + */ + public Long increment(String key, Long number){ + return increment(key, key,number); + } + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @param type 参数类型.用于标识这个key,是属于哪一类的。 + * @param number 递增参数(兼容调用方传参) + * @return 最新数值 + */ + public Long increment(String key,String type, Long number){ + return paramService.increment(key,type, number); + } + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @return 最新数值 + */ + public Long increment(String key){ + return increment(key, 1L); + } + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @param type 参数类型.用于标识这个key,是属于哪一类的。 + * @return 最新数值 + */ + public Long increment(String key,String type){ + return increment(key, type,1L); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/ParamService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/ParamService.java new file mode 100644 index 0000000..5be66e5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/ParamService.java @@ -0,0 +1,48 @@ +package jnpf.parameter.service; + +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.po.FtbParamEntity; + +/** + * 基础参数服务 + */ +public interface ParamService { + + /** + * 保存或更新基础参数 + * + * @param entity 基础参数 + */ + void saveOrUpdateParam(FtbParamEntity entity); + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @param number 递增参数(兼容调用方传参) + * @return 最新数值 + */ + default Long increment(String key, Long number){ + return increment(key, key,number); + } + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @param type 参数类型,标识这一类的key + * @param number 递增参数(兼容调用方传参) + * @return 最新数值 + */ + Long increment(String key,String type, Long number); + + /** + * 根据key递增并返回最新数值 + * + * @param key 参数key + * @return 最新数值 + */ + default Long increment(String key){ + return increment(key, 1L); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/impl/ParamServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/impl/ParamServiceImpl.java new file mode 100644 index 0000000..4a8580c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/parameter/service/impl/ParamServiceImpl.java @@ -0,0 +1,106 @@ +package jnpf.parameter.service.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.po.FtbParamEntity; +import jnpf.parameter.service.ParamService; +import jnpf.storecertificatephoto.mapper.BaseParamMapper; +import jnpf.util.RandomUtil; +import jnpf.util.ServiceException; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +/** + * 基础参数服务实现 + */ +@Service +@RequiredArgsConstructor +public class ParamServiceImpl implements ParamService { + + private static final String CONFIG_KEY_PREFIX = "ftbWarningNotice_"; + private final BaseParamMapper baseParamMapper; + + @Override + public void saveOrUpdateParam(FtbParamEntity entity) { + ServiceException.notNull(entity, "基础参数不能为空"); + String key = entity.getKey(); + + int updateCount; + try { + updateCount = baseParamMapper.updateByKey(key, entity); + } catch (DuplicateKeyException e) { + throw new ServiceException("该证照类型的预警配置已存在"); + } + if (updateCount > 0) { + return; + } + + try { + baseParamMapper.insertParam(entity); + } catch (DuplicateKeyException e) { + int retryCount = baseParamMapper.updateByKey(key, entity); + ServiceException.isTrue(retryCount > 0, "保存预警配置失败"); + } + } + + /** + * 根据key递增参数值并返回最新值。 + * 处理流程: + * 1. 先执行 value = value + number; + * 2. 若更新条数为0,则插入一条 value=1 的记录; + * 3. 若更新成功,则查询并返回最新 value。 + * + * @param key 参数key + * @param number 递增参数(兼容调用方传参) + * @return 最新数值 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long increment(String key, String type, Long number) { + String trimKey = StrUtil.trim(key); + ServiceException.isTrue(StrUtil.isNotBlank(trimKey), "参数key不能为空"); + ServiceException.notNull(number, "参数number不能为空"); + + int updateCount = baseParamMapper.incrementValueByKey(trimKey, number); + if (updateCount == 0) { + FtbParamEntity entity = new FtbParamEntity(); + entity.setId(RandomUtil.uuId()); + entity.setKey(trimKey); + entity.setType(type); + entity.setValue("1"); + entity.setSort(number); + + try { + baseParamMapper.insertParam(entity); + return 1L; + } catch (DuplicateKeyException e) { + int retryCount = baseParamMapper.incrementValueByKey(trimKey, number); + ServiceException.isTrue(retryCount > 0, "递增失败"); + } + } + + FtbParamEntity dbEntity = baseParamMapper.selectByKey(trimKey); + ServiceException.notNull(dbEntity, "基础参数不存在"); + ServiceException.isTrue(StrUtil.isNotBlank(dbEntity.getValue()), "基础参数值不能为空"); + try { + return Long.parseLong(StrUtil.trim(dbEntity.getValue())); + } catch (NumberFormatException e) { + throw new ServiceException("基础参数值不是数字"); + } + } + + /** + * 根据证照类型构建配置key + * + * @param certificateType 证照类型 + * @return 配置key + */ + private String buildConfigKey(CertificateTypeEnum certificateType) { + return CONFIG_KEY_PREFIX + DigestUtil.md5Hex(certificateType.getType()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/EnvironmentParamConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/EnvironmentParamConfig.java new file mode 100644 index 0000000..085cc48 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/EnvironmentParamConfig.java @@ -0,0 +1,16 @@ +package jnpf.personnels.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 正式环境参数 + */ +@Component +@ConfigurationProperties(prefix = "environmentcheck") +@Data +public class EnvironmentParamConfig { + + private Boolean isEnableSms; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/TengxunLicenseConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/TengxunLicenseConfig.java new file mode 100644 index 0000000..2f334a5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/config/TengxunLicenseConfig.java @@ -0,0 +1,17 @@ +package jnpf.personnels.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "config.license") +public class TengxunLicenseConfig { + + private String domain; + + private String secretId; + + private String secretKey; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/apply/FtbPersonnelsPostApplyForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/apply/FtbPersonnelsPostApplyForAppController.java new file mode 100644 index 0000000..8cc1843 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/apply/FtbPersonnelsPostApplyForAppController.java @@ -0,0 +1,218 @@ +package jnpf.personnels.controller.app.apply; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.entity.StoreEntity; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.apply.FtbPersonnelsApplyCreateDto; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.dto.staff.roster.StaffHomeDto; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyWithPerVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.model.position.PositionInfoNewVO; +import jnpf.permission.model.user.UserInfoVO; +import jnpf.personnels.service.FtbPersonnelsPostApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.store.service.StoreService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * app人事岗位晋升申请表模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/app/personnels-apply") +public class FtbPersonnelsPostApplyForAppController { + + @Resource + FtbPersonnelsPostApplyService service; + + + @Autowired + UserApi userApi; + + @Autowired + CultivatePerUtils cultivatePerUtils; + + + @Autowired + FtbPersonnelsStaffRosterService staffRosterService; + + @Resource + StoreService storeService; + + @Autowired + OrganizeApi organizeApi; + + /** + * 获取岗位申请列表 + * @param dto 岗位申请查询参数 + * @param page 分页信息 + * @return 岗位申请列表结果 + */ + @GetMapping("/getList") + public ActionResult> getList(FtbPersonnelsForAppQueryDTO dto, + CultivatePage page) { + PageListVO pageListVO = service.getListForApp(dto, page); + return ActionResult.success(pageListVO); + } + + + /** + * 我的审批,抄送,小气泡统计数 + * @param flag "1" 我的审批 "2" 抄送 + */ + @GetMapping("/getListCont") + @Deprecated(since = "移除审批数据") + public ActionResult getListCont(@RequestParam String flag) { + FtbPersonnelsBubbleCountVO countVO = service.getListCont(flag); + return ActionResult.success(countVO); + } + + + /** + * 初始化岗位申请 + * + * @param createDto 岗位申请创建数据传输对象 + * @return v1.1 返回 1 标识为门店负责人,需要进行替换 + */ + @PostMapping("/initPromApplication") + public ActionResult initiateAPromotionApplication( + @Validated @RequestBody FtbPersonnelsApplyCreateDto createDto) { + createDto.setSource(0); + createDto.setFlag("2"); + Map map = cultivatePerUtils.coverPersonalIds(Collections.singletonList(createDto.getUserId())); + if (map !=null){ + createDto.setSystemWokerId(map.get(createDto.getUserId())); + } + StaffHomeDto staffHomeDto = staffRosterService.queryWorkerHomeDetail(createDto.getUserId()); + if (ObjectUtil.isNotEmpty(staffHomeDto)){ + createDto.setCurrentPay(staffHomeDto.getCurrSalary()); + } + UserInfoVO bossUser = userApi.getLeaderInfo(createDto.getOrgId(),createDto.getUserId(),createDto.getPostId(),createDto.getPostRankId()); + if (ObjectUtil.isNotEmpty(bossUser) && bossUser.getRealName() != null) { + createDto.setImmediateSuperName(bossUser.getRealName()); + createDto.setOldImmediateSuperName(bossUser.getRealName()); + } + if (ObjectUtil.isNotEmpty(bossUser) && bossUser.getId() != null) { + createDto.setImmediateSuperId(bossUser.getId()); + createDto.setOldImmediateSuperId(bossUser.getId()); + } + String s = service.initiateAPromotionApplication(createDto); + return ActionResult.success(MsgCode.SU000.get(),s); + } + + /** + * 直接办理岗位申请 + */ + @PostMapping("/applyDirectlyForThePosition") + public ActionResult applyDirectlyForThePosition( + @Validated @RequestBody FtbPersonnelsApplyCreateDto createDto) { + extracted(createDto.getUserId(), createDto.getOrgId(),createDto.getPostId(),createDto.getPostRankId(),createDto); + String s = service.initiateAPromotionApplication(createDto); + return ActionResult.success(MsgCode.SU000.get(),s); + } + + /** + * 主管构建 + * @param userId + * @param orgId + * @param postId + * @param postRankId + * @param createDto + */ + private void extracted(String userId,String orgId,String postId,String postRankId,FtbPersonnelsApplyCreateDto createDto) { + UserInfoVO bossUser = userApi.getLeaderInfo(orgId, userId,postId,postRankId); + // 将老的主管信息赋值 + if (ObjectUtil.isNotEmpty(bossUser) && bossUser.getRealName() != null) { + createDto.setOldImmediateSuperName(bossUser.getRealName()); + } + if (ObjectUtil.isNotEmpty(bossUser) && bossUser.getId() != null) { + createDto.setOldImmediateSuperId(bossUser.getId()); + } + // 只要如果晋升主管为空,则设置为之前的主管 + if (StringUtils.isEmpty(createDto.getImmediateSuperId()) && ObjectUtil.isNotEmpty(bossUser) + && createDto.getOrgId().equals(createDto.getOrgCurrentId())){ + createDto.setImmediateSuperId(bossUser.getId()); + createDto.setImmediateSuperName(bossUser.getRealName()); + } + } + + + /** + * 根据用户id和组织id 获取晋升通道晋升岗位 + */ + @GetMapping("/queryPostInfoByOrgAndUserId") + @Deprecated(since = "培训v1.1废弃", forRemoval = true) + public ActionResult> queryPostInfoByOrgAndUserId(@RequestParam("userId") String userId, + @RequestParam("orgId") String orgId){ + List voList = service.queryPostInfoByOrgAndUserId(userId,orgId); + return ActionResult.success(voList); + } + /** + * 查询岗位申请详情 + * @param id 岗位申请主键id + * @return 响应结果对象,包含岗位申请及相关信息 + */ + @GetMapping("/viewPromApplication/{id}") + @Operation(summary = "查询岗位申请详情") + public ActionResult viewPromApplication(@PathVariable("id") String id) { + FtbPersonnelsApplyWithPerVO withPerVO = service.viewPromotionApplications(id,"1"); + return ActionResult.success(withPerVO); + } + + /** + * 审核岗位申请的接口 + * @param dto 岗位申请数据传输对象 + * @return 审核结果 + */ + @PutMapping("/auditPromApplication") + @Operation(summary = "审核岗位申请") + public ActionResult auditPromotionPostApplication(@Validated @RequestBody FtbPersonnelsSalaryAuditDto dto) { + service.auditPromotionPostApplication(dto); + return ActionResult.success(); + } + + + /** + * 获取门店负责人详情 + * @param userId 门店负责人id + * @return organizeName 组织名称 storeName 门店名称 + */ + @GetMapping("/getStoreManagerInfo") + public ActionResult>> getStoreManagerInfo(@RequestParam("userId") String userId) { + LambdaQueryWrapper objectLambdaQueryWrapper = Wrappers.lambdaQuery(); + objectLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid,userId); + List list = storeService.list(objectLambdaQueryWrapper); + List> maps = list.stream().map(item -> { + Map map = Maps.newHashMap(); + OrganizeEntity organize = organizeApi.getInfoById(item.getOrganizeid()); + map.put("organizeName", organize.getFullName()); + map.put("storeName", item.getStorename()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(maps); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskForAppController.java new file mode 100644 index 0000000..53ec88e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskForAppController.java @@ -0,0 +1,49 @@ +package jnpf.personnels.controller.app.audit; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditRunTaskVO; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * app人事审核正在执行的任务信息模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/app/personnels-task") +public class FtbPersonnelsAuditRunTaskForAppController { + + + @Resource + private FtbPersonnelsAuditRunTaskService service; + +// /** +// * 查看当前审批流程 +// * +// * @param id 审批ID +// * @return 当前审批流程的结果Action +// */ +// @GetMapping("/view-list/{id}") +// @Deprecated +// public ActionResult viewTheCurrentApprovalProcess(@PathVariable("id") String id){ +// return ActionResult.success(service.viewTheCurrentApprovalProcess(id)); +// } + + /** + * 查看对应业务的审批流程 + * + * @param id 运行的 taskId + * @return 当前审批流程的结果Action + */ + @GetMapping("/view-list-by-task/{id}") + public ActionResult viewTheCurrentApprovalProcessByTaskId(@PathVariable("id") String id){ + return ActionResult.success(service.viewTheCurrentApprovalProcessByTaskId(id)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskHistoryForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskHistoryForAppController.java new file mode 100644 index 0000000..1c87ddf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/audit/FtbPersonnelsAuditRunTaskHistoryForAppController.java @@ -0,0 +1,40 @@ +package jnpf.personnels.controller.app.audit; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.history.FtbPersonnelsAuditRunTaskHistoryVO; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskHistoryService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * app人事审核历史执行的任务信息模块 + * + * @author penghao + */ +@RestController +@RequestMapping("/app/personnels-task-history") +public class FtbPersonnelsAuditRunTaskHistoryForAppController { + + + @Resource + FtbPersonnelsAuditRunTaskHistoryService service; + + + /** + * 查看历史审批列表 + * + * @param id 审批ID + * @return 历史审批列表的结果Action + */ + @GetMapping("/view-list/{id}/{type}") + public ActionResult> viewHistoricalApprovalList( + @PathVariable("id") String id,@PathVariable("type")String type){ + return ActionResult.success(service.viewHistoricalApprovalList(id,type)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsEmEntryAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsEmEntryAppController.java new file mode 100644 index 0000000..e37610e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsEmEntryAppController.java @@ -0,0 +1,109 @@ +package jnpf.personnels.controller.app.employmentapply; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.emp.FtbEmpAddNewDTO; +import jnpf.model.personnels.dto.emp.FtbEmpConfirmDTO; +import jnpf.model.personnels.dto.emp.FtbEmpEntryDTO; +import jnpf.model.personnels.vo.emp.FtbEmpConfirmVO; +import jnpf.model.personnels.vo.emp.FtbEmpEntryVO; +import jnpf.model.personnels.vo.emp.FtbEmpResultVO; +import jnpf.personnels.service.FtbPersonnelsEmEntryService; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +/** + * app入职管理 + * @Author: peng.hao + * @create: 2025/4/7 + */ +@RestController +@RequestMapping("/app/personnels-emp-entry") +public class FtbPersonnelsEmEntryAppController { + + @Resource + FtbPersonnelsEmEntryService ftbPersonnelsEmEntryService; + + @Resource + RedissonClient redissonClient; + + + /** + * 查询入职管理列表 + */ + @GetMapping("/query-list") + public ActionResult> pageLists(FtbEmpEntryDTO empEntryDTO) { + return ActionResult.success(ftbPersonnelsEmEntryService.pageLists(empEntryDTO,"app")); + } + /** + * 新增员工/编辑未办理员工 + */ + @PutMapping("/add-or-update-new-emp") + public ActionResult addNewEmp(@RequestBody FtbEmpAddNewDTO dto) { + FtbEmpResultVO resultVO = ftbPersonnelsEmEntryService.addNewEmp(dto); + return ActionResult.success(resultVO); + } + + /** + * 终止入职 + */ + @PutMapping("/terminate-onboarding/{id}") + public ActionResult terminateOnboarding(@PathVariable String id) { + ftbPersonnelsEmEntryService.terminateOnboarding(id); + return ActionResult.success(); + } + /** + * 办理入职 + */ + @PutMapping("/handle-join-job") + public ActionResult handleJoinJob(@RequestBody FtbEmpConfirmDTO ftbEmpConfirmDTO) { + String lockKey = "HANDLE_JOIN_JOB:" + ftbEmpConfirmDTO.getPhone(); + RLock lock = redissonClient.getLock(lockKey); + try { + boolean b = lock.tryLock(1000, TimeUnit.MILLISECONDS); + if (!b) {return ActionResult.fail("当前手机号正在办理中请勿重复点击");} + ftbPersonnelsEmEntryService.handleJoinJob(ftbEmpConfirmDTO); + }catch (Exception e){ + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + }finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return ActionResult.success(); + } + + /** + * 查看入职详情 + */ + @GetMapping("/onboarding-details") + public ActionResult onboardingDetails(@RequestParam String id){ + return ActionResult.success(ftbPersonnelsEmEntryService.onboardingDetails(id)); + } + /** + * 删除员工入职记录 + * + * @param id 主键id,必传 + */ + @DeleteMapping("/delete/{id}") + public ActionResult deleteEmployeeOnboardingRecords(@PathVariable("id") String id) { + ftbPersonnelsEmEntryService.deleteEmployeeOnboardingRecords(id); + return ActionResult.success(); + } + + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsStaffEmploymentApplyAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsStaffEmploymentApplyAppController.java new file mode 100644 index 0000000..c77e31c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/employmentapply/FtbPersonnelsStaffEmploymentApplyAppController.java @@ -0,0 +1,271 @@ +package jnpf.personnels.controller.app.employmentapply; + +import cn.hutool.core.bean.BeanUtil; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.req.employment.AddStaffEmploymentApplyReq; +import jnpf.model.personnels.req.employment.AddStaffEmploymentApplyResultDto; +import jnpf.model.personnels.req.employment.EmploymentApplyCheckDto; +import jnpf.model.personnels.req.employment.QueryAppStaffEmploymentApplyListReq; +import jnpf.model.personnels.req.roster.CheckOrgAndPosAndRankExistVo; +import jnpf.model.personnels.req.roster.ConfirmOnDutyReq; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.employment.CheckPhoneStatusVo; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.concurrent.TimeUnit; + +/** + * app员工入职表模块 + * + * @author xxxxx + */ +@Slf4j +@RestController +@RequestMapping("/app/personnels-staff-employment-apply") +@Deprecated(since = "人事v2.0") +public class FtbPersonnelsStaffEmploymentApplyAppController { + + + @Autowired + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + + /** + * 查询预入职 + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @GetMapping("/query-list") + public ActionResult> pageLists(@Validated QueryAppStaffEmploymentApplyListReq req) { + String loginUserId = UserProvider.getLoginUserId(); + PageInfo pageVo = staffEmploymentApplyService.getAppPageList(req, loginUserId); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 我的审批,抄送,小气泡统计数 + * @param flag "1" 我的审批 "2" 抄送 + */ + @GetMapping("/getListCont") + public ActionResult getListCont(@RequestParam String flag) { + FtbPersonnelsBubbleCountVO countVO = staffEmploymentApplyService.getListCont(flag); + return ActionResult.success(countVO); + } + + + /** + * 检测校验组织/岗位/职级是否存在 + * + * @param vo + * @return + * * 0:成功 + * * 1:失败:组织被删除 + * * 2:失败:岗位被删除 + * * 3:失败:职等被删除 + */ + @GetMapping("/checkOrgPositionRankExist") + public ActionResult checkOrgPositionRankExist(CheckOrgAndPosAndRankExistVo vo) { + ConfirmOnDutyReq confirmOnDutyReq = BeanUtil.copyProperties(vo, ConfirmOnDutyReq.class); + Integer checkStatus = checkOrgPositionRank(confirmOnDutyReq); + return ActionResult.success(checkStatus); + } + + /** + * 确认到岗 + * + * @param req 入职参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/confirm-on-duty") + public ActionResult confirmOnDuty(@Validated @RequestBody ConfirmOnDutyReq req) { + String lockKey = "ConfirmOnDuty:" + req.getPhone(); + Integer checkStatus = checkOrgPositionRank(req); + if(!checkStatus.equals(0)){ + AddStaffEmploymentApplyResultDto dto = new AddStaffEmploymentApplyResultDto(); + dto.setId(""); + dto.setStatus(checkStatus); + return ActionResult.success("成功", dto); + } + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) { + try { + String id = staffRosterService.confirmOnDuty(req); + AddStaffEmploymentApplyResultDto dto = new AddStaffEmploymentApplyResultDto(); + dto.setId(id); + dto.setStatus(0); + return ActionResult.success("成功", dto); + } catch (Exception e) { + log.error("确认到岗失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + private Integer checkOrgPositionRank(ConfirmOnDutyReq req) { + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(req.getCurrOrg()); + if(null==organizeEntity){ + return 1; + } + PositionEntity positionEntity = personnelOrgUtils.queryPosition(req.getCurrPosition()); + if(null==positionEntity){ + return 2; + } + PositionGradesInfoVO positionGradesInfoVO = personnelOrgUtils.queryRank(req.getCurrRank()); + if(null==positionGradesInfoVO){ + return 3; + } + return 0; + } + + + /** + * 获取员工入职记录详细信息 + * + * @param id + * @return + */ + @GetMapping("/query-detail/{id}") + public ActionResult get(@PathVariable("id") String id) { + FtbPersonnelsStaffEmploymentApplyDto info = staffEmploymentApplyService.getInfo(id); + return ActionResult.success(info); + } + + + /** + * 办理入职 + * + * @param req 入职参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/handle-join-job") + public ActionResult handleJoinJob(@Validated @RequestBody AddStaffEmploymentApplyReq req) { + String id = staffEmploymentApplyService.insertData(req); + return ActionResult.success("成功", id); + } + + /** + * 审批 + * + * @param id 主键id,必传 + * @return + */ + + @PutMapping("/approval/{id}") + public ActionResult approval(@PathVariable("id") String id, @RequestBody EmploymentApplyCheckDto dto) { + dto.setId(id); + staffEmploymentApplyService.approval(dto); + return ActionResult.success(); + } + + + + /** + * 重新办理 + * + * @param id 主键id,必传 + * @return + */ + @PutMapping("/re-approval/{id}") + public ActionResult reApproval(@PathVariable("id") String id, @RequestBody AddStaffEmploymentApplyReq dto) { + staffEmploymentApplyService.reApproval(id, dto); + return ActionResult.success(); + } + + + /** + * 删除 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + staffEmploymentApplyService.deleteData(id); + return ActionResult.success(); + } + + + /** + * 办理入职发送短信 + * + * @param phone 手机号,必传 + * @return + */ + @PutMapping("/send-phone-msg/{phone}") + @Deprecated(since = "人事v1.3", forRemoval = true) + public ActionResult sendPhoneMsg(@PathVariable("phone") String phone) { + staffEmploymentApplyService.sendPhoneMsg(phone); + return ActionResult.success("发送成功"); + + } + + + /** + * 查询邀请码接口 + * + * @return + */ + @GetMapping("/query-code") + public ActionResult queryCode() { + String code = staffEmploymentApplyService.queryCode(); + return ActionResult.success("成功", code); + } + + /** + * 办理入职,检测用户在系统中的状态 + * 0、系统不存在该用户 1、该手机号等待入职 2、该手机号等待入职审批 302、试用 303、正式 304、待离职 305 离职 306 试岗 4-该手机号已被拉入黑名单 + * + * @param phone 手机号 + * @return + */ + @GetMapping("/check-phone-status/{phone}") + public ActionResult checkPhoneStatus(@PathVariable("phone") String phone) { + String status = staffEmploymentApplyService.checkPhoneStatus(phone); + CheckPhoneStatusVo vo = new CheckPhoneStatusVo(); + vo.setStatus(status); + return ActionResult.success(vo); + } + + /** + * 保存短链 + * + * @param id 入职表ID + * @param shortUrl 短链 + * @return + */ + @PutMapping("/save-short-url/{id}") + public ActionResult saveShortUrl(@PathVariable("id") String id, @RequestParam("shortUrl") String shortUrl) { + staffEmploymentApplyService.saveShortUrl(id, shortUrl); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/formdata/FtbPersonnelsStaffRegistrationFormDataAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/formdata/FtbPersonnelsStaffRegistrationFormDataAppController.java new file mode 100644 index 0000000..e957e01 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/formdata/FtbPersonnelsStaffRegistrationFormDataAppController.java @@ -0,0 +1,216 @@ +package jnpf.personnels.controller.app.formdata; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.dto.staff.field.SubFormFieldDto; +import jnpf.model.personnels.dto.staff.registerform.BankConvertDto; +import jnpf.model.personnels.dto.staff.registerform.CheckRegisterFormFillDto; +import jnpf.model.personnels.dto.staff.registerform.IdCardConverterDto; +import jnpf.model.personnels.req.registerform.OcrReq; +import jnpf.model.personnels.req.registerform.SaveAppRegisterFormDataReq; +import jnpf.model.personnels.req.registerform.SaveFormDataReq; +import jnpf.model.personnels.req.registerform.SaveMyFormDataReq; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.utils.PersonnelCardOcr; +import jnpf.util.NoDataSourceBind; +import jnpf.util.TenantUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * app员工档案信息数据表模块 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/app/personnels-staff-registration-form-data") +public class FtbPersonnelsStaffRegistrationFormDataAppController { + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Resource + private TenantUtil tenantUtil; + + @Autowired + private PersonnelCardOcr personnelIdCardOcr; + + @Resource + RedissonClient redissonClient; + + /** + * 根据租户查询入职登记表表单 + * + * @param tenantId 租户ID + * @return + */ + @GetMapping("/query-registration-form/{tenantId}") + @NoDataSourceBind + public ActionResult> queryRegistrationForm(@PathVariable("tenantId") String tenantId) { + tenantUtil.switchTenant(tenantId); + List list = registrationFormDataService.queryRegistrationForm(); + return ActionResult.success(list); + } + + /** + * 根据邀请码查询租户ID + * + * @param code 邀请码 + * @return + */ + @GetMapping("/query-registration-form/code-to-tenantId/{code}") + @NoDataSourceBind + public ActionResult codeToTenantId(@PathVariable("code") String code) { + + String tenantId = registrationFormDataService.queryTenantIdForCode(code); + if (StringUtils.isEmpty(tenantId)) { + throw new RuntimeException("邀请码不正确"); + } + return ActionResult.success("查询成功", tenantId); + } + + /** + * 保存登记表 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/save-register/{tenantId}") + @NoDataSourceBind + public ActionResult saveRegister(@PathVariable("tenantId") String tenantId, + @Validated @RequestBody SaveAppRegisterFormDataReq req) { + List fieldValueList = req.getFieldValueList(); + SubFormFieldDto phone = fieldValueList.stream().filter(v -> v.getId().equals("phone")).findFirst().orElse(null); + if (phone == null) { + return ActionResult.fail("手机号不能为空"); + } + String lockKey = "HANDLE_JOIN_JOB:" + phone.getUserValue(); + RLock lock = redissonClient.getLock(lockKey); + try { + boolean b = lock.tryLock(3000, TimeUnit.MILLISECONDS); + if (!b) {return ActionResult.fail("当前人员正在填写登记表,请勿重复提交!");} + tenantUtil.switchTenant(tenantId); + registrationFormDataService.saveRegistrationForm(tenantId, fieldValueList,1, req.getRegisterImg(),req.getModuleId()); + }catch (Exception e){ + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + }finally { + lock.unlock(); + } + + return ActionResult.success(); + } + + /** + * 检测入职登记表表单是否已经填 + * + * @param tenantId 入职登记表ID + * @return + */ + @GetMapping("/check-registration-form-fill/{tenantId}") + @NoDataSourceBind + public ActionResult checkRegistrationFormFill(@PathVariable("tenantId") String tenantId, + @RequestParam("phone") String phone) { + tenantUtil.switchTenant(tenantId); + CheckRegisterFormFillDto checkRegisterFormFill = registrationFormDataService.checkRegistrationFormFill(phone); + return ActionResult.success(checkRegisterFormFill); + } + + + /** + * 查询我的档案表单 + * + * @return + */ + @GetMapping("/query-my-archival-form") + public ActionResult> queryMyArchivalForm() { + String loginUserId = UserProvider.getLoginUserId(); + List list = registrationFormDataService.queryArchivalForm(loginUserId); + + return ActionResult.success(list); + } + + + /** + * 保存我的档案表单数据 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/save-my-archives") + public ActionResult saveMyArchives(@Validated @RequestBody SaveMyFormDataReq req) { + String loginUserId = UserProvider.getLoginUserId(); + SaveFormDataReq saveFormDataReq = new SaveFormDataReq(); + saveFormDataReq.setUserId(loginUserId); + saveFormDataReq.setFieldValueList(req.getFieldValueList()); + registrationFormDataService.saveArchives(saveFormDataReq); + return ActionResult.success(); + } + + /** + * 根据userId查询档案表单 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/query-archival-form/roster/{userId}") + public ActionResult> queryArchivalFormForRoster(@PathVariable("userId") String userId) { + List list = registrationFormDataService.queryArchivalForm(userId); + + return ActionResult.success(list); + } + + /** + * 保存用户的档案信息 + * + * @param userId 用户ID + * @return + */ + @PostMapping("/save-archives/{userId}") + public ActionResult saveMyArchives(@PathVariable("userId") String userId, @Validated @RequestBody SaveMyFormDataReq req) { + + SaveFormDataReq saveFormDataReq = new SaveFormDataReq(); + saveFormDataReq.setUserId(userId); + saveFormDataReq.setFieldValueList(req.getFieldValueList()); + registrationFormDataService.saveArchives(saveFormDataReq); + return ActionResult.success(); + } + + + /** + * 身份证ocr + * + * @param req + * @return + */ + @PostMapping("/id-card-ocr") + @NoDataSourceBind + public ActionResult idCardOcr(@RequestBody OcrReq req) { + IdCardConverterDto dto = personnelIdCardOcr.idCardOcr(req.getImgUrl()); + return ActionResult.success(dto); + } + + /** + * 银行卡ocr + * + * @param req + * @return + */ + @PostMapping("/bank-ocr") + @NoDataSourceBind + public ActionResult healthOcr(@RequestBody OcrReq req) { + BankConvertDto dto = personnelIdCardOcr.bankOcr(req.getImgUrl()); + return ActionResult.success(dto); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/goods/FtbPersonnelsGoodsAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/goods/FtbPersonnelsGoodsAppController.java new file mode 100644 index 0000000..948bfd6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/goods/FtbPersonnelsGoodsAppController.java @@ -0,0 +1,97 @@ +package jnpf.personnels.controller.app.goods; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveAddDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveReturnDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveUpdateDTO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsPageVO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceiveDetailsVO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceivePageVO; +import jnpf.personnels.service.FtbPersonnelsGoodsReceiveService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * app物品管理模块 + * + * @author wangchunxiang + * @date 2025/09/11 + */ +@RestController +@RequestMapping(value = "/app/personnels-goods-receive") +public class FtbPersonnelsGoodsAppController { + + @Resource + private FtbPersonnelsGoodsReceiveService ftbPersonnelsGoodsReceivesService; + + /** + * 添加物品领用 + */ + @PostMapping(value = "/add") + public ActionResult add(@RequestBody @Valid List form) { + ftbPersonnelsGoodsReceivesService.create(form); + return ActionResult.success(); + } + + /** + * 修改物品领用 + */ + @PostMapping(value = "/update") + public ActionResult update(@RequestBody @Valid FtbPersonnelsGoodsReceiveUpdateDTO form) { + ftbPersonnelsGoodsReceivesService.update(form); + return ActionResult.success(); + } + + /** + * 删除物品领用 + * + * @param id 物品主键id + * @return {@link ActionResult }<{@link Void }> + */ + @DeleteMapping(value = "/delete/{id}") + public ActionResult delete(@PathVariable(value = "id") String id) { + ftbPersonnelsGoodsReceivesService.delete(id); + return ActionResult.success(); + } + + /** + * 获取物品领用列表 + */ + @GetMapping(value = "/list") + public ActionResult> list(FtbPersonnelsGoodsReceiveQueryDTO formQueryDTO, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbPersonnelsGoodsReceivesService.list(page, formQueryDTO); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + + /** + * 根据物品领用主键Id获取物品领用详情 + * + * @param id 物品领用主键Id + * @return {@link ActionResult }<{@link FtbPersonnelsGoodsPageVO }> + */ + @GetMapping(value = "/details") + public ActionResult details(@RequestParam(value = "id") String id) { + FtbPersonnelsGoodsReceiveDetailsVO ftbPersonnelsGoodsPageVO = ftbPersonnelsGoodsReceivesService.details(id); + return ActionResult.success(ftbPersonnelsGoodsPageVO); + } + + /** + * 归还物品 + */ + @PostMapping(value = "/return-goods") + public ActionResult returnGoods(@RequestBody @Valid FtbPersonnelsGoodsReceiveReturnDTO returnDTO) { + ftbPersonnelsGoodsReceivesService.returnGoods(returnDTO); + return ActionResult.success(); + } + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/regular/FtbPersonnelsRegularManagementForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/regular/FtbPersonnelsRegularManagementForAppController.java new file mode 100644 index 0000000..4d3b5ae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/regular/FtbPersonnelsRegularManagementForAppController.java @@ -0,0 +1,122 @@ +package jnpf.personnels.controller.app.regular; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularInfoVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularManagementVO; +import jnpf.personnels.service.FtbPersonnelsRegularManagementService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * app转正管理模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/app/personnels-regular") +public class FtbPersonnelsRegularManagementForAppController { + + @Resource + FtbPersonnelsRegularManagementService service; + + /** + * 转正管理列表查询,返回分页列表数据 + * + * @param dto 查询参数对象 + * @return 返回分页列表数据的结果 + */ + @GetMapping("/list-query") + public ActionResult> pageList(FtbPersonnelsForAppQueryDTO dto, + CultivatePage page) { + PageListVO pageListVO = service.pageForAppList(dto,page); + return ActionResult.success(pageListVO); + } + /** + * 我的审批,抄送,小气泡统计数 + * @param flag "1" 我的审批 "2" 抄送 + */ + @GetMapping("/getListCont") + public ActionResult getListCont(@RequestParam String flag) { + FtbPersonnelsBubbleCountVO countVO = service.getListCont(flag); + return ActionResult.success(countVO); + } + + + /** + * 办理转正 + * + * @param createDTO 申请参数对象 + * @return 返回申请结果 + */ + @PutMapping("/apply-regularization") + public ActionResult applyForRegularization(@Validated @RequestBody FtbPersonnelsRegularCreateDTO createDTO) { + return ActionResult.success("", service.applyForRegularization(createDTO)); + } + + + + /** + * 查看审批的详细信息 + * + * @param id 审批id + * @return 返回 regularization 审批的详细信息结果 + */ + @GetMapping("/details-process/{id}") + public ActionResult checkTheDetailsOfRegularizationApproval(@PathVariable("id")String id) { + return ActionResult.success(service.checkTheDetailsOfRegularizationApproval(id)); + } + + /** + * 进行转正审批 + * + * @param auditDto 审批信息 + * @return 返回 regularization 审批结果 + */ + @PutMapping("/regularization-approval") + public ActionResult regularizationApproval(@Validated @RequestBody FtbPersonnelsSalaryAuditDto auditDto) { + service.regularizationApproval(auditDto); + return ActionResult.success(); + } + + /** + * 进行转正审批 + * + * @param auditDto 审批信息 + * @return 返回 regularization 审批结果 + */ + @PostMapping("/regularization-approval") + public ActionResult regularizationApprovalWithPost(@Validated @RequestBody FtbPersonnelsSalaryAuditDto auditDto) { + service.regularizationApproval(auditDto); + return ActionResult.success(); + } + + + /** + * 撤销申请 + * @param id + * @return + */ + @GetMapping("/cancellation-rectification") + public ActionResult cancellationOfApplicationForRectification(@RequestParam String id){ + service.cancellationOfApplicationForRectification(id, null); + return ActionResult.success(); + } + /** + * 删除转正申请 + * @param id + * @return + */ + @PutMapping("/delete-rectification/{id}") + public ActionResult deleteApplicationForRectification(@PathVariable("id")String id){ + service.removeById(id); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/AppPersonnelsStaffArchivesHistoryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/AppPersonnelsStaffArchivesHistoryController.java new file mode 100644 index 0000000..830216b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/AppPersonnelsStaffArchivesHistoryController.java @@ -0,0 +1,65 @@ +package jnpf.personnels.controller.app.roster; + +import cn.hutool.json.JSONUtil; +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.dto.staff.growth.FtbPersonnelsStaffGrowthLogDto; +import jnpf.model.personnels.vo.roster.FtbPersonnelsStaffArchivesHistoryVo; +import jnpf.personnels.service.FtbPersonnelsStaffArchivesHistoryService; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +/** + * App离职员工档案信息模块 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/app/personnelsStaffArchivesHistory") +public class AppPersonnelsStaffArchivesHistoryController { + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + private FtbPersonnelsStaffArchivesHistoryService archivesHistoryService; + + /** + * 查询离职档案信息 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/get/{userId}") + public ActionResult> get(@PathVariable("userId") String userId) { + FtbPersonnelsStaffArchivesHistoryVo info = archivesHistoryService.getInfo(userId); + if (null != info && StringUtils.isNotEmpty(info.getArchives())) { + List list = JSONUtil.toList(info.getArchives(), FormTypeDto.class); + return ActionResult.success(list); + } + return ActionResult.fail("未查询到该用户的档案信息"); + } + + /** + * 查询员工成长列表 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/query-growth-list/{userId}") + public ActionResult> queryGrowthList(@PathVariable("userId") String userId) { + List list = growthLogService.queryAll(userId); + return ActionResult.success(list); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/FtbPersonnelsStaffRosterAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/FtbPersonnelsStaffRosterAppController.java new file mode 100644 index 0000000..b14d087 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/roster/FtbPersonnelsStaffRosterAppController.java @@ -0,0 +1,316 @@ +package jnpf.personnels.controller.app.roster; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.FtbCultivatePositionForAppService; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.app.FtbCultivatePositionForAppNewDTO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.position.app.FtbSubordinateLearningCoursesVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionWithPersonelVO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.dto.staff.growth.FtbPersonnelsStaffGrowthLogDto; +import jnpf.model.personnels.dto.staff.roster.CheckRosterDeleteVo; +import jnpf.model.personnels.dto.staff.roster.EditUserBaseInfoDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.StaffHomeDto; +import jnpf.model.personnels.dto.staff.roster.WorkerStatisticsDto; +import jnpf.model.personnels.dto.staff.salarylog.FtbPersonnelsStaffSalaryChangeLogDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.roster.AppStaffRosterListReq; +import jnpf.permission.dto.QueryUserListDTO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.service.FtbPersonnelsStaffSalaryChangeLogService; +import jnpf.util.FtbUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * app员工花名册模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/app/personnels-staff-roster") +public class FtbPersonnelsStaffRosterAppController { + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + private FtbPersonnelsStaffSalaryChangeLogService staffSalaryChangeLogService; + + @Resource + FtbCultivatePositionForAppService positionForAppService; + + /** + * 花名册查询列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @PostMapping("/query-list") + public ActionResult> pageLists(@RequestBody @Validated AppStaffRosterListReq req) { + PageInfo pageVo = staffRosterService.getAppPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 花名册员工数量统计 + * + * @return + */ + @GetMapping("/worker-number/statistics") + public ActionResult statisticsWorkerNumber() { + return ActionResult.success(staffRosterService.statisticsWorkerNumber()); + } + + + /** + * 把员工从花名册中删除 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + //检测花名册是否有蓄电任务 值班任务 审批流程 + staffRosterService.deleteBatchData(List.of(id)); + return ActionResult.success(); + } + + + /** + * 获取员工花名册详情 + * + * @param id + * @return + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + FtbPersonnelsStaffRosterDto info = staffRosterService.getInfo(id); + return ActionResult.success(info); + } + + /** + * 获取员工花名册详情(参数) + * + * @param userId 用户ID + * @return + */ + @GetMapping("/getInfo") + public ActionResult getInfo(@RequestParam("userId") String userId) { + StaffHomeDto info = staffRosterService.queryWorkerHomeDetail(userId); + return ActionResult.success(info); + } + + + /** + * 查询员工主页信息 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/worker-home-detail/{userId}") + public ActionResult queryWorkerHomeDetail(@PathVariable("userId") String userId) { + StaffHomeDto info = staffRosterService.queryWorkerHomeDetail(userId); + return ActionResult.success(info); + } + /** + * 查询员工主页信息 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/worker-home-detail") + public ActionResult queryWorkerHomeDetailWithId(@RequestParam("userId") String userId) { + StaffHomeDto info = staffRosterService.queryWorkerHomeDetail(userId); + return ActionResult.success(info); + } + + + /** + * 查询员工成长列表 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/query-growth-list/{userId}") + public ActionResult> queryGrowthList(@PathVariable("userId") String userId) { + List list = growthLogService.queryAll(userId); + return ActionResult.success(list); + } + + + /** + * 查询员工薪酬变化列表 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/query-salary-change-list/{userId}") + public ActionResult> querySalaryChangeList(@PathVariable("userId") String userId) { + List list = staffSalaryChangeLogService.queryAll(userId); + return ActionResult.success(list); + } + + /** + * 检测当前登录用户是不是档案管理员 + * + * @return {@link ActionResult}<{@link String}> + */ + @GetMapping("/check-my-is-archive-manager") + public ActionResult checkMyIsArchiveManager() { + Boolean isManager = staffRosterService.checkCurrLoginUserArchiveManager(); + return ActionResult.success("成功", isManager); + } + + /** + * 检测当前花名册处于流程中【调岗、转正、离职】 + * + * @param id 花名册ID + * @return 0、无流程 ,1转正 2,调岗, 3离职, 4晋升5调店 6降职 + * 于 + */ + @GetMapping("/check-roster-run-task/{id}") + public ActionResult checksItInTheReviewProcess(@PathVariable("id") String id) { + String loginUserId = UserProvider.getLoginUserId(); + Integer ty = staffRosterService.checksItInTheReviewProcess(id); + return ActionResult.success("成功", ty); + } + /** + * 检测当前花名册处于流程中【调岗、转正、离职】 + * + * @param id 花名册ID + * @return 0、无流程 ,1转正 2,调岗, 3离职, 4晋升5调店 6降职 + * 于 + */ + @GetMapping("/check-roster-run-task") + public ActionResult checksItInTheReviewProcessWithId(@RequestParam("id") String id) { + Integer ty = staffRosterService.checksItInTheReviewProcess(id); + return ActionResult.success("成功", ty); + } + + + /** + * 查询直属主管 + * + * @param dto + * @return + */ + @GetMapping("/queryAllUserForOrgAndStatus") + public ActionResult> getAllUser(QueryUserListDTO dto) { + List list = staffRosterService.queryAllUserForOrgAndStatus(dto); + return ActionResult.success("成功", list); + } + + /** + * 查看培训数据 + * 培训v1.1 + * + * @param postId 岗位Id + * @param userId 用户id + */ + @GetMapping("/queryTrainData") + public ActionResult queryTrainData(@RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + FtbCultivatePromotionWithPersonelVO info = staffRosterService.queryTrainData(userId, postId); + return ActionResult.success("成功", info); + } + + /** + * 查看课程培训数据(培训v1.1) + */ + @GetMapping("/queryCourseTrainDataDetail") + public ActionResult> queryCourseTrainDataDetail(FtbCultivatePositionForAppNewDTO dto, + CultivatePage page) { + Page result = page.coverCultivatePage(); + result = positionForAppService.subordinateLearningCourses(dto, result); + return ActionResult.success(CultivatePage.coverPageList(result)); + } + + /** + * 查看鉴定培训数据(培训v1.1) + */ + @GetMapping("/queryIdentityTrainDataDetail") + public ActionResult> queryIdentityTrainDataDetail(FtbCultivatePositionForAppNewDTO dto, + CultivatePage page) { + PageListVO result = positionForAppService.queryIdentityTrainDataDetail(dto, page); + return ActionResult.success(result); + } + + /** + * 修改用户基本信息[头像 姓名 花名] + * + * @return + */ + + @PutMapping("/updateBaseUserInfo") + public ActionResult updateBaseUserInfo(@RequestBody EditUserBaseInfoDto req) { + String loginUserId = UserProvider.getLoginUserId(); + if (StringUtils.isEmpty(req.getName()) && StringUtils.isEmpty(req.getHeadLogo()) && StringUtils.isEmpty(req.getFlowerName())) { + return ActionResult.fail("用户信息不能为空"); + } + if(StringUtils.isNotEmpty(req.getName())){ + Boolean aBoolean = staffRosterService.canUpdateRealName(); + if(aBoolean==false){ + throw new RuntimeException("对不起,当前企业不支持修改用户姓名"); + } + } + + staffRosterService.updateBaseUserInfo(loginUserId, req); + return ActionResult.success("操作成功", true); + } + + + /** + * 修改用户离职证明是否签署 + * @param userId 用户ID + * @param status 签署状态 0-未签署 1-已签署 + * @return + */ + @PutMapping("/updateSignSeparation/{userId}/{status}") + public ActionResult updateSignSeparation(@PathVariable("userId") String userId,@PathVariable("status") Integer status) { + staffRosterService.updateSignSeparation(userId, status); + return ActionResult.success(); + } + + /** + * 查询用户离职证明是否签署 + * @param userId 用户ID + * @return + */ + @GetMapping("/querySignSeparation/{userId}") + public ActionResult querySignSeparation(@PathVariable("userId") String userId) { + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByUserId(userId); + if(null==roster){ + throw new RuntimeException("用户信息不存在"); + } + return ActionResult.success(roster.getIsSignSeparation()); + } + + + /** + * 查询是否能够修改用户的真实姓名(true-可以修改,false-不可以修改) + * @return true 可以修改,false 不可以修改 + */ + @GetMapping("/queryCanUpdateRealName") + public ActionResult queryCanUpdateRealName() { + Boolean check = staffRosterService.canUpdateRealName(); + return ActionResult.success("查询成功", check); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/secondment/FtbPersonnelsSecondmentAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/secondment/FtbPersonnelsSecondmentAppController.java new file mode 100644 index 0000000..b8be4f6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/secondment/FtbPersonnelsSecondmentAppController.java @@ -0,0 +1,94 @@ +package jnpf.personnels.controller.app.secondment; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.secondment.FtbHandleSecondmentDTO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentInfoVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * app借调管理 + * @Author: peng.hao + * @create: 2025/9/11 + */ +@RestController +@RequestMapping("/app/secondment") +public class FtbPersonnelsSecondmentAppController { + + @Resource + private FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentService; + + /** + * 列表查询 + */ + @GetMapping("/list-query") + public ActionResult> pageList(PersonnelsQueryDTO dto, + CultivatePage page) { + PageListVO pageListVO = ftbPersonnelsSecondmentService.pageList(dto, page); + return ActionResult.success(pageListVO); + } + + /** + * 详情 + */ + @GetMapping("/details/{id}") + public ActionResult details(@PathVariable String id) { + return ActionResult.success(ftbPersonnelsSecondmentService.details(id)); + } + + /** + * 办理借调 + */ + @PostMapping("/handle-secondment") + public ActionResult handleSecondment(@RequestBody @Validated FtbHandleSecondmentDTO ftbHandleSecondmentDTO) { + return ActionResult.success("", ftbPersonnelsSecondmentService.handleSecondment(ftbHandleSecondmentDTO)); + } + /** + * 借调删除 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable String id) { + ftbPersonnelsSecondmentService.delete(id); + return ActionResult.success(); + } + /** + * 借调撤回 + */ + @PostMapping("/revoke") + public ActionResult secondmentWithdrawal(@RequestBody FtbPersonnelsAuditDto ftbPersonnelsAuditDto) { + ftbPersonnelsSecondmentService.secondmentWithdrawal(ftbPersonnelsAuditDto); + return ActionResult.success(); + } + + /** + * 提前结束 + */ + @PutMapping("/early-end/{id}") + public ActionResult earlyEnd(@RequestBody Map map) { + ftbPersonnelsSecondmentService.earlyEnd(map); + return ActionResult.success(); + } + + /** + * 延长借调时间 + *{ + * "id": "借调id", + * "endTime" : "最新借调结束时间" + * } + */ + @PutMapping("/delay-time") + public ActionResult delayTime(@RequestBody Map map) { + ftbPersonnelsSecondmentService.delayTime(map); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsStaffTransferPositionAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsStaffTransferPositionAppController.java new file mode 100644 index 0000000..ce279a0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsStaffTransferPositionAppController.java @@ -0,0 +1,174 @@ +package jnpf.personnels.controller.app.transfer; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.entity.StoreEntity; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionCountDto; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionDto; +import jnpf.model.personnels.req.employment.EmploymentApplyCheckDto; +import jnpf.model.personnels.req.transfer.AppQueryTransferListReq; +import jnpf.model.personnels.req.transfer.SaveTransferReq; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.personnels.service.FtbPersonnelsStaffTransferPositionService; +import jnpf.store.service.StoreService; +import jnpf.util.FtbUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * app员工调岗表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/app/personnels-staff-transfer-position") +public class FtbPersonnelsStaffTransferPositionAppController { + + @Autowired + private FtbPersonnelsStaffTransferPositionService transferPositionService; + + @Resource + StoreService storeService; + + @Autowired + OrganizeApi organizeApi; + /** + * 办理调岗列表/我的调岗 + * + * @param req + * @return {@link ActionResult}<{@link TransferPositionDto}> + */ + @GetMapping("/query-list") + public ActionResult> pageLists(@Validated AppQueryTransferListReq req) { + PageInfo pageVo = transferPositionService.getAppPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 我的审批状态数量统计 + * + * @param + * @return {@link ActionResult}<{@link TransferPositionDto}> + */ + @GetMapping("/query-myCheck-count") + public ActionResult queryMyCheckCount() { + TransferPositionCountDto dto = transferPositionService.queryAppMyCheckCount(); + return ActionResult.success("成功", dto); + } + + /** + * 我的审批,抄送,小气泡统计数 + * @param flag "1" 我的审批 "2" 抄送 + */ + @GetMapping("/getListCont") + public ActionResult getListCont(@RequestParam String flag) { + FtbPersonnelsBubbleCountVO countVO = transferPositionService.getListCont(flag); + return ActionResult.success(countVO); + } + + /** + * 我的抄送状态数量统计 + * + * @param + * @return {@link ActionResult}<{@link TransferPositionDto}> + */ + @GetMapping("/query-mySend-count") + public ActionResult queryMySendCount() { + TransferPositionCountDto dto = transferPositionService.queryAppMySendCount(); + return ActionResult.success("成功", dto); + } + + + /** + * 办理调岗 + * + * @param req 调岗参数 + * @return v1.1 返回 1 标识为门店负责人,需要进行替换 + */ + @PostMapping("/deal-transfer") + public ActionResult dealTransfer(@Validated @RequestBody SaveTransferReq req) { + String string = transferPositionService.insertData(req); + return ActionResult.success("",string); + } + /** + * 查看调岗详细 + * + * @param id + * @return + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + TransferPositionDto info = transferPositionService.getInfo(id); + return ActionResult.success(info); + } + /** + * 查看调岗详细 + * + * @param id + * @return + */ + @GetMapping("/get") + public ActionResult getWithId(@RequestParam("id") String id) { + TransferPositionDto info = transferPositionService.getInfo(id); + return ActionResult.success(info); + } + + /** + * 审批 + * + * @param id 主键id,必传 + * @return + */ + @PutMapping("/approval/{id}") + public ActionResult approval(@PathVariable("id") String id, @RequestBody EmploymentApplyCheckDto dto) { + dto.setId(id); + transferPositionService.approval( dto); + return ActionResult.success(); + } + + + /** + * 重新审批 + * + * @param id 主键id,必传 + * @return + */ + + @PutMapping("/re-approval/{id}") + public ActionResult reApproval(@PathVariable("id") String id, @RequestBody SaveTransferReq dto) { + transferPositionService.reApproval(id, dto); + return ActionResult.success(); + } + /** + * 获取门店负责人详情 + * @param userId 门店负责人id + * @return organizeName 组织名称 storeName 门店名称 + */ + @GetMapping("/getStoreManagerInfo") + public ActionResult>> getStoreManagerInfo(@RequestParam("userId") String userId) { + LambdaQueryWrapper objectLambdaQueryWrapper = Wrappers.lambdaQuery(); + objectLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid,userId); + List list = storeService.list(objectLambdaQueryWrapper); + List> maps = list.stream().map(item -> { + Map map = Maps.newHashMap(); + OrganizeEntity organize = organizeApi.getInfoById(item.getOrganizeid()); + map.put("organizeName", organize.getFullName()); + map.put("storeName", item.getStorename()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(maps); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsTransferManageAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsTransferManageAppController.java new file mode 100644 index 0000000..8a6a742 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/transfer/FtbPersonnelsTransferManageAppController.java @@ -0,0 +1,68 @@ +package jnpf.personnels.controller.app.transfer; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.req.transfer.FtbHandleTransferDTO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferPageVO; +import jnpf.personnels.service.FtbPersonnelsTransferManageService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * app人事调离调动管理模块 + */ +@RestController +@RequestMapping("/app/ftb-personnels-transfer-manage") +public class FtbPersonnelsTransferManageAppController { + + @Resource + private FtbPersonnelsTransferManageService ftbPersonnelsTransferManageService; + + /** + * 办理调动,直接办理 + */ + @PostMapping("/handle-transfer") + public ActionResult handleTransfer(@RequestBody @Validated FtbHandleTransferDTO ftbHandleTransferDTO) { + String handleTransfer = ftbPersonnelsTransferManageService.handleTransfer(ftbHandleTransferDTO); + return ActionResult.success("操作成功", handleTransfer); + } + + /** + * 删除调动记录 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbPersonnelsTransferManageService.delete(id); + return ActionResult.success(); + } + + /** + * 办理调动列表 + */ + @GetMapping("/transfer-list") + public ActionResult> transferListApp(CultivatePage cultivatePage, String userName) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbPersonnelsTransferManageService.transferListApp(page, userName); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + + /** + * 我的调动 + */ + @GetMapping("/my-transfer") + public ActionResult> myTransferApp(CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbPersonnelsTransferManageService.myTransferApp(page); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/turnover/FtbPersonnelsTurnoverManagementForAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/turnover/FtbPersonnelsTurnoverManagementForAppController.java new file mode 100644 index 0000000..0dcb630 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/turnover/FtbPersonnelsTurnoverManagementForAppController.java @@ -0,0 +1,127 @@ +package jnpf.personnels.controller.app.turnover; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverCreateDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverDTO; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverInfoVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * app离职管理模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/app/personnels-turnover") +public class FtbPersonnelsTurnoverManagementForAppController { + + @Resource + FtbPersonnelsTurnoverManagementService service; + + /** + * 获取人事变动管理中的人员流动列表 + * + * @param dto 人事变动查询参数 + * @return 查询结果 + */ + @GetMapping("/list-query") + public ActionResult> getTurnoverList(FtbPersonnelsForAppQueryDTO dto, + CultivatePage page){ + PageListVO listVO = service.getTurnoverForList(dto,page); + return ActionResult.success(listVO); + } + /** + * 我的审批,抄送,小气泡统计数 + * @param flag "1" 我的审批 "2" 抄送 + */ + @GetMapping("/getListCont") + public ActionResult getListCont(@RequestParam String flag) { + FtbPersonnelsBubbleCountVO countVO = service.getListCont(flag); + return ActionResult.success(countVO); + } + + /** + * 获取辞职审批流程详情 + * + * @param id 流程id + * @return 返回辞职审批流程详情结果 + */ + @GetMapping("/details-process/{id}") + public ActionResult detailsOfResignationApprovalProcess(@PathVariable("id")String id){ + FtbPersonnelsTurnoverInfoVO info = service.detailsOfResignationApprovalProcess(id); + return ActionResult.success(info); + } + + + /** + * 申请辞职 + * + * @param createDTO 创建DTO对象 + * @return + */ + @PostMapping("/apply-resignation") + public ActionResult applyForResignation(@Validated @RequestBody FtbPersonnelsTurnoverCreateDTO createDTO){ + String s = service.applyForResignation(createDTO); + return ActionResult.success("成功",s); + } + + /** + * 撤销辞职申请 + * + * @param id 应用程序的ID + * @return 返回String类型的ActionResult对象 + */ + @PutMapping("/withdraw-application/{id}") + public ActionResult withdrawResignationApplication(@PathVariable("id")String id){ + service.withdrawResignationApplication(id, null); + return ActionResult.success(); + } + /** + * 审核离职申请 + * + * @param auditDto 审核DTO对象 + * @return "1" 为是否为审批人 2 是否为门店负责人 3是否存在角色组 4存在下属 + */ + @PutMapping("/review-resignation-application") + public ActionResult ReviewResignationApplication(@RequestBody FtbPersonnelsTurnoverDTO auditDto ){ + String string = service.reviewResignationApplication(auditDto); + return ActionResult.success("",string); + } + + + /** + * 审核离职申请 + * + * @param auditDto 审核DTO对象 + * @return "1" 为是否为审批人 2 是否为门店负责人 3是否存在角色组 4存在下属 + */ + @PostMapping("/review-resignation-application") + public ActionResult ReviewResignationApplicationWithPost(@RequestBody FtbPersonnelsTurnoverDTO auditDto ){ + String string = service.reviewResignationApplication(auditDto); + return ActionResult.success("",string); + } + + /** + * 获取用户绑定审批结果 + * @param flag 用户离职申请信息 + * @return 动态数据 + */ + @GetMapping("/get-user-bound-approval") + public ActionResult>> getUserBoundApproval(@RequestParam("flag") String flag, + @RequestParam("userId") String userId){ + return ActionResult.success(service.getUserBoundApproval(flag,userId)); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/uchisuike/FtbPersonnelsUchisuikePondAppController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/uchisuike/FtbPersonnelsUchisuikePondAppController.java new file mode 100644 index 0000000..80ad864 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/app/uchisuike/FtbPersonnelsUchisuikePondAppController.java @@ -0,0 +1,41 @@ +package jnpf.personnels.controller.app.uchisuike; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.uchisuike.app.FtbRecommendationInvitationAppDTO; +import jnpf.personnels.service.FtbPersonnelsUchisuikePondService; +import jnpf.util.NoDataSourceBind; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * app人事调离内推池模块 + * + * @author wcx + */ +@RestController +@RequestMapping("/app/personnels-uchisuike-pond") +public class FtbPersonnelsUchisuikePondAppController { + + @Resource + private FtbPersonnelsUchisuikePondService ftbPersonnelsUchisuikePondService; + + /** + * 内推邀请填写 + * + * @param ftbRecommendationInvitationAppDTO + * @return {@link ActionResult} + */ + @PostMapping("/invite-fill-in") + @NoDataSourceBind + public ActionResult internalRecommendationInvitationAdded(@RequestBody @Validated FtbRecommendationInvitationAppDTO ftbRecommendationInvitationAppDTO) { + ftbPersonnelsUchisuikePondService.internalRecommendationInvitationAdded(ftbRecommendationInvitationAppDTO); + return ActionResult.success(); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/mcp/FtbPersonStaffMCPController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/mcp/FtbPersonStaffMCPController.java new file mode 100644 index 0000000..8dde2e1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/mcp/FtbPersonStaffMCPController.java @@ -0,0 +1,98 @@ +package jnpf.personnels.controller.mcp; + +import com.fantaibao.permission.annotation.FtbCheckPermission; +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.mcp.OnboardingThisMonthStatsVO; +import jnpf.model.personnels.vo.mcp.StaffBirthdayVO; +import jnpf.model.personnels.vo.mcp.StaffNotSignContractVO; +import jnpf.model.personnels.vo.mcp.SubmittedButNotOnboardedVO; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.personnels.service.FtbPersonStaffMCPService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 人事花名册mcp模块 + * @author wcx + * @date 2026/05/06 + */ +@RestController +@RequestMapping("/mcp-ai/person-staff") +public class FtbPersonStaffMCPController { + + @Resource + private FtbPersonStaffMCPService ftbPersonStaffMCPService; + + /** + * 查询已提交登记表但未办理入职的人员 + * @return 已提交登记表但未办理入职的人员列表 + */ + @GetMapping("/submitted-but-not-onboarded") + @FtbCheckPermission("entryManagea") + public ActionResult> getSubmittedButNotOnboarded() { + List list = ftbPersonStaffMCPService.getSubmittedButNotOnboarded(); + SubmittedButNotOnboardedVO result = new SubmittedButNotOnboardedVO<>(); + result.setTotal(list.size()); + result.setList(list); + return ActionResult.success(result); + } + + /** + * 入职统计:newEmployeesThisMonth 为入职管理表在指定时间段内新增(按创建时间); + * completedOnboardingThisMonth 为其中在指定时间段内已在花名册完成入职流程的人数(审批通过/直接办理已到岗/花名册导入等口径) + * @param startTime 开始时间(格式:yyyy-MM-dd) + * @param endTime 结束时间(格式:yyyy-MM-dd) + */ + @GetMapping("/count-onboarded-this-month") + @FtbCheckPermission("entryManagea") + public ActionResult getOnboardingThisMonthStats( + @RequestParam(value = "startTime", required = false) String startTime, + @RequestParam(value = "endTime", required = false) String endTime) { + return ActionResult.success(ftbPersonStaffMCPService.getOnboardingThisMonthStats(startTime, endTime)); + } + + /** + * 查询未提交登记表的员工列表 + * @return 未提交登记表的员工列表(包含姓名、电话等详细信息) + */ + @GetMapping("/list-not-submitted-form") + @FtbCheckPermission("entryManagea") + public ActionResult> getListOfNotSubmittedForm() { + return ActionResult.success(ftbPersonStaffMCPService.getListOfNotSubmittedForm()); + } + + /** + * 查询本月生日的员工列表 + * @return 本月生日员工列表(包含姓名、组织、岗位) + */ + @GetMapping("/birthday-this-month") + @FtbCheckPermission("employeeRoster") + public ActionResult> getStaffBirthdayList() { + return ActionResult.success(ftbPersonStaffMCPService.getStaffBirthdayList()); + } + + /** + * 查询手机号是否在黑名单中 + * @param phone 手机号 + * @return true为在黑名单中,false为不在黑名单中 + */ + @GetMapping("/check-blacklist") + @FtbCheckPermission("blacklistManagement") + public ActionResult checkBlacklist(@RequestParam("phone") String phone) { + return ActionResult.success(ftbPersonStaffMCPService.isInBlacklist(phone)); + } + + /** + * 查询入职半月还未签署合同的员工列表 + * @return 未签署合同的已入职员工列表 + */ + @GetMapping("/not-signed-contract-half-month") + public ActionResult> getListNotSignedContractHalfMonth() { + return ActionResult.success(ftbPersonStaffMCPService.getListNotSignedContractHalfMonth()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelChangesController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelChangesController.java new file mode 100644 index 0000000..6ae2c06 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelChangesController.java @@ -0,0 +1,102 @@ +package jnpf.personnels.controller.web.analysis; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.personnels.service.FtbPersonnelChangesService; +import jnpf.personnels.utils.PersonnelDataAnalysisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.List; + +/** + * web人事异动分析模块 + * @author wangchunxiang + * @date 2025/04/08 + */ +@RestController +@Slf4j +@RequestMapping("/web/change-analysis") +public class FtbPersonnelChangesController { + + @Resource + private FtbPersonnelChangesService ftbPersonnelChangesService; + + /** + * 人事异动调店人数同比环比 + */ + @PostMapping("/get-reg-shop-adjustment") + public ActionResult getBrainDrainCorrectRateShopAdjustment(@RequestBody FtbPersonnlesAnalysisDTO dto) { + FtbPersonnlesBrainShopAdjustmentVO result = ftbPersonnelChangesService.getBrainDrainCorrectRateShopAdjustment(dto); + return ActionResult.success(result); + } + + /** + * 人事异动调岗人数同比环比 + */ + @PostMapping("/get-reg-transfer-post") + public ActionResult getBrainDrainCorrectRateTransferPost(@RequestBody FtbPersonnlesAnalysisDTO dto) { + FtbPersonnlesBrainShopAdjustmentVO result = ftbPersonnelChangesService.getBrainDrainCorrectRateTransferPost(dto); + return ActionResult.success(result); + } + + /** + * 人事异动调店人数 门店维度,部门维度,岗位维度 + */ + @PostMapping("/get-regularization-store-dimension-shop-adjustment") + public ActionResult> getRegularizationStoreDimensionShopAdjustment(@RequestBody FtbPersonnlesAnalysisDTO dto) { + List result = ftbPersonnelChangesService.getRegularizationStoreDimensionShopAdjustment(dto); + return ActionResult.success(result); + } + + /** + * 人事异动调岗人数 门店维度,部门维度,岗位维度 + */ + @PostMapping("/get-regularization-store-dimension-transfer-post") + public ActionResult> getRegularizationStoreDimensionTransferPost(@RequestBody FtbPersonnlesAnalysisDTO dto) { + List result = ftbPersonnelChangesService.getRegularizationStoreDimensionTransferPost(dto); + return ActionResult.success(result); + } + + /** + * 调店人数分布列表 + */ + @PostMapping("/shop-adjustment-details") + public ActionResult shopAdjustmentDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelChangesService.shopAdjustmentDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsShopAdjustmentDetailsVO.class)); + } + + /** + * 调店人数分布列表导出 + */ + @PostMapping("/shop-adjustment-details-export") + public void shopAdjustmentDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelChangesService.shopAdjustmentDetailsExport(dto); + } + + /** + * 调岗人数分布列表 + */ + @PostMapping("/post-adjustment-details") + public ActionResult postAdjustmentDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelChangesService.postAdjustmentDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsPostAdjustmentDetailsVO.class)); + } + + /** + * 调岗人数分布列表导出 + */ + @PostMapping("/post-adjustment-details-export") + public void postAdjustmentDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelChangesService.postAdjustmentDetailsExport(dto); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsBrainDrainController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsBrainDrainController.java new file mode 100644 index 0000000..9390fed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsBrainDrainController.java @@ -0,0 +1,129 @@ +package jnpf.personnels.controller.web.analysis; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.personnels.service.FtbPersonnelsBrainDrainService; +import jnpf.personnels.utils.PersonnelDataAnalysisUtil; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * web统计人才流失 + * @Author: peng.hao + * @create: 2025/4/8 + */ +@RestController +@RequestMapping("/web/brain-drain") +public class FtbPersonnelsBrainDrainController { + + @Resource + private FtbPersonnelsBrainDrainService ftbPersonnelsBrainDrainService; + + /** + * 人事异动转正人数同比环比 + */ + @PostMapping("/get-reg-comparison") + public ActionResult getBrainDrainCorrectRateComparison(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsBrainDrainService.getBrainDrainCorrectRateComparison(dto)); + } + + /** + * 人事异动新人率人数同比环比 + */ + @PostMapping("/get-reg-NewcomerRate") + public ActionResult getBrainDrainCorrectRateNewcomerRate(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsBrainDrainService.getBrainDrainCorrectRateNewcomerRate(dto)); + } + /** + * 人事异动入职人数 门店维度,部门维度,岗位维度 + */ + @PostMapping("/get-onboarding-store-dimension") + public ActionResult> getOnboardingStoreDimension(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsBrainDrainService.getOnboardingStoreDimension(dto)); + } + + /** + * 人事异动转正人数 门店维度,部门维度,岗位维度 + */ + @PostMapping("/get-regularization-store-dimension") + public ActionResult> getRegularizationStoreDimension(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsBrainDrainService.getRegularizationStoreDimension(dto)); + } + + /** + * 人事异动离职人数 门店维度,部门维度,岗位维度 + */ + @PostMapping("/get-turnover-store-dimension") + public ActionResult> getTurnoverStoreDimension(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsBrainDrainService.getTurnoverStoreDimension(dto)); + } + + /** + * 人事异动新人率 岗位维度、门店维度、部门维度 进行展示 + */ + @PostMapping("/get-new-hire-rate-dimension") + public ActionResult> getNewHireRateDimension(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsBrainDrainService.getNewHireRateDimension(dto)); + } + + + + /** + * 入职明细 + */ + @PostMapping("/get-onboarding-detail") + public ActionResult getOnboardingDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsBrainDrainService.getOnboardingDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsOnboardingDetailVO.class)); + } + + /** + * 入职明细导出 + */ + @PostMapping("/get-onboarding-detail-export") + public void getOnboardingDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsBrainDrainService.getOnboardingDetailExport(dto, response); + } + + /** + * 转正明细 + */ + @PostMapping("/get-regularization-detail") + public ActionResult getRegularizationDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsBrainDrainService.getRegularizationDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsRegularizationDetailVO.class)); + } + + /** + * 转正明细导出 + */ + @PostMapping("/get-regularization-detail-export") + public void getRegularizationDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsBrainDrainService.getRegularizationDetailExport(dto, response); + } + /** + * 新人率占比明细 + */ + @PostMapping("/get-new-hire-rate-detail") + public ActionResult getNewHireRateDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsBrainDrainService.getNewHireRateDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsNewHireRateDetailVO.class)); + } + + /** + * 新人率占比明细导出 + */ + @PostMapping("/get-new-hire-rate-detail-export") + public void getNewHireRateDetailExport(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsBrainDrainService.getNewHireRateDetailExport(dto, response); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsOverviewAnalysisController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsOverviewAnalysisController.java new file mode 100644 index 0000000..144d648 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsOverviewAnalysisController.java @@ -0,0 +1,533 @@ +package jnpf.personnels.controller.web.analysis; + + +import cn.hutool.core.date.DateUtil; +import jnpf.attendance.FtbStatisticsApi; +import jnpf.attendance.dto.AttendanceCountAvgHoursDto; +import jnpf.attendance.dto.AttendanceCountAvgHoursVo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.personnels.service.FtbPersonnelsOverviewAnalysisService; +import jnpf.personnels.service.FtbPersonnelsTurnoverAnalysisService; +import jnpf.personnels.utils.PersonnelDataAnalysisUtil; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * web数据看板-人员概况 + * + * @author wangchunxiang + * @date 2024/10/16 + */ +@RestController +@RequestMapping("/web/personnel-profile-analysis") +public class FtbPersonnelsOverviewAnalysisController { + + @Resource + private FtbPersonnelsOverviewAnalysisService ftbPersonnelOverviewAnalysisService; + + @Autowired + private FtbStatisticsApi ftbStatisticsApi; + + @Resource + FtbPersonnelsTurnoverAnalysisService ftbPersonnelsTurnoverAnalysisService; + + + /** + * 在职员工 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/current-employees") + public ActionResult currentEmployees(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PersonnelsOverviewEmployeesVO personnelsOverviewEmployeesVO = ftbPersonnelOverviewAnalysisService.currentEmployees(dto); + return ActionResult.success(personnelsOverviewEmployeesVO); + } + + /** + * 在职员工柱状图 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/status-employees") + public ActionResult> statusOfCurrentEmployees(@RequestBody FtbPersonnlesAnalysisDTO dto) { + List personnelsOverviewEmployeesVO = ftbPersonnelOverviewAnalysisService.statusOfCurrentEmployees(dto); + return ActionResult.success(personnelsOverviewEmployeesVO); + } + + /** + * 在职员工列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/status-employees-details") + public ActionResult statusOfCurrentEmployeesDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.statusOfCurrentEmployeesDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewEmployeesDetailsVO.class)); + } + + /** + * 在职员工列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/status-employees-details-export") + public void statusOfCurrentEmployeesDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.statusOfCurrentEmployeesDetailsExport(dto); + } + + + /** + * 平均年龄 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/composite-life") + public ActionResult compositeLife(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PersonnelsOverviewEmployeesVO personnelsOverviewEmployeesVO = ftbPersonnelOverviewAnalysisService.compositeLife(dto); + return ActionResult.success(personnelsOverviewEmployeesVO); + } + + /** + * 平均司龄 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/average-age") + public ActionResult averageAge(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PersonnelsOverviewEmployeesAgeVO personnelsOverviewEmployeesVO = ftbPersonnelOverviewAnalysisService.averageAge(dto); + return ActionResult.success(personnelsOverviewEmployeesVO); + } + + /** + * 性别分布 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/gender-distribution") + public ActionResult> genderDistribution(@RequestBody FtbPersonnlesAnalysisDTO dto) { + List personnelsOverviewEmployeesVO = ftbPersonnelOverviewAnalysisService.genderDistribution(dto); + return ActionResult.success(personnelsOverviewEmployeesVO); + } + + /** + * 性别分布列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/gender-distribution-details") + public ActionResult genderDistributionDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.genderDistributionDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewProportionDetailsVO.class)); + } + + /** + * 性别分布列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/gender-distribution-details-export") + public void genderDistributionDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.genderDistributionDetailsExport(dto); + } + + /** + * 员工状态占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/employee-status-proportion") + public ActionResult> employeeStatusProportion(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.employeeStatusProportion(dto)); + } + + /** + * 员工状态占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/employee-status-proportion-details") + public ActionResult employeeStatusProportionDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.employeeStatusProportionDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewSituationDetailsVO.class)); + } + + /** + * 员工状态占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/employee-status-proportion-details-export") + public void employeeStatusProportionDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.employeeStatusProportionDetailsExport(dto); + } + + /** + * 用工类型占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-employment-types") + public ActionResult> proportionOfEmploymentTypes(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.proportionOfEmploymentTypes(dto)); + } + + /** + * 员工类型占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-employment-types-details") + public ActionResult proportionOfEmploymentTypesDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.proportionOfEmploymentTypesDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewTypeDetailsVO.class)); + } + + /** + * 员工类型占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-employment-types-details-export") + public void proportionOfEmploymentTypesDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.proportionOfEmploymentTypesDetailsExport(dto); + } + + + /** + * 年龄分布占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/age-distribution-proportion") + public ActionResult> ageDistributionProportion(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.ageDistributionProportion(dto)); + } + + /** + * 年龄分布占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/age-distribution-proportion-details") + public ActionResult ageDistributionProportionDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.ageDistributionProportionDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewAgeDetailsVO.class)); + } + + /** + * 年龄分布占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/age-distribution-proportion-details-export") + public void ageDistributionProportionDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.ageDistributionProportionDetailsExport(dto); + } + + /** + * 司龄分布 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/company-distribution") + public ActionResult> ageDistribution(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.ageDistribution(dto)); + } + + /** + * 司龄分布占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/company-distribution-details") + public ActionResult companyDistributionDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.companyDistributionDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewCompanyDetailsVO.class)); + } + + /** + * 司龄分布占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/company-distribution-details-export") + public void companyDistributionDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) { + ftbPersonnelOverviewAnalysisService.companyDistributionDetailsExport(dto); + } + + /** + * 学历占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/educational-background") + public ActionResult> educationalBackground(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.educationalBackground(dto)); + } + + /** + * 学历占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/educational-background-details") + public ActionResult educationalBackgroundDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.educationalBackgroundDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewEducationDetailsVO.class)); + } + + /** + * 学历占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/educational-background-details-export") + public void educationalBackgroundDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.educationalBackgroundDetailsExport(dto); + } + + /** + * 部门人数占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/department-headcount-ratio") + public ActionResult> departmentHeadcountRatio(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.departmentHeadcountRatio(dto)); + } + + /** + * 门店人数占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-of-store-numbers") + public ActionResult> proportionOfStoreNumbers(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.proportionOfStoreNumbers(dto)); + } + + /** + * 岗位人数占比 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-job-headcount") + public ActionResult> proportionOfJobHeadcount(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelOverviewAnalysisService.proportionOfJobHeadcount(dto)); + } + + /** + * 部门人数占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/department-headcount-ratio-details") + public ActionResult departmentHeadcountRatioDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.departmentHeadcountRatioDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewDepartmentDetailsVO.class)); + } + + /** + * 部门人数占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/department-headcount-ratio-details-export") + public void departmentHeadcountRatioDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.departmentHeadcountRatioDetailsExport(dto); + } + + /** + * 门店人数占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-of-store-numbers-details") + public ActionResult proportionOfStoreNumbersDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.proportionOfStoreNumbersDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewStoreDetailsVO.class)); + } + + /** + * 门店人数占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-of-store-numbers-details-export") + public void proportionOfStoreNumbersDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.proportionOfStoreNumbersDetailsExport(dto); + } + + /** + * 岗位人数占比列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-job-headcount-details") + public ActionResult proportionOfJobHeadcountDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.proportionOfJobHeadcountDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewJobDetailsVO.class)); + } + + /** + * 岗位人数占比列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/proportion-job-headcount-details-export") + public void proportionOfJobHeadcountDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.proportionOfJobHeadcountDetailsExport(dto); + } + + + /** + * 计算考勤组平均工时(实际出勤工时) + */ + @PostMapping(value = "/countAttendanceAvgHours") + public ActionResult> countAttendanceAvgHours(@RequestBody FtbPersonnlesAnalysisDTO dto) { + AttendanceCountAvgHoursDto attendanceCountAvgHoursDto = new AttendanceCountAvgHoursDto(); + attendanceCountAvgHoursDto.setOrgIds(dto.getOrganizationId()); + attendanceCountAvgHoursDto.setStartMonth(DateUtil.format(dto.getStartTime(), "yyyy-MM")); + attendanceCountAvgHoursDto.setEndMonth(DateUtil.format(dto.getEndTime(), "yyyy-MM")); + List attendanceCountAvgHoursVos = ftbStatisticsApi.countAttendanceAvgHours(attendanceCountAvgHoursDto); + return ActionResult.success(attendanceCountAvgHoursVos); + } + + + /** + * 离职率情况 + */ + @PostMapping("/leave-rate") + public ActionResult leaveRate(@RequestBody FtbPersonnlesAnalysisDTO dto){ + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.ftbPersonnelsTurnoverAnalysisService(dto)); + } + + /** + * 离职率明细 + */ + @PostMapping("/leave-rate-detail") + public ActionResult leaveRateDetail(@RequestBody FtbPersonnlesAnalysisDTO dto){ + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeaveRateDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverDetailVO.class)); + } + + /** + * 离职率明细-导出 + */ + @PostMapping("/leave-rate-detail-export") + public void leaveRateDetailExport(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) throws Exception { + dto.setPageSize(-1); + List detailVOS = ftbPersonnelsTurnoverAnalysisService.getLeaveRateDetail(dto).getList(); + EasyExcelUtils.exportExcel(response, "离职率明细", detailVOS,FtbPersonnelsTurnoverDetailVO. class); + } + + /** + * 工龄分布统计 + */ + @PostMapping("/seniority-distribution") + public ActionResult> seniorityDistribution(@RequestBody FtbPersonnlesAnalysisDTO dto) { + List re = ftbPersonnelOverviewAnalysisService.seniorityDistribution(dto); + return ActionResult.success(re); + } + + /** + * 保险购买分析 + */ + @PostMapping("/insurance-purchase-analysis") + public ActionResult> insurancePurchaseAnalysis(@RequestBody FtbPersonnlesAnalysisDTO dto) { + List result = ftbPersonnelOverviewAnalysisService.insurancePurchaseAnalysis(dto); + return ActionResult.success(result); + } + + /** + * 工龄分布统计列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/seniority-distribution-details") + public ActionResult seniorityDistributionDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.seniorityDistributionDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewSeniorityDetailsVO.class)); + } + + /** + * 工龄分布统计列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/seniority-distribution-details-export") + public void seniorityDistributionDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.seniorityDistributionDetailsExport(dto); + } + + /** + * 保险购买分析列表 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/insurance-purchase-analysis-details") + public ActionResult insurancePurchaseAnalysisDetails(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelOverviewAnalysisService.insurancePurchaseAnalysisDetails(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, PersonnelsOverviewInsuranceDetailsVO.class)); + } + + /** + * 保险购买分析列表导出 + * + * @param dto 入参 + * @return {@link ActionResult } + */ + @PostMapping("/insurance-purchase-analysis-details-export") + public void insurancePurchaseAnalysisDetailsExport(@RequestBody FtbPersonnlesAnalysisDTO dto) throws IOException { + ftbPersonnelOverviewAnalysisService.insurancePurchaseAnalysisDetailsExport(dto); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsTurnoverAnalysisController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsTurnoverAnalysisController.java new file mode 100644 index 0000000..ba33349 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/analysis/FtbPersonnelsTurnoverAnalysisController.java @@ -0,0 +1,262 @@ +package jnpf.personnels.controller.web.analysis; + +import com.fantaibao.permission.annotation.FtbCheckPermission; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.personnels.service.FtbPersonnelsTurnoverAnalysisService; +import jnpf.personnels.utils.PersonnelDataAnalysisUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * web 人才流失分析 + * @Author: peng.hao + * @create: 2024/10/14 + */ +@RestController +@Slf4j +@RequestMapping("/web/turnover-analysis") +public class FtbPersonnelsTurnoverAnalysisController { + + @Resource + FtbPersonnelsTurnoverAnalysisService ftbPersonnelsTurnoverAnalysisService; + + /** + * 离职人数(月) + * 同比 + * 环比 + */ + @PostMapping("get-leave-people-month") + @FtbCheckPermission("leaveManage") + public ActionResult getLeavePeopleMonth(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeavePeopleMonth(dto)); + } + /** + * 离职性别分布(月) + */ + @PostMapping("get-leave-people-sex-month") + public ActionResult getLeavePeopleSexMonth(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeavePeopleSexMonth(dto)); + } + /** + * 首月离职率 + * 同比 + * 环比 + */ + @PostMapping("get-first-month-leave-rate") + public ActionResult getFirstMonthLeaveRate(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getFirstMonthLeaveRate(dto)); + } + /** + * 首周离职率 + * 同比 + * 环比 + */ + @PostMapping("get-first-week-leave-rate") + public ActionResult getFirstWeekLeaveRate(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getFirstWeekLeaveRate(dto)); + } + /** + * 首年离职率 + * 同比 + * 环比 + */ + @PostMapping("get-first-year-leave-rate") + @FtbCheckPermission("leaveManage") + public ActionResult getFirstYearLeaveRate(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getFirstYearLeaveRate(dto)); + } + + /** + * 新员工离职比例 + */ + @PostMapping("get-new-employee-leave-rate") + public ActionResult getNewEmployeeLeaveRate(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getNewEmployeeLeaveRate(dto)); + } + /** + * 离职原因分布 + */ + @PostMapping("get-leave-reason-distribution") + public ActionResult> getLeaveReasonDistribution(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeaveReasonDistribution(dto)); + } + + /** + * 离职各岗位人数占比 + */ + @PostMapping("get-leave-people-post-ratio") + public ActionResult> getLeavePeoplePostRatio(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatio(dto)); + } + /** + * 离职人员司龄分布 + */ + @PostMapping("get-leave-people-age-distribution") + public ActionResult> getLeavePeopleAgeDistribution(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeavePeopleAgeDistribution(dto)); + } + /** + * 离职人员年龄分布占比 + */ + @PostMapping("get-leave-people-age-ratio") + public ActionResult> getLeavePeopleAgeRatio(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeavePeopleAgeRatio(dto)); + } + /** + * 离职率对比 + */ + @PostMapping("get-leave-rate-comparison") + public ActionResult getLeaveRateComparison(@RequestBody FtbPersonnlesAnalysisDTO dto) { + return ActionResult.success(ftbPersonnelsTurnoverAnalysisService.getLeaveRateComparison(dto)); + } + + /** + * 新员工离职比例明细 + */ + @PostMapping("get-new-employee-leave-rate-detail") + public ActionResult getNewEmployeeLeaveRateDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getNewEmployeeLeaveRateDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverNewEmployeeDetailVO.class)); + } + + /** + * 新员工离职比例明细导出 + */ + @PostMapping("get-new-employee-leave-rate-detail-export") + public void getNewEmployeeLeaveRateDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsTurnoverAnalysisService.getNewEmployeeLeaveRateDetailExport(dto, response); + } + + /** + * 离职原因分布明细 + */ + @PostMapping("get-leave-reason-distribution-detail") + public ActionResult getLeaveReasonDistributionDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeaveReasonDistributionDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverLeaveReasonDetailVO.class)); + } + + /** + * 离职原因分布明细导出 + */ + @PostMapping("get-leave-reason-distribution-detail-export") + public void getLeaveReasonDistributionDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsTurnoverAnalysisService.getLeaveReasonDistributionDetailExport(dto, response); + } + + /** + * 各岗位人数占比明细 + */ + @PostMapping("get-leave-people-post-ratio-detail") + public ActionResult getLeavePeoplePostRatioDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatioDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverPostRatioDetailVO.class)); + } + + /** + * 各岗位人数占比明细导出 + */ + @PostMapping("get-leave-people-post-ratio-detail-export") + public void getLeavePeoplePostRatioDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatioDetailExport(dto, response); + } + + + /** + * 各部门人数占比明细 + */ + @PostMapping("/get-details-of-department-PeopleInEachStore") + public ActionResult getDetailsOfDepartmentOfPeopleInEachStore(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatioDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverPostRatioDetailVO.class)); + } + + /** + * 各部门人数占比明细导出 + */ + @PostMapping("/get-details-of-department-PeopleInEachStore-export") + public void getDetailsOfDepartmentOfPeopleInEachStoreExport(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + // 暂时调用岗位占比明细导出方法 + ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatioDetailExport(dto, response); + } + + + /** + * 各门店人数占比明细 + */ + @PostMapping("/get-details-of-proportion-PeopleInEachStore") + public ActionResult getDetailsOfTheProportionOfPeopleInEachStore(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatioDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverPostRatioDetailVO.class)); + } + + /** + * 各门店人数占比明细导出 + */ + @PostMapping("/get-details-of-proportion-PeopleInEachStore-export") + public void getDetailsOfTheProportionOfPeopleInEachStoreExport(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + // 暂时调用岗位占比明细导出方法 + ftbPersonnelsTurnoverAnalysisService.getLeavePeoplePostRatioDetailExport(dto, response); + } + + /** + * 离职人员年龄分布占比明细 + */ + @PostMapping("get-leave-people-age-ratio-detail") + public ActionResult getLeavePeopleAgeRatioDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeavePeopleAgeRatioDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverAgeDetailVO.class)); + } + + /** + * 离职人员年龄分布占比明细导出 + */ + @PostMapping("get-leave-people-age-ratio-detail-export") + public void getLeavePeopleAgeRatioDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsTurnoverAnalysisService.getLeavePeopleAgeRatioDetailExport(dto, response); + } + /** + * 离职人员司龄分布明细 + */ + @PostMapping("get-leave-people-age-distribution-detail") + public ActionResult getLeavePeopleAgeDistributionDetail(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.getLeavePeopleAgeDistributionDetail(dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverSeniorityDetailVO.class)); + } + + /** + * 离职人员司龄分布明细导出 + */ + @PostMapping("get-leave-people-age-distribution-detail-export") + public void getLeavePeopleAgeDistributionDetailExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsTurnoverAnalysisService.getLeavePeopleAgeDistributionDetailExport(dto, response); + } + /** + * 离职性别分布明细 + */ + @PostMapping("get-break-down-gender-detail") + public ActionResult breakdownOfGenderDistributionOfResignation(@RequestBody FtbPersonnlesAnalysisDTO dto) { + PageListVO result = ftbPersonnelsTurnoverAnalysisService.breakdownOfGenderDistributionOfResignation + (dto); + return ActionResult.success(PersonnelDataAnalysisUtil.encapsulatePageData(result, FtbPersonnelsTurnoverGenderDetailVO.class)); + } + + /** + * 离职性别分布明细导出 + */ + @PostMapping("get-break-down-gender-detail-export") + public void breakdownOfGenderDistributionOfResignationExprot(@RequestBody FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + ftbPersonnelsTurnoverAnalysisService.breakdownOfGenderDistributionOfResignationExprot(dto, response); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/apply/FtbPersonnelsPostApplyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/apply/FtbPersonnelsPostApplyController.java new file mode 100644 index 0000000..1ffefb0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/apply/FtbPersonnelsPostApplyController.java @@ -0,0 +1,238 @@ +package jnpf.personnels.controller.web.apply; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.constant.MsgCode; +import jnpf.entity.StoreEntity; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.apply.FtbPersonnelsApplyCreateDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.po.FtbPersonnelsPostApply; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyWithPerVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.model.position.PositionInfoNewVO; +import jnpf.permission.model.user.UserInfoVO; +import jnpf.personnels.service.FtbPersonnelsPostApplyService; +import jnpf.store.service.StoreService; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web人事岗位晋升申请表模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels-apply") +public class FtbPersonnelsPostApplyController { + + + @Resource + FtbPersonnelsPostApplyService service; + + @Autowired + UserApi userApi; + + @Resource + StoreService storeService; + + @Autowired + OrganizeApi organizeApi; + + /** + * 获取岗位申请列表 + * @param dto 岗位申请查询参数 + * @param page 分页信息 + * @return 岗位申请列表结果 + */ + @GetMapping("/getList") + public ActionResult> getList( PersonnelsQueryDTO dto, + CultivatePage page) { + PageListVO pageListVO = service.getList(dto, page); + return ActionResult.success(pageListVO); + } + + /** + * 晋升列表导出 + * + * @param dto 岗位申请查询参数 + * @param page 分页信息 + * @return 岗位申请列表结果 + */ + @GetMapping("/getList-export") + public void getListExport(PersonnelsQueryDTO dto, CultivatePage page, HttpServletResponse response) throws IOException { + page.setPageSize(-1); + PageListVO pageListVO = service.getList(dto, page); + // 转换数字->汉字 + pageListVO.getList().parallelStream().forEach(a -> { + // 晋升前岗位职等 + a.setPositionBeforePromotion(a.getPostName() + "(" + a.getPostRankName() + ")"); + // 晋升后岗位职等 + a.setPositionAfterPromotion(a.getPostPromotionName() + "(" + a.getPostRankPromotionName() + ")"); + // 申请来源 + FtbPersonnelsApplyVO.applicationSourceConversion(a); + // 状态 + a.setStateExcel(FtbPersonnelsApplyVO.stateConversion(a.getState())); + }); + String fileName = "晋升信息"; + EasyExcelUtils.exportExcel(response, fileName, pageListVO.getList(), FtbPersonnelsApplyVO.class); + } + + /** + * 初始化岗位申请 + * + * @param createDto 岗位申请创建数据传输对象 + * @return v1.1 返回 1 标识为门店负责人,需要进行替换 + */ + @PostMapping("/initPromApplication") + @Operation(summary = "初始化岗位申请") + public ActionResult initiateAPromotionApplication( + @Validated @RequestBody FtbPersonnelsApplyCreateDto createDto) { + extracted(createDto.getUserId(), createDto.getOrgId(),createDto.getPostId(),createDto.getPostRankId(),createDto); + String s = service.initiateAPromotionApplication(createDto); + return ActionResult.success(MsgCode.SU000.get(),s); + } + + /** + * 初始化岗位申请( for oa) + * + * @param createDto 岗位申请创建数据传输对象 + */ + @PostMapping("/initPromApplication-for-oa") + public ActionResult initiateAPromotionApplicationForOA( + @Validated @RequestBody FtbPersonnelsApplyCreateDto createDto) { + extracted(createDto.getUserIds().get(0), createDto.getOrgId(),createDto.getPostId(),createDto.getPostRankId(),createDto); + // 获取岗位申请信息 + if (StringUtils.isEmpty(createDto.getOrgCurrentId())) createDto.setOrgCurrentId(createDto.getOrgId()); + ActionResult s = service.initiateAPromotionApplicationForOA(createDto); + return s; + } + + /** + * 主管构建 + * @param userId + * @param orgId + * @param postId + * @param postRankId + * @param createDto + */ + private void extracted(String userId,String orgId,String postId,String postRankId,FtbPersonnelsApplyCreateDto createDto) { + UserInfoVO bossUser = userApi.getLeaderInfo(orgId, userId,postId,postRankId); + // 将老的主管信息赋值 + if (ObjectUtil.isNotEmpty(bossUser) && bossUser.getRealName() != null) { + createDto.setOldImmediateSuperName(bossUser.getRealName()); + } + if (ObjectUtil.isNotEmpty(bossUser) && bossUser.getId() != null) { + createDto.setOldImmediateSuperId(bossUser.getId()); + } + // 只要如果晋升主管为空,且组织相同 则设置为之前的主管 + if (StringUtils.isEmpty(createDto.getImmediateSuperId()) && ObjectUtil.isNotEmpty(bossUser) + && createDto.getOrgId().equals(createDto.getOrgCurrentId())){ + createDto.setImmediateSuperId(bossUser.getId()); + createDto.setImmediateSuperName(bossUser.getRealName()); + } + } + + + + + /** + * 查询岗位申请详情 + * @param id 岗位申请主键id + * @return 响应结果对象,包含岗位申请及相关信息 + */ + @GetMapping("/viewPromApplication/{id}") + @Operation(summary = "查询岗位申请详情") + public ActionResult viewPromotionApplications(@PathVariable("id") String id) { + FtbPersonnelsApplyWithPerVO withPerVO = service.viewPromotionApplications(id, "0"); + return ActionResult.success(withPerVO); + } + + /** + * 根据用户id和组织id 获取晋升通道晋升岗位 + */ + @GetMapping("/queryPostInfoByOrgAndUserId") + @Deprecated(since = "培训v1.1废弃", forRemoval = true) + public ActionResult> queryPostInfoByOrgAndUserId(@RequestParam("userId") String userId, + @RequestParam("orgId") String orgId){ + List voList = service.queryPostInfoByOrgAndUserId(userId,orgId); + return ActionResult.success(voList); + } + /** + * 审核岗位申请的接口 + * @param dto 岗位申请数据传输对象 + * @return 审核结果 + */ + @PutMapping("/auditPromApplication") + public ActionResult auditPromotionPostApplication(@Validated @RequestBody FtbPersonnelsSalaryAuditDto dto) { + service.auditPromotionPostApplication(dto); + return ActionResult.success(); + } + + + /** + * 审核岗位申请的接口 (for oa) + * @param dto 岗位申请数据传输对象 + * @return 审核结果 + */ + @PostMapping("/auditPromApplication") + public ActionResult auditPromotionPostApplicationWithOA(@Validated @RequestBody FtbPersonnelsSalaryAuditDto dto) { + ActionResult map = service.auditPromotionPostApplicationWithOA(dto); + return map; + } + + + /** + * 获取门店负责人详情 + * @param userId 门店负责人id + * @return organizeName 组织名称 storeName 门店名称 + */ + @GetMapping("/getStoreManagerInfo") + public ActionResult>> getStoreManagerInfo(@RequestParam("userId") String userId) { + LambdaQueryWrapper objectLambdaQueryWrapper = Wrappers.lambdaQuery(); + objectLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid,userId); + List list = storeService.list(objectLambdaQueryWrapper); + List> maps = list.stream().map(item -> { + Map map = Maps.newHashMap(); + OrganizeEntity organize = organizeApi.getInfoById(item.getOrganizeid()); + map.put("organizeName", organize.getFullName()); + map.put("storeName", item.getStorename()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(maps); + } + + /** + * 删除岗位申请(审批不通过的) + * @param id + * @return + */ + @DeleteMapping("/deletePostApply") + public ActionResult deletePostApply(@RequestParam("id")String id){ + FtbPersonnelsPostApply postApply = service.getById(id); + if (postApply.getState() != 3){ + return ActionResult.fail("该岗位申请不是不通过的,无法删除"); + } + service.removeById(id); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditMasterConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditMasterConfigController.java new file mode 100644 index 0000000..5ff7fdf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditMasterConfigController.java @@ -0,0 +1,59 @@ +package jnpf.personnels.controller.web.audit; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.config.FtbPersonnelsAuditConfigDTO; +import jnpf.model.personnels.dto.config.TestOrgDto; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditConfigVO; +import jnpf.personnels.service.FtbPersonnelsAuditMasterConfigService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * web人事流程审核配置模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels-config") +@Deprecated(since = "人事接入oa废弃原审批代码") +public class FtbPersonnelsAuditMasterConfigController { + + @Resource + FtbPersonnelsAuditMasterConfigService service; + + /** + * 创建或者修改人员审批流程配置 + * + * @param dto 人员审批配置参数 + * @return 操作结果的ActionResult对象 + */ + @PostMapping("/saveOrUpdate-process-config") + public ActionResult createPersonnelApprovalProcessConfiguration(@RequestBody FtbPersonnelsAuditConfigDTO dto) { + service.createApprovalConfiguration(dto); + return ActionResult.success(); + } + + /** + * 获取流程审核配置列表 + * + * @param orgId 组织ID + * @return 审计列表的结果Action + */ + @GetMapping("/get-audit-list") + public ActionResult getAuditList(@RequestParam("orgId") String orgId, + @RequestParam("configType") Integer configType){ + FtbPersonnelsAuditConfigVO auditConfigVO = service.getAuditList(orgId,configType); + return ActionResult.success(auditConfigVO); + } + /** + * 获取最新的修改 + * + */ + @GetMapping("/test") + public ActionResult getAuditList(@RequestBody TestOrgDto testDto){ + service.approverDataVerification(testDto.getUserId(),testDto.getOldDtos(),testDto.getNewDtos()); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskController.java new file mode 100644 index 0000000..053e303 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskController.java @@ -0,0 +1,152 @@ +package jnpf.personnels.controller.web.audit; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.personnels.dto.base.FtbPersonnelsBaseForOA; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditRunTaskVO; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskService; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +/** + * web审核前置校验 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels-task") +public class FtbPersonnelsAuditRunTaskController { + + + @Resource + private FtbPersonnelsAuditRunTaskService service; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + /** + * 查看对应业务的审批流程 + * + * @param id 运行的taskId + * @return 当前审批流程的结果Action + */ + @GetMapping("/view-list-by-task/{id}") + @Deprecated(since = "人事接入oa废弃原审批代码") + public ActionResult viewTheCurrentApprovalProcessByTaskId(@PathVariable("id") String id){ + return ActionResult.success(service.viewTheCurrentApprovalProcessByTaskId(id)); + } + + /** + * 查询当前用户是否处于流程中(for oa) + * + * @return null 不处于 ,1转正 2,调岗, 3离职, 4晋升 + */ + @GetMapping("/query-my-approved-review-process/userId") + public ActionResult queryMyApprovedReviewProcess(FtbPersonnelsBaseForOA dto){ + ActionResult result = new ActionResult<>(); + String string = checkWhetherTheCheckIsInTheProcessForOA(dto.getUserIds().get(0)); + if (StringUtils.isNotEmpty(string)){ + result.setMsg(string); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + }else { + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + } + return result; + } + /** + * 查询当前用户是否处于流程中 + * + * @return null 不处于 ,1 转正 2,调岗, 3离职 + */ + @GetMapping("/query-my-approved-review-process/code/userId") + public ActionResult queryMyApprovedReviewProcessWithCode(FtbPersonnelsBaseForOA dto){ + String isInTheReviewProcess = service.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(dto.getUserIds().get(0)); + return ActionResult.success("",isInTheReviewProcess); + } + private String checkWhetherTheCheckIsInTheProcessForOA(String userId) { + // null 不处于 ,1 转正 2,调岗, 3离职 4晋升 5调店 6降职 + String s = service.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(userId); + if (StringUtil.isNotEmpty(s)){ + // 1 转正 2 调岗, 3 离职 + String str=""; + switch (s){ + case "1": + str = "转正"; + break; + case "2": + str = "调岗"; + break; + case "3": + str = "离职"; + break; + case "4": + str = "晋升"; + break; + case "5": + str = "调店"; + break; + case "6": + str = "降职"; + break; + case "7": + str = "借调"; + break; + } + return ("当前人员处于"+str+"审批流程中,无法再次发起流程!"); + } + LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getUserId, userId); + queryWrapper1.ne(FtbPersonnelsStaffRoster::getWorkerStatus, "305"); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + long count = staffRosterService.count(queryWrapper1); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck,0); + queryWrapper.notIn(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 2); + queryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if (Objects.nonNull(one) && count == 0) { + throw new RuntimeException("当前选择人员还未办理入职,无法提交!"); + } + return null; + } + + + /** + * 关闭多个租户正在审核数据 归为审核历史 + * @param teantIdList 租户ID集合 + * @return + */ + @PostMapping("/closeAllExecutionData") + @NoDataSourceBind + @SaIgnore + @Deprecated(since = "人事接入oa废弃原审批代码") + public ActionResult closeAllExecutionData(@RequestBody List teantIdList){ + service.closeAllExecutionData(teantIdList); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskHistoryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskHistoryController.java new file mode 100644 index 0000000..ffde6ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/audit/FtbPersonnelsAuditRunTaskHistoryController.java @@ -0,0 +1,38 @@ +package jnpf.personnels.controller.web.audit; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.history.FtbPersonnelsAuditRunTaskHistoryVO; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskHistoryService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web人事审核历史执行的任务信息模块 + * + * @author penghao + */ +@RestController +@RequestMapping("/web/personnels-task-history") +public class FtbPersonnelsAuditRunTaskHistoryController { + + + @Resource + FtbPersonnelsAuditRunTaskHistoryService service; + + + /** + * 查看历史审批列表 + * + * @param id 审批ID + * @return 历史审批列表的结果Action + */ + @GetMapping("/view-list/{id}/{type}") + @Deprecated(since = "人事接入oa废弃原审批代码") + public ActionResult> viewHistoricalApprovalList( + @PathVariable("id") String id,@PathVariable("type")String type){ + return ActionResult.success(service.viewHistoricalApprovalList(id, type)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/authoritys/FtbPersonnelsPermissionUserController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/authoritys/FtbPersonnelsPermissionUserController.java new file mode 100644 index 0000000..a15f97d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/authoritys/FtbPersonnelsPermissionUserController.java @@ -0,0 +1,180 @@ +package jnpf.personnels.controller.web.authoritys; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsBatchDTO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsDTO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsUpdateDTO; +import jnpf.model.personnels.dto.authoritys.FtbPermissionInfoDTO; +import jnpf.model.personnels.vo.authoritys.FtbPermissionInfoVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionUserVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsScopeVO; +import jnpf.model.personnels.vo.uchisuike.FtbinternalRecommendationPoolVO; +import jnpf.personnels.service.FtbPersonnelsPermissionsService; +import jnpf.util.UserProvider; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; + +/** + * web人事调离员工权限模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-permission-user") +public class FtbPersonnelsPermissionUserController { + + @Resource + private FtbPersonnelsPermissionsService ftbPersonnelsPermissionsService; + /** + * 删除权限 + * + * @param id 员工权限主键id(必传) + * @return {@link ActionResult} + */ + @DeleteMapping("/delete-permissions/{id}") + public ActionResult deletePermissions(@PathVariable(value = "id") String id) { + ftbPersonnelsPermissionsService.deletePermissions(id); + return ActionResult.success(); + } + + /** + * 权限列表 + * + * @return {@link ActionResult}<{@link PageListVO}<{@link FtbinternalRecommendationPoolVO}>> + */ + @GetMapping("/internal-recommendation-pool-list-query") + public ActionResult> permissionList(CultivatePage cultivatePage, + FtbPermissionInfoDTO ftbPermissionInfoDTO) { + Page page = cultivatePage.coverCultivatePage("a.F_CreatorTime"); + ftbPermissionInfoDTO.setPermissionType(0); + page = ftbPersonnelsPermissionsService.permissionList(page, ftbPermissionInfoDTO); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 管理按钮时查询权限详情 + * + * @param id 员工权限主键id(必传) + * @return {@link ActionResult} + */ + @GetMapping("/admin-details") + public ActionResult adminDetails(String id) { + FtbPersonnelsPermissionUserVO ftbPersonnelsPermissionUserVO = ftbPersonnelsPermissionsService.adminDetails(id, 0L); + return ActionResult.success(ftbPersonnelsPermissionUserVO); + } + + /** + * 新增权限 + * + * @return {@link ActionResult} + */ + @PostMapping("/admin-details") + @Transactional + @SuppressWarnings("Duplicates") + public ActionResult addNewPermissions(@RequestBody @Validated FtbAddNewPermissionsBatchDTO ftbAddNewPermissionsDTO) { + List ftbAddNewPermissionsDTOs = new ArrayList<>(); + for (FtbAddNewPermissionsBatchDTO.UserInnerInfo userInnerInfo : ftbAddNewPermissionsDTO.getUserInnerInfos()) { + FtbAddNewPermissionsDTO temp = new FtbAddNewPermissionsDTO(); + temp.setPermissionType(0); + temp.setUserId(userInnerInfo.getUserId()); + temp.setUserName(userInnerInfo.getUserName()); + temp.setPhone(userInnerInfo.getPhone()); + temp.setUserCustomId(userInnerInfo.getUserCustomId()); + temp.setScopePermission(ftbAddNewPermissionsDTO.getScopePermission()); + temp.setPermissionIds(ftbAddNewPermissionsDTO.getPermissionIds()); + temp.setSpecifyOrgIds(ftbAddNewPermissionsDTO.getSpecifyOrgIds()); + + temp.setPostIds(userInnerInfo.getPostIds()); + temp.setOrgIds(userInnerInfo.getOrgIds()); + temp.setPostRankIds(userInnerInfo.getPostRankIds()); + + temp.setOrgNames(userInnerInfo.getOrgNames()); + temp.setPostNames(userInnerInfo.getPostNames()); + temp.setPostRankNames(userInnerInfo.getPostNameIds()); + + ftbAddNewPermissionsDTOs.add(temp); + } + for (FtbAddNewPermissionsDTO t : ftbAddNewPermissionsDTOs) { + ftbPersonnelsPermissionsService.addNewPermissions(t); + } + return ActionResult.success(); + } + + /** + * 修改权限 + * + * @return {@link ActionResult} + */ + @PutMapping("/admin-details-change") + public ActionResult addNewPermissionsChange(@RequestBody @Validated FtbAddNewPermissionsUpdateDTO ftbAddNewPermissionsUpdateDTO) { + ftbAddNewPermissionsUpdateDTO.setPermissionType(0); + ftbPersonnelsPermissionsService.addNewPermissionsChange(ftbAddNewPermissionsUpdateDTO); + return ActionResult.success(); + } + + /** + * 根据父级权限查询按钮权限列表(返回权限标识) + * + * @param parentPermissions 父级权限标识(必传) + * @return {@link ActionResult}<{@link List} + */ + @GetMapping("/query-permission") + public ActionResult> queryPermissionList(String parentPermissions) { + UserInfo userInfo = UserProvider.getUser(); + List result = ftbPersonnelsPermissionsService.queryPermissionList(userInfo, parentPermissions, 0); + return ActionResult.success(result); + } + + /** + * 新增时查询权限列表 + */ + @GetMapping("/query-the-permission-list-when-adding") + public ActionResult> queryThePermissionListWhenAdding() { + List ftbPersonnelsPermissionVOS = ftbPersonnelsPermissionsService.queryThePermissionListWhenAdding(0L); + return ActionResult.success(ftbPersonnelsPermissionVOS); + } + + /** + * 获取当前登录人所具有的组织范围(返回组织id) + * + * @return {@link ActionResult } + */ + @GetMapping("/select-organization-scope") + public ActionResult selectOrganizationScope() { + FtbPersonnelsScopeVO result = ftbPersonnelsPermissionsService.selectOrganizationScope(); + return ActionResult.success(result); + } + + /** + * 获取当前登录人所具有的员工范围(返回员工id) + * + * @return {@link ActionResult } + */ + @GetMapping("/select-people-range") + public ActionResult selectEmployeeRange() { + FtbPersonnelsScopeVO result = ftbPersonnelsPermissionsService.selectEmployeeRange(); + return ActionResult.success(result); + } + + /** + * 此用户是否是员工,true为是,false为否 + */ + @GetMapping("/is-this-user-an-employee") + public ActionResult isThisUserAnEmployee() { + Boolean result = ftbPersonnelsPermissionsService.isThisUserAnEmployee(); + return ActionResult.success(result); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistController.java new file mode 100644 index 0000000..9abb0e9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistController.java @@ -0,0 +1,107 @@ +package jnpf.personnels.controller.web.black; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackUpdateDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistAddDTO; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListVO; +import jnpf.personnels.service.FtbPersonnelsBlacklistService; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * web黑名单管理模块 + * + * @author wcx + * @since 2024-05-08 + */ +@RestController +@RequestMapping("/ftb-personnels-blacklist") +public class FtbPersonnelsBlacklistController { + + + @Autowired + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + /** + * 黑名单列表 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list") + public ActionResult> blacklist(CultivatePage cultivatePage + , FtbPersonnelsBlackListDTO dto) { + Page page = cultivatePage.coverCultivatePage("a.F_CreatorTime"); + page = ftbPersonnelsBlacklistService.blacklist(page, dto); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + + /** + * 黑名单列表导出 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-export") + public void listExport(CultivatePage cultivatePage + , FtbPersonnelsBlackListDTO dto) throws IOException { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletResponse httpServletResponse = requestAttributes.getResponse(); + Page page = cultivatePage.coverCultivatePage("a.F_CreatorTime"); + page.setSize(-1); + page = ftbPersonnelsBlacklistService.blacklist(page, dto); + EasyExcelUtils.exportExcel(httpServletResponse, "黑名单", page.getRecords(), FtbPersonnelsBlackListVO.class); + } + + /** + * 移除黑名单 + * + * @param id 黑名单主键id(必传) + */ + @PostMapping(value = "/remove-blacklist/{id}") + public ActionResult removeBlacklist(@PathVariable(value = "id") String id) { + ftbPersonnelsBlacklistService.removeBlacklist(id); + return ActionResult.success(); + } + + /** + * 编辑黑名单 + */ + @PostMapping(value = "/edit-blacklist") + public ActionResult editBlacklist(@Validated @RequestBody FtbPersonnelsBlackUpdateDTO fdto) { + ftbPersonnelsBlacklistService.editBlacklist(fdto); + return ActionResult.success(); + } + + /** + * 黑名单类型-OA专用 + * + * @return {@link ActionResult }<{@link List }<{@link FtbPersonnelsBlackListVO.BlackTermEnumVO }>> + */ + @GetMapping("/black-type") + public ActionResult> getBlacklist() { + List result = ftbPersonnelsBlacklistService.getBlacklist(); + return ActionResult.success(result); + } + + /** + * 添加黑名单 + */ + @PostMapping(value = "/add-blacklist") + public ActionResult addBlacklist( @RequestBody FtbPersonnelsBlacklistAddDTO ftbPersonnelsBlackListVO) { + ftbPersonnelsBlacklistService.addBlacklist(ftbPersonnelsBlackListVO); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistHistoryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistHistoryController.java new file mode 100644 index 0000000..6c71988 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistHistoryController.java @@ -0,0 +1,60 @@ +package jnpf.personnels.controller.web.black; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListHistoryDTO; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListHistoryVO; +import jnpf.personnels.service.FtbPersonnelsBlacklistHistoryService; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * web历史黑名单模块 + * + * @author wcx + * @since 2024-05-08 + */ +@RestController +@RequestMapping("/ftb-personnels-blacklist-history") +public class FtbPersonnelsBlacklistHistoryController { + + @Autowired + private FtbPersonnelsBlacklistHistoryService ftbPersonnelsBlacklistHistoryService; + + /** + * 历史黑名单列表 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list") + public ActionResult> listDropDown(CultivatePage cultivatePage + , FtbPersonnelsBlackListHistoryDTO dto) { + Page page = cultivatePage.coverCultivatePage("a.F_CreatorTime"); + page = ftbPersonnelsBlacklistHistoryService.listDropDown(page, dto); + PageListVO result = CultivatePage.coverPageList(page); + return ActionResult.success(result); + } + + /** + * 历史黑名单列表导出 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-export") + public void listExport(CultivatePage cultivatePage + , FtbPersonnelsBlackListHistoryDTO dto, HttpServletResponse response) throws IOException { + Page page = cultivatePage.coverCultivatePage("a.F_CreatorTime"); + page.setSize(-1); + page = ftbPersonnelsBlacklistHistoryService.listDropDown(page, dto); + EasyExcelUtils.exportExcel(response, "历史黑名单", page.getRecords(), FtbPersonnelsBlackListHistoryVO.class); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistTypeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistTypeController.java new file mode 100644 index 0000000..122b453 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/black/FtbPersonnelsBlacklistTypeController.java @@ -0,0 +1,86 @@ +package jnpf.personnels.controller.web.black; + + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistTypeAddDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistTypeUpdateDTO; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlacklistTypeListVO; +import jnpf.personnels.service.FtbPersonnelsBlacklistTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * web人事黑名单类型模块 + * + * @author wcx + * @since 2024-05-08 + */ +@RestController +@RequestMapping("/ftb-personnels-blacklist-type") +public class FtbPersonnelsBlacklistTypeController { + + + @Autowired + private FtbPersonnelsBlacklistTypeService ftbPersonnelsBlacklistTypeService; + + /** + * 新增黑名单类型 + * + * @param ftbPersonnelsBlacklistTypeAddDTO + * @return {@link ActionResult } + */ + @PostMapping(value = "/add") + public ActionResult add(@Validated @RequestBody FtbPersonnelsBlacklistTypeAddDTO ftbPersonnelsBlacklistTypeAddDTO) { + ftbPersonnelsBlacklistTypeService.add(ftbPersonnelsBlacklistTypeAddDTO); + return ActionResult.success(); + } + + /** + * 删除黑名单类型 + * + * @param id 主键id(必填) + * @return {@link ActionResult } + */ + @DeleteMapping(value = "/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbPersonnelsBlacklistTypeService.delete(id); + return ActionResult.success(); + } + + /** + * 编辑黑名单类型 + * + * @return {@link ActionResult } + */ + @PostMapping(value = "/update") + public ActionResult updateData(@Validated @RequestBody FtbPersonnelsBlacklistTypeUpdateDTO ftbPersonnelsBlacklistTypeUpdateDTO) { + ftbPersonnelsBlacklistTypeService.updateData(ftbPersonnelsBlacklistTypeUpdateDTO); + return ActionResult.success(); + } + + /** + * 黑名单类型列表 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list") + public ActionResult> listData() { + List result = ftbPersonnelsBlacklistTypeService.listData(); + return ActionResult.success(result); + } + + /** + * 办理离职时黑名单类型下拉查询 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-dropDown") + public ActionResult> listDropDown() { + List result = ftbPersonnelsBlacklistTypeService.listData(); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/compatibility/CompatibilityIssueController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/compatibility/CompatibilityIssueController.java new file mode 100644 index 0000000..34c07ae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/compatibility/CompatibilityIssueController.java @@ -0,0 +1,100 @@ +package jnpf.personnels.controller.web.compatibility; + +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.personnels.dto.emp.ExcelImprotEmpDTO; +import jnpf.model.personnels.dto.staff.field.FormFieldDto; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffArchivesHistory; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsStaffArchivesHistoryService; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.util.EasyExcelUtil; +import jnpf.util.NoDataSourceBind; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 兼容性问题 + * @Author: peng.hao + * @create: 2025/9/12 + */ +@RestController +@RequestMapping("/web/compatibilityIssue") +public class CompatibilityIssueController { + + @Resource + FtbPersonnelsStaffRegistrationFormDataService dataService; + + @Resource + FtbPersonnelsStaffArchivesHistoryService archivesHistoryService; + + @Resource + FtbPersonnelsTurnoverManagementService turnoverManagementService; + + @Resource + FtbPersonnelsStaffRosterMapper staffRosterMapper; + + + /** + * 处理离职历史数据变更问题 + * @param tenantIds 需要处理的租户ids + */ + @PostMapping("/processLeaveHistoryData") + @NoDataSourceBind + @Transactional + public ActionResult processLeaveHistoryData(@RequestBody List tenantIds) { + tenantIds.forEach(v->{ + try { + TenantDataSourceUtil.switchTenant(v); + } catch (LoginException e) { + throw new RuntimeException(e); + } + List archivesHistories = archivesHistoryService.list(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsTurnoverManagement::getUserId,archivesHistories.stream().map(FtbPersonnelsStaffArchivesHistory::getUserId).collect(Collectors.toList())); + List turnoverManagements = turnoverManagementService.list(queryWrapper); + Map turnoverManagementMap = turnoverManagements.stream().collect(Collectors.toMap(FtbPersonnelsTurnoverManagement::getUserId, Function.identity(), (k1, k2) -> k1)); + // 处理单个人的数据 + archivesHistories.forEach(archivesHistory->{ + if (!turnoverManagementMap.containsKey(archivesHistory.getUserId())) return; + List list = JSONUtil.toList(archivesHistory.getArchives(), FormTypeDto.class); + list.forEach(ft -> { + if (ft.getGroupFieldDataDtoList() == null) return; + List registrationFormData = ft.getGroupFieldDataDtoList().stream().filter(field -> Objects.nonNull(field) && field.getFormFieldDto() != null).map(field -> { + FormFieldDto fieldDto = field.getFormFieldDto(); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setId(IdWorker.getIdStr()); + formData.setPhone(turnoverManagementMap.get(archivesHistory.getUserId()).getPhoneNumber()); + formData.setFormTypeId(fieldDto.getFormTypeId()); + formData.setFormFieldId(fieldDto.getId()); + formData.setValue(fieldDto.getUserValue()); + return formData; + }).collect(Collectors.toList()); + dataService.saveBatch(registrationFormData); + }); + }); + }); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employeetype/FtbPersonnelsEmployeeTypeWebController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employeetype/FtbPersonnelsEmployeeTypeWebController.java new file mode 100644 index 0000000..50c311b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employeetype/FtbPersonnelsEmployeeTypeWebController.java @@ -0,0 +1,78 @@ +package jnpf.personnels.controller.web.employeetype; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.employeetype.FtbEmployeeTypeAddDTO; +import jnpf.model.personnels.dto.employeetype.FtbEmployeeTypeEditDTO; +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; +import jnpf.personnels.service.FtbPersonnelsEmployeeTypeService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +/** + * web员工类型模块 + * + * @author wangchunxiang + * @date 2025/09/11 + */ +@RestController +@RequestMapping("/web/employee-type") +public class FtbPersonnelsEmployeeTypeWebController { + + @Resource + private FtbPersonnelsEmployeeTypeService employeeTypeService; + + /** + * 添加员工类型 + */ + @PostMapping("/add") + public ActionResult addEmployeeType(@RequestBody @Validated FtbEmployeeTypeAddDTO employeeType) { + employeeTypeService.addEmployeeType(employeeType); + return ActionResult.success(); + } + + /** + * 修改员工类型 + */ + @PutMapping("/edit") + public ActionResult editEmployeeType(@RequestBody @Validated FtbEmployeeTypeEditDTO employeeType) { + employeeTypeService.editEmployeeType(employeeType); + return ActionResult.success(); + } + + /** + * 删除员工类型 + * + * @param id 员工类型主键Id + * @return {@link ActionResult }<{@link Void }> + */ + @DeleteMapping("/delete/{id}") + public ActionResult deleteEmployeeType(@PathVariable("id") String id) { + employeeTypeService.deleteEmployeeType(id); + return ActionResult.success(); + } + + /** + * 员工类型列表 + */ + @GetMapping("/employee-list") + public ActionResult> list() { + List result = employeeTypeService.employeeList(); + return ActionResult.success(result); + } + + /** + * 根据用户ID集合查询员工类型ID、名称 + * + * @param userIds 用户ID集合 + * @return key为用户Id,value为员工类型 + */ + @PostMapping("/get-employee-type-by-user-ids") + public Map getEmployeeTypeByUserIds(@RequestBody List userIds) { + return employeeTypeService.getEmployeeTypeByUserIds(userIds); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsEmEntryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsEmEntryController.java new file mode 100644 index 0000000..1cc04df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsEmEntryController.java @@ -0,0 +1,374 @@ +package jnpf.personnels.controller.web.employmentapply; + +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.emp.FtbEmpAddNewDTO; +import jnpf.model.personnels.dto.emp.FtbEmpConfirmDTO; +import jnpf.model.personnels.dto.emp.FtbEmpEntryDTO; +import jnpf.model.personnels.dto.emp.FtbEmpQueryDTO; +import jnpf.model.personnels.dto.oa.RequestForOA; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.vo.emp.FtbEmpAddNewVO; +import jnpf.model.personnels.vo.emp.FtbEmpConfirmVO; +import jnpf.model.personnels.vo.emp.FtbEmpEntryVO; +import jnpf.model.personnels.vo.emp.FtbEmpResultVO; +import jnpf.personnels.service.FtbPersonnelsEmEntryService; +import jnpf.personnels.utils.FtbPersonnlesIMUtils; +import jnpf.personnels.utils.SmsConfig; +import jnpf.personnels.utils.SmsSendUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.UserProvider; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * web入职管理 + * @Author: peng.hao + * @create: 2025/4/7 + */ +@RestController +@RequestMapping("/web/personnels-emp-entry") +public class FtbPersonnelsEmEntryController { + + @Resource + FtbPersonnelsEmEntryService ftbPersonnelsEmEntryService; + + @Resource + FtbPersonnlesIMUtils ftbPersonnlesIMUtils; + + @Resource + RedissonClient redissonClient; + + @Resource + PermissionsUtils permissionsUtils; + + @Autowired + private SmsSendUtil smsSendUtil; + + + @Autowired + private SmsConfig smsConfig; + + /** + * 查询入职管理列表 + */ + @PostMapping("/query-list") + public ActionResult> pageLists(@RequestBody FtbEmpEntryDTO empEntryDTO) { + return ActionResult.success(ftbPersonnelsEmEntryService.pageLists(empEntryDTO, "Web")); + } + + /** + * 新增员工/编辑未办理员工 + */ + @PutMapping("/add-or-update-new-emp") + public ActionResult addNewEmp(@Validated @RequestBody FtbEmpAddNewDTO dto) { + FtbEmpResultVO result = ftbPersonnelsEmEntryService.addNewEmp(dto); + return ActionResult.success(result); + } + /** + * 查询编辑未办理员工 + */ + @GetMapping("/query-new-emp") + public ActionResult queryNewEmp(String id) { + FtbEmpAddNewVO c = ftbPersonnelsEmEntryService.queryNewEmp(id); + return ActionResult.success(c); + } + + /** + * 入职办理列表-导出 + */ + @PostMapping("/onboarding-list-export") + public void onboardingListExport(@RequestBody FtbEmpEntryDTO req, HttpServletResponse response) throws IOException { + ftbPersonnelsEmEntryService.onboardingListExport(req,response); + } + + /** + * 删除员工入职记录 + * + * @param id 主键id,必传 + */ + @DeleteMapping("/delete/{id}") + public ActionResult deleteEmployeeOnboardingRecords(@PathVariable("id") String id) { + ftbPersonnelsEmEntryService.deleteEmployeeOnboardingRecords(id); + return ActionResult.success(); + } + + /** + * 终止入职 + */ + @PutMapping("/terminate-onboarding/{id}") + public ActionResult terminateOnboarding(@PathVariable String id) { + ftbPersonnelsEmEntryService.terminateOnboarding(id); + return ActionResult.success(); + } + + /** + * 办理入职 + */ + @PutMapping("/handle-join-job") + public ActionResult handleJoinJob(@Validated @RequestBody FtbEmpConfirmDTO ftbEmpConfirmDTO) { + String lockKey = "HANDLE_JOIN_JOB:" + ftbEmpConfirmDTO.getPhone(); + RLock lock = redissonClient.getLock(lockKey); + try { + boolean b = lock.tryLock(1000, TimeUnit.MILLISECONDS); + if (!b) {return ActionResult.fail("当前手机号正在办理中请勿重复点击");} + ftbPersonnelsEmEntryService.handleJoinJob(ftbEmpConfirmDTO); + }catch (Exception e){ + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + }finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return ActionResult.success(); + } + + /** + * 办理入职(oa) + */ + @PostMapping("/handle-join-job-oa") + public ActionResult handleJoinJobOA( @RequestBody FtbEmpConfirmDTO ftbEmpConfirmDTO) { + String lockKey = "HANDLE_JOIN_JOB:" + ftbEmpConfirmDTO.getPhone(); + RLock lock = redissonClient.getLock(lockKey); + try { + boolean b = lock.tryLock(1000, TimeUnit.MILLISECONDS); + if (!b) {return ActionResult.fail("当前手机号正在办理中请勿重复点击");} + ftbPersonnelsEmEntryService.handleJoinJobOA(ftbEmpConfirmDTO); + }catch (Exception e){ + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + }finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + return ActionResult.success(); + } + + /** + * 查看入职详情 + */ + @GetMapping("/onboarding-details") + public ActionResult onboardingDetails(@RequestParam String id) { + return ActionResult.success(ftbPersonnelsEmEntryService.onboardingDetails(id)); + } + + /** + * 入职审批(oa) + */ + @PostMapping("/approval") + public ActionResult onboardingApprovals(@RequestBody FtbPersonnelsAuditDto personnelsAuditDto) { + ftbPersonnelsEmEntryService.onboardingApprovals(personnelsAuditDto); + return ActionResult.success(); + } + + /** + * 入职撤回(oa) + */ + @PostMapping("/revoke") + public ActionResult onboardingWithdrawal(@RequestBody FtbPersonnelsAuditDto personnelsAuditDto) { + ftbPersonnelsEmEntryService.onboardingWithdrawal(personnelsAuditDto); + return ActionResult.success(); + } + + /** + * 发送im消息通知 + * @param userId 用户id + */ + @GetMapping("/send-im-msg") + public ActionResult sendImMsg(String userId,String url){ + UserInfo user = UserProvider.getUser(); + if(userId.equals(user.getUserId())){ + return ActionResult.fail("不能发送给自己"); + } + + ftbPersonnlesIMUtils.sendMsg(user.getTenantId(),userId,user.getUserName()); + return ActionResult.success(); + } + /** + * (oa)计算计划转正日期 + * @param workType 正式 / 试用 + * @param probationPeriod 试用期 + * @param probationPeriodDay 试用天数 + * @param actualStartDate 入职日期 + */ + @GetMapping("/calculate-planned-conversion-date") + public ActionResult> calculatePlannedConversionDate(String workType, + String probationPeriod, + String probationPeriodDay, + String actualStartDate) { + if(workType.equals("303")){ + Map map = new HashMap<>(); + map.put("conversionDate",actualStartDate); + return ActionResult.success(map); + } + // 入职日期 + int probation = 0; + // 创建一个 Calendar 实例并设置为当前日期 + Calendar calendar = Calendar.getInstance(); + long timestamp = Long.parseLong(actualStartDate); + Date date = new Date(timestamp); + calendar.setTime(date); + if ("101".equals(probationPeriod) && StringUtils.isEmpty(probationPeriodDay)) throw new RuntimeException("请选择试用期天数!"); + if ("101".equals(probationPeriod) && StringUtils.isNotEmpty(probationPeriodDay)) { + // 这里就是天数 + calendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(probationPeriodDay)); + } else { + switch (probationPeriod) { + case "102": + probation = 1; + break; + case "103": + probation = 2; + break; + case "104": + probation = 3; + break; + case "105": + probation = 4; + break; + case "106": + probation = 5; + break; + case "107": + probation = 6; + break; + case "108": + probation = 7; + break; + case "109": + probation = 8; + break; + case "110": + probation = 9; + break; + case "111": + probation = 10; + break; + case "112": + probation = 11; + break; + case "113": + probation = 12; + } + calendar.add(Calendar.MONTH, probation); + } + Date time = calendar.getTime(); + Map map = new HashMap<>(); + map.put("conversionDate",time); + return ActionResult.success(map); + } + + /** + * (oa)试用天数 返回 1到 30的值 + */ + @GetMapping("/probation-period-day") + public ActionResult>> probationPeriodDay(){ + List integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,31); + List> collect = integers.stream().map(i -> { + Map map = new HashMap<>(); + map.put("day", i); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(collect); + } + + /** + * (oa)查询入职人员列表 + */ + @GetMapping("/onboarding-list") + public ActionResult> onboardingList(String phone) { + List entryVOS = ftbPersonnelsEmEntryService.onboardingList(phone); + return ActionResult.success(entryVOS); + } + + /** + * (oa) 根据userId加组织id查询填写班组信息 + * + * @param request + * @return + */ + @PostMapping("/queryCrewsWithEntry") + public ActionResult> queryCrewsWithEntry(@RequestBody RequestForOA request) { + if (request.getPrimaryKeyId() != null && StringUtils.isNotEmpty(request.getAffiliatedOrg())){ + List workerGroupDataDto = ftbPersonnelsEmEntryService.queryCrewsWithEntry(request); + return ActionResult.success(workerGroupDataDto); + } + return ActionResult.success(); + } + /** + * 测试是否有权限发消息 + */ + @SneakyThrows + @GetMapping("/test-im-msg") + @NoDataSourceBind + public ActionResult> testImMsg(String userId,String tenantId,String currOrg,String moduleId){ + TenantDataSourceUtil.switchTenant(tenantId); + new Thread(()->{ + List stringList = permissionsUtils.getUserPersonnelOrganizationIdDataPermissions(userId, + List.of(PermissionsEnums.PERSONNEL_MANAGEMENT_Button.getValue().split(",")), + "", + List.of(PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), + PermissionsEnums.PERSONNEL_MANAGEMENT_APP.getValue()), + tenantId, currOrg,moduleId); + ftbPersonnlesIMUtils.sendMsgWithList(tenantId,stringList, "测试发送角色", moduleId); + }).start(); + return ActionResult.success(); + } + /** + * 根据userId修改手机号信息 + */ + @PutMapping("/update-phone-by-userId") + public void updatePhoneByUserId( @RequestParam("userId") String userId, + @RequestParam("phone") String phone){ + ftbPersonnelsEmEntryService.updatePhoneByUserId(userId,phone); + } + + @GetMapping("/send-sms") + @NoDataSourceBind + public ActionResult sendImMsg(String phone,String tenantId,String code) throws Exception { + Map tenants = smsConfig.getTenants(); + SmsConfig.TenantConfig tenantConfig; + if (tenants.containsKey(tenantId)){ + tenantConfig = tenants.get(tenantId); + }else { + tenantConfig = tenants.get("common"); + } + String signContent = tenantConfig.getSmsSignContent(); + HashMap map = new HashMap<>(); + map.put("1", signContent); + map.put("2", phone); + map.put("3", "0000"); + smsSendUtil.sendSms(List.of(phone), code,map ,null); + return ActionResult.success(); + } + + /** + * 薪酬搜索 电话 名称 + */ + @PostMapping("/search-phone-name") + public List searchPhoneName(@RequestBody FtbEmpQueryDTO dto){ + List maps = ftbPersonnelsEmEntryService.searchPhoneName(dto); + return maps; + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsStaffEmploymentApplyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsStaffEmploymentApplyController.java new file mode 100644 index 0000000..b435abe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/employmentapply/FtbPersonnelsStaffEmploymentApplyController.java @@ -0,0 +1,605 @@ +package jnpf.personnels.controller.web.employmentapply; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageInfo; +import io.seata.spring.annotation.GlobalTransactional; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.DownloadVO; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.dto.base.SourcePhone; +import jnpf.model.personnels.dto.oa.FtbPersonnelsEmployInfoForOA; +import jnpf.model.personnels.dto.roster.FtbRosterImportDTO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsEmployApplyDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.req.employment.AddStaffEmploymentApplyReq; +import jnpf.model.personnels.req.employment.AddStaffEmploymentApplyResultDto; +import jnpf.model.personnels.req.employment.BatchByPrimaryIdReq; +import jnpf.model.personnels.req.employment.EmploymentApplyCheckDto; +import jnpf.model.personnels.req.employment.MyWebEmploymentApplyCheckListReq; +import jnpf.model.personnels.req.employment.QueryStaffEmploymentApplyListReq; +import jnpf.model.personnels.req.roster.CheckOrgAndPosAndRankExistVo; +import jnpf.model.personnels.req.roster.ConfirmOnDutyReq; +import jnpf.model.personnels.vo.employment.CheckPhoneStatusVo; +import jnpf.permission.OrganizeApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.personnels.FtbPersonnelsEmploymentApplyApi; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonalizedTenantWhitelistUtils; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.excel.EasyExcelUtils; +import jnpf.yozo.utils.HttpRequestUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * web员工入职表模块 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/web/personnels-staff-employment-apply") +@Deprecated(since = "人事v2.0") +public class FtbPersonnelsStaffEmploymentApplyController implements FtbPersonnelsEmploymentApplyApi { + + + @Autowired + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private ConfigValueUtil configValueUtil; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Resource + FtbPersonnelsRegistrationFormFieldMapper fieldMapper; + + @Resource + FtbPersonnelsStaffRegistrationFormDataMapper dataMapper; + + @Resource + OrganizeApi organizeApi; + + @Autowired + PersonalizedTenantWhitelistUtils personalizedTenantWhitelistUtils; + + + /** + * 预入职员工列表/入职审批列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @GetMapping("/query-list") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult> pageLists(@Validated QueryStaffEmploymentApplyListReq req) { + PageInfo pageVo = staffEmploymentApplyService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + /** + * 入职办理列表 + * + */ + @GetMapping("/onboarding-list") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult> onboardingList(@Validated QueryStaffEmploymentApplyListReq req) { + PageListVO pageVo = staffEmploymentApplyService.onboardingList(req); + return ActionResult.success(pageVo); + } + /** + * 入职办理列表-导出 + */ + @PostMapping("/onboarding-list-export") + public void onboardingListExport(@RequestBody QueryStaffEmploymentApplyListReq req,HttpServletResponse response) throws IOException { + req.setPageSize(-1); + PageListVO pageVo = staffEmploymentApplyService.onboardingList(req); + List list = pageVo.getList(); + list.forEach(item->{ + item.setSourceName(FtbPersonnelsEmployApplyDto.getSource(item.getSource())); + item.setCheckStatusName(FtbPersonnelsEmployApplyDto.getCheckStatus(item.getCheckStatus())); + }); + EasyExcelUtils.exportExcel(response, "入职办理列表", list, FtbPersonnelsEmployApplyDto.class); + + } + + /** + * 我的审批(已审批/待审批) + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @GetMapping("/query-myCheck-list") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult> pageQueryMyCheckList(@Validated MyWebEmploymentApplyCheckListReq req) { + PageInfo pageVo = staffEmploymentApplyService.getWebMyCheckList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 办理入职 + * + * @param req 入职参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/handle-join-job") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult handleJoinJob(@Validated @RequestBody AddStaffEmploymentApplyReq req) { + String id = staffEmploymentApplyService.insertData(req); + return ActionResult.success("成功", id); + } + /** + * 办理入职(for oa) + * + * @param req 入职参数 + */ + @PostMapping("/handle-join-job-for-oa") + @Deprecated(since = "v2.0废弃",forRemoval= true) + @GlobalTransactional(rollbackFor = RuntimeException.class) + public ActionResult handleJoinJobForOA(@Validated @RequestBody AddStaffEmploymentApplyReq req) { + ActionResult map = staffEmploymentApplyService.handleJoinJobForOA(req); + return map; + } + + /** + * 查询邀请码接口 + * + * @return + */ + @GetMapping("/query-code") + public ActionResult queryCode() { + String code = staffEmploymentApplyService.queryCode(); + return ActionResult.success("成功", code); + } + + /** + * 获取员工入职记录详细信息 + * + * @param id + * @return + */ + @GetMapping("/get/{id}") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult get(@PathVariable("id") String id) { + FtbPersonnelsStaffEmploymentApplyDto info = staffEmploymentApplyService.getInfo(id); + return ActionResult.success(info); + } + + + /** + * 删除员工入职记录 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult delete(@PathVariable("id") String id) { + staffEmploymentApplyService.deleteData(id); + return ActionResult.success(); + } + + + /** + * 批量删除员工入职记录 + * + * @param req + * @return {@link ActionResult} + */ + @DeleteMapping("/batch-delete") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult batchDelete(@RequestBody @Validated BatchByPrimaryIdReq req) { + if (CollectionUtil.isEmpty(req.getIds())) { + throw new IllegalArgumentException("参数错误"); + } + staffEmploymentApplyService.deleteBatchData(req.getIds()); + return ActionResult.success(); + } + + + /** + * 检测校验组织/岗位/职级是否存在 + * + * @param vo + * @return + * * 0:成功 + * * 1:失败:组织被删除 + * * 2:失败:岗位被删除 + * * 3:失败:职等被删除 + */ + @GetMapping("/checkOrgPositionRankExist") + public ActionResult checkOrgPositionRankExist(CheckOrgAndPosAndRankExistVo vo) { + ConfirmOnDutyReq confirmOnDutyReq = BeanUtil.copyProperties(vo, ConfirmOnDutyReq.class); + Integer checkStatus = checkOrgPositionRank(confirmOnDutyReq); + return ActionResult.success(checkStatus); + } + + /** + * 确认到岗 + * + * @param req 入职参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/confirm-on-duty") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult confirmOnDuty(@Validated @RequestBody ConfirmOnDutyReq req) { + Integer checkStatus = checkOrgPositionRank(req); + if (!checkStatus.equals(0)) { + AddStaffEmploymentApplyResultDto dto = new AddStaffEmploymentApplyResultDto(); + dto.setId(""); + dto.setStatus(checkStatus); + return ActionResult.success("成功", dto); + } + String lockKey = "ConfirmOnDuty:" + req.getPhone(); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) { + try { + String id = staffRosterService.confirmOnDuty(req); + AddStaffEmploymentApplyResultDto dto = new AddStaffEmploymentApplyResultDto(); + dto.setId(id); + dto.setStatus(0); + return ActionResult.success("成功", dto); + } catch (Exception e) { + log.error("确认到岗失败", e); + return ActionResult.fail(e.getMessage()); + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候"); + } + } + + private Integer checkOrgPositionRank(ConfirmOnDutyReq req) { + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(req.getCurrOrg()); + if (null == organizeEntity) { + return 1; + } + PositionEntity positionEntity = personnelOrgUtils.queryPosition(req.getCurrPosition()); + if (null == positionEntity) { + return 2; + } + PositionGradesInfoVO positionGradesInfoVO = personnelOrgUtils.queryRank(req.getCurrRank()); + if (null == positionGradesInfoVO) { + return 3; + } + return 0; + } + /** + * 审批 + * + * @param id 主键id,必传 + * @return + */ + + @PutMapping("/approval/{id}") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult approval(@PathVariable("id") String id, @RequestBody EmploymentApplyCheckDto dto) { + dto.setId(id); + staffEmploymentApplyService.approval( dto); + return ActionResult.success(); + } + + /** + * 审批 (for oa) + * + * @return + */ + @PostMapping("/approval/id") + public ActionResult approvalForOA( @RequestBody EmploymentApplyCheckDto dto) { + ActionResult map = staffEmploymentApplyService.approvalForOA(dto); + return map; + } + + + /** + * 重新办理 + * + * @param id 主键id,必传 + * @return + */ + + @PutMapping("/re-approval/{id}") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult reApproval(@PathVariable("id") String id, @RequestBody AddStaffEmploymentApplyReq dto) { + staffEmploymentApplyService.reApproval(id, dto); + return ActionResult.success(); + } + + + /** + * 模版下载 + * + * @return + */ + @GetMapping("/download/template") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult templateDownload() { + DownloadVO vo = DownloadVO.builder().build(); + try { + vo.setName("预办理入职员工导出导入模板.xlsx"); + vo.setUrl("https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/xgl/%E9%A2%84%E5%8A%9E%E7%90%86%E5%85%A5%E8%81%8C%E5%91%98%E5%B7%A5%E5%AF%BC%E5%87%BA%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"); + } catch (Exception e) { + log.error("下载模板错误:" + e.getMessage()); + } + return ActionResult.success(vo); + } + + + /** + * 导入 + * + * @param file + * @return + */ + @PostMapping("/importData") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult importData(@RequestBody @Validated FtbRosterImportDTO file) { + Map stringObjectMap; + try (InputStream inputStream = EasyExcelUtils.checkExcelFile(file.getFileUrl())) { + stringObjectMap = staffEmploymentApplyService.importData(inputStream); + } catch (Exception e) { + return ActionResult.fail(e.getMessage()); + } + return ActionResult.success(stringObjectMap); + } + + /** + * 测试导入 + * + * @param file + * @return + */ + @PostMapping("/importDataFile") + public ActionResult importData(@RequestBody MultipartFile file) { + Map stringObjectMap; + try (InputStream inputStream = file.getInputStream()) { + stringObjectMap = staffEmploymentApplyService.importData(inputStream); + } catch (Exception e) { + return ActionResult.fail(e.getMessage()); + } + return ActionResult.success(stringObjectMap); + } + + + /** + * 导入点击 + */ + @GetMapping("/importData/click") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult importDataClick() { + staffEmploymentApplyService.importDataClick(); + return ActionResult.success(); + } + + /** + * 导出错误数据 + */ + @GetMapping("/exportErrDate") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public void exportErrData(HttpServletResponse response) { + staffEmploymentApplyService.exportErrData(response); + } + + /** + * 根据列表导出列表数据 + */ + @PostMapping("/exportDate") + @Deprecated(since = "人事v1.2", forRemoval = true) + public void exportDate(@RequestBody @Validated BatchByPrimaryIdReq req, HttpServletResponse response) { + staffEmploymentApplyService.exportDate(req, response); + } + + /** + * 预入职列表导出全部数据 + */ + @GetMapping("/exportDate-new") + public void exportDateNew(QueryStaffEmploymentApplyListReq req, HttpServletResponse response) throws IOException { + staffEmploymentApplyService.exportDateNew(req, response); + } + + /** + * 办理入职发送短信 + * + * @param phone 主键id,必传 + * @return + */ + @PutMapping("/send-phone-msg/{phone}") + @Deprecated(since = "人事v1.3", forRemoval = true) + public ActionResult sendPhoneMsg(@PathVariable("phone") String phone) { + staffEmploymentApplyService.sendPhoneMsg(phone); + return ActionResult.success("发送成功"); + } + + + /** + * 修改入职状态 + * + * @return + */ + @Override + @NoDataSourceBind + @GetMapping("/updateEmploymentApplyStatus") + @Deprecated(since = "v2.0废弃",forRemoval= true) + public ActionResult updateEmploymentApplyStatus(@RequestParam("tenantId") String tenantId) { +// +// // 判断是否为多租户 +// if (configValueUtil.isMultiTenancy()) { +// // 判断是不是从外面直接请求 +// if (StringUtil.isNotEmpty(tenantId)) { +// //切换成租户库 +// try { +// TenantDataSourceUtil.switchTenant(tenantId); +// } catch (LoginException e) { +// throw new RuntimeException("切换租户失败"); +// } +// } else { +// UserInfo userInfo = UserProvider.getUser(); +// Assert.notNull(userInfo.getUserId(), "缺少租户信息"); +// DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); +// } +// } +// //修改用户入职状态 +// staffEmploymentApplyService.updateEmploymentApplyStatus(); + + return ActionResult.success(); + } + + /** + * 办理入职,检测用户在系统中的状态 + * 0、系统不存在该用户 1、该手机号等待入职 2、该手机号等待入职审批 302、试用 303、正式 304、待离职 305 离职 + * + * @param phone 手机号 + * @return + */ + @GetMapping("/check-phone-status/{phone}") + public ActionResult checkPhoneStatus(@PathVariable("phone") String phone) { + String status = staffEmploymentApplyService.checkPhoneStatus(phone); + CheckPhoneStatusVo vo = new CheckPhoneStatusVo(); + vo.setStatus(status); + return ActionResult.success(vo); + } + /** + * 办理入职,检测用户在系统中的状态 + * 0、系统不存在该用户 1、该手机号等待入职 2、该手机号等待入职审批 302、试用 303、正式 304、待离职 305 离职 306 试岗 + * + * @param phone 手机号 + * @return + */ + @GetMapping("/check-phone-status") + public ActionResult checkPhoneStatusWithId(@RequestParam("phone") String phone,@RequestParam("name") String name) { + SourcePhone sourcePhone = JSONObject.parseObject(phone, SourcePhone.class); + String status = staffEmploymentApplyService.checkPhoneStatus(sourcePhone.getPhone()); + ActionResult result = new ActionResult<>(); + if ("1".equals(status)){ + result.setMsg( "该手机号等待入职"); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + }else if ("2".equals(status)){ + result.setMsg( "该手机号等待入职审批"); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + }else if ("302".equals(status) || "303".equals(status) || "304".equals(status) || "306".equals(status)){ + result.setMsg( "该员工已经到岗,请勿重复操作"); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + // 校验前后是否有空格 + Pattern spacesCheck = Pattern.compile(FtbRosterImportConstants.NAME_REGEX); + Matcher spacesCheckMatcher = spacesCheck.matcher(name); + if (!spacesCheckMatcher.matches()){ + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("请使用标准格式的中文名或英文名!"); + return result; + } + return ActionResult.success(); + } + @Operation(summary = "[校验]该岗位是否入职列表是否有人使用, 有则阻塞") + @GetMapping("/verify/user_bound") + public ActionResult verifyUserBound(@RequestParam(required = false, name = "positionId") String positionId, + @RequestParam(required = false, name = "positionGradesId") String positionGradesId) { + staffEmploymentApplyService.verifyUserBound(positionId, positionGradesId); + return ActionResult.success(); + } + + /** + * 入职办理列表查询 ForOA 回显 姓名 手机 + */ + @GetMapping("/inquire-about-the-entry-list") + public ActionResult> inquireAboutTheEntryList(@RequestParam(required = false, name = "keyWords") String keyWords, + @RequestParam(required = false,name = "phone")String phone, + @RequestParam(required = false,name = "workerName")String workerName) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsStaffEmploymentApply::getCheckStatus,0,3); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck,0); + queryWrapper.like(StringUtil.isNotEmpty(keyWords), FtbPersonnelsStaffEmploymentApply::getWorkerName,keyWords); + queryWrapper.eq(HttpRequestUtils.StringUtils.isNotEmpty(phone),FtbPersonnelsStaffEmploymentApply::getPhone,phone); + queryWrapper.eq(HttpRequestUtils.StringUtils.isNotEmpty(workerName),FtbPersonnelsStaffEmploymentApply::getWorkerName,workerName); + queryWrapper.orderByDesc(SuperBaseEntity.SuperCBaseEntity::getCreatorTime); + List list = staffEmploymentApplyService.list(queryWrapper); + Boolean itOnTheWhitelist = personalizedTenantWhitelistUtils.isItOnTheWhitelist(); + if (!itOnTheWhitelist){ + List infoForOAS = list.stream().map(FtbPersonnelsEmployInfoForOA::covert).collect(Collectors.toList()); + return ActionResult.success(infoForOAS); + } + // 租户扩展 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsRegistrationFormField::getEnabledMark,0); + wrapper.eq(FtbPersonnelsRegistrationFormField::getStatus,0); + wrapper.eq(FtbPersonnelsRegistrationFormField::getName,"意向组织"); + FtbPersonnelsRegistrationFormField field = fieldMapper.selectOne(wrapper); + List phoneList = list.stream().map(FtbPersonnelsStaffEmploymentApply::getPhone).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(phoneList)){ + List infoForOAS = list.stream().map(FtbPersonnelsEmployInfoForOA::covert).collect(Collectors.toList()); + return ActionResult.success(infoForOAS); + } + LambdaQueryWrapper registrationFormDataLambdaQueryWrapper = Wrappers.lambdaQuery(); + registrationFormDataLambdaQueryWrapper.eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId,field.getId()); + registrationFormDataLambdaQueryWrapper.in(FtbPersonnelsStaffRegistrationFormData::getPhone, phoneList); + List formData = dataMapper.selectList(registrationFormDataLambdaQueryWrapper); + Map phoneMap = formData.stream().filter(vo->vo.getPhone() !=null).collect(Collectors.toMap(FtbPersonnelsStaffRegistrationFormData::getPhone, FtbPersonnelsStaffRegistrationFormData::getValue)); + List orgIds = new ArrayList<>(phoneMap.values()); + List organizeName = organizeApi.getOrganizeName(orgIds); + Map orgMap = organizeName.stream().collect(Collectors.toMap(OrganizeEntity::getId, OrganizeEntity::getFullName)); + List infoForOAS = list.stream().map(item->{ + FtbPersonnelsEmployInfoForOA covered = FtbPersonnelsEmployInfoForOA.covert(item); + if (phoneMap.containsKey(covered.getPhone()) && orgMap.containsKey(phoneMap.get(covered.getPhone()))){ + String orgName = orgMap.get( phoneMap.get(covered.getPhone())); + String name = covered.getName() + "-意向组织:" + orgName; + covered.setName(name); + } + return covered; + }).collect(Collectors.toList()); + return ActionResult.success(infoForOAS); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formdata/FtbPersonnelsStaffRegistrationFormDataController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formdata/FtbPersonnelsStaffRegistrationFormDataController.java new file mode 100644 index 0000000..c517e6a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formdata/FtbPersonnelsStaffRegistrationFormDataController.java @@ -0,0 +1,141 @@ +package jnpf.personnels.controller.web.formdata; + +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.req.registerform.SaveFormDataReq; +import jnpf.model.personnels.req.registerform.SaveRegisterFormDataReq; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * web员工档案信息数据表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-staff-registration-form-data") +public class FtbPersonnelsStaffRegistrationFormDataController { + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 入职登记表表单(代为填写) + * + * @return + */ + @GetMapping("/query-registration-form") + public ActionResult> queryRegistrationForm() { + List list = registrationFormDataService.queryRegistrationForm(); + return ActionResult.success(list); + } + + /** + * 查询入职内推登记表表单 + * + * @param phone 手机号 + * @return + */ + @GetMapping("/query-registration-form-onboarding/{phone}") + public ActionResult> queryRegistrationFormOnboarding(@PathVariable("phone") String phone) { + List list = registrationFormDataService.queryRegistrationForm(phone,true); + return ActionResult.success(list); + } + /** + * 查询登记表表单 + * + * @param phone 手机号 + * @return + */ + @GetMapping("/query-registration-form/{phone}") + public ActionResult> queryRegistrationForm(@PathVariable("phone") String phone) { + List list = registrationFormDataService.queryRegistrationForm(phone,false); + return ActionResult.success(list); + } + + + /** + * 根据userId查询档案表单 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/query-archival-form/roster/{userId}") + public ActionResult> queryArchivalFormForRoster(@PathVariable("userId") String userId) { + List list = registrationFormDataService.queryArchivalForm(userId); + + return ActionResult.success(list); + } + + /** + * 保存登记表 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/save-register") + public ActionResult saveRegister(@Validated @RequestBody SaveRegisterFormDataReq req) { + UserInfo userInfo = UserProvider.getUser(); + registrationFormDataService.saveRegistrationForm(userInfo.getTenantId(),req.getFieldValueList(),req.getFlag(),null, req.getModuleId()); + return ActionResult.success(); + } + + /** + * 保存档案表单数据 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/save-archives") + public ActionResult saveArchives(@Validated @RequestBody SaveFormDataReq req) { + registrationFormDataService.saveArchives(req); + return ActionResult.success(); + } + /** + * 保存 修改档案表单数据通过手机号 + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PutMapping("/save-archives-with-phone") + public ActionResult saveArchivesWithPhone(@Validated @RequestBody SaveFormDataReq req) { + String lockKey = "save-archives-with-phone:" + req.getPhone(); + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) { + try { + registrationFormDataService.saveArchivesWithPhone(req); + return ActionResult.success(); + } catch (Exception e) { + throw e; + } finally { + redisTemplate.delete(lockKey); + } + } else { + throw new RuntimeException("提交中,请稍候..."); + } + } + + /** + * 检查实际入职日期通过,返回true则通过,false及提示语则不通过 + * + * @param actualStartDate 实际入职日期 + * @param userId 用户id + * @return {@link ActionResult }<{@link Boolean }> + */ + @GetMapping("/check-actual-joining-date-passed") + public ActionResult checkActualJoiningDatePassed(@RequestParam("actualStartDate") String actualStartDate, @RequestParam("userId") String userId) { + Boolean result = registrationFormDataService.checkActualJoiningDatePassed(actualStartDate, userId); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsPromiseConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsPromiseConfigController.java new file mode 100644 index 0000000..fa51b28 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsPromiseConfigController.java @@ -0,0 +1,129 @@ +package jnpf.personnels.controller.web.formfield; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.personnels.po.FtbPersonnelsPromiseConfig; +import jnpf.personnels.service.FtbPersonnelsPromiseConfigService; +import jnpf.util.NoDataSourceBind; +import org.jetbrains.annotations.NotNull; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * web本人承诺文案配置表 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels-promise-config") +public class FtbPersonnelsPromiseConfigController { + + /** + * 服务对象 + */ + @Resource + private FtbPersonnelsPromiseConfigService ftbPersonnelsPromiseConfigService; + + /** + * 查询文案 + * @return + */ + @GetMapping("/query-list") + public ActionResult> queryList() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsPromiseConfig::getEnableMark,0); + List list = ftbPersonnelsPromiseConfigService.list(wrapper); + return ActionResult.success(list); + } + + /** + * 新增文案 + * @return + */ + @PostMapping("/addNewCopy") + public ActionResult addNewCopy(@Validated @RequestBody List list) { + if (list.size() > 30 ) return ActionResult.fail("最多只能保存30条数据"); + Set stringSet = list.stream().map(FtbPersonnelsPromiseConfig::getCopyDetails).collect(Collectors.toSet()); + if (stringSet.size() != list.size()) return ActionResult.fail("添加文案中包含重复,请勿重复添加!"); + // 移除已有的 + List configList = list.stream().filter(item -> item.getIsBuiltIn() != null && item.getIsBuiltIn() != 1).collect(Collectors.toList()); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsPromiseConfig::getIsBuiltIn,0); + ftbPersonnelsPromiseConfigService.remove(updateWrapper); + ftbPersonnelsPromiseConfigService.saveBatch(configList); + // 内置的如果有直接更新 + ftbPersonnelsPromiseConfigService.updateBatchById(list.stream().filter(item -> item.getIsBuiltIn() != null && item.getIsBuiltIn() == 1).collect(Collectors.toList())); + return ActionResult.success(); + } + + /** + * 查询文案(外部) + * @return + */ + @GetMapping("/query-list-copy-writing") + @NoDataSourceBind + public ActionResult> queryListCopyWriting(@RequestParam("tenantId") String tenantId) throws LoginException { + TenantDataSourceUtil.switchTenant(tenantId); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsPromiseConfig::getEnableMark,0); + List list = ftbPersonnelsPromiseConfigService.list(wrapper); + return ActionResult.success(list); + } + + /** + * 个人承若显隐设置状态查询(内部) + * + * @return 是否显示签字板及个人承诺内容(1是 0否) + */ + @GetMapping("/query-list-copy-writing-status") + public ActionResult queryListCopyWritingStatus() { + return getIntegerActionResult(); + } + + /** + * 个人承若显隐设置状态查询(外部) + * + * @param tenantId 租户 ID + * @return 是否显示签字板及个人承诺内容(1是 0否) + */ + @GetMapping("/query-list-copy-writing-status-external") + @NoDataSourceBind + public ActionResult queryListCopyWritingStatusExternal(@RequestParam("tenantId") String tenantId) throws LoginException { + TenantDataSourceUtil.switchTenant(tenantId); + return getIntegerActionResult(); + } + + @NotNull + private ActionResult getIntegerActionResult() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsPromiseConfig::getEnableMark, 0); + wrapper.eq(FtbPersonnelsPromiseConfig::getIsBuiltIn, 1); + wrapper.last("limit 1"); + FtbPersonnelsPromiseConfig promiseConfig = ftbPersonnelsPromiseConfigService.getOne(wrapper); + return ActionResult.success(promiseConfig.getDisplay()); + } + + /** + * 修改个人承若显隐设置状态 + * + * @param display 是否显示签字板及个人承诺内容(1是 0否) + */ + @PutMapping("/update-copy-writing-status/{display}") + public ActionResult updateCopyWritingStatus(@PathVariable(value = "display") Integer display) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsPromiseConfig::getDisplay, display); + ftbPersonnelsPromiseConfigService.update(updateWrapper); + return ActionResult.success(); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsRegistrationFormFieldController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsRegistrationFormFieldController.java new file mode 100644 index 0000000..640c411 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formfield/FtbPersonnelsRegistrationFormFieldController.java @@ -0,0 +1,185 @@ +package jnpf.personnels.controller.web.formfield; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormFieldDto; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormTypeDto; +import jnpf.model.personnels.req.field.QueryRegistrationFormFieldListReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormFieldReq; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormFieldService; +import jnpf.util.FtbUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +/** + * web登记表字段模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-registration-form-field") +public class FtbPersonnelsRegistrationFormFieldController { + + @Autowired + private FtbPersonnelsRegistrationFormFieldService registrationFormFieldService; + + /** + * 登记表类别列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsRegistrationFormTypeDto}> + */ + @GetMapping("/query-list") + public ActionResult> pageLists(QueryRegistrationFormFieldListReq req) { + PageInfo pageVo = registrationFormFieldService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 切换启用/禁用 + * + * @param id 主键ID + * @param status 0、启用 1、禁用 + * @return {@link ActionResult} + */ + @PutMapping("/switch-status/{id}/{status}") + public ActionResult switchStatus(@PathVariable("id") String id, @PathVariable("status") Integer status) { + registrationFormFieldService.switchStatus(id, status); + return ActionResult.success(); + } + + /** + * 查询详情 + * + * @param id 主键ID + * @return {@link ActionResult} + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + FtbPersonnelsRegistrationFormFieldDto info = registrationFormFieldService.getInfo(id); + return ActionResult.success("成功", info); + } + + + /** + * 切换 必填/选填 + * + * @param id 主键ID + * @param status 是否必填,0、选填 1、必填 + * @return {@link ActionResult} + */ + @PutMapping("/switch-fill/{id}/{status}") + public ActionResult switchNeedFill(@PathVariable("id") String id, @PathVariable("status") Integer status) { + registrationFormFieldService.switchNeedFill(id, status); + return ActionResult.success(); + } + + /** + * 添加登记表字段,返回主键id + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody SaveRegistrationFormFieldReq req) { + String id = registrationFormFieldService.insertData(req); + return ActionResult.success("成功", id); + } + + + /** + * 修改字段信息 + * + * @param id 主键ID + * @param req + * @return {@link ActionResult} + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Validated @RequestBody SaveRegistrationFormFieldReq req) { + req.setId(id); + registrationFormFieldService.updateData(req); + return ActionResult.success(); + } + + + /** + * 删除 字段 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + registrationFormFieldService.deleteData(id); + return ActionResult.success(); + } + + /** + * 切换员工是否可见 + * + * @param id 主键ID + * @param status 0、可见 1、不可见 + * @return {@link ActionResult} + */ + @PutMapping("/switch-visible/{id}/{status}") + public ActionResult switchVisible(@PathVariable("id") String id, @PathVariable("status") Integer status) { + registrationFormFieldService.switchVisible(id, status); + return ActionResult.success(); + } + + /** + * 切换员工是否可修改本人档案 + * + * @param id 主键ID + * @param status 0、可以修改 1、不能修改 + * @return {@link ActionResult} + */ + @PutMapping("/toggle-modify-your-profile/{id}/{status}") + public ActionResult toggleWhetherYouCanModifyYourProfile(@PathVariable("id") String id, @PathVariable("status") Integer status) { + registrationFormFieldService.toggleWhetherYouCanModifyYourProfile(id, status); + return ActionResult.success(); + } + + /** + * 动态查询登记字段值 + * @param field 字段名称 + * @param workType 工作状态 + */ + @GetMapping("/query-field-value") + public ActionResult>> queryFieldValue(String field, String workType) { + List> value = registrationFormFieldService.queryFieldValue(field,workType); + return ActionResult.success("成功", value); + } + + /** + * 试用期登记字段值-forOA + * @param workType 工作状态 + */ + @GetMapping("/query-field-value-probationPeriod-oa") + public ActionResult>> probationPeriod(String workType) { + List> value = registrationFormFieldService.queryFieldValue("试用期",workType); + return ActionResult.success("成功", value); + } + + /** + * 员工状态登记字段值-forOA + */ + @GetMapping("/query-field-value-employeeStatus-oa") + public ActionResult>> employeeStatus() { + List> value = registrationFormFieldService.queryFieldValue("员工状态",null); + return ActionResult.success("成功", value); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formtype/FtbPersonnelsRegistrationFormTypeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formtype/FtbPersonnelsRegistrationFormTypeController.java new file mode 100644 index 0000000..8c8ab7b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/formtype/FtbPersonnelsRegistrationFormTypeController.java @@ -0,0 +1,130 @@ +package jnpf.personnels.controller.web.formtype; + +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormTypeDto; +import jnpf.model.personnels.req.field.QueryRegistrationFormTypeListReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormTypeReq; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormTypeService; +import jnpf.util.FtbUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * web登记表类型 表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-registration-form-type") +public class FtbPersonnelsRegistrationFormTypeController { + + + @Autowired + private FtbPersonnelsRegistrationFormTypeService registrationFormTypeService; + + /** + * 登记表类别列表(分页) + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsRegistrationFormTypeDto}> + */ + @GetMapping("/query-list") + public ActionResult> pageLists(@Validated QueryRegistrationFormTypeListReq req) { + PageInfo pageVo = registrationFormTypeService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 查询所有登记表类别 + * + * @return {@link ActionResult}<{@link FtbPersonnelsRegistrationFormTypeDto}> + */ + @GetMapping("/query-list-all") + public ActionResult> listAll() { + List list = registrationFormTypeService.listAll(null); + return ActionResult.success(list); + } + + /** + * 查询所有已经启用的登记表类别(用户添加自定义字段) + * + * @return {@link ActionResult}<{@link FtbPersonnelsRegistrationFormTypeDto}> + */ + @GetMapping("/query-list-enable-all") + public ActionResult> listEnableAll() { + List list = registrationFormTypeService.listAll(0); + return ActionResult.success(list); + } + + /** + * 切换启用/禁用 + * + * @param id 主键ID + * @param status 0、启用 1、禁用 + * @return {@link ActionResult} + */ + @PutMapping("/switch-status/{id}/{status}") + public ActionResult switchStatus(@PathVariable("id") String id, @PathVariable("status") Integer status) { + registrationFormTypeService.switchStatus(id, status); + return ActionResult.success(); + } + + /** + * 添加登记表类型,返回主键id + * + * @param req 参数 + * @return {@link ActionResult}<{@link String}> + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody SaveRegistrationFormTypeReq req) { + String id = registrationFormTypeService.insertData(req); + return ActionResult.success(id); + } + + + /** + * 修改登记表类别信息 + * + * @param id 主键ID + * @param req + * @return {@link ActionResult} + */ + @PutMapping("/update/{id}") + public ActionResult update(@PathVariable("id") String id, @Validated @RequestBody SaveRegistrationFormTypeReq req) { + req.setId(id); + registrationFormTypeService.updateData(req); + return ActionResult.success(); + } + + + /** + * 修改排序 + * + * @param id 主键ID + * @param sorts 序号 + * @return {@link ActionResult} + */ + @PutMapping("/update-sorts/{id}/{sorts}") + public ActionResult updateSorts(@PathVariable("id") String id, @PathVariable("sorts") Long sorts) { + registrationFormTypeService.updateSorts(id, sorts); + return ActionResult.success(); + } + + + /** + * 删除登记表类别 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + registrationFormTypeService.deleteData(id); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsController.java new file mode 100644 index 0000000..0e6fb9b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsController.java @@ -0,0 +1,95 @@ +package jnpf.personnels.controller.web.goods; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormUpdateDTO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsPageVO; +import jnpf.personnels.service.FtbPersonnelsGoodsService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * web物品管理模块 + * + * @author wangchunxiang + * @date 2025/09/11 + */ +@RestController +@RequestMapping(value = "/web/personnels-goods") +public class FtbPersonnelsGoodsController { + + @Resource + private FtbPersonnelsGoodsService ftbPersonnelsGoodsService; + + /** + * 添加物品 + */ + @PostMapping(value = "/add") + public ActionResult add(@RequestBody @Valid FtbPersonnelsGoodsFormDTO form) { + ftbPersonnelsGoodsService.create(form); + return ActionResult.success(); + } + + /** + * 修改物品 + */ + @PostMapping(value = "/update") + public ActionResult update(@RequestBody @Valid FtbPersonnelsGoodsFormUpdateDTO form) { + ftbPersonnelsGoodsService.update(form); + return ActionResult.success(); + } + + /** + * 删除物品 + * + * @param id 物品主键id + * @return {@link ActionResult }<{@link Void }> + */ + @DeleteMapping(value = "/delete/{id}") + public ActionResult delete(@PathVariable(value = "id") String id) { + ftbPersonnelsGoodsService.delete(id); + return ActionResult.success(); + } + + /** + * 获取物品列表 + */ + @GetMapping(value = "/list") + public ActionResult> list(FtbPersonnelsGoodsFormQueryDTO formQueryDTO, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbPersonnelsGoodsService.list(page, formQueryDTO); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + + /** + * 根据物品主键Id获取物品详情 + * + * @param id 物品主键Id + * @return {@link ActionResult }<{@link FtbPersonnelsGoodsPageVO }> + */ + @GetMapping(value = "/details") + public ActionResult details(@RequestParam(value = "id") String id) { + FtbPersonnelsGoodsPageVO ftbPersonnelsGoodsPageVO = ftbPersonnelsGoodsService.details(id); + return ActionResult.success(ftbPersonnelsGoodsPageVO); + } + + /** + * 删除物品二次确认 + * + * @param id 物品主键id + * @return {@link ActionResult }<{@link Boolean }> true为当前物品需要归还,false为当前物品不需要归还 + */ + @GetMapping(value = "/delete-confirm") + public ActionResult deleteConfirm(@RequestParam(value = "id") String id) { + Boolean deleteConfirm = ftbPersonnelsGoodsService.deleteConfirm(id); + return ActionResult.success(deleteConfirm); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsReceiveController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsReceiveController.java new file mode 100644 index 0000000..6a56e25 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/goods/FtbPersonnelsGoodsReceiveController.java @@ -0,0 +1,96 @@ +package jnpf.personnels.controller.web.goods; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveAddDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveReturnDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveUpdateDTO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsPageVO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceiveDetailsVO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceivePageVO; +import jnpf.personnels.service.FtbPersonnelsGoodsReceiveService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * web物品领用模块 + * + * @author wangchunxiang + * @date 2025/09/11 + */ +@RestController +@RequestMapping(value = "/web/personnels-goods-receive") +public class FtbPersonnelsGoodsReceiveController { + + @Resource + private FtbPersonnelsGoodsReceiveService ftbPersonnelsGoodsReceivesService; + + /** + * 添加物品领用 + */ + @PostMapping(value = "/add") + public ActionResult add(@RequestBody @Valid List form) { + ftbPersonnelsGoodsReceivesService.create(form); + return ActionResult.success(); + } + + /** + * 修改物品领用 + */ + @PostMapping(value = "/update") + public ActionResult update(@RequestBody @Valid FtbPersonnelsGoodsReceiveUpdateDTO form) { + ftbPersonnelsGoodsReceivesService.update(form); + return ActionResult.success(); + } + + /** + * 删除物品领用 + * + * @param id 物品主键id + * @return {@link ActionResult }<{@link Void }> + */ + @DeleteMapping(value = "/delete/{id}") + public ActionResult delete(@PathVariable(value = "id") String id) { + ftbPersonnelsGoodsReceivesService.delete(id); + return ActionResult.success(); + } + + /** + * 获取物品领用列表 + */ + @GetMapping(value = "/list") + public ActionResult> list(FtbPersonnelsGoodsReceiveQueryDTO formQueryDTO, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + page = ftbPersonnelsGoodsReceivesService.list(page, formQueryDTO); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + + /** + * 根据物品领用主键Id获取物品领用详情 + * + * @param id 物品领用主键Id + * @return {@link ActionResult }<{@link FtbPersonnelsGoodsPageVO }> + */ + @GetMapping(value = "/details") + public ActionResult details(@RequestParam(value = "id") String id) { + FtbPersonnelsGoodsReceiveDetailsVO ftbPersonnelsGoodsPageVO = ftbPersonnelsGoodsReceivesService.details(id); + return ActionResult.success(ftbPersonnelsGoodsPageVO); + } + + /** + * 归还物品 + */ + @PostMapping(value = "/return-goods") + public ActionResult returnGoods(@RequestBody @Valid FtbPersonnelsGoodsReceiveReturnDTO returnDTO) { + ftbPersonnelsGoodsReceivesService.returnGoods(returnDTO); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/growth/FtbPersonnelsStaffGrowthLogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/growth/FtbPersonnelsStaffGrowthLogController.java new file mode 100644 index 0000000..d6b4a9b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/growth/FtbPersonnelsStaffGrowthLogController.java @@ -0,0 +1,37 @@ +package jnpf.personnels.controller.web.growth; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.staff.growth.FtbPersonnelsStaffGrowthLogDto; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * web员工成长表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-staff-growth-log") +public class FtbPersonnelsStaffGrowthLogController { + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + /** + * 查询员工成长列表 + * + * @param userId + * @return + */ + @GetMapping("/query-list/{userId}") + public ActionResult> pageLists(@PathVariable("userId") String userId) { + List list = growthLogService.queryAll(userId); + return ActionResult.success(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/org/FtbPersonnelOrgController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/org/FtbPersonnelOrgController.java new file mode 100644 index 0000000..9d9c7f5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/org/FtbPersonnelOrgController.java @@ -0,0 +1,580 @@ +package jnpf.personnels.controller.web.org; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.http.HttpUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.personnels.dto.oa.RequestForOA; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.PersonnelsAuthorityApi; +import jnpf.permission.UserApi; +import jnpf.permission.V2GradesApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.QueryUserListDTO; +import jnpf.permission.dto.v2.grades.QueryGradeListDTO; +import jnpf.permission.dto.v2.organzie.QueryOrganizeListTargetTypesDTO; +import jnpf.permission.dto.v2.position.QueryPositionListDetailGradesDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.model.user.UserBaseVO; +import jnpf.permission.vo.position.ListGradesByPositionVO; +import jnpf.permission.vo.position.ListUsersByGradesVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionBoundOrganizeVO; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.NoDataSourceBind; +import jnpf.util.UserProvider; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * web组织信息 + */ +@RestController +@RequestMapping("/personnel-org") +public class FtbPersonnelOrgController { + + @Autowired + private UserApi userApi; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private PersonnelPerUtils personnelPerUtils; + + @Autowired + private PersonnelsAuthorityApi personnelsAuthorityApi; + + @Autowired + V2OrganizeApi v2OrganizeApi; + + @Autowired + V2PositionApi v2PositionApi; + + @Autowired + V2GradesApi v2GradesApi; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + private AttendanceGroupService attendanceGroupService; + + @Autowired + PermissionsUtils permissionsUtils; + + @Autowired + FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Autowired + QuerySalaryApi querySalaryApi; + + /** + * 查询组织下用户信息过滤已离职 + * @param organizeId + * @return + */ + @GetMapping("/query-org-info-user/{organizeId}") + public ActionResult> queryAllEmployeesRemoveResignedEmployees(@PathVariable("organizeId") String organizeId){ + // ftb_personnels_staff_roster + // 304、待离职 305 离职 + List list = new ArrayList<>(); + list.add("304"); + list.add("305"); + List rosters = staffRosterService.lambdaQuery().in(FtbPersonnelsStaffRoster::getWorkerStatus, list) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0).list(); + List collect = rosters.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList()); + ActionResult> actionResult = userApi.treePositionGradesUser(organizeId, ""); + if (actionResult == null){ + return ActionResult.fail("获取组织信息接口调用失败!"); + } + if (CollUtil.isEmpty(collect)){ + return ActionResult.success(actionResult.getData()); + } + List data = actionResult.getData(); + List result = new ArrayList<>(); + if (CollUtil.isEmpty(data)){ + return ActionResult.success(); + } + for (ListGradesByPositionVO vo : data){ + List gradesList = vo.getGradesList(); + List newGradesList =new ArrayList<>(); + for (ListUsersByGradesVO vo1 : gradesList){ + List userList = vo1.getUserList(); + List newUserList =new ArrayList<>(); + for (UserBaseVO vo2 : userList){ + if (!collect.contains(vo2.getId())){ + newUserList.add(vo2); + } + } + vo1.setUserList(newUserList); + newGradesList.add(vo1); + } + vo.setGradesList(newGradesList); + result.add(vo); + } + + return ActionResult.success(result); + } + + /** + * 获取百度地图数据 + * + * @return {@link ActionResult} + */ + @PostMapping("/get-map") + @NoDataSourceBind + public ActionResult getMap(@Valid @RequestBody GetBaiduMap getBaiduMap) { + String string = HttpUtil.get(getBaiduMap.getUrl()); + return ActionResult.success(string); + } + + @Data + public class GetBaiduMap { + /** + * url地址 + */ + @NotBlank(message = "url地址不能为空") + private String url; + + } + + /** + * 更具用户获取组织信息 + * @param + * @return + */ + @PostMapping("/get-org-list") + public ActionResult> getOrgList(@RequestBody RequestForOA request) { + List list = personnelOrgUtils.getUserOrgBoundInfo(request.getUserIds().get(0)); + // 过滤权限 + if ("0".equals(request.getWhetherToQueryPermissions())) { + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + list = list.stream().filter(a -> { + // 超级管理员 + if (CollUtil.isEmpty(orgIds)) { + return true; + } else { + return orgIds.contains(a.getAffiliatedOrg()); + } + }).collect(Collectors.toList()); + } + if (CollUtil.isNotEmpty(list)){ + String affiliatedOrg = request.getAffiliatedOrg(); + String affiliatedPosition = request.getAffiliatedPosition(); + if (StringUtils.isNotEmpty(affiliatedPosition) && StringUtils.isNotEmpty(affiliatedOrg)){ + list = list.stream().filter(item -> affiliatedPosition.equals(item.getAffiliatedPosition()) && isEquals(item, affiliatedOrg)).collect(Collectors.toList()); + return ActionResult.success(list ); + } + if (StringUtils.isNotEmpty(affiliatedOrg)){ + list = list.stream().filter(item -> isEquals(item, affiliatedOrg) && StringUtils.isNotEmpty(item.getAffiliatedPosition()) ).collect(Collectors.toList()); + return ActionResult.success(list ); + } + if ("1".equals(request.getFlag())){ + list = list.stream().filter(WorkerGroupDataDto::getIsDefault).map(vo->{ + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getAffiliatedOrg()); + workerGroupDataDto.setAffiliatedOrgName(vo.getAffiliatedOrgName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(list); + } + } + // 所有组织去重 + list = list.stream().map(vo->{ + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getAffiliatedOrg()); + workerGroupDataDto.setAffiliatedOrgName(vo.getAffiliatedOrgName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(list); + } + /** + * 更具用户获取组织信息 + * @param + * @return + */ + @PostMapping("/get-org-list-gd") + public ActionResult> getOrgListForGUDi(@RequestBody RequestForOA request){ + List list = personnelOrgUtils.getUserOrgBoundInfo(request.getUserIds().get(0)); + return ActionResult.success(list); + } + + /** + * OA控件通用(组织) + * 1.可以根据用户获取组织 + * 2.无参数查询所有组织信息 + * @param + * @return + */ + @PostMapping("/getOrganization") + public ActionResult> getOrganization(@RequestBody RequestForOA request) { + List userIds = request.getUserIds(); + // 过滤权限 + if (CollUtil.isEmpty(userIds) && "0".equals(request.getWhetherToQueryPermissions())) { + List orgIds = new ArrayList<>(); + if (!UserProvider.getUser().getIsAdministrator()){ + orgIds = permissionsUtils. + obtainPersonnelOrganizationIdDataPermissions(UserProvider.getUser().getUserId(), PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), "web"); + if (orgIds != null && orgIds.isEmpty()) return ActionResult.success(); + } + QueryOrganizeListTargetTypesDTO queryOrganizeListTargetTypesDTO = new QueryOrganizeListTargetTypesDTO(); + queryOrganizeListTargetTypesDTO.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.STORE)); + ActionResult> actionResult = v2OrganizeApi.listOrganizeByTargetTypes(queryOrganizeListTargetTypesDTO); + List lists = actionResult.getData(); + List finalOrgIds = orgIds; + List collected = lists.stream().filter(v -> { + if (v.getOrganizeCategoryEnums().equals(OrganizeCategoryEnums.STORE) && !v.getEnabled()) return false; + return true; + }).filter(v->{ + if (UserProvider.getUser().getIsAdministrator()) return true; + if (finalOrgIds == null) return true; + // 超级管理员 + if (CollUtil.isEmpty(finalOrgIds)) { + return false; + } + return finalOrgIds.contains(v.getId()); + }).map(vo -> { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getId()); + workerGroupDataDto.setAffiliatedOrgName(vo.getName()); + return workerGroupDataDto; + }).filter(Objects::nonNull).collect(Collectors.toList()); + return ActionResult.success(collected); + } + if (CollUtil.isEmpty(userIds) && StringUtils.isEmpty(request.getWhetherToQueryPermissions())){ + QueryOrganizeListTargetTypesDTO queryOrganizeListTargetTypesDTO = new QueryOrganizeListTargetTypesDTO(); + queryOrganizeListTargetTypesDTO.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.COMPANY, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.STORE)); + ActionResult> actionResult = v2OrganizeApi.listOrganizeByTargetTypes(queryOrganizeListTargetTypesDTO); + List lists = actionResult.getData(); + // 所有组织去重 + List dataDtos = lists.stream().map(vo -> { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getId()); + workerGroupDataDto.setAffiliatedOrgName(vo.getName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(dataDtos); + } + String userId = userIds.get(0); + List list = personnelOrgUtils.getUserOrgBoundInfo(userId); + // 查询当前人的数据过滤权限 + if ("0".equals(request.getWhetherToQueryPermissions()) && !UserProvider.getUser().getIsAdministrator()) { + List orgIds = permissionsUtils. + obtainPersonnelOrganizationIdDataPermissions(UserProvider.getUser().getUserId(), PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), "web"); + list = list.stream().filter(a -> { + // 超级管理员 + if (CollUtil.isEmpty(orgIds)) { + return true; + } else { + return orgIds.contains(a.getAffiliatedOrg()); + } + }).collect(Collectors.toList()); + } + if (CollUtil.isNotEmpty(list)){ + String affiliatedOrg = request.getAffiliatedOrg(); + String affiliatedPosition = request.getAffiliatedPosition(); + if (StringUtils.isNotEmpty(affiliatedPosition) && StringUtils.isNotEmpty(affiliatedOrg)){ + list = list.stream().filter(item -> affiliatedPosition.equals(item.getAffiliatedPosition()) && isEquals(item, affiliatedOrg)).collect(Collectors.toList()); + return ActionResult.success(list ); + } + if (StringUtils.isNotEmpty(affiliatedOrg)){ + list = list.stream().filter(item -> isEquals(item, affiliatedOrg) && StringUtils.isNotEmpty(item.getAffiliatedPosition()) ).collect(Collectors.toList()); + return ActionResult.success(list ); + } + // 主岗 + if ("0".equals(request.getWhetherToQueryMainPost())){ + list = list.stream().filter(WorkerGroupDataDto::getIsPrimaryPosition).map(vo->{ + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getAffiliatedOrg()); + workerGroupDataDto.setAffiliatedOrgName(vo.getAffiliatedOrgName()); + workerGroupDataDto.setIsPrimaryPosition(vo.getIsPrimaryPosition()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(list); + } + // 默认组织 + if ("1".equals(request.getFlag())){ + list = list.stream().filter(WorkerGroupDataDto::getIsDefault).map(vo->{ + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getAffiliatedOrg()); + workerGroupDataDto.setAffiliatedOrgName(vo.getAffiliatedOrgName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(list); + } + } + // 所有组织去重 + list = list.stream().map(vo->{ + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(vo.getAffiliatedOrg()); + workerGroupDataDto.setAffiliatedOrgName(vo.getAffiliatedOrgName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(list); + } + + /** + * OA控件通用(岗位) + * 1.可以根据用户获取岗位 + * 2.无参数查询所有组织信息 + * @param + * @return + */ + @PostMapping("/getPosts") + public ActionResult> getPosts(@RequestBody RequestForOA request){ + List userIds = request.getUserIds(); + String affiliatedOrg = request.getAffiliatedOrg(); + // 查询默认的所有岗位信息 + if (CollUtil.isEmpty(userIds) && StringUtils.isEmpty(affiliatedOrg)){ + QueryPositionListDetailGradesDTO gradesDTO = new QueryPositionListDetailGradesDTO(); + gradesDTO.setIsAll(true); + ActionResult> actionResult = v2PositionApi.listPositionDetailGradesConcreteInfo(gradesDTO); + List data = actionResult.getData(); + if (data == null || actionResult.getCode() != 200 ) return ActionResult.fail(actionResult.getMsg()); + List dataDtos = data.stream().map(vo -> { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedPosition(vo.getId()); + workerGroupDataDto.setAffiliatedPositionName(vo.getFullName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(dataDtos); + } + if (CollUtil.isEmpty(userIds) && StringUtils.isNotEmpty(affiliatedOrg)){ + ActionResult> actionResult = v2PositionApi.listPositionBoundInfoOrganizeIds(Arrays.asList(affiliatedOrg), null); + List data = actionResult.getData(); + if (data == null || actionResult.getCode() != 200 ) return ActionResult.fail(actionResult.getMsg()); + List dataDtos = data.stream().map(vo -> { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedPosition(vo.getId()); + workerGroupDataDto.setAffiliatedPositionName(vo.getFullName()); + return workerGroupDataDto; + }).distinct().collect(Collectors.toList()); + return ActionResult.success(dataDtos); + } + String userId = userIds.get(0); + List list = personnelOrgUtils.getUserOrgBoundInfo(userId); + if (CollUtil.isNotEmpty(list)) { + if (StringUtils.isNotEmpty(affiliatedOrg)){ + list = list.stream().filter(item -> isEquals(item, affiliatedOrg) && StringUtils.isNotEmpty(item.getAffiliatedPosition())).collect(Collectors.toList()); + // 过滤主岗查询 + if ("0".equals(request.getWhetherToQueryMainPost())) list = list.stream().filter(WorkerGroupDataDto::getIsPrimaryPosition).collect(Collectors.toList()); + return ActionResult.success(list); + } + } + return ActionResult.success(); + } + + private static boolean isEquals(WorkerGroupDataDto item, String affiliatedOrg) { + return affiliatedOrg.equals(item.getAffiliatedOrg()); + } + + /** + * OA控件通用(职等) + * 1.可以根据用户获取职等 + * 2.无参数查询所有组织信息 + * @param + * @return + */ + @PostMapping("/getGrade") + public ActionResult> getGrade(@RequestBody RequestForOA request){ + String affiliatedOrg = request.getAffiliatedOrg(); + String affiliatedPosition = request.getAffiliatedPosition(); + List userIds = request.getUserIds(); + if (CollUtil.isEmpty(userIds) && + StringUtils.isEmpty(affiliatedOrg) && + StringUtils.isEmpty(affiliatedPosition)){ + QueryGradeListDTO gradeListDTO = new QueryGradeListDTO(); + ActionResult> result = v2GradesApi.listGrades(gradeListDTO); + List data = result.getData(); + if (data == null || result.getCode() != 200 ) return ActionResult.fail(result.getMsg()); + List groupDataDtos = result.getData().stream().map(v -> { + WorkerGroupDataDto dataDto = new WorkerGroupDataDto(); + dataDto.setAffiliatedRank(v.getId()); + dataDto.setAffiliatedRankName(v.getFullName()); + return dataDto; + }).collect(Collectors.toList()); + return ActionResult.success(groupDataDtos); + } + if (CollUtil.isEmpty(userIds) && StringUtils.isNotEmpty(affiliatedPosition)){ + QueryGradeListDTO gradeListDTO = new QueryGradeListDTO(); + gradeListDTO.setPositionId(affiliatedPosition); + ActionResult> result = v2GradesApi.listGrades(gradeListDTO); + List data = result.getData(); + if (data == null || result.getCode() != 200 ) return ActionResult.fail(result.getMsg()); + List groupDataDtos = result.getData().stream().map(v -> { + WorkerGroupDataDto dataDto = new WorkerGroupDataDto(); + dataDto.setAffiliatedRank(v.getId()); + dataDto.setAffiliatedRankName(v.getFullName()); + return dataDto; + }).collect(Collectors.toList()); + return ActionResult.success(groupDataDtos); + } + String userId = userIds.get(0); + List list = personnelOrgUtils.getUserOrgBoundInfo(userId); + if (CollUtil.isNotEmpty(list)) { + if (StringUtils.isNotEmpty(affiliatedPosition) && StringUtils.isNotEmpty(affiliatedOrg)) { + list = list.stream().filter(item -> affiliatedPosition.equals(item.getAffiliatedPosition()) && isEquals(item, affiliatedOrg) && StringUtils.isNotEmpty(item.getAffiliatedRank())) + .collect(Collectors.toList()); + return ActionResult.success(list); + } + } + return ActionResult.success(); + } + /** + * OA控件通用(考情组) + * + * @param orgId 组织Id + */ + @GetMapping("/groupListByOrgId") + public ActionResult> groupListByOrgIds(String orgId) { + List groupVoList = attendanceGroupService.groupListByOrgId(orgId); + return ActionResult.success(groupVoList); + } + + /** + * OA控件通用(直属主管) + * + * @param dto + * @return + */ + @GetMapping("/queryAllUserForOrgAndStatus") + public ActionResult> getAllUser(QueryUserListDTO dto) { + List list = staffRosterService.queryAllUserForOrgAndStatus(dto); + return ActionResult.success("成功", list); + } + + /** + * OA控件通用(班组) + * + * @param request + * @return + */ + @PostMapping("/queryCrews") + public ActionResult> queryCrews(@RequestBody RequestForOA request) { + List userIds = request.getUserIds(); + String userId = CollUtil.isNotEmpty(userIds) ? userIds.get(0) : request.getUserId(); + if (CollUtil.isNotEmpty(userIds) || StringUtils.isNotEmpty(userId)){ + ActionResult usersBound = v2UserApi.getUsersBound(userId, UserProvider.getUser().getTenantId()); + if (usersBound == null || usersBound.getData() == null) return ActionResult.success(); + if (usersBound.getData() != null) { + UserBoundInfoVO data = usersBound.getData(); + WorkerGroupDataDto dataDto = new WorkerGroupDataDto(); + dataDto.setStoreTeamId(data.getStoreTeamId()); + dataDto.setStoreTeamName(data.getStoreTeamName()); + return ActionResult.success(List.of(dataDto)); + } + } + + ActionResult> actionResult; + if(StringUtils.isNotEmpty(request.getAffiliatedOrg())){ + + List affiliatedOrg = List.of(request.getAffiliatedOrg()); + ActionResult> result = v2OrganizeApi.organizesByOrganizeIds(affiliatedOrg); + if (result.getCode() != 200 ) return ActionResult.success(new ArrayList<>()); + if (result.getData().stream().filter(v->v.getOrganizeCategoryEnums().equals(OrganizeCategoryEnums.STORE)).count() == 0 ) return ActionResult.success(new ArrayList<>()); + actionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(affiliatedOrg, true,UserProvider.getUser().getTenantId()); + } else { + QueryOrganizeListTargetTypesDTO queryOrganizeListTargetTypesDTO = new QueryOrganizeListTargetTypesDTO(); + queryOrganizeListTargetTypesDTO.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.TEAM)); + actionResult = v2OrganizeApi.listOrganizeByTargetTypes(queryOrganizeListTargetTypesDTO); + } + if (actionResult == null || actionResult.getData() == null) return ActionResult.success( ); + List data = actionResult.getData(); + List list = data.stream().filter(item -> + item.getOrganizeCategoryEnums().equals(OrganizeCategoryEnums.TEAM)).map(v -> { + if (v.getId() == null) return null; + WorkerGroupDataDto dataDto = new WorkerGroupDataDto(); + dataDto.setStoreTeamId(v.getId()); + dataDto.setStoreTeamName(v.getName()); + return dataDto; + }).filter(Objects::nonNull).collect(Collectors.toList()); + return ActionResult.success("成功", list); + } + /** + * oa通用前置校验(校验用户是否未入职) + */ + @PostMapping("/check-user-is-not-entry") + public void checkUserIsNotEntry(@RequestBody RequestForOA request) { + List userIds = request.getUserIds(); + String userId = CollUtil.isNotEmpty(userIds) ? userIds.get(0) : request.getUserId(); + LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getUserId, request.getUserId()); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + long count = staffRosterService.count(queryWrapper1); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.in(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 0); + queryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if (Objects.nonNull(one) && count == 0) { + throw new RuntimeException("当前选择人员还未办理入职,无法提交!"); + } + } + /** + * oa查询当前人员薪酬 + */ + @GetMapping("/query-salary-by-userId") + public ActionResult> querySalaryByUserId(@RequestParam("userId") String userId) { +// LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); +// queryWrapper1.eq(FtbPersonnelsStaffRoster::getUserId, userId); +// queryWrapper1.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); +// queryWrapper1.last("limit 1"); +// FtbPersonnelsStaffRoster serviceOne = staffRosterService.getOne(queryWrapper1); +// LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); +// queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); +// queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); +// queryWrapper.in(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 0,1); +// queryWrapper.last("limit 1"); +// FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + HashMap map = new HashMap<>(); +// if (Objects.nonNull(one) && Objects.isNull(serviceOne)) { +// map.put("entryMoney", one.getEntryMoney() != null ? one.getEntryMoney() : ""); +// return ActionResult.success( map); +// } + ActionResult totalByUserId = querySalaryApi.getSalaryTotalByUserId(userId); + map.put("totalSalary", totalByUserId.getData() != null ? totalByUserId.getData() : ""); + return ActionResult.success(map); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/recruitmentchannels/FtbPersonnelsRecruitmentChannelsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/recruitmentchannels/FtbPersonnelsRecruitmentChannelsController.java new file mode 100644 index 0000000..7c380fc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/recruitmentchannels/FtbPersonnelsRecruitmentChannelsController.java @@ -0,0 +1,59 @@ +package jnpf.personnels.controller.web.recruitmentchannels; + +import jnpf.base.ActionResult; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.personnels.dto.recruitmentchannels.PersonnelsRecruitmentChannelsAddDTO; +import jnpf.model.personnels.po.FtbPersonnelsRecruitmentChannels; +import jnpf.personnels.service.FtbPersonnelsRecruitmentChannelsService; +import jnpf.util.NoDataSourceBind; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web人事招聘渠道 + * + * @author wcx + */ +@RestController +@RequestMapping("/web/ftb_personnels_recruitment_channels") +public class FtbPersonnelsRecruitmentChannelsController { + + @Resource + private FtbPersonnelsRecruitmentChannelsService ftbPersonnelsRecruitmentChannelsService; + + /** + * 添加人事招聘渠道 + * + * @return {@link ActionResult } + */ + @PostMapping("/add") + public ActionResult add(@RequestBody @Validated PersonnelsRecruitmentChannelsAddDTO personnelsRecruitmentChannelsAddDTO) { + ftbPersonnelsRecruitmentChannelsService.add(personnelsRecruitmentChannelsAddDTO); + return ActionResult.success(); + } + + /** + * 查询招聘渠道列表 + */ + @GetMapping("/query-list") + public ActionResult> queryRecruitmentChannels() { + List list = ftbPersonnelsRecruitmentChannelsService.queryRecruitmentChannels(); + return ActionResult.success(list); + } + + /** + * 查询招聘渠道列表(外部) + */ + @GetMapping("/query-list-external") + @NoDataSourceBind + public ActionResult> queryRecruitmentChannelsExternal(@RequestParam(value = "tenantId") String tenantId) throws LoginException { + TenantDataSourceUtil.switchTenant(tenantId); + List list = ftbPersonnelsRecruitmentChannelsService.queryRecruitmentChannels(); + return ActionResult.success(list); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/regular/FtbPersonnelsRegularManagementController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/regular/FtbPersonnelsRegularManagementController.java new file mode 100644 index 0000000..342edf0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/regular/FtbPersonnelsRegularManagementController.java @@ -0,0 +1,170 @@ +package jnpf.personnels.controller.web.regular; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.po.FtbPersonnelsRegularManagement; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularInfoVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularManagementVO; +import jnpf.personnels.service.FtbPersonnelsRegularManagementService; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.List; + +/** + * web转正管理模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels-regular") +public class FtbPersonnelsRegularManagementController { + + @Resource + FtbPersonnelsRegularManagementService service; + + /** + * 转正管理列表查询,返回分页列表数据 + * + * @param dto 查询参数对象 + * @return 返回分页列表数据的结果 + */ + @PostMapping("/list-query") + public ActionResult> pageList(@RequestBody PersonnelsQueryDTO dto) { + CultivatePage page = new CultivatePage(); + page.setCurrentPage(dto.getCurrentPage()); + page.setPageSize(dto.getPageSize()); + PageListVO pageListVO = service.pageList(dto, page, false); + return ActionResult.success(pageListVO); + } + + /** + * 转正管理列表导出 + * + * @param dto 查询参数对象 + * @return 返回分页列表数据的结果 + */ + @PostMapping("/list-query-export") + public void pageListExport(@RequestBody PersonnelsQueryDTO dto, CultivatePage page, HttpServletResponse response) throws IOException { + page.setPageSize(-1); + PageListVO pageListVO = service.pageList(dto, page, true); + // 转换数字->汉字 + List list = pageListVO.getList(); + if (CollUtil.isNotEmpty(list)) { + list.parallelStream().forEach(a -> { + // 审批状态 + FtbPersonnelsRegularManagementVO.approvalStatusTranslation(a); + // 试用期 + FtbPersonnelsRegularManagementVO.probationPeriodTranslation(a); + }); + } + + String fileName = "转正管理列表导出"; + EasyExcelUtils.exportExcel(response, fileName, pageListVO.getList(), FtbPersonnelsRegularManagementVO.class); + } + + + /** + * 办理转正 + * + * @param createDTO 申请参数对象 + * @return 返回申请结果 + */ + @PutMapping("/apply-regularization") + public ActionResult applyForRegularization(@Validated @RequestBody FtbPersonnelsRegularCreateDTO createDTO) { + return ActionResult.success("",service.applyForRegularization(createDTO)); + } + + /** + * 办理转正(for oa) + * + * @param createDTO 申请参数对象 + * @return 返回申请结果 + */ + @PostMapping("/apply-regularization-for-oa") + public ActionResult applyForRegularizationForOA(@Validated @RequestBody FtbPersonnelsRegularCreateDTO createDTO) { + + return service.applyForRegularizationForOA(createDTO); + } + + + /** + * 查看详细信息 + * + * @param id 审批id + * @return 返回 regularization 审批的详细信息结果 + */ + @GetMapping("/details-process/{id}") + public ActionResult checkTheDetailsOfRegularizationApproval(@PathVariable("id")String id) { + return ActionResult.success(service.checkTheDetailsOfRegularizationApproval(id)); + } + + /** + * 进行转正审批 + * + * @param auditDto 审批信息 + * @return 返回 regularization 审批结果 + */ + @PutMapping("/regularization-approval") + @Deprecated(since = "1.3.0") + public ActionResult regularizationApproval(@Validated @RequestBody FtbPersonnelsSalaryAuditDto auditDto) { + service.regularizationApproval(auditDto); + return ActionResult.success(); + } + + + /** + * 进行转正审批(for oa) + * + * @param auditDto 审批信息 + * @return 返回 regularization 审批结果 + */ + @PostMapping("/regularization-approval") + public ActionResult regularizationApprovalForOA(@Validated @RequestBody FtbPersonnelsSalaryAuditDto auditDto) { + ActionResult map = service.regularizationApprovalForOA(auditDto); + return map; + } + + + /** + * 撤销申请(oa) + * @param id + * @return + */ + @GetMapping("/cancellation-rectification") + public ActionResult cancellationOfApplicationForRectification(String id,String taskId){ + service.cancellationOfApplicationForRectification(id,taskId); + return ActionResult.success(); + } + + /** + * 删除转正申请 + * @param id + * @return + */ + @PutMapping("/delete-rectification/{id}") + public ActionResult deleteApplicationForRectification(@PathVariable("id")String id){ + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsRegularManagement::getEnableMark,1); + updateWrapper.set(SuperBaseEntity.SuperCUDBaseEntity::getDeleteUserId, UserProvider.getUser().getId()); + updateWrapper.set(SuperBaseEntity.SuperCUDBaseEntity::getDeleteTime,new Date()); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId,id); + service.update(updateWrapper); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationCategoryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationCategoryController.java new file mode 100644 index 0000000..844043a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationCategoryController.java @@ -0,0 +1,85 @@ +package jnpf.personnels.controller.web.resignation; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.resignation.FtbResignationConfigurationCategoryDTO; +import jnpf.model.personnels.po.FtbPersonnelsResignationCategoryConfiguration; +import jnpf.model.personnels.po.FtbPersonnelsResignationConfiguration; +import jnpf.model.personnels.vo.resignation.FtbResignationConfigurationCategoryVO; +import jnpf.personnels.service.FtbPersonnelsResignationCategoryConfigurationService; +import jnpf.personnels.service.FtbPersonnelsResignationConfigurationService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * web离职原因类别 + * @Author:peng.hao + */ +@RestController +@RequestMapping("/web/resignation-category-config") +public class FtbResignationConfigurationCategoryController { + + + @Resource + FtbPersonnelsResignationCategoryConfigurationService service; + @Resource + + private FtbPersonnelsResignationConfigurationService configurationService; + + /** + * 离职原因类别配置列表 + * + */ + @GetMapping("/list") + public ActionResult> getList(CultivatePage page) { + Page objectPage = page.coverCultivatePage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.orderByDesc(SuperBaseEntity.SuperCBaseEntity::getCreatorTime); + List list = service.list(wrapper); + return ActionResult.success(CultivatePage.paginate(list.stream().map(FtbResignationConfigurationCategoryVO::covert).collect(Collectors.toList()),objectPage)); + } + + + /** + * 离职原因类别配置保存编辑 + * + */ + @PostMapping("/configuration-save") + public ActionResult saveOrUpdate(@Validated @RequestBody List categoryDTOS) { + List names = categoryDTOS.stream().map(FtbResignationConfigurationCategoryDTO::getResignationTypeName).collect(Collectors.toList()); + List configIds = categoryDTOS.stream().filter(vo -> StringUtils.isNotEmpty(vo.getId())).map(FtbResignationConfigurationCategoryDTO::getId).collect(Collectors.toList()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsResignationCategoryConfiguration::getResignationTypeName, names); + wrapper.notIn(CollUtil.isNotEmpty(configIds), SuperBaseEntity.SuperIBaseEntity::getId,configIds); + long l = service.count(wrapper); + if (l > 0) throw new RuntimeException("离职原因分类名称已经存在,请勿重复添加!"); + service.saveOrUpdateBatch(categoryDTOS.stream().map(FtbResignationConfigurationCategoryDTO::covert).collect(Collectors.toList())); + return ActionResult.success(); + } + + /** + * 离职原因配置删除 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + LambdaQueryWrapper q = Wrappers.lambdaQuery(); + q.eq(FtbPersonnelsResignationConfiguration::getResignationTypeId,id); + long count = configurationService.count(q); + if (count > 0) return ActionResult.fail("该离职原因分类下有离职原因,请先删除其离职原因!"); + service.removeById(id); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationController.java new file mode 100644 index 0000000..c4f59bd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/resignation/FtbResignationConfigurationController.java @@ -0,0 +1,77 @@ +package jnpf.personnels.controller.web.resignation; + +import cn.hutool.core.collection.CollUtil; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.resignation.FtbResignationConfigurationDTO; +import jnpf.model.personnels.vo.resignation.FtbResignationConfigurationVO; +import jnpf.personnels.service.FtbPersonnelsResignationConfigurationService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + + +/** + * web离职原因配置 + * + * @author wangchunxiang + * @date 2024/06/28 + */ +@RestController +@RequestMapping("/web/resignation-configuration") +public class FtbResignationConfigurationController { + + @Resource + private FtbPersonnelsResignationConfigurationService ftbPersonnelsResignationConfigurationService; + + + /** + * 离职原因配置列表 + * + * @param resignationTypeId 离职原因类型id + * @param keyWords 离职原因 + * @return {@link ActionResult }<{@link List }<{@link FtbResignationConfigurationVO }>> + */ + @GetMapping("/list") + public ActionResult> getList(String keyWords, String resignationTypeId, CultivatePage page) { + PageListVO result = ftbPersonnelsResignationConfigurationService.getList(keyWords, resignationTypeId, page); + return ActionResult.success(result); + } + + /** + * 离职原因配置保存 + * + * @return {@link ActionResult } + */ + @PostMapping("/configuration-save") + public ActionResult saveOrUpdate(@RequestBody List resignationConfigurationDTO) { + ftbPersonnelsResignationConfigurationService.configurationSave(resignationConfigurationDTO); + return ActionResult.success(); + } + /** + * 离职原因配置删除 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbPersonnelsResignationConfigurationService.removeById(id); + return ActionResult.success(); + } + + /** + * 离职原因下拉列表(办理离职时) + * + * @return {@link ActionResult }<{@link List }<{@link FtbResignationConfigurationVO }>> + */ + @GetMapping("/reason-for-resignation-drop-down") + public ActionResult> reasonForResignationDropDown(Integer individualApplication) { + List result = ftbPersonnelsResignationConfigurationService.reasonForResignationDropDown(individualApplication); + if (CollUtil.isEmpty(result)) { + return ActionResult.fail("暂无原因可选,请联系管理员!"); + } + return ActionResult.success(result); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbPersonnelsRewardsPunishmentsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbPersonnelsRewardsPunishmentsController.java new file mode 100644 index 0000000..b57e7a3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbPersonnelsRewardsPunishmentsController.java @@ -0,0 +1,390 @@ +package jnpf.personnels.controller.web.rewardspunishments; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.alibaba.excel.write.metadata.fill.FillConfig; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.rewardspunishments.FtbEmployeeRewardRecordsQueryDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelSalaryRewardDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentExchangeOrderDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentStartDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentsAddDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentsUpdateDTO; +import jnpf.model.personnels.dto.roster.FtbRosterImportDTO; +import jnpf.model.personnels.dto.salary.FtbSalaryMetaDataQueryDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.vo.rewardspunishments.FtbEmployeeRewardRecordsVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbEmployeeRewardUserInfoVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelSalaryRewardVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentApprovalVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbXcEmployeeRewardRecordsVO; +import jnpf.permission.UserApi; +import jnpf.personnels.mapper.FtbPersonnelsRewardsPunishmentsMapper; +import jnpf.personnels.service.FtbPersonnelsRewardsPunishmentsService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.TenantUtil; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ResourceUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * web人事奖惩模块 + * + * @author wcx + * @since 2024-05-08 + */ +@RestController +@RequestMapping("/ftb-personnels-rewards-punishments") +public class FtbPersonnelsRewardsPunishmentsController { + + @Resource + private FtbPersonnelsRewardsPunishmentsService ftbPersonnelsRewardsPunishmentsService; + + @Autowired + private TenantUtil tenantUtil; + + @Autowired + UserApi userApi; + + @Resource + FtbPersonnelsStaffRosterService staffRosterService; + + @Resource + FtbPersonnelsRewardsPunishmentsMapper rewardsPunishmentsMapper; + + + /** + * 新增奖惩规则 + * + * @param rewardsPunishmentsAddDTO + * @return {@link ActionResult } + */ + @PostMapping(value = "/add") + public ActionResult add(@Validated @RequestBody FtbPersonnelsRewardsPunishmentsAddDTO rewardsPunishmentsAddDTO) { + ftbPersonnelsRewardsPunishmentsService.add(rewardsPunishmentsAddDTO); + return ActionResult.success(); + } + + /** + * 删除奖惩规则 + * + * @param id 主键id(必填) + * @return {@link ActionResult } + */ + @DeleteMapping(value = "/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbPersonnelsRewardsPunishmentsService.delete(id); + return ActionResult.success(); + } + + /** + * 编辑奖惩规则 + * + * @return {@link ActionResult } + */ + @PostMapping(value = "/update") + public ActionResult updateData(@Validated @RequestBody FtbPersonnelsRewardsPunishmentsUpdateDTO rewardsPunishmentsUpdateDTO) { + ftbPersonnelsRewardsPunishmentsService.updateData(rewardsPunishmentsUpdateDTO); + return ActionResult.success(); + } + + /** + * 奖惩规则启用/禁用 + * + * @return {@link ActionResult } + */ + @PostMapping(value = "/start-and-stop") + public ActionResult startAndStop(@Validated @RequestBody FtbPersonnelsRewardsPunishmentStartDTO rewardsPunishmentStartDTO) { + ftbPersonnelsRewardsPunishmentsService.startAndStop(rewardsPunishmentStartDTO); + return ActionResult.success(); + } + + /** + * 奖惩规则列表 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list") + public ActionResult> listData(FtbPersonnelsRewardsPunishmentQueryDTO rewardsPunishmentQueryDTO) { + List results = ftbPersonnelsRewardsPunishmentsService.listQuery(rewardsPunishmentQueryDTO); + return ActionResult.success(results); + } + + /** + * 奖惩规则交换顺序 + * + * @return {@link ActionResult } + */ + @PostMapping(value = "/exchange-order") + public ActionResult exchangeOrder(@Validated @RequestBody FtbPersonnelsRewardsPunishmentExchangeOrderDTO rewardsPunishmentExchangeOrderDTO) { + ftbPersonnelsRewardsPunishmentsService.exchangeOrder(rewardsPunishmentExchangeOrderDTO); + return ActionResult.success(); + } + + /** + * OA审批获取奖惩规则 + * + * @param type 类型,0奖励,1惩罚 + * @param tenantId 租户ID + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-approval-second") + @NoDataSourceBind + public ActionResult> listApproval(@RequestParam("type") Integer type + , @RequestParam(value = "tenantId") String tenantId) { + tenantUtil.switchTenant(tenantId); + List results = ftbPersonnelsRewardsPunishmentsService.listApproval(type); + return ActionResult.success(results); + } + + /** + * OA审批获取奖惩规则 + * + * @param type 类型,0奖励,1惩罚 + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-approval") + public ActionResult> listApprovalSecond(@RequestParam("type") Integer type) { + List results = ftbPersonnelsRewardsPunishmentsService.listApproval(type); + return ActionResult.success(results); + } + + /** + * 薪酬奖励合计 + */ + @PostMapping(value = "/salary-reward") + public List salaryReward(@RequestBody @Validated FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO) { + return ftbPersonnelsRewardsPunishmentsService.salaryReward(ftbPersonnelSalaryRewardDTO); + } + + /** + * 薪酬惩罚合计 + */ + @PostMapping(value = "/pay-penalty") + public List totalSalaryPenalty(@RequestBody @Validated FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO) { + return ftbPersonnelsRewardsPunishmentsService.totalSalaryPenalty(ftbPersonnelSalaryRewardDTO); + } + + /** + * OA奖惩审批金额查询 + * + * @param id 奖惩id + * @return {@link ActionResult }<{@link FtbPersonnelsRewardsPunishmentApprovalVO }> + */ + @GetMapping(value = "/list-approval-money") + public ActionResult listApprovalMoney(@RequestParam("id") String id) { + FtbPersonnelsRewardsPunishmentApprovalVO results = ftbPersonnelsRewardsPunishmentsService.listApprovalMoney(id); + return ActionResult.success(results); + } + + /** + * @param tenantId 租户ID + * @param type 奖惩类型 类型,0奖励,1惩罚 + * @param typeName 类型名称 + * @return :java.math.BigDecimal 金额 + * @decription 薪酬获取人事奖惩金额 + * @date 2024/11/5 17:27 + * @author AoTeMan + */ + @PostMapping(value = "/salaryMetaDataQuery") + @NoDataSourceBind + public BigDecimal salaryMetaDataQuery(@RequestBody FtbSalaryMetaDataQueryDto dto) { + tenantUtil.switchTenant(dto.getTenantId()); + return ftbPersonnelsRewardsPunishmentsService.salaryMetaDataQuery(dto); + } + + @PostMapping(value = "/salary-meta-data-batch-query") + @NoDataSourceBind + public List salaryBatchMetaDataQuery(@RequestBody FtbSalaryMetaDataQueryDto dto) { + tenantUtil.switchTenant(dto.getTenantId()); + return ftbPersonnelsRewardsPunishmentsService.salaryBatchMetaDataQuery(dto); + } + + + /** + * 员工奖惩记录列表 + * + * @return {@link ActionResult } + */ + @GetMapping("/employee-punishment-records-list") + public ActionResult> employeeRewardAndPunishmentRecords(CultivatePage cultivatePage, FtbEmployeeRewardRecordsQueryDTO queryDTO) { + Page page = cultivatePage.coverCultivatePage("g.operateTime"); + page = ftbPersonnelsRewardsPunishmentsService.employeeRewardAndPunishmentRecords(page, queryDTO); + return ActionResult.success(CultivatePage.coverPageList(page)); + } + + /** + * 员工详情 + */ + @GetMapping("/employee-punishment-records-detail") + public ActionResult queryDetail(@RequestParam("id") String id) { + FtbEmployeeRewardRecordsVO results = ftbPersonnelsRewardsPunishmentsService.queryDetail(id); + return ActionResult.success(results); + } + /** + * 员工奖惩记录列表导出 + */ + @GetMapping(value = "/list-export") + public void listExport(CultivatePage cultivatePage + , FtbEmployeeRewardRecordsQueryDTO queryDTO) throws IOException { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletResponse httpServletResponse = requestAttributes.getResponse(); + Page page = cultivatePage.coverCultivatePage("g.operateTime"); + page.setSize(-1); + page = ftbPersonnelsRewardsPunishmentsService.employeeRewardAndPunishmentRecords(page, queryDTO); + page.getRecords().forEach(item -> { + item.setTypeName(item.getType() == 1 ? "乐捐" : "奖励"); + // 0.审核中 1.通过 2.未通过 3撤回,4 无需审批 + Integer status = item.getStatus(); + if (status == 0){ + item.setStatusName("审核中"); + }else if (status == 1){ + item.setStatusName("已通过"); + }else if (status == 2){ + item.setStatusName("审批不通过"); + }else if (status == 3){ + item.setStatusName("已撤回"); + }else if (status == 4){ + item.setStatusName("无需审批"); + } + }); + EasyExcelUtils.exportExcel(httpServletResponse, "员工奖惩记录", page.getRecords(), FtbEmployeeRewardRecordsVO.class); + } + + /** + * 导入 + * + * @param file + * @return + */ + @PostMapping("/importData") + public ActionResult importData(@RequestBody @Validated FtbRosterImportDTO file) { + Map stringObjectMap; + try (InputStream inputStream = EasyExcelUtils.checkExcelFile(file.getFileUrl())) { + stringObjectMap = ftbPersonnelsRewardsPunishmentsService.importData(inputStream); + } catch (Exception e) { + return ActionResult.fail(e.getMessage()); + } + return ActionResult.success(stringObjectMap); + } + + /** + * 导出错误数据 + */ + @GetMapping("/exportErrDate") + public void exportErrData(HttpServletResponse response) throws IOException { + ftbPersonnelsRewardsPunishmentsService.exportErrData(response); + } + + /** + * 导入点击 + * @param type 类型,0奖励,1惩罚 + */ + @GetMapping("/importData/click") + public ActionResult importDataClick(@RequestParam("type") Integer type, + @RequestParam("orgId") String orgId) { + ftbPersonnelsRewardsPunishmentsService.importDataClick(type,orgId); + return ActionResult.success(); + } + /** + * 导出模版文件 + * @param type 类型,0奖励,1惩罚 + * @param orgId 组织id + */ + @GetMapping("/exportTheTemplate") + public void exportTheTemplate(@RequestParam("type") Integer type, + @RequestParam("orgId") String orgId, + HttpServletResponse httpServletResponse) throws IOException { + String templateFileName ="classpath:roster/rewards.xlsx"; + StaffRosterListReq req = new StaffRosterListReq(); + req.setCurrOrg(orgId); + req.setPageSize(-1); + req.setIsQueryAuth("0"); + PageInfo list = staffRosterService.getPageList(req,false); + List collection = list.getList(); + List userInfoVOList = new ArrayList<>(); + if (CollUtil.isNotEmpty(collection)) { + Map voMap = collection.stream().collect(Collectors.toMap(FtbPersonnelsStaffRosterDto::getUserId, Function.identity())); + userInfoVOList = collection.stream().map(item -> { + FtbEmployeeRewardUserInfoVO vo = new FtbEmployeeRewardUserInfoVO(); + WorkerGroupDataDto userInfoVo = voMap.get(item.getUserId()).getOrgList() + .stream().findFirst().orElse(new WorkerGroupDataDto()); + vo.setName(item.getName()); + vo.setEmployeeId(item.getSystemWokerId()); + vo.setOrgName(userInfoVo.getAffiliatedOrgName()); + vo.setPostName(userInfoVo.getAffiliatedPositionName()); + if (type == 0) { + vo.setType("奖励"); + } else { + vo.setType("乐捐"); + } + return vo; + }).collect(Collectors.toList()); + }else { + throw new RuntimeException("组织下无员工"); + } + FtbPersonnelsRewardsPunishmentQueryDTO queryDTO = new FtbPersonnelsRewardsPunishmentQueryDTO(); + queryDTO.setType(type); + queryDTO.setStatus(0); + List result = rewardsPunishmentsMapper.listQuery(queryDTO); + httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + httpServletResponse.setCharacterEncoding("utf-8"); + httpServletResponse.setHeader("Content-Disposition", + "attachment;filename*=utf-8''" + "test"+System.currentTimeMillis()+".xlsx"); + templateFileName = templateFileName.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length()); + ServletOutputStream outputStream = httpServletResponse.getOutputStream(); + try (ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(new ClassPathResource(templateFileName).getInputStream()).build()) { + FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); + WriteSheet one = EasyExcel.writerSheet(0).build(); + WriteSheet two = EasyExcel.writerSheet(1).build(); + excelWriter.fill(userInfoVOList,fillConfig, one); + excelWriter.fill(result,fillConfig, two); + } + } + /** + * 删除人员记录 + * @param id + * @param type 类型,0奖励,1惩罚 + */ + @DeleteMapping("/deleteUserInfo/{id}/{type}") + public ActionResult deleteUserInfo(@PathVariable("id") String id, + @PathVariable("type") Integer type) { + ftbPersonnelsRewardsPunishmentsService.deleteUserInfo(id,type); + return ActionResult.success("删除成功"); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbRewardsPunishmentsApproveOAController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbRewardsPunishmentsApproveOAController.java new file mode 100644 index 0000000..78b8b0b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rewardspunishments/FtbRewardsPunishmentsApproveOAController.java @@ -0,0 +1,73 @@ +package jnpf.personnels.controller.web.rewardspunishments; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.rewardspunishments.FtbAwardPassedDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbAwardSubmissionDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPenaltySubmissionDTO; +import jnpf.personnels.service.FtbRewardsPunishmentsApproveOAService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 奖惩OA审批回调 + * + * @author wangchunxiang + * @date 2024/08/14 + */ +@RestController +@RequestMapping("/approve-oa") +public class FtbRewardsPunishmentsApproveOAController { + + @Resource + private FtbRewardsPunishmentsApproveOAService ftbRewardsPunishmentsApproveOAService; + + /** + * 奖项提交事件 + * + * @return {@link ActionResult } + */ + @PostMapping("/award-submission") + public ActionResult awardSubmission(@RequestBody @Validated FtbAwardSubmissionDTO ftbAwardSubmission) { + ftbRewardsPunishmentsApproveOAService.awardSubmission(ftbAwardSubmission); + return ActionResult.success(); + } + + /** + * 奖项通过/拒绝/撤回事件 + * + * @return {@link ActionResult } + */ + @PostMapping("/award-passed") + public ActionResult awardPassed(@RequestBody @Validated FtbAwardPassedDTO ftbAwardPassedDTO) { + ftbRewardsPunishmentsApproveOAService.awardPassed(ftbAwardPassedDTO); + return ActionResult.success(); + } + + /** + * 惩罚提交事件 + * + * @return {@link ActionResult } + */ + @PostMapping("/penalty-submission") + public ActionResult penaltySubmission(@RequestBody @Validated FtbPenaltySubmissionDTO ftbPenaltySubmissionDTO) { + ftbRewardsPunishmentsApproveOAService.penaltySubmission(ftbPenaltySubmissionDTO); + return ActionResult.success(); + } + + /** + * 惩罚通过/拒绝/撤回事件 + * + * @return {@link ActionResult } + */ + @PostMapping("/penalty-passed") + public ActionResult penaltyPassed(@RequestBody @Validated FtbAwardPassedDTO ftbAwardPassedDTO) { + ftbRewardsPunishmentsApproveOAService.penaltyPassed(ftbAwardPassedDTO); + return ActionResult.success(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsContactInfoController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsContactInfoController.java new file mode 100644 index 0000000..b032d9e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsContactInfoController.java @@ -0,0 +1,106 @@ +package jnpf.personnels.controller.web.roster; + +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; +import jnpf.personnels.FtbPersonnelsContactInfoManagerApi; +import jnpf.personnels.service.FtbPersonnelsContactInfoService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; + +/** + * web电子合同同步模块 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/web/personnelsContactInfo") +public class FtbPersonnelsContactInfoController implements FtbPersonnelsContactInfoManagerApi { + @Autowired + private FtbPersonnelsContactInfoService contactInfoService; + + @Autowired + private ConfigValueUtil configValueUtil; + + + /** + * 同步合同信息 + * + * @param info + * @return + */ + @PostMapping("/syncContactInfo") + @NoDataSourceBind + public ActionResult syncContactInfo(@RequestBody ContactStatusInfo info) { + + switchTenantId(info.getTenantCode()); + contactInfoService.syncContactInfo(info); + return ActionResult.success("成功", true); + } + + + + private void switchTenantId(String tenantId) { + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } + + /** + * 查询入职未发起的合同,并发送IM消息 + * + * @param tenantId + * @return + */ + @GetMapping("/checkNotSendContactSign") + @NoDataSourceBind + public ActionResult checkNotSendContactSign(@RequestParam("tenantId") String tenantId) { + + switchTenantId(tenantId); + contactInfoService.checkNotSendContactSign(tenantId); + return ActionResult.success("成功", true); + } + + /** + * 下载合同信息 + * @param userId + * @param response + */ + @PostMapping("/downLoad/{userId}") + public void downLoad(@PathVariable("userId") String userId, HttpServletResponse response) { + contactInfoService.FileListZip("合同详情", userId, response); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsMetaDataController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsMetaDataController.java new file mode 100644 index 0000000..23fc619 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsMetaDataController.java @@ -0,0 +1,231 @@ +package jnpf.personnels.controller.web.roster; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.enums.EmployeeMetaDataType; +import jnpf.model.personnels.dto.roster.meta.PersonnelsMetaDTO; +import jnpf.model.personnels.dto.salary.FtbXcCustomFieldDto; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaDataReq; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaFuctionReq; +import jnpf.model.personnels.vo.salary.FtbXcCustomFieldVo; +import jnpf.personnels.FtbPersonnelsMetaDataManagerApi; +import jnpf.personnels.mapper.*; +import jnpf.personnels.service.FtbPersonnelsMetaDataService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * web查询人事元数据信息 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/web/personnels/metaData") +public class FtbPersonnelsMetaDataController implements FtbPersonnelsMetaDataManagerApi { + + @Autowired + FtbPersonnelsMetaDataService metaDataService; + + @Autowired + private ConfigValueUtil configValueUtil; + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + @Resource + private FtbPersonnelsRegistrationFormTypeMapper ftbPersonnelsRegistrationFormTypeMapper; + @Resource + private FtbPersonnelsRegistrationFormFieldMapper ftbPersonnelsRegistrationFormFieldMapper; + @Resource + private FtbPersonnelsRegistrationFormFieldOptionMapper ftbPersonnelsRegistrationFormFieldOptionMapper; + @Resource + private FtbPersonnelsStaffRegistrationFormDataMapper ftbPersonnelsStaffRegistrationFormDataMapper; + /** + * 查询元数据信息 + * @param req + * @return + */ + @Override + @PostMapping("/getMetaData") + @NoDataSourceBind + public List getMetaData(@RequestBody FtbPersonnelsMetaDataReq req) { + switchTenantId(req.getTenantCode()); + List list = metaDataService.getMetaData(req); + return list; + } + + @GetMapping("/test") + public ActionResult test(@RequestParam("userIds") List userIds) { + List employeeMetaDataTypes = Arrays.asList(EmployeeMetaDataType.BASIC_INFO, + EmployeeMetaDataType.EDUCATION_INFO, + EmployeeMetaDataType.JOB_INFO, + EmployeeMetaDataType.CONTRACT_INFO, + EmployeeMetaDataType.PERSONAL_MATERIAL, + EmployeeMetaDataType.ORGANIZATION_INFO, + EmployeeMetaDataType.ROLE + ); + FtbPersonnelsMetaDataReq ftbPersonnelsMetaDataReq = new FtbPersonnelsMetaDataReq(); + ftbPersonnelsMetaDataReq.setDataType(employeeMetaDataTypes); + ftbPersonnelsMetaDataReq.setTenantCode("yawen"); + ftbPersonnelsMetaDataReq.setUserIds(userIds); + List list = metaDataService.getMetaData(ftbPersonnelsMetaDataReq); + return ActionResult.success(list); + } + + /** + * 查询用户当月是否已经离职 + * + * @param userId 用户ID + * @param tenantId 租户ID + * @return true-已经离职 false-未离职 + */ + @GetMapping("/queryCurrMonthDepartStatus/{userId}/{tenantId}") + @NoDataSourceBind + public Boolean queryCurrMonthDepartStatus(@PathVariable("userId") String userId, @PathVariable("tenantId") String tenantId) { + switchTenantId(tenantId); + return metaDataService.queryCurrMonthDepartStatus(userId, tenantId); + } + + @PostMapping("/queryCurrMonthDepartStatusByDate") + @NoDataSourceBind + @Override + public Boolean queryCurrMonthDepartStatusByDate(@RequestBody FtbPersonnelsMetaFuctionReq req) { + switchTenantId(req.getTenantId()); + return metaDataService.queryCurrMonthDepartStatusByDate(req.getUserId(), req.getTenantId(),req.getStartDate(),req.getEndDate()); + } + + @PostMapping("/query-current-month-leave") + @NoDataSourceBind + @Override + public Map queryCurrMonthLeave(@RequestBody FtbPersonnelsMetaFuctionReq req) { + switchTenantId(req.getTenantId()); + return metaDataService.queryCurrMonthLeave(req); + } + + /** + * 批量查询员工自定义字段 + */ + @PostMapping("/query-custom-filed") + @Override + public List queryCustomFiled(@RequestBody FtbXcCustomFieldDto req) { + if (CollUtil.isEmpty(req.getUserIds())) { + return List.of(); + } + // 自定义类型 + LambdaQueryWrapper formTypeLambdaQueryWrapper = Wrappers.lambdaQuery(); + formTypeLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getEnabledMark,0); + formTypeLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getStatus,0); + List ftbPersonnelsRegistrationFormTypes = ftbPersonnelsRegistrationFormTypeMapper.selectList(formTypeLambdaQueryWrapper); + if (CollUtil.isEmpty(ftbPersonnelsRegistrationFormTypes)) { + return List.of(); + } + // 自定义字段查询 + List forTypeIds = ftbPersonnelsRegistrationFormTypes.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + LambdaQueryWrapper registrationFormFieldLambdaQueryWrapper = Wrappers.lambdaQuery(); + registrationFormFieldLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormField::getStatus,0); + registrationFormFieldLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormField::getEnabledMark,0); + registrationFormFieldLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormField::getSystemType,1); + registrationFormFieldLambdaQueryWrapper.notIn(FtbPersonnelsRegistrationFormField::getType,5,6); + registrationFormFieldLambdaQueryWrapper.in(FtbPersonnelsRegistrationFormField::getFormTypeId,forTypeIds); + List ftbPersonnelsRegistrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList(registrationFormFieldLambdaQueryWrapper); + if (CollUtil.isEmpty(ftbPersonnelsRegistrationFormFields)) { + return List.of(); + } + // 自定义选项,翻译汉字 + List formFields = ftbPersonnelsRegistrationFormFields.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + Map formFieldMap = ftbPersonnelsRegistrationFormFields.stream().collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, Function.identity())); + LambdaQueryWrapper fieldOptionLambdaQueryWrapper = Wrappers.lambdaQuery(); + fieldOptionLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark,0); + fieldOptionLambdaQueryWrapper.in(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId,formFields); + List ftbPersonnelsRegistrationFormFieldOptions = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList(fieldOptionLambdaQueryWrapper); + Map fieldOptionMaps = ftbPersonnelsRegistrationFormFieldOptions.stream() + .collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, FtbPersonnelsRegistrationFormFieldOption::getName)); + // 查询信息 + LambdaQueryWrapper staffRosterLambdaQueryWrapper = Wrappers.lambdaQuery(); + staffRosterLambdaQueryWrapper.in(FtbPersonnelsStaffRoster::getUserId,req.getUserIds()); + List ftbPersonnelsStaffRosters = ftbPersonnelsStaffRosterMapper.selectList(staffRosterLambdaQueryWrapper); + Map rostUserIdMap = ftbPersonnelsStaffRosters.stream().collect(Collectors.toMap(SuperBaseEntity.SuperIBaseEntity::getId, FtbPersonnelsStaffRoster::getUserId)); + List rosterIds = ftbPersonnelsStaffRosters.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + // 获取档案信息 + LambdaQueryWrapper formDataLambdaQueryWrapper = Wrappers.lambdaQuery(); + formDataLambdaQueryWrapper.in(FtbPersonnelsStaffRegistrationFormData::getRosterId,rosterIds); + formDataLambdaQueryWrapper.in(FtbPersonnelsStaffRegistrationFormData::getFormTypeId,forTypeIds); + formDataLambdaQueryWrapper.in(FtbPersonnelsStaffRegistrationFormData::getFormFieldId,formFields); + List ftbPersonnelsStaffRegistrationFormData = ftbPersonnelsStaffRegistrationFormDataMapper.selectList(formDataLambdaQueryWrapper); + if (CollUtil.isEmpty(ftbPersonnelsStaffRegistrationFormData)) { + return List.of(); + } + Map> forDataGroupBys = ftbPersonnelsStaffRegistrationFormData.stream() + .collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)); + + List result = forDataGroupBys.entrySet().stream().map(entry -> { + FtbXcCustomFieldVo ftbXcCustomFieldVo = new FtbXcCustomFieldVo(); + String userId = rostUserIdMap.get(entry.getKey()); + ftbXcCustomFieldVo.setUserId(userId); + if (CollUtil.isNotEmpty(entry.getValue())) { + List customFields = entry.getValue().stream().map(a -> { + if (StrUtil.isBlank(a.getValue())) { + return null; + } + FtbXcCustomFieldVo.CustomField customField = new FtbXcCustomFieldVo.CustomField(); + FtbPersonnelsRegistrationFormField registrationFormField = formFieldMap.get(a.getFormFieldId()); + customField.setCustomFieldName(registrationFormField.getName()); + if (registrationFormField.getType() == 2) { + customField.setCustomFieldValue(fieldOptionMaps.getOrDefault(a.getValue(),"")); + } else if (registrationFormField.getType() == 3) { + // 多选逗号隔开 + String multiSelect = Arrays.stream(a.getValue().split(",")) + .map(b -> fieldOptionMaps.getOrDefault(b, "")) + .filter(StrUtil::isNotBlank) + .collect(Collectors.joining(",")); + customField.setCustomFieldValue(multiSelect); + } else { + customField.setCustomFieldValue(a.getValue()); + } + return customField; + }).filter(Objects::nonNull).collect(Collectors.toList()); + ftbXcCustomFieldVo.setCustomFieldList(customFields); + } + return ftbXcCustomFieldVo; + }).collect(Collectors.toList()); + return result; + } + + private void switchTenantId(String tenantId) { + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffArchivesHistoryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffArchivesHistoryController.java new file mode 100644 index 0000000..6815611 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffArchivesHistoryController.java @@ -0,0 +1,98 @@ +package jnpf.personnels.controller.web.roster; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.dto.staff.growth.FtbPersonnelsStaffGrowthLogDto; +import jnpf.model.personnels.dto.staff.registerform.GroupFieldDataDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.roster.FtbPersonnelsStaffArchivesHistoryVo; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsStaffArchivesHistoryService; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * 离职员工档案信息模块 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/web/personnelsStaffArchivesHistory") +@Deprecated(since = "v2.1 二次入职时需要将登记表复原回填") +public class FtbPersonnelsStaffArchivesHistoryController { + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + private FtbPersonnelsStaffArchivesHistoryService archivesHistoryService; + + @Autowired + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + /** + * 查询离职档案信息 + * + * @param userId 用户ID + * @return + */ + @GetMapping("/get/{userId}") + public ActionResult> get(@PathVariable("userId") String userId) { + FtbPersonnelsStaffArchivesHistoryVo info = archivesHistoryService.getInfo(userId); + if (null != info && StringUtils.isNotEmpty(info.getArchives())) { + List list = JSONUtil.toList(info.getArchives(), FormTypeDto.class); + // 实际入职日期 + Stream groupFieldDataDtoStream = list.stream().flatMap(a -> a.getGroupFieldDataDtoList().stream()); + GroupFieldDataDto first = groupFieldDataDtoStream.filter(a -> Objects.nonNull(a.getFormFieldDto()) && "actualStartDate".equals(a.getFormFieldDto().getId())) + .findFirst().orElse(null); + GroupFieldDataDto companyAge = list.stream().flatMap(a -> a.getGroupFieldDataDtoList().stream()) + .filter(a -> Objects.nonNull(a.getFormFieldDto()) && "companyAge".equals(a.getFormFieldDto().getId())) + .findFirst().orElse(null); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, userId); + FtbPersonnelsStaffRoster staffRoster = ftbPersonnelsStaffRosterMapper.selectOne(queryWrapper); + if (Objects.nonNull(first) && Objects.nonNull(staffRoster) && Objects.nonNull(staffRoster.getDepartDate())) { + Date departDate = staffRoster.getDepartDate(); + String userValue = first.getFormFieldDto().getUserValue(); + DateTime dateTime = DateUtil.parse(userValue, "yyyy-MM-dd"); + long between = DateUtil.between(dateTime, departDate, DateUnit.DAY); + if (Objects.nonNull(companyAge)) { + companyAge.getFormFieldDto().setUserValue(String.valueOf(between)); + } + } + return ActionResult.success(list); + } + return ActionResult.fail("未查询到该用户的档案信息"); + } + + + /** + * 查询离职员工成长列表 + * + * @param userId + * @return + */ + @GetMapping("/query-list/{userId}") + public ActionResult> pageLists(@PathVariable("userId") String userId) { + List list = growthLogService.queryAll(userId); + return ActionResult.success(list); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffHomePageController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffHomePageController.java new file mode 100644 index 0000000..f5d952a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffHomePageController.java @@ -0,0 +1,512 @@ +package jnpf.personnels.controller.web.roster; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.enums.GrowthLogEnum; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJobTenureDTO; +import jnpf.model.personnels.dto.roster.meta.FtbPersonnlesJoeInfo; +import jnpf.model.personnels.dto.staff.employment.*; +import jnpf.model.personnels.dto.staff.roster.StaffDepartDto; +import jnpf.model.personnels.dto.staff.roster.StaffPromotionDto; +import jnpf.model.personnels.dto.staff.roster.StaffRegularDto; +import jnpf.model.personnels.dto.staff.transfer.TransferGrowthLogDto; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.po.FtbPersonnelsStaffGrowthLog; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsTransferManage; +import jnpf.model.personnels.vo.roster.FtbHomePageRewardVO; +import jnpf.model.personnels.vo.roster.FtbPersonnelsChangeInfoVO; +import jnpf.model.personnels.vo.roster.FtbPersonnlesJobTenureVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondRecordVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.*; +import jnpf.util.NoDataSourceBind; +import jnpf.util.UserProvider; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * web-app员工主页信息 + * + * @author wangchunxiang + * @date 2024/10/08 + */ +@RestController +@RequestMapping("/web-app/staff-home-page") +public class FtbPersonnelsStaffHomePageController { + + @Resource + private FtbPersonnelsStaffHomePageService ftbPersonnelsStaffHomePageService; + + @Resource + private FtbPersonnelsStaffGrowthLogService staffGrowthLogService; + + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + @Resource + FtbPersonnelsTransferManageService transferManageService; + @Resource + private FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentService; + + + @Autowired + private V2UserApi userApi; + + /** + * web员工主页-查询个人承诺 + * + * @param id 用户主键id + * @return {@link ActionResult } 图片云存储路径 + */ + @GetMapping("/check-personal-commitments/{id}") + public ActionResult checkPersonalCommitments(@PathVariable("id") String id) { + return ActionResult.success("",ftbPersonnelsStaffHomePageService.checkPersonalCommitments(id)); + } + + /** + * 员工主页-奖励记录 + * + * @param id 用户主键id + * @return {@link ActionResult }<{@link List }<{@link FtbHomePageRewardVO }>> + */ + @GetMapping("/award-record/{id}") + public ActionResult> getRewardList(@PathVariable("id") String id) { + List ftbHomePageRewardVOList = ftbPersonnelsStaffHomePageService.getRewardList(id); + return ActionResult.success(ftbHomePageRewardVOList); + } + + /** + * 员工主页-乐捐记录 + * + * @param id 用户主键id + * @return {@link ActionResult }<{@link List }<{@link FtbHomePageRewardVO }>> + */ + @GetMapping("/donation-records/{id}") + public ActionResult> donationRecords(@PathVariable("id") String id) { + List ftbHomePageRewardVOList = ftbPersonnelsStaffHomePageService.donationRecords(id); + return ActionResult.success(ftbHomePageRewardVOList); + } + + + /** + * 员工主页-异动记录-入职 + */ + @GetMapping("/entry-list") + public ActionResult> getEntryList(@RequestParam(name = "userId") String userId) { + List collect = getFtbPersonnelsEmployApplyListDtos(userId); + return ActionResult.success(collect); + } + + @NotNull + private List getFtbPersonnelsEmployApplyListDtos(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId, userId); + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,0,3); + List list = staffGrowthLogService.list(queryWrapper); + Map userNameMap = getUserNameMap(list); + List collect = list.stream().map(item -> { + FtbPersonnelsEmployApplyListDto covert = FtbPersonnelsEmployApplyListDto.covert(item); + covert.setWorkName(userNameMap.getOrDefault(item.getUserId(), "-")); + covert.setOperator(userNameMap.getOrDefault(item.getCreatorUserId(), "-")); + return covert; + }).collect(Collectors.toList()); + return collect; + } + + /** + * 员工主页-异动记录-转正 + * + * 变化类型 + * - {@link GrowthLogEnum 类型} + * + */ + @GetMapping("/regularization-list") + public ActionResult> getRegularizationList(@RequestParam(name = "userId") String userId) { + List regularVOS = getFtbPersonnelsRegularVOS(userId); + return ActionResult.success(regularVOS); + } + + @NotNull + private List getFtbPersonnelsRegularVOS(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId, userId); + // 转正 + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,GrowthLogEnum.BECOME_REGULAR_WORKER.getCode()); + List list = staffGrowthLogService.list(queryWrapper); + Map userNameMap = getUserNameMap(list); + List regularVOS = list.stream().map(item -> { + StaffRegularDto staffRegularDto = JSONUtil.toBean(item.getDetail(), StaffRegularDto.class); + FtbPersonnelsRegularVO covert = FtbPersonnelsRegularVO.covert(staffRegularDto); + covert.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + covert.setProcessingDate(item.getCreatorTime()); + covert.setEmployeeID(item.getEmployeeId()); + covert.setOperator(userNameMap.getOrDefault(item.getCreatorUserId(), "-")); + return covert; + }).collect(Collectors.toList()); + return regularVOS; + } + + /** + * 员工主页-异动记录-调岗 + */ + @GetMapping("/transfer-list") + public ActionResult> getTransferList(@RequestParam(name = "userId") String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId,userId); + // 调岗 + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,GrowthLogEnum.TRANSFER_POSITION.getCode()); + List list = staffGrowthLogService.list(queryWrapper); + Map userNameMap = getUserNameMap(list); + List regularVOS = list.stream().map(item -> { + TransferGrowthLogDto staffRegularDto = JSONUtil.toBean(item.getDetail(), TransferGrowthLogDto.class); + FtbPersonnelsTransferVO covert = FtbPersonnelsTransferVO.covert(staffRegularDto); + covert.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + covert.setProcessingDate(item.getCreatorTime()); + covert.setEmployeeID(item.getEmployeeId()); + return covert; + }).collect(Collectors.toList()); + return ActionResult.success(regularVOS); + } + /** + * 员工主页-异动记录-晋升 + */ + @GetMapping("/promotion-list") + public ActionResult> getPromotionList(@RequestParam(name = "userId") String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId,userId); + // 晋升 + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,GrowthLogEnum.promotion.getCode()); + List list = staffGrowthLogService.list(queryWrapper); + Map userNameMap = getUserNameMap(list); + List regularVOS = list.stream().map(item -> { + StaffPromotionDto staffRegularDto = JSONUtil.toBean(item.getDetail(), StaffPromotionDto.class); + FtbPersonnelsPromotionLogVO covert = FtbPersonnelsPromotionLogVO.covert(staffRegularDto); + covert.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + covert.setProcessingDate(item.getCreatorTime()); + covert.setEmployeeID(item.getEmployeeId()); + return covert; + }).collect(Collectors.toList()); + return ActionResult.success(regularVOS); + } + /** + * 员工主页-异动记录-离职 + */ + @GetMapping("/leave-list") + public ActionResult> getLeaveList(@RequestParam(name = "userId") String userId) { + List regularVOS = getFtbPersonnelsTurnoverLogVOS(userId); + return ActionResult.success(regularVOS); + } + + @NotNull + private List getFtbPersonnelsTurnoverLogVOS(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId, userId); + // 离职 + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,GrowthLogEnum.DEPART.getCode(),GrowthLogEnum.PRE_TRAIL.getCode()); + List list = staffGrowthLogService.list(queryWrapper); + Map userNameMap = getUserNameMap(list); + List regularVOS = list.stream().map(item -> { + FtbPersonnelsTurnoverLogVO covert = new FtbPersonnelsTurnoverLogVO(); + if (StringUtils.isNotEmpty(item.getDetail()) && !"[]".equals(item.getDetail())) { + // 数据兼容 + StaffDepartDto staffDepartDto; + try { + List staffDepartDtos = JSONArray.parseArray(item.getDetail(), StaffDepartDto.class); + staffDepartDto = staffDepartDtos.stream().findFirst().orElse(null); + }catch (Exception e){ + staffDepartDto = JSONObject.parseObject(item.getDetail(), StaffDepartDto.class); + } + covert = FtbPersonnelsTurnoverLogVO.covert(staffDepartDto); + } + covert.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + covert.setProcessingDate(item.getChangeDate()); + covert.setEmployeeID(item.getEmployeeId()); + covert.setOperator(userNameMap.getOrDefault(item.getCreatorUserId(), "-")); + return covert; + }).collect(Collectors.toList()); + return regularVOS; + } + + /** + * 员工主页-异动记录-调动 + */ + @GetMapping("/mobilize-list") + public ActionResult> getMobilizeList(@RequestParam(name = "userId") String userId) { + List regularVOS = getFtbPersonnelsMoveLogVOS(userId); + return ActionResult.success(regularVOS); + } + + @NotNull + private List getFtbPersonnelsMoveLogVOS(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId, userId); + // 晋升 调岗 + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,GrowthLogEnum.promotion.getCode(),GrowthLogEnum.TRANSFER_POSITION.getCode()); + List list = staffGrowthLogService.list(queryWrapper); + Map userNameMap = getUserNameMap(list); + List regularVOS = list.stream().map(item -> { + FtbPersonnelsMoveLogVO covert = new FtbPersonnelsMoveLogVO(); + if (item.getGrowthType().equals(GrowthLogEnum.promotion.getCode())) { + StaffPromotionDto staffRegularDto = JSONUtil.toBean(item.getDetail(), StaffPromotionDto.class); + covert = FtbPersonnelsMoveLogVO.covert(staffRegularDto); + } + if (item.getGrowthType().equals(GrowthLogEnum.TRANSFER_POSITION.getCode())) { + TransferGrowthLogDto staffRegularDto = JSONUtil.toBean(item.getDetail(), TransferGrowthLogDto.class); + covert = FtbPersonnelsMoveLogVO.covertTransfer(staffRegularDto); + } + covert.setOperator(userNameMap.getOrDefault(item.getCreatorUserId(), "-")); + covert.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + covert.setProcessingDate(item.getCreatorTime()); + covert.setEmployeeID(item.getEmployeeId()); + return covert; + }).collect(Collectors.toList()); + //新数据 调动类型0调岗1晋升2降职3调店 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsTransferManage::getProcessingStatus,List.of(6,2)); + wrapper.eq(FtbPersonnelsTransferManage::getImplementType,1); + wrapper.eq(FtbPersonnelsTransferManage::getUserId, userId); + List transferManages = transferManageService.list(wrapper); + List personnelsMoveLogVOS = transferManages.stream().map(item -> { + FtbPersonnelsMoveLogVO logVO = FtbPersonnelsMoveLogVO.covertPromotion(item); + logVO.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + Integer state = item.getState(); + if (state == 0) { + logVO.setType("调岗"); + } else if (state == 1) { + logVO.setType("晋升"); + } else if (state == 2) { + logVO.setType("降职"); + } else if (state == 3) { + logVO.setType("调店"); + } + List list1 = new ArrayList<>(); + if (item.getLastModifyUserId() != null) { + list1.add(item.getLastModifyUserId()); + } + list1.add(item.getCreatorUserId()); + ActionResult> boundBatch = userApi.getUserPrimaryBoundBatch(list1, UserProvider.getUser().getTenantId()); + Map userBoundVOMap = new HashMap<>(); + if (boundBatch.getData() != null) { + userBoundVOMap = boundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (r1, r2) -> r1)); + } + if (item.getLastModifyUserId() != null && item.getProcessingStatus() == 2) { + if (userBoundVOMap.containsKey(item.getLastModifyUserId())) { + logVO.setOperator(userBoundVOMap.get(item.getLastModifyUserId()).getUserName()); + } else { + logVO.setOperator("-"); + } + }else if (item.getLastModifyUserId() != null && item.getProcessingStatus() == 6) { + if (userBoundVOMap.containsKey(item.getCreatorUserId())) { + logVO.setOperator(userBoundVOMap.get(item.getCreatorUserId()).getUserName()); + } else { + logVO.setOperator("-"); + } + } + return logVO; + }).collect(Collectors.toList()); + regularVOS.addAll(personnelsMoveLogVOS); + return regularVOS; + } + + @NotNull + private Map getUserNameMap(List list) { + List userIds = new ArrayList<>(); + userIds.addAll(list.stream().map(FtbPersonnelsStaffGrowthLog::getUserId).collect(Collectors.toList())); + userIds.addAll( list.stream().map(SuperBaseEntity.SuperCBaseEntity::getCreatorUserId).collect(Collectors.toList())); + + ActionResult> boundBatch = userApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + Map userBoundVOMap = new HashMap<>(); + if (boundBatch.getData() != null) { + userBoundVOMap = boundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getUserName, (r1, r2) -> r1)); + } + return userBoundVOMap; + } + + /** + * 员工主页-异动记录-借调 + */ + @GetMapping("/borrow-list") + public ActionResult> getBorrowList(@RequestParam(name = "userId") String userId) { + List regularVOS = getFtbPersonnelsSecondRecordVOS(userId); + return ActionResult.success(regularVOS); + } + + @NotNull + private List getFtbPersonnelsSecondRecordVOS(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getUserId, userId); + queryWrapper.in(FtbPersonnelsSecondmentManagement::getTransferStatus,6,2); + List secondmentManagements = ftbPersonnelsSecondmentService.list(queryWrapper); + List userIds = new ArrayList<>(); + userIds.addAll( secondmentManagements.stream().map(SuperBaseEntity.SuperCBaseEntity::getCreatorUserId).collect(Collectors.toList())) ; + userIds.addAll( secondmentManagements.stream().map(FtbPersonnelsSecondmentManagement::getUserId).collect(Collectors.toList())); + Map userNameMap = new HashMap<>(); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); + queryWrapper1.in(FtbPersonnelsStaffRoster::getUserId, userIds); + List serviceOne = staffRosterService.list(queryWrapper1); + userNameMap = serviceOne.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, Function.identity())); + } + ActionResult> boundBatch = userApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + Map userBoundVOMap = new HashMap<>(); + if (boundBatch.getData() != null) { + userBoundVOMap = boundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getUserName, (r1, r2) -> r1)); + } + Map finalUserNameMap = userNameMap; + Map finalUserBoundVOMap = userBoundVOMap; + List regularVOS = secondmentManagements.stream().map(item -> { + FtbPersonnelsSecondRecordVO secondRecordVO = FtbPersonnelsSecondRecordVO.covert(item); + secondRecordVO.setCreatorUserName(finalUserBoundVOMap.getOrDefault(secondRecordVO.getCreatorUserId(), "-")); + FtbPersonnelsStaffRoster personnelsStaffRoster = finalUserNameMap.get(secondRecordVO.getUserId()); + secondRecordVO.setEmployeeId( personnelsStaffRoster == null ? "-" : personnelsStaffRoster.getSystemWokerId()); + secondRecordVO.setUserName(personnelsStaffRoster == null ? "-" : personnelsStaffRoster.getName()); + return secondRecordVO; + }).collect(Collectors.toList()); + return regularVOS; + } + + /** + *根据userId 查询人事异动信息 + */ + @GetMapping("/get-personnel-change-info") + public FtbPersonnelsChangeInfoVO getPersonnelChangeInfo(@RequestParam(name = "userId") String userId) { + // 入 + List ftbPersonnelsEmployApplyListDtos = getFtbPersonnelsEmployApplyListDtos(userId); + // 转正 + List regularVOS = getFtbPersonnelsRegularVOS(userId); + // 调动 + List ftbPersonnelsMoveLogVOS = getFtbPersonnelsMoveLogVOS(userId); + // 借调 + List secondRecordVOS = getFtbPersonnelsSecondRecordVOS(userId); + // 离职 + List ftbPersonnelsTurnoverLogVOS = getFtbPersonnelsTurnoverLogVOS(userId); + + FtbPersonnelsChangeInfoVO ftbPersonnelsChangeInfoVO = new FtbPersonnelsChangeInfoVO(); + ftbPersonnelsChangeInfoVO.setOnboarding(ftbPersonnelsEmployApplyListDtos); + ftbPersonnelsChangeInfoVO.setRegularization(regularVOS); + ftbPersonnelsChangeInfoVO.setMobilize(ftbPersonnelsMoveLogVOS); + ftbPersonnelsChangeInfoVO.setSeconded(secondRecordVOS); + ftbPersonnelsChangeInfoVO.setTurnover(ftbPersonnelsTurnoverLogVOS); + return ftbPersonnelsChangeInfoVO; + } + + + /** + * 查询当前用户岗龄 + */ + @PostMapping("/query-positionTenure-age") + @NoDataSourceBind + public List queryPositionTenureAge(@RequestBody FtbPersonnlesJobTenureDTO req) { + + List personnlesJoeInfos = req.getUserInfo(); + return personnlesJoeInfos.parallelStream().map(item -> { + try { + TenantDataSourceUtil.switchTenant(req.getTenantId()); + } catch (LoginException e) { + throw new RuntimeException(e); + } + // 查询调动是否有数据没有数据就查询实际入职时间 + List listDtoList = getFtbPersonnelsEmployApplyListDtos(item.getUserId()); + if (CollUtil.isEmpty(listDtoList)) { + return null; + } + // 取最后一条数据(按入职时间排序后最新的) + FtbPersonnelsEmployApplyListDto latestRecord = listDtoList.stream().peek(v->{ + if (v.getActualStartDate() == null) { + v.setActualStartDate(v.getCreationTime()); + } + }) + .max(Comparator.comparing(FtbPersonnelsEmployApplyListDto::getActualStartDate, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElse(null); + if (latestRecord == null) { + return null; + } + List moveLogVOS = getFtbPersonnelsMoveLogVOS(item.getUserId()); + // 实际入职日期 + Date actualStartDate = latestRecord.getActualStartDate() == null ? latestRecord.getCreationTime() : latestRecord.getActualStartDate(); + // 过滤调动记录在 actualStartDate 之前的数据 + List ftbPersonnelsMoveLogVOList = moveLogVOS.stream().filter(v -> ObjectUtil.isNotEmpty(v.getProcessingDate()) && v.getProcessingDate().after(actualStartDate)).collect(Collectors.toList()); + // 没有调动数据 且 岗位id一致 直接以当前时间进行计算岗龄 + if(CollUtil.isEmpty(ftbPersonnelsMoveLogVOList) && item.getPositionId().equals(latestRecord.getCurrPosition())){ + long betweenedDay = DateUtil.betweenDay(actualStartDate, new Date(), true); + FtbPersonnlesJobTenureVO tenureVO = new FtbPersonnlesJobTenureVO(); + tenureVO.setUserId(item.getUserId()); + tenureVO.setPositionId(item.getPositionId()); + tenureVO.setJobTenure(Math.toIntExact(betweenedDay)); + return tenureVO; + }else{ + // 调动数据不为空 调动前或者调动后的岗位一致,或者岗位没有发生变化 + List ftbPersonnelsMoveLogVOS = moveLogVOS.stream().filter(v -> v.getOldPosition().equals(item.getPositionId()) || v.getNewPosition().equals(item.getPositionId())).collect(Collectors.toList()); + if(CollUtil.isEmpty(ftbPersonnelsMoveLogVOS)){ + return null; + } + ftbPersonnelsMoveLogVOS.sort(Comparator.comparing(FtbPersonnelsMoveLogVO::getProcessingDate)); + int size = ftbPersonnelsMoveLogVOS.size(); + + // 累计岗龄天数 + long totalJobTenure = 0; + + // 如果第一条调动记录的调动前岗位等于入职岗位且等于查询岗位,则计算从入职到第一次调动的时间 + if(item.getPositionId().equals(latestRecord.getCurrPosition()) && + item.getPositionId().equals(ftbPersonnelsMoveLogVOS.get(0).getOldPosition())){ + long betweenedDay = DateUtil.betweenDay(actualStartDate, ftbPersonnelsMoveLogVOS.get(0).getProcessingDate(), true); + totalJobTenure += betweenedDay; + } + + // 遍历调动记录,查找当前岗位的所有任职区间并累加 + for (int i = 0; i < size; i++) { + FtbPersonnelsMoveLogVO currentMove = ftbPersonnelsMoveLogVOS.get(i); + // 如果当前岗位是调动后的岗位,计算该段任职时间 + if (item.getPositionId().equals(currentMove.getNewPosition())) { + if (i < size - 1) { + // 有下一次调动,计算到下一次调动的时间 + FtbPersonnelsMoveLogVO nextMove = ftbPersonnelsMoveLogVOS.get(i + 1); + long betweenedDay = DateUtil.betweenDay(currentMove.getProcessingDate(), nextMove.getProcessingDate(), true); + totalJobTenure += betweenedDay; + } else { + // 是最后一条记录,计算到现在的时间 + long betweenedDay = DateUtil.betweenDay(currentMove.getProcessingDate(), new Date(), true); + totalJobTenure += betweenedDay; + } + } + } + + // 如果累计岗龄大于 0,返回结果 + if (totalJobTenure > 0) { + FtbPersonnlesJobTenureVO tenureVO = new FtbPersonnlesJobTenureVO(); + tenureVO.setUserId(item.getUserId()); + tenureVO.setPositionId(item.getPositionId()); + tenureVO.setJobTenure(Math.toIntExact(totalJobTenure)); + return tenureVO; + } + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + + + @PostMapping("/get-personnel-change-info-batch") + public Map getPersonnelChangeInfoBatch(@RequestBody List userIds) { + return ftbPersonnelsStaffHomePageService.getBatchPersonnelChangeInfo(userIds); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterController.java new file mode 100644 index 0000000..d3bcb64 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterController.java @@ -0,0 +1,688 @@ +package jnpf.personnels.controller.web.roster; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.FtbCheckPermission; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.config.ConfigValueUtil; +import jnpf.cultivate.service.FtbCultivatePositionForAppService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.position.app.FtbCultivatePositionForAppNewDTO; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.position.app.FtbSubordinateLearningCoursesVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionWithPersonelVO; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.roster.QueryCompanyAgeDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.dto.staff.roster.*; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.employment.BatchByPrimaryIdReq; +import jnpf.model.personnels.req.roster.StaffRosterListExportReq; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.req.roster.StaffRosterReq; +import jnpf.permission.dto.QueryUserListDTO; +import jnpf.permission.model.user.UserListVO; +import jnpf.permission.model.user.UserQueryDto; +import jnpf.permission.vo.user.UserListMatchVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.FtbPersonnelsRosterManagerApi; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.util.FtbUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web员工花名册模块 + * + * @author xxxxx + */ +@RestController +@Slf4j +@RequestMapping("/web/personnels-staff-roster") +public class FtbPersonnelsStaffRosterController implements FtbPersonnelsRosterManagerApi { + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + + @Autowired + private ConfigValueUtil configValueUtil; + + @Resource + FtbCultivatePositionForAppService positionForAppService; + + /** + * 花名册查询列表 + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @GetMapping("/query-list") + public ActionResult> pageLists(@Validated StaffRosterListReq req) { // 记录开始时间 + PageInfo pageVo = staffRosterService.getPageList(req, false); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 花名册查询列表 _post + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @PostMapping("/query-list/post") + @FtbCheckPermission("employeeRoster") + public ActionResult> pageListsPost(@Validated @RequestBody StaffRosterListReq req) { + long startTime = System.currentTimeMillis(); + + PageInfo pageVo = staffRosterService.getPageList(req, false); + + // 记录结束时间 + long endTime = System.currentTimeMillis(); + // 计算执行时间 + long executionTime = endTime - startTime; + // 打印执行时间 + System.out.println("Execution time: " + executionTime + " milliseconds"); + + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 花名册查询列表 postWithSalary + * + * @param req + * @return {@link ActionResult}<{@link FtbPersonnelsStaffEmploymentApplyDto}> + */ + @PostMapping("/query-list/postWithSalary") + public ActionResult> postWithSalary(@Validated @RequestBody StaffRosterListReq req) { + PageInfo pageVo = staffRosterService.postWithSalary(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + @PostMapping("/query-list/post-with-salary-no-page") + public List postWithSalaryNoPage(@Validated @RequestBody StaffRosterListReq req) { + try { + return staffRosterService.postWithSalaryNoPage(req); + } catch (Exception e) { + log.error("查询花名册权限异常 :{}", e.getMessage()); + return new ArrayList<>(); + } + } + + /** + * 花名册列表查询-无权限 + * @param req + * @return ActionResult + */ + @PostMapping("/query-list/byUserIds") + @Override + public ActionResult> getPersonnelByUserIds(@RequestBody StaffRosterListReq req) { + List personnelUserList = staffRosterService.getPersonnelUserList(req); + return ActionResult.success(personnelUserList); + } + + /** + * 获取员工花名册详情 + * + * @param id + * @return + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + FtbPersonnelsStaffRosterDto info = staffRosterService.getInfo(id); + return ActionResult.success(info); + } + + /** + * 获取员工花名册详情(参数) + * + * @param userId 用户ID + * @return + */ + @GetMapping("/getInfo") + public ActionResult getInfo(@RequestParam("userId") String userId) { + StaffHomeDto info = staffRosterService.queryWorkerHomeDetail(userId); + return ActionResult.success(info); + } + + /** + * 获取员工花名册详情(数组查询第一个) + * + * @param userIds 用户ID + * @return + */ + @GetMapping("/getInfoForListOne") + public ActionResult getInfoForListOne(@RequestParam("userIds") String userIds) { + + StaffHomeDto info = staffRosterService.queryWorkerHomeDetail(userIds); + return ActionResult.success(info); + } + + + /** + * 查询员工信息(返回指定字段相关字段) + * + * @param userIds 用户ID + * @return + */ + @GetMapping("/getInfoForUserIdOneField") + public ActionResult> getInfoForUserIdOneField(@RequestParam("field") String field, @RequestParam("userIds") String userIds) { + + Map info = staffRosterService.getInfoForUserIdOneField(field,userIds); + return ActionResult.success(info); + } + + /** + * 从花名册中批量删除 + * + * @param req + * @return {@link ActionResult} + */ + @DeleteMapping("/batch-delete") + public ActionResult batchDelete(@RequestBody @Validated BatchByPrimaryIdReq req) { + if (CollectionUtil.isEmpty(req.getIds())) { + throw new IllegalArgumentException("参数错误"); + } + long controllerStart = System.currentTimeMillis(); + staffRosterService.deleteBatchData(req.getIds()); + long controllerEnd = System.currentTimeMillis(); + log.error("花名册批量删除(batch-delete) 控制层调用deleteBatchData 条数={} 耗时={}ms", + req.getIds().size(), controllerEnd - controllerStart); + return ActionResult.success(); + } + + + /** + * 检测花名册是否可以删除 + * + * @param req + * @return {@link ActionResult} + */ + @PostMapping("/check-batch-delete") + public ActionResult> checkBatchDelete(@RequestBody @Validated BatchByPrimaryIdReq req) { + CanDeleteMsg dto = staffRosterService.checkBatchDelete(req.getIds()); + List dtoData = (List) dto.getData(); + return ActionResult.success(dtoData); + } + + + /** + * 查询员工主页信息 + * + * @param userId 手机号 + * @return + */ + @GetMapping("/worker-home-detail/{userId}") + public ActionResult queryWorkerHomeDetail(@PathVariable("userId") String userId) { + StaffHomeDto info = staffRosterService.queryWorkerHomeDetail(userId); + return ActionResult.success(info); + } + + + /** + * 花名册员工数量统计 + * + * @return + */ + @GetMapping("/worker-number/statistics") + public ActionResult statisticsWorkerNumber() { + + return ActionResult.success(staffRosterService.statisticsWorkerNumber()); + } + + /** + * 花名册用户数据导出 + * + * @param req + * @param response + * @return + * @throws IOException + */ + @PostMapping("/roster-export") + public void exportToExcel(@RequestBody StaffRosterListExportReq req, HttpServletResponse response) throws IOException { + // 导出全部 + if (CollUtil.isEmpty(req.getIds())) { + req.setPageSize(Integer.MAX_VALUE); + PageInfo pageList = staffRosterService.getPageList(req, true); + List rosterIds = pageList.getList().stream().map(FtbPersonnelsStaffRosterDto::getId).collect(Collectors.toList()); + req.setIds(rosterIds); + } + //查询数据 + List dataList = staffRosterService.queryRosterInfoByIds(req.getIds(), req.getFormFieldOptionIds(),null); + EasyExcelUtils.dynamicHeaderGenerationRoster(response, "花名册数据导出", dataList); + } + + /** + * 花名册用户数据自定义方案导出 + * + * @param req + * @param response + * @return + * @throws IOException + */ + @PostMapping("/roster-export-scheme") + public void exportToExcelScheme(@RequestBody StaffRosterListExportReq req, HttpServletResponse response) throws IOException { + if (StrUtil.isEmpty(req.getSchemeId())) { + throw new IllegalArgumentException("请选择导出方案"); + } + // 导出全部 + if (CollUtil.isEmpty(req.getIds())) { + req.setPageSize(Integer.MAX_VALUE); + PageInfo pageList = staffRosterService.getPageList(req, true); + List rosterIds = pageList.getList().stream().map(FtbPersonnelsStaffRosterDto::getId).collect(Collectors.toList()); + req.setIds(rosterIds); + } + //查询数据 + List dataList = staffRosterService.queryRosterInfoByIds(req.getIds(), req.getFormFieldOptionIds(),req.getSchemeId()); + EasyExcelUtils.dynamicHeaderGeneration(response, "花名册数据导出", dataList); + } + + /** + * 检测当前登录用户是不是档案管理员 + * + * @return {@link ActionResult}<{@link String}> + */ + @GetMapping("/check-my-is-archive-manager") + public ActionResult checkMyIsArchiveManager() { + Boolean isManager = staffRosterService.checkCurrLoginUserArchiveManager(); + return ActionResult.success("成功", isManager); + } + + /** + * 检测当前花名册处于流程中【调岗、转正、离职】 + * + * @param id 花名册ID + * @return 0、无流程 ,1转正 2,调岗, 3离职, 4晋升 + */ + @GetMapping("/check-roster-run-task/{id}") + public ActionResult checksItInTheReviewProcess(@PathVariable("id") String id) { + String loginUserId = UserProvider.getLoginUserId(); + Integer ty = staffRosterService.checksItInTheReviewProcess(id); + return ActionResult.success("成功", ty); + } + + + /** + * 定时任务修改员工司年龄 + * + * @return + */ + @Override + @NoDataSourceBind + @GetMapping("/updateCompanyAge") + public ActionResult updateCompanyAge(@RequestParam("tenantId") String tenantId) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + //检测健康证到期,发送短信 + staffRosterService.updateCompanyAge(); + //修改试岗结束 + staffRosterService.timingUpdateTrialJob(tenantId); + //staffRosterService.updateWorkerAge(); + return ActionResult.success(); + } + + /** + * 查询直属主管 + * + * @param dto + * @return + */ + @GetMapping("/queryAllUserForOrgAndStatus") + public ActionResult> getAllUser(QueryUserListDTO dto) { + List list = staffRosterService.queryAllUserForOrgAndStatus(dto); + return ActionResult.success("成功", list); + } + + /** + * 定时任务发动消息 + * + * @param tenantId + * @return + */ + @NoDataSourceBind + @GetMapping("/timingAlertTrialJob/{tenantId}") + @Deprecated(since = "人事2.1废弃试岗状态") + public ActionResult> timingAlertTrialJob(@PathVariable("tenantId") String tenantId) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + //staffRosterService.timingAlertTrialJob(tenantId); + return ActionResult.success("成功"); + } + + /** + * 修改员工司年龄 + * + * @return + */ + @NoDataSourceBind + @PostMapping("/queryCompanyAge/{tenantId}") + public ActionResult> queryCompanyAge(@RequestBody List userIds, @PathVariable("tenantId") String tenantId) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + //检测健康证到期,发送短信 + List list = staffRosterService.queryCompanyAge(userIds); + return ActionResult.success("成功", list); + } + + /** + * 绑定手机号 + * + * @param userId 用户ID + * @param phone 手机号 + * @return + */ + @Override + @PostMapping("/bindPhone") + @NoDataSourceBind + public ActionResult bindPhone(@RequestParam("tenantCode") String tenantCode, @RequestParam("userId") String userId, @RequestParam("phone") String phone) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantCode)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantCode); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + Boolean result = staffRosterService.bindPhone(userId, phone); + return ActionResult.success("成功", result); + } + + /** + * 查看培训数据 + * 培训v1.1 + */ + @GetMapping("/queryTrainData") + public ActionResult queryTrainData(@RequestParam("userId") String userId, + @RequestParam("postId") String postId) { + FtbCultivatePromotionWithPersonelVO info = staffRosterService.queryTrainData(userId, postId); + return ActionResult.success("成功", info); + } + + /** + * 查看课程培训数据(培训v1.1) + */ + @GetMapping("/queryCourseTrainDataDetail") + public ActionResult> queryCourseTrainDataDetail(FtbCultivatePositionForAppNewDTO dto, + CultivatePage page) { + Page result = page.coverCultivatePage(); + result = positionForAppService.subordinateLearningCourses(dto, result); + return ActionResult.success(CultivatePage.coverPageList(result)); + } + + + /** + * 查看鉴定培训数据(培训v1.1) + */ + @GetMapping("/queryIdentityTrainDataDetail") + public ActionResult> queryIdentityTrainDataDetail(FtbCultivatePositionForAppNewDTO dto, + CultivatePage page) { + PageListVO result = positionForAppService.queryIdentityTrainDataDetail(dto, page); + return ActionResult.success(result); + } + + /** + * 查询未提交入职登记表的用户 + * + * @param tenantId + * @return + */ + @NoDataSourceBind + @GetMapping("/queryNoSubmitForm") + public ActionResult> queryNoSubmitForm(@RequestParam("tenantId") String tenantId) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + //检测健康证到期,发送短信 + List list = staffRosterService.queryNoSubmitForm(); + return ActionResult.success("成功", list); + } + + /** + * 查询健康证到期的用户 + * + * @param tenantId + * @return + */ + @NoDataSourceBind + @GetMapping("/queryHealthExpire") + public ActionResult> queryHealthExpire(@RequestParam("tenantId") String tenantId, @RequestParam(value = "days", required = false) Long days, @RequestParam(value = "months", required = false) Long months) { + + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + //检测健康证到期,发送短信 + List list = staffRosterService.queryHealthExpire(days, months); + return ActionResult.success("成功", list); + } + + /** + * 查询公司所有用户(分页) + * + * @param queryDto 查询条件 + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/userInfo/page") + public ActionResult> getPartUserInfoPage(UserQueryDto queryDto) { + return staffRosterService.getPartUserInfoPage(queryDto); + } + + /** + * 发送登记表填写邀请系统消息 + * + * @param userId 目标用户 + * @return 执行位置 + */ + @GetMapping("/invitation/send/{userId}") + public ActionResult sendInvitationInformation(@PathVariable("userId") String userId) { + + return ActionResult.success(staffRosterService.sendInvitationInformation(userId)); + } + + @Operation(summary = "[匹配]-指定user们哪些存在,哪些不存在, 包含花名册离职的用户") + @PostMapping("/user/list/match/ids") + public ActionResult getUserListByMatch(@RequestBody List userIds){ + + return ActionResult.success((staffRosterService.getUserListByMatch(userIds))); + } + /** + * 查询离职人员信息 + * + * @return + */ + @PostMapping("/queryDepartUser") + public ActionResult> queryDepartUser(@RequestBody List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return ActionResult.success("成功", new ArrayList<>()); + } + List list = staffRosterService.queryDepartUser(userIds); + return ActionResult.success("成功", list); + } + + /** + * 根据用户ID查询所属门店负责人信息 + * + * @param tenantId + * @param userIds 用户ID + * @return + */ + @NoDataSourceBind + @PostMapping("/queryShopManagerUser/{tenantId}") + public ActionResult queryShopManagerUser(@PathVariable("tenantId") String tenantId, @RequestBody List userIds) { + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + + + ShopManagerUserDto dto = staffRosterService.queryShopManagerUser(userIds); + log.error("userIds={},dto={}", JSONUtil.toJsonStr(userIds),JSONUtil.toJsonStr(dto)); + return ActionResult.success("成功", dto); + } + + + + /** + * 修改用户离职证明是否签署 + * @param userId 用户ID + * @param status 签署状态 0-未签署 1-已签署 + * @return + */ + @PutMapping("/updateSignSeparation/{userId}/{status}") + public ActionResult updateSignSeparation(@PathVariable("userId") String userId,@PathVariable("status") Integer status) { + staffRosterService.updateSignSeparation(userId, status); + return ActionResult.success(); + } + + /** + * 查询用户离职证明是否签署 + * @param userId 用户ID + * @return + */ + @GetMapping("/querySignSeparation/{userId}") + public ActionResult querySignSeparation(@PathVariable("userId") String userId) { + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByUserId(userId); + if(null==roster){ + throw new RuntimeException("用户信息不存在"); + } + return ActionResult.success(roster.getIsSignSeparation()); + } + + /** + * 根据用户ID查询用户信息 + * @param req + * @return + */ + @PostMapping("/queryWithUserIds") + public List queryWithUserIds( @RequestBody StaffRosterListReq req){ + return staffRosterService.queryWithUserIds(req); + } + + @PostMapping("/queryWithUserIds/post") + public List queryWithUserIdsPost( @RequestBody StaffRosterReq req) { + return staffRosterService.queryWithUserIdsPost(req); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterImportController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterImportController.java new file mode 100644 index 0000000..36f53a4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsStaffRosterImportController.java @@ -0,0 +1,130 @@ +package jnpf.personnels.controller.web.roster; + + +import cn.hutool.core.date.DateUtil; +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.roster.FtbRosterImportDTO; +import jnpf.model.personnels.vo.roster.FtbRosterImportFormFieldsConfigVO; +import jnpf.model.personnels.vo.roster.FtbRosterImportVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterImportService; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * web员工花名册导入模块 + * + * @author fantaibao + * @date 2024/02/02 + */ +@RestController +@RequestMapping("/web/personnels-staff-roster-import") +@Slf4j +public class FtbPersonnelsStaffRosterImportController { + + @Resource + private FtbPersonnelsStaffRosterImportService ftbPersonnelsStaffRosterImportService; + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 花名册导入 + * + * @param ftbRosterImportDTO 文件 + * @return {@link ActionResult} + */ + @PostMapping("/roster-import") + public ActionResult rosterImport(@RequestBody @Validated FtbRosterImportDTO ftbRosterImportDTO) throws IOException { + try (InputStream inputStream = EasyExcelUtils.checkExcelFile(ftbRosterImportDTO.getFileUrl())) { + FtbRosterImportVO ftbRosterImportVO = ftbPersonnelsStaffRosterImportService.rosterImportNew(inputStream); + // 设置文件地址临时保存20天,key为roster_import_file_url_yyyy-MM-dd-HH:mm:ss,值为文件地址,方便溯源 + redisTemplate.opsForValue().set("roster_import_file_url_" + DateUtil.format(new Date(), "yyyy-MM-dd-HH:mm:ss"), + ftbRosterImportDTO.getFileUrl(), 20, TimeUnit.DAYS); + return ActionResult.success(ftbRosterImportVO); + } + } + + /** + * 花名册导入(本地调试) + */ + @PostMapping("/roster-import/local") + public ActionResult rosterImport(@RequestPart("file") MultipartFile multipartFile) throws IOException { + try (InputStream inputStream = multipartFile.getInputStream()) { + FtbRosterImportVO ftbRosterImportVO = ftbPersonnelsStaffRosterImportService.rosterImportNew(inputStream); + return ActionResult.success(ftbRosterImportVO); + } + } + + /** + * 异常数据导出 + * + * @param id 导入时返回的唯一标识(必传),用于导出异常数据 + */ + @GetMapping("/exception-data-export") + public void exceptionDataExport(String id) throws IOException { + ftbPersonnelsStaffRosterImportService.exceptionDataExport(id); + } + + /** + * 正常数据导入 + * + * @param id 导入时返回的唯一标识(必传),用于导出异常数据 + * @return {@link ActionResult} + */ + @GetMapping("/normal-data-import") + public ActionResult normalDataImport(String id) { + + String tenantId = UserProvider.getUser().getTenantId(); + String lockKey = "batch_import_" + tenantId + "#" + id; + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) { + try { + ftbPersonnelsStaffRosterImportService.normalDataImport(id); + } catch (Exception e) { + throw e; + } finally { + redisTemplate.delete(lockKey); + } + return ActionResult.success(); + } else { + throw new RuntimeException("导入中,请稍候..."); + } + } + + /** + * 新版花名册导入模板下载 + * + * @param formFields 已勾选的登记表选项主键id集合 + * @throws IOException io异常 + */ + @PostMapping("/roster-import-template-download-new") + public void rosterImportTemplateDownloadNew(@RequestBody List formFields) throws IOException { + ftbPersonnelsStaffRosterImportService.rosterImportTemplateDownloadNew(formFields); + } + + /** + * 模版下载时登记表配置勾选项 + * + * @param type 类型0表示导出,1表示导入 + * @return {@link ActionResult }<{@link List }<{@link FtbRosterImportFormFieldsConfigVO }>> + */ + @GetMapping("/form-fields-config") + public ActionResult> formFieldsConfig(Integer type) { + List result = ftbPersonnelsStaffRosterImportService.formFieldsConfig(type); + return ActionResult.success(result); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsTrialController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsTrialController.java new file mode 100644 index 0000000..f15adfb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbPersonnelsTrialController.java @@ -0,0 +1,51 @@ +package jnpf.personnels.controller.web.roster; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.roster.FtbPersonnelsTrialDTO; +import jnpf.model.personnels.dto.roster.FtbjobTrialRejectedDTO; +import jnpf.personnels.service.FtbPersonnelsTrialService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 试岗模块 + * + * @author wangchunxiang + * @date 2024/07/01 + */ +@RestController +@RequestMapping("/web/trial") +public class FtbPersonnelsTrialController { + + @Resource + private FtbPersonnelsTrialService ftbPersonnelsTrialService; + + /** + * 试岗通过 + * + * @return {@link ActionResult } + */ + @PostMapping("/passed-trial-job") + public ActionResult passedTheTrialJob(@Validated @RequestBody FtbPersonnelsTrialDTO ftbPersonnelsTrialDTO) { + ftbPersonnelsTrialService.passedTheTrialJob(ftbPersonnelsTrialDTO); + return ActionResult.success(); + } + + /** + * 试岗驳回 + * + * @return {@link ActionResult } + */ + @PostMapping("/job-trial-rejected") + public ActionResult jobTrialRejected(@Validated @RequestBody FtbjobTrialRejectedDTO ftbjobTrialRejectedDTO) { + ftbPersonnelsTrialService.jobTrialRejected(ftbjobTrialRejectedDTO); + return ActionResult.success(); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbThousandFacePersonController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbThousandFacePersonController.java new file mode 100644 index 0000000..a933919 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/roster/FtbThousandFacePersonController.java @@ -0,0 +1,36 @@ +package jnpf.personnels.controller.web.roster; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.vo.roster.FtbThousandFacePersonVO; +import jnpf.personnels.service.FtbThousandFacePersonService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +/** + * 千人千面人事管理模块 + * + * @author wangchunxiang + * @date 2025/06/13 + */ +@RestController +@RequestMapping("/web/thousand-face-person") +public class FtbThousandFacePersonController { + + @Resource + private FtbThousandFacePersonService ftbThousandFacePersonService; + + + /** + * 获取千人千面人事数据 + */ + @GetMapping("/personnel-data") + public ActionResult getPersonnelData() { + FtbThousandFacePersonVO ftbThousandFacePersonVO = ftbThousandFacePersonService.getPersonnelData(); + return ActionResult.success(ftbThousandFacePersonVO); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnelsRuleConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnelsRuleConfigController.java new file mode 100644 index 0000000..6277a9b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnelsRuleConfigController.java @@ -0,0 +1,90 @@ +package jnpf.personnels.controller.web.rule; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.model.personnels.po.FtbPersonnelsRuleConfig; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentConfig; +import jnpf.personnels.mapper.FtbPersonnelsSecondmentConfigMapper; +import jnpf.personnels.service.FtbPersonnelsRuleConfigService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * web规则配置 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels_rule_config") +public class FtbPersonnelsRuleConfigController { + /** + * 服务对象 + */ + @Resource + private FtbPersonnelsRuleConfigService ftbPersonnelsRuleConfigServiceImpl; + + + @Resource + private FtbPersonnelsSecondmentConfigMapper ftbPersonnelsSecondmentConfigMapper; + + /** + * 通过主键查询单条数据 + * + * @return 单条数据 + */ + @GetMapping("/selectOne") + public ActionResult selectOne() { + return ActionResult.success(ftbPersonnelsRuleConfigServiceImpl.query().one()) ; + } + + /** + * 编辑 + */ + @PutMapping("/edit") + public ActionResult edit(@RequestBody FtbPersonnelsRuleConfig ftbPersonnelsRuleConfig) { + ftbPersonnelsRuleConfigServiceImpl.updateById(ftbPersonnelsRuleConfig); + return ActionResult.success(); + } + + /** + * 规则查询借调薪资设置 + */ + @GetMapping("/queryInfo") + public ActionResult> queryInfo() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + List ftbPersonnelsSecondmentConfigs = ftbPersonnelsSecondmentConfigMapper.selectList(queryWrapper); + return ActionResult.success(ftbPersonnelsSecondmentConfigs); + } + /** + * 编辑借调规则配置开启 + */ + @PutMapping("/editForOA") + public ActionResult editForOA(@RequestParam String id) { + // 将所有设置为关闭 + ftbPersonnelsSecondmentConfigMapper.update(new FtbPersonnelsSecondmentConfig(), + new LambdaUpdateWrapper().set(FtbPersonnelsSecondmentConfig::getIsOpen,0)); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsSecondmentConfig::getIsOpen,1); + wrapper.eq(FtbPersonnelsSecondmentConfig::getId,id); + ftbPersonnelsSecondmentConfigMapper.update(new FtbPersonnelsSecondmentConfig(),wrapper); + return ActionResult.success(); + } + /** + * 借调人员办理查询 + */ + @GetMapping("/queryInfoForOA") + public ActionResult> queryInfoForOA() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + List secondmentConfigs = ftbPersonnelsSecondmentConfigMapper.selectList(queryWrapper); + if (secondmentConfigs.stream().anyMatch(v -> v.getIsOpen() == 1 && v.getId().equals("3"))){ + return ActionResult.success(secondmentConfigs.stream().filter(v->!v.getId().equals("3")).collect(Collectors.toList())); + } + return ActionResult.success(secondmentConfigs.stream().filter(v->v.getIsOpen()==1).collect(Collectors.toList())); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnlesInfoConfigController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnlesInfoConfigController.java new file mode 100644 index 0000000..f23e98b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/rule/FtbPersonnlesInfoConfigController.java @@ -0,0 +1,74 @@ +package jnpf.personnels.controller.web.rule; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.range.FtbRangeConfigDTO; +import jnpf.model.personnels.dto.range.FtbRangeDiyQueryDTO; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; +import jnpf.personnels.FtbPersonnelsInfoConfigApi; +import jnpf.personnels.service.FtbPersonnlesInfoConfigService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * web人事范围区间配置 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/range-config") +public class FtbPersonnlesInfoConfigController implements FtbPersonnelsInfoConfigApi { + + @Resource + private FtbPersonnlesInfoConfigService personnlesInfoConfigService; + + /** + * 查询是平均范围配置 还是自定义配置 + * @param type 1 年龄 2 薪资 3 工龄 + * @return 配置类型 1平均 2自定义 + */ + @GetMapping("/query-type") + @Override + public ActionResult queryType(@RequestParam("type") Integer type) { + return ActionResult.success(personnlesInfoConfigService.queryType(type)); + } + + + /** + * 查询平均区间配置范围 + * @param type 1 年龄 2 薪资 3 工龄 + */ + @GetMapping("/query-info") + @Override + public ActionResult queryInfo(@RequestParam("type") Integer type) { + return ActionResult.success(personnlesInfoConfigService.queryInfo(type)); + } + /** + * 查询自定义区间配置范围 + * + * @param type 1 年龄 2 薪资 3 工龄 + */ + @GetMapping("/query-info-diy") + @Override + public ActionResult> queryDiyInfo(@RequestParam("type")Integer type) { + return ActionResult.success(personnlesInfoConfigService.queryDiyInfo(type)); + } + /** + * 平均编辑区间配置 + */ + @PutMapping("/update-info") + public ActionResult updateInfo(@RequestBody FtbRangeConfigDTO dto) { + personnlesInfoConfigService.updateInfo(dto); + return ActionResult.success(); + } + /** + * 按自定义范围配置 + */ + @PutMapping("/update-diy-info") + public ActionResult updateDiyInfo(@RequestBody FtbRangeDiyQueryDTO dto) { + personnlesInfoConfigService.updateDiyInfo(dto); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsSalaryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsSalaryController.java new file mode 100644 index 0000000..e341cd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsSalaryController.java @@ -0,0 +1,266 @@ +package jnpf.personnels.controller.web.salary; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import jnpf.base.ActionResult; +import jnpf.model.common.PayRollJsonItem; +import jnpf.model.hr.PersonSalaryHistoryVo; +import jnpf.model.personnels.dto.salary.FtbPersonnelRosterSalaryHistoryDTO; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryTemporaryStorageCreatDto; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.vo.salary.FtbPersonnelRosterSalaryVO; +import jnpf.model.personnels.vo.salary.FtbPersonnelsSalaryTemporaryStorageVo; +import jnpf.model.personnels.vo.salary.FtbPersonnelsSalaryVO; +import jnpf.model.vo.UserSalaryHistoryVo; +import jnpf.personnels.service.FtbPersonnelsSalaryService; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import jnpf.salary.QuerySalaryApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Title: web/app通用薪酬开放接口 + * @Author: peng.hao + * @create: 2024/5/11:15:35 + */ +@Slf4j +@RestController +@RequestMapping("/personnels-salary") +public class FtbPersonnelsSalaryController { + + @Autowired + FtbPersonnelsSalaryService salaryService; + + @Autowired + QuerySalaryApi querySalaryApi; + + @Resource + FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentManagementService; + + + /** + * 获取当前人员薪酬 + * 1.入职 使用薪酬配置 根据当前人加入职岗位职等获取薪酬 + * 2.转正 否 (根据userId+转正岗位职等获取薪酬) 是(同理) + * 3.调岗 否 (根据userId+选择调整的组织岗位职等) 是(根据userId+调岗后的岗位Id+职等id) + * 5.晋升 否 (根据userId+所属岗位/职等) 是(根据userId+晋升后岗位Id+职等Id) + * @param postId 岗位id + * @param rankId 职等id + * @param userId 用户id + */ + @GetMapping("/getPaidForYourPeople") + public ActionResult> getTheCurrentPersonSSalary(@RequestParam(value = "userId",required = false) String userId, + @RequestParam(value = "rankId") String rankId, + @RequestParam(value = "postId") String postId) { + return ActionResult.success(salaryService.getTheCurrentPersonSSalary(userId, rankId, postId)); + } + /** + * 保存薪酬信息 + */ + @PutMapping("/saveTheChangePayInformation") + public ActionResult saveTheChangePayInformation(@RequestBody FtbPersonnelsSalaryVO vo) { + salaryService.saveTheChangePayInformation(vo.getSalaryList(),vo.getUserId(),vo.getDate(), + vo.getUserInfoWithSalary(),vo.getUpdateType(),vo.getRemark(),vo.getOperationType(),0,vo.getSalaryType(), ""); + return ActionResult.success(); + } + + + public static FtbPersonnelsSalaryInfo covert(PayRollJsonItem payRollJsonItem ) { + if (payRollJsonItem == null) { + return null; + } + FtbPersonnelsSalaryInfo ftbPersonnelsSalaryInfo = new FtbPersonnelsSalaryInfo(); + ftbPersonnelsSalaryInfo.setId(payRollJsonItem.getId()); + ftbPersonnelsSalaryInfo.setFIsForce(payRollJsonItem.getfIsForce()); + ftbPersonnelsSalaryInfo.setFName(payRollJsonItem.getfName()); + ftbPersonnelsSalaryInfo.setFRemark(payRollJsonItem.getfRemark()); + ftbPersonnelsSalaryInfo.setDecimalPlaces(payRollJsonItem.getfReservedBits()); + ftbPersonnelsSalaryInfo.setFSetType(payRollJsonItem.getfSetType()); + ftbPersonnelsSalaryInfo.setFCarryRule(payRollJsonItem.getfCarryRule()); + ftbPersonnelsSalaryInfo.setFItem(payRollJsonItem.getfItem()); + ftbPersonnelsSalaryInfo.setFSetValue(payRollJsonItem.getfSetValue()); + ftbPersonnelsSalaryInfo.setFTaxType(payRollJsonItem.getfTaxType()); + ftbPersonnelsSalaryInfo.setFValueType(payRollJsonItem.getfValueType()); + ftbPersonnelsSalaryInfo.setSort(payRollJsonItem.getfSort()); + ftbPersonnelsSalaryInfo.setFDesc(payRollJsonItem.getfDesc()); + ftbPersonnelsSalaryInfo.setSequenceType(payRollJsonItem.getSequenceType()); + ftbPersonnelsSalaryInfo.setFSort(payRollJsonItem.getfSort()); + ftbPersonnelsSalaryInfo.setFTypeSort(payRollJsonItem.getfTypeSort()); + ftbPersonnelsSalaryInfo.setGroupId(payRollJsonItem.getGroupId()); + ftbPersonnelsSalaryInfo.setGroupName(payRollJsonItem.getGroupName()); + ftbPersonnelsSalaryInfo.setGroupSort(String.valueOf(payRollJsonItem.getGroupSort())); + + return ftbPersonnelsSalaryInfo; + } + + /** + * 根据用户id,查询历史薪资变化记录 + * + * @param dto 系统用户id 等查询参数 + * @return 包含花名册部分信息的历史记录信息 + */ + @GetMapping("/history/list") + public ActionResult> getSalaryHistoryByUserId(@SpringQueryMap FtbPersonnelRosterSalaryHistoryDTO dto) { + + return ActionResult.success(salaryService.getSalaryHistoryByUserId(dto)); + } + + /** + * 根据原id和type,查询历史薪资变化记录详情 + * + * @param id 历史记录的id + * @param type 历史记录原样类型 + * @return 历史记录原样 + */ + @GetMapping("/history/detail") + public ActionResult getSalaryHistoryDetailByUserId(@RequestParam(value = "id") String id, @RequestParam(value = "type") String type) { + return querySalaryApi.getSalaryHistoryDetailByUserId(id, type); + } + + /** + * 薪资临时存储接口 + */ + @PostMapping("/creatSalaryTemporaryStorage") + public ActionResult creatSalaryTemporaryStorage(@RequestBody FtbPersonnelsSalaryTemporaryStorageCreatDto creatDto){ + salaryService.creatSalaryTemporaryStorage(creatDto); + return ActionResult.success(); + } + /** + * 薪资临时修改接口 + */ + @PutMapping("/updateSalaryTemporaryStorage") + public ActionResult updateSalaryTemporaryStorage(@RequestBody FtbPersonnelsSalaryTemporaryStorageCreatDto creatDto){ + salaryService.updateSalaryTemporaryStorage(creatDto); + return ActionResult.success(); + } + /** + * 薪资临时查询接口 + * @param uerId 用户id + * @param configType 1 入职审批配置 2 转正审批配置 3 调岗审批配置 4 离职审批配置 5 晋升审批配置 + */ + @GetMapping("/querySalaryTemporaryStorage") + public ActionResult querySalaryTemporaryStorage(@RequestParam String uerId, + @RequestParam String configType){ + FtbPersonnelsSalaryTemporaryStorageVo vo = salaryService.querySalaryTemporaryStorage(uerId,configType); + + return ActionResult.success(vo); + } + /** + * 根据人和薪资生效时间查询是否有之前的薪酬数据 + * @return { + * [ + * type : 生效的模块 (入职,转正,调岗,晋升) + * time : 生效时间 (具体薪资生效时间) + * ] + * } + */ + @GetMapping("/query-salary-by-userId-time") + public ActionResult>> querySalaryByUserIdAndTime(@RequestParam("userId") String user, + @RequestParam ("date") Date date ) throws Exception { + + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String format = simpleDateFormat.format(date); + ActionResult> historyByChangeDate = querySalaryApi.getHistoryByChangeDate(format, user); + log.error("获取数据000----------->"+ historyByChangeDate); + if (historyByChangeDate == null ){ + return ActionResult.fail("获取薪酬数据失败,请稍后再试!"); + } + List data = historyByChangeDate.getData(); + List> maps = new ArrayList<>(); + if (data.stream().allMatch(item -> item.getFChangeDate() != null)) { + for (UserSalaryHistoryVo userSalaryHistoryVo : data) { + Map map = new HashMap<>(); + // 获取备注 1:转正 0:其他 2: 调岗 3 晋升 4 入职 + String fUserState = userSalaryHistoryVo.getFUserState(); + if ("1".equals(fUserState)){ + fUserState = "转正"; + }else if ("2".equals(fUserState) || "3".equals(fUserState)){ + fUserState = "调动"; + }else if ("4".equals(fUserState)){ + fUserState = "入职"; + }else if ("5".equals(fUserState)){ + fUserState = "薪酬审批调薪"; + }else if ("6".equals(fUserState)){ + fUserState = "调动"; + }else if ("7".equals(fUserState)){ + fUserState = "薪酬直接调薪"; + }else if ("8".equals(fUserState)){ + fUserState = "薪酬编辑"; + } else if ("9".equals(fUserState)){ + fUserState = "借调"; + }else if ("0".equals(fUserState)) { + fUserState = "导入"; + }else if ("10".equals(fUserState)) { + fUserState = "借调返岗"; + } + map.put("type", fUserState); + map.put("time", userSalaryHistoryVo.getFChangeDate()); + maps.add(map); + } + } + return ActionResult.success(maps); + } + + /** + * 是否作废薪资 + * @param id 主键id + * @param flag 0 不作废 1 作废 + * @param type 历史记录原样类型 + * @return + */ + @GetMapping("/whether-to-voidWages") + public ActionResult whetherToVoidWages(@RequestParam("id") String id, + @RequestParam ("flag") String flag, + @RequestParam ("type") String type, + @RequestParam ("userId") String userId, + @RequestParam("changeDate") Date changeDate){ + ActionResult byUserId = querySalaryApi.getSalaryHistoryDetailByUserId(id, type); + if (byUserId == null){ + return ActionResult.fail("历史薪资记录获取失败"); + } + PersonSalaryHistoryVo data = byUserId.getData(); + if (ObjectUtil.isEmpty(data)){ + return ActionResult.success("历史薪资记录无数据变更"); + } + List beforeItem = data.getBeforeItem(); + salaryService.whetherToVoidWages(userId,beforeItem,flag,changeDate); + return ActionResult.success(); + } + /** + * 查询借调薪酬是否有交集 + */ + @GetMapping("/query-salary-intersection") + public ActionResult>> querySalaryIntersection(@RequestParam("userId") String userId, + @RequestParam("date") Date date){ + List serviceIdList = ftbPersonnelsSecondmentManagementService.salarySecondmentCrossover(userId, + date); + if(CollUtil.isNotEmpty(serviceIdList)){ + List> hashMaps = serviceIdList.stream().map(v -> { + Map map = new HashMap<>(); + map.put("startTime", v.getSecondmentStartTime()); + map.put("endTime", v.getSecondmentEndTime()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(hashMaps); + } + return ActionResult.success(); + } + /** + * 查询借调薪酬与其他办理业务是否有交集 + */ + @GetMapping("/query-salary-intersection-with-other-business") + public ActionResult>> querySalaryIntersectionWithOtherBusiness(@RequestParam("userId") String userId, + @RequestParam("startData") Date startData, + @RequestParam("endData") Date endData) { + List> result = salaryService.querySalaryIntersectionWithOtherBusiness(userId, startData, endData); + return ActionResult.success(result); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsStaffSalaryChangeLogController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsStaffSalaryChangeLogController.java new file mode 100644 index 0000000..7308fc2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/salary/FtbPersonnelsStaffSalaryChangeLogController.java @@ -0,0 +1,39 @@ +package jnpf.personnels.controller.web.salary; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.staff.salarylog.FtbPersonnelsStaffSalaryChangeLogDto; +import jnpf.personnels.service.FtbPersonnelsStaffSalaryChangeLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * web员工薪酬变化表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-staff-salary-change-log") +public class FtbPersonnelsStaffSalaryChangeLogController { + + + @Autowired + private FtbPersonnelsStaffSalaryChangeLogService staffSalaryChangeLogService; + + /** + * 查询员工薪酬变化列表 + * + * @param userId + * @return + */ + @GetMapping("/query-list/{userId}") + @Deprecated(since = "1.0.0") + public ActionResult> queryList(@PathVariable("userId") String userId) { + List list = staffSalaryChangeLogService.queryAll(userId); + return ActionResult.success(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/scheme/FtbPersonnelsStaffRosterSchemeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/scheme/FtbPersonnelsStaffRosterSchemeController.java new file mode 100644 index 0000000..894f6d5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/scheme/FtbPersonnelsStaffRosterSchemeController.java @@ -0,0 +1,72 @@ +package jnpf.personnels.controller.web.scheme; + +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.scheme.FtbPersonAddSchemeDTO; +import jnpf.model.personnels.dto.scheme.FtbPersonEditSchemeDTO; +import jnpf.model.personnels.vo.scheme.FtbPersonSchemeDetailsVO; +import jnpf.model.personnels.vo.scheme.FtbPersonSchemeListVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterSchemeService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * web端花名册导出方案模块 + * @author wcx + * @date 2026/03/09 + */ +@RestController +@RequestMapping("/web/personnels-staff-scheme") +public class FtbPersonnelsStaffRosterSchemeController { + + @Resource + private FtbPersonnelsStaffRosterSchemeService ftbPersonnelsStaffRosterSchemeService; + + /** + * 新增方案 + */ + @PostMapping("/add-scheme") + public ActionResult addScheme(@RequestBody @Valid FtbPersonAddSchemeDTO schemeDTO) { + ftbPersonnelsStaffRosterSchemeService.addScheme(schemeDTO); + return ActionResult.success(); + } + + /** + * 删除方案 + */ + @DeleteMapping("/delete-scheme/{id}") + public ActionResult deleteScheme(@PathVariable("id") String id) { + ftbPersonnelsStaffRosterSchemeService.deleteScheme(id); + return ActionResult.success(); + } + + /** + * 修改方案 + */ + @PutMapping("/update-scheme") + public ActionResult updateScheme(@RequestBody @Valid FtbPersonEditSchemeDTO schemeDTO) { + ftbPersonnelsStaffRosterSchemeService.updateScheme(schemeDTO); + return ActionResult.success(); + } + + /** + * 方案详情 + */ + @GetMapping("/detail-scheme") + public ActionResult detailScheme(@RequestParam("id") String id) { + FtbPersonSchemeDetailsVO result = ftbPersonnelsStaffRosterSchemeService.detailScheme(id); + return ActionResult.success(result); + } + + /** + * 获取方案列表 + */ + @GetMapping("/get-scheme-list") + public ActionResult> getSchemeList() { + List result = ftbPersonnelsStaffRosterSchemeService.getSchemeList(); + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/secondment/FtbPersonnelsSecondmentController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/secondment/FtbPersonnelsSecondmentController.java new file mode 100644 index 0000000..897c72e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/secondment/FtbPersonnelsSecondmentController.java @@ -0,0 +1,210 @@ +package jnpf.personnels.controller.web.secondment; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.attendance.vo.DailyApprovalVo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.secondment.FtbHandleSecondmentDTO; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentInfoVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; +import jnpf.personnels.msg.PersonnelsConsumerSourceMsg; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * web借调管理 + * @Author: peng.hao + * @create: 2025/9/11 + */ +@RestController +@RequestMapping("/web/secondment") +public class FtbPersonnelsSecondmentController { + + @Resource + private FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentService; + + @Resource + PersonnelsConsumerSourceMsg secondmentConsumerSourceMsg; + + + /** + * 列表查询 + */ + @PostMapping("/list-query") + public ActionResult> pageList(@RequestBody PersonnelsQueryDTO dto) { + CultivatePage page = new CultivatePage(); + page.setCurrentPage(dto.getCurrentPage()); + page.setPageSize(dto.getPageSize()); + PageListVO pageListVO = ftbPersonnelsSecondmentService.pageList(dto, page); + return ActionResult.success(pageListVO); + } + /** + * 导出 + */ + @PostMapping("/list-query-export") + public void pageListExport(@RequestBody PersonnelsQueryDTO dto, HttpServletResponse response) throws IOException { + CultivatePage page = new CultivatePage(); + page.setPageSize(-1); + List secondmentVOS = ftbPersonnelsSecondmentService.pageList(dto, page).getList(); + secondmentVOS.forEach(v->{ + String message = FtbPersonnelsAuditTaskEnum.getMessage(v.getTransferStatus()); + v.setTransferStatusName(message); + }); + EasyExcelUtils.exportExcel(response, "借调列表", secondmentVOS, FtbPersonnelsSecondmentVO.class); + } + + /** + * 详情 + */ + @GetMapping("/details/{id}") + public ActionResult details(@PathVariable String id) { + return ActionResult.success(ftbPersonnelsSecondmentService.details(id)); + } + + /** + * 办理借调 + */ + @PostMapping("/handle-secondment") + public ActionResult handleSecondment(@RequestBody @Validated FtbHandleSecondmentDTO ftbHandleSecondmentDTO) { + return ActionResult.success("",ftbPersonnelsSecondmentService.handleSecondment(ftbHandleSecondmentDTO)); + } + /** + * 借调删除 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable String id) { + ftbPersonnelsSecondmentService.delete(id); + return ActionResult.success(); + } + + /** + * 办理借调-oa + */ + @PostMapping("/handle-secondment-oa") + public ActionResult handleSecondmentForOA(@RequestBody @Validated FtbHandleSecondmentDTO ftbHandleSecondmentDTO) { + ftbPersonnelsSecondmentService.handleSecondmentForOA(ftbHandleSecondmentDTO); + return ActionResult.success(); + } + /** + * 借调审批 + */ + @PostMapping("/secondment-approval") + public ActionResult secondmentApproval(@RequestBody FtbPersonnelsAuditDto ftbPersonnelsAuditDto) { + ftbPersonnelsSecondmentService.secondmentApproval(ftbPersonnelsAuditDto); + return ActionResult.success(); + } + /** + * 借调撤回 + */ + @PostMapping("/revoke") + public ActionResult secondmentWithdrawal(@RequestBody FtbPersonnelsAuditDto ftbPersonnelsAuditDto) { + ftbPersonnelsSecondmentService.secondmentWithdrawal(ftbPersonnelsAuditDto); + return ActionResult.success(); + } + /** + * 提前结束 + * { + * "id": "借调id", + * "endTime" : "最新借调结束时间" + * } + */ + @PutMapping("/early-end") + public ActionResult earlyEnd(@RequestBody Map map) { + ftbPersonnelsSecondmentService.earlyEnd(map); + return ActionResult.success(); + } + /** + * 延长借调时间 + *{ + * "id": "借调id", + * "endTime" : "最新借调结束时间" + * } + */ + @PutMapping("/delay-time") + public ActionResult delayTime(@RequestBody Map map) { + ftbPersonnelsSecondmentService.delayTime(map); + return ActionResult.success(); + } + + /** + * test 消息发送 + */ + @GetMapping("/send-message") + @NoDataSourceBind + public ActionResult sendMessage() { + secondmentConsumerSourceMsg.sendMessage("test msg"); + return ActionResult.success(); + } + /** + * 选人后校验员工是否处于借调中 + */ + @GetMapping("/check-is-in-the-process") + public ActionResult checkIsInTheProcess(String userId, Date startTime, Date endTime) { + return ActionResult.success(ftbPersonnelsSecondmentService.checkIsInTheProcess(userId,startTime,endTime)); + } + /** + * 校验是否有借调记录 + * + */ + @GetMapping("/check-whether-there-is-a-secondment-record") + public ActionResult checkWhetherThereIsASecondmentRecord(String userId, Date leaveTime) { + return ActionResult.success( ftbPersonnelsSecondmentService.checkWhetherThereIsASecondmentRecord(userId, leaveTime)); + } + /** + * 根据userI 和开始时间结束时间查询借调记录 + */ + @GetMapping("/get-secondment-record") + public List getSecondmentRecord(@RequestParam("userId") String userId, + @RequestParam(value = "startLeaveTime",required = false)String startLeaveTime, + @RequestParam(value = "endLeaveTime",required = false)String endLeaveTime) { + return ftbPersonnelsSecondmentService.getSecondmentRecord(userId, startLeaveTime,endLeaveTime); + } + /** + * 批量查询审批中和审批完成的借调记录 + */ + @PostMapping("/get-secondment-record-bath") + public List getSecondmentRecordBath(@RequestBody FtbSecondMentQueryDTO dto) { + return ftbPersonnelsSecondmentService.getSecondmentRecordBath(dto); + } + /** + * 借调返岗 + */ + @GetMapping("/secondedBackToWork") + @NoDataSourceBind + public void secondedBackToWork(String tenantId) throws LoginException { + TenantDataSourceUtil.switchTenant(tenantId); + // 定时任务检测当借调结束后薪资生成 借调返岗 + ftbPersonnelsSecondmentService.checkSecondmentEnd(tenantId); + } + /** + * 查询审批中和审批完成的借调记录 + */ + @PostMapping("/list-query-approval") + public List queryListApproval(@RequestBody FtbSecondMentQueryDTO dto) { + return ftbPersonnelsSecondmentService.queryListApproval(dto); + } + /** + * 借调作废 + */ + @PutMapping("/cancel/{id}") + public ActionResult cancel(@PathVariable String id) { + ftbPersonnelsSecondmentService.cancel(id); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/shortchain/FtbPersonnelsShortchainController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/shortchain/FtbPersonnelsShortchainController.java new file mode 100644 index 0000000..7000481 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/shortchain/FtbPersonnelsShortchainController.java @@ -0,0 +1,110 @@ +package jnpf.personnels.controller.web.shortchain; + +import cn.hutool.core.util.StrUtil; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationResponse; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.model.personnels.dto.uchisuike.FtbGenerateShortChainDTO; +import jnpf.personnels.service.FtbPersonnelsShortchainService; +import jnpf.personnels.utils.PersonalizedTenantWhitelistUtils; +import jnpf.personnels.utils.PersonnelIdCardVerificationUtils; +import jnpf.util.NoDataSourceBind; +import jnpf.util.TenantUtil; +import jnpf.util.UserProvider; +import jnpf.util.excel.ShortChainUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * web和app人事调离身份证校验模块 + * + * @author xxxxx + */ +@Slf4j +@RestController +@RequestMapping("/personnels-shortchain") +public class FtbPersonnelsShortchainController { + + @Resource + private FtbPersonnelsShortchainService ftbPersonnelsShortchainService; + @Resource + private TenantUtil tenantUtil; + @Resource + private PersonalizedTenantWhitelistUtils personalizedTenantWhitelistUtils; + @Resource + private PersonnelIdCardVerificationUtils personnelIdCardVerificationUtils; + + /** + * 根据长链地址生成短链地址 + * + * @return {@link ActionResult} + */ + @PostMapping("/generate-short-link-address") + public ActionResult generateShortChain(@Validated @RequestBody FtbGenerateShortChainDTO ftbGenerateShortChainDTO) { + String generateShortChain = ftbPersonnelsShortchainService.generateShortChain(ftbGenerateShortChainDTO.getLongChainAddress()); + // decode解密 + ftbPersonnelsShortchainService.saveShortChain(generateShortChain, ftbGenerateShortChainDTO.getLongChainAddress(), + ShortChainUtils.reverseLongChain(ftbGenerateShortChainDTO.getRequestUrl())); + UserInfo userInfo = UserProvider.getUser(); + return ActionResult.success("成功", ftbGenerateShortChainDTO.getProxyUrl() + "/" + generateShortChain + "/" + userInfo.getTenantId()); + } + + /** + * 短链地址访问,实际跳转到长链地址 + * + * @param url 短链6位地址 + */ + @GetMapping("/redirect/{url}/{tenantId}") + @NoDataSourceBind + public void redirect(@PathVariable("url") String url, @PathVariable("tenantId") String tenantId, HttpServletResponse response) throws IOException { + if (StrUtil.isBlank(tenantId)) { + log.error("短链地址:{},缺乏租户id参数:{}", url, tenantId); + return; + } + tenantUtil.switchTenant(tenantId); + String physicalAddress = ftbPersonnelsShortchainService.redirect(url); + if (StringUtils.hasText(physicalAddress)) { + log.error("正常日志请忽略,短链重定向地址到:{}", physicalAddress); + response.sendRedirect(physicalAddress); + } else { + log.error("无效的短链地址:{}", url); + } + } + + /** + * 个性化租户白名单(返回租户标识) + * + * @return {@link ActionResult} + */ + @GetMapping("/redirect/personalized-tenant-whitelist") + @NoDataSourceBind + public ActionResult> internalRecommendationInvitationAdded() { + return ActionResult.success(personalizedTenantWhitelistUtils.getTenantry()); + } + + /** + * 身份证校验 + * + * @param userName 姓名 + * @param tenantId 租户Id + * @param idCard 身份证号码 + */ + @GetMapping("/redirect/id-card-verification") + @NoDataSourceBind + public ActionResult idCardVerification(@RequestParam("userName") String userName, @RequestParam("tenantId") String tenantId, @RequestParam("idCard") String idCard) throws IOException { + if (StrUtil.isBlank(tenantId)) { + log.error("身份证校验:{},缺乏租户id参数:{}", idCard, tenantId); + return ActionResult.fail("缺少租户id参数"); + } + IdCardVerificationResponse idCardVerificationResponse = personnelIdCardVerificationUtils.idCardVerification(idCard, userName, tenantId); + return ActionResult.success(idCardVerificationResponse); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsStaffTransferPositionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsStaffTransferPositionController.java new file mode 100644 index 0000000..db8d818 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsStaffTransferPositionController.java @@ -0,0 +1,215 @@ +package jnpf.personnels.controller.web.transfer; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.entity.StoreEntity; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionDto; +import jnpf.model.personnels.req.employment.EmploymentApplyCheckDto; +import jnpf.model.personnels.req.transfer.MyWebTransferCheckListReq; +import jnpf.model.personnels.req.transfer.QueryTransferListReq; +import jnpf.model.personnels.req.transfer.SaveTransferReq; +import jnpf.permission.OrganizeApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.personnels.service.FtbPersonnelsStaffTransferPositionService; +import jnpf.store.service.StoreService; +import jnpf.util.FtbUtil; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web员工调岗表模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/personnels-staff-transfer-position") +public class FtbPersonnelsStaffTransferPositionController { + + @Autowired + private FtbPersonnelsStaffTransferPositionService transferPositionService; + + + @Resource + StoreService storeService; + + @Autowired + OrganizeApi organizeApi; + + /** + * 已调岗员工列表/调岗审批列表 + * + * @param req + * @return {@link ActionResult}<{@link TransferPositionDto}> + */ + @GetMapping("/query-list") + public ActionResult> pageLists(@Validated QueryTransferListReq req) { + PageInfo pageVo = transferPositionService.getPageList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + /** + * 调岗员工列表导出 + * + * @param req + * @return {@link ActionResult}<{@link TransferPositionDto}> + */ + @GetMapping("/query-list-export") + public void pageListsExport(@Validated QueryTransferListReq req, HttpServletResponse response) throws IOException { + req.setPageSize(Integer.MAX_VALUE); + PageInfo pageVo = transferPositionService.getPageList(req); + pageVo.getList().parallelStream().forEach(a -> { + // 状态 + TransferPositionDto.changeApprovalStatus(a); + // 试用期 + TransferPositionDto.probationStatus(a); + }); + String fileName = "调岗信息"; + EasyExcelUtils.exportExcel(response, fileName, pageVo.getList(), TransferPositionDto.class); + } + + /** + * 我的审批(已审批/待审批) + * + * @param req + * @return {@link ActionResult}<{@link TransferPositionDto}> + */ + @GetMapping("/query-myCheck-list") + @Deprecated(since = "人事v1.2接入oa废弃") + public ActionResult> queryMycheckList(@Validated MyWebTransferCheckListReq req) { + PageInfo pageVo = transferPositionService.pageQueryMyCheckList(req); + return ActionResult.page(pageVo.getList(), FtbUtil.getPagination(pageVo)); + } + + + /** + * 办理调岗 + * + * @param req 调岗参数 + * @return v1.1 返回 1 标识为门店负责人,需要进行替换 5 返回薪酬冲突 + */ + @PostMapping("/deal-transfer") + public ActionResult dealTransfer(@Validated @RequestBody SaveTransferReq req) { + String string = transferPositionService.insertData(req); + return ActionResult.success("成功",string); + } + + /** + * 办理调岗(for oa) 包含重新办理 + * + * @param req 调岗参数 + * @return + */ + @PostMapping("/deal-transfer-for-oa") + public ActionResult dealTransferForOA(@Validated @RequestBody SaveTransferReq req) { + ActionResult map = transferPositionService.dealTransferForOA(req); + return map; + } + + + /** + * 查看调岗详细 + * + * @param id + * @return + */ + @GetMapping("/get/{id}") + public ActionResult get(@PathVariable("id") String id) { + TransferPositionDto info = transferPositionService.getInfo(id); + return ActionResult.success(info); + } + /** + * 查看调岗详细(for oa) + * + * @param id + * @return + */ + @GetMapping("/get") + public ActionResult getWith(@RequestParam("id") String id) { + TransferPositionDto info = transferPositionService.getInfo(id); + return ActionResult.success(info); + } + + /** + * 删除调岗记录 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + transferPositionService.deleteData(id); + return ActionResult.success(); + } + + /** + * 审批 + * + * @param id 主键id,必传 + * @return + */ + @PutMapping("/approval/{id}") + public ActionResult approval(@PathVariable("id") String id, @RequestBody EmploymentApplyCheckDto dto) { + dto.setId(id); + transferPositionService.approval(dto); + return ActionResult.success(); + } + + + /** + * 审批(for oa) + * + */ + @PostMapping("/approval/id") + @GlobalTransactional + public ActionResult approvalForOA(@RequestBody EmploymentApplyCheckDto dto) { + return transferPositionService.approvalForOA(dto); + } + + + /** + * 重新办理 + * + * @param id 主键id,必传 + * @return + */ + + @PutMapping("/re-approval/{id}") + public ActionResult reApproval(@PathVariable("id") String id, @RequestBody SaveTransferReq dto) { + transferPositionService.reApproval(id,dto); + return ActionResult.success(); + } + + /** + * 获取门店负责人详情 + * @param userId 门店负责人id + * @return organizeName 组织名称 storeName 门店名称 + */ + @GetMapping("/getStoreManagerInfo") + public ActionResult>> getStoreManagerInfo(@RequestParam("userId") String userId) { + LambdaQueryWrapper objectLambdaQueryWrapper = Wrappers.lambdaQuery(); + objectLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid,userId); + List list = storeService.list(objectLambdaQueryWrapper); + List> maps = list.stream().map(item -> { + Map map = Maps.newHashMap(); + OrganizeEntity organize = organizeApi.getInfoById(item.getOrganizeid()); + map.put("organizeName", organize.getFullName()); + map.put("storeName", item.getStorename()); + return map; + }).collect(Collectors.toList()); + return ActionResult.success(maps); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsTransferManageController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsTransferManageController.java new file mode 100644 index 0000000..d4df6db --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/transfer/FtbPersonnelsTransferManageController.java @@ -0,0 +1,159 @@ +package jnpf.personnels.controller.web.transfer; + +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.FtbCheckPermission; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.req.transfer.FtbHandleTransferDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferOAAddDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferOaDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferQueryDTO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferDetailsVO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferPageExportVO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferPageVO; +import jnpf.personnels.service.FtbPersonnelsTransferManageService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * web人事调离调动管理模块 + * + * @author xxxxx + */ +@RestController +@RequestMapping("/web/ftb-personnels-transfer-manage") +public class FtbPersonnelsTransferManageController { + + @Resource + private FtbPersonnelsTransferManageService ftbPersonnelsTransferManageService; + + + /** + * 办理调动,直接办理 + */ + @PostMapping("/handle-transfer") + public ActionResult handleTransfer(@RequestBody @Validated FtbHandleTransferDTO ftbHandleTransferDTO) { + String handleTransfer = ftbPersonnelsTransferManageService.handleTransfer(ftbHandleTransferDTO); + return ActionResult.success("操作成功", handleTransfer); + } + + /** + * 办理调动,OA审批办理 + */ + @PostMapping("/handle-transfer-for-oa") + public ActionResult handleTransferOa(@RequestBody @Validated FtbHandleTransferOAAddDTO ftbHandleTransferDTO) { + FtbHandleTransferDTO ftbHandleTransferDTOR = ftbHandleTransferDTO.convert(ftbHandleTransferDTO); + return ftbPersonnelsTransferManageService.handleTransferOa(ftbHandleTransferDTOR); + } + + /** + * 删除调动记录 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + ftbPersonnelsTransferManageService.delete(id); + return ActionResult.success(); + } + + /** + * 手动办理撤销调动记录 + * + * @param id 主键id,必传 + * @return {@link ActionResult} + */ + @PutMapping("/revoke-the-transfer-record/{id}") + public ActionResult revokeTheTransferRecord(@PathVariable("id") String id) { + ftbPersonnelsTransferManageService.revokeTheTransferRecord(id); + return ActionResult.success(); + } + + /** + * 审批(for oa) + */ + @PostMapping("/approval") + public ActionResult approvalForOA(@Validated @RequestBody FtbHandleTransferOaDTO dto) { + ftbPersonnelsTransferManageService.approvalForOA(dto); + return ActionResult.success(); + } + + /** + * 调动管理列表页 + */ + @PostMapping("/transfer-manage-list") + @FtbCheckPermission("transferPositionMa") + public ActionResult> transferManageList(@RequestBody FtbHandleTransferQueryDTO ftbHandleTransferQueryDTO) { + Page page = ftbHandleTransferQueryDTO.coverCultivatePage(); + page = ftbPersonnelsTransferManageService.transferManageList(page, ftbHandleTransferQueryDTO); + PageListVO pageListVO = CultivatePage.coverPageList(page); + return ActionResult.success(pageListVO); + } + + /** + * 调动管理导出 + */ + @PostMapping(value = "/list-export") + public void listExport(@RequestBody FtbHandleTransferQueryDTO ftbHandleTransferQueryDTO) throws IOException { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletResponse httpServletResponse = requestAttributes.getResponse(); + Page page = ftbHandleTransferQueryDTO.coverCultivatePage(); + page.setSize(-1); + page = ftbPersonnelsTransferManageService.transferManageList(page, ftbHandleTransferQueryDTO); + List exportVOList = page.getRecords().stream().map(FtbHandleTransferPageExportVO::convert).collect(Collectors.toList()); + EasyExcelUtils.exportExcel(httpServletResponse, "调动管理", exportVOList, FtbHandleTransferPageExportVO.class); + } + + /** + * 调动详情 + * + * @param id 主键id,必传 + */ + @GetMapping("/transfer-details") + public ActionResult transferDetails(@RequestParam(value = "id") String id) { + FtbHandleTransferDetailsVO ftbHandleTransferDetailsVO = ftbPersonnelsTransferManageService.transferDetails(id); + return ActionResult.success(ftbHandleTransferDetailsVO); + } + + + /** + * 调动生效定时任务 + */ + @PostMapping("/transfer-effect-scheduled-task") + @NoDataSourceBind + public ActionResult transferEffectScheduledTask(@RequestParam(value = "tenantId") String tenantId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + ftbPersonnelsTransferManageService.transferEffectScheduledTask(tenantId); + return ActionResult.success(); + } + + /** + * 查询调动类型下拉框(OA专用) + */ + @GetMapping("/query-field-value") + public ActionResult>> queryFieldValue() { + List> value = ftbPersonnelsTransferManageService.queryFieldValue(); + return ActionResult.success("成功", value); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/turnover/FtbPersonnelsTurnoverManagementController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/turnover/FtbPersonnelsTurnoverManagementController.java new file mode 100644 index 0000000..38110b1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/turnover/FtbPersonnelsTurnoverManagementController.java @@ -0,0 +1,326 @@ +package jnpf.personnels.controller.web.turnover; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.personnels.dto.base.FtbPersonnelsBaseForOA; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverCreateDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverDTO; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurOrgInfo; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverInfoVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.FtbPersonnelsTurnoverManagementApi; +import jnpf.personnels.service.FtbPersonnelsRegularManagementService; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import jnpf.personnels.service.FtbPersonnelsTransferManageService; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * web离职管理模块 + * + * @author peng.hao + */ +@RestController +@RequestMapping("/web/personnels-turnover") +@Slf4j +public class FtbPersonnelsTurnoverManagementController implements FtbPersonnelsTurnoverManagementApi { + + @Resource + FtbPersonnelsTurnoverManagementService service; + + @Resource + private FtbPersonnelsTransferManageService ftbPersonnelsTransferManageService; + + @Resource + private FtbPersonnelsRegularManagementService ftbPersonnelsRegularManagementService; + + @Resource + FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentManagementService; + + /** + * 离职管理列表 + * + * @param dto 人事变动查询参数 + * @return 查询结果 + */ + @PostMapping("/list-query") + public ActionResult> getTurnoverList(@RequestBody PersonnelsQueryDTO dto){ + CultivatePage page = new CultivatePage(); + page.setCurrentPage(dto.getCurrentPage()); + page.setPageSize(dto.getPageSize()); + PageListVO listVO = service.getTurnoverList(dto,page); + return ActionResult.success(listVO); + } + + /** + * 离职列表导出 + * + * @param dto 人事变动查询参数 + * @return 查询结果 + */ + @PostMapping("/list-query-export") + public void getTurnoverListExport( @RequestBody PersonnelsQueryDTO dto, HttpServletResponse response) throws IOException { + service.getTurnoverListExport(dto,response); + } + + /** + * 获取辞职审批流程详情 + * + * @param id 流程id + * @return 返回辞职审批流程详情结果 + */ + @GetMapping("/details-process/{id}") + public ActionResult detailsOfResignationApprovalProcess(@PathVariable("id")String id){ + FtbPersonnelsTurnoverInfoVO info = service.detailsOfResignationApprovalProcess(id); + return ActionResult.success(info); + } + + /** + * 获取辞职审批流程详情 + * + * @param id 流程id + * @return 返回辞职审批流程详情结果 + */ + @GetMapping("/details-process") + public ActionResult detailsOfResignationApprovalProcessWithId(@RequestParam("id")String id){ + FtbPersonnelsTurnoverInfoVO info = service.detailsOfResignationApprovalProcess(id); + return ActionResult.success(info); + } + /** + * 获取辞职审批流程详情(for oa) + * + * @param id 流程id + * @return 返回辞职审批流程详情结果 + */ + @GetMapping("/details-process-with-taskInfoId") + public ActionResult detailsOfResignationApprovalProcessWithTaskInfoId(@RequestParam("id")String id){ + FtbPersonnelsTurnoverManagement one = service.lambdaQuery().eq(FtbPersonnelsTurnoverManagement::getTaskInfoId, id).one(); + if (ObjectUtil.isEmpty(one)){ + return ActionResult.fail("暂无用户离职详情!"); + } + FtbPersonnelsTurnoverInfoVO info = service.detailsOfResignationApprovalProcess(one.getId()); + return ActionResult.success(info); + } + + + /** + * 申请辞职 (包含重新办理) + * + * @param createDTO 创建DTO对象 + * @return "1" 为是否为审批人 2 是否为门店负责人 3是否存在角色组 "4" 是否有直系下属 6签了电子合同 7签的纸制合同 + */ + @PostMapping("/saveOrUpdate-apply-resignation") + public ActionResult applyForResignation(@Validated @RequestBody FtbPersonnelsTurnoverCreateDTO createDTO){ + return ActionResult.success("" , service.applyForResignation(createDTO)); + + } + + /** + * 申请辞职 (oa提交) + * + * @param createDTO 创建DTO对象 + * @return + */ + @PostMapping("/saveOrUpdate-apply-resignation-for-oa") + public ActionResult applyForResignatioForOA(@Validated @RequestBody FtbPersonnelsTurnoverCreateDTO createDTO){ + ActionResult map = service.applyForResignatioForOA(createDTO); + return map; + + } + + /** + * 获取用户绑定审批结果 + * @param flag 用户离职申请信息 + * @return 动态数据 + */ + @GetMapping("/get-user-bound-approval") + @Deprecated(since = "已弃用") + public ActionResult>> getUserBoundApproval(@RequestParam("flag") String flag, + @RequestParam("userId") String userId){ + return ActionResult.success(service.getUserBoundApproval(flag,userId)); + } + + /** + * 删除辞职申请 + * + * @param ids 应用程序的ID + * @return 返回String类型的ActionResult对象 + */ + @DeleteMapping("/delete-application") + public ActionResult deleteResignationApplication(@RequestBody List ids){ + service.deleteResignationApplicationWithId(ids); + return ActionResult.success(); + } + + /** + * 撤销辞职申请(oa) + * + * @param id 应用程序的ID + * @return 返回String类型的ActionResult对象 + */ + @GetMapping("/withdraw-application") + public ActionResult withdrawResignationApplication( String id,String taskId){ + service.withdrawResignationApplication(id,taskId); + return ActionResult.success(); + } + /** + * 审核离职申请 + * + * @param auditDto 审核DTO对象 + * @return "1" 为是否为审批人 2 是否为门店负责人 3是否存在角色组 + */ + @PutMapping("/review-resignation-application") + @Deprecated(since = "已弃用") + public ActionResult reviewResignationApplication(@RequestBody FtbPersonnelsTurnoverDTO auditDto ){ + String string = service.reviewResignationApplication(auditDto); + return ActionResult.success(string); + } + + /** + * 审核离职申请(for oa) + * + * @param auditDto 审核DTO对象 + * @return + */ + @PostMapping("/review-resignation-application-oa") + public ActionResult reviewResignationApplicationWithOA(@RequestBody FtbPersonnelsTurnoverDTO auditDto ){ + ActionResult map = service.reviewResignationApplicationWithOA(auditDto); + return map; + } + + /** + * 审核离职申请前置校验 + * + * @return 返回Map类型的ActionResult对象 message : "" , code: 1001 1002 1003 + */ + @GetMapping("/resignation-pre-check") + public ActionResult resignationPreCheck(FtbPersonnelsBaseForOA dto){ + ActionResult map = service.resignationPreCheck(dto.getUserIds().get(0),dto.getTaskFlag()); + return map; + } + + @GetMapping("/closeUserAccountRegularlyAfterResignation") + @Override + public ActionResult closeUserAccountRegularlyAfterResignation(@RequestParam("tenantId") String tenantId){ + try { + service.closeUserAccountRegularlyAfterResignation(tenantId); + }catch (Exception e) { + log.error("关闭用户账户定时任务失败",e); + } + try { + // 同步转正状态 + ftbPersonnelsRegularManagementService.syncRegularStatus(tenantId); + }catch (Exception e){ + log.error("同步转正状态失败",e); + } + try { + // 调动生效定时任务 + ftbPersonnelsTransferManageService.transferEffectScheduledTask(tenantId); + }catch (Exception e){ + log.error("调动生效定时任务失败",e); + } + try { + // 定时任务检测当借调结束后薪资生成 借调返岗 + ftbPersonnelsSecondmentManagementService.checkSecondmentEnd(tenantId); + }catch (Exception e){ + log.error("定时任务检测当借调结束后薪资生成 借调返岗失败",e); + } + + return ActionResult.success(); + } + + /** + * 离职用户已签署离职协议 + * @param userId 用户id + * @return + */ + @GetMapping("/user-has-sign-an-agreement") + @Override + public Boolean userHasSignedASeparationAgreement(@RequestParam("userId") String userId,@RequestParam("flag") Integer flag) { + return service.userHasSignedASeparationAgreement(userId, flag); + } + + @Override + public List queryTurnoverList() { + return service.queryTurnoverList(); + } + + /** + * 查询当前数据是否存在离职审批 + * @param id 主键id + * @return + */ + @GetMapping("/query-user-have-turnover") + public ActionResult queryWhetherTheUserHasTheResignationProcess(@RequestParam("id") String id){ + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getId,id); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getEnableMark,0); + List list = new ArrayList<>(); + list.add(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + list.add(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + list.add(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + list.add(FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + lambdaQuery.in(FtbPersonnelsTurnoverManagement::getTurnoverStatus,list); + long count = service.count(lambdaQuery); + Boolean flag = false; + if (count > 0){ + flag = true; + } + return ActionResult.success(flag); + } + + /** + * 根据userId获取当前人员离职是的组织信息 + */ + @GetMapping("/get-user-organize-info") + public ActionResult getUserOrganizeInfo(@RequestParam(name = "userId",required = false) String userId, + @RequestParam(name = "phone",required = false) String phone){ + return ActionResult.success(service.getUserOrganizeInfo(userId,phone)); + } + + /** + * 获取离职人员信息 + * 1.按多个userId + * 2.按多个组织id + * 3.按多个岗位id + */ + @PostMapping("/get-dep-user") + public List getInformationAboutTheDepartingPerson(@RequestBody FtbDepUserDTO dto){ + return service.getInformationAboutTheDepartingPerson(dto); + } + + @PostMapping("/not-token-get-dep-user") + @NoDataSourceBind + public List getInformationAboutTheDepartingPersonNotToken(@RequestBody FtbDepUserDTO dto){ + if(StringUtil.isNotEmpty(dto.getTenantId())){ + try { + TenantDataSourceUtil.switchTenant(dto.getTenantId()); + } catch (LoginException e) { + throw new RuntimeException(e); + } + } + return service.getInformationAboutTheDepartingPerson(dto); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/FtbPersonnelsUchisuikePondController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/FtbPersonnelsUchisuikePondController.java new file mode 100644 index 0000000..45dbeae --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/FtbPersonnelsUchisuikePondController.java @@ -0,0 +1,144 @@ +package jnpf.personnels.controller.web.uchisuike; + +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.personnels.dto.uchisuike.DeleteRecommendedPersonnelDTO; +import jnpf.model.personnels.dto.uchisuike.FtbRecommendationPoolOrgDTO; +import jnpf.model.personnels.dto.uchisuike.FtbinternalRecommendationPoolListDTO; +import jnpf.model.personnels.vo.uchisuike.FtbinternalRecommendationPoolExportVO; +import jnpf.model.personnels.vo.uchisuike.FtbinternalRecommendationPoolVO; +import jnpf.personnels.service.FtbPersonnelsUchisuikePondService; +import jnpf.util.NoDataSourceBind; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * web人事调离内推池模块 + * + * @author wcx + */ +@RestController +@RequestMapping("/web/personnels-uchisuike-pond") +public class FtbPersonnelsUchisuikePondController { + + @Resource + private FtbPersonnelsUchisuikePondService ftbPersonnelsUchisuikePondService; + + /** + * 查看简历,返回简历地址集合 + * + * @param id 内推人员主键id(必传) + * @return {@link ActionResult} + */ + @GetMapping("/view-resume") + public ActionResult> viewResume(String id) { + List result = ftbPersonnelsUchisuikePondService.viewResume(id); + return ActionResult.success(result); + } + + + /** + * 批量删除内推人员 + * + * @return {@link ActionResult} + */ + @DeleteMapping("/delete-recommended-personnel") + public ActionResult deleteRecommendedPersonnel(@RequestBody @Validated DeleteRecommendedPersonnelDTO dto) { + ftbPersonnelsUchisuikePondService.deleteRecommendedPersonnel(dto); + return ActionResult.success(); + } + + + /** + * 内推池列表查询 + * + * @param cultivatePage 分页参数 + * @param ftbinternalRecommendationPoolListDTO 查询参数 + * @return {@link ActionResult}<{@link PageListVO}<{@link FtbinternalRecommendationPoolVO}>> + */ + @GetMapping("/internal-recommendation-pool-list-query") + public ActionResult> internalRecommendationPoolListQuery(CultivatePage cultivatePage, + FtbinternalRecommendationPoolListDTO ftbinternalRecommendationPoolListDTO) { + PageListVO ftbinternalRecommendationPoolVOPageListVO = ftbPersonnelsUchisuikePondService.internalRecommendationPoolListQuery(cultivatePage + , ftbinternalRecommendationPoolListDTO); + return ActionResult.success(ftbinternalRecommendationPoolVOPageListVO); + } + + /** + * 内推池列表导出 + * + * @return {@link ActionResult } + */ + @GetMapping(value = "/list-export") + public void listExport(CultivatePage cultivatePage + , FtbinternalRecommendationPoolListDTO ftbinternalRecommendationPoolListDTO) throws IOException { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletResponse httpServletResponse = requestAttributes.getResponse(); + PageListVO ftbinternalRecommendationPoolVOPageListVO = ftbPersonnelsUchisuikePondService.internalRecommendationPoolListQuery(cultivatePage + , ftbinternalRecommendationPoolListDTO); + List result = ftbinternalRecommendationPoolVOPageListVO.getList() + .stream() + .map(FtbinternalRecommendationPoolExportVO::convert) + .collect(Collectors.toList()); + EasyExcelUtils.exportExcel(httpServletResponse, "内推池", result, FtbinternalRecommendationPoolExportVO.class); + } + + /** + * 内推池详情 + * + * @param id 内推人员主键id(必传) + * @return {@link ActionResult}<{@link FtbinternalRecommendationPoolVO}> + */ + @GetMapping("/internal-recommendation-details") + public ActionResult internalRecommendationDetails(String id) { + FtbinternalRecommendationPoolVO ftbinternalRecommendationPoolVO = ftbPersonnelsUchisuikePondService.internalRecommendationDetails(id); + return ActionResult.success(ftbinternalRecommendationPoolVO); + } + + /** + * 内推池组织变更 + */ + @PostMapping("/internal-recommendation-organizational-changes") + public ActionResult referralPoolOrganizationChange(@RequestBody @Validated FtbRecommendationPoolOrgDTO ftbRecommendationPoolOrgDTO) { + ftbPersonnelsUchisuikePondService.referralPoolOrganizationChange(ftbRecommendationPoolOrgDTO); + return ActionResult.success(); + } + + /** + * 内推池组织查询 + */ + @GetMapping("/internal-referral-pool-organization-query") + public ActionResult> internalReferralPoolOrganizationQuery() { + List results = ftbPersonnelsUchisuikePondService.internalReferralPoolOrganizationQuery(); + return ActionResult.success(results); + } + + /** + * 内推池组织查询,带租户id + */ + @GetMapping("/internal-referral-pool-organization-query-tenant") + @NoDataSourceBind + public ActionResult> internalReferralPoolOrganizationQueryTenant(@RequestParam(value = "tenantId") String tenantId) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + List results = ftbPersonnelsUchisuikePondService.internalReferralPoolOrganizationQuery(); + return ActionResult.success(results); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/PersonnelInformationSupplementController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/PersonnelInformationSupplementController.java new file mode 100644 index 0000000..5e85e92 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/controller/web/uchisuike/PersonnelInformationSupplementController.java @@ -0,0 +1,101 @@ +package jnpf.personnels.controller.web.uchisuike; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@RestController +@RequestMapping("/web/uchisuike/personnelInformationSupplement") +public class PersonnelInformationSupplementController { + + @Resource + private FtbPersonnelsStaffRegistrationFormDataMapper ftbPersonnelsStaffRegistrationFormDataMapper; + + /** + * 补录人事员工类型数据 + */ + @GetMapping("/supplementEmployeeType") + @Transactional(rollbackFor = Exception.class) + public void supplementEmployeeType() { + List result = new ArrayList<>(); + // 员工状态 + List workerStatus = ftbPersonnelsStaffRegistrationFormDataMapper.queryStaffRosterList("workerStatus"); + workerStatus.forEach(a->{ + if (StrUtil.isNotBlank(a.getWorkerStatus())) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = new FtbPersonnelsStaffRegistrationFormData(); + ftbPersonnelsStaffRegistrationFormData.setRosterId(a.getId()); + ftbPersonnelsStaffRegistrationFormData.setPhone(a.getPhone()); + ftbPersonnelsStaffRegistrationFormData.setValue(a.getWorkerStatus()); + ftbPersonnelsStaffRegistrationFormData.setFormFieldId("workerStatus"); + ftbPersonnelsStaffRegistrationFormData.setFormTypeId("3"); + result.add(ftbPersonnelsStaffRegistrationFormData); + } + }); + // 员工类型 + List employeeType = ftbPersonnelsStaffRegistrationFormDataMapper.queryStaffRosterList("workerType"); + employeeType.forEach(a -> { + if (StrUtil.isNotBlank(a.getWorkerType())) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = new FtbPersonnelsStaffRegistrationFormData(); + ftbPersonnelsStaffRegistrationFormData.setRosterId(a.getId()); + ftbPersonnelsStaffRegistrationFormData.setPhone(a.getPhone()); + ftbPersonnelsStaffRegistrationFormData.setValue(a.getWorkerType()); + ftbPersonnelsStaffRegistrationFormData.setFormFieldId("workerType"); + ftbPersonnelsStaffRegistrationFormData.setFormTypeId("3"); + result.add(ftbPersonnelsStaffRegistrationFormData); + } + }); + // 实际入职日期 + List actualStartDate = ftbPersonnelsStaffRegistrationFormDataMapper.queryStaffRosterList("actualStartDate"); + actualStartDate.forEach(a -> { + if (Objects.nonNull(a.getActualStartDate())) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = new FtbPersonnelsStaffRegistrationFormData(); + ftbPersonnelsStaffRegistrationFormData.setRosterId(a.getId()); + ftbPersonnelsStaffRegistrationFormData.setPhone(a.getPhone()); + ftbPersonnelsStaffRegistrationFormData.setValue(DateUtil.format(a.getActualStartDate(), "yyyy-MM-dd")); + ftbPersonnelsStaffRegistrationFormData.setFormFieldId("actualStartDate"); + ftbPersonnelsStaffRegistrationFormData.setFormTypeId("3"); + result.add(ftbPersonnelsStaffRegistrationFormData); + } + }); + // 姓名 + List name = ftbPersonnelsStaffRegistrationFormDataMapper.queryStaffRosterList("workerName"); + name.forEach(a -> { + if (StrUtil.isNotBlank(a.getName())) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = new FtbPersonnelsStaffRegistrationFormData(); + ftbPersonnelsStaffRegistrationFormData.setRosterId(a.getId()); + ftbPersonnelsStaffRegistrationFormData.setPhone(a.getPhone()); + ftbPersonnelsStaffRegistrationFormData.setValue(a.getName()); + ftbPersonnelsStaffRegistrationFormData.setFormFieldId("workerName"); + ftbPersonnelsStaffRegistrationFormData.setFormTypeId("1"); + result.add(ftbPersonnelsStaffRegistrationFormData); + } + }); + // 手机号 + List phone = ftbPersonnelsStaffRegistrationFormDataMapper.queryStaffRosterList("phone"); + phone.forEach(a -> { + if (StrUtil.isNotBlank(a.getPhone())) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = new FtbPersonnelsStaffRegistrationFormData(); + ftbPersonnelsStaffRegistrationFormData.setRosterId(a.getId()); + ftbPersonnelsStaffRegistrationFormData.setPhone(a.getPhone()); + ftbPersonnelsStaffRegistrationFormData.setValue(a.getPhone()); + ftbPersonnelsStaffRegistrationFormData.setFormFieldId("phone"); + ftbPersonnelsStaffRegistrationFormData.setFormTypeId("1"); + result.add(ftbPersonnelsStaffRegistrationFormData); + } + }); + Db.saveBatch(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelCommonListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelCommonListener.java new file mode 100644 index 0000000..babed15 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelCommonListener.java @@ -0,0 +1,21 @@ +package jnpf.personnels.listeners; + +import com.alibaba.excel.context.AnalysisContext; + +import java.util.List; + +public interface BaseEasyExcelCommonListener { + + default boolean before() { + return true; + } + + void dataValidation(AnalysisContext analysisContext, T data, List nomarlData, List errorData); + + void saveDataToRedis(List nomarlData, List errorData); + + default boolean after() { + return true; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelReadListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelReadListener.java new file mode 100644 index 0000000..f80a852 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/BaseEasyExcelReadListener.java @@ -0,0 +1,45 @@ +package jnpf.personnels.listeners; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.read.listener.ReadListener; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class BaseEasyExcelReadListener implements ReadListener { + + private BaseEasyExcelCommonListener baseEasyExcelCommonListener; + + public BaseEasyExcelReadListener(BaseEasyExcelCommonListener baseEasyExcelCommonListener) { + this.baseEasyExcelCommonListener = baseEasyExcelCommonListener; + } + + /** + * 最大行数 + */ + private static final int MAX_ERROR_COUNT = 1000; + + /** + * sheet对应正常内容 + */ + private final List normalData = new ArrayList<>(); + /** + * sheet对应的异常内容 + */ + private final List errorData = new ArrayList(); + + private StringBuilder errorBuilder = new StringBuilder(); + + @Override + public void invoke(T data, AnalysisContext context) { + baseEasyExcelCommonListener.dataValidation(context, data, normalData, errorData); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + baseEasyExcelCommonListener.saveDataToRedis(normalData, errorData); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisEventListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisEventListener.java new file mode 100644 index 0000000..ebaf81b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisEventListener.java @@ -0,0 +1,87 @@ +package jnpf.personnels.listeners; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.bo.FtbRosterImportHeadRuleBO; +import jnpf.model.personnels.vo.roster.FtbRosterCategoryVO; +import jnpf.personnels.service.FtbPersonnelsRosterValidService; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 动态导入分析事件侦听器 + * + * @author fantaibao + * @date 2024/02/18 + */ +@Slf4j +public class DynamicHeadAnalysisEventListener extends AnalysisEventListener> { + + private final FtbPersonnelsRosterValidService ftbPersonnelsRosterValidService; + + /** + * 最大行数 + */ + private static final int MAX_ERROR_COUNT = 1000; + /** + * 表头校验规则 + */ + private Map ftbRosterImportHeadRuleBOMap; + /** + * sheet对应正常内容 + */ + private final FtbRosterCategoryVO normalData = new FtbRosterCategoryVO(); + /** + * sheet对应的异常内容 + */ + private final FtbRosterCategoryVO errorData = new FtbRosterCategoryVO(); + + public DynamicHeadAnalysisEventListener(FtbPersonnelsRosterValidService ftbPersonnelsRosterValidService) { + this.ftbPersonnelsRosterValidService = ftbPersonnelsRosterValidService; + } + + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + log.info("解析到一条头数据:{}, currentRowHolder: {}", headMap.toString(), context.readRowHolder().getRowIndex()); + // 最大行数校验 + if (context.readSheetHolder().getApproximateTotalRowNumber() > MAX_ERROR_COUNT) { + throw new RuntimeException("导入文件中sheet名为" + context.readSheetHolder().getSheetName() + "最大导入行数不能超过" + MAX_ERROR_COUNT); + } + Map stringMap = headMap.entrySet().stream().collect(Collectors.toMap((k) -> k.getKey().toString(), Map.Entry::getValue)); + // 表头校验 + ftbRosterImportHeadRuleBOMap = ftbPersonnelsRosterValidService.headerVerification(context.readSheetHolder().getSheetName(), headMap); + normalData.setHead(stringMap); + normalData.setSheetIndex(context.readSheetHolder().getSheetNo()); + normalData.setClassificationName(context.readSheetHolder().getSheetName()); + normalData.setFtbRosterPageVOS(new ArrayList<>()); + + errorData.setHead(new HashMap<>(stringMap)); + // 手动加错误选项 + errorData.getHead().put(String.valueOf(errorData.getHead().size() + 1), FtbRosterImportConstants.ERROR_REMARKS); + errorData.setSheetIndex(context.readSheetHolder().getSheetNo()); + errorData.setClassificationName(context.readSheetHolder().getSheetName()); + errorData.setFtbRosterPageVOS(new ArrayList<>()); + } + + + public void invoke(Map data, AnalysisContext context) { + log.info("解析到一条数据:{}, currentRowIndex: {}----", data.toString(), context.readRowHolder().getRowIndex()); + ftbPersonnelsRosterValidService.dataValidation(context, ftbRosterImportHeadRuleBOMap, data, normalData, errorData); + } + + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + log.info("本次sheet解析完成,存入Redis中"); + // 存入Redis + ftbPersonnelsRosterValidService.saveDataToRedis(context, normalData, errorData); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisNewEventListener.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisNewEventListener.java new file mode 100644 index 0000000..1f44d12 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/DynamicHeadAnalysisNewEventListener.java @@ -0,0 +1,88 @@ +package jnpf.personnels.listeners; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.bo.FtbRosterImportHeadRuleBO; +import jnpf.model.personnels.vo.roster.FtbRosterCategoryVO; +import jnpf.personnels.service.FtbPersonnelsRosterValidService; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 动态导入分析事件侦听器 + * + * @author fantaibao + * @date 2024/02/18 + */ +@Slf4j +public class DynamicHeadAnalysisNewEventListener extends AnalysisEventListener> { + + private final FtbPersonnelsRosterValidService ftbPersonnelsRosterValidService; + + /** + * 最大行数 + */ + private static final int MAX_ERROR_COUNT = 500; + /** + * 表头校验规则 + */ + private Map ftbRosterImportHeadRuleBOMap; + /** + * sheet对应正常内容 + */ + private final FtbRosterCategoryVO normalData = new FtbRosterCategoryVO(); + /** + * sheet对应的异常内容 + */ + private final FtbRosterCategoryVO errorData = new FtbRosterCategoryVO(); + + public DynamicHeadAnalysisNewEventListener(FtbPersonnelsRosterValidService ftbPersonnelsRosterValidService) { + this.ftbPersonnelsRosterValidService = ftbPersonnelsRosterValidService; + } + + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + log.info("解析到一条头数据:{}, currentRowHolder: {}", headMap.toString(), context.readRowHolder().getRowIndex()); + // 最大行数校验 + if (context.readSheetHolder().getApproximateTotalRowNumber() - 1 > MAX_ERROR_COUNT) { + throw new RuntimeException("导入文件中sheet名为" + context.readSheetHolder().getSheetName() + "最大导入行数不能超过" + MAX_ERROR_COUNT); + } + Map stringMap = headMap.entrySet().stream().filter(entry -> entry.getValue() != null) + .collect(Collectors.toMap((k) -> k.getKey().toString(), Map.Entry::getValue)); + // 表头校验 + ftbRosterImportHeadRuleBOMap = ftbPersonnelsRosterValidService.headerVerification(context.readSheetHolder().getSheetName(), headMap); + normalData.setHead(stringMap); + normalData.setSheetIndex(context.readSheetHolder().getSheetNo()); + normalData.setClassificationName(context.readSheetHolder().getSheetName()); + normalData.setFtbRosterPageVOS(new ArrayList<>()); + + errorData.setHead(new HashMap<>(stringMap)); + // 手动加错误选项 + errorData.getHead().put(String.valueOf(errorData.getHead().size() + 1), FtbRosterImportConstants.ERROR_REMARKS); + errorData.setSheetIndex(context.readSheetHolder().getSheetNo()); + errorData.setClassificationName(context.readSheetHolder().getSheetName()); + errorData.setFtbRosterPageVOS(new ArrayList<>()); + } + + + public void invoke(Map data, AnalysisContext context) { + log.info("解析到一条数据:{}, currentRowIndex: {}----", data.toString(), context.readRowHolder().getRowIndex()); + ftbPersonnelsRosterValidService.dataValidation(context, ftbRosterImportHeadRuleBOMap, data, normalData, errorData); + } + + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + log.info("本次sheet解析完成,存入Redis中"); + // 存入Redis + ftbPersonnelsRosterValidService.saveDataToRedis(context, normalData, errorData); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsRewardsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsRewardsServiceImpl.java new file mode 100644 index 0000000..e1edc13 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsRewardsServiceImpl.java @@ -0,0 +1,164 @@ +package jnpf.personnels.listeners.impl; + +import com.alibaba.excel.context.AnalysisContext; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.rewardspunishments.FtbEmploymentRecordsExcelERRVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbEmploymentRewardExcelVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.api.OrganizeAndPositionAndGradeVO; +import jnpf.personnels.listeners.BaseEasyExcelCommonListener; +import jnpf.personnels.mapper.FtbPersonnelsRewardsPunishmentsMapper; +import jnpf.personnels.service.FtbPersonnelsRewardsPunishmentsService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.groups.Default; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: peng.hao + * @create: 2025/1/6 + */ +@Service(value = "ftbPersonnelsRewardsService") +@Slf4j +public class FtbPersonnelsRewardsServiceImpl implements BaseEasyExcelCommonListener { + + @Autowired + FtbPersonnelsRewardsPunishmentsService ftbPersonnelsRewardsPunishmentsService; + + @Resource + Validator validator; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + FtbPersonnelsRewardsPunishmentsMapper ftbPersonnelsRewardsPunishmentsMapper; + + @Override + public void dataValidation(AnalysisContext analysisContext, + FtbEmploymentRewardExcelVO data, + List nomarlData, + List errorData) { + log.error("解析到一条数据,{}", data.toString()); + String string = returnValidateEntity(data); + if (nomarlData.size() >= 1000){ + FtbEmploymentRecordsExcelERRVO convert = FtbEmploymentRecordsExcelERRVO.convert(data); + convert.setErrorRemarks("导入数据已经超过限制1000条;"); + errorData.add(convert); + return; + } + // 数据关系校验 + if (string.isEmpty() + && StringUtils.isNotEmpty(data.getCurrOrgName()) + && StringUtils.isNotEmpty(data.getCurrPositionName())) { + OrganizeAndPositionAndGradeVO resultCheck = v2UserApi.checkOrganizeNameBoundPositionNameGradeName(data.getCurrOrgName(), data.getCurrPositionName() + , null, null); + if (resultCheck == null) { + string += "填写组织和岗位不存在绑定关系;"; + } else { + data.setCurrOrgId(resultCheck.getOrganizeId()); + data.setCurrPositionId(resultCheck.getPositionId()); + } + } + FtbPersonnelsRewardsPunishmentQueryDTO queryDTO = new FtbPersonnelsRewardsPunishmentQueryDTO(); + queryDTO.setStatus(0); + List results = ftbPersonnelsRewardsPunishmentsMapper.listQuery(queryDTO); + String punishmentTypeName = data.getPunishmentTypeName(); + if (StringUtils.isNotEmpty(punishmentTypeName)){ + if (results.stream().noneMatch(v -> v.getRegularName().equals(punishmentTypeName))){ + string += "填写的奖惩类型名称不存在或者已经被禁用;"; + }else { + FtbPersonnelsRewardsPunishmentQueryVO approvalVO = results.stream().filter( + v -> v.getRegularName().equals(punishmentTypeName)).findFirst().get(); + data.setPunishmentTypeId(approvalVO.getId()); + data.setIsRecorded(approvalVO.getIsRecorded()); + + } + } + if ((StringUtils.isNotEmpty(data.getType() ) && !(data.getType().equals("奖励")|| "乐捐".equals(data.getType()))) ){ + string += "填写奖惩类别不存在;"; + } + FtbPersonnelsStaffRoster serviceOne = staffRosterService.getOne(Wrappers.lambdaQuery() + .eq(FtbPersonnelsStaffRoster::getName, data.getWorkerName()) + .eq(FtbPersonnelsStaffRoster::getSystemWokerId, data.getEmployeeID()) + .last("limit 1") + ); + if (serviceOne == null) { + string += "该员工信息在员工花名册不存在;"; + } else { + data.setUserId(serviceOne.getUserId()); + } + String day = data.getDay(); + if (StringUtils.isNotEmpty(day)){ + String time = "yyyy-MM-dd"; + SimpleDateFormat format = new SimpleDateFormat(time); + try { + Date parse = format.parse(day); + data.setDayReal(parse); + if (day.length() != time.length()) { + string += "填写的日期格式不正确;"; + } else if (!day.equals(format.format(parse))) { + string += "填写的核算日期不存在;"; + } + } catch (Exception e) { + string += "填写的日期格式不正确;"; + } + } + String amount = data.getAmount(); + if (StringUtils.isNotEmpty(amount)){ + try { + BigDecimal bigDecimal = BigDecimal.valueOf(Double.parseDouble(amount)).setScale(2, RoundingMode.HALF_UP); + data.setAmountReal(bigDecimal); + } catch (Exception e) { + string += "填写的奖惩金额格式不正确;"; + } + } + // 校验用户组织权限适用范围,返回null则为超级管理员 + if (string.isEmpty()) { + nomarlData.add(data); + } else { + FtbEmploymentRecordsExcelERRVO convert = FtbEmploymentRecordsExcelERRVO.convert(data); + convert.setErrorRemarks(string); + errorData.add(convert); + } + } + + @Override + public void saveDataToRedis(List nomarlData, List errorData) { + ftbPersonnelsRewardsPunishmentsService.saveDataToRedis(nomarlData, errorData); + } + + /** + * 校验对象-(有返回值) + * + * @param object 待校验对象 + * @return 如果返回空 全部校验通过 + */ + public String returnValidateEntity(Object object) { + Set> constraintViolations = validator.validate(object, Default.class); + if (!constraintViolations.isEmpty()) { + constraintViolations.iterator().next(); + return constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")) + ";"; + } + return ""; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsServiceImpl.java new file mode 100644 index 0000000..ad8bd65 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/listeners/impl/FtbPersonnelsServiceImpl.java @@ -0,0 +1,179 @@ +package jnpf.personnels.listeners.impl; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.base.ActionResult; +import jnpf.enums.personnel.PhoneStatusEnum; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.vo.employment.AddStaffEmploymentApplyExcelERRVO; +import jnpf.model.personnels.vo.employment.AddStaffEmploymentApplyExcelVO; +import jnpf.permission.ContractTypeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.dto.CheckOrgPositionGradesBoundDTO; +import jnpf.personnels.listeners.BaseEasyExcelCommonListener; +import jnpf.personnels.service.FtbPersonnelsBlacklistService; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonnelPerUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.groups.Default; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Title: FtbPersonnelsServiceImpl + * @Author: peng.hao + * @create: 2024/2/20 11:09 + */ +@Service(value = "ftbPersonnelsService") +@Slf4j +public class FtbPersonnelsServiceImpl implements BaseEasyExcelCommonListener { + + @Resource + private FtbPersonnelsStaffEmploymentApplyService service; + + @Resource + FtbPersonnelsStaffRosterService staffRosterService; + + @Resource + Validator validator; + + @Autowired + private PositionApi positionApi; + @Autowired + ContractTypeApi contractTypeApi; + + @Resource + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Resource + AttendanceGroupApi attendanceGroupApi; + + @Autowired + private PersonnelPerUtils personnelPerUtils; + + @Override + public void dataValidation(AnalysisContext analysisContext, AddStaffEmploymentApplyExcelVO data, + List nomarlData, + List errorData) { + log.error("解析到一条数据,{}", data.toString()); + String string = returnValidateEntity(data); + // 数据关系校验 + if (string.isEmpty()) { + CheckOrgPositionGradesBoundDTO checkOrgPositionGradesBoundDTO = new CheckOrgPositionGradesBoundDTO(); + checkOrgPositionGradesBoundDTO.setOrgName(data.getCurrOrgName()); + checkOrgPositionGradesBoundDTO.setPositionName(data.getCurrPositionName()); + checkOrgPositionGradesBoundDTO.setPositionGradesName(data.getCurrRankName()); + CheckOrgPositionGradesBoundDTO resultCheck = positionApi.checkOrgPositionGradesBound(checkOrgPositionGradesBoundDTO); + if (resultCheck == null) { + string += "填写所属组织和入职岗位和入职职等不存在绑定关系;"; + } else { + data.setCurrOrgId(resultCheck.getOrgId()); + data.setCurrPositionId(resultCheck.getPositionId()); + data.setCurrRankId(resultCheck.getPositionGradesId()); + } + } + if (StrUtil.isNotBlank(data.getContractTypeName())) { + String contractTypeName = contractTypeApi.checkContractTypeName(data.getContractTypeName()); + if (StrUtil.isBlank(contractTypeName)) { + string += "合同类型:" + data.getContractTypeName() + "不在系统内;"; + } else { + data.setContractTypeName(data.getContractTypeName()); + data.setContractType(contractTypeName); + } + } + // 手机号,离职黑名单校验 + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(data.getPhone())) { + string += "该人员处于黑名单中;"; + } + //手机号, 员工入职表内手机号检测检测 + FtbPersonnelsStaffEmploymentApply employmentApplyEntity = service.getOne(Wrappers.lambdaQuery() + .eq(FtbPersonnelsStaffEmploymentApply::getPhone, data.getPhone()) + .last("limit 1") + ); + if (null != employmentApplyEntity) { + if (employmentApplyEntity.getIsNeedCheck() == PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) {//0、需要审批 1、不需要审批 + string += "手机号已经在入职列表中;"; + } else { + if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode() && employmentApplyEntity.getStatus() != PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) { + string += "手机号已经在入职列表中,待到岗;"; + } else if ( employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) { + string += "手机号已经在入职审批列表中;"; + } else if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_REJECT.getCode()) { + string += "该手机号已办理入职,但审批流程未通过,请前往待审批列表,进行重新办理;"; + } + } + } + + Boolean aBoolean = staffRosterService.checkPhoneExistForRoster(data.getPhone()); + if (aBoolean) { + string += "手机号已经存在;"; + } + if (nomarlData.stream().anyMatch(item -> item.getPhone().equals(data.getPhone()))) { + int i = analysisContext.readRowHolder().getRowIndex() + 2; + string += "导入手机号在列表第" + i + "行中已经存在;"; + } + + if (StrUtil.isNotBlank(data.getAttendanceGroupName())){ + ActionResult attendanceGroupByName = attendanceGroupApi.getAttendanceGroupByName(data.getCurrOrgName(), data.getAttendanceGroupName()); + if (attendanceGroupByName == null || attendanceGroupByName.getData() == null){ + string += "考勤组不存在;"; + }else { + // 构建id + String id = attendanceGroupByName.getData().getId(); + data.setAttendanceGroup(id); + } + } + try { + List results = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + if (( Objects.nonNull(results) && data.getCurrOrgId() != null ) && !results.contains(data.getCurrOrgId())) { + string +=("导入组织与当前登录人所选权限组织不匹配;"); + } + }catch (Exception e){ + e.printStackTrace(); + string +=("导入组织与当前登录人所选权限组织不匹配;"); + } + // 校验用户组织权限适用范围,返回null则为超级管理员 + if (string.isEmpty()) { + nomarlData.add(data); + } else { + AddStaffEmploymentApplyExcelERRVO convert = AddStaffEmploymentApplyExcelERRVO.convert(data); + convert.setErrorRemarks(string); + errorData.add(convert); + } + + + + } + + @Override + public void saveDataToRedis(List nomarlData, List errorData) { + service.saveInRedis(nomarlData, errorData); + } + + /** + * 校验对象-(有返回值) + * + * @param object 待校验对象 + * @return 如果返回空 全部校验通过 + */ + public String returnValidateEntity(Object object) { + Set> constraintViolations = validator.validate(object, Default.class); + if (!constraintViolations.isEmpty()) { + constraintViolations.iterator().next(); + return constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")) + ";"; + } + return ""; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonStaffMCPMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonStaffMCPMapper.java new file mode 100644 index 0000000..0e31bcd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonStaffMCPMapper.java @@ -0,0 +1,34 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.fantaibao.permission.annotation.DataScope; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.vo.mcp.StaffBirthdayVO; +import jnpf.model.personnels.vo.mcp.StaffNotSignContractVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 人事花名册MCP模块Mapper + * @author wcx + * @date 2026/05/06 + */ +public interface FtbPersonStaffMCPMapper extends BaseMapper { + + /** + * 查询本月生日的员工列表 + * @param startBirthdayTime 月初生日日期 (MM-dd格式) + * @param endBirthdayTime 月末生日日期 (MM-dd格式) + * @return 本月生日员工列表 + */ + List getStaffBirthdayList(@Param("startBirthdayTime") String startBirthdayTime, + @Param("endBirthdayTime") String endBirthdayTime,@Param("userIds") List userIds); + + /** + * 查询入职半月还未签署合同的员工列表 + * @return 未签署合同的已入职员工列表 + */ + @DataScope(tableAlias = "r") + List getListNotSignedContractHalfMonth(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelChangesMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelChangesMapper.java new file mode 100644 index 0000000..c12f5dc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelChangesMapper.java @@ -0,0 +1,42 @@ +package jnpf.personnels.mapper; + +import jnpf.model.personnels.vo.analysis.FtbPersonnelsPostAdjustmentDetailsVO; +import jnpf.model.personnels.vo.analysis.FtbPersonnelsShopAdjustmentDetailsVO; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +public interface FtbPersonnelChangesMapper { + + /** + * 查询在指定月份进行过店铺调整的人员数量 + * + * @param month 月份,格式为YYYY-MM,用于指定查询的月份 + * @param userIds 用户ID列表,用于指定需要查询的用户范围 + * @return 返回进行过店铺调整的人员数量 + */ + BigDecimal numberOfPeopleAdjustingTheStore(@Param("startMonth") String startMonth, @Param("endMonth") String endMonth, @Param("userIds") List userIds, @Param("state") Integer state); + + /** + * 查询部门人员流动比率 + * 该方法用于统计在指定时间段内,特定组织的部门人员流动情况,包括入职、离职等状态的人数比例 + * 主要用于人力资源分析,以帮助管理层理解人员流动趋势和部门人力结构变化 + * + * @param startMonth 开始月份,格式为YYYY-MM,表示统计周期的起始时间 + * @param endMonth 结束月份,格式为YYYY-MM,表示统计周期的结束时间 + * @param userIds 用户ID + * @param state 人员状态,用于过滤统计结果,如0代表全部状态,1代表在职,2代表离职等 + * @return 返回一个列表,包含每个指定组织在指定时间段内的人员流动比率信息 + */ + List departmentHeadcountRatio(@Param("startMonth") String startMonth, @Param("endMonth") String endMonth, @Param("userIds") List userIds, @Param("state") Integer state); + /** + * 查询在指定月份进行过店铺调整的人员列表 + */ + List numberOfPeopleAdjustingTheStoreList(@Param("startMonth") String startMonth, @Param("endMonth") String endMonth, @Param("userIds") List userIds, @Param("state") Integer state); + + /** + * 查询在指定月份进行过岗位调整的人员列表 + */ + List numberOfPeopleAdjustingThePostList(@Param("startMonth") String startMonth, @Param("endMonth") String endMonth, @Param("userIds") List userIds, @Param("state") Integer state); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditCarbonRecipientMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditCarbonRecipientMapper.java new file mode 100644 index 0000000..aa5c048 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditCarbonRecipientMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuditCarbonRecipient; + +public interface FtbPersonnelsAuditCarbonRecipientMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditMasterConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditMasterConfigMapper.java new file mode 100644 index 0000000..ae4a6a6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditMasterConfigMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuditMasterConfig; + +public interface FtbPersonnelsAuditMasterConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskHistoryMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskHistoryMapper.java new file mode 100644 index 0000000..3eca0ef --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskHistoryMapper.java @@ -0,0 +1,13 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuditRunTaskHistory; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsAuditRunTaskHistoryMapper extends BaseMapper { + + List queryHistoryInfo(@Param("id") String id, + @Param("type") String type); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskMapper.java new file mode 100644 index 0000000..52031cb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditRunTaskMapper.java @@ -0,0 +1,19 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuditRunTask; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsAuditRunTaskMapper extends BaseMapper { + List queryInfoThatCurrentUserNeedsToApprove(@Param("userId") String userId, + @Param("configType") Integer configType); + + List queryMyApprovedReviewProcess(@Param("userId") String userId, + @Param("configType") Integer configType); + + String queryHaveRuntask(@Param("businessId") String businessId, @Param("configType") Integer configType, + @Param("userId") String userId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditSubConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditSubConfigMapper.java new file mode 100644 index 0000000..7107095 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditSubConfigMapper.java @@ -0,0 +1,36 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditSubConfigVO; +import jnpf.model.personnels.dto.config.MasterConfigUserBoudDto; +import jnpf.model.personnels.po.FtbPersonnelsAuditSubConfig; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsAuditSubConfigMapper extends BaseMapper { + + List queryInitializeExecutionNode(@Param("orgId") String orgId, + @Param("type") Integer type, + @Param("isOnePersonPasses")Integer isOnePersonPasses); + + FtbPersonnelsAuditSubConfigVO queryAuditSubConfig(@Param("orgId")String orgId, + @Param("userId") String userId, + @Param("configType")Integer configType); + + List> isItAnApprover(@Param("userId") String userId); + @Select("SELECT\n" + + "\tCOUNT(\n" + + "\tsub.F_Id) AS coun \n" + + "FROM\n" + + "\tftb_personnels_audit_master_config AS mas\n" + + "\tINNER JOIN ftb_personnels_audit_sub_config AS sub ON mas.F_Id = sub.F_MasterId \n" + + "WHERE\n" + + "\tmas.F_EnableMark = '0' \n" + + "\tAND sub.F_AuditorId = #{userId}") + Long checkWhetherTheCurrentPersonIsTheApprover(@Param("userId") String userId); + + List queryUserBound(String userId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditTaskInfoMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditTaskInfoMapper.java new file mode 100644 index 0000000..55e7d29 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuditTaskInfoMapper.java @@ -0,0 +1,29 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuditTaskInfo; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditRunTaskInfo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsAuditTaskInfoMapper extends BaseMapper { + /** + * 获取当前审批流程信息 + * + * @param id + * @param masterAuditConfigId + * @return + */ + List getRunTaskInfos(@Param("id") String id,@Param("masterAuditConfigId") String masterAuditConfigId); + + /** + * 获取历史数据的相信 + * + * @param id + * @param masterAuditConfigId + * @return + */ + List getHisTaskInfos(@Param("id")String id + ,@Param("masterAuditConfigId") String masterAuditConfigId); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuthoritysMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuthoritysMapper.java new file mode 100644 index 0000000..c352e94 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsAuthoritysMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuthoritys; + +public interface FtbPersonnelsAuthoritysMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistHistoryMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistHistoryMapper.java new file mode 100644 index 0000000..859ea10 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistHistoryMapper.java @@ -0,0 +1,29 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListHistoryDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistHistory; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListHistoryVO; +import org.apache.ibatis.annotations.Param; + +/** + *

+ * 历史黑名单表 Mapper 接口 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsBlacklistHistoryMapper extends BaseMapper { + + /** + * 分页查询黑名单历史记录 + * + * @param page 分页对象,包含分页信息和排序信息 + * @param dto 查询条件对象,包含需要查询的黑名单历史记录的相关信息 + * @return 返回分页查询结果,包含查询到的黑名单历史记录列表和分页信息 + */ + Page listDropDown(@Param("page") Page page, @Param("params") FtbPersonnelsBlackListHistoryDTO dto); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistMapper.java new file mode 100644 index 0000000..3a908f3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistMapper.java @@ -0,0 +1,28 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklist; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListVO; +import org.apache.ibatis.annotations.Param; + +/** + *

+ * 黑名单 Mapper 接口 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsBlacklistMapper extends BaseMapper { + + /** + * 分页查询黑名单人员信息 + * + * @param page 分页对象,包含了分页查询所需的页码、每页数量等信息 + * @param dto 查询条件对象,用于指定查询黑名单人员的具体条件 + * @return 返回一个分页对象,其中包含了查询结果和分页信息 + */ + Page blacklist(@Param("page") Page page, @Param("params") FtbPersonnelsBlackListDTO dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistTypeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistTypeMapper.java new file mode 100644 index 0000000..52c89ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsBlacklistTypeMapper.java @@ -0,0 +1,16 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistType; + +/** + *

+ * 人事黑名单类型表 Mapper 接口 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsBlacklistTypeMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsMapper.java new file mode 100644 index 0000000..f5b1478 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsMapper.java @@ -0,0 +1,21 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsGoods; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsPageVO; +import org.apache.ibatis.annotations.Param; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/11 +* +*/ +public interface FtbPersonnelsGoodsMapper extends BaseMapper { + + Page listPages(@Param("page") Page page, @Param("params") FtbPersonnelsGoodsFormQueryDTO formQueryDTO); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsReceiveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsReceiveMapper.java new file mode 100644 index 0000000..6d9dfb7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsGoodsReceiveMapper.java @@ -0,0 +1,21 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsGoodsReceive; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceivePageVO; +import org.apache.ibatis.annotations.Param; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/11 +* +*/ +public interface FtbPersonnelsGoodsReceiveMapper extends BaseMapper { + + Page listPages(@Param("page") Page page, @Param("params") FtbPersonnelsGoodsReceiveQueryDTO formQueryDTO); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsIdcardVerificationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsIdcardVerificationMapper.java new file mode 100644 index 0000000..584b1cf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsIdcardVerificationMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsIdcardVerification; + +public interface FtbPersonnelsIdcardVerificationMapper extends BaseMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsOverviewAnalysisMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsOverviewAnalysisMapper.java new file mode 100644 index 0000000..a407a21 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsOverviewAnalysisMapper.java @@ -0,0 +1,145 @@ +package jnpf.personnels.mapper; + +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.vo.analysis.*; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +public interface FtbPersonnelsOverviewAnalysisMapper { + + /** + * 根据指定月份和工作状态列表,计算该月份内处于任一给定工作状态的员工总数 + * + */ + BigDecimal employeesEmployedInTheMonth(@Param("month") String month, @Param("workStatus") List workStatus + , @Param("userIds") List userIds); + + /** + * 在职员工列表 + * + */ + List employeesEmployedInTheMonthList(@Param("month") String month, @Param("workStatus") List workStatus + , @Param("userIds") List userIds); + + /** + * 在职员工司龄统计 + */ + BigDecimal numberOfDaysOfAge(@Param("userIds") List userIds, @Param("month") String month, @Param("workStatus") List workStatus); + + /** + * 员工年龄 + */ + List employeeAge(@Param("userIds") List userIds, @Param("month") String month, @Param("workStatus") List workStatus); + + /** + * 性别分布 + */ + BigDecimal genderDistribution(@Param("month") String month, @Param("workStatus") List workStatus + , @Param("userIds") List userIds, @Param("sex") String sex); + + /** + * 性别分布列表 + */ + List genderDistributionList(@Param("month") String month, @Param("workStatus") List workStatus + , @Param("userIds") List userIds); + + /** + * 员工状态占比 + */ + List employeeStatusProportion(@Param("month") String month, @Param("userIds") List userIds); + + /** + * 员工状态占比列表 + */ + List employeeStatusProportionList(@Param("month") String month, @Param("userIds") List userIds); + + /** + * 用工类型占比 + */ + List proportionOfEmploymentTypes(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 员工类型占比列表 + */ + List proportionOfEmploymentTypesList(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 学历占比 + */ + List educationalBackground(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 司龄分布 + */ + List ageDistribution(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 年龄分布 + */ + List ageDistributionProportion(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 岗位和组织占比 + */ + List departmentHeadcountRatio(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 工龄 + */ + List seniorityDistribution(@Param("month") String month, @Param("userIds") List userIds,@Param("workStatus") List workStatus); + + /** + * 新人数 + * + * @param startMonth + * @param endMonth + * @param userIds + * @param timeRule + * @return + */ + BigDecimal queryTheNumberOfNewcomers(@Param("startMonth")String startMonth, @Param("endMonth")String endMonth, @Param("userIds") List userIds, @Param("timeRule") Integer timeRule); + + /** + * 进行保险购买分析 + *

+ * 根据指定月份、工作状态和用户ID列表,分析并返回人员概况比例信息 + * 此方法主要用于统计和分析特定条件下的保险购买情况,以帮助决策者了解保险覆盖情况 + * + * @param month 用于分析的月份,格式为"yyyy-MM" + * @param workStatus 工作状态列表,用于过滤分析对象 + * @param userIds 用户ID列表,指定需要分析的用户范围 + * @return 返回一个列表,包含根据条件分析得到的人员概况比例信息 + */ + List insurancePurchaseAnalysis(@Param("formFieldId") String formFieldId, @Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + List queryTheNewcomerID(@Param("endMonth")String endMonth, @Param("userIds") List userIds,@Param("timeRule") Integer timeRule); + + List queryTheNumberOfNewcomersList(@Param("startMonth")String startMonth, @Param("endMonth")String endMonth, @Param("userIds") List userIds, @Param("timeRule") Integer timeRule); + + /** + * 保险购买分析列表 + */ + List insurancePurchaseAnalysisList(@Param("formFieldId") String formFieldId, @Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 学历占比列表 + */ + List educationalBackgroundList(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 司龄分布占比列表 + */ + List seniorityDistributionList(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 年龄分布占比列表 + */ + List ageDistributionProportionList(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 司龄分布占比列表 + */ + List companyDistributionList(@Param("month") String month, @Param("workStatus") List workStatus, @Param("userIds") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionUserMapper.java new file mode 100644 index 0000000..f906fff --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionUserMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsPermissionUser; + +public interface FtbPersonnelsPermissionUserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionsMapper.java new file mode 100644 index 0000000..a8521a8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPermissionsMapper.java @@ -0,0 +1,57 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.authoritys.FtbPermissionInfoDTO; +import jnpf.model.personnels.po.FtbPersonnelsPermissions; +import jnpf.model.personnels.vo.authoritys.FtbPermissionInfoVO; +import jnpf.model.personnels.vo.authoritys.FtbPermissionUserVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsPermissionsMapper extends BaseMapper { + + /** + * 分页查询权限信息 + * + * @param page 分页对象,包含分页信息和结果列表 + * @param ftbPermissionInfoDTO 查询条件对象,用于指定查询条件 + * @return 返回分页查询结果,包含权限信息的列表 + */ + Page permissionList(@Param("page") Page page, @Param("params") FtbPermissionInfoDTO ftbPermissionInfoDTO); + + /** + * 获取顶级权限信息 + * + * @param type 类型标识,用于筛选特定类型的权限 + * @param permissionIds 权限ID列表,用于指定需要查询的权限 + * @return 返回顶级权限信息列表 + */ + List getTopLevelPermissions(@Param("type") Integer type, @Param("list") List permissionIds); + + /** + * 获取评审权限列表 + * + * @param permissionIds 权限ID列表,用于指定需要查询的权限 + * @return 返回与评审相关的权限列表 + */ + List getReviewerPermissions(@Param("list") List permissionIds); + + /** + * 模糊查询权限信息 + * + * @param keyword 关键词,用于模糊查询权限信息 + * @return 返回与关键词匹配的权限ID列表 + */ + List rosterFuzzyQuery(@Param("keyword") String keyword); + + /** + * 根据权限ID查询权限相关信息 + * + * @param athorityIds 权限ID列表,用于指定需要查询的权限 + * @return 返回包含权限相关信息的列表 + */ + List queryFtbPermissionUserVoByAuthorityId(@Param("athorityIds") List athorityIds); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPostApplyMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPostApplyMapper.java new file mode 100644 index 0000000..15a3ff7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPostApplyMapper.java @@ -0,0 +1,65 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsPostApply; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyVO; +import org.apache.ibatis.annotations.Param; + +public interface FtbPersonnelsPostApplyMapper extends BaseMapper { + /** + * 获取人员申请列表信息 + * + * @param page 分页对象,包含分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回分页的人员申请列表信息 + */ + Page getListInfo(Page page, @Param("dto") PersonnelsQueryDTO dto); + + /** + * 获取用于App端的人员申请列表 + * + * @param page 分页对象,包含分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回分页的、用于App端的人员申请列表信息 + */ + Page getListForApp(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + /** + * 获取需要App端审批的人员申请列表 + * + * @param page 分页对象,包含分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回分页的、需要App端审批的人员申请列表信息 + */ + Page getListByApprovalForApp(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + /** + * 获取待审批的数据列表 + * + * @param page 分页对象,包含分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回分页的、待审批的人员申请列表信息 + */ + Page listOfDataToBeApproved(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + /** + * 获取审批列表 + * + * @param page 分页对象,包含分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回分页的审批人员申请列表信息 + */ + Page getApprovalList(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + /** + * 获取抄送列表 + * + * @param page 分页对象,包含分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回分页的抄送人员申请列表信息 + */ + Page getListForCC(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPromiseConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPromiseConfigMapper.java new file mode 100644 index 0000000..eda21b1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsPromiseConfigMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsPromiseConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2024/10/8 +* +*/ +public interface FtbPersonnelsPromiseConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRecruitmentChannelsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRecruitmentChannelsMapper.java new file mode 100644 index 0000000..7b3e101 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRecruitmentChannelsMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsRecruitmentChannels; + +public interface FtbPersonnelsRecruitmentChannelsMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldMapper.java new file mode 100644 index 0000000..42ca375 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldMapper.java @@ -0,0 +1,21 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormFieldDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.req.field.QueryRegistrationFormFieldListReq; +import org.apache.ibatis.annotations.Param; + +public interface FtbPersonnelsRegistrationFormFieldMapper extends BaseMapper { + /** + * 分页查询人事注册表单字段 + * + * @param page 分页对象,包含了分页信息和查询的考试培养对象 + * @param params 查询条件对象,包含了人事注册表单字段的查询条件 + * @return 返回一个分页对象,包含了符合查询条件的人事注册表单字段列表 + */ + Page pagingQuery(@Param("page") Page page + , @Param("params") QueryRegistrationFormFieldListReq params); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldOptionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldOptionMapper.java new file mode 100644 index 0000000..e6223fc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormFieldOptionMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; + +public interface FtbPersonnelsRegistrationFormFieldOptionMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormTypeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormTypeMapper.java new file mode 100644 index 0000000..ea16054 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegistrationFormTypeMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; + +public interface FtbPersonnelsRegistrationFormTypeMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegularManagementMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegularManagementMapper.java new file mode 100644 index 0000000..fb1d158 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRegularManagementMapper.java @@ -0,0 +1,38 @@ +package jnpf.personnels.mapper; + +import cn.hutool.core.date.DateTime; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsRegularManagement; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularManagementVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsRegularManagementMapper extends BaseMapper { + + Page pageList(Page page, + @Param("dto") PersonnelsQueryDTO dto); + List queryList(@Param("dto") PersonnelsQueryDTO dto); + + Page getListForApp(Page page, + @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + Page getThePendingApprovalList(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO covert); + + Page getAuditedDataList(Page page, + @Param("dto") FtbPersonnelsForAppQueryDTO covert); + + Page getListForCC(Page page, + @Param("dto") FtbPersonnelsForAppQueryDTO covert); + + Long queryNumberOfRegulars(@Param("userIds") List userIds); + + List queryRegualList(@Param("dto") PersonnelsQueryDTO dto); + + List queryUserInfoForRegularization(@Param("startDate") DateTime startDate, @Param("endDate") DateTime endDate , @Param("userIds") List userIds); + + List getRegularizationStoreDimension(@Param("startDate") String startDate,@Param("endDate") String endDate, @Param("userIds") List userIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationCategoryConfigurationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationCategoryConfigurationMapper.java new file mode 100644 index 0000000..85bfbb0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationCategoryConfigurationMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsResignationCategoryConfiguration; + +/** +* +* +*@Author: peng.hao +*@create: 2024/11/8 +* +*/ +public interface FtbPersonnelsResignationCategoryConfigurationMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationConfigurationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationConfigurationMapper.java new file mode 100644 index 0000000..8a73277 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsResignationConfigurationMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsResignationConfiguration; + +public interface FtbPersonnelsResignationConfigurationMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRewardsPunishmentsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRewardsPunishmentsMapper.java new file mode 100644 index 0000000..cd019ba --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRewardsPunishmentsMapper.java @@ -0,0 +1,107 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.rewardspunishments.FtbEmployeeRewardRecordsQueryDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelSalaryRewardDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryDTO; +import jnpf.model.personnels.dto.salary.FtbSalaryMetaDataQueryDto; +import jnpf.model.personnels.po.FtbPersonnelsRewardsPunishments; +import jnpf.model.personnels.vo.rewardspunishments.FtbEmployeeRewardRecordsVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelSalaryRewardVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbXcEmployeeRewardRecordsVO; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/** + *

+ * 人事奖惩表 Mapper 接口 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsRewardsPunishmentsMapper extends BaseMapper { + + /** + * 查询指定类型的排序最大值 + * + * @return 最大排序值 + */ + Integer queryTheMaximumValueOfSorting(); + + /** + * 更新排序值 + * + * @param sortValue 排序值 + * @return 更新的记录数 + */ + Integer updateSortValue(@Param("sortValue") Integer sortValue); + + /** + * 查询奖励和处罚信息 + * + * @param rewardsPunishmentQueryDTO 查询参数对象 + * @return 奖励和处罚信息列表 + */ + List listQuery(@Param("params") FtbPersonnelsRewardsPunishmentQueryDTO rewardsPunishmentQueryDTO); + + /** + * 向下更新排序范围 + * + * @param minSort 最小排序值 + * @param maxSort 最大排序值 + * @param id 记录ID + * @param type 排序类型 + * @return 更新的记录数 + */ + Integer updateSortDownward(@Param("minSort") Integer minSort, @Param("maxSort") Integer maxSort, + @Param("id") String id); + + /** + * 向下更新排序范围 + * + * @param minSort 最小排序值 + * @param maxSort 最大排序值 + * @param id 记录ID + * @param type 排序类型 + * @return 更新的记录数 + */ + Integer updateSortDownwardS(@Param("minSort") Integer minSort, @Param("maxSort") Integer maxSort, + @Param("id") String id); + + /** + * 查询总工资和奖励信息 + * + * @param ftbPersonnelSalaryRewardDTO 查询参数对象 + * @return 总工资和奖励信息列表 + */ + List queryTheTotalSalaryAndRewards(@Param("params") FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO); + + /** + * 查询总工资和处罚信息 + * + * @param ftbPersonnelSalaryRewardDTO 查询参数对象 + * @return 总工资和处罚信息列表 + */ + List queryTheTotalSalaryAndPenaltys(@Param("params") FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO); + + BigDecimal salaryMetaDataQuery(FtbSalaryMetaDataQueryDto dto); + + + List salaryBatchMetaDataQuery(FtbSalaryMetaDataQueryDto dto); + + /** + * 员工奖惩记录分页查询 + * + * @param page 分页对象 + * @param queryDTO 查询参数对象 + * @return 员工奖惩记录分页数据 + */ + Page employeeRewardAndPunishmentRecords(@Param("page") Page page, @Param("params") FtbEmployeeRewardRecordsQueryDTO queryDTO); + + FtbEmployeeRewardRecordsVO queryDetail(@Param("id") String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRuleConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRuleConfigMapper.java new file mode 100644 index 0000000..da60d6c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsRuleConfigMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsRuleConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2024/10/22 +* +*/ +public interface FtbPersonnelsRuleConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSalaryTemporaryStorageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSalaryTemporaryStorageMapper.java new file mode 100644 index 0000000..722f73c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSalaryTemporaryStorageMapper.java @@ -0,0 +1,15 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsSalaryTemporaryStorage; + +/** +* +*@Title: 薪资信息临时保存 +*@Author: peng.hao +*@create: 2024/7/3:10:50 +* +*/ +public interface FtbPersonnelsSalaryTemporaryStorageMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentConfigMapper.java new file mode 100644 index 0000000..26ccfc6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentConfigMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/12 +* +*/ +public interface FtbPersonnelsSecondmentConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentManagementMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentManagementMapper.java new file mode 100644 index 0000000..ef1c6a0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsSecondmentManagementMapper.java @@ -0,0 +1,66 @@ +package jnpf.personnels.mapper; + +import cn.hutool.core.date.DateTime; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.DataScope; +import jnpf.model.attendance.vo.DailyApprovalVo; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/11 +* +*/ +public interface FtbPersonnelsSecondmentManagementMapper extends BaseMapper { + /** + * 获取列表 + * + * @param page + * @param dto + * @return + */ + @DataScope(tableAlias = "se") + Page pageList(@Param("page") Page page, + @Param("dto") PersonnelsQueryDTO dto); + + int checkIsInTheProcess(@Param("userId") String userId); + + int checkTheSecondmentTime(@Param("userId")String userId, + @Param("secondedStartTime") Date secondedStartTime, + @Param("secondedEndTime") Date secondedEndTime); + + Integer getUserGoOut(@Param("userId") String userId, + @Param("startTime") Date secondedStartTime, + @Param("endTime") Date secondedEndTime); + + Integer getUserGoOutForDay(@Param("userId") String userId, + @Param("startTime") Date secondedStartTime, + @Param("endTime") Date secondedEndTime); + + Integer getBusinessTrip(@Param("userId") String userId, + @Param("startTime") Date secondedStartTime, + @Param("endTime") Date secondedEndTime); + + List getIdList(@Param("userId")String userId,@Param("leaveTime") Date leaveTime); + + List getSecondmentRecord(@Param("userId")String userId, + @Param("startLeaveTime")String startLeaveTime, + @Param("endLeaveTime") String endLeaveTime); + + List getSecondmentRecordBath(@Param("dto") FtbSecondMentQueryDTO dto); + + List checkSecondmentEnd(@Param("date") DateTime date); + + + List queryListApproval(@Param("dto") FtbSecondMentQueryDTO dto); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsShortchainMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsShortchainMapper.java new file mode 100644 index 0000000..a5f8fc2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsShortchainMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsShortchain; + +public interface FtbPersonnelsShortchainMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffArchivesHistoryMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffArchivesHistoryMapper.java new file mode 100644 index 0000000..c34fcb2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffArchivesHistoryMapper.java @@ -0,0 +1,8 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffArchivesHistory; + +public interface FtbPersonnelsStaffArchivesHistoryMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffEmploymentApplyMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffEmploymentApplyMapper.java new file mode 100644 index 0000000..e52f434 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffEmploymentApplyMapper.java @@ -0,0 +1,162 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.emp.FtbEmpEntryDTO; +import jnpf.model.personnels.dto.emp.FtbEmpQueryDTO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsEmployApplyDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.req.employment.QueryAppStaffEmploymentApplyListReq; +import jnpf.model.personnels.req.employment.QueryStaffEmploymentApplyListReq; +import jnpf.model.personnels.vo.emp.FtbEmpEntryVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsStaffEmploymentApplyMapper extends BaseMapper { + /** + * 预入职审批列表 + * + * @param page + * @param params + * @return + */ + Page expectPagingQuery(@Param("page") Page page + , @Param("params") QueryStaffEmploymentApplyListReq params); + + + /** + * 入职审批列表 + * + * @param page + * @param params + * @return + */ + Page checkPagingQuery(@Param("page") Page page + , @Param("params") QueryStaffEmploymentApplyListReq params); + + /** + * 入职审批列表 + * + * @param page + * @param params + * @return + */ + Page myCheckPagingQuery(@Param("page") Page page + , @Param("params") QueryStaffEmploymentApplyListReq params, @Param("checkUserId") String loginUserId); + + /** + * 预入职审批列表 + * + * @param page + * @param params + * @return + */ + Page expectAppPagingQuery(@Param("page") Page page + , @Param("params") QueryAppStaffEmploymentApplyListReq params); + + /** + * 根据常规条件分页查询员工招聘申请列表(用于APP) + * + * @param page 分页对象,包含页码、页面大小等信息 + * @param checkUserId 审核用户ID,用于过滤需要该用户检查的申请 + * @param configType 配置类型,用于区分不同的查询条件 + * @return 返回分页查询结果,包含招聘申请的DTO列表 + */ + Page getListByRegularForApp(@Param("page") Page page, + @Param("checkUserId") String checkUserId, + @Param("configType")Integer configType); + + /** + * 根据用户ID和审核状态分页查询员工招聘申请列表(用于APP) + * + * @param page 分页对象,包含页码、页面大小等信息 + * @param checkUserId 审核用户ID,用于过滤需要该用户检查的申请 + * @param checkStatus 审核状态,用于过滤特定审核状态的申请 + * @return 返回分页查询结果,包含招聘申请的DTO列表 + */ + Page getListForApp(@Param("page") Page page, + @Param("checkUserId") String checkUserId, @Param("checkStatus")Integer checkStatus); + + /** + * 分页查询用户自己的待审核员工招聘申请列表 + * + * @param page 分页对象,包含页码、页面大小等信息 + * @param userId 用户ID,用于查询特定用户提交的申请 + * @param configType 配置类型,用于区分不同的查询条件 + * @return 返回分页查询结果,包含招聘申请的DTO列表 + */ + Page pagingQueryWebMyCheckList(@Param("page") Page page + , @Param("userId") String userId, + @Param("configType") Integer configType); + + /** + * 根据用户ID和审批状态分页查询已审批的员工招聘申请列表 + * + * @param page 分页对象,包含页码、页面大小等信息 + * @param userId 用户ID,用于查询特定用户提交的申请 + * @param configType 配置类型,用于区分不同的查询条件 + * @param approvalStatus 审批状态,用于过滤特定审批状态的申请 + * @return 返回分页查询结果,包含招聘申请的DTO列表 + */ + Page getApprovedList(@Param("page") Page page, + @Param("userId") String userId, + @Param("configType") Integer configType, + @Param("approvalStatus") Integer approvalStatus); + + /** + * 分页查询入职流程中的员工招聘申请列表 + * + * @param page 分页对象,包含页码、页面大小等信息 + * @param req 查询条件对象,包含过滤和排序等信息 + * @return 返回分页查询结果,包含招聘申请的DTO列表 + */ + Page onboardingList(@Param("page") Page page, + @Param("req") QueryStaffEmploymentApplyListReq req); + + Integer checkPhoneForEmploymentApply(@Param("phone") String phone); + + Page pageLists(@Param("page")Page page,@Param("dto") FtbEmpEntryDTO empEntryDTO); + + List getOnboardingPostDimension(@Param("startDate") String startDate,@Param("endDate") String endDate, @Param("userIds") List userIds); + + List onboardingListOA(@Param("phone")String phone,@Param("userIds") List userIds); + + Long queryNumberOfOnboardedEmployees(@Param("orgIds") List orgIds); + + List searchPhoneName(@Param("dto") FtbEmpQueryDTO dto); + + List ListOfOnboardingPositions(@Param("startDate") String startDate,@Param("endDate") String endDate, @Param("userIds") List userIds); + + /** + * 查询已提交登记表但未办理入职的人员 + * @return 已提交登记表但未办理入职的人员列表 + */ + List getSubmittedButNotOnboarded( @Param("userIds") List userIds); + + /** + * 在指定时间段内入职管理表新增的记录数(按 F_CreatorTime) + * + * @param startTime 开始时间(格式:yyyy-MM-dd),为空时默认本月第一天 + * @param endTime 结束时间(格式:yyyy-MM-dd),为空时默认本月最后一天 + * @param userIds + */ + Long getCountEmploymentApplyCreatedInPeriod(@Param("startTime") String startTime, @Param("endTime") String endTime,@Param("userIds") List userIds); + + /** + * 在指定时间段内统计办理入职并已进入花名册的人数(入职表与花名册关联) + * + * @param startTime 开始时间(格式:yyyy-MM-dd),为空时默认本月第一天 + * @param endTime 结束时间(格式:yyyy-MM-dd),为空时默认本月最后一天 + * @param userIds + */ + Long getCountEmploymentApplyToRosterInPeriod(@Param("startTime") String startTime, @Param("endTime") String endTime,@Param("userIds") List userIds); + + /** + * 查询未提交登记表的员工列表 + * @return 未提交登记表的员工列表 + */ + List getListOfNotSubmittedForm(@Param("userIds") List userIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffGrowthLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffGrowthLogMapper.java new file mode 100644 index 0000000..aeebe8d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffGrowthLogMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffGrowthLog; + +public interface FtbPersonnelsStaffGrowthLogMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffHomePageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffHomePageMapper.java new file mode 100644 index 0000000..092890c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffHomePageMapper.java @@ -0,0 +1,18 @@ +package jnpf.personnels.mapper; + +import jnpf.model.personnels.vo.roster.FtbHomePageRewardVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsStaffHomePageMapper { + /** + * 奖励记录 + */ + List getRewardList(@Param("id") String id); + + /** + * 乐捐记录 + */ + List donationRecords(@Param("id") String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRegistrationFormDataMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRegistrationFormDataMapper.java new file mode 100644 index 0000000..46b18a5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRegistrationFormDataMapper.java @@ -0,0 +1,62 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; + +public interface FtbPersonnelsStaffRegistrationFormDataMapper extends BaseMapper { + /** + * 查询健康证是否到期 + */ + List queryHasHealthExpire(); + + /** + * 查询用户转正日期 + */ + Date queryTheUserSRegularizationDate(@Param("userId") String userId); + + /** + * 查询用户调岗日期 + */ + Date queryUserTransferDate(@Param("userId") String userId); + + /** + * 查询用户晋升日期 + */ + Date queryUserPromotionDate(@Param("userId") String userId); + + /** + * 校验某个数据是否存在 + */ + default Integer verifyWhetherTheDataExists(String phone, String formFieldId, String value){ + if("idCardNum".equals(formFieldId)){ + return queryStaffRosterCount(phone,value); + }else { + return verifyWhetherTheDataExistsWithStaff(phone,formFieldId,value); + } + } + Integer verifyWhetherTheDataExistsWithStaff(@Param("phone") String phone, @Param("formFieldId") String formFieldId, @Param("value") String value); + /** + * 校验身份证问题 + */ + Integer verifyWhetherTheDataExistsWithIdCard(@Param("phone") String phone, @Param("value") String value); + + /** + * 根据手机号查询花名册数据 + */ + Integer queryStaffRoster( @Param("phone") String phone, @Param("value") String value); + default Integer queryStaffRosterCount(@Param("phone") String phone, @Param("value") String value) { + Integer integer = queryStaffRoster(phone,value); + Integer integer1 = verifyWhetherTheDataExistsWithIdCard(phone, value); + if (integer1 > 0 || integer > 0 ){ + return 1; + } + return 0; + } + List queryStaffRosterList(@Param("formFieldId") String formFieldId); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterMapper.java new file mode 100644 index 0000000..655984d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterMapper.java @@ -0,0 +1,42 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.DataScope; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.roster.AppStaffRosterListReq; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsStaffRosterMapper extends BaseMapper { + Page pagingQuery(@Param("page") Page page + , @Param("params") StaffRosterListReq params, @Param("userIds") List userIds); + + Page pagingAppQuery(@Param("page") Page page + , @Param("params") AppStaffRosterListReq params, @Param("userIds") List userIds); + + Page pagingQueryNewWithTurnoverManagerList(@Param("params") StaffRosterListReq params, + @Param("userIds") List userIds, + @Param("page") Page page); + + List queryNewWithTurnoverManagerList(@Param("params") StaffRosterListReq params, + @Param("userIds") List userIds); + + String canUpdateRealName(); + + List queryBirthday(@Param("startBirthdayTime") String startBirthdayTime, + @Param("endBirthdayTime") String endBirthdayTime); + + /** + * 在指定时间段内在花名册已完成入职的人数(入职办理:审批通过或无需审批直接办理;或花名册导入无对应已到岗入职记录) + * @param startTime 开始时间(格式:yyyy-MM-dd),为空时默认本月第一天 + * @param endTime 结束时间(格式:yyyy-MM-dd),为空时默认本月最后一天 + */ + @DataScope(tableAlias = "r") + Long getCountOfOnboardedInPeriod(@Param("startTime") String startTime, @Param("endTime") String endTime); + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterSchemeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterSchemeMapper.java new file mode 100644 index 0000000..4b2b1d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffRosterSchemeMapper.java @@ -0,0 +1,8 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffRosterScheme; + +public interface FtbPersonnelsStaffRosterSchemeMapper extends BaseMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffSalaryChangeLogMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffSalaryChangeLogMapper.java new file mode 100644 index 0000000..0ac59bc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffSalaryChangeLogMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffSalaryChangeLog; + +public interface FtbPersonnelsStaffSalaryChangeLogMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionHandoverMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionHandoverMapper.java new file mode 100644 index 0000000..ef2a517 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionHandoverMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPositionHandover; + +public interface FtbPersonnelsStaffTransferPositionHandoverMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionMapper.java new file mode 100644 index 0000000..68cbdbf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsStaffTransferPositionMapper.java @@ -0,0 +1,92 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPosition; +import jnpf.model.personnels.req.transfer.AppQueryTransferListReq; +import jnpf.model.personnels.req.transfer.QueryTransferListReq; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsStaffTransferPositionMapper extends BaseMapper { + + /** + * web已调岗列表 + * + * @param page + * @param params + * @return + */ + Page complatePagingQuery(@Param("page") Page page + , @Param("params") QueryTransferListReq params, @Param("orgIds") List orgIds); + + /** + * web调岗审批列表 + * + * @param page + * @param params + * @return + */ + Page waitCheckPagingQuery(@Param("page") Page page + , @Param("params") QueryTransferListReq params, @Param("orgIds") List orgIds); + + + /** + * 我的调岗 + * + * @param page + * @param loginUserId + * @return + */ + Page myPagingQuery(@Param("page") Page page, @Param("loginUserId") String loginUserId); + + /** + * 查询未完成的调岗 + * + * @param userId + * @return + */ + + TransferPositionDto queryNoCompleteForUserId(String userId); + + TransferPositionDto getInfoForId(String id); + + Page appPagingQuery(@Param("page") Page page + , @Param("params") AppQueryTransferListReq params,@Param("orgIds") List orgIds); + + + Page getListByRegularForApp(@Param("page") Page page, + @Param("dto") AppQueryTransferListReq dto, + @Param("checkUserId") String checkUserId); + + Page getListForApp(@Param("page") Page page, + @Param("dto") AppQueryTransferListReq dto, + @Param("checkUserId") String checkUserId); + + /** + * web 查询我的审批列表 + * + * @param page + * @param configType + * @return + */ + Page pagingQueryWebMyCheckList(@Param("page") Page page, + @Param("userId") String userId, + @Param("configType") Integer configType); + + Integer getCountByRegularForApp(@Param("dto") AppQueryTransferListReq dto, @Param("checkUserId") String checkUserId); + + Integer getCountForApp(@Param("dto") AppQueryTransferListReq dto, + @Param("checkUserId") String checkUserId); + + Page getApprovedList(@Param("page") Page page, + @Param("userId") String userId, + @Param("configType") Integer configType); + + Page queryAccordingToTheCorrespondingStatus(@Param("page") Page page, + @Param("checkStatus")Integer checkStatus, + @Param("userId") String userId, + @Param("configType") Integer configType); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTransferManageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTransferManageMapper.java new file mode 100644 index 0000000..91780fd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTransferManageMapper.java @@ -0,0 +1,57 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fantaibao.permission.annotation.DataScope; +import jnpf.model.personnels.po.FtbPersonnelsTransferManage; +import jnpf.model.personnels.req.transfer.FtbHandleTransferQueryDTO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferDetailsVO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferPageVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsTransferManageMapper extends BaseMapper { + + /** + * 获取处理转让管理列表 + *

+ * 该方法用于处理转让管理页面的查询请求,根据查询参数获取相应的转让信息列表,并进行分页处理 + * + * @param page 分页对象,用于封装分页信息和查询结果 + * @param ftbHandleTransferQueryDTO 查询参数对象,包含了一系列查询条件,如转让状态、转让时间等 + * @return 返回一个分页对象,其中包含符合查询条件的转让信息列表 + */ + @DataScope(tableAlias = "a") + Page transferManageList(@Param("page") Page page, @Param("params") FtbHandleTransferQueryDTO ftbHandleTransferQueryDTO); + + /** + * 查询用户间的转账记录列表(适用于App应用) + * 该方法通过接收一个用户ID列表来筛选出相关的转账记录,并将结果分页返回 + * + * @param page 分页对象,包含分页查询所需的信息,如当前页码、页面大小等 + * @param userId 用户ID列表,用于筛选转账记录,只包含指定用户间的转账记录 + * @return 返回一个分页的转账记录列表,每个记录都封装在FtbHandleTransferPageVO对象中 + */ + Page transferListApp(@Param("page") Page page, @Param("userIds") List userId, @Param("keyWord") String keyWord); + + /** + * 根据ID获取转让详情信息 + * + * @param id 转让详情的唯一标识符 + * @return 返回转让详情的对象,包含具体的转让信息 + */ + FtbHandleTransferDetailsVO transferDetails(@Param("id") String id); + + /** + * 更新转让管理信息 + *

+ * 该方法用于更新转让管理的相关信息,可能涉及到转让申请的审核、转让状态的更新等 + * 由于方法的实现细节未提供,这里仅做大致的功能描述 + * + * @return 成功更新的记录数如果返回值为null,可能表示没有可更新的记录或者更新过程中遇到了错误 + */ + Integer updateTransferManage(); + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAccountRegistrationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAccountRegistrationMapper.java new file mode 100644 index 0000000..8c014e1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAccountRegistrationMapper.java @@ -0,0 +1,9 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverAccountRegistration; + +public interface FtbPersonnelsTurnoverAccountRegistrationMapper extends BaseMapper { + + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAnalysisMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAnalysisMapper.java new file mode 100644 index 0000000..6578ae1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverAnalysisMapper.java @@ -0,0 +1,93 @@ +package jnpf.personnels.mapper; + +import org.apache.ibatis.annotations.Param; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @Author: peng.hao + * @create: 2024/10/14 + */ +public interface FtbPersonnelsTurnoverAnalysisMapper { + /** + * 离职人数 + * @return + */ + Integer getLeavePeopleMonth(@Param("startTime") Date startTime ,@Param("endTime") Date endTime,@Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + + /** + * 计算性别 + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @return + */ + Integer getLeavePeopleSexMonth(@Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + + /** + * 首月/首月/首年离职人数 + * + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @param firstMonth + * @return + */ + Integer getFirstMonthLeaveRate(@Param("startTime") Date startTime , @Param("endTime") Date endTime, @Param("userIdListByOrganizeIds") List userIdListByOrganizeIds,@Param("firstMonth") Integer firstMonth); + + /** + * 获取离职原因分布 + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @return + */ + List> getLeaveReasonDistribution(@Param("startTime") Date startTime ,@Param("endTime") Date endTime,@Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + + /** + * 离职年龄分布查询 + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @return + */ + List> getLeavePeopleAgeDistribution(@Param("startTime") Date startTime ,@Param("endTime") Date endTime,@Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + + + /** + * 离职岗位组织 + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @return + */ + List> getLeavePeoplePostRatio(@Param("startTime") Date startTime ,@Param("endTime") Date endTime,@Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + + /** + * 离职人员信息 + * + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @param workerSex + * @return + */ + List> getLeavePeopleAgeLogRatio(@Param("startTime") Date startTime , @Param("endTime") Date endTime, @Param("userIdListByOrganizeIds") List userIdListByOrganizeIds, + @Param("formId") String formId); + + Integer numberOfOnboardedEmployees(@Param("ringStartTime") Date ringStartTime,@Param("ringEndTime") Date ringEndTime,@Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + + /** + * 获取离职人员明细 + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @return + */ + List> getLeaveRateDetail(@Param("startTime") Date startTime , + @Param("endTime") Date endTime, + @Param("userIdListByOrganizeIds") List userIdListByOrganizeIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverHandoverMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverHandoverMapper.java new file mode 100644 index 0000000..7bcbae1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverHandoverMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverHandover; + +public interface FtbPersonnelsTurnoverHandoverMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverManagementMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverManagementMapper.java new file mode 100644 index 0000000..0c49a32 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsTurnoverManagementMapper.java @@ -0,0 +1,89 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbPersonnelsTurnoverManagementMapper extends BaseMapper { + /** + * 获取人事异动列表 + * + * @param page 分页对象,包含了分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回一个分页对象,其中包含符合查询条件的人事异动列表 + */ + Page getTurnoverList(Page page, @Param("dto") PersonnelsQueryDTO dto); + + /** + * 获取用于APP端的人事异动列表 + * + * @param page 分页对象,包含了分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回一个分页对象,其中包含符合查询条件的、用于APP端的人事异动列表 + */ + Page getListForApp(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + /** + * 获取单个人员的人事异动列表 + * + * @param page 分页对象,包含了分页查询的信息 + * @param dto 查询参数对象,用于传递查询条件 + * @return 返回一个分页对象,其中包含符合查询条件的、单个人员的人事异动列表 + */ + Page getTurnoverListForOnePersonnels(Page page, @Param("dto") FtbPersonnelsForAppQueryDTO dto); + + /** + * 获取待审批的人事异动列表 + * + * @param page 分页对象,包含了分页查询的信息 + * @param covert 查询参数对象,用于传递查询条件 + * @return 返回一个分页对象,其中包含符合查询条件的、待审批的人事异动列表 + */ + Page getThePendingApprovalList(Page page, @Param("covert") FtbPersonnelsForAppQueryDTO covert); + + /** + * 获取已审批的人事异动数据 + * + * @param page 分页对象,包含了分页查询的信息 + * @param covert 查询参数对象,用于传递查询条件 + * @return 返回一个分页对象,其中包含符合查询条件的、已审批的人事异动数据 + */ + Page getApprovedData(Page page, @Param("covert") FtbPersonnelsForAppQueryDTO covert); + + /** + * 获取用于CC(Cross Check)的人事异动列表 + * + * @param page 分页对象,包含了分页查询的信息 + * @param covert 查询参数对象,用于传递查询条件 + * @return 返回一个分页对象,其中包含符合查询条件的、用于CC的人事异动列表 + */ + Page getListForCC(Page page, @Param("covert") FtbPersonnelsForAppQueryDTO covert); + + /** + * 查询所有的人事异动管理用户列表 + * + * @return 返回一个包含所有人事异动管理用户的数据列表 + */ + List queryTurnOverManagementUserList(); + + List queryTurnoverList(@Param("userIds") List userIds); + + List queryUserInfoForTurnover(@Param("date") String date,@Param("userIds") List userIds); + + List queryTurnoverListWithStatics(); + + /** + * 人事异动管理用户列表 + * @param ids + * @return + */ + Long getTurnoverRateAndHeadcount(@Param("ids") List ids); + + Long getLeaveAllPeople(@Param("userIds") List userIds); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondMapper.java new file mode 100644 index 0000000..bbe2622 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsUchisuikePond; + +public interface FtbPersonnelsUchisuikePondMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondOrgMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondOrgMapper.java new file mode 100644 index 0000000..4247c14 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnelsUchisuikePondOrgMapper.java @@ -0,0 +1,7 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnelsUchisuikePondOrg; + +public interface FtbPersonnelsUchisuikePondOrgMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoConfigMapper.java new file mode 100644 index 0000000..7db428d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoConfigMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnlesInfoConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +public interface FtbPersonnlesInfoConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoDiyRangeConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoDiyRangeConfigMapper.java new file mode 100644 index 0000000..fa8ac82 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoDiyRangeConfigMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnlesInfoDiyRangeConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +public interface FtbPersonnlesInfoDiyRangeConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoRangeConfigMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoRangeConfigMapper.java new file mode 100644 index 0000000..f90483f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbPersonnlesInfoRangeConfigMapper.java @@ -0,0 +1,14 @@ +package jnpf.personnels.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import jnpf.model.personnels.po.FtbPersonnlesInfoRangeConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +public interface FtbPersonnlesInfoRangeConfigMapper extends BaseMapper { +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbThousandFacePersonMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbThousandFacePersonMapper.java new file mode 100644 index 0000000..631f772 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/mapper/FtbThousandFacePersonMapper.java @@ -0,0 +1,17 @@ +package jnpf.personnels.mapper; + +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface FtbThousandFacePersonMapper { + /** + * 花名册在职员工 + */ + Long getEmployeeNum(@Param("workStatus") List workStatus, @Param("userIds") List userIds); + + /** + * 调岗员工数量 + */ + Long getTurnoverAllPeople(@Param("userIds") List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceBinder.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceBinder.java new file mode 100644 index 0000000..758b511 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceBinder.java @@ -0,0 +1,61 @@ +package jnpf.personnels.msg; + +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.messaging.MessageChannel; + +/** + * 人事消费源 + * @Author: peng.hao + * @create: 2025/9/15 + */ +public interface PersonnelsConsumerSourceBinder { + String OUTPUT = "secondment-consumer-output"; + + String SECONDMENT_WITHDRAWAL_OUTPUT = "secondment-withdrawal-topic"; + + String SECONDMENT_WITHDRAWAL_DELAY_OUTPUT = "secondment-withdrawal-delay-topic"; + + String SECONDMENT_WITHDRAWAL_EARLY_END_OUTPUT = "secondment-withdrawal-early-end-topic"; + + String OnboardingOUTPUT = "personnel-onboarding-topic"; + + String OnboardingFailOUTPUT = "personnel-onboarding-fail-topic"; + + /** + * 生产通道 + * @return + */ + @Output(OUTPUT) + MessageChannel output(); + + /** + * 借调撤销 + */ + @Output(SECONDMENT_WITHDRAWAL_OUTPUT) + MessageChannel secondmentWithdrawal(); + + /** + * 借调延迟 + */ + @Output(SECONDMENT_WITHDRAWAL_DELAY_OUTPUT) + MessageChannel secondmentDelay(); + + /** + * 借调提前结束 + */ + @Output(SECONDMENT_WITHDRAWAL_EARLY_END_OUTPUT) + MessageChannel secondmentEarlyEnd(); + + /** + * 入职通道 + * @return + */ + @Output(OnboardingOUTPUT) + MessageChannel OnboardingOutput(); + + /** + * 入职失败通道 + */ + @Output(OnboardingFailOUTPUT) + MessageChannel OnboardingFailOutput(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceMsg.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceMsg.java new file mode 100644 index 0000000..217d368 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/msg/PersonnelsConsumerSourceMsg.java @@ -0,0 +1,75 @@ +package jnpf.personnels.msg; + +import jnpf.util.RandomUtil; +import org.apache.rocketmq.common.message.MessageConst; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.messaging.Message; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * @Author: peng.hao + * @create: 2025/9/15 + */ +@Component +@EnableBinding(PersonnelsConsumerSourceBinder.class) +public class PersonnelsConsumerSourceMsg { + + @Resource + private PersonnelsConsumerSourceBinder rocketMessageChannelBinder; + + + + public void sendMessage(String jsonString) { + Message msg = org.springframework.integration.support.MessageBuilder.withPayload(jsonString) + .setHeader(MessageConst.PROPERTY_KEYS, RandomUtil.uuId()) + .build(); + rocketMessageChannelBinder.output().send(msg); + } + + public void sendMessageSecondmentWithdrawal(String jsonString) { + Message msg = org.springframework.integration.support.MessageBuilder.withPayload(jsonString) + .setHeader(MessageConst.PROPERTY_KEYS, RandomUtil.uuId()) + .build(); + rocketMessageChannelBinder.secondmentWithdrawal().send(msg); + } + + /** + * 提前结束 + * @param jsonString + */ + public void sendMessageEarlyEnd(String jsonString) { + Message msg = org.springframework.integration.support.MessageBuilder.withPayload(jsonString) + .setHeader(MessageConst.PROPERTY_KEYS, RandomUtil.uuId()) + .build(); + // rocketMessageChannelBinder.secondmentEarlyEnd().send(msg); + } + + /** + * 延长借调 + * @param jsonString + */ + public void sendMessageExtend(String jsonString) { + Message msg = org.springframework.integration.support.MessageBuilder.withPayload(jsonString) + .setHeader(MessageConst.PROPERTY_KEYS, RandomUtil.uuId()) + .build(); + // rocketMessageChannelBinder.secondmentDelay().send(msg); + } + + public void sendMessageOnboarding(String jsonString) { + Message msg = org.springframework.integration.support.MessageBuilder.withPayload(jsonString) + .setHeader(MessageConst.PROPERTY_KEYS, RandomUtil.uuId()) + .build(); + rocketMessageChannelBinder.OnboardingOutput().send(msg); + } + + public void sendMessageOnboardingFail(String jsonString) { + Message msg = org.springframework.integration.support.MessageBuilder.withPayload(jsonString) + .setHeader(MessageConst.PROPERTY_KEYS, RandomUtil.uuId()) + .build(); + rocketMessageChannelBinder.OnboardingFailOutput().send(msg); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonStaffMCPService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonStaffMCPService.java new file mode 100644 index 0000000..1bc11a7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonStaffMCPService.java @@ -0,0 +1,50 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.vo.mcp.OnboardingThisMonthStatsVO; +import jnpf.model.personnels.vo.mcp.StaffBirthdayVO; +import jnpf.model.personnels.vo.mcp.StaffNotSignContractVO; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; + +import java.util.List; + + +public interface FtbPersonStaffMCPService { + + /** + * 查询已提交登记表但未办理入职的人员 + * @return 已提交登记表但未办理入职的人员列表 + */ + List getSubmittedButNotOnboarded(); + + /** + * 入职统计:入职管理表在指定时间段内新增人数 + 指定时间段内已完成入职流程人数(花名册口径) + * @param startTime 开始时间(格式:yyyy-MM-dd),为空时默认本月第一天 + * @param endTime 结束时间(格式:yyyy-MM-dd),为空时默认本月最后一天 + */ + OnboardingThisMonthStatsVO getOnboardingThisMonthStats(String startTime, String endTime); + + /** + * 查询未提交登记表的员工列表 + * @return 未提交登记表的员工列表 + */ + List getListOfNotSubmittedForm(); + + /** + * 查询本月生日的员工列表 + * @return 本月生日员工列表(包含姓名、组织、岗位) + */ + List getStaffBirthdayList(); + + /** + * 根据手机号查询是否在黑名单中 + * @param phone 手机号 + * @return true为在黑名单中,false为不在黑名单中 + */ + Boolean isInBlacklist(String phone); + + /** + * 查询入职半月还未签署合同的员工列表 + * @return 未签署合同的已入职员工列表 + */ + List getListNotSignedContractHalfMonth(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelChangesService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelChangesService.java new file mode 100644 index 0000000..718b8fd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelChangesService.java @@ -0,0 +1,65 @@ +package jnpf.personnels.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.FtbPersonnelsPeopleTransferRatioVO; +import jnpf.model.personnels.vo.analysis.FtbPersonnelsPostAdjustmentDetailsVO; +import jnpf.model.personnels.vo.analysis.FtbPersonnelsShopAdjustmentDetailsVO; +import jnpf.model.personnels.vo.analysis.FtbPersonnlesBrainShopAdjustmentVO; + +import java.io.IOException; +import java.util.List; + +public interface FtbPersonnelChangesService { + /** + * 人事异动调店人数同比环比 + */ + FtbPersonnlesBrainShopAdjustmentVO getBrainDrainCorrectRateShopAdjustment(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动调岗人数同比环比 + */ + FtbPersonnlesBrainShopAdjustmentVO getBrainDrainCorrectRateTransferPost(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取正则化存储维度调整后的店铺人员信息比例 + *

+ * 该方法根据提供的数据分析 DTO,计算并返回经过正则化处理和店铺维度调整后的人员信息比例 + * 主要用于数据分析和报表展示,以帮助理解店铺人员构成的比例关系 + * + * @param dto 数据分析 DTO,包含进行分析所需的参数和条件 + * @return 返回一个 PageListVO,包含每个店铺经过正则化和维度调整后的人员信息比例 + */ + List getRegularizationStoreDimensionShopAdjustment(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取人员标准化维度下的调动岗位比例信息 + *

+ * 此方法根据传入的分析维度条件,计算并返回人员在不同存储维度下的调动岗位比例 + * 主要用于数据分析,了解人员在不同维度(如部门、时间等)的岗位变动情况 + * + * @param dto 人员分析条件传输对象,包含了分析所需的条件和参数 + * @return 返回一个 PageListVO,包含每个标准化存储维度下的人员调动比例信息 + */ + List getRegularizationStoreDimensionTransferPost(FtbPersonnlesAnalysisDTO dto); + + /** + * 调店人数分布列表 + */ + PageListVO shopAdjustmentDetails(FtbPersonnlesAnalysisDTO dto); + + /** + * 调店人数分布列表导出 + */ + void shopAdjustmentDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + /** + * 调岗人数分布列表 + */ + PageListVO postAdjustmentDetails(FtbPersonnlesAnalysisDTO dto); + + /** + * 调岗人数分布列表导出 + */ + void postAdjustmentDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditCarbonRecipientService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditCarbonRecipientService.java new file mode 100644 index 0000000..d0fe9a6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditCarbonRecipientService.java @@ -0,0 +1,9 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsAuditCarbonRecipient; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbPersonnelsAuditCarbonRecipientService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditMasterConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditMasterConfigService.java new file mode 100644 index 0000000..c7b4029 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditMasterConfigService.java @@ -0,0 +1,28 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.config.FtbPersonnelsAuditConfigDTO; +import jnpf.model.personnels.po.FtbPersonnelsAuditMasterConfig; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditConfigVO; +import jnpf.permission.dto.SynUserBoundDTO; + +import java.util.List; + +public interface FtbPersonnelsAuditMasterConfigService extends IService { + + + @Deprecated + void createPersonnelApprovalProcessConfiguration(FtbPersonnelsAuditConfigDTO dto); + + FtbPersonnelsAuditConfigVO getAuditList(String orgId, Integer configType); + + void createApprovalConfiguration(FtbPersonnelsAuditConfigDTO dto); + + /** + * 审批人数据校验 + * @param userId + * @param OldDtos + * @param newDtos + */ + void approverDataVerification(String userId, List OldDtos, List newDtos); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskHistoryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskHistoryService.java new file mode 100644 index 0000000..de9b245 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskHistoryService.java @@ -0,0 +1,13 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsAuditRunTaskHistory; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.vo.history.FtbPersonnelsAuditRunTaskHistoryVO; + +import java.util.List; + +public interface FtbPersonnelsAuditRunTaskHistoryService extends IService { + + + List viewHistoricalApprovalList(String id, String type); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskService.java new file mode 100644 index 0000000..fab16ec --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditRunTaskService.java @@ -0,0 +1,157 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.po.FtbPersonnelsAuditRunTask; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditRunTaskVO; +import jnpf.util.StringUtil; + +import java.util.List; + +public interface FtbPersonnelsAuditRunTaskService extends IService { + + /** + * 开启审核流程 返回 状态码审核执行状态码 + * + * @param businessId 业务主键id + * @param type 审核类型 + * @param userId + * @return id 主键id写入对应的业务表 + */ + String startTheReviewProcess(String businessId, Integer type, String orgId, String userId); + + /** + * 审核流程 + * @param auditDto 审核DTO + * @return 返回 最终审核状态码 + */ + FtbPersonnelsAuditTaskEnum performReview(FtbPersonnelsAuditDto auditDto,String... userId); + + /** + * 撤销对应的审核流程 + * + * @param businessId 业务主键id + * @return 返回 撤销后的状态 + */ + FtbPersonnelsAuditTaskEnum withdrawTheReviewProcess(String businessId); + + /** + * 获取登录人是否到审核节点 + * @param businessId + * @return 返回待审核状态 否者还是为 审核中 + */ + FtbPersonnelsAuditTaskEnum getWhetherLoginPerToTheAuditNode(String businessId,FtbPersonnelsCofigEnum ftbPersonnelsCofigEnum); + + /** + * 获取当前审核流程配置详情 是否必填 显示项 + * @param businessId 业务主键id + * @return 返回审核流程详情 + */ + FtbPersonnelsAuditRunTaskVO viewTheCurrentApprovalProcess(String businessId); + + /** + * 根据当前用户查询当前需要我审批的审核流程 + * @param userId 当前用户id + * @param ftbPersonnelsCofigEnum 枚举标识 + * @return 返回业务主键ids + */ + List queryInfoThatCurrentUserNeedsToApprove(String userId, FtbPersonnelsCofigEnum ftbPersonnelsCofigEnum); + + + /** + * 查询我已经审批的流程 + * @param userId 当前用户id + * @param ftbPersonnelsCofigEnum 枚举标识 + * @return 返回业务主键ids + */ + List queryMyApprovedReviewProcess(String userId, FtbPersonnelsCofigEnum ftbPersonnelsCofigEnum); + + /** + * 查询当前是否为或签 + * + * @param businessId + * @param configType + * @param userId + * @return + */ + Boolean checkWhetherTheCurrentSignIsOr(String businessId, Integer configType, String userId); + + /** + * 查询当前人是否为审批人 + * @param userId + * @return + */ + Boolean checkWhetherTheCurrentApproverIs(String userId); + + /** + * 查询当前业务是否处于审核流程中 + * @param businessId + * @return false 不处于, true 处于审批中 + */ + Boolean checkIsTheCurrentBusinessInTheApprovalProcess(String businessId); + /** + * 查询当前业务是否处于审核流程中 + * @param businessId + * @return FtbPersonnelsCofigEnum 处于其中的枚举类型中 没有返回空 + */ + FtbPersonnelsCofigEnum checksItInTheReviewProcess(String businessId); + + /** + * 查询运行流程 + * @param id + * @return + */ + FtbPersonnelsAuditRunTaskVO viewTheCurrentApprovalProcessByTaskId(String id); + /** + * 查询对应的人员是否处于任意流程中 + * @param userId + * @return null 不处于 ,1转正 2,调岗, 3离职, 4晋升 + */ + String checkWhetherTheCurrentPersonnelIsInTheReviewProcess(String userId); + + /** + * 清除对应审批数据 + * @param runTaskId + * @param businessId + */ + void deleteReviewProcessAndRecords(String runTaskId, String businessId); + + void closeAllExecutionData(List teantIdList); + + default String checkWhetherTheCheckIsInTheProcessForOA(String userId) { + // null 不处于 ,1 转正 2,调岗, 3离职 4晋升 5调店 6降职 + String s = checkWhetherTheCurrentPersonnelIsInTheReviewProcess(userId); + if (StringUtil.isNotEmpty(s)) { + // 1 转正 2 调岗, 3 离职 + String str = ""; + switch (s) { + case "1": + str = "转正"; + break; + case "2": + str = "调岗"; + break; + case "3": + str = "离职"; + break; + case "4": + str = "晋升"; + break; + case "5": + str = "调店"; + break; + case "6": + str = "降职"; + break; + case "7": + str = "借调"; + break; + } + return ("当前人员处于" + str + "审批流程中,无法再次发起流程!"); + } + return null; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditSubConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditSubConfigService.java new file mode 100644 index 0000000..b5c92e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditSubConfigService.java @@ -0,0 +1,28 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.dto.config.MasterConfigUserBoudDto; +import jnpf.model.personnels.po.FtbPersonnelsAuditSubConfig; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditInfoVO; + +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsAuditSubConfigService extends IService { + /** + * 查询当前申请人是否具有填写审核权限 + * @param orgId + * @return + */ + FtbPersonnelsAuditInfoVO queryAuditSubConfig(String orgId, FtbPersonnelsCofigEnum cofigEnum); + + List> isItAnApprover(String userId); + + + List queryUserBound(String userId); + + + Long checkWhetherTheCurrentPersonIsTheApprover(String userId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditTaskInfoService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditTaskInfoService.java new file mode 100644 index 0000000..51d7357 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuditTaskInfoService.java @@ -0,0 +1,14 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsAuditTaskInfo; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditRunTaskInfo; + +import java.util.List; + +public interface FtbPersonnelsAuditTaskInfoService extends IService { + + List getRunTaskInfos(String id, String masterAuditConfigId); + List getHisTaskInfos(String id, String masterAuditConfigId); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuthoritysService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuthoritysService.java new file mode 100644 index 0000000..a9a1f4f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsAuthoritysService.java @@ -0,0 +1,9 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsAuthoritys; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbPersonnelsAuthoritysService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistHistoryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistHistoryService.java new file mode 100644 index 0000000..b98145a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistHistoryService.java @@ -0,0 +1,28 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListHistoryDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistHistory; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListHistoryVO; + +/** + *

+ * 历史黑名单表 服务类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsBlacklistHistoryService extends IService { + + /** + * 分页查询黑名单历史记录 + * + * @param page 分页对象,包含了分页请求的页码、每页数量等信息 + * @param dto 查询条件对象,用于传递前端请求的查询参数 + * @return 返回一个分页对象,包含了查询结果的总记录数和当前页数据列表 + */ + Page listDropDown(Page page, FtbPersonnelsBlackListHistoryDTO dto); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistService.java new file mode 100644 index 0000000..41c46e0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistService.java @@ -0,0 +1,64 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackUpdateDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistAddDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklist; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListVO; + +import java.util.List; + +/** + *

+ * 黑名单 服务类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsBlacklistService extends IService { + /** + * 办理离职时调用添加人员到黑名单 + */ + void addBlacklist(FtbPersonnelsBlacklistAddDTO ftbPersonnelsBlacklistAddDTO); + + /** + * 办理入职时调用,该人员是否被拉入黑名单 + * + * @param phone 手机号必传 + * @return {@link Boolean } true为是,false为否 + */ + Boolean hasItBeenBlacklisted(String phone); + + /** + * 查询黑名单分页数据 + * + * @param page 分页对象,包含分页参数和分页结果 + * @param dto 查询条件对象,用于筛选分页结果 + * @return 返回分页查询结果,包含列表数据和总记录数 + */ + Page blacklist(Page page, FtbPersonnelsBlackListDTO dto); + + /** + * 编辑黑名单信息 + * + * @param fdto 包含要更新的黑名单信息的对象 + */ + void editBlacklist(FtbPersonnelsBlackUpdateDTO fdto); + + /** + * 移除黑名单项 + * + * @param id 黑名单项的唯一标识符 + */ + void removeBlacklist(String id); + + /** + * 获取黑名单期限枚举列表 + * + * @return 返回黑名单期限枚举列表 + */ + List getBlacklist(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistTypeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistTypeService.java new file mode 100644 index 0000000..f4f88d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBlacklistTypeService.java @@ -0,0 +1,46 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistTypeAddDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistTypeUpdateDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistType; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlacklistTypeListVO; + +import java.util.List; + + +/** + *

+ * 人事黑名单类型表 服务类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsBlacklistTypeService extends IService { + + /** + * 添加新的人员黑名单类型 + * + * @param ftbPersonnelsBlacklistTypeAddDTO 包含新增黑名单类型信息的DTO对象 + */ + void add(FtbPersonnelsBlacklistTypeAddDTO ftbPersonnelsBlacklistTypeAddDTO); + + /** + * 删除指定的人员黑名单类型 + * @param id 黑名单类型的唯一标识符 + */ + void delete(String id); + + /** + * 更新人员黑名单类型的数据信息 + * @param ftbPersonnelsBlacklistTypeUpdateDTO 包含更新黑名单类型信息的DTO对象 + */ + void updateData(FtbPersonnelsBlacklistTypeUpdateDTO ftbPersonnelsBlacklistTypeUpdateDTO); + + /** + * 获取人员黑名单类型的列表数据 + * @return 返回一个包含所有黑名单类型信息的列表对象 + */ + List listData(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBrainDrainService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBrainDrainService.java new file mode 100644 index 0000000..a503317 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsBrainDrainService.java @@ -0,0 +1,167 @@ +package jnpf.personnels.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; + +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2025/4/11 + */ +public interface FtbPersonnelsBrainDrainService { + /** + * 人事异动转正人数同比环比 + */ + FtbPersonnlesBrainDrainVO getBrainDrainCorrectRateComparison(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动新人率人数同比环比 + * @param dto + * @return + */ + FtbPersonnlesBrainDrainVO getBrainDrainCorrectRateNewcomerRate(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动入职岗位维度 + * @param dto + * @return + */ + + List getOnboardingPostDimension(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动转正岗位维度 + * @param dto + * @return + */ + List getRegularPostDimension(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动离职岗位维度 + * @param dto + * @return + */ + List getTurnoverPostDimension(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动入职人数 门店维度,部门维度 + * @param dto + * @return PageListVO + */ + List getOnboardingStoreDimension(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动转正人数 门店维度,部门维度 + * @param dto + * @return PageListVO + */ + List getRegularizationStoreDimension(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动离职人数 门店维度,部门维度 + * @param dto + * @return PageListVO + */ + List getTurnoverStoreDimension(FtbPersonnlesAnalysisDTO dto); + + /** + * 人事异动新人率 岗位维度、门店维度、部门维度 进行展示 + * @param dto + * @return PageListVO + */ + List getNewHireRateDimension(FtbPersonnlesAnalysisDTO dto); + + + /** + * 入职明细 + * @param dto + * @return PageListVO + */ + PageListVO getOnboardingDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 转正明细 + * @param dto + * @return PageListVO + */ + PageListVO getRegularizationDetail(FtbPersonnlesAnalysisDTO dto); + + + /** + * 新人率占比明细 + * @param dto + * @return PageListVO + */ + PageListVO getNewHireRateDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 新人率占比明细导出 + * @param dto + * @param response + */ + void getNewHireRateDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + /** + * 入职明细导出 + * @param dto + * @param response + */ + void getOnboardingDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + /** + * 转正明细导出 + * @param dto + * @param response + */ + void getRegularizationDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + + /** + * 百分比计算(保留两位小数) + * + * @param a 分子 + * @param b 分母 + * @return {@link BigDecimal } + */ + @SuppressWarnings("Duplicates") + static BigDecimal computeDivision(Object a, Object b) { + BigDecimal result = computeDivisionNo(a, b); + return result.multiply(BigDecimal.valueOf(100)); + } + /** + * 非百分比计算 + */ + @SuppressWarnings("Duplicates") + static BigDecimal computeDivisionNo(Object a, Object b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof BigDecimal) { + if (((BigDecimal) b).compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.divide(b0, 2, RoundingMode.HALF_UP); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsContactInfoService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsContactInfoService.java new file mode 100644 index 0000000..954460f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsContactInfoService.java @@ -0,0 +1,15 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; + +import javax.servlet.http.HttpServletResponse; + +public interface FtbPersonnelsContactInfoService { + + + void syncContactInfo(ContactStatusInfo info); + + void checkNotSendContactSign(String tenantId); + + void FileListZip(String rootDir, String userId, HttpServletResponse response); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmEntryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmEntryService.java new file mode 100644 index 0000000..8f3ad16 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmEntryService.java @@ -0,0 +1,58 @@ +package jnpf.personnels.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.emp.FtbEmpAddNewDTO; +import jnpf.model.personnels.dto.emp.FtbEmpConfirmDTO; +import jnpf.model.personnels.dto.emp.FtbEmpEntryDTO; +import jnpf.model.personnels.dto.emp.FtbEmpQueryDTO; +import jnpf.model.personnels.dto.oa.RequestForOA; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.vo.emp.FtbEmpAddNewVO; +import jnpf.model.personnels.vo.emp.FtbEmpConfirmVO; +import jnpf.model.personnels.vo.emp.FtbEmpEntryVO; +import jnpf.model.personnels.vo.emp.FtbEmpResultVO; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2025/4/7 + */ +public interface FtbPersonnelsEmEntryService { + + PageListVO pageLists(FtbEmpEntryDTO empEntryDTO, String app); + + FtbEmpResultVO addNewEmp(FtbEmpAddNewDTO dto); + + String promoteOnboardingManagement(FtbEmpAddNewDTO dto, String tenantId, String moduleId); + + void terminateOnboarding(String id); + + void deleteEmployeeOnboardingRecords(String id); + + void handleJoinJob(FtbEmpConfirmDTO ftbEmpConfirmDTO); + + void handleJoinJobOA(FtbEmpConfirmDTO ftbEmpConfirmDTO); + + FtbEmpConfirmVO onboardingDetails(String id); + + void onboardingApprovals(FtbPersonnelsAuditDto personnelsSalaryAuditDto); + + void onboardingWithdrawal(FtbPersonnelsAuditDto personnelsAuditDto); + + FtbEmpAddNewVO queryNewEmp(String id); + + List queryCrewsWithEntry(RequestForOA request); + + List onboardingList(String phone); + + void updatePhoneByUserId(String userId, String phone); + + List searchPhoneName(FtbEmpQueryDTO keyword); + + void onboardingListExport(FtbEmpEntryDTO req, HttpServletResponse response) throws IOException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmployeeTypeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmployeeTypeService.java new file mode 100644 index 0000000..6604b06 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsEmployeeTypeService.java @@ -0,0 +1,22 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.employeetype.FtbEmployeeTypeAddDTO; +import jnpf.model.personnels.dto.employeetype.FtbEmployeeTypeEditDTO; +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; + +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsEmployeeTypeService { + + void addEmployeeType(FtbEmployeeTypeAddDTO employeeType); + + void editEmployeeType(FtbEmployeeTypeEditDTO employeeType); + + void deleteEmployeeType(String id); + + List employeeList(); + + Map getEmployeeTypeByUserIds(List userIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsReceiveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsReceiveService.java new file mode 100644 index 0000000..49bb927 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsReceiveService.java @@ -0,0 +1,33 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveAddDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveReturnDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveUpdateDTO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceiveDetailsVO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceivePageVO; + +import java.util.List; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/11 +* +*/ +public interface FtbPersonnelsGoodsReceiveService { + + void create(List form); + + void update(FtbPersonnelsGoodsReceiveUpdateDTO form); + + void delete(String id); + + Page list(Page page, FtbPersonnelsGoodsReceiveQueryDTO formQueryDTO); + + FtbPersonnelsGoodsReceiveDetailsVO details(String id); + + void returnGoods(FtbPersonnelsGoodsReceiveReturnDTO returnDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsService.java new file mode 100644 index 0000000..99acb23 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsGoodsService.java @@ -0,0 +1,26 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormUpdateDTO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsPageVO; + +/** + * @Author: peng.hao + * @create: 2025/9/11 + */ +public interface FtbPersonnelsGoodsService { + + void create(FtbPersonnelsGoodsFormDTO form); + + void update(FtbPersonnelsGoodsFormUpdateDTO form); + + void delete(String id); + + Page list(Page page, FtbPersonnelsGoodsFormQueryDTO formQueryDTO); + + FtbPersonnelsGoodsPageVO details(String id); + + Boolean deleteConfirm(String id); +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsMetaDataService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsMetaDataService.java new file mode 100644 index 0000000..172a4aa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsMetaDataService.java @@ -0,0 +1,74 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.roster.meta.PersonnelsMetaDTO; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaDataReq; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaFuctionReq; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsMetaDataService { + + /** + * 支持薪酬 查询花名册元数据信息 + * @param info + * @return + */ + List getMetaData(FtbPersonnelsMetaDataReq info); + /** + * 查询用户当月是否已经离职 + * + * @param userId 用户ID + * @param tenantId 租户ID + * @return true-已经离职 false-未离职 + */ + Boolean queryCurrMonthDepartStatus(String userId, String tenantId); + + /** + * 查询用户当月是否已经离职 + * + * @param userId 用户ID + * @param tenantId 租户ID + * @param tenantId startDate + * @param tenantId endDate + * @return true-已经离职 false-未离职 + */ + Boolean queryCurrMonthDepartStatusByDate(String userId, String tenantId, Date startDate, Date endDate); + + /** + * 查询用户当月是否已经离职 批量 + * + */ + Map queryCurrMonthLeave( FtbPersonnelsMetaFuctionReq req); + + /** + * 查询在A和B天数范围内入职的用户ID + * + * @param startDays 开始天数 + * @param endDays 结束天数 + * @return 用户id集合 + */ + List queryUserIdsForCompanyAge(Integer startDays, Integer endDays); + + /** + * 查询指定用户的司龄 + * @param userIds 用户ID + * @return + */ + Map queryCompanyAge(List userIds); + + /** + * 查询指定用户的司龄 + * @param rosterId 花名册iD + * @return + */ + Integer queryCompanyAgeByRosterId(String rosterId); + + /** + * 查询所有用户的司龄 + * @return + */ + Map queryAllUserCompanyAge(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsOverviewAnalysisService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsOverviewAnalysisService.java new file mode 100644 index 0000000..809b017 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsOverviewAnalysisService.java @@ -0,0 +1,233 @@ +package jnpf.personnels.service; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewAgeDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewCompanyDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewSeniorityDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewInsuranceDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewEducationDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewDepartmentDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewStoreDetailsVO; +import jnpf.model.personnels.vo.analysis.PersonnelsOverviewJobDetailsVO; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +public interface FtbPersonnelsOverviewAnalysisService { + /** + * 在职员工 + */ + PersonnelsOverviewEmployeesVO currentEmployees(FtbPersonnlesAnalysisDTO dto); + + /** + * 平均年龄 + */ + PersonnelsOverviewEmployeesVO compositeLife(FtbPersonnlesAnalysisDTO dto); + + /** + * 平均司龄 + */ + PersonnelsOverviewEmployeesAgeVO averageAge(FtbPersonnlesAnalysisDTO dto); + + /** + * 性别分布 + */ + List genderDistribution(FtbPersonnlesAnalysisDTO dto); + + /** + * 员工状态占比 + */ + List employeeStatusProportion(FtbPersonnlesAnalysisDTO dto); + + /** + * 用工类型占比 + */ + List proportionOfEmploymentTypes(FtbPersonnlesAnalysisDTO dto); + + /** + * 年龄分布占比 + */ + List ageDistributionProportion(FtbPersonnlesAnalysisDTO dto); + + /** + * 司龄分布 + */ + List ageDistribution(FtbPersonnlesAnalysisDTO dto); + + /** + * 学历占比 + */ + List educationalBackground(FtbPersonnlesAnalysisDTO dto); + + /** + * 部门人数占比 + */ + List departmentHeadcountRatio(FtbPersonnlesAnalysisDTO dto); + + /** + * 岗位人数占比 + */ + List proportionOfJobHeadcount(FtbPersonnlesAnalysisDTO dto); + + /** + * 在职员工柱状图 + */ + List statusOfCurrentEmployees(FtbPersonnlesAnalysisDTO dto); + + + /** + * 百分比计算(保留两位小数) + * + * @param a 分子 + * @param b 分母 + * @return {@link BigDecimal } + */ + @SuppressWarnings("Duplicates") + static BigDecimal computeDivision(Object a, Object b) { + BigDecimal result = computeDivisionNo(a, b); + return result.multiply(BigDecimal.valueOf(100)); + } + + /** + * 非百分比计算 + */ + @SuppressWarnings("Duplicates") + static BigDecimal computeDivisionNo(Object a, Object b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof BigDecimal) { + if (((BigDecimal) b).compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.divide(b0, 4, RoundingMode.HALF_UP); + } + + /** + * 非百分比自定义计算 + */ + static BigDecimal percentCustomCalculation(Object a, Object b, int scale) { + return computeDivisionSb(a, b, scale).divide(BigDecimal.valueOf(100), scale, RoundingMode.HALF_UP); + } + + /** + * 百分比计算自定义计算 + */ + @SuppressWarnings("Duplicates") + static BigDecimal computeDivisionSb(Object a, Object b, int scale) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof BigDecimal) { + if (((BigDecimal) b).compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.divide(b0, scale, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + + /** + * 总年龄计算 + */ + default BigDecimal totalAgeCalculation(List ages) { + Integer chainValue = ages.stream() + .filter(StrUtil::isNotBlank) + .map(DateUtil::ageOfNow) + .reduce(0, Integer::sum); + return BigDecimal.valueOf(chainValue); + } + + List seniorityDistribution(FtbPersonnlesAnalysisDTO dto); + + /** + * 保险购买分析 + */ + List insurancePurchaseAnalysis(FtbPersonnlesAnalysisDTO dto); + + List proportionOfStoreNumbers(FtbPersonnlesAnalysisDTO dto); + + PageListVO statusOfCurrentEmployeesDetails(FtbPersonnlesAnalysisDTO dto); + + void statusOfCurrentEmployeesDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO genderDistributionDetails(FtbPersonnlesAnalysisDTO dto); + + void genderDistributionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO employeeStatusProportionDetails(FtbPersonnlesAnalysisDTO dto); + + void employeeStatusProportionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO proportionOfEmploymentTypesDetails(FtbPersonnlesAnalysisDTO dto); + + void proportionOfEmploymentTypesDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO ageDistributionProportionDetails(FtbPersonnlesAnalysisDTO dto); + + void ageDistributionProportionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO companyDistributionDetails(FtbPersonnlesAnalysisDTO dto); + + void companyDistributionDetailsExport(FtbPersonnlesAnalysisDTO dto); + + PageListVO seniorityDistributionDetails(FtbPersonnlesAnalysisDTO dto); + + void seniorityDistributionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO insurancePurchaseAnalysisDetails(FtbPersonnlesAnalysisDTO dto); + + void insurancePurchaseAnalysisDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO educationalBackgroundDetails(FtbPersonnlesAnalysisDTO dto); + + void educationalBackgroundDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO departmentHeadcountRatioDetails(FtbPersonnlesAnalysisDTO dto); + + void departmentHeadcountRatioDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO proportionOfStoreNumbersDetails(FtbPersonnlesAnalysisDTO dto); + + void proportionOfStoreNumbersDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; + + PageListVO proportionOfJobHeadcountDetails(FtbPersonnlesAnalysisDTO dto); + + void proportionOfJobHeadcountDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionUserService.java new file mode 100644 index 0000000..f1e24c5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionUserService.java @@ -0,0 +1,9 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsPermissionUser; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbPersonnelsPermissionUserService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionsService.java new file mode 100644 index 0000000..bc93b8a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPermissionsService.java @@ -0,0 +1,111 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.UserInfo; +import jnpf.model.cultivate.vo.identify.IdentifyAppAuthorityAppraiserVO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsDTO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsUpdateDTO; +import jnpf.model.personnels.dto.authoritys.FtbPermissionInfoDTO; +import jnpf.model.personnels.po.FtbPersonnelsPermissions; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.vo.authoritys.FtbPermissionInfoVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionUserVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsScopeVO; + +import java.util.List; + +public interface FtbPersonnelsPermissionsService extends IService { + + + /** + * 删除指定的权限 + * + * @param id 权限的唯一标识 + */ + void deletePermissions(String id); + + /** + * 在添加时查询权限列表 + * + * @param moduleType 模块类型 + * @return 返回权限列表 + */ + List queryThePermissionListWhenAdding(Long moduleType); + + /** + * 根据用户信息、父权限和权限类型查询权限列表 + * + * @param userInfo 用户信息 + * @param parentPermissions 父权限 + * @param permissionType 权限类型 + * @return 返回权限列表 + */ + List queryPermissionList(UserInfo userInfo, String parentPermissions, Integer permissionType); + + /** + * 添加新的权限 + * + * @param ftbAddNewPermissionsDTO 包含新的权限信息的数据传输对象 + */ + void addNewPermissions(FtbAddNewPermissionsDTO ftbAddNewPermissionsDTO); + + /** + * 添加新的权限变更 + * + * @param ftbAddNewPermissionsUpdateDTO 包含新的权限变更信息的数据传输对象 + */ + void addNewPermissionsChange(FtbAddNewPermissionsUpdateDTO ftbAddNewPermissionsUpdateDTO); + + /** + * 查询管理员详情 + * + * @param id 管理员的唯一标识 + * @param moduleType 模块类型 + * @return 返回管理员详情 + */ + FtbPersonnelsPermissionUserVO adminDetails(String id, Long moduleType); + + /** + * 查询权限列表 + * + * @param page 分页对象 + * @param ftbPermissionInfoDTO 权限信息查询条件 + * @return 返回权限列表的分页结果 + */ + Page permissionList(Page page, FtbPermissionInfoDTO ftbPermissionInfoDTO); + + /** + * 查询指定组织的权限评估师 + * + * @param orgId 组织ID + * @return 返回权限评估师列表 + */ + List queryAuthorityAppraiser(String orgId); + + /** + * 选择组织范围 + * + * @return 返回组织范围信息 + */ + FtbPersonnelsScopeVO selectOrganizationScope(); + + /** + * 选择员工范围 + * + * @return 返回员工范围信息 + */ + FtbPersonnelsScopeVO selectEmployeeRange(); + + /** + * 清除用户权限 + * + * @param userId 用户主键ID + */ + void clearUserPermissions(String userId); + + /** + * 此用户是否是员工,true为是,false为否 + */ + Boolean isThisUserAnEmployee(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPostApplyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPostApplyService.java new file mode 100644 index 0000000..81e6577 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPostApplyService.java @@ -0,0 +1,46 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.apply.FtbPersonnelsApplyCreateDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.po.FtbPersonnelsPostApply; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyWithPerVO; +import jnpf.permission.model.position.PositionInfoNewVO; + +import java.util.List; + +public interface FtbPersonnelsPostApplyService extends IService { + + + PageListVO getList(PersonnelsQueryDTO dto, CultivatePage page); + + String initiateAPromotionApplication(FtbPersonnelsApplyCreateDto createDto); + + FtbPersonnelsApplyWithPerVO viewPromotionApplications(String id, String number); + + void auditPromotionPostApplication(FtbPersonnelsSalaryAuditDto dto); + + PageListVO getListForApp(FtbPersonnelsForAppQueryDTO dto, CultivatePage page); + + List queryPostInfoByOrgAndUserId(String userId, String orgId); + + FtbPersonnelsBubbleCountVO getListCont(String flag); + + /** + * 清除晋升数据 + * @param userIds + */ + void clearPromotionData(List userIds); + + ActionResult initiateAPromotionApplicationForOA(FtbPersonnelsApplyCreateDto createDto); + + + ActionResult auditPromotionPostApplicationWithOA(FtbPersonnelsSalaryAuditDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPromiseConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPromiseConfigService.java new file mode 100644 index 0000000..fdcdb2e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsPromiseConfigService.java @@ -0,0 +1,16 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.po.FtbPersonnelsPromiseConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2024/10/8 +* +*/ +public interface FtbPersonnelsPromiseConfigService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRecruitmentChannelsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRecruitmentChannelsService.java new file mode 100644 index 0000000..f2264e2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRecruitmentChannelsService.java @@ -0,0 +1,21 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.recruitmentchannels.PersonnelsRecruitmentChannelsAddDTO; +import jnpf.model.personnels.po.FtbPersonnelsRecruitmentChannels; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +public interface FtbPersonnelsRecruitmentChannelsService extends IService { + + + /** + * 人员招聘渠道添加 + */ + void add(PersonnelsRecruitmentChannelsAddDTO personnelsRecruitmentChannelsAddDTO); + + /** + * 查询招聘渠道列表 + */ + List queryRecruitmentChannels(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldOptionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldOptionService.java new file mode 100644 index 0000000..f9b4a56 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldOptionService.java @@ -0,0 +1,17 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.req.field.SaveRegistrationFormFieldReq; + +import java.util.List; + +public interface FtbPersonnelsRegistrationFormFieldOptionService extends IService { + /** + * 根据字段id查询选项 + * @param fieldId + * @return + */ + + List queryOptionsByFieldId(String fieldId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldService.java new file mode 100644 index 0000000..8288d3a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormFieldService.java @@ -0,0 +1,92 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormFieldDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.req.field.QueryRegistrationFormFieldListReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormFieldReq; + +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsRegistrationFormFieldService extends IService { + + /** + * 查询列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryRegistrationFormFieldListReq req); + + /** + * 获取详情 + * + * @param id + * @return + */ + FtbPersonnelsRegistrationFormFieldDto getInfo(String id); + + /** + * 新增 + * + * @param req + */ + String insertData(SaveRegistrationFormFieldReq req); + + /** + * 修改 + * + * @param req + */ + void updateData(SaveRegistrationFormFieldReq req); + + /** + * 删除 + * + * @param id 主键 + */ + void deleteData(String id); + + /** + * 修改状态 + * + * @param id 主键 + * @param status 0、启用 1、禁用 + */ + + void switchStatus(String id, Integer status); + + /** + * 修改是否必填 + * @param id 主键 + * @param status 是否必填,0、选填 1、必填 + */ + void switchNeedFill(String id, Integer status); + + /** + * 查询所有字段数量 + * @param typeId 字段类型 + * @return + */ + Long queryAllFieldNumForType(String typeId); + + /** + * 查询已启用字段数量 + * @param typeId 字段类型 + * @return + */ + Long queryEnableFieldNumForType(String typeId); + + FtbPersonnelsRegistrationFormField queryOneField(String field); + + /** + * 切换员工是否可见 + */ + void switchVisible(String id, Integer status); + + List> queryFieldValue(String field, String workType); + + void toggleWhetherYouCanModifyYourProfile(String id, Integer status); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormTypeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormTypeService.java new file mode 100644 index 0000000..5838625 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegistrationFormTypeService.java @@ -0,0 +1,73 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormTypeDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; +import jnpf.model.personnels.req.employment.AddStaffEmploymentApplyReq; +import jnpf.model.personnels.req.field.QueryRegistrationFormTypeListReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormTypeReq; + +import java.util.List; + +public interface FtbPersonnelsRegistrationFormTypeService extends IService { + /** + * 查询列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryRegistrationFormTypeListReq req); + + /** + * 获取详情 + * + * @param id + * @return + */ + FtbPersonnelsRegistrationFormTypeDto getInfo(String id); + + /** + * 新增 + * + * @param req + */ + String insertData(SaveRegistrationFormTypeReq req); + + /** + * 修改 + * + * @param req + */ + void updateData(SaveRegistrationFormTypeReq req); + + /** + * 删除 + * + * @param id 主键 + */ + void deleteData(String id); + + /** + * 修改状态 + * + * @param id 主键 + * @param status 0、启用 1、禁用 + */ + + void switchStatus(String id, Integer status); + + /** + * 修改序号 + * @param id 主键 + * @param sorts 序号 + */ + void updateSorts(String id, Long sorts); + + /** + * 查询所有 + * @return + */ + List listAll(Integer status); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegularManagementService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegularManagementService.java new file mode 100644 index 0000000..48f6839 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRegularManagementService.java @@ -0,0 +1,55 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.po.FtbPersonnelsRegularManagement; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularInfoVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularManagementVO; + +import java.util.List; + +public interface FtbPersonnelsRegularManagementService extends IService { + + + void cancellationOfApplicationForRectification(String id, String taskId); + + void regularizationApproval(FtbPersonnelsSalaryAuditDto auditDto); + + PageListVO pageList(PersonnelsQueryDTO dto, CultivatePage page, Boolean export); + + String applyForRegularization(FtbPersonnelsRegularCreateDTO createDTO); + + FtbPersonnelsRegularInfoVO checkTheDetailsOfRegularizationApproval(String id); + + PageListVO pageForAppList(FtbPersonnelsForAppQueryDTO dto, CultivatePage page); + + FtbPersonnelsBubbleCountVO getListCont(String flag); + + /** + * 清除历史转正数据 + * @param userIds + */ + void clearHistoricalConversionData(List userIds); + + ActionResult applyForRegularizationForOA(FtbPersonnelsRegularCreateDTO createDTO); + + + ActionResult regularizationApprovalForOA(FtbPersonnelsSalaryAuditDto auditDto); + + /** + * 同步转正状态 + */ + void syncRegularStatus(String tenantId); + + /** + * 花名册导入批量转正 + */ + void applyForRegularizationBatch(List createDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationCategoryConfigurationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationCategoryConfigurationService.java new file mode 100644 index 0000000..0e50f60 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationCategoryConfigurationService.java @@ -0,0 +1,16 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.po.FtbPersonnelsResignationCategoryConfiguration; + +/** +* +* +*@Author: peng.hao +*@create: 2024/11/8 +* +*/ +public interface FtbPersonnelsResignationCategoryConfigurationService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationConfigurationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationConfigurationService.java new file mode 100644 index 0000000..49bfd4c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsResignationConfigurationService.java @@ -0,0 +1,35 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.resignation.FtbResignationConfigurationDTO; +import jnpf.model.personnels.po.FtbPersonnelsResignationConfiguration; +import jnpf.model.personnels.vo.resignation.FtbResignationConfigurationVO; + +import java.util.List; + +public interface FtbPersonnelsResignationConfigurationService extends IService { + + + /** + * 保存配置信息 + * + * @param resignationConfigurationDTO 包含辞职配置信息的列表 + */ + void configurationSave(List resignationConfigurationDTO); + + /** + * 获取配置信息列表 + * + * @return 返回辞职配置信息的列表 + */ + PageListVO getList(String keyWords, String resignationTypeId, CultivatePage page); + + /** + * 获取离职原因下拉菜单列表 + * + * @return 返回包含辞职原因的下拉菜单项列表 + */ + List reasonForResignationDropDown(Integer individualApplication); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRewardsPunishmentsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRewardsPunishmentsService.java new file mode 100644 index 0000000..4c21513 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRewardsPunishmentsService.java @@ -0,0 +1,148 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.dto.rewardspunishments.*; +import jnpf.model.personnels.dto.salary.FtbSalaryMetaDataQueryDto; +import jnpf.model.personnels.po.FtbPersonnelsRewardsPunishments; +import jnpf.model.personnels.vo.rewardspunishments.*; +import jnpf.model.personnels.vo.rewardspunishments.FtbEmployeeRewardRecordsVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelSalaryRewardVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentApprovalVO; +import jnpf.model.personnels.vo.rewardspunishments.FtbPersonnelsRewardsPunishmentQueryVO; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import jnpf.model.personnels.vo.rewardspunishments.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Date; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + *

+ * 人事奖惩表 服务类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +public interface FtbPersonnelsRewardsPunishmentsService extends IService { + + /** + * 添加奖惩记录 + * + * @param ftbPersonnelsRewardsPunishmentsAddDTO 奖惩记录添加数据传输对象 + */ + void add(FtbPersonnelsRewardsPunishmentsAddDTO ftbPersonnelsRewardsPunishmentsAddDTO); + + /** + * 删除奖惩记录 + * + * @param id 奖惩记录的唯一标识 + */ + void delete(String id); + + /** + * 更新奖惩记录数据 + * + * @param ftbPersonnelsRewardsPunishmentsUpdateDTO 奖惩记录更新数据传输对象 + */ + void updateData(FtbPersonnelsRewardsPunishmentsUpdateDTO ftbPersonnelsRewardsPunishmentsUpdateDTO); + + /** + * 启用或停用奖惩记录 + * + * @param ftbPersonnelsRewardsPunishmentStartDTO 启用或停用奖惩记录的数据传输对象 + */ + void startAndStop(FtbPersonnelsRewardsPunishmentStartDTO ftbPersonnelsRewardsPunishmentStartDTO); + + /** + * 调整奖惩记录的顺序 + * + * @param ftbPersonnelsRewardsPunishmentExchangeOrderDTO 调整顺序的数据传输对象 + */ + void exchangeOrder(FtbPersonnelsRewardsPunishmentExchangeOrderDTO ftbPersonnelsRewardsPunishmentExchangeOrderDTO); + + /** + * 查询奖惩记录列表 + * + * @param rewardsPunishmentQueryDTO 奖惩记录查询数据传输对象 + * @return 奖惩记录查询结果列表 + */ + List listQuery(FtbPersonnelsRewardsPunishmentQueryDTO rewardsPunishmentQueryDTO); + + /** + * 查询待审批的奖惩记录列表 + * + * @param type 奖惩类型(奖励或惩罚) + * @return 待审批的奖惩记录列表 + */ + List listApproval(Integer type); + + /** + * 查询工资奖励记录列表 + * + * @param ftbPersonnelSalaryRewardDTO 工资奖励记录查询数据传输对象 + * @return 工资奖励记录列表 + */ + List salaryReward(FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO); + + /** + * 查询总工资惩罚记录列表 + * + * @param ftbPersonnelSalaryRewardDTO 工资惩罚记录查询数据传输对象 + * @return 工资惩罚记录列表 + */ + List totalSalaryPenalty(FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO); + + /** + * 根据ID查询审批金额 + * + * @param id 奖惩记录的唯一标识 + * @return 审批金额信息 + */ + FtbPersonnelsRewardsPunishmentApprovalVO listApprovalMoney(String id); + + /** + * @param type 奖惩类型 + * @param typeName 类型名称 + * @return :java.math.BigDecimal 金额 + * @decription 薪酬获取人事奖惩金额 + * @date 2024/11/5 17:27 + * @author AoTeMan + */ + BigDecimal salaryMetaDataQuery(FtbSalaryMetaDataQueryDto dto); + + /** + * 批量查询奖惩 + */ + List salaryBatchMetaDataQuery(FtbSalaryMetaDataQueryDto dto); + + /** + * 查询员工奖惩记录 + * + * @param page 分页对象 + * @param queryDTO 奖惩记录查询数据传输对象 + * @return 员工奖惩记录分页结果 + */ + Page employeeRewardAndPunishmentRecords(Page page, FtbEmployeeRewardRecordsQueryDTO queryDTO); + + Map importData(InputStream inputStream) throws IOException; + + void importDataClick(Integer type, String orgId); + + void saveDataToRedis(List nomarlData, List errorData); + + void deleteUserInfo(String id, Integer type); + + void exportErrData(HttpServletResponse response) throws IOException; + + FtbEmployeeRewardRecordsVO queryDetail(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRosterValidService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRosterValidService.java new file mode 100644 index 0000000..68584c4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRosterValidService.java @@ -0,0 +1,44 @@ +package jnpf.personnels.service; + +import com.alibaba.excel.context.AnalysisContext; +import jnpf.model.personnels.bo.FtbRosterImportHeadRuleBO; +import jnpf.model.personnels.vo.roster.FtbRosterCategoryVO; + +import java.util.Map; + +public interface FtbPersonnelsRosterValidService { + + /** + * 进行表头校验 + * 该方法根据提供的表头信息和规则,生成用于后续数据校验的规则配置 + * + * @param sheetName 工作表名称,用于识别和定位特定的表格 + * @param head 表头信息映射,键是表头的索引,值是表头的文本内容 + * @return 返回一个映射,其中键是表头索引,值是对应的导入规则对象,用于后续的数据校验 + */ + Map headerVerification(String sheetName, Map head); + + /** + * 进行数据校验 + * 根据提供的上下文、表头规则和数据,该方法将数据分为正常数据和错误数据,并进行处理 + * + * @param context 分析上下文,提供了解析过程中的状态和配置信息 + * @param head 表头规则映射,用于指导数据如何校验 + * @param data 待校验的数据映射,键是数据行号,值是该行的数据内容 + * @param normalData 用于存储校验通过的正常数据的对象 + * @param errorData 用于存储校验未通过的错误数据的对象 + */ + void dataValidation(AnalysisContext context, Map head, Map data + , FtbRosterCategoryVO normalData, FtbRosterCategoryVO errorData); + + /** + * 将数据保存到Redis + * 该方法负责将校验后的正常数据和错误数据分别存储到Redis中,以便后续使用或展示 + * + * @param context 分析上下文,可能包含操作上下文或事务信息 + * @param normalData 校验通过的正常数据对象 + * @param errorData 校验未通过的错误数据对象 + */ + void saveDataToRedis(AnalysisContext context, FtbRosterCategoryVO normalData, FtbRosterCategoryVO errorData); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRuleConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRuleConfigService.java new file mode 100644 index 0000000..5868935 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsRuleConfigService.java @@ -0,0 +1,16 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.po.FtbPersonnelsRuleConfig; + +/** +* +* +*@Author: peng.hao +*@create: 2024/10/22 +* +*/ +public interface FtbPersonnelsRuleConfigService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSalaryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSalaryService.java new file mode 100644 index 0000000..67fe9fe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSalaryService.java @@ -0,0 +1,100 @@ +package jnpf.personnels.service; + +import jnpf.model.common.PayRollJsonItem; +import jnpf.model.personnels.dto.salary.FtbPersonnelRosterSalaryHistoryDTO; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryTemporaryStorageCreatDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.salary.FtbPersonnelRosterSalaryVO; +import jnpf.model.personnels.vo.salary.FtbPersonnelsSalaryTemporaryStorageVo; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * @Title: 薪酬对接 + * @Author: peng.hao + * @create: 2024/5/22:14:56 + */ +public interface FtbPersonnelsSalaryService { + /** + * 保存薪酬信息 + * + * @param salaryList 更改list + * @param userId 用户id + * @param date 生效时间 + * @param userInfoWithSalary 用户信息 + * @param updateType 更新类@型:0 重复入职 1-调薪,2-入职 + * @param remark 备注 + * @param operationType 操作标识 1:转正 0:其他 2: 调岗 3 晋升 4 入职 5 调薪 6 调动 + * @param isDelete 1: 作废,0:不作废 默认写写0 + * @param salaryType 调薪方式 1.加薪 2.降薪 + * @param payrollSequenceId 薪资顺序id + */ + void saveTheChangePayInformation(List salaryList, + String userId, + Date date, + UserInfoWithSalary userInfoWithSalary, + String updateType, + String remark, + String operationType, + Integer isDelete, + Integer salaryType, + String payrollSequenceId); + + /** + * 保存薪酬信息 + * + * @param salaryList 更改list + * @param userId 用户id + * @param date 生效时间 + * @param userInfoWithSalary 用户信息 + * @param updateType 更新类@型:0 重复入职 1-调薪,2-入职 + * @param remark 备注 + * @param operationType 操作标识 1:转正 0:其他 2: 调岗 3 晋升 4 入职 5 调薪 6 调动 9 借调 10 借调返岗 + * @param isDelete 1: 作废,0:不作废 默认写写0 + * @param salaryType 调薪方式 1.加薪 2.降薪 + * @param payrollSequenceId 薪资顺序id + */ + String saveTheChangePayInformation(List salaryList, + String userId, + Date date, + UserInfoWithSalary userInfoWithSalary, + String updateType, + String remark, + String operationType, + Integer isDelete, + Integer salaryType, + String payrollSequenceId, String tenantId); + + /** + * 获取薪酬变化历史(额外封装花名册部分信息) + * @return 变化历史信息 + */ + List getSalaryHistoryByUserId(FtbPersonnelRosterSalaryHistoryDTO dto); + + List getTheCurrentPersonSSalary( String userId, + String rankId, + String postId); + List innerGetTheCurrentPersonSSalary(FtbPersonnelsStaffRoster entity, String rankId, String postId) ; + + void regularizationRevoked(String userId); + + void creatSalaryTemporaryStorage(FtbPersonnelsSalaryTemporaryStorageCreatDto creatDto); + + void updateSalaryTemporaryStorage(FtbPersonnelsSalaryTemporaryStorageCreatDto creatDto); + + FtbPersonnelsSalaryTemporaryStorageVo querySalaryTemporaryStorage(String uerId, String configType); + + String whetherToVoidWages(String userId, List beforeItem, String flag, Date changeDate); + + void removeUserAllSalary(String userId); + + void voidSalary(String userId, String date, String salaryRecordId); + + String lochCheck(String userId, Date lochStartTime, Date lochEndTime); + + List> querySalaryIntersectionWithOtherBusiness(String userId, Date startData, Date endData); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSecondmentManagementService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSecondmentManagementService.java new file mode 100644 index 0000000..c3a7cb4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsSecondmentManagementService.java @@ -0,0 +1,113 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.attendance.vo.DailyApprovalVo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.secondment.FtbHandleSecondmentDTO; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentInfoVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/11 +* +*/ +public interface FtbPersonnelsSecondmentManagementService extends IService { + + + /** + * 分页查询人员借调列表 + * @param dto 查询条件封装对象 + * @param page 分页参数对象 + * @return 人员借调分页列表数据 + */ + PageListVO pageList(PersonnelsQueryDTO dto, CultivatePage page); + + /** + * 根据ID获取人员借调详情信息 + * @param id 人员借调记录唯一标识 + * @return 人员借调详细信息对象 + */ + FtbPersonnelsSecondmentInfoVO details(String id); + + /** + * 处理人员借调业务操作 + * + * @param ftbHandleSecondmentDTO 借调处理数据传输对象 + * @return + */ + String handleSecondment(FtbHandleSecondmentDTO ftbHandleSecondmentDTO); + + /** + * 根据ID删除人员借调记录 + * @param id 人员借调记录唯一标识 + */ + void delete(String id); + + /** + * 处理OA系统的人员借调业务操作 + * @param ftbHandleSecondmentDTO 借调处理数据传输对象 + */ + void handleSecondmentForOA(FtbHandleSecondmentDTO ftbHandleSecondmentDTO); + + /** + * 处理人员借调审批流程 + * @param ftbPersonnelsAuditDto 人员审批数据传输对象 + */ + void secondmentApproval(FtbPersonnelsAuditDto ftbPersonnelsAuditDto); + + /** + * 处理人员借调撤回流程 + * @param ftbPersonnelsAuditDto 人员审批数据传输对象 + */ + void secondmentWithdrawal(FtbPersonnelsAuditDto ftbPersonnelsAuditDto); + + /** + * 提前结束人员借调 + * @param map 包含借调结束相关信息的参数映射 + */ + void earlyEnd(Map map); + + /** + * 延长人员借调时间 + * @param map 包含借调延期相关信息的参数映射 + */ + void delayTime(Map map); + + Boolean checkIsInTheProcess(String userId, Date startTime, Date endTime); + + /** + * 根据人员确认是否存在借调记录并根据离职时间结束 + */ + void endByUserConfirm(String userId, Date leaveTime); + /** + * 校验是否有借调记录 + */ + int checkWhetherThereIsASecondmentRecord(String userId, Date leaveTime); + + List getSecondmentRecord(String userId, String startLeaveTime, String endLeaveTime); + + List getSecondmentRecordBath(FtbSecondMentQueryDTO dto); + /** + * 定时任务检测当借调结束后薪资生成 借调返岗 + */ + void checkSecondmentEnd(String tenantId); + List getIdList(String userId, Date leaveTime); + + List salarySecondmentCrossover(String userId, Date leaveTime); + + List queryListApproval(FtbSecondMentQueryDTO dto); + + void cancel(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsShortchainService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsShortchainService.java new file mode 100644 index 0000000..11aec40 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsShortchainService.java @@ -0,0 +1,36 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.po.FtbPersonnelsShortchain; + +public interface FtbPersonnelsShortchainService extends IService { + + + /** + * 短链地址生成 + * + * @param longChainAddress 长链地址 + * @return {@link String} + */ + String generateShortChain(String longChainAddress); + + + /** + * 保存短链 + * + * @param shortChainAddress 短链地址 + * @param longChainAddress 长链地址 + * @param requestUrl 请求URL + */ + void saveShortChain(String shortChainAddress, String longChainAddress, String requestUrl); + + + /** + * 执行重定向到指定的URL + * + * @param url 要重定向的URL字符串 + * @return 返回重定向的目标URL + */ + String redirect(String url); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffArchivesHistoryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffArchivesHistoryService.java new file mode 100644 index 0000000..260c64e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffArchivesHistoryService.java @@ -0,0 +1,24 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffArchivesHistory; +import jnpf.model.personnels.vo.roster.FtbPersonnelsStaffArchivesHistoryVo; + +import java.util.List; + +public interface FtbPersonnelsStaffArchivesHistoryService extends IService { + + + /** + * 获取详情 + * + * @param id + * @return + */ + FtbPersonnelsStaffArchivesHistoryVo getInfo(String id); + + void deleteForUserIds(List userIds); + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffEmploymentApplyService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffEmploymentApplyService.java new file mode 100644 index 0000000..8faed31 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffEmploymentApplyService.java @@ -0,0 +1,163 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsEmployApplyDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.req.employment.*; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.employment.AddStaffEmploymentApplyExcelERRVO; +import jnpf.model.personnels.vo.employment.AddStaffEmploymentApplyExcelVO; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsStaffEmploymentApplyService extends IService { + + /** + * 查询列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryStaffEmploymentApplyListReq req); + + /** + * 获取详情 + * + * @param id + * @return + */ + FtbPersonnelsStaffEmploymentApplyDto getInfo(String id); + + /** + * 新增 + * + * @param req + */ + String insertData(AddStaffEmploymentApplyReq req); + + /** + * 修改 + * + * @param req + */ + void updateData(SavePaperReq req); + + /** + * 删除 + * + * @param id 主键 + */ + void deleteData(String id); + + /** + * 批量删除 根据id + * + * @param ids 主键集合 + */ + void deleteBatchData(List ids); + + /** + * 修改用户确认到岗状态 + * + * @param id + */ + void updateConfirmArrivalStatus(String id); + + FtbPersonnelsStaffEmploymentApply queryAndCheckStaffEmploymentApply(String id); + + /** + * APP 查询入职关联 + * @param req + * @return + */ + PageInfo getAppPageList(QueryAppStaffEmploymentApplyListReq req,String loginUserId); + + /** + * 审批 + * @param id + * @param dto + */ + void approval( EmploymentApplyCheckDto dto); + + /** + * 重新审批 + * @param id + * @param dto + */ + void reApproval(String id, AddStaffEmploymentApplyReq dto); + + + void saveInRedis(List nomarlData, List errorData); + + void importDataClick(); + + Map importData(InputStream inputStream); + + void exportErrData(HttpServletResponse response); + + void exportDate(BatchByPrimaryIdReq req, HttpServletResponse response); + + PageInfo getWebMyCheckList(MyWebEmploymentApplyCheckListReq req); + + String checkPhoneStatus(String phone); + + void saveShortUrl(String id, String shortUrl); + + + void sendPhoneMsg(String phone); + + /** + * 检测是否需要填写入职登记表 + * + * @param phone + * @return true 需要填写 ,false 不需要填写 + */ + Boolean checkFillRegistrationForm(String phone); + + /** + * 查询邀请码 + * @return + */ + String queryCode(); + + /** + * 修改员工逾期到岗状态 + */ + void updateEmploymentApplyStatus(); + + void deleteByPhone(String phone); + + /** + * 校验是否有信息绑定 + * @param positionId 岗位 + * @param positionGradesId 职等 + */ + void verifyUserBound(String positionId, String positionGradesId); + + FtbPersonnelsBubbleCountVO getListCont(String flag); + + void exportDateNew(QueryStaffEmploymentApplyListReq req, HttpServletResponse response) throws IOException; + + ActionResult handleJoinJobForOA(AddStaffEmploymentApplyReq req); + + ActionResult approvalForOA(EmploymentApplyCheckDto dto); + + /** + * 重新审批 + * @param id + * @param dto + */ + ActionResult reApprovalForOA(String id, AddStaffEmploymentApplyReq dto); + + PageListVO onboardingList(QueryStaffEmploymentApplyListReq req); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffGrowthLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffGrowthLogService.java new file mode 100644 index 0000000..5682fdc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffGrowthLogService.java @@ -0,0 +1,38 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.growth.FtbPersonnelsStaffGrowthLogDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffGrowthLog; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.Date; +import java.util.List; + +public interface FtbPersonnelsStaffGrowthLogService extends IService { + + /** + * 查询员工成长 + * + * @param userId + * @return + */ + List queryAll(String userId); + + /** + * 增加员工成长 + * + * @param addGrowthLogDto + */ + void addGrowthLog(AddGrowthLogDto addGrowthLogDto); + + /** + * 撤销员工成长 + * @param userId + */ + void revocationGrowthLog(String userId, Date startTime); + + /** + * 批量增加员工成长 + */ + void addGrowthLogBatch(List addGrowthLogDtos); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffHomePageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffHomePageService.java new file mode 100644 index 0000000..6029756 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffHomePageService.java @@ -0,0 +1,29 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.vo.roster.FtbHomePageRewardVO; +import jnpf.model.personnels.vo.roster.FtbPersonnelsChangeInfoVO; + +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsStaffHomePageService { + /** + * 查询个人承诺 + */ + String checkPersonalCommitments(String id); + + /** + * 员工主页-奖励记录 + */ + List getRewardList(String id); + + /** + * 员工主页-乐捐记录 + */ + List donationRecords(String id); + + /** + * 批量查询人事异动信息 + */ + Map getBatchPersonnelChangeInfo(List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRegistrationFormDataService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRegistrationFormDataService.java new file mode 100644 index 0000000..096905d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRegistrationFormDataService.java @@ -0,0 +1,219 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.dto.staff.field.ExportFormTypeDto; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.dto.staff.field.SubFormFieldDto; +import jnpf.model.personnels.dto.staff.registerform.CheckRegisterFormFillDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.req.registerform.SaveFormDataReq; + +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsStaffRegistrationFormDataService extends IService { + + + /** + * 同步数据 + * + * @param dataMap + * @param phone + */ + void syncData(Map dataMap, String phone, String rosterId); + + void importSyncData(Map dataMap, String phone, String rosterId, Map allFormFieldListMap); + + /** + * 查询入职登记表 + * + * @param phone 手机号 + * @return + */ + List queryRegistrationForm(String phone, Boolean registerForm); + + /** + * 查询入职登记表 + * + * @return + */ + List queryRegistrationForm(); + + /** + * 检测入职登记表是否已经填写 + * + * @param phone + * @return + */ + CheckRegisterFormFillDto checkRegistrationFormFill(String phone); + + /** + * 根据用户ID查询档案信息 + * + * @param userId 用户ID + * @return + */ + List queryArchivalForm(String userId); + + /** + * 根据用户ID查询档案信息 + * + * @param userId 用户ID + * @return + */ + List queryArchivalForm(String userId, String tenantId); + + List queryAndFillFormFieldValue(String phone, String userId, String rosterId, Boolean registerForm); + + /** + * 保存档案信息 + * + * @param saveFormDataReq + */ + void saveArchives(SaveFormDataReq saveFormDataReq); + + /** + * 根据手机号和花名册id查询所有档案信息 + * @param phone 手机号 + * @param rosterId 花名册id + * @return + */ + List queryFormFieldValueList(String phone, String rosterId); + /** + * 根据手机号 + * + * @param phones 手机号 + * @return + */ + Map> queryFormFieldValueListWithPhone(List phones); + /** + * 根据花名册id查询 用户的所有字段信息 + * @param rosterIds 花名册id集合 + * @return + */ + List queryFormFieldValueForRosterIds(List rosterIds); + + /** + * 查询用户的指定字段 + * @param rosterIds 花名册用户id 集合 + * @param formFieldIds 字段名称集合 + * @return + */ + List queryListForRosterIdAndField(List rosterIds, List formFieldIds); + + /** + * 更新字段 + * + * @param phone 手机号 + * @param formFieldId 字段名称 + * @param value 字段值 + */ + void updateForOneField(String phone, String formFieldId, String value); + /** + * 修改用户的一个字段数据 + * @param rosterId 花名册id + * @param phone 手机号 + * @param formFieldId 字段名称 + * @param value 字段值 + */ + void updateForOneField(String rosterId, String phone, String formFieldId, String value); + + String queryTenantIdForCode(String code); + + /** + * 查询动态表中的一个字段的值 + * + * @param phone 手机号 + * @param rosterId 花名册id + * @param fieldName 字段名称 + * @return + */ + String queryOneFieldValue(String phone, String rosterId, String fieldName); + /** + * 根据手机号查询字段的值 + * @param phone 手机号 + * @param fieldName 字段名称 + * @return + */ + String queryOneFieldValueForPhone(String phone, String fieldName); + /** + * 查询花名册中的一个字段 + * @param phone 手机号 + * @param rosterId 花名册id + * @param fieldName 字段名称 + * @return + */ + FtbPersonnelsStaffRegistrationFormData queryOneField(String phone, String rosterId, String fieldName); + + /** + * 保存入职登记表信息 + * + * @param tenantId 租户id + * @param fieldValueList + * @param moduleId + */ + void saveRegistrationForm(String tenantId, List fieldValueList, Integer flag, String registerImg, String moduleId); + + /** + * 根据手机号查询所有已经填写的字段数据 + * + * @param phone + * @return + */ + Map queryAllFormData(String phone, String rosterId); + + Map> querAllFormDataForRosterIds(List rosterIds); + + /** + * 查询所有导出字段 + * + * @return + */ + public List queryAllFields(); + + /** + * 给入职登记表中没有 绑定rosterId的绑上花名册ID + * + * @param phone + * @param id + */ + void addRosterIdToFormData(String phone, String id); + + /** + * 查询有健康证 过期日期的数据 + */ + List queryHasHealthExpire(); + + + /** + * 删除数据 + * + * @param rosterId 花名册id + */ + void deleteByRosterId(String rosterId); + + /** + * 根据字段id查询字段信息 + * + * @param rosterId 花名册id + * @param fieldName 字段id + * @return + */ + FtbPersonnelsStaffRegistrationFormData queryOneFieldForFieldId(String rosterId, String fieldName); + + void importUpdateData(Map formDataMap, String phone, String rosterId, Map allFormFieldListMap,Map oldFormDataMap); + + /** + * 批量跟新数据 + * @param formDataList + */ + void batchUpdateData(List formDataList); + + void saveArchivesWithPhone(SaveFormDataReq req); + + /** + * 检查实际入职日期通过,返回true则通过,false及提示语则不通过 + */ + Boolean checkActualJoiningDatePassed(String phone, String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterImportService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterImportService.java new file mode 100644 index 0000000..ddd999c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterImportService.java @@ -0,0 +1,65 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.vo.roster.FtbRosterImportFormFieldsConfigVO; +import jnpf.model.personnels.vo.roster.FtbRosterImportVO; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public interface FtbPersonnelsStaffRosterImportService { + + /** + * 执行名单导入 + * + * @param inputStream 包含要导入学员数据的输入流 + * @return FtbRosterImportVO 返回一个包含导入结果的对象,包括成功或失败的详情 + * @throws IOException 如果读取输入流过程中出现问题 + */ + FtbRosterImportVO rosterImport(InputStream inputStream) throws IOException; + + /** + * 异常数据导出 + * + * @param id 导出操作的标识,用于确定要导出的数据范围或类型 + * @throws IOException 如果文件写入过程中出现问题 + */ + void exceptionDataExport(String id) throws IOException; + + /** + * 正常数据导入 + * + * @param id 导入操作的标识,用于确定数据来源或导入的数据范围 + */ + void normalDataImport(String id); + + /** + * 获取表单字段配置信息列表 + *

+ * 该方法用于获取一个包含所有表单字段配置信息的列表,这些配置信息用于指导如何导入花名册数据 + * 它返回的是一个List对象,其中每个元素都是一个FtbRosterImportFormFieldsConfigVO实例,代表一个表单字段的配置信息 + * + * @return List 包含表单字段配置信息的列表 + */ + List formFieldsConfig(Integer type); + + /** + * 下载新的花名册导入模板 + * 此方法用于根据用户信息和表单字段生成并下载一个新的花名册导入模板 + * 它允许用户根据需要自定义导入模板,从而简化数据导入过程 + * + * @param formFields 表单字段列表,指定需要包含在模板中的字段 + * 这允许模板根据用户的具体需求进行定制 + */ + void rosterImportTemplateDownloadNew(List formFields) throws IOException; + + /** + * 从输入流中导入花名册信息 + * 此方法用于解析输入流中的数据,并将其转换为花名册导入对象(FtbRosterImportVO) + * 主要用途是处理文件上传或网络传输来的花名册数据,便于进一步的数据处理或存储 + * + * @param inputStream 输入流,包含花名册数据的二进制流 + * @return FtbRosterImportVO 转换后的花名册导入对象,包含解析出的花名册信息 + */ + FtbRosterImportVO rosterImportNew(InputStream inputStream); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterSchemeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterSchemeService.java new file mode 100644 index 0000000..287a4fe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterSchemeService.java @@ -0,0 +1,22 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.scheme.FtbPersonAddSchemeDTO; +import jnpf.model.personnels.dto.scheme.FtbPersonEditSchemeDTO; +import jnpf.model.personnels.vo.scheme.FtbPersonSchemeDetailsVO; +import jnpf.model.personnels.vo.scheme.FtbPersonSchemeListVO; + +import javax.validation.Valid; +import java.util.List; + +public interface FtbPersonnelsStaffRosterSchemeService { + + void addScheme(@Valid FtbPersonAddSchemeDTO schemeDTO); + + void deleteScheme(String id); + + void updateScheme(@Valid FtbPersonEditSchemeDTO schemeDTO); + + FtbPersonSchemeDetailsVO detailScheme(String id); + + List getSchemeList(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterService.java new file mode 100644 index 0000000..a172189 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffRosterService.java @@ -0,0 +1,373 @@ +package jnpf.personnels.service; + +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionWithPersonelVO; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.roster.QueryCompanyAgeDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.*; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.roster.AppStaffRosterListReq; +import jnpf.model.personnels.req.roster.ConfirmOnDutyReq; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.req.roster.StaffRosterReq; +import jnpf.model.personnels.vo.roster.FtbRosterInsertNomalVO; +import jnpf.model.personnels.vo.roster.ImportResultVO; +import jnpf.permission.dto.QueryUserListDTO; +import jnpf.permission.model.user.BaseUserInfoVo; +import jnpf.permission.model.user.UserQueryDto; +import jnpf.permission.vo.user.UserListMatchVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsStaffRosterService extends IService { + + /** + * 查询列表 + * + * @param req + * @return + */ + PageInfo getPageList(StaffRosterListReq req, Boolean isExport); + + /** + * 获取详情 + * + * @param id + * @return + */ + FtbPersonnelsStaffRosterDto getInfo(String id); + + /** + * 批量删除 根据id + * + * @param ids 主键集合 + */ + void deleteBatchData(List ids); + + /** + * 确认到岗 + * + * @param req + * @return + */ + String confirmOnDuty(ConfirmOnDutyReq req); + + /** + * 查询员工主页信息 + * + * @param userId + * @return + */ + StaffHomeDto queryWorkerHomeDetail(String userId); + + /** + * 根据手机号查询花名册信息 + * + * @param phone + * @return + */ + FtbPersonnelsStaffRoster queryRosterInfoByPhone(String phone); + + List queryRosterInfoByPhones(List phones); + + /** + * 根据用户ID查询花名册信息 + * + * @param userId + * @return + */ + FtbPersonnelsStaffRoster queryRosterInfoByUserId(String userId); + + /** + * 统计员工数量 + * + * @return + */ + WorkerStatisticsDto statisticsWorkerNumber(); + + PageInfo getAppPageList(AppStaffRosterListReq req); + + /** + * 修改转正状态 记录薪酬 和 成长日志 + * + * @param userId + * @param dto + * @param money + * @param tenantId + */ + void innerChangeRegular(String userId, StaffRegularDto dto, BigDecimal money, String tenantId); + + /** + * 删除用户重置花名册入职次数 + */ + void resetWorkerNumber(List userId); + + /** + * 撤销转正 + * + * @param userId + */ + void cancelRegular(String userId); + /** + * 修改转正前的状态 + * + * @param userId + */ + void recordPreRegularStatus(String userId); + + /** + * 撤销离职 + * + * @param userId + */ + String canceldepart(String userId); + + /** + * 修改为离职 + * + * @param userId + * @param dtoList + * @param remarks + * @param resignationDate 离职日期 + */ + Boolean innerChangeDepart(String userId, StaffDepartDto dtoList, String remarks, Date resignationDate,String tenantId); + + /** + * 晋升记录 员工成长 和 薪酬 + * + * @param userId + * @param dto + * @param money + */ + void innerPromotion(String userId, StaffPromotionDto dto, BigDecimal money); + + /** + * 修改 待离职 状态 + * + * @param userId + */ + void innerChangeWaitDepart(String userId,String taskInfoId,Integer version); + + /** + * 查询员工ID + * + * @param userId + */ + String querySystemWorkerId(String userId); + + /** + * 检测手机号是否存在花名册中 + * + * @param phone + * @return true存在 false 不存在 + */ + Boolean checkPhoneExistForRoster(String phone); + + /** + * 导入花名册 + * + * @param list + * @return + */ + @Deprecated(since = "代码重构后废弃") + List addImportRosters(List list); + + /** + * 通过ids查询花名册信息 + * + * @param ids + * @return + */ + List queryRosterInfoByIds(List ids, List formFieldOptionIds,String schemeId); + + /** + * 检测用户是否可以删除 + * + * @param ids + */ + CanDeleteMsg checkBatchDelete(List ids); + + /** + * 试岗驳回记录员工成长接口 + * + * @param userId + * @param dto + */ + void innerTravelFail(String userId, StaffTravlFailDto dto); + + void checkReportsTo(String currReportsTo) ; + + /** + * 检测用户是不是档案管理员 + * + * @param loginUserId + * @return true 是, false 否 + */ + Boolean checkMyIsArchiveManager(String loginUserId); + + Boolean checkCurrLoginUserArchiveManager(); + + Integer checksItInTheReviewProcess(String id); + + /** + * 修改员公龄 + */ + void updateCompanyAge(); + + /** + * 绑定手机号 + * + * @param userId + * @param phone + */ + Boolean bindPhone(String userId, String phone); + + /** + * 根据用户ID,批量查询员工ID + * + * @param userIds 用户ID列表 + * @return 只返回: 用户ID 花名册ID 员工ID + */ + List queryWorkerIdByUserIds(List userIds); + + /** + * 根据用户ID,批量查询员工ID + * + * @param userIds 用户ID列表 + * @return 只返回: 用户ID 花名册ID 员工ID + */ + List queryUerIdByWorkIds(List userIds); + List queryUserInfoByWorkerId(List workerIds); + + List queryCompanyAge(List userIds); + + List queryAllUserForOrgAndStatus(QueryUserListDTO dto); + + List queryRosterInfoByWorkerId(String workerNo); + /** + * 根据员工系统id查询员工 + * @param systemId 系统id + * @return 员工 + */ + List queryRosterInfoBySystemId(String systemId); + + FtbCultivatePromotionWithPersonelVO queryTrainData(String userId, String postId); + + List queryNoSubmitForm(); + + List queryHealthExpire(Long days, Long months); + + void clearAuthUserList(); + + ActionResult> getPartUserInfoPage(UserQueryDto queryDto); + + + /** + * 登出 + * + * @param tenantCode 租户编码 + * @param userId 用户id + */ + void logout(String tenantCode, String userId); + + /** + * 发送入职登记表邀请消息 + * + * @param userId 目标用户 + * @return 执行位置 + */ + Boolean sendInvitationInformation(@PathVariable("userId") String userId); + + void updateBaseUserInfo(String userId, EditUserBaseInfoDto dto); + + PageInfo postWithSalary(StaffRosterListReq req); + + + List postWithSalaryNoPage(StaffRosterListReq req); + + List getPersonnelUserList(StaffRosterListReq req); + + /** + * 匹配出, user中存在的用户(包含已离职的用户),和已经不存在的用户, + * + * @param userIds 目标匹配用户集合 + * @return 匹配结果, 两个list + */ + UserListMatchVO getUserListByMatch(List userIds); + + List queryDepartUser(List userIds); + + List queryUserBaseInfoAndOrgAndPosAndRank(List userIds, String tenantId); + + ShopManagerUserDto queryShopManagerUser(List userIds); + + /** + * 每天检查员工是否试用快结束并发送消息 + * + * @param tenantId + * @return + */ + void timingAlertTrialJob(String tenantId); + + + void timingUpdateTrialJob(String tenantId); + + /** + * 查询是否有试用期 + * + * @param registrationFormDataService + * @param rosterId + * @return + */ + default ProbationPeriodDto queryProbationPeriod(FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService, String rosterId) { + //查询是否有试用期 + FtbPersonnelsStaffRegistrationFormData probationPeriod = registrationFormDataService.queryOneFieldForFieldId(rosterId, "probationPeriod"); + if (null == probationPeriod) { + return null; + } + if (StringUtils.isNotEmpty(probationPeriod.getValue()) && probationPeriod.getValue().startsWith("{")) { + return JSONUtil.toBean(probationPeriod.getValue(), ProbationPeriodDto.class); + } + return null; + } + + void timingCheckContractOverTime(String tenantId); + + + /** + * 查询包含岗位状态的用户信息 + * + * @param userId 用户iid + */ + BaseUserInfoVo getUserMoreInfoByUserId(String userId); + + void updateSignSeparation(String userId, Integer status); + + + /** + * 查询是否能够修改用户的真实姓名 + * @return true 可以修改,false 不可以修改 + */ + Boolean canUpdateRealName(); + + Map getInfoForUserIdOneField(String field, String userIds); + + void updateWorkerAge(); + + List queryWithUserIds(StaffRosterListReq req); + + List queryWithUserIdsPost(StaffRosterReq req); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffSalaryChangeLogService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffSalaryChangeLogService.java new file mode 100644 index 0000000..57e5e8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffSalaryChangeLogService.java @@ -0,0 +1,27 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.staff.salarylog.AddSalaryChangeLogDto; +import jnpf.model.personnels.dto.staff.salarylog.FtbPersonnelsStaffSalaryChangeLogDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffSalaryChangeLog; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.List; + +public interface FtbPersonnelsStaffSalaryChangeLogService extends IService { + + /** + * 查询员工薪酬编号列表 + * + * @param userId + * @return + */ + List queryAll(String userId); + + /** + * 添加员工薪酬变化日志 + * + * @param dto + */ + + void addSalaryChangeLog(AddSalaryChangeLogDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionHandoverService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionHandoverService.java new file mode 100644 index 0000000..3e16018 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionHandoverService.java @@ -0,0 +1,9 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPositionHandover; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbPersonnelsStaffTransferPositionHandoverService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionService.java new file mode 100644 index 0000000..da1216b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsStaffTransferPositionService.java @@ -0,0 +1,105 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.github.pagehelper.PageInfo; +import jnpf.base.ActionResult; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionCountDto; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPosition; +import jnpf.model.personnels.req.employment.EmploymentApplyCheckDto; +import jnpf.model.personnels.req.transfer.AppQueryTransferListReq; +import jnpf.model.personnels.req.transfer.MyWebTransferCheckListReq; +import jnpf.model.personnels.req.transfer.QueryTransferListReq; +import jnpf.model.personnels.req.transfer.SaveTransferReq; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; + +import java.util.List; + +public interface FtbPersonnelsStaffTransferPositionService extends IService { + /** + * 查询列表 + * + * @param req + * @return + */ + PageInfo getPageList(QueryTransferListReq req); + + /** + * 获取详情 + * + * @param id + * @return + */ + TransferPositionDto getInfo(String id); + + /** + * 新增 + * + * @param req + */ + String insertData(SaveTransferReq req); + + /** + * 修改 + * + * @param req + */ + void updateData(SavePaperReq req); + + /** + * 删除 + * + * @param id 主键 + */ + void deleteData(String id); + + /** + * 审批 + * + * @param dto + */ + void approval( EmploymentApplyCheckDto dto); + + /** + * 重新审批 + * + * @param id + * @param dto + */ + void reApproval(String id, SaveTransferReq dto); + + PageInfo getAppPageList(AppQueryTransferListReq req); + + /** + * web 我的调岗审批列表 + * + * @param req + * @return + */ + PageInfo pageQueryMyCheckList(MyWebTransferCheckListReq req); + + /** + * 我的审批统计 app + * @return + */ + TransferPositionCountDto queryAppMyCheckCount(); + + /** + * 我的抄送统计 app + * @return + */ + TransferPositionCountDto queryAppMySendCount(); + + FtbPersonnelsBubbleCountVO getListCont(String flag); + + /** + * 清除调岗数据 + * @param userIds + */ + void clearJobTransferData(List userIds); + + ActionResult dealTransferForOA(SaveTransferReq req); + + ActionResult approvalForOA(EmploymentApplyCheckDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTransferManageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTransferManageService.java new file mode 100644 index 0000000..827ba41 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTransferManageService.java @@ -0,0 +1,129 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.model.personnels.po.FtbPersonnelsTransferManage; +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.personnels.req.transfer.FtbHandleTransferDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferOaDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferQueryDTO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferDetailsVO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferPageVO; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsTransferManageService extends IService { + + /** + * 获取处理转让管理列表 + * + * @param page 分页对象,包含分页信息如当前页码、页面大小等 + * @param ftbHandleTransferQueryDTO 查询条件对象,包含查询转让管理列表的条件 + * @return 返回一个分页对象,其中包含符合查询条件的转让管理信息列表 + */ + Page transferManageList(Page page, FtbHandleTransferQueryDTO ftbHandleTransferQueryDTO); + + /** + * 查询转账记录列表(适用于App端) + * 该方法用于获取经过特定用户处理的转账记录页面列表,主要供App端使用 + * + * @param page 分页对象,包含每页记录数、当前页码等信息 + * @param userName 用户名,用于筛选处理人与此用户相关的转账记录 + * @return 返回一个分页对象,其中包含符合筛选条件的转账记录列表 + */ + Page transferListApp(Page page, String userName); + + /** + * 查询我的转账申请记录 + * + * 该方法用于获取当前用户发起的转账申请记录,通过分页形式返回 + * 主要解决了用户需要查看自己历史转账申请的需求 + * + * @param page 分页对象,包含查询条件和分页参数,如当前页码、每页大小等 + * @return 返回一个分页对象,包含查询到的转账申请记录列表和分页信息 + */ + Page myTransferApp(Page page); + + /** + * 处理转账操作 + * 该方法负责处理资金从一个账户转移到另一个账户的请求 + * 它不返回任何值,但可能会抛出异常或记录错误信息,如果转账无法完成 + * + * @param ftbHandleTransferDTO 包含了转账所需的所有信息,如转出账户ID、转入账户ID和转账金额等 + */ + String handleTransfer(FtbHandleTransferDTO ftbHandleTransferDTO); + + /** + * 处理转账OA操作 + * 该方法接收一个包含转账相关信息的数据对象,并执行相应的业务逻辑处理 + * + * @param ftbHandleTransferDTO 包含转账所需信息的数据传输对象 + */ + ActionResult handleTransferOa(FtbHandleTransferDTO ftbHandleTransferDTO); + + /** + * 根据指定的ID删除资源 + * 此方法用于从数据源中删除与给定ID关联的资源它通常用于数据管理, + * 允许通过唯一的标识符来移除不再需要或已损坏的数据项 + * + * @param id 要删除的资源的唯一标识符 + */ + void delete(String id); + + /** + * 审批OA流程 + * 本方法用于处理OA系统中的审批流程,根据传入的DTO对象进行相应的审批操作 + * + * @param dto 包含处理OA审批所需的信息,包括但不限于审批人、审批意见等 + */ + void approvalForOA(FtbHandleTransferOaDTO dto); + + /** + * 根据ID获取转让详情 + * + * @param id 转让详情的唯一标识 + * @return 返回转让详情的对象,如果ID不存在,则返回null + */ + FtbHandleTransferDetailsVO transferDetails(String id); + + /** + * 计划任务中执行效果转移的操作 + * 本方法主要用于在计划任务中处理效果的转移逻辑,确保在指定的时间或条件下, + * 将某些效果从一个实体转移到另一个实体 + * + * @param tenantId 租户ID,用于标识操作的租户,以便在多租户环境中正确执行效果转移 + */ + void transferEffectScheduledTask(String tenantId); + + + /** + * 查询字段值 + *

+ * 该方法用于查询并返回一个包含字段及其对应值的列表每个字段值都以键值对的形式存在, + * 其中键代表字段名,值代表字段的内容该方法不接受任何参数,使用时不需要提供额外的输入 + * + * @return 字段值列表,每个字段值以键值对形式存在 + */ + List> queryFieldValue(); + + /** + * 根据指定ID撤销转账记录 + * 此方法用于在系统中撤销一个已存在的转账记录它通常在需要取消或回滚 + * 某笔转账操作时被调用 + * + * @param id 转账记录的唯一标识符此ID用于定位需要撤销的转账记录 + */ + void revokeTheTransferRecord(String id); + + /** + * 离职清除调动数据 + */ + void clearTransferData(List userId); + + /** + * 借调校验时间交叉 + */ + List transferCheckTimeCrossing(String userId,Date startTime,Date endTime); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTrialService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTrialService.java new file mode 100644 index 0000000..882cbe3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTrialService.java @@ -0,0 +1,21 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.roster.FtbPersonnelsTrialDTO; +import jnpf.model.personnels.dto.roster.FtbjobTrialRejectedDTO; + +public interface FtbPersonnelsTrialService { + + /** + * 处理通过试用期的员工 + * + * @param ftbPersonnelsTrialDTO 包含通过试用期的员工信息的数据传输对象 + */ + void passedTheTrialJob(FtbPersonnelsTrialDTO ftbPersonnelsTrialDTO); + + /** + * 处理未通过试用期的员工 + * + * @param ftbjobTrialRejectedDTO 包含未通过试用期的员工信息的数据传输对象 + */ + void jobTrialRejected(FtbjobTrialRejectedDTO ftbjobTrialRejectedDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverAnalysisService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverAnalysisService.java new file mode 100644 index 0000000..32765c9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverAnalysisService.java @@ -0,0 +1,196 @@ +package jnpf.personnels.service; + +import jnpf.base.vo.PageListVO; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.vo.analysis.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * @Author: peng.hao + * @create: 2024/10/14 + */ +public interface FtbPersonnelsTurnoverAnalysisService { + /** + * 获取离职人员按月分布的信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含按月分布的离职人员信息 + */ + FtbPersonnelsTurnoverWithAnalysisInfoVO getLeavePeopleMonth(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取按月分布的离职人员性别比例 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含按月分布的离职人员性别比例信息 + */ + FtbPersonnelsTurnoverPeopleSexVO getLeavePeopleSexMonth(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取首月离职率信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含首月离职率信息 + */ + FtbPersonnelsTurnoverFirstMonthLeaveRateVO getFirstMonthLeaveRate(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取首周离职率信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含首周离职率信息 + */ + FtbPersonnelsTurnoverFirstWeekLeaveRateVO getFirstWeekLeaveRate(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取首年离职率信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含首年离职率信息 + */ + FtbPersonnelsTurnoverFirstYearLeaveRateVO getFirstYearLeaveRate(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取新员工离职率信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含新员工离职率信息 + */ + FtbPersonnelsTurnoverLeaveRateVO getNewEmployeeLeaveRate(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取离职原因分布信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含离职原因分布信息 + */ + List getLeaveReasonDistribution(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取离职人员岗位比例信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含离职人员岗位比例信息 + */ + List getLeavePeoplePostRatio(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取离职人员年龄分布信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含离职人员年龄分布信息 + */ + List getLeavePeopleAgeDistribution(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取离职人员年龄比例信息 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回一个数据对象,包含离职人员年龄比例信息 + */ + List getLeavePeopleAgeRatio(FtbPersonnlesAnalysisDTO dto); + + /** + * 离职率情况 + */ + FtbPersonnelsTurnoverRateVO ftbPersonnelsTurnoverAnalysisService(FtbPersonnlesAnalysisDTO dto); + + /** + * 离职率对比 + */ + FtbPersonnelsTurnoverRateComparisonVO getLeaveRateComparison(FtbPersonnlesAnalysisDTO dto); + + /** + * 获取离职率明细 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回离职率明细列表 + */ + PageListVO getLeaveRateDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 新员工离职比例明细 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回新员工离职比例明细 PageListVO + */ + PageListVO getNewEmployeeLeaveRateDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 新员工离职比例明细导出 + * + * @param dto 分析参数对象 + * @param response HTTP 响应 + */ + void getNewEmployeeLeaveRateDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + /** + * 离职原因分布明细 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回离职原因分布明细 PageListVO + */ + PageListVO getLeaveReasonDistributionDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 离职原因分布明细导出 + * + * @param dto 分析参数对象 + * @param response HTTP 响应 + */ + void getLeaveReasonDistributionDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + /** + * 各岗位人数占比明细 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回各岗位人数占比明细 PageListVO + */ + PageListVO getLeavePeoplePostRatioDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 各岗位人数占比明细导出 + * + * @param dto 分析参数对象 + * @param response HTTP 响应 + */ + void getLeavePeoplePostRatioDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + /** + * 离职人员司龄分布明细 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回离职人员司龄分布明细 PageListVO + */ + PageListVO getLeavePeopleAgeDistributionDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 离职人员司龄分布明细导出 + * + * @param dto 分析参数对象 + * @param response HTTP 响应 + */ + void getLeavePeopleAgeDistributionDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + /** + * 离职人员年龄分布占比明细 + * + * @param dto 分析参数对象,包含查询条件等 + * @return 返回离职人员年龄分布占比明细 PageListVO + */ + PageListVO getLeavePeopleAgeRatioDetail(FtbPersonnlesAnalysisDTO dto); + + /** + * 离职人员年龄分布占比明细导出 + * + * @param dto 分析参数对象 + * @param response HTTP 响应 + */ + void getLeavePeopleAgeRatioDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); + + PageListVO breakdownOfGenderDistributionOfResignation(FtbPersonnlesAnalysisDTO dto); + + void breakdownOfGenderDistributionOfResignationExprot(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverHandoverService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverHandoverService.java new file mode 100644 index 0000000..9080a04 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverHandoverService.java @@ -0,0 +1,9 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.po.FtbPersonnelsTurnoverHandover; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface FtbPersonnelsTurnoverHandoverService extends IService { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverManagementService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverManagementService.java new file mode 100644 index 0000000..845cc04 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsTurnoverManagementService.java @@ -0,0 +1,151 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsJobTrialRejectedCreateDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverCreateDTO; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverDTO; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurOrgInfo; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverInfoVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.permission.vo.v2.user.UserBoundVO; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface FtbPersonnelsTurnoverManagementService extends IService { + /** + * 提交辞职申请 + * @param createDTO 辞职申请的创建数据传输对象 + * @return 返回一个字符串,表示辞职申请的结果或状态 + */ + String applyForResignation(FtbPersonnelsTurnoverCreateDTO createDTO); + + /** + * 获取离职列表 + * @param dto 查询条件数据传输对象 + * @param page 分页信息 + * @return 返回一个分页列表,包含符合查询条件的离职管理数据传输对象 + */ + PageListVO getTurnoverList(PersonnelsQueryDTO dto, CultivatePage page); + + /** + * 获取辞职审批流程的详细信息 + * @param id 辞职申请的唯一标识 + * @return 返回一个包含辞职审批流程详细信息的数据传输对象 + */ + FtbPersonnelsTurnoverInfoVO detailsOfResignationApprovalProcess(String id); + + /** + * 撤回辞职申请 + * + * @param id 辞职申请的唯一标识 + * @param taskId + */ + void withdrawResignationApplication(String id, String taskId); + + /** + * 审核辞职申请 + * @param auditDto 审核辞职申请的数据传输对象 + * @return 返回一个字符串,表示审核的结果或状态 + */ + String reviewResignationApplication(FtbPersonnelsTurnoverDTO auditDto); + + /** + * 获取待审批的离职列表 + * @param dto 查询条件数据传输对象 + * @param page 分页信息 + * @return 返回一个分页列表,包含待审批的离职数据传输对象 + */ + PageListVO getTurnoverForList(FtbPersonnelsForAppQueryDTO dto, CultivatePage page); + + /** + * 辞职后定期关闭用户账号 + * @param tenantId 租户ID + */ + void closeUserAccountRegularlyAfterResignation(String tenantId); + + /** + * 获取用户绑定的审批信息 + * @param flag 标识 + * @param userId 用户ID + * @return 返回一个包含用户绑定审批信息的列表 + */ + List> getUserBoundApproval(String flag, String userId); + + /** + * 获取列表数量 + * @param flag 标识 + * @return 返回一个包含列表数量的数据传输对象 + */ + FtbPersonnelsBubbleCountVO getListCont(String flag); + + /** + * 试岗驳回创建离职数据清除账号信息 + * @param createDTO 试岗驳回创建数据传输对象 + */ + void jobTrialRejected(FtbPersonnelsJobTrialRejectedCreateDTO createDTO); + + /** + * 删除离职申请 + * @param userIds 用户ID列表 + */ + void deleteResignationApplication(List userIds); + + /** + * 删除离职申请 + * @param ids 主键ID列表 + */ + void deleteResignationApplicationWithId(List ids); + + /** + * 检查用户是否已签订离职协议(已废弃) + * @param userId 用户ID + * @param flag 标识 + * @return 返回一个布尔值,表示用户是否已签订离职协议 + */ + @Deprecated(since = "人事v1.3废弃") + Boolean userHasSignedASeparationAgreement(String userId,Integer flag); + + /** + * 辞职前检查 + * @param userId 用户ID + * @param taskFlag 任务标识 + * @param whetherItPassesOrNot 是否通过的标志 + * @return 返回一个空操作的结果动作对象 + */ + ActionResult resignationPreCheck(String userId,String taskFlag,String... whetherItPassesOrNot); + + /** + * 提交辞职申请到OA系统 + * @param createDTO 辞职申请的创建数据传输对象 + * @return 返回一个空操作的结果动作对象 + */ + ActionResult applyForResignatioForOA(FtbPersonnelsTurnoverCreateDTO createDTO); + + /** + * 在OA系统中审核辞职申请 + * @param auditDto 审核辞职申请的数据传输对象 + * @return 返回一个空操作的结果动作对象 + */ + ActionResult reviewResignationApplicationWithOA(FtbPersonnelsTurnoverDTO auditDto); + + List queryTurnoverList(); + + List queryTurnoverList(List userIds); + + void getTurnoverListExport(PersonnelsQueryDTO dto, HttpServletResponse response) throws IOException; + + FtbPersonnelsTurOrgInfo getUserOrganizeInfo(String userId, String phone); + + List getInformationAboutTheDepartingPerson(FtbDepUserDTO dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsUchisuikePondService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsUchisuikePondService.java new file mode 100644 index 0000000..2becf94 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnelsUchisuikePondService.java @@ -0,0 +1,72 @@ +package jnpf.personnels.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.personnels.dto.uchisuike.DeleteRecommendedPersonnelDTO; +import jnpf.model.personnels.dto.uchisuike.FtbRecommendationPoolOrgDTO; +import jnpf.model.personnels.dto.uchisuike.FtbinternalRecommendationPoolListDTO; +import jnpf.model.personnels.dto.uchisuike.app.FtbRecommendationInvitationAppDTO; +import jnpf.model.personnels.po.FtbPersonnelsUchisuikePond; +import jnpf.model.personnels.vo.uchisuike.FtbinternalRecommendationPoolVO; + +import java.util.List; + +public interface FtbPersonnelsUchisuikePondService extends IService { + /** + * 根据ID查询推荐列表,以查看简历 + * + * @param id 培养文件的ID + */ + List viewResume(String id); + + /** + * 删除推荐人员 + * + * @param dto 包含要删除的推荐人员信息的数据传输对象 + */ + void deleteRecommendedPersonnel(DeleteRecommendedPersonnelDTO dto); + + /** + * 查询内部推荐详情 + * + * @param id 内部推荐的ID + * @return 返回内部推荐的详细信息对象 + */ + FtbinternalRecommendationPoolVO internalRecommendationDetails(String id); + + /** + * 查询内推列表 + * + * @param cultivatePage 分页查询参数 + * @param ftbinternalRecommendationPoolListDTO 查询条件参数 + * @return 返回分页的内推列表 + */ + PageListVO internalRecommendationPoolListQuery(CultivatePage cultivatePage, FtbinternalRecommendationPoolListDTO ftbinternalRecommendationPoolListDTO); + + /** + * 添加内部推荐邀请 + * + * @param ftbRecommendationInvitationAppDTO 包含推荐邀请信息的数据传输对象 + */ + void internalRecommendationInvitationAdded(FtbRecommendationInvitationAppDTO ftbRecommendationInvitationAppDTO); + + /** + * 更新工作状态 + * + * @param phone 手机号 + * @param state 状态 301、预入职 302、试用 303、正式 304、待离职 305 离职 + */ + void updateWorkStatus(String state, String phone); + + /** + * 内推池组织变更 + */ + void referralPoolOrganizationChange(FtbRecommendationPoolOrgDTO ftbRecommendationPoolOrgDTO); + + /** + * 内推池组织查询 + */ + List internalReferralPoolOrganizationQuery(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoConfigService.java new file mode 100644 index 0000000..50f5e0c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoConfigService.java @@ -0,0 +1,29 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.range.FtbRangeConfigDTO; +import jnpf.model.personnels.dto.range.FtbRangeDiyQueryDTO; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; + +import java.util.List; + +/** + * + * + *@Author: peng.hao + *@create: 2025/4/7 + * + */ +public interface FtbPersonnlesInfoConfigService{ + + + FtbRangeConfigVO queryInfo(Integer type); + + void updateInfo(FtbRangeConfigDTO dto); + + void updateDiyInfo(FtbRangeDiyQueryDTO dto); + + List queryDiyInfo(Integer type); + + Integer queryType(Integer type); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoRangeConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoRangeConfigService.java new file mode 100644 index 0000000..66972df --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbPersonnlesInfoRangeConfigService.java @@ -0,0 +1,13 @@ +package jnpf.personnels.service; + + /** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +public interface FtbPersonnlesInfoRangeConfigService{ + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbRewardsPunishmentsApproveOAService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbRewardsPunishmentsApproveOAService.java new file mode 100644 index 0000000..8218d2c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbRewardsPunishmentsApproveOAService.java @@ -0,0 +1,36 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.dto.rewardspunishments.FtbAwardPassedDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbAwardSubmissionDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPenaltySubmissionDTO; + +public interface FtbRewardsPunishmentsApproveOAService { + + /** + * 处理奖励提交事件 + * + * @param ftbAwardSubmission 奖励提交的数据传输对象,包含提交的具体信息 + */ + void awardSubmission(FtbAwardSubmissionDTO ftbAwardSubmission); + + /** + * 处理奖励通过事件 + * + * @param ftbAwardPassedDTO 奖励通过的数据传输对象,包含通过的具体信息 + */ + void awardPassed(FtbAwardPassedDTO ftbAwardPassedDTO); + + /** + * 处理处罚提交事件 + * + * @param ftbPenaltySubmissionDTO 处罚提交的数据传输对象,包含提交的具体信息 + */ + void penaltySubmission(FtbPenaltySubmissionDTO ftbPenaltySubmissionDTO); + + /** + * 处理处罚通过事件 + * + * @param ftbAwardPassedDTO 处罚通过的数据传输对象,包含通过的具体信息 + */ + void penaltyPassed(FtbAwardPassedDTO ftbAwardPassedDTO); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbThousandFacePersonService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbThousandFacePersonService.java new file mode 100644 index 0000000..64e74c8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/FtbThousandFacePersonService.java @@ -0,0 +1,9 @@ +package jnpf.personnels.service; + +import jnpf.model.personnels.vo.roster.FtbThousandFacePersonVO; + +public interface FtbThousandFacePersonService { + + FtbThousandFacePersonVO getPersonnelData(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonStaffMCPServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonStaffMCPServiceImpl.java new file mode 100644 index 0000000..674498c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonStaffMCPServiceImpl.java @@ -0,0 +1,157 @@ +package jnpf.personnels.service.impl; + +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.vo.mcp.OnboardingThisMonthStatsVO; +import jnpf.model.personnels.vo.mcp.StaffBirthdayVO; +import jnpf.model.personnels.vo.mcp.StaffNotSignContractVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonStaffMCPMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.service.FtbPersonStaffMCPService; +import jnpf.personnels.service.FtbPersonnelsBlacklistService; +import jnpf.util.UserProvider; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonStaffMCPServiceImpl implements FtbPersonStaffMCPService { + + @Resource + private FtbPersonnelsStaffEmploymentApplyMapper ftbPersonnelsStaffEmploymentApplyMapper; + + @Resource + private FtbPersonStaffMCPMapper ftbPersonStaffMCPMapper; + + @Resource + private V2UserApi v2UserApi; + + @Resource + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Autowired + PermissionsUtils permissionsUtils; + + @Override + public List getSubmittedButNotOnboarded() { + List userIds = getUserIds(); + if (userIds != null && userIds.isEmpty()) return new ArrayList<>(); + return ftbPersonnelsStaffEmploymentApplyMapper.getSubmittedButNotOnboarded(userIds); + } + + @Override + public OnboardingThisMonthStatsVO getOnboardingThisMonthStats(String startTime, String endTime) { + List userIds = getUserIds(); + if (userIds != null && userIds.isEmpty()) return new OnboardingThisMonthStatsVO(); + OnboardingThisMonthStatsVO vo = new OnboardingThisMonthStatsVO(); + Long created = ftbPersonnelsStaffEmploymentApplyMapper.getCountEmploymentApplyCreatedInPeriod(startTime, endTime,userIds); + Long completed = ftbPersonnelsStaffEmploymentApplyMapper.getCountEmploymentApplyToRosterInPeriod(startTime, endTime,userIds); + vo.setNewEmployeesThisMonth(created != null ? created : 0L); + vo.setCompletedOnboardingThisMonth(completed != null ? completed : 0L); + return vo; + } + + @Nullable + private List getUserIds() { + List userIds = null; + Boolean isAdministrator = UserProvider.getUser().getIsAdministrator(); + if (!isAdministrator) { + List workStatusEnums = Arrays.stream(UserWorkStatusEnums.values()).filter(v -> v.getCode().equals(UserWorkStatusEnums.RESIGNED.getCode())).collect(Collectors.toList()); + userIds = permissionsUtils.getPersonnelUserIdDataPermissions(UserProvider.getUser().getUserId(), workStatusEnums); + } + return userIds; + } + + @Override + public List getListOfNotSubmittedForm() { + List userIds = getUserIds(); + if (userIds != null && userIds.isEmpty()) return new ArrayList<>(); + return ftbPersonnelsStaffEmploymentApplyMapper.getListOfNotSubmittedForm(userIds); + } + + /** + * 查询本月生日的员工列表 + * @return 本月生日员工列表(包含姓名、组织、岗位) + */ + @Override + public List getStaffBirthdayList() { + LocalDate now = LocalDate.now(); + int month = now.getMonthValue(); + String startBirthdayTime = String.format("%02d-01", month); + String endBirthdayTime = String.format("%02d-31", month); + List userIdPermissions = null; + if (!UserProvider.getUser().getIsAdministrator()) { + userIdPermissions = permissionsUtils.getPersonnelUserIdDataPermissions(UserProvider.getUser().getUserId(), null); + } + List list = ftbPersonStaffMCPMapper.getStaffBirthdayList(startBirthdayTime, endBirthdayTime,userIdPermissions); + // 用户Id集合 + List userIds = list.stream().map(StaffBirthdayVO::getUserId).collect(Collectors.toList()); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(userIds, null); + // 判断是否获取成功 + if (allUserInfoBatch.getData() != null) { + List userList = allUserInfoBatch.getData(); + Map userBoundVOMap = userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 将用户信息填充到列表中 + for (StaffBirthdayVO staffBirthdayVO : list) { + UserBoundVO userBoundVO = userBoundVOMap.get(staffBirthdayVO.getUserId()); + // 判断是否为空,并设置姓名、岗位、组织 + if (userBoundVO != null) { + staffBirthdayVO.setName(userBoundVO.getName()); + staffBirthdayVO.setPositionName(userBoundVO.getPositionName()); + staffBirthdayVO.setOrgName(userBoundVO.getOrganizeName()); + } + } + } + return list; + } + + /** + * 根据手机号查询是否在黑名单中 + * @param phone 手机号 + * @return true为在黑名单中,false为不在黑名单中 + */ + @Override + public Boolean isInBlacklist(String phone) { + return ftbPersonnelsBlacklistService.hasItBeenBlacklisted(phone); + } + + @Override + public List getListNotSignedContractHalfMonth() { + List list = ftbPersonStaffMCPMapper.getListNotSignedContractHalfMonth(); + if (list == null || list.isEmpty()) { + return list; + } + // 用户Id集合 + List userIds = list.stream().map(StaffNotSignContractVO::getUserId).collect(Collectors.toList()); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(userIds, null); + // 判断是否获取成功 + if (allUserInfoBatch.getData() != null) { + List userList = allUserInfoBatch.getData(); + Map userBoundVOMap = userList.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 将用户信息填充到列表中 + for (StaffNotSignContractVO vo : list) { + UserBoundVO userBoundVO = userBoundVOMap.get(vo.getUserId()); + if (userBoundVO != null) { + vo.setName(userBoundVO.getName()); + vo.setPositionName(userBoundVO.getPositionName()); + vo.setOrgName(userBoundVO.getOrganizeName()); + } + } + } + return list; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelChangesServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelChangesServiceImpl.java new file mode 100644 index 0000000..fd159de --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelChangesServiceImpl.java @@ -0,0 +1,402 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelChangesMapper; +import jnpf.personnels.mapper.FtbPersonnelsOverviewAnalysisMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelChangesService; +import jnpf.personnels.service.FtbPersonnelsOverviewAnalysisService; +import jnpf.util.excel.EasyExcelUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelChangesServiceImpl implements FtbPersonnelChangesService { + + @Resource + private FtbPersonnelChangesMapper ftbPersonnelChangesMapper; + + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + // 在职状态 + public static final List ON_THE_JOB_STATUS = List.of("302", "303", "304", "306"); + + @Autowired + private V2UserApi v2UserApi; + + @Override + public FtbPersonnlesBrainShopAdjustmentVO getBrainDrainCorrectRateShopAdjustment(FtbPersonnlesAnalysisDTO dto) { + FtbPersonnlesBrainShopAdjustmentVO ftbPersonnlesBrainShopAdjustmentVO = new FtbPersonnlesBrainShopAdjustmentVO(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return ftbPersonnlesBrainShopAdjustmentVO; + } + // 调店人数 + BigDecimal numberOfPeopleAdjustingTheStore = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStore(DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIdListByOrganizeIds, 3); + ftbPersonnlesBrainShopAdjustmentVO.setRegularNum(numberOfPeopleAdjustingTheStore); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearStartDate = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + Date yearOnYearEndDate = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + BigDecimal yearOnYearDateValue = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStore(DatePattern.NORM_MONTH_FORMAT.format(yearOnYearStartDate), + DatePattern.NORM_MONTH_FORMAT.format(yearOnYearEndDate) + , userIdListByOrganizeIds, 3); + ftbPersonnlesBrainShopAdjustmentVO.setYearOnYear(FtbPersonnelsOverviewAnalysisService.computeDivision(numberOfPeopleAdjustingTheStore.subtract(yearOnYearDateValue), yearOnYearDateValue) + .setScale(0, RoundingMode.HALF_UP)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chainStartDate = DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(); + Date chainEndDate = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + BigDecimal chainDateValue = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStore( + DatePattern.NORM_MONTH_FORMAT.format(chainStartDate), + DatePattern.NORM_MONTH_FORMAT.format(chainEndDate), userIdListByOrganizeIds, 3); + ftbPersonnlesBrainShopAdjustmentVO.setAnnulus(FtbPersonnelsOverviewAnalysisService.computeDivision(numberOfPeopleAdjustingTheStore.subtract(chainDateValue), chainDateValue) + .setScale(0, RoundingMode.HALF_UP)); + return ftbPersonnlesBrainShopAdjustmentVO; + } + + @Override + public FtbPersonnlesBrainShopAdjustmentVO getBrainDrainCorrectRateTransferPost(FtbPersonnlesAnalysisDTO dto) { + FtbPersonnlesBrainShopAdjustmentVO ftbPersonnlesBrainShopAdjustmentVO = new FtbPersonnlesBrainShopAdjustmentVO(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return ftbPersonnlesBrainShopAdjustmentVO; + } + // 调店人数 + BigDecimal numberOfPeopleAdjustingTheStore = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStore(DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIdListByOrganizeIds, 0); + ftbPersonnlesBrainShopAdjustmentVO.setRegularNum(numberOfPeopleAdjustingTheStore); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearStartDate = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + Date yearOnYearEndDate = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + BigDecimal yearOnYearDateValue = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStore(DatePattern.NORM_MONTH_FORMAT.format(yearOnYearStartDate), + DatePattern.NORM_MONTH_FORMAT.format(yearOnYearEndDate) + , userIdListByOrganizeIds, 0); + ftbPersonnlesBrainShopAdjustmentVO.setYearOnYear(FtbPersonnelsOverviewAnalysisService.computeDivision(numberOfPeopleAdjustingTheStore.subtract(yearOnYearDateValue), yearOnYearDateValue) + .setScale(0, RoundingMode.HALF_UP)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chainStartDate = DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(); + Date chainEndDate = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + BigDecimal chainDateValue = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStore( + DatePattern.NORM_MONTH_FORMAT.format(chainStartDate), + DatePattern.NORM_MONTH_FORMAT.format(chainEndDate), userIdListByOrganizeIds, 0); + ftbPersonnlesBrainShopAdjustmentVO.setAnnulus(FtbPersonnelsOverviewAnalysisService.computeDivision(numberOfPeopleAdjustingTheStore.subtract(chainDateValue), chainDateValue) + .setScale(0, RoundingMode.HALF_UP)); + return ftbPersonnlesBrainShopAdjustmentVO; + } + + @Override + public List getRegularizationStoreDimensionShopAdjustment(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + // 类型 1 部门 2 门店 3 岗位 + Integer type = dto.getType(); + List userIds = ftbPersonnelChangesMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIdListByOrganizeIds, 3); + Map> groupingData = getTypeReturnsInfoGroupingData(type, userIds); + int totalCount = groupingData.values().stream().mapToInt(List::size).sum(); + groupingData.forEach((k, v) -> { + FtbPersonnelsPeopleTransferRatioVO ratioVO = new FtbPersonnelsPeopleTransferRatioVO(); + String[] split = k.split(":"); + // 名称 + ratioVO.setName(split[1]); + String key = split[0]; + checkTheNumberOfPositions(type, key, ratioVO, dto.getStartTime(), dto.getOrganizationId()); + // 当前数量 + ratioVO.setNumberOfPeople(BigDecimal.valueOf(v.size())); + // 比例 + if (type == 3) { + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), ratioVO.getTotalCount())); + } else { + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), totalCount)); + } + results.add(ratioVO); + }); + return results; + } + @Override + public List getRegularizationStoreDimensionTransferPost(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + // 类型 1部门 2门店 3岗位 + Integer type = dto.getType(); + List userIds = ftbPersonnelChangesMapper.departmentHeadcountRatio( + DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIdListByOrganizeIds, 0); + Map> groupingData = getTypeReturnsInfoGroupingData(type, userIds); + int totalCount = groupingData.values().stream().mapToInt(List::size).sum(); + groupingData.forEach((k, v) -> { + FtbPersonnelsPeopleTransferRatioVO ratioVO = new FtbPersonnelsPeopleTransferRatioVO(); + String[] split = k.split(":"); + // 名称 + ratioVO.setName(split[1]); + String key = split[0]; + checkTheNumberOfPositions(type, key, ratioVO, dto.getStartTime(), dto.getOrganizationId()); + // 当前数量 + ratioVO.setNumberOfPeople(BigDecimal.valueOf(v.size())); + // 比例 + if (type == 3) { + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), ratioVO.getTotalCount())); + } else { + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), totalCount)); + } + results.add(ratioVO); + }); + return results; + } + + @Override + public PageListVO shopAdjustmentDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List typeReturnsInfoData = getTypeReturnsInfoData(dto.getType(), userIdListByOrganizeIds); + List userIds = typeReturnsInfoData.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return result; + } + List resultList = ftbPersonnelChangesMapper.numberOfPeopleAdjustingTheStoreList(DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIds, 3); + if (CollUtil.isEmpty(resultList)) { + return result; + } + Map employeeIDQuery = employeeIDQuery(userIds); + Map userBoundVOMap = typeReturnsInfoData.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + resultList.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setAFOrganization(userBoundVO.getOrganizeName()); + a.setAFPosition(userBoundVO.getPositionName()); + a.setAFRank(userBoundVO.getGradeName()); + a.setUserName(userBoundVO.getUserName()); + a.setEmployeeId(employeeIDQuery.getOrDefault(a.getUserId(),"")); + } + }); + result.getPagination().setTotal(resultList.size()); + // 按照组织排序 + resultList.sort(Comparator.comparing(FtbPersonnelsShopAdjustmentDetailsVO::getAFOrganization)); + result.setList(resultList); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(),resultList.size()); + return CultivatePage.paginate(resultList, page); + } + return result; + } + + @Override + public void shopAdjustmentDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO pageListVO = shopAdjustmentDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "调店人数及占比", pageListVO.getList(), FtbPersonnelsShopAdjustmentDetailsVO.class); + } + + @Override + public PageListVO postAdjustmentDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List typeReturnsInfoData = getTypeReturnsInfoData(dto.getType(), userIdListByOrganizeIds); + List userIds = typeReturnsInfoData.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(userIds)) { + return result; + } + List resultList = ftbPersonnelChangesMapper.numberOfPeopleAdjustingThePostList(DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIds, 0); + if (CollUtil.isEmpty(resultList)) { + return result; + } + Map employeeIDQuery = employeeIDQuery(userIds); + Map userBoundVOMap = typeReturnsInfoData.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + resultList.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setOrganization(userBoundVO.getOrganizeName()); + a.setPosition(userBoundVO.getPositionName()); + a.setRank(userBoundVO.getGradeName()); + a.setUserName(userBoundVO.getUserName()); + a.setEmployeeId(employeeIDQuery.getOrDefault(a.getUserId(), "")); + } + }); + result.getPagination().setTotal(resultList.size()); + // 按照组织排序 + resultList.sort(Comparator.comparing(FtbPersonnelsPostAdjustmentDetailsVO::getOrganization)); + result.setList(resultList); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(),resultList.size()); + return CultivatePage.paginate(resultList, page); + } + return result; + } + + @Override + public void postAdjustmentDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO pageListVO = postAdjustmentDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "调岗人数分布", pageListVO.getList(), FtbPersonnelsPostAdjustmentDetailsVO.class); + } + + + /** + * 所选组织和岗位员工 + */ + private List selectedOrganizationAndPositionEmployees(List organizationId) { + QueryPageUserDTO queryPageUserDTO = new QueryPageUserDTO(); + queryPageUserDTO.setIsPage(false); + queryPageUserDTO.setOrganizeIds(organizationId); + queryPageUserDTO.setHaveChildOrganizeId(false); + ActionResult> pageListVOActionResult = v2UserApi.pagePost(queryPageUserDTO); + if (pageListVOActionResult.getCode() == 200 && Objects.nonNull(pageListVOActionResult.getData())) { + return pageListVOActionResult.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + return null; + } + + /** + * 扩展人员岗位总数 + * + * @param type + * @param key + * @param ratioVO + */ + private void checkTheNumberOfPositions(Integer type, String key, FtbPersonnelsPeopleTransferRatioVO ratioVO, Date startTime, List organizationId) { + if (type == 3) { + QueryUserBatchDTO userBatchDTO = new QueryUserBatchDTO(); + userBatchDTO.setPositionIds(List.of(key)); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(userBatchDTO); + if (userInfoBatch.getCode() == 200) { + Date chainStartDate = DateUtil.endOfMonth(startTime); + List newUserInfoBatch = userInfoBatch.getData().stream() + .filter(v -> Objects.nonNull(v.getWorkStatusEnums()) && Objects.nonNull(v.getEntryDate()) + && ON_THE_JOB_STATUS.contains(v.getWorkStatusEnums().getCode()) + && v.getEntryDate().before(chainStartDate) && organizationId.contains(v.getOrganizeId())) + .collect(Collectors.toList()); + ratioVO.setTotalCount(BigDecimal.valueOf(newUserInfoBatch.size())); + } + } + } + + /** + * 根据不同type进行统计 + * + * @param dtoType 1部门 2门店 3岗位 + * @param newcomerNumUserList + * @return + */ + public Map> getTypeReturnsInfoGroupingData(Integer dtoType, List newcomerNumUserList) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(newcomerNumUserList, null); + if (actionResult.getCode() != 200) { + throw new RuntimeException("获取用户主岗信息失败"); + } + List data = actionResult.getData(); + // 类型 1部门 2门店 3岗位 + Map> stringListMap = null; + switch (dtoType) { + case 1: + stringListMap = data.stream().filter(v -> OrganizeCategoryEnums.DEPARTMENT.getCode().equals(v.getOrganizeCategory())).collect(Collectors.groupingBy(v -> v.getOrganizeId() + ":" + v.getOrganizeName())); + break; + case 2: + stringListMap = data.stream().filter(v -> OrganizeCategoryEnums.STORE.getCode().equals(v.getOrganizeCategory())).collect(Collectors.groupingBy(v -> v.getOrganizeId() + ":" + v.getOrganizeName())); + break; + case 3: + stringListMap = data.stream().collect(Collectors.groupingBy(v -> v.getPositionId() + ":" + v.getPositionName())); + break; + } + return stringListMap; + } + + public List getTypeReturnsInfoData(Integer dtoType, List newcomerNumUserList) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(newcomerNumUserList, null); + if (actionResult.getCode() != 200) { + throw new RuntimeException("获取用户主岗信息失败"); + } + List data = actionResult.getData(); + // 类型 1部门 2门店 3岗位 + List stringListMap = null; + switch (dtoType) { + case 1: + stringListMap = data.stream().filter(v -> OrganizeCategoryEnums.DEPARTMENT.getCode().equals(v.getOrganizeCategory())).collect(Collectors.toList()); + break; + case 2: + stringListMap = data.stream().filter(v -> OrganizeCategoryEnums.STORE.getCode().equals(v.getOrganizeCategory())).collect(Collectors.toList()); + break; + case 3: + stringListMap = new ArrayList<>(data); + break; + } + return stringListMap; + } + + private PaginationVO initPaginationVO(CultivatePage dto) { + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(0); + return pagination; + } + + public static HttpServletResponse getResponse() { + // 从当前threadlocal中获取到 + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return servletRequestAttributes.getResponse(); + } + private Map employeeIDQuery(List userIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.select(FtbPersonnelsStaffRoster::getUserId,FtbPersonnelsStaffRoster::getSystemWokerId); + wrapper.in(FtbPersonnelsStaffRoster::getUserId,userIds); + List staffRosters = ftbPersonnelsStaffRosterMapper.selectList(wrapper); + return staffRosters.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getSystemWokerId)); + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditCarbonRecipientServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditCarbonRecipientServiceImpl.java new file mode 100644 index 0000000..870cf2d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditCarbonRecipientServiceImpl.java @@ -0,0 +1,16 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.personnels.mapper.FtbPersonnelsAuditCarbonRecipientMapper; +import jnpf.model.personnels.po.FtbPersonnelsAuditCarbonRecipient; +import jnpf.personnels.service.FtbPersonnelsAuditCarbonRecipientService; + +@Service +public class FtbPersonnelsAuditCarbonRecipientServiceImpl extends ServiceImpl implements FtbPersonnelsAuditCarbonRecipientService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditMasterConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditMasterConfigServiceImpl.java new file mode 100644 index 0000000..db1f5b4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditMasterConfigServiceImpl.java @@ -0,0 +1,460 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.common.collect.Maps; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.personnels.dto.config.FtbPersionnelsSendCranbonCopy; +import jnpf.model.personnels.dto.config.FtbPersonnelsAuditConfigDTO; +import jnpf.model.personnels.dto.config.FtbPersonnelsAuditConfigInfoDTO; +import jnpf.model.personnels.dto.config.MasterConfigUserBoudDto; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditConfigVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.dto.SynUserBoundDTO; +import jnpf.permission.entity.BasePositionGradesEntity; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.personnels.mapper.FtbPersonnelsAuditMasterConfigMapper; +import jnpf.personnels.mapper.FtbPersonnelsAuditRunTaskMapper; +import jnpf.personnels.service.FtbPersonnelsAuditCarbonRecipientService; +import jnpf.personnels.service.FtbPersonnelsAuditMasterConfigService; +import jnpf.personnels.service.FtbPersonnelsAuditSubConfigService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsAuditMasterConfigServiceImpl extends ServiceImpl implements FtbPersonnelsAuditMasterConfigService { + + @Resource + FtbPersonnelsAuditRunTaskMapper runTaskMapper; + + @Resource + FtbPersonnelsAuditSubConfigService subConfigService; + + @Resource + FtbPersonnelsAuditCarbonRecipientService carbonRecipientService; + + @Autowired + OrganizeApi organizeApi; + + @Autowired + PositionApi positionApi; + + @Autowired + CultivatePerUtils cultivatePerUtils; + + @Autowired + FtbPersonnelsStaffRosterService staffRosterService; + + + class Pair { + private K key; + private V value; + + public Pair(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + } + + /** + * 便利数据 + * @param dataList + * @return + */ + private List> initData(List dataList ){ + List> resultDataList = new ArrayList<>(); + // 遍历原始列表 + for (int i = 0; i < dataList.size(); i++) { + // 对于第一个元素,其"前一个"数据不存在,可以设置为null或其他默认值 + if (dataList.size() == 1) { + resultDataList.add(new Pair<>("0", "0")); + }else if (i == 0 && dataList.size() > 1) { + resultDataList.add(new Pair<>("0", dataList.get(i+1))); + } + // 对于最后一个元素,其"后一个"数据不存在,可以设置为null或其他默认值 + else if (i == dataList.size() - 1) { + resultDataList.add(new Pair<>(dataList.get(i - 1), "0")); + } + // 其他元素既有前一个也有后一个数据 + else { + // 前一个数据 后一个数据 + resultDataList.add(new Pair<>(dataList.get(i - 1), dataList.get(i+1))); + } + } + return resultDataList; + } + @Override + @Transactional(rollbackFor = Exception.class) + @Deprecated + public void createPersonnelApprovalProcessConfiguration(FtbPersonnelsAuditConfigDTO dto) { + FtbPersonnelsAuditMasterConfig ftbPersonnelsAuditMasterConfig = FtbPersonnelsAuditConfigDTO.coverFtbPersonnelsAuditMasterConfig(dto); + // 修改时需要校验是否存在正在执行的任务 + if (ObjectUtil.isNotEmpty(ftbPersonnelsAuditMasterConfig.getId())){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getMasterAuditConfigId, ftbPersonnelsAuditMasterConfig.getId()); + List list = new ArrayList<>(); + list.add(FtbPersonnelsAuditTaskEnum.PENDING.getCode()); + list.add(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + wrapper.in(FtbPersonnelsAuditRunTask::getExcuseStatus,list); + Long l = runTaskMapper.selectCount(wrapper); + if (l > 0){ + throw new RuntimeException("当前存在正在审核的任务,不允许修改!"); + } + // 直接删除之前的数据重新新增 + LambdaUpdateWrapper updateRecipient = Wrappers.lambdaUpdate(); + // 删除之前的抄送数据 + updateRecipient.eq(FtbPersonnelsAuditCarbonRecipient::getMasterConfig, ftbPersonnelsAuditMasterConfig.getId()); + carbonRecipientService.remove(updateRecipient); + baseMapper.updateById(ftbPersonnelsAuditMasterConfig); + // 处理子配置先将子配置删除 + LambdaUpdateWrapper updateSubConfig = Wrappers.lambdaUpdate(); + updateSubConfig.eq(FtbPersonnelsAuditSubConfig::getMasterId,ftbPersonnelsAuditMasterConfig.getId()); + subConfigService.remove(updateSubConfig); + }else { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsAuditMasterConfig::getOrgId, dto.getOrgId()); + queryWrapper.eq(FtbPersonnelsAuditMasterConfig::getConfigType, dto.getConfigType()); + FtbPersonnelsAuditMasterConfig db = baseMapper.selectOne(queryWrapper); + if (ObjectUtil.isNotEmpty(db)){ + throw new RuntimeException("当前组织已存在相同配置项,请勿重复新增!"); + } + baseMapper.insert(ftbPersonnelsAuditMasterConfig); + } + String masterConfigId = ftbPersonnelsAuditMasterConfig.getId(); + // 处理抄送人 + List copyRecipient = dto.getCopyRecipient(); + List carbonRecipientList = copyRecipient.stream().map(item -> { + FtbPersonnelsAuditCarbonRecipient recipient = FtbPersionnelsSendCranbonCopy.coverFtbPersonnelsAuditCarbonRecipient(item); + recipient.setMasterConfig(masterConfigId); + return recipient; + }).collect(Collectors.toList()); + carbonRecipientService.saveBatch(carbonRecipientList); + // 处理审批人 + List configInfo = dto.getConfigInfo(); + List personnelsAuditSubConfigs = configInfo.stream().map(item -> { + FtbPersonnelsAuditSubConfig ftbPersonnelsAuditSubConfig = FtbPersonnelsAuditConfigInfoDTO.coverFtbPersonnelsAuditSubConfig(item); + String Id =IdWorker.getIdStr(item); + ftbPersonnelsAuditSubConfig.setId(Id); + ftbPersonnelsAuditSubConfig.setMasterId(masterConfigId); + return ftbPersonnelsAuditSubConfig; + }).sorted(Comparator.comparingInt(FtbPersonnelsAuditSubConfig::getAuditProcessLevel)).collect(Collectors.toList()); + // 根据id进行数据处理 + List stringList = personnelsAuditSubConfigs.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + // 处理为前后的数据 + + List> pairList = initData(stringList); + // 数据进行插入 + for (int i = 0; i < personnelsAuditSubConfigs.size(); i++){ + FtbPersonnelsAuditSubConfig ftbPersonnelsAuditSubConfig = personnelsAuditSubConfigs.get(i); + Pair pair = pairList.get(i); + ftbPersonnelsAuditSubConfig.setPreReview(pair.key); + ftbPersonnelsAuditSubConfig.setNextReview(pair.value); + } + subConfigService.saveBatch(personnelsAuditSubConfigs); + } + + @Override + public FtbPersonnelsAuditConfigVO getAuditList(String orgId, Integer configType) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditMasterConfig::getConfigType,configType); + wrapper.eq(FtbPersonnelsAuditMasterConfig::getOrgId,orgId); + wrapper.eq(FtbPersonnelsAuditMasterConfig::getEnableMark,"0"); + wrapper.last("limit 1"); + FtbPersonnelsAuditMasterConfig vo = baseMapper.selectOne(wrapper); + FtbPersonnelsAuditConfigVO configVO = FtbPersonnelsAuditMasterConfig.coverFtbPersonnelsAuditConfigVO(vo); + if (ObjectUtil.isEmpty(configVO)){ + return new FtbPersonnelsAuditConfigVO(); + } + List auditSubConfigs = subConfigService.lambdaQuery().eq(FtbPersonnelsAuditSubConfig::getMasterId, vo.getId()) + .orderByAsc(FtbPersonnelsAuditSubConfig::getAuditProcessLevel) + .list(); + if (CollUtil.isNotEmpty(auditSubConfigs)){ + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.in(FtbPersonnelsStaffRoster::getUserId,auditSubConfigs.stream() + .map(FtbPersonnelsAuditSubConfig::getAuditorId).distinct() + .collect(Collectors.toList())); + List list = staffRosterService.list(lambdaQuery); + Map map = list.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getName,(a,b)->a)); + auditSubConfigs.forEach(item->{ + if (ObjectUtil.isNotEmpty(item.getAuditorId()) && map.containsKey(item.getAuditorId())){ + item.setAuditorName(map.get(item.getAuditorId())); + } + }); + } + configVO.setConfigInfo(auditSubConfigs.stream().map(FtbPersonnelsAuditSubConfig::coverFtbPersonnelsAuditConfigInfoDTO).collect(Collectors.toList())); + List list = carbonRecipientService.lambdaQuery() + .eq(FtbPersonnelsAuditCarbonRecipient::getMasterConfig, vo.getId()) + .eq(FtbPersonnelsAuditCarbonRecipient::getEnableMark,0).list(); + List sendCranbonCopies = list.stream() + .map(FtbPersonnelsAuditCarbonRecipient::coverFtbPersionnelsSendCranbonCopy).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(sendCranbonCopies)) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.in(FtbPersonnelsStaffRoster::getUserId,sendCranbonCopies.stream() + .map(FtbPersionnelsSendCranbonCopy::getCcId).distinct() + .collect(Collectors.toList())); + List staffRosters = staffRosterService.list(lambdaQuery); + Map map = staffRosters.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getName,(a,b)->a)); + sendCranbonCopies.forEach(item->{ + if (map.containsKey(item.getCcId())) item.setCcName(map.get(item.getCcId())); + }); + configVO.setCopyRecipient(sendCranbonCopies); + } + return configVO; + } + + @Override + @Transactional + public void createApprovalConfiguration(FtbPersonnelsAuditConfigDTO dto) { + FtbPersonnelsAuditMasterConfig ftbPersonnelsAuditMasterConfig = FtbPersonnelsAuditConfigDTO.coverFtbPersonnelsAuditMasterConfig(dto); + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPersonnelsStaffRoster::getUserId,dto.getConfigInfo().stream().map(FtbPersonnelsAuditConfigInfoDTO::getAuditorId).collect(Collectors.toList())); + List staffRosters = staffRosterService.list(lambdaed); + List personnelsStaffRosters = staffRosters.stream().filter(item -> item.getWorkerStatus().equals("306")).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(personnelsStaffRosters)) + throw new RuntimeException("试岗员工:"+personnelsStaffRosters.stream() + .map(FtbPersonnelsStaffRoster::getName) + .collect(Collectors.joining(","))+"不能作为审批人!"); + if (ObjectUtil.isNotEmpty(ftbPersonnelsAuditMasterConfig.getId())){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getMasterAuditConfigId, ftbPersonnelsAuditMasterConfig.getId()); + List list = new ArrayList<>(); + list.add(FtbPersonnelsAuditTaskEnum.PENDING.getCode()); + list.add(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + wrapper.in(FtbPersonnelsAuditRunTask::getExcuseStatus,list); + Long l = runTaskMapper.selectCount(wrapper); + if (l > 0){ + throw new RuntimeException("当前存在正在审核的任务,不允许修改!"); + } + // 将之前的数据移除为 不可用保留 查询历史审核配置 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsAuditMasterConfig::getEnableMark,"1"); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId,ftbPersonnelsAuditMasterConfig.getId()); + baseMapper.update(null,updateWrapper); + // 移除之前的抄送人信息 + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(); + update.set(FtbPersonnelsAuditCarbonRecipient::getEnableMark,1); + update.eq(FtbPersonnelsAuditCarbonRecipient::getMasterConfig,ftbPersonnelsAuditMasterConfig.getId()); + carbonRecipientService.update(update); + // 将修改数据的id设置为null + ftbPersonnelsAuditMasterConfig.setId(null); + + } + // 新增审批配置 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsAuditMasterConfig::getOrgId, dto.getOrgId()); + queryWrapper.eq(FtbPersonnelsAuditMasterConfig::getConfigType, dto.getConfigType()); + queryWrapper.eq(FtbPersonnelsAuditMasterConfig::getEnableMark,"0"); + FtbPersonnelsAuditMasterConfig db = baseMapper.selectOne(queryWrapper); + if (ObjectUtil.isNotEmpty(db)){ + throw new RuntimeException("当前组织已存在相同配置项,请勿重复新增!"); + } + baseMapper.insert(ftbPersonnelsAuditMasterConfig); + String masterConfigId = ftbPersonnelsAuditMasterConfig.getId(); + // 处理抄送人 + List copyRecipient = dto.getCopyRecipient(); + if (CollUtil.isNotEmpty(copyRecipient)) { + List carbonRecipientList = copyRecipient.stream().map(item -> { + FtbPersonnelsAuditCarbonRecipient recipient = FtbPersionnelsSendCranbonCopy.coverFtbPersonnelsAuditCarbonRecipient(item); + recipient.setMasterConfig(masterConfigId); + return recipient; + }).collect(Collectors.toList()); + carbonRecipientService.saveBatch(carbonRecipientList); + } + // 处理审批人 + List configInfo = dto.getConfigInfo(); + List personnelsAuditSubConfigs = configInfo.stream().map(item -> { + FtbPersonnelsAuditSubConfig ftbPersonnelsAuditSubConfig = FtbPersonnelsAuditConfigInfoDTO.coverFtbPersonnelsAuditSubConfig(item); + String Id =IdWorker.getIdStr(item); + ftbPersonnelsAuditSubConfig.setId(Id); + ftbPersonnelsAuditSubConfig.setMasterId(masterConfigId); + return ftbPersonnelsAuditSubConfig; + }).sorted(Comparator.comparingInt(FtbPersonnelsAuditSubConfig::getAuditProcessLevel)) + .collect(Collectors.toCollection(LinkedList::new)); + // 根据id进行数据处理 + List stringList = personnelsAuditSubConfigs.stream().map(SuperBaseEntity.SuperIBaseEntity::getId) + .collect(Collectors.toCollection(LinkedList::new)); + // 处理为前后的数据 + List> pairList = initData(stringList); + // 数据进行插入 + for (int i = 0; i < personnelsAuditSubConfigs.size(); i++){ + FtbPersonnelsAuditSubConfig ftbPersonnelsAuditSubConfig = personnelsAuditSubConfigs.get(i); + Pair pair = pairList.get(i); + ftbPersonnelsAuditSubConfig.setPreReview(pair.key); + ftbPersonnelsAuditSubConfig.setNextReview(pair.value); + } + subConfigService.saveBatch(personnelsAuditSubConfigs); + } + + @Override + @Transactional(rollbackFor = RuntimeException.class) + public void approverDataVerification(String userId, List OldDtos, List newDtos) { + log.error("approverDataVerification userId={},OldDtos={},newDtos={}",userId, JSONUtil.toJsonStr(OldDtos),JSONUtil.toJsonStr(newDtos)); + List masterConfigUserBoudDtos = subConfigService.queryUserBound(userId); + if (CollUtil.isEmpty(masterConfigUserBoudDtos)){ + return; + } + String orgName = masterConfigUserBoudDtos.stream().map(MasterConfigUserBoudDto::getOrgName).distinct().collect(Collectors.joining(",")); + Map> masterMap = masterConfigUserBoudDtos.stream() + .collect(Collectors.groupingBy(item->item.getOrgId()+"_"+item.getPositionId()+"_"+item.getPositionGradesId())); + Map> masterOrgMap = masterConfigUserBoudDtos.stream() + .collect(Collectors.groupingBy(MasterConfigUserBoudDto::getOrgId)); + // 匹配数据 + Map> oldOrgList = OldDtos.stream() + .collect(Collectors.groupingBy(item -> item.getOrgId() + "_" + item.getPositionId() + "_" + item.getPositionGradesId())); + // 之前数据的key集合 + Set keySet = masterMap.keySet(); + // 数据标记 + int i = 0; + for (String str : keySet) { + if (oldOrgList.containsKey(str)) { + i++; + } + } + if (i == 0){ + String collect = masterConfigUserBoudDtos.stream().map(MasterConfigUserBoudDto::getOrgName).collect(Collectors.joining(",")); + throw new RuntimeException("当前用户组织信息与审批配置"+collect+"组织信息不符合请核对后重试!"); + } + Map> newOrgMap = newDtos.stream() + .collect(Collectors.groupingBy(item -> item.getOrgId() + "_" + item.getPositionId() + "_" + item.getPositionGradesId())); + Set orgKey = oldOrgList.keySet(); + int k =0; + for (String str : orgKey){ + if (newOrgMap.containsKey(str)){ + newOrgMap.remove(str); + masterMap.remove(str); + k++; + } + } + // 无任何修改 + if (k == orgKey.size()){ + return; + } + // 新数据先校验组织是否相同 + Map> newKeyOrgMap = newDtos.stream() + .collect(Collectors.groupingBy(SynUserBoundDTO::getOrgId)); + int j = 0; + Set orgKeySet = masterOrgMap.keySet(); + for (String str : orgKeySet) { + if (newKeyOrgMap.containsKey(str)) { + j++; + } + } + if (j == 0){ + throw new RuntimeException("修改失败!该人员在"+orgName+"组织中为审批人,请将其审批人身份变更为其他人后再进行编辑。!"); + } + + // 匹配数据 + // 数据库数据包含所有的组织信息 + Set keySet1 = masterMap.keySet(); + Map dbSaveNewMap = new HashMap<>(); + for (String str : keySet1){ + String[] split = str.split("_"); + String orgId = split[0]; + String postId = split[1]; + String gradeId = split[2]; + Collection> values = newOrgMap.values(); + // 可能会有多个 + for (List value : values){ + for (SynUserBoundDTO synUserBoundDTO : value){ + + if (synUserBoundDTO.getOrgId().equals(orgId) + && synUserBoundDTO.getPositionId().equals(postId) + && synUserBoundDTO.getPositionGradesId().equals(gradeId)){ + + }else if (synUserBoundDTO.getOrgId().equals(orgId)&& + synUserBoundDTO.getPositionId().equals(postId)){ + dbSaveNewMap.put(str,synUserBoundDTO); + }else if (synUserBoundDTO.getOrgId().equals(orgId)){ + dbSaveNewMap.put(str,synUserBoundDTO); + } + } + } + } + Set dbNewSet = dbSaveNewMap.keySet(); + List newDbData=new ArrayList<>(); + List boundDTOS = new ArrayList<>(dbSaveNewMap.values()); + List gradesIds = boundDTOS.stream().map(SynUserBoundDTO::getPositionGradesId).collect(Collectors.toList()); + List gradesEntities = positionApi.getGradesEntityList(gradesIds); + Map gradesMaps = Maps.newHashMap(); + if (CollUtil.isNotEmpty(gradesEntities)) gradesMaps = gradesEntities.stream().collect(Collectors.toMap(BasePositionGradesEntity::getId, BasePositionGradesEntity::getFullName, (k1, k2) -> k1)); + for (String str : dbNewSet){ + List masterConfigUserBoudDtos1 = masterMap.get(str); + SynUserBoundDTO synUserBoundDTO = dbSaveNewMap.get(str); + for (MasterConfigUserBoudDto masterConfigUserBoudDto : masterConfigUserBoudDtos1){ + FtbPersonnelsAuditConfigVO auditList = getAuditList(masterConfigUserBoudDto.getOrgId(), masterConfigUserBoudDto.getConfigType()); + // 组装数据 + FtbPersonnelsAuditConfigDTO ftbPersonnelsAuditConfigDTO = new FtbPersonnelsAuditConfigDTO(); + ftbPersonnelsAuditConfigDTO.setId(auditList.getId()); + ftbPersonnelsAuditConfigDTO.setConfigType(auditList.getConfigType()); + ftbPersonnelsAuditConfigDTO.setOrgId(auditList.getOrgId()); + ftbPersonnelsAuditConfigDTO.setCopyRecipient(auditList.getCopyRecipient()); + ftbPersonnelsAuditConfigDTO.setIsOnePersonPasses(auditList.getIsOnePersonPasses()); + ftbPersonnelsAuditConfigDTO.setOrgName(auditList.getOrgName()); + ftbPersonnelsAuditConfigDTO.setIsApprovalComments(auditList.getIsApprovalComments()); + List configInfo = auditList.getConfigInfo(); + List newConfigInfo = new ArrayList<>(); + for (FtbPersonnelsAuditConfigInfoDTO configInfoDTO : configInfo){ + // 封装 + FtbPersonnelsAuditConfigInfoDTO ftbPersonnelsAuditConfigInfoDTO = new FtbPersonnelsAuditConfigInfoDTO(); + ftbPersonnelsAuditConfigInfoDTO.setAuditProcessLevel(configInfoDTO.getAuditProcessLevel()); + ftbPersonnelsAuditConfigInfoDTO.setAuditorId(configInfoDTO.getAuditorId()); + ftbPersonnelsAuditConfigInfoDTO.setAuditorName(configInfoDTO.getAuditorName()); + ftbPersonnelsAuditConfigInfoDTO.setPayrollAudit(configInfoDTO.getPayrollAudit()); + ftbPersonnelsAuditConfigInfoDTO.setPayrollEntry(configInfoDTO.getPayrollEntry()); + if (configInfoDTO.getAuditorId().equals(masterConfigUserBoudDto.getUserId())){ + ftbPersonnelsAuditConfigInfoDTO.setOrgId(synUserBoundDTO.getOrgId()); + OrganizeEntity infoById = organizeApi.getInfoById(synUserBoundDTO.getOrgId()); + if (infoById != null)ftbPersonnelsAuditConfigInfoDTO.setOrgName(infoById.getFullName()); + ftbPersonnelsAuditConfigInfoDTO.setPostId(synUserBoundDTO.getPositionId()); + PositionEntity positionEntity = positionApi.queryInfoById(synUserBoundDTO.getPositionId()); + if (positionEntity != null) ftbPersonnelsAuditConfigInfoDTO.setPostName(positionEntity.getFullName()); + ftbPersonnelsAuditConfigInfoDTO.setGradeId(synUserBoundDTO.getPositionGradesId()); + ftbPersonnelsAuditConfigInfoDTO.setGradeName(gradesMaps.get(synUserBoundDTO.getPositionGradesId())); + }else { + ftbPersonnelsAuditConfigInfoDTO.setOrgId(configInfoDTO.getOrgId()); + ftbPersonnelsAuditConfigInfoDTO.setOrgName(configInfoDTO.getOrgName()); + ftbPersonnelsAuditConfigInfoDTO.setPostId(configInfoDTO.getPostId()); + ftbPersonnelsAuditConfigInfoDTO.setPostName(configInfoDTO.getPostName()); + ftbPersonnelsAuditConfigInfoDTO.setGradeId(configInfoDTO.getGradeId()); + ftbPersonnelsAuditConfigInfoDTO.setGradeName(configInfoDTO.getGradeName()); + + } + newConfigInfo.add(ftbPersonnelsAuditConfigInfoDTO); + } + ftbPersonnelsAuditConfigDTO.setConfigInfo(newConfigInfo); + newDbData.add(ftbPersonnelsAuditConfigDTO); + } + } + // 创建新的审批流程 + newDbData.forEach(this::createApprovalConfiguration); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskHistoryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskHistoryServiceImpl.java new file mode 100644 index 0000000..cd9020b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskHistoryServiceImpl.java @@ -0,0 +1,29 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsAuditRunTaskHistory; +import jnpf.model.personnels.vo.history.FtbPersonnelsAuditRunTaskHistoryVO; +import jnpf.personnels.mapper.FtbPersonnelsAuditRunTaskHistoryMapper; +import jnpf.personnels.service.FtbPersonnelsAuditCarbonRecipientService; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskHistoryService; +import jnpf.personnels.service.FtbPersonnelsAuditTaskInfoService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +@Service +public class FtbPersonnelsAuditRunTaskHistoryServiceImpl extends ServiceImpl implements FtbPersonnelsAuditRunTaskHistoryService { + + @Resource + FtbPersonnelsAuditTaskInfoService taskInfoService; + + @Resource + FtbPersonnelsAuditCarbonRecipientService carbonRecipientService; + + + @Override + public List viewHistoricalApprovalList(String id, String type) { + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskServiceImpl.java new file mode 100644 index 0000000..3d8460e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditRunTaskServiceImpl.java @@ -0,0 +1,740 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.config.FtbPersionnelsSendCranbonCopy; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditRunTaskInfo; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditRunTaskVO; +import jnpf.personnels.mapper.FtbPersonnelsAuditRunTaskMapper; +import jnpf.personnels.mapper.FtbPersonnelsAuditSubConfigMapper; +import jnpf.personnels.mapper.FtbPersonnelsSecondmentManagementMapper; +import jnpf.personnels.mapper.FtbPersonnelsTransferManageMapper; +import jnpf.personnels.service.*; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +@Slf4j +public class FtbPersonnelsAuditRunTaskServiceImpl extends ServiceImpl implements FtbPersonnelsAuditRunTaskService { + + @Resource + FtbPersonnelsAuditTaskInfoService taskInfoService; + + + @Resource + FtbPersonnelsAuditRunTaskHistoryService runTaskHistoryService; + + @Resource + FtbPersonnelsAuditMasterConfigService configService; + + @Resource + FtbPersonnelsAuditSubConfigMapper subConfigMapper; + + @Resource + FtbPersonnelsAuditCarbonRecipientService carbonRecipientService; + + @Resource + FtbPersonnelsRegularManagementService regularManagementService; + + @Resource + FtbPersonnelsStaffTransferPositionService transferPositionService; + + @Resource + FtbPersonnelsTurnoverManagementService turnoverManagementService; + + @Resource + FtbPersonnelsPostApplyService personnelsPostApplyService; + + @Autowired + FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private FtbPersonnelsTransferManageMapper ftbPersonnelsTransferManageMapper; + + @Resource + FtbPersonnelsSecondmentManagementMapper ftbPersonnelsSecondmentManagementMapper; + + + @Override + @Transactional(rollbackFor = {Exception.class,RuntimeException.class}) + public String startTheReviewProcess(String businessId, Integer type, String orgId, String userId) { + // 校验是否存在对应的审核流程配置 + FtbPersonnelsAuditMasterConfig config = configService.lambdaQuery() + .eq(FtbPersonnelsAuditMasterConfig::getOrgId, orgId) + .eq(FtbPersonnelsAuditMasterConfig::getConfigType, type) + .eq(FtbPersonnelsAuditMasterConfig::getEnableMark,"0").one(); + if (ObjectUtil.isEmpty(config)){ + throw new RuntimeException("您所选的组织没有配置相关审批流程,请在“人事审批权限配置”中进行配置后再提交审批!"); + } + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbPersonnelsAuditSubConfig::getMasterId, config.getId()); + List auditSubConfigs = subConfigMapper.selectList(lambdaed); + List stringList = auditSubConfigs.stream().map(FtbPersonnelsAuditSubConfig::getAuditorId).collect(Collectors.toList()); + if (stringList.contains(userId)){ + throw new RuntimeException("您是对应组织审批人,无法对应发起流程,请替换审批人后重试!"); + } + String theReviewProcess = checkWhetherTheCurrentPersonnelIsInTheReviewProcess(userId); + if (StringUtil.isNotEmpty(theReviewProcess)){ + // 1 转正 2 调岗, 3 离职 + String str=""; + switch (theReviewProcess){ + case "1": + str = "转正"; + break; + case "2": + str = "调岗"; + break; + case "3": + str = "离职"; + break; + case "4": + str = "晋升"; + break; + } + throw new RuntimeException("当前人员处于"+str+"审批中,请勿重复进行审批!"); + } + // 1.根据业务主键查询是否存在审核记录 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getBusinessId, businessId); + FtbPersonnelsAuditRunTask auditRunTask = baseMapper.selectOne(wrapper); + if (ObjectUtil.isNotEmpty(auditRunTask)){ + // throw new RuntimeException("该业务已经存在审核记录,请勿重复提交审核"); + FtbPersonnelsAuditRunTaskHistory runTaskHistory =new FtbPersonnelsAuditRunTaskHistory(); + BeanUtil.copyProperties(auditRunTask, runTaskHistory); + runTaskHistory.setId(auditRunTask.getId()); + runTaskHistoryService.save(runTaskHistory); + baseMapper.deleteById(auditRunTask.getId()); + } + // 2.如果不存在审核记录,新建一条审核流程开启审核 + FtbPersonnelsAuditRunTask runTask = new FtbPersonnelsAuditRunTask(); + runTask.setBusinessId(businessId); + runTask.setMasterAuditConfigId(config.getId()); + // 查询是否是会签 + if ( ObjectUtil.isEmpty(config.getIsOnePersonPasses()) || config.getIsOnePersonPasses() != 1) { + // 会签初始化执行人节点id + List subConfig = subConfigMapper.queryInitializeExecutionNode(orgId, type, config.getIsOnePersonPasses()); + runTask.setUserId(subConfig.get(0).getAuditorId()); + } + runTask.setExcuseStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + baseMapper.insert(runTask); + return runTask.getId(); + } + + @Override + public Boolean checkWhetherTheCurrentSignIsOr(String businessId, Integer configType, String userId) { + // 1.查询当前业务是否开启审核 + String s = baseMapper.queryHaveRuntask(businessId, configType,userId); + if (ObjectUtil.isEmpty(s)){ + // 不存在审核记录,直接返回撤销审核结果 + return false; + } + FtbPersonnelsAuditMasterConfig masterConfig = getMasterConfig(s); + // 一人审批通过即视为该流程全部审批通过(0否,1是) + // 一人或签 + if (ObjectUtil.isEmpty(masterConfig)){ + return false; + } + if (masterConfig.getIsOnePersonPasses() != null && masterConfig.getIsOnePersonPasses() == 1){ + return true; + } + return false; + } + + @Override + public Boolean checkWhetherTheCurrentApproverIs(String userId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditSubConfig::getAuditorId,userId); + List personnelsAuditSubConfigs = subConfigMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(personnelsAuditSubConfigs)){ + return true; + } + return false; + } + + @Override + public Boolean checkIsTheCurrentBusinessInTheApprovalProcess(String businessId) { + FtbPersonnelsAuditRunTask runTask = getFtbPersonnelsAuditRunTask(businessId); + if (ObjectUtil.isEmpty(runTask)){ + return false; + } + return true; + } + + @Override + public FtbPersonnelsCofigEnum checksItInTheReviewProcess(String businessId) { + FtbPersonnelsAuditRunTask runTask = getFtbPersonnelsAuditRunTask(businessId); + if (ObjectUtil.isEmpty(runTask)){ + return null; + } + FtbPersonnelsAuditMasterConfig entity = configService.lambdaQuery().eq(SuperBaseEntity.SuperIBaseEntity::getId, + runTask.getMasterAuditConfigId()).one(); + return FtbPersonnelsCofigEnum.getConfigType(entity.getConfigType()); + } + + + + @Override + @Transactional(rollbackFor = {Exception.class,RuntimeException.class}) + public synchronized FtbPersonnelsAuditTaskEnum performReview(FtbPersonnelsAuditDto auditDto,String... userIds) { + // 1.查询当前业务是否开启审核 + String userId = CollUtil.isNotEmpty(List.of(userIds)) ? userIds[0] : UserProvider.getUser().getUserId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getBusinessId, auditDto.getBusinessId()); + FtbPersonnelsAuditRunTask runTask = baseMapper.selectOne(wrapper); + if (ObjectUtil.isEmpty(runTask)){ + FtbPersonnelsAuditRunTaskHistory entity = runTaskHistoryService.lambdaQuery() + .eq(FtbPersonnelsAuditRunTaskHistory::getBusinessId, auditDto.getBusinessId()) + .orderByDesc(SuperBaseEntity.SuperCBaseEntity::getCreatorTime).last("limit 1").one(); + if (ObjectUtil.isNotEmpty(entity)){ + boolean equals = entity.getExcuseStatus().equals(FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + if (equals){ + throw new RuntimeException("该业务申请已经撤销,请勿提交审核!"); + } + } + // 不存在审核记录,直接返回撤销审核结果 + throw new RuntimeException("不存在对应的审核流程! "); + } + FtbPersonnelsAuditMasterConfig masterConfig = getMasterConfig(runTask.getMasterAuditConfigId()); + // 校验是否审核意见为必填 + if (masterConfig.getIsApprovalComments() != null && masterConfig.getIsApprovalComments() == 1 ){ + if (ObjectUtil.isEmpty(auditDto.getApprovalComments())){ + throw new RuntimeException("审核意见为必填项"); + } + } + List subConfig = subConfigMapper.queryInitializeExecutionNode + (masterConfig.getOrgId(), masterConfig.getConfigType(), null); + // 一人审批通过即视为该流程全部审批通过(0否,1是) + // 3.查询是离职才会有 或签 执行人节点 + // UserInfo userInfo = UserProvider.getUser(); + // 一人或签 + FtbPersonnelsAuditTaskEnum result; + if (masterConfig.getIsOnePersonPasses() != null && masterConfig.getIsOnePersonPasses() == 1){ + List taskInfoList = getAuditTaskInfos(auditDto, subConfig, userId, runTask,null); + if ("0".equals(auditDto.getFlag())){ //不通过 + result = FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED; + }else { + result = FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + } + taskInfoService.saveBatch(taskInfoList); + // 一人或签更改流程最终状态 + runTask.setExcuseStatus(result.getCode()); + baseMapper.updateById(runTask); + return result; + } + // 会签 2.获取当前审核人是否为配置审核人是否相同 + if (!userId.equals(runTask.getUserId())){ + throw new RuntimeException("当前任务审核人与登录人不一致,无法提交审核!"); + } + // 4.获取当前人审核配置是否具有对应的权限 + FtbPersonnelsAuditSubConfig auditSubConfig = subConfig.stream() + .filter(item -> item.getAuditorId().equals(userId)).findFirst().orElse(null); + if (ObjectUtils.isEmpty(auditSubConfig)){ + throw new RuntimeException("当前登录人没有该审核人的权限,无法提交审核!"); + } + FtbPersonnelsAuditTaskInfo taskInfo = new FtbPersonnelsAuditTaskInfo(); + if(StringUtils.isEmpty(auditSubConfig.getBlacklistFilling()) + && "0".equals(auditSubConfig.getBlacklistFilling()) + && auditDto.getIsBeBlackList() != null){ + throw new RuntimeException("当前登录人没有填写黑名单的权限,无法提交审核!"); + }else { + taskInfo.setIsBeBlackList(auditDto.getIsBeBlackList()); + // 加入黑名单 + taskInfo.setBlacklistCause(auditDto.getBlacklistCause()); + taskInfo.setBlackListPeriod(auditDto.getBlackListPeriod()); + taskInfo.setBlackTypeId(auditDto.getBlackTypeId()); + } + if ("0".equals(auditSubConfig.getPayrollEntry()) && ObjectUtil.isNotEmpty(auditDto.getSalary())) { + throw new RuntimeException("当前登录人没有薪资填写权限,无法填写薪资!"); + }else { + taskInfo.setPayrollEntry(auditDto.getSalary()); + } + if ("0".equals(auditSubConfig.getPayrollAudit()) && ObjectUtil.isNotEmpty(auditDto.getDoesTheSalaryComplyWith())){ + throw new RuntimeException("当前登录人没有薪资审核权限,无法审核薪资!"); + }else { + taskInfo.setPayrollAudit(auditDto.getDoesTheSalaryComplyWith()); + } + // 5.执行审核 返回最终流程审核状态 + // 5.1根据对应的审核状态执行流程 + taskInfo.setRunTaskId(runTask.getId()); + taskInfo.setUserId(userId); + taskInfo.setApprovalOpinion(auditDto.getApprovalComments()); + if ("1".equals(auditDto.getFlag())){ + taskInfo.setApprovalResult("1"); + } else if ("0".equals(auditDto.getFlag())){ + taskInfo.setApprovalResult("0"); + } + taskInfoService.save(taskInfo); + String nextReview = auditSubConfig.getNextReview(); + // 创建返回值赋值 + FtbPersonnelsAuditTaskEnum returnCode = null; + // 创建历史记录表 + if ("0".equals(nextReview) && "1".equals(auditDto.getFlag())){ + // 将下一个节点置为 null 返回 + runTask.setUserId("0"); + Integer code = FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode(); + returnCode = FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + runTask.setExcuseStatus(code); + }else{ + // 5.2如果为通过且下一节点不为空 需要将执行人id 进行替换 + if ("1".equals(auditDto.getFlag())){ + String auditorId = Objects.requireNonNull(subConfig.stream().filter(item -> nextReview.equals(item.getId())).findFirst().orElse(null)).getAuditorId(); + runTask.setUserId(auditorId); + Integer underReviewCode = FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode(); + runTask.setExcuseStatus(underReviewCode); + returnCode = FtbPersonnelsAuditTaskEnum.UNDER_REVIEW; + } else if ("0".equals(auditDto.getFlag())){ + // 5.3如果为不通过则流程结束将审核结果置为不通过,记录到历史审核记录中 + runTask.setUserId("0"); + Integer auditNotPassedCode = FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode(); + runTask.setExcuseStatus(auditNotPassedCode); + returnCode = FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED; + initializationDoesNotPassData(subConfig, userId, runTask); + } + } + baseMapper.updateById(runTask); + return returnCode; + } + + /** + * 初始化不通过数据 + * @param subConfig + * @param runTask + */ + private void initializationDoesNotPassData(List subConfig, String userId, FtbPersonnelsAuditRunTask runTask) { + int k =0; + for (int i = 0; i < subConfig.size(); i++) { + FtbPersonnelsAuditSubConfig item = subConfig.get(i); + if (item.getAuditorId().equals(userId) && !item.getNextReview().equals("0")){ + k = i; + }else if (item.getAuditorId().equals(userId) && item.getNextReview().equals("0")){ + return; + } + } + if (k+1 != subConfig.size()) { + List subConfigList = subConfig.subList(k+1, subConfig.size()); + if (CollUtil.isNotEmpty(subConfigList)) { + List taskInfoList = initNotPassedInfos(subConfigList, runTask.getId()); + // 不通过直接将数据初始化 + taskInfoService.saveBatch(taskInfoList); + } + } + } + + @NotNull + private static List initNotPassedInfos(List subConfig, + String runTaskId) { + List taskInfoList = subConfig.stream().map(item -> { + FtbPersonnelsAuditTaskInfo info = new FtbPersonnelsAuditTaskInfo(); + info.setRunTaskId(runTaskId); + // 审核是否通过(0不通过,1通过,2不通过(自动)) + info.setUserId(item.getAuditorId()); + info.setUserName(item.getAuditorName()); + info.setApprovalResult("2"); + return info; + }).collect(Collectors.toList()); + return taskInfoList; + } + + @NotNull + private static List getAuditTaskInfos(FtbPersonnelsAuditDto auditDto, + List subConfig, + String userId, + FtbPersonnelsAuditRunTask runTask,String id) { + List UserIds = subConfig.stream().map(FtbPersonnelsAuditSubConfig::getAuditorId).collect(Collectors.toList()); + if (!UserIds.contains(userId)){ + throw new RuntimeException("当前配置审核人列表中不存在当前登录人,无法提交审核!"); + } + + List taskInfoList = subConfig.stream().map(item -> { + FtbPersonnelsAuditTaskInfo info = new FtbPersonnelsAuditTaskInfo(); + info.setRunTaskId(runTask.getId()); + // 为当前审批人 + if (userId.equals(item.getAuditorId())) { + if(ObjectUtil.isEmpty(item.getBlacklistFilling()) && "0".equals(item.getBlacklistFilling()) && auditDto.getIsBeBlackList() != null){ + throw new RuntimeException("当前登录人没有填写黑名单的权限,无法提交审核!"); + }else { + info.setIsBeBlackList(auditDto.getIsBeBlackList()); + // 加入黑名单 + info.setBlacklistCause(auditDto.getBlacklistCause()); + info.setBlackListPeriod(auditDto.getBlackListPeriod()); + info.setBlackTypeId(auditDto.getBlackTypeId()); + } + info.setUserId(userId); + info.setApprovalOpinion(auditDto.getApprovalComments()); + info.setApprovalResult(auditDto.getFlag()); + if (StringUtils.isNotEmpty(id)) { + info.setId(id); + } + } else { + // 审核是否通过(0不通过,1通过,2不通过(自动),3通过(自动)) + info.setUserId(item.getAuditorId()); + info.setUserName(item.getAuditorName()); + if ("0".equals(auditDto.getFlag())) { + info.setApprovalResult("2"); + } else if ("1".equals(auditDto.getFlag())) { + info.setApprovalResult("3"); + } + } + return info; + }).collect(Collectors.toList()); + return taskInfoList; + } + + @Override + @Transactional(rollbackFor = {Exception.class,RuntimeException.class}) + public FtbPersonnelsAuditTaskEnum withdrawTheReviewProcess(String businessId) { + FtbPersonnelsAuditRunTask auditRunTask = getFtbPersonnelsAuditRunTask(businessId); + if (ObjectUtil.isEmpty(auditRunTask)){ + // 不存在审核记录,直接返回撤销审核结果 + throw new RuntimeException("该申请已撤销!"); + } + // 撤销对应的审核流程将审核流程最终结果置为撤销 + auditRunTask.setExcuseStatus(FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + // 初始化剩下的审核流程节点 查询sql已处理 + // 将流程同步到历史流程中 + FtbPersonnelsAuditRunTaskHistory runTaskHistory = new FtbPersonnelsAuditRunTaskHistory(); + auditRunTask.setUserId("0"); + BeanUtil.copyProperties(auditRunTask, runTaskHistory); + runTaskHistory.setId(auditRunTask.getId()); + boolean save = runTaskHistoryService.save(runTaskHistory); + if (!save){ + throw new RuntimeException("撤销审核失败"); + } + // 将run task中的数据进行删除 + baseMapper.deleteById(auditRunTask.getId()); + return FtbPersonnelsAuditTaskEnum.NOT_PROCESSED; + } + + + @Override + public FtbPersonnelsAuditTaskEnum getWhetherLoginPerToTheAuditNode(String businessId, + FtbPersonnelsCofigEnum ftbPersonnelsCofigEnum) { + UserInfo user = UserProvider.getUser(); + // 获取主配置是或签 还是 会签 + String masterAuditConfigId = baseMapper.queryHaveRuntask(businessId, ftbPersonnelsCofigEnum.getConfigType(), user.getUserId()); + if (ObjectUtil.isEmpty(masterAuditConfigId)) return null; + FtbPersonnelsAuditMasterConfig entity = getMasterConfig(masterAuditConfigId); + // 或签 一人通过即为全部通过 + if ( entity.getIsOnePersonPasses() != null && entity.getIsOnePersonPasses() == 1){ + return FtbPersonnelsAuditTaskEnum.PENDING; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getBusinessId,businessId); + wrapper.eq(FtbPersonnelsAuditRunTask::getUserId,user.getUserId()); + FtbPersonnelsAuditRunTask auditRunTask = baseMapper.selectOne(wrapper); + log.info("当前审核任务:{}",auditRunTask); + if (ObjectUtil.isEmpty(auditRunTask)){ + // 还未到当前人审核节点 + return FtbPersonnelsAuditTaskEnum.UNDER_REVIEW; + } + + // 将当前审核节点为待审核节点 + return FtbPersonnelsAuditTaskEnum.PENDING; + } + + + @Override + public FtbPersonnelsAuditRunTaskVO viewTheCurrentApprovalProcessByTaskId(String id) { + FtbPersonnelsAuditRunTaskVO runTaskVO; + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getId,id); + wrapper.last("limit 1"); + FtbPersonnelsAuditRunTask auditRunTask = baseMapper.selectOne(wrapper); + String masterAuditConfigId = ""; + if(ObjectUtil.isEmpty(auditRunTask)){ + FtbPersonnelsAuditRunTaskHistory one = runTaskHistoryService.lambdaQuery() + .eq(SuperBaseEntity.SuperIBaseEntity::getId, id).one(); + if (ObjectUtil.isEmpty(one)){ + return new FtbPersonnelsAuditRunTaskVO(); + } + FtbPersonnelsAuditRunTask target = new FtbPersonnelsAuditRunTask(); + BeanUtil.copyProperties(one, target); + runTaskVO = FtbPersonnelsAuditRunTaskVO.coverFtbPersonnelsAuditRunTaskVO(target); + List list = taskInfoService.getHisTaskInfos(one.getId(), + one.getMasterAuditConfigId()); + masterAuditConfigId = target.getMasterAuditConfigId(); + runTaskVO.setTaskInfos(list); + }else { + runTaskVO = FtbPersonnelsAuditRunTaskVO.coverFtbPersonnelsAuditRunTaskVO(auditRunTask); + List list = taskInfoService.getRunTaskInfos(auditRunTask.getBusinessId(), + auditRunTask.getMasterAuditConfigId()); + masterAuditConfigId = auditRunTask.getMasterAuditConfigId(); + List taskHistories = runTaskHistoryService.lambdaQuery() + .eq(FtbPersonnelsAuditRunTaskHistory::getBusinessId, auditRunTask.getBusinessId()) + .list(); + if (CollUtil.isNotEmpty(taskHistories)){ + runTaskVO.setMarkForReApproval("1"); + } + runTaskVO.setTaskInfos(list); + } + setApprovalProperties(runTaskVO,masterAuditConfigId); + return runTaskVO; + } + + @Override + public String checkWhetherTheCurrentPersonnelIsInTheReviewProcess(String userId) { + // null 不处于 ,1转正 2,调岗, 3离职 + //0,待审批,1审批中 FtbPersonnelsAuditTaskEnum + List list = new ArrayList<>(); + Integer codePENDING = FtbPersonnelsAuditTaskEnum.PENDING.getCode(); + Integer codeUNDERREVIEW = FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode(); + list.add(codePENDING); + list.add(codeUNDERREVIEW); + List regularManagements = regularManagementService.lambdaQuery() + .eq(FtbPersonnelsRegularManagement::getUserId, userId) + .in(FtbPersonnelsRegularManagement::getTransferStatus, list) + .eq(FtbPersonnelsRegularManagement::getEnableMark,0).list(); + Long aLong = getStringsForOA(regularManagements.stream().map(SuperBaseEntity.SuperIBaseEntity::getId)); + if (aLong > 0){ + log.debug("当前用户正在转正审核流程中"); + return "1"; + } + List turnoverManagements = turnoverManagementService.lambdaQuery() + .eq(FtbPersonnelsTurnoverManagement::getUserId, userId) + .in(FtbPersonnelsTurnoverManagement::getTurnoverStatus, list) + .eq(FtbPersonnelsTurnoverManagement::getEnableMark,0).list(); + Long turnoverCount = getStringsForOA(turnoverManagements.stream().map(SuperBaseEntity.SuperIBaseEntity::getId)); + if (turnoverCount > 0){ + log.debug("当前用户正在离职审核流程中!"); + return "3"; + } + // 调动模块 + LambdaQueryWrapper transferManages = Wrappers.lambdaQuery(); + transferManages.eq(FtbPersonnelsTransferManage::getUserId, userId); + transferManages.in(FtbPersonnelsTransferManage::getProcessingStatus, list); + transferManages.eq(FtbPersonnelsTransferManage::getEnableMarkd, 0); + transferManages.last("limit 1"); + FtbPersonnelsTransferManage transferManage = ftbPersonnelsTransferManageMapper.selectOne(transferManages); + if (Objects.nonNull(transferManage)) { + // 调动类型0调岗1晋升2降职3调店 转换为2,调岗, 4晋升 5调店 6降职 + Integer transferType = transferManage.getState(); + switch (transferType) { + case 0: + return "2"; + case 1: + return "4"; + case 2: + return "6"; + case 3: + return "5"; + default: + break; + } + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getUserId, userId); + queryWrapper.in(FtbPersonnelsSecondmentManagement::getTransferStatus, List.of(codeUNDERREVIEW)); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getEnableMark,0); + FtbPersonnelsSecondmentManagement ftbPersonnelsSecondmentManagement = ftbPersonnelsSecondmentManagementMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbPersonnelsSecondmentManagement)){ + return "7"; + } + return null; + } + + @Override + public void deleteReviewProcessAndRecords(String runTaskId, String businessId) { + // 移除审批数据 + baseMapper.deleteById(runTaskId); + // 移除历史审批数据 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTaskHistory::getBusinessId,businessId); + List listed = runTaskHistoryService.list(wrapper); + LambdaQueryWrapper infoWrapper = Wrappers.lambdaQuery(); + List list = new ArrayList<>(); + list.add(runTaskId); + List stringList = listed.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(stringList)){ + list.addAll(stringList); + runTaskHistoryService.removeBatchByIds(stringList); + } + infoWrapper.in(FtbPersonnelsAuditTaskInfo::getRunTaskId,list); + taskInfoService.remove(infoWrapper); + } + + @Override + public void closeAllExecutionData(List teantIdList) { + for (String tenantId : teantIdList) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + // 多个租户执行 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsAuditRunTask::getExcuseStatus, 0,1); + List runTaskList = baseMapper.selectList(wrapper); + + // 一人审批单独处理 + List onePersonApprovesList = runTaskList.stream().filter(item -> StringUtil.isEmpty(item.getUserId())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(onePersonApprovesList)) onePersonApproves(onePersonApprovesList); + // 流程审批 + List processApprovalRejectionList = runTaskList.stream().filter(item -> StringUtil.isNotEmpty(item.getUserId())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(processApprovalRejectionList)) processApprovalRejection(processApprovalRejectionList); + } + } + + /** + * 流程处理 + * @param processApprovalRejectionList + */ + private void processApprovalRejection(List processApprovalRejectionList){ + FtbPersonnelsAuditDto auditDto = new FtbPersonnelsAuditDto(); + auditDto.setFlag("0"); + for (FtbPersonnelsAuditRunTask runTask : processApprovalRejectionList) { + auditDto.setBusinessId(runTask.getBusinessId()); + auditDto.setApprovalComments("审批不通过"); + auditDto.setBusinessId(runTask.getBusinessId()); + try { + performReview(auditDto,runTask.getUserId()); + }catch (Exception e){ + e.printStackTrace(); + } + } + } + + /** + * 一人审批处理 + * @param onePersonApprovesList + */ + public void onePersonApproves(List onePersonApprovesList) { + // 反向查询对应需要的userId + FtbPersonnelsAuditDto auditDto = new FtbPersonnelsAuditDto(); + auditDto.setFlag("0"); + for (FtbPersonnelsAuditRunTask runTask : onePersonApprovesList) { + String masterAuditConfigId = runTask.getMasterAuditConfigId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditSubConfig::getMasterId,masterAuditConfigId); + List auditSubConfigs = subConfigMapper.selectList(wrapper); + auditDto.setBusinessId(runTask.getBusinessId()); + auditDto.setApprovalComments("审批不通过"); + if (CollUtil.isNotEmpty(auditSubConfigs)){ + String auditorId = auditSubConfigs.get(0).getAuditorId(); + try { + performReview(auditDto, auditorId); + }catch (Exception e){ + e.printStackTrace(); + } + } + } + + } + + @NotNull + private Long getStrings(Stream stream,List list) { + List idsTransfer = stream.collect(Collectors.toList()); + if (CollUtil.isEmpty(idsTransfer)){ + return 0L; + } + LambdaQueryWrapper transferQueryWrapper = Wrappers.lambdaQuery(); + transferQueryWrapper.in(FtbPersonnelsAuditRunTask::getBusinessId,idsTransfer); + transferQueryWrapper.in(FtbPersonnelsAuditRunTask::getExcuseStatus, list); + return baseMapper.selectCount(transferQueryWrapper); + } + @NotNull + private Long getStringsForOA(Stream stream) { + List idsTransfer = stream.collect(Collectors.toList()); + if (CollUtil.isEmpty(idsTransfer)){ + return 0L; + } + // OA 不走数自定义表校验 + return (long) idsTransfer.size(); + } + + @Override + public FtbPersonnelsAuditRunTaskVO viewTheCurrentApprovalProcess(String id) { + + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getBusinessId,id); + wrapper.last("limit 1"); + FtbPersonnelsAuditRunTask auditRunTask = baseMapper.selectOne(wrapper); + if(ObjectUtil.isEmpty(auditRunTask)){ + throw new RuntimeException("当前任务未开启审批!"); + } + FtbPersonnelsAuditRunTaskVO runTaskVO = FtbPersonnelsAuditRunTaskVO.coverFtbPersonnelsAuditRunTaskVO(auditRunTask); + List list = taskInfoService.getRunTaskInfos(id, auditRunTask.getMasterAuditConfigId()); + runTaskVO.setTaskInfos(list); + setApprovalProperties( runTaskVO, auditRunTask.getMasterAuditConfigId()); + return runTaskVO; + } + + private void setApprovalProperties(FtbPersonnelsAuditRunTaskVO runTaskVO, String str) { + List carbonRecipientList = carbonRecipientService.lambdaQuery() + .eq(FtbPersonnelsAuditCarbonRecipient::getMasterConfig, str).list(); + List sendCranbonCopies = carbonRecipientList.stream().map(FtbPersonnelsAuditCarbonRecipient::coverFtbPersionnelsSendCranbonCopy).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(sendCranbonCopies)) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.in(FtbPersonnelsStaffRoster::getUserId,sendCranbonCopies.stream() + .map(FtbPersionnelsSendCranbonCopy::getCcId).distinct() + .collect(Collectors.toList())); + List staffRosters = staffRosterService.list(lambdaQuery); + Map map = staffRosters.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getName,(a, b)->a)); + sendCranbonCopies.forEach(item->{ + if (map.containsKey(item.getCcId())) item.setCcName(map.get(item.getCcId())); + }); + runTaskVO.setCopyRecipient(sendCranbonCopies); + } + } + + @Override + public List queryInfoThatCurrentUserNeedsToApprove(String userId, FtbPersonnelsCofigEnum ftbPersonnelsCofigEnum) { + + return baseMapper.queryInfoThatCurrentUserNeedsToApprove(userId,ftbPersonnelsCofigEnum.getConfigType()); + } + + @Override + public List queryMyApprovedReviewProcess(String userId, FtbPersonnelsCofigEnum ftbPersonnelsCofigEnum) { + return baseMapper.queryMyApprovedReviewProcess(userId,ftbPersonnelsCofigEnum.getConfigType()); + } + + + + /** + * 获取对应的主配置数据 + * @param mstRunTaskId 主配置id + * @return 主配置信息 + */ + private FtbPersonnelsAuditMasterConfig getMasterConfig(String mstRunTaskId) { + return configService.lambdaQuery() + .eq(SuperBaseEntity.SuperIBaseEntity::getId,mstRunTaskId).one(); + } + /** + * 是否开启审核流程 + * @param businessId 业务主键 + * @return FtbPersonnelsAuditRunTask 当前执行任务 + */ + private FtbPersonnelsAuditRunTask getFtbPersonnelsAuditRunTask(String businessId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsAuditRunTask::getBusinessId, businessId); + List list = new ArrayList<>(); + list.add(FtbPersonnelsAuditTaskEnum.PENDING.getCode()); + list.add(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + wrapper.in(FtbPersonnelsAuditRunTask::getExcuseStatus,list); + return baseMapper.selectOne(wrapper); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditSubConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditSubConfigServiceImpl.java new file mode 100644 index 0000000..b8747a6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditSubConfigServiceImpl.java @@ -0,0 +1,67 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditSubConfigVO; +import jnpf.model.personnels.dto.config.MasterConfigUserBoudDto; +import jnpf.model.personnels.po.FtbPersonnelsAuditSubConfig; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditInfoVO; +import jnpf.personnels.mapper.FtbPersonnelsAuditSubConfigMapper; +import jnpf.personnels.service.FtbPersonnelsAuditSubConfigService; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsAuditSubConfigServiceImpl extends ServiceImpl implements FtbPersonnelsAuditSubConfigService { + + @Autowired + private CultivatePerUtils cultivate; + + @Override + public FtbPersonnelsAuditInfoVO queryAuditSubConfig(String orgId, FtbPersonnelsCofigEnum cofigEnum) { + UserInfo user = UserProvider.getUser(); + String userId = user.getUserId(); + FtbPersonnelsAuditSubConfigVO auditSubConfigVO = baseMapper.queryAuditSubConfig(orgId, userId, cofigEnum.getConfigType()); + if (ObjectUtil.isEmpty(auditSubConfigVO)) { + return null; + } + return FtbPersonnelsAuditSubConfigVO.coverFtbPersonnelsAuditInfoVO(auditSubConfigVO); + } + + @Override + public List> isItAnApprover(String userId) { + List> itAnApprover = baseMapper.isItAnApprover(userId); + List orgId = itAnApprover.stream().map(item -> String.valueOf(item.get("orgId"))).collect(Collectors.toList()); + Map map = cultivate.convertOrganizationalId(orgId); + Map orgNames = cultivate.queryOrgNames(orgId); + itAnApprover.forEach(item -> { + Integer configType = Integer.valueOf(String.valueOf(item.get("configType"))); + String configName = FtbPersonnelsCofigEnum.getConfigName(configType); + String orgId1 = String.valueOf(item.get("orgId")); + item.put("orgId", map.get(orgId1)); + if (orgNames.containsKey(orgId1)) { + item.put("orgName", orgNames.get(orgId1)); + } + item.put("configType", configName); + }); + return itAnApprover; + } + + @Override + public List queryUserBound(String userId) { + return baseMapper.queryUserBound(userId); + } + + @Override + public Long checkWhetherTheCurrentPersonIsTheApprover(String userId) { + return baseMapper.checkWhetherTheCurrentPersonIsTheApprover(userId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditTaskInfoServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditTaskInfoServiceImpl.java new file mode 100644 index 0000000..13d82f5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuditTaskInfoServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsAuditTaskInfo; +import jnpf.model.personnels.vo.config.FtbPersonnelsAuditRunTaskInfo; +import jnpf.personnels.mapper.FtbPersonnelsAuditTaskInfoMapper; +import jnpf.personnels.service.FtbPersonnelsAuditTaskInfoService; +import org.springframework.stereotype.Service; + +import java.util.List; +@Service +public class FtbPersonnelsAuditTaskInfoServiceImpl extends ServiceImpl implements FtbPersonnelsAuditTaskInfoService { + + @Override + public List getRunTaskInfos(String id, String masterAuditConfigId) { + return baseMapper.getRunTaskInfos(id,masterAuditConfigId); + } + + @Override + public List getHisTaskInfos(String id, String masterAuditConfigId) { + return baseMapper.getHisTaskInfos(id,masterAuditConfigId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuthoritysServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuthoritysServiceImpl.java new file mode 100644 index 0000000..3638e1d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsAuthoritysServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsAuthoritys; +import jnpf.personnels.mapper.FtbPersonnelsAuthoritysMapper; +import jnpf.personnels.service.FtbPersonnelsAuthoritysService; +@Service +public class FtbPersonnelsAuthoritysServiceImpl extends ServiceImpl implements FtbPersonnelsAuthoritysService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistHistoryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistHistoryServiceImpl.java new file mode 100644 index 0000000..7776fac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistHistoryServiceImpl.java @@ -0,0 +1,28 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListHistoryDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistHistory; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListHistoryVO; +import jnpf.personnels.mapper.FtbPersonnelsBlacklistHistoryMapper; +import jnpf.personnels.service.FtbPersonnelsBlacklistHistoryService; +import org.springframework.stereotype.Service; + +/** + *

+ * 历史黑名单表 服务实现类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +@Service +public class FtbPersonnelsBlacklistHistoryServiceImpl extends ServiceImpl implements FtbPersonnelsBlacklistHistoryService { + + @Override + public Page listDropDown(Page page, FtbPersonnelsBlackListHistoryDTO dto) { + page = baseMapper.listDropDown(page, dto); + return page; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistServiceImpl.java new file mode 100644 index 0000000..a89cecf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistServiceImpl.java @@ -0,0 +1,149 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackListDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlackUpdateDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistAddDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklist; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistHistory; +import jnpf.model.personnels.vo.black.BlackTermEnum; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlackListVO; +import jnpf.permission.V2UserApi; +import jnpf.personnels.mapper.FtbPersonnelsBlacklistMapper; +import jnpf.personnels.service.FtbPersonnelsBlacklistHistoryService; +import jnpf.personnels.service.FtbPersonnelsBlacklistService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + *

+ * 黑名单 服务实现类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +@Service +public class FtbPersonnelsBlacklistServiceImpl extends ServiceImpl implements FtbPersonnelsBlacklistService { + + @Resource + private FtbPersonnelsBlacklistHistoryService ftbPersonnelsBlacklistHistoryService; + + @Autowired + private V2UserApi v2UserApi; + + @Override + public void addBlacklist(FtbPersonnelsBlacklistAddDTO ftbPersonnelsBlacklistAddDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsBlacklist::getUserId, ftbPersonnelsBlacklistAddDTO.getUserId()); + FtbPersonnelsBlacklist ftbPersonnelsBlacklist = baseMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbPersonnelsBlacklist)) { + doBlacklistVerification(ftbPersonnelsBlacklist.getType(), ftbPersonnelsBlacklist.getCreatorTime()); + } + FtbPersonnelsBlacklist result = ftbPersonnelsBlacklistAddDTO.convertFtbPersonnelsBlacklist(ftbPersonnelsBlacklistAddDTO); + save(result); + } + + @Override + public Boolean hasItBeenBlacklisted(String phone) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsBlacklist::getPhone, phone); + FtbPersonnelsBlacklist ftbPersonnelsBlacklist = baseMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbPersonnelsBlacklist)) { + try { + doBlacklistVerification(ftbPersonnelsBlacklist.getType(), ftbPersonnelsBlacklist.getCreatorTime()); + } catch (RuntimeException e) { + return true; + } + } + return false; + } + + @Override + @Transactional + public Page blacklist(Page page, FtbPersonnelsBlackListDTO dto) { + // 校验黑名单失效,然后移入历史黑名单中 + verificationBlacklistInvalid(); + page = baseMapper.blacklist(page, dto); + page.getRecords().forEach(item -> { + item.setExpirationTime(item.getType().calculateFailureTime(item.getCreateTime())); + }); + return page; + } + + @Override + public void editBlacklist(FtbPersonnelsBlackUpdateDTO fdto) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsBlacklist::getId, fdto.getId()) + .set(FtbPersonnelsBlacklist::getBlackTypeId, fdto.getBlackTypeId()) + .set(StrUtil.isNotBlank(fdto.getReason()), FtbPersonnelsBlacklist::getReason, fdto.getReason()) + .set(FtbPersonnelsBlacklist::getType, fdto.getType()); + baseMapper.update(new FtbPersonnelsBlacklist(), updateWrapper); + } + + @Override + @Transactional + public void removeBlacklist(String id) { + FtbPersonnelsBlacklist ftbPersonnelsBlacklist = baseMapper.selectById(id); + FtbPersonnelsBlacklistHistory ftbPersonnelsHistory = FtbPersonnelsBlacklistHistory.convert(ftbPersonnelsBlacklist); + ftbPersonnelsBlacklistHistoryService.save(ftbPersonnelsHistory); + baseMapper.deleteById(id); + } + + @Override + public List getBlacklist() { + return Arrays.stream(BlackTermEnum.values()).map(item -> { + FtbPersonnelsBlackListVO.BlackTermEnumVO blackTermEnumVO = new FtbPersonnelsBlackListVO.BlackTermEnumVO(); + blackTermEnumVO.setCode(item.getCode()); + blackTermEnumVO.setName(item.getName()); + return blackTermEnumVO; + }).collect(Collectors.toList()); + } + + private void verificationBlacklistInvalid() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsBlacklist::getEnableMark, 0); + List ftbPersonnelsBlacklists = baseMapper.selectList(queryWrapper); + Predicate predicate = ftbPersonnelsBlacklist -> { + BlackTermEnum blackTermEnum = BlackTermEnum.getByCode(ftbPersonnelsBlacklist.getType()); + Date calculateFailureTime = blackTermEnum.calculateFailureTime(ftbPersonnelsBlacklist.getCreatorTime()); + return calculateFailureTime != null && calculateFailureTime.getTime() < System.currentTimeMillis(); + }; + // 移入历史黑名单的数据 + List ftbPersonnelsBlacklistHistories = ftbPersonnelsBlacklists.stream() + .filter(predicate).map(FtbPersonnelsBlacklistHistory::convert).peek(a -> a.setStatus(0)).collect(Collectors.toList()); + // 原有黑名单失效的删除 + List blackListIds = ftbPersonnelsBlacklists.stream() + .filter(predicate).map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + ftbPersonnelsBlacklistHistoryService.saveBatch(ftbPersonnelsBlacklistHistories); + if (!CollectionUtils.isEmpty(blackListIds)) { + baseMapper.deleteBatchIds(blackListIds); + } + } + + private void doBlacklistVerification(Integer type, Date startDate) { + BlackTermEnum blackTermEnum = BlackTermEnum.getByCode(type); + Date calculateFailureTime = blackTermEnum.calculateFailureTime(startDate); + // 永久拉黑或者失效时间未过期 + if (calculateFailureTime == null || calculateFailureTime.getTime() > System.currentTimeMillis()) { + throw new RuntimeException("该用户已存在黑名单中!"); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistTypeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistTypeServiceImpl.java new file mode 100644 index 0000000..d18bb47 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBlacklistTypeServiceImpl.java @@ -0,0 +1,82 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistTypeAddDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistTypeUpdateDTO; +import jnpf.model.personnels.po.FtbPersonnelsBlacklistType; +import jnpf.model.personnels.vo.black.FtbPersonnelsBlacklistTypeListVO; +import jnpf.personnels.mapper.FtbPersonnelsBlacklistTypeMapper; +import jnpf.personnels.service.FtbPersonnelsBlacklistTypeService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + *

+ * 人事黑名单类型表 服务实现类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +@Service +public class FtbPersonnelsBlacklistTypeServiceImpl extends ServiceImpl implements FtbPersonnelsBlacklistTypeService { + + @Override + public void add(FtbPersonnelsBlacklistTypeAddDTO ftbPersonnelsBlacklistTypeAddDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsBlacklistType::getEnableMark, 0); + queryWrapper.eq(FtbPersonnelsBlacklistType::getRegularName, ftbPersonnelsBlacklistTypeAddDTO.getRegularName()); + if (baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("此黑名单类型已存在!"); + } + FtbPersonnelsBlacklistType ftbPersonnelBlacklistType = new FtbPersonnelsBlacklistType(); + ftbPersonnelBlacklistType.setStatus(0); + ftbPersonnelBlacklistType.setRegularName(ftbPersonnelsBlacklistTypeAddDTO.getRegularName()); + save(ftbPersonnelBlacklistType); + } + + @Override + public void delete(String id) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbPersonnelsBlacklistType::getEnableMark, 1); + baseMapper.update(new FtbPersonnelsBlacklistType(), updateWrapper); + } + + @Override + public void updateData(FtbPersonnelsBlacklistTypeUpdateDTO ftbPersonnelsBlacklistTypeUpdateDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.ne(SuperBaseEntity.SuperIBaseEntity::getId, ftbPersonnelsBlacklistTypeUpdateDTO.getId()); + queryWrapper.eq(FtbPersonnelsBlacklistType::getEnableMark, 0); + queryWrapper.eq(FtbPersonnelsBlacklistType::getRegularName, ftbPersonnelsBlacklistTypeUpdateDTO.getRegularName()); + if (baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("此黑名单类型已存在!"); + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbPersonnelsBlacklistTypeUpdateDTO.getId()); + updateWrapper.set(FtbPersonnelsBlacklistType::getRegularName, ftbPersonnelsBlacklistTypeUpdateDTO.getRegularName()); + baseMapper.update(new FtbPersonnelsBlacklistType(), updateWrapper); + } + + @Override + public List listData() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsBlacklistType::getEnableMark, 0); + queryWrapper.orderByDesc(SuperBaseEntity.SuperIBaseEntity::getId); + List ftbPersonnelsBlacklistTypes = baseMapper.selectList(queryWrapper); + return ftbPersonnelsBlacklistTypes.stream().map(a -> { + FtbPersonnelsBlacklistTypeListVO ftbPersonnelsBlacklistTypeListVO = new FtbPersonnelsBlacklistTypeListVO(); + ftbPersonnelsBlacklistTypeListVO.setId(a.getId()); + ftbPersonnelsBlacklistTypeListVO.setRegularName(a.getRegularName()); + return ftbPersonnelsBlacklistTypeListVO; + }).collect(Collectors.toList()); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBrainDrainServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBrainDrainServiceImpl.java new file mode 100644 index 0000000..9df4715 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsBrainDrainServiceImpl.java @@ -0,0 +1,775 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsRegularManagement; +import jnpf.model.personnels.po.FtbPersonnelsRuleConfig; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularManagementVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverStatisticVO; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.*; +import jnpf.personnels.service.FtbPersonnelsBrainDrainService; +import jnpf.personnels.service.FtbPersonnelsOverviewAnalysisService; +import jnpf.personnels.utils.CompanyAgeUtil; +import jnpf.personnels.utils.CovertDateUtils; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Author: peng.hao + * @create: 2025/4/11 + */ +@Service +public class FtbPersonnelsBrainDrainServiceImpl implements FtbPersonnelsBrainDrainService { + + + @Resource + private FtbPersonnelsOverviewAnalysisMapper ftbPersonnelsOverviewAnalysisMapper; + + @Autowired + V2UserApi v2UserApi; + + @Resource + FtbPersonnelsRegularManagementMapper regularManagementMapper; + + @Resource + FtbPersonnelsRuleConfigMapper ftbPersonnelsRuleConfigMapper; + + @Resource + FtbPersonnelsStaffEmploymentApplyMapper applyMapper; + + @Resource + FtbPersonnelsTurnoverManagementMapper turnoverManagementMapper; + + @Resource + V2PositionApi v2PositionApi; + + public static final List ON_THE_JOB_STATUS = List.of("302", "303", "304", "306"); + + + @Override + public FtbPersonnlesBrainDrainVO getBrainDrainCorrectRateComparison(FtbPersonnlesAnalysisDTO dto) { + FtbPersonnlesBrainDrainVO drainVO = new FtbPersonnlesBrainDrainVO(); + // 人事异动转正人数同比环比 + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return drainVO; + } + // 截止当前月转正人数 + Long aLong = generalQueryOfTheNumberOfRegulars(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds, dto.getOrganizationId()); + drainVO.setData(BigDecimal.valueOf(aLong)); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearDateStart = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + Date yearOnYearDateEnd = DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(); + Long yearOnYearDateValue = generalQueryOfTheNumberOfRegulars(yearOnYearDateStart, yearOnYearDateEnd, userIdListByOrganizeIds, dto.getOrganizationId()); + drainVO.setYearOnYear(FtbPersonnelsOverviewAnalysisService.computeDivisionSb(BigDecimal.valueOf(aLong).subtract(BigDecimal.valueOf(yearOnYearDateValue)), yearOnYearDateValue, 2)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chanelDateStart = DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(); + Date chainDateEnd = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + Long chainDateValue = generalQueryOfTheNumberOfRegulars(chanelDateStart, chainDateEnd, userIdListByOrganizeIds, dto.getOrganizationId()); + drainVO.setChainValue(FtbPersonnelsOverviewAnalysisService.computeDivisionSb(BigDecimal.valueOf(aLong).subtract(BigDecimal.valueOf(chainDateValue)), chainDateValue, 2)); + return drainVO; + } + + @Override + public FtbPersonnlesBrainDrainVO getBrainDrainCorrectRateNewcomerRate(FtbPersonnlesAnalysisDTO dto) { + FtbPersonnlesBrainDrainVO drainVO = new FtbPersonnlesBrainDrainVO(); + // 人事异动转正人数同比环比 + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return drainVO; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsRuleConfig::getEnableMark, 0); + FtbPersonnelsRuleConfig ruleConfig = ftbPersonnelsRuleConfigMapper.selectOne(wrapper); + Integer type = ruleConfig.getType(); + // 新人规则天数 + Integer timeRule = type == 1 ? ruleConfig.getTimeRule() : ruleConfig.getTimeRule() * 30; + List presenceOfPersonnel = getTheNumberOfPeopleInOffice(dto.getEndTime(), userIdListByOrganizeIds); + int aLong = presenceOfPersonnel.size(); + // 查询新人人员数 + BigDecimal newcomerNum = ftbPersonnelsOverviewAnalysisMapper.queryTheNumberOfNewcomers( + DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), + userIdListByOrganizeIds, timeRule); + // 本期数 + BigDecimal computeDivision = FtbPersonnelsOverviewAnalysisService.computeDivision(newcomerNum, aLong); + drainVO.setData(computeDivision); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearDateStart = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + Date yearOnYearDateEnd = DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(); + BigDecimal yearOnYearDateValue = ftbPersonnelsOverviewAnalysisMapper.queryTheNumberOfNewcomers(DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDateStart), DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDateEnd), userIdListByOrganizeIds, timeRule); + List presenceOfPersonnelY= getTheNumberOfPeopleInOffice(yearOnYearDateEnd, userIdListByOrganizeIds); + int aLongY = presenceOfPersonnelY.size(); + BigDecimal yearOnYear = FtbPersonnelsOverviewAnalysisService.computeDivision(yearOnYearDateValue, aLongY); + + drainVO.setYearOnYear(FtbPersonnelsOverviewAnalysisService.computeDivisionSb(computeDivision.subtract(yearOnYear), yearOnYear, 2)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chainDateStart = DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(); + Date chainDateEnd = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + BigDecimal chainDateValue = ftbPersonnelsOverviewAnalysisMapper.queryTheNumberOfNewcomers(DatePattern.NORM_MONTH_FORMAT.format(chainDateStart), + DatePattern.NORM_MONTH_FORMAT.format(chainDateEnd), + userIdListByOrganizeIds, + timeRule); + List presenceOfPersonnelC = getTheNumberOfPeopleInOffice(chainDateEnd, userIdListByOrganizeIds); + int aLongC = presenceOfPersonnelC.size(); + BigDecimal annulus = FtbPersonnelsOverviewAnalysisService.computeDivision(chainDateValue, aLongC); + drainVO.setChainValue(FtbPersonnelsOverviewAnalysisService.computeDivisionSb(computeDivision.subtract(annulus), annulus, 2)); + return drainVO; + } + + /** + * 在职人员 + * + * @param endTime + * @param userIdListByOrganizeIds + * @return + */ + private List getTheNumberOfPeopleInOffice(Date endTime, List userIdListByOrganizeIds) { + // 在职人员 + return ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(endTime) + , FtbPersonnelsOverviewAnalysisServiceImpl.ON_THE_JOB_STATUS, userIdListByOrganizeIds); + } + + @Override + public List getOnboardingPostDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + // 人事异动入职人数同比环比 + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + // 人事异动入职岗位维度 +// List applyList = +// applyMapper.getOnboardingPostDimension(DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), +// DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds); + List applyList = applyMapper.ListOfOnboardingPositions( + DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds); + List userIds = applyList.stream().map(FtbPersonnelsStaffEmploymentApply::getUserId).collect(Collectors.toList()); + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + List actionResultData = actionResult.getData(); + Map> stringListMap = actionResultData.stream().collect(Collectors.groupingBy(v->v.getPositionId()+":"+v.getPositionName())); + stringListMap.forEach((k, v) -> { + FtbPersonnelsPeoplePostRatioVO ratioVO = new FtbPersonnelsPeoplePostRatioVO(); + String[] split = k.split(":"); + String postId = split[0]; + ratioVO.setPostId(postId); + ratioVO.setPostName(split[1]); + Integer totalCount = getTotalCount(postId, dto.getStartTime(), dto.getOrganizationId()); + ratioVO.setTotalCount(totalCount); + int size = v.size(); + ratioVO.setCurrentCount(size); + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(size, totalCount)); + results.add(ratioVO); + }); + return results; + } + + @Override + public List getRegularPostDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + // 人事异动转正人数同比环比 + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + List managements = regularManagementMapper.queryUserInfoForRegularization( + DateUtil.beginOfMinute(dto.getStartTime()), + DateUtil.endOfMonth(dto.getStartTime()), userIdListByOrganizeIds + ); + Map> postMap = + managements.stream().collect(Collectors.groupingBy(item -> item.getRegularPostId() + ":" + item.getRegularPostName())); + postMap.forEach((k, v) -> { + FtbPersonnelsPeoplePostRatioVO ratioVO = new FtbPersonnelsPeoplePostRatioVO(); + String[] split = k.split(":"); + String postId = split[0]; + ratioVO.setPostId(postId); + ratioVO.setPostName(split[1]); + Integer totalCount = getTotalCount(postId, dto.getStartTime(), dto.getOrganizationId()); + ratioVO.setTotalCount(totalCount); + int size = v.size(); + ratioVO.setCurrentCount(size); + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(size, totalCount)); + results.add(ratioVO); + }); + return results; + } + + public Integer getTotalCount(String postId, Date startTime, List organizationId) { + QueryUserBatchDTO userBatchDTO = new QueryUserBatchDTO(); + userBatchDTO.setPositionIds(List.of(postId)); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(userBatchDTO); + if (userInfoBatch.getCode() == 200) { + DateTime dateTime = DateUtil.endOfMonth(startTime); + return Math.toIntExact(userInfoBatch.getData().stream().filter(v -> (v.getWorkStatusEnums() != null && v.getEntryDate() != null && + ON_THE_JOB_STATUS.contains(v.getWorkStatusEnums().getCode())) && + dateTime.after(v.getEntryDate()) && + organizationId.contains(v.getOrganizeId())).count()); + } + return 0; + } + + @Override + public List getTurnoverPostDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + List managements = turnoverManagementMapper.queryUserInfoForTurnover( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds + ); + List statisticVOS = managements.stream().map(item -> { + List dtos = JSONArray.parseArray(item.getOrganizationInfo(), WorkerGroupDataDto.class); + WorkerGroupDataDto dataDto = dtos.stream().filter(vo -> vo.getIsPrimaryPosition() || vo.getIsDefault()).findFirst().orElse(new WorkerGroupDataDto()); + FtbPersonnelsTurnoverStatisticVO turnoverStatisticVO = FtbPersonnelsTurnoverStatisticVO.covert(dataDto); + turnoverStatisticVO.setUserId(item.getUserId()); + turnoverStatisticVO.setUserName(item.getUserName()); + return turnoverStatisticVO; + }).collect(Collectors.toList()); + Map> postMap = + statisticVOS.stream().collect(Collectors.groupingBy(item -> item.getAffiliatedPosition() + ":" + item.getAffiliatedPositionName())); + postMap.forEach((k, v) -> { + FtbPersonnelsPeoplePostRatioVO ratioVO = new FtbPersonnelsPeoplePostRatioVO(); + String[] split = k.split(":"); + String postId = split[0]; + ratioVO.setPostId(postId); + ratioVO.setPostName(split[1]); + Integer totalCount = getTotalCount(postId, dto.getStartTime(), dto.getOrganizationId()); + ratioVO.setTotalCount(totalCount); + int size = v.size(); + ratioVO.setCurrentCount(size); + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(size, totalCount)); + results.add(ratioVO); + }); + return results; + } + + @Override + public List getOnboardingStoreDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + // 类型 1部门 2门店 3岗位 + Integer type = dto.getType(); + if (type == 3) { + List postDimension = getOnboardingPostDimension(dto); + return postDimension.stream().map(FtbPersonnelsPeopleInfoRatioVO::covert).collect(Collectors.toList()); + } + // 人事异动入职人数 门店维度,部门维度 + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + + // 人事异动入职人数 门店维度,部门维度 + List applyList = + applyMapper.ListOfOnboardingPositions(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds); + List userIds = applyList.stream().map(FtbPersonnelsStaffEmploymentApply::getUserId).collect(Collectors.toList()); + Map> groupingData = getTypeReturnsInfoGroupingData(type, userIds); + int totalCount = groupingData.values().stream().mapToInt(List::size).sum(); + groupingData.forEach((k, v) -> { + FtbPersonnelsPeopleInfoRatioVO ratioVO = new FtbPersonnelsPeopleInfoRatioVO(); + String[] split = k.split(":"); + // 名称 + ratioVO.setName(split[1]); + // 当前数量 + ratioVO.setCurrentCount(v.size()); + // List organizationId = getOrganizationId(List.of(key)); + // 比例 + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), totalCount)); + results.add(ratioVO); + }); + return results; + } + + @Override + public List getRegularizationStoreDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + // 类型 1部门 2门店 3岗位 + Integer type = dto.getType(); + if (type == 3) { + List postDimension = getRegularPostDimension(dto); + return postDimension.stream().map(FtbPersonnelsPeopleInfoRatioVO::covert).collect(Collectors.toList()); + } + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + List userIds = regularManagementMapper.getRegularizationStoreDimension( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds); + Map> groupingData = getTypeReturnsInfoGroupingData(type, userIds); + int totalCount = groupingData.values().stream().mapToInt(List::size).sum(); + groupingData.forEach((k, v) -> { + FtbPersonnelsPeopleInfoRatioVO ratioVO = new FtbPersonnelsPeopleInfoRatioVO(); + String[] split = k.split(":"); + // 名称 + ratioVO.setName(split[1]); + String key = split[0]; + checkTheNumberOfPositions(type, key, ratioVO); + // 当前数量 + ratioVO.setCurrentCount(v.size()); + // 比例 + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), totalCount)); + results.add(ratioVO); + }); + return results; + } + + @Override + public List getTurnoverStoreDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + // 类型 1部门 2门店 3岗位 + Integer type = dto.getType(); + if (type == 3) { + List postDimension = getTurnoverPostDimension(dto); + return postDimension.stream().map(FtbPersonnelsPeopleInfoRatioVO::covert).collect(Collectors.toList()); + } + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + List managements = turnoverManagementMapper.queryUserInfoForTurnover( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds + ); + // 离职人员 + List userIds = managements.stream().map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + Map> groupingData = getTypeReturnsInfoGroupingData(type, userIds); + int totalCount = groupingData.values().stream().mapToInt(List::size).sum(); + groupingData.forEach((k, v) -> { + FtbPersonnelsPeopleInfoRatioVO ratioVO = new FtbPersonnelsPeopleInfoRatioVO(); + String[] split = k.split(":"); + // 名称 + ratioVO.setName(split[1]); + String key = split[0]; + checkTheNumberOfPositions(type, key, ratioVO); + // 当前数量 + ratioVO.setCurrentCount(v.size()); + // 比例 + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), totalCount)); + results.add(ratioVO); + }); + return results; + } + + /** + * 扩展人员岗位总数 + * + * @param type + * @param key + * @param ratioVO + */ + private void checkTheNumberOfPositions(Integer type, String key, FtbPersonnelsPeopleInfoRatioVO ratioVO) { + if (type == 3) { + QueryUserBatchDTO userBatchDTO = new QueryUserBatchDTO(); + userBatchDTO.setPositionIds(List.of(key)); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(userBatchDTO); + if (userInfoBatch.getCode() == 200) { + ratioVO.setTotalCount(userInfoBatch.getData().size()); + } + } + } + + @Override + public List getNewHireRateDimension(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + if (dto.getType() == 3) { + List postDimension = getNewHireRateDimensionPosition(dto); + return postDimension.stream().map(FtbPersonnelsPeopleInfoRatioVO::covert).collect(Collectors.toList()); + } + // 新员工定义配置 + FtbPersonnelsRuleConfig ruleConfig = ftbPersonnelsRuleConfigMapper.selectById(1); + // 新员工离职人数 + Integer timeRule = ruleConfig.getTimeRule(); + Integer type = ruleConfig.getType(); + // 天数 + timeRule = type == 2 ? timeRule * 30 : timeRule; + // 查询新人人员数 + List newcomerNumUserList = ftbPersonnelsOverviewAnalysisMapper.queryTheNewcomerID( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), + userIdListByOrganizeIds, timeRule); + Integer dtoType = dto.getType(); + Map> groupingData = getTypeReturnsInfoGroupingData(dtoType, newcomerNumUserList); + int totalCount = groupingData.values().stream().mapToInt(List::size).sum(); + groupingData.forEach((k, v) -> { + FtbPersonnelsPeopleInfoRatioVO ratioVO = new FtbPersonnelsPeopleInfoRatioVO(); + String[] split = k.split(":"); + // 名称 + ratioVO.setName(split[1]); + // 当前数量 + ratioVO.setCurrentCount(v.size()); + List organizationId = getOrganizationId(List.of(split[0])); + // 比例 + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), organizationId.size())); + results.add(ratioVO); + }); + return results; + } + + private List getNewHireRateDimensionPosition(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return results; + } + // 新员工定义配置 + FtbPersonnelsRuleConfig ruleConfig = ftbPersonnelsRuleConfigMapper.selectById(1); + // 新员工离职人数 + Integer timeRule = ruleConfig.getTimeRule(); + Integer type = ruleConfig.getType(); + // 天数 + timeRule = type == 2 ? timeRule * 30 : timeRule; + // 查询新人人员数 + List newcomerNumUserList = ftbPersonnelsOverviewAnalysisMapper.queryTheNewcomerID( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), + userIdListByOrganizeIds, timeRule); + Map> postMap = getTypeReturnsInfoGroupingData(dto.getType(), newcomerNumUserList); + postMap.forEach((k, v) -> { + FtbPersonnelsPeoplePostRatioVO ratioVO = new FtbPersonnelsPeoplePostRatioVO(); + String[] split = k.split(":"); + String id = split[0]; + // 名称 + ratioVO.setPostName(split[1]); + // 当前数量 + ratioVO.setCurrentCount(v.size()); + Integer totalCount = getTotalCount(id, dto.getStartTime(), dto.getOrganizationId()); + ratioVO.setTotalCount(totalCount); + // 比例 + ratioVO.setRatio(FtbPersonnelsOverviewAnalysisService.computeDivision(v.size(), totalCount)); + results.add(ratioVO); + }); + return results; + } + + /** + * 根据不同type进行统计 + * + * @param dtoType 1部门 2门店 3岗位 + * @param newcomerNumUserList + * @return + */ + public Map> getTypeReturnsInfoGroupingData(Integer dtoType, List newcomerNumUserList) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(newcomerNumUserList, ""); + if (actionResult.getCode() != 200) { + throw new RuntimeException("获取用户主岗信息失败"); + } + List data = actionResult.getData(); + // 类型 1部门 2门店 3岗位 + Map> stringListMap = null; + switch (dtoType) { + case 1: + stringListMap = data.stream().filter(v -> OrganizeCategoryEnums.DEPARTMENT.getCode().equals(v.getOrganizeCategory())).collect(Collectors.groupingBy(v -> v.getOrganizeId() + ":" + v.getOrganizeName())); + break; + case 2: + stringListMap = data.stream().filter(v -> OrganizeCategoryEnums.STORE.getCode().equals(v.getOrganizeCategory())).collect(Collectors.groupingBy(v -> v.getOrganizeId() + ":" + v.getOrganizeName())); + break; + case 3: + stringListMap = data.stream().filter(v->StringUtils.isNotEmpty(v.getPositionName())).collect(Collectors.groupingBy(v -> v.getPositionId() + ":" + v.getPositionName())); + break; + } + return stringListMap; + } + + /** + * 所选组织和岗位员工 + */ + private List getOrganizationId(List organizationId) { + QueryPageUserDTO queryPageUserDTO = new QueryPageUserDTO(); + queryPageUserDTO.setIsPage(false); + queryPageUserDTO.setOrganizeIds(organizationId); + queryPageUserDTO.setHaveChildOrganizeId(false); + ActionResult> pageListVOActionResult = v2UserApi.pagePost(queryPageUserDTO); + if (pageListVOActionResult.getCode() == 200 && Objects.nonNull(pageListVOActionResult.getData())) { + return pageListVOActionResult.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + return null; + } + + /** + * 人事异动转正人数 + * + * @param startTime + * @param endTime + * @param userIdListByOrganizeIds + * @param organizationId + * @return + */ + private Long generalQueryOfTheNumberOfRegulars(Date startTime, Date endTime, List userIdListByOrganizeIds, List organizationId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsRegularManagement::getUserId, userIdListByOrganizeIds); + wrapper.eq(FtbPersonnelsRegularManagement::getEnableMark, 0); + wrapper.in(FtbPersonnelsRegularManagement::getTransferStatus, 2, 6); + wrapper.in(FtbPersonnelsRegularManagement::getOrgId, organizationId); + wrapper.between(FtbPersonnelsRegularManagement::getActualConverDate, DateUtil.beginOfMinute(startTime), + DateUtil.endOfMonth(endTime)); + return regularManagementMapper.selectCount(wrapper); + } + @Override + public PageListVO getOnboardingDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取入职人员信息 + List applyList = applyMapper.ListOfOnboardingPositions( + DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds); + // 获取用户信息 + List userIds = applyList.stream().map(FtbPersonnelsStaffEmploymentApply::getUserId).collect(Collectors.toList()); + List data = getUserBoundVOS(dto, userIds); + if (CollUtil.isEmpty(data)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + Map userMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, user -> user)); + Map applyMap = applyList.stream().collect(Collectors.toMap(FtbPersonnelsStaffEmploymentApply::getUserId, user -> user)); + // 转换为明细VO + for (UserBoundVO apply : data) { + FtbPersonnelsOnboardingDetailVO detailVO = new FtbPersonnelsOnboardingDetailVO(); + UserBoundVO user = userMap.get(apply.getId()); + if (user != null) { + detailVO.setName(user.getUserName()); + detailVO.setEmployeeId(user.getSystemWorkerId()); + detailVO.setOrganization(user.getOrganizeName()); + detailVO.setJobLevel(user.getPositionName()); + detailVO.setJobGrade(user.getGradeName()); + } + if (applyMap.containsKey(apply.getId())){ + FtbPersonnelsStaffEmploymentApply employmentApply = applyMap.get(apply.getId()); + detailVO.setEntryDate( CovertDateUtils.convertToStringDate(employmentApply.getActualStartDate())); + } + results.add(detailVO); + } + results.sort(Comparator.comparing(FtbPersonnelsOnboardingDetailVO::getOrganization, Comparator.nullsLast(Comparator.naturalOrder()))); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + @Override + public PageListVO getRegularizationDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取转正人员信息 + List managements = regularManagementMapper.queryUserInfoForRegularization( + DateUtil.beginOfMinute(dto.getStartTime()), + DateUtil.endOfMonth(dto.getStartTime()), userIdListByOrganizeIds + ); + // 获取用户信息 + List userIds = managements.stream().map(FtbPersonnelsRegularManagementVO::getUserId).collect(Collectors.toList()); + List data = getUserBoundVOS(dto, userIds); + if (CollUtil.isEmpty(data)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + Map userMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, user -> user)); + // 转换为明细VO + for (FtbPersonnelsRegularManagementVO management : managements) { + FtbPersonnelsRegularizationDetailVO detailVO = new FtbPersonnelsRegularizationDetailVO(); + UserBoundVO user = userMap.get(management.getUserId()); + if (user != null) { + detailVO.setName(user.getUserName()); + detailVO.setEmployeeId(user.getSystemWorkerId()); + detailVO.setOrganization(user.getOrganizeName()); + detailVO.setJobLevel(user.getPositionName()); + detailVO.setJobGrade(user.getGradeName()); + } + detailVO.setRegularizationDate( CovertDateUtils.convertToStringDate(management.getActualConverDate())); + results.add(detailVO); + } + results.sort(Comparator.comparing(FtbPersonnelsRegularizationDetailVO::getOrganization, Comparator.nullsLast(Comparator.naturalOrder()))); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + + /** + * 获取组织下员工ID集合 + * + * @param orgIds + * @return + */ + private List doEmployeesOfYourOrganization(List orgIds, String[]... postId) { + if (CollUtil.isNotEmpty(orgIds)) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark, 0); + wrapper.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition, "305"); + List managements = turnoverManagementMapper.selectList(wrapper); + List postIds; + if (ObjectUtil.isNotEmpty(postId)) { + postIds = Arrays.stream(postId).flatMap(Arrays::stream).collect(Collectors.toList()); + } else { + postIds = null; + } + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isEmpty(postIds)) + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty(JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()).stream().filter(orgIds::contains).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isNotEmpty(postIds)) + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty( + JSONObject.parseArray( + vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().filter(po -> { + if (!orgIds.contains(po.getAffiliatedOrg())) return false; + assert postIds != null; + return postIds.contains(po.getAffiliatedPosition()); + }).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + } + return null; + } + + @Override + public PageListVO getNewHireRateDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = getOrganizationId(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取新人规则天数 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsRuleConfig::getEnableMark, 0); + FtbPersonnelsRuleConfig ruleConfig = ftbPersonnelsRuleConfigMapper.selectOne(wrapper); + if (ruleConfig == null) { + return CultivatePage.paginate(results, new PaginationVO()); + } + Integer type = ruleConfig.getType(); + Integer timeRule = type == 1 ? ruleConfig.getTimeRule() : ruleConfig.getTimeRule() * 30; + + // 查询新人人员信息 + List applyList = ftbPersonnelsOverviewAnalysisMapper.queryTheNumberOfNewcomersList( + DatePattern.NORM_MONTH_FORMAT.format(dto.getStartTime()), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), + userIdListByOrganizeIds, + timeRule); + + // 获取用户信息 + List userIds = applyList.stream().map(FtbPersonnelsStaffEmploymentApply::getUserId).collect(Collectors.toList()); + List data = getUserBoundVOS(dto, userIds); + if (CollUtil.isEmpty(data)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + Map userMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, user -> user)); + // 转换为明细 VO + for (FtbPersonnelsStaffEmploymentApply apply : applyList) { + FtbPersonnelsNewHireRateDetailVO detailVO = new FtbPersonnelsNewHireRateDetailVO(); + UserBoundVO user = userMap.get(apply.getUserId()); + if (user != null) { + detailVO.setName(user.getUserName()); + detailVO.setEmployeeId(user.getSystemWorkerId()); + detailVO.setOrganization(user.getOrganizeName()); + detailVO.setJobLevel(user.getPositionName()); + detailVO.setJobGrade(user.getGradeName()); + // 计算司龄(月) + if (apply.getActualStartDate() != null) { + String string = CompanyAgeUtil.calculateSeniority(apply.getActualStartDate(), new Date()); + detailVO.setSeniority(string); + } + } + results.add(detailVO); + } + results.sort(Comparator.comparing(FtbPersonnelsNewHireRateDetailVO::getOrganization, Comparator.nullsLast(Comparator.naturalOrder()))); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + private List getUserBoundVOS(FtbPersonnlesAnalysisDTO dto, List userIds) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + if (actionResult.getCode() != 200) { + return null; + } + List data = actionResult.getData(); + // 1部门 2门店 3岗位 + if (dto.getType() == 1) { + data = data.stream().filter(v -> OrganizeCategoryEnums.DEPARTMENT.getCode().equals(v.getOrganizeCategory())).collect(Collectors.toList()); + } else if (dto.getType() == 2) { + data = data.stream().filter(v -> OrganizeCategoryEnums.STORE.getCode().equals(v.getOrganizeCategory())).collect(Collectors.toList()); + } + return data; + } + + @Override + public void getNewHireRateDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getNewHireRateDetail(dto).getList(); + exportExcel(response, "新人率占比明细", dataList, FtbPersonnelsNewHireRateDetailVO.class); + } + + + @Override + public void getOnboardingDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getOnboardingDetail(dto).getList(); + exportExcel(response, "入职明细", dataList, FtbPersonnelsOnboardingDetailVO.class); + } + + @Override + public void getRegularizationDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getRegularizationDetail(dto).getList(); + exportExcel(response, "转正明细", dataList, FtbPersonnelsRegularizationDetailVO.class); + } + + /** + * 导出 Excel 文件 + * + * @param response HTTP 响应 + * @param fileName 文件名 + * @param dataList 数据列表 + * @param clazz VO 类 + */ + @SneakyThrows + private void exportExcel(HttpServletResponse response, String fileName, List dataList, Class clazz) { + EasyExcelUtils.exportExcel(response, fileName, dataList,clazz); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsContactInfoServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsContactInfoServiceImpl.java new file mode 100644 index 0000000..a30c83f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsContactInfoServiceImpl.java @@ -0,0 +1,404 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.ContractSignPersonnelNoticeApi; +import jnpf.model.enums.ExamConfigEnums; +import jnpf.model.enums.FContractSignStatus; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import cn.hutool.json.JSONUtil; +import jnpf.model.enums.FContractSignStatus; +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.personnels.service.FtbPersonnelsContactInfoService; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.NoSendContactIMUtils; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelStaffUtils; +import jnpf.vo.ContractSignPersonnelNoticeConfigVo; +import jnpf.personnels.utils.PersonnelOrgUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Service +@Slf4j +public class FtbPersonnelsContactInfoServiceImpl implements FtbPersonnelsContactInfoService { + + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private NoSendContactIMUtils noSendContactIMUtils; + + @Autowired + private ContractSignPersonnelNoticeApi contractSignPersonnelNoticeApi; + + + @Override + @Transactional + public void syncContactInfo(ContactStatusInfo info) { + log.error("syncContactInfo data = {}", JSONUtil.toJsonStr(info)); + ContactStatusInfo.ContactFieldInfo contactFieldInfo = info.getContactFieldInfo(); + String userId = info.getUserId(); + FContractSignStatus fContractSignStatus = FContractSignStatus.fromCode(info.getContractStatus()); + if (null == fContractSignStatus) { + throw new RuntimeException("合同签署状态不正确"); + } + //查询用户 + FtbPersonnelsStaffRoster entity = staffRosterService.queryRosterInfoByUserId(userId); + if (null == entity) { + throw new RuntimeException("用户不存在"); + } + //修改花名册主表 + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + wrapper.set(FtbPersonnelsStaffRoster::getContractStatus, info.getContractStatus()); + + if (null != contactFieldInfo) { + if (StringUtils.isNotEmpty(contactFieldInfo.getContractType())) { + wrapper.set(FtbPersonnelsStaffRoster::getContractType, contactFieldInfo.getContractType()); + } else { + wrapper.set(FtbPersonnelsStaffRoster::getContractType, ""); + } + if (StringUtils.isNotEmpty(contactFieldInfo.getContractTypeName())) { + wrapper.set(FtbPersonnelsStaffRoster::getContractTypeName, contactFieldInfo.getContractTypeName()); + } else { + wrapper.set(FtbPersonnelsStaffRoster::getContractTypeName, ""); + } + if (StringUtils.isNotEmpty(contactFieldInfo.getContractEffectiveDate())) { + wrapper.set(FtbPersonnelsStaffRoster::getContractEffectiveDate, personnelOrgUtils.stringDateToDate(contactFieldInfo.getContractEffectiveDate())); + } else { + wrapper.set(FtbPersonnelsStaffRoster::getContractEffectiveDate, null); + } + if (StringUtils.isNotEmpty(contactFieldInfo.getContractDate())) { + wrapper.set(FtbPersonnelsStaffRoster::getContractEndDate, personnelOrgUtils.stringDateToDate(contactFieldInfo.getContractDate())); + } else { + wrapper.set(FtbPersonnelsStaffRoster::getContractEndDate, null); + } + + if (StringUtils.isNotEmpty(contactFieldInfo.getContractCompany())) { + wrapper.set(FtbPersonnelsStaffRoster::getContractCompany, contactFieldInfo.getContractCompany()); + } else { + wrapper.set(FtbPersonnelsStaffRoster::getContractCompany, ""); + } + } + + wrapper.eq(FtbPersonnelsStaffRoster::getUserId, userId); + wrapper.eq(FtbPersonnelsStaffRoster::getId, entity.getId()); + //人事花名册列表只展示劳务合同 + if(info.getIsPersonal().equals(1)) { + staffRosterService.update(wrapper); + } + + //修改花名册附表 + + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractStatus", fContractSignStatus.getCode().toString()); + + if (null != contactFieldInfo) { + + if (StringUtils.isNotEmpty(contactFieldInfo.getContractCompany())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractCompany", contactFieldInfo.getContractCompany()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractCompany", ""); + } + if (StringUtils.isNotEmpty(contactFieldInfo.getContractEffectiveDate())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractEffectiveDate", contactFieldInfo.getContractEffectiveDate()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractEffectiveDate", ""); + } + if (StringUtils.isNotEmpty(contactFieldInfo.getContractName())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractName", contactFieldInfo.getContractName()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractName", ""); + } + if (StringUtils.isNotEmpty(contactFieldInfo.getContractNum())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractNum", contactFieldInfo.getContractNum()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractNum", "0"); + } + + if (StringUtils.isNotEmpty(contactFieldInfo.getContractType())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractType", contactFieldInfo.getContractType()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractType", ""); + } + + if (CollectionUtil.isNotEmpty(contactFieldInfo.getContractFile())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractFile", JSONUtil.toJsonStr(contactFieldInfo.getContractFile())); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractFile", ""); + } + + if (StringUtils.isNotEmpty(contactFieldInfo.getContractDate())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractDate", contactFieldInfo.getContractDate()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractDate", ""); + } + + if (StringUtils.isNotEmpty(contactFieldInfo.getContractDetail())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractDetail", contactFieldInfo.getContractDetail()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractDetail", ""); + } + + if (StringUtils.isNotEmpty(contactFieldInfo.getContractTaskId())) { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractTaskId", contactFieldInfo.getContractTaskId()); + } else { + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "contractTaskId", ""); + } + + + } + + } + + @Override + public void checkNotSendContactSign(String tenantId) { + //todo调用接口查询通知配置 + ContractSignPersonnelNoticeConfigVo config = contractSignPersonnelNoticeApi.config(tenantId); + log.error("ContractSignPersonnelNoticeConfigVo={}", JSONUtil.toJsonStr(config)); + if (null == config) { + return; + } + //启用状态 0启用 1禁用 + if (config.getEnable().equals(1)) { + return; + } + if (CollectionUtil.isEmpty(config.getNoticePersonList())) { + return; + } + List userIds = new ArrayList<>(); + for (ContractSignPersonnelNoticeConfigVo.NoticePerson noticePerson : config.getNoticePersonList()) { + userIds.add(noticePerson.getUserId()); + } + if (CollectionUtil.isEmpty(userIds)) { + return; + } + if (StringUtils.isEmpty(config.getNoticeTime())) { + return; + } + // 解析时间 + String[] split = config.getNoticeTime().split(":"); + int alertHour = Integer.valueOf(split[0]); + LocalTime now = LocalTime.now(); + int currHour = now.getHour(); + if (currHour != alertHour) { + return; + } + + //查询用户信息 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda() + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus,StaffWorkerStatus.RESIGNED.getCode(),StaffWorkerStatus.PENDING_RESIGNATION.getCode()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = staffRosterService.list(rosterWrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return; + } + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus,StaffWorkerStatus.RESIGNED.getCode(),StaffWorkerStatus.PENDING_RESIGNATION.getCode()) + .eq(FtbPersonnelsStaffRoster::getContractStatus, FContractSignStatus.NOT_INITIATED.getCode()); + List msgRosterList = staffRosterService.list(wrapper); + if (CollectionUtil.isEmpty(msgRosterList)) { + return; + } + Date currDate = new Date(); + List sendRosterList = new ArrayList<>(); + Date today = PersonnelStaffUtils.getTodayDate(); + String strToday = DateUtil.format(today, "yyyy-MM-dd"); + for (FtbPersonnelsStaffRoster roster : msgRosterList) { + Date endDate = roster.getActualStartDate(); + if(endDate==null){ + continue; + } + if (currDate.after(endDate)) { + long betweenDay = DateUtil.betweenDay(endDate, today, false); + if (config.getNoticeFrequency().equals(1)) {//提醒频率 0:每7天提醒 1:每天提醒 + sendRosterList.add(roster); + } else if (config.getNoticeFrequency().equals(0)) { + //每7天提醒 + if (checkContactStatus(endDate, today, strToday)) { + sendRosterList.add(roster); + } + } + } + } + if (CollectionUtil.isNotEmpty(sendRosterList)) { + sendAlertMsg(tenantId, sendRosterList, rosterList); + } + } + + + public static Boolean checkContactStatus(Date endDate, Date today, String strToday) { + String strEnd = DateUtil.format(endDate, "yyyy-MM-dd"); + if (strEnd.equals(strToday)) { + return false; + } + int num = 0; + while (true) { + if (num <= 0) { + endDate = DateUtil.offsetDay(endDate, 6); + } else { + endDate = DateUtil.offsetDay(endDate, 7); + } + if (endDate.after(today)) { + break; + } + strEnd = DateUtil.format(endDate, "yyyy-MM-dd"); + + if (strToday.equals(strEnd)) { + return true; + } + num++; + } + return false; + } + + private void sendAlertMsg(String tenantId, List msgRosterList, List toRosterList) { + noSendContactIMUtils.sendMsg(tenantId, msgRosterList, toRosterList); + } + + + @Override + public void FileListZip(String rootDir, String userId, HttpServletResponse response) { + FtbPersonnelsStaffRoster oldRoster = staffRosterService.queryRosterInfoByUserId(userId); + if (null == oldRoster) { + throw new RuntimeException("未查询到员工信息"); + } + FtbPersonnelsStaffRegistrationFormData formData = registrationFormDataService.queryOneFieldForFieldId(oldRoster.getId(), "contractDetail"); + FtbPersonnelsStaffRegistrationFormData contractFile = registrationFormDataService.queryOneFieldForFieldId(oldRoster.getId(), "contractFile"); + + List contactList = new ArrayList<>(); + if (StringUtils.isNotEmpty(formData.getValue()) && !"[]".equals(formData.getValue())) { + contactList = JSONUtil.toList(formData.getValue(), ContactStatusInfo.ContactInfoFile.class); + } + if(StringUtils.isNotEmpty(contractFile.getValue()) && !contractFile.getValue().equals("[]")) { + if (!contractFile.getValue().contains("{")) { + List fileList = convertOldContractFile(contractFile.getValue()); + if(CollectionUtil.isNotEmpty(fileList)){ + contactList.addAll(fileList); + } + }else{ + contactList.addAll(JSONUtil.toList(contractFile.getValue(), ContactStatusInfo.ContactInfoFile.class)); + } + } + if(CollectionUtil.isEmpty(contactList)){ + throw new RuntimeException("未查询到员工合同信息"); + } + + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); + for (ContactStatusInfo.ContactInfoFile file : contactList) { + if(!file.getUrl().startsWith("http")){ + continue; + } + URL url = null; + try { + url = new URL(file.getUrl()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + InputStream inputStream = connection.getInputStream(); + String forname = ""; + if (StringUtils.isNotEmpty(file.getName())) { + forname = file.getName(); + } else { + forname = file.getUrl().substring(file.getUrl().lastIndexOf("/") + 1); + } + zipOutputStream.putNextEntry(new ZipEntry(forname)); + byte[] bytes = IoUtil.readBytes(inputStream); + IOUtils.write(bytes, zipOutputStream); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + try { + zipOutputStream.flush(); + zipOutputStream.closeEntry(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + + String zipName = rootDir + ".zip"; + try { + //生成zip文件 +// HttpServletResponse response = ServletUtil.getResponse(); + byte[] dateArr = outputStream.toByteArray(); + response.reset(); +// response.addHeader("Access-Control-Allow-Origin", "*"); +// response.addHeader("Access-Control-Expose-Headers", "Content-Disposition"); +// response.setHeader("Content-Disposition", "attachment; filename=\"" + zipName + "\""); + response.addHeader("Content-Length", "" + dateArr.length); +// response.setContentType("application/octet-stream; charset=UTF-8"); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(zipName, "UTF-8")); + IOUtils.write(dateArr, response.getOutputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private List convertOldContractFile(String formValueStr) { + List fileList = new ArrayList<>(); + List list = JSONUtil.toList(formValueStr, String.class); + if (CollectionUtil.isNotEmpty(list)) { + for (String urlPath : list) { + Path path = Paths.get(urlPath); + String fileName = path.getFileName().toString(); + // 分离扩展名 + int dotIndex = fileName.lastIndexOf('.'); + String extension = dotIndex == -1 ? "" : fileName.substring(dotIndex+1); + ContactStatusInfo.ContactInfoFile file = new ContactStatusInfo.ContactInfoFile(); + file.setName(fileName); + file.setUrl(urlPath); + file.setFileExtension(extension); + fileList.add(file); + } + } + return fileList; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmEntryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmEntryServiceImpl.java new file mode 100644 index 0000000..fad2662 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmEntryServiceImpl.java @@ -0,0 +1,1785 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.google.common.collect.ImmutableMap; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationResponse; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.account.PTenantAccountApi; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.engine.FlowTaskApi; +import jnpf.engine.entity.FlowTaskEntity; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.enums.*; +import jnpf.model.personnels.bo.ExportRosterOneVo; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.emp.FtbEmpAddNewDTO; +import jnpf.model.personnels.dto.emp.FtbEmpConfirmDTO; +import jnpf.model.personnels.dto.emp.FtbEmpEntryDTO; +import jnpf.model.personnels.dto.emp.FtbEmpQueryDTO; +import jnpf.model.personnels.dto.oa.RequestForOA; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.staff.field.ExportFormTypeDto; +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.StaffBaseInfoDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.dto.staff.salarylog.AddSalaryChangeLogDto; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.req.roster.UserAccountDto; +import jnpf.model.personnels.vo.emp.FtbEmpAddNewVO; +import jnpf.model.personnels.vo.emp.FtbEmpConfirmVO; +import jnpf.model.personnels.vo.emp.FtbEmpEntryVO; +import jnpf.model.personnels.vo.emp.FtbEmpResultVO; +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.permission.V2GradesApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.UserBoundInfoDTO; +import jnpf.permission.dto.v2.grades.QueryGradeListDTO; +import jnpf.permission.dto.v2.organzie.QueryOrganizeListTargetTypesDTO; +import jnpf.permission.dto.v2.user.QueryListUserDTO; +import jnpf.permission.dto.v2.user.QueryUserBoundDTO; +import jnpf.permission.dto.v2.user.SaveUserDTO; +import jnpf.permission.dto.v2.user.UpdateUserDTO; +import jnpf.permission.entity.UserCopyEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.JobStatusEnum; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionBaseInfoVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.config.EnvironmentParamConfig; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.msg.PersonnelsConsumerSourceMsg; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.*; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.*; +import jnpf.util.context.ThreadContext; +import jnpf.util.excel.EasyExcelUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @Author: peng.hao + * @create: 2025/4/7 + */ +@Slf4j +@Service +public class FtbPersonnelsEmEntryServiceImpl implements FtbPersonnelsEmEntryService { + + @Resource + FtbPersonnelsStaffEmploymentApplyMapper applyMapper; + + @Autowired + V2UserApi v2UserApi; + + @Autowired + V2OrganizeApi v2OrganizeApi; + + @Autowired + V2PositionApi v2PositionApi; + + @Autowired + V2GradesApi v2GradesApi; + + @Autowired + private PTenantAccountApi pTenantAccountApi; + + @Resource + private FtbPersonnelsStaffRosterService rosterService; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + private FtbPersonnelsStaffSalaryChangeLogService salaryChangeLogService; + + @Autowired + PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private FtbPersonnelsRegularManagementService regularManagementService; + + @Autowired + private PersonnelAsyncServiceUtils personnelAsyncServiceUtils; + + @Autowired + private SmsSendUtil smsSendUtil; + + @Resource + private PermissionsUtils permissionsUtils; + + @Resource + private FtbPersonnlesIMUtils ftbPersonnlesIMUtils; + + @Resource + FtbPersonnelsSalaryService personnelSalaryService; + + @Resource + FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Resource + ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Autowired + FlowTaskApi flowTaskApi; + + @Resource + private FtbPersonnelsEmployeeTypeService employeeTypeService; + + @Resource + private FtbPersonnelsRegistrationFormFieldOptionMapper formFieldOptionMapper; + + @Autowired + QuerySalaryApi querySalaryApi; + + @Resource + PersonnelsConsumerSourceMsg secondmentConsumerSourceMsg; + + @Autowired + EnvironmentParamConfig environmentParamConfig; + + + @Autowired + private SmsConfig smsConfig; + + + @Autowired + private RosterExportUtils rosterExportUtils; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionService fieldOptionService; + + @Autowired + PersonnelIdCardVerificationUtils idCardVerificationUtils; + + @Resource + private CertificateInstanceService certificateInstanceService; + + private static final String DEFAULT_USER_LOGO = "001.png"; + + @Override + public PageListVO pageLists(FtbEmpEntryDTO empEntryDTO, String app) { + Page page = empEntryDTO.coverCultivatePage(); + Boolean isAdministrator = UserProvider.getUser().getIsAdministrator(); + if(!isAdministrator){ + List workStatusEnums = Arrays.stream(UserWorkStatusEnums.values()).filter(v -> v.getCode().equals(UserWorkStatusEnums.RESIGNED.getCode())).collect(Collectors.toList()); + List userIds = permissionsUtils.getPersonnelUserIdDataPermissions(UserProvider.getUser().getUserId(),workStatusEnums); + if (userIds != null && userIds.isEmpty()) return CultivatePage.coverPageList(page); + if (CollUtil.isNotEmpty(userIds)) { + empEntryDTO.setUserIds(userIds); + } + } + String keyWords = empEntryDTO.getKeyWords(); + if (StringUtils.isNotEmpty(keyWords)){ + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId,"idCardNum"); + queryWrapper.like(FtbPersonnelsStaffRegistrationFormData::getValue,keyWords); + List formData = registrationFormDataService.list(queryWrapper); + List phones = formData.stream().filter(v -> StringUtils.isNotEmpty(v.getValue())).map(FtbPersonnelsStaffRegistrationFormData::getPhone).distinct().collect(Collectors.toList()); + if (CollUtil.isNotEmpty(phones)){ + empEntryDTO.setPhoneNumbers(phones); + } + } + String state = empEntryDTO.getState(); + if(StringUtils.isNotEmpty(state)){ + String[] split = state.split(","); + List stringList = new ArrayList<>(Arrays.asList(split)); + boolean contains = stringList.contains("6"); + if(contains){ + stringList.removeIf("6"::equals); + empEntryDTO.setStates(stringList); + empEntryDTO.setIsManually(true); + }else { + empEntryDTO.setStates(stringList); + } + } + // 健康证状态筛选 + if (empEntryDTO.getHealthCertificateStatus() != null) { + List healthCertificateUserIds = certificateInstanceService.getHealthCertificateUserIdsByStatus(empEntryDTO.getHealthCertificateStatus()); + if (CollUtil.isNotEmpty(healthCertificateUserIds)) { + empEntryDTO.setUserIds(healthCertificateUserIds); + } + } + page = applyMapper.pageLists(page,empEntryDTO); + List records = page.getRecords(); + Predicate predicate = v -> ObjectUtil.isNotNull(v.getCheckStatus()) && v.getCheckStatus() == 0 && v.getIsNeedCheck() == 0; + List orgIds = records.stream().filter(predicate).map(FtbEmpEntryVO::getCurrOrgId).collect(Collectors.toList()); + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(orgIds); + Map detailVOMap = new HashMap<>(); + if (listActionResult != null && listActionResult.getCode() == 200){ + List list = listActionResult.getData(); + detailVOMap = list.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item, (v1, v2) -> v1)); + } + String tenantId = UserProvider.getUser().getTenantId(); + List currPositionId = records.stream().filter(predicate).map(FtbEmpEntryVO::getCurrPositionId).collect(Collectors.toList()); + ActionResult> actionResult = v2PositionApi.listPositionBaseInfoByIds(currPositionId, tenantId); + Map positionBaseInfoVOMap = new HashMap<>(); + if (actionResult != null && actionResult.getCode() == 200){ + List list = actionResult.getData(); + positionBaseInfoVOMap = list.stream().collect(Collectors.toMap(PositionBaseInfoVO::getId, item -> item, (v1, v2) -> v1)); + } + List currRankID = records.stream().filter(predicate).map(FtbEmpEntryVO::getCurrRankID).collect(Collectors.toList()); + Map gradeVOMap = new HashMap<>(); + if (CollUtil.isNotEmpty(currRankID)){ + ActionResult> gradeActionResult = v2GradesApi.listGradeByIds(currRankID, tenantId); + if (gradeActionResult != null && gradeActionResult.getCode() == 200){ + List list = gradeActionResult.getData(); + gradeVOMap = list.stream().collect(Collectors.toMap(GradeVO::getId, item -> item, (v1, v2) -> v1)); + } + } + List currShopIds = records.stream().map(FtbEmpEntryVO::getCurrShopId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(currShopIds)){ + QueryOrganizeListTargetTypesDTO targetTypesDTO = new QueryOrganizeListTargetTypesDTO(); + targetTypesDTO.setOrganizeIds(currShopIds); + ActionResult> currShopIdsList = v2OrganizeApi.listOrganizeByTargetTypes(targetTypesDTO); + if (currShopIdsList != null && currShopIdsList.getCode() == 200){ + List list = currShopIdsList.getData(); + detailVOMap.putAll(list.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item, (v1, v2) -> v1))); + } + } + Map finalDetailVOMap = detailVOMap; + Map finalPositionBaseInfoVOMap = positionBaseInfoVOMap; + Map finalGradeVOMap = gradeVOMap; + List phones = records.stream().map(FtbEmpEntryVO::getPhone).collect(Collectors.toList()); + if(CollUtil.isEmpty(phones)) return CultivatePage.coverPageList(page); + Map> fieldValueListWithPhone = registrationFormDataService.queryFormFieldValueListWithPhone(phones); + String userId = UserProvider.getUser().getUserId(); + // 列表集合获取用户Id集合 + List userIds = records.stream().map(FtbEmpEntryVO::getUserId).collect(Collectors.toList()); + // 根据用户Id获取健康证详情 + Map healthCertificateVOMap = certificateInstanceService.getHealthCertificateDetails(userIds) + .stream().collect(Collectors.toMap(HealthCertificateDetailVO::getUserId, item -> item, (v1, v2) -> v1)); + records.forEach(item->{ + // 办理状态替换 + if(item.getIsNeedCheck() == 1){ + item.setApprovalStatus(6); + item.setApprovalStatusName("手动办理"); + }else { + item.setApprovalStatus(item.getCheckStatus()); + transformData(item); + } + if (ObjectUtil.isNotEmpty(item.getActualStartDateDate())){ + String format = DateUtil.format(item.getActualStartDateDate(), "yyyy-MM-dd"); + item.setActualStartDate(format); + } + if (ObjectUtil.isNotEmpty(item.getCheckStatus()) && item.getIsNeedCheck() == 0 && item.getCheckStatus() == 0){ + // 组织替换 + if (finalDetailVOMap.containsKey(item.getCurrOrgId())) item.setCurrOrgName(finalDetailVOMap.get(item.getCurrOrgId()).getName()); + // 岗位替换 + if (finalPositionBaseInfoVOMap.containsKey(item.getCurrPositionId())) item.setCurrPositionName(finalPositionBaseInfoVOMap.get(item.getCurrPositionId()).getFullName()); + // 职级替换 + if (finalGradeVOMap.containsKey(item.getCurrRankID())) item.setCurrRankName(finalGradeVOMap.get(item.getCurrRankID()).getName()); + // 班组替换 + if (finalDetailVOMap.containsKey(item.getCurrShopId())) item.setCurrShopName(finalDetailVOMap.get(item.getCurrShopId()).getName()); + } + if (!CollUtil.isEmpty(fieldValueListWithPhone) && fieldValueListWithPhone.containsKey(item.getPhone())){ + FtbPersonnelsStaffRegistrationFormData formData = fieldValueListWithPhone.get(item.getPhone()).stream().filter(v -> "idCardNum".equals(v.getFormFieldId())).findFirst().orElse(null); + item.setIdCard(formData != null && StringUtils.isNotEmpty(formData.getValue()) ? formData.getValue() : StringUtils.isEmpty(item.getIdCard()) ? "" : item.getIdCard()); + } + if (StringUtils.isNotEmpty(item.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(item.getTaskInfoId()); + if (ObjectUtil.isNotEmpty(byTaskId) && byTaskId.getCreatorUserId().equals(userId)) { + item.setCurrentSponsor(1); + } else { + item.setCurrentSponsor(0); + } + } + // 健康证状态回显 + if (healthCertificateVOMap.containsKey(item.getUserId())) { + item.setHealthCertificateStatus(healthCertificateVOMap.get(item.getUserId()).getStatus()); + item.setHealthCertificateStatusName(healthCertificateVOMap.get(item.getUserId()).getStatusDesc()); + } + }); + return CultivatePage.coverPageList(page); + } + + + @Override + @GlobalTransactional(rollbackFor = Throwable.class) + public FtbEmpResultVO addNewEmp(FtbEmpAddNewDTO dto) { + FtbEmpResultVO resultVO = new FtbEmpResultVO(); + // 入职手机号校验 + if (StringUtils.isEmpty(dto.getId())) verifyWhetherTheMobilePhoneNumberIsHired(dto.getPhone()); + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(dto.getPhone())) { + throw new RuntimeException("当前人员处于黑名单中,请将其移出黑名单后再办理入职!"); + } + // 身份证校验 + verifyWhetherTheIdentityCardNumberIsHired(dto.getIdCard(),dto.getPhone()); + // 主键id 编辑 + // 补充数据 + FtbPersonnelsStaffEmploymentApply apply = FtbEmpAddNewDTO.covert(dto); + supplementalData(apply,UserProvider.getUser().getTenantId()); + UpdateUserDTO userDTO = new UpdateUserDTO(); + // 动态判断文档是否提交 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRegistrationFormData::getPhone,dto.getPhone()); + List formData = registrationFormDataService.list(queryWrapper); + if (CollUtil.isNotEmpty(formData) && !formData.isEmpty() && StringUtils.isEmpty(dto.getId())) apply.setIsSubmitForm(1); + FtbPersonnelsStaffRegistrationFormData idCardNum = formData.stream().filter(v -> v.getFormFieldId().equals("idCardNum")).findFirst().orElse(null); + // 2次入职的人没有设置身份证 且之前有身份证需要回填 + if (StringUtils.isEmpty(dto.getIdCard()) + && CollUtil.isNotEmpty(formData) + && idCardNum != null) { + FtbPersonnelsStaffEmploymentApply employmentApply = null; + if (StringUtils.isNotEmpty(apply.getId())) { + employmentApply = applyMapper.selectById(apply.getId()); + } + if ( employmentApply != null && StringUtils.isNotEmpty(employmentApply.getIdCardNum())){ + apply.setIdCardNum(null); + } else { + apply.setIdCardNum(idCardNum.getValue()); + } + + } + if (StringUtils.isNotEmpty(apply.getIdCardNum())){ + IdCardVerificationResponse idCardVerification = idCardVerificationUtils.idCardVerification(apply.getIdCardNum(), apply.getWorkerName(), UserProvider.getUser().getTenantId()); + idCardVerificationUtils.checkIdCardVerification(idCardVerification); + } + String userId = null; + if (StringUtils.isNotEmpty(apply.getId())){ + FtbPersonnelsStaffEmploymentApply employmentApply = applyMapper.selectById(apply.getId()); + // 比较头像 + if (!employmentApply.getWorkerName().equals(apply.getWorkerName())){ + String userHeadLog = generaDefaultUserHeadLog(employmentApply.getWorkerName()); + apply.setUserHeadLog(userHeadLog); + userDTO.setHeadIcon(userHeadLog); + } + userId = employmentApply.getUserId(); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + if (userId == null) { + // 生成头像 + String userHeadLog = generaDefaultUserHeadLog(apply.getWorkerName()); + userId = saveUserInfo(apply, userHeadLog); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + } + userDTO.setRealName(apply.getWorkerName()); + UserBoundInfoDTO userBoundInfoDTO = new UserBoundInfoDTO(); + userBoundInfoDTO.setOrganizeId(apply.getCurrOrg()); + userBoundInfoDTO.setPositionId(apply.getCurrPosition()); + userBoundInfoDTO.setGradesId(apply.getCurrRank()); + userBoundInfoDTO.setStoreTeamId(apply.getCurrGroupId()); + userDTO.setBoundInfoDTO(userBoundInfoDTO); + // 更新用户信息 + v2UserApi.updateUserInfo(userId,userDTO); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getId,apply.getId()); + if (apply.getIdCardNum() == null){ + wrapper.set(FtbPersonnelsStaffEmploymentApply::getIdCardNum ,null); + } + if (apply.getActualStartDate() == null){ + wrapper.set(FtbPersonnelsStaffEmploymentApply::getActualStartDate ,null); + } + applyMapper.update(apply,wrapper); + }else { + // 生成头像 + String userHeadLog = generaDefaultUserHeadLog(apply.getWorkerName()); + userId = saveUserInfo(apply, userHeadLog); + apply.setUserId(userId); + apply.setSource(0); + apply.setStatus(0); + apply.setUserHeadLog(userHeadLog); + // 健康证添加 + certificateInstanceService.initHealthCertificate(userId); + applyMapper.insert(apply); + sendSms(dto.getPhone(), UserProvider.getUser().getTenantId()); + } + Map formDataMap = BeanUtil.beanToMap(apply); + if (apply.getIdCardNum() != null) { + formDataMap.put("idCardNum", apply.getIdCardNum()); + }else { + formDataMap.put("idCardNum", ""); + } + if (apply.getActualStartDate() != null) { + formDataMap.put("actualStartDate", apply.getActualStartDate()); + }else { + formDataMap.putIfAbsent("actualStartDate", ""); + } + formDataMap.put("probationPeriod", ""); + formDataMap.put("workerStatus", ""); + formDataMap.put("workerType", ""); + // 合同状态 + formDataMap.put("contractStatus", ""); + if (apply.getActualStartDate() != null) { + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(apply.getActualStartDate(), "")); + } + addRunBirthday(formDataMap); + registrationFormDataService.syncData(formDataMap, dto.getPhone(), ""); + resultVO.setUserId(apply.getUserId()); + resultVO.setRealName(apply.getWorkerName()); + resultVO.setPhone(apply.getPhone()); + resultVO.setIsSubmitForm(apply.getIsSubmitForm()); + attendanceMessageNotifications(apply.getActualStartDate(), apply.getCurrOrg(), userId); + return resultVO; + } + + public void sendSms(String phone, String tenantId) { + // 没有环境参数不发短信 + if (ObjectUtil.isEmpty(environmentParamConfig)) return; + Boolean isEnableSms = environmentParamConfig.getIsEnableSms(); + if (!isEnableSms) return; + if (personnelOrgUtils.queryIsSendPhoneMsg(phone)) { + log.error("今日已经发送了短信"); + return; + } + Map map = new LinkedHashMap<>(); + try { + if (StringUtils.isEmpty(phone)) return; + log.error("发送短信"); + Map tenants = smsConfig.getTenants(); + SmsConfig.TenantConfig tenantConfig; + String used = tenantId; + if (tenants.containsKey(tenantId)){ + tenantConfig = tenants.get(tenantId); + }else { + used = "common"; + tenantConfig = tenants.get(used); + } + String signContent = tenantConfig.getSmsSignContent(); + if (used.equals("common")) { + map.put("1", signContent); + map.put("2", getLastFourDigits(phone)); + map.put("3", "0000"); + } + smsSendUtil.sendSms(List.of(phone), "ACCOUNT_GENERATE", map,tenantId); + personnelOrgUtils.setTodayIsSendPhoneMsg(phone); + } catch (Exception e) { + e.printStackTrace(); + } + } + /** + * 提取手机号后四位数字 + * + * @param phoneNumber 完整手机号字符串 + * @return 后四位数字字符串,若输入无效则返回 null + */ + public static String getLastFourDigits(String phoneNumber) { + if (phoneNumber == null || phoneNumber.trim().isEmpty()) { + return null; + } + + // 清理手机号,只保留数字 + String cleanedNumber = phoneNumber.replaceAll("\\D", ""); + + // 检查清理后的号码长度 + if (cleanedNumber.length() < 4) { + return null; + } + + // 截取后四位 + return cleanedNumber.substring(cleanedNumber.length() - 4); + } + + @Override + public String promoteOnboardingManagement(FtbEmpAddNewDTO dto, String tenantId, String moduleId) { + if ( StringUtils.isEmpty(dto.getCurrOrgId()) || StringUtils.isEmpty(dto.getCurrPositionId()) ) { + throw new RuntimeException("未获取到正确的组织或岗位,如需办理入职,请前往入职管理新增员工!"); + } + if (StringUtils.isNotEmpty(dto.getCurrRankId())) { + ActionResult gradeVOActionResult = v2GradesApi.infoGradeNoToken(dto.getCurrRankId(),tenantId); + if (gradeVOActionResult == null || gradeVOActionResult.getData() == null) { + throw new RuntimeException("未获取到正确的职级,如需办理入职,请前往入职管理新增员工!"); + } + } + // 入职手机号校验 + if (dto.getId() == null) verifyWhetherTheMobilePhoneNumberIsHired(dto.getPhone()); + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(dto.getPhone())) { + throw new RuntimeException("当前人员处于黑名单中,请将其移出黑名单后再办理入职!"); + } + // 身份证校验 + verifyWhetherTheIdentityCardNumberIsHired(dto.getIdCard(),dto.getPhone()); + // 主键id 编辑 + FtbPersonnelsStaffEmploymentApply apply = FtbEmpAddNewDTO.covert(dto); + supplementalData(apply,tenantId); + UpdateUserDTO userDTO = new UpdateUserDTO(); + if (StringUtils.isNotEmpty(apply.getIdCardNum())){ + IdCardVerificationResponse idCardVerification = idCardVerificationUtils.idCardVerification(apply.getIdCardNum(), apply.getWorkerName(), UserProvider.getUser().getTenantId()); + idCardVerificationUtils.checkIdCardVerification(idCardVerification); + } + String resultUserId = ""; + if (StringUtils.isNotEmpty(apply.getId())){ + FtbPersonnelsStaffEmploymentApply employmentApply = applyMapper.selectById(apply.getId()); + // 比较头像 + if (!employmentApply.getWorkerName().equals(apply.getWorkerName())){ + String userHeadLog = generaDefaultUserHeadLog(employmentApply.getWorkerName()); + apply.setUserHeadLog(userHeadLog); + userDTO.setHeadIcon(userHeadLog); + } + String userId = employmentApply.getUserId(); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + // 兼容数据 + if (userId == null){ + // 后续处理入职回填userId 生成头像 + String userHeadLog = generaDefaultUserHeadLog(apply.getWorkerName()); + userId = saveUserInfo(apply, userHeadLog,tenantId); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + resultUserId = userId; + } + userDTO.setRealName(apply.getWorkerName()); + userDTO.setEntryDate(apply.getActualStartDate()); + UserBoundInfoDTO userBoundInfoDTO = new UserBoundInfoDTO(); + userBoundInfoDTO.setOrganizeId(apply.getCurrOrg()); + userBoundInfoDTO.setPositionId(apply.getCurrPosition()); + userBoundInfoDTO.setGradesId(apply.getCurrRank()); + userBoundInfoDTO.setStoreTeamId(apply.getCurrGroupId()); + userBoundInfoDTO.setTenantId(tenantId); + userDTO.setBoundInfoDTO(userBoundInfoDTO); + // 更新用户信息 + v2UserApi.updateUserInfo(userId,userDTO); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getId,apply.getId()); + if (apply.getIdCardNum() == null)wrapper.set(FtbPersonnelsStaffEmploymentApply::getIdCardNum ,null); + applyMapper.update(apply,wrapper); + }else { + // 生成头像 + apply.setVersionNum(1); + apply.setStatus(0); + applyMapper.insert(apply); + String applyId = apply.getId(); + // 后续处理入职回填userId 生成头像 + String userHeadLog = generaDefaultUserHeadLog(apply.getWorkerName()); + String userId = saveUserInfo(apply, userHeadLog,tenantId); + resultUserId = userId; + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getUserHeadLog, userHeadLog); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getId, applyId); + applyMapper.update(null,wrapper); + threadPoolTaskExecutor.execute(ThreadContext.wrap(()->{ + //Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + // FeignHolder.set(headers); + extracted(dto.getWorkerName(), tenantId, userId,apply.getCurrOrg(),moduleId); + })); + + } + sendSms(dto.getPhone(), UserProvider.getUser().getTenantId()); + return resultUserId; + } + + public void extracted(String workerName , String tenantId, String userId, String currOrg, String moduleId) { + List stringList = permissionsUtils.getUserPersonnelOrganizationIdDataPermissions(userId, + List.of(PermissionsEnums.PERSONNEL_MANAGEMENT_Button.getValue().split(",")), + "", + List.of(PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), + PermissionsEnums.PERSONNEL_MANAGEMENT_APP.getValue()), + tenantId,currOrg, moduleId); + ftbPersonnlesIMUtils.sendMsgWithList(tenantId,stringList, workerName, moduleId); + } + + /** + * 查询身份证是否在系统 + * @param idCard + */ + private void verifyWhetherTheIdentityCardNumberIsHired(String idCard,String phone) { + if(StringUtils.isEmpty(idCard)) return; + FtbPersonnelsStaffRegistrationFormDataMapper baseMapper = (FtbPersonnelsStaffRegistrationFormDataMapper) registrationFormDataService.getBaseMapper(); + Integer idCardNum = baseMapper.verifyWhetherTheDataExists(phone, "idCardNum",idCard); + if (idCardNum > 0) throw new RuntimeException("当前身份证已经存在系统,请勿重复添加!"); + } + + /** + * 查询手机号是否在已入职列表里面 + * @param phone + */ + private void verifyWhetherTheMobilePhoneNumberIsHired(String phone ) { + QueryWrapper ftbPersonnelsStaffEmploymentApplyQueryWrapper = new QueryWrapper<>(); + Long aLong = applyMapper.selectCount( + ftbPersonnelsStaffEmploymentApplyQueryWrapper.lambda().eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0)); + if (aLong > 0) throw new RuntimeException("当前手机号已经存在系统,请勿重复添加!"); + //查询手机号是否在已入职列表里面 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda().eq(FtbPersonnelsStaffRoster::getPhone, phone).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster oldRosterEntity = rosterService.getOne(rosterWrapper); + if (ObjectUtil.isNotEmpty(oldRosterEntity)) { + if (oldRosterEntity.getWorkerStatus().equals("302") || oldRosterEntity.getWorkerStatus().equals("303") || oldRosterEntity.getWorkerStatus().equals("306")) { + throw new RuntimeException("该员工已办理入职,请勿重复操作!"); + }else if (oldRosterEntity.getWorkerStatus().equals("304")) { + throw new RuntimeException("该员工已经在离职中!"); + } + } + } + + @Override + @GlobalTransactional(rollbackFor = Throwable.class) + public void terminateOnboarding(String id) { + + FtbPersonnelsStaffEmploymentApply apply = applyMapper.selectById(id); + if (apply.getStatus() == 5) { + throw new RuntimeException("该员工已终止入职,请勿重复操作!"); + } + String userId = apply.getUserId(); + if (UserProvider.getUser().getUserId().equals(userId)) throw new RuntimeException("不能操作自己!"); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getId, id); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 5); + applyMapper.update(new FtbPersonnelsStaffEmploymentApply(),wrapper); + if(StringUtils.isEmpty(userId)) return; + v2UserApi.updateUserAccount(userId,UserWorkStatusEnums.ONBOARDING_FAILED); + // 删除账号 + pTenantAccountApi.deleteUser(userId); + AuthUtil.kickoutByUserId(userId); + // 通知考勤人员终止入职 + Map map = new HashMap<>(); + map.put("userId", userId); + map.put("currOrgId", apply.getCurrOrg()); + map.put("tenantId", UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageOnboardingFail(JSONObject.toJSONString(map)); + //删除健康证 + certificateInstanceService.deleteHealthCertificate(userId); + } + + @Override + @GlobalTransactional(rollbackFor = Throwable.class) + public void deleteEmployeeOnboardingRecords(String id) { + FtbPersonnelsStaffEmploymentApply item = applyMapper.selectById(id); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsStaffRegistrationFormData::getPhone, item.getPhone()); + registrationFormDataService.remove(wrapper); + applyMapper.deleteById(id); + String userId = item.getUserId(); + if(StringUtils.isEmpty(userId)) return; + ActionResult actionResult = v2UserApi.removeUser(userId, null); + if (actionResult == null || actionResult.getCode() != 200) { + throw new RuntimeException("删除用户失败!请重试!"); + } + if(!pTenantAccountApi.deleteUserWithPlatform(userId)){ + throw new RuntimeException("删除用户账号信息失败!请重试!"); + } + AuthUtil.kickoutByUserId(userId); + } + + @Override + @GlobalTransactional(rollbackFor = Throwable.class,timeoutMills = 5*60*1000) + public void handleJoinJob(FtbEmpConfirmDTO ftbEmpConfirmDTO) { + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(ftbEmpConfirmDTO.getPhone())) { + throw new RuntimeException("当前人员处于黑名单中,请将其移出黑名单后再办理入职!"); + } + String userId = null; + String currRankId = ftbEmpConfirmDTO.getCurrRankId(); + if (StringUtils.isNotEmpty(currRankId)) { + QueryUserBoundDTO checkExistByPositionDTO = new QueryUserBoundDTO(); + checkExistByPositionDTO.setOrganizeId(ftbEmpConfirmDTO.getCurrOrgId()); + checkExistByPositionDTO.setPositionId(ftbEmpConfirmDTO.getCurrPositionId()); + checkExistByPositionDTO.setGradeId(currRankId); + doCheckOrgPositionGradesBoundDTO(checkExistByPositionDTO); + + } + // 回填入职数据 + FtbPersonnelsStaffEmploymentApply covertToEntity = FtbEmpConfirmDTO.covertToEntity(ftbEmpConfirmDTO); + covertToEntity.setIsNeedCheck(1); + FtbPersonnelsStaffEmploymentApply apply = applyMapper.selectById(covertToEntity.getId()); + covertToEntity.setCreatorUserId(apply.getCreatorUserId()); + covertToEntity.setUserId(apply.getUserId()); + // 兼容之前的入职数据 + if (StringUtils.isEmpty(apply.getUserId())){ + String userHeadLog = generaDefaultUserHeadLog(apply.getWorkerName()); + userId = saveUserInfo(apply, userHeadLog); + covertToEntity.setUserId(userId); + } + if(UserProvider.getUser().getUserId().equals(apply.getUserId())) { + throw new RuntimeException("员工本人不能为自己办理入职!"); + } + covertToEntity.setTaskInfoId(null); + // 补充数据 + supplementalData(covertToEntity,UserProvider.getUser().getTenantId()); + FtbPersonnelsStaffRoster personnelsStaffRoster = buildApplyData(covertToEntity); + applyMapper.updateById(covertToEntity); + // 数据入花名册 + FtbPersonnelsStaffEmploymentApply ftbPersonnelsStaffEmploymentApply = applyMapper.selectById(covertToEntity.getId()); + FtbPersonnelsStaffRoster staffRoster = covertRoster(ftbPersonnelsStaffEmploymentApply, + ftbEmpConfirmDTO,personnelsStaffRoster.getSystemWokerId(),personnelsStaffRoster.getJoinNum()); + if (personnelsStaffRoster.getId() != null){ + staffRoster.setId(personnelsStaffRoster.getId()); + rosterService.updateById(staffRoster); + }else { + rosterService.save(staffRoster); + } + // 同步更改姓名到组织架构 + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setRealName(ftbPersonnelsStaffEmploymentApply.getWorkerName()); + v2UserApi.updateUserInfo(ftbPersonnelsStaffEmploymentApply.getUserId(),userDTO); + // 变更内推池状态 + personnelOrgUtils.sysWorkerStatusToUchisuike(staffRoster.getPhone(), staffRoster.getWorkerStatus()); + // 数据入转正管理 + syncRegular(staffRoster, + ftbEmpConfirmDTO.getProbationPeriod(), + ftbEmpConfirmDTO.getProbationPeriodDay(), + ftbEmpConfirmDTO.getAffiliatedReportsTo(), + ftbEmpConfirmDTO.getPlanProbationaryDate()); + // 回填扩展数据 + dataSave(staffRoster, ftbEmpConfirmDTO); + //已经填写了入职登记表,那么就要把之前入职登记表中的数据加上花名册ID + registrationFormDataService.addRosterIdToFormData(staffRoster.getPhone(), staffRoster.getId()); + // 写入职信息 + //写入入职薪酬 + writeEntryPay(staffRoster); + //员工成长 + employeeGrowth(staffRoster, + ftbEmpConfirmDTO.getAffiliatedReportsTo(), + ftbEmpConfirmDTO.getActualStartDate()); + // 更改员工状态 + synchronizeEmployeeStatus(staffRoster, ftbEmpConfirmDTO.getAffiliatedReportsTo()); + // 同步薪酬数据 + syncSalaryData(covertToEntity); + } + + private void supplementalData(FtbPersonnelsStaffEmploymentApply covertToEntity,String tenantId) { + // 主键id 编辑 + ActionResult actionResult = v2OrganizeApi.organizeInfoByIdNoToken(null, covertToEntity.getCurrOrg(),tenantId); + if (ObjectUtil.isNotEmpty(actionResult) && ObjectUtil.isNotEmpty(actionResult.getData())) { + covertToEntity.setCurrOrgName(actionResult.getData().getName()); + } + if (StringUtils.isNotEmpty(covertToEntity.getCurrGroupId())){ + ActionResult organizeInfoById = v2OrganizeApi.organizeInfoByIdNoToken(OrganizeCategoryEnums.TEAM, covertToEntity.getCurrGroupId(),tenantId); + if (ObjectUtil.isNotEmpty(organizeInfoById) && ObjectUtil.isNotEmpty(organizeInfoById.getData())) { + covertToEntity.setCurrGroupName(organizeInfoById.getData().getName()); + } + }else { + covertToEntity.setCurrGroupName(""); + } + String onboardPostId = covertToEntity.getCurrPosition(); + if (StringUtils.isNotEmpty(onboardPostId)) { + ActionResult voActionResult = v2PositionApi.infoPositionNoToken(onboardPostId,tenantId); + if (ObjectUtil.isNotEmpty(voActionResult) && ObjectUtil.isNotEmpty(voActionResult.getData())) { + covertToEntity.setCurrPositionName(voActionResult.getData().getFullName()); + } + } + String onboardGradeId = covertToEntity.getCurrRank(); + if (StringUtils.isNotEmpty(onboardGradeId)) { + ActionResult gradeVOActionResult = v2GradesApi.infoGradeNoToken(onboardGradeId,tenantId); + if (ObjectUtil.isNotEmpty(gradeVOActionResult) && ObjectUtil.isNotEmpty(gradeVOActionResult.getData())){ + covertToEntity.setCurrRankName(gradeVOActionResult.getData().getFullName()); + } + }else { + covertToEntity.setCurrRankName(""); + } + } + + + @Override + @GlobalTransactional + public void handleJoinJobOA(FtbEmpConfirmDTO ftbEmpConfirmDTO) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getPhone, ftbEmpConfirmDTO.getPhone()); + FtbPersonnelsStaffRoster oldRoster = rosterService.getOne(wrapper); + if (ObjectUtil.isNotEmpty(oldRoster)) { + if (ObjectUtil.isNotEmpty(oldRoster.getDepartDate()) && + ( ftbEmpConfirmDTO.getActualStartDate().equals(oldRoster.getDepartDate()) || + ftbEmpConfirmDTO.getActualStartDate().before(oldRoster.getDepartDate())) + ) { + throw new RuntimeException("再次入职,入职时间不能早于上次离职时间" + DateUtil.format(oldRoster.getDepartDate(), "yyyy年MM月dd日!")); + } + } + if (ftbEmpConfirmDTO.getActualStartDate().after(new Date())) { + throw new RuntimeException("实际入职时间不能大于当前时间!"); + } + if (ObjectUtil.isNotEmpty(ftbEmpConfirmDTO.getEntryMoney()) && + ftbEmpConfirmDTO.getEntryMoney().compareTo(BigDecimal.valueOf(100000000)) >= 1){ + throw new RuntimeException("薪资项合计超出上限,请输入合法薪资数据"); + } + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(ftbEmpConfirmDTO.getPhone())) { + throw new RuntimeException("当前人员处于黑名单中,请将其移出黑名单后再办理入职!"); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getId, ftbEmpConfirmDTO.getId()); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getTaskInfoId, ftbEmpConfirmDTO.getTaskId()); + List applyList = applyMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(applyList)) throw new RuntimeException("该员工已在审批中,请勿重复操作!"); + // 查询员工 + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone,ftbEmpConfirmDTO.getPhone() ); + lambdaQueryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply employmentApply = applyMapper.selectOne(lambdaQueryWrapper); + // 校验重复操作 + checkWhetherYouHaveBeenHired(employmentApply.getPhone()); + if(UserProvider.getUser().getUserId().equals(employmentApply.getUserId())) { + throw new RuntimeException("员工本人不能为自己办理入职!"); + } + // 校验岗位是否还存在 + String currRankId = ftbEmpConfirmDTO.getCurrRankId(); + if (StringUtils.isNotEmpty(currRankId)) { + QueryUserBoundDTO checkExistByPositionDTO = new QueryUserBoundDTO(); + checkExistByPositionDTO.setPositionId(ftbEmpConfirmDTO.getCurrPositionId()); + checkExistByPositionDTO.setGradeId(currRankId); + doCheckOrgPositionGradesBoundDTO(checkExistByPositionDTO); + } + // 回填入职数据 + FtbPersonnelsStaffEmploymentApply covertToEntity = FtbEmpConfirmDTO.covertToEntity(ftbEmpConfirmDTO); + // 设置审批状态 + covertToEntity.setIsNeedCheck(0); + covertToEntity.setCheckStatus(1); + covertToEntity.setTaskInfoId(ftbEmpConfirmDTO.getTaskId()); + covertToEntity.setId(ftbEmpConfirmDTO.getId()); + // 回填扩展数据 + dataSaveWithOA(ftbEmpConfirmDTO); + // 补充数据 + supplementalData(covertToEntity,UserProvider.getUser().getTenantId()); + applyMapper.updateById(covertToEntity); + } + + private void checkWhetherYouHaveBeenHired(String phone){ + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getPhone, phone); + FtbPersonnelsStaffRoster oldRoster = rosterService.getOne(wrapper); + if (ObjectUtil.isNotEmpty(oldRoster)) { + //重复入职 + if (oldRoster.getEnabledMark() == 0 && (oldRoster.getWorkerStatus().equals("302") || + oldRoster.getWorkerStatus().equals("303"))) { + throw new RuntimeException("该员工已经入职,请勿重复操作"); + } else if (oldRoster.getEnabledMark() == 0 && oldRoster.getWorkerStatus().equals("304")) { + throw new RuntimeException("该员工已经在离职中"); + } + } + + } + + /*** + * 构建入职数据校验是否二次入职 + * @param covertToEntity + * @return + */ + private FtbPersonnelsStaffRoster buildApplyData(FtbPersonnelsStaffEmploymentApply covertToEntity) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getPhone, covertToEntity.getPhone()); + FtbPersonnelsStaffRoster oldRoster = rosterService.getOne(wrapper); + long count = 0; + QueryListUserDTO userDTO = new QueryListUserDTO(); + userDTO.setMobilePhone(covertToEntity.getPhone()); + ActionResult> copyInfo = v2UserApi.listUser(userDTO); + if (copyInfo.getCode() == 200 && CollUtil.isNotEmpty(copyInfo.getData())) { + UserPageListVO userPageListVO = copyInfo.getData().stream().findFirst().orElse(new UserPageListVO()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId, userPageListVO.getId()); + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType,0,3); + count = growthLogService.count(queryWrapper); + } + if (ObjectUtil.isNotEmpty(oldRoster)){ + //重复入职 + if (oldRoster.getEnabledMark() == 0 && (oldRoster.getWorkerStatus().equals("302") || + oldRoster.getWorkerStatus().equals("303"))) { + throw new RuntimeException("该员工已经入职,请勿重复操作"); + }else if (oldRoster.getEnabledMark() == 0 && oldRoster.getWorkerStatus().equals("304")) { + throw new RuntimeException("该员工已经在离职中"); + } + if (ObjectUtil.isNotEmpty(oldRoster.getJoinNum())) { + covertToEntity.setJoinNum(oldRoster.getJoinNum() + 1); + } else { + covertToEntity.setJoinNum(1); + } + if (ObjectUtil.isNotEmpty(oldRoster.getDepartDate()) && + ( covertToEntity.getActualStartDate().equals(oldRoster.getDepartDate()) || + covertToEntity.getActualStartDate().before(oldRoster.getDepartDate())) + + ){ + throw new RuntimeException("再次入职,入职时间不能早于上次离职时间"+DateUtil.format(oldRoster.getDepartDate(), "yyyy年MM月dd日!")); + } + covertToEntity.setSystemWokerId(oldRoster.getSystemWokerId()); + oldRoster.setJoinNum(covertToEntity.getJoinNum()); + return oldRoster; + + }else if (ObjectUtil.isEmpty(oldRoster) && count > 0){ + FtbPersonnelsStaffRoster oldRosterNew = new FtbPersonnelsStaffRoster(); + // 用户被删除了且 + // 生成系统id + covertToEntity.setSystemWokerId(SelfGrowthUtil.providePersonnelsCustomIDBasedOnTheModule(SelfrowingEnum.STAFF_ROSTER)); + covertToEntity.setJoinNum((int) (count+1)); + oldRosterNew.setSystemWokerId(covertToEntity.getSystemWokerId()); + oldRosterNew.setJoinNum(covertToEntity.getJoinNum()); + return oldRosterNew; + } else { + FtbPersonnelsStaffRoster oldRosterNew = new FtbPersonnelsStaffRoster(); + // 生成系统id + covertToEntity.setSystemWokerId(SelfGrowthUtil.providePersonnelsCustomIDBasedOnTheModule(SelfrowingEnum.STAFF_ROSTER)); + covertToEntity.setJoinNum(1); + oldRosterNew.setSystemWokerId(covertToEntity.getSystemWokerId()); + oldRosterNew.setJoinNum(covertToEntity.getJoinNum()); + return oldRosterNew; + } + } + + @Override + public FtbEmpConfirmVO onboardingDetails(String id) { + FtbPersonnelsStaffEmploymentApply item = applyMapper.selectById(id); + if (item == null) { + return null; + } + return getApplyInfoItem(item); + } + + @NotNull + private FtbEmpConfirmVO getApplyInfoItem(FtbPersonnelsStaffEmploymentApply item) { + FtbEmpConfirmVO dto = FtbEmpConfirmVO.covert(item); + // 重新获取 + if(item.getCheckStatus() == 3 && StringUtils.isNotEmpty(item.getUserId())){ + ActionResult usersBound = v2UserApi.getUsersBound(item.getUserId(), UserProvider.getUser().getTenantId()); + if (usersBound != null && usersBound.getCode() == 200) { + UserBoundInfoVO data = usersBound.getData(); + dto.setCurrOrgId(data.getOrganizeId()); + dto.setCurrOrgName(data.getOrganizeName()); + dto.setCurrPositionId(data.getPositionId()); + dto.setCurrPositionName(data.getPositionName()); + dto.setCurrShopId(data.getStoreTeamId()); + dto.setCurrShopName(data.getStoreTeamName()); + dto.setCurrRankId(data.getGradesId()); + dto.setCurrRankName(data.getGradesName()); + } + } + dto.setPlanProbationaryDate( personnelOrgUtils.dateToString(item.getPlanProbationaryDate(), "")); +// if (StringUtils.isNotEmpty(dto.getCurrShopId())) { +// ActionResult shopActionResult = v2OrganizeApi.organizeInfoById(null, dto.getCurrShopId()); +// if (shopActionResult != null && shopActionResult.getCode() == 200 && shopActionResult.getData() != null) { +// dto.setCurrShopName(shopActionResult.getData().getName()); +// } +// } + List creatorUserId = Stream.of(item.getCreatorUserId(), dto.getAffiliatedReportsTo()).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + if (CollUtil.isEmpty(creatorUserId)) return dto; + ActionResult> listActionResult = v2UserApi.getUserPrimaryBoundBatch(creatorUserId, UserProvider.getUser().getTenantId()); + if (listActionResult != null && listActionResult.getCode() == 200) { + Map map = listActionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2) -> k1)); + UserBoundVO first = map.get(item.getCreatorUserId()); + if (map.containsKey(item.getCreatorUserId())) { + dto.setCreateUserName(first.getUserName()); + } + if ( map.containsKey(dto.getAffiliatedReportsTo())) { + UserBoundVO second = map.get(dto.getAffiliatedReportsTo()); + dto.setAffiliatedReportsToName(second.getUserName()); + } + } + List employeeTypeVOS = employeeTypeService.employeeList(); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, "workerType"); + query.orderByAsc(SuperBaseEntity.SuperCBaseEntity::getCreatorTime); + List ftbPersonnelsRegistrationFormFieldOptions = formFieldOptionMapper.selectList(query); + if (StringUtils.isNotEmpty(dto.getWorkerType()) && CollUtil.isNotEmpty(employeeTypeVOS)){ + dto.setWorkerType(ftbPersonnelsRegistrationFormFieldOptions.stream().filter(item1 -> item1.getId().equals(dto.getWorkerType())).findFirst().get().getName()); + } + return dto; + } + + @Override + @GlobalTransactional + public void onboardingApprovals(FtbPersonnelsAuditDto personalsSalaryAuditDto) { + String taskId = personalsSalaryAuditDto.getTaskId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getTaskInfoId, taskId); + Long aLong = applyMapper.selectCount(wrapper); + if (aLong == 0L || aLong > 1) { + throw new RuntimeException("审批失败,审批任务不存在!"); + } + FtbPersonnelsStaffEmploymentApply employmentApply = applyMapper.selectOne(wrapper); + FtbPersonnelsStaffEmploymentApply updateApply = new FtbPersonnelsStaffEmploymentApply(); + updateApply.setId(employmentApply.getId()); + FtbPersonnelsAuditTaskEnum em = personalsSalaryAuditDto.getFlag().equals("1") ? FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED : + FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED; + updateApply.setCheckStatus(em.getCode()); + // 兼容之前的入职数据 + if (StringUtils.isEmpty(employmentApply.getUserId())){ + String userHeadLog = generaDefaultUserHeadLog(employmentApply.getWorkerName()); + String userId = saveUserInfo(employmentApply, userHeadLog); + updateApply.setUserId(userId); + } + FtbPersonnelsStaffRoster personnelsStaffRoster = null; + if (personalsSalaryAuditDto.getFlag().equals("1")) { + updateApply.setPhone(employmentApply.getPhone()); + updateApply.setActualStartDate(employmentApply.getActualStartDate()); + personnelsStaffRoster = buildApplyData(updateApply); + } + applyMapper.updateById(updateApply); + if (!Objects.equals(em.getCode(), FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode())) return; + // 审批通过后,生成员工rost + // 数据入花名册 + FtbPersonnelsStaffEmploymentApply staffEmploymentApply = applyMapper.selectById(employmentApply.getId()); + FtbPersonnelsStaffRoster staffRoster = covertRosterWithOA(staffEmploymentApply,personnelsStaffRoster); + if (personnelsStaffRoster != null && personnelsStaffRoster.getId() != null){ + staffRoster.setId(personnelsStaffRoster.getId()); + rosterService.updateById(staffRoster); + }else { + rosterService.save(staffRoster); + } + if (!personalsSalaryAuditDto.getFlag().equals("1")) return; + // 数据入转正管理 + syncRegular(staffRoster, + staffEmploymentApply.getProbationPeriod(), + staffEmploymentApply.getProbationPeriodDay(), + staffEmploymentApply.getAffiliatedReportsTo(), + employmentApply.getPlanProbationaryDate()); + // 回填扩展数据 + dataSaveWithOAApprovals(staffRoster,employmentApply.getActualStartDate()); + //已经填写了入职登记表,那么就要把之前入职登记表中的数据加上花名册ID + registrationFormDataService.addRosterIdToFormData(staffRoster.getPhone(), staffRoster.getId()); + // 写入职信息 + //写入入职薪酬 + writeEntryPay(staffRoster); + //员工成长 + employeeGrowth(staffRoster, + staffEmploymentApply.getAffiliatedReportsTo(),staffEmploymentApply.getActualStartDate()); + // 更改员工状态 + synchronizeEmployeeStatus(staffRoster, staffEmploymentApply.getAffiliatedReportsTo()); + // 变更内推池状态 + personnelOrgUtils.sysWorkerStatusToUchisuike(staffRoster.getPhone(), staffRoster.getWorkerStatus()); + // 同步薪酬 + syncSalaryData(employmentApply); + } + + /** + * 入职的薪酬 + * @param staffRoster + */ + private void writeEntryPay(FtbPersonnelsStaffRoster staffRoster) { + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(staffRoster.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(staffRoster.getCurrSalary()); + if (staffRoster.getJoinNum() > 1) { + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.REPEAT_JOIN.getCode()); + } else { + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.FIRST_JOIN.getCode()); + } + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + } + + @Override + @GlobalTransactional + public void onboardingWithdrawal(FtbPersonnelsAuditDto personnelsAuditDto) { + String taskId = personnelsAuditDto.getTaskId(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getTaskInfoId, taskId); + FtbPersonnelsStaffEmploymentApply employmentApply = applyMapper.selectOne(wrapper); + if (null == employmentApply) { + throw new RuntimeException("撤销失败,审批任务不存在!"); + } + FtbPersonnelsStaffEmploymentApply updateApply = new FtbPersonnelsStaffEmploymentApply(); + updateApply.setId(employmentApply.getId()); + updateApply.setCheckStatus(FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + applyMapper.updateById(updateApply); + } + + @Override + public FtbEmpAddNewVO queryNewEmp(String id) { + FtbPersonnelsStaffEmploymentApply apply = applyMapper.selectById(id); + FtbEmpAddNewVO ftbEmpAddNewVO = FtbEmpAddNewVO.covert(apply); + List fieldValueListWithPhone = registrationFormDataService.queryFormFieldValueList(ftbEmpAddNewVO.getPhone(), null); + Map detailVOMap = new HashMap<>(); + if (StringUtils.isNotEmpty(ftbEmpAddNewVO.getCurrOrgId())){ + ActionResult> listActionResult = v2OrganizeApi.organizesByOrganizeIds(List.of()); + if (listActionResult != null && listActionResult.getCode() == 200){ + List list = listActionResult.getData(); + detailVOMap = list.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item, (v1, v2) -> v1)); + } + } + Map positionBaseInfoVOMap = new HashMap<>(); + if (StringUtils.isNotEmpty(ftbEmpAddNewVO.getCurrPositionId())){ + ActionResult> actionResult = v2PositionApi.listPositionBaseInfoByIds(List.of(ftbEmpAddNewVO.getCurrPositionId()), null); + if (actionResult != null && actionResult.getCode() == 200){ + List list = actionResult.getData(); + positionBaseInfoVOMap = list.stream().collect(Collectors.toMap(PositionBaseInfoVO::getId, item -> item, (v1, v2) -> v1)); + } + } + Map gradeVOMap = new HashMap<>(); + if (StringUtils.isNotEmpty(ftbEmpAddNewVO.getCurrRankId())) { + ActionResult> gradeActionResult = v2GradesApi.listGradeByIds(List.of(ftbEmpAddNewVO.getCurrRankId()), null); + if (gradeActionResult != null && gradeActionResult.getCode() == 200) { + List list = gradeActionResult.getData(); + gradeVOMap = list.stream().collect(Collectors.toMap(GradeVO::getId, item -> item, (v1, v2) -> v1)); + } + } + if (StringUtils.isNotEmpty(ftbEmpAddNewVO.getCurrGroupId()) ) { + ActionResult> currShopIdsList = v2OrganizeApi.organizesByOrganizeIds(List.of(ftbEmpAddNewVO.getCurrGroupId())); + if (currShopIdsList != null && currShopIdsList.getCode() == 200) { + List list = currShopIdsList.getData(); + detailVOMap.putAll(list.stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, item -> item, (v1, v2) -> v1))); + } + } + // 组织替换 + if (detailVOMap.containsKey(ftbEmpAddNewVO.getCurrOrgId())) ftbEmpAddNewVO.setCurrOrgName(detailVOMap.get(ftbEmpAddNewVO.getCurrOrgId()).getName()); + // 岗位替换 + if (positionBaseInfoVOMap.containsKey(ftbEmpAddNewVO.getCurrPositionId())) ftbEmpAddNewVO.setCurrPositionName(positionBaseInfoVOMap.get(ftbEmpAddNewVO.getCurrPositionId()).getFullName()); + // 职级替换 + if (gradeVOMap.containsKey(ftbEmpAddNewVO.getCurrRankId())) ftbEmpAddNewVO.setCurrRankName(gradeVOMap.get(ftbEmpAddNewVO.getCurrRankId()).getName()); + // 班组替换 + if (detailVOMap.containsKey(ftbEmpAddNewVO.getCurrGroupId())) ftbEmpAddNewVO.setCurrGroupName(detailVOMap.get(ftbEmpAddNewVO.getCurrGroupId()).getName()); + if ( !CollUtil.isEmpty(fieldValueListWithPhone) ){ + FtbPersonnelsStaffRegistrationFormData formData = fieldValueListWithPhone.stream().filter(v -> "idCardNum".equals(v.getFormFieldId())).findFirst().orElse(null); + ftbEmpAddNewVO.setIdCard(formData != null ? formData.getValue() :null); + } + return ftbEmpAddNewVO; + } + + @Override + public List queryCrewsWithEntry(RequestForOA request) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getId,request.getPrimaryKeyId()); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getCurrOrg,request.getAffiliatedOrg()); + List apply = applyMapper.selectList(queryWrapper); + List collected = apply.stream().filter(v -> StringUtils.isNotEmpty(v.getCurrGroupId())).map(v -> { + WorkerGroupDataDto dataDto = new WorkerGroupDataDto(); + dataDto.setStoreTeamId(v.getCurrGroupId()); + ActionResult result = v2OrganizeApi.organizeInfoById(OrganizeCategoryEnums.TEAM, v.getCurrGroupId()); + if (result != null && result.getData() != null) dataDto.setStoreTeamName(result.getData().getName()); + return dataDto; + }).collect(Collectors.toList()); + return collected; + } + + @Override + public List onboardingList(String phone) { + List userIds = null; + if (!UserProvider.getUser().getIsAdministrator()) { + userIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(UserProvider.getUser().getUserId(), PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), "web"); + if (userIds != null && userIds.isEmpty()) return new ArrayList<>(); + } + return applyMapper.onboardingListOA(phone,userIds); + } + + @Override + public void updatePhoneByUserId(String userId, String phone) { + // 更新 + LambdaUpdateWrapper queryWrapper = Wrappers.lambdaUpdate(); + queryWrapper.set(FtbPersonnelsStaffEmploymentApply::getPhone,phone); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + applyMapper.update(new FtbPersonnelsStaffEmploymentApply(),queryWrapper); + } + + @Override + public List searchPhoneName(FtbEmpQueryDTO keyword) { + List ftbEmpEntryVOS = applyMapper.searchPhoneName(keyword); + List userIds = ftbEmpEntryVOS.stream().map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + ActionResult> infoBatch = v2UserApi.getAllUserInfoBatch(userIds, UserProvider.getUser().getTenantId()); + Map userBoundVOMap = new HashMap<>(); + if (infoBatch != null && infoBatch.getCode() == 200) { + userBoundVOMap = infoBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, item -> item, (v1, v2) -> v1)); + } + Map finalUserBoundVOMap = userBoundVOMap; + ftbEmpEntryVOS.stream().forEach(item -> { + if (finalUserBoundVOMap.containsKey(item.getUserId())) { + UserBoundVO userBoundVO = finalUserBoundVOMap.get(item.getUserId()); + item.setCurrOrg(userBoundVO.getOrganizeId()); + item.setCurrOrgName(userBoundVO.getOrganizeName()); + item.setCurrPosition(userBoundVO.getPositionId()); + item.setCurrPositionName(userBoundVO.getPositionName()); + item.setCurrRank(userBoundVO.getGradeId()); + item.setCurrRankName(userBoundVO.getGradeName()); + } + }); + return ftbEmpEntryVOS; + } + + @Override + public void onboardingListExport(FtbEmpEntryDTO req, HttpServletResponse response) throws IOException { + req.setPageSize(-1); + PageListVO pageVo = pageLists(req, "web"); + List list = pageVo.getList(); + list.forEach(item->{ + if(item.getIsSubmitForm() == 1) { + item.setIsSubmitFormName("已提交"); + } else { + item.setIsSubmitFormName("未提交"); + } + }); + // 封装数据 + List phones = list.stream().map(FtbEmpEntryVO::getPhone).collect(Collectors.toList()); + Map> listWithPhone = registrationFormDataService.queryFormFieldValueListWithPhone(phones); + List rosterDataList = list.stream().map(v -> { + FtbPersonnelsStaffRoster staffRoster = new FtbPersonnelsStaffRoster(); + staffRoster.setUserId(v.getUserId()); + staffRoster.setName(v.getWorkerName()); + if ( (v.getCheckStatus() != null && v.getIsNeedCheck() != null) && ( v.getCheckStatus() == 2 || v.getIsNeedCheck() == 1)){ + staffRoster.setActualStartDate(v.getActualStartDateDate()); + } + String phone = v.getPhone(); + List formData = listWithPhone.get(phone); + if (formData != null) { + formData.forEach(item -> { + if ("actualStartDate".equals(item.getFormFieldId()) && + !( (v.getCheckStatus() != null && v.getIsNeedCheck() != null) + && ( v.getCheckStatus() == 2 || v.getIsNeedCheck() == 1)) ){ + item.setValue(""); + } + }); + } + return new ExportRosterOneVo(staffRoster, formData); + }).collect(Collectors.toList()); + List> excelHeaderList = ExcelHeaderUtil.getExcelHeaderList(FtbEmpEntryVO.class); + List> lists = rosterExportUtils.extractAnnotatedFieldValues(list,FtbEmpEntryVO.class); + FtbRosterImportTemplateBO templateBO = new FtbRosterImportTemplateBO(); + templateBO.setHeader(excelHeaderList); + templateBO.setSheetName("Sheet1"); + templateBO.setData(lists); + List onboarding = rosterExportUtils.getExportFormTypesWithOnboarding(); + //查询选项 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0) + .orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getId); + List optionList = fieldOptionService.list(wrapper); + List templateBOList = rosterExportUtils.convertToFtbRosterImportTemplateBO(onboarding, optionList, rosterDataList); + templateBOList.add(templateBO); + templateBOList.sort(Comparator.comparing(FtbRosterImportTemplateBO::getSheetName)); + EasyExcelUtils.dynamicHeaderGeneration(response, "入职办理列表", templateBOList); + } + + @SneakyThrows + private String generaDefaultUserHeadLog(String name) { + String headLogo = DEFAULT_USER_LOGO; + ActionResult headResult = pTenantAccountApi.generateDefaultAvatar(name); + if (null != headResult) { + headLogo = (String) headResult.getData(); + } + return headLogo; + } + + private void transformData(FtbEmpEntryVO item) { + Integer checkStatus = item.getCheckStatus(); + switch (checkStatus){ + case 0: + item.setApprovalStatusName("未办理"); + break; + case 1: + item.setApprovalStatusName("审批中"); + break; + case 2: + item.setApprovalStatusName("审批通过"); + break; + case 3: + item.setApprovalStatusName("审批不通过"); + break; + case 4: + item.setApprovalStatusName("已撤销"); + break; + case 5: + item.setApprovalStatusName("入职失败"); + break; + default: + item.setApprovalStatusName("未知"); + break; + } + } + + private FtbPersonnelsStaffRoster covertRosterWithOA(FtbPersonnelsStaffEmploymentApply item, FtbPersonnelsStaffRoster personnelsStaffRoster) { + if (item == null) { + return null; + } + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = new FtbPersonnelsStaffRoster(); + ftbPersonnelsStaffRoster.setPhone(item.getPhone()); + ftbPersonnelsStaffRoster.setName(item.getWorkerName()); + ftbPersonnelsStaffRoster.setUserId(item.getUserId()); + if (personnelsStaffRoster != null && personnelsStaffRoster.getSystemWokerId() != null) { + ftbPersonnelsStaffRoster.setSystemWokerId(personnelsStaffRoster.getSystemWokerId()); + } + if (personnelsStaffRoster != null && personnelsStaffRoster.getJoinNum() != null) { + ftbPersonnelsStaffRoster.setJoinNum(personnelsStaffRoster.getJoinNum()); + } + ftbPersonnelsStaffRoster.setCurrOrg(item.getCurrOrg()); + ftbPersonnelsStaffRoster.setCurrPosition(item.getCurrPosition()); + ftbPersonnelsStaffRoster.setCurrRank(item.getCurrRank()); + ftbPersonnelsStaffRoster.setCurrGroupId(item.getCurrGroupId()); + ftbPersonnelsStaffRoster.setCurrReportsTo(item.getAffiliatedReportsTo()); + ftbPersonnelsStaffRoster.setContractType(item.getContractType()); + ftbPersonnelsStaffRoster.setIsSubmitForm(item.getIsSubmitForm()); + ftbPersonnelsStaffRoster.setEnabledMark(item.getEnabledMark()); + ftbPersonnelsStaffRoster.setVersionNum(item.getVersionNum()); + ftbPersonnelsStaffRoster.setRegisterImg(item.getRegisterImg()); + ftbPersonnelsStaffRoster.setContractStatus(FContractSignStatus.NOT_INITIATED.getCode()); + ftbPersonnelsStaffRoster.setContractTypeName(""); + ftbPersonnelsStaffRoster.setContractType(""); + ftbPersonnelsStaffRoster.setIsTrialFail(0); + ftbPersonnelsStaffRoster.setTrialFail(""); + ftbPersonnelsStaffRoster.setIsSignSeparation(0); + ftbPersonnelsStaffRoster.setEntrySalary(item.getEntryMoney()); + if (StringUtils.isNotEmpty(item.getProbationPeriod()) && item.getProbationPeriod().equals("100")) { + ftbPersonnelsStaffRoster.setRegularSalary(ftbPersonnelsStaffRoster.getEntrySalary()); + ftbPersonnelsStaffRoster.setActualProbationaryDate(ftbPersonnelsStaffRoster.getActualStartDate()); + } + ftbPersonnelsStaffRoster.setWorkerType(item.getWorkerType()); + ftbPersonnelsStaffRoster.setWorkerStatus(item.getWorkerStatus()); + ftbPersonnelsStaffRoster.setActualStartDate(item.getActualStartDate()); + ftbPersonnelsStaffRoster.setCreatorTime(new Date()); + return ftbPersonnelsStaffRoster; + } + + private FtbPersonnelsStaffRoster covertRoster(FtbPersonnelsStaffEmploymentApply item, FtbEmpConfirmDTO ftbEmpConfirmDTO, String systemWokerId, Integer joinNum) { + if (item == null) { + return null; + } + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = new FtbPersonnelsStaffRoster(); + ftbPersonnelsStaffRoster.setPhone(item.getPhone()); + ftbPersonnelsStaffRoster.setName(item.getWorkerName()); + ftbPersonnelsStaffRoster.setUserId(item.getUserId()); + ftbPersonnelsStaffRoster.setSystemWokerId(systemWokerId); + ftbPersonnelsStaffRoster.setCurrOrg(ftbEmpConfirmDTO.getCurrOrgId()); + ftbPersonnelsStaffRoster.setCurrPosition(ftbEmpConfirmDTO.getCurrPositionId()); + ftbPersonnelsStaffRoster.setCurrRank(ftbEmpConfirmDTO.getCurrRankId()); + ftbPersonnelsStaffRoster.setCurrGroupId(ftbEmpConfirmDTO.getCurrShopId()); + ftbPersonnelsStaffRoster.setCurrReportsTo(ftbEmpConfirmDTO.getAffiliatedReportsTo()); + ftbPersonnelsStaffRoster.setContractType(item.getContractType()); + ftbPersonnelsStaffRoster.setIsSubmitForm(item.getIsSubmitForm()); + ftbPersonnelsStaffRoster.setJoinNum(joinNum); + ftbPersonnelsStaffRoster.setEnabledMark(item.getEnabledMark()); + ftbPersonnelsStaffRoster.setVersionNum(item.getVersionNum()); + ftbPersonnelsStaffRoster.setRegisterImg(item.getRegisterImg()); + ftbPersonnelsStaffRoster.setTenantId(item.getTenantId()); + ftbPersonnelsStaffRoster.setContractStatus(FContractSignStatus.NOT_INITIATED.getCode()); + ftbPersonnelsStaffRoster.setContractTypeName(""); + ftbPersonnelsStaffRoster.setContractType(""); + ftbPersonnelsStaffRoster.setIsTrialFail(0); + ftbPersonnelsStaffRoster.setTrialFail(""); + ftbPersonnelsStaffRoster.setIsSignSeparation(0); + ftbPersonnelsStaffRoster.setEntrySalary(ftbEmpConfirmDTO.getEntryMoney()); + if (StringUtils.isNotEmpty(ftbEmpConfirmDTO.getProbationPeriod()) && ftbEmpConfirmDTO.getProbationPeriod().equals("100")) { + ftbPersonnelsStaffRoster.setRegularSalary(ftbPersonnelsStaffRoster.getEntrySalary()); + ftbPersonnelsStaffRoster.setActualProbationaryDate(ftbPersonnelsStaffRoster.getActualStartDate()); + } + ftbPersonnelsStaffRoster.setWorkerType(ftbEmpConfirmDTO.getWorkerType()); + ftbPersonnelsStaffRoster.setWorkerStatus(ftbEmpConfirmDTO.getWorkerStatus()); + ftbPersonnelsStaffRoster.setActualStartDate(ftbEmpConfirmDTO.getActualStartDate()); + ftbPersonnelsStaffRoster.setCreatorTime(new Date()); + return ftbPersonnelsStaffRoster; + } + + + + public void dataSave(FtbPersonnelsStaffRoster entity, FtbEmpConfirmDTO req){ + Map formDataMap = BeanUtil.beanToMap(req); + Map formDataMapEntity = BeanUtil.beanToMap(entity); + formDataMap.putAll(formDataMapEntity); + formDataMap.put("planProbationaryDate",personnelOrgUtils.dateToString(req.getPlanProbationaryDate(), "")); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(req.getActualStartDate(), "")); + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(req.getProbationPeriod(), req.getProbationPeriodDay()); + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + formDataMap.put("workerName", entity.getName()); + Date now = new Date(); + if (now.after(req.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(req.getActualStartDate(), now, false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + if (entity.getWorkerStatus().equals("303")) { + formDataMap.put("actualProbationaryDate", personnelOrgUtils.dateToString(entity.getActualStartDate(), "")); + } + //清楚合同信息 + formDataMap.put("contractStatus", "500"); + formDataMap.put("contractCompany", ""); + formDataMap.put("contractEffectiveDate", ""); + formDataMap.put("contractName", ""); + formDataMap.put("contractType", ""); + formDataMap.put("contractFile", ""); + formDataMap.put("contractDate", ""); + formDataMap.put("contractDetail", ""); + formDataMap.put("contractTaskId", ""); + registrationFormDataService.syncData(formDataMap, entity.getPhone(), entity.getId()); + } + private void addRunBirthday(Map formDataMap) { + // 身份证号码识别出生日期 + if (Objects.nonNull(formDataMap.get("idCardNum")) && StringUtils.isNotEmpty(formDataMap.get("idCardNum").toString())) { + Date birthday = IdcardUtil.getBirthDate(formDataMap.get("idCardNum").toString()).toJdkDate(); + formDataMap.put("birthday", DateUtil.format(birthday, "yyyy-MM-dd")); + } + String birthday = (String) formDataMap.get("birthday"); + if (org.apache.commons.lang3.StringUtils.isNotEmpty(birthday)) { + Date birth = personnelOrgUtils.stringDateToDate(birthday); + int age = personnelOrgUtils.calculateAge(personnelOrgUtils.dateToLocalDate(birth)); + String currAge = age + "岁"; + formDataMap.put("age", currAge); + } + } + + public void dataSaveWithOA(FtbEmpConfirmDTO req){ + Map formDataMap = BeanUtil.beanToMap(req); + formDataMap.put("planProbationaryDate",personnelOrgUtils.dateToString(req.getPlanProbationaryDate(), "")); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(req.getActualStartDate(), "")); + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(req.getProbationPeriod(), req.getProbationPeriodDay()); + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + formDataMap.put("workerName", req.getName()); + Date now = new Date(); + if (now.after(req.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(req.getActualStartDate(), now, false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + //清楚合同信息 + formDataMap.put("contractStatus", "500"); + formDataMap.put("contractCompany", ""); + formDataMap.put("contractEffectiveDate", ""); + formDataMap.put("contractName", ""); + formDataMap.put("contractType", ""); + formDataMap.put("contractFile", ""); + formDataMap.put("contractDate", ""); + formDataMap.put("contractDetail", ""); + formDataMap.put("contractTaskId", ""); + registrationFormDataService.syncData(formDataMap, req.getPhone(), ""); + } + public void dataSaveWithOAApprovals(FtbPersonnelsStaffRoster entity, Date actualStartDate){ + Map formDataMap = BeanUtil.beanToMap(entity); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(entity.getActualStartDate(), "")); + Date now = new Date(); + if (now.after(actualStartDate)) { + long betweenDay = DateUtil.betweenDay(actualStartDate, now, false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + if (null != entity.getActualProbationaryDate()) { + formDataMap.put("actualProbationaryDate", personnelOrgUtils.dateToString(entity.getActualProbationaryDate(), "")); + } + registrationFormDataService.syncData(formDataMap, entity.getPhone(), entity.getId()); + } + private String buildGrowthLogDetail(String currOrg, + String currPosition, + String currRank, + String currReportsTo, + Date actualStartDate) { + StaffBaseInfoDto dto = new StaffBaseInfoDto(); + dto.setCurrOrg(currOrg); + ActionResult result = v2OrganizeApi.organizeInfoById(null, currOrg); + if (result!= null && result.getCode() == 200) { + dto.setCurrOrgName(result.getData().getName()); + } + dto.setCurrPosition(currPosition); + ActionResult voActionResult = v2PositionApi.infoPosition(currPosition); + if (voActionResult!= null && voActionResult.getCode() == 200) { + dto.setCurrPositionName(voActionResult.getData().getFullName()); + } + if (StringUtils.isNotEmpty(currRank)) { + dto.setCurrRank(currRank); + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(currRank); + if (gradeVOActionResult!= null && gradeVOActionResult.getCode() == 200) { + dto.setCurrRankName(gradeVOActionResult.getData().getName()); + } + } + if (StringUtils.isNotEmpty(currReportsTo)) { + dto.setReportsTo(currReportsTo); + UserEntity userEntity = personnelOrgUtils.queryUserInfo(currReportsTo); + if (null != userEntity) { + dto.setReportsToName(userEntity.getRealName()); + } + } + dto.setActualStartDate(actualStartDate); + return JSONUtil.toJsonStr(dto); + } + /** + * 同步转正管理 + * + * @param entity + * @param probationPeriod + * @param probationPeriodDay + * @param currReportsTo + * @param planProbationaryDate + */ + private void syncRegular(FtbPersonnelsStaffRoster entity, + String probationPeriod, + String probationPeriodDay, + String currReportsTo, + Date planProbationaryDate) { + if (!(("302".equals(entity.getWorkerStatus()) || "306".equals(entity.getWorkerStatus())) && !probationPeriod.equals("100"))) return; + FtbPersonnelsRegularCreateDTO createDTO = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularInfoVO(entity); + ActionResult> infoById = v2OrganizeApi.organizesByOrganizeIds(List.of(entity.getCurrOrg())); + if (infoById.getCode() == 200) { + createDTO.setOrgName(infoById.getData().stream().findFirst().get().getName()); + } + UserBoundVO userEntity = StringUtils.isEmpty(currReportsTo) ? null : getUserPrimaryBoundVO(v2UserApi.getUserPrimaryBoundBatch(List.of(currReportsTo), null)) ; + if (userEntity != null) { + createDTO.setImmediateSuperName(userEntity.getUserName()); + createDTO.setImmediateSuperId(currReportsTo); + } + ActionResult voActionResult = v2PositionApi.infoPosition(createDTO.getRegularPostId()); + if (voActionResult!= null && voActionResult.getCode() == 200) { + PositionVO positionVO = voActionResult.getData(); + createDTO.setRegularPostName(positionVO.getFullName()); + createDTO.setOnboardPostName(positionVO.getFullName()); + } + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(createDTO.getRegularGradeId()); + if (gradeVOActionResult != null && gradeVOActionResult.getData() != null) { + createDTO.setRegularGradeName(gradeVOActionResult.getData().getFullName()); + createDTO.setOnboardGradeName(gradeVOActionResult.getData().getFullName()); + } + createDTO.setProbation(probationPeriod); + createDTO.setProbationPeriodDay(probationPeriodDay); + // 计划转正日期 + createDTO.setSchedConverDate(planProbationaryDate); + createDTO.setId(entity.getId()); + regularManagementService.applyForRegularization(createDTO); + } + + /** + * 提取数据 + * @param listActionResult + * @return + */ + private UserBoundVO getUserPrimaryBoundVO(ActionResult> listActionResult) { + if (listActionResult.getCode() == 200 && listActionResult.getData() != null){ + return listActionResult.getData().get(0); + } + return null; + } + + public void asyncDeleteHistory(List userIds) { + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + if (org.apache.commons.lang3.StringUtils.isEmpty(token)) { + token = ""; + } + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + personnelAsyncServiceUtils.deleteTurnoverUserHistory(userIds, tenantId, headers); + } + + + public String saveUserInfo(FtbPersonnelsStaffEmploymentApply apply, String userHeadLog,String... tenantIdS){ + // 生成账号建立关系信息 + SaveUserDTO saveUserDTO = new SaveUserDTO(); + saveUserDTO.setAccount(apply.getPhone()); + saveUserDTO.setRealName(apply.getWorkerName()); + saveUserDTO.setMobilePhone(apply.getPhone()); + saveUserDTO.setWorkerStatus(UserWorkStatusEnums.PRE_ONBOARDING.getCode()); + saveUserDTO.setAccount(apply.getPhone()); + saveUserDTO.setHeadIcon(userHeadLog); + saveUserDTO.setEntryDate(apply.getActualStartDate()); + UserBoundInfoDTO userBoundInfoDTO = new UserBoundInfoDTO(); + userBoundInfoDTO.setOrganizeId(apply.getCurrOrg()); + userBoundInfoDTO.setPositionId(apply.getCurrPosition()); + userBoundInfoDTO.setGradesId(apply.getCurrRank()); + userBoundInfoDTO.setStoreTeamId(apply.getCurrGroupId()); + saveUserDTO.setBoundInfoDTO(userBoundInfoDTO); + String tenantId = tenantIdS.length > 0 ? tenantIdS[0] : UserProvider.getUser().getTenantId(); + saveUserDTO.setTenantId(tenantId); + //解决组织架构分布式事务 行锁问题 不多次调用 组织架构修改用户接口一次完成 提前生成ID 租户->用户 + //生成主账号 + String userId = FtbUtil.getId(); + //判断是否离职 + UserCopyEntity copyInfo = v2UserApi.getUserCopyInfo(saveUserDTO.getMobilePhone()); + if (copyInfo != null && StrUtil.isNotBlank(copyInfo.getId())){ + userId = copyInfo.getId(); + } + UserAccountDto userAccountDto = new UserAccountDto(); + userAccountDto.setFid(userId); + userAccountDto.setUserName(apply.getWorkerName()); + userAccountDto.setTenantId(tenantId); + userAccountDto.setUserMobilePhone(apply.getPhone()); + userAccountDto.setHeadImageUrl(userHeadLog); + List addUserList = List.of(userAccountDto); + ActionResult> batchAddUserAccount = pTenantAccountApi.batchAddUserAccount(addUserList); + if(batchAddUserAccount == null || !batchAddUserAccount.getCode().equals(200)){ + // pTenantAccountApi.deleteUser(userId); + throw new RuntimeException("主账号生成失败"); + } + UserAccountDto accountDto = batchAddUserAccount.getData().stream().findFirst().orElse(new UserAccountDto()); + saveUserDTO.setId(userId); + saveUserDTO.setAccount(accountDto.getAccount()); + ActionResult actionResult = v2UserApi.saveUserInfo(saveUserDTO); + if (actionResult ==null || actionResult.getCode() != 200) { + // pTenantAccountApi.deleteUser(userId); + throw new RuntimeException(actionResult !=null ? actionResult.getMsg() : "保存用户信息失败!"); + } + return userId; + } + /** + * 同步薪酬数据 + * @param ftbEmpConfirmDTO + */ + private void syncSalaryData(FtbPersonnelsStaffEmploymentApply ftbEmpConfirmDTO) { + // 薪酬信息同步 + String userId = ftbEmpConfirmDTO.getUserId(); + String salaryData = ftbEmpConfirmDTO.getSalaryData(); + if (StringUtils.isEmpty(salaryData) || "null".equals(salaryData) || "[]".equals(salaryData)) return; + if (ftbEmpConfirmDTO.getIsAdjustSalary() == 1 && StringUtils.isNotEmpty(salaryData)) { + List salaryItemList = JsonUtil.getJsonToList(salaryData, FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + ActionResult actionResult = v2OrganizeApi.organizeInfoById(null, ftbEmpConfirmDTO.getCurrOrg()); + userInfoWithSalary.setFOrgId(ftbEmpConfirmDTO.getCurrOrg()); + if (actionResult != null && actionResult.getCode() == 200) { + userInfoWithSalary.setFOrgName(actionResult.getData().getName()); + } + ActionResult infoPosition = v2PositionApi.infoPosition(ftbEmpConfirmDTO.getCurrPosition()); + userInfoWithSalary.setPostId(ftbEmpConfirmDTO.getCurrPosition()); + if (infoPosition != null && infoPosition.getCode() == 200) { + userInfoWithSalary.setPostName(infoPosition.getData().getFullName()); + } + if (StringUtils.isNotEmpty(ftbEmpConfirmDTO.getCurrRank())) { + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(ftbEmpConfirmDTO.getCurrRank()); + userInfoWithSalary.setRankId(ftbEmpConfirmDTO.getCurrRank()); + if (gradeVOActionResult != null && gradeVOActionResult.getCode() == 200) { + userInfoWithSalary.setRankName(gradeVOActionResult.getData().getFullName()); + } + } + String creatorUserId = ftbEmpConfirmDTO.getCreatorUserId(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, creatorUserId); + FtbPersonnelsStaffRoster serviceOne = rosterService.getOne(queryWrapper); + if (serviceOne != null) userInfoWithSalary.setOperatorWorkNo(serviceOne.getSystemWokerId()); + userInfoWithSalary.setTaskId(ftbEmpConfirmDTO.getTaskInfoId()); + // 入职时间 + userInfoWithSalary.setEntryDate(ftbEmpConfirmDTO.getActualStartDate()); + // 工作状态是正式 入职时间 作为转正时间 + if ("303".equals(ftbEmpConfirmDTO.getWorkerStatus())) userInfoWithSalary.setBeComeDate(ftbEmpConfirmDTO.getActualStartDate()); + userInfoWithSalary.setUserTypeId(ftbEmpConfirmDTO.getWorkerType()); + //生效日期取,实际 入职日期 + personnelSalaryService.saveTheChangePayInformation( + salaryItemList, + userId, + ftbEmpConfirmDTO.getActualStartDate(), + userInfoWithSalary, + "2",//和薪酬沟通传入固定值2 + ftbEmpConfirmDTO.getReasonsForSalaryAdjustments(), + "4", 0,null,ftbEmpConfirmDTO.getPayrollSequenceId()); + } + } + + /** + * 同步员工状态 + * + * @param staffRoster + * @param + */ + private void synchronizeEmployeeStatus(FtbPersonnelsStaffRoster staffRoster, String leaderId) { + if (ObjectUtil.isEmpty(staffRoster)) return; + String userId = staffRoster.getUserId(); + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setEntryDate(staffRoster.getActualStartDate()); + userDTO.setWorkerStatus(staffRoster.getWorkerStatus()); + userDTO.setSystemWorkerId(staffRoster.getSystemWokerId()); + UserBoundInfoDTO boundInfoDTO = new UserBoundInfoDTO(); + boundInfoDTO.setOrganizeId(staffRoster.getCurrOrg()); + boundInfoDTO.setPositionId(staffRoster.getCurrPosition()); + boundInfoDTO.setGradesId(staffRoster.getCurrRank()); + boundInfoDTO.setStoreTeamId(staffRoster.getCurrGroupId()); + boundInfoDTO.setLeaderId(leaderId); + userDTO.setBoundInfoDTO(boundInfoDTO); + System.out.print(JSONObject.toJSONString(userDTO)); + ActionResult actionResult = v2UserApi.updateUserInfo(userId, userDTO); + if (actionResult == null || !actionResult.getCode().equals(200)) { + throw new RuntimeException("更新用户信息失败"); + } + Arrays.stream(JobStatusEnum.values()).filter(item -> item.getName().equals(staffRoster.getWorkerType())).findFirst().ifPresent(statusEnum ->{ + ActionResult actionResult1 = v2UserApi.updateUserJobStatus(statusEnum, userId); + if (actionResult1 == null || !actionResult1.getCode().equals(200)) { + throw new RuntimeException("更新用户状态失败"); + } + }); + attendanceMessageNotifications(staffRoster.getActualStartDate(), staffRoster.getCurrOrg(), userId); + } + + /** + * 考勤消息通知 + * @param entryDate + * @param organizeId + * @param userId + */ + private void attendanceMessageNotifications(Date entryDate,String organizeId,String userId) { + // 考勤消息通知 + Map hashMap = new HashMap<>(); + hashMap.put("userId", userId); + hashMap.put("entryDate", entryDate); + hashMap.put("organizeId", organizeId); + hashMap.put("tenantId", UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageOnboarding(JSONObject.toJSONString(hashMap)); + } + + /** + * 员工成长 + * @param staffRoster + * @param affiliatedReportsTo 直属主管 + * @param actualStartDate 入职时间 + */ + private void employeeGrowth(FtbPersonnelsStaffRoster staffRoster, + String affiliatedReportsTo, + Date actualStartDate) { + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(staffRoster.getUserId()); + addGrowthLogDto.setChangeDate(new Date()); + if (staffRoster.getJoinNum() > 1) { + addGrowthLogDto.setGrowthType(GrowthLogEnum.REPEAT_JOIN.getCode()); + addGrowthLogDto.setNum(staffRoster.getJoinNum()); + } else { + addGrowthLogDto.setGrowthType(GrowthLogEnum.FIRST_JOIN.getCode()); + } + String currRank = staffRoster.getCurrRank(); + String currPosition = staffRoster.getCurrPosition(); + String currOrg = staffRoster.getCurrOrg(); + + addGrowthLogDto.setDetail(buildGrowthLogDetail(currOrg, currPosition, currRank, + affiliatedReportsTo, + actualStartDate)); + addGrowthLogDto.setEmployeeId(staffRoster.getSystemWokerId()); + addGrowthLogDto.setActualStartDate(actualStartDate); + // famgch + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId, staffRoster.getUserId()); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getActualStartDate, staffRoster.getActualStartDate()); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getGrowthType, 0); + FtbPersonnelsStaffGrowthLog one = growthLogService.getOne(queryWrapper); + if (one != null) { + // 删除已有的 + growthLogService.remove(queryWrapper); + } + // 入职的 + growthLogService.addGrowthLog(addGrowthLogDto); + // 删除历史记录 + asyncDeleteHistory(List.of(staffRoster.getUserId())); + } + + /** + * 所属组织-岗位-职等校验 + */ + public void doCheckOrgPositionGradesBoundDTO(QueryUserBoundDTO dto) { + String positionId = dto.getPositionId(); + QueryGradeListDTO listDTO = new QueryGradeListDTO(); + listDTO.setPositionId(positionId); + ActionResult> listGrades = v2GradesApi.listGrades(listDTO); + if (listGrades.getCode() == 200 && CollUtil.isNotEmpty(listGrades.getData())) { + if (listGrades.getData().stream().noneMatch(v -> v.getId().equals(dto.getGradeId()))) { + throw new RuntimeException("该员工入职的目标职级不存在,无法继续提交!"); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmployeeTypeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmployeeTypeServiceImpl.java new file mode 100644 index 0000000..d111665 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsEmployeeTypeServiceImpl.java @@ -0,0 +1,116 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.employeetype.FtbEmployeeTypeAddDTO; +import jnpf.model.personnels.dto.employeetype.FtbEmployeeTypeEditDTO; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsEmployeeTypeService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsEmployeeTypeServiceImpl implements FtbPersonnelsEmployeeTypeService { + + @Resource + private FtbPersonnelsRegistrationFormFieldOptionMapper formFieldOptionMapper; + + @Resource + private FtbPersonnelsStaffRosterMapper staffRosterMapper; + + private final static String WORKER_TYPE = "workerType"; + + @Override + @Transactional(rollbackFor = Exception.class) + public void addEmployeeType(FtbEmployeeTypeAddDTO employeeType) { + // 类型数量上限20个 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, WORKER_TYPE); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + if (formFieldOptionMapper.selectCount(query) > 20) { + throw new RuntimeException("类型数量上限20个"); + } + // 类型名称不能重复 + query.eq(FtbPersonnelsRegistrationFormFieldOption::getName, employeeType.getName()); + if (formFieldOptionMapper.selectCount(query) > 0) { + throw new RuntimeException("类型名称不能重复"); + } + FtbPersonnelsRegistrationFormFieldOption option = new FtbPersonnelsRegistrationFormFieldOption(); + option.setName(employeeType.getName()); + option.setFormFieldId(WORKER_TYPE); + option.setEnabledMark(0); + formFieldOptionMapper.insert(option); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void editEmployeeType(FtbEmployeeTypeEditDTO employeeType) { + // 类型名称不能重复 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getName, employeeType.getName()); + query.ne(SuperBaseEntity.SuperIBaseEntity::getId, employeeType.getId()); + if (formFieldOptionMapper.selectCount(query) > 0) { + throw new RuntimeException("类型名称不能重复"); + } + LambdaUpdateWrapper update = new LambdaUpdateWrapper<>(); + update.eq(SuperBaseEntity.SuperIBaseEntity::getId, employeeType.getId()); + update.set(FtbPersonnelsRegistrationFormFieldOption::getName, employeeType.getName()); + formFieldOptionMapper.update(new FtbPersonnelsRegistrationFormFieldOption(), update); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteEmployeeType(String id) { + // 校验该类型是否已关联员工,提示语:该类型已绑定员工,不可删除! + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsStaffRoster::getWorkerType, id); + query.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + if (staffRosterMapper.selectCount(query) > 0) { + throw new RuntimeException("该类型已绑定员工,不可删除!"); + } + LambdaUpdateWrapper update = new LambdaUpdateWrapper<>(); + update.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, WORKER_TYPE); + update.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + update.set(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 1); + formFieldOptionMapper.update(new FtbPersonnelsRegistrationFormFieldOption(), update); + } + + @Override + public List employeeList() { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, WORKER_TYPE); + query.orderByAsc(SuperBaseEntity.SuperCBaseEntity::getCreatorTime); + List ftbPersonnelsRegistrationFormFieldOptions = formFieldOptionMapper.selectList(query); + return ftbPersonnelsRegistrationFormFieldOptions + .stream() + .map(FtbPersonnelsEmployeeTypeVO::convert) + .collect(Collectors.toList()); + } + + @Override + public Map getEmployeeTypeByUserIds(List userIds) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.in(FtbPersonnelsStaffRoster::getUserId, userIds); + List ftbPersonnelsStaffRosters = staffRosterMapper.selectList(query); + List emtbPersonnelsEmployeeTypes = employeeList(); + Map emtbPersonnelsEmployeeTypeMap = emtbPersonnelsEmployeeTypes + .stream() + .collect(Collectors.toMap(FtbPersonnelsEmployeeTypeVO::getId, a -> a)); + return ftbPersonnelsStaffRosters + .stream() + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, + a -> emtbPersonnelsEmployeeTypeMap.getOrDefault(a.getWorkerType(), new FtbPersonnelsEmployeeTypeVO()))); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsReceiveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsReceiveServiceImpl.java new file mode 100644 index 0000000..11d146e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsReceiveServiceImpl.java @@ -0,0 +1,247 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveAddDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveReturnDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsReceiveUpdateDTO; +import jnpf.model.personnels.po.FtbPersonnelsGoods; +import jnpf.model.personnels.po.FtbPersonnelsGoodsReceive; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceiveDetailsVO; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsReceivePageVO; +import jnpf.model.personnels.vo.goods.PersonnelsGoodsDistributedLock; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsGoodsMapper; +import jnpf.personnels.mapper.FtbPersonnelsGoodsReceiveMapper; +import jnpf.personnels.service.FtbPersonnelsGoodsReceiveService; +import jnpf.util.UserProvider; +import org.jetbrains.annotations.NotNull; +import org.redisson.Redisson; +import org.redisson.api.RLock; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** +* +* +*@Author: peng.hao +*@create: 2025/9/11 +* +*/ +@Service +public class FtbPersonnelsGoodsReceiveServiceImpl implements FtbPersonnelsGoodsReceiveService { + + @Resource + private FtbPersonnelsGoodsReceiveMapper ftbPersonnelsGoodsReceiveMapper; + @Resource + private FtbPersonnelsGoodsMapper ftbPersonnelsGoodsMapper; + @Resource + private Redisson redisson; + @Resource + private V2UserApi v2UserApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(List form) { + List goodIds = form.stream() + .flatMap(ftbPersonnelsGoodsReceiveAddDTO -> ftbPersonnelsGoodsReceiveAddDTO.getItemsAreCollected().stream()) + .map(FtbPersonnelsGoodsReceiveAddDTO.ItemsAreCollected::getGoodsId) + .distinct() + .collect(Collectors.toList()); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(FtbPersonnelsGoods::getId, goodIds); + queryWrapper.eq(FtbPersonnelsGoods::getEnableMark, 0); + List ftbPersonnelsGoods = ftbPersonnelsGoodsMapper.selectList(queryWrapper); + Map stringFtbPersonnelsGoodsMap = ftbPersonnelsGoods.stream() + .collect(Collectors.toMap(FtbPersonnelsGoods::getId, Function.identity())); + // 部分物品被删除,请重新提交! + if (stringFtbPersonnelsGoodsMap.size() != goodIds.size()) { + throw new RuntimeException("部分物品被删除,请重新提交!"); + } + // 如果所选员工的数量和领用数量的乘积数量小于剩余数量,需提示:XX物品剩余数量不足,请重新选择 + Map goodsNumbers = form.stream() + .flatMap(ftbPersonnelsGoodsReceiveAddDTO -> ftbPersonnelsGoodsReceiveAddDTO.getItemsAreCollected().stream()) + .collect(Collectors.groupingBy(FtbPersonnelsGoodsReceiveAddDTO.ItemsAreCollected::getGoodsId + , Collectors.summingLong(FtbPersonnelsGoodsReceiveAddDTO.ItemsAreCollected::getNumber))); + for (Map.Entry stringLongEntry : goodsNumbers.entrySet()) { + FtbPersonnelsGoods ftbPersonnelGood = stringFtbPersonnelsGoodsMap.get(stringLongEntry.getKey()); + if (ftbPersonnelGood.getReturnedQuantity() < stringLongEntry.getValue()) { + throw new RuntimeException(ftbPersonnelGood.getGoodsName() + "物品剩余数量不足,请重新选择"); + } + } + List userIds = form.stream().map(FtbPersonnelsGoodsReceiveAddDTO::getUserId).collect(Collectors.toList()); + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, null); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + throw new RuntimeException("获取员工信息失败!"); + } + Map userBoundVOMap = userPrimaryBoundBatch.getData().stream() + .collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + form.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setOrgName(userBoundVO.getOrganizeName()); + a.setUserName(userBoundVO.getName()); + } + }); + // 数据组装 + UserInfo userInfo = UserProvider.getUser(); + List ftbPersonnelsGoodsReceives = new ArrayList<>(); + for (FtbPersonnelsGoodsReceiveAddDTO ftbPersonnelsGoodsReceiveAddDTO : form) { + for (FtbPersonnelsGoodsReceiveAddDTO.ItemsAreCollected itemsAreCollected : ftbPersonnelsGoodsReceiveAddDTO.getItemsAreCollected()) { + if (itemsAreCollected.getNumber() == 0) { + continue; + } + FtbPersonnelsGoodsReceive ftbPersonnelsGoodsReceive = new FtbPersonnelsGoodsReceive(); + ftbPersonnelsGoodsReceive.setOperatorName(userInfo.getUserName()); + ftbPersonnelsGoodsReceive.setUserId(ftbPersonnelsGoodsReceiveAddDTO.getUserId()); + ftbPersonnelsGoodsReceive.setUserName(ftbPersonnelsGoodsReceiveAddDTO.getUserName()); + ftbPersonnelsGoodsReceive.setOrgName(ftbPersonnelsGoodsReceiveAddDTO.getOrgName()); + ftbPersonnelsGoodsReceive.setReceiptTime(ftbPersonnelsGoodsReceiveAddDTO.getReceiptTime()); + ftbPersonnelsGoodsReceive.setRemark(ftbPersonnelsGoodsReceiveAddDTO.getRemark()); + ftbPersonnelsGoodsReceive.setGoodsId(itemsAreCollected.getGoodsId()); + ftbPersonnelsGoodsReceive.setQuantityUse(itemsAreCollected.getNumber()); + FtbPersonnelsGoods ftbPersonnelGood = stringFtbPersonnelsGoodsMap.get(itemsAreCollected.getGoodsId()); + // 是否需要归还(0-否,1-是) + if (ftbPersonnelGood.getNeedReturn() == 1) { + ftbPersonnelsGoodsReceive.setReturnStatus(0); + } else { + ftbPersonnelsGoodsReceive.setReturnStatus(3); + } + ftbPersonnelsGoodsReceives.add(ftbPersonnelsGoodsReceive); + // 扣减库存 + doUpdateInventory(itemsAreCollected.getGoodsId(), -itemsAreCollected.getNumber()); + } + } + Db.saveBatch(ftbPersonnelsGoodsReceives); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(FtbPersonnelsGoodsReceiveUpdateDTO form) { + FtbPersonnelsGoodsReceiveAddDTO.ItemsAreCollected itemsAreCollected = form.getItemsAreCollected().get(0); + FtbPersonnelsGoodsReceive ftbPersonnelsGoodsReceive = ftbPersonnelsGoodsReceiveMapper.selectById(form.getId()); + // 如果所选员工领用数量小于剩余数量,需提示:XX物品剩余数量不足,请重新选择 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPersonnelsGoods::getId, itemsAreCollected.getGoodsId()); + FtbPersonnelsGoods ftbPersonnelGood = ftbPersonnelsGoodsMapper.selectOne(queryWrapper); + if (ftbPersonnelGood.getReturnedQuantity() < (itemsAreCollected.getNumber() - ftbPersonnelsGoodsReceive.getQuantityUse()) + && !itemsAreCollected.getNumber().equals(ftbPersonnelsGoodsReceive.getQuantityUse())) { + throw new RuntimeException(ftbPersonnelGood.getGoodsName() + "物品剩余数量不足,请重新提交"); + } + // 领用数量大于之前的领用数量,扣减库存,小于之前的领用数量,增加库存 + long quantityUse = ftbPersonnelsGoodsReceive.getQuantityUse() - itemsAreCollected.getNumber(); + doUpdateInventory(itemsAreCollected.getGoodsId(), quantityUse); + LambdaUpdateWrapper updateWrapper = form.convert(form); + updateWrapper.set(FtbPersonnelsGoodsReceive::getQuantityUse, itemsAreCollected.getNumber()); + updateWrapper.set(FtbPersonnelsGoodsReceive::getOperatorName, UserProvider.getUser().getUserName()); + ftbPersonnelsGoodsReceiveMapper.update(new FtbPersonnelsGoodsReceive(), updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(String id) { + FtbPersonnelsGoodsReceive ftbPersonnelsGoodsReceive = ftbPersonnelsGoodsReceiveMapper.selectById(id); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbPersonnelsGoodsReceive::getEnableMark, 1); + // 更新库存 + //doUpdateInventory(ftbPersonnelsGoodsReceive.getGoodsId(), ftbPersonnelsGoodsReceive.getQuantityUse() - ftbPersonnelsGoodsReceive.getReturnedQuantity()); + ftbPersonnelsGoodsReceiveMapper.update(new FtbPersonnelsGoodsReceive(), updateWrapper); + } + + @Override + public Page list(Page page, FtbPersonnelsGoodsReceiveQueryDTO formQueryDTO) { + page = ftbPersonnelsGoodsReceiveMapper.listPages(page, formQueryDTO); + return page; + } + + @Override + public FtbPersonnelsGoodsReceiveDetailsVO details(String id) { + FtbPersonnelsGoodsReceive ftbPersonnelsGoodsReceive = ftbPersonnelsGoodsReceiveMapper.selectById(id); + FtbPersonnelsGoodsReceiveDetailsVO goodsReceiveDetailsVO = FtbPersonnelsGoodsReceiveDetailsVO.convert(ftbPersonnelsGoodsReceive); + // 物品名称和剩余数量 + FtbPersonnelsGoods ftbPersonnelsGoods = ftbPersonnelsGoodsMapper.selectById(ftbPersonnelsGoodsReceive.getGoodsId()); + goodsReceiveDetailsVO.setGoodsName(ftbPersonnelsGoods.getGoodsName()); + goodsReceiveDetailsVO.setRemainingQuantity(ftbPersonnelsGoods.getReturnedQuantity()); + return goodsReceiveDetailsVO; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void returnGoods(FtbPersonnelsGoodsReceiveReturnDTO returnDTO) { + LambdaUpdateWrapper updateWrapper = returnDTO.convert(returnDTO); + FtbPersonnelsGoodsReceive ftbPersonnelsGoodsReceive = ftbPersonnelsGoodsReceiveMapper.selectById(returnDTO.getId()); + // 校验物品是否已被归还或者部分归还,物品已归还或者部分归还后,提示物品已归还,请勿重复操作! + if (ftbPersonnelsGoodsReceive.getReturnStatus() == 2) { + throw new RuntimeException("物品已归还,请勿重复操作!"); + } + // 增加库存 + doUpdateInventory(ftbPersonnelsGoodsReceive.getGoodsId(), returnDTO.getReturnedQuantity()); + updateWrapper.set(FtbPersonnelsGoodsReceive::getOperatorName, UserProvider.getUser().getUserName()); + ftbPersonnelsGoodsReceiveMapper.update(new FtbPersonnelsGoodsReceive(), updateWrapper); + } + + + /** + * 更新库存 + * + * @param goodsId 商品ID + * @param newReturnedQuantity 新退货数量(正数为增加库存,负数为扣减库存) + */ + private void doUpdateInventory(String goodsId, Long newReturnedQuantity) { + String lockGoods = String.format(PersonnelsGoodsDistributedLock.LOCK_KEY_GOODS, goodsId); + RLock rLock = redisson.getFairLock(lockGoods); + try { + // 尝试获取锁,最多等10秒,持有锁10秒后自动释放 + if (!rLock.tryLock(10, 10, java.util.concurrent.TimeUnit.SECONDS)) { + throw new RuntimeException("当前操作该物品任务过多,请稍后再试"); + } + // 更新物品信息 + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, goodsId); + String setSql = getSetSql(newReturnedQuantity); + updateWrapper.setSql(setSql); + ftbPersonnelsGoodsMapper.update(new FtbPersonnelsGoods(), updateWrapper); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("系统繁忙,请稍后重试", e); + } finally { + // 检查当前线程是否持有锁,避免解锁异常 + if (rLock.isHeldByCurrentThread() && rLock.isLocked()) { + rLock.unlock(); + } + } + } + + @NotNull + private static String getSetSql(Long newReturnedQuantity) { + String setSql = ""; + if (newReturnedQuantity > 0) { + // 增加库存:直接相加 + setSql = "F_ReturnedQuantity = F_ReturnedQuantity + " + newReturnedQuantity; + } else if (newReturnedQuantity < 0) { + // 扣减库存:确保不会出现负数 + setSql = "F_ReturnedQuantity = CASE WHEN F_ReturnedQuantity >= " + + Math.abs(newReturnedQuantity) + " THEN F_ReturnedQuantity - " + + Math.abs(newReturnedQuantity) + " ELSE 0 END"; + } + return setSql; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsServiceImpl.java new file mode 100644 index 0000000..ad7ce49 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsGoodsServiceImpl.java @@ -0,0 +1,143 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormQueryDTO; +import jnpf.model.personnels.dto.goods.FtbPersonnelsGoodsFormUpdateDTO; +import jnpf.model.personnels.po.FtbPersonnelsGoods; +import jnpf.model.personnels.po.FtbPersonnelsGoodsReceive; +import jnpf.model.personnels.vo.goods.FtbPersonnelsGoodsPageVO; +import jnpf.model.personnels.vo.goods.PersonnelsGoodsDistributedLock; +import jnpf.personnels.mapper.FtbPersonnelsGoodsMapper; +import jnpf.personnels.mapper.FtbPersonnelsGoodsReceiveMapper; +import jnpf.personnels.service.FtbPersonnelsGoodsService; +import org.redisson.Redisson; +import org.redisson.api.RLock; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +/** + * + * + *@Author: peng.hao + *@create: 2025/9/11 + * + */ +@Service +public class FtbPersonnelsGoodsServiceImpl implements FtbPersonnelsGoodsService { + + @Resource + private FtbPersonnelsGoodsMapper ftbPersonnelsGoodsMapper; + @Resource + private FtbPersonnelsGoodsReceiveMapper ftbPersonnelsGoodsReceiveMapper; + @Resource + private Redisson redisson; + + @Override + @Transactional(rollbackFor = Exception.class) + public void create(FtbPersonnelsGoodsFormDTO form) { + // 校验物品名称是否重复 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPersonnelsGoods::getGoodsName, form.getGoodsName()); + queryWrapper.eq(FtbPersonnelsGoods::getEnableMark, 0); + if (ftbPersonnelsGoodsMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("物品名称已存在"); + } + // 校验物品编号是否重复 + queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPersonnelsGoods::getGoodsCode, form.getGoodsCode()); + queryWrapper.eq(FtbPersonnelsGoods::getEnableMark, 0); + if (ftbPersonnelsGoodsMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("物品编号已存在"); + } + FtbPersonnelsGoods ftbPersonnelsGoods = form.convert(form); + ftbPersonnelsGoodsMapper.insert(ftbPersonnelsGoods); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(FtbPersonnelsGoodsFormUpdateDTO form) { + // 校验物品名称是否重复 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.ne(SuperBaseEntity.SuperIBaseEntity::getId, form.getId()); + queryWrapper.eq(FtbPersonnelsGoods::getGoodsName, form.getGoodsName()); + queryWrapper.eq(FtbPersonnelsGoods::getEnableMark, 0); + if (ftbPersonnelsGoodsMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("物品名称已存在"); + } + String lockGoods = String.format(PersonnelsGoodsDistributedLock.LOCK_KEY_GOODS, form.getId()); + RLock rLock = redisson.getFairLock(lockGoods); + try { + // 尝试获取锁,最多等10秒,持有锁10秒后自动释放 + if (!rLock.tryLock(10, 10, java.util.concurrent.TimeUnit.SECONDS)) { + throw new RuntimeException("当前操作该物品任务过多,请稍后再试"); + } + // 查询当前物品信息 + queryWrapper.clear(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, form.getId()); + FtbPersonnelsGoods ftbPersonnelsGoods = ftbPersonnelsGoodsMapper.selectOne(queryWrapper); + // 检查物品是否存在 + if (ftbPersonnelsGoods == null) { + throw new RuntimeException("物品信息不存在"); + } + // 计算已使用的物品数量 + long usedQuantity = ftbPersonnelsGoods.getQuantity() - ftbPersonnelsGoods.getReturnedQuantity(); + // 物品数量不能小于已领用的数量总额 + if (form.getQuantity() < usedQuantity) { + throw new RuntimeException("物品数量不能小于已领用的数量总额"); + } + // 计算新的剩余数量 + long newReturnedQuantity = ftbPersonnelsGoods.getReturnedQuantity() + + (form.getQuantity() - ftbPersonnelsGoods.getQuantity()); + // 更新物品信息 + LambdaUpdateWrapper updateWrapper = form.convert(form); + updateWrapper.set(FtbPersonnelsGoods::getReturnedQuantity, newReturnedQuantity); + ftbPersonnelsGoodsMapper.update(new FtbPersonnelsGoods(), updateWrapper); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("系统繁忙,请稍后重试", e); + } finally { + // 检查当前线程是否持有锁,避免解锁异常 + if (rLock.isHeldByCurrentThread() && rLock.isLocked()) { + rLock.unlock(); + } + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(String id) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbPersonnelsGoods::getEnableMark, 1); + ftbPersonnelsGoodsMapper.update(new FtbPersonnelsGoods(), updateWrapper); + } + + @Override + public Page list(Page page, FtbPersonnelsGoodsFormQueryDTO formQueryDTO) { + page = ftbPersonnelsGoodsMapper.listPages(page, formQueryDTO); + return page; + } + + @Override + public FtbPersonnelsGoodsPageVO details(String id) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + FtbPersonnelsGoods ftbPersonnelsGoods = ftbPersonnelsGoodsMapper.selectOne(queryWrapper); + return FtbPersonnelsGoodsPageVO.convert(ftbPersonnelsGoods); + } + + @Override + public Boolean deleteConfirm(String id) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPersonnelsGoodsReceive::getGoodsId, id); + queryWrapper.in(FtbPersonnelsGoodsReceive::getReturnStatus, 0, 1); + queryWrapper.eq(FtbPersonnelsGoodsReceive::getEnableMark, 0); + return ftbPersonnelsGoodsReceiveMapper.selectCount(queryWrapper) > 0; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsMetaDataServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsMetaDataServiceImpl.java new file mode 100644 index 0000000..90d572c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsMetaDataServiceImpl.java @@ -0,0 +1,788 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.model.enums.EmployeeMetaDataType; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.dto.roster.meta.*; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaDataReq; +import jnpf.model.personnels.req.roster.FtbPersonnelsMetaFuctionReq; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.permission.ContractTypeApi; +import jnpf.permission.RoleApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.model.role.RoleListVO; +import jnpf.permission.model.role.UserBoundRolesVO; +import jnpf.permission.vo.contract.ContractTypeVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsMetaDataService; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormFieldOptionService; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.util.JsonUtil; +import jnpf.yozo.utils.HttpRequestUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.Period; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsMetaDataServiceImpl implements FtbPersonnelsMetaDataService { + + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + @Autowired + private FtbPersonnelsStaffEmploymentApplyMapper ftbPersonnelsStaffEmploymentApplyMapper; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private RoleApi roleApi; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionService fieldOptionService; + + @Autowired + private ContractTypeApi contractTypeApi; + + @Autowired + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Override + public List getMetaData(FtbPersonnelsMetaDataReq info) { + log.error("getMetaData={}", info); + + List userIds = info.getUserIds(); + List dataTypeList = info.getDataType(); + List retList = new ArrayList<>(); + if (CollectionUtil.isEmpty(userIds)) { + return retList; + } + if (CollectionUtil.isEmpty(dataTypeList)) { + return retList; + } + Page page = new Page(); + page.setSize(-1); + page.setCurrent(1); + Page ftbPersonnelsStaffRosterDtoPage = ftbPersonnelsStaffRosterMapper.pagingQueryNewWithTurnoverManagerList(new StaffRosterListReq(), userIds, page); + List records = ftbPersonnelsStaffRosterDtoPage.getRecords(); + if (CollectionUtil.isEmpty(records)) { + return new ArrayList<>(); + } + List rosterList = records.stream().map(personnel -> { + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = JsonUtil.getJsonToBean(personnel, FtbPersonnelsStaffRoster.class); + return ftbPersonnelsStaffRoster; + }).collect(Collectors.toList()); + + + List rosterIds = new ArrayList<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { + if(StrUtil.isNotEmpty(roster.getId())) { + rosterIds.add(roster.getId()); + } + } + List allList = registrationFormDataService.queryFormFieldValueForRosterIds(rosterIds); + Map> allFormDataMap = allList.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)); + + + Map> orgMap = new HashMap<>(); + + if (dataTypeList.contains(EmployeeMetaDataType.ORGANIZATION_INFO)) { + orgMap = personnelOrgUtils.getUserOrgBoundInfoForUserListAndTenantId(userIds, info.getTenantCode(), false); + Set allOrgIds = new HashSet<>(); + if (CollectionUtil.isNotEmpty(orgMap)) { + for (Map.Entry> entry : orgMap.entrySet()) { + List workerGroupDataDtos = entry.getValue(); + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtos) { + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedOrg())) { + allOrgIds.add(workerGroupDataDto.getAffiliatedOrg()); + } + } + } + Map orgToParentOrgInfoMap = personnelOrgUtils.queryParentOrgInfo(allOrgIds, info.getTenantCode()); + if (CollectionUtil.isNotEmpty(orgToParentOrgInfoMap)) { + for (Map.Entry> entry : orgMap.entrySet()) { + List workerGroupDataDtos = entry.getValue(); + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtos) { + OrganizeEntity parentOrganizeEntity = orgToParentOrgInfoMap.get(workerGroupDataDto.getAffiliatedOrg()); + if (parentOrganizeEntity != null) { + workerGroupDataDto.setParentOrg(parentOrganizeEntity.getId()); + workerGroupDataDto.setParentOrgEncode(parentOrganizeEntity.getEnCode()); + workerGroupDataDto.setParentOrgName(parentOrganizeEntity.getFullName()); + } + } + } + } + } + + } + + Map> roleDtoMap = new HashMap<>(); + if (dataTypeList.contains(EmployeeMetaDataType.ROLE)) { + roleDtoMap = queryRoleListForUserIdsNoToken(userIds, info.getTenantCode()); + } + + Map optionMap = queryAllOption(); + + for (FtbPersonnelsStaffRoster roster : rosterList) { + PersonnelsMetaDTO dto = new PersonnelsMetaDTO(); + List formDataList = allFormDataMap.get(roster.getId()); + List workerGroupDataDtoList = orgMap.get(roster.getUserId()); + List roleListVOS = roleDtoMap.get(roster.getUserId()); + for (EmployeeMetaDataType employeeMetaDataType : dataTypeList) { + fillMetaData(dto, roster, formDataList, workerGroupDataDtoList, roleListVOS, optionMap, employeeMetaDataType); + } + retList.add(dto); + } + + return retList; + } + + @Override + public Boolean queryCurrMonthDepartStatus(String userId, String tenantId) { + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByUserId(userId); + if (roster == null) { + throw new RuntimeException("员工不存在"); + } + if (roster.getEnabledMark() == 1) { + throw new RuntimeException("员工不存在"); + } + if (roster.getWorkerStatus().equals(StaffWorkerStatus.RESIGNED.getCode())) { + if (roster.getDepartDate() != null) { + if (isSameYearAndMonth(roster.getDepartDate())) { + return true; + } + } + } + return false; + } + + @Override + public Boolean queryCurrMonthDepartStatusByDate(String userId, String tenantId, Date startDate, Date endDate) { + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByUserId(userId); + if (roster == null) { + throw new RuntimeException("员工不存在"); + } + if (roster.getEnabledMark() == 1) { + throw new RuntimeException("员工不存在"); + } + if (roster.getWorkerStatus().equals(StaffWorkerStatus.RESIGNED.getCode())) { + if (roster.getDepartDate() != null) { + //判断离职离职是否大于开始小于结束 + return roster.getDepartDate().compareTo(startDate) >= 0 && roster.getDepartDate().compareTo(endDate) <= 0; + } + } + return false; + } + + @Override + public Map queryCurrMonthLeave(FtbPersonnelsMetaFuctionReq req) { + if (CollUtil.isEmpty(req.getUserIds())){ + throw new RuntimeException("用户ID不能为空"); + } + Map result = new HashMap<>(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery() + .in(FtbPersonnelsStaffRoster::getUserId, req.getUserIds()) + .ge(FtbPersonnelsStaffRoster::getDepartDate,req.getStartDate()) + .le(FtbPersonnelsStaffRoster::getDepartDate,req.getEndDate()); + List rosters = ftbPersonnelsStaffRosterMapper.selectList(queryWrapper); + Map rosterMap = rosters.stream() + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, Function.identity(), (a, b) -> b)); + for (String userId : req.getUserIds()) { + Boolean leaveStatus = Boolean.FALSE; + FtbPersonnelsStaffRoster aDefault = rosterMap.getOrDefault(userId, null); + if (aDefault != null) { + leaveStatus = Boolean.TRUE; + } + result.put(userId, leaveStatus); + } + return result; + } + + public static boolean isSameYearAndMonth(Date targetDate) { + Calendar now = Calendar.getInstance(); + Calendar target = Calendar.getInstance(); + target.setTime(targetDate); + + // 获取当前年份和月份 + int currentYear = now.get(Calendar.YEAR); + int currentMonth = now.get(Calendar.MONTH); // 注意月份是从0开始的 + + // 获取目标日期的年份和月份 + int targetYear = target.get(Calendar.YEAR); + int targetMonth = target.get(Calendar.MONTH); + + // 比较年份和月份 + return currentYear == targetYear && currentMonth == targetMonth; + } + + + private void fillMetaData(PersonnelsMetaDTO dto, FtbPersonnelsStaffRoster roster, List formDataList, + List workerGroupDataDtoList, List roleList, + Map optionMap, EmployeeMetaDataType employeeMetaDataType) { + Map fieldValueMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(formDataList)) { + for (FtbPersonnelsStaffRegistrationFormData formData : formDataList) { + fieldValueMap.put(formData.getFormFieldId(), formData); + } + } + dto.setId(roster.getId()); + dto.setName(roster.getName()); + dto.setUserId(roster.getUserId()); + switch (employeeMetaDataType) { + case BASIC_INFO://员工基本信息 + PersonnelsBaseMetaData baseMetaData = dto.getBaseMetaData(); + baseMetaData.setId(roster.getId()); + baseMetaData.setUserId(roster.getUserId()); + baseMetaData.setName(roster.getName()); + baseMetaData.setFlowerName(getFlowerName(fieldValueMap)); + String workerSex = getWorkerSex(fieldValueMap); + baseMetaData.setWorkerSex(workerSex); + if (HttpRequestUtils.StringUtils.isNotEmpty(workerSex)) { + FtbPersonnelsRegistrationFormFieldOption option = optionMap.get(workerSex); + if (option != null) { + baseMetaData.setWorkerSexName(option.getName()); + } + } + baseMetaData.setIdCardNum(getCommonValue(fieldValueMap, "idCardNum")); + String birthday = getCommonValue(fieldValueMap, "birthday"); + if (StringUtils.isNotEmpty(birthday)) { + baseMetaData.setBirthday(personnelOrgUtils.stringDateToDate(birthday)); + int age = calculateAge(dateToLocalDate(baseMetaData.getBirthday())); + baseMetaData.setAge(String.valueOf(age)); + } else { + String age = getCommonValue(fieldValueMap, "birthday"); + baseMetaData.setAge(age); + } + + String nation = getCommonValue(fieldValueMap, "nation"); + if (StringUtils.isNotEmpty(nation)) { + FtbPersonnelsRegistrationFormFieldOption option = optionMap.get(nation); + if (option != null) { + baseMetaData.setNation(option.getName()); + } + } + String workAge = getCommonValue(fieldValueMap, "workAge"); + if (StringUtils.isNotEmpty(workAge)) { + workAge = removeChineseCharacters(workAge); + if (StringUtils.isNotEmpty(workAge)) { + baseMetaData.setWorkAge(Integer.parseInt(workAge)); + } + } + baseMetaData.setPhone(roster.getPhone()); + baseMetaData.setSystemWokerId(roster.getSystemWokerId()); + baseMetaData.setWorkerNo(roster.getWorkerNo()); + baseMetaData.setActualStartDate(roster.getActualStartDate()); + baseMetaData.setWorkerStatus(roster.getWorkerStatus()); + baseMetaData.setWorkerStatusName(convertWorkerStatusName(roster.getWorkerStatus())); + //重复入职不显示离职时间 + if (StaffWorkerStatus.RESIGNED.getCode().equals(roster.getWorkerStatus())) { + baseMetaData.setDepartDate(roster.getDepartDate()); + } + if (!StaffWorkerStatus.RESIGNED.getCode().equals(roster.getWorkerStatus())) { + baseMetaData.setActualProbationaryDate(Objects.nonNull(roster.getActualProbationaryDate()) + ?roster.getActualProbationaryDate() + :roster.getActualStartDate()); + } + //司龄 + if (roster.getActualStartDate() == null) { + baseMetaData.setCompanyAge(0); + } else { + // 将 Date 转换为 LocalDate + LocalDate startDate = roster.getActualStartDate().toInstant() + .atZone(ZoneId.of("Asia/Shanghai")) + .toLocalDate(); + // 获取当前日期 + LocalDate currentDate = LocalDate.now(); + // 计算完整月数差 + long monthsBetween = ChronoUnit.MONTHS.between(startDate, currentDate); + // 计算入职日期到当前日期之间的天数差 + long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate); + // 获取当前月份的天数 + long daysInMonth = currentDate.lengthOfMonth(); + // 如果有多出的天数(即非整月的部分),算作多一个月 + if (daysBetween % daysInMonth > 0) { + monthsBetween++; + } + baseMetaData.setCompanyAge((int) monthsBetween); + } + break; + case EDUCATION_INFO://学历信息 + PersonnelsEducationMetaData educationMetaData = dto.getEducationMetaData(); + String education = getCommonValue(fieldValueMap, "education"); + if (StringUtils.isNotEmpty(education)) { + FtbPersonnelsRegistrationFormFieldOption option = optionMap.get(education); + if (option != null) { + educationMetaData.setEducation(option.getName()); + } + } + break; + case JOB_INFO://工作信息 + PersonnelsWorkerMetaData workerMetaData = dto.getWorkerMetaData(); + workerMetaData.setWorkerStatus(roster.getWorkerStatus()); + workerMetaData.setWorkerStatusName(convertWorkerStatusName(roster.getWorkerStatus())); + + workerMetaData.setWorkerType(roster.getWorkerType()); + if (StringUtils.isNotEmpty(roster.getWorkerType())) { + FtbPersonnelsRegistrationFormFieldOption option = optionMap.get(roster.getWorkerType()); + if (option != null) { + workerMetaData.setWorkerTypeName(option.getName()); + } + } + workerMetaData.setSystemWokerId(roster.getSystemWokerId()); + workerMetaData.setWorkerNo(roster.getWorkerNo()); + //司龄 + if (roster.getActualStartDate() == null) { + workerMetaData.setCompanyAge(0); + } else { + // 将 Date 转换为 LocalDate + LocalDate startDate = roster.getActualStartDate().toInstant() + .atZone(ZoneId.of("Asia/Shanghai")) + .toLocalDate(); + // 获取当前日期 + LocalDate currentDate = LocalDate.now(); + // 计算完整月数差 + long monthsBetween = ChronoUnit.MONTHS.between(startDate, currentDate); + // 计算入职日期到当前日期之间的天数差 + long daysBetween = ChronoUnit.DAYS.between(startDate, currentDate); + // 获取当前月份的天数 + long daysInMonth = currentDate.lengthOfMonth(); + // 如果有多出的天数(即非整月的部分),算作多一个月 + if (daysBetween % daysInMonth > 0) { + monthsBetween++; + } + workerMetaData.setCompanyAge((int) monthsBetween); + } + workerMetaData.setTrialJobDay(roster.getTrialJobDay()); + String probationPeriod = getCommonValue(fieldValueMap, "probationPeriod"); + ProbationPeriodDto bean = new ProbationPeriodDto(); + + if (org.apache.commons.lang3.StringUtils.isNotEmpty(probationPeriod) && probationPeriod.startsWith("{")) { + bean = JSONUtil.toBean(probationPeriod, ProbationPeriodDto.class); + if (StringUtils.isEmpty(bean.getDays())) { + bean.setDays("0"); + } + workerMetaData.setProbationPeriodDto(bean); + } else { + bean.setType("100"); + bean.setDays("0"); + workerMetaData.setProbationPeriodDto(bean); + } + workerMetaData.setPeriodDay(runPeriodDay(bean)); + break; + case CONTRACT_INFO://合同信息 + PersonnelsContactMetaData contactMetaData = dto.getContactMetaData(); + + String contractType = getCommonValue(fieldValueMap, "contractType"); + if (StringUtils.isNotEmpty(contractType)) { + // 合同类型 转换 + ActionResult info = contractTypeApi.getInfo(contractType); + if (Objects.nonNull(info.getData())) { + Optional.ofNullable(info.getData().getFullName()) + .ifPresentOrElse(contactMetaData::setContractType, + () -> contactMetaData.setContractType("")); + } else { + contactMetaData.setContractType(""); + } + } + + String contractNum = getCommonValue(fieldValueMap, "contractNum"); + if (StringUtils.isNotEmpty(contractNum)) { + contactMetaData.setContractNum(Integer.valueOf(contractNum)); + } + + break; + case PERSONAL_MATERIAL://个人材料 + PersonnelsMaterialMetaData materialMetaData = dto.getMaterialMetaData(); + String educationCertificate = getCommonValue(fieldValueMap, "educationCertificate"); + if (StringUtils.isNotEmpty(educationCertificate)) { + materialMetaData.setEducationCertificate(educationCertificate); + } + + String academicDegreeCertificate = getCommonValue(fieldValueMap, "academicDegreeCertificate"); + if (StringUtils.isNotEmpty(academicDegreeCertificate)) { + materialMetaData.setAcademicDegreeCertificate(academicDegreeCertificate); + } + break; + case ORGANIZATION_INFO://组织信息 + PersonnelsOrgMetaData orgMetaData = dto.getOrgMetaData(); + if (orgMetaData != null) { + if (CollectionUtil.isEmpty(workerGroupDataDtoList)){ + orgMetaData.setList(new ArrayList<>()); + }else { + orgMetaData.setList(workerGroupDataDtoList); + } + orgMetaData.getList().stream().filter(WorkerGroupDataDto::getIsPrimaryPosition).findFirst().ifPresent(data -> { + orgMetaData.setAffiliatedOrg(data.getAffiliatedOrg()); + orgMetaData.setAffiliatedOrgName(data.getAffiliatedOrgName()); + orgMetaData.setAffiliatedPosition(data.getAffiliatedPosition()); + orgMetaData.setAffiliatedPositionName(data.getAffiliatedPositionName()); + orgMetaData.setAffiliatedRank(data.getAffiliatedRank()); + orgMetaData.setAffiliatedRankName(data.getAffiliatedRankName()); + orgMetaData.setParentOrg(data.getParentOrg()); + orgMetaData.setParentOrgName(data.getParentOrgName()); + }); + // 获取入职失败的组织信息 + if (CollectionUtil.isEmpty(orgMetaData.getList())) { + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsStaffEmploymentApply::getUserId, roster.getUserId()); + query.last("limit 1"); + FtbPersonnelsStaffEmploymentApply apply = ftbPersonnelsStaffEmploymentApplyMapper.selectOne(query); + if (apply != null) { + orgMetaData.setAffiliatedOrg(apply.getCurrOrg()); + orgMetaData.setAffiliatedOrgName(apply.getCurrOrgName()); + orgMetaData.setAffiliatedPosition(apply.getCurrPosition()); + orgMetaData.setAffiliatedPositionName(apply.getCurrPositionName()); + orgMetaData.setAffiliatedRank(apply.getCurrRank()); + orgMetaData.setAffiliatedRankName(apply.getCurrRankName()); + } + } + } + break; + case ROLE://角色 + Optional.ofNullable(dto.getRoleMetaData()).ifPresentOrElse(roleListItem->{ + PersonnelsRoleMetaData roleMetaData = dto.getRoleMetaData(); + if (CollectionUtil.isNotEmpty(roleList)){ + roleList.forEach(role -> { + Optional.ofNullable(role.getGlobalMark()).ifPresentOrElse(global -> { + role.setType(global == 0 ? "组织" : "全局"); + }, () -> { + role.setType("无效类型"); + }); + }); + roleMetaData.setList(roleList); + } + },()->{ + dto.setRoleMetaData(new PersonnelsRoleMetaData(){{setList(new ArrayList<>());}}); + }); + + break; + } + } + + private Integer runPeriodDay(ProbationPeriodDto probationPeriodDto) { + if (probationPeriodDto == null) { + return 0; + } + if ("100".equals(probationPeriodDto.getType())) { + return 0; + } + int month = 0; + if (probationPeriodDto.getType().equals("101")) { + if (StringUtils.isNotEmpty(probationPeriodDto.getDays())) { + return Integer.valueOf(probationPeriodDto.getDays()); + } else { + return 0; + } + } else if (probationPeriodDto.getType().equals("102")) { + month = 1; + } else if (probationPeriodDto.getType().equals("103")) { + month = 2; + } else if (probationPeriodDto.getType().equals("104")) { + month = 3; + } else if (probationPeriodDto.getType().equals("105")) { + month = 4; + } else if (probationPeriodDto.getType().equals("106")) { + month = 5; + } else if (probationPeriodDto.getType().equals("107")) { + month = 6; + } else if (probationPeriodDto.getType().equals("108")) { + month = 7; + } else if (probationPeriodDto.getType().equals("109")) { + month = 8; + } + return 30 * month; + } + + private String getWorkerSex(Map fieldValueMap) { + FtbPersonnelsStaffRegistrationFormData formData = fieldValueMap.get("workerSex"); + if (formData != null) { + return formData.getValue(); + } + return ""; + } + + private String getFlowerName(Map fieldValueMap) { + FtbPersonnelsStaffRegistrationFormData formData = fieldValueMap.get("flowerName"); + if (formData != null) { + return formData.getValue(); + } + return ""; + } + + private String getCommonValue(Map fieldValueMap, String fieldId) { + FtbPersonnelsStaffRegistrationFormData formData = fieldValueMap.get(fieldId); + if (formData != null) { + return formData.getValue(); + } + return ""; + } + + public Map> queryRoleListForUserIdsNoToken(List userIds, String tenantId) { + if (CollectionUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + ActionResult> listActionResult = roleApi.listUsersBoundNotoken(userIds, tenantId); + if (listActionResult == null || CollectionUtil.isEmpty(listActionResult.getData())) { + return new HashMap<>(); + } + + Map> ret = new HashMap<>(); + List list = listActionResult.getData(); + for (UserBoundRolesVO userBoundRolesVO : list) { + ret.put(userBoundRolesVO.getId(), userBoundRolesVO.getRoleBounds()); + } + return ret; + } + + + public Map queryAllOption() { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0).orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getId); + List optionList = fieldOptionService.list(wrapper); + if (CollectionUtil.isEmpty(optionList)) { + return new HashMap<>(); + } + Map optionMap = optionList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, Function.identity())); + return optionMap; + } + + + /** + * 计算年龄 + * + * @param birthDate + * @return + */ + public static int calculateAge(LocalDate birthDate) { + LocalDate today = LocalDate.now(); + Period period = Period.between(birthDate, today); + return period.getYears(); + } + + /** + * 将 java.util.Date 转换为 java.time.LocalDate。 + * + * @param date 日期对象 + * @return LocalDate 对象 + */ + private static LocalDate dateToLocalDate(Date date) { + return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + + + /** + * 去掉字符串中的汉字。 + * + * @param input 输入字符串 + * @return 去掉汉字后的字符串 + */ + public static String removeChineseCharacters(String input) { + // 正则表达式匹配汉字 + String regex = "[\\u4e00-\\u9fa5]"; + // 替换掉所有匹配到的汉字 + return input.replaceAll(regex, ""); + } + + + public String convertWorkerStatusName(String workerStatus) { + if (StringUtils.isEmpty(workerStatus)) { + return ""; + } + if ("301".equals(workerStatus)) { + return "预入职"; + } else if ("302".equals(workerStatus)) { + return "试用"; + } else if ("303".equals(workerStatus)) { + return "正式"; + } else if ("304".equals(workerStatus)) { + return "待离职"; + } else if ("305".equals(workerStatus)) { + return "离职"; + } else if ("306".equals(workerStatus)) { + return "试岗"; + } + return ""; + } + + public String convertContactStatusName(String contractStatus) { + if (StringUtils.isEmpty(contractStatus)) { + return ""; + } + + // 合同签署状态,500-未发起 501-待签署 502-已到期 503-已签署 + if ("500".equals(contractStatus)) { + return "未发起"; + } else if ("501".equals(contractStatus)) { + return "待签署"; + } else if ("502".equals(contractStatus)) { + return "已到期"; + } else if ("503".equals(contractStatus)) { + return "已签署"; + } + return ""; + } + + + /** + * 查询在A和B天数范围内入职的用户ID + * + * @param startDays 开始天数 + * @param endDays 结束天数 + * @return 用户id集合 + */ + @Override + public List queryUserIdsForCompanyAge(Integer startDays, Integer endDays) { + List retList = new ArrayList<>(); + //查询用户信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getActualStartDate) + .in(FtbPersonnelsStaffRoster::getWorkerStatus, "302", "303", "306") + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = staffRosterService.list(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return retList; + } + Set retSet = new HashSet<>(); + Date now = DateUtil.date(); + for (FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster : rosterList) { + if (ftbPersonnelsStaffRoster.getActualStartDate() == null) { + continue; + } + //计数入职天数 + Integer days = Integer.parseInt(String.valueOf(DateUtil.between(ftbPersonnelsStaffRoster.getActualStartDate(), now, DateUnit.DAY))); + if (days >= startDays && days <= endDays) { + retSet.add(ftbPersonnelsStaffRoster.getUserId()); + } + } + return new ArrayList<>(retSet); + } + + /** + * 查询指定用户的司龄 + * + * @param userIds 用户ID + * @return + */ + @Override + public Map queryCompanyAge(List userIds) { + Map retMap = new HashMap<>(); + //查询用户信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getActualStartDate) + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = staffRosterService.list(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return retMap; + } + + Date now = DateUtil.date(); + for (FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster : rosterList) { + if (ftbPersonnelsStaffRoster.getActualStartDate() == null) { + continue; + } + //计数入职天数 + Integer days = Integer.parseInt(String.valueOf(DateUtil.between(ftbPersonnelsStaffRoster.getActualStartDate(), now, DateUnit.DAY))); + retMap.put(ftbPersonnelsStaffRoster.getUserId(), days); + } + + return retMap; + } + + @Override + public Integer queryCompanyAgeByRosterId(String rosterId) { + + List allList = registrationFormDataService.queryListForRosterIdAndField(List.of(rosterId), List.of("companyAge")); + if (CollUtil.isEmpty(allList)) { + return null; + } + for (FtbPersonnelsStaffRegistrationFormData formData : allList) { + String companyAge = formData.getValue(); + if (StringUtils.isEmpty(companyAge)) { + continue; + } + return Integer.parseInt(companyAge); + } + return null; + } + + /** + * 查询所有用户的年龄 + * + * @return + */ + @Override + public Map queryAllUserCompanyAge() { + Map retMap = new HashMap<>(); + //查询用户信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone) + .in(FtbPersonnelsStaffRoster::getWorkerStatus, "302", "303", "306") + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = staffRosterService.list(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return retMap; + } + Map rosterMap = new HashMap<>();//id->FtbPersonnelsStaffRoster + + List rosterIds = new ArrayList<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { + rosterIds.add(roster.getId()); + rosterMap.put(roster.getId(), roster); + } + List allList = registrationFormDataService.queryListForRosterIdAndField(rosterIds, List.of("companyAge")); + if (CollUtil.isEmpty(allList)) { + return retMap; + } + + for (FtbPersonnelsStaffRegistrationFormData formData : allList) { + String companyAge = formData.getValue(); + if (StringUtils.isEmpty(companyAge)) { + continue; + } + Integer companyAgeInt = Integer.parseInt(companyAge); + + FtbPersonnelsStaffRoster roster = rosterMap.get(formData.getRosterId()); + if (roster != null) { + retMap.put(roster.getUserId(), companyAgeInt); + } + + } + return retMap; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsOverviewAnalysisServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsOverviewAnalysisServiceImpl.java new file mode 100644 index 0000000..4c0e48d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsOverviewAnalysisServiceImpl.java @@ -0,0 +1,1660 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.dto.range.RangeDTO; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.personnels.mapper.*; +import jnpf.personnels.service.FtbPersonnelsOverviewAnalysisService; +import jnpf.personnels.service.FtbPersonnlesInfoConfigService; +import jnpf.personnels.utils.CompanyAgeUtil; +import jnpf.util.RangeConfigUtil; +import jnpf.util.excel.EasyExcelUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.ServletWebRequest; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsOverviewAnalysisServiceImpl implements FtbPersonnelsOverviewAnalysisService { + + @Resource + private FtbPersonnelsOverviewAnalysisMapper ftbPersonnelsOverviewAnalysisMapper; + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Resource + private FtbPersonnelsRegistrationFormFieldMapper ftbPersonnelsRegistrationFormFieldMapper; + @Resource + private FtbPersonnelsRegistrationFormFieldOptionMapper ftbPersonnelsRegistrationFormFieldOptionMapper; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + private V2OrganizeApi v2OrganizeApi; + + @Resource + private FtbPersonnelsTurnoverManagementMapper turnoverManagementMapper; + + // 在职状态 + public static final List ON_THE_JOB_STATUS = List.of("302", "303", "304", "306"); + + + @Resource + private FtbPersonnlesInfoConfigService personnlesInfoConfigService; + + @Override + public PersonnelsOverviewEmployeesVO currentEmployees(FtbPersonnlesAnalysisDTO dto) { + PersonnelsOverviewEmployeesVO personnelsOverviewEmployeesVO = new PersonnelsOverviewEmployeesVO(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return personnelsOverviewEmployeesVO; + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + personnelsOverviewEmployeesVO.setData(employedInTheMonth); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearDate = DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(); + BigDecimal yearOnYearDateValue = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDate), + ON_THE_JOB_STATUS, userIdListByOrganizeIds); + personnelsOverviewEmployeesVO.setYearOnYearValue(FtbPersonnelsOverviewAnalysisService.computeDivision(employedInTheMonth.subtract(yearOnYearDateValue), yearOnYearDateValue)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chainDate = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + BigDecimal chainDateValue = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(chainDate) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + personnelsOverviewEmployeesVO.setChainValue(FtbPersonnelsOverviewAnalysisService.computeDivision(employedInTheMonth.subtract(chainDateValue), chainDateValue)); + return personnelsOverviewEmployeesVO; + } + + @Override + public PersonnelsOverviewEmployeesVO compositeLife(FtbPersonnlesAnalysisDTO dto) { + PersonnelsOverviewEmployeesVO personnelsOverviewEmployeesVO = new PersonnelsOverviewEmployeesVO(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return personnelsOverviewEmployeesVO; + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 员工年龄 + List ages = ftbPersonnelsOverviewAnalysisMapper.employeeAge(userIdListByOrganizeIds, DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS); + BigDecimal numberOfCurrentPeriod = totalAgeCalculation(ages); + personnelsOverviewEmployeesVO.setData(FtbPersonnelsOverviewAnalysisService.computeDivisionSb(numberOfCurrentPeriod, employedInTheMonth, 2) + .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP)); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearDate = DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(); + List yearOnYearAges = ftbPersonnelsOverviewAnalysisMapper.employeeAge(userIdListByOrganizeIds, DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDate) + , ON_THE_JOB_STATUS); + BigDecimal yearOnYearBigDecimal = totalAgeCalculation(yearOnYearAges); + // 同比在职员工人数 + BigDecimal yearOnYearEmployedEmployees = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDate) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 同比 + BigDecimal yearOnYearAverageAge = FtbPersonnelsOverviewAnalysisService.percentCustomCalculation(yearOnYearBigDecimal, yearOnYearEmployedEmployees, 2); + personnelsOverviewEmployeesVO.setYearOnYearValue(FtbPersonnelsOverviewAnalysisService.computeDivision( + personnelsOverviewEmployeesVO.getData().subtract(yearOnYearAverageAge) + , yearOnYearAverageAge)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chainDate = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + List chainDateValueAge = ftbPersonnelsOverviewAnalysisMapper.employeeAge(userIdListByOrganizeIds, DatePattern.NORM_MONTH_FORMAT.format(chainDate) + , ON_THE_JOB_STATUS); + BigDecimal chainBigDecimal = totalAgeCalculation(chainDateValueAge); + // 环比在职员工人数 + BigDecimal monthOnMonthEmployment = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(chainDate) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 环比数 + BigDecimal chainNumber = FtbPersonnelsOverviewAnalysisService.percentCustomCalculation(chainBigDecimal, monthOnMonthEmployment, 2); + + personnelsOverviewEmployeesVO.setChainValue(FtbPersonnelsOverviewAnalysisService.computeDivision( + personnelsOverviewEmployeesVO.getData().subtract(chainNumber) + , chainNumber)); + return personnelsOverviewEmployeesVO; + } + + @Override + public PersonnelsOverviewEmployeesAgeVO averageAge(FtbPersonnlesAnalysisDTO dto) { + PersonnelsOverviewEmployeesAgeVO personnelsOverviewEmployeesVO = new PersonnelsOverviewEmployeesAgeVO(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return personnelsOverviewEmployeesVO; + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 截止月份的总司龄天数 + BigDecimal numberOfDaysOfAge = ftbPersonnelsOverviewAnalysisMapper.numberOfDaysOfAge(userIdListByOrganizeIds + , DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS); + // 平均司龄 + personnelsOverviewEmployeesVO.setData(averageAgeCalculation(numberOfDaysOfAge, employedInTheMonth)); + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date yearOnYearDate = DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(); + BigDecimal yearOnYearDateValue = ftbPersonnelsOverviewAnalysisMapper.numberOfDaysOfAge(userIdListByOrganizeIds + , DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDate), ON_THE_JOB_STATUS); + // 同比截止月份员工 + BigDecimal a = FtbPersonnelsOverviewAnalysisService.percentCustomCalculation(numberOfDaysOfAge, employedInTheMonth, 2); + BigDecimal yearOnYearEmployeesAsOfMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(yearOnYearDate) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + BigDecimal b = FtbPersonnelsOverviewAnalysisService.percentCustomCalculation(yearOnYearDateValue, yearOnYearEmployeesAsOfMonth, 2); + personnelsOverviewEmployeesVO.setYearOnYearValue(FtbPersonnelsOverviewAnalysisService.computeDivision(a.subtract(b), b)); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Date chainDate = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + BigDecimal chainDateValue = ftbPersonnelsOverviewAnalysisMapper.numberOfDaysOfAge(userIdListByOrganizeIds + , DatePattern.NORM_MONTH_FORMAT.format(chainDate), ON_THE_JOB_STATUS); + // 环比截止月份员工 + BigDecimal employeesAsOfMonthOnMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(chainDate) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + BigDecimal c = FtbPersonnelsOverviewAnalysisService.percentCustomCalculation(chainDateValue, employeesAsOfMonthOnMonth, 2); + personnelsOverviewEmployeesVO.setChainValue(FtbPersonnelsOverviewAnalysisService.computeDivision(a.subtract(c), c)); + return personnelsOverviewEmployeesVO; + } + + @Override + public List genderDistribution(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 男 + BigDecimal male = ftbPersonnelsOverviewAnalysisMapper.genderDistribution(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds, "1"); + PersonnelsOverviewProportionVO malePersonnelsOverviewProportionVO = new PersonnelsOverviewProportionVO(); + malePersonnelsOverviewProportionVO.setDataName("男"); + malePersonnelsOverviewProportionVO.setData(male.intValue()); + malePersonnelsOverviewProportionVO.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(male, employedInTheMonth)); + result.add(malePersonnelsOverviewProportionVO); + // 女 + BigDecimal female = ftbPersonnelsOverviewAnalysisMapper.genderDistribution(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds, "2"); + PersonnelsOverviewProportionVO femalePersonnelsOverviewProportionVO = new PersonnelsOverviewProportionVO(); + femalePersonnelsOverviewProportionVO.setDataName("女"); + femalePersonnelsOverviewProportionVO.setData(female.intValue()); + femalePersonnelsOverviewProportionVO.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(female, employedInTheMonth)); + result.add(femalePersonnelsOverviewProportionVO); + return result; + } + + @Override + public List employeeStatusProportion(FtbPersonnlesAnalysisDTO dto) { + List resignedEmployeesIds = doEmployeesOfResignOrganization(dto.getOrganizationId()); + // 截止月份入职日期的离职员工数 + BigDecimal resignedEmployees = BigDecimal.ZERO; + if (CollUtil.isNotEmpty(resignedEmployeesIds)) { + resignedEmployees = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , List.of("305"), resignedEmployeesIds); + } + List result = List.of( + new PersonnelsOverviewProportionVO("302", 0, BigDecimal.ZERO, "试用期员工"), + new PersonnelsOverviewProportionVO("303", 0, BigDecimal.ZERO, "正式员工"), + new PersonnelsOverviewProportionVO("304", 0, BigDecimal.ZERO, "待离职员工"), + new PersonnelsOverviewProportionVO("305", resignedEmployees.intValue(), BigDecimal.ZERO, "离职员工")); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return handleEmployeeStatusProportion(result, List.of(), resignedEmployees); + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + List temp = ftbPersonnelsOverviewAnalysisMapper.employeeStatusProportion( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), userIdListByOrganizeIds); + temp.add(new PersonnelsOverviewProportionVO("305", resignedEmployees.intValue(), BigDecimal.ZERO, "离职员工")); + // 数值换算及百分比 + return handleEmployeeStatusProportion(result, temp, employedInTheMonth.add(resignedEmployees)); + } + + @Override + public List proportionOfEmploymentTypes(FtbPersonnlesAnalysisDTO dto) { + /** + * 员工类型 + */ + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, "workerType"); + query.orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getCreatorTime); + List result = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList(query) + .stream() + .map(a -> new PersonnelsOverviewProportionVO(a.getId(), 0, BigDecimal.ZERO, a.getName())) + .collect(Collectors.toList()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return handleEmployeeStatusProportion(result, List.of(), BigDecimal.ZERO); + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + List temp = ftbPersonnelsOverviewAnalysisMapper.proportionOfEmploymentTypes( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 数值换算及百分比 + return handleEmployeeStatusProportion(result, temp, employedInTheMonth); + } + + @Override + public List ageDistributionProportion(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return getAgeDistributions(List.of(AgeDistributionSecond.values()), List.of(), BigDecimal.ZERO); + } + Integer configType = personnlesInfoConfigService.queryType(1); + if (configType == null) { + throw new RuntimeException("获取年龄配置失败"); + } + // 1平均 2自定义 + List rangeDTOS; + if (configType == 1){ + FtbRangeConfigVO configVO = personnlesInfoConfigService.queryInfo(1); + rangeDTOS = RangeConfigUtil.generateRanges(configVO.getFirstGear(), configVO.getIntervalValue(), configVO.getNumberOfGears(),configVO.getPreviewValue()); + }else { + List rangeConfigDIYVOS = personnlesInfoConfigService.queryDiyInfo(1); + rangeDTOS = rangeConfigDIYVOS.stream().map(v->{ + RangeDTO rangeDTO = new RangeDTO(); + rangeDTO.setStartPrice(v.getStartInterval()); + rangeDTO.setEndPrice(v.getEndInterval()); + rangeDTO.setPriceBandDisplayStr(v.getPreviewValue()); + return rangeDTO; + }).collect(Collectors.toList()); + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + List temp = ftbPersonnelsOverviewAnalysisMapper.ageDistributionProportion( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds) + .stream() + .filter(a -> Objects.nonNull(a) && StrUtil.isNotBlank(a.getDataName())) + .peek(a -> a.setData(DateUtil.ageOfNow(a.getDataName()))) + .collect(Collectors.toList()); + return rangeDTOS.stream().map(a -> { + PersonnelsOverviewProportionVO proportionVO = new PersonnelsOverviewProportionVO(); + proportionVO.setDataName(a.getPriceBandDisplayStr()); + Integer startPrice = a.getStartPrice(); + Integer endPrice = a.getEndPrice(); + long count = temp.stream().filter(v -> { + if (StringUtils.isEmpty(v.getDataName())) return false; + BigDecimal decimal = BigDecimal.valueOf(v.getData()); + BigDecimal startPriceDecimal = BigDecimal.valueOf(startPrice); + if (endPrice == null) { + if (decimal.compareTo(startPriceDecimal) >= 0) { + return true; + } + }else { + BigDecimal endPriceDecimal = BigDecimal.valueOf(endPrice); + if ( decimal.compareTo(startPriceDecimal) >= 0 && decimal.compareTo(endPriceDecimal) < 0) { + // 条件成立时的逻辑 + return true; + } + } + // 判断是否在范围内 + return false; + }).count(); + proportionVO.setData(Math.toIntExact(count)); + proportionVO.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(count, employedInTheMonth)); + return proportionVO; + }).collect(Collectors.toList()); + } + + @Override + public List ageDistribution(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return getAgeDistributions(List.of(AgeDistributionFirst.values()), List.of(), BigDecimal.ZERO); + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + List temp = ftbPersonnelsOverviewAnalysisMapper.ageDistribution( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + return getAgeDistributions(List.of(AgeDistributionFirst.values()), temp, employedInTheMonth); + } + + @Override + public List educationalBackground(FtbPersonnlesAnalysisDTO dto) { + /** + * 学历占比 + * 18 初中 + * 19 高中 + * 20 中专 + * 21 大专 + * 22 本科 + * 23 硕士 + * 24 博士 + * 25 其他 + * 33 小学 + */ + List result = List.of( + new PersonnelsOverviewProportionVO("33", 0, BigDecimal.ZERO, "小学"), + new PersonnelsOverviewProportionVO("18", 0, BigDecimal.ZERO, "初中"), + new PersonnelsOverviewProportionVO("19", 0, BigDecimal.ZERO, "高中"), + new PersonnelsOverviewProportionVO("20", 0, BigDecimal.ZERO, "中专"), + new PersonnelsOverviewProportionVO("21", 0, BigDecimal.ZERO, "大专"), + new PersonnelsOverviewProportionVO("22", 0, BigDecimal.ZERO, "本科"), + new PersonnelsOverviewProportionVO("23", 0, BigDecimal.ZERO, "硕士"), + new PersonnelsOverviewProportionVO("24", 0, BigDecimal.ZERO, "博士"), + new PersonnelsOverviewProportionVO("25", 0, BigDecimal.ZERO, "其他") + ); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return handleEmployeeStatusProportion(result, List.of(), BigDecimal.ZERO); + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + List temp = ftbPersonnelsOverviewAnalysisMapper.educationalBackground( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 数值换算及百分比 + return handleEmployeeStatusProportion(result, temp, employedInTheMonth); + } + + @Override + public List departmentHeadcountRatio(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(dto.getOrganizationId(), false, null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return result; + } + List orgIds = listActionResult.getData() + .stream().filter(a -> OrganizeCategoryEnums.DEPARTMENT.equals(a.getOrganizeCategoryEnums())) + .map(OrganizeGeneralDetailVO::getId) + .filter(id -> dto.getOrganizationId().contains(id)).collect(Collectors.toList()); + if (CollUtil.isEmpty(orgIds)) { + return result; + } + ActionResult> userPageList = v2UserApi.listTargetOrganizes(orgIds, null,null); + if (userPageList == null || CollUtil.isEmpty(userPageList.getData())) { + return result; + } + List userIdListByOrganizeIds = userPageList.getData().stream().map(UserPageListVO::getId).collect(Collectors.toList()); + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 部门人数分组占比 + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(presenceOfPersonnel, null); + if (actionResult.getCode() != 200) { + throw new RuntimeException("获取用户信息失败"); + } + Map departmentProportionMap = actionResult.getData().stream() + .filter(a -> { + if (CollUtil.isNotEmpty(orgIds)) { + return orgIds.contains(a.getOrganizeId()); + } + return true; + }) + .collect(Collectors.groupingBy(a -> a.getOrganizeId() + "#" + a.getOrganizeName() + , Collectors.collectingAndThen( + Collectors.mapping(UserBoundVO::getId, Collectors.toSet()) + , a -> Long.valueOf(a.size())))); + + return getPersonnelsOverviewProportionVOS(presenceOfPersonnel, departmentProportionMap); + } + + @Override + public List proportionOfJobHeadcount(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 岗位人数分组占比 + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(presenceOfPersonnel, ""); + if (actionResult.getCode() != 200) { + throw new RuntimeException("获取用户信息失败"); + } + Map departmentProportionMap = actionResult.getData().stream() + .filter(a -> { + if (CollUtil.isNotEmpty(dto.getPostId())) { + return dto.getPostId().contains(a.getPositionId()) && dto.getOrganizationId().contains(a.getOrganizeId()); + } + return dto.getOrganizationId().contains(a.getOrganizeId()); + }) + .collect(Collectors.groupingBy(a -> a.getPositionId() + "#" + a.getPositionName(), Collectors.counting())); + return getPersonnelsOverviewProportionVOS(presenceOfPersonnel, departmentProportionMap); + } + + @NotNull + private List getPersonnelsOverviewProportionVOS(List presenceOfPersonnel, Map departmentProportionMap) { + return departmentProportionMap.entrySet().stream() + .map(a -> { + PersonnelsOverviewProportionVO personnelsOverviewProportionVO = new PersonnelsOverviewProportionVO(); + personnelsOverviewProportionVO.setDataName(a.getKey().split("#")[1]); + personnelsOverviewProportionVO.setData(a.getValue().intValue()); + personnelsOverviewProportionVO.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(a.getValue(), presenceOfPersonnel.size())); + return personnelsOverviewProportionVO; + }).collect(Collectors.toList()); + } + + @Override + public List statusOfCurrentEmployees(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + long betweenMonth = DateUtil.betweenMonth(dto.getStartTime(), dto.getEndTime(), true); + for (int i = 0; i <= betweenMonth; i++) { + PersonnelsOverviewProportionVO personnelsOverviewProportionVO = new PersonnelsOverviewProportionVO(); + Date offsetTime = DateUtil.offset(dto.getStartTime(), DateField.MONTH, i).toJdkDate(); + String month = DatePattern.NORM_MONTH_FORMAT.format(offsetTime); + personnelsOverviewProportionVO.setDataName(month); + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(month, ON_THE_JOB_STATUS, + userIdListByOrganizeIds); + personnelsOverviewProportionVO.setData(employedInTheMonth.intValue()); + result.add(personnelsOverviewProportionVO); + } + return result; + } + + @Override + public List seniorityDistribution(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + Integer configType = personnlesInfoConfigService.queryType(3); + if (configType == null) { + throw new RuntimeException("获取工龄配置失败"); + } + // 1平均 2自定义 + List rangeDTOS; + if (configType == 1){ + FtbRangeConfigVO configVO = personnlesInfoConfigService.queryInfo(3); + rangeDTOS = RangeConfigUtil.generateRanges(configVO.getFirstGear(), configVO.getIntervalValue(), configVO.getNumberOfGears(), configVO.getPreviewValue()); + }else { + List rangeConfigDIYVOS = personnlesInfoConfigService.queryDiyInfo(3); + rangeDTOS = rangeConfigDIYVOS.stream().map(v -> { + RangeDTO rangeDTO = new RangeDTO(); + rangeDTO.setStartPrice(v.getStartInterval()); + rangeDTO.setEndPrice(v.getEndInterval()); + rangeDTO.setPriceBandDisplayStr(v.getPreviewValue()); + return rangeDTO; + }).collect(Collectors.toList()); + } + List seniorityDistributionList = new ArrayList<>(); + if (CollUtil.isNotEmpty(presenceOfPersonnel)) { + seniorityDistributionList = ftbPersonnelsOverviewAnalysisMapper.seniorityDistribution(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()),presenceOfPersonnel,ON_THE_JOB_STATUS); + } + List finalSeniorityDistributionList = seniorityDistributionList; + return rangeDTOS.stream().map(item -> { + PersonnelsOverviewProportionVO proportionVO = new PersonnelsOverviewProportionVO(); + proportionVO.setDataName(item.getPriceBandDisplayStr()); + Integer startPrice = item.getStartPrice(); + Integer endPrice = item.getEndPrice(); + long count = finalSeniorityDistributionList.stream().filter(a -> { + if( endPrice == null && a.compareTo(new BigDecimal(startPrice)) >= 0 ) return true; + // 判断是否在区间内 + if (a.compareTo(new BigDecimal(startPrice)) >= 0 && a.compareTo(new BigDecimal(endPrice)) <= 0) { + return true; + } else { + return false; + } + }).count(); + proportionVO.setData(Math.toIntExact(count)); + proportionVO.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(count, presenceOfPersonnel.size())); + return proportionVO; + }).collect(Collectors.toList()); + } + + @Override + public List insurancePurchaseAnalysis(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + // 保险购买情况 + LambdaQueryWrapper formFieldWrapper = Wrappers.lambdaQuery(); + formFieldWrapper.eq(FtbPersonnelsRegistrationFormField::getName, "保险购买情况"); + formFieldWrapper.eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0); + formFieldWrapper.last("limit 1"); + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectOne(formFieldWrapper); + if (Objects.isNull(ftbPersonnelsRegistrationFormFields)) { + return result; + } + // 获取保险购买情况下拉选项值 + List options = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList( + Wrappers.lambdaQuery() + .eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, ftbPersonnelsRegistrationFormFields.getId()) + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0)); + result = options.stream() + .map(a -> new PersonnelsOverviewProportionVO(a.getId(), 0, BigDecimal.ZERO, a.getName())) + .collect(Collectors.toList()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return handleEmployeeStatusProportion(result, List.of(), BigDecimal.ZERO); + } + // 截止月份在职员工 + BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + List temp = ftbPersonnelsOverviewAnalysisMapper.insurancePurchaseAnalysis( + ftbPersonnelsRegistrationFormFields.getId(), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 数值换算及百分比 + return handleEmployeeStatusProportion(result, temp, employedInTheMonth); + } + + @Override + public List proportionOfStoreNumbers(FtbPersonnlesAnalysisDTO dto) { + List result = new ArrayList<>(); + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(dto.getOrganizationId(), false, null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return result; + } + List orgIds = listActionResult.getData() + .stream().filter(a -> OrganizeCategoryEnums.STORE.equals(a.getOrganizeCategoryEnums())) + .map(OrganizeGeneralDetailVO::getId) + .filter(id -> dto.getOrganizationId().contains(id)).collect(Collectors.toList()); + if (CollUtil.isEmpty(orgIds)) { + return result; + } + ActionResult> userPageList = v2UserApi.listTargetOrganizes(orgIds, null,null); + if (userPageList == null || CollUtil.isEmpty(userPageList.getData())) { + return result; + } + List userIdListByOrganizeIds = userPageList.getData().stream().map(UserPageListVO::getId).collect(Collectors.toList()); + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + // 部门人数分组占比 + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(presenceOfPersonnel, null); + if (actionResult.getCode() != 200) { + throw new RuntimeException("获取用户信息失败"); + } + Map departmentProportionMap = actionResult.getData().stream() + .filter(a -> { + if (CollUtil.isNotEmpty(orgIds)) { + return orgIds.contains(a.getOrganizeId()); + } + return true; + }) + .collect(Collectors.groupingBy(a -> a.getOrganizeId() + "#" + a.getOrganizeName() + , Collectors.collectingAndThen( + Collectors.mapping(UserBoundVO::getId, Collectors.toSet()) + , a -> Long.valueOf(a.size())))); + + return getPersonnelsOverviewProportionVOS(presenceOfPersonnel, departmentProportionMap); + } + + @Override + public PageListVO statusOfCurrentEmployeesDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonthList(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(presenceOfPersonnel)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 获取员工类型选项 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, "workerType"); + List options = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList(query); + Map workerTypeMap = options.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, FtbPersonnelsRegistrationFormFieldOption::getName)); + presenceOfPersonnel.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + if (StrUtil.isNotBlank(a.getEmployeeType())) { + a.setEmployeeType(workerTypeMap.getOrDefault(a.getEmployeeType(),a.getEmployeeType())); + } + }); + result.getPagination().setTotal(presenceOfPersonnel.size()); + // 按照组织排序 + presenceOfPersonnel.sort(Comparator.comparing(PersonnelsOverviewEmployeesDetailsVO::getOrganization)); + result.setList(presenceOfPersonnel); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), presenceOfPersonnel.size()); + return CultivatePage.paginate(presenceOfPersonnel, page); + } + return result; + } + + @Override + public void statusOfCurrentEmployeesDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewEmployeesDetailsVOPageListVO = statusOfCurrentEmployeesDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-在职员工", personnelsOverviewEmployeesDetailsVOPageListVO.getList(), PersonnelsOverviewEmployeesDetailsVO.class); + } + + @Override + public PageListVO genderDistributionDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List males = ftbPersonnelsOverviewAnalysisMapper.genderDistributionList(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(males)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + males.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 0未知1男2女 + if ("0".equals(a.getGender())) { + a.setGender("未知"); + } else if ("1".equals(a.getGender())) { + a.setGender("男"); + } else if ("2".equals(a.getGender())) { + a.setGender("女"); + } + }); + result.getPagination().setTotal(males.size()); + // 按照组织排序 + males.sort(Comparator.comparing(PersonnelsOverviewProportionDetailsVO::getOrganization)); + result.setList(males); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(),males.size()); + return CultivatePage.paginate(males, page); + } + return result; + } + + @Override + public void genderDistributionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewProportionDetailsVOPageListVO = genderDistributionDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-性别分布", personnelsOverviewProportionDetailsVOPageListVO.getList(), PersonnelsOverviewProportionDetailsVO.class); + } + + @Override + public PageListVO employeeStatusProportionDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + // 离职员工 + List employeesOfResignOrganizationList = doEmployeesOfResignOrganizationList(dto.getOrganizationId()); + if (CollUtil.isNotEmpty(employeesOfResignOrganizationList)) { + List resignedEmployeesIds = employeesOfResignOrganizationList.stream().map(PersonnelsOverviewSituationDetailsVO::getUserId).collect(Collectors.toList()); + List personnelsOverviewEmployeesDetailsVOS = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonthList(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , List.of("305"), resignedEmployeesIds); + resignedEmployeesIds = personnelsOverviewEmployeesDetailsVOS.stream().map(PersonnelsOverviewEmployeesDetailsVO::getUserId).collect(Collectors.toList()); + List finalResignedEmployeesIds = resignedEmployeesIds; + employeesOfResignOrganizationList = employeesOfResignOrganizationList.stream() + .filter(a -> finalResignedEmployeesIds.contains(a.getUserId())) + .collect(Collectors.toList()); + } + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds) && CollUtil.isEmpty(employeesOfResignOrganizationList)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.employeeStatusProportionList(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , userIdListByOrganizeIds); + if (CollUtil.isNotEmpty(queryLists)) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 工作状态,301、预入职 302、试用 303、正式 304、待离职 305 离职 306试岗 + switch (a.getEmployeeStatus()) { + case "301": + a.setEmployeeStatus("预入职"); + break; + case "302": + a.setEmployeeStatus("试用"); + break; + case "303": + a.setEmployeeStatus("正式"); + break; + case "304": + a.setEmployeeStatus("待离职"); + break; + case "305": + a.setEmployeeStatus("离职"); + break; + case "306": + a.setEmployeeStatus("试岗"); + break; + default: + a.setEmployeeStatus("未知"); + } + }); + } + // 离职员工 + if (CollUtil.isNotEmpty(employeesOfResignOrganizationList)) { + // 员工Id填充 + List userIds = employeesOfResignOrganizationList.stream().map(PersonnelsOverviewSituationDetailsVO::getUserId).collect(Collectors.toList()); + Map employeeIDCollection = employeeIDQuery(userIds); + employeesOfResignOrganizationList.forEach(a -> { + a.setEmployeeId(employeeIDCollection.getOrDefault(a.getUserId(),"")); + // 已离职 + a.setEmployeeStatus("已离职"); + }); + queryLists.addAll(employeesOfResignOrganizationList); + } + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewSituationDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(),queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void employeeStatusProportionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewSituationDetailsVOPageListVO = employeeStatusProportionDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-员工状态分布", personnelsOverviewSituationDetailsVOPageListVO.getList(), PersonnelsOverviewSituationDetailsVO.class); + } + + @Override + public PageListVO proportionOfEmploymentTypesDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.proportionOfEmploymentTypesList(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(queryLists)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 获取员工类型选项 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + query.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, "workerType"); + List options = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList(query); + Map workerTypeMap = options.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, FtbPersonnelsRegistrationFormFieldOption::getName)); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 员工类型转换 + if (StrUtil.isNotBlank(a.getEmployeeType())) { + a.setEmployeeType(workerTypeMap.getOrDefault(a.getEmployeeType(), a.getEmployeeType())); + } + }); + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewTypeDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void proportionOfEmploymentTypesDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewTypeDetailsVOPageListVO = proportionOfEmploymentTypesDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-员工类型分布", personnelsOverviewTypeDetailsVOPageListVO.getList(), PersonnelsOverviewTypeDetailsVO.class); + } + + @Override + public PageListVO ageDistributionProportionDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.ageDistributionProportionList( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(queryLists)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 年龄计算和格式转换 + if (StrUtil.isNotBlank(a.getAge())) { + try { + int age = DateUtil.ageOfNow(a.getAge()); + a.setAge(String.valueOf(age)); + } catch (Exception e) { + // 保持原格式 + } + } + }); + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewAgeDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void ageDistributionProportionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewAgeDetailsVOPageListVO = ageDistributionProportionDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-年龄分布", personnelsOverviewAgeDetailsVOPageListVO.getList(), PersonnelsOverviewAgeDetailsVO.class); + } + + @Override + public PageListVO companyDistributionDetails(FtbPersonnlesAnalysisDTO dto) { + Date now = new Date(); + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.companyDistributionList( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(queryLists)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 司龄格式转换 + if (StrUtil.isNotBlank(a.getCompanyAge())) { + a.setCompanyAge(CompanyAgeUtil.calculateServiceAgeShort(DateUtil.parse(a.getCompanyAge()), now)); + } + }); + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewCompanyDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void companyDistributionDetailsExport(FtbPersonnlesAnalysisDTO dto) { + dto.setPageSize(-1); + PageListVO personnelsOverviewCompanyDetailsVOPageListVO = companyDistributionDetails(dto); + try { + EasyExcelUtils.exportExcel(getResponse(), "人员概览-司龄分布", personnelsOverviewCompanyDetailsVOPageListVO.getList(), PersonnelsOverviewCompanyDetailsVO.class); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public PageListVO seniorityDistributionDetails(FtbPersonnlesAnalysisDTO dto) { + Date now = new Date(); + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.seniorityDistributionList( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(queryLists)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 工龄格式转换 + if (StrUtil.isNotBlank(a.getSeniority())) { + Date ageWorkAge = DateUtil.parse(a.getSeniority()); + long age = DateUtil.betweenYear(ageWorkAge, now, false); + a.setSeniority(age + "年"); + } + }); + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewSeniorityDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void seniorityDistributionDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewSeniorityDetailsVOPageListVO = seniorityDistributionDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-司龄分布", personnelsOverviewSeniorityDetailsVOPageListVO.getList(), PersonnelsOverviewSeniorityDetailsVO.class); + } + + @Override + public PageListVO insurancePurchaseAnalysisDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 获取保险购买情况字段ID + LambdaQueryWrapper formFieldWrapper = Wrappers.lambdaQuery(); + formFieldWrapper.eq(FtbPersonnelsRegistrationFormField::getName, "保险购买情况"); + formFieldWrapper.eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0); + formFieldWrapper.last("limit 1"); + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectOne(formFieldWrapper); + if (Objects.isNull(ftbPersonnelsRegistrationFormFields)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.insurancePurchaseAnalysisList( + ftbPersonnelsRegistrationFormFields.getId(), + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(queryLists)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 获取保险购买情况选项 + List options = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList( + Wrappers.lambdaQuery() + .eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, ftbPersonnelsRegistrationFormFields.getId()) + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0)); + Map insurancePurchaseMap = options.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, FtbPersonnelsRegistrationFormFieldOption::getName)); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 保险购买情况转换 + if (StrUtil.isNotBlank(a.getInsurancePurchase())) { + a.setInsurancePurchase(insurancePurchaseMap.getOrDefault(a.getInsurancePurchase(), a.getInsurancePurchase())); + } + }); + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewInsuranceDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void insurancePurchaseAnalysisDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewInsuranceDetailsVOPageListVO = insurancePurchaseAnalysisDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-保险购买分析", personnelsOverviewInsuranceDetailsVOPageListVO.getList(), PersonnelsOverviewInsuranceDetailsVO.class); + } + + @Override + public PageListVO educationalBackgroundDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + List queryLists = ftbPersonnelsOverviewAnalysisMapper.educationalBackgroundList( + DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()), ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(queryLists)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdListByOrganizeIds, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map userBoundVOMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity())); + // 学历转换映射 + Map educationMap = new HashMap<>(); + educationMap.put("33", "小学"); + educationMap.put("18", "初中"); + educationMap.put("19", "高中"); + educationMap.put("20", "中专"); + educationMap.put("21", "大专"); + educationMap.put("22", "本科"); + educationMap.put("23", "硕士"); + educationMap.put("24", "博士"); + educationMap.put("25", "其他"); + queryLists.forEach(a -> { + UserBoundVO userBoundVO = userBoundVOMap.get(a.getUserId()); + if (Objects.nonNull(userBoundVO)) { + a.setUserName(userBoundVO.getUserName()); + a.setOrganization(userBoundVO.getOrganizeName()); + a.setJobLevel(userBoundVO.getPositionName()); + a.setJobGrade(userBoundVO.getGradeName()); + } + // 学历转换 + if (StrUtil.isNotBlank(a.getEducation())) { + a.setEducation(educationMap.getOrDefault(a.getEducation(), a.getEducation())); + } + }); + result.getPagination().setTotal(queryLists.size()); + // 按照组织排序 + queryLists.sort(Comparator.comparing(PersonnelsOverviewEducationDetailsVO::getOrganization)); + result.setList(queryLists); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), queryLists.size()); + return CultivatePage.paginate(queryLists, page); + } + return result; + } + + @Override + public void educationalBackgroundDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewEducationDetailsVOPageListVO = educationalBackgroundDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-学历分布", personnelsOverviewEducationDetailsVOPageListVO.getList(), PersonnelsOverviewEducationDetailsVO.class); + } + + @Override + public PageListVO departmentHeadcountRatioDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + // 获取组织ID列表 + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(dto.getOrganizationId(), false, null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return result; + } + List storeOrgIds = listActionResult.getData() + .stream().filter(a -> OrganizeCategoryEnums.DEPARTMENT.equals(a.getOrganizeCategoryEnums())) + .map(OrganizeGeneralDetailVO::getId) + .filter(id -> dto.getOrganizationId().contains(id)).collect(Collectors.toList()); + if (CollUtil.isEmpty(storeOrgIds)) { + return result; + } + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(storeOrgIds, null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(presenceOfPersonnel)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(presenceOfPersonnel, null); + if (actionResult.getCode() != 200) { + return result; + } + Map employeeIDQuery = employeeIDQuery(presenceOfPersonnel); + List detailsList = new ArrayList<>(); + actionResult.getData() + .forEach(a -> { + PersonnelsOverviewDepartmentDetailsVO detailsVO = new PersonnelsOverviewDepartmentDetailsVO(); + detailsVO.setUserId(a.getId()); + detailsVO.setUserName(a.getUserName()); + detailsVO.setOrganization(a.getOrganizeName()); + detailsVO.setJobLevel(a.getPositionName()); + detailsVO.setJobGrade(a.getGradeName()); + detailsVO.setEmployeeId(employeeIDQuery.getOrDefault(a.getId(),"")); + detailsList.add(detailsVO); + }); + result.getPagination().setTotal(detailsList.size()); + // 按照组织排序 + detailsList.sort(Comparator.comparing(PersonnelsOverviewDepartmentDetailsVO::getOrganization)); + result.setList(detailsList); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), detailsList.size()); + return CultivatePage.paginate(detailsList, page); + } + return result; + } + + @Override + public void departmentHeadcountRatioDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewDepartmentDetailsVOPageListVO = departmentHeadcountRatioDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-部门人数分布", personnelsOverviewDepartmentDetailsVOPageListVO.getList(), PersonnelsOverviewDepartmentDetailsVO.class); + } + + @Override + public PageListVO proportionOfStoreNumbersDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + // 获取门店组织ID列表 + ActionResult> listActionResult = v2OrganizeApi.organizesOrHaveChildByOrganizeIds(dto.getOrganizationId(), false, null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData())) { + return result; + } + List storeOrgIds = listActionResult.getData() + .stream().filter(a -> OrganizeCategoryEnums.STORE.equals(a.getOrganizeCategoryEnums())) + .map(OrganizeGeneralDetailVO::getId) + .filter(id -> dto.getOrganizationId().contains(id)).collect(Collectors.toList()); + if (CollUtil.isEmpty(storeOrgIds)) { + return result; + } + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(storeOrgIds, null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(presenceOfPersonnel)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(presenceOfPersonnel, null); + if (actionResult.getCode() != 200) { + return result; + } + Map employeeIDQuery = employeeIDQuery(presenceOfPersonnel); + List detailsList = new ArrayList<>(); + actionResult.getData() + .forEach(a -> { + PersonnelsOverviewStoreDetailsVO detailsVO = new PersonnelsOverviewStoreDetailsVO(); + detailsVO.setUserId(a.getId()); + detailsVO.setUserName(a.getUserName()); + detailsVO.setOrganization(a.getOrganizeName()); + detailsVO.setJobLevel(a.getPositionName()); + detailsVO.setJobGrade(a.getGradeName()); + detailsVO.setEmployeeId(employeeIDQuery.getOrDefault(a.getId(),"")); + detailsList.add(detailsVO); + }); + result.getPagination().setTotal(detailsList.size()); + // 按照组织排序 + detailsList.sort(Comparator.comparing(PersonnelsOverviewStoreDetailsVO::getOrganization)); + result.setList(detailsList); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), detailsList.size()); + return CultivatePage.paginate(detailsList, page); + } + return result; + } + + @Override + public void proportionOfStoreNumbersDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewStoreDetailsVOPageListVO = proportionOfStoreNumbersDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-门店人数分布", personnelsOverviewStoreDetailsVOPageListVO.getList(), PersonnelsOverviewStoreDetailsVO.class); + } + + @Override + public PageListVO proportionOfJobHeadcountDetails(FtbPersonnlesAnalysisDTO dto) { + PageListVO result = new PageListVO<>(); + result.setPagination(initPaginationVO(dto)); + result.setList(new ArrayList<>()); + List userIdListByOrganizeIds = selectedOrganizationAndPositionEmployees(dto.getOrganizationId(), null); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return result; + } + // 在职人员 + List presenceOfPersonnel = ftbPersonnelsOverviewAnalysisMapper.departmentHeadcountRatio(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) + , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + if (CollUtil.isEmpty(presenceOfPersonnel)) { + return result; + } + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(presenceOfPersonnel, ""); + if (actionResult.getCode() != 200) { + return result; + } + Map employeeIDQuery = employeeIDQuery(presenceOfPersonnel); + List detailsList = new ArrayList<>(); + actionResult.getData().stream() + .filter(a -> { + if (CollUtil.isNotEmpty(dto.getPostId())) { + return dto.getPostId().contains(a.getPositionId()) && dto.getOrganizationId().contains(a.getOrganizeId()); + } + return dto.getOrganizationId().contains(a.getOrganizeId()); + }) + .forEach(a -> { + PersonnelsOverviewJobDetailsVO detailsVO = new PersonnelsOverviewJobDetailsVO(); + detailsVO.setUserId(a.getId()); + detailsVO.setUserName(a.getUserName()); + detailsVO.setOrganization(a.getOrganizeName()); + detailsVO.setJobLevel(a.getPositionName()); + detailsVO.setJobGrade(a.getGradeName()); + detailsVO.setEmployeeId(employeeIDQuery.getOrDefault(a.getId(),"")); + detailsList.add(detailsVO); + }); + result.getPagination().setTotal(detailsList.size()); + // 按照组织排序 + detailsList.sort(Comparator.comparing(PersonnelsOverviewJobDetailsVO::getOrganization)); + result.setList(detailsList); + // 分页 + if (dto.getPageSize() != -1) { + Page page = Page.of(dto.getCurrentPage(), dto.getPageSize(), detailsList.size()); + return CultivatePage.paginate(detailsList, page); + } + return result; + } + + @Override + public void proportionOfJobHeadcountDetailsExport(FtbPersonnlesAnalysisDTO dto) throws IOException { + dto.setPageSize(-1); + PageListVO personnelsOverviewJobDetailsVOPageListVO = proportionOfJobHeadcountDetails(dto); + EasyExcelUtils.exportExcel(getResponse(), "人员概览-岗位人数分布", personnelsOverviewJobDetailsVOPageListVO.getList(), PersonnelsOverviewJobDetailsVO.class); + } + + /** + * 所选组织和岗位员工 + */ + private List selectedOrganizationAndPositionEmployees(List organizationId, List postIds) { + QueryPageUserDTO queryPageUserDTO = new QueryPageUserDTO(); + queryPageUserDTO.setIsPage(false); + queryPageUserDTO.setOrganizeIds(organizationId); + queryPageUserDTO.setHaveChildOrganizeId(false); + ActionResult> pageListVOActionResult = v2UserApi.pagePost(queryPageUserDTO); + if (pageListVOActionResult.getCode() == 200 && Objects.nonNull(pageListVOActionResult.getData())) { + return pageListVOActionResult.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + } + return null; + } + + /** + * 计算平均司龄 + * + * @param numberOfDaysOfAge 总司龄天数 + * @param employedInTheMonth 在职员工数 + * @return 平均司龄字符串表示,格式为"年月天" + *

+ * 此方法用于根据总司龄天数和在职员工数计算平均司龄,并以字符串形式返回 + * 如果输入为null或除数/被除数为0,则返回"0"表示无有效数据进行计算 + * 计算过程中,使用四舍五入的方式处理除法运算,并将结果转换为年、月、天的格式 + * 如果计算过程中发生算术异常(如除以零),同样返回"0" + */ + private String averageAgeCalculation(BigDecimal numberOfDaysOfAge, BigDecimal employedInTheMonth) { + // 检查输入参数是否为null + if (numberOfDaysOfAge == null || employedInTheMonth == null) { + return "0"; + } + // 除数或被除数为0 + if (numberOfDaysOfAge.compareTo(BigDecimal.ZERO) == 0 || employedInTheMonth.compareTo(BigDecimal.ZERO) == 0) { + return "0"; + } + StringBuilder result = new StringBuilder(); + try { + // 总司龄天数/在职员工=平均司龄天数 + BigDecimal averageAge = numberOfDaysOfAge.divide(employedInTheMonth, 0, RoundingMode.HALF_UP); + long orginalValue = averageAge.longValue(); + long yearValue = orginalValue / 365; + orginalValue %= 365; + + // 如果年份不为0,追加到结果字符串 + if (yearValue != 0) { + result.append(yearValue).append("年"); + } + + // 如果剩余天数为0,直接返回结果 + if (orginalValue == 0) { + return result.toString(); + } + + // 计算月份和剩余天数 + long monthValue = orginalValue / 30; + long dayValue = orginalValue % 30; + + // 如果月份不为0,追加到结果字符串 + if (monthValue != 0) { + result.append(monthValue).append("月"); + } + + // 如果天数不为0,追加到结果字符串 + if (dayValue != 0) { + result.append(dayValue).append("天"); + } + + return result.toString(); + } catch (ArithmeticException e) { + // 处理除法异常,返回默认值 + return "0"; + } + } + + /** + * 类型占比 + */ + private List handleEmployeeStatusProportion(List result + , List data, BigDecimal employedInTheMonth) { + result.forEach(a -> { + Optional overviewProportionVO = data.stream() + .filter(b -> a.getDataName().equals(b.getDataName())) + .findFirst(); + a.setDataName(a.getDataTranslationValue()); + if (overviewProportionVO.isPresent()) { + PersonnelsOverviewProportionVO baseData = overviewProportionVO.get(); + a.setData(baseData.getData()); + a.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(baseData.getData(), employedInTheMonth)); + } + }); + return result; + } + + + interface AgeDistribution { + /** + * 定义司龄和年龄计算 + */ + default PersonnelsOverviewProportionVO algorithmDefinition(List data, BigDecimal employedInTheMonth) { + PersonnelsOverviewProportionVO personnelOverviewProportionVO = + new PersonnelsOverviewProportionVO(getDataName(), 0, BigDecimal.ZERO, getDataName()); + long count = data.stream().filter(this::calculationLogic).count(); + personnelOverviewProportionVO.setData((int) count); + personnelOverviewProportionVO.setPercentage(FtbPersonnelsOverviewAnalysisService.computeDivision(count, employedInTheMonth)); + return personnelOverviewProportionVO; + } + + /** + * 逻辑判断 + */ + boolean calculationLogic(PersonnelsOverviewProportionVO a); + + /** + * 汉字转义 + */ + String getDataName(); + } + + /** + * 获取司龄和年龄计算 + */ + private List getAgeDistributions(List data + , List calculatedData, BigDecimal employedInTheMonth) { + return data.stream() + .map(a -> a.algorithmDefinition(calculatedData, employedInTheMonth)) + .collect(Collectors.toList()); + } + + @Getter + @RequiredArgsConstructor + enum AgeDistributionFirst implements AgeDistribution { + // 不满3个月 + // 3-6个月 + // 7个月~1年 + // 1~3年 + // 4~5年 + // 6~10年 + // 10年以上 + LESS_THAN_THREE_MONTHS("不满3个月") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() < 30 * 3; + } + }, + THREE_TO_SIX_MONTHS("3-6个月") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 30 * 3 && a.getData() < 30 * 6; + } + }, + SEVEN_MONTHS_TO_ONE_YEAR("6个月~1年") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 30 * 6 && a.getData() < 365; + } + }, + ONE_TO_THREE_YEARS("1~3年") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 365 && a.getData() < 365 * 3; + } + }, + FOUR_TO_FIVE_YEARS("3~5年") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 365 * 3 && a.getData() < 365 * 5; + } + }, + SIX_TO_TEN_YEARS("5~10年") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 365 * 5 && a.getData() < 365 * 10; + } + }, + MORE_THAN_TEN_YEARS("10年以上") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 365 * 10; + } + }; + + private final String dataName; + + } + + @Getter + @RequiredArgsConstructor + enum AgeDistributionSecond implements AgeDistribution { + /** + * 25岁以下 + * 25~35岁 + * 36~45岁 + * 46~55岁 + * 55岁以上 + */ + LESS_THAN_25("25岁以下") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() < 25; + } + }, + BETWEEN_25_AND_35("25~35岁") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() >= 25 && a.getData() <= 35; + } + }, + BETWEEN_36_AND_45("36~45岁") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() > 35 && a.getData() <= 45; + } + }, + BETWEEN_46_AND_55("46~55岁") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() > 45 && a.getData() <= 55; + } + }, + MORE_THAN_55("55岁以上") { + @Override + public boolean calculationLogic(PersonnelsOverviewProportionVO a) { + return a.getData() > 55; + } + }; + + private final String dataName; + + } + + /** + * 获取组织下离职员工ID集合 + * + * @param orgIds + * @return + */ + @SuppressWarnings("Duplicates") + private List doEmployeesOfResignOrganization(List orgIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark, 0); + wrapper.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition, "305"); + List managements = turnoverManagementMapper.selectList(wrapper); + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty(JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()).stream().filter(orgIds::contains).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + } + + private List doEmployeesOfResignOrganizationList(List orgIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark, 0); + wrapper.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition, "305"); + List managements = turnoverManagementMapper.selectList(wrapper); + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty(JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()).stream().filter(orgIds::contains).collect(Collectors.toList())) + ).map(a -> { + List workerGroupDataDtos = JSONObject.parseArray(a.getOrganizationInfo(), WorkerGroupDataDto.class); + PersonnelsOverviewSituationDetailsVO vo = new PersonnelsOverviewSituationDetailsVO(); + vo.setUserId(a.getUserId()); + vo.setUserName(a.getUserName()); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto workerGroupDataDto = workerGroupDataDtos.get(0); + vo.setOrganization(workerGroupDataDto.getAffiliatedOrgName()); + vo.setJobLevel(workerGroupDataDto.getAffiliatedPositionName()); + vo.setJobGrade(workerGroupDataDto.getAffiliatedRankName()); + } + return vo; + }).collect(Collectors.toList()); + } + + private Map employeeIDQuery(List userIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.select(FtbPersonnelsStaffRoster::getUserId,FtbPersonnelsStaffRoster::getSystemWokerId); + wrapper.in(FtbPersonnelsStaffRoster::getUserId,userIds); + List staffRosters = ftbPersonnelsStaffRosterMapper.selectList(wrapper); + return staffRosters.stream() + .filter(a->StrUtil.isNotBlank(a.getSystemWokerId())) + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getSystemWokerId)); + } + + private PaginationVO initPaginationVO(CultivatePage dto) { + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(0); + return pagination; + } + + public static HttpServletResponse getResponse() { + // 从当前threadlocal中获取到 + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return servletRequestAttributes.getResponse(); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionUserServiceImpl.java new file mode 100644 index 0000000..71e287a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionUserServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.personnels.mapper.FtbPersonnelsPermissionUserMapper; +import jnpf.model.personnels.po.FtbPersonnelsPermissionUser; +import jnpf.personnels.service.FtbPersonnelsPermissionUserService; +@Service +public class FtbPersonnelsPermissionUserServiceImpl extends ServiceImpl implements FtbPersonnelsPermissionUserService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionsServiceImpl.java new file mode 100644 index 0000000..7b4a255 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPermissionsServiceImpl.java @@ -0,0 +1,484 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.cultivate.vo.identify.IdentifyAppAuthorityAppraiserVO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsDTO; +import jnpf.model.personnels.dto.authoritys.FtbAddNewPermissionsUpdateDTO; +import jnpf.model.personnels.dto.authoritys.FtbPermissionInfoDTO; +import jnpf.model.personnels.po.FtbPersonnelsAuthoritys; +import jnpf.model.personnels.po.FtbPersonnelsPermissionUser; +import jnpf.model.personnels.po.FtbPersonnelsPermissions; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.authoritys.FtbPermissionInfoVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionUserVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsPermissionVO; +import jnpf.model.personnels.vo.authoritys.FtbPersonnelsScopeVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.ListIdDTO; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.user.SubordinateUserInfoVO; +import jnpf.permission.model.user.UserBoundMoreInfoVO; +import jnpf.personnels.mapper.FtbPersonnelsAuthoritysMapper; +import jnpf.personnels.mapper.FtbPersonnelsPermissionsMapper; +import jnpf.personnels.service.FtbPersonnelsPermissionUserService; +import jnpf.personnels.service.FtbPersonnelsPermissionsService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsPermissionsServiceImpl extends ServiceImpl implements FtbPersonnelsPermissionsService { + + @Resource + private FtbPersonnelsPermissionUserService ftbPersonnelsPermissionUserService; + + @Resource + private FtbPersonnelsAuthoritysMapper ftbPersonnelsAuthoritysMapper; + + @Resource + private PositionApi positionApi; + + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private UserApi userApi; + + @Resource + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Resource + private PersonnelPerUtils personnelPerUtils; + + /** + * 特殊权限标识 + */ + public static final Set SPECIAL_PERMISSION_IDENTIFIER = Set.of("Personnel:Configuration:configuration", "Examination:Paper:Marking" + , "Appraisal:Practical:Review", "Audit:Cases"); + + @Override + @Transactional + public void deletePermissions(String id) { + LambdaQueryWrapper ftbPersonnelsPermissionUserLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsPermissionUserLambdaQueryWrapper.eq(FtbPersonnelsPermissionUser::getPermissionId, id); + ftbPersonnelsPermissionUserService.remove(ftbPersonnelsPermissionUserLambdaQueryWrapper); + this.baseMapper.deleteById(id); + } + + @Override + public List queryThePermissionListWhenAdding(Long moduleType) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsAuthoritys::getModuleType, moduleType); + queryWrapper.eq(FtbPersonnelsAuthoritys::getEnableMark, 0); + List ftbPersonnelsAuthoritysList = ftbPersonnelsAuthoritysMapper.selectList(queryWrapper); + return recursivePermissions(ftbPersonnelsAuthoritysList, "0", null); + } + + @Override + public List queryPermissionList(UserInfo userInfo, String parentPermissions, Integer permissionType) { + List authorityIds = null; + if (!userInfo.getIsAdministrator()) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPermissions::getUserId, userInfo.getUserId()); + queryWrapper.eq(FtbPersonnelsPermissions::getPermissionType, permissionType); + FtbPersonnelsPermissions ftbPersonnelsPermissions = this.baseMapper.selectOne(queryWrapper); + if (Objects.isNull(ftbPersonnelsPermissions)) { + return null; + } + LambdaQueryWrapper ftbPersonnelsPermissionUserLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsPermissionUserLambdaQueryWrapper.eq(FtbPersonnelsPermissionUser::getPermissionId, ftbPersonnelsPermissions.getId()); + List ftbPersonnelsPermissionUserList = ftbPersonnelsPermissionUserService.list(ftbPersonnelsPermissionUserLambdaQueryWrapper); + authorityIds = ftbPersonnelsPermissionUserList.stream().map(FtbPersonnelsPermissionUser::getAuthorityId).collect(Collectors.toList()); + } + + LambdaQueryWrapper ftbPersonnelsAuthoritysLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsAuthoritysLambdaQueryWrapper.in(FtbPersonnelsAuthoritys::getAuthorityIdentify, List.of(parentPermissions.split(StringPool.COMMA))); + List ftbPersonnelsAuthoritys = ftbPersonnelsAuthoritysMapper.selectList(ftbPersonnelsAuthoritysLambdaQueryWrapper); + // 登记表配置、试卷批阅、批阅实操鉴定、审核案例 + if (SPECIAL_PERMISSION_IDENTIFIER.contains(parentPermissions)) { + if (userInfo.getIsAdministrator() || authorityIds.contains("68") + || authorityIds.contains("70") || authorityIds.contains("71") + || authorityIds.contains("135")) { + return List.of(parentPermissions); + } + } + ftbPersonnelsAuthoritysLambdaQueryWrapper.clear(); + List authoritysIds = ftbPersonnelsAuthoritys.stream().map(FtbPersonnelsAuthoritys::getId).collect(Collectors.toList()); + ftbPersonnelsAuthoritysLambdaQueryWrapper.in(FtbPersonnelsAuthoritys::getParentId, authoritysIds); + ftbPersonnelsAuthoritysLambdaQueryWrapper.in(!CollectionUtils.isEmpty(authorityIds), FtbPersonnelsAuthoritys::getId, authorityIds); + return ftbPersonnelsAuthoritysMapper.selectList(ftbPersonnelsAuthoritysLambdaQueryWrapper) + .stream() + .map(FtbPersonnelsAuthoritys::getAuthorityIdentify) + .collect(Collectors.toList()); + + } + + @Override + @Transactional + public void addNewPermissions(FtbAddNewPermissionsDTO ftbAddNewPermissionsDTO) { + // 是否已添加 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPermissions::getUserId, ftbAddNewPermissionsDTO.getUserId()); + queryWrapper.eq(FtbPersonnelsPermissions::getPermissionType, ftbAddNewPermissionsDTO.getPermissionType()); + if (this.baseMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("请勿对同一用户设置重复权限"); + } + savePermissons(ftbAddNewPermissionsDTO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addNewPermissionsChange(FtbAddNewPermissionsUpdateDTO ftbAddNewPermissionsUpdateDTO) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbAddNewPermissionsUpdateDTO.getId()); + LambdaQueryWrapper ftbPersonnelsPermissionUserLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsPermissionUserLambdaQueryWrapper.eq(FtbPersonnelsPermissionUser::getPermissionId, ftbAddNewPermissionsUpdateDTO.getId()); + FtbPersonnelsPermissions ftbPersonnelsPermissions = this.baseMapper.selectById(ftbAddNewPermissionsUpdateDTO.getId()); + ftbAddNewPermissionsUpdateDTO.setOrgNames(ftbPersonnelsPermissions.getOrgNames()); + ftbAddNewPermissionsUpdateDTO.setPostRankNames(ftbPersonnelsPermissions.getPostRankNames()); + ftbAddNewPermissionsUpdateDTO.setPostNames(ftbPersonnelsPermissions.getPostNames()); + ftbAddNewPermissionsUpdateDTO.setOrgIds(ftbPersonnelsPermissions.getOrgIds()); + ftbAddNewPermissionsUpdateDTO.setPostRankIds(ftbPersonnelsPermissions.getPostRankIds()); + ftbAddNewPermissionsUpdateDTO.setPostIds(ftbPersonnelsPermissions.getPostIds()); + ftbAddNewPermissionsUpdateDTO.setPhone(ftbAddNewPermissionsUpdateDTO.getPhone()); + this.baseMapper.delete(queryWrapper); + ftbPersonnelsPermissionUserService.remove(ftbPersonnelsPermissionUserLambdaQueryWrapper); + savePermissons(ftbAddNewPermissionsUpdateDTO); + } + + @Override + public FtbPersonnelsPermissionUserVO adminDetails(String id, Long moduleType) { + FtbPersonnelsPermissions ftbPersonnelsPermissions = this.baseMapper.selectById(id); + FtbPersonnelsPermissionUserVO ftbPersonnelsPermissionUserVO = FtbPersonnelsPermissionUserVO.convertFtbPersonnelsPermissionUserVO(ftbPersonnelsPermissions); + LambdaQueryWrapper ftbpersonstaffstaffLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbpersonstaffstaffLambdaQueryWrapper.select(FtbPersonnelsStaffRoster::getName, FtbPersonnelsStaffRoster::getPhone); + ftbpersonstaffstaffLambdaQueryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, ftbPersonnelsPermissionUserVO.getUserId()); + ftbpersonstaffstaffLambdaQueryWrapper.last("limit 1"); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRosters = ftbPersonnelsStaffRosterService.getBaseMapper().selectOne(ftbpersonstaffstaffLambdaQueryWrapper); + // 姓名 + ftbPersonnelsPermissionUserVO.setUserName(Objects.nonNull(ftbPersonnelsStaffRosters) ? ftbPersonnelsStaffRosters.getName() : "花名册中暂无此人姓名"); + ftbPersonnelsPermissionUserVO.setPhone(Objects.nonNull(ftbPersonnelsStaffRosters) ? ftbPersonnelsStaffRosters.getPhone() : "花名册中暂无此人号码"); + LambdaQueryWrapper ftbPersonnelsPermissionUserLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsPermissionUserLambdaQueryWrapper.eq(FtbPersonnelsPermissionUser::getPermissionId, id); + List ftbPersonnelsPermissionUsers = ftbPersonnelsPermissionUserService.list(ftbPersonnelsPermissionUserLambdaQueryWrapper); + List authorityIds = ftbPersonnelsPermissionUsers.stream().map(FtbPersonnelsPermissionUser::getAuthorityId).collect(Collectors.toList()); + LambdaQueryWrapper ftbPersonnelsAuthoritysLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsAuthoritysLambdaQueryWrapper.eq(FtbPersonnelsAuthoritys::getEnableMark, 0); + ftbPersonnelsAuthoritysLambdaQueryWrapper.eq(FtbPersonnelsAuthoritys::getModuleType, moduleType); + List ftbPersonnelsAuthoritys = ftbPersonnelsAuthoritysMapper.selectList(ftbPersonnelsAuthoritysLambdaQueryWrapper); + List ftbPersonnelsPermissionVOS = recursivePermissions(ftbPersonnelsAuthoritys, "0", authorityIds); + ftbPersonnelsPermissionUserVO.setFtbPersonnelsPermissionVOS(ftbPersonnelsPermissionVOS); + // 直属主管 + List employeeOrganizationInformationVOS = new ArrayList<>(); + // 获取组织 + // List orgPositionInfoList = positionApi.getOrgPositionInfoList(ftbPersonnelsPermissionUserVO.getUserId()); + List userBoundMoreInfosByUserId = userApi.getUserBoundMoreInfosByUserId(ftbPersonnelsPermissionUserVO.getUserId()); + for (UserBoundMoreInfoVO userBoundMoreInfoVO : userBoundMoreInfosByUserId) { + FtbPersonnelsPermissionUserVO.EmployeeOrganizationInformationVO employeeOrganizationInformationVO = new FtbPersonnelsPermissionUserVO.EmployeeOrganizationInformationVO(); + employeeOrganizationInformationVO.setPostName(userBoundMoreInfoVO.getPositionName()); + employeeOrganizationInformationVO.setPostRankName(userBoundMoreInfoVO.getPositionGradesName()); + employeeOrganizationInformationVO.setOrgName(userBoundMoreInfoVO.getOrganizeName()); + employeeOrganizationInformationVO.setNameOfImmediateSupervisor(userBoundMoreInfoVO.getManagerRealName()); + employeeOrganizationInformationVOS.add(employeeOrganizationInformationVO); + } + ftbPersonnelsPermissionUserVO.setEmployeeOrganizationInformationVOS(employeeOrganizationInformationVOS); + return ftbPersonnelsPermissionUserVO; + } + + @Override + public Page permissionList(Page page, FtbPermissionInfoDTO ftbPermissionInfoDTO) { + boolean keywordExists = StrUtil.isNotBlank(ftbPermissionInfoDTO.getKeyword()); + if (keywordExists) { + List userIds = baseMapper.rosterFuzzyQuery(ftbPermissionInfoDTO.getKeyword()); + ftbPermissionInfoDTO.getUserId().addAll(userIds); + } + // 是否勾选岗位、组织、职等 + boolean orgIdsExist = StrUtil.isNotBlank(ftbPermissionInfoDTO.getOrgId()) + || StrUtil.isNotBlank(ftbPermissionInfoDTO.getPostId()) + || StrUtil.isNotBlank(ftbPermissionInfoDTO.getPostRankId()); + if (orgIdsExist) { + List userIdsByGradesId = userApi.getUserIdsByGradesId(ftbPermissionInfoDTO.getOrgId(), ftbPermissionInfoDTO.getPostId(), ftbPermissionInfoDTO.getPostRankId()); + if (keywordExists && (CollectionUtils.isEmpty(ftbPermissionInfoDTO.getUserId()) || CollectionUtils.isEmpty(userIdsByGradesId))) { + return page; + } else if (!keywordExists && !CollectionUtils.isEmpty(userIdsByGradesId)) { + ftbPermissionInfoDTO.getUserId().addAll(userIdsByGradesId); + } else { + Collection intersection = CollUtil.intersection(ftbPermissionInfoDTO.getUserId(), userIdsByGradesId); + ftbPermissionInfoDTO.getUserId().clear(); + ftbPermissionInfoDTO.getUserId().addAll(intersection); + } + } + if ((keywordExists || orgIdsExist) && CollectionUtils.isEmpty(ftbPermissionInfoDTO.getUserId())) { + return page; + } + Page result = this.baseMapper.permissionList(page, ftbPermissionInfoDTO); + List userIds = result.getRecords().stream().map(FtbPermissionInfoVO::getUserId).collect(Collectors.toList()); + // 实时查询用户名 + Map userNames; + Map> positionGradesInfoBoundVOMap; + if (!userIds.isEmpty()) { + LambdaQueryWrapper ftbpersonstaffstaffLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbpersonstaffstaffLambdaQueryWrapper.select(FtbPersonnelsStaffRoster::getName, + FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone); + ftbpersonstaffstaffLambdaQueryWrapper.in(FtbPersonnelsStaffRoster::getUserId, userIds); + List ftbPersonnelsStaffRosters = ftbPersonnelsStaffRosterService.getBaseMapper().selectList(ftbpersonstaffstaffLambdaQueryWrapper); + userNames = ftbPersonnelsStaffRosters.stream() + .filter(a -> StrUtil.isNotBlank(a.getName()) && StrUtil.isNotBlank(a.getPhone())) + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, a -> a.getName() + "#" + a.getPhone())); + + ListIdDTO remoteUserIds = new ListIdDTO(); + remoteUserIds.setIds(userIds); + List orgPositionInfoLists = positionApi.postOrgPositionInfoList(remoteUserIds); + positionGradesInfoBoundVOMap = orgPositionInfoLists.stream().filter(a -> StrUtil.isNotBlank(a.getUserId())) + .collect(Collectors.groupingBy(PositionGradesInfoBoundVO::getUserId)); + } else { + userNames = new HashMap<>(); + positionGradesInfoBoundVOMap = new HashMap<>(); + } + UserInfo userInfo = UserProvider.getUser(); + result.getRecords().parallelStream().forEach(ftbPermissionInfoVO -> { + try { + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + // 用户名、手机号、权限名 + String userNamesOrDefault = userNames.getOrDefault(ftbPermissionInfoVO.getUserId(), " # "); + String[] userNamesSplit = userNamesOrDefault.split("#"); + ftbPermissionInfoVO.setUserName(userNamesSplit[0]); + ftbPermissionInfoVO.setPhone(userNamesSplit[1]); + if (StrUtil.isNotBlank(ftbPermissionInfoDTO.getPermissionId())) { + List authorityNames = this.baseMapper + .getTopLevelPermissions(null, Arrays.asList(ftbPermissionInfoVO.getPermissionIds().split(","))); + ftbPermissionInfoVO.setPermissionNames(authorityNames); + } else { + if (StrUtil.isNotBlank(ftbPermissionInfoVO.getPermissionIds())) { + List authorityNames = this.baseMapper + .getTopLevelPermissions(1, Arrays.asList(ftbPermissionInfoVO.getPermissionIds().split(","))); + ftbPermissionInfoVO.setPermissionNames(authorityNames); + } + } + // 根据员工id获取其所有组织、岗位、职等 + List orgPositionInfoList = positionGradesInfoBoundVOMap.get(ftbPermissionInfoVO.getUserId()); + if (!CollectionUtils.isEmpty(orgPositionInfoList)) { + List orgName = orgPositionInfoList.stream().map(PositionGradesInfoBoundVO::getOrganizeNames) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .distinct().collect(Collectors.toList()); + List postName = orgPositionInfoList.stream().map(PositionGradesInfoBoundVO::getPositionName).distinct().collect(Collectors.toList()); + List postRankName = orgPositionInfoList.stream().map(PositionGradesInfoBoundVO::getPositionGradesName).distinct().collect(Collectors.toList()); + ftbPermissionInfoVO.setOrgName(orgName); + ftbPermissionInfoVO.setPostName(postName); + ftbPermissionInfoVO.setPostRankName(postRankName); + } + } finally { + DataSourceContextHolder.clearDatasourceType(); + } + }); + return result; + } + + @Override + @SuppressWarnings("Duplicates") + public List queryAuthorityAppraiser(String orgId) { + List reviewerPermissions = baseMapper.getReviewerPermissions(List.of("71")); + if (CollectionUtils.isEmpty(reviewerPermissions)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, reviewerPermissions); + List ftbPersonnelsPermissions = baseMapper.selectList(queryWrapper); + if (CollectionUtils.isEmpty(ftbPersonnelsPermissions)) { + return null; + } + String userId = UserProvider.getLoginUserId(); + // 校验是否有关联关系 + List userIds = new ArrayList<>(); + String userIdAndOrgId = userId + "#" + orgId; + for (FtbPersonnelsPermissions ftbPersonnelsPermission : ftbPersonnelsPermissions) { + if (ftbPersonnelsPermission.getScopePermission() == 2) { + // 通过人员去排查 + List subordinateUserInfoVOS = userApi.userInfoByLeaderId(ftbPersonnelsPermission.getUserId()); + if (CollectionUtils.isEmpty(subordinateUserInfoVOS)) { + continue; + } + List childrenUserIds = subordinateUserInfoVOS.stream() + .filter(a -> !CollectionUtils.isEmpty(a.getPositionMoreBoundVOList())) + .flatMap(a -> a.getPositionMoreBoundVOList().stream()) + .map(a -> a.getUserId() + "#" + a.getOrganizeId()).collect(Collectors.toList()); + if (childrenUserIds.contains(userIdAndOrgId)) { + userIds.add(ftbPersonnelsPermission.getUserId()); + } + } + // 所在组织和下级组织员工,查询组织id进行比对 + List organizeIdsAdnChildByUserId = null; + if (ftbPersonnelsPermission.getScopePermission() == 0) { + organizeIdsAdnChildByUserId = organizeApi.getOrganizeIdsAdnChildByUserId(ftbPersonnelsPermission.getUserId()); + } + // 所在组织员工,查询组织id进行比对 + if (ftbPersonnelsPermission.getScopePermission() == 1) { + organizeIdsAdnChildByUserId = organizeApi.getOrganizeIdsByUserId(ftbPersonnelsPermission.getUserId()); + } + // 指定组织,组织id进行比对 + if (ftbPersonnelsPermission.getScopePermission() == 3) { + organizeIdsAdnChildByUserId = Arrays.asList(ftbPersonnelsPermission.getSpecifyOrgIds().split(StringPool.COMMA)); + } + if (!CollectionUtils.isEmpty(organizeIdsAdnChildByUserId) && organizeIdsAdnChildByUserId.contains(orgId)) { + userIds.add(ftbPersonnelsPermission.getUserId()); + } + } + // 数据组装,过滤自己 + return userIds.stream().filter(a -> !userId.equals(a)).map(a -> { + IdentifyAppAuthorityAppraiserVO identifyAppAuthorityAppraiserVO = new IdentifyAppAuthorityAppraiserVO(); + identifyAppAuthorityAppraiserVO.setId(a); + UserEntity userEntity = userApi.getInfoById(a); + if (Objects.nonNull(userEntity)) { + identifyAppAuthorityAppraiserVO.setRealName(userEntity.getRealName()); + identifyAppAuthorityAppraiserVO.setHeadIcon(UploaderUtil.uploaderImg(userEntity.getHeadIcon())); + } + // 组织信息 + List userBoundMoreInfosByUserId = userApi.getUserBoundMoreInfosByUserId(a); + List boundInfos = new ArrayList<>(); + if (!CollectionUtils.isEmpty(userBoundMoreInfosByUserId)) { + userBoundMoreInfosByUserId.forEach(userBoundMoreInfoVO -> { + IdentifyAppAuthorityAppraiserVO.IdentifyAppAuthorityAppraiserInner identifyAppAuthorityAppraiserInner + = new IdentifyAppAuthorityAppraiserVO.IdentifyAppAuthorityAppraiserInner(); + identifyAppAuthorityAppraiserInner.setOrganizeId(userBoundMoreInfoVO.getOrganizeId()); + identifyAppAuthorityAppraiserInner.setOrganizeName(userBoundMoreInfoVO.getOrganizeName()); + identifyAppAuthorityAppraiserInner.setPositionId(userBoundMoreInfoVO.getPositionId()); + identifyAppAuthorityAppraiserInner.setPositionName(userBoundMoreInfoVO.getPositionName()); + identifyAppAuthorityAppraiserInner.setPositionGradesId(userBoundMoreInfoVO.getPositionGradesId()); + identifyAppAuthorityAppraiserInner.setPositionGradesName(userBoundMoreInfoVO.getPositionGradesName()); + boundInfos.add(identifyAppAuthorityAppraiserInner); + }); + } + identifyAppAuthorityAppraiserVO.setBoundInfos(boundInfos); + return identifyAppAuthorityAppraiserVO; + }).collect(Collectors.toList()); + } + + @Override + public FtbPersonnelsScopeVO selectOrganizationScope() { + FtbPersonnelsScopeVO ftbPersonnelsScopeVO = new FtbPersonnelsScopeVO(); + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + if (orgIds == null) { + ftbPersonnelsScopeVO.setSuperAdmin(1); + return ftbPersonnelsScopeVO; + } + ftbPersonnelsScopeVO.setSuperAdmin(0); + ftbPersonnelsScopeVO.setDataPermissions(orgIds); + return ftbPersonnelsScopeVO; + } + + @Override + public FtbPersonnelsScopeVO selectEmployeeRange() { + FtbPersonnelsScopeVO ftbPersonnelsScopeVO = new FtbPersonnelsScopeVO(); + List userIds = personnelPerUtils.obtainPersonnelDataPermissions(); + if (userIds == null) { + ftbPersonnelsScopeVO.setSuperAdmin(1); + return ftbPersonnelsScopeVO; + } + ftbPersonnelsScopeVO.setSuperAdmin(0); + ftbPersonnelsScopeVO.setDataPermissions(userIds); + return ftbPersonnelsScopeVO; + } + + @Override + public void clearUserPermissions(String userId) { + LambdaQueryWrapper permissionsLambdaQueryWrapper = Wrappers.lambdaQuery(); + permissionsLambdaQueryWrapper.select(FtbPersonnelsPermissions::getId); + permissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + permissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getUserId, userId); + List ftbPersonnelsPermissions = this.baseMapper.selectList(permissionsLambdaQueryWrapper); + ftbPersonnelsPermissions.forEach(a -> deletePermissions(a.getId())); + } + + @Override + public Boolean isThisUserAnEmployee() { + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + return false; + } + LambdaQueryWrapper permissionsLambdaQueryWrapper = Wrappers.lambdaQuery(); + permissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + permissionsLambdaQueryWrapper.eq(FtbPersonnelsPermissions::getUserId, userInfo.getUserId()); + Long count = this.baseMapper.selectCount(permissionsLambdaQueryWrapper); + return count == 0; + } + + + /** + * 权限新增 + */ + private void savePermissons(T ftbAddNewPermissionsDTO) { + FtbPersonnelsPermissions ftbPersonnelsPermissions = FtbAddNewPermissionsDTO.convertFtbPersonnelsPermissions(ftbAddNewPermissionsDTO); + String userCustomId = ftbPersonnelsStaffRosterService.querySystemWorkerId(ftbAddNewPermissionsDTO.getUserId()); + // 根据员工id获取其所有组织、岗位、职等 + List orgPositionInfoList = positionApi.getOrgPositionInfoList(ftbAddNewPermissionsDTO.getUserId()); + if (!CollectionUtils.isEmpty(orgPositionInfoList)) { + String orgIds = orgPositionInfoList.stream().map(PositionGradesInfoBoundVO::getOrganizeIds) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .distinct().collect(Collectors.joining(",")); + String postIds = orgPositionInfoList.stream().map(PositionGradesInfoBoundVO::getPositionId).distinct().collect(Collectors.joining(",")); + String postRankIds = orgPositionInfoList.stream().map(PositionGradesInfoBoundVO::getPositionGradesId).distinct().collect(Collectors.joining(",")); + ftbPersonnelsPermissions.setOrgIds(orgIds); + ftbPersonnelsPermissions.setPostIds(postIds); + ftbPersonnelsPermissions.setPostRankIds(postRankIds); + } + ftbPersonnelsPermissions.setUserCustomId(userCustomId); + this.baseMapper.insert(ftbPersonnelsPermissions); + List ftbPersonalsPermissionUsers = ftbAddNewPermissionsDTO.getPermissionIds().stream().map(permissionId -> { + FtbPersonnelsPermissionUser ftbPersonnelsPermissionUser = new FtbPersonnelsPermissionUser(); + ftbPersonnelsPermissionUser.setAuthorityId(permissionId); + ftbPersonnelsPermissionUser.setPermissionId(ftbPersonnelsPermissions.getId()); + return ftbPersonnelsPermissionUser; + }).collect(Collectors.toList()); + ftbPersonnelsPermissionUserService.saveBatch(ftbPersonalsPermissionUsers); + } + + /** + * 递归权限 + */ + private List recursivePermissions(List ftbPersonnelsAuthoritysList, + String parentId, List includeMap) { + List result = new ArrayList<>(); + for (FtbPersonnelsAuthoritys authority : ftbPersonnelsAuthoritysList) { + FtbPersonnelsPermissionVO permissionVO = new FtbPersonnelsPermissionVO(); + permissionVO.setId(authority.getId()); + permissionVO.setAuthorityName(authority.getAuthorityName()); + permissionVO.setAuthorityType(authority.getAuthorityType()); + permissionVO.setAuthorityIdentify(authority.getAuthorityIdentify()); + permissionVO.setParentId(authority.getParentId()); + permissionVO.setChecked(includeMap != null && includeMap.contains(authority.getId())); + if (parentId.equals(permissionVO.getParentId())) { + permissionVO.setChildrenList(recursivePermissions(ftbPersonnelsAuthoritysList, authority.getId(), includeMap)); + result.add(permissionVO); + } + } + return result; + } + + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPostApplyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPostApplyServiceImpl.java new file mode 100644 index 0000000..661bcb0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPostApplyServiceImpl.java @@ -0,0 +1,1139 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.attendance.AttendanceUserApi; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.mapper.FtbCultivatePositionIdentifyResultMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionMemberMapper; +import jnpf.cultivate.mapper.FtbCultivatePromotionNewMapper; +import jnpf.cultivate.service.CultivateIdentifyApplyService; +import jnpf.cultivate.service.FtbCultivateCourseService; +import jnpf.cultivate.service.FtbCultivateExamUserService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.cultivate.service.impl.FtbCultivateFileService; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.StoreEntity; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.vo.UserSalaryHistoryVo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineFileDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.po.org.FtbPositionGradesInfoBoundVO; +import jnpf.model.cultivate.req.exam.QueryCultivateExamReq; +import jnpf.model.cultivate.resp.MyExamListVo; +import jnpf.model.cultivate.resp.UserExamDetailVo; +import jnpf.model.cultivate.vo.course.web.PromotionChannelLearnCourseVO; +import jnpf.model.cultivate.vo.identify.IdentifyApplyInfoApiVo; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionVO; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.dto.apply.FtbPersonnelsApplyCreateDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.staff.roster.StaffHomeDto; +import jnpf.model.personnels.dto.staff.roster.StaffPromotionDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsPostApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsApplyWithPerVO; +import jnpf.model.personnels.vo.apply.FtbPersonnelsCourseVO; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditInfoVO; +import jnpf.model.personnels.vo.task.FtbPersonnelsResult; +import jnpf.permission.PositionApi; +import jnpf.permission.RoleApi; +import jnpf.permission.UserApi; +import jnpf.permission.UserPrimaryPositionApi; +import jnpf.permission.dto.SynUserBoundRoleDTO; +import jnpf.permission.dto.UpdateUserManagerBoundDTO; +import jnpf.permission.dto.position.CheckExistByPositionDTO; +import jnpf.permission.dto.relation.BaseUserPrimaryPositionDTO; +import jnpf.permission.dto.role.RemoveUserRolesDTO; +import jnpf.permission.entity.BasePositionGradesEntity; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoBoundVO; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.model.position.PositionInfoNewVO; +import jnpf.permission.model.role.RoleListVO; +import jnpf.permission.model.role.UserBoundRolesVO; +import jnpf.personnels.mapper.FtbPersonnelsPostApplyMapper; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.salary.QuerySalaryApi; +import jnpf.store.service.StoreService; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsPostApplyServiceImpl extends ServiceImpl implements FtbPersonnelsPostApplyService { + + + @Resource + private FtbCultivateFileService ftbCultivateFileService; + + @Resource + private FtbCultivateExamUserService ftbCultivateExamUserService; + + @Resource + private FtbCultivatePositionIdentifyResultMapper ftbCultivatePositionIdentifyResultMapper; + + @Resource + private CultivateIdentifyApplyService cultivateIdentifyApplyService; + + @Resource + private FtbCultivateCourseService ftbCultivateCourseService; + + @Autowired + private UserApi userApi; + + @Resource + private PositionApi positionApi; + + @Resource + private FtbPersonnelsAuditRunTaskService auditRunTaskService; + + @Resource + private FtbPersonnelsAuditSubConfigService subConfigService; + + @Resource + public FtbCultivatePromotionMemberMapper memberMapper; + + @Resource + public PersonnelPerUtils personnelPerUtils; + + @Resource + private FtbPersonnelsStaffRosterService rosterService; + + @Resource + private FtbCultivatePromotionNewService promotionNewService; + + @Resource + private AttendanceGroupApi attendanceGroupApi; + + @Resource + private AttendanceUserApi attendanceUserApi; + + @Resource + private StoreService storeService; + + @Autowired + FtbPersonnelsSalaryService salaryService; + + @Resource + PersonnelOrgUtils personnelOrgUtils; + + @Resource + QuerySalaryApi querySalaryApi; + + @Resource + FtbCultivatePromotionNewMapper cultivatePromotionNewMapper; + + @Autowired + RoleApi roleApi; + + @Autowired + UserPrimaryPositionApi userPrimaryPositionApi; + + @Override + public PageListVO getList(PersonnelsQueryDTO dto, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + UserInfo user = UserProvider.getUser(); + Page mapperList; + if ("2".equals(dto.getMyApprovalFlag())) { + FtbPersonnelsForAppQueryDTO covert = FtbPersonnelsForAppQueryDTO.covert(dto); + covert.setUserId(user.getUserId()); + covert.setConfigType(FtbPersonnelsCofigEnum.PROMOTION_APPROVAL_CONFIGURATION.getConfigType()); + if (dto.getApprovalStatus() == 0){ + mapperList =baseMapper.listOfDataToBeApproved(page, covert); + }else { + mapperList = baseMapper.getApprovalList(page, covert); + } + }else { + // 数据权限orgIds + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + if (CollUtil.isNotEmpty(orgIds)){ + dto.setOrgIds(orgIds); + } + mapperList = baseMapper.getListInfo(page, dto); + } + // 转换数据 + Map map = new HashMap<>(); + List> postCourseIds = cultivatePromotionNewMapper.doQueryTheLearnedPositionIdFirst(null); + if (CollUtil.isNotEmpty(postCourseIds)) { + Map> postMapIds = postCourseIds.stream().collect(Collectors.toMap(k -> k.get("postId"), v -> List.of(v.get("courseIds").split(",")))); + Set postIds = postMapIds.keySet(); + List userIds = mapperList.getRecords().stream().map(FtbPersonnelsApplyVO::getUserId).collect(Collectors.toList()); + for (String post : postIds) { + List courseIds = postMapIds.get(post); + // 查询这个课程是多少人在学习 + // 用户对应的学习记录 + List courseVOS = cultivatePromotionNewMapper.doQueryTheLearnedPositionIdSecondNew(userIds, courseIds, List.of(1)); + if (CollUtil.isEmpty(courseVOS)) continue; + Map userCourseMap = courseVOS.stream().collect(Collectors.toMap(FtbPersonnelsCourseVO::getUserId, FtbPersonnelsCourseVO::getCount, (a, b) -> a)); + for (String userId : userIds) { + if (userCourseMap.containsKey(userId) && userCourseMap.get(userId) == courseIds.size()) { + if (map.containsKey(userId)) { + map.put(userId, map.get(userId) + 1); + } else { + map.put(userId, 1); + } + } + } + } + } + // 用户学习记录 + mapperList.getRecords().forEach(item ->{ + if (map.containsKey(item.getUserId())) item.setNumberOfPositionsStudied(map.get(item.getUserId())); + }); + return CultivatePage.coverPageList(mapperList); + } + + @Override + public PageListVO getListForApp(FtbPersonnelsForAppQueryDTO dto, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + // 2.我的审批 3 抄送我的 4 我的申请 + Page mapperList =null; + dto.setUserId(UserProvider.getUser().getUserId()); + if (!"4".equals(dto.getFlagByApp())) { + // 数据权限orgIds + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + if (CollUtil.isNotEmpty(orgIds)) { + dto.setOrgIds(orgIds); + } + } + mapperList = baseMapper.getListForApp(page, dto); + mapperList.getRecords().forEach(item ->{ + // 查询当前申请时上传的文件信息 + List list = ftbCultivateFileService.lambdaQuery().eq(FtbCultivateFile::getBusinessId, item.getId()).list(); + List fileVOS = list.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + item.setFiles(fileVOS); + }); + return CultivatePage.coverPageList(mapperList); + } + + @Override + public List queryPostInfoByOrgAndUserId(String userId, String orgId) { + List promotionVOList = memberMapper.queryPromotionByUserOrOrgId(userId, orgId); + if (CollUtil.isEmpty(promotionVOList)){ + throw new RuntimeException("当前并未启动晋升通道,无法选择晋升岗位!"); + } + List postIds = promotionVOList.stream().map(FtbCultivatePromotionVO::getChannelIniPoId).collect(Collectors.toList()); + List positionGradesInfoBoundVOS = getPositionGradesInfoBoundVOS(userId, orgId); + List positionInfoNewVOS = postIds.stream().map(id -> { + PositionInfoNewVO data = null; + try { + ActionResult infoNew = positionApi.getInfoNew(id); + if (infoNew.getCode() == 200) { + data = infoNew.getData(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return data; + }).collect(Collectors.toList()); + Map stringStringMap = positionGradesInfoBoundVOS.stream() + .collect( + Collectors.toMap(FtbPositionGradesInfoBoundVO::getPositionId, + FtbPositionGradesInfoBoundVO::getPositionGradesId, + (a, b) -> a)); + List result = new LinkedList<>(); + for (PositionInfoNewVO positionInfoNewVO : positionInfoNewVOS) { + List positionGradesList = positionInfoNewVO.getPositionGradesList(); + // 通过岗位ID获取职等id 进行过滤 + String positionGradesId =stringStringMap.get(positionInfoNewVO.getId()); + if (stringStringMap.containsKey(positionInfoNewVO.getId())) { + // 通过职等ID过滤已经存在的职等 + List newPositionGradesList = + positionGradesList.stream().filter(item->!item.getId().equals(positionGradesId)) + .collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(newPositionGradesList)){ + positionInfoNewVO.setPositionGradesList(null); + positionInfoNewVO.setPositionGradesList(newPositionGradesList); + // 过滤已存在的 + result.add(positionInfoNewVO); + } + }else { + // 封装每一个不存在的岗位 + result.add(positionInfoNewVO); + } + } + return result; + } + + @Override + public FtbPersonnelsBubbleCountVO getListCont(String flag) { + // flag "1" 我的审批 "2" 抄送 + FtbPersonnelsBubbleCountVO countVO=new FtbPersonnelsBubbleCountVO(); + FtbPersonnelsForAppQueryDTO appQueryDTO =new FtbPersonnelsForAppQueryDTO(); + appQueryDTO.setUserId(UserProvider.getUser().getUserId()); + if ("1".equals(flag)){ + appQueryDTO.setFlagByApp("2"); + appQueryDTO.setConfigType(FtbPersonnelsCofigEnum.PROMOTION_APPROVAL_CONFIGURATION.getConfigType()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.PENDING.getCode()); + countVO.setPendingCount(baseMapper.listOfDataToBeApproved(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + countVO.setQuantityUnderCount(baseMapper.getApprovalList(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + countVO.setApprovedCount(baseMapper.getApprovalList(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + countVO.setApprovalFailedCount(baseMapper.getApprovalList(new Page<>(), appQueryDTO).getTotal()); + }else if ("2".equals(flag)){ + appQueryDTO.setFlagByApp("3"); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + appQueryDTO.init("3",FtbPersonnelsCofigEnum.PROMOTION_APPROVAL_CONFIGURATION); + countVO.setQuantityUnderCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setStatusList(null); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + countVO.setApprovedCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + countVO.setApprovalFailedCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + } + return countVO; + } + + @Override + public void clearPromotionData(List userIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsPostApply::getUserId,userIds); + List managements = baseMapper.selectList(wrapper); + if (CollUtil.isEmpty(managements)){ + return; + } + // 清除具有审批数据的数据 + managements.stream().filter(vo-> StringUtils.isNotEmpty(vo.getTaskInfoId())) + .forEach (item->auditRunTaskService.deleteReviewProcessAndRecords(item.getTaskInfoId(),item.getId())); + // 晋升数据 + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.in(FtbPersonnelsPostApply::getId,managements.stream().map(FtbPersonnelsPostApply::getId).collect(Collectors.toList())); + baseMapper.delete(lambdaed); + } + + + + @SneakyThrows + @Override + @Transactional(rollbackFor = {Exception.class,RuntimeException.class}) + public String initiateAPromotionApplication(FtbPersonnelsApplyCreateDto createDto) { + String resultStr = ""; + FtbPersonnelsPostApply ftbPersonnelsPostApply = FtbPersonnelsApplyCreateDto.coverFtbPersonnelsApplyVO(createDto); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsPostApply::getUserId, ftbPersonnelsPostApply.getUserId()); + List list = new ArrayList<>(); + list.add(0); + list.add(1); + wrapper.in(FtbPersonnelsPostApply::getState, list); + FtbPersonnelsPostApply postApply = baseMapper.selectOne(wrapper); + if (ObjectUtil.isNotNull(postApply)) { + throw new RuntimeException("您上次提交的晋升申请还在审核中,请等待审核完成再提交!"); + } + + // 校验直属主管是否为试岗员工 + if (StringUtils.isNotEmpty(ftbPersonnelsPostApply.getImmediateSuperId())){ + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbPersonnelsStaffRoster::getUserId,ftbPersonnelsPostApply.getImmediateSuperId()); + FtbPersonnelsStaffRoster one = rosterService.getOne(lambdaed); + if (one.getWorkerStatus().equals("306")) throw new RuntimeException("试岗员工:"+one.getName()+"不能作为直属主管!"); + } + List positionGradesInfoBoundVOS = + getPositionGradesInfoBoundVOS(ftbPersonnelsPostApply.getUserId(), ftbPersonnelsPostApply.getOrgId()); + if (ObjectUtil.isEmpty(positionGradesInfoBoundVOS)) { + throw new RuntimeException("当前用户未分配岗位,无法进行申请!"); + } + Set collect = positionGradesInfoBoundVOS.stream().map(FtbPositionGradesInfoBoundVO::getPositionGradesId).collect(Collectors.toSet()); + if (!collect.contains(ftbPersonnelsPostApply.getPostRankId())) { + throw new RuntimeException("当前申请变更岗位,在个人岗位中不存在请重新选择进行重试!"); + } + if (StringUtils.isNotEmpty(ftbPersonnelsPostApply.getPromotionReason()) && ftbPersonnelsPostApply.getPromotionReason().length() > 500) { + throw new RuntimeException("晋升原因过长,不能超过500个字,请修改后重试!"); + } + if ( (ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId())) && + (ftbPersonnelsPostApply.getPostId().equals(ftbPersonnelsPostApply.getPostPromotionId())) + && (ftbPersonnelsPostApply.getPostRankId() + .equals(ftbPersonnelsPostApply.getPostRankPromotionId()))){ + throw new RuntimeException("晋升组织岗位职等不能与当前组织岗位职等一致,请重新选择进行重试!"); + } + + List userList = personnelOrgUtils.getUserOrgBoundInfo(ftbPersonnelsPostApply.getUserId()); + if (CollUtil.isNotEmpty(userList) && userList.size() > 1){ + // 员工所属组织 + for (WorkerGroupDataDto dataDto : userList) { + // 员工调岗组织岗位 都相同 不能有不同职等 + if ( !(ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId()) && + ftbPersonnelsPostApply.getPostId().equals(ftbPersonnelsPostApply.getPostPromotionId())) + && (dataDto.getAffiliatedOrg().equals(ftbPersonnelsPostApply.getOrgCurrentId()) && + dataDto.getAffiliatedPosition().equals(ftbPersonnelsPostApply.getPostPromotionId()))){ + throw new RuntimeException("您在目标组织中已所属于该岗位,不可重复选择同一组织下相同岗位!"); + } + } + } + // v1.1 添加门店负责人校验 + // 跨组织校验 + if (!ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId())) { + LambdaQueryWrapper storeEntityLambdaQueryWrapper = Wrappers.lambdaQuery(); + storeEntityLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid, ftbPersonnelsPostApply.getUserId()); + long count = storeService.count(storeEntityLambdaQueryWrapper); + if (count > 0) return "1"; + } + FtbPersonnelsResult result = FtbPersonnelsResult.getResult(createDto.getSource(),createDto.getFlag()); + ftbPersonnelsPostApply.setState(result.state); + ftbPersonnelsPostApply.setSource(result.source); + // 添加薪资详情 + StaffHomeDto staffHomeDto = rosterService.queryWorkerHomeDetail(ftbPersonnelsPostApply.getUserId()); + // 晋升前薪资覆盖 + if (staffHomeDto != null) ftbPersonnelsPostApply.setCurrentPay(staffHomeDto.getCurrSalary()); + List salaryStructureList = createDto.getSalaryStructureList(); + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(createDto.getIsAdjustSalary()) ){ + throw new RuntimeException("目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + } + Integer isDelete = createDto.getIsDelete(); + if ("1".equals(createDto.getIsAdjustSalary()) && isDelete == null){ + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String format = simpleDateFormat.format(createDto.getPayrollEffectiveDate()); + ActionResult> historyByChangeDate = querySalaryApi.getHistoryByChangeDate(format, createDto.getUserId()); + if (historyByChangeDate == null){ + throw new RuntimeException("获取薪酬数据失败!"); + } + List data = historyByChangeDate.getData(); + if (CollUtil.isNotEmpty(data) && StringUtils.isNotEmpty(data.get(0).getFRemark())) return "5"; + // 没有薪酬默认不做废 + isDelete = 0; + } + if ("1".equals(createDto.getIsAdjustSalary()) && CollUtil.isNotEmpty(salaryStructureList)){ + ftbPersonnelsPostApply.setSalaryStructureList(JSONObject.toJSONString(salaryStructureList)); + } + + // 重新申请 + if (StringUtils.isNotEmpty(createDto.getId())){ + // 离职编辑 删除之前的信息 + // 删除上传的文件重新上传 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateFile::getBusinessId, createDto.getId()); + ftbCultivateFileService.remove(updateWrapper); + ftbPersonnelsPostApply.setId(createDto.getId()); + baseMapper.updateById(ftbPersonnelsPostApply); + }else { + baseMapper.insert(ftbPersonnelsPostApply); + } + // 创建审批 + String postApplyId = ftbPersonnelsPostApply.getId(); + UserInfo userInfo = UserProvider.getUser(); + if (ftbPersonnelsPostApply.getUserId().equals(userInfo.getUserId())){ + throw new RuntimeException("无法给自己进行手动晋升!"); + } + // 手动变更薪资详情 + if (createDto.getIsAdjustSalary().equals("1")){ + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + userInfoWithSalary.setRankName(ftbPersonnelsPostApply.getPostRankPromotionName()); + userInfoWithSalary.setRankId(ftbPersonnelsPostApply.getPostRankPromotionId()); + userInfoWithSalary.setFOrgId(ftbPersonnelsPostApply.getOrgCurrentId()); + userInfoWithSalary.setFOrgName(ftbPersonnelsPostApply.getOrgCurrentName()); + userInfoWithSalary.setPostId(ftbPersonnelsPostApply.getPostPromotionId()); + userInfoWithSalary.setPostName(ftbPersonnelsPostApply.getPostPromotionName()); + salaryService.saveTheChangePayInformation(salaryStructureList, + ftbPersonnelsPostApply.getUserId(), + ftbPersonnelsPostApply.getPayrollEffectiveDate(), + userInfoWithSalary, + "1", + ftbPersonnelsPostApply.getReasonsForSalaryAdjustments(), + "3", + isDelete,null, ""); + } + // 晋升通过后,变更当前人员岗位职等, + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + String userId = ftbPersonnelsPostApply.getUserId(); + updateUserManagerBoundDTO.setUserId(userId); + updateUserManagerBoundDTO.setOldOrgId(ftbPersonnelsPostApply.getOrgId()); + updateUserManagerBoundDTO.setOldPositionId(ftbPersonnelsPostApply.getPostId()); + updateUserManagerBoundDTO.setOldPositionGradesId(ftbPersonnelsPostApply.getPostRankId()); + updateUserManagerBoundDTO.setOldManagerId(ftbPersonnelsPostApply.getOldImmediateSuperId()); + updateUserManagerBoundDTO.setOldManagerName(ftbPersonnelsPostApply.getOldImmediateSuperName()); + updateUserManagerBoundDTO.setOrgId(ftbPersonnelsPostApply.getOrgCurrentId()); + updateUserManagerBoundDTO.setPositionId(ftbPersonnelsPostApply.getPostPromotionId()); + updateUserManagerBoundDTO.setPositionGradesId(ftbPersonnelsPostApply.getPostRankPromotionId()); + updateUserManagerBoundDTO.setManagerId(ftbPersonnelsPostApply.getImmediateSuperId()); + updateUserManagerBoundDTO.setManagerName(ftbPersonnelsPostApply.getImmediateSuperName()); + BaseUserPrimaryPositionDTO primaryPositionDTO = new BaseUserPrimaryPositionDTO(); + primaryPositionDTO.setUserId(userId); + primaryPositionDTO.setOrganizeId(ftbPersonnelsPostApply.getOrgId()); + primaryPositionDTO.setPositionId(ftbPersonnelsPostApply.getPostId()); + primaryPositionDTO.setPositionGradesId(ftbPersonnelsPostApply.getPostRankId()); + primaryPositionDTO.setManagerId(ftbPersonnelsPostApply.getOldImmediateSuperId()); + ActionResult position = userPrimaryPositionApi.isUserUserPrimaryPosition(primaryPositionDTO); + if (position != null && position.getData()){ + // 更新用户主岗 + BaseUserPrimaryPositionDTO positionDTO = new BaseUserPrimaryPositionDTO(); + positionDTO.setUserId(userId); + positionDTO.setOrganizeId(ftbPersonnelsPostApply.getOrgCurrentId()); + positionDTO.setPositionId(ftbPersonnelsPostApply.getPostPromotionId()); + positionDTO.setPositionGradesId(ftbPersonnelsPostApply.getPostRankPromotionId()); + positionDTO.setManagerId(ftbPersonnelsPostApply.getImmediateSuperId()); + userPrimaryPositionApi.updateUserPrimaryPosition(positionDTO); + } + // 变更角色 + boolean flag = ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId()); + asyncRole(ftbPersonnelsPostApply.getUserId(),flag); + Boolean b1 = userApi.UserManagerBoundChange(Collections.singletonList(updateUserManagerBoundDTO)); + if (b1 == null || !b1){ + throw new RuntimeException("审核变更岗位失败!"); + } + StaffPromotionDto cover = StaffPromotionDto.cover(ftbPersonnelsPostApply,staffHomeDto.getActualStartDate()); + rosterService.innerPromotion(ftbPersonnelsPostApply.getUserId(),cover, ftbPersonnelsPostApply.getPostPromotionPay()); + // 审核通过考情组变更 + if (StringUtils.isNotEmpty(ftbPersonnelsPostApply.getAttendanceGroup())){ + changeTheExaminationGroup(ftbPersonnelsPostApply.getAttendanceGroup(),ftbPersonnelsPostApply.getUserId()); + } + rosterService.logout(userInfo.getTenantId(),ftbPersonnelsPostApply.getUserId()); + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(createDto.getFiles()) + .businessTypeID(postApplyId) + .type(FileEventDTO.FileType.JOB_PROMOTION) + .build())); + return resultStr; + } + /** + * 同步用户角色 + * + * @param userId + * @param flag 标识是否跨组织 + */ + private void asyncRole(String userId, boolean flag) { + // 更改用户角色逻辑 + // 2.1 取消调整角色字段,oa审批单同理 + // 2.2 若变更组织,组织角色需清除,全局角色不清除。不变更组 + // 织则不清除角色,维持原样 + ActionResult> listActionResult = roleApi.listUsersBound(List.of(userId)); + if (listActionResult == null ) throw new RuntimeException("获取用户角色失败!"); + List data = listActionResult.getData(); + if (CollUtil.isEmpty(data)) return; + // 查询用户绑定角色 + UserBoundRolesVO userBoundRolesVO = data.stream().filter(item -> userId.equals(item.getId())).findFirst().orElse(new UserBoundRolesVO()); + List roleBounds = userBoundRolesVO.getRoleBounds(); + List roleIds; + if (flag){ + roleIds = roleBounds.stream().map(RoleListVO::getId).collect(Collectors.toList()); + }else { + // 清除跨组织的角色 + List targetRoleIds = roleBounds.stream().filter(item -> item.getGlobalMark() != 1).map(RoleListVO::getId).collect(Collectors.toList()); + RemoveUserRolesDTO rolesDTO = new RemoveUserRolesDTO(); + rolesDTO.setTargetRoleIds(targetRoleIds); + rolesDTO.setUserIds(List.of(userId)); + roleApi.removeRoleRelationBatch(rolesDTO); + roleIds = roleBounds.stream().filter(item -> item.getGlobalMark() == 1).map(RoleListVO::getId).collect(Collectors.toList()); + } + // 无角色信息变更 + if (CollUtil.isEmpty(roleIds)) return; + SynUserBoundRoleDTO dto = new SynUserBoundRoleDTO(); + dto.setUserId(userId); + dto.setRoleIds(roleIds); + personnelOrgUtils.sysUserRole(dto); + } + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public ActionResult initiateAPromotionApplicationForOA(FtbPersonnelsApplyCreateDto createDto) { + ActionResult result = new ActionResult<>(); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + String filesWithOa = createDto.getFilesWithOa(); + List files = new ArrayList<>(); + if (StringUtils.isNotEmpty(filesWithOa)){ + files = JSONObject.parseArray(filesWithOa, FtbCultivateOfflineFileDTO.class); + } + FtbPersonnelsPostApply ftbPersonnelsPostApply = FtbPersonnelsApplyCreateDto.coverFtbPersonnelsApplyVO(createDto); + FtbPersonnelsResult result1 = FtbPersonnelsResult.getResult(createDto.getSource(),createDto.getFlag()); + ftbPersonnelsPostApply.setState(result1.state); + ftbPersonnelsPostApply.setSource(result1.source); + if ( createDto.getSource() != null && createDto.getSource() != 0) { + // 权限用户 + List userIds = personnelPerUtils.obtainPersonnelDataPermissions(); + // 当前人不是本人申请,且权限中有当前可以办理人 + if ((ftbPersonnelsPostApply.getSource() != 0) && (CollUtil.isNotEmpty(userIds) && !userIds.contains(ftbPersonnelsPostApply.getUserId()))) { + result.setMsg("当前人办理人不具有该员工办理权限!"); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + } + if (ftbPersonnelsPostApply.getUserId().equals(ftbPersonnelsPostApply.getImmediateSuperId())){ + result.setMsg("当前申请直属主管不能是办理人!"); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + //数据构建 + dataBuilding(ftbPersonnelsPostApply); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsPostApply::getUserId, ftbPersonnelsPostApply.getUserId()); + List list = new ArrayList<>(); + list.add(0); + list.add(1); + wrapper.in(FtbPersonnelsPostApply::getState, list); + FtbPersonnelsPostApply postApply = baseMapper.selectOne(wrapper); + if (ObjectUtil.isNotNull(postApply)) { + result.setMsg("您上次提交的晋升申请还在审核中,请等待审核完成再提交!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + + // 校验直属主管是否为试岗员工 + if (StringUtils.isNotEmpty(ftbPersonnelsPostApply.getImmediateSuperId())){ + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbPersonnelsStaffRoster::getUserId,ftbPersonnelsPostApply.getImmediateSuperId()); + FtbPersonnelsStaffRoster one = rosterService.getOne(lambdaed); + if (one.getWorkerStatus().equals("306")){ + result.setMsg("试岗员工:"+one.getName()+"不能作为直属主管!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + } + List positionGradesInfoBoundVOS = + getPositionGradesInfoBoundVOS(ftbPersonnelsPostApply.getUserId(), ftbPersonnelsPostApply.getOrgId()); + if (ObjectUtil.isEmpty(positionGradesInfoBoundVOS)) { + result.setMsg("当前用户未分配岗位,无法进行申请!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + Set collect = positionGradesInfoBoundVOS.stream().map(FtbPositionGradesInfoBoundVO::getPositionGradesId).collect(Collectors.toSet()); + if (!collect.contains(ftbPersonnelsPostApply.getPostRankId())) { + result.setMsg("当前申请变更岗位,在个人岗位中不存在请重新选择进行重试!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + if (StringUtils.isNotEmpty(ftbPersonnelsPostApply.getPromotionReason()) && ftbPersonnelsPostApply.getPromotionReason().length() > 500) { + result.setMsg("晋升原因过长,不能超过500个字,请修改后重试!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + if ( (ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId())) && + (ftbPersonnelsPostApply.getPostId().equals(ftbPersonnelsPostApply.getPostPromotionId())) + && (ftbPersonnelsPostApply.getPostRankId() + .equals(ftbPersonnelsPostApply.getPostRankPromotionId()))){ + result.setMsg("晋升组织岗位职等不能与当前组织岗位职等一致,请重新选择进行重试!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + + List userList = personnelOrgUtils.getUserOrgBoundInfo(ftbPersonnelsPostApply.getUserId()); + if (CollUtil.isNotEmpty(userList) && userList.size() > 1){ + // 员工所属组织 + for (WorkerGroupDataDto dataDto : userList) { + boolean orgBoolean = dataDto.getAffiliatedOrg().equals(ftbPersonnelsPostApply.getOrgCurrentId()); + boolean promotionBoolean = dataDto.getAffiliatedPosition().equals(ftbPersonnelsPostApply.getPostPromotionId()); + boolean orgCurrenBoolean = ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId()); + boolean postBoolean = ftbPersonnelsPostApply.getPostId().equals(ftbPersonnelsPostApply.getPostPromotionId()); + // 员工调岗组织岗位 都相同 不能有不同职等 + if ( !(orgCurrenBoolean && postBoolean) && (orgBoolean && promotionBoolean)){ + result.setMsg("您在目标组织中已所属于该岗位,不可重复选择同一组织下相同岗位!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + } + } + // v1.1 添加门店负责人校验 + // 跨组织校验 + if (!ftbPersonnelsPostApply.getOrgId().equals(ftbPersonnelsPostApply.getOrgCurrentId())) { + LambdaQueryWrapper storeEntityLambdaQueryWrapper = Wrappers.lambdaQuery(); + storeEntityLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid, ftbPersonnelsPostApply.getUserId()); + List list1 = storeService.list(storeEntityLambdaQueryWrapper); + if (!list1.isEmpty()) { + String storeNames = list1.stream().map(StoreEntity::getStorename).collect(Collectors.joining(",")); + result.setMsg("当前晋升人员为"+storeNames+"门店负责人,请替换后再进行操作!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + } + // 添加薪资详情 + List salaryStructureList = createDto.getSalaryStructureList(); + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(createDto.getIsAdjustSalary()) ){ + result.setMsg( "目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + }else if (CollUtil.isNotEmpty(salaryStructureList) && "1".equals(createDto.getIsAdjustSalary())) { + ftbPersonnelsPostApply.setSalaryStructureList(JSONObject.toJSONString(salaryStructureList)); + } + ftbPersonnelsPostApply.setVersionNum(1); + // 修改 + if (StringUtils.isNotEmpty(createDto.getSpecialBusinessId())){ + // 删除上传的文件重新上传 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateFile::getBusinessId, createDto.getSpecialBusinessId()); + ftbCultivateFileService.remove(updateWrapper); + ftbPersonnelsPostApply.setId(createDto.getSpecialBusinessId()); + baseMapper.updateById(ftbPersonnelsPostApply); + }else { + baseMapper.insert(ftbPersonnelsPostApply); + } + // 创建审批 + String postApplyId = ftbPersonnelsPostApply.getId(); + if (result1.source != 2 ){ + // 同步id + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.eq(FtbPersonnelsPostApply::getId,postApplyId); + lambdaUpdate.set(FtbPersonnelsPostApply::getTaskInfoId, createDto.getTaskId()); + baseMapper.update(null,lambdaUpdate); + } + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(files) + .businessTypeID(postApplyId) + .type(FileEventDTO.FileType.JOB_PROMOTION) + .build())); + return result; + } + + private void dataBuilding(FtbPersonnelsPostApply ftbPersonnelsPostApply) { + // 晋升前 + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(ftbPersonnelsPostApply.getOrgId()); + if (ObjectUtil.isNotEmpty(organizeEntity))ftbPersonnelsPostApply.setOrgName(organizeEntity.getFullName()); + PositionEntity positionEntity = personnelOrgUtils.queryPosition(ftbPersonnelsPostApply.getPostId()); + if (ObjectUtil.isNotEmpty(positionEntity)) ftbPersonnelsPostApply.setPostName(positionEntity.getFullName()); + PositionGradesInfoVO gradesInfoVO = personnelOrgUtils.queryRank(ftbPersonnelsPostApply.getPostRankId()); + if (ObjectUtil.isNotEmpty(gradesInfoVO))ftbPersonnelsPostApply.setPostRankName(gradesInfoVO.getFullName()); + // 晋升后 + OrganizeEntity organizeEntity1 = personnelOrgUtils.queryOrganizeInfo(ftbPersonnelsPostApply.getOrgCurrentId()); + if (ObjectUtil.isNotEmpty(organizeEntity1))ftbPersonnelsPostApply.setOrgCurrentName(organizeEntity1.getFullName()); + PositionEntity positionEntity1 = personnelOrgUtils.queryPosition(ftbPersonnelsPostApply.getPostPromotionId()); + if (ObjectUtil.isNotEmpty(positionEntity1)) ftbPersonnelsPostApply.setPostPromotionName(positionEntity1.getFullName()); + PositionGradesInfoVO positionGradesInfoVO = personnelOrgUtils.queryRank(ftbPersonnelsPostApply.getPostRankPromotionId()); + if (ObjectUtil.isNotEmpty(positionGradesInfoVO))ftbPersonnelsPostApply.setPostRankPromotionName(positionGradesInfoVO.getFullName()); + if(StringUtils.isNotEmpty(ftbPersonnelsPostApply.getImmediateSuperId())){ + UserEntity userEntity = personnelOrgUtils.queryUserInfo(ftbPersonnelsPostApply.getImmediateSuperId()); + if (ObjectUtil.isNotEmpty(userEntity))ftbPersonnelsPostApply.setImmediateSuperName(userEntity.getRealName()); + } + StaffHomeDto staffHomeDto = rosterService.queryWorkerHomeDetail(ftbPersonnelsPostApply.getUserId()); + // 工号 + if (ObjectUtil.isNotEmpty(staffHomeDto)){ + ftbPersonnelsPostApply.setJobNumber(staffHomeDto.getWorkerNo()); + ftbPersonnelsPostApply.setSystemWokerId(staffHomeDto.getSystemWokerId()); + ftbPersonnelsPostApply.setCurrentPay(staffHomeDto.getCurrSalary()); + } + + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public ActionResult auditPromotionPostApplicationWithOA(FtbPersonnelsSalaryAuditDto dto) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPostApply::getTaskInfoId,dto.getTaskId()); + FtbPersonnelsPostApply postApply = baseMapper.selectOne(queryWrapper); + if (ObjectUtil.isEmpty(postApply)){ + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "需要审批的数据不存在,或已经进行审批,请重新提交处理!"); + return result; + } + // 校验岗位是否存在 + // 选择通过需要校验组织是否存在 + String businessId = postApply.getId(); + if ("1".equals(dto.getFlag())){ + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(postApply.getOrgCurrentId()); + checkExistByPositionDTO.setPositionId(postApply.getPostPromotionId()); + checkExistByPositionDTO.setPositionGradesId(postApply.getPostRankPromotionId()); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "该员工晋升后目标岗位不存在,无法继续审批;请选择“不通过”结束审批流程!"); + return result; + } + } + if ("1".equals(dto.getFlag())){ + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(postApply.getOrgId()); + checkExistByPositionDTO.setPositionId(postApply.getPostId()); + checkExistByPositionDTO.setPositionGradesId(postApply.getPostRankId()); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "该员工的岗位在提交晋升审批后发生了变更,无法继续审批;请选择“不通过”结束审批流程!"); + return result; + } + } + // 通过oa传值进行数据核对 + FtbPersonnelsAuditTaskEnum code = dto.getFlag().equals("0") ? FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED : + FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsPostApply::getState, code.getCode()); + if (ObjectUtil.isNotEmpty(dto.getSalary()) && dto.getSalary().compareTo(BigDecimal.ZERO) > 0){ + wrapper.set(FtbPersonnelsPostApply::getPostPromotionPay,dto.getSalary()); + } + if (ObjectUtil.isNotEmpty(dto.getDoesTheSalaryComplyWith())){ + wrapper.set(FtbPersonnelsPostApply::getPayCompliance,dto.getDoesTheSalaryComplyWith()); + } + if (ObjectUtil.isNotEmpty(dto.getPayrollEffectiveDate())){ + wrapper.set(FtbPersonnelsPostApply::getPayrollEffectiveDate,dto.getPayrollEffectiveDate()); + } + + if (StringUtils.isNotEmpty(dto.getSalaryStructure())){ + wrapper.set(FtbPersonnelsPostApply::getSalaryStructure,dto.getSalaryStructure()); + } + if (StringUtils.isNotEmpty(dto.getReasonsForSalaryAdjustments())){ + wrapper.set(FtbPersonnelsPostApply::getReasonsForSalaryAdjustments,dto.getReasonsForSalaryAdjustments()); + } + List salaryStructureList1 = dto.getSalaryStructureList(); + if (ObjectUtil.isNotEmpty(dto.getIsAdjustSalary())){ + if (CollUtil.isEmpty(salaryStructureList1) && "1".equals(dto.getIsAdjustSalary()) ){ + result.setMsg( "目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return result; + } + wrapper.set(FtbPersonnelsPostApply::getIsAdjustSalary,dto.getIsAdjustSalary()); + } + if(CollUtil.isNotEmpty(salaryStructureList1)){ + wrapper.set(FtbPersonnelsPostApply::getSalaryStructureList,JSONObject.toJSONString(salaryStructureList1)); + } + wrapper.set(SuperBaseEntity.SuperCUBaseEntity::getLastModifyTime,new Date()); + wrapper.eq(FtbPersonnelsPostApply::getId, businessId); + baseMapper.update(null,wrapper); + if (FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode().equals(code.getCode())){ + // 薪资变更 + FtbPersonnelsPostApply thiPostApply = baseMapper.selectById(businessId); + if ("1".equals(thiPostApply.getIsAdjustSalary())){ + String salaryStructureList = thiPostApply.getSalaryStructureList(); + List salaryInfos = JSONObject.parseArray(salaryStructureList, FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + userInfoWithSalary.setRankName(thiPostApply.getPostRankPromotionName()); + userInfoWithSalary.setRankId(thiPostApply.getPostRankPromotionId()); + userInfoWithSalary.setFOrgId(thiPostApply.getOrgCurrentId()); + userInfoWithSalary.setFOrgName(thiPostApply.getOrgCurrentName()); + userInfoWithSalary.setPostId(thiPostApply.getPostPromotionId()); + userInfoWithSalary.setPostName(thiPostApply.getPostPromotionName()); + salaryService.saveTheChangePayInformation(salaryInfos, + thiPostApply.getUserId(), + thiPostApply.getPayrollEffectiveDate(), + userInfoWithSalary, + "1", + thiPostApply.getReasonsForSalaryAdjustments(), + "3", 0,null, ""); + } + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsPostApply::getId, businessId); + FtbPersonnelsPostApply promotionPostApply = baseMapper.selectOne(lambdaQuery); + // 当前岗位职等 + String postRankId = promotionPostApply.getPostRankId(); + // 晋升岗位职等 + String postRankPromotionId = promotionPostApply.getPostRankPromotionId(); + // 变更用户id + String userId1 = promotionPostApply.getUserId(); + String orgId = postApply.getOrgId(); + String orgCurrentId = postApply.getOrgCurrentId(); + // 晋升通过后,变更当前人员岗位职等, + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + String userId = promotionPostApply.getUserId(); + updateUserManagerBoundDTO.setUserId(userId); + updateUserManagerBoundDTO.setOldOrgId(orgId); + updateUserManagerBoundDTO.setOldPositionId(promotionPostApply.getPostId()); + updateUserManagerBoundDTO.setOldPositionGradesId(postRankId); + updateUserManagerBoundDTO.setOldManagerId(promotionPostApply.getOldImmediateSuperId()); + updateUserManagerBoundDTO.setOldManagerName(promotionPostApply.getOldImmediateSuperName()); + updateUserManagerBoundDTO.setOrgId(orgCurrentId); + updateUserManagerBoundDTO.setPositionId(promotionPostApply.getPostPromotionId()); + updateUserManagerBoundDTO.setPositionGradesId(postRankPromotionId); + updateUserManagerBoundDTO.setManagerId(promotionPostApply.getImmediateSuperId()); + updateUserManagerBoundDTO.setManagerName(promotionPostApply.getImmediateSuperName()); + BaseUserPrimaryPositionDTO primaryPositionDTO = new BaseUserPrimaryPositionDTO(); + primaryPositionDTO.setUserId(userId); + primaryPositionDTO.setOrganizeId(promotionPostApply.getOrgId()); + primaryPositionDTO.setPositionId(promotionPostApply.getPostId()); + primaryPositionDTO.setPositionGradesId(promotionPostApply.getPostRankId()); + primaryPositionDTO.setManagerId(promotionPostApply.getOldImmediateSuperId()); + ActionResult position = userPrimaryPositionApi.isUserUserPrimaryPosition(primaryPositionDTO); + if (position != null && position.getData()){ + // 更新用户主岗 + BaseUserPrimaryPositionDTO positionDTO = new BaseUserPrimaryPositionDTO(); + positionDTO.setUserId(userId); + positionDTO.setOrganizeId(promotionPostApply.getOrgCurrentId()); + positionDTO.setPositionId(promotionPostApply.getPostPromotionId()); + positionDTO.setPositionGradesId(promotionPostApply.getPostRankPromotionId()); + positionDTO.setManagerId(promotionPostApply.getImmediateSuperId()); + userPrimaryPositionApi.updateUserPrimaryPosition(positionDTO); + } + // 变更角色 + boolean flag = promotionPostApply.getOrgId().equals(promotionPostApply.getOrgCurrentId()); + asyncRole(userId,flag); + Boolean b1 = userApi.UserManagerBoundChange(Collections.singletonList(updateUserManagerBoundDTO)); + if (b1 == null || !b1){ + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "审核变更岗位失败!"); + return result; + } + StaffHomeDto staffHomeDto = rosterService.queryWorkerHomeDetail(userId); + StaffPromotionDto cover = StaffPromotionDto.cover(promotionPostApply, staffHomeDto.getActualStartDate()); + rosterService.innerPromotion(userId1,cover, promotionPostApply.getPostPromotionPay()); + if (StringUtils.isNotEmpty(promotionPostApply.getAttendanceGroup())) { + changeTheExaminationGroup(promotionPostApply.getAttendanceGroup(), userId1); + } + } + return result; + } + + @Override + public FtbPersonnelsApplyWithPerVO viewPromotionApplications(String id, String appFlag) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsPostApply::getId, id); + FtbPersonnelsPostApply postApply = baseMapper.selectOne(wrapper); + FtbPersonnelsApplyWithPerVO withPerVO = FtbPersonnelsApplyWithPerVO.cultivatePromotionPostApplyWithPerVO(postApply); + LambdaQueryWrapper staffRosterLambdaQueryWrapper = Wrappers.lambdaQuery(); + staffRosterLambdaQueryWrapper.eq(FtbPersonnelsStaffRoster::getUserId,withPerVO.getUserId()); + FtbPersonnelsStaffRoster one = rosterService.getOne(staffRosterLambdaQueryWrapper); + withPerVO.setUserName(one.getName()); + withPerVO.setSystemWokerId(one.getSystemWokerId()); + if (StringUtils.isNotEmpty(withPerVO.getAttendanceGroup())){ + AttendanceGroup attendanceGroup = attendanceGroupApi.queryTheNameOfTheAttendanceGroup(withPerVO.getAttendanceGroup()); + if (attendanceGroup != null) withPerVO.setAttendanceGroupName(attendanceGroup.getGroupName()); + } + // 查询当前申请时上传的文件信息 + LambdaQueryWrapper wrapperFile = Wrappers.lambdaQuery(); + wrapperFile.eq(FtbCultivateFile::getBusinessId, postApply.getId()); + LambdaQueryChainWrapper fileLambdaQueryChainWrapper = ftbCultivateFileService.lambdaQuery(); + fileLambdaQueryChainWrapper.eq(FtbCultivateFile::getBusinessId, postApply.getId()); + fileLambdaQueryChainWrapper.eq(FtbCultivateFile::getType,FileEventDTO.FileType.JOB_PROMOTION.getType()); + List fileLists = ftbCultivateFileService.list(wrapperFile); + List fileVOS = fileLists.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + withPerVO.setFiles(fileVOS); + FtbPersonnelsAuditInfoVO auditInfoVO = subConfigService.queryAuditSubConfig(withPerVO.getOrgId(),FtbPersonnelsCofigEnum.PROMOTION_APPROVAL_CONFIGURATION); + withPerVO.setAuditInfo(auditInfoVO); + if (appFlag.equals("1")){ + return withPerVO; + } + String userId = withPerVO.getUserId(); + // 考试成绩列表 + QueryCultivateExamReq examReq = new QueryCultivateExamReq(); + examReq.setUserId(userId); + examReq.setPageSize(-1); + PageInfo listVoPageInfo = ftbCultivateExamUserService.queryExamList(examReq); + withPerVO.setAListOfTestScores(listVoPageInfo.getList().stream().map(UserExamDetailVo::covert).collect(Collectors.toList())); + // 实操鉴定列表 + List userIdentifyInfoApi = cultivateIdentifyApplyService.getUserIdentifyInfoWithUserId(userId); + withPerVO.setListOfPracticalQualifications(userIdentifyInfoApi); + // 已学习课程列表 + // 当前申请人id + List promotionChannelLearnCourseVOS = ftbCultivateCourseService.promotionPathwayCourses(userId); + withPerVO.setHaveStudyCourseLists(promotionChannelLearnCourseVOS); + + return withPerVO; + } + + @Override + @Transactional(rollbackFor = {Exception.class,RuntimeException.class}) + public void auditPromotionPostApplication(FtbPersonnelsSalaryAuditDto dto) { + String businessId = dto.getBusinessId(); + FtbPersonnelsPostApply postApply = baseMapper.selectById(businessId); + // 校验岗位是否存在 + // 选择通过需要校验组织是否存在 + if ("1".equals(dto.getFlag()))verifyTo(postApply); + if ("1".equals(dto.getFlag()))verifyFrom(postApply); + // 获取审核节点 + FtbPersonnelsAuditTaskEnum code = auditRunTaskService.performReview(dto); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsPostApply::getState, code.getCode()); + if (ObjectUtil.isNotEmpty(dto.getSalary()) && dto.getSalary().compareTo(BigDecimal.ZERO) > 0){ + wrapper.set(FtbPersonnelsPostApply::getPostPromotionPay,dto.getSalary()); + } + if (ObjectUtil.isNotEmpty(dto.getDoesTheSalaryComplyWith())){ + wrapper.set(FtbPersonnelsPostApply::getPayCompliance,dto.getDoesTheSalaryComplyWith()); + } + if (ObjectUtil.isNotEmpty(dto.getPayrollEffectiveDate())){ + wrapper.set(FtbPersonnelsPostApply::getPayrollEffectiveDate,dto.getPayrollEffectiveDate()); + } + + if (StringUtils.isNotEmpty(dto.getSalaryStructure())){ + wrapper.set(FtbPersonnelsPostApply::getSalaryStructure,dto.getSalaryStructure()); + } + List salaryStructureList1 = dto.getSalaryStructureList(); + if (ObjectUtil.isNotEmpty(dto.getIsAdjustSalary())){ + if (CollUtil.isEmpty(salaryStructureList1) && "1".equals(dto.getIsAdjustSalary()) ){ + throw new RuntimeException("目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + } + wrapper.set(FtbPersonnelsPostApply::getIsAdjustSalary,dto.getIsAdjustSalary()); + } + if(CollUtil.isNotEmpty(salaryStructureList1)){ + wrapper.set(FtbPersonnelsPostApply::getSalaryStructureList,JSONObject.toJSONString(salaryStructureList1)); + } + wrapper.set(SuperBaseEntity.SuperCUBaseEntity::getLastModifyTime,new Date()); + wrapper.eq(FtbPersonnelsPostApply::getId, businessId); + baseMapper.update(null,wrapper); + if (FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode().equals(code.getCode())){ + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsPostApply::getId, businessId); + FtbPersonnelsPostApply promotionPostApply = baseMapper.selectOne(lambdaQuery); + // 当前岗位职等 + String postRankId = promotionPostApply.getPostRankId(); + // 晋升岗位职等 + String postRankPromotionId = promotionPostApply.getPostRankPromotionId(); + // 变更用户id + String userId1 = promotionPostApply.getUserId(); + String orgId = postApply.getOrgId(); + String orgCurrentId = postApply.getOrgCurrentId(); + // 晋升通过后,变更当前人员岗位职等, + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + String userId = promotionPostApply.getUserId(); + updateUserManagerBoundDTO.setUserId(userId); + updateUserManagerBoundDTO.setOldOrgId(orgId); + updateUserManagerBoundDTO.setOldPositionId(promotionPostApply.getPostId()); + updateUserManagerBoundDTO.setOldPositionGradesId(postRankId); + updateUserManagerBoundDTO.setOldManagerId(promotionPostApply.getOldImmediateSuperId()); + updateUserManagerBoundDTO.setOldManagerName(promotionPostApply.getOldImmediateSuperName()); + updateUserManagerBoundDTO.setOrgId(orgCurrentId); + updateUserManagerBoundDTO.setPositionId(promotionPostApply.getPostPromotionId()); + updateUserManagerBoundDTO.setPositionGradesId(postRankPromotionId); + updateUserManagerBoundDTO.setManagerId(promotionPostApply.getImmediateSuperId()); + updateUserManagerBoundDTO.setManagerName(promotionPostApply.getImmediateSuperName()); + // 角色 + String roleId = promotionPostApply.getRoleId(); + SynUserBoundRoleDTO synUserBoundRoleDTO = new SynUserBoundRoleDTO(); + synUserBoundRoleDTO.setUserId(userId); + if (StringUtils.isNotEmpty(roleId)){ + String[] split = roleId.split(","); + synUserBoundRoleDTO.setRoleIds(List.of(split)); + } + Boolean b = userApi.synchronousUserBoundRole(synUserBoundRoleDTO); + if (b == null || !b){ + throw new RuntimeException("审核角色变更失败变更失败!"); + } + Boolean b1 = userApi.UserManagerBoundChange(Collections.singletonList(updateUserManagerBoundDTO)); + if (b1 == null || !b1){ + throw new RuntimeException("审核变更岗位失败!"); + } + StaffHomeDto staffHomeDto = rosterService.queryWorkerHomeDetail(userId); + StaffPromotionDto cover = StaffPromotionDto.cover(promotionPostApply, staffHomeDto.getActualStartDate()); + rosterService.innerPromotion(userId1,cover, promotionPostApply.getPostPromotionPay()); + if (StringUtils.isNotEmpty(promotionPostApply.getAttendanceGroup())) { + changeTheExaminationGroup(promotionPostApply.getAttendanceGroup(), userId1); + } + // 薪资变更 + FtbPersonnelsPostApply thiPostApply = baseMapper.selectById(businessId); + if ("1".equals(thiPostApply.getIsAdjustSalary())){ + String salaryStructureList = thiPostApply.getSalaryStructureList(); + List salaryInfos = JSONObject.parseArray(salaryStructureList, FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + userInfoWithSalary.setRankName(postApply.getPostRankPromotionName()); + userInfoWithSalary.setRankId(postApply.getPostRankPromotionId()); + userInfoWithSalary.setFOrgId(postApply.getOrgCurrentId()); + userInfoWithSalary.setFOrgName(postApply.getOrgCurrentName()); + userInfoWithSalary.setPostId(postApply.getPostPromotionId()); + userInfoWithSalary.setPostName(postApply.getPostPromotionName()); + salaryService.saveTheChangePayInformation(salaryInfos, + thiPostApply.getUserId(), + thiPostApply.getPayrollEffectiveDate(), + userInfoWithSalary, + "1", + "晋升调薪", + "3", 0,null, ""); + } + } + } + + /** + * 校验岗位是否存在 + * @param postApply + */ + private void verifyTo(FtbPersonnelsPostApply postApply) { + + } + /** + * 校验岗位是否存在 + * @param postApply + */ + private void verifyFrom(FtbPersonnelsPostApply postApply) { + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(postApply.getOrgId()); + checkExistByPositionDTO.setPositionId(postApply.getPostId()); + checkExistByPositionDTO.setPositionGradesId(postApply.getPostRankId()); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + throw new RuntimeException("该员工的岗位在提交晋升审批后发生了变更,无法继续审批;请选择“不通过”结束审批流程!"); + } + } + + /** + * 变更考情组 + * @param groupId + * @param userId1 + */ + private void changeTheExaminationGroup(String groupId, String userId1) { + // 变更考情组 + GroupUpdateByUserDTO groupUpdateByUserDTO = new GroupUpdateByUserDTO(); + groupUpdateByUserDTO.setUserIds(Collections.singletonList(userId1)); + groupUpdateByUserDTO.setType(4); + groupUpdateByUserDTO.setTenantId(UserProvider.getUser().getTenantId()); + groupUpdateByUserDTO.setToGroupId(groupId); + attendanceUserApi.groupUpdateByPersonnel(groupUpdateByUserDTO); + } + + + /** + * 过滤当前组织用户 + * @param userId + * @param orgId + * @return + */ + @NotNull + public List getPositionGradesInfoBoundVOS(String userId, String orgId) { + List orgPositionInfoList = positionApi.getOrgPositionInfoList(userId); + List list = new ArrayList<>(); + if (CollUtil.isNotEmpty(orgPositionInfoList)){ + orgPositionInfoList.forEach(item->{ + FtbPositionGradesInfoBoundVO post = new FtbPositionGradesInfoBoundVO(); + if (CollUtil.isEmpty(item.getOrganizeIds())){ + return; + } + List organizeIds = item.getOrganizeIds(); + List organizeNames = item.getOrganizeNames(); + for (int i=0; i < organizeIds.size();i++) { + if (organizeIds.get(i).equals(orgId)) { + post.setOrganizeId(organizeIds.get(i)); + post.setOrganizeName(organizeNames.get(i)); + BeanUtil.copyProperties(item,post); + list.add(post); + } + } + }); + } + return list; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPromiseConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPromiseConfigServiceImpl.java new file mode 100644 index 0000000..842296b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsPromiseConfigServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsPromiseConfig; +import jnpf.personnels.mapper.FtbPersonnelsPromiseConfigMapper; +import jnpf.personnels.service.FtbPersonnelsPromiseConfigService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2024/10/8 +* +*/ +@Service +public class FtbPersonnelsPromiseConfigServiceImpl extends ServiceImpl implements FtbPersonnelsPromiseConfigService { + + @Resource + private FtbPersonnelsPromiseConfigMapper ftbPersonnelsPromiseConfigMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRecruitmentChannelsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRecruitmentChannelsServiceImpl.java new file mode 100644 index 0000000..3ce6be5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRecruitmentChannelsServiceImpl.java @@ -0,0 +1,39 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.recruitmentchannels.PersonnelsRecruitmentChannelsAddDTO; +import jnpf.model.personnels.po.FtbPersonnelsRecruitmentChannels; +import jnpf.personnels.mapper.FtbPersonnelsRecruitmentChannelsMapper; +import jnpf.personnels.service.FtbPersonnelsRecruitmentChannelsService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class FtbPersonnelsRecruitmentChannelsServiceImpl extends ServiceImpl implements FtbPersonnelsRecruitmentChannelsService { + + @Override + @Transactional + public void add(PersonnelsRecruitmentChannelsAddDTO personnelsRecruitmentChannelsAddDTO) { + List ftbPersonnelsRecruitmentChannels = personnelsRecruitmentChannelsAddDTO.convert(personnelsRecruitmentChannelsAddDTO); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.ne(FtbPersonnelsRecruitmentChannels::getInternalStatus, 0); + baseMapper.delete(queryWrapper); + saveBatch(ftbPersonnelsRecruitmentChannels); + } + + @Override + public List queryRecruitmentChannels() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(SuperBaseEntity.SuperIBaseEntity::getId, + FtbPersonnelsRecruitmentChannels::getRegularName, + FtbPersonnelsRecruitmentChannels::getInternalStatus); + queryWrapper.eq(FtbPersonnelsRecruitmentChannels::getEnableMark, 0); + queryWrapper.orderByAsc(FtbPersonnelsRecruitmentChannels::getInternalStatus); + return baseMapper.selectList(queryWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldOptionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldOptionServiceImpl.java new file mode 100644 index 0000000..31801ed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldOptionServiceImpl.java @@ -0,0 +1,33 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormTypeDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormFieldOptionService; +@Service +public class FtbPersonnelsRegistrationFormFieldOptionServiceImpl extends ServiceImpl implements FtbPersonnelsRegistrationFormFieldOptionService { + + @Override + public List queryOptionsByFieldId(String fieldId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0) + .eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, fieldId) + .orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getCreatorTime); + List optionList = baseMapper.selectList(wrapper); + if(CollectionUtil.isNotEmpty(optionList)) { + return optionList; + } + return new ArrayList<>(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldServiceImpl.java new file mode 100644 index 0000000..0404018 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormFieldServiceImpl.java @@ -0,0 +1,427 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.model.enums.FormFieldType; +import jnpf.model.personnels.dto.staff.field.EditFormFieldDto; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormFieldDto; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormFieldOptionDto; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormTypeDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.req.field.QueryRegistrationFormFieldListReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormFieldOptionReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormFieldReq; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormFieldOptionService; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormFieldService; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormTypeService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsRegistrationFormFieldServiceImpl extends ServiceImpl implements FtbPersonnelsRegistrationFormFieldService { + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionService fieldOptionService; + + @Autowired + private FtbPersonnelsRegistrationFormTypeService formTypeService; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataMapper ftbPersonnelsStaffRegistrationFormDataMapper; + + @Override + public PageInfo getPageList(QueryRegistrationFormFieldListReq req) { + Page queryPage = baseMapper.pagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req); + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + List formTypeDtoList = formTypeService.listAll(null); + if (CollectionUtil.isNotEmpty(formTypeDtoList)) { + Map formTypeMap = formTypeDtoList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormTypeDto::getId, Function.identity())); + for (FtbPersonnelsRegistrationFormFieldDto record : records) { + FtbPersonnelsRegistrationFormTypeDto formTypeDto = formTypeMap.get(record.getFormTypeId()); + if (null != formTypeDto) { + record.setFormTypeName(formTypeDto.getName()); + } + } + } + + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public EditFormFieldDto getInfo(String id) { + FtbPersonnelsRegistrationFormFieldDto dto = queryAndCheckFormField(id); + //查询字段类别名称 + EditFormFieldDto fieldDto = BeanUtil.copyProperties(dto, EditFormFieldDto.class); + //查询类别名称 + FtbPersonnelsRegistrationFormType typeEntity = formTypeService.getById(dto.getFormTypeId()); + if (null != typeEntity) { + fieldDto.setFieldTypeName(typeEntity.getName()); + } + //查询选项 + if (fieldDto.getType().equals(2) || fieldDto.getType().equals(3)) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, dto.getId()) + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0).orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getId); + List optionList = fieldOptionService.list(wrapper); + if (CollectionUtil.isNotEmpty(optionList)) { + List optionDtoList = BeanUtil.copyToList(optionList, FtbPersonnelsRegistrationFormFieldOptionDto.class); + fieldDto.setOptionDtoList(optionDtoList); + } + } + return fieldDto; + } + + private FtbPersonnelsRegistrationFormFieldDto queryAndCheckFormField(String id) { + + FtbPersonnelsRegistrationFormField entity = baseMapper.selectById(id); + if (entity == null) { + throw new RuntimeException("该字段不存在"); + } + if (entity.getEnabledMark().equals(1)) { + throw new RuntimeException("该字段已经删除"); + } + FtbPersonnelsRegistrationFormFieldDto dto = BeanUtil.copyProperties(entity, FtbPersonnelsRegistrationFormFieldDto.class); + return dto; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String insertData(SaveRegistrationFormFieldReq req) { + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormField::getName, req.getName()) + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("字段名称已经存在"); + } + //添加 + FtbPersonnelsRegistrationFormField entity = BeanUtil.copyProperties(req, FtbPersonnelsRegistrationFormField.class); + entity.setIsUpdateManager(0); + entity.setAllowNeedFill(0); + entity.setSystemType(1);//0、系统类型 1、自定义类型 + entity.setIsAllowStop(0); + entity.setIsRegisterFormShow(0); + entity.setSorts(System.currentTimeMillis() / 1000); + // 类型:0、单行文本 1、多行文本 2、单选 3、多选 4、日期 5、附件 6、图片 7、整数 8、小数 + // 单行文本上限20字 多行文本上限500字 附件上限10个单个不超过500m 图片上限10张单个不超过500m + if (req.getType() == 2 || req.getType() == 3) { + entity.setOptionType(1); + } + if (req.getType() == 0) { + entity.setLimits(20L); + } else if (req.getType() == 1) { + entity.setLimits(500L); + } else if (req.getType() == 5 || req.getType() == 6) { + entity.setLimits(10L); + entity.setFileSize(500); + } + baseMapper.insert(entity); + + //如果是单选和多选 就写入选项 + FormFieldType formFieldType = FormFieldType.fromCode(req.getType()); + if (FormFieldType.RADIO_BUTTON == formFieldType || FormFieldType.CHECK_BOX == formFieldType) { + List optionList = req.getOptionList(); + if(CollectionUtil.isEmpty(optionList)){ + throw new RuntimeException("选择类型的字段的选项不能为空"); + } + if (CollectionUtil.isNotEmpty(optionList)) { + List optionEntityList = new ArrayList<>(); + for (SaveRegistrationFormFieldOptionReq option : optionList) { + FtbPersonnelsRegistrationFormFieldOption optionEntity = new FtbPersonnelsRegistrationFormFieldOption(); + optionEntity.setFormFieldId(entity.getId()); + optionEntity.setName(option.getName()); + optionEntity.setEnabledMark(0); + optionEntityList.add(optionEntity); + } + if (CollectionUtil.isNotEmpty(optionEntityList)) { + fieldOptionService.saveBatch(optionEntityList); + } + } + } + + return entity.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateData(SaveRegistrationFormFieldReq req) { + + //检测重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormField::getName, req.getName()) + .ne(FtbPersonnelsRegistrationFormField::getId, req.getId()) + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("字段名称已经存在"); + } + + FtbPersonnelsRegistrationFormFieldDto dto = queryAndCheckFormField(req.getId()); + if (dto.getSystemType().equals(0)) { + throw new RuntimeException("系统字段不能够编辑"); + } + + //修改 + FtbPersonnelsRegistrationFormField entity = BeanUtil.copyProperties(req, FtbPersonnelsRegistrationFormField.class); + entity.setId(req.getId()); + // 类型:0、单行文本 1、多行文本 2、单选 3、多选 4、日期 5、附件 6、图片 7、整数 8、小数 + // 单行文本上限20字 多行文本上限500字 附件上限10个单个不超过500m 图片上限10张单个不超过500m + if (req.getType() == 2 || req.getType() == 3) { + entity.setOptionType(1); + } + if (req.getType() == 0) { + entity.setLimits(20L); + } else if (req.getType() == 1) { + entity.setLimits(500L); + } else if (req.getType() == 5 || req.getType() == 6) { + entity.setLimits(10L); + entity.setFileSize(500); + } + baseMapper.updateById(entity); + + //修改选项 + FormFieldType formFieldType = FormFieldType.fromCode(req.getType()); + if (FormFieldType.RADIO_BUTTON == formFieldType || FormFieldType.CHECK_BOX == formFieldType) { + //查询已经存在的选项 + List exiestOptionList = fieldOptionService.queryOptionsByFieldId(req.getId()); + + List optionList = req.getOptionList(); + if(CollectionUtil.isEmpty(optionList)){ + throw new RuntimeException("选择类型的字段的选项不能为空"); + } + //修改的idlist + Set editSet = new HashSet<>(); + //添加的列表 + List addAndUpdateList = new ArrayList<>(); + for (SaveRegistrationFormFieldOptionReq optionReq : optionList) { + FtbPersonnelsRegistrationFormFieldOption optionEntity = new FtbPersonnelsRegistrationFormFieldOption(); + optionEntity.setName(optionReq.getName()); + optionEntity.setFormFieldId(req.getId()); + if (StringUtils.isNotEmpty(optionReq.getId())) { + optionEntity.setId(optionReq.getId()); + editSet.add(optionReq.getId()); + } + addAndUpdateList.add(optionEntity); + } + //删除已经存在但是没有在请求列表中的 + if (CollectionUtil.isNotEmpty(exiestOptionList)) { + List delList = new ArrayList<>(); + exiestOptionList.forEach(exiestOption -> { + if (!editSet.contains(exiestOption.getId())) { + delList.add(exiestOption.getId()); + } + }); + if (CollectionUtil.isNotEmpty(delList)) { + deleteFieldOptionByIds(delList); + } + + } + //添加修改 + fieldOptionService.saveOrUpdateBatch(addAndUpdateList); + } + //如果字段类型发送变化就清除已有数据 + if(!dto.getType().equals(req.getType())){ + UpdateWrapper formDataWrapper = new UpdateWrapper<>(); + formDataWrapper.lambda().set(FtbPersonnelsStaffRegistrationFormData::getValue, "") + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, dto.getId()); + ftbPersonnelsStaffRegistrationFormDataMapper.update(null,formDataWrapper); + } + } + + private void deleteFieldOptionByIds(List delList) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 1) + .in(FtbPersonnelsRegistrationFormFieldOption::getId, delList); + fieldOptionService.update(wrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String id) { + FtbPersonnelsRegistrationFormFieldDto dto = queryAndCheckFormField(id); + if (dto.getSystemType().equals(0)) { + throw new RuntimeException("系统字段不允许删除"); + } + if (dto.getStatus().equals(0)) { + throw new RuntimeException("该字段启用中,不能删除"); + } + //删除字段 + FtbPersonnelsRegistrationFormField entity = new FtbPersonnelsRegistrationFormField(); + entity.setId(id); + entity.setEnabledMark(1); + baseMapper.updateById(entity); + //删除字段选项 + FormFieldType formFieldType = FormFieldType.fromCode(dto.getType()); + if (FormFieldType.RADIO_BUTTON == formFieldType || FormFieldType.CHECK_BOX == formFieldType) { + deleteFieldOptionByFieldId(id); + } + } + + /** + * 根据字段ID 删除字段选项 + * + * @param fieldId + */ + private void deleteFieldOptionByFieldId(String fieldId) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 1) + .eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, fieldId); + fieldOptionService.update(); + } + + @Override + public void switchStatus(String id, Integer status) { + FtbPersonnelsRegistrationFormFieldDto dto = queryAndCheckFormField(id); + if (dto.getIsAllowStop().equals(1)) { + if (status == 1) { + throw new RuntimeException("该字段不允许被停用"); + } + } + FtbPersonnelsRegistrationFormField entity = new FtbPersonnelsRegistrationFormField(); + entity.setId(id); + entity.setStatus(status); + baseMapper.updateById(entity); + } + + @Override + public void switchNeedFill(String id, Integer status) { + FtbPersonnelsRegistrationFormFieldDto dto = queryAndCheckFormField(id); + if (dto.getAllowNeedFill().equals(1)) { + if (status == 0) { + throw new RuntimeException("该字段不允许切换成选填"); + } + } + FtbPersonnelsRegistrationFormField entity = new FtbPersonnelsRegistrationFormField(); + entity.setId(id); + entity.setIsNeedFill(status); + baseMapper.updateById(entity); + } + + /** + * 查询字段类型下的所有字段数量 + * + * @param typeId 字段类型 + * @return + */ + @Override + public Long queryAllFieldNumForType(String typeId) { + return baseMapper.selectCount(new LambdaQueryWrapper() + .eq(FtbPersonnelsRegistrationFormField::getFormTypeId, typeId) + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0)); + } + + /** + * 查询字段类型下的可用字段数量 + * + * @param typeId 字段类型 + * @return + */ + @Override + public Long queryEnableFieldNumForType(String typeId) { + return baseMapper.selectCount(new LambdaQueryWrapper() + .eq(FtbPersonnelsRegistrationFormField::getFormTypeId, typeId) + .eq(FtbPersonnelsRegistrationFormField::getStatus, 0) + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0)); + } + + @Override + public FtbPersonnelsRegistrationFormField queryOneField(String field) { + return baseMapper.selectById(field); + } + + @Override + @Transactional + public void switchVisible(String id, Integer status) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsRegistrationFormField::getId, id); + updateWrapper.set(FtbPersonnelsRegistrationFormField::getIsVisibleWorker, status); + baseMapper.update(new FtbPersonnelsRegistrationFormField(), updateWrapper); + } + + @Override + public List> queryFieldValue(String field, String workType) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbPersonnelsRegistrationFormField::getName,field); + queryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId,baseMapper.selectOne(query).getId()); + List list = fieldOptionService.list(queryWrapper); + if("员工状态".equals(field)){ + // 试用和正式 + return list.stream().filter(v->v.getName().equals("正式")||v.getName().equals("试用")) + .map(v->{ + Map map = new HashMap<>(); + map.put("name",v.getName()); + map.put("id",v.getId()); + return map; + }).collect(Collectors.toList()); + } + if ("试用期".equals(field) && "303".equals(workType)){ + return list.stream().filter(v->v.getName().equals("无试用期")).map(v->{ + Map map = new HashMap<>(); + map.put("name",v.getName()); + map.put("id",v.getId()); + return map; + }).collect(Collectors.toList()); + } + if ("试用期".equals(field)){ + return list.stream().filter(v->!v.getName().equals("无试用期")).map(v->{ + Map map = new HashMap<>(); + map.put("name",v.getName()); + map.put("id",v.getId()); + return map; + }).collect(Collectors.toList()); + } + return list.stream().map(v->{ + Map map = new HashMap<>(); + map.put("name",v.getName()); + map.put("id",v.getId()); + return map; + }).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void toggleWhetherYouCanModifyYourProfile(String id, Integer status) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsRegistrationFormField::getId, id); + updateWrapper.set(FtbPersonnelsRegistrationFormField::getIsUpdateWorker, status); + baseMapper.update(new FtbPersonnelsRegistrationFormField(), updateWrapper); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormTypeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormTypeServiceImpl.java new file mode 100644 index 0000000..c5face5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegistrationFormTypeServiceImpl.java @@ -0,0 +1,191 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import jnpf.model.personnels.dto.staff.field.FtbPersonnelsRegistrationFormTypeDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; +import jnpf.model.personnels.req.field.QueryRegistrationFormTypeListReq; +import jnpf.model.personnels.req.field.SaveRegistrationFormTypeReq; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormTypeMapper; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormFieldService; +import jnpf.personnels.service.FtbPersonnelsRegistrationFormTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class FtbPersonnelsRegistrationFormTypeServiceImpl extends ServiceImpl implements FtbPersonnelsRegistrationFormTypeService { + + @Autowired + private FtbPersonnelsRegistrationFormFieldService registrationFormFieldService; + + @Override + public PageInfo getPageList(QueryRegistrationFormTypeListReq req) { + + Page queryPage = baseMapper.selectPage(new Page(req.getCurrentPage(), req.getPageSize()), new LambdaQueryWrapper().eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0).orderByAsc(FtbPersonnelsRegistrationFormType::getSorts)); + List records = queryPage.getRecords(); + //转换查询对象 + List list = new ArrayList<>(); + records.forEach(entity -> { + FtbPersonnelsRegistrationFormTypeDto vo = BeanUtil.copyProperties(entity, FtbPersonnelsRegistrationFormTypeDto.class); + //查询所有字段数量 + vo.setAllFieldNum(registrationFormFieldService.queryAllFieldNumForType(vo.getId())); + //查询启用字段数量 + vo.setFieldNum(registrationFormFieldService.queryEnableFieldNumForType(vo.getId())); + list.add(vo); + }); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(list); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public FtbPersonnelsRegistrationFormTypeDto getInfo(String id) { + FtbPersonnelsRegistrationFormType entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("数据不存在"); + } + if (entity.getEnabledMark() == 1) { + throw new RuntimeException("数据已经删除"); + } + return BeanUtil.copyProperties(entity, FtbPersonnelsRegistrationFormTypeDto.class); + } + + @Override + public String insertData(SaveRegistrationFormTypeReq req) { + if(req.getSorts()==null){ + req.setSorts(0L); + } + //检测名称是否重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsRegistrationFormType::getName, req.getName()).eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("类别名称已经存在"); + } + //检测排序重复 + QueryWrapper sortWrapper = new QueryWrapper<>(); + sortWrapper.lambda().eq(FtbPersonnelsRegistrationFormType::getSorts, req.getSorts()).eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0); + Long sortCount = baseMapper.selectCount(sortWrapper); + if (null != sortCount && sortCount > 0) { + throw new RuntimeException("排序已经存在"); + } + //添加 + FtbPersonnelsRegistrationFormType entity = BeanUtil.copyProperties(req, FtbPersonnelsRegistrationFormType.class); + entity.setStatus(0);//0、启用 1、禁用 + entity.setSystemType(1);//0、系统类型 1、自定义类型 + baseMapper.insert(entity); + return entity.getId(); + } + + @Override + public void updateData(SaveRegistrationFormTypeReq req) { + //检测重复 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsRegistrationFormType::getName, req.getName()).ne(FtbPersonnelsRegistrationFormType::getId, req.getId()).eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0); + Long count = baseMapper.selectCount(wrapper); + if (null != count && count > 0) { + throw new RuntimeException("类别名称已经存在"); + } + //检测排序重复 + QueryWrapper sortWrapper = new QueryWrapper<>(); + sortWrapper.lambda().eq(FtbPersonnelsRegistrationFormType::getSorts, req.getSorts()).ne(FtbPersonnelsRegistrationFormType::getId, req.getId()).eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0); + Long sortCount = baseMapper.selectCount(sortWrapper); + if (null != sortCount && sortCount > 0) { + throw new RuntimeException("排序已经存在"); + } + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbPersonnelsRegistrationFormType::getId, req.getId()); + updateWrapper.set(FtbPersonnelsRegistrationFormType::getSorts, req.getSorts()); + updateWrapper.set(FtbPersonnelsRegistrationFormType::getName, req.getName()); + baseMapper.update(new FtbPersonnelsRegistrationFormType(), updateWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String id) { + //检测是否是系统类别,系统类别不能删除 + FtbPersonnelsRegistrationFormTypeDto dto = getInfo(id); + if (dto.getSystemType().equals(0)) { + throw new RuntimeException("系统类别不能删除"); + } + //删除类别 + FtbPersonnelsRegistrationFormType entity = new FtbPersonnelsRegistrationFormType(); + entity.setEnabledMark(1); + entity.setId(id); + baseMapper.updateById(entity); + + //删除字段 + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbPersonnelsRegistrationFormField::getEnabledMark, 1) + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0) + .eq(FtbPersonnelsRegistrationFormField::getFormTypeId, id); + registrationFormFieldService.update(wrapper); + } + + @Override + public void switchStatus(String id, Integer status) { + //检测是否是系统类别 + FtbPersonnelsRegistrationFormTypeDto dto = getInfo(id); + if (dto.getStatus().equals(status)) { + return; + } + + //修改系统状态 启用和禁用 + FtbPersonnelsRegistrationFormType entity = new FtbPersonnelsRegistrationFormType(); + entity.setStatus(status); + entity.setId(id); + baseMapper.updateById(entity); + } + + /** + * 修改排序号 + * + * @param id 主键 + * @param sorts 序号 + */ + @Override + public void updateSorts(String id, Long sorts) { + getInfo(id); + FtbPersonnelsRegistrationFormType entity = new FtbPersonnelsRegistrationFormType(); + entity.setSorts(sorts); + entity.setId(id); + baseMapper.selectById(entity); + } + + /** + * 查询所有类别 + * + * @return + */ + @Override + public List listAll(Integer status) { + // 排除合同类型,人事v1.2 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0) + .eq(status != null, FtbPersonnelsRegistrationFormType::getStatus, status) + .ne(FtbPersonnelsRegistrationFormType::getId, "5") + .orderByAsc(FtbPersonnelsRegistrationFormType::getSorts); + List formTypeList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(formTypeList)) { + return BeanUtil.copyToList(formTypeList, FtbPersonnelsRegistrationFormTypeDto.class); + } + return CollectionUtil.newArrayList(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegularManagementServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegularManagementServiceImpl.java new file mode 100644 index 0000000..0f1249e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRegularManagementServiceImpl.java @@ -0,0 +1,1137 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fantaibao.permission.handling.PermissionHandling; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.impl.FtbCultivateFileService; +import jnpf.engine.FlowTaskApi; +import jnpf.engine.entity.FlowTaskEntity; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.exception.DataException; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineFileDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.im.UserSuccessDTO; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsSalaryAuditDto; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.staff.roster.StaffHomeDto; +import jnpf.model.personnels.dto.staff.roster.StaffRegularDto; +import jnpf.model.personnels.po.FtbPersonnelsRegularManagement; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularInfoVO; +import jnpf.model.personnels.vo.regular.FtbPersonnelsRegularManagementVO; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditInfoVO; +import jnpf.model.personnels.vo.task.FtbPersonnelsResult; +import jnpf.model.vo.UserSalaryHistoryVo; +import jnpf.permission.V2GradesApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.grades.QueryGradeListDTO; +import jnpf.permission.dto.v2.user.QueryUserBoundDTO; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserRelationBaseVO; +import jnpf.personnels.mapper.FtbPersonnelsRegularManagementMapper; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import jnpf.util.im.ImMessageNoticeUtils; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsRegularManagementServiceImpl extends ServiceImpl implements FtbPersonnelsRegularManagementService { + + @Resource + private FtbPersonnelsAuditRunTaskService runTaskService; + + @Resource + private FtbCultivateFileService ftbCultivateFileService; + + @Resource + private FtbPersonnelsAuditSubConfigService subConfigService; + + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + V2UserApi v2UserApi; + + @Resource + public PersonnelPerUtils personnelPerUtils; + + @Resource + private AttendanceGroupApi attendanceGroupApi; + + @Autowired + private V2PositionApi v2PositionApi; + + @Autowired + private V2GradesApi v2GradesApi; + + @Autowired + ImMessageNoticeUtils noticeUtils; + + @Autowired + FtbPersonnelsSalaryService salaryService; + + @Autowired + PersonnelOrgUtils personnelOrgUtils; + + @Autowired + QuerySalaryApi querySalaryApi; + + @Autowired + FtbPersonnelsStaffGrowthLogService logService; + + @Resource + private PermissionHandling permissionHandling; + + @Resource + PermissionsUtils permissionsUtils; + + @Resource + FlowTaskApi flowTaskApi; + + @Resource + private FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentManagementService; + @Override + @Transactional + public void cancellationOfApplicationForRectification(String id, String taskId) { + if (StringUtils.isEmpty(id) && StringUtils.isEmpty(taskId)) { + throw new RuntimeException("参数错误!"); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StringUtils.isNotEmpty(id),FtbPersonnelsRegularManagement::getId,id); + queryWrapper.eq(StringUtils.isNotEmpty(taskId),FtbPersonnelsRegularManagement::getTaskInfoId,taskId); + FtbPersonnelsRegularManagement vo = baseMapper.selectOne(queryWrapper); + Integer transferStatus = vo.getTransferStatus(); + if (!List.of(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode(), + FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode()).contains(transferStatus)) { + throw new RuntimeException("该审批无法撤销!"); + } + // 时间先清除 + logService.revocationGrowthLog(vo.getUserId(),vo.getCreatorTime()); + // 撤销 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + // 转正金额设置为空 + wrapper.set(FtbPersonnelsRegularManagement::getTransferStatus, FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + // 手动办理将已有的taskId设置为空 + if (FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode().equals(transferStatus)) wrapper.set(FtbPersonnelsRegularManagement::getTaskInfoId, null); + wrapper.eq(StringUtils.isNotEmpty(id),FtbPersonnelsRegularManagement::getId,id); + wrapper.eq(StringUtils.isNotEmpty(taskId),FtbPersonnelsRegularManagement::getTaskInfoId,taskId); + baseMapper.update(null, wrapper); + // 同步撤销状态到花名册 + staffRosterService.cancelRegular(vo.getUserId()); + // 如果使用了薪酬模版 需要 进行数据撤销回滚 + if ("1".equals(vo.getIsAdjustSalary())) { + // 撤销成功 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String format = sdf.format(vo.getPayrollEffectiveDate()); + salaryService.voidSalary(vo.getUserId(),format, vo.getSalaryRecordId()); + } + + } + + @Override + @Transactional + public void regularizationApproval(FtbPersonnelsSalaryAuditDto auditDto) { + // 判断该员工的岗位是否在提交转正审批后发生了变更(该员工所属组织的岗位模板发生变化导致该员工的岗位发生同步变化) ; + // 以及判断转正后目标岗位是否不存在 + FtbPersonnelsRegularManagement regularManagement = baseMapper.selectById(auditDto.getBusinessId()); + // 选择通过需要校验组织是否存在 + if ("1".equals(auditDto.getFlag()))verify(regularManagement); + FtbPersonnelsAuditTaskEnum performReview = runTaskService.performReview(auditDto); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsRegularManagement::getTransferStatus, performReview.getCode()); + wrapper.eq(FtbPersonnelsRegularManagement::getId, auditDto.getBusinessId()); + if (StringUtils.isNotEmpty(auditDto.getIsAdjustSalary())){ + wrapper.set(FtbPersonnelsRegularManagement::getIsAdjustSalary,auditDto.getIsAdjustSalary()); + } + if (StringUtils.isNotEmpty(auditDto.getSalaryStructure())){ + wrapper.set(FtbPersonnelsRegularManagement::getSalaryStructure,auditDto.getSalaryStructure()); + } + if ( ObjectUtil.isNotEmpty(auditDto.getSalary())){ + wrapper.set(FtbPersonnelsRegularManagement::getRegularSalary,auditDto.getSalary()); + } + if (ObjectUtil.isNotEmpty(auditDto.getPayrollEffectiveDate())){ + wrapper.set(FtbPersonnelsRegularManagement::getPayrollEffectiveDate,auditDto.getPayrollEffectiveDate()); + } + List salaryStructureList1 = auditDto.getSalaryStructureList(); + if(CollUtil.isNotEmpty(salaryStructureList1)){ + wrapper.set(FtbPersonnelsRegularManagement::getSalaryStructureList, + JSONObject.toJSONString(salaryStructureList1)); + } + // 薪资员工自己申请的 + if(ObjectUtil.isNotEmpty(auditDto.getIsAdjustSalary())){ + if (CollUtil.isEmpty(salaryStructureList1) && "1".equals(auditDto.getIsAdjustSalary()) ){ + throw new RuntimeException("目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + } + wrapper.set(FtbPersonnelsRegularManagement::getIsAdjustSalary,auditDto.getIsAdjustSalary()); + } + if(ObjectUtil.isNotEmpty(auditDto.getSalaryStructure())){ + wrapper.set(FtbPersonnelsRegularManagement::getSalaryStructure,auditDto.getSalaryStructure()); + } + wrapper.set(StringUtils.isNotEmpty(auditDto.getReasonsForSalaryAdjustments()), + FtbPersonnelsRegularManagement::getReasonsForSalaryAdjustments, + auditDto.getReasonsForSalaryAdjustments()); + baseMapper.update(null, wrapper); + if (FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.equals(performReview)) { + // 转正员工同步 + FtbPersonnelsRegularManagement thisRegularInfo = baseMapper.selectById(auditDto.getBusinessId()); + // 成功通知 + messageNotifications(regularManagement.getUserId(), true); + synchronizePayrollData(thisRegularInfo); + }else if (FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.equals(performReview)){ + // 失败通知 + messageNotifications(regularManagement.getUserId(), false); + } + } + /** + * 校验岗位是否存在 + * @param management + */ + private void verify(FtbPersonnelsRegularManagement management) { + // 校验岗位是否还存在 + QueryUserBoundDTO checkExistByPositionDTO = new QueryUserBoundDTO(); + checkExistByPositionDTO.setOrganizeId(management.getOrgId()); + checkExistByPositionDTO.setPositionId(management.getRegularPostId()); + checkExistByPositionDTO.setGradeId(management.getRegularGradeId()); + ActionResult> result = v2UserApi.listUserBound(checkExistByPositionDTO); + if (result == null || result.getData() == null ){ + throw new RuntimeException("该员工的岗位在提交转正审批后发生了变更,无法继续审批;请选择“不通过”结束审批流程!"); + } + } + + /** + * 消息通知 + * @param userId + * @param success + */ + private void messageNotifications(String userId, boolean success) { + UserSuccessDTO successDTO = new UserSuccessDTO(); + successDTO.setUserIds(List.of(userId)); + successDTO.setSuccess(success); + noticeUtils.becomeRegularEmployee(List.of(successDTO)); + } + + @Override + public PageListVO pageList(PersonnelsQueryDTO dto, CultivatePage cultivatePage, Boolean export) { + Page page = cultivatePage.coverCultivatePage(); + // 超过转正时间(审批通过,超过实际转正时间,列表不显示) +// if (!export) { +// removeTheCorrectionReviewTimeExceeded(); +// } + UserInfo user = UserProvider.getUser(); + if ( !user.getIsAdministrator() ) { + // 数据权限userIds + List userIds = permissionHandling.getUserIdsByUserId(user.getUserId()); + if (userIds != null && userIds.isEmpty()) return CultivatePage.coverPageList(page); + if (CollUtil.isNotEmpty(userIds)) { + dto.setUserIds(userIds); + } + } + List regularManagementVOList = baseMapper.queryList(dto); + List userIds = regularManagementVOList.stream().filter(item -> item.getTransferStatus() == 5).map(FtbPersonnelsRegularManagementVO::getUserId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)){ + supplementalData(userIds, regularManagementVOList); + } + if (CollUtil.isNotEmpty(dto.getOrgIds())) { + List orgIds = dto.getOrgIds(); + regularManagementVOList = regularManagementVOList.stream() + .filter(v -> StringUtils.isNotEmpty(v.getOrgId()) && orgIds.contains(v.getOrgId())).collect(Collectors.toList()); + } + regularManagementVOList.stream().filter(v->v.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()) || + v.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.CANCEL.getCode())).forEach(item->{ + if (StringUtils.isNotEmpty(item.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(item.getTaskInfoId()); + if (ObjectUtil.isNotEmpty(byTaskId) && user.getUserId().equals(byTaskId.getCreatorUserId())) { + item.setCurrentSponsor(1); + } else { + item.setCurrentSponsor(0); + } + } + }); + return CultivatePage.paginate(regularManagementVOList,page); + } + + /** + * 超过转正时间(审批通过,超过实际转正时间,列表不显示) + */ + private void removeTheCorrectionReviewTimeExceeded() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsRegularManagement::getEnableMark,0); + List ftbPersonnelsRegularManagements = baseMapper.selectList(queryWrapper); + Date date = new Date(); + List userIds = ftbPersonnelsRegularManagements.stream().filter(item -> item.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.NOT_PROCESSED.getCode())).map(FtbPersonnelsRegularManagement::getUserId).collect(Collectors.toList()); + Map userMap = new HashMap<>(); + if (CollUtil.isNotEmpty(userIds)) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIds, null); + if (actionResult != null || CollUtil.isNotEmpty(actionResult.getData())) { + return; + } + userMap = actionResult.getData().stream() + .collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2) -> k1)); + } + // 将未办理数据实时更新最新的组织信息 + Map finalUserMap = userMap; + ftbPersonnelsRegularManagements.forEach(item -> { + if (item.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()) || + item.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode())) { + boolean sameDay =false; + if (item.getActualConverDate() != null ) { + sameDay = isSameDay(date, item.getActualConverDate()); + } + if (item.getActualConverDate() != null && (date.after(item.getActualConverDate()) || sameDay )) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsRegularManagement::getEnableMark, 1); + wrapper.set(SuperBaseEntity.SuperCUDBaseEntity::getDeleteUserId,UserProvider.getUser().getUserId()); + wrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, item.getId()); + baseMapper.update(new FtbPersonnelsRegularManagement(), wrapper); + } + } + if ( finalUserMap.containsKey(item.getUserId()) && item.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.NOT_PROCESSED.getCode())){ + FtbPersonnelsRegularManagement management = new FtbPersonnelsRegularManagement(); + UserBoundVO boundMoreInfoVO = finalUserMap.get(item.getUserId()); + // 任一一个有变化进行更新 + if ((StringUtils.isNotEmpty(boundMoreInfoVO.getOrganizeId()) && !boundMoreInfoVO.getOrganizeId().equals(item.getOrgId()))|| + (StringUtils.isNotEmpty(boundMoreInfoVO.getOrganizeName()) && !boundMoreInfoVO.getOrganizeName().equals(item.getOrgName()))|| + (StringUtils.isNotEmpty(boundMoreInfoVO.getPositionId()) && !boundMoreInfoVO.getPositionId().equals(item.getRegularPostId())) || + (StringUtils.isNotEmpty(boundMoreInfoVO.getPositionName()) && !boundMoreInfoVO.getPositionName().equals(item.getRegularPostName()) )|| + (StringUtils.isNotEmpty(boundMoreInfoVO.getGradeId()) && !boundMoreInfoVO.getGradeId().equals(item.getRegularGradeId()) )|| + (StringUtils.isNotEmpty(boundMoreInfoVO.getGradeName()) && !boundMoreInfoVO.getGradeName().equals(item.getRegularGradeName()))){ + management.setId(item.getId()); + management.setOrgId(boundMoreInfoVO.getOrganizeId()); + management.setOrgName(boundMoreInfoVO.getOrganizeName()); + management.setRegularPostId(boundMoreInfoVO.getPositionId()); + management.setRegularPostName(boundMoreInfoVO.getPositionName()); + management.setRegularGradeId(boundMoreInfoVO.getGradeId()); + management.setRegularGradeName(boundMoreInfoVO.getGradeName()); + baseMapper.updateById(management); + } + + } + }); + } + + private boolean isSameDay(Date d1, Date d2) { + LocalDate localDate1 = d1.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate localDate2 = d2.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return localDate1.equals(localDate2); + } + @Override + public PageListVO pageForAppList(FtbPersonnelsForAppQueryDTO dto, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + // 超过转正时间(审批通过,超过实际转正时间,列表不显示) + //removeTheCorrectionReviewTimeExceeded(); + dto.setUserId(UserProvider.getUser().getUserId()); + if ("1".equals(dto.getFlagByApp())) { + if ( !UserProvider.getUser().getIsAdministrator() ) { + // 数据权限userIds + List userIds = permissionHandling.getUserIdsByUserId(UserProvider.getUser().getUserId()); + if (CollUtil.isNotEmpty(userIds)) { + dto.setUserIds(userIds); + } + } + } + Page listForApp = baseMapper.getListForApp(page, dto); + // 查询最新主管 + List userIds = listForApp.getRecords().stream().filter(item -> item.getTransferStatus() == 5).map(FtbPersonnelsRegularManagementVO::getUserId).collect(Collectors.toList()); + supplementalData(userIds, listForApp.getRecords()); + String userId = UserProvider.getUser().getUserId(); + listForApp.getRecords().stream().filter(v->v.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()) || + v.getTransferStatus().equals(FtbPersonnelsAuditTaskEnum.CANCEL.getCode())).forEach(item->{ + if (StringUtils.isNotEmpty(item.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(item.getTaskInfoId()); + if (ObjectUtil.isNotEmpty(byTaskId) && userId.equals(byTaskId.getCreatorUserId())) { + item.setCurrentSponsor(1); + } else { + item.setCurrentSponsor(0); + } + } + }); + return CultivatePage.coverPageList(listForApp); + } + + /** + * 补充数据 + * @param userIds + * @param + */ + private void supplementalData(List userIds,List records ) { + Map userMap = new HashMap<>(); + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIds, UserProvider.getUser().getTenantId()); + if (actionResult != null && CollUtil.isNotEmpty(actionResult.getData())) { + userMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2)-> k1)); + } + Map finalUserMap = userMap; + records.forEach(item->{ + String userId = item.getUserId(); + if (finalUserMap.containsKey(userId)){ + UserBoundVO boundMoreInfoVO = finalUserMap.get(userId); + item.setOrgId(boundMoreInfoVO.getOrganizeId()); + item.setOrgName(boundMoreInfoVO.getOrganizeName()); + item.setRegularPostId(boundMoreInfoVO.getPositionId()); + item.setRegularPostName(boundMoreInfoVO.getPositionName()); + item.setRegularGradeId(boundMoreInfoVO.getGradeId()); + item.setRegularGradeName(boundMoreInfoVO.getGradeName()); + item.setImmediateSuperName(boundMoreInfoVO.getLeaderName()); + item.setImmediateSuperId(boundMoreInfoVO.getLeaderId()); + } + }); + } + + @Override + public FtbPersonnelsBubbleCountVO getListCont(String flag) { + // flag "1" 我的审批 "2" 抄送 + FtbPersonnelsBubbleCountVO countVO = new FtbPersonnelsBubbleCountVO(); + FtbPersonnelsForAppQueryDTO appQueryDTO = new FtbPersonnelsForAppQueryDTO(); + appQueryDTO.setUserId(UserProvider.getUser().getUserId()); + if ("1".equals(flag)) { + appQueryDTO.setFlagByApp("2"); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.PENDING.getCode()); + appQueryDTO.setConfigType(FtbPersonnelsCofigEnum.CONVERSION_APPROVAL_CONFIGURATION.getConfigType()); + countVO.setPendingCount(baseMapper.getThePendingApprovalList(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + countVO.setQuantityUnderCount(baseMapper.getAuditedDataList(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + countVO.setApprovedCount(baseMapper.getAuditedDataList(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + countVO.setApprovalFailedCount(baseMapper.getAuditedDataList(new Page<>(), appQueryDTO).getTotal()); + } else if ("2".equals(flag)) { + appQueryDTO.setFlagByApp("3"); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + appQueryDTO.init("3",FtbPersonnelsCofigEnum.CONVERSION_APPROVAL_CONFIGURATION); + countVO.setQuantityUnderCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setStatusList(null); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + countVO.setApprovedCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + countVO.setApprovalFailedCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + } + return countVO; + } + + @Override + public void clearHistoricalConversionData(List userIds) { + // 清除所有之前的转正数据 + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsRegularManagement::getUserId,userIds); + List managements = baseMapper.selectList(wrapper); + // 没有转正数据清除 + if (CollUtil.isEmpty(managements)){ + return; + } + // 清除具有审批数据的数据 + managements.stream().filter(vo-> StringUtils.isNotEmpty(vo.getTaskInfoId())) + .forEach (item->runTaskService.deleteReviewProcessAndRecords(item.getTaskInfoId(),item.getId())); + // 清楚转正数据 + List collect = managements.stream().map(FtbPersonnelsRegularManagement::getId).collect(Collectors.toList()); + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.in(FtbPersonnelsRegularManagement::getId, collect); + baseMapper.delete(lambdaed); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + @SneakyThrows + public String applyForRegularization(FtbPersonnelsRegularCreateDTO createDto) { + // 校验是否是重复办理转正 + String taskId = createDto.getTaskId(); + String id = createDto.getId(); + checkWhetherItIsRepeated(id, taskId); + String resultStr = ""; + // 校验转正是否完成 + verifyWhetherTheUserHasCompletedTheProcess(createDto.getUserId()); + // 校验是否处于流程中 + checkWhetherTheCheckIsInTheProcess(createDto.getUserId()); + FtbPersonnelsRegularManagement regularManagement = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularManagement(createDto); + if (StringUtils.isNotEmpty(regularManagement.getId())) { + // 删除上传的文件重新上传 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateFile::getBusinessId, regularManagement.getId()); + ftbCultivateFileService.remove(updateWrapper); + } + String onboardPostId = regularManagement.getOnboardPostId(); + if (StringUtils.isNotEmpty(onboardPostId)) { + ActionResult voActionResult = v2PositionApi.infoPosition(onboardPostId); + if (ObjectUtil.isNotEmpty(voActionResult) && ObjectUtil.isNotEmpty(voActionResult.getData()))regularManagement.setOnboardPostName(voActionResult.getData().getFullName()); + } + String onboardGradeId = regularManagement.getOnboardGradeId(); + if (StringUtils.isNotEmpty(onboardGradeId)) { + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(onboardPostId); + if (ObjectUtil.isNotEmpty(gradeVOActionResult) && ObjectUtil.isNotEmpty(gradeVOActionResult.getData()))regularManagement.setOnboardPostName(gradeVOActionResult.getData().getFullName()); + } + FtbPersonnelsResult result = + FtbPersonnelsResult.getResult(createDto.getSource(), createDto.getFlag()); + String userId = UserProvider.getUser().getUserId(); + // 如果员工自己申请需要清除未办理数据 + if (createDto.getUserId().equals(userId) && result.source == 0){ + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsRegularManagement::getUserId,createDto.getUserId()); + lambdaQuery.eq(FtbPersonnelsRegularManagement::getTransferStatus,FtbPersonnelsAuditTaskEnum.NOT_PROCESSED.getCode()); + FtbPersonnelsRegularManagement management = baseMapper.selectOne(lambdaQuery); + if (ObjectUtil.isNotEmpty(management)){ + baseMapper.deleteById(management.getId()); + } + } + regularManagement.setTransferStatus(result.state); + regularManagement.setApplicationSource(result.source); + regularManagement.setCreatorTime(new Date()); + regularManagement.setTaskInfoId(null); + // 重新设置为有效 + regularManagement.setEnableMark(0L); + regularManagement.setVersionNum(1); + // 添加薪资详情 + List salaryStructureList = createDto.getSalaryStructureList(); + // 校验岗位是否还存在 + QueryUserBoundDTO checkExistByPositionDTO = new QueryUserBoundDTO(); + checkExistByPositionDTO.setPositionId(createDto.getRegularPostId()); + checkExistByPositionDTO.setGradeId(createDto.getRegularGradeId()); + doCheckOrgPositionGradesBoundDTO(checkExistByPositionDTO); + // 薪酬冲突 + // 没有值时进行校验薪酬时间冲突问题 + Integer isDelete = createDto.getIsDelete(); + if ("1".equals(createDto.getIsAdjustSalary()) && isDelete == null){ + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + createDto.setPayrollEffectiveDate(createDto.getActualConverDate()); + String format = simpleDateFormat.format(createDto.getPayrollEffectiveDate()); + ActionResult> historyByChangeDate = querySalaryApi.getHistoryByChangeDate(format, createDto.getUserId()); + if (historyByChangeDate == null){ + throw new RuntimeException("获取薪酬数据失败!"); + } + List data = historyByChangeDate.getData(); + if (CollUtil.isNotEmpty(data)) return "5"; + } + if ("1".equals(createDto.getIsAdjustSalary() ) && CollUtil.isNotEmpty(salaryStructureList)) { + regularManagement.setSalaryStructureList(JSONObject.toJSONString(salaryStructureList)); + } + // 办理审批人更新 + regularManagement.setCreatorUserId(UserProvider.getUser().getUserId()); + this.saveOrUpdate(regularManagement); + String managementId = regularManagement.getId(); + // 转正前调用花名册进行员工状态同步 + staffRosterService.recordPreRegularStatus(regularManagement.getUserId()); + // 创建审批 + if (result.source != 2 && result.source != 3) { + String runTaskId = runTaskService.startTheReviewProcess(managementId, + FtbPersonnelsCofigEnum.CONVERSION_APPROVAL_CONFIGURATION.getConfigType(), + createDto.getOrgId(), + regularManagement.getUserId()); + // 同步id + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.set(FtbPersonnelsRegularManagement::getTaskInfoId, runTaskId); + lambdaUpdate.eq(FtbPersonnelsRegularManagement::getId,managementId); + baseMapper.update(null,lambdaUpdate); + }else if (result.source == 2){ + UserInfo userInfo = UserProvider.getUser(); + if (regularManagement.getUserId().equals(userInfo.getUserId())){ + throw new RuntimeException("无法给自己进行手动转正!"); + } + // 转正通知 + // 转正员工同步 + messageNotifications(regularManagement.getUserId(),true); + // 手动调整薪酬 + if ("1".equals(regularManagement.getIsAdjustSalary()) && CollUtil.isNotEmpty(salaryStructureList) ){ + synchronizePayrollData(regularManagement); + } + // 实际转正时间选择当前时间之前的直接手动变更数据 + changeEmployeeData(regularManagement); + } + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(createDto.getFiles()) + .businessTypeID(managementId) + .type(FileEventDTO.FileType.REGULAR_MANAGEMENT) + .build())); + + return resultStr; + } + + /** + * 实际转正时间选择当前时间之前的直接手动变更数据 + * + * @param regularManagement + */ + public void changeEmployeeData(FtbPersonnelsRegularManagement regularManagement){ + if ( regularManagement.getActualConverDate() == null || !DateUtil.date().isAfter(regularManagement.getActualConverDate()) )return; + syncRegularStatus(UserProvider.getUser().getTenantId()); + } + + private void checkWhetherItIsRepeated(String id, String taskId) { + if (StringUtils.isNotEmpty(id) || StringUtils.isNotEmpty(taskId)){ + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq( StringUtils.isNotEmpty(id),SuperBaseEntity.SuperIBaseEntity::getId, id); + queryWrapper.eq(StringUtils.isNotEmpty(taskId),FtbPersonnelsRegularManagement::getTaskInfoId, taskId); + queryWrapper.in(FtbPersonnelsRegularManagement::getTransferStatus,List.of(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode(), + FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode())); + Long aLong = baseMapper.selectCount(queryWrapper); + if (aLong > 0){ + throw new RuntimeException("该用户已办理转正,无需重复办理!"); + } + } + } + + @GlobalTransactional + @Override + public ActionResult applyForRegularizationForOA(FtbPersonnelsRegularCreateDTO createDto) { + + // 校验是否处于流程中 + ActionResult actionResult = new ActionResult<>(); + actionResult.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + checkWhetherTheCheckIsInTheProcess(createDto.getUserId()); + boolean after = createDto.getDateOfEntry().after(createDto.getActualConverDate()); + if (after) throw new RuntimeException("转正日期不能小于入职日期!"); + List fileDTOS = new ArrayList<>(); + if (StringUtils.isNotEmpty(createDto.getFilesWithOa())){ + String filesWithOa = createDto.getFilesWithOa(); + fileDTOS = JSONObject.parseArray(filesWithOa, FtbCultivateOfflineFileDTO.class); + } + FtbPersonnelsRegularManagement regularManagement = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularManagement(createDto); + // 校验转正是否完成 + verifyWhetherTheUserHasCompletedTheProcess(regularManagement.getUserId()); + if(createDto.getSource() == 1 && regularManagement.getUserId().equals(UserProvider.getUser().getUserId())) { + throw new DataException("不能为自己办理!"); + } + checkWhetherItIsRepeated(regularManagement.getId(), null); + StaffHomeDto staffHomeDto = staffRosterService.queryWorkerHomeDetail(regularManagement.getUserId()); + //①工作状态为试用②工作状态为试岗且有试用期 + boolean workStatusForSg = "306".equals(staffHomeDto.getWorkerStatus()) && !"100".equals(staffHomeDto.getProbationPeriod()); + if (!("302".equals(staffHomeDto.getWorkerStatus()) || workStatusForSg)){ + actionResult.setMsg("该员工无试用期,不需要发起转正!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + if(staffHomeDto.getActualStartDate().after(regularManagement.getActualConverDate()) || + staffHomeDto.getActualStartDate().compareTo(regularManagement.getActualConverDate()) == 0){ + actionResult.setMsg("转正日期不能早于或者等于入职日期!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + + FtbPersonnelsResult result = FtbPersonnelsResult.getResult(createDto.getSource(), createDto.getFlag()); + regularManagement.setTransferStatus(result.state); + regularManagement.setApplicationSource(result.source); + if (result.source != 0){ + Date payrollEffectiveDate = regularManagement.getPayrollEffectiveDate(); + payrollEffectiveDate = DateUtil.endOfDay(payrollEffectiveDate); + List serviceIdList = ftbPersonnelsSecondmentManagementService.salarySecondmentCrossover(regularManagement.getUserId(), + payrollEffectiveDate); + if(CollUtil.isNotEmpty(serviceIdList)){ + String collected = serviceIdList.stream().map(v -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + return sdf.format(v.getSecondmentStartTime()) + "~" + sdf.format(v.getSecondmentEndTime()); + }).collect(Collectors.joining(",")); + String joined = String.format("当前办理转正选择日期为%s生效,\n" + + "检测到您有一项借调业务的起止时间(%s)与当前办理业务的生效日期\n" + + "重叠,请重新选择日期(可选择借调开始之前的日期或者借调结束之后的日期)", + DateUtil.format(regularManagement.getPayrollEffectiveDate(),"yyyy-MM-dd"),collected); + throw new RuntimeException(joined); + } + } + if (result.source != 0 && !UserProvider.getUser().getIsAdministrator()) { + // 权限用户 + List userIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(UserProvider.getUser().getUserId(), PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), "Web"); + // 当前人不是本人申请,且权限中有当前可以办理人 + if ((regularManagement.getApplicationSource() != 0) && (CollUtil.isNotEmpty(userIds) && !userIds.contains(regularManagement.getUserId()))) { + actionResult.setMsg("当前人办理人不具有该员工办理权限!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + } + if (ObjectUtil.isNotEmpty(staffHomeDto)){ + buildYourData(regularManagement,staffHomeDto); + } + if (StringUtils.isNotEmpty(regularManagement.getId())) { + // 删除上传的文件重新上传 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateFile::getBusinessId, regularManagement.getId()); + ftbCultivateFileService.remove(updateWrapper); + } + String onboardPostId = regularManagement.getOnboardPostId(); + if (StringUtils.isNotEmpty(onboardPostId)) { + ActionResult voActionResult = v2PositionApi.infoPosition(onboardPostId); + if (ObjectUtil.isNotEmpty(voActionResult) && voActionResult.getData() != null )regularManagement.setOnboardPostName(voActionResult.getData().getFullName()); + } + String onboardGradeId = regularManagement.getOnboardGradeId(); + if (StringUtils.isNotEmpty(onboardGradeId)) { + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(onboardPostId); + if (ObjectUtil.isNotEmpty(gradeVOActionResult) && gradeVOActionResult.getData() != null)regularManagement.setOnboardPostName(gradeVOActionResult.getData().getFullName()); + } + String userId = UserProvider.getUser().getUserId(); + // 如果员工自己申请需要清除未办理数据 + if (regularManagement.getUserId().equals(userId) && result.source == 0){ + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsRegularManagement::getUserId,regularManagement.getUserId()); + lambdaQuery.eq(FtbPersonnelsRegularManagement::getTransferStatus,FtbPersonnelsAuditTaskEnum.NOT_PROCESSED.getCode()); + FtbPersonnelsRegularManagement management = baseMapper.selectOne(lambdaQuery); + if (ObjectUtil.isNotEmpty(management)){ + baseMapper.deleteById(management.getId()); + } + regularManagement.setLastModifyUserId(UserProvider.getUser().getUserId()); + regularManagement.setLastModifyTime(new Date()); + } + regularManagement.setCreatorTime(new Date()); + // 重新设置为有效 + regularManagement.setEnableMark(0L); + regularManagement.setVersionNum(1); + // 第二办理没有填写转正备注将数据进行清除 + if (regularManagement.getId() != null ){ + FtbPersonnelsRegularManagement management = baseMapper.selectById(regularManagement.getId()); + if (management != null && StringUtils.isNotEmpty(management.getRemarksConver())){ + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId,regularManagement.getId()); + updateWrapper.set(FtbPersonnelsRegularManagement::getRemarksConver,null); + baseMapper.update(null,updateWrapper); + } + } + // 添加薪资详情 + List salaryStructureList = createDto.getSalaryStructureList(); + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(createDto.getIsAdjustSalary()) ){ + actionResult.setMsg("目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + }else if (CollUtil.isNotEmpty(salaryStructureList) && "1".equals(createDto.getIsAdjustSalary())){ + regularManagement.setSalaryStructureList(JSONObject.toJSONString(salaryStructureList)); + } + UserInfo userInfo = UserProvider.getUser(); + if (result.source == 2 && regularManagement.getUserId().equals(userInfo.getUserId())){ + actionResult.setMsg("无法给自己进行手动转正!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + // 清除之前的数据 + if (StringUtils.isNotEmpty(regularManagement.getUserId())){ + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsRegularManagement::getEnableMark,0); + queryWrapper.eq(FtbPersonnelsRegularManagement::getUserId,regularManagement.getUserId()); + List management = baseMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(management)){ + // 清除未办理数据 + management.forEach(v->baseMapper.deleteById(v.getId())); + } + } + regularManagement.setCreatorUserId(UserProvider.getUser().getUserId()); + this.saveOrUpdate(regularManagement); + String managementId = regularManagement.getId(); + // 转正前调用花名册进行员工状态同步 + staffRosterService.recordPreRegularStatus(regularManagement.getUserId()); + // 创建审批 + if (result.source == 0 || result.source == 1) { + // 同步id + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.set(FtbPersonnelsRegularManagement::getTaskInfoId, createDto.getTaskId()); + lambdaUpdate.eq(FtbPersonnelsRegularManagement::getId,managementId); + baseMapper.update(null,lambdaUpdate); + } + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(fileDTOS) + .businessTypeID(managementId) + .type(FileEventDTO.FileType.REGULAR_MANAGEMENT) + .build())); + return actionResult; + } + + /** + * 构建数据 + * @param regularManagement + * @param staffHomeDto + */ + private void buildYourData(FtbPersonnelsRegularManagement regularManagement, StaffHomeDto staffHomeDto) { + regularManagement.setSystemWokerId(staffHomeDto.getSystemWokerId()); + regularManagement.setPhoneNumber(staffHomeDto.getPhone()); + regularManagement.setUserName(staffHomeDto.getName()); + regularManagement.setDateOfEntry(staffHomeDto.getActualStartDate()); + regularManagement.setJobNumber(staffHomeDto.getWorkerNo()); + regularManagement.setProbation(staffHomeDto.getProbationPeriod()); + regularManagement.setProbationPeriodDay(staffHomeDto.getProbationPeriodDay()); + regularManagement.setImmediateSuperId(staffHomeDto.getReportsTo()); + regularManagement.setImmediateSuperName(staffHomeDto.getReportsToName()); + regularManagement.setSchedConverDate(staffHomeDto.getPlanProbationaryDate()); + regularManagement.setStartingSalary(staffHomeDto.getEntrySalary()); + regularManagement.setOnboardPostId(staffHomeDto.getCurrPosition()); + regularManagement.setOnboardPostName(staffHomeDto.getCurrPositionName()); + regularManagement.setOnboardGradeId(staffHomeDto.getCurrRank()); + regularManagement.setOnboardGradeName(staffHomeDto.getCurrRankName()); + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(regularManagement.getOrgId()); + if (ObjectUtil.isNotEmpty(organizeEntity))regularManagement.setOrgName(organizeEntity.getFullName()); + PositionEntity positionEntity = personnelOrgUtils.queryPosition(regularManagement.getRegularPostId()); + if (ObjectUtil.isNotEmpty(positionEntity)) regularManagement.setRegularPostName(positionEntity.getFullName()); + PositionGradesInfoVO gradesInfoVO = personnelOrgUtils.queryRank(regularManagement.getRegularGradeId()); + if (ObjectUtil.isNotEmpty(gradesInfoVO))regularManagement.setRegularGradeName(gradesInfoVO.getFullName()); + } + + @GlobalTransactional + @Override + public ActionResult regularizationApprovalForOA(FtbPersonnelsSalaryAuditDto auditDto) { + ActionResult actionResult = new ActionResult<>(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsRegularManagement::getTaskInfoId,auditDto.getTaskId()); + FtbPersonnelsRegularManagement regularManagement = baseMapper.selectOne(queryWrapper); + // 选择通过需要校验组织是否存在 + if ("1".equals(auditDto.getFlag()))verify(regularManagement); + //FtbPersonnelsAuditTaskEnum performReview = runTaskService.performReview(auditDto); + // 根据oa返回进行状态更新 + FtbPersonnelsAuditTaskEnum performReview = auditDto.getFlag().equals("0") ? FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED : FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsRegularManagement::getId, regularManagement.getId()); + wrapper.set(FtbPersonnelsRegularManagement::getTransferStatus, performReview.getCode()); + if (StringUtils.isNotEmpty(auditDto.getIsAdjustSalary())){ + wrapper.set(FtbPersonnelsRegularManagement::getIsAdjustSalary,auditDto.getIsAdjustSalary()); + } + if (StringUtils.isNotEmpty(auditDto.getSalaryStructure())){ + wrapper.set(FtbPersonnelsRegularManagement::getSalaryStructure,auditDto.getSalaryStructure()); + } + if ( ObjectUtil.isNotEmpty(auditDto.getSalary())){ + wrapper.set(FtbPersonnelsRegularManagement::getRegularSalary,auditDto.getSalary()); + } + if (ObjectUtil.isNotEmpty(auditDto.getPayrollEffectiveDate())){ + wrapper.set(FtbPersonnelsRegularManagement::getPayrollEffectiveDate,auditDto.getPayrollEffectiveDate()); + } + if (StringUtils.isNotEmpty(auditDto.getReasonsForSalaryAdjustments())) { + wrapper.set(FtbPersonnelsRegularManagement::getReasonsForSalaryAdjustments, auditDto.getReasonsForSalaryAdjustments()); + } + if (StringUtils.isNotEmpty(auditDto.getPayrollSequenceId())){ + wrapper.set(FtbPersonnelsRegularManagement::getPayrollSequenceId, auditDto.getPayrollSequenceId()); + } + if (StringUtils.isNotEmpty(auditDto.getPayrollSequenceName())){ + wrapper.set(FtbPersonnelsRegularManagement::getPayrollSequenceName, auditDto.getPayrollSequenceName()); + } + if (ObjectUtil.isNotEmpty(auditDto.getPayrollSequence())){ + wrapper.set(FtbPersonnelsRegularManagement::getPayrollSequence, auditDto.getPayrollSequence()); + } + List salaryStructureList = auditDto.getSalaryStructureList(); + if(CollUtil.isNotEmpty(salaryStructureList)){ + wrapper.set(FtbPersonnelsRegularManagement::getSalaryStructureList, + JSONObject.toJSONString(salaryStructureList)); + } + // 薪资员工自己申请的 + if(ObjectUtil.isNotEmpty(auditDto.getIsAdjustSalary())){ + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(auditDto.getIsAdjustSalary()) ){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + return actionResult; + } + wrapper.set(FtbPersonnelsRegularManagement::getIsAdjustSalary,auditDto.getIsAdjustSalary()); + } + if(ObjectUtil.isNotEmpty(auditDto.getSalaryStructure())){ + wrapper.set(FtbPersonnelsRegularManagement::getSalaryStructure,auditDto.getSalaryStructure()); + } + baseMapper.update(null, wrapper); + if (FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.equals(performReview)) { + // 成功通知 + FtbPersonnelsRegularManagement thisRegularInfo = baseMapper.selectById(regularManagement.getId()); + if (thisRegularInfo.getApplicationSource() == 0){ + Date payrollEffectiveDate = thisRegularInfo.getPayrollEffectiveDate(); + payrollEffectiveDate = DateUtil.endOfDay(payrollEffectiveDate); + List serviceIdList = ftbPersonnelsSecondmentManagementService.salarySecondmentCrossover(thisRegularInfo.getUserId(), + payrollEffectiveDate); + if(CollUtil.isNotEmpty(serviceIdList)){ + String collected = serviceIdList.stream().map(v -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + return sdf.format(v.getSecondmentStartTime()) + "~" + sdf.format(v.getSecondmentEndTime()); + }).collect(Collectors.joining(",")); + String joined = String.format("当前办理转正选择日期为%s生效,\n" + + "检测到您有一项借调业务的起止时间(%s)与当前办理业务的生效日期\n" + + "重叠,请重新选择日期(可选择借调开始之前的日期或者借调结束之后的日期)", + DateUtil.format(payrollEffectiveDate,"yyyy-MM-dd"),collected); + throw new RuntimeException(joined); + } + } + messageNotifications(regularManagement.getUserId(), true); + synchronizePayrollData(thisRegularInfo); + changeEmployeeData(regularManagement); + }else { + // 失败通知 + messageNotifications(regularManagement.getUserId(), false); + } + return actionResult; + } + + public void verifyWhetherTheUserHasCompletedTheProcess(String userId){ + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsRegularManagement::getTransferStatus, + List.of( FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode(), FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode())); + queryWrapper.eq(FtbPersonnelsRegularManagement::getEnableMark, 0); + queryWrapper.eq(FtbPersonnelsRegularManagement::getUserId, userId); + Long aLong = baseMapper.selectCount(queryWrapper); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsStaffRoster::getWorkerStatus,"303"); + wrapper.eq(FtbPersonnelsStaffRoster::getUserId,userId); + long rostCount = staffRosterService.count(wrapper); + if(aLong > 0 && rostCount > 0) throw new RuntimeException("当前人员已经完成了转正请勿重复办理!"); + LambdaQueryWrapper wrapper2 = Wrappers.lambdaQuery(); + wrapper2.notIn(FtbPersonnelsStaffRoster::getWorkerStatus,"304","305","303"); + wrapper2.eq(FtbPersonnelsStaffRoster::getUserId,userId); + long rostCount2 = staffRosterService.count(wrapper2); + if(aLong > 0 && rostCount2 > 0) throw new RuntimeException("当前人员已经办理转正,还未到实际转正时间,请勿重复办理!"); + } + @Override + public void syncRegularStatus(String tenantId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsRegularManagement::getTransferStatus, + List.of( FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode(), FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode())); + queryWrapper.eq(FtbPersonnelsRegularManagement::getEnableMark, 0); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsStaffRoster::getWorkerStatus,"302"); + List list = staffRosterService.list(wrapper); + List userIds = list.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList()); + if(CollUtil.isEmpty(userIds)) return; + queryWrapper.in(FtbPersonnelsRegularManagement::getUserId, userIds); + List managements = baseMapper.selectList(queryWrapper); + Date date = new Date(); + try { + managements.stream().filter(v->{ + Date actualConverDate = v.getActualConverDate(); + if (date.after(actualConverDate)) return true; + return false; + }).forEach(regularManagement->{ + StaffRegularDto regularDto = StaffRegularDto.coverFtbPersonnelsRegularManagement(regularManagement); + staffRosterService.innerChangeRegular(regularManagement.getUserId(), regularDto, regularManagement.getRegularSalary(),tenantId); + }); + + }catch (Exception e){ + e.printStackTrace(); + log.error("执行员工转正变更状态失败!"); + } + + } + + @Override + public void applyForRegularizationBatch(List createDTO) { + long start = System.currentTimeMillis(); + List ftbPersonnelsRegularManagements = createDTO.stream().map(createDto -> { + FtbPersonnelsRegularManagement regularManagement = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularManagement(createDto); + FtbPersonnelsResult result = + FtbPersonnelsResult.getResult(createDto.getSource(), createDto.getFlag()); + regularManagement.setTransferStatus(result.state); + regularManagement.setApplicationSource(result.source); + regularManagement.setCreatorTime(new Date()); + regularManagement.setTaskInfoId(null); + // 重新设置为有效 + regularManagement.setEnableMark(0L); + regularManagement.setVersionNum(1); + return regularManagement; + }).collect(Collectors.toList()); + // 转正通知 + this.saveBatch(ftbPersonnelsRegularManagements); + long end = System.currentTimeMillis(); + log.error(String.format("花名册导入批量转正(applyForRegularizationBatch) 记录数:%d 耗时:%dms", + ftbPersonnelsRegularManagements.size(), (end - start))); + } + + private void synchronizePayrollData(FtbPersonnelsRegularManagement thisRegularInfo,String... tenantIds) { + String dataSalaryStructureList = thisRegularInfo.getSalaryStructureList(); + if ("1".equals(thisRegularInfo.getIsAdjustSalary()) && StringUtils.isNotEmpty(dataSalaryStructureList) && + !("null".equals(dataSalaryStructureList) || "[]".equals(dataSalaryStructureList))){ + List salaryInfos = JSONObject.parseArray(dataSalaryStructureList, FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = getUserInfoWithSalary(thisRegularInfo); + String creatorUserId = thisRegularInfo.getCreatorUserId(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, creatorUserId); + FtbPersonnelsStaffRoster serviceOne = staffRosterService.getOne(queryWrapper); + if (Objects.nonNull(serviceOne)){ + userInfoWithSalary.setOperatorWorkNo(serviceOne.getSystemWokerId()); + } + userInfoWithSalary.setTaskId(thisRegularInfo.getTaskInfoId()); + // 入职时间 + userInfoWithSalary.setEntryDate(thisRegularInfo.getDateOfEntry()); + // 计划转正时间 + userInfoWithSalary.setBeComeDate(thisRegularInfo.getActualConverDate()); + LambdaQueryWrapper queryWrapper2 = Wrappers.lambdaQuery(); + queryWrapper2.eq(FtbPersonnelsStaffRoster::getUserId, thisRegularInfo.getUserId()); + FtbPersonnelsStaffRoster staffRoster = staffRosterService.getOne(queryWrapper2); + if (Objects.nonNull(staffRoster)){ + userInfoWithSalary.setUserTypeId(staffRoster.getWorkerType()); + } + String recordId = salaryService.saveTheChangePayInformation(salaryInfos, + thisRegularInfo.getUserId(), + thisRegularInfo.getPayrollEffectiveDate(), + userInfoWithSalary, + "1", + thisRegularInfo.getReasonsForSalaryAdjustments(), + "1", + 0, + thisRegularInfo.getSalaryType(), + thisRegularInfo.getPayrollSequenceId(), + tenantIds.length > 0 ? tenantIds[0] : null); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsRegularManagement::getSalaryRecordId, recordId); + wrapper.eq(FtbPersonnelsRegularManagement::getId, thisRegularInfo.getId()); + baseMapper.update(null, wrapper); + } + } + + @NotNull + private static UserInfoWithSalary getUserInfoWithSalary(FtbPersonnelsRegularManagement regularManagement) { + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + userInfoWithSalary.setRankName(regularManagement.getRegularGradeName()); + userInfoWithSalary.setRankId(regularManagement.getRegularGradeId()); + userInfoWithSalary.setFOrgId(regularManagement.getOrgId()); + userInfoWithSalary.setFOrgName(regularManagement.getOrgName()); + userInfoWithSalary.setPostId(regularManagement.getRegularPostId()); + userInfoWithSalary.setPostName(regularManagement.getRegularPostName()); + return userInfoWithSalary; + } + + + private void checkWhetherTheCheckIsInTheProcess(String userId) { + // null 不处于 ,1 转正 2,调岗, 3离职 + String s = runTaskService.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(userId); + if (StringUtil.isNotEmpty(s)){ + // 1 转正 2 调岗, 3 离职 + String str=""; + if (StringUtil.isNotEmpty(s)) { + // 1 转正 2 调岗, 3 离职 + switch (s) { + case "1": + str = "转正"; + break; + case "2": + str = "调岗"; + break; + case "3": + str = "离职"; + break; + case "4": + str = "晋升"; + break; + case "5": + str = "调店"; + break; + case "6": + str = "降职"; + break; + case "7": + str = "借调"; + break; + } + } + throw new RuntimeException("当前人员处于"+str+"审批中,请勿重复进行审批!"); + } + } + + @Override + public FtbPersonnelsRegularInfoVO checkTheDetailsOfRegularizationApproval(String id) { + FtbPersonnelsRegularManagement regularManagement = baseMapper.selectById(id); + if (regularManagement == null) {return null;} + FtbPersonnelsRegularInfoVO regularInfoVO = FtbPersonnelsRegularManagement.coverFtbPersonnelsRegularInfoVO(regularManagement); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRoster::getUserId,regularInfoVO.getUserId()); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(wrapper); + if (Objects.equals(regularInfoVO.getTransferStatus(), FtbPersonnelsAuditTaskEnum.NOT_PROCESSED.getCode())){ + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(List.of(regularInfoVO.getUserId()), null); + if (actionResult != null && CollUtil.isNotEmpty(actionResult.getData())) { + List workerGroupDataDtos = actionResult.getData(); + UserBoundVO boundMoreInfoVO = workerGroupDataDtos.get(0); + regularInfoVO.setOrgId(boundMoreInfoVO.getOrganizeId()); + regularInfoVO.setOrgName(boundMoreInfoVO.getOrganizeName()); + regularInfoVO.setRegularPostId(boundMoreInfoVO.getPositionId()); + regularInfoVO.setRegularPostName(boundMoreInfoVO.getPositionName()); + regularInfoVO.setRegularGradeId(boundMoreInfoVO.getGradeId()); + regularInfoVO.setRegularGradeName(boundMoreInfoVO.getGradeName()); + regularInfoVO.setTeamID(boundMoreInfoVO.getStoreTeamId()); + regularInfoVO.setTeamName(boundMoreInfoVO.getStoreTeamName()); + regularInfoVO.setImmediateSuperId(boundMoreInfoVO.getLeaderId()); + regularInfoVO.setImmediateSuperName(boundMoreInfoVO.getLeaderName()); + } + } + regularInfoVO.setUserName(one.getName()); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(List.of(regularManagement.getCreatorUserId()), null); + if (ObjectUtil.isNotEmpty(allUserInfoBatch) && CollUtil.isNotEmpty(allUserInfoBatch.getData())){ + regularInfoVO.setCreatorUserName(allUserInfoBatch.getData().get(0).getName()); + } + FtbPersonnelsAuditInfoVO auditInfoVO = subConfigService.queryAuditSubConfig(regularInfoVO.getOrgId(), FtbPersonnelsCofigEnum.CONVERSION_APPROVAL_CONFIGURATION); + regularInfoVO.setAuditInfo(auditInfoVO); + // 查询当前申请时上传的文件信息 + List list = ftbCultivateFileService.lambdaQuery().eq(FtbCultivateFile::getBusinessId, id) + .eq(FtbCultivateFile::getType,FileEventDTO.FileType.REGULAR_MANAGEMENT.getType()).list(); + List fileVOS = list.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + regularInfoVO.setFiles(fileVOS); + //查询框考勤组信息 + List attendanceUserGroup = attendanceGroupApi.getAttendanceUserGroup(List.of(regularManagement.getUserId())); + if (CollectionUtil.isNotEmpty(attendanceUserGroup)) { + List groupName = new ArrayList<>(); + //stram获取考勤组名称 + for (AttendanceUserGroupVo attendanceUserGroupVo : attendanceUserGroup) { + groupName.add(attendanceUserGroupVo.getGroupName()); + } + regularInfoVO.setAttendanceGroupName(String.join(", ", groupName)); + } + return regularInfoVO; + } + + /** + * 所属组织-岗位-职等校验 + */ + public void doCheckOrgPositionGradesBoundDTO(QueryUserBoundDTO dto) { + String positionId = dto.getPositionId(); + ActionResult voActionResult = v2PositionApi.infoPosition(dto.getPositionId()); + if ( Objects.isNull(voActionResult) || voActionResult.getCode() != 200 ) { + throw new RuntimeException("目标岗位不存在,无法继续提交!"); + } + if (StringUtils.isEmpty(dto.getGradeId())) return; + QueryGradeListDTO listDTO = new QueryGradeListDTO(); + listDTO.setPositionId(positionId); + ActionResult> listGrades = v2GradesApi.listGrades(listDTO); + if (listGrades.getCode() == 200 && CollUtil.isNotEmpty(listGrades.getData())) { + if (listGrades.getData().stream().noneMatch(v -> v.getId().equals(dto.getGradeId()))) { + throw new RuntimeException("目标职级不存在,无法继续提交!"); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationCategoryConfigurationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationCategoryConfigurationServiceImpl.java new file mode 100644 index 0000000..2073cc1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationCategoryConfigurationServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsResignationCategoryConfiguration; +import jnpf.personnels.mapper.FtbPersonnelsResignationCategoryConfigurationMapper; +import jnpf.personnels.service.FtbPersonnelsResignationCategoryConfigurationService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2024/11/8 +* +*/ +@Service +public class FtbPersonnelsResignationCategoryConfigurationServiceImpl extends ServiceImpl implements FtbPersonnelsResignationCategoryConfigurationService{ + + @Resource + private FtbPersonnelsResignationCategoryConfigurationMapper ftbPersonnelsResignationCategoryConfigurationMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationConfigurationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationConfigurationServiceImpl.java new file mode 100644 index 0000000..c734765 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsResignationConfigurationServiceImpl.java @@ -0,0 +1,99 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.resignation.FtbResignationConfigurationDTO; +import jnpf.model.personnels.po.FtbPersonnelsResignationCategoryConfiguration; +import jnpf.model.personnels.po.FtbPersonnelsResignationConfiguration; +import jnpf.model.personnels.vo.resignation.FtbResignationConfigurationVO; +import jnpf.personnels.mapper.FtbPersonnelsResignationConfigurationMapper; +import jnpf.personnels.service.FtbPersonnelsPermissionsService; +import jnpf.personnels.service.FtbPersonnelsResignationCategoryConfigurationService; +import jnpf.personnels.service.FtbPersonnelsResignationConfigurationService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsResignationConfigurationServiceImpl extends ServiceImpl implements FtbPersonnelsResignationConfigurationService { + + @Resource + FtbPersonnelsResignationCategoryConfigurationService categoryConfigurationService; + + @Resource + private FtbPersonnelsPermissionsService ftbPersonnelsPermissionsService; + + + + @Override + @Transactional + public void configurationSave(List resignationConfigurationDTO) { + List names = resignationConfigurationDTO.stream().map(FtbResignationConfigurationDTO::getResignationName) + .collect(Collectors.toList()); + List configIds = resignationConfigurationDTO.stream().map(FtbResignationConfigurationDTO::getConfigId) + .filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsResignationConfiguration::getResignationName, names); + wrapper.notIn(CollUtil.isNotEmpty(configIds), SuperBaseEntity.SuperIBaseEntity::getId,configIds); + Long l = baseMapper.selectCount(wrapper); + if (l > 0) throw new RuntimeException("离职原因名称已经存在,请勿重复添加!"); + resignationConfigurationDTO.stream().filter(vo -> StringUtils.isNotEmpty(vo.getConfigId())) + .map(FtbResignationConfigurationDTO::convertUpdate) + .forEach(this::update); + List collect = resignationConfigurationDTO. + stream().filter(vo -> StringUtils.isEmpty(vo.getConfigId())) + .map(FtbPersonnelsResignationConfiguration::convert) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(collect)) this.saveBatch(collect); + } + + @Override + public PageListVO getList(String keyWords, String resignationTypeId, CultivatePage page) { + Page objectPage = page.coverCultivatePage(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(StrUtil.isNotBlank(resignationTypeId), FtbPersonnelsResignationConfiguration::getResignationTypeId, resignationTypeId); + wrapper.like(StrUtil.isNotBlank(keyWords), FtbPersonnelsResignationConfiguration::getResignationName, keyWords); + wrapper.orderByDesc(SuperBaseEntity.SuperCBaseEntity::getCreatorTime); + List collect = this.list(wrapper) + .stream() + .map(vo -> { + FtbResignationConfigurationVO convert = FtbResignationConfigurationVO.convert(vo); + LambdaQueryWrapper tLambdaQueryWrapper = Wrappers.lambdaQuery(); + tLambdaQueryWrapper.eq(FtbPersonnelsResignationCategoryConfiguration::getId, vo.getResignationTypeId()); + FtbPersonnelsResignationCategoryConfiguration one = categoryConfigurationService.getOne(tLambdaQueryWrapper); + if (ObjectUtil.isNotEmpty(one))convert.setResignationTypeName(one.getResignationTypeName()); + return convert; + }) + .collect(Collectors.toList()); + return CultivatePage.paginate(collect,objectPage); + } + + @Override + public List reasonForResignationDropDown(Integer individualApplication) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsResignationConfiguration::getChecked, 0); + if (individualApplication == null) { + queryWrapper.in(FtbPersonnelsResignationConfiguration::getIsVisible, 0, 1); + }else { + queryWrapper.eq(FtbPersonnelsResignationConfiguration::getIsVisible, 1); + } + queryWrapper.eq(FtbPersonnelsResignationConfiguration::getEnableMark, 0); + return this.list(queryWrapper) + .stream() + .map(FtbResignationConfigurationVO::convert) + .collect(Collectors.toList()); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRewardsPunishmentsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRewardsPunishmentsServiceImpl.java new file mode 100644 index 0000000..1e6bdbd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRewardsPunishmentsServiceImpl.java @@ -0,0 +1,352 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import com.fantaibao.permission.handling.PermissionHandling; +import com.google.common.collect.Maps; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.entity.workflow.PunishmentsApproval; +import jnpf.entity.workflow.RewardApproval; +import jnpf.model.personnels.bo.FtbRewardsImportRedisBO; +import jnpf.model.personnels.dto.rewardspunishments.*; +import jnpf.model.personnels.dto.salary.FtbSalaryMetaDataQueryDto; +import jnpf.model.personnels.po.FtbPersonnelsRewardsPunishments; +import jnpf.model.personnels.vo.rewardspunishments.*; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.listeners.BaseEasyExcelCommonListener; +import jnpf.personnels.mapper.FtbPersonnelsRewardsPunishmentsMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsRewardsPunishmentsService; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import jnpf.workflow.mapper.PunishmentsApprovalMapper; +import jnpf.workflow.mapper.RewardApprovalMapper; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +/** + *

+ * 人事奖惩表 服务实现类 + *

+ * + * @author wcx + * @since 2024-05-08 + */ +@Service +public class FtbPersonnelsRewardsPunishmentsServiceImpl extends ServiceImpl implements FtbPersonnelsRewardsPunishmentsService { + + @Autowired + private V2UserApi v2UserApi; + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Resource(name = "ftbPersonnelsRewardsService") + private BaseEasyExcelCommonListener baseEasyExcelCommonListener; + + @Resource + private RedisUtil redisUtil; + + @Resource + private PermissionHandling permissionHandling; + + public static final String R_REDIS_KEY = "ftb:personnels:rewards:punishment:%s"; + + @Resource + RewardApprovalMapper rewardApprovalMapper; + + @Resource + PunishmentsApprovalMapper punishmentsApprovalMapper; + + @Override + public synchronized void add(FtbPersonnelsRewardsPunishmentsAddDTO ftbPersonnelsRewardsPunishmentsAddDTO) { + // 校验名称重复 + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbPersonnelsRewardsPunishments::getEnableMark, 0); + query.eq(FtbPersonnelsRewardsPunishments::getType, ftbPersonnelsRewardsPunishmentsAddDTO.getType()); + query.eq(FtbPersonnelsRewardsPunishments::getRegularName, ftbPersonnelsRewardsPunishmentsAddDTO.getRegularName()); + if (baseMapper.selectCount(query) > 0) { + throw new RuntimeException("类型名称不允许重复"); + } + FtbPersonnelsRewardsPunishments ftbPersonnelsRewardsPunishments = FtbPersonnelsRewardsPunishmentsAddDTO.convertFtbPersonnelsRewardsPunishments(ftbPersonnelsRewardsPunishmentsAddDTO); + ftbPersonnelsRewardsPunishments.setLastModifyTime(new Date()); + ftbPersonnelsRewardsPunishments.setLastModifyUserId(UserProvider.getUser().getUserId()); + // 排序计算 + Integer sorting = baseMapper.queryTheMaximumValueOfSorting(); + ftbPersonnelsRewardsPunishments.setSort(sorting + 1); + baseMapper.insert(ftbPersonnelsRewardsPunishments); + } + + @Override + @Transactional + public void delete(String id) { + FtbPersonnelsRewardsPunishments ftbPersonnelsRewardsPunishments = baseMapper.selectById(id); + Integer sorting = baseMapper.queryTheMaximumValueOfSorting(); + // 所有小于该sort值的数据-1 + if (!ftbPersonnelsRewardsPunishments.getSort().equals(sorting)) { + baseMapper.updateSortValue(ftbPersonnelsRewardsPunishments.getSort()); + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getEnableMark, 1); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + baseMapper.update(new FtbPersonnelsRewardsPunishments(), updateWrapper); + } + + @Override + public void updateData(FtbPersonnelsRewardsPunishmentsUpdateDTO ftbPersonnelsRewardsPunishmentsUpdateDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbPersonnelsRewardsPunishmentsUpdateDTO.getId()); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getRegularName, ftbPersonnelsRewardsPunishmentsUpdateDTO.getRegularName()); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getMoney, ftbPersonnelsRewardsPunishmentsUpdateDTO.getMoney()); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getIsRecorded, ftbPersonnelsRewardsPunishmentsUpdateDTO.getIsRecorded()); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getType, ftbPersonnelsRewardsPunishmentsUpdateDTO.getType()); + baseMapper.update(new FtbPersonnelsRewardsPunishments(), updateWrapper); + } + + @Override + public void startAndStop(FtbPersonnelsRewardsPunishmentStartDTO ftbPersonnelsRewardsPunishmentStartDTO) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbPersonnelsRewardsPunishmentStartDTO.getId()); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getStatus, ftbPersonnelsRewardsPunishmentStartDTO.getStatus()); + baseMapper.update(new FtbPersonnelsRewardsPunishments(), updateWrapper); + } + + @Override + @Transactional + public void exchangeOrder(FtbPersonnelsRewardsPunishmentExchangeOrderDTO ftbPersonnelsRewardsPunishmentExchangeOrderDTO) { + // 点击数据,即拖动 + doExchangeOrder(ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getFirstExchangeInner()); + // 从下往上点 + if (ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getFirstExchangeInner().getSort() < + ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getSecondExchangeInner().getSort()) { + // 点击的数据做递增 + baseMapper.updateSortDownward(ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getFirstExchangeInner().getSort(), + ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getSecondExchangeInner().getSort(), + ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getFirstExchangeInner().getId()); + } else { + // 从上往下点 + baseMapper.updateSortDownwardS(ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getSecondExchangeInner().getSort(), + ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getFirstExchangeInner().getSort(), + ftbPersonnelsRewardsPunishmentExchangeOrderDTO.getFirstExchangeInner().getId()); + } + } + + @Override + public List listQuery(FtbPersonnelsRewardsPunishmentQueryDTO rewardsPunishmentQueryDTO) { + List result = baseMapper.listQuery(rewardsPunishmentQueryDTO); + List userIds = result.stream().map(FtbPersonnelsRewardsPunishmentQueryVO::getLastModifyUserId).collect(Collectors.toList()); + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, null); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return result; + } + Map userBoundVOMap = userPrimaryBoundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v)); + for (FtbPersonnelsRewardsPunishmentQueryVO ftbPersonnelsRewardsPunishmentQueryVO : result) { + UserBoundVO userBoundVO = userBoundVOMap.get(ftbPersonnelsRewardsPunishmentQueryVO.getLastModifyUserId()); + if (Objects.nonNull(userBoundVO)) { + ftbPersonnelsRewardsPunishmentQueryVO.setUserName(userBoundVO.getUserName()); + } + } + return result; + } + + @Override + public List listApproval(Integer type) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsRewardsPunishments::getEnableMark, 0); + queryWrapper.eq(FtbPersonnelsRewardsPunishments::getStatus, 0); + queryWrapper.eq(type != null,FtbPersonnelsRewardsPunishments::getType, type); + queryWrapper.orderByAsc(FtbPersonnelsRewardsPunishments::getSort); + List ftbPersonnelsRewardsPunishments = baseMapper.selectList(queryWrapper); + return ftbPersonnelsRewardsPunishments.stream().map(FtbPersonnelsRewardsPunishmentApprovalVO::convert).collect(Collectors.toList()); + } + + @Override + public List salaryReward(FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO) { + return this.baseMapper.queryTheTotalSalaryAndRewards(ftbPersonnelSalaryRewardDTO); + } + + @Override + public List totalSalaryPenalty(FtbPersonnelSalaryRewardDTO ftbPersonnelSalaryRewardDTO) { + return this.baseMapper.queryTheTotalSalaryAndPenaltys(ftbPersonnelSalaryRewardDTO); + } + + @Override + public FtbPersonnelsRewardsPunishmentApprovalVO listApprovalMoney(String id) { + FtbPersonnelsRewardsPunishments ftbPersonnelsRewardsPunishments = baseMapper.selectById(id); + return FtbPersonnelsRewardsPunishmentApprovalVO.convert(ftbPersonnelsRewardsPunishments); + } + + @Override + public BigDecimal salaryMetaDataQuery(FtbSalaryMetaDataQueryDto dto) { + BigDecimal value = baseMapper.salaryMetaDataQuery(dto); + return Optional.ofNullable(value) + .orElse(BigDecimal.ZERO); + } + + @Override + public List salaryBatchMetaDataQuery(FtbSalaryMetaDataQueryDto dto) { + return baseMapper.salaryBatchMetaDataQuery(dto); + } + + @Override + public Page employeeRewardAndPunishmentRecords(Page page, FtbEmployeeRewardRecordsQueryDTO queryDTO) { + UserInfo userInfo = UserProvider.getUser(); + if (!userInfo.getIsAdministrator()) { + List userIdsByUserId = permissionHandling.getUserIdsByUserId(userInfo.getUserId()); + if (userIdsByUserId != null && userIdsByUserId.isEmpty()) { + return page; + } + queryDTO.setUserIds(userIdsByUserId); + } + Page ftbEmployeeRewardRecordsVOPage = baseMapper.employeeRewardAndPunishmentRecords(page, queryDTO); + if (ftbEmployeeRewardRecordsVOPage.getRecords().isEmpty()) { + return ftbEmployeeRewardRecordsVOPage; + } + // 组织、岗位,操作人姓名 + List operatorsIds = ftbEmployeeRewardRecordsVOPage.getRecords().stream() + .map(FtbEmployeeRewardRecordsVO::getOperator) + .collect(Collectors.toList()); + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(operatorsIds, null); + // 检查远程调用状态 + if (userPrimaryBoundBatch == null) { + throw new RuntimeException("远程获取用户信息失败"); + } + Map operatorMaps = userPrimaryBoundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getUserName)); + ftbEmployeeRewardRecordsVOPage.getRecords().forEach(a -> { + a.setOperator(operatorMaps.getOrDefault(a.getOperator(), "--")); + }); + return ftbEmployeeRewardRecordsVOPage; + } + + @Override + public Map importData(InputStream inputStream) throws IOException { + EasyExcelUtils.universalImport(FtbEmploymentRewardExcelVO.class, + inputStream, + baseEasyExcelCommonListener, + 0,2); + String redisKey = getRedisKey(); + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已经过期,请重新导入!"); + } + FtbRewardsImportRedisBO importRedisBO = getRedisBO(redisKey); + HashMap map = Maps.newHashMap(); + map.put("normalData", importRedisBO.getNormalData().size()); + map.put("errorData", importRedisBO.getErrorData().size()); + map.put("data", importRedisBO); + return map; + } + + @Override + public void importDataClick(Integer type, String orgId) { + String redisKey = getRedisKey(); + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已经过期,请重新导入!"); + } + FtbRewardsImportRedisBO importRedisBO = getRedisBO(redisKey); + List normalData = importRedisBO.getNormalData(); + if (CollUtil.isEmpty(normalData)) { + throw new RuntimeException("当前暂无数据导入!"); + } + List list = new ArrayList<>(); + for (FtbEmploymentRewardExcelVO item : normalData){ + if (StringUtils.isNotEmpty(item.getCurrOrgId()) && orgId.equals(item.getCurrOrgId())){ + list.add(item); + }else { + list.add(item); + } + } + if (CollUtil.isEmpty(list) && !normalData.isEmpty()){ + throw new RuntimeException("当前选择的组织下暂无导入数据!"); + } + // 0奖励,1惩罚 + if (type == 0){ + List approvals = list.stream() + .filter(a -> a.getType().equals("奖励") ).map(FtbEmploymentRewardExcelVO::convertReward).collect(Collectors.toList()); + Db.saveBatch(approvals); + }else { + List approvals = list.stream() + .filter(a -> a.getType().equals("乐捐")).map(FtbEmploymentRewardExcelVO::convertDonate).collect(Collectors.toList()); + Db.saveBatch(approvals); + } + + } + + @Override + public void saveDataToRedis(List nomarlData, List errorData) { + String redisKey = getRedisKey(); + redisUtil.insert(redisKey, JSON.toJSONString(new FtbRewardsImportRedisBO(nomarlData, errorData))); + // 30分钟自动过期 + redisUtil.expire(redisKey, 30 * 60); + } + + @Override + public void deleteUserInfo(String id, Integer type) { + if (type == 0) { + LambdaQueryWrapper updateWrapper = Wrappers.lambdaQuery(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.eq(RewardApproval::getStatus,4); + rewardApprovalMapper.delete(updateWrapper); + }else { + LambdaQueryWrapper updateWrapper = Wrappers.lambdaQuery(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.eq(PunishmentsApproval::getStatus,4); + punishmentsApprovalMapper.delete(updateWrapper); + } + } + + @Override + public void exportErrData(HttpServletResponse response) throws IOException { + String redisKey = getRedisKey(); + FtbRewardsImportRedisBO importRedisBO = getRedisBO(redisKey); + assert importRedisBO != null; + List errorData = importRedisBO.getErrorData(); + String fileName = "导入奖惩员工错误信息"; + EasyExcelUtils.exportExcel(response, fileName, errorData, FtbEmploymentRecordsExcelERRVO.class); + } + + @Override + public FtbEmployeeRewardRecordsVO queryDetail(String id) { + return baseMapper.queryDetail(id); + } + + @Nullable + private FtbRewardsImportRedisBO getRedisBO(String redisKey) { + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已经过期,请重新导入!"); + } + Object utilString = redisUtil.getString(redisKey); + return JSON.parseObject(utilString.toString(), FtbRewardsImportRedisBO.class); + } + private String getRedisKey() { + String loginUserId = UserProvider.getLoginUserId(); + return String.format(R_REDIS_KEY, loginUserId); + } + + private void doExchangeOrder(FtbPersonnelsRewardsPunishmentExchangeOrderDTO.ExchangeInner exchangeInner) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, exchangeInner.getId()); + updateWrapper.set(FtbPersonnelsRewardsPunishments::getSort, exchangeInner.getSort()); + baseMapper.update(null, updateWrapper); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceImpl.java new file mode 100644 index 0000000..22214bc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceImpl.java @@ -0,0 +1,387 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.config.StringToDateConverter; +import jnpf.entity.StoreEntity; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.bo.FtbRosterImportHeadRuleBO; +import jnpf.model.personnels.bo.FtbRosterImportRedisBO; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.roster.FtbRosterAttributesVO; +import jnpf.model.personnels.vo.roster.FtbRosterCategoryVO; +import jnpf.model.personnels.vo.roster.FtbRosterPageVO; +import jnpf.permission.dto.CheckOrgPositionGradesBoundDTO; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormTypeMapper; +import jnpf.personnels.service.FtbPersonnelsBlacklistService; +import jnpf.personnels.service.FtbPersonnelsRosterValidService; +import jnpf.personnels.utils.CacheExcelUtils; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service("FtbPersonnelsRosterValidServiceOld") +public class FtbPersonnelsRosterValidServiceImpl implements FtbPersonnelsRosterValidService { + + @Autowired + private RedisUtil redisUtil; + + + @Autowired + private CacheExcelUtils cacheExcelUtils; + + @Autowired + private FtbPersonnelsRegistrationFormTypeMapper ftbPersonnelsRegistrationFormTypeMapper; + + @Autowired + private FtbPersonnelsRegistrationFormFieldMapper ftbPersonnelsRegistrationFormFieldMapper; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionMapper ftbPersonnelsRegistrationFormFieldOptionMapper; + + @Resource + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Override + public Map headerVerification(String sheetName, Map head) { + // 登记表配置 + LambdaQueryWrapper formTypeLambdaQueryWrapper = Wrappers.lambdaQuery(); + formTypeLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0); + formTypeLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getName, sheetName); + formTypeLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getStatus, 0); + formTypeLambdaQueryWrapper.last("limit 1"); + FtbPersonnelsRegistrationFormType ftbPersonnelsRegistrationFormType = ftbPersonnelsRegistrationFormTypeMapper.selectOne(formTypeLambdaQueryWrapper); + // 登记表属性 + List registrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList( + FtbRosterImportConstants.getRegistrationFormFieldQueryWrapper(ftbPersonnelsRegistrationFormType.getId())); + // 额外项姓名和手机号码 + FtbRosterImportConstants.nameAndMobileNumberAdded(registrationFormFields, ftbPersonnelsRegistrationFormType.getName()); + if (head.size() != registrationFormFields.size()) { + throw new RuntimeException("文件中sheet名为" + sheetName + "存在表头不匹配,请重新下载最新模板导入"); + } + Map result = new HashMap<>(head.size()); + head.forEach((k, v) -> { + FtbRosterImportHeadRuleBO ftbRosterImportHeadRuleBO = new FtbRosterImportHeadRuleBO(); + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormField = registrationFormFields.stream().filter(f -> { + if (v.contains("*")) { + return f.getName().equals(v.substring(v.indexOf("*") + 1)); + } + return f.getName().equals(v); + }).findFirst().orElse(null); + if (ftbPersonnelsRegistrationFormField == null) { + throw new RuntimeException("文件中sheet名为" + sheetName + "存在表头(" + v + ")不匹配,请重新下载最新模板导入"); + } + ftbRosterImportHeadRuleBO.setHeadName(v); + ftbRosterImportHeadRuleBO.setVerificationRule(ftbPersonnelsRegistrationFormField.getRules()); + ftbRosterImportHeadRuleBO.setRequired(ftbPersonnelsRegistrationFormField.getIsNeedFill() == 1); + // 校验字符仅限单行文本和多行文本 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && + (ftbPersonnelsRegistrationFormField.getType() == 0 || ftbPersonnelsRegistrationFormField.getType() == 1)) { + ftbRosterImportHeadRuleBO.setVerificationCharacter(ftbPersonnelsRegistrationFormField.getLimits()); + } + ftbRosterImportHeadRuleBO.setOptionType(ftbPersonnelsRegistrationFormField.getOptionType()); + ftbRosterImportHeadRuleBO.setFormTypeId(StringUtils.hasText(ftbPersonnelsRegistrationFormField.getId()) ? + ftbPersonnelsRegistrationFormType.getId() : null); + ftbRosterImportHeadRuleBO.setFormFieldId(ftbPersonnelsRegistrationFormField.getId()); + // 日期 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && ftbPersonnelsRegistrationFormField.getType() == 4) { + ftbRosterImportHeadRuleBO.setOptionType(100); + } + // 兼容自定义登记表字段单选和多选未设置选项情况 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && + ftbPersonnelsRegistrationFormField.getSystemType() == 1 && + (ftbPersonnelsRegistrationFormField.getType() == 2 || ftbPersonnelsRegistrationFormField.getType() == 3)) { + ftbRosterImportHeadRuleBO.setOptionType(1); + } + // 选项表中选项 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && + (ftbPersonnelsRegistrationFormField.getOptionType() == 1 || + ftbPersonnelsRegistrationFormField.getType() == 2 || ftbPersonnelsRegistrationFormField.getType() == 3)) { + LambdaQueryWrapper formFieldOptionLambdaQueryWrapper = Wrappers.lambdaQuery(); + formFieldOptionLambdaQueryWrapper.select(FtbPersonnelsRegistrationFormFieldOption::getId, FtbPersonnelsRegistrationFormFieldOption::getName); + formFieldOptionLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + formFieldOptionLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, ftbPersonnelsRegistrationFormField.getId()); + List ftbPersonnelsRegistrationFormFieldOptions = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList(formFieldOptionLambdaQueryWrapper); + ftbRosterImportHeadRuleBO.setVerificationOptions( + ftbPersonnelsRegistrationFormFieldOptions.stream() + .collect(Collectors.toMap( + FtbPersonnelsRegistrationFormFieldOption::getName, + FtbPersonnelsRegistrationFormFieldOption::getId, + (s, a) -> a))); + } + result.put(k, ftbRosterImportHeadRuleBO); + }); + return result; + } + + @Override + public void dataValidation(AnalysisContext context, Map head, Map data, + FtbRosterCategoryVO normalData, FtbRosterCategoryVO errorData) { + StringBuilder errorBuilder = new StringBuilder(); + FtbRosterPageVO ftbRosterPageVO = new FtbRosterPageVO(); + ftbRosterPageVO.setRowMark(context.readRowHolder().getRowIndex()); + List ftbRosterAttributesVOS = new ArrayList<>(); + data.forEach((k, v) -> { + FtbRosterAttributesVO ftbRosterAttributesVO = new FtbRosterAttributesVO(); + ftbRosterAttributesVO.setLineMark(context.readRowHolder().getRowIndex()); + ftbRosterAttributesVO.setColumnLabel(k); + ftbRosterAttributesVO.setAttributeValue(v); + FtbRosterImportHeadRuleBO ftbRosterImportHeadRuleBO = head.get(k); + // 数据超出表头 + if (ftbRosterImportHeadRuleBO == null) { + errorBuilder.append("所填数据无对应表头标题;"); + return; + } + ftbRosterAttributesVO.setAttributeName(ftbRosterImportHeadRuleBO.getHeadName()); + ftbRosterAttributesVO.setFormTypeId(ftbRosterImportHeadRuleBO.getFormTypeId()); + ftbRosterAttributesVO.setFormFieldId(ftbRosterImportHeadRuleBO.getFormFieldId()); + // 必填项没必填 + if (ftbRosterImportHeadRuleBO.getRequired() && StrUtil.isBlank(v)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()).append("字段内容必填;"); + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + return; + } + if (StrUtil.isBlank(v)) { + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + return; + } + // 校验字符 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getVerificationCharacter()) && + ftbRosterImportHeadRuleBO.getVerificationCharacter() > 0 + && v.length() > ftbRosterImportHeadRuleBO.getVerificationCharacter()) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容不能超过") + .append(ftbRosterImportHeadRuleBO.getVerificationCharacter()) + .append("字符;"); + } + // 校验规则 + if (StrUtil.isNotBlank(ftbRosterImportHeadRuleBO.getVerificationRule())) { + boolean regularCheck = Pattern.matches(ftbRosterImportHeadRuleBO.getVerificationRule(), v); + if (!regularCheck) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容格式有误;"); + } + } + // 校验匹配选项 1、选项表中选项 2、省市区选项 3、组织选项 4、岗位选项 5、职等选项 6、门店选项 7、直接主管选项 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 1) { + // 区分单选和多选 + String multipleSelectionContent = Stream.of(v.split(StringPool.COMMA)).map(a -> { + if (!ftbRosterImportHeadRuleBO.getVerificationOptions().containsKey(a)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容").append(a).append("不在系统选项范围内;"); + } + return ftbRosterImportHeadRuleBO.getVerificationOptions().get(a); + }).filter(StrUtil::isNotBlank).collect(Collectors.joining(StringPool.COMMA)); + ftbRosterAttributesVO.setAttributeValueId(multipleSelectionContent); + } + // 日期校验 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 100) { + String date = StringToDateConverter.dateFormatCheck(v); + if (StrUtil.isBlank(date)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容格式有误;"); + } else { + ftbRosterAttributesVO.setAttributeValue(date); + } + } + // 合同类型 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 8) { + String contractTypeName = cacheExcelUtils.doContractTypeName(v); + if (StrUtil.isBlank(contractTypeName)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;"); + } else { + ftbRosterAttributesVO.setAttributeValueId(contractTypeName); + } + } + + // 手机号码和姓名校验 + if ("*手机号码".equals(ftbRosterImportHeadRuleBO.getHeadName())) { +// if (cacheExcelUtils.doCheckPhone(v)) { +// errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) +// .append("字段").append(v).append("在系统内已经存在;"); +// } + + // 手机号,离职黑名单校验 + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(v)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("已被拉入黑名单;"); + } + + // 校验手机号是否重复 + normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(FtbRosterImportConstants.phonePredicate) + .filter(c -> Objects.equals(v, c.getAttributeValue())).findFirst() + .ifPresent(rosterAttributesVO -> errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在第").append(rosterAttributesVO.getLineMark()).append("行存在重复;")); + } + // 门店校验 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 6) { + StoreEntity storeEntity = cacheExcelUtils.doStoreEntity(v); + if (storeEntity == null) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;"); + } else { + ftbRosterAttributesVO.setAttributeValueId(storeEntity.getId()); + } + } + // 角色校验 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 9) { + FtbRosterAttributesVO organizationPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.organizationPredicate).findFirst().orElse(null); + if (organizationPredicate != null) { + String roleIds = cacheExcelUtils.doCheckRoleInfoVO(organizationPredicate.getAttributeValue(), v); + if (!StringUtils.hasText(roleIds)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;不存在组织和角色绑定关系"); + } else { + ftbRosterAttributesVO.setAttributeValueId(roleIds); + } + } + } + // 考勤组校验 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 10) { + FtbRosterAttributesVO organizationPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.organizationPredicate).findFirst().orElse(null); + if (organizationPredicate != null) { + String attendanceGroupId = cacheExcelUtils.doCheckAttendanceGroup(organizationPredicate.getAttributeValue(), v); + if (!StringUtils.hasText(attendanceGroupId)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;不存在组织和考勤组绑定关系"); + } else { + ftbRosterAttributesVO.setAttributeValueId(attendanceGroupId); + } + } + } + // 直属主管校验 + if ("入职直属主管ID".equals(ftbRosterImportHeadRuleBO.getHeadName())) { + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = cacheExcelUtils.doVerificationByDirectSupervisor(v); + if (ftbPersonnelsStaffRoster == null) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;"); + } else { + // 手机号 + FtbRosterAttributesVO phoneFtbRosterAttributesVO = normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst() + .orElseGet(FtbRosterAttributesVO::new); + // 直属主管校验 + if (StaffWorkerStatus.PRE_TRIAL.getCode().equals(ftbPersonnelsStaffRoster.getWorkerStatus())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(",该用户处于试岗期"); + } else if (StaffWorkerStatus.RESIGNED.getCode().equals(ftbPersonnelsStaffRoster.getWorkerStatus())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(",该用户处于已经离职"); + } else if (ftbPersonnelsStaffRoster.getPhone().equals(phoneFtbRosterAttributesVO.getAttributeValue())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(",直属主管不能是自己"); + } + FtbRosterAttributesVO organizationPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.organizationPredicate).findFirst().orElse(null); + if (organizationPredicate != null) { +// String nameAndUserId = cacheExcelUtils.checkOrgNameAndUserId(organizationPredicate.getAttributeValue(), ftbPersonnelsStaffRoster.getUserId()); +// if (StrUtil.isBlank(nameAndUserId)) { +// errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) +// .append("字段").append(v).append("不在系统内;"); +// } else { +// for (FtbRosterAttributesVO rosterAttributesVO : ftbRosterAttributesVOS) { +// if ("入职直属主管".equals(rosterAttributesVO.getAttributeName())) { +// rosterAttributesVO.setAttributeValueId(nameAndUserId); +// break; +// } +// } +// } + } + } + } + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + }); + if ("工作信息".equals(context.readSheetHolder().getSheetName())) { + if (StrUtil.isBlank(errorBuilder.toString())) { + FtbRosterAttributesVO organizationPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.organizationPredicate).findFirst().orElse(null); + FtbRosterAttributesVO postPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.postPredicate).findFirst().orElse(null); + FtbRosterAttributesVO gradePredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.gradePredicate).findFirst().orElse(null); + if (Objects.nonNull(organizationPredicate) && Objects.nonNull(postPredicate) && Objects.nonNull(gradePredicate)) { + CheckOrgPositionGradesBoundDTO checkOrgPositionGradesBoundDTO = new CheckOrgPositionGradesBoundDTO(); + checkOrgPositionGradesBoundDTO.setOrgName(organizationPredicate.getAttributeValue()); + checkOrgPositionGradesBoundDTO.setPositionName(postPredicate.getAttributeValue()); + checkOrgPositionGradesBoundDTO.setPositionGradesName(gradePredicate.getAttributeValue()); +// CheckOrgPositionGradesBoundDTO resultCheck = cacheExcelUtils.doCheckOrgPositionGradesBoundDTO(checkOrgPositionGradesBoundDTO); +// // 所属组织-岗位-职等校验 +// if (Objects.isNull(resultCheck)) { +// errorBuilder.append("所填写的组织名称、岗位名称、职等名称之间不存在绑定关系;"); +// } else { +// // 校验用户组织权限适用范围,返回null则为超级管理员 +// List organizationIdDataPermissions = cacheExcelUtils.doPersonnelOrganizationIdDataPermissions(); +// if (Objects.nonNull(organizationIdDataPermissions) && !organizationIdDataPermissions.contains(resultCheck.getOrgId())) { +// errorBuilder.append("导入组织与当前登录人所选权限组织不匹配;"); +// } +// for (FtbRosterAttributesVO rosterAttributesVO : ftbRosterAttributesVOS) { +// if (FtbRosterImportConstants.organizationPredicate.test(rosterAttributesVO)) { +// rosterAttributesVO.setAttributeValueId(resultCheck.getOrgId()); +// } +// if (FtbRosterImportConstants.postPredicate.test(rosterAttributesVO)) { +// rosterAttributesVO.setAttributeValueId(resultCheck.getPositionId()); +// } +// if (FtbRosterImportConstants.gradePredicate.test(rosterAttributesVO)) { +// rosterAttributesVO.setAttributeValueId(resultCheck.getPositionGradesId()); +// } +// } +// } + } + // 实际入职日期校验是否大于今天 + FtbRosterAttributesVO actualDateOfJoining = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.actualDateOfJoining).findFirst().orElse(null); + if (Objects.nonNull(actualDateOfJoining)) { + String actualDateOfJoiningAttributeValue = actualDateOfJoining.getAttributeValue(); + Date date = StringToDateConverter.dateFormatParser(actualDateOfJoiningAttributeValue); + if (date.after(new Date())) { + errorBuilder.append("实际入职日期不能大于当前日期;"); + } + } + } + } + // 有错误信息 + String errorMsg = errorBuilder.toString(); + if (StrUtil.isNotBlank(errorMsg)) { + // 手动加错误,逆向导出 + FtbRosterAttributesVO ftbRosterAttributesVO = new FtbRosterAttributesVO(); + ftbRosterAttributesVO.setLineMark(context.readRowHolder().getRowIndex()); + ftbRosterAttributesVO.setColumnLabel(errorData.getHead().size() + 1); + ftbRosterAttributesVO.setAttributeName(FtbRosterImportConstants.ERROR_REMARKS); + ftbRosterAttributesVO.setAttributeValue(errorMsg); + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + ftbRosterPageVO.setFtbRosterAttributesVOS(ftbRosterAttributesVOS); + errorData.getFtbRosterPageVOS().add(ftbRosterPageVO); + } else { + ftbRosterPageVO.setFtbRosterAttributesVOS(ftbRosterAttributesVOS); + normalData.getFtbRosterPageVOS().add(ftbRosterPageVO); + } + } + + @Override + public void saveDataToRedis(AnalysisContext context, FtbRosterCategoryVO normalData, FtbRosterCategoryVO errorData) { + String loginUserId = UserProvider.getLoginUserId(); + String redisKey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_SHEET_REDIS_VARIABLES, loginUserId); + redisUtil.insertHash(redisKey, context.readSheetHolder().getSheetName(), JSON.toJSONString(new FtbRosterImportRedisBO(normalData, errorData))); + // 30分钟自动过期 + redisUtil.expire(redisKey, 30 * 60); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceNewImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceNewImpl.java new file mode 100644 index 0000000..05c1e5e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRosterValidServiceNewImpl.java @@ -0,0 +1,555 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationResponse; +import jnpf.config.StringToDateConverter; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.bo.FtbRosterImportHeadRuleBO; +import jnpf.model.personnels.bo.FtbRosterImportRedisBO; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.roster.FtbRosterAttributesVO; +import jnpf.model.personnels.vo.roster.FtbRosterCategoryVO; +import jnpf.model.personnels.vo.roster.FtbRosterPageVO; +import jnpf.permission.dto.v2.user.QueryUserBoundDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.api.OrganizeAndPositionAndGradeVO; +import jnpf.permission.vo.v2.user.api.OrganizeAndStoreTeamVO; +import jnpf.permission.vo.v2.user.api.OrganizeAndUserVO; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.service.FtbPersonnelsBlacklistService; +import jnpf.personnels.service.FtbPersonnelsRosterValidService; +import jnpf.personnels.utils.CacheExcelUtils; +import jnpf.personnels.utils.PersonnelIdCardVerificationUtils; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service("FtbPersonnelsRosterValidServiceNew") +public class FtbPersonnelsRosterValidServiceNewImpl implements FtbPersonnelsRosterValidService { + + @Autowired + private RedisUtil redisUtil; + + @Autowired + private CacheExcelUtils cacheExcelUtils; + + @Autowired + private FtbPersonnelsRegistrationFormFieldMapper ftbPersonnelsRegistrationFormFieldMapper; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataMapper ftbPersonnelsStaffRegistrationFormDataMapper; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionMapper ftbPersonnelsRegistrationFormFieldOptionMapper; + + @Resource + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Resource + private PersonnelIdCardVerificationUtils personnelIdCardVerificationUtils; + + @Override + public Map headerVerification(String sheetName, Map head) { + LambdaQueryWrapper queryWrapper = FtbRosterImportConstants.getRegistrationFormFieldQueryWrapper(null); + List registrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList(queryWrapper); + // 额外项排除 + FtbRosterImportConstants.nameAndMobileNumberAddedNew(registrationFormFields, "3"); + // 校验必填项是否勾选一致 + long first = registrationFormFields.stream().filter(f -> f.getIsNeedFill() == 1).count(); + long second = head.values().stream().filter(a -> Objects.nonNull(a) && a.contains("*")).count(); + if (first != second) { + throw new RuntimeException("表头必填项与配置项不一致,请重新勾选配置项下载最新模板导入"); + } + Map result = new HashMap<>(head.size()); + head.forEach((k, v) -> { + FtbRosterImportHeadRuleBO ftbRosterImportHeadRuleBO = new FtbRosterImportHeadRuleBO(); + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormField = registrationFormFields.stream().filter(f -> { + if (StrUtil.isNotBlank(v) && v.contains("*")) { + return f.getName().equals(v.substring(v.indexOf("*") + 1)); + } + return f.getName().equals(v); + }).findFirst().orElse(null); + if (ftbPersonnelsRegistrationFormField == null) { + throw new RuntimeException("表头不存在,请重新勾选配置项下载最新模板导入"); + } + ftbRosterImportHeadRuleBO.setHeadName(v); + ftbRosterImportHeadRuleBO.setVerificationRule(ftbPersonnelsRegistrationFormField.getRules()); + ftbRosterImportHeadRuleBO.setRequired(ftbPersonnelsRegistrationFormField.getIsNeedFill() == 1); + // 校验字符仅限单行文本和多行文本 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && + (ftbPersonnelsRegistrationFormField.getType() == 0 || ftbPersonnelsRegistrationFormField.getType() == 1)) { + ftbRosterImportHeadRuleBO.setVerificationCharacter(ftbPersonnelsRegistrationFormField.getLimits()); + } + ftbRosterImportHeadRuleBO.setOptionType(ftbPersonnelsRegistrationFormField.getOptionType()); + ftbRosterImportHeadRuleBO.setFormTypeId(ftbPersonnelsRegistrationFormField.getFormTypeId()); + ftbRosterImportHeadRuleBO.setFormFieldId(ftbPersonnelsRegistrationFormField.getId()); + // 日期 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && ftbPersonnelsRegistrationFormField.getType() == 4) { + ftbRosterImportHeadRuleBO.setOptionType(100); + } + // 兼容自定义登记表字段单选和多选未设置选项情况 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && + ftbPersonnelsRegistrationFormField.getSystemType() == 1 && + (ftbPersonnelsRegistrationFormField.getType() == 2 || ftbPersonnelsRegistrationFormField.getType() == 3)) { + ftbRosterImportHeadRuleBO.setOptionType(1); + } + // 选项表中选项 + if (Objects.nonNull(ftbPersonnelsRegistrationFormField.getType()) && + (ftbPersonnelsRegistrationFormField.getOptionType() == 1 || + ftbPersonnelsRegistrationFormField.getType() == 2 || ftbPersonnelsRegistrationFormField.getType() == 3)) { + LambdaQueryWrapper formFieldOptionLambdaQueryWrapper = Wrappers.lambdaQuery(); + formFieldOptionLambdaQueryWrapper.select(FtbPersonnelsRegistrationFormFieldOption::getId, FtbPersonnelsRegistrationFormFieldOption::getName); + formFieldOptionLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0); + formFieldOptionLambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, ftbPersonnelsRegistrationFormField.getId()); + List ftbPersonnelsRegistrationFormFieldOptions = ftbPersonnelsRegistrationFormFieldOptionMapper.selectList(formFieldOptionLambdaQueryWrapper); + ftbRosterImportHeadRuleBO.setVerificationOptions( + ftbPersonnelsRegistrationFormFieldOptions.stream() + .collect(Collectors.toMap( + FtbPersonnelsRegistrationFormFieldOption::getName, + FtbPersonnelsRegistrationFormFieldOption::getId, + (s, a) -> a))); + } + result.put(k, ftbRosterImportHeadRuleBO); + }); + return result; + } + + @Override + public void dataValidation(AnalysisContext context, Map head, Map data, + FtbRosterCategoryVO normalData, FtbRosterCategoryVO errorData) { + StringBuilder errorBuilder = new StringBuilder(); + FtbRosterPageVO ftbRosterPageVO = new FtbRosterPageVO(); + ftbRosterPageVO.setRowMark(context.readRowHolder().getRowIndex()); + List ftbRosterAttributesVOS = new ArrayList<>(); + // 数据补全,无法识别多行为空 + if (head.size()-1 > data.size()-1) { + int dataSize = data.size()-1; + for (int i=1;i<=head.size()-1-dataSize;i++) { + data.put(dataSize+i, ""); + } + } + data.forEach((k, v) -> { + FtbRosterAttributesVO ftbRosterAttributesVO = new FtbRosterAttributesVO(); + ftbRosterAttributesVO.setLineMark(context.readRowHolder().getRowIndex()); + ftbRosterAttributesVO.setColumnLabel(k); + ftbRosterAttributesVO.setAttributeValue(v); + FtbRosterImportHeadRuleBO ftbRosterImportHeadRuleBO = head.get(k); + // 数据超出表头 + if (ftbRosterImportHeadRuleBO == null) { + errorBuilder.append("所填数据无对应表头标题;"); + return; + } + ftbRosterAttributesVO.setAttributeName(ftbRosterImportHeadRuleBO.getHeadName()); + ftbRosterAttributesVO.setFormTypeId(ftbRosterImportHeadRuleBO.getFormTypeId()); + ftbRosterAttributesVO.setFormFieldId(ftbRosterImportHeadRuleBO.getFormFieldId()); + // 必填项没必填 + if (ftbRosterImportHeadRuleBO.getRequired() && StrUtil.isBlank(v)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()).append("字段内容必填;"); + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + return; + } + if (StrUtil.isBlank(v)) { + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + return; + } + // 校验字符 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getVerificationCharacter()) && + ftbRosterImportHeadRuleBO.getVerificationCharacter() > 0 + && v.length() > ftbRosterImportHeadRuleBO.getVerificationCharacter()) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容不能超过") + .append(ftbRosterImportHeadRuleBO.getVerificationCharacter()) + .append("字符;"); + } + // 校验规则 + if (StrUtil.isNotBlank(ftbRosterImportHeadRuleBO.getVerificationRule())) { + boolean regularCheck = Pattern.matches(ftbRosterImportHeadRuleBO.getVerificationRule(), v); + if (!regularCheck) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容格式有误;"); + } + } + // 校验匹配选项 1、选项表中选项 2、省市区选项 3、组织选项 4、岗位选项 5、职等选项 6、门店选项 7、直接主管选项 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 1) { + // 区分单选和多选 + String multipleSelectionContent = Stream.of(v.split(StringPool.COMMA)).map(a -> { + if (!ftbRosterImportHeadRuleBO.getVerificationOptions().containsKey(a)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容").append(a).append("不在系统选项范围内;"); + } + return ftbRosterImportHeadRuleBO.getVerificationOptions().get(a); + }).filter(StrUtil::isNotBlank).collect(Collectors.joining(StringPool.COMMA)); + ftbRosterAttributesVO.setAttributeValueId(multipleSelectionContent); + } + // 日期校验 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 100) { + String date = StringToDateConverter.dateFormatCheck(v); + if (StrUtil.isBlank(date)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段内容格式有误;"); + } else { + // 校验出生日期 + if ("出生日期".equals(ftbRosterImportHeadRuleBO.getHeadName())) { + // 出生日期需要小于今天 + Date now = new Date(); + if (DateUtil.parse(date).isAfter(now) || date.equals(DateUtil.format(now, "yyyy-MM-dd"))) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()).append("字段内容不能大于今天;"); + } + } else if ("endHealthDate".equals(ftbRosterImportHeadRuleBO.getFormFieldId())) { + // 结束有效日期不能小于今天 + Date now = new Date(); + if (DateUtil.parse(date).isBefore(now) || date.equals(DateUtil.format(now, "yyyy-MM-dd"))) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()).append("字段内容不能小于今天;"); + } + // 校验健康证结束有效日期不能小于健康证开始有效日期 + ftbRosterAttributesVOS.stream() + .filter(a->"*开始有效日期".contains(a.getAttributeName())) + .findFirst().ifPresent(a -> { + if (StrUtil.isNotBlank(v)) { + if (DateUtil.parse(date).isBefore(DateUtil.parse(a.getAttributeValue()))) { + errorBuilder.append("健康证结束有效日期不能小于健康证开始有效日期"); + } + } + }); + } + ftbRosterAttributesVO.setAttributeValue(date); + } + } + // 合同类型 + if (Objects.nonNull(ftbRosterImportHeadRuleBO.getOptionType()) && ftbRosterImportHeadRuleBO.getOptionType() == 8) { + String contractTypeName = cacheExcelUtils.doContractTypeName(v); + if (StrUtil.isBlank(contractTypeName)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;"); + } else { + ftbRosterAttributesVO.setAttributeValueId(contractTypeName); + } + } + // 手机号码和姓名校验 + if ("*手机号码".equals(ftbRosterImportHeadRuleBO.getHeadName())) { + // 手机号,离职黑名单校验 + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(v)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("已被拉入黑名单;"); + } + + // 校验手机号是否重复 + normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(FtbRosterImportConstants.phonePredicate) + .filter(c -> Objects.equals(v, c.getAttributeValue())).findFirst() + .ifPresent(rosterAttributesVO -> errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在第").append(rosterAttributesVO.getLineMark()).append("行存在重复;")); + } + // 身份证号校验 + if ("*身份证号码".contains(ftbRosterImportHeadRuleBO.getHeadName())) { + normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(a -> "*身份证号码".contains(a.getAttributeName())) + .filter(c -> Objects.equals(v, c.getAttributeValue())).findFirst() + .ifPresent(rosterAttributesVO -> errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在第").append(rosterAttributesVO.getLineMark()).append("行存在重复;")); + ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst().ifPresent(a -> { + if (StrUtil.isNotBlank(v)) { + if (ftbPersonnelsStaffRegistrationFormDataMapper.verifyWhetherTheDataExistsWithStaff(a.getAttributeValue(), "idCardNum", v) > 0) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在系统内已存在;"); + } + // 校验身份证号码出生日期是未来时间 + boolean regularCheck = Pattern.matches(ftbRosterImportHeadRuleBO.getVerificationRule(), v); + if (regularCheck) { + DateTime ageByIdCard = IdcardUtil.getBirthDate(v); + if (ageByIdCard.isAfter(new Date())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("值不正确;"); + } + } + } + }); + // 姓名+身份证号校验 + ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.userNamePredicate) + .findFirst().ifPresent(a -> { + if (StrUtil.isNotBlank(v) && StrUtil.isNotBlank(a.getAttributeValue())) { + IdCardVerificationResponse idCardVerificationResponse = personnelIdCardVerificationUtils.idCardVerification(v, + a.getAttributeValue(), UserProvider.getUser().getTenantId()); + if (Objects.nonNull(idCardVerificationResponse) && !"0".equals(idCardVerificationResponse.getResult())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(idCardVerificationResponse.getDescription()).append(";"); + } + } + }); + } + // 校验银行卡号 + if ("*银行卡号".contains(ftbRosterImportHeadRuleBO.getHeadName())) { + normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(a -> "*银行卡号".contains(a.getAttributeName())) + .filter(c -> Objects.equals(v, c.getAttributeValue())).findFirst() + .ifPresent(rosterAttributesVO -> errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在第").append(rosterAttributesVO.getLineMark()).append("行存在重复;")); + ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst().ifPresent(a -> { + if (StrUtil.isNotBlank(v)) { + if (ftbPersonnelsStaffRegistrationFormDataMapper.verifyWhetherTheDataExists(a.getAttributeValue(), "bankCardNo", v) > 0) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在系统内已存在;"); + } + } + }); + } + // 校验公积金号 + if ("*个人公积金帐号".contains(ftbRosterImportHeadRuleBO.getHeadName())) { + normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(a -> "*个人公积金帐号".contains(a.getAttributeName())) + .filter(c -> Objects.equals(v, c.getAttributeValue())).findFirst() + .ifPresent(rosterAttributesVO -> errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在第").append(rosterAttributesVO.getLineMark()).append("行存在重复;")); + ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst().ifPresent(a -> { + if (StrUtil.isNotBlank(v)) { + if (ftbPersonnelsStaffRegistrationFormDataMapper.verifyWhetherTheDataExists(a.getAttributeValue(), "providentFundAccount", v) > 0) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在系统内已存在;"); + } + } + }); + } + // 校验社保账号 + if ("*个人社保帐号".contains(ftbRosterImportHeadRuleBO.getHeadName())) { + normalData.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(a -> "*个人社保帐号".contains(a.getAttributeName())) + .filter(c -> Objects.equals(v, c.getAttributeValue())).findFirst() + .ifPresent(rosterAttributesVO -> errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在第").append(rosterAttributesVO.getLineMark()).append("行存在重复;")); + ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst().ifPresent(a -> { + if (StrUtil.isNotBlank(v)) { + if (ftbPersonnelsStaffRegistrationFormDataMapper.verifyWhetherTheDataExists(a.getAttributeValue(), "socialAccount", v) > 0) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("在系统内已存在;"); + } + } + }); + } + // 直属主管校验 + if ("直属主管ID".equals(ftbRosterImportHeadRuleBO.getHeadName())) { + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = cacheExcelUtils.doVerificationByDirectSupervisor(v); + if (ftbPersonnelsStaffRoster == null) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;"); + } else { + // 手机号 + FtbRosterAttributesVO phoneFtbRosterAttributesVO = ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst() + .orElseGet(FtbRosterAttributesVO::new); + // 直属主管校验 + if (StaffWorkerStatus.PRE_TRIAL.getCode().equals(ftbPersonnelsStaffRoster.getWorkerStatus())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(",该用户处于试岗期"); + } else if (StaffWorkerStatus.RESIGNED.getCode().equals(ftbPersonnelsStaffRoster.getWorkerStatus())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(",该用户处于已经离职"); + } else if (ftbPersonnelsStaffRoster.getPhone().equals(phoneFtbRosterAttributesVO.getAttributeValue())) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append(",直属主管不能是自己"); + } + FtbRosterAttributesVO organizationPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.organizationPredicate).findFirst().orElse(null); + if (organizationPredicate != null) { + OrganizeAndUserVO organizeAndUserVO = cacheExcelUtils.checkOrgNameAndUserId(organizationPredicate.getAttributeValue(), ftbPersonnelsStaffRoster.getUserId()); + if (Objects.isNull(organizeAndUserVO)) { + errorBuilder.append(ftbRosterImportHeadRuleBO.getHeadName()) + .append("字段").append(v).append("不在系统内;"); + } else { + ftbRosterAttributesVO.setAttributeValueId(organizeAndUserVO.getUserId()); + } + } + } + } + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + }); + if (StrUtil.isBlank(errorBuilder.toString())) { + // 手机号 + FtbRosterAttributesVO phoneFtbRosterAttributesVO = ftbRosterAttributesVOS.stream() + .filter(FtbRosterImportConstants.phonePredicate) + .findFirst() + .orElseGet(FtbRosterAttributesVO::new); + UserBoundVO userBoundVO = cacheExcelUtils.doCheckPhoneExist(phoneFtbRosterAttributesVO.getAttributeValue()); + // 不能互为直属主管 A->B,B->A,则A->A + if (Objects.nonNull(userBoundVO)) { + ftbRosterAttributesVOS.stream().filter(a -> "入职直属主管ID".equals(a.getAttributeName())).findFirst().ifPresent(a -> { + UserBoundVO secondUserBoundVO = cacheExcelUtils.doQueryDirectlyUnderTheSupervisor(a.getAttributeValueId()); + boolean isSameUserBound = Objects.nonNull(secondUserBoundVO) && + userBoundVO.getId().equals(secondUserBoundVO.getLeaderId()); + if (isSameUserBound) { + errorBuilder.append(userBoundVO.getUserName()) + .append("是") + .append(secondUserBoundVO.getUserName()) + .append("的直属主管,不能互为主管!"); + } + }); + } + // 校验已存在的员工直属主管是否填写的 + // 校验所属组织、岗位、职级之间的绑定关系 + FtbRosterAttributesVO organizationPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.organizationPredicate).findFirst().orElse(null); + FtbRosterAttributesVO postPredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.postPredicate).findFirst().orElse(null); + FtbRosterAttributesVO gradePredicate = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.gradePredicate).findFirst().orElse(null); + FtbRosterAttributesVO team = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.team).findFirst().orElse(null); + if (Objects.nonNull(organizationPredicate) && Objects.nonNull(postPredicate)) { + QueryUserBoundDTO dto = new QueryUserBoundDTO(); + dto.setOrganizeName(organizationPredicate.getAttributeValue()); + dto.setPositionName(postPredicate.getAttributeValue()); + dto.setGradeName(Objects.nonNull(gradePredicate) ? gradePredicate.getAttributeValue() : null); + OrganizeAndPositionAndGradeVO resultCheck = cacheExcelUtils.doCheckOrgPositionGradesBoundDTO(dto); + // 所属组织-岗位-职级校验 + if (Objects.isNull(resultCheck) && Objects.nonNull(gradePredicate)) { + errorBuilder.append("所填写的组织名称、岗位名称、职级名称之间不存在绑定关系;"); + } else if (Objects.isNull(resultCheck)) { + errorBuilder.append("所填写的组织名称、岗位名称之间不存在绑定关系;"); + } else { + // 校验用户组织权限适用范围,返回null则为超级管理员 + List organizationIdDataPermissions = cacheExcelUtils.doPersonnelOrganizationIdDataPermissions(); + if (Objects.nonNull(organizationIdDataPermissions) && !organizationIdDataPermissions.contains(resultCheck.getOrganizeId())) { + errorBuilder.append("导入组织与当前登录人所选权限组织不匹配;"); + } + for (FtbRosterAttributesVO rosterAttributesVO : ftbRosterAttributesVOS) { + if (FtbRosterImportConstants.organizationPredicate.test(rosterAttributesVO)) { + rosterAttributesVO.setAttributeValueId(resultCheck.getOrganizeId()); + } + if (FtbRosterImportConstants.postPredicate.test(rosterAttributesVO)) { + rosterAttributesVO.setAttributeValueId(resultCheck.getPositionId()); + } + if (FtbRosterImportConstants.gradePredicate.test(rosterAttributesVO)) { + rosterAttributesVO.setAttributeValueId(resultCheck.getGradeId()); + } + } + if (Objects.nonNull(userBoundVO)) { + // 校验组织、岗位是否与原有的保持一致 + if (!userBoundVO.getOrganizeId().equals(resultCheck.getOrganizeId())) { + errorBuilder.append("填写的组织与员工现组织不符合;"); + } else if (!userBoundVO.getPositionId().equals(resultCheck.getPositionId())) { + errorBuilder.append("填写的岗位与员工现岗位不符合;"); + } + } + } + } + if (Objects.nonNull(organizationPredicate) && Objects.nonNull(team)) { + // 校验所属组织、班组之间的绑定关系 + if (StrUtil.isNotBlank(team.getAttributeValue())) { + OrganizeAndStoreTeamVO organizeAndStoreTeamVO = cacheExcelUtils.doOrganizeTeamVerification(organizationPredicate.getAttributeValue(), team.getAttributeValue()); + if (Objects.isNull(organizeAndStoreTeamVO)) { + errorBuilder.append("所填写的组织名称、班组名称之间不存在绑定关系;"); + } else { + for (FtbRosterAttributesVO rosterAttributesVO : ftbRosterAttributesVOS) { + if (FtbRosterImportConstants.team.test(rosterAttributesVO)) { + rosterAttributesVO.setAttributeValueId(organizeAndStoreTeamVO.getStoreTeamId()); + } + } + if (Objects.nonNull(userBoundVO)) { + // 校验组织、岗位是否与原有的保持一致 + if (!userBoundVO.getOrganizeId().equals(organizeAndStoreTeamVO.getOrganizeId())) { + errorBuilder.append("填写的班组与现组织不匹配;"); + } + } + } + } + } + // 实际入职日期校验是否大于今天 + FtbRosterAttributesVO actualDateOfJoining = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.actualDateOfJoining).findFirst().orElse(null); + if (Objects.nonNull(actualDateOfJoining)) { + String actualDateOfJoiningAttributeValue = actualDateOfJoining.getAttributeValue(); + Date date = StringToDateConverter.dateFormatParser(actualDateOfJoiningAttributeValue); + if (date.after(new Date())) { + errorBuilder.append("实际入职日期不能大于当前日期;"); + } + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = cacheExcelUtils.doCheckPhoneStaffRosterExist(phoneFtbRosterAttributesVO.getAttributeValue()); + // 实际入职日期要晚于上一次离职日期 + if (Objects.nonNull(ftbPersonnelsStaffRoster) && Objects.nonNull(ftbPersonnelsStaffRoster.getDepartDate()) + && "305".equals(ftbPersonnelsStaffRoster.getWorkerStatus())) { + if (date.before(ftbPersonnelsStaffRoster.getDepartDate()) || ftbPersonnelsStaffRoster.getDepartDate().compareTo(date) == 0) { + errorBuilder.append("实际入职日期不能早于上一次离职日期") + .append(DateUtil.format(ftbPersonnelsStaffRoster.getDepartDate(), DatePattern.CHINESE_DATE_PATTERN)) + .append(";"); + } + } + // 校验是否在入职管理存在,但未进入花名册中 + if (Objects.isNull(ftbPersonnelsStaffRoster) && cacheExcelUtils.checkInOnboardingManagement(phoneFtbRosterAttributesVO.getAttributeValue())) { + errorBuilder.append("该员工处于入职流程中") + .append(";"); + } + // 已在花名册中存在且已离职,目前处理入职管理中。但是又从花名册导入 + if (Objects.nonNull(ftbPersonnelsStaffRoster) && "305".equals(ftbPersonnelsStaffRoster.getWorkerStatus()) + && cacheExcelUtils.checkInOnboardingManagement(phoneFtbRosterAttributesVO.getAttributeValue())) { + errorBuilder.append("该员工处于入职流程中") + .append(";"); + } + } + // 员工状态为试用期,但是未填写试用期 + FtbRosterAttributesVO ftbRosterAttributesVO = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.employeeStatus).findFirst().orElse(null); + if (Objects.nonNull(ftbRosterAttributesVO) && "试用".equals(ftbRosterAttributesVO.getAttributeValue())) { + FtbRosterAttributesVO probationPeriod = ftbRosterAttributesVOS.stream().filter(FtbRosterImportConstants.probationPeriod).findFirst().orElse(null); + if (Objects.isNull(probationPeriod) || StrUtil.isBlank(probationPeriod.getAttributeValue())) { + errorBuilder.append("员工状态为试用,请填写试用期;"); + } + } + } + // 有错误信息 + String errorMsg = errorBuilder.toString(); + if (StrUtil.isNotBlank(errorMsg)) { + // 手动加错误,逆向导出 + FtbRosterAttributesVO ftbRosterAttributesVO = new FtbRosterAttributesVO(); + ftbRosterAttributesVO.setLineMark(context.readRowHolder().getRowIndex()); + ftbRosterAttributesVO.setColumnLabel(errorData.getHead().size() + 1); + ftbRosterAttributesVO.setAttributeName(FtbRosterImportConstants.ERROR_REMARKS); + ftbRosterAttributesVO.setAttributeValue(errorMsg); + ftbRosterAttributesVOS.add(ftbRosterAttributesVO); + ftbRosterPageVO.setFtbRosterAttributesVOS(ftbRosterAttributesVOS); + errorData.getFtbRosterPageVOS().add(ftbRosterPageVO); + } else { + ftbRosterPageVO.setFtbRosterAttributesVOS(ftbRosterAttributesVOS); + normalData.getFtbRosterPageVOS().add(ftbRosterPageVO); + } + } + + @Override + public void saveDataToRedis(AnalysisContext context, FtbRosterCategoryVO normalData, FtbRosterCategoryVO errorData) { + if (normalData.getHead() == null) { + throw new RuntimeException("导入文件模板不正确"); + } + String loginUserId = UserProvider.getLoginUserId(); + String redisKey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_SHEET_REDIS_VARIABLES, loginUserId); + redisUtil.insertHash(redisKey, context.readSheetHolder().getSheetName(), JSON.toJSONString(new FtbRosterImportRedisBO(normalData, errorData))); + // 30分钟自动过期 + redisUtil.expire(redisKey, 30 * 60); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRuleConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRuleConfigServiceImpl.java new file mode 100644 index 0000000..299010f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsRuleConfigServiceImpl.java @@ -0,0 +1,23 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsRuleConfig; +import jnpf.personnels.mapper.FtbPersonnelsRuleConfigMapper; +import jnpf.personnels.service.FtbPersonnelsRuleConfigService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +/** +* +* +*@Author: peng.hao +*@create: 2024/10/22 +* +*/ +@Service +public class FtbPersonnelsRuleConfigServiceImpl extends ServiceImpl implements FtbPersonnelsRuleConfigService{ + + @Resource + private FtbPersonnelsRuleConfigMapper ftbPersonnelsRuleConfigMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSalaryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSalaryServiceImpl.java new file mode 100644 index 0000000..baa22b3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSalaryServiceImpl.java @@ -0,0 +1,467 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.enums.UserStateEnum; +import jnpf.model.common.PayRollJsonItem; +import jnpf.model.dto.FtbXcLochCheckDto; +import jnpf.model.dto.UserSalaryDto; +import jnpf.model.hr.PersonSalaryHistoryVo; +import jnpf.model.personnels.dto.salary.FtbPersonnelRosterSalaryHistoryDTO; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryTemporaryStorageCreatDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsRegularManagement; +import jnpf.model.personnels.po.FtbPersonnelsSalaryTemporaryStorage; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.salary.FtbPersonnelRosterSalaryVO; +import jnpf.model.personnels.vo.salary.FtbPersonnelsSalaryTemporaryStorageVo; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.model.vo.FtbXcLochCheckVo; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.BasePositionGradesEntity; +import jnpf.permission.model.position.PositionGradesSampleBoundVO; +import jnpf.permission.model.user.UserBoundMoreInfoVO; +import jnpf.personnels.controller.web.salary.FtbPersonnelsSalaryController; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.personnels.mapper.FtbPersonnelsSalaryTemporaryStorageMapper; +import jnpf.personnels.service.FtbPersonnelsRegularManagementService; +import jnpf.personnels.service.FtbPersonnelsSalaryService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.service.FtbPersonnelsTransferManageService; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Title: web薪酬 + * @Author: peng.hao + * @create: 2024/5/22:14:56 + */ +@Service +@Slf4j +public class FtbPersonnelsSalaryServiceImpl implements FtbPersonnelsSalaryService { + + @Autowired + QuerySalaryApi salaryApi; + + @Autowired + FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + PositionApi positionApi; + + @Autowired + UserApi userApi; + + @Resource + FtbPersonnelsSalaryTemporaryStorageMapper temporaryStorageMapper; + + @Resource + private FtbPersonnelsRegistrationFormFieldOptionMapper formFieldOptionMapper; + + @Resource + FtbPersonnelsRegularManagementService regularManagementService; + + @Resource + FtbPersonnelsTransferManageService transferManageService; + + + @Override + public void saveTheChangePayInformation(List salaryList, + String userId, + Date date, + UserInfoWithSalary userInfo, + String updateType, + String remark, + String operationType, + Integer isDelete, + Integer salaryType, String payrollSequenceId) { + saveTheChangePayInformation(salaryList, userId, date, userInfo, updateType, remark, operationType, isDelete, salaryType, payrollSequenceId, null); + } + + @Override + public String saveTheChangePayInformation(List salaryList, + String userId, Date date, UserInfoWithSalary userInfo, + String updateType, String remark, String operationType, Integer isDelete, + Integer salaryType, String payrollSequenceId, String tenantId) { + log.error("人事调用[薪酬信息变更]: " + + "userid[" + userId + "] " + + "userInfo[" + userInfo + "] " + + "updateType[" + updateType + "] " + + "operationType[" + operationType + "] " + ); + if (CollUtil.isEmpty(salaryList)){ + throw new RuntimeException("目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + } + // 根据用户信息构建基础信息 + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbPersonnelsStaffRoster::getUserId, userId); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(query); + + UserSalaryDto userSalaryDto = new UserSalaryDto(); + if (StringUtils.isNotEmpty(one.getPhone())) { + userSalaryDto.setFCardId(one.getPhone()); + userSalaryDto.setUserName(one.getName()); + userSalaryDto.setOperatorWorkNo(one.getSystemWokerId()); + } else { + throw new RuntimeException("该用户没有手机号,无法使用薪酬功能;建议前往设置-账号与安全页面绑定手机号。"); + } + userSalaryDto.setRankId(userInfo.getRankId()); + userSalaryDto.setRankName(userInfo.getRankName()); + userSalaryDto.setPostId(userInfo.getPostId()); + userSalaryDto.setPostName(userInfo.getPostName()); + userSalaryDto.setFOrgName(userInfo.getFOrgName()); + userSalaryDto.setFOrgId(userInfo.getFOrgId()); + // 更改为手机号 + userSalaryDto.setUserId(userId); + userSalaryDto.setOperationType(operationType); + // 更改类型 + userSalaryDto.setUpdateType(updateType); + // 生效时间 + if (ObjectUtil.isEmpty(date)) { + throw new RuntimeException("薪资生效时间不能为空!"); + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String format = sdf.format(date); + userSalaryDto.setChangeDate(format); + // 加薪原因 +// if (StringUtils.isEmpty(remark)) userSalaryDto.setRemark("调整薪酬"); + if (StringUtils.isNotEmpty(remark)) userSalaryDto.setRemark(remark); + userSalaryDto.setUpdateUserName(UserProvider.getUser().getUserName()); + userSalaryDto.setUpdateUserId(UserProvider.getUser().getUserId()); + // 移除过滤信息 + // 正则表达式匹配中文 + String regex = "^\\d+(\\.\\d+)?$"; + BigDecimal fTotalMoney = salaryList.stream().filter(item -> item.getFSetValue().matches(regex)).map(item -> BigDecimal.valueOf(Double.parseDouble(item.getFSetValue()))).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + userSalaryDto.setFTotalMoney(fTotalMoney); + List collect = salaryList.stream().map(this::covert).collect(Collectors.toList()); + // 修改的薪资详情 + String jsonString = JSONObject.toJSONString(collect); + userSalaryDto.setAfterSalaryJson(jsonString); + userSalaryDto.setIsDelete(isDelete.toString()); + userSalaryDto.setSalaryType(salaryType); + userSalaryDto.setSequenceId(payrollSequenceId); + userSalaryDto.setTenantId(tenantId); + userSalaryDto.setOperatorWorkNo(userInfo.getOperatorWorkNo()); + userSalaryDto.setTaskId(userInfo.getTaskId()); + userSalaryDto.setUserTypeId(userInfo.getUserTypeId()); + if (StringUtils.isNotEmpty(userInfo.getUserTypeId())){ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getId, userInfo.getUserTypeId()); + FtbPersonnelsRegistrationFormFieldOption formFieldOption = formFieldOptionMapper.selectOne(queryWrapper); + userSalaryDto.setUserTypeName(formFieldOption == null ? "" : formFieldOption.getName()); + } + userSalaryDto.setEntryDate(userInfo.getEntryDate()); + userSalaryDto.setBeComeDate(userInfo.getBeComeDate()); + ActionResult actionResult = salaryApi.updateSalaryJson(userSalaryDto); + if (actionResult == null) { + log.error("updateSalaryJson={}",userSalaryDto); + throw new RuntimeException("薪资修改调整失败!"); + } + if (actionResult.getCode() == 400) { + throw new RuntimeException(actionResult.getMsg()); + } + return actionResult.getData(); + } + + + public void removeUserAllSalary(String userId){ + ActionResult actionResult = salaryApi.removeUserAllSalary(userId); + log.error("重复入职,没有选择薪资项,清除薪资项,userId={},res={}",userId, JSONUtil.toJsonStr(actionResult)); + } + + @SneakyThrows + @Override + public void voidSalary(String userId, String date, String salaryRecordId) { + salaryApi.voidSalary(userId, date, salaryRecordId); + } + + @Override + public String lochCheck(String userId, Date lochStartTime, Date lochEndTime) { + FtbXcLochCheckDto lochCheckDto = new FtbXcLochCheckDto(); + lochCheckDto.setUserId(userId); + lochCheckDto.setLochStartTime(lochStartTime); + lochCheckDto.setLochEndTime(lochEndTime); + List ftbXcLochCheckVos = salaryApi.lochCheck(lochCheckDto); + if (CollUtil.isEmpty(ftbXcLochCheckVos)) { + return null; + } + return ftbXcLochCheckVos.stream().map(v->String.valueOf(v.getChangeDate())).collect(Collectors.joining(",")); + } + + @Override + public List> querySalaryIntersectionWithOtherBusiness(String userId, Date startData, Date endData) { + startData = DateUtil.beginOfDay(startData); + endData = DateUtil.endOfDay(endData); + FtbXcLochCheckDto lochCheckDto = new FtbXcLochCheckDto(); + lochCheckDto.setUserId(userId); + lochCheckDto.setLochStartTime(startData); + lochCheckDto.setLochEndTime(endData); + List ftbXcLochCheckVos = salaryApi.lochCheckAllSalary(lochCheckDto); + List> hashMaps = new ArrayList<>(); + if (CollUtil.isEmpty(ftbXcLochCheckVos)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsRegularManagement::getUserId, userId); + queryWrapper.between(FtbPersonnelsRegularManagement::getActualConverDate, startData,endData ); + queryWrapper.in(FtbPersonnelsRegularManagement::getTransferStatus,2,6); + queryWrapper.eq(FtbPersonnelsRegularManagement::getEnableMark,0); + List managements = regularManagementService.list(queryWrapper); + if (CollUtil.isNotEmpty(managements)){ + List> regularMaps = managements.stream().map(v -> { + Map map = new HashMap<>(); + map.put("time", v.getActualConverDate()); + map.put("type", "转正"); + return map; + }).collect(Collectors.toList()); + hashMaps.addAll(regularMaps); + } + List dates = transferManageService.transferCheckTimeCrossing(userId, startData, endData); + if (CollUtil.isNotEmpty(dates)){ + List> transferMaps = dates.stream().map(v -> { + Map map = new HashMap<>(); + map.put("time", v); + map.put("type", "调动"); + return map; + }).collect(Collectors.toList()); + hashMaps.addAll(transferMaps); + } + FtbXcLochCheckDto lochCheckDto1 = new FtbXcLochCheckDto(); + lochCheckDto1.setUserId(userId); + lochCheckDto1.setLochStartTime(startData); + lochCheckDto1.setLochEndTime(endData); + List ftbXcLochCheckVos1 = salaryApi.lochCheck(lochCheckDto1); + if (CollUtil.isNotEmpty(ftbXcLochCheckVos1)){ + List> hashMaps1 = ftbXcLochCheckVos1.stream().map(v -> { + Map map = new HashMap<>(); + UserStateEnum userState = v.getUserState(); + map.put("time", v.getChangeDate()); + map.put("type", userState.getDesc()); + return map; + }).collect(Collectors.toList()); + hashMaps.addAll(hashMaps1); + } + }else{ + hashMaps = ftbXcLochCheckVos.stream().map(v -> { + Map map = new HashMap<>(); + UserStateEnum userState = v.getUserState(); + map.put("time", v.getChangeDate()); + map.put("type", userState.getDesc()); + return map; + }).collect(Collectors.toList()); + } + return hashMaps; + } + + @Override + public List getSalaryHistoryByUserId(FtbPersonnelRosterSalaryHistoryDTO dto) { + if (StrUtil.isBlank(dto.getUserId())) { + return new ArrayList<>(); + } + List salaryHistoryVos = salaryApi.getSalaryHistoryByUserId(dto.getUserId()).getData(); + if (CollUtil.isEmpty(salaryHistoryVos)) { + return new ArrayList<>(); + } + FtbPersonnelsStaffRoster personnelStaffRosters = staffRosterService.queryRosterInfoByUserId(dto.getUserId()); + List vos = JsonUtil.getJsonToList(salaryHistoryVos, FtbPersonnelRosterSalaryVO.class); + if (personnelStaffRosters != null) { + vos.forEach(vo -> { + if(null!=vo) { + vo.setSystemWokerId(personnelStaffRosters.getSystemWokerId()); + } + }); + } + + return vos; + } + + @Override + public List getTheCurrentPersonSSalary(String userId, String rankId, String postId) { + // 根据用户信息构建基础信息 + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbPersonnelsStaffRoster::getUserId, userId); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(query); + String userName = ""; + String phone = ""; + if (ObjectUtil.isNotEmpty(one)) { + phone = one.getPhone(); + userName = one.getName(); + } + List gradesEntityList = positionApi.getGradesEntityList(List.of(rankId)); + String rankName = ""; + if (CollUtil.isNotEmpty(gradesEntityList)) { + rankName = gradesEntityList.stream().findFirst().get().getFullName(); + } + ActionResult> salaryJsonByUserId = salaryApi.getSalaryJsonByUserId(userId, userName, phone, rankId, rankName, postId); + return getListActionResult(salaryJsonByUserId); + } + + @Override + public List innerGetTheCurrentPersonSSalary(FtbPersonnelsStaffRoster one, String rankId, String postId) { + String userId = one.getUserId(); + String userName = ""; + String phone = ""; + if (ObjectUtil.isNotEmpty(one)) { + phone = one.getPhone(); + userName = one.getName(); + } +// List gradesEntityList = positionApi.getGradesEntityList(List.of(rankId)); + String rankName = ""; +// if (CollUtil.isNotEmpty(gradesEntityList)) { +// rankName = gradesEntityList.stream().findFirst().get().getFullName(); +// } + ActionResult> salaryJsonByUserId = salaryApi.getSalaryJsonByUserId(userId, userName, phone, rankId, rankName, postId); + return getListActionResult(salaryJsonByUserId); + } + + @Override + public void regularizationRevoked(String userId) { + ActionResult actionResult = salaryApi.revokeUserConfirmation(userId); + if (actionResult == null) { + throw new RuntimeException("转正薪资撤销失败!"); + } + if (actionResult.getCode() == 400) { + throw new RuntimeException(actionResult.getMsg()); + } + } + + @Override + public String whetherToVoidWages(String userId, List beforeItem, String flag, Date changeDate) { + UserSalaryDto userSalaryDto = new UserSalaryDto(); + BigDecimal fTotalMoney = beforeItem.stream().filter(item -> + StringUtils.isNotEmpty(item.getfSetValue()) + && "1".equals(item.getfValueType()) + ).map(item -> BigDecimal.valueOf(Double.parseDouble(item.getfSetValue()))).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + userSalaryDto.setFTotalMoney(fTotalMoney); + userSalaryDto.setIsDelete(flag); + userSalaryDto.setChangeDate(DateUtil.format(changeDate, DatePattern.NORM_DATE_PATTERN)); + userSalaryDto.setUpdateUserId(UserProvider.getUser().getUserId()); + userSalaryDto.setUpdateUserName(UserProvider.getUser().getUserName()); + userSalaryDto.setAfterSalaryJson(JSONObject.toJSONString(beforeItem)); + userSalaryDto.setUserId(userId); + userSalaryDto.setUpdateType("1"); + userSalaryDto.setOperationType("1"); + // 根据用户信息构建基础信息 + LambdaQueryWrapper query = Wrappers.lambdaQuery(); + query.eq(FtbPersonnelsStaffRoster::getUserId, userId); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(query); + if (StringUtils.isNotEmpty(one.getPhone())) { + userSalaryDto.setFCardId(one.getPhone()); + userSalaryDto.setUserName(one.getName()); + } else { + throw new RuntimeException("该用户没有手机号,无法使用薪酬功能;建议前往设置-账号与安全页面绑定手机号。"); + } + List workerGroupDataDtos = userApi.getUserBoundMoreInfosByUserId(userId); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + UserBoundMoreInfoVO boundMoreInfoVO = workerGroupDataDtos.stream().filter(PositionGradesSampleBoundVO::getIsDefaultOrganize).findFirst().orElse(new UserBoundMoreInfoVO()); + userSalaryDto.setRankId(boundMoreInfoVO.getPositionGradesId()); + userSalaryDto.setRankName(boundMoreInfoVO.getPositionGradesName()); + userSalaryDto.setPostId(boundMoreInfoVO.getPositionId()); + userSalaryDto.setPostName(boundMoreInfoVO.getPositionName()); + userSalaryDto.setFOrgName(boundMoreInfoVO.getOrganizeName()); + userSalaryDto.setFOrgId(boundMoreInfoVO.getOrganizeId()); + } + userSalaryDto.setRemark("作废薪资"); + ActionResult actionResult = salaryApi.updateSalaryJson(userSalaryDto); + if (actionResult == null) { + throw new RuntimeException("薪资修改调整失败!"); + } + if (actionResult.getCode() == 400) { + throw new RuntimeException(actionResult.getMsg()); + } + return actionResult.getData(); + } + + @Override + public void creatSalaryTemporaryStorage(FtbPersonnelsSalaryTemporaryStorageCreatDto creatDto) { + FtbPersonnelsSalaryTemporaryStorage covert = FtbPersonnelsSalaryTemporaryStorageCreatDto.covert(creatDto); + List structureList = creatDto.getSalaryStructureList(); + if (CollUtil.isNotEmpty(structureList)){ + String string = JSONObject.toJSONString(structureList); + covert.setSalaryStructureList(string); + } + temporaryStorageMapper.insert(covert); + } + + @Override + public void updateSalaryTemporaryStorage(FtbPersonnelsSalaryTemporaryStorageCreatDto creatDto) { + + } + + @Override + public FtbPersonnelsSalaryTemporaryStorageVo querySalaryTemporaryStorage(String uerId, String configType) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsSalaryTemporaryStorage::getUserId,uerId); + wrapper.eq(FtbPersonnelsSalaryTemporaryStorage::getConfigType,configType); + FtbPersonnelsSalaryTemporaryStorage temporaryStorage = temporaryStorageMapper.selectOne(wrapper); + FtbPersonnelsSalaryTemporaryStorageVo coverted = FtbPersonnelsSalaryTemporaryStorageVo.covert(temporaryStorage); + if (StringUtils.isNotEmpty(temporaryStorage.getSalaryStructureList())){ + coverted.setSalaryStructureList(JSONObject.parseArray(temporaryStorage.getSalaryStructureList(), FtbPersonnelsSalaryInfo.class)); + } + return coverted; + } + + private List getListActionResult(ActionResult> result) { + if (result != null && result.getCode() == 200) { + if (ObjectUtils.isEmpty(result.getData())) { + return null; + } + return result.getData().stream().map(FtbPersonnelsSalaryController::covert).collect(Collectors.toList()); + } + if (result != null && result.getCode() == 400) { + // 薪酬报错不做处理返回空数据 + return null; + } + return null; + } + + public PayRollJsonItem covert(FtbPersonnelsSalaryInfo salaryInfo) { + if (salaryInfo == null) { + return null; + } + PayRollJsonItem payRollJsonItem = new PayRollJsonItem(); + payRollJsonItem.setId(salaryInfo.getId()); + payRollJsonItem.setfIsForce(salaryInfo.getFIsForce()); + payRollJsonItem.setfCarryRule(salaryInfo.getFCarryRule()); + payRollJsonItem.setfName(salaryInfo.getFName()); + payRollJsonItem.setfSetType(salaryInfo.getFSetType()); + payRollJsonItem.setfSetValue(salaryInfo.getFSetValue()); + payRollJsonItem.setfValueType(salaryInfo.getFValueType()); + payRollJsonItem.setfReservedBits(salaryInfo.getDecimalPlaces()); + payRollJsonItem.setfRemark(salaryInfo.getFRemark()); + payRollJsonItem.setfItem(salaryInfo.getFItem()); + payRollJsonItem.setfTaxType(salaryInfo.getFTaxType()); + payRollJsonItem.setfSort(salaryInfo.getSort()); + payRollJsonItem.setfDesc(salaryInfo.getFDesc()); + payRollJsonItem.setSequenceType(salaryInfo.getSequenceType()); + payRollJsonItem.setfTypeSort(salaryInfo.getFTypeSort()); + payRollJsonItem.setfSort(salaryInfo.getFSort()); + payRollJsonItem.setGroupId(salaryInfo.getGroupId()); + payRollJsonItem.setGroupName(salaryInfo.getGroupName()); + if (StrUtil.isNotBlank(salaryInfo.getGroupSort())) { + payRollJsonItem.setGroupSort(Integer.parseInt(salaryInfo.getGroupSort())); + } + return payRollJsonItem; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSecondmentManagementServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSecondmentManagementServiceImpl.java new file mode 100644 index 0000000..98a73b1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsSecondmentManagementServiceImpl.java @@ -0,0 +1,673 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.engine.FlowTaskApi; +import jnpf.engine.entity.FlowTaskEntity; +import jnpf.model.attendance.vo.DailyApprovalVo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.secondment.FtbHandleSecondmentDTO; +import jnpf.model.personnels.dto.secondment.FtbSecondMentQueryDTO; +import jnpf.model.personnels.dto.secondment.msg.SecondmentMsgUserInfo; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentInfoVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondmentVO; +import jnpf.permission.V2GradesApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsSecondmentManagementMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.msg.PersonnelsConsumerSourceMsg; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskService; +import jnpf.personnels.service.FtbPersonnelsSalaryService; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.UserProvider; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * + * + *@Author: peng.hao + *@create: 2025/9/11 + * + */ +@Slf4j +@Service +public class FtbPersonnelsSecondmentManagementServiceImpl extends ServiceImpl implements FtbPersonnelsSecondmentManagementService{ + + @Resource + PersonnelsConsumerSourceMsg secondmentConsumerSourceMsg; + + + @Resource + FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Resource + V2OrganizeApi v2OrganizeApi; + @Resource + V2PositionApi v2PositionApi; + @Resource + V2GradesApi v2GradesApi; + + @Autowired + FtbPersonnelsSalaryService salaryService; + + @Autowired + FlowTaskApi flowTaskApi; + + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + FtbPersonnelsAuditRunTaskService ftbPersonnelsAuditRunTaskService; + + @Autowired + QuerySalaryApi querySalaryApi; + + @Autowired + V2UserApi v2UserApi; + + @Autowired + AttendanceDailyRuleService attendanceDailyRuleService; + + @Override + public PageListVO pageList(PersonnelsQueryDTO dto, CultivatePage page) { + Page cultivatePage = page.coverCultivatePage(); + if (dto.getMyApprovalFlag().equals("2")) { + dto.setUserId(UserProvider.getUser().getUserId()); + } + Page pageListVO = baseMapper.pageList(cultivatePage, dto); + List list = pageListVO.getRecords(); + String userId = UserProvider.getUser().getUserId(); + list.forEach(item -> { + if (StringUtils.isNotEmpty(item.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(item.getTaskInfoId()); + if (ObjectUtil.isNotEmpty(byTaskId) && byTaskId.getCreatorUserId().equals(userId)) { + item.setCurrentSponsor(1); + } else { + item.setCurrentSponsor(0); + } + } + }); + return CultivatePage.coverPageList(pageListVO); + } + + @Override + public FtbPersonnelsSecondmentInfoVO details(String id) { + FtbPersonnelsSecondmentManagement selectById = baseMapper.selectById(id); + FtbPersonnelsSecondmentInfoVO convert = FtbPersonnelsSecondmentInfoVO.convert(selectById); + // 创建人名称 + String creatorUserId = selectById.getCreatorUserId(); + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(List.of(creatorUserId, selectById.getUserId()), UserProvider.getUser().getTenantId()); + Map stringMap = new HashMap<>(); + if (actionResult != null && actionResult.getData() != null) { + stringMap = actionResult.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2) -> k1)); + } + convert.setCreatorUserName(stringMap.containsKey(creatorUserId) ? stringMap.get(creatorUserId).getUserName() : ""); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRoster::getUserId, selectById.getUserId()); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(wrapper); + if (ftbPersonnelsStaffRoster != null) { + convert.setEmployeeId(ftbPersonnelsStaffRoster.getSystemWokerId()); + convert.setUserName(ftbPersonnelsStaffRoster.getName()); + } + return convert; + } + + @Override + @Transactional + public String handleSecondment(FtbHandleSecondmentDTO ftbHandleSecondmentDTO) { + List> maps = salaryService.querySalaryIntersectionWithOtherBusiness(ftbHandleSecondmentDTO.getUserId(), + ftbHandleSecondmentDTO.getSecondmentStartTime(), ftbHandleSecondmentDTO.getSecondmentEndTime()); + if (CollUtil.isNotEmpty(maps) ) return "5"; + checkWhetherItIsInTheProcessOfApproval(ftbHandleSecondmentDTO.getUserId()); + // 提交时,需校验借调起止时间是否与该员工未开始的借调时间交叉重合 + checkTheSecondmentTime(ftbHandleSecondmentDTO.getUserId(), ftbHandleSecondmentDTO.getSecondmentStartTime(), + ftbHandleSecondmentDTO.getSecondmentEndTime()); + FtbPersonnelsSecondmentManagement ftbPersonnelsSecondmentManagement = + FtbPersonnelsSecondmentManagement.covert(ftbHandleSecondmentDTO); + ftbPersonnelsSecondmentManagement.setTransferStatus(FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode()); + // 修改 + if (StringUtils.isNotEmpty(ftbPersonnelsSecondmentManagement.getId())){ + baseMapper.updateById(ftbPersonnelsSecondmentManagement); + }else { + baseMapper.insert(ftbPersonnelsSecondmentManagement); + } + // 考勤人员通知 + SecondmentMsgUserInfo msgUserInfo = FtbHandleSecondmentDTO.convert(ftbHandleSecondmentDTO); + msgUserInfo.setTenantId(UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessage(JSONObject.toJSONString(msgUserInfo)); + + // 薪酬保存 + if (ftbPersonnelsSecondmentManagement.getIsResetSalary() == 1 + && StringUtils.isNotEmpty(ftbPersonnelsSecondmentManagement.getSalaryStructureList())){ + extracted(ftbPersonnelsSecondmentManagement); + } + return null; + } + + /** + *校验是否在审批中 + * @param userId + */ + public void checkWhetherItIsInTheProcessOfApproval(String userId){ + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getUserId, userId); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getTransferStatus, FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getEnableMark,0); + boolean b = baseMapper.selectCount(queryWrapper) > 0; + if (b) throw new RuntimeException("该员工存在借调审批中,请勿重复操作!"); + LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getWorkerStatus,"304"); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getUserId, userId); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getEnabledMark,0); + long count = staffRosterService.count(queryWrapper1); + if (count > 0) throw new RuntimeException("该员工处于待离职,不允许办理借调!"); + String checkIsInTheProcessForOA = ftbPersonnelsAuditRunTaskService.checkWhetherTheCheckIsInTheProcessForOA(userId); + if (StrUtil.isNotBlank(checkIsInTheProcessForOA)) { + throw new RuntimeException(checkIsInTheProcessForOA); + } + } + + + /** + * 校验借调时间 + * @param userId + * @param secondedStartTime + * @param secondedEndTime + */ + private void checkTheSecondmentTime(String userId, Date secondedStartTime, Date secondedEndTime){ + if(secondedStartTime.equals(secondedEndTime)) throw new RuntimeException("借调开始时间和结束时间不能相同!"); + if(secondedStartTime.before(new Date())) throw new RuntimeException("借调开始时间不能早于当前时间!"); + if(secondedEndTime.before(secondedStartTime)) throw new RuntimeException("借调结束时间不能早于借调开始时间!"); + int count = baseMapper.checkTheSecondmentTime(userId, secondedStartTime, secondedEndTime); + if (count > 0){ + throw new RuntimeException("借调时间存在交叉且其中包含未结束的借调记录,不能重复发起"); + } + // 外出审批中校验 + Integer userWorkByTimeSlot = baseMapper.getUserGoOut(userId, null, null); + Integer goOutForDay = baseMapper.getUserGoOutForDay(userId, null, null); + if ((null != userWorkByTimeSlot && userWorkByTimeSlot > 0) || (null != goOutForDay && goOutForDay > 0)) { + throw new RuntimeException("该员工当前处于外出申请审批流程中,不可办理其他业务!"); + } + // 外出校验 + userWorkByTimeSlot = baseMapper.getUserGoOut(userId, secondedStartTime, secondedEndTime); + goOutForDay = baseMapper.getUserGoOutForDay(userId, secondedStartTime, secondedEndTime); + if ((null != userWorkByTimeSlot && userWorkByTimeSlot > 0) || (null != goOutForDay && goOutForDay > 0)) { + throw new RuntimeException("申请时间段内已有申请通过的外出申请,请重新选择!"); + } + // 出差审批中校验 + Integer businessTrip = baseMapper.getBusinessTrip(userId, null, null); + if (null != businessTrip && businessTrip > 0) { + throw new RuntimeException("该员工当前处于出差申请审批流程中,不可办理其他业务!"); + } + // 出差校验 + businessTrip = baseMapper.getBusinessTrip(userId, secondedStartTime, secondedEndTime); + if (null != businessTrip && businessTrip > 0) { + throw new RuntimeException("申请时间段内已有申请通过的出差申请,请重新选择!"); + } + String lockedCheck = salaryService.lochCheck(userId, null, null); + if (StringUtils.isNotEmpty(lockedCheck)) { + throw new RuntimeException("当前人员存在审批中的调薪,不允许办理借调!"); + } + } + /** + * 校验借调时间 + * @param userId + * @param secondedStartTime + * @param secondedEndTime + */ + private void checkTheSecondmentDelayTime(String userId, Date secondedStartTime, Date secondedEndTime){ + if(secondedStartTime.equals(secondedEndTime)) throw new RuntimeException("借调开始时间和结束时间不能相同!"); + if(secondedStartTime.before(new Date())) throw new RuntimeException("借调开始时间不能早于当前时间!"); + if(secondedEndTime.before(secondedStartTime)) throw new RuntimeException("借调结束时间不能早于借调开始时间!"); + int count = baseMapper.checkTheSecondmentTime(userId, secondedStartTime, secondedEndTime); + if (count > 0){ + throw new RuntimeException("借调时间存在交叉且其中包含未结束的借调记录,延长失败!"); + } + // 外出审批中校验 + Integer userWorkByTimeSlot = baseMapper.getUserGoOut(userId, null, null); + Integer goOutForDay = baseMapper.getUserGoOutForDay(userId, null, null); + if ((null != userWorkByTimeSlot && userWorkByTimeSlot > 0) || (null != goOutForDay && goOutForDay > 0)) { + throw new RuntimeException("申请时间段内已有申请中的外出申请,延长失败!"); + } + // 外出校验 + userWorkByTimeSlot = baseMapper.getUserGoOut(userId, secondedStartTime, secondedEndTime); + goOutForDay = baseMapper.getUserGoOutForDay(userId, secondedStartTime, secondedEndTime); + if ((null != userWorkByTimeSlot && userWorkByTimeSlot > 0) || (null != goOutForDay && goOutForDay > 0)) { + throw new RuntimeException("申请时间段内已有申请通过的外出申请,延长失败!"); + } + // 出差审批中校验 + Integer businessTrip = baseMapper.getBusinessTrip(userId, null, null); + if (null != businessTrip && businessTrip > 0) { + throw new RuntimeException("申请时间段内已有申请中的出差申请,延长失败!"); + } + // 出差校验 + businessTrip = baseMapper.getBusinessTrip(userId, secondedStartTime, secondedEndTime); + if (null != businessTrip && businessTrip > 0) { + throw new RuntimeException("申请时间段内已有申请通过的出差申请,延长失败!"); + } + String lockedCheck = salaryService.lochCheck(userId, null, null); + if (StringUtils.isNotEmpty(lockedCheck)) { + throw new RuntimeException("当前人员存在审批中的调薪,延长失败!"); + } + } + + @Override + @Transactional + public void delete(String id) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsSecondmentManagement::getId, id); + wrapper.set(FtbPersonnelsSecondmentManagement::getEnableMark, 1); + baseMapper.update(new FtbPersonnelsSecondmentManagement(), wrapper); + } + + @Override + @GlobalTransactional + public void handleSecondmentForOA(FtbHandleSecondmentDTO ftbHandleSecondmentDTO) { + FtbPersonnelsSecondmentManagement secondmentManagement = FtbHandleSecondmentDTO.convertSecondmentManagement(ftbHandleSecondmentDTO); + if (secondmentManagement.getOriginalPositionId().equals(secondmentManagement.getSecondedPositionId()) + && secondmentManagement.getSecondedOrganizationId().equals(secondmentManagement.getOriginalOrganizationId())) throw new RuntimeException("原岗位和借调岗位不能相同!"); + // 薪资校验oa + verifyTheSalary( + secondmentManagement.getSecondmentStartTime(), + secondmentManagement.getSecondmentEndTime(), + secondmentManagement.getUserId()); + ActionResult result = v2OrganizeApi.organizeInfoById(null, + secondmentManagement.getSecondedOrganizationId()); + // 组织 + checkWhetherItIsInTheProcessOfApproval(secondmentManagement.getUserId()); + // 提交时,需校验借调起止时间是否与该员工未开始的借调时间交叉重合 + checkTheSecondmentTime(secondmentManagement.getUserId(), + secondmentManagement.getSecondmentStartTime(), + secondmentManagement.getSecondmentEndTime()); + if (result!= null && result.getCode() == 200) { + secondmentManagement.setSecondedOrganization(result.getData().getName()); + } + // 岗位 + ActionResult voActionResult = v2PositionApi.infoPosition(secondmentManagement.getSecondedPositionId()); + if (voActionResult!= null && voActionResult.getCode() == 200) { + secondmentManagement.setSecondedPosition(voActionResult.getData().getFullName()); + } + // 职级 + if (StringUtils.isNotEmpty(secondmentManagement.getSecondedRankId())) { + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(secondmentManagement.getSecondedRankId()); + if (gradeVOActionResult!= null && gradeVOActionResult.getCode() == 200) { + secondmentManagement.setSecondedRank(gradeVOActionResult.getData().getName()); + } + } + String originalOrganizationId = secondmentManagement.getOriginalOrganizationId(); + ActionResult result2 = v2OrganizeApi.organizeInfoById(null, + originalOrganizationId); + if (result2!= null && result2.getCode() == 200) { + secondmentManagement.setOriginalOrganization(result2.getData().getName()); + } + ActionResult voActionResult2 = v2PositionApi.infoPosition(secondmentManagement.getOriginalPositionId()); + if (voActionResult2!= null && voActionResult2.getCode() == 200) { + secondmentManagement.setOriginalPosition(voActionResult2.getData().getFullName()); + } + if (StringUtils.isNotEmpty(secondmentManagement.getOriginalRankId())) { + ActionResult gradeVOActionResult2 = v2GradesApi.infoGrade(secondmentManagement.getOriginalRankId()); + if (gradeVOActionResult2!= null && gradeVOActionResult2.getCode() == 200) { + secondmentManagement.setOriginalRank(gradeVOActionResult2.getData().getName()); + } + } + secondmentManagement.setTransferStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + // 修改 + if (StringUtils.isNotEmpty(secondmentManagement.getId())){ + baseMapper.updateById(secondmentManagement); + }else { + baseMapper.insert(secondmentManagement); + } + } + + public void verifyTheSalary(Date startTime,Date endTime,String userId){ + List> maps = salaryService.querySalaryIntersectionWithOtherBusiness(userId, startTime, endTime); + if (maps.isEmpty()) return; + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String str = maps.stream() + .map(v-> v.get("type")+"业务(于"+simpleDateFormat.format(v.get("time"))+"生效)") + .collect(Collectors.joining(";")); + throw new RuntimeException("当前办理借调的起止时间为" + + simpleDateFormat.format(startTime) + "~" + + simpleDateFormat.format(endTime) + ";" + + "检测到当前人员有(" + (str) + ")" + + "与当前办理借调的日期重叠,请重新选择借调起止日期!"); + } + + @Override + @GlobalTransactional + public void secondmentApproval(FtbPersonnelsAuditDto ftbPersonnelsAuditDto) { + // 审批状态 0 否 1是 + String dtoFlag = ftbPersonnelsAuditDto.getFlag(); + FtbPersonnelsAuditTaskEnum em = dtoFlag.equals("1") ? + FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED : + FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED; + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsSecondmentManagement::getTaskInfoId, ftbPersonnelsAuditDto.getTaskId()); + wrapper.set(FtbPersonnelsSecondmentManagement::getTransferStatus, em.getCode()); + if (dtoFlag.equals("1")) { + // 最终审批字段 + wrapper.set(FtbPersonnelsSecondmentManagement::getApprovalTime, new Date()); + wrapper.set(FtbPersonnelsSecondmentManagement::getLastApprovalUserId, UserProvider.getUser().getUserId()); + + } + baseMapper.update(new FtbPersonnelsSecondmentManagement(),wrapper); + LambdaQueryWrapper wrapper2 = Wrappers.lambdaQuery(); + wrapper2.eq(FtbPersonnelsSecondmentManagement::getTaskInfoId, ftbPersonnelsAuditDto.getTaskId()); + FtbPersonnelsSecondmentManagement selectOne = baseMapper.selectOne(wrapper2); + if (dtoFlag.equals("1")){ + // 考勤人员通知 + SecondmentMsgUserInfo msgUserInfo = FtbHandleSecondmentDTO.convert(selectOne); + msgUserInfo.setTenantId(UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessage(JSONObject.toJSONString(msgUserInfo)); + } + // 薪酬保存 + if (dtoFlag.equals("1") && selectOne.getIsResetSalary() == 1 + && StringUtils.isNotEmpty(selectOne.getSalaryStructureList())){ + extracted(selectOne); + } + } + + private void extracted(FtbPersonnelsSecondmentManagement selectOne) { + List salaryInfos = JSONObject.parseArray(selectOne.getSalaryStructureList(), FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = getUserInfoWithSalary(selectOne); + LambdaQueryWrapper queryWrapper2 = Wrappers.lambdaQuery(); + queryWrapper2.eq(FtbPersonnelsStaffRoster::getUserId, selectOne.getUserId()); + FtbPersonnelsStaffRoster staffRoster = staffRosterService.getOne(queryWrapper2); + userInfoWithSalary.setUserTypeId(staffRoster.getWorkerType()); + userInfoWithSalary.setEntryDate(staffRoster.getActualStartDate()); + userInfoWithSalary.setBeComeDate(staffRoster.getActualProbationaryDate() == null ? staffRoster.getActualStartDate() : staffRoster.getActualProbationaryDate()); + userInfoWithSalary.setTaskId(selectOne.getTaskInfoId()); + List personSSalary = salaryService.getTheCurrentPersonSSalary(selectOne.getUserId(), + selectOne.getOriginalPositionId(), selectOne.getOriginalRankId()); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + if (CollUtil.isNotEmpty(personSSalary)){ + wrapper.set(FtbPersonnelsSecondmentManagement::getBeforeSalaryStructureList, + JSONObject.toJSONString(personSSalary)); + } + String recordId = salaryService.saveTheChangePayInformation(salaryInfos, + selectOne.getUserId(), + selectOne.getSecondmentStartTime(), + userInfoWithSalary, + "1", + selectOne.getSecondmentRemarks(), + "9", + 0, + selectOne.getSalaryType(), + selectOne.getPayrollSequenceId(), UserProvider.getUser().getTenantId()); + wrapper.set(FtbPersonnelsSecondmentManagement::getSalaryRecordId, recordId); + wrapper.eq(FtbPersonnelsSecondmentManagement::getId, selectOne.getId()); + baseMapper.update(null, wrapper); + } + + @Override + @GlobalTransactional + public void secondmentWithdrawal(FtbPersonnelsAuditDto ftbPersonnelsAuditDto) { + // 撤销 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(StringUtils.isNotEmpty(ftbPersonnelsAuditDto.getTaskId()) ,FtbPersonnelsSecondmentManagement::getTaskInfoId, ftbPersonnelsAuditDto.getTaskId()); + wrapper.eq(StringUtils.isNotEmpty(ftbPersonnelsAuditDto.getBusinessId()) ,FtbPersonnelsSecondmentManagement::getId, ftbPersonnelsAuditDto.getBusinessId()); + wrapper.set(FtbPersonnelsSecondmentManagement::getTransferStatus, FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + baseMapper.update(null, wrapper); + LambdaQueryWrapper wrapper1 = Wrappers.lambdaQuery(); + wrapper1.eq(StringUtils.isNotEmpty(ftbPersonnelsAuditDto.getTaskId()) ,FtbPersonnelsSecondmentManagement::getTaskInfoId, ftbPersonnelsAuditDto.getTaskId()); + wrapper1.eq(StringUtils.isNotEmpty(ftbPersonnelsAuditDto.getBusinessId()) ,FtbPersonnelsSecondmentManagement::getId, ftbPersonnelsAuditDto.getBusinessId()); + FtbPersonnelsSecondmentManagement vo = baseMapper.selectOne(wrapper1); + // 如果使用了薪酬模版 需要 进行数据撤销回滚 + if (1 == vo.getIsResetSalary()) { + // 撤销成功 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String format = sdf.format(vo.getSecondmentStartTime()); + salaryService.voidSalary(vo.getUserId(),format,vo.getSalaryRecordId()); + } + // 考勤人员通知 + SecondmentMsgUserInfo msgUserInfo = FtbHandleSecondmentDTO.convert(vo); + msgUserInfo.setTenantId(UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageSecondmentWithdrawal(JSONObject.toJSONString(msgUserInfo)); + } + + @Override + @Transactional + public void earlyEnd(Map map) { + Date endTime = getEndTime(map); + // 提前结束 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsSecondmentManagement::getId, map.get("id")); + wrapper.set(FtbPersonnelsSecondmentManagement::getAdvanceEndTime,endTime); + baseMapper.update(new FtbPersonnelsSecondmentManagement(),wrapper ); + + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getId, map.get("id")); + FtbPersonnelsSecondmentManagement secondmentManagement = baseMapper.selectOne(queryWrapper); + + // 考勤人员通知 + SecondmentMsgUserInfo msgUserInfo = FtbHandleSecondmentDTO.convert(secondmentManagement); + msgUserInfo.setTenantId(UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageEarlyEnd(JSONObject.toJSONString(msgUserInfo)); + } + + @Override + @Transactional + public void delayTime(Map map) { + Date endTime = getEndTime(map); + // 延迟时间校验 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsSecondmentManagement::getId, map.get("id")); + FtbPersonnelsSecondmentManagement secondmentManagement = baseMapper.selectOne(queryWrapper); + String userId = secondmentManagement.getUserId(); + checkTheSecondmentDelayTime(userId,secondmentManagement.getSecondmentEndTime(),endTime); + // 延长时间 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsSecondmentManagement::getId, map.get("id")); + wrapper.set(FtbPersonnelsSecondmentManagement::getSecondmentEndTime, endTime); + baseMapper.update(new FtbPersonnelsSecondmentManagement(),wrapper); + + // 考勤人员通知 + SecondmentMsgUserInfo msgUserInfo = FtbHandleSecondmentDTO.convert(secondmentManagement); + msgUserInfo.setTenantId(UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageExtend(JSONObject.toJSONString(msgUserInfo)); + } + + /** + * 时间校验 + * @param map + * @return + */ + @NotNull + private Date getEndTime(Map map) { + return DateUtil.parse(String.valueOf(map.get("endTime")), DatePattern.NORM_DATETIME_MINUTE_PATTERN); + } + + @Override + public Boolean checkIsInTheProcess(String userId, Date startTime, Date endTime) { + checkTheSecondmentTime(userId,startTime,endTime); + return true; + } + + @Override + public void endByUserConfirm(String userId, Date leaveTime) { + List list = getIdList(userId, leaveTime); + if (!list.isEmpty()) { + list.forEach(secondmentManagement -> { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsSecondmentManagement::getId, secondmentManagement.getId()); + updateWrapper.set(FtbPersonnelsSecondmentManagement::getSecondmentEndTime, leaveTime); + baseMapper.update(new FtbPersonnelsSecondmentManagement(),updateWrapper); + }); + } + // 并删除离职时间之后的调动数据 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.gt(FtbPersonnelsSecondmentManagement::getSecondmentEndTime,leaveTime); + wrapper.eq(FtbPersonnelsSecondmentManagement::getUserId,userId); + wrapper.in(FtbPersonnelsSecondmentManagement::getTransferStatus,2,6); + baseMapper.delete(wrapper); + // 离职删除借调数据 + LambdaUpdateWrapper wrapper1 = Wrappers.lambdaUpdate(); + wrapper1.eq(FtbPersonnelsSecondmentManagement::getUserId,userId); + wrapper1.set(FtbPersonnelsSecondmentManagement::getEnableMark,1); + baseMapper.update(new FtbPersonnelsSecondmentManagement(),wrapper1); + } + @Override + public int checkWhetherThereIsASecondmentRecord(String userId, Date leaveTime) { + return getIdList(userId, leaveTime).size(); + } + + @Override + public List getSecondmentRecord(String userId, String startLeaveTime, String endLeaveTime) { + return baseMapper.getSecondmentRecord(userId,startLeaveTime,endLeaveTime); + } + + @Override + public List getSecondmentRecordBath(FtbSecondMentQueryDTO dto) { + return baseMapper.getSecondmentRecordBath(dto); + } + + @Override + @SneakyThrows + public void checkSecondmentEnd(String tenantId) { + TenantDataSourceUtil.switchTenant(tenantId); + List list = baseMapper.checkSecondmentEnd( DateUtil.date()); + // 批量处理, 薪资数据进行处理 + List stringList = list.stream().map(secondmentManagement -> { + List salaryInfos = JSONObject.parseArray(secondmentManagement.getBeforeSalaryStructureList(), FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + userInfoWithSalary.setRankName(secondmentManagement.getOriginalRank()); + userInfoWithSalary.setRankId(secondmentManagement.getOriginalRankId()); + userInfoWithSalary.setFOrgId(secondmentManagement.getOriginalOrganizationId()); + userInfoWithSalary.setFOrgName(secondmentManagement.getOriginalOrganization()); + userInfoWithSalary.setPostId(secondmentManagement.getOriginalPositionId()); + userInfoWithSalary.setPostName(secondmentManagement.getOriginalPosition()); + LambdaQueryWrapper queryWrapper2 = Wrappers.lambdaQuery(); + queryWrapper2.eq(FtbPersonnelsStaffRoster::getUserId, secondmentManagement.getUserId()); + FtbPersonnelsStaffRoster staffRoster = staffRosterService.getOne(queryWrapper2); + userInfoWithSalary.setBeComeDate(staffRoster.getActualProbationaryDate() == null ? staffRoster.getActualStartDate() : staffRoster.getActualProbationaryDate()); + userInfoWithSalary.setEntryDate(staffRoster.getActualStartDate()); + userInfoWithSalary.setUserTypeId(staffRoster.getWorkerType()); + try { + Date date = secondmentManagement.getAdvanceEndTime() != null ? secondmentManagement.getAdvanceEndTime() : secondmentManagement.getSecondmentEndTime(); + // 将时间推迟一天 并格式为 yyyy-MM-dd 00:00:00 + date = DateUtil.offsetDay(date, 1); + date = DateUtil.beginOfDay(date); + salaryService.saveTheChangePayInformation(salaryInfos, + secondmentManagement.getUserId(), + date, + userInfoWithSalary, + "1", + "借调返岗", + "10",// 借调返岗 + 0, + secondmentManagement.getSalaryType(), + secondmentManagement.getPayrollSequenceId(), UserProvider.getUser().getTenantId()); + }catch (Exception e) { + e.printStackTrace(); + return null; + } + return secondmentManagement.getId(); + }).filter(Objects::nonNull).collect(Collectors.toList()); + if (stringList.isEmpty()) { + return; + } + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.in(FtbPersonnelsSecondmentManagement::getId, stringList); + wrapper.set(FtbPersonnelsSecondmentManagement::getBeforeSalaryStructureList,null); + baseMapper.update(new FtbPersonnelsSecondmentManagement(),wrapper); + log.error("定时任务借调返岗执行成功:{}", DateUtil.formatDate(new Date())); + } + + @Override + public List getIdList(String userId, Date leaveTime){ + return baseMapper.getIdList(userId,leaveTime); + } + + @Override + public List salarySecondmentCrossover(String userId, Date leaveTime) { + List idList = baseMapper.getIdList(userId, leaveTime); + if (idList != null && !idList.isEmpty()) { + return idList; + } + return new ArrayList<>(); + } + + @Override + public List queryListApproval(FtbSecondMentQueryDTO dto) { + return baseMapper.queryListApproval(dto); + } + + @Override + @GlobalTransactional + public void cancel(String id) { + FtbPersonnelsSecondmentManagement vo = baseMapper.selectById(id); + Date secondedStartTime = vo.getSecondmentStartTime(); + Date secondedEndTime = vo.getSecondmentEndTime(); + String userId = vo.getUserId(); + boolean byUserIdAndTime = attendanceDailyRuleService.hasRuleByUserIdAndTime(userId, secondedStartTime, secondedEndTime); + if (byUserIdAndTime) { + throw new RuntimeException("在借调考勤组已产生申请记录或排班记录;无法作废!"); + } + // 直接删除记录 + baseMapper.deleteById(id); + // 撤销薪资 + if (1 == vo.getIsResetSalary()) { + // 撤销成功 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + String format = sdf.format(secondedStartTime); + salaryService.voidSalary(userId,format, vo.getSalaryRecordId()); + } + // 考勤人员通知 + SecondmentMsgUserInfo msgUserInfo = FtbHandleSecondmentDTO.convert(vo); + msgUserInfo.setTenantId(UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageSecondmentWithdrawal(JSONObject.toJSONString(msgUserInfo)); + } + + @NotNull + private UserInfoWithSalary getUserInfoWithSalary(FtbPersonnelsSecondmentManagement secondmentManagement) { + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + userInfoWithSalary.setRankName(secondmentManagement.getSecondedRank()); + userInfoWithSalary.setRankId(secondmentManagement.getSecondedRankId()); + userInfoWithSalary.setFOrgId(secondmentManagement.getSecondedOrganizationId()); + userInfoWithSalary.setFOrgName(secondmentManagement.getSecondedOrganization()); + userInfoWithSalary.setPostId(secondmentManagement.getSecondedPositionId()); + userInfoWithSalary.setPostName(secondmentManagement.getSecondedPosition()); + return userInfoWithSalary; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsShortchainServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsShortchainServiceImpl.java new file mode 100644 index 0000000..2ef473c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsShortchainServiceImpl.java @@ -0,0 +1,64 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsShortchain; +import jnpf.personnels.mapper.FtbPersonnelsShortchainMapper; +import jnpf.personnels.service.FtbPersonnelsShortchainService; +import jnpf.util.excel.ShortChainUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +@Service +public class FtbPersonnelsShortchainServiceImpl extends ServiceImpl implements FtbPersonnelsShortchainService { + + @Override + public String generateShortChain(String longChainAddress) { + String[] resUrl = ShortChainUtils.shortUrl(longChainAddress); + Random random = new Random(); + int nextInt = random.nextInt(4); + String shortUrl = resUrl[nextInt]; + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsShortchain::getShortChainAddr, shortUrl); + if (this.baseMapper.selectCount(queryWrapper) > 0) { + return shortUrl + nextInt; + } + return shortUrl; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveShortChain(String shortChainAddress, String longChainAddress, String requestUrl) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsShortchain::getShortChainAddr, shortChainAddress); + if (this.baseMapper.selectCount(queryWrapper) == 0) { + FtbPersonnelsShortchain ftbPersonnelsShortchain = new FtbPersonnelsShortchain(); + ftbPersonnelsShortchain.setShortChainAddr(shortChainAddress); + ftbPersonnelsShortchain.setLongChainAddr(longChainAddress); + ftbPersonnelsShortchain.setRequestUrl(requestUrl); + this.baseMapper.insert(ftbPersonnelsShortchain); + } + } + + @Override + public String redirect(String url) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPersonnelsShortchain::getRequestUrl, FtbPersonnelsShortchain::getLongChainAddr); + queryWrapper.eq(FtbPersonnelsShortchain::getShortChainAddr, url); + FtbPersonnelsShortchain ftbPersonnelsShortchain = this.baseMapper.selectOne(queryWrapper); + if (ftbPersonnelsShortchain != null) { + String chinesePath = ShortChainUtils.reverseLongChain(ftbPersonnelsShortchain.getLongChainAddr()); + chinesePath = chinesePath.substring(chinesePath.indexOf("?")); + // 使用UTF-8编码将中文转换为URL编码格式 + String encodedChinesePath = URLEncoder.encode(chinesePath, StandardCharsets.UTF_8); + return ftbPersonnelsShortchain.getRequestUrl() + "?" + encodedChinesePath; + } + return null; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffArchivesHistoryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffArchivesHistoryServiceImpl.java new file mode 100644 index 0000000..905b89c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffArchivesHistoryServiceImpl.java @@ -0,0 +1,46 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.personnels.po.FtbPersonnelsStaffArchivesHistory; +import jnpf.model.personnels.vo.roster.FtbPersonnelsStaffArchivesHistoryVo; +import jnpf.personnels.mapper.FtbPersonnelsStaffArchivesHistoryMapper; +import jnpf.personnels.service.FtbPersonnelsStaffArchivesHistoryService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class FtbPersonnelsStaffArchivesHistoryServiceImpl extends ServiceImpl implements FtbPersonnelsStaffArchivesHistoryService { + + + @Override + public FtbPersonnelsStaffArchivesHistoryVo getInfo(String userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffArchivesHistory::getUserId, userId) + .eq(FtbPersonnelsStaffArchivesHistory::getEnabledMark, 1); + List list = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(list)) { + return null; + } + return BeanUtil.copyProperties(list.get(0), FtbPersonnelsStaffArchivesHistoryVo.class); + } + + + @Override + public void deleteForUserIds(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return; + } + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.in(FtbPersonnelsStaffArchivesHistory::getUserId, userIds); + lambdaUpdate.set(FtbPersonnelsStaffArchivesHistory::getEnabledMark, 0); + baseMapper.update(null, lambdaUpdate); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffEmploymentApplyServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffEmploymentApplyServiceImpl.java new file mode 100644 index 0000000..b6f13a7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffEmploymentApplyServiceImpl.java @@ -0,0 +1,1418 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.FileEntity; +import jnpf.entity.StoreEntity; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.enums.personnel.PhoneStatusEnum; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.bo.FtbAddStaffImportRedisBO; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsEmployApplyDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.dto.staff.roster.StaffBaseInfoDto; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.req.employment.*; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.employment.AddStaffEmploymentApplyExcelERRVO; +import jnpf.model.personnels.vo.employment.AddStaffEmploymentApplyExcelVO; +import jnpf.model.personnels.vo.employment.FtbPersonnelsStaffEmploymentApplyVO; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditInfoVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.personnels.listeners.BaseEasyExcelCommonListener; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldOptionMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsUchisuikePondMapper; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.PersonalizedTenantWhitelistUtils; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.personnels.utils.SmsSendUtil; +import jnpf.util.JsonUtil; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsStaffEmploymentApplyServiceImpl extends ServiceImpl implements FtbPersonnelsStaffEmploymentApplyService { + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private PositionApi positionApi; + + + @Autowired + private FtbPersonnelsAuditRunTaskService auditRunTaskService; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private FtbPersonnelsStaffRosterService rosterService; + + @Resource + private RedisUtil redisUtil; + + @Autowired + private FtbPersonnelsAuditSubConfigService subConfigService; + + @Autowired + private SmsSendUtil smsSendUtil; + + @Autowired + private PersonnelPerUtils personnelPerUtils; + + @Autowired + private FtbPersonnelsUchisuikePondMapper uchisuikePondMapper; + + @Resource + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Resource + private AttendanceGroupApi attendanceGroupApi; + + @Autowired + FtbPersonnelsRegistrationFormFieldOptionMapper optionMapper; + + @Autowired + PersonalizedTenantWhitelistUtils personalizedTenantWhitelistUtils; + + @Resource + FtbPersonnelsRegistrationFormFieldMapper registrationFormFieldMapper; + + + public static final String REDIS_KEY = "ftb:personnels:staff:employment:apply:%s"; + + + @Resource(name = "ftbPersonnelsService") + private BaseEasyExcelCommonListener baseEasyExcelCommonListener; + + + @Override + public PageInfo getPageList(QueryStaffEmploymentApplyListReq req) { + + Page queryPage = null; + List records = new ArrayList<>(); + List selectOrgList; + List authOrgList = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions();//获取用户组织权限 + if (CollectionUtil.isEmpty(authOrgList)) { + authOrgList = new ArrayList<>(); + } + if (StringUtils.isEmpty(req.getCurrOrg())) { + selectOrgList = authOrgList; + } else { + if (CollectionUtil.isEmpty(authOrgList)) { + selectOrgList = new ArrayList<>(List.of(req.getCurrOrg().split(StringPool.COMMA))); + } else { + List userSubList = List.of(req.getCurrOrg().split(StringPool.COMMA)); + // 使用Stream API获取交集 + selectOrgList = authOrgList.stream() + .filter(userSubList::contains) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(selectOrgList)) { + return new PageInfo<>(records); + } + } + } + Boolean whitelist = personalizedTenantWhitelistUtils.isItOnTheWhitelist(); + req.setExt(whitelist); + req.setInnerSelectOrgList(selectOrgList); + if (req.getListType() == 0) { + queryPage = baseMapper.expectPagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req); + records = queryPage.getRecords(); + } else { + queryPage = baseMapper.checkPagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req); + records = queryPage.getRecords(); + } + if (CollectionUtil.isNotEmpty(records)) { + //补充组织 岗位 职等 信息 推荐人、推荐门店 + personnelOrgUtils.fillOrgName(records); + personnelOrgUtils.fillPositionName(records); + personnelOrgUtils.fillRankName(records); + personnelOrgUtils.fillExpireDayNum(records); + //查询合同类型 + personnelOrgUtils.fillContractTypeName(records); + // 数据补充 + if (whitelist) { + List intentionOrgIds = records.stream().map(FtbPersonnelsStaffEmploymentApplyDto::getIntentionOrgId).collect(Collectors.toList()); + List organizeList = organizeApi.getOrganizeName(intentionOrgIds); + Map orgMap = organizeList.stream().collect(Collectors.toMap(OrganizeEntity::getId, OrganizeEntity::getFullName)); + records.forEach(item -> { + if (orgMap.containsKey(item.getIntentionOrgId())) { + item.setIntentionOrgName(orgMap.get(item.getIntentionOrgId())); + } + }); + } + + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public PageInfo getAppPageList(QueryAppStaffEmploymentApplyListReq req, String loginUserId) { + Page queryPage = null; + List records = new ArrayList<>(); + if (req.getListType() == 0) { + List authOrgList = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions();//获取用户组织权限 + if (CollectionUtil.isEmpty(authOrgList)) { + authOrgList = new ArrayList<>(); + } + req.setInnerSelectOrgList(authOrgList); + queryPage = baseMapper.expectAppPagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req); + } else if (req.getListType() == 2) { + // 2.我的审批 + Integer configType = FtbPersonnelsCofigEnum.ONBOARDING_APPROVAL_CONFIGURATION.getConfigType(); + // 待审批 + if (req.getCheckStatus() == 0 ){ + queryPage = baseMapper.pagingQueryWebMyCheckList(Page.of(req.getCurrentPage(), + req.getPageSize()), + loginUserId, + configType); + }else { + if (req.getCheckStatus() == 1) queryPage = baseMapper.getApprovedList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, configType,1); + if (req.getCheckStatus() == 2) queryPage = baseMapper.getApprovedList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, configType,2); + if (req.getCheckStatus() == 3) queryPage = baseMapper.getApprovedList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, configType,3); + } + + } else if (req.getListType() == 3) { + if (req.getCheckStatus() == 1) queryPage = baseMapper.getListForApp(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, 1); + if (req.getCheckStatus() == 2) queryPage = baseMapper.getListForApp(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, 2); + if (req.getCheckStatus() == 3) queryPage = baseMapper.getListForApp(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, 3); + } + records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + //补充组织 岗位 职等 信息 推荐人、推荐门店 + personnelOrgUtils.fillOrgName(records); + personnelOrgUtils.fillPositionName(records); + personnelOrgUtils.fillRankName(records); + personnelOrgUtils.fillExpireDayNum(records); + //查询合同类型 + personnelOrgUtils.fillContractTypeName(records); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + records.forEach(item -> { + Boolean b = auditRunTaskService.checkIsTheCurrentBusinessInTheApprovalProcess(item.getId()); + if (b) { + item.setIsItUnderReview("1"); + } else { + item.setIsItUnderReview("0"); + } + }); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + @Transactional + public void approval(EmploymentApplyCheckDto dto) { + String id = dto.getId(); + FtbPersonnelsStaffEmploymentApply entity = queryAndCheckStaffEmploymentApply(id); + if (entity.getIsNeedCheck() == 1) { + throw new RuntimeException("该员工入职不需要审核"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + throw new RuntimeException("该员工入职审核已经通过"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 3) { + throw new RuntimeException("该员工入职审核已经不通过"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 4) { + throw new RuntimeException("该员工入职审核已经撤销"); + } + FtbPersonnelsAuditDto checkDto = new FtbPersonnelsAuditDto(); + checkDto.setBusinessId(entity.getId()); + if (ObjectUtil.isNotEmpty(dto.getIsConformSalary())) { + checkDto.setDoesTheSalaryComplyWith(dto.getIsConformSalary()); + } + if (ObjectUtil.isNotEmpty(dto.getMsg())) { + checkDto.setApprovalComments(dto.getMsg()); + } + if (ObjectUtil.isNotEmpty(dto.getIsPass())) { + checkDto.setFlag(String.valueOf(dto.getIsPass())); + } + FtbPersonnelsAuditTaskEnum code = auditRunTaskService.performReview(checkDto); + switch (code) { + case PENDING: + entity.setCheckStatus(0); + break; + case UNDER_REVIEW: + entity.setCheckStatus(1); + break; + case EXAMINATION_PASSED: + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), "301"); + entity.setCheckStatus(2); + break; + case AUDIT_NOT_PASSED: + entity.setCheckStatus(3); + break; + case CANCEL: + entity.setCheckStatus(4); + break; + default: + entity.setCheckStatus(3); + break; + } + baseMapper.updateById(entity); + } + + @Override + @Transactional + public void reApproval(String id, AddStaffEmploymentApplyReq req) { + FtbPersonnelsStaffEmploymentApply entity = queryAndCheckStaffEmploymentApply(id); + if (entity.getIsNeedCheck() == 1) { + throw new RuntimeException("该员工不需要审核"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + throw new RuntimeException("该员工审核已经通过"); + } + + //查询手机号是否在已入职列表里面 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, req.getPhone()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + ; + FtbPersonnelsStaffRoster oldRosterEntity = rosterService.getOne(rosterWrapper); + if (null != oldRosterEntity) { + if (!oldRosterEntity.getWorkerStatus().equals("305")) { + throw new RuntimeException("手机号所属员工已经存在花名册中"); + } + } + String loginUserId = UserProvider.getLoginUserId(); + + + //写入数据库 + FtbPersonnelsStaffEmploymentApply newEntity = BeanUtil.copyProperties(req, FtbPersonnelsStaffEmploymentApply.class); + newEntity.setStatus(0); + newEntity.setEnabledMark(0); + + newEntity.setId(entity.getId()); + newEntity.setIsSubmitForm(entity.getIsSubmitForm()); + if (req.getIsNeedCheck() == 0) { + newEntity.setCheckStatus(1); + } + newEntity.setJoinNum(1); + if (oldRosterEntity != null && oldRosterEntity.getJoinNum() != null) { + newEntity.setJoinNum(oldRosterEntity.getJoinNum() + 1); + } + //获取当前登录用户 + newEntity.setOpUser(loginUserId); + newEntity.setOpUserName(UserProvider.getUser().getUserName()); + newEntity.setCreatorTime(new Date()); + baseMapper.updateById(newEntity); + + //判断是否需要发起审批 0、需要审批 + if (req.getIsNeedCheck() == 0) { + // 创建审批 + String runTaskId = auditRunTaskService.startTheReviewProcess(newEntity.getId(), + FtbPersonnelsCofigEnum.ONBOARDING_APPROVAL_CONFIGURATION.getConfigType(), + newEntity.getCurrOrg(), entity.getId()); + // 同步id + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.eq(FtbPersonnelsStaffEmploymentApply::getId, newEntity.getId()); + lambdaUpdate.set(FtbPersonnelsStaffEmploymentApply::getTaskInfoId, runTaskId); + baseMapper.update(null, lambdaUpdate); + } else { + personnelOrgUtils.sysWorkerStatusToUchisuike(newEntity.getPhone(), "301"); + } + //同步数据到formData + Map formDataMap = BeanUtil.beanToMap(newEntity); + formDataMap.put("expectedStartDate", personnelOrgUtils.dateToString(req.getExpectedStartDate(), "")); + registrationFormDataService.syncData(formDataMap, newEntity.getPhone(), ""); + } + + + + @Override + public void saveInRedis(List nomarlData, List errorData) { + String redisKey = getRedisKey(); + redisUtil.insert(redisKey, JSON.toJSONString(new FtbAddStaffImportRedisBO(nomarlData, errorData))); + // 30分钟自动过期 + redisUtil.expire(redisKey, 30 * 60); + } + + @Override + @Transactional(rollbackFor = {RuntimeException.class, Exception.class}) + public void importDataClick() { + String redisKey = getRedisKey(); + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已经过期,请重新导入!"); + } + FtbAddStaffImportRedisBO importRedisBO = getAddStaffImportRedisBO(redisKey); + List nomarlData = importRedisBO.getNormalData(); + if (CollUtil.isEmpty(nomarlData)) { + throw new RuntimeException("当前暂无数据导入!"); + } + // 插入 + nomarlData.forEach(item -> { + AddStaffEmploymentApplyReq convert = AddStaffEmploymentApplyReq.convert(item); + convert.setIsNeedCheck(1); + //有填考勤组的时候 + if (StrUtil.isBlank(item.getAttendanceGroup()) && StrUtil.isNotBlank(item.getAttendanceGroupName())) { + //考勤组id查询 + AttendanceGroupVo attendanceGroupVo = attendanceGroupApi.getAttendanceGroupByName(item.getCurrOrgName(), item.getAttendanceGroupName()).getData(); + if (attendanceGroupVo != null) { + convert.setAttendanceGroup(attendanceGroupVo.getId()); + } else { + throw new RuntimeException("组织[" + item.getCurrOrgName() + "] 不存在考勤组[" + item.getAttendanceGroupName()); + } + } else if (StrUtil.isNotBlank(item.getAttendanceGroup())) { + convert.setAttendanceGroup(item.getAttendanceGroup()); + } + + insertData(convert); + }); + redisUtil.remove(redisKey); + } + + @SneakyThrows + @Override + public Map importData(InputStream inputStream) { + EasyExcelUtils.universalImport(AddStaffEmploymentApplyExcelVO.class, + inputStream, + baseEasyExcelCommonListener, + 1); + String redisKey = getRedisKey(); + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已经过期,请重新导入!"); + } + FtbAddStaffImportRedisBO importRedisBO = getAddStaffImportRedisBO(redisKey); + HashMap map = Maps.newHashMap(); + map.put("normalData", importRedisBO.getNormalData().size()); + map.put("errorData", importRedisBO.getErrorData().size()); + map.put("data", importRedisBO); + return map; + } + + @Nullable + private FtbAddStaffImportRedisBO getAddStaffImportRedisBO(String redisKey) { + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已经过期,请重新导入!"); + } + Object utilString = redisUtil.getString(redisKey); + return JSON.parseObject(utilString.toString(), FtbAddStaffImportRedisBO.class); + } + + private String getRedisKey() { + String loginUserId = UserProvider.getLoginUserId(); + return String.format(REDIS_KEY, loginUserId); + } + + @SneakyThrows + @Override + public void exportErrData(HttpServletResponse response) { + String redisKey = getRedisKey(); + FtbAddStaffImportRedisBO importRedisBO = getAddStaffImportRedisBO(redisKey); + assert importRedisBO != null; + List errorData = importRedisBO.getErrorData(); + String fileName = "预办理入职员工错误信息"; + EasyExcelUtils.exportExcel(response, fileName, errorData, AddStaffEmploymentApplyExcelERRVO.class); + } + + @SneakyThrows + @Override + public void exportDate(BatchByPrimaryIdReq req, HttpServletResponse response) { + if (CollectionUtil.isEmpty(req.getIds())) { + throw new RuntimeException("请选择要导出的数据"); + } + List entityList = baseMapper.selectBatchIds(req.getIds()); + + if (CollUtil.isEmpty(entityList)) { + throw new RuntimeException("无数据导出!"); + } + List list = BeanUtil.copyToList(entityList, FtbPersonnelsStaffEmploymentApplyDto.class); + + if (CollectionUtil.isNotEmpty(list)) { + //补充组织 岗位 职等 信息 推荐人、推荐门店 + personnelOrgUtils.fillOrgName(list); + personnelOrgUtils.fillPositionName(list); + personnelOrgUtils.fillRankName(list); + personnelOrgUtils.fillExpireDayNum(list); + + //查询合同类型 + personnelOrgUtils.fillContractTypeName(list); + } + List excelVOS = list.stream().map(FtbPersonnelsStaffEmploymentApplyDto::covert).collect(Collectors.toList()); + String fileName = "预办理入职员工信息"; + EasyExcelUtils.exportExcel(response, fileName, excelVOS, AddStaffEmploymentApplyExcelVO.class); + } + + @Override + public PageInfo getWebMyCheckList(MyWebEmploymentApplyCheckListReq req) { + //审批状态:0、待审批 1、已审批 + String loginUserId = UserProvider.getLoginUserId(); + Page queryPage; + Integer configType = FtbPersonnelsCofigEnum.ONBOARDING_APPROVAL_CONFIGURATION.getConfigType(); + if (0 == req.getCheckStatus()) { + queryPage = baseMapper.pagingQueryWebMyCheckList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, configType); + } else { + queryPage = baseMapper.getApprovedList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, configType,7); + } + + List records = queryPage.getRecords(); + + if (CollectionUtil.isNotEmpty(records)) { + // 移除主流程状态替换 + //补充组织 岗位 职等 信息 推荐人、推荐门店 + personnelOrgUtils.fillOrgName(records); + personnelOrgUtils.fillPositionName(records); + personnelOrgUtils.fillRankName(records); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + + @Override + public FtbPersonnelsStaffEmploymentApplyDto getInfo(String id) { + //查询基本信息 + FtbPersonnelsStaffEmploymentApply entity = queryAndCheckStaffEmploymentApply(id); + FtbPersonnelsStaffEmploymentApplyDto dto = JsonUtil.getJsonToBean(entity, FtbPersonnelsStaffEmploymentApplyDto.class); +// FtbPersonnelsStaffEmploymentApplyDto dto = BeanUtil.copyProperties(entity, FtbPersonnelsStaffEmploymentApplyDto.class); + fillOrgNameById(dto); + fillPositionNameById(dto); + fillRankNameById(dto); + fillContactInfo(dto); + //考勤组信息返回 + if (StrUtil.isNotBlank(entity.getAttendanceGroup())) { + AttendanceGroup attendanceGroup = attendanceGroupApi.queryTheNameOfTheAttendanceGroup(entity.getAttendanceGroup()); + if (attendanceGroup != null) { + dto.setAttendanceGroupName(attendanceGroup.getGroupName()); + } + } + //需要审批 并且审批还没有通过时需要返回 + if (dto.getIsNeedCheck() == 0) { + FtbPersonnelsAuditInfoVO auditInfoVO = subConfigService.queryAuditSubConfig(entity.getCurrOrg(), FtbPersonnelsCofigEnum.ONBOARDING_APPROVAL_CONFIGURATION); + dto.setAuditInfo(auditInfoVO); + } + //转换临时薪酬信息 + if (StrUtil.isNotBlank(entity.getSalaryData())) { + dto.setSalaryItemList(JsonUtil.getJsonToList(entity.getSalaryData(), FtbPersonnelsSalaryInfo.class)); + } + Boolean whitelist = personalizedTenantWhitelistUtils.isItOnTheWhitelist(); + if (whitelist) { + // 封装 LambdaQueryWrapper 创建逻辑 + FtbPersonnelsRegistrationFormField field = getFieldByName("意向组织"); + if (ObjectUtil.isEmpty(field)) { + return dto; // 如果字段不存在,直接退出 + } + String phone = dto.getPhone(); + if (ObjectUtil.isEmpty(phone)) { + return dto; // 如果电话号码为空,直接退出 + } + FtbPersonnelsStaffRegistrationFormData formData = getFormDataByPhoneAndFieldId(phone, field.getId()); + if (formData == null || ObjectUtil.isEmpty(formData.getValue())) { + return dto; // 如果表单数据不存在或值为空,直接退出 + } + try { + OrganizeEntity organizeList = organizeApi.getInfoById(formData.getValue()); + if (organizeList != null && ObjectUtil.isNotEmpty(organizeList.getFullName())) { + dto.setIntentionOrgName(organizeList.getFullName()); + } + } catch (Exception e) { + // 记录异常日志,确保程序不会因外部 API 异常而崩溃 + log.error("获取意向组织信息失败,phone: {}, formFieldId: {}", phone, field.getId(), e); + } + } + return dto; + } + // 封装方法:根据名称获取字段 + private FtbPersonnelsRegistrationFormField getFieldByName(String name) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsRegistrationFormField::getEnabledMark,0); + lambdaQuery.eq(FtbPersonnelsRegistrationFormField::getStatus, 0); + lambdaQuery.eq(FtbPersonnelsRegistrationFormField::getName, name); + return registrationFormFieldMapper.selectOne(lambdaQuery); + } + + // 封装方法:根据电话和字段 ID 获取表单数据 + private FtbPersonnelsStaffRegistrationFormData getFormDataByPhoneAndFieldId(String phone, String fieldId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRegistrationFormData::getPhone, phone); + wrapper.eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, fieldId); + return registrationFormDataService.getOne(wrapper); + } + private String queryRelationNameById(Integer source, String sourceId) { + String sourceName = ""; + if (null == source || source == 0) { + return sourceName; + } + if (source == 2) { + StoreEntity storeEntity = personnelOrgUtils.queryStoreInfo(sourceId); + if (null != storeEntity) { + sourceName = storeEntity.getStorename(); + } + } else if (source == 3) { + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(sourceId); + if (null != organizeEntity) { + sourceName = organizeEntity.getFullName(); + } + } else if (source == 1) { + UserEntity userEntity = personnelOrgUtils.queryUserInfo(sourceId); + if (null != userEntity) { + sourceName = userEntity.getRealName(); + } + } + return sourceName; + } + + private void fillContactInfo(FtbPersonnelsStaffEmploymentApplyDto dto) { + if (StringUtils.isNotEmpty(dto.getContractType())) { + String contractTypeName = personnelOrgUtils.queryContractTypeName(dto.getContractType()); + dto.setContractTypeName(contractTypeName); + } + } + + private void fillRankNameById(T dto) { + ActionResult gradesInfo = null; + try { + gradesInfo = positionApi.getGradesInfo(dto.getCurrRank()); + if (gradesInfo != null && gradesInfo.getData() != null) { + dto.setCurrRankName(gradesInfo.getData().getFullName()); + } + } catch (Exception e) { + log.error("根据职等ID查询职等信息异常,rankId={},{}", dto.getCurrRank(), e); + } + + } + + private void fillPositionNameById(T dto) { + PositionEntity entity = positionApi.queryInfoById(dto.getCurrPosition()); + if (entity != null) { + dto.setCurrPositionName(entity.getFullName()); + } + } + + private void fillOrgNameById(T dto) { + OrganizeEntity entity = organizeApi.getInfoById(dto.getCurrOrg()); + if (null != entity) { + dto.setCurrOrgName(entity.getFullName()); + } + } + + public FtbPersonnelsStaffEmploymentApply queryAndCheckStaffEmploymentApply(String id) { + FtbPersonnelsStaffEmploymentApply entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("数据不存在"); + } + if (entity.getEnabledMark() == 1) { + throw new RuntimeException("数据已经删除"); + } + return entity; + } + + + @Override + @Transactional(rollbackFor = RuntimeException.class) + public String insertData(AddStaffEmploymentApplyReq req) { + if (null == req.getEntryMoney()) { + req.setEntryMoney(BigDecimal.ZERO); + } + String phone = req.getPhone(); + // 手机号,离职黑名单校验 + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(req.getPhone())) { + throw new RuntimeException("当前人员处于黑名单中,请将其移出黑名单后再办理入职!"); + } + //查询手机号是否在已入职列表里面 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, req.getPhone()) + ; + FtbPersonnelsStaffRoster oldRosterEntity = rosterService.getOne(rosterWrapper); + if (null != oldRosterEntity) { + if (oldRosterEntity.getEnabledMark() == 0) { + if (oldRosterEntity.getWorkerStatus().equals("302") || oldRosterEntity.getWorkerStatus().equals("303") || oldRosterEntity.getWorkerStatus().equals("306")) { + throw new RuntimeException("该员工已经到岗,请勿重复操作!"); + } + + if (oldRosterEntity.getWorkerStatus().equals("304")) { + throw new RuntimeException("该员工已经在离职中!"); + } + } + } + String loginUserId = UserProvider.getLoginUserId(); + + //写入数据库 + FtbPersonnelsStaffEmploymentApply entity = BeanUtil.copyProperties(req, FtbPersonnelsStaffEmploymentApply.class); + entity.setStatus(0); + entity.setIsSubmitForm(0); + entity.setEnabledMark(0); + if (null == entity.getSource()) { + entity.setSource(0); + } + entity.setSourceRelation(queryRelationNameById(req.getSource(), req.getSourceRelationId())); + if (req.getIsNeedCheck() == 0) { + entity.setCheckStatus(1); + } + entity.setJoinNum(1); + if (oldRosterEntity != null && oldRosterEntity.getJoinNum() != null) { + entity.setJoinNum(oldRosterEntity.getJoinNum() + 1); + } + //获取当前登录用户 + entity.setOpUser(loginUserId); + + //临时存储薪酬数据 + entity.setSalaryData(JsonUtil.getObjectToString(req.getSalaryItemList())); + + entity.setOpUserName(UserProvider.getUser().getUserName()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone,phone); + FtbPersonnelsStaffEmploymentApply employmentApply = baseMapper.selectOne(queryWrapper); + if(employmentApply != null){ + entity.setId(employmentApply.getId()); + baseMapper.updateById(entity); + } else { + baseMapper.insert(entity); + } + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), "301"); + //同步数据到formData + Map formDataMap = BeanUtil.beanToMap(entity); + formDataMap.put("expectedStartDate", personnelOrgUtils.dateToString(req.getExpectedStartDate(), "")); + //查询内推池用户的基本信息 + QueryWrapper suikeWrap = new QueryWrapper<>(); + suikeWrap.lambda() + .eq(FtbPersonnelsUchisuikePond::getPhone, phone) + .eq(FtbPersonnelsUchisuikePond::getEnableMark, 0); + List ftbPersonnelsUchisuikePonds = uchisuikePondMapper.selectList(suikeWrap); + if (CollectionUtil.isNotEmpty(ftbPersonnelsUchisuikePonds)) { + FtbPersonnelsUchisuikePond ftbPersonnelsUchisuikePond = ftbPersonnelsUchisuikePonds.get(0); + //性别0男1女 + String workerSex = "0"; + if (ftbPersonnelsUchisuikePond.getSex() == null) { + workerSex = "0"; + } else if (ftbPersonnelsUchisuikePond.getSex() == 0) { + workerSex = "1"; + } else if (ftbPersonnelsUchisuikePond.getSex() == 1) { + workerSex = "2"; + } + formDataMap.put("workerSex", workerSex); + formDataMap.put("idCardNum", ftbPersonnelsUchisuikePond.getIdCard()); + //考勤组id,来自于考勤组 + if (StrUtil.isNotBlank(req.getAttendanceGroup())) { + formDataMap.put("attendanceGroup", req.getAttendanceGroup()); + } + + } + registrationFormDataService.syncData(formDataMap, entity.getPhone(), ""); + + return entity.getId(); + } + + @Override + public ActionResult handleJoinJobForOA(AddStaffEmploymentApplyReq req) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + if (null == req.getEntryMoney()) { + req.setEntryMoney(BigDecimal.ZERO); + } + String phone = req.getPhone(); + req.setPhone(phone); + // 手机号,离职黑名单校验 + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(req.getPhone())) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("当前人员处于黑名单中,请将其移出黑名单后再办理入职!"); + return result; + } + if (req.getSpecialBusinessId() != null) { + result = reApprovalForOA(req.getSpecialBusinessId(), req); + } else{ + // 手动做校验数据 + if (!PersonnelOrgUtils.isValidPhoneNumber(phone)) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("手机号格式不正确"); + return result; + } + //查询手机号是否在已入职列表里面 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, req.getPhone()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster oldRosterEntity = rosterService.getOne(rosterWrapper); + if (null != oldRosterEntity) { + if (oldRosterEntity.getEnabledMark() == 0) { + if (oldRosterEntity.getWorkerStatus().equals("302") || oldRosterEntity.getWorkerStatus().equals("303") || oldRosterEntity.getWorkerStatus().equals("306")) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("该员工已经到岗,请勿重复操作!"); + return result; + } + + if (oldRosterEntity.getWorkerStatus().equals("304")) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("该员工已经在离职中!"); + return result; + } + } + } + String loginUserId = UserProvider.getLoginUserId(); + String photoFiles = req.getPhotoFiles(); + if (StringUtils.isNotEmpty(photoFiles)) { + List entities = JSONObject.parseArray(photoFiles, FileEntity.class); + if (entities.size() > 1) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("考勤人脸录入图片最多只能上传一张!"); + return result; + } + } + //写入数据库 + FtbPersonnelsStaffEmploymentApply entity = BeanUtil.copyProperties(req, FtbPersonnelsStaffEmploymentApply.class); + entity.setStatus(0); + entity.setIsSubmitForm(0); + entity.setEnabledMark(0); + if (null == entity.getSource()) { + entity.setSource(0); + } + entity.setSourceRelation(queryRelationNameById(req.getSource(), req.getSourceRelationId())); + entity.setIsNeedCheck(0); + entity.setCheckStatus(1); + if (oldRosterEntity != null && oldRosterEntity.getJoinNum() != null) { + entity.setJoinNum(oldRosterEntity.getJoinNum() + 1); + } + //获取当前登录用户 + entity.setOpUser(loginUserId); + //临时存储薪酬数据 + entity.setSalaryData(JsonUtil.getObjectToString(req.getSalaryItemList())); + entity.setOpUserName(UserProvider.getUser().getUserName()); + entity.setVersionNum(1); + entity.setTaskInfoId(req.getTaskId()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone,phone); + FtbPersonnelsStaffEmploymentApply employmentApply = baseMapper.selectOne(queryWrapper); + if(employmentApply != null) entity.setId(employmentApply.getId()); + baseMapper.updateById(entity); + //同步数据到formData + Map formDataMap = BeanUtil.beanToMap(entity); + formDataMap.put("expectedStartDate", personnelOrgUtils.dateToString(req.getExpectedStartDate(), "")); + //查询内推池用户的基本信息 + QueryWrapper suikeWrap = new QueryWrapper<>(); + suikeWrap.lambda() + .eq(FtbPersonnelsUchisuikePond::getPhone, phone) + .eq(FtbPersonnelsUchisuikePond::getEnableMark, 0); + List ftbPersonnelsUchisuikePonds = uchisuikePondMapper.selectList(suikeWrap); + if (CollectionUtil.isNotEmpty(ftbPersonnelsUchisuikePonds)) { + FtbPersonnelsUchisuikePond ftbPersonnelsUchisuikePond = ftbPersonnelsUchisuikePonds.get(0); + //性别0男1女 + String workerSex = "0"; + if (ftbPersonnelsUchisuikePond.getSex() == null) { + workerSex = "0"; + } else if (ftbPersonnelsUchisuikePond.getSex() == 0) { + workerSex = "1"; + } else if (ftbPersonnelsUchisuikePond.getSex() == 1) { + workerSex = "2"; + } + formDataMap.put("workerSex", workerSex); + formDataMap.put("idCardNum", ftbPersonnelsUchisuikePond.getIdCard()); + //考勤组id,来自于考勤组 + if (StrUtil.isNotBlank(req.getAttendanceGroup())) { + formDataMap.put("attendanceGroup", req.getAttendanceGroup()); + } + + } + registrationFormDataService.syncData(formDataMap, entity.getPhone(), ""); + + } + return result; + } + + @Override + public ActionResult reApprovalForOA(String id, AddStaffEmploymentApplyReq req) { + ActionResult result = new ActionResult<>(); + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + + FtbPersonnelsStaffEmploymentApply entity = queryAndCheckStaffEmploymentApply(id); + if (!entity.getPhone().equals(req.getPhone())){ + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("重新办理选择用户不一致!请重新选择正确的用户!"); + return result; + } + if (entity.getIsNeedCheck() == 1) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("该员工不需要审核"); + return result; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("该员工审核已经通过"); + return result; + } + + //查询手机号是否在已入职列表里面 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, req.getPhone()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + ; + FtbPersonnelsStaffRoster oldRosterEntity = rosterService.getOne(rosterWrapper); + if (null != oldRosterEntity) { + if (!oldRosterEntity.getWorkerStatus().equals("305")) { + result.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg("手机号所属员工已经存在花名册中"); + return result; + } + } + String loginUserId = UserProvider.getLoginUserId(); + //写入数据库 + FtbPersonnelsStaffEmploymentApply newEntity = BeanUtil.copyProperties(req, FtbPersonnelsStaffEmploymentApply.class); + newEntity.setStatus(0); + newEntity.setEnabledMark(0); + newEntity.setId(id); + newEntity.setIsSubmitForm(entity.getIsSubmitForm()); + if (req.getIsNeedCheck() == 0) { + newEntity.setCheckStatus(1); + } + newEntity.setJoinNum(1); + if (oldRosterEntity != null && oldRosterEntity.getJoinNum() != null) { + newEntity.setJoinNum(oldRosterEntity.getJoinNum() + 1); + } + //获取当前登录用户 + newEntity.setOpUser(loginUserId); + newEntity.setTaskInfoId(req.getTaskId()); + newEntity.setOpUserName(UserProvider.getUser().getUserName()); + newEntity.setCreatorTime(new Date()); + newEntity.setVersionNum(1); + baseMapper.updateById(newEntity); + //同步数据到formData + Map formDataMap = BeanUtil.beanToMap(newEntity); + formDataMap.put("expectedStartDate", personnelOrgUtils.dateToString(req.getExpectedStartDate(), "")); + registrationFormDataService.syncData(formDataMap, newEntity.getPhone(),"" ); + return result; + } + + @Override + public PageListVO onboardingList(QueryStaffEmploymentApplyListReq req) { + Page page = new Page<>(); + page.setCurrent(req.getCurrentPage()); + page.setSize(req.getPageSize()); + Integer checkStatus = req.getCheckStatus(); + if (checkStatus != null){ + boolean b = checkStatus == 4; + if (b){ + req.setIsNeedCheck(1); + req.setCheckStatus(null); + }else { + req.setIsNeedCheck(0); + } + } + Boolean whitelist = personalizedTenantWhitelistUtils.isItOnTheWhitelist(); + req.setExt(whitelist); + page = baseMapper.onboardingList(page, req); + // 数据补全 + List records = page.getRecords(); + if (CollUtil.isEmpty( records)) return CultivatePage.coverPageList(page); + List phoneList = records.stream().map(FtbPersonnelsEmployApplyDto::getPhone).collect(Collectors.toList()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsStaffRegistrationFormData::getPhone,phoneList); + // idCardNum 身份证 + // flowerName 花名 + // birthday 出生日期 + wrapper.in(FtbPersonnelsStaffRegistrationFormData::getFormFieldId,"idCardNum","flowerName","birthday","workerSex"); + List formData = registrationFormDataService.list(wrapper); + Map> formDataPhoneMap = formData.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getPhone, Collectors.toList())); + // 数据补充 + Map orgMap = new HashMap<>(); + if (whitelist) { + List intentionOrgIds = records.stream().map(FtbPersonnelsEmployApplyDto::getIntentionOrgId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(intentionOrgIds)) { + List organizeList = organizeApi.getOrganizeName(intentionOrgIds); + orgMap = organizeList.stream().collect(Collectors.toMap(OrganizeEntity::getId, OrganizeEntity::getFullName)); + } + } + Map finalOrgMap = orgMap; + records.forEach(item->{ + List registrationFormData = formDataPhoneMap.get(item.getPhone()); + if (CollUtil.isNotEmpty(registrationFormData)) { + Map stringMap = registrationFormData.stream().collect(Collectors.toMap(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, FtbPersonnelsStaffRegistrationFormData::getValue, (a, b) -> a)); + item.setIdCardNum(stringMap.get("idCardNum")); + item.setFlowerName(stringMap.get("flowerName")); + item.setBirthday(stringMap.get("birthday")); + String str = stringMap.getOrDefault("workerSex", null); + if (StringUtils.isNotEmpty(str)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getFormFieldId, "workerSex"); + queryWrapper.eq(FtbPersonnelsRegistrationFormFieldOption::getId, str); + FtbPersonnelsRegistrationFormFieldOption fieldOption = optionMapper.selectOne(queryWrapper); + item.setGender(fieldOption.getName()); + } + } + if (item.getIsNeedCheck() != null && 1 == item.getIsNeedCheck()){ + item.setIsNeedCheck(null); + item.setCheckStatus(4); + } + if (whitelist && finalOrgMap.containsKey(item.getIntentionOrgId())) { + item.setIntentionOrgName(finalOrgMap.get(item.getIntentionOrgId())); + } + }); + return CultivatePage.coverPageList(page); + } + + @Override + @GlobalTransactional(rollbackFor = RuntimeException.class) + public ActionResult approvalForOA(EmploymentApplyCheckDto dto) { + ActionResult result = new ActionResult<>(); + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + result.setMsg( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getTaskInfoId,dto.getTaskId()); + FtbPersonnelsStaffEmploymentApply entity = baseMapper.selectOne(queryWrapper); + if (entity.getIsNeedCheck() == 1) { + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "该员工入职不需要审核"); + return result; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "该员工入职审核已经通过"); + return result; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 3) { + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "该员工入职审核已经不通过"); + return result; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 4) { + result.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + result.setMsg( "该员工入职审核已经撤销"); + return result; + } + FtbPersonnelsAuditDto checkDto = new FtbPersonnelsAuditDto(); + checkDto.setBusinessId(entity.getId()); + if (ObjectUtil.isNotEmpty(dto.getIsConformSalary())) { + checkDto.setDoesTheSalaryComplyWith(dto.getIsConformSalary()); + } + if (ObjectUtil.isNotEmpty(dto.getMsg())) { + checkDto.setApprovalComments(dto.getMsg()); + } + if (ObjectUtil.isNotEmpty(dto.getIsPass())) { + checkDto.setFlag(String.valueOf(dto.getIsPass())); + } + // 根据这个OA审批结果,设置状态 + FtbPersonnelsAuditTaskEnum code = dto.getIsPass() == 0 ? FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED : FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + switch (code) { + case EXAMINATION_PASSED: + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), "301"); + entity.setCheckStatus(2); + break; + case AUDIT_NOT_PASSED: + entity.setCheckStatus(3); + break; + } + baseMapper.updateById(entity); + return result; + } + + + /** + * 根据手机号检测状态 + * 0、系统不存在该用户 1、该手机号等待入职 2、该手机号等待入职审批 3、该手机号入职审批流程不通过,请重新办理 302、试用 303、正式 304、待离职 305 离职 4-该手机号已被拉入黑名单 + * + * @param phone + * @return + */ + @Override + public String checkPhoneStatus(String phone) { + + //手机号黑名单校验 + if (ftbPersonnelsBlacklistService.hasItBeenBlacklisted(phone)) { + return Integer.toString(PhoneStatusEnum.BASE_BLACKLIST.getCode()); + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0) + .last("limit 1") + ; + + FtbPersonnelsStaffEmploymentApply employmentApplyEntity = baseMapper.selectOne(wrapper); + if (null != employmentApplyEntity) { + if (employmentApplyEntity.getIsNeedCheck() == PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) {//0、需要审批 1、不需要审批 + return Integer.toString(PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()); + } else { + if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode() && employmentApplyEntity.getStatus() != PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) { + return Integer.toString(PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()); + } else if ( employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) { + return Integer.toString(PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode()); + } else if (( employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode())) { + return Integer.toString(PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode()); + } else if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_REJECT.getCode()) { + return Integer.toString(PhoneStatusEnum.BASE_APPROVAL_REJECT.getCode()); + } + } + } + + + + //查询手机号是否在已入职列表里面 + QueryWrapper rosterWrapper = new QueryWrapper<>(); + rosterWrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, phone) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .last("limit 1") + ; + FtbPersonnelsStaffRoster roster = rosterService.getOne(rosterWrapper); + if (null != roster) { + return roster.getWorkerStatus(); + } + return "0"; + + } + + @Override + public void saveShortUrl(String id, String shortUrl) { + FtbPersonnelsStaffEmploymentApply employmentApply = queryAndCheckStaffEmploymentApply(id); + employmentApply.setShortUrl(shortUrl); + baseMapper.updateById(employmentApply); + } + + @Override + public void sendPhoneMsg(String phone) { + + if (!personnelOrgUtils.isValidPhoneNumber(phone)) { + throw new RuntimeException("手机号格式不正确"); + } + Boolean isFill = checkFillRegistrationForm(phone); + if (!isFill) { + throw new RuntimeException("该手机号已经填写了入职登记表"); + } +// if (personnelOrgUtils.queryIsSendPhoneMsg(phone)) { +// throw new RuntimeException("今日已经发送了短信"); +// } + // Map map = new LinkedHashMap<>(); +// try { +// UserInfo userInfo = UserProvider.getUser(); +// String storeCode = personnelOrgUtils.queryCodeForTenantId(userInfo.getTenantId()); +// map.put("code", storeCode); +// smsSendUtil.sendSms(List.of(phone), "ENTRY_FORM", map); +// personnelOrgUtils.setTodayIsSendPhoneMsg(phone); +// +// } catch (Exception e) { +// log.error("发送验证码异常:{}", e.getMessage()); +// throw new RuntimeException("短信发送失败", e); +// } + } + + /** + * 检测是否需要填写入职登记表 + * + * @param phone + * @return true 需要填写 ,false 不需要填写 + */ + @Override + public Boolean checkFillRegistrationForm(String phone) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getIsSubmitForm, 1); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply apply = baseMapper.selectOne(queryWrapper); + log.error("校验入职信息apply:{}", apply); + if (ObjectUtil.isNotEmpty(apply)) { + return false; + } + // 查询手机号是否在员工信息列表里面 + LambdaQueryWrapper rosterWrapper = Wrappers.lambdaQuery(); + rosterWrapper.eq(FtbPersonnelsStaffRoster::getPhone, phone); + rosterWrapper.notIn(FtbPersonnelsStaffRoster::getWorkerStatus, "305"); + rosterWrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster roster = rosterService.getOne(rosterWrapper); + log.error("花名册 roster:{}", apply); + if (ObjectUtil.isNotEmpty(roster)) { + return false; + } + return true; + } + + /** + * 查询租户编码 + * + * @return + */ + + @Override + public String queryCode() { + UserInfo userInfo = UserProvider.getUser(); + String storeCode = personnelOrgUtils.queryCodeForTenantId(userInfo.getTenantId()); + return storeCode; + } + + @Override + public void updateEmploymentApplyStatus() { + //修改修改不需要审批的 + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(FtbPersonnelsStaffEmploymentApply::getStatus, 2) + .lt(FtbPersonnelsStaffEmploymentApply::getExpectedStartDate, new Date()) + .eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 1) + .eq(FtbPersonnelsStaffEmploymentApply::getStatus,0) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + baseMapper.update(null, wrapper); + + //修改需要审批的 + wrapper = new LambdaUpdateWrapper() + .set(FtbPersonnelsStaffEmploymentApply::getStatus, 2) + .lt(FtbPersonnelsStaffEmploymentApply::getExpectedStartDate, new Date()) + .eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 0) + .eq(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 2) + .eq(FtbPersonnelsStaffEmploymentApply::getStatus,0) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + baseMapper.update(null, wrapper); + } + + @Override + public void deleteByPhone(String phone) { + QueryWrapper applay = new QueryWrapper<>(); + applay.lambda().eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone); + baseMapper.delete(applay); + } + + @Override + public void verifyUserBound(String positionId, String positionGradesId) { + + if (StrUtil.isNotBlank(positionId)) { + List staffEmploymentApplies = this.list(Wrappers.lambdaQuery() + .eq(FtbPersonnelsStaffEmploymentApply::getCurrPosition, positionId) + .eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 0) + .in(FtbPersonnelsStaffEmploymentApply::getCheckStatus,0,1) + ); + if (CollUtil.isNotEmpty(staffEmploymentApplies)) { + throw new RuntimeException("该岗位存在于办理入职的用户中,不允许删除"); + } + } + if (StrUtil.isNotBlank(positionGradesId)) { + List staffEmploymentApplies = this.list(Wrappers.lambdaQuery() + .eq(FtbPersonnelsStaffEmploymentApply::getCurrRank, positionGradesId) + .eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 0) + .in(FtbPersonnelsStaffEmploymentApply::getCheckStatus,0,1) + ); + if (CollUtil.isNotEmpty(staffEmploymentApplies)) { + throw new RuntimeException("该职等存在于办理入职的用户中,不允许删除"); + } + } + } + + @Override + public FtbPersonnelsBubbleCountVO getListCont(String flag) { + // flag "1" 我的审批 "2" 抄送 + FtbPersonnelsBubbleCountVO countVO = new FtbPersonnelsBubbleCountVO(); + String userId = UserProvider.getUser().getUserId(); + Integer configType = FtbPersonnelsCofigEnum.ONBOARDING_APPROVAL_CONFIGURATION.getConfigType(); + if ("1".equals(flag)) { + // 待审批 + Page dtoPage = baseMapper.pagingQueryWebMyCheckList(Page.of(1, 1), userId, configType); + countVO.setPendingCount(dtoPage.getTotal()); + // 审核中 + Page quantityUnderCount = baseMapper.getApprovedList(Page.of(1, 1), userId, configType, 1); + countVO.setQuantityUnderCount(quantityUnderCount.getTotal()); + // 审核通过 + Page approvedCount = baseMapper.getApprovedList(Page.of(1, 1), userId, configType, 2); + countVO.setApprovedCount(approvedCount.getTotal()); + // 审核不通过 + Page approvalFailedCount = baseMapper.getApprovedList(Page.of(1, 1), userId, configType,3); + countVO.setApprovalFailedCount(approvalFailedCount.getTotal()); + } else { + Page quantityUnderCount = baseMapper.getListForApp(Page.of(1, 1), userId, 1); + countVO.setQuantityUnderCount(quantityUnderCount.getTotal()); + Page approvedCount = baseMapper.getListForApp(Page.of(1, 1), userId, 2); + countVO.setApprovedCount(approvedCount.getTotal()); + Page approvalFailedCount = baseMapper.getListForApp(Page.of(1, 1), userId, 3); + countVO.setApprovalFailedCount(approvalFailedCount.getTotal()); + } + return countVO; + } + + @Override + public void exportDateNew(QueryStaffEmploymentApplyListReq req, HttpServletResponse response) throws IOException { + req.setPageSize(Integer.MAX_VALUE); + PageInfo pageVo = this.getPageList(req); + List list = pageVo.getList() + .parallelStream() + .map(FtbPersonnelsStaffEmploymentApplyVO::convert) + .collect(Collectors.toList()); + String fileName = "预办理入职员工信息"; + EasyExcelUtils.exportExcel(response, fileName, list, FtbPersonnelsStaffEmploymentApplyVO.class); + } + + + @Override + public void updateData(SavePaperReq req) { + + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteData(String id) { + FtbPersonnelsStaffEmploymentApply entity = queryAndCheckStaffEmploymentApply(id); + //检测是否可以删除 + //不需要审批 未到岗可以删除 + if (entity.getIsNeedCheck() == 1) { + if (entity.getStatus() == 1) { + throw new RuntimeException("该员工已经到岗,不能删除"); + } + } else { + //需要审批,审批不通过可以删除 + if (entity.getCheckStatus() == 0) { + throw new RuntimeException("该员工等待审批,不能删除"); + } else if (entity.getCheckStatus() == 1) { + throw new RuntimeException("该员工审批中,不能删除"); + } else if (entity.getCheckStatus() == 2 && entity.getStatus() == 1) { + throw new RuntimeException("该员工已经入职,不能删除"); + } + } + //删除 + baseMapper.deleteById(id); + //删除表单字段 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRegistrationFormData::getPhone, entity.getPhone()); + registrationFormDataService.remove(wrapper); + //重置今日发送短信 + personnelOrgUtils.resetTodaySendPhoneMsg(entity.getPhone()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteBatchData(List ids) { + //查询数据 + List employmentApplyList = baseMapper.selectBatchIds(ids); + if (CollectionUtil.isEmpty(employmentApplyList)) { + return; + } + for (FtbPersonnelsStaffEmploymentApply entity : employmentApplyList) { + //检测是否可以删除 + //不需要审批 未到岗可以删除 + if (entity.getIsNeedCheck() == 1) { + if (entity.getStatus() == 1) { + throw new RuntimeException("员工[" + entity.getWorkerName() + "]已经到岗,不能删除"); + } + } else { + //需要审批,审批不通过可以删除 + if (entity.getCheckStatus() == 0) { + throw new RuntimeException("员工[" + entity.getWorkerName() + "]待审批,不能删除"); + } else if (entity.getCheckStatus() == 1) { + throw new RuntimeException("员工[" + entity.getWorkerName() + "]入职审批中,不能删除"); + } else if (entity.getCheckStatus() == 2 && entity.getStatus() == 1) { + throw new RuntimeException("员工[" + entity.getWorkerName() + "]已经入职,不能删除"); + } + } + } + + //批量删除 + baseMapper.deleteBatchIds(ids); + //批量删除表单字段 + List phoneList = employmentApplyList.stream() + .map(FtbPersonnelsStaffEmploymentApply::getPhone) // 提取每个元素的电话号码 + .filter(Objects::nonNull) // 过滤掉null值(如果电话号码可能为null的话) + .collect(Collectors.toList()); + +// 查询花名册是否存在 + + if(CollectionUtil.isNotEmpty(phoneList)) { + List rosterList = rosterService.list(new QueryWrapper().lambda() + .in(FtbPersonnelsStaffRoster::getPhone, phoneList) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .ne(FtbPersonnelsStaffRoster::getWorkerStatus, "305") + ); + if(CollectionUtil.isNotEmpty(rosterList)){ + List deletePhoneList = new ArrayList<>(); + List notDeletePhoneList = new ArrayList<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { + notDeletePhoneList.add(roster.getPhone()); + } + for (String phone : phoneList) { + if(!notDeletePhoneList.contains(phone)){ + deletePhoneList.add(phone); + } + } + if(CollectionUtil.isNotEmpty(deletePhoneList)){ + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().in(FtbPersonnelsStaffRegistrationFormData::getPhone, deletePhoneList); + registrationFormDataService.remove(wrapper); + } + }else { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().in(FtbPersonnelsStaffRegistrationFormData::getPhone, phoneList); + registrationFormDataService.remove(wrapper); + } + } + + //重置今日发送短信 + for (FtbPersonnelsStaffEmploymentApply entity : employmentApplyList) { + personnelOrgUtils.resetTodaySendPhoneMsg(entity.getPhone()); + } + } + + @Override + public void updateConfirmArrivalStatus(String id) { + // baseMapper.deleteById(id); + FtbPersonnelsStaffEmploymentApply employmentApply = new FtbPersonnelsStaffEmploymentApply(); + employmentApply.setId(id); + employmentApply.setStatus(1); + baseMapper.updateById(employmentApply); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffGrowthLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffGrowthLogServiceImpl.java new file mode 100644 index 0000000..a4e1059 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffGrowthLogServiceImpl.java @@ -0,0 +1,111 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.enums.GrowthLogEnum; +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.growth.FtbPersonnelsStaffGrowthLogDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffGrowthLog; +import jnpf.personnels.mapper.FtbPersonnelsStaffGrowthLogMapper; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsStaffGrowthLogServiceImpl extends ServiceImpl implements FtbPersonnelsStaffGrowthLogService { + + @Override + public List queryAll(String userId) { + if(StringUtils.isEmpty(userId)){ + return new ArrayList<>(); + } + List list = baseMapper.selectList( + new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffGrowthLog::getUserId, userId) + .orderByDesc(FtbPersonnelsStaffGrowthLog::getCreatorTime) + ); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return BeanUtil.copyToList(list, FtbPersonnelsStaffGrowthLogDto.class); + } + + /** + * 添加成长记录 + * + * @param dto + */ + @Override + public void addGrowthLog(AddGrowthLogDto dto) { + FtbPersonnelsStaffGrowthLog entity = new FtbPersonnelsStaffGrowthLog(); + entity.setUserId(dto.getUserId()); + entity.setChangeDate(dto.getChangeDate()); + GrowthLogEnum growthLogEnum = GrowthLogEnum.fromCode(dto.getGrowthType()); + entity.setGrowthType(dto.getGrowthType()); + if (GrowthLogEnum.REPEAT_JOIN == growthLogEnum) { + entity.setTitle(String.format(growthLogEnum.getMsg(), dto.getNum())); + entity.setNum(dto.getNum()); + } + if (GrowthLogEnum.FIRST_JOIN == growthLogEnum) { + entity.setTitle(growthLogEnum.getMsg()); + entity.setNum(1); + } else { + entity.setTitle(growthLogEnum.getMsg()); + } + entity.setDetail(dto.getDetail()); + entity.setRemarks(dto.getRemarks()); + entity.setStartSalary(dto.getStartSalary()); + entity.setActualStartDate(dto.getActualStartDate()); + entity.setEmployeeId(dto.getEmployeeId()); + baseMapper.insert(entity); + } + + @Override + public void revocationGrowthLog(String userId, Date startTime) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getUserId,userId); + queryWrapper.eq(FtbPersonnelsStaffGrowthLog::getGrowthType,1); + DateTime begin = DateUtil.beginOfDay(startTime); + DateTime end = DateUtil.endOfDay(startTime); + queryWrapper.between(FtbPersonnelsStaffGrowthLog::getChangeDate,begin,end); + baseMapper.delete(queryWrapper); + } + + @Override + public void addGrowthLogBatch(List addGrowthLogDtos) { + List entityList = addGrowthLogDtos.stream().map(dto -> { + FtbPersonnelsStaffGrowthLog entity = new FtbPersonnelsStaffGrowthLog(); + entity.setUserId(dto.getUserId()); + entity.setChangeDate(dto.getChangeDate()); + GrowthLogEnum growthLogEnum = GrowthLogEnum.fromCode(dto.getGrowthType()); + entity.setGrowthType(dto.getGrowthType()); + if (GrowthLogEnum.REPEAT_JOIN == growthLogEnum) { + entity.setTitle(String.format(growthLogEnum.getMsg(), dto.getNum())); + entity.setNum(dto.getNum()); + } + if (GrowthLogEnum.FIRST_JOIN == growthLogEnum) { + entity.setTitle(growthLogEnum.getMsg()); + entity.setNum(1); + } else { + entity.setTitle(growthLogEnum.getMsg()); + } + entity.setDetail(dto.getDetail()); + entity.setRemarks(dto.getRemarks()); + entity.setStartSalary(dto.getStartSalary()); + entity.setActualStartDate(dto.getActualStartDate()); + entity.setEmployeeId(dto.getEmployeeId()); + return entity; + }).collect(Collectors.toList()); + this.saveBatch(entityList); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffHomePageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffHomePageServiceImpl.java new file mode 100644 index 0000000..2d944dd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffHomePageServiceImpl.java @@ -0,0 +1,392 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.model.enums.GrowthLogEnum; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsEmployApplyListDto; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsMoveLogVO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsRegularVO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsTurnoverLogVO; +import jnpf.model.personnels.dto.staff.roster.StaffDepartDto; +import jnpf.model.personnels.dto.staff.roster.StaffPromotionDto; +import jnpf.model.personnels.dto.staff.roster.StaffRegularDto; +import jnpf.model.personnels.dto.staff.transfer.TransferGrowthLogDto; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.po.FtbPersonnelsStaffGrowthLog; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsTransferManage; +import jnpf.model.personnels.vo.roster.FtbHomePageRewardVO; +import jnpf.model.personnels.vo.roster.FtbPersonnelsChangeInfoVO; +import jnpf.model.personnels.vo.secondment.FtbPersonnelsSecondRecordVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffHomePageMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import jnpf.personnels.service.FtbPersonnelsStaffHomePageService; +import jnpf.personnels.service.FtbPersonnelsTransferManageService; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsStaffHomePageServiceImpl implements FtbPersonnelsStaffHomePageService { + + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + @Resource + private FtbPersonnelsStaffHomePageMapper ftbPersonnelsStaffHomePageMapper; + @Resource + private V2UserApi v2UserApi; + @Resource + private FtbPersonnelsStaffGrowthLogService staffGrowthLogService; + @Resource + FtbPersonnelsTransferManageService transferManageService; + @Resource + private FtbPersonnelsSecondmentManagementService ftbPersonnelsSecondmentService; + + @Override + public String checkPersonalCommitments(String id) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPersonnelsStaffRoster::getRegisterImg); + queryWrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, id); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbPersonnelsStaffRoster)) { + return ftbPersonnelsStaffRoster.getRegisterImg(); + } + return ""; + } + + @Override + public List getRewardList(String id) { + List rewardList = ftbPersonnelsStaffHomePageMapper.getRewardList(id); + operatorHandling(rewardList); + return rewardList; + } + + @Override + public List donationRecords(String id) { + List ftbHomePageRewardVOS = ftbPersonnelsStaffHomePageMapper.donationRecords(id); + operatorHandling(ftbHomePageRewardVOS); + return ftbHomePageRewardVOS; + } + + @Override + public Map getBatchPersonnelChangeInfo(List userIds) { + if (CollUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + + // 1. 批量查询所有相关 growth_log + LambdaQueryWrapper logWrapper = Wrappers.lambdaQuery(); + logWrapper.in(FtbPersonnelsStaffGrowthLog::getUserId, userIds); + // 可选:只查需要的类型,减少数据量 + // logWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType, 需要的类型列表); + List allLogs = staffGrowthLogService.list(logWrapper); + + // 2. 批量查询所有相关的 transfer_manage + LambdaQueryWrapper transferWrapper = Wrappers.lambdaQuery(); + transferWrapper.in(FtbPersonnelsTransferManage::getUserId, userIds); + transferWrapper.in(FtbPersonnelsTransferManage::getProcessingStatus, List.of(6, 2)); + transferWrapper.eq(FtbPersonnelsTransferManage::getImplementType, 1); + List allTransfers = transferManageService.list(transferWrapper); + + // 3. 批量查询所有相关的 secondment + LambdaQueryWrapper secondWrapper = Wrappers.lambdaQuery(); + secondWrapper.in(FtbPersonnelsSecondmentManagement::getUserId, userIds); + secondWrapper.in(FtbPersonnelsSecondmentManagement::getTransferStatus, 6, 2); + List allSecondments = ftbPersonnelsSecondmentService.list(secondWrapper); + + LambdaQueryWrapper staffRosterLambdaQueryWrapper = Wrappers.lambdaQuery(); + staffRosterLambdaQueryWrapper.in(FtbPersonnelsStaffRoster::getUserId, userIds); + List selectList = ftbPersonnelsStaffRosterMapper.selectList(staffRosterLambdaQueryWrapper); + Map staffRosterMap = selectList + .stream() + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, Function.identity())); + // 4. 收集所有涉及到的 userId(包括 creatorUserId) + Set allRelatedUserIds = new HashSet<>(userIds); + + allLogs.forEach(log -> { + allRelatedUserIds.add(log.getCreatorUserId()); + allRelatedUserIds.add(log.getUserId()); + }); + allTransfers.forEach(t -> { + allRelatedUserIds.add(t.getCreatorUserId()); + if (t.getLastModifyUserId() != null) { + allRelatedUserIds.add(t.getLastModifyUserId()); + } + }); + allSecondments.forEach(s -> allRelatedUserIds.add(s.getCreatorUserId())); + + // 5. 批量获取用户名 + ActionResult> boundResult = v2UserApi.getUserPrimaryBoundBatch( + new ArrayList<>(allRelatedUserIds), UserProvider.getUser().getTenantId()); + + Map userNameMap = new HashMap<>(); + if (boundResult.getData() != null) { + userNameMap = boundResult.getData().stream() + .collect(Collectors.toMap(UserBoundVO::getId, UserBoundVO::getUserName, (a, b) -> a)); + } + + // 6. 按 userId 分组 growth log + Map> logByUser = allLogs.stream() + .collect(Collectors.groupingBy(FtbPersonnelsStaffGrowthLog::getUserId)); + + // 7. 按 userId 分组 transfer + Map> transferByUser = allTransfers.stream() + .collect(Collectors.groupingBy(FtbPersonnelsTransferManage::getUserId)); + + // 8. 按 userId 分组 secondment + Map> secondByUser = allSecondments.stream() + .collect(Collectors.groupingBy(FtbPersonnelsSecondmentManagement::getUserId)); + + // 9. 按 userId 批量构建 VO + Map result = new LinkedHashMap<>(); + + for (String uid : userIds) { + FtbPersonnelsChangeInfoVO vo = new FtbPersonnelsChangeInfoVO(); + + // 入职 + vo.setOnboarding(convertToEmployApplyList(logByUser.getOrDefault(uid, Collections.emptyList()), userNameMap)); + + // 转正 + vo.setRegularization(convertToRegularList(logByUser.getOrDefault(uid, Collections.emptyList()), userNameMap)); + + // 调动(包含旧 growth_log + 新 transfer_manage) + vo.setMobilize(convertToMoveLogList( + logByUser.getOrDefault(uid, Collections.emptyList()), + transferByUser.getOrDefault(uid, Collections.emptyList()), + userNameMap)); + + // 借调 + vo.setSeconded(convertToSecondRecordList( + secondByUser.getOrDefault(uid, Collections.emptyList()), + userNameMap, + staffRosterMap + )); + + // 离职 + vo.setTurnover(convertToTurnoverList(logByUser.getOrDefault(uid, Collections.emptyList()), userNameMap)); + + result.put(uid, vo); + } + + return result; + } + + private List convertToEmployApplyList( + List logs, Map nameMap) { + return logs.stream() + .filter(log -> log.getGrowthType() == 0 || log.getGrowthType() == 3) + .map(log -> { + FtbPersonnelsEmployApplyListDto dto = FtbPersonnelsEmployApplyListDto.covert(log); + dto.setWorkName(nameMap.getOrDefault(log.getUserId(), "-")); + dto.setOperator(nameMap.getOrDefault(log.getCreatorUserId(), "-")); + return dto; + }) + .collect(Collectors.toList()); + } + + private List convertToRegularList( + List logs, + Map userNameMap) { + + return logs.stream() + .filter(log -> Objects.equals(log.getGrowthType(), GrowthLogEnum.BECOME_REGULAR_WORKER.getCode())) + .map(log -> { + StaffRegularDto dto = JSONUtil.toBean(log.getDetail(), StaffRegularDto.class); + + FtbPersonnelsRegularVO vo = FtbPersonnelsRegularVO.covert(dto); + vo.setName(userNameMap.getOrDefault(log.getUserId(), "-")); + vo.setProcessingDate(log.getCreatorTime()); + vo.setEmployeeID(log.getEmployeeId()); + vo.setOperator(userNameMap.getOrDefault(log.getCreatorUserId(), "-")); + + return vo; + }) + .collect(Collectors.toList()); + } + + private List convertToTurnoverList( + List logs, + Map userNameMap) { + + return logs.stream() + .filter(log -> Objects.equals(log.getGrowthType(), GrowthLogEnum.DEPART.getCode()) || + Objects.equals(log.getGrowthType(), GrowthLogEnum.PRE_TRAIL.getCode())) + .map(log -> { + FtbPersonnelsTurnoverLogVO vo = new FtbPersonnelsTurnoverLogVO(); + + if (StrUtil.isNotBlank(log.getDetail()) && !"[]".equals(log.getDetail())) { + StaffDepartDto dto; + try { + // 优先尝试数组格式(兼容旧数据) + List list = JSONArray.parseArray(log.getDetail(), StaffDepartDto.class); + dto = CollUtil.isNotEmpty(list) ? list.get(0) : null; + } catch (Exception e) { + // 再尝试单个对象 + dto = JSONObject.parseObject(log.getDetail(), StaffDepartDto.class); + } + + if (dto != null) { + vo = FtbPersonnelsTurnoverLogVO.covert(dto); + } + } + + vo.setName(userNameMap.getOrDefault(log.getUserId(), "-")); + vo.setProcessingDate(log.getChangeDate() != null ? log.getChangeDate() : log.getCreatorTime()); + vo.setEmployeeID(log.getEmployeeId()); + vo.setOperator(userNameMap.getOrDefault(log.getCreatorUserId(), "-")); + + return vo; + }) + .collect(Collectors.toList()); + } + + private List convertToSecondRecordList( + List secondments, + Map userNameMap, + Map rosterMap + ) { + return secondments.stream() + .map(item -> { + FtbPersonnelsSecondRecordVO vo = FtbPersonnelsSecondRecordVO.covert(item); + vo.setCreatorUserName(userNameMap.getOrDefault(item.getCreatorUserId(), "-")); + // 如果有 rosterMap,可以这样取 + FtbPersonnelsStaffRoster roster = rosterMap.getOrDefault(item.getUserId(), null); + vo.setEmployeeId(roster != null ? roster.getSystemWokerId() : "-"); + vo.setUserName(roster != null ? roster.getName() : "-"); + // 如果没有 rosterMap,保持原逻辑用 "-" 占位 + vo.setEmployeeId("-"); + vo.setUserName("-"); + + return vo; + }) + .collect(Collectors.toList()); + } + + private List convertToMoveLogList( + List growthLogs, + List transferManages, + Map userNameMap) { + + List result = new ArrayList<>(); + + // 第一部分:来自旧的 growth_log(晋升 + 调岗) + growthLogs.stream() + .filter(log -> Objects.equals(log.getGrowthType(), GrowthLogEnum.promotion.getCode()) || + Objects.equals(log.getGrowthType(), GrowthLogEnum.TRANSFER_POSITION.getCode())) + .forEach(log -> { + FtbPersonnelsMoveLogVO vo = new FtbPersonnelsMoveLogVO(); + + if (Objects.equals(log.getGrowthType(), GrowthLogEnum.promotion.getCode())) { + StaffPromotionDto dto = JSONUtil.toBean(log.getDetail(), StaffPromotionDto.class); + vo = FtbPersonnelsMoveLogVO.covert(dto); // 假设有这个方法 + } else if (Objects.equals(log.getGrowthType(), GrowthLogEnum.TRANSFER_POSITION.getCode())) { + TransferGrowthLogDto dto = JSONUtil.toBean(log.getDetail(), TransferGrowthLogDto.class); + vo = FtbPersonnelsMoveLogVO.covertTransfer(dto); // 假设有这个方法 + } + + vo.setName(userNameMap.getOrDefault(log.getUserId(), "-")); + vo.setOperator(userNameMap.getOrDefault(log.getCreatorUserId(), "-")); + vo.setProcessingDate(log.getCreatorTime()); + vo.setEmployeeID(log.getEmployeeId()); + + // 可选:设置 type + vo.setType(Objects.equals(log.getGrowthType(), GrowthLogEnum.promotion.getCode()) ? "晋升" : "调岗"); + + result.add(vo); + }); + + // 第二部分:来自新的 transfer_manage(状态为已实施或已通过) + transferManages.forEach(item -> { + FtbPersonnelsMoveLogVO vo = FtbPersonnelsMoveLogVO.covertPromotion(item); // 假设有这个方法 + + vo.setName(userNameMap.getOrDefault(item.getUserId(), "-")); + + // 类型判断(新字段 state) + Integer state = item.getState(); + String type = "未知异动"; + if (state != null) { + switch (state) { + case 0: + type = "调岗"; + break; + case 1: + type = "晋升"; + break; + case 2: + type = "降职"; + break; + case 3: + type = "调店"; + break; + } + } + vo.setType(type); + + // 操作人逻辑(已实施用 creator,已通过用 lastModify) + String operatorId = (item.getProcessingStatus() == 6 && item.getCreatorUserId() != null) + ? item.getCreatorUserId() + : (item.getProcessingStatus() == 2 && item.getLastModifyUserId() != null) + ? item.getLastModifyUserId() + : null; + + vo.setOperator(userNameMap.getOrDefault(operatorId, "-")); + + vo.setProcessingDate(item.getCreatorTime()); // 或用其他合适的时间字段 + vo.setEmployeeID("-"); // 如果有 employeeId 字段可补充 + + result.add(vo); + }); + + // 可选:按时间排序(如果前端需要按时间倒序) + result.sort(Comparator.comparing( + FtbPersonnelsMoveLogVO::getProcessingDate, + Comparator.nullsLast(Comparator.reverseOrder()) + )); + + return result; + } + + private void operatorHandling(List rewardList) { + if (rewardList.isEmpty()) { + return; + } + List userIds = rewardList.stream().map(FtbHomePageRewardVO::getOperator).collect(Collectors.toList()); + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(userIds, null); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return; + } + Map> map = userPrimaryBoundBatch.getData().stream().collect(Collectors.groupingBy(UserBoundVO::getId)); + for (FtbHomePageRewardVO ftbHomePageRewardVO : rewardList) { + List orDefault = map.getOrDefault(ftbHomePageRewardVO.getOperator(), null); + if (CollUtil.isEmpty(orDefault)) { + ftbHomePageRewardVO.setOperator("--"); + continue; + } + ftbHomePageRewardVO.setOperator(orDefault.get(0).getUserName()); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRegistrationFormDataServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRegistrationFormDataServiceImpl.java new file mode 100644 index 0000000..6a94798 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRegistrationFormDataServiceImpl.java @@ -0,0 +1,2389 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationResponse; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.account.PTenantAccountApi; +import jnpf.attendance.AttendanceDailyRuleApi; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.StoreEntity; +import jnpf.enums.personnel.PersonnelFormDataSystemRosterFields; +import jnpf.enums.personnel.PhoneStatusEnum; +import jnpf.exception.LoginException; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.dto.contractinfo.ContactStatusInfo; +import jnpf.model.personnels.dto.emp.FtbEmpAddNewDTO; +import jnpf.model.personnels.dto.staff.field.*; +import jnpf.model.personnels.dto.staff.registerform.CheckRegisterFormFillDto; +import jnpf.model.personnels.dto.staff.registerform.GroupFieldDataDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.registerform.WorkAddressDto; +import jnpf.model.personnels.dto.staff.roster.SaffRoleDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.req.registerform.SaveFormDataReq; +import jnpf.model.personnels.req.roster.UserAccountDto; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.SaveUserManagerAddDTO; +import jnpf.permission.dto.v2.UserBoundInfoDTO; +import jnpf.permission.dto.v2.user.SaveUserDTO; +import jnpf.permission.dto.v2.user.UpdateUserDTO; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.UserCopyEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.mapper.FtbPersonnelsUchisuikePondMapper; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.*; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.AuthUtil; +import jnpf.util.FtbUtil; +import jnpf.util.RegexUtils; +import jnpf.util.UserProvider; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsStaffRegistrationFormDataServiceImpl extends ServiceImpl implements FtbPersonnelsStaffRegistrationFormDataService { + + @Autowired + private FtbPersonnelsRegistrationFormTypeService registrationFormTypeService; + + @Autowired + private FtbPersonnelsRegistrationFormFieldService registrationFormFieldService; + + @Autowired + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Autowired + private FtbPersonnelsEmEntryService ftbPersonnelsEmEntryService; + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionService fieldOptionService; + + private final static String AFFILIATED_GROUP_NAME = "affiliated"; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private FtbPersonnelsAuditMasterConfigService auditMasterConfigService; + + @Resource + private AttendanceGroupService attendanceGroupService; + + @Autowired + private PersonnelAsyncServiceUtils personnelAsyncServiceUtils; + + @Autowired + FtbPersonnelsStaffEmploymentApplyMapper applyMapper; + + @Autowired + private AttendanceDailyRuleApi attendanceDailyRuleApi; + + @Autowired + private Executor threadPoolExecutor; + @Resource + private QuerySalaryApi querySalaryApi; + @Resource + private FtbPersonnelsUchisuikePondMapper ftbPersonnelsUchisuikePondMapper; + + @Autowired + PermissionsUtils permissionsUtils; + + @Autowired + FtbPersonnlesIMUtils ftbPersonnlesIMUtils; + + @Autowired + ThreadPoolTaskExecutor taskExecutor; + + @Autowired + V2UserApi v2UserApi; + + @Resource + private AuthUtil authUtil; + + @Autowired + private PTenantAccountApi pTenantAccountApi; + + @Resource + private PersonnelIdCardVerificationUtils personnelIdCardVerificationUtils; + @Resource + private CertificateInstanceService certificateInstanceService; + + private static final String DEFAULT_USER_LOGO = "001.png"; + + public static String getAffiliatedGroupName() { + return AFFILIATED_GROUP_NAME; + } + + private List fillFieldOption(List fieldList) { + + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0) + .notIn(FtbPersonnelsRegistrationFormFieldOption::getId, "301") + .orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getId); + List optionList = fieldOptionService.list(wrapper); + if (CollectionUtil.isEmpty(optionList)) { + return new ArrayList<>(); + } + List optionDtoList = BeanUtil.copyToList(optionList, FtbPersonnelsRegistrationFormFieldOptionDto.class); + //按照formFieldid 分组 + Map> fieldMap = optionDtoList.stream().collect(Collectors.groupingBy(FtbPersonnelsRegistrationFormFieldOptionDto::getFormFieldId)); + + for (FormFieldDto formFieldDto : fieldList) { + List dtoList = fieldMap.get(formFieldDto.getId()); + formFieldDto.setOptionDtoList(dtoList); + } + return optionList; + } + + + private List convertGroupFieldDataDto(List formFieldDtoList, String affiliatedGroupData, String userId) { + List retList = new ArrayList<>(); + List groupList = new ArrayList<>(); + for (FormFieldDto formFieldDto : formFieldDtoList) { + if (StringUtils.isNotEmpty(formFieldDto.getGroupName())) { + groupList.add(formFieldDto); + } else { + if (CollectionUtil.isNotEmpty(groupList)) { + GroupFieldDataDto dto = new GroupFieldDataDto(); + dto.setIsMulitField(true); + dto.setGroupFieldDtoList(personnelOrgUtils.buildMulitField(groupList, affiliatedGroupData, userId)); + retList.add(dto); + dto.setGroupName(groupList.get(0).getGroupName()); + groupList = new ArrayList<>(); + } + GroupFieldDataDto dto = new GroupFieldDataDto(); + if ("phone".equals(formFieldDto.getId()) || "workAge".equals(formFieldDto.getId()) || "age".equals(formFieldDto.getId())) { + formFieldDto.setCurrIsUpdate(false); + } + dto.setFormFieldDto(formFieldDto); + retList.add(dto); + } + } + return retList; + } + + private List convertGroupFieldDataDto(List formFieldDtoList, String affiliatedGroupData, String userId, String tenantId) { + List retList = new ArrayList<>(); + List groupList = new ArrayList<>(); + for (FormFieldDto formFieldDto : formFieldDtoList) { + if (StringUtils.isNotEmpty(formFieldDto.getGroupName())) { + groupList.add(formFieldDto); + } else { + if (CollectionUtil.isNotEmpty(groupList)) { + GroupFieldDataDto dto = new GroupFieldDataDto(); + dto.setIsMulitField(true); + dto.setGroupFieldDtoList(personnelOrgUtils.buildMulitField(groupList, affiliatedGroupData, userId, tenantId)); + retList.add(dto); + dto.setGroupName(groupList.get(0).getGroupName()); + groupList = new ArrayList<>(); + } + GroupFieldDataDto dto = new GroupFieldDataDto(); + dto.setFormFieldDto(formFieldDto); + retList.add(dto); + + + } + } + return retList; + } + + + /** + * 查询字段类型 + * + * @return + */ + private List queryFormTypeList() { + return registrationFormTypeService.list(new QueryWrapper() + .lambda() + .select(FtbPersonnelsRegistrationFormType::getId, FtbPersonnelsRegistrationFormType::getName, FtbPersonnelsRegistrationFormType::getSystemType, + FtbPersonnelsRegistrationFormType::getSorts, FtbPersonnelsRegistrationFormType::getStatus, FtbPersonnelsRegistrationFormType::getEnabledMark) + .eq(FtbPersonnelsRegistrationFormType::getEnabledMark, 0) //1无效 0有效 + .eq(FtbPersonnelsRegistrationFormType::getStatus, 0) //0、启用 1、禁用 + .orderByAsc(FtbPersonnelsRegistrationFormType::getSorts)); + } + + /** + * 查询字段 + * + * @param registerFormField true 表示只查询入职登记表中字段,false 查询所有字段 + * @return + */ + private List queryFormFieldList(Boolean registerFormField) { + return registrationFormFieldService.list(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0) //1无效 0有效 + .eq(FtbPersonnelsRegistrationFormField::getStatus, 0) //0、启用 1、禁用 + .eq(registerFormField, FtbPersonnelsRegistrationFormField::getIsRegisterFormShow, 0) + .orderByAsc(FtbPersonnelsRegistrationFormField::getSorts)); + } + + @Override + public List queryFormFieldValueList(String phone, String rosterId) { + return baseMapper.selectList(new QueryWrapper() + .lambda() + .eq(StringUtils.isNotEmpty(rosterId), FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(StringUtils.isNotEmpty(phone), FtbPersonnelsStaffRegistrationFormData::getPhone, phone)); + } + + @Override + public Map> queryFormFieldValueListWithPhone(List phones) { + List formData = baseMapper.selectList(new QueryWrapper() + .lambda() + .in(FtbPersonnelsStaffRegistrationFormData::getPhone, phones)); + Map> listMap = formData.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getPhone)); + + return listMap; + } + + @Override + public List queryFormFieldValueForRosterIds(List rosterIds) { + if (CollectionUtil.isEmpty(rosterIds)) { + return new ArrayList<>(); + } + return baseMapper.selectList(new QueryWrapper() + .lambda() + .select(FtbPersonnelsStaffRegistrationFormData::getId, FtbPersonnelsStaffRegistrationFormData::getRosterId, + FtbPersonnelsStaffRegistrationFormData::getPhone, FtbPersonnelsStaffRegistrationFormData::getValue, + FtbPersonnelsStaffRegistrationFormData::getFormTypeId, FtbPersonnelsStaffRegistrationFormData::getFormFieldId) + .in(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterIds)); + } + + @Override + public List queryListForRosterIdAndField(List rosterIds, List formFieldIds) { + if (CollectionUtil.isEmpty(rosterIds)) { + return new ArrayList<>(); + } + return baseMapper.selectList(new QueryWrapper() + .lambda() + .in(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, formFieldIds) + .in(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterIds)); + } + + @Override + public void updateForOneField(String phone, String formFieldId, String value) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbPersonnelsStaffRegistrationFormData::getValue, value) + .eq(FtbPersonnelsStaffRegistrationFormData::getPhone, phone) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, formFieldId); + baseMapper.update(null, wrapper); + } + + @Override + public void updateForOneField(String rosterId, String phone, String formFieldId, String value) { + List oldData = baseMapper.selectList(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, formFieldId)); + if (CollectionUtil.isNotEmpty(oldData)) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda().set(FtbPersonnelsStaffRegistrationFormData::getValue, value) + .eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, formFieldId); + baseMapper.update(new FtbPersonnelsStaffRegistrationFormData(), wrapper); + } else { + FtbPersonnelsRegistrationFormField field = registrationFormFieldService.getById(formFieldId); + if (null == field) { + return; + } + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setRosterId(rosterId); + formData.setPhone(phone); + formData.setFormTypeId(field.getFormTypeId()); + formData.setFormFieldId(formFieldId); + formData.setValue(value); + baseMapper.insert(formData); + } + } + + public String queryOneFieldValue(String phone, String rosterId, String fieldName) { + List formDataList = baseMapper.selectList(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(FtbPersonnelsStaffRegistrationFormData::getPhone, phone) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, fieldName)); + if (CollectionUtil.isNotEmpty(formDataList)) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = formDataList.get(0); + if (StringUtils.isNotEmpty(ftbPersonnelsStaffRegistrationFormData.getValue())) { + return ftbPersonnelsStaffRegistrationFormData.getValue(); + } + } + return ""; + } + + public FtbPersonnelsStaffRegistrationFormData queryOneFieldForFieldId(String rosterId, String fieldName) { + List formDataList = baseMapper.selectList(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, fieldName)); + if (CollectionUtil.isNotEmpty(formDataList)) { + return formDataList.get(0); + + } + return null; + } + + public String queryOneFieldValueForPhone(String phone, String fieldName) { + List formDataList = baseMapper.selectList(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsStaffRegistrationFormData::getPhone, phone) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, fieldName)); + if (CollectionUtil.isNotEmpty(formDataList)) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = formDataList.get(0); + if (StringUtils.isNotEmpty(ftbPersonnelsStaffRegistrationFormData.getValue())) { + return ftbPersonnelsStaffRegistrationFormData.getValue(); + } + } + return ""; + } + + + public FtbPersonnelsStaffRegistrationFormData queryOneField(String phone, String rosterId, String fieldName) { + List formDataList = baseMapper.selectList(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(FtbPersonnelsStaffRegistrationFormData::getPhone, phone) + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, fieldName)); + if (CollectionUtil.isNotEmpty(formDataList)) { + return formDataList.get(0); + + } + return null; + } + + @Override + public String queryTenantIdForCode(String code) { + return personnelOrgUtils.queryTenantIdForCode(code); + } + + /** + * 根据手机号查询所有已经填写的字段 + * + * @param phone + * @return + */ + @Override + public Map queryAllFormData(String phone, String rosterId) { + List formFieldValueList = queryFormFieldValueList(phone, rosterId); + if (CollectionUtil.isEmpty(formFieldValueList)) { + return new HashMap<>(); + } + return personnelOrgUtils.convertFormValueMap(formFieldValueList); + } + + @Override + public Map> querAllFormDataForRosterIds(List rosterIds) { + Map> resultMap = new HashMap<>(); + if (CollUtil.isEmpty(rosterIds)) { + return resultMap; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterIds); + wrapper.and(a -> + a.eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, "probationPeriod") + .or() + .eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, "planProbationaryDate") + ); + List formData = baseMapper.selectList(wrapper); + + if (CollUtil.isNotEmpty(formData)) { + Map> listMap = formData.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId, Collectors.toList())); + Set keySet = listMap.keySet(); + keySet.forEach(str -> { + List data = listMap.get(str); + Map map = data.stream().collect(Collectors.toMap(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, FtbPersonnelsStaffRegistrationFormData::getValue)); + resultMap.put(str, map); + }); + } + return resultMap; + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class,lockRetryInternal = 5000 ,lockRetryTimes = 30) + public void saveRegistrationForm(String tenantId, List inputFieldValueList, Integer flag, String registerImg, String moduleId) { + if (CollectionUtil.isEmpty(inputFieldValueList)) { + throw new RuntimeException("提交数据为空"); + } + //获取手机号 + String phone = personnelOrgUtils.getPhoneFormInput(inputFieldValueList); + QueryWrapper wrappe1r = new QueryWrapper<>(); + wrappe1r.lambda() + .eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0).last("limit 1") + ; + FtbPersonnelsStaffEmploymentApply employmentApplyEntity = applyMapper.selectOne(wrappe1r); + if (null != employmentApplyEntity) { + if (employmentApplyEntity.getIsNeedCheck() == PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) {//0、需要审批 1、不需要审批 + throw new RuntimeException("手机号已经在入职列表中!"); + } else { + if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode() && employmentApplyEntity.getStatus() != PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) { + throw new RuntimeException("手机号已经在入职列表中!"); + }else if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_WAIT.getCode()) { + throw new RuntimeException("该手机号等待入职审批中!"); + }else if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_ONBOARD_WAIT.getCode()) { + throw new RuntimeException("该手机号等待入职审批中!"); + } else if (employmentApplyEntity.getCheckStatus() == PhoneStatusEnum.BASE_APPROVAL_REJECT.getCode()) { + throw new RuntimeException("该手机号已办理入职,但审批流程未通过,请前往待审批列表,进行重新办理!"); + } + } + } + if (StringUtils.isEmpty(phone)) { + throw new RuntimeException("手机号不能为空"); + } + if (!personnelOrgUtils.isValidPhoneNumber(phone)) { + throw new RuntimeException("手机号格式不正确"); + } + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByPhone(phone); + String rosterId = ""; + if (null != roster) { + rosterId = roster.getId(); + } + List formFeildList = queryFormFieldList(true); + if (CollectionUtil.isEmpty(formFeildList)) { + return; + } + // 获取姓名 + String workerUserName = inputFieldValueList.stream() + .filter(item -> "workerName".equals(item.getId())).findFirst() + .map(SubFormFieldDto::getUserValue) + .orElse(""); + // 校验身份证号 + inputFieldValueList.stream().filter(item -> "idCardNum".equals(item.getId())&& !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(phone, "idCardNum", a.getUserValue()) > 0) { + throw new RuntimeException("身份证号已存在"); + } + if (!RegexUtils.checkIdCard(a.getUserValue())) { + throw new RuntimeException("请输入正确的身份证号码"); + } + DateTime ageByIdCard = IdcardUtil.getBirthDate(a.getUserValue()); + if (ageByIdCard.isAfter(new Date())) { + throw new RuntimeException("请输入正确的身份证号码"); + } + // 姓名+身份证号校验 + IdCardVerificationResponse idCardVerificationResponse = personnelIdCardVerificationUtils.idCardVerification(a.getUserValue(), + workerUserName, tenantId); + personnelIdCardVerificationUtils.checkIdCardVerification(idCardVerificationResponse); + }); + // 校验银行卡号 + inputFieldValueList.stream().filter(item -> "bankCardNo".equals(item.getId())&& !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(phone, "bankCardNo", a.getUserValue()) > 0) { + throw new RuntimeException("银行卡号已存在"); + } + }); + // 校验公积金号 + inputFieldValueList.stream().filter(item -> "providentFundAccount".equals(item.getId())&& !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(phone, "providentFundAccount", a.getUserValue()) > 0) { + throw new RuntimeException("公积金号已存在"); + } + }); + // 校验社保账号 + inputFieldValueList.stream().filter(item -> "socialAccount".equals(item.getId())&& !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(phone, "socialAccount", a.getUserValue()) > 0) { + throw new RuntimeException("社保账号已存在"); + } + }); + //查询所有字段 + Map formFieldMap = formFeildList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormField::getId, Function.identity())); + + //写入formData数据 + List formFieldValueList = queryFormFieldValueList(phone, rosterId); + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formFieldValueList); + //组装数据 + List formDataList = new ArrayList<>(); + FtbPersonnelsStaffRoster updateRosterEntity = new FtbPersonnelsStaffRoster(); + //需要同步的数据 + + String flowerName = ""; + String workerName = ""; + String workerSex = ""; + String workerStatus = ""; + String actualProbationaryDate = ""; + String birthday = ""; + String idCardNum = ""; + String headLogo = ""; + String currGroupId = ""; + String healthCertificate = ""; + String startHealthDate = ""; + String endHealthDate = ""; + Date now = new Date(); + String actualStartDate = personnelOrgUtils.getPhoneFormInputForField(inputFieldValueList, "actualStartDate"); + Boolean canUpdateRealName = staffRosterService.canUpdateRealName(); + for (SubFormFieldDto subFormFieldDto : inputFieldValueList) { + if (StringUtils.isEmpty(subFormFieldDto.getId())) { + throw new RuntimeException("字段id不能为空"); + } + if ("workerName".equals(subFormFieldDto.getId()) && canUpdateRealName == false) { + if (null != roster) { + subFormFieldDto.setUserValue(roster.getName()); + } + } + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormField = formFieldMap.get(subFormFieldDto.getId()); + if (!AFFILIATED_GROUP_NAME.equals(subFormFieldDto.getId())) { + if (null == ftbPersonnelsRegistrationFormField) { + throw new RuntimeException("登记表配置已更新,请退出页面后重新填写!"); + //throw new RuntimeException("[" + subFormFieldDto.getId() + "] 字段不存在"); + } + if (ftbPersonnelsRegistrationFormField.getIsNeedFill() == 1 && ( + StringUtils.isEmpty(subFormFieldDto.getUserValue()) + || "[]".equals(subFormFieldDto.getUserValue()) + )) { + throw new RuntimeException(ftbPersonnelsRegistrationFormField.getName() + "必填"); + } + } + if ("flowerName".equals(subFormFieldDto.getId())) { + flowerName = subFormFieldDto.getUserValue(); + } else if ("workerName".equals(subFormFieldDto.getId())) { + workerName = subFormFieldDto.getUserValue(); + } else if ("workerSex".equals(subFormFieldDto.getId())) { + workerSex = subFormFieldDto.getUserValue(); + } else if ("workerStatus".equals(subFormFieldDto.getId())) { + workerStatus = subFormFieldDto.getUserValue(); + } else if ("actualProbationaryDate".equals(subFormFieldDto.getId())) { + actualProbationaryDate = subFormFieldDto.getUserValue(); + } else if ("idCardNum".equals(subFormFieldDto.getId())) { + idCardNum = subFormFieldDto.getUserValue(); + } else if ("headLogo".equals(subFormFieldDto.getId())) { + headLogo = subFormFieldDto.getUserValue(); + if (StringUtils.isNotEmpty(headLogo) && !headLogo.startsWith("[")) { + throw new RuntimeException("提交的头像数据格式不正确"); + } + } else if ("birthday".equals(subFormFieldDto.getId())) { + checkBirthdayFormat(subFormFieldDto.getUserValue()); + birthday = subFormFieldDto.getUserValue(); + } else if ("companyAge".equals(subFormFieldDto.getId())) { + if (StringUtils.isNotEmpty(actualStartDate) + && null != roster + && (!roster.getWorkerStatus().equals("304") || !roster.getWorkerStatus().equals("305"))) { + long betweenDay = 0L; + DateTime parse = DateUtil.parse(actualStartDate, "yyyy-MM-dd"); + if (now.after(parse)) { + betweenDay = DateUtil.betweenDay(parse, now, false); + } + subFormFieldDto.setUserValue(String.valueOf(betweenDay)); + } + } else if ("healthCertificate".equals(subFormFieldDto.getId())) { + healthCertificate = subFormFieldDto.getUserValue(); + } else if ("startHealthDate".equals(subFormFieldDto.getId())) { + startHealthDate = subFormFieldDto.getUserValue(); + } else if ("endHealthDate".equals(subFormFieldDto.getId())) { + endHealthDate = subFormFieldDto.getUserValue(); + } + personnelOrgUtils.fillRoster(updateRosterEntity, subFormFieldDto); + FtbPersonnelsStaffRegistrationFormData oldFormData = formFieldValueMap.get(subFormFieldDto.getId()); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + if (null != oldFormData) { + formData.setId(oldFormData.getId()); + } + if (StringUtils.isNotEmpty(rosterId)) { + formData.setRosterId(rosterId); + } else { + formData.setRosterId(""); + } + formData.setPhone(phone); + formData.setFormFieldId(subFormFieldDto.getId()); + formData.setFormTypeId(subFormFieldDto.getFormTypeId()); + formData.setValue(subFormFieldDto.getUserValue()); + + formDataList.add(formData); + } + // 身份证号码识别出生日期 + if (StringUtils.isNotEmpty(idCardNum) && !"null".equals(idCardNum) && StringUtils.isEmpty(birthday)) { + Date birthdayDate = IdcardUtil.getBirthDate(idCardNum).toJdkDate(); + FtbPersonnelsRegistrationFormField registrationFormField = formFieldMap.get("birthday"); + FtbPersonnelsRegistrationFormField registrationFormFieldAge = formFieldMap.get("age"); + if (Objects.nonNull(registrationFormField) && Objects.nonNull(registrationFormFieldAge)) { + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setRosterId(rosterId); + formData.setPhone(phone); + formData.setFormTypeId(registrationFormField.getFormTypeId()); + formData.setFormFieldId(registrationFormField.getId()); + formData.setValue(DateUtil.format(birthdayDate, "yyyy-MM-dd")); + int age = personnelOrgUtils.calculateAge(personnelOrgUtils.dateToLocalDate(birthdayDate)); + FtbPersonnelsStaffRegistrationFormData formDataAge = new FtbPersonnelsStaffRegistrationFormData(); + formDataAge.setRosterId(rosterId); + formDataAge.setPhone(phone); + formDataAge.setFormTypeId(registrationFormFieldAge.getFormTypeId()); + formDataAge.setFormFieldId(registrationFormFieldAge.getId()); + formDataAge.setValue(String.valueOf(age)); + formDataList.add(formDataAge); + formDataList.add(formData); + } + } + // 校验前后是否有空格 + Pattern spacesCheck = Pattern.compile(FtbRosterImportConstants.NAME_REGEX); + Matcher spacesCheckMatcher = spacesCheck.matcher(workerName); + if (!spacesCheckMatcher.matches()){ + throw new RuntimeException("请使用标准格式的中文名或英文名!"); + } + //写入数据库 + if (CollectionUtil.isNotEmpty(formDataList)) { + updateData(formDataList); + } + if(employmentApplyEntity == null) { + String userId = onboardingData(tenantId, flag, registerImg, phone, workerName,moduleId); + // 健康证同步 + certificateInstanceService.saveHealthCertificate(userId, healthCertificate, startHealthDate, endHealthDate); + }else { + // 更新身份证号 + LambdaUpdateWrapper lambdaUpdateWrapper = Wrappers.lambdaUpdate(); + lambdaUpdateWrapper.set(FtbPersonnelsStaffEmploymentApply::getIdCardNum, idCardNum); + lambdaUpdateWrapper.eq(FtbPersonnelsStaffEmploymentApply::getId, employmentApplyEntity.getId()); + applyMapper.update(new FtbPersonnelsStaffEmploymentApply(),lambdaUpdateWrapper); + // 未登记数据进行更新 + String userId = ""; + if (employmentApplyEntity.getIsSubmitForm() != 1) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + if (StringUtils.isNotEmpty(workerName)) { + wrapper.set(FtbPersonnelsStaffEmploymentApply::getWorkerName, workerName); + userId = employmentApplyEntity.getUserId(); + // 老数据兼容 + if (userId == null){ + String userHeadLog = generaDefaultUserHeadLog(employmentApplyEntity.getWorkerName()); + userId = saveUserInfo(employmentApplyEntity, userHeadLog); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + }else { + syncWorkName(workerName, tenantId, userId); + } + } + wrapper.set(FtbPersonnelsStaffEmploymentApply::getIsSubmitForm, 1); + wrapper.set(FtbPersonnelsStaffEmploymentApply::getRegisterImg, registerImg); + wrapper.eq(FtbPersonnelsStaffEmploymentApply::getId, employmentApplyEntity.getId()); + applyMapper.update(new FtbPersonnelsStaffEmploymentApply(), wrapper); + extracted(workerName, tenantId, employmentApplyEntity.getUserId(),employmentApplyEntity.getCurrOrg(),moduleId); + } + // 健康证同步 + certificateInstanceService.saveHealthCertificate(userId, healthCertificate, startHealthDate, endHealthDate); + } + // 内推池状态变更 + personnelOrgUtils.sysWorkerStatusToUchisuike(phone, updateRosterEntity.getWorkerStatus()); + if (null != roster) { + updateRosterEntity.setId(roster.getId()); + updateRosterEntity.setIsSubmitForm(1); + staffRosterService.updateById(updateRosterEntity); + String inputHeadLogo = getInputHeadLogo(headLogo); + if (StringUtils.isNotEmpty(inputHeadLogo)) { + personnelOrgUtils.sysUserHeadLog(roster.getUserId(), inputHeadLogo); + } + //同步姓名 花名册等信息到组织架构 + Boolean isUpdateTenant = false; + SaveUserManagerAddDTO dto = new SaveUserManagerAddDTO(); + dto.setUserId(roster.getUserId()); + if (StringUtils.isNotEmpty(workerName)) { + dto.setRealName(workerName); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(workerSex)) { + dto.setGender(Integer.valueOf(workerSex)); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(inputHeadLogo)) { + dto.setHeadIcon(inputHeadLogo); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(flowerName)) { + dto.setNickName(flowerName); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(actualProbationaryDate)) { + dto.setBecomeDate(DateUtil.parse(actualProbationaryDate, "yyyy-MM-dd")); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(actualStartDate)) { + dto.setEntryDate(DateUtil.parse(actualStartDate, "yyyy-MM-dd")); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(birthday)) { + dto.setBirthday(DateUtil.parse(birthday, "yyyy-MM-dd")); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(idCardNum)) { + dto.setCertificatesNumber(idCardNum); + dto.setCertificatesType("a745d425adbb4321880817661cae8910"); + isUpdateTenant = true; + } + if (isUpdateTenant) { + personnelOrgUtils.sysTenantAccountBaseNoLogin(dto, tenantId); + } + } + } + + @SneakyThrows + private String generaDefaultUserHeadLog(String name) { + String headLogo = DEFAULT_USER_LOGO; + ActionResult headResult = pTenantAccountApi.generateDefaultAvatar(name); + if (null != headResult) { + headLogo = (String) headResult.getData(); + } + return headLogo; + } + public String saveUserInfo(FtbPersonnelsStaffEmploymentApply apply, String userHeadLog,String... tenantIdS){ + // 生成账号建立关系信息 + SaveUserDTO saveUserDTO = new SaveUserDTO(); + saveUserDTO.setAccount(apply.getPhone()); + saveUserDTO.setRealName(apply.getWorkerName()); + saveUserDTO.setMobilePhone(apply.getPhone()); + saveUserDTO.setWorkerStatus(UserWorkStatusEnums.PRE_ONBOARDING.getCode()); + saveUserDTO.setAccount(apply.getPhone()); + saveUserDTO.setHeadIcon(userHeadLog); + saveUserDTO.setEntryDate(apply.getExpectedStartDate()); + UserBoundInfoDTO userBoundInfoDTO = new UserBoundInfoDTO(); + userBoundInfoDTO.setOrganizeId(apply.getCurrOrg()); + userBoundInfoDTO.setPositionId(apply.getCurrPosition()); + userBoundInfoDTO.setGradesId(apply.getCurrRank()); + userBoundInfoDTO.setStoreTeamId(apply.getCurrGroupId()); + saveUserDTO.setBoundInfoDTO(userBoundInfoDTO); + String tenantId = tenantIdS.length > 0 ? tenantIdS[0] : UserProvider.getUser().getTenantId(); + saveUserDTO.setTenantId(tenantId); + //解决组织架构分布式事务 行锁问题 不多次调用 组织架构修改用户接口一次完成 提前生成ID 租户->用户 + //生成主账号 + String userId = FtbUtil.getId(); + //判断是否离职 + UserCopyEntity copyInfo = v2UserApi.getUserCopyInfo(saveUserDTO.getMobilePhone()); + if (copyInfo != null && StrUtil.isNotBlank(copyInfo.getId())){ + userId = copyInfo.getId(); + } + UserAccountDto userAccountDto = new UserAccountDto(); + userAccountDto.setFid(userId); + userAccountDto.setUserName(apply.getWorkerName()); + userAccountDto.setTenantId(tenantId); + userAccountDto.setUserMobilePhone(apply.getPhone()); + userAccountDto.setHeadImageUrl(userHeadLog); + List addUserList = List.of(userAccountDto); + ActionResult> batchAddUserAccount = pTenantAccountApi.batchAddUserAccount(addUserList); + if(batchAddUserAccount == null || !batchAddUserAccount.getCode().equals(200)){ + pTenantAccountApi.deleteUser(userId); + throw new RuntimeException("主账号生成失败"); + } + UserAccountDto accountDto = batchAddUserAccount.getData().stream().findFirst().orElse(new UserAccountDto()); + saveUserDTO.setId(userId); + saveUserDTO.setAccount(accountDto.getAccount()); + ActionResult actionResult = v2UserApi.saveUserInfo(saveUserDTO); + if (actionResult ==null || actionResult.getCode() != 200) { + pTenantAccountApi.deleteUser(userId); + throw new RuntimeException(actionResult !=null ? actionResult.getMsg() : "保存用户信息失败!"); + } + return userId; + } + public void extracted(String workerName , String tenantId, String userId,String currOrg,String moduleId) { + taskExecutor.execute(()->{ + List stringList = permissionsUtils.getUserPersonnelOrganizationIdDataPermissions(userId, + List.of(PermissionsEnums.PERSONNEL_MANAGEMENT_Button.getValue().split(",")), + "", + List.of(PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), + PermissionsEnums.PERSONNEL_MANAGEMENT_APP.getValue()), + tenantId, currOrg, moduleId); + ftbPersonnlesIMUtils.sendMsgWithList(tenantId,stringList, workerName,moduleId); + }); + } + + + // @Async + public String onboardingData(String tenantId, Integer flag, String registerImg, String phone, String workerName, String moduleId) { + // 根据入职填写新增 入职办理列表数据 + // 是否内推 + LambdaQueryWrapper uchisuikePondLambdaQueryWrapper = Wrappers.lambdaQuery(); + uchisuikePondLambdaQueryWrapper.eq(FtbPersonnelsUchisuikePond::getPhone, phone); + uchisuikePondLambdaQueryWrapper.eq(FtbPersonnelsUchisuikePond::getEnableMark, 0); + uchisuikePondLambdaQueryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply employmentApply = new FtbPersonnelsStaffEmploymentApply(); + FtbPersonnelsUchisuikePond uchisuikePond = ftbPersonnelsUchisuikePondMapper.selectOne(uchisuikePondLambdaQueryWrapper); + if (uchisuikePond != null) { + employmentApply.setSource(Math.toIntExact(uchisuikePond.getSource())); + employmentApply.setCurrOrg(uchisuikePond.getOrgId()); + employmentApply.setCurrPosition(uchisuikePond.getPostId()); + employmentApply.setCurrRank(uchisuikePond.getPostRankId()); + } else { + if (flag != null && flag == 4) { + employmentApply.setSource(0); + } else { + //同步入职登记表已经提交的员工信息 + employmentApply.setSource(1); + } + } + if (StringUtils.isNotEmpty(registerImg)) { + employmentApply.setRegisterImg(registerImg); + } + employmentApply.setWorkerName(workerName); + employmentApply.setPhone(phone); + employmentApply.setVersionNum(1); + employmentApply.setStatus(0); + employmentApply.setIsSubmitForm(1); + employmentApply.setEnabledMark(0); + FtbEmpAddNewDTO covert = FtbPersonnelsStaffEmploymentApply.covert(employmentApply); + return ftbPersonnelsEmEntryService.promoteOnboardingManagement(covert, tenantId,moduleId); + } + + private String getInputHeadLogo(String headLogo) { + if (StringUtils.isEmpty(headLogo) || "[]".equals(headLogo)) { + return ""; + } else { + List headList = JSONUtil.toList(headLogo, String.class); + if (CollectionUtil.isNotEmpty(headList)) { + headLogo = headList.get(0); + } else { + headLogo = ""; + } + } + return headLogo; + } + + /** + * 同步数据 + * @param dataMap + * @param phone + * @param rosterId + */ + public void syncData(Map dataMap, String phone, String rosterId) { + //获取所有字段 + List formFeildList = queryFormFieldList(false); + if (CollectionUtil.isEmpty(formFeildList)) { + return; + } + //查询所有字段 + Map formFieldMap = formFeildList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormField::getId, Function.identity())); + //查询字段已经有的值 + List formFieldValueList = queryFormFieldValueList(phone, ""); + // 登记表字段主键ID : vo + Map oldDataMap = formFieldValueList.stream().collect(Collectors.toMap(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, Function.identity(), (a, b) -> a)); + //组装数据 + List formDataList = new ArrayList<>(); + formFieldMap.forEach((key,value)->{ + // 没有数据进行新增 + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + // 校验字段是否存在 + Object o = dataMap.get(key); + if (ObjectUtil.isNull(o)){ + return; + } + // 字段存在进行数据更新 + // 查看是否存在历史数据进行覆盖 + FtbPersonnelsStaffRegistrationFormData registrationFormDataOld = oldDataMap.get(key); + // 将id进行覆盖 + if (ObjectUtil.isNotEmpty(registrationFormDataOld)){ + formData.setId(registrationFormDataOld.getId()); + } + // 补充主键id + if (StringUtils.isNotEmpty(rosterId)) { + formData.setRosterId(rosterId); + } + FtbPersonnelsRegistrationFormField field = formFieldMap.get(key); + formData.setPhone(phone); + formData.setFormFieldId(field.getId()); + formData.setFormTypeId(field.getFormTypeId()); + formData.setValue(o.toString()); + formDataList.add(formData); + }); + + //写入数据库 + // 分批写入 + if (CollectionUtil.isNotEmpty(formDataList)) { + Predicate predicateUpdate = item -> StringUtils.isNotEmpty(item.getId()); + Predicate predicateSave = item -> StringUtils.isEmpty(item.getId()); + List staffRegistrationFormDataUpdate = getData(formDataList, predicateUpdate); + if (CollectionUtil.isNotEmpty(staffRegistrationFormDataUpdate))staffRegistrationFormDataUpdate.forEach(item-> baseMapper.updateById(item)); + List staffRegistrationFormDataSave = getData(formDataList, predicateSave); + if (CollectionUtil.isNotEmpty(staffRegistrationFormDataSave))staffRegistrationFormDataSave.forEach(item -> baseMapper.insert(item)); + + } + } + + @NotNull + private static List getData(List formDataList, Predicate predicate) { + return formDataList.stream().filter(predicate).collect(Collectors.toList()); + } + + @Override + @Transactional + public void importSyncData(Map dataMap, String phone, String rosterId, Map formFieldMap) { + //查询字段已经有的值(此处是新增,数据库必然没有值,暂时屏蔽),正常情况是没有问题,由于用户先办理入职,但是还没有确认到岗,这个时候用户又使用导入功能就会有问题 + List formFieldValueList = queryFormFieldValueList(phone, rosterId); + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formFieldValueList); + //组装数据 + List formDataList = new ArrayList<>(); + dataMap.forEach((k, v) -> { + if (null == v) { + return; + } + FtbPersonnelsRegistrationFormField field = formFieldMap.get(k); + if (null != field) { + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + FtbPersonnelsStaffRegistrationFormData fieldValue = formFieldValueMap.get(k); + if (null != fieldValue) { + formData.setId(fieldValue.getId()); + } + if (StringUtils.isNotEmpty(rosterId)) { + formData.setRosterId(rosterId); + } else { + formData.setRosterId(""); + } + formData.setPhone(phone); + formData.setFormFieldId(field.getId()); + formData.setFormTypeId(field.getFormTypeId()); + formData.setValue(v.toString()); + formDataList.add(formData); + } + }); + + //写入数据库 + if (CollectionUtil.isNotEmpty(formDataList)) { + saveBatch(formDataList); + } + } + + /** + * 查询员工入职登记表 + * + * @param phone 手机号 + * @return + */ + @Override + public List queryRegistrationForm(String phone, Boolean registerForm) { + String userId = "",rosterId = ""; + if (!registerForm) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRoster::getPhone, phone); + queryWrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark,0); + FtbPersonnelsStaffRoster serviceOne = staffRosterService.getOne(queryWrapper); + if (ObjectUtil.isNotEmpty(serviceOne)) { + userId = serviceOne.getUserId(); + rosterId = serviceOne.getId(); + } + } + return queryAndFillFormFieldValue(phone, userId, rosterId, registerForm); + } + + + /** + * 查询员工入职登记表 + * + * @return + */ + @Override + public List queryRegistrationForm() { + //查询字段类型 + List formTypeList = queryFormTypeList(); + if (CollectionUtil.isEmpty(formTypeList)) { + return new ArrayList<>(); + } + List formTypeDtoList = BeanUtil.copyToList(formTypeList, FormTypeDto.class); + //查询字段 + List formFeildList = queryFormFieldList(true); + if (CollectionUtil.isEmpty(formFeildList)) { + return formTypeDtoList; + } + + List fieldList = BeanUtil.copyToList(formFeildList, FormFieldDto.class); + fillFieldOption(fieldList); + //按照formTypeId 分组 + Map> fieldMap = fieldList.stream().collect(Collectors.groupingBy(FormFieldDto::getFormTypeId)); + for (FormTypeDto formTypeDto : formTypeDtoList) { + List formFieldDtoList = fieldMap.get(formTypeDto.getId()); + if (CollectionUtil.isNotEmpty(formFieldDtoList)) { + formTypeDto.setGroupFieldDataDtoList(convertGroupFieldDataDto(formFieldDtoList, "", "")); + } + } + return filterNullFieldType(formTypeDtoList, true); + } + + @Override + public CheckRegisterFormFillDto checkRegistrationFormFill(String phone) { + Boolean isFill = staffEmploymentApplyService.checkFillRegistrationForm(phone); + CheckRegisterFormFillDto dto = new CheckRegisterFormFillDto(); + dto.setIsFill(isFill); + return dto; + } + + /** + * 查询员工归档登记表 + * + * @param + * @return + */ + @Override + public List queryArchivalForm(String userId) { + //查询入职登记信息 + FtbPersonnelsStaffRoster rosterEntity = staffRosterService.queryRosterInfoByUserId(userId); + if (null == rosterEntity) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.in(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 0); + queryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if(Objects.nonNull(one)){ + throw new RuntimeException("未办理入职,暂无档案信息!"); + } + throw new RuntimeException("员工花名册信息不存在"); + } + String phone = rosterEntity.getPhone(); + return queryAndFillFormFieldValue(phone, rosterEntity.getUserId(), rosterEntity.getId(), false); + } + + /** + * 查询员工归档登记表 + * + * @param userId + * @return + */ + @Override + public List queryArchivalForm(String userId, String tenantId) { + //查询入职登记信息 + FtbPersonnelsStaffRoster rosterEntity = staffRosterService.queryRosterInfoByUserId(userId); + if (null == rosterEntity) { + throw new RuntimeException("员工花名册信息不存在"); + } + String phone = rosterEntity.getPhone(); + return queryAndFillFormFieldValue(phone, rosterEntity.getUserId(), rosterEntity.getId(), false, tenantId); + } + + /** + * @param phone + * @param registerForm true 表示只查询入职登记表中字段,false 查询所有字段 + * @return + */ + public List queryAndFillFormFieldValue(String phone, String userId, String rosterId, Boolean registerForm, String tenantId) { + //查询字段类型 + List formTypeList = queryFormTypeList(); + if (CollectionUtil.isEmpty(formTypeList)) { + return new ArrayList<>(); + } + List formTypeDtoList = BeanUtil.copyToList(formTypeList, FormTypeDto.class); + //查询字段 + List formFeildList = queryFormFieldList(registerForm); + if (CollectionUtil.isEmpty(formFeildList)) { + return formTypeDtoList; + } + List fieldList = BeanUtil.copyToList(formFeildList, FormFieldDto.class); + List optionList = fillFieldOption(fieldList); + Map optionMap = optionList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, Function.identity())); + //查询值 + String affiliatedGroupData = "";//组织 岗位 职等 + List formFieldValueList = queryFormFieldValueList(phone, rosterId); + + if (CollectionUtil.isNotEmpty(formFieldValueList)) { + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formFieldValueList); + if (null != formFieldValueMap.get(AFFILIATED_GROUP_NAME)) { + affiliatedGroupData = formFieldValueMap.get(AFFILIATED_GROUP_NAME).getValue(); + } + List roleDtoList = personnelOrgUtils.queryRoleListForUserIdNoToken(userId, tenantId); + Map> userOrgBoundInfoForUserListAndTenantIdMap = personnelOrgUtils.getUserOrgBoundInfoForUserListAndTenantId(List.of(userId), tenantId, true); + List workerGroupDataDtoList = userOrgBoundInfoForUserListAndTenantIdMap.get(userId); + for (FormFieldDto formFieldDto : fieldList) { + FtbPersonnelsStaffRegistrationFormData formFieldValue = formFieldValueMap.get(formFieldDto.getId()); + String formValueStr = ""; + if (null != formFieldValue) { + formValueStr = formFieldValue.getValue(); + formFieldDto.setUserValue(formFieldValue.getValue()); + if (formFieldDto.getType() == 2 || formFieldDto.getType() == 3) { + if (StringUtils.isNotEmpty(formFieldValue.getValue())) { + FtbPersonnelsRegistrationFormFieldOption optionEntity = null; + if ("probationPeriod".equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formFieldValue.getValue()) && formFieldValue.getValue().startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(formFieldValue.getValue(), ProbationPeriodDto.class); + String key = bean.getType(); + optionEntity = optionMap.get(key); + } + + } else if ("contractType".equals(formFieldDto.getId())) { + String contractTypeName = personnelOrgUtils.queryContractTypeNameNoToken(formFieldValue.getValue(), tenantId); + formFieldDto.setUserValueLable(contractTypeName); + } else if ("currShopId".equals(formFieldDto.getId())) { + StoreEntity storeEntity = personnelOrgUtils.queryStoreInfo(formFieldValue.getValue()); + if (null != storeEntity) { + formFieldDto.setUserValueLable(storeEntity.getStorename()); + } + } else { + optionEntity = optionMap.get(formFieldValue.getValue()); + } + if (null != optionEntity) { + formFieldDto.setUserValueLable(optionEntity.getName()); + } + } + + + } else { + if ("headLogo".equals(formFieldDto.getId())) { + List headLogoList = personnelOrgUtils.parseHeadLogo(formFieldValue.getValue()); + if (CollectionUtil.isNotEmpty(headLogoList)) { + formFieldDto.setUserValue(JSONUtil.toJsonStr(headLogoList)); + } + } + + } + } + + if ("currRole".equals(formFieldDto.getId())) { + if (CollectionUtil.isNotEmpty(roleDtoList)) { + //获取所有角色名称 + List roleName = roleDtoList.stream().map(SaffRoleDto::getFullName).collect(Collectors.toList()); + formFieldDto.setUserValueLable(String.join(",", roleName)); + List roleIds = roleDtoList.stream().map(SaffRoleDto::getId).collect(Collectors.toList()); + formFieldDto.setUserValue(String.join(",", roleIds)); + } else { + formFieldDto.setUserValue(""); + } + } else if ("currOrg".equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formValueStr)) { + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfoNoToken(formValueStr, tenantId); + if (null != organizeEntity) { + formFieldDto.setUserValueLable(organizeEntity.getFullName()); + } else { + formFieldDto.setUserValue(""); + } + } else { + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + formFieldDto.setUserValue(workerGroupDataDtoList.get(0).getAffiliatedOrg()); + formFieldDto.setUserValueLable(workerGroupDataDtoList.get(0).getAffiliatedOrgName()); + } + + } + } else if ("currPosition".equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formValueStr)) { + PositionEntity positionEntity = personnelOrgUtils.queryPositionNoToken(formValueStr, tenantId); + if (null != positionEntity) { + formFieldDto.setUserValueLable(positionEntity.getFullName()); + } else { + formFieldDto.setUserValue(""); + } + } else { + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + formFieldDto.setUserValue(workerGroupDataDtoList.get(0).getAffiliatedPosition()); + formFieldDto.setUserValueLable(workerGroupDataDtoList.get(0).getAffiliatedPositionName()); + } + } + } else if ("currRank".equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formValueStr)) { + PositionGradesInfoVO rankVo = personnelOrgUtils.queryRankNoToken(formValueStr, tenantId); + if (null != rankVo) { + formFieldDto.setUserValueLable(rankVo.getFullName()); + } else { + formFieldDto.setUserValue(""); + } + } else { + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + formFieldDto.setUserValue(workerGroupDataDtoList.get(0).getAffiliatedRank()); + formFieldDto.setUserValueLable(workerGroupDataDtoList.get(0).getAffiliatedRankName()); + } + } + } else if ("currReportsTo".equals(formFieldDto.getId())) { + FtbPersonnelsStaffRoster userEntity = personnelOrgUtils.queryUserInfoLocal(formValueStr); + if (null != userEntity) { + formFieldDto.setUserValueLable(userEntity.getName()); + } else { + formFieldDto.setUserValue(""); + } + } else if (PersonnelFormDataSystemRosterFields.AGE.getId().equals(formFieldDto.getId())) { + fieldList.stream().filter(data -> data.getId().equals(PersonnelFormDataSystemRosterFields.BIRTHDAY.getId())).findFirst().ifPresent(data -> { + FtbPersonnelsStaffRegistrationFormData birthdayFieldValue = formFieldValueMap.get(data.getId()); + if (birthdayFieldValue == null || StrUtil.isBlank(birthdayFieldValue.getValue())) { + formFieldDto.setUserValue(null); + } else { + Date dateBirthday = DateUtil.parse(birthdayFieldValue.getValue()); + long age = DateUtil.betweenYear(dateBirthday, DateUtil.date(), false); + formFieldDto.setUserValue(Long.toString(age)); + } + } + ); + formFieldDto.setCurrIsUpdate(false); + } else if (PersonnelFormDataSystemRosterFields.WORK_AGE.getId().equals(formFieldDto.getId())) { + fieldList.stream().filter(data -> data.getId().equals(PersonnelFormDataSystemRosterFields.FIRST_WORK_DATE.getId())).findFirst().ifPresent(data -> { + FtbPersonnelsStaffRegistrationFormData workAgeFieldValue = formFieldValueMap.get(data.getId()); + if (workAgeFieldValue == null || StrUtil.isBlank(workAgeFieldValue.getValue())) { + formFieldDto.setUserValue(null); + } else { + Date ageWorkAge = DateUtil.parse(workAgeFieldValue.getValue()); + long age = DateUtil.betweenYear(ageWorkAge, DateUtil.date(), false); + formFieldDto.setUserValue(Long.toString(age)); + } + } + ); + formFieldDto.setCurrIsUpdate(false); + } else if (PersonnelFormDataSystemRosterFields.ATTENDANCE_GROUP.getId().equals(formFieldDto.getId())) { + formFieldDto.setUserValue(StrUtil.EMPTY); + formFieldDto.setUserValueLable(StrUtil.EMPTY); + List attendanceUserGroup = attendanceGroupService.getAttendanceUserGroup(List.of(userId)); + if (CollectionUtil.isNotEmpty(attendanceUserGroup)) { + AttendanceUserGroupVo attendanceUserGroupVo = attendanceUserGroup.get(0); + formFieldDto.setUserValue(attendanceUserGroupVo.getGroupName()); + formFieldDto.setUserValueLable(attendanceUserGroupVo.getGroupName()); + } +// attendanceGroupService.getAttendanceUserListGroupVO(List.of(userId)).stream().findFirst().ifPresent(vo -> { +// formFieldDto.setUserValue(vo.getGroupName()); +// formFieldDto.setUserValueLable(vo.getGroupFullName()); +// }); + } + + } + } + + //按照formTypeId 分组 + Map> fieldMap = fieldList.stream().collect(Collectors.groupingBy(FormFieldDto::getFormTypeId)); + for ( + FormTypeDto formTypeDto : formTypeDtoList) { + List formFieldDtoList = fieldMap.get(formTypeDto.getId()); + if (CollectionUtil.isNotEmpty(formFieldDtoList)) { + formTypeDto.setGroupFieldDataDtoList(convertGroupFieldDataDto(formFieldDtoList, affiliatedGroupData, userId, tenantId)); + } + } + return filterNullFieldType(formTypeDtoList, registerForm); + + } + + + /** + * @param phone + * @param registerForm true 表示只查询入职登记表中字段,false 查询所有字段 + * @return + */ + @Override + public List queryAndFillFormFieldValue(String phone, String userId, String rosterId, Boolean registerForm) { + //查询字段类型 + List formTypeList = queryFormTypeList(); + if (CollectionUtil.isEmpty(formTypeList)) { + return new ArrayList<>(); + } + List formTypeDtoList = BeanUtil.copyToList(formTypeList, FormTypeDto.class); + //查询字段 + List formFeildList = queryFormFieldList(registerForm); + if (CollectionUtil.isEmpty(formFeildList)) { + return formTypeDtoList; + } + List fieldList = BeanUtil.copyToList(formFeildList, FormFieldDto.class); + List optionList = fillFieldOption(fieldList); + Map optionMap = optionList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, Function.identity())); + //查询值 + String affiliatedGroupData = "";//组织 岗位 职等 + List formFieldValueList = queryFormFieldValueList(phone, rosterId); + FtbPersonnelsStaffRoster exiestRosterEntity = null; + if (StringUtils.isNotEmpty(rosterId)) { + exiestRosterEntity = staffRosterService.getById(rosterId); + }else{ + // 兼容离职数据 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRoster::getPhone,phone); + queryWrapper.eq(FtbPersonnelsStaffRoster::getWorkerStatus,"305"); + FtbPersonnelsStaffRoster serviceOne = staffRosterService.getOne(queryWrapper); + if (Objects.nonNull(serviceOne)){ + exiestRosterEntity = serviceOne; + } + } + + if (CollectionUtil.isNotEmpty(formFieldValueList)) { + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formFieldValueList); + if (null != formFieldValueMap.get(AFFILIATED_GROUP_NAME)) { + affiliatedGroupData = formFieldValueMap.get(AFFILIATED_GROUP_NAME).getValue(); + } + // 健康证 + Optional healthCertificateDetail = certificateInstanceService.getHealthCertificateDetail(userId); + for (FormFieldDto formFieldDto : fieldList) { + FtbPersonnelsStaffRegistrationFormData formFieldValue = formFieldValueMap.get(formFieldDto.getId()); + FtbPersonnelsRegistrationFormFieldOption optionEntity = null; + String formValueStr = ""; + if (null != formFieldValue) { + formValueStr = formFieldValue.getValue(); + formFieldDto.setUserValue(formFieldValue.getValue()); + if (formFieldDto.getType() == 2 || formFieldDto.getType() == 3) { + if (StringUtils.isNotEmpty(formFieldValue.getValue())) { + if ("probationPeriod".equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formFieldValue.getValue()) && formFieldValue.getValue().startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(formFieldValue.getValue(), ProbationPeriodDto.class); + String key = bean.getType(); + optionEntity = optionMap.get(key); + } + + } else if ("contractType".equals(formFieldDto.getId())) { + String contractTypeName = personnelOrgUtils.queryContractTypeName(formFieldValue.getValue()); + formFieldDto.setUserValueLable(contractTypeName); + } else if ("currShopId".equals(formFieldDto.getId())) { + StoreEntity storeEntity = personnelOrgUtils.queryStoreInfo(formFieldValue.getValue()); + if (null != storeEntity) { + formFieldDto.setUserValueLable(storeEntity.getStorename()); + } + } else if ("意向组织".equals(formFieldDto.getName())) { + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(formValueStr); + formFieldDto.setOptionDtoList(new ArrayList<>()); + if (null != organizeEntity) { + formFieldDto.setUserValueLable(organizeEntity.getFullName()); + } else { + formFieldDto.setUserValue(""); + formFieldDto.setUserValueLable(""); + } + } else { + optionEntity = optionMap.get(formFieldValue.getValue()); + } + if (null != optionEntity) { + formFieldDto.setUserValueLable(optionEntity.getName()); + } + } + + } else { + if ("headLogo".equals(formFieldDto.getId())) { + List headLogoList = personnelOrgUtils.parseHeadLogo(formFieldValue.getValue()); + if (CollectionUtil.isNotEmpty(headLogoList)) { + formFieldDto.setUserValue(JSONUtil.toJsonStr(headLogoList)); + } + } else if (PersonnelFormDataSystemRosterFields.CONTRACT_FILE.getId().equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formValueStr) && !formValueStr.equals("[]")) { + if (!formValueStr.contains("{")) { + formFieldDto.setUserValue(convertOldContractFile(formValueStr)); + } + } + } + + } + } + // 修改司龄 + if ("companyAge".equals(formFieldDto.getId()) && ObjectUtil.isNotEmpty(exiestRosterEntity)){ + Date departDate = exiestRosterEntity.getDepartDate(); + String workerStatus = exiestRosterEntity.getWorkerStatus(); + // 实际入职时间 + Date actualStartDate = exiestRosterEntity.getActualStartDate(); + String companyAge = ""; + if ("305".equals(workerStatus) && ObjectUtil.isNotEmpty(departDate)) { + companyAge = CompanyAgeUtil.calculateServiceAgeShort(actualStartDate,departDate); + }else if (ObjectUtil.isNotEmpty(actualStartDate)) { + companyAge = CompanyAgeUtil.calculateServiceAgeShort(actualStartDate,new Date()); + } + formFieldDto.setUserValue(companyAge); + } else if ("currOrg".equals(formFieldDto.getId())) { + String formValueStrTeamId = formValueStr; + if (Objects.nonNull(exiestRosterEntity) && StringUtils.isNotEmpty(exiestRosterEntity.getCurrOrg())) { + formValueStrTeamId = exiestRosterEntity.getCurrOrg(); + } + if (StringUtils.isNotEmpty(formValueStrTeamId)) { + formFieldDto.setUserValue(formValueStrTeamId); + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(formValueStrTeamId); + if (null != organizeEntity) { + formFieldDto.setUserValueLable(organizeEntity.getFullName()); + } else { + formFieldDto.setUserValueLable(""); + } + } else { + formFieldDto.setUserValue(""); + formFieldDto.setUserValueLable(""); + + } + } else if ("currPosition".equals(formFieldDto.getId())) { + String formValueStrTeamId = formValueStr; + if (Objects.nonNull(exiestRosterEntity) && StringUtils.isNotEmpty(exiestRosterEntity.getCurrPosition())) { + formValueStrTeamId = exiestRosterEntity.getCurrPosition(); + } + if (StringUtils.isNotEmpty(formValueStrTeamId)) { + formFieldDto.setUserValue(formValueStrTeamId); + PositionEntity positionEntity = personnelOrgUtils.queryPosition(formValueStrTeamId); + if (null != positionEntity) { + formFieldDto.setUserValueLable(positionEntity.getFullName()); + } else { + formFieldDto.setUserValueLable(""); + } + } else { + formFieldDto.setUserValue(""); + formFieldDto.setUserValueLable(""); + } + } else if ("currRank".equals(formFieldDto.getId())) { + String formValueStrTeamId = formValueStr; + if (Objects.nonNull(exiestRosterEntity) && StringUtils.isNotEmpty(exiestRosterEntity.getCurrRank())) { + formValueStrTeamId = exiestRosterEntity.getCurrRank(); + } + if (StringUtils.isNotEmpty(formValueStrTeamId)) { + formFieldDto.setUserValue(formValueStrTeamId); + PositionGradesInfoVO rankVo = personnelOrgUtils.queryRank(formValueStrTeamId); + if (null != rankVo) { + formFieldDto.setUserValueLable(rankVo.getFullName()); + } else { + formFieldDto.setUserValueLable(""); + } + } else { + formFieldDto.setUserValue(""); + formFieldDto.setUserValueLable(""); + } + } else if ("currReportsTo".equals(formFieldDto.getId())) { + String formValueStrTeamId = formValueStr; + if (Objects.nonNull(exiestRosterEntity) && StringUtils.isNotEmpty(exiestRosterEntity.getCurrReportsTo())) { + formValueStrTeamId = exiestRosterEntity.getCurrReportsTo(); + } + UserEntity userEntity = personnelOrgUtils.queryUserInfo(formValueStrTeamId); + formFieldDto.setUserValue(formValueStrTeamId); + if (null != userEntity) { + formFieldDto.setUserValueLable(userEntity.getRealName()); + } else { + formFieldDto.setUserValue(""); + formFieldDto.setUserValueLable(""); + } + } else if ("onboardingTeam".equals(formFieldDto.getId())) { + String formValueStrTeamId = formValueStr; + if (Objects.nonNull(exiestRosterEntity) && StringUtils.isNotEmpty(exiestRosterEntity.getCurrGroupId())) { + formValueStrTeamId = exiestRosterEntity.getCurrGroupId(); + } + if (StringUtils.isNotEmpty(formValueStrTeamId)) { + formFieldDto.setUserValue(formValueStrTeamId); + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(formValueStrTeamId); + if (null != organizeEntity) { + formFieldDto.setUserValueLable(organizeEntity.getFullName()); + } else { + formFieldDto.setUserValueLable(""); + } + } else { + formFieldDto.setUserValue(""); + formFieldDto.setUserValueLable(""); + } + } else if (PersonnelFormDataSystemRosterFields.AGE.getId().equals(formFieldDto.getId())) { + fieldList.stream().filter(data -> data.getId().equals(PersonnelFormDataSystemRosterFields.BIRTHDAY.getId())).findFirst().ifPresent(data -> { + FtbPersonnelsStaffRegistrationFormData birthdayFieldValue = formFieldValueMap.get(data.getId()); + if (birthdayFieldValue == null || StrUtil.isBlank(birthdayFieldValue.getValue())) { + formFieldDto.setUserValue(null); + } else { + Date dateBirthday = DateUtil.parse(birthdayFieldValue.getValue()); + long age = DateUtil.betweenYear(dateBirthday, DateUtil.date(), false); + formFieldDto.setUserValue(Long.toString(age)); + } + } + ); + formFieldDto.setCurrIsUpdate(false); + } else if (PersonnelFormDataSystemRosterFields.WORK_AGE.getId().equals(formFieldDto.getId())) { + fieldList.stream().filter(data -> data.getId().equals(PersonnelFormDataSystemRosterFields.FIRST_WORK_DATE.getId())).findFirst().ifPresent(data -> { + FtbPersonnelsStaffRegistrationFormData workAgeFieldValue = formFieldValueMap.get(data.getId()); + if (workAgeFieldValue == null || StrUtil.isBlank(workAgeFieldValue.getValue())) { + formFieldDto.setUserValue(null); + } else { + Date ageWorkAge = DateUtil.parse(workAgeFieldValue.getValue()); + long age = DateUtil.betweenYear(ageWorkAge, DateUtil.date(), false); + formFieldDto.setUserValue(Long.toString(age)); + } + } + ); + formFieldDto.setCurrIsUpdate(false); + } else if (PersonnelFormDataSystemRosterFields.CONTRACT_STATUS.getId().equals(formFieldDto.getId())) { + if (formFieldValue != null && StringUtils.isNotEmpty(formFieldValue.getValue())) { + optionEntity = optionMap.get(formFieldValue.getValue()); + if (null != optionEntity) { + formFieldDto.setUserValue(formFieldValue.getValue()); + formFieldDto.setUserValueLable(optionEntity.getName()); + } + } else { + if (null != exiestRosterEntity) { + optionEntity = optionMap.get(exiestRosterEntity.getContractStatus().toString()); + if (null != optionEntity) { + formFieldDto.setUserValue(exiestRosterEntity.getContractStatus() + ""); + formFieldDto.setUserValueLable(optionEntity.getName()); + } + } + } + } else if (PersonnelFormDataSystemRosterFields.HEALTH_CERTIFICATE.getId().equals(formFieldDto.getId())) { + if (healthCertificateDetail.isPresent()) { + formFieldDto.setUserValue(healthCertificateDetail.get().getCertificateImage()); + formFieldDto.setUserValueLable(healthCertificateDetail.get().getCertificateImage()); + } + } else if ("startHealthDate".equals(formFieldDto.getId())) { + if (healthCertificateDetail.isPresent() && healthCertificateDetail.get().getIssueDate() != null) { + String issueDate = DateUtil.format(healthCertificateDetail.get().getIssueDate(),"yyyy-MM-dd"); + formFieldDto.setUserValue(issueDate); + formFieldDto.setUserValueLable(issueDate); + } + } else if ("endHealthDate".equals(formFieldDto.getId())) { + if (healthCertificateDetail.isPresent() && healthCertificateDetail.get().getExpireDate() != null) { + String issueDate = DateUtil.format(healthCertificateDetail.get().getExpireDate(),"yyyy-MM-dd"); + formFieldDto.setUserValue(issueDate); + formFieldDto.setUserValueLable(issueDate); + } + } + } + } + + //按照formTypeId 分组 + Map> fieldMap = fieldList.stream().collect(Collectors.groupingBy(FormFieldDto::getFormTypeId)); + for ( + FormTypeDto formTypeDto : formTypeDtoList) { + List formFieldDtoList = fieldMap.get(formTypeDto.getId()); + if (CollectionUtil.isNotEmpty(formFieldDtoList)) { + formTypeDto.setGroupFieldDataDtoList(convertGroupFieldDataDto(formFieldDtoList, affiliatedGroupData, userId)); + } + } + return filterNullFieldType(formTypeDtoList, registerForm); + + } + + private String convertOldContractFile(String formValueStr) { + List fileList = new ArrayList<>(); + List list = JSONUtil.toList(formValueStr, String.class); + if (CollectionUtil.isNotEmpty(list)) { + for (String urlPath : list) { + Path path = Paths.get(urlPath); + String fileName = path.getFileName().toString(); + // 分离扩展名 + int dotIndex = fileName.lastIndexOf('.'); + String extension = dotIndex == -1 ? "" : fileName.substring(dotIndex + 1); + ContactStatusInfo.ContactInfoFile file = new ContactStatusInfo.ContactInfoFile(); + file.setName(fileName); + file.setUrl(urlPath); + file.setFileExtension(extension); + fileList.add(file); + } + } + return JSONUtil.toJsonStr(fileList); + } + + /** + * 过滤没有字段的项 + * + * @param formTypeDtoList + * @return + */ + private List filterNullFieldType(List formTypeDtoList, Boolean registerForm) { + List retList = new ArrayList<>(); + for (FormTypeDto formTypeDto : formTypeDtoList) { + if (!registerForm) { + //系统类型直接保留 + if (formTypeDto.getSystemType().equals(0)) { + retList.add(formTypeDto); + continue; + } + } + if (CollectionUtil.isNotEmpty(formTypeDto.getGroupFieldDataDtoList())) { + retList.add(formTypeDto); + } + } + return retList; + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 5*60*1000) + public void saveArchivesWithPhone(SaveFormDataReq req) { + String phone = req.getPhone(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRoster::getPhone, phone); + wrapper.ne(FtbPersonnelsStaffRoster::getWorkerStatus,"305"); + wrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark,0); + long count = staffRosterService.count(wrapper); + if (count > 0) throw new RuntimeException("当前员工手机号已经存在系统,请勿重复添加!"); + //组装数据 + List formDataList = new ArrayList<>(); + FtbPersonnelsStaffRoster updateEntity = new FtbPersonnelsStaffRoster(); + //需要同步的数据 + String currRole = ""; + String headLogo = ""; + String birthday = ""; + String idCardNum = ""; + String workerName = ""; + List inputFieldValueList = req.getFieldValueList(); + if (CollectionUtil.isEmpty(inputFieldValueList)) { + throw new RuntimeException("提交数据为空"); + } + QueryWrapper wrapper1 = new QueryWrapper<>(); + wrapper1.lambda() + .eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0).last("limit 1"); + //查询字段 + List formFeildList = queryFormFieldList(false); + if (CollectionUtil.isEmpty(formFeildList)) { + log.error("未查询到字段"); + return; + } + // 提取姓名 + String workIdCardName = inputFieldValueList.stream() + .filter(item -> "workerName".equals(item.getId())) + .map(SubFormFieldDto::getUserValue) + .findFirst() + .orElse(null); + // 获取租户Id + String tenantId = UserProvider.getUser().getTenantId(); + // 校验身份证号 + inputFieldValueList.stream().filter(item -> "idCardNum".equals(item.getId())&& !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "idCardNum", a.getUserValue()) > 0) { + throw new RuntimeException("身份证号已存在"); + } + if (!RegexUtils.checkIdCard(a.getUserValue())) { + throw new RuntimeException("请输入正确的身份证号码"); + } + DateTime ageByIdCard = IdcardUtil.getBirthDate(a.getUserValue()); + DateTime dateTime = DateUtil.beginOfDay(DateUtil.date()); + if (ageByIdCard.isAfterOrEquals(dateTime)) { + throw new RuntimeException("请输入正确的身份证号码"); + } + // 身份证合法校验 + IdCardVerificationResponse idCardVerificationResponse = personnelIdCardVerificationUtils.idCardVerification(a.getUserValue(), workIdCardName, tenantId); + personnelIdCardVerificationUtils.checkIdCardVerification(idCardVerificationResponse); + }); + // 校验银行卡号 + inputFieldValueList.stream().filter(item -> "bankCardNo".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "bankCardNo", a.getUserValue()) > 0) { + throw new RuntimeException("银行卡号已存在"); + } + }); + // 校验公积金号 + inputFieldValueList.stream().filter(item -> "providentFundAccount".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "providentFundAccount", a.getUserValue()) > 0) { + throw new RuntimeException("公积金号已存在"); + } + }); + // 校验社保账号 + inputFieldValueList.stream().filter(item -> "socialAccount".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "socialAccount", a.getUserValue()) > 0) { + throw new RuntimeException("社保账号已存在"); + } + }); + Map formFieldMap = formFeildList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormField::getId, Function.identity())); + //写入formData数据 + List formFieldValueList = queryFormFieldValueList(phone, null); + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formFieldValueList); + //健康证几个字段 + String healthCertificate = ""; + String startHealthDate = ""; + String endHealthDate = ""; + for (SubFormFieldDto subFormFieldDto : inputFieldValueList) { + String field = subFormFieldDto.getId(); + if (StringUtils.isEmpty(field)) { + throw new RuntimeException("字段id不能为空"); + } + if ( StringUtils.isEmpty(subFormFieldDto.getUserValue())) { + subFormFieldDto.setUserValue(""); + } + + if (!AFFILIATED_GROUP_NAME.equals(field)) { + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormField = formFieldMap.get(field); + if (null == ftbPersonnelsRegistrationFormField) { + throw new RuntimeException("登记表配置已更新,请退出页面后重新填写!"); + // throw new RuntimeException(field + "字段不存在"); + } + if (ftbPersonnelsRegistrationFormField.getIsNeedFill() == 1 && ( + StringUtils.isEmpty(subFormFieldDto.getUserValue()) + || "[]".equals(subFormFieldDto.getUserValue()) + )) { + throw new RuntimeException(ftbPersonnelsRegistrationFormField.getName() + "不能为空"); + } + Boolean isCheck = true; + + if (ftbPersonnelsRegistrationFormField.getIsUpdateWorker() == 1 && ftbPersonnelsRegistrationFormField.getIsUpdateManager() == 1) { + isCheck = false; + } + + //如果是字符串判断长度不能超限 + if (isCheck == true && (ftbPersonnelsRegistrationFormField.getType() == 0 || ftbPersonnelsRegistrationFormField.getType() == 1)) { + Long limits = ftbPersonnelsRegistrationFormField.getLimits(); + if (limits != null && limits > 0) { + long length = StringUtils.length(subFormFieldDto.getUserValue()); + if (length > limits) { + throw new RuntimeException(ftbPersonnelsRegistrationFormField.getName() + "长度不能超过" + limits + "个字符"); + } + } + } + } + if ("birthday".equals(subFormFieldDto.getId())) { + birthday = subFormFieldDto.getUserValue(); + checkBirthdayFormat(birthday); + } else if ("idCardNum".equals(subFormFieldDto.getId())) { + idCardNum = subFormFieldDto.getUserValue(); + } else if ("workerName".equals(subFormFieldDto.getId())) { + workerName = subFormFieldDto.getUserValue(); + // 校验前后是否有空格 + Pattern spacesCheck = Pattern.compile(FtbRosterImportConstants.NAME_REGEX); + Matcher spacesCheckMatcher = spacesCheck.matcher(workerName); + if (!spacesCheckMatcher.matches()){ + throw new RuntimeException("请使用标准格式的中文名或英文名!"); + } + }else if ("currRole".equals(subFormFieldDto.getId())) { + currRole = subFormFieldDto.getUserValue(); + if (StringUtils.isNotEmpty(currRole) && currRole.startsWith("{")) { + throw new RuntimeException("角色字段不能为json格式"); + } + } else if ("headLogo".equals(subFormFieldDto.getId())) { + headLogo = subFormFieldDto.getUserValue(); + if (StringUtils.isNotEmpty(headLogo) && !headLogo.startsWith("[")) { + throw new RuntimeException("提交的头像数据格式不正确"); + } + } else if ("healthCertificate".equals(subFormFieldDto.getId())) { + healthCertificate = subFormFieldDto.getUserValue(); + } else if ("startHealthDate".equals(subFormFieldDto.getId())) { + startHealthDate = subFormFieldDto.getUserValue(); + } else if ("endHealthDate".equals(subFormFieldDto.getId())) { + endHealthDate = subFormFieldDto.getUserValue(); + } + personnelOrgUtils.fillRoster(updateEntity, subFormFieldDto); + FtbPersonnelsStaffRegistrationFormData oldFormData = formFieldValueMap.get(subFormFieldDto.getId()); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + if (null != oldFormData) { + formData.setId(oldFormData.getId()); + } + formData.setPhone(phone); + formData.setFormFieldId(subFormFieldDto.getId()); + formData.setFormTypeId(subFormFieldDto.getFormTypeId()); + formData.setValue(subFormFieldDto.getUserValue()); + formDataList.add(formData); + } + // 身份证号码识别出生日期 + if (StringUtils.isNotEmpty(idCardNum) && !"null".equals(idCardNum) && StringUtils.isEmpty(birthday)) { + Date birthdayDate = IdcardUtil.getBirthDate(idCardNum).toJdkDate(); + FtbPersonnelsRegistrationFormField registrationFormField = formFieldMap.get("birthday"); + FtbPersonnelsRegistrationFormField registrationFormFieldAge = formFieldMap.get("age"); + if (Objects.nonNull(registrationFormField) && Objects.nonNull(registrationFormFieldAge)) { + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setPhone(phone); + formData.setFormTypeId(registrationFormField.getFormTypeId()); + formData.setFormFieldId(registrationFormField.getId()); + formData.setValue(DateUtil.format(birthdayDate, "yyyy-MM-dd")); + int age = personnelOrgUtils.calculateAge(personnelOrgUtils.dateToLocalDate(birthdayDate)); + FtbPersonnelsStaffRegistrationFormData formDataAge = new FtbPersonnelsStaffRegistrationFormData(); + formDataAge.setPhone(phone); + formDataAge.setFormTypeId(registrationFormFieldAge.getFormTypeId()); + formDataAge.setFormFieldId(registrationFormFieldAge.getId()); + formDataAge.setValue(String.valueOf(age)); + formDataList.add(formDataAge); + formDataList.add(formData); + } + } + //写入数据库 + if (CollectionUtil.isNotEmpty(formDataList)) { + updateData(formDataList); + /*List updateFormData = formDataList.stream().filter(item -> StringUtils.isNotEmpty(item.getId())).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(updateFormData)) Db.updateBatchById(updateFormData); + List insertFormData = formDataList.stream().filter(item -> StringUtils.isEmpty(item.getId())).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(insertFormData)) Db.saveBatch(insertFormData);*/ + } + // 内推池状态变更 + personnelOrgUtils.sysWorkerStatusToUchisuike(phone, updateEntity.getWorkerStatus()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone,phone); + FtbPersonnelsStaffEmploymentApply staffEmploymentApply = staffEmploymentApplyService.getBaseMapper().selectOne(queryWrapper); + // 根据入职填写新增 入职办理列表数据 + String certificateUserId = ""; + if (req.getSource().equals("1")) { + if (staffEmploymentApply == null) { + // 新增入职 + certificateUserId = onboardingData(UserProvider.getUser().getTenantId(), 4, null, phone, workerName, permissionsUtils.getRequestModule()); + }else { + LambdaUpdateWrapper wrapper3 = Wrappers.lambdaUpdate(); + wrapper3.set(StringUtils.isNotEmpty(workerName), FtbPersonnelsStaffEmploymentApply::getWorkerName,workerName); + if (StringUtils.isNotEmpty(workerName)) { + wrapper3.set(FtbPersonnelsStaffEmploymentApply::getWorkerName,workerName); + String userId = staffEmploymentApply.getUserId(); + if (userId == null){ + String userHeadLog = generaDefaultUserHeadLog(staffEmploymentApply.getWorkerName()); + userId = saveUserInfo(staffEmploymentApply, userHeadLog); + wrapper3.set(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + } + certificateUserId = userId; + syncWorkName(workerName, UserProvider.getUser().getTenantId(), userId); + } + wrapper3.set(FtbPersonnelsStaffEmploymentApply::getIdCardNum,idCardNum); + wrapper3.set(FtbPersonnelsStaffEmploymentApply::getIsSubmitForm,1); + wrapper3.eq(FtbPersonnelsStaffEmploymentApply::getId, staffEmploymentApply.getId()); + applyMapper.update(new FtbPersonnelsStaffEmploymentApply(),wrapper3); + } + }else if (req.getSource().equals("2") && staffEmploymentApply != null){ + String userId = staffEmploymentApply.getUserId(); + if (userId == null){ + String userHeadLog = generaDefaultUserHeadLog(staffEmploymentApply.getWorkerName()); + userId = saveUserInfo(staffEmploymentApply, userHeadLog); + staffEmploymentApply.setUserId(userId); + } + if (staffEmploymentApply.getIsSubmitForm() != 1) { + // 代为填写 + staffEmploymentApply.setIsSubmitForm(1); + extracted(workerName, UserProvider.getUser().getTenantId(), userId, staffEmploymentApply.getCurrOrg(),permissionsUtils.getRequestModule()); + } + if (StringUtils.isNotEmpty(workerName)){ + staffEmploymentApply.setWorkerName(workerName); + + syncWorkName(workerName, UserProvider.getUser().getTenantId(), userId); + } + certificateUserId = userId; + staffEmploymentApply.setIdCardNum(idCardNum); + staffEmploymentApplyService.updateById(staffEmploymentApply); + } + // 健康证同步 + certificateInstanceService.saveHealthCertificate(certificateUserId,healthCertificate,startHealthDate,endHealthDate); + } + @Transactional(rollbackFor = Exception.class) + private void updateData(List formDataList) { + formDataList.forEach(item -> { + if (StringUtils.isNotEmpty(item.getId())) { + baseMapper.updateById(item); + } else { + baseMapper.insert(item); + } + }); + } + + /** + * 同步员工名称 + * @param workerName + * @param tenantId + * @param userId + */ + private void syncWorkName(String workerName, String tenantId, String userId){ + if (StringUtils.isEmpty(userId)) { + return; + } + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setRealName(workerName); + userDTO.setTenantId(tenantId); + v2UserApi.updateUserInfo(userId,userDTO); + } + + @Override + public Boolean checkActualJoiningDatePassed(String phone, String userId) { + doJoiningDateVerification(phone, userId); + return true; + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void saveArchives(SaveFormDataReq req) { + //查询花名册 + FtbPersonnelsStaffRoster rosterEntity = staffRosterService.queryRosterInfoByUserId(req.getUserId()); + String phone = rosterEntity.getPhone(); + if (StrUtil.isEmpty(req.getPhone())) { + req.setPhone(phone); + } + + List inputFieldValueList = req.getFieldValueList(); + if (CollectionUtil.isEmpty(inputFieldValueList)) { + throw new RuntimeException("提交数据为空"); + } + // 姓名 + String workerUserName = inputFieldValueList.stream() + .filter(item -> "workerName".equals(item.getId())) + .findFirst() + .map(SubFormFieldDto::getUserValue) + .orElse(null); + // 校验身份证号 + inputFieldValueList.stream().filter(item -> "idCardNum".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "idCardNum", a.getUserValue()) > 0) { + throw new RuntimeException("身份证号已存在"); + } + if (!RegexUtils.checkIdCard(a.getUserValue())) { + throw new RuntimeException("请输入正确的身份证号码"); + } + DateTime ageByIdCard = IdcardUtil.getBirthDate(a.getUserValue()); + if (ageByIdCard.isAfter(new Date())) { + throw new RuntimeException("请输入正确的身份证号码"); + } + // 姓名+身份证号校验 + IdCardVerificationResponse idCardVerificationResponse = personnelIdCardVerificationUtils.idCardVerification(a.getUserValue(), + workerUserName, UserProvider.getUser().getTenantId()); + personnelIdCardVerificationUtils.checkIdCardVerification(idCardVerificationResponse); + }); + // 校验银行卡号 + inputFieldValueList.stream().filter(item -> "bankCardNo".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "bankCardNo", a.getUserValue()) > 0) { + throw new RuntimeException("银行卡号已存在"); + } + }); + // 校验公积金号 + inputFieldValueList.stream().filter(item -> "providentFundAccount".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "providentFundAccount", a.getUserValue()) > 0) { + throw new RuntimeException("公积金号已存在"); + } + }); + // 校验社保账号 + inputFieldValueList.stream().filter(item -> "socialAccount".equals(item.getId()) && !StringUtils.isEmpty(item.getUserValue())).findFirst().ifPresent(a -> { + if (baseMapper.verifyWhetherTheDataExists(req.getPhone(), "socialAccount", a.getUserValue()) > 0) { + throw new RuntimeException("社保账号已存在"); + } + }); + String workerNo = personnelOrgUtils.getPhoneFormInputForField(inputFieldValueList, "workerNo"); + if (StringUtils.isNotEmpty(workerNo)) { + List rosterList = staffRosterService.queryRosterInfoByWorkerId(workerNo); + if (CollectionUtil.isNotEmpty(rosterList)) { + if (rosterList.size() > 1) { + throw new RuntimeException("员工工号在系统中已经存在"); + } + FtbPersonnelsStaffRoster roster = rosterList.get(0); + if (!roster.getId().equals(rosterEntity.getId())) { + throw new RuntimeException("员工工号在系统中已经存在"); + } + } + } + String currReportsTo = personnelOrgUtils.getPhoneFormInputForField(inputFieldValueList, "currReportsTo"); + if (StringUtils.isNotEmpty(currReportsTo) && rosterEntity.getUserId().equals(currReportsTo)) { + throw new RuntimeException("入职的直属主管不能是自己"); + } + //查询字段 + List formFeildList = queryFormFieldList(false); + if (CollectionUtil.isEmpty(formFeildList)) { + log.error("未查询到字段"); + return; + } + Map formFieldMap = formFeildList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormField::getId, Function.identity())); + + //写入formData数据 + List formFieldValueList = queryFormFieldValueList(null, rosterEntity.getId()); + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formFieldValueList); + //组装数据 + List formDataList = new ArrayList<>(); + FtbPersonnelsStaffRoster updateEntity = new FtbPersonnelsStaffRoster(); + //需要同步的数据 + String affiliated = ""; + String currRole = ""; + String headLogo = ""; + String flowerName = ""; + String workerName = ""; + String workerSex = ""; + String workerStatus = ""; + String actualProbationaryDate = ""; + String birthday = ""; + String idCardNum = ""; + //健康证几个字段 + String healthCertificate = ""; + String startHealthDate = ""; + String endHealthDate = ""; + Date now = new Date(); + String actualStartDate = personnelOrgUtils.getPhoneFormInputForField(inputFieldValueList, "actualStartDate"); + + for (SubFormFieldDto subFormFieldDto : inputFieldValueList) { + String field = subFormFieldDto.getId(); + if (StringUtils.isEmpty(field)) { + throw new RuntimeException("字段id不能为空"); + } + if ( ( + StringUtils.isEmpty(subFormFieldDto.getUserValue()) + || "[]".equals(subFormFieldDto.getUserValue()) + )) { + subFormFieldDto.setUserValue(""); + } + + if (!AFFILIATED_GROUP_NAME.equals(field)) { + FtbPersonnelsRegistrationFormField ftbPersonnelsRegistrationFormField = formFieldMap.get(field); + if (null == ftbPersonnelsRegistrationFormField) { + throw new RuntimeException(field + "字段不存在"); + } + if (ftbPersonnelsRegistrationFormField.getIsNeedFill() == 1 && ( + StringUtils.isEmpty(subFormFieldDto.getUserValue()) + || "[]".equals(subFormFieldDto.getUserValue()) + )) { + throw new RuntimeException(ftbPersonnelsRegistrationFormField.getName() + "不能为空"); + } + Boolean isCheck = true; + + if (ftbPersonnelsRegistrationFormField.getIsUpdateWorker() == 1 && ftbPersonnelsRegistrationFormField.getIsUpdateManager() == 1) { + isCheck = false; + } + + //如果是字符串判断长度不能超限 + if (isCheck == true && (ftbPersonnelsRegistrationFormField.getType() == 0 || ftbPersonnelsRegistrationFormField.getType() == 1)) { + Long limits = ftbPersonnelsRegistrationFormField.getLimits(); + if (limits != null && limits.longValue() > 0) { + long length = StringUtils.length(subFormFieldDto.getUserValue()); + if (length > limits.longValue()) { + throw new RuntimeException(ftbPersonnelsRegistrationFormField.getName() + "长度不能超过" + limits + "个字符"); + } + } + } + } + if ("phone".equals(subFormFieldDto.getId())) { + continue; + } else if ("flowerName".equals(subFormFieldDto.getId())) { + flowerName = subFormFieldDto.getUserValue(); + } else if ("workerName".equals(subFormFieldDto.getId())) { + workerName = subFormFieldDto.getUserValue(); + } else if ("workerSex".equals(subFormFieldDto.getId())) { + workerSex = subFormFieldDto.getUserValue(); + } else if ("workerStatus".equals(subFormFieldDto.getId())) { + workerStatus = subFormFieldDto.getUserValue(); + } else if ("actualProbationaryDate".equals(subFormFieldDto.getId())) { + actualProbationaryDate = subFormFieldDto.getUserValue(); + } else if ("idCardNum".equals(subFormFieldDto.getId())) { + idCardNum = subFormFieldDto.getUserValue(); + } else if ("birthday".equals(subFormFieldDto.getId())) { + birthday = subFormFieldDto.getUserValue(); + checkBirthdayFormat(birthday); + } else if ("currRole".equals(subFormFieldDto.getId())) { + currRole = subFormFieldDto.getUserValue(); + if (StringUtils.isNotEmpty(currRole) && currRole.startsWith("{")) { + throw new RuntimeException("角色字段不能为json格式"); + } + } else if ("headLogo".equals(subFormFieldDto.getId())) { + headLogo = subFormFieldDto.getUserValue(); + if (StringUtils.isNotEmpty(headLogo) && !headLogo.startsWith("[")) { + throw new RuntimeException("提交的头像数据格式不正确"); + } + } else if (AFFILIATED_GROUP_NAME.equals(subFormFieldDto.getId())) { + affiliated = subFormFieldDto.getUserValue(); + } else if ("companyAge".equals(subFormFieldDto.getId())) { + if (StringUtils.isNotEmpty(actualStartDate) + && null != rosterEntity + && (!rosterEntity.getWorkerStatus().equals("304") || !rosterEntity.getWorkerStatus().equals("305"))) { + long betweenDay = 0L; + DateTime parse = DateUtil.parse(actualStartDate, "yyyy-MM-dd"); + if (now.after(parse)) { + betweenDay = DateUtil.betweenDay(parse, now, false); + } + subFormFieldDto.setUserValue(String.valueOf(betweenDay)); + } + } else if ("healthCertificate".equals(subFormFieldDto.getId())) { + healthCertificate = subFormFieldDto.getUserValue(); + } else if ("startHealthDate".equals(subFormFieldDto.getId())) { + startHealthDate = subFormFieldDto.getUserValue(); + } else if ("endHealthDate".equals(subFormFieldDto.getId())) { + endHealthDate = subFormFieldDto.getUserValue(); + } + personnelOrgUtils.fillRoster(updateEntity, subFormFieldDto); + FtbPersonnelsStaffRegistrationFormData oldFormData = formFieldValueMap.get(subFormFieldDto.getId()); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + if (null != oldFormData) { + formData.setId(oldFormData.getId()); + } + formData.setRosterId(rosterEntity.getId()); + formData.setPhone(phone); + formData.setFormFieldId(subFormFieldDto.getId()); + formData.setFormTypeId(subFormFieldDto.getFormTypeId()); + formData.setValue(subFormFieldDto.getUserValue()); + + formDataList.add(formData); + } + // 同步主管、班组 + if (StringUtils.isNotEmpty(affiliated) && !"[]".equals(affiliated)) { + List> parseGroupData = personnelOrgUtils.parseGroupData(affiliated); + List workerGroupDataDtoList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(parseGroupData)) { + for (List parseGroupDatum : parseGroupData) { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + for (SubMulitFieldValDto subMulitFieldValDto : parseGroupDatum) { + if ("affiliatedReportsTo".equals(subMulitFieldValDto.getId())) { + workerGroupDataDto.setReportsTo(subMulitFieldValDto.getUserValue()); + } else if ("team".equals(subMulitFieldValDto.getId())) { + workerGroupDataDto.setStoreTeamId(subMulitFieldValDto.getUserValue()); + } else if ("affiliatedRank".equals(subMulitFieldValDto.getId())) { + workerGroupDataDto.setAffiliatedRank(subMulitFieldValDto.getUserValue()); + } else if ("affiliatedPosition".equals(subMulitFieldValDto.getId())) { + workerGroupDataDto.setAffiliatedPosition(subMulitFieldValDto.getUserValue()); + } else if ("affiliatedOrg".equals(subMulitFieldValDto.getId())) { + workerGroupDataDto.setAffiliatedOrg(subMulitFieldValDto.getUserValue()); + } + } + workerGroupDataDtoList.add(workerGroupDataDto); + } + } + //同步直属主管、班组 + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + //检验直属主管不能是自己 + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtoList) { + // 获取调动后的直属主管是否是被办理用户 + if (StringUtils.isNotEmpty(workerGroupDataDto.getReportsTo())) { + // 是否环形依赖 + ActionResult> usersBoundAfterDirectSupervisor = v2UserApi.getUserPrimaryBoundBatch(List.of(workerGroupDataDto.getReportsTo()), null); + Boolean b = v2UserApi.willFormCycle(req.getUserId(), workerGroupDataDto.getReportsTo()); + if (b && Objects.nonNull(usersBoundAfterDirectSupervisor.getData())) { + UserBoundVO data = usersBoundAfterDirectSupervisor.getData().get(0); + String errorMsg = "被办理人是" + data.getUserName() + "的直属主管,不能互为主管!"; + throw new RuntimeException(errorMsg); + } + } + if (StringUtils.isNotEmpty(workerGroupDataDto.getReportsTo()) && req.getUserId().equals(workerGroupDataDto.getReportsTo())) { + throw new RuntimeException("直属主管不能是自己"); + } + staffRosterService.checkReportsTo(workerGroupDataDto.getReportsTo()); + } + personnelOrgUtils.sysUserBoundList(workerGroupDataDtoList, rosterEntity.getUserId()); + } + } + + String inputHeadLogo = getInputHeadLogo(headLogo); + if (StringUtils.isNotEmpty(inputHeadLogo)) { + personnelOrgUtils.sysUserHeadLog(rosterEntity.getUserId(), inputHeadLogo); + } + //同步姓名 花名册等信息到组织架构 + Boolean isUpdateTenant = false; + SaveUserManagerAddDTO dto = new SaveUserManagerAddDTO(); + dto.setUserId(rosterEntity.getUserId()); + if (StringUtils.isNotEmpty(workerName)) { + dto.setRealName(workerName); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(workerSex)) { + dto.setGender(Integer.valueOf(workerSex)); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(inputHeadLogo)) { + dto.setHeadIcon(inputHeadLogo); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(flowerName)) { + dto.setNickName(flowerName); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(workerStatus)) { + dto.setJobStatus(personnelOrgUtils.convertAccountStatus(workerStatus)); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(actualProbationaryDate)) { + dto.setBecomeDate(DateUtil.parse(actualProbationaryDate, "yyyy-MM-dd")); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(actualStartDate)) { + dto.setEntryDate(DateUtil.parse(actualStartDate, "yyyy-MM-dd")); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(birthday)) { + dto.setBirthday(DateUtil.parse(birthday, "yyyy-MM-dd")); + isUpdateTenant = true; + } + if (StringUtils.isNotEmpty(idCardNum)) { + dto.setCertificatesNumber(idCardNum); + dto.setCertificatesType("a745d425adbb4321880817661cae8910"); + isUpdateTenant = true; + } + + if (isUpdateTenant) { + personnelOrgUtils.sysTenantAccountBase(dto); + } + // 最后入库操作 + //写入数据库 + if (CollectionUtil.isNotEmpty(formDataList)) { + String tenantId = UserProvider.getUser().getTenantId(); + threadPoolExecutor.execute(()-> { + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException(e); + } + List updateFormData = formDataList.stream().filter(item -> StringUtils.isNotEmpty(item.getId())).collect(Collectors.toList()); + this.updateBatchById(updateFormData); + List insertFormData = formDataList.stream().filter(item -> StringUtils.isEmpty(item.getId())).collect(Collectors.toList()); + this.saveBatch(insertFormData); + }); + } + //同步数据到入职登记表中 并且 入职登记表已经提交 + updateEntity.setId(rosterEntity.getId()); + staffRosterService.updateById(updateEntity); + // 健康证同步 + certificateInstanceService.saveHealthCertificate(req.getUserId(),healthCertificate,startHealthDate,endHealthDate); + } + + /** + * 检测出生日期格式 + * + * @param birthday + */ + private void checkBirthdayFormat(String birthday) { + if (StringUtils.isNotEmpty(birthday)) { + try { + DateUtil.parse(birthday, "yyyy-MM-dd"); + } catch (Exception e) { + throw new RuntimeException("出生日期格式不正确。正确格式如:1999-12-03"); + } + } + } + + + /** + * 查询所有字段 + * + * @return + */ + @Override + public List queryAllFields() { + //查询字段类型 + List formTypeList = queryFormTypeList(); + if (CollectionUtil.isEmpty(formTypeList)) { + return new ArrayList<>(); + } + List formTypeDtoList = BeanUtil.copyToList(formTypeList, ExportFormTypeDto.class); + //查询字段 + List formFeildList = registrationFormFieldService.list(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0) //1无效 0有效 + .eq(FtbPersonnelsRegistrationFormField::getStatus, 0) //0、启用 1、禁用 + .notIn(FtbPersonnelsRegistrationFormField::getType, 5, 6, 9) + .orderByAsc(FtbPersonnelsRegistrationFormField::getSorts)); + + if (CollectionUtil.isEmpty(formFeildList)) { + return formTypeDtoList; + } + List fieldList = BeanUtil.copyToList(formFeildList, ExportFormFieldDto.class); + //按照 formTypeId 分组 + Map> formTypeFieldMap = fieldList.stream().collect(Collectors.groupingBy(ExportFormFieldDto::getFormTypeId)); + List retList = new ArrayList<>(); + for (ExportFormTypeDto exportFormTypeDto : formTypeDtoList) { + List exportFormFieldDtoList = formTypeFieldMap.get(exportFormTypeDto.getId()); + if (CollectionUtil.isNotEmpty(exportFormFieldDtoList)) { + exportFormTypeDto.setFieldDtoList(exportFormFieldDtoList); + retList.add(exportFormTypeDto); + } + } + return retList; + } + + @Override + public void addRosterIdToFormData(String phone, String rosterId) { + //同步入职登记表已经提交的员工信息 + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper() + .set(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId) + .eq(FtbPersonnelsStaffRegistrationFormData::getPhone, phone); + baseMapper.update(new FtbPersonnelsStaffRegistrationFormData(), wrapper); + } + + @Override + public List queryHasHealthExpire() { + return baseMapper.queryHasHealthExpire(); + } + + @Override + public void deleteByRosterId(String rosterId) { + if (StringUtils.isEmpty(rosterId)) { + return; + } + QueryWrapper applay = new QueryWrapper<>(); + applay.lambda().eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId); + baseMapper.delete(applay); + } + + + @Override + public void importUpdateData(Map dataMap, String phone, String rosterId, Map formFieldMap, Map formFieldValueMap) { + //查询字段已经有的值 + //组装数据 + List formDataList = new ArrayList<>(); + dataMap.forEach((k, v) -> { + if (null == v) { + return; + } + FtbPersonnelsRegistrationFormField field = formFieldMap.get(k); + if (null == field) { + dataMap.put(k, ""); + return; + } + + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + FtbPersonnelsStaffRegistrationFormData fieldValue = formFieldValueMap.get(k); + if (k.equals("workAddress")) { + if (null != fieldValue && StringUtils.isNotEmpty(fieldValue.getValue())) { + if (fieldValue.getValue().startsWith("{")) { + WorkAddressDto workAddressDto = JSONUtil.toBean(fieldValue.getValue(), WorkAddressDto.class); + if (StringUtils.isNotEmpty(workAddressDto.getAddress())) { + dataMap.put(k, ""); + return; + } + } + } + } else if (null != fieldValue && StringUtils.isNotEmpty(fieldValue.getValue())) { + //dataMap.put(k, ""); + return; + } + if (null != fieldValue) { + formData.setId(fieldValue.getId()); + } + formData.setRosterId(rosterId); + formData.setPhone(phone); + formData.setFormFieldId(field.getId()); + formData.setFormTypeId(field.getFormTypeId()); + formData.setValue(v.toString()); + formDataList.add(formData); + + }); + + //写入数据库 + if (CollectionUtil.isNotEmpty(formDataList)) { + asyncBatchUpdateFormData(formDataList); + } + } + + @Override + public void batchUpdateData(List formDataList) { + saveOrUpdateBatch(formDataList); + } + + + @Deprecated(since = "代码重构后此代码废弃") + private void asyncBatchUpdateFormData(List formDataList) { + String tenantId = personnelOrgUtils.getTenantId(); + Map header = personnelOrgUtils.getHeadersForLogin(); + // personnelAsyncServiceUtils.asyncBatchUpdateFormData(formDataList, tenantId, header); + } + + /** + * 触发入职日期选择框时,判断是否具备修改条件;当有以下限制时,不允许修改,且页面提示:当前员工已有考勤排班记录或薪资档案,不能修改。 + * (一)当前已有该员工的薪资档案,有则不能修改 + * (二)当前员工已有考勤数据,有则不能修改 + * 判断所选日期是否在转正/调岗/晋升日期以前,如果不是则不允许提交,提示:入职日期不能晚于转正/调岗/晋升日期,需判断所选日期是否在今天之后。 + * 如果是则不允许提交,提示:入职日期不能晚于今天 + */ + private void doJoiningDateVerification(String joiningDate, String userId) { + if (StringUtils.isEmpty(joiningDate)) { + throw new RuntimeException("实际入职日期不能为空"); + } + Date actualStartDate = DateUtil.parse(joiningDate, "yyyy-MM-dd").toJdkDate(); + Date now = new Date(); + if (actualStartDate.after(now)) { + throw new RuntimeException("入职日期不能晚于今天"); + } + // 考勤校验 + ActionResult userIsScheduling = attendanceDailyRuleApi.userIsScheduling(UserProvider.getUser().getTenantId(), userId); + if (userIsScheduling.getCode() == 200 && userIsScheduling.getData()) { + throw new RuntimeException("当前员工已有考勤排班记录,不允许修改实际入职日期"); + } + // 薪资校验 + Boolean salaryHistory = querySalaryApi.haveSalaryHistory(userId, UserProvider.getUser().getTenantId()); + if (Objects.nonNull(salaryHistory) && salaryHistory) { + throw new RuntimeException("当前员工已有薪资记录,不允许修改实际入职日期"); + } + // 转正日期判断 + Date userSRegularizationDate = baseMapper.queryTheUserSRegularizationDate(userId); + if (Objects.nonNull(userSRegularizationDate) && actualStartDate.after(userSRegularizationDate)) { + throw new RuntimeException("入职日期不能晚于转正日期"); + } + // 调岗日期判断 + Date queryUserTransferDate = baseMapper.queryUserTransferDate(userId); + if (Objects.nonNull(queryUserTransferDate) && actualStartDate.after(queryUserTransferDate)) { + throw new RuntimeException("入职日期不能晚于调岗日期"); + } + // 晋升日期判断 + Date queryUserPromotionDate = baseMapper.queryUserPromotionDate(userId); + if (Objects.nonNull(queryUserPromotionDate) && actualStartDate.after(queryUserPromotionDate)) { + throw new RuntimeException("入职日期不能晚于晋升日期"); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterImportServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterImportServiceImpl.java new file mode 100644 index 0000000..702bb78 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterImportServiceImpl.java @@ -0,0 +1,491 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelReader; +import com.alibaba.excel.read.metadata.ReadSheet; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.bo.FtbRosterImportRedisBO; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormType; +import jnpf.model.personnels.vo.employeetype.FtbPersonnelsEmployeeTypeVO; +import jnpf.model.personnels.vo.roster.*; +import jnpf.personnels.listeners.DynamicHeadAnalysisEventListener; +import jnpf.personnels.listeners.DynamicHeadAnalysisNewEventListener; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormTypeMapper; +import jnpf.personnels.service.FtbPersonnelsEmployeeTypeService; +import jnpf.personnels.service.FtbPersonnelsRosterValidService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterImportService; +import jnpf.personnels.utils.CacheExcelUtils; +import jnpf.personnels.utils.StaffRosterImportSaveUtils; +import jnpf.util.RedisUtil; +import jnpf.util.UserProvider; +import jnpf.util.excel.EasyExcelUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsStaffRosterImportServiceImpl implements FtbPersonnelsStaffRosterImportService { + + @Resource + private RedisUtil redisUtil; + + @Resource + private FtbPersonnelsRegistrationFormTypeMapper ftbPersonnelsRegistrationFormTypeMapper; + + @Resource + private FtbPersonnelsRegistrationFormFieldMapper ftbPersonnelsRegistrationFormFieldMapper; + + @Resource + private FtbPersonnelsEmployeeTypeService employeeTypeService; + + @Resource(name = "FtbPersonnelsRosterValidServiceOld") + private FtbPersonnelsRosterValidService ftbPersonnelsRosterValidService; + + @Resource(name = "FtbPersonnelsRosterValidServiceNew") + private FtbPersonnelsRosterValidService ftbPersonnelsRosterValidServiceNew; + + @Autowired + private CacheExcelUtils cacheExcelUtils; + @Resource + private StaffRosterImportSaveUtils staffRosterImportSaveUtils; + + + @Override + public FtbRosterImportVO rosterImport(InputStream inputStream) { + String loginUserId = UserProvider.getLoginUserId(); + String redisKey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_SHEET_REDIS_VARIABLES, loginUserId); + redisUtil.remove(redisKey); + long totalStart = System.currentTimeMillis(); + try (ExcelReader excelReader = EasyExcel + .read(inputStream, new DynamicHeadAnalysisEventListener(ftbPersonnelsRosterValidService)) + .build()) { + long excelInitEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) ExcelReader初始化耗时:{}ms", excelInitEnd - totalStart); + cacheExcelUtils.init(); + // 获取所有sheet列表,跳过第一个sheet + List sheets = excelReader.excelExecutor().sheetList() + .stream() + .filter(sheet -> sheet.getSheetNo() > 0 && !"导入须知".equals(sheet.getSheetName())) + .collect(Collectors.toList()); + long sheetsPreparedEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) 准备sheet列表耗时:{}ms", sheetsPreparedEnd - excelInitEnd); + // 校验sheet是否匹配 + if (!verifySheetMatches(sheets)) { + throw new RuntimeException("excel导入模板有变更,请重新下载最新模板导入。"); + } + long excelReadStart = System.currentTimeMillis(); + for (ReadSheet readSheet : sheets) { + excelReader.read(readSheet); + } + long excelReadEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) 读取所有sheet耗时:{}ms", excelReadEnd - excelReadStart); + String idStr = IdWorker.getIdStr(); + FtbRosterImportVO ftbRosterImportVO = new FtbRosterImportVO(); + ftbRosterImportVO.setUniqueId(idStr); + String rediskey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_SHEET_REDIS_VARIABLES, UserProvider.getLoginUserId()); + for (ReadSheet readSheet : sheets) { + String sheetName = readSheet.getSheetName(); + String hashValues = redisUtil.getHashValues(rediskey, sheetName); + FtbRosterImportRedisBO ftbRosterImportRedisBO = JSON.parseObject(hashValues, FtbRosterImportRedisBO.class); + ftbRosterImportVO.getNormalRosterCategoryVOS().add(ftbRosterImportRedisBO.getNormalData()); + ftbRosterImportVO.getErrorRosterCategoryVOS().add(ftbRosterImportRedisBO.getErrorData()); + } + long redisDataAssembleEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) 从Redis组装sheet数据耗时:{}ms", redisDataAssembleEnd - excelReadEnd); + long errorOfAbnormalDataItems = ftbRosterImportVO.getErrorRosterCategoryVOS() + .stream().mapToLong(ftbRosterCategoryVO -> ftbRosterCategoryVO.getFtbRosterPageVOS().size()).sum(); + long dataEncapsulationStart = System.currentTimeMillis(); + List data = rosterDataEncapsulation(ftbRosterImportVO.getNormalRosterCategoryVOS(), + ftbRosterImportVO.getErrorRosterCategoryVOS()); + long dataEncapsulationEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) 正常数据封装(rosterDataEncapsulation)耗时:{}ms", dataEncapsulationEnd - dataEncapsulationStart); + ftbRosterImportVO.setNormalNumberOfDataItems((long) data.size()); + ftbRosterImportVO.setErrorOfAbnormalDataItems(errorOfAbnormalDataItems); + ftbRosterImportVO.setData(data); + redisUtil.remove(rediskey); + long redisWriteStart = System.currentTimeMillis(); + redisUtil.insert(String.format(FtbRosterImportConstants.ROSTER_IMPORT_REDIS_VARIABLES, idStr) + , ftbRosterImportVO, 30 * 60); + long redisWriteEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) 结果写入Redis耗时:{}ms", redisWriteEnd - redisWriteStart); + // 防止大数据导入时正常数据将nginx缓存打满及skywalking减少采集数据量 + ftbRosterImportVO.getNormalRosterCategoryVOS().clear(); + ftbRosterImportVO.getData().clear(); + long totalEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImport) 总体耗时:{}ms", totalEnd - totalStart); + return ftbRosterImportVO; + } finally { + cacheExcelUtils.clear(); + } + } + @Override + public void exceptionDataExport(String id) throws IOException { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + assert servletRequestAttributes != null; + HttpServletResponse httpServletResponse = servletRequestAttributes.getResponse(); + String redisKey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_REDIS_VARIABLES, id); + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + FtbRosterImportVO ftbRosterImportVO = JSON.parseObject(redisUtil.getString(redisKey).toString(), FtbRosterImportVO.class); + List errorRosterCategoryVOS = ftbRosterImportVO.getErrorRosterCategoryVOS(); + List ftbRosterImportTemplateBOS = errorRosterCategoryVOS.stream() + .sorted(Comparator.comparing(FtbRosterCategoryVO::getSheetIndex)) + .map(ftbRosterCategoryVO -> { + FtbRosterImportTemplateBO ftbRosterImportTemplateBO = new FtbRosterImportTemplateBO(); + ftbRosterImportTemplateBO.setSheetName(ftbRosterCategoryVO.getClassificationName()); + ftbRosterImportTemplateBO.setHeader(ftbRosterCategoryVO.convertHead()); + ftbRosterImportTemplateBO.setData(ftbRosterCategoryVO.convertData()); + return ftbRosterImportTemplateBO; + }).collect(Collectors.toList()); + EasyExcelUtils.dynamicHeaderGeneration(httpServletResponse, "花名册错误数据", "classpath:roster/roster.xlsx" + , ftbRosterImportTemplateBOS, getEmployeeTypeName()); + } + + @Override + @GlobalTransactional(rollbackFor = {Throwable.class,Exception.class},timeoutMills = 300000) + public void normalDataImport(String id) { + String redisKey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_REDIS_VARIABLES, id); + if (!redisUtil.exists(redisKey)) { + throw new RuntimeException("导入数据已过期,请重新导入!"); + } + long redisReadStart = System.currentTimeMillis(); + FtbRosterImportVO ftbRosterImportVO = JSON.parseObject(redisUtil.getString(redisKey).toString(), FtbRosterImportVO.class); + long redisReadEnd = System.currentTimeMillis(); + log.error("花名册导入(normalDataImport) 从Redis读取并反序列化数据耗时:{}ms", redisReadEnd - redisReadStart); + List data = ftbRosterImportVO.getData(); + staffRosterImportSaveUtils.addImportRosters(data); + } + + @Override + public List formFieldsConfig(Integer type) { + List ftbPersonnelsRegistrationFormTypes; + if (type == 0) { + ftbPersonnelsRegistrationFormTypes = ftbPersonnelsRegistrationFormTypeMapper.selectList(FtbRosterImportConstants.getRegistrationFormTypeQueryWrapperExport()); + } else { + ftbPersonnelsRegistrationFormTypes = ftbPersonnelsRegistrationFormTypeMapper.selectList(FtbRosterImportConstants.getRegistrationFormTypeQueryWrapper()); + } + return ftbPersonnelsRegistrationFormTypes.stream().map(a -> { + FtbRosterImportFormFieldsConfigVO ftbRosterImportFormFieldsConfigVO = new FtbRosterImportFormFieldsConfigVO(); + ftbRosterImportFormFieldsConfigVO.setCategoryName(a.getName()); + ftbRosterImportFormFieldsConfigVO.setCategoryId(a.getId()); + // 登记表属性 + List registrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList( + FtbRosterImportConstants.getRegistrationFormFieldQueryWrapper(a.getId())); + if (CollUtil.isNotEmpty(registrationFormFields)) { + // 导出附件、图片都要 + if (type == 0) { + FtbRosterImportConstants.nameAndMobileNumberAddedNewExport(registrationFormFields, a.getId()); + } else { + // 导入额外项排除 + FtbRosterImportConstants.nameAndMobileNumberAddedNew(registrationFormFields, a.getId()); + } + ftbRosterImportFormFieldsConfigVO.setOptionNames(registrationFormFields.stream().map(b -> { + FtbRosterImportFormFieldsConfigVO.OptionName opName = new FtbRosterImportFormFieldsConfigVO.OptionName(); + opName.setOptionId(b.getId()); + opName.setOptionName(b.getName()); + opName.setIsRequired(b.getIsNeedFill() == 1); + return opName; + }).collect(Collectors.toList())); + } + return ftbRosterImportFormFieldsConfigVO; + }).collect(Collectors.toList()); + } + + @Override + public void rosterImportTemplateDownloadNew(List formFields) throws IOException { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + assert servletRequestAttributes != null; + HttpServletResponse httpServletResponse = servletRequestAttributes.getResponse(); + List ftbRosterImportTemplateBOS = new ArrayList<>(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, formFields); + queryWrapper.orderByAsc(FtbPersonnelsRegistrationFormField::getSorts); + // 登记表属性 + FtbRosterImportTemplateBO ftbRosterImportTemplateBO = new FtbRosterImportTemplateBO(); + ftbRosterImportTemplateBO.setSheetName("sheet1"); + List registrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList(queryWrapper); + // 入职直属主管处理 + formFields.stream().filter("currReportsToId"::equals).findFirst() + .ifPresent(a -> registrationFormFields.addAll(FtbRosterImportConstants.ROSTER_IMPORT_JOB_INFORMATION_EXTRA_FIELDS)); + // 姓名和手机号排序靠前用于后续校验 + Predicate workerNamePredicate = a -> "workerName".equals(a.getId()); + FtbPersonnelsRegistrationFormField workerNameRegistrationFormField = registrationFormFields.stream() + .filter(workerNamePredicate) + .findFirst() + .get(); + registrationFormFields.removeIf(workerNamePredicate); + registrationFormFields.add(0, workerNameRegistrationFormField); + Predicate phonePredicate = a -> "phone".equals(a.getId()); + FtbPersonnelsRegistrationFormField phoneRegistrationFormField = registrationFormFields.stream() + .filter(phonePredicate) + .findFirst() + .get(); + registrationFormFields.removeIf(phonePredicate); + registrationFormFields.add(1, phoneRegistrationFormField); + List> headers = registrationFormFields.stream().map(personnelsRegistrationFormField -> { + List list = new ArrayList<>(); + if (personnelsRegistrationFormField.getIsNeedFill() == 1) { + list.add("*" + personnelsRegistrationFormField.getName()); + } else { + list.add(personnelsRegistrationFormField.getName()); + } + return list; + }).collect(Collectors.toList()); + ftbRosterImportTemplateBO.setHeader(headers); + ftbRosterImportTemplateBOS.add(ftbRosterImportTemplateBO); + EasyExcelUtils.dynamicHeaderGeneration(httpServletResponse, "花名册模板", "classpath:roster/roster.xlsx" + , ftbRosterImportTemplateBOS, getEmployeeTypeName()); + } + + @Override + public FtbRosterImportVO rosterImportNew(InputStream inputStream) { + String loginUserId = UserProvider.getLoginUserId(); + String redisKey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_SHEET_REDIS_VARIABLES, loginUserId); + redisUtil.remove(redisKey); + long totalStart = System.currentTimeMillis(); + try (ExcelReader excelReader = EasyExcel + .read(inputStream, new DynamicHeadAnalysisNewEventListener(ftbPersonnelsRosterValidServiceNew)) + .build()) { + long excelInitEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) ExcelReader初始化耗时:{}ms", excelInitEnd - totalStart); + cacheExcelUtils.init(); + // 获取所有sheet列表,跳过第一个sheet + List sheets = excelReader.excelExecutor().sheetList() + .stream() + .filter(sheet -> !"导入须知".equals(sheet.getSheetName())) + .collect(Collectors.toList()); + long sheetsPreparedEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) 准备sheet列表耗时:{}ms", sheetsPreparedEnd - excelInitEnd); + long excelReadStart = System.currentTimeMillis(); + for (ReadSheet readSheet : sheets) { + excelReader.read(readSheet); + } + long excelReadEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) 读取所有sheet耗时:{}ms", excelReadEnd - excelReadStart); + String idStr = IdWorker.getIdStr(); + FtbRosterImportVO ftbRosterImportVO = new FtbRosterImportVO(); + ftbRosterImportVO.setUniqueId(idStr); + String rediskey = String.format(FtbRosterImportConstants.ROSTER_IMPORT_SHEET_REDIS_VARIABLES, UserProvider.getLoginUserId()); + for (ReadSheet readSheet : sheets) { + String sheetName = readSheet.getSheetName(); + String hashValues = redisUtil.getHashValues(rediskey, sheetName); + FtbRosterImportRedisBO ftbRosterImportRedisBO = JSON.parseObject(hashValues, FtbRosterImportRedisBO.class); + ftbRosterImportVO.getNormalRosterCategoryVOS().add(ftbRosterImportRedisBO.getNormalData()); + Map sortMap = ftbRosterImportRedisBO.getErrorData().getHead().entrySet() + .stream() + .sorted(Comparator.comparingInt(a -> Integer.parseInt(a.getKey()))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (oldValue, newValue) -> oldValue, + LinkedHashMap::new + )); + ftbRosterImportRedisBO.getErrorData().setHead(sortMap); + ftbRosterImportVO.getErrorRosterCategoryVOS().add(ftbRosterImportRedisBO.getErrorData()); + } + long redisDataAssembleEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) 从Redis组装sheet数据耗时:{}ms", redisDataAssembleEnd - excelReadEnd); + long errorOfAbnormalDataItems = ftbRosterImportVO.getErrorRosterCategoryVOS() + .stream().mapToLong(ftbRosterCategoryVO -> ftbRosterCategoryVO.getFtbRosterPageVOS().size()).sum(); + long dataEncapsulationStart = System.currentTimeMillis(); + List data = rosterDataEncapsulationNew(ftbRosterImportVO.getNormalRosterCategoryVOS()); + long dataEncapsulationEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) 正常数据封装(rosterDataEncapsulationNew)耗时:{}ms", dataEncapsulationEnd - dataEncapsulationStart); + ftbRosterImportVO.setNormalNumberOfDataItems((long) data.size()); + ftbRosterImportVO.setErrorOfAbnormalDataItems(errorOfAbnormalDataItems); + ftbRosterImportVO.setData(data); + redisUtil.remove(rediskey); + long redisWriteStart = System.currentTimeMillis(); + redisUtil.insert(String.format(FtbRosterImportConstants.ROSTER_IMPORT_REDIS_VARIABLES, idStr) + , ftbRosterImportVO, 30 * 60); + long redisWriteEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) 结果写入Redis耗时:{}ms", redisWriteEnd - redisWriteStart); + // 防止大数据导入时正常数据将nginx缓存打满及skywalking减少采集数据量 + ftbRosterImportVO.getNormalRosterCategoryVOS().clear(); + ftbRosterImportVO.getData().clear(); + long totalEnd = System.currentTimeMillis(); + log.error("花名册导入(rosterImportNew) 总体耗时:{}ms", totalEnd - totalStart); + return ftbRosterImportVO; + } finally { + cacheExcelUtils.clear(); + } + } + + private List rosterDataEncapsulation(List normalRosterCategoryVOS + , List errorRosterCategoryVOS) { + List result = new ArrayList<>(); + // 错误数据手机号 + List errorPhones = errorRosterCategoryVOS.stream().filter(a -> !CollectionUtils.isEmpty(a.getFtbRosterPageVOS())) + .flatMap(ftbRosterCategoryVO -> ftbRosterCategoryVO.getFtbRosterPageVOS().stream()) + .filter(b -> !CollectionUtils.isEmpty(b.getFtbRosterAttributesVOS())) + .flatMap(ftbRosterPageVO -> ftbRosterPageVO.getFtbRosterAttributesVOS().stream()) + .filter(FtbRosterImportConstants.phonePredicate) + .map(FtbRosterAttributesVO::getAttributeValue) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(normalRosterCategoryVOS)) { + FtbRosterCategoryVO ftbRosterCategoryVO = normalRosterCategoryVOS + .stream() + .max(Comparator.comparing(FtbRosterCategoryVO::getSorted)).get(); + List rosterCategoryVOS = normalRosterCategoryVOS.stream().filter(a -> !a.getSheetIndex().equals(ftbRosterCategoryVO.getSheetIndex())) + .collect(Collectors.toList()); + // 手机号最多的那条 + retry: + for (FtbRosterPageVO ftbRosterPageVO : ftbRosterCategoryVO.getFtbRosterPageVOS()) { + FtbRosterAttributesVO phoneRosterAttributesVO = ftbRosterPageVO.getFtbRosterAttributesVOS().stream().filter(FtbRosterImportConstants.phonePredicate) + .findFirst().get(); + FtbRosterAttributesVO userNameRosterAttributesVO = ftbRosterPageVO.getFtbRosterAttributesVOS().stream().filter(FtbRosterImportConstants.userNamePredicate) + .findFirst().get(); + // 只要手机号在错误数据中存在,则移除属于正常数据,由于跨sheet匹配原因 + if (errorPhones.contains(phoneRosterAttributesVO.getAttributeValue())) { + continue; + } + FtbRosterInsertNomalVO ftbRosterInsertNomalVO = new FtbRosterInsertNomalVO(); + // 手机号 + ftbRosterInsertNomalVO.setPhone(phoneRosterAttributesVO.getAttributeValue()); + ftbRosterInsertNomalVO.setUserName(userNameRosterAttributesVO.getAttributeValue()); + List ftbRosterInsertAttributesVOS = new ArrayList<>(); + for (FtbRosterCategoryVO rosterCategoryVO : rosterCategoryVOS) { + // 根据手机号匹配行标,通过行标去匹配数据,手机号没有,则跳出 + FtbRosterAttributesVO ftbRosterAttributesVO = rosterCategoryVO.getFtbRosterPageVOS().stream() + .flatMap(a -> a.getFtbRosterAttributesVOS().stream()) + .filter(b -> { + boolean equals = false; + if (StringUtils.isNotEmpty(b.getAttributeValue())) { //添加非空判断 + equals = b.getAttributeValue().equals(phoneRosterAttributesVO.getAttributeValue()); + } + return equals; + } + ).findFirst().orElse(null); + if (ftbRosterAttributesVO != null) { + FtbRosterPageVO rosterPageVO = rosterCategoryVO.getFtbRosterPageVOS() + .stream() + .filter(a -> a.getRowMark().equals(ftbRosterAttributesVO.getLineMark())) + .findFirst().get(); + dataCleaning(rosterPageVO, ftbRosterInsertNomalVO, ftbRosterInsertAttributesVOS); + } else { + continue retry; + } + } + dataCleaning(ftbRosterPageVO, ftbRosterInsertNomalVO, ftbRosterInsertAttributesVOS); + ftbRosterInsertNomalVO.setFtbRosterInsertAttributesVOS(ftbRosterInsertAttributesVOS); + result.add(ftbRosterInsertNomalVO); + } + } + return result; + } + + private static void dataCleaning(FtbRosterPageVO rosterPageVO, FtbRosterInsertNomalVO ftbRosterInsertNomalVO, List ftbRosterInsertAttributesVOS) { + rosterPageVO.getFtbRosterAttributesVOS().forEach(a -> { + FtbRosterInsertAttributesVO ftbRosterInsertAttributesVO = new FtbRosterInsertAttributesVO(); + // 转正薪酬和入职薪酬特殊化处理 + if ("入职薪酬".equals(a.getAttributeName())) { + ftbRosterInsertNomalVO.setEntrySalary(a.getAttributeValue()); + } + if ("转正薪酬".equals(a.getAttributeName())) { + ftbRosterInsertNomalVO.setRegularSalary(a.getAttributeValue()); + } + // 排除自定义属性 + if (StrUtil.isBlank(a.getFormFieldId()) && StrUtil.isBlank(a.getFormTypeId())) { + return; + } + if (StrUtil.isBlank(a.getAttributeValue())) { + return; + } + ftbRosterInsertAttributesVO.setAttributeName(a.getAttributeName()); + ftbRosterInsertAttributesVO.setHeaderName(a.getAttributeName()); + ftbRosterInsertAttributesVO.setFormTypeId(a.getFormTypeId()); + ftbRosterInsertAttributesVO.setFormFieldId(a.getFormFieldId()); + ftbRosterInsertAttributesVO.setAttributeValue(a.getAttributeValue()); + ftbRosterInsertAttributesVO.setOriginalValue(a.getAttributeValue()); + if (StrUtil.isNotBlank(a.getAttributeValueId())) { + ftbRosterInsertAttributesVO.setAttributeValue(a.getAttributeValueId()); + } + ftbRosterInsertAttributesVOS.add(ftbRosterInsertAttributesVO); + }); + } + + + private boolean verifySheetMatches(List sheets) { + List sheetNames = sheets.stream() + .map(ReadSheet::getSheetName).collect(Collectors.toList()); + // 登记类型配置 + List formTypeList = ftbPersonnelsRegistrationFormTypeMapper. + selectList(FtbRosterImportConstants.getRegistrationFormTypeQueryWrapper()); + // 标记 剔除没有数据的sheet + List filterFormTypeList = formTypeList.stream() + .filter(a -> { + List ftbPersonnelsRegistrationFormFields = + ftbPersonnelsRegistrationFormFieldMapper.selectList(FtbRosterImportConstants.getRegistrationFormFieldQueryWrapper(a.getId())); + if (CollUtil.isNotEmpty(ftbPersonnelsRegistrationFormFields)) { + return true; + } + return false; + }).collect(Collectors.toList()); + + // sheet有变更 + if (filterFormTypeList.size() != sheetNames.size()) { + return false; + } + for (FtbPersonnelsRegistrationFormType ftbPersonnelsRegistrationFormType : filterFormTypeList) { + if (!sheetNames.contains(ftbPersonnelsRegistrationFormType.getName())) { + return false; + } + } + return true; + } + + + private List rosterDataEncapsulationNew(List normalRosterCategoryVOS) { + List result = new ArrayList<>(); + if (CollUtil.isNotEmpty(normalRosterCategoryVOS)) { + for (FtbRosterPageVO ftbRosterPageVO : normalRosterCategoryVOS.get(0).getFtbRosterPageVOS()) { + FtbRosterAttributesVO phoneRosterAttributesVO = ftbRosterPageVO.getFtbRosterAttributesVOS().stream().filter(FtbRosterImportConstants.phonePredicate) + .findFirst().get(); + FtbRosterAttributesVO userNameRosterAttributesVO = ftbRosterPageVO.getFtbRosterAttributesVOS().stream().filter(FtbRosterImportConstants.userNamePredicate) + .findFirst().get(); + FtbRosterInsertNomalVO ftbRosterInsertNomalVO = new FtbRosterInsertNomalVO(); + // 手机号 + ftbRosterInsertNomalVO.setPhone(phoneRosterAttributesVO.getAttributeValue()); + ftbRosterInsertNomalVO.setUserName(userNameRosterAttributesVO.getAttributeValue()); + List ftbRosterInsertAttributesVOS = new ArrayList<>(); + dataCleaning(ftbRosterPageVO, ftbRosterInsertNomalVO, ftbRosterInsertAttributesVOS); + ftbRosterInsertNomalVO.setFtbRosterInsertAttributesVOS(ftbRosterInsertAttributesVOS); + result.add(ftbRosterInsertNomalVO); + } + } + return result; + } + + private String getEmployeeTypeName() { + List result = employeeTypeService.employeeList(); + return result.stream() + .map(FtbPersonnelsEmployeeTypeVO::getName) + .collect(Collectors.joining(",")); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterSchemeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterSchemeServiceImpl.java new file mode 100644 index 0000000..f54c3af --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterSchemeServiceImpl.java @@ -0,0 +1,97 @@ +package jnpf.personnels.service.impl; + +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.personnels.dto.scheme.FtbPersonAddSchemeDTO; +import jnpf.model.personnels.dto.scheme.FtbPersonEditSchemeDTO; +import jnpf.model.personnels.po.FtbPersonnelsStaffRosterScheme; +import jnpf.model.personnels.vo.scheme.FtbPersonSchemeDetailsVO; +import jnpf.model.personnels.vo.scheme.FtbPersonSchemeListVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterSchemeMapper; +import jnpf.personnels.service.FtbPersonnelsStaffRosterSchemeService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsStaffRosterSchemeServiceImpl implements FtbPersonnelsStaffRosterSchemeService { + + @Resource + private FtbPersonnelsStaffRosterSchemeMapper ftbPersonnelsStaffRosterSchemeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void addScheme(FtbPersonAddSchemeDTO schemeDTO) { + // 方案上限十个,每个方案字数上限10字 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRosterScheme::getEnableMark, 0); + if (ftbPersonnelsStaffRosterSchemeMapper.selectCount(queryWrapper) > 10) { + throw new RuntimeException("方案数量已达上限10个"); + } + // 方案名称不能重复 + queryWrapper.eq(FtbPersonnelsStaffRosterScheme::getSchemeName, schemeDTO.getSchemeName()); + if (ftbPersonnelsStaffRosterSchemeMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("方案名称不能重复"); + } + FtbPersonnelsStaffRosterScheme ftbPersonnelsStaffRosterScheme = schemeDTO.convert(schemeDTO); + ftbPersonnelsStaffRosterSchemeMapper.insert(ftbPersonnelsStaffRosterScheme); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteScheme(String id) { + LambdaUpdateWrapper deleteWrapper = Wrappers.lambdaUpdate(); + deleteWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + deleteWrapper.set(FtbPersonnelsStaffRosterScheme::getEnableMark,1); + ftbPersonnelsStaffRosterSchemeMapper.update(new FtbPersonnelsStaffRosterScheme(), deleteWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateScheme(FtbPersonEditSchemeDTO schemeDTO) { + // 方案名称不能重复 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRosterScheme::getSchemeName, schemeDTO.getSchemeName()); + queryWrapper.ne(SuperBaseEntity.SuperIBaseEntity::getId, schemeDTO.getSchemeId()); + queryWrapper.eq(FtbPersonnelsStaffRosterScheme::getEnableMark, 0); + if (ftbPersonnelsStaffRosterSchemeMapper.selectCount(queryWrapper) > 0) { + throw new RuntimeException("方案名称不能重复"); + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, schemeDTO.getSchemeId()); + updateWrapper.set(FtbPersonnelsStaffRosterScheme::getSchemeName, schemeDTO.getSchemeName()); + updateWrapper.set(FtbPersonnelsStaffRosterScheme::getFormFields, JSON.toJSONString(schemeDTO.getFormFields())); + ftbPersonnelsStaffRosterSchemeMapper.update(new FtbPersonnelsStaffRosterScheme(), updateWrapper); + } + + @Override + public FtbPersonSchemeDetailsVO detailScheme(String id) { + FtbPersonnelsStaffRosterScheme ftbPersonnelsStaffRosterScheme = ftbPersonnelsStaffRosterSchemeMapper.selectById(id); + FtbPersonSchemeDetailsVO ftbPersonSchemeDetailsVO = new FtbPersonSchemeDetailsVO(); + ftbPersonSchemeDetailsVO.setSchemeId(ftbPersonnelsStaffRosterScheme.getId()); + ftbPersonSchemeDetailsVO.setSchemeName(ftbPersonnelsStaffRosterScheme.getSchemeName()); + ftbPersonSchemeDetailsVO.setFormFields(JSON.parseArray(ftbPersonnelsStaffRosterScheme.getFormFields(), String.class)); + return ftbPersonSchemeDetailsVO; + } + + @Override + public List getSchemeList() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffRosterScheme::getEnableMark, 0); + queryWrapper.orderByDesc(FtbPersonnelsStaffRosterScheme::getCreatorTime); + return ftbPersonnelsStaffRosterSchemeMapper.selectList(queryWrapper).stream().map(item -> { + FtbPersonSchemeListVO ftbPersonSchemeListVO = new FtbPersonSchemeListVO(); + ftbPersonSchemeListVO.setSchemeId(item.getId()); + ftbPersonSchemeListVO.setSchemeName(item.getSchemeName()); + return ftbPersonSchemeListVO; + }).collect(Collectors.toList()); + + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterServiceImpl.java new file mode 100644 index 0000000..1cb6403 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffRosterServiceImpl.java @@ -0,0 +1,4713 @@ +package jnpf.personnels.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fantaibao.permission.handling.PermissionHandling; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.ImmutableMap; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.account.PTenantAccountApi; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.attendance.service.AttendanceUserFaceService; +import jnpf.authority.service.FtbPermissionRoleAuthorizePersonService; +import jnpf.authority.utils.PermissionsApplicableEnums; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.cultivate.service.FtbCultivatePromotionNewService; +import jnpf.engine.FlowTaskApi; +import jnpf.enums.personnel.SalaryApplyTypeEnum; +import jnpf.file.FileApi; +import jnpf.model.attendance.dto.UserFaceDto; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineFileDTO; +import jnpf.model.cultivate.resp.CanDeleteMsg; +import jnpf.model.cultivate.vo.promotion.FtbCultivatePromotionWithPersonelVO; +import jnpf.model.enums.*; +import jnpf.model.im.UserAndLinkDTO; +import jnpf.model.personnels.bo.ExportRosterOneVo; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.roster.FtbjobTrialRejectedDTO; +import jnpf.model.personnels.dto.roster.QueryCompanyAgeDto; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.staff.field.ExportFormTypeDto; +import jnpf.model.personnels.dto.staff.field.FormFieldDto; +import jnpf.model.personnels.dto.staff.field.FormTypeDto; +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.registerform.GroupFieldDataDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.registerform.WorkAddressDto; +import jnpf.model.personnels.dto.staff.roster.*; +import jnpf.model.personnels.dto.staff.salarylog.AddSalaryChangeLogDto; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.req.roster.*; +import jnpf.model.personnels.vo.roster.*; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.model.store.vo.StoreUserRelationVo; +import jnpf.model.user.GenerateHeadFileForm; +import jnpf.permission.*; +import jnpf.permission.dto.QueryUserListDTO; +import jnpf.permission.dto.SaveUserManagerAddDTO; +import jnpf.permission.dto.SynUserBoundRoleDTO; +import jnpf.permission.dto.UpdateUserManagerBoundDTO; +import jnpf.permission.dto.relation.BaseUserPrimaryPositionDTO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.dto.v2.user.QueryUserBatchDTO; +import jnpf.permission.dto.v2.user.UpdateUserDTO; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.model.user.*; +import jnpf.permission.vo.contract.ContractTypeVO; +import jnpf.permission.vo.user.UserBasicWithMoreBoundVO; +import jnpf.permission.vo.user.UserBoundPhoneVO; +import jnpf.permission.vo.user.UserListMatchVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.permission.vo.v2.user.V2UserListMatchVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffArchivesHistoryMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.mapper.FtbPersonnelsTurnoverManagementMapper; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.*; +import jnpf.util.Constants; +import jnpf.util.JsonUtil; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.compress.utils.Lists; +import org.apache.commons.lang3.StringUtils; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsStaffRosterServiceImpl extends ServiceImpl implements FtbPersonnelsStaffRosterService { + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private PositionApi positionApi; + + @Autowired + private UserApi userApi; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + private FtbPermissionRoleAuthorizePersonService ftbPermissionRoleAuthorizePersonService; + + @Autowired + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Autowired + private FtbPersonnelsStaffSalaryChangeLogService salaryChangeLogService; + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private PTenantAccountApi pTenantAccountApi; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionService fieldOptionService; + + @Autowired + private FtbPersonnelsAuditRunTaskService ftbPersonnelsAuditRunTaskService; + + @Autowired + private FtbPersonnelsRegularManagementService regularManagementService; + + @Autowired + private FtbPersonnelsPermissionsService personnelsPermissionsService; + @Autowired + private PersonnelPerUtils personnelPerUtils; + + @Autowired + private FtbPersonnelsSalaryService personnelSalaryService; + + private static final String DEFAULT_USER_LOGO = "001.png"; + private static final String ACCOUNT_TEMP_PRE = "tmp"; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + FtbPersonnelsTurnoverManagementMapper turnoverManagementMapper; + + @Autowired + FtbCultivatePromotionNewService promotionService; + + @Autowired + private PersonnelAsyncOldData personnelAsyncOldData; + + + @Autowired + private PersonnelPreTrailIMUtils personnelPreTrailIMUtils; + + @Resource + private AttendanceUserFaceService attendanceUserFaceService; + @Resource + private FileApi fileApi; + @Resource + private AttendanceGroupService attendanceGroupService; + + @Autowired + private PersonnelAsyncServiceUtils personnelAsyncServiceUtils; + + @Autowired + private FtbPersonnelsStaffArchivesHistoryService ftbPersonnelsStaffArchivesHistoryService; + + @Autowired + FtbPersonnelsSalaryService personnelSalaryServiceImpl; + @Autowired + private FtbPersonnelsTurnoverManagementService turnoverManagementService; + + @Autowired + private PersonnelAsyncImportUtils personnelAsyncImportUtils; + @Autowired + private FtbPersonnelsRegistrationFormFieldService registrationFormFieldService; + + @Autowired + private FlowTaskApi flowTaskApi; + + @Autowired + FtbPersonnelsStaffArchivesHistoryMapper staffArchivesHistoryMapper; + + @Autowired + private Executor threadPoolExecutor; + + @Autowired + FtbPersonnelsStaffRegistrationFormDataMapper dataMapper; + + @Autowired + UserPrimaryPositionApi userPrimaryPositionApi; + + @Autowired + private PermissionsUtils permissionsUtils; + + @Autowired + private RosterExportUtils rosterExportUtils; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + private PermissionHandling permissionHandling; + + @Value("${invite-url}") + private String joinUrl; + + @Autowired + RedissonClient redissonClient; + + @Autowired + FtbPersonnelsSecondmentManagementService secondmentManagementService; + + @Resource + private CertificateInstanceService certificateInstanceService; + + + private final static List mainTableField = Arrays.asList("id", "userId", "systemWokerId", "systemContractId", "name", "phone", "workerNo", "entrySalary", + "regularSalary", "workerType", "joinNum", "actualProbationaryDate", "actualStartDate", "isTrialJob", "trialJobDay", "isSignSeparation", "workerStatus"); + + private final static List extTableField = Arrays.asList("userRoleList", "orgList", "currReportsTo", "currReportsToName", "attendanceGroup", "attendanceGroupName", "currSalary"); + + @Override + public PageInfo getPageList(StaffRosterListReq req, Boolean isExport) { + if (StringUtils.isNotEmpty(req.getKeyWord())) { + req.setKeyWord(StringUtils.trim(req.getKeyWord())); + } + if (StringUtils.isNotEmpty(req.getWorkerType())) { + String[] split = req.getWorkerType().split(","); + req.setWorkerTypeList(Arrays.asList(split)); + } + //查询用户数据权限 + List userList = new ArrayList<>(); + boolean isAdministrator = false; + if (!"0".equals(req.getIsQueryAuth())) { + List authUserList = queryCacheAuthUserList(PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), null); + // 非超级管理员但是无数据 + if (authUserList != null && authUserList.isEmpty()) { + return new PageInfo<>(new ArrayList<>()); + } + if (CollectionUtil.isNotEmpty(authUserList)) userList.addAll(authUserList); + isAdministrator = authUserList == null; + } + String currOrg = req.getCurrOrg(); + if (StringUtils.isNotEmpty(currOrg)) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setOrganizeIds(List.of(currOrg.split(","))); + dto.setIsPage(false); + dto.setPositionId(req.getCurrPosition()); + dto.setGradeId(req.getCurrRank()); + ActionResult> listByLevel = v2UserApi.pagePost(dto); + List userSubQueryList = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + if (CollectionUtil.isEmpty(userList)) { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userSubQueryList; + } else { + return new PageInfo<>(new ArrayList<>()); + } + } else { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userList.stream() + .filter(userSubQueryList::contains) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(userList)) { + return new PageInfo<>(new ArrayList<>()); + } + } else { + return new PageInfo<>(new ArrayList<>()); + } + } + } + // 健康证状态筛选 + if (req.getHealthCertificateStatus() != null) { + List healthCertificateUserIds = certificateInstanceService.getHealthCertificateUserIdsByStatus(req.getHealthCertificateStatus()); + if (CollUtil.isNotEmpty(healthCertificateUserIds)) { + // 超管,状态无人 + if (UserProvider.getUser().getIsAdministrator()) { + userList.addAll(healthCertificateUserIds); + } else { + userList = userList.stream() + .filter(healthCertificateUserIds::contains) + .collect(Collectors.toList()); + } + if (CollectionUtil.isEmpty(userList)) { + return new PageInfo<>(new ArrayList<>()); + } + } else { + return new PageInfo<>(new ArrayList<>()); + } + } + // 补充生日筛选区间 + String startBirthdayTime = req.getStartBirthdayTime(); + String endBirthdayTime = req.getEndBirthdayTime(); + if (startBirthdayTime != null && endBirthdayTime != null) { + // 只去月份+日期 + List phoneNumber = baseMapper.queryBirthday(startBirthdayTime, endBirthdayTime); + if (CollUtil.isNotEmpty(phoneNumber)) { + req.setPhoneNumberList(phoneNumber); + } else { + return new PageInfo<>(new ArrayList<>()); + } + } + // 添加自定义查询用户 + if (CollUtil.isNotEmpty(req.getUserIds())) { + List list = req.getUserIds(); + userList.addAll(list); + } + Page queryPage = baseMapper.pagingQuery( + Page.of(req.getCurrentPage(), req.getPageSize()), + req, userList); + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records) && !isExport) { + fillContractTypeName(records);//补充合同类型名称 + fillAllUserOrgInfo(records, false); + fillFormDataRoster(records, false); + List userIds = records.stream().map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + List healthCertificateDetails = certificateInstanceService.getHealthCertificateDetails(userIds); + Map healthCertificateDetailMap = healthCertificateDetails.stream() + .collect(Collectors.toMap(HealthCertificateDetailVO::getUserId, v -> v)); + records.forEach(item -> { + // + if ("304".equals(item.getWorkerStatus())) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getUserId, item.getUserId()); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getEnableMark, 0); + lambdaQuery.last("limit 1"); + FtbPersonnelsTurnoverManagement management = turnoverManagementMapper.selectOne(lambdaQuery); + if (management != null && management.getTurnoverStatus() == 6) { + item.setTurnoverStatusFlag("1"); + } else { + item.setTurnoverStatusFlag(""); + } + } + // 健康证状态回填 + HealthCertificateDetailVO healthCertificateDetail = healthCertificateDetailMap.get(item.getUserId()); + if (healthCertificateDetail != null) { + item.setHealthCertificateStatus(healthCertificateDetail.getStatus()); + } + }); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + @Override + public PageInfo postWithSalary(StaffRosterListReq req) { + Integer pageSize = req.getPageSize(); + Integer currentPage = req.getCurrentPage(); + Page page = new Page(); + page.setSize(pageSize); + page.setCurrent(currentPage); + //薪酬的筛选单独处理 + //查询用户数据权限 + List userList = new ArrayList<>(); + String isQueryAuth = req.getIsQueryAuth(); + if (CollUtil.isNotEmpty(req.getUserIds())) { + userList = req.getUserIds(); + } + if (!"0".equals(isQueryAuth)){ + if (!UserProvider.getUser().getIsAdministrator()) { + List authUserList = queryCacheAuthUserList(req.getModuleCode(), req.getCategory()); + if (CollectionUtil.isNotEmpty(authUserList)) { + List finalUserList = userList; + // 筛选出当前用户权限下的用户 + userList = authUserList.stream().filter(v -> { + if (CollUtil.isEmpty(finalUserList)) return true; + if (finalUserList.contains(v)) { + return true; + } else { + return false; + } + }).collect(Collectors.toList()); + if (CollUtil.isEmpty(userList)) { + return new PageInfo<>(new ArrayList<>()); + } + } + } + String targetOrganizeId = req.getTargetOrganizeId(); + String targetPositionId = req.getTargetPositionId(); + if (StringUtils.isNotEmpty(targetOrganizeId)) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setOrganizeIds(List.of(targetOrganizeId.split(","))); + dto.setIsPage(false); + dto.setPositionId(targetPositionId); + ActionResult> listByLevel = v2UserApi.pagePost(dto); + List userSubQueryList = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + Collection intersection = CollectionUtil.intersection(userSubQueryList, userList); + if (CollectionUtil.isEmpty(userList)) { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userSubQueryList; + } else { + return new PageInfo<>(new ArrayList<>()); + } + } else { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userList.stream() + .filter(userSubQueryList::contains) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(userList)) { + return new PageInfo<>(new ArrayList<>()); + } + } else { + return new PageInfo<>(new ArrayList<>()); + } + } + } + List orgIds = null; + List turUserIds = null; + List filteOrgIds = StringUtils.isNotEmpty(targetOrganizeId) ? Arrays.asList(targetOrganizeId.split(",")) : null; + if (!UserProvider.getUser().getIsAdministrator() ) { + // 数据权限userIds + orgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(UserProvider.getUser().getUserId(),req.getModuleCode(), req.getCategory()); + PermissionsApplicableObject permissionsApplicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(UserProvider.getUser().getUserId()); + // 数据权限 仅下属单独处理 + if (permissionsApplicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE){ + // 查询当前登录人的下属 + turUserIds = inquireAboutSubordinates(UserProvider.getUser().getUserId()); + } else { + if ( orgIds != null ) { + turUserIds = doEmployeesOfYourOrganizationTur(orgIds, targetPositionId); + } + } + } + if (filteOrgIds != null || StrUtil.isNotBlank(targetPositionId)){ + if (CollUtil.isNotEmpty(filteOrgIds) && CollUtil.isNotEmpty(orgIds)) { + assert orgIds != null; + orgIds = orgIds.stream().filter(filteOrgIds::contains).collect(Collectors.toList()); + }else if (CollUtil.isNotEmpty(filteOrgIds) && orgIds == null) { + orgIds =filteOrgIds; + } + turUserIds = doEmployeesOfYourOrganizationTur(orgIds,targetPositionId); + } + if (CollUtil.isNotEmpty(turUserIds)) { + if (turUserIds.get(0).equals("-1")) { + //组织/岗位筛选无数据 + userList = turUserIds; + }else { + if (!UserProvider.getUser().getIsAdministrator() && (StrUtil.isNotBlank(targetPositionId) || StrUtil.isNotBlank(targetOrganizeId))) { + Collection intersection = CollectionUtil.intersection(userList, turUserIds); + // 离职人员 + Collection subtract = CollectionUtil.subtract(turUserIds, userList); + // 合并交集与差集 + List mergedList = new ArrayList<>(intersection); + mergedList.addAll(subtract); + userList = mergedList; + if (CollectionUtil.isEmpty(userList)) { + userList.add("-1"); + } + }else { + userList.addAll(turUserIds); + } + } + } + } + Page list = baseMapper.pagingQueryNewWithTurnoverManagerList(req, userList, page); + List records = list.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + //补充组织 岗位 职等 信息 列表不在查询入职的组织岗位 职等信息 + fillAllUserOrgInfo(records, true); + fillFormDataRoster(records, true); + Map managementMap = new HashMap<>(); + List userIds = records + .stream() + .filter(item -> "305".equals(item.getWorkerStatus())) + .map(FtbPersonnelsStaffRosterDto::getUserId) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPersonnelsTurnoverManagement::getUserId, userIds); + List managements = turnoverManagementMapper.selectList(lambdaed); + managementMap = managements.stream().collect(Collectors.toMap(FtbPersonnelsTurnoverManagement::getUserId, Function.identity(), (v1, v2) -> v1)); + } + Map finalManagementMap = managementMap; + records.forEach(item -> { + if ("305".equals(item.getWorkerStatus()) && finalManagementMap.containsKey(item.getUserId())) { + FtbPersonnelsTurnoverManagement management = finalManagementMap.get(item.getUserId()); + String organizationInfo = management.getOrganizationInfo(); + List dataDos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + item.setOrgList(dataDos); + } + }); + } + + //构建分页返回数据 + return CultivatePage.coverPageInfo(page); + } + @Override + public List queryWithUserIdsPost(StaffRosterReq req) { + List userSubQueryList = new ArrayList<>(); + if (CollUtil.isNotEmpty(req.getUserIds())) { + userSubQueryList.addAll(req.getUserIds()); + } + + if (CollUtil.isNotEmpty(req.getOrgIds())) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setOrganizeIds(req.getOrgIds()); + dto.setIsPage(false); + ActionResult> listByLevel = v2UserApi.pagePost(dto); + List list = new ArrayList<>(); + List userIds = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + List userIdsTur = doEmployeesOfYourOrganizationTur(req.getOrgIds(), null); + if (CollUtil.isNotEmpty(userIdsTur)){ + list.addAll(userIdsTur); + } + if (CollUtil.isNotEmpty(userIds)){ + list.addAll(userIds); + } + // 判断组织中是否包含查询人员 + if (CollUtil.isNotEmpty(list) && CollUtil.isNotEmpty(userSubQueryList)) { + userSubQueryList = list.stream().filter(userSubQueryList::contains).collect(Collectors.toList()); + }else { + userSubQueryList = list; + } + } + if (CollUtil.isEmpty(userSubQueryList)) return new ArrayList<>(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(CollUtil.isNotEmpty(userSubQueryList),FtbPersonnelsStaffRoster::getUserId, userSubQueryList.stream().distinct().collect(Collectors.toList())); + wrapper.like(StringUtils.isNotBlank(req.getKeyWords()) ,FtbPersonnelsStaffRoster::getName, req.getKeyWords()); + List personnelsStaffRosters = baseMapper.selectList(wrapper); + if (CollUtil.isEmpty(personnelsStaffRosters)) return new ArrayList<>(); + List staffRosterDtos = personnelsStaffRosters.stream().map(FtbPersonnelsStaffRosterDto::changeEscape).collect(Collectors.toList()); + fillAllUserOrgInfo(staffRosterDtos, true); + //fillFormDataRoster(staffRosterDtos, true); + Map managementMap = new HashMap<>(); + List userIds = staffRosterDtos + .stream() + .filter(item -> "305".equals(item.getWorkerStatus())) + .map(FtbPersonnelsStaffRosterDto::getUserId) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPersonnelsTurnoverManagement::getUserId, userIds); + List managements = turnoverManagementMapper.selectList(lambdaed); + managementMap = managements.stream().collect(Collectors.toMap(FtbPersonnelsTurnoverManagement::getUserId, Function.identity(), (v1, v2) -> v1)); + } + Map finalManagementMap = managementMap; + staffRosterDtos.forEach(item -> { + if ("305".equals(item.getWorkerStatus()) && finalManagementMap.containsKey(item.getUserId())) { + FtbPersonnelsTurnoverManagement management = finalManagementMap.get(item.getUserId()); + String organizationInfo = management.getOrganizationInfo(); + List dataDos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + item.setOrgList(dataDos); + } + }); + return staffRosterDtos; + } + + @Override + public List postWithSalaryNoPage(StaffRosterListReq req) { + //薪酬的筛选单独处理 + //查询用户数据权限 + List userList = new ArrayList<>(); + String isQueryAuth = req.getIsQueryAuth(); + if (CollUtil.isNotEmpty(req.getUserIds())) { + userList = req.getUserIds(); + } + if (!"0".equals(isQueryAuth)) { + if (!UserProvider.getUser().getIsAdministrator()) { + List authUserList = queryCacheAuthUserList(req.getModuleCode(), req.getCategory()); + if (CollectionUtil.isNotEmpty(authUserList)) { + List finalUserList = userList; + // 筛选出当前用户权限下的用户 + userList = authUserList.stream().filter(v -> { + if (CollUtil.isEmpty(finalUserList)) return true; + if (finalUserList.contains(v)) { + return true; + } else { + return false; + } + }).collect(Collectors.toList()); + if (CollUtil.isEmpty(userList)) { + return new ArrayList<>(); + } + } + } + String targetOrganizeId = req.getTargetOrganizeId(); + String targetPositionId = req.getTargetPositionId(); + if (StringUtils.isNotEmpty(targetOrganizeId)) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setOrganizeIds(List.of(targetOrganizeId.split(","))); + dto.setIsPage(false); + dto.setPositionId(targetPositionId); + ActionResult> listByLevel = v2UserApi.pagePost(dto); + List userSubQueryList = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + if (CollectionUtil.isEmpty(userList)) { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userSubQueryList; + } else { + return new ArrayList<>(); + } + } else { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userList.stream() + .filter(userSubQueryList::contains) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(userList)) { + return new ArrayList<>(); + } + } else { + return new ArrayList<>(); + } + } + } + List orgIds = null; + List turUserIds = null; + List filteOrgIds = StringUtils.isNotEmpty(targetOrganizeId) ? Arrays.asList(targetOrganizeId.split(",")) : null; + if (!UserProvider.getUser().getIsAdministrator()) { + // 数据权限userIds + orgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(UserProvider.getUser().getUserId(), req.getModuleCode(), req.getCategory()); + PermissionsApplicableObject permissionsApplicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(UserProvider.getUser().getUserId()); + // 数据权限 仅下属单独处理 + if (permissionsApplicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE) { + // 查询当前登录人的下属 + turUserIds = inquireAboutSubordinates(UserProvider.getUser().getUserId()); + } else { + if (orgIds != null) { + turUserIds = doEmployeesOfYourOrganizationTur(orgIds, targetPositionId); + } + } + } + if (filteOrgIds != null || StrUtil.isNotBlank(targetPositionId)) { + if (CollUtil.isNotEmpty(filteOrgIds) && CollUtil.isNotEmpty(orgIds)) { + assert orgIds != null; + orgIds = orgIds.stream().filter(filteOrgIds::contains).collect(Collectors.toList()); + } else if (CollUtil.isNotEmpty(filteOrgIds) && orgIds == null) { + orgIds = filteOrgIds; + } + turUserIds = doEmployeesOfYourOrganizationTur(orgIds, targetPositionId); + } + if (CollUtil.isNotEmpty(turUserIds)) { + if ("-1".equals(turUserIds.get(0))) { + //组织/岗位筛选无数据 + userList = turUserIds; + } else { + if (!UserProvider.getUser().getIsAdministrator() && (StrUtil.isNotBlank(targetPositionId) || StrUtil.isNotBlank(targetOrganizeId))) { + Collection intersection = CollectionUtil.intersection(userList, turUserIds); + // 离职人员 + Collection subtract = CollectionUtil.subtract(turUserIds, userList); + // 合并交集与差集 + List mergedList = new ArrayList<>(intersection); + mergedList.addAll(subtract); + userList = mergedList; + if (CollectionUtil.isEmpty(userList)) { + userList.add("-1"); + } + } else { + userList.addAll(turUserIds); + } + } + } + } + List records = baseMapper.queryNewWithTurnoverManagerList(req, userList); + if (CollectionUtil.isNotEmpty(records)) { + //补充组织 岗位 职等 信息 列表不在查询入职的组织岗位 职等信息 + fillAllUserOrgInfo(records, true); + fillFormDataRoster(records, true); + Map managementMap = new HashMap<>(); + List userIds = records + .stream() + .filter(item -> "305".equals(item.getWorkerStatus())) + .map(FtbPersonnelsStaffRosterDto::getUserId) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPersonnelsTurnoverManagement::getUserId, userIds); + List managements = turnoverManagementMapper.selectList(lambdaed); + managementMap = managements + .stream() + .collect(Collectors.toMap(FtbPersonnelsTurnoverManagement::getUserId, Function.identity(), (v1, v2) -> v1)); + } + Map finalManagementMap = managementMap; + records.forEach(item -> { + if ("305".equals(item.getWorkerStatus()) && finalManagementMap.containsKey(item.getUserId())) { + FtbPersonnelsTurnoverManagement management = finalManagementMap.get(item.getUserId()); + String organizationInfo = management.getOrganizationInfo(); + List dataDos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + item.setOrgList(dataDos); + } + }); + } + return records; + } + + /** + * 获取花名册用户-无权限 + * @param req + * @return + */ + @Override + public List getPersonnelUserList(StaffRosterListReq req) { + if (ObjectUtil.isNull(req.getCurrentPage())) { + req.setCurrentPage(1); + req.setPageSize(1000); + } + Page page = new Page(); + page.setSize(req.getPageSize()); + page.setCurrent(req.getCurrentPage()); + Page ftbPersonnelsStaffRosterDtoPage = baseMapper.pagingQueryNewWithTurnoverManagerList(req, req.getUserIds(), page); + return ftbPersonnelsStaffRosterDtoPage.getRecords(); + } + /** + * 查询下属的离职id + * @param userId + * @return + */ + public List inquireAboutSubordinates(String userId){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark ,0); + List managements = turnoverManagementMapper.selectList(wrapper); + return managements.stream().filter(v -> { + String organizationInfo = v.getOrganizationInfo(); + List dataDto = JSONArray.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isEmpty(dataDto)) { + return false; + } + long count = dataDto.stream().filter(data -> data.getIsDefault() && userId.equals(data.getReportsTo())).count(); + return count > 0; + }).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + } + + private List doEmployeesOfYourOrganizationTur(List orgIds, String targetPositionId) { + + List personnelsStaffRosterUserIdList = new ArrayList<>(); + if (StrUtil.isNotBlank(targetPositionId)) { + QueryUserBatchDTO queryUserBatchDTO = new QueryUserBatchDTO(); + queryUserBatchDTO.setTenantId(UserProvider.getUser().getTenantId()); + queryUserBatchDTO.setPositionIds(List.of(targetPositionId)); + ActionResult> userInfoBatch = v2UserApi.getUserInfoBatch(queryUserBatchDTO); + List userBoundVOList = userInfoBatch.getData(); + personnelsStaffRosterUserIdList = userBoundVOList.stream().map(UserBoundVO::getId).collect(Collectors.toList()); + if (CollectionUtil.isEmpty(personnelsStaffRosterUserIdList)) { + personnelsStaffRosterUserIdList.add("-1"); + } + } + if (CollectionUtil.isNotEmpty(orgIds)) { + ActionResult> listActionResult = v2UserApi.listTargetOrganizes(orgIds, UserProvider.getUser().getTenantId(),null); + List userIds = listActionResult.getData().stream().map(UserPageListVO::getId).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(personnelsStaffRosterUserIdList)) { + //取交集 + Collection intersection = CollectionUtil.intersection(personnelsStaffRosterUserIdList, userIds); + personnelsStaffRosterUserIdList = new ArrayList<>(intersection); + }else { + personnelsStaffRosterUserIdList = userIds; + } + } + if (CollUtil.isNotEmpty(orgIds) || StrUtil.isNotBlank(targetPositionId)){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark ,0); + List managements = turnoverManagementMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(orgIds) && StringUtils.isEmpty(targetPositionId)) { + List managerUserIds = managements.stream().filter(vo -> com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty(JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()).stream().filter(orgIds::contains).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + managerUserIds.addAll(personnelsStaffRosterUserIdList); + return managerUserIds; + } + if (CollUtil.isNotEmpty(orgIds) && StringUtils.isNotEmpty(targetPositionId)) { + + List finalPostIds = List.of(targetPositionId); + List orgAndPostIdFilterUserIdList = managements.stream().filter(vo -> com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty( + JSONObject.parseArray( + vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().filter(po -> orgIds.contains(po.getAffiliatedOrg()) && finalPostIds.contains(po.getAffiliatedPosition())).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + orgAndPostIdFilterUserIdList.addAll(personnelsStaffRosterUserIdList); + return CollectionUtil.isEmpty(orgAndPostIdFilterUserIdList) ? List.of("-1") : orgAndPostIdFilterUserIdList; + } + if (StrUtil.isNotBlank(targetPositionId) && CollectionUtil.isEmpty(orgIds)) { + List finalPostIds = List.of(targetPositionId); + List filterPostUserIdList = managements.stream().filter(vo -> com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty( + JSONObject.parseArray( + vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().filter(po -> finalPostIds.contains(po.getAffiliatedPosition())).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getUserId).collect(Collectors.toList()); + filterPostUserIdList.addAll(personnelsStaffRosterUserIdList); + return CollectionUtil.isEmpty(filterPostUserIdList) ? List.of("-1") : filterPostUserIdList; + } + } + return null; + } + + /** + * 匹配出, user中存在的用户(包含已离职的用户),和已经不存在的用户, + * + * @param userIds 目标匹配用户集合 + * @return 匹配结果, 两个list + */ + @Override + public UserListMatchVO getUserListByMatch(List userIds) { + //先得到离职的用户 + List personnelStaffRosters = this.list(Wrappers.lambdaQuery() + .eq(FtbPersonnelsStaffRoster::getWorkerStatus, StaffWorkerStatus.RESIGNED.getCode()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + ); + //有离职的用户则筛选出离职的人 + List existingUserList = new ArrayList<>(); + //匹配到离职用户的离职的信息 + if (CollUtil.isNotEmpty(personnelStaffRosters)) { + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPersonnelsTurnoverManagement::getUserId, personnelStaffRosters.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList())); + lambdaed.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition, StaffWorkerStatus.RESIGNED.getCode()); + List managements = turnoverManagementMapper.selectList(lambdaed); + managements.forEach(management -> { + UserBasicWithMoreBoundVO boundVO = new UserBasicWithMoreBoundVO(); + boundVO.setId(management.getUserId()); + boundVO.setRealName(management.getUserName()); + boundVO.setMobilePhone(management.getPhoneNumber()); + + List moreInfoVOS = new ArrayList<>(); + String organizationInfo = management.getOrganizationInfo(); + List dataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(dataDtos)) { + dataDtos.forEach(dataDto -> { + UserBoundMoreInfoVO moreInfoVO = new UserBoundMoreInfoVO(); + moreInfoVO.setOrganizeId(dataDto.getAffiliatedOrg()); + moreInfoVO.setOrganizeEnCode(dataDto.getOrgEncode()); + moreInfoVO.setOrganizeName(dataDto.getAffiliatedOrgName()); + moreInfoVO.setPositionId(dataDto.getAffiliatedPosition()); + moreInfoVO.setPositionEnCode(dataDto.getPositionEncode()); + moreInfoVO.setPositionName(dataDto.getAffiliatedPositionName()); + moreInfoVO.setPositionGradesId(dataDto.getAffiliatedRank()); + moreInfoVO.setPositionGradesName(dataDto.getAffiliatedRankName()); + moreInfoVO.setManagerId(dataDto.getReportsTo()); + moreInfoVO.setManagerRealName(dataDto.getReportsToName()); + + moreInfoVOS.add(moreInfoVO); + }); + } + + boundVO.setMoreInfoVOS(moreInfoVOS); + existingUserList.add(boundVO); + } + ); + //remove more ids + userIds.removeAll(managements.stream().map(FtbPersonnelsTurnoverManagement::getUserId).distinct().collect(Collectors.toList())); + } + //再去匹配未离职用户 + V2UserListMatchVO v2UserListMatchVO = v2UserApi.getUserListByMatch(userIds).getData(); + UserListMatchVO userListMatchVO = new UserListMatchVO(); + userListMatchVO.setEliminatedUserIdList(v2UserListMatchVO.getEliminatedUserIdList()); + userListMatchVO.setExistingUserList(v2UserListMatchVO.getExistingUserList().stream().map(vo -> { + UserBasicWithMoreBoundVO boundVO = new UserBasicWithMoreBoundVO(); + boundVO.setId(vo.getId()); + boundVO.setRealName(vo.getUserName()); + boundVO.setMobilePhone(vo.getPhone()); + boundVO.setHeadIcon(vo.getHeadIcon()); + boundVO.setThisOrganizeId(vo.getOrganizeId()); + List moreInfoVOS = new ArrayList<>(); + UserBoundMoreInfoVO moreInfoVO = new UserBoundMoreInfoVO(); + moreInfoVO.setOrganizeId(vo.getOrganizeId()); + moreInfoVO.setOrganizeEnCode(vo.getOrganizeEnCode()); + moreInfoVO.setOrganizeName(vo.getOrganizeName()); + moreInfoVO.setPositionId(vo.getPositionId()); + moreInfoVO.setPositionEnCode(vo.getPositionEnCode()); + moreInfoVO.setPositionName(vo.getPositionName()); + moreInfoVO.setPositionGradesId(vo.getGradeId()); + moreInfoVO.setPositionGradesName(vo.getGradeName()); + moreInfoVOS.add(moreInfoVO); + boundVO.setMoreInfoVOS(moreInfoVOS); + return boundVO; + }).collect(Collectors.toList())); + if (CollUtil.isNotEmpty(existingUserList)) { + userListMatchVO.getExistingUserList().addAll(existingUserList); + } + + return userListMatchVO; + } + + /** + * 查询用户权限 + * + * @return + */ + private List queryCacheAuthUserList(String moduleCode, String category) { + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + return null; + } + if (StringUtils.isNotEmpty(moduleCode) && StringUtils.isNotEmpty(category)) { + return permissionsUtils.obtainPersonnelUserIdDataPermissions(userInfo.getUserId(), moduleCode, category); + } + return permissionHandling.getUserIdsByUserId(userInfo.getUserId()); + } + + /** + * 清除用户权限缓存 + */ + public void clearAuthUserList() { + UserInfo userInfo = UserProvider.getUser(); + String cacheKey = "dataPermissions:" + userInfo.getTenantId() + ":" + userInfo.getUserId(); + redisTemplate.delete(cacheKey); + } + + private Integer getRandom() { + Random random = new Random(); + return random.nextInt(60); + } + + /** + * 填充用户的组织岗位职等信息 + * + * @param records + * @param isSelectShop + */ + private void fillAllUserOrgInfo(List records, Boolean isSelectShop) { + if (CollectionUtil.isEmpty(records)) { + return; + } + List userIds = records.stream().map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + Map> map = personnelOrgUtils.getUserOrgBoundInfoForUserList(userIds, isSelectShop); + for (FtbPersonnelsStaffRosterDto record : records) { + List list = map.get(record.getUserId()); + record.setOrgList(list); + //兼容老数据 + dealCurrOrgInfo(list, record); + } + } + + /** + * @param records + * @param isNeedQueryRole 是否需要查询角色 true 需要,false不需要 + */ + public void fillFormDataRoster(List records, Boolean isNeedQueryRole) { + List rosterIds = records.stream().map(FtbPersonnelsStaffRosterDto::getId).collect(Collectors.toList()); + List allList = registrationFormDataService.queryFormFieldValueForRosterIds(rosterIds); + //按照rosterid分组 + Map> map = allList.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)); + for (FtbPersonnelsStaffRosterDto entity : records) { + List formDataList = map.get(entity.getId()); + personnelOrgUtils.fillRosterDtoData(entity, formDataList, isNeedQueryRole); + } + } + + private void dealCurrOrgInfo(List list, FtbPersonnelsStaffRosterDto record) { + if (CollectionUtil.isNotEmpty(list)) { + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); +// String token = userInfo.getToken(); +// Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + WorkerGroupDataDto workerGroupDataDto = list.get(0); + record.setCurrOrg(workerGroupDataDto.getAffiliatedOrg()); + record.setCurrOrgName(workerGroupDataDto.getAffiliatedOrgName()); + record.setCurrPosition(workerGroupDataDto.getAffiliatedPosition()); + record.setCurrPositionName(workerGroupDataDto.getAffiliatedPositionName()); + record.setCurrRank(workerGroupDataDto.getAffiliatedRank()); + record.setCurrRankName(workerGroupDataDto.getAffiliatedRankName()); + record.setCurrReportsTo(workerGroupDataDto.getReportsTo()); + record.setCurrReportsToName(workerGroupDataDto.getReportsToName()); + if (StringUtils.isEmpty(record.getCurrOrg()) || StringUtils.isEmpty(record.getCurrPosition())) { + //把第一个组织岗位职等作为入职的 兼容老数据 + personnelAsyncOldData.dealOldData(record, tenantId, null); + } + } + } + + /** + * app端查询花名册列表 + * + * @param req + * @return + */ + @Override + public PageInfo getAppPageList(AppStaffRosterListReq req) { + //查询用户数据权限 + List userList = queryCacheAuthUserList(null, "App"); + if (userList != null && userList.isEmpty()) { + return new PageInfo<>(new ArrayList<>()); + } + if (CollUtil.isNotEmpty(req.getOrganizeIds())) { + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setOrganizeIds(req.getOrganizeIds()); + dto.setIsPage(false); + ActionResult> listByLevel = v2UserApi.pagePost(dto); + List userSubQueryList = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + if (CollectionUtil.isEmpty(userList)) { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userSubQueryList; + } else { + return new PageInfo<>(new ArrayList<>()); + } + } else { + if (CollectionUtil.isNotEmpty(userSubQueryList)) { + userList = userList.stream() + .filter(userSubQueryList::contains) + .collect(Collectors.toList()); + if (CollectionUtil.isEmpty(userList)) { + return new PageInfo<>(new ArrayList<>()); + } + } else { + return new PageInfo<>(new ArrayList<>()); + } + } + } + // 补充生日筛选区间 + String startBirthdayTime = req.getStartBirthdayTime(); + String endBirthdayTime = req.getEndBirthdayTime(); + if (startBirthdayTime != null && endBirthdayTime != null) { + List phoneNumber = baseMapper.queryBirthday(startBirthdayTime, endBirthdayTime); + if (CollUtil.isNotEmpty(phoneNumber)) { + req.setPhoneNumberList(phoneNumber); + } else { + return new PageInfo<>(new ArrayList<>()); + } + } + //健康证状态 + Integer healthCertificateStatus = req.getHealthCertificateStatus(); + if(Objects.nonNull(healthCertificateStatus)){ + List userIdsByStatus = certificateInstanceService.getHealthCertificateUserIdsByStatus(healthCertificateStatus); + if(userList == null){ + userList = userIdsByStatus; + }else{ + Set userIdsByStatusSet = new HashSet<>(userIdsByStatus); + userList.removeIf(u-> !userIdsByStatusSet.contains(u)); + } + } + Page queryPage = baseMapper.pagingAppQuery( + Page.of(req.getCurrentPage(), req.getPageSize()), + req, userList); + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + //补充组织 岗位 职等 信息 推荐人、推荐门店 + //去掉入职的组织岗位职等信息的查询 因为列表不需要 +// personnelOrgUtils.fillCurrOrgName(records); +// personnelOrgUtils.fillCurrPositionName(records); +// personnelOrgUtils.fillCurrRankName(records); +// personnelOrgUtils.fillReportsToInfo(records); + fillContractTypeName(records);//补充合同类型名称 + fillAllUserOrgInfo(records, false); + } + List userIds = records.stream().map(FtbPersonnelsStaffRosterDto::getUserId).collect(Collectors.toList()); + List healthCertificateDetails = certificateInstanceService.getHealthCertificateDetails(userIds); + Map healthCertificateDetailMap = healthCertificateDetails.stream() + .collect(Collectors.toMap(HealthCertificateDetailVO::getUserId, v -> v)); + records.forEach(item -> { + if ("304".equals(item.getWorkerStatus())) { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getUserId, item.getUserId()); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getEnableMark, 0); + lambdaQuery.last("limit 1"); + FtbPersonnelsTurnoverManagement management = turnoverManagementMapper.selectOne(lambdaQuery); + if (management != null && management.getTurnoverStatus() == 6) { + item.setTurnoverStatusFlag("1"); + } else { + item.setTurnoverStatusFlag(""); + } + } + // 健康证回填 + HealthCertificateDetailVO healthCertificateDetail = healthCertificateDetailMap.get(item.getUserId()); + if (healthCertificateDetail != null) { + item.setHealthCertificateStatus(healthCertificateDetail.getStatus()); + item.setHealthCertificateId(healthCertificateDetail.getCertificateInstanceId()); + } + }); + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + /** + * 修改转正状态 记录薪酬 和 成长日志 + * + * @param userId + * @param dto + * @param money + * @param tenantId + */ + @Override + public void innerChangeRegular(String userId, StaffRegularDto dto, BigDecimal money, String tenantId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null == entity) { + throw new RuntimeException("花名册用户数据不存在"); + } + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setUserId(userId); + roster.setWorkerStatus("303"); + roster.setId(entity.getId()); + if (null == dto.getRegularDate()) { + roster.setActualProbationaryDate(new Date()); + } else { + roster.setActualProbationaryDate(dto.getRegularDate()); + } + roster.setCurrSalary(money); + roster.setRegularSalary(money); + baseMapper.updateById(roster); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "workerStatus", "303"); + //写入薪酬 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(entity.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(money); + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.BECOME_REGULAR_WORKER.getCode()); + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + //员工成长 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(entity.getUserId()); + addGrowthLogDto.setChangeDate(new Date()); + addGrowthLogDto.setGrowthType(GrowthLogEnum.BECOME_REGULAR_WORKER.getCode()); + addGrowthLogDto.setDetail(JSONUtil.toJsonStr(dto)); + addGrowthLogDto.setRemarks(dto.getRemarks()); + addGrowthLogDto.setEmployeeId(entity.getSystemWokerId()); + // 添加转正日期 + // 转正的 + growthLogService.addGrowthLog(addGrowthLogDto); + + List workerGroupDataDtoList = personnelOrgUtils.getUserOrgBoundInfo(userId,tenantId); + if (CollectionUtil.isEmpty(workerGroupDataDtoList)) { + throw new RuntimeException("用户没有组织岗位职等"); + } + // 2025-01-15转正要求屏蔽掉组织同步,不需要同步组织岗位职等bug编号4356 + //changeUserOrgInfo(workerGroupDataDtoList, userId, dto); + //调整实际转正日期 + registrationFormDataService.updateForOneField(roster.getId(), entity.getPhone(), "actualProbationaryDate", personnelOrgUtils.dateToString(roster.getActualProbationaryDate(), "")); + registrationFormDataService.updateForOneField(roster.getId(), entity.getPhone(), "confirmationRemarks", dto.getRemarks()); + //同步状态到内推池子 + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), "303"); + //同步转正日期 + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setWorkerStatus("303"); + userDTO.setTenantId(tenantId); + userDTO.setBecomeDate(dto.getRegularDate()); + v2UserApi.updateUserInfo(entity.getUserId(),userDTO); + } + + /** + * 调整用户的组织岗位职等 直属主管等信息 + * + * @param workerGroupDataDtoList + * @param userId + * @param dto + */ + private void changeUserOrgInfo(List workerGroupDataDtoList, String userId, StaffRegularDto dto) { + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtoList) { + if (workerGroupDataDto.getAffiliatedOrg().equals(dto.getNewOrg()) && + workerGroupDataDto.getAffiliatedPosition().equals(dto.getNewPosition()) && + workerGroupDataDto.getAffiliatedRank().equals(dto.getNewRank()) + ) { + if (StringUtils.isNotEmpty(workerGroupDataDto.getReportsTo()) && StringUtils.isNotEmpty(dto.getImmediateSuperId())) { + if (workerGroupDataDto.getReportsTo().equals(dto.getImmediateSuperId())) { + return; + } + } + } + } + WorkerGroupDataDto defaultWorker = null; + for (WorkerGroupDataDto workerGroupDataDto : workerGroupDataDtoList) { + if (workerGroupDataDto.getIsDefault()) { + defaultWorker = workerGroupDataDto; + break; + } + } + if (null == defaultWorker) { + defaultWorker = workerGroupDataDtoList.get(0); + } + + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + updateUserManagerBoundDTO.setUserId(userId); + updateUserManagerBoundDTO.setOldOrgId(defaultWorker.getAffiliatedOrg()); + updateUserManagerBoundDTO.setOldPositionId(defaultWorker.getAffiliatedPosition()); + updateUserManagerBoundDTO.setOldPositionGradesId(defaultWorker.getAffiliatedRank()); + updateUserManagerBoundDTO.setOldManagerId(defaultWorker.getReportsTo()); + updateUserManagerBoundDTO.setOrgId(dto.getNewOrg()); + updateUserManagerBoundDTO.setPositionId(dto.getNewPosition()); + updateUserManagerBoundDTO.setPositionGradesId(dto.getNewRank()); + updateUserManagerBoundDTO.setManagerId(dto.getImmediateSuperId()); + personnelOrgUtils.updateSysUserBound(updateUserManagerBoundDTO); + + } + + /** + * 内部试用试岗 直接转正 + * + * @param entity + * @param dto + */ + private void innerPreTrailChangeRegular(FtbPersonnelsStaffRoster entity, StaffRegularDto dto) { + + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setUserId(entity.getUserId()); + roster.setWorkerStatus(StaffWorkerStatus.FULL_TIME.getCode()); + roster.setId(entity.getId()); + roster.setActualProbationaryDate(entity.getActualStartDate()); + roster.setCurrSalary(entity.getEntrySalary()); + roster.setRegularSalary(entity.getEntrySalary()); + baseMapper.updateById(roster); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "workerStatus", StaffWorkerStatus.FULL_TIME.getCode()); + //写入薪酬 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(entity.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(entity.getEntrySalary()); + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.BECOME_REGULAR_WORKER.getCode()); + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + //员工成长 + //调整实际转正日期 + registrationFormDataService.updateForOneField(roster.getId(), entity.getPhone(), "actualProbationaryDate", personnelOrgUtils.dateToString(roster.getActualProbationaryDate(), "")); + registrationFormDataService.updateForOneField(roster.getId(), entity.getPhone(), "confirmationRemarks", dto.getRemarks()); + //同步状态到内推池子 + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), StaffWorkerStatus.FULL_TIME.getCode()); + //同步转正日期 + SaveUserManagerAddDTO saveUserManagerAddDTO = new SaveUserManagerAddDTO(); + saveUserManagerAddDTO.setUserId(roster.getUserId()); + saveUserManagerAddDTO.setBecomeDate(roster.getActualProbationaryDate()); + personnelOrgUtils.sysTenantAccountBase(saveUserManagerAddDTO); + } + + /** + * 删除用户重置花名册入职次数 + * + * @param userId 用户ID + * @return + */ + @Override + public void resetWorkerNumber(List userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsStaffRoster::getUserId, userId); + List list = baseMapper.selectList(queryWrapper); + List phones = list.stream().map(FtbPersonnelsStaffRoster::getPhone).collect(Collectors.toList()); + LambdaUpdateWrapper updateWraper = Wrappers.lambdaUpdate(); + updateWraper.in(FtbPersonnelsStaffRoster::getUserId, userId); + updateWraper.set(FtbPersonnelsStaffRoster::getJoinNum,0); + baseMapper.update(new FtbPersonnelsStaffRoster(), updateWraper); + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + deleteWrapper.in(FtbPersonnelsStaffRegistrationFormData::getPhone, phones); + registrationFormDataService.remove(deleteWrapper); + } + + /** + * 撤销转正状态 + * + * @param userId + */ + @Override + public void cancelRegular(String userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null != entity) { + LambdaUpdateWrapper updateWraper = new LambdaUpdateWrapper<>(); + updateWraper.set(FtbPersonnelsStaffRoster::getWorkerStatus, entity.getPreRegStatus()); + updateWraper.set(FtbPersonnelsStaffRoster::getActualProbationaryDate, null); + + entity.setRegularSalary(new BigDecimal(0)); + if (entity.getEntrySalary() == null) { + entity.setCurrSalary(entity.getEntrySalary()); + } else { + entity.setCurrSalary(entity.getEntrySalary()); + } + + updateWraper.eq(FtbPersonnelsStaffRoster::getId, entity.getId()); + this.update(updateWraper); + + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "workerStatus", entity.getPreRegStatus()); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "actualProbationaryDate", ""); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "confirmationRemarks", ""); + } + } + + /** + * 修改转正前的状态(内部调用) + * + * @param userId 用户ID + */ + @Override + public void recordPreRegularStatus(String userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null != entity) { + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setUserId(userId); + roster.setPreRegStatus(entity.getWorkerStatus()); + roster.setId(entity.getId()); + baseMapper.updateById(roster); + } + } + + + /** + * 撤销离职 + * + * @param userId + */ + @Override + public String canceldepart(String userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null == entity) { + throw new RuntimeException("花名册用户数据不存在"); + } + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setId(entity.getId()); + String departStatus = entity.getDepartStatus(); + roster.setWorkerStatus(departStatus); + roster.setIsSignSeparation(0); + baseMapper.updateById(roster); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "workerStatus", roster.getWorkerStatus()); + + return departStatus; + } + + /** + * 修改离职状态 + * + * @param userId + * @param listDto + * @param resignationDate + */ + @Override + public Boolean innerChangeDepart(String userId, StaffDepartDto listDto, String remarks, Date resignationDate, String tenantId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null != entity && !entity.getWorkerStatus().equals(StaffWorkerStatus.RESIGNED.getCode())) { + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setUserId(userId); + roster.setWorkerStatus(StaffWorkerStatus.RESIGNED.getCode()); + roster.setId(entity.getId()); + //记录离职时间 + roster.setDepartDate(resignationDate); + baseMapper.updateById(roster); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "workerStatus", StaffWorkerStatus.RESIGNED.getCode()); + + QueryWrapper removeWrapper = new QueryWrapper<>(); + removeWrapper.lambda().eq(FtbPersonnelsStaffEmploymentApply::getPhone, entity.getPhone()); + //同步状态到内推池子 + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), StaffWorkerStatus.RESIGNED.getCode()); + staffEmploymentApplyService.remove(removeWrapper); + //员工成长 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(entity.getUserId()); + addGrowthLogDto.setChangeDate(resignationDate); + addGrowthLogDto.setGrowthType(GrowthLogEnum.DEPART.getCode()); + addGrowthLogDto.setDetail(JSONUtil.toJsonStr(listDto)); + addGrowthLogDto.setRemarks(remarks); + addGrowthLogDto.setEmployeeId(entity.getSystemWokerId()); + // 离职的 + growthLogService.addGrowthLog(addGrowthLogDto); + + //同步离职日期 + SaveUserManagerAddDTO saveUserManagerAddDTO = new SaveUserManagerAddDTO(); + saveUserManagerAddDTO.setUserId(roster.getUserId()); + saveUserManagerAddDTO.setDepartDate(resignationDate); + personnelOrgUtils.sysTenantAccountBase(saveUserManagerAddDTO); + + //TODO 历史数据兼容问题重置 +// List list = registrationFormDataService.queryArchivalForm(userId, tenantId); +// list = removeOptionData(list); +// log.error("离职timing={}", userId); +// FtbPersonnelsStaffArchivesHistory history = new FtbPersonnelsStaffArchivesHistory(); +// history.setUserId(userId); +// history.setArchives(JSONUtil.toJsonStr(list)); +// history.setEnabledMark(1); +// history.setCreatorTime(new Date()); +// ftbPersonnelsStaffArchivesHistoryService.save(history); +// //离职清除老数据 +// QueryWrapper formDataUpdateWrapper = new QueryWrapper<>(); +// formDataUpdateWrapper.lambda().eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, entity.getId()); +// registrationFormDataService.remove(formDataUpdateWrapper); + + } + return true; + + } + + + /** + * 去除字段选项 + * @param list + * @return + */ + private List removeOptionData(List list) { + if(CollUtil.isEmpty(list)){ + return list; + } + for (FormTypeDto formTypeDto : list) { + List groupFieldDataDtoList = formTypeDto.getGroupFieldDataDtoList(); + if(CollUtil.isEmpty(groupFieldDataDtoList)){ + continue; + } + for (GroupFieldDataDto groupFieldDataDto : groupFieldDataDtoList){ + FormFieldDto formFieldDto = groupFieldDataDto.getFormFieldDto(); + if(formFieldDto==null){ + continue; + } + formFieldDto.setOptionDtoList(Collections.EMPTY_LIST); + } + } + return list; + } + + + /** + * 内部调用 用户晋升 + * + * @param userId 用户ID + * @param dto + * @param money 金额 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void innerPromotion(String userId, StaffPromotionDto dto, BigDecimal money) { + log.error("晋升={},userid={},money={}", userId, dto, money); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null == entity) { + throw new RuntimeException("花名册用户数据不存在"); + } + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setUserId(userId); + roster.setId(entity.getId()); + roster.setCurrSalary(money); + baseMapper.updateById(roster); + + + //写入薪酬 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(entity.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(money); + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.promotion.getCode()); + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + //员工成长 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(entity.getUserId()); + addGrowthLogDto.setChangeDate(new Date()); + addGrowthLogDto.setGrowthType(GrowthLogEnum.promotion.getCode()); + + addGrowthLogDto.setDetail(JSONUtil.toJsonStr(dto)); + addGrowthLogDto.setEmployeeId(entity.getSystemWokerId()); + // 晋升的 + growthLogService.addGrowthLog(addGrowthLogDto); + + } + + /** + * 内部调用 修改 待离职 状态 + * + * @param userId 用户ID + */ + @Override + public void innerChangeWaitDepart(String userId, String taskInfoId, Integer version) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null != entity) { + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setUserId(userId); + roster.setDepartStatus(entity.getWorkerStatus()); + roster.setWorkerStatus("304"); + roster.setId(entity.getId()); + roster.setTaskInfoId(taskInfoId); + roster.setVersionNum(version); + baseMapper.updateById(roster); + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "workerStatus", "304"); + } + } + + /** + * 根据用户ID查询用户的基本信息 + * + * @param userId 用户ID + * @return + */ + @Override + public String querySystemWorkerId(String userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + if (null != entity) { + return entity.getSystemWokerId(); + } + return ""; + } + + + /** + * 检查手机号是否存在花名册中 + * + * @param phone 手机号吗 + * @return + */ + @Override + public Boolean checkPhoneExistForRoster(String phone) { + FtbPersonnelsStaffRoster oldRoster = queryRosterInfoByPhone(phone); + if (null == oldRoster) { + return false; + } + if (oldRoster.getEnabledMark().equals(1)) { + return false; + } + //重复入职 + if (oldRoster.getWorkerStatus().equals("305")) { + return false; + } + return true; + } + + /** + * 填充合同名称 + * + * @param records + */ + + private void fillContractTypeName(List records) { + //获取合同类型ids + List contractTypeIds = new ArrayList<>(); + for (FtbPersonnelsStaffRosterDto record : records) { + + if (StringUtils.isNotEmpty(record.getContractTypeName())) { + continue; + } + if (StringUtils.isNotEmpty(record.getContractType())) { + contractTypeIds.add(record.getContractType()); + } + } + if (CollectionUtil.isEmpty(contractTypeIds)) { + return; + } + List contractTypeVOS = personnelOrgUtils.queryContractTypeForIds(contractTypeIds); + if (CollectionUtil.isEmpty(contractTypeVOS)) { + return; + } + Map contractTypeMap = contractTypeVOS.stream().collect(Collectors.toMap(ContractTypeVO::getId, ContractTypeVO::getFullName)); + + for (FtbPersonnelsStaffRosterDto record : records) { + if (StringUtils.isNotEmpty(record.getContractType()) && StringUtils.isEmpty(record.getContractTypeName())) { + record.setContractTypeName(contractTypeMap.get(record.getContractType())); + } + } + } + + /** + * 查询检查花名册数据是否存在 + * + * @param id 花名册id + * @return + */ + private FtbPersonnelsStaffRoster queryRosterAndCheckById(String id) { + FtbPersonnelsStaffRoster entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("数据不存在"); + } + if (entity.getEnabledMark().equals(1)) { + throw new RuntimeException("数据已经删除"); + } + return entity; + } + + + /** + * 根据花名册id批量删除 + * + * @param ids 主键集合 + */ + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void deleteBatchData(List ids) { + long totalStart = System.currentTimeMillis(); + List rosterList = baseMapper.selectBatchIds(ids); + if (CollectionUtil.isEmpty(rosterList)) { + throw new RuntimeException("数据不存在"); + } + List deleteList = new ArrayList<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { +// if ("304".equals(roster.getWorkerStatus())) { +// throw new RuntimeException(roster.getName() + "员工处于离职流程中,不可办理其他业务"); +// } + if (roster.getEnabledMark().equals(0)) { + deleteList.add(roster); + } + } + if (CollectionUtil.isEmpty(deleteList)) { + return; + } + long filterEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 过滤可删除数据 条数={} 耗时={}ms", + deleteList.size(), filterEnd - totalStart); + // 逻辑删除 + long logicStart = System.currentTimeMillis(); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.set(FtbPersonnelsStaffRoster::getEnabledMark, 1); + updateWrapper.set(FtbPersonnelsStaffRoster::getJoinNum,0); + updateWrapper.in(FtbPersonnelsStaffRoster::getId, ids); + baseMapper.update(new FtbPersonnelsStaffRoster(), updateWrapper); + long logicEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 花名册逻辑删除 条数={} 耗时={}ms", + ids.size(), logicEnd - logicStart); + String tenantId = UserProvider.getUser().getTenantId(); + long asyncStart = System.currentTimeMillis(); + for (FtbPersonnelsStaffRoster roster : deleteList) { + asyncDeleteRosterOtherData(roster); + } + long asyncEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 异步删除其他数据+踢人 总耗时={}ms", asyncEnd - asyncStart); + // 删除人员信息通知培训 + List userIdList = deleteList.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList()); + // 删除角色权限 + long permStart = System.currentTimeMillis(); + ftbPermissionRoleAuthorizePersonService.deleteEmployeeAllPermission(userIdList); + long permEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 删除角色权限(deleteEmployeeAllPermission) 条数={} 耗时={}ms", + userIdList.size(), permEnd - permStart); + // 删除租户服务信息 + long tenantDelStart = System.currentTimeMillis(); + pTenantAccountApi.deleteUsers(userIdList); + long tenantDelEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 调用PTenantAccountApi.deleteUsers 条数={} 耗时={}ms", + userIdList.size(), tenantDelEnd - tenantDelStart); + long permUserDelStart = System.currentTimeMillis(); + v2UserApi.removeUsers(userIdList,null); + long permUserDelEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 调用V2UserApi.removeUsers 条数={} 耗时={}ms", + userIdList.size(), permUserDelEnd - permUserDelStart); + RTopic theCompany_topic = redissonClient.getTopic("leavingTheCompany_topic"); + JSONObject args = new JSONObject(); + args.put("userIdList", userIdList); + args.put("tenantId", tenantId); + long topicStart = System.currentTimeMillis(); + theCompany_topic.publish(args.toJSONString()); + long topicEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 发布Redisson离职主题消息 耗时={}ms", topicEnd - topicStart); + long totalEnd = System.currentTimeMillis(); + log.error("花名册批量删除(deleteBatchData) 总体耗时={}ms", totalEnd - totalStart); + } + + /** + * 异步删除花名册相关数据 + * + * @param roster + */ + private void asyncDeleteRosterOtherData(FtbPersonnelsStaffRoster roster) { + String tenantId = personnelOrgUtils.getTenantId(); + Map headers = personnelOrgUtils.getHeadersForLogin(); + personnelAsyncServiceUtils.asyncDeleteRosterOtherData(roster, tenantId, headers); + + } + + /** + * 生成系统员工ID + * + * @return + */ + private String generateSystemWorkerId() { + return SelfGrowthUtil.providePersonnelsCustomIDBasedOnTheModule(SelfrowingEnum.STAFF_ROSTER); +// int currNum = 0; +// int max = 30; +// while (true) { +// String workerId = SelfGrowthUtil.providePersonnelsCustomIDBasedOnTheModule(SelfrowingEnum.STAFF_ROSTER); +// QueryWrapper wrapper = new QueryWrapper<>(); +// wrapper.lambda() +// .eq(FtbPersonnelsStaffRoster::getSystemWokerId, workerId); +// Long count = baseMapper.selectCount(wrapper); +// if (null != count && count > 0) { +// currNum++; +// if (currNum > max) { +// throw new RuntimeException("系统员工ID生成失败," + workerId); +// } else { +// continue; +// } +// } +// return workerId; +// } + } + + /** + * 确认到岗 + * + * @param req + * @return + */ + @Override + @GlobalTransactional + public String confirmOnDuty(ConfirmOnDutyReq req) { + String tenantId = personnelOrgUtils.getTenantId(); + Map headers = personnelOrgUtils.getHeadersForLogin(); + Date now = new Date(); + + checkReportsTo(req.getCurrReportsTo()); + checkInputParam(req); + if (req.getEntrySalary() == null) { + req.setEntrySalary(BigDecimal.ZERO); + } + String employmentId = req.getEmploymentId(); + FtbPersonnelsStaffRoster entity = BeanUtil.copyProperties(req, FtbPersonnelsStaffRoster.class); + + //查询入职信息 + FtbPersonnelsStaffEmploymentApply employmentApply = staffEmploymentApplyService.queryAndCheckStaffEmploymentApply(employmentId); + if (employmentApply.getEntryMoney() == null) { + entity.setEntrySalary(new BigDecimal(0)); + } else { + entity.setEntrySalary(employmentApply.getEntryMoney()); + } + // 入职更新 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsStaffEmploymentApply::getId, employmentId); + updateWrapper.set(FtbPersonnelsStaffEmploymentApply::getStatus, 1); + boolean update = staffEmploymentApplyService.update(updateWrapper); + log.info("确认到岗:" + employmentApply.getOpUserName() + "确认到岗, 更新入职表状态返回值:" + update); + + // 签名信息迁移至花名册 + entity.setRegisterImg(employmentApply.getRegisterImg()); + //查询花名册中是否已经有数据 + UserInfoVO userInfoVO = personnelOrgUtils.queryTenantAccount("", entity.getPhone()); + FtbPersonnelsStaffRoster oldRoster = queryRosterInfoByPhone(employmentApply.getPhone()); + String updateType = SalaryApplyTypeEnum.ONBOARDING.getValue().toString(); + if (null != oldRoster) { + //重复入职 + if (oldRoster.getEnabledMark().equals(0)) { + if (oldRoster.getWorkerStatus().equals("302") || oldRoster.getWorkerStatus().equals("303")) { + throw new RuntimeException("该员工已经到岗,请勿重复操作"); + } + + if (oldRoster.getWorkerStatus().equals("304")) { + throw new RuntimeException("该员工已经在离职中"); + } + } + entity.setUserId(oldRoster.getUserId()); + entity.setId(oldRoster.getId()); + if (null != oldRoster.getJoinNum()) { + entity.setJoinNum(oldRoster.getJoinNum() + 1); + } else { + entity.setJoinNum(1); + } + entity.setSystemWokerId(generateSystemWorkerId()); + updateType = SalaryApplyTypeEnum.DUPLICATE_ONBOARDING.getValue().toString(); + } else { + //第一次入职 + entity.setJoinNum(1); + //生成员工ID + entity.setSystemWokerId(generateSystemWorkerId()); + if (null != userInfoVO && StringUtils.isNotEmpty(userInfoVO.getId())) { + oldRoster = queryRosterInfoByUserId(userInfoVO.getId()); + if (null != oldRoster) { + entity.setUserId(oldRoster.getUserId()); + entity.setId(oldRoster.getId()); + if (null != oldRoster.getJoinNum()) { + entity.setJoinNum(oldRoster.getJoinNum() + 1); + } else { + entity.setJoinNum(1); + } + updateType = SalaryApplyTypeEnum.DUPLICATE_ONBOARDING.getValue().toString(); + } + } + + } + + if (StringUtils.isNotEmpty(entity.getWorkerNo())) { + List rosterList = queryRosterInfoByWorkerId(entity.getWorkerNo()); + if (CollectionUtil.isNotEmpty(rosterList)) { + if (rosterList.size() > 1) { + throw new RuntimeException("员工工号已经存在"); + } + FtbPersonnelsStaffRoster workerRoster = rosterList.get(0); + if (null != entity.getId()) { + if (!workerRoster.getId().equals(entity.getId())) { + throw new RuntimeException("员工工号已经存在"); + } + } else { + throw new RuntimeException("员工工号已经存在"); + } + } + } + entity.setCurrSalary(entity.getEntrySalary()); + //100、无试用期 101、1个月内 102、1个月 + if (req.getProbationPeriod().equals("100")) { + entity.setRegularSalary(entity.getEntrySalary()); + entity.setActualProbationaryDate(req.getActualStartDate()); + } + //写入数据库 + entity.setEnabledMark(0); + entity.setIsSubmitForm(employmentApply.getIsSubmitForm()); + entity.setPhone(employmentApply.getPhone()); + entity.setName(employmentApply.getWorkerName()); + entity.setCreatorTime(new Date()); + fillTrialInfo(entity, req);//填充试岗信息 + entity.setContractStatus(FContractSignStatus.NOT_INITIATED.getCode()); + entity.setContractTypeName(""); + entity.setContractType(""); + entity.setIsTrialFail(0); + entity.setTrialFail(""); + entity.setIsSignSeparation(0); + saveOrUpdate(entity); + //已经填写了入职登记表,那么就要把之前入职登记表中的数据加上花名册ID + registrationFormDataService.addRosterIdToFormData(entity.getPhone(), entity.getId()); + //同步数据到formData + Map formDataMap = BeanUtil.beanToMap(req); + Map formDataMapEntity = BeanUtil.beanToMap(entity); + formDataMap.putAll(formDataMapEntity); + formDataMap.put("planProbationaryDate", personnelOrgUtils.dateToString(req.getPlanProbationaryDate(), "")); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(req.getActualStartDate(), "")); + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(req.getProbationPeriod(), req.getProbationPeriodDay()); + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + formDataMap.put("workerName", entity.getName()); + if (now.after(entity.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(entity.getActualStartDate(), new Date(), false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + if (null != entity.getActualProbationaryDate()) { + formDataMap.put("actualProbationaryDate", personnelOrgUtils.dateToString(entity.getActualProbationaryDate(), "")); + } + //清楚合同信息 + formDataMap.put("contractStatus", "500"); + formDataMap.put("contractCompany", ""); + formDataMap.put("contractEffectiveDate", ""); + formDataMap.put("contractName", ""); + formDataMap.put("contractType", ""); + formDataMap.put("contractFile", ""); + formDataMap.put("contractDate", ""); + formDataMap.put("contractDetail", ""); + formDataMap.put("contractTaskId", ""); + registrationFormDataService.syncData(formDataMap, entity.getPhone(), entity.getId()); + Map formMap = queryFormDataForPhone(entity.getPhone(), entity.getId()); + + String currRank = entity.getCurrRank(); + String currPosition = entity.getCurrPosition(); + String currOrg = entity.getCurrOrg(); + if (null == userInfoVO || StringUtils.isEmpty(userInfoVO.getId())) { + //生成系统租户账号 + String workerSex = personnelOrgUtils.getFormDataForField(formMap, "workerSex"); + SaveUserManagerAddDTO userManagerAddDTO = new SaveUserManagerAddDTO(); + userManagerAddDTO.setAccount(ACCOUNT_TEMP_PRE + entity.getPhone()); + userManagerAddDTO.setMobilePhone(entity.getPhone()); + userManagerAddDTO.setRealName(entity.getName()); + if (StringUtils.isEmpty(workerSex)) { + userManagerAddDTO.setGender(3); + } else { + userManagerAddDTO.setGender(Integer.valueOf(workerSex)); + } + userManagerAddDTO.setJobStatus(personnelOrgUtils.convertAccountStatus(entity.getWorkerStatus())); + userManagerAddDTO.setOrgId(currOrg); + userManagerAddDTO.setPositionId(currPosition); + userManagerAddDTO.setPositionGradesId(currRank); + userManagerAddDTO.setManagerId(entity.getCurrReportsTo()); + userManagerAddDTO.setEntryDate(entity.getActualStartDate()); + try { + log.error("正常日志,租户账户={}", JSONUtil.toJsonStr(userManagerAddDTO)); + String userId = userApi.userManagerBoundAdd(userManagerAddDTO); + if (StringUtils.isEmpty(userId) || userId.startsWith("{")) { + log.error("生成租户账号失败,dto={},userId={}", JSONUtil.toJsonStr(userManagerAddDTO), userId); + throw new RuntimeException("生成账户失败"); + } + entity.setUserId(userId); + } catch (Exception e) { + log.error("生成租户账号失败,dto={}", JSONUtil.toJsonStr(userManagerAddDTO), e); + throw new RuntimeException("生成账户失败"); + } + + } else { + //同步组织岗位职等 直属主管 + entity.setUserId(userInfoVO.getId()); + updateUserOrgAndPosition(entity.getUserId(), entity.getCurrOrg(), entity.getCurrPosition(), entity.getCurrRank(), entity.getCurrReportsTo()); + } + + //生成主账号 + UserAccountDto userAccountDto = new UserAccountDto(); + userAccountDto.setFid(entity.getUserId()); + userAccountDto.setUserName(entity.getName()); + UserInfo userInfo = UserProvider.getUser(); + userAccountDto.setTenantId(userInfo.getTenantId()); + userAccountDto.setUserMobilePhone(entity.getPhone()); + String flowerName = personnelOrgUtils.getFormDataForField(formMap, "flowerName"); + if (StringUtils.isNotEmpty(flowerName)) { + userAccountDto.setUserNickName(flowerName); + } + + String userHeadLog = getUserHeadLog(formMap, entity); + userAccountDto.setHeadImageUrl(userHeadLog); + + List addUserList = List.of(userAccountDto); + ActionResult> actionResult = null; + long tenantAccountStart = System.currentTimeMillis(); + actionResult = pTenantAccountApi.batchAddUserAccount(addUserList); + long tenantAccountEnd = System.currentTimeMillis(); + try { + log.error("正常日志create main user={},result={}, PTenantAccountApi.batchAddUserAccount RT:{}ms", + JSONUtil.toJsonStr(userAccountDto), JSONUtil.toJsonStr(actionResult), tenantAccountEnd - tenantAccountStart); + if (null == actionResult || !actionResult.getCode().equals(200) || CollectionUtil.isEmpty(actionResult.getData())) { + //如果主账号生成失败就删除刚刚建立的用户 + throw new RuntimeException("主账号生成失败"); + } + } catch (Exception e) { + personnelOrgUtils.rollbackTenantUser(addUserList); + throw new RuntimeException("主账号生成失败"); + } + batchFillAccountForUserId(actionResult.getData(), tenantId, headers); + //修改用户头像 + registrationFormDataService.updateForOneField(entity.getId(), entity.getPhone(), "headLogo", JSONUtil.toJsonStr(List.of(userHeadLog))); + //修改用户的userId + baseMapper.updateById(entity); + + //写入入职薪酬 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(entity.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(entity.getCurrSalary()); + if (entity.getJoinNum() > 1) { + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.REPEAT_JOIN.getCode()); + } else { + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.FIRST_JOIN.getCode()); + } + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + //员工成长 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(entity.getUserId()); + addGrowthLogDto.setChangeDate(new Date()); + if (entity.getJoinNum() > 1) { + addGrowthLogDto.setGrowthType(GrowthLogEnum.REPEAT_JOIN.getCode()); + addGrowthLogDto.setNum(entity.getJoinNum()); + } else { + addGrowthLogDto.setGrowthType(GrowthLogEnum.FIRST_JOIN.getCode()); + } + addGrowthLogDto.setDetail(buildGrowthLogDetail(currOrg, currPosition, currRank, req.getCurrReportsTo(), req.getActualStartDate())); + addGrowthLogDto.setEmployeeId(entity.getSystemWokerId()); + // 入职的 + growthLogService.addGrowthLog(addGrowthLogDto); + + + //同步用户角色 + if (StringUtils.isNotEmpty(entity.getCurrRole())) { + SynUserBoundRoleDTO synUserBoundRoleDTO = new SynUserBoundRoleDTO(); + synUserBoundRoleDTO.setUserId(entity.getUserId()); + synUserBoundRoleDTO.setRoleIds(Arrays.asList(entity.getCurrRole().split(","))); + personnelOrgUtils.sysUserRole(synUserBoundRoleDTO); + } + //同步用户门店 + personnelOrgUtils.addUserStoreRelation(entity.getUserId(), req.getCurrShopId(), req.getCurrOrg(), req.getCurrPosition(), req.getCurrRank()); + //同步状态到内推池子 + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), entity.getWorkerStatus()); + + // 同步转正管理 + // 302、试用 + if (("302".equals(entity.getWorkerStatus()) + || "306".equals(entity.getWorkerStatus())) && !req.getProbationPeriod().equals("100")) { + syncRegular(entity, req.getProbationPeriod(), req.getProbationPeriodDay(), req.getCurrReportsTo()); + } + clearAuthUserList(); + + //同步人脸 + if (StrUtil.isNotBlank(employmentApply.getPhotoFiles()) && !"[null]".equals(employmentApply.getPhotoFiles()) && !"[]".equals(employmentApply.getPhotoFiles())) { + if (JSONUtil.isTypeJSONArray(employmentApply.getPhotoFiles())) { + JSONUtil.toList(employmentApply.getPhotoFiles(), FtbCultivateOfflineFileDTO.class).stream().findFirst().ifPresent( + photo -> { + UserFaceDto userFaceDto = new UserFaceDto(); + userFaceDto.setFaceData(fileApi.getTencentDownloadStringUrl(photo.getUrl())); + userFaceDto.setUserId(entity.getUserId()); + attendanceUserFaceService.updateUserFace(userFaceDto); + } + ); + } else { + log.error(employmentApply.getOpUserName() + "-确认到岗, 人脸数据格式不对, 未同步人脸至考勤!"); + } + } + // 薪酬信息同步 + try { + if (StrUtil.isNotBlank(employmentApply.getSalaryData()) && !"null".equals(employmentApply.getSalaryData()) && !"[]".equals(employmentApply.getSalaryData())) { + if (JSONUtil.isTypeJSONArray(employmentApply.getSalaryData())) { + List salaryItemList = JsonUtil.getJsonToList(employmentApply.getSalaryData(), FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + PositionGradesInfoVO positionGradesInfoVO = personnelOrgUtils.queryRank(currRank); + PositionEntity positionEntity = personnelOrgUtils.queryPosition(currPosition); + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(currOrg); + userInfoWithSalary.setRankName(positionGradesInfoVO.getFullName()); + userInfoWithSalary.setRankId(currRank); + userInfoWithSalary.setFOrgId(currOrg); + userInfoWithSalary.setFOrgName(organizeEntity.getFullName()); + userInfoWithSalary.setPostId(currPosition); + userInfoWithSalary.setPostName(positionEntity.getFullName()); + //生效日期取,实际 入职日期 + if (CollUtil.isNotEmpty(salaryItemList)) { + personnelSalaryService.saveTheChangePayInformation( + salaryItemList, + entity.getUserId(), + entity.getActualStartDate(), + userInfoWithSalary, + "2",//和薪酬沟通传入固定值2 + employmentApply.getReasonsForSalaryAdjustments(), + "4", 0, null, ""); + } + } else { + log.error(employmentApply.getOpUserName() + "-确认到岗, 薪酬信息数据格式不对, 未同步薪资项到考勤!"); + } + } else { + if (updateType.equals(SalaryApplyTypeEnum.DUPLICATE_ONBOARDING.getValue().toString())) { + // 暂时注释掉从新入职没有薪资 清除旧的薪资数据 +// personnelSalaryService.removeUserAllSalary(entity.getUserId()); + } + } + } catch (Exception e) { + personnelOrgUtils.deleteTenantUser(entity.getUserId()); + pTenantAccountApi.deleteUser(entity.getUserId()); + throw e; + } + + asyncDeleteHistory(List.of(entity.getUserId())); + if (oldRoster != null) { + asyncDeleteHistory(List.of(oldRoster.getUserId())); + } + // 不需要进行邀请填写 +// if (employmentApply.getIsSubmitForm() == 0) { +// personnelAsyncServiceUtils.sendRegisterForm(entity.getUserId(), entity.getPhone(), tenantId, headers); +// } + personnelOrgUtils.updateBaseInfoForBaseUser(entity); + // 最后去下发 + //同步考勤组 + String token = userInfo.getToken(); + Map headers1 = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + threadPoolExecutor.execute(() -> { + FeignHolder.sendFeign(headers1, () -> { + try { + GroupUpdateByUserDTO groupUpdateByUserDTO = new GroupUpdateByUserDTO(); + groupUpdateByUserDTO.setTenantId(userInfo.getTenantId()); + groupUpdateByUserDTO.setType(1);//1入职 2离职 3调岗 + groupUpdateByUserDTO.setToGroupId(req.getAttendanceGroup()); + groupUpdateByUserDTO.setUserIds(List.of(entity.getUserId())); + groupUpdateByUserDTO.setJoiningDate(entity.getActualStartDate()); + personnelOrgUtils.syncAttendanceGroup(groupUpdateByUserDTO); + log.error("确认到岗, 同步考勤信息成功!"); + // 更新用户主岗 + BaseUserPrimaryPositionDTO positionDTO = new BaseUserPrimaryPositionDTO(); + positionDTO.setUserId(entity.getUserId()); + positionDTO.setOrganizeId(entity.getCurrOrg()); + positionDTO.setPositionId(entity.getCurrPosition()); + positionDTO.setPositionGradesId(entity.getCurrRank()); + positionDTO.setManagerId(entity.getCurrReportsTo()); + userPrimaryPositionApi.updateUserPrimaryPosition(positionDTO); + } catch (Exception e) { + throw new RuntimeException(e); + } + return null; + }); + }); + return entity.getId(); + } + + /** + * 检查用户作为直属主管是否可以 + * + * @param currReportsTo + */ + public void checkReportsTo(String currReportsTo) { + if (StringUtils.isEmpty(currReportsTo)) { + return; + } + FtbPersonnelsStaffRoster roster = queryRosterInfoByUserId(currReportsTo); + if (null == roster) { + throw new RuntimeException("直属主管不存在"); + } + if (StaffWorkerStatus.RESIGNED.getCode().equals(roster.getWorkerStatus())) { + throw new RuntimeException("直属主管离职"); + } + if (StaffWorkerStatus.PRE_TRIAL.getCode().equals(roster.getWorkerStatus())) { + throw new RuntimeException("直属主管不能是试岗员工"); + } + } + + public void asyncDeleteHistory(List userIds) { + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + if (StringUtils.isEmpty(token)) { + token = ""; + } + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + personnelAsyncServiceUtils.deleteTurnoverUserHistory(userIds, tenantId, headers); + } + + /** + * 异步 给任务添加新入职用户 + * + * @param userIds + */ + public void asyncAddNewPersonToTask(List userIds,String tenantId) { + personnelAsyncServiceUtils.asyncAddNewPersonToTask(userIds, tenantId); + } + + private void asyncDeleteHistory(List userIds, String tenantId, Map headers) { + if (CollectionUtil.isEmpty(userIds)) { + return; + } + personnelAsyncServiceUtils.deleteTurnoverUserHistory(userIds, tenantId, headers); + } + + private void fillTrialInfo(FtbPersonnelsStaffRoster entity, ConfirmOnDutyReq req) { + TrialPeriodStatus trialPeriodStatus = TrialPeriodStatus.fromCode(req.getIsTrialJob()); + entity.setIsTrialJob(trialPeriodStatus.getCode()); + if (trialPeriodStatus == TrialPeriodStatus.YES) { + entity.setTrialJobDay(req.getTrialJobDay()); + } else { + entity.setTrialJobDay(0); + } + + // + String probationPeriod = req.getProbationPeriod(); + if (probationPeriod.equals("100")) {//无试用期 + if (trialPeriodStatus == TrialPeriodStatus.YES) { + entity.setWorkerStatus(StaffWorkerStatus.PRE_TRIAL.getCode()); + } else { + entity.setWorkerStatus(StaffWorkerStatus.FULL_TIME.getCode()); + } + } else { + if (trialPeriodStatus == TrialPeriodStatus.YES) { + entity.setWorkerStatus(StaffWorkerStatus.PRE_TRIAL.getCode()); + } else { + entity.setWorkerStatus(StaffWorkerStatus.TRIAL.getCode()); + } + } + } + + /** + * 检查入参 + * + * @param req + */ + private void checkInputParam(ConfirmOnDutyReq req) { + if (null == req.getIsTrialJob()) { + req.setIsTrialJob(0); + } + String probationPeriod = req.getProbationPeriod(); + TrialPeriodStatus trialPeriodStatus = TrialPeriodStatus.fromCode(req.getIsTrialJob()); + if (probationPeriod.equals("100")) {//无试用期 + if (trialPeriodStatus == TrialPeriodStatus.YES) { + if (req.getTrialJobDay() < 1) { + throw new RuntimeException("试岗天数不能小于1天"); + } else if (req.getTrialJobDay() > 999) { + throw new RuntimeException("试岗天数不能大于999天"); + } + } + } else { + int maxDay = PersonnelStaffUtils.calculatePeriodDays(req.getProbationPeriod(), req.getProbationPeriodDay()); + if (probationPeriod.equals("101")) {//1个月内 + if (maxDay < 1) { + throw new RuntimeException("试用期天数不能小于1天"); + } else if (maxDay > 30) { + throw new RuntimeException("试用期天数不能超过1个月"); + } + } + if (trialPeriodStatus == TrialPeriodStatus.YES) { + if (req.getTrialJobDay() < 1) { + throw new RuntimeException("试岗天数不能小于1天"); + } else if (req.getTrialJobDay() > maxDay) { + throw new RuntimeException("试岗天数不能大于试用天数"); + } + } + } + } + + + private void updateUserOrgAndPosition(String userId, String orgId, String positionId, String rankId, String reportTo) { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrg(orgId); + workerGroupDataDto.setAffiliatedPosition(positionId); + workerGroupDataDto.setAffiliatedRank(rankId); + workerGroupDataDto.setReportsTo(reportTo); + personnelOrgUtils.sysUserBoundList(List.of(workerGroupDataDto), userId); + } + + /** + * 批量回填用户账号 + * + * @param list + */ + private void batchFillAccountForUserId(List list, String tenantCode, Map headers) { + personnelAsyncServiceUtils.batchFillAccountForUserId(list, tenantCode, headers); + } + + /** + * 同步转正管理 + * + * @param entity + * @param probationPeriod + * @param probationPeriodDay + * @param currReportsTo + */ + private void syncRegular(FtbPersonnelsStaffRoster entity, + String probationPeriod, + String probationPeriodDay, + String currReportsTo) { + try { + FtbPersonnelsRegularCreateDTO createDTO = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularInfoVO(entity); + OrganizeEntity infoById = organizeApi.getInfoById(createDTO.getOrgId()); + createDTO.setOrgName(infoById.getFullName()); + UserEntity userEntity = StringUtils.isEmpty(currReportsTo) ? null : userApi.getInfoById(currReportsTo); + if (userEntity != null) { + createDTO.setImmediateSuperName(userEntity.getRealName()); + createDTO.setImmediateSuperId(currReportsTo); + } + PositionEntity positionEntity = positionApi.queryInfoById(createDTO.getRegularPostId()); + createDTO.setRegularPostName(positionEntity.getFullName()); + createDTO.setOnboardPostName(positionEntity.getFullName()); + ActionResult gradesInfo = positionApi.getGradesInfo(createDTO.getRegularGradeId()); + if (gradesInfo != null && gradesInfo.getData() != null) { + createDTO.setRegularGradeName(gradesInfo.getData().getFullName()); + createDTO.setOnboardGradeName(gradesInfo.getData().getFullName()); + } + calculatePlannedConversionTime(probationPeriod, probationPeriodDay, entity, createDTO); + createDTO.setId(entity.getId()); + regularManagementService.applyForRegularization(createDTO); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 计算计划转正时间 + * + * @param probationPeriod 是否是天 + * @param probationPeriodDay 时间 + * @param entity + * @param createDTO + */ + private void calculatePlannedConversionTime(String probationPeriod, + String probationPeriodDay, + FtbPersonnelsStaffRoster entity, + FtbPersonnelsRegularCreateDTO createDTO) { + // 入职日期 + Date actualStartDate = entity.getActualStartDate(); + int probation = 0; + // 创建一个 Calendar 实例并设置为当前日期 + Calendar calendar = Calendar.getInstance(); + calendar.setTime(actualStartDate); + if ("101".equals(probationPeriod)) { + // 这里就是天数 + createDTO.setProbationPeriodDay(probationPeriodDay); + calendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(probationPeriodDay)); + } else { + switch (probationPeriod) { + case "102": + probation = 1; + break; + case "103": + probation = 2; + break; + case "104": + probation = 3; + break; + case "105": + probation = 4; + break; + case "106": + probation = 5; + break; + case "107": + probation = 6; + break; + case "108": + probation = 7; + break; + case "109": + probation = 8; + break; + } + calendar.add(Calendar.MONTH, probation); + } + Date time = calendar.getTime(); + createDTO.setProbation(probationPeriod); + // 计算实际转正时间 + createDTO.setSchedConverDate(time); + } + + + public Map queryFormDataForPhone(String phone, String rosterId) { + List formDataList = registrationFormDataService.queryFormFieldValueList(phone, rosterId); + if (CollectionUtil.isEmpty(formDataList)) { + return new HashMap<>(); + } + Map map = new HashMap<>(); + for (FtbPersonnelsStaffRegistrationFormData formData : formDataList) { + map.put(formData.getFormFieldId(), formData); + } + return map; + } + + @Override + @Transactional + public List addImportRosters(List list) { + if (CollectionUtil.isEmpty(list)) { + return CollectionUtil.newArrayList(); + } + + if (list.size() > 200) { + throw new RuntimeException("导入数据不能超过200条"); + } + List innerImportDtoList = new ArrayList<>(); + //转换并检 + Map rosterMap = batchQueryPhone(list); + List oldUserList = new ArrayList<>(list.size()); + List oldRosterList = new ArrayList<>(list.size()); + + for (FtbRosterInsertNomalVO ftbRosterInsertNomalVO : list) { + log.info("excel import data={}", ftbRosterInsertNomalVO); + FtbPersonnelsStaffRoster newRoster = personnelOrgUtils.buildImportRoster(ftbRosterInsertNomalVO); + + FtbPersonnelsStaffRoster oldRoster = rosterMap.get(newRoster.getPhone()); + if (null != oldRoster) { + if (oldRoster.getWorkerStatus().equals("302") || oldRoster.getWorkerStatus().equals("303") || oldRoster.getWorkerStatus().equals("306")) { + ftbRosterInsertNomalVO.setIsNew(false); + oldUserList.add(oldRoster.getUserId()); + oldRosterList.add(oldRoster.getId()); + } + + if (oldRoster.getWorkerStatus().equals("304")) { + ftbRosterInsertNomalVO.setIsNew(false); + oldUserList.add(oldRoster.getUserId()); + oldRosterList.add(oldRoster.getId()); + continue; + } + } + innerImportDtoList.add(new InnerImportRosterDto(newRoster, oldRoster, ftbRosterInsertNomalVO)); + } + + Map> formDataValueMap = queryFormDataValueForRosterId(oldRosterList); + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + Map headers = personnelOrgUtils.getHeadersForLogin(); + + return doRealImportRoster(innerImportDtoList, tenantId, headers, userInfo, formDataValueMap); + } + + /** + * 批量查询用户附表数据 + * + * @param oldRosterList + * @return + */ + private Map> queryFormDataValueForRosterId(List oldRosterList) { + List formDataList = registrationFormDataService.queryFormFieldValueForRosterIds(oldRosterList); + if (CollectionUtil.isNotEmpty(formDataList)) { + return formDataList.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)); + } + return new HashMap<>(); + } + + private Map batchQueryPhone(List list) { + Set phoneSet = new HashSet<>(); + for (FtbRosterInsertNomalVO vo : list) { + if (phoneSet.contains(vo.getPhone())) { + throw new RuntimeException("姓名:" + vo.getUserName() + ",手机号:" + vo.getPhone() + ",手机号重复"); + } + phoneSet.add(vo.getPhone()); + } + List rosterList = queryRosterInfoByPhones(new ArrayList<>(phoneSet)); + return rosterList.stream() + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getPhone, + Function.identity(), (a, b) -> a)); + } + + public List> groupByBatch(List list, int batchSize) { + List> groupedData = new ArrayList<>(); + for (int i = 0; i < list.size(); i += batchSize) { + int end = Math.min(i + batchSize, list.size()); + groupedData.add(new ArrayList<>(list.subList(i, end))); + } + return groupedData; + } + + public List> groupByBatchTenant(List list, int batchSize) { + List> groupedData = new ArrayList<>(); + for (int i = 0; i < list.size(); i += batchSize) { + int end = Math.min(i + batchSize, list.size()); + groupedData.add(new ArrayList<>(list.subList(i, end))); + } + return groupedData; + } + + + private List doRealImportRoster(List innerImportDtoList, String tenantCode, + Map headers, UserInfo userInfo, + Map> formDataValueMap) { + List resultVOList = new ArrayList<>(); + //批量查询字段 + List allFormFieldList = bachQueryField(); + //批量写入租户账号 + Map phoneUserMap = batchWriteTenant(innerImportDtoList); + //批量写入平台账号 + Map userToHeadMap = batchWritePlatAccount(innerImportDtoList, phoneUserMap, userInfo); + + Map allFormFieldListMap = allFormFieldList.stream() + .collect(Collectors.toMap(FtbPersonnelsRegistrationFormField::getId, Function.identity())); + //导入操作 + for (InnerImportRosterDto innerImportRosterDto : innerImportDtoList) { + InnerImportResultVO resultVO = null; + if (innerImportRosterDto.getFtbRosterInsertNomalVO().getIsNew()) { + resultVO = addImportOneRoster(innerImportRosterDto.getFtbRosterInsertNomalVO(), innerImportRosterDto.getNewRoster(), innerImportRosterDto.getOldRoster(), + userInfo, phoneUserMap, userToHeadMap, allFormFieldListMap); + } else { + List formDataValueList = formDataValueMap.get(innerImportRosterDto.getOldRoster().getId()); + resultVO = updateImportOneRoster(innerImportRosterDto.getFtbRosterInsertNomalVO(), innerImportRosterDto.getNewRoster(), innerImportRosterDto.getOldRoster(), + allFormFieldList, allFormFieldListMap, formDataValueList); + } + resultVOList.add(new ImportResultVO(resultVO.getSuccess(), "", innerImportRosterDto.getFtbRosterInsertNomalVO().getPhone())); + } + + asyncDeleteHistory(new ArrayList<>(userToHeadMap.keySet()), tenantCode, headers); + return resultVOList; + } + + private List bachQueryField() { + List list = registrationFormFieldService.list(new QueryWrapper() + .lambda() + .eq(FtbPersonnelsRegistrationFormField::getEnabledMark, 0) //1无效 0有效 + .eq(FtbPersonnelsRegistrationFormField::getStatus, 0) //0、启用 1、禁用 + .orderByAsc(FtbPersonnelsRegistrationFormField::getSorts)); + + if (CollectionUtil.isNotEmpty(list)) { + return list; + } + return new ArrayList<>(); + } + + private InnerImportResultVO updateImportOneRoster(FtbRosterInsertNomalVO ftbRosterInsertNomalVO, FtbPersonnelsStaffRoster entity, FtbPersonnelsStaffRoster oldRoster, + List allFormFieldList, Map allFormFieldListMap, List formDataValueList) { + Map formFieldValueMap = personnelOrgUtils.convertFormValueMap(formDataValueList); + Map innerFieldValueCacheMap = ftbRosterInsertNomalVO.getInnerFieldValueCacheMap(); + //同步数据到formData + Map formDataMap = convertToUpdateFormData(innerFieldValueCacheMap, allFormFieldList); + formDataMap.put("probationPeriod", ""); + formDataMap.put("actualStartDate", ""); + formDataMap.put("companyAge", ""); + formDataMap.put("planProbationaryDate", ""); + formDataMap.put("actualProbationaryDate", ""); + + LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); + String probationPeriod = innerFieldValueCacheMap.get("probationPeriod"); + + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(probationPeriod, "0"); + Date now = new Date(); + //写入数据库 + String planProbationaryDate = "";//计划转正日期 + if (entity.getActualStartDate() != null && StringUtils.isNotEmpty(probationPeriod)) { + + int month = personnelOrgUtils.calMonth(probationPeriodDto); + Date toDay = DateUtil.offsetMonth(entity.getActualStartDate(), month); + String toDayString = personnelOrgUtils.dateToString(toDay, "yyyy-MM-dd"); + planProbationaryDate = toDayString; + } + + + if (StringUtils.isEmpty(oldRoster.getWorkerNo()) && StringUtils.isNotEmpty(entity.getWorkerNo())) { + wrapper.set(FtbPersonnelsStaffRoster::getWorkerNo, entity.getWorkerNo()); + formDataMap.put("workerNo", entity.getWorkerNo()); + } + if (StringUtils.isEmpty(oldRoster.getCurrOrg()) && StringUtils.isNotEmpty(entity.getCurrOrg())) { + wrapper.set(FtbPersonnelsStaffRoster::getCurrOrg, entity.getCurrOrg()); + formDataMap.put("currOrg", entity.getCurrOrg()); + } + if (StringUtils.isEmpty(oldRoster.getCurrPosition()) && StringUtils.isNotEmpty(entity.getCurrPosition())) { + wrapper.set(FtbPersonnelsStaffRoster::getCurrPosition, entity.getCurrPosition()); + formDataMap.put("currPosition", entity.getCurrPosition()); + } + if (StringUtils.isEmpty(oldRoster.getCurrRank()) && StringUtils.isNotEmpty(entity.getCurrRank())) { + wrapper.set(FtbPersonnelsStaffRoster::getCurrRank, entity.getCurrRank()); + formDataMap.put("currRank", entity.getCurrRank()); + } + if (StringUtils.isEmpty(oldRoster.getCurrShopId()) && StringUtils.isNotEmpty(entity.getCurrShopId())) { + wrapper.set(FtbPersonnelsStaffRoster::getCurrShopId, entity.getCurrShopId()); + formDataMap.put("currShopId", entity.getCurrShopId()); + } + if (null == oldRoster.getEntrySalary() && null != entity.getEntrySalary()) { + wrapper.set(FtbPersonnelsStaffRoster::getEntrySalary, entity.getEntrySalary()); + formDataMap.put("entrySalary", entity.getEntrySalary()); + } + if (StringUtils.isEmpty(oldRoster.getCurrReportsTo()) && StringUtils.isNotEmpty(entity.getCurrReportsTo())) { + wrapper.set(FtbPersonnelsStaffRoster::getCurrReportsTo, entity.getCurrReportsTo()); + formDataMap.put("currReportsTo", entity.getCurrReportsTo()); + } + //老的试用期 + Boolean isProbationPeriod = false; + FtbPersonnelsStaffRegistrationFormData oldProbationPeriod = formFieldValueMap.get("probationPeriod"); + if (oldProbationPeriod != null) { + if (StringUtils.isNotEmpty(oldProbationPeriod.getValue()) && oldProbationPeriod.getValue().startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(oldProbationPeriod.getValue(), ProbationPeriodDto.class); + String key = bean.getType(); + if (StringUtils.isNotEmpty(key)) { + isProbationPeriod = true; + } + } + } + //是否又技化转正 + Boolean isplanProbationaryDate = false; + FtbPersonnelsStaffRegistrationFormData oldplanProbationaryDate = formFieldValueMap.get("planProbationaryDate"); + if (oldplanProbationaryDate != null) { + if (StringUtils.isNotEmpty(oldplanProbationaryDate.getValue())) { + isplanProbationaryDate = true; + } + } + //实际转正 + FtbPersonnelsStaffRegistrationFormData oldActualProbationaryDate = formFieldValueMap.get("actualProbationaryDate"); + Boolean isactualProbationaryDate = false; + if (oldActualProbationaryDate != null) { + if (StringUtils.isNotEmpty(oldActualProbationaryDate.getValue())) { + isactualProbationaryDate = true; + } + } + if (oldRoster.getWorkerStatus().equals(StaffWorkerStatus.FULL_TIME.getCode()) || oldRoster.getWorkerStatus().equals(StaffWorkerStatus.PENDING_RESIGNATION.getCode())) { + + if (null == oldRoster.getActualStartDate() && isProbationPeriod == false && isplanProbationaryDate == false) { + if (null != entity.getActualStartDate()) { + wrapper.set(FtbPersonnelsStaffRoster::getActualStartDate, entity.getActualStartDate()); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(entity.getActualStartDate(), "yyyy-MM-dd")); + + if (now.after(entity.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(entity.getActualStartDate(), new Date(), false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + } + if (StringUtils.isNotEmpty(probationPeriod)) { + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + } + if (StringUtils.isNotEmpty(planProbationaryDate)) { + formDataMap.put("planProbationaryDate", planProbationaryDate); + } + + } else if (null != oldRoster.getActualStartDate() && isProbationPeriod == false && isplanProbationaryDate == false) { + if (isProbationPeriod == false && StringUtils.isNotEmpty(probationPeriod)) { + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + Date toDay = DateUtil.offsetMonth(oldRoster.getActualStartDate(), personnelOrgUtils.calMonth(probationPeriodDto)); + String toDayString = personnelOrgUtils.dateToString(oldRoster.getActualStartDate(), "yyyy-MM-dd"); + planProbationaryDate = toDayString; + formDataMap.put("planProbationaryDate", planProbationaryDate); + } + + } else { + if (null == oldRoster.getActualStartDate() && null != entity.getActualStartDate()) { + wrapper.set(FtbPersonnelsStaffRoster::getActualStartDate, entity.getActualStartDate()); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(entity.getActualStartDate(), "yyyy-MM-dd")); + if (now.after(entity.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(entity.getActualStartDate(), new Date(), false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + } + if (isProbationPeriod == false && StringUtils.isNotEmpty(probationPeriod)) { + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + } + + if (isplanProbationaryDate == false && StringUtils.isNotEmpty(planProbationaryDate)) { + formDataMap.put("planProbationaryDate", planProbationaryDate); + } + } + + String newactualProbationaryDate = innerFieldValueCacheMap.get("actualProbationaryDate"); + if (isactualProbationaryDate == false && StringUtils.isNotEmpty(newactualProbationaryDate)) { + wrapper.set(FtbPersonnelsStaffRoster::getActualProbationaryDate, personnelOrgUtils.stringDateToDate(newactualProbationaryDate)); + formDataMap.put("actualProbationaryDate", newactualProbationaryDate); + } + + + } else { + + if (null != oldRoster.getActualStartDate() && isProbationPeriod == false && StringUtils.isNotEmpty(probationPeriod)) { + if (isProbationPeriod == false && StringUtils.isNotEmpty(probationPeriod)) { + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + Date toDay = DateUtil.offsetMonth(oldRoster.getActualStartDate(), personnelOrgUtils.calMonth(probationPeriodDto)); + String toDayString = personnelOrgUtils.dateToString(oldRoster.getActualStartDate(), "yyyy-MM-dd"); + planProbationaryDate = toDayString; + formDataMap.put("planProbationaryDate", planProbationaryDate); + } + } else { + if (null == oldRoster.getActualStartDate() && null != entity.getActualStartDate()) { + wrapper.set(FtbPersonnelsStaffRoster::getActualStartDate, entity.getActualStartDate()); + formDataMap.put("actualStartDate", personnelOrgUtils.dateToString(entity.getActualStartDate(), "yyyy-MM-dd")); + if (now.after(entity.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(entity.getActualStartDate(), new Date(), false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + } + + if (isProbationPeriod == false && StringUtils.isNotEmpty(probationPeriod)) { + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + } + + if (isplanProbationaryDate == false && StringUtils.isNotEmpty(planProbationaryDate)) { + formDataMap.put("planProbationaryDate", planProbationaryDate); + } + } + formDataMap.put("actualProbationaryDate", ""); + wrapper.set(FtbPersonnelsStaffRoster::getActualProbationaryDate, null); + } + + + if (StringUtils.isEmpty(oldRoster.getWorkerType()) && StringUtils.isNotEmpty(entity.getWorkerType())) { + wrapper.set(FtbPersonnelsStaffRoster::getWorkerType, entity.getWorkerType()); + formDataMap.put("workerType", entity.getWorkerType()); + } + + wrapper.set(FtbPersonnelsStaffRoster::getUserId, oldRoster.getUserId()); + + wrapper.eq(FtbPersonnelsStaffRoster::getId, oldRoster.getId()); + wrapper.set(FtbPersonnelsStaffRoster::getEnabledMark, 0); + this.update(wrapper); + + String workAddress = innerFieldValueCacheMap.get("workAddress"); + if (StringUtils.isNotEmpty(workAddress)) { + WorkAddressDto workAddressDto = new WorkAddressDto(); + workAddressDto.setAddress(workAddress); + formDataMap.put("workAddress", JSONUtil.toJsonStr(workAddressDto)); + } + addRunBirthday(formDataMap); + registrationFormDataService.importUpdateData(formDataMap, oldRoster.getPhone(), oldRoster.getId(), allFormFieldListMap, formFieldValueMap); + + batchUpdateAccount(entity, oldRoster, formDataMap, innerFieldValueCacheMap); + InnerImportResultVO resultVO = new InnerImportResultVO(); + resultVO.setSuccess(true); + return resultVO; + } + + + private Map convertToUpdateFormData(Map valueMap, List allFormFieldList) { + Map map = new HashMap<>(); + if (CollectionUtil.isEmpty(allFormFieldList)) { + return map; + } + for (FtbPersonnelsRegistrationFormField field : allFormFieldList) { + String val = valueMap.get(field.getId()); + if (StringUtils.isNotEmpty(val)) { + map.put(field.getId(), val); + } + } + return map; + } + + + private Map batchWritePlatAccount(List innerImportDtoList, Map phoneUserMap, UserInfo userInfo) { + + String tenantId = personnelOrgUtils.getTenantId(); + Map headers = personnelOrgUtils.getHeadersForLogin(); + + Map userIdToHeadMap = new HashMap<>();//用户ID ->头像 + List headLogoList = new ArrayList<>(); + for (InnerImportRosterDto innerImportRosterDto : innerImportDtoList) { + if (innerImportRosterDto.getFtbRosterInsertNomalVO().getIsNew()) { + FtbPersonnelsStaffRoster newRoster = innerImportRosterDto.getNewRoster(); + String userId = phoneUserMap.get(newRoster.getPhone()); + GenerateHeadFileForm form = new GenerateHeadFileForm(); + form.setName(newRoster.getName()); + String filename = UUID.randomUUID() + ".png"; + form.setFilename(filename); + headLogoList.add(form); + userIdToHeadMap.put(userId, filename); + } + } + if (CollectionUtil.isNotEmpty(headLogoList)) { + personnelAsyncServiceUtils.batchGeneralHead(headLogoList, tenantId, headers); + } + List platUserAccountDtoList = new ArrayList<>(); + for (InnerImportRosterDto innerImportRosterDto : innerImportDtoList) { + if (!innerImportRosterDto.getFtbRosterInsertNomalVO().getIsNew()) { + continue; + } else { + FtbPersonnelsStaffRoster newRoster = innerImportRosterDto.getNewRoster(); + FtbRosterInsertNomalVO ftbRosterInsertNomalVO = innerImportRosterDto.getFtbRosterInsertNomalVO(); + String userId = phoneUserMap.get(newRoster.getPhone()); + //生成主账号 + UserAccountDto userAccountDto = new UserAccountDto(); + userAccountDto.setFid(userId); + userAccountDto.setUserName(newRoster.getName()); + userAccountDto.setTenantId(userInfo.getTenantId()); + userAccountDto.setUserMobilePhone(newRoster.getPhone()); + String userHeadLog = userIdToHeadMap.get(userId); + userAccountDto.setHeadImageUrl(userHeadLog); + String flowerName = ftbRosterInsertNomalVO.getInnerFieldValueCacheMap().get("flowerName"); + if (StringUtils.isNotEmpty(flowerName)) { + userAccountDto.setUserNickName(flowerName); + } + platUserAccountDtoList.add(userAccountDto); + } + } + + if (CollectionUtil.isNotEmpty(platUserAccountDtoList)) { + List> allList = groupByBatchTenant(platUserAccountDtoList, 50); + ActionResult> actionResult = null; + for (List userAccountDtos : allList) { + try { + actionResult = pTenantAccountApi.batchAddUserAccount(userAccountDtos); + log.error("create main user={},result={}", JSONUtil.toJsonStr(userAccountDtos), JSONUtil.toJsonStr(actionResult)); + if (null == actionResult || !actionResult.getCode().equals(200) || CollectionUtil.isEmpty(actionResult.getData())) { + throw new RuntimeException("账号生成失败了"); + } + } catch (Exception e) { + personnelOrgUtils.rollbackTenantUser(userAccountDtos); + throw new RuntimeException("账号生成失败了"); + } + batchFillAccountForUserId(actionResult.getData(), tenantId, headers); + } + } + return userIdToHeadMap; + } + + private Map batchWriteTenant(List innerImportDtoList) { + Map retMap = new HashMap<>();//手机号->到用户ID + //查询用户 + List phoneList = new ArrayList<>(); + for (InnerImportRosterDto vo : innerImportDtoList) { + if (vo.getFtbRosterInsertNomalVO().getIsNew()) { + phoneList.add(vo.getNewRoster().getPhone()); + } + } + // + List exiestUser = personnelOrgUtils.queryTenantAccountForPhoneList(phoneList); + Map exisetUserMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(exiestUser)) { + for (UserInfoVO userInfoVO : exiestUser) { + exisetUserMap.put(userInfoVO.getMobilePhone(), userInfoVO); + } + } + + + //生成系统租户账号 + List userManagerAddDTOList = new ArrayList<>(); + for (InnerImportRosterDto vo : innerImportDtoList) { + if (!vo.getFtbRosterInsertNomalVO().getIsNew()) { + retMap.put(vo.getOldRoster().getPhone(), vo.getOldRoster().getUserId()); + continue; + } + FtbPersonnelsStaffRoster entity = vo.getNewRoster(); + UserInfoVO userInfoVO = exisetUserMap.get(entity.getPhone()); + if (null != userInfoVO) { + retMap.put(entity.getPhone(), userInfoVO.getId()); + continue; + } + Map importInfoMap = vo.getFtbRosterInsertNomalVO().getInnerFieldValueCacheMap(); + String workerSex = importInfoMap.get("workerSex"); + String flowerName = importInfoMap.get("flowerName"); + if (null == workerSex) { + workerSex = "0"; + } + SaveUserManagerAddDTO userManagerAddDTO = new SaveUserManagerAddDTO(); + userManagerAddDTO.setAccount(ACCOUNT_TEMP_PRE + entity.getPhone()); + userManagerAddDTO.setMobilePhone(entity.getPhone()); + userManagerAddDTO.setRealName(entity.getName()); + userManagerAddDTO.setNickName(flowerName); + if (StringUtils.isEmpty(workerSex)) { + userManagerAddDTO.setGender(3); + } else { + userManagerAddDTO.setGender(Integer.valueOf(workerSex)); + } + userManagerAddDTO.setJobStatus(personnelOrgUtils.convertAccountStatus(entity.getWorkerStatus())); + userManagerAddDTO.setOrgId(entity.getCurrOrg()); + userManagerAddDTO.setPositionId(entity.getCurrPosition()); + userManagerAddDTO.setPositionGradesId(entity.getCurrRank()); + userManagerAddDTO.setManagerId(entity.getCurrReportsTo()); + if (null != entity.getActualStartDate()) { + userManagerAddDTO.setEntryDate(entity.getActualStartDate()); + } + userManagerAddDTOList.add(userManagerAddDTO); + } + + List retList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(userManagerAddDTOList)) { + try { + retList = userApi.userManagerBoundListAdd(userManagerAddDTOList); + log.error("生成租户账号,dto={},rs={}", JSONUtil.toJsonStr(userManagerAddDTOList), JSONUtil.toJsonStr(retList)); + if (CollectionUtil.isEmpty(retList)) { + throw new RuntimeException("生成账户失败"); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("生成账户失败"); + } + } + for (UserBoundPhoneVO userBoundPhoneVO : retList) { + retMap.put(userBoundPhoneVO.getMobilePhone(), userBoundPhoneVO.getId()); + } + // 更新用户主岗 + userManagerAddDTOList.forEach(item -> { + // 更新用户主岗 + BaseUserPrimaryPositionDTO positionDTO = new BaseUserPrimaryPositionDTO(); + positionDTO.setUserId(retMap.get(item.getMobilePhone())); + positionDTO.setOrganizeId(item.getOrgId()); + positionDTO.setPositionId(item.getPositionId()); + positionDTO.setPositionGradesId(item.getPositionGradesId()); + positionDTO.setManagerId(item.getManagerId()); + userPrimaryPositionApi.updateUserPrimaryPosition(positionDTO); + }); + return retMap; + + } + + void batchUpdateAccount(FtbPersonnelsStaffRoster newRoster, FtbPersonnelsStaffRoster oldRoster, Map importInfoMap, Map innerFieldValueCacheMap) { + + + String workerSex = (String) importInfoMap.get("workerSex"); + String flowerName = (String) importInfoMap.get("flowerName"); + String workerName = (String) importInfoMap.get("workerName"); + String actualStartDate = (String) importInfoMap.get("actualStartDate"); + String birthday = (String) importInfoMap.get("birthday"); + String actualProbationaryDate = (String) importInfoMap.get("actualProbationaryDate"); + String idCardNum = (String) importInfoMap.get("idCardNum"); + + SaveUserManagerAddDTO userManagerAddDTO = new SaveUserManagerAddDTO(); + Boolean isUpdate = false; + userManagerAddDTO.setUserId(oldRoster.getUserId()); + if (StringUtils.isNotEmpty(workerName) && StringUtils.isEmpty(oldRoster.getName())) { + isUpdate = true; + userManagerAddDTO.setRealName(workerName); + } + if (StringUtils.isNotEmpty(workerSex)) { + isUpdate = true; + userManagerAddDTO.setGender(Integer.valueOf(workerSex)); + } + if (StringUtils.isNotEmpty(flowerName)) { + isUpdate = true; + userManagerAddDTO.setNickName(flowerName); + } + + if (StringUtils.isNotEmpty(actualProbationaryDate)) { + isUpdate = true; + userManagerAddDTO.setBecomeDate(DateUtil.parse(actualProbationaryDate, "yyyy-MM-dd")); + } + + if (StringUtils.isNotEmpty(actualStartDate)) { + isUpdate = true; + userManagerAddDTO.setEntryDate(DateUtil.parse(actualStartDate, "yyyy-MM-dd")); + } + if (StringUtils.isNotEmpty(birthday)) { + isUpdate = true; + userManagerAddDTO.setBirthday(DateUtil.parse(birthday, "yyyy-MM-dd")); + } + + if (StringUtils.isNotEmpty(actualStartDate)) { + isUpdate = true; + userManagerAddDTO.setEntryDate(newRoster.getActualStartDate()); + } + + if (StringUtils.isNotEmpty(idCardNum)) { + isUpdate = true; + userManagerAddDTO.setCertificatesNumber(idCardNum); + userManagerAddDTO.setCertificatesType("a745d425adbb4321880817661cae8910"); + } + + if (isUpdate) { + personnelOrgUtils.batchSysTenantAccountBase(List.of(userManagerAddDTO)); + } + } + + private void syncImportFormData(FtbRosterInsertNomalVO ftbRosterInsertNomalVO, Map formDataMap) { + + List voList = ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS(); + if (CollectionUtil.isNotEmpty(voList)) { + for (FtbRosterInsertAttributesVO vo : voList) { + formDataMap.put(vo.getFormFieldId(), vo.getAttributeValue()); + } + } + } + + private String getImportFormDataForFieldId(FtbRosterInsertNomalVO ftbRosterInsertNomalVO, String fieldId) { + + List voList = ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS(); + if (CollectionUtil.isNotEmpty(voList)) { + for (FtbRosterInsertAttributesVO vo : voList) { + if (fieldId.equals(vo.getFormFieldId())) { + return vo.getAttributeValue(); + } + } + } + return ""; + } + + @Override + public List queryRosterInfoByIds(List ids, List formFieldOptionIds,String schemeId) { + //查询字段 + List allFields = rosterExportUtils.getExportFormTypes(formFieldOptionIds,schemeId); + if (CollectionUtil.isEmpty(ids)) { + return new ArrayList<>(); + } + //查询选项 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0).orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getId); + List optionList = fieldOptionService.list(wrapper); + //查询花名册 + List rosterDataList = new ArrayList<>(); + List rosterList = baseMapper.selectBatchIds(ids); + + + List allFormDataList = registrationFormDataService.queryFormFieldValueForRosterIds(ids); + //按照rosterid分组 + Map> map = allFormDataList.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)); + + for (FtbPersonnelsStaffRoster roster : rosterList) { + List formValue = map.getOrDefault(roster.getId(), new ArrayList<>()); + rosterDataList.add(new ExportRosterOneVo(roster, formValue)); + } + return rosterExportUtils.convertToFtbRosterImportTemplateBO(allFields, optionList, rosterDataList); + + } + + @Override + public CanDeleteMsg checkBatchDelete(List ids) { + List list = new ArrayList<>(); + List rosterList = baseMapper.selectBatchIds(ids); + for (FtbPersonnelsStaffRoster roster : rosterList) { + String checkFlag = ftbPersonnelsAuditRunTaskService.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(roster.getUserId()); + LambdaQueryWrapper queryWrapper4 = Wrappers.lambdaQuery(); + queryWrapper4.eq(FtbPersonnelsSecondmentManagement::getUserId, roster.getUserId()); + queryWrapper4.in(FtbPersonnelsSecondmentManagement::getTransferStatus, 1); + queryWrapper4.eq(FtbPersonnelsSecondmentManagement::getEnableMark, 0); + long count1 = secondmentManagementService.count(queryWrapper4); + if (count1 > 0 && StringUtils.isEmpty(checkFlag)) { + checkFlag = "借调"; + } + if (StringUtils.isNotEmpty(checkFlag)) { + list.add(convertToCheckRosterDeleteVo(roster, 3, false)); + continue; + } + ActionResult actionResult = flowTaskApi.checkReloadWait(roster.getUserId()); + if (actionResult != null && actionResult.getCode() == 200 && (boolean) actionResult.getData()) { + list.add(convertToCheckRosterDeleteVo(roster, 3, true)); + } + } + if (CollectionUtil.isNotEmpty(list)) { + return new CanDeleteMsg(false, "不能删除", list); + } else { + return new CanDeleteMsg(true, "可以删除"); + } + } + + /** + * 试岗驳回接口 + * + * @param userId + * @param dto + */ + @Override + @Transactional + public void innerTravelFail(String userId, StaffTravlFailDto dto) { + FtbPersonnelsStaffRoster roster = queryRosterInfoByUserId(userId); + + if (null != roster) { + FtbPersonnelsStaffRoster updateRoster = new FtbPersonnelsStaffRoster(); + updateRoster.setUserId(userId); + updateRoster.setWorkerStatus(dto.getStaffWorkerStatus().getCode()); + updateRoster.setId(roster.getId()); + updateRoster.setDepartDate(new Date()); + if (StringUtils.isNotEmpty(dto.getRemarks())) { + updateRoster.setTrialFail(dto.getRemarks()); + } + if (dto.getStaffWorkerStatus().getCode().equals(StaffWorkerStatus.PENDING_RESIGNATION.getCode())) { + updateRoster.setIsTrialFail(1); + } + + baseMapper.updateById(updateRoster); + + registrationFormDataService.updateForOneField(roster.getId(), roster.getPhone(), "workerStatus", dto.getStaffWorkerStatus().getCode()); + + if (dto.getStaffWorkerStatus().getCode().equals(StaffWorkerStatus.RESIGNED.getCode())) { + QueryWrapper removeWrapper = new QueryWrapper<>(); + removeWrapper.lambda().eq(FtbPersonnelsStaffEmploymentApply::getPhone, roster.getPhone()); + //同步状态到内推池子 + personnelOrgUtils.sysWorkerStatusToUchisuike(roster.getPhone(), StaffWorkerStatus.RESIGNED.getCode()); + staffEmploymentApplyService.remove(removeWrapper); + + + //同步离职日期 + SaveUserManagerAddDTO saveUserManagerAddDTO = new SaveUserManagerAddDTO(); + saveUserManagerAddDTO.setUserId(roster.getUserId()); + saveUserManagerAddDTO.setDepartDate(new Date()); + personnelOrgUtils.sysTenantAccountBase(saveUserManagerAddDTO); + + String s = personnelOrgUtils.dateToString(roster.getCreatorTime(), "yyyy-MM-dd"); + //员工成长 + dto.setDutyDate(personnelOrgUtils.stringDateToDate(s)); + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(userId); + addGrowthLogDto.setChangeDate(new Date()); + addGrowthLogDto.setGrowthType(GrowthLogEnum.PRE_TRAIL.getCode()); + StaffDepartDto staffDepartDto = StaffDepartDto.covert(dto); + addGrowthLogDto.setDetail(JSONUtil.toJsonStr(staffDepartDto)); + + // 试岗 + growthLogService.addGrowthLog(addGrowthLogDto); + + List list = registrationFormDataService.queryArchivalForm(userId); + list = removeOptionData(list); + FtbPersonnelsStaffArchivesHistory history = new FtbPersonnelsStaffArchivesHistory(); + history.setUserId(userId); + history.setArchives(JSONUtil.toJsonStr(list)); + history.setEnabledMark(1); + history.setCreatorTime(new Date()); + ftbPersonnelsStaffArchivesHistoryService.save(history); + + //离职清除老数据 + QueryWrapper formDataUpdateWrapper = new QueryWrapper<>(); + formDataUpdateWrapper.lambda().eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, roster.getId()); + registrationFormDataService.remove(formDataUpdateWrapper); + } + } + + } + + /** + * 修改员工司年龄 + */ + @Override + public void updateCompanyAge() { + Date now = new Date(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getActualStartDate) + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus, "305") + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + for (FtbPersonnelsStaffRoster roster : rosterList) { + if (null != roster.getActualStartDate() && StringUtils.isNotEmpty(roster.getPhone())) { + long betweenDay = 0L; + if (now.after(roster.getActualStartDate())) { + betweenDay = DateUtil.betweenDay(roster.getActualStartDate(), new Date(), false); + } + registrationFormDataService.updateForOneField(roster.getId(), roster.getPhone(), "companyAge", String.valueOf(betweenDay)); + } + } + } + } + + @Override + public void updateWorkerAge() { + Date now = new Date(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getActualStartDate) + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus, "305") + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + for (FtbPersonnelsStaffRoster roster : rosterList) { + FtbPersonnelsStaffRegistrationFormData formData = registrationFormDataService.queryOneFieldForFieldId(roster.getId(), "birthday"); + if (formData != null && StringUtils.isNotEmpty(formData.getValue())) { + String birthday = formData.getValue(); + Date birth = personnelOrgUtils.stringDateToDate(birthday); + int age = personnelOrgUtils.calculateAge(personnelOrgUtils.dateToLocalDate(birth)); + String currAge = age + "岁"; + registrationFormDataService.updateForOneField(roster.getId(), roster.getPhone(), "age", currAge); + } + } + } + } + + @Override + public List queryWithUserIds(StaffRosterListReq req) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + //wrapper.select(FtbPersonnelsStaffRoster::getUserId); + wrapper.in(CollectionUtil.isNotEmpty(req.getUserIds()), FtbPersonnelsStaffRoster::getUserId, req.getUserIds()); + wrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + wrapper.eq(StringUtils.isNotEmpty(req.getWorkerStatus()), FtbPersonnelsStaffRoster::getWorkerStatus, req.getWorkerStatus()); + List list = baseMapper.selectList(wrapper); + return list.stream().map(FtbPersonnelsStaffRosterDto::changeEscape).collect(Collectors.toList()); + } + + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public Boolean bindPhone(String userId, String phone) { + log.error("正常日志,bindPhone,userId={},phone={}", userId, phone); + //当前人员还未入花名册 + FtbPersonnelsStaffRoster roster = queryRosterInfoByUserId(userId); + FtbPersonnelsStaffRoster rosterPhone = queryRosterInfoByPhone(phone); + if (null != rosterPhone) { + if (!rosterPhone.getUserId().equals(userId)) { + log.error("错误日志,bindPhone,userId={},phone={},花名册中手机号已存在,不能绑定", userId, phone); + throw new RuntimeException("花名册中手机号已存在,不能绑定"); + } + } + if (null == roster) { + // 查询当前用户是否存在入职管理 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); + queryWrapper.or(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if (Objects.nonNull(one)) { + // 更改入职管理手机号 + LambdaUpdateWrapper wrapperApply = Wrappers.lambdaUpdate(); + wrapperApply.set(FtbPersonnelsStaffEmploymentApply::getPhone, phone); + wrapperApply.eq(FtbPersonnelsStaffEmploymentApply::getId, one.getId()); + staffEmploymentApplyService.update(new FtbPersonnelsStaffEmploymentApply(),wrapperApply); + // 获取旧手机号 + String onePhone = one.getPhone(); + //查询手机号字段F + LambdaQueryWrapper formDataLambdaQueryWrapper = Wrappers.lambdaQuery(); + formDataLambdaQueryWrapper.eq(FtbPersonnelsStaffRegistrationFormData::getPhone, onePhone); + List formData = registrationFormDataService.list(formDataLambdaQueryWrapper); + // 修改手机号 + List registrationFormData = formData.stream().map(v -> { + v.setPhone(phone); + if (v.getFormFieldId().equals("phone")) { + v.setValue(phone); + } + return v; + }).collect(Collectors.toList()); + registrationFormDataService.updateBatchById(registrationFormData); + } + }else { + // 查询当前用户是否存在入职管理 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); + queryWrapper.or(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if (Objects.nonNull(one)) { + // 更改入职管理手机号 + LambdaUpdateWrapper wrapperApply = Wrappers.lambdaUpdate(); + wrapperApply.set(FtbPersonnelsStaffEmploymentApply::getPhone, phone); + wrapperApply.eq(FtbPersonnelsStaffEmploymentApply::getId, one.getId()); + staffEmploymentApplyService.update(new FtbPersonnelsStaffEmploymentApply(),wrapperApply); + } + //修改手机号 + LambdaUpdateWrapper rosterLambdaUpdateWrapper = Wrappers.lambdaUpdate(); + rosterLambdaUpdateWrapper.set(FtbPersonnelsStaffRoster::getPhone, phone); + rosterLambdaUpdateWrapper.eq(FtbPersonnelsStaffRoster::getId, roster.getId()); + baseMapper.update(new FtbPersonnelsStaffRoster(), rosterLambdaUpdateWrapper); + //修改档案 + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.eq(FtbPersonnelsStaffRegistrationFormData::getRosterId, roster.getId()); + lambdaUpdate.set(FtbPersonnelsStaffRegistrationFormData::getPhone, phone); + registrationFormDataService.update(new FtbPersonnelsStaffRegistrationFormData(), lambdaUpdate); + + //查询手机号字段 + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = registrationFormDataService.queryOneField(phone, roster.getId(), "phone"); + + FtbPersonnelsStaffRegistrationFormData addSave; + if (ftbPersonnelsStaffRegistrationFormData != null) { + addSave = ftbPersonnelsStaffRegistrationFormData; + addSave.setValue(phone); + } else { + addSave = new FtbPersonnelsStaffRegistrationFormData(); + addSave.setRosterId(roster.getId()); + addSave.setPhone(phone); + addSave.setFormTypeId("1"); + addSave.setFormFieldId("phone"); + addSave.setValue(phone); + } + // 转正手机号修改 + LambdaUpdateWrapper reg = Wrappers.lambdaUpdate(); + reg.eq(FtbPersonnelsRegularManagement::getUserId, userId); + reg.set(FtbPersonnelsRegularManagement::getPhoneNumber,phone); + regularManagementService.update(new FtbPersonnelsRegularManagement(),reg); + // 离职手机号修改 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsTurnoverManagement::getPhoneNumber, phone); + wrapper.eq(FtbPersonnelsTurnoverManagement::getUserId, userId); + turnoverManagementMapper.update(new FtbPersonnelsTurnoverManagement(),wrapper); + registrationFormDataService.saveOrUpdate(addSave); + } + return true; + } + + /** + * 查询员工id + * + * @param userIds + * @return + */ + + @Override + public List queryWorkerIdByUserIds(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getSystemWokerId, FtbPersonnelsStaffRoster::getUserId + , FtbPersonnelsStaffRoster::getName) + .in(FtbPersonnelsStaffRoster::getUserId, userIds); + return baseMapper.selectList(wrapper); + + } + + /** + * 查询员工id + * + * @param userIds + * @return + */ + + @Override + public List queryUerIdByWorkIds(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getSystemWokerId, + FtbPersonnelsStaffRoster::getUserId + , FtbPersonnelsStaffRoster::getName) + .in(FtbPersonnelsStaffRoster::getUserId, userIds); + return baseMapper.selectList(wrapper); + + } + + /** + * 根据工人ID列表查询用户信息 + * + * 此方法旨在通过一个或多个工人ID来检索相应的用户信息它首先检查输入的工人ID列表是否为空, + * 如果为空,则返回一个空列表这样做是为了避免进行不必要的数据库查询当ID列表不为空时, + * 方法会构造一个查询条件,选择特定的字段,并根据系统工人ID进行查询 + * + * @param workerIds 工人ID列表,用于查询用户信息如果列表为空或null,将返回一个空列表 + * @return 返回一个包含查询到的用户信息的列表如果无任何匹配的记录,将返回一个空列表 + */ + @Override + public List queryUserInfoByWorkerId(List workerIds) { + // 检查输入的工人ID列表是否为空或null,如果为空,直接返回一个空列表 + if (CollectionUtil.isEmpty(workerIds)) { + return new ArrayList<>(); + } + // 构造查询条件,选择特定字段,并指定系统工人ID必须在给定的工人ID列表中 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getSystemWokerId, FtbPersonnelsStaffRoster::getUserId + , FtbPersonnelsStaffRoster::getName) + .in(FtbPersonnelsStaffRoster::getSystemWokerId, workerIds); + // 执行查询并返回结果列表 + return baseMapper.selectList(wrapper); + } + + + + /** + * 根据用户ID查询司龄 + * + * @param userIds 用户id + * @return + */ + @Override + public List queryCompanyAge(List userIds) { + List retList = new ArrayList<>(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getActualStartDate) + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + for (FtbPersonnelsStaffRoster roster : rosterList) { + QueryCompanyAgeDto dto = new QueryCompanyAgeDto(); + dto.setUserId(roster.getUserId()); + //查询司龄 + String companyAge = registrationFormDataService.queryOneFieldValue(roster.getPhone(), roster.getId(), "companyAge"); + if (StringUtils.isNotEmpty(companyAge)) { + dto.setConpanyAge(Long.valueOf(companyAge)); + } else { + dto.setConpanyAge(0L); + } + //查询工龄 + String workAge = registrationFormDataService.queryOneFieldValue(roster.getPhone(), roster.getId(), "workAge"); + if (StringUtils.isNotEmpty(workAge)) { + dto.setWorkerAge(Long.valueOf(workAge)); + } else { + dto.setWorkerAge(0L); + } + //入职日期 + String actualStartDate = registrationFormDataService.queryOneFieldValue(roster.getPhone(), roster.getId(), "actualStartDate"); + if (StringUtils.isNotEmpty(actualStartDate)) { + dto.setActualStartDate(actualStartDate); + } + //生日 + String birthday = registrationFormDataService.queryOneFieldValue(roster.getPhone(), roster.getId(), "birthday"); + if (StringUtils.isNotEmpty(birthday)) { + dto.setBirthday(birthday); + } + //员工名称 + String workerName = registrationFormDataService.queryOneFieldValue(roster.getPhone(), roster.getId(), "workerName"); + if (StrUtil.isNotBlank(workerName)) { + dto.setWorkerName(workerName); + } + retList.add(dto); + } + } + return retList; + } + + @Override + public List queryAllUserForOrgAndStatus(QueryUserListDTO dto) { + String organizeId = dto.getOrganizeId(); + ActionResult> actionResult = v2UserApi.listTargetOrganizes(List.of(organizeId), null,null); + if (actionResult == null || CollUtil.isEmpty(actionResult.getData())) { + return new ArrayList<>(); + } + List userListVOS = actionResult.getData(); + //获取所有用户ID + List userIds = userListVOS.stream().map(UserPageListVO::getId).collect(Collectors.toList()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return new ArrayList<>(); + } + //转换成userid 的map + Map rosterMap = rosterList.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, Function.identity())); + + List newList = new ArrayList<>(); + for (UserPageListVO vo : userListVOS) { + FtbPersonnelsStaffRoster roster = rosterMap.get(vo.getId()); + if (null == roster) { + continue; + } + if (roster.getEnabledMark() == 1) { + continue; + } + if ("304".equals(roster.getWorkerStatus()) || "305".equals(roster.getWorkerStatus())) { + continue; + } + newList.add(vo); + } + + return newList; + } + + /** + * 检测是否档案管理员 + * + * @param loginUserId + * @return + */ + @Override + public Boolean checkMyIsArchiveManager(String loginUserId) { + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(loginUserId); + List authList = personnelsPermissionsService.queryPermissionList(userInfo, "Employeer:list", 0); + if (CollectionUtil.isEmpty(authList)) { + return false; + } + if (authList.contains("Employeer:update")) { + return true; + } + return false; + } + + + /** + * 检测是否档案管理员 + * + * @return + */ + @Override + public Boolean checkCurrLoginUserArchiveManager() { + UserInfo user = UserProvider.getUser(); + List authList = personnelsPermissionsService.queryPermissionList(user, "Employeer:list", 0); + if (CollectionUtil.isEmpty(authList)) { + return false; + } + if (authList.contains("Employeer:update")) { + return true; + } + return false; + } + + @Override + public Integer checksItInTheReviewProcess(String id) { + FtbPersonnelsStaffRoster roster = queryRosterAndCheckById(id); + String checkFlag = ftbPersonnelsAuditRunTaskService.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(roster.getUserId()); + if (StringUtils.isNotEmpty(checkFlag)) { + return Integer.valueOf(checkFlag); + } + return 0; + } + + + private CheckRosterDeleteVo convertToCheckRosterDeleteVo(FtbPersonnelsStaffRoster roster, Integer noDeleteType, Boolean canDelete) { + CheckRosterDeleteVo checkRosterDeleteVo = new CheckRosterDeleteVo(); + checkRosterDeleteVo.setUserId(roster.getUserId()); + checkRosterDeleteVo.setPhone(roster.getPhone()); + checkRosterDeleteVo.setName(roster.getName()); + checkRosterDeleteVo.setWorkerStatus(roster.getWorkerStatus()); + checkRosterDeleteVo.setNoDeleteType(noDeleteType); + checkRosterDeleteVo.setRosterId(roster.getId()); + checkRosterDeleteVo.setCanDelete(canDelete); + List list = personnelOrgUtils.getUserOrgBoundInfo(roster.getUserId()); + checkRosterDeleteVo.setOrgList(list); + return checkRosterDeleteVo; + } + + private InnerImportResultVO addImportOneRoster(FtbRosterInsertNomalVO ftbRosterInsertNomalVO, FtbPersonnelsStaffRoster entity, FtbPersonnelsStaffRoster oldRoster, + UserInfo userInfo, Map phoneUserMap, Map userToHeadMap, Map allFormFieldListMap) { + Map innerFieldValueCacheMap = ftbRosterInsertNomalVO.getInnerFieldValueCacheMap(); + String probationPeriod = innerFieldValueCacheMap.get("probationPeriod"); + if (StringUtils.isEmpty(probationPeriod)) { + probationPeriod = "100"; + } + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(probationPeriod, "0"); + Date now = new Date(); + InnerImportResultVO resultVO = new InnerImportResultVO(); + String userId = phoneUserMap.get(entity.getPhone()); + if (StringUtils.isEmpty(userId)) { + throw new RuntimeException("姓名:" + entity.getName() + ",手机号:" + entity.getPhone() + ",租户账户生成失败"); + } + entity.setUserId(userId); + //查询花名册中是否已经有数据 + if (null != oldRoster) { + //重复入职 + entity.setId(oldRoster.getId()); + if (null != oldRoster.getJoinNum()) { + entity.setJoinNum(oldRoster.getJoinNum() + 1); + } else { + entity.setJoinNum(1); + } + } else { + //第一次入职 + entity.setJoinNum(1); + } + //生成员工ID + entity.setSystemWokerId(generateSystemWorkerId()); + + //写入数据库 + entity.setEnabledMark(0); + entity.setIsSubmitForm(1); + entity.setContractStatus(FContractSignStatus.NOT_INITIATED.getCode()); + entity.setIsTrialJob(0); + entity.setTrialJobDay(0); + String planProbationaryDate = "";//计划转正日期 + String actualProbationaryDate = "";//实际转正日期 + if (StaffWorkerStatus.TRIAL.getCode().equals(entity.getWorkerStatus())) { + int month = personnelOrgUtils.calMonth(probationPeriodDto); + Date toDay = DateUtil.offsetMonth(entity.getActualStartDate(), month); + String toDayString = personnelOrgUtils.dateToString(toDay, "yyyy-MM-dd"); + planProbationaryDate = toDayString; + actualProbationaryDate = ""; + + } else if (StaffWorkerStatus.FULL_TIME.getCode().equals(entity.getWorkerStatus())) { + entity.setActualProbationaryDate(entity.getActualStartDate()); + String toDayString = personnelOrgUtils.dateToString(entity.getActualStartDate(), "yyyy-MM-dd"); + planProbationaryDate = toDayString; + actualProbationaryDate = toDayString; + } + entity.setIsTrialFail(0); + + entity.setTrialFail(""); + entity.setIsSignSeparation(0); + entity.setCurrSalary(new BigDecimal(0)); + + if (StringUtils.isEmpty(entity.getId())) { + baseMapper.insert(entity); + } else { + baseMapper.updateById(entity); + } + registrationFormDataService.addRosterIdToFormData(entity.getPhone(), entity.getId()); + + //同步数据到formData + Map formDataMap = BeanUtil.beanToMap(entity); + //同步到字段动态表 + syncImportFormData(ftbRosterInsertNomalVO, formDataMap); + String headLogo = userToHeadMap.get(userId); + List headList = new ArrayList<>(); + headList.add(headLogo); + formDataMap.put("headLogo", JSONUtil.toJsonStr(headList)); + formDataMap.put("actualStartDate", innerFieldValueCacheMap.get("actualStartDate")); + formDataMap.put("planProbationaryDate", planProbationaryDate); + + if (StaffWorkerStatus.TRIAL.getCode().equals(entity.getWorkerStatus())) { + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + } else if (StaffWorkerStatus.FULL_TIME.getCode().equals(entity.getWorkerStatus())) { + probationPeriodDto = new ProbationPeriodDto("100", "0"); + formDataMap.put("probationPeriod", JSONUtil.toJsonStr(probationPeriodDto)); + } + String workAddress = innerFieldValueCacheMap.get("workAddress"); + WorkAddressDto workAddressDto = new WorkAddressDto(); + workAddressDto.setAddress(workAddress); + formDataMap.put("workAddress", JSONUtil.toJsonStr(workAddressDto)); + formDataMap.put("actualProbationaryDate", actualProbationaryDate); + + if (null != entity.getActualStartDate()) { + if (now.after(entity.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(entity.getActualStartDate(), new Date(), false); + formDataMap.put("companyAge", String.valueOf(betweenDay)); + } else { + formDataMap.put("companyAge", "0"); + } + + } + addRunBirthday(formDataMap); + asyncUpdateFormData(formDataMap, entity.getPhone(), entity.getId(), allFormFieldListMap); + asyncDealImportRosterOtherData(entity, userInfo, probationPeriodDto); + resultVO.setSuccess(true); + return resultVO; + } + + private void addRunBirthday(Map formDataMap) { + // 身份证号码识别出生日期 + if (Objects.nonNull(formDataMap.get("idCardNum"))) { + Date birthday = IdcardUtil.getBirthDate(formDataMap.get("idCardNum").toString()).toJdkDate(); + formDataMap.put("birthday", DateUtil.format(birthday, "yyyy-MM-dd")); + } + String birthday = (String) formDataMap.get("birthday"); + if (StringUtils.isNotEmpty(birthday)) { + Date birth = personnelOrgUtils.stringDateToDate(birthday); + int age = personnelOrgUtils.calculateAge(personnelOrgUtils.dateToLocalDate(birth)); + String currAge = age + "岁"; + formDataMap.put("age", currAge); + } + } + + + private void asyncUpdateFormData(Map formDataMap, String phone, String rosterId, Map allFormFieldListMap) { + String tenantId = personnelOrgUtils.getTenantId(); + Map header = personnelOrgUtils.getHeadersForLogin(); + personnelAsyncImportUtils.asyncUpdateFormData(formDataMap, phone, rosterId, allFormFieldListMap, tenantId, header); + } + + private void asyncDealImportRosterOtherData(FtbPersonnelsStaffRoster entity, UserInfo userInfo, ProbationPeriodDto probationPeriodDto) { + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + if (StringUtils.isEmpty(token)) { + token = ""; + } + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + personnelAsyncImportUtils.asyncDealImportRosterOtherData(entity, probationPeriodDto, tenantId, headers); + } + + private String getUserHeadLog(Map formMap, FtbPersonnelsStaffRoster entity) { + String headLogo = personnelOrgUtils.getFormDataForField(formMap, "headLogo"); + if (StringUtils.isEmpty(headLogo) || "[]".equals(headLogo)) { + headLogo = generaDefaultUserHeadLog(entity); + } else { + List headList = JSONUtil.toList(headLogo, String.class); + if (CollectionUtil.isNotEmpty(headList)) { + headLogo = headList.get(0); + } else { + headLogo = ""; + } + if (StringUtils.isEmpty(headLogo)) { + headLogo = generaDefaultUserHeadLog(entity); + } + } + if (StringUtils.isEmpty(headLogo)) { + headLogo = DEFAULT_USER_LOGO; + } + return headLogo; + } + + private String generaDefaultUserHeadLog(FtbPersonnelsStaffRoster entity) { + String headLogo = DEFAULT_USER_LOGO; + try { + ActionResult headResult = pTenantAccountApi.generateDefaultAvatar(entity.getName()); + log.error("生成默认头像,name={},result={}", entity.getName(), JSONUtil.toJsonStr(headResult)); + if (null != headResult) { + headLogo = (String) headResult.getData(); + registrationFormDataService.updateForOneField(entity.getPhone(), "headLogo", headLogo); + } + } catch (IOException e) { + log.error("生成默认头像失败", e); + } + return headLogo; + } + + public List> splitIntoBatches(List userAccountDtoList, Integer batchSize) { + List> batches = new ArrayList<>(); + for (int i = 0; i < userAccountDtoList.size(); i += batchSize) { + int end = Math.min(i + batchSize, userAccountDtoList.size()); + batches.add(new ArrayList<>(userAccountDtoList.subList(i, end))); + } + return batches; + } + + + private String buildGrowthLogDetail(String currOrg, String currPosition, String currRank, String currReportsTo, Date actualStartDate) { + StaffBaseInfoDto dto = new StaffBaseInfoDto(); + dto.setCurrOrg(currOrg); + dto.setCurrPosition(currPosition); + dto.setCurrRank(currRank); + dto.setReportsTo(currReportsTo); + personnelOrgUtils.fillOrgNameById(dto); + personnelOrgUtils.fillPositionNameById(dto); + personnelOrgUtils.fillRankNameById(dto); + UserEntity userEntity = personnelOrgUtils.queryUserInfo(currReportsTo); + if (null != userEntity) { + dto.setReportsToName(userEntity.getRealName()); + } + dto.setActualStartDate(actualStartDate); + return JSONUtil.toJsonStr(dto); + } + + + /** + * 根据手机号查询花名册信息 + * + * @param phone + * @return + */ + + public FtbPersonnelsStaffRoster queryRosterInfoByPhone(String phone) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getPhone, phone); +// wrapper.lambda().last("limit 1"); +// wrapper.lambda().ne(FtbPersonnelsStaffRoster::getWorkerStatus,"305"); +// wrapper.lambda().eq(FtbPersonnelsStaffRoster::getEnabledMark,0); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + return entity; + } + + /** + * 更加手机号批量查询花名册 + * + * @param phones 手机号集合 + * @return + */ + public List queryRosterInfoByPhones(List phones) { + QueryWrapper wrapper = new QueryWrapper<>(); +// wrapper.lambda().in(FtbPersonnelsStaffRoster::getPhone, phones).eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + wrapper.lambda().in(FtbPersonnelsStaffRoster::getPhone, phones); + return baseMapper.selectList(wrapper); + + } + + /** + * 根据用户ID查询花名册信息 + * + * @param userId 用户ID + * @return + */ + + public FtbPersonnelsStaffRoster queryRosterInfoByUserId(String userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getUserId, userId); + FtbPersonnelsStaffRoster entity = baseMapper.selectOne(wrapper); + return entity; + } + + /** + * 根据员工工号查询花名册信息 + * + * @param workderId 员工工号 + * @return + */ + public List queryRosterInfoByWorkerId(String workderId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getWorkerNo, workderId); + return baseMapper.selectList(wrapper); + + } + + /** + * 根据员工系统id查询员工 + * @param systemId 系统id + * @return 员工花名册列表 + */ + @Override + public List queryRosterInfoBySystemId(String systemId) { + if (StringUtils.isEmpty(systemId)) { + return new ArrayList<>(); + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(FtbPersonnelsStaffRoster::getSystemWokerId, systemId); + return baseMapper.selectList(wrapper); + } + + + + @Override + public FtbCultivatePromotionWithPersonelVO queryTrainData(String userId, String postId) { + return promotionService.queryTrainData(userId, postId); + } + + /** + * 统计员工数量 + * 在职员工:工作状态为“试用+正式+待离职” + * 全职员工:员工类型为“全职” + * 兼职员工:员工类型为“兼职” + * 实习员工:合同类型为“实习协议” + * 待离职员工:工作状态为“待离职” + * 正式员工:工作状态为“正式” + * 试用员工:工作状态为“试用” + * + * @return + */ + @Override + public WorkerStatisticsDto statisticsWorkerNumber() { + List userIds = queryCacheAuthUserList(null, null); + + WorkerStatisticsDto dto = new WorkerStatisticsDto(); + if (userIds != null && userIds.isEmpty()) { + return dto; + } + //在职 + Long jobNum = baseMapper.selectCount((new QueryWrapper()).lambda() + .in(FtbPersonnelsStaffRoster::getWorkerStatus, "302", "303", "304", "306") + .in(CollUtil.isNotEmpty(userIds), FtbPersonnelsStaffRoster::getUserId, userIds) + .ne(FtbPersonnelsStaffRoster::getUserId,"349057407209541") + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + ); + dto.setJobNum(jobNum); + + /** + * 实习员工数量 + */ + Long practiceNum = baseMapper.selectCount((new QueryWrapper()).lambda() + .eq(FtbPersonnelsStaffRoster::getWorkerType, "28") + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus, "305") + .in(CollUtil.isNotEmpty(userIds), FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + ); + dto.setPracticeNum(practiceNum); + + /** + * 正式员工数量 + */ + Long formalNum = baseMapper.selectCount((new QueryWrapper()).lambda() + .eq(FtbPersonnelsStaffRoster::getWorkerStatus, "303") + .in(CollUtil.isNotEmpty(userIds), FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + ); + dto.setFormalNum(formalNum); + + + /** + * 试用员工数量 + */ + Long waitJoinNum = baseMapper.selectCount((new QueryWrapper()).lambda() + .eq(FtbPersonnelsStaffRoster::getWorkerStatus, "302") + .in(CollUtil.isNotEmpty(userIds), FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + ); + dto.setWaitJoinNum(waitJoinNum); + + return dto; + } + + + @Override + public StaffHomeDto queryWorkerHomeDetail(String userId) { + FtbPersonnelsStaffRoster entity = queryRosterInfoByUserId(userId); + if (null == entity) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId,userId); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 0); + queryWrapper.notIn(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 2); + queryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if(Objects.nonNull(one)){ + throw new RuntimeException("该员工未办理入职,暂无员工档案"); + } + throw new RuntimeException("花名册数据不存在"); + } + StaffHomeDto dto = BeanUtil.copyProperties(entity, StaffHomeDto.class); + // 补充工作状态转移 + UserEntity userEntity = personnelOrgUtils.queryUserInfo(dto.getCurrReportsTo()); + if (null != userEntity) { + dto.setCurrReportsToName(userEntity.getRealName()); + } + if (entity.getWorkerStatus().equals("305")) { + // 如果员工离职 取离职档案信息 + LambdaQueryWrapper archivesHistoryLambdaQueryWrapper = new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffArchivesHistory::getUserId, userId) + .eq(FtbPersonnelsStaffArchivesHistory::getEnabledMark, 1) + .last("limit 1"); + FtbPersonnelsStaffArchivesHistory archivesHistory = staffArchivesHistoryMapper.selectOne(archivesHistoryLambdaQueryWrapper); + if (null != archivesHistory) { + List list = JSONUtil.toList(archivesHistory.getArchives(), FormTypeDto.class); + encapsulationParameters(list, dto); + } + } else { + // 补充直属主管 + //补充dataform数据 + List formDataList = registrationFormDataService.queryFormFieldValueList(entity.getPhone(), entity.getId()); + personnelOrgUtils.fillRosterDtoData(dto, formDataList, true); + } + + List list = personnelOrgUtils.getUserOrgBoundInfo(dto.getUserId()); + dto.setOrgList(list); + if (CollUtil.isNotEmpty(list)) { + WorkerGroupDataDto groupDataDto = list.stream().filter(WorkerGroupDataDto::getIsDefault).findFirst().orElse(new WorkerGroupDataDto()); + dto.setReportsTo(groupDataDto.getReportsTo()); + dto.setReportsToName(groupDataDto.getReportsToName()); + dto.setCurrOrg(groupDataDto.getAffiliatedOrg()); + dto.setCurrOrgName(groupDataDto.getAffiliatedOrgName()); + dto.setCurrPosition(groupDataDto.getAffiliatedPosition()); + dto.setCurrPositionName(groupDataDto.getAffiliatedPositionName()); + dto.setCurrRank(groupDataDto.getAffiliatedRank()); + dto.setCurrRankName(groupDataDto.getAffiliatedRankName()); + dto.setCurrReportsTo(groupDataDto.getReportsTo()); + dto.setCurrReportsToName(groupDataDto.getReportsToName()); + dto.setCurrGroupName(groupDataDto.getStoreTeamName()); + dto.setCurrGroupId(groupDataDto.getStoreTeamId()); + } + if (StrUtil.isNotBlank(dto.getBirthday())) { + Date dateBirthday = DateUtil.parse(dto.getBirthday()); + long age = DateUtil.betweenYear(dateBirthday, DateUtil.date(), false); + dto.setAge(Long.toString(age)); + } else { + dto.setAge("0"); + } + List ftbPersonnelsSalaryInfos = personnelSalaryServiceImpl.innerGetTheCurrentPersonSSalary(entity, "", ""); + if (CollectionUtil.isNotEmpty(ftbPersonnelsSalaryInfos)) { + ftbPersonnelsSalaryInfos.stream() + .filter(item -> item.getFSetValue().matches("^\\d+(\\.\\d+)?$")) + .map(item -> + BigDecimal.valueOf(Double.parseDouble(item.getFSetValue()))).reduce(BigDecimal::add) + .ifPresentOrElse(item -> + dto.setCurrSalary(item.setScale(2, BigDecimal.ROUND_HALF_UP)), + () -> dto.setCurrSalary(BigDecimal.ZERO)); + + } + FtbPersonnelsStaffRosterDto.changeEscape(dto); + return dto; + } + + /** + * 数据封装 + * + * @param list + * @param dto + */ + private void encapsulationParameters(List list, StaffHomeDto dto) { + FormTypeDto formTypeDto = list.stream().filter(vo -> "1".equals(vo.getId())).findFirst().orElse(null); + if (formTypeDto == null) return; + List fieldDataDtoList = formTypeDto.getGroupFieldDataDtoList(); + fieldDataDtoList.forEach(item -> { + FormFieldDto formFieldDto = item.getFormFieldDto(); + if ("idCardNum".equals(formFieldDto.getId())) { + dto.setIdCardNum(formFieldDto.getUserValue()); + } else if ("workerSex".equals(formFieldDto.getId())) { + dto.setWorkerSex(formFieldDto.getUserValueLable()); + } else if ("birthday".equals(formFieldDto.getId())) { + dto.setBirthday(formFieldDto.getUserValue()); + } + }); + + } + + @Override + public FtbPersonnelsStaffRosterDto getInfo(String id) { + FtbPersonnelsStaffRoster entity = queryRosterAndCheckById(id); + + FtbPersonnelsStaffRosterDto dto = BeanUtil.copyProperties(entity, FtbPersonnelsStaffRosterDto.class); + //补充组织 岗位 职等 信息 推荐人、推荐门店 + personnelOrgUtils.fillCurrOrgName(Collections.singletonList(dto)); + personnelOrgUtils.fillCurrPositionName(Collections.singletonList(dto)); + personnelOrgUtils.fillCurrRankName(Collections.singletonList(dto)); + fillContractTypeName(Collections.singletonList(dto)); + UserEntity userEntity = personnelOrgUtils.queryUserInfo(dto.getCurrReportsTo()); + if (null != userEntity) { + dto.setCurrReportsToName(userEntity.getRealName()); + } + //补充dataform数据 + List formDataList = registrationFormDataService.queryFormFieldValueList(entity.getPhone(), entity.getId()); + personnelOrgUtils.fillRosterDtoData(dto, formDataList, true); + + List list = personnelOrgUtils.getUserOrgBoundInfo(dto.getUserId()); + dto.setOrgList(list); + //兼容老数据 + //dealCurrOrgInfo(list, dto); + + //绑定的考勤组信息 + attendanceGroupService.getAttendanceUserGroup(List.of(entity.getUserId())).stream().findFirst().ifPresent(group -> { + dto.setAttendanceGroupName(group.getGroupName()); + }); + return dto; + + } + + /** + * 查询未填写入职登记表的用户 + * + * @return + */ + + @Override + public List queryNoSubmitForm() { + + + List retList = new ArrayList<>(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffRoster::getIsSubmitForm, 0) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return retList; + } + retList = BeanUtil.copyToList(rosterList, StaffRosterInfoDto.class); + return retList; + } + + /** + * 查询健康证到期的人 + * + * @param days 天数 + * @param months 月数 + * @return + */ + @Override + public List queryHealthExpire(Long days, Long months) { + if (days == null && months == null) { + return new ArrayList<>(); + } + List retList = new ArrayList<>(); + Map expireFormDataMap = new HashMap<>(); + //查询并检测是否过期 + List formDataList = registrationFormDataService.queryHasHealthExpire(); + + if (CollectionUtil.isNotEmpty(formDataList)) { + for (FtbPersonnelsStaffRegistrationFormData formData : formDataList) { + if (StringUtils.isEmpty(formData.getValue()) || StringUtils.isEmpty(formData.getRosterId())) { + continue; + } + // 将目标日期字符串解析为日期对象 + Date targetDate = DateUtil.parse(formData.getValue()); + // 获取当前日期 + Date currentDate = DateUtil.beginOfDay(DateUtil.date()); + // 判断目标日期是否尚未到来,未到来才提醒 + if (targetDate.after(currentDate)) { + + if (days != null && months == null) { + // 计算距离现在还有多少天 + long daysDiff = DateUtil.betweenDay(currentDate, targetDate, false); + if (daysDiff == days) { + expireFormDataMap.put(formData.getRosterId(), formData); + } + } else if (days == null) { + // 获取*个月的日期 + Date nextMonth = DateUtil.offsetMonth(currentDate, months.intValue()); + if (DateUtil.isSameDay(nextMonth, targetDate)) { + expireFormDataMap.put(formData.getRosterId(), formData); + } + } else { + // 计算距离现在还有多少天 + long daysDiff = 0; + try { + daysDiff = DateUtil.betweenDay(currentDate, targetDate, false); + } catch (Exception e) { + log.error("健康证,计算距离现在还有多少天异常currentDate[{}],targetDate[{}], RosterId[{}]", currentDate, targetDate, formData.getRosterId()); + } + if (daysDiff == days) { + expireFormDataMap.put(formData.getRosterId(), formData); + } + // 月 + Date nextMonth = DateUtil.offsetMonth(currentDate, months.intValue()); + if (DateUtil.isSameDay(nextMonth, targetDate)) { + expireFormDataMap.put(formData.getRosterId(), formData); + } + } + + } else { + if (days != null && months == null) { + if (days == 0L) { + // 计算距离现在还有多少天 + long daysDiff = DateUtil.betweenDay(currentDate, targetDate, false); + if (daysDiff == days) { + expireFormDataMap.put(formData.getRosterId(), formData); + } + } + } else if (days == null) { + if (months == 0L) { + // 获取*个月的日期 + Date nextMonth = DateUtil.offsetMonth(currentDate, months.intValue()); + if (DateUtil.isSameDay(nextMonth, targetDate)) { + expireFormDataMap.put(formData.getRosterId(), formData); + } + } + } + } + } + } + + //查询过期的用户信息 + if (!expireFormDataMap.isEmpty()) { + //获取花名册用户ID + List rosterIds = expireFormDataMap.keySet().stream().collect(Collectors.toList()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbPersonnelsStaffRoster::getId, rosterIds) + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus, "305") + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + retList = BeanUtil.copyToList(rosterList, StaffRosterInfoDto.class); + for (StaffRosterInfoDto staffRosterInfoDto : retList) { + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = expireFormDataMap.get(staffRosterInfoDto.getId()); + if (null != ftbPersonnelsStaffRegistrationFormData) { + staffRosterInfoDto.setEndHealthDate(ftbPersonnelsStaffRegistrationFormData.getValue()); + } + } + } + } + return retList; + } + + + /** + * 获取今日的日期 去掉 时分秒 + * + * @return + */ + private Date getTodayDate() { + Calendar calendar = Calendar.getInstance(); + // 设置时间为零点零分零秒 + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // 获取调整后的时间 + return calendar.getTime(); + } + + + @Override + public ActionResult> getPartUserInfoPage(UserQueryDto queryDto) { + ActionResult> ret = new ActionResult<>(); + PageListVO retPage = new PageListVO<>(); + + ActionResult> partUserInfoPage = personnelOrgUtils.getPartUserInfoPage(queryDto); + ret.setData(retPage); + ret.setCode(partUserInfoPage.getCode()); + ret.setMsg(partUserInfoPage.getMsg()); + PageListVO page = partUserInfoPage.getData(); + if (null != page) { + List list = page.getList(); + retPage.setPagination(page.getPagination()); + if (CollectionUtil.isNotEmpty(list)) { + //获取用户ID列表 + List userIds = list.stream().map(PartUserInfoVo::getUserId).collect(Collectors.toList()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + //rosterList转换成userID 的map + Map rosterMap = rosterList.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, roster -> roster)); + + List partUserInfoVoAndWorkerNos = BeanUtil.copyToList(list, PartUserInfoVoAndWorkerNo.class); + for (PartUserInfoVoAndWorkerNo partUserInfoVoAndWorkerNo : partUserInfoVoAndWorkerNos) { + FtbPersonnelsStaffRoster roster = rosterMap.get(partUserInfoVoAndWorkerNo.getUserId()); + if (null != roster) { + partUserInfoVoAndWorkerNo.setWorkerNo(roster.getWorkerNo()); + partUserInfoVoAndWorkerNo.setSystemWokerId(roster.getSystemWokerId()); + } + } + retPage.setList(partUserInfoVoAndWorkerNos); + + } + } + return ret; + } + + + /** + * 登出 + * + * @param tenantCode 租户编码 + * @param userId 用户id + */ + @Override + public void logout(String tenantCode, String userId) { + String loginId = tenantCode + ":" + userId; + log.error("离职推出,loginId={}", loginId); + StpUtil.logout(loginId); + } + + /** + * 发送入职登记表邀请消息 + * + * @param userId 目标用户 + * @return 执行位置 + */ + @Override + public Boolean sendInvitationInformation(String userId) { + //获取邀请 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + UserInfo userInfo = UserProvider.getUser(); + String currJoinUrl = joinUrl + "&phone=" + one.getPhone(); + String storeCode = personnelOrgUtils.queryCodeForTenantId(userInfo.getTenantId()); + UserAndLinkDTO userAndLinkDTO = new UserAndLinkDTO( + userId, + String.format(currJoinUrl, userInfo.getTenantId(), storeCode) + ); + // 不需要邀请填写 + // imMessageService.inviteToJoin(List.of(userAndLinkDTO)); + + return true; + } + + /** + * APP端修改用户基本信息【头像 姓名 化名】 同步给组织架构 + * + * @param userId 用户id + * @param dto + */ + @Override + @Transactional + public void updateBaseUserInfo(String userId, EditUserBaseInfoDto dto) { + FtbPersonnelsStaffRoster rosterEntity = queryRosterInfoByUserId(userId); + String name = dto.getName(); + String flowerName = dto.getFlowerName(); + String headLogo = dto.getHeadLogo(); + + if(rosterEntity == null){ + updateNotExistRoster(userId, dto); + return; + } + List formDataList = new ArrayList<>(); + FtbPersonnelsStaffRoster saveRoster = new FtbPersonnelsStaffRoster(); + saveRoster.setId(rosterEntity.getId()); + SaveUserManagerAddDTO saveUserManagerAddDTO = new SaveUserManagerAddDTO(); + saveUserManagerAddDTO.setUserId(rosterEntity.getUserId()); + + if (StringUtils.isNotEmpty(name)) { + saveUserManagerAddDTO.setRealName(name); + saveRoster.setName(name); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setRosterId(rosterEntity.getId()); + formData.setPhone(rosterEntity.getPhone()); + formData.setFormTypeId("1"); + formData.setFormFieldId("workerName"); + formData.setValue(name); + formDataList.add(formData); + } + if (StringUtils.isNotEmpty(headLogo)) { + saveUserManagerAddDTO.setHeadIcon(headLogo); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setRosterId(rosterEntity.getId()); + formData.setPhone(rosterEntity.getPhone()); + formData.setFormTypeId("1"); + formData.setFormFieldId("headLogo"); + List headList = new ArrayList<>(); + headList.add(headLogo); + formData.setValue(JSONUtil.toJsonStr(headList)); + formDataList.add(formData); + } + if (StringUtils.isNotEmpty(flowerName)) { + saveUserManagerAddDTO.setNickName(flowerName); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setRosterId(rosterEntity.getId()); + formData.setPhone(rosterEntity.getPhone()); + formData.setFormTypeId("1"); + formData.setFormFieldId("flowerName"); + formData.setValue(flowerName); + formDataList.add(formData); + } + //修改花名册 + if (StringUtils.isNotEmpty(name)) { + baseMapper.updateById(saveRoster); + } + + //修改花名册附件信息 + List addFormDataList = new ArrayList<>(); + List editFormDataList = new ArrayList<>(); + for (FtbPersonnelsStaffRegistrationFormData item : formDataList) { + FtbPersonnelsStaffRegistrationFormData oldFormData = registrationFormDataService.queryOneFieldForFieldId(item.getRosterId(), item.getFormFieldId()); + if (null != oldFormData) { + item.setId(oldFormData.getId()); + editFormDataList.add(item); + } else { + addFormDataList.add(item); + } + } + if (CollectionUtil.isNotEmpty(addFormDataList)) { + registrationFormDataService.saveBatch(addFormDataList); + } + + if (CollectionUtil.isNotEmpty(editFormDataList)) { + registrationFormDataService.updateBatchById(editFormDataList); + } + + //同步信息permiss + personnelOrgUtils.sysTenantAccountBase(saveUserManagerAddDTO); + //同步平台 + if (StringUtils.isNotEmpty(headLogo)) { + personnelOrgUtils.sysUserHeadLog(rosterEntity.getUserId(), headLogo); + } + + + } + + /** + * 修改用户未到花名册的情况 + * @param userId 用户id + * @param dto 请求对象 + */ + private void updateNotExistRoster(String userId, EditUserBaseInfoDto dto) { + SaveUserManagerAddDTO saveUserManagerAddDTO = new SaveUserManagerAddDTO(); + saveUserManagerAddDTO.setUserId(userId); + if (StringUtils.isNotEmpty(dto.getName())) { + saveUserManagerAddDTO.setRealName(dto.getName()); + } + if (StringUtils.isNotEmpty(dto.getHeadLogo())) { + saveUserManagerAddDTO.setHeadIcon(dto.getHeadLogo()); + } + if (StringUtils.isNotEmpty(dto.getFlowerName())) { + saveUserManagerAddDTO.setNickName(dto.getFlowerName()); + } + //同步信息permiss + personnelOrgUtils.sysTenantAccountBase(saveUserManagerAddDTO); + //同步平台 + if (StringUtils.isNotEmpty(dto.getHeadLogo())) { + personnelOrgUtils.sysUserHeadLog(userId, dto.getHeadLogo()); + } + } + + /** + * 根据提供的用户id 查询用户是否离职 + * + * @param userIds 用户id集合 + * @return + */ + @Override + public List queryDepartUser(List userIds) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getWorkerStatus, StaffWorkerStatus.RESIGNED.getCode()); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isEmpty(rosterList)) { + return Collections.emptyList(); + } + return BeanUtil.copyToList(rosterList, FtbPersonnelsStaffRosterDto.class); + } + + @Override + public List queryUserBaseInfoAndOrgAndPosAndRank(List userIds, String tenantId) { + List rosterList = this.lambdaQuery() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getName, + FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getWorkerStatus) + .in(FtbPersonnelsStaffRoster::getUserId, userIds) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .orderByAsc(FtbPersonnelsStaffRoster::getCreatorTime) + .list(); + List formDataList = CollUtil.isNotEmpty(rosterList) ? registrationFormDataService.lambdaQuery() + .in(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterList.stream().map(FtbPersonnelsStaffRoster::getId).collect(Collectors.toList())) + .in(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, "flowerName", "headLogo") + .list() : Lists.newArrayList(); + Map> formDataMap = CollUtil.isNotEmpty(formDataList) ? + formDataList.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)) : new HashMap<>(); + + List retList = personnelOrgUtils.queryUserBaseInfoAndOrgAndPosAndRank(userIds, tenantId); + Map userOrgMap = CollUtil.isNotEmpty(retList) ? + retList.stream().collect(Collectors.toMap(StaffRosterSimpleBaseInfoDto::getUserId, a -> a, (k1, k2) -> k1)) : new HashMap<>(); + return CollUtil.isNotEmpty(rosterList) ? rosterList.stream().map(roster -> { + StaffRosterSimpleBaseInfoDto baseInfoDto = StaffRosterSimpleBaseInfoDto.builder().build(); + BeanUtil.copyProperties(roster, baseInfoDto); + //设置组织架构的信息 + if (userOrgMap.containsKey(roster.getUserId()) && CollUtil.isNotEmpty(userOrgMap.get(roster.getUserId()).getInfoList())) { + baseInfoDto.setInfoList(userOrgMap.get(roster.getUserId()).getInfoList()); + } + //设置头像、花名 + if (formDataMap.containsKey(roster.getId())) { + List userFormDataList = formDataMap.get(roster.getId()); + for (FtbPersonnelsStaffRegistrationFormData item : userFormDataList) { + if ("flowerName".equals(item.getFormFieldId())) { + baseInfoDto.setFlowerName(item.getValue()); + } else if ("headLogo".equals(item.getFormFieldId())) { + baseInfoDto.setHeadLogo(personnelOrgUtils.convertHeadLogo(item.getValue())); + } + } + } + return baseInfoDto; + }).collect(Collectors.toList()) : Lists.newArrayList(); + } + + /** + * 根据用户ID查询所属门店负责人信息 + * + * @param userIds 用户ID集合 + * @return + */ + @Override + public ShopManagerUserDto queryShopManagerUser(List userIds) { + List list = personnelOrgUtils.queryStoreManagerForUserId(userIds); + Map> map = new HashMap<>(); + for (StoreUserRelationVo vo : list) { + Set userSet = map.get(vo.getStoreManagerUserId()); + if (CollectionUtil.isEmpty(userSet)) { + userSet = new HashSet<>(); + } + userSet.add(vo.getUserId()); + map.put(vo.getStoreManagerUserId(), userSet); + } + ShopManagerUserDto dto = new ShopManagerUserDto(); + dto.setMap(map); + return dto; + } + + @Override + public void timingAlertTrialJob(String tenantId) { + Date now = new Date(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffRoster::getWorkerStatus, StaffWorkerStatus.PRE_TRIAL.getCode()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + List alertList = new ArrayList<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { + Date today = PersonnelStaffUtils.getTodayDate(); + Date endDate = DateUtil.offsetDay(roster.getActualStartDate(), roster.getTrialJobDay() - 1); + if (now.before(endDate)) { + long betweenDay = DateUtil.betweenDay(endDate, today, false); + if (betweenDay == 1) { + //刚刚相差1天 ,给办理入职 权限人员 推送消息 + alertList.add(roster); + } + } else if (personnelOrgUtils.dateToString(today, "yyyy-MM-dd").equals(personnelOrgUtils.dateToString(endDate, "yyyy-MM-dd"))) { + + } else { + } + } + if (CollectionUtil.isNotEmpty(alertList)) { + sendTrialAlert(tenantId, alertList); + } + } + } + + @Override + public void timingUpdateTrialJob(String tenantId) { + Date now = new Date(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffRoster::getWorkerStatus, StaffWorkerStatus.PRE_TRIAL.getCode()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + List alertList = new ArrayList<>(); + for (FtbPersonnelsStaffRoster roster : rosterList) { + Date today = PersonnelStaffUtils.getTodayDate(); + Date endDate = DateUtil.offsetDay(roster.getActualStartDate(), roster.getTrialJobDay() - 1); + if (now.before(endDate)) { + + } else if (personnelOrgUtils.dateToString(today, "yyyy-MM-dd").equals(personnelOrgUtils.dateToString(endDate, "yyyy-MM-dd"))) { + + } else { + //试岗结束 进入试用期(有试用期) 直接转正(无试用期) + dealWithPreTrial(roster); + } + } + + } + } + + + @Override + public BaseUserInfoVo getUserMoreInfoByUserId(String userId) { + if (StrUtil.isBlank(userId)) { + return null; + } + BaseUserInfoVo vo = userApi.getUserInfoById(userId); + if (vo == null) { + return null; + } + this.list(Wrappers.lambdaQuery() + .eq(FtbPersonnelsStaffRoster::getUserId, userId) + ).stream().findFirst().ifPresent(roster -> { + vo.setWorkStatus(roster.getWorkerStatus()); + vo.setWorkStatusName(StaffWorkerStatus.getWorkerStatusNameByValue(roster.getWorkerStatus())); + }); + + return vo; + } + + private void dealWithPreTrial(FtbPersonnelsStaffRoster roster) { + ProbationPeriodDto probationPeriodDto = queryProbationPeriod(registrationFormDataService, roster.getId()); + if (null == probationPeriodDto || probationPeriodDto.getType().equals("100")) {//没有配置试用期直接转正 + innerPreTrailChangeRegular(roster, buildStaffRegularDto(roster.getUserId())); + return; + } + //有试用期,直接改成试用 + FtbPersonnelsStaffRoster updateRoster = new FtbPersonnelsStaffRoster(); + updateRoster.setUserId(roster.getUserId()); + updateRoster.setWorkerStatus(StaffWorkerStatus.TRIAL.getCode()); + updateRoster.setId(roster.getId()); + baseMapper.updateById(updateRoster); + registrationFormDataService.updateForOneField(roster.getId(), roster.getPhone(), "workerStatus", StaffWorkerStatus.TRIAL.getCode()); + + + } + + private StaffRegularDto buildStaffRegularDto(String userId) { + List workerGroupDataDtoList = personnelOrgUtils.getUserOrgBoundInfo(userId); + StaffRegularDto dto = new StaffRegularDto(); + dto.setRegularDate(new Date()); + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + WorkerGroupDataDto item = workerGroupDataDtoList.get(0); + dto.setNewOrg(item.getAffiliatedOrg()); + dto.setNewPosition(item.getAffiliatedPosition()); + dto.setNewRank(item.getAffiliatedRank()); + dto.setNewOrgName(item.getAffiliatedOrgName()); + dto.setNewPositionName(item.getAffiliatedPositionName()); + dto.setNewRankName(item.getAffiliatedRankName()); + dto.setImmediateSuperId(item.getReportsToName()); + dto.setRemarks(""); + } + return dto; + } + + /** + * 发送试岗消息 + * + * @param alertList + */ + private void sendTrialAlert(String tenantId, List alertList) { + Map> userToOrgMap = new HashMap<>();//用户->组织id,每个用户拥有的组织 + Map userMap = new HashMap<>();//用户ID ->用户详情 + for (FtbPersonnelsStaffRoster roster : alertList) { + userMap.put(roster.getUserId(), roster); + Map> map = personnelOrgUtils.getUserOrgBoundInfoForUserListAndTenantId(List.of(roster.getUserId()), tenantId, false); + List workerGroupDataDtoList = map.get(roster.getUserId()); + List orgList = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + for (WorkerGroupDataDto item : workerGroupDataDtoList) { + orgList.add(item.getAffiliatedOrg()); + } + } + userToOrgMap.put(roster.getUserId(), orgList); + + } + if (CollectionUtil.isEmpty(userToOrgMap)) { + return; + } + + Map> managerUserToUserMap = new HashMap<>();//管理人员ID->对应用户列表 + + for (Map.Entry> entry : userToOrgMap.entrySet()) { + String userId = entry.getKey(); //用户ID + List orgIds = entry.getValue();//组织ID + if (CollectionUtil.isEmpty(orgIds)) { + continue; + } + for (String orgId : orgIds) { + List powderUserIds = personnelPerUtils.obtainPersonnelUserIdDataPermissionsByOrgId(orgId, userId, tenantId); + log.info("用户ID={},组织ID={},组织下用户ID={}", userId, orgId, JSONUtil.toJsonStr(powderUserIds)); + if (CollectionUtil.isNotEmpty(powderUserIds)) { + for (String powderUserId : powderUserIds) { + Set set = managerUserToUserMap.get(powderUserId); + if (CollectionUtil.isEmpty(set)) { + set = new HashSet<>(); + } + set.add(userId); + managerUserToUserMap.put(powderUserId, set); + } + } + } + } + //发送消息 + for (String managerUserId : managerUserToUserMap.keySet()) { + Set currUserSet = managerUserToUserMap.get(managerUserId);//用户ID + if (CollectionUtil.isEmpty(currUserSet)) { + continue; + } + + List sendUserNameList = new ArrayList<>(); + for (String userId : currUserSet) { + FtbPersonnelsStaffRoster roster = userMap.get(userId); + if (null != roster) { + sendUserNameList.add(roster.getName()); + } + } + if (CollectionUtil.isEmpty(sendUserNameList)) { + continue; + } + personnelPreTrailIMUtils.sendMsg(tenantId, List.of(managerUserId), sendUserNameList); + } + } + + + @Override + @Transactional + public void updateSignSeparation(String userId, Integer status) { + + FtbPersonnelsStaffRoster roster = queryRosterInfoByUserId(userId); + if (null == roster) { + throw new RuntimeException("用户不存在"); + } + FtbPersonnelsStaffRoster updateEntity = new FtbPersonnelsStaffRoster(); + updateEntity.setIsSignSeparation(status); + updateEntity.setId(roster.getId()); + baseMapper.updateById(updateEntity); + + if (status == 1 && roster.getWorkerStatus().equals(StaffWorkerStatus.PENDING_RESIGNATION.getCode())) { + if (roster.getIsTrialFail() == 1) { + List workerGroupDataDtoList = personnelOrgUtils.getUserOrgBoundInfo(userId); + StaffTravlFailDto dto = new StaffTravlFailDto(); + dto.setRemarks(roster.getTrialFail()); + dto.setList(workerGroupDataDtoList); + dto.setFailDate(new Date()); + dto.setStaffWorkerStatus(StaffWorkerStatus.RESIGNED); + innerTravelFail(userId, dto); + + FtbjobTrialRejectedDTO ftbjobTrialRejectedDTO = new FtbjobTrialRejectedDTO(); + ftbjobTrialRejectedDTO.setReasonsForJobRejection(dto.getRemarks()); + ftbjobTrialRejectedDTO.setUserId(userId); + ftbjobTrialRejectedDTO.setSystemWokerId(roster.getSystemWokerId()); + ftbjobTrialRejectedDTO.setPhoneNumber(roster.getPhone()); + ftbjobTrialRejectedDTO.setUserName(roster.getName()); + ftbjobTrialRejectedDTO.setJobNumber(roster.getWorkerNo()); + ftbjobTrialRejectedDTO.setDateOfEntry(roster.getCreatorTime()); + ftbjobTrialRejectedDTO.setResignationDate(new Date()); + ftbjobTrialRejectedDTO.setOrganizationInfo(workerGroupDataDtoList); + ftbjobTrialRejectedDTO.setOrgId(roster.getCurrOrg()); + ftbjobTrialRejectedDTO.setEntryPostId(roster.getCurrPosition()); + ftbjobTrialRejectedDTO.setEntryGradeId(roster.getCurrRank()); + turnoverManagementService.jobTrialRejected(ftbjobTrialRejectedDTO); + } else { + turnoverManagementService.userHasSignedASeparationAgreement(userId, status); + } + } else { + turnoverManagementService.userHasSignedASeparationAgreement(userId, status); + } + } + + + /** + * 检查合同到期并修改状态 + * + * @param tenantId + */ + @Override + public void timingCheckContractOverTime(String tenantId) { + Date now = new Date(); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(FtbPersonnelsStaffRoster::getId, FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getContractStatus) + .notIn(FtbPersonnelsStaffRoster::getWorkerStatus, "305") + .eq(FtbPersonnelsStaffRoster::getContractStatus, FContractSignStatus.SIGNED.getCode()) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List rosterList = baseMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(rosterList)) { + //获取花名册ID列表 + List rosterIds = rosterList.stream().map(FtbPersonnelsStaffRoster::getId).collect(Collectors.toList()); + List formDataList = registrationFormDataService.queryListForRosterIdAndField(rosterIds, List.of("contractDate")); + if (CollectionUtil.isEmpty(formDataList)) { + return; + } + for (FtbPersonnelsStaffRegistrationFormData formData : formDataList) { + if (StringUtils.isEmpty(formData.getValue())) { + continue; + } + Date date = personnelOrgUtils.stringDateToDate(formData.getValue()); + + if (now.after(date)) {//当前时间在date之后 + FtbPersonnelsStaffRoster updateRoster = new FtbPersonnelsStaffRoster(); + updateRoster.setId(formData.getRosterId()); + updateRoster.setContractStatus(FContractSignStatus.EXPIRED.getCode()); + baseMapper.updateById(updateRoster); + registrationFormDataService.updateForOneField(formData.getRosterId(), formData.getPhone(), "contractStatus", FContractSignStatus.EXPIRED.getDescription()); + } + + } + + } + } + + @Override + public Boolean canUpdateRealName() { + //花名册能够修改真实姓名(0-可以修改 1-不可修改) + String flag = baseMapper.canUpdateRealName(); + if (StringUtils.isEmpty(flag)) { + return true; + } + if (flag.equals("1")) { + return false; + } + return true; + } + + @Override + public Map getInfoForUserIdOneField(String field, String userId) { + Map retMap = new HashMap<>(); + + FtbPersonnelsStaffRoster roster = queryRosterInfoByUserId(userId); + if (null == roster) { + throw new RuntimeException("用户不存在"); + } + StaffHomeDto dto = BeanUtil.copyProperties(roster, StaffHomeDto.class); + fillContractTypeName(Collections.singletonList(dto)); + // 补充工作状态转移 + FtbPersonnelsStaffRosterDto.changeEscape(dto); + BeanUtil.beanToMap(dto, retMap, false, false); + if (mainTableField.contains(field)) { + return retMap; + } + if (extTableField.contains(field)) { + if ("userRoleList".equals(field)) { + List saffRoleDtos = personnelOrgUtils.queryRoleListForUserId(roster.getUserId()); + retMap.put("userRoleList", saffRoleDtos); + return retMap; + } + if ("orgList".equals(field) || "currReportsTo".equals(field) || "currReportsToName".equals(field)) { + List list = personnelOrgUtils.getUserOrgBoundInfo(roster.getUserId()); + retMap.put("orgList", list); + if (CollUtil.isNotEmpty(list)) { + WorkerGroupDataDto groupDataDto = list.stream().filter(user -> user.getIsDefaultOrganize() && user.getIsDefaultPosition()).findFirst().orElse(new WorkerGroupDataDto()); + retMap.put("currReportsTo", groupDataDto.getReportsTo()); + retMap.put("currReportsToName", groupDataDto.getReportsToName()); + } + return retMap; + } + + if ("attendanceGroup".equals(field) || "attendanceGroupName".equals(field)) { + List attendanceUserGroupList = personnelOrgUtils.queryAttendanceGroup(roster.getUserId()); + List groupName = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(attendanceUserGroupList)) { + //stram获取考勤组名称 + for (AttendanceUserGroupVo attendanceUserGroupVo : attendanceUserGroupList) { + groupName.add(attendanceUserGroupVo.getGroupName()); + } + } + retMap.put("attendanceGroupName", String.join(",", groupName)); + return retMap; + } + if ("currSalary".equals(field)) { + List ftbPersonnelsSalaryInfos = personnelSalaryServiceImpl.innerGetTheCurrentPersonSSalary(roster, "", ""); + if (CollectionUtil.isNotEmpty(ftbPersonnelsSalaryInfos)) { + BigDecimal bigDecimal = ftbPersonnelsSalaryInfos.stream() + .filter(item -> item.getFSetValue().matches("^\\d+(\\.\\d+)?$")) + .map(v -> BigDecimal.valueOf(Double.parseDouble(v.getFSetValue()))).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + retMap.put("currSalary", bigDecimal); + return retMap; + } + } + //查询框考勤组信息 + } + FtbPersonnelsRegistrationFormField formFieldDto = registrationFormFieldService.queryOneField(field); + if (formFieldDto == null) { + retMap.put(field, null); + return retMap; + } + FtbPersonnelsStaffRegistrationFormData formFieldValue = registrationFormDataService.queryOneFieldForFieldId(roster.getId(), field); + if (formFieldValue == null) { + retMap.put(field, null); + return retMap; + } + + String formValueStr = formFieldValue.getValue(); + retMap.put(field, formFieldValue.getValue()); + if ("headLogo".equals(formFieldDto.getId())) { + List headLogoList = personnelOrgUtils.parseHeadLogo(formFieldValue.getValue()); + retMap.put("headLogo", headLogoList.get(0)); + } else if ("probationPeriod".equals(formFieldDto.getId())) { + if (StringUtils.isNotEmpty(formFieldValue.getValue()) && formFieldValue.getValue().startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(formFieldValue.getValue(), ProbationPeriodDto.class); + retMap.put("probationPeriod", bean.getType()); + retMap.put("probationPeriodDay", bean.getDays()); + retMap.put("probationPeriodDayName", personnelOrgUtils.convertProbationPeriod(bean)); + } + } else if (formFieldDto.getType() == 2 || formFieldDto.getType() == 3) { + List optionList = fieldOptionService.queryOptionsByFieldId(formFieldDto.getId()); + Map optionMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(optionList)) { + optionMap = optionList.stream().collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, Function.identity())); + } + + if (StringUtils.isNotEmpty(formValueStr)) { + FtbPersonnelsRegistrationFormFieldOption ftbPersonnelsRegistrationFormFieldOption = optionMap.get(formValueStr); + if (ftbPersonnelsRegistrationFormFieldOption != null) { + retMap.put(field, ftbPersonnelsRegistrationFormFieldOption.getName()); + } else { + retMap.put(field, null); + } + } + } + return retMap; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffSalaryChangeLogServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffSalaryChangeLogServiceImpl.java new file mode 100644 index 0000000..ace9b3d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffSalaryChangeLogServiceImpl.java @@ -0,0 +1,55 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.model.enums.SalaryChangeTypeEnum; +import jnpf.model.personnels.dto.staff.salarylog.AddSalaryChangeLogDto; +import jnpf.model.personnels.dto.staff.salarylog.FtbPersonnelsStaffSalaryChangeLogDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffSalaryChangeLog; +import jnpf.personnels.mapper.FtbPersonnelsStaffSalaryChangeLogMapper; +import jnpf.personnels.service.FtbPersonnelsStaffSalaryChangeLogService; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +public class FtbPersonnelsStaffSalaryChangeLogServiceImpl extends ServiceImpl implements FtbPersonnelsStaffSalaryChangeLogService { + + @Override + public List queryAll(String userId) { + + List list = baseMapper.selectList( + new LambdaQueryWrapper() + .eq(FtbPersonnelsStaffSalaryChangeLog::getUserId, userId) + .orderByDesc(FtbPersonnelsStaffSalaryChangeLog::getCreatorTime) + ); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return BeanUtil.copyToList(list, FtbPersonnelsStaffSalaryChangeLogDto.class); + } + + + @Override + public void addSalaryChangeLog(AddSalaryChangeLogDto dto) { + if (dto.getSalary() == null) { + return; + } + int comparisonResult = dto.getSalary().compareTo(BigDecimal.ZERO); + if (comparisonResult <= 0) { + return; + } + FtbPersonnelsStaffSalaryChangeLog entity = new FtbPersonnelsStaffSalaryChangeLog(); + entity.setUserId(dto.getUserId()); + entity.setChangeDate(dto.getChangeDate()); + entity.setSalary(dto.getSalary()); + SalaryChangeTypeEnum salaryChangeTypeEnum = SalaryChangeTypeEnum.fromCode(dto.getChangeType()); + entity.setTitle(salaryChangeTypeEnum.getMsg()); + entity.setSalaryType(salaryChangeTypeEnum.getCode()); + baseMapper.insert(entity); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionHandoverServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionHandoverServiceImpl.java new file mode 100644 index 0000000..af22bd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionHandoverServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.personnels.mapper.FtbPersonnelsStaffTransferPositionHandoverMapper; +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPositionHandover; +import jnpf.personnels.service.FtbPersonnelsStaffTransferPositionHandoverService; +@Service +public class FtbPersonnelsStaffTransferPositionHandoverServiceImpl extends ServiceImpl implements FtbPersonnelsStaffTransferPositionHandoverService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionServiceImpl.java new file mode 100644 index 0000000..b4f9e7b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsStaffTransferPositionServiceImpl.java @@ -0,0 +1,1558 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.entity.AttendanceGroup; +import jnpf.entity.StoreEntity; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.vo.UserSalaryHistoryVo; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.req.paper.SavePaperReq; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.enums.GrowthLogEnum; +import jnpf.model.enums.SalaryChangeTypeEnum; +import jnpf.model.personnels.dto.audit.FtbPersonnelsAuditDto; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.dto.staff.salarylog.AddSalaryChangeLogDto; +import jnpf.model.personnels.dto.staff.transfer.FtbPersonnelsStaffTransferPositionHandoverDto; +import jnpf.model.personnels.dto.staff.transfer.TransferGrowthLogDto; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionCountDto; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPosition; +import jnpf.model.personnels.po.FtbPersonnelsStaffTransferPositionHandover; +import jnpf.model.personnels.req.employment.EmploymentApplyCheckDto; +import jnpf.model.personnels.req.transfer.*; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.model.personnels.vo.task.FtbPersonnelsAuditInfoVO; +import jnpf.permission.PositionApi; +import jnpf.permission.RoleApi; +import jnpf.permission.UserApi; +import jnpf.permission.UserPrimaryPositionApi; +import jnpf.permission.dto.SynUserBoundRoleDTO; +import jnpf.permission.dto.UpdateUserManagerBoundDTO; +import jnpf.permission.dto.position.CheckExistByPositionDTO; +import jnpf.permission.dto.relation.BaseUserPrimaryPositionDTO; +import jnpf.permission.dto.role.RemoveUserRolesDTO; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.model.role.RoleListVO; +import jnpf.permission.model.role.UserBoundRolesVO; +import jnpf.permission.model.user.UserInfoVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffTransferPositionMapper; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.PersonnelOrgUtils; +import jnpf.personnels.utils.PersonnelPerUtils; +import jnpf.salary.QuerySalaryApi; +import jnpf.store.service.StoreService; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsStaffTransferPositionServiceImpl extends ServiceImpl implements FtbPersonnelsStaffTransferPositionService { + + @Autowired + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private FtbPersonnelsStaffSalaryChangeLogService salaryChangeLogService; + + @Autowired + private FtbPersonnelsStaffTransferPositionHandoverService transferPositionHandoverService; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private FtbPersonnelsAuditRunTaskService auditRunTaskService; + + @Autowired + private PersonnelPerUtils personnelPerUtils; + + @Autowired + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + private FtbPersonnelsAuditSubConfigService subConfigService; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + private FtbPersonnelsAuditRunTaskService ftbPersonnelsAuditRunTaskService; + + @Autowired + FtbPersonnelsSalaryService personnelSalaryServiceImpl; + + @Autowired + StoreService storeService; + + @Autowired + PositionApi positionApi; + + @Resource + private AttendanceGroupApi attendanceGroupApi; + + @Autowired + FtbPersonnelsSalaryService salaryService; + + @Autowired + QuerySalaryApi querySalaryApi; + + @Autowired + RoleApi roleApi; + + @Autowired + UserPrimaryPositionApi userPrimaryPositionApi; + + @Autowired + UserApi userApi; + + @Override + public PageInfo getPageList(QueryTransferListReq req) { + Page queryPage = null; + List records = new ArrayList<>(); + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + if (req.getListType() == 0) { + queryPage = baseMapper.complatePagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req, orgIds); + } else if (req.getListType() == 1) { + queryPage = baseMapper.waitCheckPagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req, orgIds); + } + if (queryPage != null) { + records = queryPage.getRecords(); + } + if (CollectionUtil.isNotEmpty(records)) { + fillOtherInfo(records); + fillFormDataToTransfer(records); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + + private void fillAllUserOrgInfo(List records) { + List userIds = records.stream().map(TransferPositionDto::getUserId).collect(Collectors.toList()); + Map> userOrgBoundInfo = personnelOrgUtils.getUserOrgBoundInfo(userIds); + records.forEach(item->{ + if (userOrgBoundInfo.containsKey(item.getUserId())) { + item.setOrgList(userOrgBoundInfo.get(item.getUserId())); + } + }); + } + + private void fillFormDataToTransfer(List records) { + List rosterIds = records.stream().map(TransferPositionDto::getStaffRosterId).collect(Collectors.toList()); + Map> map = registrationFormDataService.querAllFormDataForRosterIds(rosterIds); + for (TransferPositionDto record : records) { + String staffRosterId = record.getStaffRosterId(); + if (!map.containsKey(staffRosterId)){ + continue; + } + Map data = map.get(staffRosterId); + String probationPeriod =""; + if (data.containsKey("probationPeriod")) probationPeriod = data.get("probationPeriod"); + String planProbationaryDate =""; + if (data.containsKey("planProbationaryDate")) planProbationaryDate =data.get("planProbationaryDate"); + if (StringUtils.isNotEmpty(probationPeriod) && probationPeriod.startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(probationPeriod, ProbationPeriodDto.class); + record.setProbationPeriod(bean.getType()); + record.setProbationPeriodDay(bean.getDays()); + } + if (StringUtils.isNotEmpty(planProbationaryDate)) { + record.setPlanProbationaryDate(personnelOrgUtils.stringDateToDate(planProbationaryDate)); + } + } + } + + private void fillFormDataToTransferItem(TransferPositionDto record) { + Map map = registrationFormDataService.queryAllFormData(record.getPhone(), record.getStaffRosterId()); + FtbPersonnelsStaffRegistrationFormData probationPeriod = map.get("probationPeriod"); + if (null != probationPeriod) { + String value = probationPeriod.getValue(); + if (StringUtils.isNotEmpty(value) && value.startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(value, ProbationPeriodDto.class); + record.setProbationPeriod(bean.getType()); + record.setProbationPeriodDay(bean.getDays()); + } + } + + FtbPersonnelsStaffRegistrationFormData planProbationaryDate = map.get("planProbationaryDate"); + if (null != planProbationaryDate) { + String value = planProbationaryDate.getValue(); + if (StringUtils.isNotEmpty(value)) { + record.setPlanProbationaryDate(personnelOrgUtils.stringDateToDate(value)); + } + } + } + + + @Override + public PageInfo pageQueryMyCheckList(MyWebTransferCheckListReq req) { + + //审批状态:0、待审批 1、已审批 + String loginUserId = UserProvider.getLoginUserId(); + Page queryPage; + Integer configType = FtbPersonnelsCofigEnum.POST_TRANSFER_APPROVAL_CONFIGURATION.getConfigType(); + if (0 == req.getCheckStatus()) { + queryPage = baseMapper.pagingQueryWebMyCheckList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId,configType); + } else { + queryPage = baseMapper.getApprovedList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId,configType); + } + List records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + fillOtherInfo(records); + fillFormDataToTransfer(records); + } + //构建分页返回数据 + PageInfo pageInfo = new PageInfo<>(); + pageInfo.setList(records); + pageInfo.setPageSize((int) queryPage.getSize()); + pageInfo.setPageNum((int) queryPage.getCurrent()); + pageInfo.setTotal((int) queryPage.getTotal()); + return pageInfo; + } + + + @Override + public PageInfo getAppPageList(AppQueryTransferListReq req) { + String loginUserId = UserProvider.getLoginUserId(); + Page queryPage = null; + List records = null; + if (req.getListType() == 0) { + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + queryPage = baseMapper.appPagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), req, orgIds); + } else if (req.getListType() == 1) { + //1、我的审批 + Integer configType = FtbPersonnelsCofigEnum.POST_TRANSFER_APPROVAL_CONFIGURATION.getConfigType(); + if (req.getCheckStatus() == 0) { + queryPage = baseMapper.pagingQueryWebMyCheckList(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId, configType); + } else { + queryPage = baseMapper.queryAccordingToTheCorrespondingStatus(Page.of(req.getCurrentPage(), + req.getPageSize()), + req.getCheckStatus(), loginUserId, configType); + } + } else if (req.getListType() == 2) { + //2、抄送给我的 + queryPage = baseMapper.getListForApp(Page.of(req.getCurrentPage(), req.getPageSize()), req, loginUserId); + } else if (req.getListType() == 3) { + //3、我的调岗 + queryPage = baseMapper.myPagingQuery(Page.of(req.getCurrentPage(), req.getPageSize()), loginUserId); + } + records = queryPage.getRecords(); + if (CollectionUtil.isNotEmpty(records)) { + fillOtherInfo(records); + fillFormDataToTransfer(records); + } + return CultivatePage.coverPageInfo(queryPage); + } + + /** + * 查询我的待审批数量 + * + * @return + */ + @Override + public TransferPositionCountDto queryAppMyCheckCount() { + TransferPositionCountDto dto = new TransferPositionCountDto(); + + + String loginUserId = UserProvider.getLoginUserId(); + Integer configType = FtbPersonnelsCofigEnum.POST_TRANSFER_APPROVAL_CONFIGURATION.getConfigType(); + Page transferPositionDtoPage = baseMapper.pagingQueryWebMyCheckList(Page.of(1, 1), loginUserId, configType); + dto.setWaitCount((int) transferPositionDtoPage.getTotal()); + + Page dtoPage = baseMapper.queryAccordingToTheCorrespondingStatus(Page.of(1, + 1), 1, loginUserId, configType); + dto.setQuantityUnderCount((int) dtoPage.getTotal()); + + Page pass = baseMapper.queryAccordingToTheCorrespondingStatus(Page.of(1, 1), 2, loginUserId, configType); + dto.setPassCount((int) pass.getTotal()); + + + Page noPass = baseMapper.queryAccordingToTheCorrespondingStatus(Page.of(1, 1), 3, loginUserId, configType); + dto.setNoPassCount((int) noPass.getTotal()); + return dto; + } + + /** + * 查询我的抄送审批批数量 + * + * @return + */ + @Override + public TransferPositionCountDto queryAppMySendCount() { + TransferPositionCountDto dto = new TransferPositionCountDto(); + String loginUserId = UserProvider.getLoginUserId(); + AppQueryTransferListReq req = new AppQueryTransferListReq(); + req.setCheckStatus(1); + Long count = baseMapper.getListForApp(new Page<>(), req, loginUserId).getTotal(); + dto.setQuantityUnderCount(Math.toIntExact(count)); + + req.setCheckStatus(2); + Long pass = baseMapper.getListForApp(new Page<>(), req, loginUserId).getTotal(); + dto.setPassCount(Math.toIntExact(pass)); + + + req.setCheckStatus(3); + Long noPass = baseMapper.getListForApp(new Page<>(), req, loginUserId).getTotal(); + dto.setNoPassCount(Math.toIntExact(noPass)); + return dto; + } + + @Override + public FtbPersonnelsBubbleCountVO getListCont(String flag) { + FtbPersonnelsBubbleCountVO covert; + if ("1".equals(flag)) { + TransferPositionCountDto transferPositionCountDto = queryAppMyCheckCount(); + covert = TransferPositionCountDto.covert(transferPositionCountDto); + } else { + TransferPositionCountDto transferPositionCountDto = queryAppMySendCount(); + covert = TransferPositionCountDto.covert(transferPositionCountDto); + } + return covert; + } + + @Override + public void clearJobTransferData(List userIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsStaffTransferPosition::getUserId,userIds); + List managements = baseMapper.selectList(wrapper); + if(CollectionUtil.isEmpty(managements)){ + return; + } + // 清除具有审批数据的数据 + managements.stream().filter(vo-> StringUtils.isNotEmpty(vo.getTaskInfoId())) + .forEach (item->ftbPersonnelsAuditRunTaskService.deleteReviewProcessAndRecords(item.getTaskInfoId(),item.getId())); + // 清楚转正数据 + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.in(FtbPersonnelsStaffTransferPosition::getId,managements.stream().map(FtbPersonnelsStaffTransferPosition::getId).collect(Collectors.toList())); + baseMapper.delete(lambdaed); + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public ActionResult dealTransferForOA(SaveTransferReq req) { + // 重构调岗办理代码 + ActionResult actionResult = new ActionResult(); + String taskId = req.getTaskId(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + if (req.getToMoney() == null) { + req.setToMoney(BigDecimal.ZERO); + } + if (CollUtil.isNotEmpty(req.getToRoles())) req.setToRole(req.getToRoles().stream().collect(Collectors.joining(","))); + String loginUserId = UserProvider.getLoginUserId(); + //查询用户是否已经有调岗流程 + List userIds = req.getUserIds(); + String userId = userIds.get(0); + req.setUserId(userId); + String formMangerId = personnelOrgUtils.getLeaderInfo(req.getFromOrg(), userId, req.getFromPositionId(), req.getFromRankId()); + req.setFromManager(formMangerId); + List obUserIds = new ArrayList<>(); + // 权限用户 + String source = req.getSource(); + if(!"0".equals(source)) obUserIds = personnelPerUtils.obtainPersonnelDataPermissions(); + // 当前人不是本人申请,且权限中有当前可以办理人 + if (CollUtil.isNotEmpty(obUserIds) && !obUserIds.contains(userId)){ + actionResult.setMsg("当前人办理人不具有该员工办理权限!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + TransferPositionDto entity = baseMapper.queryNoCompleteForUserId(userId); + if (null != entity) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "用户已经存在调岗审批流程"); + return actionResult; + } + // 校验直属主管是否为试岗员工 + if (StringUtils.isNotEmpty(req.getToManager())){ + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbPersonnelsStaffRoster::getUserId,req.getToManager()); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(lambdaed); + if (one.getWorkerStatus().equals("306")) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "试岗员工:"+one.getName()+"不能作为直属主管!"); + return actionResult; + } + } + //查询用户花名册信息 + FtbPersonnelsStaffRoster rosterEntity = staffRosterService.queryRosterInfoByUserId(userId); + if (null == rosterEntity) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "用户花名册信息不存在"); + return actionResult; + } + if (rosterEntity.getEnabledMark() == 1) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "用户花名册信息不存在"); + return actionResult; + } + if ("304".equals(rosterEntity.getWorkerStatus())) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "员工处于离职中,不能调岗"); + return actionResult; + } + if ("305".equals(rosterEntity.getWorkerStatus())) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "员工已经离职"); + return actionResult; + } + if (req.getFromOrg().equals(req.getToOrg()) && + req.getFromPositionId().equals(req.getToPositionId()) + && req.getFromRankId().equals(req.getToRankId())){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "员工调岗的组织岗位职等,无变化,不能办理调岗"); + return actionResult; + } + List list = personnelOrgUtils.getUserOrgBoundInfo(userId); + if (CollUtil.isNotEmpty(list) && list.size() > 1){ + // 员工所属组织 + for (WorkerGroupDataDto dataDto : list) { + // 员工调岗组织岗位 都相同 不能有不同职等 + if (!( req.getToOrg().equals(req.getFromOrg()) && req.getToPositionId().equals(req.getFromPositionId())) + && (dataDto.getAffiliatedOrg().equals(req.getToOrg()) && + dataDto.getAffiliatedPosition().equals(req.getToPositionId()))){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "您在目标组织中已所属于该岗位,不可重复选择同一组织下相同岗位!"); + return actionResult; + } + } + } + String toRankId = req.getToRankId(); + String toPositionId = req.getToPositionId(); + String toOrg = req.getToOrg(); + if (toOrg.equals(req.getFromOrg()) + && toPositionId.equals(req.getFromPositionId()) + && toRankId.equals(req.getFromRankId()) + ) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "原组织岗位职等和新组织岗位职等相同,无需调岗"); + return actionResult; + } + if (rosterEntity.getUserId().equals(req.getToManager())) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "调岗后的直属主管不能是自己"); + return actionResult; + } + // v1.1 添加门店负责人校验 + // 跨组织校验 + if (!req.getFromOrg().equals(req.getToOrg())) { + LambdaQueryWrapper storeEntityLambdaQueryWrapper = Wrappers.lambdaQuery(); + storeEntityLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid, userId); + List storeEntities = storeService.list(storeEntityLambdaQueryWrapper); + if (!storeEntities.isEmpty()){ + String storeNames = storeEntities.stream().map(StoreEntity::getStorename).collect(Collectors.joining(",")); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该人员为"+storeNames+"负责人,请为相关门店替换负责人,否则无法办理调岗。"); + return actionResult; + } + } + //写入调岗表 + FtbPersonnelsStaffTransferPosition insertEntity = BeanUtil.copyProperties(req, FtbPersonnelsStaffTransferPosition.class); + // 更新数据重新提交 + if (req.getSpecialBusinessId() != null ){ + insertEntity.setId(req.getSpecialBusinessId()); + } + insertEntity.setEnabledMark(0); + insertEntity.setPhone(rosterEntity.getPhone()); + insertEntity.setCheckStatus(1); + insertEntity.setIsNeedCheck(0); + insertEntity.setCreatorTime(new Date()); + insertEntity.setOpUser(loginUserId); + insertEntity.setVersionNum(1); + if ("0".equals(source)){ + insertEntity.setSource(0); + }else if ("1".equals(source)){ + insertEntity.setSource(1); + } + String remarks = req.getRemarks(); + if (StringUtils.isNotEmpty(req.getSpecialBusinessId()) && StringUtils.isEmpty(remarks)){ + insertEntity.setRemarks(null); + }else{ + insertEntity.setRemarks(remarks); + } + insertEntity.setOpUserName(UserProvider.getUser().getUserName()); + // 添加薪资详情 + List salaryStructureList = req.getSalaryStructureList(); + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(req.getIsAdjustSalary()) ){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + return actionResult; + }else if (CollUtil.isNotEmpty(salaryStructureList) && "1".equals(req.getIsAdjustSalary())) { + insertEntity.setSalaryStructureListStr(JSONObject.toJSONString(salaryStructureList)); + } + insertEntity.setTaskInfoId(taskId); + if (req.getSpecialBusinessId() != null) { + //删除工作物品交接表 + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda().set(FtbPersonnelsStaffTransferPositionHandover::getEnabledMark, 1) + .eq(FtbPersonnelsStaffTransferPositionHandover::getStaffTransferPositionId, req.getSpecialBusinessId()); + transferPositionHandoverService.update(updateWrapper); + } + // 构建主管 + UserInfoVO bossUser = userApi.getLeaderInfo(insertEntity.getFromOrg(), insertEntity.getUserId() + ,insertEntity.getFromPositionId(),insertEntity.getFromRankId()); + if (ObjectUtil.isNotEmpty(bossUser)) insertEntity.setFromManager(bossUser.getId()); + saveOrUpdate(insertEntity); + //写入调岗交接表 + List handoverList = new ArrayList<>(); + // 工作交接 + List recipientOfTheWorkUserIds = req.getRecipientOfTheWorkUserId(); + if (CollUtil.isNotEmpty(recipientOfTheWorkUserIds)) { + String recipientOfTheWorkInfo = req.getRecipientOfTheWorkInfo(); + if (StringUtils.isNotEmpty(recipientOfTheWorkInfo) && StringUtil.length(recipientOfTheWorkInfo) > 50){ + actionResult.setMsg("调岗交接详情不能超过50字!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + String recipientOfTheWorkUserId = recipientOfTheWorkUserIds.get(0); + if( rosterEntity.getUserId().equals(recipientOfTheWorkUserId)){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + + actionResult.setMsg( "工作:"+ recipientOfTheWorkInfo +" 的交接人不能是自己"); + return actionResult; + } + FtbPersonnelsStaffTransferPositionHandover handover = new FtbPersonnelsStaffTransferPositionHandover(); + handover.setStaffTransferPositionId(insertEntity.getId()); + handover.setType(1); + handover.setName(recipientOfTheWorkInfo); + handover.setToUserId(recipientOfTheWorkUserId); + UserEntity userEntity = personnelOrgUtils.queryUserInfo(recipientOfTheWorkUserId); + if (ObjectUtil.isNotEmpty(userEntity))handover.setToUserName(userEntity.getRealName()); + handover.setEnabledMark(0); + handoverList.add(handover); + } + // 物品交接 + List recipientOfTheItemUserIds = req.getRecipientOfTheItemUserId(); + if (CollUtil.isNotEmpty(recipientOfTheItemUserIds)) { + String recipientOfTheItemInfo = req.getRecipientOfTheItemInfo(); + if (StringUtils.isNotEmpty(recipientOfTheItemInfo) && StringUtil.length(recipientOfTheItemInfo) > 50){ + actionResult.setMsg("调岗交接详情不能超过50字!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + String recipientOfTheItemUserId = recipientOfTheItemUserIds.get(0); + if( rosterEntity.getUserId().equals(recipientOfTheItemUserId)){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "物品:"+recipientOfTheItemInfo+" 的接收人不能是自己"); + return actionResult; + } + FtbPersonnelsStaffTransferPositionHandover handover = new FtbPersonnelsStaffTransferPositionHandover(); + handover.setStaffTransferPositionId(insertEntity.getId()); + handover.setType(0); + handover.setName(recipientOfTheItemInfo); + handover.setToUserId(recipientOfTheItemUserId); + UserEntity userEntity = personnelOrgUtils.queryUserInfo(recipientOfTheItemUserId); + if (ObjectUtil.isNotEmpty(userEntity)) handover.setToUserName(userEntity.getRealName()); + handover.setEnabledMark(0); + handoverList.add(handover); + } + if (CollectionUtil.isNotEmpty(handoverList)) { + transferPositionHandoverService.saveBatch(handoverList); + } + //花名册设置用户调岗中 + FtbPersonnelsStaffRoster staffRoster = new FtbPersonnelsStaffRoster(); + staffRoster.setId(rosterEntity.getId()); + staffRoster.setTransferPosition(1); + staffRosterService.updateById(staffRoster); + return actionResult; + } + + @Override + @Transactional(rollbackFor = RuntimeException.class) + public ActionResult approvalForOA(EmploymentApplyCheckDto dto) { + ActionResult actionResult = new ActionResult(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffTransferPosition::getTaskInfoId,dto.getTaskId()); + FtbPersonnelsStaffTransferPosition entity = baseMapper.selectOne(queryWrapper); + if (ObjectUtil.isNull(entity)){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg("该审批数据不存在,无法进行审批操作!"); + return actionResult; + } + // 校验 + // 选择通过需要校验组织是否存在 + String toRankId = entity.getToRankId(); + String toPositionId = entity.getToPositionId(); + String toOrg = entity.getToOrg(); + if (1 == dto.getIsPass()){ + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(toOrg); + checkExistByPositionDTO.setPositionId(toPositionId); + checkExistByPositionDTO.setPositionGradesId(toRankId); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该员工的岗位在提交调岗审批后发生了变更,无法继续审批;请选择“不通过”结束审批流程!"); + return actionResult; + } + } + if (1 == dto.getIsPass()){ + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(entity.getFromOrg()); + checkExistByPositionDTO.setPositionId(entity.getFromPositionId()); + checkExistByPositionDTO.setPositionGradesId(entity.getFromRankId()); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该员工的岗位在提交调岗审批后发生了变更,无法继续审批;请选择“不通过”结束审批流程!"); + return actionResult; + } + } + if ( entity.getEnabledMark() == 1) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "调岗信息不存在"); + return actionResult; + } + if (entity.getIsNeedCheck() == 1) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该员工调岗不需要审核"); + return actionResult; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该员工调岗审核已经通过"); + return actionResult; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 3) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该员工调岗审核已经不通过"); + return actionResult; + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 4) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg( "该员工调岗审核已经撤销"); + return actionResult; + } + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByUserId(entity.getUserId()); + FtbPersonnelsAuditDto checkDto = new FtbPersonnelsAuditDto(); + checkDto.setBusinessId(entity.getId()); + if (ObjectUtil.isNotEmpty(dto.getIsConformSalary())) { + checkDto.setDoesTheSalaryComplyWith(dto.getIsConformSalary()); + } + if (ObjectUtil.isNotEmpty(dto.getMsg())) { + checkDto.setApprovalComments(dto.getMsg()); + } + if (ObjectUtil.isNotEmpty(dto.getIsPass())) { + checkDto.setFlag(String.valueOf(dto.getIsPass())); + } + //FtbPersonnelsAuditTaskEnum code = auditRunTaskService.performReview(checkDto); + + entity.setTransferDate(new Date()); + // 添加薪资详情 + List salaryStructureList = dto.getSalaryStructureList(); + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(dto.getIsAdjustSalary()) ){ + actionResult.setMsg( "目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + actionResult.setCode( FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + }else if (CollUtil.isNotEmpty(salaryStructureList) && "1".equals(dto.getIsAdjustSalary())) { + entity.setToMoney(dto.getToMoney()); + entity.setSalaryStructure(dto.getSalaryStructure()); + entity.setIsAdjustSalary(dto.getIsAdjustSalary()); + entity.setSalaryStructureListStr(JSONObject.toJSONString(salaryStructureList)); + entity.setReasonsForSalaryAdjustments(dto.getReasonsForSalaryAdjustments()); + entity.setPayrollEffectiveDate(dto.getPayrollEffectiveDate()); + } + FtbPersonnelsAuditTaskEnum code = checkDto.getFlag().equals("0") ? FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED : FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + switch (code) { + case EXAMINATION_PASSED: + entity.setCheckStatus(2); + // 提出公共部分 + raisePublicPart(entity, roster, toOrg, toPositionId, toRankId); + break; + case AUDIT_NOT_PASSED: + entity.setCheckStatus(3); + roster.setTransferPosition(3); + staffRosterService.updateById(roster); + break; + } + baseMapper.updateById(entity); + return actionResult; + } + + private void raisePublicPart(FtbPersonnelsStaffTransferPosition entity, FtbPersonnelsStaffRoster roster, String toOrg, String toPositionId, String toRankId) { + //写入调岗薪资变化流程 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(entity.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(entity.getToMoney()); + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.TRANSFER_POSITION.getCode()); + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + + transferToWriteGrowth(entity.getUserId(), entity, entity.getRemarks(), roster); + + roster.setTransferPosition(2); + roster.setCurrSalary(entity.getToMoney()); + staffRosterService.updateById(roster); + //同步角色 + boolean flag = entity.getToOrg().equals(entity.getFromOrg()); + asyncRole(entity.getUserId(),flag); + + //修改用户组织岗位职等直属主管 + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + updateUserManagerBoundDTO.setUserId(entity.getUserId()); + updateUserManagerBoundDTO.setOldOrgId(entity.getFromOrg()); + updateUserManagerBoundDTO.setOldPositionId(entity.getFromPositionId()); + updateUserManagerBoundDTO.setOldPositionGradesId(entity.getFromRankId()); + updateUserManagerBoundDTO.setOldManagerId(entity.getFromManager()); + updateUserManagerBoundDTO.setOrgId(toOrg); + updateUserManagerBoundDTO.setPositionId(toPositionId); + updateUserManagerBoundDTO.setPositionGradesId(toRankId); + updateUserManagerBoundDTO.setManagerId(entity.getToManager()); + personnelOrgUtils.updateSysUserBound(updateUserManagerBoundDTO); + BaseUserPrimaryPositionDTO primaryPositionDTO = new BaseUserPrimaryPositionDTO(); + primaryPositionDTO.setUserId(entity.getUserId()); + primaryPositionDTO.setOrganizeId(entity.getFromOrg()); + primaryPositionDTO.setPositionId(entity.getFromPositionId()); + primaryPositionDTO.setPositionGradesId(entity.getFromRankId()); + primaryPositionDTO.setManagerId(entity.getFromManager()); + ActionResult position = userPrimaryPositionApi.isUserUserPrimaryPosition(primaryPositionDTO); + if (position != null && position.getData()){ + // 更新用户主岗 + BaseUserPrimaryPositionDTO positionDTO = new BaseUserPrimaryPositionDTO(); + positionDTO.setUserId(entity.getUserId()); + positionDTO.setOrganizeId(entity.getToOrg()); + positionDTO.setPositionId(entity.getToPositionId()); + positionDTO.setPositionGradesId(entity.getToRankId()); + positionDTO.setManagerId(entity.getToManager()); + userPrimaryPositionApi.updateUserPrimaryPosition(positionDTO); + } + + /** + * 同步用户考勤组 + */ + if (StringUtils.isNotEmpty(entity.getAttendanceGroup())) { + UserInfo userInfo = UserProvider.getUser(); + GroupUpdateByUserDTO groupUpdateByUserDTO = new GroupUpdateByUserDTO(); + groupUpdateByUserDTO.setTenantId(userInfo.getTenantId()); + groupUpdateByUserDTO.setType(3);// 1入职 2离职 3调岗 + groupUpdateByUserDTO.setToGroupId(entity.getAttendanceGroup()); + groupUpdateByUserDTO.setUserIds(List.of(entity.getUserId())); + personnelOrgUtils.syncAttendanceGroup(groupUpdateByUserDTO); + } + + UserInfo userInfo = UserProvider.getUser(); + staffRosterService.logout(userInfo.getTenantId(), entity.getUserId()); + // 同步薪资详情 + String salaryStructureList = entity.getSalaryStructureListStr(); + if (StringUtils.isNotEmpty(salaryStructureList) && "1".equals(entity.getIsAdjustSalary())) { + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + PositionGradesInfoVO positionGradesInfoVO = personnelOrgUtils.queryRank(toRankId); + PositionEntity positionEntity = personnelOrgUtils.queryPosition(toPositionId); + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(toOrg); + userInfoWithSalary.setRankName(positionGradesInfoVO.getFullName()); + userInfoWithSalary.setRankId(toRankId); + userInfoWithSalary.setFOrgId(toOrg); + userInfoWithSalary.setFOrgName(organizeEntity.getFullName()); + userInfoWithSalary.setPostId(toPositionId); + userInfoWithSalary.setPostName(positionEntity.getFullName()); + // 审核通过同步数据 + List list = JSONUtil.toList(salaryStructureList, FtbPersonnelsSalaryInfo.class); + salaryService.saveTheChangePayInformation(list, + entity.getUserId(), + entity.getPayrollEffectiveDate(), + userInfoWithSalary, + "1", + entity.getReasonsForSalaryAdjustments(), + "2", 0,null, ""); + } + } + + private void transferToWriteGrowth(String entity, FtbPersonnelsStaffTransferPosition entity1, String entity2, FtbPersonnelsStaffRoster roster) { + //写入员工成长记录 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(entity); + addGrowthLogDto.setChangeDate(new Date()); + addGrowthLogDto.setGrowthType(GrowthLogEnum.TRANSFER_POSITION.getCode()); + TransferPositionDto positionDto = BeanUtil.copyProperties(entity1, TransferPositionDto.class); + String json = buildTransferGrowthLogDetail(positionDto,roster.getActualStartDate()); + addGrowthLogDto.setDetail(json); + addGrowthLogDto.setRemarks(entity2); + addGrowthLogDto.setEmployeeId(roster.getSystemWokerId()); + addGrowthLogDto.setActualStartDate(roster.getActualStartDate()); + // 调岗的 + growthLogService.addGrowthLog(addGrowthLogDto); + } + + + /** + * 提取相同code + * @param records + * @param first + * @param second + * @return + */ + protected List extractTheSameCode(List records, + Function first, + Function second) { + return CollUtil.unionAll(records.stream().map(first).collect(Collectors.toList()), + records.stream().map(second).collect(Collectors.toList())); + } + + private void fillOtherInfo(List records) { + Map orgNames = personnelOrgUtils.getOrgNames( + extractTheSameCode(records,TransferPositionDto::getToOrg,TransferPositionDto::getFromOrg)); + Map postNames = personnelOrgUtils.getPostNames( + extractTheSameCode(records,TransferPositionDto::getToPositionId,TransferPositionDto::getFromPositionId)); + Map gradesNames = personnelOrgUtils.getGradesNames( + extractTheSameCode(records,TransferPositionDto::getToRankId,TransferPositionDto::getFromRankId)); + Map toManagerNames = personnelOrgUtils.getUserNames( + extractTheSameCode(records,TransferPositionDto::getToManager,TransferPositionDto::getFromManager)); + List roleIds = records.stream().map(TransferPositionDto::getToRole) + .filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + List newRoleIds = new ArrayList<>(); + if (CollectionUtil.isNotEmpty(roleIds)){ + for (String roleId : roleIds) { + if (roleId.contains(",")){ + newRoleIds.addAll( List.of(roleId.split(","))) ; + }else { + newRoleIds.add(roleId); + } + } + } + Map newRoleNames = personnelOrgUtils.getRoleNames(newRoleIds); + for (TransferPositionDto info : records) { + //补充岗位信息 + // personnelOrgUtils.fillOtherInfoForItem(info); + if (orgNames.containsKey(info.getFromOrg())) { + info.setFromOrgName(orgNames.get(info.getFromOrg())); + } + if (orgNames.containsKey(info.getToOrg())) { + info.setToOrgName(orgNames.get(info.getToOrg())); + } + if (postNames.containsKey(info.getFromPositionId())) { + info.setFromPositionIdName(postNames.get(info.getFromPositionId())); + } + if (postNames.containsKey(info.getToPositionId())) { + info.setToPositionIdName(postNames.get(info.getToPositionId())); + } + + if (gradesNames.containsKey(info.getFromRankId())) { + info.setFromRankIdName(gradesNames.get(info.getFromRankId())); + } + if (gradesNames.containsKey(info.getToRankId())) { + info.setToRankIdName(gradesNames.get(info.getToRankId())); + } + String toRole = info.getToRole(); + String roleName =""; + if (StringUtils.isNotEmpty(toRole)) { + if (toRole.contains(",")) { + String[] split = toRole.split(","); + roleName = Arrays.stream(split).map(newRoleNames::get).collect(Collectors.joining(",")); + } else { + roleName = newRoleNames.get(toRole); + } + info.setToRoleName(roleName); + } + + if (toManagerNames.containsKey(info.getToManager())) { + info.setToManagerName(toManagerNames.get(info.getToManager())); + } + if (toManagerNames.containsKey(info.getFromManager())) { + info.setFromManagerName(toManagerNames.get(info.getFromManager())); + } + } + } + + @Override + public TransferPositionDto getInfo(String id) { + TransferPositionDto info = baseMapper.getInfoForId(id); + if (ObjectUtil.isNotEmpty(info)) { + //补充交接数据 + List goodList = transferPositionHandoverService.list(new QueryWrapper().lambda().eq(FtbPersonnelsStaffTransferPositionHandover::getEnabledMark, 0).eq(FtbPersonnelsStaffTransferPositionHandover::getType, 0).eq(FtbPersonnelsStaffTransferPositionHandover::getStaffTransferPositionId, id)); + List handovers = transferPositionHandoverService.list(new QueryWrapper().lambda().eq(FtbPersonnelsStaffTransferPositionHandover::getEnabledMark, 0).eq(FtbPersonnelsStaffTransferPositionHandover::getStaffTransferPositionId, id)); + Map rosterMap =new HashMap<>(); + if (CollectionUtil.isNotEmpty(handovers)) { + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.in(FtbPersonnelsStaffRoster::getUserId, handovers.stream().map(FtbPersonnelsStaffTransferPositionHandover::getToUserId).collect(Collectors.toList())); + List staffRosters = staffRosterService.list(lambdaed); + rosterMap= staffRosters.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, Function.identity(), (k1, k2) -> k1)); + } + if (CollectionUtil.isNotEmpty(goodList)) { + Map finalRosterMap = rosterMap; + goodList.forEach(item->{ + if(finalRosterMap.containsKey(item.getToUserId())){ + item.setToUserId(finalRosterMap.get(item.getToUserId()).getSystemWokerId()); + }}); + info.setGoodsPositionHandoverList(BeanUtil.copyToList(goodList, FtbPersonnelsStaffTransferPositionHandoverDto.class)); + } + List workList = transferPositionHandoverService.list(new QueryWrapper().lambda().eq(FtbPersonnelsStaffTransferPositionHandover::getEnabledMark, 0).eq(FtbPersonnelsStaffTransferPositionHandover::getType, 1).eq(FtbPersonnelsStaffTransferPositionHandover::getStaffTransferPositionId, id)); + if (CollectionUtil.isNotEmpty(workList)) { + Map finalRosterMap = rosterMap; + workList.forEach(item->{ + if(finalRosterMap.containsKey(item.getToUserId())) { + item.setToUserId(finalRosterMap.get(item.getToUserId()).getSystemWokerId()); + } + }); + info.setWorkerPositionHandoverList(BeanUtil.copyToList(workList, FtbPersonnelsStaffTransferPositionHandoverDto.class)); + } + if(StringUtils.isNotEmpty(info.getSalaryStructureStr())){ + List parseArray = JSONObject.parseArray(info.getSalaryStructureStr(), FtbPersonnelsSalaryInfo.class); + info.setSalaryStructureList(parseArray); + } + //补充岗位信息 + personnelOrgUtils.fillOtherInfoForItem(info); + List list = personnelOrgUtils.getUserOrgBoundInfo(info.getUserId()); + if (info.getFromManager() != null) { + UserEntity userEntity = personnelOrgUtils.queryUserInfo(info.getFromManager()); + info.setFromManagerName(userEntity !=null ? userEntity.getRealName() : null); + } + info.setOrgList(list); + fillFormDataToTransferItem(info); + //需要审批 并且审批还没有通过时需要返回 + if (info.getIsNeedCheck() == 0) { + FtbPersonnelsAuditInfoVO auditInfoVO = subConfigService.queryAuditSubConfig(info.getFromOrg(), FtbPersonnelsCofigEnum.POST_TRANSFER_APPROVAL_CONFIGURATION); + info.setAuditInfo(auditInfoVO); + } + if (StringUtils.isNotEmpty(info.getAttendanceGroup())){ + AttendanceGroup attendanceGroup = attendanceGroupApi.queryTheNameOfTheAttendanceGroup(info.getAttendanceGroup()); + if (attendanceGroup != null) info.setAttendanceGroupName(attendanceGroup.getGroupName()); + } + //当前薪酬 + FtbPersonnelsStaffRoster staffRoster = staffRosterService.queryRosterInfoByUserId(info.getUserId()); + List ftbPersonnelsSalaryInfos = personnelSalaryServiceImpl.innerGetTheCurrentPersonSSalary(staffRoster, "", ""); + BigDecimal sum = new BigDecimal(0); + if (CollectionUtil.isNotEmpty(ftbPersonnelsSalaryInfos)) { + for (FtbPersonnelsSalaryInfo ftbPersonnelsSalaryInfo : ftbPersonnelsSalaryInfos) { + if (ftbPersonnelsSalaryInfo.getFValueType().equals("1") && StringUtils.isNotEmpty(ftbPersonnelsSalaryInfo.getFSetValue())) { + sum = sum.add(new BigDecimal(ftbPersonnelsSalaryInfo.getFSetValue())); + } + } + } + sum = sum.setScale(2, RoundingMode.DOWN); + info.setCurrSalary(sum); + } + + return info; + } + + + @SneakyThrows + @Override + @Transactional(rollbackFor = Exception.class) + public String insertData(SaveTransferReq req) { + String returnData = ""; + if (req.getToMoney() == null) { + req.setToMoney(BigDecimal.ZERO); + } + UserInfo user = UserProvider.getUser(); + String loginUserId = user.getUserId(); + if (!"0".equals(req.getSource()) && req.getUserId().equals(loginUserId)){ + throw new RuntimeException("办理调岗无法给自己进行手动调岗!"); + } + //查询用户是否已经有调岗流程 + TransferPositionDto entity = baseMapper.queryNoCompleteForUserId(req.getUserId()); + if (null != entity) { + throw new RuntimeException("用户已经存在调岗审批流程"); + } + // 校验直属主管是否为试岗员工 + if (StringUtils.isNotEmpty(req.getToManager())){ + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbPersonnelsStaffRoster::getUserId,req.getToManager()); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(lambdaed); + if (one.getWorkerStatus().equals("306")) throw new RuntimeException("试岗员工:"+one.getName()+"不能作为直属主管!"); + } + //查询用户花名册信息 + FtbPersonnelsStaffRoster rosterEntity = staffRosterService.queryRosterInfoByUserId(req.getUserId()); + if (null == rosterEntity) { + throw new RuntimeException("用户花名册信息不存在"); + } + if (rosterEntity.getEnabledMark() == 1) { + throw new RuntimeException("用户花名册信息不存在"); + } + if ("304".equals(rosterEntity.getWorkerStatus())) { + throw new RuntimeException("员工处于离职中,不能调岗"); + } + if ("305".equals(rosterEntity.getWorkerStatus())) { + throw new RuntimeException("员工已经离职"); + } + if (req.getFromOrg().equals(req.getToOrg()) && + req.getFromPositionId().equals(req.getToPositionId()) + && req.getFromRankId().equals(req.getToRankId())){ + throw new RuntimeException("员工调岗的组织岗位职等,无变化,不能办理调岗"); + } + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(req.getToOrg()); + checkExistByPositionDTO.setPositionId(req.getToPositionId()); + checkExistByPositionDTO.setPositionGradesId(req.getToRankId()); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + throw new RuntimeException( "该员工调岗的目标岗位不存在,无法继续提交!"); + } + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO1 = new CheckExistByPositionDTO(); + checkExistByPositionDTO1.setOrganizeId(req.getFromOrg()); + checkExistByPositionDTO1.setPositionId(req.getFromPositionId()); + checkExistByPositionDTO1.setPositionGradesId(req.getFromRankId()); + ActionResult booleanActionResult2 = positionApi.checkExistByPositionMore(checkExistByPositionDTO1); + if (booleanActionResult2 == null || !booleanActionResult2.getData()){ + throw new RuntimeException( "该员工的岗位在提交调岗后发生了变更,无法继续提交!"); + } + List list = personnelOrgUtils.getUserOrgBoundInfo(req.getUserId()); + if (CollUtil.isNotEmpty(list) && list.size() > 1){ + // 员工所属组织 + for (WorkerGroupDataDto dataDto : list) { + // 员工调岗组织岗位 都相同 不能有不同职等 + if (!( req.getToOrg().equals(req.getFromOrg()) && req.getToPositionId().equals(req.getFromPositionId())) + && (dataDto.getAffiliatedOrg().equals(req.getToOrg()) && + dataDto.getAffiliatedPosition().equals(req.getToPositionId()))){ + throw new RuntimeException("您在目标组织中已所属于该岗位,不可重复选择同一组织下相同岗位!"); + } + } + } + // ,1转正 2,调岗, 3离职, 4晋升 + String checkFlag = ftbPersonnelsAuditRunTaskService.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(rosterEntity.getUserId()); + if (null != checkFlag) { + if ("1".equals(checkFlag)) { + throw new RuntimeException("当前用户正在办理转正,不能办理调岗"); + } else if ("2".equals(checkFlag)) { + throw new RuntimeException("当前用户正在办理调岗,不能办理调岗"); + } else if ("3".equals(checkFlag)) { + throw new RuntimeException("当前用户正在办理离职,不能办理调岗"); + } else if ("4".equals(checkFlag)) { + throw new RuntimeException("当前用户正在办理晋升,不能办理调岗"); + } + } + String toRankId = req.getToRankId(); + String toPositionId = req.getToPositionId(); + String toOrg = req.getToOrg(); + if (toOrg.equals(req.getFromOrg()) + && toPositionId.equals(req.getFromPositionId()) + && toRankId.equals(req.getFromRankId()) + ) { + throw new RuntimeException("原组织岗位职等和新组织岗位职等相同,无需调岗"); + } + if (rosterEntity.getUserId().equals(req.getToManager())) { + throw new RuntimeException("调岗后的直属主管不能是自己"); + } + // v1.1 添加门店负责人校验 + // 跨组织校验 + if (!req.getFromOrg().equals(req.getToOrg())) { + LambdaQueryWrapper storeEntityLambdaQueryWrapper = Wrappers.lambdaQuery(); + storeEntityLambdaQueryWrapper.eq(StoreEntity::getStoreheaduserid, req.getUserId()); + long count = storeService.count(storeEntityLambdaQueryWrapper); + if (count > 0) return "1"; + } + //写入调岗表 + FtbPersonnelsStaffTransferPosition insertEntity = BeanUtil.copyProperties(req, FtbPersonnelsStaffTransferPosition.class); + // 修改id + insertEntity.setEnabledMark(0); + insertEntity.setPhone(rosterEntity.getPhone()); + insertEntity.setIsNeedCheck(1); + insertEntity.setTransferDate(new Date()); + insertEntity.setCreatorTime(new Date()); + insertEntity.setOpUser(loginUserId); + insertEntity.setSource(2); + insertEntity.setOpUserName(UserProvider.getUser().getUserName()); + // 添加薪资详情 + List salaryStructureList = req.getSalaryStructureList(); + if (CollUtil.isEmpty(salaryStructureList) && "1".equals(req.getIsAdjustSalary()) ){ + throw new RuntimeException("目标岗位暂无薪资结构,无法进行薪酬调整;建议选择是否调整薪酬为“否”!"); + } + Integer isDelete = req.getIsDelete(); + if ("1".equals(req.getIsAdjustSalary()) && isDelete == null){ + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String format = simpleDateFormat.format(req.getPayrollEffectiveDate()); + ActionResult> historyByChangeDate = querySalaryApi.getHistoryByChangeDate(format, req.getUserId()); + if (historyByChangeDate == null){ + throw new RuntimeException("获取薪酬数据失败!"); + } + List data = historyByChangeDate.getData(); + if (CollUtil.isNotEmpty(data) && com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(data.get(0).getFRemark())) return "5"; + // 没有薪酬默认不做废 + isDelete = 0; + } + String remarks = req.getRemarks(); + if (StringUtils.isNotEmpty(req.getId()) && StringUtils.isEmpty(remarks)){ + insertEntity.setRemarks(null); + }else{ + insertEntity.setRemarks(remarks); + } + saveOrUpdate(insertEntity); + // 调岗交接表 + raisePublicPart(req, req.getId(), rosterEntity, insertEntity); + //是否需要审批:0、需要审批 1、不需要审批 + if ("1".equals(req.getIsAdjustSalary())) { + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + PositionGradesInfoVO positionGradesInfoVO = personnelOrgUtils.queryRank(toRankId); + PositionEntity positionEntity = personnelOrgUtils.queryPosition(toPositionId); + OrganizeEntity organizeEntity = personnelOrgUtils.queryOrganizeInfo(toOrg); + userInfoWithSalary.setRankName(positionGradesInfoVO.getFullName()); + userInfoWithSalary.setRankId(toRankId); + userInfoWithSalary.setFOrgId(toOrg); + userInfoWithSalary.setFOrgName(organizeEntity.getFullName()); + userInfoWithSalary.setPostId(toPositionId); + userInfoWithSalary.setPostName(positionEntity.getFullName()); + // 薪资同步到薪酬 + salaryService.saveTheChangePayInformation(salaryStructureList, + req.getUserId(), + req.getPayrollEffectiveDate(), + userInfoWithSalary, + "1", + insertEntity.getReasonsForSalaryAdjustments(), + "2", isDelete,null, ""); + + } + //写入调岗薪资变化流程 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(req.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(insertEntity.getToMoney()); + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.TRANSFER_POSITION.getCode()); + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + //写入员工成长记录 + transferToWriteGrowth(req.getUserId(), insertEntity, remarks, rosterEntity); + //修改用户组织岗位职等直属主管 + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + updateUserManagerBoundDTO.setUserId(rosterEntity.getUserId()); + updateUserManagerBoundDTO.setOldOrgId(insertEntity.getFromOrg()); + updateUserManagerBoundDTO.setOldPositionId(insertEntity.getFromPositionId()); + updateUserManagerBoundDTO.setOldPositionGradesId(insertEntity.getFromRankId()); + updateUserManagerBoundDTO.setOldManagerId(insertEntity.getFromManager()); + updateUserManagerBoundDTO.setOrgId(insertEntity.getToOrg()); + updateUserManagerBoundDTO.setPositionId(insertEntity.getToPositionId()); + updateUserManagerBoundDTO.setPositionGradesId(insertEntity.getToRankId()); + updateUserManagerBoundDTO.setManagerId(insertEntity.getToManager()); + personnelOrgUtils.updateSysUserBound(updateUserManagerBoundDTO); + BaseUserPrimaryPositionDTO primaryPositionDTO = new BaseUserPrimaryPositionDTO(); + primaryPositionDTO.setUserId(rosterEntity.getUserId()); + primaryPositionDTO.setOrganizeId(insertEntity.getFromOrg()); + primaryPositionDTO.setPositionId(insertEntity.getFromPositionId()); + primaryPositionDTO.setPositionGradesId(insertEntity.getFromRankId()); + primaryPositionDTO.setManagerId(insertEntity.getFromManager()); + ActionResult position = userPrimaryPositionApi.isUserUserPrimaryPosition(primaryPositionDTO); + if (position != null && position.getData()){ + // 更新用户主岗 + BaseUserPrimaryPositionDTO positionDTO = new BaseUserPrimaryPositionDTO(); + positionDTO.setUserId(rosterEntity.getUserId()); + positionDTO.setOrganizeId(insertEntity.getToOrg()); + positionDTO.setPositionId(insertEntity.getToPositionId()); + positionDTO.setPositionGradesId(insertEntity.getToRankId()); + positionDTO.setManagerId(insertEntity.getToManager()); + userPrimaryPositionApi.updateUserPrimaryPosition(positionDTO); + } + //修改用户角色 + boolean flag = insertEntity.getToOrg().equals(insertEntity.getFromOrg()); + asyncRole(insertEntity.getUserId(),flag); + //修改薪资 + FtbPersonnelsStaffRoster staffRoster = new FtbPersonnelsStaffRoster(); + staffRoster.setId(rosterEntity.getId()); + staffRoster.setTransferPosition(2); + staffRoster.setCurrSalary(insertEntity.getToMoney()); + staffRosterService.updateById(staffRoster); + /** + * 同步用户考勤组 + */ + if (StringUtils.isNotEmpty(req.getAttendanceGroup())) { + GroupUpdateByUserDTO groupUpdateByUserDTO = new GroupUpdateByUserDTO(); + groupUpdateByUserDTO.setTenantId(user.getTenantId()); + groupUpdateByUserDTO.setType(3);// 1入职 2离职 3调岗 + groupUpdateByUserDTO.setToGroupId(req.getAttendanceGroup()); + groupUpdateByUserDTO.setUserIds(List.of(rosterEntity.getUserId())); + personnelOrgUtils.syncAttendanceGroup(groupUpdateByUserDTO); + } + staffRosterService.logout(user.getTenantId(),rosterEntity.getUserId()); + + return returnData; + } + + private void raisePublicPart(SaveTransferReq req, String id, FtbPersonnelsStaffRoster rosterEntity, FtbPersonnelsStaffTransferPosition insertEntity) { + if (null != id) { + //删除工作物品交接表 + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda().set(FtbPersonnelsStaffTransferPositionHandover::getEnabledMark, 1) + .eq(FtbPersonnelsStaffTransferPositionHandover::getStaffTransferPositionId, id); + transferPositionHandoverService.update(updateWrapper); + } + //写入调岗交接表 + List handoverList = new ArrayList<>(); + List workerPositionHandoverList = req.getWorkerPositionHandoverList(); + if (CollectionUtil.isNotEmpty(workerPositionHandoverList)) { + for (TransferPositionHandoverReq handoverReq : workerPositionHandoverList) { + if(StringUtils.isNotEmpty(handoverReq.getToUserId()) && rosterEntity.getUserId().equals(handoverReq.getToUserId())){ + throw new RuntimeException("工作:"+handoverReq.getName()+" 的交接人不能是自己"); + } + FtbPersonnelsStaffTransferPositionHandover handover = new FtbPersonnelsStaffTransferPositionHandover(); + handover.setStaffTransferPositionId(insertEntity.getId()); + handover.setType(1); + handover.setName(handoverReq.getName()); + handover.setToUserId(handoverReq.getToUserId()); + handover.setToUserName(handoverReq.getToUserName()); + handover.setEnabledMark(0); + handoverList.add(handover); + } + } + List goodsPositionHandoverList = req.getGoodsPositionHandoverList(); + if (CollectionUtil.isNotEmpty(goodsPositionHandoverList)) { + for (TransferPositionHandoverReq handoverReq : goodsPositionHandoverList) { + if(StringUtils.isNotEmpty(handoverReq.getToUserId()) && rosterEntity.getUserId().equals(handoverReq.getToUserId())){ + throw new RuntimeException("物品:"+handoverReq.getName()+" 的接收人不能是自己"); + } + FtbPersonnelsStaffTransferPositionHandover handover = new FtbPersonnelsStaffTransferPositionHandover(); + handover.setStaffTransferPositionId(insertEntity.getId()); + handover.setType(0); + handover.setName(handoverReq.getName()); + handover.setToUserId(handoverReq.getToUserId()); + handover.setToUserName(handoverReq.getToUserName()); + handover.setEnabledMark(0); + handoverList.add(handover); + } + } + if (CollectionUtil.isNotEmpty(handoverList)) { + transferPositionHandoverService.saveBatch(handoverList); + } + } + + /** + * 重新审批 + * + * @param id + * @param req + */ + @Override + @Transactional + public void reApproval(String id, SaveTransferReq req) { + FtbPersonnelsStaffTransferPosition entity = baseMapper.selectById(id); + // 校验直属主管是否为试岗员工 + if (StringUtils.isNotEmpty(req.getToManager())){ + LambdaQueryWrapper lambdaed = Wrappers.lambdaQuery(); + lambdaed.eq(FtbPersonnelsStaffRoster::getUserId,req.getToManager()); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(lambdaed); + if (one.getWorkerStatus().equals("306")) throw new RuntimeException("试岗员工:"+one.getName()+"不能作为直属主管!"); + } + // 校验 + verify(entity.getToOrg(), entity.getToPositionId(), entity.getToRankId()); + if ( entity.getEnabledMark() == 1) { + throw new RuntimeException("调岗信息不存在"); + } + if (entity.getIsNeedCheck() == 1) { + throw new RuntimeException("该员工调岗不需要审核"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + throw new RuntimeException("该员工调岗审核已经通过"); + } + + //查询用户花名册信息 + FtbPersonnelsStaffRoster rosterEntity = staffRosterService.queryRosterInfoByUserId(req.getUserId()); + if (null == rosterEntity) { + throw new RuntimeException("用户花名册信息不存在"); + } + if (rosterEntity.getEnabledMark() == 1) { + throw new RuntimeException("用户花名册信息不存在"); + } + if ("305".equals(rosterEntity.getWorkerStatus())) { + throw new RuntimeException("员工已经离职"); + } + + //写入调岗表 + FtbPersonnelsStaffTransferPosition newEntity = BeanUtil.copyProperties(req, FtbPersonnelsStaffTransferPosition.class); + newEntity.setEnabledMark(0); + newEntity.setCheckStatus(0); + newEntity.setId(entity.getId()); + newEntity.setUserId(entity.getUserId()); + if (req.getIsNeedCheck() == 1) { + newEntity.setTransferDate(new Date()); + } else { + newEntity.setCheckStatus(1); + newEntity.setTransferDate(new Date()); + } + + String loginUserId = UserProvider.getLoginUserId(); + newEntity.setOpUser(loginUserId); + newEntity.setOpUserName(UserProvider.getUser().getUserName()); + newEntity.setCreatorTime(new Date()); + // + baseMapper.updateById(newEntity); + //删除工作物品交接表 + UpdateWrapper updateWrapper = new UpdateWrapper<>(); + updateWrapper.lambda().set(FtbPersonnelsStaffTransferPositionHandover::getEnabledMark, 1) + .eq(FtbPersonnelsStaffTransferPositionHandover::getStaffTransferPositionId, newEntity.getId()); + transferPositionHandoverService.update(updateWrapper); + //写入调岗交接表 + List handoverList = new ArrayList<>(); + List workerPositionHandoverList = req.getWorkerPositionHandoverList(); + if (CollectionUtil.isNotEmpty(workerPositionHandoverList)) { + for (TransferPositionHandoverReq handoverReq : workerPositionHandoverList) { + FtbPersonnelsStaffTransferPositionHandover handover = new FtbPersonnelsStaffTransferPositionHandover(); + handover.setStaffTransferPositionId(newEntity.getId()); + handover.setType(1); + handover.setName(handoverReq.getName()); + handover.setToUserId(handoverReq.getToUserId()); + handover.setToUserName(handoverReq.getToUserName()); + handover.setEnabledMark(0); + handoverList.add(handover); + } + } + List goodsPositionHandoverList = req.getGoodsPositionHandoverList(); + if (CollectionUtil.isNotEmpty(goodsPositionHandoverList)) { + for (TransferPositionHandoverReq handoverReq : goodsPositionHandoverList) { + FtbPersonnelsStaffTransferPositionHandover handover = new FtbPersonnelsStaffTransferPositionHandover(); + handover.setStaffTransferPositionId(newEntity.getId()); + handover.setType(0); + handover.setName(handoverReq.getName()); + handover.setToUserId(handoverReq.getToUserId()); + handover.setToUserName(handoverReq.getToUserName()); + handover.setEnabledMark(0); + handoverList.add(handover); + } + } + if (CollectionUtil.isNotEmpty(handoverList)) { + transferPositionHandoverService.saveBatch(handoverList); + } + + //是否需要审批:0、需要审批 1、不需要审批 + if (req.getIsNeedCheck() == 0) { + // 创建审批 + String runTaskId = auditRunTaskService.startTheReviewProcess(newEntity.getId(), + FtbPersonnelsCofigEnum.POST_TRANSFER_APPROVAL_CONFIGURATION.getConfigType(), + newEntity.getFromOrg(), newEntity.getUserId()); + // 同步id + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.eq(FtbPersonnelsStaffTransferPosition::getId, newEntity.getId()); + lambdaUpdate.set(FtbPersonnelsStaffTransferPosition::getTaskInfoId, runTaskId); + baseMapper.update(null, lambdaUpdate); + //花名册设置用户调岗中 + FtbPersonnelsStaffRoster staffRoster = new FtbPersonnelsStaffRoster(); + staffRoster.setId(rosterEntity.getId()); + staffRoster.setTransferPosition(1); + staffRosterService.updateById(staffRoster); + } else { + + //写入调岗薪资变化流程 + AddSalaryChangeLogDto addSalaryChangeLogDto = new AddSalaryChangeLogDto(); + addSalaryChangeLogDto.setUserId(req.getUserId()); + addSalaryChangeLogDto.setChangeDate(new Date()); + addSalaryChangeLogDto.setSalary(newEntity.getToMoney()); + addSalaryChangeLogDto.setChangeType(SalaryChangeTypeEnum.TRANSFER_POSITION.getCode()); + salaryChangeLogService.addSalaryChangeLog(addSalaryChangeLogDto); + //写入员工成长记录 + transferToWriteGrowth(req.getUserId(), newEntity, req.getRemarks(), rosterEntity); + //修改用户组织岗位职等直属主管 + UpdateUserManagerBoundDTO updateUserManagerBoundDTO = new UpdateUserManagerBoundDTO(); + updateUserManagerBoundDTO.setUserId(rosterEntity.getUserId()); + updateUserManagerBoundDTO.setOldOrgId(newEntity.getFromOrg()); + updateUserManagerBoundDTO.setOldPositionId(newEntity.getFromPositionId()); + updateUserManagerBoundDTO.setOldPositionGradesId(newEntity.getFromRankId()); + updateUserManagerBoundDTO.setOldManagerId(newEntity.getFromManager()); + updateUserManagerBoundDTO.setOrgId(newEntity.getToOrg()); + updateUserManagerBoundDTO.setPositionId(newEntity.getToPositionId()); + updateUserManagerBoundDTO.setPositionGradesId(newEntity.getToRankId()); + updateUserManagerBoundDTO.setManagerId(newEntity.getToManager()); + personnelOrgUtils.updateSysUserBound(updateUserManagerBoundDTO); + //修改薪资 + FtbPersonnelsStaffRoster staffRoster = new FtbPersonnelsStaffRoster(); + staffRoster.setId(rosterEntity.getId()); + staffRoster.setTransferPosition(2); + staffRoster.setCurrSalary(newEntity.getToMoney()); + staffRosterService.updateById(staffRoster); + //修改用户角色 + boolean flag = newEntity.getToOrg().equals(newEntity.getFromOrg()); + asyncRole(newEntity.getUserId(),flag); + } + } + + /** + * 同步用户角色 + * + * @param userId + * @param flag 标识是否跨组织 + */ + private void asyncRole(String userId, boolean flag) { + // 更改用户角色逻辑 + // 2.1 取消调整角色字段,oa审批单同理 + // 2.2 若变更组织,组织角色需清除,全局角色不清除。不变更组 + // 织则不清除角色,维持原样 + ActionResult> listActionResult = roleApi.listUsersBound(List.of(userId)); + if (listActionResult == null ) throw new RuntimeException("获取用户角色失败!"); + List data = listActionResult.getData(); + // 查询用户绑定角色 + UserBoundRolesVO userBoundRolesVO = data.stream().filter(item -> userId.equals(item.getId())).findFirst().orElse(new UserBoundRolesVO()); + List roleBounds = userBoundRolesVO.getRoleBounds(); + List roleIds; + if (flag){ + roleIds = roleBounds.stream().map(RoleListVO::getId).collect(Collectors.toList()); + }else { + // 清除跨组织的角色 + List targetRoleIds = roleBounds.stream().filter(item -> item.getGlobalMark() != 1).map(RoleListVO::getId).collect(Collectors.toList()); + RemoveUserRolesDTO rolesDTO = new RemoveUserRolesDTO(); + rolesDTO.setTargetRoleIds(targetRoleIds); + rolesDTO.setUserIds(List.of(userId)); + roleApi.removeRoleRelationBatch(rolesDTO); + roleIds = roleBounds.stream().filter(item -> item.getGlobalMark() == 1).map(RoleListVO::getId).collect(Collectors.toList()); + } + // 无角色信息变更 + if (CollUtil.isEmpty(roleIds)) return; + SynUserBoundRoleDTO dto = new SynUserBoundRoleDTO(); + dto.setUserId(userId); + dto.setRoleIds(roleIds); + personnelOrgUtils.sysUserRole(dto); + } + + private String buildTransferGrowthLogDetail(TransferPositionDto entity, Date actualStartDate) { + personnelOrgUtils.fillOtherInfoForItem(entity); + TransferGrowthLogDto dto = TransferGrowthLogDto.covert(entity); + dto.setActualStartDate(actualStartDate); + return JSONUtil.toJsonStr(dto); + } + + @Override + public void updateData(SavePaperReq req) { + + } + + @Override + public void deleteData(String id) { + FtbPersonnelsStaffTransferPosition entity = baseMapper.selectById(id); + if (null == entity) { + throw new RuntimeException("调岗信息不存在"); + } + if (entity.getEnabledMark() == 1) { + throw new RuntimeException("调岗信息已经删除"); + } + if (entity.getIsNeedCheck() == 1) { + throw new RuntimeException("调岗信息不能删除"); + } + if (entity.getIsNeedCheck() == 0 && (entity.getCheckStatus() == 0 || entity.getCheckStatus() == 1 || entity.getCheckStatus() == 2)) { + throw new RuntimeException("调岗信息不能删除"); + } + entity.setEnabledMark(1); + baseMapper.updateById(entity); + } + + /** + * 审批 + * + * @param dto + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void approval(EmploymentApplyCheckDto dto) { + String id = dto.getId(); + FtbPersonnelsStaffTransferPosition entity = baseMapper.selectById(id); + // 校验 + // 选择通过需要校验组织是否存在 + String toRankId = entity.getToRankId(); + String toPositionId = entity.getToPositionId(); + String toOrg = entity.getToOrg(); + if (1 == dto.getIsPass()) verify(toOrg, toPositionId, toRankId); + if (1 == dto.getIsPass()) verify(entity.getFromOrg(), entity.getFromPositionId(), entity.getFromRankId()); + if ( entity.getEnabledMark() == 1) { + throw new RuntimeException("调岗信息不存在"); + } + if (entity.getIsNeedCheck() == 1) { + throw new RuntimeException("该员工调岗不需要审核"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 2) { + throw new RuntimeException("该员工调岗审核已经通过"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 3) { + throw new RuntimeException("该员工调岗审核已经不通过"); + } + if (entity.getIsNeedCheck() == 0 && entity.getCheckStatus() == 4) { + throw new RuntimeException("该员工调岗审核已经撤销"); + } + FtbPersonnelsStaffRoster roster = staffRosterService.queryRosterInfoByUserId(entity.getUserId()); + FtbPersonnelsAuditDto checkDto = new FtbPersonnelsAuditDto(); + checkDto.setBusinessId(entity.getId()); + if (ObjectUtil.isNotEmpty(dto.getIsConformSalary())) { + checkDto.setDoesTheSalaryComplyWith(dto.getIsConformSalary()); + } + if (ObjectUtil.isNotEmpty(dto.getMsg())) { + checkDto.setApprovalComments(dto.getMsg()); + } + if (ObjectUtil.isNotEmpty(dto.getIsPass())) { + checkDto.setFlag(String.valueOf(dto.getIsPass())); + } + FtbPersonnelsAuditTaskEnum code = auditRunTaskService.performReview(checkDto); + switch (code) { + case PENDING: + entity.setCheckStatus(0); + break; + case UNDER_REVIEW: + entity.setCheckStatus(1); + break; + case EXAMINATION_PASSED: + entity.setCheckStatus(2); + //写入调岗薪资变化流程 + raisePublicPart(entity, roster, toOrg, toPositionId, toRankId); + break; + case AUDIT_NOT_PASSED: + entity.setCheckStatus(3); + roster.setTransferPosition(3); + staffRosterService.updateById(roster); + break; + case CANCEL: + entity.setCheckStatus(4); + break; + default: + entity.setCheckStatus(3); + break; + } + baseMapper.updateById(entity); + + } + + /** + * 校验岗位是否存在 + */ + private void verify(String organizeId,String positionId,String positionGradesId) { + // 校验岗位是否还存在 + CheckExistByPositionDTO checkExistByPositionDTO = new CheckExistByPositionDTO(); + checkExistByPositionDTO.setOrganizeId(organizeId); + checkExistByPositionDTO.setPositionId(positionId); + checkExistByPositionDTO.setPositionGradesId(positionGradesId); + ActionResult booleanActionResult = positionApi.checkExistByPositionMore(checkExistByPositionDTO); + if (booleanActionResult == null || !booleanActionResult.getData()){ + throw new RuntimeException("该员工的岗位在提交调岗审批后发生了变更,无法继续审批;请选择“不通过”结束审批流程!"); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTransferManageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTransferManageServiceImpl.java new file mode 100644 index 0000000..36451ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTransferManageServiceImpl.java @@ -0,0 +1,638 @@ +package jnpf.personnels.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.engine.FlowTaskApi; +import jnpf.engine.entity.FlowTaskEntity; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.turnover.FtbPersonnelsTurnoverHandoverDTO; +import jnpf.model.personnels.po.FtbPersonnelsSecondmentManagement; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsTransferManage; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.req.transfer.FtbHandleTransferDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferOaDTO; +import jnpf.model.personnels.req.transfer.FtbHandleTransferQueryDTO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferDetailsVO; +import jnpf.model.personnels.vo.transfer.FtbHandleTransferPageVO; +import jnpf.model.vo.UserSalaryHistoryVo; +import jnpf.permission.V2GradesApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.UserBoundInfoDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.mapper.FtbPersonnelsTransferManageMapper; +import jnpf.personnels.msg.PersonnelsConsumerSourceMsg; +import jnpf.personnels.service.FtbPersonnelsAuditRunTaskService; +import jnpf.personnels.service.FtbPersonnelsSecondmentManagementService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.service.FtbPersonnelsTransferManageService; +import jnpf.personnels.utils.PersonSalaryUtils; +import jnpf.salary.QuerySalaryApi; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsTransferManageServiceImpl extends ServiceImpl implements FtbPersonnelsTransferManageService { + @Resource + private PersonnelsConsumerSourceMsg secondmentConsumerSourceMsg; + + @Resource + private PermissionsUtils permissionsUtils; + + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + @Resource + private V2UserApi v2UserApi; + + @Resource + private V2OrganizeApi v2OrganizeApi; + + @Resource + private V2PositionApi v2PositionApi; + + @Resource + private V2GradesApi v2GradesApi; + @Resource + private FtbPersonnelsAuditRunTaskService ftbPersonnelsAuditRunTaskService; + + @Resource + private PersonSalaryUtils personSalaryUtils; + + @Resource + private QuerySalaryApi querySalaryApi; + + @Resource + private FlowTaskApi flowTaskApi; + + @Resource + private FtbPersonnelsSecondmentManagementService ftbPersonnelSecondmentManagementService; + + @Override + public Page transferManageList(Page page, FtbHandleTransferQueryDTO ftbHandleTransferQueryDTO) { + Page ftbHandleTransferPageVOPage = this.baseMapper.transferManageList(page, ftbHandleTransferQueryDTO); + UserInfo userInfo = UserProvider.getUser(); + ftbHandleTransferPageVOPage.getRecords().forEach(a -> { + // 判断审批Id不能为空 + if (StrUtil.isNotBlank(a.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(a.getTaskInfoId()); + a.setCurrentSponsor(0); + if (userInfo.getUserId().equals(byTaskId.getCreatorUserId())) { + a.setCurrentSponsor(1); + } + } + }); + return ftbHandleTransferPageVOPage; + } + + @Override + public Page transferListApp(Page page, String userName) { + // 当前登录人权限 + UserInfo userInfo = UserProvider.getUser(); + List dataPermissions = null; + if (!userInfo.getIsAdministrator()) { + dataPermissions = permissionsUtils.obtainPersonnelUserIdDataPermissions(userInfo.getUserId(), PermissionsEnums.PERSONNEL_MANAGEMENT_APP.getValue(), "APP"); + } + Page result = this.baseMapper.transferListApp(page, dataPermissions, userName); + result.getRecords().forEach(a -> { + // 判断审批Id不能为空 + if (StrUtil.isNotBlank(a.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(a.getTaskInfoId()); + a.setCurrentSponsor(0); + if (userInfo.getUserId().equals(byTaskId.getCreatorUserId())) { + a.setCurrentSponsor(1); + } + } + }); + return result; + } + + @Override + public Page myTransferApp(Page page) { + String userId = UserProvider.getUser().getUserId(); + Page result = this.baseMapper.transferListApp(page, List.of(userId), null); + return result; + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public String handleTransfer(FtbHandleTransferDTO ftbHandleTransferDTO) { + // 校验该员工(调动的员工)是否处于其他流程中 + String checkIsInTheProcessForOA = ftbPersonnelsAuditRunTaskService.checkWhetherTheCheckIsInTheProcessForOA(ftbHandleTransferDTO.getUserId()); + if (StrUtil.isNotBlank(checkIsInTheProcessForOA)) { + throw new RuntimeException(checkIsInTheProcessForOA); + } + // 薪资作废 + if (ftbHandleTransferDTO.getIsAdjustSalary() == 1 && ftbHandleTransferDTO.getIsDelete() == null) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); + String format = simpleDateFormat.format(ftbHandleTransferDTO.getEffectiveDate()); + ActionResult> historyByChangeDate = null; + try { + historyByChangeDate = querySalaryApi.getHistoryByChangeDate(format, ftbHandleTransferDTO.getUserId()); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (historyByChangeDate == null) { + throw new RuntimeException("获取薪酬数据失败!"); + } + List data = historyByChangeDate.getData(); + if (CollUtil.isNotEmpty(data)) return "5"; + // 没有薪酬默认不做废 + ftbHandleTransferDTO.setIsDelete(0); + } + // 员工本人不能给自己办理调动 + if (ftbHandleTransferDTO.getUserId().equals(UserProvider.getUser().getUserId())) { + throw new RuntimeException("员工本人不能给自己办理调动"); + } + // 数据校验 + transferDataCheck(ftbHandleTransferDTO); + FtbPersonnelsTransferManage ftbPersonnelsTransferManage = ftbHandleTransferDTO.convert(ftbHandleTransferDTO); + ftbPersonnelsTransferManage.setProcessingStatus(6); + // 调动生效执行 + if (ftbPersonnelsTransferManage.getImplementType() == 1) { + // 同步数据至组织架构 + syncDataToOrganization(ftbHandleTransferDTO.getUserId(), ftbHandleTransferDTO.getAfterOrgId(), + ftbHandleTransferDTO.getAfterPostId(), ftbHandleTransferDTO.getAfterRankId(), ftbHandleTransferDTO.getAfterTeamId(), + ftbHandleTransferDTO.getAfterDirectSupervisorId(), UserProvider.getUser().getTenantId()); + // 账号下线 + accountOffline(UserProvider.getUser().getTenantId(), ftbHandleTransferDTO.getUserId()); + } + // 同步数据至薪酬 + if (ftbHandleTransferDTO.getIsAdjustSalary() == 1) { + String recordId = personSalaryUtils.syncSalaryData( + ftbPersonnelsTransferManage.getSalaryStructureList(), + ftbHandleTransferDTO.getUserId(), + ftbHandleTransferDTO.getAfterOrgId(), + ftbHandleTransferDTO.getAfterPostId(), + ftbHandleTransferDTO.getAfterRankId(), + "1", + ftbHandleTransferDTO.getEffectiveDate(), + ftbHandleTransferDTO.getRemarks(), + "6", + ftbHandleTransferDTO.getSalaryType(), + ftbHandleTransferDTO.getPayrollSequenceId(), + null, null); + ftbPersonnelsTransferManage.setSalaryRecordId(recordId); + } + // 重新办理 + if (StrUtil.isNotBlank(ftbHandleTransferDTO.getId())) { + ftbPersonnelsTransferManage.setId(ftbHandleTransferDTO.getId()); + this.removeById(ftbHandleTransferDTO.getId()); + } + ftbPersonnelsTransferManage.setLastModifyUserId(UserProvider.getUser().getUserId()); + ftbPersonnelsTransferManage.setLastModifyTime(new Date()); + this.save(ftbPersonnelsTransferManage); + // 发送调动通知mq至考勤,用于考勤排班 + sendTransferNoticeMq(ftbHandleTransferDTO.getUserId(),ftbHandleTransferDTO.getAfterOrgId() + ,ftbHandleTransferDTO.getEffectiveDate(),ftbHandleTransferDTO.getBeforeOrgId()); + return null; + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public ActionResult handleTransferOa(FtbHandleTransferDTO ftbHandleTransferDTO) { + ActionResult actionResult = new ActionResult<>(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + // 数据校验 + try { + transferDataCheck(ftbHandleTransferDTO); + // 员工本人发起调动,审批同意时校验 + if (!UserProvider.getUser().getUserId().equals(ftbHandleTransferDTO.getUserId())) { + checkTransferAndSecondmentConflict(ftbHandleTransferDTO.getUserId(), ftbHandleTransferDTO.getEffectiveDate()); + } + } catch (RuntimeException runtimeException) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(runtimeException.getMessage()); + return actionResult; + } + // 调动前组织名称 + ActionResult organizesByTargetCategoryOrganizeIds = v2OrganizeApi.organizeInfoById(null, ftbHandleTransferDTO.getBeforeOrgId()); + ftbHandleTransferDTO.setBeforeOrgName(organizesByTargetCategoryOrganizeIds.getData().getName()); + // 调动前岗位名称 + ActionResult positionVOActionResult = v2PositionApi.infoPosition(ftbHandleTransferDTO.getBeforePostId()); + ftbHandleTransferDTO.setBeforePostName(positionVOActionResult.getData().getFullName()); + // 调动前职等Id + if (ftbHandleTransferDTO.getBeforeRankId() != null) { + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(ftbHandleTransferDTO.getBeforeRankId()); + ftbHandleTransferDTO.setBeforeRankName(gradeVOActionResult.getData().getName()); + } + // 调动前班组id + if (ftbHandleTransferDTO.getBeforeTeamId() != null) { + ActionResult organizeGeneralDetailVOActionResult = v2OrganizeApi.organizeInfoById(OrganizeCategoryEnums.TEAM, ftbHandleTransferDTO.getBeforeTeamId()); + ftbHandleTransferDTO.setBeforeTeamName(organizeGeneralDetailVOActionResult.getData().getName()); + } + // 调动后组织名称 + ActionResult organizesByTargetCategoryOrganizeIds1 = v2OrganizeApi.organizeInfoById(null, ftbHandleTransferDTO.getAfterOrgId()); + ftbHandleTransferDTO.setAfterOrgName(organizesByTargetCategoryOrganizeIds1.getData().getName()); + // 调动后岗位名称 + ActionResult positionVOActionResult1 = v2PositionApi.infoPosition(ftbHandleTransferDTO.getAfterPostId()); + ftbHandleTransferDTO.setAfterPostName(positionVOActionResult1.getData().getFullName()); + // 调动后职等Id + if (ftbHandleTransferDTO.getAfterRankId() != null) { + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(ftbHandleTransferDTO.getAfterRankId()); + ftbHandleTransferDTO.setAfterRankName(gradeVOActionResult.getData().getName()); + } + // 调动后班组id + if (ftbHandleTransferDTO.getAfterTeamId() != null) { + ActionResult organizeGeneralDetailVOActionResult = v2OrganizeApi.organizeInfoById(OrganizeCategoryEnums.TEAM, ftbHandleTransferDTO.getAfterTeamId()); + ftbHandleTransferDTO.setAfterTeamName(organizeGeneralDetailVOActionResult.getData().getName()); + } + // 调动前直属主管名称 + ftbHandleTransferDTO.setBeforeDirectSupervisorName(getUserName(ftbHandleTransferDTO.getBeforeDirectSupervisorId())); + // 调动后直属主管名称 + ftbHandleTransferDTO.setAfterDirectSupervisorName(getUserName(ftbHandleTransferDTO.getAfterDirectSupervisorId())); + FtbPersonnelsTransferManage ftbPersonnelsTransferManage = ftbHandleTransferDTO.convert(ftbHandleTransferDTO); + ftbPersonnelsTransferManage.setProcessingStatus(1); + ftbPersonnelsTransferManage.setEffectiveDate(DateUtil.beginOfDay(ftbPersonnelsTransferManage.getEffectiveDate())); + // 重新办理 + if (StrUtil.isNotBlank(ftbHandleTransferDTO.getSpecialBusinessId())) { + ftbPersonnelsTransferManage.setId(ftbHandleTransferDTO.getSpecialBusinessId()); + this.removeById(ftbHandleTransferDTO.getSpecialBusinessId()); + } + this.save(ftbPersonnelsTransferManage); + return actionResult; + } + + @Override + @Transactional + public void delete(String id) { + LambdaUpdateWrapper deleteWrapper = Wrappers.lambdaUpdate(); + deleteWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + deleteWrapper.set(FtbPersonnelsTransferManage::getEnableMarkd, 1); + baseMapper.update(new FtbPersonnelsTransferManage(), deleteWrapper); + } + + private void accountOffline(String tenantCode, String userId) { + String loginId = tenantCode + ":" + userId; + StpUtil.logout(loginId); + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void approvalForOA(FtbHandleTransferOaDTO dto) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(FtbPersonnelsTransferManage::getTaskInfoId, dto.getTaskId()); + // 是否通过 0:不通过 1通过 2撤销 + if (dto.getIsPass() == 0) { + updateWrapper.set(FtbPersonnelsTransferManage::getProcessingStatus, 3); + } else if (dto.getIsPass() == 1) { + updateWrapper.set(FtbPersonnelsTransferManage::getProcessingStatus, 2); + } else if (dto.getIsPass() == 2) { + updateWrapper.set(FtbPersonnelsTransferManage::getProcessingStatus, 4); + } + if (dto.getIsPass() == 1) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsTransferManage::getTaskInfoId, dto.getTaskId()); + FtbPersonnelsTransferManage ftbHandleTransferDTO = this.baseMapper.selectOne(queryWrapper); + boolean after = new Date().after(ftbHandleTransferDTO.getEffectiveDate()); + // 薪酬延迟填写 + if (ftbHandleTransferDTO.getSalaryStructure() == null && dto.getIsAdjustSalary() != null) { + // 薪酬 + ftbHandleTransferDTO.setSalaryStructure(dto.getIsAdjustSalary()); + ftbHandleTransferDTO.setSalaryType(dto.getSalaryType()); + ftbHandleTransferDTO.setPayrollSequence(dto.getPayrollSequence()); + ftbHandleTransferDTO.setPayrollSequenceId(dto.getPayrollSequenceId()); + ftbHandleTransferDTO.setSalaryStructureList(JSON.toJSONString(dto.getSalaryList())); + ftbHandleTransferDTO.setSalaryTotal(dto.getSalaryTotal()); + ftbHandleTransferDTO.setPayrollSequenceName(dto.getPayrollSequenceName()); + updateWrapper.set(FtbPersonnelsTransferManage::getSalaryStructure, dto.getIsAdjustSalary()); + updateWrapper.set(FtbPersonnelsTransferManage::getSalaryType, dto.getSalaryType()); + updateWrapper.set(FtbPersonnelsTransferManage::getPayrollSequence, dto.getPayrollSequence()); + updateWrapper.set(FtbPersonnelsTransferManage::getPayrollSequenceId, dto.getPayrollSequenceId()); + updateWrapper.set(FtbPersonnelsTransferManage::getSalaryStructureList, ftbHandleTransferDTO.getSalaryStructureList()); + updateWrapper.set(FtbPersonnelsTransferManage::getSalaryTotal, dto.getSalaryTotal()); + updateWrapper.set(FtbPersonnelsTransferManage::getPayrollSequenceName, dto.getPayrollSequenceName()); + } + checkTransferAndSecondmentConflict(ftbHandleTransferDTO.getUserId(), ftbHandleTransferDTO.getEffectiveDate()); + if (after) { + updateWrapper.set(FtbPersonnelsTransferManage::getImplementType, 1); + // 同步数据至组织架构 + syncDataToOrganization(ftbHandleTransferDTO.getUserId(), ftbHandleTransferDTO.getAfterOrgId(), + ftbHandleTransferDTO.getAfterPostId(), ftbHandleTransferDTO.getAfterRankId(), ftbHandleTransferDTO.getAfterTeamId(), + ftbHandleTransferDTO.getAfterDirectSupervisorId(), UserProvider.getUser().getTenantId()); + // 账号下线 + accountOffline(UserProvider.getUser().getTenantId(), ftbHandleTransferDTO.getUserId()); + } + // 同步数据至薪酬 + if (Objects.nonNull(ftbHandleTransferDTO.getSalaryStructure()) && ftbHandleTransferDTO.getSalaryStructure() == 1) { + String recordId = personSalaryUtils.syncSalaryData( + ftbHandleTransferDTO.getSalaryStructureList(), + ftbHandleTransferDTO.getUserId(), + ftbHandleTransferDTO.getAfterOrgId(), + ftbHandleTransferDTO.getAfterPostId(), + ftbHandleTransferDTO.getAfterRankId(), + "1", + ftbHandleTransferDTO.getEffectiveDate(), + ftbHandleTransferDTO.getRemarks(), + "6", + ftbHandleTransferDTO.getSalaryType(), + ftbHandleTransferDTO.getPayrollSequenceId(), + null, dto.getTaskId()); + updateWrapper.set(FtbPersonnelsTransferManage::getSalaryRecordId, recordId); + } + // 发送调动通知mq至考勤,用于考勤排班 + sendTransferNoticeMq(ftbHandleTransferDTO.getUserId(),ftbHandleTransferDTO.getAfterOrgId() + ,ftbHandleTransferDTO.getEffectiveDate(),ftbHandleTransferDTO.getBeforeOrgId()); + } + this.baseMapper.update(new FtbPersonnelsTransferManage(), updateWrapper); + } + + @Override + public FtbHandleTransferDetailsVO transferDetails(String id) { + FtbPersonnelsTransferManage ftbPersonnelsTransferManage = this.baseMapper.selectById(id); + FtbHandleTransferDetailsVO ftbHandleTransferDetailsVO = FtbHandleTransferDetailsVO.convert(ftbPersonnelsTransferManage); + // 物品和工作交接增加员工ID + if (StrUtil.isNotBlank(ftbPersonnelsTransferManage.getWorkHandover())) { + List turnoverHandovers = JSON.parseArray(ftbPersonnelsTransferManage.getWorkHandover(), FtbPersonnelsTurnoverHandoverDTO.class); + StaffRosterListReq listReq = new StaffRosterListReq(); + listReq.setIsQueryAuth("0"); + listReq.setPageSize(-1); + listReq.setUserIds(turnoverHandovers.stream().map(FtbPersonnelsTurnoverHandoverDTO::getArticleRecipientId).collect(Collectors.toList())); + PageInfo ftbPersonnelsStaffRosterDtoPageInfo = staffRosterService.postWithSalary(listReq); + Map userMaps = ftbPersonnelsStaffRosterDtoPageInfo.getList().stream() + .collect(Collectors.toMap(FtbPersonnelsStaffRosterDto::getUserId, Function.identity(), (k1, k2) -> k1)); + ftbHandleTransferDetailsVO.setWorkHandover(JSON.toJSONString(turnoverHandovers.stream().peek(item -> { + if (userMaps.containsKey(item.getArticleRecipientId())) { + item.setArticleRecipientName(userMaps.get(item.getArticleRecipientId()).getName()); + item.setWorkerStatus(userMaps.get(item.getArticleRecipientId()).getWorkerStatus()); + item.setSystemWokerId(userMaps.get(item.getArticleRecipientId()).getSystemWokerId()); + } else { + // 已删除的设置为空串 + item.setWorkerStatus(""); + item.setSystemWokerId(""); + } + }).collect(Collectors.toList()))); + } + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(ftbPersonnelsTransferManage.getUserId() + , ftbHandleTransferDetailsVO.getHandlePersonId()), UserProvider.getUser().getTenantId()); + if (userPrimaryBoundBatch.getCode() == 200 || userPrimaryBoundBatch.getData() != null) { + Map map = userPrimaryBoundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2) -> k1)); + UserBoundVO first = map.get(ftbPersonnelsTransferManage.getUserId()); + if (first != null) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPersonnelsStaffRoster::getSystemWokerId); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, ftbPersonnelsTransferManage.getUserId()); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(queryWrapper); + // 员工ID + ftbHandleTransferDetailsVO.setSystemWorkerId(ftbPersonnelsStaffRoster.getSystemWokerId()); + // 员工姓名 + ftbHandleTransferDetailsVO.setUserName(first.getUserName()); + } + UserBoundVO second = map.get(ftbHandleTransferDetailsVO.getHandlePersonId()); + if (second != null) { + ftbHandleTransferDetailsVO.setHandlePersonName(second.getUserName()); + } + } + return ftbHandleTransferDetailsVO; + } + + @Override + public void transferEffectScheduledTask(String tenantId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsTransferManage::getEnableMarkd, 0); + queryWrapper.in(FtbPersonnelsTransferManage::getProcessingStatus, 2, 6); + queryWrapper.eq(FtbPersonnelsTransferManage::getImplementType, 0); + queryWrapper.apply("DATE_FORMAT(F_EffectiveDate, '%Y-%m-%d') <= DATE_FORMAT(NOW(), '%Y-%m-%d')"); + List ftbPersonnelsTransferManages = this.baseMapper.selectList(queryWrapper); + ftbPersonnelsTransferManages.forEach(a -> { + // 同步数据至组织架构 + syncDataToOrganization(a.getUserId(), a.getAfterOrgId(), + a.getAfterPostId(), a.getAfterRankId(), a.getAfterTeamId(), + a.getAfterDirectSupervisorId(), tenantId); + // 账号下线 + accountOffline(tenantId, a.getUserId()); + }); + this.baseMapper.updateTransferManage(); + log.error("定时任务调动执行成功:{}", DateUtil.formatDate(new Date())); + } + + @Override + public List> queryFieldValue() { + List> result = new ArrayList<>(); + // 调动类型0调岗1晋升2降职3调店 + Map transferType = new HashMap<>(); + transferType.put("id", "0"); + transferType.put("name", "调岗"); + result.add(transferType); + Map promotion = new HashMap<>(); + promotion.put("id", "1"); + promotion.put("name", "晋升"); + result.add(promotion); + Map demotion = new HashMap<>(); + demotion.put("id", "2"); + demotion.put("name", "降职"); + result.add(demotion); + Map transferStore = new HashMap<>(); + transferStore.put("id", "3"); + transferStore.put("name", "调店"); + result.add(transferStore); + return result; + } + + @Override + @Transactional + public void revokeTheTransferRecord(String id) { + FtbPersonnelsTransferManage ftbPersonnelsTransferManage = this.baseMapper.selectById(id); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, id); + updateWrapper.set(FtbPersonnelsTransferManage::getProcessingStatus, 4); + this.update(new FtbPersonnelsTransferManage(), updateWrapper); + // 薪资作废 + if (ftbPersonnelsTransferManage.getSalaryStructure() == 1) { + ActionResult actionResult = null; + try { + actionResult = querySalaryApi.voidSalary(ftbPersonnelsTransferManage.getUserId(), + DateUtil.format(ftbPersonnelsTransferManage.getEffectiveDate(), "yyyy-MM-dd"), + ftbPersonnelsTransferManage.getSalaryRecordId()); + } catch (Exception e) { + throw new RuntimeException(e); + } + if (actionResult.getCode() != 200) { + throw new RuntimeException(actionResult.getMsg()); + } + } + } + + @Override + public void clearTransferData(List userId) { + LambdaUpdateWrapper queryWrapper = Wrappers.lambdaUpdate(); + queryWrapper.in(FtbPersonnelsTransferManage::getUserId, userId); + queryWrapper.set(FtbPersonnelsTransferManage::getEnableMarkd, 1); + this.baseMapper.update(new FtbPersonnelsTransferManage(), queryWrapper); + } + + @Override + public List transferCheckTimeCrossing(String userId, Date startTime, Date endTime) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPersonnelsTransferManage::getEffectiveDate); + queryWrapper.eq(FtbPersonnelsTransferManage::getUserId, userId); + queryWrapper.in(FtbPersonnelsTransferManage::getProcessingStatus,2,6); + queryWrapper.apply("DATE_FORMAT(F_EffectiveDate, '%Y-%m-%d') >= DATE_FORMAT('" + DateUtil.format(startTime, "yyyy-MM-dd") + "', '%Y-%m-%d')"); + queryWrapper.apply("DATE_FORMAT(F_EffectiveDate, '%Y-%m-%d') <= DATE_FORMAT('" + DateUtil.format(endTime, "yyyy-MM-dd") + "', '%Y-%m-%d')"); + return this.baseMapper.selectList(queryWrapper) + .stream() + .map(FtbPersonnelsTransferManage::getEffectiveDate) + .collect(Collectors.toList()); + + } + + /** + * 调动数据校验 + */ + private void transferDataCheck(FtbHandleTransferDTO ftbHandleTransferDTO) { + String before = ftbHandleTransferDTO.getBeforeOrgId() + "#" + ftbHandleTransferDTO.getBeforePostId(); + before = before + "#" + (StrUtil.isBlank(ftbHandleTransferDTO.getBeforeRankId()) ? "" : ftbHandleTransferDTO.getBeforeRankId()) + "#" + + (StrUtil.isBlank(ftbHandleTransferDTO.getBeforeTeamId()) ? "" : ftbHandleTransferDTO.getBeforeTeamId()); + String after = ftbHandleTransferDTO.getAfterOrgId() + "#" + ftbHandleTransferDTO.getAfterPostId(); + after = after + "#" + (StrUtil.isBlank(ftbHandleTransferDTO.getAfterRankId()) ? "" : ftbHandleTransferDTO.getAfterRankId()) + "#" + + (StrUtil.isBlank(ftbHandleTransferDTO.getAfterTeamId()) ? "" : ftbHandleTransferDTO.getAfterTeamId()); + if (before.equals(after)) { + throw new RuntimeException("调动前/后的信息无变更,不可提交"); + } + // 调动生效日期不能早于实际入职日期 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPersonnelsStaffRoster::getActualStartDate); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, ftbHandleTransferDTO.getUserId()); + queryWrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(queryWrapper); + if (ftbPersonnelsStaffRoster != null && ftbPersonnelsStaffRoster.getActualStartDate().after(ftbHandleTransferDTO.getEffectiveDate())) { + throw new RuntimeException("调动生效日期不能早于实际入职日期"); + } + // 薪资总额校验 + if (Objects.nonNull(ftbHandleTransferDTO.getSalaryTotal()) && ftbHandleTransferDTO.getSalaryTotal().compareTo(new BigDecimal("1000000000")) >= 1) { + throw new RuntimeException("薪资项合计超出上限,请输入合法薪资数据"); + } + // 调动前组织信息与当前组织信息是否一致 + ActionResult> usersBoundAc = v2UserApi.getUserPrimaryBoundBatch(List.of(ftbHandleTransferDTO.getUserId()), null); + if (Objects.nonNull(usersBoundAc.getData())) { + UserBoundVO data = usersBoundAc.getData().get(0); + String curr = data.getOrganizeId() + "#" + data.getPositionId() + "#"; + curr = curr + (StrUtil.isBlank(data.getGradeId()) ? "" : data.getGradeId()) + "#" + (StrUtil.isBlank(data.getStoreTeamId()) ? "" : data.getStoreTeamId()); + if (!before.equals(curr)) { + throw new RuntimeException("该员工调动前组织信息与当前所在组织信息不一致,不能办理!"); + } + } + // 获取调动后的直属主管是否是被办理用户 + if (StrUtil.isNotBlank(ftbHandleTransferDTO.getAfterDirectSupervisorId())) { + ActionResult> usersBoundAfterDirectSupervisor = v2UserApi.getUserPrimaryBoundBatch(List.of(ftbHandleTransferDTO.getAfterDirectSupervisorId()), null); + if (Objects.nonNull(usersBoundAfterDirectSupervisor.getData())) { + UserBoundVO data = usersBoundAfterDirectSupervisor.getData().get(0); + if (ftbHandleTransferDTO.getUserId().equals(data.getLeaderId())) { + String errorMsg = "被办理人是" + ftbHandleTransferDTO.getAfterDirectSupervisorName() + "(已选择的主管)的直属主管,不能互为主管!"; + throw new RuntimeException(errorMsg); + } + } + } + } + + /** + * 校验调动与借调时间是否冲突 + * + * @param userId 用户ID + * @param effectiveDate 生效日期 + */ + private void checkTransferAndSecondmentConflict(String userId, Date effectiveDate) { + List serviceIdList = ftbPersonnelSecondmentManagementService.salarySecondmentCrossover(userId, effectiveDate); + if (CollUtil.isNotEmpty(serviceIdList)) { + String collected = serviceIdList.stream().map(v -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + return sdf.format(v.getSecondmentStartTime()) + "~" + sdf.format(v.getSecondmentEndTime()); + }).collect(Collectors.joining(",")); + String joined = String.format("当前办理调动选择日期为%s生效,\n" + + "检测到您有一项借调业务的起止时间(%s)与当前办理业务的生效日期\n" + + "重叠,请重新选择日期(可选择借调开始之前的日期或者借调结束之后的日期)", + DateUtil.formatDate(effectiveDate), collected); + throw new RuntimeException(joined); + } + } + + /** + * 同步数据至组织架构 + */ + private void syncDataToOrganization(String userId, String organizeId, String positionId, String gradesId, String storeTeamId, String leaderId, String tenantId) { + UserBoundInfoDTO dto = new UserBoundInfoDTO(); + dto.setOrganizeId(organizeId); + dto.setPositionId(positionId); + dto.setGradesId(gradesId); + dto.setStoreTeamId(storeTeamId); + dto.setLeaderId(leaderId); + dto.setTenantId(tenantId); + ActionResult booleanActionResult = v2UserApi.updateUserBoundInfo(userId, dto); + if (booleanActionResult == null || !booleanActionResult.getData()) { + throw new RuntimeException("更新用户绑定信息失败"); + } + } + + /** + * 获取直属主管姓名 + * + * @param userId 用户 ID + * @return {@link String } + */ + private String getUserName(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(FtbPersonnelsStaffRoster::getName); + queryWrapper.eq(FtbPersonnelsStaffRoster::getUserId, userId); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbPersonnelsStaffRoster)) { + return ftbPersonnelsStaffRoster.getName(); + } + return null; + } + + /** + * 发送调动通知mq至考勤,用于考勤排班 + * + * @param userId 用户ID + * @param organizeId 组织ID + * @param entryDate 生效日期 + */ + private void sendTransferNoticeMq(String userId, String organizeId, Date entryDate,String beforeOrgId) { + Map hashMap = new HashMap<>(); + hashMap.put("userId", userId); + hashMap.put("entryDate", entryDate); + hashMap.put("organizeId", organizeId); + hashMap.put("beforeOrgId", beforeOrgId); + hashMap.put("tenantId", UserProvider.getUser().getTenantId()); + secondmentConsumerSourceMsg.sendMessageOnboarding(JSONObject.toJSONString(hashMap)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTrialServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTrialServiceImpl.java new file mode 100644 index 0000000..ef5ce1f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTrialServiceImpl.java @@ -0,0 +1,107 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.ContractSignPersonnelNoticeApi; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistAddDTO; +import jnpf.model.personnels.dto.roster.FtbPersonnelsTrialDTO; +import jnpf.model.personnels.dto.roster.FtbjobTrialRejectedDTO; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.StaffTravlFailDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.personnels.service.*; +import jnpf.util.UserProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; + +@Service +public class FtbPersonnelsTrialServiceImpl implements FtbPersonnelsTrialService { + + @Resource + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Resource + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Resource + private FtbPersonnelsTurnoverManagementService ftbPersonnelsTurnoverManagementService; + + @Resource + private FtbPersonnelsBlacklistService ftbPersonnelsBlacklistService; + + @Resource + private ContractSignPersonnelNoticeApi contractSignPersonnelNoticeAPI; + + @Override + @Transactional + public void passedTheTrialJob(FtbPersonnelsTrialDTO ftbPersonnelsTrialDTO) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRoster::getId, ftbPersonnelsTrialDTO.getId()); + wrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterService.getOne(wrapper); + if (null != ftbPersonnelsStaffRoster && ftbPersonnelsStaffRoster.getWorkerStatus().equals("306")) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, ftbPersonnelsStaffRoster.getId()); + // 结束试岗后若该员工有试用期,工作状态变更为“试用”,若无试用期,工作状态则变更为“正式” + String workerStatus = "302"; + // 是否有试岗期 + ProbationPeriodDto probationPeriodDto = ftbPersonnelsStaffRosterService.queryProbationPeriod(registrationFormDataService, ftbPersonnelsStaffRoster.getId()); + if (probationPeriodDto == null || "100".equals(probationPeriodDto.getType())) { + workerStatus = "303"; + } + updateWrapper.set(FtbPersonnelsStaffRoster::getWorkerStatus, workerStatus); + // 更新工作状态 + registrationFormDataService.updateForOneField(ftbPersonnelsStaffRoster.getId(), + ftbPersonnelsStaffRoster.getPhone(), "workerStatus", workerStatus); + ftbPersonnelsStaffRosterService.update(new FtbPersonnelsStaffRoster(), updateWrapper); + + } + + } + + @Override + @Transactional + public void jobTrialRejected(FtbjobTrialRejectedDTO ftbjobTrialRejectedDTO) { + UserInfo userInfo = UserProvider.getUser(); + // 驳回原因 + StaffTravlFailDto dto = new StaffTravlFailDto(); + dto.setRemarks(ftbjobTrialRejectedDTO.getReasonsForJobRejection()); + dto.setList(ftbjobTrialRejectedDTO.getOrganizationInfo()); + dto.setFailDate(new Date()); + // 试岗员工试岗驳回时,系统判断该员工是否已签署劳动合同 +// ActionResult result = contractSignPersonnelNoticeAPI.signPersonnel(ftbjobTrialRejectedDTO.getUserId(), userInfo.getTenantId()); +// ContractSignPersonnelFlagVo contractSignPersonnelFlagVo = result.getData(); +// if (Objects.isNull(contractSignPersonnelFlagVo)) { +// throw new RuntimeException("电子合同降级处理查询员工是否已签署劳动合同失败"); +// } + //【判断结果一】已签署-需要签署离职证明,完成签署后工作状态为“已离职”;已发起离职证明完成签署前,工作状态为“待离职” + //【判断结果二】未签署-可直接变更为“已离职” + dto.setStaffWorkerStatus(StaffWorkerStatus.RESIGNED); +// if (!contractSignPersonnelFlagVo.getSignType().isEmpty()) { +// FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterService.queryRosterInfoByUserId(ftbjobTrialRejectedDTO.getUserId()); +// // 是否签署离职证明 0-未签署 1-已签署 +// if (ftbPersonnelsStaffRoster.getIsSignSeparation() == 0) { +// dto.setStaffWorkerStatus(StaffWorkerStatus.PENDING_RESIGNATION); +// } +// } + ftbPersonnelsStaffRosterService.innerTravelFail(ftbjobTrialRejectedDTO.getUserId(), dto); + // 办理离职,工作状态为已离职才进行数据清除 + if (dto.getStaffWorkerStatus().equals(StaffWorkerStatus.RESIGNED)) { + ftbPersonnelsTurnoverManagementService.jobTrialRejected(ftbjobTrialRejectedDTO); + } + // 是否纳入黑名单,0不纳入,1纳入 + if (ftbjobTrialRejectedDTO.getWhetherToBeIncluded() == 1) { + FtbPersonnelsBlacklistAddDTO blacklistAddDTO = FtbjobTrialRejectedDTO.convertBlacklistAddDto(ftbjobTrialRejectedDTO); + ftbPersonnelsBlacklistService.addBlacklist(blacklistAddDTO); + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverAnalysisServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverAnalysisServiceImpl.java new file mode 100644 index 0000000..8f0b996 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverAnalysisServiceImpl.java @@ -0,0 +1,1205 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateField; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.personnels.dto.analysis.FtbPersonnlesAnalysisDTO; +import jnpf.model.personnels.dto.range.RangeDTO; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsRuleConfig; +import jnpf.model.personnels.vo.analysis.*; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryListUserByLevelDTO; +import jnpf.permission.dto.v2.user.QueryPageUserDTO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsRuleConfigMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.mapper.FtbPersonnelsTurnoverAnalysisMapper; +import jnpf.personnels.mapper.FtbPersonnelsTurnoverManagementMapper; +import jnpf.personnels.service.FtbPersonnelsTurnoverAnalysisService; +import jnpf.personnels.service.FtbPersonnlesInfoConfigService; +import jnpf.personnels.utils.CompanyAgeUtil; +import jnpf.personnels.utils.CovertDateUtils; +import jnpf.util.RangeConfigUtil; +import jnpf.util.excel.EasyExcelUtils; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @Author: peng.hao + * @create: 2024/10/14 + */ +@Service +public class FtbPersonnelsTurnoverAnalysisServiceImpl implements FtbPersonnelsTurnoverAnalysisService { + + @Resource + FtbPersonnelsTurnoverAnalysisMapper ftbPersonnelsTurnoverAnalysisMapper; + + @Autowired + private V2UserApi v2UserApi; + + // 在职状态 + private static final List ON_THE_JOB_STATUS = List.of("302", "303", "304", "306"); + // 首月 + private static final Integer FIRST_MONTH = 30; + // 首周 + private static final Integer FIRST_WEEK = 7; + // 首年 + private static final Integer FIRST_YEAR = 365; + + @Resource + private FtbPersonnelsRuleConfigMapper ftbPersonnelsRuleConfigMapper; + + @Resource + FtbPersonnelsTurnoverManagementMapper turnoverManagementMapper; + + @Resource + private FtbPersonnlesInfoConfigService personnlesInfoConfigService; + + @Resource + private FtbPersonnelsStaffRegistrationFormDataMapper ftbPersonnelsStaffRegistrationFormDataMapper; + + + @Override + public FtbPersonnelsTurnoverWithAnalysisInfoVO getLeavePeopleMonth(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverWithAnalysisInfoVO(); + } + // 当前时间 + Integer peopleMonth = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(dto.getStartTime(),dto.getEndTime(),userIdListByOrganizeIds); + FtbPersonnelsTurnoverWithAnalysisInfoVO infoVO = new FtbPersonnelsTurnoverWithAnalysisInfoVO(); + // 当月离职 + infoVO.setPercentageOfHeadcount(peopleMonth); + // 同比 去年 这个月 + // 同比 (本期数 - 同期数) / 同期数 × 100% + Date lastYearStartTime = DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(); + Date lastYearEndTime = DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(); + // 同期数 + Integer yearOnYear = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(lastYearStartTime,lastYearEndTime, userIdListByOrganizeIds); + // 同比 (本期数 - 同期数) / 同期数 × 100% + BigDecimal setScale = yearOnYearAndMonthOnMonth(peopleMonth, yearOnYear); + infoVO.setYearOnYear(setScale); + // 环比 上月 + Date ringStartTime = DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(); + Date ringEndTime = DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(); + // 环比 (本期数 - 上期数) / 上期数 × 100% + Integer leavePeopleMonth = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(ringStartTime, ringEndTime, userIdListByOrganizeIds); + BigDecimal decimal = yearOnYearAndMonthOnMonth(peopleMonth, leavePeopleMonth); + infoVO.setAnnulus(decimal); + return infoVO; + } + + @Override + public FtbPersonnelsTurnoverPeopleSexVO getLeavePeopleSexMonth(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverPeopleSexVO(); + } + // 离职人数 + Integer peopleMonth = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(dto.getStartTime(),dto.getEndTime(),userIdListByOrganizeIds); + //0 workerSex 未知 + //1 workerSex 男 + //2 workerSex 女 + FtbPersonnelsTurnoverPeopleSexVO ftbPersonnelsTurnoverPeopleSexVO = new FtbPersonnelsTurnoverPeopleSexVO(); + // 离职人员的数据项 + List femaleUserIds = dynamicallyObtainTheRequiredLogFieldValueWithWorkSex(dto, userIdListByOrganizeIds, "2"); + // 女 + Integer femaleCount = 0; + if (CollUtil.isNotEmpty(femaleUserIds)) femaleCount = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleSexMonth(dto.getStartTime(),dto.getEndTime(),femaleUserIds); + ftbPersonnelsTurnoverPeopleSexVO.setMale(femaleCount); + // 男 + List maleUserIds = dynamicallyObtainTheRequiredLogFieldValueWithWorkSex(dto, userIdListByOrganizeIds, "1"); + Integer maleCount = 0; + if (CollUtil.isNotEmpty(maleUserIds)) maleCount = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleSexMonth(dto.getStartTime(),dto.getEndTime(), maleUserIds); + ftbPersonnelsTurnoverPeopleSexVO.setFemale(maleCount); +// // 截止月份在职员工 +// BigDecimal employedInTheMonth = ftbPersonnelsOverviewAnalysisMapper.employeesEmployedInTheMonth(DatePattern.NORM_MONTH_FORMAT.format(dto.getEndTime()) +// , ON_THE_JOB_STATUS, userIdListByOrganizeIds); + ftbPersonnelsTurnoverPeopleSexVO.setMaleRatio(calculateTheScale(femaleCount, BigDecimal.valueOf(peopleMonth))); + ftbPersonnelsTurnoverPeopleSexVO.setFemaleRatio(calculateTheScale(maleCount, BigDecimal.valueOf(peopleMonth))); + return ftbPersonnelsTurnoverPeopleSexVO; + } + + @Override + public FtbPersonnelsTurnoverFirstMonthLeaveRateVO getFirstMonthLeaveRate(FtbPersonnlesAnalysisDTO dto) { + // 首月离职率 + FtbPersonnelsTurnoverFirstMonthLeaveRateVO ftbPersonnelsTurnoverFirstMonthLeaveRateVO = new FtbPersonnelsTurnoverFirstMonthLeaveRateVO(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return ftbPersonnelsTurnoverFirstMonthLeaveRateVO; + } + // 这个月时间离职率 + Date monthStartTime = dto.getStartTime(); + Date monthEndTime = dto.getEndTime(); + DateTime startTime = DateUtil.beginOfMonth(monthStartTime); + DateTime endTime = DateUtil.endOfMonth(monthEndTime); + BigDecimal firstMonthLeaveRate = turnoverRateSequentially(startTime, endTime, userIdListByOrganizeIds,FIRST_MONTH,dto.getOrganizationId()); + ftbPersonnelsTurnoverFirstMonthLeaveRateVO.setFirstMonthLeaveRate(firstMonthLeaveRate); + // 同比 去年 这个月 + Date lastYearStartTime = DateUtil.offset(startTime, DateField.YEAR, -1).toJdkDate(); + Date lastYearEndTime = DateUtil.offset(endTime, DateField.YEAR, -1).toJdkDate(); + // 同期数 + BigDecimal turnoverRate = turnoverRateSequentially(lastYearStartTime, lastYearEndTime, userIdListByOrganizeIds, FIRST_MONTH, dto.getOrganizationId()); + // 同比 (本期数 - 同期数) / 同期数 × 100% + BigDecimal calculatedValue = yearOnYearAndMonthOnMonth(firstMonthLeaveRate, turnoverRate); + ftbPersonnelsTurnoverFirstMonthLeaveRateVO.setFirstMonthLeaveRateYearOnYear(calculatedValue); + // 环比 (本期数 - 上期数) / 上期数 × 100% + // 环比 上月 + Date ringStartTime = DateUtil.offset(startTime, DateField.MONTH, -1).toJdkDate(); + Date ringEndTime = DateUtil.offset(endTime, DateField.MONTH, -1).toJdkDate(); + // 上期数 + BigDecimal previousPeriodRatio = turnoverRateSequentially(ringStartTime, ringEndTime, userIdListByOrganizeIds, FIRST_MONTH, dto.getOrganizationId()); + // 环比 + BigDecimal annulus = yearOnYearAndMonthOnMonth(firstMonthLeaveRate, previousPeriodRatio); + ftbPersonnelsTurnoverFirstMonthLeaveRateVO.setFirstMonthLeaveRateAnnulus(annulus); + return ftbPersonnelsTurnoverFirstMonthLeaveRateVO; + } + + @Override + public FtbPersonnelsTurnoverFirstWeekLeaveRateVO getFirstWeekLeaveRate(FtbPersonnlesAnalysisDTO dto) { + // 首周 + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverFirstWeekLeaveRateVO(); + } + FtbPersonnelsTurnoverFirstWeekLeaveRateVO firstWeekLeaveRateVO = new FtbPersonnelsTurnoverFirstWeekLeaveRateVO(); + BigDecimal firstWeekLeaveRate = turnoverRateSequentially(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds, FIRST_WEEK, dto.getOrganizationId()); + // 首周离职率 + firstWeekLeaveRateVO.setFirstWeekLeaveRate(firstWeekLeaveRate); + // 首周离职率同比 + firstWeekLeaveRateVO.setFirstWeekLeaveRateYearOnYear( + yearOnYearAndMonthOnMonth(firstWeekLeaveRate, + turnoverRateSequentially( + DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(), + DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(), + userIdListByOrganizeIds, + FIRST_WEEK, dto.getOrganizationId()))); + // 首周离职率环比 + firstWeekLeaveRateVO.setFirstWeekLeaveRateAnnulus( + yearOnYearAndMonthOnMonth(firstWeekLeaveRate, + turnoverRateSequentially( + DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(), + DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(), + userIdListByOrganizeIds, + FIRST_WEEK, dto.getOrganizationId()))); + return firstWeekLeaveRateVO; + } + + @Override + public FtbPersonnelsTurnoverFirstYearLeaveRateVO getFirstYearLeaveRate(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverFirstYearLeaveRateVO(); + } + FtbPersonnelsTurnoverFirstYearLeaveRateVO leaveRateVO = new FtbPersonnelsTurnoverFirstYearLeaveRateVO(); + // 离职率 + Date startTime = dto.getStartTime(); + Date endTime = dto.getEndTime(); + DateTime monthStartTime = DateUtil.beginOfMonth(startTime); + DateTime monthEndTime = DateUtil.endOfMonth(endTime); + BigDecimal firstYearLeaveRate = turnoverRateSequentially(monthStartTime, monthEndTime, userIdListByOrganizeIds, FIRST_YEAR, dto.getOrganizationId()); + leaveRateVO.setFirstYearLeaveRate(firstYearLeaveRate); + // 同比 + leaveRateVO.setFirstYearLeaveRateYearOnYear(yearOnYearAndMonthOnMonth(firstYearLeaveRate, + turnoverRateSequentially( + DateUtil.offset(monthStartTime, DateField.YEAR, -1).toJdkDate(), + DateUtil.offset(monthEndTime, DateField.YEAR, -1).toJdkDate(), + userIdListByOrganizeIds, + FIRST_YEAR, dto.getOrganizationId()))); + // 环比 + leaveRateVO.setFirstYearLeaveRateAnnulus(yearOnYearAndMonthOnMonth(firstYearLeaveRate, + turnoverRateSequentially( + DateUtil.offset(monthStartTime, DateField.MONTH, -1).toJdkDate(), + DateUtil.offset(monthEndTime, DateField.MONTH, -1).toJdkDate(), + userIdListByOrganizeIds, + FIRST_YEAR, dto.getOrganizationId()))); + return leaveRateVO; + } + + @Override + public FtbPersonnelsTurnoverLeaveRateVO getNewEmployeeLeaveRate(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverLeaveRateVO(); + } + // 新员工定义配置 + FtbPersonnelsRuleConfig ruleConfig = ftbPersonnelsRuleConfigMapper.selectById(1); + FtbPersonnelsTurnoverLeaveRateVO leaveRateVO = new FtbPersonnelsTurnoverLeaveRateVO(); + // 新员工离职比例 + // 离职总人数 + Date startTime = dto.getStartTime(); + Date endTime = dto.getEndTime(); + Integer lastInstallment = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(startTime, endTime, userIdListByOrganizeIds); + leaveRateVO.setLeaveCount(lastInstallment); + // 新员工离职人数 + Integer timeRule = ruleConfig.getTimeRule(); + Integer type = ruleConfig.getType(); + // 天数 + timeRule = type == 2 ? timeRule * 30 : timeRule; + Integer leaveRate = ftbPersonnelsTurnoverAnalysisMapper.getFirstMonthLeaveRate(startTime, endTime, userIdListByOrganizeIds, timeRule); + leaveRateVO.setLeaveTotal(leaveRate); + // 新员工离职比例 + leaveRateVO.setLeaveRate(calculateTheScale(leaveRate, lastInstallment)); + // 新员工离职比例 同比 + leaveRateVO.setLeaveRateYearOnYear(yearOnYearAndMonthOnMonth(calculateTheScale(leaveRate, lastInstallment), + turnoverRateSequentially( + DateUtil.offset(dto.getStartTime(), DateField.YEAR, -1).toJdkDate(), + DateUtil.offset(dto.getEndTime(), DateField.YEAR, -1).toJdkDate(), + userIdListByOrganizeIds, + timeRule, dto.getOrganizationId()))); + // 新员工离职比例 环比 + leaveRateVO.setLeaveRateAnnulus(yearOnYearAndMonthOnMonth(calculateTheScale(leaveRate, lastInstallment), + turnoverRateSequentially( + DateUtil.offset(dto.getStartTime(), DateField.MONTH, -1).toJdkDate(), + DateUtil.offset(dto.getEndTime(), DateField.MONTH, -1).toJdkDate(), + userIdListByOrganizeIds, + timeRule, dto.getOrganizationId()))); + return leaveRateVO; + } + + @Override + public List getLeaveReasonDistribution(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new ArrayList<>(); + } + // 离职原因分布 + List> result = ftbPersonnelsTurnoverAnalysisMapper.getLeaveReasonDistribution(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 将数据转化为map + List> list = result.stream().map(item -> { + List reasons = List.of(item.get("reason").split(",")); + return reasons.stream().map(reason -> { + FtbPersonnelsTurnoverLeaveReasonDistributionVO distributionVO = new FtbPersonnelsTurnoverLeaveReasonDistributionVO(); + distributionVO.setLeaveReason(reason); + distributionVO.setCount(1); + return distributionVO; + }).collect(Collectors.toList()); + }).collect(Collectors.toList()); + Map stringIntegerMap = list.stream().flatMap(List::stream).collect( + Collectors.groupingBy(FtbPersonnelsTurnoverLeaveReasonDistributionVO::getLeaveReason, + Collectors.summingInt(FtbPersonnelsTurnoverLeaveReasonDistributionVO::getCount))); + // 原因统计 + return stringIntegerMap.entrySet().stream().map(item -> { + FtbPersonnelsTurnoverLeaveReasonDistributionVO leaveReasonDistributionVO = new FtbPersonnelsTurnoverLeaveReasonDistributionVO(); + leaveReasonDistributionVO.setLeaveReason(item.getKey()); + leaveReasonDistributionVO.setCount(item.getValue()); + return leaveReasonDistributionVO; + }).collect(Collectors.toList()); + } + + @Override + public List getLeavePeoplePostRatio(FtbPersonnlesAnalysisDTO dto) { + List positionIds = dto.getPostId(); + String[] str = new String[0]; + if (CollUtil.isNotEmpty(positionIds)) { + str = positionIds.toArray(String[]::new); + } + List organizationId = dto.getOrganizationId(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(organizationId, str); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new ArrayList<>(); + } + // 离职人员岗位信息对应 + List> postRatioList = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeoplePostRatio(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 转化为map + List> lists = postRatioList.stream().map(item -> { + List workerGroupDataDtos = JSONObject.parseArray(item.get("organizeInfo"), WorkerGroupDataDto.class); + // 做数据过滤 + return workerGroupDataDtos.stream().filter(vo->{ + List dtoOrganizationIds = dto.getOrganizationId(); + List dtoPostIds = dto.getPostId(); + if (CollUtil.isNotEmpty(dtoOrganizationIds) + && CollUtil.isNotEmpty(dtoPostIds) + && dtoOrganizationIds.contains(vo.getAffiliatedOrg()) + && dtoPostIds.contains(vo.getAffiliatedPosition())){ + return true; + } + if (CollUtil.isNotEmpty(dtoOrganizationIds) + && CollUtil.isEmpty(dtoPostIds) + && dtoOrganizationIds.contains(vo.getAffiliatedOrg())){ + return true; + } + if ( CollUtil.isEmpty(dtoOrganizationIds) + && CollUtil.isNotEmpty(dtoPostIds) + && dtoPostIds.contains(vo.getAffiliatedPosition())){ + return true; + } + if (CollUtil.isEmpty(dtoOrganizationIds) + && CollUtil.isEmpty(dtoPostIds)){ + return true; + } + return false; + }).map(vo -> { + FtbPersonnelsTurnoverLeavePeoplePostRatioVO ratioVO = new FtbPersonnelsTurnoverLeavePeoplePostRatioVO(); + ratioVO.setPostName(vo.getAffiliatedPositionName()); + ratioVO.setPostId(vo.getAffiliatedPosition()); + ratioVO.setCount(1); + return ratioVO; + }).collect(Collectors.toList()); + }).collect(Collectors.toList()); + // 统计 + Map stringIntegerMap = lists.stream().flatMap(List::stream).collect( + Collectors.groupingBy(vo->vo.getPostId()+"::"+vo.getPostName(), + Collectors.summingInt(FtbPersonnelsTurnoverLeavePeoplePostRatioVO::getCount))); + // 总人数表示:指定岗位下截至筛选月以前的在职人数+筛选月当月的岗位离职人数 + return stringIntegerMap.entrySet().stream().map(item -> { + FtbPersonnelsTurnoverLeavePeoplePostRatioVO postRatioVO = new FtbPersonnelsTurnoverLeavePeoplePostRatioVO(); + String itemKey = item.getKey(); + String[] split = itemKey.split("::"); + String postId = split[0]; + postRatioVO.setPostId(postId); + String postName = split[1]; + postRatioVO.setPostName(postName); + // 离职人数 + Integer count = item.getValue(); + postRatioVO.setCount(count); + // 获取总人数 + // 截止月份在职员工 + Integer totalCount1 = getTotalCount(postId, dto.getEndTime(),organizationId); + // 总人数表示:指定岗位下截至筛选月以前的在职人数+筛选月当月的岗位离职人数 + int totalCount = BigDecimal.valueOf(totalCount1).add(BigDecimal.valueOf(count)).intValue(); + postRatioVO.setTotalCount(totalCount); + // 占比率 + postRatioVO.setRatio(calculateTheScale(count,totalCount)); + return postRatioVO; + }).collect(Collectors.toList()); + } + public Integer getTotalCount(String postId, Date startTime, List organizationId) { + QueryListUserByLevelDTO userBatchDTO = new QueryListUserByLevelDTO(); + userBatchDTO.setPositionIds(List.of(postId)); + userBatchDTO.setOrganizeIds(organizationId); + ActionResult> userInfoBatch = v2UserApi.listUserByLevel(userBatchDTO); + if (userInfoBatch.getCode() == 200) { + DateTime dateTime = DateUtil.endOfMonth(startTime); + return Math.toIntExact(userInfoBatch.getData().stream().filter(v -> (v.getWorkStatusEnums() != null && v.getEntryDate() != null && + ON_THE_JOB_STATUS.contains(v.getWorkStatusEnums().getCode())) && + dateTime.after(v.getEntryDate())).count()); + } + return 0; + } + + // 离职人员司龄类型 + // 1 不满3个月 + // 2 3-6个月 + // 3 7个月~1年 + // 4 1~3年 + // 5 4~5年 + // 6 6~10年 + // 7 10年以上 + private static final List LEAVE_RANGE = Arrays.asList(1, 2, 3, 4, 5, 6, 7); + + @Override + public List getLeavePeopleAgeDistribution(FtbPersonnlesAnalysisDTO dto) { + // 初始化司龄类型 + List list = LEAVE_RANGE.stream().map(item -> { + FtbPersonnelsTurnoverLeavePeopleAgeDistributionVO leavePeopleAgeDistributionVO = new FtbPersonnelsTurnoverLeavePeopleAgeDistributionVO(); + leavePeopleAgeDistributionVO.setLeavePeopleAgeDistributionType(item); + leavePeopleAgeDistributionVO.setCount(0); + leavePeopleAgeDistributionVO.setRatio(BigDecimal.ZERO); + return leavePeopleAgeDistributionVO; + }).collect(Collectors.toList()); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return list; + } + // 离职总人数 + Integer lastInstallment = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + List> mapList = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleAgeDistribution(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + list.forEach(item->{ + // 计算不同的司龄 + Integer distributionType = item.getLeavePeopleAgeDistributionType(); + Predicate> predicate = null; + switch (distributionType) { + // 1 不满3个月 + case 1: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) <= 30 * 3; + break; + // 2 3-6个月 + case 2: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) > 30 * 3 && Double.parseDouble(vo.get("deff").toString()) <= 30 * 6; + break; + // 3 7个月~1年 + case 3: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) > 30 * 6 && Double.parseDouble(vo.get("deff").toString()) <= 365; + break; + // 4 1~3年 + case 4: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) > 365 && Double.parseDouble(vo.get("deff").toString()) <= 365 * 3; + break; + // 5 4~5年 + case 5: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) > 365 * 3 && Double.parseDouble(vo.get("deff").toString()) <= 365 * 5; + break; + // 6 6~10年 + case 6: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) > 365 * 5 && Double.parseDouble(vo.get("deff").toString()) <= 365 * 10; + break; + // 7 10年以上 + case 7: + predicate = vo ->Double.parseDouble(vo.get("deff").toString()) > 365 * 10; + break; + } + Long count = mapList.stream().filter(v->ObjectUtil.isNotNull(v.get("deff"))).filter(predicate).count(); + item.setCount(count.intValue()); + // 计算比例 + item.setRatio( calculateTheScale(count.intValue(), lastInstallment)); + }); + return list; + } + + @Override + public List getLeavePeopleAgeRatio(FtbPersonnlesAnalysisDTO dto) { + //离职人员类型 + Integer configType = personnlesInfoConfigService.queryType(1); + if (configType == null) { + throw new RuntimeException("获取年龄配置失败"); + } + // 1平均 2自定义 + List rangeDTOS; + if (configType == 1){ + FtbRangeConfigVO configVO = personnlesInfoConfigService.queryInfo(1); + rangeDTOS = RangeConfigUtil.generateRanges(configVO.getFirstGear(), configVO.getIntervalValue(), configVO.getNumberOfGears(), configVO.getPreviewValue()); + }else { + List rangeConfigDIYVOS = personnlesInfoConfigService.queryDiyInfo(1); + rangeDTOS = rangeConfigDIYVOS.stream().map(v->{ + RangeDTO rangeDTO = new RangeDTO(); + rangeDTO.setStartPrice(v.getStartInterval()); + rangeDTO.setEndPrice(v.getEndInterval()); + rangeDTO.setPriceBandDisplayStr(v.getPreviewValue()); + return rangeDTO; + }).collect(Collectors.toList()); + } + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return rangeDTOS.stream().map(v->{ + FtbPersonnelsTurnoverLeavePeopleAgeRatioVO ageRatioVO = new FtbPersonnelsTurnoverLeavePeopleAgeRatioVO(); + ageRatioVO.setLeavePeopleAgeRatioName(v.getPriceBandDisplayStr()); + ageRatioVO.setCount(0); + ageRatioVO.setRatio(BigDecimal.ZERO); + return ageRatioVO; + }).collect(Collectors.toList()); + } + // 离职总人数 + //Integer lastInstallment = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + List> mapList = dynamicallyObtainTheRequiredLogFieldValue(dto, userIdListByOrganizeIds); + return rangeDTOS.stream().map(a->{ + FtbPersonnelsTurnoverLeavePeopleAgeRatioVO ageRatioVO = new FtbPersonnelsTurnoverLeavePeopleAgeRatioVO(); + ageRatioVO.setLeavePeopleAgeRatioName(a.getPriceBandDisplayStr()); + Integer startPrice = a.getStartPrice(); + Integer endPrice = a.getEndPrice(); + long count = mapList.stream().filter(v -> { + if (StringUtils.isEmpty(v.get("age").toString())) return false; + BigDecimal decimal = BigDecimal.valueOf(Double.parseDouble(v.get("age").toString())); + BigDecimal startPriceDecimal = BigDecimal.valueOf(startPrice); + if (endPrice == null) { + if (decimal.compareTo(startPriceDecimal) >= 0) { + return true; + } + }else { + BigDecimal endPriceDecimal = BigDecimal.valueOf(endPrice); + if ( decimal.compareTo(startPriceDecimal) >= 0 && decimal.compareTo(endPriceDecimal) < 0) { + // 条件成立时的逻辑 + return true; + } + } + // 判断是否在范围内 + return false; + }).count(); + ageRatioVO.setRatio(calculateTheScale((int) count, mapList.size())); + ageRatioVO.setCount((int) count); + return ageRatioVO; + }).collect(Collectors.toList()); + } + + @NotNull + private List> dynamicallyObtainTheRequiredLogFieldValue(FtbPersonnlesAnalysisDTO dto, List userIdListByOrganizeIds) { + // 离职人员的数据项 + List> mapListWithLog = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleAgeLogRatio(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds, "birthday"); + List> mapList = mapListWithLog.stream().map( + item -> { + if (ObjectUtil.isEmpty( item.get("data")) || StringUtils.isEmpty((String) item.get("data"))) return null; + String data = item.get("data").toString(); + Map map = new HashMap<>(); + if (StringUtils.isNotEmpty(data) ) { + // 将字符串转换为 Date 对象 + long betweenYear = DateUtil.ageOfNow(data); + map.put("age", betweenYear); + map.put("userId", item.get("userId")); + } + return map; + } + ).filter(ObjectUtil::isNotEmpty).collect(Collectors.toList()); + return mapList; + } + @NotNull + private List dynamicallyObtainTheRequiredLogFieldValueWithWorkSex(FtbPersonnlesAnalysisDTO dto, List userIdListByOrganizeIds,String number) { + // 离职人员的数据项 + List> mapListWithLog = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleAgeLogRatio(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds,"workerSex"); + List mapList = mapListWithLog.stream().map( + item -> { + if (ObjectUtil.isEmpty( item.get("data")) || StringUtils.isEmpty((String) item.get("data"))) return null; + String data = item.get("data").toString(); + // 0 未知 1 男 2 女 + if (StringUtils.isNotEmpty(data) && number.equals(data) ) { + // 将字符串转换为 Date 对象 + return item.get("userId").toString(); + } + return null; + } + ).filter(ObjectUtil::isNotEmpty).collect(Collectors.toList()); + return mapList; + } + + @Override + public FtbPersonnelsTurnoverRateVO ftbPersonnelsTurnoverAnalysisService(FtbPersonnlesAnalysisDTO dto) { + FtbPersonnelsTurnoverRateVO turnoverRateVO = new FtbPersonnelsTurnoverRateVO(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverRateVO(); + } + List listOfDates = new LinkedList<>(); + // 当月离职人数 + List theNumberOfDeparturesInTheMonth = new LinkedList<>(); + // 首年离职率 + List firstYearTurnoverRate = new LinkedList<>();; + // 首月离职率 + List turnoverRateInTheFirstMonth = new LinkedList<>(); + // 日期列表 + long betweenMonth = DateUtil.betweenMonth(dto.getStartTime(), dto.getEndTime(), true); + for (int i = 0; i <= betweenMonth; i++) { + // 月份 + Date offsetTime = DateUtil.offset(dto.getStartTime(), DateField.MONTH, i).toJdkDate(); + // 构建时间 + DateTime monthStartTime = DateUtil.beginOfMonth(offsetTime); + DateTime monthEndTime = DateUtil.endOfMonth(offsetTime); + Integer peopleMonth = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(monthStartTime,monthEndTime,userIdListByOrganizeIds); + // 当月离职人数 + theNumberOfDeparturesInTheMonth.add(peopleMonth); + // 首月离职率 + BigDecimal firstMonthLeaveRate = turnoverRateSequentially(monthStartTime, monthEndTime, userIdListByOrganizeIds,FIRST_MONTH, dto.getOrganizationId()); + turnoverRateInTheFirstMonth.add(firstMonthLeaveRate); + // 首年离职率 + BigDecimal sequentially = turnoverRateSequentially(monthStartTime, monthEndTime, userIdListByOrganizeIds, FIRST_YEAR, dto.getOrganizationId()); + firstYearTurnoverRate.add(sequentially); + // 封装 + String month = DatePattern.NORM_MONTH_FORMAT.format(offsetTime); + listOfDates.add(month); + } + // 日期列表 + turnoverRateVO.setListOfDates(listOfDates); + turnoverRateVO.setTheNumberOfDeparturesInTheMonth(theNumberOfDeparturesInTheMonth); + turnoverRateVO.setTurnoverRateInTheFirstMonth(turnoverRateInTheFirstMonth); + turnoverRateVO.setFirstYearTurnoverRate(firstYearTurnoverRate); + return turnoverRateVO; + } + @Override + public PageListVO getLeaveRateDetail(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new PageListVO<>(); + } + // 构建时间 + DateTime monthStartTime = DateUtil.beginOfMonth(dto.getStartTime()); + DateTime monthEndTime = DateUtil.endOfMonth(dto.getEndTime()); + // 获取离职人员明细数据 + List> leaveRateDetail = ftbPersonnelsTurnoverAnalysisMapper.getLeaveRateDetail(monthStartTime, monthEndTime, userIdListByOrganizeIds); + // 转换为VO对象列表 + List collect = leaveRateDetail.stream().map(item -> { + FtbPersonnelsTurnoverDetailVO detailVO = new FtbPersonnelsTurnoverDetailVO(); + detailVO.setName((String) item.get("name")); + detailVO.setEmployeeId((String) item.get("employeeId")); + detailVO.setEntryDate(CovertDateUtils.convertToStringDate( item.get("entryDate"))); + detailVO.setLeaveDate( CovertDateUtils.convertToStringDate(item.get("leaveDate"))); + + // 解析组织信息 + if (item.containsKey("organizationInfo")) { + Object o = item.get("organizationInfo"); + String organizationInfo = (String) o; + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + + return detailVO; + }).collect(Collectors.toList()); + PaginationVO paginationVO = new PaginationVO(); + paginationVO.setCurrentPage(dto.getCurrentPage()); + paginationVO.setPageSize(dto.getPageSize()); + paginationVO.setTotal(collect.size()); + return CultivatePage.paginate( collect,paginationVO); + } + + @Override + public FtbPersonnelsTurnoverRateComparisonVO getLeaveRateComparison(FtbPersonnlesAnalysisDTO dto) { + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return new FtbPersonnelsTurnoverRateComparisonVO(); + } + // 离职人员岗位信息对应 + List> postRatioList = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeoplePostRatio(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 1部门 2 门店 3岗位 + Integer type = dto.getType(); + List> maps = postRatioList.stream().map(item -> { + Map map = new HashMap<>(); + List workerGroupDataDtos = JSONObject.parseArray(item.get("organizeInfo"), WorkerGroupDataDto.class); + map.put("userId", item.get("userId")); + Map mapList = workerGroupDataDtos.stream().map(vo -> { + Map hashMap = new HashMap<>(); + String id = null; + String name = null; + // 部门 + if (type == 1) { + id = vo.getDepartmentId(); + name = vo.getDepartmentName(); + } else if (type == 2) { + // 2 门店 + id = vo.getShopId(); + name = vo.getShopName(); + } else if (type == 3) { + // 3 岗位 + id = vo.getAffiliatedPosition(); + name = vo.getAffiliatedPositionName(); + } + hashMap.put("id", id); + hashMap.put("name", name); + return hashMap; + }).filter(ObjectUtil::isNotEmpty).findFirst().orElse(new HashMap<>()); + map.put("workerGroupDataDtos", JSONObject.toJSONString(mapList)); + return map; + }).collect(Collectors.toList()); + // 根据过滤数据进行分组 计算人数 + List rateInfoVOS = maps.stream().collect(Collectors.groupingBy(item -> item.get("workerGroupDataDtos"), + Collectors.mapping(item -> item.get("userId"), Collectors.toList()))) + .entrySet().stream().map(vo -> { + FtbPersonnelsTurnoverRateInfoVO rateInfoVO = new FtbPersonnelsTurnoverRateInfoVO(); + Object name = JSONObject.parseObject(vo.getKey(), Map.class).get("name"); + // 对应分类名称 + rateInfoVO.setName(String.valueOf(name)); + // 离职人数 + int totalNumberOfPeople = postRatioList.size(); + rateInfoVO.setTotal(totalNumberOfPeople); + int percentageOfPeople = vo.getValue().size(); + rateInfoVO.setTurnOverRate(calculateTheScale(percentageOfPeople, totalNumberOfPeople)); + return rateInfoVO; + }).collect(Collectors.toList()); + FtbPersonnelsTurnoverRateComparisonVO ftbPersonnelsTurnoverRateComparisonVO = new FtbPersonnelsTurnoverRateComparisonVO(); + ftbPersonnelsTurnoverRateComparisonVO.setList(rateInfoVOS); + ftbPersonnelsTurnoverRateComparisonVO.setType(type); + return ftbPersonnelsTurnoverRateComparisonVO; + } + + + + @Override + public PageListVO getNewEmployeeLeaveRateDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 新员工定义配置 + FtbPersonnelsRuleConfig ruleConfig = ftbPersonnelsRuleConfigMapper.selectById(1); + Integer timeRule = ruleConfig.getTimeRule(); + Integer type = ruleConfig.getType(); + // 天数 + timeRule = type == 2 ? timeRule * 30 : timeRule; + // 获取新员工离职人员 + List> leaveRateDetail = ftbPersonnelsTurnoverAnalysisMapper.getLeaveRateDetail(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 转换为 VO 对象列表 + for (Map item : leaveRateDetail) { + Date entryDate = CovertDateUtils.convertToDate(item.get("entryDate")); + Date leaveDate = CovertDateUtils.convertToDate(item.get("leaveDate")); + if (entryDate != null && leaveDate != null) { + long days = DateUtil.betweenDay(entryDate, leaveDate, true); + if (days <= timeRule) { + FtbPersonnelsTurnoverNewEmployeeDetailVO detailVO = new FtbPersonnelsTurnoverNewEmployeeDetailVO(); + detailVO.setName((String) item.get("name")); + detailVO.setEmployeeId((String) item.get("employeeId")); + String toStringDate = CovertDateUtils.convertToStringDate(item.get("leaveDate")); + detailVO.setLeaveDate(toStringDate); + // 计算司龄 + detailVO.setSeniority(CompanyAgeUtil.calculateSeniority(entryDate, leaveDate)); + // 解析组织信息 + String organizationInfo = (String) item.get("organizationInfo"); + if (StringUtils.isNotEmpty(organizationInfo)) { + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + + results.add(detailVO); + } + } + } + results.sort(Comparator.comparing(FtbPersonnelsTurnoverNewEmployeeDetailVO::getOrganization)); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + @Override + public PageListVO getLeaveReasonDistributionDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取离职人员明细数据 + List> leaveRateDetail = ftbPersonnelsTurnoverAnalysisMapper.getLeaveRateDetail(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 转换为 VO 对象列表 + results = leaveRateDetail.stream().map(item -> { + FtbPersonnelsTurnoverLeaveReasonDetailVO detailVO = new FtbPersonnelsTurnoverLeaveReasonDetailVO(); + detailVO.setName((String) item.get("name")); + detailVO.setEmployeeId((String) item.get("employeeId")); + detailVO.setLeaveDate(CovertDateUtils.convertToStringDate(item.get("leaveDate"))); + + // 解析组织信息 + String organizationInfo = (String) item.get("organizationInfo"); + if (StringUtils.isNotEmpty(organizationInfo)) { + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + + // 获取离职原因 + detailVO.setLeaveReason((String) item.get("reason")); + return detailVO; + }).collect(Collectors.toList()); + results.sort(Comparator.comparing(FtbPersonnelsTurnoverLeaveReasonDetailVO::getOrganization)); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + + @Override + public PageListVO getLeavePeoplePostRatioDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List positionIds = dto.getPostId(); + String[] str = new String[0]; + if (CollUtil.isNotEmpty(positionIds)) { + str = positionIds.toArray(String[]::new); + } + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId(),str); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取离职人员明细数据 + List> leaveRateDetail = ftbPersonnelsTurnoverAnalysisMapper.getLeaveRateDetail(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 转换为 VO 对象列表 + results = leaveRateDetail.stream().filter(item -> item.get("organizationInfo") != null).map(item -> { + FtbPersonnelsTurnoverPostRatioDetailVO detailVO = new FtbPersonnelsTurnoverPostRatioDetailVO(); + detailVO.setName((String) item.get("name")); + detailVO.setEmployeeId((String) item.get("employeeId")); + detailVO.setLeaveDate(CovertDateUtils.convertToStringDate(item.get("leaveDate"))); + + // 解析组织信息 + String organizationInfo = (String) item.get("organizationInfo"); + if (StringUtils.isNotEmpty(organizationInfo)) { + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + + return detailVO; + }).collect(Collectors.toList()); + results.sort(Comparator.comparing(FtbPersonnelsTurnoverPostRatioDetailVO::getOrganization)); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + + @Override + public PageListVO getLeavePeopleAgeDistributionDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取离职人员明细数据 + List> leaveRateDetail = ftbPersonnelsTurnoverAnalysisMapper.getLeaveRateDetail(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + // 转换为 VO 对象列表 + results = leaveRateDetail.stream().map(item -> { + FtbPersonnelsTurnoverSeniorityDetailVO detailVO = new FtbPersonnelsTurnoverSeniorityDetailVO(); + detailVO.setName((String) item.get("name")); + detailVO.setEmployeeId((String) item.get("employeeId")); + detailVO.setLeaveDate(CovertDateUtils.convertToStringDate(item.get("leaveDate"))); + + // 计算司龄 + Date entryDate = CovertDateUtils.convertToDate(item.get("entryDate")); + Date leaveDate = CovertDateUtils.convertToDate(item.get("leaveDate")); + if (entryDate != null && leaveDate != null) { + detailVO.setSeniority(CompanyAgeUtil.calculateSeniority(entryDate, leaveDate)); + } + + // 解析组织信息 + String organizationInfo = (String) item.get("organizationInfo"); + if (StringUtils.isNotEmpty(organizationInfo)) { + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + + return detailVO; + }).collect(Collectors.toList()); + results.sort(Comparator.comparing(FtbPersonnelsTurnoverSeniorityDetailVO::getOrganization)); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + + @Override + public PageListVO getLeavePeopleAgeRatioDetail(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取离职人员年龄信息 + List> mapList = dynamicallyObtainTheRequiredLogFieldValue(dto, userIdListByOrganizeIds); + // 获取离职人员明细数据 + List> leaveRateDetail = ftbPersonnelsTurnoverAnalysisMapper.getLeaveRateDetail(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds); + Map> leaveRateMap = leaveRateDetail.stream().collect(Collectors.toMap(item -> (String) item.get("userId"), item -> item)); + + // 转换为VO对象列表 + for (Map item : mapList) { + String userId = (String) item.get("userId"); + Map leaveInfo = leaveRateMap.get(userId); + if (leaveInfo != null) { + FtbPersonnelsTurnoverAgeDetailVO detailVO = new FtbPersonnelsTurnoverAgeDetailVO(); + detailVO.setName((String) leaveInfo.get("name")); + detailVO.setEmployeeId((String) leaveInfo.get("employeeId")); + detailVO.setLeaveDate(CovertDateUtils.convertToStringDate(leaveInfo.get("leaveDate"))); + detailVO.setAge(Integer.parseInt(item.get("age").toString())); + + // 解析组织信息 + String organizationInfo = (String) leaveInfo.get("organizationInfo"); + if (StringUtils.isNotEmpty(organizationInfo)) { + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + + results.add(detailVO); + } + } + results.sort(Comparator.comparing(FtbPersonnelsTurnoverAgeDetailVO::getOrganization)); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + + @Override + public PageListVO breakdownOfGenderDistributionOfResignation(FtbPersonnlesAnalysisDTO dto) { + List results = new ArrayList<>(); + List userIdListByOrganizeIds = doEmployeesOfYourOrganization(dto.getOrganizationId()); + if (CollUtil.isEmpty(userIdListByOrganizeIds)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取离职人员年龄信息 + List> mapListWithList = ftbPersonnelsTurnoverAnalysisMapper. + getLeavePeopleAgeLogRatio(dto.getStartTime(), dto.getEndTime(), userIdListByOrganizeIds,"workerSex"); + List genderList = List.of("0","1","2"); + mapListWithList = mapListWithList.stream().filter(v->{ + if (ObjectUtil.isEmpty( v.get("data")) || StringUtils.isEmpty( v.get("data").toString())) return false; + String string = v.get("data").toString(); + if (genderList.contains(string)) return true; + return false; + }).collect(Collectors.toList()); + if (CollUtil.isEmpty(mapListWithList)) { + return CultivatePage.paginate(results, new PaginationVO()); + } + // 获取离职人员明细数据 + Map> leaveRateMap = mapListWithList.stream().collect(Collectors.toMap(item -> (String) item.get("userId"), item -> item)); + List strings = mapListWithList.stream().map(v -> (String) v.get("phone")).collect(Collectors.toList()); + // 转换为VO对象列表 + for (Map item : mapListWithList) { + String userId = (String) item.get("userId"); + Map leaveInfo = leaveRateMap.get(userId); + if (leaveInfo != null) { + FtbPersonnelsTurnoverGenderDetailVO detailVO = new FtbPersonnelsTurnoverGenderDetailVO(); + detailVO.setName((String) leaveInfo.get("name")); + detailVO.setEmployeeId((String) leaveInfo.get("employeeId")); + detailVO.setLeaveDate(CovertDateUtils.convertToStringDate(leaveInfo.get("leaveDate"))); + String phone = (String)item.get("phone"); + if (ObjectUtil.isNotEmpty(item.get("data"))) { + String value = item.get("data").toString(); + // 0 未知 1 男 2 女 + String gender = null; + switch (value){ + case "0": + gender = "未知"; + break; + case "1": + gender = "男"; + break; + case "2": + gender = "女"; + break; + } + detailVO.setGender(gender); + } + // 解析组织信息 + String organizationInfo = (String) leaveInfo.get("organizationInfo"); + if (StringUtils.isNotEmpty(organizationInfo)) { + List workerGroupDataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + WorkerGroupDataDto firstOrg = workerGroupDataDtos.get(0); + detailVO.setOrganization(firstOrg.getAffiliatedOrgName()); + detailVO.setJobLevel(firstOrg.getAffiliatedPositionName()); + detailVO.setJobGrade(firstOrg.getAffiliatedRankName()); + } + } + results.add(detailVO); + } + } + results.sort(Comparator.comparing(FtbPersonnelsTurnoverGenderDetailVO::getOrganization)); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(dto.getCurrentPage()); + pagination.setPageSize(dto.getPageSize()); + pagination.setTotal(results.size()); + return CultivatePage.paginate(results, pagination); + } + /** + * 计算同比环比 + * @return + */ + private BigDecimal yearOnYearAndMonthOnMonth(Object a, Object b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof BigDecimal) { + if (((BigDecimal) b).compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.subtract(b0).divide(b0,4, BigDecimal.ROUND_HALF_UP) + .multiply(BigDecimal.valueOf(100)).setScale(2, BigDecimal.ROUND_HALF_UP); + } + + /** + * 获取组织下员工ID集合 + * @param orgIds + * @return + */ + private List doEmployeesOfYourOrganization(List orgIds, String[]... postId) { + if (CollUtil.isNotEmpty(orgIds)){ + List managements = turnoverManagementMapper.queryTurnoverListWithStatics(); + List postIds; + if (ObjectUtil.isNotEmpty(postId)){ + postIds = Arrays.stream(postId).flatMap(Arrays::stream).collect(Collectors.toList()); + } else { + postIds = null; + } + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isEmpty(postIds)) + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> { + List org = JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()); + if (org.stream().anyMatch(orgIds::contains))return true; + return false; + } + ).map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isNotEmpty(postIds)) return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo ->CollUtil.isNotEmpty( + JSONObject.parseArray( + vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().filter(po->orgIds.contains(po.getAffiliatedOrg()) && postIds.contains(po.getAffiliatedPosition())).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + } + return null; + } + /** + * 比率计算 + * @param a 分子 + * @param b 分母 + * @return + */ + private BigDecimal calculateTheScale(Object a, Object b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof BigDecimal) { + if (((BigDecimal) b).compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.divide(b0, 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP); + } + + + /** + * 首月 / 首周 / 首年 离职率 + * + * @param ringStartTime + * @param ringEndTime + * @param userIdListByOrganizeIds + * @param firstMonth + * @param organizationId + * @return + */ + private BigDecimal turnoverRateSequentially(Date ringStartTime, Date ringEndTime, List userIdListByOrganizeIds, Integer firstMonth, List organizationId) { + // Integer lastInstallment = ftbPersonnelsTurnoverAnalysisMapper.getLeavePeopleMonth(ringStartTime, ringEndTime, userIdListByOrganizeIds); + // 当月入职人数 取实际入职日期 + + QueryPageUserDTO dto = new QueryPageUserDTO(); + dto.setOrganizeIds(organizationId); + dto.setIsPage(false); + ActionResult> listByLevel = v2UserApi.pagePost(dto); + List userSubQueryList = listByLevel.getCode() == 200 ? + listByLevel.getData().getList().stream().map(UserBoundVO::getId).collect(Collectors.toList()) + : new ArrayList<>(); + userIdListByOrganizeIds.addAll(userSubQueryList); + Integer lastInstallment = ftbPersonnelsTurnoverAnalysisMapper.numberOfOnboardedEmployees(ringStartTime, ringEndTime, userIdListByOrganizeIds); + Integer leaveRate = ftbPersonnelsTurnoverAnalysisMapper.getFirstMonthLeaveRate(ringStartTime, ringEndTime, userIdListByOrganizeIds,firstMonth); + return calculateTheScale( leaveRate, lastInstallment); + } + + @Override + public void getNewEmployeeLeaveRateDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getNewEmployeeLeaveRateDetail(dto).getList(); + exportExcel(response, "新员工离职比例明细", dataList, FtbPersonnelsTurnoverNewEmployeeDetailVO.class); + } + + @Override + public void getLeaveReasonDistributionDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getLeaveReasonDistributionDetail(dto).getList(); + exportExcel(response, "离职原因分布明细", dataList, FtbPersonnelsTurnoverLeaveReasonDetailVO.class); + } + + @Override + public void getLeavePeoplePostRatioDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getLeavePeoplePostRatioDetail(dto).getList(); + exportExcel(response, "各岗位人数占比明细", dataList, FtbPersonnelsTurnoverPostRatioDetailVO.class); + } + + @Override + public void getLeavePeopleAgeDistributionDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getLeavePeopleAgeDistributionDetail(dto).getList(); + exportExcel(response, "离职人员司龄分布明细", dataList, FtbPersonnelsTurnoverSeniorityDetailVO.class); + } + + @Override + public void getLeavePeopleAgeRatioDetailExport(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = getLeavePeopleAgeRatioDetail(dto).getList(); + exportExcel(response, "离职人员年龄分布占比明细", dataList, FtbPersonnelsTurnoverAgeDetailVO.class); + } + + + @Override + public void breakdownOfGenderDistributionOfResignationExprot(FtbPersonnlesAnalysisDTO dto, HttpServletResponse response) { + dto.setPageSize(-1); + List dataList = breakdownOfGenderDistributionOfResignation(dto).getList(); + exportExcel(response, "离职人员性别分布明细", dataList, FtbPersonnelsTurnoverGenderDetailVO.class); + } + + + /** + * 导出 Excel 文件 + * + * @param response HTTP 响应 + * @param fileName 文件名 + * @param dataList 数据列表 + * @param clazz VO 类 + */ + @SneakyThrows + private void exportExcel(HttpServletResponse response, String fileName, List dataList, Class clazz) { + EasyExcelUtils.exportExcel(response, fileName, dataList, clazz); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverHandoverServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverHandoverServiceImpl.java new file mode 100644 index 0000000..1ebae88 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverHandoverServiceImpl.java @@ -0,0 +1,13 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.personnels.mapper.FtbPersonnelsTurnoverHandoverMapper; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverHandover; +import jnpf.personnels.service.FtbPersonnelsTurnoverHandoverService; +@Service +public class FtbPersonnelsTurnoverHandoverServiceImpl extends ServiceImpl implements FtbPersonnelsTurnoverHandoverService { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverManagementServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverManagementServiceImpl.java new file mode 100644 index 0000000..f086f7e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsTurnoverManagementServiceImpl.java @@ -0,0 +1,1879 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.pagehelper.PageInfo; +import com.google.common.collect.Maps; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.ContractSignPersonnelNoticeApi; +import jnpf.EvaluateManageApi; +import jnpf.account.PTenantAccountApi; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.authority.service.FtbPermissionRoleAuthorizePersonService; +import jnpf.authority.utils.PermissionsApplicableEnums; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.authority.utils.PermissionsEnums; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.cultivate.service.impl.FtbCultivateFileService; +import jnpf.cultivate.utils.CultivatePerUtils; +import jnpf.engine.FlowTaskApi; +import jnpf.engine.entity.FlowTaskEntity; +import jnpf.engine.model.flowstatistics.vo.ReloadTaskVO; +import jnpf.entity.StoreEntity; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.dto.offline.FtbCultivateOfflineFileDTO; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.cultivate.vo.offline.FtbCultivateOfflineFileVO; +import jnpf.model.enums.FtbPersonnelsAuditTaskEnum; +import jnpf.model.enums.FtbPersonnelsCofigEnum; +import jnpf.model.personnels.bo.ExportRosterOneVo; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.base.PersonnelsQueryDTO; +import jnpf.model.personnels.dto.black.FtbPersonnelsBlacklistAddDTO; +import jnpf.model.personnels.dto.regular.FtbPersonnelsForAppQueryDTO; +import jnpf.model.personnels.dto.staff.field.ExportFormTypeDto; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.dto.staff.roster.StaffDepartDto; +import jnpf.model.personnels.dto.staff.roster.StaffHomeDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.dto.turnover.*; +import jnpf.model.personnels.po.*; +import jnpf.model.personnels.req.roster.StaffRosterListReq; +import jnpf.model.personnels.vo.app.FtbPersonnelsBubbleCountVO; +import jnpf.model.personnels.vo.task.FtbPersonnelsResult; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurOrgInfo; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverInfoVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.RoleApi; +import jnpf.permission.UserApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.UpdateUserDTO; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.organize.OrganizeListVO; +import jnpf.permission.model.role.RoleCountUserRelationVO; +import jnpf.permission.model.role.RoleUserBoundVO; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.model.user.PositionMoreBoundVO; +import jnpf.permission.model.user.SubordinateUserInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.*; +import jnpf.personnels.service.*; +import jnpf.personnels.utils.*; +import jnpf.store.service.StoreService; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.context.SpringContext; +import jnpf.util.excel.EasyExcelUtils; +import jnpf.vo.ContractSignPersonnelFlagVo; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class FtbPersonnelsTurnoverManagementServiceImpl extends ServiceImpl implements FtbPersonnelsTurnoverManagementService { + + @Resource + private FtbPersonnelsTurnoverHandoverService handoverService; + + @Resource + private FtbCultivateFileService ftbCultivateFileService; + + @Resource + private FtbPersonnelsAuditRunTaskService runTaskService; + + @Resource + private FtbPersonnelsAuditSubConfigService subConfigService; + + @Resource + private FtbPersonnelsTurnoverAccountRegistrationMapper registrationMapper; + + @Resource + private StoreService storeService; + + @Autowired + private OrganizeApi organizeApi; + + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private RoleApi roleApi; + + @Resource + public PersonnelPerUtils personnelPerUtils; + + @Autowired + private UserApi userApi; + + @Resource + private CultivatePerUtils cultivatePerUtils; + + @Resource + private FtbPersonnelsBlacklistService blacklistService; + + @Resource + private FtbPersonnelsBlacklistTypeMapper ftbPersonnelsBlacklistTypeMapper; + + @Resource + FtbPersonnelsRegularManagementService regularManagementService; + + @Resource + FtbPersonnelsPostApplyService personnelsPostApplyService; + + @Resource + FtbPersonnelsStaffTransferPositionService transferPositionService; + + @Resource + FtbPersonnelsPermissionsService personnelsPermissionsService; + + @Resource + FlowTaskApi flowTaskApi; + @Resource + ContractSignPersonnelNoticeApi contractSignPersonnelNoticeApi; + + @Resource + EvaluateManageApi evaluateManageApi; + + @Resource + PermissionsUtils permissionsUtils; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + FtbPermissionRoleAuthorizePersonService ftbPermissionRoleAuthorizePersonService; + + + @Autowired + FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + + @Autowired + FtbPersonnelsTransferManageService transferManageService; + + @Autowired + RedissonClient redissonClient; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataMapper dataMapper; + + @Autowired + FtbPersonnelsSecondmentManagementService secondmentManagementService; + + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + + @Autowired + private RosterExportUtils rosterExportUtils; + + @Autowired + private FtbPersonnelsRegistrationFormFieldOptionService fieldOptionService; + + @Autowired + private PTenantAccountApi pTenantAccountApi; + + + @Override + @Transactional + public String applyForResignation(FtbPersonnelsTurnoverCreateDTO createDto) { + List userOrgBoundInfo = personnelOrgUtils.getUserOrgBoundInfo(createDto.getUserId()); + if (CollUtil.isNotEmpty(userOrgBoundInfo)) createDto.setOrganizationInfo(userOrgBoundInfo); + FtbPersonnelsTurnoverManagement management = FtbPersonnelsTurnoverCreateDTO.coverFtbPersonnelsTurnoverManagement(createDto); + UserInfo userInfo = UserProvider.getUser(); + if (management.getUserId().equals(userInfo.getUserId())){ + throw new RuntimeException("无法给自己进行手动离职!"); + } + if (CollUtil.isNotEmpty(createDto.getOrganizationInfo())) { + management.setOrganizationInfo( JSONObject.toJSONString(createDto.getOrganizationInfo())); + } + if (CollUtil.isNotEmpty(createDto.getOrganizationInfoForApp())) { + management.setOrganizationInfo( JSONObject.toJSONString(createDto.getOrganizationInfoForApp())); + } + String userId = management.getUserId(); + // 离职交接物品 + List createDtoHandover = createDto.getHandover(); + if (CollUtil.isNotEmpty(createDtoHandover)){ + //校验物品人是不是选择了本人 + List usrIds = createDtoHandover.stream().map(FtbPersonnelsTurnoverHandoverDTO::getArticleRecipientId).collect(Collectors.toList()); + if (usrIds.contains(userId)){ + throw new RuntimeException("离职交接物品不能选择本人!"); + } + } + // 校验是否处于流程中 + checkWhetherTheCheckIsInTheProcess(userId); + // 校验 + String flag = calibrationNumber(userId); + int record = secondmentManagementService.checkWhetherThereIsASecondmentRecord(userId, createDto.getResignationDate()); + if (record > 0){ + log.error("该员工离职日期处于借调时间段中,到达离职日期将自动结束借调!"); + flag = "3"; + } + // 标识强制申请 + if (createDto.getForceDelete() == null && flag != null) return flag; + // 设计多态审核 + // 编辑 + String newFlag = ""; + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition,"304"); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getUserId,userId); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark,0); + Long l = baseMapper.selectCount(queryWrapper); + if (l>0) throw new RuntimeException("该员工处于待离职,请勿重复操作!"); + FtbPersonnelsTurnoverManagement turnoverManagement = baseMapper.selectById(management.getId()); + if (ObjectUtil.isNotEmpty(turnoverManagement)){ + // 离职编辑 删除之前的信息 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsTurnoverHandover::getManagementId, management.getId()); + handoverService.remove(wrapper); + // 删除上传的文件重新上传 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateFile::getBusinessId, management.getId()); + ftbCultivateFileService.remove(updateWrapper); + } + FtbPersonnelsResult result = FtbPersonnelsResult.getResult(createDto.getSource(),createDto.getFlag()); + management.setTurnoverStatus(result.state); + management.setApplicationSource(result.source); + if (result.source == 0){ + management.setEmpResignationDate(createDto.getResignationDate()); + }else { + management.setResignationDate(createDto.getResignationDate()); + } + management.setTaskInfoId(null); + management.setCreatorTime(new Date()); + management.setEnableMark(0L); + management.setWorkingCondition("304"); + // 改为待离职状态 + staffRosterService.innerChangeWaitDepart(userId,null,0); + + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setWorkerStatus("304"); + v2UserApi.updateUserInfo(userId,userDTO); + + saveOrUpdate(management); + String managementId = management.getId(); + // 查询人员是否签署电子合同进行弹窗 + ActionResult actionResult = contractSignPersonnelNoticeApi.signPersonnel(userId, UserProvider.getUser().getTenantId()); + if (actionResult != null && actionResult.getCode() == 200){ + ContractSignPersonnelFlagVo data = actionResult.getData(); + if (data != null && CollUtil.isNotEmpty(data.getSignType()) ){ + if ( data.getSignType().contains(0)) { + newFlag = "6"; + }else if ( ! data.getSignType().contains(0) && data.getSignType().contains(1)){ + newFlag = "7" ; + } + } + } + //加入黑名单 + if (management.getIsBeBlackList() == 1){ + // 加入黑名单 + FtbPersonnelsBlacklistAddDTO ftbPersonnelsBlacklistAddDTO = FtbPersonnelsTurnoverCreateDTO.covertBlacklist(management); + blacklistService.addBlacklist(ftbPersonnelsBlacklistAddDTO); + } + + if (CollUtil.isNotEmpty(createDtoHandover)){ + Map> listMap = + createDtoHandover.stream().collect(Collectors.groupingBy(FtbPersonnelsTurnoverHandoverDTO::getTurnoverType, Collectors.toList())); + listMap.keySet().forEach(item->{ + List handoverDTOS = listMap.get(item); + List turnoverHandovers = handoverDTOS.stream().map(vo -> { + FtbPersonnelsTurnoverHandover handover = FtbPersonnelsTurnoverHandoverDTO.coverFtbPersonnelsTurnoverHandover(vo); + handover.setManagementId(managementId); + if (StringUtils.isEmpty(handover.getArticleRecipientId()) && + handover.getArticleRecipientId().equals(management.getUserId())) throw new RuntimeException("离职交接不能选择本人!"); + return handover; + }).collect(Collectors.toList()); + handoverService.saveBatch(turnoverHandovers); + }); + } + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(createDto.getFiles()) + .businessTypeID(managementId) + .type(FileEventDTO.FileType.RESIGNATION_MANAGEMENT) + .build())); + // 同步数据 + removeAccountAndAddDataSynchronizationStatus(managementId, management.getResignationDate(),userId,management.getUserName()); + return newFlag; + } + + + + @Override + @GlobalTransactional + public ActionResult applyForResignatioForOA(FtbPersonnelsTurnoverCreateDTO createDto) { + ActionResult actionResult = new ActionResult<>(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg( FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg() ); + if (StringUtils.isNotEmpty(createDto.getReasonForResignation())){ + List stringList = JSONObject.parseArray(createDto.getReasonForResignation(), String.class); + // 数据转换 + createDto.setReasonForResignation(stringList.stream().collect(Collectors.joining(","))); + } + Integer isBeBlackListForOA = createDto.getIsBeBlackListForOA(); + if (ObjectUtil.isNotEmpty(isBeBlackListForOA)){ + if (isBeBlackListForOA == 2) { + createDto.setIsBeBlackList(1); + } else { + createDto.setIsBeBlackList(0); + } + } + Integer blackListPeriodForOA = createDto.getBlackListPeriodForOA(); + if (ObjectUtil.isNotEmpty(blackListPeriodForOA)){ + createDto.setBlackListPeriod(blackListPeriodForOA -1 ); + } + FtbPersonnelsTurnoverManagement management = FtbPersonnelsTurnoverCreateDTO.coverFtbPersonnelsTurnoverManagement(createDto); + FtbPersonnelsResult result = FtbPersonnelsResult.getResult(createDto.getSource(),createDto.getFlag()); + management.setTurnoverStatus(result.state); + management.setApplicationSource(result.source); + if (result.source == 0){ + management.setEmpResignationDate(createDto.getResignationDate()); + }else { + management.setResignationDate(createDto.getResignationDate()); + } + List fileDTOS = new ArrayList<>(); + if (StringUtils.isNotEmpty(createDto.getFilesWithOa())){ + String filesWithOa = createDto.getFilesWithOa(); + fileDTOS = JSONObject.parseArray(filesWithOa, FtbCultivateOfflineFileDTO.class); + } + // 个人办理不需要验证权限 + if (ObjectUtil.isNotEmpty(createDto.getSource()) && createDto.getSource() != 0 && !UserProvider.getUser().getIsAdministrator()) { + // 权限用户 + List userIds = permissionsUtils.obtainPersonnelUserIdDataPermissions(UserProvider.getUser().getUserId(), PermissionsEnums.PERSONNEL_MANAGEMENT_WEB.getValue(), "Web"); + // 当前人不是本人申请,且权限中有当前可以办理人 + if ((management.getApplicationSource() != 0) && (CollUtil.isNotEmpty(userIds) && !userIds.contains(management.getUserId()))) { + actionResult.setMsg("当前人办理人不具有该员工办理权限!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + } + // 构建数据 + buildYourData(management); + String userId = management.getUserId(); + List userOrgBoundInfo = personnelOrgUtils.getUserOrgBoundInfo(userId); + if (CollUtil.isNotEmpty(userOrgBoundInfo)) management.setOrganizationInfo(JSONObject.toJSONString(userOrgBoundInfo)); + // 离职交接物品 + // oa单独处理只能选一个人 + if (CollUtil.isNotEmpty(createDto.getRecipientOfTheWorkUserId()) ){ + //校验物品人是不是选择了本人 + List recipientOfTheWorkUserIds = createDto.getRecipientOfTheWorkUserId(); + if (userId.equals(recipientOfTheWorkUserIds.get(0)) ){ + actionResult.setMsg("离职交接物品不能选择本人!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + } + if (management.getResignationDate() !=null && management.getDateOfEntry().after(management.getResignationDate())){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg("离职日期不能小于入职日期!"); + return actionResult; + } + if (CollUtil.isNotEmpty(createDto.getRecipientOfTheItemUserId()) ){ + //校验物品人是不是选择了本人 + List recipientOfTheItemUserIds = createDto.getRecipientOfTheItemUserId(); + if (userId.equals(recipientOfTheItemUserIds.get(0))){ + actionResult.setMsg("离职交接物品不能选择本人!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + } + // 校验是否处于流程中 + checkWhetherTheCheckIsInTheProcess(userId); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition,"304"); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getUserId,userId); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark,0); + Long l = baseMapper.selectCount(queryWrapper); + if (l>0) { + actionResult.setMsg("该员工处于待离职,请勿重复操作!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + // 编辑 + FtbPersonnelsTurnoverManagement turnoverManagement = baseMapper.selectById(management.getId()); + if (ObjectUtil.isNotEmpty(turnoverManagement)){ + // 离职编辑 删除之前的信息 + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(FtbPersonnelsTurnoverHandover::getManagementId, management.getId()); + handoverService.remove(wrapper); + // 删除上传的文件重新上传 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbCultivateFile::getBusinessId, management.getId()); + ftbCultivateFileService.remove(updateWrapper); + } + management.setCreatorTime(new Date()); + management.setEnableMark(0L); + // 标识为新审批 + management.setVersionNum(1); + String taskId = createDto.getTaskId(); + management.setTaskInfoId(taskId); + management.setWorkingCondition("304"); + management.setTurnoverStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + saveOrUpdate(management); + String managementId = management.getId(); + // 改为待离职状态 + staffRosterService.innerChangeWaitDepart(userId,taskId,1); + // 修改状态 + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setWorkerStatus("304"); + v2UserApi.updateUserInfo(userId,userDTO); + // 物品接收人 + if (CollUtil.isNotEmpty(createDto.getRecipientOfTheWorkUserId()) || CollUtil.isNotEmpty(createDto.getRecipientOfTheItemUserId())){ + List turnoverHandoverList = new ArrayList<>(); + // 离职工作接收人 + if(CollUtil.isNotEmpty(createDto.getRecipientOfTheWorkUserId())){ + String recipientOfTheWorkInfo = createDto.getRecipientOfTheWorkInfo(); + String recipientOfTheWorkUserId = createDto.getRecipientOfTheWorkUserId().get(0); + if (StringUtils.isNotEmpty(recipientOfTheWorkInfo) && StringUtil.length(recipientOfTheWorkInfo) > 50){ + actionResult.setMsg("离职交接详情不能超过50字!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + FtbPersonnelsTurnoverHandover turnoverHandovers = getFtbPersonnelsTurnoverHandover("2", managementId, + recipientOfTheWorkInfo, recipientOfTheWorkUserId); + turnoverHandoverList.add(turnoverHandovers); + } + // 离职物品接收人 + if(CollUtil.isNotEmpty(createDto.getRecipientOfTheItemUserId()) ){ + String recipientOfTheItemInfo = createDto.getRecipientOfTheItemInfo(); + if (StringUtils.isNotEmpty(recipientOfTheItemInfo) && StringUtil.length(recipientOfTheItemInfo) > 50){ + actionResult.setMsg("离职交接详情不能超过50字!"); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + return actionResult; + } + String recipientOfTheItemUserId = createDto.getRecipientOfTheItemUserId().get(0); + FtbPersonnelsTurnoverHandover turnoverHandovers = getFtbPersonnelsTurnoverHandover("1", managementId, + recipientOfTheItemInfo, recipientOfTheItemUserId); + turnoverHandoverList.add(turnoverHandovers); + } + handoverService.saveBatch(turnoverHandoverList); + } + //通过文件事件监听信息 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(fileDTOS) + .businessTypeID(managementId) + .type(FileEventDTO.FileType.RESIGNATION_MANAGEMENT) + .build())); + return actionResult; + } + + @NotNull + private FtbPersonnelsTurnoverHandover getFtbPersonnelsTurnoverHandover(String number, String managementId, String info, + String userId) { + FtbPersonnelsTurnoverHandover turnoverHandovers = new FtbPersonnelsTurnoverHandover(); + turnoverHandovers.setTurnoverType(number); + turnoverHandovers.setManagementId(managementId); + turnoverHandovers.setHandoverItems(info); + turnoverHandovers.setArticleRecipientId(userId); + UserEntity infoById = userApi.getInfoById(userId); + if (ObjectUtil.isNotEmpty(infoById)) turnoverHandovers.setArticleRecipientName(infoById.getRealName()); + return turnoverHandovers; + } + + private void buildYourData(FtbPersonnelsTurnoverManagement management) { + StaffHomeDto staffHomeDto = staffRosterService.queryWorkerHomeDetail(management.getUserId()); + management.setPhoneNumber(staffHomeDto.getPhone()); + management.setUserName(staffHomeDto.getName()); + management.setJobNumber(staffHomeDto.getWorkerNo()); + management.setDateOfEntry(staffHomeDto.getActualStartDate()); + management.setActualConverDate(staffHomeDto.getActualProbationaryDate()); + management.setOrgId(staffHomeDto.getCurrOrg()); + management.setOrgName(staffHomeDto.getCurrOrgName()); + management.setEntryPostId(staffHomeDto.getCurrPosition()); + management.setEntryPostName(staffHomeDto.getCurrPositionName()); + management.setEntryGradeId(staffHomeDto.getCurrRank()); + management.setEntryGradeName(staffHomeDto.getCurrRankName()); + management.setSystemWokerId(staffHomeDto.getSystemWokerId()); + management.setProbation(staffHomeDto.getProbationPeriod()); + management.setProbationPeriodDay(staffHomeDto.getProbationPeriodDay()); + + + } + + + private void checkWhetherTheCheckIsInTheProcess(String userId) { + // null 不处于 ,1 转正 2,调岗, 3离职 + String s = runTaskService.checkWhetherTheCurrentPersonnelIsInTheReviewProcess(userId); + if (StringUtil.isNotEmpty(s)){ + // 1 转正 2 调岗, 3 离职 + String str=""; + // 1 转正 2 调岗, 3 离职 + switch (s) { + case "1": + str = "转正"; + break; + case "2": + str = "调岗"; + break; + case "3": + str = "离职"; + break; + case "4": + str = "晋升"; + break; + case "5": + str = "调店"; + break; + case "6": + str = "降职"; + break; + case "7": + str = "借调"; + break; + } + throw new RuntimeException("当前人员处于"+str+"审批中,请勿重复进行审批!"); + } + // 添加 + LambdaQueryWrapper queryWrapper4 = Wrappers.lambdaQuery(); + queryWrapper4.eq(FtbPersonnelsSecondmentManagement::getUserId, userId); + queryWrapper4.in(FtbPersonnelsSecondmentManagement::getTransferStatus, 1); + queryWrapper4.eq(FtbPersonnelsSecondmentManagement::getEnableMark,0); + long count1 = secondmentManagementService.count(queryWrapper4); + if (count1 > 0){ + throw new RuntimeException("该员工处于借调审批中,请先完成审批,否则无法办理离职!"); + } + } + @Override + public void jobTrialRejected(FtbPersonnelsJobTrialRejectedCreateDTO createDTO) { + String tenantId = UserProvider.getUser().getTenantId(); + FtbPersonnelsTurnoverCreateDTO turnoverCreateDTO = FtbPersonnelsTurnoverCreateDTO.covertJobRejected(createDTO); + FtbPersonnelsTurnoverManagement management = FtbPersonnelsTurnoverCreateDTO.coverFtbPersonnelsTurnoverManagement(turnoverCreateDTO); + management.setWorkingCondition("305"); + // 组织岗位信息 + if (CollUtil.isNotEmpty(turnoverCreateDTO.getOrganizationInfo())) { + management.setOrganizationInfo(JSONObject.toJSONString(turnoverCreateDTO.getOrganizationInfo())); + } + // 试岗驳回 + management.setApplicationSource(3); + // 离职时间为当前时间 + management.setResignationDate(new Date()); + baseMapper.insert(management); + String userId = management.getUserId(); + UserEntity userEntity = new UserEntity(); + userEntity.setId(userId); + // 关闭用户 + userApi.deleteNoToken(userEntity,tenantId); + // 清楚缓存 + staffRosterService.logout(tenantId,userId); + //清楚考勤组 + GroupUpdateByUserDTO groupUpdateByUserDTO = new GroupUpdateByUserDTO(); + groupUpdateByUserDTO.setTenantId(tenantId); + groupUpdateByUserDTO.setType(2);//1入职 2离职 3调岗 + groupUpdateByUserDTO.setUserIds(List.of(userId)); + personnelOrgUtils.syncAttendanceGroup(groupUpdateByUserDTO); + } + + @Override + public void deleteResignationApplication(List userIds) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsTurnoverManagement::getUserId,userIds); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark,0); + List managementList = baseMapper.selectList(wrapper); + if (CollUtil.isEmpty(managementList)){ + return; + } + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.set(FtbPersonnelsTurnoverManagement::getEnableMark,1); + lambdaed.in(FtbPersonnelsTurnoverManagement::getId,managementList.stream() + .map(SuperBaseEntity.SuperIBaseEntity::getId) + .collect(Collectors.toList())); + baseMapper.update(null,lambdaed); + if (CollUtil.isNotEmpty(managementList) ){ + // 删除审核配置 + managementList.stream().filter(vo->StringUtils.isNotEmpty(vo.getTaskInfoId()) && vo.getVersionNum() == 0) + .forEach(item->runTaskService.deleteReviewProcessAndRecords(item.getTaskInfoId(),item.getId())); + } + + } + + @Override + public void deleteResignationApplicationWithId(List id) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(FtbPersonnelsTurnoverManagement::getId,id); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark,0); + List managementList = baseMapper.selectList(wrapper); + if (CollUtil.isEmpty(managementList)){ + return; + } + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.set(FtbPersonnelsTurnoverManagement::getEnableMark,1); + lambdaed.in(FtbPersonnelsTurnoverManagement::getId,id); + baseMapper.update(null,lambdaed); + if (CollUtil.isNotEmpty(managementList)){ + // 删除审核配置 + managementList.stream().filter(vo-> + StringUtils.isNotEmpty(vo.getTaskInfoId()) && ObjectUtil.isNotEmpty(vo.getVersionNum()) + && vo.getVersionNum() == 0) + .forEach(item->runTaskService.deleteReviewProcessAndRecords(item.getTaskInfoId(),item.getId())); + } + } + + @Override + public Boolean userHasSignedASeparationAgreement(String userId,Integer flag) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverAccountRegistration::getUserId,userId); + wrapper.eq(FtbPersonnelsTurnoverAccountRegistration::getEnableMark,0); + Long aLong = registrationMapper.selectCount(wrapper); + if( aLong == 0 ){ + return true; + } + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(); + // 0-未签署 1-已签署 + //update.set(FtbPersonnelsTurnoverAccountRegistration::getSignAgreement,flag); + update.eq(FtbPersonnelsTurnoverAccountRegistration::getUserId,userId); + return registrationMapper.update(null, update) > 0; + } + + @Override + public ActionResult resignationPreCheck(String userId,String taskFlag,String... whetherItPassesOrNot) { + ActionResult maps = new ActionResult<>(); + // 校验是否处于流程中 + if ("1".equals(taskFlag)) { + // 添加 + LambdaQueryWrapper queryWrapper4 = Wrappers.lambdaQuery(); + queryWrapper4.eq(FtbPersonnelsSecondmentManagement::getUserId, userId); + queryWrapper4.in(FtbPersonnelsSecondmentManagement::getTransferStatus, 1); + queryWrapper4.eq(FtbPersonnelsSecondmentManagement::getEnableMark,0); + long count1 = secondmentManagementService.count(queryWrapper4); + if (count1 > 0){ + throw new RuntimeException("该员工处于借调审批中,请先完成审批,否则无法办理离职!"); + } + checkWhetherTheCheckIsInTheProcess(userId); + } + String s = calibrationNumber(userId); + + LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getUserId, userId); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + long count = staffRosterService.count(queryWrapper1); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId, userId); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 0); + queryWrapper.notIn(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 2); + queryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply one = staffEmploymentApplyService.getOne(queryWrapper); + if (Objects.nonNull(one) && count == 0) { + throw new RuntimeException("当前选择人员还未办理入职,无法提交!"); + } + + // "1" 为是否为审批人 2 是否为门店负责人 3是否存在角色组 4 自属主管 + int checkStatusCodeEnum = 200; + String msg = ""; + if ("4".equals(s)){ + checkStatusCodeEnum = FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode(); + }else if ("1".equals(s) || "3".equals(s) || "2".equals(s)){ + checkStatusCodeEnum = FtbPersonnelsCheckStatusCodeEnum.CHECK_WARNING.getCode(); + } + if (StringUtils.isNotEmpty(s)) { + switch (s) { + case "1": + msg = "该员工当前有待处理的审批单,可在'OA-审批交接'主菜单中办理交接!"; + break; + case "2": + List> userBoundApproval = getUserBoundApproval("2", userId); + String string1= userBoundApproval.stream().map(item -> item.get("storeName").toString()).collect(Collectors.toList()).stream().collect(Collectors.joining(",")); + msg = "该人员为"+string1+"的负责人,离职后该门店将暂无负责人。"; + break; + case "3": + List> mapList = getUserBoundApproval("3", userId); + String string= mapList.stream().map(item -> item.get("roleName").toString()).collect(Collectors.toList()).stream().collect(Collectors.joining(",")); + msg = "该人员存在于"+string+"角色组中,建议对仅1人存在的角色组成员管理调整,可以避免业务断链的情况。"; + break; + case "4": + msg = "该人员有直系下属,请为相关下属重新绑定直属主管,否则无法办理离职。"; + break; + } + } + if (StringUtils.isNotEmpty(msg)) maps.setMsg(msg); + maps.setCode(checkStatusCodeEnum); + // 合同提示 + // 查询人员是否签署电子合同进行弹窗 + // 是否通过 0否,1是 + String flag = whetherItPassesOrNot != null && whetherItPassesOrNot.length > 0 ? whetherItPassesOrNot[0] : null; + if (StringUtils.isEmpty(flag) || "1".equals(flag)){ + ActionResult personnel = contractSignPersonnelNoticeApi.signPersonnel(userId, UserProvider.getUser().getTenantId()); + if (personnel != null && personnel.getCode() == 200){ + ContractSignPersonnelFlagVo data = personnel.getData(); + if (data != null && CollUtil.isNotEmpty(data.getSignType()) ){ + if ( data.getSignType().contains(0)) { + maps.setMsg("该员工已办理电子版劳动合同,办理后需在电脑端电子合同模块作废劳动合同并发起离职证明。"); + maps.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_WARNING.getCode()); + }else if ( ! data.getSignType().contains(0) && data.getSignType().contains(1)){ + maps.setMsg(" 该员工已办理纸质版合同,办理离职后可在电脑端电子合同模块直接发起离职证明。"); + maps.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_WARNING.getCode()); + } + } + } + } + return maps; + } + + + + + /** + * 组装离职数据 + * + * @param management + * @return + */ + @NotNull + private StaffDepartDto getStaffDepartDtos(FtbPersonnelsTurnoverManagement management, int type) { + StaffDepartDto dto = new StaffDepartDto(); + dto.setReasonForLeaving(management.getReasonForResignation()); + // 类型 1办理离职 2 试岗驳回 + dto.setType(type); + dto.setActualStartDate(management.getDateOfEntry()); + dto.setEmployeeID(management.getSystemWokerId()); + List workerGroupDataDtoList = personnelOrgUtils.getUserOrgBoundInfo(management.getUserId()); + if(CollectionUtil.isEmpty(workerGroupDataDtoList)) return dto; + dto.setWorkerGroupDataDto(workerGroupDataDtoList); + return dto; + } + + @Override + public List> getUserBoundApproval(String flag, String userId) { + // "1" 为是否为审批人 2 是否为门店负责人 3是否存在角色组 + List> list = new ArrayList<>(); + switch (flag){ + case "1": + ActionResult reloadTask = flowTaskApi.getReloadTask(userId); + if (reloadTask !=null && reloadTask.getCode() == 200 && ObjectUtil.isNotEmpty(reloadTask.getData())) { + List reloadTaskVOS = JSONObject.parseArray(reloadTask.getData().toString(), ReloadTaskVO.class); + // 处理数据 + return reloadTaskVOS.stream().map(vo -> { + Map hashMap = new HashMap<>(); + // 审批单编号 + String taskCoding = vo.getTaskCoding(); + // 审批单名称 + String taskName = vo.getTaskName(); + hashMap.put("taskCoding", taskCoding); + hashMap.put("taskName", taskName); + return hashMap; + }).collect(Collectors.toList()); + } + break; + case "2": + List storeEntities = storeService.lambdaQuery().eq(StoreEntity::getStoreheaduserid, userId).list(); + List collect1 = storeEntities.stream().map(StoreEntity::getOrganizeid).collect(Collectors.toList()); + Map orgMap = cultivatePerUtils.convertOrganizationalId(collect1); + list = storeEntities.stream().map(item -> { + Map hashMap = Maps.newHashMap(); + OrganizeEntity organize = organizeApi.getInfoById(item.getOrganizeid()); + hashMap.put("orgId",orgMap.get(item.getOrganizeid())); + hashMap.put("orgName",organize.getFullName()); + hashMap.put("storeName",item.getStorename()); + return hashMap; + }).collect(Collectors.toList()); + break; + case "3": + // 获取用户绑定信息 + List userBoundRolesList = roleApi.getUserBoundRolesList(userId); + if (CollUtil.isEmpty(userBoundRolesList)){ + break; + } + list = getMaps(userBoundRolesList); + break; + case "4": + List subordinateUserInfoVOS = userApi.userInfoByLeaderId(userId); + if (CollUtil.isEmpty(subordinateUserInfoVOS)) { + break; + } + List orgIds = subordinateUserInfoVOS.stream().map(PartUserInfoVo::getOrganizeId).collect(Collectors.toList()); + Map map = cultivatePerUtils.convertOrganizationalId(orgIds); + list = subordinateUserInfoVOS.stream().map(item -> { + List collect = item.getPositionMoreBoundVOList().stream().map(PositionMoreBoundVO::getOrganizeName).collect(Collectors.toList()); + Map hashMap = Maps.newHashMap(); + hashMap.put("orgName", String.join(",", collect)); + hashMap.put("orgId",map.get(item.getOrganizeId())); + hashMap.put("userName",item.getRealName()); + return hashMap; + }).collect(Collectors.toList()); + break; + } + return list; + } + + @NotNull + private List> getMaps(List userBoundRolesList) { + List> list; + list = userBoundRolesList.stream().map(item -> { + List relationCount = roleApi.getRolesUserRelationCount(Collections.singletonList(item.getId())); + if (CollUtil.isNotEmpty(relationCount)) { + Map hashMap = Maps.newHashMap(); + hashMap.put("orgName", ""); + hashMap.put("roleName", ""); + hashMap.put("roleNubmer", ""); + hashMap.put("orgId", ""); + List organizeList = item.getOrganizeList(); + if (CollUtil.isNotEmpty(organizeList)){ + List stringList = organizeList.stream().map(OrganizeListVO::getId).collect(Collectors.toList()); + Map orgMap = cultivatePerUtils.convertOrganizationalId(stringList); + String orgName = organizeList.stream().map(vo -> { + String orgId = vo.getId(); + OrganizeEntity infoById = organizeApi.getInfoById(orgId); + return infoById.getFullName(); + }).collect(Collectors.joining(",")); + String sysOrgId = organizeList.stream().map(vo -> orgMap.get(vo.getId())).collect(Collectors.joining(",")); + hashMap.put("orgName", orgName); + hashMap.put("orgId", sysOrgId); + } + RoleCountUserRelationVO roleCountUserRelationVO = relationCount.stream().findFirst().orElse(new RoleCountUserRelationVO()); + hashMap.put("roleName", roleCountUserRelationVO.getFullName()); + hashMap.put("roleNubmer", roleCountUserRelationVO.getUserBoundCount()); + return hashMap; + } + return null; + }).collect(Collectors.toList()); + return list; + } + + @Override + public FtbPersonnelsBubbleCountVO getListCont(String flag) { + // flag "1" 我的审批 "2" 抄送 + FtbPersonnelsBubbleCountVO countVO=new FtbPersonnelsBubbleCountVO(); + FtbPersonnelsForAppQueryDTO appQueryDTO =new FtbPersonnelsForAppQueryDTO(); + appQueryDTO.setUserId(UserProvider.getUser().getUserId()); + if ("1".equals(flag)){ + appQueryDTO.setFlagByApp("2"); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.PENDING.getCode()); + Integer total = getTurnoverForList(appQueryDTO, new CultivatePage()).getPagination().getTotal(); + countVO.setPendingCount(Long.valueOf(total)); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + appQueryDTO.setConfigType(FtbPersonnelsCofigEnum.RESIGNATION_APPROVAL_CONFIGURATION.getConfigType()); + // 审批中 + countVO.setQuantityUnderCount(baseMapper.getApprovedData(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + countVO.setApprovedCount(baseMapper.getApprovedData(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + countVO.setApprovalFailedCount(baseMapper.getApprovedData(new Page<>(), appQueryDTO).getTotal()); + }else if ("2".equals(flag)){ + appQueryDTO.setFlagByApp("3"); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()); + appQueryDTO.init("3",FtbPersonnelsCofigEnum.RESIGNATION_APPROVAL_CONFIGURATION); + countVO.setQuantityUnderCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setStatusList(null); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.getCode()); + countVO.setApprovedCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + appQueryDTO.setApprovalStatus(FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.getCode()); + countVO.setApprovalFailedCount(baseMapper.getListForCC(new Page<>(), appQueryDTO).getTotal()); + } + return countVO; + + } + + + + @Override + public PageListVO getTurnoverList(PersonnelsQueryDTO dto, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + // 校验离职用户是否都已经过期 + // extracted(); + Page managementVOPage = null ; + if (dto.getApprovalStatus() !=null && 8 == dto.getApprovalStatus()){ + dto.setApprovalStatus(7); + } + // 如果有组织筛选且数据权限为管理员 + // 已筛序为准 + String postId = dto.getPostId(); + String gradeId = dto.getGradeId(); + List orgIds = null; + List filteOrgIds = CollUtil.isNotEmpty(dto.getOrgIds()) ? dto.getOrgIds() : null; + if (!UserProvider.getUser().getIsAdministrator() ) { + // 数据权限userIds + String userId = UserProvider.getUser().getUserId(); + orgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userId); + List ids; + PermissionsApplicableObject permissionsApplicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(UserProvider.getUser().getUserId()); + // 数据权限 仅下属单独处理 + if (permissionsApplicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE){ + // 查询当前登录人的下属 + ids = inquireAboutSubordinates(userId); + } else { + ids = doEmployeesOfYourOrganization(orgIds, postId, gradeId); + } + // 只要不是全部且数据为空 + if (!(orgIds == null) && CollUtil.isEmpty(ids)) { + return CultivatePage.coverPageList(page); + } + dto.setIds(ids); + } + if ( filteOrgIds != null){ + if (CollUtil.isNotEmpty(filteOrgIds) && CollUtil.isNotEmpty(orgIds)) { + assert orgIds != null; + orgIds = orgIds.stream().filter(filteOrgIds::contains).collect(Collectors.toList()); + }else if (CollUtil.isNotEmpty(filteOrgIds) && orgIds == null) { + orgIds =filteOrgIds; + } + dto.setIds(doEmployeesOfYourOrganization(orgIds, postId, gradeId)); + if (CollUtil.isEmpty(dto.getIds())) { + return CultivatePage.coverPageList(page); + } + } + managementVOPage = baseMapper.getTurnoverList(page, dto); + List records = managementVOPage.getRecords(); + Map blacklistMap = new HashMap<>(); + List userIds = records.stream().map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsBlacklist::getUserId,userIds); + List list = blacklistService.list(queryWrapper); + blacklistMap = list.stream().collect(Collectors.toMap(FtbPersonnelsBlacklist::getUserId, Function.identity(), (a, b) -> a)); + } + String userId = UserProvider.getUser().getUserId(); + Map finalBlacklistMap = blacklistMap; + replaceTheNameBatch(records); + records.forEach(item->{ + // 计算司龄 + item.setSiLing(calculateTheAgeOfTheDivision(item.getResignationDate(),item.getDateOfEntry(), item.getWorkingCondition())); + // 离职管理--状态为“已手动办理”,如果超过离职日期,操作项去掉撤销按钮 + if (item.getResignationDate() != null && + FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode().equals(dto.getApprovalStatus()) + && item.getResignationDate().before(new Date())){ + item.setRemoveTheUndoButtonLogo("1"); + }else { + item.setRemoveTheUndoButtonLogo("0"); + } + + if (StringUtils.isNotEmpty(item.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(item.getTaskInfoId()); + if (ObjectUtil.isNotEmpty(byTaskId) && byTaskId.getCreatorUserId().equals(userId)) { + item.setCurrentSponsor(1); + } else { + item.setCurrentSponsor(0); + } + } + // 是否纳入黑名单 + item.setWhetherToBeIncluded(finalBlacklistMap.containsKey(item.getUserId()) ? 1 : 0); + }); + return CultivatePage.coverPageList(managementVOPage); + } + + /** + * 查询下属的离职id + * @param userId + * @return + */ + public List inquireAboutSubordinates(String userId){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark ,0); + List managements = baseMapper.selectList(wrapper); + return managements.stream().filter(v -> { + String organizationInfo = v.getOrganizationInfo(); + List dataDto = JSONArray.parseArray(organizationInfo, WorkerGroupDataDto.class); + if (CollUtil.isEmpty(dataDto)) { + return false; + } + long count = dataDto.stream().filter(data -> data.getIsDefault() && userId.equals(data.getReportsTo())).count(); + return count > 0; + }).map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + } + + @Override + public void getTurnoverListExport(PersonnelsQueryDTO dto, HttpServletResponse response) throws IOException { + + // 校验离职用户是否都已经过期 + List listVO = getUsers(dto); + // 转换数字->汉字 + listVO.parallelStream().forEach(a -> { + // 是否纳入黑名单 + if ("305".equals(a.getWorkingCondition())){ + a.setWhetherToBeIncludedName(a.getWhetherToBeIncluded() == 1 ? "是" : "否" ); + }else { + a.setWhetherToBeIncludedName("--"); + } + // 工作状态 + FtbPersonnelsTurnoverManagementVO.changeWorkStatus(a); + // 申请来源 + FtbPersonnelsTurnoverManagementVO.changeApplySource(a); + // 审批状态 + FtbPersonnelsTurnoverManagementVO.changeApprovalStatus(a); + Date resignationDate = a.getResignationDate(); + if (resignationDate != null) { + a.setResignationDateStr(DateUtil.format(resignationDate, "yyyy-MM-dd")); + } + if (a.getDateOfEntry() != null) { + a.setDateOfEntryStr(DateUtil.format(a.getDateOfEntry(), "yyyy-MM-dd")); + } + }); + List userIds = listVO.stream().map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + List phones = listVO.stream().map(FtbPersonnelsTurnoverManagementVO::getPhone).collect(Collectors.toList()); + Map staffRosterMap = new HashMap<>(); + if (CollUtil.isNotEmpty(userIds)) { + // 封装数据 + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.in(FtbPersonnelsStaffRoster::getUserId, userIds); + List personnelsStaffRosters = staffRosterService.list(lambdaQueryWrapper); + if (CollUtil.isNotEmpty(personnelsStaffRosters)) { + staffRosterMap = personnelsStaffRosters.stream().collect(Collectors.toMap(FtbPersonnelsStaffRoster::getUserId, Function.identity())); + } + } + Map> listWithPhone = registrationFormDataService.queryFormFieldValueListWithPhone(phones); + Map finalStaffRosterMap = staffRosterMap; + List rosterDataList = listVO.stream().map(v -> { + FtbPersonnelsStaffRoster staffRoster = new FtbPersonnelsStaffRoster(); + staffRoster.setUserId(v.getUserId()); + staffRoster.setName(v.getUserName()); + if (finalStaffRosterMap.containsKey(v.getUserId())) { + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = finalStaffRosterMap.get(v.getUserId()); + staffRoster.setId(ftbPersonnelsStaffRoster.getId()); + } + List formData = listWithPhone.get(v.getPhone()); + return new ExportRosterOneVo(staffRoster, formData); + }).collect(Collectors.toList()); + List> excelHeaderList = ExcelHeaderUtil.getExcelHeaderList(FtbPersonnelsTurnoverManagementVO.class); + List> lists = rosterExportUtils.extractAnnotatedFieldValues(listVO,FtbPersonnelsTurnoverManagementVO.class); + FtbRosterImportTemplateBO templateBO = new FtbRosterImportTemplateBO(); + templateBO.setHeader(excelHeaderList); + templateBO.setSheetName("Sheet1"); + templateBO.setData(lists); + // 入职所属组织 + //入职所属岗位 + //入职所属职级 + //入职直属主管 + //入职所属门店 + //入职班组 + String[] stringList = new String[]{"入职所属组织", "入职所属岗位", "入职所属职级", "入职直属主管", "入职所属门店", "入职班组"}; + List onboarding = rosterExportUtils.getExportFormTypesWithOnboarding(stringList); + //查询选项 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsRegistrationFormFieldOption::getEnabledMark, 0) + .orderByAsc(FtbPersonnelsRegistrationFormFieldOption::getId); + List optionList = fieldOptionService.list(wrapper); + List templateBOList = rosterExportUtils.convertToFtbRosterImportTemplateBO(onboarding, optionList, rosterDataList); + templateBOList.add(templateBO); + templateBOList.sort(Comparator.comparing(FtbRosterImportTemplateBO::getSheetName)); + EasyExcelUtils.dynamicHeaderGeneration(response, "离职员工信息", templateBOList); + } + public List getUsers(PersonnelsQueryDTO dto){ + List filterList = new ArrayList<>(); + if (dto.getApprovalStatus() !=null && 8 == dto.getApprovalStatus()){ + dto.setApprovalStatus(7); + } + // 如果有组织筛选且数据权限为管理员 + // 已筛序为准 + String postId = dto.getPostId(); + String gradeId = dto.getGradeId(); + List orgIds = null; + List filteOrgIds = CollUtil.isNotEmpty(dto.getOrgIds()) ? dto.getOrgIds() : null; + if (!UserProvider.getUser().getIsAdministrator() ) { + // 数据权限userIds + String userId = UserProvider.getUser().getUserId(); + orgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userId); + List ids; + PermissionsApplicableObject permissionsApplicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(UserProvider.getUser().getUserId()); + // 数据权限 仅下属单独处理 + if (permissionsApplicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE){ + // 查询当前登录人的下属 + ids = inquireAboutSubordinates(userId); + } else { + ids = doEmployeesOfYourOrganization(orgIds, postId, gradeId); + } + // 只要不是全部且数据为空 + if (!(orgIds == null) && CollUtil.isEmpty(ids)) { + return filterList; + } + dto.setIds(ids); + } + if ( filteOrgIds != null){ + if (CollUtil.isNotEmpty(filteOrgIds) && CollUtil.isNotEmpty(orgIds)) { + assert orgIds != null; + orgIds = orgIds.stream().filter(filteOrgIds::contains).collect(Collectors.toList()); + }else if (CollUtil.isNotEmpty(filteOrgIds) && orgIds == null) { + orgIds =filteOrgIds; + } + dto.setIds(doEmployeesOfYourOrganization(orgIds, postId, gradeId)); + if (CollUtil.isEmpty(dto.getIds())) { + return filterList; + } + } + Page page = new Page<>(); + page.setSize(-1); + Page managementVOPage = baseMapper.getTurnoverList(page, dto); + filterList = managementVOPage.getRecords(); + Map blacklistMap = new HashMap<>(); + List userIds = filterList.stream().map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsBlacklist::getUserId,userIds); + List list = blacklistService.list(queryWrapper); + blacklistMap = list.stream().collect(Collectors.toMap(FtbPersonnelsBlacklist::getUserId, Function.identity(), (a, b) -> a)); + } + Map finalBlacklistMap = blacklistMap; + filterList.forEach(item->{ + // 是否纳入黑名单 + item.setWhetherToBeIncluded(finalBlacklistMap.containsKey(item.getUserId()) ? 1 : 0); + }); + replaceTheNameBatch(filterList); + return filterList; + } + + @Override + public FtbPersonnelsTurOrgInfo getUserOrganizeInfo(String userId, String phone) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StringUtils.isNotEmpty(userId), FtbPersonnelsTurnoverManagement::getUserId, userId); + queryWrapper.eq(StringUtils.isNotEmpty(phone), FtbPersonnelsTurnoverManagement::getPhoneNumber, phone); + queryWrapper.orderByAsc(FtbPersonnelsTurnoverManagement::getCreatorTime); + queryWrapper.last("limit 1"); + FtbPersonnelsTurnoverManagement management = baseMapper.selectOne(queryWrapper); + // idCardNum + if (management == null) return null; + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId,"idCardNum"); + wrapper.eq(FtbPersonnelsStaffRegistrationFormData::getPhone, management.getPhoneNumber()); + wrapper.last("limit 1"); + FtbPersonnelsStaffRegistrationFormData formData = dataMapper.selectOne(wrapper); + List dtos = new ArrayList<>(); + if (StringUtils.isNotEmpty(management.getOrganizationInfo())) { + dtos = JSONArray.parseArray(management.getOrganizationInfo(), WorkerGroupDataDto.class); + } + FtbPersonnelsTurOrgInfo ftbPersonnelsTurOrgInfo = new FtbPersonnelsTurOrgInfo(); + ftbPersonnelsTurOrgInfo.setIdCard(formData == null ? "" : formData.getValue()); + ftbPersonnelsTurOrgInfo.setUserId(management.getUserId()); + ftbPersonnelsTurOrgInfo.setUserName(management.getUserName()); + ftbPersonnelsTurOrgInfo.setPhone(management.getPhoneNumber()); + ftbPersonnelsTurOrgInfo.setWorkerGroupDataDto(dtos.stream().filter(v -> v.getIsPrimaryPosition() || v.getIsDefault()).findFirst().orElse(new WorkerGroupDataDto())); + return ftbPersonnelsTurOrgInfo; + } + @Override + public List getInformationAboutTheDepartingPerson(FtbDepUserDTO dto) { + List ids = null; + if (ObjectUtil.isNotEmpty(dto) && (CollUtil.isNotEmpty(dto.getOrganizeIds())|| CollUtil.isNotEmpty(dto.getPositionIds()))) { + ids = getStrings(dto.getOrganizeIds(), dto.getPositionIds(), null); + // 只要不是全部且数据为空 + if (CollUtil.isEmpty(ids))return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(CollUtil.isNotEmpty(ids), SuperBaseEntity.SuperIBaseEntity::getId, ids); + queryWrapper.in(ObjectUtil.isNotEmpty(dto) && CollUtil.isNotEmpty(dto.getUserIds()), + FtbPersonnelsTurnoverManagement::getUserId, dto.getUserIds()); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getWorkingCondition ,"305"); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark ,0); + List turnoverManagements = baseMapper.selectList(queryWrapper); + if(CollUtil.isEmpty(turnoverManagements)) return null; + return turnoverManagements.stream().map(item -> { + UserBoundVO userBoundVO = FtbPersonnelsTurnoverManagement.coverUserBoundVO(item); + List dataDto = JSONArray.parseArray(item.getOrganizationInfo(), WorkerGroupDataDto.class); + if (CollUtil.isEmpty(dataDto)) { + return userBoundVO; + } + WorkerGroupDataDto groupDataDto = dataDto.stream().filter(v -> v.getIsPrimaryPosition() || v.getIsDefault()).findFirst().orElse(new WorkerGroupDataDto()); + userBoundVO.setOrganizeId(groupDataDto.getAffiliatedOrg()); + userBoundVO.setOrganizeName(groupDataDto.getAffiliatedOrgName()); + userBoundVO.setPositionId(groupDataDto.getAffiliatedPosition()); + userBoundVO.setPositionName(groupDataDto.getAffiliatedPositionName()); + userBoundVO.setGradeId(groupDataDto.getAffiliatedRank()); + userBoundVO.setGradeName(groupDataDto.getAffiliatedRankName()); + return userBoundVO; + }).collect(Collectors.toList()); + } + + private List doEmployeesOfYourOrganization(List orgIds, String postId,String grade) { + List postIds = new ArrayList<>(); + List grades = new ArrayList<>(); + if (StringUtils.isNotEmpty(postId)) { + postIds =List.of(postId); + } + if (StringUtils.isNotEmpty(grade)) { + grades =List.of(grade); + } + List managements = getStrings(orgIds, postIds, grades); + if (managements != null) return managements; + return null; + } + + @Nullable + private List getStrings(List orgIds, List postIds, List grades) { + if (CollUtil.isNotEmpty(orgIds)){ + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark ,0); + List managements = baseMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isEmpty(postIds) && CollUtil.isEmpty(grades)) + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty( JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()).stream().filter(orgIds::contains).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getId).collect(Collectors.toList()); + + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isNotEmpty(postIds) && CollUtil.isEmpty(grades)) { + List finalPostIds = postIds; + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo ->CollUtil.isNotEmpty( + JSONObject.parseArray( + vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().filter(po->orgIds.contains(po.getAffiliatedOrg()) && finalPostIds.contains(po.getAffiliatedPosition())).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getId).collect(Collectors.toList()); + } + if (CollUtil.isNotEmpty(orgIds) && CollUtil.isNotEmpty(postIds) && CollUtil.isNotEmpty(grades)) { + List finalPostIds1 = postIds; + List finalGrades = grades; + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo ->CollUtil.isNotEmpty( + JSONObject.parseArray( + vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().filter(po->orgIds.contains(po.getAffiliatedOrg()) + && finalPostIds1.contains(po.getAffiliatedPosition()) + && finalGrades.contains(po.getAffiliatedRank())).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getId).collect(Collectors.toList()); + } + } + return null; + } + private String calculateTheAgeOfTheDivision(Date resignationDate, Date dateOfEntry, String workingCondition) { + // 离职到入职时间 + String calculated ="0天"; + if ("305".equals(workingCondition) && ObjectUtil.isNotEmpty(resignationDate) && ObjectUtil.isNotEmpty(dateOfEntry)){ + // 离职时间 + //入职时间 + calculated = CompanyAgeUtil.calculateSeniority(dateOfEntry,resignationDate); + } else if (ObjectUtil.isNotEmpty(dateOfEntry)){ + // 未填写离职时间 + calculated = CompanyAgeUtil.calculateSeniority(dateOfEntry,new Date()); + } + return calculated; + } + + @Deprecated(since = "优化为批量处理") + private void replaceTheName(FtbPersonnelsTurnoverManagementVO item) { + List workerGroupDataDtos = new ArrayList<>(); + if (StringUtils.isNotEmpty(item.getOrganizationInfo())){ + workerGroupDataDtos = JSONObject.parseArray(item.getOrganizationInfo(), WorkerGroupDataDto.class); + }else { + workerGroupDataDtos = personnelOrgUtils.getUserOrgBoundInfo(item.getUserId()); + } + if (CollUtil.isNotEmpty(workerGroupDataDtos)) { + String affiliatedOrg = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.joining(",")); + String affiliatedOrgName = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedOrgName).collect(Collectors.joining(",")); + String affiliatedPosition = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedPosition).collect(Collectors.joining(",")); + String affiliatedPositionName = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedPositionName).collect(Collectors.joining(",")); + String affiliatedRank = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedRank).collect(Collectors.joining(",")); + String affiliatedRankName = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedRankName).collect(Collectors.joining(",")); + item.setOrgId(affiliatedOrg); + item.setOrgName(affiliatedOrgName); + item.setEntryPostId(affiliatedPosition); + item.setEntryPostName(affiliatedPositionName); + item.setEntryGradeId(affiliatedRank); + item.setEntryGradeName(affiliatedRankName); + } + } + + private void replaceTheNameBatch(List item) { + List userIds = item.stream().map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + Map> orgUtilsUserOrgBoundInfo = new HashMap<>(); + if (CollUtil.isNotEmpty(item)){ + orgUtilsUserOrgBoundInfo = item.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .collect(Collectors.toMap(FtbPersonnelsTurnoverManagementVO::getUserId, vo -> JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class),(a1, a2)-> a1 + )); + }else { + orgUtilsUserOrgBoundInfo = personnelOrgUtils.getUserOrgBoundInfo(userIds); + } + Map> finalOrgUtilsUserOrgBoundInfo = orgUtilsUserOrgBoundInfo; + item.forEach(vo ->{ + if (finalOrgUtilsUserOrgBoundInfo.containsKey(vo.getUserId())) { + List workerGroupDataDtos = finalOrgUtilsUserOrgBoundInfo.get(vo.getUserId()); + String affiliatedOrg = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.joining(",")); + String affiliatedOrgName = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedOrgName).collect(Collectors.joining(",")); + String affiliatedPosition = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedPosition).collect(Collectors.joining(",")); + String affiliatedPositionName = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedPositionName).collect(Collectors.joining(",")); + String affiliatedRank = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedRank).collect(Collectors.joining(",")); + String affiliatedRankName = workerGroupDataDtos.stream().map(WorkerGroupDataDto::getAffiliatedRankName).collect(Collectors.joining(",")); + vo.setOrgId(affiliatedOrg); + vo.setOrgName(affiliatedOrgName); + vo.setEntryPostId(affiliatedPosition); + vo.setEntryPostName(affiliatedPositionName); + vo.setEntryGradeId(affiliatedRank); + vo.setEntryGradeName(affiliatedRankName); + } + }); + + } + + + @Override + public PageListVO getTurnoverForList(FtbPersonnelsForAppQueryDTO dto, CultivatePage cultivatePage) { + Page page = cultivatePage.coverCultivatePage(); + // 1.办理 2.我的审批 3 抄送我的 4 我的申请 + String flagByApp = dto.getFlagByApp(); + // 初始化抄送人 + String userId = UserProvider.getUser().getUserId(); + dto.setUserId(userId); + if ("1".equals(flagByApp)) { + // 数据权限 orgIds + if ( !UserProvider.getUser().getIsAdministrator() ) { + // 数据权限userIds + List orgIds = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userId); + List ids; + PermissionsApplicableObject permissionsApplicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(UserProvider.getUser().getUserId()); + // 数据权限 仅下属单独处理 + if (permissionsApplicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE){ + // 查询当前登录人的下属 + ids = inquireAboutSubordinates(userId); + } else { + ids = doEmployeesOfYourOrganization(orgIds,null,null); + } + // 只要不是全部且数据为空 + if (!(orgIds == null) && CollUtil.isEmpty(ids)) { + return CultivatePage.coverPageList(page); + } + dto.setIds(ids); + } + } + Page mapperList = baseMapper.getListForApp(page, dto); + List records = mapperList.getRecords(); + List userIds = records.stream().map(FtbPersonnelsTurnoverManagementVO::getUserId).collect(Collectors.toList()); + Map blacklistMap = new HashMap<>(); + if (CollUtil.isNotEmpty(userIds)) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsBlacklist::getUserId,userIds); + List list = blacklistService.list(queryWrapper); + blacklistMap = list.stream().collect(Collectors.toMap(FtbPersonnelsBlacklist::getUserId, Function.identity(), (a, b) -> a)); + } + Map finalBlacklistMap = blacklistMap; + mapperList.getRecords().forEach(item->{ + if (StringUtils.isNotEmpty(item.getTaskInfoId())) { + FlowTaskEntity byTaskId = flowTaskApi.findByTaskId(item.getTaskInfoId()); + if (ObjectUtil.isNotEmpty(byTaskId) && userId.equals(byTaskId.getCreatorUserId())) { + item.setCurrentSponsor(1); + } else { + item.setCurrentSponsor(0); + } + } + // 是否纳入黑名单 + item.setWhetherToBeIncluded(finalBlacklistMap.containsKey(item.getUserId()) ? 1 : 0); + }); + return CultivatePage.coverPageList(mapperList); + } + + @NotNull + private List getStrings(Integer configType, String userId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark,0); + List list = new ArrayList<>(); + list.add(0); + list.add(1); + wrapper.in(FtbPersonnelsTurnoverManagement::getTurnoverStatus,list); + List managements = baseMapper.selectList(wrapper); + List ids = new ArrayList<>(); + for (int i = 0; i < managements.size(); i++){ + FtbPersonnelsTurnoverManagement turnoverManagement = managements.get(i); + Boolean b = runTaskService.checkWhetherTheCurrentSignIsOr(turnoverManagement.getId(),configType,userId); + if (b) { + ids.add(turnoverManagement.getUserId()); + } + } + return ids; + } + + @Override + public void closeUserAccountRegularlyAfterResignation(String tenantId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverAccountRegistration::getEnableMark,0); + // 是否已经签署离职解除协议(0 : 否 1:是) + List registrations = registrationMapper.selectList(wrapper); + if (CollUtil.isEmpty(registrations)){ + return; + } + List deleteInfoList = new ArrayList<>(); + List userIdList = new ArrayList<>(); + // 当前时间 + LocalDateTime nowDate = LocalDateTime.now(); + for (FtbPersonnelsTurnoverAccountRegistration registration: registrations){ + Date resignationDate = registration.getResignationDate(); + String userId = registration.getUserId(); + if(ObjectUtil.isEmpty(resignationDate)){ + userIdList.add(userId); + deleteInfoList.add(registration); + continue; + } + // 离职时间 + LocalDateTime localDate = resignationDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + if (nowDate.isAfter(localDate)){ + // 调用关停账号接口 + userIdList.add(userId); + deleteInfoList.add(registration); + } + } + if(CollUtil.isNotEmpty(userIdList)) { + extracted(tenantId, userIdList,deleteInfoList); + } + } + private void extracted(String tenantId, List userIdList, List deleteInfoList) { + Map> listMap = deleteInfoList.stream().collect(Collectors.groupingBy(FtbPersonnelsTurnoverAccountRegistration::getUserId)); + List noDataExists = new ArrayList<>(); + userIdList.forEach(item -> { + List accountRegistrations = listMap.get(item); + FtbPersonnelsTurnoverAccountRegistration registration = accountRegistrations.stream().findFirst().orElse(new FtbPersonnelsTurnoverAccountRegistration()); + if (ObjectUtil.isNotEmpty(registration)) { + FtbPersonnelsTurnoverManagement turnoverManagement = baseMapper.selectById(registration.getBussesId()); + if(ObjectUtil.isEmpty(turnoverManagement)){ + noDataExists.add(registration.getId()); + return; + } + // 工作状态,301、预入职 302、试用 303、正式 304、待离职 305 离职 + StaffDepartDto dtoList = getStaffDepartDtos(turnoverManagement, 1); + Date resignationDate = turnoverManagement.getResignationDate() == null ? turnoverManagement.getEmpResignationDate() : turnoverManagement.getResignationDate(); + staffRosterService.innerChangeDepart(turnoverManagement.getUserId(), dtoList, turnoverManagement.getReasonForResignation(),resignationDate, tenantId); + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + // 修改为离职 + lambdaUpdate.set(FtbPersonnelsTurnoverManagement::getWorkingCondition, "305"); + //lambdaUpdate.set(FtbPersonnelsTurnoverManagement::getEnableMark,"1"); + lambdaUpdate.set(SuperBaseEntity.SuperCUDBaseEntity::getDeleteTime, new Date()); + lambdaUpdate.eq(SuperBaseEntity.SuperIBaseEntity::getId, turnoverManagement.getId()); + baseMapper.update(null, lambdaUpdate); + } + }); + if (CollUtil.isNotEmpty(noDataExists)) registrationMapper.deleteBatchIds(noDataExists); + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsTurnoverAccountRegistration::getEnableMark, 1); + updateWrapper.in(FtbPersonnelsTurnoverAccountRegistration::getUserId, userIdList); + registrationMapper.update(new FtbPersonnelsTurnoverAccountRegistration(), updateWrapper); + //清楚考勤组 + if(CollectionUtil.isNotEmpty(userIdList)) { + GroupUpdateByUserDTO groupUpdateByUserDTO = new GroupUpdateByUserDTO(); + groupUpdateByUserDTO.setTenantId(tenantId); + groupUpdateByUserDTO.setType(2);//1入职 2离职 3调岗 + groupUpdateByUserDTO.setUserIds(userIdList); + personnelOrgUtils.syncAttendanceGroup(groupUpdateByUserDTO); + } + + // 关闭用户 + userIdList.forEach(str->{ + v2UserApi.removeUser(str, UserWorkStatusEnums.RESIGNED); + pTenantAccountApi.deleteUserWithPlatform(str); + }); + // 转正, + regularManagementService.clearHistoricalConversionData(userIdList); + // 晋升, + personnelsPostApplyService.clearPromotionData(userIdList); + // 调岗数据 清除 + transferPositionService.clearJobTransferData(userIdList); + // 调动数据 清楚 + // 调岗数据 + transferManageService.clearTransferData(userIdList); + // 人事权限配置删除 + userIdList.forEach(str-> personnelsPermissionsService.clearUserPermissions(str)); + ftbPermissionRoleAuthorizePersonService.deleteEmployeeAllPermission(userIdList); + // 回退整改任务 + userIdList.forEach(v-> evaluateManageApi.revertTaskStatus(v)); + // 借调结束 + userIdList.forEach(v->{ + FtbPersonnelsTurnoverAccountRegistration registration = listMap.get(v).stream().findFirst().orElse(new FtbPersonnelsTurnoverAccountRegistration()); + secondmentManagementService.endByUserConfirm(v,registration.getResignationDate()); + }); + userIdList.forEach(v->UserProvider.kickoutByUserId(v,tenantId)); + RTopic theCompany_topic = redissonClient.getTopic("leavingTheCompany_topic"); + JSONObject args = new JSONObject(); + args.put("userIdList", userIdList); + args.put("tenantId", tenantId); + theCompany_topic.publish(args.toJSONString()); + // 清楚缓存 + userIdList.forEach(s -> staffRosterService.logout(tenantId,s)); + } + + @Override + public FtbPersonnelsTurnoverInfoVO detailsOfResignationApprovalProcess(String id) { + FtbPersonnelsTurnoverManagement management = baseMapper.selectById(id); + if (ObjectUtil.isEmpty(management)){ + return null; + } + FtbPersonnelsTurnoverInfoVO turnoverInfoVO = FtbPersonnelsTurnoverManagement.coverFtbPersonnelsTurnoverInfoVO(management); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsStaffRoster::getUserId,turnoverInfoVO.getUserId()); + FtbPersonnelsStaffRoster one = staffRosterService.getOne(wrapper); + turnoverInfoVO.setUserName(one.getName()); + if(StringUtils.isNotEmpty(turnoverInfoVO.getBlackTypeId())) turnoverInfoVO.setBlackTypeName(ftbPersonnelsBlacklistTypeMapper.selectById(turnoverInfoVO.getBlackTypeId()).getRegularName()); + turnoverInfoVO.setAuditInfo(subConfigService.queryAuditSubConfig(turnoverInfoVO.getOrgId(),FtbPersonnelsCofigEnum.RESIGNATION_APPROVAL_CONFIGURATION)); + turnoverInfoVO.setWorkingCondition(one.getWorkerStatus()); + // 查询当前申请时上传的文件信息 + List list = ftbCultivateFileService.lambdaQuery().eq(FtbCultivateFile::getBusinessId, id) + .eq(FtbCultivateFile::getType,FileEventDTO.FileType.RESIGNATION_MANAGEMENT.getType()).list(); + List fileVOS = list.stream() + .map(FtbCultivateOfflineFileVO::convertFtbCultivateOfflineFileVO).collect(Collectors.toList()); + turnoverInfoVO.setFiles(fileVOS); + List turnoverHandovers = handoverService.lambdaQuery().eq(FtbPersonnelsTurnoverHandover::getManagementId, id).list(); + Map userMaps =new HashMap<>(); + if (CollectionUtil.isNotEmpty(turnoverHandovers)) { + StaffRosterListReq listReq = new StaffRosterListReq(); + listReq.setIsQueryAuth("0"); + listReq.setPageSize(-1); + listReq.setUserIds(turnoverHandovers.stream().map(FtbPersonnelsTurnoverHandover::getArticleRecipientId).collect(Collectors.toList())); + PageInfo ftbPersonnelsStaffRosterDtoPageInfo = staffRosterService.postWithSalary(listReq); + if (CollUtil.isNotEmpty(ftbPersonnelsStaffRosterDtoPageInfo.getList())) userMaps = ftbPersonnelsStaffRosterDtoPageInfo.getList().stream().collect(Collectors.toMap(FtbPersonnelsStaffRosterDto::getUserId, Function.identity(), (k1, k2) -> k1)); + } + Map> listMap = turnoverHandovers.stream().collect(Collectors.groupingBy(FtbPersonnelsTurnoverHandover::getTurnoverType, Collectors.toList())); + if (CollUtil.isNotEmpty(listMap)) { + //(1,物品,)类型 + if (listMap.containsKey("1")){ + List ftbPersonnelsTurnoverHandovers = listMap.get("1"); + Map finalUserMaps = userMaps; + ftbPersonnelsTurnoverHandovers.forEach(item->{ + if(finalUserMaps.containsKey(item.getArticleRecipientId())){ + item.setArticleRecipientId(finalUserMaps.get(item.getArticleRecipientId()).getUserId()); + item.setSystemWokerId(finalUserMaps.get(item.getArticleRecipientId()).getSystemWokerId()); + item.setWorkerStatus(finalUserMaps.get(item.getArticleRecipientId()).getWorkerStatus()); + } + }); + turnoverInfoVO.setHandover(ftbPersonnelsTurnoverHandovers); + } + //2工作 + if (listMap.containsKey("2")){ + List ftbPersonnelsTurnoverHandovers = listMap.get("2"); + Map finalUserMaps = userMaps; + ftbPersonnelsTurnoverHandovers.forEach(item->{ + if(finalUserMaps.containsKey(item.getArticleRecipientId())){ + item.setArticleRecipientId(finalUserMaps.get(item.getArticleRecipientId()).getUserId()); + item.setSystemWokerId(finalUserMaps.get(item.getArticleRecipientId()).getSystemWokerId()); + item.setWorkerStatus(finalUserMaps.get(item.getArticleRecipientId()).getWorkerStatus()); + } + }); + turnoverInfoVO.setHandoverWork(ftbPersonnelsTurnoverHandovers); + } + } + // 计算司龄 + turnoverInfoVO.setSiLing(calculateTheAgeOfTheDivision(turnoverInfoVO.getResignationDate(),turnoverInfoVO.getDateOfEntry(),turnoverInfoVO.getWorkingCondition())); + Integer applicationSource = management.getApplicationSource(); + String userId = UserProvider.getUser().getUserId(); + if ( 0 == applicationSource ){ + turnoverInfoVO.setMarkFlag("0"); + }else { + turnoverInfoVO.setMarkFlag("1"); + } + if (StringUtils.isNotEmpty(management.getOrganizationInfo())){ + String organizationInfo = management.getOrganizationInfo(); + List dataDtos = JSONObject.parseArray(organizationInfo, WorkerGroupDataDto.class); + turnoverInfoVO.setOrganizationInfo(dataDtos); + } + return turnoverInfoVO; + } + + @Override + @GlobalTransactional + public void withdrawResignationApplication(String id, String taskId) { + if (StringUtils.isEmpty(id) && StringUtils.isEmpty(taskId)){ + throw new RuntimeException("参数错误!"); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StringUtils.isNotEmpty(id),FtbPersonnelsTurnoverManagement::getId,id); + queryWrapper.eq(StringUtils.isNotEmpty(taskId),FtbPersonnelsTurnoverManagement::getTaskInfoId,taskId); + // 撤销 + FtbPersonnelsTurnoverManagement vo = baseMapper.selectOne(queryWrapper); + Integer turnoverStatus = vo.getTurnoverStatus(); + if (!(Objects.equals(turnoverStatus, FtbPersonnelsAuditTaskEnum.UNDER_REVIEW.getCode()) || Objects.equals(turnoverStatus, FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode()))) { + throw new RuntimeException("该审批无法撤销!"); + } + // 撤销离职 + String userId = vo.getUserId(); + String canceldepart = staffRosterService.canceldepart(userId); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsTurnoverManagement::getTurnoverStatus,FtbPersonnelsAuditTaskEnum.CANCEL.getCode()); + if(Objects.equals(turnoverStatus, FtbPersonnelsAuditTaskEnum.MANUAL_CONTROL.getCode())){ + wrapper.set(FtbPersonnelsTurnoverManagement::getTaskInfoId,null); + } + wrapper.set(FtbPersonnelsTurnoverManagement::getWorkingCondition,canceldepart); + wrapper.set(FtbPersonnelsTurnoverManagement::getVersionNum,null); + wrapper.eq(StringUtils.isNotEmpty(id),FtbPersonnelsTurnoverManagement::getId,id); + wrapper.eq(StringUtils.isNotEmpty(taskId),FtbPersonnelsTurnoverManagement::getTaskInfoId,taskId); + baseMapper.update(null,wrapper); + // 删除已经提交的离职申请账号 + LambdaUpdateWrapper lambdaUpdate = Wrappers.lambdaUpdate(); + lambdaUpdate.set(FtbPersonnelsTurnoverAccountRegistration::getEnableMark,1); + lambdaUpdate.eq(FtbPersonnelsTurnoverAccountRegistration::getBussesId,id); + registrationMapper.delete(lambdaUpdate); + if (vo.getIsBeBlackList() == 1) { + // 删除黑名单接口 + LambdaUpdateWrapper lambdaed = Wrappers.lambdaUpdate(); + lambdaed.eq(FtbPersonnelsBlacklist::getUserId, vo.getUserId()); + blacklistService.remove(lambdaed); + } + // 修改状态 + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setWorkerStatus(canceldepart); + v2UserApi.updateUserInfo(userId,userDTO); + } + + @Override + @Transactional + public String reviewResignationApplication(FtbPersonnelsTurnoverDTO auditDto) { + if (ObjectUtil.isNotEmpty(auditDto.getIsBeBlackList()) && auditDto.getIsBeBlackList() == 1){ + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getIsBeBlackList,auditDto.getIsBeBlackList()); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getBlackListPeriod,auditDto.getBlackListPeriod()); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getBlackTypeId,auditDto.getBlackTypeId()); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getBlacklistCause,auditDto.getBlacklistCause()); + updateWrapper.eq(FtbPersonnelsTurnoverManagement::getId,auditDto.getBusinessId()); + baseMapper.update(null,updateWrapper); + } + // 校验 + FtbPersonnelsTurnoverManagement turnoverManagement = baseMapper.selectById(auditDto.getBusinessId()); + String userId = turnoverManagement.getUserId(); + String number = null; + if ( "1".equals(auditDto.getFlag())) number =calibrationNumber(userId); + // 强制申请 或者审核通过 + if (!"1".equals(auditDto.getForceDelete()) && number != null ) return number; + FtbPersonnelsAuditTaskEnum performReview = runTaskService.performReview(auditDto); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsTurnoverManagement::getTurnoverStatus,performReview.getCode()); + if (ObjectUtil.isNotEmpty(auditDto.getResignationDate())) { + wrapper.set(FtbPersonnelsTurnoverManagement::getResignationDate, auditDto.getResignationDate()); + } + // 不足时间关停账号 + if (FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.equals(performReview)) { + //v1.1 加入黑名单 + if (ObjectUtil.isNotEmpty(turnoverManagement.getIsBeBlackList()) && + turnoverManagement.getIsBeBlackList() == 1){ + // 加入黑名单 + FtbPersonnelsBlacklistAddDTO ftbPersonnelsBlacklistAddDTO = + FtbPersonnelsTurnoverCreateDTO.covertBlacklist(turnoverManagement); + blacklistService.addBlacklist(ftbPersonnelsBlacklistAddDTO); + } + // 修改状态 + UserEntity user = new UserEntity(); + user.setId(userId); + user.setEnabledMark(3); + userApi.updateUserStatusById(user); + // 到当前时间了 + removeAccountAndAddDataSynchronizationStatus(turnoverManagement.getId(),turnoverManagement.getResignationDate(), userId,turnoverManagement.getUserName()); + }else if (FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED.equals(performReview)){ + // 审批不通过 + String canceldepart = staffRosterService.canceldepart(userId); + wrapper.set(FtbPersonnelsTurnoverManagement::getWorkingCondition,canceldepart); + } + wrapper.eq(FtbPersonnelsTurnoverManagement::getId,auditDto.getBusinessId()); + baseMapper.update(null,wrapper); + return null; + } + + @Override + @GlobalTransactional + public ActionResult reviewResignationApplicationWithOA(FtbPersonnelsTurnoverDTO auditDto) { + ActionResult actionResult = new ActionResult<>(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg() ); + log.error("人事入转调离审核->{}", JSONObject.toJSONString(auditDto)); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsTurnoverManagement::getTaskInfoId,auditDto.getTaskId()); + if (ObjectUtil.isNotEmpty(auditDto.getIsBeBlackList()) && auditDto.getIsBeBlackList() == 1){ + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getIsBeBlackList,auditDto.getIsBeBlackList()); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getBlackListPeriod,auditDto.getBlackListPeriod()); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getBlackTypeId,auditDto.getBlackTypeId()); + updateWrapper.set(FtbPersonnelsTurnoverManagement::getBlacklistCause,auditDto.getBlacklistCause()); + updateWrapper.eq(FtbPersonnelsTurnoverManagement::getTaskInfoId, auditDto.getTaskId()); + baseMapper.update(null,updateWrapper); + } + // 校验 + FtbPersonnelsTurnoverManagement turnoverManagement = baseMapper.selectOne(queryWrapper); + if (ObjectUtil.isEmpty(turnoverManagement)) throw new RuntimeException("审批数据不存在"); + String managementId = turnoverManagement.getId(); + String userId = turnoverManagement.getUserId(); + // 强制申请 或者审核通过 + // 离职校验 + String flag = auditDto.getFlag(); + if ("1".equals(flag)) actionResult = resignationPreCheck(userId,"", flag); + // 如果是拒绝不能进行离职申请, 警告继续执行 + if (FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode() == actionResult.getCode() ) return actionResult; + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + FtbPersonnelsAuditTaskEnum performReview = "0".equals(flag) ? FtbPersonnelsAuditTaskEnum.AUDIT_NOT_PASSED : FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED; + wrapper.set(FtbPersonnelsTurnoverManagement::getTurnoverStatus,performReview.getCode()); + if (ObjectUtil.isNotEmpty(auditDto.getResignationDate())) { + wrapper.set(FtbPersonnelsTurnoverManagement::getResignationDate, auditDto.getResignationDate()); + } + // 不足时间关停账号 + if (FtbPersonnelsAuditTaskEnum.EXAMINATION_PASSED.equals(performReview)) { + // 补充 自动通过表单没有选择离职时间 使用员工期望离职时间作为离职时间 + //v1.1 加入黑名单 + if (ObjectUtil.isNotEmpty(turnoverManagement.getIsBeBlackList()) && + turnoverManagement.getIsBeBlackList() == 1){ + // 加入黑名单 + FtbPersonnelsBlacklistAddDTO ftbPersonnelsBlacklistAddDTO = + FtbPersonnelsTurnoverCreateDTO.covertBlacklist(turnoverManagement); + blacklistService.addBlacklist(ftbPersonnelsBlacklistAddDTO); + } + // 离职时间 + Date resignationDate = turnoverManagement.getResignationDate(); + if (ObjectUtil.isEmpty(resignationDate)) { + if (ObjectUtil.isNotEmpty(auditDto.getResignationDate())) { + resignationDate = auditDto.getResignationDate(); + } else if (ObjectUtil.isNotEmpty(turnoverManagement.getEmpResignationDate())) { + resignationDate = turnoverManagement.getEmpResignationDate(); + wrapper.set(FtbPersonnelsTurnoverManagement::getResignationDate,resignationDate); + } + } + // 添加同意借调提示 + int record = secondmentManagementService.checkWhetherThereIsASecondmentRecord(userId,resignationDate); + if (record > 0){ + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_WARNING.getCode()); + actionResult.setMsg("该员工离职日期处于借调时间段中,到达离职日期将自动结束借调!"); + } + removeAccountAndAddDataSynchronizationStatus(managementId, resignationDate, userId,turnoverManagement.getUserName()); + }else { + // 审批不通过 + String canceldepart = staffRosterService.canceldepart(userId); + // 修改状态 + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setWorkerStatus(canceldepart); + v2UserApi.updateUserInfo(userId,userDTO); + wrapper.set(FtbPersonnelsTurnoverManagement::getWorkingCondition,canceldepart); + } + wrapper.eq(FtbPersonnelsTurnoverManagement::getId, managementId); + baseMapper.update(null,wrapper); + return actionResult; + } + + @Override + public List queryTurnoverList() { + List ftbPersonnelsTurnoverManagementVOS = baseMapper.queryTurnoverList(null); + ftbPersonnelsTurnoverManagementVOS.forEach(item->{ + if (StringUtils.isNotEmpty(item.getOrganizationInfo())){ + item.setOrganizationList(JSONObject.parseArray(item.getOrganizationInfo(), WorkerGroupDataDto.class)); + } + }); + return ftbPersonnelsTurnoverManagementVOS; + } + + @Override + public List queryTurnoverList(List userIds) { + List ftbPersonnelsTurnoverManagementVOS = baseMapper.queryTurnoverList(userIds); + ftbPersonnelsTurnoverManagementVOS.forEach(item->{ + if (StringUtils.isNotEmpty(item.getOrganizationInfo())){ + item.setOrganizationList(JSONObject.parseArray(item.getOrganizationInfo(), WorkerGroupDataDto.class)); + } + }); + return ftbPersonnelsTurnoverManagementVOS; + } + + + + + /** + *移除账号添加数据同步状态 + */ + private void removeAccountAndAddDataSynchronizationStatus(String businessId, Date resignationDate, String userId,String userName) { + // 避免重复添加 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsTurnoverAccountRegistration::getBussesId,businessId); + queryWrapper.eq(FtbPersonnelsTurnoverAccountRegistration::getUserId,userId); + queryWrapper.eq(FtbPersonnelsTurnoverAccountRegistration::getEnableMark,0); + FtbPersonnelsTurnoverAccountRegistration accountRegistration = registrationMapper.selectOne(queryWrapper); + if (ObjectUtil.isNotEmpty(accountRegistration)) return; + FtbPersonnelsTurnoverAccountRegistration registration = new FtbPersonnelsTurnoverAccountRegistration(); + registration.setUserId(userId); + registration.setBussesId(businessId); + registration.setUserName(userName); + registration.setResignationDate(resignationDate); + registrationMapper.insert(registration); + Date date = new Date(); + if (date.after(resignationDate)){ + extracted(UserProvider.getUser().getTenantId(),List.of(userId),List.of(registration)); + } + } + + + /** + * 校验逾期未办理 + */ + @Deprecated + private void extracted() { + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnelsTurnoverManagement::getTurnoverStatus, FtbPersonnelsAuditTaskEnum.NOT_PROCESSED.getCode()); + List managements = baseMapper.selectList(lambdaQuery); + Date date = new Date(); + managements.forEach(item->{ + if (ObjectUtil.isNotEmpty(item.getResignationDate()) ){ + if (date.after(item.getResignationDate())){ + updateOverdueStatus(item); + } + }else { + if (ObjectUtil.isNotEmpty(item.getEmpResignationDate())) { + if (date.after(item.getEmpResignationDate())) { + updateOverdueStatus(item); + } + } + } + }); + } + + /** + * 更新状态 + * @param item + */ + private void updateOverdueStatus(FtbPersonnelsTurnoverManagement item) { + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.set(FtbPersonnelsTurnoverManagement::getTurnoverStatus,FtbPersonnelsAuditTaskEnum.OVERDUE_FOR_APPROVAL.getCode()); + wrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, item.getId()); + baseMapper.update(new FtbPersonnelsTurnoverManagement(),wrapper); + } + + + @Nullable + private String calibrationNumber(String userId) { + // 离职校验 + // 1.是否为审批人 + ActionResult actionResult = flowTaskApi.checkReloadWait(userId); + if (actionResult !=null && actionResult.getCode() == 200 && (boolean)actionResult.getData()){ + log.error("当前用户为审批人,不能申请离职"); + return "1"; + } + // 2是否为门店负责人 + Long aLong = storeService.lambdaQuery().eq(StoreEntity::getStoreheaduserid, userId).count(); + if (aLong > 0){ + log.error("当前用户为门店负责人,不能申请离职"); + return "2"; + } +// // 直属主管 +// List subordinateUserInfoVOS = userApi.userInfoByLeaderId(userId); +// if (CollUtil.isNotEmpty(subordinateUserInfoVOS)) { +// return "4"; +// } +// List userBoundRolesList = roleApi.getUserBoundRolesList(userId); +// if (CollUtil.isNotEmpty(userBoundRolesList)){ +// List> maps = getMaps(userBoundRolesList); +// log.error("当前用户有角色组,不能申请离职"); +// if (CollUtil.isNotEmpty(maps) || maps.get(0) != null){ +// return "3"; +// } +// } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsUchisuikePondServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsUchisuikePondServiceImpl.java new file mode 100644 index 0000000..01b27d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnelsUchisuikePondServiceImpl.java @@ -0,0 +1,284 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.enums.SqlMethod; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationResponse; +import jnpf.base.ActionResult; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.mapper.FtbCultivateFileMapper; +import jnpf.model.cultivate.CultivatePage; +import jnpf.model.cultivate.event.JnpfApplicationEvent; +import jnpf.model.cultivate.event.dto.file.FileEventDTO; +import jnpf.model.cultivate.po.FtbCultivateFile; +import jnpf.model.personnels.dto.uchisuike.DeleteRecommendedPersonnelDTO; +import jnpf.model.personnels.dto.uchisuike.FtbRecommendationPoolOrgDTO; +import jnpf.model.personnels.dto.uchisuike.FtbinternalRecommendationPoolListDTO; +import jnpf.model.personnels.dto.uchisuike.app.FtbRecommendationInvitationAppDTO; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.po.FtbPersonnelsUchisuikePond; +import jnpf.model.personnels.po.FtbPersonnelsUchisuikePondOrg; +import jnpf.model.personnels.vo.uchisuike.FtbinternalRecommendationPoolVO; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsUchisuikePondMapper; +import jnpf.personnels.mapper.FtbPersonnelsUchisuikePondOrgMapper; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.service.FtbPersonnelsUchisuikePondService; +import jnpf.personnels.utils.PersonnelIdCardVerificationUtils; +import jnpf.util.TenantUtil; +import jnpf.util.context.SpringContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +public class FtbPersonnelsUchisuikePondServiceImpl extends ServiceImpl implements FtbPersonnelsUchisuikePondService { + + @Resource + private FtbCultivateFileMapper ftbCultivateFileMapper; + + @Resource + private TenantUtil tenantUtil; + + @Resource + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Autowired + private V2UserApi v2UserApi; + + @Autowired + private V2OrganizeApi v2OrganizeApi; + + @Resource + private FtbPersonnelsUchisuikePondOrgMapper ftbPersonnelsUchisuikePondOrgMapper; + + @Resource + private FtbPersonnelsStaffEmploymentApplyMapper applyMapper; + @Resource + private PersonnelIdCardVerificationUtils personnelIdCardVerificationUtils; + + @Override + public List viewResume(String id) { + LambdaQueryWrapper ftbCultivateFileLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbCultivateFileLambdaQueryWrapper.eq(FtbCultivateFile::getBusinessId, id); + ftbCultivateFileLambdaQueryWrapper.eq(FtbCultivateFile::getEnabledMark, 0); + ftbCultivateFileLambdaQueryWrapper.eq(FtbCultivateFile::getType, FileEventDTO.FileType.INTERNAL_RECOMMENDATION_POOL_RESUME.getType()); + return ftbCultivateFileMapper.selectList(ftbCultivateFileLambdaQueryWrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteRecommendedPersonnel(DeleteRecommendedPersonnelDTO dto) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, dto.getIds()); + updateWrapper.set(FtbPersonnelsUchisuikePond::getEnableMark, 1); + this.update(new FtbPersonnelsUchisuikePond(), updateWrapper); + } + + @Override + public FtbinternalRecommendationPoolVO internalRecommendationDetails(String id) { + FtbPersonnelsUchisuikePond ftbPersonnelsUchisuikePond = this.baseMapper.selectById(id); + return FtbinternalRecommendationPoolVO.convert(ftbPersonnelsUchisuikePond); + } + + @Override + public PageListVO internalRecommendationPoolListQuery(CultivatePage cultivatePage, FtbinternalRecommendationPoolListDTO ftbinternalRecommendationPoolListDTO) { + PageListVO pageListVO = new PageListVO<>(); + Page page = cultivatePage.coverCultivatePage(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.orderByDesc(SuperBaseEntity.SuperCBaseEntity::getId); + queryWrapper.eq(FtbPersonnelsUchisuikePond::getEnableMark, 0); + // 工作状态 + if (Objects.nonNull(ftbinternalRecommendationPoolListDTO.getWorkState())) { + if (ftbinternalRecommendationPoolListDTO.getWorkState() == 0) { + queryWrapper.eq(FtbPersonnelsUchisuikePond::getState, 0); + } else if (ftbinternalRecommendationPoolListDTO.getWorkState() == 1) { + queryWrapper.gt(FtbPersonnelsUchisuikePond::getState, 0); + } + } + // 来源 + queryWrapper.eq(Objects.nonNull(ftbinternalRecommendationPoolListDTO.getSource()), FtbPersonnelsUchisuikePond::getSource, ftbinternalRecommendationPoolListDTO.getSource()); + // 时间 + queryWrapper.between(Objects.nonNull(ftbinternalRecommendationPoolListDTO.getBeginTime()) && Objects.nonNull(ftbinternalRecommendationPoolListDTO.getEndTime()), + SuperBaseEntity.SuperCBaseEntity::getCreatorTime, ftbinternalRecommendationPoolListDTO.getBeginTime(), ftbinternalRecommendationPoolListDTO.getEndTime()); + // 关键字 + queryWrapper.and(StringUtils.isNotBlank(ftbinternalRecommendationPoolListDTO.getKeyword()), t -> { + t.like(FtbPersonnelsUchisuikePond::getUserName, ftbinternalRecommendationPoolListDTO.getKeyword()); + t.or(); + t.like(FtbPersonnelsUchisuikePond::getPhone, ftbinternalRecommendationPoolListDTO.getKeyword()); + t.or(); + t.like(FtbPersonnelsUchisuikePond::getIdCard, ftbinternalRecommendationPoolListDTO.getKeyword()); + t.or(); + t.like(FtbPersonnelsUchisuikePond::getOrgName, ftbinternalRecommendationPoolListDTO.getKeyword()) + ; + }); + Page ftbPersonnelsUchisuikePondPage = this.baseMapper.selectPage(page, queryWrapper); + // 返回结果 + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(ftbPersonnelsUchisuikePondPage.getCurrent()); + pagination.setPageSize(ftbPersonnelsUchisuikePondPage.getSize()); + long total = ftbPersonnelsUchisuikePondPage.getTotal(); + pagination.setTotal(Math.toIntExact(total)); + pageListVO.setPagination(pagination); + List ftbinternalRecommendationPoolVOS = ftbPersonnelsUchisuikePondPage.getRecords() + .stream().map(FtbinternalRecommendationPoolVO::convert) + .peek(ftbinternalRecommendationPoolVO -> { + ftbinternalRecommendationPoolVO.setPushUserName( + StrUtil.isNotBlank(ftbinternalRecommendationPoolVO.getPushUserId()) ? + v2UserApi.getUsersBound(ftbinternalRecommendationPoolVO.getPushUserId(), null).getData().getUserName() : null + ); + if (ftbinternalRecommendationPoolVO.getState() > 0) { + ftbinternalRecommendationPoolVO.setIsSubmitForm(1); + } else { + // 登记表状态 + LambdaQueryWrapper applyQueryWrapper = Wrappers.lambdaQuery(); + applyQueryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getPhone, ftbinternalRecommendationPoolVO.getPhone()); + applyQueryWrapper.last("limit 1"); + FtbPersonnelsStaffEmploymentApply apply = applyMapper.selectOne(applyQueryWrapper); + if (Objects.nonNull(apply)) { + ftbinternalRecommendationPoolVO.setIsSubmitForm(apply.getIsSubmitForm()); + } else { + LambdaQueryWrapper rosterQueryWrapper = Wrappers.lambdaQuery(); + rosterQueryWrapper.eq(FtbPersonnelsStaffRoster::getPhone, ftbinternalRecommendationPoolVO.getPhone()); + rosterQueryWrapper.last("limit 1"); + rosterQueryWrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + FtbPersonnelsStaffRoster roster = ftbPersonnelsStaffRosterService.getOne(rosterQueryWrapper); + if (Objects.nonNull(roster)) { + ftbinternalRecommendationPoolVO.setIsSubmitForm(1); + } else { + ftbinternalRecommendationPoolVO.setIsSubmitForm(0); + } + } + } + }) + .collect(Collectors.toList()); + pageListVO.setList(ftbinternalRecommendationPoolVOS); + return pageListVO; + } + + @Override + @DSTransactional + public void internalRecommendationInvitationAdded(FtbRecommendationInvitationAppDTO ftbRecommendationInvitationAppDTO) { + tenantUtil.switchTenant(ftbRecommendationInvitationAppDTO.getTenantId()); + // 校验手机号是否重复 + var queryWrapper = this.lambdaQuery() + .eq(FtbPersonnelsUchisuikePond::getPhone, ftbRecommendationInvitationAppDTO.getPhone()) + .eq(FtbPersonnelsUchisuikePond::getEnableMark, 0) + .count(); + if (queryWrapper > 0) { + throw new RuntimeException("此手机号已存在于系统中,请勿重复添加"); + } + // 校验身份证+姓名是否合法 + IdCardVerificationResponse idCardVerificationResponse = personnelIdCardVerificationUtils.idCardVerification(ftbRecommendationInvitationAppDTO.getIdCard(), ftbRecommendationInvitationAppDTO.getUserName(), ftbRecommendationInvitationAppDTO.getTenantId()); + personnelIdCardVerificationUtils.checkIdCardVerification(idCardVerificationResponse); + // 校验身份证号是否存在 + var queryWrapper1 = this.lambdaQuery() + .eq(FtbPersonnelsUchisuikePond::getIdCard, ftbRecommendationInvitationAppDTO.getIdCard()) + .eq(FtbPersonnelsUchisuikePond::getEnableMark, 0) + .count(); + if (queryWrapper1 > 0) { + throw new RuntimeException("此身份证号已存在于系统中,请勿重复添加"); + } + if (ftbPersonnelsStaffRosterService.checkPhoneExistForRoster(ftbRecommendationInvitationAppDTO.getPhone())) { + throw new RuntimeException("此手机号已存在于系统中,请勿重复添加"); + } + FtbPersonnelsUchisuikePond ftbPersonnelsUchisuikePond = ftbRecommendationInvitationAppDTO.convertFtbPersonnelsUchisuikePond(ftbRecommendationInvitationAppDTO); + // 来源0员工内推,1门店/组织内推 + if (ftbRecommendationInvitationAppDTO.getSource() == 1) { + ActionResult organizeGeneralDetailVOActionResult = v2OrganizeApi.organizeInfoByIdNoToken(null, ftbPersonnelsUchisuikePond.getPushOrgId(), ftbRecommendationInvitationAppDTO.getTenantId()); + if (organizeGeneralDetailVOActionResult.getData() != null) { + if (OrganizeCategoryEnums.STORE.equals(organizeGeneralDetailVOActionResult.getData().getOrganizeCategoryEnums())) { + ftbPersonnelsUchisuikePond.setSource(2L); + } else { + ftbPersonnelsUchisuikePond.setSource(3L); + } + } + } else { + ftbPersonnelsUchisuikePond.setSource(1L); + } + this.baseMapper.insert(ftbPersonnelsUchisuikePond); + // 简历附件 + SpringContext.getApplicationContext().publishEvent(new JnpfApplicationEvent<>(FileEventDTO.builder() + .files(ftbRecommendationInvitationAppDTO.getFiles()) + .businessTypeID(ftbPersonnelsUchisuikePond.getId()) + .type(FileEventDTO.FileType.INTERNAL_RECOMMENDATION_POOL_RESUME) + .build())); + } + + @Override + public void updateWorkStatus(String state, String phone) { + if (StrUtil.isBlank(state) || StrUtil.isBlank(phone)) { + return; + } + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnelsUchisuikePond::getPhone, phone); + updateWrapper.set(FtbPersonnelsUchisuikePond::getState, workingStatusCalculation(state)); + this.baseMapper.update(new FtbPersonnelsUchisuikePond(), updateWrapper); + } + + @Override + @Transactional + public void referralPoolOrganizationChange(FtbRecommendationPoolOrgDTO ftbRecommendationPoolOrgDTO) { + List ftbPersonnelsUchisuikePondOrgs = ftbRecommendationPoolOrgDTO.getOrgIds() + .stream() + .map(a -> { + FtbPersonnelsUchisuikePondOrg ftbPersonnelsUchisuikePondOrg = new FtbPersonnelsUchisuikePondOrg(); + ftbPersonnelsUchisuikePondOrg.setPushOrgId(a); + return ftbPersonnelsUchisuikePondOrg; + }).collect(Collectors.toList()); + ftbPersonnelsUchisuikePondOrgMapper.delete(null); + String sqlStatement = SqlHelper.getSqlStatement(FtbPersonnelsUchisuikePondOrgMapper.class, SqlMethod.INSERT_ONE); + SqlHelper.executeBatch(FtbPersonnelsUchisuikePondOrg.class, + this.log, + ftbPersonnelsUchisuikePondOrgs, + ftbPersonnelsUchisuikePondOrgs.size(), + (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)); + } + + @Override + public List internalReferralPoolOrganizationQuery() { + return ftbPersonnelsUchisuikePondOrgMapper.selectList(null) + .stream() + .map(FtbPersonnelsUchisuikePondOrg::getPushOrgId) + .collect(Collectors.toList()); + } + + private Long workingStatusCalculation(String state) { + // 301、预入职 302、试用 303、正式 304、待离职 305 离职 306 试岗 + // 工作状态0未办理入职1预入职2已入职(试用)3已入职(正式)4已入职(待离职)5已入职(已离职) + switch (state) { + case "301": + case "302": + return 1L; + case "306": + return 2L; + case "303": + return 3L; + case "304": + return 4L; + case "305": + return 5L; + default: + return 0L; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoConfigServiceImpl.java new file mode 100644 index 0000000..e9684e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoConfigServiceImpl.java @@ -0,0 +1,131 @@ +package jnpf.personnels.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import jnpf.model.personnels.dto.range.FtbRangeConfigDIYDTO; +import jnpf.model.personnels.dto.range.FtbRangeConfigDTO; +import jnpf.model.personnels.dto.range.FtbRangeDiyQueryDTO; +import jnpf.model.personnels.po.FtbPersonnlesInfoConfig; +import jnpf.model.personnels.po.FtbPersonnlesInfoDiyRangeConfig; +import jnpf.model.personnels.po.FtbPersonnlesInfoRangeConfig; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; +import jnpf.model.personnels.vo.range.FtbRangeConfigVO; +import jnpf.personnels.mapper.FtbPersonnlesInfoConfigMapper; +import jnpf.personnels.mapper.FtbPersonnlesInfoDiyRangeConfigMapper; +import jnpf.personnels.mapper.FtbPersonnlesInfoRangeConfigMapper; +import jnpf.personnels.service.FtbPersonnlesInfoConfigService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +@Service +public class FtbPersonnlesInfoConfigServiceImpl implements FtbPersonnlesInfoConfigService{ + + @Resource + private FtbPersonnlesInfoConfigMapper ftbPersonnlesInfoConfigMapper; + + @Resource + private FtbPersonnlesInfoDiyRangeConfigMapper diyRangeConfigMapper; + @Resource + private FtbPersonnlesInfoRangeConfigMapper rangeConfigMapper; + + @Override + public FtbRangeConfigVO queryInfo(Integer type) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnlesInfoConfig::getConfigType,1); + wrapper.eq(FtbPersonnlesInfoConfig::getType,type); + FtbPersonnlesInfoConfig config = ftbPersonnlesInfoConfigMapper.selectOne(wrapper); + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnlesInfoRangeConfig::getConfigId,config.getId()); + FtbPersonnlesInfoRangeConfig rangeConfig = rangeConfigMapper.selectOne(lambdaQuery); + return FtbRangeConfigVO.covert(rangeConfig); + } + @Override + public List queryDiyInfo(Integer type) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnlesInfoConfig::getConfigType,2); + wrapper.eq(FtbPersonnlesInfoConfig::getType,type); + FtbPersonnlesInfoConfig config = ftbPersonnlesInfoConfigMapper.selectOne(wrapper); + if (config == null){ + throw new RuntimeException("该配置不存在"); + } + LambdaQueryWrapper lambdaQuery = Wrappers.lambdaQuery(); + lambdaQuery.eq(FtbPersonnlesInfoDiyRangeConfig::getConfigId,config.getId()); + List rangeConfigs = diyRangeConfigMapper.selectList(lambdaQuery); + return rangeConfigs.stream().map(FtbRangeConfigDIYVO::covert).collect(Collectors.toList()); + } + + @Override + public Integer queryType(Integer type) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnlesInfoConfig::getType,type); + FtbPersonnlesInfoConfig config = ftbPersonnlesInfoConfigMapper.selectOne(wrapper); + return config.getConfigType(); + } + + @Override + public void updateInfo(FtbRangeConfigDTO dto) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnlesInfoConfig::getType,dto.getType()); + FtbPersonnlesInfoConfig config = ftbPersonnlesInfoConfigMapper.selectOne(wrapper); + //List dtoList = RangeConfigUtil.generateRanges(dto.getFirstGear(), dto.getIntervalValue(), dto.getNumberOfGears()); + FtbPersonnlesInfoRangeConfig entity = FtbPersonnlesInfoRangeConfig.covert(dto); + entity.setPreviewValue(dto.getPreviewValue()); + entity.setConfigId(config.getId()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnlesInfoRangeConfig::getConfigId,config.getId()); + rangeConfigMapper.delete(queryWrapper); + rangeConfigMapper.insert(entity); + changeTheDefaultType(1, config.getId()); + // 清理 自定义数据 + LambdaUpdateWrapper update = Wrappers.lambdaUpdate(); + update.eq(FtbPersonnlesInfoDiyRangeConfig::getConfigId, config.getId()); + diyRangeConfigMapper.delete(update); + } + @Override + public void updateDiyInfo(FtbRangeDiyQueryDTO diyQueryDTO) { + List dto = diyQueryDTO.getDiydtos(); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + String type = diyQueryDTO.getType(); + wrapper.eq(FtbPersonnlesInfoConfig::getType, type); + FtbPersonnlesInfoConfig config = ftbPersonnlesInfoConfigMapper.selectOne(wrapper); + List diyRangeConfig = dto.stream().map(item->{ + FtbPersonnlesInfoDiyRangeConfig covert = FtbPersonnlesInfoDiyRangeConfig.covert(item); + covert.setConfigId(config.getId()); + return covert;}) + .collect(Collectors.toList()); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnlesInfoDiyRangeConfig::getConfigId, config.getId()); + diyRangeConfigMapper.delete(queryWrapper); + Db.saveBatch(diyRangeConfig); + changeTheDefaultType(2,config.getId()); + // 清理 平均数据 + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(FtbPersonnlesInfoRangeConfig::getConfigId,config.getId()); + rangeConfigMapper.delete(updateWrapper); + } + /** + * 更改默认类型 + * @param configType 配置类型 + *@param configId + */ + private void changeTheDefaultType(Integer configType,String configId) { + // 更改默认类型 + FtbPersonnlesInfoConfig infoConfig = new FtbPersonnlesInfoConfig(); + infoConfig.setId(configId); + infoConfig.setConfigType(configType); + ftbPersonnlesInfoConfigMapper.updateById(infoConfig); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoDiyRangeConfigService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoDiyRangeConfigService.java new file mode 100644 index 0000000..b05265b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoDiyRangeConfigService.java @@ -0,0 +1,19 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import jnpf.personnels.mapper.FtbPersonnlesInfoDiyRangeConfigMapper; +/** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +@Service +public class FtbPersonnlesInfoDiyRangeConfigService{ + + @Resource + private FtbPersonnlesInfoDiyRangeConfigMapper ftbPersonnlesInfoDiyRangeConfigMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoRangeConfigServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoRangeConfigServiceImpl.java new file mode 100644 index 0000000..65137e6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbPersonnlesInfoRangeConfigServiceImpl.java @@ -0,0 +1,20 @@ +package jnpf.personnels.service.impl; + +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import jnpf.personnels.mapper.FtbPersonnlesInfoRangeConfigMapper; +import jnpf.personnels.service.FtbPersonnlesInfoRangeConfigService; +/** +* +* +*@Author: peng.hao +*@create: 2025/4/7 +* +*/ +@Service +public class FtbPersonnlesInfoRangeConfigServiceImpl implements FtbPersonnlesInfoRangeConfigService{ + + @Resource + private FtbPersonnlesInfoRangeConfigMapper ftbPersonnlesInfoRangeConfigMapper; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbRewardsPunishmentsApproveOAServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbRewardsPunishmentsApproveOAServiceImpl.java new file mode 100644 index 0000000..8cf3b51 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbRewardsPunishmentsApproveOAServiceImpl.java @@ -0,0 +1,174 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.base.ActionResult; +import jnpf.entity.workflow.PunishmentsApproval; +import jnpf.entity.workflow.PunishmentsApprovalUser; +import jnpf.entity.workflow.RewardApproval; +import jnpf.entity.workflow.RewardApprovalUser; +import jnpf.model.personnels.dto.rewardspunishments.FtbAwardPassedDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbAwardSubmissionDTO; +import jnpf.model.personnels.dto.rewardspunishments.FtbPenaltySubmissionDTO; +import jnpf.model.personnels.po.FtbPersonnelsRewardsPunishments; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsRewardsPunishmentsMapper; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.service.FtbRewardsPunishmentsApproveOAService; +import jnpf.workflow.mapper.PunishmentsApprovalMapper; +import jnpf.workflow.mapper.PunishmentsApprovalUserMapper; +import jnpf.workflow.mapper.RewardApprovalMapper; +import jnpf.workflow.mapper.RewardApprovalUserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +public class FtbRewardsPunishmentsApproveOAServiceImpl implements FtbRewardsPunishmentsApproveOAService { + + @Resource + private RewardApprovalMapper reputationMapper; + @Resource + private RewardApprovalUserMapper reputationUserMapper; + @Resource + private PunishmentsApprovalMapper punishmentsApprovalMapper; + @Resource + private PunishmentsApprovalUserMapper punishmentsApprovalUserMapper; + @Resource + private FtbPersonnelsRewardsPunishmentsMapper ftbPersonnelsRewardsPunishmentsMapper; + @Autowired + private V2UserApi v2UserApi; + @Resource + private FtbPersonnelsStaffRosterService staffRosterService; + @Autowired + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void awardSubmission(FtbAwardSubmissionDTO ftbAwardSubmission) { + validatePunishments(ftbAwardSubmission.getUserDtos()); + RewardApproval rewardApproval = FtbAwardSubmissionDTO.convertRewardApproval(ftbAwardSubmission); + rewardApproval.setStatus(0); + // 类别名称 + LambdaQueryWrapper ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper.select(FtbPersonnelsRewardsPunishments::getRegularName, FtbPersonnelsRewardsPunishments::getIsRecorded); + ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper.eq(FtbPersonnelsRewardsPunishments::getId, ftbAwardSubmission.getType()); + FtbPersonnelsRewardsPunishments ftbPersonnelsRewardsPunishments = ftbPersonnelsRewardsPunishmentsMapper.selectOne(ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper); + rewardApproval.setTypeName(ftbPersonnelsRewardsPunishments.getRegularName()); + rewardApproval.setIsRecorded(ftbPersonnelsRewardsPunishments.getIsRecorded()); + reputationMapper.insert(rewardApproval); + String rewardApprovalId = rewardApproval.getId(); + // 奖励人员 + if (CollUtil.isNotEmpty(ftbAwardSubmission.getUserDtos())) { + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(ftbAwardSubmission.getUserDtos(), null); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + throw new RuntimeException("获取人员信息失败"); + } + Map userIdMap = userPrimaryBoundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v)); + ftbAwardSubmission.getUserDtos().forEach(a -> { + RewardApprovalUser rewardApprovalUser = new RewardApprovalUser(); + rewardApprovalUser.setUserId(a); + rewardApprovalUser.setApprovalId(rewardApprovalId); + // 默认组织、岗位、职等 + UserBoundVO partUserInfoVo = userIdMap.get(a); + if (Objects.nonNull(partUserInfoVo)) { + rewardApprovalUser.setOrgName(partUserInfoVo.getOrganizeName()); + rewardApprovalUser.setPostName(partUserInfoVo.getPositionName()); + rewardApprovalUser.setGradesName(partUserInfoVo.getGradeName()); + } + reputationUserMapper.insert(rewardApprovalUser); + }); + } + + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void awardPassed(FtbAwardPassedDTO ftbAwardPassedDTO) { + LambdaUpdateWrapper updater = Wrappers.lambdaUpdate(); + updater.eq(RewardApproval::getFFlowid, ftbAwardPassedDTO.getTaskId()); + updater.set(RewardApproval::getStatus, ftbAwardPassedDTO.getType()); + reputationMapper.update(new RewardApproval(), updater); + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void penaltySubmission(FtbPenaltySubmissionDTO ftbPenaltySubmissionDTO) { + validatePunishments(ftbPenaltySubmissionDTO.getUserDtos()); + PunishmentsApproval penaltySubmission = FtbPenaltySubmissionDTO.convertPunishmentsApproval(ftbPenaltySubmissionDTO); + penaltySubmission.setStatus(0); + // 类别名称 + LambdaQueryWrapper ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper = Wrappers.lambdaQuery(); + ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper.select(FtbPersonnelsRewardsPunishments::getRegularName, FtbPersonnelsRewardsPunishments::getIsRecorded); + ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper.eq(FtbPersonnelsRewardsPunishments::getId, ftbPenaltySubmissionDTO.getType()); + FtbPersonnelsRewardsPunishments ftbPersonnelsRewardsPunishments = ftbPersonnelsRewardsPunishmentsMapper.selectOne(ftbPersonnelsRewardsPunishmentsLambdaQueryWrapper); + penaltySubmission.setTypeName(ftbPersonnelsRewardsPunishments.getRegularName()); + penaltySubmission.setIsRecorded(ftbPersonnelsRewardsPunishments.getIsRecorded()); + punishmentsApprovalMapper.insert(penaltySubmission); + String punishmentsApprovalId = penaltySubmission.getId(); + if (CollUtil.isNotEmpty(ftbPenaltySubmissionDTO.getUserDtos())) { + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(ftbPenaltySubmissionDTO.getUserDtos(), null); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + throw new RuntimeException("获取人员信息失败"); + } + Map userIdMap = userPrimaryBoundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, v -> v)); + ftbPenaltySubmissionDTO.getUserDtos().forEach(a -> { + PunishmentsApprovalUser punishmentsApprovalUser = new PunishmentsApprovalUser(); + punishmentsApprovalUser.setUserId(a); + punishmentsApprovalUser.setPunishmentsId(punishmentsApprovalId); + // 默认组织、岗位、职等 + UserBoundVO partUserInfoVo = userIdMap.get(a); + if (Objects.nonNull(partUserInfoVo)) { + punishmentsApprovalUser.setOrgName(partUserInfoVo.getOrganizeName()); + punishmentsApprovalUser.setPostName(partUserInfoVo.getPositionName()); + punishmentsApprovalUser.setGradesName(partUserInfoVo.getGradeName()); + } + punishmentsApprovalUserMapper.insert(punishmentsApprovalUser); + }); + } + } + + @Override + @GlobalTransactional(rollbackFor = Exception.class) + public void penaltyPassed(FtbAwardPassedDTO ftbAwardPassedDTO) { + LambdaUpdateWrapper updater = Wrappers.lambdaUpdate(); + updater.eq(PunishmentsApproval::getFFlowid, ftbAwardPassedDTO.getTaskId()); + updater.set(PunishmentsApproval::getStatus, ftbAwardPassedDTO.getType()); + punishmentsApprovalMapper.update(new PunishmentsApproval(), updater); + } + + /** + * 校验当前用户是否办理入职 + * + * @param userIds 用户 ID + */ + private void validatePunishments(List userIds) { + LambdaQueryWrapper queryWrapper1 = Wrappers.lambdaQuery(); + queryWrapper1.in(FtbPersonnelsStaffRoster::getUserId, userIds); + queryWrapper1.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + List staffStaffRosterList = staffRosterService.list(queryWrapper1); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsStaffEmploymentApply::getUserId, userIds); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getIsNeedCheck, 0); + queryWrapper.notIn(FtbPersonnelsStaffEmploymentApply::getCheckStatus, 2); + List one = staffEmploymentApplyService.list(queryWrapper); + if (staffStaffRosterList.size() != userIds.size() && !one.isEmpty()) { + String names = one.stream().map(FtbPersonnelsStaffEmploymentApply::getWorkerName).collect(Collectors.joining(",")); + throw new RuntimeException("当前选择人员" + names + "还未办理入职,无法提交!"); + } + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbThousandFacePersonServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbThousandFacePersonServiceImpl.java new file mode 100644 index 0000000..7d54596 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/service/impl/FtbThousandFacePersonServiceImpl.java @@ -0,0 +1,135 @@ +package jnpf.personnels.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.FtbPersonnelsTurnoverManagement; +import jnpf.model.personnels.vo.roster.FtbThousandFacePersonVO; +import jnpf.personnels.mapper.FtbPersonnelsRegularManagementMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsTurnoverManagementMapper; +import jnpf.personnels.mapper.FtbThousandFacePersonMapper; +import jnpf.personnels.service.FtbThousandFacePersonService; +import jnpf.personnels.utils.PersonnelPerUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class FtbThousandFacePersonServiceImpl implements FtbThousandFacePersonService { + + @Resource + private FtbThousandFacePersonMapper ftbThousandFacePersonMapper; + + @Resource + private FtbPersonnelsTurnoverManagementMapper ftbPersonnelsTurnoverManagementMapper; + + @Resource + private FtbPersonnelsRegularManagementMapper ftbPersonnelsRegularManagementMapper; + + @Resource + private FtbPersonnelsStaffEmploymentApplyMapper ftbPersonnelsStaffEmploymentApplyMapper; + + @Autowired + private PersonnelPerUtils personnelPerUtils; + + // 在职状态 + private static final List ON_THE_JOB_STATUS = List.of("302", "303", "304", "306"); + + @Override + public FtbThousandFacePersonVO getPersonnelData() { + List userIds = selectedOrganizationAndPositionEmployees(); + FtbThousandFacePersonVO ftbThousandFacePersonVO = new FtbThousandFacePersonVO(); + // 花名册在职员工 + Long employeeNum = ftbThousandFacePersonMapper.getEmployeeNum(ON_THE_JOB_STATUS, userIds); + ftbThousandFacePersonVO.setEmployeeNum(employeeNum); + // 调岗员工数量 + Long turnoverNum = ftbThousandFacePersonMapper.getTurnoverAllPeople(userIds); + ftbThousandFacePersonVO.setTurnoverNum(turnoverNum); + //离职 + List stringList = doEmployeesOfYourOrganization(); + Long lastInstallment = ftbPersonnelsTurnoverManagementMapper.getLeaveAllPeople(userIds); + Long leaveNum = ftbPersonnelsTurnoverManagementMapper.getTurnoverRateAndHeadcount(stringList); + ftbThousandFacePersonVO.setLeaveNum(lastInstallment); + ftbThousandFacePersonVO.setLeaveRate(calculateTheScale(lastInstallment, leaveNum)); + //转正 + Long regularNum = ftbPersonnelsRegularManagementMapper.queryNumberOfRegulars(userIds); + ftbThousandFacePersonVO.setRegularNum(regularNum); + //入职 + List orgIds = selectedOrganization(); + Long onboardingNum = ftbPersonnelsStaffEmploymentApplyMapper.queryNumberOfOnboardedEmployees(orgIds); + ftbThousandFacePersonVO.setOnboardingNum(onboardingNum); + return ftbThousandFacePersonVO; + } + + + /** + * 获取人事用户数据权限,返回为空,则表示为超级管理员(可以查询所有数据) + */ + private List selectedOrganizationAndPositionEmployees() { + return personnelPerUtils.obtainPersonnelDataPermissions(); + } + + private List selectedOrganization() { + return personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + } + + /** + * 离职人员数据权限 + * @return + */ + private List doEmployeesOfYourOrganization() { + List orgIds = personnelPerUtils.obtainPersonnelOrganizationIdDataPermissions(); + if (CollUtil.isNotEmpty(orgIds)) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(FtbPersonnelsTurnoverManagement::getEnableMark, 0); + List managements = ftbPersonnelsTurnoverManagementMapper.selectList(wrapper); + if (CollUtil.isNotEmpty(orgIds)) + return managements.stream().filter(vo -> StringUtils.isNotEmpty(vo.getOrganizationInfo())) + .filter(vo -> CollUtil.isNotEmpty(JSONObject.parseArray(vo.getOrganizationInfo(), WorkerGroupDataDto.class) + .stream().map(WorkerGroupDataDto::getAffiliatedOrg).collect(Collectors.toList()).stream().filter(orgIds::contains).collect(Collectors.toList())) + ).map(FtbPersonnelsTurnoverManagement::getId).collect(Collectors.toList()); + } + return null; + } + + /** + * 比率计算 + * @return + */ + private BigDecimal calculateTheScale(Object a, Object b) { + if (a == null || b == null) { + return BigDecimal.ZERO; + } + // a/b 除数为0 + if (b instanceof Integer) { + Integer b0 = (Integer) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof Long) { + Long b0 = (Long) b; + if (b0 == 0) { + return BigDecimal.ZERO; + } + } + if (b instanceof BigDecimal) { + if (((BigDecimal) b).compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + } + BigDecimal a0 = new BigDecimal(String.valueOf(a)); + BigDecimal b0 = new BigDecimal(String.valueOf(b)); + return a0.divide(b0, 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/AuthServerConstant.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/AuthServerConstant.java new file mode 100644 index 0000000..c80d828 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/AuthServerConstant.java @@ -0,0 +1,17 @@ +package jnpf.personnels.utils; + +public class AuthServerConstant { + /** + * 验证码key + */ + public static final String SMS_CODE_CACHE_PREFIX = "sms:code:"; + /** + * 账户注销key + */ + public static final String ACCOUNT_LOGOFF_CACHE_PREFIX = "account:logoff:"; + + /** + * 成功返回码 + */ + public static final String SUCCESS = "Ok"; +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CacheExcelUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CacheExcelUtils.java new file mode 100644 index 0000000..ed599e7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CacheExcelUtils.java @@ -0,0 +1,332 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.entity.StoreEntity; +import jnpf.model.attendance.vo.AttendanceGroupVo; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.ContractTypeApi; +import jnpf.permission.RoleApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.user.QueryUserBoundDTO; +import jnpf.permission.model.role.RoleInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.api.OrganizeAndPositionAndGradeVO; +import jnpf.permission.vo.v2.user.api.OrganizeAndStoreTeamVO; +import jnpf.permission.vo.v2.user.api.OrganizeAndUserVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffEmploymentApplyMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.store.service.StoreService; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Component +@Slf4j +public class CacheExcelUtils { + + @Autowired + private ContractTypeApi contractTypeApi; + + @Autowired + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Autowired + private FtbPersonnelsStaffEmploymentApplyMapper ftbPersonnelsStaffEmploymentApplyMapper; + + @Autowired + private StoreService storeService; + + @Autowired + private RoleApi roleApi; + + @Autowired + private AttendanceGroupApi attendanceGroupApi; + + @Autowired + private PermissionsUtils permissionsUtils; + + @Autowired + private V2UserApi v2UserApi; + + private static final ThreadLocal> a = new ThreadLocal<>(); + + private static final ThreadLocal> b = new ThreadLocal<>(); + + private static final ThreadLocal> c = new ThreadLocal<>(); + + private static final ThreadLocal> d = new ThreadLocal<>(); + + private static final ThreadLocal> e = new ThreadLocal<>(); + + private static final ThreadLocal> g = new ThreadLocal<>(); + + private static final ThreadLocal> h = new ThreadLocal<>(); + + private static final ThreadLocal>> i = new ThreadLocal<>(); + + private static final ThreadLocal> j = new ThreadLocal<>(); + + public void init() { + a.set(new HashMap<>()); + b.set(new HashMap<>()); + c.set(new HashMap<>()); + d.set(new HashMap<>()); + e.set(new HashMap<>()); + g.set(new HashMap<>()); + h.set(new HashMap<>()); + i.set(new HashMap<>()); + j.set(new HashMap<>()); + } + + public void clear() { + a.remove(); + b.remove(); + c.remove(); + d.remove(); + e.remove(); + g.remove(); + h.remove(); + i.remove(); + j.remove(); + } + + public OrganizeAndUserVO checkOrgNameAndUserId(String attributeValue, String userId) { + Map cache = g.get(); + String key = attributeValue + "#" + userId; + if (cache.containsKey(key)) { + return cache.get(key); + } + OrganizeAndUserVO nameAndUserId = v2UserApi.checkOrganizeNameBoundUserName(attributeValue, userId, UserProvider.getUser().getTenantId()); + cache.put(key, nameAndUserId); + return nameAndUserId; + } + + /** + * 获取已存在员工绑定的组织信息 + * + * @param phone + * @return {@link UserBoundVO } + */ + public UserBoundVO doCheckPhoneExist(String phone) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, phone) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .last("limit 1"); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(wrapper); + if (Objects.nonNull(ftbPersonnelsStaffRoster)) { + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(ftbPersonnelsStaffRoster.getUserId()), null); + if (userPrimaryBoundBatch != null && CollUtil.isNotEmpty(userPrimaryBoundBatch.getData())) { + return userPrimaryBoundBatch.getData().get(0); + } + } + return null; + } + + public FtbPersonnelsStaffRoster doCheckPhoneStaffRosterExist(String phone) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsStaffRoster::getPhone, phone) + .eq(FtbPersonnelsStaffRoster::getEnabledMark, 0) + .last("limit 1"); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(wrapper); + if (Objects.nonNull(ftbPersonnelsStaffRoster)) { + return ftbPersonnelsStaffRoster; + } + return null; + } + + /** + * 校验是否在入职管理存在 + */ + public Boolean checkInOnboardingManagement(String phone) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(FtbPersonnelsStaffEmploymentApply::getPhone, phone) + .eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark, 0) + .last("limit 1"); + Long ftbPersonnelsStaffEmploymentApply = ftbPersonnelsStaffEmploymentApplyMapper.selectCount(wrapper); + if (ftbPersonnelsStaffEmploymentApply > 0) { + return true; + } + return false; + } + + /** + * 根据用户Id获取用户信息 + */ + public UserBoundVO doQueryDirectlyUnderTheSupervisor(String userId) { + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), null); + if (userPrimaryBoundBatch != null && CollUtil.isNotEmpty(userPrimaryBoundBatch.getData())) { + return userPrimaryBoundBatch.getData().get(0); + } + return null; + } + + public StoreEntity doStoreEntity(String storename) { + Map cache = e.get(); + if (cache.containsKey(storename)) { + return cache.get(storename); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(StoreEntity::getId); + queryWrapper.eq(StoreEntity::getStorename, storename); + queryWrapper.last("limit 1"); + StoreEntity storeEntity = storeService.getOne(queryWrapper); + cache.put(storename, storeEntity); + return storeEntity; + } + + /** + * 角色校验 + */ + public String doCheckRoleInfoVO(String orgName, String roleName) { + Map cache = d.get(); + String key = orgName + "##" + roleName; + if (cache.containsKey(key)) { + return cache.get(key); + } + String roleIds = ""; + //查询全局角色 + RoleInfoVO roleInfoByGlobalRoleName = roleApi.getRoleInfoByGlobalRoleName(roleName); + if (null != roleInfoByGlobalRoleName) { + roleIds = roleInfoByGlobalRoleName.getId(); + } + //查询和组织相关的角色 + RoleInfoVO roleInfoByRoleName = roleApi.getRoleInfoByRoleNameAndOrgName(roleName, orgName); + if (null != roleInfoByRoleName) { + if (StringUtils.isNotEmpty(roleIds)) { + roleIds = roleIds + "," + roleInfoByRoleName.getId(); + } else { + roleIds = roleInfoByRoleName.getId(); + } + } + cache.put(key, roleIds); + return roleIds; + } + + /** + * 考勤组校验 + */ + public String doCheckAttendanceGroup(String orgName, String attendanceName) { + Map cache = h.get(); + String key = orgName + "##" + attendanceName; + if (cache.containsKey(key)) { + return cache.get(key); + } + String attendanceId = ""; + // 查询和组织相关的考勤组 + ActionResult attendanceGroupByName = attendanceGroupApi.getAttendanceGroupByName(orgName, attendanceName); + if (attendanceGroupByName.getCode() == 200 && Objects.nonNull(attendanceGroupByName.getData())) { + attendanceId = attendanceGroupByName.getData().getId(); + } + cache.put(key, attendanceId); + return attendanceId; + } + + /** + * 所属组织-岗位-职等校验 + */ + public OrganizeAndPositionAndGradeVO doCheckOrgPositionGradesBoundDTO(QueryUserBoundDTO dto) { + Map cache = c.get(); + if (cache.containsKey(dto)) { + return cache.get(dto); + } + OrganizeAndPositionAndGradeVO result = v2UserApi.checkOrganizeNameBoundPositionNameGradeName(dto.getOrganizeName(), + dto.getPositionName(), + dto.getGradeName(), + UserProvider.getUser().getTenantId()); + cache.put(dto, result); + return result; + } + + /** + * 直属主管校验 + * + * @param systemWokerId + * @return + */ + public FtbPersonnelsStaffRoster doVerificationByDirectSupervisor(String systemWokerId) { + Map cache = b.get(); + if (cache.containsKey(systemWokerId)) { + return cache.get(systemWokerId); + } + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.select(FtbPersonnelsStaffRoster::getUserId, FtbPersonnelsStaffRoster::getPhone, FtbPersonnelsStaffRoster::getWorkerStatus); + lambdaQueryWrapper.eq(FtbPersonnelsStaffRoster::getSystemWokerId, systemWokerId); + lambdaQueryWrapper.eq(FtbPersonnelsStaffRoster::getEnabledMark, 0); + lambdaQueryWrapper.last("limit 1"); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(lambdaQueryWrapper); + cache.put(systemWokerId, ftbPersonnelsStaffRoster); + return ftbPersonnelsStaffRoster; + + } + + /** + * 合同校验 + * + * @param contractName + * @return + */ + public String doContractTypeName(String contractName) { + Map cache = a.get(); + if (cache.containsKey(contractName)) { + return cache.get(contractName); + } + String contractTypeName = contractTypeApi.checkContractTypeName(contractName); + cache.put(contractName, contractTypeName); + return contractTypeName; + } + + /** + * 用户组织权限校验 + * + * @return + */ + public List doPersonnelOrganizationIdDataPermissions() { + UserInfo userInfo = UserProvider.getUser(); + Map> cache = i.get(); + if (cache.containsKey(userInfo.getUserId())) { + return cache.get(userInfo.getUserId()); + } + if (userInfo.getIsAdministrator()) { + cache.put(userInfo.getUserId(), null); + return null; + } + List results = permissionsUtils.obtainPersonnelOrganizationIdDataPermissions(userInfo.getUserId()); + cache.put(userInfo.getUserId(), results); + return results; + } + + /** + * 组织班组校验 + * + * @return + */ + public OrganizeAndStoreTeamVO doOrganizeTeamVerification(String orgName, String teamName) { + Map cache = j.get(); + String key = orgName + "##" + teamName; + if (cache.containsKey(key)) { + return cache.get(key); + } + OrganizeAndStoreTeamVO organizeAndStoreTeamVO = v2UserApi.checkOrganizeNameBoundStoreTeamName(orgName, teamName, UserProvider.getUser().getTenantId()); + cache.put(key, organizeAndStoreTeamVO); + return organizeAndStoreTeamVO; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CompanyAgeUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CompanyAgeUtil.java new file mode 100644 index 0000000..1f78d37 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CompanyAgeUtil.java @@ -0,0 +1,248 @@ +package jnpf.personnels.utils; + +import java.time.LocalDate; +import java.time.Period; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +/** + * 司龄计算工具类 + * 用于计算员工司龄并显示为自然年月日格式(示例:1 年 3 个月 15 天) + * + * @author Assistant + * @date 2026/03/06 + */ +public class CompanyAgeUtil { + + /** + * 计算司龄,返回自然年月日格式 + * 格式:X 年 Y 个月 Z 天 + * + * @param startDate 入职日期 + * @param endDate 结束日期(通常为当前日期,为 null 时使用当前日期) + * @return 司龄字符串,格式如:1 年 3 个月 15 天 + */ + public static String calculateServiceAge(Date startDate, Date endDate) { + if (startDate == null) { + return "0 年 0 个月 0 天"; + } + + // 如果结束日期为空,使用当前日期 + if (endDate == null) { + endDate = new Date(); + } + + // 转换为 LocalDate 进行计算 + LocalDate startLocalDate; + LocalDate endLocalDate; + if (startDate instanceof java.sql.Date) { + startLocalDate = ((java.sql.Date) startDate).toLocalDate(); + } else { + startLocalDate = startDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + if (endDate instanceof java.sql.Date) { + endLocalDate = ((java.sql.Date) endDate).toLocalDate(); + } else { + endLocalDate = endDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + + // 确保开始日期早于结束日期 + if (startLocalDate.isAfter(endLocalDate)) { + return "0 年 0 个月 0 天"; + } + + // 计算完整的月份差异 + long monthsBetween = ChronoUnit.MONTHS.between(startLocalDate, endLocalDate); + + // 计算剩余天数 + LocalDate tempDate = startLocalDate.plusMonths(monthsBetween); + long daysBetween = ChronoUnit.DAYS.between(tempDate, endLocalDate); + + // 计算年、月、日 + long years = monthsBetween / 12; + long months = monthsBetween % 12; + long days = daysBetween; + + // 处理边界情况:如果天数接近一个月,调整为月 + if (days >= 30) { + months += days / 30; + days = days % 30; + } + + // 如果月份超过 12,调整为年 + if (months >= 12) { + years += months / 12; + months = months % 12; + } + + return years + "年" + months + "个月" + days + "天"; + } + + /** + * 计算司龄到当前日期 + * + * @param startDate 入职日期 + * @return 司龄字符串,格式如:1 年 3 个月 15 天 + */ + public static String calculateServiceAgeToNow(Date startDate) { + return calculateServiceAge(startDate, null); + } + + /** + * 精简版司龄显示(省略为 0 的单位) + * 例如:1 年 3 个月、3 个月 15 天、1 年、15 天 + * + * @param startDate 入职日期 + * @param endDate 结束日期(为 null 时使用当前日期) + * @return 精简版司龄字符串 + */ + public static String calculateServiceAgeShort(Date startDate, Date endDate) { + String fullAge = calculateServiceAge(startDate, endDate); + + StringBuilder result = new StringBuilder(); + + // 提取年、月、日 + String[] parts = fullAge.split("年"); + long years = Long.parseLong(parts[0]); + + String[] monthParts = parts[1].split("个月"); + long months = Long.parseLong(monthParts[0]); + + String[] dayParts = monthParts[1].split("天"); + long days = Long.parseLong(dayParts[0]); + + if (years > 0) { + result.append(years).append("年"); + } + + if (months > 0) { + result.append(months).append("个月"); + } + + if (days > 0 || result.length() == 0) { + result.append(days).append("天"); + } + + return result.toString(); + } + + /** + * 获取司龄的总天数 + * + * @param startDate 入职日期 + * @param endDate 结束日期(为 null 时使用当前日期) + * @return 总天数 + */ + public static long getTotalDays(Date startDate, Date endDate) { + if (startDate == null) { + return 0; + } + + if (endDate == null) { + endDate = new Date(); + } + + LocalDate startLocalDate; + LocalDate endLocalDate; + if (startDate instanceof java.sql.Date) { + startLocalDate = ((java.sql.Date) startDate).toLocalDate(); + } else { + startLocalDate = startDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + if (endDate instanceof java.sql.Date) { + endLocalDate = ((java.sql.Date) endDate).toLocalDate(); + } else { + endLocalDate = endDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + + return ChronoUnit.DAYS.between(startLocalDate, endLocalDate); + } + + /** + * 获取司龄的总月数(包含零头天数折算) + * + * @param startDate 入职日期 + * @param endDate 结束日期(为 null 时使用当前日期) + * @return 总月数(保留两位小数) + */ + public static double getTotalMonths(Date startDate, Date endDate) { + if (startDate == null) { + return 0.0; + } + + if (endDate == null) { + endDate = new Date(); + } + + LocalDate startLocalDate; + LocalDate endLocalDate; + if (startDate instanceof java.sql.Date) { + startLocalDate = ((java.sql.Date) startDate).toLocalDate(); + } else { + startLocalDate = startDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + if (endDate instanceof java.sql.Date) { + endLocalDate = ((java.sql.Date) endDate).toLocalDate(); + } else { + endLocalDate = endDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + + long months = ChronoUnit.MONTHS.between(startLocalDate, endLocalDate); + LocalDate tempDate = startLocalDate.plusMonths(months); + long days = ChronoUnit.DAYS.between(tempDate, endLocalDate); + + return months + (days / 30.0); + } + + /** + * 计算员工从入职日期到现在的司龄(以年、月、天为单位) + * + * @param startDateD 入职日期 + * @param endDateD 结束日期 + * @return 司龄字符串,格式如 "x年x月x天" + */ + public static String calculateSeniority(Date startDateD,Date endDateD) { + + // 参数校验 + if (startDateD == null || endDateD == null) { + return "0天"; + } + // 处理 java.sql.Date 和 java.util.Date + LocalDate startDate; + LocalDate endDate; + if (startDateD instanceof java.sql.Date) { + startDate = ((java.sql.Date) startDateD).toLocalDate(); + } else { + startDate = startDateD.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } + if (endDateD instanceof java.sql.Date) { + endDate = ((java.sql.Date) endDateD).toLocalDate(); + } else { + endDate = endDateD.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } + // 确保开始日期不晚于当前日期 + if (startDate.isAfter(endDate)) { + return "0天"; + } + + Period period = Period.between(startDate, endDate); + int years = period.getYears(); + int months = period.getMonths(); + int days = period.getDays(); + + StringBuilder sb = new StringBuilder(); + + if (years > 0) { + sb.append(years).append("年"); + } + if (months > 0) { + sb.append(months).append("月"); + } + if (days > 0 || sb.length() == 0) { // 避免返回空字符串,至少保留“0天” + sb.append(days).append("天"); + } + + return sb.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CovertDateUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CovertDateUtils.java new file mode 100644 index 0000000..3ff214f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/CovertDateUtils.java @@ -0,0 +1,58 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.date.DateUtil; + +import java.time.ZoneId; +import java.util.Date; + +/** + * @Author: peng.hao + * @create: 2026/3/18 + */ +public class CovertDateUtils { + + /** + * 将对象转换为 Date 类型(支持 LocalDateTime 和 Date) + * + * @param obj 待转换的对象 + * @return Date 对象,如果转换失败则返回 null + */ + public static Date convertToDate(Object obj) { + if (obj == null) { + return null; + } + + if (obj instanceof Date) { + return (Date) obj; + } else if (obj instanceof java.time.LocalDateTime) { + java.time.LocalDateTime localDateTime = (java.time.LocalDateTime) obj; + Date from = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + // 格式化为 yyyy-MM-dd + String format = DateUtil.format(from, "yyyy-MM-dd"); + return DateUtil.parseDate(format); + } + return null; + } + + /** + * 将对象转换为 String 类型的日期(支持 LocalDateTime 和 Date) + * + * @param obj 待转换的对象 + * @return 格式化的日期字符串(yyyy-MM-dd),如果转换失败则返回 null + */ + public static String convertToStringDate(Object obj) { + if (obj == null) { + return null; + } + + if (obj instanceof Date) { + return DateUtil.format((Date) obj, "yyyy-MM-dd"); + } else if (obj instanceof java.time.LocalDateTime) { + java.time.LocalDateTime localDateTime = (java.time.LocalDateTime) obj; + Date from = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + // 格式化为 yyyy-MM-dd + return DateUtil.format(from, "yyyy-MM-dd"); + } + return null; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/ExcelHeaderUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/ExcelHeaderUtil.java new file mode 100644 index 0000000..b772808 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/ExcelHeaderUtil.java @@ -0,0 +1,45 @@ +package jnpf.personnels.utils; + +import com.alibaba.excel.annotation.ExcelProperty; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +/** + * Excel头部信息工具类 + * + * @author + * @since 2025-01-01 + */ +public class ExcelHeaderUtil { + + + /** + * 获取类中所有带有@ExcelProperty注解的字段名称,组成列表 + * + * @param clazz Excel实体类的Class对象 + * @return 包含@ExcelProperty注解value的列表 + */ + public static List> getExcelHeaderList(Class clazz) { + List> headersList = new ArrayList<>(); + // 获取所有声明的字段,包括私有字段 + Field[] fields = clazz.getDeclaredFields(); + + for (Field field : fields) { + List headers = new ArrayList<>(); + // 检查字段是否带有ExcelProperty注解 + if (field.isAnnotationPresent(ExcelProperty.class)) { + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + // 获取注解中的value值 + String[] values = excelProperty.value(); + if (values.length > 0) { + headers.add(values[0]); // 取第一个值作为标题 + headersList.add(headers); + } + } + } + + return headersList; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/FtbPersonnlesIMUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/FtbPersonnlesIMUtils.java new file mode 100644 index 0000000..a5ab6e6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/FtbPersonnlesIMUtils.java @@ -0,0 +1,156 @@ +package jnpf.personnels.utils; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.*; +import jnpf.model.personnels.po.FtbPersonnelsStaffEmploymentApply; +import jnpf.personnels.service.FtbPersonnelsStaffEmploymentApplyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriUtils; + +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; + +/** + * + */ +@Component +@Slf4j +public class FtbPersonnlesIMUtils { + + @Autowired + private ImRobotApi imRobotApi; + private static final String NOTICE_APP_NAME = "人事管理"; + private static final String APP_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/665ecb69e4b0ae5df114c87a.png"; + private static final String BUTTON_NAME = "去填写"; + private static final String BUTTON_NAME_BANLI = "去办理"; + private static final String BUTTON_LINK = "%s/pages/personnel/archives/verify?tenantId=%s&code=%s&phone=%s&name=%s&idCardNum=%s";//跳转页面链接 + + private static final String BUTTON_LINK_banli = "pages/personnel/personnelManage/index";//跳转页面链接 + private static final String BUTTON_LINK_Page = "pages/webview/webview";//跳转页面链接 + + @Autowired + FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Value("${personnel.ementry.url}") + private String url; + + /** + * 发送消息 + * + * @param tenantId 租户 + * @return + */ + public Boolean sendMsg(String tenantId, String toUserId, String userName) { + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(APP_LOGO);//固定图片 + robotNoticeDataForm.setAppName(NOTICE_APP_NAME); + robotNoticeDataForm.setTitle("邀请填写登记表"); + robotNoticeDataForm.setContent(userName+"邀请你填写登记表"); + JumpUrlListModel jumpUrlListModel = new JumpUrlListModel(); + jumpUrlListModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jumpUrlListModel.setButtonName(BUTTON_NAME); + jumpUrlListModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jumpUrlListModel.setType(1); + String queryCode = staffEmploymentApplyService.queryCode(); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getUserId,toUserId); + queryWrapper.eq(FtbPersonnelsStaffEmploymentApply::getEnabledMark,0); + FtbPersonnelsStaffEmploymentApply serviceOne = staffEmploymentApplyService.getOne(queryWrapper); + String workerName = serviceOne.getWorkerName(); + String encode = UriUtils.encode(workerName, StandardCharsets.UTF_8); + String idCardNum = serviceOne.getIdCardNum(); + if (idCardNum == null) { + idCardNum = ""; + } + String encodeIdCardNum = UriUtils.encode(idCardNum, StandardCharsets.UTF_8); + String format = String.format(BUTTON_LINK,url, tenantId, queryCode, serviceOne.getPhone(), encode, encodeIdCardNum); + jumpUrlListModel.setUrl(format); + MiniAppUrl miniAppUrl = new MiniAppUrl(); + miniAppUrl.setMpPage(BUTTON_LINK_Page); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("url",format); + miniAppUrl.setMpParam(jsonObject.toJSONString()); + miniAppUrl.setMpId("__UNI__1EF82DA"); + jumpUrlListModel.setMiniAppUrl(miniAppUrl); + jumpUrlListModel.setMpId("__UNI__1EF82DA"); + LinkedList jumpUrlListModel1 = new LinkedList<>(); + jumpUrlListModel1.add(jumpUrlListModel); + robotNoticeDataForm.setJumpUrlList(jumpUrlListModel1); + form.setRobotNoticeDataForm(robotNoticeDataForm); + form.setToUserIds(List.of(toUserId)); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + Integer code = actionResult.getCode(); + if (code.equals(200)) { + log.info("send msg success"); + return true; + } else { + log.error("send msg fail"); + return false; + } + } + + + /** + * 发送消息 + * + * @param tenantId 租户 + * @param moduleId + * @return + */ + public Boolean sendMsgWithList(String tenantId, List toUserId, String userName, String moduleId) { + if(toUserId == null){ + return true; + } + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(APP_LOGO);//固定图片 + robotNoticeDataForm.setAppName(NOTICE_APP_NAME); + robotNoticeDataForm.setTitle("登记表填写完成"); + robotNoticeDataForm.setContent(userName+"已完成登记表填写,您可以前往入职管理模块为该员工办理入职"); + JumpUrlListModel jumpUrlListModel = new JumpUrlListModel(); + jumpUrlListModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jumpUrlListModel.setButtonName(BUTTON_NAME_BANLI); + jumpUrlListModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jumpUrlListModel.setType(1); + jumpUrlListModel.setUrl(BUTTON_LINK_banli); + jumpUrlListModel.setMpId("__UNI__1EF82DA"); + + MiniAppUrl miniAppUrl = new MiniAppUrl(); + miniAppUrl.setMpPage(BUTTON_LINK_banli); + miniAppUrl.setMpId("__UNI__1EF82DA"); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("moduleId",moduleId); + miniAppUrl.setMpParam(jsonObject.toJSONString()); + jumpUrlListModel.setMiniAppUrl(miniAppUrl); + + LinkedList jumpUrlListModel1 = new LinkedList<>(); + jumpUrlListModel1.add(jumpUrlListModel); + robotNoticeDataForm.setJumpUrlList(jumpUrlListModel1); + form.setRobotNoticeDataForm(robotNoticeDataForm); + form.setToUserIds(toUserId); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + Integer code = actionResult.getCode(); + if (code.equals(200)) { + log.info("send msg success"); + return true; + } else { + log.error("send msg fail"); + return false; + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/NoSendContactIMUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/NoSendContactIMUtils.java new file mode 100644 index 0000000..d1d1ae3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/NoSendContactIMUtils.java @@ -0,0 +1,104 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 劳动合同未发起消息通知 + */ +@Component +@Slf4j +public class NoSendContactIMUtils { + + @Autowired + private ImRobotApi imRobotApi; + private static final String NOTICE_APP_NAME = "电子合同"; + private static final String NOTICE_MPID = "__UNI__8684C05"; + private static final String NOTICE_APP_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/668ba53ae4b0621053741b0e.png"; + private static final String NOTICE_BUTTON_NAME = "查看详细内容 >"; + private static final String NOTICE_BUTTON_LINK = "/pages/announcement/components/info?id=%s";//跳转页面链接 + + + /** + * 发送消息 + * + * @param tenantId 租户 + * @param msgRosterList 接收人 + * @param toRosterList 接收人 + * @return + */ + public Boolean sendMsg(String tenantId, List msgRosterList, List toRosterList) { + + if (CollUtil.isEmpty(msgRosterList) || CollUtil.isEmpty(toRosterList)) { + return true; + } + List collect = toRosterList.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList()); + + for (FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster : msgRosterList) { + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(NOTICE_APP_LOGO);//固定图片 + robotNoticeDataForm.setAppName(NOTICE_APP_NAME); + + robotNoticeDataForm.setTitle("您有条劳务合同未发起,请登录到后台发起"); + robotNoticeDataForm.setContent(buildNoticeContent(ftbPersonnelsStaffRoster)); + form.setMessageAttributionRobotMpId(NOTICE_MPID); + +// robotNoticeDataForm.setJumpUrlList(buildBtnUrl(roster)); + form.setRobotNoticeDataForm(robotNoticeDataForm); + + form.setToUserIds(collect); + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); +// log.error("send msg ContractSign={},result={}",JSONUtil.toJsonStr(form),JSONUtil.toJsonStr(actionResult)); + if (actionResult == null || !actionResult.getCode().equals(200)) { +// log.error("合同未发起通知消息,send msg req={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + + } + return true; + } + + private String buildNoticeContent(FtbPersonnelsStaffRoster roster) { + StringBuilder sb = new StringBuilder(); + sb.append("姓名:"); + sb.append(roster.getName()); + sb.append("
"); + + sb.append("入职日期:"); + if (null != roster.getActualStartDate()) { + sb.append(DateUtil.format(roster.getActualStartDate(), "yyyy.MM.dd")); + } + return sb.toString(); + + } + + + private LinkedList buildBtnUrl(FtbPersonnelsStaffRoster roster) { + LinkedList btn = new LinkedList<>(); + JumpUrlListModel urlModel = new JumpUrlListModel(); + urlModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + urlModel.setButtonName(NOTICE_BUTTON_NAME); + urlModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + urlModel.setType(1);//1-跳转链接 2-表单提交 + urlModel.setUrl(String.format(NOTICE_BUTTON_LINK, roster.getUserId())); + btn.add(urlModel); + return btn; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonSalaryUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonSalaryUtils.java new file mode 100644 index 0000000..f5cc986 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonSalaryUtils.java @@ -0,0 +1,123 @@ +package jnpf.personnels.utils; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.base.ActionResult; +import jnpf.model.personnels.dto.salary.FtbPersonnelsSalaryInfo; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.salary.UserInfoWithSalary; +import jnpf.permission.V2GradesApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2PositionApi; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsSalaryService; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * 人事薪酬调动类 + * + * @author wangchunxiang + * @date 2025/05/16 + */ +@Component +public class PersonSalaryUtils { + + @Resource + private FtbPersonnelsSalaryService personnelSalaryService; + + @Resource + private FtbPersonnelsStaffRosterMapper ftbPersonnelsStaffRosterMapper; + + @Autowired + private V2OrganizeApi v2OrganizeApi; + + @Autowired + private V2PositionApi v2PositionApi; + + @Autowired + private V2GradesApi v2GradesApi; + + /** + * 同步薪酬数据 + * + * @param salaryData 薪资项数据 + * @param userId 用户ID + * @param currOrg 当前组织id + * @param currPosition 当前岗位id + * @param currRank 当前职级id + * @param date 薪资生效日期 + * @param updateType 更新类型:0 重复入职 1-调薪,2-入职 + * @param reasonsForSalaryAdjustments 备注 + * @param operationType 操作标识 1:转正 0:其他 2: 调岗 3 晋升 4 入职,6调动 + * @param salaryType 调薪方式 1.加薪 2.降薪 + * @param payrollSequenceId 薪资顺序id + * @param tenantId 租户Id + * @param taskId 任务Id + */ + public String syncSalaryData(String salaryData, String userId, String currOrg, String currPosition, String currRank, + String updateType, Date date, String reasonsForSalaryAdjustments, String operationType, Integer salaryType + , String payrollSequenceId, String tenantId, String taskId) { + // 薪酬信息同步 + List salaryItemList = JsonUtil.getJsonToList(salaryData, FtbPersonnelsSalaryInfo.class); + UserInfoWithSalary userInfoWithSalary = new UserInfoWithSalary(); + ActionResult actionResult = v2OrganizeApi.organizeInfoByIdNoToken(null, currOrg, tenantId); + userInfoWithSalary.setFOrgId(currOrg); + userInfoWithSalary.setTaskId(taskId); + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(FtbPersonnelsStaffRoster::getUserId, userId); + query.last("limit 1"); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = ftbPersonnelsStaffRosterMapper.selectOne(query); + if (Objects.nonNull(ftbPersonnelsStaffRoster)) { + userInfoWithSalary.setEntryDate(ftbPersonnelsStaffRoster.getActualStartDate()); + if (Objects.nonNull(ftbPersonnelsStaffRoster.getActualProbationaryDate())) { + userInfoWithSalary.setBeComeDate(ftbPersonnelsStaffRoster.getActualProbationaryDate()); + } else { + userInfoWithSalary.setBeComeDate(ftbPersonnelsStaffRoster.getActualStartDate()); + } + userInfoWithSalary.setUserTypeId(ftbPersonnelsStaffRoster.getWorkerType()); + LambdaQueryWrapper query2 = new LambdaQueryWrapper<>(); + query2.eq(FtbPersonnelsStaffRoster::getUserId, UserProvider.getUser().getUserId()); + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster2 = ftbPersonnelsStaffRosterMapper.selectOne(query2); + // 操作人工号 + if (Objects.nonNull(ftbPersonnelsStaffRoster2)) { + userInfoWithSalary.setOperatorWorkNo(ftbPersonnelsStaffRoster2.getWorkerNo()); + } + } + if (actionResult != null && actionResult.getCode() == 200) { + userInfoWithSalary.setFOrgName(actionResult.getData().getName()); + } + ActionResult infoPosition = v2PositionApi.infoPositionNoToken(currPosition, tenantId); + userInfoWithSalary.setPostId(currPosition); + if (infoPosition != null && infoPosition.getCode() == 200) { + userInfoWithSalary.setPostName(infoPosition.getData().getFullName()); + } + if (StringUtils.isNotEmpty(currRank)) { + ActionResult gradeVOActionResult = v2GradesApi.infoGradeNoToken(currRank, tenantId); + userInfoWithSalary.setRankId(currRank); + if (gradeVOActionResult != null && gradeVOActionResult.getCode() == 200) { + userInfoWithSalary.setRankName(gradeVOActionResult.getData().getFullName()); + } + } + // 生效日期取实际入职日期 + return personnelSalaryService.saveTheChangePayInformation( + salaryItemList, + userId, + date, + userInfoWithSalary, + updateType, + reasonsForSalaryAdjustments, + operationType, 0, salaryType, payrollSequenceId, tenantId); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonalizedTenantWhitelistUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonalizedTenantWhitelistUtils.java new file mode 100644 index 0000000..838f41a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonalizedTenantWhitelistUtils.java @@ -0,0 +1,36 @@ +package jnpf.personnels.utils; + +import jnpf.util.UserProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 个性化租户白名单 + * + * @author wangchunxiang + * @date 2025/04/14 + */ +@Component +public class PersonalizedTenantWhitelistUtils { + + @Value("#{'${individuation.tenantry}'.split(',')}") + private List tenantry; + + /** + * 是否在个性化配置白名单 + */ + public Boolean isItOnTheWhitelist() { + String tenantId = UserProvider.getUser().getTenantId(); + return tenantry.contains(tenantId); + } + + /** + * 获取白名单列表 + */ + public List getTenantry() { + return tenantry; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncImportUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncImportUtils.java new file mode 100644 index 0000000..c80eda3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncImportUtils.java @@ -0,0 +1,245 @@ +package jnpf.personnels.utils; + +import cn.hutool.json.JSONUtil; +import jnpf.base.ActionResult; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.enums.GrowthLogEnum; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.StaffBaseInfoDto; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormField; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.permission.OrganizeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.PositionEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.personnels.service.FtbPersonnelsRegularManagementService; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.service.FtbPersonnelsStaffSalaryChangeLogService; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + +@Component +@Slf4j +public class PersonnelAsyncImportUtils { + + @Autowired + @Lazy + private FtbPersonnelsStaffSalaryChangeLogService salaryChangeLogService; + + @Autowired + @Lazy + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + @Lazy + private FtbPersonnelsStaffGrowthLogService growthLogService; + + @Autowired + @Lazy + private OrganizeApi organizeApi; + + @Autowired + @Lazy + private PositionApi positionApi; + + @Autowired + @Lazy + private UserApi userApi; + + @Autowired + @Lazy + private FtbPersonnelsRegularManagementService regularManagementService; + + @Autowired + @Lazy + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Async + public void asyncUpdateFormData(Map formDataMap, String phone, String rosterId, Map allFormFieldListMap, String tenantCode, Map headers) { + + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + registrationFormDataService.importSyncData(formDataMap, phone, rosterId, allFormFieldListMap); + } catch (Exception e) { + log.error("异步处理组织岗位异常"); + } finally { + FeignHolder.clear(); + } + } + + @Async + public void asyncDealImportRosterOtherData(FtbPersonnelsStaffRoster entity, ProbationPeriodDto probationPeriodDto, String tenantCode, Map headers) { + try { + TenantDataSourceUtil.switchTenant(tenantCode); + //员工成长 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setUserId(entity.getUserId()); + addGrowthLogDto.setChangeDate(new Date()); + if (entity.getJoinNum() > 1) { + addGrowthLogDto.setGrowthType(GrowthLogEnum.REPEAT_JOIN.getCode()); + addGrowthLogDto.setNum(entity.getJoinNum()); + } else { + addGrowthLogDto.setGrowthType(GrowthLogEnum.FIRST_JOIN.getCode()); + } + // 添加导入实际入职日期 + addGrowthLogDto.setDetail(buildGrowthLogDetail(entity.getCurrOrg(), entity.getCurrPosition(), entity.getCurrRank(), entity.getCurrReportsTo(),entity.getActualStartDate())); + addGrowthLogDto.setStartSalary(entity.getEntrySalary()); + addGrowthLogDto.setActualStartDate(entity.getActualStartDate()); + addGrowthLogDto.setEmployeeId(entity.getSystemWokerId()); + // 导入的 + growthLogService.addGrowthLog(addGrowthLogDto); + + //同步状态到内推池子 + personnelOrgUtils.sysWorkerStatusToUchisuike(entity.getPhone(), entity.getWorkerStatus()); + + // 过滤为试用员工 + // 同步转正管理 + // 302、试用 + if (("302".equals(entity.getWorkerStatus()) + || "306".equals(entity.getWorkerStatus())) && !probationPeriodDto.getType().equals("100")) { + syncRegular(entity, probationPeriodDto.getType(), probationPeriodDto.getDays(), entity.getCurrReportsTo()); + } + FeignHolder.sendFeign(headers, () -> { + personnelOrgUtils.updateBaseInfoForBaseUser(entity); + return null; + }); + } catch (Exception e) { + e.printStackTrace(); + log.error("异步导入花名册异常"); + } finally { + FeignHolder.clear(); + } + } + + private String buildGrowthLogDetail(String currOrg, String currPosition, String currRank, String currReportsTo, Date actualStartDate) { + StaffBaseInfoDto dto = new StaffBaseInfoDto(); + dto.setCurrOrg(currOrg); + dto.setCurrPosition(currPosition); + dto.setCurrRank(currRank); + dto.setReportsTo(currReportsTo); + personnelOrgUtils.fillOrgNameById(dto); + personnelOrgUtils.fillPositionNameById(dto); + personnelOrgUtils.fillRankNameById(dto); + UserEntity userEntity = personnelOrgUtils.queryUserInfo(currReportsTo); + if (null != userEntity) { + dto.setReportsToName(userEntity.getRealName()); + } + dto.setActualStartDate(actualStartDate); + return JSONUtil.toJsonStr(dto); + } + + /** + * 同步转正管理 + * + * @param entity + * @param probationPeriod + * @param probationPeriodDay + * @param currReportsTo + */ + private void syncRegular(FtbPersonnelsStaffRoster entity, + String probationPeriod, + String probationPeriodDay, + String currReportsTo) { + try { + FtbPersonnelsRegularCreateDTO createDTO = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularInfoVO(entity); + OrganizeEntity infoById = organizeApi.getInfoById(createDTO.getOrgId()); + createDTO.setOrgName(infoById.getFullName()); + UserEntity userEntity = StringUtils.isEmpty(currReportsTo) ? null : userApi.getInfoById(currReportsTo); + if (userEntity != null) { + createDTO.setImmediateSuperName(userEntity.getRealName()); + createDTO.setImmediateSuperId(currReportsTo); + } + PositionEntity positionEntity = positionApi.queryInfoById(createDTO.getRegularPostId()); + createDTO.setRegularPostName(positionEntity.getFullName()); + createDTO.setOnboardPostName(positionEntity.getFullName()); + ActionResult gradesInfo = positionApi.getGradesInfo(createDTO.getRegularGradeId()); + if (gradesInfo != null && gradesInfo.getData() != null) { + createDTO.setRegularGradeName(gradesInfo.getData().getFullName()); + createDTO.setOnboardGradeName(gradesInfo.getData().getFullName()); + } + calculatePlannedConversionTime(probationPeriod, probationPeriodDay, entity, createDTO); + createDTO.setId(entity.getId()); + regularManagementService.applyForRegularization(createDTO); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 计算计划转正时间 + * + * @param probationPeriod 是否是天 + * @param probationPeriodDay 时间 + * @param entity + * @param createDTO + */ + private void calculatePlannedConversionTime(String probationPeriod, + String probationPeriodDay, + FtbPersonnelsStaffRoster entity, + FtbPersonnelsRegularCreateDTO createDTO) { + // 入职日期 + Date actualStartDate = entity.getActualStartDate(); + int probation = 0; + // 创建一个 Calendar 实例并设置为当前日期 + Calendar calendar = Calendar.getInstance(); + calendar.setTime(actualStartDate); + if ("101".equals(probationPeriod)) { + // 这里就是天数 + createDTO.setProbationPeriodDay(probationPeriodDay); + calendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(probationPeriodDay)); + } else { + switch (probationPeriod) { + case "102": + probation = 1; + break; + case "103": + probation = 2; + break; + case "104": + probation = 3; + break; + case "105": + probation = 4; + break; + case "106": + probation = 5; + break; + case "107": + probation = 6; + break; + case "108": + probation = 7; + break; + case "109": + probation = 8; + break; + } + calendar.add(Calendar.MONTH, probation); + } + Date time = calendar.getTime(); + createDTO.setProbation(probationPeriod); + // 计算实际转正时间 + createDTO.setSchedConverDate(time); + } + +} + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncOldData.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncOldData.java new file mode 100644 index 0000000..170c791 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncOldData.java @@ -0,0 +1,61 @@ +package jnpf.personnels.utils; + +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.personnels.dto.staff.roster.FtbPersonnelsStaffRosterDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class PersonnelAsyncOldData { + + @Autowired + @Lazy + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + @Lazy + private FtbPersonnelsStaffRosterService rosterService; + + @Async + public void dealOldData(FtbPersonnelsStaffRosterDto record, String tenantCode, Map headers) { + try { +// FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + FtbPersonnelsStaffRoster roster = new FtbPersonnelsStaffRoster(); + roster.setId(record.getId()); + roster.setCurrOrg(record.getCurrOrg()); + roster.setCurrPosition(record.getCurrPosition()); + roster.setCurrRank(record.getCurrRank()); + roster.setCurrShopId(record.getCurrShopId()); + roster.setCurrReportsTo(record.getCurrReportsTo()); + roster.setCurrGroupId(record.getCurrGroupId()); + rosterService.updateById(roster); + Map formDataMap = new HashMap<>(); + formDataMap.put("currOrg", record.getCurrOrg()); + formDataMap.put("currPosition", record.getCurrPosition()); + formDataMap.put("currRank", record.getCurrRank()); + formDataMap.put("currReportsTo", record.getCurrReportsTo()); + formDataMap.put("currShopId", record.getCurrShopId()); + formDataMap.put("onboardingTeam", record.getCurrGroupId()); + registrationFormDataService.syncData(formDataMap, record.getPhone(), record.getId()); + } catch (Exception e) { + log.error("异步处理组织岗位异常"); + } finally { + FeignHolder.clear(); + } + } +} + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncServiceUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncServiceUtils.java new file mode 100644 index 0000000..d07f2f8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelAsyncServiceUtils.java @@ -0,0 +1,242 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import jnpf.account.PTenantAccountApi; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.im.UserAndLinkDTO; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.roster.UserAccountDto; +import jnpf.model.user.GenerateHeadFileForm; +import jnpf.model.user.GenerateHeadForm; +import jnpf.permission.UserApi; +import jnpf.permission.dto.SaveUserManagerAddDTO; +import jnpf.personnels.service.*; +import jnpf.util.UserProvider; +import jnpf.util.im.ImMessageNoticeUtils; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class PersonnelAsyncServiceUtils { + + @Value("${invite-url}") + private String joinUrl; + + @Autowired + @Lazy + private FtbPersonnelsTurnoverManagementService ftbPersonnelsTurnoverManagementService; + + @Autowired + @Lazy + private FtbPersonnelsStaffArchivesHistoryService ftbPersonnelsStaffArchivesHistoryService; + + @Autowired + @Lazy + private PTenantAccountApi pTenantAccountApi; + + @Autowired + @Lazy + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + @Lazy + private ImMessageNoticeUtils noticeUtils; + + + @Autowired + @Lazy + private UserApi userApi; + + @Autowired + @Lazy + private FtbPersonnelsPermissionsService personnelsPermissionsService; + + @Autowired + @Lazy + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Autowired + @Lazy + private FtbPersonnelsStaffEmploymentApplyService staffEmploymentApplyService; + + @Autowired + private CultivateLearnUtils cultivateLearnUtils; + + @Autowired + FtbPersonnelsRegularManagementService regularManagementService; + + + @Autowired + FtbPersonnelsTransferManageService transferManageService; + + @Autowired + FtbPersonnelsSecondmentManagementService secondmentManagementService; + + + /** + * 异步删除用户历史档案信息 + * + * @param userIds + * @param tenantCode + * @param headers + */ + + @Async + public void deleteTurnoverUserHistory(List userIds, String tenantCode, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + ftbPersonnelsTurnoverManagementService.deleteResignationApplication(userIds); + ftbPersonnelsStaffArchivesHistoryService.deleteForUserIds(userIds); + } catch (Exception e) { + e.printStackTrace(); + log.error("deleteTurnoverUserHistory 异常"); + } finally { + FeignHolder.clear(); + } + } + + /** + * 异步删除用户历史档案信息 + * + * @param userIds + * @param tenantCode + */ + + @Async + public void asyncAddNewPersonToTask(List userIds, String tenantCode) { + try { + TenantDataSourceUtil.switchTenant(tenantCode); + cultivateLearnUtils.addNewPersonToTask(userIds,tenantCode); + } catch (Exception e) { + e.printStackTrace(); + log.error("asyncAddNewPersonToTask 异常"); + } finally { + } + } + + + @Async + public void batchGeneralHead(List headLogoList, String tenantCode, Map headers) { + try { + log.error("批量生成头像={}", JSONUtil.toJsonStr(headLogoList)); + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + GenerateHeadForm generateHeadForm = new GenerateHeadForm(); + generateHeadForm.setFileFormList(headLogoList); + long start = System.currentTimeMillis(); + pTenantAccountApi.generateDefaultAvatar(generateHeadForm); + long end = System.currentTimeMillis(); + log.error("批量生成头像调用PTenantAccountApi.generateDefaultAvatar 数量:{} 耗时:{}ms", headLogoList.size(), end - start); + } catch (Exception e) { + log.error("批量生成头像异常"); + } finally { + FeignHolder.clear(); + } + } + + @Async + public Boolean sendRegisterForm(String userId,String phone, String tenantCode, Map headers) { + try { + log.error("发送填写入职登记表alert={}", JSONUtil.toJsonStr(userId)); + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + String currJoinUrl = joinUrl + "&phone=" + phone; + //获取邀请码 + String storeCode = personnelOrgUtils.queryCodeForTenantId(tenantCode); + UserAndLinkDTO userAndLinkDTO = new UserAndLinkDTO( + userId, + String.format(currJoinUrl, tenantCode, storeCode) + ); + noticeUtils.inviteToJoin(List.of(userAndLinkDTO)); + return true; + } catch (Exception e) { + log.error("发送填写入职登记表alert异常"); + } finally { + FeignHolder.clear(); + } + return false; + } + + @Async + public void batchFillAccountForUserId(List list, String tenantCode, Map headers) { + try { + log.error("花名册导入回填账号信息={}", JSONUtil.toJsonStr(list)); + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantCode); + + if (CollectionUtil.isEmpty(list)) { + return; + } + for (UserAccountDto userAccountDto : list) { + SaveUserManagerAddDTO dto = new SaveUserManagerAddDTO(); + dto.setAccount(userAccountDto.getAccount()); + dto.setUserId(userAccountDto.getFid()); + dto.setRealName(userAccountDto.getUserName()); + + String fileName = userAccountDto.getHeadImageUrl(); + if (StringUtils.isNotEmpty(fileName)) { + int lastIndex = fileName.lastIndexOf('/'); + fileName = fileName.substring(lastIndex + 1); + } + dto.setHeadIcon(fileName); + dto.setNickName(userAccountDto.getUserNickName()); + Boolean aBoolean = userApi.userInfoUpdate(dto); + if (null == aBoolean || !aBoolean) { + log.error("导入回填账号信息异常,dto={},result={}", JSONUtil.toJsonStr(dto), aBoolean); + } else { + log.error("导入回填账号信息正常,修改租户账号,dto={},result={}", JSONUtil.toJsonStr(dto), aBoolean); + } + } + } catch (Exception e) { + log.error("花名册导入回填账号信息异常"); + } finally { + FeignHolder.clear(); + } + } + + @Async + public void asyncDeleteRosterOtherData(FtbPersonnelsStaffRoster roster, String tenantId, Map headers) { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantId); + personnelsPermissionsService.clearUserPermissions(roster.getUserId()); + //删除动态字段 + // registrationFormDataService.deleteByRosterId(roster.getId()); + //删除入职信息 + staffEmploymentApplyService.deleteByPhone(roster.getPhone()); + //pTenantAccountApi.deleteUser(roster.getUserId()); + // 转正, + regularManagementService.clearHistoricalConversionData(List.of(roster.getUserId())); + // 调岗数据 + transferManageService.clearTransferData(List.of(roster.getUserId())); + // 借调数据 + secondmentManagementService.endByUserConfirm(roster.getUserId(),new Date()); + // 删除离职信息 + ftbPersonnelsTurnoverManagementService.deleteResignationApplication(List.of(roster.getUserId())); + UserProvider.kickoutByUserId(roster.getUserId(), tenantId); + } catch (Exception e) { + log.error("异步删除花名册异常"); + } finally { + FeignHolder.clear(); + } + } + +} + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelCardOcr.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelCardOcr.java new file mode 100644 index 0000000..24c7051 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelCardOcr.java @@ -0,0 +1,118 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.ocr.v20181119.OcrClient; +import com.tencentcloudapi.ocr.v20181119.models.BankCardOCRRequest; +import com.tencentcloudapi.ocr.v20181119.models.BankCardOCRResponse; +import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRRequest; +import com.tencentcloudapi.ocr.v20181119.models.IDCardOCRResponse; +import jnpf.model.personnels.dto.staff.registerform.BankConvertDto; +import jnpf.model.personnels.dto.staff.registerform.IdCardConverterDto; +import jnpf.personnels.config.TengxunLicenseConfig; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Date; + +@Component +@Slf4j +public class PersonnelCardOcr { + @Resource + private TengxunLicenseConfig licenseConfig; + + /** + * 身份证识别 + * + * @param imgUrl + * @return + */ + public IdCardConverterDto idCardOcr(String imgUrl) { + try { + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 + // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 + // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + Credential cred = new Credential(licenseConfig.getSecretId(), licenseConfig.getSecretKey()); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint(licenseConfig.getDomain()); + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile); + // 实例化一个请求对象,每个接口都会对应一个request对象 + IDCardOCRRequest req = new IDCardOCRRequest(); + req.setImageUrl(imgUrl); + + // 返回的resp是一个IDCardOCRResponse的实例,与请求对象对应 + IDCardOCRResponse resp = client.IDCardOCR(req); + // 输出json格式的字符串回包 + log.error("idcard={}", IDCardOCRResponse.toJsonString(resp)); + IdCardConverterDto dto = new IdCardConverterDto(); + dto.setName(resp.getName()); + dto.setIdNum(resp.getIdNum()); + dto.setBirth(resp.getBirth()); + dto.setNation(resp.getNation()); + dto.setSex(resp.getSex()); + dto.setAddress(resp.getAddress()); + if (StringUtils.isNotEmpty(dto.getBirth())) { + //计算年龄 + DateTime parse = DateUtil.parse(dto.getBirth(), "yyyy/MM/dd"); + long year = DateUtil.betweenYear(parse, new Date(), false); + dto.setAge(year); + } + return dto; + + } catch (Exception e) { + log.error("身份证识别失败", e); + throw new RuntimeException("身份证识别失败"); + } + } + + /** + * 银行卡识别 + * + * @param imgUrl + * @return + */ + + public BankConvertDto bankOcr(String imgUrl) { + try { + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密 + // 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305 + // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + Credential cred = new Credential(licenseConfig.getSecretId(), licenseConfig.getSecretKey()); + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setEndpoint(licenseConfig.getDomain()); + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + ClientProfile clientProfile = new ClientProfile(); + clientProfile.setHttpProfile(httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + OcrClient client = new OcrClient(cred, "ap-guangzhou", clientProfile); + // 实例化一个请求对象,每个接口都会对应一个request对象 + BankCardOCRRequest req = new BankCardOCRRequest(); + req.setImageUrl(imgUrl); + + // 返回的resp是一个BankCardOCRResponse的实例,与请求对象对应 + BankCardOCRResponse resp = client.BankCardOCR(req); + // 输出json格式的字符串回包 + log.error(BankCardOCRResponse.toJsonString(resp)); + BankConvertDto dto = new BankConvertDto(); + dto.setCardNo(resp.getCardNo()); + dto.setBankInfo(resp.getBankInfo()); + dto.setCardName(resp.getCardName()); + dto.setCardType(resp.getCardType()); + return dto; + } catch (Exception e) { + log.error("银行卡识别失败,", e); + throw new RuntimeException("银行卡识别失败"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelDataAnalysisUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelDataAnalysisUtil.java new file mode 100644 index 0000000..daf010f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelDataAnalysisUtil.java @@ -0,0 +1,158 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.excel.annotation.ExcelProperty; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.model.personnels.vo.analysis.PersonnelDataAnalysisListVO; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 人事数据分析工具类 + */ +@Slf4j +public class PersonnelDataAnalysisUtil { + + /** + * 将 PageListVO 数据封装到 PersonnelDataAnalysisListVO + * @param pageListVO 分页数据 + * @return PersonnelDataAnalysisListVO + */ + public static PersonnelDataAnalysisListVO encapsulatePageData(PageListVO pageListVO,Class clazz) { + + // 提取列信息(只处理一次) + List columns = extractColumns(clazz); + if (CollUtil.isEmpty(columns)) { + throw new RuntimeException("没有字段存在@ExcelProperty 注解!"); + } + // 参数校验 + if (pageListVO == null || CollUtil.isEmpty(pageListVO.getList())) { + assert pageListVO != null; + return createEmptyResult(columns,pageListVO.getPagination()); + } + // 构建列名到 Column 的映射,提高查找效率 + Map columnMap = columns.stream() + .collect(Collectors.toMap( + PersonnelDataAnalysisListVO.Column::getProp, + col -> col, + (k1, k2) -> k1, + LinkedHashMap::new + )); + List list = pageListVO.getList(); + // 转换数据列表 + List> listItems = convertToListItems(list, columnMap); + + // 组装结果 + PersonnelDataAnalysisListVO result = new PersonnelDataAnalysisListVO(); + result.setColumns(columns); + result.setList(listItems); + result.setPagination(pageListVO.getPagination()); + + return result; + } + + /** + * 创建空结果 + */ + private static PersonnelDataAnalysisListVO createEmptyResult(List columns, PaginationVO pagination) { + PersonnelDataAnalysisListVO result = new PersonnelDataAnalysisListVO(); + result.setColumns(columns); + result.setList(new ArrayList<>()); + result.setPagination(pagination); + return result; + } + + /** + * 提取类的列信息(带 ExcelProperty 注解的字段) + */ + private static List extractColumns(Class clazz) { + // 缓存未命中,通过反射获取 + Field[] declaredFields = clazz.getDeclaredFields(); + List validFields = new ArrayList<>(); + + for (Field field : declaredFields) { + if (field.isAnnotationPresent(ExcelProperty.class)) { + validFields.add(field); + } + } + return buildColumnsFromFields(validFields); + } + + /** + * 从字段列表构建 Column 列表 + */ + private static List buildColumnsFromFields(List fields) { + List columns = new ArrayList<>(fields.size()); + + for (Field field : fields) { + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + String[] values = excelProperty.value(); + + PersonnelDataAnalysisListVO.Column column = new PersonnelDataAnalysisListVO.Column(); + column.setLabel(values.length > 0 ? values[0] : field.getName()); + column.setProp(field.getName()); + + columns.add(column); + } + + return columns; + } + + /** + * 转换数据列表为 ListItem 列表 + */ + private static List> convertToListItems( + List list, Map columnMap) { + + List> result = new ArrayList<>(list.size()); + + for (T data : list) { + if (data == null) { + result.add(new ArrayList<>()); + continue; + } + + List itemList = new ArrayList<>(columnMap.size()); + Class dataClass = data.getClass(); + + // 遍历 columnMap 而不是字段,确保顺序一致且高效 + for (Map.Entry entry : columnMap.entrySet()) { + String fieldName = entry.getKey(); + PersonnelDataAnalysisListVO.ListItem item = new PersonnelDataAnalysisListVO.ListItem(); + item.setName(fieldName); + item.setValue(getFieldValueAsString(data, dataClass, fieldName)); + itemList.add(item); + } + + result.add(itemList); + } + + return result; + } + + /** + * 安全地获取字段值并转换为字符串 + */ + private static String getFieldValueAsString(Object obj, Class objClass, String fieldName) { + try { + Field field = objClass.getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(obj); + return ObjectUtil.isNotEmpty(value) ? String.valueOf(value) : ""; + } catch (NoSuchFieldException e) { + log.warn("字段不存在:{}", fieldName, e); + return ""; + } catch (IllegalAccessException e) { + log.error("无法访问字段:{}", fieldName, e); + return ""; + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelIdCardVerificationUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelIdCardVerificationUtils.java new file mode 100644 index 0000000..4c957b4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelIdCardVerificationUtils.java @@ -0,0 +1,143 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.faceid.v20180301.FaceidClient; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationRequest; +import com.tencentcloudapi.faceid.v20180301.models.IdCardVerificationResponse; +import io.seata.spring.annotation.GlobalTransactional; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.personnels.po.FtbPersonnelsIdcardVerification; +import jnpf.personnels.config.TengxunLicenseConfig; +import jnpf.personnels.mapper.FtbPersonnelsIdcardVerificationMapper; +import jnpf.util.TenantUtil; +import jnpf.util.data.DataSourceContextHolder; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.*; + +/** + * 身份信息认证(二要素核验) + * @author wcx + * @date 2026/03/10 + */ +@Component +public class PersonnelIdCardVerificationUtils { + + @Resource + private FaceidClient faceidClient; + + @Resource + private FtbPersonnelsIdcardVerificationMapper ftbPersonnelsIdcardVerificationMapper; + + @Resource + private RedissonClient redissonClient; + + @Value("${idcard.tenant.enabled:false}") + private boolean isEnable; + + @Value("${idcard.tenant.code:''}") + private String tenantIdcardList; + + private final ExecutorService threadPoolExecutor = Executors.newSingleThreadExecutor(); + + /** + * 校验身份信息认证一致 + * + * @param idCard 身份证号 + * @param name 姓名 + * @return {@link IdCardVerificationResponse } + */ + public IdCardVerificationResponse idCardVerification(String idCard,String name,String tenantId) { + if (!isEnable) { + return null; + } + if (StrUtil.isBlank(tenantIdcardList)) { + return null; + } + if (!Arrays.asList(tenantIdcardList.split(",")).contains(tenantId)) { + return null; + } + RLock rLock = redissonClient.getLock("ftb-idCard-lock-" + idCard); + try { + rLock.lock(30, TimeUnit.SECONDS); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsIdcardVerification::getIdCard, idCard); + queryWrapper.eq(FtbPersonnelsIdcardVerification::getUserName, name); + queryWrapper.last("limit 1"); + IdCardVerificationResponse verificationResult = getVerificationResult(queryWrapper); + if (Objects.nonNull(verificationResult)) { + return verificationResult; + } + IdCardVerificationRequest req = new IdCardVerificationRequest(); + req.setIdCard(idCard); + req.setName(name); + IdCardVerificationResponse response = faceidClient.IdCardVerification(req); + FtbPersonnelsIdcardVerification insertValue = new FtbPersonnelsIdcardVerification(); + insertValue.setIdCard(idCard); + insertValue.setUserName(name); + insertValue.setVerificationResult(JSON.toJSONString(response)); + Future integerFuture = threadPoolExecutor.submit(() -> { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + return ftbPersonnelsIdcardVerificationMapper.insert(insertValue); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } finally { + DataSourceContextHolder.clearDatasourceType(); + } + }); + integerFuture.get(); + return response; + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + if (rLock.isHeldByCurrentThread()) { + rLock.unlock(); + } + } + } + + /** + * 统一校验结果 + * @param response + */ + public void checkIdCardVerification(IdCardVerificationResponse response) { + if (Objects.nonNull(response)) { + if (!"0".equals(response.getResult())) { + throw new RuntimeException(response.getDescription()); + } + } + } + + private IdCardVerificationResponse getVerificationResult(LambdaQueryWrapper queryWrapper) { + FtbPersonnelsIdcardVerification ftbPersonnelsIdcardVerification = ftbPersonnelsIdcardVerificationMapper.selectOne(queryWrapper); + if (Objects.nonNull(ftbPersonnelsIdcardVerification)) { + return JSON.parseObject(ftbPersonnelsIdcardVerification.getVerificationResult(), IdCardVerificationResponse.class); + } + return null; + } + + + @Bean + public FaceidClient faceidClient(TengxunLicenseConfig tengxunLicenseConfig) { + Credential credential = new Credential(tengxunLicenseConfig.getSecretId(), tengxunLicenseConfig.getSecretKey()); + return new FaceidClient(credential,""); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelOrgUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelOrgUtils.java new file mode 100644 index 0000000..2a6cdb1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelOrgUtils.java @@ -0,0 +1,2533 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.RegexPool; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import jnpf.account.PTenantAccountApi; +import jnpf.attendance.AttendanceGroupApi; +import jnpf.attendance.AttendanceUserApi; +import jnpf.attendance.dto.AttendanceUserGroupVo; +import jnpf.attendance.dto.GroupUpdateByUserDTO; +import jnpf.attendance.service.AttendanceGroupService; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.vo.PageListVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.StoreEntity; +import jnpf.entity.StoreUserRelation; +import jnpf.enums.personnel.PersonnelFormDataSystemRosterFields; +import jnpf.exception.LoginException; +import jnpf.model.personnels.bo.ExportRosterOneVo; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.staff.employment.FtbPersonnelsStaffEmploymentApplyDto; +import jnpf.model.personnels.dto.staff.field.*; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.registerform.WorkAddressDto; +import jnpf.model.personnels.dto.staff.roster.*; +import jnpf.model.personnels.dto.staff.transfer.TransferPositionDto; +import jnpf.model.personnels.dto.turnover.SaveTenantUserForm; +import jnpf.model.personnels.po.FtbPersonnelsRegistrationFormFieldOption; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.req.roster.UserAccountDto; +import jnpf.model.personnels.vo.roster.FtbRosterInsertAttributesVO; +import jnpf.model.personnels.vo.roster.FtbRosterInsertNomalVO; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurOrgInfo; +import jnpf.model.personnels.vo.turnover.FtbPersonnelsTurnoverManagementVO; +import jnpf.model.store.vo.StoreUserRelationVo; +import jnpf.permission.*; +import jnpf.permission.dto.QueryUserListDTO; +import jnpf.permission.dto.SaveUserManagerAddDTO; +import jnpf.permission.dto.SynUserBoundRoleDTO; +import jnpf.permission.dto.UpdateUserManagerBoundDTO; +import jnpf.permission.dto.v2.UserBoundInfoDTO; +import jnpf.permission.dto.v2.user.UpdateUserDTO; +import jnpf.permission.entity.*; +import jnpf.permission.model.position.PositionGradesInfoVO; +import jnpf.permission.model.role.RoleListVO; +import jnpf.permission.model.role.UserBoundRolesVO; +import jnpf.permission.model.user.*; +import jnpf.permission.vo.contract.ContractTypeVO; +import jnpf.permission.vo.user.UserBasicVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsStaffRosterService; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.personnels.service.FtbPersonnelsUchisuikePondService; +import jnpf.store.mapper.StoreMapper; +import jnpf.store.mapper.StoreUserRelationMapper; +import jnpf.util.Constants; +import jnpf.util.UploaderUtil; +import jnpf.util.UserProvider; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import javax.annotation.Resource; +import javax.validation.constraints.NotBlank; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +@Slf4j +public class PersonnelOrgUtils { + + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private PositionApi positionApi; + + @Autowired + private UserApi userApi; + + @Autowired + private RoleApi roleApi; + + @Autowired + private StoreMapper storeMapper; + + @Autowired + private ContractTypeApi contractTypeApi; + + @Autowired + private PTenantAccountApi pTenantAccountApi; + + @Autowired + private FTBApi ftbApi; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private FtbPersonnelsUchisuikePondService ftbPersonnelsUchisuikePondService; + + @Autowired + private StoreUserRelationMapper storeUserRelationMapper; + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private AttendanceUserApi attendanceUserApi; + + @Autowired + private AttendanceGroupApi attendanceGroupApi; + + @Autowired + private RosterExportThreadPool rosterExportThreadPool; + + @Autowired + private AttendanceGroupService attendanceGroupService; + @Autowired + private FtbPersonnelsStaffRosterService ftbPersonnelsStaffRosterService; + + @Autowired + V2OrganizeApi v2OrganizeApi; + + @Autowired + V2PositionApi v2PositionApi; + + @Autowired + V2GradesApi v2GradesApi; + + @Autowired + V2UserApi v2UserApi; + + @Resource + FtbPersonnelsTurnoverManagementService turnoverManagementService; + + final static String redisTenantIdToCode = "ftb:personnel:tentantIdToCode"; + final static String redisCodeToTenantId = "ftb:personnel:codeToTenantId"; + final static String redisAllCode = "ftb:personnel:allCode"; + final static List excludeExportFields = List.of("contractDetail"); + + + /** + * 根据id查询组织信息 + * + * @param id + * @return + */ + public OrganizeEntity queryOrganizeInfo(String id) { + if (StringUtils.isEmpty(id)) { + return null; + } + ActionResult actionResult = v2OrganizeApi.organizeInfoById(null,id); + if (null == actionResult || ObjectUtil.isNull(actionResult.getData())) { + return null; + } + return covertOrganizeEntity( actionResult.getData()); + + } + private OrganizeEntity covertOrganizeEntity(OrganizeGeneralDetailVO data) { + if (data == null) { + return null; + } + OrganizeEntity organizeEntity = new OrganizeEntity(); + organizeEntity.setId(data.getId()); + organizeEntity.setFullName(data.getName()); + organizeEntity.setEnCode(data.getEnCode()); + organizeEntity.setParentId(data.getParentId()); + organizeEntity.setOrganizeIdTree(data.getOrganizeIdTree()); + organizeEntity.setOrganizeTreeName(data.getOrganizeTreeName()); + return organizeEntity; + } + + /** + * 根据id查询组织信息 + * + * @param id + * @return + */ + public OrganizeEntity queryOrganizeInfoNoToken(String id, String tanentId) { + if (StringUtils.isEmpty(id)) { + return null; + } + ActionResult actionResult = v2OrganizeApi.organizeInfoById(null,id); + if (null == actionResult || ObjectUtil.isNull(actionResult.getData())) { + return null; + } + return covertOrganizeEntity( actionResult.getData()); + } + + /** + * 查询岗位 + * + * @param id + * @return + */ + public PositionEntity queryPosition(String id) { + if (StringUtils.isEmpty(id)) { + return null; + } + ActionResult actionResult = v2PositionApi.infoPosition(id); + if (null == actionResult || ObjectUtil.isNull(actionResult.getData())) { + return null; + } + return covertPositionEntity(actionResult.getData()); + } + public PositionEntity covertPositionEntity(PositionVO data) { + if (data == null) { + return null; + } + PositionEntity positionEntity = new PositionEntity(); + positionEntity.setId(data.getId()); + positionEntity.setFullName(data.getFullName()); + positionEntity.setEnCode(data.getEnCode()); + positionEntity.setType(data.getType()); + positionEntity.setDescription(data.getDescription()); + positionEntity.setPositionDescription(data.getPositionDescription()); + positionEntity.setPositionDescriptionUrl(data.getPositionDescriptionUrl()); + return positionEntity; + } + /** + * 查询岗位 + * + * @param id + * @return + */ + public PositionEntity queryPositionNoToken(String id, String tenantId) { + if (StringUtils.isEmpty(id)) { + return null; + } + ActionResult actionResult = v2PositionApi.infoPosition(id); + if (null == actionResult || ObjectUtil.isNull(actionResult.getData())) { + return null; + } + return covertPositionEntity(actionResult.getData()); + } + + /** + * 根据职等ID查询职等信息 + * + * @param id + * @return + */ + public PositionGradesInfoVO queryRank(String id) { + if (StringUtils.isEmpty(id)) { + return null; + } + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(id); + if (gradeVOActionResult != null && gradeVOActionResult.getData() != null) { + return covertPositionGradesInfoVO( gradeVOActionResult.getData()); + } + return null; + } + public PositionGradesInfoVO covertPositionGradesInfoVO(GradeVO data) { + if (data == null) { + return null; + } + PositionGradesInfoVO positionGradesInfoVO = new PositionGradesInfoVO(); + positionGradesInfoVO.setId(data.getId()); + positionGradesInfoVO.setFullName(data.getName()); + return positionGradesInfoVO; + } + + public PositionGradesInfoVO queryRankNoToken(String id, String tenantId) { + if (StringUtils.isEmpty(id)) { + return null; + } + ActionResult gradeVOActionResult = v2GradesApi.infoGrade(id); + if (gradeVOActionResult != null && gradeVOActionResult.getData() != null) { + return covertPositionGradesInfoVO( gradeVOActionResult.getData()); + } + return null; + } + + public String queryContractTypeName(String id) { + if (StringUtils.isEmpty(id)) { + return ""; + } + if ("1".equals(id)) { + return "固定期限劳动合同"; + } else if ("2".equals(id)) { + return "无固定期限劳动合同"; + } else if ("3".equals(id)) { + return "实习协议"; + } else if ("4".equals(id)) { + return "劳务协议"; + } else if ("5".equals(id)) { + return "劳务派遣合同"; + } else if ("6".equals(id)) { + return "返聘协议"; + } else if ("7".equals(id)) { + return "短期劳动合同"; + } + ActionResult info = contractTypeApi.getInfo(id); + if (info != null && info.getData() != null) { + return info.getData().getFullName(); + } + return ""; + } + + public String queryContractTypeNameNoToken(String id, String tenantId) { + if (StringUtils.isEmpty(id)) { + return ""; + } + if ("1".equals(id)) { + return "固定期限劳动合同"; + } else if ("2".equals(id)) { + return "无固定期限劳动合同"; + } else if ("3".equals(id)) { + return "实习协议"; + } else if ("4".equals(id)) { + return "劳务协议"; + } else if ("5".equals(id)) { + return "劳务派遣合同"; + } else if ("6".equals(id)) { + return "返聘协议"; + } else if ("7".equals(id)) { + return "短期劳动合同"; + } + ActionResult info = contractTypeApi.getInfoNoToken(id, tenantId); + if (info != null && info.getData() != null) { + return info.getData().getFullName(); + } + return ""; + } + + public List queryContractTypeForIds(List ids) { + if (CollectionUtil.isEmpty(ids)) { + return new ArrayList<>(); + } + + ActionResult> listInfo = contractTypeApi.getListInfo(ids); + if (null != listInfo && CollectionUtil.isNotEmpty(listInfo.getData())) { + return listInfo.getData(); + } + return new ArrayList<>(); + } + + public StoreEntity queryStoreInfo(String id) { + StoreEntity entity = storeMapper.getStoreById(id); + return entity; + } + + public UserEntity queryUserInfo(String id) { + if (StringUtils.isEmpty(id)) { + return null; + } + List userEntityList = userApi.getUserName(List.of(id)); + if (CollectionUtil.isNotEmpty(userEntityList)) { + return userEntityList.get(0); + } + return null; + } + + public FtbPersonnelsStaffRoster queryUserInfoLocal(String id) { + if (StringUtils.isEmpty(id)) { + return null; + } + return ftbPersonnelsStaffRosterService.getById(id); + + + } + + + public void fillRankName(List records) { + for (FtbPersonnelsStaffEmploymentApplyDto record : records) { + try { + ActionResult gradesInfo = positionApi.getGradesInfo(record.getCurrRank()); + if (gradesInfo != null && gradesInfo.getData() != null) { + record.setCurrRankName(gradesInfo.getData().getFullName()); + } + } catch (Exception e) { + log.error("根据职等ID查询职等信息异常,rankId={},{}", record.getCurrRank(), e); + } + } + } + + /** + * 填充合同类型 + * + * @param records + */ + public void fillContractTypeName(List records) { + for (FtbPersonnelsStaffEmploymentApplyDto record : records) { + String contractTypeName = queryContractTypeName(record.getContractType()); + record.setContractTypeName(contractTypeName); + } + } + + public void fillPositionName(List list) { + //获取组织id列表 + List ids = new ArrayList<>(); + list.forEach(item -> { + if (StringUtils.isNotBlank(item.getCurrPosition())) { + ids.add(item.getCurrPosition()); + } + }); + + List entityLists = positionApi.getPositionName(ids); + if (CollectionUtil.isEmpty(entityLists)) { + return; + } + Map listMap = entityLists.stream().collect(Collectors.toMap(PositionEntity::getId, Function.identity())); + for (FtbPersonnelsStaffEmploymentApplyDto dto : list) { + PositionEntity entity = listMap.get(dto.getCurrPosition()); + if (null == entity) { + continue; + } + dto.setCurrPositionName(entity.getFullName()); + } + } + + public void fillOrgName(List list) { + //获取组织id列表 + List ids = new ArrayList<>(); + list.forEach(item -> { + if (StringUtils.isNotBlank(item.getCurrOrg())) { + ids.add(item.getCurrOrg()); + } + }); + List entityList = organizeApi.getOrganizeName(ids); + if (CollectionUtil.isEmpty(entityList)) { + return; + } + Map listMap = entityList.stream().collect(Collectors.toMap(OrganizeEntity::getId, Function.identity())); + for (FtbPersonnelsStaffEmploymentApplyDto dto : list) { + OrganizeEntity organizeEntity = listMap.get(dto.getCurrOrg()); + if (null == organizeEntity) { + continue; + } + dto.setCurrOrgName(organizeEntity.getFullName()); + } + } + + public void fillExpireDayNum(List records) { + Date now = new Date(); + records.forEach(item -> { + if (item.getIsNeedCheck() == 1 || (item.getIsNeedCheck() == 0 && item.getCheckStatus() == 2)) { + if (item.getStatus() == 2) { + if (now.after(item.getExpectedStartDate())) { + item.setExpireDayNum(DateUtil.betweenDay(item.getExpectedStartDate(), now, false)); + } + } + } + + }); + } + + + public void addUserStoreRelation(String userId, String storeId, String orgId, String postionId, String rankId) { + if (StringUtils.isEmpty(storeId) || StringUtils.isEmpty(orgId)) { + return; + } + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(StoreUserRelation::getUserId, userId) + .eq(StoreUserRelation::getOrganizeId, orgId) + .eq(StoreUserRelation::getPositionId, postionId) + .eq(StoreUserRelation::getRankId, rankId) + .eq(StoreUserRelation::getStoreId, storeId) + .eq(StoreUserRelation::getDeleteMark, 0); + List list = storeUserRelationMapper.selectList(wrapper); + + if (CollectionUtil.isNotEmpty(list)) { + return; + } + StoreUserRelation entity = new StoreUserRelation(); + entity.setStoreId(storeId); + entity.setUserId(userId); + entity.setOrganizeId(orgId); + entity.setPositionId(postionId); + entity.setRankId(rankId); + entity.setDeleteMark(0); + storeUserRelationMapper.insert(entity); + + } + + /** + * 根据用户ID查询门店信息 + * + * @param userIds + * @return + */ + public List queryStoreManagerForUserId(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + List list = storeUserRelationMapper.queryList(userIds); + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + return list; + } + + public List> parseGroupData(String affiliatedGroupData) { + + Gson gson = new Gson(); + Type type = new TypeToken>>() { + }.getType(); + List> dataList = gson.fromJson(affiliatedGroupData, type); + return dataList; + } + + public List> buildMulitField(List groupList, String affiliatedGroupData, String userId, String tenantId) { + if (StringUtils.isEmpty(userId)) { + return List.of(groupList); + } + Map> map = getUserOrgBoundInfoForUserListAndTenantId(List.of(userId), tenantId, true); + List list = map.get(userId); + if (CollectionUtil.isEmpty(list)) { + return List.of(groupList); + } + + List> retList = new ArrayList<>(); + for (WorkerGroupDataDto workerGroupDataDto : list) { + List newGroupList = new ArrayList<>(); + for (FormFieldDto formFieldDto : groupList) { + FormFieldDto newFormFieldDto = new FormFieldDto(); + BeanUtil.copyProperties(formFieldDto, newFormFieldDto); + if ("workerName".equals(newFormFieldDto.getId()) || "phone".equals(newFormFieldDto.getId())) { + newFormFieldDto.setCurrIsUpdate(false); + } + if ("affiliatedOrg".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedOrg()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedOrgName()); + } else if ("affiliatedPosition".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedPosition()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedPositionName()); + } else if ("affiliatedRank".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedRank()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedRankName()); + } else if ("affiliatedShop".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedShop()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedShopName()); + } else if ("affiliatedReportsTo".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getReportsTo()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getReportsToName()); + } + newGroupList.add(newFormFieldDto); + } + retList.add(newGroupList); + } + return retList; + } + + public List> buildMulitField(List groupList, String affiliatedGroupData, String userId) { + if (StringUtils.isEmpty(userId)) { + return List.of(groupList); + } + List list = getUserOrgBoundInfo(userId); + if (CollectionUtil.isEmpty(list)) { + return List.of(groupList); + } + + List> retList = new ArrayList<>(); + for (WorkerGroupDataDto workerGroupDataDto : list) { + List newGroupList = new ArrayList<>(); + for (FormFieldDto formFieldDto : groupList) { + FormFieldDto newFormFieldDto = new FormFieldDto(); + BeanUtil.copyProperties(formFieldDto, newFormFieldDto); + // 是否为主岗 + newFormFieldDto.setIsPrimaryPosition(workerGroupDataDto.getIsPrimaryPosition()); + if ("affiliatedOrg".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedOrg()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedOrgName()); + } else if ("affiliatedPosition".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedPosition()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedPositionName()); + } else if ("affiliatedRank".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedRank()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedRankName()); + } else if ("affiliatedShop".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getAffiliatedShop()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getAffiliatedShopName()); + } else if ("affiliatedReportsTo".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getReportsTo()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getReportsToName()); + } else if ("team".equals(newFormFieldDto.getId())) { + newFormFieldDto.setUserValue(workerGroupDataDto.getStoreTeamId()); + newFormFieldDto.setUserValueLable(workerGroupDataDto.getStoreTeamName()); + } + newGroupList.add(newFormFieldDto); + } + retList.add(newGroupList); + } + return retList; + } + + + public UpdateUserDTO convertSynUserBoundDTOList(List list, String userId) { + if (CollectionUtil.isEmpty(list)) { + return null; + } + WorkerGroupDataDto dataDto = list.stream().findFirst().orElse(new WorkerGroupDataDto()); + UpdateUserDTO updateUserDTO = new UpdateUserDTO(); + updateUserDTO.setId(userId); + UserBoundInfoDTO boundInfoDTO = new UserBoundInfoDTO(); + boundInfoDTO.setOrganizeId(dataDto.getAffiliatedOrg()); + boundInfoDTO.setPositionId(dataDto.getAffiliatedPosition()); + boundInfoDTO.setGradesId(dataDto.getAffiliatedRank()); + boundInfoDTO.setLeaderId(dataDto.getReportsTo()); + boundInfoDTO.setStoreTeamId(dataDto.getStoreTeamId()); + updateUserDTO.setBoundInfoDTO(boundInfoDTO); + return updateUserDTO; + } + + public Boolean sysUserBoundList(List list, String userId) { + //todo同步组织 + UpdateUserDTO boundInfoDTO = convertSynUserBoundDTOList(list, userId); + Boolean aBoolean = null; + try { + ActionResult actionResult = v2UserApi.updateUserInfo(userId, boundInfoDTO); + } catch (Exception e) { + e.printStackTrace(); + log.error("同步组织岗位等信息异常"); + throw new RuntimeException(e); + } + return aBoolean; + } + + /** + * 同步角色 + * + * @param dto + * @return + */ + public Boolean sysUserRole(SynUserBoundRoleDTO dto) { + //同步角色 + + Boolean aBoolean = userApi.synchronousUserBoundRole(dto); + log.error("正常日志,同步角色={},result={}", JSONUtil.toJsonStr(dto), aBoolean); + return aBoolean; + } + + /** + * 根据组织岗位职等获取用户id列表 + * + * @param orgId + * @param positionId + * @param gradesId + * @return + */ + public List queryUserIdList(String orgId, String positionId, String gradesId) { + return userApi.getUserIdsByGradesId(orgId, positionId, gradesId); + } + + + public String getImportWorkerSex(List attributesVOList) { + String workerSex = null; + + if (CollectionUtil.isNotEmpty(attributesVOList)) { + for (FtbRosterInsertAttributesVO attributesVO : attributesVOList) { + String formFieldId = attributesVO.getFormFieldId(); + String attributeValue = attributesVO.getAttributeValue(); + if ("workerSex".equals(formFieldId)) { + workerSex = attributeValue; + break; + } + } + } + return workerSex; + } + + public Map getImportInfoMap(List attributesVOList) { + Map map = new HashMap<>(); + if (CollectionUtil.isNotEmpty(attributesVOList)) { + for (FtbRosterInsertAttributesVO attributesVO : attributesVOList) { + String formFieldId = attributesVO.getFormFieldId(); + String attributeValue = attributesVO.getAttributeValue(); + map.put(formFieldId, attributeValue); + } + } + return map; + } + + public FtbPersonnelsStaffRoster buildImportRoster(FtbRosterInsertNomalVO vo) { + FtbPersonnelsStaffRoster entity = new FtbPersonnelsStaffRoster(); + entity.setName(vo.getUserName()); + entity.setPhone(vo.getPhone()); + if (StringUtils.isNotEmpty(vo.getEntrySalary())) { + entity.setEntrySalary(new BigDecimal(vo.getEntrySalary())); + } + if (StringUtils.isNotEmpty(vo.getRegularSalary())) { + entity.setRegularSalary(new BigDecimal(vo.getRegularSalary())); + } + + + List attributesVOList = vo.getFtbRosterInsertAttributesVOS(); + String probationPeriod = ""; + if (CollectionUtil.isNotEmpty(attributesVOList)) { + Map importInfoMap = new HashMap<>(); + for (FtbRosterInsertAttributesVO attributesVO : attributesVOList) { + String formFieldId = attributesVO.getFormFieldId(); + String attributeValue = attributesVO.getAttributeValue(); + importInfoMap.put(formFieldId, attributeValue); + if ("systemWokerId".equals(formFieldId)) { + entity.setSystemWokerId(attributeValue); + } else if ("systemContractId".equals(formFieldId)) { + entity.setSystemContractId(attributeValue); + } else if ("workerNo".equals(formFieldId)) { + entity.setWorkerNo(attributeValue); + } else if ("currOrg".equals(formFieldId)) { + entity.setCurrOrg(attributeValue); + } else if ("currPosition".equals(formFieldId)) { + entity.setCurrPosition(attributeValue); + } else if ("currRank".equals(formFieldId)) { + entity.setCurrRank(attributeValue); + } else if ("currShopId".equals(formFieldId)) { + entity.setCurrShopId(attributeValue); + } else if ("currReportsTo".equals(formFieldId)) { + entity.setCurrReportsTo(attributeValue); + } else if ("contractNo".equals(formFieldId)) { + entity.setContractNo(attributeValue); + } else if ("contractEffectiveDate".equals(formFieldId)) { + entity.setContractEffectiveDate(stringDateToDate(attributeValue)); + } else if ("contractDate".equals(formFieldId)) { + entity.setContractEndDate(stringDateToDate(attributeValue)); + } else if ("actualStartDate".equals(formFieldId)) { + entity.setActualStartDate(stringDateToDate(attributeValue)); + } else if ("workerType".equals(formFieldId)) { + entity.setWorkerType(attributeValue); + } else if ("contractType".equals(formFieldId)) { + entity.setContractType(attributeValue); + } else if ("workerStatus".equals(formFieldId)) { + entity.setWorkerStatus(attributeValue); + } else if ("actualProbationaryDate".equals(formFieldId)) { + entity.setActualProbationaryDate(stringDateToDate(attributeValue)); + } else if ("currRole".equals(formFieldId)) { + entity.setCurrRole(attributeValue); + } else if ("probationPeriod".equals(formFieldId)) { + probationPeriod = attributeValue; + } + } + vo.setInnerFieldValueCacheMap(importInfoMap); + } + + if (StringUtils.isEmpty(probationPeriod)) { + probationPeriod = "100"; + } + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(probationPeriod, "0"); + + if ("302".equals(entity.getWorkerStatus())) { + entity.setCurrSalary(entity.getEntrySalary()); + if (probationPeriodDto.getType().equals("100")) { + throw new RuntimeException("姓名:" + entity.getName() + ",手机号:" + entity.getPhone() + ",员工状态是试用,请配置试用期限"); + } + } else { + entity.setCurrSalary(entity.getRegularSalary()); + } + + if (null == entity.getCurrSalary()) { + if (null != entity.getRegularSalary()) { + entity.setCurrSalary(entity.getRegularSalary()); + } else if (null != entity.getEntrySalary()) { + entity.setCurrSalary(entity.getEntrySalary()); + } + } + return entity; + } + + /** + * 从excel中获取指定值 + * + * @param attributesVOList + * @param key + * @return + */ + public String getValueForExcel(List attributesVOList, String key) { + String value = ""; + if (CollectionUtil.isEmpty(attributesVOList)) { + return value; + } + for (FtbRosterInsertAttributesVO attributesVO : attributesVOList) { + String formFieldId = attributesVO.getFormFieldId(); + String attributeValue = attributesVO.getAttributeValue(); + if (key.equals(formFieldId)) { + value = attributeValue; + break; + } + } + return value; + + } + + public Date convertToLocalDateToDate(LocalDate localDate) { + // 将LocalDate转换为LocalDateTime,默认时间设置为0点 + LocalDateTime localDateTime = localDate.atStartOfDay(); + + // 将LocalDateTime转换为ZonedDateTime,使用系统默认时区 + ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault()); + + // 从ZonedDateTime获取Instant对象 + Instant instant = zonedDateTime.toInstant(); + + // 使用Instant构造Date对象 + return Date.from(instant); + } + + public Date stringDateToDate(String str) { + if (StringUtils.isEmpty(str)) { + return null; + } + try { + if (str.contains(":")) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date date = formatter.parse(str); + return date; + } else if (str.contains("/")) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd"); + Date date = formatter.parse(str); + return date; + } else if (str.contains("-")) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + Date date = formatter.parse(str); + return date; + } else { + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd"); + Date date = formatter.parse(str); + return date; + } + } catch (ParseException e) { + log.error("转换日期失败 ={}", str); + } + return null; + } + + public Date stringDateToDateTime(String str) { + if (StringUtils.isEmpty(str)) { + return null; + } + try { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + // 将字符串转换为Date对象 + Date date = formatter.parse(str); + return date; + } catch (ParseException e) { + log.error("转换日期失败 ={}", str); + } + return null; + } + + public List queryUserBaseInfoAndOrgAndPosAndRank(List userIdList, String tenantId) { + List selectList = userApi.getUserBoundMoreInfosByUserIdsNoToken(userIdList, tenantId); + if (CollectionUtil.isEmpty(selectList)) { + return new ArrayList<>(); + } + List retList = new ArrayList<>(); + Map> map = selectList.stream().collect(Collectors.groupingBy(UserBoundMoreInfoVO::getUserId)); + for (Map.Entry> entry : map.entrySet()) { + String userId = entry.getKey(); // 用户ID + StaffRosterSimpleBaseInfoDto baseInfoDto = new StaffRosterSimpleBaseInfoDto(); + baseInfoDto.setUserId(userId); + List list = entry.getValue(); // 获取值列表 + List workerGroupDataDtoList = new ArrayList<>(); + for (UserBoundMoreInfoVO vo : list) { + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setReportsToName(vo.getManagerRealName()); + dto.setReportsTo(vo.getManagerId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getPositionGradesId()); + dto.setAffiliatedRankName(vo.getPositionGradesName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setIsDefaultOrganize(vo.getIsDefaultOrganize()); + dto.setIsDefaultPosition(vo.getIsDefaultPosition()); + workerGroupDataDtoList.add(dto); + } + baseInfoDto.setInfoList(workerGroupDataDtoList); + retList.add(baseInfoDto); + } + return retList; + } + + public Map> getUserOrgBoundInfoForUserList(List userIdList) { + List selectList = userApi.getUserBoundMoreInfosByUserIds(userIdList); + if (CollectionUtil.isEmpty(selectList)) { + return new HashMap<>(); + } + Map> retMap = new HashMap<>(); + Map> map = selectList.stream().collect(Collectors.groupingBy(UserBoundMoreInfoVO::getUserId)); + for (Map.Entry> entry : map.entrySet()) { + String userId = entry.getKey(); // 用户ID + List list = entry.getValue(); // 获取值列表 + List workerGroupDataDtoList = new ArrayList<>(); + for (UserBoundMoreInfoVO vo : list) { + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setReportsToName(vo.getManagerRealName()); + dto.setReportsTo(vo.getManagerId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getPositionGradesId()); + dto.setAffiliatedRankName(vo.getPositionGradesName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); +// //补充门店 +// SimpleStoreUserDto simpleStoreUserDto = queryStoreUser(userId, vo.getOrganizeId(), vo.getPositionId(), vo.getPositionGradesId()); +// if (null != simpleStoreUserDto) { +// dto.setAffiliatedShop(simpleStoreUserDto.getStoreId()); +// dto.setAffiliatedShopName(simpleStoreUserDto.getStoreName()); +// } + workerGroupDataDtoList.add(dto); + } + retMap.put(userId, workerGroupDataDtoList); + } + + return retMap; + } + + + /** + * @param userIdList + * @param isSelectShop true-查询门店信息 false-不查询门店信息 + * @return + */ + public Map> getUserOrgBoundInfoForUserList(List userIdList, Boolean isSelectShop) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userIdList, UserProvider.getUser().getTenantId()); + if (actionResult == null || CollectionUtil.isEmpty(actionResult.getData())) { + return new HashMap<>(); + } + //补充门店,如果为true表示要查询门店信息 + List simpleStoreUserDtos; + if (isSelectShop) { + HashSet objects = new HashSet<>(); + objects.addAll(userIdList); + simpleStoreUserDtos = storeUserRelationMapper.querySimpleStoreUserDto(objects); + } else { + simpleStoreUserDtos = new ArrayList<>(); + } + Map> retMap = new HashMap<>(); + actionResult.getData().forEach(vo->{ + String userId = vo.getId(); + List workerGroupDataDtoList = new ArrayList<>(); + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setReportsToName(vo.getLeaderName()); + dto.setOrganizeCategory(vo.getOrganizeCategory()); + dto.setReportsTo(vo.getLeaderId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getGradeId()); + dto.setAffiliatedRankName(vo.getGradeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setStoreTeamId(vo.getStoreTeamId()); + dto.setStoreTeamName(vo.getStoreTeamName()); + if (isSelectShop && CollUtil.isNotEmpty(simpleStoreUserDtos)) { + SimpleStoreUserDto simpleStoreUserDto = simpleStoreUserDtos.stream() + .filter(item -> userId.equals(item.getUserId())).findFirst().orElse(null); + if (null != simpleStoreUserDto) { + dto.setAffiliatedShop(simpleStoreUserDto.getStoreId()); + dto.setAffiliatedShopName(simpleStoreUserDto.getStoreName()); + } + } + workerGroupDataDtoList.add(dto); + retMap.put(userId, workerGroupDataDtoList); + }); + return retMap; + } + + + /** + * @param userIdList + * @param isSelectShop true-查询门店信息 false-不查询门店信息 + * @return + */ + public Map> getUserOrgBoundInfoForUserListAndTenantId(List userIdList, String tenantId, Boolean isSelectShop) { + Map> retMap = new HashMap<>(); +// List selectList = userApi.getUserBoundMoreInfosByUserIdsNoToken(userIdList, tenantId); + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(userIdList, tenantId); + List userBoundVOList = allUserInfoBatch.getData(); + if (CollectionUtil.isEmpty(userBoundVOList)) { + try { + TenantDataSourceUtil.switchTenant(tenantId); + List managementVOS = turnoverManagementService.queryTurnoverList(userIdList); + managementVOS.forEach(v->{ + retMap.put(v.getUserId(), v.getOrganizationList()); + }); + } catch (LoginException e) { + throw new RuntimeException(e); + } + return retMap; + } +// Map> map = selectList.stream().collect(Collectors.groupingBy(UserBoundMoreInfoVO::getUserId)); + Map> map = userBoundVOList.stream().collect(Collectors.groupingBy(UserBoundVO::getId)); + for (Map.Entry> entry : map.entrySet()) { + String userId = entry.getKey(); // 用户ID + List list = entry.getValue(); // 获取值列表 + List workerGroupDataDtoList = new ArrayList<>(); + for (UserBoundVO vo : list) { + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setIsDefault(true); +// dto.setReportsToName(vo.getManagerRealName()); + dto.setReportsTo(vo.getLeaderId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getGradeId()); + dto.setAffiliatedRankName(vo.getGradeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setIsPrimaryPosition(true); + //补充门店,如果为true表示要查询门店信息 + if (isSelectShop) { + SimpleStoreUserDto simpleStoreUserDto = queryStoreUser(userId, vo.getOrganizeId(), vo.getPositionId(), vo.getGradeId()); + if (null != simpleStoreUserDto) { + dto.setAffiliatedShop(simpleStoreUserDto.getStoreId()); + dto.setAffiliatedShopName(simpleStoreUserDto.getStoreName()); + } + } + workerGroupDataDtoList.add(dto); + } + retMap.put(userId, workerGroupDataDtoList); + } + Set associatedDataUserIds = retMap.keySet(); + // 获取userIdList中,没有关联数据的用户id + List thereIsNoAffiliationUserIds = userIdList.stream().filter(v -> !associatedDataUserIds.contains(v)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(thereIsNoAffiliationUserIds)){ + try { + TenantDataSourceUtil.switchTenant(tenantId); + List managementVOS = turnoverManagementService.queryTurnoverList(thereIsNoAffiliationUserIds); + managementVOS.forEach(v->{ + retMap.put(v.getUserId(), v.getOrganizationList()); + }); + } catch (LoginException e) { + throw new RuntimeException(e); + } + } + return retMap; + } + + public List getUserOrgBoundInfo(String userId,String... tenantIds) { + if (StringUtils.isEmpty(userId)) { + return CollectionUtil.newArrayList(); + } + String tenantId = tenantIds.length > 0 ? tenantIds[0] : null; + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(List.of(userId), tenantId); + if (actionResult == null || CollectionUtil.isEmpty(actionResult.getData())) { + FtbPersonnelsTurOrgInfo userOrganizeInfo = turnoverManagementService.getUserOrganizeInfo(userId, null); + if (ObjectUtil.isNotEmpty(userOrganizeInfo)){ + WorkerGroupDataDto workerGroupDataDto = userOrganizeInfo.getWorkerGroupDataDto(); + return List.of(workerGroupDataDto); + } + return CollectionUtil.newArrayList(); + } + List retList = new ArrayList<>(); + for (UserBoundVO vo : actionResult.getData()) { + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setIsDefault(true); + dto.setOrganizeCategory(vo.getOrganizeCategory()); + dto.setIsDefaultOrganize(true); + dto.setReportsToName(vo.getLeaderName()); + dto.setReportsTo(vo.getLeaderId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getGradeId()); + dto.setAffiliatedRankName(vo.getGradeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setIsPrimaryPosition(true); + dto.setStoreTeamId(vo.getStoreTeamId()); + dto.setStoreTeamName(vo.getStoreTeamName()); + //补充门店 + SimpleStoreUserDto simpleStoreUserDto = queryStoreUser(userId, vo.getOrganizeId(), vo.getPositionId(), vo.getGradeId()); + if (null != simpleStoreUserDto) { + dto.setAffiliatedShop(simpleStoreUserDto.getStoreId()); + dto.setAffiliatedShopName(simpleStoreUserDto.getStoreName()); + } + retList.add(dto); + } + return retList; + } + + public Map> getUserOrgBoundInfo(List userId) { + ActionResult> actionResult = v2UserApi.getUserPrimaryBoundBatch(userId, UserProvider.getUser().getTenantId()); + List data = actionResult.getData(); + Map userIdMap = data.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (r1, r2) -> r1)); + if (CollectionUtil.isEmpty(userIdMap)) { + return Maps.newHashMap(); + } + HashMap> map = Maps.newHashMap(); + userId.forEach(str -> { + if (ObjectUtil.isNotEmpty(userIdMap) && userIdMap.containsKey(str)) { + UserBoundVO moreInfoVOS = userIdMap.get(str); + map.put(str, List.of(coverUserBand(moreInfoVOS))); + } + }); + return map; + } + + public WorkerGroupDataDto coverUserBand(UserBoundVO vo) { + if (null == vo) { + return null; + } + WorkerGroupDataDto dto = new WorkerGroupDataDto(); + dto.setIsDefault(true); + dto.setOrganizeCategory(vo.getOrganizeCategory()); + dto.setIsDefaultOrganize(true); + dto.setReportsToName(vo.getLeaderName()); + dto.setReportsTo(vo.getLeaderId()); + dto.setAffiliatedOrg(vo.getOrganizeId()); + dto.setAffiliatedOrgName(vo.getOrganizeName()); + dto.setAffiliatedPosition(vo.getPositionId()); + dto.setAffiliatedPositionName(vo.getPositionName()); + dto.setAffiliatedRank(vo.getGradeId()); + dto.setAffiliatedRankName(vo.getGradeName()); + dto.setOrgEncode(vo.getOrganizeEnCode()); + dto.setPositionEncode(vo.getPositionEnCode()); + dto.setIsPrimaryPosition(true); + dto.setStoreTeamId(vo.getStoreTeamId()); + dto.setStoreTeamName(vo.getStoreTeamName()); + return dto; + } + + public SimpleStoreUserDto queryStoreUser(String userId, String orgId, String postionId, String rankId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda() + .eq(StoreUserRelation::getUserId, userId) + .eq(StoreUserRelation::getOrganizeId, orgId) + .eq(StoreUserRelation::getPositionId, postionId) + .eq(StoreUserRelation::getRankId, rankId) + .eq(StoreUserRelation::getDeleteMark, 0); + List list = storeUserRelationMapper.selectList(wrapper); + if (CollectionUtil.isNotEmpty(list)) { + StoreUserRelation storeUserRelation = list.get(0); + StoreEntity entity = storeMapper.getStoreById(storeUserRelation.getStoreId()); + SimpleStoreUserDto dto = new SimpleStoreUserDto(); + dto.setUserId(userId); + dto.setStoreId(storeUserRelation.getStoreId()); + if (null != dto) { + dto.setStoreName(entity.getStorename()); + } + return dto; + } + return null; + } + + /** + * 设置今日已经发送短信 + * + * @param phone + */ + public Boolean setTodayIsSendPhoneMsg(String phone) { + Date date = new Date(); // 获取当前日期和时间对象 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); // 设置日期格式 + String today = formatter.format(date); // 转换为字符串 + String lockKey = "employment:apply:" + today + ":" + phone; + if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 1, TimeUnit.DAYS)) { + return false; + } else { + return true; + } + } + + /** + * 重置今日发送短信 + * + * @param phone + */ + public void resetTodaySendPhoneMsg(String phone) { + Date date = new Date(); // 获取当前日期和时间对象 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); // 设置日期格式 + String today = formatter.format(date); // 转换为字符串 + String lockKey = "employment:apply:" + today + ":" + phone; + redisTemplate.delete(lockKey); + } + + /** + * 设置今日已经发送短信 + * + * @param phone + * @return false 未发送 true 已经发送 + */ + public Boolean queryIsSendPhoneMsg(String phone) { + Date date = new Date(); // 获取当前日期和时间对象 + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); // 设置日期格式 + String today = formatter.format(date); // 转换为字符串 + String lockKey = "employment:apply:" + today + ":" + phone; + String val = (String) redisTemplate.opsForValue().get(lockKey); + if (StringUtils.isNotEmpty(val)) { + return true; + } else { + return false; + } + } + + + /** + * 通过租户ID 查询code + * + * @param tenantId + * @return + */ + public String queryCodeForTenantId(String tenantId) { + String tenantIdToCode = redisTenantIdToCode + ":" + tenantId; + String val = (String) redisTemplate.opsForValue().get(tenantIdToCode); + if (StringUtils.isNotEmpty(val)) { + return val; + } + String storeCode = randCode(); + while (true) { + if (redisTemplate.opsForSet().isMember(redisAllCode, storeCode)) { + storeCode = randCode(); + continue; + } + redisTemplate.opsForSet().add(redisAllCode, storeCode); + break; + } + redisTemplate.opsForValue().set(tenantIdToCode, storeCode); + String codeToTenantId = redisCodeToTenantId + ":" + storeCode; + redisTemplate.opsForValue().set(codeToTenantId, tenantId); + return storeCode; + } + + /** + * 通过code 查询租户ID + * + * @param storeCode + * @return + */ + public String queryTenantIdForCode(String storeCode) { + + String codeToTenantId = redisCodeToTenantId + ":" + storeCode; + String tenantId = (String) redisTemplate.opsForValue().get(codeToTenantId); + return tenantId; + } + + private String randCode() { + Random random = new Random(); + int randomNumber = random.nextInt(9000) + 1000; // 生成1000到9999之间的随机数 + return String.valueOf(randomNumber); + } + + public String getPhoneFormInput(List fieldValueList) { + String phone = ""; + if (CollectionUtil.isNotEmpty(fieldValueList)) { + for (SubFormFieldDto dto : fieldValueList) { + if (dto.getId().equals("phone")) { + phone = dto.getUserValue(); + break; + } + } + } + return phone; + } + + + public Map convertFormValueMap(List formFieldValueList) { + Map map = new HashMap<>(); + if (CollectionUtil.isEmpty(formFieldValueList)) { + return map; + } + for (FtbPersonnelsStaffRegistrationFormData vo : formFieldValueList) { + map.put(vo.getFormFieldId(), vo); + } + return map; + } + + public String getPhoneFormInputForField(List fieldValueList, String field) { + String value = ""; + if (CollectionUtil.isNotEmpty(fieldValueList)) { + for (SubFormFieldDto dto : fieldValueList) { + if (dto.getId().equals(field)) { + value = dto.getUserValue(); + break; + } + } + } + return value; + } + + /** + * 获取一个字段的值 + * + * @param map + * @return + */ + public String getFormDataForField(Map map, String fieldId) { + if (CollectionUtil.isEmpty(map)) { + return ""; + } + FtbPersonnelsStaffRegistrationFormData ftbPersonnelsStaffRegistrationFormData = map.get(fieldId); + if (null != ftbPersonnelsStaffRegistrationFormData) { + return ftbPersonnelsStaffRegistrationFormData.getValue(); + } + return ""; + } + + /** + * @param dto + * @param formDataList + * @param isNeedQueryRole 是否需要查询角色 true 需要,false不需要 + */ + public void fillRosterDtoData(FtbPersonnelsStaffRosterDto dto, List formDataList, Boolean isNeedQueryRole) { + if (CollectionUtil.isEmpty(formDataList)) { + return; + } + Date now = new Date(); + for (FtbPersonnelsStaffRegistrationFormData formData : formDataList) { + if (StringUtils.isEmpty(formData.getValue())) { + continue; + } else if (formData.getFormFieldId().equals("headLogo")) { + List headListLogo = personnelOrgUtils.parseHeadLogo(formData.getValue()); + if (CollectionUtil.isNotEmpty(headListLogo)) { + dto.setHeadLogo(headListLogo.get(0)); + } + } else if (formData.getFormFieldId().equals("idCardNum")) { + dto.setIdCardNum(formData.getValue()); + } else if (formData.getFormFieldId().equals("workerSex")) { + dto.setWorkerSexName(PersonnelStaffUtils.convertSexName(formData.getValue())); + } else if (formData.getFormFieldId().equals("flowerName")) { + dto.setFlowerName(formData.getValue()); + } else if (formData.getFormFieldId().equals("birthday")) { + dto.setBirthday(formData.getValue()); + } else if (formData.getFormFieldId().equals("age")) { + dto.setAge(formData.getValue()); + } else if (formData.getFormFieldId().equals("companyAge")) { + dto.setCompanyAge(formData.getValue()); + } else if (formData.getFormFieldId().equals("probationPeriod")) { + if (StringUtils.isNotEmpty(formData.getValue()) && formData.getValue().startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(formData.getValue(), ProbationPeriodDto.class); + dto.setProbationPeriod(bean.getType()); + dto.setProbationPeriodDay(bean.getDays()); + if (dto.getProbationPeriod().equals("100") && dto.getIsTrialJob() != null && dto.getIsTrialJob().equals(1)) { + dto.setWorkerStatusFormal(1); + } + } + } else if (formData.getFormFieldId().equals("planProbationaryDate")) { + dto.setPlanProbationaryDate((DateUtil.parse(formData.getValue(), "yyyy-MM-dd"))); + } else if (formData.getFormFieldId().equals("contractTaskId")) { + dto.setContractTaskId(formData.getValue()); + } + } + if (null != dto.getActualStartDate()) { + if (!"305".equals(dto.getWorkerStatus())) { + dto.setCompanyAge(String.valueOf(DateUtil.between(dto.getActualStartDate(), now, DateUnit.DAY))); + } + String s = calculateSeniorityForDate(dto.getActualStartDate().toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate()); + dto.setSiLing(s); + } else { + dto.setSiLing(calculateSeniority("0天")); + dto.setCompanyAge("0"); + } + /** + * 查询用户的角色 + */ + if (isNeedQueryRole) { + List saffRoleDtos = queryRoleListForUserId(dto.getUserId()); + dto.setUserRoleList(saffRoleDtos); + } + } + + public String getInputHeadLogo(String headLogo) { + if (StringUtils.isEmpty(headLogo) || "[]".equals(headLogo)) { + return ""; + } else { + List headList = JSONUtil.toList(headLogo, String.class); + if (CollectionUtil.isNotEmpty(headList)) { + headLogo = headList.get(0); + } else { + headLogo = ""; + } + } + return headLogo; + } + + public List parseHeadLogo(String headLogo) { + if (StringUtils.isEmpty(headLogo) || "[]".equals(headLogo)) { + return new ArrayList<>(); + } + List headList = JSONUtil.toList(headLogo, String.class); + if (CollectionUtil.isEmpty(headList)) { + return new ArrayList<>(); + } + ArrayList list = new ArrayList<>(); + for (String head : headList) { + if(head.startsWith("http")){ + list.add(head); + }else if (!head.startsWith("/api")) { + list.add(UploaderUtil.uploaderImg(head)); + } else { + list.add(head); + } + } + return list; + } + + public String convertHeadLogo(String headLogo) { + if (StringUtils.isEmpty(headLogo) || "[]".equals(headLogo)) { + return ""; + } + List headList = JSONUtil.toList(headLogo, String.class); + if (CollectionUtil.isEmpty(headList)) { + return ""; + } + ArrayList list = new ArrayList<>(); + for (String head : headList) { + if(head.startsWith("http")){ + list.add(head); + }else if (!head.startsWith("/api")) { + list.add(UploaderUtil.uploaderImg(head)); + } else { + list.add(head); + } + } + return list.get(0); + } + + /** + * 计算员工从入职日期到现在的司龄(以年、月、天为单位) + * + * @return 司龄字符串,格式如 "x年x月x天" + */ + public String calculateSeniority(String days) { + if (StringUtils.isEmpty(days) || "0".equals(days)) { + return "0天"; + } + try { + Integer totalDays = Integer.valueOf(days); + return formatDaysToYearMonthDay(totalDays); + } catch (Exception e) { + return "0天"; + } + } + + public String calculateSeniorityForDate(LocalDate startDate) { + LocalDate currentDate = LocalDate.now(); // 获取当前日期 + Period period = Period.between(startDate, currentDate); // 计算两个日期之间的差值 + int years = period.getYears(); + int months = period.getMonths(); + int days = period.getDays(); + if (years == 0 && months == 0 && days == 0) { + return "0天"; + } else if (years == 0 && months == 0 && days != 0) { + return String.format("%d天", days); + } else if (years == 0 && months != 0 && days != 0) { + return String.format("%d月%d天", months, days); + } else { + return String.format("%d年%d月%d天", years, months, days); + } + + } + + public String formatDaysToYearMonthDay(int days) { + LocalDate startDate = LocalDate.now(); + LocalDate endDate = startDate.plusDays(days); + long years = ChronoUnit.YEARS.between(startDate, endDate); + long months = ChronoUnit.MONTHS.between(startDate, endDate); + long remainingDays = ChronoUnit.DAYS.between(startDate.plusYears(years).plusMonths(months), endDate); + return years + "年" + months + "月" + remainingDays + "天"; + } + + public Integer convertAccountStatus(String workerStatus) { + //1.全职-转正、2.全职-试用、3.在校实习、4.企业见习、 5.兼职 + if ("302".equals(workerStatus)) { + return 2; + } else if ("303".equals(workerStatus)) { + return 1; + } + return 0; + } + + public String dateToString(Date date, String parrseFormat) { + if (null == date) { + return ""; + } + if (StringUtils.isEmpty(parrseFormat)) { + parrseFormat = "yyyy-MM-dd"; + } + SimpleDateFormat formatter = new SimpleDateFormat(parrseFormat); + return formatter.format(date); + } + + public void fillRoster(FtbPersonnelsStaffRoster updateEntity, SubFormFieldDto subFormFieldDto) { + String userValue = subFormFieldDto.getUserValue(); + + if ("workerName".equals(subFormFieldDto.getId())) { + updateEntity.setName(userValue); + } else if ("workerNo".equals(subFormFieldDto.getId())) { + updateEntity.setWorkerNo(userValue); + } else if ("currOrg".equals(subFormFieldDto.getId())) { + updateEntity.setCurrOrg(userValue); + } else if ("currPosition".equals(subFormFieldDto.getId())) { + updateEntity.setCurrPosition(userValue); + } else if ("currRank".equals(subFormFieldDto.getId())) { + updateEntity.setCurrRank(userValue); + } else if ("currShopId".equals(subFormFieldDto.getId())) { + updateEntity.setCurrShopId(userValue); + } else if ("entrySalary".equals(subFormFieldDto.getId())) { + updateEntity.setEntrySalary(new BigDecimal(userValue)); + } else if ("currReportsTo".equals(subFormFieldDto.getId())) { + updateEntity.setCurrReportsTo(userValue); + } else if ("contractEffectiveDate".equals(subFormFieldDto.getId()) && !StringUtils.isEmpty(userValue)) { + updateEntity.setContractEffectiveDate(DateUtil.parse(userValue, "yyyy-MM-dd")); + } else if ("actualProbationaryDate".equals(subFormFieldDto.getId()) && !StringUtils.isEmpty(userValue)) { + updateEntity.setActualProbationaryDate(DateUtil.parse(userValue, "yyyy-MM-dd")); + } else if ("contractEndDate".equals(subFormFieldDto.getId()) && !StringUtils.isEmpty(userValue)) { + updateEntity.setContractEndDate(DateUtil.parse(userValue, "yyyy-MM-dd")); + } else if ("workerType".equals(subFormFieldDto.getId())) { + updateEntity.setWorkerType(userValue); + } else if ("contractType".equals(subFormFieldDto.getId())) { + updateEntity.setContractType(userValue); + } else if ("workerStatus".equals(subFormFieldDto.getId())) { + updateEntity.setWorkerStatus(userValue); + } else if ("currRole".equals(subFormFieldDto.getId())) { + updateEntity.setCurrRole(userValue); + } else if ("actualStartDate".equals(subFormFieldDto.getId()) && !StringUtils.isEmpty(userValue)) { + updateEntity.setActualStartDate(DateUtil.parse(userValue, "yyyy-MM-dd")); + } + + } + + /** + * 查询用户角色 + * + * @param userId + * @return + */ + public List queryRoleListForUserId(String userId) { + if (StringUtils.isEmpty(userId)) { + return new ArrayList<>(); + } + List roleList = roleApi.getByUserId(userId); + if (CollectionUtil.isEmpty(roleList)) { + return new ArrayList<>(); + } + return BeanUtil.copyToList(roleList, SaffRoleDto.class); + } + + /** + * 查询用户角色 + * + * @param userId + * @return + */ + public List queryRoleListForUserIdNoToken(String userId, String tenantId) { + if (StringUtils.isEmpty(userId)) { + return new ArrayList<>(); + } + List roleList = roleApi.getByUserIdNoToken(userId, tenantId); + if (CollectionUtil.isEmpty(roleList)) { + return new ArrayList<>(); + } + return BeanUtil.copyToList(roleList, SaffRoleDto.class); + } + + public Map> queryRoleListForUserIds(List userIds) { + if (CollectionUtil.isEmpty(userIds)) { + return new HashMap<>(); + } + ActionResult> listActionResult = roleApi.listUsersBound(userIds); + if (listActionResult == null || CollectionUtil.isEmpty(listActionResult.getData())) { + return new HashMap<>(); + } + + return listActionResult.getData() + .stream() + .collect(Collectors.toMap(UserBasicVO::getId, UserBoundRolesVO::getRoleBounds, (o1, o2) -> o1)); + } + + + /** + * 查询角色信息 + * + * @param id + * @return + */ + public RoleEntity queryRoleInfoForId(String id) { + RoleEntity roleEntity = roleApi.getInfoById(id); + return roleEntity; + } + + /** + * 同步状态到内推池 + * + * @param phone + * @param status + */ + public void sysWorkerStatusToUchisuike(String phone, String status) { + ftbPersonnelsUchisuikePondService.updateWorkStatus(status, phone); + } + + public Map getOrgNames(List ids) { + if (CollUtil.isEmpty(ids)) { + return Maps.newHashMap(); + } + ids = ids.stream().distinct().collect(Collectors.toList()); + List organizeName = organizeApi.getOrganizeName(ids); + return CollUtil.isEmpty(organizeName) ? Maps.newHashMap() : + organizeName.stream() + .collect(Collectors.toMap(OrganizeEntity::getId, + OrganizeEntity::getFullName, + (a, b) -> a)); + } + + public Map getPostNames(List ids) { + if (CollUtil.isEmpty(ids)) { + return Maps.newHashMap(); + } + ids = ids.stream().distinct().collect(Collectors.toList()); + List positionEntities = positionApi.getPositionName(ids); + return CollUtil.isEmpty(positionEntities) ? Maps.newHashMap() : + positionEntities.stream() + .collect(Collectors.toMap(PositionEntity::getId, + PositionEntity::getFullName, + (a, b) -> a)); + } + + public Map getGradesNames(List ids) { + if (CollUtil.isEmpty(ids)) { + return Maps.newHashMap(); + } + ids = ids.stream().distinct().collect(Collectors.toList()); + List gradesEntityList = positionApi.getGradesEntityList(ids); + return CollUtil.isEmpty(gradesEntityList) ? Maps.newHashMap() : + gradesEntityList.stream() + .collect(Collectors.toMap(BasePositionGradesEntity::getId, + BasePositionGradesEntity::getFullName, + (a, b) -> a)); + } + + public Map getRoleNames(List ids) { + if (CollUtil.isEmpty(ids)) { + return Maps.newHashMap(); + } + ids = ids.stream().distinct().collect(Collectors.toList()); + List listByIds = roleApi.getListByIds(ids); + return CollUtil.isEmpty(listByIds) ? Maps.newHashMap() : + listByIds.stream() + .collect(Collectors.toMap(RoleEntity::getId, + RoleEntity::getFullName, + (a, b) -> a)); + } + + public Map getUserNames(List ids) { + if (CollUtil.isEmpty(ids)) { + return Maps.newHashMap(); + } + ids = ids.stream().distinct().collect(Collectors.toList()); + List userName = userApi.getUserName(ids); + return CollUtil.isEmpty(userName) ? Maps.newHashMap() : + userName.stream() + .collect(Collectors.toMap(UserEntity::getId, + UserEntity::getRealName, + (a, b) -> a)); + } + + public void fillOtherInfoForItem(TransferPositionDto info) { + OrganizeEntity organizeEntity = queryOrganizeInfo(info.getFromOrg()); + if (null != organizeEntity) { + info.setFromOrgName(organizeEntity.getFullName()); + } + organizeEntity = queryOrganizeInfo(info.getToOrg()); + if (null != organizeEntity) { + info.setToOrgName(organizeEntity.getFullName()); + } + + PositionEntity positionEntity = queryPosition(info.getFromPositionId()); + if (null != positionEntity) { + info.setFromPositionIdName(positionEntity.getFullName()); + } + positionEntity = queryPosition(info.getToPositionId()); + if (null != positionEntity) { + info.setToPositionIdName(positionEntity.getFullName()); + } + + PositionGradesInfoVO rankVo = queryRank(info.getFromRankId()); + if (null != rankVo) { + info.setFromRankIdName(rankVo.getFullName()); + } + rankVo = queryRank(info.getToRankId()); + if (null != rankVo) { + info.setToRankIdName(rankVo.getFullName()); + } + List roleEntities = queryRoleListForIds(info.getToRole()); + + if (CollectionUtil.isNotEmpty(roleEntities)) { + List roleNames = roleEntities.stream() + .filter(Objects::nonNull) + .map(RoleEntity::getFullName) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + info.setToRoleName(String.join(",", roleNames)); + } + + UserEntity userEntity = queryUserInfo(info.getToManager()); + if (null != userEntity) { + info.setToManagerName(userEntity.getRealName()); + } + userEntity = queryUserInfo(info.getFromManager()); + if (null != userEntity) { + info.setFromManagerName(userEntity.getRealName()); + } + } + + private List queryRoleListForIds(String toRole) { + if (StringUtils.isEmpty(toRole)) { + return new ArrayList<>(); + } + List list = Arrays.asList(toRole.split(",")); + return roleApi.getListByIds(list); + } + + /** + * 检测手机号是否正确 + * + * @param phoneNumber + * @return + */ + public static boolean isValidPhoneNumber(String phoneNumber) { + if (phoneNumber == null || phoneNumber.isEmpty()) { + return false; + } + Pattern pattern = Pattern.compile(RegexPool.MOBILE); + Matcher matcher = pattern.matcher(phoneNumber); + return matcher.matches(); + } + + /** + * 变更组织岗位职等 直属主管 + * 把旧的 ->新的 + * + * @param updateUserManagerBoundDTO + */ + + public void updateSysUserBound(UpdateUserManagerBoundDTO updateUserManagerBoundDTO) { + try { + Boolean aBoolean = userApi.UserManagerBoundChange(List.of(updateUserManagerBoundDTO)); + log.error("正常日志,updateSysUserBound req={},result={}", JSONUtil.toJsonStr(updateUserManagerBoundDTO), aBoolean); + if (aBoolean == false) { + throw new RuntimeException("调整用户组织架构信息失败"); + } + } catch (Exception e) { + throw new RuntimeException("调整用户组织架构信息失败"); + } + } + + public List> groupByBatch(List rosterDataList) { + List> groupedData = new ArrayList<>(); + int batchSize = 100; + + for (int i = 0; i < rosterDataList.size(); i += batchSize) { + int end = Math.min(i + batchSize, rosterDataList.size()); + groupedData.add(new ArrayList<>(rosterDataList.subList(i, end))); + } + + return groupedData; + } + + public List convertToFtbRosterImportTemplateBO(List fieldList, + List optionList, + List rosterDataList) { + if (CollectionUtil.isEmpty(rosterDataList)) { + return new ArrayList<>(); + } + for (ExportFormTypeDto field : fieldList) { + List fieldDtoList = field.getFieldDtoList(); + if (CollectionUtil.isEmpty(fieldDtoList)) { + continue; + } + List newField = new ArrayList<>(); + for (ExportFormFieldDto dto : fieldDtoList) { + if (!excludeExportFields.contains(dto.getId())) { + newField.add(dto); + } + } + field.setFieldDtoList(newField); + } + + //分组 + List> allList = groupByBatch(rosterDataList); + // 主线程中获取 + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + //多线程调用 convertToFtbRosterImportTemplateBatch 这个方法 + List>> futures = allList.stream() + .map(rosterData -> CompletableFuture.supplyAsync(() -> { + try { + return convertToFtbRosterImportTemplateBatch(fieldList, optionList, rosterData, requestAttributes, tenantId, headers); + } finally { + FeignHolder.clear(); + } + }, rosterExportThreadPool.getExecutor())) + .collect(Collectors.toList()); + //搜集结果并返回 + return futures.stream() + .map(CompletableFuture::join) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + public List convertToFtbRosterImportTemplateBatch(List allFields, List optionList, List rosterDataList, RequestAttributes requestAttributes, String tenantCode, Map headers) { + log.info("requestAttr={},tenantId={}", JSONUtil.toJsonStr(requestAttributes), tenantCode); + FeignHolder.set(headers); + try { + TenantDataSourceUtil.switchTenant(tenantCode); + } catch (LoginException e) { + throw new RuntimeException(e); + } finally { + FeignHolder.clear(); + } + List userIds = rosterDataList.stream() + .map(a -> a.getRoster().getUserId()) + .collect(Collectors.toList()); + + List attendanceUserGroup = attendanceGroupService.getAttendanceUserGroup(userIds); + Map attendanceUserGroupVoMap = attendanceUserGroup.stream() + .collect(Collectors.toMap(AttendanceUserGroupVo::getUserId, Function.identity(), (a, b) -> a)); + + Map> roleDtoMap = queryRoleListForUserIds(userIds); + Map> workerGroupDataDtoMap = getUserOrgBoundInfoForUserList(userIds, true); + Map optionMap = optionList.stream() + .collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, Function.identity(), (a, b) -> a)); + //构建表头 + Map contactNameMap = new HashMap<>();//合同ID 到合同名称 缓存 + List list = new ArrayList<>(); + for (ExportFormTypeDto formType : allFields) { + FtbRosterImportTemplateBO bo = new FtbRosterImportTemplateBO(); + list.add(bo); + //header头 + List> header = new ArrayList<>(); + header.add(List.of("姓名")); + header.add(List.of("手机号")); + List fieldList = formType.getFieldDtoList(); + for (ExportFormFieldDto field : fieldList) { + if ("phone".equals(field.getId()) || "workerName".equals(field.getId())) { + continue; + } + if (StringUtils.isNotEmpty(field.getGroupName())) { + continue; + } + if ("currOrg".equals(field.getId())) { + header.add(List.of("组织、岗位、职等、直属主管、门店")); + } else if ("currPosition".equals(field.getId())) { + // header.add(List.of("所属岗位")); + } else if ("currRank".equals(field.getId())) { + // header.add(List.of("所属职等")); + } else if ("currReportsTo".equals(field.getId())) { + // header.add(List.of("直属主管")); + } else if ("currShopId".equals(field.getId())) { + // header.add(List.of("直属主管")); + } else { + header.add(List.of(field.getName())); + } + } + bo.setHeader(header); + //sheetName + bo.setSheetName(formType.getName()); + //构建数据 + List> data = new ArrayList<>(); + + for (ExportRosterOneVo exportRosterOneVo : rosterDataList) { + FtbPersonnelsStaffRoster roster = exportRosterOneVo.getRoster(); + List formValueList = exportRosterOneVo.getFormValue(); + //查询用户 + List workerGroupDataDtoList = workerGroupDataDtoMap.get(roster.getUserId()); + //查询角色 + List roleDtoList = roleDtoMap.get(roster.getUserId()); + Map valueMap = convertFormValueMap(formValueList); + List exportDataList = new ArrayList<>(); + exportDataList.add(roster.getName()); + exportDataList.add(roster.getPhone()); + for (ExportFormFieldDto field : fieldList) { + String showValue = ""; + if ("phone".equals(field.getId()) || "workerName".equals(field.getId())) { + continue; + } + if (StringUtils.isNotEmpty(field.getGroupName())) { + continue; + } + if ("currRole".equals(field.getId())) { + if (CollectionUtil.isNotEmpty(roleDtoList)) { + List tmpString = new ArrayList<>(); + for (RoleListVO saffRoleDto : roleDtoList) { + try { + Field declaredField = RoleListVO.class.getDeclaredField("fullName"); + declaredField.setAccessible(true); + String fullName =(String) declaredField.get(saffRoleDto); + tmpString.add(fullName); + } catch (NoSuchFieldException | IllegalAccessException e ) { + throw new RuntimeException(e); + } + } + showValue = String.join(", ", tmpString); + } + exportDataList.add(showValue); + continue; + } else if ("currOrg".equals(field.getId())) { + if (CollectionUtil.isNotEmpty(workerGroupDataDtoList)) { + List tmpString = new ArrayList<>(); + for (int i = 0; i < workerGroupDataDtoList.size(); i++) { + WorkerGroupDataDto workerGroupDataDto = workerGroupDataDtoList.get(i); + StringBuilder sb = new StringBuilder(); + sb.append("["); + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedOrgName())) { + sb.append(workerGroupDataDto.getAffiliatedOrgName()); + } + + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedPositionName())) { + sb.append("、"); + sb.append(workerGroupDataDto.getAffiliatedPositionName()); + } + + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedRankName())) { + sb.append("、"); + sb.append(workerGroupDataDto.getAffiliatedRankName()); + } + + + if (StringUtils.isNotEmpty(workerGroupDataDto.getReportsToName())) { + sb.append("、"); + sb.append(workerGroupDataDto.getReportsToName()); + } + + if (StringUtils.isNotEmpty(workerGroupDataDto.getAffiliatedShopName())) { + sb.append("、"); + sb.append(workerGroupDataDto.getAffiliatedShopName()); + } + sb.append("]"); + tmpString.add(sb.toString()); + } + showValue = String.join("\r\n", tmpString); + } + exportDataList.add(showValue); + continue; + } else if ("currPosition".equals(field.getId())) { + continue; + } else if ("currRank".equals(field.getId())) { + continue; + } else if ("currReportsTo".equals(field.getId())) { + continue; + } else if ("currShopId".equals(field.getId())) { + continue; + } + + FtbPersonnelsStaffRegistrationFormData formFieldValue = valueMap.get(field.getId()); + FtbPersonnelsRegistrationFormFieldOption optionEntity = null; + if (null != formFieldValue) { + if (field.getType() == 2 || field.getType() == 3) { + + if (StringUtils.isNotEmpty(formFieldValue.getValue())) { + if ("probationPeriod".equals(field.getId()) && formFieldValue.getValue().startsWith("{")) { + ProbationPeriodDto bean = JSONUtil.toBean(formFieldValue.getValue(), ProbationPeriodDto.class); + String key = bean.getType(); + if ("101".equals(key)) { + showValue = bean.getDays() + "天"; + } else { + optionEntity = optionMap.get(key); + if (null != optionEntity) { + showValue = optionEntity.getName(); + } + } + } else if ("contractType".equals(field.getId())) { + + String contactName = contactNameMap.get(formFieldValue.getValue()); + if (StringUtils.isNotEmpty(contactName)) { + showValue = contactName; + } else { + String contractTypeName = queryContractTypeName(formFieldValue.getValue()); + showValue = contractTypeName; + contactNameMap.put(formFieldValue.getValue(), contractTypeName); + } + + } else if ("attendanceGroup".equals(field.getId())) { + AttendanceUserGroupVo attendanceUserGroupVo = attendanceUserGroupVoMap.get(roster.getUserId()); + if (attendanceUserGroupVo != null) { + showValue = attendanceUserGroupVo.getGroupName(); + } + } else { + if (field.getType() == 3) { + showValue = Stream.of(formFieldValue.getValue().split(",")) + .map(optionMap::get) + .filter(Objects::nonNull) + .map(FtbPersonnelsRegistrationFormFieldOption::getName) + .collect(Collectors.joining(",")); + + } else { + optionEntity = optionMap.get(formFieldValue.getValue()); + if (null != optionEntity) { + showValue = optionEntity.getName(); + } + } + } + } + } else { + if ("workAddress".equals(field.getId())) { + if (formFieldValue.getValue().startsWith("{")) { + WorkAddressDto bean = JSONUtil.toBean(formFieldValue.getValue(), WorkAddressDto.class); + showValue = bean.getAddress(); + } + } else { + showValue = formFieldValue.getValue(); + } + } + } + if (PersonnelFormDataSystemRosterFields.CONTRACT_STATUS.getId().equals(field.getId())) { + + if (formFieldValue != null && StringUtils.isNotEmpty(formFieldValue.getValue())) { + optionEntity = optionMap.get(formFieldValue.getValue()); + if (null != optionEntity) { + showValue = optionEntity.getName(); + } + } else { + optionEntity = optionMap.get(roster.getContractStatus().toString()); + if (null != optionEntity) { + showValue = optionEntity.getName(); + } + } + } + exportDataList.add(showValue); + + } + data.add(exportDataList); + } + bo.setData(data); + } + return list; + } + + /** + * 删除租户用户 + * + * @param userId + */ + + public void deleteTenantUser(String userId) { + v2UserApi.removeUser(userId, null); + } + + /** + * 根据手机号查询账号 + * + * @param account + * @param phone + * @return + */ + public UserInfoVO queryTenantAccount(String account, String phone) { + UserInfoVO info = userApi.getUserInfoByAccount(account, phone); + return info; + } + + /** + * 检测用户是不是直属主管 + * + * @param userId + * @return true 是, false 不是, + */ + public Boolean checkUserIsManager(String userId) { + return userApi.checkUserIsManager(userId); + } + + /** + * 查询是否是门店负责人 + * + * @return + */ + public Boolean queryShopManager(String userId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + //queryWrapper.eq(StoreEntity::getDisabled,0); + queryWrapper.eq(StoreEntity::isDeletemark,0); + queryWrapper.eq(StoreEntity::getStoreheaduserid,userId); + List entities = storeMapper.selectList(queryWrapper); + if (CollUtil.isNotEmpty(entities)) { + return true; + } + return false; + } + + public void fillRankNameById(T dto) { + ActionResult gradesInfo = null; + try { + gradesInfo = positionApi.getGradesInfo(dto.getCurrRank()); + if (gradesInfo != null && gradesInfo.getData() != null) { + dto.setCurrRankName(gradesInfo.getData().getFullName()); + } + } catch (Exception e) { + log.error("根据职等ID查询职等信息异常,rankId={},{}", dto.getCurrRank(), e); + } + + } + + public void fillPositionNameById(T dto) { + PositionEntity entity = positionApi.queryInfoById(dto.getCurrPosition()); + if (entity != null) { + dto.setCurrPositionName(entity.getFullName()); + } + } + + public void fillOrgNameById(T dto) { + OrganizeEntity entity = organizeApi.getInfoById(dto.getCurrOrg()); + if (null != entity) { + dto.setCurrOrgName(entity.getFullName()); + } + } + + + public void fillContactInfo(FtbPersonnelsStaffRosterDto dto) { + if (StringUtils.isNotEmpty(dto.getContractType())) { + String contractTypeName = queryContractTypeName(dto.getContractType()); + dto.setContractTypeName(contractTypeName); + } + } + + public void fillCurrRankName(List records) { + List list = records.stream().map(StaffBaseInfoDto::getCurrRank).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + ActionResult> listActionResult = v2GradesApi.listGradeByIds(list,null); + if (listActionResult == null || CollUtil.isEmpty(listActionResult.getData()) ) { + return; + } + Map gradeVOMap = listActionResult.getData().stream().collect(Collectors.toMap(GradeVO::getId, Function.identity(), (v1, v2) -> v1)); + records.forEach( + item -> { + if (StringUtils.isNotEmpty(item.getCurrRank())) { + GradeVO gradeVO = gradeVOMap.get(item.getCurrRank()); + if (null != gradeVO) { + item.setCurrRankName(gradeVO.getFullName()); + } + } + } + ); + } + + public void fillCurrPositionName(List list) { + //获取组织id列表 + List ids = new ArrayList<>(); + list.forEach(item -> { + if (StringUtils.isNotBlank(item.getCurrPosition())) { + ids.add(item.getCurrPosition()); + } + }); + + ActionResult> listActionResult = v2PositionApi.listPositionDetailInfo(ids); + if (listActionResult == null && CollectionUtil.isEmpty(listActionResult.getData())) { + return; + } + Map listMap = listActionResult.getData().stream().collect(Collectors.toMap(PositionVO::getId, Function.identity())); + for (FtbPersonnelsStaffRosterDto dto : list) { + PositionVO entity = listMap.get(dto.getCurrPosition()); + if (null == entity) { + continue; + } + dto.setCurrPositionName(entity.getFullName()); + } + } + + + public List queryPostionInfoForIds(List ids) { + + List entityLists = positionApi.getPositionName(ids); + if (CollectionUtil.isEmpty(entityLists)) { + return new ArrayList<>(); + } + return entityLists; + } + + + public void fillCurrOrgName(List list) { + //获取组织id列表 + List ids = list.stream().map(FtbPersonnelsStaffRosterDto::getCurrOrg).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + List currGroupId = list.stream().map(FtbPersonnelsStaffRosterDto::getCurrGroupId).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + ids.addAll(currGroupId); + ActionResult> actionResult = v2OrganizeApi.organizesByOrganizeIds(ids); + if ( actionResult == null || CollectionUtil.isEmpty(actionResult.getData())) { + return; + } + Map listMap = actionResult.getData().stream().collect(Collectors.toMap(OrganizeGeneralDetailVO::getId, Function.identity())); + for (FtbPersonnelsStaffRosterDto dto : list) { + if (listMap.containsKey(dto.getCurrGroupId())){ + dto.setCurrGroupName(listMap.get(dto.getCurrGroupId()).getName()); + } + if (listMap.containsKey(dto.getCurrOrg())){ + dto.setCurrOrgName(listMap.get(dto.getCurrOrg()).getName()); + } + } + } + + + public void fillReportsToInfo(List list) { + List ids = new ArrayList<>(); + for (FtbPersonnelsStaffRosterDto dto : list) { + if (StringUtils.isNotEmpty(dto.getCurrReportsTo())) { + ids.add(dto.getCurrReportsTo()); + } + } + + Map userMap = new HashMap<>(); + if (CollectionUtil.isNotEmpty(ids)) { + List userEntityList = userApi.getUserName(ids); + if (CollectionUtil.isNotEmpty(userEntityList)) { + //按照用户ID 转成map + userMap = userEntityList.stream().collect(Collectors.toMap(UserEntity::getId, userEntity -> userEntity)); + } + } + for (FtbPersonnelsStaffRosterDto vo : list) { + UserEntity userEntity = userMap.get(vo.getCurrReportsTo()); + if (userEntity != null) { + vo.setCurrReportsToName(userEntity.getRealName()); + } + } + } + + /** + * 根据用户ID查询用户列表 + * + * @param userIds + * @return + */ + public List queryUserInfoByIds(List userIds) { + List userEntityList = userApi.getUserName(userIds); + if (CollectionUtil.isNotEmpty(userEntityList)) { + return userEntityList; + } + return new ArrayList<>(); + } + + /** + * 是否是最高组织 + * + * @param currOrg + * @return false 不是,true 是最高组织 + */ + + public Boolean checkIsTopOrg(String currOrg) { + OrganizeEntity organizeEntity = queryOrganizeInfo(currOrg); + if (null == organizeEntity) { + log.error("未查询到组织的相关信息,currOrg={}", currOrg); + throw new RuntimeException("未查询到组织的相关信息"); + } + if (StringUtils.isEmpty(organizeEntity.getParentId()) || "-1".equals(organizeEntity.getParentId())) { + return true; + } else { + return false; + } + } + + /** + * 同步用户头像 + * + * @param userId + * @param inputHeadLogo + */ + public void sysUserHeadLog(String userId, String inputHeadLogo) { + + SaveTenantUserForm saveTenantUserForm = new SaveTenantUserForm(); + saveTenantUserForm.setId(userId); + saveTenantUserForm.setHeadIcon(inputHeadLogo); + pTenantAccountApi.userInfoSynchronous(saveTenantUserForm); + } + + public Boolean checkStoreUser(String userId) { + Integer integer = storeMapper.checkStoreUser(userId); + if (integer > 0) { + return true; + } else { + return false; + } + } + + + public List queryAllUserForOrgAndStatus(QueryUserListDTO req) { + return userApi.getAllUser(req); + } + + public void rollbackTenantUser(List addUserList) { + if (CollectionUtil.isEmpty(addUserList)) { + return; + } + for (UserAccountDto userAccountDto : addUserList) { + personnelOrgUtils.deleteTenantUser(userAccountDto.getFid()); + } + } + + public ActionResult> getPartUserInfoPage(UserQueryDto queryDto) { + return ftbApi.getPartUserInfoPage(queryDto); + } + + public List getAllRoleForOrgIds(List orgList) { + ActionResult> listActionResult = roleApi.partAllList(orgList); + if (null == listActionResult || CollectionUtil.isEmpty(listActionResult.getData())) { + return new ArrayList<>(); + } + return listActionResult.getData(); + } + + public List getRoleInfoForIds(List subRole) { + return roleApi.getListByIds(subRole); + + } + + public List queryTenantAccountForPhoneList(List phoneList) { + if (CollectionUtil.isEmpty(phoneList)) { + return new ArrayList<>(); + } + ActionResult> userInfoByPhones = userApi.getUserInfoByPhones(phoneList); + if (null == userInfoByPhones) { + return new ArrayList<>(); + } + return userInfoByPhones.getData(); + } + + public void sysTenantAccountBase(SaveUserManagerAddDTO dto) { + + if (StringUtils.isNotEmpty(dto.getHeadIcon())) { + int lastIndex = dto.getHeadIcon().lastIndexOf('/'); + String fileName = dto.getHeadIcon().substring(lastIndex + 1); + dto.setHeadIcon(fileName); + } + UpdateUserDTO userDTO = new UpdateUserDTO(); + userDTO.setId(dto.getUserId()); + userDTO.setAccount(dto.getAccount()); + userDTO.setRealName(dto.getRealName()); + userDTO.setNickName(dto.getNickName()); + userDTO.setHeadIcon(dto.getHeadIcon()); + userDTO.setMobilePhone(dto.getMobilePhone()); + userDTO.setWorkerStatus(dto.getWorkerStatus()); + userDTO.setHeadIcon(dto.getHeadIcon()); + userDTO.setBecomeDate(dto.getBecomeDate()); + userDTO.setDepartDate(dto.getDepartDate()); + v2UserApi.updateUserInfo(dto.getUserId(),userDTO); + } + + public void batchSysTenantAccountBase(List list) { + for (SaveUserManagerAddDTO dto : list) { + sysTenantAccountBase(dto); + } + } + + public void sysTenantAccountBaseNoLogin(SaveUserManagerAddDTO dto, String tenantId) { + + if (StringUtils.isNotEmpty(dto.getHeadIcon())) { + int lastIndex = dto.getHeadIcon().lastIndexOf('/'); + String fileName = dto.getHeadIcon().substring(lastIndex + 1); + dto.setHeadIcon(fileName); + } + log.error("正常日志:sysTenantAccountBase nologin={}", JSONUtil.toJsonStr(dto)); + Boolean aBoolean = userApi.userInfoUpdateNoToken(dto, tenantId); + } + + /** + * 同步用户考勤组 + * + * @param dto + */ + public void syncAttendanceGroup(GroupUpdateByUserDTO dto) { + ActionResult actionResult = attendanceUserApi.groupUpdateByPersonnel(dto); + if (actionResult == null) { + log.error("考勤组变更失败,req={},result=null", JSONUtil.toJsonStr(dto)); + } + } + + /** + * 查询用户考勤组 + * + * @param userId + */ + public List queryAttendanceGroup(String userId) { + List attendanceUserGroup = attendanceGroupApi.getAttendanceUserGroup(List.of(userId)); + if (CollectionUtil.isEmpty(attendanceUserGroup)) { + return new ArrayList<>(); + } + return attendanceUserGroup; + } + + public String getTenantId() { + UserInfo userInfo = UserProvider.getUser(); + return userInfo.getTenantId(); + } + + public Map getHeadersForLogin() { + UserInfo userInfo = UserProvider.getUser(); + String token = userInfo.getToken(); + if (StringUtils.isEmpty(token)) { + token = ""; + } + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + return headers; + } + + public Integer calMonth(ProbationPeriodDto probationPeriodDto) { + int month = 0; + if (probationPeriodDto.getType().equals("101")) { + month = 1; + } else if (probationPeriodDto.getType().equals("102")) { + month = 1; + } else if (probationPeriodDto.getType().equals("103")) { + month = 2; + } else if (probationPeriodDto.getType().equals("104")) { + month = 3; + } else if (probationPeriodDto.getType().equals("105")) { + month = 4; + } else if (probationPeriodDto.getType().equals("106")) { + month = 5; + } else if (probationPeriodDto.getType().equals("107")) { + month = 6; + } else if (probationPeriodDto.getType().equals("108")) { + month = 7; + } else if (probationPeriodDto.getType().equals("109")) { + month = 8; + } + return month; + } + + public void updateBaseInfoForBaseUser(FtbPersonnelsStaffRoster entity) { + //同步姓名 花名册等信息到组织架构 + Boolean isUpdateTenant = false; + SaveUserManagerAddDTO dto = new SaveUserManagerAddDTO(); + dto.setUserId(entity.getUserId()); + dto.setRealName(entity.getName()); + + if (entity.getActualProbationaryDate() != null) { + dto.setBecomeDate(entity.getActualProbationaryDate()); + isUpdateTenant = true; + } + if (entity.getActualStartDate() != null) { + dto.setEntryDate(entity.getActualStartDate()); + isUpdateTenant = true; + } + if (isUpdateTenant) { + try { + sysTenantAccountBase(dto); + } catch (Exception e) { + log.error("入职同步组织架构失败", e); + } + } + } + + public String convertProbationPeriod(ProbationPeriodDto bean) { + if (bean == null) { + return ""; + } + if ("100".equals(bean.getType())) { + return "无试用期"; + } else if ("101".equals(bean.getType())) { + return "1个月内" + "(" + bean.getDays() + "天)"; + } else if ("102".equals(bean.getType())) { + return "1个月"; + } else if ("103".equals(bean.getType())) { + return "2个月"; + } else if ("104".equals(bean.getType())) { + return "3个月"; + } else if ("105".equals(bean.getType())) { + return "4个月"; + } else if ("106".equals(bean.getType())) { + return "5个月"; + } else if ("107".equals(bean.getType())) { + return "6个月"; + } else if ("108".equals(bean.getType())) { + return "7个月"; + } else if ("109".equals(bean.getType())) { + return "8个月"; + } + return ""; + } + + public int calculateAge(LocalDate birthDate) { + LocalDate today = LocalDate.now(); + Period period = Period.between(birthDate, today); + return period.getYears(); + } + + /** + * 将 java.util.Date 转换为 java.time.LocalDate。 + * + * @param date 日期对象 + * @return LocalDate 对象 + */ + public LocalDate dateToLocalDate(Date date) { + return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + } + + public Map queryParentOrgInfo(Set allOrgIds, String tenantCode) { + return organizeApi.batchQueryParentOrgInfo(new ArrayList<>(allOrgIds),tenantCode); + } + + + public String getLeaderInfo(@NotBlank(message = "原组织不能为空") String fromOrg, + String userId, + @NotBlank(message = "原岗位不能为空") String fromPositionId, + @NotBlank(message = "原职等不能为空") String fromRankId) { + UserInfoVO leaderInfo = userApi.getLeaderInfo(fromOrg, userId, fromPositionId, fromRankId); + return leaderInfo != null ? leaderInfo.getId() : null; + } +} + + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPerUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPerUtils.java new file mode 100644 index 0000000..0931066 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPerUtils.java @@ -0,0 +1,418 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.cultivate.utils.AsyncExamQuestionUtils; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.model.personnels.dto.authoritys.PermissionsCacheDTO; +import jnpf.model.personnels.po.FtbPersonnelsPermissions; +import jnpf.model.personnels.vo.authoritys.FtbPermissionUserVO; +import jnpf.permission.OrganizeApi; +import jnpf.permission.UserApi; +import jnpf.permission.dto.user.DecideUserUnderlingDTO; +import jnpf.permission.model.user.SubordinateUserInfoVO; +import jnpf.personnels.mapper.FtbPersonnelsPermissionsMapper; +import jnpf.util.UserProvider; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + + +/** + * 人事权限工具类 + * + * @author fantaibao + * @date 2024/02/06 + */ +@Slf4j +@Component +public class PersonnelPerUtils { + + @Resource + private FtbPersonnelsPermissionsMapper ftbPersonnelsPermissionsMapper; + + @Autowired + private UserApi userApi; + + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private AsyncExamQuestionUtils asyncExamQuestionUtils; + + /** + * 获取培训批阅人id + * + * @return {@link String} + */ + @Nullable + public String obtainTrainingApproverPermissions(String userId) { + return doObtainTrainUserPermissions(List.of("70"), userId); + } + + /** + * 获取培训审核案例人所具有的用户id + * + * @return {@link String} 用户id,逗号隔开 + */ + @Nullable + public String obtainReviewCasePermissions(String userId) { + return doObtainTrainUserPermissionsSpecify(List.of("135"), userId, 1); + } + + /** + * 获取培训实操鉴定人id + * + * @return {@link String} + */ + @Nullable + public String obtainTrainedAppraiser(String userId) { + return doObtainTrainUserPermissions(List.of("71"), userId); + } + + /** + * 获取人事用户数据权限,返回为空,则表示为超级管理员(可以查询所有数据) + * + * @return {@link List} 用户id + */ + @Nullable + public List obtainPersonnelDataPermissions() { + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + return null; + } + List userIds = obtainPersonnelDataPermissions0(userInfo, 0); + if (CollectionUtils.isEmpty(userIds)) { + throw new RuntimeException("该用户暂未设置人事数据权限,请对该用户设置权限"); + } + return userIds; + } + + /** + * 获取人事用户数据权限,返回为空,则表示为超级管理员(可以查询所有数据) + * + * @return {@link List} 组织id + */ + @Nullable + public List obtainPersonnelOrganizationIdDataPermissions() { + UserInfo userInfo = UserProvider.getUser(); + if (userInfo.getIsAdministrator()) { + return null; + } + List orgIds = obtainPersonnelOrganizationIdDataPermissions0(userInfo, 0); + if (CollectionUtils.isEmpty(orgIds)) { + throw new RuntimeException("该用户暂未设置人事数据权限,请对该用户设置权限"); + } + return orgIds; + } + + /** + * 根据组织id获取该组织“办理入职”权限的人员 + * + * @return {@link List} 用户id + */ + @Nullable + public List obtainPersonnelUserIdDataPermissionsByOrgId(String orgId, String userId, String... tenantIds) { + // 办理入职 + List userVOS = ftbPersonnelsPermissionsMapper.queryFtbPermissionUserVoByAuthorityId(List.of("133", "134")); + String tenantId = tenantIds.length > 0 ? tenantIds[0] : null; + Predicate predicate = a -> { + if (a.getScopePermission() == 2) { + // 校验是否为下属关系 + DecideUserUnderlingDTO dto = new DecideUserUnderlingDTO(); + dto.setUserId(userId); + dto.setManagerId(a.getUserId()); + dto.setTenantId(tenantId); + ActionResult booleanActionResult = userApi.decideUnderling(dto); + if (booleanActionResult.getCode() == 200) { + return booleanActionResult.getData(); + } + } + // 所在组织和下级组织员工 + if (a.getScopePermission() == 0) { + return doCheckDataPermissionsByOrgId( + tenantId != null + ? organizeApi.getOrganizeIdsAdnChildByUserIdAndTenantId(a.getUserId(), tenantId) + : organizeApi.getOrganizeIdsAdnChildByUserId(a.getUserId()) + , orgId); + } + // 所在组织员工 + if (a.getScopePermission() == 1) { + return doCheckDataPermissionsByOrgId( + tenantId != null + ? organizeApi.getOrganizeIdsByUserIdAndTenantId(a.getUserId(), tenantId) + : organizeApi.getOrganizeIdsByUserId(a.getUserId()) + , orgId); + } + // 指定组织 + if (a.getScopePermission() == 3) { + return doCheckDataPermissionsByOrgId(List.of(a.getSpecifyOrgIds().split(StringPool.COMMA)), orgId); + } + return false; + }; + return userVOS.stream() + .filter(predicate) + .map(FtbPermissionUserVO::getUserId) + .collect(Collectors.toList()); + } + + private List obtainPersonnelOrganizationIdDataPermissions0(UserInfo userInfo, Integer permissionType) { + FtbPersonnelsPermissions ftbPersonnelsPermissions = doPermissionQuery(userInfo, permissionType); + if (Objects.isNull(ftbPersonnelsPermissions)) { + return null; + } + if (ftbPersonnelsPermissions.getScopePermission() == 2) { + throw new RuntimeException("该用户数据权限为仅下属,暂无权限办理或查看"); + } + // 所在组织和下级组织员工 + if (ftbPersonnelsPermissions.getScopePermission() == 0) { + return organizeApi.getOrganizeIdsAdnChildByUserId(userInfo.getUserId()); + } + // 所在组织员工 + if (ftbPersonnelsPermissions.getScopePermission() == 1) { + return organizeApi.getOrganizeIdsByUserId(userInfo.getUserId()); + } + // 指定组织 + if (ftbPersonnelsPermissions.getScopePermission() == 3) { + return List.of(ftbPersonnelsPermissions.getSpecifyOrgIds().split(StringPool.COMMA)); + } + return null; + } + + + private List obtainPersonnelDataPermissions0(UserInfo userInfo, Integer permissionType) { + FtbPersonnelsPermissions ftbPersonnelsPermissions = doPermissionQuery(userInfo, permissionType); + if (Objects.isNull(ftbPersonnelsPermissions)) { + return null; + } + return doGetUserIds(userInfo, ftbPersonnelsPermissions); + } + + @Nullable + private List doGetUserIds(UserInfo userInfo, FtbPersonnelsPermissions ftbPersonnelsPermissions) { + // 权限适用范围,0所在组织和下级组织员工,1所在组织员工,2仅下属,3指定组织 + if (ftbPersonnelsPermissions.getScopePermission() == 2) { + // List userSubordinateVOS = userApi.userSubordinateVOListByLeaderId(userInfo.getUserId()); + List subordinateUserInfoVOS = userApi.userInfoByLeaderId(userInfo.getUserId()); + if (CollectionUtils.isNotEmpty(subordinateUserInfoVOS)) { + return subordinateUserInfoVOS.stream() + .map(SubordinateUserInfoVO::getUserId) + .collect(Collectors.toList()); + }else { + throw new RuntimeException("暂未查询到下属用户信息!"); + } + } + if (ftbPersonnelsPermissions.getScopePermission() == 3) { + List orgIds = List.of(ftbPersonnelsPermissions.getSpecifyOrgIds().split(StringPool.COMMA)); + ActionResult> userIdListByOrganizeIds = userApi.getUserIdListByOrganizeIds(orgIds); + if (CollectionUtils.isEmpty(userIdListByOrganizeIds.getData())) { + throw new RuntimeException("暂未查询到指定组织员工!"); + } + return userIdListByOrganizeIds.getData().stream().distinct().collect(Collectors.toList()); + } + // 所在组织 + List orgIds = organizeApi.getOrganizeIdsByUserId(userInfo.getUserId()); + // 所在组织和下级组织员工 + if (ftbPersonnelsPermissions.getScopePermission() == 0) { + ActionResult> listActionResult = userApi.getUserIdListByOrganizeIdsAndChild(orgIds); + if (CollectionUtils.isEmpty(listActionResult.getData())) { + throw new RuntimeException("暂未查询到所在组织和下级组织员工!"); + } + return listActionResult.getData().stream().distinct().collect(Collectors.toList()); + } + // 所在组织员工 + if (ftbPersonnelsPermissions.getScopePermission() == 1) { + ActionResult> userIdListByOrganizeIds = userApi.getUserIdListByOrganizeIds(orgIds); + if (CollectionUtils.isEmpty(userIdListByOrganizeIds.getData())) { + throw new RuntimeException("暂未查询到所在组织员工!"); + } + return userIdListByOrganizeIds.getData().stream().distinct().collect(Collectors.toList()); + } + return null; + } + + private FtbPersonnelsPermissions doPermissionQuery(UserInfo userInfo, Integer permissionType) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + queryWrapper.eq(FtbPersonnelsPermissions::getPermissionType, permissionType); + queryWrapper.eq(FtbPersonnelsPermissions::getUserId, userInfo.getUserId()); + return ftbPersonnelsPermissionsMapper.selectOne(queryWrapper); + } + + @Nullable + private String doObtainTrainUserPermissions(List permissionIds, String userId) { + List reviewerPermissions = ftbPersonnelsPermissionsMapper.getReviewerPermissions(permissionIds); + if (CollectionUtils.isEmpty(reviewerPermissions)) { + return null; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, reviewerPermissions); + List ftbPersonnelsPermissions = ftbPersonnelsPermissionsMapper.selectList(queryWrapper); + if (CollectionUtils.isEmpty(ftbPersonnelsPermissions)) { + return null; + } + // 校验是否有关联关系 + List userIds = new ArrayList<>(); + // 查询用户所属组织做筛选 + List localOrganizeIds = organizeApi.getOrganizeIdsByUserId(userId); + for (FtbPersonnelsPermissions ftbPersonnelsPermission : ftbPersonnelsPermissions) { + if (ftbPersonnelsPermission.getScopePermission() == 2) { + // 通过人员去排查 + List subordinateUserInfoVOS = userApi.userInfoByLeaderId(ftbPersonnelsPermission.getUserId()); + if (CollectionUtils.isEmpty(subordinateUserInfoVOS)) { + continue; + } + List childrenUserIds = subordinateUserInfoVOS.stream() + .map(SubordinateUserInfoVO::getUserId) + .collect(Collectors.toList()); + if (childrenUserIds.contains(userId)) { + userIds.add(ftbPersonnelsPermission.getUserId()); + } + } + // 所在组织和下级组织员工,查询组织id进行比对 + List organizeIdsAdnChildByUserId = null; + if (ftbPersonnelsPermission.getScopePermission() == 0) { + organizeIdsAdnChildByUserId = organizeApi.getOrganizeIdsAdnChildByUserId(ftbPersonnelsPermission.getUserId()); + } + // 所在组织员工,查询组织id进行比对 + if (ftbPersonnelsPermission.getScopePermission() == 1) { + organizeIdsAdnChildByUserId = organizeApi.getOrganizeIdsByUserId(ftbPersonnelsPermission.getUserId()); + } + // 指定组织 + if (ftbPersonnelsPermission.getScopePermission() == 3) { + organizeIdsAdnChildByUserId = Arrays.asList(ftbPersonnelsPermission.getSpecifyOrgIds().split(StringPool.COMMA)); + } + if (haveIntersection(localOrganizeIds, organizeIdsAdnChildByUserId)) { + userIds.add(ftbPersonnelsPermission.getUserId()); + } + } + if (CollectionUtils.isNotEmpty(userIds)) { + return String.join(",", userIds); + } + return null; + } + + public PermissionsCacheDTO doQueryUserPermissions(List permissionIds) { + PermissionsCacheDTO permissionsCacheDTO = new PermissionsCacheDTO(); + List reviewerPermissions = ftbPersonnelsPermissionsMapper.getReviewerPermissions(permissionIds); + if (CollectionUtils.isEmpty(reviewerPermissions)) { + return permissionsCacheDTO; + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(FtbPersonnelsPermissions::getEnableMark, 0); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, reviewerPermissions); + List ftbPersonnelsPermissions = ftbPersonnelsPermissionsMapper.selectList(queryWrapper); + if (CollectionUtils.isEmpty(ftbPersonnelsPermissions)) { + return permissionsCacheDTO; + } + // 校验是否有关联关系 + ConcurrentHashMap> allUserListIdsMap = new ConcurrentHashMap<>(); + ConcurrentHashMap> allOrgListMap = new ConcurrentHashMap<>(); + String tenantId = asyncExamQuestionUtils.getTenantId(); + Map headers = asyncExamQuestionUtils.getHeadersForLogin(); + // 查询用户所属组织做筛选 + ftbPersonnelsPermissions.parallelStream().forEach(ftbPersonnelsPermission -> { + try { + FeignHolder.set(headers); + TenantDataSourceUtil.switchTenant(tenantId); + if (ftbPersonnelsPermission.getScopePermission() == 2) { + // 通过人员去排查 + List subordinateUserInfoVOS = userApi.userInfoByLeaderId(ftbPersonnelsPermission.getUserId()); + if (CollectionUtils.isEmpty(subordinateUserInfoVOS)) { + return; + } + List childrenUserIds = subordinateUserInfoVOS.stream() + .map(SubordinateUserInfoVO::getUserId) + .collect(Collectors.toList()); + if (CollectionUtils.isNotEmpty(childrenUserIds)) { + allUserListIdsMap.put(ftbPersonnelsPermission, childrenUserIds); + } + } + // 所在组织和下级组织员工,查询组织id进行比对 + List organizeIdsAdnChildByUserId = null; + if (ftbPersonnelsPermission.getScopePermission() == 0) { + organizeIdsAdnChildByUserId = organizeApi.getOrganizeIdsAdnChildByUserId(ftbPersonnelsPermission.getUserId()); + } + // 所在组织员工,查询组织id进行比对 + if (ftbPersonnelsPermission.getScopePermission() == 1) { + organizeIdsAdnChildByUserId = organizeApi.getOrganizeIdsByUserId(ftbPersonnelsPermission.getUserId()); + } + // 指定组织 + if (ftbPersonnelsPermission.getScopePermission() == 3) { + organizeIdsAdnChildByUserId = Arrays.asList(ftbPersonnelsPermission.getSpecifyOrgIds().split(StringPool.COMMA)); + } + if (CollectionUtils.isNotEmpty(organizeIdsAdnChildByUserId)) { + allOrgListMap.put(ftbPersonnelsPermission, organizeIdsAdnChildByUserId); + } + } catch (Exception e) { + log.error("多线程查询权限:tenantCode={}", tenantId); + } finally { + FeignHolder.clear(); + } + }); + + permissionsCacheDTO.setAllUserListIdsMap(allUserListIdsMap); + permissionsCacheDTO.setAllOrgListMap(allOrgListMap); + return permissionsCacheDTO; + } + + @Nullable + private String doObtainTrainUserPermissionsSpecify(List permissionIds, String userId, Integer permissionType) { + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + FtbPersonnelsPermissions ftbPersonnelsPermissions = doPermissionQuery(userInfo, permissionType); + if (Objects.isNull(ftbPersonnelsPermissions)) { + // 未对此人设置培训权限 + return null; + } + // 校验此用户是否设置了permissionIds的权限 + List reviewerPermissions = ftbPersonnelsPermissionsMapper.getReviewerPermissions(permissionIds); + if (CollectionUtils.isEmpty(reviewerPermissions)) { + return null; + } + if (reviewerPermissions.contains(ftbPersonnelsPermissions.getId())) { + List userIds = doGetUserIds(userInfo, ftbPersonnelsPermissions); + if (CollectionUtils.isEmpty(userIds)) { + return null; + } + return String.join(StringPool.COMMA, userIds); + } + return null; + } + + public boolean haveIntersection(List localOrganizeIds, List organizeIdsByUserId) { + if (CollectionUtils.isEmpty(localOrganizeIds) || CollectionUtils.isEmpty(organizeIdsByUserId)) { + return false; + } + // 小数据驱动大数据 + for (String localOrganizeId : localOrganizeIds) { + if (organizeIdsByUserId.contains(localOrganizeId)) { + return true; + } + } + return false; + } + + private boolean doCheckDataPermissionsByOrgId(List orgIds, String organizationId) { + if (CollUtil.isEmpty(orgIds)) { + return false; + } + return orgIds.contains(organizationId); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPreTrailIMUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPreTrailIMUtils.java new file mode 100644 index 0000000..d3862dd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelPreTrailIMUtils.java @@ -0,0 +1,68 @@ +package jnpf.personnels.utils; + +import cn.hutool.json.JSONUtil; +import com.alibaba.fastjson.JSONObject; +import jnpf.ImRobotApi; +import jnpf.base.ActionResult; +import jnpf.from.ImRobotTypeEnum; +import jnpf.from.JumpUrlListModel; +import jnpf.from.SendRobotNoticeDataForm; +import jnpf.from.SingleSendRobotNoticeForm; +import jnpf.model.notice.dto.FtbNoticeAnnouncementsDto; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.LinkedList; +import java.util.List; + +/** + * 发送im 试岗消息 + */ +@Component +@Slf4j +public class PersonnelPreTrailIMUtils { + + @Autowired + private ImRobotApi imRobotApi; + private static final String APP_NAME = "人事管理"; + private static final String APP_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/665ecb69e4b0ae5df114c87a.png"; + private static final String MSG_TEMPLATE = "您所管辖的组织中,以下人员将于明日结束试岗期:%s,若人员不符合标准,您可以在花名册中找到该人员,点击\"驳回试岗\";若该人员符合标准,您不需要做任何操作。"; + + + /** + * 发送消息 + * + * @param tenantId 租户 + * @param toUserIds 接收人 + * @param userNameList 用户姓名列表 + * @return + */ + public Boolean sendMsg(String tenantId, List toUserIds, List userNameList) { + + SingleSendRobotNoticeForm form = new SingleSendRobotNoticeForm(); + form.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + form.setToUserIds(toUserIds); + form.setTenantId(tenantId); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setLogo(APP_LOGO);//固定图片 + robotNoticeDataForm.setAppName(APP_NAME); + + robotNoticeDataForm.setTitle("试岗即将到期提醒"); + String userNameStr = String.join("、", userNameList); + robotNoticeDataForm.setContent(String.format(MSG_TEMPLATE, userNameStr)); + form.setRobotNoticeDataForm(robotNoticeDataForm); + + + ActionResult actionResult = imRobotApi.sendSingleRobotNotice(form); + if (actionResult == null || !actionResult.getCode().equals(200)) { + log.error("试岗快结束im发送失败,send msg req={},result={}", JSONUtil.toJsonStr(form), JSONUtil.toJsonStr(actionResult)); + } + + return true; + } + + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelStaffUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelStaffUtils.java new file mode 100644 index 0000000..a288dad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/PersonnelStaffUtils.java @@ -0,0 +1,92 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollectionUtil; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import jnpf.model.personnels.dto.staff.field.SubMulitFieldValDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import org.apache.commons.lang3.StringUtils; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class PersonnelStaffUtils { + public static String convertSexName(String value) { + if ("1".equals(value)) { + return "男"; + } else if ("2".equals(value)) { + return "女"; + } else { + return ""; + } + } + + public static List buildWorkerGroupDataDtoList(String affiliatedGroupData) { + if (StringUtils.isEmpty(affiliatedGroupData)) { + return CollectionUtil.newArrayList(); + } + Gson gson = new Gson(); + Type type = new TypeToken>>() { + }.getType(); + List> dataList = gson.fromJson(affiliatedGroupData, type); + + List groupList = new ArrayList<>(); + for (List subList : dataList) { + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + for (SubMulitFieldValDto fieldValDto : subList) { + if (fieldValDto.getId().equals("affiliatedOrg")) { + workerGroupDataDto.setAffiliatedOrg(fieldValDto.getUserValue()); + } else if (fieldValDto.getId().equals("affiliatedShop")) { + workerGroupDataDto.setAffiliatedShop(fieldValDto.getUserValue()); + } else if (fieldValDto.getId().equals("reportsTo")) { + workerGroupDataDto.setReportsTo(fieldValDto.getUserValue()); + } else if (fieldValDto.getId().equals("affiliatedPosition")) { + workerGroupDataDto.setAffiliatedPosition(fieldValDto.getUserValue()); + } else if (fieldValDto.getId().equals("affiliatedRank")) { + workerGroupDataDto.setAffiliatedRank(fieldValDto.getUserValue()); + } + } + groupList.add(workerGroupDataDto); + + } + return groupList; + } + + public static int calculatePeriodDays(String probationPeriod, String probationPeriodDay) { + int maxDay = 0; + if (probationPeriod.equals("101")) {//1个月内 + maxDay = Integer.valueOf(probationPeriodDay); + } else if (probationPeriod.equals("102")) { + maxDay = 30; + } else if (probationPeriod.equals("103")) { + maxDay = 30 * 2; + } else if (probationPeriod.equals("104")) { + maxDay = 30 * 3; + } else if (probationPeriod.equals("105")) { + maxDay = 30 * 4; + } else if (probationPeriod.equals("106")) { + maxDay = 30 * 5; + } else if (probationPeriod.equals("107")) { + maxDay = 30 * 6; + } else if (probationPeriod.equals("108")) { + maxDay = 30 * 7; + } else if (probationPeriod.equals("109")) { + maxDay = 30 * 8; + } + return maxDay; + } + + + public static Date getTodayDate() { + Calendar calendar = Calendar.getInstance(); // 获取当前日期时间的Calendar实例 + calendar.set(Calendar.HOUR_OF_DAY, 0); // 设置小时为0点 + calendar.set(Calendar.MINUTE, 0); // 设置分钟为0分 + calendar.set(Calendar.SECOND, 0); // 设置秒数为0秒 + calendar.set(Calendar.MILLISECOND, 0); // 设置毫秒为0 + + return calendar.getTime(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportThreadPool.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportThreadPool.java new file mode 100644 index 0000000..b52552f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportThreadPool.java @@ -0,0 +1,93 @@ +package jnpf.personnels.utils; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +@Component +public class RosterExportThreadPool { + private static final int DEFAULT_CORE_POOL_SIZE = 0; // 默认核心线程数为0 + private static final int DEFAULT_MAXIMUM_POOL_SIZE = 20; // 默认最大线程数为10 + private static final long DEFAULT_KEEP_ALIVE_TIME = 120L; // 默认空闲线程存活时间(单位:秒) + private static final String DEFAULT_THREAD_NAME_PREFIX = "roster-export-"; // 默认线程名称前缀 + + private ThreadPoolExecutor executor; + + public ThreadPoolExecutor getExecutor() { + return executor; + } + + private String threadNamePrefix; + + /** + * 创建线程池,使用默认配置。 + */ + public RosterExportThreadPool() { + this(DEFAULT_CORE_POOL_SIZE, DEFAULT_MAXIMUM_POOL_SIZE, DEFAULT_KEEP_ALIVE_TIME, TimeUnit.SECONDS, DEFAULT_THREAD_NAME_PREFIX); + } + + /** + * 创建线程池,自定义核心线程数、最大线程数、空闲线程存活时间和线程名称前缀。 + * + * @param corePoolSize 核心线程数 + * @param maximumPoolSize 最大线程数 + * @param keepAliveTime 空闲线程存活时间 + * @param timeUnit 时间单位 + * @param threadNamePrefix 线程名称前缀 + */ + public RosterExportThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit timeUnit, String threadNamePrefix) { + this.threadNamePrefix = threadNamePrefix; + ThreadFactory threadFactory = new NamedThreadFactory(threadNamePrefix); + BlockingQueue workQueue = new ArrayBlockingQueue<>(25); + this.executor = new ThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + timeUnit, + workQueue, + threadFactory, + new ThreadPoolExecutor.CallerRunsPolicy() + ); + } + + /** + * 提交任务到线程池执行。 + * + * @param task 待执行的任务 + */ + public void execute(Runnable task) { + executor.execute(task); + } + + /** + * 关闭线程池,不再接受新任务并等待已提交任务执行完毕。 + */ + public void shutdown() { + executor.shutdown(); + } + + /** + * 关闭线程池,不再接受新任务并立即停止所有正在执行的任务。 + */ + public void shutdownNow() { + executor.shutdownNow(); + } + + // 自定义ThreadFactory,设置线程名称 + private static class NamedThreadFactory implements ThreadFactory { + private final String namePrefix; + private final AtomicInteger nextId = new AtomicInteger(1); + + public NamedThreadFactory(String namePrefix) { + this.namePrefix = namePrefix; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setName(namePrefix + nextId.getAndIncrement()); + return t; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportUtils.java new file mode 100644 index 0000000..a68104f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/RosterExportUtils.java @@ -0,0 +1,720 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.google.common.collect.ImmutableMap; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.model.certificate.vo.app.HealthCertificateDetailVO; +import jnpf.model.personnels.bo.ExportRosterOneVo; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.model.personnels.dto.staff.field.ExportFormFieldDto; +import jnpf.model.personnels.dto.staff.field.ExportFormTypeDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.registerform.WorkAddressDto; +import jnpf.model.personnels.dto.staff.roster.SimpleStoreUserDto; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import jnpf.model.personnels.po.*; +import jnpf.permission.ContractTypeApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.vo.contract.ContractTypeVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormFieldMapper; +import jnpf.personnels.mapper.FtbPersonnelsRegistrationFormTypeMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRegistrationFormDataMapper; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterSchemeMapper; +import jnpf.personnels.mapper.FtbPersonnelsTurnoverManagementMapper; +import jnpf.store.mapper.StoreUserRelationMapper; +import jnpf.util.Constants; +import jnpf.util.UserProvider; +import jnpf.utils.FeignHolder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import javax.annotation.Resource; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +@Component +public class RosterExportUtils { + + @Resource + private FtbPersonnelsRegistrationFormFieldMapper ftbPersonnelsRegistrationFormFieldMapper; + + @Resource + private FtbPersonnelsStaffRegistrationFormDataMapper ftbPersonnelsStaffRegistrationFormDataMapper; + + @Resource + private FtbPersonnelsRegistrationFormTypeMapper ftbPersonnelsRegistrationFormTypeMapper; + + @Resource + private V2UserApi v2UserApi; + + @Resource + private CertificateInstanceService certificateInstanceService; + + @Autowired + private StoreUserRelationMapper storeUserRelationMapper; + + @Resource + private ContractTypeApi contractTypeApi; + + @Resource + private FtbPersonnelsStaffRosterSchemeMapper ftbPersonnelsStaffRosterSchemeMapper; + + @Resource + private FtbPersonnelsTurnoverManagementMapper ftbPersonnelsTurnoverManagementMapper; + + public List getExportFormTypes(List formFieldOptionIds,String schemeId) { + if (StrUtil.isNotBlank(schemeId)) { + FtbPersonnelsStaffRosterScheme ftbPersonnelsStaffRosterScheme = ftbPersonnelsStaffRosterSchemeMapper.selectById(schemeId); + formFieldOptionIds = JSON.parseArray(ftbPersonnelsStaffRosterScheme.getFormFields(), String.class); + } + ExportFormTypeDto exportFormTypeDto = new ExportFormTypeDto(); + exportFormTypeDto.setName("Sheet1"); + // 入职直属主管ID特殊处理 + if (formFieldOptionIds.contains("currReportsToId")) { + formFieldOptionIds.remove("currReportsToId"); + formFieldOptionIds.add("affiliatedReportsTo"); + } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(SuperBaseEntity.SuperIBaseEntity::getId, formFieldOptionIds); + queryWrapper.eq(FtbPersonnelsRegistrationFormField::getEnabledMark,0); + queryWrapper.eq(FtbPersonnelsRegistrationFormField::getStatus,0); + queryWrapper.orderByAsc(FtbPersonnelsRegistrationFormField::getSorts); + List registrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList(queryWrapper); + // 姓名和手机号置顶 + Predicate workerNamePredicate = a -> "workerName".equals(a.getId()); + registrationFormFields.stream() + .filter(workerNamePredicate) + .findFirst() + .ifPresent(a -> { + registrationFormFields.removeIf(workerNamePredicate); + registrationFormFields.add(0, a); + }); + Predicate phonePredicate = a -> "phone".equals(a.getId()); + registrationFormFields.stream() + .filter(phonePredicate) + .findFirst() + .ifPresent(a -> { + registrationFormFields.removeIf(phonePredicate); + registrationFormFields.add(1, a); + }); + List fieldDtoList = registrationFormFields.stream().map(item -> { + ExportFormFieldDto exportFormFieldDto = new ExportFormFieldDto(); + exportFormFieldDto.setId(item.getId()); + exportFormFieldDto.setFormTypeId(item.getFormTypeId()); + exportFormFieldDto.setName(item.getName()); + exportFormFieldDto.setSystemType(item.getSystemType()); + exportFormFieldDto.setType(item.getType()); + exportFormFieldDto.setIsUpdateManager(item.getIsUpdateManager()); + exportFormFieldDto.setIsUpdateWorker(item.getIsUpdateWorker()); + exportFormFieldDto.setGroupName(item.getGroupName()); + exportFormFieldDto.setIsNeedFill(item.getIsNeedFill()); + return exportFormFieldDto; + }).collect(Collectors.toList()); + // 导出时工作信息的完整组织 + if (formFieldOptionIds.contains("completeOrganization")) { + ExportFormFieldDto exportFormFieldDto = new ExportFormFieldDto(); + exportFormFieldDto.setId("completeOrganization"); + exportFormFieldDto.setName("完整组织"); + exportFormFieldDto.setSystemType(0); + exportFormFieldDto.setType(0); + exportFormFieldDto.setIsNeedFill(0); + fieldDtoList.add(exportFormFieldDto); + } + // 健康证状态特殊处理 + if (formFieldOptionIds.contains("healthCertificateStatus")) { + ExportFormFieldDto exportFormFieldDto = new ExportFormFieldDto(); + exportFormFieldDto.setId("healthCertificateStatus"); + exportFormFieldDto.setName("健康证状态"); + exportFormFieldDto.setSystemType(0); + exportFormFieldDto.setType(0); + exportFormFieldDto.setIsNeedFill(0); + fieldDtoList.add(exportFormFieldDto); + } + exportFormTypeDto.setFieldDtoList(fieldDtoList); + return List.of(exportFormTypeDto); + } + public List getExportFormTypesWithOnboarding(String[]... nams) { + LambdaQueryWrapper lambdaQueryWrapper = Wrappers.lambdaQuery(); + lambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getStatus, 0); + lambdaQueryWrapper.eq(FtbPersonnelsRegistrationFormType::getEnabledMark,0); + List formTypeList = ftbPersonnelsRegistrationFormTypeMapper.selectList(lambdaQueryWrapper); + if (CollUtil.isEmpty(formTypeList)) { + return new ArrayList<>(); + } + ExportFormTypeDto exportFormTypeDto = new ExportFormTypeDto(); + exportFormTypeDto.setName("Sheet2"); + List typeIds = formTypeList.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + // 入职直属主管 ID 特殊处理 + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsRegistrationFormField::getStatus, 0); + queryWrapper.eq(FtbPersonnelsRegistrationFormField::getEnabledMark,0); + queryWrapper.in(FtbPersonnelsRegistrationFormField::getFormTypeId, typeIds); + queryWrapper.notIn(FtbPersonnelsRegistrationFormField::getType,5,6); + // 将可变参数转换为 List + List nameList = new ArrayList<>(); + if (nams != null) { + for (String[] namArray : nams) { + if (namArray != null) { + nameList.addAll(Arrays.asList(namArray)); + } + } + } + if (CollUtil.isNotEmpty(nameList)) { + queryWrapper.notIn(FtbPersonnelsRegistrationFormField::getName, nameList); + } + queryWrapper.orderByAsc(FtbPersonnelsRegistrationFormField::getSorts); + List ftbPersonnelsRegistrationFormFields = ftbPersonnelsRegistrationFormFieldMapper.selectList(queryWrapper); + // 额外项排除 + List fieldDtoList = ftbPersonnelsRegistrationFormFields.stream().map(item -> { + ExportFormFieldDto exportFormFieldDto = new ExportFormFieldDto(); + exportFormFieldDto.setId(item.getId()); + exportFormFieldDto.setFormTypeId(item.getFormTypeId()); + exportFormFieldDto.setName(item.getName()); + exportFormFieldDto.setSystemType(item.getSystemType()); + exportFormFieldDto.setType(item.getType()); + exportFormFieldDto.setIsUpdateManager(item.getIsUpdateManager()); + exportFormFieldDto.setIsUpdateWorker(item.getIsUpdateWorker()); + exportFormFieldDto.setGroupName(item.getGroupName()); + return exportFormFieldDto; + }).distinct().collect(Collectors.toList()); + sortPriorityFields(fieldDtoList); + exportFormTypeDto.setFieldDtoList(fieldDtoList); + return List.of(exportFormTypeDto); + } + + /** + * 对字段列表进行排序,按照指定的优先级顺序排序 + * + * @param fieldDtoList 字段列表 + */ + private void sortPriorityFields(List fieldDtoList) { + // 定义优先字段的固定顺序 + Map priorityOrder = new LinkedHashMap<>(); + priorityOrder.put("姓名", 0); + priorityOrder.put("性别", 1); + priorityOrder.put("身份证号码", 2); + priorityOrder.put("出生日期", 3); + priorityOrder.put("年龄", 4); + priorityOrder.put("手机号码", 5); + priorityOrder.put("民族", 6); + priorityOrder.put("婚姻状况", 7); + priorityOrder.put("学历", 8); + priorityOrder.put("员工状态", 9); + priorityOrder.put("员工类型", 10); + priorityOrder.put("所属组织", 11); + priorityOrder.put("岗位", 12); + priorityOrder.put("职级", 13); + priorityOrder.put("班组", 14); + priorityOrder.put("直属主管", 15); + priorityOrder.put("紧急联系人姓名", 16); + priorityOrder.put("联系人关系", 17); + priorityOrder.put("联系人电话", 18); + priorityOrder.put("住址", 19); + + java.text.Collator collator = java.text.Collator.getInstance(java.util.Locale.CHINA); + fieldDtoList.sort((field1, field2) -> { + String name1 = field1.getName(); + String name2 = field2.getName(); + + Integer order1 = priorityOrder.get(name1); + Integer order2 = priorityOrder.get(name2); + + // 如果两个字段都在优先级列表中,按优先级顺序排序 + if (order1 != null && order2 != null) { + return order1.compareTo(order2); + } + // 如果只有其中一个在优先级列表中,则在列表中的排在前面 + if (order1 != null) { + return -1; + } + if (order2 != null) { + return 1; + } + // 如果两个字段都不在优先级列表中,则按拼音排序 + return collator.compare(name1, name2); + }); + } + + public List convertToFtbRosterImportTemplateBO(List fieldList, + List optionList, + List rosterDataList) { + if (CollectionUtil.isEmpty(rosterDataList)) { + return new ArrayList<>(); + } + //分组 + List> allList = groupByBatch(rosterDataList); + // 主线程中获取 + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + //多线程调用 convertToFtbRosterImportTemplateBatch 这个方法 + List>> futures = allList.stream() + .map(rosterData -> CompletableFuture.supplyAsync(() -> { + try { + return convertToFtbRosterImportTemplateBatch(fieldList, optionList, rosterData, requestAttributes, tenantId, headers); + } finally { + FeignHolder.clear(); + } + })) + .collect(Collectors.toList()); + //搜集结果并返回 + return futures.stream() + .map(CompletableFuture::join) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + public List> groupByBatch(List rosterDataList) { + List> groupedData = new ArrayList<>(); + int batchSize = 100; + + for (int i = 0; i < rosterDataList.size(); i += batchSize) { + int end = Math.min(i + batchSize, rosterDataList.size()); + groupedData.add(new ArrayList<>(rosterDataList.subList(i, end))); + } + + return groupedData; + } + + public List convertToFtbRosterImportTemplateBatch(List allFields, List optionList, List rosterDataList, RequestAttributes requestAttributes, String tenantCode, Map headers) { + log.info("requestAttr={},tenantId={}", JSONUtil.toJsonStr(requestAttributes), tenantCode); + FeignHolder.set(headers); + try { + TenantDataSourceUtil.switchTenant(tenantCode); + } catch (LoginException e) { + throw new RuntimeException(e); + } + // 用户组织信息 + List userIds = rosterDataList.stream().map(item -> item.getRoster().getUserId()).collect(Collectors.toList()); + List rosterIds = rosterDataList.stream().map(item -> item.getRoster().getId()).collect(Collectors.toList()); + Map contractInformationMap = getContractInformation(rosterIds); + // 获取健康证状态信息 + Map healthStatusMap = getHealthStatus(userIds); + Map workerGroupDataDtoMap = getUserOrgBoundInfoForUserList(userIds, tenantCode); + Map optionMap = optionList.stream() + .collect(Collectors.toMap(FtbPersonnelsRegistrationFormFieldOption::getId, Function.identity(), (a, b) -> a)); + // 预处理表头数据,避免在循环中重复创建 + List list = new ArrayList<>(allFields.size()); + Map> formTypeFieldMap = new HashMap<>(); + + // 预先构建表头和字段映射 + for (ExportFormTypeDto formType : allFields) { + FtbRosterImportTemplateBO bo = new FtbRosterImportTemplateBO(); + list.add(bo); + + List fieldList = formType.getFieldDtoList(); + formTypeFieldMap.put(formType.getName(), fieldList); + + // 构建表头 + List> header = new ArrayList<>(fieldList.size()); + for (ExportFormFieldDto field : fieldList) { + if (field.getIsNeedFill() != null && field.getIsNeedFill() == 1) { + header.add(List.of("*"+field.getName())); + } else { + header.add(List.of(field.getName())); + } + } + bo.setHeader(header); + bo.setSheetName(formType.getName()); + } + + // 预先构建系统字段处理器映射,避免每次重复判断 + Map> systemFieldProcessor = Map.of( + "affiliatedOrg", dto -> dto != null ? dto.getAffiliatedOrgName() : "", + "affiliatedPosition", dto -> dto != null ? dto.getAffiliatedPositionName() : "", + "affiliatedRank", dto -> dto != null ? dto.getAffiliatedRankName() : "", + "affiliatedReportsTo", dto -> dto != null ? dto.getReportsToName() : "", + "affiliatedShop", dto -> dto != null ? dto.getAffiliatedShopName() : "", + "team", dto -> dto != null ? dto.getStoreTeamName() : "" + ); + + // 批量处理每种表单类型的数据 + for (ExportFormTypeDto formType : allFields) { + FtbRosterImportTemplateBO bo = list.stream() + .filter(b -> formType.getName().equals(b.getSheetName())) + .findFirst() + .orElse(null); + + if (bo != null) { + List fieldList = formTypeFieldMap.get(formType.getName()); + + // 使用预分配容量的列表提高性能 + List> data = new ArrayList<>(rosterDataList.size()); + + // 批量处理数据 + for (ExportRosterOneVo exportRosterOneVo : rosterDataList) { + FtbPersonnelsStaffRoster roster = exportRosterOneVo.getRoster(); + List formValueList = exportRosterOneVo.getFormValue(); + + // 查询用户组织信息 + WorkerGroupDataDto workerGroupDataDto = workerGroupDataDtoMap.get(roster.getUserId()); + + // 将表单值转换为映射,避免重复构建 + Map valueMap = convertFormValueMap(formValueList); + + // 预分配容量,避免动态扩容 + List exportDataList = new ArrayList<>(fieldList.size()); + + // 批量处理字段值 + for (ExportFormFieldDto field : fieldList) { + String showValue = calculateFieldValue( + field, + workerGroupDataDto, + valueMap, + roster, + optionMap, + systemFieldProcessor,contractInformationMap, + healthStatusMap + ); + exportDataList.add(showValue); + } + + data.add(exportDataList); + } + + bo.setData(data); + } + } + return list; + } + + private Map getHealthStatus(List userIds) { + List healthCertificateDetails = certificateInstanceService.getHealthCertificateDetails(userIds); + return healthCertificateDetails.stream().collect(Collectors.toMap(HealthCertificateDetailVO::getUserId, HealthCertificateDetailVO::getStatusDesc)); + } + + /** + * 计算字段显示值 + */ + private String calculateFieldValue( + ExportFormFieldDto field, + WorkerGroupDataDto workerGroupDataDto, + Map valueMap, + FtbPersonnelsStaffRoster roster, + Map optionMap, + Map> systemFieldProcessor, + Map contractInformationMap, + Map healthStatusMap) { + + // 添加空值检查 + if (field == null) { + return ""; + } + + // 检查是否为系统字段 + if (systemFieldProcessor != null && systemFieldProcessor.containsKey(field.getId())) { + Function processor = systemFieldProcessor.get(field.getId()); + if (processor != null) { + return processor.apply(workerGroupDataDto); + } + } + + String showValue = ""; + FtbPersonnelsStaffRegistrationFormData formFieldValue = null; + if (valueMap != null) { + formFieldValue = valueMap.get(field.getId()); + } + FtbPersonnelsRegistrationFormFieldOption optionEntity = null; + + if (formFieldValue != null) { + if (field.getType() == 2 || field.getType() == 3) { + if (StringUtils.isNotEmpty(formFieldValue.getValue())) { + String fieldValue = formFieldValue.getValue(); + if ("probationPeriod".equals(field.getId()) && fieldValue != null && fieldValue.startsWith("{")) { + try { + ProbationPeriodDto bean = JSONUtil.toBean(fieldValue, ProbationPeriodDto.class); + String key = bean != null ? bean.getType() : null; + if ("101".equals(key)) { + showValue = bean.getDays() + "天"; + } else { + if (optionMap != null && key != null) { + optionEntity = optionMap.get(key); + if (optionEntity != null) { + showValue = optionEntity.getName(); + } + } + } + } catch (Exception e) { + log.warn("Failed to parse probationPeriod JSON: " + fieldValue, e); + } + } else if ("contractType".equals(field.getId()) && MapUtil.isNotEmpty(contractInformationMap)) { + // 完整组织 + showValue = contractInformationMap.getOrDefault(fieldValue, ""); + } else { + if (field.getType() == 3) { + if (optionMap != null ) { + String value = formFieldValue.getValue(); + // {"label":"多选2,多选3","value":"1958346454656946178,1958346454656946179"} + if (value != null && value.startsWith("{")) { + JSONObject jsonObject = JSONUtil.parseObj(value); + if (jsonObject.containsKey("label")) { + showValue = jsonObject.getStr("label"); + } + }else if (value != null ) { + showValue = Stream.of(value.split(",")) + .map(optionMap::get) + .filter(Objects::nonNull) + .map(FtbPersonnelsRegistrationFormFieldOption::getName) + .collect(Collectors.joining(",")); + } + } + } else { + if (optionMap != null) { + String value = formFieldValue.getValue(); + if (value != null) { + optionEntity = optionMap.get(value); + if (optionEntity != null) { + showValue = optionEntity.getName(); + } + } + } + } + } + } + } else { + if ("workAddress".equals(field.getId()) ) { + String fieldValue = formFieldValue.getValue(); + if (fieldValue != null && fieldValue.startsWith("{")) { + try { + WorkAddressDto bean = JSONUtil.toBean(fieldValue, WorkAddressDto.class); + showValue = bean != null ? bean.getAddress() : ""; + } catch (Exception e) { + log.warn("Failed to parse workAddress JSON: " + fieldValue, e); + } + } + } else if ("completeOrganization".equals(field.getId())) { + // 完整组织 + showValue = workerGroupDataDto.getOrganizeTreeName(); + } else { + String fieldValue = formFieldValue.getValue(); + showValue = fieldValue != null ? fieldValue : ""; + } + } + } + + // 特殊字段处理 + if ("age".equals(field.getId())) { + // 根据出生日期计算 + FtbPersonnelsStaffRegistrationFormData birthdayFieldValue = null; + if (valueMap != null) { + birthdayFieldValue = valueMap.get("birthday"); + } + if (Objects.nonNull(birthdayFieldValue) && StrUtil.isNotBlank(birthdayFieldValue.getValue())) { + try { + Date dateBirthday = DateUtil.parse(birthdayFieldValue.getValue()); + long age = DateUtil.betweenYear(dateBirthday, DateUtil.date(), false); + showValue = String.valueOf(age); + } catch (Exception e) { + log.warn("Failed to parse birthday: " + birthdayFieldValue.getValue(), e); + } + } + } else if ("companyAge".equals(field.getId())) { + if (roster != null) { + Date departDate = roster.getDepartDate(); + String workerStatus = roster.getWorkerStatus(); + // 实际入职时间 + Date actualStartDate = roster.getActualStartDate(); + String day = ""; + if ("305".equals(workerStatus) && ObjectUtil.isNotEmpty(departDate)) { + day = CompanyAgeUtil.calculateServiceAgeShort(actualStartDate,departDate); + } else if (ObjectUtil.isNotEmpty(actualStartDate)) { + day = CompanyAgeUtil.calculateServiceAgeShort(actualStartDate,new Date()); + } + showValue = day; + } + } else if ("workAge".equals(field.getId())) { + FtbPersonnelsStaffRegistrationFormData firstWorkDate = null; + if (valueMap != null) { + firstWorkDate = valueMap.get("firstWorkDate"); + } + if (Objects.nonNull(firstWorkDate) && StrUtil.isNotBlank(firstWorkDate.getValue())) { + try { + Date ageWorkAge = DateUtil.parse(firstWorkDate.getValue()); + long age = DateUtil.betweenYear(ageWorkAge, DateUtil.date(), false); + showValue = age + "年"; + } catch (Exception e) { + log.warn("Failed to parse firstWorkDate: " + firstWorkDate.getValue(), e); + } + } + } else if ("completeOrganization".equals(field.getId()) && Objects.nonNull(workerGroupDataDto)) { + // 完整组织 + showValue = workerGroupDataDto.getOrganizeTreeName(); + } else if ("healthCertificateStatus".equals(field.getId())) { + // 健康状态 + showValue = healthStatusMap.get(roster.getUserId()); + } + + return showValue; + } + + private Map convertFormValueMap(List formFieldValueList) { + if (CollectionUtil.isEmpty(formFieldValueList)) { + return new HashMap<>(); + } + return formFieldValueList.stream().filter(v->ObjectUtil.isNotEmpty(v.getFormFieldId())) + .collect(Collectors.toMap(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, a -> a, (a, b) -> b)); + + } + + private Map getUserOrgBoundInfoForUserList(List userIdList, String tenantId) { + ActionResult> remoteResult = v2UserApi.getUserPrimaryBoundBatch(userIdList, tenantId); + if (Objects.isNull(remoteResult)) { + return new HashMap<>(); + } + Map retMap = new HashMap<>(); + LambdaQueryWrapper lambdaedQuery = Wrappers.lambdaQuery(); + lambdaedQuery.select(FtbPersonnelsTurnoverManagement::getUserId,FtbPersonnelsTurnoverManagement::getOrganizationInfo); + lambdaedQuery.in(FtbPersonnelsTurnoverManagement::getUserId, userIdList); + List personnelsTurnoverManagements = ftbPersonnelsTurnoverManagementMapper.selectList(lambdaedQuery); + Map> turUserMaps = personnelsTurnoverManagements.stream().collect(Collectors.groupingBy(FtbPersonnelsTurnoverManagement::getUserId)); + List data = remoteResult.getData(); + Map map = data.stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2) -> k1)); + //补充门店,如果为 true 表示要查询门店信息 + // 合并在职和离职人员的 userId(去重) + Set userIds = new HashSet<>(); + if (CollUtil.isNotEmpty(map)) { + userIds.addAll(map.keySet()); + } + if (CollUtil.isNotEmpty(personnelsTurnoverManagements)) { + personnelsTurnoverManagements.stream() + .map(FtbPersonnelsTurnoverManagement::getUserId) + .filter(Objects::nonNull) + .forEach(userIds::add); + } + if (CollUtil.isEmpty(userIds)) { + return retMap; + } + List simpleStoreUserDtos = storeUserRelationMapper.querySimpleStoreUserDto(userIds); + // 在职数据构建组织信息(优先级高) + for (Map.Entry entry : map.entrySet()) { + String userId = entry.getKey(); // 用户 ID + UserBoundVO list = entry.getValue(); // 获取值列表 + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + workerGroupDataDto.setAffiliatedOrgName(list.getOrganizeName()); + workerGroupDataDto.setAffiliatedPositionName(list.getPositionName()); + workerGroupDataDto.setAffiliatedRankName(list.getGradeName()); + workerGroupDataDto.setReportsToName(list.getLeaderName()); + workerGroupDataDto.setStoreTeamName(list.getStoreTeamName()); + workerGroupDataDto.setOrganizeTreeName(list.getOrganizeTreeName()); + simpleStoreUserDtos.stream().filter(a -> a.getUserId().equals(userId)).findFirst().ifPresent(a -> { + workerGroupDataDto.setAffiliatedShopName(a.getStoreName()); + }); + retMap.put(userId, workerGroupDataDto); + } + + // 离职数据补全组织信息(优先级低,仅当 retMap 中不存在该 userId 时才补充) + for (Map.Entry> entry : turUserMaps.entrySet()) { + String userId = entry.getKey(); + // 只有当 retMap 中没有该 userId 的数据时,才从 turUserMaps 中补充 + if (!retMap.containsKey(userId)) { + List turnoverList = entry.getValue(); + if (CollUtil.isNotEmpty(turnoverList)) { + FtbPersonnelsTurnoverManagement turnover = turnoverList.get(0); + WorkerGroupDataDto workerGroupDataDto = new WorkerGroupDataDto(); + // 从离职人员表中获取组织信息 + String organizationInfo = turnover.getOrganizationInfo(); + if (StrUtil.isNotBlank(organizationInfo)) { + // 假设 organizationInfo 是 JSON 格式,需要解析 + // 这里根据实际的 organizationInfo 格式进行解析 + List dtos = JSONArray.parseArray(organizationInfo, WorkerGroupDataDto.class); + workerGroupDataDto = dtos.stream().findFirst().orElse(workerGroupDataDto); + } + retMap.put(userId, workerGroupDataDto); + } + } + } + + return retMap; + } + + private Map getContractInformation(List rosterIds) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsStaffRegistrationFormData::getRosterId,rosterIds); + queryWrapper.eq(FtbPersonnelsStaffRegistrationFormData::getFormFieldId,"contractType"); + List ftbPersonnelsStaffRegistrationFormData = ftbPersonnelsStaffRegistrationFormDataMapper.selectList(queryWrapper); + List contractTypeIds = ftbPersonnelsStaffRegistrationFormData.stream() + .map(FtbPersonnelsStaffRegistrationFormData::getValue) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(contractTypeIds)) { + ActionResult> contractTypeApiListInfo = contractTypeApi.getListInfo(contractTypeIds); + if (null != contractTypeApiListInfo && CollectionUtil.isNotEmpty(contractTypeApiListInfo.getData())) { + return contractTypeApiListInfo.getData().stream().collect(Collectors.toMap(ContractTypeVO::getId, ContractTypeVO::getFullName)); + } + } + return Map.of(); + } + + + /** + * 通用方法:从对象列表中提取指定注解的字段值 + * @param list 对象列表 + * @param clazz 对象的 Class + * @return 包含所有指定注解字段值的二维列表 + */ + public List> extractAnnotatedFieldValues(List list, Class clazz) { + if (list == null || list.isEmpty()) { + return new ArrayList<>(); + } + + List> result = new ArrayList<>(); + Field[] fields = clazz.getDeclaredFields(); + + // 获取所有带有指定注解的字段 + List annotatedFields = Arrays.stream(fields) + .filter(field -> field.isAnnotationPresent(ExcelProperty.class)) + .collect(Collectors.toList()); + + // 遍历每个对象 + for (T obj : list) { + List row = new ArrayList<>(); + + // 对于每个对象,按顺序提取指定注解字段的值 + for (Field field : annotatedFields) { + try { + field.setAccessible(true); // 设置访问权限以获取 private 字段 + Object value = field.get(obj); + row.add(value); + } catch (IllegalAccessException e) { + log.warn("无法访问字段:" + field.getName(), e); + row.add(null); // 如果无法访问字段,则添加 null 作为默认值 + } + } + result.add(row); + } + + return result; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsConfig.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsConfig.java new file mode 100644 index 0000000..dfbd117 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsConfig.java @@ -0,0 +1,43 @@ +package jnpf.personnels.utils; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; +import java.util.Map; + +/** + * 短信配置 + * + * @author yanwenfu + * @create 2025-05-20 + */ +@Component +@RefreshScope +@ConfigurationProperties(prefix = "config.sms-config") +@Getter +@Setter +@ToString +public class SmsConfig { + + private Map tenants; + + @Getter + @Setter + @ToString + public static class TenantConfig { + private String smsSecretId; + private String smsSecretKey; + private String smsSdkAppId; + private String smsSdkAppKey; + private String smsEndpoint; + private String smsRegion; + private List smsTemplateList; + private String smsSignContent; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsProperties.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsProperties.java new file mode 100644 index 0000000..6989245 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsProperties.java @@ -0,0 +1,37 @@ +package jnpf.personnels.utils; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Data +@Component +@ConfigurationProperties(prefix = "config") +public class SmsProperties { + /****** im聊天相关配置 ********/ + /** + * SdkAppId + */ + private Long SDKAPPID; + /** + * 秘钥 + */ + private String KEY; + /** + * 管理员账号 + */ + private String IDENTIFIER; + + + /****** 短信相关配置 ********/ + private String smsSecretId; + private String smsSecretKey; + private String smsSdkAppId; + private String smsSdkAppKey; + private String smsEndpoint; + private String smsRegion; + private List smsTemplateList; + private String smsSignContent; +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsSendUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsSendUtil.java new file mode 100644 index 0000000..2590c80 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsSendUtil.java @@ -0,0 +1,108 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollectionUtil; +import com.google.common.base.Joiner; +import jnpf.base.ActionResult; +import jnpf.base.SmsModel; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import jnpf.util.message.SmsUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class SmsSendUtil { + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private SmsProperties smsProperties; + @Autowired + private SmsConfig smsConfig; + + /** + * 发送短信 + * + * @param phoneList 手机号 + * @param templateCode + * @return + */ + public void sendSms(List phoneList, String templateCode, Map map, String tenantId) throws Exception { + SmsTemplate template = null; + //验证短信模板是否配置 + List smsTemplateList; + SmsConfig.TenantConfig tenantConfig; + if (smsConfig != null) { + tenantConfig = smsConfig.getTenants().get(tenantId); + if (null == tenantConfig) { + tenantConfig = smsConfig.getTenants().get("common"); + } + if (null == tenantConfig) { + throw new Exception("短信配置异常"); + } + smsTemplateList = tenantConfig.getSmsTemplateList(); + } else { + smsTemplateList = smsProperties.getSmsTemplateList(); + tenantConfig = JsonUtil.getJsonToBean(smsProperties, SmsConfig.TenantConfig.class); + } + if (CollectionUtil.isEmpty(smsTemplateList) || !smsTemplateList.stream().map(SmsTemplate::getCode).collect(Collectors.toList()).contains(templateCode)) { + log.error("未配置相应的短信模板,请到配置中心配置相应的模板"); + throw new Exception("未配置相应的短信模板,请到配置中心配置相应的模板"); + } else { + template = smsTemplateList.stream().filter(v -> v.getCode().equals(templateCode)).collect(Collectors.toList()).get(0); + } + SmsModel smsModel = new SmsModel(); + smsModel.setTencentSecretId(tenantConfig.getSmsSecretId()); + smsModel.setTencentSecretKey(tenantConfig.getSmsSecretKey()); + smsModel.setTencentAppId(tenantConfig.getSmsSdkAppId()); + smsModel.setTencentAppKey(tenantConfig.getSmsSdkAppKey()); +// phoneList.removeIf(item -> !isPhone(item)); + String respStr = SmsUtil.sentSms(2, smsModel, tenantConfig.getSmsEndpoint(), tenantConfig.getSmsRegion(), + String.join(", ", phoneList), tenantConfig.getSmsSignContent(), template.getTemplateId(), map); + if (!respStr.equals(AuthServerConstant.SUCCESS)) { + throw new Exception(respStr); + } + } + + /** + * 校验短信验证码是否正确 + * + * @param code + * @param phone + * @return + */ + public ActionResult checkCode(String code, String phone, Integer smsTimeOut) { + try { + String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); + if (!StringUtils.isEmpty(redisCode)) { + String codeRedis = redisCode.split("_")[0]; + if (!codeRedis.equals(code)) { + return ActionResult.fail("短信验证码错误,请重新输入!"); + } + long redisDate = Long.parseLong(redisCode.split("_")[1]); + if (System.currentTimeMillis() - redisDate > smsTimeOut * 60 * 1000) { + return ActionResult.fail("验证码已过期"); + } + } else { + return ActionResult.fail("验证码已过期"); + } + return ActionResult.success(); + } catch (Exception e) { + e.printStackTrace(); + log.error("检验验证码错误:{}", e.getMessage()); + return ActionResult.fail("检验验证码错误"); + } + } + + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsTemplate.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsTemplate.java new file mode 100644 index 0000000..4211de3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/SmsTemplate.java @@ -0,0 +1,20 @@ +package jnpf.personnels.utils; + +import lombok.Data; +import org.springframework.stereotype.Component; + +@Data +public class SmsTemplate { + /** + * 模板标识 + */ + private String code; + /** + * 模板ID + */ + private String templateId; + /** + * 模板说明 + */ + private String desc; +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/StaffRosterImportSaveUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/StaffRosterImportSaveUtils.java new file mode 100644 index 0000000..f248b83 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/personnels/utils/StaffRosterImportSaveUtils.java @@ -0,0 +1,741 @@ +package jnpf.personnels.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdcardUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import com.google.common.collect.ImmutableMap; +import io.seata.core.context.RootContext; +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.base.entity.SuperBaseEntity; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.model.enums.FContractSignStatus; +import jnpf.model.enums.GrowthLogEnum; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.model.enums.StaffWorkerStatus; +import jnpf.model.personnels.bo.FtbRosterImportConstants; +import jnpf.model.personnels.dto.regular.FtbPersonnelsRegularCreateDTO; +import jnpf.model.personnels.dto.staff.growth.AddGrowthLogDto; +import jnpf.model.personnels.dto.staff.registerform.ProbationPeriodDto; +import jnpf.model.personnels.dto.staff.roster.StaffBaseInfoDto; +import jnpf.model.personnels.po.FtbPersonnelsStaffGrowthLog; +import jnpf.model.personnels.po.FtbPersonnelsStaffRegistrationFormData; +import jnpf.model.personnels.po.FtbPersonnelsStaffRoster; +import jnpf.model.personnels.vo.roster.FtbRosterInsertAttributesVO; +import jnpf.model.personnels.vo.roster.FtbRosterInsertNomalVO; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.UserBoundInfoDTO; +import jnpf.permission.dto.v2.user.BatchUpdateUserDTO; +import jnpf.permission.dto.v2.user.SaveUserDTO; +import jnpf.permission.vo.v2.user.SaveUserVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.mapper.FtbPersonnelsStaffRosterMapper; +import jnpf.personnels.service.FtbPersonnelsRegularManagementService; +import jnpf.personnels.service.FtbPersonnelsStaffGrowthLogService; +import jnpf.personnels.service.FtbPersonnelsStaffRegistrationFormDataService; +import jnpf.personnels.service.FtbPersonnelsUchisuikePondService; +import jnpf.util.Constants; +import jnpf.util.SelfGrowthUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class StaffRosterImportSaveUtils { + @Autowired + private FtbPersonnelsUchisuikePondService ftbPersonnelsUchisuikePondService; + + @Autowired + private FtbPersonnelsRegularManagementService regularManagementService; + + @Autowired + private PersonnelOrgUtils personnelOrgUtils; + + @Autowired + private PersonnelAsyncServiceUtils personnelAsyncServiceUtils; + + @Autowired + private FtbPersonnelsStaffRegistrationFormDataService registrationFormDataService; + + @Resource + private FtbPersonnelsStaffRosterMapper staffRosterMapper; + + @Autowired + private V2UserApi v2UserApi; + + @Resource + private FtbPersonnelsStaffGrowthLogService ftbPersonnelsStaffGrowthLogService; + + @Resource + private CertificateInstanceService certificateInstanceService; + + private static final String DEFAULT_USER_LOGO = "001.png"; + + @SuppressWarnings("Duplicates") + public void addImportRosters(List list) { + if (CollectionUtil.isEmpty(list)) { + return; + } + long totalStartTime = System.currentTimeMillis(); + if (list.size() > 500) { + throw new RuntimeException("导入数据不能超过500条"); + } + long startTimeBegin = System.currentTimeMillis(); + // 查询手机号是否在花名册列表中已存在 + List phones = list.stream().map(FtbRosterInsertNomalVO::getPhone).collect(Collectors.toList()); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().in(FtbPersonnelsStaffRoster::getPhone, phones); + List rosterList = staffRosterMapper.selectList(wrapper); + Map rosterExistMap = rosterList.stream() + .collect(Collectors.toMap(FtbPersonnelsStaffRoster::getPhone, + Function.identity(), (a, b) -> a)); + // 已存在的数据 + List rosterIds = rosterList.stream().map(SuperBaseEntity.SuperIBaseEntity::getId).collect(Collectors.toList()); + Map> existFormDataMaps = new HashMap<>(); + if (!rosterIds.isEmpty()) { + LambdaQueryWrapper formDataQueryWrapper = new LambdaQueryWrapper<>(); + formDataQueryWrapper.in(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterIds); + List formDataList = registrationFormDataService.list(formDataQueryWrapper); + existFormDataMaps = formDataList.stream() + .collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getRosterId)); + } + long existDataEndTime = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 查询花名册及档案历史数据耗时:{}ms", existDataEndTime - startTimeBegin); + List existUserIds = rosterList.stream().map(FtbPersonnelsStaffRoster::getUserId).collect(Collectors.toList()); + Map existUserMap = new HashMap<>(); + long userBoundStart = System.currentTimeMillis(); + ActionResult> userPrimaryBoundBatch = v2UserApi.getUserPrimaryBoundBatch(existUserIds, null); + long userBoundEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 查询已有用户绑定关系(getUserPrimaryBoundBatch)耗时:{}ms", userBoundEnd - userBoundStart); + if (userPrimaryBoundBatch != null && CollUtil.isNotEmpty(userPrimaryBoundBatch.getData())) { + existUserMap = userPrimaryBoundBatch.getData().stream().collect(Collectors.toMap(UserBoundVO::getId, Function.identity(), (k1, k2) -> k1)); + } + // 构建数据入库 + // 用户组织架构组装 + List saveUserDTOs = new ArrayList<>(); + // 待新增的花名册数据 + List newRosters = new ArrayList<>(); + // 待新增的花名册表单数据 + List newFormDatas = new ArrayList<>(); + // 员工成长 + List addGrowthLogDtos = new ArrayList<>(); + // 转正信息 + List createDTOs = new ArrayList<>(); + // 更新信息 + List updateUserBoundInfoDTOs = new ArrayList<>(); + // 待更新的花名册信息 + List> updateWrappers = new ArrayList<>(); + // 离职员工导入花名册,清除之前的数据,重新生成 + List cleanRosterIds = new ArrayList<>(); + long buildDataStart = System.currentTimeMillis(); + Set needAddUserIds = new HashSet<>(); + for (FtbRosterInsertNomalVO ftbRosterInsertNomalVO : list) { + FtbPersonnelsStaffRoster oldRoster = rosterExistMap.get(ftbRosterInsertNomalVO.getPhone()); + if (null != oldRoster && !"305".equals(oldRoster.getWorkerStatus()) && oldRoster.getEnabledMark() == 0) { + if (oldRoster.getWorkerStatus().equals("304")) { + // 待离职不处理 + continue; + } + if (oldRoster.getWorkerStatus().equals("302") || oldRoster.getWorkerStatus().equals("303") || oldRoster.getWorkerStatus().equals("306")) { + // 更新数据,对于未存在的数据做新增,已存在的数据不做处理 + FtbPersonnelsStaffRoster ftbPersonnelsStaffRoster = buildImportUpdateRoster(ftbRosterInsertNomalVO, oldRoster); + List ftbPersonnelsStaffRegistrationFormData = existFormDataMaps.getOrDefault(oldRoster.getId(), new ArrayList<>()); + List formFieldIds = ftbPersonnelsStaffRegistrationFormData.stream() + .filter(a -> { + if (StringUtils.isNotBlank(a.getValue())) { + if ("[]".equals(a.getValue()) || "null".equals(a.getValue())) { + return false; + } + return true; + } + return false; + }) + .map(FtbPersonnelsStaffRegistrationFormData::getFormFieldId).collect(Collectors.toList()); + List newFormData = buildImportFormData(ftbRosterInsertNomalVO, ftbPersonnelsStaffRoster, null); + // 若为空则可将新增的内容进行补充,若不为空则不进行补充 + newFormData.removeIf(a -> formFieldIds.contains(a.getFormFieldId())); + // 更新入职班组、入职职级、入职直属主管 + UserBoundVO userBoundVO = existUserMap.get(oldRoster.getUserId()); + if (Objects.nonNull(userBoundVO)) { + if ((StrUtil.isEmpty(userBoundVO.getStoreTeamId()) && StrUtil.isNotEmpty(ftbPersonnelsStaffRoster.getCurrGroupId())) + || (StrUtil.isEmpty(userBoundVO.getGradeId()) && StrUtil.isNotEmpty(ftbPersonnelsStaffRoster.getCurrRank())) + || (StrUtil.isEmpty(userBoundVO.getLeaderId()) && StrUtil.isNotEmpty(ftbPersonnelsStaffRoster.getCurrReportsTo()))) { + BatchUpdateUserDTO userDTO = new BatchUpdateUserDTO(); + userDTO.setUserId(oldRoster.getUserId()); + UserBoundInfoDTO userBoundInfoDTO = new UserBoundInfoDTO(); + userBoundInfoDTO.setOrganizeId(ftbPersonnelsStaffRoster.getCurrOrg()); + userBoundInfoDTO.setPositionId(ftbPersonnelsStaffRoster.getCurrPosition()); + if (StrUtil.isEmpty(userBoundVO.getGradeId()) && StrUtil.isNotEmpty(ftbPersonnelsStaffRoster.getCurrRank())) { + userBoundInfoDTO.setGradesId(ftbPersonnelsStaffRoster.getCurrRank()); + } else { + userBoundInfoDTO.setGradesId(userBoundVO.getGradeId()); + } + userBoundInfoDTO.setStoreTeamId(ftbPersonnelsStaffRoster.getCurrGroupId()); + userBoundInfoDTO.setLeaderId(ftbPersonnelsStaffRoster.getCurrReportsTo()); + userDTO.setBoundInfoDTO(userBoundInfoDTO); + // 更新用户信息 + updateUserBoundInfoDTOs.add(userDTO); + } + } + // 更新转正日期 + if (Objects.isNull(oldRoster.getActualProbationaryDate()) && Objects.nonNull(ftbPersonnelsStaffRoster.getActualProbationaryDate())) { + LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate(); + updateWrapper.eq(SuperBaseEntity.SuperIBaseEntity::getId, oldRoster.getId()); + updateWrapper.set(FtbPersonnelsStaffRoster::getActualProbationaryDate, ftbPersonnelsStaffRoster.getActualProbationaryDate()); + updateWrappers.add(updateWrapper); + } + newFormDatas.addAll(newFormData); + } + } else { + // 新增数据,构建花名册 + FtbPersonnelsStaffRoster newRoster = buildImportRoster(ftbRosterInsertNomalVO); + // 离职员工导入花名册,清除之前的数据,重新生成 + if (null != oldRoster) { + newRoster.setId(oldRoster.getId()); + cleanRosterIds.add(oldRoster.getId()); + // 离职且未删除状态为多次入职 + if ("305".equals(oldRoster.getWorkerStatus()) && oldRoster.getEnabledMark() == 0) { + newRoster.setJoinNum(oldRoster.getJoinNum() + 1); + } else if (oldRoster.getEnabledMark() == 1) { + // 之前员工已删除 + newRoster.setJoinNum(1); + } + } else { + // 生成Id主键 + newRoster.setId(IdWorker.getIdStr()); + } + // 转正数据整理 + FtbPersonnelsRegularCreateDTO createDTO = FtbPersonnelsRegularCreateDTO.coverFtbPersonnelsRegularInfoVO(newRoster); + fillPositiveData(createDTO, newRoster, ftbRosterInsertNomalVO); + // 员工档案信息 + List ftbPersonnelsStaffRegistrationFormData = buildImportFormData(ftbRosterInsertNomalVO, newRoster, createDTO); + // 生成账号建立关系信息 + SaveUserDTO saveUserDTO = new SaveUserDTO(); + saveUserDTO.setAccount(newRoster.getPhone()); + saveUserDTO.setRealName(newRoster.getName()); + saveUserDTO.setMobilePhone(newRoster.getPhone()); + saveUserDTO.setWorkerStatus(newRoster.getWorkerStatus()); + saveUserDTO.setHeadIcon(DEFAULT_USER_LOGO); + // 花名册员工id + saveUserDTO.setSystemWorkerId(newRoster.getSystemWokerId()); + saveUserDTO.setWorkerStatus(newRoster.getWorkerStatus()); + // 入职时间 + saveUserDTO.setEntryDate(newRoster.getActualStartDate()); + // 转正时间 + saveUserDTO.setBecomeDate(newRoster.getActualProbationaryDate()); + UserBoundInfoDTO userBoundInfoDTO = new UserBoundInfoDTO(); + userBoundInfoDTO.setOrganizeId(newRoster.getCurrOrg()); + userBoundInfoDTO.setPositionId(newRoster.getCurrPosition()); + userBoundInfoDTO.setGradesId(newRoster.getCurrRank()); + userBoundInfoDTO.setStoreTeamId(newRoster.getCurrGroupId()); + userBoundInfoDTO.setLeaderId(newRoster.getCurrReportsTo()); + saveUserDTO.setBoundInfoDTO(userBoundInfoDTO); + saveUserDTOs.add(saveUserDTO); + newRosters.add(newRoster); + newFormDatas.addAll(ftbPersonnelsStaffRegistrationFormData); + // 员工成长 + AddGrowthLogDto addGrowthLogDto = new AddGrowthLogDto(); + addGrowthLogDto.setChangeDate(new Date()); + addGrowthLogDto.setNum(newRoster.getJoinNum()); + addGrowthLogDto.setGrowthType(GrowthLogEnum.FIRST_JOIN.getCode()); + // 添加导入实际入职日期 + addGrowthLogDto.setDetail(buildGrowthLogDetail(ftbRosterInsertNomalVO, newRoster)); + addGrowthLogDto.setActualStartDate(newRoster.getActualStartDate()); + addGrowthLogDto.setEmployeeId(newRoster.getSystemWokerId()); + addGrowthLogDto.setPhone(newRoster.getPhone()); + // 导入的 + addGrowthLogDtos.add(addGrowthLogDto); + // 试用期状态同步转正数据 + if (newRoster.getWorkerStatus().equals(StaffWorkerStatus.TRIAL.getCode())) { + createDTOs.add(createDTO); + } + } + } + long buildDataEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 遍历导入数据构建新增/更新实体耗时:{}ms", buildDataEnd - buildDataStart); + // 员工信息更新 + if (CollUtil.isNotEmpty(updateUserBoundInfoDTOs)) { + long startTime = System.currentTimeMillis(); + v2UserApi.updateBatchUserInfo(updateUserBoundInfoDTOs); + long costTime = System.currentTimeMillis() - startTime; + log.error("员工信息更新耗时:{}ms", costTime); + } + // 清楚离职员工导入花名册 + doCleanPersonData(cleanRosterIds); + // 花名册数据更新 + updateWrappers.forEach(a -> { + staffRosterMapper.update(new FtbPersonnelsStaffRoster(), a); + }); + // 数据集体入库 + if (CollUtil.isNotEmpty(saveUserDTOs)) { + long startTime = System.currentTimeMillis(); + List actionResult = v2UserApi.saveUserInfoBatch(saveUserDTOs); + long costTime = System.currentTimeMillis() - startTime; + log.error("数据集体入库耗时:{}ms", costTime); + if (CollUtil.isNotEmpty(actionResult)) { + Map saveUserBatchMaps = actionResult.stream().collect(Collectors.toMap(SaveUserVO::getPhone, SaveUserVO::getUserId)); + List userIds = actionResult.stream().map(SaveUserVO::getUserId).collect(Collectors.toList()); + needAddUserIds.addAll(userIds); + // 重复入职 + //Map numberOfOnboardings = getEntryCount(userIds); + newRosters.parallelStream().forEach(a -> { + String userId = saveUserBatchMaps.get(a.getPhone()); + a.setUserId(userId); + // 重复入职 +// Integer numberOfOnboarding = numberOfOnboardings.get(userId); +// if (Objects.nonNull(numberOfOnboarding)) { +// a.setJoinNum(numberOfOnboarding + 1); +// } else { +// a.setJoinNum(1); +// } + }); + addGrowthLogDtos.parallelStream().forEach(a -> { + String userId = saveUserBatchMaps.get(a.getPhone()); + a.setUserId(userId); + }); + createDTOs.parallelStream().forEach(a -> { + String userId = saveUserBatchMaps.get(a.getPhoneNumber()); + a.setUserId(userId); + }); + // 花名册数据入库 + long rosterSaveStart = System.currentTimeMillis(); + Db.saveBatch(newRosters); + long rosterSaveEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 花名册主表批量入库耗时:{}ms", rosterSaveEnd - rosterSaveStart); + // 员工成长数据入库 + if (CollUtil.isNotEmpty(addGrowthLogDtos)) { + long growthSaveStart = System.currentTimeMillis(); + ftbPersonnelsStaffGrowthLogService.addGrowthLogBatch(addGrowthLogDtos); + long growthSaveEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 员工成长批量入库耗时:{}ms", growthSaveEnd - growthSaveStart); + } + // 转正数据异步入库 + if (CollUtil.isNotEmpty(createDTOs)) { + long regularSaveStart = System.currentTimeMillis(); + regularManagementService.applyForRegularizationBatch(createDTOs); + long regularSaveEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 转正记录批量入库耗时:{}ms", regularSaveEnd - regularSaveStart); + } + // 删除用户历史档案信息 + long asyncHistoryStart = System.currentTimeMillis(); + asyncDeleteHistory(userIds); + long asyncHistoryEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 异步删除历史档案提交耗时(不含后台执行):{}ms", asyncHistoryEnd - asyncHistoryStart); + } + } + // 花名册表单数据入库 + if (CollUtil.isNotEmpty(newFormDatas)) { + long formDataStart = System.currentTimeMillis(); + // 优化:批量删除,减少数据库交互次数 + LambdaQueryWrapper deleteWrapper = Wrappers.lambdaQuery(); + List phonesQuery = newFormDatas.stream() + .map(FtbPersonnelsStaffRegistrationFormData::getPhone) + .distinct() + .collect(Collectors.toList()); + List formFieldIdsQuery = newFormDatas.stream() + .map(FtbPersonnelsStaffRegistrationFormData::getFormFieldId) + .distinct() + .collect(Collectors.toList()); + deleteWrapper.in(FtbPersonnelsStaffRegistrationFormData::getPhone, phonesQuery); + deleteWrapper.in(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, formFieldIdsQuery); + List staffRegistrationFormDatas = registrationFormDataService.list(deleteWrapper); + Map> deleteMapStaffFormDatas = staffRegistrationFormDatas.stream() + .collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getPhone, + Collectors.toMap(FtbPersonnelsStaffRegistrationFormData::getFormFieldId, + Function.identity(), + (old, newOne) -> newOne))); + Map> phoneMaps = newFormDatas.stream().collect(Collectors.groupingBy(FtbPersonnelsStaffRegistrationFormData::getPhone)); + List forDataIds = new ArrayList<>(); + phoneMaps.forEach((phone, formDatas) -> { + Map registrationFormDataMap = deleteMapStaffFormDatas.get(phone); + if (Objects.nonNull(registrationFormDataMap)) { + List formFieldIds = formDatas.stream().map(FtbPersonnelsStaffRegistrationFormData::getFormFieldId).collect(Collectors.toList()); + List forDataId = registrationFormDataMap.entrySet().stream() + .filter(entry -> formFieldIds.contains(entry.getKey())) + .map(entry -> entry.getValue().getId()).collect(Collectors.toList()); + forDataIds.addAll(forDataId); + } + }); + if (CollUtil.isNotEmpty(forDataIds)) { + long formDeleteStart = System.currentTimeMillis(); + List> forDataIdsPartitionIds = ListUtil.partition(forDataIds, 100); + forDataIdsPartitionIds.forEach(a -> { + registrationFormDataService.removeBatchByIds(a); + }); + long formDeleteEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 花名册表单数据批量删除耗时:{}ms", formDeleteEnd - formDeleteStart); + } + long formInsertStart = System.currentTimeMillis(); + Db.saveBatch(newFormDatas); + long formInsertEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 花名册表单数据批量入库耗时:{}ms", formInsertEnd - formInsertStart); + long formDataEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 花名册表单整体处理耗时:{}ms", formDataEnd - formDataStart); + } + // 内推池状态修改 + long pondStart = System.currentTimeMillis(); + newRosters.forEach(a -> ftbPersonnelsUchisuikePondService.updateWorkStatus(a.getWorkerStatus(), a.getPhone())); + //健康证初始化 + certificateInstanceService.batchInitHealthCertificate(needAddUserIds); + long pondEnd = System.currentTimeMillis(); + log.error("花名册导入(addImportRosters) 内推池状态更新耗时:{}ms", pondEnd - pondStart); + log.error("花名册导入整体耗时(方法内):{}ms", System.currentTimeMillis() - startTimeBegin); + log.error("花名册导入整体耗时(含前置校验到结束):{}ms", System.currentTimeMillis() - totalStartTime); + } + + /** + * 构建员工成长json + */ + private String buildGrowthLogDetail(FtbRosterInsertNomalVO ftbRosterInsertNomalVO, FtbPersonnelsStaffRoster newRoster) { + StaffBaseInfoDto dto = new StaffBaseInfoDto(); + ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS() + .stream() + .filter(FtbRosterImportConstants.organizationPredicate) + .findFirst() + .ifPresent(data -> { + dto.setCurrOrg(data.getAttributeValue()); + dto.setCurrOrgName(data.getOriginalValue()); + }); + ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS() + .stream() + .filter(FtbRosterImportConstants.postPredicate) + .findFirst() + .ifPresent(data -> { + dto.setCurrPosition(data.getAttributeValue()); + dto.setCurrPositionName(data.getOriginalValue()); + }); + ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS() + .stream() + .filter(FtbRosterImportConstants.gradePredicate) + .findFirst() + .ifPresent(data -> { + dto.setCurrRank(data.getAttributeValue()); + dto.setCurrRankName(data.getOriginalValue()); + }); + ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS() + .stream() + .filter(a -> "入职直属主管ID".equals(a.getHeaderName())) + .findFirst() + .ifPresent(data -> { + dto.setReportsTo(data.getAttributeValue()); + dto.setReportsToName(data.getOriginalValue()); + }); + dto.setActualStartDate(newRoster.getActualStartDate()); + return JSONUtil.toJsonStr(dto); + } + + + /** + * 删除用户历史档案信息 + */ + public void asyncDeleteHistory(List userIds) { + UserInfo userInfo = UserProvider.getUser(); + String tenantId = userInfo.getTenantId(); + String token = userInfo.getToken(); + Map headers = ImmutableMap.of(Constants.AUTHORIZATION.toLowerCase(), token); + personnelAsyncServiceUtils.deleteTurnoverUserHistory(userIds, tenantId, headers); + } + + private FtbPersonnelsStaffRoster buildImportRoster(FtbRosterInsertNomalVO vo) { + FtbPersonnelsStaffRoster entity = new FtbPersonnelsStaffRoster(); + entity.setName(vo.getUserName()); + entity.setPhone(vo.getPhone()); + List attributesVOList = vo.getFtbRosterInsertAttributesVOS(); + if (CollectionUtil.isNotEmpty(attributesVOList)) { + for (FtbRosterInsertAttributesVO attributesVO : attributesVOList) { + String formFieldId = attributesVO.getFormFieldId(); + String attributeValue = attributesVO.getAttributeValue(); + if ("actualStartDate".equals(formFieldId)) { + entity.setActualStartDate(personnelOrgUtils.stringDateToDate(attributeValue)); + } else if ("workerType".equals(formFieldId)) { + entity.setWorkerType(attributeValue); + } else if ("workerStatus".equals(formFieldId)) { + entity.setWorkerStatus(attributeValue); + } else if ("actualProbationaryDate".equals(formFieldId)) { + entity.setActualProbationaryDate(personnelOrgUtils.stringDateToDate(attributeValue)); + } + } + } + entity.setSystemWokerId(SelfGrowthUtil.providePersonnelsCustomIDBasedOnTheModule(SelfrowingEnum.STAFF_ROSTER)); + entity.setJoinNum(1); + entity.setEnabledMark(0); + entity.setIsSubmitForm(1); + entity.setContractStatus(FContractSignStatus.NOT_INITIATED.getCode()); + entity.setIsTrialJob(0); + entity.setTrialJobDay(0); + entity.setIsTrialFail(0); + entity.setTrialFail(""); + entity.setIsSignSeparation(0); + return entity; + } + + private FtbPersonnelsStaffRoster buildImportUpdateRoster(FtbRosterInsertNomalVO vo, FtbPersonnelsStaffRoster oldRoster) { + FtbPersonnelsStaffRoster newRoster = new FtbPersonnelsStaffRoster(); + BeanUtils.copyProperties(oldRoster, newRoster); + newRoster.setCurrRank(null); + vo.getFtbRosterInsertAttributesVOS().forEach(a -> { + String formFieldId = a.getFormFieldId(); + String attributeValue = a.getAttributeValue(); + if ("affiliatedOrg".equals(formFieldId)) { + newRoster.setCurrOrg(attributeValue); + } else if ("affiliatedPosition".equals(formFieldId)) { + newRoster.setCurrPosition(attributeValue); + } else if ("affiliatedRank".equals(formFieldId)) { + newRoster.setCurrRank(attributeValue); + } else if ("currReportsToId".equals(formFieldId)) { + newRoster.setCurrReportsTo(attributeValue); + } else if ("team".equals(formFieldId)) { + newRoster.setCurrGroupId(attributeValue); + } else if (Objects.isNull(newRoster.getActualProbationaryDate()) && "actualProbationaryDate".equals(formFieldId)) { + newRoster.setActualProbationaryDate(personnelOrgUtils.stringDateToDate(attributeValue)); + } + }); + return newRoster; + } + + private List buildImportFormData(FtbRosterInsertNomalVO vo, FtbPersonnelsStaffRoster newRoster, FtbPersonnelsRegularCreateDTO createDTO) { + Date now = new Date(); + List result = vo.getFtbRosterInsertAttributesVOS().stream().map(data -> { + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setPhone(vo.getPhone()); + formData.setFormFieldId(data.getFormFieldId()); + formData.setFormTypeId(data.getFormTypeId()); + formData.setValue(data.getAttributeValue()); + formData.setRosterId(newRoster.getId()); + // 试用期 + if ("probationPeriod".equals(data.getFormFieldId())) { + ProbationPeriodDto probationPeriodDto = new ProbationPeriodDto(StringUtils.isEmpty(data.getAttributeValue()) ? "100" : data.getAttributeValue(), "0"); + if (StaffWorkerStatus.FULL_TIME.getCode().equals(newRoster.getWorkerStatus())) { + probationPeriodDto = new ProbationPeriodDto("100", "0"); + } + formData.setValue(JSONUtil.toJsonStr(probationPeriodDto)); + } + return formData; + }).collect(Collectors.toList()); + // 司龄(系统自动计算),单位天 + if (null != newRoster.getActualStartDate()) { + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setPhone(vo.getPhone()); + formData.setFormFieldId("companyAge"); + formData.setFormTypeId("3"); + formData.setRosterId(newRoster.getId()); + if (now.after(newRoster.getActualStartDate())) { + long betweenDay = DateUtil.betweenDay(newRoster.getActualStartDate(), new Date(), false); + formData.setValue(String.valueOf(betweenDay)); + } else { + formData.setValue("0"); + } + result.add(formData); + } + // 身份证号码识别出生日期 + vo.getFtbRosterInsertAttributesVOS().stream() + .filter(data -> "idCardNum".equals(data.getFormFieldId()) && StringUtils.isNotEmpty(data.getAttributeValue())) + .findFirst() + .ifPresent(data -> { + // 出生日期 + Date birthDate = IdcardUtil.getBirthDate(data.getAttributeValue()); + result.add(new FtbPersonnelsStaffRegistrationFormData() {{ + setPhone(vo.getPhone()); + setFormFieldId("birthday"); + setFormTypeId("1"); + setRosterId(newRoster.getId()); + setValue(DateUtil.format(birthDate, "yyyy-MM-dd")); + }}); + // 年龄 + result.add(new FtbPersonnelsStaffRegistrationFormData() {{ + setPhone(vo.getPhone()); + setFormFieldId("age"); + setFormTypeId("1"); + setRosterId(newRoster.getId()); + setValue(DateUtil.ageOfNow(birthDate) + "岁"); + }}); + }); + // 正式状态员工,实际转正日期与实际入职日期保持一致 + if (newRoster.getWorkerStatus().equals(StaffWorkerStatus.FULL_TIME.getCode())) { + result.removeIf(a -> "actualProbationaryDate".equals(a.getFormFieldId())); + result.add(new FtbPersonnelsStaffRegistrationFormData() {{ + setPhone(vo.getPhone()); + setFormFieldId("actualProbationaryDate"); + setFormTypeId("3"); + setRosterId(newRoster.getId()); + setValue(DateUtil.format(newRoster.getActualStartDate(), "yyyy-MM-dd")); + }}); + } + // 员工状态为试用期,自动填充计划转正日期,并将实际转正日期清空 + if (newRoster.getWorkerStatus().equals(StaffWorkerStatus.TRIAL.getCode())) { + newRoster.setActualProbationaryDate(null); + result.removeIf(a -> "actualProbationaryDate".equals(a.getFormFieldId())); + // 自动填充计划转正日期 + FtbRosterInsertAttributesVO ftbRosterInsertAttributesVO = vo.getFtbRosterInsertAttributesVOS() + .stream() + .filter(a -> "probationPeriod".equals(a.getFormFieldId())) + .findFirst() + .orElseGet(FtbRosterInsertAttributesVO::new); + result.removeIf(a -> "planProbationaryDate".equals(a.getFormFieldId())); + FtbPersonnelsStaffRegistrationFormData formData = new FtbPersonnelsStaffRegistrationFormData(); + formData.setPhone(vo.getPhone()); + formData.setFormFieldId("planProbationaryDate"); + formData.setFormTypeId("3"); + formData.setRosterId(newRoster.getId()); + String dateIsCalculated = thePlannedConversionDateIsCalculated(newRoster.getActualStartDate(), ftbRosterInsertAttributesVO.getAttributeValue()); + formData.setValue(dateIsCalculated); + result.add(formData); + // 仅限新增花名册导入 + if (Objects.nonNull(createDTO)) { + createDTO.setProbation(ftbRosterInsertAttributesVO.getAttributeValue()); + // 计划转正日期 + createDTO.setSchedConverDate(DateUtil.parse(dateIsCalculated, "yyyy-MM-dd")); + } + } + return result; + } + + /** + * 清空花名册和用户详情数据 + */ + private void doCleanPersonData(List rosterId) { + if (CollUtil.isNotEmpty(rosterId)) { + staffRosterMapper.deleteBatchIds(rosterId); + String xid = RootContext.getXID(); + RootContext.unbind(); + LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); + deleteWrapper.in(FtbPersonnelsStaffRegistrationFormData::getRosterId, rosterId); + registrationFormDataService.remove(deleteWrapper); + RootContext.bind(xid); + } + } + + /** + * 计划转正日期计算 + * 100 无试用期 + * 101 1个月内 + * 102 1个月 + * 103 2个月 + * 104 3个月 + * 105 4个月 + * 106 5个月 + * 107 6个月 + * 108 7个月 + * 109 8个月 + * 110 9个月 + * 111 10个月 + * 112 11个月 + * 113 12个月 + */ + private String thePlannedConversionDateIsCalculated(Date actualStartDate, String probationPeriod) { + if (StrUtil.isBlank(probationPeriod) || "100".equals(probationPeriod)) { + return DateUtil.format(actualStartDate, "yyyy-MM-dd"); + } + int probation = 0; + switch (probationPeriod) { + case "102": + probation = 1; + break; + case "103": + probation = 2; + break; + case "104": + probation = 3; + break; + case "105": + probation = 4; + break; + case "106": + probation = 5; + break; + case "107": + probation = 6; + break; + case "108": + probation = 7; + break; + case "109": + probation = 8; + break; + case "110": + probation = 9; + break; + case "111": + probation = 10; + break; + case "112": + probation = 11; + break; + case "113": + probation = 12; + } + Date time = DateUtil.offsetMonth(actualStartDate, probation); + return DateUtil.format(time, "yyyy-MM-dd"); + } + + /** + * 填充转正及花名册组织、岗位、职等、班组信息 + */ + private void fillPositiveData(FtbPersonnelsRegularCreateDTO createDTO, FtbPersonnelsStaffRoster newRoster, FtbRosterInsertNomalVO ftbRosterInsertNomalVO) { + if (CollectionUtil.isNotEmpty(ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS())) { + for (FtbRosterInsertAttributesVO attributesVO : ftbRosterInsertNomalVO.getFtbRosterInsertAttributesVOS()) { + String formFieldId = attributesVO.getFormFieldId(); + String attributeValue = attributesVO.getAttributeValue(); + String originalValue = attributesVO.getOriginalValue(); + if ("affiliatedOrg".equals(formFieldId)) { + newRoster.setCurrOrg(attributeValue); + createDTO.setOrgId(attributeValue); + createDTO.setOrgName(originalValue); + } else if ("affiliatedPosition".equals(formFieldId)) { + newRoster.setCurrPosition(attributeValue); + createDTO.setRegularPostId(attributeValue); + createDTO.setOnboardPostId(attributeValue); + createDTO.setRegularPostName(originalValue); + createDTO.setOnboardPostName(originalValue); + } else if ("affiliatedRank".equals(formFieldId)) { + newRoster.setCurrRank(attributeValue); + createDTO.setRegularGradeId(attributeValue); + createDTO.setOnboardGradeId(attributeValue); + createDTO.setRegularGradeName(originalValue); + createDTO.setOnboardGradeName(originalValue); + } else if ("currReportsToId".equals(formFieldId)) { + newRoster.setCurrReportsTo(attributeValue); + createDTO.setImmediateSuperName(originalValue); + createDTO.setImmediateSuperId(attributeValue); + } else if ("team".equals(formFieldId)) { + newRoster.setCurrGroupId(attributeValue); + } + } + } + } + + /** + * 入职次数 + * + * @param userIds 用户Id集合 + * @return key为userId, value为入职次数 + */ + private Map getEntryCount(List userIds) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getUserId, userIds); + queryWrapper.in(FtbPersonnelsStaffGrowthLog::getGrowthType, 0, 3); + List ftbPersonnelsStaffGrowthLogs = ftbPersonnelsStaffGrowthLogService.list(queryWrapper); + return ftbPersonnelsStaffGrowthLogs.stream() + .collect(Collectors.groupingBy(FtbPersonnelsStaffGrowthLog::getUserId, Collectors.summingInt(log -> 1))); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/preperties/CustomChannels.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/preperties/CustomChannels.java new file mode 100644 index 0000000..4eb747f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/preperties/CustomChannels.java @@ -0,0 +1,12 @@ +package jnpf.preperties; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.messaging.SubscribableChannel; + +public interface CustomChannels { + String INPUT_CHANNEL_JNPF = "jnpf"; + + @Input(INPUT_CHANNEL_JNPF) + SubscribableChannel jnpfTopic(); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumer.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumer.java new file mode 100644 index 0000000..19b0c16 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumer.java @@ -0,0 +1,340 @@ +package jnpf.qualifications.consummer; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.cultivate.mapper.FtbCultivatePromotionMemberNewMapper; +import jnpf.cultivate.service.FtbCultivatePositionService; +import jnpf.cultivate.utils.CultivateLearnUtils; +import jnpf.cultivate.v2.service.V2CultivatePositionService; +import jnpf.cultivate.v2.service.V2CultivatePromotionService; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.entity.qualifications.Qualifications; +import jnpf.exception.HandleException; +import jnpf.exception.LoginException; +import jnpf.message.enums.permission.v2.OperationTypeMessageEnums; +import jnpf.message.model.permission.PermissionRelationUserPositionListDTO; +import jnpf.message.model.permission.v2.GradeRelationPositionMessageDTO; +import jnpf.message.model.permission.v2.GradeUpdateMessageDTO; +import jnpf.message.model.permission.v2.PositionUpdateMessageDTO; +import jnpf.model.cultivate.po.position.FtbCultivatePosition; +import jnpf.model.cultivate.po.promotion.FtbCultivatePromotionPostNew; +import jnpf.qualifications.service.QualificationsService; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.context.annotation.Lazy; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Slf4j +@Component +@EnableBinding(PositionGradeConsumerSource.class) +public class PositionGradeConsumer { + + @Value("${ftb.consumer.group.start.date:2025-09-24}") + private String startDate; + @Autowired + @Lazy + private ConfigValueUtil configValueUtil; + + @Autowired + @Lazy + private QualificationsService qualificationsService; + + @Autowired + @Lazy + private FtbCultivatePositionService ftbCultivatePositionService; + + @Autowired + @Lazy + private FtbCultivatePromotionMemberNewMapper ftbCultivatePromotionMemberNewMapper; + + + private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + + @Autowired + @Lazy + private V2CultivatePromotionService v2CultivatePromotionService; + @Autowired + @Lazy + private V2CultivatePositionService v2CultivatePositionService; + + + @Autowired + @Lazy + private CultivateLearnUtils cultivateLearnUtils; + + /** + * 处理职级删除消息 + * + * @param message 消息 + */ + @StreamListener(target = PositionGradeConsumerSource.INPUT, condition = "headers['ROCKET_TAGS'] == 'TAG_GRADE'") + public void receiveDeleteGrade(Message message) { + // 获取消息体 + String payload = message.getPayload(); + // 获取 headers + MessageHeaders headers = message.getHeaders(); + if (StringUtil.isEmpty(payload)) { + return; + } + if (!checkTime(headers)) { + return; + } + List messageList = null; + try { + messageList = JSONUtil.toList(payload, GradeUpdateMessageDTO.class); + } catch (Exception e) { + log.error("message={}", payload); + log.error("jnpf.consumer.PositionGradeConsumer.receiveDeleteGrade信息解析异常:", e); + return; + } + + if (CollectionUtil.isEmpty(messageList)) { + return; + } + for (GradeUpdateMessageDTO messageDTO : messageList) { + if (StringUtil.isEmpty(messageDTO.getId()) || messageDTO.getOperationTypeEnum() == null || !OperationTypeMessageEnums.DELETE.getCode().equals(messageDTO.getOperationTypeEnum().getCode())) { + continue; + } + + if (StringUtil.isEmpty(messageDTO.getTenantId())) { + continue; + } + switchDb(messageDTO.getTenantId()); + List qualifications = qualificationsService.selectByGradeId(messageDTO.getId()); + if (CollectionUtil.isEmpty(qualifications)) { + continue; + } + for (Qualifications qualification : qualifications) { + try { + qualificationsService.delete(qualification.getId()); + } catch (HandleException e) { + log.error("删除失败", e); + } + } + } + } + + /** + * 岗位和职级解绑 + * + * @param message 消息 + */ + @StreamListener(target = PositionGradeConsumerSource.INPUT, condition = "headers['ROCKET_TAGS'] == 'TAG_GRADE_RELATION_POSITION'") + public void receiveUnbindPositionAndGrade(Message message) { + // 获取消息体 + String payload = message.getPayload(); + // 获取 headers + MessageHeaders headers = message.getHeaders(); + if (StringUtil.isEmpty(payload)) { + return; + } + if (!checkTime(headers)) { + return; + } + List messageList = null; + try { + messageList = JSONUtil.toList(payload, GradeRelationPositionMessageDTO.class); + } catch (Exception e) { + log.error("message={}", payload); + log.error("jnpf.consumer.PositionGradeConsumer.receiveUnbindPositionAndGrade信息解析异常:", e); + return; + } + + if (CollectionUtil.isEmpty(messageList)) { + return; + } + for (GradeRelationPositionMessageDTO messageDTO : messageList) { + if (StringUtil.isEmpty(messageDTO.getId()) || StringUtil.isEmpty(messageDTO.getLinkId()) || messageDTO.getOperationTypeEnum() == null || !OperationTypeMessageEnums.DELETE.getCode().equals(messageDTO.getOperationTypeEnum().getCode())) { + continue; + } + switchDb(messageDTO.getTenantId()); + String positionId = messageDTO.getLinkId(); + String gradeId = messageDTO.getId(); + + List qualifications = qualificationsService.selectByGradeIdAndPositionId(positionId, gradeId); + if (CollectionUtil.isEmpty(qualifications)) { + continue; + } + for (Qualifications qualification : qualifications) { + try { + qualificationsService.delete(qualification.getId()); + } catch (HandleException e) { + log.error("删除失败", e); + } + } + + } + } + + + /** + * 删除岗位 + * + * @param message 消息 + */ + @StreamListener(target = PositionGradeConsumerSource.INPUT, condition = "headers['ROCKET_TAGS'] == 'TAG_POSITION'") + public void receiveDeletePosition(Message message) { + // 获取消息体 + String payload = message.getPayload(); + // 获取 headers + MessageHeaders headers = message.getHeaders(); + if (StringUtil.isEmpty(payload)) { + return; + } + if (!checkTime(headers)) { + return; + } + List messageList = null; + try { + messageList = JSONUtil.toList(payload, PositionUpdateMessageDTO.class); + } catch (Exception e) { + log.error("message={}", payload); + log.error("jnpf.consumer.PositionGradeConsumer.receiveDeletePosition信息解析异常:", e); + return; + } + + if (CollectionUtil.isEmpty(messageList)) { + return; + } + for (PositionUpdateMessageDTO messageDTO : messageList) { + if (StringUtil.isEmpty(messageDTO.getId()) || messageDTO.getOperationTypeEnum() == null || !OperationTypeMessageEnums.DELETE.getCode().equals(messageDTO.getOperationTypeEnum().getCode())) { + continue; + } + if (StringUtil.isEmpty(messageDTO.getTenantId())) { + continue; + } + switchDb(messageDTO.getTenantId()); + String positionId = messageDTO.getId(); + List promotionPostNewList = ftbCultivatePromotionMemberNewMapper.queryRelationMap(positionId); + if (CollUtil.isNotEmpty(promotionPostNewList)) { + v2CultivatePromotionService.updatePromotionError(promotionPostNewList); + } + + LambdaQueryWrapper positionLambdaQueryWrapper = Wrappers.lambdaQuery(); + positionLambdaQueryWrapper.eq(FtbCultivatePosition::getEnabledMark, 0); + positionLambdaQueryWrapper.eq(FtbCultivatePosition::getPostId, positionId); + List list = ftbCultivatePositionService.list(positionLambdaQueryWrapper); + if (CollectionUtil.isEmpty(list)) { + continue; + } + for (FtbCultivatePosition ftbCultivatePosition : list) { + v2CultivatePositionService.delete(ftbCultivatePosition.id); + } + } + } + + + /** + * 用户岗位信息变更 + * + * @param message 消息 + */ + @StreamListener(target = PositionGradeConsumerSource.INPUT, condition = "headers['ROCKET_TAGS'] == 'TAG_USER_RELATION_POSITION'") + public void receiveUserPositionChange(Message message) { + // 获取消息体 + String payload = message.getPayload(); + log.error("TAG_USER_RELATION_POSITION={}", payload); + // 获取 headers + MessageHeaders headers = message.getHeaders(); + if (StringUtil.isEmpty(payload)) { + return; + } + if (!checkTime(headers)) { + return; + } + List messageList = null; + try { + messageList = JSONUtil.toList(payload, PermissionRelationUserPositionListDTO.class); + } catch (Exception e) { + log.error("message={}", payload); + log.error("jnpf.message.model.permission.PermissionRelationUserPositionListDTO信息解析异常:", e); + return; + } + + if (CollectionUtil.isEmpty(messageList)) { + return; + } + for (PermissionRelationUserPositionListDTO messageDTO : messageList) { + if (StringUtil.isEmpty(messageDTO.getUserId())) { + continue; + } + if (StringUtil.isEmpty(messageDTO.getTenantId())) { + continue; + } + switchDb(messageDTO.getTenantId()); + String userId = messageDTO.getUserId(); + String positionId = messageDTO.getPositionId(); + String oldPositionId = messageDTO.getOldPositionId(); + if (StringUtil.isEmpty(positionId)) { + continue; + } + if (StringUtil.isNotEmpty(oldPositionId) && oldPositionId.equals(positionId)) { + continue; + } + cultivateLearnUtils.addPromotionUser(List.of(userId), messageDTO.getTenantId()); + + } + } + + + public void switchDb(String tenantId) { + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } + + @PreDestroy + public void destroy() { + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdown(); + } + } + + + private boolean checkTime(MessageHeaders headers) { + + Long timestamp = headers.getTimestamp(); + if (timestamp == null) { + return false; + } + // 将timestamp格式化为字符串 + String timestampStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(timestamp)); + // 比较字符串大小 + return timestampStr.compareTo(startDate) >= 0; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumerSource.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumerSource.java new file mode 100644 index 0000000..d484172 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/consummer/PositionGradeConsumerSource.java @@ -0,0 +1,17 @@ +package jnpf.qualifications.consummer; + +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.SubscribableChannel; + +public interface PositionGradeConsumerSource { + String OUTPUT = "permission-output"; //生产通道 + String INPUT = "permission-input"; //消费通道 + + @Output(OUTPUT) + MessageChannel output(); + + @Input(INPUT) + SubscribableChannel input(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsController.java new file mode 100644 index 0000000..5b2b7ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsController.java @@ -0,0 +1,171 @@ +package jnpf.qualifications.controller; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.qualifications.Qualifications; +import jnpf.exception.HandleException; +import jnpf.model.qualifications.dto.PaginationQualificationsDTO; +import jnpf.model.qualifications.dto.SaveQualificationsDTO; +import jnpf.model.qualifications.vo.*; +import jnpf.permission.PositionApi; +import jnpf.permission.dto.v2.position.QueryPagePositionDTO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.qualifications.service.QualificationsService; +import jnpf.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.openfeign.SpringQueryMap; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 岗位任职资格标准管理 + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Tag(name = "岗位任职资格标准管理") +@RestController +@RequestMapping("/qualifications") +public class QualificationsController { + + @Resource + private QualificationsService qualificationsService; + + @Autowired + private UserApiV2Util userApiV2Util; + + @Operation(summary = "岗位任职资格标准管理(分页or全列表)") + @GetMapping("/page") + public ActionResult> page(@SpringQueryMap @Valid PaginationQualificationsDTO dto) { + List data = qualificationsService.pageList(dto); + List voList = new ArrayList<>(); + if (CollUtil.isNotEmpty(data)) { + voList = JsonUtil.getJsonToList(data, QualificationsListVO.class); + } + + //all position glades + if (CollUtil.isNotEmpty(voList)) { +// List positionEntityList = positionApi.getPositionName(data.stream().map(Qualifications::getPositionId).collect(Collectors.toList())); + List positionEntityList = userApiV2Util.listPositionDetailInfoByIds(data.stream().map(Qualifications::getPositionId).collect(Collectors.toList()), null); +// List positionGradesEntityList = positionApi.getGradesEntityList(data.stream().map(Qualifications::getPositionGradesId).collect(Collectors.toList())); + List positionGradesEntityList = userApiV2Util.listGradeByIds(data.stream().map(Qualifications::getPositionGradesId).collect(Collectors.toList()), null); + voList.forEach(vo -> { + positionEntityList.stream().filter(position -> position.getId().equals(vo.getPositionId())).findFirst().ifPresent( + positionEntity -> { + vo.setPositionEncode(positionEntity.getEnCode()); + vo.setPositionName(positionEntity.getFullName()); + } + ); + if (CollUtil.isNotEmpty(positionGradesEntityList)) { + positionGradesEntityList.stream().filter(positionGrades -> positionGrades.getId().equals(vo.getPositionGradesId())).findFirst().ifPresent(positionGradesEntity -> { + vo.setPositionGradesName(positionGradesEntity.getFullName()); + }); + } + }); + } + + PaginationVO paginationVO = JsonUtil.getJsonToBean(dto, PaginationVO.class); + + return ActionResult.page(voList, paginationVO); + } + + @Operation(summary = "根据一个岗位id获取职级列表,返回该职级是否配置") + @GetMapping("/rank-list/{id}") + public ActionResult> rankList(@PathVariable("id") String id) throws Exception { + + return ActionResult.success(qualificationsService.rankList(id)); + } + + @Operation(summary = "分页查询岗位列表带职级") + @PostMapping("/page-rank-list") + public ActionResult> rankList(@RequestBody QueryPagePositionDTO dto) throws Exception { + ActionResult> pageListVOActionResult = userApiV2Util.pageIncludingRankPositions(dto, null); + PageListVO page = new PageListVO<>(); + if (pageListVOActionResult != null && pageListVOActionResult.getData() != null && CollUtil.isNotEmpty(pageListVOActionResult.getData().getList())) { + BeanUtil.copyProperties(pageListVOActionResult.getData(),page); + Map map = qualificationsService.rankAllSetList(); + List positionAndGradesVOExps = BeanUtil.copyToList(page.getList(), PositionAndGradesVOExp.class); + page.setList(positionAndGradesVOExps); + List list = page.getList(); + if (CollUtil.isNotEmpty(list)) { + for (PositionAndGradesVOExp positionAndGradesVOExp : list) { + if (CollUtil.isNotEmpty(positionAndGradesVOExp.getList())) { + for (GradeVOExp gradeVOExp : positionAndGradesVOExp.getList()) { + Qualifications qualifications = map.get(positionAndGradesVOExp.getId()+"-"+gradeVOExp.getId()); + if (qualifications == null) { + gradeVOExp.setIsHave(false); + } else { + gradeVOExp.setIsHave(true); + gradeVOExp.setQualificationsId(qualifications.getId()); + } + } + } + } + } + } + return ActionResult.success(page); + } + + @Operation(summary = "一个岗位任职资格标准详情信息") + @GetMapping("/{id}") + public ActionResult infoQualifications(@PathVariable("id") String id) throws Exception { + + return ActionResult.success(qualificationsService.info(id)); + } + + @Operation(summary = "添加一个岗位任职资格标准") + @PostMapping("") + public ActionResult addQualifications(@RequestBody @Valid SaveQualificationsDTO qualificationsDTO) throws HandleException { + + return ActionResult.success("添加成功", qualificationsService.insert(qualificationsDTO)); + } + + @Operation(summary = "更新一个岗位任职资格标准") + @PutMapping("/{id}") + public ActionResult updateQualifications(@PathVariable("id") String id, @RequestBody @Valid SaveQualificationsDTO qualificationsDTO) throws HandleException { + qualificationsDTO.setId(id); + + return ActionResult.success(qualificationsService.update(qualificationsDTO)); + } + + @Operation(summary = "更新一个岗位任职资格标准") + @DeleteMapping("/{id}") + public ActionResult deleteQualifications(@PathVariable("id") String id) throws HandleException { + + return ActionResult.success(qualificationsService.delete(id)); + } + + @Operation(summary = "统计任职资格标准的数量") + @GetMapping("/statistics/organize") + public ActionResult qualificationsStatistics(@RequestParam(value = "organizeId") String organizeId) { + + return ActionResult.success(qualificationsService.statistics(organizeId)); + } + + @Operation(summary = "检查统计任职资格标准是否存在") + @GetMapping("/check/exist") + public ActionResult checkQualificationsByField(@RequestParam(value = "fieldId") String fieldId, @RequestParam(value = "itemId") String itemId) { + if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(fieldId)) { + return ActionResult.success(qualificationsService.checkExistByItem(fieldId, itemId)); + } else if (StrUtil.isNotBlank(fieldId)) { + return ActionResult.success(qualificationsService.checkExistByField(fieldId)); + } else { + return ActionResult.fail("请求参数有误"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldCategoryController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldCategoryController.java new file mode 100644 index 0000000..07195eb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldCategoryController.java @@ -0,0 +1,80 @@ +package jnpf.qualifications.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.entity.qualifications.QualificationsFieldCategory; +import jnpf.qualifications.service.QualificationsFieldCategoryService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * 标准字段分类管理 + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Tag(name = "标准字段分类管理") +@RestController +@RequestMapping("/qualifications_field_category") +public class QualificationsFieldCategoryController { + + @Resource + private QualificationsFieldCategoryService qualificationsFieldCategoryService; + + /** + * 标准字段分类创建 + * @param qualificationsFieldCategory + * @return + */ + @PostMapping("/create") + public ActionResult create(@RequestBody QualificationsFieldCategory qualificationsFieldCategory){ + qualificationsFieldCategoryService.create(qualificationsFieldCategory); + return ActionResult.success("添加成功"); + } + + /** + * id查询分类 + * @param id + * @return + */ + @GetMapping("/getCategoryById") + public ActionResult getCategoryById(String id){ + return ActionResult.success(qualificationsFieldCategoryService.getCategoryById(id)); + } + + /** + * 分类id删除 + * @param id + * @return + */ + @DeleteMapping("/deleteCategoryById") + public ActionResult deleteCategoryById(String id){ + return ActionResult.success(qualificationsFieldCategoryService.deleteCategoryById(id)); + } + + /** + * 修改分类 + * @param qualificationsFieldCategory + * @return + */ + @PostMapping("/updateCategoryById") + public ActionResult updateCategoryById(@RequestBody QualificationsFieldCategory qualificationsFieldCategory){ + qualificationsFieldCategory.setCreatorTime(new Date()); + return ActionResult.success(qualificationsFieldCategoryService.updateById(qualificationsFieldCategory)); + } + + /** + * @Description: + * @params: + * @return: 分类列表 + * @Author: vinson + * @Date 2024/3/6 17:32 + */ + @GetMapping("/getCategoryList") + public ActionResult> getCategoryList(){ + return ActionResult.success(qualificationsFieldCategoryService.getCategoryList()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldController.java new file mode 100644 index 0000000..5164183 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/controller/QualificationsFieldController.java @@ -0,0 +1,115 @@ +package jnpf.qualifications.controller; + +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; + +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.model.qualifications.dto.QualificationsDto; +import jnpf.model.qualifications.vo.QualificationsCategoryVo; +import jnpf.model.qualifications.vo.QualificationsItemsVo; +import jnpf.permission.model.user.MiniUserVo; +import jnpf.qualifications.service.QualificationsFieldService; +import jnpf.qualifications.service.QualificationsService; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 标准字段配置管理 + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Tag(name = "标准字段配置管理") +@RestController +@RequestMapping("/qualifications_field") +public class QualificationsFieldController { + + @Resource + private QualificationsFieldService qualificationsFieldService; + + /** + * @Description:标准字段创建 + * @params: + * @return: + * @Author: vinson + * @Date 2024/3/5 16:55 + */ + @PostMapping("/create") + public ActionResult create(@RequestBody QualificationsDto qualificationsDto){ + return ActionResult.success(qualificationsFieldService.create(qualificationsDto)); + } + + /** + * @Description:标准字段修改 + * @params: QualificationsDto + * @return: Boolean + * @Author: vinson + * @Date 2024/3/5 16:53 + */ + @PostMapping("/updateById") + public ActionResult updateById(@RequestBody QualificationsDto QualificationsDto){ + return ActionResult.success(qualificationsFieldService.updateById(QualificationsDto)); + } + + /** + * @Description:标准字段配置分页查询 + * @params: + * @return: + * @Author: vinson + * @Date 2024/3/5 16:54 + */ + @GetMapping("/getQualificationsFieldPages") + public ActionResult> getQualificationsFieldPages(@RequestParam(value = "keyword",required = false) String keyword, + @RequestParam(value = "categoryId",required = false) String categoryId, + @RequestParam(value = "currentPage") Integer currentPage, + @RequestParam(value = "pageSize") Integer pageSize){ + return ActionResult.success(qualificationsFieldService.getQualificationsFieldPages(keyword, categoryId,currentPage,pageSize)); + } + + /** + * @Description:注释项详情页 + * @params: + * @return: + * @Author: vinson + * @Date 2024/3/7 15:10 + */ + @GetMapping("/getQualificationsFieldItem") + public ActionResult getQualificationsFieldItem( + @RequestParam(value = "fieldId") String fieldId){ + return ActionResult.success(qualificationsFieldService.getQualificationsFieldItem(fieldId)); + } + + /** + * @Description:标准字段删除 + * @params: + * @return: + * @Author: vinson + * @Date 2024/3/7 16:15 + */ + @PostMapping("/deleteFieldById") + public ActionResult deleteFieldById( + @RequestParam(value = "fieldId") String fieldId){ + return ActionResult.success(qualificationsFieldService.deleteFieldById(fieldId)); + } + + /** + * @Description: + * @params: + * @return: + * @Author: vinson + * @Date 2024/3/11 15:38 + */ + @Operation(summary = "获取分类下字段信息") + @GetMapping("getCategoryFieldList") + public ActionResult getCategoryFieldList(String categoryId){ + return ActionResult.success(qualificationsFieldService.getCategoryFieldList(categoryId)); + } + +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldCategoryMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldCategoryMapper.java new file mode 100644 index 0000000..ed66377 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldCategoryMapper.java @@ -0,0 +1,21 @@ +package jnpf.qualifications.mapper; + +import com.github.pagehelper.PageInfo; +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.qualifications.QualificationsFieldCategory; +import jnpf.model.qualifications.vo.QualificationsCategoryVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Mapper +public interface QualificationsFieldCategoryMapper extends SuperMapper { + List getQualificationsFieldPages(@Param("keyWord") String keyWord, @Param("categoryId") String categoryId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemMapper.java new file mode 100644 index 0000000..553d36c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemMapper.java @@ -0,0 +1,13 @@ +package jnpf.qualifications.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.qualifications.QualificationsFieldItem; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsFieldItemMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemRelationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemRelationMapper.java new file mode 100644 index 0000000..3627f4a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldItemRelationMapper.java @@ -0,0 +1,13 @@ +package jnpf.qualifications.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.qualifications.QualificationsFieldItemRelation; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsFieldItemRelationMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldMapper.java new file mode 100644 index 0000000..769edb1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsFieldMapper.java @@ -0,0 +1,22 @@ +package jnpf.qualifications.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.qualifications.QualificationsField; +import jnpf.entity.qualifications.QualificationsFieldItem; +import jnpf.model.qualifications.vo.QualificationsItemsVo; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Repository +public interface QualificationsFieldMapper extends SuperMapper { + QualificationsItemsVo getQualificationsFieldItem(String fieldId); + + List getItems(String fieldId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsMapper.java new file mode 100644 index 0000000..4fb5841 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/mapper/QualificationsMapper.java @@ -0,0 +1,15 @@ +package jnpf.qualifications.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.qualifications.Qualifications; +import org.springframework.stereotype.Repository; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Repository +public interface QualificationsMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldCategoryService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldCategoryService.java new file mode 100644 index 0000000..1ea34ac --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldCategoryService.java @@ -0,0 +1,26 @@ +package jnpf.qualifications.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.qualifications.QualificationsFieldCategory; +import jnpf.model.qualifications.vo.QualificationsCategoryVo; + +import java.util.List; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsFieldCategoryService extends SuperService { + + void create(QualificationsFieldCategory qualificationsFieldCategory); + + QualificationsFieldCategory getCategoryById(String id); + + Long deleteCategoryById(String id); + + List getQualificationsFieldPages(String keyWord, String categoryId,Integer currentPage, Integer pageSize); + + List getCategoryList(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemRelationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemRelationService.java new file mode 100644 index 0000000..7fcda0e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemRelationService.java @@ -0,0 +1,13 @@ +package jnpf.qualifications.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.qualifications.QualificationsFieldItemRelation; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsFieldItemRelationService extends SuperService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemService.java new file mode 100644 index 0000000..b997050 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldItemService.java @@ -0,0 +1,15 @@ +package jnpf.qualifications.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.qualifications.QualificationsFieldItem; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsFieldItemService extends SuperService { + + Long countItem(String fieldId, String itemName); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldService.java new file mode 100644 index 0000000..7fc37e5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsFieldService.java @@ -0,0 +1,33 @@ +package jnpf.qualifications.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.qualifications.QualificationsField; +import jnpf.model.qualifications.dto.QualificationsDto; +import jnpf.model.qualifications.vo.QualificationsCategoryVo; +import jnpf.model.qualifications.vo.QualificationsItemsVo; +import jnpf.model.qualifications.vo.QualificationsVo; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsFieldService extends SuperService { + + Boolean create(QualificationsDto qualificationsDto); + + Boolean updateById(QualificationsDto qualificationsDto); + + PageInfo getQualificationsFieldPages(String keyWord, String categoryId, Integer currentPage, Integer pageSize); + + QualificationsItemsVo getQualificationsFieldItem(String fieldId); + + Boolean deleteFieldById(String fieldId); + + List getCategoryFieldList(String categoryId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsService.java new file mode 100644 index 0000000..08486f2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/QualificationsService.java @@ -0,0 +1,56 @@ +package jnpf.qualifications.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.qualifications.Qualifications; +import jnpf.exception.HandleException; +import jnpf.model.qualifications.dto.PaginationQualificationsDTO; +import jnpf.model.qualifications.dto.SaveQualificationsDTO; +import jnpf.model.qualifications.vo.GradeVOExp; +import jnpf.model.qualifications.vo.QualificationsInfoVO; +import jnpf.model.qualifications.vo.QualificationsStatisticsVO; + +import java.util.List; +import java.util.Map; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +public interface QualificationsService extends SuperService { + + List pageList(PaginationQualificationsDTO queryDTO); + + /** + * 根据职级查询 + * + * @param gradeId 职级id + * @return + */ + List selectByGradeId(String gradeId); + List selectByGradeIdAndPositionId(String positionId,String gradeId); + + QualificationsInfoVO info(String id) throws Exception; + + String insert(SaveQualificationsDTO qualificationsDTO) throws HandleException; + + Boolean update(SaveQualificationsDTO qualificationsDTO) throws HandleException; + + Boolean delete(String id) throws HandleException; + + Boolean deleteAllItemRelation(String qualificationsId); + + Boolean deleteAllItemRelationByFieldId(String fieldId); + + Boolean deleteAllItemRelationByItemIds(List itemIds); + + QualificationsStatisticsVO statistics(String organizeId); + + Boolean checkExistByField(String fieldId); + + Boolean checkExistByItem(String fieldId, String itemId); + + List rankList(String id); + Map rankAllSetList(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldCategoryServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldCategoryServiceImpl.java new file mode 100644 index 0000000..229c426 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldCategoryServiceImpl.java @@ -0,0 +1,132 @@ +package jnpf.qualifications.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.qualifications.QualificationsField; +import jnpf.entity.qualifications.QualificationsFieldCategory; +import jnpf.entity.qualifications.QualificationsFieldItem; +import jnpf.model.qualifications.vo.QualificationsCategoryVo; +import jnpf.qualifications.mapper.QualificationsFieldCategoryMapper; +import jnpf.qualifications.service.QualificationsFieldCategoryService; +import jnpf.qualifications.service.QualificationsFieldItemService; +import jnpf.qualifications.service.QualificationsFieldService; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Service +@Slf4j +public class QualificationsFieldCategoryServiceImpl extends SuperServiceImpl implements QualificationsFieldCategoryService { + + private final QualificationsFieldCategoryMapper qualificationsFieldCategoryMapper; + + private final QualificationsFieldItemService qualificationsFieldItemService; + + private final QualificationsFieldService qualificationsFieldService; + + @Lazy + public QualificationsFieldCategoryServiceImpl(QualificationsFieldCategoryMapper qualificationsFieldCategoryMapper, QualificationsFieldItemService qualificationsFieldItemService, QualificationsFieldService qualificationsFieldService) { + this.qualificationsFieldCategoryMapper = qualificationsFieldCategoryMapper; + this.qualificationsFieldItemService = qualificationsFieldItemService; + this.qualificationsFieldService = qualificationsFieldService; + } + + @Override + public void create(QualificationsFieldCategory qualificationsFieldCategory){ + QualificationsFieldCategory build = QualificationsFieldCategory.builder() + .name(qualificationsFieldCategory.getName()) + .fieldNum(qualificationsFieldCategory.getFieldNum()) + .description(qualificationsFieldCategory.getDescription()) + .sortCode(qualificationsFieldCategory.getSortCode()) + .propertyJson(qualificationsFieldCategory.getPropertyJson()).build(); + this.save(build); + } + + @Override + public QualificationsFieldCategory getCategoryById(String id) { + return this.getById(id); + } + + @Override + public Long deleteCategoryById(String id) { + QualificationsFieldCategory categoryById = this.getCategoryById(id); + ServiceException.notNull(categoryById, "当前分类不存在"); + if(0==categoryById.getFieldNum()){ + this.removeById(id); + return categoryById.getFieldNum(); + } + return categoryById.getFieldNum(); + } + + @Override + public List getQualificationsFieldPages(String keyWord, String categoryId, Integer currentPage, Integer pageSize) { + List qualificationsFieldPages = qualificationsFieldCategoryMapper.getQualificationsFieldPages(keyWord, categoryId); + //根据FieldId查询每个字段下有多少个字段 将对应的FieldId和ItemNum封装到map中 + Map countMap = new HashMap<>(); + if(!qualificationsFieldPages.isEmpty()){ + qualificationsFieldPages.forEach(item->{ + Long l = qualificationsFieldItemService.getBaseMapper().selectCount(new LambdaQueryWrapper().eq(QualificationsFieldItem::getFieldId, item.getFieldId())); + countMap.put(item.getFieldId(),l); + }); + if(!countMap.isEmpty()){ + Set fieldIds = countMap.keySet(); + for (String fieldId : fieldIds) { + LambdaUpdateWrapper updateWrapper = Wrappers.update() + .lambda() + .set(QualificationsField::getItemNum, countMap.get(fieldId)); + updateWrapper.eq(QualificationsField::getFId, fieldId); + qualificationsFieldService.getBaseMapper().update(null, updateWrapper); + } + } + } + PageHelper.startPage(currentPage,pageSize); + return qualificationsFieldCategoryMapper.getQualificationsFieldPages(keyWord, categoryId); + } + + @Override + public List getCategoryList() { + List fCreatorTime = baseMapper.selectList(Wrappers.query() + .orderByDesc("F_CreatorTime")); + //获取分类下字段数量 + Map countMap = new HashMap<>(); + if(!fCreatorTime.isEmpty()){ + fCreatorTime.forEach(f->{ + Long l = qualificationsFieldService.getBaseMapper().selectCount(new LambdaQueryWrapper().eq(QualificationsField::getCategoryId, f.getId())); + countMap.put(f.getId(),l); + }); + if(!countMap.isEmpty()){ + Set categoryIds = countMap.keySet(); + for (String categoryId : categoryIds) { + // 检查是否有对应的值,避免null值 + Long fieldNum = countMap.get(categoryId); + if (fieldNum != null) { + LambdaUpdateWrapper updateWrapper = Wrappers.update() + .lambda() + .set(QualificationsFieldCategory::getFieldNum, fieldNum); + updateWrapper.eq(QualificationsFieldCategory::getId, categoryId); + this.getBaseMapper().update(null, updateWrapper); + } + } + } + } + return baseMapper.selectList(Wrappers.query() + .orderByDesc("F_CreatorTime")); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemRelationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemRelationServiceImpl.java new file mode 100644 index 0000000..0fd18f9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemRelationServiceImpl.java @@ -0,0 +1,19 @@ +package jnpf.qualifications.service.impl; + +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.qualifications.QualificationsFieldItemRelation; +import jnpf.qualifications.mapper.QualificationsFieldItemRelationMapper; +import jnpf.qualifications.service.QualificationsFieldItemRelationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Slf4j +@Service +public class QualificationsFieldItemRelationServiceImpl extends SuperServiceImpl implements QualificationsFieldItemRelationService { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemServiceImpl.java new file mode 100644 index 0000000..1e8fe0f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldItemServiceImpl.java @@ -0,0 +1,27 @@ +package jnpf.qualifications.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.qualifications.QualificationsFieldItem; +import jnpf.qualifications.mapper.QualificationsFieldItemMapper; +import jnpf.qualifications.service.QualificationsFieldItemService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Slf4j +@Service +public class QualificationsFieldItemServiceImpl extends SuperServiceImpl implements QualificationsFieldItemService { + @Override + public Long countItem(String fieldId, String itemName) { + return baseMapper.selectCount(new LambdaQueryWrapper() + .eq(QualificationsFieldItem::getFieldId,fieldId) + .eq(QualificationsFieldItem::getName,itemName)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldServiceImpl.java new file mode 100644 index 0000000..c7ce081 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsFieldServiceImpl.java @@ -0,0 +1,183 @@ +package jnpf.qualifications.service.impl; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.qualifications.QualificationsField; +import jnpf.entity.qualifications.QualificationsFieldCategory; +import jnpf.entity.qualifications.QualificationsFieldItem; +import jnpf.model.qualifications.dto.QualificationsDto; +import jnpf.model.qualifications.vo.QualificationsCategoryVo; +import jnpf.model.qualifications.vo.QualificationsItemsVo; +import jnpf.qualifications.mapper.QualificationsFieldMapper; +import jnpf.qualifications.service.QualificationsFieldCategoryService; +import jnpf.qualifications.service.QualificationsFieldItemService; +import jnpf.qualifications.service.QualificationsFieldService; +import jnpf.qualifications.service.QualificationsService; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.filters.RemoteIpFilter; +import org.apache.commons.collections4.ListUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Slf4j +@Service +public class QualificationsFieldServiceImpl extends SuperServiceImpl implements QualificationsFieldService { + + @Resource + private QualificationsFieldCategoryService qualificationsFieldCategoryService; + + @Resource + private QualificationsFieldItemService qualificationsFieldItemService; + + @Resource + @Lazy + private QualificationsService qualificationsService; + @Override + @Transactional + public Boolean create(QualificationsDto qualificationsDto) { + List qualificationsFieldItems = qualificationsDto.getQualificationsFieldItems(); + examine(qualificationsDto,0L); + Long fieldCount = baseMapper.selectCount(Wrappers.query().lambda().eq(QualificationsField::getFId, qualificationsDto.getFieldId())); + QualificationsField fileBuild = QualificationsField.builder() + .name(qualificationsDto.getFieldName()) + .categoryId(qualificationsDto.getCategoryId()) + .itemNum(fieldCount+(long) qualificationsFieldItems.size()) + .propertyJson(qualificationsDto.getPropertyJson()) + .description(qualificationsDto.getDescription()) + .sortCode(qualificationsDto.getSortCode()) + .build(); + QualificationsField qualificationsField = baseMapper.selectOne(Wrappers.query() + .lambda() + .eq(QualificationsField::getCategoryId, qualificationsDto.getCategoryId()) + .eq(QualificationsField::getName, qualificationsDto.getFieldName())); + fileBuild.setLastModifyTime(new Date()); + fileBuild.setCreatorTime(new Date()); + this.save(fileBuild); + qualificationsFieldItems.forEach(item->{ + item.setFieldId(StringUtil.isNotNull(qualificationsField)?qualificationsField.getFId():fileBuild.getId()); + examine(item,0L); + //注释项每一条都需要校验 因此循环 校验 添加 + qualificationsFieldItemService.save(item); + }); + QualificationsFieldCategory categoryById = qualificationsFieldCategoryService.getCategoryById(qualificationsDto.getCategoryId()); + qualificationsFieldCategoryService.update(Wrappers.update() + .lambda() + .set(QualificationsFieldCategory::getFieldNum,categoryById.getFieldNum()+1L) + .eq(QualificationsFieldCategory::getId,qualificationsDto.getCategoryId())); + return true; + } + + private void examine(QualificationsFieldItem item,Long num) { + Long itemCount = qualificationsFieldItemService.countItem(item.getFieldId(), item.getName()); + if(itemCount>num){ + throw new ServiceException(String.format("分类字段下注释项:%s,不可重复!", item.getName())); + } + } + + private void examine(QualificationsDto qualificationsDto,Long num) { + Long l = baseMapper.selectCount(Wrappers.query() + .lambda() + .eq(QualificationsField::getCategoryId, qualificationsDto.getCategoryId()) + .eq(QualificationsField::getName, qualificationsDto.getFieldName())); + if(l>num){ + throw new ServiceException(String.format("分类下字段:%s,不可重复!", qualificationsDto.getFieldName())); + } + } + + @Override + @Transactional + public Boolean updateById(QualificationsDto qualificationsDto) { + //新的集合 + List qualificationsFieldItems = qualificationsDto.getQualificationsFieldItems(); + QualificationsField one = this.lambdaQuery().eq(QualificationsField::getId, qualificationsDto.getFieldId()).one(); + if(!qualificationsDto.getCategoryId().equals(one.getCategoryId())){ + QualificationsFieldCategory categoryById = qualificationsFieldCategoryService.getCategoryById(qualificationsDto.getCategoryId()); + QualificationsFieldCategory categoryById1 = qualificationsFieldCategoryService.getCategoryById(one.getCategoryId()); + categoryById.setFieldNum(categoryById.getFieldNum()+1); + categoryById1.setFieldNum(categoryById.getFieldNum()-1); + qualificationsFieldCategoryService.updateById(categoryById); + qualificationsFieldCategoryService.updateById(categoryById1); + one.setCategoryId(qualificationsDto.getCategoryId()); + } + one.setCreatorTime(new Date()); + one.setName(qualificationsDto.getFieldName()); + examine(qualificationsDto,1L); + //原有集合 + List qualificationsFieldItemList = qualificationsFieldItemService.getBaseMapper().selectList(Wrappers.query().lambda() + .eq(QualificationsFieldItem::getFieldId, qualificationsDto.getFieldId())); + //新的id集合 + List items = new ArrayList<>(); + //原有id集合 + List itemList = new ArrayList<>(); + qualificationsFieldItems.forEach(i->{ + items.add(i.getId()); + }); + qualificationsFieldItemList.forEach(i->{itemList.add(i.getId());}); + for(String i : items){ + for(QualificationsFieldItem item:qualificationsFieldItems){ + examine(item,1L); + boolean contains = itemList.contains(i); + if(contains){ + qualificationsFieldItemService.updateById(item); + } + if(StringUtil.isEmpty(item.getId())){ + one.setItemNum(one.getItemNum()+1); + qualificationsFieldItemService.save(item); + } + } + } + itemList.forEach(i->{ + if(!items.contains(i)){ + one.setItemNum(one.getItemNum()-1); + qualificationsFieldItemService.removeById(i); + qualificationsService.deleteAllItemRelationByItemIds(Collections.singletonList(i)); + } + }); + this.updateById(one); + return true; + } + + @Override + public PageInfo getQualificationsFieldPages(String keyWord, String categoryId, Integer currentPage, Integer pageSize) { + return new PageInfo<>(qualificationsFieldCategoryService.getQualificationsFieldPages(keyWord, categoryId, currentPage, pageSize)); + } + + @Override + public QualificationsItemsVo getQualificationsFieldItem(String fieldId) { + return getBaseMapper().getQualificationsFieldItem(fieldId); + } + + @Override + @Transactional + public Boolean deleteFieldById(String fieldId) { + QualificationsField byId = this.getById(fieldId); + QualificationsFieldCategory categoryById = qualificationsFieldCategoryService.getCategoryById(byId.getCategoryId()); + categoryById.setFieldNum(categoryById.getFieldNum()-1L); + qualificationsFieldCategoryService.updateById(categoryById); + //新增删除标准字段关联数据 + qualificationsService.deleteAllItemRelationByFieldId(fieldId); + return this.removeById(fieldId); + } + + @Override + public List getCategoryFieldList(String categoryId) { + return baseMapper.selectList(Wrappers.query().lambda().eq(QualificationsField::getCategoryId,categoryId)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsServiceImpl.java new file mode 100644 index 0000000..dfeaf73 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/qualifications/service/impl/QualificationsServiceImpl.java @@ -0,0 +1,423 @@ +package jnpf.qualifications.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.QualificationsConstant; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.qualifications.*; +import jnpf.exception.HandleException; +import jnpf.model.qualifications.dto.PaginationQualificationsDTO; +import jnpf.model.qualifications.dto.SaveQualificationsDTO; +import jnpf.model.qualifications.dto.SaveQualificationsFieldDTO; +import jnpf.model.qualifications.dto.SaveQualificationsFieldItemDTO; +import jnpf.model.qualifications.vo.GradeVOExp; +import jnpf.model.qualifications.vo.QualificationsInfoVO; +import jnpf.model.qualifications.vo.QualificationsStatisticsVO; +import jnpf.permission.vo.v2.grades.GradeVO; +import jnpf.permission.vo.v2.position.PositionAndGradesVO; +import jnpf.permission.vo.v2.position.PositionVO; +import jnpf.qualifications.mapper.QualificationsMapper; +import jnpf.qualifications.service.*; +import jnpf.util.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * 任职资格标准 + * + * @author Flynn Chan + * @create 2024-03-04 + */ +@Slf4j +@Service +public class QualificationsServiceImpl extends SuperServiceImpl implements QualificationsService { + + @Resource + private QualificationsFieldItemRelationService qualificationsFieldItemRelationService; + + @Resource + private QualificationsFieldService qualificationsFieldService; + @Resource + private QualificationsFieldCategoryService qualificationsFieldCategoryService; + @Resource + private QualificationsFieldItemService qualificationsFieldItemService; + @Autowired + private UserApiV2Util userApiV2Util; + + @Override + public List pageList(PaginationQualificationsDTO queryDTO) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + if (!StringUtil.isEmpty(queryDTO.getKeyword())) { + + List voList = userApiV2Util.likeQueryPositionForPositionName(queryDTO.getKeyword(), null); + //null 则return + if (CollUtil.isEmpty(voList)) { + return new ArrayList<>(); + } + queryWrapper.lambda().in(Qualifications::getPositionId, voList.stream().map(PositionAndGradesVO::getId).collect(Collectors.toList())); + } + // position grades + if (StrUtil.isNotBlank(queryDTO.getPositionId())) { + queryWrapper.lambda().eq(Qualifications::getPositionId, queryDTO.getPositionId()); + } + if (StrUtil.isNotBlank(queryDTO.getPositionGradesId())) { + queryWrapper.lambda().eq(Qualifications::getPositionGradesId, queryDTO.getPositionGradesId()); + } + if (queryDTO.getUpdateDateStart() != null) { + queryWrapper.lambda().ge(Qualifications::getLastModifyTime, DateUtil.date(queryDTO.getUpdateDateStart())); + } + if (queryDTO.getUpdateDateEnd() != null) { + queryWrapper.lambda().le(Qualifications::getLastModifyTime, DateUtil.date(queryDTO.getUpdateDateEnd())); + } + + queryWrapper.lambda().orderByAsc(Qualifications::getSortCode).orderByDesc(Qualifications::getPositionId).orderByDesc(Qualifications::getLastModifyTime); + if (queryDTO.getIsPage()) { + PageHelper.startPage((int) queryDTO.getCurrentPage(), (int) queryDTO.getPageSize()); + PageHelper.startPage((int) queryDTO.getCurrentPage(), (int) queryDTO.getPageSize()); + PageInfo page = new PageInfo<>(this.list(queryWrapper)); + + return queryDTO.setData(page.getList(), page.getTotal()); + } else { + return this.list(queryWrapper); + } + } + + /** + * 根据职级查询 + * @param gradeId 职级id + * @return + */ + @Override + public List selectByGradeId(String gradeId) { + return baseMapper.selectList(new LambdaQueryWrapper().eq(Qualifications::getPositionGradesId, gradeId).eq(Qualifications::getDeleteMark, 0)); + } + + @Override + public List selectByGradeIdAndPositionId(String positionId, String gradeId) { + return baseMapper.selectList(new LambdaQueryWrapper().eq(Qualifications::getPositionId, positionId).eq(Qualifications::getPositionGradesId, gradeId).eq(Qualifications::getDeleteMark, 0)); + } + + @Override + public QualificationsInfoVO info(String id) throws Exception { + Qualifications qualifications = this.getById(id); + if (qualifications == null) { + throw new HandleException("该任职资格信息不存在"); + } + QualificationsInfoVO qualificationsInfoVO = new QualificationsInfoVO(); + qualificationsInfoVO.setId(qualifications.getId()); + qualificationsInfoVO.setPositionId(qualifications.getPositionId()); + qualificationsInfoVO.setPositionEncode(qualifications.getPositionEncode()); + qualificationsInfoVO.setPositionGradesId(qualifications.getPositionGradesId()); + //position and glades +// PositionEntity position = positionApi.queryInfoById(qualificationsInfoVO.getPositionId()); + PositionVO position = userApiV2Util.infoPosition(qualificationsInfoVO.getPositionId(), null); + if (position != null) { + qualificationsInfoVO.setPositionName(position.getFullName()); + } + List gradeVOS = userApiV2Util.listGradeByIds(List.of(qualificationsInfoVO.getPositionGradesId().split(",")), null); + if (CollUtil.isNotEmpty(gradeVOS)) { + qualificationsInfoVO.setPositionGradesName(gradeVOS.get(0).getFullName()); + } + + List qualificationsFieldItemRelations = qualificationsFieldItemRelationService.list( + Wrappers.lambdaQuery() + .eq(QualificationsFieldItemRelation::getQualificationsId, qualifications.getId()) + ); + List qualificationsFieldDTOS = new ArrayList<>(); + Map> dataList = qualificationsFieldItemRelations.stream() + .collect(Collectors.groupingBy(QualificationsFieldItemRelation::getFieldId)); + for (Map.Entry> entry : dataList.entrySet()) { + String fieldId = entry.getKey(); + SaveQualificationsFieldDTO saveQualificationsFieldDTO = new SaveQualificationsFieldDTO(); + saveQualificationsFieldDTO.setFieldId(fieldId); + QualificationsField qualificationsField = qualificationsFieldService.getById(fieldId); + saveQualificationsFieldDTO.setFieldName(qualificationsField == null ? QualificationsConstant.NONE_FIELD : qualificationsField.getName()); + if (qualificationsField == null) { + saveQualificationsFieldDTO.setCategoryId(QualificationsConstant.NONE_FIELD_CATEGORY); + saveQualificationsFieldDTO.setCategoryName(QualificationsConstant.NONE_FIELD_CATEGORY); + } else { + QualificationsFieldCategory qualificationsFieldCategory = qualificationsFieldCategoryService.getCategoryById(qualificationsField.getCategoryId()); + saveQualificationsFieldDTO.setCategoryId(qualificationsFieldCategory == null ? QualificationsConstant.NONE_FIELD_CATEGORY : qualificationsFieldCategory.getId()); + saveQualificationsFieldDTO.setCategoryName(qualificationsFieldCategory == null ? QualificationsConstant.NONE_FIELD_CATEGORY : qualificationsFieldCategory.getName()); + } + + List subList = entry.getValue(); + List saveQualificationsFieldItemDTOS = new ArrayList<>(); + for (QualificationsFieldItemRelation itemRelation : subList) { + SaveQualificationsFieldItemDTO saveQualificationsFieldItemDTO = new SaveQualificationsFieldItemDTO(); + QualificationsFieldItem qualificationsFieldItem = qualificationsFieldItemService.getById(itemRelation.getItemId()); + + saveQualificationsFieldItemDTO.setItemId(itemRelation.getItemId()); + saveQualificationsFieldItemDTO.setContent(itemRelation.getContent()); + saveQualificationsFieldItemDTO.setId(itemRelation.getId()); + //none + saveQualificationsFieldItemDTO.setItemName(qualificationsFieldItem == null ? QualificationsConstant.NONE_ITEM : qualificationsFieldItem.getName()); + + saveQualificationsFieldItemDTOS.add(saveQualificationsFieldItemDTO); + } + + saveQualificationsFieldDTO.setQualificationsFieldItems(saveQualificationsFieldItemDTOS); + qualificationsFieldDTOS.add(saveQualificationsFieldDTO); + } + + qualificationsInfoVO.setQualificationsFields(qualificationsFieldDTOS); + + return qualificationsInfoVO; + } + + @Override + @Transactional + public String insert(SaveQualificationsDTO qualificationsDTO) throws HandleException { + //position + PositionVO position = userApiV2Util.infoPosition(qualificationsDTO.getPositionId(), null); + if (position == null) { + throw new HandleException("该岗位信息不存在,请检查"); + } + Qualifications qualifications = new Qualifications( + position.getId(), + position.getEnCode(), + qualificationsDTO.getPositionGradesId() + ); + //check Only + if (!checkQualificationsOnly(qualifications.getPositionId(), qualifications.getPositionGradesId())) { + throw new HandleException("该岗位职等的任职资格标准已存在"); + } + qualifications.setCreatorTime(new Date()); + qualifications.setLastModifyTime(new Date()); + this.save(qualifications); + + if (CollUtil.isEmpty(qualificationsDTO.getQualificationsFields())) { + throw new HandleException("必须绑定一个标准字段"); + } + List qualificationsFieldItemRelations = new ArrayList<>(); + qualificationsDTO.getQualificationsFields().forEach(qualificationsField -> { + if (CollUtil.isNotEmpty(qualificationsField.getQualificationsFieldItems())) { + qualificationsField.getQualificationsFieldItems().forEach(item -> { + QualificationsFieldItemRelation qualificationsFieldItemRelation = new QualificationsFieldItemRelation( + qualifications.getId(), + qualificationsField.getFieldId(), + item.getItemId(), + item.getContent() + ); + + qualificationsFieldItemRelation.setCreatorTime(new Date()); + qualificationsFieldItemRelation.setLastModifyTime(new Date()); + qualificationsFieldItemRelations.add(qualificationsFieldItemRelation); + }); + } + + }); + qualificationsFieldItemRelationService.saveBatch(qualificationsFieldItemRelations); + + return qualifications.getId(); + } + + @Override + @Transactional + public Boolean update(SaveQualificationsDTO qualificationsDTO) throws HandleException { + Qualifications qualifications = this.getById(qualificationsDTO.getId()); + if (qualifications == null) { + throw new HandleException("该任职资格信息不存在"); + } + qualifications.setId(qualificationsDTO.getId()); + qualifications.setPositionId(qualificationsDTO.getPositionId()); + qualifications.setPositionEncode(qualificationsDTO.getPositionEncode()); + qualifications.setPositionGradesId(qualificationsDTO.getPositionGradesId()); + + this.saveOrUpdate(qualifications); + + if (CollUtil.isEmpty(qualificationsDTO.getQualificationsFields())) { + throw new HandleException("必须绑定一个标准字段"); + } + List qualificationsFieldItemRelations = new ArrayList<>(); + qualificationsDTO.getQualificationsFields().forEach(qualificationsField -> { + if (CollUtil.isNotEmpty(qualificationsField.getQualificationsFieldItems())) { + qualificationsField.getQualificationsFieldItems().forEach(item -> { + QualificationsFieldItemRelation qualificationsFieldItemRelation = new QualificationsFieldItemRelation( + qualifications.getId(), + qualificationsField.getFieldId(), + item.getItemId(), + item.getContent() + ); + qualificationsFieldItemRelation.setId(item.getId()); + + qualificationsFieldItemRelations.add(qualificationsFieldItemRelation); + }); + } + + }); + //delete all + qualificationsFieldItemRelationService.remove(Wrappers.lambdaQuery() + .eq(QualificationsFieldItemRelation::getQualificationsId, qualifications.getId()) + ); + + return qualificationsFieldItemRelationService.saveBatch(qualificationsFieldItemRelations); + } + + @Override + @Transactional + public Boolean delete(String id) throws HandleException { + Qualifications qualifications = this.getById(id); + if (qualifications == null) { + throw new HandleException("该任职资格信息不存在"); + } + //delete all + this.deleteAllItemRelation(id); + + return this.removeById(id); + } + + @Override + @Transactional + public Boolean deleteAllItemRelation(String qualificationsId) { + return qualificationsFieldItemRelationService.remove(Wrappers.lambdaQuery() + .eq(QualificationsFieldItemRelation::getQualificationsId, qualificationsId) + ); + } + + @Override + @Transactional + public Boolean deleteAllItemRelationByFieldId(String fieldId) { + return qualificationsFieldItemRelationService.remove(Wrappers.lambdaQuery() + .eq(QualificationsFieldItemRelation::getFieldId, fieldId) + ); + } + + @Override + @Transactional + public Boolean deleteAllItemRelationByItemIds(List itemIds) { + return qualificationsFieldItemRelationService.remove(Wrappers.lambdaQuery() + .in(QualificationsFieldItemRelation::getItemId, itemIds) + ); + } + + @Override + public QualificationsStatisticsVO statistics(String organizeId) { + List positionInfoNewVOList = new ArrayList<>(); + QualificationsStatisticsVO statisticsVO = new QualificationsStatisticsVO( + organizeId, + 0, 0, 0 + ); + if (StrUtil.isNotBlank(organizeId)) { + //position +// positionInfoNewVOList = positionApi.getListByOrganizeIds(organizeId).getData(); + positionInfoNewVOList = userApiV2Util.listPositionAndGradesByPositionNameForOrgIds(organizeId, null); + } else { + //all +// positionInfoNewVOList = positionApi.allInfoList().getData(); + positionInfoNewVOList = userApiV2Util.listAllPositionAndGradesByPositionName(null); + } + + List gradesIds = new ArrayList<>(); + + if (CollUtil.isNotEmpty(positionInfoNewVOList)) { + statisticsVO.setPositionNum(positionInfoNewVOList.size()); + AtomicReference gradesNum = new AtomicReference<>(0); + positionInfoNewVOList.forEach(positionInfoNewVO -> { + if (CollUtil.isNotEmpty(positionInfoNewVO.getList())) { + gradesNum.getAndSet(gradesNum.get() + positionInfoNewVO.getList().size()); + positionInfoNewVO.getList().forEach(grades -> { + gradesIds.add(grades.getId()); + }); + } + }); + //org relation + if (CollUtil.isNotEmpty(gradesIds)) { + //grades num + statisticsVO.setPositionGradesNum(gradesIds.size()); + List qualificationsList = this.list(Wrappers.lambdaQuery() + .in(Qualifications::getPositionGradesId, gradesIds) + ); + // link num + statisticsVO.setQualificationsLinkPositionGradesNum(qualificationsList.size()); + } + } + + return statisticsVO; + } + + @Override + public Boolean checkExistByField(String fieldId) { + List qualificationsFieldItemRelations = qualificationsFieldItemRelationService.list(Wrappers.lambdaQuery() + .eq(QualificationsFieldItemRelation::getFieldId, fieldId) + ); + + return CollUtil.isNotEmpty(qualificationsFieldItemRelations); + } + + @Override + public Boolean checkExistByItem(String fieldId, String itemId) { + List qualificationsFieldItemRelations = qualificationsFieldItemRelationService.list(Wrappers.lambdaQuery() + .eq(QualificationsFieldItemRelation::getFieldId, fieldId) + .eq(QualificationsFieldItemRelation::getItemId, itemId) + ); + + return CollUtil.isNotEmpty(qualificationsFieldItemRelations); + } + + @Override + public List rankList(String id) { + List gradeVOS = userApiV2Util.listGrades(id, null); + if (CollUtil.isEmpty(gradeVOS)) { + return new ArrayList<>(); + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(Qualifications::getPositionId, id); + List qualificationsList = baseMapper.selectList(wrapper); + Map map = new HashMap<>(); + if (CollUtil.isNotEmpty(qualificationsList)) { + for (Qualifications qualifications : qualificationsList) { + map.put(id + "-" + qualifications.getPositionGradesId(), qualifications); + } + } + List gradeVOExps = BeanUtil.copyToList(gradeVOS, GradeVOExp.class); + for (GradeVOExp gradeVO : gradeVOExps) { + Qualifications qualifications = map.get(id + "-" + gradeVO.getId()); + if (qualifications == null) { + gradeVO.setIsHave(false); + } else { + gradeVO.setIsHave(true); + } + } + + + return gradeVOExps; + } + + @Override + public Map rankAllSetList() { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + List qualificationsList = baseMapper.selectList(wrapper); + Map map = new HashMap<>(); + if (CollUtil.isNotEmpty(qualificationsList)) { + for (Qualifications qualifications : qualificationsList) { + map.put(qualifications.getPositionId() + "-" + qualifications.getPositionGradesId(), qualifications); + } + } + return map; + } + + private Boolean checkQualificationsOnly(String positionId, String positionGradesId) { + List qualifications = this.list(Wrappers.lambdaQuery() + .eq(Qualifications::getPositionId, positionId) + .eq(Qualifications::getPositionGradesId, positionGradesId) + ); + + return !CollUtil.isNotEmpty(qualifications); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/StoreController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/StoreController.java new file mode 100644 index 0000000..2e73670 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/StoreController.java @@ -0,0 +1,566 @@ +package jnpf.store.controller; + +import com.alibaba.nacos.common.utils.StringUtils; +import com.github.pagehelper.PageInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jnpf.base.ActionResult; +import jnpf.base.vo.PageListVO; +import jnpf.doclibrary.StoreApi; +import jnpf.entity.StoreEntity; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.UpdateStoreDto; +import jnpf.model.store.Store; +import jnpf.model.store.StorePositionInfoVo; +import jnpf.model.store.StoreUserNumVo; +import jnpf.model.store.dto.*; +import jnpf.model.store.vo.StoreBaseListVO; +import jnpf.model.store.vo.StoreLocationVO; +import jnpf.model.store.vo.StoreUsersVo; +import jnpf.model.store.vo.UserStoreListVo; +import jnpf.model.vo.StoreExecutionVo; +import jnpf.store.service.StoreService; +import jnpf.util.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + + +/** + * 门店管理 + * + * @版本: V3.1.0 + * @版权: 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @作者: JNPF开发平台组 + * @日期: 2023-07-11 + */ +@Slf4j +@RestController("ftbStoreController") +@Tag(name = "门店管理", description = "Store") +@RequestMapping("/Store") +public class StoreController implements StoreApi { + @Autowired + private StoreService storeService; + @Autowired + private CustomTenantUtil customTenantUtil; + + /** + * 查询门店下岗位的成员数量 + * + * @param storeId 门店id + * @param positionList 岗位id集合 + * @return java.util.List + */ + @Override + @GetMapping(value = "/storePositionInfo/list") + public List getStorePositionInfoList(@RequestParam(value = "storeId") String storeId, @RequestParam(value = "positionList") List positionList) { + + return storeService.getStorePositionInfoList(storeId, positionList); + } + + @Override + @GetMapping(value = "/storePositionInfo/getUserNum") + public List getUserNum(@RequestParam(value = "storeIds") List storeIds) { + + return storeService.getUserNum(storeIds); + } + + /** + * 检查哪些用户不能被选择 + * + * @param currentChoose 当前选中的人 + * @param exceptChoose 需要排除的人 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/userCantChoose") + public ActionResult> checkUserCantChoose(String currentChoose, @RequestParam(required = false) String exceptChoose) { + + List list = storeService.checkUserCantChoose(currentChoose, exceptChoose); + return ActionResult.success(list); + } + + /** + * 列表 + * + * @return + */ + @Override + @Operation(summary = "获取列表") + @GetMapping("/getList") + public ActionResult> getList(@RequestParam("selectKey") String selectKey, + @RequestParam(value = "organizeid", required = false) String organizeId, + @RequestParam(value = "longitude", required = false) String longitude, + @RequestParam(value = "latitude", required = false) String latitude) { + + // 根据组织名、门店名、地址、负责人 + // 根据经纬度范围查询 + // 根据组织id查询门店列表 + + // 1.根据selectKey判断查询类型,如果为byOrganizeId,执行方法2 如果为byKeyword,执行方法3 如果为byRange,执行方法4 + // 2.判断organizeid是否为空 + // 2.1.为空,报错 + // 2.2.不为空,根据organizeid查询对应的门店列表,并根据修改时间排序 + // 4.判断经度或纬度是否为空 + // 4.1.为空,报错 + // 4.2.不为空,判断organizeid是否为空 + // 4.2.1.为空,直接查出所有门店,并根据经纬度算出距离,根据距离排序 + // 4.2.2.不为空,根据organizeid查出对应门店,并根据经纬度算出距离,根据距离排序 + + if ("byOrganizeId".equals(selectKey)) { + if (StringUtils.isEmpty(organizeId)) { + return ActionResult.fail("组织id不能为空!"); + } + return ActionResult.success(storeService.getStoreByOrganizeId(organizeId)); + } else if ("byRange".equals(selectKey)) { + if (StringUtils.isEmpty(longitude) || StringUtils.isEmpty(latitude)) { + return ActionResult.fail("经度或纬度不能为空!"); + } + return ActionResult.success(storeService.getStoreByRange(organizeId, longitude, latitude)); + } else if ("enabled".equals(selectKey)) { + return ActionResult.success(storeService.getStoreEnabled()); + } else { + return ActionResult.fail("查询方式不合法!"); + } + } + + /** + * 查询组织下的门店 + * + * @param organizeId 组织id + * @return java.util.List + */ + @Override + @GetMapping(value = "/list") + public List getStoreList(@RequestParam(value = "organizeId", required = false) String organizeId) { + + return storeService.getStoreList(organizeId, null); + } + + @Override + @GetMapping(value = "/orgList") + public List getOrgStoreList(@RequestParam(value = "organizeId", required = false) String organizeId, @RequestParam(value = "queryStoreIds", required = false) List queryStoreIds) { + + return storeService.getOrgStoreList(organizeId, queryStoreIds); + } + + /** + * 查询组织下的门店 + * + * @param organizeId 组织id + * @return java.util.List + */ +// @Override + @GetMapping(value = "/list/v2") + public ActionResult> getStoreListV2(@RequestParam(value = "organizeId", required = false) String organizeId, @RequestParam(value = "storeName", required = false) String storeName) { + + return ActionResult.success(storeService.getStoreList(organizeId, storeName)); + } + + /** + * 查询组织下的门店 + * + * @param organizeId 组织id + * @return java.util.List + */ + @GetMapping(value = "/allList") + public ActionResult> getAllStoreList(@RequestParam(value = "organizeId", required = false) String organizeId) { + + List storeList = storeService.getStoreList(organizeId, null); + return ActionResult.success(storeList); + } + + + @Operation(summary = "获取所有列表") + @GetMapping("/getAllList") + public ActionResult> getListByKeyword(@RequestParam("keyword") String keyword, int currentPage, int pageSize, @RequestParam(value = "searchStatus", required = false) String searchStatus) { + //门店状态( 全部:-1 禁用:1 正常:0/不传入默认0正常) + Integer searchState = 0; + if (StringUtil.isNotEmpty(searchStatus)) { + //字符串转成小写 + searchStatus = searchStatus.toLowerCase(); + if (searchStatus.equals("true")) { + searchState = 1; + } else if (searchStatus.equals("false")) { + searchState = 0; + } else { + searchState = Integer.valueOf(searchStatus); + } + } + PageInfo page = storeService.getStoreByKeyword(keyword, currentPage, pageSize, searchState); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + @Operation(summary = "获取所有门店下属值班人员") + @GetMapping("/duty/users") + public ActionResult> getUsersByStore(@RequestParam("keyword") String keyword, @RequestParam("currentPage") int currentPage, @RequestParam("pageSize") int pageSize) { + PageInfo page = storeService.getUsersByStore(keyword, currentPage, pageSize); + return ActionResult.page(page.getList(), FtbUtil.getPagination(page)); + } + + /** + * 设置值班人 + * + * @param storeUserDto 值班人设置实体 + * @return + */ + @PostMapping("/updateStoreUsers") + @Transactional + @Operation(summary = "设置值班人") + public ActionResult updateStoreUsers(@RequestBody StoreUserDto storeUserDto) { + storeService.updateStoreUsers(storeUserDto); + return ActionResult.success(); + } + + /** + * 创建 + * + * @param store + * @return + */ + @PostMapping + @Transactional + @Operation(summary = "创建") + @Deprecated + public ActionResult create(@RequestBody @Valid Store store) throws Exception { + String b = storeService.checkForm(store, 0); + if (StringUtil.isNotEmpty(b)) { + return ActionResult.fail(b + "不能重复"); + } + try { + storeService.create(store); + return ActionResult.success("创建成功"); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + + /** + * 信息 + * + * @param id + * @return + */ + @Operation(summary = "信息") + @GetMapping("/{id}") + public ActionResult info(@PathVariable("id") String id) { + Store store = storeService.getInfo(id); + return ActionResult.success(store); + } + + /** + * 查询门店信息 + * + * @param id 门店id + * @return jnpf.model.store.Store + */ + @Override + @GetMapping("/info/{id}") + public Store getStoreInfo(@PathVariable("id") String id) { + + return storeService.getInfo(id); + } + + /** + * 查询门店信息值班调用fegin + * + * @param id 门店id + * @return jnpf.model.store.Store + */ + @Override + @GetMapping("/getStoreInfoNoData") + @NoDataSourceBind + public StoreEntity getStoreInfoNoData(@RequestParam("id") String id, @RequestParam("tenantId") String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return storeService.getStoreInfoDutyQuery(id); + } + + /** + * 根据门店 ids 分页查询门店记录(POST,避免大量 id 撑爆 URL) + */ + @PostMapping(value = "/store/pageByIds") + @Override + public PageInfo getStorePageByIds(@RequestBody StorePageByIdsQueryDTO query) { + if (query == null) { + throw new RuntimeException("请求体不能为空"); + } + String storeIds = joinStoreIdsForLegacyService(query.getStoreIds()); + return storeService.getStorePageByIds(storeIds, query.getStoreName(), query.getCurrentPage(), query.getPageSize(), query.getSortNum()); + } + + /** + * 根据门店 ids 分页查询门店记录(切租户、无数据源绑定;POST) + */ + @PostMapping(value = "/store/pageByIdsNoDataSource") + @NoDataSourceBind + @Override + public PageInfo getStorePageByIdsNoDataSource(@RequestBody StorePageByIdsNoDsQueryDTO query) { + if (query == null) { + throw new RuntimeException("请求体不能为空"); + } + if (StringUtils.isEmpty(query.getTenantId())) { + throw new RuntimeException("租户不能为空"); + } + customTenantUtil.checkOutTenant(query.getTenantId()); + String storeIds = joinStoreIdsForLegacyService(query.getStoreIds()); + return storeService.getStorePageByIds(storeIds, query.getStoreName(), query.getCurrentPage(), query.getPageSize(), ConstantUtil.NUM_TRUE); + } + + /** + * 查询异常的门店(POST) + */ + @PostMapping(value = "/store/abnormal/record") + @Override + public List getAbnormalStoreIds(@RequestBody StoreAbnormalIdsQueryDTO query) { + if (query == null) { + return storeService.getAbnormalStoreIds(null); + } + return storeService.getAbnormalStoreIds(query.getStoreIds()); + } + + /** + * 根据组织ids查询门店列表 + * + * @param organizeIdList 组织ids + * @return java.util.List + */ + @Override + @PostMapping(value = "/store/list/byOrganizeList") + public List getStoreListByOrganizeList(@RequestBody List organizeIdList) { + + return storeService.getStoreListByOrganizeList(organizeIdList); + } + + @GetMapping(value = "/store/checkStoreUser") + @Override + public boolean checkStoreUser(String userId) { + + return storeService.checkStoreUser(userId); + } + + /** + * 查询门店信息(未绑定数据库) + * + * @param tenantId 租户id + * @param storeIds 门店ids + * @return java.util.List + */ + @NoDataSourceBind + @Override + @GetMapping(value = "/store/list/noDataSource") + public List getListByIdsNoDataSource(@RequestParam(value = "tenantId") String tenantId, @RequestParam(value = "storeIds") List storeIds) { + + customTenantUtil.checkOutTenant(tenantId); + return storeService.getListByIds(storeIds); + } + + /** + * 更新 + * + * @param id + * @param store + * @return + */ + @PutMapping("/{id}") + @Transactional + @Operation(summary = "更新") + public ActionResult update(@PathVariable("id") String id, @RequestBody @Valid Store store) throws Exception { + try { + storeService.update(id, store); + return ActionResult.success(); + } catch (Exception e) { + e.printStackTrace(); + return ActionResult.fail(e.getMessage()); + } + } + + /** + * 获取用户门店列表信息 + * + * @return 返回值 + */ + @Override + @GetMapping(value = "/userStoreList") + public ActionResult> getUserStoreList() { + return ActionResult.success(storeService.getUserStoreList()); + } + + /** + * 获取用户门店列表信息(值班) + * + * @return 返回值 + */ + @Override + @GetMapping(value = "/userStoreListDuty") + public ActionResult> getUserStoreListDuty() { + return ActionResult.success(storeService.getUserStoreListDuty()); + } + + /** + * 获取用户门店列表信息 + * + * @return 返回值 + */ + @Override + @GetMapping(value = "/listByUserId/{userId}") + public List getListByUserId(@PathVariable(value = "userId") String userId) { + + return storeService.getStoreListByUser(userId); + } + + /** + * 获取所有用户的门店集合 + * + * @return java.util.List + * @author hlp + */ + @Override + @GetMapping(value = "/getAllUserStores") + public List getAllUserStores() { + return storeService.getAllUserStores(); + } + + /** + * 根据门店ids查询门店信息 + * + * @param storeIds 门店ids + * @return java.util.List + */ + @Override + @GetMapping(value = "/listByIds") + public List getListByIds(@RequestParam(value = "storeIds") List storeIds) { + + return storeService.getListByIds(storeIds); + } + + /** + * 批量新增门店 + * + * @param batchSaveStoreDto + * @return ActionResult + */ + @PostMapping("/batchSaveStore") + public ActionResult batchSaveStore(@RequestBody BatchSaveStoreDto batchSaveStoreDto) throws HandleException { + storeService.batchSaveStore(batchSaveStoreDto); + return ActionResult.success(); + } + + /** + * 根据组织id查询门店 + * + * @return + */ + @GetMapping("/queryByOrganizeId/{id}") + public ActionResult queryByOrganizeId(@PathVariable("id") String id) { + List storeList = storeService.getStoreByOrganizeId(id); + return ActionResult.success(storeList); + } + + /** + * 检查门店是否有门店负责人 + * + * @return ActionResult + */ + @GetMapping("/checkHeadId/{id}") + public ActionResult checkHeadId(@PathVariable("id") String id) { + Boolean isHasHead = storeService.checkStore(id); + return ActionResult.success(isHasHead); + } + + /** + * 改变门店状态 禁用/启用 + * + * @param id 门店id + * @param updateStoreDto 是否禁用 1.是 0.否 + * @return ActionResult + */ + @PutMapping("/updateStatus/{id}") + public ActionResult updateStatus(@PathVariable("id") String id, @RequestBody UpdateStoreDto updateStoreDto) { + storeService.updateStatus(id, updateStoreDto.getDisabled()); + return ActionResult.success(); + } + + /** + * 根据组织id获取所有门店(包括禁用门店) + * + * @param organizeId 组织id + * @return ActionResult> + */ + @GetMapping("/getAllStoreByOrgId/{organizeId}") + public ActionResult> getAllByOrgId(@PathVariable("organizeId") String organizeId) { + List allStoreByOrganizeId = storeService.getAllStoreByOrganizeId(organizeId); + return ActionResult.success(allStoreByOrganizeId); + } + + @Override + @Operation(description = "[列表] 目标组织,及其子组织所有已启用的门店") + @GetMapping(value = "/store/list/organizeChildren") + public ActionResult> getStoreListByOrganizeIdAndChildOrganize(@RequestParam(value = "organizeId") String organizeId, @RequestParam(required = false, value = "disabled") Boolean disabled) { + if (disabled == null) { + disabled = false; + } + return ActionResult.success(storeService.getStoreListByOrganizeIdAndChildOrganize(organizeId, disabled)); + } + + @Override + @GetMapping(value = "/storePositionInfo/list/nodata") + @NoDataSourceBind + public List getStorePositionInfoListNodata(@RequestParam(value = "storeId") String storeId, @RequestParam(value = "positionList") List positionList, @RequestParam(value = "tenantId") String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return storeService.getStorePositionInfoListNodata(storeId, positionList, tenantId); + } + + @Override + @GetMapping(value = "/listByUserId/nodata/{userId}") + @NoDataSourceBind + public List getListByUserIdNodata(@PathVariable(value = "userId") String userId, @RequestParam(value = "tenantId") String tenantId) { + customTenantUtil.checkOutTenant(tenantId); + return storeService.getStoreListByUserNodata(userId, tenantId); + } + + /** + * 获取门店位置信息 + * @param storeId 门店id + * @return jnpf.model.store.vo.StoreLocationVO + */ + @Override + @GetMapping(value = "/getStoreLocation") + public StoreLocationVO getStoreLocation(String storeId) { + return storeService.getStoreLocation(storeId); + } + + /** + * 与 {@link StoreService#getStorePageByIds(String, String, Integer, Integer, Integer)} 入参约定一致:空或全空白为「0」表示不按 id 列表过滤。 + */ + private static String joinStoreIdsForLegacyService(List storeIds) { + if (storeIds == null || storeIds.isEmpty()) { + return "0"; + } + if (storeIds.size() == 1 && "0".equals(storeIds.get(0))) { + return "0"; + } + StringBuilder sb = new StringBuilder(); + for (String id : storeIds) { + if (id == null) { + continue; + } + String t = id.trim(); + if (t.isEmpty()) { + continue; + } + if (sb.length() > 0) { + sb.append(','); + } + sb.append(t); + } + if (sb.length() == 0) { + return "0"; + } + return sb.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/TestController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/TestController.java new file mode 100644 index 0000000..4414f9a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/controller/TestController.java @@ -0,0 +1,106 @@ +package jnpf.store.controller; + +import cn.xuyanwu.spring.file.storage.FileInfo; +import jnpf.AppStatisticsApi; +import jnpf.base.ActionResult; +import jnpf.base.vo.DownloadVO; +import jnpf.constant.FileTypeConstant; +import jnpf.file.FileApi; +import jnpf.file.FileUploadApi; +import jnpf.model.vo.SalaryStaticsVo; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.util.RandomUtil; +import jnpf.util.UpUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.commons.CommonsMultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/07/17 + */ +@Slf4j +@RestController +//@Tag(name = "测试管理" , description = "example") +@RequestMapping("/test") +public class TestController { + @Autowired + private UserApi userApi; + + @Autowired + private FileApi fileApi; + @Autowired + private FileUploadApi fileUploadApi; + + @Autowired + private AppStatisticsApi appStatisticsApi; + + @PostMapping("/getTestList") + public ActionResult list()throws IOException { + List list = userApi.getList(); + return ActionResult.success(list); + } + + + @GetMapping("/getSalaryStatics") + public ActionResult getSalaryStatics(@RequestParam List userIds, @RequestParam("startDay") String startDay, @RequestParam("endDay") String endDay)throws IOException { + List salaryStatics = appStatisticsApi.getSalaryStatics(userIds, startDay, endDay); + return ActionResult.success(salaryStatics); + } + @PostMapping("/testUploadFile") + public ActionResult test()throws Exception { + File file = new File("E:\\个人资料\\test.xlsx"); + FileItem fileItem = this.getMultipartFile(file, "test"); + MultipartFile multipartFile = new CommonsMultipartFile(fileItem); + + if (multipartFile.getOriginalFilename().endsWith(".xlsx") || multipartFile.getOriginalFilename().endsWith(".xls")) { + String filePath = fileApi.getPath(FileTypeConstant.TEMPORARY); + String fileName = RandomUtil.uuId() + "." + UpUtil.getFileType(multipartFile); + //上传文件 + FileInfo fileInfo = fileUploadApi.uploadFile(multipartFile, filePath, fileName); + DownloadVO vo = DownloadVO.builder().build(); + vo.setName(fileInfo.getFilename()); + return ActionResult.success(vo); + } else { + return ActionResult.fail("选择文件不符合导入"); + } + + } + + + private FileItem getMultipartFile(File file, String fieldName) { + FileItemFactory factory = new DiskFileItemFactory(16, null); + FileItem item = factory.createItem(fieldName, "text/plain", true, file.getName()); + int bytesRead = 0; + int len = 8192; + byte[] buffer = new byte[len]; + try { + FileInputStream fis = new FileInputStream(file); + OutputStream os = item.getOutputStream(); + while ((bytesRead = fis.read(buffer, 0, len)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.close(); + fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return item; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreMapper.java new file mode 100644 index 0000000..a23f5fa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreMapper.java @@ -0,0 +1,151 @@ +package jnpf.store.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.StoreEntity; +import jnpf.model.store.Store; +import jnpf.model.vo.StoreExecutionVo; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * 门店管理 + * 版本: V3.1.0 + * 版权: 引迈信息技术有限公司(https://www.jnpfsoft.com) + * 作者: JNPF开发平台组 + * 日期: 2023-07-11 + */ +@Component("ftbStoreMapper") +public interface StoreMapper extends SuperMapper { + + void createStore(StoreEntity storeEntity); + + void update(@Param("id") String id, @Param("store") StoreEntity storeEntity); + + StoreEntity getStoreById(String id); + + List getStoreByIds(@Param("list") List ids); + + List getStoresByOrganizeId(@Param("organizeId") String organizeId); + + List getStores(); + + List getStoresByPage(@Param("keyword") String keyword, @Param("searchStatus") Integer searchStatus); + + List getStoresByStorePage(@Param("keyword") String keyword); + + int getStoresCount(@Param("keyword") String keyword); + + List getStoresEnabled(); + + /** + * 判断门店是否已经存在 + * + * @param storeName 门店名称 + * @return int + */ + int getStoreByName(@Param("storeName") String storeName, @Param("storeId") String storeId); + + /** + * 判断门店是否在该组织下存在 + * + * @param storeName 门店名称 + * @param orgId 组织id + * @return int + */ + int getStoreByNameAndOrgId(@Param("storeName") String storeName, @Param("orgId") String orgId); + + /** + * 查询所有门店 + * + * @return java.util.List + */ + List getAllStoreList(@Param("list") List list, @Param("storeName") String storeName); + + /** + * 查询所有门店 + * + * @return java.util.List + */ + List getAllStoreListNew(@Param("list") List list, @Param("storeName") String storeName, @Param("queryStoreIds") List queryStoreIds); + + /** + * 获取登录用户的门店信息 + * + * @param userId 用户id + * @return 返回值 + */ + List getUserStoreList(@Param("userId") String userId); + + /** + * 获取登录用户的门店信息 + * @param userId 用户id + * @return 返回值 + */ + List getUserStoreListDuty(@Param("userId") String userId,@Param("positionIds") List positionIds); + + /** + * 检查哪些用户不能被选择 + * + * @param chooseList 当前选中的人 + * @param exceptList 需要排除的人 + * @return java.util.List + */ + List checkUserCantChoose(@Param("chooseList") List chooseList, @Param("exceptList") List exceptList); + + /** + * 根据组织查询门店列表 + * + * @param organizeIds 组织ids + * @return java.util.List + */ + List getStoreListByOrganizeIds(@Param("list") List organizeIds); + + /** + * 获取所有的门店列表 + * + * @return java.util.List + * @author hlp + */ + List getAllStore(); + + /** + * 根据组织id获取所有门店 + * + * @param organizeId + * @return + */ + List getAllStoreByOrganizeId(@Param("organizeId") String organizeId); + + /** + * 根据门店ids分页查询门店 + * + * @param storeIds 门店ids + * @param storeName 门店名称 + * @param sortNum 是否排序(1: 是, 0: 否) + * @return java.util.List + */ + List getStorePageByIds(@Param("list") List storeIds, @Param("storeName") String storeName, @Param("sortNum") Integer sortNum); + + /** + * 查询异常的门店 + * + * @param storeIds 门店ids + * @return java.util.List + */ + List getAbnormalStoreIds(@Param("list") List storeIds); + + List getStorePageByIds(@Param("list") List storeIds); + + /** + * 校验是否是值班人 + * + * @param userId 用户Id + */ + Integer checkStoreUser(@Param("userId") String userId); + + List queryShopManager(@Param("userId") String userId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreRegionMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreRegionMapper.java new file mode 100644 index 0000000..2036aba --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreRegionMapper.java @@ -0,0 +1,14 @@ +package jnpf.store.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.StoreRegion; +import org.springframework.stereotype.Component; + +/** + * 门店区域管理表 + */ +@Component("ftbStoreRegionMapper") +public interface StoreRegionMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserMapper.java new file mode 100644 index 0000000..ad8b87f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserMapper.java @@ -0,0 +1,37 @@ +package jnpf.store.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.StoreUserEntity; +import jnpf.model.personnels.dto.staff.roster.SimpleStoreUserDto; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 门店管理 + * 版本: V3.1.0 + * 版权: 引迈信息技术有限公司(https://www.jnpfsoft.com) + * 作者: JNPF开发平台组 + * 日期: 2023-07-11 + */ +@Component("ftbStoreUserMapper") +public interface StoreUserMapper extends SuperMapper { + + void createStoreUser(List users); + + List getByStoreId(@Param("storeId") String storeId); + + void deleteByStoreId(String storeId); + + /** + * 查询门店下的成员 + * + * @param storeId 门店id + * @return java.util.List + */ + List getStoreUserList(String storeId); + + SimpleStoreUserDto queryStoreUserInfo(@Param("userId") String userId, @Param("orgId") String orgId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserRelationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserRelationMapper.java new file mode 100644 index 0000000..a65923c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/mapper/StoreUserRelationMapper.java @@ -0,0 +1,31 @@ +package jnpf.store.mapper; + + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.StoreUserRelation; +import jnpf.model.personnels.dto.staff.roster.SimpleStoreUserDto; +import jnpf.model.store.vo.StoreUserRelationVo; +import org.apache.ibatis.annotations.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Set; + +/** + * 用户门店关联表 + */ +@Repository("ftbStoreUserRelationMapper") +public interface StoreUserRelationMapper extends SuperMapper { + /** + * 查询用户门店关联列表 + * @param userIds 用户ID集合 + * @return + */ + List queryList(@Param("userIds") List userIds); + + /** + * 查询门店信息 + */ + List querySimpleStoreUserDto(@Param("userIds") Set userIds); + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreRegionService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreRegionService.java new file mode 100644 index 0000000..a0a78ca --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreRegionService.java @@ -0,0 +1,26 @@ + +package jnpf.store.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.StoreRegion; +import jnpf.model.store.StoreRegionVo; + +import java.util.List; + +/** + * 门店区域管理表 + */ +public interface StoreRegionService extends SuperService { + /** + * 新增、更新、删除门店区域 + * @param storeId + * @param regionList + */ + void batchSaveOrUpdateOrDelete(String storeId, List regionList); + /** + * 新增、更新、删除门店区域 + * @param storeId + * @param regionList + */ + List getStoreRegionList(String storeId, List regionList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreService.java new file mode 100644 index 0000000..bf7f5ee --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreService.java @@ -0,0 +1,199 @@ +package jnpf.store.service; + +import com.github.pagehelper.PageInfo; +import jnpf.base.service.SuperService; +import jnpf.entity.StoreEntity; +import jnpf.exception.HandleException; +import jnpf.model.store.Store; +import jnpf.model.store.StorePositionInfoVo; +import jnpf.model.store.StoreUserNumVo; +import jnpf.model.store.dto.BatchSaveStoreDto; +import jnpf.model.store.dto.StoreUserDto; +import jnpf.model.store.vo.StoreBaseListVO; +import jnpf.model.store.vo.StoreLocationVO; +import jnpf.model.store.vo.StoreUsersVo; +import jnpf.model.store.vo.UserStoreListVo; +import jnpf.model.vo.StoreExecutionVo; + +import java.util.List; + +/** + * 门店管理 + * 版本: V3.1.0 + * 版权: 引迈信息技术有限公司(https://www.jnpfsoft.com) + * 作者: JNPF开发平台组 + * 日期: 2023-07-11 + */ +public interface StoreService extends SuperService { + + + List getStoreByOrganizeId(String organizeId); + + PageInfo getStoreByKeyword(String keyword, int currentPage, int pageSize, Integer searchStatus); + + List getStoreEnabled(); + + List getStoreByRange(String organizeId, String longitude, String latitude); + + Store getInfo(String id); + + StoreEntity getStoreInfoDutyQuery(String id); + + @Deprecated + void create(Store store) throws Exception; + + @Deprecated + void update(String id, Store store) throws Exception; + + //验证表单 + String checkForm(Store form, int i); + + /** + * 查询组织下的门店 + * + * @param organizeId 组织id + * @return java.util.List + */ + List getStoreList(String organizeId, String storeName); + + /** + * 获取登录用户的门店信息 + * + * @return 返回值 + */ + List getUserStoreList(); + + /** + * 获取登录用户的门店信息 + * + * @return 返回值 + */ + List getUserStoreListDuty(); + + + /** + * 检查哪些用户不能被选择 + * + * @param currentChoose 当前被选中的人 + * @param exceptChoose 需要排除的人 + * @return java.util.List + */ + List checkUserCantChoose(String currentChoose, String exceptChoose); + + List getStoreListByUser(String userId); + + List getStoreListByUserNodata(String userId, String tenantId); + + /** + * 根据门店ids查询门店信息 + * + * @param storeIds 门店ids + * @return java.util.List + */ + List getListByIds(List storeIds); + + /** + * 查询门店下岗位的成员数量 + * + * @param storeId 门店id + * @param positionList 岗位id集合 + * @return java.util.List + */ + List getStorePositionInfoList(String storeId, List positionList); + + List getStorePositionInfoListNodata(String storeId, List positionList, String tenantId); + + /** + * 获取门店下成员数 + * + * @param storeIds 门店id + * @return java.lang.Integer + * @author hlp + */ + List getUserNum(List storeIds); + + //==================================2023-12-15新增方法===================== + + /** + * 批量新增门店 + * + * @param batchSaveStoreDto + */ + void batchSaveStore(BatchSaveStoreDto batchSaveStoreDto) throws HandleException; + + /** + * 获取所有用户的门店集合 + * + * @return java.util.List + * @author hlp + */ + List getAllUserStores(); + + Boolean checkStore(String id); + + /** + * 改变门店状态 + * + * @param disabled 是否禁用 1.是 0.否 + */ + void updateStatus(String id, Boolean disabled); + + /** + * 根据组织id获取所有门店 + * + * @param organizeId 组织id + * @return List + */ + List getAllStoreByOrganizeId(String organizeId); + + PageInfo getUsersByStore(String keyword, int currentPage, int pageSize); + + void updateStoreUsers(StoreUserDto storeUserDto); + + /** + * 根据门店ids分页查询门店记录 + * + * @param storeIds 门店ids + * @param storeName 门店名称 + * @param currentPage 当前页码 + * @param pageSize 每页条数 + * @return com.github.pagehelper.PageInfo + */ + PageInfo getStorePageByIds(String storeIds, String storeName, Integer currentPage, Integer pageSize, Integer sortNum); + + /** + * 查询异常的门店 + * + * @param storeIds 门店ids + * @return java.util.List + */ + List getAbnormalStoreIds(List storeIds); + + /** + * 根据组织ids查询门店列表 + * + * @param organizeIdList 组织ids + * @return java.util.List + */ + List getStoreListByOrganizeList(List organizeIdList); + + List getOrgStoreList(String organizeId, List queryStoreIds); + + PageInfo getStorePageByIds(String storeIds, Integer currentPage, Integer pageSize); + + /** + * 校验是否是值班人 + * + * @param userId 用户Id + */ + boolean checkStoreUser(String userId); + + List getStoreListByOrganizeIdAndChildOrganize(String organizeId, Boolean disabled); + + /** + * 获取门店位置信息 + * @param storeId 门店id + * @return jnpf.model.store.vo.StoreLocationVO + */ + StoreLocationVO getStoreLocation(String storeId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreUserService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreUserService.java new file mode 100644 index 0000000..7699d48 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/StoreUserService.java @@ -0,0 +1,26 @@ + +package jnpf.store.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.StoreUserEntity; + +import java.util.List; + +/** + * + * 门店管理 + * 版本: V3.1.0 + * 版权: 引迈信息技术有限公司(https://www.jnpfsoft.com) + * 作者: JNPF开发平台组 + * 日期: 2023-07-11 + */ +public interface StoreUserService extends SuperService { + + void createStoreUsers(List users); + + List getUsersByStoreId(String storeId); + + List getUsersByStoreId(List storeIds); + + void delete(String storeId); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreRegionServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreRegionServiceImpl.java new file mode 100644 index 0000000..a020f54 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreRegionServiceImpl.java @@ -0,0 +1,118 @@ +package jnpf.store.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.StoreRegion; +import jnpf.model.store.StoreRegionVo; +import jnpf.store.mapper.StoreRegionMapper; +import jnpf.store.service.StoreRegionService; +import jnpf.util.RandomUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 门店区域管理表 + */ +@Slf4j +@Service("ftbStoreRegionServiceImpl") +public class StoreRegionServiceImpl extends SuperServiceImpl implements StoreRegionService { + @Autowired + private UserProvider userProvider; + + @Override + public void batchSaveOrUpdateOrDelete(String storeId, List regionList) { + List allList = lambdaQuery().eq(StoreRegion::getStoreId, storeId).orderByDesc(StoreRegion::getCreatorTime).list(); + List regions = allList.stream().filter(m -> m.getDeleteMark().equals(0)).map(StoreRegion::getId).collect(Collectors.toList()); + //新增数据 + List regionAddList = regionList.stream().filter(item -> !regions.contains(item.getId())).collect(Collectors.toList()); + //修改数据 + List regionUpdateList = regionList.stream().filter(item -> regions.contains(item.getId())).collect(Collectors.toList()); + //删除的数据 + List regionReqIdList = regionList.stream().map(StoreRegionVo::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); + List regionDeleteIdList = new ArrayList<>(CollUtil.subtract(regions, regionReqIdList)); + List regionDeleteList = allList.stream().filter(item -> regionDeleteIdList.contains(item.getId())).collect(Collectors.toList()); + List storeRegionVos = CollUtil.newArrayList(); + List storeRegionVoUpdateList = CollUtil.newArrayList(); + Date newDate = new Date(); + String userId = userProvider.get().getUserId(); + for (StoreRegionVo regionVo : regionAddList) { + StoreRegion storeRegion = new StoreRegion(); + BeanUtils.copyProperties(regionVo, storeRegion); + this.initStoreRegionEntity(storeId, storeRegion); + storeRegionVos.add(storeRegion); + } + for (StoreRegionVo regionVo : regionUpdateList) { + StoreRegion region = allList.stream().filter(t -> t.getId().equals(regionVo.getId())).findFirst().orElse(null); + BeanUtils.copyProperties(regionVo, region); + storeRegionVoUpdateList.add(region); + } + for (StoreRegion regionVo : regionDeleteList) { + regionVo.setDeleteMark(1); + regionVo.setDeleteTime(newDate); + regionVo.setDeleteUserId(userId); + storeRegionVoUpdateList.add(regionVo); + } + if (CollUtil.isNotEmpty(storeRegionVos)) { + saveBatch(storeRegionVos); + } + if (CollUtil.isNotEmpty(storeRegionVoUpdateList)) { + updateBatchById(storeRegionVoUpdateList); + } + } + + @Override + public List getStoreRegionList(String storeId, List regionList) { + List allList = lambdaQuery().eq(StoreRegion::getStoreId, storeId).orderByDesc(StoreRegion::getCreatorTime).list(); + List regions = allList.stream().filter(m -> m.getDeleteMark().equals(0)).map(StoreRegion::getId).collect(Collectors.toList()); + //新增数据 + List regionAddList = regionList.stream().filter(item -> !regions.contains(item.getId())).collect(Collectors.toList()); + //修改数据 + List regionUpdateList = regionList.stream().filter(item -> regions.contains(item.getId())).collect(Collectors.toList()); + //删除的数据 + List regionReqIdList = regionList.stream().map(StoreRegionVo::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); + List regionDeleteIdList = new ArrayList<>(CollUtil.subtract(regions, regionReqIdList)); + List regionDeleteList = allList.stream().filter(item -> regionDeleteIdList.contains(item.getId())).collect(Collectors.toList()); + List storeRegionVos = CollUtil.newArrayList(); + Date newDate = new Date(); + String userId = userProvider.get().getUserId(); + for (StoreRegionVo regionVo : regionAddList) { + StoreRegion storeRegion = new StoreRegion(); + BeanUtils.copyProperties(regionVo, storeRegion); + this.initStoreRegionEntity(storeId, storeRegion); + storeRegionVos.add(storeRegion); + } + for (StoreRegionVo regionVo : regionUpdateList) { + StoreRegion region = allList.stream().filter(t -> t.getId().equals(regionVo.getId())).findFirst().orElse(null); + BeanUtils.copyProperties(regionVo, region); + storeRegionVos.add(region); + } + for (StoreRegion regionVo : regionDeleteList) { + regionVo.setDeleteMark(1); + regionVo.setDeleteTime(newDate); + regionVo.setDeleteUserId(userId); + storeRegionVos.add(regionVo); + } + return storeRegionVos; + } + + private void initStoreRegionEntity(String storeId, StoreRegion storeRegion) { + String userId = userProvider.get().getUserId(); + DateTime nowTime = DateTime.now(); + storeRegion.setId(RandomUtil.uuId()); + storeRegion.setStoreId(storeId); + storeRegion.setCreatorUserId(userId); + storeRegion.setCreatorTime(nowTime); + storeRegion.setLastModifyUserId(userId); + storeRegion.setLastModifyTime(nowTime); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreServiceImpl.java new file mode 100644 index 0000000..1464d1f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreServiceImpl.java @@ -0,0 +1,995 @@ +package jnpf.store.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.entity.StoreEntity; +import jnpf.entity.StoreRegion; +import jnpf.entity.StoreUserEntity; +import jnpf.exception.HandleException; +import jnpf.model.store.*; +import jnpf.model.store.dto.BatchSaveStoreDto; +import jnpf.model.store.dto.StoreUserDto; +import jnpf.model.store.vo.StoreBaseListVO; +import jnpf.model.store.vo.StoreLocationVO; +import jnpf.model.store.vo.StoreUsersVo; +import jnpf.model.store.vo.UserStoreListVo; +import jnpf.model.vo.StoreExecutionVo; +import jnpf.permission.FTBApi; +import jnpf.permission.OrganizeApi; +import jnpf.permission.PositionApi; +import jnpf.permission.UserApi; +import jnpf.permission.entity.OrganizeEntity; +import jnpf.permission.entity.UserEntity; +import jnpf.permission.model.organize.OrganizeNewVo; +import jnpf.permission.model.user.*; +import jnpf.store.mapper.StoreMapper; +import jnpf.store.mapper.StoreUserMapper; +import jnpf.store.service.StoreRegionService; +import jnpf.store.service.StoreService; +import jnpf.store.service.StoreUserService; +import jnpf.util.JsonUtil; +import jnpf.util.RandomUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 门店管理 + * 版本: V3.1.0 + * 版权: 引迈信息技术有限公司(https://www.jnpfsoft.com) + * 作者: JNPF开发平台组 + * 日期: 2023-07-11 + */ +@Service +@Slf4j +public class StoreServiceImpl extends SuperServiceImpl implements StoreService { + + @Autowired + private UserProvider userProvider; + + @Autowired + private StoreMapper storeMapper; + + @Autowired + private StoreUserMapper storeUserMapper; + + @Autowired + private StoreUserService storeUserService; + @Autowired + private StoreRegionService storeRegionService; + + @Autowired + private UserApi userApi; + + @Autowired + private PositionApi positionApi; + + @Autowired + private OrganizeApi organizeApi; + + @Autowired + private FTBApi ftbApi; + + @Autowired + private UserApiV2Util userApiV2Util; + + private static int calculateLineDistance(double longitude, double latitude, double longitude2, double latitude2) { + double var2 = longitude; + double var4 = latitude; + double var6 = longitude2; + double var8 = latitude2; + var2 *= 0.01745329251994329D; + var4 *= 0.01745329251994329D; + var6 *= 0.01745329251994329D; + var8 *= 0.01745329251994329D; + double var10 = Math.sin(var2); + double var12 = Math.sin(var4); + double var14 = Math.cos(var2); + double var16 = Math.cos(var4); + double var18 = Math.sin(var6); + double var20 = Math.sin(var8); + double var22 = Math.cos(var6); + double var24 = Math.cos(var8); + double[] var27 = new double[3]; + double[] var28 = new double[3]; + var27[0] = var16 * var14; + var27[1] = var16 * var10; + var27[2] = var12; + var28[0] = var24 * var22; + var28[1] = var24 * var18; + var28[2] = var20; + float v = (float) (Math.asin(Math.sqrt((var27[0] - var28[0]) * (var27[0] - var28[0]) + (var27[1] - var28[1]) * (var27[1] - var28[1]) + (var27[2] - var28[2]) * (var27[2] - var28[2])) / 2.0D) * 1.27420015798544E7D); + return Math.round(v); + } + + @Override + public List getStoreByOrganizeId(String organizeId) { + // 1.根据organizeId查询出对应的门店列表 + // 2.判断门店列表是否为空 + // 2.1.为空,直接返回 + // 2.2.不为空,查询返回 + List storeEntities = storeMapper.getStoresByOrganizeId(organizeId); + if (storeEntities.size() == 0) { + return null; + } + List storesList = storeEntities.stream().map(storeEntity -> JsonUtil.getJsonToBean(storeEntity, Store.class)).collect(Collectors.toList()); + List stores = storesList.stream().map(this::parseStoreDetail).collect(Collectors.toList()); + return stores; + } + + @Override + public PageInfo getStoreByKeyword(String keyword, int currentPage, int pageSize, Integer searchStatus) { + + PageHelper.startPage(currentPage, pageSize); + PageInfo pageEntity = new PageInfo<>(storeMapper.getStoresByPage(keyword, searchStatus)); + List storesList = pageEntity.getList().stream().map(storeEntity -> JsonUtil.getJsonToBean(storeEntity, Store.class)).collect(Collectors.toList()); + List stores = storesList.stream().map(this::parseStoreDetail).collect(Collectors.toList()); + PageInfo page = new PageInfo<>(); + BeanUtils.copyProperties(pageEntity, page); + stores.forEach(store -> { + if ("0".equals(store.getDisabled())) { + store.setDisabled("false"); + store.setDisableStatus("已启用"); + } else if ("1".equals(store.getDisabled())) { + store.setDisabled("true"); + store.setDisableStatus("已禁用"); + } + }); + if (CollUtil.isNotEmpty(stores)) { + stores.sort(Comparator.comparing(Store::getDisabled)); + } + page.setList(stores); + return page; + } + + @Override + public List getStoreEnabled() { + List storeEntities = storeMapper.getStoresEnabled(); + if (storeEntities.size() == 0) { + return new ArrayList<>(); + } + List storesList = storeEntities.stream().map(storeEntity -> JsonUtil.getJsonToBean(storeEntity, Store.class)).collect(Collectors.toList()); + + //门店的用户 组织信息 + List headUserIds = storesList.stream().map(Store::getStoreHeadUserId).collect(Collectors.toList()); + UserInfoMapByIdsPost userInfoMapByIdsPost = new UserInfoMapByIdsPost(); + userInfoMapByIdsPost.setUserIds(headUserIds); + //门店负责人用户信息查询 + Map infoMapByIdsPost = userApi.getInfoMapByIdsPost(userInfoMapByIdsPost); + + //门店组织信息查询 + List orgIdList = new ArrayList<>(); + for (Store store : storesList) { + String[] orgIdArr = store.getOrganizeId().split(","); + orgIdList.addAll(ListUtil.toList(orgIdArr)); + } + + List orgList = organizeApi.getOrganizeByIds(orgIdList); + Map orgMap = orgList.stream().collect(Collectors.toMap(OrganizeEntity::getId, org -> org)); + + + for (Store store : storesList) { + //负责人信息 + PartUserInfoVo partUserInfoVo = infoMapByIdsPost.get(store.getStoreHeadUserId()); + store.setStoreHeadUserName(partUserInfoVo == null ? null : partUserInfoVo.getRealName()); + + //组织信息 + String organizeid = store.getOrganizeId(); + if (organizeid.contains(",")) { + //多个组织 + String[] split = organizeid.split(","); + List orgNameList = new ArrayList<>(); + for (String orgId : split) { + OrganizeEntity organizeEntity = orgMap.get(orgId); + if (organizeEntity != null) { + orgNameList.add(organizeEntity.getFullName()); + } + } + String orgNameJoin = CollectionUtil.join(orgNameList, "/"); + store.setOrganizeName(orgNameJoin); + } else { + //只有一个组织 + OrganizeEntity organizeEntity = orgMap.get(organizeid); + store.setOrganizeName(organizeEntity == null ? null : organizeEntity.getFullName()); + } + } +// List stores = storesList.stream().map(this::parseStoreDetail).collect(Collectors.toList()); + return storesList; + } + + @Override + public List getStoreByRange(String organizeId, String longitude, String latitude) { + // 1.判断organizeId是否为空 + // 1.1.为空,查询所有门店 + // 1.2.不为空,根据organizeId查询门店 + // 2.将门店实体转换为门店模型 + // 3.根据查出的经纬度,和传入的经纬度计算每个门店的距离 + // 4.根据距离排序 + List storeEntities = new ArrayList<>(); + if (StringUtils.isEmpty(organizeId)) { + storeEntities = storeMapper.getStores(); + } else { + storeEntities = storeMapper.getStoresByOrganizeId(organizeId); + } + List storesList = storeEntities.stream().map(storeEntity -> JsonUtil.getJsonToBean(storeEntity, Store.class)).collect(Collectors.toList()); + List stores = storesList.stream().map(store -> { + try { + if (StringUtils.isNotEmpty(store.getLatitude()) && StringUtils.isNotEmpty(store.getLongitude())) { + int distance = this.calculateLineDistance(Double.parseDouble(longitude), Double.parseDouble(latitude), Double.parseDouble(store.getLongitude()), Double.parseDouble(store.getLatitude())); + store.setDistance(distance); + } else { + store.setDistance(0); + } + } catch (Exception e) { + e.printStackTrace(); + log.error("查询门店列表 距离计算失败"); + store.setDistance(0); + } + return store; + }).collect(Collectors.toList()); + Collections.sort(stores, Comparator.comparingInt(Store::getDistance)); + return stores; + } + + @Override + public Store getInfo(String id) { + StoreEntity storeEntity = storeMapper.getStoreById(id); + if (ObjectUtils.isEmpty(storeEntity)) { + return null; + } + List storeUsers = storeUserService.getUsersByStoreId(id); + storeEntity.setUsers(storeUsers); + Store store = JsonUtil.getJsonToBean(storeEntity, Store.class); + List users = store.getUsers().stream().map(this::parseStoreUserDetail).collect(Collectors.toList()); + store.setUsers(users); + store = this.parseStoreDetail(store); + return store; + } + + @Override + public StoreEntity getStoreInfoDutyQuery(String id) { + + return this.getById(id); + } + + @Override + @Transactional + public void create(Store store) throws Exception { + if (store.getSetRegion().equals(0) && store.getRegionList().size() > 1) { + if (store.getRegionList().size() > 1) { + throw new Exception("设置区域开关未打开,区域数量超限"); + } + if (!store.getRegionList().get(0).getIsDefault().equals(1)) { + throw new Exception("参数不正确,是否默认数据未打开"); + } + } + if (store.getSetRegion().equals(1)) { + if (store.getRegionList().stream().map(StoreRegionVo::getIsDefault).collect(Collectors.toList()).contains(1)) { + throw new Exception("参数不正确,设置区域开关已打开,区域列表包含默认数据"); + } + } + // 判断门店是否存在 + int count = storeMapper.getStoreByName(store.getStoreName(), null); + if (count > 0) { + throw new HandleException("门店已存在"); + } + StoreEntity storeEntity = JsonUtil.getJsonToBean(store, StoreEntity.class); + storeEntity = this.initStoreEntity(storeEntity); + storeEntity.setDisabled(StrUtil.isBlank(store.getDisabled()) ? 0 : 1); + storeMapper.createStore(storeEntity); + /** 设置门店区域*/ + storeRegionService.batchSaveOrUpdateOrDelete(storeEntity.getId(), store.getRegionList()); + /** 同步组织门店数量*/ + organizeApi.storeNumAdd(store.getOrganizeId(), 1); + if (storeEntity.getUsers().size() == 0) { + return; + } + String storeId = storeEntity.getId(); + List users = storeEntity.getUsers(); + List storeUserEntities = users.stream().map(storeUserEntity -> this.initStoreUserEntity(storeUserEntity, storeId)).collect(Collectors.toList()); + storeUserService.createStoreUsers(storeUserEntities); + } + + @Override + @Transactional + public void update(String id, Store store) throws Exception { + int count = storeMapper.getStoreByName(store.getStoreName(), id); + if (count > 0) { + throw new Exception("门店已存在"); + } + String userId = userProvider.get().getUserId(); + StoreEntity storeEntity = JsonUtil.getJsonToBean(store, StoreEntity.class); + storeEntity.setLastmodifyuserid(userId); + storeEntity.setLastmodifytime(DateTime.now()); + storeMapper.update(storeEntity.getId(), storeEntity); + /** 新增、更新、删除门店区域*/ + storeRegionService.batchSaveOrUpdateOrDelete(storeEntity.getId(), store.getRegionList()); + } + + /** + * 检查门店是否有负责人 + * + * @param id + */ + @Override + public Boolean checkStore(String id) { + StoreEntity storeEntity = storeMapper.selectById(id); + return storeEntity != null && StrUtil.isNotBlank(storeEntity.getStoreheaduserid()); + } + + @Override + public void updateStatus(String id, Boolean disabled) { + StoreEntity storeEntity = storeMapper.selectById(id); + storeEntity.setDisabled(disabled ? 1 : 0); + //如果是禁用门店则清空负责人信息,和组织信息 + if (disabled) { + storeEntity.setStoreheaduserid(""); + //如果当前组织不存在了,则清空组织信息 + OrganizeEntity organizeEntity = organizeApi.getInfoById(storeEntity.getOrganizeid()); + if (organizeEntity == null) { + storeEntity.setOrganizeid(""); + } + } + storeMapper.updateById(storeEntity); + } + + @Override + public String checkForm(Store form, int i) { + int total = 0; + boolean isUp = StringUtil.isNotEmpty(form.getId()) && !form.getId().equals("0"); + String id = ""; + String countRecover = ""; + if (isUp) { + id = form.getId(); + } + + return countRecover; + } + + @Override + public List getStoreList(String organizeId, String storeName) { + + if (StringUtils.isEmpty(organizeId)) { + // 组织id为空, 查询所有门店 + return storeMapper.getAllStoreList(null, storeName); + } + // 组织id不为空, 查询组织及子组织下的门店 + List organizeIds = organizeApi.getChildrenById(organizeId); + if (organizeIds.isEmpty()) { + return new ArrayList<>(); + } + return storeMapper.getAllStoreList(organizeIds, storeName); + } + + @Override + public List getUserStoreList() { + + return storeMapper.getUserStoreList(userProvider.get().getUserId()); + } + + @Override + public List getUserStoreListDuty() { + UserInfo user = UserProvider.getUser(); + List userRelationEntities = userApi.getUserBoundMoreInfosByUserIdsNoToken(List.of(user.getUserId()), user.getTenantId()); + + List positionIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(userRelationEntities)) { + positionIds.addAll(userRelationEntities.stream().map(UserBoundMoreInfoVO::getPositionId).distinct().collect(Collectors.toList())); + } + return storeMapper.getUserStoreListDuty(userProvider.get().getUserId(), positionIds); + } + + @Override + public List checkUserCantChoose(String currentChoose, String exceptChoose) { + + String[] chooseArray = currentChoose.split(","); + List chooseList = Arrays.asList(chooseArray); + List exceptList = new ArrayList<>(); + if (!StringUtils.isEmpty(exceptChoose)) { + String[] split = exceptChoose.split(","); + exceptList = Arrays.asList(split); + } + return storeMapper.checkUserCantChoose(chooseList, exceptList); + } + + @Override + public List getStoreListByUser(String userId) { + + List organizeList = userApi.getUserOrganizeInfoByUserId(userId); + if (organizeList.isEmpty()) { + return new ArrayList<>(); + } + List organizeIds = organizeList.stream().map(OrganizeInfoVo::getOrganizeId).collect(Collectors.toList()); + return storeMapper.getStoreListByOrganizeIds(organizeIds); + } + + @Override + public List getStoreListByUserNodata(String userId, String tenantId) { + + List organizeList = userApi.getUserOrganizeInfoByUserIdNodata(userId, tenantId); + if (organizeList.isEmpty()) { + return new ArrayList<>(); + } + List organizeIds = organizeList.stream().map(OrganizeInfoVo::getOrganizeId).collect(Collectors.toList()); + return storeMapper.getStoreListByOrganizeIds(organizeIds); + } + + @Override + public List getListByIds(List storeIds) { + + List storeList = storeMapper.getStoreByIds(storeIds); + if (storeList.isEmpty()) { + return new ArrayList<>(); + } + List storesList = JsonUtil.getJsonToList(storeList, Store.class); + return storesList.stream().map(this::parseStoreDetail).collect(Collectors.toList()); + } + + @Override + public List getStorePositionInfoList(String storeId, List positionList) { + + List returnList = new ArrayList<>(); + // 查询门店下的用户 + List userIds = storeUserMapper.getStoreUserList(storeId); + if (userIds.isEmpty()) { + positionList.forEach(position -> { + returnList.add(new StorePositionInfoVo(position)); + }); + return returnList; + } + // 查询用户岗位信息 + List userPositionList = ftbApi.getPositionInfoByUserList(userIds); + Map map = new HashMap<>(); + positionList.forEach(position -> { + StorePositionInfoVo vo = map.get(position); + if (null == vo) { + vo = new StorePositionInfoVo(position); + map.put(position, vo); + } + // 筛选岗位下的门店成员 + List userIdList = userPositionList.stream().filter(v -> v.getPositionId().equals(position)).map(PositionInfoVo::getUserId).distinct().collect(Collectors.toList()); + if (!userIdList.isEmpty()) { + vo.setStoreUserNum(userIdList.size()); + vo.getUserIdList().addAll(userIdList); + } + }); + returnList.addAll(map.values()); + return returnList; + } + + @Override + public List getStorePositionInfoListNodata(String storeId, List positionList, String tenantId) { + + List returnList = new ArrayList<>(); + // 查询门店下的用户 + List userIds = storeUserMapper.getStoreUserList(storeId); + if (userIds.isEmpty()) { + positionList.forEach(position -> { + returnList.add(new StorePositionInfoVo(position)); + }); + return returnList; + } + // 查询用户岗位信息 + List userPositionList = ftbApi.getPositionInfoByUserListNodata(userIds, tenantId); + Map map = new HashMap<>(); + positionList.forEach(position -> { + StorePositionInfoVo vo = map.get(position); + if (null == vo) { + vo = new StorePositionInfoVo(position); + map.put(position, vo); + } + // 筛选岗位下的门店成员 + List userIdList = userPositionList.stream().filter(v -> v.getPositionId().equals(position)).map(PositionInfoVo::getUserId).distinct().collect(Collectors.toList()); + if (!userIdList.isEmpty()) { + vo.setStoreUserNum(userIdList.size()); + vo.getUserIdList().addAll(userIdList); + } + }); + returnList.addAll(map.values()); + return returnList; + } + + @Override + public List getUserNum(List storeIds) { + List storeUserNumVos = new ArrayList<>(); + for (String storeId : storeIds) { + StoreUserNumVo storeUserNumVo = new StoreUserNumVo(); + List userIds = storeUserMapper.getStoreUserList(storeId); + storeUserNumVo.setId(storeId); + storeUserNumVo.setUserNum(null == userIds ? 0 : userIds.size()); + storeUserNumVos.add(storeUserNumVo); + } + return storeUserNumVos; + } + + private StoreEntity initStoreEntity(StoreEntity storeEntity) { + DateTime nowTime = DateTime.now(); + String storeId = RandomUtil.uuId(); + String userId = userProvider.get().getUserId(); + storeEntity.setId(storeId); + storeEntity.setCreatoruserid(userId); + storeEntity.setCreatortime(nowTime); + storeEntity.setLastmodifyuserid(userId); + storeEntity.setLastmodifytime(nowTime); + return storeEntity; + } + + private StoreUserEntity initStoreUserEntity(StoreUserEntity storeUser, String storeId) { + DateTime nowTime = DateTime.now(); + String userId = userProvider.get().getUserId(); + storeUser.setStoreId(storeId); + storeUser.setCreatorUserId(userId); + storeUser.setCreatorTime(nowTime); + storeUser.setLastModifyUserId(userId); + storeUser.setLastModifyTime(nowTime); + return storeUser; + } + + private Store parseStoreDetail(Store store) { + UserEntity user = userApi.getInfoById(store.getStoreHeadUserId()); + store.setStoreHeadUserName(Objects.isNull(user) ? "" : user.getRealName()); + if (StringUtils.contains(store.getOrganizeId(), ",")) { + String[] split = store.getOrganizeId().split(","); + StringBuilder organizeName = new StringBuilder(); + for (int i = 0; i < split.length; i++) { + OrganizeEntity organize = organizeApi.getInfoById(split[i]); + if (Objects.isNull(organize)) { + continue; + } + organizeName.append(organize.getFullName()); + if (i < split.length - 1) { + organizeName.append("/"); + } + } + store.setOrganizeName(organizeName.toString()); + } else { + OrganizeEntity organize = organizeApi.getInfoById(store.getOrganizeId()); + store.setOrganizeName(Objects.isNull(organize) ? "" : organize.getFullName()); + } + //设置区域信息 + if (Objects.isNull(store.getSetRegion())) { + store.setSetRegion(0); + } + if (store.getSetRegion().equals(0)) { + StoreRegion storeRegion = this.storeRegionService.lambdaQuery() + .eq(StoreRegion::getStoreId, store.getId()) + .eq(StoreRegion::getIsDefault, 1) + .eq(StoreRegion::getDeleteMark, 0) + .last("limit 1") + .one(); + if (Objects.isNull(storeRegion)) { + store.setRegionList(List.of(StoreRegionVo.builder() + .isDefault(1) + .deskCount(0) + .seatCount(0) + .build())); + } else { + store.setRegionList(List.of(StoreRegionVo.builder() + .id(storeRegion.getId()) + .isDefault(storeRegion.getIsDefault()) + .name(storeRegion.getName()) + .deskCount(storeRegion.getDeskCount()) + .seatCount(storeRegion.getSeatCount()) + .build())); + } + } else { + List storeRegionList = this.storeRegionService.lambdaQuery() + .eq(StoreRegion::getStoreId, store.getId()) + .eq(StoreRegion::getIsDefault, 0) + .eq(StoreRegion::getDeleteMark, 0) + .list(); + if (CollUtil.isNotEmpty(storeRegionList)) { + store.setRegionList(storeRegionList.stream().map(item -> StoreRegionVo.builder() + .id(item.getId()) + .name(item.getName()) + .isDefault(item.getIsDefault()) + .deskCount(item.getDeskCount()) + .seatCount(item.getSeatCount()) + .build()).collect(Collectors.toList())); + } + } + return store; + } + +// private static int calculateDistance(double lon1, double lat1, double lon2, double lat2) { +// GeometryFactory geometryFactory = new GeometryFactory(); +// Coordinate coord1 = new Coordinate(lon1, lat1); +// Coordinate coord2 = new Coordinate(lon2, lat2); +// Point point1 = geometryFactory.createPoint(coord1); +// Point point2 = geometryFactory.createPoint(coord2); +// +// double distance = point1.distance(point2); +// return (int) Math.round(distance); +// } + + private StoreUser parseStoreUserDetail(StoreUser storeUser) { + List list = userApi.getInfoByIds(Stream.of(storeUser.getUserid()).collect(Collectors.toList())); + if (!list.isEmpty()) { + PartUserInfoVo user = list.get(0); + storeUser.setMobilenum(user.getMobilePhone()); + storeUser.setUsername(user.getRealName()); + storeUser.setPosition(user.getPositionName()); + storeUser.setOrganizeName(user.getOrganizeName()); + storeUser.setAccount(user.getAccount()); + } + return storeUser; + } + + /** + * 批量新增门店 + * + * @param batchSaveStoreDto + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void batchSaveStore(BatchSaveStoreDto batchSaveStoreDto) throws HandleException { + String organizeid = batchSaveStoreDto.getOrganizeid(); + QueryWrapper checkIsExistStoreWrapper = new QueryWrapper<>(); + checkIsExistStoreWrapper.lambda() + .eq(StoreEntity::getOrganizeid, organizeid); + List storeListByOrgId = storeMapper.selectList(checkIsExistStoreWrapper); + if (CollectionUtil.isNotEmpty(storeListByOrgId)) { + return; + } + if (StrUtil.isBlank(batchSaveStoreDto.getOrganizeid())) { + throw new HandleException("请选择组织"); + } + + /** 门店批量新增*/ + List storeList = batchSaveStoreDto.getStoreList(); + List hasStore = new ArrayList<>(); + List storeEntityList = new ArrayList<>(); + List storeRegionList = new ArrayList<>(); + List storeUserList = new ArrayList<>(); + for (Store store : storeList) { + if (store.getSetRegion().equals(0) && store.getRegionList().size() > 1) { + if (store.getRegionList().size() > 1) { + throw new HandleException("设置区域开关未打开,区域数量超限"); + } + if (!store.getRegionList().get(0).getIsDefault().equals(1)) { + throw new HandleException("参数不正确,是否默认数据未打开"); + } + } + if (store.getSetRegion().equals(1)) { + if (store.getRegionList().stream().map(StoreRegionVo::getIsDefault).collect(Collectors.toList()).contains(1)) { + throw new HandleException("参数不正确,设置区域开关已打开,区域列表包含默认数据"); + } + } + // 判断门店是否存在 + int count = storeMapper.getStoreByNameAndOrgId(store.getStoreName(), organizeid); + if (count > 0) { + hasStore.add(store.getStoreName()); + break; + } + StoreEntity storeEntity = JsonUtil.getJsonToBean(store, StoreEntity.class); + storeEntity = this.initStoreEntity(storeEntity); + storeEntity.setDisabled(1); + storeEntity.setOrganizeid(batchSaveStoreDto.getOrganizeid()); + storeEntityList.add(storeEntity); + /** 门店与值班人员id关联*/ + List users = store.getUsers(); + List storeUserEntityList = JsonUtil.getJsonToList(users, StoreUserEntity.class); + if (CollectionUtil.isNotEmpty(users)) { + for (StoreUserEntity user : storeUserEntityList) { + user.setStoreId(storeEntity.getId()); + initStoreUserEntity(user, storeEntity.getId()); + } + } + storeUserList.addAll(storeUserEntityList); + List regionList = storeRegionService.getStoreRegionList(storeEntity.getId(), store.getRegionList()); + if (CollUtil.isNotEmpty(regionList)) { + storeRegionList.addAll(regionList); + } + } + if (CollectionUtil.isNotEmpty(hasStore)) { + throw new HandleException("门店" + CollectionUtil.join(hasStore, ",") + "已存在"); + } + /** 批量新增门店信息*/ + if (CollUtil.isNotEmpty(storeEntityList)) { + saveBatch(storeEntityList); + } + /** 批量新增门店区域列表*/ + if (CollUtil.isNotEmpty(storeRegionList)) { + storeRegionService.saveOrUpdateBatch(storeRegionList); + } + organizeApi.storeNumAdd(batchSaveStoreDto.getOrganizeid(), CollectionUtil.size(storeEntityList)); + if (CollectionUtil.isEmpty(storeUserList)) { + return; + } + /** 批量新增门店值班人员*/ + storeUserService.saveBatch(storeUserList); + } + + @Override + public List getAllUserStores() { + List userStoreListVos = new ArrayList<>(); + List allUserOrgList = userApi.getAllUserOrgList(); + if (null != allUserOrgList && allUserOrgList.size() > 0) { + // 查询所有门店 + List allStore = storeMapper.getAllStore(); + Map> storeMap; + if (null != allStore && allStore.size() > 0) { + storeMap = allStore.stream().collect(Collectors.groupingBy(Store::getOrganizeId)); + } else { + storeMap = null; + } + allUserOrgList.forEach(v -> { + UserStoreListVo userStoreListVo = new UserStoreListVo(); + userStoreListVo.setUserId(v.getUserId()); + if (null != storeMap) { + List list = new ArrayList<>(); + for (String orgId : v.getOrgIds().split(",")) { + List store = storeMap.get(orgId); + if (null != store) { + list.addAll(store); + } + } + userStoreListVo.setStores(list); + } + userStoreListVos.add(userStoreListVo); + }); + } + return userStoreListVos; + } + + @Override + public PageInfo getUsersByStore(String keyword, int currentPage, int pageSize) { + PageHelper.startPage(currentPage, pageSize); + PageInfo pageEntity = new PageInfo<>(storeMapper.getStoresByStorePage(keyword)); + List storesList = pageEntity.getList().stream().map(storeEntity -> JsonUtil.getJsonToBean(storeEntity, StoreUsersVo.class)).collect(Collectors.toList()); + List collect = storesList.stream().map(StoreUsersVo::getId).collect(Collectors.toList()); + if (CollUtil.isEmpty(collect)) { + PageInfo page = new PageInfo<>(); + page.setTotal(pageEntity.getTotal()); + page.setPageNum(currentPage); + page.setPageSize(pageSize); + page.setList(storesList); + return page; + } + List usersByStoreId = storeUserService.getUsersByStoreId(collect); + if (CollUtil.isEmpty(usersByStoreId)) { + PageInfo page = new PageInfo<>(); + page.setTotal(pageEntity.getTotal()); + page.setPageNum(currentPage); + page.setPageSize(pageSize); + page.setList(storesList); + return page; + } + List storeUsers = BeanUtil.copyToList(usersByStoreId, StoreUser.class); + List userIds = usersByStoreId.stream().map(StoreUserEntity::getUserId).collect(Collectors.toList()); + List list = userApi.getInfoByIds(userIds); + Map collect2 = list.stream().collect(Collectors.toMap(PartUserInfoVo::getUserId, vo -> vo)); + // 查询岗位信息 + List organizeIds = usersByStoreId.stream().map(StoreUserEntity::getOrganizeId).collect(Collectors.toList()); + List organizeList = userApi.getOrganizeListByIds(organizeIds); + // 查询岗位信息 + List positionIds = usersByStoreId.stream().map(StoreUserEntity::getPositionId).collect(Collectors.toList()); + List positionList = userApi.getPositionListByIds(positionIds); + // 查询组织信息 + List gradeIds = usersByStoreId.stream().map(StoreUserEntity::getGradeId).collect(Collectors.toList()); + List gradeList = userApi.getGradeListByIds(gradeIds); + // 补全信息 + storeUsers.forEach(storeUser -> { + PartUserInfoVo user = collect2.get(storeUser.getUserid()); + if (Objects.isNull(user)) { + return; + } + OrganizeInfoVo organize = organizeList.stream().filter(v -> v.getOrganizeId().equals(storeUser.getOrganizeId())).findFirst().orElse(null); + PositionInfoVo position = positionList.stream().filter(v -> v.getPositionId().equals(storeUser.getPositionId())).findFirst().orElse(null); + GradeInfoVo grade = gradeList.stream().filter(v -> v.getGradeId().equals(storeUser.getGradeId())).findFirst().orElse(null); + storeUser.setMobilenum(user.getMobilePhone()); + storeUser.setUsername(user.getRealName()); + storeUser.setAccount(user.getAccount()); + storeUser.setOrganizeName(null == organize ? "" : organize.getOrganizeName()); + storeUser.setPosition(null == position ? "" : position.getPositionName()); + storeUser.setGradeName(null == grade ? "" : grade.getGradeName()); + }); + Map> collect1 = storeUsers.stream().collect(Collectors.groupingBy(StoreUser::getStoreid)); + storesList.forEach(vo -> { + List orDefault = collect1.getOrDefault(vo.getId(), CollUtil.newArrayList()); + vo.setUsers(orDefault.stream().filter(user -> StringUtil.isNotEmpty(user.getUsername())).collect(Collectors.toList())); + }); + PageInfo page = new PageInfo<>(); + BeanUtils.copyProperties(pageEntity, page); + page.setList(storesList); + return page; + } + + @Override + public void updateStoreUsers(StoreUserDto storeUserDto) { + storeUserService.delete(storeUserDto.getStoreId()); + if (CollectionUtils.isEmpty(storeUserDto.getUsers())) { + return; + } + List storeUserEntities = BeanUtil.copyToList(storeUserDto.getUsers(), StoreUserEntity.class); + List storeUsers = storeUserEntities.stream().map(storeUser -> + initStoreUserEntity(storeUser, storeUserDto.getStoreId()) + ).collect(Collectors.toList()); + storeUserService.saveBatch(storeUsers); + } + + @Override + public PageInfo getStorePageByIds(String storeIds, String storeName, Integer currentPage, Integer pageSize, Integer sortNum) { + + List storeIdList = new ArrayList<>(); + if (!storeIds.equals("0")) { + String[] split = storeIds.split(","); + storeIdList.addAll(Arrays.asList(split)); + } + // 查询全部 + PageHelper.startPage(currentPage, pageSize); + return new PageInfo<>(storeMapper.getStorePageByIds(storeIdList, storeName, sortNum)); + } + + @Override + public List getAbnormalStoreIds(List storeIds) { + + if (null == storeIds || storeIds.isEmpty()) { + return new ArrayList<>(); + } + return storeMapper.getAbnormalStoreIds(storeIds); + } + + @Override + public List getStoreListByOrganizeList(List organizeIdList) { + + List storeList = storeMapper.getAllStoreList(organizeIdList, null); + //设置组织名称 + if (CollectionUtil.isNotEmpty(storeList)) { + List organizeIds = storeList.stream().map(Store::getOrganizeId).collect(Collectors.toList()); + if (CollectionUtil.isNotEmpty(organizeIds)) { + List organizeName = organizeApi.getOrganizeName(organizeIds); + if (CollectionUtil.isNotEmpty(organizeName)) { + for (Store store : storeList) { + Optional organizeEntity = organizeName.stream().filter(item -> item.getId().equals(store.getOrganizeId())).findFirst(); + organizeEntity.ifPresent(organize -> store.setOrganizeName(organize.getFullName())); + } + } + } + } + return storeList; + } + + @Override + public List getOrgStoreList(String organizeId, List queryStoreIds) { + + // 组织id不为空, 查询组织及子组织下的门店 + List childrenOrgById = organizeApi.getChildrenOrgById(organizeId); + if (null == childrenOrgById || childrenOrgById.isEmpty()) { + return new ArrayList<>(); + } + List organizeIds = childrenOrgById.stream().map(OrganizeNewVo::getOrganizeId).distinct().collect(Collectors.toList()); + List allStoreList = storeMapper.getAllStoreListNew(organizeIds, null, queryStoreIds); + if (null != allStoreList && !allStoreList.isEmpty()) { + Map orgMap = childrenOrgById.stream().collect(Collectors.toMap(OrganizeNewVo::getOrganizeId, a -> a)); + allStoreList.forEach(v -> { + OrganizeNewVo organizeNewVo = orgMap.get(v.getOrganizeId()); + if (null != organizeNewVo) { + v.setOrganizeName(organizeNewVo.getOrganizeName()); + v.setParentOrganizeId(organizeNewVo.getParentOrganizeId()); + v.setParentOrganizeName(organizeNewVo.getParentOrganizeName()); + } + }); + + } + return allStoreList; + } + + @Override + public PageInfo getStorePageByIds(String storeIds, Integer currentPage, Integer pageSize) { + + List storeIdList = new ArrayList<>(); + if (!storeIds.equals("0")) { + String[] split = storeIds.split(","); + storeIdList.addAll(Arrays.asList(split)); + } + // 查询全部 + PageHelper.startPage(currentPage, pageSize); + return new PageInfo<>(storeMapper.getStorePageByIds(storeIdList)); + } + + @Override + public boolean checkStoreUser(String userId) { + return storeMapper.checkStoreUser(userId) > 0; + } + + @Override + public List getStoreListByOrganizeIdAndChildOrganize(String organizeId, Boolean disabled) { + List vos = new ArrayList<>(); + List organizeIds = organizeApi.getChildrenById(organizeId); + if (CollUtil.isEmpty(organizeIds)) { + return vos; + } + this.list(Wrappers.lambdaQuery() + .eq(StoreEntity::getDisabled, disabled) + .in(StoreEntity::getOrganizeid, organizeIds) + ).forEach(storeEntity -> { + vos.add(new StoreBaseListVO(storeEntity.getId(), + storeEntity.getStorename(), + storeEntity.getOrganizeid(), + storeEntity.getStoreheaduserid(), + storeEntity.getAddress(), + storeEntity.getDisabled(), + storeEntity.getLongitude(), + storeEntity.getLatitude() + )); + }); + + return vos; + } + + @Override + public StoreLocationVO getStoreLocation(String storeId) { + if (StringUtils.isEmpty(storeId)) { + return null; + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.select(StoreEntity::getLongitude, StoreEntity::getLatitude, StoreEntity::getAddress); + queryWrapper.eq(StoreEntity::getId, storeId); + queryWrapper.last("limit 1"); + StoreEntity storeEntity = storeMapper.selectOne(queryWrapper); + return StoreLocationVO.convert(storeEntity); + } + + @Override + public List getAllStoreByOrganizeId(String organizeId) { + + List storeEntities = storeMapper.getAllStoreByOrganizeId(organizeId); + if (storeEntities.isEmpty()) { + return null; + } + List storesList = storeEntities.stream().map(storeEntity -> JsonUtil.getJsonToBean(storeEntity, Store.class)).collect(Collectors.toList()); + return storesList.stream().map(this::parseStoreDetail).collect(Collectors.toList()); + } + + /** + * 删除组织下所有门店 + * + * @param organizeId + */ + private void deleteStore(String organizeId) { + /** 先删除该组织下所有门店*/ + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(StoreEntity::getOrganizeid, organizeId); + List storeEntities = storeMapper.selectList(queryWrapper); + if (CollectionUtil.isNotEmpty(storeEntities)) { + List storeIds = storeEntities.stream().map(StoreEntity::getId).collect(Collectors.toList()); + /** 删除门店关联的值班用户*/ + UpdateWrapper deleteStoreUser = new UpdateWrapper<>(); + deleteStoreUser.lambda() + .in(StoreUserEntity::getStoreId, storeIds); + storeUserMapper.delete(deleteStoreUser); + } + UpdateWrapper deleteStoreWrapper = new UpdateWrapper<>(); + deleteStoreWrapper.lambda() + .eq(StoreEntity::getOrganizeid, organizeId); + int deleteNum = storeMapper.delete(deleteStoreWrapper); + organizeApi.storeNumDecr(organizeId, deleteNum); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreUserServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreUserServiceImpl.java new file mode 100644 index 0000000..ed31b87 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/store/service/impl/StoreUserServiceImpl.java @@ -0,0 +1,38 @@ +package jnpf.store.service.impl; + +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.StoreUserEntity; +import jnpf.store.mapper.StoreUserMapper; +import jnpf.store.service.StoreUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service("ftbStoreUserService") +public class StoreUserServiceImpl extends SuperServiceImpl implements StoreUserService { + + @Autowired + private StoreUserMapper storeUserMapper; + + @Override + public void createStoreUsers(List users) { + storeUserMapper.createStoreUser(users); + } + + @Override + public List getUsersByStoreId(String storeId) { + return storeUserMapper.getByStoreId(storeId); + } + + @Override + public List getUsersByStoreId(List storeIds) { + return lambdaQuery().in(StoreUserEntity::getStoreId, storeIds).list(); + } + + + @Override + public void delete(String storeId) { + storeUserMapper.deleteByStoreId(storeId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/StoreCertificatePhotoController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/StoreCertificatePhotoController.java new file mode 100644 index 0000000..f086c90 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/StoreCertificatePhotoController.java @@ -0,0 +1,120 @@ +package jnpf.storecertificatephoto.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.base.ActionResult; +import jnpf.certificate.service.CertificateInstanceService; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoAddReq; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoUpdateReq; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.permission.StoreApi; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.dto.store.StoreBaseInfoIdsQueryDTO; +import jnpf.permission.vo.store.BaseStoreVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.storecertificatephoto.service.StoreCertificatePhotoService; +import jnpf.util.UserProvider; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.constraints.NotBlank; +import java.util.*; +import java.util.stream.Collectors; + +/** + * web门店自定义证件配置照控制器 + */ +@RestController +@Validated +@RequestMapping("/web/store-certificate-photo") +public class StoreCertificatePhotoController { + + @Autowired + private StoreCertificatePhotoService storeCertificatePhotoService; + @Autowired + private CertificateInstanceService certificateInstanceService; + @Autowired + private V2OrganizeApi v2OrganizeApi; + @Autowired + private StoreApi storeApi; + + /** + * 新增门店证件照配置 + * + * @param req 门店证件照请求参数 + * @return 操作结果 + */ + @PostMapping("/add") + public ActionResult add(@Validated @RequestBody StoreCertificatePhotoAddReq req) { + storeCertificatePhotoService.add(req); + return ActionResult.success(); + } + + /** + * 编辑门店自定义证件配置配置 + * + * @param req 门店证件照编辑参数 + * @return 操作结果 + */ + @PutMapping("/update") + public ActionResult update(@Validated @RequestBody StoreCertificatePhotoUpdateReq req) { + storeCertificatePhotoService.update(req); + return ActionResult.success(); + } + + /** + * 删除门店自定义证件配置配置 + * + * @param id 门店证照自定义配置id + * @return 操作结果 + */ + @DeleteMapping("/delete/{id}") + public ActionResult delete(@PathVariable("id") String id) { + Set orgIds = certificateInstanceService.list(new LambdaQueryWrapper() + .eq(CertificateInstanceEntity::getTemplateId,id) + .eq(CertificateInstanceEntity::getEnabledMark,0)) + .stream() + .map(CertificateInstanceEntity::getSubjectId) + .collect(Collectors.toSet()); + if(CollectionUtils.isNotEmpty(orgIds)){ + List baseStoreVOS = storeApi.listBaseInfoByIds(new StoreBaseInfoIdsQueryDTO(orgIds,UserProvider.getUser().getTenantId())); + String orgNames = baseStoreVOS.stream().map(BaseStoreVO::getStoreName).collect(Collectors.joining(",")); + return ActionResult.fail(400,"已被["+orgNames+"]门店使用,不能删除!"); + } + + boolean succ = storeCertificatePhotoService.update(new LambdaUpdateWrapper() + .eq(StoreCertificatePhotoEntity::getId,id) + .set(StoreCertificatePhotoEntity::getEnabledMark,1) + .set(StoreCertificatePhotoEntity::getLastModifyUserId, UserProvider.getLoginUserId()) + .set(StoreCertificatePhotoEntity::getLastModifyTime,new Date())); + return ActionResult.success(); + } + + /** + * 查询门店自定义证件配置列表 + * + * @return 门店证件照列表 + */ + @GetMapping("/query-list") + public ActionResult> queryList() { + return ActionResult.success(storeCertificatePhotoService.queryList()); + } + + /** + * 根据ID查询门店自定义证件配置照详情 + * + * @param id 主键ID + * @return 门店证件照详情 + */ + @GetMapping("/query-info/{id}") + public ActionResult queryInfo(@PathVariable("id") @NotBlank(message = "主键ID不能为空") String id) { + return ActionResult.success(storeCertificatePhotoService.queryInfo(id)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/WarningNoticeController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/WarningNoticeController.java new file mode 100644 index 0000000..2134315 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/controller/WarningNoticeController.java @@ -0,0 +1,109 @@ +package jnpf.storecertificatephoto.controller; + +import jnpf.base.ActionResult; +import jnpf.model.certificate.vo.CertificateTypeOptionVO; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoIdNameVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.req.WarningNoticeSaveReq; +import jnpf.model.warningnotice.vo.WarningNoticeVO; +import jnpf.storecertificatephoto.service.StoreCertificatePhotoService; +import jnpf.storecertificatephoto.service.WarningNoticeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.ArrayList; +import java.util.List; + +/** + * web预警通知控制器 + */ +@RestController +@Validated +@RequestMapping("/web/warning-notice") +public class WarningNoticeController { + + @Autowired + private WarningNoticeService warningNoticeService; + @Autowired + private StoreCertificatePhotoService storeCertificatePhotoService; + + /** + * 保存预警通知配置 + * + * @param req 保存请求参数 + * @return 预警通知配置 + */ + @PostMapping("/save") + public ActionResult save(@Validated @RequestBody WarningNoticeSaveReq req) { + return ActionResult.success("保存成功", warningNoticeService.save(req)); + } + + /** + * 批量保存预警通知配置 + * + * @param reqList 批量保存请求参数 + * @return 预警通知配置列表 + */ + @PostMapping("/saveList") + public ActionResult> saveList( + @RequestBody @NotEmpty(message = "保存参数不能为空") List<@Valid WarningNoticeSaveReq> reqList) { + return ActionResult.success("保存成功", warningNoticeService.saveList(reqList)); + } + + /** + * 根据证照类型查询预警通知配置 + * + * @param type 证照类型 + * @return 预警通知配置 + */ + @GetMapping("/query-by-type") + public ActionResult queryByType(@RequestParam("type") @NotBlank(message = "证照类型,或者模板id不能为空!") String type) { + return ActionResult.success("获取成功", warningNoticeService.queryByType(type)); + } + + /** + * 查询所有预警通知配置 + * + * @return 预警通知配置列表 + */ + @GetMapping("/query-all") + public ActionResult> queryAll() { + return ActionResult.success("获取成功", warningNoticeService.queryAll()); + } + + /** + * 查询证照类型选项。 + * + * @return 证照类型选项 + */ + @GetMapping("/query-certificate-type-list") + public ActionResult> queryCertificateTypeList() { + List customerCertificateList = storeCertificatePhotoService.queryIdNameList(); + List result = new ArrayList<>(CertificateTypeEnum.values().length + customerCertificateList.size()); + for (CertificateTypeEnum type : CertificateTypeEnum.values()){ + CertificateTypeOptionVO option = new CertificateTypeOptionVO(); + option.setLabel(type.getLabel()); + option.setKey(type.getType()); + option.setType(type.getType()); + result.add(option); + } + for (StoreCertificatePhotoIdNameVO customerCertificate : customerCertificateList){ + CertificateTypeOptionVO option = new CertificateTypeOptionVO(); + option.setLabel(customerCertificate.getCertificateName()); + option.setKey(customerCertificate.getId()); + option.setType(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()); + result.add(option); + } + return ActionResult.success(result); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/helper/StoreCertificatePhotoHelper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/helper/StoreCertificatePhotoHelper.java new file mode 100644 index 0000000..bd0bed9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/helper/StoreCertificatePhotoHelper.java @@ -0,0 +1,51 @@ +package jnpf.storecertificatephoto.helper; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Component +public class StoreCertificatePhotoHelper { + + @Autowired + private StoreCertificatePhotoMapper storeCertificatePhotoMapper; + + public Map buildStoreCertificateIdAndNames(Collection templateIds,Integer status){ + if(CollectionUtil.isEmpty(templateIds)){ + return Collections.emptyMap(); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .select(StoreCertificatePhotoEntity::getId, StoreCertificatePhotoEntity::getCertificateName) + .in(StoreCertificatePhotoEntity::getId, templateIds); + if(Objects.nonNull(status)){ + queryWrapper.eq(StoreCertificatePhotoEntity::getStatus, status); + } + return storeCertificatePhotoMapper.selectList(queryWrapper) + .stream() + .collect(Collectors.toMap(StoreCertificatePhotoEntity::getId, StoreCertificatePhotoEntity::getCertificateName)); + } + + public Map buildStoreCertificateById(Collection templateIds,Integer status){ + if(CollectionUtil.isEmpty(templateIds)){ + return Collections.emptyMap(); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .in(StoreCertificatePhotoEntity::getId, templateIds); + if(Objects.nonNull(status)){ + queryWrapper.eq(StoreCertificatePhotoEntity::getStatus, status); + } + return storeCertificatePhotoMapper.selectList(queryWrapper) + .stream() + .collect(Collectors.toMap(StoreCertificatePhotoEntity::getId, v->v)); + } + +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/BaseParamMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/BaseParamMapper.java new file mode 100644 index 0000000..ce14082 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/BaseParamMapper.java @@ -0,0 +1,81 @@ +package jnpf.storecertificatephoto.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.warningnotice.po.FtbParamEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 基础参数Mapper + */ +public interface BaseParamMapper extends SuperMapper { + + /** + * 根据参数键查询参数 + * + * @param key 参数键 + * @return 基础参数 + */ + FtbParamEntity selectByKey(@Param("key") String key); + + /** + * 根据参数键更新参数 + * + * @param sourceKey 原参数键 + * @param entity 基础参数 + * @return 更新条数 + */ + int updateByKey(@Param("sourceKey") String sourceKey, @Param("entity") FtbParamEntity entity); + + /** + * 新增参数 + * + * @param entity 基础参数 + * @return 新增条数 + */ + int insertParam(@Param("entity") FtbParamEntity entity); + + /** + * 批量新增参数 + * + * @param entities 参数列表 + * @return 新增条数 + */ + int batchInsertParam(@Param("entities") List entities); + + /** + * 批量按ID更新参数 + * + * @param entities 参数列表 + * @return 更新条数 + */ + int batchUpdateById(@Param("entities") List entities); + + /** + * 根据key递增参数值 + * + * @param key 参数key + * @param number 递增值 + * @return 影响行数 + */ + int incrementValueByKey(@Param("key") String key, @Param("number") Long number); + + /** + * 根据key删除参数 + * + * @param key 参数key + * @return 影响行数 + */ + int deleteByKey(@Param("key") String key); + + /** + * 根据type删除参数。not key + * + * @param type 参数type + * @param notKey 不包含的key + * @return 影响行数 + */ + int deleteByTypeNotKey(@Param("type") String type,@Param("notKey") String notKey); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoItemMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoItemMapper.java new file mode 100644 index 0000000..d4e4c8f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoItemMapper.java @@ -0,0 +1,11 @@ +package jnpf.storecertificatephoto.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoItemEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 门店证件照子项Mapper + */ +public interface StoreCertificatePhotoItemMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoMapper.java new file mode 100644 index 0000000..36bcb06 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/mapper/StoreCertificatePhotoMapper.java @@ -0,0 +1,11 @@ +package jnpf.storecertificatephoto.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 门店证件照Mapper + */ +public interface StoreCertificatePhotoMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/StoreCertificatePhotoService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/StoreCertificatePhotoService.java new file mode 100644 index 0000000..3573c35 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/StoreCertificatePhotoService.java @@ -0,0 +1,52 @@ +package jnpf.storecertificatephoto.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoAddReq; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoUpdateReq; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoIdNameVO; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoVO; + +import java.util.List; + +/** + * 门店证件照服务 + */ +public interface StoreCertificatePhotoService extends IService { + + /** + * 新增门店证件照配置 + * + * @param req 门店证件照请求参数 + */ + void add(StoreCertificatePhotoAddReq req); + + /** + * 编辑门店证件照配置 + * + * @param req 门店证件照编辑参数 + */ + void update(StoreCertificatePhotoUpdateReq req); + + /** + * 查询门店证件照列表 + * + * @return 门店证件照列表 + */ + List queryList(); + + /** + * 查询门店证件照ID和名称列表 + * + * @return 门店证件照ID和名称列表 + */ + List queryIdNameList(); + + /** + * 根据ID查询门店证件照详情 + * + * @param id 主键ID + * @return 门店证件照详情 + */ + StoreCertificatePhotoVO queryInfo(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/WarningNoticeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/WarningNoticeService.java new file mode 100644 index 0000000..79b938a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/WarningNoticeService.java @@ -0,0 +1,43 @@ +package jnpf.storecertificatephoto.service; + +import jnpf.model.warningnotice.req.WarningNoticeSaveReq; +import jnpf.model.warningnotice.vo.WarningNoticeVO; + +import java.util.List; + +/** + * 预警通知服务 + */ +public interface WarningNoticeService { + + /** + * 保存预警通知配置 + * + * @param req 保存请求参数 + * @return 预警通知配置 + */ + WarningNoticeVO save(WarningNoticeSaveReq req); + + /** + * 批量保存预警通知配置 + * + * @param reqList 批量保存请求参数 + * @return 预警通知配置列表 + */ + List saveList(List reqList); + + /** + * 根据证照类型查询预警通知配置 + * + * @param type 证照类型 + * @return 预警通知配置 + */ + WarningNoticeVO queryByType(String type); + + /** + * 查询所有预警通知配置 + * + * @return 预警通知配置 + */ + List queryAll(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/StoreCertificatePhotoServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/StoreCertificatePhotoServiceImpl.java new file mode 100644 index 0000000..f76f18c --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/StoreCertificatePhotoServiceImpl.java @@ -0,0 +1,518 @@ +package jnpf.storecertificatephoto.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jnpf.certificate.mapper.CertificateInstanceItemMapper; +import jnpf.certificate.mapper.CertificateInstanceMapper; +import jnpf.model.certificate.po.CertificateInstanceEntity; +import jnpf.model.certificate.po.CertificateInstanceItemEntity; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoItemEntity; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoAddReq; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoItemReq; +import jnpf.model.storecertificatephoto.req.StoreCertificatePhotoUpdateReq; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoIdNameVO; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoItemVO; +import jnpf.model.storecertificatephoto.vo.StoreCertificatePhotoVO; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoItemMapper; +import jnpf.storecertificatephoto.mapper.StoreCertificatePhotoMapper; +import jnpf.storecertificatephoto.service.StoreCertificatePhotoService; +import jnpf.util.UserProvider; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 门店证件照服务实现 + */ +@Service +public class StoreCertificatePhotoServiceImpl extends ServiceImpl + implements StoreCertificatePhotoService { + + private static final int ITEM_TYPE_IMAGE = 1; + private static final int ITEM_TYPE_TEXT = 2; + + @Autowired + private StoreCertificatePhotoItemMapper storeCertificatePhotoItemMapper; + @Autowired + private CertificateInstanceItemMapper certificateInstanceItemMapper; + @Autowired + private CertificateInstanceMapper certificateInstanceMapper; + @Autowired + private RedissonClient redissonClient; + private static final String lockPrefix = "certificate:customer:add:"; + + /** + * 新增门店证件照配置 + * + * @param req 门店证件照请求参数 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void add(StoreCertificatePhotoAddReq req) { + validateAddReq(req); + String certificateName = req.getCertificateName().trim(); + + if (existsCertificateName(certificateName, null)) { + throw new RuntimeException("证照名称已存在"); + } + + String lockKey = buildCertificateCustomerAddLock(); + + RLock lock = redissonClient.getLock(lockKey); + try { + lock.lock(10, TimeUnit.SECONDS); + Long count = baseMapper.selectCount(new LambdaQueryWrapper() + .eq(StoreCertificatePhotoEntity::getEnabledMark,0)); + if(Objects.nonNull(count) && count >= 15){ + throw new RuntimeException("超出15个证照,无法添加!"); + } + }finally { + lock.unlock(); + } + + StoreCertificatePhotoEntity entity = req.convert(); + insertPhotoEntity(entity); + + List items = new ArrayList<>(); + items.addAll(buildItems(entity.getId(), req.getImageItemList(), ITEM_TYPE_IMAGE)); + items.addAll(buildItems(entity.getId(), req.getTextItemList(), ITEM_TYPE_TEXT)); + for (StoreCertificatePhotoItemEntity item : items) { + storeCertificatePhotoItemMapper.insert(item); + } + } + + private String buildCertificateCustomerAddLock() { + return lockPrefix+UserProvider.getUser().getTenantId(); + } + + /** + * 编辑门店证件照配置 + * + * @param req 门店证件照编辑参数 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void update(StoreCertificatePhotoUpdateReq req) { + validateAddReq(req); + String id = req.getId().trim(); + StoreCertificatePhotoEntity dbEntity = getEntityById(id); + // 先查询当前模板项,再与传入项对比,识别本次删除项。 + List originItems = queryCurrentItems(id); + List deletedItemIds = collectDeletedItemIds(originItems, req); + // 删除前先校验是否已被证照实例使用。 + validateDeleteItemReference(deletedItemIds); + // 未被使用的项执行逻辑删除。 + deleteItemsByIds(deletedItemIds); + String certificateName = req.getCertificateName().trim(); + if (existsCertificateName(certificateName, id)) { + throw new RuntimeException("证照名称已存在"); + } + + StoreCertificatePhotoEntity entity = req.convert(); + entity.setId(dbEntity.getId()); + entity.setEnabledMark(dbEntity.getEnabledMark()); + updatePhotoEntity(entity); + + if(!dbEntity.getStatus().equals(entity.getStatus())){ + certificateInstanceMapper.update(null,new LambdaUpdateWrapper() + .eq(CertificateInstanceEntity::getTemplateId, req.getId()) + .eq(CertificateInstanceEntity::getCertificateType, CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.getType()) + .eq(CertificateInstanceEntity::getEnabledMark, 0) + .set(CertificateInstanceEntity::getTemplateStatus, req.getStatus()) + .set(CertificateInstanceEntity::getLastModifyUserId, UserProvider.getLoginUserId()) + .set(CertificateInstanceEntity::getLastModifyTime, new Date())); + } + + upsertItems(id, req.getImageItemList(), ITEM_TYPE_IMAGE); + upsertItems(id, req.getTextItemList(), ITEM_TYPE_TEXT); + } + + /** + * 查询门店证件照列表 + * + * @return 门店证件照列表 + */ + @Override + public List queryList() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + queryWrapper.orderByDesc(StoreCertificatePhotoEntity::getCreatorTime); + List entityList = baseMapper.selectList(queryWrapper); + if (CollUtil.isEmpty(entityList)) { + return Collections.emptyList(); + } + + Map> itemMap = queryItemMap(entityList.stream() + .map(StoreCertificatePhotoEntity::getId) + .collect(Collectors.toList())); + return entityList.stream().map(entity -> buildVO(entity, itemMap)).collect(Collectors.toList()); + } + + @Override + public List queryIdNameList() { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.select(StoreCertificatePhotoEntity::getId, StoreCertificatePhotoEntity::getCertificateName); + queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + queryWrapper.orderByDesc(StoreCertificatePhotoEntity::getCreatorTime); + return baseMapper.selectList(queryWrapper).stream() + .map(StoreCertificatePhotoIdNameVO::convert) + .collect(Collectors.toList()); + } + + /** + * 根据ID查询门店证件照详情 + * + * @param id 主键ID + * @return 门店证件照详情 + */ + @Override + public StoreCertificatePhotoVO queryInfo(String id) { + String photoId = id.trim(); + StoreCertificatePhotoEntity entity = getEntityById(photoId); + Map> itemMap = queryItemMap(Collections.singletonList(photoId)); + return buildVO(entity, itemMap); + } + + /** + * 校验新增参数 + * + * @param req 门店证件照请求参数 + */ + private void validateAddReq(StoreCertificatePhotoAddReq req) { + if (req.getImageCount() == null || req.getImageCount() < 0) { + throw new RuntimeException("证照图片数量必须大于等于0"); + } + if (req.getImageCount() != req.getImageItemList().size()) { + throw new RuntimeException("证照图片数量必须与图片名称数量一致"); + } + + if (Objects.nonNull(req.getExpireDateEnabled()) && + 1== req.getExpireDateEnabled()&& + StrUtil.isBlank(req.getExpireDateName())) { + throw new RuntimeException("到期日期名称不能为空"); + } + + validateItemNames(req.getImageItemList(), "图片名称"); + validateItemNames(req.getTextItemList(), "文本名称"); + validateItemIds(req.getImageItemList(), req.getTextItemList()); + } + + /** + * 校验子项名称 + * + * @param itemList 子项列表 + * @param label 提示文案 + */ + private void validateItemNames(List itemList, String label) { + Set nameSet = new LinkedHashSet<>(); + for (StoreCertificatePhotoItemReq item : itemList) { + if (item == null || StrUtil.isBlank(item.getItemName())) { + throw new RuntimeException(label + "不能为空"); + } + if (!nameSet.add(item.getItemName().trim())) { + throw new RuntimeException(label + "不能重复"); + } + } + } + + /** + * 校验更新请求中子项ID不能重复 + * + * @param imageItemList 图片子项列表 + * @param textItemList 文本子项列表 + */ + private void validateItemIds(List imageItemList, + List textItemList) { + Set itemIdSet = new LinkedHashSet<>(); + List allItems = new ArrayList<>(); + if (CollUtil.isNotEmpty(imageItemList)) { + allItems.addAll(imageItemList); + } + if (CollUtil.isNotEmpty(textItemList)) { + allItems.addAll(textItemList); + } + for (StoreCertificatePhotoItemReq itemReq : allItems) { + if (itemReq == null || StrUtil.isBlank(itemReq.getId())) { + continue; + } + String itemId = StrUtil.trim(itemReq.getId()); + if (!itemIdSet.add(itemId)) { + throw new RuntimeException("子项ID不能重复"); + } + } + } + + /** + * 构建门店证件照子项 + * + * @param photoId 门店证件照ID + * @param itemReqList 子项请求列表 + * @param itemType 子项类型 + * @return 子项实体列表 + */ + private List buildItems(String photoId, + List itemReqList, + Integer itemType) { + List itemList = new ArrayList<>(); + for (int i = 0; i < itemReqList.size(); i++) { + StoreCertificatePhotoItemReq itemReq = itemReqList.get(i); + StoreCertificatePhotoItemEntity itemEntity = new StoreCertificatePhotoItemEntity(); + itemEntity.setId(StrUtil.isNotBlank(itemReq.getId()) ? StrUtil.trim(itemReq.getId()) : null); + itemEntity.setPhotoId(photoId); + itemEntity.setItemType(itemType); + itemEntity.setItemName(itemReq.getItemName().trim()); + itemEntity.setTextPrompt(ITEM_TYPE_TEXT == itemType && StrUtil.isNotBlank(itemReq.getTextPrompt()) + ? itemReq.getTextPrompt().trim() + : null); + itemEntity.setSorts((long) i + 1); + itemEntity.setEnabledMark(0); + itemList.add(itemEntity); + } + return itemList; + } + + /** + * 更新时按子项ID进行增改 + * + * @param photoId 门店证件照ID + * @param itemReqList 子项请求列表 + * @param itemType 子项类型 + */ + private void upsertItems(String photoId, List itemReqList, Integer itemType) { + if (CollUtil.isEmpty(itemReqList)) { + return; + } + + List itemIds = itemReqList.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getId())) + .map(item -> StrUtil.trim(item.getId())) + .distinct() + .collect(Collectors.toList()); + + Map dbItemMap = new HashMap<>(); + if (CollUtil.isNotEmpty(itemIds)) { + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.in(StoreCertificatePhotoItemEntity::getId, itemIds); + itemWrapper.eq(StoreCertificatePhotoItemEntity::getPhotoId, photoId); + itemWrapper.eq(StoreCertificatePhotoItemEntity::getEnabledMark, 0); + List dbItems = storeCertificatePhotoItemMapper.selectList(itemWrapper); + dbItemMap = dbItems.stream() + .collect(Collectors.toMap(StoreCertificatePhotoItemEntity::getId, item -> item, (a, b) -> a)); + } + + List items = buildItems(photoId, itemReqList, itemType); + for (StoreCertificatePhotoItemEntity item : items) { + if (StrUtil.isBlank(item.getId())) { + storeCertificatePhotoItemMapper.insert(item); + continue; + } + StoreCertificatePhotoItemEntity dbItem = dbItemMap.get(item.getId()); + if (dbItem == null) { + throw new RuntimeException("子项ID不存在或不属于当前配置"); + } + item.setItemType(itemType); + item.setEnabledMark(dbItem.getEnabledMark()); + storeCertificatePhotoItemMapper.updateById(item); + } + } + + /** + * 判断证照名称是否已存在 + * + * @param certificateName 证照名称 + * @param excludeId 排除的主键ID + * @return 是否存在 + */ + private boolean existsCertificateName(String certificateName, String excludeId) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.eq(StoreCertificatePhotoEntity::getCertificateName, certificateName); + queryWrapper.eq(StoreCertificatePhotoEntity::getEnabledMark, 0); + queryWrapper.ne(StrUtil.isNotBlank(excludeId), StoreCertificatePhotoEntity::getId, excludeId); + return baseMapper.selectCount(queryWrapper) > 0; + } + + /** + * 查询当前模板下所有有效项。 + * + * @param photoId 模板ID + * @return 当前模板项列表 + */ + private List queryCurrentItems(String photoId) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(StoreCertificatePhotoItemEntity::getPhotoId, photoId); + wrapper.eq(StoreCertificatePhotoItemEntity::getEnabledMark, 0); + return storeCertificatePhotoItemMapper.selectList(wrapper); + } + + /** + * 对比原有模板项与本次请求项,得到被删除的模板项ID。 + * + * @param originItems 原有模板项 + * @param req 编辑请求 + * @return 待删除模板项ID列表 + */ + private List collectDeletedItemIds(List originItems, + StoreCertificatePhotoUpdateReq req) { + if (CollUtil.isEmpty(originItems)) { + return Collections.emptyList(); + } + Set currentItemIds = new LinkedHashSet<>(); + appendReqItemIds(currentItemIds, req.getImageItemList()); + appendReqItemIds(currentItemIds, req.getTextItemList()); + + return originItems.stream() + .map(StoreCertificatePhotoItemEntity::getId) + .filter(StrUtil::isNotBlank) + .map(StrUtil::trim) + .filter(itemId -> !currentItemIds.contains(itemId)) + .collect(Collectors.toList()); + } + + /** + * 收集请求中的子项ID(仅收集更新场景中有值的itemId)。 + * + * @param targetSet 目标ID集合 + * @param itemReqList 请求项列表 + */ + private void appendReqItemIds(Set targetSet, List itemReqList) { + if (CollUtil.isEmpty(itemReqList)) { + return; + } + for (StoreCertificatePhotoItemReq itemReq : itemReqList) { + if (itemReq == null || StrUtil.isBlank(itemReq.getId())) { + continue; + } + targetSet.add(StrUtil.trim(itemReq.getId())); + } + } + + /** + * 校验待删除模板项是否已被证照实例使用。 + * + * @param deletedItemIds 待删除模板项ID列表 + */ + private void validateDeleteItemReference(List deletedItemIds) { + if (CollUtil.isEmpty(deletedItemIds)) { + return; + } + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.in(CertificateInstanceItemEntity::getTemplateItemId, deletedItemIds); + wrapper.eq(CertificateInstanceItemEntity::getEnabledMark, 0); + Long count = certificateInstanceItemMapper.selectCount(wrapper); + if (count != null && count > 0) { + throw new RuntimeException("该字段已经被使用,无法删除"); + } + } + + /** + * 执行模板项逻辑删除。 + * + * @param deletedItemIds 待删除模板项ID列表 + */ + private void deleteItemsByIds(List deletedItemIds) { + if (CollUtil.isEmpty(deletedItemIds)) { + return; + } + for (String itemId : deletedItemIds) { + StoreCertificatePhotoItemEntity item = new StoreCertificatePhotoItemEntity(); + item.setId(itemId); + item.setEnabledMark(1); + storeCertificatePhotoItemMapper.updateById(item); + } + } + + /** + * 根据ID查询有效的门店证件照配置。 + * + * @param id 主键ID + * @return 门店证件照实体 + */ + private StoreCertificatePhotoEntity getEntityById(String id) { + StoreCertificatePhotoEntity entity = baseMapper.selectById(id); + if (entity == null || !Integer.valueOf(0).equals(entity.getEnabledMark())) { + throw new RuntimeException("门店证件照配置不存在"); + } + return entity; + } + + /** + * 查询子项映射 + * + * @param photoIds 门店证件照ID列表 + * @return 子项映射 + */ + private Map> queryItemMap(List photoIds) { + if (CollUtil.isEmpty(photoIds)) { + return Collections.emptyMap(); + } + LambdaQueryWrapper itemWrapper = Wrappers.lambdaQuery(); + itemWrapper.in(StoreCertificatePhotoItemEntity::getPhotoId, photoIds); + itemWrapper.eq(StoreCertificatePhotoItemEntity::getEnabledMark, 0); + itemWrapper.orderByAsc(StoreCertificatePhotoItemEntity::getPhotoId) + .orderByAsc(StoreCertificatePhotoItemEntity::getItemType) + .orderByAsc(StoreCertificatePhotoItemEntity::getSorts); + return storeCertificatePhotoItemMapper.selectList(itemWrapper).stream() + .collect(Collectors.groupingBy(StoreCertificatePhotoItemEntity::getPhotoId)); + } + + /** + * 构建门店证件照返回对象 + * + * @param entity 门店证件照实体 + * @param itemMap 子项映射 + * @return 门店证件照返回对象 + */ + private StoreCertificatePhotoVO buildVO(StoreCertificatePhotoEntity entity, + Map> itemMap) { + StoreCertificatePhotoVO vo = StoreCertificatePhotoVO.convert(entity); + List currentItems = itemMap.getOrDefault(entity.getId(), Collections.emptyList()); + vo.setImageItemList(currentItems.stream() + .filter(item -> ITEM_TYPE_IMAGE == item.getItemType()) + .map(StoreCertificatePhotoItemVO::convert) + .collect(Collectors.toList())); + vo.setTextItemList(currentItems.stream() + .filter(item -> ITEM_TYPE_TEXT == item.getItemType()) + .map(StoreCertificatePhotoItemVO::convert) + .collect(Collectors.toList())); + return vo; + } + + /** + * 新增门店证件照主表数据 + * + * @param entity 门店证件照实体 + */ + private void insertPhotoEntity(StoreCertificatePhotoEntity entity) { + try { + baseMapper.insert(entity); + } catch (DuplicateKeyException e) { + throw new RuntimeException("证照名称已存在"); + } + } + + /** + * 更新门店证件照主表数据 + * + * @param entity 门店证件照实体 + */ + private void updatePhotoEntity(StoreCertificatePhotoEntity entity) { + try { + baseMapper.updateById(entity); + } catch (DuplicateKeyException e) { + throw new RuntimeException("证照名称已存在"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/WarningNoticeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/WarningNoticeServiceImpl.java new file mode 100644 index 0000000..633bece --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/storecertificatephoto/service/impl/WarningNoticeServiceImpl.java @@ -0,0 +1,641 @@ +package jnpf.storecertificatephoto.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.model.storecertificatephoto.po.StoreCertificatePhotoEntity; +import jnpf.model.warningnotice.enums.CertificateTypeEnum; +import jnpf.model.warningnotice.po.FtbParamEntity; +import jnpf.model.warningnotice.req.WarningNoticeSaveReq; +import jnpf.model.warningnotice.req.WarningNoticeTargetReq; +import jnpf.model.warningnotice.req.WarningNoticeUserConfigReq; +import jnpf.model.warningnotice.vo.WarningNoticeTargetVO; +import jnpf.model.warningnotice.vo.WarningNoticeUserConfigVO; +import jnpf.model.warningnotice.vo.WarningNoticeVO; +import jnpf.storecertificatephoto.helper.StoreCertificatePhotoHelper; +import jnpf.storecertificatephoto.mapper.BaseParamMapper; +import jnpf.storecertificatephoto.service.WarningNoticeService; +import jnpf.util.FtbUtil; +import jnpf.util.JsonUtil; +import jnpf.util.ServiceException; +import jnpf.util.StringUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 预警通知服务实现 + */ +@Service +public class WarningNoticeServiceImpl implements WarningNoticeService { + + private static final String CONFIG_KEY_PREFIX = "ftbWarningNotice_"; + /** + * 预警配置类型 + */ + public static final String certificateWarnNoticeType = "certificateWarnNoticeType"; + private static final Long DEFAULT_SORT = 0L; + + @Autowired + private BaseParamMapper baseParamMapper; + @Autowired + private PlatformTransactionManager transactionManager; + @Autowired + private StoreCertificatePhotoHelper storeCertificatePhotoHelper; + + /** + * 保存预警通知配置 + * + * @param req 保存请求参数 + * @return 预警通知配置 + */ + @Override + public WarningNoticeVO save(WarningNoticeSaveReq req) { + List result = saveList(Collections.singletonList(req)); + ServiceException.isTrue(CollUtil.isNotEmpty(result), "保存预警配置失败"); + return result.get(0); + } + + /** + * 批量保存预警通知配置 + * + * @param reqList 批量保存请求参数 + * @return 预警通知配置列表 + */ + @Override + public List saveList(List reqList) { + ServiceException.isTrue(CollUtil.isNotEmpty(reqList), "保存参数不能为空"); + + List preparedList = prepareSaveDataList(reqList); + validateBatchUniqueness(preparedList); + BatchParamSplit split = splitById(preparedList); + executeBatch(split); + + // 批量更新/插入后,按key回查最新数据,保证返回包含数据库最终ID + List keys = preparedList.stream() + .map(item -> item.getEntity().getKey()) + .collect(Collectors.toList()); + List result = queryByKeys(keys); + ServiceException.isTrue(result.size() == keys.size(), "保存预警配置失败"); + return result; + } + + /** + * 根据证照类型查询预警通知配置 + * + * @param type 证照类型 + * @return 预警通知配置 + */ + @Override + public WarningNoticeVO queryByType(String type) { + String key = buildConfigKey(type); + FtbParamEntity entity = baseParamMapper.selectByKey(key); + if (entity == null || StrUtil.isBlank(entity.getValue())) { + return null; + } + + WarningNoticeVO warningNoticeVO = buildWarningNoticeVO(entity); + return normalizeWarningNoticeVO(warningNoticeVO); + } + + private WarningNoticeVO buildWarningNoticeVO(FtbParamEntity entity) { + WarningNoticeVO warningNoticeVO = JsonUtil.getJsonToBean(entity.getValue(), WarningNoticeVO.class); + ServiceException.notNull(warningNoticeVO, "预警通知配置数据异常"); + warningNoticeVO.setKey(entity.getKey()); + String certificateType = buildCertificateTypeOrTemplateId(entity.getKey()); + Optional certificateTypeEnumOptional = CertificateTypeEnum.getByType(certificateType); + ServiceException.isTrue(certificateTypeEnumOptional.isPresent(), "预警通知配置数据异常.证照类型异常"); + CertificateTypeEnum certificateTypeEnum = certificateTypeEnumOptional.get(); + warningNoticeVO.setType(certificateTypeEnum.getType()); + warningNoticeVO.setTypeOrTemplateId(certificateType); + warningNoticeVO.setId(entity.getId()); + return warningNoticeVO; + } + + @Override + public List queryAll() { + List ftbParamEntities = baseParamMapper.selectList(new LambdaQueryWrapper().eq(FtbParamEntity::getType, certificateWarnNoticeType)); + if(CollectionUtil.isEmpty(ftbParamEntities)){ + return Collections.emptyList(); + } + return ftbParamEntities.stream() + .map(this::buildWarningNoticeVO) + .collect(Collectors.toList()); + } + + /** + * 构建基础参数实体 + * + * @param saveData 预警通知配置 + * @param reqId 请求中的ID + * @return 基础参数实体 + */ + private FtbParamEntity buildBaseParamEntity(WarningNoticeVO saveData, String reqId) { + FtbParamEntity entity = new FtbParamEntity(); + entity.setId(StrUtil.isNotBlank(reqId) ? normalizeText(reqId) : FtbUtil.getId()); + entity.setKey(saveData.getKey()); + entity.setType(certificateWarnNoticeType); + entity.setValue(JsonUtil.getObjectToString(saveData)); + entity.setSort(DEFAULT_SORT); + return entity; + } + + /** + * 构建保存对象 + * + * @param req 保存请求参数 + * @param certificateType 证照类型 + * @param key 参数键 + * @return 预警通知配置 + */ + private WarningNoticeVO buildSaveData(WarningNoticeSaveReq req, CertificateTypeEnum certificateType, String key) { + WarningNoticeVO warningNoticeVO = new WarningNoticeVO(); + warningNoticeVO.setKey(key); + warningNoticeVO.setType(certificateType.getType()); + warningNoticeVO.setExpiryReminderDays(req.getExpiryReminderDays()); + warningNoticeVO.setNoticeFrequencyDays(req.getNoticeFrequencyDays()); + + List noticeConfigList = new ArrayList<>(); + for (WarningNoticeUserConfigReq configReq : req.getNoticeConfigList()) { + WarningNoticeUserConfigVO configVO = new WarningNoticeUserConfigVO(); + configVO.setNoticeUserType(normalizeText(configReq.getNoticeUserType())); + + List noticeUserList = new ArrayList<>(); + for (WarningNoticeTargetReq targetReq : configReq.getNoticeUserList()) { + WarningNoticeTargetVO targetVO = new WarningNoticeTargetVO(); + targetVO.setId(normalizeText(targetReq.getId())); + targetVO.setName(normalizeText(targetReq.getName())); + noticeUserList.add(targetVO); + } + configVO.setNoticeUserList(noticeUserList); + noticeConfigList.add(configVO); + } + warningNoticeVO.setNoticeConfigList(noticeConfigList); + return warningNoticeVO; + } + + /** + * 规范化返回对象 + * + * @param warningNoticeVO 预警通知配置 + * @return 预警通知配置 + */ + private WarningNoticeVO normalizeWarningNoticeVO(WarningNoticeVO warningNoticeVO) { + warningNoticeVO.setId(normalizeText(warningNoticeVO.getId())); + warningNoticeVO.setKey(normalizeText(warningNoticeVO.getKey())); + warningNoticeVO.setType(normalizeText(warningNoticeVO.getType())); + if (warningNoticeVO.getNoticeConfigList() == null) { + warningNoticeVO.setNoticeConfigList(new ArrayList<>()); + return warningNoticeVO; + } + + for (WarningNoticeUserConfigVO configVO : warningNoticeVO.getNoticeConfigList()) { + configVO.setNoticeUserType(normalizeText(configVO.getNoticeUserType())); + if (configVO.getNoticeUserList() == null) { + configVO.setNoticeUserList(new ArrayList<>()); + continue; + } + for (WarningNoticeTargetVO targetVO : configVO.getNoticeUserList()) { + targetVO.setId(normalizeText(targetVO.getId())); + targetVO.setName(normalizeText(targetVO.getName())); + } + } + return warningNoticeVO; + } + + /** + * 校验通知人员配置 + * + * @param noticeConfigList 通知人员配置列表 + */ + private void validateNoticeConfigList(List noticeConfigList) { + ServiceException.isTrue(CollUtil.isNotEmpty(noticeConfigList), "临期通知人员不能为空"); + Set noticeTypeSet = new HashSet<>(); + for (WarningNoticeUserConfigReq configReq : noticeConfigList) { + ServiceException.notNull(configReq, "通知人员配置不能为空"); + String noticeUserType = normalizeText(configReq.getNoticeUserType()); + ServiceException.isTrue(StrUtil.isNotBlank(noticeUserType), "通知人员类型不能为空"); + ServiceException.isTrue(noticeTypeSet.add(noticeUserType), "通知人员类型不能重复"); + validateNoticeTargetList(configReq.getNoticeUserList()); + } + } + + /** + * 校验通知对象列表 + * + * @param noticeUserList 通知对象列表 + */ + private void validateNoticeTargetList(List noticeUserList) { + ServiceException.isTrue(CollUtil.isNotEmpty(noticeUserList), "通知对象不能为空"); + Set targetIdSet = new HashSet<>(); + for (WarningNoticeTargetReq targetReq : noticeUserList) { + ServiceException.notNull(targetReq, "通知对象不能为空"); + String id = normalizeText(targetReq.getId()); + String name = normalizeText(targetReq.getName()); + ServiceException.isTrue(StrUtil.isNotBlank(id), "通知对象ID不能为空"); + ServiceException.isTrue(targetIdSet.add(id), "同一通知人员类型下的通知对象不能重复"); + } + } + + /** + * 生成配置键 + * + * @param certificateType 证照类型 + * @return 配置键 + */ + private String buildConfigKey(CertificateTypeEnum certificateType,String typeOrTemplateId) { + if(CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.equals(certificateType)){ + return buildConfigKey(typeOrTemplateId); + } + return buildConfigKey(certificateType.getType()); + } + + /** + * 生成配置键 + * + * @param certificateType 证照类型 + * @return 配置键 + */ + private String buildConfigKey(String certificateType) { + return CONFIG_KEY_PREFIX + certificateType; + } + + /** + * 根据配置键找出证照类型,如果是自定义证照,则是模板id + * + * @param key 配置键 + * @return 证照类型 + */ + private String buildCertificateTypeOrTemplateId(String key) { + if(StringUtil.isBlank(key)){ + return null; + } + return key.substring(CONFIG_KEY_PREFIX.length()); + } + + /** + * 规范化文本 + * + * @param text 原始文本 + * @return 规范化后的文本 + */ + private String normalizeText(String text) { + return StrUtil.trim(text); + } + + /** + * 批量执行保存。 + * 先按ID分流为更新/插入;若出现唯一键冲突,则根据type全量回查后重新分流并重试一次。 + * + * @param split 批量分流数据 + */ + private void executeBatch(BatchParamSplit split) { + try { + executeBatchOnceInTransaction(split); + } catch (Exception e) { + if (!isDuplicateKeyException(e)) { + throw e; + } + BatchParamSplit retrySplit = rebuildSplitByTypeFromDbInNewTransaction(split.getAllEntities()); + try { + executeBatchOnceInTransaction(retrySplit); + } catch (Exception ex) { + if (!isDuplicateKeyException(ex)) { + throw ex; + } + throw new ServiceException("该证照类型的预警配置已存在"); + } + } + } + + /** + * 单次执行批量更新+批量插入。 + * + * @param split 分流结果 + */ + private void executeBatchOnce(BatchParamSplit split) { + if (split == null || CollUtil.isEmpty(split.getAllEntities())) { + return; + } + if (CollUtil.isNotEmpty(split.getUpdateEntities())) { + baseParamMapper.batchUpdateById(split.getUpdateEntities()); + } + if (CollUtil.isNotEmpty(split.getInsertEntities())) { + baseParamMapper.batchInsertParam(split.getInsertEntities()); + } + } + + /** + * 在事务中执行一次批量写入。 + * + * @param split 分流结果 + */ + private void executeBatchOnceInTransaction(BatchParamSplit split) { + TransactionTemplate template = buildTransactionTemplate(TransactionDefinition.PROPAGATION_REQUIRED, false); + template.execute(status -> { + executeBatchOnce(split); + return null; + }); + } + + /** + * 预处理批量请求:校验参数并构建VO/实体。 + * + * @param reqList 请求列表 + * @return 预处理结果列表 + */ + private List prepareSaveDataList(List reqList) { + List result = new ArrayList<>(reqList.size()); + Map stringStoreCertificatePhotoEntityMap = storeCertificatePhotoHelper.buildStoreCertificateById(reqList.stream() + .map(WarningNoticeSaveReq::getTypeOrTemplateId) + .filter(this::isStoreCertificateType) + .collect(Collectors.toSet()),null); + + for (WarningNoticeSaveReq req : reqList) { + ServiceException.notNull(req, "保存参数不能为空"); + String typeOrTemplateId = req.getTypeOrTemplateId(); + Optional certificateTypeEnumOptional= CertificateTypeEnum.getByType(typeOrTemplateId); + ServiceException.isTrue(certificateTypeEnumOptional.isPresent(), "证照类型不能为空"); + CertificateTypeEnum certificateType = certificateTypeEnumOptional.get(); + Integer expiryReminderDays = req.getExpiryReminderDays(); + if (certificateType != CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE) { + ServiceException.notNull(expiryReminderDays, "临期提醒时间不能为空"); + ServiceException.isTrue(expiryReminderDays >= 0, "临期提醒时间必须大于等于0"); + ServiceException.isTrue(expiryReminderDays<=180, "临期提醒时间最大180"); + }else{ + StoreCertificatePhotoEntity storeCertificatePhotoEntity = stringStoreCertificatePhotoEntityMap.get(typeOrTemplateId); + ServiceException.notNull(storeCertificatePhotoEntity, "自定义证照不存在["+typeOrTemplateId+"]"); + expiryReminderDays = storeCertificatePhotoEntity.getExpiryReminderDays(); + } + ServiceException.notNull(req.getNoticeFrequencyDays(), "通知频率不能为空"); + if(Objects.nonNull(expiryReminderDays) && expiryReminderDays > 0){ + ServiceException.isTrue(req.getNoticeFrequencyDays() <= expiryReminderDays, "通知频率必须小于等于临期提醒"); + } +// validateNoticeConfigList(req.getNoticeConfigList()); + + String key = buildConfigKey(certificateType,typeOrTemplateId); + WarningNoticeVO saveData = buildSaveData(req, certificateType, key); + String reqId = normalizeText(req.getId()); + FtbParamEntity entity = buildBaseParamEntity(saveData, reqId); + result.add(new WarningNoticePreparedData(typeOrTemplateId, reqId, entity)); + } + return result; + } + + private boolean isStoreCertificateType(String typeOrTemplateId) { + Optional certificateTypeEnumOptional= CertificateTypeEnum.getByType(typeOrTemplateId); + ServiceException.isTrue(certificateTypeEnumOptional.isPresent(), "证照类型不能为空"); + CertificateTypeEnum certificateType = certificateTypeEnumOptional.get(); + return CertificateTypeEnum.STORE_CUSTOM_CERTIFICATE.equals(certificateType); + } + + /** + * 按是否有ID进行分流:有ID走更新,无ID走插入。 + * + * @param preparedList 预处理结果列表 + * @return 分流结果 + */ + private BatchParamSplit splitById(List preparedList) { + List updateEntities = new ArrayList<>(); + List insertEntities = new ArrayList<>(); + List allEntities = new ArrayList<>(); + for (WarningNoticePreparedData data : preparedList) { + FtbParamEntity entity = data.getEntity(); + String reqId = data.getReqId(); + allEntities.add(entity); + if (StrUtil.isNotBlank(reqId)) { + updateEntities.add(entity); + } else { + insertEntities.add(entity); + } + } + return new BatchParamSplit(updateEntities, insertEntities, allEntities); + } + + /** + * 批量参数唯一性校验。 + * 同一次请求中同一type只能出现一次,同一id只能出现一次。 + * + * @param preparedList 预处理列表 + */ + private void validateBatchUniqueness(List preparedList) { + Set typeSet = new HashSet<>(); + Set reqIdSet = new HashSet<>(); + for (WarningNoticePreparedData data : preparedList) { + String type = normalizeText(data.getType()); + ServiceException.isTrue(StrUtil.isNotBlank(type), "证照类型不能为空"); + ServiceException.isTrue(typeSet.add(type), "同一次请求中证照类型不能重复"); + + String reqId = normalizeText(data.getReqId()); + if (StrUtil.isBlank(reqId)) { + continue; + } + ServiceException.isTrue(reqIdSet.add(reqId), "同一次请求中ID不能重复"); + } + } + + /** + * duplicate key 后按 type 全量回查,重建更新/插入集合。 + * + * @param entities 本次要保存的实体 + * @return 重建后的分流结果 + */ + private BatchParamSplit rebuildSplitByTypeFromDb(List entities) { + if (CollUtil.isEmpty(entities)) { + return new BatchParamSplit(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + List dbList = baseParamMapper.selectList( + new LambdaQueryWrapper().eq(FtbParamEntity::getType, certificateWarnNoticeType) + ); + Map dbByKey = dbList.stream() + .filter(Objects::nonNull) + .filter(item -> StrUtil.isNotBlank(item.getKey())) + .collect(Collectors.toMap(item -> StrUtil.trim(item.getKey()), item -> item, (a, b) -> a)); + + List updateEntities = new ArrayList<>(); + List insertEntities = new ArrayList<>(); + List allEntities = new ArrayList<>(); + for (FtbParamEntity entity : entities) { + if (entity == null || StrUtil.isBlank(entity.getKey())) { + continue; + } + FtbParamEntity saveEntity = cloneParamEntity(entity); + FtbParamEntity dbEntity = dbByKey.get(StrUtil.trim(saveEntity.getKey())); + if (dbEntity != null && StrUtil.isNotBlank(dbEntity.getId())) { + saveEntity.setId(StrUtil.trim(dbEntity.getId())); + updateEntities.add(saveEntity); + } else { + if (StrUtil.isBlank(saveEntity.getId())) { + saveEntity.setId(FtbUtil.getId()); + } + insertEntities.add(saveEntity); + } + allEntities.add(saveEntity); + } + return new BatchParamSplit(updateEntities, insertEntities, allEntities); + } + + /** + * 在新事务中按type回查并重建分流结果。 + * + * @param entities 本次要保存的实体 + * @return 重建后的分流结果 + */ + private BatchParamSplit rebuildSplitByTypeFromDbInNewTransaction(List entities) { + TransactionTemplate template = buildTransactionTemplate(TransactionDefinition.PROPAGATION_REQUIRES_NEW, true); + BatchParamSplit split = template.execute(status -> rebuildSplitByTypeFromDb(entities)); + return split == null + ? new BatchParamSplit(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()) + : split; + } + + /** + * 构建事务模板。 + * + * @param propagationBehavior 事务传播级别 + * @param readOnly 是否只读 + * @return 事务模板 + */ + private TransactionTemplate buildTransactionTemplate(int propagationBehavior, boolean readOnly) { + TransactionTemplate template = new TransactionTemplate(transactionManager); + template.setPropagationBehavior(propagationBehavior); + template.setReadOnly(readOnly); + return template; + } + + /** + * 按key列表查询预警配置,返回值与入参顺序一致。 + * + * @param keys 配置key列表 + * @return 预警配置列表 + */ + private List queryByKeys(List keys) { + if (CollUtil.isEmpty(keys)) { + return Collections.emptyList(); + } + List dbList = baseParamMapper.selectList( + new LambdaQueryWrapper() + .eq(FtbParamEntity::getType, certificateWarnNoticeType) + .in(FtbParamEntity::getKey, keys) + ); + Map voByKey = dbList.stream() + .filter(Objects::nonNull) + .filter(item -> StrUtil.isNotBlank(item.getKey())) + .collect(Collectors.toMap( + item -> StrUtil.trim(item.getKey()), + item -> normalizeWarningNoticeVO(buildWarningNoticeVO(item)), + (a, b) -> a + )); + + List result = new ArrayList<>(keys.size()); + for (String key : keys) { + WarningNoticeVO vo = voByKey.get(StrUtil.trim(key)); + if (vo != null) { + result.add(vo); + } + } + return result; + } + + /** + * 复制参数实体,避免重试时修改原对象。 + * + * @param source 原实体 + * @return 复制后的实体 + */ + private FtbParamEntity cloneParamEntity(FtbParamEntity source) { + FtbParamEntity target = new FtbParamEntity(); + target.setId(normalizeText(source.getId())); + target.setKey(normalizeText(source.getKey())); + target.setType(normalizeText(source.getType())); + target.setValue(source.getValue()); + target.setSort(source.getSort()); + return target; + } + + /** + * 判断异常链中是否包含唯一键冲突。 + * + * @param throwable 异常 + * @return 是否唯一键冲突 + */ + private boolean isDuplicateKeyException(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof DuplicateKeyException) { + return true; + } + String message = current.getMessage(); + if (StrUtil.isNotBlank(message) + && StrUtil.containsIgnoreCase(message, "duplicate") + && StrUtil.containsIgnoreCase(message, "key")) { + return true; + } + current = current.getCause(); + } + return false; + } + + /** + * 预处理数据载体。 + */ + private static class WarningNoticePreparedData { + private final String type; + private final String reqId; + private final FtbParamEntity entity; + + private WarningNoticePreparedData(String type, String reqId, FtbParamEntity entity) { + this.type = type; + this.reqId = reqId; + this.entity = entity; + } + + private String getType() { + return type; + } + + private String getReqId() { + return reqId; + } + + private FtbParamEntity getEntity() { + return entity; + } + } + + /** + * 批量更新/插入分流结果。 + */ + private static class BatchParamSplit { + private final List updateEntities; + private final List insertEntities; + private final List allEntities; + + private BatchParamSplit(List updateEntities, + List insertEntities, + List allEntities) { + this.updateEntities = updateEntities; + this.insertEntities = insertEntities; + this.allEntities = allEntities; + } + + private List getUpdateEntities() { + return updateEntities; + } + + private List getInsertEntities() { + return insertEntities; + } + + private List getAllEntities() { + return allEntities; + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/controller/TempModuleController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/controller/TempModuleController.java new file mode 100644 index 0000000..babaae2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/controller/TempModuleController.java @@ -0,0 +1,105 @@ +package jnpf.tempmodule.controller; + +import jnpf.base.ActionResult; +import jnpf.model.tempmodule.AppBannerVo; +import jnpf.model.tempmodule.RollImageVo; +import jnpf.model.tempmodule.dto.RollImageDto; +import jnpf.tempmodule.service.AppBannerService; +import jnpf.tempmodule.service.RollImageService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; + +/** + * 临时功能控制器 + * + * @author yanwenfu + * @create 2023-10-10 + */ +@RestController +@RequestMapping(value = "/tempmodule") +public class TempModuleController { + + @Resource + private RollImageService rollImageService; + + @Resource + private AppBannerService appBannerService; + + @GetMapping(value = "/banner/list") + public ActionResult> getAppBannerList(@RequestHeader(value = "User-Agent", required = false) String userAgent) { + + if (StringUtils.isNotEmpty(userAgent)) { + if (userAgent.equals("1 android") || userAgent.equals("1 iphone")) { + List list = rollImageService.getRollImageList(0); + if (list.isEmpty()) { + return ActionResult.success(new ArrayList<>()); + } + List bannerList = new ArrayList<>(); + list.forEach(rollImage -> { + AppBannerVo bannerVo = new AppBannerVo(rollImage.getImageUrl(), rollImage.getJumpTo(), rollImage.getSort()); + bannerList.add(bannerVo); + }); + return ActionResult.success(bannerList); + } + } + List list = appBannerService.getAppBannerList(); + return ActionResult.success(list); + } + + /** + * pc端获取banner列表 + * @return {@link ActionResult }<{@link List }<{@link AppBannerVo }>> + */ + @GetMapping(value = "/banner/list-pc") + public ActionResult> getAppBannerListPc() { + List list = rollImageService.getRollImageList(1); + if (list.isEmpty()) { + return ActionResult.success(new ArrayList<>()); + } + List bannerList = new ArrayList<>(); + list.forEach(rollImage -> { + AppBannerVo bannerVo = new AppBannerVo(rollImage.getImageUrl(), rollImage.getJumpTo(), rollImage.getSort()); + bannerList.add(bannerVo); + }); + return ActionResult.success(bannerList); + } + + /** + * 获取banner列表 + * @param bannerType banner类型 0app1pc + * @return {@link ActionResult }<{@link List }<{@link RollImageVo }>> + */ + @GetMapping(value = "/rollImage/list") + public ActionResult> getRollImageList(Integer bannerType) { + List list = rollImageService.getRollImageList(bannerType); + return ActionResult.success(list); + } + + /** + * 新增轮播图 + * @param rollImageDto 滚动图dto + * @return jnpf.base.ActionResult + */ + @PostMapping(value = "/rollImage") + public ActionResult addRollImage(@RequestBody @Valid RollImageDto rollImageDto) { + rollImageService.addRollImage(rollImageDto); + return ActionResult.success(); + } + + /** + * 删除轮播图 + * @param id 轮播图id + * @return jnpf.base.ActionResult + */ + @DeleteMapping(value = "/rollImage/{id}") + public ActionResult deleteRollImage(@PathVariable(value = "id") String id) { + + rollImageService.deleteRollImage(id); + return ActionResult.success(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/AppBannerMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/AppBannerMapper.java new file mode 100644 index 0000000..765cc21 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/AppBannerMapper.java @@ -0,0 +1,13 @@ +package jnpf.tempmodule.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.AppBanner; + +/** + * banner mapper + * + * @author yanwenfu + * @create 2023-10-10 + */ +public interface AppBannerMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/RollImageMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/RollImageMapper.java new file mode 100644 index 0000000..bd8027a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/mapper/RollImageMapper.java @@ -0,0 +1,14 @@ +package jnpf.tempmodule.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.AppBanner; +import jnpf.entity.RollImage; + +/** + * 轮播图mapper + * + * @author yanwenfu + * @create 2023-10-10 + */ +public interface RollImageMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/AppBannerService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/AppBannerService.java new file mode 100644 index 0000000..86e99bb --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/AppBannerService.java @@ -0,0 +1,16 @@ +package jnpf.tempmodule.service; + +import jnpf.model.tempmodule.AppBannerVo; + +import java.util.List; + +/** + * banner服务 + * + * @author yanwenfu + * @create 2023-10-10 + */ +public interface AppBannerService { + + List getAppBannerList(); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/RollImageService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/RollImageService.java new file mode 100644 index 0000000..c342aaf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/RollImageService.java @@ -0,0 +1,30 @@ +package jnpf.tempmodule.service; + +import jnpf.model.tempmodule.RollImageVo; +import jnpf.model.tempmodule.dto.RollImageDto; + +import javax.validation.Valid; +import java.util.List; + +/** + * 轮播图服务 + * + * @author yanwenfu + * @create 2023-10-10 + */ +public interface RollImageService { + + List getRollImageList(Integer bannerType); + + /** + * 新增轮播图 + * @param rollImageDto 滚动图dto + */ + void addRollImage(@Valid RollImageDto rollImageDto); + + /** + * 删除轮播图 + * @param id 轮播图id + */ + void deleteRollImage(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/AppBannerServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/AppBannerServiceImpl.java new file mode 100644 index 0000000..3230601 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/AppBannerServiceImpl.java @@ -0,0 +1,34 @@ +package jnpf.tempmodule.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.entity.AppBanner; +import jnpf.model.tempmodule.AppBannerVo; +import jnpf.tempmodule.mapper.AppBannerMapper; +import jnpf.tempmodule.service.AppBannerService; +import jnpf.util.JsonUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * banner服务实现 + * + * @author yanwenfu + * @create 2023-10-10 + */ +@Service +public class AppBannerServiceImpl implements AppBannerService { + + @Resource + private AppBannerMapper appBannerMapper; + + @Override + public List getAppBannerList() { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .orderByAsc(AppBanner::getSort); + List list = appBannerMapper.selectList(queryWrapper); + return JsonUtil.getJsonToList(list, AppBannerVo.class); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/RollImageServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/RollImageServiceImpl.java new file mode 100644 index 0000000..73c371f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/tempmodule/service/impl/RollImageServiceImpl.java @@ -0,0 +1,71 @@ +package jnpf.tempmodule.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.entity.RollImage; +import jnpf.model.tempmodule.RollImageVo; +import jnpf.model.tempmodule.dto.RollImageDto; +import jnpf.tempmodule.mapper.RollImageMapper; +import jnpf.tempmodule.service.RollImageService; +import jnpf.util.ConstantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.JsonUtil; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 轮播图服务实现 + * + * @author yanwenfu + * @create 2023-10-10 + */ +@Service +public class RollImageServiceImpl implements RollImageService { + + @Resource + private RollImageMapper rollImageMapper; + + @Override + public List getRollImageList(Integer bannerType) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(RollImage::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(RollImage::getPublished, ConstantUtil.NUM_TRUE) + .eq(RollImage::getBannerType, bannerType) + .orderByAsc(RollImage::getSort); + List list = rollImageMapper.selectList(queryWrapper); + return JsonUtil.getJsonToList(list, RollImageVo.class); + } + + @Override + public void addRollImage(RollImageDto rollImageDto) { + // 查询数据库最后一张轮播图 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(RollImage::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(RollImage::getBannerType, rollImageDto.getBannerType()) + .orderByDesc(RollImage::getSort) + .last("limit 1"); + RollImage image = rollImageMapper.selectOne(queryWrapper); + int sort = null == image ? 1 : image.getSort() + 1; + RollImage rollImage = new RollImage(); + rollImage.setBannerType(rollImageDto.getBannerType()); + rollImage.setId(FtbUtil.getId()); + rollImage.setImageUrl(rollImageDto.getImageUrl()); + rollImage.setImageType(1); + rollImage.setPublished(ConstantUtil.NUM_TRUE); + rollImage.setSort(sort); + rollImage.setDeleteMark(ConstantUtil.NUM_FALSE); + rollImageMapper.insert(rollImage); + } + + @Override + public void deleteRollImage(String id) { + + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .eq(RollImage::getId, id) + .set(RollImage::getDeleteMark, ConstantUtil.NUM_TRUE); + rollImageMapper.update(null, updateWrapper); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Base64Util.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Base64Util.java new file mode 100644 index 0000000..ed42d2e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Base64Util.java @@ -0,0 +1,71 @@ +package jnpf.util; + +import cn.xuyanwu.spring.file.storage.FileInfo; +import cn.xuyanwu.spring.file.storage.MockMultipartFile; +import jnpf.constant.FileTypeConstant; +import jnpf.file.FileApi; +import jnpf.file.FileUploadApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.util.Base64; +import java.util.UUID; + +/** + * base64工具 + * + * @author yanwenfu + * @create 2025-09-08 + */ +@Component +@Slf4j +public class Base64Util { + + @Autowired + private FileUploadApi fileUploadApi; + @Autowired + private FileApi fileApi; + + /** + * 将 Base64 转换为图片文件并上传 + * @param base64Str Base64 字符串 + * @return 上传后的 URL + */ + public String convertAndUpload(String base64Str) { + try { + // 1. 获取系统临时目录 + String tempDir = System.getProperty("java.io.tmpdir"); + // 文件名用时间戳,避免重复 + String suffix = ".jpg"; + String fileName = "temp_" + System.currentTimeMillis() + suffix; + File file = new File(tempDir, fileName); + // 2. Base64 解码并写入文件 + byte[] imageBytes = Base64.getDecoder().decode(base64Str); + try (OutputStream out = new FileOutputStream(file)) { + out.write(imageBytes); + } + // 3. 上传 + String url = uploadFile(file, suffix); + // 4. 删除临时文件(可选,看你是否需要保留) + file.delete(); + return url; + } catch (Exception e) { + log.error("转换并上传base64图片失败, {}", e.getMessage()); + return null; + } + } + + private String uploadFile(File file, String suffix) throws IOException { + + FileInputStream input = new FileInputStream(file); + String uuid = UUID.randomUUID().toString(); + MultipartFile multiFile = new MockMultipartFile(uuid + TemplateExcelUtils.SUFFIX, file.getName(), MediaType.MULTIPART_FORM_DATA_VALUE, input); + input.close(); + FileInfo fileInfo = fileUploadApi.uploadFileCustomName(multiFile, fileApi.getPath(FileTypeConstant.TEMPORARY), uuid + suffix); + return fileInfo.getUrl().replace(ConstantUtil.EXTRA_PATH, ""); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/CustomTenantUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/CustomTenantUtil.java new file mode 100644 index 0000000..d9ab4c6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/CustomTenantUtil.java @@ -0,0 +1,104 @@ +package jnpf.util; + +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.database.model.TenantVO; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.util.data.DataSourceContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import java.util.Objects; + +/** + * 租户工具类 + * + * @author yanwenfu + * @create 2023-11-08 + */ +@Component("ftbCustomTenantUtil") +@Slf4j +public class CustomTenantUtil { + + @Autowired + private ConfigValueUtil configValueUtil; + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private AuthUtil authUtil; + private static final String SCHEDULETASK_TOKEN = "scheduletask::fill::token::%s::%s"; + + public void checkOutTenant(String tenantId) { + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + // throw new RuntimeException("切换租户失败"); + log.error("切换租户失败, 租户id: {}", tenantId); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } + + public void checkOutTenant(String tenantId, String userId) { + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + TenantVO tenantVO; + try { + tenantVO = TenantDataSourceUtil.getRemoteTenantInfo(tenantId); + TenantDataSourceUtil.switchTenant(tenantId, tenantVO); + } catch (LoginException e1) { + + throw new RuntimeException("切换租户失败"); + } + if (StringUtil.isEmpty(userId)) { + log.error("当前切库用户为空"); + return; + } + String tokenKey = String.format(SCHEDULETASK_TOKEN, tenantId, userId); + Object tokenObj = redisTemplate.opsForValue().get(tokenKey); + String token = Objects.isNull(tokenObj) ? null : (String) tokenObj; + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + userInfo.setToken(token); + userInfo.setTenantDbConnectionString(tenantVO.getDbName()); + userInfo.setAssignDataSource(DataSourceContextHolder.isAssignDataSource()); + userInfo.setTenantId(tenantId); + if (StringUtil.isNotBlank(token) && UserProvider.isValidToken(token)) { + UserProvider.setLocalLoginUser(userInfo); + return; + } + try { + token = authUtil.loginTempUser(userId, tenantId); + } catch (Exception e) { + throw new IllegalStateException("生成token失败", e); + } + // 检查 token 是否为空 + if (token == null || token.isEmpty()) { + log.error("当前用户为[{}],租户为[{}]", userId, tenantId); + throw new IllegalStateException("Token is empty or null"); + } + redisTemplate.opsForValue().set(tokenKey, token); + // 设置请求头中的 Token + userInfo.setToken(token); + UserProvider.setLocalLoginUser(userInfo); + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateConvertUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateConvertUtil.java new file mode 100644 index 0000000..9877277 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateConvertUtil.java @@ -0,0 +1,149 @@ +package jnpf.util; + +import cn.hutool.core.date.DateTime; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; +import java.util.HashMap; +import java.util.Objects; +@Slf4j +public class DateConvertUtil { + /** + * 计算时间差 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param returnType 返回值类型(1-分钟 2- 小时 3-天) + * @param scale 保留小数位 + * @param attendanceRatio 换算比(小时折算天) + * @return + */ + public static BigDecimal dateConvert(Date startTime, Date endTime, Integer returnType, Integer scale, BigDecimal attendanceRatio) { + long diffMillis = endTime.getTime() - startTime.getTime(); + BigDecimal returnData = BigDecimal.ZERO; + switch (returnType) { + case 1: { + returnData = new BigDecimal(diffMillis).divide(new BigDecimal(60 * 1000), scale, RoundingMode.HALF_UP); + break; + } + case 2: { + returnData = new BigDecimal(diffMillis).divide(new BigDecimal(60 * 60 * 1000), scale, RoundingMode.HALF_UP); + break; + } + case 3: { + returnData = new BigDecimal(diffMillis).divide(attendanceRatio.multiply(new BigDecimal(60 * 60 * 1000)), scale, RoundingMode.HALF_UP); + break; + } + } + return returnData; + } + + /** + * 分钟换位 + * + * @param number 分钟数 + * @param returnType 返回值类型(1- 小时 2-天) + * @param scale 保留小数位 + * @param attendanceRatio 换算比(小时折算天) + * @return + */ + public static BigDecimal minuteConvert(BigDecimal number, Integer returnType, Integer scale, BigDecimal attendanceRatio) { + BigDecimal returnData = BigDecimal.ZERO; + switch (returnType) { + case 1: { + returnData = number.divide(new BigDecimal(60), scale, RoundingMode.HALF_UP); + break; + } + case 2: { + returnData = number.divide(new BigDecimal(60).multiply(attendanceRatio), scale, RoundingMode.HALF_UP); + break; + } + } + return returnData; + } + + /** + * 秒换位 + * + * @param number 秒数 + * @param returnType 返回值类型(1- 分钟 2-小时 3-天) + * @param scale 保留小数位 + * @param attendanceRatio 换算比(小时折算天) + * @return + */ + public static BigDecimal secondConvert(Integer number, Integer returnType, Integer scale, BigDecimal attendanceRatio) { + BigDecimal returnData = BigDecimal.ZERO; + switch (returnType) { + case 1: { + returnData = new BigDecimal(number).divide(new BigDecimal(60), scale, RoundingMode.HALF_UP); + break; + } + case 2: { + returnData = new BigDecimal(number).divide(new BigDecimal(60 * 60), scale, RoundingMode.HALF_UP); + break; + } + case 3: { + returnData = new BigDecimal(number).divide(new BigDecimal(60 * 60).multiply(attendanceRatio), scale, RoundingMode.HALF_UP); + break; + } + } + return returnData; + } + public static HashMap setOverlap(DateTime startDate1, + DateTime endDate1, DateTime startDate2, DateTime endDate2, boolean isStrict) throws Exception { + HashMap intersection = new HashMap<>(); + if (endDate1.compareTo(startDate1) < 0) { + return null; + } + if (endDate2.compareTo(startDate2) < 0) { + return null; + } + if (isStrict) { + if (!(endDate1.getTime() <= startDate2.getTime() || startDate1.getTime() >= endDate2.getTime())) { + //存在交集 取 两个时间中最大的开始时间和最小的结束时间 + intersection.put("startDate", startDate1.getTime() < startDate2.getTime() ? startDate2 : startDate1); + intersection.put("endDate", endDate1.getTime() < endDate2.getTime() ? endDate1 : endDate2); + } + } else { + if (!(endDate1.getTime() < startDate2.getTime() || startDate1.getTime() > endDate2.getTime())) { + //存在交集 取 两个时间中最大的开始时间和最小的结束时间 + intersection.put("startDate", startDate1.getTime() < startDate2.getTime() ? startDate2 : startDate1); + intersection.put("endDate", endDate1.getTime() < endDate2.getTime() ? endDate1 : endDate2); + } + } + return intersection; + } + + /** + * 判断2个时间段是否有重叠(交集) + * + * @param startDate1 时间段1开始时间 + * @param endDate1 时间段1结束时间 + * @param startDate2 时间段2开始时间 + * @param endDate2 时间段2结束时间 + * @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等 + * @return ashMap key startDate endDate + */ + public static HashMap getOverlap(DateTime startDate1,DateTime endDate1, DateTime startDate2, DateTime endDate2, boolean isStrict) throws Exception { + Objects.requireNonNull(startDate1, "startDate1"); + Objects.requireNonNull(endDate1, "endDate1"); + Objects.requireNonNull(startDate2, "startDate2"); + Objects.requireNonNull(endDate2, "endDate2"); + return setOverlap(startDate1, endDate1, startDate2, endDate2, isStrict); + } + public static void main(String[] args) { + DateTime startDate1 = new DateTime(DateUtil.stringToDates("2024-01-28")); + DateTime endDate1 = new DateTime(DateUtil.stringToDates("2024-01-31")); + DateTime startDate2 = new DateTime(DateUtil.stringToDates("2024-01-29")); + DateTime endDate2 = new DateTime(DateUtil.stringToDates("2024-02-28")); + try { + HashMap hashMap = getOverlap(startDate1, endDate1, startDate2, endDate2, false); + log.error("交叉数据:{}", JSON.toJSON(hashMap)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateRange.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateRange.java new file mode 100644 index 0000000..cb41356 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DateRange.java @@ -0,0 +1,86 @@ +package jnpf.util; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.time.DateUtils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +/** + * 时间范围操作类 + * @author Lengyunxiong + * @date 2023/3/09 16:31 + */ +@Setter +@Getter +@Data +public class DateRange { + + private Date start; + private Date end; + + public DateRange() { + super(); + } + + public DateRange(Date start, Date end) { + super(); + this.start = start; + this.end = end; + } + + /** + * 判断时间范围是否不为空,即开始时间不在结束时间之后 + */ + public boolean isNotEmptyDateRange() { + return start.before(end) || start.equals(end); + } + + public List splitByMonth() { + List result = new ArrayList<>(); + + if(!this.isNotEmptyDateRange()) { + return result; + } + + Calendar sc = Calendar.getInstance(); + sc.setTime(start); + Calendar ec = Calendar.getInstance(); + ec.setTime(end); + + if (this.isSameMonth(sc, ec)) { + result.add(this); + return result; + } + + while(sc.before(ec)) { + DateRange d = new DateRange(); + d.setStart(sc.getTime()); + //向时间点之后取整,这里以月取整,就是下月初第一天零点 + //还有一个方法是truncate是向之前的时间取整,和celling正好相反 + //为什么celling有向后取整的意思,因为吊灯,天花板,之类的英文就是celling + sc = DateUtils.ceiling(sc, Calendar.MONDAY); + //这里主要考虑最后一个月的情况,这个if其实可以提到while外面,性能会更好一点 + //我懒得提了 + if(sc.before(ec)) { + d.setEnd(sc.getTime()); + } else { + d.setEnd(ec.getTime()); + } + result.add(d); + } + + return result; + + } + + private boolean isSameMonth(final Calendar cal1, final Calendar cal2) { + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) + && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DynDicUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DynDicUtil.java new file mode 100644 index 0000000..06fb48d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/DynDicUtil.java @@ -0,0 +1,196 @@ +package jnpf.util; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import jnpf.base.ActionResult; +import jnpf.base.DataInterFaceApi; +import jnpf.base.DictionaryDataApi; +import jnpf.base.entity.DictionaryDataEntity; +import jnpf.base.model.datainterface.DataInterfaceActionVo; +import jnpf.util.context.SpringContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author JNPF开发平台组 + * @version V3.1.0 + * @copyright 引迈信息技术有限公司(https://www.jnpfsoft.com) + * @date 2021/3/16 + */ +@Component +public class DynDicUtil { + + @Autowired + private RedisUtil redisUtil; + @Autowired + private CacheKeyUtil cacheKeyUtil; + @Autowired + private DictionaryDataApi dictionaryDataApi; + @Autowired + private DataInterFaceApi dataInterfaceApi; + + + public final String regEx = "[\\[\\]\"]"; + + /** + * 获取数据字典数据 + * + * @param feild + * @return + */ + public String getDicName(String feild,String parentId) { + if (redisUtil.exists(cacheKeyUtil.getDictionary() + feild)) { + return redisUtil.getString(cacheKeyUtil.getDictionary() + feild).toString(); + } + if (StringUtil.isNotEmpty(feild)) { + //去除中括号以及双引号 + feild = feild.replaceAll(regEx, ""); + //判断多选框 + String[] feilds = feild.split(","); + if (feilds.length > 1) { + StringBuilder feildsValue = new StringBuilder(); + DictionaryDataEntity dictionaryDataEntity; + for (String feil : feilds) { + dictionaryDataEntity = dictionaryDataApi.getSwapInfo(feil,parentId); + if (dictionaryDataEntity != null) { + feildsValue.append(dictionaryDataEntity.getFullName() + ","); + } + } + String finalValue; + if (StringUtil.isEmpty(feildsValue) || feildsValue.equals("")) { + finalValue = feildsValue.toString(); + } else { + finalValue = feildsValue.substring(0, feildsValue.length() - 1); + } + redisUtil = SpringContext.getBean(RedisUtil.class); + redisUtil.insert(cacheKeyUtil.getDictionary() + feild, finalValue, 20); + return finalValue; + } + DictionaryDataEntity dictionaryDataentity = dictionaryDataApi.getSwapInfo(feild,parentId); + if (dictionaryDataentity != null) { + redisUtil = SpringContext.getBean(RedisUtil.class); + redisUtil.insert(cacheKeyUtil.getDictionary() + feild, dictionaryDataentity.getFullName(), 20); + return dictionaryDataentity.getFullName(); + } + return feild; + } + return feild; + } + + /** + * 获取远端数据 + * + * @param urlId + * @param label + * @param value + * @param feildValue + * @return + * @throws IOException + */ + public String getDynName(String urlId, String label, String value, String feildValue) { + String rediskey = cacheKeyUtil.getDynamic() + "_" + urlId + "_" + feildValue; + if (redisUtil.exists(rediskey)) { + return redisUtil.getString(rediskey).toString(); + } + if (StringUtil.isNotEmpty(feildValue)) { + //去除中括号以及双引号 + feildValue = feildValue.replaceAll(regEx, ""); + //获取远端数据 + Map a = new HashMap<>(); + ActionResult object = dataInterfaceApi.getDataInterfaceInfo(urlId); + if (object.getData() != null && object.getData() instanceof DataInterfaceActionVo) { + DataInterfaceActionVo vo= (DataInterfaceActionVo) object.getData(); + List> dataList = (List>) vo.getData(); + //判断是否多选 + String[] feildValues = feildValue.split(","); + if (feildValues.length > 0) { + //转换的真实值 + StringBuilder feildVa = new StringBuilder(); + for (String feild : feildValues) { + for (Map data : dataList) { + if (String.valueOf(data.get(value)).equals(feild)) { + feildVa.append(data.get(label) + ","); + } + } + } + String finalValue; + if (StringUtil.isEmpty(feildVa) || feildVa.equals("")) { + finalValue = feildVa.toString(); + } else { + finalValue = feildVa.substring(0, feildVa.length() - 1); + } + redisUtil = SpringContext.getBean(RedisUtil.class); + redisUtil.insert(rediskey, finalValue, 20); + return finalValue; + } + for (Map data : dataList) { + if (feildValue.equals(String.valueOf(data.get(value)))) { + redisUtil = SpringContext.getBean(RedisUtil.class); + redisUtil.insert(rediskey, data.get(label).toString(), 20); + return data.get(label).toString(); + } + return feildValue; + } + } + return feildValue; + } + return feildValue; + } + + /** + * 获取远端数据 + * + * @param urlId + * @param name + * @param id + * @param children + * @param feildValue + * @return + */ + public String getDynName(String urlId, String name, String id, String children, String feildValue) { + String rediskey = cacheKeyUtil.getDynamic() + "_" + urlId + "_" + feildValue; + if (redisUtil.exists(rediskey)) { + return redisUtil.getString(rediskey).toString(); + } + List result = new ArrayList<>(); + if (StringUtil.isNotEmpty(feildValue)) { + Map a = new HashMap<>(); + ActionResult object = dataInterfaceApi.getDataInterfaceInfo(urlId); + if (object.getData() != null && object.getData() instanceof DataInterfaceActionVo) { + DataInterfaceActionVo vo= (DataInterfaceActionVo) object.getData(); + List> dataList = (List>) vo.getData(); + JSONArray dataAll = JsonUtil.getListToJsonArray(dataList); + List> list = new ArrayList<>(); + treeToList(id, name, children, dataAll, list); + String value = feildValue.replaceAll("\\[", "").replaceAll("\\]", ""); + result = list.stream().filter(t -> value.contains(String.valueOf(t.get(id)))).map(t -> String.valueOf(t.get(name))).collect(Collectors.toList()); + } + } + return String.join(",", result); + } + + + /** + * 树转成list + **/ + private void treeToList(String id, String fullName, String children, JSONArray data, List> result) { + for (int i = 0; i < data.size(); i++) { + JSONObject ob = data.getJSONObject(i); + Map tree = new HashMap<>(16); + tree.put(id, String.valueOf(ob.get(id))); + tree.put(fullName, String.valueOf(ob.get(fullName))); + result.add(tree); + if (ob.get(children) != null) { + JSONArray childArray = ob.getJSONArray(children); + treeToList(id, fullName, children, childArray, result); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/EasyExcelUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/EasyExcelUtil.java new file mode 100644 index 0000000..3a729d1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/EasyExcelUtil.java @@ -0,0 +1,180 @@ +package jnpf.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.http.Header; +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.util.ListUtils; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jnpf.handler.CustomCellStyleHandler; +import jnpf.handler.CustomCellWriteHandler; +import jnpf.handler.CustomRowHeightHandler; +import jnpf.model.cultivate.vo.teaching.SkillCountVo; +import jnpf.model.cultivate.vo.teaching.SummaryPageListVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +/** + * easyExcel导入导出工具类 + * @author yier + */ +public class EasyExcelUtil { + + private static final Logger logger = LoggerFactory.getLogger(EasyExcelUtil.class); + + public static void simpleWrite(List list, String sheetName, Class clazz, HttpServletResponse response) throws IOException { + response.setContentType("application/octet-stream"); + response.setCharacterEncoding("utf-8"); + OutputStream outputStream = response.getOutputStream(); + try (outputStream) { + String fileName = URLEncoder.encode(sheetName, StandardCharsets.UTF_8); + response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); + EasyExcel.write(outputStream, clazz).sheet(sheetName).doWrite(list); + } + } + + /** + * Excel导出功能 + * + * @param response 相应数据 + * @param fileName 文件名 + * @param sheetName sheet名 + * @param cls 对象属性名的值 + */ + public static void export(HttpServletResponse response, String fileName, String sheetName, Class cls, List list) throws Exception { + ServletOutputStream outputStream = response.getOutputStream(); + response.setCharacterEncoding("utf-8"); + response.setHeader(Header.CONTENT_DISPOSITION.toString(), "attachment; filename=".concat(URLEncoder.encode(fileName, StandardCharsets.UTF_8))); + response.setContentType("application/octet-stream"); + EasyExcel.write(outputStream, cls).sheet(sheetName).doWrite(list); + } + + /** + * 动态表头导出 + * @param headList 表头 + * @param dataList 导出数据 + * @param filename 文件名 + * @param sheetName sheet名称 + */ + public static void dynamicTableExport(List> headList, List> dataList, String filename, String sheetName) { + ExcelWriter excelWriter = null; + HttpServletResponse response = ServletUtil.getResponse(); + try { + response.setCharacterEncoding("utf-8"); + response.setContentType("application/octet-stream"); + + response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8)); + excelWriter = EasyExcel.write() + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .file(response.getOutputStream()) + .inMemory(true).build(); + + WriteSheet writeSheet = EasyExcel.writerSheet(0, sheetName) + .head(headList).build(); + excelWriter.write(dataList, writeSheet); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (excelWriter != null) { + excelWriter.finish(); + } + } + + } + + /** + * 获取动态表头 + * @param dataList 数据集合 + * @param list 已存在的动态表头 + * @return 返回动态表头 + */ + public static List> getHeaders(List dataList, List> list) { + //获取最长的技能点集合 + List skillList = dataList.stream().map(SummaryPageListVo::getSkillList).max(Comparator.comparingInt(List::size)).orElse(new LinkedList<>()); + if (CollUtil.isNotEmpty(dataList) && CollUtil.isNotEmpty(skillList)) { + for (SkillCountVo skillCountVo : skillList) { + List head = new ArrayList<>(); + head.add(skillCountVo.getName()); + list.add(head); + } + } + return list; + } + + /** + * 获取表内容数据 + * @param dataList 数据集合 + * @return 表内容数据 + */ + public static List> getDataList(List dataList) { + List> list = ListUtils.newArrayList(); + if (CollUtil.isNotEmpty(dataList)) { + for (SummaryPageListVo rankingDTO : dataList) { + List data = ListUtils.newArrayList(); + data.add(rankingDTO.getStoreName()); + data.add(rankingDTO.getUserName()); + data.add(rankingDTO.getPostName()); + data.add(rankingDTO.getTotalCount()); + if (CollUtil.isNotEmpty(rankingDTO.getSkillList())) { + for (SkillCountVo skillCountVo : rankingDTO.getSkillList()) { + data.add(skillCountVo.getCount()); + } + } + list.add(data); + } + } + return list; + } + + /** + * 导出排行榜数据 + * + * @param list 导出数据 + * @param headers 表头 + * @param fileName 导出文件名称 + * @param sheetName sheet名称 + * @param response 响应对象 + * @throws Exception 抛出异常 + */ + public static void rankingExportData(List> list, List> headers, String fileName, + String sheetName, HttpServletResponse response) throws Exception { + EasyExcelUtil.export(response, fileName + ".xlsx", sheetName + "排行", list, headers); + } + + /** + * Excel导出功能(根据自定义表头) + * + * @param response 响应数据 + * @param fileName 文件名 + * @param sheetName sheet名 + * @param list 数据列表 + * @param headers 表头集合 + */ + public static void export(HttpServletResponse response, String fileName, String sheetName, + List> list, List> headers) throws Exception { + ServletOutputStream outputStream = response.getOutputStream(); + response.setCharacterEncoding("utf-8"); + response.setHeader(Header.CONTENT_DISPOSITION.toString(), "attachment; filename=".concat(URLEncoder.encode(fileName, StandardCharsets.UTF_8))); + response.setContentType("application/octet-stream"); + EasyExcel.write(outputStream) + .head(headers) + .sheet(sheetName) + .registerWriteHandler(new CustomRowHeightHandler()) + .registerWriteHandler(new CustomCellStyleHandler()) + .registerWriteHandler(new CustomCellWriteHandler()) + .doWrite(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/GenUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/GenUtil.java new file mode 100644 index 0000000..69ea36f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/GenUtil.java @@ -0,0 +1,473 @@ +package jnpf.util; + +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import jnpf.onlinedev.util.onlineDevUtil.OnlineDatabaseUtils; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@Data +public class GenUtil { + /** + * 字段说明 + */ + private String fieldName; + /** + * 运算符 + */ + private String operator; + /** + * 逻辑拼接符号 + */ + private String logic; + /** + * 组件标识 + */ + private String jnpfKey; + /** + * 字段key + */ + private String field; + /** + * 自定义的值 + */ + private String fieldValue; + /** + * 自定义的值2 + */ + private String fieldValue2; + + private List selectIgnore; + + /** + * 数据库类型 + */ + private String dbType; + /** + * 日期格式 + */ + private String format; + /** + * 数字精度 + */ + private String precision; + + + /** + * @param wrapper wrapper对象 + * @param fieldDb 数据库字段名实际包括前缀 + * @return + */ + public QueryWrapper solveValue(QueryWrapper wrapper, String fieldDb) { + MyType myType = myControl(jnpfKey); + if ("||".equals(logic)) { + wrapper.or(); + } + if (fieldValue == null) { + fieldValue = ""; + } + try { + ArrayList splitKey = new ArrayList() {{ + add("date"); + add("time"); + add("numInput"); + }}; + if (splitKey.contains(jnpfKey) && "between".equals(operator)) { + List data = JsonUtil.getJsonToList(fieldValue, String.class); + fieldValue = data.get(0); + fieldValue2 = data.get(1); + } + + // 显示组织还是部门,全部拿最后一级 + if (jnpfKey.equals("currOrganize") && StringUtils.isNoneBlank(fieldValue)) { + List data = JsonUtil.getJsonToList(fieldValue, String.class); + fieldValue = data.get(data.size() - 1); + } + + selectIgnore = new ArrayList() {{ + add("comSelect"); + add("address"); + add("cascader"); + add("checkbox"); + add("depSelect"); + }}; + + myType.judge(wrapper, fieldDb); + return wrapper; + } catch (Exception e) { + return wrapper; + } + + } + + /** + * 判断控件的所属类型 + * + * @param jnpfKey 控件标识 + * @return 控件类型 + */ + public MyType myControl(String jnpfKey) { + MyType myType = null; + switch (jnpfKey) { + /** 基础 */ + case "comInput": + case "textarea": + case "billRule": + case "popupTableSelect": + case "relationForm": + case "relationFormAttr": + case "popupSelect": + case "popupAttr": + myType = new BasicControl(); + break; + // 数字类型 + case "numInput": + case "calculate": + myType = new NumControl(); + break; + // 日期类型 + case "date": + case "createTime": + case "modifyTime": + myType = new DateControl(); + break; + // 时间类型 + case "time": + myType = new TimeControl(); + break; + // 下拉类型 + default: + myType = new SelectControl(); + } + return myType; + } + + public void getNullWrapper(QueryWrapper wrapper, String fieldDb) { + if ("||".equals(logic)) { + wrapper.or(t -> { + t.isNull(fieldDb); + t.or().eq(fieldDb, ""); + t.or().eq(fieldDb, "[]"); + }); + } else { + wrapper.and(t -> { + t.isNull(fieldDb); + t.or().eq(fieldDb, ""); + t.or().eq(fieldDb, "[]"); + }); + } + } + + private void getNotNullWrapper(QueryWrapper wrapper, String fieldDb) { + if ("||".equals(logic)) { + wrapper.or(t -> { + t.isNotNull(fieldDb); + t.ne(fieldDb, ""); + t.ne(fieldDb, "[]"); + }); + } else { + wrapper.and(t -> { + t.isNotNull(fieldDb); + t.ne(fieldDb, ""); + t.ne(fieldDb, "[]"); + }); + } + } + + /** + * 基础类型 + */ + class BasicControl extends MyType { + + @Override + void judge(QueryWrapper wrapper, String fieldDb) { + switch (operator) { + case "null": + getNullWrapper(wrapper, fieldDb); + break; + case "notNull": + getNotNullWrapper(wrapper, fieldDb); + break; + case "==": + wrapper.eq(fieldDb, fieldValue); + break; + case "<>": + wrapper.ne(fieldDb, fieldValue); + break; + case "like": + wrapper.like(fieldDb, fieldValue); + break; + case "notLike": + wrapper.notLike(fieldDb, fieldValue); + break; + + } + } + } + + class NumControl extends MyType { + + + @Override + void judge(QueryWrapper wrapper, String fieldDb) { + BigDecimal num1 = new BigDecimal(fieldValue); + BigDecimal num2 = null; + if(fieldValue2!=null){ + num2 = new BigDecimal(fieldValue2); + } + // 精度处理 + String fieldPrecisionValue; + String fieldPrecisionValue2; + if(StringUtils.isNotBlank(precision)){ + String zeroNum = "0."+ StringUtils.repeat("0", Integer.parseInt(precision)); + DecimalFormat numFormat = new DecimalFormat(zeroNum); + fieldPrecisionValue = numFormat.format(new BigDecimal(fieldValue)); + num1 = new BigDecimal(fieldPrecisionValue); + if(fieldValue2 != null ){ + fieldPrecisionValue2 = numFormat.format(new BigDecimal(fieldValue2)); + num2 = new BigDecimal(fieldPrecisionValue2); + } + } + + switch (operator) { + case "null": + getNullWrapper(wrapper, fieldDb); + break; + case "notNull": + getNotNullWrapper(wrapper, fieldDb); + break; + case "==": + wrapper.eq(fieldDb, num1); + break; + case "<>": + wrapper.ne(fieldDb, num1); + break; + case ">": + wrapper.gt(fieldDb, num1); + + break; + case "<": + wrapper.lt(fieldDb, num1); + break; + case ">=": + wrapper.ge(fieldDb, num1); + break; + case "<=": + wrapper.le(fieldDb, num1); + break; + case "between": + wrapper.between(fieldDb, num1, num2); + break; + } + } + } + + class DateControl extends MyType { + @Override + void judge(QueryWrapper wrapper, String fieldDb) { + + long time = 0; + Date date = new Date(); + if (StringUtils.isNoneBlank(fieldValue)) { + time = Long.parseLong(fieldValue); + date.setTime(time); + fieldValue = DateUtil.daFormat(date); + } + Date fieldValueDate = DateUtil.stringToDates(fieldValue); + switch (operator) { + case "null": + getNullWrapper(wrapper, fieldDb); + break; + case "notNull": + getNotNullWrapper(wrapper, fieldDb); + break; + case "==": + wrapper.between(fieldDb, fieldValueDate, new Date(time + 60 * 60 * 24 * 1000)); + break; + case "<>": + wrapper.ne(fieldDb, fieldValueDate); + break; + case ">": + wrapper.gt(fieldDb, fieldValueDate); + break; + case "<": + wrapper.lt(fieldDb, fieldValueDate); + break; + case ">=": + wrapper.ge(fieldDb, fieldValueDate); + break; + case "<=": + wrapper.le(fieldDb, fieldValueDate); + break; + case "between": + long time2 = Long.parseLong(fieldValue2); + wrapper.between(fieldDb, fieldValueDate, new Date(time2 + 60 * 60 * 24 * 1000)); + break; + } + } + + + } + + class TimeControl extends MyType { + @Override + void judge(QueryWrapper wrapper, String fieldDb) { + switch (operator) { + case "null": + getNullWrapper(wrapper, fieldDb); + + break; + case "notNull": + getNotNullWrapper(wrapper, fieldDb); + break; + case "==": + wrapper.eq(fieldDb, fieldValue); + break; + case "<>": + wrapper.ne(fieldDb, fieldValue); + break; + case ">": + wrapper.gt(fieldDb, fieldValue); + break; + case "<": + wrapper.lt(fieldDb, fieldValue); + break; + case ">=": + wrapper.ge(fieldDb, fieldValue); + break; + case "<=": + wrapper.le(fieldDb, fieldValue); + break; + case "between": + wrapper.between(fieldDb, fieldValue, fieldValue2); + break; + } + } + } + + /** + * 下拉控件类型 + */ + class SelectControl extends MyType { + + @Override + void judge(QueryWrapper wrapper, String fieldDb) { + List list = new ArrayList<>(); + if (StringUtils.isNoneBlank(fieldValue) && fieldValue.charAt(0) == '[' && !selectIgnore.contains(jnpfKey)) { + list = JSONUtil.toList(fieldValue, String.class); + if (!Objects.equals(operator, "in") && !Objects.equals(operator, "notIn")) { + fieldValue = String.join(",", list); + } + } + if (selectIgnore.contains(jnpfKey)) { + if (StringUtils.isBlank(fieldValue)) { + fieldValue = "[]"; + } + list = JsonUtil.getJsonToList(fieldValue, String.class); + } + + switch (operator) { + case "null": + getNullWrapper(wrapper, fieldDb); + + break; + case "notNull": + getNotNullWrapper(wrapper, fieldDb); + break; + case "==": + wrapper.eq(fieldDb, fieldValue); + break; + case "<>": + wrapper.ne(fieldDb, fieldValue); + break; + case "like": + wrapper.like(fieldDb, fieldValue); + break; + case "notLike": + wrapper.notLike(fieldDb, fieldValue); + break; + case "in": + + if (list.size() > 0) { + List finalList = list; + if ("||".equals(logic)) { + wrapper.or(t -> { + if (finalList.size() > 0) { + for (int i = 0; i < finalList.size(); i++) { + String value = finalList.get(i); + if (i == 0) { + t.like(fieldDb, value); + } else { + t.or().like(fieldDb, value); + } + } + } + }); + } else { + wrapper.and(t -> { + if (finalList.size() > 0) { + for (int i = 0; i < finalList.size(); i++) { + String value = finalList.get(i); + if (i == 0) { + t.like(fieldDb, value); + } else { + t.or().like(fieldDb, value); + } + } + } + }); + } + + } + break; + case "notIn": + if (list.size() > 0) { + List finalList1 = list; + if ("||".equals(logic)) { + wrapper.or(t -> { + if (finalList1.size() > 0) { + for (int i = 0; i < finalList1.size(); i++) { + String value = finalList1.get(i); + if (i == 0) { + t.notLike(fieldDb, value); + } else { + t.notLike(fieldDb, value); + } + + } + } + }); + } else { + wrapper.and(t -> { + if (finalList1.size() > 0) { + for (int i = 0; i < finalList1.size(); i++) { + String value = finalList1.get(i); + if (i == 0) { + t.notLike(fieldDb, value); + } else { + t.notLike(fieldDb, value); + } + + } + } + }); + } + + } + break; + } + } + } + + abstract class MyType { + abstract void judge(QueryWrapper wrapper, String fieldDb); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Html2Text.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Html2Text.java new file mode 100644 index 0000000..6e3dbfa --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/Html2Text.java @@ -0,0 +1,57 @@ +package jnpf.util; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Whitelist; + +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.html.parser.ParserDelegator; +import java.io.*; + +public class Html2Text extends HTMLEditorKit.ParserCallback { + + private static Html2Text html2Text = new Html2Text(); + StringBuffer s; + + public Html2Text() { + } + + public static void main(String[] args) { + String ss = "

首先要说明的是,我既没有资格也没有能力来评价这本书。作者的深厚功力,读过这本书的人都应该能够体会得到。这一本薄薄的册子,把新中国建立以来我国在社会主义建设的过程中的种种艰难,明明白白地展示在我们面前。笔法平实,却又引人入胜,同时又让我们对整体的时代有了一个框架行的了解,实属不易。


其实对于这段历史,最精辟的总结,还是十九届六中全会公报中的那两段话:

全会提出,社会主义革命和建设时期,党面临的主要任务是,实现从新民主主义到社会主义的转变,进行社会主义革命,推进社会主义建设,为实现中华民族伟大复兴奠定根本政治前提和制度基础。在这个时期,以毛泽东同志为主要代表的中国共产党人提出关于社会主义建设的一系列重要思想。毛泽东思想是马克思列宁主义在中国的创造性运用和发展,是被实践证明了的关于中国革命和建设的正确的理论原则和经验总结,是马克思主义中国化的第一次历史性飞跃。党领导人民自力更生、发愤图强,创造了社会主义革命和建设的伟大成就,实现了中华民族有史以来最为广泛而深刻的社会变革,实现了一穷二白、人口众多的东方大国大步迈进社会主义社会的伟大飞跃。我国建立起独立的比较完整的工业体系和国民经济体系,农业生产条件显著改变,教育、科学、文化、卫生、体育事业有很大发展,人民解放军得到壮大和提高,彻底结束了旧中国的屈辱外交。中国共产党和中国人民以英勇顽强的奋斗向世界庄严宣告,中国人民不但善于破坏一个旧世界、也善于建设一个新世界,只有社会主义才能救中国,只有社会主义才能发展中国。 

全会提出,改革开放和社会主义现代化建设新时期,党面临的主要任务是,继续探索中国建设社会主义的正确道路,解放和发展社会生产力,使人民摆脱贫困、尽快富裕起来,为实现中华民族伟大复兴提供充满新的活力的体制保证和快速发展的物质条件。党的十一届三中全会以后,以邓小平同志为主要代表的中国共产党人,团结带领全党全国各族人民,深刻总结新中国成立以来正反两方面经验,围绕什么是社会主义、怎样建设社会主义这一根本问题,借鉴世界社会主义历史经验,创立了邓小平理论,解放思想,实事求是,作出把党和国家工作中心转移到经济建设上来、实行改革开放的历史性决策,深刻揭示社会主义本质,确立社会主义初级阶段基本路线,明确提出走自己的路、建设中国特色社会主义,科学回答了建设中国特色社会主义的一系列基本问题,制定了到二十一世纪中叶分三步走、基本实现社会主义现代化的发展战略,成功开创了中国特色社会主义。 


原谅我把这么长的原文罗列在这,因为当你读完这本书以后,就会发现这两短话确实相当凝练且精准。当然,既然是读书笔记,也不能不说点自己的想法,其实也就是简单的以下几条:


当我们在讨论一段历史的时候,我们很容易会带入一种幻想,就是如果当初怎么怎么样,现在就会是怎么怎么样。确实,对于发现问题这件事来说,我们在事后总会更容易的发现,毕竟我们是基于已经发生了的事实来去回溯过去存在的问题。但是这里总归还是要思考两个方面的事情。

首先,我们是以过来人的视角来看过去的已经发生的事情,所以从现在看过去很多事情都已经是确定了的,但是在当时的人并不知道未来会发生什么。就拿当时领导人对世界局势的判断来说,在转型时期,领导人对世界局势的判断是总还要又一次大战,所以很多决策是基于这样的局势判断下做出的。虽然我们从后世看到并没有什么世界性的战争,但是我们并不能就以此作为当时人们的判断错误的依据。当我们在回看过去的历史的时候,一定要把当时的情境考虑进去,才能公平和客观地看待当时的所有决策。

其次,我们要看到中国变成现在这个样子,虽然过程很艰难,但是一定是作对了什么。历史并不是历史书上的断代史一样,到了一个时间节点,出现了一个人,发生了一个什么事,历史就立马转向了。不会的。每一个变化其实都是在连续的、积累的过程中逐渐发生的。虽然不可否认的是,政策的变化会使得历史走向有很大的转折,但是政策也绝不是某个人头脑一热的简单决定。至少在我个人的观点里,政策更多时候是一种社会能量积累以后的结果,一定是在各方进行长时间博弈以后,为了破解或者保持一个局面而产生的。所以我们在看待历史的时候,除了关注关键节点的重要事件,还要关注整个世界和社会的发展趋势,才能真正理解历史是如何演变的,也才能真正在历史中学习到我们应该学到的宏观视角。


书的最后的几篇文章里,有位学者提出了一个观点,就是党的理论更多是事后的总结,并对今后一段时期具有指导意义的理论,而不是一种先验性的、全能式的理论。我个人深以为然,或者说,党的理论最重要的一点,就是实事求是。中国的体量和复杂性,确实不是一般国家可以比拟的。中国几个领先省份的GDP,单拿出来都可以排在全球国家的前列的。但是,我们总归要知道的是,没有全国的资源、市场等等的支持,这或许也是很难达到的。想象一下如果每个省份都需要像一个独立国家一样,需要花费大量的精力来获取资源和市场,怎么可能有这么快的发展速度。也正是因为如此,在发展的过程中肯定就会遇到太多太多的问题,而有些问题在一个如此复杂、如此大体量的国家内,就会被更加放大,变得更加复杂。也正是在这样的实际情况下,党领导的国家总是在一步一步地小心探索,我们既尊重当今世界上的最新的理论研究,同时我们也非常注重是否和我们的实际情况符合。这是一个相当难的事情,就跟我们说为什么懂了很多道理却过不好一生一样,每一个人都已经如此复杂,不能有一个个理论来解释和说明,更别说如此大的一个国家了。而这也是我为什么相信党、相信政府的原因,因为我们并不是不知道有什么问题,而是在整体大局的框架下,我们需要根据实际情况用更合理和有效的方式去解决。这需要勇气、智慧和时间。


还有最后一点,是最重要也是最简单的一点,就是党的执政基础是全体人民。正因为是全体人民,所以我们就不能只是一部分人的利益受到保护,就不能是某个方面的单独发展,更不能是放弃某一类人的权利。所以这里面就会有不同群体的矛盾和声音,这是必然的。就拿疫情防控的政策来说,我们正是为了不放弃每一个人的生命,所以才要做到动态清零。这中间就会有很多的质疑和不理解,但是这就是担当和责任,无他。


奋斗的路程总归是艰难的,对一个人是如此,对一个国家更是如此。且前行,无他。


"; +// String str = getContent(ss); +// System.out.println(str); + + String prettyPrintedBodyFragment = Jsoup.clean(ss, "", Whitelist.none().addTags("br", "p"), new Document.OutputSettings().prettyPrint(true)); + String clean = Jsoup.clean(prettyPrintedBodyFragment, "", Whitelist.none(), new Document.OutputSettings().prettyPrint(false)); + System.out.println(clean); + } + + //获取富文本内容 + public static String getContent(String str) { + try { + html2Text.parse(str); + } catch (IOException e) { + e.printStackTrace(); + } + return html2Text.getText(); + } + + + public void parse(String str) throws IOException { + InputStream iin = new ByteArrayInputStream(str.getBytes()); + Reader in = new InputStreamReader(iin); + s = new StringBuffer(); + ParserDelegator delegator = new ParserDelegator(); + delegator.parse(in, this, Boolean.TRUE); + iin.close(); + in.close(); + } + + public void handleText(char[] text, int pos) { + s.append(text); + } + + public String getText() { + return s.toString(); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpStatus.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpStatus.java new file mode 100644 index 0000000..2a36c33 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpStatus.java @@ -0,0 +1,93 @@ +package jnpf.util; + +/** + * 返回状态码 + * + * @author admin + */ +public class HttpStatus { + /** + * 操作成功 + */ + public static final int SUCCESS = 200; + + /** + * 对象创建成功 + */ + public static final int CREATED = 201; + + /** + * 请求已经被接受 + */ + public static final int ACCEPTED = 202; + + /** + * 操作已经执行成功,但是没有返回数据 + */ + public static final int NO_CONTENT = 204; + + /** + * 资源已被移除 + */ + public static final int MOVED_PERM = 301; + + /** + * 未登录 + */ + public static final int NO_LOGIN = 302; + + /** + * 重定向 + */ + public static final int SEE_OTHER = 303; + + /** + * 资源没有被修改 + */ + public static final int NOT_MODIFIED = 304; + + /** + * 参数列表错误(缺少,格式不匹配) + */ + public static final int BAD_REQUEST = 400; + + /** + * 未授权 + */ + public static final int UNAUTHORIZED = 401; + + /** + * 访问受限,授权过期 + */ + public static final int FORBIDDEN = 403; + + /** + * 资源,服务未找到 + */ + public static final int NOT_FOUND = 404; + + /** + * 不允许的http方法 + */ + public static final int BAD_METHOD = 405; + + /** + * 资源冲突,或者资源被锁 + */ + public static final int CONFLICT = 409; + + /** + * 不支持的数据,媒体类型 + */ + public static final int UNSUPPORTED_TYPE = 415; + + /** + * 系统内部错误 + */ + public static final int ERROR = 500; + + /** + * 接口未实现 + */ + public static final int NOT_IMPLEMENTED = 501; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpUtil.java new file mode 100644 index 0000000..98c26f8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/HttpUtil.java @@ -0,0 +1,173 @@ +package jnpf.util; + +import com.alibaba.fastjson.JSONObject; +import org.apache.http.Consts; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class HttpUtil { + private static final CloseableHttpClient httpclient = HttpClients.createDefault(); + + /** + * 发送HttpGet请求 + * + * @param url + * @return + */ + public static String sendGet(String url) { + + HttpGet httpget = new HttpGet(url); + CloseableHttpResponse response = null; + try { + response = httpclient.execute(httpget); + } catch (IOException e1) { + e1.printStackTrace(); + } + String result = null; + try { + HttpEntity entity = response.getEntity(); + if (entity != null) { + result = EntityUtils.toString(entity); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + response.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } + + /** + * 发送HttpPost请求,参数为map + * + * @param url + * @param map + * @return + */ + public static String sendPost(String url, Map map) { + List formparams = new ArrayList(); + for (Map.Entry entry : map.entrySet()) { + formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8); + HttpPost httppost = new HttpPost(url); + httppost.setEntity(entity); + CloseableHttpResponse response = null; + try { + response = httpclient.execute(httppost); + } catch (IOException e) { + e.printStackTrace(); + } + HttpEntity entity1 = response.getEntity(); + String result = null; + try { + result = EntityUtils.toString(entity1); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } + + /** + * 发送不带参数的HttpPost请求 + * + * @param url + * @return + */ + public static String sendPost(String url) { + HttpPost httppost = new HttpPost(url); + CloseableHttpResponse response = null; + try { + response = httpclient.execute(httppost); + } catch (IOException e) { + e.printStackTrace(); + } + HttpEntity entity = response.getEntity(); + String result = null; + try { + result = EntityUtils.toString(entity); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } + + public static String doPost2(String url, JSONObject param) { + HttpPost httpPost = null; + String result = null; + try { + HttpClient client = new DefaultHttpClient(); + httpPost = new HttpPost(url); + if (param != null) { + StringEntity se = new StringEntity(param.toString(), "utf-8"); + httpPost.setEntity(se); // post方法中,加入json数据 + httpPost.setHeader("Content-Type", "application/json"); + httpPost.setHeader("Authorization", param.getString("authorization")); + } + + HttpResponse response = client.execute(httpPost); + if (response != null) { + HttpEntity resEntity = response.getEntity(); + if (resEntity != null) { + result = EntityUtils.toString(resEntity, "utf-8"); + } + } + + } catch (Exception ex) { + ex.printStackTrace(); + } + return result; + } + + // 将 JSON 字符串的字段首字母大写 + public static String convertFirstLetterToUpperCase(String jsonString) { + StringBuilder result = new StringBuilder(); + + int index = 0; + while (index < jsonString.length()) { + char currentChar = jsonString.charAt(index); + if (currentChar == '"' || currentChar == '{' || currentChar == ',') { + result.append(currentChar); + index++; + } else { + int fieldStartIndex = index; + while (index < jsonString.length() && jsonString.charAt(index) != ':') { + index++; + } + int fieldEndIndex = index; + + String fieldName = jsonString.substring(fieldStartIndex, fieldEndIndex).trim(); + String upperCaseFieldName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1); + result.append(upperCaseFieldName); + + while (index < jsonString.length() && jsonString.charAt(index) != ',' && jsonString.charAt(index) != '}') { + result.append(jsonString.charAt(index)); + index++; + } + } + } + + return result.toString(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/MultithreadExecutors.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/MultithreadExecutors.java new file mode 100644 index 0000000..de9292f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/MultithreadExecutors.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022-2024 Ponfee (http://www.ponfee.cn/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jnpf.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Multi Thread executor + * + * @author Ponfee + */ +public class MultithreadExecutors { + + public static void run(Collection coll, Consumer action, Executor executor) { + run(coll, action, executor, 2); + } + + /** + * Run async, action the T collection + * + * @param coll the collection + * @param action the action + * @param executor the executor + * @param dataSizeThreshold the dataSizeThreshold + * @param the collection element type + */ + public static void run(Collection coll, Consumer action, Executor executor, int dataSizeThreshold) { + if (coll == null || coll.isEmpty()) { + return; + } + if (dataSizeThreshold <= 0 || coll.size() < dataSizeThreshold) { + coll.forEach(action); + return; + } + coll.stream() + .map(e -> CompletableFuture.runAsync(() -> action.accept(e), executor)) + .collect(Collectors.toList()) + .forEach(CompletableFuture::join); + } + + public static List call(Collection coll, Function mapper, Executor executor) { + return call(coll, mapper, executor, 2); + } + + /** + * Convert collection element data + * + * @param coll the collection + * @param mapper the mapper + * @param executor the executor + * @param dataSizeThreshold the executor + * @param the source collection element type + * @param the target collection element type + * @return target collection + */ + public static List call(Collection coll, Function mapper, Executor executor, int dataSizeThreshold) { + if (coll == null) { + return null; + } + if (coll.isEmpty()) { + return Collections.emptyList(); + } + if (dataSizeThreshold <= 0 || coll.size() < dataSizeThreshold) { + return coll.stream().map(mapper).collect(Collectors.toList()); + } + return coll.stream() + .map(e -> CompletableFuture.supplyAsync(() -> mapper.apply(e), executor)) + .collect(Collectors.toList()) + .stream() + .map(CompletableFuture::join) + .collect(Collectors.toList()); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/NumberUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/NumberUtils.java new file mode 100644 index 0000000..ff072e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/NumberUtils.java @@ -0,0 +1,24 @@ +package jnpf.util; + +import jnpf.exception.HandleException; + +/** + * 数字转换类 + * + * @author yanwenfu + * @create 2025-04-17 + */ +public class NumberUtils { + + public static float safeToFloat(Object obj) throws HandleException { + if (obj instanceof Number) { + Number num = (Number) obj; + return num.floatValue(); + } + try { + return Float.parseFloat(String.valueOf(obj)); + } catch (NumberFormatException e) { + throw new HandleException("转换失败"); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/OptionalUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/OptionalUtils.java new file mode 100644 index 0000000..59059b8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/OptionalUtils.java @@ -0,0 +1,280 @@ +package jnpf.util; + + +import java.util.*; + +/** + * 空判断工具 + * + * @author tq + */ +public class OptionalUtils { + + /** + * 或 处理 + * 所有if里,只要有一个满足条件,最后才会执行done方法 + * 每个if满足条件则会执行then方法 + *
+     *     
+     * if(){
+     * then();
+     * }  if(){
+     * then();
+     * }  if(){
+     * then();
+     * }
+     * ...
+     * if(条件N){
+     * then();
+     * }
+     * if(任一满足){
+     * done();
+     * }
+     * 例子:
+     *
+     *  OptionalUtils.or()
+     *                 .ifPresent(req.getProvinceCode()).then(() -> update.setProvinceCode(req.getProvinceCode()))
+     *                 .ifPresent(req.getCityCode()).then(() -> update.setCityCode(req.getCityCode()))
+     *                 .ifPresent(req.getDistrictCode()).then(() -> update.setDistrictCode(req.getDistrictCode()))
+     *                 .ifPresent(req.getCategoryId()).then(() -> update.setCategoryId(req.getCategoryId()))
+     *                 .ifPresent(req.getAddress()).then(() -> update.setAddress(req.getAddress()))
+     *                 .done(() -> {
+     *                     this.updateById(update);
+     *                 });
+     *     
+     * 
+ * + * @param + * @return + */ + public static Condition or() { + return new OrCondition<>(); + } + + /** + * 并且 处理 + * 所有if里,必须所有满足条件,最后才会执行done方法 + * 每个if满足条件则会执行then方法 + *
+     *     
+     * if(条件1){
+     * then();
+     * }
+     * if(条件2){
+     * then();
+     * }
+     * if(条件3){
+     * then();
+     * }
+     * ...
+     * if(条件N){
+     * then();
+     * }
+     * if(所有满足){
+     * done();
+     * }
+     *
+     * 例子:
+     *  OptionalUtils.and()
+     *                 .ifPresent(req.getProvinceCode()).then(() -> update.setProvinceCode(req.getProvinceCode()))
+     *                 .ifPresent(req.getCityCode()).then(() -> update.setCityCode(req.getCityCode()))
+     *                 .ifPresent(req.getDistrictCode()).then(() -> update.setDistrictCode(req.getDistrictCode()))
+     *                 .ifPresent(req.getCategoryId()).then(() -> update.setCategoryId(req.getCategoryId()))
+     *                 .ifPresent(req.getAddress()).then(() -> update.setAddress(req.getAddress()))
+     *                 .done(() -> {
+     *                     this.updateById(update);
+     *                 });
+     *     
+     * 
+ * + * @param + * @return + */ + public static Condition and() { + return new AndCondition<>(); + } + + public static void ifPresent(T value, RunBlockNoReturnFunction runBlock) { + if (!isEmpty(value)) { + runBlock.run(value); + } + } + + public static R ifPresent(T value, RunBlockReturnFunction runBlock) { + if (!isEmpty(value)) { + return runBlock.run(value); + } + return null; + } + + public static R ifPresent(T value, R defaultValue, RunBlockReturnFunction runBlock) { + if (!isEmpty(value)) { + return runBlock.run(value); + } + return defaultValue; + } + + public static void ifNotPresent(T value, String exceptionMsg) { + if (isEmpty(value)) { + throw new ServiceException(exceptionMsg); + } + } + + public static void ifNotPresent(T value, RunBlockNoParameterNoReturnFunction runBlock) { + if (isEmpty(value)) { + runBlock.run(); + } + } + + public static R ifNotPresent(T value, RunBlockNoParameterReturnFunction runBlock, R defaultValue) { + if (isEmpty(value)) { + return runBlock.run(); + } + return defaultValue; + } + + @FunctionalInterface + public interface RunBlockNoParameterReturnFunction { + R run(); + } + + @FunctionalInterface + public interface RunBlockReturnFunction { + R run(T t); + } + + @FunctionalInterface + public interface RunBlockNoReturnFunction { + void run(T t); + } + + @FunctionalInterface + public interface RunBlockNoParameterNoReturnFunction { + void run(); + } + + public static class Execute { + + private final T value; + private final Condition condition; + + public Execute(T value, Condition condition) { + this.value = value; + this.condition = condition; + } + + public Condition then(RunBlockNoParameterNoReturnFunction runBlock) { + runBlock.run(); + return condition; + } + } + + public static class NullExecute extends Execute { + private final Condition condition; + + public NullExecute(Condition condition) { + super(null, condition); + this.condition = condition; + } + + @Override + public Condition then(RunBlockNoParameterNoReturnFunction runBlock) { + return condition; + } + } + + public abstract static class Condition { + + protected final List stepNullOptional = new ArrayList<>(); + + public Condition() { + } + + public Execute ifPresent(T value) { + if (!isEmpty(value)) { + stepNullOptional.add(true); + return new Execute<>(value, this); + } else { + stepNullOptional.add(false); + return new NullExecute<>(this); + } + } + + public Execute ifPresent(T value, RunBlockReturnFunction runBlock) { + if (!runBlock.run(value)) { + stepNullOptional.add(true); + return new Execute<>(value, this); + } else { + stepNullOptional.add(false); + return new NullExecute<>(this); + } + } + + public Execute ifNotPresent(T value) { + return new NullExecute<>(this); + } + + public Execute ifNotPresent(T value, RunBlockNoParameterNoReturnFunction runBlock) { + runBlock.run(); + return new NullExecute<>(this); + } + + public abstract E done(RunBlockNoParameterReturnFunction runBlock); + + public abstract void done(RunBlockNoParameterNoReturnFunction runBlock); + + } + + public static class AndCondition extends Condition { + + @Override + public E done(RunBlockNoParameterReturnFunction runBlock) { + if (!stepNullOptional.contains(false)) { + return runBlock.run(); + } + return null; + } + + @Override + public void done(RunBlockNoParameterNoReturnFunction runBlock) { + if (!stepNullOptional.contains(false)) { + runBlock.run(); + } + } + } + + public static class OrCondition extends Condition { + + @Override + public E done(RunBlockNoParameterReturnFunction runBlock) { + if (stepNullOptional.contains(true)) { + return runBlock.run(); + } + return null; + } + + @Override + public void done(RunBlockNoParameterNoReturnFunction runBlock) { + if (stepNullOptional.contains(true)) { + runBlock.run(); + } + } + } + + private static boolean isEmpty(T value) { + if (Objects.isNull(value)) { + return true; + } + if (value instanceof String) { + if ("".equals(value) || "".equals(((String) value).trim())) { + return true; + } + } else if (Collection.class.isAssignableFrom(value.getClass())) { + return ((Collection) (value)).isEmpty(); + } else if (Map.class.isAssignableFrom(value.getClass())) { + return ((Map) value).isEmpty(); + } + return false; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PageUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PageUtil.java new file mode 100644 index 0000000..a558392 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PageUtil.java @@ -0,0 +1,34 @@ +package jnpf.util; + +import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO; +import jnpf.base.vo.PaginationVO; + +import java.util.List; + +public class PageUtil { + public static PaginationVO page(PageDTO page){ + PaginationVO paginationVO = new PaginationVO(); + paginationVO.setCurrentPage(page.getCurrent()); + paginationVO.setPageSize(page.getSize()); + paginationVO.setTotal((int)page.getTotal()); + return paginationVO; + } + + public static List getListPage(int page, int pageSize, List list) { + if (list == null || list.size() == 0) { + return list; + } + int totalCount = list.size(); + page = page - 1; + int fromIndex = page * pageSize; + if (fromIndex >= totalCount) { + return list; + } + int toIndex = ((page + 1) * pageSize); + if (toIndex > totalCount) { + toIndex = totalCount; + } + return list.subList(fromIndex, toIndex); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ParamUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ParamUtil.java new file mode 100644 index 0000000..0aeb08f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ParamUtil.java @@ -0,0 +1,125 @@ +package jnpf.util; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import jnpf.annotation.check.CheckLength; +import jnpf.annotation.check.CheckListSize; +import jnpf.annotation.check.CheckNull; +import jnpf.exception.HandleException; + +import java.lang.reflect.Field; +import java.util.List; + +/** + * 参数校验 + */ +public class ParamUtil { + + /** + * 参数校验 + * @param t + */ + public static void checkParam(Object t) throws Exception{ +// Class aClass = ClassUtil.getClass(t); + Class aClass = t.getClass(); + Field[] fields = aClass.getDeclaredFields(); + + for (Field field : fields) { + /** 校验字段是否是空*/ + CheckNull annotation = field.getAnnotation(CheckNull.class); + if (annotation == null) { + continue; + } + field.setAccessible(true); + Class type = field.getType(); + if (type == String.class) { + /** 字符串类型*/ + try { + Object o = field.get(t); + String s = ObjectUtil.isNull(o) ? null : String.valueOf(o); + if (StrUtil.isBlank(s)) { + throw new HandleException(annotation.message()); + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + }else { + /** 普通对象类型*/ + try { + if (field.get(t) == null) { + throw new HandleException(annotation.message()); + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (HandleException e) { + throw new RuntimeException(e); + } + } + } + + for (Field field : fields) { + field.setAccessible(true); + /** 校验字段长度*/ + CheckLength checkLength = field.getAnnotation(CheckLength.class); + if (checkLength == null) { + continue; + } + int max = checkLength.max(); + int min = checkLength.min(); + try { + Object o = field.get(t); + String str = String.valueOf(o); + if (StrUtil.length(str) > max && max > 0) { + throw new HandleException(checkLength.message()); + } + if (StrUtil.length(str) < min && min > 0) { + throw new HandleException(checkLength.message()); + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + /** 检查数组长度*/ + for (Field field : fields) { + check(field, t); +// field.setAccessible(true); +// CheckListSize listSize = field.getAnnotation(CheckListSize.class); +// if (listSize == null) { +// continue; +// } +// try { +// Object o = field.get(t); +// if (o instanceof List) { +// List list = (List) o; +// if (CollectionUtil.isEmpty(list)) { +// throw new ApiException(listSize.message()); +// } +// } +// } catch (IllegalAccessException e) { +// e.printStackTrace(); +// } + } + } + + private static void check(Field field, T t) throws Exception{ + field.setAccessible(true); + CheckListSize listSize = field.getAnnotation(CheckListSize.class); + if (listSize != null) { + try { + Object o = field.get(t); + if (ObjectUtil.isNull(o)) { + throw new HandleException(listSize.message()); + } + if (o instanceof List) { + List list = (List) o; + if (CollectionUtil.isEmpty(list)) { + throw new HandleException(listSize.message()); + } + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PingYinUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PingYinUtil.java new file mode 100644 index 0000000..342fec5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/PingYinUtil.java @@ -0,0 +1,63 @@ +package jnpf.util; + +import net.sourceforge.pinyin4j.PinyinHelper; + +public class PingYinUtil { + + + /** + * 提取每个汉字的首字母(大写) + * + * @param str + * @return + */ + public static String getPinYinHeadChar(String str) { + if (isNull(str)) { + return ""; + } + String convert = ""; + for (int j = 0; j < str.length(); j++) { + char word = str.charAt(j); + // 提取汉字的首字母 + String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(word); + if (pinyinArray != null) { + convert += pinyinArray[0].charAt(0); + } + else { + convert += word; + } + } + + convert = string2AllTrim(convert); + return convert.toLowerCase(); + } + + /* + * 判断字符串是否为空 + */ + + public static boolean isNull(Object strData) { + if (strData == null || String.valueOf(strData).trim().equals("")) { + return true; + } + return false; + } + + /** + * 去掉字符串包含的所有空格 + * + * @param value + * @return + */ + public static String string2AllTrim(String value) { + if (isNull(value)) { + return ""; + } + return value.trim().replace(" ", ""); + } + public static void main(String[] args) { + String s = PingYinUtil.getPinYinHeadChar("张"); + System.out.println(s); + } +} + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionAnalysisUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionAnalysisUtil.java new file mode 100644 index 0000000..f506af4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionAnalysisUtil.java @@ -0,0 +1,736 @@ +package jnpf.util; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import jnpf.model.cultivate.po.exam.FtbCultivateExam; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUser; +import jnpf.model.cultivate.po.exam.FtbCultivateExamUserDetail; +import jnpf.model.cultivate.po.paper.FtbCultivateTestPaper; +import jnpf.model.cultivate.po.question.FtbCultivateQuestion; +import jnpf.model.cultivate.req.paper.PaperConfigReq; +import jnpf.model.cultivate.resp.*; +import jnpf.model.cultivate.v2.exam.po.CultivateExam; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForPersonExcelVo; +import jnpf.model.cultivate.v2.exam.vo.V2ExamStatisticsForPersonVo; +import jnpf.model.enums.CourseEnums; +import jnpf.model.personnels.dto.staff.roster.WorkerGroupDataDto; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 题目分析工具类 + */ +public class QuestionAnalysisUtil { + /** + * 初始化返回值 + * + * @return + */ + public static Map initAnalysQuestionCount() { + HashMap map = new HashMap<>(); + map.put(String.valueOf(CourseEnums.QuestionType.SINGLE.getCode()), new PaperConfigReq.QuestionNum(0, 0, 0)); + map.put(String.valueOf(CourseEnums.QuestionType.MULTI.getCode()), new PaperConfigReq.QuestionNum(0, 0, 0)); + map.put(String.valueOf(CourseEnums.QuestionType.JUDGE.getCode()), new PaperConfigReq.QuestionNum(0, 0, 0)); + map.put(String.valueOf(CourseEnums.QuestionType.FILL.getCode()), new PaperConfigReq.QuestionNum(0, 0, 0)); + map.put(String.valueOf(CourseEnums.QuestionType.INPUT.getCode()), new PaperConfigReq.QuestionNum(0, 0, 0)); + map.put(String.valueOf(CourseEnums.QuestionType.ONE_OR_MULTI.getCode()), new PaperConfigReq.QuestionNum(0, 0, 0)); + return map; + } + + /** + * 分析题目数量 + * + * @param map + * @param questionList + * @return + */ + public static void analysQuestionCount(Map map, + List questionList) { + if (CollectionUtil.isNotEmpty(questionList)) { + for (FtbCultivateQuestion question : questionList) { + String type = String.valueOf(question.getType()); + PaperConfigReq.QuestionNum questionNum = map.get(type); + if (question.getDifficulty().equals(CourseEnums.QuestionDifficulty.EASY.getCode())) { + questionNum.setSimpleNum(questionNum.getSimpleNum() + 1); + } else if (question.getDifficulty().equals(CourseEnums.QuestionDifficulty.MIDDLE.getCode())) { + questionNum.setGeneralNum(questionNum.getGeneralNum() + 1); + } else if (question.getDifficulty().equals(CourseEnums.QuestionDifficulty.MAX.getCode())) { + questionNum.setHardNum(questionNum.getHardNum() + 1); + } + } + } + } + + /** + * 计算百分比 + * + * @param score 用户考试分数 + * @param total 试卷总分数 + * @return + */ + public static Integer calculatePercentage(int score, int total) { + if (total == 0) { + return 0; // 避免除以零的错误 + } + return (int) Math.round((score / (float) total) * 100); + } + + + /** + * 计算用户考试的状态 + * + * @param exam 考试信息 + * @param paper 试卷信息 + * @param score 用户考试的总分数 + * @return + */ + + public static Integer calculateUserExamStatus(FtbCultivateExam exam, FtbCultivateTestPaper paper, Integer score) { + int examTotleScore = paper.getTotalScore();//试卷总分数 + //合格 + Integer passType = exam.getPassType();//合格分数类型(1固定分,2百分比 + Integer passMark = exam.getPassMark();//合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(passType)) { + if (score >= passMark) { + //已经合格,检测是否优秀 + if (checkIsVeryPass(examTotleScore, score, exam.getExcellentType(), exam.getExcellentMark())) { + return CourseEnums.ExamStatus.VERY_PASS.getCode(); + } + return CourseEnums.ExamStatus.PASS.getCode(); + } else { + ///不合格 + return CourseEnums.ExamStatus.NO_PASS.getCode(); + } + } else { + Integer calculateScore = QuestionAnalysisUtil.calculateScore(passMark, examTotleScore); + if (score >= calculateScore) { + //已经合格 判断是否优秀 + if (checkIsVeryPass(examTotleScore, score, exam.getExcellentType(), exam.getExcellentMark())) { + return CourseEnums.ExamStatus.VERY_PASS.getCode(); + } + return CourseEnums.ExamStatus.PASS.getCode(); + } else { + //不合格 + return CourseEnums.ExamStatus.NO_PASS.getCode(); + } + } + } + + public static Integer calculateUserExamStatus(FtbCultivateExam exam, Integer examTotleScore, Integer score) { + //合格 + Integer passType = exam.getPassType();//合格分数类型(1固定分,2百分比 + Integer passMark = exam.getPassMark();//合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(passType)) { + if (score >= passMark) { + //已经合格,检测是否优秀 + if (checkIsVeryPass(examTotleScore, score, exam.getExcellentType(), exam.getExcellentMark())) { + return CourseEnums.ExamStatus.VERY_PASS.getCode(); + } + return CourseEnums.ExamStatus.PASS.getCode(); + } else { + ///不合格 + return CourseEnums.ExamStatus.NO_PASS.getCode(); + } + } else { + Integer calculateScore = QuestionAnalysisUtil.calculateScore(passMark, examTotleScore); + if (score >= calculateScore) { + //已经合格 判断是否优秀 + if (checkIsVeryPass(examTotleScore, score, exam.getExcellentType(), exam.getExcellentMark())) { + return CourseEnums.ExamStatus.VERY_PASS.getCode(); + } + return CourseEnums.ExamStatus.PASS.getCode(); + } else { + //不合格 + return CourseEnums.ExamStatus.NO_PASS.getCode(); + } + } + } + + /** + * 判断是否优秀 + * + * @param totleScore 考试总分数 + * @param score 用户考试分数 + * @param excellentType 优秀分数类型(1固定分,2百分比) + * @param excellentMark 优秀分数 + * @return false 不优秀 true 优秀 + */ + public static boolean checkIsVeryPass(Integer totleScore, Integer score, Integer excellentType, Integer excellentMark) { + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(excellentType)) { + //固定分 + if (score >= excellentMark) { + return true; + } + return false; + } + + Integer calculateScore = QuestionAnalysisUtil.calculateScore(excellentMark, totleScore); + if (score >= calculateScore) { + return true; + } + return false; + } + + + /** + * 转换试卷题目 + * + * @param questionList + * @param examUserDetailList + * @return + */ + public static PaperQuestionVo convertPaperQuestionVo(List questionList, List examUserDetailList) { + PaperQuestionVo vo = new PaperQuestionVo(new HashMap<>()); + if (CollectionUtil.isEmpty(questionList)) { + return vo; + } + List appQuestionVoList = BeanUtil.copyToList(questionList, AppQuestionVo.class); + if (CollectionUtil.isNotEmpty(examUserDetailList)) { + //examUserDetailList 转换成 题目id 的map + Map examUserDetailMap = examUserDetailList.stream().collect(Collectors.toMap(FtbCultivateExamUserDetail::getQuestionId, Function.identity())); + //填充用户答案 + for (AppQuestionVo appQuestionVo : appQuestionVoList) { + FtbCultivateExamUserDetail detail = examUserDetailMap.get(appQuestionVo.getQuestionId()); + if (null != detail) { + appQuestionVo.setUserAnswer(examUserDetailMap.get(appQuestionVo.getId()).getUserAnswer()); + appQuestionVo.setIsComplete(true); + } + } + } + + + Map> questionOptionMap = new HashMap<>(); + //填充题目选项 + for (AppQuestionVo appQuestionVo : appQuestionVoList) { + String type = String.valueOf(appQuestionVo.getType()); + List questionOptionVoList = questionOptionMap.get(type); + if (CollectionUtil.isEmpty(questionOptionVoList)) { + questionOptionVoList = new ArrayList<>(); + } + questionOptionVoList.add(appQuestionVo); + questionOptionMap.put(type, questionOptionVoList); + } + vo.setQuestionMap(questionOptionMap); + return vo; + } + + /** + * 多选题判断是否正确 + * + * @param answer 标准答案 + * @param userAnswer 用户答案 + * @return true 正确 false 错误 + */ + public static boolean checkMultiRight(String answer, String userAnswer) { + if (StringUtils.isEmpty(answer) || StringUtils.isEmpty(userAnswer)) { + return false; + } + if (answer.equals(userAnswer)) { + return true; + } + List answerList = Arrays.asList(answer.split(",")); + List userAnswerList = Arrays.asList(userAnswer.split(",")); + if (answerList.size() != userAnswerList.size()) { + return false; + } + Collections.sort(answerList); + Collections.sort(userAnswerList); + String answerStr = String.join(",", answerList); + String userAnswerStr = String.join(",", userAnswerList); + if (answerStr.equals(userAnswerStr)) { + return true; + } + return false; + + } + + /** + * 计算两个日期之间的秒数 + * + * @param start + * @param end + * @return + */ + public static Long differenceSecond(Date start, Date end) { + //计算两个日期之间的秒数 + Calendar calendar1 = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + + // 设置Calendar对象的时间为date1和date2 + calendar1.setTime(start); + calendar2.setTime(end); + + // 计算两个日期之间的秒数差 + return (calendar2.getTimeInMillis() - calendar1.getTimeInMillis()) / 1000; + } + + /** + * 统计用户考试总数和已完成数 + * + * @param examUserList + * @return + */ + public static UserExamCount countCompleteAndTotleExamNum(List examUserList) { + Set totle = new HashSet<>(); + Set complete = new HashSet<>(); + for (FtbCultivateExamUser examUser : examUserList) { + StringBuilder sbTotle = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + totle.add(sbTotle.toString()); + if (!CourseEnums.ExamStatus.WAIT.getCode().equals(examUser.getStatus()) && + !CourseEnums.ExamStatus.OVERDUE.getCode().equals(examUser.getStatus())) { + StringBuilder completeTotle = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + complete.add(completeTotle.toString()); + } + } + UserExamCount userExamCount = new UserExamCount(); + userExamCount.setTotleNum(totle.size()); + userExamCount.setCompleteNum(complete.size()); + return userExamCount; + } + + /** + * 获取统计开始日期 + * + * @param date 目标日期 + * @param type 统计类型 1月,2季度,3年 + * @return + */ + + public static Date getStatisticsStartDate(Date date, Integer type) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + + if (type == 1) { + calendar.add(Calendar.MONTH, -1); + } else if (type == 2) { + calendar.add(Calendar.MONTH, -3); + } else { + calendar.add(Calendar.YEAR, -1); + } + return calendar.getTime(); + } + + /** + * 统计用户考试合格率 + * + * @param examUserList + */ + public static StatisticsResultDto statisticeLv(List examUserList) { + StatisticsResultDto dto = new StatisticsResultDto(); + if (CollectionUtil.isEmpty(examUserList)) { + return dto; + } + int totle = examUserList.size(); + int pass = 0; + int noPass = 0; + int excellent = 0; + + for (FtbCultivateExamUser examUser : examUserList) { + if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus())) { + pass++; + } else if (CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { + pass++; + excellent++; + } else if (CourseEnums.ExamStatus.NO_PASS.getCode().equals(examUser.getStatus())) { + noPass++; + } + } + + dto.setPass(pass); + dto.setNoPass(noPass); + dto.setTotle(totle); + dto.setExcellent(excellent); + dto.setPassLv(Double.valueOf(Math.round((pass / (float) totle) * 100))); + dto.setExcellentLv(Double.valueOf(Math.round((excellent / (float) totle) * 100))); + dto.setNoPassLv(Double.valueOf(Math.round((noPass / (float) totle) * 100))); + return dto; + } + + public static StatisticsResultDto statisticeLvForAppExam(List examUserList) { + StatisticsResultDto dto = new StatisticsResultDto(); + if (CollectionUtil.isEmpty(examUserList)) { + return dto; + } + int totle = examUserList.size(); + int pass = 0; + int noPass = 0; + int excellent = 0; + + for (AppExamListVo examUser : examUserList) { + if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus())) { + pass++; + } else if (CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { + excellent++; + } else if (CourseEnums.ExamStatus.NO_PASS.getCode().equals(examUser.getStatus())) { + noPass++; + } + } + + dto.setPass(pass); + dto.setNoPass(noPass); + dto.setTotle(totle); + dto.setExcellent(excellent); + dto.setPassLv(Double.valueOf(Math.round(((pass + excellent) / (float) totle) * 100))); + dto.setExcellentLv(Double.valueOf(Math.round((excellent / (float) totle) * 100))); + dto.setNoPassLv(Double.valueOf(Math.round((noPass / (float) totle) * 100))); + return dto; + } + + /** + * 统计合格数量 和 总数量 + * + * @param examUserList + * @return + */ + public static UserExamCount countPassAndTotleExamNum(List examUserList) { + Set totle = new HashSet<>(); + Set pass = new HashSet<>(); + Set noPassNum = new HashSet<>(); + Set waitNumSet = new HashSet<>(); + for (FtbCultivateExamUser examUser : examUserList) { + StringBuilder sbTotle = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getUserId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + totle.add(sbTotle.toString()); + if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus()) || + CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { + StringBuilder passTotle = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getUserId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + pass.add(passTotle.toString()); + } + + if (CourseEnums.ExamStatus.NO_PASS.getCode().equals(examUser.getStatus())) { + StringBuilder noPassSb = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getUserId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + noPassNum.add(noPassSb.toString()); + } + + if (CourseEnums.ExamStatus.WAIT_CHECK.getCode().equals(examUser.getStatus())) { + StringBuilder waitSb = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getUserId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + waitNumSet.add(waitSb.toString()); + } + } + UserExamCount userExamCount = new UserExamCount(); + userExamCount.setTotleNum(totle.size()); + userExamCount.setPassTotleNum(pass.size()); + userExamCount.setNoPassNum(noPassNum.size()); + userExamCount.setWaitNum(waitNumSet.size()); + return userExamCount; + } + + /** + * 统计初试复试合格率 + * + * @param examUserList + * @return + */ + public static StatisticsResultFirstAndRepeatDto statisticeFirstAndRepeatLv(List examUserList) { + + Set totle = new HashSet<>(); + Map pass = new HashMap<>(); + for (FtbCultivateExamUser examUser : examUserList) { + StringBuilder sbTotleKey = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + totle.add(sbTotleKey.toString()); + if (CourseEnums.ExamStatus.PASS.getCode().equals(examUser.getStatus()) || + CourseEnums.ExamStatus.VERY_PASS.getCode().equals(examUser.getStatus())) { + StringBuilder sbPassKey = new StringBuilder() + .append(examUser.getExamId()) + .append(examUser.getExamSource()) + .append(examUser.getRelationRankId()) + .append(examUser.getRelationCourseExamId()) + .append(examUser.getRelationPositionExamId()); + String key = sbPassKey.toString(); + Integer num = pass.get(key); + if (null == num) { + pass.put(key, 1); + } else { + pass.put(key, num + 1); + } + } + } + StatisticsResultFirstAndRepeatDto dto = new StatisticsResultFirstAndRepeatDto(); + dto.setTotle(totle.size()); + //遍历 pass + int firstPass = 0; + int repeatPass = 0; + for (Map.Entry entry : pass.entrySet()) { + String key = entry.getKey(); + Integer num = entry.getValue(); + if (num == 1) { + firstPass++; + } else { + repeatPass++; + } + } + dto.setFirstPass(firstPass); + dto.setRepeatPass(repeatPass); + dto.setFirstPassLv(Double.valueOf(Math.round((firstPass / (float) dto.getTotle()) * 100))); + dto.setRepeatPassLv(Double.valueOf(Math.round((repeatPass / (float) dto.getTotle()) * 100))); + return dto; + } + + /** + * 计算合格分数 + * + * @param num1 + * @param num2 + * @return + */ + public static Integer calculateScore(Integer num1, Integer num2) { + if (num1 == null || num1 == 0) { + return 0; + } + if (num2 == null || num2 == 0) { + return 0; + } + + double lv = (double) num1 / 100 * num2; + DecimalFormat df = new DecimalFormat("#"); + return Integer.valueOf(df.format(lv)); + } + + public static BigDecimal calculateScoreV2(Integer num1, Integer num2) { + if (num1 == null || num1 == 0) { + return new BigDecimal(0); + } + if (num2 == null || num2 == 0) { + return new BigDecimal(0); + } + return new BigDecimal(num1).multiply(new BigDecimal(num2)).divide(new BigDecimal(100)).setScale(2, RoundingMode.HALF_UP); + } + + public static void main(String[] args) throws InterruptedException { + Date date = new Date(); + Thread.sleep(1000); + Date now = new Date(); + if (now.after(date)) { + System.out.println("在之后"); + } else { + System.out.println("不在之后"); + } + + } + + /** + * 获取最高分 + * + * @param list + * @return + */ + public static UserRankingVo getMaxScore(List list) { + UserRankingVo vo = list.get(0); + for (UserRankingVo userRankingVo : list) { + int score = Integer.parseInt(userRankingVo.getScore()); + int maxScore = Integer.parseInt(vo.getScore()); + if (score > maxScore) { + vo = userRankingVo; + } + } + return vo; + } + + public static ExamStatisticsForPersonExcelVo convertToExcelPersonvo(ExamStatisticsForPersonVo vo) { + ExamStatisticsForPersonExcelVo excel = new ExamStatisticsForPersonExcelVo(); + excel.setUserName(vo.getUserName()); + excel.setSystemWokerId(vo.getSystemWokerId()); + List userOrgList = vo.getUserOrgList(); + if (CollectionUtil.isNotEmpty(userOrgList)) { + List names = userOrgList.stream() + .map(WorkerGroupDataDto::getAffiliatedOrgName) + .collect(Collectors.toList()); + excel.setOrgName(String.join(",", names)); + + List orgIds = userOrgList.stream() + .map(WorkerGroupDataDto::getOrgEncode) + .collect(Collectors.toList()); + excel.setOrgId(String.join(",", orgIds)); + + + List positionAndRanks = new ArrayList<>(); + for (WorkerGroupDataDto workerGroupDataDto : userOrgList) { + positionAndRanks.add(workerGroupDataDto.getAffiliatedPositionName() + "_" + workerGroupDataDto.getAffiliatedRankName()); + } + + excel.setPositionAndRank(String.join(",", positionAndRanks)); + + List positionIds = userOrgList.stream() + .map(WorkerGroupDataDto::getPositionEncode) + .collect(Collectors.toList()); + excel.setPositonId(String.join(",", positionIds)); + + } + if (null != vo.getStudyPostionName()) { + excel.setStudyPositionAndRank(vo.getStudyPostionName()); + } + //试卷类型,1岗位学习试卷,2常规试卷 + if (vo.getExamType() == 0) { + excel.setExamType("岗位学习考试"); + } else if (vo.getExamType() == 1) { + excel.setExamType("自定义考试"); + } + if (null != vo.getFinishtime()) { + excel.setFinishtime(DateUtil.format(vo.getFinishtime(), DatePattern.NORM_DATETIME_PATTERN)); + } + if (null != vo.getDuration()) { + excel.setDuration(vo.getDuration() / 60 + "分钟"); + } + if (null != vo.getExamTime()) { + excel.setExamTime(vo.getExamTime() + "分钟"); + } + if (null != vo.getScore()) { + excel.setScore(vo.getScore() + ""); + } + //(0待考试,1待批阅,2已逾期,3合格,4不合格,5优秀) + if (vo.getStatus() == 0 || vo.getStatus() == 2) { + excel.setExamStatus("待考"); + } else { + excel.setExamStatus("已考"); + } + + if (vo.getStatus() == 1) { + excel.setExamResult("待批阅"); + } else if (vo.getStatus() == 3) { + excel.setExamResult("合格"); + } else if (vo.getStatus() == 4) { + excel.setExamResult("不合格"); + } else if (vo.getStatus() == 5) { + excel.setExamResult("优秀"); + } + return excel; + } + + public static V2ExamStatisticsForPersonExcelVo convertToExcelPersonvoV2(V2ExamStatisticsForPersonVo vo) { + V2ExamStatisticsForPersonExcelVo excel = new V2ExamStatisticsForPersonExcelVo(); + excel.setUserName(vo.getUserName()); + excel.setSystemWorkerId(vo.getSystemWorkerId()); + excel.setOrgName(vo.getOrganizeName()); + excel.setOrgId(vo.getOrganizeId()); + if (StringUtils.isEmpty(vo.getGradeName())) { + excel.setPositionAndRank(vo.getPositionName()); + } else { + excel.setPositionAndRank(vo.getPositionName() + "_" + vo.getGradeName()); + } + excel.setPositionId(vo.getPositionEnCode()); + if (null != vo.getStudyPositionName()) { + excel.setStudyPositionAndRank(vo.getStudyPositionName()); + } + excel.setExamName(vo.getExamName()); + + //试卷类型,1岗位学习试卷,2常规试卷 + if (vo.getExamType() == 0) { + excel.setExamType("岗位学习考试"); + } else if (vo.getExamType() == 1) { + excel.setExamType("自定义考试"); + } + if (null != vo.getFinishtime()) { + excel.setFinishtime(DateUtil.format(vo.getFinishtime(), DatePattern.NORM_DATETIME_PATTERN)); + } + if (null != vo.getDuration()) { + excel.setDuration(vo.getDuration() / 60 + "分钟"); + } + if (null != vo.getExamTime()) { + excel.setExamTime(vo.getExamTime() + "分钟"); + } + if (null != vo.getScore()) { + excel.setScore(vo.getScore() + ""); + } + //(0待考试,1待批阅,2已逾期,3合格,4不合格,5优秀) + if (vo.getStatus() == 0 || vo.getStatus() == 2) { + excel.setExamStatus("待考"); + } else { + excel.setExamStatus("已考"); + } + + if (vo.getStatus() == 1) { + excel.setExamResult("待批阅"); + } else if (vo.getStatus() == 3) { + excel.setExamResult("合格"); + } else if (vo.getStatus() == 4) { + excel.setExamResult("不合格"); + } else if (vo.getStatus() == 5) { + excel.setExamResult("优秀"); + } + return excel; + } + + /** + * 对应字符串list去重复 + * + * @param list + * @return + */ + public static List uniqueStringList(List list) { + if (CollectionUtil.isEmpty(list)) { + return new ArrayList<>(); + } + List filteredList = list.stream() + .filter(str -> !str.isEmpty()) + .collect(Collectors.toList()); + Set uniqueSet = new HashSet<>(filteredList); + return new ArrayList<>(uniqueSet); + } + + public static BigDecimal calPassScore(Integer totalScore, CultivateExam exam) { + //合格 + Integer type = exam.getPassType();//合格分数类型(1固定分,2百分比 + Integer mark = exam.getPassMark();//合格分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(type)) { + return new BigDecimal(mark); + } else { + return calculateScoreV2(mark, totalScore); + } + } + + public static BigDecimal calExcellentScore(Integer totalScore, CultivateExam exam) { + Integer type = exam.getExcellentType();//优秀分数类型(1固定分,2百分比 + Integer mark = exam.getExcellentMark();//优秀分数 + if (CourseEnums.ExamScoreCheckType.FIXED.getCode().equals(type)) { + return new BigDecimal(mark); + } else { + return calculateScoreV2(mark, totalScore); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionBankIDGenerator.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionBankIDGenerator.java new file mode 100644 index 0000000..cb72ee4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/QuestionBankIDGenerator.java @@ -0,0 +1,29 @@ +package jnpf.util; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 题库ID生成规则 + */ +@Component +public class QuestionBankIDGenerator { + + private static AtomicInteger counter = new AtomicInteger(0); + + public String generateID() { + int currentCounter = counter.incrementAndGet(); + int letter = currentCounter / 1000; + char firstChar = (char) (letter + 'A'); + int remainingCounter = currentCounter % 1000; + String threeDigits = String.format("%03d", remainingCounter); + return "TK" + firstChar + threeDigits; + } + +// public static void main(String[] args) { +// for (int i = 0; i < 10000; i++) { +// System.out.println(generateID()); +// } +// } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RangeConfigUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RangeConfigUtil.java new file mode 100644 index 0000000..730477e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RangeConfigUtil.java @@ -0,0 +1,194 @@ +package jnpf.util; + + +import jnpf.model.personnels.dto.range.FtbRangeConfigDIYDTO; +import jnpf.model.personnels.dto.range.RangeDTO; +import jnpf.model.personnels.vo.range.FtbRangeConfigDIYVO; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 范围计算 + * @Author:peng.hao + */ +public class RangeConfigUtil { + /** + * 平均计算 + * + * @param initialValue 起始值 + * @param increment 步长值 + * @param numberOfIntervals 档位数 + * @param previewValue + */ + public static List generateRanges(int initialValue, int increment, int numberOfIntervals, String previewValue) { + List RangeDTOList = Collections.synchronizedList(new ArrayList<>()); + int start = initialValue - increment; + int end = initialValue; + String[] split = previewValue.split("、"); + RangeDTO priceBandStartDTO = new RangeDTO(0, end, "0_" + end, split[0]); + RangeDTOList.add(priceBandStartDTO); + for (int i = 1; i < numberOfIntervals - 1; i++) { + start += increment; + end = start + increment; + RangeDTO rangeDTO = new RangeDTO(start, end, start + "_" + end, split[i]); + RangeDTOList.add(rangeDTO); + } + RangeDTO rangeDTO = new RangeDTO(end, null, end + "", split[split.length - 1]); + RangeDTOList.add(rangeDTO); + return RangeDTOList; + } + + /** + * 计算区间 + * + * @param priceBandVOListConfig 自定义区间 + */ + public static List generateTakeOutRangesDto(List priceBandVOListConfig) { + List RangeDTOList = new ArrayList<>(); + List collected = priceBandVOListConfig.stream().sorted(Comparator.comparing(FtbRangeConfigDIYDTO::getIntervalCount)).collect(Collectors.toList()); + Map bandVOMap = collected.stream().collect(Collectors.toMap(FtbRangeConfigDIYDTO::getIntervalCount, Function.identity())); + Set keySet = bandVOMap.keySet(); + for (Integer integer : keySet) { + FtbRangeConfigDIYDTO priceBandVO = bandVOMap.get(integer); + Integer initialValue = priceBandVO.getInitialValue(); + // 10以下 + if (priceBandVO.getIntervalCount() == 1) { + RangeDTO RangeDTO = new RangeDTO(0, initialValue, "0_" + initialValue, initialValue + "以下"); + RangeDTOList.add(RangeDTO); + } + // 结束值 + Integer endInterval = priceBandVO.getEndInterval(); + // 间隔值 + Integer intervalValue = priceBandVO.getIntervalValue(); + if (priceBandVO.getIsAbove() == 1) { + initialValue = priceBandVO.getStartInterval(); + for (int i = initialValue; i < endInterval; i += intervalValue) { + if (i < initialValue) continue; + RangeDTO RangeDTO = new RangeDTO(i, + i + intervalValue, + i + "_" + (i + intervalValue), + i + "-" + (i + intervalValue)); + RangeDTOList.add(RangeDTO); + // 最后一个 + if (integer == keySet.size()) { + RangeDTO newRangeDTO = new RangeDTO((i + intervalValue), + (i + intervalValue + intervalValue), + (i + intervalValue) + "_" + (i + intervalValue + intervalValue), + (i + intervalValue) + "-" + (i + intervalValue + intervalValue)); + RangeDTOList.add(newRangeDTO); + } + } + + } else { + for (int i = initialValue; i < endInterval; i += intervalValue) { + if (i < initialValue) continue; + RangeDTO RangeDTO; + if (endInterval < i + intervalValue) { + RangeDTO = new RangeDTO(i, endInterval, i + "_" + endInterval, i + "-" + endInterval); + } else { + RangeDTO = new RangeDTO(i, + i + intervalValue, + i + "_" + (i + intervalValue), + i + "-" + (i + intervalValue)); + } + RangeDTOList.add(RangeDTO); + // 最后一个 + if (integer == keySet.size() ) { + RangeDTO newRangeDTO = new RangeDTO((i + intervalValue), + (i + intervalValue + intervalValue), + (i + intervalValue) + "_" + (i + intervalValue + intervalValue), + (i + intervalValue) + "-" + (i + intervalValue + intervalValue)); + RangeDTOList.add(newRangeDTO); + } + + } + // 只有一个 + if ( integer == 1) { + return RangeDTOList; + } + } + } + return RangeDTOList; + } + /** + * 计算区间 + * + * @param priceBandVOListConfig 自定义区间 + */ + public static List generateTakeOutRangesVo(List priceBandVOListConfig) { + List RangeDTOList = new ArrayList<>(); + List collected = priceBandVOListConfig.stream().sorted(Comparator.comparing(FtbRangeConfigDIYVO::getIntervalCount)).collect(Collectors.toList()); + Map bandVOMap = collected.stream().collect(Collectors.toMap(FtbRangeConfigDIYVO::getIntervalCount, Function.identity())); + Set keySet = bandVOMap.keySet(); + for (Integer integer : keySet) { + FtbRangeConfigDIYVO priceBandVO = bandVOMap.get(integer); + Integer initialValue = priceBandVO.getInitialValue(); + // 10以下 + if (priceBandVO.getIntervalCount() == 1) { + RangeDTO RangeDTO = new RangeDTO(0, initialValue, "0_" + initialValue, initialValue + "以下"); + RangeDTOList.add(RangeDTO); + } + // 结束值 + Integer endInterval = priceBandVO.getEndInterval(); + // 间隔值 + Integer intervalValue = priceBandVO.getIntervalValue(); + if (priceBandVO.getIsAbove() == 1) { + initialValue = priceBandVO.getStartInterval(); + for (int i = initialValue; i < endInterval; i += intervalValue) { + if (i < initialValue) continue; + RangeDTO RangeDTO = new RangeDTO(i, + i + intervalValue, + i + "_" + (i + intervalValue), + i + "-" + (i + intervalValue)); + RangeDTOList.add(RangeDTO); + // 最后一个 + if (integer == keySet.size()) { + RangeDTO newRangeDTO = new RangeDTO((i + intervalValue), + (i + intervalValue + intervalValue), + (i + intervalValue) + "_" + (i + intervalValue + intervalValue), + (i + intervalValue) + "-" + (i + intervalValue + intervalValue)); + RangeDTOList.add(newRangeDTO); + } + } + + } else { + for (int i = initialValue; i < endInterval; i += intervalValue) { + if (i < initialValue) continue; + RangeDTO RangeDTO; + if (endInterval < i + intervalValue) { + RangeDTO = new RangeDTO(i, endInterval, i + "_" + endInterval, i + "-" + endInterval); + } else { + RangeDTO = new RangeDTO(i, + i + intervalValue, + i + "_" + (i + intervalValue), + i + "-" + (i + intervalValue)); + } + RangeDTOList.add(RangeDTO); + // 最后一个 + if (integer == keySet.size() ) { + RangeDTO newRangeDTO = new RangeDTO((i + intervalValue), + (i + intervalValue + intervalValue), + (i + intervalValue) + "_" + (i + intervalValue + intervalValue), + (i + intervalValue) + "-" + (i + intervalValue + intervalValue)); + RangeDTOList.add(newRangeDTO); + } + + } + // 只有一个 + if ( integer == 1) { + return RangeDTOList; + } + } + } + return RangeDTOList; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RedisDistributedLock.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RedisDistributedLock.java new file mode 100644 index 0000000..3e191a8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/RedisDistributedLock.java @@ -0,0 +1,77 @@ +package jnpf.util; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Component +public class RedisDistributedLock { + + private final RedisTemplate redisTemplate; + + // Lua脚本用于原子性解锁 + private static final String UNLOCK_SCRIPT = + "if redis.call('get', KEYS[1]) == ARGV[1] then " + + " return redis.call('del', KEYS[1]) " + + "else " + + " return 0 " + + "end"; + + @Autowired + public RedisDistributedLock(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * 尝试获取分布式锁 + * @param lockKey 锁的key + * @param requestId 请求标识(建议使用UUID) + * @param expireTime 锁的过期时间(毫秒) + * @return 是否获取成功 + */ + public boolean tryLock(String lockKey, String requestId, long expireTime) { + return Boolean.TRUE.equals( + redisTemplate.opsForValue().setIfAbsent( + lockKey, + requestId, + expireTime, + TimeUnit.MILLISECONDS + ) + ); + } + + /** + * 释放分布式锁 + * @param lockKey 锁的key + * @param requestId 请求标识 + * @return 是否释放成功 + */ + public boolean releaseLock(String lockKey, String requestId) { + DefaultRedisScript redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class); + Long result = redisTemplate.execute( + redisScript, + Collections.singletonList(lockKey), + requestId + ); + return result != null && result == 1; + } + + /** + * 获取锁的简单方法(自动生成requestId) + * @param lockKey 锁的key + * @param expireTime 锁的过期时间(毫秒) + * @return requestId 如果获取成功,否则null + */ + public String lock(String lockKey, long expireTime) { + String requestId = UUID.randomUUID().toString(); + if (tryLock(lockKey, requestId, expireTime)) { + return requestId; + } + return null; + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SeetaFaceUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SeetaFaceUtil.java new file mode 100644 index 0000000..f611e90 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SeetaFaceUtil.java @@ -0,0 +1,137 @@ +//package jnpf.util; +// +//import com.seeta.sdk.*; +//import com.seeta.sdk.util.SeetafaceUtil; +//import jnpf.exception.HandleException; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.web.multipart.MultipartFile; +// +//import javax.imageio.ImageIO; +//import java.awt.image.BufferedImage; +//import java.io.InputStream; +//import java.net.HttpURLConnection; +//import java.net.URL; +// +///** +// * 人脸识别工具类 +// * +// * @author yanwenfu +// * @create 2025-04-08 +// */ +//@Slf4j +//public class SeetaFaceUtil { +// +// //人脸检测器 +// private static final FaceDetector detector; +// //关键点定位器 5点 +// private static final FaceLandmarker faceLandmarker; +// //人脸向量特征提取和对比器 +// private static final FaceRecognizer faceRecognizer; +// static { +// try { +// detector = new FaceDetector(new SeetaModelSetting(SeetaConstant.face_detector, SeetaDevice.SEETA_DEVICE_AUTO)); +// faceLandmarker = new FaceLandmarker(new SeetaModelSetting(SeetaConstant.face_landmarker_pts5, SeetaDevice.SEETA_DEVICE_AUTO)); +// faceRecognizer = new FaceRecognizer(new SeetaModelSetting(SeetaConstant.face_recognizer, SeetaDevice.SEETA_DEVICE_AUTO)); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } +// +// // 资源释放方法 +// public static synchronized void releaseModels() { +// try { +// if (detector != null) { +// detector.dispose(); // 实际方法名以SDK为准 +// } +// if (faceLandmarker != null) { +// faceLandmarker.dispose(); +// } +// if (faceRecognizer != null) { +// faceRecognizer.dispose(); +// } +// } catch (Exception e) { +// log.error("模型资源释放异常", e); +// } +// } +// +// /** +// * 获取照片特征(参数必须传递一个) +// * @param file 文件 +// * @param url 远程路径 +// * @return float[] +// */ +// public static float[] analyzeEcognizer(MultipartFile file, String url) throws HandleException { +// float[] features1 = null; +// try { +// //第1张照片 +// SeetaImageData image1; +// if (null != file) { +// image1 = SeetafaceUtil.toSeetaImageData(imageAsBufferedImage(file)); +// } else { +// image1 = SeetafaceUtil.toSeetaImageData(downloadImageAsBufferedImage(url)); +// } +// //第一张照片人脸识别 +// SeetaRect[] detects1 = detector.Detect(image1); +// if (null == detects1 || detects1.length == 0) { +// throw new HandleException("未识别到人脸,请重试"); +// } +// SeetaPointF[] pointFS1 = new SeetaPointF[faceRecognizer.GetExtractFeatureSize()]; +// int[] masks1 = new int[faceRecognizer.GetExtractFeatureSize()]; +// //第一张图片,第一个人脸关键点定位,有多个人脸的情况下,只取第一个人脸(这是测试,先这样写) +// faceLandmarker.mark(image1, detects1[0], pointFS1, masks1); +// //第一张图片,第一个人脸向量特征提取features1 +// features1 = new float[faceRecognizer.GetExtractFeatureSize()]; +// faceRecognizer.Extract(image1, pointFS1, features1); +// } catch (Exception e) { +// throw new HandleException(e.getMessage()); +// } +// return features1; +// } +// +// public static Boolean compareEigenvalue(float[] features1, float[] features2) { +// try { +// //人脸向量特征提取和对比器 +// // FaceRecognizer faceRecognizer = new FaceRecognizer(new SeetaModelSetting(SeetaConstant.face_recognizer, SeetaDevice.SEETA_DEVICE_AUTO)); +// if (features1 != null && features2 != null ) { +// float calculateSimilarity = faceRecognizer.CalculateSimilarity(features1, features2); +// +// if (calculateSimilarity > 0.75) { +// return true; +// } +// } +// } catch (Exception e) { +// log.error(e.getMessage()); +// return false; +// } +// return false; +// } +// +// public static BufferedImage imageAsBufferedImage(MultipartFile file) throws Exception { +// // 获取输入流 +// InputStream inputStream = file.getInputStream(); +// // 使用ImageIO读取InputStream到BufferedImage +// BufferedImage bufferedImage = ImageIO.read(inputStream); +// // 关闭输入流 +// inputStream.close(); +// return bufferedImage; +// } +// +// public static BufferedImage downloadImageAsBufferedImage(String imageUrl) throws Exception { +// // 创建URL对象 +// URL url = new URL(imageUrl); +// // 打开连接 +// HttpURLConnection connection = (HttpURLConnection) url.openConnection(); +// connection.setRequestMethod("GET"); +// // 连接到资源 +// connection.connect(); +// // 获取输入流 +// InputStream inputStream = connection.getInputStream(); +// // 使用ImageIO读取InputStream到BufferedImage +// BufferedImage bufferedImage = ImageIO.read(inputStream); +// // 关闭输入流 +// inputStream.close(); +// // 断开连接 +// connection.disconnect(); +// return bufferedImage; +// } +//} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SelfGrowthUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SelfGrowthUtil.java new file mode 100644 index 0000000..bc07f4b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/SelfGrowthUtil.java @@ -0,0 +1,108 @@ +package jnpf.util; + + +import cn.hutool.core.collection.CollUtil; +import jnpf.model.enums.SelfrowingEnum; +import jnpf.util.context.SpringContext; +import org.springframework.data.redis.core.RedisTemplate; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Date; + +/** + * @Title: SelfGrowthUtil 自增长工具类 + * @Author: peng.hao + * @create: 2023/12/1818:07 + */ +public class SelfGrowthUtil { + + /** + * 提供基于模块自定义 ID + * 提供基于模块自定义 ID + * + * @param selfrowingEnum SelfRowing 枚举 + * @return {@link String} + */ + public static String provideACustomIDBasedOnTheModule(SelfrowingEnum selfrowingEnum,Integer... args){ + RedisTemplate redisTemplate = SpringContext.getBean("redisTemplate"); + String key = UserProvider.getUser().getTenantId() + ":" + selfrowingEnum.getRedisKey(); + boolean temp = false; + Long result; + synchronized (SelfGrowthUtil.class){ + //只会为key设置一次过期时间 + if (!redisTemplate.hasKey(key)) { + temp = true; + } + result = redisTemplate.opsForValue().increment(key); + } + // 设置键的过期时间为当天23:59:59 + Date expirationDate = Date.from(LocalDate.now().atTime(23, 59, 59).atZone(ZoneOffset.of("+8")).toInstant()); + if (temp) { + redisTemplate.expireAt(key, expirationDate); + } + //向前补齐默认为3位,可以动态设置补位 + String perfix = "%03d"; + if (CollUtil.isNotEmpty(Arrays.asList(args))) perfix = "%0" + args[0] + "d"; + String format= String.format(perfix, result); + SimpleDateFormat spd =new SimpleDateFormat("yyyyMMdd"); + //当前年月日 + String nowDate = spd.format(expirationDate); + return selfrowingEnum.getReturnsThePrefix() + nowDate + format; + } + + public static String providePersonnelsCustomIDBasedOnTheModule(SelfrowingEnum selfrowingEnum){ + RedisTemplate redisTemplate = SpringContext.getBean("redisTemplate"); + String key = UserProvider.getUser().getTenantId() + ":" + selfrowingEnum.getRedisKey(); + boolean temp = false; + Long result; + synchronized (SelfGrowthUtil.class){ + //只会为key设置一次过期时间 + if (!redisTemplate.hasKey(key)) { + temp = true; + } + result = redisTemplate.opsForValue().increment(key); + } + // 设置键的过期时间为当天23:59:59 + Date expirationDate = Date.from(LocalDate.now().atTime(23, 59, 59).atZone(ZoneOffset.of("+8")).toInstant()); + if (temp) { + redisTemplate.expireAt(key, expirationDate); + } + //向前补齐0 + String format= String.format("%03d", result); + SimpleDateFormat spd =new SimpleDateFormat("yyMMdd"); + //当前年月日 + String nowDate = spd.format(expirationDate); + return selfrowingEnum.getReturnsThePrefix() + nowDate + format; + } + + + public static String provideACustomIDBasedOnTheModule(SelfrowingEnum selfrowingEnum, String tenantId) { + RedisTemplate redisTemplate = SpringContext.getBean("redisTemplate"); + String key = tenantId + ":" + selfrowingEnum.getRedisKey(); + boolean temp = false; + Long result; + synchronized (SelfGrowthUtil.class) { + //只会为key设置一次过期时间 + if (!redisTemplate.hasKey(key)) { + temp = true; + } + result = redisTemplate.opsForValue().increment(key); + } + // 设置键的过期时间为当天23:59:59 + Date expirationDate = Date.from(LocalDate.now().atTime(23, 59, 59).atZone(ZoneOffset.of("+8")).toInstant()); + if (temp) { + redisTemplate.expireAt(key, expirationDate); + } + //向前补齐0 + String format = String.format("%03d", result); + SimpleDateFormat spd = new SimpleDateFormat("yyyyMMdd"); + //当前年月日 + String nowDate = spd.format(expirationDate); + return selfrowingEnum.getReturnsThePrefix() + nowDate + format; + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ServiceException.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ServiceException.java new file mode 100644 index 0000000..f0ca17f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ServiceException.java @@ -0,0 +1,94 @@ +package jnpf.util; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * 业务异常 + * + * @author admin + */ +public final class ServiceException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * 错误码 + */ + private Integer code; + + /** + * 错误提示 + */ + private String message; + + /** + * 错误明细,内部调试错误 + *

+ * 和 {@link CommonResult#getDetailMessage()} 一致的设计 + */ + private String detailMessage; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(String message) { + this.message = message; + } + + public ServiceException(String message, Integer code) { + this.message = message; + this.code = code; + } + + public String getDetailMessage() { + return detailMessage; + } + + @Override + public String getMessage() { + return message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + + public ServiceException setDetailMessage(String detailMessage) { + this.detailMessage = detailMessage; + return this; + } + + public static void isTrue(boolean expression, String message) throws RuntimeException { + if (!expression) { + throw new ServiceException(message); + } + } + + public static void isTrue(Supplier expression, String message) throws RuntimeException { + if (!expression.get()) { + throw new ServiceException(message); + } + } + + public static void notNull(Object obj, String message) throws RuntimeException { + if (Objects.isNull(obj)) { + throw new ServiceException(message); + } + } + + public static void isNull(Object obj, String message) { + if (Objects.nonNull(obj)) { + throw new ServiceException(message); + } + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/TenantUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/TenantUtil.java new file mode 100644 index 0000000..fa23bc3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/TenantUtil.java @@ -0,0 +1,38 @@ +package jnpf.util; + +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.exception.LoginException; +import jnpf.util.data.DataSourceContextHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +@Component +public class TenantUtil { + @Autowired + private ConfigValueUtil configValueUtil; + + public void switchTenant(){ + switchTenant("yawen"); + } + public void switchTenant(String tenantId){ + // 判断是否为多租户 + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/V2UserQueryUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/V2UserQueryUtil.java new file mode 100644 index 0000000..bf891a1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/V2UserQueryUtil.java @@ -0,0 +1,101 @@ +package jnpf.util; + +import cn.hutool.core.collection.CollectionUtil; +import jnpf.authority.FtbAuthorityApi; +import jnpf.base.ActionResult; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.permission.vo.v2.user.UserBoundVO; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * V2版本用户查询工具类,提供多种方式获取用户信息的方法。 + */ +@Component +public class V2UserQueryUtil { + + @Resource + private V2UserApi v2UserApi; + @Resource + private FtbAuthorityApi authorityApi; + + /** + * 根据用户ID获取单个用户信息 + * @param id 用户唯一标识符 + * @return PartUserInfoVo 用户部分信息对象 + */ + public PartUserInfoVo getUserById(String id) { + return getUserListToPartUserInfoVo(List.of(id)).get(0); + } + + /** + * 批量根据用户ID列表获取用户信息列表 + * @param userIds 用户ID集合 + * @return List 用户信息列表 + */ + public List getUserListToPartUserInfoVo(List userIds) { + ActionResult> allUserInfoBatch = v2UserApi.getAllUserInfoBatch(userIds, UserProvider.getUser().getTenantId()); + List userList = allUserInfoBatch.getData(); + if (CollectionUtil.isEmpty(userList)) { + return new ArrayList<>(); + } + return userList.stream().map(user -> { + PartUserInfoVo vo = new PartUserInfoVo(); + vo.setUserId(user.getId()); + vo.setRealName(user.getName()); + vo.setNickName(user.getNickname()); + vo.setOrganizeId(user.getOrganizeId()); + vo.setOrganizeName(user.getOrganizeName()); + vo.setPositionId(user.getPositionId()); + vo.setPositionName(user.getPositionName()); + vo.setHeadIcon(user.getHeadIcon()); + vo.setMobilePhone(user.getPhone()); + vo.setAccount(user.getAccount()); + return vo; + }).collect(Collectors.toList()); + } + + /** + * 获取用户列表(包含离职用户) + * @param userIds 用户ID集合 + * @param tenantId 租户ID + * @return List 用户信息列表(含离职用户) + */ + public List getUserListAndCopy(List userIds, String tenantId) { + List userBoundVos = v2UserApi.userListAndCopy(userIds, null, tenantId); + if (CollectionUtil.isEmpty(userBoundVos)) { + return new ArrayList<>(); + } + return userBoundVos.stream().map(user -> { + PartUserInfoVo vo = new PartUserInfoVo(); + vo.setUserId(user.getId()); + vo.setRealName(user.getName()); + vo.setNickName(user.getNickname()); + vo.setOrganizeId(user.getOrganizeId()); + vo.setOrganizeName(user.getOrganizeName()); + vo.setPositionId(user.getPositionId()); + vo.setPositionName(user.getPositionName()); + vo.setHeadIcon(user.getHeadIcon()); + vo.setMobilePhone(user.getPhone()); + vo.setAccount(user.getAccount()); + return vo; + }).collect(Collectors.toList()); + } + + /** + * 获取指定组织及其子组织下的所有用户 + * @param organizeId 组织ID + * @return List 用户绑定信息列表 + */ + public List getOrganizeChildUserList(String organizeId) { + return authorityApi.listTargetOrganizeIdAuthApi(organizeId, List.of(UserWorkStatusEnums.ONBOARDING_FAILED, + UserWorkStatusEnums.PENDING_RESIGNATION, + UserWorkStatusEnums.RESIGNED)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ValidatorUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ValidatorUtils.java new file mode 100644 index 0000000..18dbfcd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/ValidatorUtils.java @@ -0,0 +1,37 @@ +package jnpf.util; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; + +/** + * 验证类 + * + * @author lx + * @version 1.0 + * @since 2021-10-26 + */ +public class ValidatorUtils { + private static Validator validator; + + static { + validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + + /** + * 校验对象 + * + * @param object 待校验对象 + * @param groups 待校验的组 + * @throws ServiceException 校验不通过,则报自定义异常 + */ + public static void validateEntity(Object object, Class... groups) + throws ServiceException { + Set> constraintViolations = validator.validate(object, groups); + if (!constraintViolations.isEmpty()) { + ConstraintViolation constraint = constraintViolations.iterator().next(); + throw new ServiceException(constraint.getMessage(), HttpStatus.BAD_REQUEST); + } + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/AttendanceGroupUserStatusUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/AttendanceGroupUserStatusUtil.java new file mode 100644 index 0000000..8fbb317 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/AttendanceGroupUserStatusUtil.java @@ -0,0 +1,244 @@ +package jnpf.util.attendance; + +import cn.hutool.core.collection.CollUtil; +import jnpf.entity.AttendanceGroupUser; +import jnpf.enums.attendance.GroupUserTypeEnum; +import jnpf.model.attendance.vo.SecondmentDateVo; +import jnpf.util.DateDetail; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 考勤组成员在组 / 借调 / 离组状态判断工具。 + *

从 {@link jnpf.attendance.service.impl.AttendanceDailyRuleServiceImpl} 抽取, + * 供排班、考勤本、统计等模块统一复用。

+ */ +public final class AttendanceGroupUserStatusUtil { + + /** + * 查询当日该用户是否存在于当前考勤组(详细状态)。 + * + * @return -1离组 0未入 1在组 2借调 + */ + public static Integer findUserIsExistsStatusByDay(List users, Date date) { + Date start = cn.hutool.core.date.DateUtil.beginOfDay(date); + Date end = cn.hutool.core.date.DateUtil.endOfDay(date); + return findUserIsExistsByDay(users, start, end); + } + + /** + * 查询当日该用户是否存在于当前考勤组(简化:1存在,0不存在)。 + */ + public static Integer findUserIsExistsByDay(List users, Date date) { + return findUserIsExistsStatusByDay(users, date) != 1 ? 0 : 1; + } + + /** + * 查询时间范围内该用户是否存在于当前考勤组。 + * + * @return -1离组 0未入 1在组 2借调 + */ + public static Integer findUserIsExistsByDay(List users, Date start, Date end) { + if (CollUtil.isEmpty(users)) { + return 0; + } + boolean isInGroup = users.stream().anyMatch(user -> Objects.isNull(user.getRemoveTime()) + ? Boolean.TRUE + : user.getRemoveTime().compareTo(start) > 0); + if (!isInGroup) { + return -1; + } + isInGroup = users.stream().anyMatch(user -> user.getUserGroupType() == 2 + ? Boolean.TRUE + : Objects.isNull(user.getCreatorTime()) ? Boolean.FALSE : end.compareTo(user.getCreatorTime()) >= 0); + if (!isInGroup) { + return 0; + } + boolean isInSecondment = users.stream() + .filter(user -> Objects.equals(user.getType(), GroupUserTypeEnum.CUR.getCode())) + .anyMatch(user -> { + List secondmentDateVos = parseSecondmentDates(user.getTimeJson()); + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.FALSE; + } + return secondmentDateVos.stream().anyMatch(dateVo -> + DateDetail.checkTimeBetween(start, dateVo.getStartTime(), dateVo.getEndTime()) + && DateDetail.checkTimeBetween(end, dateVo.getStartTime(), dateVo.getEndTime())); + }); + List borrowUsers = users.stream() + .filter(user -> Objects.equals(user.getType(), GroupUserTypeEnum.BORROW.getCode())) + .collect(Collectors.toList()); + boolean isSecondment = borrowUsers.stream().anyMatch(user -> { + List secondmentDateVos = parseSecondmentDates(user.getTimeJson()); + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.FALSE; + } + return Objects.nonNull(getSecondmentType(secondmentDateVos, start, end)); + }); + if (users.size() == borrowUsers.size() && !isSecondment) { + if (borrowUsers.stream().allMatch(user -> { + List secondmentDateVos = parseSecondmentDates(user.getTimeJson()); + return secondmentDateVos.stream().allMatch(vo -> vo.getEndTime().before(start)); + })) { + return -1; + } + return 0; + } + return !isInSecondment & (CollUtil.isEmpty(borrowUsers) ? Boolean.TRUE : !isSecondment) ? 1 : 2; + } + + /** + * 批量判断用户在某时间范围内是否在组(简化:1存在,0不存在)。 + */ + public static Map findUserIsExistsByUserList(List users, Date start, Date end) { + Map> userListMap = users.stream() + .collect(Collectors.groupingBy(AttendanceGroupUser::getUserId)); + Map map = new HashMap<>(); + for (Map.Entry> user : userListMap.entrySet()) { + Integer existStatus = isExistStatus(user.getValue(), start, end); + map.put(user.getKey(), Objects.equals(existStatus, 1) + || Objects.equals(existStatus, 3) + || Objects.equals(existStatus, 4) ? 1 : 0); + } + return map; + } + + /** + * 判断当天内用户的状态:-1已离 0未入 1正常 2全天被借调 3部分被借调 4借调。 + */ + public static Integer isExistStatus(List users, Date date) { + return isExistStatus(users, date, cn.hutool.core.date.DateUtil.endOfDay(date)); + } + + /** + * 判断时间范围内用户的状态:-1已离 0未入 1正常 2全天被借调 3部分被借调 4借调。 + */ + public static Integer isExistStatus(List users, Date start, Date end) { + if (CollUtil.isEmpty(users)) { + return 0; + } + List borrowUsers = users.stream() + .filter(user -> Objects.equals(user.getType(), GroupUserTypeEnum.BORROW.getCode())) + .collect(Collectors.toList()); + Boolean inSecondment = isInSecondment(users, start, end); + if (CollUtil.isNotEmpty(borrowUsers)) { + List borrowDates = borrowUsers.stream() + .flatMap(user -> StringUtil.isEmpty(user.getTimeJson()) + ? Stream.empty() + : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class).stream()) + .collect(Collectors.toList()); + List curInBorrowGroup = users.stream() + .filter(vo -> borrowUsers.stream().anyMatch(b -> vo.getGroupId().equals(b.getGroupId())) + && Objects.equals(vo.getType(), GroupUserTypeEnum.CUR.getCode())) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(curInBorrowGroup)) { + int isExist = getIsExist(curInBorrowGroup, start); + if (isExist != 0) { + return isExist; + } + } + if (inSecondment) { + return 4; + } + Date startTime = borrowDates.stream().map(SecondmentDateVo::getStartTime).max(Date::compareTo).orElse(null); + if (Objects.nonNull(startTime) && end.before(startTime)) { + return 0; + } + return -1; + } + List allSecondmentDates = users.stream() + .flatMap(user -> StringUtil.isEmpty(user.getTimeJson()) + ? Stream.empty() + : JsonUtil.getJsonToList(user.getTimeJson(), SecondmentDateVo.class).stream()) + .collect(Collectors.toList()); + if (inSecondment) { + if (allSecondmentDates.stream().anyMatch(vo -> + DateDetail.checkTimeBetween(start, vo.getStartTime(), vo.getEndTime()) + && DateDetail.checkTimeBetween(end, vo.getStartTime(), vo.getEndTime()))) { + return 2; + } + return 3; + } + return getIsExist(users, start); + } + + /** + * 是否借调:不管借调还是被借调,只要命中借调时间则返回 true。 + */ + public static Boolean isInSecondment(List users, Date start, Date end) { + if (CollUtil.isEmpty(users)) { + return Boolean.FALSE; + } + return users.stream().anyMatch(user -> { + List secondmentDateVos = parseSecondmentDates(user.getTimeJson()); + if (CollUtil.isEmpty(secondmentDateVos)) { + return Boolean.FALSE; + } + return Objects.nonNull(getSecondmentType(secondmentDateVos, start, end)); + }); + } + + /** + * 判断用户在某日是否处于「可维护考勤本」的在组状态。 + *

existStatus = 1 视为在组;3(部分被借调)、4(借调)按业务约定也视为在组。

+ */ + public static boolean isUserInGroupOnDay(List userGroupUsers, Date day) { + if (CollUtil.isEmpty(userGroupUsers) || day == null) { + return false; + } + Integer existStatus = isExistStatus(userGroupUsers, day); + if (Objects.equals(existStatus, 4) || Objects.equals(existStatus, 3)) { + existStatus = 1; + } + return Objects.equals(existStatus, 1); + } + + /** + * 获取目标时间范围涉及的借调时段。 + */ + public static SecondmentDateVo getSecondmentType(List secondmentDateVos, Date inPoint, Date outPoint) { + if (CollUtil.isEmpty(secondmentDateVos)) { + return null; + } + for (SecondmentDateVo dateVo : secondmentDateVos) { + if (DateDetail.checkTimeBetween(inPoint, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(outPoint, dateVo.getStartTime(), dateVo.getEndTime()) + || DateDetail.checkTimeBetween(dateVo.getStartTime(), inPoint, outPoint) + || DateDetail.checkTimeBetween(dateVo.getEndTime(), inPoint, outPoint)) { + return dateVo; + } + } + return null; + } + + private static List parseSecondmentDates(String timeJson) { + return StringUtil.isEmpty(timeJson) + ? CollUtil.newArrayList() + : JsonUtil.getJsonToList(timeJson, SecondmentDateVo.class); + } + + private static int getIsExist(List groupUserVos, Date date) { + if (Boolean.TRUE.equals(isInSecondment(groupUserVos, date, cn.hutool.core.date.DateUtil.endOfDay(date)))) { + return 1; + } + boolean isIn = groupUserVos.stream() + .allMatch(user -> cn.hutool.core.date.DateUtil.beginOfDay(user.getCreatorTime()).after(date)); + boolean isOut = groupUserVos.stream().anyMatch(user -> { + if (Objects.isNull(user.getRemoveTime())) { + user.setRemoveTime(jnpf.util.DateUtil.dateAddYears(date, 1)); + } + return DateDetail.checkTimeBetween(date, + cn.hutool.core.date.DateUtil.beginOfDay(user.getCreatorTime()), + cn.hutool.core.date.DateUtil.endOfDay(user.getRemoveTime())); + }); + return isIn ? 0 : !isOut ? -1 : 1; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DailyRuleUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DailyRuleUtil.java new file mode 100644 index 0000000..0632c8e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DailyRuleUtil.java @@ -0,0 +1,51 @@ +package jnpf.util.attendance; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import jnpf.entity.attendance.FtbAttendanceDailyRule; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +public class DailyRuleUtil { + + public static List mergeRules(List dayRules) { + dayRules.sort(Comparator.comparing(FtbAttendanceDailyRule::getOutPoint)); + FtbAttendanceDailyRule preRule = dayRules.get(0); + List mergeRules = CollUtil.newArrayList(preRule); + for (int i = 1; i < dayRules.size(); i++) { + FtbAttendanceDailyRule oldRule = preRule; + FtbAttendanceDailyRule currRule = dayRules.get(i); + if (oldRule.getOutPoint().compareTo(currRule.getInPoint()) >= 0 && oldRule.getOutPoint().compareTo(currRule.getOutPoint()) <= 0) { + oldRule.setOutPoint(currRule.getOutPoint()); + continue; + } + preRule = currRule; + mergeRules.add(preRule); + } + return mergeRules; + } + + /** + * 向上取半小时为最小单位的时间 + * + * @param dateTime + * @return + */ + public static Date ceilToHalfHour(Date dateTime) { + if (Objects.isNull(dateTime)) { + dateTime = DateUtil.offsetMonth(DateUtil.endOfDay(new Date()), 12); + } + int minute = cn.hutool.core.date.DateUtil.minute(dateTime); + if (minute % 30 == 0) { + return dateTime; + } + + int nextHalfHour = ((minute / 30) + 1) * 30; + dateTime.setMinutes(nextHalfHour); + dateTime.setSeconds(0); + return dateTime; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DayStatisticsUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DayStatisticsUtils.java new file mode 100644 index 0000000..e97df61 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/DayStatisticsUtils.java @@ -0,0 +1,3553 @@ +package jnpf.util.attendance; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.databind.ObjectMapper; +import jnpf.attendance.dto.MonthStatsDailySituationVo; +import jnpf.base.vo.PageListVO; +import jnpf.base.vo.PaginationVO; +import jnpf.entity.attendance.*; +import jnpf.enums.attendance.*; +import jnpf.model.attendance.dto.SalaryAttendanceSupportDto; +import jnpf.model.attendance.model.*; +import jnpf.model.attendance.vo.*; +import jnpf.model.attendance.vo.attendance.*; +import jnpf.model.common.DateRangeDto; +import jnpf.model.common.DateRangeDto1; +import jnpf.model.cultivate.CultivatePage; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.util.*; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.BeanUtils; + +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static cn.hutool.core.collection.CollUtil.newArrayList; +import static com.alibaba.fastjson.JSON.parseArray; +import static jnpf.enums.attendance.StatisticsEnumUtil.TabDataTypeEnum.values; + +@Slf4j +public class DayStatisticsUtils { + /** + * 获取表头 + * + * @return 表头 + */ + public static List getHead() { + List head1 = new ArrayList<>(); + head1.add("成员"); + head1.add("考勤组"); + head1.add("部门"); + head1.add("岗位"); + head1.add("日期"); + head1.add("班次时间"); + head1.add("打卡时间"); + head1.add("其余打卡时间"); + return head1; + } + + /** + * 获取列宽 + * + * @return 列宽 + */ + public static List getWidths() { + List newWidths = new ArrayList<>(); + newWidths.add(100); + newWidths.add(100); + newWidths.add(100); + newWidths.add(100); + newWidths.add(100); + newWidths.add(150); + newWidths.add(150); + newWidths.add(300); + return newWidths; + } + + /** + * 筛选班次打卡记录 + * + * @param clockInResultList 筛选打卡记录集合 + * @param statisticsTypeEnum 考勤统计类型 + */ + public static List getClockInResultFilterList(List clockInResultList, StatisticsEnumUtil.StatisticsTypeEnum statisticsTypeEnum) { + List clockInResultFilterList = newArrayList(); + switch (statisticsTypeEnum) { + case CD: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue()) && m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList()); + break; + } + case QK: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue()) && m.getAbsence().equals(ConstantUtil.NUM_FALSE)).collect(Collectors.toList()); + break; + } + case ZT: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue()) && m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList()); + break; + } + case KG: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getDeleteMark().equals(ConstantUtil.NUM_FALSE) && m.getAbsence().equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList()); + break; + } + case JD: + case JB: { + clockInResultFilterList = clockInResultList; + break; + } + case WQ: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)).collect(Collectors.toList()); + break; + } + case BK: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getRepaired().equals(1)).collect(Collectors.toList()); + break; + } + case SJCQ: { + clockInResultFilterList = clockInResultList.stream().filter(m -> m.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) || m.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue()) || m.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue()) || m.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue())).collect(Collectors.toList()); + break; + } + + } + return clockInResultFilterList; + } + + /** + * 筛选考勤统计类型 + * + * @param groupList 筛选班次记录集合 + * @param statisticsTypeEnum 考勤统计类型 + */ + public static List getWebClassesFilterList(List groupList, StatisticsEnumUtil.StatisticsTypeEnum statisticsTypeEnum) { + List groupFilterList = newArrayList(); + switch (statisticsTypeEnum) { + case QB: { + break; + } + + case XX: { + groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).collect(Collectors.toList()); + break; + } + case CD: + case KG: + case ZT: { + groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())).collect(Collectors.toList()); + break; + } + case YCQ: { + groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList()); + break; + } + case QK: + case BK: + case WQ: { + groupFilterList = groupList.stream().filter(m -> (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode()))).collect(Collectors.toList()); + break; + } + case QJ: { + groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList()); + break; + } + case JD: { + groupFilterList = groupList.stream().filter(m -> !m.getSelfGroup().equals(SecondmentTypeEnum.NOT.getCode())).collect(Collectors.toList()); + break; + } + case JB: { + groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())).collect(Collectors.toList()); + break; + } + case JQ: { + groupFilterList = groupList.stream().filter(m -> m.getAttendanceType().equals(AttendanceTypeEnum.HOLIDAYS.getCode())).collect(Collectors.toList()); + break; + } + case CC: { + groupFilterList = groupList.stream().filter(m -> (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) && (m.getApplyViewEnable().equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode()) && m.getValidDuration() > 0)).collect(Collectors.toList()); + break; + } + case WC: { + groupFilterList = groupList.stream().filter(m -> (m.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode()) || m.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) && (m.getApplyViewEnable().equals(AttendanceTypeEnum.STEP_OUT.getCode()) && m.getValidDuration() > 0)).collect(Collectors.toList()); + break; + } + } + return groupFilterList; + } + + /** + * 判断两个日期是否是同一天 + * + * @param date1 第一个日期 + * @param date2 第二个日期 + * @return 如果是同一天,则返回true;否则返回false + */ + public static boolean isSameDay(Date date1, Date date2) { + Calendar calendar1 = Calendar.getInstance(); + Calendar calendar2 = Calendar.getInstance(); + calendar1.setTime(date1); + calendar2.setTime(date2); + calendar1.set(Calendar.HOUR_OF_DAY, 0); + calendar1.set(Calendar.MINUTE, 0); + calendar1.set(Calendar.SECOND, 0); + calendar1.set(Calendar.MILLISECOND, 0); + calendar2.set(Calendar.HOUR_OF_DAY, 0); + calendar2.set(Calendar.MINUTE, 0); + calendar2.set(Calendar.SECOND, 0); + calendar2.set(Calendar.MILLISECOND, 0); + return calendar1.getTimeInMillis() == calendar2.getTimeInMillis(); + } + + /** + * 获取月份列表 + * + * @param start 开始月份 + * @param end 结束月份 + * @return 月份列表 + */ + public static List generateMonthList(String start, String end) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + YearMonth startYearMonth = YearMonth.parse(start, formatter); + YearMonth endYearMonth = YearMonth.parse(end, formatter); + List monthList = new ArrayList<>(); + YearMonth current = startYearMonth; + while (!current.isAfter(endYearMonth)) { + monthList.add(current.format(formatter)); + current = current.plusMonths(1); + } + return monthList; + } + + /** + * 根据月份获取日期列表 + * + * @param monthString 月份 + * @param yyyyMM + * @return 日期列表 + */ + public static List getDatesForMonth(String monthString, String yyyyMM) { + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern(yyyyMM); + YearMonth yearMonth = YearMonth.parse(monthString, monthFormatter); + LocalDate firstDayOfMonth = yearMonth.atDay(1); + //判断是不是挡墙月,当前月截止日期是当天 + LocalDate lastDayOfMonth; + if (monthString.equals(DateUtil.format(new Date(), yyyyMM))) { + lastDayOfMonth = LocalDate.now(); + } else { + lastDayOfMonth = yearMonth.atEndOfMonth(); + } + List dateList = new ArrayList<>(); + LocalDate currentDate = firstDayOfMonth; + while (!currentDate.isAfter(lastDayOfMonth)) { + dateList.add(currentDate); + currentDate = currentDate.plusDays(1); + } + return dateList; + } + + public static List getStatisticsDataPackage(StatisticsDataQueryVo dayStatisticsDataVo) { + StatisticsEnumUtil.TabStaBlockEnum[] values = StatisticsEnumUtil.TabStaBlockEnum.values(); + List statisticsDataVoList = new ArrayList<>(); + for (StatisticsEnumUtil.TabStaBlockEnum value : values) { + Integer count = 0; + if (Objects.nonNull(dayStatisticsDataVo)) { + switch (value) { + case QB: + count = dayStatisticsDataVo.getTotalCount(); + break; + case ZC: + count = dayStatisticsDataVo.getNormalCount(); + break; + case CD: + count = dayStatisticsDataVo.getLateCount(); + break; + case ZT: + count = dayStatisticsDataVo.getEarlyLeaveCount(); + break; + case WPB: + count = dayStatisticsDataVo.getNotSchedulingCount(); + break; + case QK: + count = dayStatisticsDataVo.getAbsenceCardCount(); + break; + case QQ: + count = dayStatisticsDataVo.getAbsenceCount(); + break; + case QJ: + count = dayStatisticsDataVo.getLeaveCount(); + break; + case CC: + count = dayStatisticsDataVo.getBusCount(); + break; + case WC: + count = dayStatisticsDataVo.getOutCount(); + break; + case JD: + count = dayStatisticsDataVo.getSelfCount(); + break; + case JB: + count = dayStatisticsDataVo.getOvertimeCount(); + break; + } + } + statisticsDataVoList.add(DayStatisticsDataVo.builder().typeId(value.getCode()).count(count).staName(value.getMsg()).build()); + } + return statisticsDataVoList; + } + + /** + * 获取用户每个请假类型的数据 + * + * @param attendanceRatio 出勤换算比 + * @param leaveForApplyIds 用户当日请假申请ID集合 + * @param leaveAllList 请假数据集合 + * @return 用户请假数据 + */ + public static Map getUserLeaveTypeSum(BigDecimal attendanceRatio, + List leaveForApplyIds, List leaveAllList) { + Map userLeaveTypeMap = new HashMap<>(); + Map> leaveTypeMap = leaveForApplyIds.stream().filter(m -> StringUtil.isNotEmpty(m.getLeaveId())).collect(Collectors.groupingBy(LeaveSituationData::getLeaveId)); + for (Map.Entry> entry : leaveTypeMap.entrySet()) { + LeaveTypeStaModel leaveTypeStaModel = new LeaveTypeStaModel(); + leaveTypeStaModel.setIsRest(entry.getValue().get(0).getIsRest()); + List applyIds = entry.getValue().stream().map(LeaveSituationData::getApplyId).distinct().collect(Collectors.toList()); + List leaveAllListFilter = leaveAllList.stream().filter(m -> applyIds.contains(m.getApplyId())).collect(Collectors.toList()); + if (StringUtil.isEmpty(leaveAllListFilter)) { + continue; + } + BigDecimal totalHours = BigDecimal.ZERO; + BigDecimal totalDays = BigDecimal.ZERO; + List applyList = new ArrayList<>(); + for (FtbAttendanceDailyRule dailyRule : leaveAllListFilter) { + BigDecimal leaveHours = DateConvertUtil.minuteConvert(new BigDecimal(dailyRule.getValidDuration()), 1, 2, null); + BigDecimal leaveDays; + // 划线排班计算天数需要根据出勤换算比计算 + if (dailyRule.getFixedMark().equals(2)) { + leaveDays = totalHours.divide(attendanceRatio, 2, RoundingMode.HALF_UP).compareTo(BigDecimal.ONE) > 0 ? + BigDecimal.ONE : totalHours.divide(attendanceRatio, 2, RoundingMode.HALF_UP); + if (leaveDays.compareTo(BigDecimal.ONE) > 0) { + leaveDays = BigDecimal.ONE; + } + } else { + leaveDays = dailyRule.getLeaveDay(); + } + totalHours = totalHours.add(leaveHours); + totalDays = totalDays.add(leaveDays); + LeaveTypeStaDetailsModel model = new LeaveTypeStaDetailsModel(); + model.setApplyId(dailyRule.getApplyId()); + model.setLeaveDays(leaveDays); + model.setLeaveHours(leaveHours); + model.setLeavePayrollHours(leaveHours); + applyList.add(model); + } + leaveTypeStaModel.setLeaveHours(totalHours); + leaveTypeStaModel.setLeavePayrollHours(totalHours); + leaveTypeStaModel.setLeaveDays(totalDays); + leaveTypeStaModel.setApplyList(applyList); + userLeaveTypeMap.put(entry.getKey(), leaveTypeStaModel); + } + return userLeaveTypeMap; + } + + /** + * 获取用户每个请假类型的数据 + * + * @param leaveTypeMap 请假类型聚合数据 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 用户请假数据 + */ + public static Map getUserLeaveTypeSum(Map> leaveTypeMap, Date startDate, Date endDate) { + Map userLeaveTypeMap = new HashMap<>(); + for (Map.Entry> entry : leaveTypeMap.entrySet()) { + List dataList = entry.getValue(); + if (CollUtil.isEmpty(dataList)) { + continue; + } + // 构建请假类型统计模型 + LeaveTypeStaModel model = buildLeaveTypeModel(dataList, startDate, endDate); + model.setIsRest(dataList.get(0).getIsRest()); + userLeaveTypeMap.put(entry.getKey(), model); + } + return userLeaveTypeMap; + } + + /** + * 构建请假类型统计模型 + */ + private static LeaveTypeStaModel buildLeaveTypeModel(List dataList, Date startDate, Date endDate) { + LeaveTypeStaModel model = new LeaveTypeStaModel(); + LeaveStatisticsTotal stats = new LeaveStatisticsTotal(); + for (LeaveSituationData data : dataList) { + processLeaveData(data, startDate, endDate, stats); + } + model.setLeaveDays(stats.getDaysTotal()); + model.setLeaveHours(stats.getHoursTotal()); + model.setLeavePayrollHours(stats.getPayrollHoursTotal()); + model.setApplyList(stats.getDetailList()); + return model; + } + + /** + * 处理单条请假数据 + */ + private static void processLeaveData(LeaveSituationData data, Date startDate, Date endDate, LeaveStatisticsTotal stats) { + // 过滤老数据 + if (StrUtil.isEmpty(data.getBalanceJsonNew())) { + return; + } + // 解析班次信息 + LeaveShiftVo shiftVo = JsonUtil.getJsonToBean(data.getShiftInvolved(), LeaveShiftVo.class); + if (Objects.isNull(shiftVo)) { + return; + } + // 移除无效的日程规则 + shiftVo.getDailyRuleVos().removeIf(rule -> Objects.isNull(rule.getFromType())); + // 计算请假数据 + ProcessResult calcResult = calculateLeaveData(data, shiftVo, startDate, endDate); + // 更新统计 + if (calcResult != null && calcResult.getHours().compareTo(BigDecimal.ZERO) > 0) { + stats.add(calcResult); + // 添加明细 + LeaveTypeStaDetailsModel detail = new LeaveTypeStaDetailsModel(); + detail.setApplyId(data.getApplyId()); + detail.setLeaveDays(calcResult.getDays()); + detail.setLeaveHours(calcResult.getHours()); + detail.setLeavePayrollHours(calcResult.getPayrollHours()); + stats.addDetail(detail); + } + } + + /** + * 计算请假数据(核心逻辑) + */ + private static ProcessResult calculateLeaveData(LeaveSituationData data, LeaveShiftVo shiftVo, Date startDate, Date endDate) { + // 检查出勤换算比 + if (Objects.isNull(shiftVo.getAttendanceRatio())) { + return null; + } + // 过滤请假班次规则(toType=3表示请假) + List validRules = shiftVo.getDailyRuleVos().stream().filter(rule -> rule.getToType() == 3).collect(Collectors.toList()); + // 判断是否命中班次 + boolean hasHitRules = CollUtil.isNotEmpty(validRules); + if (!hasHitRules) { + // 未命中班次,按基础规则计算 + return calculateWithoutHitRules(data, shiftVo, startDate, endDate); + } else { + // 命中班次,按班次规则计算 + return calculateWithHitRules(data, shiftVo, startDate); + } + } + + /** + * 未命中班次的计算逻辑 + */ + private static ProcessResult calculateWithoutHitRules(LeaveSituationData data, LeaveShiftVo shiftVo, Date startDate, Date endDate) { + int unit = data.getUnit(); + if (unit == 1) { + // 小时假 + MutablePair entityInterval = MutablePair.of(data.getStartTime(), data.getEndTime()); + MutablePair filterInterval = MutablePair.of(startDate, endDate); + BigDecimal hitHours = calculateIntersectionHours(shiftVo.getAttendanceRatio(), entityInterval, filterInterval); + BigDecimal hitDays = hitHours.divide(shiftVo.getAttendanceRatio(), 2, RoundingMode.HALF_UP); + return new ProcessResult(hitDays, hitHours, hitHours); + } else if (unit == 2) { + // 全天假 + BigDecimal hitHours = shiftVo.getAttendanceRatio(); + return new ProcessResult(BigDecimal.ONE, hitHours, hitHours); + } else { + // 半天假 + BigDecimal hitHours = shiftVo.getAttendanceRatio().divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + // 请假开始日期和结束日期-跟统计日期是同一天 + if (DateUtil.isSameDay(data.getStartTime(), startDate) && DateUtil.isSameDay(data.getEndTime(), startDate)) { + if (data.getStartTimeType().equals(2) || data.getEndTimeType().equals(1)) { + return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours); + } else { + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } + } + // 请假开始日期跟统计日期不是同一天,和结束日期是同一天 + if (!DateUtil.isSameDay(data.getStartTime(), startDate) && DateUtil.isSameDay(data.getEndTime(), startDate)) { + if (data.getEndTimeType().equals(1)) { + return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours); + } else { + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } + } + // 请假开始日期跟统计日期是同一天,和结束日期不是同一天 + if (DateUtil.isSameDay(data.getStartTime(), startDate) && !DateUtil.isSameDay(data.getEndTime(), startDate)) { + if (data.getStartTimeType().equals(2)) { + return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours); + } else { + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } + } + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } + } + + /** + * 命中班次的计算逻辑 + */ + private static ProcessResult calculateWithHitRules(LeaveSituationData data, LeaveShiftVo shiftVo, Date startDate) { + int unit = data.getUnit(); + List todayRules = getRulesByDate(shiftVo.getDailyRuleVos(), startDate); + if (unit == 1) { + // 小时假 + return calculateHourlyLeaveWithRules(data, shiftVo, todayRules, startDate); + } else if (unit == 2) { + // 全天假 + return calculateFullDayLeaveWithRules(data, shiftVo, todayRules, startDate); + } else { + // 半天假 + return calculateHalfDayLeaveWithRules(shiftVo, todayRules); + } + } + + /** + * 小时假 - 命中班次计算 + */ + private static ProcessResult calculateHourlyLeaveWithRules(LeaveSituationData data, LeaveShiftVo shiftVo, List todayRules, Date startDate) { + // 判断是否同一天 + boolean isSameDay = DateUtil.isSameDay(data.getStartTime(), data.getEndTime()); + if (isSameDay) { + // 未跨日 + if (CollUtil.isEmpty(todayRules)) { + // 未命中班次 + BigDecimal hours = DateConvertUtil.dateConvert(data.getStartTime(), data.getEndTime(), 2, 2, null); + if (hours.compareTo(shiftVo.getAttendanceRatio()) > 0) { + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } else { + BigDecimal hitDays = hours.divide(shiftVo.getAttendanceRatio(), 2, RoundingMode.HALF_UP); + return new ProcessResult(hitDays, hours, hours); + } + } else { + // 命中班次 + return calculateFromRules(todayRules, shiftVo.getAttendanceRatio()); + } + } else { + // 跨日处理 + return calculateCrossDayHourlyLeave(data, shiftVo, todayRules, startDate); + } + } + + /** + * 小时假 - 跨日计算 + */ + private static ProcessResult calculateCrossDayHourlyLeave(LeaveSituationData data, LeaveShiftVo shiftVo, List todayRules, Date startDate) { + if (DateUtil.isSameDay(data.getEndTime(), startDate)) { + // 结束时间是今天,检查昨天 + Date yesterday = DateUtil.offsetDay(startDate, -1); + List yesterdayRules = getRulesByDate(shiftVo.getDailyRuleVos(), yesterday); + return handleCrossDayRules(todayRules, yesterdayRules, shiftVo.getAttendanceRatio()); + } else { + // 结束时间是明天,检查明天 + Date nextDay = DateUtil.offsetDay(startDate, 1); + List nextDayRules = getRulesByDate(shiftVo.getDailyRuleVos(), nextDay); + return handleCrossDayRules(todayRules, nextDayRules, shiftVo.getAttendanceRatio()); + } + } + + /** + * 处理跨日班次规则 + */ + private static ProcessResult handleCrossDayRules(List todayRules, List otherDayRules, BigDecimal attendanceRatio) { + boolean todayEmpty = CollUtil.isEmpty(todayRules); + boolean otherEmpty = CollUtil.isEmpty(otherDayRules); + if (todayEmpty && otherEmpty) { + // 都无班次,跳过 + return null; + } else if (!todayEmpty && otherEmpty) { + // 只有今天有 + return calculateFromRules(todayRules, attendanceRatio); + } else if (todayEmpty) { + // 只有其他天有,跳过 + return null; + } else { + // 两天都有,只算今天 + return calculateFromRules(todayRules, attendanceRatio); + } + } + + /** + * 全天假 - 命中班次计算 + */ + private static ProcessResult calculateFullDayLeaveWithRules(LeaveSituationData data, LeaveShiftVo shiftVo, List todayRules, Date startDate) { + if (DateUtil.isSameDay(data.getEndTime(), data.getStartTime())) { + // 单天请假 + if (CollUtil.isEmpty(todayRules)) { + BigDecimal hours = DateConvertUtil.dateConvert(data.getStartTime(), data.getEndTime(), 2, 2, null); + if (hours.compareTo(shiftVo.getAttendanceRatio()) > 0) { + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } else { + BigDecimal hitDays = hours.divide(shiftVo.getAttendanceRatio(), 2, RoundingMode.HALF_UP); + return new ProcessResult(hitDays, hours, hours); + } + } else { + return calculateFromRules(todayRules, shiftVo.getAttendanceRatio()); + } + } else { + // 多天请假 + if (CollUtil.isEmpty(shiftVo.getDailyRuleVos())) { + // 从未命中过班次 + return new ProcessResult(BigDecimal.ONE, shiftVo.getAttendanceRatio(), shiftVo.getAttendanceRatio()); + } else if (CollUtil.isNotEmpty(todayRules)) { + return calculateFromRules(todayRules, shiftVo.getAttendanceRatio()); + } else { + // 命中过但今天未命中 + return null; + } + } + } + + /** + * 半天假 - 命中班次计算 + */ + private static ProcessResult calculateHalfDayLeaveWithRules(LeaveShiftVo shiftVo, List todayRules) { + if (CollUtil.isNotEmpty(shiftVo.getDailyRuleVos()) && CollUtil.isEmpty(todayRules)) { + return new ProcessResult(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO); + } + if (CollUtil.isEmpty(todayRules)) { + BigDecimal hitHours = shiftVo.getAttendanceRatio().divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP); + return new ProcessResult(new BigDecimal("0.5"), hitHours, hitHours); + } + return calculateFromRules(todayRules, shiftVo.getAttendanceRatio()); + } + + /** + * 从规则列表计算汇总 + */ + private static ProcessResult calculateFromRules(List rules, BigDecimal attendanceRatio) { + BigDecimal hitHours = sumRuleField(rules, DailyRuleResultVo::getDuration, attendanceRatio); + BigDecimal hitPayrollHours = sumRuleField(rules, DailyRuleResultVo::getPayrollHours, attendanceRatio); + BigDecimal hitDays = sumRuleField(rules, DailyRuleResultVo::getLeaveDays, attendanceRatio); + return new ProcessResult(hitDays, hitHours, hitPayrollHours); + } + + /** + * 汇总规则字段 + */ + private static BigDecimal sumRuleField(List rules, Function fieldExtractor, BigDecimal attendanceRatio) { + BigDecimal reduce = rules.stream().map(rule -> Optional.ofNullable(fieldExtractor.apply(rule)).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add); + if (reduce.compareTo(BigDecimal.ZERO) == 0) { + BigDecimal hours = rules.stream().map(rule -> Optional.ofNullable(rule.getDuration()).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add); + reduce = hours.compareTo(attendanceRatio) < 0 ? hours.divide(attendanceRatio, 2, RoundingMode.HALF_UP) : BigDecimal.ONE; + } + return reduce; + } + + /** + * 按日期过滤规则 + */ + private static List getRulesByDate(List rules, Date targetDate) { + return rules.stream().filter(rule -> DateUtil.isSameDay(rule.getDate(), targetDate)).collect(Collectors.toList()); + } + + /** + * 计算两个时间区间的交集小时数 + * + * @param attendanceRatio 出勤换算比 + * @param entityInterval 实体时间区间 (left=开始时间, right=结束时间) + * @param filterInterval 筛选时间区间 (left=开始时间, right=结束时间) + * @return 交集小时数,无交集返回BigDecimal.ZERO + * @throws IllegalArgumentException 参数不合法时抛出异常 + */ + public static BigDecimal calculateIntersectionHours(BigDecimal attendanceRatio, MutablePair entityInterval, MutablePair filterInterval) { + // 参数校验 + if (entityInterval == null || filterInterval == null) { + throw new IllegalArgumentException("时间区间参数不能为空"); + } + Date entityStart = entityInterval.getLeft(); + Date entityEnd = entityInterval.getRight(); + Date filterStart = filterInterval.getLeft(); + Date filterEnd = filterInterval.getRight(); + if (entityStart == null || entityEnd == null || filterStart == null || filterEnd == null) { + throw new IllegalArgumentException("时间区间中的时间不能为空"); + } + // 时间顺序校验 + if (entityStart.after(entityEnd)) { + throw new IllegalArgumentException("实体时间区间开始时间不能晚于结束时间"); + } + if (filterStart.after(filterEnd)) { + throw new IllegalArgumentException("筛选时间区间开始时间不能晚于结束时间"); + } + // 计算交集起止时间 + Date intersectionStart = entityStart.after(filterStart) ? entityStart : filterStart; + Date intersectionEnd = entityEnd.before(filterEnd) ? entityEnd : filterEnd; + // 检查是否有交集 + if (intersectionStart.after(intersectionEnd)) { + return BigDecimal.ZERO; + } + // 计算毫秒差并转换为小时 + long diffMillis = intersectionEnd.getTime() - intersectionStart.getTime(); + BigDecimal bigDecimal = new BigDecimal(diffMillis).divide(new BigDecimal(3600000), 2, RoundingMode.HALF_UP); + return bigDecimal.compareTo(attendanceRatio) > 0 ? attendanceRatio : bigDecimal; + } + + /** + * 判断班次是否旷工 + * + * @param clockInResultList 班次打卡结果信息 + * @return 是否旷工 + */ + public static Boolean isShiftsJsonAbsence1(List clockInResultList) { + boolean isAbsence = Boolean.FALSE; + if (CollUtil.isNotEmpty(clockInResultList)) { + List isAbsenceList = clockInResultList.stream().map(ShiftsJsonClockInResultVo::getAbsence).filter(absence -> absence.equals(ConstantUtil.NUM_TRUE)).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(isAbsenceList)) { + isAbsence = Boolean.TRUE; + } + } + return isAbsence; + } + + /** + * 生成日常情况横坐标 + * + * @return 日常情况横坐标 + */ + public static List createDailySituationDirectory(MonthStatsDailySituationQueryVo dailySituation) { + List teamTabVoList = new ArrayList<>(); + StatisticsEnumUtil.DailySituationEnum[] values = StatisticsEnumUtil.DailySituationEnum.values(); + for (StatisticsEnumUtil.DailySituationEnum value : values) { + MonthStatsDailySituationVo teamTabVo = new MonthStatsDailySituationVo(); + teamTabVo.setTypeName(value.getMsg()); + switch (value) { + case ZC: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getNormalDays() : BigDecimal.ZERO); + break; + case CD: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getLateDays() : BigDecimal.ZERO); + break; + case ZT: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getEarlyLeaveDays() : BigDecimal.ZERO); + break; + case WPB: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getNotSchedulingDays() : BigDecimal.ZERO); + break; + case QK: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getAbsenceCardDays() : BigDecimal.ZERO); + break; + case QQ: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getAbsenceDays() : BigDecimal.ZERO); + break; + case QJ: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getLeaveDays() : BigDecimal.ZERO); + break; + case CC: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getBusDays() : BigDecimal.ZERO); + break; + case WC: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getOutDays() : BigDecimal.ZERO); + break; + case JD: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getSelfDays() : BigDecimal.ZERO); + break; + case JB: + teamTabVo.setAvgDays(Objects.nonNull(dailySituation) ? dailySituation.getOvertimeDays() : BigDecimal.ZERO); + break; + } + teamTabVoList.add(teamTabVo); + } + return teamTabVoList; + } + + /** + * 获取用户列表 + * + * @param workStatusEnum 用户在职状态 + * @return 获取用户列表 + */ + public static List getWorkStatusEnumList(StatisticsEnumUtil.WorkStatusEnum workStatusEnum) { + //排除掉无效状态 + List workStatusEnumList = new ArrayList<>(); + if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.QB)) { + workStatusEnumList.addAll(List.of(UserWorkStatusEnums.ONBOARDING_FAILED, UserWorkStatusEnums.PRE_ONBOARDING, UserWorkStatusEnums.TRIAL, UserWorkStatusEnums.FULL_TIME, UserWorkStatusEnums.PENDING_RESIGNATION, UserWorkStatusEnums.RESIGNED)); + } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.YRZ)) { + // 入职失败要归属到预入职 + workStatusEnumList.add(UserWorkStatusEnums.ONBOARDING_FAILED); + workStatusEnumList.add(UserWorkStatusEnums.PRE_ONBOARDING); + } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.SY)) { + workStatusEnumList.add(UserWorkStatusEnums.TRIAL); + } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.ZS)) { + workStatusEnumList.add(UserWorkStatusEnums.FULL_TIME); + } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.DLZ)) { + workStatusEnumList.add(UserWorkStatusEnums.PENDING_RESIGNATION); + } else if (workStatusEnum.equals(StatisticsEnumUtil.WorkStatusEnum.LZ)) { + workStatusEnumList.add(UserWorkStatusEnums.RESIGNED); + } + return workStatusEnumList; + } + + /** + * 获取交集用户 + * + * @param userIds 用户ID集合 + * @param reqUserIds 用户ID集合 + * @return 返回交集数据ID集合 + */ + public static List getUserList(List userIds, List reqUserIds) { + if (CollUtil.isEmpty(userIds)) { + return new ArrayList<>(); + } + if (CollUtil.isNotEmpty(reqUserIds)) { + if (CollUtil.isEmpty(CollectionUtils.intersection(userIds, reqUserIds))) { + return new ArrayList<>(); + } else { + return (List) CollectionUtils.intersection(userIds, reqUserIds); + } + } else { + return userIds; + } + } + + /** + * 获取排班类型 + * + * @param attendanceType 排班类型 + * @return 排班类型 + */ + public static Integer getType(Integer attendanceType) { + if (AttendanceTypeEnum.LEAVE.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.LEAVE.getCode(); + } + if (AttendanceTypeEnum.ORDINARY.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.NORMAL.getCode(); + } + if (AttendanceTypeEnum.REST.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.REST.getCode(); + } + if (AttendanceTypeEnum.HOLIDAYS.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.ORDINARY_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.DEDUCTION_HOLIDAYS.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.EXCHANGE_HOLIDAYS.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.PAID_HOLIDAYS.getCode(); + } + if (AttendanceTypeEnum.BUSINESS_TRIP.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.BUSINESS_TRIP.getCode(); + } + if (AttendanceTypeEnum.STEP_OUT.getCode().equals(attendanceType)) { + return SchedulesTypeEnum.STEP_OUT.getCode(); + } + return null; + } + + public static void enclosureData(DayStatisticsQueryVo item, DayStatisticsShiftsJsonVo shiftsJsonVo, List clockTimeList, StringBuilder clockTimeStr) { + if (CollUtil.isNotEmpty(shiftsJsonVo.getClockInResultList())) { + if (isShiftsJsonAbsence1(shiftsJsonVo.getClockInResultList())) { + clockTimeList.add("旷工"); + clockTimeStr.append("旷工").append("|"); + } else { + StringBuilder sb = new StringBuilder(); + List onClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList()); + ShiftsJsonClockInResultVo on = CollUtil.isNotEmpty(onClockInResultList) ? onClockInResultList.get(0) : null; + List offClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList()); + ShiftsJsonClockInResultVo off = CollUtil.isNotEmpty(offClockInResultList) ? offClockInResultList.get(0) : null; + if (Objects.nonNull(on)) { + if (on.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + sb.append("缺卡").append("~"); + } else if ((on.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(on.getClockInTime()))) { + sb.append(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("~"); + } else { + sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")).append("~"); + } + } + if (Objects.nonNull(off)) { + if (off.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + sb.append("缺卡"); + } else if ((off.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(off.getClockInTime()))) { + sb.append(isSameDay(item.getDate(), shiftsJsonVo.getOutPoint()) ? DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm") : "(次日)" + DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")); + } else { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm")); + } + } + clockTimeList.add(sb.toString()); + clockTimeStr.append(sb).append("|"); + } + } + } + + public static void enclosurePayrollData(DayPayrollStatisticsQueryVo item, DayStatisticsShiftsJsonVo shiftsJsonVo, List clockTimeList, StringBuilder clockTimeStr) { + if (CollUtil.isNotEmpty(shiftsJsonVo.getClockInResultList())) { + if (isShiftsJsonAbsence1(shiftsJsonVo.getClockInResultList())) { + clockTimeList.add("旷工"); + clockTimeStr.append("旷工").append("|"); + } else { + StringBuilder sb = new StringBuilder(); + List onClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.ON_WORK)).collect(Collectors.toList()); + ShiftsJsonClockInResultVo on = CollUtil.isNotEmpty(onClockInResultList) ? onClockInResultList.get(0) : null; + List offClockInResultList = shiftsJsonVo.getClockInResultList().stream().filter(m -> m.getClockInType().equals(ConstantUtil.OFF_WORK)).collect(Collectors.toList()); + ShiftsJsonClockInResultVo off = CollUtil.isNotEmpty(offClockInResultList) ? offClockInResultList.get(0) : null; + if (Objects.nonNull(on)) { + if (on.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + sb.append("缺卡").append("~"); + } else if ((on.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(on.getClockInTime()))) { + sb.append(DateUtil.format(shiftsJsonVo.getInPoint(), "HH:mm")).append("~"); + } else { + sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")).append("~"); + } + } + if (Objects.nonNull(off)) { + if (off.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + sb.append("缺卡"); + } else if ((off.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue()) && Objects.isNull(off.getClockInTime()))) { + sb.append(isSameDay(item.getDate(), shiftsJsonVo.getOutPoint()) ? DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm") : "(次日)" + DateUtil.format(shiftsJsonVo.getOutPoint(), "HH:mm")); + } else { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm")); + } + } + clockTimeList.add(sb.toString()); + clockTimeStr.append(sb).append("|"); + } + } + } + + /** + * 封装考勤组用户月统计提醒数据 + * + * @param teamMonthStatisticsList 月统计数据 + * @return 月统计提醒数据 + */ + public static Map getTeamMonthNoticeModelMap(List teamMonthStatisticsList) { + Map teamMonthNoticeModelMap = new HashMap<>(); + for (TeamMonthStatisticsNoticeQueryVo item : teamMonthStatisticsList) { + TeamMonthNoticeModel teamMonthNoticeModel = TeamMonthNoticeModel.builder().build(); + BeanUtils.copyProperties(item, teamMonthNoticeModel); + teamMonthNoticeModelMap.put(item.getGroupId(), teamMonthNoticeModel); + } + return teamMonthNoticeModelMap; + } + + /** + * 封装日统计数据生成通知消息 + */ + public static String getClockInResultStr(boolean isOvertime, DayStatisticsNoticeQueryVo item, ShiftsJsonClockInResultVo on, ShiftsJsonClockInResultVo off) { + StringBuilder sb = new StringBuilder(); + if (Objects.nonNull(on)) { + if (on.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + sb.append("缺卡"); + } else if (on.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue())) { + sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")); + if (isOvertime) { + sb.append("(加班)-"); + } + sb.append("(正常)"); + } else if (on.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue())) { + sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")); + sb.append("(迟到)"); + } else if (on.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue())) { + sb.append(DateUtil.format(on.getClockInTime(), "HH:mm")); + sb.append("(早退)"); + } else if (on.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) { + sb.append(DateUtil.format(on.getEffectiveTime(), "HH:mm")); + sb.append("(无需打卡) "); + } + sb.append("至"); + } + if (Objects.nonNull(off)) { + if (off.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue())) { + sb.append("缺卡"); + } else if (off.getClockInStatus().equals(ClockInStatusEnum.NORMAL.getValue())) { + if (off.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + sb.append("旷工"); + } else { + if (off.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)) { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm")); + sb.append("外勤"); + } else { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm")); + sb.append("(正常) "); + } + } + } else if (off.getClockInStatus().equals(ClockInStatusEnum.WORK_LATE.getValue())) { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm")); + sb.append("(迟到)"); + } else if (off.getClockInStatus().equals(ClockInStatusEnum.HOME_EARLY.getValue())) { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getClockInTime(), "HH:mm")); + sb.append("(早退)"); + } else if (off.getClockInStatus().equals(ClockInStatusEnum.NEED_NOT.getValue())) { + sb.append(isSameDay(item.getDate(), off.getClockInTime()) ? DateUtil.format(off.getClockInTime(), "HH:mm") : "(次日)" + DateUtil.format(off.getEffectiveTime(), "HH:mm")); + sb.append("(无需打卡) "); + } + } + return sb.toString(); + } + + public static String getClockInMethod(Integer deviceType) { + if (null == deviceType) { + return ""; + } + switch (deviceType) { + case ConstantUtil.DEVICE_PLACE: + return "地点打卡"; + case ConstantUtil.DEVICE_WIFI: + return "Wi-Fi打卡"; + case ConstantUtil.DEVICE_MACHINE: + return "考勤机打卡"; + default: + return ""; + } + } + + public static ClockClassRecord clock2ClockClassRecord(ClockClassRecord vo, Map> clockInPicMap) { + vo.setPictureList(clockInPicMap.getOrDefault(vo.getClockInId(), new ArrayList<>()).stream().map(AttendanceClockInPic::getPicUrl).collect(Collectors.toList())); + return vo; + } + + /** + * 获取借调次数 + * + * @param userSecondRecordList 用户借调数据 + */ + public static Integer handleDateJsonObject(DateRangeDto dateRangeDto, List userSecondRecordList) throws Exception { + int count = 0; + for (UserSecondRecord item : userSecondRecordList) { + if (com.github.pagehelper.util.StringUtil.isNotEmpty(item.getTimeJson())) { + List dateVoList = JsonUtil.getJsonToList(item.getTimeJson(), SecondmentDateVo.class); + //判断借调时间段是否在筛选日期内 + if (CollUtil.isNotEmpty(dateVoList)) { + for (SecondmentDateVo secondmentDateVo : dateVoList) { + DateTime startDate1 = new DateTime(secondmentDateVo.getStartTime()); + DateTime endDate1 = new DateTime(secondmentDateVo.getEndTime()); + DateTime startDate2 = new DateTime(dateRangeDto.getStartDate()); + DateTime endDate2 = new DateTime(dateRangeDto.getEndDate()); + HashMap hashMap = getOverlap(startDate1, endDate1, startDate2, endDate2, false); + if (CollUtil.isNotEmpty(hashMap)) { + count += 1; + } + } + } + } + } + return count; + } + + /** + * 判断2个时间段是否有重叠(交集) + * + * @param startDate1 时间段1开始时间 + * @param endDate1 时间段1结束时间 + * @param startDate2 时间段2开始时间 + * @param endDate2 时间段2结束时间 + * @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等 + * @return ashMap key startDate endDate + */ + public static HashMap getOverlap(DateTime startDate1, DateTime endDate1, DateTime startDate2, DateTime endDate2, boolean isStrict) throws Exception { + Objects.requireNonNull(startDate1, "startDate1"); + Objects.requireNonNull(endDate1, "endDate1"); + Objects.requireNonNull(startDate2, "startDate2"); + Objects.requireNonNull(endDate2, "endDate2"); + return setOverlap(startDate1, endDate1, startDate2, endDate2, isStrict); + } + + public static HashMap setOverlap(DateTime startDate1, DateTime endDate1, DateTime startDate2, DateTime endDate2, boolean isStrict) throws Exception { + return DateConvertUtil.setOverlap(startDate1, endDate1, startDate2, endDate2, isStrict); + } + + /** + * 封装数据 + * + * @param dataTypeEnum 查询类型 + * @param queryVoList 日统计数据 + * @param dataDetailsMap 用户异常数据 + * @param boundVoMap 用户数据集合 + */ + public static List dateEncapsulationV2(StatisticsEnumUtil.TabDataTypeEnum dataTypeEnum, List queryVoList, Map> dataDetailsMap, Map boundVoMap) { + List statisticsListVoList = new ArrayList<>(); + for (MonthStatisticsQueryVo monthStatisticsVo : queryVoList) { + AppTeamStatisticsListVo statisticsListVo = new AppTeamStatisticsListVo(); + statisticsListVo.setUserId(monthStatisticsVo.getUserId()); + if (!boundVoMap.containsKey(monthStatisticsVo.getUserId())) { + continue; + } + UserBoundVO userInfoVo = boundVoMap.get(monthStatisticsVo.getUserId()); + statisticsListVo.setUserHeadSculpture(com.github.pagehelper.util.StringUtil.isNotEmpty(userInfoVo.getHeadIcon()) ? UploaderUtil.uploaderImg(userInfoVo.getHeadIcon()) : null); + statisticsListVo.setUserName(com.github.pagehelper.util.StringUtil.isNotEmpty(userInfoVo.getUserName()) ? userInfoVo.getUserName() : com.github.pagehelper.util.StringUtil.isNotEmpty(userInfoVo.getNickname()) ? userInfoVo.getNickname() : null); + statisticsListVo.setIsSecond(0); + statisticsListVo.setNumber(dataTypeEnum.getTimes(monthStatisticsVo)); + statisticsListVo.setDetalList(dataDetailsMap.getOrDefault(monthStatisticsVo.getUserId() + "&" + monthStatisticsVo.getGroupId(), new ArrayList<>())); + statisticsListVoList.add(statisticsListVo); + } + return statisticsListVoList; + } + + /** + * 根据出勤类型枚举,生成类型列表数据 + * + * @return 列表数据 + */ + public static List createTabDataDirectory() { + List teamTabVoList = new ArrayList<>(); + StatisticsEnumUtil.TabDataTypeEnum[] values = values(); + for (StatisticsEnumUtil.TabDataTypeEnum value : values) { + AppStatisticsTeamTabVo teamTabVo = new AppStatisticsTeamTabVo(); + teamTabVo.setName(value.getMsg()); + teamTabVo.setDataType(value.getCode()); + teamTabVo.setDataList(newArrayList()); + teamTabVoList.add(teamTabVo); + } + return teamTabVoList; + } + + /** + * @param allRuleIds 当天的考勤规则ids + * @param day 当天日期 判断次日使用 + * @param clockInList 打卡的所有列表 + * @param clockIns 所有的打卡结果记录 + * @return 集合 + */ + public static List setOldDataNew(List allRuleIds, Date day, List clockInList, List clockIns) { + // 过滤出打卡结果不是空的 + if (CollUtil.isNotEmpty(clockInList)) { + List collect = clockInList.stream().filter(v -> allRuleIds.contains(v.getRuleId())).collect(Collectors.toList()); + if (!collect.isEmpty()) { + List indexList = new ArrayList<>(); + collect.forEach(v -> indexList.add(clockInList.indexOf(v))); + Integer minDelIndex = null; + Integer maxDelIndex = null; + // 获取最大 和 最小的 index + int minIndex = indexList.get(0); + int maxIndex = indexList.get(indexList.size() - 1); + if (minIndex > 0) { + // 往上查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往上所有记录 + for (int i = indexList.get(0) - 1; i > 0; i--) { + ClockClassRecord vo = clockInList.get(i); + if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) { + minDelIndex = i; + break; + } + } + } + if (maxIndex < clockInList.size() - 1) { + // 往下查找不在 allRuleIds 中的 ruleId 不为空的记录 并移除往下所有记录 + for (int i = indexList.get(0) + 1; i < clockInList.size(); i++) { + ClockClassRecord vo = clockInList.get(i); + if (vo.getRuleId() != null && !allRuleIds.contains(vo.getRuleId())) { + maxDelIndex = i; + break; + } + } + } + // 排除 ruleId 在 ruleIds 中的记录 和 minDelIndex 往上的记录 和 maxDelIndex 往下的记录 + List removeList = new ArrayList<>(); + if (null != minDelIndex) { + removeList.addAll(clockInList.subList(0, minDelIndex)); + } + if (null != maxDelIndex) { + removeList.addAll(clockInList.subList(maxDelIndex, clockInList.size() - 1)); + } + clockInList.removeAll(removeList); + // 因为传入的数据都是符合本考勤组的数据,所以只用过滤空 + clockInList.removeIf(v -> null != v.getRuleId()); + } + if (!clockInList.isEmpty()) { + // 转换数据 clockInTime 格式化, 打卡方式 转换成文字 + clockInList.forEach(v -> { + String str = DateDetail.getDate2Str(day, DateDetail.DF).equals(DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF)) ? "" : "次日"; + v.setClockInTimeStr(str + DateDetail.getDate2Str(v.getClockInTime(), DateDetail.DF10)); + v.setClockInMethod(getClockInMethod(null == v.getClockInMethod() || v.getClockInMethod().isEmpty() ? ConstantUtil.DEVICE_UNKNOWN : Integer.parseInt(v.getClockInMethod()))); + }); + + // 将clockInList插入到今日出勤中[插入到两个班次时间之间] + if (clockIns.isEmpty()) { + clockIns.addAll(clockInList.stream().filter(v -> null == v.getRuleId() || allRuleIds.contains(v.getRuleId())).sorted(Comparator.comparing(ClockClassRecord::getClockInTime)).collect(Collectors.toList())); + } else { + Map> map = new HashMap<>(); + for (int i = 0; i < clockIns.size(); i++) { + ClockClassRecord v1 = clockIns.get(i); + if (Objects.isNull(v1.getWorkDate())) { + continue; + } + ClockClassRecord v2 = null; + if (i + 1 < clockIns.size()) { + v2 = clockIns.get(i + 1); + } + if (i == 0) { + // 判断小于v1的clockInList + try { + List list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().before(v1.getWorkDate())).collect(Collectors.toList()); + map.put(null, list); + } catch (Exception e) { + log.error("判断小于v1的clockInList异常:{}", e.getMessage()); + } + } + if (null != v2) { + ClockClassRecord v = v2; + // 如果v2不为空, 判断哪些clockInList在v1, v2班次之间 + List list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate()) && clockIn.getClockInTime().before(v.getWorkDate())).collect(Collectors.toList()); + map.putIfAbsent(v1, list); + } else { + // 如果v2为空, 判断哪些clockInList大于v1班次 + List list = clockInList.stream().filter(clockIn -> clockIn.getClockInTime().after(v1.getWorkDate())).collect(Collectors.toList()); + map.putIfAbsent(v1, list); + + } + } + map.keySet().forEach(key -> { + List list = map.get(key).stream().sorted(Comparator.comparing(ClockClassRecord::getClockInTime)).collect(Collectors.toList()); + if (key == null) { + clockIns.addAll(0, list); + } else { + int index = clockIns.indexOf(key); + if (index + 1 > clockIns.size() - 1) { + clockIns.addAll(list); + } else { + clockIns.addAll(index + 1, list); + } + } + }); + } + } + } + return clockIns; + } + + public static void setClockTime(String groupId, Map keyMap, String userId, String day, List dailyClockInVos, SimpleDateFormat ymd, SimpleDateFormat hms) { + // 打卡集合 + if (null != dailyClockInVos && !dailyClockInVos.isEmpty()) { + StringBuilder clockTime = new StringBuilder(); + StringBuilder otherClockTime = new StringBuilder(); + List oldRuleIds = new ArrayList<>(); + List oldClockRuleIds = new ArrayList<>(); + // 当天旷工 将打卡信息的userId,date,groupId组合为key + ClockInExportVo clockInExportVo = keyMap.get(userId + "-" + day + "-" + groupId); + int index = 0; + int count = Integer.parseInt(dailyClockInVos.stream().filter(m -> m.getIsEffectiveClock() == 1).count() + ""); + if (null != clockInExportVo) { + if (null != clockInExportVo.getClockTime()) { + clockTime.append(clockInExportVo.getClockTime()); + } + boolean last = false; + for (int i = 0; i < dailyClockInVos.size(); i++) { + ClockClassRecord clockClassRecord = dailyClockInVos.get(i); + if (1 == clockClassRecord.getIsEffectiveClock()) { + index++; + if (1 == clockClassRecord.getAbsence()) { + if (!oldRuleIds.contains(clockClassRecord.getRuleId())) { + last = true; + oldRuleIds.add(clockClassRecord.getRuleId()); + if (i < 1) { + clockTime.append("旷工"); + } else { + // 旷工 ==》会有 旷工|旷工 + clockTime.append(" | 旷工"); + } + } + } else { + // 打卡去重 + if (last) { + clockTime.append(" | "); + } + last = false; + // 组装 09:01-14:01 | 17:55-次日08:05 + if (null != clockClassRecord.getClockInStatus()) { + + if (!oldRuleIds.contains(clockClassRecord.getRuleId())) { + + if (!oldClockRuleIds.contains(clockClassRecord.getRuleId())) { + oldClockRuleIds.add(clockClassRecord.getRuleId()); + // 该组出勤记录第一次进入 + if (-1 == clockClassRecord.getClockInStatus()) { + if (!oldRuleIds.contains(clockClassRecord.getRuleId())) { + // 没被记录为旷工时打缺卡 + clockTime.append("缺卡"); + addSplicing(index, count, clockTime); + } + } else if (-2 == clockClassRecord.getClockInStatus()) { + clockTime.append("无需打卡"); + addSplicing(index, count, clockTime); + } else { + if (null != clockClassRecord.getClockInTime()) { + // 比对是否在次日 + if (!day.equals(ymd.format(clockClassRecord.getClockInTime()))) { + clockTime.append("次日"); + } + clockTime.append(hms.format(clockClassRecord.getClockInTime())); + } + addSplicing(index, count, clockTime); + } + } else { + if (-2 == clockClassRecord.getClockInStatus()) { + clockTime.append("无需打卡"); + addSplicing(index, count, clockTime); + } else { + // 不是第一次进入 + if (-1 == clockClassRecord.getClockInStatus()) { + clockTime.append("缺卡"); + addSplicing(index, count, clockTime); + } else { + if (null != clockClassRecord.getClockInTime()) { + // 比对是否在次日 + if (!day.equals(ymd.format(clockClassRecord.getClockInTime()))) { + clockTime.append("次日"); + } + clockTime.append(hms.format(clockClassRecord.getClockInTime())); + } + addSplicing(index, count, clockTime); + } + } + } + } + } else { + // 其他打卡 + // 比对是否在次日 + if (null != clockClassRecord.getClockInTime()) { + if (!day.equals(ymd.format(clockClassRecord.getClockInTime()))) { + otherClockTime.append("次日"); + } + otherClockTime.append(hms.format(clockClassRecord.getClockInTime())).append(" "); + } + } + } + } else { + if (null != clockClassRecord.getClockInTime()) { + // 其他打卡 + otherClockTime.append(hms.format(clockClassRecord.getClockInTime())).append(" "); + } + } + } + clockInExportVo.setClockTime(clockTime.toString()); + clockInExportVo.setOtherClockTime(otherClockTime.toString()); + } + // 暂时不管没有命中情况 + } + } + + /** + * 添加拼接符 + * + * @param i 当前次数从0开始 + * @param size 总个数 + * @param clockTime 结果 + */ + public static void addSplicing(int i, int size, StringBuilder clockTime) { + if (i % 2 != 0) { + clockTime.append("-"); + } else { + if (i != size) { + clockTime.append(" | "); + } + } + } + + public static void checkOrdinary(List dayList, RemarkVo remarkVo) { + // 假设当天是班次时间1 9:00 - 18:00 请假12:00 - 14:00 会生成本组出勤规则==》1. 9-12 普班 班次1 班次时间1 9:00 - 18:00 2.12-14 假 3. 14-18 普班 班次1 班次时间1 9:00 - 18:00 当为这个时,需要拼接为 9:00-12:00|14:00-18:00(12:00-14:00请假) + // 假设当天是班次时间1 9:00 - 18:00 请假9:00 - 12:00 会生成本组出勤规则==》1. 9-12 假 2. 12-18 普班 班次1 班次时间1 9:00 - 18:00 当为这个时,需要拼接为 12:00-18:00(9:00-12:00请假) + // 假设当天是班次时间1 9:00 - 18:00 借调9:00 - 14:00 会生成本组出勤规则==》1. (借调回岗时间)14-18 普班 班次1 班次时间1 9:00 - 18:00 当为这个时,需要拼接为 14:00-18:00 + // 过滤出普班和加班的出勤规则,当加班时对比时间全部加入,当普班时时间相同只加一个 + List shiftList = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), dayRule.getAttendanceType())).collect(Collectors.toList()).stream().sorted(Comparator.comparing(UserRuleListVo::getInPoint)).collect(Collectors.toList()); + + if (!shiftList.isEmpty()) { + SimpleDateFormat formatter = new SimpleDateFormat("HH:mm"); + // 班次日期 + String day = dayList.get(0).getDay(); + SimpleDateFormat ymd = new SimpleDateFormat("yyyy-MM-dd"); + // 9:00-12:00 | 14:00-18:00 + for (int i = 0; i < shiftList.size(); i++) { + UserRuleListVo vo = shiftList.get(i); + + if (i == 0) { + if (!day.equals(ymd.format(vo.getInPoint()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-"); + if (!day.equals(ymd.format(vo.getOutPoint()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(vo.getOutPoint())); + } else { + remarkVo.getShiftPeriod().append(" | "); + if (!day.equals(ymd.format(vo.getInPoint()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-"); + if (!day.equals(ymd.format(vo.getOutPoint()))) { + remarkVo.getShiftPeriod().append("次日"); + } + remarkVo.getShiftPeriod().append(formatter.format(vo.getOutPoint())); + } + } + } + } + + public static void checkLeave(List dayList, RemarkVo remarkVo) { + List leave = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.LEAVE.getCode(), dayRule.getAttendanceType())).collect(Collectors.toList()).stream().sorted(Comparator.comparing(UserRuleListVo::getInPoint)).collect(Collectors.toList()); + SimpleDateFormat formatter = new SimpleDateFormat("HH:mm"); + // 组装请假 + if (!leave.isEmpty()) { + // 如果当天全是请假,那么通过请假命中的时段组装当天上班时段 + if (remarkVo.getShiftPeriod().toString().isEmpty()) { + for (int i = 0; i < leave.size(); i++) { + UserRuleListVo vo = leave.get(i); + if (i == 0) { + remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint())); + } else { + remarkVo.getShiftPeriod().append(" | ").append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint())); + } + } + } + + int size = (int) dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.ORDINARY.getCode(), dayRule.getAttendanceType()) || Objects.equals(AttendanceTypeEnum.WORKOVERTIME.getCode(), dayRule.getAttendanceType())).count(); + boolean b = size == 0; + // 当天没有普班 + List leaveBoolean = new ArrayList<>(); + // 组装请假时段 09:00-18:00(09:00-10:00请假)09:00-18:00(当天请假)09:00-18:00(上半天请假) + leave.forEach(v -> { + if (null != v.getApplyUnit()) { + boolean s = false; + switch (v.getApplyUnit()) { + // 申请单位(1小时 2日 3半日) + case 1: + remarkVo.getShiftPeriod().append("(").append(formatter.format(v.getInPoint())).append("-").append(formatter.format(v.getOutPoint())).append("请假)"); + s = true; + break; + case 2: + remarkVo.getShiftPeriod().append("(当天请假 )"); + remarkVo.getClockTime().append("无需打卡"); + break; + case 3: + // 上半天 下半天 班次类型 0全天班次 1上午班次 2下午班次 + remarkVo.getShiftPeriod().append("(").append(0 == v.getSchedulesType() ? "当天请假)" : 1 == v.getSchedulesType() ? "上半天请假)" : "下半天请假)"); + if (0 == v.getSchedulesType()) { + remarkVo.getClockTime().append("无需打卡"); + } + default: + break; + } + leaveBoolean.add(s); + } + }); + if (b && leaveBoolean.stream().allMatch(Boolean::booleanValue)) { + // 仅小时假使用 + remarkVo.getClockTime().append("无需打卡"); + } + } + } + + /** + * 判断给定的开始时间和结束时间是否跨月 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 如果跨月则返回true,否则返回false + */ + public static boolean isCrossMonth(LocalDateTime startDate, LocalDateTime endDate) { + return !startDate.getMonth().equals(endDate.getMonth()); + } + + /** + * 处理借调记录(json对象) + * + * @param timeAllList 借调记录集合 + * @param userSecondRecordList 借调记录集合 + */ + public static void handleDateJsonObject(DateRangeDto dateRangeDto, List timeAllList, List userSecondRecordList) throws Exception { + if (CollUtil.isNotEmpty(userSecondRecordList)) { + for (UserSecondRecord item : userSecondRecordList) { + if (StringUtil.isNotEmpty(item.getTimeJson())) { + String dbGroupUserTimeJson = item.getTimeJson(); + List dateVoList = JsonUtil.getJsonToList(dbGroupUserTimeJson, SecondmentDateVo.class); + if (CollUtil.isNotEmpty(dateVoList)) { + dateVoList = dateVoList.stream().filter(distinctByKey(item1 -> item1.getStartTime() + "_" + item1.getEndTime())).collect(Collectors.toList()); + } + //判断借调时间段是否在筛选日期内 + if (CollUtil.isNotEmpty(dateVoList)) { + for (SecondmentDateVo secondmentDateVo : dateVoList) { + DateTime startDate1 = new DateTime(secondmentDateVo.getStartTime()); + DateTime endDate1 = new DateTime(secondmentDateVo.getEndTime()); + DateTime startDate2 = new DateTime(dateRangeDto.getStartDate()); + DateTime endDate2 = new DateTime(dateRangeDto.getEndDate()); + HashMap hashMap = getOverlap(startDate1, endDate1, startDate2, endDate2, false); + if (CollUtil.isNotEmpty(hashMap)) { + TimeJson json = new TimeJson(); + json.setApprovalId(secondmentDateVo.getApprovalId()); + json.setStartTime(secondmentDateVo.getStartTime()); + json.setEndTime(secondmentDateVo.getEndTime()); + json.setGroupId(item.getGroupId()); + timeAllList.add(json); + } + } + } + } + } + } + } + + /** + * 根据指定键去重 + */ + private static Predicate distinctByKey(Function keyExtractor) { + Set seen = new HashSet<>(); + return t -> seen.add(keyExtractor.apply(t)); + } + + /** + * 获取两个时间范围的交集 + * + * @param start1 时间1的开始时间 + * @param end1 时间1的结束时间 + * @param start2 时间2的开始时间 + * @param end2 时间2的结束时间 + * @return 时间范围的交集 + */ + public static DateRangeDto1 getIntersection(LocalDateTime start1, LocalDateTime end1, LocalDateTime start2, LocalDateTime end2) { + // 首先,确保每个时间范围的开始时间早于或等于结束时间 + if (start1.isAfter(end1) || start2.isAfter(end2)) { + throw new IllegalArgumentException("时间范围无效:开始时间不能晚于结束时间"); + } + // 确定交集的起始时间和结束时间 + LocalDateTime intersectionStart = start1.isAfter(start2) ? start1 : start2; + LocalDateTime intersectionEnd = end1.isBefore(end2) ? end1 : end2; + // 检查是否有交集 + if (intersectionStart.isAfter(intersectionEnd)) { + // 没有交集,返回null或空数组(这里返回空数组) + return null; + } else { + // 有交集,返回交集的开始和结束时间 + return new DateRangeDto1(intersectionStart, intersectionEnd); + } + } + + /** + * 判断班次是否旷工 + */ + public static Boolean isAbsence(List clockInResultList) { + boolean isAbsence = Boolean.FALSE; + if (CollUtil.isNotEmpty(clockInResultList)) { + //判断是否旷工 + List isAbsenceList = clockInResultList.stream().filter(m -> m.getDeleteMark().equals(ConstantUtil.NUM_FALSE) && m.getAbsence().equals(ConstantUtil.NUM_TRUE)).map(ClockInResult::getAbsence).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(isAbsenceList)) { + isAbsence = Boolean.TRUE; + } + } + return isAbsence; + } + + /** + * 是否手动变更为旷工,并且班次没有缺卡 + */ + public static Boolean isHeadAbsence(List clockInResultList) { + boolean isAbsence = Boolean.FALSE; + if (CollUtil.isNotEmpty(clockInResultList)) { + //判断是否旷工 + List isAbsenceList = clockInResultList.stream().filter(m -> m.getDeleteMark().equals(ConstantUtil.NUM_FALSE) && m.getAbsence().equals(ConstantUtil.NUM_TRUE)).map(ClockInResultRecordVo::getAbsence).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(isAbsenceList)) { + isAbsence = Boolean.TRUE; + } + } + return isAbsence; + } + + /** + * 获取每天的出勤状态 + * + * @param dayStatisticsQueryVos 日统计数据 + * @return List + */ + public static List getClockStatus(List dayStatisticsQueryVos) { + List clockStatus = new ArrayList<>(); + if (CollUtil.isNotEmpty(dayStatisticsQueryVos)) { + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getLateTimes() != null && item.getLateTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.CD.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getAbsenceCardTimes() != null && item.getAbsenceCardTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.QK.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getEarlyLeaveTimes() != null && item.getEarlyLeaveTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.ZT.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getAbsenceTimes() != null && item.getAbsenceTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.KG.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getOutworkTimes() != null && item.getOutworkTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.WQ.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getOvertimeTimes() != null && item.getOvertimeTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.JB.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getSelfGroup() != null && item.getSelfGroup().equals(ConstantUtil.NUM_TRUE))) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.JD.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getLeaveBatchNumber() != null && StringUtil.isNotEmpty(item.getLeaveBatchNumber()))) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.QJ.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getBusDays() != null && item.getBusDays().compareTo(BigDecimal.ZERO) > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.CC.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> (item.getOutDays() != null && item.getOutDays().compareTo(BigDecimal.ZERO) > 0) || (item.getOutHours() != null && item.getOutHours().compareTo(BigDecimal.ZERO) > 0))) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.WC.getCode()); + } + if (dayStatisticsQueryVos.stream().anyMatch(item -> item.getRestTimes() != null && item.getRestTimes() > 0)) { + clockStatus.add(StatisticsEnumUtil.StatisticsTypeEnum.XX.getCode()); + } + } + return clockStatus; + } + + /** + * 处理出差 + * + * @param dayList 每日出勤规则 + * @param remarkVo 组装信息 + */ + public static void checkBusinessTrip(List dayList, RemarkVo remarkVo) { + List businessTrip = dayList.stream().filter(dayRule -> Objects.equals(AttendanceTypeEnum.BUSINESS_TRIP.getCode(), dayRule.getApplyViewEnable())).collect(Collectors.toList()).stream().sorted(Comparator.comparing(UserRuleListVo::getInPoint)).collect(Collectors.toList()); + // 组装请假 + if (!businessTrip.isEmpty()) { + // 如果当天全是外出,那么通过外出命中的时段组装当天上班时段 + if (remarkVo.getShiftPeriod().toString().isEmpty()) { + SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss"); + for (int i = 0; i < businessTrip.size(); i++) { + UserRuleListVo vo = businessTrip.get(i); + if (i == 0) { + remarkVo.getShiftPeriod().append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint())); + } else { + remarkVo.getShiftPeriod().append(" | ").append(formatter.format(vo.getInPoint())).append("-").append(formatter.format(vo.getOutPoint())); + } + } + + } + // 组装请假时段 09:00-18:00(当天出差) + remarkVo.getShiftPeriod().append("(当天出差)"); + } + } + + //按照用户名拼音首字母排序 + public static List getCollect(List clockUserList) { + return clockUserList.stream().sorted(Comparator.comparing(ClockUser::getIdentifying)).collect(Collectors.toList()); + } + + /** + * 将列表按照指定大小进行分割 + * + * @param list 需要分割的列表 + * @param size 每个子列表的大小 + * @param 列表元素的类型 + * @return 分割后的子列表集合 + */ + public static List> partition(List list, int size) { + return IntStream.iterate(0, i -> i < list.size(), i -> i + size).mapToObj(i -> list.subList(i, Math.min(i + size, list.size()))).collect(Collectors.toList()); + } + + /** + * 将 DayStatisticsPageListVo 对象的属性字段名和属性值解析成 Map + * + * @param dayStaVo DayStatisticsPageListVo 对象 + * @param aTrue 是否日统计 + * @return 字段名和属性值的映射关系 + */ + @SneakyThrows + public static Map getObjectFieldMap(T dayStaVo, Boolean aTrue) { + Map fieldMap = new HashMap<>(); + Class clazz = dayStaVo.getClass(); + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + if ("customLeaveList".equals(field.getName()) && Objects.nonNull(field.get(dayStaVo))) { + Object fieldValue = field.get(dayStaVo); + List customLeaveList = new ArrayList<>(); + if (fieldValue instanceof List) { + ObjectMapper mapper = new ObjectMapper(); + List rawList = (List) fieldValue; + for (Object item : rawList) { + if (item instanceof LeaveTypeJsonData) { + customLeaveList.add((LeaveTypeJsonData) item); + } else { + // 如果是Map类型,可以转换 + LeaveTypeJsonData data = mapper.convertValue(item, LeaveTypeJsonData.class); + customLeaveList.add(data); + } + } + } + if (CollUtil.isNotEmpty(customLeaveList)) { + customLeaveList.forEach(item -> { + fieldMap.put(item.getField(), aTrue ? item.getLeaveHours() : item.getLeaveDays()); + }); + } + } + ExcelProperty annotation = field.getAnnotation(ExcelProperty.class); + // 排除掉集合字段 + if (!Collection.class.isAssignableFrom(field.getType()) && annotation != null) { + if (String.class.isAssignableFrom(field.getType())) { + fieldMap.put(field.getName(), Objects.nonNull(field.get(dayStaVo)) ? field.get(dayStaVo).toString().replaceAll("~", "-") : ""); + } else { + fieldMap.put(field.getName(), field.get(dayStaVo)); + } + } + } + + return fieldMap; + } + + /** + * 获取日统计导出数据列表 + * + * @param dayStaVo 查询参数 + * @param leaveHeadersMap 请假头 + * @return 数据列表 + */ + public static Map getDayStaDataList(DayStatisticsPageListVo dayStaVo, LinkedHashMap leaveHeadersMap) { + Map headerFieldFixMap = new HashMap<>(); + headerFieldFixMap.put("workStatus", "workStatusStr"); + headerFieldFixMap.put("shiftList", "shiftStr"); + headerFieldFixMap.put("shiftTimeList", "shiftTimeStr"); + headerFieldFixMap.put("clockTimeList", "clockTimeStr"); + Map headerFieldMap = getObjectFieldMap(dayStaVo, Boolean.TRUE); + if (CollUtil.isNotEmpty(dayStaVo.getCustomLeaveList())) { + List customLeaveList = dayStaVo.getCustomLeaveList(); + customLeaveList.forEach(jsonData -> { + headerFieldMap.put(jsonData.getField(), Objects.isNull(jsonData.getLeaveHours()) ? "0" : jsonData.getLeaveHours()); + }); + } + AtomicInteger atomicInteger = new AtomicInteger(0); + Map dataList = new HashMap<>(); + leaveHeadersMap.forEach((key, value) -> { + Object orDefault = headerFieldMap.getOrDefault(headerFieldFixMap.getOrDefault(key, key), headerFieldFixMap.containsKey(key) ? "" : 0); + dataList.put(atomicInteger.getAndIncrement(), orDefault); + }); + return dataList; + } + + /** + * 获取月统计导出数据列表 + * + * @param monthStaVo 查询参数 + * @param leaveHeadersMap 请假头 + * @return 数据列表 + */ + public static Map getMonthStaDataList(MonthStatisticsPageListVo monthStaVo, Map leaveHeadersMap) { + Map headerFieldFixMap = new HashMap<>(); + headerFieldFixMap.put("workStatus", "workStatusStr"); + headerFieldFixMap.put("cycleList", "cycleStr"); + Map headerFieldMap = getObjectFieldMap(monthStaVo, Boolean.TRUE); + if (CollUtil.isNotEmpty(monthStaVo.getCustomLeaveList())) { + List customLeaveList = monthStaVo.getCustomLeaveList(); + customLeaveList.forEach(jsonData -> { + headerFieldMap.put(jsonData.getField(), Objects.isNull(jsonData.getLeaveDays()) ? "0" : jsonData.getLeaveDays()); + }); + } + AtomicInteger atomicInteger = new AtomicInteger(0); + Map dataList = new HashMap<>(); + leaveHeadersMap.forEach((key, value) -> { + Object orDefault = headerFieldMap.getOrDefault(headerFieldFixMap.getOrDefault(key, key), headerFieldFixMap.containsKey(key) ? "" : 0); + dataList.put(atomicInteger.getAndIncrement(), orDefault); + }); + return dataList; + } + + /** + * 获取日统计自定义请假数据 + * + * @param customLeaveList 自定义请假数据 + * @return 自定义请假数据 + */ + public static List getDayCustomLeaveList(List> customLeaveList) { + if (CollUtil.isEmpty(customLeaveList)) { + return new ArrayList<>(); + } + ObjectMapper mapper = new ObjectMapper(); + List customLeaveJsonList = customLeaveList.stream().map(item -> mapper.convertValue(item, LeaveTypeJsonData.class)).collect(Collectors.toList()); + Map> customLeaveMap = customLeaveJsonList.stream().collect(Collectors.groupingBy(LeaveTypeJsonData::getField)); + List typeJsonDataArrayList = new ArrayList<>(customLeaveMap.size()); + customLeaveMap.forEach((field, customizeTableVo) -> { + if (CollUtil.isEmpty(customizeTableVo)) { + return; + } + LeaveTypeJsonData firstItem = customizeTableVo.get(0); + BigDecimal totalLeaveDays = customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveDays).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal totalLeaveHours = customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal totalLeavePayrollHours = customizeTableVo.stream().map(LeaveTypeJsonData::getLeavePayrollHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + LeaveTypeJsonData builder = LeaveTypeJsonData.builder() + .field(firstItem.getField()) + .fieldName(firstItem.getFieldName()) + .headName(firstItem.getHeadName()) + .leaveDays(totalLeaveDays) + .leaveHours(totalLeaveHours) + .leavePayrollHours(totalLeavePayrollHours) + .applyList(firstItem.getApplyList()) + .build(); + typeJsonDataArrayList.add(builder); + }); + return typeJsonDataArrayList; + } + + /** + * 获取日统计自定义数据 + * + * @param dayMinutesList 自定义数据 + * @return 自定义数据 + */ + public static List getCustomDataList(List>> dayMinutesList, Class clazz) { + List result = new ArrayList<>(); + if (CollUtil.isEmpty(dayMinutesList)) { + return result; + } + for (List> item : dayMinutesList) { + item.forEach(linkedHashMap -> { + ObjectMapper mapper = new ObjectMapper(); + T data = mapper.convertValue(linkedHashMap, clazz); + result.add(data); + }); + } + return result; + } + + /** + * 获取月统计自定义请假数据 + * + * @param customLeaveList 自定义请假数据 + * @return 自定义请假数据 + */ + public static List getMonthCustomLeaveList(List> customLeaveList) { + if (CollUtil.isEmpty(customLeaveList)) { + return new ArrayList<>(); + } + List customLeaveJsonList = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + for (LinkedHashMap item : customLeaveList) { + // 移除不需要的字段,避免影响原始数据 + LinkedHashMap filteredItem = new LinkedHashMap<>(item); + // 转换为目标对象 + LeaveTypeJsonData data = mapper.convertValue(filteredItem, LeaveTypeJsonData.class); + customLeaveJsonList.add(data); + } + List typeJsonDataArrayList = new ArrayList<>(); + Map> customLeaveMap = customLeaveJsonList.stream().collect(Collectors.groupingBy(LeaveTypeJsonData::getField)); + customLeaveMap.forEach((field, customizeTableVo) -> { + LeaveTypeJsonData builder = LeaveTypeJsonData.builder() + .field(customizeTableVo.get(0).getField()) + .fieldName(customizeTableVo.get(0).getFieldName()) + .headName(customizeTableVo.get(0).getHeadName()) + .leaveDays(customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveDays).reduce(BigDecimal.ZERO, BigDecimal::add)) + .leaveHours(customizeTableVo.stream().map(LeaveTypeJsonData::getLeaveHours).reduce(BigDecimal.ZERO, BigDecimal::add)) + .leavePayrollHours(customizeTableVo.stream().map(LeaveTypeJsonData::getLeavePayrollHours).reduce(BigDecimal.ZERO, BigDecimal::add)) + .applyList(customizeTableVo.stream().flatMap(item -> item.getApplyList().stream()).collect(Collectors.toList())) + .build(); + typeJsonDataArrayList.add(builder); + }); + return typeJsonDataArrayList; + } + + /** + * 获取日范围 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param formatter 时间格式 + */ + public static String formatDayRange(Date startDate, Date endDate, DateTimeFormatter formatter) { + return DateUtil.format(startDate, formatter) + "至" + DateUtil.format(endDate, formatter); + } + + /** + * 处理打卡结果 + * + * @param results 打卡结果 + * @param userRuleRecord 用户规则 + * @param overtimeRecord 加班记录 + */ + public static void processClockInResults(List results, UserRuleRecord userRuleRecord, OvertimeRecord overtimeRecord) { + Date startTime = null, endTime = null; + if (results.size() == 2) { + for (ClockInResultRecordVo result : results) { + if (Objects.equals(result.getClockInType(), ConstantUtil.ON_WORK)) { + overtimeRecord.setOnIsAbsenceCard(isAbsenceCard(result)); + startTime = getEffectiveTime(result, userRuleRecord.getInPoint()); + } else { + overtimeRecord.setOffIsAbsenceCard(isAbsenceCard(result)); + endTime = getEffectiveTime(result, userRuleRecord.getOutPoint()); + } + } + } else if (results.size() == 1) { + ClockInResultRecordVo result = results.get(0); + boolean isOnWork = Objects.equals(result.getClockInType(), ConstantUtil.ON_WORK); + if (isOnWork) { + overtimeRecord.setOnIsAbsenceCard(isAbsenceCard(result)); + startTime = getEffectiveTime(result, userRuleRecord.getInPoint()); + endTime = userRuleRecord.getOutPoint(); + overtimeRecord.setOffIsAbsenceCard(1); + } else { + overtimeRecord.setOnIsAbsenceCard(1); + startTime = userRuleRecord.getInPoint(); + endTime = getEffectiveTime(result, userRuleRecord.getOutPoint()); + overtimeRecord.setOffIsAbsenceCard(isAbsenceCard(result)); + } + } + overtimeRecord.setOvertimeDate(userRuleRecord.getDay()); + overtimeRecord.setStartTime(startTime); + overtimeRecord.setEndTime(endTime); + if (overtimeRecord.getOnIsAbsenceCard().equals(ConstantUtil.NUM_FALSE) && overtimeRecord.getOffIsAbsenceCard().equals(ConstantUtil.NUM_FALSE)) { + overtimeRecord.setOvertimeDuration(DateConvertUtil.dateConvert(Objects.requireNonNull(startTime), Objects.requireNonNull(endTime), 2, 2, null)); + } + } + + public static void handleEmptyClockInResults(UserRuleRecord userRuleRecord, OvertimeRecord overtimeRecord) { + overtimeRecord.setOvertimeDate(userRuleRecord.getDay()); + overtimeRecord.setStartTime(userRuleRecord.getInPoint()); + overtimeRecord.setEndTime(userRuleRecord.getOutPoint()); + overtimeRecord.setOnIsAbsenceCard(1); + overtimeRecord.setOffIsAbsenceCard(1); + } + + /** + * 判断是否为补卡 + * + * @param result 打卡结果 + * @return 是否为补卡 + */ + private static Integer isAbsenceCard(ClockInResultRecordVo result) { + return result.getClockInStatus() != null && result.getClockInStatus().equals(ClockInStatusEnum.NO_CLOCK.getValue()) ? 1 : 0; + } + + /** + * 获取有效的时间 + * + * @param result 打卡结果 + * @param defaultTime 默认时间 + * @return 有效时间 + */ + public static Date getEffectiveTime(ClockInResultRecordVo result, Date defaultTime) { + return Objects.nonNull(result.getEffectiveTime()) ? result.getEffectiveTime() : defaultTime; + } + + /** + * 安全地执行加法运算 + * + * @param a 数字1 + * @param b 数字2 + * @return 结果 + */ + public static BigDecimal safeAdd(BigDecimal a, BigDecimal b) { + if (a == null) { + a = BigDecimal.ZERO; + } + if (b == null) { + b = BigDecimal.ZERO; + } + return a.add(b); + } + + /** + * 获取默认值 + * + * @param value 值 + * @param defaultValue 默认值 + * @return 默认值 + */ + public static int defaultIfNull(T value, int defaultValue) { + return value == null ? defaultValue : value.intValue(); + } + + /** + * 计算字符串中逗号分隔的元素个数 + * + * @param str 输入字符串 + * @return 元素个数 + */ + public static int countSplitElements(String str) { + if (StrUtil.isBlank(str)) { + return 0; + } + String trimmed = str.trim(); + if (trimmed.isEmpty()) { + return 0; + } + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + List shiftsJsonVoList = parseArray(trimmed, BatchNumberResult.class); + return Math.toIntExact(shiftsJsonVoList.stream().map(BatchNumberResult::getBatchNumberId).distinct().count()); + } else { + return trimmed.split(",").length; + } + } + + /** + * 获取加班数据 + * + * @param attendanceRatio1 出勤换算比 + * @param dataList 数据列表 + * @return 处理结果 + */ + private static Map processOvertime(BigDecimal attendanceRatio1, List dataList) { + Map resultMap = new HashMap<>(); + Arrays.stream(OvertimeType.values()).forEach(type -> resultMap.put(type, new ProcessResult())); + try { + List overtimeList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.JB); + for (UserDaySituationData result : overtimeList) { + if (CollUtil.isEmpty(result.getClockInResultList())) { + continue; + } + List clockInResults = getClockInResultFilterList(result.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.JB); + if (CollUtil.isEmpty(clockInResults) || clockInResults.size() != 2) { + continue; + } + // 排除加班未打卡的记录 + boolean hasNoClock = clockInResults.stream().map(ClockInResult::getClockInStatus).collect(Collectors.toList()).contains(ClockInStatusEnum.NO_CLOCK.getValue()); + if (hasNoClock) { + continue; + } + ClockInResult on = clockInResults.stream().filter(m -> ConstantUtil.ON_WORK.equals(m.getClockInType())).findFirst().orElse(null); + ClockInResult off = clockInResults.stream().filter(m -> ConstantUtil.OFF_WORK.equals(m.getClockInType())).findFirst().orElse(null); + // 得到加班时长 + if (on != null && off != null) { + BigDecimal hours = DateConvertUtil.dateConvert(on.getEffectiveTime(), off.getEffectiveTime(), 2, 2, null); + if (StrUtil.isBlank(result.getPeriodInfo())) { + continue; + } + OvertimeRuleDetailVo overtimeRuleDetailVo = JSONObject.parseObject(result.getPeriodInfo(), OvertimeRuleDetailVo.class); + if (overtimeRuleDetailVo.getOvertimeType().equals(OvertimeType.WORK_DAY.getValue())) { + ProcessResult workDayInfo = resultMap.get(OvertimeType.WORK_DAY); + workDayInfo.setCount(workDayInfo.getCount() + 1); + workDayInfo.setHours(workDayInfo.getHours().add(hours)); + workDayInfo.setDays(workDayInfo.getDays().add(hours.divide(attendanceRatio1, 2, RoundingMode.HALF_UP))); + } + if (overtimeRuleDetailVo.getOvertimeType().equals(OvertimeType.REST_DAY.getValue())) { + ProcessResult restDayInfo = resultMap.get(OvertimeType.REST_DAY); + restDayInfo.setCount(restDayInfo.getCount() + 1); + restDayInfo.setHours(restDayInfo.getHours().add(hours)); + restDayInfo.setDays(restDayInfo.getDays().add(hours.divide(attendanceRatio1, 2, RoundingMode.HALF_UP))); + } + if (overtimeRuleDetailVo.getOvertimeType().equals(OvertimeType.HOLIDAY.getValue())) { + ProcessResult holidayInfo = resultMap.get(OvertimeType.HOLIDAY); + holidayInfo.setCount(holidayInfo.getCount() + 1); + holidayInfo.setHours(holidayInfo.getHours().add(hours)); + holidayInfo.setDays(holidayInfo.getDays().add(hours.divide(attendanceRatio1, 2, RoundingMode.HALF_UP))); + } + } + } + return resultMap; + } catch (Exception e) { + log.error("获取加班数据时发生异常", e); + return resultMap; + } + } + + /** + * 获取外勤数据 + * + * @param dataList 数据列表 + * @param schedulesTypeMap 排班类型数据 + * @return 外勤数据 + */ + private static ProcessResult processOutworkClock(List dataList, Map>> schedulesTypeMap) { + ProcessResult outworkResult = new ProcessResult(); + try { + List outworkList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.WQ); + for (UserDaySituationData itemRule : outworkList) { + if (CollUtil.isEmpty(itemRule.getClockInResultList())) { + continue; + } + if (isAbsence(itemRule.getClockInResultList())) { + continue; + } + List clockInResults = getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.WQ); + if (CollUtil.isEmpty(clockInResults)) { + continue; + } + outworkResult.setCount(outworkResult.getCount() + clockInResults.size()); + // 只有普通班才会计算小时数和天数 + if (itemRule.getAttendanceType().equals(AttendanceTypeEnum.WORKOVERTIME.getCode())) { + continue; + } + boolean isAllOutwork = clockInResults.stream().allMatch(m -> m.getClockInKind().equals(ConstantUtil.KIND_OUTSIDE)); + if (!isAllOutwork) { + continue; + } + // 根据实际打卡记录计算小时数和天数 + Date actualAttendStartTime = null; + Date actualAttendEndTime = null; + Integer restMinute = 0; + for (ClockInResult result : clockInResults) { + if (ConstantUtil.ON_WORK.equals(result.getClockInType())) { + if (Objects.nonNull(itemRule.getInPoint())) { + actualAttendStartTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().before(itemRule.getInPoint()) ? itemRule.getInPoint() : result.getClockInTime()) : result.getEffectiveTime(); + } + } else if (ConstantUtil.OFF_WORK.equals(result.getClockInType())) { + if (Objects.nonNull(itemRule.getOutPoint())) { + actualAttendEndTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().after(itemRule.getOutPoint()) ? itemRule.getOutPoint() : result.getClockInTime()) : result.getEffectiveTime(); + } + restMinute = result.getRestMinute(); + } + } + if (actualAttendStartTime != null && actualAttendEndTime != null) { + // 减去休息分钟数 + BigDecimal minute = DateConvertUtil.dateConvert(actualAttendStartTime, actualAttendEndTime, 1, 2, null).subtract(new BigDecimal(restMinute)); + BigDecimal hours = DateConvertUtil.minuteConvert(minute, 1, 2, null); + outworkResult.setHours(outworkResult.getHours().add(hours)); + // 获取计薪工时占比 + BigDecimal payrollProportion; + // 是否为划线排班 + if (schedulesTypeMap.containsKey(-1)) { + BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP); + outworkResult.setDays(outworkResult.getDays().add(days)); + payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } else { + //获取小时数跟天数的占比 + BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft(); + if (proportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + outworkResult.setDays(outworkResult.getDays().add(minute.divide(proportion, 2, RoundingMode.HALF_UP))); + payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } + // 设置实际计薪工时 + if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + // 设置外勤计薪工时 + outworkResult.setPayrollHours(outworkResult.getPayrollHours().add(minute.multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP))); + } + } + } catch (Exception e) { + log.error("获取外勤数据时发生异常", e); + return outworkResult; + } + return outworkResult; + } + + /** + * 获取旷工数据 + * + * @param dataList 数据列表 + * @param schedulesTypeMap 排班类型数据 + * @return 旷工数据 + */ + private static ProcessResult processAbsence(List dataList, Map>> schedulesTypeMap) { + ProcessResult result = new ProcessResult(); + try { + List absenceList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.KG); + for (UserDaySituationData itemRule : absenceList) { + if (CollUtil.isEmpty(itemRule.getClockInResultList())) { + continue; + } + if (!isAbsence(itemRule.getClockInResultList())) { + continue; + } + BigDecimal hours = DateConvertUtil.minuteConvert(new BigDecimal(itemRule.getValidDuration()), 1, 2, null); + result.setCount(result.getCount() + 1); + result.setMinutes(result.getMinutes() + itemRule.getValidDuration()); + result.setHours(result.getHours().add(hours)); + // 获取计薪工时占比 + BigDecimal payrollProportion; + // 是否为划线排班 + if (schedulesTypeMap.containsKey(-1)) { + BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP); + result.setDays(result.getDays().add(days)); + payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } else { + //获取小时数跟天数的占比 + BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft(); + if (proportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + result.setDays(result.getDays().add(BigDecimal.valueOf(itemRule.getValidDuration()).divide(proportion, 2, RoundingMode.HALF_UP))); + payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } + // 设置实际计薪工时 + if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + // 设置旷工计薪工时 + result.setPayrollHours(result.getPayrollHours().add(BigDecimal.valueOf(itemRule.getValidDuration()).multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP))); + } + } catch (Exception e) { + log.error("获取旷工数据时发生异常", e); + return result; + } + return result; + } + + /** + * 获取缺卡数据 + * + * @param attendanceRatio 出勤换算比 + * @param dataList 数据列表 + * @param schedulesTypeMap 排班类型 + * @return 缺卡数据 + */ + private static ProcessResult processAbsenceCard(BigDecimal attendanceRatio, List dataList, Map>> schedulesTypeMap) { + ProcessResult result = new ProcessResult(); + try { + List absenceCardList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.QK); + for (UserDaySituationData itemRule : absenceCardList) { + if (CollUtil.isEmpty(itemRule.getClockInResultList())) { + continue; + } + if (isAbsence(itemRule.getClockInResultList())) { + continue; + } + List clockInResults = getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.QK); + if (CollUtil.isEmpty(clockInResults)) { + continue; + } + result.setCount(result.getCount() + clockInResults.size()); + if (AttendanceTypeEnum.WORKOVERTIME.getCode().equals(itemRule.getAttendanceType())) { + BigDecimal hours = DateConvertUtil.dateConvert(itemRule.getInPoint(), itemRule.getOutPoint(), 2, 2, null); + result.setHours(result.getHours().add(hours)); + result.setDays(result.getDays().add(hours.divide(attendanceRatio, 2, RoundingMode.HALF_UP))); + } else { + BigDecimal hours = DateConvertUtil.minuteConvert(new BigDecimal(itemRule.getValidDuration()), 1, 2, null); + result.setHours(result.getHours().add(hours)); + // 获取计薪工时占比 + BigDecimal payrollProportion; + // 是否为划线排班 + if (schedulesTypeMap.containsKey(-1)) { + BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP); + result.setDays(result.getDays().add(days)); + payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } else { + //获取小时数跟天数的占比 + BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft(); + if (proportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + result.setDays(result.getDays().add(BigDecimal.valueOf(itemRule.getValidDuration()).divide(proportion, 2, RoundingMode.HALF_UP))); + payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } + // 设置实际计薪工时 + if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + // 设置缺卡计薪工时 + result.setPayrollHours(result.getPayrollHours().add(BigDecimal.valueOf(itemRule.getValidDuration()).multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP))); + } + } + } catch (Exception e) { + log.error("获取缺卡数据时发生异常", e); + return result; + } + return result; + } + + /** + * 获取补卡数据 + * @param dataList 出勤数据 + * @return 补卡数据 + */ + private static int processMakeUpCard(List dataList) { + List makeUpCardList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.BK); + int makeUpCardTimes = 0; + try { + for (UserDaySituationData item : makeUpCardList) { + if (CollUtil.isEmpty(item.getClockInResultList())) { + continue; + } + if (isAbsence(item.getClockInResultList())) { + continue; + } + List clockInResults = getClockInResultFilterList(item.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.BK); + if (CollUtil.isNotEmpty(clockInResults)) { + makeUpCardTimes += clockInResults.size(); + } + } + } catch (Exception e) { + log.error("获取补卡数据时发生异常", e); + return makeUpCardTimes; + } + return makeUpCardTimes; + } + + /** + * 获取迟到数据 + * @param dataList 出勤数据 + * @return 迟到数据 + */ + private static ProcessTimeResult processLate(List dataList) { + List lateList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.CD); + ProcessTimeResult result = new ProcessTimeResult(); + try { + for (UserDaySituationData item : lateList) { + if (CollUtil.isEmpty(item.getClockInResultList())) { + continue; + } + if (isAbsence(item.getClockInResultList())) { + continue; + } + List clockInResults = getClockInResultFilterList(item.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.CD); + if (CollUtil.isNotEmpty(clockInResults)) { + result.setCount(result.getCount() + 1); + for (ClockInResult clockInResult : clockInResults) { + result.setTime(result.getTime().add(Objects.nonNull(clockInResult.getAbnormalMinute()) ? + DateConvertUtil.minuteConvert(new BigDecimal(clockInResult.getAbnormalMinute()), 1, 2, null) : BigDecimal.ZERO)); + } + } + } + } catch (Exception e) { + log.error("获取迟到数据时发生异常", e); + return result; + } + return result; + } + + /** + * 获取早退数据 + * + * @param dataList 出勤数据 + * @return 早退数据 + */ + private static ProcessTimeResult processEarlyLeave(List dataList) { + List earlyLeaveList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.ZT); + ProcessTimeResult result = new ProcessTimeResult(); + try { + for (UserDaySituationData item : earlyLeaveList) { + if (CollUtil.isEmpty(item.getClockInResultList())) { + continue; + } + if (isAbsence(item.getClockInResultList())) { + continue; + } + List clockInResults = getClockInResultFilterList(item.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.ZT); + if (CollUtil.isNotEmpty(clockInResults)) { + result.setCount(result.getCount() + 1); + for (ClockInResult clockInResult : clockInResults) { + result.setTime(result.getTime().add(Objects.nonNull(clockInResult.getAbnormalMinute()) ? DateConvertUtil.secondConvert(clockInResult.getAbnormalMinute(), 1, 2, null) : BigDecimal.ZERO)); + } + } + } + } catch (Exception e) { + log.error("获取早退数据时发生异常", e); + return result; + } + return result; + } + + /** + * 获取应出勤\实际出勤数据 + * + * @param dataList 用户出勤数据 + * @param schedulesTypeMap 排班类型 + * @param statistics 考勤统计 + */ + public static void processShouldAttendData(List dataList, Map>> schedulesTypeMap, AttendanceDayStatistics statistics) { + List shouldAttendList = DayStatisticsUtils.getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.YCQ); + ProcessResult shouldAttendResult = new ProcessResult(); + ProcessResult actualAttendResult = new ProcessResult(); + try { + // 计算应出勤天数 + if (CollUtil.isNotEmpty(shouldAttendList)) { + // 获取应出勤天数 + shouldAttendResult.setDays(getShouldDay(shouldAttendList, schedulesTypeMap)); + } + // 判断当天是否为划线排班 + boolean isUnderlineScheduling = schedulesTypeMap.containsKey(-1); + // 设置应计薪工时 + shouldAttendResult.setPayrollHours(schedulesTypeMap.values().stream().map(MutablePair::getRight).map(MutablePair::getLeft).reduce(BigDecimal::add).orElse(BigDecimal.ZERO)); + for (UserDaySituationData itemRule : shouldAttendList) { + shouldAttendResult.setHours(shouldAttendResult.getHours().add(DateConvertUtil.minuteConvert(new BigDecimal(itemRule.getValidDuration()), 1, 2, null).stripTrailingZeros())); + // 请假不会扣减时段工时换算比,非划线排班 + if (!isUnderlineScheduling && Objects.isNull(itemRule.getPeriodWorkDay())) { + continue; + } + if (CollUtil.isEmpty(itemRule.getClockInResultList())) { + continue; + } + if (DayStatisticsUtils.isAbsence(itemRule.getClockInResultList())) { + continue; + } + // 没有缺卡并且不是旷工才算实际出勤 + List clockInResults = DayStatisticsUtils.getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.SJCQ); + if (CollUtil.isEmpty(clockInResults) || clockInResults.size() != 2) { + continue; + } + Date actualAttendStartTime = null; + Date actualAttendEndTime = null; + Integer restMinute = 0; + for (ClockInResult result : clockInResults) { + if (ConstantUtil.ON_WORK.equals(result.getClockInType())) { + if (Objects.nonNull(itemRule.getInPoint())) { + actualAttendStartTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().before(itemRule.getInPoint()) ? itemRule.getInPoint() : result.getClockInTime()) : result.getEffectiveTime(); + } + } else if (ConstantUtil.OFF_WORK.equals(result.getClockInType())) { + if (Objects.nonNull(itemRule.getOutPoint())) { + actualAttendEndTime = Objects.nonNull(result.getClockInTime()) ? (result.getClockInTime().after(itemRule.getOutPoint()) ? itemRule.getOutPoint() : result.getClockInTime()) : result.getEffectiveTime(); + } + restMinute = result.getRestMinute(); + } + } + if (actualAttendStartTime != null && actualAttendEndTime != null) { + // 减去休息分钟数 + BigDecimal minute = DateConvertUtil.dateConvert(actualAttendStartTime, actualAttendEndTime, 1, 2, null).subtract(new BigDecimal(restMinute)); + BigDecimal hours = DateConvertUtil.minuteConvert(minute, 1, 2, null); + actualAttendResult.setHours(actualAttendResult.getHours().add(hours)); + // 获取计薪工时占比 + BigDecimal payrollProportion; + // 是否划线排班 + if (isUnderlineScheduling) { + BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP); + // 计算实际出勤天数 + actualAttendResult.setDays(actualAttendResult.getDays().add(days)); + payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } else { + // 计算小时数跟天数的占比 + BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft(); + if (proportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + // 计算实际出勤天数 + actualAttendResult.setDays(actualAttendResult.getDays().add(minute.divide(proportion, 2, RoundingMode.HALF_UP))); + payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } + // 设置实际计薪工时 + if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + actualAttendResult.setPayrollHours(actualAttendResult.getPayrollHours().add(minute.multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP))); + } + } + } catch (Exception e) { + log.error("获取应出勤||实际出勤数据时发生异常", e); + } + statistics.setShouldAttendDays(shouldAttendResult.getDays()); + statistics.setShouldAttendHours(shouldAttendResult.getHours()); + statistics.setShouldAttendPayrollHours(shouldAttendResult.getPayrollHours()); + statistics.setActualAttendDays(actualAttendResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : actualAttendResult.getDays()); + statistics.setActualAttendHours(actualAttendResult.getHours()); + statistics.setActualAttendPayrollHours(actualAttendResult.getPayrollHours()); + } + + /** + * 获取有效出勤数据 + * + * @param dataList 用户出勤数据 + * @param schedulesTypeMap 排班类型 + * @param statistics 考勤统计 + */ + public static void processEffectiveAttendData(List dataList, Map>> schedulesTypeMap, AttendanceDayStatistics statistics) { + List shouldAttendList = DayStatisticsUtils.getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.YCQ); + ProcessResult effectiveAttendResult = new ProcessResult(); + try { + // 判断当天是否为划线排班 + boolean isUnderlineScheduling = schedulesTypeMap.containsKey(-1); + for (UserDaySituationData itemRule : shouldAttendList) { + // 请假不会扣减时段工时换算比,非划线排班 + if (!isUnderlineScheduling && Objects.isNull(itemRule.getPeriodWorkDay())) { + continue; + } + if (CollUtil.isEmpty(itemRule.getClockInResultList())) { + continue; + } + if (DayStatisticsUtils.isAbsence(itemRule.getClockInResultList())) { + continue; + } + // 没有缺卡并且不是旷工才算实际出勤 + List clockInResults = DayStatisticsUtils.getClockInResultFilterList(itemRule.getClockInResultList(), StatisticsEnumUtil.StatisticsTypeEnum.SJCQ); + if (CollUtil.isEmpty(clockInResults) || clockInResults.size() != 2) { + continue; + } + Date effectiveAttendStartTime = null; + Date effectiveAttendEndTime = null; + Integer restMinute = 0; + for (ClockInResult result : clockInResults) { + if (ConstantUtil.ON_WORK.equals(result.getClockInType())) { + if (Objects.nonNull(itemRule.getInPoint())) { + effectiveAttendStartTime = result.getEffectiveTime(); + } + } else if (ConstantUtil.OFF_WORK.equals(result.getClockInType())) { + if (Objects.nonNull(itemRule.getOutPoint())) { + effectiveAttendEndTime = result.getEffectiveTime(); + } + restMinute = result.getRestMinute(); + } + } + if (effectiveAttendStartTime != null && effectiveAttendEndTime != null) { + // 减去休息分钟数 + BigDecimal minute = DateConvertUtil.dateConvert(effectiveAttendStartTime, effectiveAttendEndTime, 1, 2, null).subtract(new BigDecimal(restMinute)); + BigDecimal hours = DateConvertUtil.minuteConvert(minute, 1, 2, null); + effectiveAttendResult.setHours(effectiveAttendResult.getHours().add(DateConvertUtil.minuteConvert(minute, 1, 2, null))); + // 获取计薪工时占比 + BigDecimal payrollProportion; + // 是否划线排班 + if (isUnderlineScheduling) { + BigDecimal days = hours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP); + //计算实际出勤天数 + effectiveAttendResult.setDays(effectiveAttendResult.getDays().add(days)); + payrollProportion = schedulesTypeMap.getOrDefault(-1, MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } else { + //计算小时数跟天数的占比 + BigDecimal proportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getLeft(); + if (proportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + //计算有效出勤天数 + effectiveAttendResult.setDays(effectiveAttendResult.getDays().add(minute.divide(proportion, 2, RoundingMode.HALF_UP))); + payrollProportion = schedulesTypeMap.getOrDefault(itemRule.getSchedulesType(), MutablePair.of(BigDecimal.ZERO, MutablePair.of(BigDecimal.ZERO, BigDecimal.ZERO))).getRight().getRight(); + } + // 设置实际计薪工时 + if (payrollProportion.compareTo(BigDecimal.ZERO) == 0) { + continue; + } + // 设置实际计薪工时 + effectiveAttendResult.setPayrollHours(effectiveAttendResult.getPayrollHours().add(minute.multiply(payrollProportion).setScale(2, RoundingMode.HALF_UP))); + } + } + } catch (Exception e) { + log.error("获取有效出勤数据时发生异常", e); + } + statistics.setEffectiveAttendDays(effectiveAttendResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : effectiveAttendResult.getDays()); + statistics.setEffectiveAttendHours(effectiveAttendResult.getHours()); + statistics.setEffectiveAttendPayrollHours(effectiveAttendResult.getPayrollHours()); + } + + /** + * 获取应出勤天数 + * + * @param dataList 排班数据 + * @param schedulesTypeMap 排班类型 + * @return 应出勤天数 + */ + private static BigDecimal getShouldDay(List dataList, Map>> schedulesTypeMap) { + List dailyRuleList = dataList.stream().filter(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList()); + Map> classTypeMap = dailyRuleList.stream().filter(itemRule -> Objects.nonNull(itemRule.getSchedulesType())).collect(Collectors.groupingBy(UserDaySituationData::getSchedulesType)); + // 判断当天是否为划线排班 + boolean isUnderlineScheduling = schedulesTypeMap.containsKey(-1); + BigDecimal shouldHours = BigDecimal.ZERO; + BigDecimal shouldDay = BigDecimal.ZERO; + for (Map.Entry> entry : classTypeMap.entrySet()) { + // 如果是划线排班,天数需要根据出勤换算比来计算 + if (isUnderlineScheduling) { + Integer validDuration = entry.getValue().stream().map(UserDaySituationData::getValidDuration).filter(Objects::nonNull).reduce(0, Integer::sum); + shouldHours = shouldHours.add(DateConvertUtil.minuteConvert(new BigDecimal(validDuration), 1, 2, null)); + BigDecimal days = shouldHours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP).compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : shouldHours.divide(schedulesTypeMap.get(-1).getLeft(), 2, RoundingMode.HALF_UP); + shouldDay = shouldDay.add(days); + continue; + } + // 请假是否全覆盖 全天班、上午班、下午班 + boolean isHasAllLeave = entry.getValue().stream().allMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())); + if (isHasAllLeave) { + shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getLeaveDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + } else { + // 全天班、上午班、下午班班次内是否有请假,有的话,普通班的天数只会会统计一次 + boolean isHasLeave = entry.getValue().stream().anyMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())); + if (isHasLeave) { + shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).findFirst().orElse(BigDecimal.ZERO)); + entry.getValue().removeIf(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())); + if (CollUtil.isNotEmpty(entry.getValue())) { + shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + } + } else { + // 公休天数+普班天数 + shouldDay = shouldDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + } + } + } + return shouldDay.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : shouldDay; + } + + /** + * 统计项处理 + * + * @param attendanceRatio 出勤换算比 + * @param dataList 数据 + * @param schedulesTypeMap 排班类型 + * @param statistics 统计 + */ + public static void processStatisticalItems(BigDecimal attendanceRatio, List dataList, Map>> schedulesTypeMap, AttendanceDayStatistics statistics) { + // 迟到处理 + ProcessTimeResult lateResult = processLate(dataList); + statistics.setLateTimes(lateResult.getCount()); + statistics.setLateMinutes(lateResult.getTime()); + // 早退处理 + ProcessTimeResult earlyLeaveResult = processEarlyLeave(dataList); + statistics.setEarlyLeaveTimes(earlyLeaveResult.getCount()); + statistics.setEarlyLeaveMinutes(earlyLeaveResult.getTime()); + // 缺卡处理 + ProcessResult absenceCardResult = processAbsenceCard(attendanceRatio, dataList, schedulesTypeMap); + statistics.setAbsenceCardTimes(absenceCardResult.getCount()); + statistics.setAbsenceCardHours(absenceCardResult.getHours()); + statistics.setAbsenceCardPayrollHours(absenceCardResult.getPayrollHours()); + statistics.setAbsenceCardDays(absenceCardResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : absenceCardResult.getDays()); + // 旷工处理 + ProcessResult absenceResult = processAbsence(dataList, schedulesTypeMap); + statistics.setAbsenceTimes(absenceResult.getCount()); + statistics.setAbsenceHours(absenceResult.getHours()); + statistics.setAbsencePayrollHours(absenceResult.getPayrollHours()); + statistics.setAbsenceDays(absenceResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : absenceResult.getDays()); + // 补卡处理 + statistics.setMakeUpCardTimes(processMakeUpCard(dataList)); + // 外勤打卡处理 + ProcessResult outworkResult = processOutworkClock(dataList, schedulesTypeMap); + statistics.setOutworkTimes(outworkResult.getCount()); + statistics.setOutworkHours(outworkResult.getHours()); + statistics.setOutworkPayrollHours(outworkResult.getPayrollHours()); + statistics.setOutworkDays(outworkResult.getDays().compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : outworkResult.getDays()); + // 加班处理 + Map overtimeResultMap = processOvertime(attendanceRatio, dataList); + // 总加班汇总 + statistics.setOvertimeTimes(overtimeResultMap.values().stream().mapToInt(ProcessResult::getCount).sum()); + statistics.setOvertimeHours(overtimeResultMap.values().stream().map(ProcessResult::getHours).reduce(BigDecimal.ZERO, BigDecimal::add)); + statistics.setOvertimeDays(overtimeResultMap.values().stream().map(ProcessResult::getDays).reduce(BigDecimal.ZERO, BigDecimal::add)); + // 工作日加班处理 + statistics.setWeekdayOvertimeTimes(overtimeResultMap.get(OvertimeType.WORK_DAY).getCount()); + statistics.setWeekdayOvertimeHours(overtimeResultMap.get(OvertimeType.WORK_DAY).getHours()); + statistics.setWeekdayOvertimeDays(overtimeResultMap.get(OvertimeType.WORK_DAY).getDays()); + // 节假日加班处理 + statistics.setHolidaysOvertimeTimes(overtimeResultMap.get(OvertimeType.HOLIDAY).getCount()); + statistics.setHolidaysOvertimeHours(overtimeResultMap.get(OvertimeType.HOLIDAY).getHours()); + statistics.setHolidaysOvertimeDays(overtimeResultMap.get(OvertimeType.HOLIDAY).getDays()); + // 公休日加班处理 + statistics.setPublicHolidaysOvertimeTimes(overtimeResultMap.get(OvertimeType.REST_DAY).getCount()); + statistics.setPublicHolidaysOvertimeHours(overtimeResultMap.get(OvertimeType.REST_DAY).getHours()); + statistics.setPublicHolidaysOvertimeDays(overtimeResultMap.get(OvertimeType.REST_DAY).getDays()); + // 公休处理 + ProcessResult publicHolidaysResult = processPublicHolidays(attendanceRatio, dataList); + statistics.setPublicHolidaysHours(publicHolidaysResult.getHours()); + statistics.setPublicHolidaysPayrollHours(publicHolidaysResult.getPayrollHours()); + statistics.setPublicHolidaysDays(publicHolidaysResult.getDays()); + } + + /** + * 公休处理 + * + * @param attendanceRatio 出勤换算比 + * @param dataList 数据 + * @return 公休处理结果 + */ + private static ProcessResult processPublicHolidays(BigDecimal attendanceRatio, List dataList) { + ProcessResult result = new ProcessResult(); + try { + List publicHolidaysList = getWebClassesFilterList(dataList, StatisticsEnumUtil.StatisticsTypeEnum.XX); + for (UserDaySituationData daySituationData : publicHolidaysList) { + BigDecimal hours, payrollHours, days; + if (Objects.isNull(daySituationData.getSchedulesType())) { + continue; + } + if (daySituationData.getSchedulesType().equals(0)) { + hours = attendanceRatio; + payrollHours = hours; + days = BigDecimal.ONE; + } else { + hours = DateConvertUtil.minuteConvert(new BigDecimal(daySituationData.getValidDuration()), 1, 2, null); + payrollHours = daySituationData.getPayrollHours(); + days = Objects.isNull(daySituationData.getPeriodWorkDay()) ? BigDecimal.ZERO : daySituationData.getPeriodWorkDay(); + } + result.setHours(result.getHours().add(hours)); + result.setPayrollHours(result.getPayrollHours().add(payrollHours)); + result.setDays(result.getDays().add(days)); + } + } catch (Exception e) { + log.error("公休处理数据时发生异常", e); + return result; + } + return result; + } + + /** + * 获取出差外出时长 + * + * @param busOrOutApproveVoList 出差外出列表 + * @param dayStaVo 统计结果 + * @param attendanceRatio 考勤比例 + */ + public static void processBusOrOutData(List busOrOutApproveVoList, AttendanceDayStatistics dayStaVo, BigDecimal attendanceRatio) { + if (CollUtil.isEmpty(busOrOutApproveVoList)) { + return; + } + dayStaVo.setBusHours(BigDecimal.ZERO); + dayStaVo.setBusDays(BigDecimal.ZERO); + dayStaVo.setOutHours(BigDecimal.ZERO); + dayStaVo.setOutDays(BigDecimal.ZERO); + // 出差 + List busApproveVoList = busOrOutApproveVoList.stream().filter(item -> item.getType().equals(2)).collect(Collectors.toList()); + Date day = dayStaVo.getDate(); + if (CollUtil.isNotEmpty(busApproveVoList)) { + dayStaVo.setBusBatchNumber(busApproveVoList.stream().map(OutOrBusApproveVo::getId).distinct().collect(Collectors.joining(","))); + busApproveVoList.forEach(item -> { + BigDecimal hoursForDate = item.getDurationForDate(day, Boolean.FALSE); + dayStaVo.setBusDays(dayStaVo.getOutDays().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? BigDecimal.ONE : hoursForDate.divide(attendanceRatio, 2, RoundingMode.HALF_UP))); + dayStaVo.setBusHours(dayStaVo.getOutHours().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? attendanceRatio : hoursForDate)); + }); + } + // 外出(要按照天维度分组,因为一天会存在多次外出) + List outApproveVoList = busOrOutApproveVoList.stream().filter(item -> item.getType().equals(1)).collect(Collectors.toList()); + // 将外出申请按照日期范围展开,每一天都生成一条记录 + Map> outApproveByDayMap = new HashMap<>(); + for (OutOrBusApproveVo approveVo : outApproveVoList) { + Date startDate = DateUtil.beginOfDay(approveVo.getStartTime()); + Date endDate = DateUtil.endOfDay(approveVo.getEndTime()); + // 遍历申请时间范围内的每一天 + Date currentDate = startDate; + while (!currentDate.after(endDate)) { + Date dayKey = DateUtil.beginOfDay(currentDate); + outApproveByDayMap.computeIfAbsent(dayKey, k -> new ArrayList<>()).add(approveVo); + currentDate = DateUtil.offsetDay(currentDate, 1); + } + } + // 获取当前统计日期对应的外出申请 + List todayOutApproveList = outApproveByDayMap.getOrDefault(day, new ArrayList<>()); + if (CollUtil.isNotEmpty(todayOutApproveList)) { + List batchNumberResultList = todayOutApproveList.stream().map(item -> { + BigDecimal hoursForDate = item.getDurationForDate(day, Boolean.FALSE); + return BatchNumberResult.builder().day(day).batchNumberId(item.getId()).days(hoursForDate.compareTo(attendanceRatio) >= 0 ? BigDecimal.ONE : hoursForDate.divide(attendanceRatio, 2, RoundingMode.HALF_UP)).hours(hoursForDate.compareTo(attendanceRatio) >= 0 ? attendanceRatio : hoursForDate).build(); + }).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(batchNumberResultList)) { + dayStaVo.setOutBatchNumber(JSONObject.toJSONString(batchNumberResultList)); + } + // 计算当天的外出时长 + BigDecimal hoursForDate = todayOutApproveList.stream().map(item -> item.getDurationForDate(day, Boolean.FALSE)).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + dayStaVo.setOutDays(dayStaVo.getOutDays().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? BigDecimal.ONE : hoursForDate.divide(attendanceRatio, 2, RoundingMode.HALF_UP))); + dayStaVo.setOutHours(dayStaVo.getOutHours().add(hoursForDate.compareTo(attendanceRatio) >= 0 ? attendanceRatio : hoursForDate)); + } + } + + /** + * 通用数据汇总方法 - 对指定字段求和 + * + * @param dataList 数据列表 + * @param valueExtractor 值提取器 + * @param 数据类型 + * @param 值类型(需为Number类型) + * @return 所有数据的值求和结果 + */ + public static BigDecimal calculateSum(List dataList, Function valueExtractor) { + if (CollUtil.isEmpty(dataList)) { + return BigDecimal.ZERO; + } + return dataList.stream().filter(Objects::nonNull).map(item -> { + V value = valueExtractor.apply(item); + if (value == null) { + return BigDecimal.ZERO; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } else { + return new BigDecimal(value.toString()); + } + }).reduce(BigDecimal.ZERO, BigDecimal::add).stripTrailingZeros(); + } + + /** + * 计算两数相减,保留两位小数 + * + * @param minuend 被减数 + * @param subtrahend 减数 + * @return 结果,保留两位小数 + */ + public static BigDecimal calculateSubtract(BigDecimal minuend, BigDecimal subtrahend) { + if (minuend == null || subtrahend == null) { + return null; + } + BigDecimal subtract = minuend.subtract(subtrahend); + if (subtract.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return subtract.setScale(4, RoundingMode.HALF_UP).stripTrailingZeros(); + } + + /** + * 通用数据求和方法 - 对Integer类型字段求和 + * + * @param dataList 数据列表 + * @param valueExtractor 值提取器 + * @param 数据类型 + * @return 所有数据的值求和结果 + */ + public static Integer calculateSumToInt(List dataList, Function valueExtractor) { + if (CollUtil.isEmpty(dataList)) { + return 0; + } + return dataList.stream().filter(Objects::nonNull).map(valueExtractor).filter(Objects::nonNull).mapToInt(Integer::intValue).sum(); + } + + /** + * 获取指定日期的月份 + * + * @param date 日期 + * @return 月份 + */ + public static String getMonthFromDate(Date date) { + YearMonth yearMonth = YearMonth.from(date.toInstant().atZone(java.time.ZoneId.systemDefault())); + return yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM")); + } + + /** + * + * @param dto 请求参数 + * @param userStaMap 用户统计数据 + * @return 用户统计数据 + */ + public static Map initSalaryUserData(SalaryAttendanceSupportDto dto, Map userStaMap) { + return dto.getUserIds().stream().map(item -> { + if (CollUtil.isEmpty(userStaMap)) { + SalaryAttendanceSupportVo supportVo = new SalaryAttendanceSupportVo(); + supportVo.setUserId(item); + return supportVo; + } + if (userStaMap.containsKey(item)) { + return userStaMap.get(item); + } + SalaryAttendanceSupportVo supportVo = new SalaryAttendanceSupportVo(); + supportVo.setUserId(item); + return supportVo; + }).collect(Collectors.toMap(SalaryAttendanceSupportVo::getUserId, item -> item)); + } + + /** + * 获取指定日期范围内的所有日期集合 + * + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 日期集合 + */ + public static List getDatesBetween(Date startDateFilter, Date endDateFilter, Date startDate, Date endDate) { + List dates = new ArrayList<>(); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(startDate); + while (!calendar.getTime().after(endDate)) { + // 节假日日期要存在于我周期范围内才算 + if (calendar.getTime().compareTo(startDateFilter) >= 0 && calendar.getTime().compareTo(endDateFilter) <= 0) { + dates.add(calendar.getTime()); + } + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + return dates; + } + + /** + * 获取用户节假日天数 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param autoSchedulingDaysMap 用户的所有节假日 + * @return 用户自动排班天数 + */ + public static Map> getUserHolidayDaysStaMapMap(Date startDate, Date endDate, Map> autoSchedulingDaysMap) { + Map> daysStaMap = new HashMap<>(); + autoSchedulingDaysMap.forEach((userId, autoSchedulingDaysList) -> { + Set dateSet = new HashSet<>(); + // 排除掉不是自动排版的数据 + List collect = autoSchedulingDaysList.stream().filter(item -> item.getAutoScheduling() != 1).collect(Collectors.toList()); + // 获取每个节假日Day天数集合,每个节假日是有范围的,要匹配我的 + if (CollUtil.isNotEmpty(collect)) { + collect.forEach(festival -> { + dateSet.addAll(parseFestivalDates(festival.getFestivalDate(), startDate, endDate)); + }); + } + if (CollUtil.isNotEmpty(dateSet)) { + daysStaMap.put(userId, dateSet); + } + }); + return daysStaMap; + } + + /** + * 获取用户自动排班天数 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param autoSchedulingDaysMap 用户的所有节假日 + * @return 用户自动排班天数 + */ + public static Map getUserAutoSchedulingDaysStaMap(Date startDate, Date endDate, Map> autoSchedulingDaysMap) { + Map autoSchedulingDaysSta = new HashMap<>(); + autoSchedulingDaysMap.forEach((userId, autoSchedulingDaysList) -> { + Set dateSet = new HashSet<>(); + // 排除掉不是自动排版的数据 + List collect = autoSchedulingDaysList.stream().filter(item -> item.getAutoScheduling() != 1).collect(Collectors.toList()); + // 获取每个节假日Day天数集合,每个节假日是有范围的,要匹配我的 + if (CollUtil.isNotEmpty(collect)) { + collect.forEach(festival -> { + dateSet.addAll(parseFestivalDates(festival.getFestivalDate(), startDate, endDate)); + }); + } + if (CollUtil.isNotEmpty(dateSet)) { + autoSchedulingDaysSta.put(userId, dateSet.size()); + } + }); + return autoSchedulingDaysSta; + } + + /** + * 从 festivalDate 字段解析节假日日期集合 + * 字段值格式:"2026-03-25,2026-03-26" + * + * @param festivalDate 节日日期字符串,格式:"yyyy-MM-dd,yyyy-MM-dd" + * @param startDate 查询开始日期 + * @param endDate 查询结束日期 + * @return 符合条件的日期集合 + */ + public static Set parseFestivalDates(String festivalDate, Date startDate, Date endDate) { + Set dateSet = new HashSet<>(); + if (StrUtil.isNotBlank(festivalDate)) { + String[] dateStrings = festivalDate.split(","); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + for (String dateStr : dateStrings) { + try { + Date holidayDate = sdf.parse(dateStr.trim()); + // 判断日期是否在查询范围内 + if (holidayDate.compareTo(startDate) >= 0 && holidayDate.compareTo(endDate) <= 0) { + dateSet.add(holidayDate); + } + } catch (Exception e) { + log.error("解析节日日期失败:{}", dateStr, e); + } + } + } + return dateSet; + } + + /** + * 获取用户法定节假日天数 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param autoSchedulingDaysMap 用户的所有节假日 + * @return 用户自动排班天数 + */ + public static Map> getLegalHolidaysStaMap(Date startDate, Date endDate, Map> autoSchedulingDaysMap) { + Map> legalHolidaysStaMap = new HashMap<>(); + autoSchedulingDaysMap.forEach((userId, autoSchedulingDaysList) -> { + // 获取法定节假日名称 + List legalHolidayNames = Arrays.asList("元旦节", "春节", "清明节", "劳动节", "端午节", "中秋节", "国庆节"); + autoSchedulingDaysList = autoSchedulingDaysList.stream().filter(item -> legalHolidayNames.contains(item.getName())).collect(Collectors.toList()); + if (CollUtil.isNotEmpty(autoSchedulingDaysList)) { + return; + } + Map festivalRulesMap = autoSchedulingDaysList.stream().collect(Collectors.toMap(AttendanceFestivalRules::getId, item -> item)); + Map map = new HashMap<>(); + festivalRulesMap.forEach((field, item) -> { + HolidaysTypeJsonData1 jsonData = new HolidaysTypeJsonData1(); + jsonData.setField(field); + jsonData.setFieldName(item.getName()); + Set dateList = parseFestivalDates(item.getFestivalDate(), startDate, endDate); + if(CollUtil.isEmpty(dateList)){ + return; + } + jsonData.setLeaveDays(BigDecimal.valueOf(dateList.size())); + map.put(field, jsonData); + }); + if (CollUtil.isEmpty(map)) { + return; + } + legalHolidaysStaMap.put(userId, map); + }); + return legalHolidaysStaMap; + } + + + /** + * 构建请假天数数据 + * + * @param leaveVos 请假数据 + * @param leaveTypeMap 请假类型数据 + * @param supportVoList 日统计数据 + * @return 请假数据 + */ + public static Map> buildLeaveMap(List leaveVos, Map leaveTypeMap, List supportVoList) { + if (CollUtil.isEmpty(leaveVos)) { + return new HashMap<>(); + } + Map> leaveMap = supportVoList.stream().collect(Collectors.groupingBy(SalaryAttendanceSupportQuery::getUserId)); + return leaveVos.stream().collect(Collectors.groupingBy(LeaveApprovalModel::getUserId, Collectors.groupingBy(LeaveApprovalModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> { + if (CollUtil.isEmpty(list)) { + return null; + } + LeaveApprovalModel infoModel = list.get(0); + String leaveTypeId = infoModel.getLeaveTypeId(); + String userId = infoModel.getUserId(); + List> customLeaveListFlat = leaveMap.get(userId).stream().filter(item -> CollUtil.isNotEmpty(item.getCustomLeaveList())).flatMap(item -> item.getCustomLeaveList().stream()).collect(Collectors.toList()); + List dayCustomLeaveList = DayStatisticsUtils.getDayCustomLeaveList(customLeaveListFlat).stream().filter(item -> item.getField().equals(leaveTypeId)).collect(Collectors.toList()); + Map leaveTypeJsonDataMap = CollUtil.isNotEmpty(dayCustomLeaveList) ? dayCustomLeaveList.stream().collect(Collectors.toMap(LeaveTypeJsonData::getField, item -> item, (existing, replacement) -> { + existing.setLeaveDays(existing.getLeaveDays().add(replacement.getLeaveDays())); + existing.setLeaveHours(existing.getLeaveHours().add(replacement.getLeaveHours())); + existing.setLeavePayrollHours(existing.getLeavePayrollHours().add(replacement.getLeavePayrollHours())); + if (replacement.getApplyList() != null && !replacement.getApplyList().isEmpty()) { + existing.getApplyList().addAll(replacement.getApplyList()); + } + return existing; + })) : new HashMap<>(); + LeaveTypeJsonData jsonData = new LeaveTypeJsonData(); + jsonData.setField(leaveTypeId); + jsonData.setFieldName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : ""); + jsonData.setHeadName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : ""); + LeaveTypeJsonData leaveTypeJsonData = leaveTypeJsonDataMap.getOrDefault(leaveTypeId, new LeaveTypeJsonData()); + jsonData.setLeaveDays(leaveTypeJsonData.getLeaveDays()); + jsonData.setLeaveHours(leaveTypeJsonData.getLeaveHours()); + jsonData.setLeavePayrollHours(leaveTypeJsonData.getLeavePayrollHours()); + jsonData.setApplyList(leaveTypeJsonData.getApplyList()); + return jsonData; + })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, entry -> + entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + } + + /** + * 构建请假余额数据 + * + * @param balanceInfoModels 请假余额数据 + * @param leaveTypeMap 请假类型数据 + * @param ratioMap 比例数据 + * @return 节假日数据 + */ + public static Map> buildLeaveBalanceMap(List balanceInfoModels, Map leaveTypeMap, Map ratioMap) { + if (CollUtil.isEmpty(balanceInfoModels)) { + return new HashMap<>(); + } + return balanceInfoModels.stream().collect(Collectors.groupingBy(LeaveBalanceInfoModel::getUserId, Collectors.groupingBy(LeaveBalanceInfoModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> { + if (CollUtil.isEmpty(list)) { + return null; + } + LeaveBalanceInfoModel infoModel = list.get(0); + if (!leaveTypeMap.containsKey(infoModel.getLeaveTypeId())) { + return null; + } + String userId = infoModel.getUserId(); + String leaveTypeId = infoModel.getLeaveTypeId(); + LeaveTypeJsonData jsonData = new LeaveTypeJsonData(); + jsonData.setField(leaveTypeId); + jsonData.setFieldName(leaveTypeMap.get(infoModel.getLeaveTypeId()).getName()); + jsonData.setHeadName(leaveTypeMap.get(infoModel.getLeaveTypeId()).getName()); + BigDecimal days = list.stream().map(LeaveBalanceInfoModel::getValue).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal ratio = ratioMap.getOrDefault(userId, BigDecimal.ZERO); + jsonData.setLeaveDays(days.stripTrailingZeros()); + jsonData.setLeaveHours(days.multiply(ratio).stripTrailingZeros()); + return jsonData; + })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + } + + /** + * 构建请假抵扣天数数据 + * + * @param leaveVos 请假数据 + * @param leaveTypeMap 请假类型数据 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 请假数据 + */ + public static Map> buildLeaveDeductMap(List leaveVos, Map leaveTypeMap, Date startDate, Date endDate) { + if (CollUtil.isEmpty(leaveVos)) { + return new HashMap<>(); + } + return leaveVos.stream().collect(Collectors.groupingBy(LeaveApprovalModel::getUserId, Collectors.groupingBy(LeaveApprovalModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> { + if (CollUtil.isEmpty(list)) { + return null; + } + List jsonDetailVos = list.stream().filter(Objects::nonNull).filter(item -> StringUtil.isNotEmpty(item.getBalanceJson())).map(item -> { + LeaveConsumptionDetailVo jsonToBean = JsonUtil.getJsonToBean(item.getBalanceJson(), LeaveConsumptionDetailVo.class); + LeaveDeductDetailVo jsonDetailVo = new LeaveDeductDetailVo(); + jsonDetailVo.setStartTime(item.getStartTime()); + jsonDetailVo.setEndTime(item.getEndTime()); + jsonDetailVo.setDeduction(jsonToBean.getRetirementLeave().add(jsonToBean.getInputTypeBalance())); + jsonDetailVo.setAttendanceRatio(jsonToBean.getAttendanceRatio()); + return jsonDetailVo; + }).collect(Collectors.toList()); + LeaveApprovalModel infoModel = list.get(0); + String leaveTypeId = infoModel.getLeaveTypeId(); + LeaveTypeJsonData jsonData = new LeaveTypeJsonData(); + jsonData.setField(leaveTypeId); + jsonData.setFieldName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : ""); + jsonData.setHeadName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : ""); + final BigDecimal[] days = {BigDecimal.ZERO}; + final BigDecimal[] hours = {BigDecimal.ZERO}; + jsonDetailVos.stream().filter(Objects::nonNull).forEach(vo -> { + Date leaveStart = vo.getStartTime(); + Date leaveEnd = vo.getEndTime(); + // 计算交集时间段 + Date intersectionStart = leaveStart.after(startDate) ? leaveStart : startDate; + Date intersectionEnd = leaveEnd.before(endDate) ? leaveEnd : endDate; + // 如果有交集,则返回未抵扣天数,否则返回0 + if (intersectionStart.before(intersectionEnd)) { + days[0] = days[0].add(vo.getDeduction() != null ? vo.getDeduction() : BigDecimal.ZERO); + hours[0] = hours[0].add(vo.getDeduction() != null ? vo.getDeduction().multiply(vo.getAttendanceRatio()) : BigDecimal.ZERO); + } + }); + if (days[0].compareTo(BigDecimal.ZERO) == 0) { + return null; + } + jsonData.setLeaveDays(days[0]); + jsonData.setLeaveHours(hours[0]); + return jsonData; + })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + } + + /** + * 构建请假扣薪天数数据 + * + * @param leaveVos 请假数据 + * @param leaveTypeMap 请假类型数据 + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return 请假数据 + */ + public static Map> buildLeaveDeductWagesMap(List leaveVos, Map leaveTypeMap, Date startDate, Date endDate) { + if (CollUtil.isEmpty(leaveVos)) { + return new HashMap<>(); + } + return leaveVos.stream().collect(Collectors.groupingBy(LeaveApprovalModel::getUserId, Collectors.groupingBy(LeaveApprovalModel::getLeaveTypeId, Collectors.collectingAndThen(Collectors.toList(), list -> { + if (CollUtil.isEmpty(list)) { + return null; + } + List jsonDetailVos = list.stream().filter(Objects::nonNull).filter(item -> StringUtil.isNotEmpty(item.getBalanceJson())).map(item -> { + LeaveConsumptionDetailVo jsonToBean = JsonUtil.getJsonToBean(item.getBalanceJson(), LeaveConsumptionDetailVo.class); + LeaveDeductWagesJsonDetailVo jsonDetailVo = new LeaveDeductWagesJsonDetailVo(); + jsonDetailVo.setStartTime(item.getStartTime()); + jsonDetailVo.setEndTime(item.getEndTime()); + jsonDetailVo.setUnconsumedBalance(jsonToBean.getUnconsumedBalance()); + jsonDetailVo.setAttendanceRatio(jsonToBean.getAttendanceRatio()); + return jsonDetailVo; + }).collect(Collectors.toList()); + LeaveApprovalModel infoModel = list.get(0); + String leaveTypeId = infoModel.getLeaveTypeId(); + LeaveTypeJsonData jsonData = new LeaveTypeJsonData(); + jsonData.setField(leaveTypeId); + jsonData.setFieldName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : ""); + jsonData.setHeadName(leaveTypeMap.containsKey(infoModel.getLeaveTypeId()) ? leaveTypeMap.get(infoModel.getLeaveTypeId()).getName() : ""); + final BigDecimal[] days = {BigDecimal.ZERO}; + final BigDecimal[] hours = {BigDecimal.ZERO}; + jsonDetailVos.stream().filter(Objects::nonNull).forEach(vo -> { + Date leaveStart = vo.getStartTime(); + Date leaveEnd = vo.getEndTime(); + // 计算交集时间段 + Date intersectionStart = leaveStart.after(startDate) ? leaveStart : startDate; + Date intersectionEnd = leaveEnd.before(endDate) ? leaveEnd : endDate; + // 如果有交集,则返回未抵扣天数,否则返回0 + if (intersectionStart.before(intersectionEnd)) { + days[0] = days[0].add(vo.getUnconsumedBalance() != null ? vo.getUnconsumedBalance() : BigDecimal.ZERO); + hours[0] = hours[0].add(vo.getUnconsumedBalance() != null ? vo.getUnconsumedBalance().multiply(vo.getAttendanceRatio()) : BigDecimal.ZERO); + } + }); + if (days[0].compareTo(BigDecimal.ZERO) == 0) { + return null; + } + jsonData.setLeaveDays(days[0]); + jsonData.setLeaveHours(hours[0]); + return jsonData; + })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, + entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + } + + /** + * 构建节假日数据 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @param autoSchedulingDaysMap 节假日数据 + * @param ratioMap 比例数据 + * @return 节假日数据 + */ + public static Map> buildHolidayTypeMap(Date startDate, Date endDate, Map> autoSchedulingDaysMap, Map ratioMap) { + if (CollUtil.isEmpty(autoSchedulingDaysMap)) { + return new HashMap<>(); + } + Map> result = new HashMap<>(); + autoSchedulingDaysMap.forEach((userId, list) -> { + Map map = new HashMap<>(); + if (CollUtil.isEmpty(list)) { + return; + } + Map festivalRulesMap = list.stream().collect(Collectors.toMap(AttendanceFestivalRules::getId, item -> item)); + festivalRulesMap.forEach((field, item) -> { + HolidaysTypeJsonData1 jsonData = new HolidaysTypeJsonData1(); + jsonData.setField(field); + jsonData.setFieldName(item.getName()); + Set dateList = parseFestivalDates(item.getFestivalDate(), startDate, endDate); + if(CollUtil.isEmpty(dateList)){ + return; + } + jsonData.setLeaveDays(BigDecimal.valueOf(dateList.size())); + jsonData.setLeaveHours(BigDecimal.valueOf(dateList.size()).multiply(ratioMap.getOrDefault(userId, BigDecimal.ZERO)).stripTrailingZeros()); + map.put(field, jsonData); + }); + if (CollUtil.isEmpty(map)) { + return; + } + result.put(userId, map); + }); + return result; + } + + /** + * 构建加班节假日数据 + * + * @param overtimeHolidaysInfoList 加班节假日换休数据 + * @param overtimeSalaryJson 加班节假日换薪数据 + * @param ratioMap 比例数据 + * @return 加班节假日数据 + */ + public static Map> buildOvertimeHolidaysMap(List overtimeHolidaysInfoList, List overtimeSalaryJson, Map ratioMap) { + if (CollUtil.isEmpty(overtimeHolidaysInfoList) && CollUtil.isEmpty(overtimeSalaryJson)) { + return new HashMap<>(); + } + // 同一天会存在多个节假日 需要拆分出来 + List splitHolidayList = CollUtil.isNotEmpty(overtimeHolidaysInfoList) ? overtimeHolidaysInfoList.stream().flatMap(info -> { + if (StringUtil.isNotEmpty(info.getHolidayNameList())) { + return Arrays.stream(info.getHolidayNameList().split("@")).map(holidayName -> { + OvertimeHolidaysInfoModel newInfo = new OvertimeHolidaysInfoModel(); + BeanUtils.copyProperties(info, newInfo); + newInfo.setHolidayNameList(holidayName); + return newInfo; + }); + } else { + return Stream.of(info); + } + }).collect(Collectors.toList()) : new ArrayList<>(); + if (CollUtil.isNotEmpty(overtimeSalaryJson)) { + splitHolidayList.addAll(overtimeSalaryJson.stream().flatMap(info -> { + OvertimeHolidaysInfoModel newInfo = new OvertimeHolidaysInfoModel(); + newInfo.setUserId(info.getUserId()); + newInfo.setHolidaysOvertimeDays(info.getHolidaysOvertimeSalaryDays()); + newInfo.setHolidayNameList(info.getFestivalStr()); + if (StringUtil.isNotEmpty(info.getFestivalStr())) { + return Arrays.stream(info.getFestivalStr().split("@")).map(holidayName -> { + OvertimeHolidaysInfoModel newInfo1 = new OvertimeHolidaysInfoModel(); + newInfo1.setUserId(info.getUserId()); + newInfo1.setHolidayNameList(holidayName); + newInfo1.setHolidaysOvertimeDays(info.getHolidaysOvertimeSalaryDays()); + return newInfo1; + }); + } else { + return Stream.of(newInfo); + } + }).collect(Collectors.toList())); + } + if (CollUtil.isEmpty(splitHolidayList)) { + return new HashMap<>(); + } + return splitHolidayList.stream().collect(Collectors.groupingBy(OvertimeHolidaysInfoModel::getUserId, Collectors.groupingBy(OvertimeHolidaysInfoModel::getHolidayNameList, Collectors.collectingAndThen(Collectors.toList(), list -> { + if (CollUtil.isEmpty(list)) { + return null; + } + OvertimeHolidaysInfoModel infoModel = list.get(0); + String userId = infoModel.getUserId(); + HolidaysTypeJsonData1 jsonData = new HolidaysTypeJsonData1(); + jsonData.setField(infoModel.getHolidayNameList()); + jsonData.setFieldName(infoModel.getHolidayNameList()); + BigDecimal days = list.stream().map(OvertimeHolidaysInfoModel::getHolidaysOvertimeDays).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal ratio = ratioMap.getOrDefault(userId, BigDecimal.ZERO); + jsonData.setLeaveDays(days); + jsonData.setLeaveHours(days.multiply(ratio).stripTrailingZeros()); + return jsonData; + })))).entrySet().stream().filter(e -> CollUtil.isNotEmpty(e.getValue())).collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().entrySet().stream().filter(inner -> inner.getValue() != null).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + } + + /** + * 将 ConfirmDetailsInfo 对象转换为 Map + * @param info ConfirmDetailsInfo 实例 + * @return Map + */ + @SneakyThrows + public static Map convertToMap(T info) { + Map resultMap = new HashMap<>(); + Class clazz = info.getClass(); + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); + if ("customLeaveList".equals(field.getName()) && Objects.nonNull(field.get(info))) { + Object fieldValue = field.get(info); + List customLeaveList = new ArrayList<>(); + if (fieldValue instanceof List) { + ObjectMapper mapper = new ObjectMapper(); + List rawList = (List) fieldValue; + for (Object item : rawList) { + if (item instanceof LeaveTypeJsonData) { + customLeaveList.add((LeaveTypeJsonData) item); + } else { + // 如果是Map类型,可以转换 + LeaveTypeJsonData data = mapper.convertValue(item, LeaveTypeJsonData.class); + customLeaveList.add(data); + } + } + } + if (CollUtil.isNotEmpty(customLeaveList)) { + customLeaveList.forEach(item -> { + resultMap.put(item.getField() + "&" + item.getFieldName(), item.getLeaveDays()); + }); + } + continue; + } + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty != null && excelProperty.value().length > 0) { + String key = excelProperty.value()[0]; + try { + Object value = field.get(info); + resultMap.put(field.getName() + "&" + key, value); + } catch (IllegalAccessException e) { + log.error("获取字段值失败: {}", field.getName(), e); + } + } + } + return resultMap; + } + + /** + * 判断当前时间是否等于指定的时间 + * @param sealTime 指定的时间 + * @return true 表示当前时间等于指定的时间,false 表示当前时间不等于指定的时间 + */ + public static boolean isCurrentTimeEqualsSealTime(String sealTime) { + if (sealTime == null || sealTime.isEmpty()) { + return false; + } + try { + // 获取当前时间 + Calendar now = Calendar.getInstance(); + // 解析 sealTime + String[] timeParts = sealTime.split(":"); + int sealHour = Integer.parseInt(timeParts[0]); + int sealMinute = Integer.parseInt(timeParts[1]); + // 比较小时和分钟 + return now.get(Calendar.HOUR_OF_DAY) == sealHour && now.get(Calendar.MINUTE) == sealMinute; + } catch (Exception e) { + return false; + } + } + + /** + * 计算班次占比 + * + * @param dataList 日常班次 + * @param attendanceRatio 出勤换算比 + * @param payrollProportion 当天划线排班计薪工时占比MutablePair<计薪工时小时数, 计薪工时小时数/分钟> + * @return 班次占比 + */ + public static Map>> processClassTypeData(List dataList, BigDecimal attendanceRatio, MutablePair payrollProportion) { + if (CollUtil.isEmpty(dataList)) { + return new HashMap<>(); + } + Map>> result = new HashMap<>(); + // 排除掉加班班次(因为只有普通班和请假才会算班次占比) + dataList = dataList.stream().filter(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode()) || dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())).collect(Collectors.toList()); + // 判断当天是否为划线排班 + boolean isUnderlineScheduling = dataList.stream().anyMatch(item -> item.getFixedMark().equals(2)); + if (isUnderlineScheduling) { + result.put(-1, MutablePair.of(attendanceRatio, payrollProportion)); + return result; + } + Map> classTypeMap = CollUtil.isNotEmpty(dataList) ? dataList.stream().filter(dailyRule -> Objects.nonNull(dailyRule.getSchedulesType())).collect(Collectors.groupingBy(UserDaySituationData::getSchedulesType)) : new HashMap<>(); + for (Map.Entry> entry : classTypeMap.entrySet()) { + // 请假是否全覆盖 全天班、上午班、下午班 + boolean isHasAllLeave = entry.getValue().stream().allMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())); + int payrollMinute = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getValidDuration).mapToInt(Integer::intValue).sum(); + BigDecimal payrollHours; + BigDecimal periodWorkDay; + if (isHasAllLeave) { + periodWorkDay = entry.getValue().stream().map(UserDaySituationData::getLeaveDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + payrollHours = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getPayrollHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + } else { + // 全天班、上午班、下午班班次内是否有请假,有的话,普通班的天数只会会统计一次 + boolean isHasLeave = entry.getValue().stream().anyMatch(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.LEAVE.getCode())); + if (isHasLeave) { + periodWorkDay = entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).findFirst().orElse(BigDecimal.ZERO); + payrollHours = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getPayrollHours).findFirst().orElse(BigDecimal.ZERO); + entry.getValue().removeIf(dailyRule -> dailyRule.getAttendanceType().equals(AttendanceTypeEnum.ORDINARY.getCode())); + if (CollUtil.isNotEmpty(entry.getValue())) { + periodWorkDay = periodWorkDay.add(entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add)); + } + } else { + // 公休天数+普班天数 + periodWorkDay = entry.getValue().stream().map(UserDaySituationData::getPeriodWorkDay).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + payrollHours = entry.getValue().stream().filter(dailyRule -> !dailyRule.getAttendanceType().equals(AttendanceTypeEnum.REST.getCode())).map(UserDaySituationData::getPayrollHours).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + } + } + if (periodWorkDay.compareTo(BigDecimal.ZERO) > 0) { + MutablePair mutablePair = MutablePair.of(payrollHours, BigDecimal.ZERO); + if (payrollHours.compareTo(BigDecimal.ZERO) > 0 && payrollMinute > 0) { + mutablePair = MutablePair.of(payrollHours, payrollHours.divide(BigDecimal.valueOf(payrollMinute), 6, RoundingMode.HALF_UP)); + } + result.put(entry.getKey(), MutablePair.of(BigDecimal.valueOf(payrollMinute).divide(periodWorkDay, 2, RoundingMode.HALF_UP), mutablePair)); + } + } + return result; + } + + /** + * 处理请假、调休数据 + * @param statisticsListVo 日常统计数据 + * @param leaveTypeMap 请假数据 + */ + public static void processLeaveOrCompensationData(AttendanceDayStatistics statisticsListVo, Map leaveTypeMap) { + if (CollUtil.isEmpty(leaveTypeMap)) { + return; + } + statisticsListVo.setLeaveDays(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getLeaveDays).reduce(BigDecimal::add).orElse(BigDecimal.ZERO)); + statisticsListVo.setLeaveHours(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getLeaveHours).reduce(BigDecimal::add).orElse(BigDecimal.ZERO)); + statisticsListVo.setLeavePayrollHours(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getLeavePayrollHours).reduce(BigDecimal::add).orElse(BigDecimal.ZERO)); + statisticsListVo.setLeaveBatchNumber(leaveTypeMap.values().stream().map(LeaveTypeStaModel::getApplyList).flatMap(Collection::stream).map(LeaveTypeStaDetailsModel::getApplyId).collect(Collectors.joining(","))); + // 获取当天的调休数据 + LeaveTypeStaModel compensationInfo = CollUtil.isNotEmpty(leaveTypeMap) ? leaveTypeMap.values().stream().filter(item -> Objects.nonNull(item.getIsRest()) && item.getIsRest() == 1).findFirst().orElse(null) : null; + if (Objects.nonNull(compensationInfo)) { + statisticsListVo.setCompensationHours(compensationInfo.getLeaveHours()); + statisticsListVo.setCompensationPayrollHours(compensationInfo.getLeavePayrollHours()); + statisticsListVo.setCompensationDays(compensationInfo.getLeaveDays()); + statisticsListVo.setCompensationTimes(compensationInfo.getApplyList().size()); + } + } + + /** + * 获取自定义请假数据 + * @param leaveTypeNameMap 请假数据 + * @param leaveTypeMap 请假数据 + * @return 自定义请假数据 + */ + public static String getCustomLeaveJson(Map leaveTypeNameMap, Map leaveTypeMap) { + List leaveTypeJsonDataList = CollUtil.newArrayList(); + for (Map.Entry entry : leaveTypeMap.entrySet()) { + if (!leaveTypeMap.containsKey(entry.getKey())) { + continue; + } + LeaveTypeJsonData typeJsonData = LeaveTypeJsonData.builder().field(entry.getKey()) + .fieldName(leaveTypeNameMap.getOrDefault(entry.getKey(), "未知请假类型")) + .headName(leaveTypeNameMap.getOrDefault(entry.getKey(), "未知请假类型")) + .leaveDays(entry.getValue().getLeaveDays()).leaveHours(entry.getValue() + .getLeaveHours()).leavePayrollHours(entry.getValue().getLeavePayrollHours()) + .applyList(entry.getValue().getApplyList()).build(); + leaveTypeJsonDataList.add(typeJsonData); + } + return JSON.toJSONString(leaveTypeJsonDataList); + } + + /** + * 解析请假批次号字符串,支持多种格式 + * 格式1: [111,222] 或 ["111","222"] + * 格式2: 11111,222 + * + * @param batchNumber 批次号字符串 + * @return 解析后的批次号列表 + */ + public static List parseBatchNumbers(String batchNumber) { + if (batchNumber == null || batchNumber.isEmpty()) { + return Collections.emptyList(); + } + + if (batchNumber.startsWith("[") && batchNumber.endsWith("]")) { + // 处理数组格式: [111,222] 或 ["111","222"] + String content = batchNumber.substring(1, batchNumber.length() - 1); + return Arrays.stream(content.split(",")).map(String::trim).map(s -> s.replaceAll("^\"|\"$", "")) // 去除引号 + .filter(s -> !s.isEmpty()).collect(Collectors.toList()); + } else { + // 处理逗号分隔格式: 11111,222 + return Arrays.stream(batchNumber.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList()); + } + } + + /** + * 解析外出批次号字符串,支持多种格式 + * 格式1: [111,222] 或 ["111","222"] + * 格式2: 11111,222 + * + * @param batchNumber 批次号字符串 + * @return 解析后的批次号列表 + */ + public static List parseBatchNumber(String batchNumber) { + if (batchNumber == null || batchNumber.isEmpty()) { + return Collections.emptyList(); + } + if (batchNumber.startsWith("[") && batchNumber.endsWith("]")) { + List shiftsJsonVoList = parseArray(batchNumber, BatchNumberResult.class); + return shiftsJsonVoList.stream().map(BatchNumberResult::getBatchNumberId).distinct().collect(Collectors.toList()); + } else { + return Arrays.stream(batchNumber.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList()); + } + } + + /** + * 获取空数据列表 + * + * @param pageListDto 分页参数 + * @return 空数据列表 + */ + public static PageListVO returnEmptyList(CultivatePage pageListDto) { + PageListVO pageInfo = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(pageListDto.getCurrentPage()); + pagination.setPageSize(pageListDto.getPageSize()); + pagination.setTotal(0); + pageInfo.setList(new ArrayList<>()); + pageInfo.setPagination(pagination); + return pageInfo; + } + + /** + * 获取数据列表 + * + * @param listVoPage 分页信息 + * @param pageListDto 分页参数 + * @return 数据列表 + */ + public static PageListVO returnList(Page listVoPage, CultivatePage pageListDto) { + PageListVO pageListVO = new PageListVO<>(); + PaginationVO pagination = new PaginationVO(); + pagination.setCurrentPage(pageListDto.getCurrentPage()); + pagination.setPageSize(pageListDto.getPageSize()); + pagination.setTotal(Math.toIntExact(listVoPage.getTotal())); + pageListVO.setList(listVoPage.getRecords()); + pageListVO.setPagination(pagination); + return pageListVO; + } + + /** + * 获取指定月份的开始时间和结束时间 + * + * @param startMonth 开始月份 + * @param endMonth 结束月份 + * @return 包含开始时间和结束时间的MutablePair对象 + */ + public static MutablePair getDateBetweenByMonth(String startMonth, String endMonth) { + DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM", Locale.ENGLISH); + YearMonth yearStartMonth = YearMonth.parse(startMonth, monthFormatter); + LocalDate startDate = yearStartMonth.atDay(1); + YearMonth yearEndMonth = YearMonth.parse(endMonth, monthFormatter); + LocalDate endDate = yearEndMonth.atEndOfMonth().isAfter(LocalDate.now()) ? LocalDate.now() : yearEndMonth.atEndOfMonth(); + return MutablePair.of(startDate, endDate); + } + + /** + * 计算增长率 + * 公式:(本期数 - 同期数) / 同期数 * 100 + * + * @param currentPeriod 本期数 + * @param previousPeriod 同期数 + * @return 增长率百分比 + */ + public static BigDecimal calculateGrowthRate(BigDecimal currentPeriod, BigDecimal previousPeriod) { + if (currentPeriod == null) { + currentPeriod = BigDecimal.ZERO; + } + if (previousPeriod == null || previousPeriod.compareTo(BigDecimal.ZERO) == 0) { + // 如果同期数为 0,则无法计算增长率 + return currentPeriod.compareTo(BigDecimal.ZERO) > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO; + } + // 增长率 = (本期数 - 同期数) / 同期数 * 100 + return currentPeriod.subtract(previousPeriod).divide(previousPeriod, 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)); + } + + /** + * 获取两个月份之间的所有月份列表 + * + * @param startMonth 开始月份(yyyy-MM) + * @param endMonth 结束月份(yyyy-MM) + * @return 月份列表 + */ + public static List getAllMonthsBetween(String startMonth, String endMonth) { + List months = new ArrayList<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM"); + YearMonth start = YearMonth.parse(startMonth, formatter); + YearMonth end = YearMonth.parse(endMonth, formatter); + while (!start.isAfter(end)) { + months.add(start.format(formatter)); + start = start.plusMonths(1); + } + return months; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ExpiresTimeUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ExpiresTimeUtil.java new file mode 100644 index 0000000..528ed0a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ExpiresTimeUtil.java @@ -0,0 +1,136 @@ +package jnpf.util.attendance; + +import jnpf.util.DateUtil; +import org.springframework.stereotype.Component; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.Month; +import java.time.Period; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.Calendar; +import java.util.Date; + +/** + * @author panpan + */ +@Component +public class ExpiresTimeUtil { + + /** + * 计算过期时间 + * @param lifespanType 生效类型 0.永久有效 1.固定天数 2.指定日期 + * @param fixedDay 固定天数 + * @param specifyDay 指定日期 格式MM-dd + * @return 过期时间 + */ + public Date getExpiresTime(Integer lifespanType, Integer fixedDay, String specifyDay){ + //永久有效 1-固定天数 (当前时间加上设置的天数) 2-指定日期 拼接今年的年判断有无过去,过去后取下一年,没有过去取今年 + if (lifespanType == 0){ + return null; + } + SimpleDateFormat yyyy = new SimpleDateFormat("yyyy" ); + SimpleDateFormat yMd = new SimpleDateFormat("yyyy-MM-dd" ); + SimpleDateFormat yMdHms = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); + SimpleDateFormat hms = new SimpleDateFormat("HH:mm:ss" ); + if (lifespanType == 1){ + return DateUtil.dateAddDays(new Date(), fixedDay); + } + if (lifespanType == 2){ + Date time = new Date(); + String year = yyyy.format(time); + String expiresTime = year + "-" + specifyDay + " " + hms.format(time); + System.out.println("拼接时间: "+expiresTime); + String today = yMd.format(time); + System.out.println("今天: "+today); + try { + if (yMd.parse(expiresTime).getTime() <= yMd.parse(today).getTime()){ + System.out.println("已过期"); + // 获取下一年的指定时间 + Calendar c = Calendar.getInstance(); + c.setTime(yMdHms.parse(expiresTime)); + c.set(Calendar.YEAR, c.get(Calendar.YEAR) + 1); + return c.getTime(); + } else { + return yMdHms.parse(expiresTime); + } + } catch (ParseException e) { + throw new RuntimeException(e); + } + + } + return DateUtil.dateAddDays(new Date(), 365); + } + + + + /** + * 计算员工从入职日期到现在的司龄返回月数 + * + * @param startDate 员工入职日期 + * @return 司龄(单位:月) + */ + public Integer calculateSeniorityMonth(Date startDate) { + // 参数校验 + if (startDate == null ) { + return 1; + } + // 入职日期 + LocalDate startLocalDate = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + // 当前日期 + LocalDate currentDate = LocalDate.now(); + // 确保开始日期不晚于当前日期 + if (startLocalDate.isAfter(currentDate)) { + return 1; + } + Period period = Period.between(startLocalDate, currentDate); + int years = period.getYears(); + int months = period.getMonths(); + int days = period.getDays(); + // 返回司龄(单位:月) 哪怕是一天,也算一个月 + return years * 12 + months + (0 != days ? 1 : 0); + } + + // 获取指定日期的日 + /** + * 检查指定日期是否在当前日期的月份内 + * @param startDate 指定日期 + * @param currentDate 当前日期 + * @return true 在当前日期的月份内 false 不在当前日期的月份内 + */ + public boolean checkMonthDay(Date startDate,LocalDate currentDate) { + // 当前日期 + int monthValue = currentDate.getMonthValue(); + int day = currentDate.getDayOfMonth(); + // 当前月份 最后一天的日 + int monthLastDay = currentDate.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); + // 返回入参对应的月份 + LocalDate startLocalDate = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + int checkMonth = startLocalDate.getMonthValue(); + int checkDay = startLocalDate.getDayOfMonth(); + return checkMonth == monthValue && (checkDay == day || (day == monthLastDay && checkDay >= day)); + } + + + /** + * 检查指定日期是否在当前日期的月份内 + * @param startDate 指定日期 + * @param monthDayStr 格式MM-dd + * @return true 在当前日期的月份内 false 不在当前日期的月份内 + */ + public boolean checkMonthDayStr(Date startDate,String monthDayStr) { + String[] monthDay = monthDayStr.split("-"); + int month = Integer.parseInt(monthDay[0]); + int day = Integer.parseInt(monthDay[1]); + // 获取今年对应月份的最后一天日 + int monthLastDay = LocalDate.now().withMonth(month).with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); + // 当前月份 最后一天的日 + // 返回入参对应的月份 + LocalDate startLocalDate = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + int checkMonth = startLocalDate.getMonthValue(); + int checkDay = startLocalDate.getDayOfMonth(); + return checkMonth == month && (checkDay == day || (day >= monthLastDay && checkDay >= day)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataDetails.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataDetails.java new file mode 100644 index 0000000..f1aff6a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataDetails.java @@ -0,0 +1,74 @@ +package jnpf.util.attendance; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 票据核验-库区详情 + * + * @author yew + * @version v1.0 + * @date 2020-11-06 10:15 + */ +@Data +public class GetPdfPrintDataDetails { + /** + * 烟叶编码 + */ + private String sxCode; + /** + * 烟叶名称 + */ + private String sxName; + /** + * 类型 + */ + private String lxName; + /** + * 年份 + */ + private String year; + /** + * 产地 + */ + private String cdName; + /** + * 等级 + */ + private String djName; + /** + * 品种 + */ + private String pzName; + /** + * 特标 + */ + private String tbName; + /** + * 批次号 + */ + private String batchCode; + /** + * 数量 + */ + private Integer totalNum; + /** + * 重量 + */ + private BigDecimal totalWeight; + /** + * 库存区域 + */ + private String dwName; + /** + * 质检结果 + */ + private Integer qualityResult; + /** + * 现场抽样结果 (-1 :未抽样 0:不合格 1:合格) + */ + private Integer samplingResult; + private String remark; + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataModel.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataModel.java new file mode 100644 index 0000000..99cce1d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/GetPdfPrintDataModel.java @@ -0,0 +1,56 @@ +package jnpf.util.attendance; + +import lombok.Data; + +import java.util.List; + +/** + * 票据核验-库区详情 + * + * @author yew + * @version v1.0 + * @date 2020-11-06 10:15 + */ +@Data +public class GetPdfPrintDataModel { + /** + * 文档编号 + */ + private String docCode; + /** + * 烟厂名称 + */ + private String ycName; + /** + * 申报部门 + */ + private String declareDepartment; + /** + * 仓库名称 + */ + private String ckName; + /** + * 来货单位 + */ + private String shippingUnit; + /** + * 到货日期 + */ + private String arrivalDate; + /** + * 申报日期 + */ + private String declareDate; + /** + * 申报人 + */ + private String declareOpt; + /** + * 联系电话 + */ + private String phone; + /** + * 数据集合 + */ + private List details; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ImageCompressUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ImageCompressUtil.java new file mode 100644 index 0000000..4f18dd8 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/ImageCompressUtil.java @@ -0,0 +1,88 @@ +package jnpf.util.attendance; + +import net.coobird.thumbnailator.Thumbnails; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +/** + * 图片压缩 + * + * @author yanwenfu + * @create 2025-12-11 + */ +public class ImageCompressUtil { + + /** + * 压缩人脸图片 + * @param input 原图 + * @param output 输出文件 + * @param targetKB 目标大小 KB + * @throws Exception 异常 + */ + public static void compressFace(File input, File output, long targetKB) throws Exception { + compressResizeAndQuality(input, output, targetKB, 0.9f, 900); + } + + /** + * 压缩缩略图 + * @param input 原图 + * @param output 输出文件 + * @param targetKB 目标大小 KB + * @throws Exception 异常 + */ + public static void compressThumbnail(File input, File output, long targetKB) throws Exception { + compressResizeAndQuality(input, output, targetKB, 0.7f, 300); + } + + /** + * 核心压缩方法:按最大边长缩放 + 固定质量 + * + * @param input 原文件 + * @param output 输出文件 + * @param targetKB 目标大小 KB + * @param quality JPEG质量(0~1) + * @param maxSide 最大边长 + * @throws Exception + */ + private static void compressResizeAndQuality(File input, File output, long targetKB, float quality, int maxSide) throws Exception { + + long targetBytes = targetKB * 1024; + BufferedImage img = ImageIO.read(input); + int width = img.getWidth(); + int height = img.getHeight(); + // 计算缩放比例 + double scale = 1.0; + if (width > maxSide || height > maxSide) { + scale = Math.min((double) maxSide / width, (double) maxSide / height); + } + // 是否是 jpg + String name = input.getName().toLowerCase(); + boolean isJpg = name.endsWith(".jpg") || name.endsWith(".jpeg"); + // ===== 性能直通:jpg + 小图 + 不缩放 ===== + if (isJpg && scale == 1.0 && input.length() <= targetBytes) { + Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + return; + } + // ===== 统一编码路径:转 RGB(解决 PNG 透明) ===== + BufferedImage rgbImage = new BufferedImage( + img.getWidth(), + img.getHeight(), + BufferedImage.TYPE_INT_RGB + ); + Graphics2D g = rgbImage.createGraphics(); + g.setColor(Color.WHITE); // 背景色,可按需要调整 + g.fillRect(0, 0, width, height); + g.drawImage(img, 0, 0, null); + g.dispose(); + // Thumbnails 一步到位:缩放 + 固定质量 + Thumbnails.of(rgbImage) + .scale(scale) + .outputQuality(quality) + .toFile(output); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MachineStrategyFactory.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MachineStrategyFactory.java new file mode 100644 index 0000000..f6c6f7d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MachineStrategyFactory.java @@ -0,0 +1,26 @@ +package jnpf.util.attendance; + +import jnpf.attendance.service.MachineStrategy; +import jnpf.enums.attendance.MachineEnum; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 考勤机策略工厂 + * + * @author yanwenfu + * @create 2021-01-26 + */ +@Component +public class MachineStrategyFactory { + + @Autowired + private Map machineStrategyMap; + + public MachineStrategy getMachineStrategy(MachineEnum machine) { + + return machineStrategyMap.get(machine.getDescription()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttPushClient.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttPushClient.java new file mode 100644 index 0000000..1819ccc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttPushClient.java @@ -0,0 +1,181 @@ +package jnpf.util.attendance; + +import jnpf.config.MqttConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.io.UnsupportedEncodingException; + +/** + * mqtt push 客户端 + * + * @author yanwenfu + * @create 2024-04-09 + */ +@Slf4j +public class MqttPushClient { + + private static MqttClient client; + + public static MqttClient getClient() { + return client; + } + + public static void setClient(MqttClient client) { + MqttPushClient.client = client; + } + + /** + * 编辑连接信息 + * @param userName + * @param password + * @param outTime + * @param KeepAlive + * @return + */ + private MqttConnectOptions getOption(String userName, String password, int outTime, int KeepAlive) { + //MQTT连接设置 + MqttConnectOptions option = new MqttConnectOptions(); + //设置是否清空session,false表示服务器会保留客户端的连接记录,true表示每次连接到服务器都以新的身份连接 + option.setCleanSession(false); + //设置连接的用户名 + option.setUserName(userName); + //设置连接的密码 + option.setPassword(password.toCharArray()); + //设置超时时间 单位为秒 + option.setConnectionTimeout(outTime); + //设置会话心跳时间 单位为秒 服务器会每隔(1.5*keepTime)秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制 + option.setKeepAliveInterval(KeepAlive); + //setWill方法,如果项目中需要知道客户端是否掉线可以调用该方法。设置最终端口的通知消息 + //option.setWill(topic, "close".getBytes(StandardCharsets.UTF_8), 2, true); + option.setMaxInflight(1000); + return option; + } + + /** + * 发起连接 + */ + public void connect(MqttConfiguration mqttConfiguration) { + MqttClient client; + try { + client = new MqttClient(mqttConfiguration.getHost(), mqttConfiguration.getClientId(), new MemoryPersistence()); + MqttConnectOptions options = getOption(mqttConfiguration.getUsername(), mqttConfiguration.getPassword(), + mqttConfiguration.getTimeout(), mqttConfiguration.getKeepAlive()); + MqttPushClient.setClient(client); + try { + client.setCallback(new PushCallback(this, mqttConfiguration)); + if (!client.isConnected()) { + client.connect(options); + log.info("================>>>MQTT连接成功<<======================"); + } else {//这里的逻辑是如果连接不成功就重新连接 + client.disconnect(); + client.connect(options); + log.info("===================>>>MQTT断连成功<<<======================"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + /** + * 断线重连 + * + * @throws Exception + */ + public Boolean reConnect() throws Exception { + Boolean isConnected = false; + if (null != client) { + client.connect(); + if (client.isConnected()) { + isConnected = true; + } + } + return isConnected; + } + /** + * 发布,默认qos为0,非持久化 + * + * @param topic + * @param pushMessage + */ + public void publish(String topic, String pushMessage) { + + publish(0, false, topic, pushMessage); + + } + /** + * 发布 + * + * @param qos + * @param retained + * @param topic + * @param pushMessage + */ + public void publish(int qos, boolean retained, String topic, String pushMessage) { + MqttMessage message = new MqttMessage(); + message.setQos(qos); + message.setRetained(retained); + try { + message.setPayload(pushMessage.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + MqttTopic mTopic = MqttPushClient.getClient().getTopic(topic); + if (null == mTopic) { + log.error("===============>>>MQTT topic 不存在<<======================="); + } + MqttDeliveryToken token; + try { + token = mTopic.publish(message); + token.waitForCompletion(); + } catch (MqttPersistenceException e) { + e.printStackTrace(); + } catch (MqttException e) { + e.printStackTrace(); + } + } + /** + * 发布消息的服务质量(推荐为:2-确保消息到达一次。0-至多一次到达;1-至少一次到达,可能重复), + * retained 默认:false-非持久化(是指一条消息消费完,就会被删除;持久化,消费完,还会保存在服务器中,当新的订阅者出现,继续给新订阅者消费) + * + * @param topic + * @param pushMessage + */ + public void publish(int qos, String topic, String pushMessage) { + + publish(qos, false, topic, pushMessage); + + } + + /** + * + * 订阅多个主题 + * + * @param topic + * @param qos + */ + public void subscribe(String[] topic, int[] qos) { + try { + MqttPushClient.getClient().unsubscribe(topic); + MqttPushClient.getClient().subscribe(topic, qos); + } catch (MqttException e) { + e.printStackTrace(); + } + } + + /** + * 清空主题 + * @param topic + */ + public void cleanTopic(String topic) { + try { + MqttPushClient.getClient().unsubscribe(topic); + } catch (MqttException e) { + log.error(e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttSender.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttSender.java new file mode 100644 index 0000000..30e8314 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/MqttSender.java @@ -0,0 +1,73 @@ +package jnpf.util.attendance; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.*; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * 消息发布 + * + * @author yanwenfu + * @create 2024-04-09 + */ +@Component(value = "mqttSender") +@Slf4j +public class MqttSender { + + @Async + public void send(String queueName, String msg) { + log.info("=====================>>>>发送主题"+queueName); + publish(2,queueName, msg); + } + + /** + * 发布,默认qos为0,非持久化 + * @param topic + * @param pushMessage + */ + public void publish(String topic, String pushMessage){ + + publish(1, false, topic, pushMessage); + } + + /** + * 发布 + * @param qos + * @param retained + * @param topic + * @param pushMessage + */ + public void publish(int qos,boolean retained,String topic,String pushMessage) { + MqttMessage message = new MqttMessage(); + message.setQos(qos); + message.setRetained(retained); + message.setPayload(pushMessage.getBytes(StandardCharsets.UTF_8)); + MqttTopic mTopic = MqttPushClient.getClient().getTopic(topic); + if(null == mTopic){ + log.error("===================>>>MQTT topic 不存在<<================="); + } + MqttDeliveryToken token; + try { + token = mTopic.publish(message); + token.waitForCompletion(); + } catch (MqttPersistenceException e) { + log.error("============>>>publish fail",e); + e.printStackTrace(); + } catch (MqttException e) { + e.printStackTrace(); + } + } + + /** + * 发布消息的服务质量(推荐为:2-确保消息到达一次。0-至多一次到达;1-至少一次到达,可能重复), + * retained 默认:false-非持久化(是指一条消息消费完,就会被删除;持久化,消费完,还会保存在服务器中,当新的订阅者出现,继续给新订阅者消费) + * @param topic + * @param pushMessage + */ + public void publish(int qos, String topic, String pushMessage){ + publish(qos, false, topic, pushMessage); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/OcsWatermarkUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/OcsWatermarkUtils.java new file mode 100644 index 0000000..160b138 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/OcsWatermarkUtils.java @@ -0,0 +1,35 @@ +package jnpf.util.attendance; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Base64; + +@Slf4j +public class OcsWatermarkUtils { + + /** + * 水印样式 + */ + private static final String OCS_WATERMARK = "?watermark/2/text/%s/fill/I0Y1RjVGNQ==/fontsize/100/dissolve/17/gravity/northeast/dx/20/dy/20/batch/1/spacing/100/degree/45"; + + /** + * 生成样式URL + * + * @param value 水印文案 + * @return 返回完整水印图片url + */ + public static String watermarkByStr(String value) { + return String.format(OCS_WATERMARK, processBase64String(convertToBase64(value))); + } + + public static String convertToBase64(String imagePath) { + return Base64.getEncoder().encodeToString(imagePath.getBytes()); + } + + public static String processBase64String(String base64EncodedString) { + String modifiedString = base64EncodedString.replace("+", "-"); + modifiedString = modifiedString.replace("/", "_"); + modifiedString = modifiedString.replace("=", ""); + return modifiedString; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PermissionUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PermissionUtil.java new file mode 100644 index 0000000..69b64d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PermissionUtil.java @@ -0,0 +1,19 @@ +package jnpf.util.attendance; + +import java.util.HashSet; +import java.util.Set; + +/** + * 权限工具类 + */ +public class PermissionUtil { + + /** 获取权限模版*/ + public static Set getPermissionTemplate(String... permissions) { + Set permissionSet = new HashSet<>(); + permissionSet.forEach(permission -> { + permissionSet.add(permission); + }); + return permissionSet; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PushCallback.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PushCallback.java new file mode 100644 index 0000000..d86735e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/PushCallback.java @@ -0,0 +1,76 @@ +package jnpf.util.attendance; + +import jnpf.config.MqttConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * PushCallback + * + * @author yanwenfu + * @create 2024-04-09 + */ +@Slf4j +public class PushCallback implements MqttCallback { + + private MqttPushClient client; + private MqttConfiguration mqttConfiguration; + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + public PushCallback(MqttPushClient client, MqttConfiguration mqttConfiguration) { + this.client = client; + this.mqttConfiguration = mqttConfiguration; + } + + @Override + public void connectionLost(Throwable cause) { + // 连接丢失后,一般在这里面进行重连 + if(client != null) { + scheduler.scheduleAtFixedRate(() -> { + try { + MqttPushClient mqttPushClient = new MqttPushClient(); + mqttPushClient.connect(mqttConfiguration); + if(MqttPushClient.getClient().isConnected()) { + log.info("=============>>重连成功"); + scheduler.shutdown(); + } + } catch (Exception e) { + log.error("=============>>>[MQTT] 连接断开,重连失败!<<============="); + } + }, 20, 20, TimeUnit.SECONDS); + } + log.info(cause.getMessage()); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { + //publish后会执行到这里 + log.info("publish后会执行到这里"); + log.info("pushComplete==============>>>" + token.isComplete()); + } + + /** + * 监听对应的主题消息 + * @param topic + * @param message + * @throws Exception + */ + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + // subscribe后得到的消息会执行到这里面 + String Payload = new String(message.getPayload()); + log.info("============》》接收消息主题 : " + topic); + log.info("============》》接收消息Qos : " + message.getQos()); + log.info("============》》接收消息内容 : " + Payload); + log.info("============》》接收ID : " + message.getId()); + log.info("接收数据结束 下面可以执行数据处理操作"); + } +} \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleExcelImportUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleExcelImportUtil.java new file mode 100644 index 0000000..fc148f1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleExcelImportUtil.java @@ -0,0 +1,25 @@ +package jnpf.util.attendance; + +import jnpf.enums.attendance.ScheduleImportEnum; +import jnpf.model.attendance.model.ScheduleImportFailModel; +import org.apache.commons.compress.utils.Lists; + +import java.util.ArrayList; +import java.util.List; + +public class RuleExcelImportUtil { + + public static Boolean addFail(Boolean isTrue, ScheduleImportEnum scheduleImportEnum, String userName, List failList) { + if (!isTrue) { + return false; + } + if (failList.stream().anyMatch(model -> model.getError().equals(scheduleImportEnum))) { + failList.stream().filter(model -> model.getError().equals(scheduleImportEnum)).findFirst().ifPresent(scheduleImportFailModel -> scheduleImportFailModel.getUserNames().add(userName)); + return true; + } + ArrayList objects = Lists.newArrayList(); + objects.add(userName); + failList.add(ScheduleImportFailModel.builder().error(scheduleImportEnum).userNames(objects).build()); + return true; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleScopeUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleScopeUtil.java new file mode 100644 index 0000000..2203ad3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/RuleScopeUtil.java @@ -0,0 +1,443 @@ +package jnpf.util.attendance; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.google.common.collect.Lists; +import jnpf.attendance.service.RuleScopeService; +import jnpf.base.ActionResult; +import jnpf.entity.attendance.AttendanceRuleScope; +import jnpf.enums.attendance.ScopeBizType; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.UpdateChangeDto; +import jnpf.model.attendance.dto.UserOrgDto; +import jnpf.model.personnels.dto.turnover.FtbDepUserDTO; +import jnpf.permission.V2UserApi; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.personnels.service.FtbPersonnelsTurnoverManagementService; +import jnpf.util.ConstantUtil; +import jnpf.util.FtbUtil; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.*; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +/** + * 规则适配范围工具类 + * + * @author yanwenfu + * @create 2025-09-18 + */ +@Component +public class RuleScopeUtil { + + @Resource + private RuleScopeService ruleScopeService; + @Autowired + private V2UserApi v2UserApi; + @Resource + private FtbPersonnelsTurnoverManagementService ftbPersonnelsTurnoverManagementService; + + + public AttendanceRuleScope getRuleScope(String ruleId, String value, Integer ruleScope, ScopeBizType scopeBizType) { + return getRuleScope(ruleId, value, ruleScope, scopeBizType, null); + } + /** + * 生成适配范围 + * + * @param value 值 + * @param ruleScope 适配范围 + * @param scopeBizType 业务类型 + * @return jnpf.entity.attendance.AttendanceRuleScope + */ + public AttendanceRuleScope getRuleScope(String ruleId, String value, Integer ruleScope, ScopeBizType scopeBizType, String leaveTypeId) { + + AttendanceRuleScope attendanceRuleScope = new AttendanceRuleScope(); + attendanceRuleScope.setId(FtbUtil.getId()); + attendanceRuleScope.setBizType(scopeBizType.getValue()); + attendanceRuleScope.setRuleId(ruleId); + attendanceRuleScope.setExpand(leaveTypeId); + attendanceRuleScope.setScopeType(ruleScope); + attendanceRuleScope.setScopeValue(value); + attendanceRuleScope.setPriority(ruleScope); + attendanceRuleScope.setCreatorUserId(UserProvider.getLoginUserId()); + attendanceRuleScope.setLastModifyUserId(UserProvider.getLoginUserId()); + attendanceRuleScope.setDeleteMark(ConstantUtil.NUM_FALSE); + return attendanceRuleScope; + } + + public void coverRuleScope(String ruleId, Integer ruleScope, MutablePair, List> dataList, ScopeBizType bizType) { + coverRuleScope(ruleId, ruleScope, dataList, bizType, null); + } + + /** + * 覆盖适配范围(适用于加班&公休&假期) + * + * @param ruleId 规则id + * @param ruleScope 适配范围(0: 全部, 1: 指定组织/成员) + * @param dataList 组织列表&用户列表 + * @param bizType 适配范围业务类型 + */ + public void coverRuleScope(String ruleId, Integer ruleScope, MutablePair, List> dataList, ScopeBizType bizType, String leaveTypeId) { + if (ConstantUtil.SCOPE_ALL.equals(ruleScope)) { + // 删除其他相同业务类型的适配范围 + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getBizType, bizType.getValue()) + .eq(StringUtil.isNotEmpty(leaveTypeId), AttendanceRuleScope::getExpand, leaveTypeId) + .ne(AttendanceRuleScope::getRuleId, ruleId)); + } else { + if (CollectionUtils.isNotEmpty(dataList.getLeft())) { + // 查询组织下的所有人 + ActionResult> actionResult = v2UserApi.listTargetOrganizesOrHaveChild(dataList.getLeft(), false, List.of(UserWorkStatusEnums.RESIGNED), UserProvider.getUser().getTenantId()); + if (null != actionResult && actionResult.getCode() == 200 && !actionResult.getData().isEmpty()) { + List userIds = actionResult.getData().stream().map(UserBoundVO::getId).collect(Collectors.toList()); + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getBizType, bizType.getValue()) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_USER) + .eq(StringUtil.isNotEmpty(leaveTypeId), AttendanceRuleScope::getExpand, leaveTypeId) + .in(AttendanceRuleScope::getScopeValue, userIds) + .ne(AttendanceRuleScope::getRuleId, ruleId)); + } + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getBizType, bizType.getValue()) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_ORG) + .eq(StringUtil.isNotEmpty(leaveTypeId), AttendanceRuleScope::getExpand, leaveTypeId) + .in(AttendanceRuleScope::getScopeValue, dataList.getLeft()) + .ne(AttendanceRuleScope::getRuleId, ruleId)); + } + if (CollectionUtils.isNotEmpty(dataList.getRight())) { + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getBizType, bizType.getValue()) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_USER) + .eq(StringUtil.isNotEmpty(leaveTypeId), AttendanceRuleScope::getExpand, leaveTypeId) + .in(AttendanceRuleScope::getScopeValue, dataList.getRight()) + .ne(AttendanceRuleScope::getRuleId, ruleId)); + } + } + } + + /** + * 生成适配范围列表 + * + * @param ruleId 规则id + * @param ruleScope 适配范围(0: 全部, 1: 指定组织/成员) + * @param dataList 组织列表&用户列表 + * @param bizType 适配范围业务类型 + * @return java.util.List + */ + public List getRuleScopeList(String ruleId, Integer ruleScope, MutablePair, List> dataList, ScopeBizType bizType) throws HandleException { + return getRuleScopeList(ruleId, ruleScope, dataList, bizType, null); + } + + /** + * 生成适配范围列表 + * + * @param ruleId 规则id + * @param ruleScope 适配范围(0: 全部, 1: 指定组织/成员) + * @param dataList 组织列表&用户列表 + * @param bizType 适配范围业务类型 + * @param leaveTypeId 2.0假期类型Id列表 + * @return java.util.List + */ + public List getRuleScopeList(String ruleId, Integer ruleScope, MutablePair, List> dataList, ScopeBizType bizType, String leaveTypeId) throws HandleException { + + List organizeList = dataList.getLeft(); + List userIdList = dataList.getRight(); + List ruleScopeList = new ArrayList<>(); + if (ruleScope.equals(ConstantUtil.SCOPE_ALL)) { + ruleScopeList.add(getRuleScope(ruleId, null, ConstantUtil.RULE_SCOPE_ALL, bizType, leaveTypeId)); + } else { + if (null == organizeList) organizeList = List.of(); + if (null == userIdList) userIdList = List.of(); + if (organizeList.isEmpty() && userIdList.isEmpty()) { + throw new HandleException("至少选择一个组织或成员"); + } + if (!organizeList.isEmpty()) { + organizeList.forEach(orgId -> ruleScopeList.add(getRuleScope(ruleId, orgId, ConstantUtil.RULE_SCOPE_ORG, bizType, leaveTypeId))); + } + if (!userIdList.isEmpty()) { + userIdList.forEach(userId -> ruleScopeList.add(getRuleScope(ruleId, userId, ConstantUtil.RULE_SCOPE_USER, bizType, leaveTypeId))); + } + } + if (bizType.equals(ScopeBizType.OVERTIME_RULE) || bizType.equals(ScopeBizType.PUBLIC_HOLIDAY_RULE )|| bizType.equals(ScopeBizType.LEAVE_RULES)) { + coverRuleScope(ruleId, ruleScope, dataList, bizType, leaveTypeId); + } + return ruleScopeList; + } + + public void saveBatch(List list) { + + ruleScopeService.saveBatch(list); + } + + /** + * 根据适配范围的变更, 更新数据库适配范围 + * + * @param ruleId 加班规则id + * @param oldScope 数据库适配范围(0: 全部, 1: 指定组织/成员) + * @param newScope 更新适配范围(0: 全部, 1: 指定组织/成员) + * @param dataList 更新组织/成员数据 + * @param bizType 适配范围业务类型 + * @return jnpf.model.attendance.dto.UpdateChangeDto 返回变更的组织和用户[新增, 删除] + */ + public UpdateChangeDto updateRuleScopeList(String ruleId, Integer oldScope, Integer newScope, MutablePair, List> dataList, ScopeBizType bizType) throws HandleException { + return updateRuleScopeList(ruleId, oldScope, newScope, dataList, bizType, null); + } + + /** + * 根据适配范围的变更, 更新数据库适配范围 + * + * @param ruleId 加班规则id + * @param oldScope 数据库适配范围(0: 全部, 1: 指定组织/成员) + * @param newScope 更新适配范围(0: 全部, 1: 指定组织/成员) + * @param dataList 更新组织/成员数据 + * @param bizType 适配范围业务类型 + * @return jnpf.model.attendance.dto.UpdateChangeDto 返回变更的组织和用户[新增, 删除] + */ + public UpdateChangeDto updateRuleScopeList(String ruleId, Integer oldScope, Integer newScope, MutablePair, List> dataList, ScopeBizType bizType, String leaveTypeId) throws HandleException { + + // 查询已有的适配范围数据 + List oldDetails = ruleScopeService.list( + new LambdaQueryWrapper().eq(AttendanceRuleScope::getRuleId, ruleId) + ); + if (oldScope.equals(newScope)) { + // 0 -> 0 无需更新 1 -> 1 判定是否需要更新 + if (newScope.equals(ConstantUtil.SCOPE_ORG_USER)) { + Set newOrgSet = new HashSet<>(dataList.getLeft()); + Set newUserSet = new HashSet<>(dataList.getRight()); + // 数据库组织范围 + Set oldOrgSet = oldDetails.stream() + .filter(d -> Objects.equals(d.getScopeType(), ConstantUtil.RULE_SCOPE_ORG)) + .map(AttendanceRuleScope::getScopeValue) + .collect(Collectors.toSet()); + // 数据库成员范围 + Set oldUserSet = oldDetails.stream() + .filter(d -> Objects.equals(d.getScopeType(), ConstantUtil.RULE_SCOPE_USER)) + .map(AttendanceRuleScope::getScopeValue) + .collect(Collectors.toSet()); + // 找出要删除的 + Set delOrgSet = new HashSet<>(oldOrgSet); + delOrgSet.removeAll(newOrgSet); + Set delUserSet = new HashSet<>(oldUserSet); + delUserSet.removeAll(newUserSet); + // 找出要新增的 + Set addOrgSet = new HashSet<>(newOrgSet); + addOrgSet.removeAll(oldOrgSet); + Set addUserSet = new HashSet<>(newUserSet); + addUserSet.removeAll(oldUserSet); + // 删除 + if (!delOrgSet.isEmpty()) { + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, ruleId) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_ORG) + .in(AttendanceRuleScope::getScopeValue, delOrgSet)); + } + if (!delUserSet.isEmpty()) { + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, ruleId) + .eq(AttendanceRuleScope::getScopeType, ConstantUtil.RULE_SCOPE_USER) + .in(AttendanceRuleScope::getScopeValue, delUserSet)); + } + // 新增 + List addOrgList = new ArrayList<>(addOrgSet); + List addUserList = new ArrayList<>(addUserSet); + if (!addOrgList.isEmpty() || !addUserList.isEmpty()) { + List insertList = getRuleScopeList(ruleId, newScope, MutablePair.of(addOrgList, addUserList), bizType, leaveTypeId); + if (!insertList.isEmpty()) { + ruleScopeService.saveBatch(insertList); + } + } + UpdateChangeDto updateChange = new UpdateChangeDto(); + updateChange.getAddOrgList().addAll(addOrgList); + updateChange.getAddUserList().addAll(addUserList); + updateChange.getDelOrgList().addAll(delOrgSet); + updateChange.getDelUserList().addAll(delUserSet); + return updateChange; + } + } else { + // 范围变更 + ruleScopeService.remove(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, ruleId)); + if (Objects.equals(newScope, ConstantUtil.SCOPE_ALL)) { + // 1 -> 0 删除原有记录, 生成一条全部范围的记录 + AttendanceRuleScope allScope = getRuleScope(ruleId, null, ConstantUtil.RULE_SCOPE_ALL, bizType, leaveTypeId); + ruleScopeService.save(allScope); + } else if (Objects.equals(newScope, ConstantUtil.SCOPE_ORG_USER)) { + // 0 -> 1 删除原有记录, 按选择的组织和成员生成新记录 + List insertList = getRuleScopeList(ruleId, newScope, dataList, bizType, leaveTypeId); + if (!insertList.isEmpty()) { + ruleScopeService.saveBatch(insertList); + } + UpdateChangeDto updateChange = new UpdateChangeDto(); + updateChange.getAddOrgList().addAll(dataList.getLeft()); + updateChange.getAddUserList().addAll(dataList.getRight()); + return updateChange; + } + } + if (bizType.equals(ScopeBizType.OVERTIME_RULE) || bizType.equals(ScopeBizType.PUBLIC_HOLIDAY_RULE)) { + coverRuleScope(ruleId, newScope, dataList, bizType); + } + return new UpdateChangeDto(); + } + + /** + * 根据规则id查询适配范围[组织, 用户] + * 适配范围是全部返回null + * + * @param ruleId 规则id + * @param bizType 规则类型 + * @return org.apache.commons.lang3.tuple.MutablePair,java.util.List> + */ + public MutablePair, List> selectScopeList(String ruleId, ScopeBizType bizType) { + + List list = ruleScopeService.list(new LambdaQueryWrapper() + .eq(AttendanceRuleScope::getRuleId, ruleId) + .eq(AttendanceRuleScope::getBizType, bizType.getValue()) + .orderByDesc(AttendanceRuleScope::getLastModifyTime)); + return getList(list); + } + + /** + * 根据规则id集合查询适配范围 + * Map<规则id, [组织, 用户]> + * + * @param ruleIds 规则id集合 + * @param bizType 规则类型 + * @return java.util.Map,java.util.List>> + */ + public Map, List>> selectScopeListBatch(List ruleIds, ScopeBizType bizType) { + + Map, List>> returnMap = new HashMap<>(); + List list = ruleScopeService.list(new LambdaQueryWrapper() + .in(AttendanceRuleScope::getRuleId, ruleIds) + .eq(AttendanceRuleScope::getBizType, bizType.getValue()) + .orderByDesc(AttendanceRuleScope::getLastModifyTime)); + ConcurrentMap> map = list.stream().collect(Collectors.groupingByConcurrent(AttendanceRuleScope::getRuleId)); + map.forEach((k, v) -> returnMap.put(k, getList(v))); + return returnMap; + } + + private MutablePair, List> getList(List list) { + + AttendanceRuleScope scope = list.stream().filter(v -> v.getScopeType().equals(ConstantUtil.RULE_SCOPE_ALL)).findFirst().orElse(null); + if (null != scope) { + return null; + } + List orgList = new ArrayList<>(); + List userList = new ArrayList<>(); + list.forEach(v -> { + if (v.getScopeType().equals(ConstantUtil.RULE_SCOPE_ORG)) { + orgList.add(v.getScopeValue()); + } + if (v.getScopeType().equals(ConstantUtil.RULE_SCOPE_USER)) { + userList.add(v.getScopeValue()); + } + }); + return MutablePair.of(orgList, userList); + } + + /** + * 查询用户生效中的规则 + * + * @param userId 用户id + * @param bizType 业务类型 + * @return java.util.List + */ + public List selectUserEffectList(String userId, ScopeBizType bizType, String tenantId) { + + UserBoundInfoVO data = null; + ActionResult usersBoundResult = v2UserApi.getUsersBound(userId, tenantId); + if (null == usersBoundResult || null == usersBoundResult.getData()) { + FtbDepUserDTO dto = new FtbDepUserDTO(); + dto.setUserIds(List.of(userId)); + dto.setTenantId(tenantId); + List person = ftbPersonnelsTurnoverManagementService.getInformationAboutTheDepartingPerson(dto); + if (null != person && !person.isEmpty()) { + data = new UserBoundInfoVO(); + data.setUserId(person.get(0).getId()); + data.setOrganizeId(person.get(0).getOrganizeId()); + } + } else { + data = usersBoundResult.getData(); + } + if (null == data) { + return List.of(); + } + return ruleScopeService.selectUserEffectList(data.getUserId(), data.getOrganizeId(), bizType); + } + public List selectUserEffectList(String userId, String orgId, ScopeBizType bizType) { + return ruleScopeService.selectUserEffectList(userId, orgId, bizType); + } + /** + * 查询用户生效中的规则[批量] + * + * @param userIds 用户id集合 + * @param bizType 业务类型 + * @param priority 使用优先级(1: 是, 0: 否) + * @return java.util.List + */ + public List selectUserEffectListBatch(List userIds, ScopeBizType bizType, Integer priority) { + return selectUserEffectListBatch(userIds, bizType, priority, null, null); + } + /** + * 查询用户生效中的规则[批量] + * + * @param userIds 用户id集合 + * @param bizType 业务类型 + * @param priority 使用优先级(1: 是, 0: 否) + * @return java.util.List + */ + public List selectUserEffectListBatch2(List userIds, ScopeBizType bizType, Integer priority,String tenantId) { + return selectUserEffectListBatch(userIds, bizType, priority, null,tenantId); + } + public List selectUserEffectListBatch(List userIds, ScopeBizType bizType, Integer priority, List leaveTypeIds) { + return selectUserEffectListBatch(userIds, bizType, priority, leaveTypeIds, null); + } + /** + * 查询用户生效中的规则[批量] + * + * @param userIds 用户id集合 + * @param bizType 业务类型 + * @param priority 使用优先级(1: 是, 0: 否) + * @return java.util.List + */ + public List selectUserEffectListBatch(List userIds, ScopeBizType bizType, Integer priority, List leaveTypeIds,String tenantId) { + + ActionResult> batchResult = v2UserApi.getAllUserInfoBatch(userIds, Objects.requireNonNullElse(tenantId, UserProvider.getUser().getTenantId())); + if (null == batchResult || null == batchResult.getData()) { + return List.of(); + } + List data = batchResult.getData(); + List userOrgList = new ArrayList<>(); + data.forEach(v -> userOrgList.add(new UserOrgDto(v.getId(), v.getOrganizeId()))); + // 分批查询 + List> partition = Lists.partition(userOrgList, 200); + List scopeList = new ArrayList<>(); + for (List subList : partition) { + List partResult = ruleScopeService.selectUserEffectListBatch( + subList, bizType, priority, leaveTypeIds + ); + if (partResult != null && !partResult.isEmpty()) { + scopeList.addAll(partResult); + } + } + return scopeList; + } + + /** + * 查询组织生效中假期的规则[批量] + * + * @param oldOrganizeId 上级组织id + * @return java.util.List + */ + public List selectOrgEffectListBatch(String oldOrganizeId) { + return ruleScopeService.selectOrgEffectListBatch(oldOrganizeId); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/SecondmentTypeUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/SecondmentTypeUtil.java new file mode 100644 index 0000000..c4e9383 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/SecondmentTypeUtil.java @@ -0,0 +1,50 @@ +package jnpf.util.attendance; + +import cn.hutool.core.collection.CollUtil; +import jnpf.entity.AttendanceGroupUser; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * 借调类型(secondmentType)解析工具。 + *

统一封装 {@link AttendanceGroupUserStatusUtil#isExistStatus} 与 VO 字段 secondmentType 的映射口径。

+ */ +public final class SecondmentTypeUtil { + + private SecondmentTypeUtil() { + } + + /** + * 将 {@code isExistStatus} 返回值映射为 secondmentType。 + * + * @param existStatus isExistStatus 返回值(-1已离 0未入 1正常 2全天被借调 3部分被借调 4借调) + * @return 0无借调 1借调入 2借调出 + */ + public static int toSecondmentType(Integer existStatus) { + return Objects.equals(existStatus, 4) + ? 2 + : (Objects.equals(existStatus, 2) || Objects.equals(existStatus, 3) ? 1 : 0); + } + + /** + * 根据考勤组成员与单日解析 secondmentType。 + */ + public static int resolveSecondmentType(List users, Date date) { + if (CollUtil.isEmpty(users) || date == null) { + return 0; + } + return toSecondmentType(AttendanceGroupUserStatusUtil.isExistStatus(users, date)); + } + + /** + * 根据考勤组成员与时间范围解析 secondmentType。 + */ + public static int resolveSecondmentType(List users, Date start, Date end) { + if (CollUtil.isEmpty(users) || start == null || end == null) { + return 0; + } + return toSecondmentType(AttendanceGroupUserStatusUtil.isExistStatus(users, start, end)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/UnionQualityExcelUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/UnionQualityExcelUtil.java new file mode 100644 index 0000000..213a9ba --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/attendance/UnionQualityExcelUtil.java @@ -0,0 +1,176 @@ +package jnpf.util.attendance; + +import com.alibaba.nacos.common.utils.CollectionUtils; +import jnpf.model.attendance.model.WebExportNewDetailsModel; +import jnpf.model.attendance.model.WebExportNewModel; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; + +public class UnionQualityExcelUtil { + /** + * 生成sheet + * + * @return + */ + public static void generateSheet(Workbook workbook, List exportNewModelList) { + Sheet sheet = workbook.createSheet("考勤月度统计报表"); + sheet.setDisplayGridlines(true); + sheet.getPrintSetup().setLandscape(true); + CellStyle headStyle = workbook.createCellStyle(); + Font headFont = workbook.createFont(); + headStyle = setCellStyle(headStyle, headFont, (short) 12, true, true, HorizontalAlignment.CENTER); + //创建表头 + String[] heads = {"用户名", "名称", "应出勤天数", "实际出勤天数", "有效出勤天数", "应出勤工时", "实际出勤工时", "有效出勤工时", "计薪假抵扣时长", + "不计薪假扣时长", "请假次数", "请假时长", "未抵扣请假时长", "加班次数", "加班时长", "外勤打卡次数", "缺勤次数", "缺卡次数", "补卡次数", "迟到次数" + , "迟到累计分钟", "早退次数", "早退累计分钟"}; + Row headRow = sheet.createRow(0); + headRow.setHeightInPoints(25); + for (int cn = 0; cn < heads.length; cn++) { + Cell cell = headRow.createCell(cn); + cell.setCellValue(heads[cn]); + cell.setCellStyle(headStyle); + } + Font contentFont = workbook.createFont(); + CellStyle contentStyle = workbook.createCellStyle(); + contentStyle = setCellStyle(contentStyle, contentFont, (short) 10, true, true, HorizontalAlignment.CENTER); + CellStyle contentDetailsStyle = workbook.createCellStyle(); + contentDetailsStyle = setCellStyle(contentDetailsStyle, contentFont, (short) 9, false, true, HorizontalAlignment.CENTER); + if (CollectionUtils.isNotEmpty(exportNewModelList)) { + //外层用户循环 + for (int index = 0; index < exportNewModelList.size(); index++) { + WebExportNewModel newModel = exportNewModelList.get(index); + String[] a = createString(newModel); + int temp = sheet.getLastRowNum() + 1; + Row row = sheet.createRow(temp); + row.setHeightInPoints(25); + for (int i = 0; i < a.length; i++) { + Cell cell = row.createCell(i); + cell.setCellValue(a[i]); + cell.setCellStyle(contentStyle); + } + if (CollectionUtils.isNotEmpty(newModel.getDetails())) { + for (int detailIndex = 0; detailIndex < newModel.getDetails().size(); detailIndex++) { + String[] details = createString1(newModel.getDetails().get(detailIndex)); + int detailsTemp = sheet.getLastRowNum() + 1; + Row detailsRow = sheet.createRow(detailsTemp); + detailsRow.setHeightInPoints(25); + for (int i = 0; i < details.length; i++) { + Cell cell = detailsRow.createCell(i); + cell.setCellValue(details[i]); + cell.setCellStyle(contentDetailsStyle); + } + } + sheet.addMergedRegion(new CellRangeAddress(temp, temp + newModel.getDetails().size(), 0, 0)); + } + } + } + sheet.setColumnWidth(0, 15 * 256); + sheet.setColumnWidth(1, 15 * 256); + sheet.setColumnWidth(2, 15 * 256); + sheet.setColumnWidth(3, 17 * 256); + sheet.setColumnWidth(4, 17 * 256); + sheet.setColumnWidth(5, 17 * 256); + sheet.setColumnWidth(6, 17 * 256); + sheet.setColumnWidth(7, 17 * 256); + sheet.setColumnWidth(8, 18 * 256); + sheet.setColumnWidth(9, 18 * 256); + sheet.setColumnWidth(10, 18 * 256); + sheet.setColumnWidth(11, 18 * 256); + sheet.setColumnWidth(12, 18 * 256); + sheet.setColumnWidth(13, 17 * 256); + sheet.setColumnWidth(14, 17 * 256); + sheet.setColumnWidth(15, 17 * 256); + sheet.setColumnWidth(16, 17 * 256); + sheet.setColumnWidth(17, 17 * 256); + sheet.setColumnWidth(18, 17 * 256); + sheet.setColumnWidth(19, 17 * 256); + sheet.setColumnWidth(20, 17 * 256); + sheet.setColumnWidth(21, 17 * 256); + sheet.setColumnWidth(22, 17 * 256); + sheet.setColumnWidth(23, 17 * 256); + } + + /** + * 设置单元格样式 + * + * @param style + * @param font + * @param fontSize + * @param bold + * @param isBoder + * @param horizontalAlignment + * @return + */ + public static CellStyle setCellStyle(CellStyle style, Font font, short fontSize, Boolean bold, Boolean isBoder, HorizontalAlignment horizontalAlignment) { + short stringFormat = (short) BuiltinFormats.getBuiltinFormat("TEXT"); + // 将字体大小设置为9px + font.setFontHeightInPoints(fontSize); + // 将字体设置为加粗 + font.setBold(bold); + //字体样式 + font.setFontName("宋体"); + //字体大小 + style.setFont(font); + // 设置单元格内容是否自动换行 + style.setWrapText(true); + if (isBoder) { + //下边框 + style.setBorderBottom(BorderStyle.THIN); + //左边框 + style.setBorderLeft(BorderStyle.THIN); + //上边框 + style.setBorderTop(BorderStyle.THIN); + //右边框 + style.setBorderRight(BorderStyle.THIN); + } + //水平居中 + style.setAlignment(horizontalAlignment); + //上下居中 + style.setVerticalAlignment(VerticalAlignment.CENTER); + //设置自动换行 + style.setWrapText(true); + style.setDataFormat(stringFormat); + return style; + } + + private static String[] createString(WebExportNewModel exportDTO) { + String[] a = {exportDTO.getUserName(), exportDTO.getName(), exportDTO.getShouldAttendDays().toString(), compareNumber(exportDTO.getActualAttendDays()), + compareNumber(exportDTO.getEffectiveAttendDays()), compareNumber(exportDTO.getShouldAttendHours()), compareNumber(exportDTO.getActualAttendHours()), + compareNumber(exportDTO.getEffectiveAttendHours()), compareNumber(exportDTO.getPaidLeaveHours()), compareNumber(exportDTO.getUnpaidLeaveHours()), + exportDTO.getLeaveTimes().toString(), compareNumber(exportDTO.getLeaveHours()), compareNumber(exportDTO.getUnDeductedLeaveHours()), + exportDTO.getOvertimeTimes().toString(), compareNumber(exportDTO.getOvertimeHours()), exportDTO.getOutworkClockTimes().toString(), exportDTO.getAbsenceTimes().toString(), + exportDTO.getAbsenceCardTimes().toString(), exportDTO.getMakeUpCardTimes().toString(), exportDTO.getLateTimes().toString(), exportDTO.getLateAccumulatedMinutes().toString(), + exportDTO.getEarlyLeaveTimes().toString(), exportDTO.getEarlyLeaveAccumulatedMinutes().toString() + }; + return a; + } + + private static String[] createString1(WebExportNewDetailsModel exportDTO) { + String[] a = {exportDTO.getUserName(), exportDTO.getName(), exportDTO.getShouldAttendDays().toString(), compareNumber(exportDTO.getActualAttendDays()), + compareNumber(exportDTO.getEffectiveAttendDays()), compareNumber(exportDTO.getShouldAttendHours()), compareNumber(exportDTO.getActualAttendHours()), + compareNumber(exportDTO.getEffectiveAttendHours()), compareNumber(exportDTO.getPaidLeaveHours()), compareNumber(exportDTO.getUnpaidLeaveHours()), + exportDTO.getLeaveTimes().toString(), compareNumber(exportDTO.getLeaveHours()), compareNumber(exportDTO.getUnDeductedLeaveHours()), + exportDTO.getOvertimeTimes().toString(), compareNumber(exportDTO.getOvertimeHours()), exportDTO.getOutworkClockTimes().toString(), exportDTO.getAbsenceTimes().toString(), + exportDTO.getAbsenceCardTimes().toString(), exportDTO.getMakeUpCardTimes().toString(), exportDTO.getLateTimes().toString(), exportDTO.getLateAccumulatedMinutes().toString(), + exportDTO.getEarlyLeaveTimes().toString(), exportDTO.getEarlyLeaveAccumulatedMinutes().toString() + }; + return a; + } + + public static String compareNumber(BigDecimal number) { + if (!"".equals(number) && number != null) { + if (new BigDecimal(number.intValue()).compareTo(number) == 0) { + //整数 + return String.valueOf(number.intValue()); + } else { + //小数 + return String.valueOf(number.setScale(2, RoundingMode.HALF_UP)); + } + } + return ""; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionController.java new file mode 100644 index 0000000..a85c0ad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionController.java @@ -0,0 +1,23 @@ +package jnpf.util.auth; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 对外 HTTP 与 Feign 契约:{@link V2AuthPermissionApi} + */ +@RestController +@RequestMapping("/permission/auth") +public class V2AuthPermissionController implements V2AuthPermissionApi { + + @Resource + private V2AuthPermissionUtils v2AuthPermissionUtils; + + @Override + public List getLoginUserAuthOrganizeIds() { + return v2AuthPermissionUtils.getLoginUserAuthOrganizeIds(); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionUtils.java new file mode 100644 index 0000000..74cfd15 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/auth/V2AuthPermissionUtils.java @@ -0,0 +1,414 @@ +package jnpf.util.auth; + +import cn.hutool.core.collection.CollUtil; +import jnpf.authority.utils.PermissionsApplicableEnums; +import jnpf.authority.utils.PermissionsApplicableObject; +import jnpf.authority.utils.PermissionsUtils; +import jnpf.base.ActionResult; +import jnpf.cultivate.utils.UserApiV2Util; +import jnpf.permission.V2OrganizeApi; +import jnpf.permission.V2UserApi; +import jnpf.permission.dto.v2.organzie.QueryOrganizeListTargetTypesDTO; +import jnpf.permission.eum.v2.OrganizeCategoryEnums; +import jnpf.permission.eum.v2.TargetAuthEnums; +import jnpf.permission.eum.v2.UserWorkStatusEnums; +import jnpf.permission.vo.v2.TargetAuthIdsVO; +import jnpf.permission.vo.v2.organzie.OrganizeGeneralDetailVO; +import jnpf.permission.vo.v2.user.UserBoundInfoVO; +import jnpf.permission.vo.v2.user.UserBoundVO; +import jnpf.permission.vo.v2.user.UserPageListVO; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 权限校验公共工具 + * + * @author Flynn Chan + * @create 2025-05-20 + */ +@Component +@Slf4j +public class V2AuthPermissionUtils { + @Resource + private PermissionsUtils permissionsUtils; + @Resource + private V2OrganizeApi organizeV2Api; + @Resource + private V2UserApi userV2Api; + + /** + * 获取登录人权限范围内的门店id/组织id, null为全部,[]为无, 人的权限都算[] + */ + public List getLoginUserAuthOrganizeIds() { + String userId = UserProvider.getLoginUserId(); + //超级管理员也返回null,也获取全部 + if (UserProvider.getUser().getIsAdministrator()) { + return null; + } + PermissionsApplicableObject applicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userId); + if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.ALL) { + //全部返回null + return null; + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_AND_SUBORDINATE_EMPLOYEES) { + UserBoundInfoVO usersBound = userV2Api.getUsersBound(userId, null).getData(); + if (null != usersBound) { + List organizeGeneralDetailVOS = organizeV2Api.organizesOrHaveChildByOrganizeIds(List.of(usersBound.getOrganizeId()), true, null).getData(); + //过滤班组 + organizeGeneralDetailVOS = organizeGeneralDetailVOS.stream().filter(ctx -> !OrganizeCategoryEnums.TEAM.equals(ctx.getOrganizeCategoryEnums())).collect(Collectors.toList()); + return organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList()); + } else { + return new ArrayList<>(); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_EMPLOYEES) { + return new ArrayList<>(); + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE) { + return new ArrayList<>(); + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SPECIFIC_ORGANIZATION) { + return applicableObject.getOrgIds(); + } else { + log.error("未得到用户[" + userId + "]对应权限!"); + return new ArrayList<>(); + } + } + + /** + * 获取当前登录人权限 + * + * @param sourceCategoryEnum + * @return + */ + public TargetAuthIdsVO processAuthIds() { + TargetAuthIdsVO targetAuthIdsVO = new TargetAuthIdsVO(); + //停用 主动传userid + String userId = UserProvider.getLoginUserId(); + //超级管理员也返回null,也获取全部 + if (UserProvider.getUser().getIsAdministrator()) { + return null; + } + PermissionsApplicableObject applicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userId); + if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.ALL) { + //全部返回null + return null; + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_AND_SUBORDINATE_EMPLOYEES) { + targetAuthIdsVO.setTargetAuthEnums(TargetAuthEnums.ORGANIZE); + UserBoundInfoVO usersBound = userV2Api.getUsersBound(userId, null).getData(); + if (null != usersBound) { + List organizeGeneralDetailVOS = organizeV2Api.organizesOrHaveChildByOrganizeIds(List.of(usersBound.getOrganizeId()), true, null).getData(); + //过滤班组 + organizeGeneralDetailVOS = organizeGeneralDetailVOS.stream().filter(ctx -> !OrganizeCategoryEnums.TEAM.equals(ctx.getOrganizeCategoryEnums())).collect(Collectors.toList()); + targetAuthIdsVO.setIds(organizeGeneralDetailVOS.stream().map(OrganizeGeneralDetailVO::getId).collect(Collectors.toList())); + } else { + targetAuthIdsVO.setIds(new ArrayList<>()); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_EMPLOYEES) { + targetAuthIdsVO.setTargetAuthEnums(TargetAuthEnums.USER); + UserBoundInfoVO usersBound = userV2Api.getUsersBound(userId, null).getData(); + if (null != usersBound) { + //当前组织的人 todo 这里处理冗余没效率,后期可以优化 + List userBoundVOList = userV2Api.listTargetOrganizesOrHaveChild(List.of(usersBound.getOrganizeId()), false, UserWorkStatusEnums.getAllUserWorkStatusEnums(), null).getData(); + targetAuthIdsVO.setIds(userBoundVOList.stream().map(UserBoundVO::getId).distinct().collect(Collectors.toList())); + } else { + targetAuthIdsVO.setIds(new ArrayList<>()); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE) { + targetAuthIdsVO.setTargetAuthEnums(TargetAuthEnums.USER); + List userPageListVO = userV2Api.listUnderlingTargetUser(userId, null).getData(); + targetAuthIdsVO.setIds(userPageListVO.stream().map(UserPageListVO::getId).collect(Collectors.toList())); + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SPECIFIC_ORGANIZATION) { + targetAuthIdsVO.setTargetAuthEnums(TargetAuthEnums.ORGANIZE); + targetAuthIdsVO.setIds(applicableObject.getOrgIds()); + } else { + log.error("未得到用户[" + userId + "]对应权限!"); + targetAuthIdsVO.setTargetAuthEnums(TargetAuthEnums.NONE); + targetAuthIdsVO.setIds(new ArrayList<>()); + } + + return targetAuthIdsVO; + } + + /** + * 批量查询用户的权限范围的门店 + * + * @param userIds + * @param status 状态 1:禁用 0:启用 -1-所有 + * @return + */ + public Map> batchAuthOrganizesForUserIds(List userIds, Integer status) { + log.info("[批量]未登录人的门店,userIds={}", userIds); + List cacheAllOrgIds = new ArrayList<>();//所有门店id + QueryOrganizeListTargetTypesDTO dto = new QueryOrganizeListTargetTypesDTO(); + dto.setOrganizeCategoryEnums(List.of(OrganizeCategoryEnums.STORE)); + ActionResult> listActionResult = organizeV2Api.listOrganizeByTargetTypes(dto); + if (listActionResult != null && CollUtil.isNotEmpty(listActionResult.getData())) { + for (OrganizeGeneralDetailVO vo : listActionResult.getData()) { + if (status.equals(-1)) { + cacheAllOrgIds.add(vo.getId()); + } else if (status.equals(1)) { + if (!vo.getEnabled()) { + cacheAllOrgIds.add(vo.getId()); + } + } else if (status.equals(0)) { + if (vo.getEnabled()) { + cacheAllOrgIds.add(vo.getId()); + } + } + } + } + + Map> cacheOrgIds = new HashMap<>(); + Map> returnMap = new HashMap<>(); + Map userPrimaryBoundBatchReturnMap = getUserPrimaryBoundBatchReturnMap(userIds, UserProvider.getUser().getTenantId()); + String moduleId = getModuleForHeader(); + for (String userId : userIds) { + returnMap.put(userId, new ArrayList<>()); + //判断用户是否是管理员 + UserBoundVO usersBound = userPrimaryBoundBatchReturnMap.get(userId); + if (null == usersBound) { + continue; + } + if (usersBound.getIsAdministrator()) { + returnMap.put(userId, cacheAllOrgIds); + continue; + } + try { + PermissionsApplicableObject applicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userId, moduleId,UserProvider.getUser().getTenantId()); + if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.ALL) { + returnMap.put(userId, cacheAllOrgIds); + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_AND_SUBORDINATE_EMPLOYEES) { + List cacheIds = cacheOrgIds.get(usersBound.getOrganizeId()); + if (CollUtil.isNotEmpty(cacheIds)) { + returnMap.put(userId, cacheIds); + continue; + } + List organizeGeneralDetailVOS = organizeV2Api.organizesOrHaveChildByOrganizeIds(List.of(usersBound.getOrganizeId()), true, null).getData(); + //过滤门店 + cacheIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(organizeGeneralDetailVOS)) { + for (OrganizeGeneralDetailVO organizeGeneralDetailVO : organizeGeneralDetailVOS) { + if (OrganizeCategoryEnums.STORE.equals(organizeGeneralDetailVO.getOrganizeCategoryEnums())) { + if (status.equals(-1)) { + cacheIds.add(organizeGeneralDetailVO.getId()); + } else if (status.equals(1)) { + if (!organizeGeneralDetailVO.getEnabled()) { + cacheIds.add(organizeGeneralDetailVO.getId()); + } + } else if (status.equals(0)) { + if (organizeGeneralDetailVO.getEnabled()) { + cacheIds.add(organizeGeneralDetailVO.getId()); + } + } + } + } + cacheOrgIds.put(usersBound.getOrganizeId(), cacheIds); + returnMap.put(userId, cacheIds); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_EMPLOYEES) { + if (cacheAllOrgIds.contains(usersBound.getOrganizeId())) { + returnMap.put(userId, List.of(usersBound.getOrganizeId())); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE) { + + List userPageListVO = userV2Api.listUnderlingTargetUser(userId, null).getData(); + if(CollUtil.isNotEmpty(userPageListVO)){ + List cacheIds = new ArrayList<>(); + for (UserPageListVO pageListVO : userPageListVO) { + if(cacheAllOrgIds.contains(pageListVO.getOrganizeId())){ + cacheIds.add(pageListVO.getOrganizeId()); + } + } + returnMap.put(userId, cacheIds); + } + + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SPECIFIC_ORGANIZATION) { + returnMap.put(userId, UserApiV2Util.getIntersection(applicableObject.getOrgIds(), cacheAllOrgIds)); + } + } catch (Exception e) { + e.printStackTrace(); + log.error("[批量]未登录人的门店,userId={},e={}", userId, e); + } + } + log.info("[批量]未登录人的门店,returnMap= {}", returnMap); + return returnMap; + } + /** + * 批量用户有权限的门店 + * + * @param userIds 用户ids + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param moduleId 模块id + * @param tenantId 租户id + * @return + */ + public Map> batchAuthOrganizesForUserIdsAndTenantId(List userIds, Integer status, String moduleId, String tenantId) { + return batchAuthOrganizesForUserIdsAndTenantId(userIds, List.of(OrganizeCategoryEnums.STORE), status, moduleId, tenantId); + } + public Map> batchAuthOrganizesAll(List userIds, Integer status, String moduleId, String tenantId) { + return batchAuthOrganizesForUserIdsAndTenantId(userIds, List.of(OrganizeCategoryEnums.STORE, OrganizeCategoryEnums.DEPARTMENT, OrganizeCategoryEnums.COMPANY), status, moduleId, tenantId); + } + /** + * 批量用户有权限的门店 + * + * @param userIds 用户ids + * @param status 状态 1:禁用 0:启用 -1-所有 + * @param moduleId 模块id + * @param tenantId 租户id + * @return + */ + public Map> batchAuthOrganizesForUserIdsAndTenantId(List userIds, List organizeCategoryEnums, Integer status, String moduleId, String tenantId) { + log.info("[批量]未登录人的门店,userIds={}", userIds); + List cacheAllOrgIds = new ArrayList<>();//所有门店id + QueryOrganizeListTargetTypesDTO dto = new QueryOrganizeListTargetTypesDTO(); + dto.setTenantId(tenantId); + dto.setOrganizeCategoryEnums(organizeCategoryEnums); + ActionResult> listActionResult = organizeV2Api.listOrganizeByTargetTypes(dto); + if (listActionResult != null && CollUtil.isNotEmpty(listActionResult.getData())) { + for (OrganizeGeneralDetailVO vo : listActionResult.getData()) { + if (status.equals(-1)) { + cacheAllOrgIds.add(vo.getId()); + } else if (status.equals(1)) { + if (!vo.getEnabled()) { + cacheAllOrgIds.add(vo.getId()); + } + } else if (status.equals(0)) { + if (vo.getEnabled()) { + cacheAllOrgIds.add(vo.getId()); + } + } + } + } + + Map> cacheOrgIds = new HashMap<>(); + Map> returnMap = new HashMap<>(); + Map userPrimaryBoundBatchReturnMap = getUserPrimaryBoundBatchReturnMap(userIds,tenantId); + // Map objectMap = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userIds, moduleId, tenantId); + for (String userId : userIds) { + returnMap.put(userId, new ArrayList<>()); + //判断用户是否是管理员 + UserBoundVO usersBound = userPrimaryBoundBatchReturnMap.get(userId); + if (null == usersBound) { + continue; + } + if (usersBound.getIsAdministrator()) { + returnMap.put(userId, cacheAllOrgIds); + continue; + } + try { + PermissionsApplicableObject applicableObject = permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userId, moduleId, tenantId); + log.error("permissionsApplicableEnums={}", applicableObject.getPermissionsApplicableEnums()); + if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.ALL) { + returnMap.put(userId, cacheAllOrgIds); + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_AND_SUBORDINATE_EMPLOYEES) { + List cacheIds = cacheOrgIds.get(usersBound.getOrganizeId()); + if (CollUtil.isNotEmpty(cacheIds)) { + returnMap.put(userId, cacheIds); + continue; + } + List organizeGeneralDetailVOS = organizeV2Api.organizesOrHaveChildByOrganizeIds(List.of(usersBound.getOrganizeId()), true, tenantId).getData(); + //过滤门店 + cacheIds = new ArrayList<>(); + if (CollUtil.isNotEmpty(organizeGeneralDetailVOS)) { + for (OrganizeGeneralDetailVO organizeGeneralDetailVO : organizeGeneralDetailVOS) { + if (OrganizeCategoryEnums.STORE.equals(organizeGeneralDetailVO.getOrganizeCategoryEnums())) { + if (status.equals(-1)) { + cacheIds.add(organizeGeneralDetailVO.getId()); + } else if (status.equals(1)) { + if (!organizeGeneralDetailVO.getEnabled()) { + cacheIds.add(organizeGeneralDetailVO.getId()); + } + } else if (status.equals(0)) { + if (organizeGeneralDetailVO.getEnabled()) { + cacheIds.add(organizeGeneralDetailVO.getId()); + } + } + } + } + cacheOrgIds.put(usersBound.getOrganizeId(), cacheIds); + returnMap.put(userId, cacheIds); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_ORGANIZATION_EMPLOYEES) { + if (cacheAllOrgIds.contains(usersBound.getOrganizeId())) { + returnMap.put(userId, List.of(usersBound.getOrganizeId())); + } + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SUBORDINATE) { + + List userPageListVO = userV2Api.listUnderlingTargetUser(userId, tenantId).getData(); + if (CollUtil.isNotEmpty(userPageListVO)) { + List cacheIds = new ArrayList<>(); + for (UserPageListVO pageListVO : userPageListVO) { + if (cacheAllOrgIds.contains(pageListVO.getOrganizeId())) { + cacheIds.add(pageListVO.getOrganizeId()); + } + } + returnMap.put(userId, cacheIds); + } + + } else if (applicableObject.getPermissionsApplicableEnums() == PermissionsApplicableEnums.SCOPE_SPECIFIC_ORGANIZATION) { + returnMap.put(userId, UserApiV2Util.getIntersection(applicableObject.getOrgIds(), cacheAllOrgIds)); + } + } catch (Exception e) { + e.printStackTrace(); + log.error("[批量]未登录人的门店,userId={},e={}", userId, e); + } + } + log.info("[批量]未登录人的门店,returnMap= {}", returnMap); + return returnMap; + } + + /** + * 批量获取用户信息 + * + * @param userIds 用户id列表 + * @param tenantId + * @return 用户信息映射,key为用户ID,value为用户信息对象 + */ + public Map getUserPrimaryBoundBatchReturnMap(List userIds, String tenantId) { + Map map = new HashMap<>(); + if (CollUtil.isEmpty(userIds)) { + return map; + } + if (StringUtils.isEmpty(tenantId)) { + tenantId = UserProvider.getUser().getTenantId(); + } + ActionResult> userPrimaryBoundBatch = userV2Api.getUserPrimaryBoundBatch(userIds, tenantId); + if (userPrimaryBoundBatch == null || CollUtil.isEmpty(userPrimaryBoundBatch.getData())) { + return map; + } + for (UserBoundVO userPrimaryBoundVO : userPrimaryBoundBatch.getData()) { + map.put(userPrimaryBoundVO.getId(), userPrimaryBoundVO); + } + return map; + } + + /** + * 从header头中获取module + * + * @return + */ + private String getModuleForHeader() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new RuntimeException("请传入权限菜单id"); + } + String module = attributes.getRequest().getHeader("Module"); + if (StringUtil.isEmpty(module)) { + throw new RuntimeException("请传入权限菜单id"); + } + return module; + } + + public Map batchAuthOrganizesAllForUserIds(List userIds) { + return permissionsUtils.obtainTheScopeOfUserPermissionsEnums(userIds, getModuleForHeader(), UserProvider.getUser().getTenantId()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/EasyExcelUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/EasyExcelUtils.java new file mode 100644 index 0000000..b76fcfc --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/EasyExcelUtils.java @@ -0,0 +1,359 @@ +package jnpf.util.excel; + + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.util.MapUtils; +import com.alibaba.excel.write.handler.CellWriteHandler; +import com.alibaba.excel.write.handler.SheetWriteHandler; +import com.alibaba.excel.write.handler.context.CellWriteHandlerContext; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jnpf.model.personnels.bo.FtbRosterImportTemplateBO; +import jnpf.personnels.listeners.BaseEasyExcelCommonListener; +import jnpf.personnels.listeners.BaseEasyExcelReadListener; +import lombok.Data; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.poifs.filesystem.FileMagic; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.xssf.usermodel.XSSFRichTextString; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.UrlResource; +import org.springframework.util.ResourceUtils; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * excel工具类 + * + * @author fantaibao + * @date 2024/02/02 + */ +@UtilityClass +@Slf4j +public class EasyExcelUtils { + + public static final int BUFFER_SIZE = 4096; + + + /** + * 校验上传的文件是否是Excel文件及是否是xls或xlsx格式 + * + * @param file 文件 + */ + public InputStream checkExcelFile(String file) { + try { + URL url = ResourceUtils.getURL(file); + UrlResource urlResource = new UrlResource(url); + // 根据Excel魔数判断该文件是否为Excel文件 + InputStream inputStream = urlResource.getInputStream(); + InputStream fileMagics = FileMagic.prepareToCheckMagic(inputStream); + FileMagic fileMagic = FileMagic.valueOf(fileMagics); + + // FileMagic.OLE2表示xls格式 FileMagic.OOXML表示xlsx格式 + if (Objects.equals(fileMagic, FileMagic.OLE2) || Objects.equals(fileMagic, FileMagic.OOXML)) { + return fileMagics; + } else { + log.error("file format error,please upload an Excel file in xls or xlsx format"); + throw new RuntimeException("文件格式错误,请上传xls或xlsx格式的Excel文件"); + } + } catch (Exception e) { + log.error("请上传xls或xlsx格式的Excel文件", e); + throw new RuntimeException(e.getMessage()); + } + + } + + /** + * 导出excel + * @param response 响应 + * @param fileName 文件名 + * @param list 数据 + * @throws IOException + */ + public void exportExcel(HttpServletResponse response, String fileName, List list,Class tClass) throws IOException { + String fileRealName = URLEncoder.encode(fileName+System.currentTimeMillis()/1000, "UTF-8").replaceAll("\\+", "%20"); + try(ServletOutputStream outputStream = response.getOutputStream()){ + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileRealName + ".xlsx"); + EasyExcel.write(outputStream, tClass) + .registerWriteHandler(new EasyExcelUtils.CustomCellWriteHandler()) + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .inMemory(true) + .sheet("sheet1").doWrite(list); + } + } + + /** + * 动态表头生成(通过模板) + */ + public void dynamicHeaderGeneration(HttpServletResponse httpServletResponse, + String fileNameOriginal, String fileSource, + List ftbRosterImportTemplateBOS, String employeeType) throws IOException { + // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman + httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + httpServletResponse.setCharacterEncoding("utf-8"); + // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 + String fileName = URLEncoder.encode(fileNameOriginal + System.currentTimeMillis(), "UTF-8").replaceAll("\\+", "%20"); + httpServletResponse.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + if (fileSource.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + fileSource = fileSource.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length()); + } + try (InputStream inputStream = new ClassPathResource(fileSource).getInputStream(); OutputStream outputStream = httpServletResponse.getOutputStream()) { + Map map = MapUtils.newHashMap(); + // 员工类型 + map.put("employeeType", employeeType); + ExcelWriter excelWriter = EasyExcel.write() + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .registerWriteHandler(new CustomRosterCellWriteHandler()) + .withTemplate(inputStream) + .file(outputStream) + .inMemory(true).build(); + WriteSheet writeSheetFill = EasyExcel.writerSheet(0).build(); + excelWriter.fill(map, writeSheetFill); + for (int i = 0; i < ftbRosterImportTemplateBOS.size(); i++) { + FtbRosterImportTemplateBO ftbRosterImportTemplateBO = ftbRosterImportTemplateBOS.get(i); + // 这里放入动态头,跳过第一个sheet + WriteSheet writeSheet = EasyExcel.writerSheet(ftbRosterImportTemplateBO.getSheetName()) + .head(ftbRosterImportTemplateBO.getHeader()).build(); + excelWriter.write(ftbRosterImportTemplateBO.getData(), writeSheet); + } + excelWriter.finish(); + } + + } + + /** + * 动态表头生成 + */ + public void dynamicHeaderGeneration(HttpServletResponse httpServletResponse, + String fileNameOriginal, List ftbRosterImportTemplateBOS) throws IOException { + // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman + httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + httpServletResponse.setCharacterEncoding("utf-8"); + // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 + String fileName = URLEncoder.encode(fileNameOriginal + System.currentTimeMillis(), "UTF-8").replaceAll("\\+", "%20"); + httpServletResponse.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + try (OutputStream outputStream = httpServletResponse.getOutputStream()) { + ExcelWriter excelWriter = EasyExcel.write() + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .registerWriteHandler(new CustomCellWriteHandler()) + .file(outputStream) + .inMemory(true).build(); + for (int i = 0; i < ftbRosterImportTemplateBOS.size(); i++) { + FtbRosterImportTemplateBO ftbRosterImportTemplateBO = ftbRosterImportTemplateBOS.get(i); + // 这里放入动态头,跳过第一个sheet + WriteSheet writeSheet = EasyExcel.writerSheet(ftbRosterImportTemplateBO.getSheetName()) + .head(ftbRosterImportTemplateBO.getHeader()).build(); + excelWriter.write(ftbRosterImportTemplateBO.getData(), writeSheet); + } + excelWriter.finish(); + } + } + + /** + * 动态表头生成 + */ + public void dynamicHeaderGenerationRoster(HttpServletResponse httpServletResponse, + String fileNameOriginal, List ftbRosterImportTemplateBOS) throws IOException { + // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman + httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + httpServletResponse.setCharacterEncoding("utf-8"); + // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 + String fileName = URLEncoder.encode(fileNameOriginal + System.currentTimeMillis(), "UTF-8").replaceAll("\\+", "%20"); + httpServletResponse.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + try (OutputStream outputStream = httpServletResponse.getOutputStream()) { + ExcelWriter excelWriter = EasyExcel.write() + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .registerWriteHandler(new CustomRosterCellWriteHandler()) + .file(outputStream) + .inMemory(true).build(); + for (int i = 0; i < ftbRosterImportTemplateBOS.size(); i++) { + FtbRosterImportTemplateBO ftbRosterImportTemplateBO = ftbRosterImportTemplateBOS.get(i); + // 这里放入动态头,跳过第一个sheet + WriteSheet writeSheet = EasyExcel.writerSheet(ftbRosterImportTemplateBO.getSheetName()) + .head(ftbRosterImportTemplateBO.getHeader()).build(); + excelWriter.write(ftbRosterImportTemplateBO.getData(), writeSheet); + } + excelWriter.finish(); + } + } + + /** + * 动态表头生成(匹配对应sheet 对应的数据) + */ + public void dynamicHeaderGeneration(HttpServletResponse httpServletResponse, + String fileNameOriginal, String fileSource, + List ftbRosterImportTemplateBOS, + List pullList) throws IOException { + // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman + httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + httpServletResponse.setCharacterEncoding("utf-8"); + // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 + String fileName = URLEncoder.encode(fileNameOriginal + System.currentTimeMillis(), "UTF-8").replaceAll("\\+", "%20"); + httpServletResponse.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); + if (fileSource.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { + fileSource = fileSource.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length()); + } + // 匹配对应的数据进行处理 数据下标构建 + for (ExcelSelectedResolve excelSelectedResolve : pullList){ + List list = new ArrayList<>(); + list.add(excelSelectedResolve.getHeaderName()); + int j = ftbRosterImportTemplateBOS.stream() + .filter(vo -> excelSelectedResolve.getSheetName().equals(vo.getSheetName())) + .findFirst() + .orElse(new FtbRosterImportTemplateBO()) + .getHeader() + .indexOf(list); + excelSelectedResolve.setRowNum(j); + } + try (InputStream inputStream = new ClassPathResource(fileSource).getInputStream(); OutputStream outputStream = httpServletResponse.getOutputStream()) { + ExcelWriter excelWriter = EasyExcel.write() + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + .registerWriteHandler(new CustomCellWriteHandler()) + .withTemplate(inputStream) + .file(outputStream) + .inMemory(true).build(); + for (int i = 0; i < ftbRosterImportTemplateBOS.size(); i++) { + FtbRosterImportTemplateBO ftbRosterImportTemplateBO = ftbRosterImportTemplateBOS.get(i); + // 这里放入动态头,跳过第一个sheet + WriteSheet writeSheet = EasyExcel.writerSheet(ftbRosterImportTemplateBO.getSheetName()) + .registerWriteHandler(new SelectedSheetWriteHandler(pullList)) + .head(ftbRosterImportTemplateBO.getHeader()).build(); + excelWriter.write(ftbRosterImportTemplateBO.getData(), writeSheet); + } + excelWriter.finish(); + } + + } + + + /** + * 通用导入 + */ + public void universalImport(Class tClass, + InputStream inputStream, + BaseEasyExcelCommonListener baseEasyExcelCommonListener, + Integer sheetNo, + Integer... integers) throws IOException { + int integer = 1; + if (integers.length > 0) integer = integers[0]; + baseEasyExcelCommonListener.before(); + EasyExcel.read(inputStream, tClass, new BaseEasyExcelReadListener<>(baseEasyExcelCommonListener)) + .headRowNumber(integer) + .sheet(sheetNo).doRead(); + baseEasyExcelCommonListener.after(); + } + + @Slf4j + public class CustomRosterCellWriteHandler implements CellWriteHandler { + + @Override + public void afterCellDispose(CellWriteHandlerContext context) { + Cell cell = context.getCell(); + // 表头样式判断 + if (cell.getRowIndex() == 0) { + Workbook workbook = cell.getSheet().getWorkbook(); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 11); + XSSFRichTextString richTextString = new XSSFRichTextString(cell.getStringCellValue()); + if (cell.getStringCellValue().contains("*")) { + font.setColor(IndexedColors.RED.getIndex()); + richTextString.applyFont(0, 1, font); + font.setColor(IndexedColors.BLACK.getIndex()); + richTextString.applyFont(1, cell.getStringCellValue().length(), font); + } else { + font.setColor(IndexedColors.BLACK.getIndex()); + richTextString.applyFont(font); + } + cell.setCellValue(richTextString); + } + } + + + } + + @Slf4j + public class CustomCellWriteHandler implements CellWriteHandler { + + @Override + public void afterCellDispose(CellWriteHandlerContext context) { + Cell cell = context.getCell(); + // 表头样式判断 + if (cell.getRowIndex() == 0) { + Workbook workbook = cell.getSheet().getWorkbook(); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 11); + + String cellValue = cell.getStringCellValue(); + if (cellValue != null && !cellValue.isEmpty()) { + if (cellValue.contains("*")) { + font.setColor(IndexedColors.RED.getIndex()); + // 使用 POI 的原生方式设置样式,避免转换为 RichTextString + CellStyle cellStyle = cell.getCellStyle(); + cellStyle.setFont(font); + } else { + font.setColor(IndexedColors.BLACK.getIndex()); + CellStyle cellStyle = cell.getCellStyle(); + cellStyle.setFont(font); + } + } + } + } + + + } + + @Slf4j + @Data + public class SelectedSheetWriteHandler implements SheetWriteHandler{ + + private final List resolveList; + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + // 这里可以对cell进行任何操作 + Sheet sheet = writeSheetHolder.getSheet(); + String sheetName = sheet.getSheetName(); + DataValidationHelper helper = sheet.getDataValidationHelper(); + for (ExcelSelectedResolve resolve : resolveList) { + if (!sheetName.equals(resolve.getSheetName()) || resolve.getRowNum() == -1){ + continue; + } + // 设置下拉列表的行: 首行,末行,首列,末列 + CellRangeAddressList rangeList = new CellRangeAddressList(resolve.getFirstRow(), + resolve.getLastRow(), + resolve.getRowNum(), + resolve.getRowNum()); + // 设置下拉列表的值 + DataValidationConstraint constraint = helper.createExplicitListConstraint(resolve.getSource()); + // 设置约束 + DataValidation validation = helper.createValidation(constraint, rangeList); + // 阻止输入非下拉选项的值 + validation.setErrorStyle(DataValidation.ErrorStyle.STOP); + validation.setShowErrorBox(true); + validation.setSuppressDropDownArrow(true); + validation.createErrorBox("提示", "请输入下拉选项中的内容"); + sheet.addValidationData(validation); + } + } + } +} + + diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ExcelSelectedResolve.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ExcelSelectedResolve.java new file mode 100644 index 0000000..5633154 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ExcelSelectedResolve.java @@ -0,0 +1,33 @@ +package jnpf.util.excel; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Data +public class ExcelSelectedResolve{ + /** + * 需要匹配的sheet名称 + */ + private String sheetName; + /** + * 需要匹配的header名称 + */ + private String headerName; + /** + * 下拉内容 自己构建 + */ + private String[] source; + + /** + * 设置下拉框的起始行,默认为第二行 + */ + private int firstRow = 1; + + /** + * 设置下拉框的结束行,默认为最后一行 + */ + private int lastRow = 100; + + private int rowNum; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ShortChainUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ShortChainUtils.java new file mode 100644 index 0000000..bff90cd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/excel/ShortChainUtils.java @@ -0,0 +1,103 @@ +package jnpf.util.excel; + + +import cn.hutool.core.util.StrUtil; +import lombok.experimental.UtilityClass; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Base64; + +/** + * 短链工具类 + * + * @author fantaibao + * @date 2024/02/02 + */ +@UtilityClass +public class ShortChainUtils { + + /** + * 短链地址生成 + * + * @param url 网址 + * @return {@link String} + */ + public String[] shortUrl(String url) { + // 可以自定义生成 MD5 加密字符传前的混合 KEY + String key = "wang6ge666"; + // 要使用生成 URL 的字符 + String[] chars = new String[]{"a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", + "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", + "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z" + + }; + // 对传入网址进行 MD5 加密 + String hex = md5ByHex(key + url); + + String[] resUrl = new String[4]; + for (int i = 0; i < 4; i++) { + + // 把加密字符按照 8 位一组 16 进制与 0x3FFFFFFF 进行位与运算 + String sTempSubString = hex.substring(i * 8, i * 8 + 8); + + // 这里需要使用 long 型来转换,因为 Inteper .parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用long ,则会越界 + long lHexLong = 0x3FFFFFFF & Long.parseLong(sTempSubString, 16); + String outChars = ""; + for (int j = 0; j < 6; j++) { + // 把得到的值与 0x0000003D 进行位与运算,取得字符数组 chars 索引 + long index = 0x0000003D & lHexLong; + // 把取得的字符相加 + outChars += chars[(int) index]; + // 每次循环按位右移 5 位 + lHexLong = lHexLong >> 5; + } + // 把字符串存入对应索引的输出数组 + resUrl[i] = outChars; + } + return resUrl; + } + + public String md5ByHex(String src) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] b = src.getBytes(); + md.reset(); + md.update(b); + byte[] hash = md.digest(); + String hs = ""; + String stmp = ""; + for (int i = 0; i < hash.length; i++) { + stmp = Integer.toHexString(hash[i] & 0xFF); + if (stmp.length() == 1) + hs = hs + "0" + stmp; + else { + hs = hs + stmp; + } + } + return hs.toUpperCase(); + } catch (Exception e) { + return ""; + } + } + + /** + * 长链decode + * + * @param encodeUrl 编码 + * @return {@link String} + */ + public String reverseLongChain(String encodeUrl) { + if (StrUtil.isNotBlank(encodeUrl)) { + String decode = URLDecoder.decode(encodeUrl, StandardCharsets.UTF_8); + byte[] decoded = Base64.getDecoder().decode(decode); + return new String(decoded); + } + return encodeUrl; + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImConst.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImConst.java new file mode 100644 index 0000000..805f8ed --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImConst.java @@ -0,0 +1,27 @@ +package jnpf.util.im; + +/** + * todo + * + * @author Flynn Chan + * @create 2024-04-10 + */ +public class ImConst { + + public static final String SYSTEM_APP_NAME = "人事管理"; + public static final String HR_APP_NAME = "人事管理"; + public static final String SYSTEM_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/665ecb69e4b0ae5df114c87a.png"; + public static final String HR_LOGO = "https://jnpf-resource-1304460613.cos.ap-chengdu.myqcloud.com/jnpf-resources/UserAvatar/665ecb69e4b0ae5df114c87a.png"; + /** + * 转正 + **/ + public static String TITLE_BECOME_REGULAR_EMPLOYEE = "转正结果"; + public static String SUCCESSFULLY_BECAME_A_REGULAR_EMPLOYEE = "%s, 恭喜您通过试用期考核,成为正式员工!希望今后继续努力,取得更好成绩。"; + public static String FAILED_TO_BECOME_A_REGULAR_EMPLOYEE = "%s, 很抱歉通知您,因没有达到岗位期望要求,您的试用期考核结果为不合格,感谢您在公司的这段时间以来的努力和表现!"; + /** + * 入职登记邀请 + **/ + public static String TITLE_INVITE_TO_JOIN = "邀请填写入职登记表"; + public static String INVITE_TO_JOIN = "您的入职登记表还未提交,现在就去填写吧!"; + public static String BUTTON_INVITE_TO_JOIN = "点击填写入职信息"; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImMessageNoticeUtils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImMessageNoticeUtils.java new file mode 100644 index 0000000..4a8e16d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/im/ImMessageNoticeUtils.java @@ -0,0 +1,108 @@ +package jnpf.util.im; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import com.alibaba.fastjson.JSON; +import jnpf.ImRobotApi; +import jnpf.from.*; +import jnpf.model.im.UserAndLinkDTO; +import jnpf.model.im.UserSuccessDTO; +import jnpf.permission.UserApi; +import jnpf.permission.entity.UserEntity; +import jnpf.util.UserProvider; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class ImMessageNoticeUtils { + @Autowired + private UserApi userApi; + @Resource + private ImRobotApi imRobotApi; + @Resource + private UserProvider userProvider; + + public void becomeRegularEmployee(List successDTOS) { + successDTOS.forEach( + dto -> { + if (CollUtil.isNotEmpty(dto.getUserIds())) { + List userEntityList = userApi.getUserName(dto.getUserIds()); + userEntityList.forEach(userEntity -> { + SingleSendRobotNoticeForm singleSendRobotNoticeForm = new SingleSendRobotNoticeForm(); + singleSendRobotNoticeForm.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + singleSendRobotNoticeForm.setMessageAttributionRobotMpId("__UNI__1EF82DA"); + singleSendRobotNoticeForm.setTenantId(userProvider.get().getTenantId()); + singleSendRobotNoticeForm.setToUserIds(List.of(userEntity.getId())); + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setAppName(ImConst.SYSTEM_APP_NAME); + robotNoticeDataForm.setLogo(ImConst.SYSTEM_LOGO); + robotNoticeDataForm.setTitle(ImConst.TITLE_BECOME_REGULAR_EMPLOYEE); + String content = String.format(dto.getSuccess() ? ImConst.SUCCESSFULLY_BECAME_A_REGULAR_EMPLOYEE : ImConst.FAILED_TO_BECOME_A_REGULAR_EMPLOYEE, userEntity.getRealName()); + robotNoticeDataForm.setContent(content); + singleSendRobotNoticeForm.setRobotNoticeDataForm(robotNoticeDataForm); + imRobotApi.sendSingleRobotNotice(singleSendRobotNoticeForm); + }); + + } + } + ); + } + + public void inviteToJoin(List dtoList) { + if (CollUtil.isNotEmpty(dtoList)) { + List userEntityList = userApi.getUserName(dtoList.stream().map(UserAndLinkDTO::getUserId).distinct().collect(Collectors.toList())); + dtoList.forEach(dto -> { + UserEntity thisUser = userEntityList.stream().filter(user -> user.getId().equals(dto.getUserId())).findFirst().orElse(null); + if (thisUser != null) { + SingleSendRobotNoticeForm singleSendRobotNoticeForm = new SingleSendRobotNoticeForm(); + singleSendRobotNoticeForm.setRobotTypeEnum(ImRobotTypeEnum.FTB_XZS); + singleSendRobotNoticeForm.setTenantId(userProvider.get().getTenantId()); + singleSendRobotNoticeForm.setToUserIds(List.of(thisUser.getId())); + + SendRobotNoticeDataForm robotNoticeDataForm = new SendRobotNoticeDataForm(); + robotNoticeDataForm.setAppName(ImConst.HR_APP_NAME); + robotNoticeDataForm.setLogo(ImConst.HR_LOGO); + robotNoticeDataForm.setTitle(ImConst.TITLE_INVITE_TO_JOIN); + String content = String.format(ImConst.INVITE_TO_JOIN); + + robotNoticeDataForm.setContent(content); + + if (StrUtil.isNotBlank(dto.getLinkUrl())) { + JumpUrlListModel jumpUrlListModel = new JumpUrlListModel(); + jumpUrlListModel.setDisplayMethodEnum(JumpUrlListModel.DisplayMethodEnum.TRUE); + jumpUrlListModel.setButtonName(ImConst.BUTTON_INVITE_TO_JOIN); + jumpUrlListModel.setReqMethod(JumpUrlListModel.ReqMethodEnum.GET); + jumpUrlListModel.setType(1); + jumpUrlListModel.setUrl(dto.getLinkUrl()); + MiniAppUrl miniAppUrl = new MiniAppUrl(); + miniAppUrl.setMpId("__UNI__1EF82DA"); + String[] split = dto.getLinkUrl().split("url="); + miniAppUrl.setMpPage(split[0]); + JSONObject entries = new JSONObject(); + entries.set("url",split[1]); + miniAppUrl.setMpParam(JSON.toJSONString(entries)); + jumpUrlListModel.setMiniAppUrl(miniAppUrl); + + log.error("入职请链接生成: " + dto.getLinkUrl()); + + LinkedList jumpUrlListModels = new LinkedList<>(); + jumpUrlListModels.add(jumpUrlListModel); + robotNoticeDataForm.setJumpUrlList(jumpUrlListModels); + } + + singleSendRobotNoticeForm.setRobotNoticeDataForm(robotNoticeDataForm); + + imRobotApi.sendSingleRobotNotice(singleSendRobotNoticeForm); + } + }); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/mapper/MybatisUtil.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/mapper/MybatisUtil.java new file mode 100644 index 0000000..bf205d9 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/mapper/MybatisUtil.java @@ -0,0 +1,109 @@ +package jnpf.util.mapper; + +import cn.hutool.core.util.ArrayUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; + +import java.security.SecureRandom; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class MybatisUtil { + + /** + * 根据字段查询 + * @param mapper 持久层DAO + * @param c 字段 + * @param value 值 + * @param + * @return + */ + public static T findByFiled(BaseMapper mapper, SFunction c, Object value, Boolean isDelete) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(c, value); + if (isDelete) { + queryWrapper.eq("F_DeleteMark", 0); + } + return mapper.selectOne(queryWrapper); + } + + /** + * 根据字段查询列表 + * @param mapper 持久层DAO + * @param c 字段 + * @param value 值 + * @param + * @return + */ + public static List findListByFiled(BaseMapper mapper, SFunction c, Object value, Boolean hasDelete) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.lambda() + .eq(c, value); + if (hasDelete) { + queryWrapper.eq("F_DeleteMark", 0); + } + return mapper.selectList(queryWrapper); + } + + /** + * 根据字段查询 + * @param mapper 持久层DAO + * @param c 字段 + * @param value 值 + * @param + * @return + */ + public static Integer findCountByField(BaseMapper mapper, SFunction c, V... value) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + LambdaQueryWrapper lambdaQueryWrapper = queryWrapper.lambda(); + if (ArrayUtil.isNotEmpty(value)) { + for (V v : value) { + lambdaQueryWrapper.eq(c, value); + } + } + return Integer.parseInt(String.valueOf(mapper.selectCount(queryWrapper))); + } + + /** + * 随机查询 + * + * @param mapper 持久层DAO + * @param limit 随机条数 + * @return java.util.List + * @since 2021/8/10 15:30 + */ + public static List getAny(BaseMapper mapper, T condition, Integer limit) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(condition); + Integer total = Integer.parseInt(String.valueOf(mapper.selectCount(wrapper))); + if (limit == null || limit <= 0 || total == 0) { + return Collections.emptyList(); + } + List list = Optional.of(limit).filter(l -> l > total).map(l -> mapper.selectList(wrapper)).orElseGet(() -> mapper.selectList(wrapper.last("LIMIT " + new SecureRandom().nextInt(total - (limit - 1)) + "," + limit))); + Collections.shuffle(list); + return list; + } + + /** + * 随机查询 + * + * @param mapper 持久层DAO + * @param limit 随机条数 + * @return java.util.List + * @since 2021/8/10 15:30 + */ + public static List getAny(BaseMapper mapper, T condition, Integer limit, QueryWrapper wrapper) { +// LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(condition); + Integer total = Integer.parseInt(String.valueOf(mapper.selectCount(wrapper))); + if (limit == null || limit <= 0 || total == 0) { + return Collections.emptyList(); + } + List list = Optional.of(limit).filter(l -> l > total).map(l -> mapper.selectList(wrapper)).orElseGet(() -> mapper.selectList(wrapper.last("LIMIT " + new SecureRandom().nextInt(total - (limit - 1)) + "," + limit))); + Collections.shuffle(list); + return list; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/permssion/V2Utils.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/permssion/V2Utils.java new file mode 100644 index 0000000..378a8e3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/util/permssion/V2Utils.java @@ -0,0 +1,263 @@ +package jnpf.util.permssion; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import jnpf.model.common.CheckVo; +import jnpf.permission.model.util.IdDiffResult; +import jnpf.permission.model.util.IdPair; +import jnpf.permission.model.util.IdPairDiffResult; +import jnpf.permission.vo.TreeNodeVo; +import jnpf.util.JsonUtil; + +import java.util.*; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * todo + * + * @author Flynn Chan + * @create 2025-10-02 + */ +public class V2Utils { + + + public static > List changeToTreeNoParent(List list, Class c) { + + CheckVo checkVo = new CheckVo(false); + List listCopy = JsonUtil.getJsonToList(list, c); + for (T t : list) { + T tt = listCopy.stream().filter(v -> v.getId().equals(t.getId())).findFirst().orElse(null); + if (tt == null) { + continue; + } + findParent(checkVo, listCopy, tt); + if (checkVo.getFlag()) { + listCopy.removeIf(v -> v.getId().equals(t.getId())); + checkVo.setFlag(false); + } + } + return listCopy; + } + + public static > List changeToTreeNoParentFast(List list, Class c) { + // 拷贝副本 + List listCopy = BeanUtil.copyToList(list, c); + + // 构建 Map 方便快速访问 + Map nodeMap = listCopy.stream().collect(Collectors.toMap(T::getId, Function.identity())); + + // 存放根节点 + List rootList = new ArrayList<>(); + + for (T node : listCopy) { + String pid = node.getPid(); + if (StrUtil.isNotBlank(pid) && nodeMap.containsKey(pid)) { + T parent = nodeMap.get(pid); + parent.getChildren().add(node); + } else { + rootList.add(node); + } + } + + return rootList; + } + + + private static > void findParent(CheckVo checkVo, List list, T t) { + + if (!list.isEmpty()) { + for (T value : list) { + if (value.getId().equals(t.getPid())) { + value.getChildren().add(t); + checkVo.setFlag(true); + return; + } + findParent(checkVo, value.getChildren(), t); + } + } + } + + public static > void nodeAddChildren(List list, ConcurrentMap> map) { + + if (!list.isEmpty()) { + for (T value : list) { + List children = map.get(value.getId()); + if (null != children && !children.isEmpty()) { + value.getChildren().addAll(children); + } + nodeAddChildren(value.getChildren(), map); + } + } + } + + /** + * 树递归排序 + * + * @param list 数据源 + */ + public static > void treeSort(List list) { + + if (!list.isEmpty()) { + list.sort(Comparator.nullsLast(Comparator.comparing(t -> t.getChildren().size(), Comparator.nullsLast(Comparable::compareTo)))); + for (T value : list) { + treeSort(value.getChildren()); + } + } + } + + /** + * 将对象base64编码 + * + * @param jsonToBean 对象 + * @return base64编码字符串 + */ + public static String encodeJson(Object jsonToBean) { + return new JSONObject(jsonToBean).toString(); + } + + /** + * 把逗号分隔的字符串转换为数组 + * + * @param input + * @return + */ + public static List convertToList(String input) { + if (input == null || input.trim().isEmpty()) { + return Collections.emptyList(); + } + + // 判断格式是否符合:多个用逗号隔开的非空串 + if (!input.matches("^(\\S+)(,\\S+)*$")) { + return Collections.emptyList(); + } + + // 拆分并转为 List + return new ArrayList<>(Arrays.asList(input.split(","))); + } + + /** + * 始终取list最后一条数据并且剔除,没有则空 + */ + public static String pollLast(List list) { + if (list == null || list.isEmpty()) { + return null; + } + // 移除并返回最后一条 + return list.remove(list.size() - 1); + } + + /** + * 使用 Set 比较两个 ID 列表,适用于大数据量。 + * + * @param oldIds 原始 ID 列表 + * @param targetIds 目标 ID 列表 + * @return 包含 removedIds 和 addedIds 的结果对象 + */ + public static IdDiffResult compareIdLists(List oldIds, List targetIds) { + oldIds = oldIds == null ? new ArrayList<>() : oldIds; + targetIds = targetIds == null ? new ArrayList<>() : targetIds; + Set oldSet = new HashSet<>(oldIds); + Set targetSet = new HashSet<>(targetIds); + + Set removedSet = new HashSet<>(oldSet); + removedSet.removeAll(targetSet); + + Set addedSet = new HashSet<>(targetSet); + addedSet.removeAll(oldSet); + + return new IdDiffResult(new ArrayList<>(removedSet), new ArrayList<>(addedSet)); + } + + + /** + * 获取两个列表对象(两个String属性)的差异 + * + * @param oldList 旧列表 + * @param targetList 新列表 + */ + public static IdPairDiffResult compareIdPairs(List oldList, List targetList) { + oldList = oldList == null ? new ArrayList<>() : oldList; + targetList = targetList == null ? new ArrayList<>() : targetList; + + // 用 id1-id2 作为唯一 key 做对比 + Map oldMap = oldList.stream() + .collect(Collectors.toMap(V2Utils::buildKey, p -> p)); + + Map targetMap = targetList.stream() + .collect(Collectors.toMap(V2Utils::buildKey, p -> p)); + + Set oldKeys = oldMap.keySet(); + Set targetKeys = targetMap.keySet(); + + // removed: old 有而 target 没有的 key + List removed = oldKeys.stream() + .filter(k -> !targetKeys.contains(k)) + .map(oldMap::get) + .collect(Collectors.toList()); + + // added: target 有而 old 没有的 key + List added = targetKeys.stream() + .filter(k -> !oldKeys.contains(k)) + .map(targetMap::get) + .collect(Collectors.toList()); + + return new IdPairDiffResult(removed, added); + } + + private static String buildKey(IdPair pair) { + return (pair.getId1() == null ? "" : pair.getId1()) + "-" + (pair.getId2() == null ? "" : pair.getId2()); + } + + + // 提取字符串中的第一个连续数字 + public static Integer extractNumber(String str) { + if (str == null) return null; + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\d+"); + java.util.regex.Matcher matcher = pattern.matcher(str); + if (matcher.find()) { + return Integer.parseInt(matcher.group()); + } + return null; + } + + /** + * 根据倒序索引获取单个组织ID + * + * @param organizeIdTree 逗号分隔的组织ID树 + * @param count 倒序第几个,1表示最后一个,2表示倒数第二个 + * @return 对应的ID,如果越界返回 null + */ + public static String getIdByReverseOrder(String organizeIdTree, int count) { + if (organizeIdTree == null || organizeIdTree.isEmpty() || count <= 0) { + return null; + } + String[] ids = organizeIdTree.split(","); + int index = ids.length - count; + if (index < 0) { + return null; // 越界时返回 null + } + return ids[index]; + } + + /** + * 获取最后 count 个组织ID + * + * @param organizeIdTree 逗号分隔的组织ID树 + * @param count 要返回的个数(从最后往前数) + * @return List,如果 count 超过长度则返回全部 + */ + public static List getLastIds(String organizeIdTree, int count) { + if (organizeIdTree == null || organizeIdTree.isEmpty() || count <= 0) { + return Collections.emptyList(); + } + String[] ids = organizeIdTree.split(","); + int length = ids.length; + + int start = Math.max(length - count, 0); + + return new ArrayList<>(Arrays.asList(ids).subList(start, length)); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/v2/personnels/V2PersonnelController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/v2/personnels/V2PersonnelController.java new file mode 100644 index 0000000..4d553ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/v2/personnels/V2PersonnelController.java @@ -0,0 +1,17 @@ +package jnpf.v2.personnels; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 2.0 人事管理接口 + * + * @author Flynn Chan + * @create 2025-03-18 + */ +@RequestMapping("/v2/personnel") +@RestController +public class V2PersonnelController { + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyClockInController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyClockInController.java new file mode 100644 index 0000000..81620cf --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyClockInController.java @@ -0,0 +1,278 @@ +package jnpf.workflow.controller; + +import cn.hutool.json.JSONUtil; +import jnpf.base.ActionResult; +import jnpf.constant.MsgCode; +import jnpf.engine.enums.FlowStatusEnum; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.entity.workflow.ApplyAttendanceOutside; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.enums.personnel.FtbPersonnelsCheckStatusCodeEnum; +import jnpf.model.workflow.dto.ApplyAttendanceChangeDto; +import jnpf.model.workflow.dto.ApplyAttendanceOutsideDto; +import jnpf.model.workflow.dto.ApplyAttendanceRepairDto; +import jnpf.model.workflow.dto.ApplyAttendanceViolationDto; +import jnpf.model.workflow.vo.ApplyAttendanceChangeVo; +import jnpf.model.workflow.vo.ApplyAttendanceOutsideVo; +import jnpf.model.workflow.vo.ApplyAttendanceRepairVo; +import jnpf.model.workflow.vo.ClockKindVo; +import jnpf.util.ConstantUtil; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import jnpf.workflow.service.ApplyAttendanceChangeService; +import jnpf.workflow.service.ApplyAttendanceOutsideService; +import jnpf.workflow.service.ApplyAttendanceRepairService; +import jnpf.workflow.service.ApplyAttendanceViolationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 打卡申请控制器 + * + * @author yanwenfu + * @create 2023-12-11 + */ +@Slf4j +@RestController +@RequestMapping(value = "/apply") +public class ApplyClockInController { + + @Resource + private ApplyAttendanceChangeService applyAttendanceChangeService; + + @Resource + private ApplyAttendanceRepairService applyAttendanceRepairService; + + @Resource + private ApplyAttendanceOutsideService applyAttendanceOutsideService; + + @Resource + private ApplyAttendanceViolationService applyAttendanceViolationService; + + /** + * 查询审批记录 + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/clockIn/{id}") + public ActionResult getApplyAttendanceChange(@PathVariable(value = "id") String id) { + + ApplyAttendanceChangeVo vo = applyAttendanceChangeService.getApplyAttendanceChange(id); + return ActionResult.success(vo); + } + + /** + * 新建考勤变更申请 + * @param applyAttendanceChangeDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/clockIn") + public Object create(@RequestBody @Valid ApplyAttendanceChangeDto applyAttendanceChangeDto) { + + log.error("出勤变更发起 -> {}", JSONUtil.toJsonStr(applyAttendanceChangeDto)); + ActionResult actionResult = new ActionResult<>(); + try { + // 解析 changeData + if (null == applyAttendanceChangeDto.getChangeData() || applyAttendanceChangeDto.getChangeData().length != 2) { + throw new Exception("变更类型异常"); + } + applyAttendanceChangeDto.setChangeType(Integer.parseInt(applyAttendanceChangeDto.getChangeData()[0])); + applyAttendanceChangeDto.setChangeMinute(Integer.parseInt(applyAttendanceChangeDto.getChangeData()[1])); + ApplyAttendanceChange entity = JsonUtil.getJsonToBean(applyAttendanceChangeDto, ApplyAttendanceChange.class); + entity.setStatus(ConstantUtil.STATUS_PENDING_APPROVAL); + applyAttendanceChangeService.save(entity, applyAttendanceChangeDto); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 修改考勤变更申请 + * @param applyAttendanceChangeDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/clockIn/{id}") + public Object update(@RequestBody @Valid ApplyAttendanceChangeDto applyAttendanceChangeDto, @PathVariable("id") String id) throws Exception { + + ApplyAttendanceChange entity = JsonUtil.getJsonToBean(applyAttendanceChangeDto, ApplyAttendanceChange.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(applyAttendanceChangeDto.getStatus())) { + applyAttendanceChangeService.save(entity, applyAttendanceChangeDto); + return ActionResult.success(MsgCode.SU002.get()); + } + applyAttendanceChangeService.submit(id, entity, applyAttendanceChangeDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + /** + * 查询补卡审批记录 + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/repair/{id}") + public ActionResult getApplyAttendanceRepair(@PathVariable(value = "id") String id) { + + ApplyAttendanceRepairVo vo = applyAttendanceRepairService.getApplyAttendanceRepair(id); + return ActionResult.success(vo); + } + + /** + * 新建补卡审批申请 + * @param applyAttendanceRepairDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/repair") + public ActionResult create(@RequestBody @Valid ApplyAttendanceRepairDto applyAttendanceRepairDto) { + + ApplyAttendanceRepair entity = JsonUtil.getJsonToBean(applyAttendanceRepairDto, ApplyAttendanceRepair.class); + entity.setStatus(ConstantUtil.STATUS_PENDING_APPROVAL); + ActionResult actionResult = new ActionResult<>(); + try { + applyAttendanceRepairService.save(entity, applyAttendanceRepairDto); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + /*if (FlowStatusEnum.save.getMessage().equals(applyAttendanceRepairDto.getStatus())) { + applyAttendanceRepairService.save(id, entity, applyAttendanceRepairDto); + return ActionResult.success(MsgCode.SU002.get()); + } + applyAttendanceRepairService.submit(id, entity, applyAttendanceRepairDto); + return ActionResult.success(MsgCode.SU006.get());*/ + } + + /** + * 修改补卡审批申请 + * @param applyAttendanceRepairDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/repair/{id}") + public Object update(@RequestBody @Valid ApplyAttendanceRepairDto applyAttendanceRepairDto, @PathVariable("id") String id) throws Exception { + + ApplyAttendanceRepair entity = JsonUtil.getJsonToBean(applyAttendanceRepairDto, ApplyAttendanceRepair.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(applyAttendanceRepairDto.getStatus())) { + applyAttendanceRepairService.save(entity, applyAttendanceRepairDto); + return ActionResult.success(MsgCode.SU002.get()); + } + applyAttendanceRepairService.submit(id, entity, applyAttendanceRepairDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + /** + * 查询外勤审批记录 + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/outside/{id}") + public ActionResult getApplyAttendanceOutside(@PathVariable(value = "id") String id) { + + ApplyAttendanceOutsideVo vo = applyAttendanceOutsideService.getApplyAttendanceOutside(id); + return ActionResult.success(vo); + } + + /** + * 新建外勤审批申请 + * @param applyAttendanceOutsideDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/outside") + public Object create(@RequestBody @Valid ApplyAttendanceOutsideDto applyAttendanceOutsideDto) { + if (StringUtil.isNotEmpty(applyAttendanceOutsideDto.getSpecialBusinessId())) { + applyAttendanceOutsideDto.setClockInId(applyAttendanceOutsideDto.getSpecialBusinessId()); + } + ApplyAttendanceOutside entity = JsonUtil.getJsonToBean(applyAttendanceOutsideDto, ApplyAttendanceOutside.class); + entity.setStatus(ConstantUtil.STATUS_PENDING_APPROVAL); + ActionResult actionResult = new ActionResult<>(); + try { + applyAttendanceOutsideService.save(entity, applyAttendanceOutsideDto); + } catch (Exception e) { + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 修改外勤审批申请 + * @param applyAttendanceOutsideDto 表单对象 + * @return java.lang.Object + */ + @PutMapping("/outside") + public Object update(@RequestBody @Valid ApplyAttendanceOutsideDto applyAttendanceOutsideDto) throws Exception { + ApplyAttendanceOutside entity = JsonUtil.getJsonToBean(applyAttendanceOutsideDto, ApplyAttendanceOutside.class); + entity.setId(applyAttendanceOutsideDto.getTaskId()); + if (FlowStatusEnum.save.getMessage().equals(applyAttendanceOutsideDto.getStatus())) { + applyAttendanceOutsideService.save(entity, applyAttendanceOutsideDto); + return ActionResult.success(MsgCode.SU002.get()); + } + applyAttendanceOutsideService.submit(entity, applyAttendanceOutsideDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + /** + * 新建违规打卡审批申请 + * @param applyAttendanceViolationDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/violation") + public Object createViolation(@RequestBody @Valid ApplyAttendanceViolationDto applyAttendanceViolationDto) { + + log.error("违规打卡审批: {}", applyAttendanceViolationDto.toString()); + if (StringUtil.isNotEmpty(applyAttendanceViolationDto.getSpecialBusinessId())) { + applyAttendanceViolationDto.setClockInId(applyAttendanceViolationDto.getSpecialBusinessId()); + } + ActionResult actionResult = new ActionResult<>(); + try { + applyAttendanceViolationService.saveRecord(applyAttendanceViolationDto); + } catch (Exception e) { + e.printStackTrace(); + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_REFUSE.getCode()); + actionResult.setMsg(e.getMessage()); + return actionResult; + } + actionResult.setCode(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getCode()); + actionResult.setMsg(FtbPersonnelsCheckStatusCodeEnum.CHECK_PASS.getMsg()); + return actionResult; + } + + /** + * 获取打卡类型 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/violation/clock-kind") + public ActionResult> getClockKindList() { + + List list = List.of(new ClockKindVo(1, "内勤打卡"), new ClockKindVo(2, "外勤打卡")); + return ActionResult.success(list); + } + + /** + * 获取打卡设备类型 + * @return jnpf.base.ActionResult> + */ + @GetMapping(value = "/violation/device-type") + public ActionResult> getDeviceTypeList() { + + List list = List.of(new ClockKindVo(1, "地点打卡"), new ClockKindVo(2, "WIFI打卡"), new ClockKindVo(3, "考勤机打卡")); + return ActionResult.success(list); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyController.java new file mode 100644 index 0000000..2b343b6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/ApplyController.java @@ -0,0 +1,153 @@ +package jnpf.workflow.controller; + +import cn.hutool.core.date.DateUtil; +import jnpf.base.ActionResult; +import jnpf.constant.MsgCode; +import jnpf.engine.enums.FlowStatusEnum; +import jnpf.entity.workflow.AttendanceBusinessTripApprove; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveDto; +import jnpf.model.workflow.dto.AttendanceGoOutApproveDto; +import jnpf.model.workflow.vo.AttendanceBusinessTripApproveVo; +import jnpf.model.workflow.vo.AttendanceGoOutApproveVo; +import jnpf.util.JsonUtil; +import jnpf.workflow.service.BusinessTripApproveService; +import jnpf.workflow.service.GoOutApproveService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * 审批触发接口 + * + * @Author huanglinpan + * @Date 2024/5/14 18:15 + * @Version 1.0 (版本号) + */ +@Slf4j +@RestController +@RequestMapping(value = "/apply") +public class ApplyController { + + + @Resource + private BusinessTripApproveService businessTripApproveService; + + @Resource + private GoOutApproveService goOutApproveService; + + /** + * 新建出差申请 + * + * @param attendanceBusinessTripApproveDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/businessTrip/{id}") + public Object createGoOut(@RequestBody @Valid AttendanceBusinessTripApproveDto attendanceBusinessTripApproveDto, @PathVariable("id") String id) { + log.error("开始出差了参数................: {}", attendanceBusinessTripApproveDto); + AttendanceBusinessTripApprove entity = JsonUtil.getJsonToBean(attendanceBusinessTripApproveDto, AttendanceBusinessTripApprove.class); + if (!entity.getEndTime().after(entity.getStartTime())) { + entity.setEndTime(DateUtil.endOfDay(entity.getEndTime())); + } + if (FlowStatusEnum.save.getMessage().equals(attendanceBusinessTripApproveDto.getStatus())) { + businessTripApproveService.save(id, entity, attendanceBusinessTripApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + businessTripApproveService.submit(id, entity, attendanceBusinessTripApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + /** + * 查询出差审批记录 + * + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/businessTrip/{id}") + public ActionResult getGoOut(@PathVariable(value = "id") String id) { + + AttendanceBusinessTripApproveVo vo = businessTripApproveService.getOne(id); + return ActionResult.success(vo); + } + + + /** + * 修改出差申请 + * + * @param attendanceBusinessTripApproveDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/businessTrip/{id}") + public Object updateGoOut(@RequestBody @Valid AttendanceBusinessTripApproveDto attendanceBusinessTripApproveDto, @PathVariable("id") String id) { + + AttendanceBusinessTripApprove entity = JsonUtil.getJsonToBean(attendanceBusinessTripApproveDto, AttendanceBusinessTripApprove.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(attendanceBusinessTripApproveDto.getStatus())) { + businessTripApproveService.save(id, entity, attendanceBusinessTripApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + businessTripApproveService.submit(id, entity, attendanceBusinessTripApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + /** + * 新建外出申请 + * + * @param attendanceGoOutApproveDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/out/{id}") + public Object createBusinessTrip(@RequestBody @Valid AttendanceGoOutApproveDto attendanceGoOutApproveDto, @PathVariable("id") String id) { + log.error("开始外出申请了参数................: {}", attendanceGoOutApproveDto); + AttendanceGoOutApprove entity = JsonUtil.getJsonToBean(attendanceGoOutApproveDto, AttendanceGoOutApprove.class); + if (!entity.getEndTime().after(entity.getStartTime())) { + entity.setEndTime(DateUtil.endOfDay(entity.getEndTime())); + } + if (FlowStatusEnum.save.getMessage().equals(attendanceGoOutApproveDto.getStatus())) { + goOutApproveService.save(id, entity, attendanceGoOutApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + goOutApproveService.submit(id, entity, attendanceGoOutApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + /** + * 查询外出审批记录 + * + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/out/{id}") + public ActionResult getBusinessTrip(@PathVariable(value = "id") String id) { + + AttendanceGoOutApproveVo vo = goOutApproveService.getOne(id); + return ActionResult.success(vo); + } + + + /** + * 修改外出申请 + * + * @param attendanceGoOutApproveDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/out/{id}") + public Object updateBusinessTrip(@RequestBody @Valid AttendanceGoOutApproveDto attendanceGoOutApproveDto, @PathVariable("id") String id) { + + AttendanceGoOutApprove entity = JsonUtil.getJsonToBean(attendanceGoOutApproveDto, AttendanceGoOutApprove.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(attendanceGoOutApproveDto.getStatus())) { + goOutApproveService.save(id, entity, attendanceGoOutApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + goOutApproveService.submit(id, entity, attendanceGoOutApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/LeaveApproveController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/LeaveApproveController.java new file mode 100644 index 0000000..82df140 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/LeaveApproveController.java @@ -0,0 +1,84 @@ +package jnpf.workflow.controller; + +import jnpf.base.ActionResult; +import jnpf.constant.MsgCode; +import jnpf.engine.enums.FlowStatusEnum; +import jnpf.entity.workflow.AttendanceLeaveApprove; +import jnpf.model.workflow.dto.AttendanceLeaveApproveDto; +import jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo; +import jnpf.util.JsonUtil; +import jnpf.workflow.service.LeaveApproveService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/12/11 + */ +@Slf4j +@RestController +@RequestMapping(value = "/attendanceApprove/leave") +public class LeaveApproveController { + + @Resource + private LeaveApproveService leaveApproveService; + + /** + * 新建请假申请 + * @param attendanceLeaveApproveDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/{id}") + public Object create(@RequestBody @Valid AttendanceLeaveApproveDto attendanceLeaveApproveDto, @PathVariable("id") String id) { + log.error("开始请假了................"); + log.error("开始请假了参数................: {}",attendanceLeaveApproveDto); + AttendanceLeaveApprove entity = JsonUtil.getJsonToBean(attendanceLeaveApproveDto, AttendanceLeaveApprove.class); + if (FlowStatusEnum.save.getMessage().equals(attendanceLeaveApproveDto.getSubmitStatus())) { + leaveApproveService.save(id, entity, attendanceLeaveApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + leaveApproveService.submit(id, entity, attendanceLeaveApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + + + /** + * 查询请假审批记录 + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/{id}") + public ActionResult getAttendanceLeave(@PathVariable(value = "id") String id) { + + AttendanceLeaveFlowApproveVo vo = leaveApproveService.getAttendanceLeave(id); + return ActionResult.success(vo); + } + + + /** + * 修改请假申请 + * @param attendanceLeaveApproveDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/{id}") + public Object update(@RequestBody @Valid AttendanceLeaveApproveDto attendanceLeaveApproveDto, @PathVariable("id") String id) { + + AttendanceLeaveApprove entity = JsonUtil.getJsonToBean(attendanceLeaveApproveDto, AttendanceLeaveApprove.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(attendanceLeaveApproveDto.getSubmitStatus())) { + leaveApproveService.save(id, entity, attendanceLeaveApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + leaveApproveService.submit(id, entity, attendanceLeaveApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/RewardsPunishmentsApproveController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/RewardsPunishmentsApproveController.java new file mode 100644 index 0000000..ecbb703 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/RewardsPunishmentsApproveController.java @@ -0,0 +1,202 @@ +package jnpf.workflow.controller; + +import jnpf.base.ActionResult; +import jnpf.base.UserInfo; +import jnpf.config.ConfigValueUtil; +import jnpf.constant.MsgCode; +import jnpf.database.util.TenantDataSourceUtil; +import jnpf.engine.enums.FlowStatusEnum; +import jnpf.entity.workflow.PunishmentsApproval; +import jnpf.entity.workflow.RewardApproval; +import jnpf.exception.LoginException; +import jnpf.model.workflow.dto.PunishmentsApprovalDto; +import jnpf.model.workflow.dto.RewardApprovalDto; +import jnpf.model.workflow.vo.PunishmentsApprovalVo; +import jnpf.model.workflow.vo.RewardApprovalVo; +import jnpf.util.JsonUtil; +import jnpf.util.NoDataSourceBind; +import jnpf.util.StringUtil; +import jnpf.util.UserProvider; +import jnpf.util.data.DataSourceContextHolder; +import jnpf.workflow.service.PunishmentsApprovalService; +import jnpf.workflow.service.RewardService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * 此模块已废除,奖惩审批由OA提供 + * @Author huanglinpan + * @Date 2024/5/21 17:22 + * @Version 1.0 (版本号) + */ +@Slf4j +@RestController +@RequestMapping(value = "/approve") +@Deprecated(since = "人事v1.2", forRemoval = true) +public class RewardsPunishmentsApproveController { + + + @Resource + private RewardService rewardService; + + @Resource + private PunishmentsApprovalService punishmentsApprovalService; + + @Autowired + private ConfigValueUtil configValueUtil; + + + /** + * 新建奖励申请 + * @param dto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/reward/{id}") + public Object create(@RequestBody @Valid RewardApprovalDto dto, @PathVariable("id") String id) { + RewardApproval entity = JsonUtil.getJsonToBean(dto, RewardApproval.class); + if (FlowStatusEnum.save.getMessage().equals(dto.getStatus())) { + rewardService.saveReward(id, entity, dto); + return ActionResult.success(MsgCode.SU002.get()); + } + rewardService.submitReward(id, entity, dto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + + + /** + * 查询奖励审批记录 + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/reward/{id}") + public ActionResult getAttendanceLeave(@PathVariable(value = "id") String id) { + + RewardApprovalVo vo = rewardService.getReward(id); + return ActionResult.success(vo); + } + + + /** + * 修改奖励申请 + * @param dto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/reward/{id}") + public Object update(@RequestBody @Valid RewardApprovalDto dto, @PathVariable("id") String id) { + + RewardApproval entity = JsonUtil.getJsonToBean(dto, RewardApproval.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(dto.getStatus())) { + rewardService.saveReward(id, entity, dto); + return ActionResult.success(MsgCode.SU002.get()); + } + rewardService.submitReward(id, entity, dto); + return ActionResult.success(MsgCode.SU006.get()); + } + + /** + * 奖励审批通过/拒绝/撤回 + * @param tenantId 租户Id + * @param applyId 审批Id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 3.撤回 + */ + @GetMapping("/reward/pass") + @NoDataSourceBind + public void rewardApprove(@RequestParam(value = "tenantId") String tenantId,@RequestParam(value = "applyId") String applyId,@RequestParam(value = "status") Integer status) { + log.info("/reward/pass tenantId:{},applyId:{},status:{}",tenantId,applyId,status); + checkOutTenant(tenantId); + rewardService.rewardApprove(applyId, status); + } + + + /** + * 新建奖励申请 + * @param dto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/punishments/{id}") + public Object createPunishments(@RequestBody @Valid PunishmentsApprovalDto dto, @PathVariable("id") String id) { + PunishmentsApproval entity = JsonUtil.getJsonToBean(dto, PunishmentsApproval.class); + if (FlowStatusEnum.save.getMessage().equals(dto.getStatus())) { + punishmentsApprovalService.savePunishments(id, entity, dto); + return ActionResult.success(MsgCode.SU002.get()); + } + punishmentsApprovalService.submitPunishments(id, entity, dto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + + + /** + * 查询奖励审批记录 + * @param id 审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/punishments/{id}") + public ActionResult getPunishments(@PathVariable(value = "id") String id) { + + PunishmentsApprovalVo vo = punishmentsApprovalService.getPunishments(id); + return ActionResult.success(vo); + } + + + /** + * 修改奖励申请 + * @param dto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/punishments/{id}") + public Object updatePunishments(@RequestBody @Valid PunishmentsApprovalDto dto, @PathVariable("id") String id) { + + PunishmentsApproval entity = JsonUtil.getJsonToBean(dto, PunishmentsApproval.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(dto.getStatus())) { + punishmentsApprovalService.savePunishments(id, entity, dto); + return ActionResult.success(MsgCode.SU002.get()); + } + punishmentsApprovalService.submitPunishments(id, entity, dto); + return ActionResult.success(MsgCode.SU006.get()); + } + + /** + * 惩罚审批通过/拒绝/撤回 + * @param tenantId 租户Id + * @param applyId 审批Id + * @param status 是否审核通过 0.待审核 1.通过 2.未通过 3.撤回 + */ + @GetMapping("/punishments/pass") + @NoDataSourceBind + public void punishmentsApprove(@RequestParam(value = "tenantId") String tenantId,@RequestParam(value = "applyId") String applyId,@RequestParam(value = "status") Integer status) { + log.info("punishments/pass tenantId:{},applyId:{},status:{}",tenantId,applyId,status); + checkOutTenant(tenantId); + punishmentsApprovalService.punishmentsApprove(applyId, status); + } + + public void checkOutTenant(String tenantId) { + if (configValueUtil.isMultiTenancy()) { + // 判断是不是从外面直接请求 + if (StringUtil.isNotEmpty(tenantId)) { + //切换成租户库 + try { + TenantDataSourceUtil.switchTenant(tenantId); + } catch (LoginException e) { + throw new RuntimeException("切换租户失败"); + } + } else { + UserInfo userInfo = UserProvider.getUser(); + Assert.notNull(userInfo.getUserId(), "缺少租户信息"); + DataSourceContextHolder.setDatasource(userInfo.getTenantId(), userInfo.getTenantDbConnectionString(), userInfo.isAssignDataSource()); + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/SelfApproveController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/SelfApproveController.java new file mode 100644 index 0000000..e037cf7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/SelfApproveController.java @@ -0,0 +1,84 @@ +package jnpf.workflow.controller; + +import jnpf.base.ActionResult; +import jnpf.constant.MsgCode; +import jnpf.engine.enums.FlowStatusEnum; +import jnpf.entity.workflow.SelfApprove; +import jnpf.model.workflow.dto.SelfApproveDto; +import jnpf.model.workflow.vo.SelfApproveVo; +import jnpf.util.JsonUtil; +import jnpf.workflow.service.SelfApproveService; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * describe + * 借调审批控制器 + * @author HuangLinPan + * @date 2023/12/13 + */ +@RestController +@RequestMapping(value = "/attendanceApprove/selfApprove") +public class SelfApproveController { + + @Resource + private SelfApproveService selfApproveService; + + /** + * 新建借调审批 + * @param selfApproveDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/{id}") + public Object create(@RequestBody @Valid SelfApproveDto selfApproveDto, @PathVariable("id") String id) { + if (selfApproveDto.getGroupId().equals(selfApproveDto.getSelfGroupId())){ + return ActionResult.fail("不能同组借调"); + } + SelfApprove entity = JsonUtil.getJsonToBean(selfApproveDto, SelfApprove.class); + if (FlowStatusEnum.save.getMessage().equals(selfApproveDto.getSubmitStatus())) { + selfApproveService.save(id, entity, selfApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + selfApproveService.submit(id, entity, selfApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + + + /** + * 查询借调审批记录 + * @param id 加班审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/{id}") + public ActionResult getAttendanceLeave(@PathVariable(value = "id") String id) { + + SelfApproveVo vo = selfApproveService.getAttendanceLeave(id); + return ActionResult.success(vo); + } + + + /** + * 修改借调审批 + * @param selfApproveDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/{id}") + public Object update(@RequestBody @Valid SelfApproveDto selfApproveDto, @PathVariable("id") String id) { + if (selfApproveDto.getGroupId().equals(selfApproveDto.getSelfGroupId())){ + return ActionResult.fail("不能同组借调"); + } + SelfApprove entity = JsonUtil.getJsonToBean(selfApproveDto, SelfApprove.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(selfApproveDto.getSubmitStatus())) { + selfApproveService.save(id, entity, selfApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + selfApproveService.submit(id, entity, selfApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/WorkOvertimeApproveController.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/WorkOvertimeApproveController.java new file mode 100644 index 0000000..72bd190 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/controller/WorkOvertimeApproveController.java @@ -0,0 +1,91 @@ +package jnpf.workflow.controller; + +import jnpf.base.ActionResult; +import jnpf.constant.MsgCode; +import jnpf.engine.enums.FlowStatusEnum; +import jnpf.entity.workflow.AttendanceWorkOvertimeApprove; +import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto; +import jnpf.model.workflow.vo.AttendanceWorkOvertimeApproveVo; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import jnpf.workflow.service.WorkOvertimeApproveService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * describe + * 加班审批控制器 + * @author HuangLinPan + * @date 2023/12/12 + */ + +@Slf4j +@RestController +@RequestMapping(value = "/attendanceApprove/workOvertime") +public class WorkOvertimeApproveController { + + @Resource + private WorkOvertimeApproveService workOvertimeApproveService; + @Autowired + private UserProvider userProvider; + + /** + * 新建加班申请 + * @param attendanceWorkOvertimeApproveDto 表单对象 + * @return java.lang.Object + */ + @PostMapping("/{id}") + public Object create(@RequestBody @Valid AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto, @PathVariable("id") String id) { + log.error("新建加班申请 参数 : {}",attendanceWorkOvertimeApproveDto); + String userId = userProvider.get().getUserId(); + attendanceWorkOvertimeApproveDto.setUserId(userId); + AttendanceWorkOvertimeApprove entity = JsonUtil.getJsonToBean(attendanceWorkOvertimeApproveDto, AttendanceWorkOvertimeApprove.class); + if (FlowStatusEnum.save.getMessage().equals(attendanceWorkOvertimeApproveDto.getSubmitStatus())) { + workOvertimeApproveService.save(id, entity, attendanceWorkOvertimeApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + workOvertimeApproveService.submit(id, entity, attendanceWorkOvertimeApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } + + + + + /** + * 查询加班审批记录 + * @param id 加班审批id + * @return jnpf.base.ActionResult + */ + @GetMapping(value = "/{id}") + public ActionResult getAttendanceLeave(@PathVariable(value = "id") String id) { + + AttendanceWorkOvertimeApproveVo vo = workOvertimeApproveService.getAttendanceLeave(id); + return ActionResult.success(vo); + } + + + /** + * 修改加班申请 + * @param attendanceWorkOvertimeApproveDto 表单对象 + * @param id 主键 + * @return java.lang.Object + */ + @PutMapping("/{id}") + public Object update(@RequestBody @Valid AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto, @PathVariable("id") String id) { + log.error("修改加班申请 参数 : {}",attendanceWorkOvertimeApproveDto); +// String userId = userProvider.get().getUserId(); +// attendanceWorkOvertimeApproveDto.setUserId(userId); + AttendanceWorkOvertimeApprove entity = JsonUtil.getJsonToBean(attendanceWorkOvertimeApproveDto, AttendanceWorkOvertimeApprove.class); + entity.setId(id); + if (FlowStatusEnum.save.getMessage().equals(attendanceWorkOvertimeApproveDto.getSubmitStatus())) { + workOvertimeApproveService.save(id, entity, attendanceWorkOvertimeApproveDto); + return ActionResult.success(MsgCode.SU002.get()); + } + workOvertimeApproveService.submit(id, entity, attendanceWorkOvertimeApproveDto); + return ActionResult.success(MsgCode.SU006.get()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceChangeMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceChangeMapper.java new file mode 100644 index 0000000..9a56269 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceChangeMapper.java @@ -0,0 +1,23 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.model.attendance.vo.ApplyResultVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 出勤变更mapper + * + * @author yanwenfu + * @create 2023-12-11 + */ +public interface ApplyAttendanceChangeMapper extends SuperMapper { + + /** + * 批量更新旧打卡结果 + * @param list 审批打卡结果vo + */ + void updateBatchOldResult(@Param("list") List list); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceOutsideMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceOutsideMapper.java new file mode 100644 index 0000000..3484c15 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceOutsideMapper.java @@ -0,0 +1,14 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.ApplyAttendanceOutside; + +/** + * 外勤审批mapper + * + * @author yanwenfu + * @create 2023-12-13 + */ +public interface ApplyAttendanceOutsideMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceRepairMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceRepairMapper.java new file mode 100644 index 0000000..3c3a648 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceRepairMapper.java @@ -0,0 +1,23 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.model.attendance.vo.ApplyResultVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 补卡审批mapper + * + * @author yanwenfu + * @create 2023-12-13 + */ +public interface ApplyAttendanceRepairMapper extends SuperMapper { + + /** + * 批量更新旧打卡结果 + * @param list 审批打卡结果vo + */ + void updateBatchOldResult(@Param("list") List list); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceViolationMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceViolationMapper.java new file mode 100644 index 0000000..e97e42e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/ApplyAttendanceViolationMapper.java @@ -0,0 +1,13 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.ApplyAttendanceViolation; + +/** + * 违规打卡审批mapper + * + * @author yanwenfu + * @create 2025-09-22 + */ +public interface ApplyAttendanceViolationMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceLeaveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceLeaveMapper.java new file mode 100644 index 0000000..9a5c07a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceLeaveMapper.java @@ -0,0 +1,14 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.AttendanceLeaveApprove; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/12/11 + */ +public interface AttendanceLeaveMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceWorkOvertimeApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceWorkOvertimeApproveMapper.java new file mode 100644 index 0000000..b1f5928 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/AttendanceWorkOvertimeApproveMapper.java @@ -0,0 +1,13 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.AttendanceWorkOvertimeApprove; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/12/12 + */ +public interface AttendanceWorkOvertimeApproveMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/BusinessTripApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/BusinessTripApproveMapper.java new file mode 100644 index 0000000..c96e8dd --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/BusinessTripApproveMapper.java @@ -0,0 +1,13 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.AttendanceBusinessTripApprove; +import jnpf.entity.workflow.AttendanceLeaveApprove; + +/** + * @Author huanglinpan + * @Date 2024/5/14 19:07 + * @Version 1.0 (版本号) + */ +public interface BusinessTripApproveMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/GoOutApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/GoOutApproveMapper.java new file mode 100644 index 0000000..b24d074 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/GoOutApproveMapper.java @@ -0,0 +1,13 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.entity.workflow.AttendanceLeaveApprove; + +/** + * @Author huanglinpan + * @Date 2024/5/14 19:07 + * @Version 1.0 (版本号) + */ +public interface GoOutApproveMapper extends SuperMapper { +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalMapper.java new file mode 100644 index 0000000..ab57580 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalMapper.java @@ -0,0 +1,21 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.PunishmentsApproval; +import jnpf.model.workflow.dto.UserSelfDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/24 10:45 + * @Version 1.0 (版本号) + */ +public interface PunishmentsApprovalMapper extends SuperMapper { + void saveUser(@Param("id") String id, @Param("userDtos") List userDtos); + + void deleteUserById(@Param("id") String id); + + List getListById(@Param("id") String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalUserMapper.java new file mode 100644 index 0000000..56f6224 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/PunishmentsApprovalUserMapper.java @@ -0,0 +1,15 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.PunishmentsApprovalUser; + + +/** + * 惩罚审批用户映射器 + * + * @author wangchunxiang + * @date 2024/08/14 + */ +public interface PunishmentsApprovalUserMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalMapper.java new file mode 100644 index 0000000..b43e3d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalMapper.java @@ -0,0 +1,21 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.RewardApproval; +import jnpf.model.workflow.dto.UserSelfDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/21 17:27 + * @Version 1.0 (版本号) + */ +public interface RewardApprovalMapper extends SuperMapper { + void saveUser(@Param("id") String id, @Param("userDtos") List userDtos); + + void deleteUserById(@Param("id") String id); + + List getListById(@Param("id") String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalUserMapper.java new file mode 100644 index 0000000..892741a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/RewardApprovalUserMapper.java @@ -0,0 +1,15 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.RewardApprovalUser; + + +/** + * 奖励审批用户映射器 + * + * @author wangchunxiang + * @date 2024/08/14 + */ +public interface RewardApprovalUserMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveMapper.java new file mode 100644 index 0000000..68014c4 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveMapper.java @@ -0,0 +1,8 @@ +package jnpf.workflow.mapper; + +import jnpf.base.mapper.SuperMapper; +import jnpf.entity.workflow.SelfApprove; + +public interface SelfApproveMapper extends SuperMapper { + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveUserMapper.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveUserMapper.java new file mode 100644 index 0000000..f3620d7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/mapper/SelfApproveUserMapper.java @@ -0,0 +1,43 @@ +package jnpf.workflow.mapper; + +import jnpf.model.workflow.dto.UserSelfDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface SelfApproveUserMapper { + + /** + * 批量新增借调申请中的被借调用户信息 + * @param userIds 被借调用户列表 + * @param id 借调申请id + * @author hlp + */ + void addList(@Param("userIds") List userIds, @Param("id") String id); + + void addUserList(@Param("userSelfDtos") List userSelfDtos, @Param("id") String id); + + /** + * 根据借调申请id删除关联的用户信息 + * @param id 借调申请id + * @author hlp + */ + void deleteById(@Param("id") String id); + + /** + * 获取借调审批对应的借调用户列表 + * @param id 借调申请id + * @return java.util.List + * @author hlp + */ + List getListById(@Param("id") String id); + + List getIdListById(@Param("id") String id); + + /** + * 绑定被借调用户 + * @param userIds 被借调用户 + * @param id 借调Id + */ + void addUserListForOa(@Param("userIds") List userIds, @Param("id") String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceChangeService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceChangeService.java new file mode 100644 index 0000000..006bb5f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceChangeService.java @@ -0,0 +1,37 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.model.workflow.dto.ApplyAttendanceChangeDto; +import jnpf.model.workflow.vo.ApplyAttendanceChangeVo; + +/** + * 打卡申请服务 + * + * @author yanwenfu + * @create 2023-12-11 + */ +public interface ApplyAttendanceChangeService extends SuperService { + + /** + * 查询审批记录 + * @param id 审批id + * @return jnpf.model.workflow.vo.ApplyAttendanceChangeVo + */ + ApplyAttendanceChangeVo getApplyAttendanceChange(String id); + + /** + * 新增审批记录 + * @param entity 实体 + * @param applyAttendanceChangeDto 考勤变更dto + */ + void save(ApplyAttendanceChange entity, ApplyAttendanceChangeDto applyAttendanceChangeDto) throws Exception; + + /** + * 提交审批记录 + * @param id 审批id + * @param entity 实体 + * @param applyAttendanceChangeDto 考勤变更dto + */ + void submit(String id, ApplyAttendanceChange entity, ApplyAttendanceChangeDto applyAttendanceChangeDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceOutsideService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceOutsideService.java new file mode 100644 index 0000000..6472b6b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceOutsideService.java @@ -0,0 +1,36 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.ApplyAttendanceOutside; +import jnpf.model.workflow.dto.ApplyAttendanceOutsideDto; +import jnpf.model.workflow.vo.ApplyAttendanceOutsideVo; + +/** + * 外勤审批服务 + * + * @author yanwenfu + * @create 2023-12-13 + */ +public interface ApplyAttendanceOutsideService extends SuperService { + + /** + * 查询外勤审批记录 + * @param id 审批id + * @return jnpf.model.workflow.vo.ApplyAttendanceOutsideVo + */ + ApplyAttendanceOutsideVo getApplyAttendanceOutside(String id); + + /** + * 新建外勤审批申请 + * @param entity 外勤实体 + * @param applyAttendanceOutsideDto 外勤dto + */ + void save(ApplyAttendanceOutside entity, ApplyAttendanceOutsideDto applyAttendanceOutsideDto) throws Exception; + + /** + * 修改外勤审批申请 + * @param entity 外勤实体 + * @param applyAttendanceOutsideDto 外勤dto + */ + void submit(ApplyAttendanceOutside entity, ApplyAttendanceOutsideDto applyAttendanceOutsideDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceRepairService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceRepairService.java new file mode 100644 index 0000000..27ddb7d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceRepairService.java @@ -0,0 +1,38 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.model.workflow.dto.ApplyAttendanceRepairDto; +import jnpf.model.workflow.vo.ApplyAttendanceRepairVo; + +/** + * 补卡审批服务 + * + * @author yanwenfu + * @create 2023-12-13 + */ +public interface ApplyAttendanceRepairService extends SuperService { + + /** + * 查询补卡审批记录 + * @param id 审批id + * @return jnpf.model.workflow.vo.ApplyAttendanceRepairVo + */ + ApplyAttendanceRepairVo getApplyAttendanceRepair(String id); + + /** + * 保存补卡审批记录 + * @param entity 补卡实体 + * @param applyAttendanceRepairDto 补卡审批dto + * @throws Exception 异常抛出 + */ + void save(ApplyAttendanceRepair entity, ApplyAttendanceRepairDto applyAttendanceRepairDto) throws Exception; + + /** + * 提交补卡审批记录 + * @param id 审批id + * @param entity 补卡实体 + * @param applyAttendanceRepairDto 补卡审批dto + */ + void submit(String id, ApplyAttendanceRepair entity, ApplyAttendanceRepairDto applyAttendanceRepairDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceViolationService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceViolationService.java new file mode 100644 index 0000000..2273747 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyAttendanceViolationService.java @@ -0,0 +1,20 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.ApplyAttendanceViolation; +import jnpf.model.workflow.dto.ApplyAttendanceViolationDto; + +/** + * 违规打卡审批服务 + * + * @author yanwenfu + * @create 2025-09-22 + */ +public interface ApplyAttendanceViolationService extends SuperService { + + /** + * 新建违规打卡审批申请 + * @param applyAttendanceViolationDto 违规打卡dto + */ + void saveRecord(ApplyAttendanceViolationDto applyAttendanceViolationDto) throws Exception; +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyPicService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyPicService.java new file mode 100644 index 0000000..5c95b6d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/ApplyPicService.java @@ -0,0 +1,27 @@ +package jnpf.workflow.service; + +import java.util.List; + +/** + * 审批图片服务 + * + * @author yanwenfu + * @create 2025-09-22 + */ +public interface ApplyPicService { + + /** + * 获取图片数组 + * 前端给的值为[List], 所以需要再转换 + * @param picUrlList 图片路径数组 + * @return java.util.List + */ + List getPicList(List picUrlList); + + /** + * 保存图片 + * @param taskId 任务id + * @param urlList 图片列表 + */ + void saveClockPic(String taskId, List urlList); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/BusinessTripApproveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/BusinessTripApproveService.java new file mode 100644 index 0000000..c056fa7 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/BusinessTripApproveService.java @@ -0,0 +1,37 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.AttendanceBusinessTripApprove; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveDto; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveOaDto; +import jnpf.model.workflow.vo.AttendanceBusinessTripApproveVo; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/14 18:33 + * @Version 1.0 (版本号) + */ +public interface BusinessTripApproveService extends SuperService { + + void save(String id, AttendanceBusinessTripApprove entity, AttendanceBusinessTripApproveDto dto); + + + void submit(String id, AttendanceBusinessTripApprove entity, AttendanceBusinessTripApproveDto dto); + + + AttendanceBusinessTripApproveVo getOne(String id); + + /** + * 批量获取出差申请记录 + * + * @param groupIds 考勤组ID + * @param ids 出差申请ID + */ + List getBatchByIds(List groupIds, List ids); + + void saveForOa(AttendanceBusinessTripApproveOaDto approveOaDto); + + void submitForOa(String id, AttendanceBusinessTripApproveOaDto approveOaDto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/FlowTaskService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/FlowTaskService.java new file mode 100644 index 0000000..4ed3fd6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/FlowTaskService.java @@ -0,0 +1,22 @@ +package jnpf.workflow.service; + +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; + +/** + * 工作流服务 + * + * @author yanwenfu + * @create 2024-08-19 + */ +public interface FlowTaskService { + + /** + * 查询下一个节点审批信息 + * @param taskId 任务id + * @param nodeId 节点id + * @param tenantId 租户id + * @param type 类型 0 :不需要校验userOperators对象审核通过时使用 其他:默认需要校验 + * @return jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo + */ + ApproverByTaskIdAndNodeIdVo getApproveInfo(String taskId, String nodeId, String tenantId, Integer type); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/GoOutApproveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/GoOutApproveService.java new file mode 100644 index 0000000..60d437d --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/GoOutApproveService.java @@ -0,0 +1,36 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.model.attendance.dto.GoOutApproveForOaDto; +import jnpf.model.workflow.dto.AttendanceGoOutApproveDto; +import jnpf.model.workflow.vo.AttendanceGoOutApproveVo; + +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/14 18:32 + * @Version 1.0 (版本号) + */ +public interface GoOutApproveService extends SuperService { + + void save(String id, AttendanceGoOutApprove entity, AttendanceGoOutApproveDto dto); + + + void submit(String id, AttendanceGoOutApprove entity, AttendanceGoOutApproveDto dto); + + + AttendanceGoOutApproveVo getOne(String id); + + /** + * 批量获取外出申请记录 + * + * @param groupIds 考勤组ID + * @param ids 申请记录ID + * @return 外出申请记录 key=用户ID+考勤组ID + */ + List getBatchByIds(List groupIds, List ids); + + void saveForOa(GoOutApproveForOaDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/LeaveApproveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/LeaveApproveService.java new file mode 100644 index 0000000..12ade04 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/LeaveApproveService.java @@ -0,0 +1,42 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.AttendanceLeaveApprove; +import jnpf.model.attendance.dto.LeaveApproveForOaDto; +import jnpf.model.workflow.dto.AttendanceLeaveApproveDto; +import jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo; + +import java.util.List; +import java.util.Map; + +public interface LeaveApproveService extends SuperService { + + /** + * 新增审批记录 + * + * @param id 审批id + * @param entity 实体 + * @param attendanceLeaveApproveDto 请假dto + */ + void save(String id, AttendanceLeaveApprove entity, AttendanceLeaveApproveDto attendanceLeaveApproveDto); + + /** + * 提交审批记录 + * + * @param id 审批id + * @param entity 实体 + * @param attendanceLeaveApproveDto 请假dto + */ + void submit(String id, AttendanceLeaveApprove entity, AttendanceLeaveApproveDto attendanceLeaveApproveDto); + + /** + * 通过请假id获取请假详情 + * + * @param id 请假id + * @return jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo + * @author hlp + */ + AttendanceLeaveFlowApproveVo getAttendanceLeave(String id); + + void saveForOa(LeaveApproveForOaDto dto); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/PunishmentsApprovalService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/PunishmentsApprovalService.java new file mode 100644 index 0000000..42775c1 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/PunishmentsApprovalService.java @@ -0,0 +1,23 @@ +package jnpf.workflow.service; + + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.PunishmentsApproval; +import jnpf.model.workflow.dto.PunishmentsApprovalDto; +import jnpf.model.workflow.vo.PunishmentsApprovalVo; +import org.apache.ibatis.annotations.Param; + +/** + * @Author huanglinpan + * @Date 2024/5/24 10:43 + * @Version 1.0 (版本号) + */ +public interface PunishmentsApprovalService extends SuperService { + void punishmentsApprove( String applyId, Integer status); + + void savePunishments(String id, PunishmentsApproval entity, PunishmentsApprovalDto dto); + + void submitPunishments(String id, PunishmentsApproval entity, PunishmentsApprovalDto dto); + + PunishmentsApprovalVo getPunishments(String id); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/RewardService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/RewardService.java new file mode 100644 index 0000000..3475904 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/RewardService.java @@ -0,0 +1,26 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.RewardApproval; +import jnpf.model.workflow.dto.RewardApprovalDto; +import jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo; +import jnpf.model.workflow.vo.RewardApprovalVo; + +/** + * @Author huanglinpan + * @Date 2024/5/21 17:25 + * @Version 1.0 (版本号) + */ +public interface RewardService extends SuperService { + + + void saveReward(String id, RewardApproval entity, RewardApprovalDto dto); + + + void submitReward(String id, RewardApproval entity, RewardApprovalDto dto); + + + RewardApprovalVo getReward(String id); + + void rewardApprove(String applyId, Integer status); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/SelfApproveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/SelfApproveService.java new file mode 100644 index 0000000..c2166d6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/SelfApproveService.java @@ -0,0 +1,41 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.SelfApprove; +import jnpf.model.workflow.dto.SelfApproveDto; +import jnpf.model.workflow.vo.SelfApproveVo; + +import java.util.List; + +public interface SelfApproveService extends SuperService { + + /** + * 新增借调审批记录 + * @param id 审批id + * @param entity 实体 + * @param selfApproveDto dto + */ + void save(String id, SelfApprove entity, SelfApproveDto selfApproveDto); + + /** + * 提交借调审批记录 + * @param id 借调审批id + * @param entity 实体 + * @param selfApproveDto 借调审批dto + */ + void submit(String id, SelfApprove entity, SelfApproveDto selfApproveDto); + + /** + * 通过借调审批id获取借调审批详情 + * @param id 借调审批id + * @return jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo + * @author hlp + */ + SelfApproveVo getAttendanceLeave(String id); + + /** + * oa收拢保存借调 + * @param entity + */ + void saveForOa(SelfApprove entity, List userIds); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/WorkOvertimeApproveService.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/WorkOvertimeApproveService.java new file mode 100644 index 0000000..c3daeb2 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/WorkOvertimeApproveService.java @@ -0,0 +1,38 @@ +package jnpf.workflow.service; + +import jnpf.base.service.SuperService; +import jnpf.entity.workflow.AttendanceWorkOvertimeApprove; +import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto; +import jnpf.model.workflow.vo.AttendanceWorkOvertimeApproveVo; + +public interface WorkOvertimeApproveService extends SuperService { + /** + * 新增加班记录 + * @param id 审批id + * @param entity 实体 + * @param attendanceWorkOvertimeApproveDto 请假dto + */ + void save(String id, AttendanceWorkOvertimeApprove entity, AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto); + + /** + * 提交加班审批记录 + * @param id 加班审批id + * @param entity 实体 + * @param attendanceWorkOvertimeApproveDto 加班dto + */ + void submit(String id, AttendanceWorkOvertimeApprove entity, AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto); + + /** + * 通过加班id获取加班详情 + * @param id 加班id + * @return jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo + * @author hlp + */ + AttendanceWorkOvertimeApproveVo getAttendanceLeave(String id); + + /** + * oa收拢保存加班审批 + * @param entity + */ + void saveForOa(AttendanceWorkOvertimeApprove entity); +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceChangeServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceChangeServiceImpl.java new file mode 100644 index 0000000..8716245 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceChangeServiceImpl.java @@ -0,0 +1,246 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.bean.ChangeConfig; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.service.AttendanceClockInResultService; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.workflow.ApplyAttendanceChange; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.exception.QueryException; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.ApplyAttendanceChangeDto; +import jnpf.model.workflow.vo.ApplyAttendanceChangeVo; +import jnpf.permission.UserApi; +import jnpf.permission.model.user.BaseUserInfoVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.ApplyAttendanceChangeMapper; +import jnpf.workflow.service.ApplyAttendanceChangeService; +import jnpf.workflow.service.FlowTaskService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + + +/** + * 出勤变更 + * + * @author yanwenfu + * @create 2023-12-11 + */ +@Service +public class ApplyAttendanceChangeServiceImpl extends SuperServiceImpl implements ApplyAttendanceChangeService { + @Resource + private AttendanceClockInResultService attendanceClockInResultService; + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + @Resource + private FlowTaskService flowTaskService; + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private UserApi userApi; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private RedisUtil redisUtil; + @Resource + private CustomTenantUtil customTenantUtil; + @Resource + private ChangeConfig changeConfig; + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + + @Override + public ApplyAttendanceChangeVo getApplyAttendanceChange(String id) { + + ApplyAttendanceChange applyAttendanceChange = this.getById(id); + return JsonUtil.getJsonToBean(applyAttendanceChange, ApplyAttendanceChangeVo.class); + } + + @Override + public void save(ApplyAttendanceChange entity, ApplyAttendanceChangeDto applyAttendanceChangeDto) throws Exception { + + int count = attendanceClockInResultMapper.getReplyingCount(applyAttendanceChangeDto.getClockInResultId()); + if (count > 0) { + throw new QueryException("当前记录正在审批中,请审批结束后再重新执行操作"); + } + //查询班次信息 + AttendanceClockInResult clockInResult = attendanceClockInResultService.lambdaQuery() + .eq(AttendanceClockInResult::getId, applyAttendanceChangeDto.getClockInResultId()) + .eq(AttendanceClockInResult::getDeleteMark,0) + .last("limit 1").one(); + if (null == clockInResult) { + throw new QueryException("当天班次已变更,请重新发起!"); + } + // 查询出勤规则 + FtbAttendanceDailyRule dailyRule = attendanceDailyRuleService.getById(clockInResult.getRuleId()); + if (null == dailyRule) { + throw new QueryException("当天班次已变更,请重新发起!"); + } + // 判定当前变更是否合法 + if (!applyAttendanceChangeDto.getChangeType().equals(ClockInStatusEnum.ROLLBACK.getValue())) { + List clockInStatusEnums = changeConfig.getChangeMap().get(clockInResult.getClockInType()).get(ClockInStatusEnum.getClockInStatusEnum(clockInResult.getClockInStatus())); + if (null == clockInStatusEnums || !clockInStatusEnums.contains(ClockInStatusEnum.getClockInStatusEnum(applyAttendanceChangeDto.getChangeType()))) { + throw new Exception("无法变更为指定的状态"); + } + } + // 查询变更用户信息 + BaseUserInfoVo user = userApi.getUserInfoById(clockInResult.getUserId()); + entity.setChangeUserId(user.getUserId()); + entity.setChangeUserName(user.getUserName()); + entity.setApplyDate(new Date()); + // 变更结果为单个, 变更上班则将下班时间置空, 变更下班置空上班 + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + entity.setOnWorkTime(dailyRule.getInPoint()); + entity.setOffWorkTime(null); + } else { + entity.setOnWorkTime(null); + entity.setOffWorkTime(dailyRule.getOutPoint()); + } + entity.setId(applyAttendanceChangeDto.getTaskId()); + save(entity); + updateClockInResult(entity.getId(), entity.getClockInResultId(), entity.getChangeType()); + //sendToIm(clockInResult.getClockInType(),entity, userProvider.get().getTenantId()); + } + + private void updateClockInResult(String id, String clockInResultId, Integer changeType) { + + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .set(AttendanceClockInResult::getApplyType, ConstantUtil.APPLY_CHANGE) + .set(AttendanceClockInResult::getApplyId, id) + // .set(AttendanceClockInResult::getAbsenceLeader, userProvider.get().getUserId()) + .eq(AttendanceClockInResult::getDeleteMark, ConstantUtil.NUM_FALSE) + .eq(AttendanceClockInResult::getId, clockInResult.getId()); + attendanceClockInResultService.update(updateWrapper); + } + + @Override + public void submit(String id, ApplyAttendanceChange entity, ApplyAttendanceChangeDto applyAttendanceChangeDto) { + //查询班次信息 + AttendanceClockInResult clockInResult = attendanceClockInResultService.lambdaQuery() + .eq(AttendanceClockInResult::getId, applyAttendanceChangeDto.getClockInResultId()) + .eq(AttendanceClockInResult::getDeleteMark,0) + .last("limit 1").one(); + if (entity.getChangeType().equals(3) || entity.getChangeType().equals(4)) { + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + entity.setOffWorkTime(null); + } else { + entity.setOnWorkTime(null); + } + } + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + //sendToIm(clockInResult.getClockInType(), entity, userProvider.get().getTenantId()); + } else { + entity.setId(id); + updateById(entity); + } + updateClockInResult(entity.getId(), entity.getClockInResultId(), entity.getChangeType()); + } + + private void sendToIm(Integer clockInType, ApplyAttendanceChange entity, String tenantId) { + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(() -> { + customTenantUtil.checkOutTenant(tenantId); + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId, null); + if (null == data) { + return; + } + AttendanceApproveImVo approveIm = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + approveIm.setId(data.getTaskId()); + List userList = userAntifreeze.getAllByIds(List.of(entity.getCreatorUserId()), tenantId); + PartUserInfoVo userInfo = userList.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(tenantId, AttendanceNoticeEnum.APPROVE); + String content = "%s的【出勤变更】审批"; + String title = String.format(content, null != userInfo ? userInfo.getRealName() : "--"); + ApproveBaseImVo imDetail = new ApproveBaseImVo(title, ApprovalSettingTypeEnum.ACTION_RESULT.getCode(), entity.getChangeUserName(), + getRuleTime(clockInType,entity), getResult(entity.getClockInResultId()), getChangeResult(entity.getChangeType())); + data.getUserOperators().forEach(v -> { + approveIm.setTaskId(v.getTaskOperatorId()); + approveImVo.setUserIds(List.of(v.getUserId())); + String encode = FtbUtil.encodeJson(approveIm); + String url = AttendanceConstant.APPROVE_URL + encode; + imDetail.setUrl(url); + approveImVo.setApproveBaseImVo(imDetail); + attendanceNoticeHandler.send(approveImVo); + redisUtil.insert(ConstantUtil.APPROVE_NOTICE_CHANGE + approveIm.getId(), JSONUtil.toJsonStr(approveImVo)); + }); + }, 1, TimeUnit.SECONDS); + scheduler.shutdown(); + } + + private String getChangeResult(Integer changeType) { + + switch (changeType) { + case 1: + return "旷工"; + case 2: + return "撤销旷工"; + case 3: + return "正常"; + case 4: + return "补卡"; + default: + return "--"; + } + } + + private String getRuleTime(Integer clockInType, ApplyAttendanceChange entity) { + switch (entity.getChangeType()) { + case 1: + case 2: + return DateDetail.getDate2Str(entity.getOnWorkTime(), DateDetail.DF9) + "-" + DateDetail.getDate2Str(entity.getOffWorkTime(), DateDetail.DF10); + case 3: + case 4: + return clockInType.equals(ConstantUtil.ON_WORK)? DateDetail.getDate2Str(entity.getOnWorkTime(), DateDetail.DF9):DateDetail.getDate2Str(entity.getOffWorkTime(), DateDetail.DF9); + default: + return "变更类型不正确"; + } + } + + private String getResult(String resultId) { + + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(resultId); + if (null != clockInResult.getAbsence() && clockInResult.getAbsence().equals(ConstantUtil.NUM_TRUE)) { + return ClockInStatusEnum.ABSENCE.getDescription(); + } else { + switch (clockInResult.getClockInStatus()) { + case -1: + return ClockInStatusEnum.NO_CLOCK.getDescription(); + case 1: + return ClockInStatusEnum.NORMAL.getDescription(); + case 2: + return ClockInStatusEnum.WORK_LATE.getDescription(); + case 3: + return ClockInStatusEnum.HOME_EARLY.getDescription(); + default: + return "--"; + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceOutsideServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceOutsideServiceImpl.java new file mode 100644 index 0000000..d8da4ad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceOutsideServiceImpl.java @@ -0,0 +1,144 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceClockInPicService; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.attendance.AttendanceClockInPic; +import jnpf.entity.workflow.ApplyAttendanceOutside; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.ApplyAttendanceOutsideDto; +import jnpf.model.workflow.vo.ApplyAttendanceOutsideVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.ApplyAttendanceOutsideMapper; +import jnpf.workflow.service.ApplyAttendanceOutsideService; +import jnpf.workflow.service.ApplyPicService; +import jnpf.workflow.service.FlowTaskService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 外勤审批服务实现 + * + * @author yanwenfu + * @create 2023-12-13 + */ +@Slf4j +@Service +public class ApplyAttendanceOutsideServiceImpl extends SuperServiceImpl implements ApplyAttendanceOutsideService { + + @Resource + private FlowTaskService flowTaskService; + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private UserProvider userProvider; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private RedisUtil redisUtil; + @Resource + private CustomTenantUtil customTenantUtil; + @Resource + private AttendanceClockInService attendanceClockInService; + @Resource + private ApplyPicService applyPicService; + + @Override + public ApplyAttendanceOutsideVo getApplyAttendanceOutside(String id) { + + ApplyAttendanceOutside applyAttendanceOutside = this.getById(id); + return JsonUtil.getJsonToBean(applyAttendanceOutside, ApplyAttendanceOutsideVo.class); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void save(ApplyAttendanceOutside entity, ApplyAttendanceOutsideDto applyAttendanceOutsideDto) throws Exception { + log.error("发起外勤审批: {}", applyAttendanceOutsideDto.toString()); + //保存外勤打卡图片 picUrlList + List urlList = applyPicService.getPicList(applyAttendanceOutsideDto.getPicUrlList()); + if (!urlList.isEmpty()) { + applyAttendanceOutsideDto.getPicUrlList().clear(); + applyAttendanceOutsideDto.getPicUrlList().addAll(urlList); + } + applyPicService.saveClockPic(applyAttendanceOutsideDto.getTaskId(), applyAttendanceOutsideDto.getPicUrlList()); + UserInfo userInfo = userProvider.get(); + entity.setApplyUser(userInfo.getUserId()); + entity.setApplyDate(DateDetail.getStr2DateTime(applyAttendanceOutsideDto.getApplyDateStr())); + entity.setTenantId(userInfo.getTenantId()); + entity.setId(applyAttendanceOutsideDto.getTaskId()); + entity.setRemark(applyAttendanceOutsideDto.getRemark()); + save(entity); + attendanceClockInService.outsideClockIn(applyAttendanceOutsideDto.getTaskId(), userInfo.getUserId(), applyAttendanceOutsideDto.getClockInId()); + } + + private void sendToIm(ApplyAttendanceOutside entity, String tenantId) { + + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(() -> { + customTenantUtil.checkOutTenant(tenantId); + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId, null); + if (null == data) { + return; + } + AttendanceApproveImVo approveIm = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + approveIm.setId(data.getTaskId()); + List userList = userAntifreeze.getAllByIds(List.of(entity.getApplyUserId()), tenantId); + PartUserInfoVo userInfo = userList.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(tenantId, AttendanceNoticeEnum.APPROVE); + String content = "%s的【外勤】审批"; + String title = String.format(content, null != userInfo ? userInfo.getRealName() : "--"); + ApproveBaseImVo imDetail = new ApproveBaseImVo(title, ApprovalSettingTypeEnum.OUT.getCode(), entity.getCreatorTime(), entity.getAddress()); + data.getUserOperators().forEach(v -> { + approveIm.setTaskId(v.getTaskOperatorId()); + approveImVo.setUserIds(List.of(v.getUserId())); + String encode = FtbUtil.encodeJson(approveIm); + String url = AttendanceConstant.APPROVE_URL + encode; + imDetail.setUrl(url); + approveImVo.setApproveBaseImVo(imDetail); + attendanceNoticeHandler.send(approveImVo); + redisUtil.insert(ConstantUtil.APPROVE_NOTICE_OUTSIDE + approveIm.getId(), JSONUtil.toJsonStr(approveImVo)); + }); + }, 1, TimeUnit.SECONDS); + scheduler.shutdown(); + } + + @Override + public void submit(ApplyAttendanceOutside entity, ApplyAttendanceOutsideDto applyAttendanceOutsideDto) { + //保存外勤打卡图片 + applyPicService.saveClockPic(applyAttendanceOutsideDto.getTaskId(), applyAttendanceOutsideDto.getPicUrlList()); + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(applyAttendanceOutsideDto.getTaskId()); + save(entity); + //sendToIm(entity, userProvider.get().getTenantId()); + } else { + entity.setId(applyAttendanceOutsideDto.getTaskId()); + updateById(entity); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceRepairServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceRepairServiceImpl.java new file mode 100644 index 0000000..21764fe --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceRepairServiceImpl.java @@ -0,0 +1,232 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.mapper.AttendanceClockInResultMapper; +import jnpf.attendance.mapper.AttendanceDailyRuleMapper; +import jnpf.attendance.service.AttendanceClockInResultService; +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.attendance.service.AttendanceDayStatisticsService; +import jnpf.attendance.service.AttendanceRepairService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.attendance.AttendanceClockInResult; +import jnpf.entity.attendance.AttendanceRepair; +import jnpf.entity.attendance.FtbAttendanceDailyRule; +import jnpf.entity.workflow.ApplyAttendanceRepair; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.ClockInStatusEnum; +import jnpf.exception.HandleException; +import jnpf.exception.QueryException; +import jnpf.model.attendance.vo.AttendanceRuleVo; +import jnpf.model.attendance.vo.GroupRuleVo; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.ApplyAttendanceRepairDto; +import jnpf.model.workflow.vo.ApplyAttendanceRepairVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.ApplyAttendanceRepairMapper; +import jnpf.workflow.service.ApplyAttendanceRepairService; +import jnpf.workflow.service.FlowTaskService; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 补卡审批服务实现 + * + * @author yanwenfu + * @create 2023-12-13 + */ +@Service +public class ApplyAttendanceRepairServiceImpl extends SuperServiceImpl implements ApplyAttendanceRepairService { + + @Resource + private AttendanceClockInResultService attendanceClockInResultService; + @Resource + private AttendanceClockInResultMapper attendanceClockInResultMapper; + @Resource + private AttendanceClockInService attendanceClockInService; + @Resource + private AttendanceDayStatisticsService attendanceDayStatisticsService; + @Resource + private AttendanceDailyRuleMapper attendanceDailyRuleMapper; + @Resource + private AttendanceRepairService attendanceRepairService; + @Resource + private FlowTaskService flowTaskService; + @Autowired + private UserAntifreeze userAntifreeze; + @Autowired + private UserProvider userProvider; + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Autowired + private RedisUtil redisUtil; + @Resource + private CustomTenantUtil customTenantUtil; + + @Override + public ApplyAttendanceRepairVo getApplyAttendanceRepair(String id) { + + ApplyAttendanceRepair applyAttendanceRepair = this.getById(id); + return JsonUtil.getJsonToBean(applyAttendanceRepair, ApplyAttendanceRepairVo.class); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public void save(ApplyAttendanceRepair entity, ApplyAttendanceRepairDto applyAttendanceRepairDto) throws Exception { + + UserInfo userInfo = userProvider.get(); + // 提交前校验 + submitCheckRepair(entity.getClockInResultId()); + entity.setId(applyAttendanceRepairDto.getTaskId()); + entity.setApplyUser(userInfo.getUserId()); + entity.setApplyDate(new Date()); + entity.setTenantId(userInfo.getTenantId()); + setExtraValue(entity); + save(entity); + //sendToIm(entity, userInfo.getTenantId()); + // 表单信息 + /*if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + sendToIm(entity, userProvider.get().getTenantId()); + } else { + entity.setId(id); + updateById(entity); + }*/ + } + + private void submitCheckRepair(String clockInResultId) throws Exception { + + // 判断当前出勤结果是否在进行别的审批 + int count = attendanceClockInResultMapper.getReplyingCount(clockInResultId); + if (count > 0) { + throw new QueryException("当前记录正在审批中,请审批结束后再重新执行操作"); + } + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(clockInResultId); + // 查询考勤规则 + AttendanceRuleVo rule = attendanceClockInService.getAttendanceRule(clockInResult.getRuleId()); + // 判定是否封账 + String monthDate = DateDetail.getDate2Str(rule.getDay(), DateDetail.DF15); + Map map = attendanceDayStatisticsService.selectUserIsSeal(List.of(clockInResult.getUserId()), monthDate); + if (map.get(clockInResult.getUserId())) { + throw new HandleException(monthDate + "已封账,过去的考勤记录无法修改!"); + } + // 用户补卡次数判定 (已使用次数 + 审批中的补卡) + AttendanceRepair attendanceRepair = attendanceRepairService.getAttendanceRepair(rule.getUserId(), rule.getGroupId()); + String beginDate = String.format("%d-%02d-%02d", attendanceRepair.getGenerateYear(), attendanceRepair.getGenerateMonth(), attendanceRepair.getGenerateDay()); + String endDate = DateDetail.getDate2Str(attendanceRepair.getExpireDate(), DateDetail.DF); + int applyCount = attendanceClockInResultMapper.selectRepairApplyCount(rule.getUserId(), rule.getGroupId(), beginDate, endDate); + if (attendanceRepair.getRepairNum() - applyCount <= 0) { + throw new QueryException("当前暂无补卡次数"); + } + } + + private void setExtraValue(ApplyAttendanceRepair entity) { + // 根据打卡结果id查询打卡结果 + AttendanceClockInResult clockInResult = attendanceClockInResultService.getById(entity.getClockInResultId()); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .eq(AttendanceClockInResult::getId, clockInResult.getId()) + .set(AttendanceClockInResult::getApplyType, ConstantUtil.APPLY_REPAIR) + .set(AttendanceClockInResult::getApplyId, entity.getId()); + // .set(AttendanceClockInResult::getAbsenceLeader, userProvider.get().getUserId()); + attendanceClockInResultService.update(updateWrapper); + // 查询出勤规则 + FtbAttendanceDailyRule dailyRule = attendanceDailyRuleMapper.selectById(clockInResult.getRuleId()); + Date date; + if (clockInResult.getClockInType().equals(ConstantUtil.ON_WORK)) { + date = dailyRule.getInPoint(); + } else { + date = dailyRule.getOutPoint(); + } + entity.setRepairDateStr(DateDetail.getDate2Str(date, DateDetail.DF)); + entity.setRepairTimeStr(DateDetail.getDate2Str(date, DateDetail.DF10)); + entity.setClockInStatus(clockInResult.getClockInStatus()); + entity.setAbsence(clockInResult.getAbsence()); + } + + @Override + public void submit(String id, ApplyAttendanceRepair entity, ApplyAttendanceRepairDto applyAttendanceRepairDto) { + + setExtraValue(entity); + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + //sendToIm(entity, userProvider.get().getTenantId()); + } else { + entity.setId(id); + updateById(entity); + } + } + + private void sendToIm(ApplyAttendanceRepair entity, String tenantId) { + + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduler.schedule(() -> { + customTenantUtil.checkOutTenant(tenantId); + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId,null); + if (null == data) { + return; + } + AttendanceApproveImVo approveIm = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + approveIm.setId(data.getTaskId()); + List userList = userAntifreeze.getAllByIds(List.of(entity.getCreatorUserId()), tenantId); + PartUserInfoVo userInfo = userList.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(tenantId, AttendanceNoticeEnum.APPROVE); + String content = "%s的【补卡】审批"; + String title = String.format(content, null != userInfo ? userInfo.getRealName() : "--"); + ApproveBaseImVo imDetail = new ApproveBaseImVo(title, ApprovalSettingTypeEnum.ROUTINE.getCode(), entity.getRepairDateStr() + " " + entity.getRepairTimeStr(), getResult(entity.getClockInStatus(), entity.getAbsence())); + data.getUserOperators().forEach(v -> { + approveIm.setTaskId(v.getTaskOperatorId()); + approveImVo.setUserIds(List.of(v.getUserId())); + String encode = FtbUtil.encodeJson(approveIm); + String url = AttendanceConstant.APPROVE_URL + encode; + imDetail.setUrl(url); + approveImVo.setApproveBaseImVo(imDetail); + String key = ConstantUtil.APPROVE_NOTICE_REPAIR + approveIm.getId(); + redisUtil.insert(key, JSONUtil.toJsonStr(approveImVo)); + attendanceNoticeHandler.send(approveImVo); + }); + }, 1, TimeUnit.SECONDS); + scheduler.shutdown(); + } + + private String getResult(Integer clockInStatus, Integer absence) { + + if (null != absence && absence.equals(ConstantUtil.NUM_TRUE)) { + return ClockInStatusEnum.ABSENCE.getDescription(); + } else { + switch (clockInStatus) { + case -1: + return ClockInStatusEnum.NO_CLOCK.getDescription(); + case 2: + return ClockInStatusEnum.WORK_LATE.getDescription(); + case 3: + return ClockInStatusEnum.HOME_EARLY.getDescription(); + default: + return "--"; + } + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceViolationServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceViolationServiceImpl.java new file mode 100644 index 0000000..72bc9c6 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyAttendanceViolationServiceImpl.java @@ -0,0 +1,71 @@ +package jnpf.workflow.service.impl; + +import jnpf.attendance.service.AttendanceClockInService; +import jnpf.base.UserInfo; +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.workflow.ApplyAttendanceViolation; +import jnpf.enums.attendance.ApplyTypeEnum; +import jnpf.model.attendance.dto.ClockInDto; +import jnpf.model.workflow.dto.ApplyAttendanceViolationDto; +import jnpf.util.ConstantUtil; +import jnpf.util.DateDetail; +import jnpf.util.JsonUtil; +import jnpf.util.UserProvider; +import jnpf.workflow.mapper.ApplyAttendanceViolationMapper; +import jnpf.workflow.service.ApplyAttendanceViolationService; +import jnpf.workflow.service.ApplyPicService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 违规打卡审批服务实现 + * + * @author yanwenfu + * @create 2025-09-22 + */ +@Slf4j +@Service +public class ApplyAttendanceViolationServiceImpl extends SuperServiceImpl implements ApplyAttendanceViolationService { + + @Resource + private ApplyPicService applyPicService; + @Resource + private AttendanceClockInService attendanceClockInService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveRecord(ApplyAttendanceViolationDto dto) throws Exception { + + ApplyAttendanceViolation entity = JsonUtil.getJsonToBean(dto, ApplyAttendanceViolation.class); + entity.setStatus(ConstantUtil.STATUS_PENDING_APPROVAL); + log.error("发起异常打卡审批: 任务id: {}, 打卡类型: {}", dto.getTaskId(), dto.getClockInKind()); + //保存外勤打卡图片 picUrlList + List urlList = applyPicService.getPicList(dto.getPicUrlList()); + if (!urlList.isEmpty()) { + dto.getPicUrlList().clear(); + dto.getPicUrlList().addAll(urlList); + } + applyPicService.saveClockPic(dto.getTaskId(), dto.getPicUrlList()); + UserInfo userInfo = UserProvider.getUser(); + entity.setApplyUser(userInfo.getUserId()); + entity.setApplyDate(DateDetail.getStr2DateTime(dto.getApplyDateStr())); + entity.setTenantId(userInfo.getTenantId()); + entity.setId(dto.getTaskId()); + entity.setRemark(dto.getRemark()); + ClockInDto clockInDto = new ClockInDto(dto.getDeviceType(), dto.getClockInKind(), dto.getTaskId(), ApplyTypeEnum.VIOLATION, dto.getAddress(), dto.getLng(), dto.getLat(), dto.getRemark()); + MutablePair pair; + if (StringUtils.isEmpty(dto.getClockInId())) { + pair = attendanceClockInService.clockIn(clockInDto); + } else { + pair = attendanceClockInService.updateClockIn(dto.getClockInId(), clockInDto); + } + entity.setClockInResultId(pair.getRight()); + save(entity); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyPicServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyPicServiceImpl.java new file mode 100644 index 0000000..9409c6f --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/ApplyPicServiceImpl.java @@ -0,0 +1,68 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jnpf.attendance.service.AttendanceClockInPicService; +import jnpf.entity.attendance.AttendanceClockInPic; +import jnpf.util.FtbUtil; +import jnpf.workflow.service.ApplyPicService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 审批图片服务实现 + * + * @author yanwenfu + * @create 2025-09-22 + */ +@Service +public class ApplyPicServiceImpl implements ApplyPicService { + + @Resource + private AttendanceClockInPicService attendanceClockInPicService; + + @Override + public List getPicList(List picUrlList) { + + if (null != picUrlList && !picUrlList.isEmpty()) { + String str = picUrlList.get(0); + JSONArray array = new JSONArray(str); + List list = new ArrayList<>(); + array.forEach(v -> { + JSONObject json = new JSONObject(v); + Object o = json.get("previewUrl"); + if (null != o) { + list.add(o.toString()); + } + json.clear(); + }); + array.clear(); + return list; + } + return List.of(); + } + + @Override + public void saveClockPic(String taskId, List urlList) { + + if (CollUtil.isNotEmpty(urlList)) { + List picUrlList = CollUtil.newArrayList(); + picUrlList.addAll(urlList.stream().map(item -> { + AttendanceClockInPic clickInPic = AttendanceClockInPic.builder() + .approvalCode(taskId) + .picUrl(item) + .build(); + clickInPic.setId(FtbUtil.getId()); + return clickInPic; + }).collect(Collectors.toList())); + attendanceClockInPicService.remove(new LambdaQueryWrapper().eq(AttendanceClockInPic::getApprovalCode, taskId)); + attendanceClockInPicService.saveBatch(picUrlList); + } + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/BusinessTripApproveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/BusinessTripApproveServiceImpl.java new file mode 100644 index 0000000..c20d539 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/BusinessTripApproveServiceImpl.java @@ -0,0 +1,178 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.workflow.AttendanceBusinessTripApprove; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveDto; +import jnpf.model.workflow.dto.AttendanceBusinessTripApproveOaDto; +import jnpf.model.workflow.vo.AttendanceBusinessTripApproveVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.BusinessTripApproveMapper; +import jnpf.workflow.service.BusinessTripApproveService; +import jnpf.workflow.service.FlowTaskService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @Author huanglinpan + * @Date 2024/5/14 18:33 + * @Version 1.0 (版本号) + */ +@Service +@Slf4j +public class BusinessTripApproveServiceImpl extends SuperServiceImpl implements BusinessTripApproveService { + + + @Autowired + private UserAntifreeze userAntifreeze; + + @Resource + private FlowTaskService flowTaskService; + + @Autowired + private UserProvider userProvider; + + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Resource + private CustomTenantUtil customTenantUtil; + + + @Override + public void save(String id, AttendanceBusinessTripApprove entity, AttendanceBusinessTripApproveDto dto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + String tenantId = userProvider.get().getTenantId(); + new Thread(()->{ + // 睡眠1秒,确保事务提交完成 + try { + Thread.sleep(1000); + businessSendIm(entity,tenantId); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + + } else { + entity.setId(id); + updateById(entity); + } + } + + private void businessSendIm(AttendanceBusinessTripApprove entity,String tenantId) { + // 张三的【出差】审批 + // 出发地:地点名称地点名称地点名称地点名称 + // 目的地:地点名称地点名称地点名称地点名称 + customTenantUtil.checkOutTenant(tenantId); + // 查询第一节点审批人 + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId,null); + if (null != data) { + AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + jsonToBean.setId(data.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(entity.getUserId()), tenantId); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(tenantId); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.BUSINESS_TRIP.getCode()); + approveBaseImVo.setDestination(entity.getDestination()); + approveBaseImVo.setDeparture(entity.getDeparture()); + approveBaseImVo.setTitle(stringBuffer.append("的【出差】审批").toString()); + data.getUserOperators().forEach(v->{ + // 每个人的这个Id不同,所以轮询触发 + jsonToBean.setTaskId(v.getTaskOperatorId()); + // URL 拼接 +// String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean)); + String encode = FtbUtil.encodeJson(jsonToBean); + String url = AttendanceConstant.APPROVE_URL + encode; + approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId())); + approveBaseImVo.setUrl(url); + approveImVo.setApproveBaseImVo(approveBaseImVo); + attendanceNoticeHandler.send(approveImVo); + }); + } + } + + @Override + public void submit(String id, AttendanceBusinessTripApprove entity, AttendanceBusinessTripApproveDto dto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + String tenantId = userProvider.get().getTenantId(); + new Thread(()->{ + // 睡眠1秒,确保事务提交完成 + try { + Thread.sleep(1000); + businessSendIm(entity,tenantId); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + } else { + entity.setId(id); + updateById(entity); + } + } + + @Override + public AttendanceBusinessTripApproveVo getOne(String id) { + AttendanceBusinessTripApprove attendanceLeaveApprove = this.getById(id); + return JsonUtil.getJsonToBean(attendanceLeaveApprove, AttendanceBusinessTripApproveVo.class); + } + + @Override + public List getBatchByIds(List groupIds, List ids) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(AttendanceBusinessTripApprove::getId, ids); + queryWrapper.in(CollUtil.isNotEmpty(groupIds), AttendanceBusinessTripApprove::getGroupId, groupIds); + List list = this.list(queryWrapper); + return CollUtil.isNotEmpty(list) ? list: new ArrayList<>(); + } + + @Override + public void saveForOa(AttendanceBusinessTripApproveOaDto approveOaDto) { + AttendanceBusinessTripApprove entity = JsonUtil.getJsonToBean(approveOaDto, AttendanceBusinessTripApprove.class); + // 计算两个时间戳之间的日期差 + + log.error("FoeOa开始存数据库出差了参数................: {}", entity); + save(entity); + } + + @Override + public void submitForOa(String id, AttendanceBusinessTripApproveOaDto approveOaDto) { + AttendanceBusinessTripApprove entity = JsonUtil.getJsonToBean(approveOaDto, AttendanceBusinessTripApprove.class); + log.error("FoeOa开始x修改数据库出差了参数................: {}", entity); + entity.setId(id); + updateById(entity); + } + + +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/FlowTaskServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/FlowTaskServiceImpl.java new file mode 100644 index 0000000..db3dee3 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/FlowTaskServiceImpl.java @@ -0,0 +1,52 @@ +package jnpf.workflow.service.impl; + +import jnpf.engine.FlowTaskApi; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.workflow.service.FlowTaskService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 工作流服务实现 + * + * @author yanwenfu + * @create 2024-08-19 + */ +@Service +public class FlowTaskServiceImpl implements FlowTaskService { + + @Resource + private FlowTaskApi flowTaskApi; + + @Override + public ApproverByTaskIdAndNodeIdVo getApproveInfo(String taskId, String nodeId, String tenantId, Integer type) { + + ApproverByTaskIdAndNodeIdVo vo = null; + int index = 1; + while (index <= 5) { + try { + index++; + vo = flowTaskApi.getApproverByTaskIdAndNodeId(taskId, nodeId, tenantId).getData(); + if (null != type && type == 0 ){ + if (null != vo){ + break; + }else { + vo = null; + Thread.sleep((long) index * 1000); + } + }else { + if (null == vo || null == vo.getUserOperators() || vo.getUserOperators().isEmpty()) { + vo = null; + Thread.sleep((long) index * 1000); + } else { + break; + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return vo; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/GoOutApproveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/GoOutApproveServiceImpl.java new file mode 100644 index 0000000..3c7e8f0 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/GoOutApproveServiceImpl.java @@ -0,0 +1,167 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.dto.GoOutApproveForOaDto; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.AttendanceGoOutApproveDto; +import jnpf.model.workflow.vo.AttendanceGoOutApproveVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.GoOutApproveMapper; +import jnpf.workflow.service.FlowTaskService; +import jnpf.workflow.service.GoOutApproveService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @Author huanglinpan + * @Date 2024/5/14 18:32 + * @Version 1.0 (版本号) + */ +@Service +@Slf4j +public class GoOutApproveServiceImpl extends SuperServiceImpl implements GoOutApproveService { + + @Autowired + private UserAntifreeze userAntifreeze; + + @Resource + private FlowTaskService flowTaskService; + + @Autowired + private UserProvider userProvider; + + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Resource + private CustomTenantUtil customTenantUtil; + + @Override + public void save(String id, AttendanceGoOutApprove entity, AttendanceGoOutApproveDto dto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + String tenantId = userProvider.get().getTenantId(); + new Thread(() -> { + // 睡眠1秒,确保事务提交完成 + try { + Thread.sleep(1000); + goOutSendIm(entity, tenantId); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + } else { + entity.setId(id); + updateById(entity); + } + } + + private void goOutSendIm(AttendanceGoOutApprove entity, String tenantId) { + //张三的【外出】审批 + //开始时间:2024年5月16日 + //结束时间:2024年5月17日 + //时长:2天 + customTenantUtil.checkOutTenant(tenantId); + // 查询第一节点审批人 + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId, null); + if (null != data) { + AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + jsonToBean.setId(data.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(entity.getUserId()), tenantId); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(tenantId); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.GO_OUT.getCode()); + approveBaseImVo.setStartTime(entity.getStartTime()); + approveBaseImVo.setEndTime(entity.getEndTime()); + approveBaseImVo.setDuration(entity.getDayNum() + "天"); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【外出】审批").toString()); + data.getUserOperators().forEach(v -> { + // 每个人的这个Id不同,所以轮询触发 + jsonToBean.setTaskId(v.getTaskOperatorId()); + // URL 拼接 +// String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean)); + String encode = FtbUtil.encodeJson(jsonToBean); + String url = AttendanceConstant.APPROVE_URL + encode; + approveBaseImVo.setUrl(url); + approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId())); + approveImVo.setApproveBaseImVo(approveBaseImVo); + attendanceNoticeHandler.send(approveImVo); + }); + } + } + + @Override + public void submit(String id, AttendanceGoOutApprove entity, AttendanceGoOutApproveDto dto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + String tenantId = userProvider.get().getTenantId(); + new Thread(() -> { + // 睡眠1秒,确保事务提交完成 + try { + Thread.sleep(1000); + goOutSendIm(entity, tenantId); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }).start(); + } else { + entity.setId(id); + updateById(entity); + } + } + + @Override + public AttendanceGoOutApproveVo getOne(String id) { + AttendanceGoOutApprove attendanceLeaveApprove = this.getById(id); + return JsonUtil.getJsonToBean(attendanceLeaveApprove, AttendanceGoOutApproveVo.class); + } + + @Override + public List getBatchByIds(List groupIds, List ids) { + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + queryWrapper.in(AttendanceGoOutApprove::getId, ids); + queryWrapper.in(CollUtil.isNotEmpty(groupIds), AttendanceGoOutApprove::getGroupId, groupIds); + List list = this.list(queryWrapper); + return CollUtil.isNotEmpty(list) ? list : new ArrayList<>(); + } + + @Override + public void saveForOa(GoOutApproveForOaDto dto) { + AttendanceGoOutApprove entity = JsonUtil.getJsonToBean(dto, AttendanceGoOutApprove.class); + entity.setGroupId(dto.getGroupId()); + log.error("FoeOa开始存数据库出差了参数................: {}", entity); + save(entity); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/LeaveApproveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/LeaveApproveServiceImpl.java new file mode 100644 index 0000000..dd146ea --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/LeaveApproveServiceImpl.java @@ -0,0 +1,196 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.attendance.ApplyParam; +import jnpf.entity.workflow.AttendanceGoOutApprove; +import jnpf.entity.workflow.AttendanceLeaveApprove; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.enums.attendance.AttendanceTypeEnum; +import jnpf.exception.HandleException; +import jnpf.model.attendance.dto.LeaveApproveForOaDto; +import jnpf.model.attendance.vo.DailyRuleResultVo; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.AttendanceLeaveApproveDto; +import jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.AttendanceLeaveMapper; +import jnpf.workflow.service.FlowTaskService; +import jnpf.workflow.service.LeaveApproveService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/12/11 + */ +@Service +@Slf4j +public class LeaveApproveServiceImpl extends SuperServiceImpl implements LeaveApproveService { + @Autowired + private UserAntifreeze userAntifreeze; + + @Resource + private FlowTaskService flowTaskService; + + @Autowired + private UserProvider userProvider; + + @Resource + private AttendanceDailyRuleService attendanceDailyRuleService; + + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Resource + private CustomTenantUtil customTenantUtil; + + @Override + @Transactional(rollbackFor = Exception.class) + public void save(String id, AttendanceLeaveApprove entity, AttendanceLeaveApproveDto attendanceLeaveApproveDto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + try { + // 发送审批消息 + String tenantId = userProvider.get().getTenantId(); + new Thread(() -> { + // 睡眠1秒,确保事务提交完成 + try { + Thread.sleep(1000); + leaveSendIm(entity, tenantId); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } catch (Exception e) { + log.error("发送请假审批消息通知第一级审批人失败", e); + throw new RuntimeException(e); + } + } else { + entity.setId(id); + updateById(entity); + } + } + + private void leaveSendIm(AttendanceLeaveApprove entity, String tenantId) throws HandleException { + customTenantUtil.checkOutTenant(tenantId); + // 查询第一节点审批人 + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId, null); + if (null != data) { + AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + jsonToBean.setId(data.getTaskId()); + List dailyRuleResultVos = attendanceDailyRuleService.applyVerifyHandle(new ApplyParam(entity.getUserId(), entity.getStartTime(), entity.getEndTime(), AttendanceTypeEnum.LEAVE)); + // 实时获取申请时长 + BigDecimal hours = new BigDecimal("0"); + for (DailyRuleResultVo dailyRuleResultVo : dailyRuleResultVos) { + hours = hours.add(dailyRuleResultVo.getDuration()); + } + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(entity.getUserId()), tenantId); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(tenantId); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.LEAVE.getCode()); + approveBaseImVo.setStartTime(entity.getStartTime()); + approveBaseImVo.setEndTime(entity.getEndTime()); + approveBaseImVo.setStartTimeType(entity.getStartTimeType()); + approveBaseImVo.setEndTimeType(entity.getEndTimeType()); + approveBaseImVo.setUnit(entity.getUnit()); + // 请假类型 + approveBaseImVo.setLeaveType(entity.getType()); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【请假】审批").toString()); + data.getUserOperators().forEach(v -> { + approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId())); + jsonToBean.setTaskId(v.getTaskOperatorId()); + // URL 拼接 +// String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean)); + String encode = FtbUtil.encodeJson(jsonToBean); + String url = AttendanceConstant.APPROVE_URL + encode; + approveBaseImVo.setUrl(url); + approveImVo.setApproveBaseImVo(approveBaseImVo); + attendanceNoticeHandler.send(approveImVo); + }); + } + } + + @Override + public void submit(String id, AttendanceLeaveApprove entity, AttendanceLeaveApproveDto attendanceLeaveApproveDto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + try { + // 发送审批消息 + String tenantId = userProvider.get().getTenantId(); + new Thread(() -> { + // 睡眠1秒,确保事务提交完成 + try { + Thread.sleep(1000); + leaveSendIm(entity, tenantId); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } catch (Exception e) { + log.error("发送请假审批消息通知第一级审批人失败", e); + throw new RuntimeException(e); + } + } else { + entity.setId(id); + updateById(entity); + } + } + + @Override + public AttendanceLeaveFlowApproveVo getAttendanceLeave(String id) { + AttendanceLeaveApprove attendanceLeaveApprove = this.getById(id); + AttendanceLeaveFlowApproveVo jsonToBean = JsonUtil.getJsonToBean(attendanceLeaveApprove, AttendanceLeaveFlowApproveVo.class); + // 当审核未通过时动态取值 +// if (1 != jsonToBean.getStatus()){ + // 动态获取涉及班次/借调班次 + + // 实际使用余额/未抵扣余额/剩余余额(兑换券/抵扣劵) + + +// } + return jsonToBean; + } + + @Override + public void saveForOa(LeaveApproveForOaDto dto) { + AttendanceLeaveApprove entity = JsonUtil.getJsonToBean(dto, AttendanceLeaveApprove.class); + entity.setUserId(JSONUtil.toList(dto.getApplyUser(), String.class).get(0)); + log.error("FoeOa开始存数据库请假参数................: {}", entity); + save(entity); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/PunishmentsApprovalServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/PunishmentsApprovalServiceImpl.java new file mode 100644 index 0000000..e508ef5 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/PunishmentsApprovalServiceImpl.java @@ -0,0 +1,86 @@ +package jnpf.workflow.service.impl; + +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.workflow.PunishmentsApproval; +import jnpf.entity.workflow.RewardApproval; +import jnpf.model.workflow.dto.PunishmentsApprovalDto; +import jnpf.model.workflow.dto.UserSelfDto; +import jnpf.model.workflow.vo.PunishmentsApprovalVo; +import jnpf.model.workflow.vo.RewardApprovalVo; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import jnpf.workflow.mapper.PunishmentsApprovalMapper; +import jnpf.workflow.service.PunishmentsApprovalService; +import org.springframework.stereotype.Service; +import org.yaml.snakeyaml.events.Event; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/24 10:43 + * @Version 1.0 (版本号) + */ +@Service +public class PunishmentsApprovalServiceImpl extends SuperServiceImpl implements PunishmentsApprovalService { + + @Resource + private PunishmentsApprovalMapper punishmentsApprovalMapper; + + + @Override + public void punishmentsApprove(String applyId, Integer status) { + PunishmentsApproval punishmentsApproval = this.getById(applyId); + punishmentsApproval.setStatus(status); + if (1 == status){ + punishmentsApproval.setEffectiveDate(new Date()); + } + updateById(punishmentsApproval); + } + + @Override + public void savePunishments(String id, PunishmentsApproval entity, PunishmentsApprovalDto dto) { + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + // 处理用户 + punishmentsApprovalMapper.saveUser(id,dto.getUserDtos()); + } else { + entity.setId(id); + updateById(entity); + punishmentsApprovalMapper.deleteUserById(id); + // 处理用户 + punishmentsApprovalMapper.saveUser(entity.getId(),dto.getUserDtos()); + } + } + + @Override + public void submitPunishments(String id, PunishmentsApproval entity, PunishmentsApprovalDto dto) { + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + // 处理用户 + punishmentsApprovalMapper.saveUser(id,dto.getUserDtos()); + } else { + entity.setId(id); + updateById(entity); + punishmentsApprovalMapper.deleteUserById(id); + // 处理用户 + punishmentsApprovalMapper.saveUser(entity.getId(),dto.getUserDtos()); + } + } + + @Override + public PunishmentsApprovalVo getPunishments(String id) { + PunishmentsApproval punishmentsApproval = this.getById(id); + PunishmentsApprovalVo jsonToBean = JsonUtil.getJsonToBean(punishmentsApproval, PunishmentsApprovalVo.class); + // 拼接用户 + if (null != jsonToBean){ + List userSelfDtos = punishmentsApprovalMapper.getListById(id); + jsonToBean.setUserDtos(userSelfDtos); + } + return jsonToBean; + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/RewardServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/RewardServiceImpl.java new file mode 100644 index 0000000..0229572 --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/RewardServiceImpl.java @@ -0,0 +1,88 @@ +package jnpf.workflow.service.impl; + +import jnpf.base.service.SuperServiceImpl; +import jnpf.entity.workflow.AttendanceLeaveApprove; +import jnpf.entity.workflow.RewardApproval; +import jnpf.model.workflow.dto.RewardApprovalDto; +import jnpf.model.workflow.dto.UserSelfDto; +import jnpf.model.workflow.vo.AttendanceLeaveFlowApproveVo; +import jnpf.model.workflow.vo.RewardApprovalVo; +import jnpf.util.JsonUtil; +import jnpf.util.StringUtil; +import jnpf.workflow.mapper.AttendanceLeaveMapper; +import jnpf.workflow.mapper.RewardApprovalMapper; +import jnpf.workflow.service.LeaveApproveService; +import jnpf.workflow.service.RewardService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * @Author huanglinpan + * @Date 2024/5/21 17:25 + * @Version 1.0 (版本号) + */ +@Service +public class RewardServiceImpl extends SuperServiceImpl implements RewardService { + + @Resource + private RewardApprovalMapper rewardApprovalMapper; + + + @Override + public void saveReward(String id, RewardApproval entity, RewardApprovalDto dto) { + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + // 处理用户 + rewardApprovalMapper.saveUser(id,dto.getUserDtos()); + } else { + entity.setId(id); + updateById(entity); + rewardApprovalMapper.deleteUserById(id); + // 处理用户 + rewardApprovalMapper.saveUser(entity.getId(),dto.getUserDtos()); + } + } + + @Override + public void submitReward(String id, RewardApproval entity, RewardApprovalDto dto) { + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + // 处理用户 + rewardApprovalMapper.saveUser(id,dto.getUserDtos()); + } else { + entity.setId(id); + updateById(entity); + rewardApprovalMapper.deleteUserById(id); + // 处理用户 + rewardApprovalMapper.saveUser(entity.getId(),dto.getUserDtos()); + } + } + + @Override + public RewardApprovalVo getReward(String id) { + RewardApproval rewardApproval = this.getById(id); + RewardApprovalVo jsonToBean = JsonUtil.getJsonToBean(rewardApproval, RewardApprovalVo.class); + // 拼接用户 + if (null != jsonToBean){ + List userSelfDtos = rewardApprovalMapper.getListById(id); + jsonToBean.setUserDtos(userSelfDtos); + } + return jsonToBean; + } + + @Override + public void rewardApprove(String id, Integer status) { + RewardApproval rewardApproval = this.getById(id); + rewardApproval.setStatus(status); + if (1 == status) { + rewardApproval.setEffectiveDate(new Date()); + } + //修改状态 + updateById(rewardApproval); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/SelfApproveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/SelfApproveServiceImpl.java new file mode 100644 index 0000000..763b77a --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/SelfApproveServiceImpl.java @@ -0,0 +1,191 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.AttendanceDailyRuleService; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.FlowTaskApi; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.workflow.SelfApprove; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.dto.WorkflowImQueryDto; +import jnpf.model.attendance.vo.AttendanceSelfApproveVo; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.SelfApproveDto; +import jnpf.model.workflow.dto.UserSelfDto; +import jnpf.model.workflow.vo.SelfApproveVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.SelfApproveMapper; +import jnpf.workflow.mapper.SelfApproveUserMapper; +import jnpf.workflow.service.FlowTaskService; +import jnpf.workflow.service.SelfApproveService; +import org.apache.logging.log4j.util.Base64Util; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +/** + * describe + * 借调审批实现类 + * @author HuangLinPan + * @date 2023/12/13 + */ +@Service +public class SelfApproveServiceImpl extends SuperServiceImpl implements SelfApproveService { + + @Resource + private SelfApproveUserMapper selfApproveUserMapper; + + @Autowired + private UserAntifreeze userAntifreeze; + + @Resource + private FlowTaskService flowTaskService; + + @Autowired + private UserProvider userProvider; + + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + @Resource + private CustomTenantUtil customTenantUtil; + + @Override + @Transactional(rollbackFor = Exception.class) + public void save(String id, SelfApprove entity, SelfApproveDto selfApproveDto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + // 保存对应的用户集合 +// selfApproveUserMapper.addList(selfApproveDto.getUserIds(),id); + selfApproveUserMapper.addUserList(selfApproveDto.getUserSelfDtos(),id); + // 发送消息 +// String tenantId = userProvider.get().getTenantId(); +// new Thread(()->{ +// // 睡眠1秒,确保事务提交完成 +// try { +// Thread.sleep(1000); +// secondedSendIm(entity,tenantId); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// }).start(); + } else { + entity.setId(id); + updateById(entity); + // 删除以前绑定的用户集合 + selfApproveUserMapper.deleteById(id); + // 保存对应的用户集合 +// selfApproveUserMapper.addList(selfApproveDto.getUserIds(),id); + selfApproveUserMapper.addUserList(selfApproveDto.getUserSelfDtos(),id); + } + } + + private void secondedSendIm(SelfApprove entity,String tenantId) { + //张三的【借调】审批 + //借调开始时间:2024年5月16日 16:23 + //借调结束时间:2024年5月17日 09:00 + //借调考勤组:小露测试考勤组 + //被借调考勤组:小露验收考勤组 + customTenantUtil.checkOutTenant(tenantId); + // 查询第一节点审批人 + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId,null); + if (null != data) { + AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + jsonToBean.setId(data.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(entity.getCreatorUserId()), tenantId); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(tenantId); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.SECONDED.getCode()); + approveBaseImVo.setStartTime(entity.getStartTime()); + approveBaseImVo.setEndTime(entity.getEndTime()); + approveBaseImVo.setGroupName(entity.getGroupName()); + approveBaseImVo.setSecondedGroupName(entity.getSelfGroupName()); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【借调】审批").toString()); + data.getUserOperators().forEach(v->{ + approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId())); + jsonToBean.setTaskId(v.getTaskOperatorId()); + // URL 拼接 +// String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean)); + String encode = FtbUtil.encodeJson(jsonToBean); + String url = AttendanceConstant.APPROVE_URL + encode; + approveBaseImVo.setUrl(url); + approveImVo.setApproveBaseImVo(approveBaseImVo); + // 发送消息 + attendanceNoticeHandler.send(approveImVo); + }); + } + } + + @Override + public void submit(String id, SelfApprove entity, SelfApproveDto selfApproveDto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); + // 保存对应的用户集合 +// selfApproveUserMapper.addList(selfApproveDto.getUserIds(),id); + selfApproveUserMapper.addUserList(selfApproveDto.getUserSelfDtos(),id); + + // 发送消息 +// String tenantId = userProvider.get().getTenantId(); +// new Thread(()->{ +// // 睡眠1秒,确保事务提交完成 +// try { +// Thread.sleep(1000); +// secondedSendIm(entity,tenantId); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// }).start(); + } else { + entity.setId(id); + updateById(entity); + // 删除以前绑定的用户集合 + selfApproveUserMapper.deleteById(id); + // 保存对应的用户集合 +// selfApproveUserMapper.addList(selfApproveDto.getUserIds(),id); + selfApproveUserMapper.addUserList(selfApproveDto.getUserSelfDtos(),id); + } + } + + @Override + public SelfApproveVo getAttendanceLeave(String id) { + SelfApprove selfApprove = this.getById(id); + SelfApproveVo jsonToBean = JsonUtil.getJsonToBean(selfApprove, SelfApproveVo.class); + if (null != jsonToBean) { + // 获取对应的用户集合 + List userSelfDtos = selfApproveUserMapper.getListById(id); + jsonToBean.setUserSelfDtos(userSelfDtos); +// List userIds = selfApproveUserMapper.getIdListById(id); +// jsonToBean.setUserIds(userIds); + } + return jsonToBean; + } + + @Override + public void saveForOa(SelfApprove entity,List userIds) { + save(entity); + selfApproveUserMapper.addUserListForOa(userIds,entity.getId()); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/WorkOvertimeApproveServiceImpl.java b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/WorkOvertimeApproveServiceImpl.java new file mode 100644 index 0000000..8489a4b --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/java/jnpf/workflow/service/impl/WorkOvertimeApproveServiceImpl.java @@ -0,0 +1,157 @@ +package jnpf.workflow.service.impl; + +import cn.hutool.core.collection.CollUtil; +import jnpf.attendance.antifreeze.UserAntifreeze; +import jnpf.attendance.service.handle.notice.AttendanceNoticeHandler; +import jnpf.base.service.SuperServiceImpl; +import jnpf.constants.AttendanceConstant; +import jnpf.engine.vo.ApproverByTaskIdAndNodeIdVo; +import jnpf.entity.workflow.AttendanceWorkOvertimeApprove; +import jnpf.enums.attendance.ApprovalSettingTypeEnum; +import jnpf.enums.attendance.AttendanceNoticeEnum; +import jnpf.model.attendance.vo.attendance.ApproveBaseImVo; +import jnpf.model.attendance.vo.attendance.ApproveImVo; +import jnpf.model.attendance.vo.attendance.AttendanceApproveImVo; +import jnpf.model.workflow.dto.AttendanceWorkOvertimeApproveDto; +import jnpf.model.workflow.vo.AttendanceWorkOvertimeApproveVo; +import jnpf.permission.model.user.PartUserInfoVo; +import jnpf.util.*; +import jnpf.workflow.mapper.AttendanceWorkOvertimeApproveMapper; +import jnpf.workflow.service.FlowTaskService; +import jnpf.workflow.service.WorkOvertimeApproveService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +/** + * describe + * + * @author HuangLinPan + * @date 2023/12/12 + */ +@Service +public class WorkOvertimeApproveServiceImpl extends SuperServiceImpl implements WorkOvertimeApproveService { + + + @Autowired + private UserAntifreeze userAntifreeze; + + @Resource + private FlowTaskService flowTaskService; + + @Autowired + private UserProvider userProvider; + + @Autowired + private AttendanceNoticeHandler attendanceNoticeHandler; + + @Resource + private CustomTenantUtil customTenantUtil; + + @Override + @Transactional(rollbackFor = Exception.class) + public void save(String id, AttendanceWorkOvertimeApprove entity, AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); +// String tenantId = userProvider.get().getTenantId(); +// new Thread(()->{ +// // 睡眠1秒,确保事务提交完成 +// try { +// Thread.sleep(1000); +// workSendIm(entity,tenantId); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// }).start(); + } else { + entity.setId(id); + updateById(entity); + } + } + + private void workSendIm(AttendanceWorkOvertimeApprove entity,String tenantId) { + //张三的【加班】审批 + //选择日期:2024年5月16日 + //开始时间:22:00 + //结束时间:次日08:00 + customTenantUtil.checkOutTenant(tenantId); + // 查询第一节点审批人 + ApproverByTaskIdAndNodeIdVo data = flowTaskService.getApproveInfo(entity.getId(), null, tenantId,null); + if (null == data) { + return; + } + AttendanceApproveImVo jsonToBean = JsonUtil.getJsonToBean(data, AttendanceApproveImVo.class); + jsonToBean.setId(data.getTaskId()); + List allByIds = userAntifreeze.getAllByIds(CollUtil.newArrayList(entity.getUserId()), tenantId); + PartUserInfoVo partUserInfoVo = allByIds.stream().findFirst().orElse(null); + // 组装消息 + ApproveImVo approveImVo = new ApproveImVo(); + approveImVo.setTenantId(tenantId); + approveImVo.setAttendanceNoticeEnum(AttendanceNoticeEnum.APPROVE); + ApproveBaseImVo approveBaseImVo = new ApproveBaseImVo(); + approveBaseImVo.setType(ApprovalSettingTypeEnum.OVERTIME.getCode()); +// approveBaseImVo.setSelectTime(entity.getWorkDay()); + approveBaseImVo.setStartTime(entity.getStartTime()); + approveBaseImVo.setEndTime(entity.getEndTime()); + StringBuilder stringBuffer = new StringBuilder(); + if (!Objects.isNull(partUserInfoVo)) { + stringBuffer.append(partUserInfoVo.getRealName()); + } + approveBaseImVo.setTitle(stringBuffer.append("的【加班】审批").toString()); + data.getUserOperators().forEach(v->{ + jsonToBean.setTaskId(v.getTaskOperatorId()); + approveImVo.setUserIds(CollUtil.newArrayList(v.getUserId())); + // URL 拼接 +// String encode = Base64Util.encode(JSONUtil.toJsonStr(jsonToBean)); + String encode = FtbUtil.encodeJson(jsonToBean); + String url = AttendanceConstant.APPROVE_URL + encode; + approveBaseImVo.setUrl(url); + approveImVo.setApproveBaseImVo(approveBaseImVo); + attendanceNoticeHandler.send(approveImVo); + }); + } + + @Override + public void submit(String id, AttendanceWorkOvertimeApprove entity, AttendanceWorkOvertimeApproveDto attendanceWorkOvertimeApproveDto) { + //表单信息 + if (StringUtil.isEmpty(entity.getId())) { + entity.setId(id); + save(entity); +// String tenantId = userProvider.get().getTenantId(); +// new Thread(()->{ +// // 睡眠1秒,确保事务提交完成 +// try { +// Thread.sleep(1000); +// workSendIm(entity,tenantId); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// }).start(); + } else { + entity.setId(id); + updateById(entity); + } + } + + @Override + public AttendanceWorkOvertimeApproveVo getAttendanceLeave(String id) { + AttendanceWorkOvertimeApprove attendanceWorkOvertimeApprove = this.getById(id); + AttendanceWorkOvertimeApproveVo jsonToBean = JsonUtil.getJsonToBean(attendanceWorkOvertimeApprove, AttendanceWorkOvertimeApproveVo.class); + // 当审核未通过时动态取值 +// if (1 != jsonToBean.getStatus()){ + // 动态获取涉及班次/借调班次 +// } + return jsonToBean; + } + + @Override + public void saveForOa(AttendanceWorkOvertimeApprove entity) { + save(entity); + } +} diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/resources/application.yml b/jnpf-ftb/jnpf-ftb-biz/src/main/resources/application.yml new file mode 100644 index 0000000..306fe1e --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/resources/application.yml @@ -0,0 +1,44 @@ +# 应用服务器 +server: + tomcat: + uri-encoding: UTF-8 + accept-count: 5000 + threads: + max: 2000 + port: 30033 +netty: + ip: 162.14.105.247 + port: 8888 + application: + name: jnpf-ftb-netty +spring: + servlet: + multipart: + max-file-size: 500MB + max-request-size: 600MB + +management: + endpoints: + web: + exposure: + include: '*' + endpoint: + shutdown: + enabled: true + health: + show-details: always + # 开启在线日志查看功能 + logfile: + enabled: true + +# 智能排班:按营业额选相似日时的「带状档」最少历史样本天数(1~90,算法内再 clamp)。新店可改为 7 等。 +jnpf: + ftb: + schedule: + template-similar-days-min-band-sample-days: 10 + # 营业额带半宽 λ₁(tier1 同星期几·严带)、λ₂(tier2/3·宽带),须在 (0,1);非法则回退算法默认 + template-similar-days-band-strict: '0.10' + template-similar-days-band-relaxed: '0.15' + # 考勤班次 fuzzy 绑定:覆盖率与效率阈值 [0,1],对齐 AttendanceGroupShiftMatchConfig + attendance-shift-match-min-history-coverage: '0.7' + attendance-shift-match-min-candidate-efficiency: '0.6' \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/resources/bootstrap.yml b/jnpf-ftb/jnpf-ftb-biz/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..dc1ecad --- /dev/null +++ b/jnpf-ftb/jnpf-ftb-biz/src/main/resources/bootstrap.yml @@ -0,0 +1,76 @@ +spring: + application: + # 应用名称 + name: jnpf-ftb + main: + allow-bean-definition-overriding: true + allow-circular-references: true + cloud: + nacos: + username: ${NACOS_USER_NAME:nacos} + password: ${NACOS_PASSWORD:FTB_nacos_30099} + discovery: + # 服务注册地址 + server-addr: ${NACOS_HOST:192.168.3.24:30099} + namespace: ${NACOS_NAMESPACE:69c4eecb-05bd-4041-81fe-1473f95f578c} + #ip: ${LOCAL_SERVER_IP:127.0.0.1} + config: + server-addr: ${spring.cloud.nacos.discovery.server-addr} + file-extension: yaml + group: DEFAULT_GROUP + namespace: ${spring.cloud.nacos.discovery.namespace} + extension-configs: + - # 数据源及Redis配置 + data-id: datasource.yaml + group: DEFAULT_GROUP + refresh: true + - # 静态资源配置 + data-id: resources.yaml + group: DEFAULT_GROUP + refresh: true + - # 系统配置 + data-id: system-config.yaml + group: DEFAULT_GROUP + refresh: true + - # 框架中间件配置 + data-id: frame-config.yaml + group: DEFAULT_GROUP + refresh: true + - # 日志配置 + data-id: logger.yaml + group: DEFAULT_GROUP + refresh: true + stream: + rocketmq: + binder: + name-server: ${ROCKETMQ_HOST:192.168.3.24:30094} + bindings: + output: + producer: + sync: true + group: jnpf-group1 + permission-certificate-input: + consumer: + push: + orderly: true + consumeFromWhere: CONSUME_FROM_LAST_OFFSET + permission-franchisee-input: + consumer: + consumeFromWhere: CONSUME_FROM_LAST_OFFSET + bindings: + permission-output: #生产 + content-type: text/json + destination: permission-topic + group: jnpf-group1 + permission-input: #消费 + content-type: text/json + destination: permission-topic + group: jnpf-ftb-consumer-group + permission-certificate-input: #证照消费(顺序) + content-type: text/json + destination: permission-topic + group: jnpf-ftb-certificate-consumer-group + permission-franchisee-input: #加盟商消费 + content-type: text/json + destination: permission-topic + group: jnpf-ftb-franchisee-consumer-group \ No newline at end of file diff --git a/jnpf-ftb/jnpf-ftb-biz/src/main/resources/fonts/MiSans-Normal.ttf b/jnpf-ftb/jnpf-ftb-biz/src/main/resources/fonts/MiSans-Normal.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2e1951e8539be80b9d7496566f2c82a3a726ba73 GIT binary patch literal 8201144 zcmeEv3!F~X{{C8P&*i<2F*Al4=Hfogj5{GQn9xLZB*~GJk|gQf?>p&os#B69Ngb6` z>PV6tr*uO%?C)B8?X}ikd+)Wr&-b$S zdW951B!R?;?7|Dr>53y9$UKCuu7ziJ`E1K)@8Yzp!z$Z_lXIS;&N!QU0VfA0tBbcjJ7k!AOYjdegfil%v~I zXYjSfH_nq=f5GtuLN4Dm^tvksT(WV{W>NfeA&w7TJK)AZ@fvxz7`~_g`Ll-&xc18P z=Z)%uYl?=Td7=YoN6`heoA?{(05KGF znD{5?Sn&|(MDa4{Y%vFPu6PIZUGX94M`8)+GVvMcTJZ(wm)NA(C^mua6hDLhf=!BF zrC&&yBCCO>%XH8TnE{$5vq0<1GeFzPcA)Jg+9Ny4PM}>RS|z*5?w~zoPtabn7ie#J zG3a0AV9+6Q24bXqv*Es%%vow63ZPT3stR_E(pIUZDnphExc2xVjPaE_D~^ed<2Y32GwfQ|c+uX=)niboDIgY&9G7b%i@s zOVv`)PZaJ;tyXwz>Pxi&bd$ncQ(M*7pgYu$pg*boLK<;K9B5So_hO_O=s)HF6KyaD znOB2eV-|xBH*XR$*e}=*^xoh&Awxq$LqSJ{@Z>`eh0u#a4~Ou~Lo-7gg%SE8vcgMbG zXNxYKJNC*JJv;XV?SF2^zS&|(kMp}{i{a;WFU%GrdY<1STa4?a^E}$C=egNpnp2Au zK2g<)E8)kOw`(^<0!E4*j}etbx)YBTi5NY0dYq^%YB=$Dkt8ylxJx9XrFI^-ND*01 z+#{-pY*F)?D}%$tLek}=t4TMIZX?}Ex?fW%NL{3fq}8q+5WGfalQtr4M_NSMhjb8W zG3f}>`$?Z5oq6pw*Ipy%kj^JvM7n}>E$JrG?WDU%4``|ssRyq?Iq&IIhq0*Hz?+U0 zak#?ibfiSQaPiJhWvKf|wrC~Vg9o1{dW-&I5T1Yw&!Df2k+m3i`7ckL*l5w;C*6`c zqQtv%LSYdc!~9a{5!bj`n494*-OdtEE~I@V9tchSeEHg0Xl z6W*cgv_8ci^<=HDb$|H1e(Z0&SouDC@@+uRT5YY-^`Q5B3Hohxog5Ln?WYi(e(K}s zzj|Cn{eGW5g;9f%9Qpm0JxX82`VqX({@vz;IJTx*FIZnW$0r$B`*Ba>E2_j=T|U*C z3y!wNT7dWNwl-Ls!5NoQ)+nF#I8&_s;R4qCC9!Rm)*eoqANu?bYv1uT$0}xiEf2)l zBPXhSa#UPcQ>=%rnbu1{mMzvR)^pbD*3|HMT(`IN29V|@>wWO=H7EM^t=T0ter&Bf z?t3}Tdk`Im%30AEvmVC?n24t`$=YkZZM}-V_;t9f^`rFx#>gAtq_T4wsNJ*9RCsIj$>-yvDB=&cG(}^2Cx#(whVGQ(h*|P0CdAvXQ8p3To)-W<^D=vX? zU7^fo?JS=k{A{bWEJ_Nmpk#D8K$Nno?}GWhQ@GZbDc@qbCy$Wl{RrAcazUTEE^>q`L&wrTTavZr z+DA6ET8p}+pQ-f$+RUGgz}5!mh|)_re8)*^kDVh-f$Fd8xJ|>UOqm0@id%H5wyfAc zYr1t?i8P3$aLeov*A7dH`rHIj7gTc#SX%VY`nT2IiE&(5Z`ud~iB9+DS@t<6hraKR zo%rY0v(e{{DIV35qvy94RhZ}O3R90YvB9xstT5lnmCkkQRO4rA4g9I^r!|A^>n=Ry zHP+K20n*A;^rQ9GN7l#IYt{_wU5uQ!tl`!i^!ax&s&+$y`3id0A)xS1>u2cnUm*4? z#>*Qx{s3IyW%PvypwF*@PP-SjhwpKI6UuBlF^*Q@Gx*ID%kqAk+t{`dJe@t(#+K@EFw{s5lx4|saptzV$)lxP`c>;9vTqj`oN$rgMx*3Y^%TKBQ-2qk=& zjkINehz1ZX6QG2{&sYOw4O*dey@-Xa67~~AZ~TGJ=sVFAXLp@CiIUCyjfl3h;BKOk zCa!`@fdg5;M$$264wjXdB4?vw^`qjaRWw@9K(CIH15c}F{sxV)rX8(P=*z3M)j&TZ z`#D)}hRc*ZwivZUj^|!*2?hF;X`X%`E%e5yT~WB4RfcKULARi zy)1GKyJT7YM31Xu8e8Qrdi_&z!GCZ)D9Z);uYOKiJP;wO4~!k)n@(2S#gkm0vNr>ufH;3L5io)=RDq)_b{+zXFgsxE*l zZiXbb2{`gCB>7c9vmbCZM?Z_I@`R&~fAMZ!?Hjuq>w0XC{lmJ?nq^%bu3p9QSEU%8`5oJ&pwEv~n+u^bd%e;Nu%@1~+(Yc~6G12Lz<9HfuZ$+q8 zI_I$;*_v%cF9*5tJGI)yVHpunP zk5>P(s2V;Qmh8e|I6V@=O0oko$j@jgHTlSyQylX|kM0wx8&9HQ-9B8ICmi{s!?F@w z^wY3tU^a?#@uNp-|99lP_6=z(B+55u zI9Z=V@7ata&4ILZO8b$m72_yM?hp4MdY&nPI(p+4Sdo@kzeKKcGO_TL*|KlImp(hT z+73H9z6HNsXKSFxR@3Q{EBe{vP5cV}zuRsbC1yy?(Z7fdxKwH>D{yJOK9UdpZ7<4a zRa{3TUv%tb$7*z4M~g>%?RW;dpB{!29<3gfEO~0t_Hk%hhpv^>IKfI?L zZJV-QDSdx-|9};50kp)&pc(3LEfDjS62yECrL=`PMPz^*%)-o&rAWcprDpjq#>EWi zmbTV4!5R%+^dajh*aVA_`#xx=cY{ydVT}YH|Ae~l!9K%!(3)(Gr{!=GYI)pxShosk zkAr@TlzYH=w(uJBkn*fPYyZGwW?3VUwgt6+ijn;m5NiRDWxc2`I-}k1qP96l(<#GT zC|qmlkh0XLxWIZOR;#p^p> zp`Rleet#PcNB^UC3#d6^R%7`eHH$5Hr?A<&vF6*D1J1Gs z5+CRAg==T&E!HH|;Yi)M!V2)sWf;bm(Ixn_r!cf;=-XG#z(8tD(O`Gjrv)`zHN zQCPZgS_iE({EK*j`6Ou9hlU9|rjvK34)J`x#eHZy<~)6;;tbsFhu}tvXP2?;TI&;> zr>+-~^fbqE z+pBT(6~gDON1Q8+vu?L;vX-O#YK)FAfbVZZ7io&Q%UY7yp(Ptg3U9zqJR8?}7Gq%= zuK0}gBx>FQZ$0!D+`oR(kE3QSw_r4DDI@&D(|Q)N$@fI6O&EtO(Do0IcLDlD8%Pq% zaJwuG>xz;pUNI+hspmM?KX=m)z0K~kax zLFYQ+uXXgev{oE3Z^=pM;LdeQl8zk7z&!TD|J3#Mw8!|q^$!gc=ZZVg`hSOIn!^|R z^Ph=Efj|G<9_|j0P5L}s=|4~U|Dx^SR6l)U_XJ2b3oxH(AvpDFaH1uYX!e$t8&36= zPpA5{hu^kGX?|}bf24i?SNe&@DqHdcx-Ylpm3{(1Ltx{d|A&#V54Mmu;iXvwZF!=; zW5_L-C!(#QkWKbrglJj)M@WGC&|CIFp4$gG^(V{=((`1#hje-DvfXL>Kq13E7H(Ww zDTWf;d*QQ(4;v0F;Y|G6V)pO&|mQDL9XIEJ>S97CS+ zS-O=^DwUFmFIIi{cKQGR^b_d!dp-to$44=Tcm*u{pTR<)HP|KCmttnZFZ`=SKR|}^)cSHZBf_+r28y#1=^SOJ%(p? zL{3G;B0jLF#8VcH%x|@Qx~Ht-G8OSlxoNNoZG`1#ne{^CG@rsYybWzxfla33U+JfC zswM8<>nY$4_f~v=|JPD=f56jNqx(bYdq0fFrF5o0!4WK$)}P=uPWl!~WMe;MHvwN$ z70?WO#u{QV+5&-|qHX4SW?Phwd1St+OW}t(F{OTOqmG)D#7q10U>UW4^c2Bc#Gf1X zkL~EOJF)F?(oSJu`+w-ydFK>X`#)3_ZT<8t0U_4d9O4gc{)tJXe`u3W;NoRRNZBg1 z1pSBn9PiVo@CKEc)$xZk=a0Ck)|{KeyO$vvi!-o7oIVyM`gAJNwdP%wsbONjPtN@e zCm~D$XFapz*n>HRrFouxYCuVj$WMw4*Z~@2 zRiGT$J(^)%r4;NHcq>av9wuVT^Yk-gUS#wyBD+PW{u$$8)S6yVB{L+y0s~6L^qSrkQ1N zpv>uVhex;6btzZo)F#+(RM#1a9rX&ZDebZ6h+xLrH`(X5o% zvKCR@k|QB$^f%5?ZHSACA6wKJrFzcWk*4m##GfYpwA7Zgb;%k7ZE`;5puZ0r^qb%cn7{3K^Et;0IXEw|(cTUtR z8J;m$Qg5k^!@PkhxmgB%;tlYLMUV&f zV)h375jgXnT^VQRmcmhtx`W8AQ4{IMj?TKi@T}8tLrZ&JFuQ6suD%hNul-Y)Q*xxA zZei)!B+=XeccQmLxR-tSj%+V_eJTF~=QXY7d@eovVp-%$^vY;CDazg&IUA0Z_R54) zPhA+kv!gwQlfPor_hDPg*LD)JNcqaNMdvtvM@Et3sOTtJ>8*VZ-!iPhx1($9)t~Jm zn7!_dh$B@|x-T5xh&WQcP8)s-dxC%4zQCt2R$mjN;+wMHYUwtd;=XY7TmJ)c*p_&H zRWlo!6@7JoKrs}-dm9hs9>*p>l6~p>rcszw`C>;WS{u}P; zrN5Xdo$vq75qRLV@ZFZ-$#D@oLm08bIsa@Sda*qgQ()VV9+emo`d)1>K-m@d4)YYE z%U7JGA|FM$oJW$}Pt~X^dr_vNE-_&*JZIlAQGM@?y@EL-#GaeQ>0 zx9q%))1%#gL@Vqs$RRU*8`c-Xtq60_@NxO@f1xjwZ*@f}HWpz%-S5i6GTgF) z!bg8j6%;ywq~H2Fur6yULECrt@UAPBYNWf=QK^H7{!uEq{6Wdw6r8G8{`B#rl-320 zr{q!d+Fpc|D6h)SRpH{^$3Zyvb>-(z}C6CtbO4WjPoO0`}8>l zEAv6Dh_D{^=Pi(uqUTTPC$mOB9qezy|GOPIVVRD!y}Z0|*d7xpR4x`->7?u_XgAN` z+Ams*?LG?bwHg-k1K9SL&0GGMUy8zpuFr5Cs!~~Kb^eY?NjMMAhxHsj&>LbE`XkCd zn^M*NE(gD%tmL#G`1rJrVhxq`(W_9M?Cl+0lRMcfoXYZe3X^Dk+yX!Cr=@G=3Nl~n zG@Sds^f{!J@@y~{JTu4piymWT%UZADD%AG%8CcuGnts%{EL*1Z@!@R(W%BL9dTbx# z8|%-(8DQtLf6;6E=oG!yE4ec`wEe@=D~aQLxn~s4ceJqlvqWvU%d$1{+oqqKmC(yQ z|B9}HH}^H_`4#7CR&+ipZ8Gk60*@!-Ut%XmooR|$rT2&e8z)Nf29vppNoz!vz)~kA zM+2k$FXuD$D$=)0avVO=w}MlptXQWSDfM%A>#4^;ZXM?|okP=DRyT-FR$gf0xjMQH8eMuxKDi%Fn zqLRx+D;yEJ7nk{QM0|ny1@yd@IeD1FV|^=Ji(|B|1m9C`j98-&Q|+HLnV$Pe&>&V> z&*4ct6uC|IyknA7%W~349VZiu_Me|jJ;zoSob+9t6TYWCrFdYe;uYc9sTmQaF5?^= zY2KY4K^2Xnit0I@G-oc$QuGK&tkJ(Cd$^)={;$U|Uu2TL2iRV}`af!yR}FgU70v## z`7gV%vK};GtSGV{cGzjqo~C0py~p7RQk(~~O-ohB_Qs*}yc0d2rs7i8Jxo_Q>vRh) zT-d8Ud|_v!1skB*{Q!Ah{~!0(u-~=fI)2+!$Hufn%UpOSyYLT>`S=3=2R@{l%HOGm~#ar8dj6THD6(7%$^A(bHwRJOQy*!I2^Ay&hiTHT2 zsjvykVSh{%TK~e>n~eAZ%ru>44Z)hq zBav%9{s(X&{)g=y{Eyl@WjR#o=2}-+!=Q1$glj^AjjZ2}+JJOO`2aQkjj?-Z9T;Ml z*BsF9gSgfMq62e0W&J%;gPqUG{iZdNImU;dcjPR1=!dx9S7D=sR?b{}9#~oWW4)#i zuH)zw&`Un%vGdM!{6v0Xs|B9jyO8ZxJ7YYc-xQG{Eu;b24$8+k|W2H!~~4M@J8>`?2N>q``_cE8VB< zSyW-|53T?9=(XB8@Ej`Od11+y+Q|>h8DoDp#J2AiwsrqlFH&K?vUc(cGnGk<@KOBl z=fCl$c4BqjY4+WM<2wHs09+df3CS71rJozlIUJ^R)zn63*?t`G$S*PKKjWOZ>A*gcz$p|u+ZpZN-MgD2P8jI1PtqF6> z)`HOq8Bwo~9sN7}Si(5jB2-p4@_=3!4#)RZus`9cyoP791Tw}X*toZ14)*8zUh$-k zGA1#G9T~NpHRPD)L`7O{&q%;=eav=$4fU=;TNmJ|W4-H0uIL!%v1|nQ{~Gx=gA>;Y zpF@cq@I>qY2ELD(Pe=O#*^#fprFE#SHn?+@*+f65UOO&o4PKdrv@Pg;J1QDi(WMU` zM~}vP)U8GP_Ub2pJU_1Tu_#gkJo88N$*;kmf5p3UB%a9m(lP$y#pfkyrL2KE73ipW z@nL?DwsxZJ`wk~Lu$cQWg|%;JJLKjqj=il(1^K*KEx=o5g#cy)&wBv9aWdqYO=Z__ z)ES~Zv=zzXcs`$D9q=E4XY}(_aBVxO+!JB#Dr&`qGtMgM4H z`|jx0mWus|7=QY^h7xNvrI&R``AEMj^dN0hwcmMN#J<7Pd4oM`t$vf$Cstp9b?tqy zOxMDD-HClK_Je5OBq60jI0gH3?9X9;8M{V{dD!2_u8$XEUxB;cmtC8YmW^;b!g|>AoUj=}z2{*pJ%^g-;41H<_7x~8t?SV$X>}wGlHNmFKzadb zS4|In#rROt`lRQRj?whc!=wwDa)GL>^DC6Fy6CtyknzEc7m#)(Jx9|+4>Rr|y@Irl zreYsffRN%l^(9*0U)L=AtGS?;>U4RjuG3IDZYZ56y-xa&$TOGSk*6p2K6obk5%x!T z2*M6Hiu7Pl#GZ;BTtL=xj=_s$Bjg>39a5+*tRn`da$X!e3$VRe-Y%2?77uj9*lzn7>IZzIf0jX=hMUIf8tz9kpPMckLmRdOA2Z<5>O_j0#9fKGKnQ|plMle2t@v)4LWiQBOJeTSD`0iWEraCTe(COj^##vU_WiMf=ChR*+SSpD% z_>~`Ja~Q9|cs<6`8P8%oOZP!plXR+15r-I$WqiBNDQ-gFkXTVvpDniYGd_&hdW!L1 zb$Zp&)m*1>-z?iBQ!#Rc(<^l^WzVdjU+O+-_sHdPIL=4!Lv;UidMDzidEYG3?@AW& zy+!xUhD@)^^uzSf=-wwgFsI$4hq0{P7ovM^bbpK<6Em290&94W@%MCGtkQ9@is{cY z{dvaMB2~(@%oE+~b5#|jNB80yELB7I#FRr-UW@cZu>*DN!oD9pc_Kn^Nbv;rsn};= z*Bahz>~pctca9e#T#75iU1vpcCqJ6fKk|&@w+R{lJ@(x~ z#6O7e0K!MHPsTnC`%LUFVt)<$Th8$Ugo}i9VHmpNoOFF2O5uB!pG6&!|~VR-->@NegW%U6u&%P@2le1 zRk&}8-xmLU{O8j&u=+dFyo48tGZ|Cad>UKOk zu5ljrUV2Bn`-XSdWvK5m)MbY{-pVz|H5B=RdUxIEx~<$k(ly3)BK!TW39gFva9xvJ zQ(Su2$J1TUmD*o+&2d$<&vU)+3h#@Z_zKr**YWP_QKq7Ovuk@vT{~TS89(S&t}?sZ z!adbJ!#&GA+dbDk-@VYil=)Y>*CO2D-s;|Qvb$@ZdzX8^`;e~36XWrC5nZSHcklOfIoaLQ-P2Ro;pyY)k82KecF4A#3U^Py zGu(5FX9V}r$TiOMpyyG~WY09uOwWs+*F0}|7BF1oS?Vccl8nX_1?|i?cSZG_PySNKIL=y5_~D%y}ooIeA&Kw zzC2$u#9I5>yC?bzeMPOE^uf2#`?zlCimv2AP4*8b)WBeX}qCeGN!(Ypv!{bK& z7QPMsw*CTt7tNdf-TgiNef<3yALt+A5BP`sZ}E@d@o4`z|AYQVHNPd^09Q1>)qT(p zZ1n?M83(@lfv$n_EO(!f( zYK{47reDqUD2Z0=XQ{h*_8&YOE!PH^9-vGarB$i!I;R+}&x+x!!Pc$*#u{v?_e_?( zfO*C;PkA{wTC*F;{3Drjq<$}QB=4>P)3aD}l$Ix>W#|j6ROIi^oc&pYZ6S!V4dASP zZvFY(zG6<=BxJPw9WCGQVV*s@Hbs3<)ngmxGkv~}D@uQIH}hoZbd}0Hv{lG`OiyG^ zYMLsJDUDde5FM9$d5`;*w=vshwz5C0L>%&CBhe^I5hI>#?<9TT|Z5`fVNA;Mq#d zSqa~{OKHe&(Uuho60uBUdbZ9Zu4Sp3j8hlFzi;4O3(Cu4sFs%nH6=mWS(waeGM&9q z1Q-u8J;?ZtjNi!kb&OxfxUG)|t;T3WoKBaqJo_-|MADh0wte7L9ha}`vT_dEA>|u< z0=DjC>;6%?6SNrQ5s>AvZDY<%KEq6X*J3r>z;P#5GQLee2eE}$yhqJO{k3_m2Kc{K ziT%ob+R^$f#yhkC8D(LB=B7(stXltpHYFK0vsJqpA49rV zr>F$ytiGZ>W>@guf7R*gF`nI{bBgh5fTqSZpi>xslJTc>T*esCYQ&>VpJb=YMM!y! z@yD57z&vL&K7sTBru1R_JDW296Lu-mPE7B__ydgJ&G;C`FEZxgD(e|v&-4kTqnR>V z$K@5gi!1cgQ0Mc$&ew6nrQ?Q+aXuM?ydq|!PLJ8h{Ok{E3-hdC{uMeduhCCiT}pbL zrp5qGV=mMb2nQPqEXutF^gjHDjQ}?m{1=-M2qBB%-)@9>5Ib~B9#2LH4CJv+*JXf( z0$3=3h2kyLbhEf!j1pt`;`uQQU^y zV~}$KQm4RvKOHu8v{v`#lec&u`pO8Pwl2y79cWAi9c;7(El?Nd3`$TQD0JBya6v4+!vQQ6v7{MWRqxe$Yto|;xsDG$`h_BTh>Q1pu-KFjl->QGAe~KOIZgn?&4ELye#rJBZ z`c(X=R^vJUq&5Mk52~%|YhkHx)HiV2>`*^SrG8TTq)Yv*ewKbi84A7`g?4TckUimh z`3|lA0ch%y$v}$+GE>$T&1GHLL$raN|4MPWyh>grhRdtvP2vVvOm7vVp*4#`3ThB$e)fuHPR#2In2tKl+SvYX*EQsg;CRU=LIG^!ibSi}9X~VDHc+=si-TsUGTlb%FYe>Zz_&gVa@Oh#Cs3*mcTOL3N9|Ro$j;S0mLZ zHCl~Pc8rF^@4g)y{ukQ^VB=)U9~{Hr`}f| zsD)~=T4s1rE>3->HiDCGSKlf<+AFC8>Y(~nSq3b0MywHUxDAg{#Yi>MjT%M<-jqA^ zLI|@{La&BiMSp)i^tw=?H$ra+BlKqIO%W4X5PDC2oR z0VyWJ`!@$hvSR!5#%fx3ug|*@focR*ldt#&51W(~k@iRu? zFW6RyU!`AsB2#2Fv00|ebn%VMkQrjT%!0M~TUlS8A$G`ivK{iZmmS0**->@^Tj_#L z$!_p;85jdSrAzk0=9azX#gI7vDhEp+JPbon<4}2>tS&?HdenH6ycwRdTjXuBj=U3F zeR-E01<%&>IZHN|ugf{|Tsc?1DSOEG;3GR(4cO=ZY?Rko@v$E&)kuAHFis|NC6m9HAhM^#hRTt2Q^sy1@6I#acmPpbk|AfHv8 zRA)KE9AFNVGtEKf)pC}3jae*THiw%x$+z%UlhFIn=g{L8g+2-`4lO~i`#7{B^l4~S z=(Eu0p>?4zLR#}mM8AC%{q@bzC!v+0)uA<^wW0N)4WW(bpF2Z8qK`_EP%cOGc_k7x zHx?3zaF;N^k)IWDKn9Ehpu-2Ci_OJCf+wd51&l}+vA~Jyh}Q_#Kpc31F#yCsp2oq( zNN*Bsg7l`rrieEKg2bY4CK5e7fvJJ1!UYU@S{Q+8foVboo(Vi7;sVnH(?x9H*}$_v zl^KBvp=vOd43N3j64SdkcPX$S6w;?J)QYil~Fi84?e^qe(==Q-n zgIO9Wcn$(Jme!Mnx;LPpQElCs3iluC`a_g zKgXqm&uCB5qmYm!_{uBDrFqU7;tgz-z(p2(96WXQ&y7 z|402tcpwYBfU}TnKwnX>fX;?SnWtV=uOiPJHAl2iZ>l$u4v7aT@2YoE!~1F>QXu^x z4(SK=EK|#n^0E3DbcI@hoSy)dn?ded3CU%>S`WHa!A}W1{#qDX?!!IqR{Ic#w1oVS zlSD&wLkWr>XpDhprKKg2t)&f6pWzevhTrfbUI}t~zL97oBA#RDgMA z5)H{Q8O&8%nEL#g^^eU!>Eb5N>* z*#M<-&0LhqGxJa?-^@p;MrI?FY7DKRn%Tr`f>KS*rYP0SY=%BK#KRckVJz{` zMLdip9=bw(ArsdC|Dz`6Aa9I0${ZzPd2LsD{Dp?J6761Pt`Z(|wYge&gUP{U;buDw zwj-YHFxZaxU}i8A_^Z)5E()CuwkDpfG1wZ7oX?P3OLFU~kR0m)1v-kZpqg_laUJXi zeta{159s}n88gJg*!<+^206MR9)}c}K)xPBz8*`y9!I_&52WpB<1@+QGa(~hENYO`Cy~=vBd4!MPG60j zzM8xSmYVAFT6ry~=K5)nRc{iNAgkUEIs!AnD#?*@B&bG%G@?Ng(IAazkVZ5}BO0U; z4HAh4i9~}$qCq0jAdxtbNE}Eb4kQu>5{Uz8#DO&8KpJr%oj8zA97rb)q!R}!69+02 z2a<>bNyLF9;y^NSAW80%zlao~ftP5IOf*O)8YB}9d_)5u(ZEMEkm@pZneamcx&l-q zgQ4|4QHA&r2dyY1GE}iD##xOOidbO~D-7!7ej-K;5hEUY;V6+x+=x^6LMQh_M;wRR zG={i|A!)>rL}Ex9F(i!`l1L0mBZedrLz0OhiNp{uF(i!`l12VT+r=4N6R&yNN9JP5Z9<;5LM!d zDse=Wc%n)oQ6-J2l15ZXBdR15RT7CRiA0qoqDmrBC5fn#NK~mtR7oPLBob91!^7sK zr7{CEr?WB4^k`SGq%_2M&J$-Ly*+fs7*T*N7F?hU$I{n}!qj8nHu~mZ2Yz$;= z%~=wmjot^@Rr8l5*wH4S4G}JrEFQs@LSB;$Uh^y@)*0e?$h@<_cT&lBs*&%ck?*9F z?^Gw>sX;4|w0Q*WLlRp0MR@MLRd3-{7c1B*R3FtBZ|JY8A5tz=e?#2v!2{F)#4lHu z1MzilR_x8m?8oWqYFLR^u*o->L!2!CIWP zd)PYI1zoTre1Y0vNkH80ZC}Bj>{6Ry!K%cbm&~45ojordc82dz8*B~8vlHGQKP(QA zv|)Al2`O6Isjlr?=rc;5PsC`>p41-wC>zJRC&i0gY%%OZ1BoH=>=`YHA@O|E@qEW! z`F7)pAzr>+AK$K*NaE)kt;{!?#5dZKZ#0>2v=x!02a%+ZNYaBy(t}7+NF?b&Bq_iw zHcNCQo)k#@5Z5|G0a2xpsM15m!XMO;*ishJ%~rNhys}a8=lFY6i>7% zlvOZWxEe91P*#Hlq#BW@5cZ1dqAKyH5Z1m-(T6Bh2unpXkxncs#9ZH2h- z;iYLK+lapqoeJfd@=V0F9#ROqNqgkf+DIXhsydOX6OpPhY$!$IT;f$H;#DVYNkKdN z$V)(N{iL7lCxXPT&cv?H#IDY;I9@IK62Usd&N5UK%VF|5SRw_hzfs;OZY9cP5#_Roa#=*VETUWwdAGb<+#IF(^pN+;dtu`ohndhf z5%+q)W^+H{S{Lde$IJ2JMq*$O%yfPbWgn6c;jGq<3SrlISkxja7Q(*sB;r$`DHY16 zbv9FXw}5{Yu-C-V^7^_vHuT0;21A@D2+E4mPeo4_u05OJ-W zok=uqEG;EPQ(|&s7@iHpwVu`(cB?qV<5j%qLX>U?51&VzMXYY8?OBNH8P69Ix7!i7 zo5Mca)8UL~kTdoMH@s9_L2ejN4j4}k=p~*fz><3v5Ljz{Ey)cPY`WLM0&6N0r8LfW zB_E6@AH0Yhum#cHOAgq9nD2%C_cn1AxnVq!zcOs{cZl1`4U>ugiR6hbVIjU7Tv+qL zcuD~YlmZH1E508Rfz~H`z+U{2XhVshP}^Lgi$0zD|x8Fm{wbt5$5=Uc79B0DXy#y^;s?e_}ee{6+e!1vJ8KehgkV48JjVXgPhfVx5 zF@$K*iZ~HZNu)U}Y%t7_V{1L0LnN$lm-7( zL+xh}nUr^WXdj9gOsS^_rJlyH+sBFllzj?e!H*Yz)m{j34dtLh!wbuQgD4%l&`2;6 zMAaxwyO1(bXGhb9^Ss>nM=e7YNRQl%R-ky22`-PX**m8;OkaV2e!DcA5k$&p@0wD1Jr!5HL6n7_3Ac;%-~wZVqu5_Bsj6 zBXKv5xLb#~8%NwtfPRPB3GkXFgKGUQo`~xv;>Htk-9+4YBCZRFTSNGWxJf|VELgCz zWw!9jnzE)yhUWnG30Yg#7Jhi&>WZ^vJy{R@v_7nWIl$&z=#P0a4?0@D%tySDY>apl zV7AsDTOdzMV79GA<`A>vf!Sw@uEgmad6qm&R3=($y|M%H7XY)HYmW!6-dT2rCRGS) zVjX$5JR5OsQ_Lrdw!%{r3dVbK45uI#4m)EF%^h@2};>m{#W@|$QByWtI12{ z0O*pJuZyTnj6YlZUeKz4LZ8hi($^=_w}n=F57NiN(pisq zUx#>Kk9c2)cwdirpHIB6L%gp`ysuBZuSdMkA>QW_?{k3nkHG>uNlt>z^>O();!ntb zBR&~&KrZ9}%zBYiAqV6_4wwN8=zkyw|^g zat-XY%_u9h#n&_Ifc;;{uW|Kl@;lKE-+BFj_)gey&!Ya`mil{pN)I{kW$lG5tF`!! zlpYEwJ#?b<(4NvmA*F}*lpYExJ#?n@P)O;a0Mdg?oUPoD9=cI_=tAkCo6^$5Ig}oX zi0E~Q#C5b+T6ECfPLTtD=R}c6-0ce7eH<7$8TQ?_MBE%8?o`B|R!<|Rw(`akaoZDd zbBMT|h`4RFFB>`G%Ldhm+mtB|SevHaP;Vfv5w{KzH;0JZmWW%2 zD4RnxZBI1q1P}NU)Tz<54skSxINFvtnnN6=XB>GnhUE~q>JYJN5~bP_nR19pIlv*z zw<7|%fIx|eS2ijGjkK*djR=$j?5T$IG+2Dw5_@dC$<^L-{MWUDl&pOyNz}0}hPIw1 z@~LTEN=>T~boK^F(fZb<)ZFu_b^VQ+dp>oqM%3B!sk2`Oo&6Hz*LwN|C@~CiZP#lA zJse&MYG#e7gXdE}t4tj{pITZYYH0?wG~0t?+xr?*ON*nH7EdiLky@IIT3QZKqzD-D zHE=@XM-jEOBI1W4eiRWut|xv}p{|xnU9AdrwN$BfwJO96C1YfaC?aO05;Lk0Gg65e zRfrj>QtN8nX%nnMU9A;$wMN8`9>fnt{D2<_o*+g1s7m}$#E;?B*c9>OAJo_sHMTTr zY>FCN8Z|aW?%$9a+pW~t6g9S6i3ExmkVcJ7k^A33y{$PpeqVC@1akbpQg7=^y{#|x zwpQ>?UnB;@JKY=HL0cYMQc7ImcqzVjtxkQe4Y>amh-=?d8#zc0LR@>G z+K~IVA@@HAeE(W;4t2Y$$^C~>x4V|QT`O5Ei*YTj;kBZMS40i374hIk;z1@gyi8(1 zCNW?LF(8wAUJ?0zcUm)x$nmcsuP-8(FCv$}ja!!@S?*PRs??ij<}r~TM_y7?c~?}$gew* zU-u)w?nHjwkNmn7`E@_?Ym@xC74^1rsj;0)F5I2ET6c2W?&P)!7sId)#Evp~SYQ61zJrf1-%hYA?(+z+XE1x=CBkFLrO*V!)TwUsLiPYiF zQ$rN^12ws1TD0=1&z-M=Du{Hg)m5fe*NAqld}?(UI5u2u-O8tKH^Q;!-l}eeR-tEF zHKJ}8M@v^ebvqYzyIAUWiPY`R!wevBB-*?3spmDKo)<%Ou{FFRYIu6)(*)>(TEk1D zhF6MK|U^Qr&Ui?T|)sQ=Za{+CGo?=RH<22k_sPtEUgYJLN$`CU$HVLq*e zF6w`Zn%`AK%?8x`8dCGiBZ3wYK{KfH6;bEQC7Kpd=S!f@mq%n()bkYeJlmJromOu} zlpRhzuRCquidb8nSlfnJ+nsuzqMp}~SX)HhE{j@Sh+5qp)ar&(tGk5yT!8wVpZeT& z)aN|Z=Pscp7oaBRrzUqDH8~Icq+K}su?zjA?dT^xn|{*H^pmzUv(0SLg5J`O^p+OT zTiT4?(lhBT?Lu#9J9{fKUC|yiaIZ>*y0LGSc#OdDpRUbl$#W#BuV*5Q8H4Li400Z ziqcRFt<^EKR>zi;gOVIMNXtGx$~!*FIORQ?KFTs-pJv3HsVJ@ZD5v-+q4+49_$Zb5 zD3ACkiTEge_$YVyC~^24Z>FBBl0fwK61}}dZ!dA%OC0tQfqleYA5qswy!8=jeZ*KF z(bY!`^b-U94&rH@qLPhl4yI|#V*)WMk(i^Rd?qnOm?WZy;rK?hKSUBYVu%$nL5BbNH*C>ybdN>5Jl;iR6ol95Inx zP?7V+(DLOX&r6EpWIl%{={X%fawACjkn?@yIzI9kA9_`?Iv?n45S=Bhq%Ilu{PGbpL&QBu!kzs{g^o!IU&!Uu^ODXv*O2E04 zfLl@m&ZX>|OW8M$=fS7p!kP~z-FiL)bR&2)}U$OW5IQk9Bmu^ay zk}_pF`9UH1ftNml_T&e)f3!NshM!|2nY_Wvu~CV#V+_Ye0y%_1+0jit;pGUixkU#3 z0*Uks5sj()d##Bm;Z74P7QfiE&)R;>ilSipBmz*Y#QlpFfrV6FTJW7p8 zlo~5jYRsk77*DA&mr`RKrN&%JjZMj)@+dXdro5O#88MeKVtq=7t;xOe$i1r23s8?T zVk%|CT*`=5)FQP=oIy!3iIQSvN{VMuQp}|dqZ#GJTynlVa=t2*7pqcIOrwlgKrZMd z7pzJy=%p0cfxOU9UKm3z<0D5jDF4-<{l6NyV+`fIJlgp)Decvzw3kh3uNI}fwv_gA zY3FZFnXdt5zFf+D_MEv~^4BVq`D|-S9_{>%Xj5rIj+;mM&qX_bOLE^l%70mu|5}n0 z=TY*@q2$+6&+-?o$eZ&h|J9=WmrFZ;3)=Y`Qx?ppEYgm$NIP25&!o(eLXJKjUM0oe zBiUm-`fCIcDO`dT^f9s3h(lLcuhV~AApxH zgkMME{b7zZ-<-7X5Ldm299>c_Ng+p1pbz_te1b^zM~VC()_}fuSy}WN+GX`k3wpDmXasRpIL{zDu!Gt zg`k&9#f5cC7&E6iyS4%nc<{$&${F#DdZ)Ryd)(`ql_mn@sgKR zAusWee|X71vdA;)lUvwaA&2}RiTog!I$#$0K?CxGhU5oX94(c}39>l)vpD)QIpVW0 ziejnd#;_d*HCROsCaJG#E#Nvw3()gd(m2lS5thagmc|j5=2$@W>~+JD_VtYQc#bMB zN0pb-4=j+dL8fxtq;cGMIc~fhH{KF2RR(-gdh~cXdb}JxUP?2`96er+8!yL=mvTue z<&sJqHO9>-|5{8!&MhYbiFC~l=N*G?+)lw*7csaJb z99v$>2dR_~QYjy#QZh)5lI_ztdhB`M(UzoijuLxhM9=6;i<0W&iPLG0T}R9FUiND* z`?Z(-+Rq;CWzY2zDJ!v`dfEHD?01PovdZjt_DpSClDFq;hiBGl896=b-6zp*A!)aW zwfg~kqCM}COsVXT-V*zNC6UU$ z>1E%nL}W@OGF4?i^%9wqiAmLoNj_qd#vUJABiRCc*9kv)JdsmzbV!a4566XpcDO|x zzHIY|Sk~@ljV{(0gSu{IPmd*{9J!|}Vu^=XqRMFpRoUzP?Dc;3dOv%8MpUo&v)B9C z>;3HYDU|B{?Dc;3dOv%;pS`|1d%d5%-p^j|Ct75(?`N>@r?T&-vhSy|@29fwXRz=4 z+4mFJ_x@XRz;Q5NR?w0y2m+ znM9gQB26a8L@W^{dQ2!HN)n~tbVvFv#*9~cWGF}L(^ka9>y;val*3jRg%DbRsFSO9==5CgfAoY_mMYXw(7m$_)p<0$k)N;m*M-z zEs&XhfqYaM^9$MvcgMbGXN%&_9eZVqS)F@=?l`w&-)x!F$q84mYtwm>XF}}pS7T=KHi7)jZ5mPa%bq;j!<@knb zE539(2rpL>zEsjB@zovX&sGz);k9cc3Nib(4{TV+46{637;r!gD$4SbvVu~-MY z(=PF=)ZZItI%N&az)i#KjmBvI*_^dI5ZHIKxC^#{$FL&63y>2(z}K8#K;QofUun95 zUV3$9ecc$$)lJ7*z)i%Nm=SzFQuwjs6RW_zcz#coQ@HKf#K#--zA9f;X;; z%y!DgVs3ABU~5y+4)cgF5P!w&U=y=~|A|!v9v9QaOIUH>L$MOymTnh&U`_DKRLs?| zuN#LM!8PFNY=)W1-NawuySxhD+vDqK@ql;&vw&X~Z;M6ZQ?UW_Que}UsO@F7oU-xs zo#u$<@Qa=!dcx;9ScLGU{@qv&;oq1&{EC=|Z(LX5d)6IdAM6)?cmr!YWnK6lE))LM z7NP^ZmKTct;%ZSW@I5wWCr=jtfyeV5td;v2zJ&cw><0%-0H3SllyzgCau()Ow}dD5 zT+s_(-3}4gi`yYhOu*NLGsUY|L1772+5HMDhWrd$NhN&YTGuJ-!EESkcywDq4(To~ z5|@cmxnZu z^gPl4>3GtYNmr5X*VLU(+K#mUpc}3p=)RqF6zN#f@uU+;pCFw|I)ii;>1@)ugLMt_ zNf(kXC0$9nmUIK@R?;1$yGZww9vXVx6+=BSq#n{l(p1tKq_s$MNE?y17!y(ygRBNOzI$Cq1O8H-^+hni#rz*dT8zX${g^q&cLGNL!G$B`qNBLfW0QXDHO9 zskaYlf6{@ZLr4Rp!%1%;9YH#pbR6k}pv}CGl1?F=LHZ);9Mbuui%3_Ht|i?>x*fE+ zcNggaO?`^gLz+aIPFjn!A!#$xwxpdvTlk7dFCgtr+K=>d(!r#|NQ+5tCcT|>6lhD| zSkm#N6G@*Sok}``bQbAs(z&GbNf&~)@+~D@NxGJF1L;=M9i;kBTlw~r9@5kwL#prh z41XeNDrpT;eb;CBb4VMJwjkB_eulq*v(ANH*r23t<_Um`r+OOYfYyS{Z{Z3o^ z^*e3t*YC8oe*~$1r>*_^owo7ociP7PDCuOD(GhpxXo@D=aKd*#z;x% zOL`gUAkv|vLDCyZZzCN^I)?Oq(h0+_3>GI$BAr4yo%A`WuM(uL%8-DPA&pdlgj*GI zV;be%|DWU3KMx71O~1F3p*2~<$xwSl#IPPaZc|LFN84ga?RfNA57SSaVg&}s=z2V! z+E9OwtlNDu)WsT3kosQ4)ob}?l1?L?O!_D(*4)O~ais9gB0ho?s~sRd+(}W;*6R3f z#?#SZ&BMwh+dE+^ zCv4<|^_&pjhO=C%6Y3R1bbVM=Ow{+aAjOIjBCSE1N{Y4AaMnW_qp4S~G+5u) z5H!bEhg7eRl;f*Lnnda&#k!D4*Y7sRi*;N;cavhJGQ{j&dw*DOFEl$4roK~%cRedPA8p0I*D`w>HVZ*Na1TosoO}$gXVh2l8z$1o%CkX zV$xxxgGnzZ?MK?1v_EK`w-0Gg((a^PNDD~YlC~giM5>>5o>xEZJoa3@o{NN)`NXOI z_aBKbc2Atbet;TIhBjgiCqwgD!^u$ne}xjZj}xP)^|g7O-CyVnwT7_&Q@CzPKpHI#r|a?ZX6ot1t~;AG+ONODGB}S8a1*yD;m9 zX_zURZRb*Y9HNveaKiRZINAyI3Zhu~19MEK!3Vhwwzzsmp>dgUi!s5NWh^u{7<*%U zG1)OKV!Fiii5VPoBlF9ceoi>Z34=~}yA$5;gj1Ywwi7OP!p%;2$PQ!Eov^hN_Hn|? z8LHS}vA4teggc3KS|0nLeFw36jOuhfhIKg24&$D1!b5hsc$a+~pXnUuImexx@B$~) zV;l9v-{ORt6X;{7{&>wX^s(kNI^1f9uHJUNSWBAe{)JAs$O$#CLVo`yC)|rTSF8e! zfjwyw=4h%u&q3+{BuglUN&b5m#Wop7d{nRZ`*V^;83u zhZW5lsm7`a)--FTnyVIA)vT2|L$y|IR9mcV)=r&;zS}_+sE!J&n_)&_Tg)VEhZ%(J zF>_GQ80>`kd5>d;p7h@jdzSQ%ftA$|UjN7nHhn+q(MG{%ApLWZ@A3+^xy<3gy5MEE zM^S@b@ibh+&BEi)F1Ovx^KL&x=2BZ1DQ*QaQ#8k7s|m%^u@U`#i&h*R!&MZE2~Rx(T1>xeB-G zCHbTW&)buY7V1{E&~Lz#fPEbH;z*hs{K9j)ldGl9<=%!Ay`Hcxhm~7BLlJXfkHL&u zLryTkBR&1`PuJ2tR}2)&(+~XQCG%x-zxlIy!2HELX#N^(5NsIC4dS~}PhVK>KZL#h zBjDx|*s+(v%keQh4xhmK|0yi-pTR1=2ELZh;lKC-zJxDf*WV~MVb#S%thtz^l2r;; zT})L~RW+=;n69d;8Y%J@{+< zR>syccp(E;0@-Jb{L$5I_~C+Ql6eduUoa)To}ZfrV=g{Pj|@o9#{hK<`jHk zuu$lj)-o_h!qW;^`&Dodwi%)8LwMi#>PmX5MU^?Hq|CWa8HF;NgI8huZ|H^))^3*= z_wIutCGb;VH`WK6`C=DfmMeens`G{eadH3?rJBY=hmc_z_)-Tbs zy)oC@UWXWfi!<*FWxj6CK|ZW9=pKiw{fgDhEUaQCvEYD#waa2ny~sxzqg7+-3e`?l$+BhfFJoSla)`SPL_a8_8CfIb0}<4PU)NPpOH4fhUCm>xvhk2iGVOG^5_~KXLi=6fFu5ZUWJG<2Z_{Zbm z^G-6V8MTZ&qlMAV=wx&=&NF%%eT;s_WyU~bh!HS`Ln*cjiz#DjzXgw+aNkWSm;tCo7E3mknnRxWfwT0IkAxYRe@;P88}ilGbAYdd`H zl4E+n@%LgyLl>mkv+(6h1Jerx*oV~(U65|u<13hkrVog)--KKSX}1Huh{-kmK!~5Q z_Mr>XZvnoJ$ukpx7zeNpq6^Y+M|>%hZ&m_={DL(RU677D;j5WOW@2DU;2_pVG_XR1 zZmEHv-uI|i{>++1+1(RDkrF}@S`H_jPC^L1gY-bac14;sF!{P5JJPc>K+H`K@^NI=uOIrY*<%jOHoRTKf|VaomSpI>{1w zEW$3v?!3u7f&Uq6^7{~LY(%Ts>q&lli2W8Nv)>BEPKlkuw`8A8hrGW>ZHN6lJ06vl zZCzN!Q$pNqBx@%ouzF%9D?Scny~rV~aygUL7w53f$-dO!k^?TJHRR12$ zisnQ+L?h9XXt!vOXs>9WXjyb%baM32=;6`o=yELn7uMVm6sf+Qd zt5W@nRfM$b|1qzp;MY16&@s?0bv39nlWiqP=02J0*4A^ftc}Awxe)R5tRMBqkHm`> zCcNl4|Ad4GU0qkZ2_LfMTw>eMOzO6Jmn~zrdI9>Rrm<$R=CKyBJz_0mtzxZXcCY!m zwsKZt&U2-m??~&=*_hN=YCGZjBi9}~BTCd+!f%srEfMb(U##u8iKdDNo@2gWhPesxyaOStPbz z_um@h{;U0u$4Q(cTYfoV>i!d%JUg|u-Z8ZoIpad@oQ|ZrTK>kIb~6$o^kn#*L`Dqj z^XzdZ;|g*h|Mtk6;oqUQoA}PYE~6XPDatPGEs19jGQ20mS;?Eh&*G`Gi)7s^D_z&K zw)I~8@F{kS`I?)BD|9-az#l&4f);%T9ec@F$* zp6&jXQE6+&r5(M9*PW4Rsn?g$=`cp7v%LA<5#9;jS>8F`1>SYuTB60z+i~dIp^l+~ zP@hnFs3J5dG&nRSG&gim=+Mv+p^HP8hpq|T5V|MyNa%&od!fytuR}k!8j(9DcYN-| z+{)al+!?w1=Kdr1fZXM|SLd$DeI)mJwoDumJ~e!8_^&Q|Mw&#LN7_WPBON2*NPeU^ z(k(I|GBh$GGBz?XGBq+ivQK0|l20NVmp)ngbm^wj zuS&l!-BKDa>(;kh-?F|pl%HRIZTWTOPnW+^{zm!6%KKKozxu<~U#$LS^$)9myruJP zd;Qh(ucQCE^sg`f`gu$1Eg4(dZE3$HZ%g5pZt)KBviR8ejQD=>Me##xTi14|jnodV z9a%fGc24c*tn|cFtzSi0={eY}N%5;IoHf?3?qsF(1J2{@j`11yFaBcg6UMKqc((N@ zo_txsGcA{}=g(F6)dp53KZ0L9%QGXqJBO9U-{Dur`c->Y>lW1c)gW&ies!R?48J-B zzdFym(7Sac`gJsmdT;o%kd)f)UN6lsQEWk%XZI!7XrqDa?BX=G4j zcw}^Be8R8hMHWS(k!6u%@T(P(b0arIZjIb#{p!IKzsk(Z#;+pQulCA2Aa7pYio6T* zE=%#N4C`09)~`zNtA6=I6Mi)>e?k7z6u+8RxUBGuB2!dVw6f^TqKk{x6m2MawCLHQ z7m8jf+EkpD;#UWhtVsCP*(rW?zxAu1N=<26X>MsAel@Cea_K>(3rkNfJ+t(@(hEvg zm0nkRd+CPKr%InG-CX*0=?|qfWodmQ*00VhzdGSpua|G^$0Fg?AFTdp^_Q!^#jiTm z^Q%4Zt4#c=%a(#ICGj@#(s+4%VtnuT{_%rro!U0F9c%MyhwvV+18P61tz8!J2iN{i z&l=!iCce7%lK6GCO`}iCzgVf#O(cH0{M`0mZN;`@{z}Y;HL@q^@1kF({w;bMDX(Mn zc>^=DHO%F%W?pz@^fDqkE2GCpdn|o~Z!Ul_wWIkt-loLQIRA>zg@a(>wiM$U+2$8< zSuRnD}( z?Kl3Yx}@stsza(y=GzBU9mCHlRYBE)s*(1u!>htoi8EGptjek~(>7O)n{wuqK~pN_ z@0Ek?&vN@yWf@?R52ZCtVOgpJ2;JbGjG#(5j(Y&>A&ejE4M zIAh~j`HuWIYQUIz#*DbIKK$#n5!7LlG3_|+HR`aDpUZbf{5|5y5f6-bcEmHUwSM&( zV_tm<9)FAZz&_6{KY96p<(-ds@rWmnxb=uzj=1@V8;-c@}FA;hq-^~zvO3VU5MFDQbV_gHiTXay&w8G^iAj|e*05eByC{Y@U)R>6L^Wr z%(PkjJRxmG8orfwQrdZG7o=UCc7579{slYxFYQJ?bBBCFex^N`_BcPE2J+Gb+gh47 z8Qx@MlhI8$SCc7CW;EHm$sCRrH95G+k|uKJCGlSqTi@~z`McG_nk;W}R1@mA2{}ve zo8F(F{nLl0k4zt%PHO2B()UfDl|CnZzJ5M^Ui!i5OZolM^l0KYOA@+BRetg*KA-$f z;_oxl=joCBOvfJloGCx$zx02kCvucNlk+9crhl`4-#h&qh9te4;#s^seKJnE%1{Z zHTECx8UL*57rartW3x!J!OceUq5;a<4Er}bteMZZyx7TpN$g5Sl&gZ7epfpZJEXK4gS-r)sbgZ)~WHiY;IC%PCAG-eOJTuXibq~iHc7o<0tYk;D zSxz>0S*dZZIh=i{PjPPNmclA? zvU3}6;=9|N>pX1EcOEn6IggqPooCG@?Dc%H^PIWJ`8QFVH_eso}HJtxtFX_45GPsga*vIB(=QDGI^QpPf*<@~VHke@{98^vD@5H<{S3dta7Ha=V~6$tGvfP zmv0bXKZ%`a+B$8BDi3i-x}$@eg6o4XgRg?yf}4Z2!7ahM;KtyF;Ok&>@J;YT@NMu# za7=I=ap$4#sNmS(_~69ggy6VfWw0W+Be;_{q1_kU&%4g<3DyU<2M-4K26qJ;mUURx z$^FhB$ec#rqW81=i`?25J{=7M0F|(^BJTrbMSEFinOv=Bw4)zJyz?|!DW?heQUqIdn_%GgZ_yBhalwh`klu8h0}Rb`wZrBXR@2+ zS?mUJHY4wInAM%jorm*y2C%{%!1G`Q>^*xXd#&EjKBoU-9RH5nk~hWu6#Qb}8utm$ z{5NCw!PCsc?C$e`^I!9jvxS{%na^@puoZjzwqaM_4EFVH%f7y?+2=Q%r^T9?-%WGw zq_i-9GCTc^9Sh6Zk+g#SNQav;rzbn$_BQ>TKBmGcWyajg?8{z-d$VidG0yeuw0@&m z=3H%-JJ*^c+0peVXN@_6{asIRZZ<2NTg^&mtvQJuT~BmwF}FCMo3-qGdYkjLS?_#n z?r^>{cd`fS?anvm3D+}^^PZ}w+%)sF+r%H~5AlcjBYeB3gWo200sBdHCQ2ePgZ9DV z;E>oEvD;%O`B(W@1_uQT{31W!&-IHXeiFMmc5&?L*d4KRVwVx6ydidD>>|I?ALmc> zC-~$2N&XanPk(Z3kXtS*THJx`*?FKp*Pre09~>N1v)|O*zz>38p8ue~!N1qP-+#!z z$G^{iz`xi(&R^}Hfh)e?yvE$^>6U6^H=zRe~N#y&mwpK82{*?lfN}62-1RZ zV1h1ezu(!92f2Q2(9!?R|G}>bI{3f%-}!$B`N6PYKoAP@f+0ag;049O#Gt?bV=&JD zJs27^4T^#ZK|lZdU{C+oU~rHgv=5pGUHzZ^Z-X8|OMgoc30ef*f<3f)f9aU|`TIXdN{1Kk+~F|MI`_|KtDH|JwgJ7#WoLn}ZQS zslO?h5{&V`3?>Jo{V)7K{jdCwg5g0Q|I=VnFv|ZtXd7e%nL)cCC&&)6{1^P^{pb9D z`_KB%_)q&U`A_*T`WyXs{I~sg{Wttq{8#;#{rCM3{15&2f^&m2gL8tjg0qA3gA0NS zgNuVpf{TLl{K3R5`^PSfURM2jY(E>{lbD`F$0RaFtS^WLOC`o>V|#^u0TH=3h&R4P z#PMxnj_(q6e82kl>feYwwj=hKP4uxn@y9O9+AYgkmc1-zS*vBOiAEL^kL*fBvO6)! zokM@xxHP9iS3gvjJ!#3q+Umq(9?9u++%dR+8`=!rINd1UnH=&{k`y+UuW*Wc^y zm3Sk(0bYgIhd4^9SMCk*MtNgcb2r@H^D82@(#0F-b@jU0h)aPt$jkG3*t>$gyfSa3 z*Ed+jdp@rUP7f|ymcOiESz-0MWkt)1qgO;Pk3Hyf=a_rXFUY-Txv$Kf=vVx9+=G5y z?l8+8Xu0n!ccbMlw7oMecUI-T^R=;SVrycnW2<6U#MZ~o_bXU^a4T`So47laT!A2~ z53Z8vBzI9W{3BU+;Ky$9_hO~Ox!j9-Cib|$kUK7SvL-@SP29!0hX=S%`y}fqSZ%=_ z8Scq)x9V=zQf!F*$;yaVgOg()`?s*F;%4sK9?WWqQ-WpwAzbzNZ|uSHlTq{re#jUf zmgS+%6@M%$5i@~Hs);1cnYt;H`v3lyTx8euEsbiqeXtBb(xeWIa zEWAC-Fa|O*L*@NjhNn6$v9r(~ihnHHQ{g?Dh7pv72&3UST?^}Ej9q)m`cqRC#g{Z0RSW(~bcDj&I*l2rFb-u}mLPfvl)u4`eO&r`Ds<9)`W$~EY|upit9{{SiXBOrMam|1eq+rm5249^Q&*p<+5H^{<$ zBV(9n2)^WDuEN{YjG3n}F4yu%nSO%#iqi@`NO73ongxnHpUquFizBv`85cNWd$9#L zVq@`b;O#7iI|vr;1{zi)Scp#>p7^ym0eYB1{Mzu0xW$orU#hUnx*ldD1p@ftxB*7U8@A*3*r;NiV4FL-4@=NXIR^9@y|flD}GD#4keI0+^KN)&G5{< zC6GMat@x*+QU~`?4MWg-mEdz!d>8yn(fgG^d_d|6xC?A}&f5}vk4l{ZZ#6XLVTJq1 z>i3V3j?5BhQf=|5fp@lC1g*AjF^Ur!+I;Ee?0 zgKs8?qHiU@*I8Yth;7A3g`+|I5}4T<);K9rckd;*6_xfNTnit-hai6XQ3A2|#|h3t z|D!lkzE2e9$%d7T%Fn3yqVN}NR+vQ-T1-%jim!l^j((vyd!Sz`>|btJzhrUxp;r^9jrvr<#02SW?RvH*qK^b-wFwQ*mC&dY)l2>rVSAS7>&w^pZZ{clB zhS9#oxeEPVaqdF@PC#3L9q^vzu>zfxWT8zJ880w9 zQsleTnXR{Cv_*oBDD`CPssL@7ARBF^$aunOouCWaMv?Ia^JXQ2wpC<&;L!Fg{n2)c zj4z0qD3Yd}1K7*Nh<{l6qLNpT@rBbNfqX{t3Nkj~ePxQ|fi^G5SV69BmPsf+Vf|t> zD)|;BKtz#ogp-$G3MzI48EZIV3t=)8Dl(36#HPZ2Aa(>9bI5hnvJZ++Tfg2Pm2wJ8 zK=KMQCJnrjzOhfLBo&fYb-b_(H}dmh;i^ii|;Qy-MAl2T~uxxiCqQ@rbQU zsly9F>KoXL#yDax;dYp+$aut&`Vdw@mBPL^#+jbrHgtx<9xsL{mvT3{x5CamhDe|C zFuJe84mj#V;tP*~)G_eHmT|;yghyef!p=2D#$J|ZQK?H{cNp6zO20^y&k#wpuwKg$ zkyD;S=PB%BW1IsM{2P@z2G)caA{@$_sMImAPmdw`p}dZYKLLCH7$TR-YpD1o$k>6H zoboFwbqO-okny7BM^x$(WNiT}*OZUYrHYJ2h=MA=pixD}FHS7M52&xm_(jH`mY>jN zii}OHPm5pw1vFKBZJ05*m zk(n{kVvAfqWxQ_3>{aM%3B)(WpFqa@&Kn8D-`-S=l<%zsAEO%;89VdNA>}jl9Yx07 z&btXdMc-3o3@-Bo%O>;#MaJXKhY2>Lk`~C^lE|I17y5-Fb5o*uiu5mHJCL~<5k6%Z zDt-wv_a(Znj7G&Tg{AOUf(y_sitKkl6jN!6)+q8ctIT06Ez!8b^PMc!NkHAY?G>KV zGj0ck$hvV!S70r?Vg0j3?CO%PU?!nm6zQMb0gALwd%v=ZbF|=iI?UkML$Qk>;{tbY zMSRVruM_0D?Mhl8*H3ppMXs;1CP|QMh`T?`)vX$hg&| z9}}cayJ91dzQH|QaaN$zu^@ebTdhc2ccY5*0j`t*oYPU-fE#dp5sD89G8T4^P^1rV zmn$+Zc8^k|-MU9B&eP~IifyZsCvYA|=_3SbukHy7cSVdVd8HgOwsucaoR`p(6}h&% zrzq0bx{_~@cIKX`NE>pcY~cKio(<=4{5yKCBEIaNmmnQIUy*UVOB=AXLN83v0u@_; zj2GNX;8KpKqGAh>v8TIAk-3L^6zyD1qehS@<_`53$vAN+9`q zUJ0apFDQYO<3%NqvWkB|AZ2<@3B*pXD}mVWT_yM$6&pdY8GT;~#Qq;B!4IhT5Cq?% zA1T2X=*LPRe*K9ONL_uJpg+1P!BAArA&i30mEc%Z(t?0C;7XYxkhb}i5=fhnyh3ms z`kfN2M5S#&umb&6369nUke951R_Uq=Bys z%Q~Q^D$6>drzv7@?{viHYQ*YkwAax~qxHCZpD)tq!LF(^$KL20zg#=<_v6qkqFD1x9#a==?5PJzy53eVX zdUylKm-{>Vc7j~i_87o_HQ2P_#Sr;J6y?smS#*)Ju`;UZ^+p z;WNjerBKfC`Dj0=;P_IsKMdk{6*^dv{ysDW#_;S=Doo?|kD*de(>cBz6?*}1N;aXr6!&6O>KLTI4(+43 z_oHIdfAIT<(EVX1$M>RQUy%MgB=rY!?FxylL9TD1If`6oLUR?lZiHxyf?V%I2Py93 zsMH6@^*Ci~N!dWIPoe7-8H0pwP~>_Ql6nI7RP;_ou3aH1Ke(r% zQfDC7vCsy^JsrJAk!xB=>HuU+6Oy_CxweI*jzPvaA*ma1Uq&BRWULcx=JNp4JWuzRkr$b2$4PP2wHHu#?q{EXs%Lhuuc&k4aVD1IgcpQ8Ad5Mb}z=at|S zBU3Po>4ef}3ldum4_C~=D0UGXd@sC0k$WIv+Jhi-yD)w)$i0*B$%^w|6#o_seip{3 z1(_FyuT|KM*M!$7rX6~nLN-kJdc|a;Hz*QI4&SJ-4#tFUQY3C2zF9GC&|4H!gWjr` z40Np`@$B$A#k56lQzW(>zFlEmjR~(;Bo-dNLow;-or?Jty-Q*JjS1hanBUP2irg~~ z-=j!8K76mjIvo?fPceU@_baU3F=4SInBUL`6*CYOJAzY=N*#b1j6R}B95no>V({(o zV~Qj7`A@}^q4=U8G1~Csijg#*P$WJZeo`_0(5DoM*M{*s!N|GrJHf#}!}y(G`l9%q zAaUO?ekaI%<1oG_NbD_4zaq$e<}iJhAhEjeONu!LeOY0Jk_o?}nB&k_6-RvGHAU{D zhF@1$$z;NBC~^-q{HDU{CKG;3F-M{s6;?c%@Y{+x3Vla$)}Zeya&I>Lp2Er~6MkQj zd#d3N6i0mUL&dB>KT;g=!H*R~n+g9%VO5n0f1;R^&`%XsTAA=BMefmtH!G~ZGU3k@ zBgdaB64wcTp%_W?OGV;3;ja|AzZ(8paYVmSjQIGsiX-}+V#MFSR~-4>4~pD(4gaV( z^7o$U79Q|Ek#h3~Ip&0SCKNU9({jXx4M*mXWCdPEx zQ}M#aL_&%`66HIBKLq7-f=^qBG*j4J!9!=ZGv&d^zVL#h-+V-NB!N9<2C#qEZLoPe!9kOzaU;Tx=SlABh}I z{>5jGQFxoBi5#o2dzOjNCIxpOx&qGR^BL&5a305Rpi=%DIoE#ZtqN}yH<7i9FXfZ+ zQ~&wxq(^!V(DZybVZ9)h*Z(cKn zXERM+rV?PMymm@ZjpF|Ty9}8;d|e30LmoaZ1lTMu0(l(!C_X0y zg6;*BHAY?Jk=MLA98W~$J7ROm+X^^|W9*$L-v=KXso|H@~&wbME{M#pm4lZ55uk zH~CqLA4NMV{v~Lx;;%-rpWvT`c2RutnvXpNADibFC_Z(OU!?eyBfkWC@R_qwY%BOn z(SC}L&*fJrKJ}a5U-8$V0~G&Sl(sJTH=x55|2lMn;-7}negvPo%b%h6_)`8n#b1F^ zXM!J~3l#qpbfMy(j4o3A@=uL_*ik}Pq z^~MxneOAE>6&51$c&&M0k7@Z)H6CBPSoS}1-kx`z_rFGVdC|2MRi z;{SlQR{R>YjS_S~@k7D?1;qw}{~emC_?Gxcpcs|1K`;>&dqIE?7Kx3({}GKS!8laT0sil(7Ln?it37fJm=Ahwru!T$r5x`9B-A$|aX*maT;h%Zl8f+lE{;(vlpSNzY=8H)cG zx|iaAgYK>P|3UXr{Qsi+D*o5#ev1Dw`VS?La__GMQg1UAe=~Z35{R8)K*~2?3B;}kDgGDe0>%FmU8wk9p^Fs%BXqG6NF5%m1mc5- zDE_DD5+xA39;yUVpNA>_=jh=|&=y^)1Q}>GM5&idG^PYOsILUsXrKgH=t?+)`h5gF zQ}JIw&rS%DKsl{~9X!2LBCI(gpt&G=cvr#|iwGIhMM|AN}`H zu_^fPv0Ob3nqb4JXfq|KMDa5rz^{v`dm)%^Oi88^%s{cJ5bTBGdnNc*fM1uafQz|K zJz-35Y%YWx^b{rJqSS*R@$}x%Fm)Ao2A+KPdtA+2>~^z!&=bq6Fll&#&+sWjPD|9scB)dhSE} z>_a;aC|jQ`N^lYSw-TIZOsP?P`nXccEcoRpWfuJYC}kF67oyl-h+c+LW}*6Zlrjsk z{ZPtWN?BvnT`A=(rF^l^&^>_i#Wtaot+X}A-=S?FgX0g;wn`91GnHT|+D?gmg;Iw? z>>D&2ayb40%~kwDG_3dqXdZOqGxSNNv~3}{79FKjzln}ks^37zDAgO$u}bw@=$=aT zJLov2`fYT)QjLF>PEe}x|I&#{^#|xArCMw<8Rla*Depl_HT71yK&hrKOBcdPeC7}I zWF;!)K1GR&Jx*1kl)v;eB`WqfU5R!;&rqW6QT(P9KZ5WRXD|(X>?SbB`M7yK6DA8W%txB{f zx>kwyLDwnK-so*g6sIk{9quNdebEi@B*%l$r{PV{J{`Yse(2-SC^sg5nEXsORcOQR;aMWvc!cN?x~6U)4{rJRiRn*j>`ZNmF3= zMia-k1%Cia`U3kiO*7EpQ5vs7`_>w zqr~t{yF<=gzIzqN^Ay>MC4Qh1I~$#^up6iDrgISXk@{btuqUV8VMlh^VV6!5U!<^a zr->h|usf-VAEL0=r-?67V%MRED(vOSRuc+4otj!#iO~jYJtg)yO1%m`=c`RqVo#v> zfe@p;)TS%3XHn|1wi%z7^zbtwb{E=0@#UEIBCyMQhe&AcD54x1f8S!H>2`z z@Tuq8PZgi~tlgyex1*bt;1u*TC0K@juK2Rc=jj$x%}%t2om#0!kaoe$%F_`M-jGOh2#Z*6U+-~PvO!W)gvS(jh zFy46M4!s;`i{1p|4riZYFy18Nj$$ubFx7l^;s+Mvu?za7<1NNxZ}mx}3&yKtsV?tm z0^{*crhVpHOf}`$=T?jHrWtn*`2kb?vT^6*U@fI2P&g(74 zTg+0T66nhB4@L*TK#rH7-&>4#Slu#D;~ma1=K$j^MXN2w6J;M^FkTd;e!+M#mJfw2 z#^dkzwy_v5K=Cn1;%y!S_o@s33Quo#azxtIC^;~j0>`&t71 zh<6N1dBJ$p`F(f5og9;w`|hzA??jaJ!FVgs-z>&kY25oeLoUBR2`vV($th@ei>wE9 z@9$}m^?>gE_{05|@fq6O{p&5pI~&DrAZrBO2i97QcP>hs1X(laKJbM_)(*N4P!^E& zgYE-07Q^~M_rWF><6VR{w;1nYlzf8mE~R>{BMi#E@R0pegnq49K}Dtcvqm< z1&oK^J%W#c@o4vtvJW{J?<$s%VQVnn)hJ~K<6VRPX))fl#{FkEi}6+)_i>IN@6Kn| zpp+Mkhkrf(f<@Lpx=(Dh81F{(J&W;fLMb~K?`GpZN#4MCx1iV;jCU)_cR42=Wbf3mvAZsn%r?CsjI!pJN(H2>2={}23 zg7G$>?JdT`cb?6&81G(`@`Ca1Lwi6kj_*hNSd8}&N}6E2htWwend3)TLWci<@g76@ zJQ(kvD0T$n{R@4gLO-<_?`iZ`i}CQa=hH35dltoi zzqSY4TeS`)U<6);4ms^bYAM_}T@jgNEH!vRddy%>a<84BzgBKs? zcrz+xdxhiAS%$Z-#dx&cm*!fG_a*u}{K03wLa8q>-ZyBi#dzPcR1Z79?D6~WQ0fAV z_XB#i#dtrW^bugZpV0H+0*-%1SHl{PsoR%n4`4jb`7-|f@*N!ifnrB6-hYkzN_&g( zXj`vzv>0y-T3|8W-)OPLcr|Eui}A?&E7$~#7e{G3U_8q4%EcBFGW;hyjyGkMRU6Zq zT~Avx%BZUS)NRJxl{Yi-=Zw6J%)E>|w^@9APv^k+YOhDl{obUSkKHYX&$c!WdoFF} zlP!qH7O+!Gfm5E&rY`QOGv>y>n>)j4JH5xDA0FyBUVN*&DHi|GDI^p6J=!#Hz2cas zbJI(lly5u!kdp(LHSeb;luYLu)6!&_4wR<2qP$&pdEfp>lY)9Fwi@5R zdqF|Biam!qL+YoSJ#lzmNlD)Di8bG*CQfQ0&1Wc&d}fe7*AvNUlg7`oy4TrVGA8x! zk)PkaVglQW)jLxtuVQ}WnlVEo{5Nz=&2RP2#<^;zxu?=MYgvl6FHKIOI5}9iZFUyr z%P+3z*T3A^R6X7Yr^7(Fu_ZlBY+bBVJd zM$7u^30=;fII(~C{QMpj69+j%lV`T|9m+d0GXFn~6t&K-_#8GZGW0QoqAT&$jNHSgH8_a3F4JA_-eXx+SJ zr>4F3DC^jWTrNm*A5zYP}ezgKde^|-l>Nl9;vLXnN=^rWV^YrF1?cU zk^$v8_0n5AcTQK5ysv(Gp(#_kEL~djalQ0X+evfsm#yt2XUA0C<}{Gb4W(=w$ITNa z1*2QjWvoy?&mbpBD5GI8*dJEz9DUY=VfvZI=;)UYt8& zLe174>ssu>n7@U|F%GCP!9_Z{KUytE3|9Dg!FSRh-fF(>^expE^mO=4~@;)})?EYGTU zHGS#tznzD+OL1iVX3N;1Hs8Gkd$u=RUqS=25*MriI!JH|nl#Chj#LHv2 z+mkRL=vQ2?1WuR1gL`xzG^l%aUL+^0OP8$e`e!%Jp9c@_-ed4pk?ia)U9z(yH3z4f zN&CE0LjAE(Hm$c<`#eWVmsz|m2kDMU$$|`xoao?{B~Iy__*VzasL9Nh^JHf_ZhU>D zOOBk$JA6|t)W2V>OU-n9!slD{=pb3?(4*DX0p4`|P!roCR4b!s1O z!}YRN$CT@(Q(fPyMt023?9rz+SILgqdbw;`^F(Sj7`OIE<9N5?XRQr$J;tk9c@=r- z1rEypxL1Zd(m6OjDgNhm`#Y~WAI5KT#>W@E{ed$({{6&w+}$+sw~5ZM_#Ms+2FErn zJ1)(nh1tWZX&x zWvCQnIM0u%I&FFUC#Us-Wkbs2Z#_0*%GA;O%%3;oG3T5%v!^UP+}(80zC)(Aahm-+ zYS_RbTko1UcI?>rx6*#II2UiQpyd@AW_5Mv(%R2SxBl7aiQP$Kt4=#I{&QxZves<| zmCoJMDX}@&NDiivgSp#su&S&ir!ccy`PgYQ8qSMjDr$daPu8Dh^ju^|Q*v@U#7bva zm2bNNJJxbfD>-HQqO11a`>I7%rxg$Ha>#(GM~oVE#MA+c^Og@AH|Ngf%Wj)BZdi}r z116m{XYP5G1DS5i9K@ziJVD+~W1Imw>CR5i;*4H8WTn%&(NlQ^r}d9+4H`RP@_g4Z zc48{y$T%z~J#*)$Tqq;6l7l{I{G{z-@&fD|RfjcC26pr5%{2r}r;cC(17= z5ewSp%}B1d+mMBw#~0@fFFK*JWk$;`-3L~*&uQ7YO}IT^*cTXA>DR?7}A9Wpr6GH{Q5SOt`u_ zJY{S5#!OOo-lORIWe$XXkq>%{vVchk(9W3iiOPQ3EG^RC>=y%fs8 zTNnu`Y-VK772QduP>@kjQJ#^RNGFR6aE5cNCiVC1*?-&Q-g?X3tm!!mCp!JME|yfk zof!X&bhW=(&vVO7n7eYElyJ&_8=v?-B_)ReZ2s~}L(y<*Q{t5VXdo5IJ(Z%Z_pCz%(@ ztWf)+)Wn=jt=Y#?l4`GAO?=O~gp%pV{EBokWiFUrlA6xDCG*G6FPWQ~qY(4JE>q$y z>rzSjT%nDZG`F)rtYl}F^;2_>IlkA~PwbfFx+5#*pT-0*cS1ZPC0X%h#^(9N(ItP( z{u;=ko6`7q%^gN-<-qNQR&p$H7 zvfou48F6J+le>89#*_kRTg*?)^_b~7iQ%cnI`SK|(M0BXI(QEkp1Py0YPL=CGM?J8 zDiZBKpJ(l5j>ilyv*EBtev|Z^9qap+JxV)u$YpreqGiWsJ>?%VR%_9sW3$kXS$U{O zY3tTDj-f-fPFXkXScwT6B<6LDQd4{D?3Bv0Be+DXa?-R5rk`f=CLMNe{N)sbm!{0= zWPTUVs3?!-dmR(mZ9` z6fmP+J5J_J=6{m6it?<~T6yU2_}3}PwAdt#^hv%X|UDXKF+x4dn3 z$E+I0=MJx)Fm*-V@bCcxEBBwjZ1^T-iP_dIyu=;1y3mhCrg(%#Jm{yBcs7bHlzZ2H_o(=nw+_uB6KL?;k# z;;Nn1==l<{zj$_or`7A<9+at>cymGGT6RsP^Jx4+XCG&3{2t67m$f|HBakunC5+)C z{A=5Qy`rEZFGHGcy%D`LWckPu!PMb%itcwpi*H?g#q_b5Wd;u53dt2=@lMX37*QRz>vPLkTF))l;$5CQBHeQF z+?|zNhtoGUerjv~j6^+`?M6*|{RgCU+=T9TR`un5n>A~mX*<@gg^l|`&C0HMUI+T) z{M@23bZp7GkTJfEQzmYEFvj1dQ8pw;`A6=gN|M9;s~R_q`u`vJe^iK-i zEEex97Z1&H|Cqm>_fJb^ezT|+&$?T=FKHl$?2`Bv{X9ISasbTwP!^|Dl# zPGVe{h}mbRrt`GMutz1*(YQPpgw&#`EzC8Pjk~VWS6x#n@hNwc?K7(An|%8>8N2dV zljxc&I1#tW=+X*iWsb}k)@@1Uf&1zgx3(G6zh|#LXS(67Z_S@iGUT<4^So!r_uTiC z@qG?yl`*a#TWN>$h4zeYE${g@|F!q&GPWPb%Qptw@q4%aQ{cdZ|MowVhl07is+F$=C-!@Mtp>N@UV88H`(2kjGZ!v3{#qk!TWA z58SWij%^`_?>%W_YSR93P`CcKosNeOS^3DeBy7;E=BMova*d?(IqRjGFsqmOsrc>o zNjd7n@1>-sm#Uguwo6a@#DMbH)dr4KOnHV=?zq8%_^%7;6aO(k{weQ}KQ#Ueo#OdU zEdH9dZRtyc?fbKWZ3#HfXn(dbC7o^eP8&$CHiI`}Nj@0mHgoGza%IfMtgi05=LbsD`Z!$Wx9h7{^_J;DwM2BcUEq$lhw-bBNd3H3(=Sys= z@8?Y!cDPM>{2guAuGQ4-=dqujpL@lcgUoG4(}1|m(qr334JCz%{MC%B8zK~srgy75 zzm$hQS<_3MU)rRcQOO`cBAxh6ojZ4suz>i8yzSReR+;v9?=4LT z|HVf*PtZCmsWml`9lc{)YS)*wkQOC%SyGr% zzM5By3lc*~^%MNQ*0bs3b<`~tWk(44B^5S&Uf~pER9Q<++Ow#jL%3`A_`yko)jTtG zzZR34SM;6a9Cz&U` zM!(RSJLK$%m>i4hjBz;O|CBo`PVQwhW?Z&#;pH=CT)uEhFk(b7rP3ch+^=joYyI-& z>u2fDi3~sIoij;)Qm>w!i@j!jBUA3e>s4UqXBvjXOlah6>OJ4>&N(GUnrnKIi+WK~ zcNgUxS2yxn*jOIw!l*SL?Ia_~vdert+uqq#uijM#=IRLUjKaVOP*V= znmadrplYi`)dskxVI{VEav>}a}cttTS5yDI&&$M-zziCyGkTZFgfmd0}; z`IzRuX5+nLgTCY=Id<5o6;j8x&Mqv{FSkQZr%pK?k|E)pnC6X6vPjN`h#R#LmptsW zD;rh!JRy=8=Z|U3OH!i9HHS8CBw9zZzH|*yv0*ttBa!7@>jYX<==8W6yoS09Qm;wB* zkr~yl3_U$L+j=2+5O2&6wx5I5+_p1kkUonxlei}I*yUBBUQ=o83Q^Za8oyMewvA-J zDKYKczGmbPUfT84#%v&SyPDm)X2_hc3u`!cXaT#bk)mEbd-tyx(psY9joG2zJaK2< zF5?2*hw`k6Ucr(<+(f@+mz7rZBX%qk1-HN4voEe&1mT`KnQOGVZ|A|SySAU%p`zQ^ zVMnH1wB3_#IK1wn9dg{kk(_Sb1`ZtPZ9S*n6+E>r@`!C1S-BkgpK{~eK7@rz<93-P zyXNJl-OH98S4iKk_K~pf|CFc1dIzy)`EFRNePRjpZd&bBiD2l0=>He?n%7)mIryL5%XHF~Qs)EY4JbRW^TI~g zV7J}9_7bm^1=$TQFgsy)n$iUgtU9Zy*ShMiy*wWzaiSa>t7rZ;0Cv^mv>CJKtb^c= z#(KSi%Zkoy4>eyk-s2~2Dsd|9Yl&3t>da|>Z*J_I#I|bQX#8}XC8u_+`y+ceWN`<+ zg#IKw(QswllkD9Fge3Tt;}jRzsw#1^%iSLjn?|R0Diw zz?w_0a?-1%?PgA#^{m=+?%Iv)+`*$J`MZ@Ra-#jc%mcd_`m@GIH@m2vUy>e6kk2lO ztPApajaAx?p}(5VJE=o^Z3+!%jL=DJM-`R#%_-+rh5cV5HWjSOEy!n{6?R!q)}NJR z_S#fYb-~<6C#N}gbBlIre1j8>k4`IGIe+d2RaML*r}!frXM|r_85{0=wSLwmr?H}v zd#C4(-)HvvBbZZ8K4Ohc$8;tOQrP8i_Sl7Rb}Pbiw`?& z@!9U|VCmA}N_W#~@zX0OPp*{6px9$QW2T>IL9E|p4LshY<<}L8*;!@)Q@`yl(yx`5 z%^h;!q=Ob+Hhuc#3m08BBmUohQwC3+P&cu5vM%Wx?d$iHs1-%AF+O+Gt*N73bc=FQNJlMEPDZPoko@`rcP`vY;d`kdk z=+1L9d7}`3HcnJ3{66?&8as1sdkkd*dBggMn*6P9}yO%pMgw5L9 zdK<&q*d~pxi!DxBv#}FHx_wSw+Gg7#*^F@5E|TpuX?gz6su{yBI6O48ZoO<)Th_~F zEo?e*@J{O;8*9I)^)7KAY@Ar(_CMJ8O`W#S+$|F)qd~i8Vev^X(JmNq{tqo9efyyP zE}DuuXIXi(R!!PY6n_x|=7)w@%=S zJ#~WP$=4`@P20xnxht4x%bCfOG}VThX3v$p5lxuho|JsC23_(Mk$g3_{5+Y~Ud>Zl zQF5tyr{=a4!|R%fE-PPj4ES@_0m>DyU8C`ks{zI27bY6*f-*?5>u`Bj<-i}wu zz9ZzxreXVJxf4g9+_s^V+SB_UnUb3AWVcrAG);+h=W&jDk?sal&l^|KL!V&YKlL1S zA?>Zj4V@^Ncj>#gtpacG1cO<>G^b>K|J2kIeeu@9ouw^(mZmL{>W0!bwRYL)oO(G= zbVA=Xl(ZcOP;ZHt8>7`mBW?}VL@y@tcDd*ax+WnfcVud*b_lKg)?gk&TJND^awDrO zCAvi2+aYq|5wp}Vn%huL#3wR$o}24Vt%y$FZY66?Lpe(3rwjI$bUKZB4z1;yOS|$Cz-btTglkvf63TZ)^kkXd8U4@I5o0crb_0AeI@GZPS&yh zFZuc8?>ly$o2y2SJal)=BW-Y6q7UMJT|>Q4svp(sk;;?HVjAe0Uf;GvX8S%$Y~N5r z6dMmpZ3+@$ZKR8;OH*R2atCm`HO394?>H%ojjvBVQR-5gnugoF_SJ6?8xcs8}uBw)6?1Wb#=8o+)LXk^nMrx^Z!kG2OeIs(%INxc?nXUx$E-&N_oX!Wc@Cqp~Q^V?&+{ATMsI1SLZy= zu?^*+UhvW1X=ajjExyDaSi7)&;~~ce{VYDEV>32+NZiZUMhbCf)vYMaPE0M&8TG+Rv$Jyv%8*N-ikFH$2zn6>{ZY^u*LY5o@ zNUV_Yi}ZsHU8_F%yD+uE5)s_msiEZTm@41)XVQvf2}OFjUG&yBxejiCjEQx8MT2$2 zQ)kU^SP0jtZFcd{VP!Lp8O2y@|HZ6;>b~fljL&z>hHMqSGm*4gE-#IxNp z2C$7o!#y?>tPh2iH)JQLe8`X<-3AZtCXY1dL?St<6)}9Sz#-Uv~JOirp0;fEa{~aek{{7yc zr+vP6{CipZa=sheD!r#el*aMy;)o~~<%f}Tnwc+#UfjRd zndWnM>fHK4y>w*tAo)&=`DIolm;VMv+S`(56Hy)MZ+k-QiImeM>n^$Z$JWiM zCnVpsT@!Ya^{I7bt~VM>vDyL2!C0#K{%J3^ojYu^gZRJ>c6^PI*MyX;NwsfhN3FN( z+#e;XZCgdXm7X5+`({+&rCkOUcPh=uYu%xBMo#NTrqYdG=Cm2qE355@R(oW1j6V>6 z+mYc$UA@TlnCxx89y+tGylPW9pb~yShr&x%7*S4IA$lx1D88$_)-4Dok0$ zJS63u^*ck^_#xgYrP3T?4-T=Ygm23|3x`Lq+;Ducvu^9p&Z@ZY{OaDi_4X5iZ>P!L5cXT2;M-1i>Nl->&#n9Rw(raQ8Q)ICOLFRxa9oX;yj?Gq#I!lOEumyS zq>rL@Q$0H+vHi6_YTx;pddVeM)2~TQ?_v7)GVHL!-fLdjUUTE$&)v((WG}7{cG!FE zXY99?@3m%U7TQ?d-Vk|u^I^w*DBMHFOqcB-oK_9&0x@TDPN%C)#;H z+_4XZbD`Vr0er`4g*`qY4e4X;s%3jOEX$FOb;k$69j61I-^k~sZ?z#rd8E-<)WCyt zj(Lgl_oVz0sd3qOMb?=PXygI5H+Vc~+0@~)i+MQckee4@F~dGL*KI^WJ zHYT(Hp_mdv5)z6DgpfiBNeJNuVn|4#q(CU4CiK7y-mC9-X70UvS6a#9egEJ8`JT^a zRq(wtXU?2CbNUhZXIJu&aU~d?ukR{6GlpkKmVv2LA7lM8f}^7XSah!|@Qb1^1F*wh zG1koQCA=H0!leZLA|{$yJR!Fv|ENLHm{0Qi0rt&-eOpv~?RDHe3qK&-+^*e?dHzU$QP!=wFk ze3<>4BK1B@W{V0V^@}Xbt1kG;|-tZQOtw13vTCEZe#X)`QL+Y1pISw zwmhGMXMc@neaQ3wd8YUFfV=ze4)|{GN#_rP?)M?jBQFZ?BZXqNEGgxkMwa{o-b=@O z-m>I#Gi>asfTzZ0{JdpJ=Jzkq#Fy&x>P;)Xiwo@z_eXQ?XM^7~dziKHb?NUT%^7Y{ z>mtP|Jl)7xT4qDds)_Q-q=ZdHX()EbEV;Q^CDM?;IVGonSu9xzrl1XjamMJF=M&;% z;>Lk%%mV?hjpeBKUY76)oo1eT+uV7M2bhy{X_`z-vwk4X7!~t;Vw^E<0@I*|Y6s?d zUyAgTq_6WmE<_I02T~f{fl<8nRvBFYFhxSPCE=EABq(c=ZT`#J)jj=1=lt5azk63b zf(aVdrZmR1WwV!dtzNUEVrTnvldb13U%tPkqS%r~#~pkhV@q|?yF4DpNXoEL)KsF% zYICQF>qh)ecPIBj zA=N*Jj+a~Ttd?_h6$_K^kRoMks}!O9`+H(6edfp4 z9i1%P-(i>EN3>&vd?x#v`vZ0}Iy`hmv6 zx-D&WqY3`3@$+}yVNCw1f82OW%apBqWq0=&ec0Ik4qN_;w&QJgwvCtAI`;Rk-m_=b z>N9q)!JM?WZvaNY?<{6ovk;r&G`WdgecnaR-w$rAE5A@aaj>ekwu+rRDMhSs*7QU? z&z|~8XJ7%%18s}UBriyBHbT|kxsgmk%$iP zmpzZMa7Ll)$62ea2BZ0j-;7)tcwPT*98a>&FTa$3`d>sdl3yO^@pr&mBGphpeOE+M zT9qOOoEDAPKnepw8VSH4N;i0S z?TvYb1)Ij?@ATzc+f1#+`CF-Jqd}SL3;dKy6`Dx1lom7N^V=IPJahh8rPaqvkGk($ z%-sLPUgeNS-Miw>qM-OKC6u6&uf6ty1z>Rwb>vyCc^yOU?=Jw5zrY3Rr&BHA0*WCg zYy^7w{T2lk;ihackQsuHmtOqMML|dYo4lD`(1nUG1SR|rFs4_W7lpk^QKC3LRaz7p zf2(;s{?Fm_^7?5DPaQhlP8)WY9#8u!_{2Fw{r(8hevMz-Ed4th!2J2f` z$(fNF73`m%J`IOrEIC>E!NnmdVap-c_O(MaAcNC2@q7 zc~Y9q+Nu^fb%xY}B2lL$Dv#HPOfvpzQ7cn<(!Ze3n@G-7GVm7#^czCgziobOOWg<4 z*Q`lD=>FaUyg-HFFFF`Rj@KfX@twk$wg7Gw*Uc3uYe65T?9xP4^r_0S_C|L=5u;1c{tDM(dA(~ z3f_Ka*z&)?TU`Bg&WBjL2JB|vV)6Ddd)crCye1A&0f^D^^3@Wr`AQpW)^nN5 zPYs55$QXsNDr8U3P`E>j8( zEQIo)f?}nVnaWijUc_pYyGVKvaZHsC8?8g~DW_>e+h>u$<{x zF$nJM3mkmnJ#^0t`458Vq9j255*CmKwKKJVSg1QoDj)pn@*&;X3&0^gS$aHmaq{6m zaAxu;bJEK?PA6tj)OJ^X5oM1>@cX!OflYcQ8b^fR!lB`j?y&`Sh-7rq>q4+cF;rtV zOUUCoZ4m{10(EI)h}5r;LYx7KqgBmQwY5{t){(RTXJ?+HxTHHT-&I=FmCJ^Ttz8HE zd(Uq#FB&>ISzbO-S2salg-+0k`7+h{kQr~Za#cW0&4Qwn&6OyRA*s_5kTzl!(^ZWo z1{iX?ib`GidEF(&jyx!8Lq+B7=lAv>?6MXQ{g%%}^G(*$eCod6j-P~HKpyGE=mpoj z`OyM6-UyHNWeZ?d@++RnV4q=~xzxZ?SCd>(6@6SX+^?Mt1S`4_dxQJ1v&^Ifxx9ga zhx-ws7Kw^4nazogeCsTL$lLuVO-0i9AX)KM-oY_3*UP;q4E+WHTR=I_`yiifN8YsR?|S`4(v{~^TBLy9mj9%@4y z#ECb*BIH893D@yG+>wjod=c|(#sb`jqtuoss>Y|sF2D=r7t|{A#Z*!~JvI*EVV?8x z6WopEe$d3`_pU99Cgzv5)4d5;uh~lzYBkTOyb>X5FR0)GZrDd)Tlkl9w*bM&jxd{r zElM;}K}-l|EhyavvmRL;*yXE^o-@{A>xXe+m2G0$84x!~#)a-*)s;wTpvK8eVO^M7 z$;Q2?QNfd80Q2QT>S+K3K@@oHY1^Vj`)L<0+1iAa!-C0C{4_|hv=9$93tR zs{EJ2^7eCk1`auJVVs<-sFnzV;Su)Inr@N3cBHYbbrGe7~WKUEd{tA-=M|uqE>6 zkGa?JZ$zW|S}pLDpQc(16&#zlVoS;D`4jP3;F>iYSGlmi7jSzzmb=sWUIvS(@*Rx20{VS=zpTXz={@y0YBt{PM~A zhE?VH*|}wkEG5RD!Ook2e{S)lJB02>Zswmg%fDgHtDAQwX#;LM_leWb_aSTvkQFs+ z#zIq`5@w9e^UvC+STVjm@5F@1OpZrLQm5%h;2X=V3&>1aQyGR-t6W__g1E@6_{r_F zF<%1=7uWjFX-5!#%`Weq;AcSW!OI#>yesJV+6JX^jeUNcGzd*_cwUrnx&0xIlMG(H zh~Ix1mO!JqQq7RokpSI8u|)lJ4IvNZ(!q6o%j=f5vczhy#T(75Yr<;_GV>h?1szPX zS2y?Nk(YoPNE-+5n0B4j)_A6?^Nf}@|1x`qqq(uYs4T0b$$g@DsM0!AvZ;3nfwKm#Ki<#l z@x}9+wctXAxmu(T4A|3M_1hNIBqb{4=YB$`PtJpj)wmD%eNCRhxs&rcxfF=(eE~?T zm(QC1h8vs@cI9fytaM$6`##<5S9f%GyT1!fwpQH_zV{}>k1>B4$yb@$bu_DDw-2Zw zpHdlb|5;EO$`9E#wRY@Ftw|m(?HC;G$xJu>L4I)-q)vA9nLzod4%oN%Y;D_G+tQd* zVx8#d91O1fT&~o?&a$6t{^vb&VEQ)YbFX&4S%{M^#HAe)F$<_=%OaCQ3d5`!QA?e{ zq1n;$f9qkqF26`}|569zok6(P3yKW&Q-%`v)!x9;xhn>~6j}>_T$l}AT}d2Ku3cW| zFV;$GyjQuMiR_xjSyq$y#P@Wte9dmFx!+?sVcDl1Pl|uS8v^CL8Eyp(R09H0;#qp{ zvj+x{)@=Ns7&4Q#vI;qE798wi_s?~JycNLA9d;c~Kst5ulpYdx1wtP$2fKxiQ8@FE zKgDcNO)#5ZEB>F*#c&lNQ`HB002;Ga&Y^n(FIvQ1WCR)h>{v78kXj}AA9H|}UpU`% zrTeF|Ag;DnXQT|qGuB+y)LD?2STK75J05v_?h@p)&8CFpvlTjIry#3|d|PkrPL*(s ziYCWSDe8|a!Dian*1fB)W?Pr4tFc(9H~Bd^6}GnXhlcmJ9oQB~YMHWkn1l4OB-9k~ zrH##*X+`~E^jh^yN6MntbV8;2v8qzdGbj04v*@<)b_7N(yv~<9xK|nlre9qM0?nT) zKRpi^;-0|T$rqMN(Ev(KhtmMSZe7P)jUHZT`jZDqgp_knH#hlu#N2c{TH{VP_t~Og z`1nF|+g7iJtR_D)-`sRQK9-^s-&Y37NTh=+{YnFIRDA6~oRHMmpRoB+lqlP=Hsx#| zV0?oc2Mp(wUYvG22gG(@TZ%U*{}i-GaP$KIDh{Yh*P^fxB_vCtODkIhMt+fkVuvVn zD=ml_d4FBZBN&a|i0RZMiG*ujP9$dHFFnFuCR99thO3QpnAswS&l)Vy`9 zf_U;v&@7|RY&P?ERIv&z_0?8*n2M{Q%WBg?xXn&7MivyS9fs+LB`3)*yjuRNH#KU! zP{Fn0$KtpKX9A50%Wj$O=hF}XWh%y{q{Q8c59MAQTmVpMfP&ujANkk50pxNm&dYy6 zuQ5|~Qhh$yX&_>f(iwI^rny>xk&q-W#JlOtiaUv5@Wm^DFy&VAI;y%GYR;9Ro>6{N zabu#_t)V}hDju{uD|)U4aSkzUw_L~;qIT(v-&u$r?u@L^Im_g2^MRz@U*t#PcQ^H^Jn*U{5`lG7JYGj}nP71nnkqXKC1C(svKU+w_)^{ePs40=PB>~?! zm&+>w&XK`BiovwPe)TudpCp~)@uo5O{Sp5Aalj<>P=EX$!*zeUDT0h@*+fAVQQYc) z7*s{F<58*X&~#0CW6N3Nlz6en+2S1N%*knPZ?5iNvu;RA>uD@*Uh2Q3xx9P0?fl`P z{cUn$NnS}|dw#a1VCU-9J25r%e83`AV{YQo92pi%kZyWU9U~NA(PrmGQ(zpcqT-lR z$&!e;rkLOuV}C>Y8DkxLQ*E(><+aY`?R9m{?M>ySU9q;bt*y=tA+^g8?#fpB*94Xs zmsH01hjdif2dYbQ%{gT`c~%aX#hhDG)l?OW2D=0w$BlM&^>#|37@~#?aXaiK6?0)L zw$;?_>TcU;vL(3ko&4;ay{W64SwjX|q_npE!$aq{*(!2!@~2jqQnnpvTjA^*@MAO2 z&zM*rV%U*s8>kc(k0c%%XC!sWNG(Z1(&EiI<^A@el*Ei2Yrp;Fu+|W3iFBxUMQB^7 zwZy%@cZA?o&pG*lN-2wZ{u@O{i#nr&Mt*;%eC3j4QF~nJP@j+zg_dp$i?QcYmwF|> zN^t|pH_mf97QnAs9!Fe=QK*kYR*&9Yb+)aL&;!`{ zd@FMNK%aAkwvWmdUV+xagCcr-}$7 zHsi*g{~YnRuqn@LZK&Y=!RmY&5?*3Qo(Y_5E3l(In52yTk+5Z|5;`4g+GyR0Lh&J} zc6^c*Yq@|PI`3T4b6h-ke>TTHL31)x7eTB%Xc(Dlh*_tAYB)P^WVtXvg#3^4tFLzU z_4V5e(vs4p6Xhk;t-?4OM0+{?k>%X zq8_is0s0@&Y;}53P%43eqyW7l;~RLU|yrT#5SlUkzH;SsD@?8l5P5d#qgyVhw?rSyT!xDI*!B;7%F78th!R zDBxa9Vy}ww^OeDNksd*+&;G@LES;;C7PxFt&A@`?_^yoinMluW+cV)D>?_-`?D{rJ7xLmLn8tyA`YICwIc>XRU2Lx1a8lbd33; zGN$TAgZCuXN*PB=eNNWBJ>3@XD(xC7>?=)j)~&QwZ|RagthK^Vv~#k4RfYWNlcA2Y zoc-sLP1FM?xFIUU&^>2_r-B;hsiLtWLJ+juCim@g?%S8tyK11C7hY6KyQ17hbB4DF!c{ zjfFd;TNLBgcikT#(Gh=mE~MX8fWZI?z|&VbDX)+JqvThIe}7A&_z`3uQ~U_^Ev!S3 zWU+%sgH-S*r5WU2`+bc0mr~C&ZwZUusMOp9*-oif&5dbLVUT9t!TZ$HRP8BAbp+`} zmxvBAIHf)j1=8qw=RLghxkPalA)I!JzUZA8kt2YaM3ls3{1wB@UqI^t($9HpD=1Qr zC4ERaVXO^TCJ#OMWimG%#wYJ^N_)tT2*Y(kviN?ESi6FIif{0Rdo_H1nL|>86^^QF zF!;63ShRhc1rx!chf%kSqw0owZ>PIV%SZ!F@ZO-`RQJxHAT$i=9?1^r8KiS4dxxBH z=Jzae?IMT@=R}?pMx)h|EYZr_7{1|%Z zMSQCezxc{jYXny`w6)#NyKbh$pL85G#b)FbIvnlV8p~C)>@J$o9TC9MX> z0IG_j7MfXR2Q>wk4ymA=dW9DYLvXbe;{L$X8$1-WmZ#vtYYwsss@M7nSPDC$9yW_~kc^1GYE z3JFP%56NqfRw>Y4scno03*YqK3&zL189W7*hcvqr-{}PP?U%h@!CDE9k2t-8hwxZ6 zuleoZIv%2+P_$Ej@i~T@^AVzDai4iFW0*Kf_Szq*aHx#s)bG6q6Y#h>e-UGG4aWhm>*cKLaa)mW2_;190> zXZUq02jPs8Ec71a7+7XdPoK-z-sRtnXpm`?VjRq;vfyo_|;E z=@>2g(KFqP9}5pKOPlxsnkl@s?!P?EQpGrRA3Z~NWamtzGB0j%7K9RJ%G`4rrLw<2 z`MdyT5=~O$%0hy}qEP`{iaaSlqQU0=m6?>f_^eD`?3?49=j6W!`4d-bmf1q8hOr95 zhmnH2*?UtTJDo>LdYg*t^QYM-pGXm}-%ychPc7O@xK{A;F4geJ;-Go33-m;@%Yhyt zwE#!Y3nU3?LcZ%`Kt^K(L7un^dk)+(s(ug|WF~V8TSJKbuRl-T!IoQ3=1Zbw)(lRy7>o#Cg zI)?BrWljT2!<@jNnso$bKt}KoqdB|1%Tbu~laTh%0jHt$)YB!o`|)*3zsD9lQs(TJg93G$->6dWz!f_^iK zIS@HqmBD3TtI&KcTup`3Laiqf$Bl(|t#jo4y}GuceSGEA$f5Sygs%LG`XfKuIL^kU zBks%dQq7q~;l`f&wt;PVIeB?m&8a!Nx*V7a>xl*}e~7v0ra@_6L6wn8lid%yXaNp~ zvy0uBBEL@%2d21ESf%_H>SWM4<@HACj3c&61(hnl8Ce;#$&COkm1W8=u!}qF0v2|m zy^iKZ%??98*H1|gR5IJ*MCO2(N~h?B9Zvs*zQPJutbfvkZE(`jT-n&|Y^kX4K~`ID zp0&2BqJGC(p(c)W!Q>2eajZjr0&7J63!jT*chn@Id}?%ZIsJ_6V#I*U*UvYvhyj0S z;n}fH*xPBHw6pW-XJlEXMSDRdot*3y(BT8YSO z_!|P$pI}Qe+_AJI+fj1PNcV=_7Jr8+yez~DC7Bb1F(7fmHZ*AtcdtHgthu7Tmr1>Z zua$cXSWHP}=xxU%!hiNeROCBzCH%N zBI*1=7gdaQmN_aZXBDtWMH=$GVR1HBG`jGmzLyk8Z9bu@j&!jaQd!43ptACF0KJ#| z!n$*yI{=`2kJ<}bhwsGO93to&V-S7Ei~9)11qHdfkv;%9|4u)eM!`PDBpp#IlU~PN z2q6mcHc3i|z(VNZ+>x_Psb!;sS9pzG5K)*hIy#o){(K>rNze7Nx6B<}7K*RiYxF)P z6XHG}gHyg6%mo~$kT5;mMTd*H*;k$ynpb(Zub7SV7ra_oEE@mcYuIji-D?%3d%D3v z=Hj384Za3oU%vt${yG(O?wA9!Ad8nqD0=olt~jFanTKIBasxjk9_#IA`oYCEvm0QD zgAqzK7;(dtml18F;_H*vkynqyjb7e-Z0%0f9E%>>A(!tNMzf{O!3}f@S_+i=mfa?d zeJW~Jqs<)f4A`afF?Krnm*K_WEXBfE%PgK^1TzVUjg8aziFAHam(%2^7_m50W6DA+ z&B~0XD0yA7D{D;&$6Cy!@z$ICV|?$++oPW|xMYqxcFZm+G~-sv12bvi~z9ZTEx4cSU)<(vMwL%}`!w^B?M|If_z&k_ z~07&k!+kJ(qBYvXipl_T-377*xl!H5aO7|0> zUx&J*)RP&WZ+bp|FZNnpFP)v!&#gB~F0lG?wGOmm?sWTx;~AZ8P{bUE8K%EFJ_;J9 zb3U4XD-880c5#-HoVFl7K>G#}M{D7I+=#R(JdCewV zKX^N5OM*E$#>O1G za#l`<9YmG$9yp7^5;vBMcHWANp~F`+#Y`_A##g3uE@Ta-!xz#S;PL;VUsKQGc7;;B4Wh!>-F zpJoLID6_#+{*SozFBh>4jbF>42I|>u2IqiGQiab#0?D1u_Vw$W?dz`c#Vq`%nTxxOX`;E5W(bV=EJkJ1?E8A%Wmm8?*tmrPMA8dB{wb6uka;hAo@o(o;N@=UQ zrorX%#|zhV?!pfRBfEF4YAGJAbtL!E6i)q*|F*n^O38<@7o&!W~Y)U zJ*NSKIGlYEpeXz!jO$^!6zMX*CKjb$0?J{~>qR7*_@H=*Cb7PDU9Xo3p~O9pfY0g7 za3yGfauX00HY+0B!o!7CA%toVkXmD@*9Jd%A--OIa1>4Ex|<41sY*@NFDXmj`8=;brhBK^V4i#x#i*yIGYEA)^!=3^tp3*hRSS_!V zqZK?6i#0g7kMGAtROdupbloJkVF)}c^$&pOIW>B|BAyLDl3yyawx8o0(NqJbSjxh_ z&aD9hV})At1*TatR#=Gs)V-gBf(tbigf2tu^-^nk)KW)SXq3nqM>QC#0h59nFud3t zh(XkVA(UJfSQ3rGFMVh6N-#x|ln>&ViyNYe5Y(sOG#H=;0coe>boKShM^d%=dIhc$ z)Ytnx%PKDGIv@({4WHx%??gcYP7fkSeO6At5_+VY6{K;xoPL^Gm(!2gd2HsXhxGtF zf|$%cBvy+Oe}DtxA@`F`VSf-YquTj+tQPzMIAEqt%|Rjc<7KetZK-PRE9}aVe+SSN z`_q(So8kARYH3p6zMY~3JR@kvUhi>cK822F!u%Ghy%$YKTZyfVL%nig)(WEk86;dy zq&ned)(50t6_y&4RvZ~!AD>edT@zRnU6q?s9Ti!Sk#FqIjkXmQ*`jhI5Tw2=(myVc z-I*Mh)f5qzTX6JfVNPshQ}R3yJ9{c(MVdQYt;_o}vz=^zdyhRYS3X92j}KdiXDPp(qVwiEk)*An{#10PB`hq{ zSP*74MTVjFl2V5$BCNZ0IeR)U_cHE5G@SS&@*Plb$*>hu(HwruVDF`x_VBc-Hd*e} zfJwg$%vJGu-p-fI;=Wn&fi8nwQv{zOXjb))s6(+L-A26TPE|HQ zg7H?Y_ZHxvtWFeXD8Um_=@hOsDYKItAQ08of+Renc#DCSVB*A+u)MYSsm0@ z1lAS%+%V5xQSOMqm$3XU1QwTF&6fy+S@S~pQp;yAh%QbqkqT{s|1X9*=kUpe$Td}q zAwl@ig0%aT7FZ(s!*|YnA|)bz4cZ0qQDG8QGiH_OEtp3+-%+eMk><{+H7hq)m-bKY zo2tt**W3HYdo78T_03gvEeFVx4o^Q!QSU`ehRS(F~1%$)_V5IbPrHzMd8gr^7(%9Zy7!YbMj4TVaWk*bnw5&JP zCU#Poh`k^-#8ei}>b8X$lbypJdRdmlvht(y8bcGa+Xkx3mB^9s6vYcns0W9?bG2MsR%QI9`+& zpPHQ8(LFfmIL@AypD8X2X$u{6v1M(fjB%Sddld=AtyEe&{#dkkMq%z z@@Oyhbf1Qs==aAqUlRoGAEVtO93L+xbbOUn$*|P5Xl6oW-z` zzH9dalh(?|G*|##+bnW-@=O({ty ztOnr~E6bPJ@-lPVONs}W)Mjh#v1FQa6y$Lr(^H!DB@(L(3QBl=I+Wny`;*CBfEjj6 ze*v+@Le#|17ugliJ(w^I?CQ{C28U&tvuXt<=H(N{mF)~fHNhH7W?qqhSwL*$((uUG z)e+q`zNaegN&-~8MJhBxQo`I#z}$1A6urr&2fQY>ObK3_O+)TqWu$<^#4A30Wv|ew z611Gf^OjXg)_KD#0?7`luARg&sR}wH2t;C(EMDGc&rhf1@uVESJ(u>|?@zw82@5pS8z5Viz4sIz~E~b7+Xp=PN?+GAf6vMUgyzN=J*< zWKhq0-0Z`|Y3N>+UvgR*P#B|>kJs&VhT!kNNqPVl0zmIaY>;Bi6bdAsqlLi*aU$i{ zIJ#8DSf!gbj?3?_!?3@#l!=JA@`fX!B1SG^ZypOk0Ngm6wtwe4%hZ^-f2v_}*~#+l zxR6PHM6DgjdolP0)6NM1LQV=L6^*WQcGb2or&u49Lf*cP^~gWB5AgUNva8S-y4W0~ z5zHOwBPeUHdJRTl-N;LrJs|Kv{c%5*IssQ4{46BddPY=s5B3QE{Wh0I5A(J&O@~2_}wYe!|E8fY2>gir8LAV;xdUB-u|~b$&gXQW&2cNXc`a(TKM}b#K49<{Fc_`` zG!*p#HG^b*alI@O5~4ZtX`uNJsBpt952r7PRCo>n-%;?rp|h8=pI-?ib)WlA1J*x8 z_}7j_Kt}#xzF+?#N|ZR=0OJuU`pK_r7`S!q7-xHZ>hPJ$mSz zP~9Zral}h}9X?4%-rgKlj^@`T45mu6xnuc^Ge%Shz%F(|XN{eak`b4iYXe^gonm~#6D$SfIra}khS=QOLYsxHCU444pHqbQe{{!{rFs+{CC9*ywEb;>(q=fOgVeaYYpHO&va=*SPc*uuavycb(bb!4%uiKXG@Cl_4Z|%ATLDlX( z;irX9Q}xpNyyxyy+_P8cPl2TRWpjO;F!fB6PnP1C!4rLseD9kZyv7tcjk69FXp7Ek z2ITrkI!&tG6V)z!oF|^*XqCdWCNi2(=S24yecKqlbcZxY6a>ROWjMEGn1|OCfRL?d zdpIddPgzA0E4NfgWoH!IF5I>Iu&wxv7xs^JcaQFuB0K!eyF1Q2bm+{E-Ddv|+u+gh z4I9QU9?@1WuOk9+M61#uUL&xJ3>Bddfl|Mqp@?as3zGoHh9%H_wHg<@}E|WoVjk@sQv7- zvA2Pk@81}Q<};&dgV|tS(f@Sc$==)0)I0)BZ+}R9j6up^hG0BLejM-a4u<=GFmTxa zVE>_jL;WZE5BVP?IG3|K>4W$0mWK$(;(g-%$aFM2ey0mx7M6z?q2=!Kbo_w>*=1HU~azX0n^gxtsObyI{s)rX>IX}w{B>na<` z%ukaHuf64=hS~2D9e^?wJxF`~7QX+Jg`N>~JFkvmQ05nVo<6_6qYe8z&t8j8dC9-@ z%;4E%C18>^S)EH=E8zuNw{nu0SsY=(`E*zsM*-cWq_~6>4PUjqQf@Ntj|K5|h~#+> zOd4gVaDdOi7kwjaLa;84F)3h4E%B%a(-( zmr&M$wnYk0{1pB_&@SkmjtR4}{HF$Q4fxI=vka0UgxxFmNw4DBG@OkGLFd7EmZ^Q` z*bqo5h~Tu& zYiDS$pA%2x#YzXJ7gK)Y@n|Vm80nD*n(&x9inb&AX4|Sf~XsxsgixUvb-belQ zE~(A%qF9amhePJei%`#9S7JTOM9Zs_cRcclD59>yO0_bG!gMN@XT2;_Jx52qU{5I8q^_b5PA|+*rDc}&9ntaGm%S4 zTT+W+({dw9qnk?aiH^F|UU&b}jKavuw)i{kbaYn8?L7JtP$AQeFLTS_(?1i^3#*tPm7E@e~P^D^gbt}ilUczTA#B+ zlS=!q+SYv&(~dyioy_V&cQQ|3x6!GUvpS#CTtS%YHPEi;;pij&W=>B<7Q`c4^i8r5 z=;TsDf2c&!{7teLi^qJw$J*ep@bs8c7z#yEkY$aVWZAE^<}SjVPmPVb5e2wVVPR*mn*!Nm!5=$6Extd04e34rqCtf z#e{uU>O1;tzFtN6_u$88eMfeezWSODQ&iwFuWSASkArf=oC1%GC1E|!a+>qY(kKp8 zt=7C^Nm{|Dd5T_1>=*oeD|L=izn0%X9BMC!8(~Zw-yz?IXX%clHMHsX)jduVP#g3#UOR#zi{y z+;~hD=m^jUWTKhhq60F;e6M}A*gNo%gi#-SM2o#Evd$NK$JhqY&qW+>bS60N)yOrr z!4r7}!A=RK;el`PIRoFn5cJ_W)t(3ry(gSs-0c2sDRl|GWlA*7C>-B)30-*=4M#@C zr{;vyC~oeiv=~55F9FBd@%nP;rk94mxj`?Qf(p5;4XzxGN#i z`nIq>;|PqOC?8qpfMaHbFFz9Vn*qMw!Whs)vlP9TJq5)#@LPR4#h3Z*#`)QY=OFDX z&yhOAl^K=}EhgXqJ_iT5Rzp&=w<3x|i+TQv_h;!cQijlF?i=-?%eXPXq1|nzhMugW^e>iElA%s)Myk} z9&8~frNYA!JIWl**=b2R#sagWqOrjcQkYzjmztLw7b7(nWEEQ@l2X&ts%lGqVTp~e zNK0N8Z48eBTZHVH$Ub4YhW@WG*I-RgXLWXZaZ+LKfUT?9u(T?zth~6SFeQO~Qdv-5 z8Jm`wo8H`9_1B`rlD!QcaY(dN3E+b-bz|>8(1`Lqm4^&|pVyNl9)_Nl6a- zWOZNf=*dx&7+ z(Y@-pl@%MNEA+_aGp=tf@?`{n*PAbdDmX_uvRkgKMBoqP5q?wo5W6HabIrLS&yk(Axn3%|@*x0B69KvX0Lt(z*TgIr!*x1M@qnr_B z3yhCtjq(EprFb%6Vj$QNWt}V**?S6;x zJa~<#GEqiDOt`7Z@K^b#F}3vstH9Gk*dK5h51zELBpSN39|&*x2}wC2(z4>Tg3^ZM zxa9Q4352++B`{|Ms@QL2b*U<%z57XLFXq6?=Q$R zRyiCE4fvRHa!e+R#UwQk+S`YQ+U&>!c}!@Mq^4g#D5z#@R`DpDR&UBOC+w>Wv$(y=>Jg!ixdBnv3Ugm#Z+D&)nVKD>>V5Yg3?F#rMmO z{o2aURuCk&3MwA_*_GG*`^w8+H`G63D|(nBaLEVrYiXsS(~qp$RF;>bRk$vP$Ds>J zD_?*8b>|!Sb|<)Vz^@kFRr z^y!Ymrh1pFF0(QsGb}UC0wT|eKb4SHRW`k?pd>bWPc1b`Vrgb`YQ0pEGhIE{-aeF) zln`aCvs!Jb73qPQNjU`!{i@>Am#iO*Ge*ZepAc`19p|vloI;nuLcp4bhzaD^Q{vO& zU|k%jmIMvdTs@@cD1fFW#dAPw({z9qMQ;F{QO4&J;*4<|&T00rbUkn!4trP*YRH3| zR#ACxGfEI5-Zj#SQY)vy<0)xPQ9c_^8J6;j*-z6l8){ZfTC0*0Hx;L4WTdm~+?=dZ zsr6R*#TPES>;-1I!rz>tpe-Ya!116Bdpr#p@_Lp7cv2DN4nJa1i224OkN^U;wVOh90YH&v`11!{&Gl)(YsyLd+7#d`-8l(#cR>|+J8emcV z`CI?Gl`(_tmR{W@|CNEo@!sW<6Yqs_&4QC3=n!T*W3)nhk*xB_6;~a5h20~6^J`Pv zwoQGF{b2i+&C}AWUGK^_cG^2S<<~&+u#}P^O**+p^@dtAPltz#4Nzq$bCCs>%+uMEB};=;5p-CKXI-EMi%^QPgf ziX-Xra?k6^9%UHL!+bo$S#QDd@|)>KWdTXNrE^^J(^Q2SKCu+}q;8fHul8)o@v>2y zk^8j#1F$~S^~*9t>x_g-1}i;SF24E)?3Ax_$=B^2q$HX4d z3d~Tzac3~;m@7cu+~*2l*P$$Bsx<2Up7g5w`_hQIPsa>5!+RiXUAovs_@}ioSV?Cq zr2eed&U%*Y3Y53>x%}DnhS;tK(z6=6tiH!3x+c)D|M0kbMq6r|wtVRshW0+tIRI#XDSHPD=Uj&jBypjD9 zOLoZ{*>Pp>&QbS{UZaU|-xu`s$h%xF_HEXL@~dO=?RfJSSZ4)b{2l0v?iih=oJcLu z49mZobxam>mU8XDz_lATTsJUq-G=Txjg5P{UAr3^cDt62pV+(i#JKp{<+yb9>PsEs zE5`%z$w|Njd5d@#0d=!WTIP~31^j^h1_j^5mqWP3qVi=_{?>SB|K1A^?0Lxb#n8mW z&32bcyA{{*5;d4c)l0 z??`q-+UBaBGn<>w?5UbE?X9;@{$TGJca7WY^NXq+mrSm@+*MtSWnlbRtfN`1BMbY1 zYMW*@*z>-W|8FMU&_#ThL4IRW51t|avg)>cZ|n?t?Ad^ykFj^zXYx9BGuF$WuI|4Vo=}vS7&P$H00LKx<+*b&q+D;t~wazH6Y)C8Nx8HlGiQzj3|vjyz{yPxUOq4 zg!Nv%u4mm^9NRfZ=T0F+QlHc zS6DjXs2T6|;yq!PQ+p2J3Q}Zvsn-=4zs0`mkZaAr$Yweo%vs)W)dipbYyFvLZFn2& z1&_kr^r)CS0QV9|oIY0R0o+EfxY!x4H5VTPaZO1#xTpB@zz_96Cuu=_8s7~+K6Tv1 z8hcoyi)fKQ4tnI{^PbT-gZk%T8aEq$f9e9|oh`xjy4T!O&uOp`y<;xGM#qn4G_y17 zf;nifXrPHW13DjbY2T`&eIb#!6mjISJ=4>BK>4r8M^SIO2edEEpcjF31LpLWYe1vA zQ7$@i`BleWzMuJhZR7Us8^6ZHI5%|Z1_Jy-uK5UKg@ALxO2*XF2(F2BWN9Z-K-D7! z_Q+Kmw{6o#B*+@*WOpgxf!@Ij{j)Kq5wfFaM2#bYKWZo45P9T^tB<|nVUR`a$6wQr zuklJ$9M|E1WF&tJ=7=I&lm@k6fV2ai8KMajuW*KVMb9Nu+qX|$B7{8U7WgeImw&4h z_F$_3Zz%?ELEO1MTQA$9^xDyK!8nsa8~|sa&tI0)WZK-;y*_8C!@0V-d5>f0JWIW4 zZB0pUk+rAExuK!yjP9k5)`DtVm(5a<6JyNlZ?2uNw$}NXdDMi0MGk1hAh1U^XEfv-8CiSn;NZ1umtUOIkiM;c^N|p?UA{W7 zWm|h)MM+V8>%i*+eC69-A{tVuO z`8b*wzEU?rXFW1U#^MI>PuSGHZEw$T_lk`@(($e?`GtzcD-S>S#MJJyHarH7!et87 zw|xS*jTK#E#2t%U3M_({gtbAk&aVC-z09xN)mIG%Ug>gO8902^YH{U?>sR_v+_{4{ zZIkZ@NBGgU8wcG^?+aMM8D^+iL-n?C*D%ZlQ353g$N&ud3+ug9t!oNb!yX2OMX~chozYI^>Ula*Ub}z8{5g6OhfsH8f%NBaCxfER2AaRR&W) zTorr&nk!sq9}IK_9XQ){<<%#pT6y^L%h`#*n{M5*<;ZUN7PfZxku6)kd2=AE?0V%D zx^r<>RM`pIu;;vpLkabWTiCft9OnjmTg6Wk4-6uC>P;?5ZQxrxeKe@8CBd3+sZ3~X zUUp>4xvNQPX-}xMS-MOm6AE#z=i|{s@jkurDXZU9e23SJy&|IOLljm{I{!%b@O-VUMAfE=PZlN6hv-Z z_ksEVO|eeCl}p_6H&`81M32xde@A|d=A^suO1uw^lR@M*R8i$><$Qn?tkKe**BM*& z8q0Asxm-<-yr-nsFAca;IY8?O?Joj zTG#4<6`Ne+7awE2jB$o%)*%XYZbR`p<{1Y^cClrt?PeDIx|&yPa^_(5pBb zF=2EqMWLTAovjBhcXnL5Vd79rfaGsDZ|A}02J^)u>&{*(cS_sWOCPR2v2*u5YgXK_ z)|7hjO=np;z%~z`x9?)WDRjSMhBY|hpd2M0Ueh^0`0^G_4K|pXni_Dy=rmz2i)(x# zmUIyxe1X7K0o5~sZ5M^G&q9uD+j%rl4hlH3Yu1@RcHqD<_chlXJ9Z6+Q|J%`C&i~? zM>Ngh#Fr0i8UtWF^@>MVfNdAPeD*u|@+H3y_&8JtIKmJC4w*x@BXLBqsyOWUGARgU zpkSpHca`2tcIWw5URG3}Xv%DQDCbnThh>S?sY7FNmF%Kj}+0-Tv)y2z8iD1vMusi`2|CFftG2G>oA9A zJG;us`s6Q}6hV4A@*nuWQ|u#7yT^9+bHqIk@h7zaU!m8F=Z~uTED&$Ph=C41gOTo)`4;mdGxN?MlJyMj9D-~uWyFm|8MrZ=k;AN=Y!mqR!p5?_5Q^&E>8$mAu5QDAPgO# zA)Okox`J=xHno(vJ5xw#OO8LdPzU+}HE&7%F8|th%Wd?Y0e|@jIEU>G0S^Y;JTMBw z?Sc(LRQSJ{|J2*gx2Kr@`)giuzO;rV%5O3z&+t#eXK&#TXhrQG&rgC+OIx63YEwJO ze}A5~>_e#CjIQlVeY9ckYaLG#J=zueq+CGgi)8O1iw}t>w~jjfS?Ee94}M?6zKQ7G zTRClYVR>%!a^ki&?17i{`YUa_yiGN-=997wMVV8*Fa4#&UN@Vcw=1K2FGo4&E2_C_cx06s=4e zo*C9?Zd<#FjXR!64DK1J$O+;R(ND?mY#z1ObmBDuCvxvZz6fBXi=Dp>k@za-rtbyZ zI^_ubhT~>bnvIaZ&qjdr@5zrb__LHXej)aW_cjvxfRX(Gx!tchovic~_XqF1BSpNT zudU9m^?EnRV*>-vxF0WKCd~2__qmB$fD$FQC};kk{LJIV#QgEt_lGe*#?b6f5zZ6r zJ9jfQ8wYQ~J8FJ|#ALi9`5~T-{fE=aTsxD9XJY1`+gnW;oKSQb3Eq^V1n7(UftTP8 zj|4QNhpJKmDj@|(v) zuHUog`jE$e6LQ_2k?q0krr_-(&je4eSTPMYOl!gY#`8NQ=H}}!!|yL*{ladf#&KX; z)%W|NBQSQXWN4kUt+us~Hver)>5f(R-QLzWV80DR&>lig!asT0fnd{-BpFHisSa>t z+Ici@mn)xD>BX&k9%q97N^ZBq`A+vcPVH&-lsdivk@#HZBtcXA3%gN(yM@2AJfv3l z8coFEVhU|>9zgHH&-cas1l12UsN5eMqjiWoi}Itq?WI=Di)CanJGZRe__FhGXM20s zAvQiXIJi=ZctCy^rN>IKG}w&(1Adu@F=pG0^rQ6gQ7(J_LK^0<7zV_q8;9<2Kgh2H z%Pf15kzp08y-0(I{lqCBoCVc)uv_nUsGwnes5fI6#+WfAL20%e?UYA*{B=g@+#Or7 zQuWZA*tiPp%7MX^*hz8z*$NNP9DfFsj+muy5+}iJ4w6!BMMmj7J~}_(DbB-PZG_Fv zk?!slba6-#Pd@piJX26wTKEvBS+(Yv#DrW9*^1E_hCO9on+>!Dl32T(@f3CL)Z)?O zoq4UJOaAkpC8I4hy#8$aeyxhkWM5k^#RFI{{wrd9kDRI6YoI=pTr6%Z?`pRg__NQJ zjJA3T-kv`=ctLwz8G1vn>TT_Z<#t zw^FlRt=-Q1n9j^-eIO;a74tCqNTJ|0(A?qs!q$j*9pcyAE&#moAQ$%ib)s9!iWMk* z6PPeDf4^az|Bn$fN%6huG^%QY5&m>OT9j(R09TRPc^%q~7lfSWbe zRc&n*v8$6OpO5INajtOxq%=Q|$6ldMq>1-j&K5X;i6uj+!Zj~8Zd&hX=S>w0d^BO+ zrL*&lAbXTy3{wSQ-4 z=h(m}pA3w3cJAE&(o5H0ez_t8D|P)-c?BVDHwo31R!G^x{(_Jbp>GlLxS~kMZM}#K z=NsHH;-6O>VM-{AFU(mw*ytQ#`Ni>N38sj`+>qgBb+g5mx2|8AxnxO1R%eeqwq;Fr z#F8bMt|7u5<;bFbSgNqQ@XOW9?vgP;3A1OWH0771M@Lc$TLfJLJ*=v`F13|%*Dwa` zjSB6M&BaJ)7cr0QA^FRrN1el9iuU7OZ@$^}3ZFA@<}5=j@Jm_-**Hjs(n-BRafVn} z5bE`U@B5EI!I&lae)b2#XNL=WC#ONmqj8$)gOo zXHD>7_Qepu?^C!hCg$=LEuHWU*@o^whzn{?veEp##JD!L(R&mFK|M?fHo`9=xkQ$!Ax?@w z)I+P5X?p)908dFZl~4T!@0Q`+V!R96F@JZi!~#Q9F|98kZnSh5Rn;N3I5EmAmXhgJ z5B`9}mb+x=Su~a+`zZDp8p?+~hF#%s1V-30>iQ?0l|^;dMlJ*;=Y^cH%`iQ2-|bD^ zL1*s^?yJL)kgqy1CiR4un4sTT`+&$YZt$l25c^P47t!i#A9(KvuA9!X%cy1v906RZm+?nr6u%%D6xduYylijGjr{*N3s5+h;KhWn~LHjLao>RQP zGIm4QJ4n6~+R_7Wo>KKCiOR!YhwaxC6s|8Tykapn89i@k9SUKsA;Ya5%LCT<(xm%7znOl&8Trr8ux~xXYNR{dE2J9t^HMJDE8{lb#m59O z+}sT3O0mttIZ#2_p;D?mg~a|3*$wh;_ObMR_ibIZ(qxx9{^DaIyzTnt(?&%Ya7yNl zjJTea%pV-VqC26L^7YT-4->fAgSC?1+RVu9#jWYmp7rv_8xYSivR3{FV`p!bKZ5D| z3bsqssTMu}K~JzLU?ZU4t{p^79E%=&gTh8&wcMr5{lAJeQR0L|oC@hI%zU$}k6rKY z>XWzdi4Y(}CQ_V;EbLnX)?$DLi}ld18Ssgw-3yZ~?8QWRjNNXL!`LTqp~*YDJV!7Px%;I(i_m#1s_9JS69H74slM% z7kRq)ldtBtFrdyW$BjrcJYYnL8@Y<&Mt1K$%v%31#Enz~*8c!^6q5j@-}?dt z?U6&`4%ZF0x^BJRwc|Y3`8%Z7esag1!OuRsHn0?NL>VfxnL z;ajISe7`K9t7h*r<3~H-k9+pa8f=;Y`%H1FpW-K;%|Vrxa2B;_)paOsDz$iVc z^z%aQoT#!e0MC}e6MF4N;R)^D)6js(GWsgUcK>Xl_%gmW_@fEOO6>xV$BrOm=ghFN zrvjcDoAK-6u*x6mTz6PoC|r$8`TG_DF=UI=QbZ`aeJ3hZaqYlJ``z*joGSJ-JjEw* zKQS+DJhPY6#{WyP4&=pv?^21OFk$zh_Nc~reax#t^K1Ie%!zpYrN`5$#Z-{3o<8o^ z^!O9L(h;0X*xkvVN>w_bx4`T`*Htharsrg|7sm!#Dopzk8Yf@-?-pA}MaviRpIIKK zO@eoX$67**A&Lo@Gs-91YY)o*_8!VW5zU#ihXWOe@kGooi5T;&f9@Wi zk-wF*CRGDWRAg+J4S?u{7Q$@|nRII_V!z5^M8=)MS-6`7Y%9bWkcgb#soL79X6s0r z)=y(@ZGfSZljT}73oH)wOTA4fMmUzDljc_K=J6%b!bS=U3T!7@kVePm)nnUgOZ(TI zv8H-$g1^%_?r0xc(_5L?+}2WE*CK`NIBUzwh84>Up&N!Kc9rGHFSADn*7Y`b_O!P& zHgpZUFh8OMpj%OnYy@~|xn!{-sSy4{(!Y)7lRGBH4`lgwwuW|xtzY9{YnkzeH~w(W zQ)e9h?Aqr5yXZmsA?yP+y+-_J)1iR&;DjwMF+@t6F0>{3`0z3J6<1w$*;SlIoHPIJ z_ZQFz+f-biKg~Y*M2dL*hKfvkYSCUk4{R`a_d~@7LxE1h;$z{aAogDWt^VKYf7%qD zofw@Gkx^7w7t5#=m=r#oWeTZ}E~Vb!PMjwOSRrSiD-W+n&bxJ}CmE3>PN%)Ob-B|S zkkDV0*Bl*yzDx2ShgPn%Gn@PUzJfB7N&Zs6Gc&{9{C_xm55T6%^?$tYIZ4wtU0Kae zvy-Mtv-eK--lZ+Hg%(PorR*Vl3Nl0l6jV@A0TtW}z1MkhTvXh8-FxHpdfk_t{-5`x zW!(Aw{{rbuPtNna&-gs!eclPWd7u|~oG(kmY`%fn+FCJtK1AqLc;MZQ*HO#p*H6d@ zSjK;q5n3~7K}pHUIT=NxRZ2+!1ZKsmfwn-!jaQ?8@^iayPzbJkuLi z2J#C;bWpC$00B5*sj>EBKi;L$XgMB;Skbt*Yb2PA# z6-?TByjB?=DK8%h2WG~6T~bDECCp>6!`vA9d?dteSYCed`B<*rP_WnS?xkhVGUlc8 z^?+TN^cDY0gC`65Eo!GZSnZ0;tq9Fl1|VS%r^pqBU9MPh@Nlq1ZI$yS){e4@`7t`N zUefHaMYTeqzSLDsWBQc+s{#Jt>Ah*tA?AdX3+sYJCQaISOPvJ{kwzu^NQ*6XWM*9| z7hThZ+Lo7AFy$kq4NDWH%Uh<*nKK1HL6_i6gWu3DxIUo9)EtEg{<~FZ*KxTrQxf*y zgzzNu3BF^VV|$XC;|Ep4R8IzN3C?K)OnyL*7OK2MmE)8A^lE}*yR^J>W99i-ezC1! z_GlY#CGLgwlj51N<0p>Mm=OC2uy;b71I9fjsiGk^z{I`P8#`+@>@lc1jpmWQN$tT- z8}-h>lDNlie||#)>*PuHk;@X77>XB^&sxiV_VUYN7s3}=E2p&$%)pJCIB#b~Z$mB? z_X3cq=5%Zb+|Z{hWH&~Nmnj4@FhKX)1@B=B9U>6P9(+Q^y?*VV}n|(^y z*P6%3Z4ZRI>?MZgB3o&|8>@}DL*+J8i@|dH(seWXm0lGN?yl;Z)z^Bciv%X z1D|-_7SNCdiYVz5d5Cd8_dCemSN}C&AckOoeDa|-tFwAt>J+|NwBLMIV;QmIU1~8xmPc=Y-6hpBI1*_^!0a}{1T0o&Gyie5BF;|WbE@##; zn#*e27$eJ<+q*-vmoTXtCUwqWHdJ`a(B6t$@7=$C+QJ=M24=+jz4sqUBsT4834>fI zpjW{K_FPQM6)xt}n?EkL>l!!AO;V+B+l9uRbae=&j>!=)>||bBAlz-2iaLqI3NaOw zD9XUR*MNDifTCZ?s0(^0$L+gguC?Oh(GvI>w(*UP*v5lQji13zB0AfIPoD%#0l-SdS04jzum5)ku)gSw$)qtyQ4CAhe>#vHrL7)&n_Nc9agGzW zIzkXaANnPPM*C$QxAxVP-_;X(eD%@mh%-z6SVxVZy*($}U5_5${xR@!+SqI4r6dca z0r!1qpixr5#|?K_E%0Zr3d2SO>qwH9elFB}x2`-;`l$c0YYp4(bWz{NUm$hV)jHwF zZsN(S=bFEKP11d8`t~QA!WBmrq1tll{@K@2zhg`?JoY@fj$}Z_2qOexDj1`$O+t1~ zMXp7w*B5<|yu0BPvAz{ipTX1#w6Ex@7o)+E579_D>~%3ve*s@UIQBBRmDJ(8)nR0d zp)HZRQMU>=K;j)a*u#=rKaEu@`7^$9%(=!&J%j4xc@=pQw2*q;8V-~xt7E8{I`9W~ z=ijd{U$~03Y<@ZQKp4yv++5n4v5(0uqzAtTQ*U4zJYIO>Yp1hx_){7Sx?N#Yu#hZO z6b-65k$a!_*qgua(ziStGW9eY zucTg^-%$~kl-&xgJSc59$a5X(#xWFZ^oBjFu<0A{HSGF^yy5Rei!xnb8`;Xa1+hSHKt>)Ku~VPV2yRW{jO8*OLyqUrPmipqY$U5EA|%i7IkE3I3LUd@HHW^J;#L<|Y_zVg64*X-T0zy34L_fUW7^K_u>8&brh zv?*gBlG{i#?vcZ1OwvG(&LdzE!Oe2O0|0n9^^d-vxqRK+$ZD&`y{o!+NByolU3p4MR$*Uw{4A&wiiQTE1W%T;w+ZbkyI=;d+zD&X5bqTwJ+h zrjbm8PFTPdl5Xm|A?hA3+MVyb?{Xe0MVGZ#Om{p(9XpJ9cq^<&6KTPfP912diYFxL zMJ9N2HzaQ^ukjPjSFYrkpV*9!GO3#^x14dO@%|;q1f(`@v2?Vyjf2hN#j~rIttEC= zw`i-p72EVxf7*#ksfQ1I+UC_XRTIYK`xRbH=g~AJOl$dq?xCd9=ddd~lpG*eZQ6_K4_LVf7RTG7}LeQzm}DK&U1~35dLg?YT zVI@_-j-I9vaaHm@MzA?&CmMV&T=6xvZ!g>zN^3;@0tF zJ?9@+6Ax4;-_0O?NoEmu!)x||%y)sW0c*m<77o({Y6;dKs6_X&uWfJ15>W5jsrBd$ zm?|#Mp1*`w(cJqy;#E**s4v&fg=-9e9&QBLgdVkjr{^g}2M>V$zUa~&mqa$WG@-p! zgIBjyZt9|Tk}4SbaMiTdmT3-4$nJ$xql> zO+9SNz?(5)HCV)Nn|y1IwVqm_edplTOLSWe;|ff+!=Kz4mkF0>E7*(O}}-z zFBBbUIo4KT7e}WjGIyzI19W;NmRsDBd+!x4*tZF#1^ChHZ*H zyOtGEYOs9=(3cs-+jgz8FP&aDA}y8n#hRj#ME8olVE*nlKDnoF5!^rGjlEC2NfzOp zA5*WJP#__3;1cSuZEyNwo@Yycovps=*FKcpVrcla`t#LTKFPF)(io(b-rK;!6&=`r ztzyF7Et0-H(we-CSekqpeQ|sD0{5aG2Jv%QayKzTx%Lc~%()u#Z5i0!%Se18(zw4& z*_a6@StgYx*9bayTWU%+HxiO~yv)DEs4NS%mE4S09?v^Y-LkuE^DIg4w?lBP(Xm&- z=P)pq!AWh*bvN7(Fn+YY3bKUahd^K81aP=lfr=27aGz?8i^JN3xuFV zSf^NEc3?3v$N}e>O1WTD0I4Y27F~1pK6H&-R%H-Tmk^gH7a+rDNcZWdvClq>efo?z zDz@e-auxEEhWcEnX-y%+Bq9h>}%EzHyC^NZcLU#S%rsG>y++`=u*n1774 zhOv9d+sOu)H{?%?rj-J{d^`pWNWhQ~YCC|G0RUXkAS(c5NXH|z-G)@60XE;kf(huc zE*f0NC9;^y*JgVIA-{_~G?zJT+VttM;o<%-Y(|OCVaU}xbj9}O9TAjq_RHWG$L7yR zH@DQrJC;>w9~yg-JVs7~m_-W_mm{q+4X0l92H1+Cf2gr4#jQgHf}~SX?1ozC5~g>97L5MvVqO4t(jO z?kZ@o6+}2FC=Bv#<*KPnf@{=UUD^(U%rLdrXe#P7EJhs~YU37i)deu}`G+4+#1N)z z=RE2b-4;dNvVBU{M9`Oh{^q zA1WQKkp>>Ey(}EwV3{U&!*oj)^&3(UEHXVDZpkUtXki>86TMOXJ!?swCRKf`qP!FQ&vQ^PF@k=jVrJ50v zG^(ie=7Hc;>!G~pPZC-Dl@P#R*Vv=vVX_idwh%Jb0b0x8p1^1zOvZ$wrAl@XqC$)Z zHXaNLqDjOcJcUZZj(FU<2|`eTZH2MLuO_Bar$j3kIcF~xojjdRFdjv}{75hu3}jB$ zg;$tnwh=oR$Qhq(v&Kb*KAo5#lLwTt$+A*R&?Zx#KW36qpIr6%Y+v7VmzG8KE6d74 zu`<;p=afKgDFFSj#p;7WeqU=P;vz$RQ(p9Rj3$iMztGqtZjoFa1gPRzch6hX?qMrYn<8>T_eeD&*w5?t8&o8 znamZ9oywUH6>eQEU9!T;AWBK1t;gTr?=P>b^32ZRvFz1>`ufJkKy`KXkL6V^S)D3I z{c`#xlvR~!ddpD4mLjubtAa?FEuqK^cdag_NYwcYw1i9xGHM=sjNDH)fxg;6Mw@{8 zaVnh#NX5gR3A~orsIm~yhc$CtQh^hgCmb&iXa%r}8-?(kq?BcK(;^LG4Q^(?pNjhT z@UzkfsXR`ME%|Ji!>8KGj0Z+Ju2^c|f(-z!k> zpzcF;6ota0u+Hhwd;B!mBut|RCLFe;&4lTFu*!TKz_7px!YZZw1(sE(4(#fLazX-w zrs14C2xu!ML^ynf(}*XuHpL4#+%_d0@Ny6MQYNXTP^JL{Vzl+f^ z$-R>LfGt;;vJBiD@*CUsJ$t;@U58eh`NaZrQrneTbdQKVwXw15uifqKXjOx)p`m=5 zL{wxkl{H+=H0rbDndlptW91Xmyp`*l_ia>9J9{{sj?~m!tmNO6h1ou4E+0LWJWb4+ z9$dLHIK5r#Pbxd8?gM4hY)0f=B^qL;&pg zy<{ig3T;{$*zoCT!)bHV7N#vt+mf~;B_%1@AZYGkv>@;T(U>iCWC-G~!^n%)17H>K zL_m8q$YG^khx1_ukDb;h{99U{1Yr78EECZA9C!)*MhL%WfhT9;E5eoW9|FE}tC#@) zy~KyF_ptJ<+1Y1wa0*-M*o_8)eqE_TIqz!`%6g zi;hlhEk_Shdt-IY5t3oG%I)b|8?~{{tZ3c4?8(Or@ zphkzE*k;jttU*~Z-+=j;lkmha95;+JaWFfP+5%XQtLkXW#~&leOv1>U9z z^6@wc2Og6I)uQhn-y2O29rFQwDi94!j0&RxOw71agDR${$b}bxbU6j+X*le9>EPjD zfkHdr+K8&R3RTx1g5$8h8+q$3^xeDfQk8S(qKyj|CWl8xh%EcU25I90ThOtpRouSH z{?_-wU&*59lh0M7bq0&~s=q~}OQzk_^ZK*T?(XTK{(>6Mc$?J$W4rIQSG-+@fVSQ3 z4~1}VTSi)J>_zf6z^MY7B0J0{^MgiBgUEqi4YkPMa;?K=1SrCU!fn&Y4l98d zo-FtcRt6!&O&BJgGqy4zHz-?ukV8A{(|7=MG9A5 z!Sx?~^zq04KsKqez(JjME0r$56(;b#N6BjNyv0x{aHZjnL47JIpDP+ICZ{I#N z4aulaiOb_JQ_<2=^u){YM;_UflxPk4i41}8;RRdji$l|PUcULZ+dexwgE}#W*G&NT zc#N!tOq&d>U_5*enWjlLC~%H5n_jAbQT& zHBRvJJ9q9xpJeb+bULqCzA_^1UTWPo#5-C)txR0F`dS``$P-8E<$5mKJUhDBM?tb(Ht z7wKk%ySf&@W=;mdEi#n2Eii;Z(^TiIsaK%6FPHsrw?H8*;IUv+*)^Py$XBICr8O3R zVS;}fBlo4JC5%!+vOhj|WxU&xRooKaG4;+nqdWQP+{MKfZb=lC2#2>Vh`9Xym-GtD z(N=0nIrX7dm{}lk#F58#xkq$u$4iHr3%!>*A(uR9>?QJYn0u=Rdo;c>F#Gu;4-4QA zXV+5dm}-6*H#rx?&Sm%9bFI2DVJi07>Q)(6R?jFinpT%esb>$|c_&dAu-0^k^Zf~* z%Rl8*4AqidYoo_vTW2RMB_Ug9x1(*+%&pv-((HZ{3&C$Reqqy;fOPLj?8{Mv*T;RybrRMO= zv&PP@TgaVV$S3w;?ot{(Qhy{;@xk zH<31oHzwl!20)}8Y29hPU|XPWhU*{`jv8-p(SRv{ECBr#)}7SzKT!)NQ3X%hA%cNq z!s~IW2Vq+YPa%9ndmo^d_@zp|Ix4FF^Ku6(|4N1a#YeO^9K&W(;4G==01gD~z=2~i zWJ(SWqxqv$a(Eb#qtx%i!}~_5hp$aNR+ihss6lUt8;AYSq;{0cRR;v9kvdx8D6__G zXkmO=v^fzsd+oELDEsW!WNU>a0FUW@zkk3759-6R`{MJ5ulG`FYi0xx+kl?RJwmC)Nj2xCu zthVRPV*A-DhnzCM_a4Jl;p*wpma|aH(N>0%kN9_^KrP?;-q}0eEw4eryQhy9EPfqE zN4_?%V8$%!lMys`PL1l`iaGHStr9)uj>mEgnGt8i*_o#)b9MzpYO=6EwyMTL6vuFDuVlvol31Br#qET*0Kd)%t4aysW& z;~In9FKxG4+ogVq$zNi1jLn%d#~Hvj)I0VZ`Dd~nta$@SEhJwd--LOIkqbUggfmDu}AbodC6S=bF416x{5kOPljE6~<|lD9MS&jH(L(!Bqj zCA1=+&oqpSBw#(9Z2*A-K^*5Ij&>kR37Ic9q;OS&pH}2ONAsg8gDW@Gh(lcA@n>bK z5BrucN5RnWaOh9rrAxzqT2r#AN$l`YkDE=BvQ7HOriI;;i4fWshCOnH)JNgA=o)WM z0p~B#4Mn+7SB5B~X=%Vy6G+!(y% zlHJrlUZfnzd*h9BcA?38JiB&z_D~AX_F}1hiyifPH_D~zO(k0s%RJfK{10NAol1}I zj`qA@d8Eu0-5h%hZEN)o_;7~WH{flh?j!sI)K?wwuy?|P`a)PCbt-#yUn@L69)| zdKfgrxHEy1g?WVw!S0D1I1_jW_(TX16qTpA@sEXt9}F&EJ}38XVA{}7oNqQYi-@yx zxfW9!yFn(+q&_Y7<*`-S_328*T680I#oh-GTOEb65sA`~TX^Ku4bfe#t)HQFT~b+g z8)^2+%m#!Mb2yR&wFa#td%e};JZr?<&tyXp$DdhH(4e$rWm(t} zhQ#10M{{cZ?Lw#D;ulKX{#t5xIZD>L4q4cWLW?^f)$vS5Xp5I~Hu?as^Z|Z*f~qk`lTV}*W>2}@ID*o( zliKw)5UZn=_N6AiW{TmOYtZnu*WU2q7M?<@E`Sj{-%0`wYqmwJ%b(n|BaX7`%|(x0 zVK*8*zw)Lm=y3##>ZjK6(2H!7u#~T7uM1y&b$CaDQhuQ13zYoXVR0f)r)^t)_Uw0L zYy7*|eB2rgwHyQV;T$6ep^_tmnnp^W>H({^r$Ijx2*&wroEw4%u%Pt%L&GQZ>VPCL z|4T5T0$}0BaX;*U#Rv~#%oH%tv6uu3zpiD+)Z7O{$@IK@@eH3Ymv?hT7xlt>D;3&H zi2J8-?E1ty!Pqw58w#-4z|N&f5IY zq^4!?puXB%=Qfl)0NH{S)Ek!>4MINM58NpQTU!f~fVtCyqiG%goJfF) zz|LF4Id66Fl)3TJf!m8NW*>a(^aA@>ph77&F~Q4gKzqQ{T)-)KcVu)_6dNR?GxU9f zqCHZ*zgOku=D*Q)*)E=>P$57`Qcg%_dN2pefrIE1o#&x8s%oXgaLGW86F8MNGn-t3 z)Qb?CDz1T_0JCNTvu;r)9D0-ev7XDDd{yh(kx1bTT+t{n6$aK*uRSd<;1_nG1sWo> z{3xf`CP?K$M+}SZJTc%p0Bx@6Ut{wP9-r=1 z!^m?&=IF1-*$97-itY>p3h{v3!^yk8Y!dY*slSmY3lu3eH!97Lpsq{MlYY6^%T``&y`nt583U>yDpLHmlA!Wqsc_2qq>?X zhp#THgfCBLsYQ9ZY)d+qFll|4+{$sO{2+o$!**MdP8+B;yX#hM^_g5kT~&e7pXbRO z63%f{FW$3ZQei?H(K*}quR}Fm{F0&q&)fC(L}IlQiG1Fo`Un=&`PEsN;mtW(5m&>p zv3P)w_+xv>2cg$Vm=**H#nl8FQ%;zoaX$}S(m}JwM9vLzo*Kagk)_A&gc%~V&*v13 z$78^FTml1?M~&}N8SmY7*HceLPoBK{ZeobcWfCH{y*IZ-t;r(Y;u+%Jp%PYlkYu1=T&^ll1wO;C!lN6Y_6|GT1*N%KE&2=V z*y~59=(=t^c__W?N_*9c4`CB5i~0p6zR;8m`KlUyEy|cA+!AReAz2KM^fcqxpCHH8 z47n10S_$NHTEOo?o`jC6Q$(jE3*%}Qgwz+-Gj-#bj}Dx@JFtJ_A_|v9 z^UIc7U;a4q?I)A>3%7Si`Zn$#O{bn`U$%I00zwrDH!L!VA_5dPR+<`RZ)bB*<4nG} za7{v5y~}acLf+JMX7zTNeC4%bHX+Fib}9^9v|rxo!r74N6`ryw8@JDn?kFtpDmS2J z=nX4hA#GL^${OS|ye+yZMyk9}3;7R)PvO%fRBmyIr!UA0fy;8qx7kG^y*NtuE3!Zb zABP%s1om&)An%7|q8cnZMu!*k020{~r3f#}4N8V3h69eC{;YB1x`WV5^N@#?5VL^h z252G(1%vg)JgDuy%ULlp5`%o}bM1!(om)$W?-SfURewVG($kbZ=dOFd@lm(N3iUm= zM^w6yDjYb{AZWZkNImn`3CSZL*be=^KsHMKgmYQ;q2W2L7TF1nU8w7S5dCeOzjP6v~d-6ov}&ckklHfUxEv*i=z$t!5VAr2y}8o z9<6EYDRMJe1G^XG;MY=lU7Q8QIfrxkJ}@0_M2ER<76iNS6$0=A9*cgmAQXnza(M77 zBDvL1?elN-c2eP}-~WRz9zd?<4?90>e!97NXXnmlvgYb!ekeqIfA!T@CtaC@EII5| zL;C31EaEvT@pt+Wg$7H;#1pWE9u{`g) zN|aWGV2trhA(tS}3?Af{IVg5e8Ylv@k*BLTDijq5%3UIh%hOm56K&GdYM{5^F|v=Yd6hv|!PGQp7x{mzd0o6L zblFq*&$TWb>H%KIp$qxg3W6PR8H|oRNx9YMWVClUS5TirsjDbc$IW4UY1;}A{F-a1 z$4l(?C+s!{albX+ly9Ss)?4f9%BMn&iz_pg)?aHiGL7hWamkvK)4WydoAz#2PfOl% zafvIBcVUT(s%8;|$weLou^@RWgLvVCO=_Nc1Erm)a5-z8Q;>KTPtZ==te&w4$eW=r z33pa;09E1)ZyIDHX~T;EE@(I~qYP&Co$raksz+CJoh*=%bFB!F`vWn>;DIz!FGtYr z5h`#p_RMCIMG%C*VybqOC^pV5p1at=Bw{3)>lCZi1`&eMrS8p}Hxutt*@QRw9HC2! zYDmW?hYnHK6x0=(in%5JN2VXB^i;0Denr)#Z??zY)QW|=RvsS-Zn)utn{EPsY6>Zn0p3GEFBEj)fprnGaCU?iX)rGs0i@2*698#IDQprrSFo?i zH#92z;s(haWxhi>qT50|pC5}6%E<|`;=%H)h-dEHh16+({k>T)H?Oqls$5MLo5am8 zG_!VWzT)(y8!}XK-MkiR3F-M$L8kkt2fZ2c9ckq!4%@Qh264@)fFPg=wJrZ8KRv`O zLx?4irof^+Jgay-x` z1V-ED1&fQ;b}Za#lD1tow^0-6wGAqyrTed(N!^`o>XC;fla$Mi9Jg_?>p?{2)>opL zI>)xi6MDNpt3<}%6xkHindL)udrt_2!GatVNqaC$5`R_c}*^n zH$R|ns?xMX9mktRWru96YMr>{v2t0+nCMz$NQ8TP-}dN3yuwT2XoBExNn=rH(u$ZN znD3LssDmb0#0Ew_D{ur+*nUD2!9%9 zla8+Qr#i`0azf>vsGq|xfV-(Kv2>Q!Eo7Yo;^$OErxP;CqsbSp$jR;aI5#Jp!+oiX zDM*Id)sx-YM#VCb$W^`KF!_CUeL0F{a`r=J_BeW$^1@JfL6K@gHhyMV=?)K(rL;9B3XqLB=sdR>h-#8E~`AhFzgc6-yE3vnxx@6 zYhH=HaLoW=B8MUe+!t2{$!}juIz{EP2oyyj;nIR zaq^e!x@xp@436u*yq+-)nFccA2XW8PJMVn{IUJ{ z7fRM69fSH1Nkq0BM~+R1-qc!LVt1EF=F60^r{r=yY(6?2>VHR4cfBpIR93kC2}wj3 z>s!$pbCqWzFTrQ~S?nrT>~o*p?{fL=zRzu&@ME=CFLe}`s=^XU8TF7W;k2kHDY=@- zntR-n1>yE=Z&OZTqd3dil@YDO`Nb;0O9!DRhX>W{1lh8UN*%_?Oi7Dx*UjD?lN%n&2}T+UoFC zc{}Zn0tPFOpq}J4Z(Zh`x2~Q?{Rvet5QD5_5;qj|OYO30BC@VCTm_FXWPa|ivY8c? z1)Gf2o26E_FrKT`2Txf>uG`Y#YFxgxuT;u0Mb(Z1YLj%fq*yWtJ65137KEO~d&$W_ zsSG?$4CE#amkpu82#W!ou9zSbJ5<4it#bf71Ry&psQ{a%8wXD~M8Fyd1}zmMu)fTW zdK=J{X=BOLKR)rqyYEIm`|SDTnZnMX(3xd4KKBfC zIkjMhdxoF7e2Ul8tTai9uF6FQkOayb+)c0f{4%Qh(n%%a7|e%({B;8Q8*e2`K?3kh zs#K-g0Coqurzb3j7o-u6UU33l^am`R0CdkSDuXu0pqS(GO2-Ivkbpx1nl~__S@kzX zO`%gVNmNg;lE2+BGII6MHBhczfDg<_=ia8}W)B@DTINWW?68`P_NTug%FA%pNT!#e=wq4B9@iUtMmhPB{Jh6s)|f}5vt}(oKam7S zt5%tH^W?Sl=BhGDC%S(rGdC|w$|1;GIL|yLjZ3eK4{VJ@tJ#&F$cn`$PDFO+r5`pH zaZDj%J$2Qc>#FVXMF+Q0p-^&%xPZmim=*yYPCCBsgR1aUpaT{GV>Y-w*lhA&EBZJM zjGNs5C3&0z$P@k;%kX%=F_3qJ-yap^v?tP?+ix-IyGJDiTq2@JfkbBLV4NorTh!rvpK^(``dEGox}*o@Gl7DzVLNlGe^Cj?UO$*tyaD?w8Sj9OJzFv^2d8MoA_b zuZ?vV=Xs;CJ>3sK9NCtWu6;n-QtWS(8wYG&fXOZSUT&FRK5tHm%E^`V1m@_5L*2%J zX|m_Dk477Orm4Yz*9)af(D1gg6Oh3*Laa1_g2RA=({wl;MHeq=(*+`p(-Id}*zxd_ ziJbz_vaUK;mI9wd=Pf!*CIC;-KcN^Mq^yEo&##tP14(IrcrukOeD z+K>ws)|_R_no||UAXj+$E0OM}MbJg`XW7zTQ`=&da>I~m@p|5y@1qkZ63;z{KD#Pa zM!mRx=!!%A@NFcWdng`>?(3a&b+xzg@L(c~e|_u^e~29vWF>GfhZpv&+yn858ukak z4|w#AOc>7;E&oIcRPHCh5q;Yn_>&8N09E3(m?dN)PiXQ-N$R~n z{Gsfz$39Db@*{llIQn9J^zDx5I;ei;za3p)WQ?VXpHW{Zo?lwJFS27tgzC6861g=t ze|`+-QlevTkgLc6;5o)EI7?6lQ4@~;v4tGx^Tqd{Vm9^^bZ65AtzdXtqXxO@017d< z!jm7!ls^Ke&)t7&4NxZ9P0fiR4)xb=bdql^9?b67NvIQ1bSXZUm+T0IqY+OWlM+kS zvw@UcNi1@t&Gn^kDFtnrMJ4>8SnatNZ5SBv^+CH6)`2+Gm|ldQA0GJf^U({;ng%SH z66O?xodPE7{~}%P@kTu8sz~J{QxOW#dtk_3iyZV|2J7nrSZaw)I*nD_V|`9k6-59f_3>z2&79nA#TRa{z=Uik*#ra z+#vV3lG*4gYPRdS{{H@bJ@CM?fHC=7&~w`fa^03T2)*6Q(_qt~_umP!@M2*{skRyL z9~afCC#udMNrn`>gE$Po6b5WwpaFpBRUFu;ozf9}X(9L^Y^H#X)FR%a%3SuyNPx*N z(o_h;xsn49NE9CpUvdfYP+lUvB-fgKdA?rTkP(%NGVI0c>qX%a{T;tBw$HLI!zZ4aDzG+W*yH;>qgdvmhYPnVbPN43=b=bt1y zS>y2)l;>!)L=T52rSA42KkAd)3FGnk6orYdzN^4R?%d$ z!)k>%p&4?{_mWcqNz0%H1nudm+E+>|0A$c`5!Yeqe-IO3`X=@SK#%~!q7p!|1N;qb~If`(~ZVW#5e+dRlaE==Ts>-?liHPu}JD6DK`^k zlgfAAx$fO}g=K+AWHr?eS@$c@vE-b_QD>LO@9!O*vg=@nzkeD!9RAy*wTY<#c3$51 zOHwxeC9oMakO@|SSHk-V8_)Syh-ttS3P{+-xDXIj+kM&s-hi_03DNA8~fM{r)L)fEBHQCo$~`@Cko}y|aaM zZZH5A6Z$KlR|WcV0p6!%5Of;T>ZIw#K5u--7hvs)1=T?o6@LqTu;&Q}fB}gLFp%%% zN#T^SWJu~*P(;e_m8ScIEV7Gf#N9e!-&_fIK5S)S@;2e<9iHtvZ@swG23Rs2-s0%VWPWgC1 zOZ!DYhRl>(1fjqkN;oJ)iIwKEq8?Y2ExaY{K~EHKs@In;GRJ!k7$$GBJ@WiVAH{$8 z;l<>e1<&X>>?qp1YuBq+5?8KT96s=Ij;%3tjQ32%DqFZ@QH1&&3AfG+d)Bm|v;~nR zOCk%#()ts(_&8!NdI0wP?%%a*7igDx>|5v)8Gyo?rH^f_4(Vht6BJnb>mMimkRWQNzFpidJA<|#V&g`q#16d`v3Cjr#)6{vDJFYqL|{PJg?c}%}ok_C4RhivMn-o zaN4lnInCYl!uHaIs{EPZ(kuSl_%d~0%oSnF<%Pu?>@czBI~0sio8MY`Z$-u3mryg5 zi9(<$2(txlg*u!Z@YeY1;0{%~);lg=fV*e}4+6%~IsX$?X`omd78BES>u2J5Q}i7R zl!CrCV@F@k{)5}Lg&(FeBL7T}BvP;j(}R>~-5QkK0o0(&6SMdEW5M11ox7@iRoC_> z^52g`!RclGzoTY3>$|s3VnX17_X>Ulx!HcGDLUw`q>HClc%cZ2ts*#d19>4C9`E_Z z0gr$~6?ozoMm*kC?Hvy594s0G$95KW$0|44AT!B*HmgJTj0mj@r&@W zX?O<*>q6q_6(1*#1iy}}MI;<8H(qU|7ap`AmA}T}A2>`}c+gj93$?5J%$RhxC~Ex@{qX#K_ff5t$}KF> zlR~J&-d0AvDYx3VA+}uoWKkBHf}r)aE=(;&?9NV4cX#r!h^L~VR$ab%h~aa`!x>27 z_mBEyY=$6AD41C0SwQdefRQ!OwSc3(D)5Y*xYrC!|Ang#a154n0v-Uqr6WTo>w@Vn z7p^yn9U|_Q7TCG>B$woilpPhS% zN+WX^Vvlnqdxk-q&tPN{-^%^Md6JJ29C*+C93IKn_Hx23!Oa%bUr;8u$f5TcaU2;1 zc8;HqvQ8O`xnZ_Mb`KhKlv2NMzWs`3S^Z_#uSFG+xa*?`^;?MzBt@>aUTwu(`w-Pd zs3kSMz22#piioT*A;}2Y=j%1P+4fxN)LfHPvp|J;MIYtJcS zzm_l2S#q5Dn+g|C5q7UKN-tR`Sv;5b@_S75OxEDC7g)Y-t6m#Y-|tfEN;h|-ETy1G zTq80Og$GIIugN8!W(aPvknCJ3)6bCPQWexASn{$IYDhuQau>OOid`{V%@R(M3dNJe zTB#`{6E=&e%0Q(cAZ(0>8Z~}JdC;Y^XFJUGxi(3-NToD!vUG*|z$8nqQ6!i4X%wV6 zE1+~0Xz&_T>|rjiC#O>v)b2 zunwL~2U3h}7p}IDIJ1G{S==)^(S1$AB&r(<#0Or=$0#% zD-4PpX+d_Sr_8nNQdF1x*=}DsNvLYCi#S3dM+81n3=}2_x!7N`y*(?a_6<{A!~v@9 zvKtmBG=bT>*P!55sCp4bQv>Z*t`4D@F} zZsv2yV^0N%D+TSu$Q5_rVj{4zCCC6i!7hx5qZ>cPnS>y1Sn}Ej` zE*J2G13e+HLoI6r^1*I8-b{56;~v@NX&cinPrEAZVA_$iJJRj}F5w-AKs=5s$G;R} zb(;7Fe+kh#b6o8}!|?ZXyD^SGh0vJ?K$E@%*e!zrm78Ro7A<#pXpKwph#-Ar>*SSW=F$fSo}&5y8x0WH1>}W+tfkLdIRZ+)Vd#%NTca zvstd&MNuey7|idBVw?hwPkEmxn4ZNbXfF7e8xa;LYLYk8!oDgHN_d4qgITY&m&bj= zfH612oUd)wl(~f#cZoqoB@x*t^;hkHqfk>C!SUNq+_@%8X;unVYN0w`V%G75+(K#2 zg!q45Rjep%G49;L&0MxbCCJOp*9YcXJf-%`+-&5dp0c1fB##l@S%f6y-NnAbWguC}+YaaE z2SunOoiw-?W=>a25b=>}iDbk~l@eQ$%ZN!9KL;`jHnB*gEHcSua*J4OY7e;W&J3}{ zC<~NKsdGB(9JMZ2t>d?2hb(r1S6Va!@uj+4#}E`0atcbW6qFSQrBAp4`&h)HAI?Swj3{qXJk;rn2lsf-a#I1z=b2JxTR>Yi)ws$iaCD}$| zMq6M29)Z@@wV=@<}ZANn})AM_uJHd4#%3^^l{*rYu`CU??aK>2x}K zt4+EFy;P<5AJJ87Yp5rq^V_}RXdbH=gudeWp8JV0 z*o#p`YF3wTlL=&FP_Gf+#^Zze)Y33C zww!9pcUzUs_f>17wnXpJ$9~coxv>J}`-mIfzh&o4tQF>fy-tFEKtpu%M05*IP z_M^;(Jo7N@@L31>qW@h-#er!`tImH;#kDxI=fQ#l9tj&{`fD(IU?uS%oL4-ocj+t= zs}N$T08&F}kF)n2u&iimTVQ+kzAf6q+OABFHOo>zp}FNssWtTqocUIsd&>hdjgDUv))|8QLbk~+6gp5I-(MBTa=DC5^Yn_z zO-`v7y_Z9sZtB&KXcyLRQMT0Io_qSFFd@1kzGz#xs5hg|86Ljo;fI5pa}3()0exZ?bW%5>;RMQYpDx*GsqR8MXsg}%81i~v(cI$9m*j4N=ws~5N z=+%>=z?S6AF?)ECYZWR^P*;XZXj>J4nEEN$3!JK9;Ew8XnYy@!foT7N>%iDNrDSM= zz1ZvHC!YH?v8fD!vxDx{(3<3#Wqo~Rd&>IzsW|t~h55O}x^?BbIc8J6Ji?Pb{+>*I zX4b}y=!ZRfs4HzVbY8=d6-{oO;whhwI$*Xydi+oDh*MRujiF3_{yWk2AtBEkJ=)F< zM4?|fvOfA69P#!$b=bKnq6=^Hpu#yDif3)db{Nkr{s8dIOt8Zds9nJL#(%2u{d+QW z(36VEgdr)k@L)=4YeILTfD9oz_PS6|N_n>y=CWtc_A}Wmlcua7%oS`pD3d(fF))BU zfrNp)Vpj|uIEbN5t6=i}W|4SAp-Uc41mfFb z+mNV~x+w})g9M#=j=T^0qi`+~c7E$X@>327^x?vaA6`DNiO^Vqkt#q}+_sB@Fqi>H zV}+D4rL)Uu?`Z9Es{DfQ-_8*dCBI&_t~a$y$w=G_e8$tZOuvYC_fXT;9+ zuX9-ptDVxh(+snRbTR4d$&;Ne-Se%M>v~2b$D^A!M~_Fh4Gj|?#xxypTJ4HacS@)eo_w)Q|~b~e?^QCwQ}VZLRV{>0~j(&#J6 z6Y?+wKYlmFi8w)!_pLl2oG1SH{m)qZ!xmykaG#02%B88KZi#Zrr^tt%RB-=4#=ZkE zsw(@N`(EnIWG0j8z1Pf4CewRMPe_4;6as-1Adt{|5$OmBihvzZL;+Dm?7gp|V((?w zU2LnXZn48WzjNPALM*%Ae;^Qs1l~FK+|$qRsN|}o@}?<83CG?@d`t1Z_+Dz+??ajN zAzc*7AOOjMdh|=sf7}JVGc_Qs(?@P1wuKqz8Uf%h7=F6fB5!2K1!84dz0r5@_q`Tg zKV6T+P^>_&8~f&dFE)?)Ce*Zx>W%w*Lzi4abmZu5E|V;QNJ%Ve@q0zNsDB_S3e^BA zU(u{t!$;>agd8!n^oc?t>aAedN_dHaY5)3{Uf~oaEJPp_f{sYdf*{3j2|7(i_b)7d z=TL{oZ>{NUQlrPj=77FrQ+V&*@Xjb@`C3)XQ?31xD)APh)6}Q68;2V`hc*@UEBuox zTlH|(c`!qOb0d8aiH(_YfZVDelMn*=gu~4OyMo9Hpo9!Uiv^rV&pKeQIpaon$rIEU z3F=K$`uU-DPnTzx#}?3(U4E82q_ss#Bf8a{Xxi~Fj~w~(c;d)|ctm*+9a^Vcr>tuh z2WzOFL1M?^S?L*sdO}m|dSsyh*mWaSK%?tjHi#$pkONqG{gDf z{Ic+o#Q8s^uX*eN0!i85AlrAU#L07|_*Yh&{oI5s zj)FOau-xuo6ga(QafzHOR}`CM1XZf=a)OE~bX7Qvnxj#`m24&slhmEhpbscLI^*6h zC|xZ&TF;)rFWo~OFih1emzOTyJyl)0)8C?Eg#revfP|E9S@7`_3OTgl6Jkxhe`7#A z6K0x8s2~3abZ{Na?1;f{!4=V8&+7Ogzk_QdQ1AxR><7&R{Gg3TUjDz9!WhmwC2Qu+ zjra8Y0K*ACA&TmmGO1{R(z8A32}bLOJU;vMruKVG8pQxi*(`y9l+@{Tdx9)YrXO6e zoqwBM_r`*G^7R+pJW1DZ)mng?!Ovv;kqH+D%t9Tk3Y2>^r**6R)*B zT~Uw|uHNGbh$?ChI&`iF#K38JBA`P*DF=tMVwl_v-VpR7-BuV^8uyaCH!>j?PE{F9n%Qb7Vv8B6=dFB2BM7GR{ zcEc+Q^YH2U%EEGERgN6K1$9*X^iu`(sD!!a=+QZra=p^)Ag@-EDTQiUVm-xfIzo-=G<*>X%uS4kWfy%bpO}s5C8cfy6;l8*Jf=Vb`DHR zlctA+dKdI}o&fuV zvCDBq8u*$WFM|;?+}Syrw1x6z-l)+9EOzf$VFI5U?F^m^R0XS+qEDy}p3@Psm+{8^ zLOg(qNwKrEli6g~k~yq(xyIVKD_p6|n>QaIk)Kbm2pfrc>Gv7L9chSNiz*V-f7ae| z=|ol2rfU{{dN4$7g`hL4aVU{gck%7p8_m@lZ&(zM`Blu!y`KcQXpcbOKoMYv0XD4R zR}*r%WA98>ma>7U7qag79r%jTL^Tem;0IhGI1mBKN&5Z&^d$oXxO|~r|MWm-XJER& zms(B8gO5DGwHR#-n0CK7xIXmI-PbT+imtOpotdJ0rf7L&5&eo_tFoLD95^tr?F|6a z3JKI(sV7nF9P(@%!1vq%b9FC}OTp5pL2to)5|;YR@&ibd(|G_M%;961r>_8>vB95b zN@BPxHS<30*J2HUKQ17E`v7(cy8IzbKAN^V0b0g}Rw;@r)xz_>iuX0;<@Oj1(Uidc z*lEm%)*}z+h>txfl0H3k{d%0Za@Y zopSk=T|r-?2OxX;?fLr<_cZ%7kw@l)Y6jQ?4jlrx!sqx;ebm+3RPVkd;R-}*$G3R> zZ7mas4>M+kk)XcV?Y`BMbS30g^&%_cUY&k=-leM->cP+`HmqL0Q9otNMwkzT^P2X{ zBT%nw29F@qBRLLfXqLmgTlOJ+ae@{j`c~tc4M}1Fpmksmoafak2pEDpMxFcQlkXTg z$%MDa24y@lmpqYr;f337d+f0k%02&W4CS8 (6U+Qixi0=^VeCJUJ{XlDxEsMw4) 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�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

WUoE^TaL!!l9mVE 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

v`8pY-)|;yEf`PN?biv-1hBDQgWM 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

  • X)wHUWol$(kfKfV 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ⅆo|lAqn&j0Y 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�%soU~h zVd=UnivWySdq+56=~`&L;$f}4P9{f#54r)%eS9OiTGtZ!Y5u(PLT#<(SL@d}oJ$1K znx58qp6ji8?ic`JO!H9<>Zfjl!Kcq$&a~T3T^kA`i(Z_uXKd+#ph2B<*SW|*PMCoE zf}V5^Hn{4|9UaMmSYvmpFRtz439G5sOYM;h56}=n;GC-*m$dI{fpi83_ANaQIOk)a zyYf&M?4hM_nyzADMNVIRtl=wKp59K)h9zb-(A$MWH5IM=na6Ax;GQj6G3EB^A2w*i zCTCdX%t+cS_!RVkV!>ryIekg}t|!z>59dDN{-ADz3O($8)VrM|z;%7*o2c6Xb@qAb+HiP~hzB=YTe7@n_e|Iu5U9LGHlS6h zrH){iKIi8o&FWsO#lemAc~E<{Kkk@bFdR!x1YV>*gyz$%%CQnn2{_c%sD11jRs3kM zrm#O`iS8(F-Yz8%`>@XA!F=44tSecgm8ozq$YBaNa>bl=bGE|y(|mV&P?|BKq%%Y? z5(!ZPZ6koNbe{)pIS|gd&-!+W%5`F{2)+kHfu))4a-E48nM(?xBM{z&=1L0vqMEFb zeZSY`b{hm~t!c19HJeP@}V9L(l?2 zpI*)Wnmc;V-Mrvpx1Y>hlJ|RBw|5{%I?&$i*VUGm`RUQqmzbGwpe6%1@&U)VPz{%?`WeQC+jqkSc8K4 z>mio|)*>r3TRAavCnun4cS8!e%4hYb`h9{rN6(Kc0njH=Su_itwaKbC<33p z2*;Pa>Ue20Z%M>n^}*9mKYbQw#t;*36YJ2}(@$UN6M4iYVfl5ro0s6a0e+Ed;6-W; zE7^)LS6+>HH>xhVJ+F)g=ka&|>A0p!e+wIb3-j>Daf6nVs`W&iA#c)Qw)E7x-Sy~#pxKae&h>f|&GW); z`6|EI?IMqcV>aoctHe$+L9Ot`6w80CN#uyP7rW|ub@;q6pwn>EVp)CI73`)?_?q!o z2@-lOH&q+)I4$Y%t80C^guk(*oBTqZ%2@p|#WM6VN3BEk7Yglw_~{JUeU|o%VfSSQ zJgcVt&W^OLzGYs*t6m@6?(#@VzHCdHB#V8`$$WUQM3TK@ZPX3@iS*+9emI@w!s^Gy zxp8|k3b?IHe8r30Zsx_heF9&^Yl{xtM;7LFknX}{FUd*uFDe8*L(PB-4Y`^;Cdg)l>5LyUp%lYT0MH&g0`SiTl0#4QLpIa}R}q0dgen1TYS}bk6utS9@xf&D z%gkzA)czIq2@;p^dNnks{KPQK`TLDi@(0Wftu@}rde+MF%>4p!8~UWv8m11Q4b*S+ zH}aK~z`C(0nBcx!PoR40T(pxqm)Oxn4iA$}iM+1c5YY}x*XrWw>U407`YM_9`ER*p z_=VQ+urBB1Q%3+L$Y-H8|M}1USRWuK1COS@e;IT`Bjjx`hGr3>q%r7-h9N~6E!!a7 zz$T1UWLOn=ixztLu@%n638iqi}YZJ;20ebcL zi0aNg9rc$kkllAsi$dj9$otu6m!g@UAXLs8qxw2-JsV{-X--g4@FH`}1x{i0M7lE7~kaCrip3nzb5h0n;>3*HU`vOkWIa9`-V5 zRAQPtFmupU0Zsub#7KbS1lnMo3seIAI*XDAvd3+HOkHrznn3+p z>IiII_FJeeUw!3EOizQ%6aaJVILs{Q>3+okU2Eup=^5z!g*}Zw^ohXZ#sy!@7Z7IS zKn4Fyup>+|ypkCw=^qatWl|aNZd!HGmdO!k*oaO}z@)*U zZ|M%T>Dxop1GnFvoQ(I^tI4ML#rxhii2xtpyroX$;d*h@0{3OiQJU zU=mki7(EV6R3!dsbORMu${;{AX+V@{CkV89rGUT~4SKQbY#s~7r6r83&Nx!QF{eFs z2F#C@U#ptA3VI4IM^9?Ff}#1Uv7e@3ibx}oWyE@#ies`jlzq zJiB4Jk0kBY3SMK_n$Zr0Nsn7*ZX~Ny9!t*W_BNV(m61{Zq-8oG8?uzIMXykoklvWi zj@;D5cR9Zgu(H)fSx}l$>*z(_(2i-`dS3vFE_e>%c^X%v2C_R@x;qwQ#7)iFHfPtI zJ*Trgvj3dVVE-WOp$mC5U(w>n2pruNm?W9tx-!3y&tNr&8((k)fKMv1KE$pl9?3bA zeZ{`#Y~}Y8^N7Vh)KaeRw3A5C-DfX$$`ykR(@C=4=ANtHy4&5rOCL=LJeg(7bm0i> z6@$h!PeksQ{4dWhjYxxhDzG~2P&A4%Y;l*mpcoLKqvBY#G+awcZ)YV4R<$>AAmmFb z4O;=1Y@@k3W)H~9A7`uySBf}lXxGFWbJSW--_q9i{-hDiKbuU1rt9((fto)oYYe;A zWirdy+;ok;HYMP1$mo=QpVu7wdPASqxgzqaZG%_g*<`<9+L07z_>pUUf1j&NAR^i> z>*DAvx(Z!o&JEmznngcs}0_-RsCh2gEy&m$D zgAiwJ0G$AWjKv?{pk0Bs15Otk55NDQ6%$B48(aa^*&q-qc&SPS?hl&|;o(w{Kd2Ny zEr}K$JS+xZ$z1%|MsZ?1^i?DtjUs#A9cuFqNBmNq@!PMeJVimAr@qd_by|EaWl20A z(E0gFfy>vcioysE!E^dW3u?mP)c;M*&WOrZKn>TcZT>9{*Thd?zX7aSJzg* z=;7Uoru>oa@B9yW(JV7({q5DcJZz8!J6_=fa*j^m$=B4P1HR06Rc2NV-!H_IoA~sS6h1ZdCdSE#~ni@imz3DWyD|OLr;0(x$Pr{Xa%4aAXQrmADqEzU zYe-nko@A3I#i>SDv8qt2Wtcl;F$)Qz8sQ!q6sP`S@*|z!O2DjQ`2udAIIHaBqUV1@ z>>05{eC>_<9&S!1n;+hH=WcbUEwR`c_W67xfk1#-=5#v^&d^70b0j&QmPceWVhG96dKHa+R4{u+_DeDA!b6NZ+UeC97;h6TrzdOF93k?*jKW) z7p?X{m*ET@mNxv0;VE{IptmGp!;|ZbfX-GJAO?iQIxrm&wo+jr{-kq;+RSm0yFx-I7aWDQ}BBP>}gmph&*vj9yau5}~-GYbBx z#~!1Oblbu`HsYs2QdFA``BNz=LJ5S}xu&v8Gn|&xRS|0z4`}asK`_{yb#8UkTJ{Kv zOHIYa-%_{cLZmr>1R-}kWR^E3A;jx%vFjg>zE!R9y5`lRBK1gIiBNxkq`&{|vm*BO zBvqT*($subDv;lQ-9}p>Yp%cOmtAO(OPjfK#Dw1S&m(ZurBX0Iig>s+sI=SCwmGnQxjoa+a zI_iL7BII73C;CcTC)8&jw3shDPdBqCPrZXoc?8I<1ogLYz}F1LWRX8(A4d;xW#(QF zjHbH+ea7-1xg*+wc9chKfn=t&U2~kBlwX*kUQQ%*NR*)-8ElENqv91Zr_m{Co8(8V zjs~q$+2cx1hNu%u=jv0=)jh2%gIU|?*1kkGk;tYb<{9X0T|-J;T1zC&2vyJAgn9-Z+vF^j6^ zU`k;>h$bSC53h5_3J?StQVcd*4QN!FrnG;B2WUY7fB~wk$owy(o&eVs&mp!tr>FAe zdj`5(n?k5JZ*oph|Lj)}bf?=kX1v)81_kCZrxsLS5>>JMi3?0l9)j&MPFsjGz`t zsn>k1_fH_h?whG4M*I8zPLqS*Q=D)JTuE>6*~V@!PtNTy5_bux-;Hc_ZM5&~J!n`y zyvL&WIE^M?7;=ca`(x@_Sse@rc1Y269}_i$OXGo4FQ-o{;9v&;ODlb zNBi}|>d{rNHnc=|Qck0p>dPkc8>71s6ykoIO0JOT_-oM$>V1Pr7=9|5)Rx|D5UU|4 z;!5D=YmB@31n^=XNWTu`9(P>h>@iewfb~FK(zgU-7Z^3ERDqEOO5#b=*}dnuZxQGQ z1f<27(Y8SEA81<_kR9_Posxi4u>TE9Jh(XD-+XiGsnqe~si!FaaM~k_v{k((*P|!6 ztSWUPI4J11T3{DC=NBTretyWXj#TG;`i02{p)Q2_s14SjIoWIPMBk>VvmbFT&^sG_ zF~W$qpO+sn=o8ukf0)ba>m);Pke!{K&kjbyQeS-)^^Do|2S*^LN{Vgv+9N8xxHh5> zHX8Lobu=(+6kZj3#%f8}hqACK_UmJr%pn^9LZ_@gPi8ZB^3DlZe;WW{#`Jp*dU8g9 z_i#5SMwlALDB9Tfo+TlW4}08jquC73RJ0u^M`p(-y(Pi{v)8z72Ruhi%KuC(;w;gu zo&+vt1-C!@;Dfn)@BQ@-b=S{jyc|z)dvAF)-))Bd7%27q92N)FQXjqYIMIH&atU?a zt+%4z%v(166YbtxGxBh7DSwq-QwUpEEUfFqJCA$>7T7qi&xxEz}wiJ6Q5-oAmQKHzpFn zp)^kOfmJENjKs}QpOArn*a`mf5Z%+yBpk4_L0e_aXtPH6-?6zc?eM#1Z$MXEh-G|9 z3%1cTRk0IOnbjs(g=?=ZJXAP#tnd)^)nb=>AWbZ}eyWh#yU;YWH}e%Ty_Crmw&X9% zPfSpsZb8p{0|3TUa(`2~&@+>tD6I|qL8 zt8i~1ZDSZ*OaBZB1_4bX&5K^v%_y9c@if-*{Dh{dpV!w}o~t3oU2G@9zda znXY7RxL(^bLQJ)pFZ!Mi}}D)tm)3wkD?D}s49 z^dd3BrD7f=VS=)}4VnM;H|mS)t*(GAY`RCCl7tkIo2ZXI{+Qa@(BPhM*DWDkyga*^o%>1EBmrBnqDT#4AQ+o2sB5X1}GjEHZoL zXUov1i9}1HA>j|(+(osnU1>4o;c3fd^|@e=i+UCMFzSociG@yRe#j<=+{y5=uJ%x? zEjwPS<;F9+RLk2rQhg>9$R$#|=J%S^7~1GwrFEG#2z#wCKj9o0&Ku_;-oWTF1<0f> zq-SnsW}^)im_$rddb05ZD*NR6(krB z6`Nk>r=s0PI=5^9IP$Mo1zg?@pBNN~`Wmq}!5XzY?!wck7VkLc)7+$UMUt?bcOK7R z>gP1cCDm74(e*eIFQslY``Xw=UF{_`_KiJ{BGGbfpNK#^sQpuR{i1jBgf~xbEP<^h z#Dx>~sAb_~ts<_4706wzINM=%Kb7o?4F!Brt;r+`R*Plgs4^dmb#-{H0l;4uCkTN) z1uI-(88Azy7E;U5VqOh(tQtkA7hTtqV9Js>wg`X!4EWY~m#4lm5itUJ#Kjd1q;ux% zo~0)Z2~{aCff2-v1#TVIMc_t5Ad6d2Xj?#g+PEc7rNaI;X9QVC;;TeI`l4toSJ>8bjWDSO~gL&g+LnnNRMNiD8PAV6n^1BYq#*b8|nsKrczi z@Q;!q8F@$>*U9len}wHY2Y-! z)FgILspyC4^oOat;Q)T-L2cjx&ELIz0W{?oWMsBwlG*}W zdEzs*y5MVdX9qKljhVsLCH7VrHxEz`qZd;1LWig~8wR0?ez4~(z_jd^a_bG=gC{DTYMFVy&YfCBWM4&I>Xj7i5Anc~nT;DW&j7mE&R5F{HjB4vLTe;LnU4RO2HAOn`0kpyS**e5u!*l5s?V3tVIs21*q05wT(fDV`ndbX4MH zx?d5Om`F%;fhNFG%rwKgR27mw(;-6DnCY<32Iv=eQo?%w3fAhy6FPKWG8ic`k@3m?B17j0llX*X_Fe|=Jbxt`ES3Cq-bbUdWO3H{LGH@IqCB6&>dX) zfIR4KkxzFfVhf!A%Wj}P(}MIMis#|G{2t7Te z!1%@tHfJxy7|Z5>#YPZPoi@h*iWQ+$VQdJ@en2OBOA_Pf;TC?lVW=V|yXXQMgVtly z!V7|Y#)`wI!I#D!QVb@Uu}96iH7oJDy98ZU<OsjZ8&D}-9p_vtZ`{fMM#3fN!0jgEC0J;-nt<~`bZ#%>~`6KUdM{RSsyIn4nCg{Mpg5Q{q9wm>r0 zoOYM6;v}JfN`D#6whoYY9J|5)j&2*0cEyuYDf(=|(9o*#itVGLT$`)IFkkP=EcmRW zBi0u0?4(S@+`QD$F@(4s9oyS>bas9x6kMY#O?H{5Vgow0eLRG&*|jTqjXWlrI(#^} zb!+)ic~M&64OhR$=GCcT``TrLGYW^9T$fmvOu}8vFV^=dqJcimI{Kgj9fN}>-4jBg8tcpnKli$S*nCO9~a_!%%At#&cGD!fShad{wbfN4lptTUTl3RT4>4u$qEm2bKa7V+Pa@Ifi90)EYQI(FcLO4ZsWnko_gnpFctLk` zi#(bNerCT6o5TpG{2+}77yux_gsaI8|&qzwm@KmTnu zhV=D>ig~oT00MohZm7U(t462l>TYZ;c|3a}b1haPoZRu$E?cqrDnPZT zK0W8yOi`1Xw`t<8joS3??(`z$`uKmc-4_ii_|ERM{qduFPKdTWHf&Qy-4=tg551fp zvK70i*He?8)^7Wb9al_GXKJXcYfZ?j-?$6&EgyWO2Y_!IARD2fr@=A*HbQ)fJ%P>G zkH^dk3blgor63`o0p5fiirDTjzWM3;AW%?+=fhM$v5Kos!WFgu(%frStM_$?`T$npQr1M`` zaLzrPHpMO*ATBfytNof@>)&gTDv|iMTBoThq~yFmx295)ZB4MSpshaS_ZQlY4nsh* zs#x?CuHIk^#>4)6sWwKvlMZxyvW}IFU!(QHnpCw;k`xM6311|q<$tzG3931EyR2yR znwnIjnih}8+i9YG)5jo&>Y_U^7DL8-EpYdCsEyBZIM{ORz(b4DY)deZ>fZt9XufyB z)cFtet0LwClSR7=l|D0^mW+}N5LcyMjuRg^*n-cU_WJP?eBoFpxIBSFrwu3xf5I)K zz9Q91tGi8-5K5M6!-jQRR>Cor^%;2twfrXs4rF)k{24oqSm@DT5ga+`BxBt*HFC9r zD=aTr=5y$Vt;A%`;S40oP5N<-=aQNA>u)iz+ax5XMf~`Bd_35bNQQ!`DprZ~3PWhA z1CZVh){8yI8 zlnzdT`>@IYMNFsMfI^^Mz^vicN(bEIM@%U2ukQd&9mYDn8r(FTiet2OrTqmRxs;R1 zMpJS2j&pbSYUAfR)1C_w#_=PE2<4HBF3KD@@Z(8+{Gcgoyf|T`p75k?uFfp==XKgV z5!kM8rq1h1Idkoo=vHMoebQ5%n;ZSPg@uBAYwmaX<+APh-{rQ-AIlvneDX=*NbZ&V z+S+sSuRP=~}A}a-cOGE;PNyp?<1Tz&2=*c|7az82FoQ^n5+upEg^0&CoeE zW-kEkievwXu{4Yp0GWoiMLGf>lQ{8%Israt!n(4WsL69J3FI^J5bwCiX1K_lP^9WKy(*XOC^vT%>8^?^m7NEM|b+FO&N$75d zkAvd_>_#^*1FL{{uE1yUqgme(2?qUHaqXj9#O&IDLFKdhh{gNVV!z5166n)eT~^AW zqOK(dM{oz)1c~fVbJQ(c#PeNOoRCP1Vga&JFZw5Mrw+klUiS&)z2*29_Y=fBduzGh zJyKl@lG31Ps2$-EQ)lgzz+M8aP^(Ux8;cK;PJ;%}Zd3A=(*ja)o@GE8w=Sg&{n@>H zvsR}Cwb=u;ZY^~x@9%%qIIKxtUWgq!|yG56})6>stwJ{y)Ii;d2;=pG{j^kYLE_UcYBx z@PKM_Ph07R`*ek{Vyoxq=5lRXICgsX1n==W} z>(?*8fQ(1F%)QH(?=%VBn!F&-ML#3!>T{%(O!#s}KV*X!py?VlbZWAv;>*z`)SBh- z5RW`Uk@CIaQy?1RO6^fht zb4bB;rwxK^xTz`J9H-v&+ZJddno;`>o`Y(ZxscXNy{;HEs*S_)vkMC(!wvV?XTlQS zy5I|K(|p&o3Fs-M=bo>Exxq8@?|32$?{CKw2(x>lu+EzY6sF~vo`eTpqI*8z4_4@m zL6}7n!4q3p-J{C8urrO{4TWT^10e8&kINsk`*QO06=n0Q@HVsmX>G>D;^w9 z#fA;kU5~(=D)cE057#&srV8z_K<-{@ZXj5>7p`RvDYxO>28jhZtk=v`4mvOPOYqCu z>1}d&cA3eH;rN5W_s7NG(?W=UhXQ_v!scQOBm#{N@d@2>2#eI{Ukp`tM%&LKeZzCC z^D)`rW7v3N_5Uv$h`{N?0f_5|G(%5ZNZomO)25v}Qx{yYdiAHh5tnt04Z0i=T8GkZ zN)T0bou1Y#3+0e9&kfElb2oG0`akt1yXiE}6zt@ATOP~*q z2i8LY*$5-_wBwy?vpq?$kqjpeX&H4?r~|?b=$B?Q#&ob9bni9h zHQE`$TnRVU0L4DZs}(gsV}4^{ncXwrOx!}9O+9y~!@nC1p$n*!s_Mh!?PLd6LGV+j zQautuR$VSn=WDz9#MP&$yKlc8b)71Idpq$lbua4F*?qNkvNhO`n?8Qh+pFB0@=Qs3 zdv7QTsB_|Rv|CXcq_!s#==^}Ywi!_eWkDnjT{kd*1}?&P10Z%lhZNmsTmo;@NJm?3 zp#O)VwuxulHk`rhSE~N+`pIm{04xF|6lgUDFVP|jOo!PE#;jD#5>pprBETlY%TVT{ zxe_pD8ImP2xxp}0{vIi_0&VMslJi zF6x2krwoXEsSh>D=o$(0}`!^XDAL z`9-=%75946`bvT290D>rD^o$4$HV$yRGADJCO)GDfL%$I>i=4wKx+uc50aOVP_KBC zXLMxcn_#H`gUXE^X=cH~4|5M1s2hw-n{h%tOC?}nTkTZ|EuDGM>__Z@4 z2QOkZEUz(hLVRP*!73LgB(POqmQ8w#Ny6bWbyfFAC#t8#TCGloh!Bfq@=?$C%R>GJ zL7maazvGT%KXt1{mJ+9UZS1Sr;aWd0A|hI<2(DJAb!yt9X{X=Cznd%J2swyF^gXua z%5z#R^B%n6nuH#1pN~2V2^ay#bpoK;K!yAyMcp43)IERPZxUGL`mRbQg>Sg(A zu=6Wn?R&v&6dJs@dfL_my$-UFX?Pa+p~&T=ks2Nw(mmO z^QpVELm6&+)ee)aB+V`2pyzk(BF~3<;69%Ipb#DT~cFGpQ zFH?Vo@F`|&Q{Jg;R3-JzGSJyM*ok%$>Wv!k{oVB36-MqYK@@+Jt~@_*!0atg#dd^YcP_QBuqk}|E%-C)G{EhfUf{Af^j-#eKTw= zAfo~IE~rp0od_r78uXK15_q`fifEJAF;Yx70DBGuCT;wWg7*zM+eIiawgO{0XylCa%2N zZXYx3z@^+3P!0J=Q>#I;QVsj62Mx82NUHwvmLA2nuAfC#`$f@Bfj8^72%0z4%=B<{ zhvnFY@ByR#m(brE1z%VIRruK$J_cU#81OXXt%242|A$h~@N=c$0SXOA77&)HB$foj zzVuaOdQD(BA1;NI+}F|Vv<=$Ps{jDkkN8r*dx}2-s8cWPyc&IYjJoUU`;L{>)@{|k z>>~7>fwY7Q+8|r>q|WBA)eTaI=Qh`>ONx~%5pTtc)zr>zey&@T5TSsKKov%iSneZSfo&h zWFuA7(@`l3KqnE#M1BtDoEw0H@kBX}wdko@;885o_!m}C)^N#!jv)TM@CN8lOpy)$ zWta`%qe08lAEucP%Q$92q`;Bw>Bsr5!kwW_p` zW(iS?m)pZFC880@sgLC0z-I1@$;6{xM!~(*ar996xZb1JvvTsHjJQ{6ir73No7So_ zr|br+O@I8_RjaOr1yJ^=S{4YT0x9?}Kt@etWH3Gcg-+o%1!Q`&N@kMVjCzAISTTo?)u)+}9?%Cz84?9}x`$B`kXcAzwyY$dYM9U#!`i}BI#$Tw z{}Br6s~LfXWQo-PSm2EQJb98dn)N%|Zash1s`N6IG8xWo|L*y^l5fmOoU8Mhmy|EE z7L+_0$GgUl9(xUt^_QPKNwpfF;#ylL@2$Qi@#3*1OHlYE@vD5^)i2@tOY;AXMXBCQ z290Pm6pWVBv>@k9=dXX>M4)=!HqW5$G>SIbT=wOg~sgU^~I5FqXA=$r3Evcpe<%Ph*=E1M1}~8kX7ALC=0-VD~d*N}P^Dpg4>xNdFbs zj&TBU#SUlYX*-7PR;8B`-06xUK4Y;kHxq*Ee!X`7{P_0x!i5{2`7w2%9tAaVyDw-f z$(F5-4-IXkejdog^B%&meS7(?PET+vdSWFKtS-N3GJDtsL0UwpSFVh&rp|S*@*4fC zJxJ5E)Udb>JvKyQXk2h%u!Rb;)J^;)(nXijOFzp>@OUIEkbxN1Fm%DK_}u4X{Lg- z9cpoL2hXu4?JT=j^PEyGST-;zDlWbRV25fB*FY(HUwhB=n0*3z)b4Giw7L1 z*%#yGR{R1S^kVlLITC3V$sMpd<00z^(lsVX-aNO%-YzCR)LO^cj?wmJVb(9mBcaeC z@`wWLZ9G38u+iCQld!=njMaYkm2yC+;(Fsn3sU5WZCL={e|*3m3~ZquT}mL+i!Wwg ze)(EQ`F-P5p4-Ry+m$a}K^V;Soyo!BuxH5aE=C(tG57qg{(+>s-|K^FL_)X4vNG0I zZ&puhWJ{IWCbdT0Bbq1g5o*QVVuNy_N!+1+PTC=mbK>Ew{*lWr8>gOL6|)wq1l1&W z;_0TQr^jo8f8kP>N!2K94Pg%iOc3rdd5oUZX0}L@;45I1{~dGAo3nq;VYm(!7LJv1 zBQS|OMrdhg&te)VlQ2qA>Fi`emL$oh-vX5je5EiK&jvWyWI=wAzJRk=3M7QYR*be} zHEfdpEK_@>^)&rv71JaE>OS8j)948}ktos^6Uog@fxfwb|+Qbe=rCG+ zJx_R$`h6Y)@C>!K4(%U&Ih?4Cx?73CQ>UmCFx@IRb*j8cr(m^TELn z2D1)-gZ<3`(jFgSHH^pIja_$HTQZq&nlhk2mG`k!*$1fKKZ^ePzyrx*qr#mTZ<|Mb zb9)>R^OtJ=Ad;z3O`CUQ<9aA@*9p9%b16Y?c`kZc$pm`QpbJl%6okl+slERP& zt*9LIS7O~hTgILR>LNwSH?qQ{Bg5ISk<>nv3rEgZ<$c~=Gk~6 zXPdUvW4dlrI2B!X6nq1*rB&ktnE2W?)bB5to^BS%Vr~)0XD+sQ@z_}T99dBwE_l&F zrNgT=Q6H%6UYX~?W>_MZui-We)8Yo2Zb=%Nrq^4jj}p=VLV4g-T-W{|y1uHd_!V%3kY z7nH#yF%XR()Le@1%cgf(h^;G$i}j-9Yh1Oxr0 zOSuO-7jyrQ4sG0c;dOGKAgPY5gq^Sp zB`IiPE|x#qU~(0PHfi1Kf?VqJ*tk14I9#7778CVwPyI|MZLLmmduq@@G9r-G@LANG zu!twjw?3=pgla`LXx?E(s<=e7|H2;VGK-NJY7<)Qrd}_6_+jB#{#ZV^$tkPb3=fW> z2ZCqAs=NJ}8BH+WrwQi<)YJ>{dBNo11k^$|+_DjlA_q1ksKr{8)Ezuo8_cS#g2#iK zJiMNBUp+fhv!t`iI4yUw#ukE3&%wUHd!Q$t4Y?92OqAiQEbbA@(zVWU$Vf7ixG?F& zNB{;+!8{H+n!cku36y{vtg!s+?30nelj6)U?yNb0_7OG=sKEpe=yt5`u~(|XF&G_$ z{kFO2H7BK}QB^E13QAMxXH@G20|UGI&KVpmKW*>Oy8H|8CThyx5K=z18`h!D7&i6n zQTp;>Ex)?hDBV4A0PVTmm)^m-7pi`5Axrs)UQsVi^9R_(dox>&CL7D0R2ia)O~i$r z*`P5gG0wE^l$tk09=_w;emJP(jy6rmHMSx|DN>=_rqots$x=7?gYqU$tt98rCj#A< zU+#7T<}GgvKLrIc&Ja7HFFR{|Dnb;d+%F^boU)aTI2J zFviv%@PaD#(p629BT3GgwGmj|FdNmua-fR-x13>iY*RqdtFQ+O&OAJ$pJEijMCIQs zxWsJsmwr2#oCJ5QwBLRPN(<4M+{Pdccg#N#yb?Pdi*)Tb9jAJ5dI zKT?rr??YmlpA+CDdT%$fHJl(%CcIMuDWot(3blfGROa!b06LG_0v+J@pt;oD)a#ty z8sPtG!W)9lc(F%t0by1LnWvKy)FG!nY_zdb!_DUCumX0Y*Tk!3 z-11c^Ro0grUR(a|LsnPn^BpVDoIoJyH}%S-iuwNJCTUJuD+?Nx<}4wd|Aovg>vrht zOTza85pT3bOFgfbfGlXKPtp#vNaho@RnIHQ>K9~vydIy`?%TRTDV1spt@A{a&G@<% zVXpNi=;f0@=eQkwR;KQcu~HcOei7J@GriFCg@7srZ6Uy)WMi~(U~XD*RsNiw-lDY> z)>L5AD~20`ZDLtsZgg56Yc2e>AXbb^%UmCO;=ir4LPO?2DyTzIm88xtM3QnzGwgLN zi+`tm2F#$01a0 zZrTaR;fvuqVRWP>s8g;08^0cM_>~MK7!J4=^tcmky0OT>l{vjY0>=%|;=-mc;_MXn z>3EQ~_l&QG?KpT}D)@p~x2^ILOp(FW!zvNycNnWejP%)4BIQHukSHwoUTt<$jjiUW zFf25yBV)F_X`#)j9ka%>@4V~qbfw6kr=9q5yg?@K%=jldQUt%Hz5Ks?r`XjJTx`i4 zwR5U95&ne^qpc|rFG>TKC%J5Ykovw@e6&=e4(UYPj_N#jl#Tv=_&2=1_KaqaLy_Dx zzw5S*euu189&PlURcBe_4ck}RN1Mo`$F;w&!xLLhY3ugPox8WrJN4VE7R=Lz&$@f0 ztJW`DJ?1>?^0(fyN(?Dcebh2N;wbJLcTLb6~*KFH1;t<+h z+$!pNk+vpKqZQt#PN0V!adrPEObcAl7&h6=he!4BZr z4%*=b3lewHiA!2{G5v7ZeZ}4u5ZMB4g|UjA0s6~<$h3yTiI7;7Op-LNBO?sh(Z!bt z{%4S2!V-uhJNPD??I&uIzxYM+YVxI*$g8QhbR#xmje5F|=Kp9ROq6~!(iSR9Wo?5-Dy_#(E5Q^P6`U1L$D zdJuXj-tocj^YMiZ)O}i6y)+BG;Aqw6%?*Ily}3P(n*-u)ZKS$Y8%;En9~%nVbtYrD zCFS;q+jq68{Ew{IKa*E?U9hrchSV-zo9g-FP3ISjVl~Ii(-#uv)VzVgwYAr;9d;X( zJ`efbg;8m^gs7KRMA19((a}*Ezw~y4+<;=5-6I!5dy1I$1#SU9Yj*#F@&{S_FpUS3 z1N}9>;%%vDf9l`dEa2s8u=s4@r#(wbl46(s>u;FUav}cAI!Qv?1Y|VH*ac`DT zmp{09X6A~?D^{(dSaVZe_aX!PLggkY~~ES6+D~fNjm} zaXY60wt?!A!FROSr^>45l~VH@8Ph;ISt!70A^1soz=XLG@C~Feq3NN!1Mn8iGn!-? z_ecTj&Kg#jjG^nVvyEoB>C6R;iP05-V7Q~;2Z6T)(FGAEQ8Zh+!<_<{!)enCdK;y^vk_Mn4^HqV zO-2Fr{&T6vOO0XQ5PJA>^xoCfy_esAb$OR@L52&Ha5`k7-ud9}yNTEQ37dNu5VUxS zlHA18NfQaLM(M8hrya)O<&w-n&t#x0OWlG5<=?pd(j-@{K`O+fzNFR?$9S!MUUE8% zYa$_hw}VSv3UFvXtNXLVu~;A;$GTYzy5Szk#rH$a&N0VFcX(%^0)lhN}N;8PQ4dyc?HYVW02JlRd0_Q`a3%RlroN7h**Sxr#jQg|b4;%b zraE-zFSmJuS097OP%& zQsTCYJk)MIuQMkpRU>6s9T2&tXr0|0a2V?K;t;=9oYRM@#B3t0x2k<6v(vN8E0!Ar z9YIfRNEuT&m8i65vb=&279y@iZ=d(ddRrj4$m~i86y+OGX>K5D9-{u3^mvk<@EdQ0 z|8()ii;M|%z_mg_>eS>)PMCU9UP3%Ab-0vK8C$iNWlO3)ucE~(HG~|Z%{*DNp7A^J z?xUq(MMHEvjA!2Gf%RNG=i)hs=3F}GTF?%sIp3mC0iV;n4(SP+%D5l}RAfW)VYdAi zdiAl3!-lLM*5#yy?P6nR6krW)xb%WWg}ycpco{ALa{}axQ6Hzji7sQ&8CpCoX`vxZ zF)RZOTMj$|-vb%HU)qxnPbbVvNdePXm>XJO+t6wCq&C!hjq|U1z+zf$=4#fNgU#HG zLGnmrRVssGh!V~17a~LThn%g_Ft9BFNPq-t$5 z!Culj|1#D7doriWjEMzZCtZym6@lhS_V5E1j-SXC+}0KmU$K}B1wT! z2)<)6pK$t%$k^-Xv9R;(+G>Fks*%*{u+gp~jV-YruPMQn%C7yrNyO(#*uU3=P0`?f zuh;KA6rQI`2I`_%7V#X`Ly!+20jb10X7JWyj9H4KixGOlW!-;hHdyeGq%SR8L*|Mw zUA0OO8pv5-IjhJWc&3buft96`9B>LAo-w~T>;~(PgF`wuOIEHPT58k#-4P`z6TY}7 z8Biwfarurk+HpH__*Ryph@g!64j-XVNxDbY4~**2Wu5=29lz zotFkxaf8EJE#i_AQ|bvzKH}?Yx+j>t^wMOIt5nxJ1HomX)pV11+&uI|u3v`#@^0p>-S9X;{nCBs)VkAQ8Ah z;K8u1*joO#H0_!E?5uvoPSI(Piq6yG>;;|5z)B3_HYQnuvF|`I=o2J;#c&HQehs=H z;Sv3SIOyhusyK=8f(3njlhpS!lamFWbRe_)eEvm88&qPwLKg6(_#u;7SXG*D3{P5F zHR%S8wL`}hxZ_%Hm*Lzcfk?3J9IH$z$R`{s19giu7;L<;I$Rs#h}o|HF_=8kz_)tn zKh;n|tv}fA3u(h1GfxYU?YtDHP<2RO9RT>U`YQC7t1qic=We?#bw~OXBwy2a6y|<@ zU2|&a=3`>@%;8W>5fZzc`S3tP&^8g$YjUB0vShLw-1dO3(-Z#~j(AuV|9^CS2Yl4! z{Xft5a=BcR8@avr-g~dyW$#Hs21y|7A$!Z-8!VtGf&u~xf_p0pYDKM9t;IU4oz~V$ zr`3G@f1d9pfbH*pye=`xCEoLSKJ)!qp_ zfed*^s6(@TdzVks68n823!t2Nst~qa{ za>&?nErnXSHP~aKOg(1kv5Tb|Ti}Z^4{FTig%{)<;=Et`ql+Bn4F3PGk{S#e%vZ@5 z%~6sCE(1>KUE=3Nepi}K9R1+Ak%LbMw{8t?-1ywqt$SyCZ+U0cD)Mja&ZMxGLCCgjA-6Z(+CZ#U z_Q`G0?u_4`y!T!Zfw>v{xL?;Hhs}@V9_OSTzEu&YA+ju<-zVo6<_4Qh=LBOX9=`t0Jmn`M+D1gE$_@iymAFn4!@p4aelT z?i#2Hasp3r;dQ?NHLM~KlV-4aN_|oeQCy2e4wKjF;JYRY*+H(Tp+yrEa8MI@yWU|d zB|j3^c~#L;;^(9_SXg7=75N3IB1gc2zIMBz-rm$y--^;1BAX)`Ru+xu{UO(Es2i({ za{U}sVVtNnXorl5(blS$wSNI* zqA>Xur{a@J@?L3^)}(8fK9a0(h|8T|A2?ti9;fYup8QgXsg=O#`TeXZ=*GJhY7)0W zM0eY`JK%n9p4Lk#7ctmrP$p1Fh{=58zoC9qkAOv~M8BDm5-LCg82~zCnqvvTw+fU8 z#Y9~Z#1(llx$zRP{-dv07r?FW1AB#!KCVv4%GZMd_td4oy>pUy#) zTe{V2<}A4S%Q?Kc@B9>1yuIN459aXZ{QL_1=gEWImpJlGErj;Fm`Sb@59`wQYr^JNb$<;oVe6FvynYF`L&5POuY0PlA}s;-2#zURH8DNgBBe^niwfcd#xNsxqM1EW$#j<*5cBMLZ@Qp0%#SP zx|n>nq(rZZvPsXa7H@NbyW8pPM(S|>x>@}pnNYAp8WobHEHgjgA#LhL8S-Gnu zY_3G_4^ zN_GuWXgM14YJy#hKi74*ViIp&d}nE_$gax6jAc@gOT0Vr zNx&H~j-StvXh0$-L9HX6Q@WHwwel8-4h_d(gP?s|8-#T=lyjU%-@#J@iswBzJVaVd z2_FEDW*nQtE!}wHE5=__SDn9d8hoM(cd&NEQj2SK@Xtli=q!Fq-l=ppWKn?sDN%Nf z2eZ~&2pidlPT=RrxqgSWDMdWhD@plc%LJPbW$lUk3cliTN>O$J@7V?97w;Z9a_-!w zZ-S2ozu9!|+?{v+e$O5h-@A8YyR6K`O>-hlg7YXm+G~F4uo3gM{Ypnu!)*84Z@X=_ z2W&QrgZyT?-QHCGAB)x~=5xc6EYy!=&F+M~kj=D8lp%f0VzL^nhQ*5w_zC}MLL6}0 zW)@bz+rW){=i|Y5+H4LODGBp&6EHXWG%fw3o)q0^#M(PrKZd(yah>)Ch>P;ssXSbQ zWdLY5mg57`=Vgg1wzhLH?1k zeBf-6FYUO9L>ZMm`E;m5e)2R*>qPnqz1lJu;{??Wq^xQ|Hj5;c3v)SFWu+@e)>l?8^iws zVN7o^L|A#akYafdmAoB-ude%Q`A&XHNb*h|qDi;~9a!{lR;{V{s%K&<%* zMt{$vYi7`z4v!m01AT>im_$&ejq~czQBEoQ|IZX3fNn!!14WiGFArP@2IUXXsBgpH z;ue*dL_$+3moxGwW7O}_5(i3ITuw;=1CeKs3EX{4gxTh_p>tX|jW*k#q7}Uv42Rv( zXU)?kdYxA5J+c)1($ibsoZ;?%bPG&Ane=RXWr|lXGTtuq^eqTAhILWv(5Wr^_d5g& z-G?OxRMas|-!}*wIcuR7@)+cJ$^gOAK%F_v1!R~FR1*XqvdaL?J60VAh#znhHMHn@ z(?tGA*v$z*+m#=%3qgMJX&8O@+QVYBGlMLTJt(;MNgo<4UB`ARhg2iK=FC&t>NxjKRQVe;5+R4Yk;IQ|20cWJ!^6E+??6 ztTGX)s*)xZTxYQ>)Qc{B`f29(zrToHcwyvGPg#QLEJYj3-8%*Dvklu?TwVK)t%AL1 zf7J28pMcMdcw7#Ea~=!;$nWvmYu>KhHT&o^pBw zsbYl+)uivXu4|f+%BptzWfPUZk#QqGgWPKY%u#rWwi|W_Xd!mQdrf~7l|Z}zsuIqzd_pKNP)75K}^Z)WfKpLY=b49<;Bn(ubsR&JUi*A zTfS`=jkFPeI{I?gl;OsEZR@HxR#`iDA76lGkYDgrB_R&^zVwX{n|IFZ>Ys-mLX%+& z9iBg$1N`9_jR(k`0BW(ZYyyynMxAy8RniYFE7)m(v?l&EmUSHK_Mxl|6j6IrnSH;6 z&VUW%a$1hOoIWt$y5Q&k;B>Q6%8rpKsx&*r&q&eFfXPxE8+%uoBFB&v6K^|7HQM{Y z`pk(}v)NniQ<_t4`^h_{Mem%|8|IDur9RKw0yFWY8mn1is#1fNcGOT_k ze^-Kl&*GJt|VIDe#7vN9| z$5hqVULuES4)O!FH+A$qnq%3Af{t??N&m|r=qc;}hSGV@e}!#LGUuB?H6 z$BaZFL0^yGl}^izzk7CCE7#c;Q)SfTmHqadZPEDVmELst&bd{Mv}u}&kSQC~(yE{g zA}&%0K}M3`8{H4HrOnWDmIb62zjN&HvV>O(SE19QRu0(MB9>)TtTHTDKCtfggLHH7i`c*Y(I&$ zE%9$Z>`pt^4~^d#PQ}lxn7x2o)T5uEvFUqtL@gvgCIQc7z)s&!YX%#Qaqa6CsA3;TtpGjGfN5WWk!Z62cAK=L4ROV$IBCh+Z zuohr+z8S+z1rXE+F`R;W`vla8;;VtPLccd4DsWT*1MBwxJ}HN)qyv7W?PC_wL|$h# zE9W#Ib~jy7QRS~x+DpC=%32(o%G5oEu(Vz`*)0!PH`@2}#6go(D}kL*NfbjPUlr|e ztLiubdngj(#d$n~)7rR}WAQTz)I4G-SuxY$;wKCq^a}r>*Y4~$wCRK630q)`XfBPBNu5j8KSzd+!nBqAEQ1dZ{)l2aZ$!FxwUqhJ*90K zuH&0&tT0=alr_uTuJPweU0EMTEtX@jx&Um&dD?BXTF|*J(7Iqij5k|4$`Qg-N6W&1 z(cz3VHjQD-LBTTM81W6z8FvFiv^8oK(N^n1SoYwHMn$o1=VnxlPMha=2Ew}W-G=HK zW8oepr`WgI=KYn)Z)F)^wfXGu$Q!U5Iaq8con=2%v%bMFVcT8H&|LDXVX!w!hbN8S%wlSLvadjI?XpjU!VIZJI9GN6Yu z;7{ELvo~aLK0K+M@>{S{1@^vvkN9kG7>1GZs<1yb+Sv>k$JmK*`9c{0B#2ogAf{>j zvZ^9h0XOCIMHAALMdM#ZwAa)m$$uqtxg=sHM@YmtWMc7yV$9sb;3-p`XP+ zxny2cGcp+!@A1wqOL&}tm^3-wL;i*dSr)d2Sl9jHeDZYa{P`5>nsZ?&+J0qC=KT50 znhzQgr@8*3>5hh-4X%zI_bvqO^!xQtQ5roPWVb)Z#x|V+HvI{holp(*$Gd|Pz+t#1 zf;z*^id0<40N%nP$H#>aW1t5WO6Qp_2(=BPg}cT6Ieb*j1%rN=>QiCldb@JORn-j#55mn5cmFVss8R^^FAj{3(x$x*S&pbn( zl)fk~6&7(qZ%C&-z1%5y`st_P+=WoT@GSVsE#Q5b!T-XZW((NS(dY=GB#fnEi$cXo z_~TgQgaI==cXCYAj7EZ?0PyE^MT4!eNW2$TH7-QE$V(`hK(1-ipmnmFJdF;1e;RED z``SakK+OFHKXgNPuN2?^j88J@#76n?`ykVa(lpv_+E$6JD?oO5dn}d;J(2Z!eBRpH z$(~`4&l3#7hgW=)e58LuQk>9FX%96d)Jc7Iu$@s>NLUmT)h#kppNHYAc51^KICBBq z^)s}4XdMvo;;P0b(5;v+v7dl#2`CK=9c#fDaflmtk3mKF8RZ_($D%XvkWzsN0>=4{Q&GVRrd(*P5wLfvF7kAB*Jf>Dwhfd8lp4MC(7y2b*^` z+1hrUSd3==uvYWpthzqWSEa^(>eCa~&hkc`{WF{8(v;Bc2WZff+SV>{DdDc8aPE_| z0jPh-cZ|g_dI2(c-HD)b*!Yg~TR!ors9A#fs=^!IH>H+LW^>++LP?n}We?a>GgN3N zl9RuMeV#9mV1#YQMEX{wn8BH~=|~`*vQ~}sD-vZXUP1bS9Z<;p0b=FE2Y&p zRlqv=p{iJ->hXT^oz$sx`c&%P(t->_&QJ5$BVYBe(e-Y4#kIjB^KbQhSH#)q6}wmH z1QiEa_FFx0t)PE=KU{%YoL1N_h-dtc!=MXv2VtWC)($(Z`D>z_aq6~4-SN>%J3}7u zxN+swG^0WWW7JmAiiRtl4b$soGUI;M`)Yo%Gi#l)Qn@zzGLxPuDETn*Agxef=?mMN zJ4rTE)~)pTW1NW77YPdzl>%$YWZ@2)r8La4(LP-o@nyC?yb3puHKu0AxI*6VW4Y+i z>+AMj33s5{@~7AG1qWs~!@Bs|jyCN|hq1Y_c5RipdHem_Q5E?`uhPKVBB_?X5McQ) zVhp`-Ts7$44d4q)pau!|w3dV4Fb>gZV>&7n=JPO^kZ103z_y^580MP}8y17(gxMRa za-kHIpD)p(Z785+nukkr{q{s5Gacl%uZ z`wL{n5vH^V8Sr+Fw+|g6J|yoYf76&wvZ212j|v47It&|M;6(l8$!zhEDyP?P@ipzN zw>R%OxdgP?hZ}X-4RPbsvC{CV)PAxYja#=awQ(bkRh)p4;5tkx)pI%xYA``R-*f`% zPC+8E*~CMcv8e$!5#lYZvTxv`@DtEhl&6nVKtK8s^h9D!mj7+|WdYu&a`W%zfBEJY zpd`PU8;8qI$|7-vO*I+1^$i?1r)a3J?EXg>$pV6eeeOjCvao+SYp&VMh1Bn*(!f9q z`RCG;*1gm!gxRzjw zWxV~?zQsdfW97Cbi3*+|i|-@fW7G^?Ex2wV3l{T7vGC}1z-8c!9Nfhy0z|1$yzmpR zXCAen;3teqp_!^Er|6IRW^)n&R}j|9_;gNBD=NxqY{>T2$X9=Q4!!cm$a4ki;*fz& z9P2l2tha_bN~?_0xIpC}Zy)x)%$Zu_sT?X@(Y?NDOM|~|-Q7#jM)K1trzSglsIkG| zMX#n(`;tlWLE{Ut(o4PMCyjySM}8HwBw>>KvDSFZHr_p8y(>BK;YM@rcndVW(Z>ZS zpL{&bw26Xz;i}1Tyqk0e_BAR03^)pyD-U>$wkqI&1^5v>Hf&k+SjI+W4VO;fY_S+f zM3xv^q#j8pH{tfTBvW-9S80!q*lTBH$X{ioMq}e-&4o9moHx;zpRxtWa8cLU`TdI} zc1Db4C?WRwYt>O{eaHw^ri<%c)3kQetm;iQ_LaXogL=u&2PG!na{FsCCb>JEM)L*I zn0F|eNq1y29qCL(ld~!Es+<)$9iC+BE00WcreA*@^a`Hsa18QA1K<~_0qw^ALB0?5 zM%4~wEAw`c62>sTNPga5xQ??@UIX?e;NK|8MmxgtQynN%Votrjr>eESUfy}w6;|`n z3lmq&{(`gj<;M0aJHPDf-`KVbmLhB?m!!!h@aZL#A`hdfX*3l+r8SZSFQ%*-=~M*E z;+i!2z{X2y<#+H!86ltW%k+u>%rDYUcPrK0NV!uejIuQ%iAnf*Okr`6ot5EqI$Rm9 z2qzNZim&xkbcXo+!`7*GS=;P)1`^W21iO5YJD36AAr7+_9-#F>9D=vl!rVNBRRPcd zG!b>9pj2^UtdO!e*ZBeN7kpbvx#AHE40tKr`KT5FC6(V7llMFDvOghuq}bE13syBC z9leOBVY!2|JQlK;knrBStgiDJz< z-q-&|XroT)Z?uKjTr~4F^fw|=?p+#I-O|A>ES1>GjO7PS(UI>;yFEvjxO@I`8kFyw zvyBqoMUN>qqYFuGUvg!?kv!i#Lz&hNS7(PU3Cr}l6tl#gOgS{OR&wmd0MO=9Xz+ilW{`>-wZagEjrmSM2Mm!>C?-_0z<4{M5c3bn5Tz=+ zQS_%XG%4NW>daJIxw>vbG*&H~(%^APs$~WCR#&0|`%Ty%eimZSS~!OZvWz+4X;4v= zzP^Dc>7oA_SQ|4fylLR;u-J%^O9&0FD{)Zpu&@WC6bvD)jocc0_sutxAAb08eB=?L zXrJ#EZp0Ca#)QclA35R;`)EVgJemH|UW@E6e_2QV5zW1e;LScY7#|vn58kI6ta;mU zZ{^x5TgUEW%R$Efw$@NW{-G2}$>zk|lq)4R4kpaQ(b)1bN>5Dy923tQD+Axw1~-9y zTi7doV*xVd+k=<_s|EcqMi2`jK2BZ_!@w~(8J6TxwVeMt%!h9S*rJ1J(Qqsr2#w`T z0fm7b9eA|Dq->2URWwjxkF^R~vuI6ag^ZR)C6Rg7uC%Z*MeZu|@x03DxF5>=T({6y ze1PrX`gyi8Bw#1Ro*HR&0h`Wp!}_uy`Ri-flK=eY8^lzxSjpiPa?r1hHGP$nr+J+d zs(qPuZ?mPsRMAm6Jhj3Zm=T_sFjpF%(sao+j%mxZ6UAz6x8zZ6uR^M6)6bFgX${kD zFr21U5O&5IpRe1NzIrviEj4>jWm<2UigkMyXto3VDG5cbu|5EZ2;j`vL4^Ch`NFaE zUWc71LID0i)vHl=4*Y>_Ss3b;Al#)21%&xJPPCrcB1gq#F=L6_7AT84qS2%*87Io^ z@fa%zx#8QB2T~tTOqzD?f#q<&-3xQpenQBLB1|QE^T0c|-~P^lW!V|%cCRJ}t z;IHVYSW{)`+Ie&Vx`q5<@6#h>z?@+4`4gUc>J?(B-EUvKIGtQ!;vbW~6)MSnC||sI z@dbD;#xsXA!0QjtR3NuwGoAAL9mioGWvl@T3>XL`e8jwr`5rjsdNY(54yPjWp#|^^ z@Ds$mVSgb+_+rdJK)td21X{G7+<-Rkm_Gfs$Z&%faJocUShnw6GXY~Ia|LJ#Yhkfl+9f9qtO2=?Glhdr1<~11_1SK3 z#COU$po9s`w7}9F~pVZzdF5csAuc)}_Z2;Vy59p!BQ{Ik~B9vPm5h zmiHuOGKnx+WnqL0XaS$s=kfb8S{UnVRcj9V4z2t96_lbcIU=;y;_yw zgzX`JSe)vx>-Eu+>0(sfeNew@IN?~@YV5z&_32+x^QfYTz&%PIR5o{3IKJ$v7o%*2AAg*BAgR) zC}T?oK;MO_iytEoFgj2M7y-n?ILhr6wUy|_$`B%=5%TXL^hIND!b*OH+O74iIZLvy zYQADHSKqSkW$JOqr2E#&&b{b1{q#ZkzVj^vks;8h^VCl1G8k|>P2p5bD?>jiL=}Mm z`C*}4P7k`}MJVd8NDoCKi&~RVSX&eK2dtr3Bv4uDt?;$+zw50~X6#d2YHo3+_03bV z-egh1rbTj#U>O|*$Eujb!oGVDcpdK{#rbtFOhkZ@E6~z3rp>Pp$D#Ne#t4A|{IRv8 zgC?L19TSN-VkCzmunso7Yys@mMS`S237g#30!*Wh3e`6U}B zjeR{t*)@EZXP}0d54dmua$HT+ex7`$3zSkmoTnDU&`m-!j$C<|P4kf}E+59s2@OHu zZQYc?8xv|!alq@w(Yy=w_o(lP@ask5^gz}mP{=siVP8bZdBfMWw1?nJ41ww4#-6g4 z4o0F7)sq*PYzb7d!KUc&S*D0+=$>HCc8Z0QCRlp=G@u4N;ZV*>Znbo-nWR}(*{f2S zvK`g{kCXK8<;&bobixxb!6tf+Z_Z|3NR@1w*6W>|Mg2dFJE32Q${)?l;>ZRwLruk| z#ZFWlY~!lPB~R8wVq4qWw&#MG^@o?-d6z{nse6<7K;xoB-@5scaJptlBljj3bxKU~ zW7AbSuXju?E`iV12K5TnK*w(A1{{QL2IzM)ilL_WdQA%zvQb1F^~Wj6z{zteX$w;X z^2TvYCcqm4m66XnfmDn(Lcm5N@cp!4EXMlZXtSaOK?t1I)n41<3WI)E?(kz`cDz)G z4EN{OX-vH5MVbMbv;>W8p&LAQmS5Ju@>xTkfFPC&N(Z*kEP-NW5xM{%cJx4-=k+yf zP;%2I^3s~K+k6w9WXtHgTu zil(i#ruMxjm!ZMGZq(UNbtwK*95@&JZlG`e{I(h7xq*S;06efR@q=%F2J&?Ri1D#^ z=!a?Qc-9M4sNnjEF@In*4*~5;qq!GYV#WZGfz{_hhZ2{$UVOxr=W4(j-;60MRAlH>>brGGq2t6AopGAwv*U;4>fzcEZ#l++p&Q_(F zGtuT;VrpuVAmSKWdvATTcb6OSuz)=MO#zlcQjYIhhnkXB3`9&o=n=wm92?) zXc}RJ0pM-r)UL}$$eQB`^C>Dz2eIIFC>5KEd@h#qitzVu*bwRmI1HptZWfP5gcJd= z_W+g_Qgi2LCvpRZ#7xa2k0cHsK7<|)4H%R9fvBZ+vEh!x$yY8L7baGDDpIcO9)6Lz zD9y2PNQc9h<_DyxNGvHYk{6@-w~(J8+xofw?D@{*(WA-o+wKjltsq}V_S|Z}FLj|e zefQnzj`Q(lt!q_%3uHdQbUDwtNO!S%hcK|*`J1lAEc|nkqdzh0vEho>SdAdpoy7W>4i%m$X)A*H%kL6h%q^A2r2FUiVmZN_1PxGvI_q1F=4VJpj%#K|l$E-Ap zvKHCg&DM7X(-h`h=K@`)9wuSv$PIx9110_kgXE>5@`)##*96KPQ&`V6!wZOoVF|QhqIC^3}Zsx#U_Y^n+9}$)lScis}(qQl6 z=Ew>d2&@$zm)mV7MDnu<@^$1dBDruD ziWly8v5&0Q?r5I+n8y&}DsLe_Lz-VA_Ef4~tqA!470{*CfW?|2i{igN!y}J~VTQgb zcAz|}{FF)12XNn^Fu-8I;#9CD=HJ{z46P8<=`jWP@Wfd&+-~&u!cr-R=McMG5q7{4 z@OUMOjJL*$E_9vKr99-ju)gE`Fs~DdJ0l}sz=Z#hmi|xl71wGq73*~TiNP+sn`B>C zaN16*VeJU+c>!K=t4B*>ouCS^{D6m4I#HD;P3_li(Wf5zH69Vn>d{) z{&j|oqCb+0bp}q9ubTHHJib8kL-_~XHyv=Y=O8~_5B^1dhY2Xp{~HBi!0ppPOm2ds zDeFwB0!$j}D+($b%U1w-z>p(Qrxd}!#Re+=yGYV62}qiuTEp#+NaGzWKVx1omrmTy zU?Q3%?Dw&JdLxIRF^Quk-12&3w9ee13uh&F%Q#jfBOiqMrk;9r5Isy@k@As&yh7eu zq+&+7T2uqOGBAo9G%X7D4^AN zl6cux$fa*gYdAj*|5eNn8hcWxntaNyM(oZ`^4q}qVDW^DZ*;;0JD?Sw8GRb?1~F*P z8lV@hZpfb;^grs7rnFCcy@eKTw`VnR>;E_VcO-Krm4O~;uKJXtmLNGJYAX5i5 zNnWs+i>&7^UW{M9JaU_FqT$e)w(`P<4;8zc4;^y#8>S^%I?#vDJ|a2wlHdCB1@YBK z7(@j@&=wf_l~zr!o|G%6sH{_?#FIh4(!D&uQyb%ZlNT-|_mclsG})5fEJjSAix;zp zk}_>F-16Yhdi`Gy>naVC8Y`#UDqWj*$N}G+2Ky=n zG%-xoHJEM^9IR2{P#{j=!~q*nhszTI&`h9Pl(H+t=v&gRpJ$f)hj;Hr6L##_J#yI@Ld-%f^z@>UCs66ttL49{QFn^! z*4Mh4w;f%D7t}8|WN#U2EY%bx8@e6?!QTAs z(QnkEU|ol+n#NuTr2`ZSN`as_2E!-{Mw7uR+z2thhs8k^Ob@@kj4ITU#GN8ZftR-i zo&3Eg+F4p}QD4Y$7_Nj5b~7IK6mEGS^2Mv@@~ffUmv`?Td7BQ|Iu2b+L*Ct?-Dq00 zpuU4+R?lqSSZQk6dhc?mBK)LMXDn2!pP5d6>~|$3+Fz_;1caO?xl%QqQy67!gqc%Y zx&V3E_uk~mlgYEzyBb+lvOA<_;j77TyG2kN{3`8Xh_&35bdLvmOad=^El7Eu0-(&s zv|o5k_n9dPAN?4HxC8@b4s zCVw8xXujAI4FtPnalQa8;}sW3{Lbo<9L&Ej5M+5~X;9MY?y|Sp>wVMB19sR0tQ7S7 zQtkY5cGwk*L_|rko9-%ZRS`wb`hU|LjBtsSM(DUTVs)KC5%dI<<#J-TFFx+m53vU^wYF>KsGMook#?pPEj}f2&{x-;zE)&>qsf=jvDV+rQZ| z%wDj8%$yW$oU|s*;zVU8JKr6D;*3rz)VJf=O-TLu|A4Rf6SxU z17{#SW*0L5`R%yaDFN;SFCDXKUfn>f?uSv()Fv7*(s-7~xAf3>pS+G8@^$(u$v5obLM_DivDi=Is4n&kc%Y@} znt2ZM)P@bUp4!bvSEA|UHBU|(PG^vSC1<2z5D+<1+b!v-EtzSQs#>*ci)UyQ)9vKn zk)&Ai(=cfztRq(m{m922q&|2M;B(+fCg6UTfhYSxPThd}B%p?}iP|*=b{hI|G4Pk) z{X_{HEL6qZ2Iacg;NIXtK#f5=VNL~ni^4##dgAK<9M)5D57=d+5MmqLw+viIML+9T zllud!4)3Cg?LM8wyfP|`Xbc@-9Xv!$!VW(x>BoFB$;%b9?a;2v2Kpo95cw)vUDcuNmd1(j(Hd16q(TOqhI`S=f{7>gp{zL*ned2szY z;b1c;CA_1>Gh>U4aGlg_)JmQw-1_pkEu2sl70gE43W^G_7Hc%g=|lnTUMM2?^pj6& zOMarX@Fv^D{jKW$@sgNS5#jnJ&A*IUU1<|q6-UDf=#8g|$h{9tYWKPwjXjn6X!nlF zP+aZiw8hy@&G2w|$`taCJ>F$jte}AF_LFa) zL*MQp4?gk4H_tsLI`xvr`0`_tdoE;C#41y-!c;q++}st{NeY({!^8V34e8|*-y{EU z5nXmB6YE{<#sJPss9Uw}F%p}cF z>@RW?Pmp!6Q2aTRo6@;YwWllUUecy%U*Y=b-_h@gp>U!pp&ZV!trm0JLMyY3sUCT^ zDVkkX=V{yp-t9#4&x#hEb@3{w#zr#98zCsB+W+#)!QP*D*M*w~uD;{zv`0HtY$RAO_<_rmgUMG%cIe3r_?gu#(mru@-_B= zm@a7CUK?uO2E7}Dwpr}fJW>R0e1FZgupZcqm#EXxUJU+griZl5<#sP zzQ!5G>p)YGc539qYfMhMfB~9<%+Ph+3b$Z@6ZJU*VPT???JVijZ!)t2335S= zE33sa;lYslmob+7*@*Q1`@J>f-KS2^+ZBmyHd}bHVxf2`9Gl81JOOoeb&ylSHI(_> zBko4XGc<5O8DvlMU*LiW>S*A)LT@pgTByglZhoFBVtB>C4Z`(;OFCEvh>ltNXdJSiI(>6fK>X@0pBT}=owEDv|P zOjKT^WD?a`f34^KPJUyS3|kU@MJpgdDcDVeXaCOw`3J$JjHifdr@#5iU{l*@ zb?A>gIxHIy+jzmGQYryFT#5=HJEPRfQzfr1!GvJd0Khi%^$7{StrTc(A^CD?aqmEXjXl8#0ZC{~65reo>P{#u#NI?Eg z*}Oi`grRne zUsu`KF!E8TCA+c`^3!*(2L18xrIu}>g;kb?Qltx?4;O~chu_!kF)MwW40P{osKW#o zr6SWRODno^i5_iA)t-pcbfwmVY=VDbC`YL=N4N=p80TW0BDXTAK}DK+ zd41}4g~BA4`+0IiT4*ed@RTLQrO+;G?+o%yTSvx}=&;QeCZ@F+7A{b!b}Te)++FgA zFB6E^FDWP!`P{;mWNXvb22=ayV~f!m^7lED(bwQT5Nps%B>^7Df`$ZH=j8$5Lq%U1F3~sq?ZH1ig#r z5b>_(Z#}qc^?y-F1V^QOG8{hOrHOPXSAZ;zlF$Z$dN*_ms9E5v(Xs{Pu4Bi1xJn_> zZdpkb&Lyfy1J~h6d7a>Z!9>?v!r?Hj1Q*IreVJp_Pbulr3*UQh9(fgQIn3F&9B~tej5%r4(@n)36ROP2Ll&HrvXg zLQOnsu8AZEO7iFQ{_qCvL6EULKj3^7-U0PvegL-%t4?4DJR4>-hXwoqSDe?TR6ZUU z0t^Xa=%GQGK}}Tmmw|_3#-U|KUCx1oRU{@!D53@S;vNlAw1`KPl5bDsGn2(-yZ-VJ zH^dJLyaIA^ft?ZYF^Hc$jts18jkEQ$b&I?9D<)RipL?#uH{7tfLEEwW-X#DR{6Sw` zXyD8>4OTuNM(8_YLY1S)mifnw&#X&La?32wAJv~J*ne|uT)UP;=}>%0XeDY)lZ#^h zBT(guZEGd$7&%I-f}EEevJN4r_RN7a%)(j*V+Uhkv8Vv+PvaQ7kAep`dF)tKfDGjG zl%oz7P%A&P7E=6JC6CQ|z-1lap#V{U89?*V8DjA9pMChac<-6oTuB9@S4#_Sh1;8E zjJtK7fSqJSjdo=_yqO0sI?OL!5Z!*NA&V5HZe1>=DB(SCJ-ti4 zA~VIRHr$#dHXJ;EKJimUM7-m|g~Y*;1tdj)A zO2uXkZcNgXor>06q|K62MeCzx8$I3<;N2orDZO?}q1fx!8XpiV!oyoK5s$mxEcNy+ z-@7U>JRIn!^{fik*6MUko=o0vc?#r53;z9$x+OnV*+=>JMzBTT#8UsplE66iiE~&# z@?G;qG>kDu1N{Qpd_9ViucF5wWa~$FlSiOH@1-X$!vj4{j*y!!KmGi}()(WWSzbFU zKX9QDRS>k#^n(VQZoq_GmPr~*)nuBMthYqbE7AIAU#zYU*AzE2xT>qkpA6{A4Fk31 zQ>{6uVv5JBxtxjW2ANM%uVE;g6rK$3ThG^_ou&=LY4ZCXah4Q}Emg)50%5^!2&Eyf zhj)NP0N@B8$3EbbgNcUk6wvYEupz<~9#ArLGq#8sulh*uUyw@5gP_64fC2O5$^URI zESD&%zZ#MHm8vhuEd`7sunarc67&hos?riPas(wuQmB?3X`6`tg!;?wdBG~(pUR}_HIg#_bW=+WYN>7Us?=UCOHk_N+e;1njJ(PuvzxvAGO1Ut^Qsiy zG;G9;I{pxfN+%?eU8;yQ*%U5S_;^gOq(WkaZ8LXi8(@Mv&?g1D^Ekx78mN27!Hku0 z5Sffxk?R!6gRfvGKnyU%irvN2@`!i4ZFEC+F@1G*pbGaT?$o#4l0 z0ei)>AfYe{>i0&K02~e|VZ0veQAiS2q+l~3)k!&W`5YG3B(PB$R}0}L3OX*UphH0r z@JXJ90PX}@QXU^L7rc7ru}hZ{moMK#ZXnD?tJ9<$;fd{xgi*I=N+{xw-T zx2a{Za(-Qx5mjsZM$RBa{(xT7SJtXXCikpfzfwBXCL7|2rrfoN>1x z7kakHI#Ffp^$YsiRIP2YD95k#7@V)FQchEtcoCWoUO+$8+akuwj@+(YQG=>Nj_`C+SmC5$ROUMQVu=v=OB;9g_%|uCBLyfvkOZoRkJ}E zDyq0#z#)L^0G;!Nub1)&ab^)CQWVB8s*nM%1Kwo-#)7Z}-`qNnzZwlJh-E!_d(am7}YEdAZnomztMj$V<>WXKk_8gNM3e;d8SP?T2wZ=J!RbmdFc|G#WaV zN|CFXDzwHk$D*~*a10L)t~4&N$T$XJz1=c&4{tpAckf_C*}UXQ*8&eexXk{={91nf z9L}tEj(LF@h(g7hPlNnc1Fne?D!kjI9X2!m*gv8ehjN#pS?#8%8hc9A@P|CM41x^G zom$%l0*WJ0JarYED$smDs!{5qATP@bx_tpZKU&UnmM%tgMH7dR-)D=YN&`m8fwipS zt7T4m#A)XcvMY3E(G%Z)Nmv3*9YLHRNesv#qCo7h_*m7#hs70ouOTOzrkmNmyUjX& z`;iqeWaHgi%u?fP0jG7DV!`F4Gc7Yeh$g#h$Zt2F-`ApVUVG;zRGma2bfE)JJqR_9?V#Ckd{z(B;l}}5iJ1g+RGwF`v{8y0 zK5Id|u;LPl^934Xq6g4J7?ZG&fh{4Zd%O({O8fIB0y|83MR}95q#eF(!MeM-+owmJ zUXLa^tUZ#rFMc*op4W1@$dv3ETryHs)a7ui%2^(3*ze&cIpuT*zgfM5CS^pJxWU|) ztBMT2@{fNczWw&qS3mkF@%iVkAXsA%Q3a(Io!z1J82X%dPTsh2@|`>p-<|auxcc__ zDXXh1x4hEWw)Wr}X!H1|pts1VoNgGhy=h_v9}VsIXmxIKVmdf^axfiCgrd<9&QaaK z_k^R_sSI#Y4_JY5H}Jh5)rDe=FMnz<4P!W87&ljs;(S0mse1H z1niQpj{>Ign}0Mq5Azss7nQDoj6VZ}3P-9ykvvw8YvFP6EO^=1^FJW~jw$ST6d=I< z8u8jkBof&TQ-h&W*=bGC{Q5iCi6n1THgI5cp(`McS9sK&1XI(+n|t8`xsfgj7b;NZ z0)8A}En_pJKG%Zcc^VD3pm;5___;!Qv86t%NmOVUg^SUnXt@u`3~}$~LmXYAgFh`$ z<*oLT4;Z@}ceLoc4&1*O4gGbo#kG1>vtd|*-m4<7R(()p_;#>*S@quNDzmjL$`p&E z4zo?@u1Mi|X5fy)Yy;dAfO~11z^5>QR;r;oh^J7KKmXUKz(EWIsDKoY`V*j_7-&pL zv9MjQKBfYk{OdufQxnq0*eg$Nkm0e~V=BrK_n&|DRao(Onw*6mkckTstyoxI+-8vP z*11s6qg6}Fdi~8>scM2EgnRL)1wyxvl+a`v8fAqDwHk3}1nicKtcsg*hOx6oy^_lK^1=YeBeKf zF+Bk~tQlfJoUg%~q@mUbFs4yugVk5w^`wE0zRq@I3JWVUa8C1T3sG)Y{#9Tm@n3)m z5;%D!!cY=zRN)hj@Va&32O_uK7J1;y7)!;Dx^2FYAS!jTyv#mLK|z73tC;)`Q&}7~ zm!Kb%OXu@vZZ_-oFBPoZDE#eT(JScHmk}ylMLvky@ROL*5F3U^tf67#fz~o>;=&e&&#f0zcN zl%#xI+}}+HX936xYo)XdEyfCNs;Qim+@yM@qTG)l-8$8t`iz&vZ!`=FbDpMOs z*02gI=0WLDPMnjtS}~*v6Nqph{Cyz1e5i568`H*mXmOX{824dP0oz}z?zQId3o$*tb_^IF~Co#lBUrtm=@d(j7ZV$d-e{JIQ-mmfnh&cK=;E!);d%@kFslG=wG(50g@cDOjSWqgy8(81@ehU70lL(D0!l@>m=_kRabc0sJKX9;^m5 z%2FMKJsCaZj96xkInrCRy2e|(<_L!5KAb0#1_JzIi)1rK`3vn2$WoAfg5IYu^?! z|MDMB35sr4Ryj1`Ry|qVn3dP%cretWLt!;_KMb3d0`jl927#yH?eZ@Sh3-kKSI3qv zU5D;j^wwLkci&yJCbkG2UOL+i{*=ic()c2-p@m#g5;iD7&KbneY!ZiYLgarrsBqNf0{#JjoYv1)d#M zB*HLL*MiGPu#-y#@@2HsBQGITEJR5ySy7~%?bKI7%eK`Q6h$HeC#y}qmZ>TZF!W56 zHrHqzBB^vJ8dk>*vy_qQc+H*AKFHxaZB->nrLyEtBGq9D*TF7~3S6?Uxk}B_@MQt> zwGdhqBEF17w3Rf@0&;E2u-)wKhGqs|!@je4XxlF*nWfUbPJay2NvfA(Ljs#XQEF$! z4K|xyob{L-CUcd;{=+ykR2G%U$qW3DmeDakh`Q4Mak%jVI!;prgzW5)V2SmfZ7{-Qg3J!45A-^zK+}A7`U{>l7|JL_Y7*2lx`@T4YG207(9}SiqWY(AF$CKc08vk7LZdf&cn+qP`kHu4;eOApYw1*pAyJD?!l-M+4N^KjMLMt#@rV=E!Y z_5Bb_$Xk?|oV72s5}c8r+MQ(ywn`;Pk>N_L`z7az4z@X0!9)nFX20_0l*#6@z6Nvm~S3 z36TYm3A7=GSwWJ=C`Pd>C>Or;A&k|9^yi1z=oN_IK}_%uL27V6l8L)(le)WmsZ*uU7Hxr2+Cu44 zD6qIoac7|vw?!5fmIYQJkMGmTiSCP#2di`Dsg;?@ z&WvY%Q!ZDQ&$zv|+AEy}e~YeY<6#U=4K(tAmpTFJHRpln1&8r0fgoNe#VBM^)BJ|N z;OPDz?}2XluU2xAPBKbuzan%x{YHy`CRN3{6Vo(@Ac; z(5xFMY}_~Az-&02U{tx5E%Vi?;-=$Aj#V5y>M;NQD)Gks)fqH4+vTchA*X2TxH3nw ziTIR!Yvm1>^+qdKU$+8{OOoS4g{3=MoL)~`sG43DEfaBKtn$W?z|$7o+U^T@dqTD4 zM5Ii}j` zz)I-C_wp2vQg@|q5K{G>8-@}CsGmGOkQho5)p)$I*)d)f>zwbZQYc$e*M`2~b-qlJ z)i4Hov+yWAvfDe&tMJeDePo|(wG8*D%@a+k!c>WQhFRJ( z2>FFokPC1OavwxsH&+4Lgrk(WKlOa*$Nyh?v{?Z=2craoCt)a_jzMGvYDO4l`h~B9 z#*s4s4mRV^B@-T0=^Y5a=qPAa5{=aRl;}KHH#5`Q0}ZzP(84fL?^!~WL(ZoC?W0ny zMJZz;B2-4Rg~{7I*0+vGGzO!DMucf}dk~ox9$ZBJE*K4>z@F8s6Ni!u7w#K*8O=QO zPGXtg7+f1c9hWt$z3cp|>^qybHU-))JH8s)SdGjwADh#%&lW`VCnE+4Qz8{XqPItn z;FYo`BRWZiMlR&&#BRf@Lqo*SkdgeH1KIbo`SDh`;`0-U`SYJr7K|}{jq;XiTS}Fe z!kK2nnZ|kHxHek@@fw_0(hL?3o-2k?&LS#_mB?tr5h_xHGxRWiNh#c7nSdB*jcEmE z8yNL`-w&R_>wqYXGABOwD3P#1(OK9Rk|ztCfNU(UtgNF#Ye3(VLUb>=1+q+ZA znbF$$6YKwm_~Z`>@=K)rv3};vdO}tBV#$&O3@pOmme=|lE^c@CTyoP=G)SJFtJ2!9 z6?Z!)i7mtq^7}*3d*yY>Ytixz*Zds-;nR_xaA#!7lt^c|C(;*<_QAEY2QKhG@<5B5 z!T-R%SpaYqJkbefZNlwSXYVXq0Dc0;ut=6TXhe{6;Q3my`5}hh@})lj9>NAo5kUx3 zRcb&TTKU0)@k5E7I}?Y<#S{2&TEINcC_F;{7J}{^MC9L)js=mGXP^JqzY_oW$8)C= z-+c3OlKdU&!w*_(f4m*8I1D}Y?#)#>iq4fS>7$S;f;h2{5M$=C|wZ_-y|O!7}3 zb;K7figzII_5OH#ZB0$I2G)q{0B)!CLLXM#@e#`&NXI;vK@^sPt#!^?fSrSt7QF$` zjo3o~S&c1O>MUGXAuw9^`5#!M6`N)9x12Gvw?!SLsa|gk1-{T(VOlCbBZX0TjUtRdR>5-zcQHCT%@%%(hwopv-MfSl={uJSwmBIG*QE4gthfbUrPjqYJzPz7%)2bs~c)> z!WT&J9zcyIQ8R_dK*z*lM6*~*0=$7SZ;B<(hb^fT8%jOO&n6`m$IpV`rt~8G31x%9 zH!-IFXvS92^eRblT5tEr?PJ^9wHlYdpiGF==&|P7+L8B78GgtGz44`~2sg-UQ6i=x zX# zC)1bv?T*TI&BACZeRrDNofxXSs3F+A?z&~5CSRYUQyQ-E)z|pi>QEqax0!Dm>zj=b zo;7jz-G6}wjwi{VpONObJ?vTHt?;k#etJ=sRkMWdo^LB3!g>5S-iPgqE-F*NT&k2W z&S)Nk199+$^%-@Ju#Covt#}ln8Wh+QDq%}vK8{-?P;wr}PC@>|Ct(gj6r;fx7-?~; zi9B#N#l~kvI&m#|4+K6@>g^Osrrsv+F;4XzIg(0whBdU|B{`v7uNFdXEQ-;~tp1GC znsW51$dCT{*w(G|$8Q(iaw=kb_8!I6x3`obR0%=$jpiY(O*hU$O4l1Lx=DJXcWn$~ zAhCMLHTZ&M5Vl0ekyrHy44fY;we3`c;h}%$Xp|ac`1Ns073vl(3HMqB;iwOadgt zLro~MfpUXm90o#RK~)sm{}GYFoXY@opy*JU8SkaWoUHOj>K0x%5YQ}h9&``$~^r9O8xPVh_48WKfRc?T`aHHi4hIV1EY7DRX?4WN&d+k;z0MwXh;y( zvrTNz#xkb0!o&QAsbodzIwo->8ZZQNdKJr)sAJ`=N;K^TS;y{JS-EFPlE1D`($&iE zpLh`6{+L9k6V*Q^DuSjVzu-YS$WF zES8s5Kk{X^rDb!yxpmutEl?WrbwC)YFg%wX%TBB6z*COh6yeIBjgo(EF+^-bO+0s^ z5Dn_QYHnX`P~Yv7Nep>~KUgq$l|gwb5D-XUEKgV(kXgWAE#^Zsf;6=P;vS}S5U!7> zYIed!8xe`KY=ozK$V2{6MwcS9)^3 z-2G0hjIX@5;qo*&(cjy=slnT@ z`OtE-ntXPtOK!W{o(nBXBU#mZJng-HHjln7Ir6F|JA25JS>O5DXI6#2`Uam{GsEGJ z&70idO>b`h?6VWgmMsPOnECZNs3{o>)Wh9VaaW0AT@8*@z{xgZtqhbo&nP)ZF$j#o zVWkrEppQ_IZV0z@hOiKTcQ|(uj9_ym1SX_AnNu8 zqKZVG<6*Z$g_6(`<3aR(L`mq`5eb7|hMJHM@UwDWkBmolCkU<52zDI%%^Pl*jbtO& zBKe0OCcdO`gF%y6**4P1=junKy1?16rjcf*b`n)=$(@vToZ_$}jHRSmC$ry|L<4J&oG0y%F*tt_6 zpP?B%_l1DattpJv^5Xy{~O{N z1(NVtMGb9-G7NrjSCuHUHOvw%?XH-!=&>U@hb^YmZB~RUyejYhe@Jv5iROshKI?W9 zZ-b9RLyrubSnL#Hkv&M*thG2mc?}JQXYCs;N$gxH6g2#8lj;0zv48j&pB{`nm+1 z;7Li?CM@m+@NKE11;R&{a_-MBPAr;;Ftp;#OD|nV-g*&v^f9uIDBl^l?Y1Lm!{Oim zY4PGCi-i`po3OKa&c!OT$09`!z5o6n%+-lVV5BALN`<`WgMciz1ULA z__iy1CMXs+y*Ra?bgmT{XITWc>3Wc81?ba{F(oE=pEv^B0+a9Ye7!NnUY_>&g}xTB zA?Rv+imm1-hK1sC3Od5r5q2_gFJ5?gbTnHuiiyRfL;JJ{r~|C$spAgaSD{}k{5zO? z>BXTj_$^4m1XV%@c?bhcj{kGyt)H}6@~#0v%o_~Hq!}eg#_{w3vQDs8p@5$)X4_p^ zxy4oCl8n4bv(aJ>2Jx`dptbpV?o8PnF^6uOZmLL=n-xOnX`7=l%q|Cv-J%7aM`Z+| zvbtGlVkSdA!;856-hh}$BL{hgL3|#M4{=At%No`<1e&)UUW2CoI>s!}^YzQhrQD0! z_uuy+UoS{&CR;N~ZC1b6{%m-LzfmeAKPcE%jDizhe zezU7OC6|XyEA=+DZ)29+u*=s*(a&s~pTanWYaunmqk3c>~7x;%-&@&l`N)~72 z;kYlHdXNBeCZ5d>O^u4b*s0e`t!nJ&$Hsnb(^#8|9&b6*}9KWBQ6s;EAu$aG8@4o=dak++qUOJBu-NubPu><|eGz#)c!{chtXCvQpOBB= zoc1b4h(U60#F8wRDNteLGxT0hPpG>aVsBWc-U8Tx9Q^7S)G+1oqBTeb`-dL3Fk4ygv8YK#a*u+n_+<$+%4>}W^IPv@6uSYL#xbx0i zpspJA5L+XWI~~MNg6DU^^u??=SM83ht<}PP4zajPY;?zL*_K79rP{S! z+ItASV~E*<3?*mhkke`sxps~{v2@kY`Oe9|M_~QxlUX>sc zwLqgWjOXPbZv*EBosTg0Kvn$wF-tGNd9clh`K&YnxL8tEe4)sI;4@;ZnfeHR#falV zxU}@Q527e?VdJo&6)olnVOddxeGV9`a$A!o&QmPq@q+r93&&O2W5S9xd=@Kc-LiWF zT!+aOe5q#KlqqY-8^deYB6x}1aN$qUlP6?{PX#Tf?p54)vNDI(&WzgHJ1FcYF`j5m zxeJ~f2Y8Nrz{#5)YT{4X&q;Z$8b!tIP*b2b*x*MMO07>r{ibbdYI>};w)WA6h6eKa zsk)3|d~IfmHEn3>iUu+QCX^Am>QVsvxSUX2SH$g~0Kakwv~~#c5^&FVoHIKe;+h-A zYz3~yQw=FQ2~!l5%2}In^Z*xWQ|xJnIgw838Op1KXYeip=@>6o(SV5#>K=oA`*zR%_-|g z2ci)viX+|%iQV2Ih&T|d(OaG$DdU)U9+r=RCJ9+{x$Mu%9bS%xO|D@XSuT#6h0344 zU3T=DpyiqSB!})OWYLDXAyZc~SpQ_Ypft;YEq;sdjKo_um3cR**0&2+YU(9?IyG}r@}MOn9Jbr%f8qWWXeF6^ImPR`&10J0d22~2hh-~~Mfzk`Uuq8YXp?-CZ( zVw`*fU@u0sG3E1X@sNWCM1QA_QDMur-HKs))gJ3eyBy=d+@$KD9{+vhz6A3hJFI-Bi z&FH9dKiX+^yIP3O6??=sXLKg%L#J2*2I<}iIJtE0bXV+>I&w~W#ftPCl9_IhGSod? zmv#qxx5F5jNk4D3YOc1oPF6bmoC=@DSK)lOYBZhWzn7c%e7l69`UP z`{wm28v{hRdak-KY)M+CHz_lHrjuW7XrWC+_1@ z|CUq?={C`(`<%+j;?5>*M}0-A(?XI4SHR>#9bn=n$T!xlyL5~v2==qY}Zcmd9~vsy^f@N{H%%N$NM`SSJkzuvc1r3Vh_16_T1p$;AQZDc6h~H z{zpK^HUSQedo|)IGB}zzs%uM~vB2ay%fxcZ&Hx(YKn8Ynu_XZyAfTB}_4UD%elW}m zk?t^lU?^ZT)(AMEeN{FmiWAZ@A%(ilAWg z+_Ht-3avy;gATx+vhr+QxPjjKv^dNOxNUBaAR6=WECP{;F^$Ly!sRpU1qh^S0QX4%7k#Gb<1*CZPA&OO=*x0trEy$= z>knUly|?jD2x@Y~s*#+#IDUjf&yVBBmylTB9_o*)8lfa7}S4gEUSrzDCu< z;vPdFkwS^uZwcx7X5Xl*#zAnT)?$;;-RQb|}uP zK1u0{YKSFI0oxPXN*CnZP~40O3a19xof7sySRg%R{R6GT!1))x401>O8?T5mVsZXq z@$kWPCHyDFJ&fU;rQ&fdYL2J)W@E%?V59Y7Nv1-NhVHy{lPDziNYwU8@o2Ahd~)Fi z^4qoOqPnommW?L6lv4w%(XQ?BKmYlur~dGVGb9<0fA!TL{?M*IcC~oV4KB^`W1{;H zrsrg17F!4TNNv(+A0SSXpKm+1p;=qE_S&te(QJ^Yve7CZN5@t1T3I}hdBRY_w$=Bqcw%^l(VTzA-M z&XvV&UNuht0wk#(YH$uiJhvFJALZpmFthujFR?#wa>dNC>btNOS5t z5EA$i6BF+YdEh4QbclYh-24oU>+$%iZa;sc@x7mDAcOfQqgDA&RcB+Pq_Z9=Y7TAe2biDs>rs z)a;fjJ-#ijVJ}Xb^A5XA$tzm<>D<_gIk7p>8Bw$?v}MlJ5L`!QTsND1XRuq^HTn7c zWMgi+VqB%9sf(|lYT{H+#Qs$^%mO`5tAza8(f-culqaxp%r@X}U|`&A6KIC{viJb> z4q|8E&z2rEcqfXnJm??}VgTwj;dh9vo#p%@`GJigpA*OTxUoxKl;q?~BH{ z{gt8`KQVI#ZR!+aBN|vwOrAnan?cMy%uPkV{_)3m9uXcmVD9aGG|@UqKDlSvmc-`G zt#Q#}^w)#X)>zh4y?R6Zvdhf*cJi$!Zxm7Y+Q)~vQNevs5W6@{5 zn-Hf$;+3+4wx}(HOyq|l_$A^azuDA_?EgM#G+r`8=OSuIg- zjB4GQG=4=O&|7aGQYW-Sx#X}puB&YgnSCsgQ_S2n*S8KnE032K(C?c#pGMA z-EyP$206#J(5AP}2PJ*6*e=~i<1(ef@{m+c3l)=CB02w*KxEr0?_nN9lozV2Eo)ts?my5 zfa9o@fFT^AWx%#9=9G>>R4y8&|4R91U@KCT2HP+eKTB6EDlj90{lpC8&^;3yF&K!U z9U;Yf3%V~$c4X62jZ-d-lAk@LvWbHhzh`q;e(GlWNp$=w<`uWez3+PM1$scmipakj zIe_jUd+G44qxe8Sd90*96hg~`Ey=o7g}f~yw|J^;T|InSHBBR`67YuGefAcHLl3!V z($ z!s2O@#zFF7I~YIU*n{B*Y75p5N0-5j<1>kN_JqP^itCL`UxoR`Td#xb4zj^uurB%L z6z{cvZ89k$QqbJ`uv|iKIo=>Ni4NRAUG|>94rvJ87OFNJ)jS+oQGQSpvlFw&t`@QV z365M|?TtA@u9){LpOri_qarVx_5C|vvFIZN@k1Ho9}j&v-BPo0&kp2d+Z9PJ{h!yw z_J;Pw$QvbsR(&|=jYjRa(;rs`VqnT#oquUk^Mm@|_>Npg7LWe`t)E&0bi*OGs)czadwWB(obH&W5 zAf#~gxx*%T-06$zWTA@Xs)R9?c8`oB=;UC~78h%w6;=pc7R{atL14ES(|m!-%`0|9 z_fhCmu_OO)ms46z@dq6rIJ^g=^*|swKng-CL@`$ZtfJgo*VncV)+yRC#U4@123>X36?Y?QG$7ueXx}umzC* zll|dh`Gh*ZU1#qco~+Fu~|Nug(HVBf(*Nq%vglh%!Q#F6X}A zZ6TlWdVi5>QueGN-UwulHKzL=S?`b8IUs||Addee?QU8D@(V3+wxB~TRZAC_7Y|Qq z4{;$5NPTKK6a~(2{4n|k2B`4iqvTeJuhAz1{Zp!(Lv3}rP^NiC))SjQIG&7l^g~O@-r4|1&vlt+=1w9S6i(k*I;FpT@F)Cc4u+LAo(p3K(b#Jp@o5=%Q|#zD<&!U z)mxDN+jlDt2kN33p3x|tpLo?@A7~sAMz#;Dk`vlMDgyav%@q#L~^KTUH8{g>v zxPK!7lVBv-cNkV%+HPiLZ_BP4(La=3Pt!Z&MlCb9Dlmf^8#2E|4B(Y zgJNx*a#r%P&L%O&AXj}9;)Ki`9Lhq|>i>2ky|_$oiE1>g%o1V|K`Zm6zRCLYZO_-p zn%?SCnx4{WV4^4aYz|$S$H)MF&>F}OGhGpK3YxUy#%_IX#r`dD%YQ+p-(~GFN2v1e zd&HHhw5Cd|wQ1N&l_feNMyJSJ?6Z0EGV>u-1o#`r=WhbLNdbDb7Nm7A_HzC^V<4*9 zo=Uu|0LfKROuff}cZ9>e6d%PYJsce^79Ndm+hZ{Jxw6&B( z#+kF!-WE-bv(Y_BkSl()2rkLN-@oNRVhWM~WFt%rrBbgFlIX&ERnwecDzKqWebuBh zKRoitd+T*e?ufO-?ptQs{KsdW`Fn*hsc@$pYTh&=xTj>6W0=vgzFSzp8yMYGP-7(3V))d$+<;v-C* zVLaQZ7kX@DAs@Jyn*dS?OQz8XCAG$)YGOi|3say-0?s8jJ#<*#YQD47w*6{AwV42Ps z(P%kQ$mzT5*7@oPF}y?@MGs~#%Y6TR=CbVjnZ*^GGw=U8rg=%VB|D*zk*v(VlbOw5 znRzF>N_>47l*izTgRRP%Rr^_*Nwn`oZ$rI%XGO z{#TB-OXl;n6GREu}cEBa9Ao$?WmwqcU5hfu; zN&F2x0C9>ToIi} zldB>|Nn|h*eG(0&KFq-cv_<4)kH#l9aBCMJv9iS^C1}Wlpb1cgPP5Eyfx1$f&}`c1 zwHkLWm#kf_c*v|ubXr$PxU9tC!-t2xGQ_Li`=&%SLn8qNvP-IXAx-{tITPTHl}8yn zMPaceUEDDJ@>&7m>*%R$NTAD!Z0y?8pJ9bQk9ff zq0zHgOe+Tzmd-<7El6s0JfkP*Z#2Y=RqjE#-`pCI*ccqvB8@Gs^a!PDDA?$bi^R&x zrxjA~WjakNC9_;Ba`7#SFy2Q5_|IFkYiL|R-mnzDz@ecGVQ>b3Yw>)4EIkL(Hd<+o7?f2C3`Sn< z&cN!-nM&t!2hXy={?9_;pOt@6YFYSR@GR3Ofu}0K1}SNJ3+1SoaXt#(Y@D-Kvf`=C zKCA#o-LIliMIErB-oUT{_NlScMys1@owvc7-AQ}Wz~`bxkEo1Q)t1Iea?<3<(vWX} z*A2IZbTb=`iAn0qZAqHxs;|&SH2r6|nq#S|`>!A$sIQw8;zAFv@5q6%*>H8RE=2gq z40*fDC(g3fYE(Jl-iCDShSv1cboni0I=vJc6|BYUB4AyQ(I$Y*)WS&_10ANZu7d?Z z^n?0wo|?m2-CRKpas)9NN6#^?$Zs`QXs@5l<7Z6V46S@@e$%F{XhLNr`Gui^TloPR zrwNmHqLhzc&58UtuY-Nx{WMeGd+!xK^rEMKS&i!F=Q&~_`ikUrQ}>dEUi!zNo;fBuL2^7=Bar_b=+bG-<5YvqAD%+I1g?CSHg*@XIWf-jH+{%^Cj1(E( zc_;tz#}A{29!E*F0lG*!T~%(R8J^62;utfQJawv){E;P~_ZP@cbh3cd;JCqsebxtP zYiNyB1~x_*#-Ji;&LhtG_XIKmo?_qTob3|@pCyd;FUz@@U20+9K$if@DE0IlgV~sy zH!sa+DCy`#j_k@w{ob_SEBbpji)x+ZQB)@&bgX){?&BE+ZvFGUM05JxcMC5{=tQ}J z9Hg!LWfKjW!Cd$F&L(5a+7tUp5tM^d^uu*Ew?Ld&1DSlq`(pZDI7sP%=3$gCmn zK^dN=l4(M;s)g)_Z>L}M>Z_H1QyUNFNF(}a-Sz8R9F5EO?nP#nO<9mxSTA9I!}Ahv z0=nv}Xwd=U>AmKfSFYGu`DjMyi=fjzqL9u@K0J|b7l+#m0c?TG? z(+SfTPgFSU5uQt>U|pQvmVWZfFVkP3d0K^(ya6>jrPb`jp9Y3mEl&;6Z9T8NQu(^$ zZNtAu%>Ts@xJ_e8Ie=^c zIA*jkHID7cFz_up$Pli`ru&!z>!xPDBr8@kGG&aK?7Vqvi7^bCEE4>@G6Mk9CX4@J zqgg?VQg=_`b zEkaM$OB6<)MS9aDO?bJJd~Z=OI4upndJN)DTLClUK+FK|5+&t|Xu^L;=?m9EFR|Td zK^)ePzf`Rn#Zur=DPz5fwIbitDi|5PfrMKHqG;-s7l%LO<)y zoB7q@d|T}fm7Cm$`ZektJM`7uesmGZHtcz5xfZ4&?3Z(>!Wexqdur$HcVEfckDkF_i?8OyvL#+Nv%m60=ZnbzrK;Lu71oYIzc4?GCO=-ve z_8!SU^X<2J@-*F2l|9OvAa{zoc!I1@L(ejpP1&KLp@lj0K#qJ0WyzctIRiaz^E4Ue*Bu!(|I*7Ywkbj&cv(J`P%= z3XcS>-RjRQ=gf9#$OmA{|0wNL`nC0HTHPD@7mX&Poji;hW3{P|mq)m3|436ctKVpo zn&^3^K}tU2UgAcs;Bk_7q_VVQ{4;m2y($-#fd9E@KDtOGj`V%Xseq#i{k zEP!BO%2;ss{@OcP)jV6|9M*?)!)}6Tp6A{g92r=VQ>+zhS*eR|oL_67bNZx~w{E6l z)!gsURuV=hgGl^QZx7q+lqt6CrAm|q(fP$@Z!l_(-|SrIv-lTz#Z5EZYeTM_I+2C~ z+Jih>z!|Ho7Oyr`1@nnG`P_ocjvbi=KA+n?JEV~~`uj03PXinGQLrzDL2h~g2ZVfm zYLQGUwP47`r%Z(Nt-$j8P$?iIGcvn`;_VF;oP-ceDPAEndGcU6D(II@QVy@S>b5V_tY0Vk%Rdm>tguCz z-Em$8n*AI?KMDez+`Z9mk^JBxFGc;iYG&%pni!|y(Fydmr%&e|wb`!g zn5oShr!-Vebz~efnsY3UKkLgFX08+1oVlEzQw{{cF*mHA`vC{V9d-xsxhRpK*p3-k zn_7>}4jGKtTsof?=W7otdj5a)Wk%20&8cbn7+;N?g-PQtl3hP_>QVWQ;kbVtV`g@{ z(7B*i$fc-3XJ)fgri{Itov0bij(hj#pR<4d^9wJ$^G^2buiqucntj17S&h;aAI`g_ z8FMV+OBp>dS?N2So7{~C)71jsTPtFm<|oFMRpsA)JO8i^IeGQYNaM)Q&B?WElg+W# zWOsM6wI~CM{c`I-hT-~Yi~z>ZNwWX3R$yt|nWL6iG82{x@P zUem|z@|p?G)n%ImNrg`y=U*mi6#HxKh%1hQ`5h5QA_U=2wj~@%xglwi6XXI_lpn8% zD3Ay2>K>=8R_6Y4NmY5nQxl1N{;juiuT+2mQut))U?k>ip46ol<-;dn96?s=c-$|r zOfl)q6OH5<%>un?X)^A;CL##!3tpAxGzDS~d+ggYFb~UMO708`6T2YU1!Lt=uvhT> zaSP4?RUqwIiq|ZneXyRG79UY zjD1RK*4$$g(~~BpC*GV`RYCQ1!t3Y(O91aE&N+Y@fU~v5MQQinb`*U73w!xPvtmeP zE1eR0Q3envzZ?1Ha~){hC8?dVLv3JJ0pgTomSG=qN;!J}ge%?5t2N7?Lbs)H-8A_( zZND$#>CvLxus0#^*AO4(a#KYlI|6&wny;?=Y+)^hKn<8)c_sh6*D(3}m@_9ezK3MF z1D9qt#O>kLX>_37=)T-tlm#`Q(|MA71l$cxds4GR}u-dk-=C{SylE4x~~wz+A- z=R47T3a_TdIkM*8|K7E0*OzD`xjsiuM2CJ<8$&j~G^Kbehbqx6QS`f5-S37?3HO4= zROXU_h(8mlN;^@et}b2}FxD@*vtecXOOsPux27ih95#PXM4G6rO_l6y9P1Z=^u{>~ zaH!7vf+Rz*$FSW|`ogA3@grwdbkQz>0P=|}`91_HAi;VU>|zqcOjZ zK~{oXnuoB)5(TX^XazD>CS5_!(&@ccpjlPH*%jT~ik- zJu(QEZ-mWYj|sqD+y}WWI>>A)=5}J6xe7X|O@cfBKQt>48!H=ZAma-E(SoK?kZp8a z9mt{527(kS>TLK+5b|ZPlV&Dbvmmoq;9K0tJMmr&H25QBqm7Q~$@&s(C zoLMwfiq;t9N#S)fKzA`QaVgDd|1_ym-WZ>i%h9M;(UzkIaf8Ir0PX7KftVU`?V*@3Bq0rofFPz$p#=#@=I0 zjB!@O80Y+TokDw_Jc-9Rs~xX;=d_ulmew|JWu3uV=MGe-WI>pg+S~d7htCHWp~=mx zVR{g;q>t15*l?M|#Zx`>Q2sj^$MpW>mZa{qq{FVAY-f+(BMI&ZpNVXWRm{71Rg7QJ znRNRz_ZFu(no=LjFE-^AuZ379~2xl=|*ezymKnhqRqcSmdsv}WO@EJ4)jV236 zYB+JHRY2n%=JCR9B4f70)ohH{$PURw4x}be3T&3?xoUJD`La?9LGDLU6@H*$iJH6; z^*AajS>Z1x4zQY@8b?p(UVb_Is!*Zlu8OR71U!NyM`t4KDr;Qe4W-jQzo99k7QGPr zL0||ptVMO?g8>r~bah2=>l>%WzS_%4n3Obh}1^=`=hlI=X{UUV~I}9 zxpgUXD&$J*o&IVKPvoBHS22P!R&*9VZ)pyb53Wq@+Lc-vaodNMDdnF2{tVSi=rQn@ znm~%>-)}4i2-F?%hce zu1{sP=76O}inuH@PbUdgx>2W_ybiTRDw+OohsKp}|CA<+R_I=NC3}WOC&X;SA{hOX zQ`t|VDaM?F{O83dF6}gTu060BFo;C{%Auj;;CBBqXi~7)FDxih7X2nLoN#TxBVbRp zLFI3;OXwIY*0@;qUZ~$nP)aG~K`sY*jqk(OHQ&%LVQ8m;hiv;qB`ZHp; zX*j)7Vw;|23*stXiXa%V^x$AqIcs2!ZbSNIL3t&ckIK^@8loFb5&Jz4<937ZmLDr# z@pv`5UE+jVluJ=hIxh@;HZ+d4@lR!HeI=0L8LL6rddFTo%fmg2lqPu?r3esIZc< z0$PA{#D&g)sZf?>oGKn-FquFuIlO|}hhy_Zp6)1%7g2G;G=@Lc)6<2lYMZT19AOI| zuxHbqqKQ>Ca}`rE7UO=YHKLMu10CX6IQ#_H>!(6Xd@*8I0{s&LJ=#!cI{9Y(dRK0)Ws+@8yQ6ahWt(E$;T}qcXaFNE zP&N?oN(mY)Wfft736MBAN+lHLf9V%!a0I*qngpb4(Ww9iD#;*9n_yKVrQJ-bT*-*T z&c8@I?I)&yp4Wdqg9EK&rp!#P7xjo;8D2$9sAeW;>~M0*l<|yv*^HK7JesX>!YRt||@NCX$)8HwHRqFga0W|3Y1iu~p zc95(HFaBIRoM@>>HV|cWoxm+r^G2K94hUWN{}WuXg3yuEfJC&LS4wHT4Z&0or^BH* zv2ELwe@@u3<9KS1VH<+xZHz2O{{6_KtVzkmT&Z$(jgS=;$jV~OvRra-upKR9Yo0QR z_v|5#Hn5zeBp*ns?dXHayIwp#U+>QZmi&OPs`M~@mjE1(`(AVEKWG-uTJM#OR5J#_ej|)3FBHqV` z&Kx|LICLm+kUV3X8r1nA_`t}D`}l@bn;&&^bO2fOarkO>p0AU>y*kfnercerF7erC z`N!mZ?UUQAc40W6>dJlYD%3#UU$8SGR(o`&lWW#mre|ydkpO+r)z!6~GOpRS zHeFEDY^Hey1y;bF(F0zA=ky#0U8RG*cevZ<*ios3Ni-UhI!{qS2%oZ0a2J5mIih~z zXCSQc15Oe?OQR25sz{Z30$tI#!PI2AM6oQ#7Q{5X7*U=`_4IgE90ZQk$Q4Nc!3Tf+ z>*>?GSFA{WlH9uWvUlG3>t8?E_Q@yY>FZD5fTs8Wyz32d!H548B=KeMmElB->?s-F zLVi++MhoGpXwCik)v?~*zP{M%{QWi2sxUF3_GWM|pg}WS45*=ajCPDR2vtgI;3Yrk zA?Wj0@)k;F2<{&Qa(C1yDQ!Qk$OZ5QWCs;jS)7Swhc_sEn<^dt-vPCy}7#;o8;`I#&V=UPtIg}v|IL9YAPFvXLb*6$&v1e+U>r^WY9;2cv z`n+wtC9EB1Bwz68Lzbl>v&Zu(0uOyKYKy1yPHQHUqPb-OAM?#c@y(m#i~N4)&?J@E z0)zy*v7txGqfj5!23~Zr=B7AnXcBZtSpf871dA%^f1N*|Asj#$)&xL;4f^>T?N9++ zlHE8!0MYeO9LOHW#;zRRXF~i=UTo@xmHH@P2=a>w2XNw0qQ@oLljjR!;3q|CjAWv< zH6>&deNfL?Zk8&Y6%rEz62AANX#j;)(|JLO_b{EIt0?EQ1Yj8<8uXUuE6ImZje`9C z{afK8|6m=fwit$uKTP+VCI?Zgi@X-K*rmC`PivYvZO;x7>FgVCq~BGsY>I*;orZ%!XDBoj44#%&s@$q->2-B> zZ<`78A{}JfqmaAQ4t8Qc^iiA&@(kBd0If>;{l8HYgjta>i|mLkqEdxOQ6ru^glF@N zp}qk!jq!OdR<+7;rZHOWSFlCQ_#(h$w-f}Tq*%j9(wRxX_r@|&vikmtM|$*ISIJha zR($v^{n=;VTcw+MIMyHk{c_um&tHF?j>eRCnCs8l4OMx;<%LtS`AETvoz4E$VOM#a6Xz}s?{!TWeA^aao6w8t<=f1!19-nWI z(LCkY1X*dI6RKO^3A~5;_|njey<=HNeXgQ7fn8QU(2j;;sH%42B$bEYFF1u&6h~+iCm%;tjSV05;PQSUPYfPQ=OTY!>VyZ9aURT(?df8LM#b@#c#g< z{PT&o-}ctB|Ek?OH%CoRW0JkleKS2xu3Y>jo_ZQKVB1s}kAS%ZpC=mjtjBSFbgEz5 z*5j!MejE;>)|YfnK>hdLvbg8z@tPv^U!`AjSw2l9dWh+1f_)r7#1wteLLu7PIuMHu z#PWHl4J7txBee}^O1VOBf!|k~QC@lf-&10bJgFpot z^uQprU=sMJ2#6i1M~F)>fUqIROvEs#IU55-4t#>R0Rj+Y_2H7?J*au+-|*l-3-Oik zBzRfyV<%7zs+jA~gIvu9sv0tMB$PG!Rbi1| z=W_(&87A&=5rd=Hcpnjj2(zpq&4jsG5_Bwo_{`N=2mvdzib0dlQIqS#ZC2njfdSWR69_DHngrFi4w zCwOV`#os-F-Sz$7P=kGa61S(lzG9R-`Y4;iyV!=r^H3d4=0M!S^n7-Ec=} z8ftX6fbsmIO9TDH(w&3iuwW}|bBbE(Y{iwO1}SL~L2GK78X4)qG~Xxx@ErE+vwuq5 zm$-8C=D%#)ws-U9zir#L`m+`4znPuk2B%6g9vh8~6Zb3sio3A|lb(<4We<^Jem_1W zy6LGJ`{Y}1rC%}qb>ICRu~^6b``)*q z1_-mi3B1`H^hcoyyD8w4jSw3lOtlrD($M6{vOfzMwd4E;^ooLiVtMZ0=!F3*s1PtZ z7XyLJSHG=(Uj_7EYMBSMiCnj3tAMR02xT1SPbeMsq%;}8?(bDNLTO3FY0idXLN+H{ zMeS0mcIKf0%c!LJ^)!&Mf~}5u>+QlP6e+hj+Q@n(UWup4C%#Ky6ZiyX!5uN&Nigh# z44{X$Cs4@q#U1yXsbXSqm@p2hoG!{l8uoRmoQaens`GnBx&{UlF7_3D5VF3#iIgDz z-gvL@{C8_xQb6w)92jq}LNd@iV;nI`yVQqtts1SirTLWRo+g#Fvzg6beG|LE1^^VL z(yqeJTs)iQX#W7%)_$P96{a{^f%d~3t|LM$MO*6-?NLAJmuQb@Sv{2hKk4h1s17tY z;hgJCs@1NgXP1{LKnpd@Izx9D$I{i_JZym=6j&7J_u8DM3~U*~?yD%B&Q^VgH5ZBE z^=y$pPw$nhPH$}#k_ItV%SEq4c%LLBbISq}*4O54@{9h+Yv3DeeD0EhrgAIal{`=IM^4p z$jj-avx3BLPo)J{yvi%)!83do1~4_HW3w+RRU->NUp4{)YNNqBMZNW1UFU?+} z?mLs(aJT%>e8eBg=q~T^HE4eJ8#?8Gt`76pH^Q_+pRqn&rx1OxzN`M5Z{m)TH{M`= zqq1NNOeDfU#WK(Srrsb7*Z=MJ^_X@cvRkC82xUgE+2()$p18iLNj*P0YK~9FT=Dps zOjoc>@ybF~L#Vmbd|qpu(36U=t>}x}Nk`ZRy?{G)O~y?wH(3W~Rs;U%PkGOrl}!&bQ*X=OA|S@j62NZcH+gAl=M@Dg_{4y1zA2%(45MA%kyw_|gntCxT1n)D4f z;?$}XEyM7`ux!PZI^)l3LC5-oHux+jxhat4k6iV&3I#*WvH`9Q{ za|e%tIxRwTMVzKW&d`!gO%TWJ^FVCJ8C39cKz5e`Vb>RErFv1P- zF*u%=rl6ds&85a9Ljj+>zP#2DSmzcg^I3B-Z_mfl_DmS>#H95Ne!YoNx+FiiTJmeF zRJ{J<)#A1VOz_huq)x4(K~h&EVt3!e=S{ZrRt%PhU%0J*$XbwRil_IkoiBP$OOs?W zl!`E|fUYZFPWsj0Uft6R86 zQjh!wovjT7NNx|L<68?4=v}2TeJ7rnIATH)aa z{xyypq#mYrKvm!uI4|5{e0kWgf1CB3J<^9C!>+q7_1yU4*#;d$^ocrL?xy#l2N)}_WuJZdXm_89 zQKizSb&YnCj=6X?9q>`kcyZJ*n9FxLrVBq&3IAF>qrzwZ!Mho9sMRh;fE7d2ZpV*u z!2cIyirWBVMgm+bL*|?!S3a|goV75v&B-JYBGO^1 zW!lzPE#GsHb+6!QxTO5NL{k9^1!N^h_qyEofk8;B%2{@9AqNjc@4Zdm+nzQv{2(8P4(sNE?O zw5&EpSGgLMOh%tc0%AAiC=>ZUec5*HL}S)=S$FOn*%i6;)$aO*4%?~h&)X!YJ4_>w zYj}q4{P3#}bXCknc`9>$_sagHYeOm<4|`JHa7&l9EtkuJeoerf;d#)1W&;kcZf{a4 zV{mO>6rJVO#_=i)3c$+YUyqSFoE5NqO=u?Z=h&5aT@5Dp!0M8u9@4kMNvuW#ovlPv z$-`RPu)`WP5i5&}Hj9{_tti|Ti$EF;no?dP6^oes@d%a1j=e*#UCZloT(O=#a*5m1 zlomAWu*HPz?k6rDw++4l4F6%M8b9|>Tes@cNHS7 zo}S|3W*+a;#gP&G*y3w=kjrWR81h9Gz_-nVSBC1Z9G27P$5-WCV0I2{>uVl2Vs9H? z4weTQd5BrLuRO&e<VM9rhIA$BP)fGSHIKSXU@wrxwXCX`Lb!2=*-2P8DFTSU)vwg=Kd$wwnfk0 z84hDv1*6=dOAuLWpQ##E-Onq# zVUIAsGNKR8xkd75$wIc68G^KA;zjIF_4T#c)?smX+*Ko}d4b0l`TE+Ng?5Lq_L`L$ z?EVbQJJak>aki^8`r4g+1E#DZUD{RI)|RtxN@pm4EaghKj+=V(siOJ9@p2+#4$TKu z=C)IY;?ZE=WLfj+Kd?*LEhLOBqH9KdkszynaZk6`d}ro#$EDkOcr7DchMV5j#;`>sTP)LQ2y$(&;9~EYIQ4GAP@Nsu)7T~ zKeC*sKoca@xEZbyB8V@U*ORa-Tou>;1cHg6+3<(nNU@it-}-B&XlvF&0z%yH@hSw_ zQ;e&78l`~_Qzm0c$4Dz3#%zbN&DW+6A5O!_@m-6?4p{qqS7(2D^yQ+Ahm|8H6KQXtZT zw}aMnxwwV}q+X6o!eg6T0&a^gmFjG4^LwO%o(aRML6OQ=w&e*s6^~myiFXfa_fO51 zvF~PQ$1l}fyWxsl-(+;`*awU6{_VvV3&-$(ttMt(IXuv(hbt;?ZQatE={_e(`V+|* z)j4h-D2BV(-C@R+?%SKmo-sgLv#ySh*JayhS?tbi_Rj43^-DZ?2QLHHxRTpF@?|-! zdiE!(5^!aY^ZuJ`OJaE3vrw5|eiD={igeIF$gHAuHrT3U*{s(ki8f*0?P{t^v|ESr z4GMo)PxvfUm^3pXd|OUpx8_B(O2J==NiF8M+0=-)H#x10h}6Mq2D#fsx?K{Srk`zp zl^q#*a3!yCxH9(6b$x>-Mv*Jc^b8bZ9jBF20T18fMI_K);A^8>ag(c zL#Gzi*)UJ0M`Hj(B+^KJaEE8__4tQNd#0>9buvII^d5>c3+ifnpR!tFo4c#tV zBj4I%G*~8VLIn}khAB%p6w?Jm*hg1i&0bx9)3vADGPVu9#kGkvvwQUU{^S6?emy<# zG^4Bc#yp;wRHdsI;Ji8iaNJ=25yGke|VqfKwp2&ni7%<;T-ZeY!%sMxA z=~`w??YU57re$%OtxSIz29FZTq0!Kjjo^ z-0eCtX(gkyG4IEO*If78qxcba-z~NHb=ck`SnYY1no?uLLj{eJG^DIE`A+QH=bp=s zrD6_(q}>{k-J*1R2z+HvdGtky!p#}bRT~Q%TS|^KR5|ZT=6z&)zqvnIN*&$1ckd5l zU1y!u%FULb-nnC7FEijHEaU2aT}ptceQs?Kk6947F2cFQ_NLM-4$!;zi{)d^b6kV-t`3^N~k@4^V&TV;~E?VEehq z3}tmt)q6=}fP&}1&}O{wA1UaARvNTcSmeX1+Hy`UxIn|=0jE=n^9z$2*Q8sdPDeH2 zq$LwcLNf>+C^E#Ln|*+W=KJz9ngw4#CJ*|th<5(s?Zqg)uxCu0K{%Ef_UAUW$`-ER z@$vldYxnj7GpIAU^IB(zvX=Esk)-T7J6RmIcjeQq9T%=PUp~qHW6MtCEK&!+2U#wM zO9B_5di2+|wtUD@xT?GYVVp`090GIcoLz~bYBhFRfpY3JWPD0A>~Ki=10%BG2@$M{ zS246X6Q-?X44)OhI=0PeX$zMUf=r#zH}^dD!+U=53&|;v+D_q%1I);4_g4B$IdwL> zcM!H*7tSw|aevVr&x}~Q(^U4;e`b2eH?Hw4JaAFnK;{PMZo{(d<9DTB#2)w6$!rxSetVvH>F~ENI)5|YU!sy1Tb|T+%=i)DOIfquAAZ8kXI1+yG6!WHT zk&4M1n1U@8OSrO8>~CQob()0P68Gmd_CdxolLSXn7m0e3dzej|m^};Uo3>9Rv8LYK z$ZJ37shEn&baq!|wl6upJxdco#*-=y*xO4v02x3ydJg~{3ZSHwMw_^NaM5@Q3+ zfG8*|MOq6|^=ca+gCB=NfSsOC+jROY30BPGlk?Lvy5P87ERRyARLqu&GiD};i}}Ng zkDuGT{Vdf@xPpB}{rC&oUjxD$_HqhfDMr{kc(tS1@mFr{8M5azx$L^0k>QkUHQ7pr z{B$_lHEJEsWwOs4OQ#Q{b}-D2)B&~`!F^?!Yp;cKM*C2I$on=g0oPZ>NgM?~=xbjg zctbFAf|wZP72p91!>e&MXlig0k+ue-&Uyae^>Zr|l-TLzghSVyqz1((jaJt(tqDy! z#R@v9qm$MoI5YtUpZaXYORv-@#tAScaCX#*O+s3m}I0<-g7Vh7B{d@U4hMA z@zNE{6<6@^>CD(Gw<2#wo^4s(x4u$zuTQoTNpH@Z?i@AtWn(?<3+%Z>w~1&fieQm; zqB*JbX~RRV0ly~W#m-zvvOhxTipe$l&NXYGt)xnuH$eBHxYi3%LkYS!C%-mr5RkUq ziRhu`2!wi^xd_p<{2WTcpvT;R0C-J^N3iWw-fe+=+hAj_)7d2K95Mu^oML&bq|L`| z@leo8MasBv@#EQ8Qj?UXT3Yh$;S9Tedd9Zdt#Z#AAY?4T z7ueu#jCW-1TbwHQjB$P37{0{5&CYu7Wte+28PI_l$l2Y-ivf)?TvzysO1~=aC&~se z1!#{dzkm{~DnDS8T2PLhZv29axOIsLF_0r*44X@)O-e}6b%|QR^Q%jD7~Fjpk;F^s z$&f7`jhNzo?6&>;OYu79j%;JFbN~JwF#1*&J@~y@S0y;&iK!^5MWL?$=79@eK6qX| zth(LP&s#CkI`YhI{e$L=JYC#foGa4K)nqyBgNgF!;Ba%Llq)X&;5VsbVt%I9u3#ih zucUsHNcx)NxL_(!Ex$@P_yq3VC=Cn|1vG%4T^Lp?2MWQF5&pUR zQV%^?H#PC;k&YAw%H(KMvgRedz_N`{G2+BqN`w8pO0IZIAux`*fU&l5ekft zo3;n+uE^gS6P+u&U{73Ws8gZD9&U{YH?b{0)5)52;eMPM?HYUO$iRR(ugsKB>t5fV z_pVJQ6aIK68f~A@52kY+>_>a{WcMN)gZfjC@O6LZ~F zHmLd%OY(xa=kr9=7i?0k{UDY@o+&cpsIeKq8$d&2_bP)vf(c*}T~kJ7{Mq8EYBfc6 zhzf3pwAR`ZPzJhPVnvWP&{12Ih#IJ9q>#pbZa!tyFuB&I-!@~KT(7$DP~H9aUww7* z5uY!4&N=7Qo_`Mh?ay}IysyKw>apvwY{4e;cP6iR8q#hS(9q>A`EZA`WErFYdf&@< zLtQ?UpVU8FZArst`A{s#JHEIZ%o53fT!RNZ<0ZOBVC zL(a5XEr9NC1Q*YNV=i?Efp{&6)X9oKOV$dJ=?n64-vOnTH;*i#Q1h}2y3SLfWvaa) z1DL;ZeIM;jjj?-Dd0VrFCi}!)ZtzA5eFpcqy+KCD4B?!CiBftd_&@r&cJDdl<mDR&J_ZIMC z6Yi9YDURDlm{jJ*^68BCl&z^xKLn_S!SK)4ts^1%12>eDJO;T8lt*rZOva|KZL6Rq zZh1LORUdIwTml2;!g`o-0^h!_YU#f)^xSKl65`^2?A(0Hq*l{ZNu2RI!LW~*0@F@W zQ?zJojao7kWe(GrY|Civ4Awj@Xu0@*-}P=9X&&C095Pb#6!%V!>#N zh;&ZbMv9R(T2RZ>G&d$2#G8`ARwv``314(^yx+~(x?)N8pE@fQ>oH|@$za+VSErgB zYOgNqp~9Af0jtET%>(8p$YmZdSFZxO)Pa9L2G$MK7pgiO4`n1HLHPpzfZTz~K3O94 zOQ!E+v7%DVwQ!(84KB;by9zeV)nLtJbXGLxv^QcHA7ig{L<8%Hlu~zov1D%sG7pP- zJ#Hz#utpu8aR}69TGK)rQ!&y=(%8|<8QV1BO0T#7;23j1Ak{V%n*C9deNGXRgc2tF z*U_gJ*cXCPMOtjO>amB`5;5=Q{qlIkF|%NT)zE+{tMRV#`s#>T*JIr4HE<)T+Py zJp3s4+tg}!FX#sN^?a@kefgJi2|N;C>0kw{(d+K?&3qD!Q4HKusoH^VD(BbM<1eO-!KRKLU9Z2XR>&Wf*gUk8fTf0!sIO5f5j5L7Rg!eQ*Dq_9X<`OS z>vyLW)G4FckymaWtUAQ2x~5Ca(7Jp9LO_2gJ#QN>6tasW%DCL?muuBF%9S<|krqW# zR#Tv*&OwP>B^GMMo~917oOV$`A{k303^KLGtYMJuO>>w!T_6{B(7k;i8{pfl;3i6z z>5h(tBQfW0)n_NtW` z?1>EfFq-?tE=^}rg-9k5>~cCBE#Dp#7EE;a=70dZ`oi;J^W3KA@9wQYR-jPcQkd<@ z_3TZ?eNl$;RVEE1S%R_;6731+R3Wk037_@|!CZ3<=;18nN{>PvTMf2}ix^RhHu!BQ zH;SU7rTi$Mo^osrNtCL+0`C9+do%dW7&Pf;5RAifPh>Fx`#-Ne&%T#0+6|bcknCt^ z_xhALCa8?4eY36xWlI>gKHDJrwWOoiyX@_-QycEr)3@A0ufoPZoTbOuqqtzaZRF|8 ztd${4L76Qi2HMW-$-A~^V#%C6O-DLM^*x1L8+*;z7zqP=bt@vDz0SLzH^k+G#-UI1 zG;X&!2p&3bu1$?Q_AkH-AnOS)EL9J_bpA^xe*yy_7Y4D5T3x5)s{j9nPOv;sgK?@7 z-)Nd&?Vg>|^#*f3dcerY)!4b38ci(NE^INGCA=C@O{Fo=o%5yJJdJFxQbiE0qO8*< zsWp~-w)wn3Ztb=d2@4&eEOgjM=drT{Ua>mr^?r+laT`U@x0diFHRe8t4rg1MBT_&w zP-A}%X2_BQ|MvF7l~C*XE8ANGnSGU}Sde|#<%N2LPVYSwnmlWPcE;wm^j-Sgw}<)- zDae-YC{L@im5-cj9}#;psi-%d zSbQUqavIfiqQ9ZTJ&bSt+Qi1>m%=QhMdvb*y_F0{ut?_!XPXj(yicPW~n{uQnq6(OD z&Y(HtN<=ihB`0V5X~>J*1-;1>*nKqJw1LZfBJ1+GaY1>0G|>zE-LyOuw$up6(W#pC zhw{*huj#p?COT9#Ks6`cU3|V%Pk2SO>2cqw8M#*Ysl!V--53_Gr}?8|@Zy`Y&x`d@ zEotv;l!|1+CVuVWRn5(GyjRrbaX0ZTWuHFO4c$pWS`&}k5qmRqpf~yE8*hky^rOi5 z_)Ga*IkDc{G}#lHTHE*f4eb5vp6D{=ADzYy9y%1ui*xz>JBPO4-tf+)gxjncAZRS` z+O<$~O+lq(Qzc$;c{%+T_O0Z7z*5{C@H60_x4~QlszJ9xZ_fl5?=5%hK?0=8 zPW+QoD>fs_v1vHSNP5olK_3vA%d9SrK+SbZpOGEPk>fsFUr- zR>Z%ze?PCSV}B&&@+Sy)B$+&?yRENd^zCb*E>uwFN;`X3&Je-Vsa!Y)EfqxfsG+kn zn|rcva;ma{O;H|NV>umfX43%+DN#>8{w-_+d*vUdrsC63W8iG)Lp(I=isHLgz<7h6 z12jJm^g%NLbtLCrpe88(*9`5D)d2fcjgDRDgU}O@L*a4`NJVlX4kVgjU!$VIWTkDU zdLw4bY&E5Am_?go4>D~QjVclEZYa4RFAt>`?=X~Jk^y}pVWwaKT-J|`s>sHF*uogC zURBZ^V6>ZN&8MywKlv;^r`9GI3!}k|SK;_W_(<|Q+Yat%v1GTMv-4^TMwf0Zsd9v; z=ljEz(HCxpL}pr%DK2!6^f9hARE9~q)8Rz#xM`r=ntN5#q9iixw$@FSMC)mZ=}3Y) zC6@|1ZSjD1Ej!gq-nJTuw=`)+)GpbJ8ZH&gQ5Swe!W9Gkh&^id1OrBEy6mdG{zeAF zZn>esUP!q++nSWUt#5%7WiT*AMwIkAWqebQ+Zvp(iWIS=HBHH>+AyhzI5v%5I;y_e zCKF+AGVE0h#;-qouIZ&#)46+;JGQz7wOEda<4-4&rd*KyttV3sCbx;berYmX$ogVr zZz$Xs#kO>phF`z8Qqg6avgOSk(OFxK0i%vek5J+y=4 zD-7=m8g98O9Nfbyd>NnzaY^*17)sRe4*G~jLw7Jq)y~clJ9pNWl8Xn4fLRG~lSdr1 zm_)T?tEM+2kl7>Js0$$TT{^cDUu#wP#kP9(yeXJB8QIX=n>g#Njtjd z=l*qTx(-&?zMZZ={QewI2VGz<{iMIokXB`jXSU9jQoY+U8IPCr2a=P`1%@pDY&#{f zuQ4t@o8*Zz*F(YS?Zx$vVyVxL0bS5s@V%hp`atI=ztH*M{vy{G_=U?r!Pl=Tv{e@m zLDD+7jBz!21$2WcSV9ez1^gPUY4KjH=1uwr`g(ft<6f`TprMe%0qHA#V#weeu!*Jq zs4*V2rXxv{kH&Sj1S!yLXP+GAVY(Mzq+fjTIu(R^SH5%=zIu$N#~wcW``kcx^!S$Fcf^QujJM~nA~Hx5tM!&n&TtrX;4?gHI! z=4Qf%VM>1|=q*GtPW2!#6z~R68WY`@nG0;;$?`8J2pLysRzWs^BN$K&fqZ@`#(+c& zSO@qe_{@8kCB*R>k88meB-Md$egIlQxOfL9eO22=f$SxSfrd_(y|K1fX&&hm$f6Wv zVDx|-qYGuQipLdgjLN=Q8&XM<;$Tyj;9#D z*Od#@Xmg+=HJjRyg1;}#CWoH6yAKM}@=VLF;)Y^N+b$*<@}&Z?!klr8jt(w%4z_94 zGxqh37ZlyH;6d-CzrpOWxV*9mao=sm=K5xBTXb)t>EcF~7}Ic;WECkU0gI3ATxHWFSaQZZ0GU4@LO;2dsTv znRaV`*Eb^DMzm8Mq8^j~pdq|B&R!Mu5Gu>Se3aj1I0QJBaW8y3G?$QObXTbQLfUYT zzD8LxX$>6;pm+*uWq0%Dpx3PG_pO0xFF>FGDuC!h>jeDa96;oja0+7SE8=<%0tjj$ z9MhwJYnDCX<$0Yi9}S2Dw_FqC1U`?0o>RjE{8#;F*tQfBn-Zn%j*B|aIdID@(BZV- zV~h&ZiZw|EUAXs|OIunNf2^kBP-U|_rM3BPZC|%YLL}vM)R>4x^Z_E6V$3RR?T(4ls+6<@|M1Zqd;eYJ)Zhw1Fp=}NhQBySba8Q zZB{4AA#ug+l-A@2O@T3oSV<(b2~-^-AfUjWxMlH$;i2N_3%3pqTC$34%gX#%d)Bcs(ajM4On_{g zH20M9*~R-92AinUNhyi0ZY+8Ed-@(%$TeVkDws9L+u8sl0QKbE$(sPGOmqF7n*a$J z&2F9v(hO&QoX~wf0bFG(aHOjk^-x=KksSFzl>!mJ=3o%&;6=QNEHF1o2w%aze-3WQ z+xo+Y?4h(UVU(Xw?2kWq?AUKDQPO4?V?v3bKE*fv@eeri&+b~>EBorcKxbsBCV3T_B{AqD()2h7Hjkk_08jGg6)WYCg`$&NF%Aaf|w0<_@% z0iqzXR4qFaQqH=g+R4}S08gMJ0z*ZT%S~T`YaVh(^^J3BednRXjk}}|k4Wp+n+%F8 zzSAf<^!5vx%nS4@nanG{VE-tPV`~>5Zv)6YXd@bEXm>cJ{BoDJbxbG^(RwCiO2!F8 zERNZ~iLJc)`ESx!zjEV^zqr-wrOeMtBzO0Cgf&)m%tGiLjilUEhdTsrTTJV@p4b#R zbztDs&?e$~jW32ztfWR>`5xG&ye5}Ft#U>_n>v(82K=c&s4!uu(8<>8vv3Qde_qYQ z3b{sGR;K~_rn$O!C*-dbfbCH}j>gM5EFdx(lH^PF){3Yj*W)9DUJ5GVKBHagX(2-`7PsH)bJL|n*6a;>-KP(w$6vh-Dt~EZrZC$(RnFR0 z5-s^aFdd9^j9Ge$WZ~2M-JQON9}EveXZ(7q4fC;Yv{6$O_Ef1<+Fgbl^1ox?e}9iR z!}V>U8X)qVmMr^{bBL@sx`XXTTL3uYQw!gGv3eW08Yxx2>+5kXyvqgpa~ZLh50BOG zyYO2Q=ZrMZ9L%Q9n$b;fPrTQdNaclFCPF=JSW}H`hc3vmPn`Yy zsD9#w^XNxD+-BbW&xf(M;{6WFKb1|7dgGq%L}N7Bi4%kUwoXFO;xf$}d<|BSs_fu1 zx`fp~pbgV}i^}8}Y_fYW!zc~MuvA(+yJdQU>b@jJ_(EYa(lKM|Z_lJy^FThKD**}w zlw(8b345z=x+C5>=LPo)YfduH2D=d?4=?h*7Yiez;%jzL?5}O(>q+)U^XjzI(cJEkCzUo>2Ft$b?$Q&@ z9d7nt#vv+7kC-~0t6E|$3oy7`4|NrU7tsSTH;ScNAg9p-o!F>ft!j>{(IL{iOEJM$ zgajvgu^4EFr9}t-29#5k4eY?r|N7U|#~;Hw79s2>5dPeii<^zwWTMoNh18iK+inf? zo5W4wh=C5+z&Wx6qd3KSvFiYuc$&MLcJ0(%w1ADPZ)nmrsBbUg7t`^fp*S63;&JB4 zY;y9K*TCd&23m2?=vkj7Tx*#^IOt9WLp_6*?pC6my&U!ncFsnlfkO~5H!wMv2Vpec=dQs{ws#NLH@DWaf6m1!xp43 zAd3TD7H}IdB;qYTs0B+^c|cNvtEeJ-h&JD#UF4)?DZX-XYGqj!fHvW~VAFwKOUC+y zW$0|HV|oq#0b_$Q8rEeU)>zzQwZtMJ2H&0(*JvVvbVJf=YT)teaCTl8Peh^-8J-iF z1%XwyJF)yJ^~XN&G`cXetvMR8JJJgB6QQ>*B$Sx3aS{6{51VNKfRj&>3S2&}%sw?YVlW5P7en{tb_gP9U!6C~?7yG@B zJiex6L$ltz+S-4x*U`Rp--ZXE>kd{E*6PG5Wm8vkv&KGY$S1G3QqjlPE1DVJ_a`O_ zUgtP_|9n7Z9&0|}KfPfM?10-9;D^rf{c3v`e;&1|a5fNO$)>>mmB6P(bDqmF0qE;$ zJix^QNDWle!W{X5vrp!uecAVSfD*2*>u@dCQ9+mdo8`k_8#%lA_6k$ES6kfXd1A$9 z>o(Nx#neN>-fv(vi(8Y?%-zD$qfUS5z4b&17(TIg&g6U4|k3n0g!oD0b z2Cbez3VS@mK6K!~v3>iF9Z2oNg>#UG*?9To>t<+ke~`LSSj+IW5=Pv3gu-6>PJgdH zrN|Yx6j!$t+ICld&|^6EKodSZZ`Rdn)<=-&-|!php285m6o1gjO4O(Fed* z+XRpTa>t+J7b9pb-r1dz@PKbF6jUf0oIyrR2{k`X)H&^u;l)Fts70Yk#Ij<>Ws}rU zEqZ_1ENt=yjnRN98BXZ^R_x3Y^^8-{nS4xrw{Xi&YH(_D z(zkBa)}ww!WaOu_wdL{LdCw27ndVvg%Y!c+tMo&yGuv`n`^p02pG&pGVjjvz6vwQ6 zR4TKW5MOthQ;=@NUSQTIZ)4b5v3YUtfmgMnjT<)-8}1W|xg3=cW;Y*!n3Mp$;RUp& zC{!k@dbK)#jZ!M=|I=!r@Cz-Wb%7d2OBHyaeu0qHayK|$2z2ClmkXOsCaukwCJ`SL zbt2z1H>mTWRJu22NqP-bJZy>wG9Q18MLzi8fA{alB4?dN!xGYu81@s4{}%SAx0%Cl zTz~x=hneenjpHR~b?@&pW;D6n8GRc&vd&EmP56CGIFK5*wPiA839JZ@Lr!k-B0%M& z2eBOc*n+06Pwpj%9*v$ zW;Q77ga{PlTe>WI+lakh76-Vzm?;~9l1<{!_qZFjOCC^>l8BNu0FoCi4k@ugiCM%* zonultQ==4MziNACQqoFokex|R6O-|23L8u(u<2E8qputW2b6)z!Lg&z0s@hkj$!hiCt@in$=6||iucI*I4jcA&M z`sp1|qZR>jU77398-ZHa8ZOfWya5E|N~Oe?Is#5YKR5u`vw~!iZzC-z+9EA{*hvnD zf)LhKY6-sl*s12L5$nMnED<^lREHp9bxJNpiXCMZ&FLoXACrsUQ>e*!S3?PPizNG+ zJrxUI7Rt&p0ey;~)y{Azl)WTKPlsdArp+P7Bn)AO=s+c9_IpT)4idFq1c|ed@XI_Et=3 zRiV5UT>PAC9g|E%h+;I@x}cpU{jIiATfx^GXl~x)+qtu?Ev{&5t5GyGXnnnj=H6bV z1e59){_Fcc_yc=JeAJyW(ani`M5#8)9C~Z39(@1wXYYXTS%%&WgzGZJ<(3E(kS^U_ z%a1>Il$P>IRmMJ zr8(oehmIb7<{8$ZQig-A4QZ>nu{P4L?P(FnToE(vF$5xRT|g7`MjlqBVn*svlVFFm z;hF~aZ*%Kh3yxE}FHjJDIdUT28}EuO#QTVe38F8)5bJ^`Ca_<>j9kx*GFw_#80ktC zPcP-eQ5s6%1G?61!DsHyyK+}4a+k+iElgnXeI{1gOYOs8wZvgqT>*S51e!ene42v{ zeYIDoYiZ7D)e30M{hIO5Qn_YXH`MW$-F%V$$njN%42=WF? zngcO6a4U*Uv4u2)3XViyV<*Hw{8FECxLqKN(7HHfV8Vns8pR#38|g34Ai;kP6V#CT zalx?PTJzG~+-;Lp9#9){R%t|PWIMK zSV~Mfpr)d-?udfuxUywRTp5>jb_u#Wwos2g+Bsv2>0z2IF7QPnKyS2{8|_OyexAGx z9uUf0fgcZ^cr`4KtvHF{`7$>`S!h|aDwne*C_9B!E0d_U%jp1!(!nMnG+E2%4Tne# z1A;$+zjG~Hb4_eM#)dSQiir1!`yFOs9W|mFE{U4lDJ>B-C4x~)EF4UwFz5Ut{CR$U z{u=nh{^ESzQwm+V)*X)cTmsz2$}xNe`)8|@%9}ie{A$JS%NfkT_Tm4-evAphePMt6 z%F)4oV^*1IUpKURq_uZ%A?A*zebKH7b6*=-vJd4(AjjR<{#!kXY`C%MTOFGi zb7E{}{Ibv{rz{*Zn}a^R(;-eaubMj?3<5>dpS=lwQ3;&&wi;vi$wo*$c6c zvWo}H*UZkwr=~8OoyG6U>|KyO@ONQH`IJ@3$=8lRgK7r4@-|m?7R$Z6nM^PU<4zsp zhT&wqhwX`Duf|6od~kI1z-Yp^!DzB?H^j!y{3S^qMK{!6fO7G7LcA1(3P^R1H@38Y z02%t~9p#50DZivGIrzloajqB-uAH;~|F?UDcITpm2+H%juuO(Ol2EI1T6}f}pPIx^ z52rUq*R9*Q1s|Khr>0>xp&_w&O>-`0g#aFy5wdqM@!i_4bXTkmG9Rgp#0-gG#1M?g zBj<+f{sVFB^!4j)uf2n-3_3!j6YQYROz!*DudpU;WCy*OePCbmOn7GTKN$Sfx%A}g zkaULpd%k5`<;+r>V?#Qd2!&~HvSrH9mrb^R`lxo)8FWq>o@wijUCY_=N1%rXVG4vG zr<8-v^5tykN#n{BI0#wpY7zsrHKAk%e1)_I*bq>MaOXlD47rPFjRr?3eznT?g{xIX ztV7I))F{SlR>z}VqN2ktsm-;ix?3BR-mummHc+vsp78iPoLI+CWUn6k7diX*Q;XDh zAJO$%!$IjiN>NH`NyK4_JjfXB4&R`b7RrvuYZDEc*MIgj+&=nY^@ht{za5)JGdI>kHGE>a5aq#cu0p<+z1kCae-fi zJQ~h_26_E@p}V?DIIfW7bjcyRkCJ)lbD)@k$O;`Zu=gA&OrH1zdae4?pdG6}wfq$F z{gIC3FF(LYtf{Uk1?zgC<)W5mb!15f0enH2HJik>8B%R5n;V2Kk3Q-!M52LaZ_rN> zJEIqRwU8bZT3cdM&=Tq zERuJ1**%SBzFFRSNkSgwn;ifuC%Qh<(CKqs>XGb4fP;M zBj?Ypslm3McE_~VDwfFX!Lhxwt84I0Tkf3EjUU=_Q>|7)NyHcJKTC0=h`7w#ad69# zTAhrReD~*rftA0%zg|OKasDsz#U2Iyf-v^t;8AP_WMU*QpLhCxbv>1YvJD&* zbU8s22;gEOu|O^__W&spZd$H-vd6v3NALUmWoWi`Aq)YqZcFAA1gE|kQ<2fGhO)~h z6`0aq{a``Z6bc(MPUwz+6%NsGDfVNj+eLfa5}Y|BWRKc9d>uV*S6$?Ef4w)devP}| zkaYb#=tx+4{3}aKo!R>l;!%#jrU zl-Nemen&d$=omJ4Lq5+nMebmKdFCw_^lK~UT)PJwq}jj)$)Gbk9O>oPMr$?VxKJ<} z87g>*>?M10l&5`Fq#w3BMr#$KNS$CJq9fhj&rT)DW5@nyi1Xv}93D>=^i&mJ0QnRM z%e3lOoDirjCBaTmR*?Hcj@Z&2iU!~_)Pfp9p)Eq!t%W-e(ew*tg<=qplH+1S2dJsf zRn_V=H>()$ZD?~^CAH;Xd<8`U8kz5icZfv%2EMEghHJ1e59jf%Y83!Z3Y{KD#@|>^ z3$k@F7`wT&;hLHT+PJuA2u97OhKq=sZn~!_)Og;enyn2mI|`62u-6$tO}k?)wVl6^ zt0IA%khWRHHSwgOi{dxg?UtZJm+*zOer?#w;NOy48YEs?wfGV2F?$cI{ot|7F7ry| z{i)Y8ydWW$`5Kxr$4z{n0sXdE0?4nC}qniv|mIsg&5?Xl)b;MY1M=Kqu5FVgW%wP}uz3 zV*vY{6?#d#|ZquOYd5nL{yJao`hixwb8AQIdi zs?9Mj>aV)s93xfdZ(slxjuKfb?nlvM&|vTw3GN9V7JA2t85T5H3&&i`CF|hG5r2Jg zCjmW$C*}*aYQm0%N?x;+J;Fb0VPSG|<;tztf!f+6UiY0l1yZY(&_pc!25BRoU)NZ} zuIti7MdBfs*{*W@v|XL36H9`QfZFekF(wtJh{k$^omQx>7~5@)#)?TGiTG8%s3{up z8v+5mRZ6M6jx>8=I(@d+(JbDvdiCt=`t|L+L)Lzs*LZ$SYlp`w^U{)4WSJ~bP)9kk z{Sx{ITZgZcE*#3coJpy^$A>*Re`@)H=Oa8n+Ws=Dz;!kr%!@JYWD{ zu60XaV%}D^>`WzMq$}-8_Kaw{)5-RSbbSW9e`X)+;p?NB3qS0p3h=8c0t*)_JP3Z> z2>2h>jS3a06kNIDTJS<{U2zvkdDWI!(BL(spW$Fn;)$F@7$Hl;hrAysMDzi0j>zIc zHygH$8Umh3^#G9@m>|(vqE&zlQ592jjZKwI2n5%{-b=nSuT#hUDs4m#w{F9L7jMiOJfgo|`op<1; zgw$Ab>CkT!`VD3k&RaW-DKvdH6-Vo!U0L+#9~M~8+iDOI6mYjLtW)3&>uj`}h_qB3h3Efw?D^~8Cxq3}Ebf@`n$ z{y)mz1HP$p{~zZ$Cu#Ohnlw9YlQc=Q_fB^UEiF(=X`!^V&@x&^%idd2Km|b&T&O6{ ztJgiaj_cmIySQ^LvF`7);e5o3aW;UJ&!Kfv%h2G%h=Ld)y?plF~O{2fRv!D<3Du>__sg@eRB zBCUsE-W%38^htdo@eA~f3cOe;bBv6dK3`; zig^n9JmEtT?;Nj}3Qr))gF}2H-&He-S?=hqlv(E+88*MFW}lbsDLq^!4Em;| zzr*cEvFkP(XI%hUeamv%nm!sg0c=61iedIUk8>e4St{%D-Zi04D;WxG$)Y$~>MpH)_!GNL8Q&I*M`lO0O9{ zig4 zcWMXZbC1n%j~qGTf@!F4tK`(TNK{~P9kDhQTdmej#j=1xSD=$KJv=M-)RRxD)lWR3 zb~ZPU9}hios!qR7-T_#l1g!d8KVGR6_#xMPR|>_fcvKIjZP0gMB+^2S95PT7<;K}F zb=8hdY1HhlNUm~azhp6~Bf>qaR>3IO-78kOIvq7}BtVfB!n>%s2aOlpQ%K!;!N(?3 zdxZrt)BjGx^9NA9a1Ry0zioi=2*A?Ccv*=A_ddnB=8&=)rn&R{dm!bNmDE=TDYxPc z3du^lN$;1n+})ct?LzGrtX@r4pIVKQ^C|>VG5U<+Hb2ZLkkzrB!Wu1-RsL8BPx}8> z!c31_AeWg7-Fb1A4#q_!Kn2WjLGGXCxccnM$@PC^8{`mK3o(QenC^{N zdq&0t%{b5D{FO?=GT0H)lPZk(mI;R4rPM3TDb!Exg2(pnzX5H(<(g}r=uD-^+Eb@& zZa`qn4kO?T)5qZ9x3nUIb)WUmMw?v) zAjH6gbD#l^vs(aCM3(oV$y*u7;nUhm{-aDJ{K=pM%@T-(3MlL*sUqqzNg>4i)}hq8 zrAzyj4`>I9f~wj2>{C9A-AnADzTb1_`ug&*Lwk0kaoLnwCeurJds%a5)G$l3>0=cY z$I>Tt95lgskK5$_iwEx=0XXZMa9$#ak@{fgK$4!>Iv^HIqV)Ix*l{f2^CJ1|or7q; zMe&$mf>o(NM5nz zj!eZfoz#bVMp#ruYLGPZMFyVKDFeNy6{r6R{dDljY9I%847S)|0u28D=W&fBj9}zE zeB+G290t{ZBR?keI=Ypb>|+E%w^#5kUcGt~YS_MZ?PUaq`fCv~ll%f^NnL6Vzu)Ck zG8im^daGm@Z1pk8?8AC4w^)$WB2E1BWb*Sk^{o!2cb4spdm}1nfm>YKWq2u-+dJGJ z|7KDSH4Na);wc;Gzza4C)}fK1Evp zc8ijFX`b!b+hKfsjM|Pip(+AxLHDN9!^y=-cv6QddQBCn%PRXVl~wB#if|(?;K4n( z9w!fzLg?uZX^Hua&NDg|2r>6YOB)2n6TThTrav&^;V=I#E-S0#$JI*K_V~8=6Ymlz z+qsJ@8Go0T!!1!pWGh#0Ldo^())BAnpcdP0;xtRCL5b|cL=80-eYYc(-LI}F5EiS>;Q|h98jz=0Eyyz0K*UPKKK9&LYN+L?m@!W=z3uE z$9+}MVZq0p(UPh@V^nQO&1s%qumC++xRzDGG(_VAf^Me+&XuuN@hmeiAx+etx(8iA zJ&BUkev$|&@Tg?fs4$u%lQNgxdSEO z^1?;VF;2@qu}ijisbsN*8GRp|?`z$>Jx@RKI&p-`lavdyIfK>AcHQgdf znVUj&-VK5%3WL`LUhqW+I%ZsL+20YYQ|zxgcI+-yQ>=Ag_GHC32Ck!P<;wLawPEGT zDUmIfDS=u=cx-cXW30A5x^r3@^<0|Vou0NUUY8l-yIGZN8RBI=uc-Jub6PC;?cg;3 ziYBFCxU{Yd-4U9)+!C60XRA+{XyDwPg>RoLx zY@F-fx}Ec%lW8bOA@0+T)p>OFI!aACsaLp{u2``iRc+a@;WE^6a&~O;gLCIS*b!X( z%heS~M15PKU16D4Vk_=2QwjqsQc-j|{c$Gqar!$=Za;IqnOb4KniXm>F{+WLTcB5K z7;cM&rQ-R!7(P3Ih|Y|XLk}H7RT_!;D)p6xQWzW;Yo!vT?fo=gIMzCM?l1~&7#zG1 znJ!rFe5}H-ZP+zub)2Y11hKTY$2{@Q)q*LJ%uC9@QjQW%Qd&bmXl_jAcm|$dnY*4J zpSj?%>4#clb=$9>Gk=;yYy|qmI2YE#Jm?nWgxETs7zKiW328ch37K<)|A$P+q5%v) zpab%H)|xc+7z;I|FEtLDZ3}9tS4rnb8LoL2Hc6^BE?pWZG?sj_sL?)q3Noj})F()4 zPdSKG_Iac=_UQdHGL*ppHg=jRN5a%M$>cZCEtU%|+Wy%I@1&;DJ$?k_7w*wx1XOMe zMrzZ?SCbH{K`4p+8d794Glbs z%Sybj5g7*-E?kSEYZolgw?ro&8KAW2qrTfagR|bbD1H2Rdi$H5gtjVUDV`vpcv?k0 zE5RxLGWB*M@pkH6t`_z+3Ucfgayr;IPQbii0kkB`2{K%sl~?~@+m5P#$ji)osiEK~ z@{W%J(H>*^%6LgF^=z@JrNV>+nHMW6Ud&`s@#PiN^C;9j zr_<+mk87)}U|h22a>#F}1igGASpW!>5ju~QfyNob#EIRo(J;A)CY|%FZxGAy%~9tC zy?`F8q@G+)#=cdtYpX{_R-@3$g$uP^=y6(yU_Am?oUAzYL{0s3T49ut5ktC7=99|G zPclCjsL;U8H>VG0a0D@@pK+Z)160@kU;3FwJwZqR*-etSeDWNi?qPrcRh%7(i(^lp zIFa}yJ9Qf~@oJi@U9@c3Iuu{Oem$}6BI=4VyEM&Ks*x)D8_c1cjo!K_xn)cCouW#v zRZz(&gy#JJOr`!w_vI&`PsG!Z+a-nF0(-)Irtc{BSum_dZyX3a9N~luENa?>IA7)! zfY=~!6Wrgu*{q94$A*avQ6!%j`vPptVBpG^UzSi;5G{`jB@E7nk&(41wPn+$f>5Z? znyPP^In~;SIDaAU+)&jjCJv=CR}^lh-T=w7S}O~aOjKz^!sKVE)Mv@lNGM=o{abPR zN01pDS}y1GZwS;bXombaEY|Wx9Iab)J5rOLq!bh`bevWyKT{; zu8TxbvaMeefo2&UPJsMWFS z)}b9Ya4b^>2ZvF~@SHiwef)TMXo%!wq3fV5r7kA6ociS{twp0VB@1MzF!^;V^>y+P zwJ3(v_!_yCN4dM<3fZWz+}{|7g~IS1jwGt!Aqp343}jCpW1r9c&T zXIR#@fq?}`yuHz{i&Xn<$(lT zrIG6zN?tMV>U7!aPUY1#$%H%Yw-mheO5){L3p}w)=44MWiaV`43;BvjZ1w6bD7|jk zvWq_30~X7xdlHvk8kR&H_KKnkTV<%l+FYI)Ku0D{q}n!&FLR1F8(9iYF(XY1pg1XW zBAq^wIb}jyR>C8;o;Q5P;OQ^F$#AOtL z2C3bN@A3U$KKTpXL(vFW1fB~^`*`O`Y2X{$6vt2+Rb_3ZcIU2&|PuevVQ*%Uhia9Ag@P z`cv`%*t9&rvt*FfuYqZJC9oRo>7U610`Ixsges(4@#*ua2vLZN|1Zp3Qp%a3D+zARNFc0vawoV>JGr|H1OcT}{u} zz;FVivNs>0Ah&i>C%c8ch|@)}nAI=X_=Z`-!=Rg1&z}$Tbpyf|XRI-cJtcwK1Up_J zkd2MMhrWLAg%?ER7-NkjDXuHdF4c-7yvhPKLaF!D>GxCL3Z)MvtH-|pd% zHBJ_yhf}E=)5mGrc@g~ED)4b|)-PTGI0k;WD;9m1IU+gVjg){c1@VfY(tMym!O4I6*T;K%{W$v zT%|UDtf)3%q2@{&nh0LzhSrY#p2W~?WA|54e>D`S%`rw1k|n>4$H5o+m8U?u#E;2j ze1%0Ct1wL~TTq-V3{Bq#e3_zW*>wRXXM=bYt_#W4J6Urxfnc|xWF}|MpRd>eodGgC zE5u=CU@hnf$O!wXbbhhDNJ7mNgcleinK{VpDWjfX)GWM2v~|Ubbtt`I$&wMN7d zm-lmLTGXSnJV}mug{@30*Q8@xyq1myvamh-H;q_dnk)vC39Gu)Z@L01OX#U7(0nag zRN@7a?CFWln31Ec%G3V^zQ%JQ&9v4A3mM1obD$!Ym1j;$fysvepw?o|d8OyAPZB0> z|8Mq#A0K}B7Oh4*X}Krq+*q&Cs&09R>PVptE9$eP>YKE1^TLJeQSJ6st9E^GXCiTD z>Y8AYJW;WKUf&JgDn()RniP6rWq5FJcB2tMoeCWir~i}5{3lIOCRz?@>(48}{&)R)rY2V*=Sbn z1nWjdRw4hYg$swXanzW2^UXI2Uplqp%VBHF_Ul$5B|DJvu(Pl0c}`JA|4ZtFWb%X5 z=j#8Uebk()mUV)s;2L_fOD{L$a*y`nX?tXN2D3rY5@x8>> z3)9qRNR|HjYt*>NaitXHo z$!p0rz#pn0->L~>tmk&f&EFKB5e)YR1P-BeurJPr(R14W{M7)p;{X~u6F?)cWsv2u zcAv|1(Zkg_qat~B)}i}~43T1pa8Z0ReRqhvaL$}X$h2f=sLOPNBkZ^?+0=jQsLI@vj3BB_k{}*&uBtU}=m?iDm}Ki3_T2a4XPp8u=7ro$ zN7}lrNdI({?W&nQdl0FHdV0p2QRf#$0~eH3eo(yof+|$4AE)wa$Lpx~Qx(oo6Dg)P zCi;@IvJaaWW&8|Vgk(9Ojrvu@nK>opSUTa7nVHxX3p2H1cDt$5WHEOch)H7|iJ7pb z0+4kNfxkBe^tS`_M+9WuRG_cCWCPU=_Xi9^YjC`PKayLnS}xBn06^s^5C3R>$@EH1 z47_HT({z<2vCOVsySCj4dZ>d*A$L}y+A$yIk+b9ttP?l;FSPP<|G6t0I&ma7kPZh3cBhYJISyC zzb6DRM6r)Z16X;tl_qyO5QLVGoo0XJt3{KE-tQN%5<9;;YM*p1u85$|{pc;$?s!I~~ zsg>7U5pOMfq*xHl06f7+f0l;lkA#Y)inUv8vV^j@Cg!V4fZ>3BnulPHpdjyKK@jl& zM0cny>F!xNr$l&7D2F&NUOQEVRyy%KSEK zAW^hrqjnOb>bfYiSuI$h^T{ZMCpWoM8g4lK3nE&z~iHaK`$G1C+IRY$O51vR`Vn(h zXQ$DpAS#<3-bPn@&|>5LCUo1J6RGt}kdmuc8_YcFr6o(2c&nFemmIyz$;*fc5T4l+ z99A($I}uLux5P)u%nzy|X`&+Wr2Fo*p8yXV27)4>Yzsx?;~| zp9;jgFh8fjr#nJgK_6B?JP3DdfmP$!6)jS+(RJ3G&5`hET`Sg!upJ6GH`tH!HpC3y z#hxF=19Ei+?Tx>Fk^S=X(_iON>XyvkHcQ3tX8EACWvo*0U8eNyzn%ov2+FhvVCRDl910X-c^jgp~bL z@^~tBJh^wqSM(oQSa3}9w#Ky@Mq2(Lel;jGiIXisHN!AP>4Dh zSanr!c4Thw8p2ln&}S?3P1}xaK+f#$Vx5~z5@H6MMBL3~hrEM)gO_eRs84ym0rYfuy@`B_XzX*a0dGb7Kfx&tHIb57S z>YQpJ7d@x3nkT9qnhtZar+D(9MecP@6gE4w)PGBn@0C}+{PLxj60b$0+20Bn)LZtx1?BQc6+ONxxokk zF^^Es-i8*XBQ2qp&}8cCmPi`CGZ{XnJcXUH*kG(Rk!X!U;WCyNI39H^^v&aely}me z6volOTY(#e3)eSd{FwCz^aiq%vGos*6x=*;2*yeb|(O-125-K#8FIc}* zxT3Fb2#E(~%|aWZFTWgp<(0jtch4((qkD*t?_a(8{x0!Q_5nkRwAj-nNR-BYO8UzY zYTi>%q08@D29F4cizS%P(~y63Gx#WS(A&Oz-7LP_LNJ+VOaR|A*4_}}!w|uF`~a8% z=yres$#Y$BFi+0-;N;DJu{AoKPHLg3BYfviYGHz_5>k(l)G=}4yv2)GBJ<$9d9@yN zRYYW67_Y8J_eou@Y1~dt5r=vLrSL_Zh&kH*)_^qJ5RWB`WwJy`_I_<^nyJSqW)hx1 z%K2zjN?clz%Qw99mit94AlF`OGp!RTP}wVet1q&D{Z9! zc5p_K0^{Uz_RqFm!sdVe%jZSpa>5WxfAGrSyfDUv^R9C5h}QCOknc7ZE@wLYiE_IL zIe&A+V*D_~@3#>8vS_i6O&unsci2Vhu~Vnc_@S$WWlvJQ}62-5n+Z^h@^>=33x)7l_OxH z{-Z~U#~=Ux!TpB&_JPc+f_&fIB*vL@T{m#FbQJmygRIN1GWSYa!9 zvN`?zi;#M7U|u%f3b= zU$*c!EnK(?xtI3!EkR>B(pUILdzPBTfwG`CqGqI797T;M_0mh|wQ&zF=Qf7Iai>06 zu(C_pJY{NKY2&T4+1-f=1Jq#v0@?9mBuRdigy-ooM({-H+x;`bWyz79bI8TPrZx$> zcrzq#U><&yya00ivBVk89t0j%=8!--R!biw2uUm`>7~GOAjj$q;hU8pkfiji*bvYn@9CDyJ@%sNE~vm{JB*4cd}L4JxW zLBhlr@%R^s(?vv0_F>erWlK)3R)O!jhnxy>ALnya<@t9E*4hNzxfC2C5Uu_{Jyfum zfld`l3=*DUbS!Cemn<)3X-Rt_H7=6~Cle>iqJgKguaU;5?EDqIy$g_Pu)p8!M88s7 zndsqCkCt1g6ORjx4K>7K4e&CKAkiV}wONKi}>Rg1Pu2z75q5r<{TAP{`}&2THR?4I&?mo01@Y^5b~? z-P^j*oN332_b`&5^o>@wL!`98f zAu{FsIdjsbs_z_=Gv>r3I|^sEl+~{Lv}Ul~ykLEaap#D2^X2@LUnCKi`e8&bF7y?; zh%0-p(=><_8U1xV*KX_p6Y1j>701)xiMR#mGEbvFP(IG{c5=QeJfF&THaSy? z1$EU+BJs$s@u_RCO=Vn4liC0VC-w&)1>IW*Rs!yTbf$mg+01(2S6bg<%FJJi9MOU; z0UZ42*b@Mr*ufy~Te3#CYA_mHF-6<9Jn_Nz_m3MFn;l;f>+St&HoD=fU$ajY1YVa5 zrNFGSz{%+9ibT<^E8bimj$}qY_%PmS3z|DCq8+F>ws7Glc);|=PvafYDxEmN08<+= zliwy1-zL9@(E1wa_Kh*xry*x(3fMj#@EF#fk-u;ja9%9`xn19Zob!nYn67cW2aL76 z**!|xXT%UQUw#0iO2KY|t)Q=Sl~))i&#)kogRU|FeH1E3a$i)ua+l6^r&$k53KS(~9EM zGnGpYuC3LK-FR@rjfwIK@tOo>)ANmz4C3=}7%d)$Fq$GBc9u&OVLy`cLgJxms_S;S zcN_?q6ADw&VJ&q(iYTf7m`nIPeX&@dlZENi$HDHegM1v^=LJVgs)2$a+J>nfATZd_ zJlW7b3bycRh8f-09PwbY1&c+TRe|4fy$o_t4O!Pq1<(VOsBXBuJpwhNZEcj148N@9 zsyljn2ayIM-Hmp1g!&B+GRa^6t_!+6TFDRO(YuX)->#ZqlU4e zr)WRmmJmuI&zLnbOnM*{&I}h&LLB+Z$fiw^E2*8N=A>S@a_-y(NB}iG%N{8ZqD3+o zBmh;({xZluyW3ms^tH;0k&yatKW7FZpfQjLx;5>4exD}8e zkXS4j(}V2}&}P_CfKSe7=KRrtM#If$SO6O0s+`~`FJS`|l9_OZKY8TQE5g*~=;im` zA?50!TSUxqk=(@#Ds4Aq|4uk>oD38taf|!=mmup%cXv4bb8G9RXmwM$z7s z03qW{sUD^*uO=>Aki9YzA^I02v|^`HqiI(eCKMLBDzi`MIL^XKf(_GI5Fdi>5PJVj zAVu>R9HIW%r7K@mGbv$oYTXs_UAtx&BridD4BIO&lN-q*$ov`Y)q!obbF2|qML1W( z%(CL+0}uh&8%skVZcMW|=QOO$?N5%b@!B!AFzPfBSAFV^wnL{K-R;H2N|(^Y_eiGR zTOeC{Tjazu=%NB z265SJyEw{{MG$`zG;rI}j)ox2EKQ|8O8=rU8Ay*DhR+~&`lEDjZ|_xb^eo`O#QU zoixOiZB}*lSSH)tB~lf7YSE(LHg%w|dC8JsU-nt*gwQgvuWvpQ&YLk~WVudUVp7RQ zI&LuSogQ(n=rnb&biMa|1V(2WYp!qR*zUjKiqD7ufjAhqF7cY)x zKWa#`Vd!Gwi)8YP#1DG(ZAM_y1>2~=;tA2_rQiHF)#DAjXJjG^7e=67sTT6`ZY8y# z8}yKiUj`Yuep+&Y<$i8^3RoQQ0Gu#wb;BaTvxIY{!Ll%XiXBnxVdXSG6beE;^q+h9 z5G@{FO>ISM)(s6^*i~gs>(N}_bW?juvu0ewoPTUbH|c`Lwz46!(jrzVvVZyI7qrDG zwu*{{)CW+udz1R+Q-{!YIv%`_EQIrJqVBm1=MKuyagP%pOxgB`)9P^c-`;ZOlikf3qJZon{_ z^ZoT_i~+KtqegJDI8iST3}b>g=fZnHH|@a!f@TE#{(o5lqlg{+i8GiR@sG9@B?FD& z-tdGGbM@k~&6iwr&5j*c?18Ru(tu)mSQRZc-1g+Is;caFgzQBxU)wr$>TJZFGi}-& z)P$*f=al`u;VDzT*H2wyx= z3*KoJLkS7z|BlE1o%&IS{xvfMSsL(1Xx9y7LI!;EYsnVKkq^@nvI;sO=O`m5zwv5w zRzi;VVbP;n?T=~=%SJGna_xG4pSB)`_7wnE(CmlhDo9`Ap--}>Ogbwx`HmEdZ{W*= z=2zZ&^2sNjpbik;!#<(F9h^IN2w{iSP_r3Ij_~6O(Ryw{fjZz?${RMB#0<_RhUk74 zLoj`I@$?B?lCcFnhF1F#H&W(IUehW_mzIWPgZ^rNjfXm_6L>@kQXoQ#oOefMISTYl z^&>Dag?g4q}m~??hZwq6Qev`1TtFj4C`A#ZW{8rS);fto*OmBjFtFV zG>oPgEG~im=qpblq4Ogz;N;=Wg59D8CwvGkVQ?FmQHYi}h}B;d7g&8evAx}gjt2$? z+JB)QW2l}9@g&YdV_)AOQVb0Y%teB|uk7Fd%HCu9_a7S=h|ZgL9e&pzUb!;d9_k5n zBRBP0cc3R^&@&-iNeY-8L?k}~Ihy=Vi{5K!XjoHUUmxo3-qQmQEJN9VL%#|>y%KBz zoJHu*Q^#bW4QvR5`lGc0UO9Zul!PI5kCv>o0hIeDn2WFn=T`qKO;Fkjl__9WvgpJ= zuovm76k^6p0fgQ}C*DN`zBfYDwuPwlRW(Q6F>TsxMD|Wyv2GnA*RI_RFT~olAxeV2 zj!{iAw3x@#M!Z+EulAHMS=46T8e)hTT6%Mh&sTGJ$EDkC1sYOAdOY->h3!DJ3rJhaK*2z} z*s9AVg0rkxo3Q>ge?$?M(Q2lCVcW;snEH02O{ckrtpjp^bqPkEeE zOlq%ro#8?OR6B)T^7lm5vAGX3{D1KaM*8~Zfc^kbd$A1~wFD5MZ&up1E!xw_Kh2^U z;1(j&YO}_#m+X!G`fKbolm-&Aa8G2WcyVNUjOB6bV)vH{v_W-xk*ZuCYHH(9cWXo* zLBfDoiSH8d{7Hu*;e^>&sntx3G=?GIbGHndHV%L+qvicI7-l0-S*h+@W`eABtN? zp#wS@z=U(Z@E@X4BKS`z5|~sNWJ~v1whx=?lG4%CFVV%(HaL<3; z)a|m_d-R09>gq4!Z@{%E(h7GiLlspXh zbqMs*e5fm420KZYQs5{$^qv{@uR=j^)&Nbx)+hGK@CN;^GXyQ2bG93;z|I(LxD^Nd za2|=Fw=)+&!X_{5_h`FQ;!;$oix$0gka>M!Q9DfcAP^B|GR%S1cI#$x+YOFL$YvA1 z7dv^9dWvD)o-*<^;F}B}&Ole!Y_$Hh*QDk4X`Bgq9q;Y8S4Wt~pJ7e!fkHfM3-yH_ zu%aU39{&_^U9;FLVX0+Ip@gkuOYEgZCaX;77q-56&4=^#<2GHk5EWb<^<`f)^2%hK z3{!nN@liDTQQ~X8BU}qD2SZ$?(ih%YfI>_~fLAc&h?x2|7}KWOB03KGDD34p$cGFA zx&d)=h*q4V$-$4!%VzwFU{^*zJ^#lrs`W>yR)TBuAs7t83_B6IH-Z~pytu!AXehjJ z;jCGZk9=@`Umpw!%6&0H6*1}@TVw*|#P03^BpRGLm7@}bSm-y(NAQ43&~d5`@;W+e zqmx3d(az3jYiLpwHRG2^T^*!Q;xA@z)X4dkf)vD4dC89xiI0=Nkp)ZGO zpJNT3yMIjRS_qn}7_7(qpCIBed!S1b5Xe9BHdS;HLQF0Z6=jZZ_=kA7vY`EU3>jF{aV3d-RL~7I|6K;n4Lfgb%luR)-AI9XE^k zd)|7%<9Q+eN+|ToKd65T_^5Nx;@wn}y^HC;!zV0gDemc+ix}NAX3RpeOHj|=7cPnK zeeJ-3e_S`VK4^MMKTa3bSw&%mk*^Y$8w1ATt3yMc1+A?MJVT+Y%hZ9fwsOt=2BA-s zL4_Ou2|kI(KS@%0^gJFr;KuKcRaRDht}0H#a3LJ~d=By}@q8)=SVOqyGN6Z;e`rI1 zCKHU9{V7I(@!)f6kYN}-G`GRh9IS47hiOHdH@|@-;qNMORhAr}gF~UqE(;x^<}=*a zx<&H`1_ls!ZeQO-A;AmFJkEMvjX}?)KJ+TQMq@antu;7oU8XG;mQ3cPuL}#DnMIxe zIiV|xyZlGHy1KF<=oll2YmqSeMHHT=3z_J#?B7kL;wXf#kX9Wk424UpsMnD>vAERX zJk*%%D-a~uT49{WA54A+qr)Mo5cj_f(|LDhKo!-rhc(&<80$ok;1bC5D)>_smkZ!v z5D41%U^9?@Cj&mp^JdN&Q-Ws#+qN4$O~s<5e9JoF>RGeqA?aXmZ$EklFoLmEJz>kf zee3Y>)$^`dy!e5RUCELAq#^93pH2AuoS0_h@1Lv3ZB1d z31K>Iuc)~7p6%H1qu2tg&RY`El=$0%p-JIV*%OuJ4mxKW&n$ilzra>FOE;S-Rk;M)>BNgVZ-@aOZO`87vc7Xn3j7 z-pH<0OJ>b#*#G|2{VgWPI8L=jI>cVuaPa;1CSvZ6ZMW}eSG8@vc{g(1OIatS3V5R5 z68}mj|CKmdf?jHFrXCfmcwRx=<*N6p%5?T)?u4V*DOLsCmP&Vdt5PMbvzwg4!l=wE zS5-M}lb8Zb8@c$<&0tT8!SjxR9-2+hVFXP}dyq7i0J6f2zW_7>+k$xkzVg4Aheix@ z8s}Vbj6cRAndU(u2zXF2883cT#J2Js^&qR|tPtdiO39~^|vSoQvtFXd0C z7x@;|`_VM`1n&{9%f-IcIOv_-3BAU~Kw3N&2DH8cmdN9C!BLHKmMTg>ba?QLlmGW0 zH{)$SV{Fnp1iKU_YHhxNcd{+iT@>}qS-CPWZ{C1XV`eMGoTw$LNA`wtUvR29(yd9k z=0RcSMHdY#4Q7^7%Bl8F&u$<+R}_h-x3;!_JbA)|_I`Ji`V{GhdK{s>zfQZ9%N z4Mi6`ZZ?L+s`XV3)j>afnIL_nL7X-@kYih2M(lT$6$}-P%Qik1|QHD6TchHX$IR4yWTmg z5pTku`~prhIA2*|v=f`3)=&^8Dbm)CIiKObaK5Cz#*)a0XGy)nB%8Sz3ED{ZOcdEl zGW7)_sic5FF6a}+P{R~rwLIxEg)@>~kzWw!p*2X}OLA6buO&_+?yR1knTRo`ySudN zA>RbSc8$WFKu;&AyU}=j5rL}yw9`L1;O<5pp2lFX$sOQ{+KL2xb%s}DW7>53(wL>z zQtp@(aL+(fL$$$RZ3yTvlT;ygwi7A&A?w`ismi7DO15?=I zrW&fzJ4JU1T7FB3{SE5J;?mUz4-!IN!R@DRKo`?`;PO$p>LJiR$?uIFaJE)M&5D0Vy z#{2!_16^fm%&QF0{ZE09SWxADB&XaV z*#*7Gn_?C== zyvQ_#QL#78@@Ceqay4s9Dm?Leg2@(KoJ~VQ3kWpoGh&!}52qXLD&UdE2=(HotCzKF znwRZfgXU4C`^P4os%o>phRN_SBwTL+cdWn3>~W57@>Tj%43?WI6Zp97Dwt1b(>O2M zU+<<)D_uU{l;GGPJg2Bkc%N|YB91wWj>U({xE$BAYEGjPX?u zIC(a+ZwmEK>hIg+(wL&?IN{pk22F|Z?}Hq`l}6T; zAWTfAQv(5upL?pe*Eeg{<-NW82WlbY{9_1rntlR3^u!M8PBZ~u#5L8SQ1!jkvl>>N zJjE(TWa6`U{IkSYW$5YZrMTnO1gbH#WJ#z3N93-I$K6n^5Bk4??(^3SSuufpRR+$@ z28nP^uU_!KMl}(8>dGXErP zR3^Pm7cjs6#&htXE+bEw*hSN3&t8b6v-|oC{dpJKtykb)icamB>_q`d3x6t|! zBD_{_W`Bz?RMsZx4^;cBeALVOaV(tsQwY_EP=iRF;waGr)eluIsiJ<^{FE7{p`nuE z)tfg1hgEFG>#l}4=?&y`$lT3kdrg8ljaXLTGYn(8Kl6gw052FWT6|iuKZ=zV-N@ve zjRAuOnrG2mLz~L5Zg`dbxp=jbH)Oj?m1)&XhsIL2)U#c{aqBN#t(q{u_+DYGafELg zZ@dj%ArKa13KfJ%x?krLst(_O9Xwv3${eMxDrvzL^y4k{y{C?Vczpy_Q~MxQe=oc~ za-EzM`V^{|gkdpi%LnuvrA{<+NvTf}EfYE|i2}Q#s??`ca};IPi8`fEq4YR>Wp1ml z{DjR~HqLgv??U$lKI)cgzkhyw%9oV{Jd?4 z;YV=XajIb6y)$Q0AM_s>KtGSC zj!%rb0$Mv-I`!^OHgs+_#9UIn+Ao}O_tZqlI(6lH@0FHQ4;D#lxhaMPk#U?L9skNg z$QRKMsu54Egp6A7H4~@b13B6NwqX#k*hyI9{4SB!q6Bj}h^55Y|0I!^V=yO`-NcW3bup zVmZ_>dZDWeMla0wHwPO7?S#E}nNu0Nkdo;bMn{YzN9w%m{fmZ;HqH0Ov)@$sQcDAC zkTXr~PL_lIP0>Esbnqs^kb3~>;#h59D4yN|dZiG;4u`xTMMin&&&;~8IY&M4V_+1X z+jtg(jqug;luM4nXad9E#PJ}ku5rpUR$_%p*fSr7#K^Mu;{2SI;N50vT7@99}Uup(up28-4!;POpc9o#ZEJx$e7E8}e+M5u78 zHC)fll*%_Vn3G-Gw>O%}J&Owb<(pZI5VPR*7ulk;x3ntF;fj<39-qTwp{&oB@l<)* zSVsOtgJ!HsT^5H=Gt>>bCg@ouOne-ReVjP0UsO?MHaWEKXNujWCOfy#Z1ES{;;l6@ zor(~(1}q_&AyF7Y0#=hZ#U%q=ji1Y^ZLCGT9&gI;h?;B5toH_c!t_wb)JV~DEe~*nJ=!{hIKkKjymDePbV&77;P}^xG zFWp66yq$a?FmP4y)?2T-g?Yh6j9r&84!$eTBpXjb#EW_xiocE%7XxAuq+Ym@6bAV$ zv|`z9XcoJO=jSL2(!uIr)0QoVe{KjgyxSP83D$?Htv3%-*Zcil-U(iKcJX8Ukai4c z;wa>ZJp}P{?B`bj64nbC;sro&d!dW*^;m;}n9iR(4i3_5#&+`WK9-oEiiW0{Jb~sk zIZzlz0hBoJ)q=%+o`{qzFpeN=9Lsp<#wR=T5flE%89K!caLI@;NdkclOwfO=^cdY0Kz&6j6-4w|m)|w$9GE zh}GB8F=raOgj!gll-P?z+_XSeKG5n`Xp%kBogGzyWsJYdMeNYK%h~rUgsd=%5(MHF zu!>}I15Y3zLIjr$0S*?6WHG2jJ=|S|9HqxWkIaOMIvNToD;!@Hk1eb(C3-|n3Rjyc zUM8*7P1dLy4TgA`KB%9hQ#I)g9=|qVqQ1II%eU~740$$}+V_p>@!M8FS6vR1sbtuM zWk#i4pt9J*C30$Axwpp45pYo%^@__~>)~+)$nq*1MwTknsuD-p&SfJT%yNN9Zu8t_ zR@j9loVY_|=bMCKjj_z4u!l3 z6MKUA@ZcCNq+hD^ThFNQb5uLlQ1n^Q%rJV&IZ1J*bIzzWK>sY9J^n*~uDswsf*8uh zOY>TcT*meku=&;!ebSJgS?p3#i0?18DXC|Ww}=s87LM>I3Ov36ks)5_l(`j6YE8Lz zqIr%`;1D}qoPwyZ=aj6Z3YzRUkutkbDB@&45t))or_^8ATr4UtnNqSuCUgklL;jPZ zP-s2V^{`Z+Y@I&6AHc`%ZkuOEYC$65O{Khvc~!Ey;)KhY4#oUx5u`BvYBKa~58j6g zskg3=vAw}@Y*0_$R;7R?;7mMra>V_eLTMxXO!&9cw zIs$X=s0D_nN97{>uzPTTAe7Excl7pyFv3C#s#4DSAEP`=hpI6j!>?g^CW99m#LgeZ zXp9qcBrr8;+I|umao+WX*ir7c+n}aVD@tiq|K2h(Lg`qnU*&UW&6y2G@=i?+ng25+zs1qjx{GJ&5X)=wgI(IrjB zj6c9>^p$xc)Ml2RSI(9zeuUwbLV;bVkPflRgjSxFjXG3;GO1mZeU;Q+o8j_cT29xG zGdem7n~2$2$eVip`Nz<6ue|^MMzy=B0R3hDA+xcfDcsrRH?B9!lYCiC?AkdCgKleg z^VCkSZG5yenl6^M8YQx5L<$qo%m$f0^W$#rGTYE5NPc9)LrS5y#MB#Hee z8vRe~G)sA7B(fwpJqXVwP2vW5&bGyLd5D|@6j}-%F|Ouo0The&hOlzPS045}vtjwI zfs$@){L`WbcJzGD!0IXR!tdh@F6SP@7oJHCs*)`#&vi1{M0pRQro*rmRvpmnk7r9VOjCmX#%aMxLi?LTcWj>%qkfY zi0xvtsJwuOuBE;SO0~LTuxEtgSvt3IWHCf=1`fbf!8x1FVyCqD6@iKEW)%xIusJ?9 z6V0A8ht$77wdjkABryng3KE}2qn{?SYQi=P9@%%j=6wdjfVp9|QZ~kz}l8;c*3jquGszT(=&_Gx}g*Azr|rv$Nzb)k*gG3#1vm~Slr+XSHHFz zsTiy;pZRbw(01$*WZ+t)aUK)slYsoe#{hS3g8VSZbcWOw5NrTCKvZrt)fx5&lnH#M zK;rxW9G1nJ`?>tWaif6T!V9Hfoe?D9eK#;3G>f=fqKyX+MsK*GDY_M1;2d!KWFBdV z6d%x=6@W}eqGe85*PC?`V+kV~v{uHUG595wOMP6mMOAm!34~FvZTp>8R%L|8{e`-5P38cajS!a&>m#9l z9%FT5q4oo7%P|O-0_Ku}u}A_A!OaW9IRT$PPY^{QVt|$Yur3z-x&&SHMQrJnSH`cn z0_{;~W(ySjfU!IvzP1oL#TKc`EfGt(a_aFGajBD$jFr_Tna$MUyTR(*REds0ley=f z%rlEtzdhd&Xt(iKJ|wL21lQiP>s2=K^@=;UPcV#Ie-l*L7nG}FeE!eWb(^EtpgzP9 zXD`8Rz;o-l59ShJB&`KvbYl>{`!kjpeRzMMMrrO)SSWvLe^?!?HvdPIWbOp5#NQ;^ z8FH1+sSh!^dzF^LqL?odh`BpouaoH9Opm`L9b}HB?iO9DG|TD=)R3%M$ZET#N<4Oo zRNbMH28y&YGrxrsVmI^AZ(E+9ubpc;cdQ8Q11(_d*p)G*u*!td_LD) zvHFHpsFYP+ln`=&8dg?bjb~5#cIuo zD=U`sx#dal;h!t229Z{hwGnuL;Cic z?J&}ToZwq$m3XaTzx)wJv8Gv5R&Fes`D%^GR)k3OjciZmu;4}m-t|ca8`^u_|%M|3f=q@AGjt@5>fy9;40mPLD9M=MD-qj-@Pl5 zlK1)Z$LA!QUVaoAxn^mM$5aT8s5;vgm0Nm!$g*B)+a9KxDw2tawYc8XqHd23B+3Kx zV`J%<+N0#dWCnNz=R{+xq!*^Mjlpv4Pg#cMcf9I6$H2A})l&+O$sRdY zq?i{gCS=r4k7+Nq@;>b5JIXwX*6-H6R6zc4=YIyZ4QmdsLv}$lDq>SVQ3rNuuR^_u z&){f9dS;5L6;W@R7nI27x5&(MOU^ zvsCiMg$ff4o&?2uR^Ez1WB~=Ua5pt&&U&Ol&0MB|g80NBpUlt+yre=5AsYtwVyRK* zm9Y(_UbarDX1hI}K!9KB(aCQqi}|)wj}fSoVbri%AMy*#QoGw$RwiUbsb{GlTmg^A z9Sq9lf*|^E;;9~);vzp^Fr#k#SdrFI8XEp(;WKQ4I(7NJHB~wR&q3UJ>#gshOCB9I zPX7AJhkF!DKQvTRTXdqt{Oy;bFvlv73YpZW)D;WeeeeAG55eU_L&tK3h}4Zj<;a!5 zw>kf<1#(Fm>6zw`y#{^;Yzihcy8j~-1G{wnb-NDSdvEC8d#}1G-HM*Bj50kcvQRxur19#kjG$nr z(op#S@%0^mQP$`GeBbx>-h1!8_sCt6%OnX2gbiWuJ!J{X5R@sPGTd4e5nNaYZn5r# zZU1&?)mm$pRcouY+{^#@zIO>=?eE}{++D`K&-a<1XE_}HRLbp>jeet5qKh(;sq>%B zo9A}udf#_(1*I8K7r6UipRm{a=n}FoMR)PK{I&N%-S@f(ffkd!>1MIAgJk zXS~oX;b%!=C2EIW2E|7o!4%>rsK3g@$j#y~Jh41CYS%D$1xZtjt0^ywYf5<-HmRyu z?AD5JF)D+GW+8n`X(v7-S`EMPyTBsg3Jvx{(8=3D8wsD?Ajkj$(?*~cM8IIGi$eVaD^lN$2+WBa zBajQ%0L#>a!3$jg4^m7nzoLDvZE`W*7f(;}t}4u5b=`II=Iz>PG>0TX$*7PzDfJva zy(L?{hrwIs)GOpZo!e{FcCQ;FYHxJ+Y9b6|mLkaOQ*R4f+; zeFvZkV8y4nYvQ-lM~A0hm70*4kerzc$Mu>HLr+L$tE6V`O;9^kp~=3h{(R-*4A$Sy zytW|U^3+d}g>BGgG@=iG{z`dr_)bo$;DxT${uI44#}N*yWJO0SwtM2f;y9Ad(cX;gh8leqo1irofcB+^Ozgs z%+~6isGqtYX@2|Lf8-YAa`UnTIh~qoGK}sN7N^`M@abdu4KHqai$#8O>aPp+4Q>R} z?szE{z{?~;hL(Faf|$D0SpnzF-(H;Fc>|h;6yhH0_{$R}q+m<$XWwxC`YA{9#xF3Q z+zc}W@2BUKO6mF)Twytut{udY?l6dNhjbdwOkjtDmNWz{=4oIwU>lkpK?tRw2Wtl7 zq8JZZV~nfN7yusQE5*sJq4o;kE_9Z9DE4&Xg&6gRfX;9Bo)`N>ewPLLb?Uh+ub*XP z@s%MK`=~6Jm5Bgo_PT^)UTzT^VvJDIor^O(dg^`Z4}UpxW)k&m^!6opEM0oXlGsvW z`KE_Yo_zT9D3>nSHN(`Y(&kIl{ZlRs8>iJh#?x*#dS?qfCV%QJBzg@jMRoUyYub!0 zt8U+i(mb~^2U_kk>iG89Q8XWExy*YZTi&sPdNhGH?Sd@W&Uz{g>I`vgdIB){MnI|N z!e;UvfW#jgcHgj5!yX0-(#ca;@B#b3aKMUlGBopGjR-yoJO(bS7u11z#MGtBR%kNg z*XwaVJPl>#KlA}<>2)K#K1?OYzYla6+a0T=g=(g#?i138JpKDGeKDN^&l64np9pl_ z(Vx-%)5gD#*08DHTjCs~(=GZ`E;0$`C$#>YIU7~wTTCJ`)9Nd6BPJy(EbkGHTAPGm zd0wk@%VZ97$%u_6MTq1qw8-UZo6=!XD7sek37Z;?Z8A1;a6jize!V{`@JNV7qEmK4 zI&tb$|Fmg*J1OuQgw%^Bm5a^G?RSw^PMH!I7|`iB(*5zNNosp+AV#R=PM`l_B6!Nj zW4oOpyH~4^rgb4pOPbBpdXnLY%Btdd6Dp4;?dB`2VWrPj5V1qje{7k-lR0{$lh0rC z41@LM^FN#0mOpbESvUr5QYrcUCr`vjSP5A)di{zE!?=APHdV(*`HT>|>quYUkq$%( z?1D+r9j$q(T0`>45WD+uL&M?Wi16*j&~7_myl?~kf@ku50REK(a0=Xk40{7Rju%klhmDp=X35DpDyx zzll^%>@Ah9Z=Xe-@0Ibzn(j9AhsMV00k>OR;;L1|Xv#dJ#X1&xP~n=!Laz5t*i|G1 zPrMA*l%@d&HhhGyVEf(0OO%0O{=p^2*+g>a3c+r{^&nnjI{i(yF-sg`5ro(0a+whq zp=YC~+5ZGjjnOg-eL+1c`Hm0?1RALTa_p}U4}_ZIoB4#LI;CPKHdSBb5pPy^S|>OI z4s%F|R^=j8vbA}j01AlMT`phWRQ-60R3OoAR1-_Hwh2A-b&LbN~Ew!>b`#bhk%|s9%bBliuMJ8f1d@bVqs<@hY4F>G7F87Lxx= z>=DjCO(FqVnT@`L>-koU_#&B9h)Bjuz3xykM|M9*SzP*(ScNuLrxLeQ8p8cxb*p)* zCFp>W%V()nOY`J>{gU=kOqE!oZRe-2rE>{>Y3fHkXK zfh(Po{r8d7F9L6NqS+C)CV>tDy#b0F;36lebyV0!ZFwY6ChlS~7%s0;Z-%;F4)Lt( zH18{`ghR%MqhC;u2)-4%<;+((T}XjoQ|PuOJ>Q7Wav&>x=`o zkVE8ljYh_iRJfxs2R6sZi2V%>`ze-EEYd9Shf+)90x1{G+B9y}{03KU^1Ml>sQF9o zv)uznN7uK#05LNQasqg6>|ONfDi9<)5uhu&Zj7LU?pWHN|{Jr59nHA_S&*AJt4&I$i0L>vKE3^P5* z>f9A8(C6B;EDDoN)>6Ib!R6O2?}+55UA3SjyHG4rfB*BHA-grkIrmbMYU(J@$n&H^ zVpp+vnBpr0B9&#b=|d!~QWv7LY=+fzO*f7;en(?_c-DL`SY$vwA=d{pMr-GqRKUeq z*-kd@hz4Z>X9rpZk2$fX6A;xyQUri`+QhhFB@O5q)}Ryu+d~=zoV%siCyzl9RaGSK!*{* zR_Yv96;a*$vYte?Pf5AcwrUcg+1$-{EcI!wYB*#UM_A1fyWOv!+oOHTn1QJiT9kn` zYk5o7oI30=aigZgsbET3kV}GNRE>A(loF|d*fPxn1JKgGl(TG!kEC?6m^79!0YBtn zHuGKN0L;^!3N{z2lLs@QAhy8tgS4TE69*b$Lkge3X~Pv-Kpwn=3pRh!3pnNK`Y;Wc z2}2VYFFr_tE`uUn`a7WEKn+&g&Wh3+8GIg~*94*g#-j(a4GyOWmVQ&~La=X@a>%rD zqWfNhe&ujmqcdjQSuALTZ@bOPOauBLOZaJOH z1*4XX%bQ$$jWndpcg&cO%#B(w+a#BniI|}5a7kmOKg`R8@@$3Dchq1IaFm)kE^DsE zTHPgS333#$Yn+Sa49{oAv(I=iQwZ;dYsWKK0Bxs3jlr=DOxTJ{tH&SHkGLMHMpS4a zQP*4?@R)({kbbX;+S?Cj@4WLCwDMMBCyQZrtMvu};yv$ul>FKv5u#iddYyV!^c^T0 zBoML1T*Ryn6GoLuQB3x4Swff+B$Q}mpd8F5QKm?}0w2(gg#tBy-bU~TL@pBN7V^7x=qO{5_y1{oh(sIW@~^C*pbKF)VL zSYRGlup!55by)}wLeE7{v%WEb!K=&w1M#rr`#}T2f|8kg?jbns;lOBT_0@aGNPJ>- zS2ywef?)L~(==<)j*Z^BkXFkCv)42^&6U;69>o9~p&>V_?|+|Kzp&s~mhJr2uW0?* z0{aHFN&k8>Qgh z0`+b2KH_PD#jrYzRwLpOCq28l@9M!%$&ZB)%;}@7k`h&(L8Xu_1{~$u6gb@Kl*ZpI*0y@;{ zo(ot?c8z&|9~dZD$Cf;u1J-ALzFY~Yc1V;L38xkI@4w>?qL)kPT~e2s$v_V|?`3>$ z5W(Wh4)jy%E#dD;E{`wdgP+5^HvmN5V4U&yI)XL&}BClz6KKC*ziEhG_$RmOQr;n-=$wk0*Woqpk0;CG7Y z^R3Nf9ma&H6s{5TaNId)IA}+7&@2wRp>YylCrD`mIGs+h)YhofOZ3ot)Lyh=eqY~Q z^#2%y)g!Xl#E3)OVcp4j+o(se61o|N*S#;`OIb1@Vxa1yH{RIaZ(6<_{Zw95`^!Fp zlooHLo_}hCxj*F<*&z=zZ86mlZRpNO^u)nJ;TB3`lp&c>)uAM0)j3b067}tS)US;r z4Xz&OXvXr{47u3jBpc2bPxrxf+i=Ept^H$qXiyq!PBoTf9Cv~w<7{gEy0CDP$Dzbh zV6n>_b|@*l-+CkQnvu^UBJJo5^(y~U0pduMQVzic$9nT&p7dtwpZNts(eQWQCFIhy zDqOZypL+KpW53TW2Co|(r;_PXPl^Mjt8i9_D5XgvH7_TG#bQ%$_01MyO<@eR@@eWr z^BALbVv_Ofv$SmXT=*q<8}KgfbeKkG-v4(`Ck+eYg7P6d1+ENODtO3-+J>4Kr+;7o zX=?|s4Y~ke|LbngnBvn<-+c3_Q^f}!xb@bPCwEoSe6kNsmokz~(|DsJHWH9C#to#P zxee`uFC+PU+(lsTU`-HhC8)4ME@5_@AUmsf$kF3407|Oy!uDtuLA~SHm=GN;pqtvE zP`N2SVg!7sB&@l`lgvpf9(<6G>F+K-3aS`zZAa2=LiKd4#7Yl zL;uI52OFYAokuusuSm^TCgOd*+?L$RmANf_8?~#;IqFY0I9(!Zl;iu;>aM+Z&jfp} zjd~`WZ!d6#QsTC9`BrL}QXtgzwASRL5#|UTBVACj9{~$K3;NY*bicl;9D@8IKOd$9 z4jO7E?k`Hx#y|)7KDSym$YDuX3k&lhY7QtkS+peppXe9~vTnC( z{qFbV$%GfSQlc)QLZE=4hw>uoA@+eSsBOnKq%aSt-I^)p>Q!W8^==t@f@I4ydb0}F znr0CTv4WX+(93{1Wl(9ffk8#ZDmkMa<@Z!1kyRn$dR>RkW*0gl|F6(AuFDtjHg>g5 zs%j+?ZBu-eAyQO4md&1$jj~KoxE13~@8xoBk=;6#RB1|NCAK!Gg|vBftzy6aj5!&B z_)3Bd7sArG&@RR_WDT20=SU1d!%;(67{pc=gGC@Coeh!Va$9h6u;-1(^Psx?xzg!c zcsQtNhXVfiCH_$gj~>1p23xrr_H6K-7R8Fh^3;=kNjFqSMRL@eXnR{XF^NSQZ5ADP zOAJpu>rcnn`&`!$kJ$J;lx#vfsV@aDK>r{pECKA=B$%kWfBCT$D~>HstRRBau%t7m zFid*>`L~G~qrz@cN*gHTi*2pc`XdpC$mu+5?&`2qTK+?YvJE4X92MXLx!k@n)PrL+ z5-1OuCHl$Al`HRu`$|kBjH6;cO9CcP0GwlZ9W{i_7HT#IwOF(SVr-zsc|UG;E>%h3 zE~?buouu*C`f3RF8(Qry3)ru8dSqdy+R_^^HX>lo^%nD)ct59JJY- zA=qlQUbS>ez%j)~y^Tgvf6d-;2ewy*3vU7@IEwZqmkwJ!Z1cqk#pYJkf)OAToPUi( z_!%61OXKdi0()jM;3_rH5n_#aEL~f1g<#@C-BH5~cA3(G;(FF4j#|R)R;fX~IWdHd zqnQ$cz9OSgHE+|z97qtTc*n$qV)pfGL0gL*b{${nkC=4qv&fI+G}MgI6ymt}hmkOsDdGsX(OT4NnT6$&UMK zol`t@Rg|N#u7a+2pg-OzdoS2jEpTUF-Ky4W12J=AQV#*!sCEP);z#hFi!=q>0``Ws z7Vrz`No>(G=0sF8Eq>atD_#==%QO1iR_+31L&2ZZyQm<5#EGSUcQSzCx zVsv&zc~`f0oM)!zpgKo!-)PhvM)6=D_$vDmxTL5#p*z+;rjw1N$^c3ksU_xk%)1Zc^Bs$lb#H9pa-a5@_$7VEX0#=WqcrFb<3oK zli+`55=to=)PV|Y*qB-Ixh9_fYG!X{+S>dyXehj4T7GF`*x^=+U7pPD>=7f?5G&FC z=so~uY?V2GXjBUkG!rzTbI^Z(csksuO~yXGXH){?QFu=p_&s1F@I(rz;BYEwqW}mD zP~?BKzJP;PSBK!C{RlW!2ZJ$jKpjGtLR|-b{?mptXYT4ZwVytCu;I|5$A;S`{e)Oc z0!7R=j>9e_5G7IFw(7y6s-K7}&-=?V|!!0zzatAZLIOAL@tohz9V@O`0ZP?L)gt z!<{o+AYYXW@6&snlrWLW3Hfjc!)i66DV+KODh%cx(zrmh{*Vo3awyg;5f|MDtnq1- zs_v0s|1=G<^f1x#yH=K}y=l*$;d!&VI!i&+Dt9)o9 zq7qV(gxUTOIlOvYfga)#DuWGnJuyh?Y4=T<6`cfA&$aS%1fP3DX6x3>E+J>mhabM) znfUiQs+34Z?e0iFw2Y_SDYjIM2=GE5R?DSEXY(>?ArdfS-vG}7x)0*$#IQ+#JmFeU zz@7k7!>3=bAYr?K$w5K5;SQ9bKhPwhMP-l{umrKL%hUg*%fY1`N(;~fj2SgY72^tQ zT%H25b1suPWq(^2HZD_L=UO~Uc4gzpv|2aoD!yvd!82#}?|<;Y^mES<>)_fgZk5Y~ zNO#dI+suB(AmoznMyP4~obxOn34~lR#N?EH;X<^4xQ^rO(Nq3~3%ewW6~=&?xN?1J zZAT_v(Wj#=nxHmovwHRRXj&ahR#I(=w&b{R$u??4$8boUrp{U>CaANq#_j@NBqacn zIYiBwtP+d$H=w0$`Pj9VdF~yRe;aLCRmO*X2o~7Lv*1IFfO?7^;GbGWN}ZNCXuVv` z)C>PE0x>x1`(NJsAOr=IfChC8s2ZH-Ldl07I(V>gvO8W$HqA6GYuSFo6HgGwKtGd4 zr%7i3Jo{$8&(t5uM|Ib6-ZG2DWU3P#q5dp*PXyFr0kuH?RQJuiX~vBB%$e~S zsGyK$ruehvKUU|CiBDYug%!E^)lG(JmWWO0a-dhQwYrwu`WvZdk_GHs$%uX6TvaKB zNUSr=)co?r{?sTzyUBl9($Y!J%b9>Pf43eRD z84V@@g5=XN$93Fy%w{rH0Lyib{~HFEE8)`+-M*Lcq|wOr z>(^6nC)+k`7#=;+(e2ibcY)8@1UCIn$lvhkzJNU7Sc`^1RTDI~<1HSzZa*%^v$x`O znjVN#k6b_{0lh%;LLCVmdOBO1^aBWSKJ&YE{eq8FBW3 z7Lp{Hq$1$Wr`@2S5u(-v(6tQ8C00uqE%%aL)mvrgF%BUyMhtS2O})^nsW_zNYlw!Z zDQV0^8!5^bdU%}BDM$!ie5Fy&mMbH|6w3r12s_7&ZnjBO%(yhl*NFnMgw!woZOLx2 z3T;t+L8&@yhz2rjvEZ9zvUFT4mFT^(iLkzm7nZv~E^#j--dCpu+tvxz4eG<>G$DXn zSW#TTKbWlsW~vv30X{qw`y_`FPD3162V;`Jf$kc=;m$ENE#UBl8;Fi|V-oTfph>?l zJcC|%jY%j!64|U4p4KF3i?_$xl4L8J(Aw-8oq)~UZe7DTWmJ%n99ni^81HEr;);X- zwvi`I?x@%+f{9%uk)H2#7S?KKkJitaCVb_sEILQ5nVZcjSUrDrx+H12`PkcUfA}HH zYA#Y^Z;sj_3PRyqscHXn<7^{m zSAQqmp7OX-FIgML+6I!D+iF=@9hpujQfCe8y6^`0O&Zv!S*t$8bxm03Ulv@0?M%}WT8opCIR5}w z3|=O4kbbog8&eRFi2n|=A+KHsy8x*e)Ov%pOsM!&DAUbvb{V#DU8*q8!Vj`}>n1Y$ zRGYJBZbi${JCOHttF<~lVmM`85@$S*2;zW#6Z;7xxb1FC$j`X1gG zXT$&0Y;=4a^}9qMqqY7LhGKS#O^bq3omI$Aa*3_fX*?x)33dH%gx95AVz=u}hjyyM zQS4Ik?(iki*LL(caE-IL-Q7J^yl zSzJjj$Mr`wT?J_fT=@koqXw3#2a6!TgRvr6uc)h$7pdalc0reMjmtnw!t;1=9=J2$ z(yYTDg2&wU!!VBnX?TSs_$ZG#;xhfQ%EU zO+FX_#{)tB+sKj0(W8%QD`sb?o7%$2#oI@;xf-@DAuH}=L4p*y+~PFISqx@fJ^=oh zgwES^)MvwCzYMsImX%spDlL42?l&l=L3-2+F)qbrk}w9s4$5<=Du)S`4nq$>>B)f| z+3>mMq|zxK)|#9xw;`C5yCteoYDe-~Vhy!quINh*!1+vhIDI zN|#hUT9zIrb6MyS=6r=C?N|}=|hJT3CbIBCFRyd zgpu;QjVYme<_m;~x&_VHGI{duQ5D;$<|ND?$9r(z1HWSgoj({`(^=SW2mJzf&{}l> zQTe0g1cN{#w!+{BvtTFuCG^6{!}>p{QUa}~Mh~T_t5S^GT=vY$!)D&|N?jDMu3^#2K| zp$PQKnZH|5mLJ11hWAxH4Y}o!um=s-=eI(1hwHj(UF2Bf0DB~HiN#PX2dccrPeT@u zO%9=^=|LT?t~s={NwxTa9QueP2$9L4)N8na`Ng8z zHSCjC1(~Rz4HuB$G=s_F3M4Qu7;uBz8W-7pJ<+Y1rQXI__O(8r;R;t_5>y0!pNcEe zbL{A@c!(gj^8flSabo%X$rY5*G{FcY6FJ>4T1J(q=VRH9xWrCe2Y|sR)SrYx zu1Kftil7Z$)55l%6cJk*Xq__JaZNbpxYjd!D)nswrJ_xd&E>h3mg)W(Fd6*mTW$dz zpS|z|$YdwXA*?`bJRN#lmjd;`RR9}b=P1xG@EH3H^;|Mgjt!(8qU5EB=|xQr-h6!) z4;%uxW=aNxm-QMDIoJpQ5Zw?u!(aIzK4(t+Xky;H#8KiL2_&+3B)H6I2ghRR@RV)x zt5{FzgnZ;FK!g6b+@}aGPeAK(LEA?nQHoTupQKKp0Cfx{kkB9U1RPxSa3b+b;Xb2) zlOU#!fr$}QrX>6ODa|OkLkN9kDfF{z_3ruBDFNzCG};y8N&%nGW{=LIazQziB6W2v zJC=^Sh1^z6dTFXzRv13EH|h#CRYt%f<(|i*6McM<;GbUu{qm5@JV8!`xO#Mb(k1SE zpfxhiVbOGi4q^II0WYj^q!K6#YHQFwrqkHJ0IvGKG#vy4AmI2|AQn(!hl0+I{pFfX ze@@JwH*cTX;WNtKb*DAz-FIFSi=pR8((D#PFv;-Qn|-sHd-YebpE8KRB(|X4)K}aW zaaNlrA|-qRp{irw%$e8V0X$DV_89Vv{v?c~qmylqy*xG4`^ZmGc7`m`m>qLv>Z6UC z41|wAv*spP_dHReLYd~_d7eZ<+}_Y|+c@g<7?nt@HBZ)~-@VtK9P=Iw!)~KqUW?_l z06FdZ$gzMyE5TO{gSWaA`Z5Qn9MjCiVmrYM0#5)but^-m?;wh%pM)5}?yRk_$Dknn z9V*tc`jkOTka@7U!9pS7BU#jyPPCb%LK0ZpT|}u>Jt~$|xHwAO?i-gBcjNg5H#wW;{~T9>RcGMIU^smC2YxD&g0DHlcZZ4^06L?#QTpW zlf~P0aMK@)`{!oIrdQvJi>6O2#Cpt22 zM}pIg(i13Orl~8(lSql9x!e(oP>MvlE~Q_HPE6Vjn^0yyJ2L0&D>9SG2XBkmQCex( znd%*zbsu0aD%(r5LB{dC7`zi)2f35Uz_(lQGzYM=_=0QslglA$xcHj6sR=gN3rJr6 z(22Pu(5H)j;hTWvF$qBqE`0|63Oof-Z|yZe73g3jm1fftQll#<7R=Q!$6LH)vd=g_ zpc>s}A3f6e?6WD@p|>2OR*_LJaa$0>+vs)|LJ)E7H*Dbm(Lt@2&>kFY2>2qV1X7*V zw=@o~*csC*TuMvYX_Bk0)X$A}wZoB=>zz8C#6~znW-~gZG!`8akJf0FyG&}QO`uSH z0>Q1x`K3$ql-n@c<^n|z+GJ|n zHlo?HZUT-}y0biqm^cb)1B#;(iS&Vmh^6+Yq^1U|)nN0r*}NK)!=m#Uywk9~@o&Y^Yq|K)9|1|yqG@Ekg_sf!F8 z8D=4L&m7hfy_h|xzrT}nl}5-;A)O^`Qk~_PGdz{}@Tyg_2L=E@B}FD3-)RGh*=AfU zIEA&juM2I3p!z9T$s{CTP&-*&*1D#>eNF4S_V!qFGqM@Nys#*u2q)7cVgg74<(4LH z@uZugJjmVWj+~n5AARyDnk6qQ6ftXd-tU&iS+io0@o_}C6_lGq`GO5?AUhJ`Ck+j^ zE2`0U`eiLIP7-Yr)|UrQ>4i6-u-)x;_j?j_fqhOU=Ul9W}!`n9KOV5(qGqgo27~ zG>&~;B`hcvw{~a7{&he3`V&t)fUZCJ$Rlr4=hr>3X3YcZl52=@vDmz5(jbo{qVuTL z*#WmpjF1EH)qxcCbTr$Y-~%%sDi&{=O?|Qw*95bco6(==Q7;MZ@oqPci~mK(lrgis zCIIOq)R%==iHDlYQIa|1_3P^11L^uUaR3IXh!T>)FkAUg>_1I>d{JH}w_HO)}7 zlGs52N22C=&{_eHvI1o_=xij@nB#zu|E-ehDUBG;KVsbuX9g9Tpu%hXpT>>{gb%nq z1ih`$UwQq;)9AUESuf}_*XS3FQ!N}2{`>>tkqvk3kNbrIrfH=~Psk91VTQ=Z$neUt zTA!hfwOhZ2{jeTWK7kg&47}&T58(n<%Z2l)9_;K~i;B%AZ6|VsY+fVrRS#bYn>&hR zt1lF9qxLovN#&xHZG^X=%NZKONkSDw%i7}I25FfmN+zqTCER=yc2Qoonfk3(*Sznk zNUE`$=3p2oc(|W>bUgM^h7U)#w5_=(Zg=@p4K}kLW)im>by6qo~)V%Cwa;70k**jV#}kyLNR=)Qs=0em<(QasPmo*?f17RSI3P zY>{!aj4eo&x{0fk3_|9zry>rLN!@Qo!fU-6^FoiJnMn|fH+PK6QOM^B-B|I zG8RGVv(zt6zO%v7zvhyb~9pOA1#Yh=1#^#s9U9On7uh%&`T;vcjQi7*U zG=&>oX#vUEu(mz$DZj;T7rEn0cB@Jf4?isPG2^~8o6r4oEY`5ks^lrvIBw%X&)nNk z8`KB2t9YLnuHhU36)+oyT@4eHaMltNre<4#G{EnJ@!Jq!c`@j?h``}*1%FTt@G^Ex zYB4{|kcT??i!pAUnZb?5&ko_EU`_EU;3|8UyVie$M)H!f$p%MidwfmSnp+;1iCB_0=Y-%JIP+3q-2FJLQgd9%hBxuj;R$FA^>$XJn3CE3^t4Xqggk}%nr;cnX0*-hP$M*F4trtaJ!E;GZ_OY$)0>W>3aw?_={ zAbOFCCmK7`e368>sZ_c_UWrhiDKfrD7oUoD=qg%Wwz$L6ZZLT4l)t&xF)3AqrGkg> zWL1@PhVFx!DQ$OR zA0HxlpgdhuQ6o79$8aW&bfuF2}?L)H*$M_Qj#pb z_0M>U>Z`A!`^xrolh7=lz>$lkx6C`7!N^=*L#Q*bQ<*fyI0C`n(mr=A@(&o* z5)McI5i$7!5xw`t+&ABhoK!Leio?5aVJczrnjn!(`V)T0^K&5<|1Ei%%mThRxP2Dq zDW}qPbuJQ29`G0>WNANpu$uVCSaZlVhm_=yw+&RqGY6n!lpcgjD{nPU^2%%06kbxY z!83uMKwm`mhXjhe$u`Xqu-lsDUFZfD!{U+YbOOY^$*`6El(`0HTm#VA8v?cvm(FAC z>`c}fx7pM=v*x}Jo#RJtd5ii4cb5`V8a70t%7{cD21x~U_}f!OB|RAy65jYz81jQ4&Wj5jzO+NNYn zD!u5s#?D-B_&LS2xOGw*&7|HfM-1MMalt&dz=Lg)ifMMRgw6DPmyr-d%IP|tb~+1; z^XeFH!BSi1!bDJ#w(CE&&-}7&8of37(!BejKPv z&r7$0yH`^M@QKb0^HOgms9O>*PZ9@UCVvnbeDtAUT!p@dT%N@(aTrK%!7IBGk^n$i z5dSUIKRM5G2#$~eNnu7+gZ!u;!pof;x1eaM{`BnGU>t3uPOV;jeATM>>eca8u(lao@PQvdP%JMlTpG`yT=#{q?r;}X&Z@joYu-^Cd=?;ihv}z1~pl(@qCe6b1d4MMYwW8pRz&Z`kwzQfA64p>CIG!OGsy4SC zj)OikJfH-=qCX5eGE6AQy~Oz&Qroni$A7qXH0e~zjhk=O>OY3u(cBKdAao$3(67Qf zd!t~$NLLU=-gsD9mg%MjCc=!y?MrhE9_w@{WZU@g!#34n*3JEM90I%1=d_841Y#)S z&gxU0ErQceF(;|C-l!)m``0!S-41mWqm#z?NagSX8HFqnq40Z}<9-4_D>vFiP%*6E zn@*6e)w^WqQHVK_M4vS%0h1vR^JHT?zUaV08 z$*iB^r8QA>9k!ah`{xP^Nhr>ij0^HZ*Sa#EG$W=gy(mo0^sY z5K^Y(>(;$QHeur45Br=Y2Cqp@K%IFGx>6a6fG-KP1}sEf6F>`@RI^MYB|A{Ydqq_C zF7c04li|%bRS89V)aJc5Cox5-m!fEYyyfn@TUu6Dnis$=+Hy-WSD`CC6yRO5%Ql zP=ee=H1jL&aUO#wA)ucG5$8Y9upL@9^0VY@2V24V=m5UKG9iLk&2iY`)eD`Se{HB7 z*S<9IcG;%B{Tg?2Y$-#XL^qMc!r>kc#UJJD+`aLI7r32r@l>hDZl9#kw%8ry3Zo`4 zyRBO%xZ{@g9m}74PNHS_IVx?B$!4TJv`(^zt@e0!hxQQlOr+eA63B(b!F>K^>h2>d zF%szw3uTO0CAR5zw7JQ5YtH zO$XbJPk@eH@hR62yM|8IUQ5?c51(KS90jO=!B%$PTfM{OZ?C zH7}+Y&SG=IunmpP-~~nG`Kzy{)KL9%NRA!>oI|RO8Wbcm1K27fkjsO^493uhwlM(z zSBc41HD0`QYWFRnH#FMkVg4JQ`M7yE6NIC+httn1QDNm%8I$_M%z>XNlxeVrZ zB_k_0JJ)IWQkyC;QfW{`4J}$DV|jvk{_ilv$bd_{*mVD<+VT&IZJQfvYLliy2yVo=MFnW;c7 z@Hq6N$RU$jfqqgvTXfT~Z9@}`j{;B7CJ^)`#1U8{Bmv&P80M9T%VBQ1&i$fy2f_^u zfrAq^{3vD$b}|m9T#vy)`~h?VP6Do?`6rpAc}|IEm(U=3jYa4-gNrU9Xl@+s5V{xN zPu!ijE%Dy7=%;8ei{NRra;+9p(&Q%HY|foDOcwqJF6U&yJSe;x({5!sF!qKDCcEy9$yZsA2ZX6o^`%TL4%{2q6Eg_C(vq$Dq zPxfepBE6&H5JHWf!_f?zRCk9)v^(@(hFI#0T7n}y7K2tVDLMK4pNNbh18b7xQ0zh4Y7acko@%OBMx>xwdh7GEA9+zQQ^% z1Y@ayaljp;*)j#Jg)J8L!C>Gh?OD;*1+Ys@j6s56qTn>G5|*n0Ll4Hmb%jK8AN*DD zj9`S5Tc(~iI@~wO!b{N8r#z=0xFE*w5wK+jR-+MU=Qn%+r?3C)44@$@oXigmUWKFq^TDp5mZd4)7yKm8HGo1LF|v~`S8BZBt2c@|K3TCeH2h! zLw8JxAB^CF7A7FVMajX80=fW96O7)0&+xND`^cGC`+U}Og>6|WNiWzSuA zZQi{7YM(li*Nlp5;yTy;FYE%R&lP3wKb^|@wZOA+r@A*MY3 z{`*%$Y?;P`vqE2gjp@M$8;|F)7C;{u>8gM4upr1zZHH3#kLiXqCPzIBjK8jDcIhi+ z!egwuu|#3A)!Mv(Tj0tu(;!>g=lf|o98Pb#`rti;`qTkR{nEfdbn4W-0|N)9R`1)i ziI9KuP5Rq!A47NlbTf69x@QxiqRyfMS-3l2eWHiBA%E9htd?Z5<@D*6)g$rk&QB?R zdey3QGjtfeZ=7=M!*f_8 zy5D>=_epk9B<KfzDC+oUH{QiYq@@ym;|X7QqdlAqlljpVRf_ z=3!0Xo6-IlNFw~Z7{=g>#OHwj!8o%t1gqG5UL?iklz`pE5D5LRSMB6I=MO_S(w9i` zFMmTcGRBfqNPkgAbE!kn<^B9S#fcLhIm(!Q#m=3pHL6%nlM*whr+4jIgTkk%*^lnp zb)`a)%u3RvXhHhsn|JJh2H-SQ12;FjBb%BuZu9u@iFU~0TswuqN|AgH#1hlu6Vndk zgju4fXxg;I#Q2O@Y({)y(ruhEz!xRyr*MsP0dfT=057Qk)ubCE=?=+W-`U*4x|qr z+`j!M)Vlk2?V2Z4xQ$uADlDQN3%k6MgzV4To8%>R$Mpx5p_vV(iasxo2HnPFpDWS1 z*sU6oNCo;Owty{T8Wqj`1#Y=Ux6jp<3fUs-`wk2zTeJ7ypFNd(GM|4kcdDrwxstFr zy2Ef=Ys#b?ZUVWv2KE`RgPK7$=ms%Z3^izjVR)~q6s{R#z`#}DmerV_gf4B`uwrQe z%RmoA!$B6IM49Snlqwd#C*tRe-+x~`&soM25FArFdW63}>gDoKb)~5+N57UII+BS2kA<#oX&YbTr}uTnD#EmK9~qfU|`WIT?T_O7;crPInl z%T^FXi6B0vzR7Rz+Sb#vtt;C@?5)1lk!^Iu1E_B*j0rmV{@nxq{$sBk)Ri|B3s=sq zPHZLqLcR8%|NMt~?R&W4ob1dCk3r2`g+@XLwJU6;tEH>xu}pZi0SpQK@i%z&yem!m ztyhE>UzsW);nwZduP7?Fx&7+4wyWE-ZLRY^+O_!)%M-KS`2K-SyKIs_Qg3b`In_1o z#5>dnfBfSgsSmz@8|Vi%ypJa#jvEbM2|>>*>8jdBpd8J|?lk+dKM$y2y?pPo2~sc* zwhjqGi!;>D;(x^ddxf%|OO(E+zDsZG+1k}b>|sDw+nI2gZdRmnscc+|lzh2N4&&CL zoYCHs>LMN~7O$5MREM_{RqB;$wMt3%afLy@g381BG{_qfIUROd&vR%&;2v4QWp(jkd7LwuReQH2AZ#*31(3FtbnGr~maV@r1H{|1wvUNo}<#m3HD( zb$loB74_D4-+f2D2YX4`_>Qlw4Q7MhM%qD}4@zDT&_|Fu-0WlpRXiwja?s#lfZ-pl z-62yksM}8vH&hoO-`|k`%P$-L_P2&FZ>H9ksU_(8Pi?M1S1Rv(u8a)Sr=*?Qe-08# zH$xHnbSjs>@y7hRxF;}rnYPm3pIZlYFVmquVePbwK*R1#Je_ zd6L>!{8wd&h#bi4JVV|C?^_Q2>&-*{KlE`T%oc(+;fAZS1%js;RCm+=G7kC$;g*arr#;uYBm2 zdUgYGV?*^be5HT?JHFDtlP%N?fYqfKl2YTEIm%mS;3y)oxN_k!Dp_$QN(N(WR zo2iYF7r}AeGa7FAn$`o3I}du82W3S)w9A8*7!VpMhW}`9wEn&^XIepMY6oXcu177@ z8FVjdt&Vu*6?7kLZCq6%o+wv;cK$rd54&&yQ7@e@4ZA>EVCw-)`Y&$ZzWw}XkPSYc zMg%O+c>^9p{Q=m>@i6D*UNSnQTMd9M4^E=R6YX#jQVItD;rC(1?*coIHC&6$t|_M) zIHkUPJ2w{aI6b`+^BG=K4$uY+<2W*v`f@RIA>vK1l3A3esLsx-+uPCS>bN8w_wMkS z5>d@LL0po+i+^XW376Lkmivv=^Qcs)PVY>&e=i@dkBGbxIq|jJ$ZBF3dHOS!+FcL8qH2gX@;n<7!vgRYoz z;dzK3hQkTqo?$O&i59xEe3+vaQnElpTvqi6CJ)gO(-1MRhyf{R&cj_%wTHB38B`#| z>#OFQ#w;`G)-HD}Ug|q{F8BN2BkHxUGwYhxK)I(+Ac?pGQink2iaQGeG?OnWTK!I& zPLt|XR_8aRn*RN>Oe(&6>V#`5>EimkHXkqT*;6{@m7@jKN;@Q*eg^7fQ$u2SyWLt) z4(~CvKG4Z8CzIyMq06uH1l_&~VIx#^4rTK#KNy75@ULk~6BO>rYSUI|GU0=COZ zjrFx!mn^xDB&x0GYPjG->(;HK-imZ>*wC6%QQsjBzT@kgLf2sT)~kRo&I!O(ki#5w zbwLeH!JA3&P%dz<@QO0p;R0g-28sq0@GCX10g{c-EfUAtq#kfVdKU!23i9BD7vyls z>OseBK^g!zsmHxChJ+-yC_0MW)1L4f-&;|)v}H`W(} zq&dY9sFGfnQe5Z_6nw7<6>Nd*`7lX3Z%WVA7)`@E@XMWyn<>xX8|qE0kLD^Jf2Oe#6@6TAHBlpn?p{)fy>ssDJ4CPU$@>k)u1x$5;_K=tv<5pfaU`R@$lX#t4>zo% zr$A5O7IMOnUcfssY8~M}@FF1lM|%-iPJZA=l}=H@FvTYP&}R$GbKn|q8$ZqD9%>oI1@ z7Eom+)Yp=zB$`rFziv)65uY!k@+)9{cTQxrs?fPb$$T)kvLeZOs&Wt@a6mv^R_|Z& zudkgg-b7+dzVz>Zm$ZA)Z3{vc^LQgmJI<%otf^Ln2qGJZ52`KfbNP#Axi>DbdwS@ZPXFse04b50OwBqjG_SFg!|16qn#P| zsN|=A&W`t{JQEsHg+dBsM=)#=_-N}ucS^w@@dFNtyRq6Ke^BeHoeJLr7cmqYVY{Hd z>=Dob0zR50;I%Ydd>x>y=ZFvx5PL8G%V9yTuClG%Y|OJ`^IZ9^=PG4QfxjT-s&p$_ z^KpJc5`c~EUn}zwSwMzbl<}C<0ENJ!f;><*R9&R7R9)WINNg@usZTyZmQOyRKKi8L z6Qa4T!*RhB=#M`Qv0FzS&7O!S2Qb~(Rl|2M z7wI}e0Xha;1R%=LYxOE>DD0($i$*m-$AFyC-;4bTP`=>T4celMv9a(S`S8XalD5=y z;g(43ru@=7;+8~7<5{oCi4v;xZS%aHi)`xnz>*YWDl0PFVd1AZ()*lzv z-^5PNI@B;aVxHyJIHwy7{z(ms_ON>Ls=vP?7||vaY6XRRc!I|arH8!Xzf0ygj!irLU;aJ_oS@92ehM2X9lbYoTO-lSb zr?g+0vgwr6)2^U*L~{)XJ1=h zw&asL(VL+bQ^4Myj!(7)tu-N@M;&t%_;fu^L$|dkAhgU2*py~`3^_-<9z5^&u%9XuE~cu;IKSv-mt17drrNq}#v@}_?q zM>j^v3QqE*UdGR$#2o4R>WE}+)>1GbTZcA)AFh4vwOgVYd5>v-k6xvzReyjlpoz>g ziMHa^zGu6;>8>vD=c+;HlKt?ue66;fnR7Fa$NT@;6VIp?F(Xamob_KlE+pgvL1AVX z|~Iv1Im*FTX_oufD>+{WA4cL8(tBkBC{OigNpC zS&>357~W*?OH+XTvxiw$S*^}jp_0VJ_^{XU4lIXnt`zE}Hm-hAR7 z4C=zC-S;#!G_23>01!-=0{sVyAOdtk`6vD`7nA|PBA`;c* z{JY>q^+M`IOhgDjO9H)dCm4N%9>99M6TyfyWPqhM+3p7)M7RF&kGPpeefTeEe-K?C zhFuoF8Q3#@I$F8x)Y7HdUk(JOA72(+O1&P5P!W9Jui+8w@9)p9K@4U!PprmCM;9(l)}xU&Su zFrrJM{{<+LhS*VuqCI_1QlY__k?6;^1)Y5AnZ1npZAH3!dxC>ICDYQA4 zSKDqjo4erI4|Ao+Y$<+x%0F#dU#aZqKmPId+dm*Tj9r-kDB#*3KZLt=TyeMK+d;?U zyBL`VmS10VWj+q>n$)(&p{EybG;cUah(inrf}dg1cK=6N=0R$6b~7@6f{Y)1M1S%L z{m~jy3%&3S8pSu?I_tpL7+N-R^Q>9eHfxp-cKMEs_-4Tp^6M|01fStuRQ9!@))lR- zh!lxlPNJCrBunF`3LNij@p`=j1R9MGyocVq0lp%S0S`QwSGW58v?pSXTfGfFd8x|j z^h7+s54;Hj(64+q_#MSe##jj%*YW;~Ccs9LUKb{NoMYQ7IYCS+$i!xl1?9{{YQi!3 zC#)9)z+7HcfHdSnKbTuelDJF;4i=)2e+EVkqz8Tr1dPBl@GGG{2fw9YB!ZRUJL$t& zXAo&WWFncTMN*~7FZW4rqJ&=MZ(eu=9>2xeGw2@Xf&ZL=AZIT6PUN(QioNA!#VUK3 zN+rrj@X}$EO5uL;I6@)izXkD-%%PU}K)97lLpzPx-Vx<`r9#fMi5i1X10#+)c*`cW zsf1>~TN~i;4C>p^{HQN7vU@jQko|cJN=*H@rRC$PQwy|Nfy)?~C=fq0^Tuh4%-jR} z(2{JC+Yp&36u&TcSF^Ng?jf)=cR_RPZbP!+q1{Yp8vZ*m1$l`28rp9Xem0 zcmh57fok*P1y#wjzxJ)Oxqwr&(Et$)9vS23gorcNeUq)N<&IxIOsJS895Ej6< zuarrmy9kpNoXjqgMZ?iaVb(mg#IObp^2}99%q#Io!@__Ny^MnXXJoHaR1~S7nvo05; z^87Mrla#=GZfAB5a$UHPL=&jvU#>i~qs=gIVyHQ7a=XP>Rgy|*CbBThhnvmAJWq48z%MtA~vnaJDt#CXuekoJSgLZoCuOc}gaNbl*$ zT8)Ae**WrfEZPudg&X9et*Fb9v6f;v&o2%~%BX!+Fhg1yiQrc}AvvrL4g-2TR^dWi zyQP$({#EtWy2D$$tkVz8)7evnQv_&Im#S5VU$pFS$@jM6uS;7LPN!JvbE1;_s_-k1 zTQyvbo}7WqHG2y3?`;z#kZFoQWOOB`FdBnl6gY@XJ^{jGv*l7ZAG7lSLZT28=U7PJ z4iRFQ1S2X>(f)LjvsR_ph^8r1S`@tS54kleFqcq^)|xw4IVB$FNb$_FGHz0)_s-H+ z>KFQ6G7on2zfRYr-?u1~ zCb+{8?C(1Xe5eAwn%e_(QQFxwb0_1P|B|2(rp(a*{sMzj;O`_)1f>>WQ_x-T0&p(e zIou~Ik|kpFq&Pdkl9>2%ETk(x3BedoYqZSYWGgon?-I< zz*EUzi|m_=!3F!ZxzXEpGw#&XIvg1db@GdqNA`9ZhK`Ks3LveT;nu?qON*K{iO((9 z1eDdAHQ9fgt4&o+F@03yUfH^I>->7-q@mSIUuLOADLub0*}lS+OrWLd5u0@gVg{f= z0`%Pp=--w?R1t?<>1BLOj(LfZiIO8SIj-`^$>EHQD^0GMIrs!}Y(tQwhft9;9uLB1 z+jvx=SdF^1X&s7Qc&o@%5-B$GsNY}LJA6!iTYq0|>Fnu_Sv|!Q3+AJL3_e&{@912P zZ(VGPS3J=DJ`CGA`NfLc_x0#!9vU@NRKYl=o_R&f6JJ|*SS5z7{wxkRO^Js5_1#;R zKRLrX{Me?U%D8`fqa5T^uS${-hWbCPCy0!igV_U!dOuGxYS;D zRM3e;?||b8A8=CUh9Uk&C6ZVySQDHmG>64X!LSYWOXXF{a=70=%iZBh9rdT*tXco7 zvU^v|YWyvRTz(oU@q53nSvvP2EU50Bo1jj7zUJ_rF6+$O=M~8+xM?;hsdkS`5{f6k z=-Mt@J1_fXsJWR=*)y%zB-bYT`x9%En>*rm6kn9CBLNh-KZ1!SzKa$zn@MsmqD`5* z$zRY#bqcl%ZV-Gm0$?ZD=Sv_O<|3hV4j#g^Kz%eCAdm|p zyp}%*xf#NX1XTwg;ER#tz(fJKqyq9hmK{|kBf;7@M_r|p70)uEc11OtSA=CH{;0rK zglZ z_0CoI?wM@sIk=!E!*(jfR%A?8h!*4D>Q{@y!sc7>s$b)u$5G+)sQ9C^AAfxBC!pI? z48P>$fYgaEYCJ;Xyc8X;MRMoyFXoXdiykU-^tkldCkO_m_n@%nT|(?6JNIP5?|j}J$QSYVsecjd{-8#5uN4=$b!{*`tVV6l$yn^TOrz7Dd$nm=(7O)v(+a8Ad>IQ$boROBF z+O`~%7?71RVYUGci(S&-Nv|EdgpOh3>qz{Uzob5+{!#V8n!9(mSiA2T4QnO|n-Qv; zTZ(5k?@}0V@Ju|A@^3c477C5&Q(Hoobg%KH4kXvF2N5Uok^+5psvyOj!vGrXvYR9_ zLtm-5z-rv(|(vd(zoYfjsN(LRMMVyg<>+v?%CWzb0DAN>W zr6m<`UpL3ryUr1Jt?hA*ZE?N(RU8#jhdx_%>c+{onYYgO>Z`a>HEQbQ4OM+$-{BQ+ zo%u|Qs?rsEaNfOB{Z0GY@Cvl6drzak?aaI+Odlt`kAxqI4JP`_#o((Zpeyf$L$KeB z=;s|m(xXGJf>>O*nOqWpq_%WYY5|I7$JDU^osMzJ3i~CM^&tGJBo38WO2eW6rCT!9 z>xb2EYi5M%$IyJ*hO;sBZVc~53-Mm+y~M|Bf3~N=*>iBApukrxGm9+9F|!oU^Izi- z`hIrRwoz+FoLI8iJT%Ok2k#-m7bBaA6! zMC#%(pb$a?jZt4go!K%W6oez?OWdF!$O*xz7%ycLUssuoh8kk5F{!eIF*ib1QeBQI zZly3z3(#$_+moZ?aA>;qAA&B*7Xq z=Q($G!qTAXs#F)3Yk?oh4v$BGABq0f!#xt5KS9dntx@j$9Qc&KN}vOy5s5(pHGWZr zOCe(b(sc>wc`7@mOhm$yqHKNVM48v?M?E|?x5kjgTn#@e)3DLa$xoAbF!9-WbmYF| z`SZ#9sDGutSbf*_4p0BBBl1{5rb25Fql{Ic7tX`a`Zq;{-RopeZ_WNTlcD^7x(yzw z+u@P3L60%M5V1k^&~tXhyC-KLY$&skH=ld=pdO*#!yeBd!Qlds3IX9FKWh-f&*v_Y z1CSs#2kvj4IuVZ6CpoqriEEiodNw8Nm;N3&_BJTL#}|icP#1; ziP`^ScyiGvigjxeJ65na-iW__VYrX_d-CJe$G5fHhHsy%2-Sigh?n12MrbXhvcdQ4Elq;l=kwTzb!r4{wSTvXku^dvDM6S3NP0^%rjYmrx zT<=$*uT%I2v}*iGy;t?c>N~b~dirjiqvENH)7(bXJ&TJwZfaEwTo10$&g=>wI<|6U za^=bgVHFPXQHd{+!}pNAf%*5y0o?(5A;(wD8x7GQNqLn>Zo?!c>9>oQL41xNRDZzb zpykvMngk1B>EW`3+LF||%%y-BsATa=4!BzZx)7(X7r5X&|t;jVW;=N z4CBK~6#|ba7*;-v-fkbP*P0sc4_DA5=1vXucJk9TCwEOX%sf1&x-bdhTq2%!(9$(z`a=K(KDX_?KkSJ;5@L*~OtIMq0cf?O_PK~Nb?>O#8w2)_}d(q!s;~ zI{W2@6Fb^WgL@W)_*Ero4+;(oq|;;Ax6{F|+~(gu`BrnpwxJrohlJ?`kzn%rsdW#2 z)*o+fj`y#l9SMmR#+fiaJK@(_uz6a*RFE3f&;u9e}_aV~~)l8e4{>T!pP z0JsW{X6MLa5xOSA;wtzsOu;yYdSx*sF0ZJ76eS9lxubfuAvi;hUP%wY;f*DVkoZ+>jfY+Q`CgpJ{* zX9u!JGFzMRI<%?jn(A=V;aTzh`{N@=AP#2klK5ByjBX@o-pfx%c$P>YIacm)x$NaQ z@{EQ7pW~)HXt)RvCxs`H4ynxieVzU7io_uRN*qeAj0QukG})C+Amk1 z+S_LAKQihojg`jKs3oP9+UxOen~(Ed8_F$;eTm0mbAe$o&zo}CMXuy?Wvr?5rrBuy zr0z^bVqcA09oSVZh@0lm2j7yR1@UkC^0|^LXh9+eh8WBp6KViaWO6144S5upO?O^G<;V85n`Ryw)7X-%kgNzziyDPxRbu?B*uHWxeU+~*nXF)? z<#@Z@DUKB>l*ozC!FH`%(HuO8-b#)0^3NgX)xqScNuS~bq`U08$q{odbzVQoVW#&* zuLa6Ua-M8WJFX($j$2PcYVDeSr$)G4pQ;o{2Dh@@PO7B74|imPJ(M6!i%FGf#O zA4b1kaddB+WyWozyy_~I)Q-A)g?QMp!yyU}HylHz>rdznx{zV2 zoc3Vki#UpxGZI*40+?YEPlW$0IiW&uEdZXEhiGapN@hnS(MYH|#41(us^p?}t`>C( zQ`lGPjqo9(dR0{7arN@LHPAr+1L`Cq1#87UHRteHX|TU>~ybrj;cRrd>=+v!KM&)V$5NRd*BUWrC)T#4JB?Q^*L zf`b~LrRk*4Z{dkj_V*^ zkY6;641EfK2$#n{phU0NSF!k8SWfTIZGao#H*8>`4Vx_8Znx}lVr@e zyx1*?mIcsVa;Gtnw3!<@w24k+etpUkm{DOH)Kwf)0wPIJ;83{l$e7(chRBH>i-jwREEbOvm z|4j8}Pt?TFAw1j}y0&WW2)M-p>xww?2V9O)PvnUJg%FwRR3tQ*h zj#Whq%_>}r9wSfc#fvGZ&wRZ6$i8;S@sCcO z1nu4JerIVYf`4DR%_(*4G%rUi{6q4F?Cq!?Kjci1U5UB8$VuoUB=HPEtX=tD8TMiv z=kW-RgmA!}OcV|}k>KZn^$1r&kOQxB;A^=bQi^jFkx(=pV_OqKxoiz;)5OaPvA)PA zq=gPH^&b8!!LcWpH2`lvY#aTLfE}F(+g%Z7Rpm3UziHb_eG>m>(~0Zb^!+Dh%XPeT zQ8k4IrV5*7_(kj9u#mpVa%O$({Q21W*pYZxTvDRo+Tur!93ecFV&a$SO#G7TJ|vME zQ@bYz3W1FUiD5F0q&k^%M~KXk^NP`@|D<+BR;Y%CLcLVO#aijOv_7w?{FnR}l4pj`U55@AGIRQMwPrqVZKuCZrU zCZ`V3Qb>~gd&AKira9XW%(Y3I79U?Xax=S^#yvFSo;Cc`MsbS&G{{eodn$jH${Y=BvfhmME*9e z`E$p=Xabn;FkcZxn9F#>wSs;G{RxB`54+)NgADotq^?s=`(qAo*+jLyl*27hl+8s0 z5>bj@DCA&Ok*$=j&{OZ;_)t^hG5MBMG+>QYNoB0Ry@S7{2Jk=M`>4>8KC>S0IfM4! z`_D7cd+(*bj{P`cH9sa0r%Mj`&WZI4qpdowO5(&~b? zm_e_H0TVj0rve41ghEsB3oTxSnvLc+!XwzE^ffj48M!4sK@XEZ^AoHw;ootpaM)hL z<~hO5+YFl62^yNj;O3d=5R!oedkYDfoL7CB9?b*4AO}N!4P#os?;Mtg>8LxPnrLd3 ziMkDFhAN}RGG1kA$fa7aY{TdXO!`dV-Lt4?j3X3Wf8HcjqJ6b57#l;|!Q!T;mgVUcX>G0JQ@ zb-2svrL{&&dD!j`n^YC5XvkArdrqh4hlS-ce)!?}U%QP7YY!b&6W=uFFCAlSiWYJ- zWJhGrW%Pj)5r)Y}BYBewYVFc6PLP$KY%!z3m!jcSlEb0p@*K;NBnLqdg0h=eRu!dd z(}imls?a1fP$)>Lv7#8fQn8y$oe04uZ=++j$q;qUEK_M$c%({?MuD;)$68{;!@~ts z<{N#XOb=GcKsmElQyZx-;$LpOXIGoG|M0wqbcx2KLM`Rd(%JZF+s>dgxX-957W*YO zow*YE_Dcieh%_cmsil&#-{bH3D7-1YOr$O2*;wGitc%57iUCbL3}&aB!3~-qtC0FH zU2kF1IXUe?4D9%aRG!bcf?mlT+-w2OLAVGq%Mg*=p6!Q81>x!-Yq<;@B+sNU=9^T} z;=mo~Ioyu!!D)1%Ee@xpHb1d7e$$BJ++bfl`d#CUMMtrwdh+xL^`F=eS0A~$%{*}1 z+}iYHG^0Z_2mkK+*?jktHGV+A1-|iaY--wwht*XohazIug<69CHosH+%apB>$?ub$ zzfV9Of%y3qP?v3B`Zccf^e;Mxd5tfVPb6}XI~1u&lN0AkWD*W|$MDU(kDP0DgFGgq zPY|VwM?=+dPM1hg*kM8a<-CMifl?RV;00&_%)ohL=Z%^*dknhUc4#*2NJ3IT8Ak^E z*2H`HfN(Vp+MF(DqX_-HP-2=^DVkDHX7RO`QxAT(>EsPlExoso`grLAx3mQHMkPvE z2>s)Pz(Ydc<`BL;kn~(L*u62DaL+%mq=!^Dr@IX6UG9+8+uK5$=2#HEqS;lhEN!cV{DUAREyK!UIwW_aM`4-EYcc!=vO>T6M@SkY>6Q22 zkKKdzo_X({*cs}(_=W3EZf(~M-LgzxCWx~=3Nh;T$`$4)D1!UMnLF^o$chz_S`@B} zCX=v*{nrmQ!y^RH-v&4)oHNAm_jHIy34X^9QBn&?{v}%h=VXpKt`oswA;yB)+yzEH zUlS!j@k?XU^WY2u(1V%+5qBu}b|?bw5V!!&T3Oj*wA_;LX1@dbPZbyH*(mWmb?xu& zkKxMbxzNUqcca$_T!u{rmZ+F(_LLY7K`;|KXSV%Y2VFnH&}yl-y~m|TV?QBhK5{P>BBueU<2qMe+Is0 zKNEw;a_=T5-di4hi#>wF75U-pZL?w;wP@bf9_4(7XJ{hYZ!FSPEg;ZE^IiztJc|h z`=SJ=y0FFygN-C3_?Na_l~Vssy?!|PNzG#GjP+9_aa98GlE3^VDjrS!dqJ1C7wi|D zC(P7D2)05Eh=a~?oy`~_4wJymTXu4JV7z2Mofg9UcgpNS2?;3MUt|lA)}Xh zJsyEljqclb4i7xKZ{KNj% z)9b5iR56jq+ZygpM06caSFj@LNmyK++G!OP4t&c;5fuI?@k%`Y3eX_&v{@Ym;l2 z$U8mwbLw_nP1_S(i3}~x9%1U4PcS$JCHS^Um<0eE^)7lcKe5z|XPXOv|CcoIMeQxW z=-&WMa;iJe5@4uj$m3C8U5K?vA(?7GGXa)Z?fPT8rdIadw!A)F z=$9hYl~5MzOltgMpj+;i32Khs(soNA>fTt7KSYAedUwRPr|VUc>Iv9&omN+{+G#WQ zERa`f?0$#G*qpE~+05hy9%bqP#HP7GBIA;^`_Wbz_z$O@!@H?H7}as77b=rZE9n3;55zDmpi0uX0yJP8Ez;Kqre1Ng`= zBvNBFA$GS`Zk&&X#Ikgm0#k)n0WI+DK~yoDpC}X-Ax;8&`Vv?944!Li(1OaNHm&0p z&u{*KRN4JV`WpHd^`Ge1o9^D*3Tep|epyPCvZ0X8sS1bjuPwXXV()|Y11hOWAP*=H z^!6^-wA*}^4$~|0Ayu%vU;R31GXqd3v0*OoVeW^TxD2qgA@E^ZK?hEUZU*pU7|nK3 z`r)J`T}kc_a;X;`cLGTd=F~Z8IT1KFIgbTo1{5FAp#Y2kkfeYi()e<&&l3&7c(`y( zDP9J{qpJntVgRnbrUbO89G$sZV7JHlK5co?u2s=%uDSK=nWpJyNLEMc16FLvv>AjKz0O#}P-LzU<_#s;eDZ z#|A1F&2vz zE|hz^!K)LeHRZUNZQzIb+G1*;?0j~^{qm(~qN+IR`1X-MDvn_Q^ZqSK_BrB1)PEA6 zpZn)3Yx~)`nY7X;L-kROGLXQ}8+V6FgV*`}YZHmJ@wiMfI}Yp4kqf^Xj9haK+<6X< z*3IO<#%JY_S&x_dt`}XioaZWM6ruED%XQ92gZ5Cu-0(ecSFT7}u%VQktd6mVq_SNQ z%PP`pHQey4d>dWpU{h}$TNY0qZmnoKT8BSC?sri3ugHu)SbG}pJ@xLX3NCar{H`i4xs?8Z4`s{fl3P1~myqUpSgU!1$Tyf-rgm zy0UVeS!B)|=@}ScaN?w*Nv0ZJAq`6+^m2lker>u`Cd0Ly(XL_H17kpn{FhX|{2gFofs5iU`1X!t7{BCR>UIj5Rzh z=9?8Eeq|B$Ies+?n+_G{W30I-xVUg&I}~+Tk;o0AgEvr}Zdh=lbBApjy2CG5_1pLo zoe}^qUxsmZ{n*G-|O39D%iU-d( z;Ik=L)YDDRVaS4UWZdZ}U^YEwY6Lanie z6h8EP^5>l`j`R@j@->)zmKHC1q4nlLv?Ydj$A@^2ytojSk}k%(FFmQxA{W-&y=RJX z#?b{@M}}9&Mx8SYNAa_x%f+cfzkBR4QA3j})4A2r>bC?toH2Km8ZR!_qSf+fWqk%h zpRtHL0yEHHCI!Kvodh|M!1>9%rtzsAL>6*cEjS5WIDuB=O*+FHpuORY5Tr8Ni&XZ- zSLVLQb2gBm00lrg*+@nUtlWSy5%xiCcual`>Ml#Eig78&z>o1vJnD^Xc49P0$AYM+ zR1$IhJBn!z#Vs}Jq3&HKniZNA92pt84L;AJE+p{h>rd}!1Se-+Ri+6oZkDDi@ME^U zE|Gb6pyr0xdJH{(*mF>0RQ2zK#heIm9Uqx3g0NT>z5yOb|426T!GqX2f}uXe)ciG| zyGg9hbjx2!0fH_ec50jm0Li;lApR-)v*b*7QQq|h=VqKSpeVutwjk3^!Rzl=#)C9K ztd}Yj%TO1K64VH}SW#pZ#GG>Ke3)GzbJ+*|?#NZcGXJ)?TA!&bNBDILXT`jd@UMRL zAC?Fe$MD?P(=yIcbPJw~ZlS(TLL_(h_C{Op;Ux`eZXKE>W~WLb4*XQ*4GzhUX|+6I zyh2}U_5Nj#UzY^ab45C?H=`*z(Yibz#_BhB$48dEs{zWdVH|HMMuDIXO)(>uxeOx9LL#ua>sau4>y5 zy<%mpnG-q{D0MZgSivf0Pc}R&4~YC?TJ{^W$t`x4P>uTq8>W3(rU=?CzYe* z6xm4R#|c||_{$c&@L+Qwwq;uTwF!Uty3?zlew_BbBJP4o>pcUV?!{Z^g}jM0TMJl? zlGU^w@O6`|vm93i6~U{Wf>qO{?)7%!55z#G?gp76v43t~0N~XxqZg1Xk30aciLCx5 z9j(NDB19PH4q`k(`Q|_1E5Gyyc|071cBHvV*#a~qD^5!#uxjZU0g`6_`aG9|rzpy! z;xg{<(OKMv&OQRg>{eK3Pnx^`POZf6|3E!~zu0hcd!xPg;G*hGQ3j2LR8mhcu8874 zBa_zj?Xo4p=HZZi(wdqYd?-x=PNg~&s)i(HA=CvQWAa&~ujIsoG%!cj?V4BP;@{{eX{5`vIunPNMdRiaGER9Ia`he{(n>SXq_ z56^vY-+i%j=VJF6W=vAgzh=O@=sxZRwU}Q|ZNc|B-7PJe&@}4PNOt3iZBuLm2NonV zb!f060CS@83mv;9)|<^|@SDiI4l*@sNtZTWZW4zznVHjGjRw0SMWrRr1%ovfSI}*N zwkv|KI|ur)mFcf=Gw3=p`*#9@w|to!d@%|p3Ia?gGur6#=ocpPjR(ECp)3S_O%x-c z4hRSQNsU6iyFcFOOVI2^3U03n4auX}&$SkZ1m+^N@%~8fHP?WXb{rKN>^;SeYVoZc zyWMLxm%Oxp|96lyh!D&5K{#(tlt`Hv|6{6 z%ox^nO)EL`0IGM@YFfc5)HJ(b`+TW-N(8O0Yp)1NOQ)tPYNrU54YWQTbatz&?Kf;d z)Hk3K)|}Z7HularIpzp(%l%%A6Lc`I|CZ#-bzTQmNipS|YLeOeXi* z`o;c`tvwtS7(-#BHDGg0u}YJ!YC4dbxqKc^nbPg{c#XPBT3>$q7;O*` zoLwC35Sb4~upx0~Lk1ITWGy58hou?K|N>8k;5Tcr>R4Gl!!q|USpcQX3;8j(D zN&QpWf-Swn>ow{?~N;~Z@`42ANhjMSpQ?=b*T!0*2zhXEAI{R0*# zIbdP90A_a*z#TxHGo)u;WJrDq4Z$I>7QnR(qT1|KvDX()cv;qzLTo6FqZx`+MKKq7 zGJ((*m||vMRVAxi>*D%9J1^i&Ywe+K?tyDfPIk^zo_HU;bYD+V8zdcbvu_3a5${lClB5 zC2UVAt+tR8V0`iz?95e`#luF~Ql;4?379~4kR1O*;4hHg)m)~8ph&={D@@MZW;>#KSRq5g{RSngV`x_(0P0vS~vcrp3L6=>8Ppe z)KZzRnw#OFHkVE8js@{wS5{UVVxIOuC=_T9MD(?lou+HOqWYzpR(!kP|9*Y_ikg*a zu#F+ek)AA=0X{UD?2)VRlPKbUi6`Me5I^KJ9qE4}J8uxk9=JwKs)Y1Eku$>JPR^6u1uGeX~4_dRMJV+;BtUUpPM+_SwjHPs4=l z58~gfJhH#1V&+|=lhRW(sA{%gC4Mfwl~uwKAF5dt3UxxObp+s(*F^yS@MG{)0B=P6 zzx&p#AvQk&xL?wnCIfxsWTxAYdt|zf{*SzV{$#|BAsPkF0;eKJ@=Kxc?=N6NgiDBX zlt;&$&h{%}p~$2NyP{1lUW2+#lk5O4H?fLYF`h!gdjh>WIf@HnQ_-#X&UeCR&in(t zSQ#EG?lY;tHESbJ>YFfLd(Ta+rs2bjC3H1KG#<#gJkd@1eka8XI zX)k@1kC=f7aCV|c2<%}zI)=Xz%}g3@;afp8zO@xx1IDRfeqH{DgJAjUWqwZ}=4TBl zWpmI%H7{8RGmH4afYyS%VyE40xAC9ev|bVNy1ib>KGZQ*lsy0UzkgHVS3SC6;#C4w zTqUF^=fsH`+knNqkXno;_+TcaR%^GKS9?+>i%IYvU!;Zs*k414z47F>cJquomL!rz zDHiIF%S%h!@Nc4bi>7aBT-Uy^KT_$RI&O9YOLeBmm8|^J^*Kvr8bXi&H^#LNb1Z$;Uua0;17W4b3sv zE3~6%NL0#ADk>y;Q~|SYER-DgaLs(!1ul31@JwB39=>TM)n<=f$K9Y-aIj?>S)34h z?m6l|;cqvc+&NV@cxa{EljK#KP{^njD=Mq1n*xD}zQ5Y9w~PIYV~Y+S_QnC3ID)f& zQ>aL$coM$|L+(8J8Zq&)jsvd|AFB!`*^w=Wmqp!-n#@@vusYR zM-03M{AidSOPW_0i~z~QgB&uy4nz`O2C;a2L{4ZHq?N zpHm-S^%c@YQQ2348{*WL5sWvS-P5cax^r<{R8t7MW>9}uiJ+{QW5)mETvk^a*k%0f zx)LKl#JBKjCE!M-C5;LNS8ZH1ms?&=73irr`?C*V8}3l{p7J^2H{MuL6N}D^w6{lm zGvqL%DkrbKjJ%FdnrQ)AlD(Wi(Z82$4v}3gMMfroaOQm0E1PNQ0+2+ARp5KDnF4+q zd|TpJk<>jg_ZH?)QmcUKBwZ6_t&rP?(U4G)D0bpPV?{_3I6wi==52UTlvV#27< zR2p#!s1F|1+T%P|x!}l=VVg}xUi-LaJBO%g;4QQI_?t_i!2?4ZFPQip`4jGJL zA>v9L01f5O{j8j@8;8i~47xs;^s|;pOV%S;>lW9P0kqy15Ex26llX*gzyV#ex$q&i z1XTzB^rz)q{CaRwOtV72zW*krzsmP5z0a%D)VEaXQ|`soN0Be?`^Qqtj3W!0tJzJc z-5!+3;`sNT2ZVu5K9;3lRj!_CIR2a~lnxe`p!e`!&SWzFbQ<@D`DXK2X!LJk{Jzmt zVs$Jx+hD+m(cvwJvN-YiDnWmfJSv%|VP*6;=|sEa^T7cxu*@zubquRsIFmKs4YDGlfN;X{INa12njB=?zDw;(Q{HHZW-FF>&z4ob>pVAaT!B}SeYyy|~%2+ETwub|+EFRlcl zer;ROqT13OT1HRZs@pkj&zZk*{P9$^pVc9iYiFTBxnz<~2nnEG^w!a%=MS3%ZjF6M zL>f@3pL+kUwzjOc&_Fu((96rsEq!2)v0$}-|lAJ?W z8CZBJbK=r%S(!v>b)hZ882_+%kTlU zA|fxR{+@Oi=~@@h;n7>|)m!Lq@aCIuraq5;yXM&LPVkxMHl=xu<)~^>xwO1D``)IM zJ}K??aV328!xT~3Bz*e5h|++m6>~}l*A2g%>~f|3(eg@TEB)u}9_rLtSjgGmPiPST z@jl>}J_ZZs088IZY}2KDAj23JH3o_78Cfu0awf%Kroe8Ga|=+3bbEKgZ@CqKe5^>6E(_9(LA0m=Hx>s|H~zC^=^=EaV+VKMnr!b!|$$bZSDAM zXaKiU{3o4-{5g${4KGb;YMR33vfb=L>MZ_b^RdmfmF>5!wv;7cYkVOZQf3rAs@UhA z4Q9!a(4$Y?a^HiyuY_Mvph|9plD!iCMJ)D<@Uw!Rf&Gg{ z4?z(*#~Y+StBt{WlbsxmIgAa|k1q)f(He|m09Z7U95BQ=F&h^vI6lPVB!em^sZEX67$aVwIa3>CL2KcMb$4=w&_s6I>Rk2LWv81F0u$=qKSyox`*TH-J_da zt7qt2W?N3YEaUzBOmR`F6o!6$CoDmIOUX_Q@_avf@?`X$d#Hylth#4!Yvth4MZ!d; zBp^kd18j>*AH_e~cF;o8#`?ee>m#K3{| z&6gMm+&@AX5SZ3beF(BV1AK*G1)8qoMP&vo7rFz2%EAr)Mt_6uvRE`qM4SI8w<*X0wk-A#tc_q^SXe{t=# z(+0PfSMUQYfvDk6e>(by!D%2hYGDpb0(KZKp8&!9Pc<$PDLm+3NAS^Ruv`&t@puLB%7-RMz<{-+O`7Yo)$e11Q zk^$UtceY&?@&r=>)^ag#vJ3UGdAw>PR`AT?px9JOJ)~35RGG{=@vnFiJs1B4zxzFY z6-~kaiP+hL!>|CzBPw=S6dpe7_7+so(}yP3nk=fAvJw5dUDNIMb!wYwzwtY5Y_7%} z5EB%9F=67ti@AS*^o<;~D#% zhc*{P3b^99^G7ro>5m*e`pjbo4sZncZ>Cs8sW$qTXsL(3U2%N>R2>wQb%so_O@gNP z3em>1NqcSk8?Z0#}q(*>|vN)NqVZwnYq(f&?g_@f=84C5KZ9Zd}QFl91$U81r`nF zNlF4(^7{C^E)vL+z(Q&uKGkJ)0~jpAlG1@_8a4>G%dLQ?s*D?jFxu%|Nh;(qqCWrj z;+J7jYHC2)x7G#SFri5ZJ-P*HKXj|IU5tUkWC-8l2uBDp6nhSe$4+7-3t$dA>h#hb_Q+xksGf0MINRM=y% z`ORrlFfG&0u|$k?nO7I6E-VNYNO*K{VO>BURMQn1>r!_x?6y}_Dl9%VZQe{o8ZZhm zz)YWFFwY}L42u5;uXw-HR9Q4Wj;X;TKX0X!FEp|bL}Eeq+)|-zvKi8?$^@S;#w8qR8VCxV=sc1{eBPBatt)UsGL@r zQdIaqQGCK`MZJFI3k@3>-;>nt9s}QVCKId0@)R5SsV>YrpZV+x6Xr8;9h%tyN zO(c{JqcoZ1Nv@1dbRcjhaWQgFGO`((dYDPpp!TN8g8pb#fHkb%jAn`CkVG#iD(6Ma zJfttA0!8$2yQO1TdAqP`(h{+5k_0ePy;_+|yV!!B+=YJ`D$-Z>TYLi1igLeZ^R*i` zY{0Lt$UUgfBj2qd?cqa57DVFp9MooYs2Aav0#}E5r)q!Z)^O+VSrGQq3!4@GmKDV= z+ft8YW?NCnFRytb(6g9sME@29DsJ3e8gW93G_)wRkpA-`{K>-OiwVDx-JS$1G{D#p zf@jP1K$3jilzgOnnGI&VcQ7L3`2ujzKoCJn^Trky+k%rq%ABCxgfO{`^g|Lz%thQx zUV;k_XM4rLK(xxwo+Gz)p&=NwE3Lp2S>=413xi^QkKEb0$o=W3(N8}4^np(wcnb^A zL-=a6kvy~S2Tsy_?}-@N7Ci-BhIc!uZvq%!b#hm$amLNds*=*60|FSm#88c&wQLU; zTW_gkI~?wvF4xXMKY2WiUqZfar?b-O>~c7caVrJBQagV&-@*3_U0g;ElVDrVL7tz? zo(cgidL}HGumY^>hH;)^xCQ>W7|`S$wQ*=8Ox9yI27vSty9q#W=Fdy+$;EFY_#0Rd zu*9rOx#FBvhK-4r;RKoP(sS9iRAVR+FIp^9FGRBhWmPcpjU#l0q5U2SC5_37Kt?nw zb_(i>3Ra=gk%@(iQF=uId+R=NeMBEf2xswK{F*{ox2DG%`&m26cw8S^XooGNdUu)9 zS0t2_a477HqQA%RO(ZLd=22hLU!Ta%Hn*Nzkf|z3sZo{Ar!Y?focV3Sz-FJIYF4FJ z*W$J|u%i}-$78p8uXUNcReffsev;GLz=>4&J@_BK&{PMH&*@QcLsnIpp*^nP1Jt(; z8j>HfpQx?%R6`M^2zpo_WO55vVh;j2#NN2A7aQE)}ZoCm4oN;4364^0UHo6TwI;4*P3A2&wr&EvpxctN|?UjRf&8y8!E*`5y;aWePORpzPDx z9+4jmr)Q$ESs`742FiFDp$3`z4%73WD}Q=v?CQHOyj*A!1O-+O)rg-*p;u?ogQj$! zY~E)LY{8WBrwWDhCK;>?dF()kVSQ!8r@rmm@$)-&P+!wOUVVILtA6@zYh<28VXXzl zlTtxh9sap_PJHP=XzKc_Gqh!=Ul7=BpaNLj+Ft9mjwYUaPHc=Rwv`(vhuy|D?!?I{ zZ`;~upWV~n2Pj6+S4qe{ohoQy{EH~~V1!146V^|-3U72e0olHja)pXQ~>|%z(CtA5evVoX~Wrcr`MfaT=*d2ZHPhb+Kx z7Epc~o-=^`Gz~-$yRv#9`!k=C>_6*PDj0;xlKBFl3P>^d2P}|*cs%ydC^rqLlu2w* z(GWf6NBF-m5orRbK7<~WOpP*K?55Na1eqHJUQZR|!0X5Vpd==K5k)J)FQHq7F+daZgzozw1ssz<9(Vv=!f9j4)8!CI-74nOk}^@R#si<09w=dnlxe5DSc z0Q@oBYkvkXQv{ZB04NJxhu9y($`mkfkkv4!!FTiwF*bqmkFi5W4I?pNI)g62Vd#F? z^nr_u^Yi`em$CLTR-Y)duMa}?{lDkhFG>fh1I0eN#;y_b=b;Iblnf=w6H}MF4{%LF zqe?E)YoHhhFm4S_%V#Am^ zHaluiX=Pl8p#|y$dC@WR%$UacK;v5hZ!>aj2kS5a9?9gasbCGw{WAv-wS*}C6$90A zsssBZU{B*>axjJ-dMtBNX;XI&&2Di_R5Cj6zQZIdm zXgwNTASq+3$$Yx500jk_h0JXl+uKMGIj1Ft#hwsFkGzA#$6PV>NFh0<5w>iUiBQk) z=^qm!mC2Wq$JzWRrP+x>@yB1i|Ni?V=k?4*qd|AU>YH~o8$0**mL(ePXqe2+6;+V; zShu?A1@(vIggu`tK2@04Bk9!GC}~ zwSf#3Lt4;uS_ltPNPzn=Fr7gD2SZ#C?4jPTMesROXp(Wi0Sg*%ZBo)j@5q;kCZXBJ zsD>SjAT{{`%1Zy^Tk=2W3Sm!G;Z))}NsN=eDEs9t^!BqTyL|HK(Sj;ctTedE4Dx?dS6 z_B)_|-RI%vF7^5xv#lxRK5&a>Shrhv-j(iNnks12ih?GiqTVblkSH8holPUsdP~Ts zp<;nc(fbt4lEJ+K*J^;nkkJ^}EH=1}52!sD<;hs)CLC;pvk;4*IEXvA_68f|xGK2c zrP22U_a8(fBhSST=?P;*I z-!ZkJba*!E7|xzdUNP?pvDI5E?jhelmx5)7C2*y`hc!y4iy(dkAMb9;Tx;t4uM9rM zA?|?pN#j0ABWR%rRdZMY*85*&+cOR{$nt=*NL7R3PMd4e%EjcsE$#ywJ%ItSMx%5V z&{qsU%>0!S^Nga>2cX+x1kdY@CcW9dlrXP_Cr6#iGF|~GC~yZ0!8hFeWDuo(<2!9- z%wm&_ys`^Dx08Ho*ZG~~C52mM4@eyx##4T~dK`JPwpPJXS4^M&^YrPni0?zcuQ|1| zMcs9{S7$HI_7`xWY;Ht;&`!P(zr7F~oO`|{{lp{Rew)76><|Q`depiTJJ~M$;fhRS zmbwjmF~bn$YZEU$K^GR-*&L0f_{H# zFngMWHHnW#i+L#z3|e@rpos0$D=*n%ezTSLVUXI0TPk;n$p1V+Tt*zpHU)=3eh6jL zbnaWsMSF=GVFU97boAfr9$L39g#W9KrSGcTGGc_My?x_|5yazQt)JP}s_!~7Z&)fP z1^ZV=cvSp}K{6d&;pHxw*>Landwb-Sv(`;|vs+=-yUI8|Poas!&(Gs33ZVH!&X?;8 z%mo2*{o{2;qp{Wm*TR`r>r87ITv&(WSO(TLnfVk|5K(NRboku12LeG$VA!g zFu9@CT@g&WvteI|B(Eu-pND43!+Mg>v-&xPT%>as*GVk(s+Bux0`wyrau=;zw@Eo2 zJ+Yd%MYk@jci2Uhra((B&Bqe@xx)jzk+xy) zH^3O|tquNP%6f}8x)*6>l4S~4D&^3bBgIk2p&L`FpTsG-Sy3)N=wOsMWl1jHixr}H zI@51h0acVO;G=f++e||uNExaEM-J(u0Of-{pa3@feU!F>K+r(aMLYNn09Hc=UNgT$ zY@#$D{NMQa04cS_M|NwqPz_B(TzXOd(@5%b9Nz zyM@}SLcbZdBOf6TAshKRteE3O$cgaRiNr6-t4nU#-lQLUbar`ZJ(?i%$c!oS@!~U* zz^)?K>bknsdW9qE^7;*8Lyb9P4>&DKTfpV@8Fj33UfT?bq?q5*B$tb$;&ocRL7J!F z>)6?aC0uz?$#PXnYM1mKIpU3y=bv~27AQhfZedmwY^=<_jG;R0+ID*^37Hu2fCON8 z=r3K#*aBi)WQbcGFb7jjZTRnh`krsd&#|e2O$uuKJiHHw_22{{QGIi|H99N^gRW*s zu0!Lc>=-nBb8?LwpF>ETfg;s3d1Cr>cb9j{l#O^L66zp7M5^@XcDvr9x7$B-yY&`> z)A_h@|6IYx<14Oa3D@tC!X8~diXr4Cz6+37Z~b_lcI@d{H6?A?Xt+-)_WQ`^3|su1 z>sxuJ9e8F*T^)Jz`Sa(kBZge0uaqA)FPRn$NlP6b21h6?%=KX1#sb_pL(O@@`$h)l zO;3kjgZ!j#~buci+}N;4pL2WB6rI9p zSrjgG6jc<6wK;rOovYee`(54T>Iu`-g~LjBG}JToo$6E08FbYcZGJ6vMtD}ic&e_V z2(prMpo3>s)^%B1vUX(c&Dx)J=#TzHKZ8>zK>@rNV+I(t26$x!25U<(rGe}K^o7=1~#>1~_%j&|!fU1-!7i2Ubgy z01A0Zf{Y%~YBaV`LX^_Ac_b^}65%ogd0i+(K8wcUwr-T?q0x9I!x*>e&YNyJO#G8m zkYABgR1~!9kU|u=A&dxrjsnSEzV(srj(pQXNbT0D#YACfe_&IdHeaidktnEuk5ErNJfp0vo`puLJ#;R!icemS+?hX2(_wwV>2#)_ zmayzf6;oqf>$b;LPMa+|KX-RxQc_bfXWS7hW9p0>))lh+_Q;M>zQmF*%gvK>*iu5f z8-8#D$IX=MEHxYpPo7}C7>PudN|;gVg58qE5Ie)ZjdL*jlF`uv7WQI@Z?2~5(2s3{F%opR%#xtuXJNIu5*q z62TjX6oB?0B=7+o)c^t#*Z1DcZ*L}FYwKaMG+x~IkLi+8>$~Vf&khHQg{S}cE zsYWtVh3qB4d7(v%7TrEC2yW$x60MuBR%tX6^{kVV^pg+IE>AV*pmBU(9?R+tk-yga z9O0iVOw~^+>vcYEnPP@?tyWv1t5mO0X^Z4FFzKwT+yEE(>+}1h!lD8ervk3lg^tXH z@h&@SCFH_3z+Qx0(BT}gM>Cs!fw>v|sVshGs|e^<(7uQkP0#y^DXLN;N%}tox1%Jz zidS>zu;{I$&?lP!jfk(wRcMW>k~#83S66c4#PsQMLDYTkXx+=zh1)FTFVw#|@#<@E z_EqpNW-6MqAo~wC703@Ra~dj<_@wa*OJXJ?4mTz=h@mY3>Njxs8ur!bxqy&JK;!2w zeI?P#XA&Dz*)*}w+Y$>tO1pG4`(#H|P7h)?Ge_O{ISq2(+2m&meTfqGI;N}l?Ab$0 za9i~0^(o@vs_Jy7s<7ALe7{M_lq*op zKgrKO{~VqtCu=_Jz+DV8Jy~FFDxpro02_sYj+6mH6>b6TAow=$fSBQ^R4P`lz@oIp zDe|k5>xJC1TqW5Wq{761?&Br|O#K@fbq)vh;iCcw`4dT(WwmQHMZKlQ_%T!lu; zhduY`-bAU0Q_7Ujx=Ari;&+PCN3*W^bBQV~b5( zHZhl1g0_+KOUSp2L?XSn;bJ-QL{$ePHR4cYt4ZnG;(U4Mc!l@6Ai3a=WvM&`r= ztClOq%nIds?&t7m?1}aBka9z*ZfPUAJbImGWeM>sc`vFpMp;(NmX?-Kb2ACMx-IrF zYPzYwkf)A7>wtQi7ODv(ag=>v#PoVgSP@ED3tZXRC@JA7vV(D>r%aDjDSuc8Prmu; zCE9qJ5q!zaW?nO7(!yDpY0cDtgTX!#N}qvi0!2@-gc)jW;RWhQz<})xpacgy_(KB? z{D?#oS!U;)f~j&z-p{!P9hjwgCQ~9Tsk!A(kn0Wj2$ozO8l{xejGFHCd)jYb=%-#nYx;sUUaR9cXU*yUiP$VQ?Il zqn@kJ({{pMKjx5#3&w_PlEWCf5e$wAGE!hLQSgq9D;(hlg+Rdt)J(#ILJ<;38-{zF zwa3u~#AXV6?v7P)xx__A9<&#Fd{xZ2$-pMEX*mb-_#*{c%_M_!m~yi1xQt~&X7VUs zs+;1C!LI1%3Z*dU{3-ID#fyoJf5aDhfw>sqUV z;XV>aiz4av#Aj+)&jr8X3tS_n;uNdFfeKq%e)>e;dfV4uQ@lJH>WD7@CNinB%gk%R zygc;jGe!mA3YJq0iIm#A_5rgz2z39449Gyx1-?NYF%SXwe88Y3@r?Uo)rwpqscuLV zIdzd=!iFMaqD8Ue77p{+c?RQ!SM%cZ?5}@h5Z3((|)2g!pAzCDPqP^b*u=aPi#ZW_7minE*w)um*^7v#I0cu&+C-{*PM;}z#b*s=;=sv(X?WpR#x4cchq(%Hs8smongj<2MY!{}r=Lzd`Q%x0 zCgE_pRcZzEmwZ7yw@xeD{?KIZ$|}R~q;PU17(|gki2Nt{G$Bzqm3%bwfW;ruq(7Z8 z!_(7qj|dG*l8WTZg52A2;1PXH>@V{d7yHZn^JN~sr9dE;+7q%uyHM^S&v1P*vrtv& z5h<(5baO+p-tT_bSti6aq5W1}PDdSj|Ry{LkCu?7JC^9LEtj*o3#yWMMJ ze)CO7!acc2$MWO}#I5dxi@fdIhwS6Wn`~w^*Dl0zF$v`2JcWZ&;G0agA4-=DL)3F9 zlSKtMhb}MLpQBZjz+eJU*a(~FDF&h_K=BR+3W`3|&p`>`{sM|*I3Cy^hDM%!!Gt&` zRKOXrqR?atnhmhndx6L7)M}IBs%o8?;h66zFuS`aII7e>%j;gfU)N-rFu`rgvCpwF zrPi971WU>^lE)@YAT}PmoF=D-CWnq$Dv9T(gmRjOS;_ZS9pBiZYFu^0D%2Js?Hf}z zqoUE_Y0Sy-(#0%ae%=UAThw7M+qonaFx5_=d0O)5Ub=wkX5^0cFas<-Q^)<{i}1JK zJ~=xSx_tSkwk&qmWSG%;9_(2jOkmB-Ch>w@&`ebm_JAM1FjLLcFM1$W8O#hAOxT^E z?43Wd7pN1gI%t$)$y9v}Fj^)_N4X&(;5TeH;NOP9mX0Y4E_#sqR)5y7zaSAE4g40I zW%bLP-1xQtxl5>4I`|GQ`OiGT=p(^me!i)vhq#H{2eVJoh&B!J(-%sgyLdzF^BsuQTBt${@Bl+iZjr$;q%QkXYO= z!N8nc0XXnr`~$gh+5jDCkIyj7jLx%9;VTMXM{yB4DC4I5HRShFvtYNorIjoTObm6lrb=H-wY?C$6Zv84qP*4SO6%^iCt zJA$t2!06KCBae_@eC1JzMRLoiUoT$7`TGde^6Z9rqb%sHYlAsV7~!Z{N?@B}#0mi= z2F!JkM2df?9ee%$PB1{rlr~|P7AT9=J4Ga%?R#>o(H3WKtd{M&5 zkN@z)cjz%1^lFB@+ibSNSoy61LHMCzc{0UJogR!=cg32impKd0mtX&g;Mnrc@lJZQ z%u(t@m2uSOis{K8ej}fQWlad>Z0Z=*TR~Vm5_#N0#2g;EKIwBQj25oIY5Qgl+art# z{DlsMqJXJTY80fit=yoF^Bu5+DkirD$2YI3aF;CXyrrznKb~JTX;PlXC=54Qz0gpF zXNf-vHbEU^;09urb=V4`o*6$erTYHBK=i>eRoLJg2*`x+G$tR2GB$O2KqlZIiUHB9 z9Fx2(;+$8u%v5MK$d1JJ>HNyE%h4;@G@;vmDjq@e$Y=Go-N;T)QM~#7JNg-8<|ALl>s(%8yoNYUj`-4V(YjYW2<9V9 zVJ5EvC#>ph2%6?sPTPKdBKX~SPBgX+GL!aU;!e?^84E|6@zS7441+N~QmoE^2y_TV zaRH}rU}+%Lz#ZTb9GMdH;V0NGfX0N3r3!l5z{oP=XeFw^wi>aRQboOQPHE1 z!h$YWcPZdQPF5M@32p(K&jcPS&J*C7n?s-d|9p6$K)=zOp$v9k!2_Tkn(o60!3XpZ z0^H)~uNf=rPL$2n%qr3QMI6Mlm;+XG;rZA(bEs(cCZWenu3su_E;H4)v+i?m3-7c) znw=dRp6(?lwxRZqhofcG18*?U*l7uBxZzXGA_ua7L@c0U#! zwJJ38Kw?%Zl-M^uhEnIxlb2#fUd=4D{jtX;7D)bu=Tm(pF>h{hbyrZ&e;HyQZB(=+ zlQ+fN=5R{BAMK#-4ra~umoMVy9U|gEY{0;eao#>7=QQlqz;`H9kdUoIEb;Pxl-Iqs zTADtv7tNko(mUlr&O2*&U*VqW?HOgFhk{Oz2cZh`-(cCGYF%7P_Wc?p_2}KJS>$ia zTPJt4(Awrqh&g+ssRhnh`?`<(ezLSYC>6&wwty_c(>p6;lZUw@lOvK%p%W)U{tB+E z5xG;}H8zGrdR-XXFG=9-TPWVf+gQR-=g|UI%+OQ*-#HwCzQtANI8X}ul3_Gult73- zfOo+CL9Ay*Vt(eF<1;ifORJabmR438a|v<48}tQ4XTx`zgWCC<1U?J-COP{~X+(vV z&i!|{)74O^E2^|PT4Q&Nsaj}UQkG0uy(xnvrgnLQ|4@gt_QF_2q`IlGE;6iP+P;}L ztZp>at~|5^(Rb{KFXc{sEZ(vrNWeUeS^PEMKK9tbEv42u;~7=6J+?`U(s%bx9~T(g zQ(s{W*9;pjn9?2}hMJeRJEtTt_Tzf}TWB>9AqrCy_$EQ7H%L_r4fp||(jYuBBpiud zH!%A$!DyUUq#j-P-wZ7}ZnZb(BDB#G=*sWV3wtVQW3Nx# z#qfln6ovir9qjX`f2o=`^IrBpH|=?pdura45uUu6jz9>NCs47~ue^$W1^EAQV_RqI z2%?jgo896YInq~`or~nMf|On7=PUJjxedV^Ztzyw>H7(=TK21$eSjv??IXtm>%UCHvogtT#b651d zHENo*Qy6iii<>G7j-Ej@6i39dn~5C9B29CxsIin7x#T8}$xz2E(F-7If8m5SR<2Ep zo_HcKa%3P9DXFTe3@59n9-q2xLz}8$?Y~e;oV@Q*EFSAIHLRl?o6RYoq{r zK&X}Qj1E^vw8z*TaYr1Zl&Y6=mFj>>&e$DUul1|ym+eZ$Djh*#L>dfq`^&4$L49f5 zJ;~T!ZVgx?Fvh=QBw8Ou2NUEwy}frVSa3&gd;z+?t=lCDB#cYj+I=mfOY8JYQuAhC zx2926wR-19l(T+)Y&C2ClZ@`Bo-UDNvc9ZcK6=#5k%F2jO;wum-tIA?5v?s%@+Y56 z|A<{x-}h_Qta$`lOI85gazXy^EL1#y4C@|Du}M^2Do7#Fs6WmC9FeizDLfuBeeD0x zOf(=5jR9f`V@bTQhc6TP4Tn`S-R;ZMlUBD@vLUA)xtg&JYn|hr>^FCOm)-dCqiV}O=GK6F?6Ijiss#m z-r}0&8cQMRz87CaxiD&pSo}o57%2_93sh{I9Hv>3?o=dIU0)R_uM4ehnz46XgSvj* zz7;43b7H}qi;3nnVKnUGMRGoU!5k@UWTv5Bn5KP8jv6W3+Ju!oHuu!x+O}O>YWG7n z(D4%Juv*xu*#O86H9rt0#p(+yD{P!#jsvy=RsClR27@+G8Z;OVQvJa@#~_DaV7B4N zB%*>{{W_649Y`u;xke{*>4j!#c%pSmyvL$&LQeB#q_!LMvQ5FwIzn-jQb!^6AyyN^iCA-))3W+0!O!ny!`2?~+cN#(wVQIQj=F$PtEg9b+6F zog4M~grsre!OPPVoe!Qx^tEe~%UJWCh;8Ypsv*X8N2Bi0uLdJ0lFJugc=OF~zJWTo zw!5Bqg8aC2tTd$Ph)3GhLG`HODACwRM1k!<=lfxP_I=cx@yte$>5#F^;%D}FVR`|} z;kX?pOJE4YWh{H_;t!QtYz9JyB9%#GTnmSW`e~jdQUtIv<&=Uu0Qv)~%X_3vm$HS- z1XFdgNjxe{eiSCYnX8+Vs_xbHR+i|vgs0f&aPv=v&Y8o;IoI+0=JZFC#Cy?avv$M6 zXNkw&lwWOC(X;6{6$C4d9+!xbLtFf+g6U@$og_(GE;^_5$5n)7bC4NU6iVBJn|F*D zv4iB61*__tssj~GlMn5Ga)F|G!=d?zv3+~-vVwV!C7M=+(3#C&Kla#{?X_k*FDTZ_ zD$B-IINim=V#U%>g-EDq3t4=(t!rvu33Q}q`KW%87NDqu@-C+W6*Do>Ayfk~#k}`d zWTU3mf(%lunGtv()lkVF5PafmF zuL>nUZ(ND8m)C1OY9pK0!mW@i4Rw826T!;H<|=Pxb6{1+;ir1#4Vw-w!sd2lEo;G( zk><6L;X+rGUU*%y)LOPE9pG=dBpg6h-U8&296^jh?DnA5ju(9b+9ix?ZB!RN1O} z4n5NqsDI`Tl(Tki3dHqNd`pLMDsh$k^*~9kqtC5;%PIC@`Uv^k6bOd6RC@gO8G+X0+mZ%HdAG) zMvDU{Pj23PpjjP1xpQY=$BuI;Ro#J2o6eje|E(~)We)P`a$@gc)x^EbPg_RmrWj_& z=SZ8Iw4JS^tRqHs`1DoQ>WRCb>~)WR?lj6-vnC1R2o!7S%^mKq^Vj>P`9-q`3B!sq>queR0J^H@}cGk?}BQCyk}V2KoC< zf3n{GH_3!-+W^II+J>SMrW9!641eMzM#h}WBy@o3OyK(9QbtTuir2uHyOzh#VPvE{ zgcF(A`=_-HSGPrzlXX*)F|B|whI|$i>#+Yuol8Aw3D>Dl%WAv}7k78pqUE&{CoY^4 zi0b23okrvIujEOTTCH4bcA=@{7et`vJ{a6Du2~Bgs#TkPvh*`Pl{t|g5VJk5dRX|m zPVRVG5Mo(#MOzcWP^_^5W(+k=z46Fn^A(Nj_bo>`Ten8nu@+vAG_4FNoh_i2DiMeL zw$fMa4VRmTc~yF)H&&!ooAesJO+{2ElRsX*{AAJ#ZwRifYhQBRR}O7}D>X}{mOOWM z30u6YK*bPeTZREf7GM1id<-?@90oFiSrGB2+(wW~>JJ^QOau(W6 z-a>ps-hy_J^N^>jZ~x?%ckGC5+49u(?P$`44r=kOfXPm?YSB)M9C-*a;WS1E=7Kn~AC?2!^yFJ$Tg=i2&aQ zDD*{1y<*a>VQ*OvdSqOKyYA#{>DUVLMkzX&larGx356eG-f1=$=Fn(lr@b>p?3vhi zemvZDjJj)Lo<$$Dt*R;-IjnSCQRgf}%wW-VXdDu|l^bBor1|`v0x=vsXB zQ#i+oaE{tR?1oMspdw~wxOr?WAgF+#YWsn{8Dk1^ANXvC*7V+-LUUP?I46gfhq{vF zIGu(`zDUgM6Ziw$;JIh??VtYa)~$*4>o0BDLOj~OSK*dw3W|h#+sOwD6v~OoYQ`8>?Ww8c%e;}Jq6M0qUNF?qs+aI%7cu&jXqOh!Snsi!2ctP^l&*SmW6Hw!L9(?HX zs~^Lj@DVU&5@$cI@jezvZHD==RXC(H02qi$x8gJ!u9)KgHw%^T>-r8UVW<>Sg1$_D ztCyqCICk6h`KzrE-A%-6FQdPcPW1Q6uD;WqFYewAJpa`0-2`*YoM4ejz=vuz1$j}e zVfndo;bIhYyN4C`rc&hB4}`-Hgup{v1NU8e^&P0Y91U@CGhoO-NB$tkXZRgfkqoE- zh?#@Ya_y-^A$TgPH~-I)`)Cq6Ko|+82L%&L9tzgoIa;#izP;pUg>SE(Gea+Z8!-%$ zGO_Fh5BUb4m)b>cCysXZo#}jO*RI&MZC9?pp18evM@@l@Gk#~w>}ZwErHVNHQEfu3 zgY=6!>NRtWMf0O#0n2dVU-9^9d}ZY06R1%X0rF%fKfxqy{Aqs;)_d~26 zSEyhF3iv67haV2$65|!Ns~~#&XR)69*mdMBv=F)9De*~DJKUlekA1%%R)AXsA$@-P|@(PjBjmJge6rLmb9dZy!)+i_$>(kVX(@} zs=E3i?HKgSF{vsX+%Gdw%a?^POyT(^@Hx|fm4(G9kXa~kty^JWkh*E$sp+Cjoz7oy zBQ2C(4ilwi2{jrWC*L%8=t`61@uj>~(-V&zD7y7ox>ROCdt_-{*^VzxUvSr57%|N#>GwwJdXfvX{?}afD-SK6kOP zIT>ptKI-Z_I_0I^yW^WTU*5J2ZT&K}Z(r)m;vMczUn*Y2w5K2hpbt9BpL@;by5)^% z^o{sUaDly6b@ku0!w^5pR19VM8*zUGuD#6S1CRFq0iJ{V!og)i&|AveF`)h~z z5C07%MXd+6a@Xz|uX(t>R9;by%B%^CVpM2TSKn=upWe1Dymc!)D>2gJ@HiroPS;o{ zQgvR0zj?@mEuy$+R54GI?Twmvaeh(QU1Zlm1vy=ymAiwAMMwPpBfjZy;aFoW;N)rW z$urw42PUh+iD7dE9t9((XX7IAn~-`yQA?b#FBz5Cfcdx%lt$JSkZWbI?& zUWd2H-CDwy+JpP%N2X7Y%qM@093Ve`Di*`*T{eS$%LB~(1TfPCn3=&CJiP>Oc7ePr zACLo-8y75SFrl#ph@le)U9fiyhBB2-4EaGLF#3z*0`LQjHv>aF4Eg)CIus)OD}JyKpoHy)*f#=H0|qQh)&aOHUffr8mIlPhJ@Nk>!~T80Y_2*bP? zIW+R0&~y5r!+a01ern&L8PDw45y_Z#MWK9t0Xj*(SmbeAEofON)WWohgV~&%u_}>B z$+Z@+YrG~)AmwPXdtIaa!8zG8vj1{|J^6i=qIsrZ~=XA56~BY!6vFp z%}Ui(;mJY+d`>+os6@~tRB0)7z5`Yv$Rs3U!PcV8RmwD^^d!&`Lp64UarPfrf+EZi zL$P@LI@kXoSOZCZm%R}kT0?K^<8aZ%-`s1a2-kLyz3r<|4Jc5FO#dd{%8FRIZ3S1NJo}7cMj#Z`tj3ci{`$LoCR>VSd3`C_ZE$l3)+j zfX9Zn-DG;37XV^W)}LaSqQV-|Bg$O?>x1x638tasra^ zxwCg5-X!TWuwUx?uD-()U)i@Wyko~xH{M9xS*36bZMvTPWQFr?E>9+Z*zrn$qtXUt zZf%*>R;+QUQf96(kDi~e%rl7QLsTROpNR?10}jOqpZ@}p z32srb!=02x#Ar$1f%@pmm9csAT49K~aNJEUTIFwVk+FM255PY0Qx6qMgepGs_CPe` z6oQC78@Ef=!#4k@fQmBwJT z!at=a#O7R`-C#4;uyl$7Z4rdO3&o7MU7o|DbLeaC2?Xx(VqZMN<0U}vGZ2f&_@a0- zHQq~%^CLJrg83Z!cLtRjR%{S(9#D8gKSM_h)^-@*adZ~Yk1{stfA|ahl&UQSRRKtV zV`6`X16pL(EKqi1$rE_CW-(qxK;8+{{~kjn>|-%!`Y9l6($Z_Lf1?M5x= z)5Ce!k+1dU(DtG3wD{qN(Q8maV1F1%A4yLm4tMq4H06ceyQ4tf>#wI3HaZ&A4y_#; z*wuEe1HN=fnp&rmk_#kj<$pFWIA=L!mfo!%CPtXeD-w!BE@$p{}7G0iJ@31I~ zq_z=imr#P}iSmvK1-m*%?&OebW{Tp6`HYBiYgqh-H&N_QpArpcGSP}>$iUM}kbf09 zI&$H{N)%jm>#a4HXWTC5skfPt`wGjkUYLHFINH^B zu!caa@kwY!aw+fXisoid`x5l{4;nwCd!wbT^ss78R8)x0HxQh>nZn@r(F4y($!!C81~oW~fgLMo208drTiLe`t)* zZz7*)X^A&A?Vt+B8Cla{M&&KEaEO zn-_SX-cWg8Fx{1eVDy!~KD4Oe{+J{7Kr1=1kI1gQys)VE(VDcgk0wgpQ&I zQfVGRzvY&z-ecvi%HyG5Pn>vn%!RUO^4u8T*|XTjoD6jk7ijsQZE+WJ5%}Y{9wMu# zAE`k{0yqQP!<`_AdFr`%^e@~n6&a@9gl`TT+{9;#o6Sc@k3DCAb$%F|zMMsI~}Aybqb1J>b@>52HVnX{Hfm(7^)pJT-(NB=X4u%zqSXt}}N zueFbSefz;ZdwOS#SB#o}!@+}Z?A;68)qV8|s7EqV_nPTp0h}CI+k)vx*+|?>LKtX? zD@O61(awIH`tZYrCHWIjP8+joW%^%9Bzp(PiH@MtrQ|qtycDr{ImP*QVL3-geop#n z!L*Q|pFUlD?AX6fox(gm4d`^9)&#!tz@&vN1JDHQEC`H4kOoeUT2X{8j6t`U(tMe! zUa-`M)C6WoDIRi*=b7w0mPsJC_ zo&54|O&Y0EX%7)qX=5wR@&1Vuv17;niTNN0_~2gJ5*int6h~z;c^E9kpn5=x2gE1D z2MfKwJqJ_`PJ&`8U`wi>3ywy!u0Dkd(enzLGU1rI96+J;4757H9{NE( zMLka@iFU!v1@3Wky!RnOm(d4r|B$|JBV06o+S)=hle>04(-^C`b;kJH zn`0whT2qo^tf{q@mcGd^*>m`C>fk}(Zj8-2S>vc0zJ`8E4MaizXeDD^M=3naq>#O) z?i-jBa%ga>11C%D)T~>iow3OzT{z3KY##g4l>-N2ms`@8FU2o!LZ59+@7}a&6N=5f zDG>E-pV+ar+Fx^UYO(y);?t+ezZaut($-d*<-KFa%3sucv-0@ycd%ULKwbS^pgVGb zZWw+3NV$w`_p=+m$^N1RAQ`mFfK6oRXF4?DN`zLrK6Zg7FDvmUlQP=!6*CAli_G1Q z4nph1#H=Hr5E%7t+$8C^Go>w-Mo=`$cHcw{pG<^UcM z!v&|!g#~qa?a}#+W#rk&mv^H3P&}VbNP^yqf|$`zh=|m1DN2dSCts)8(w%fre>Cm4mytIQ&S^Bq5IK4WQhH;|iv6DC9Nf^!gLT1K2wbC~STw-Q1D^+O9O4l`PslnA zF-7L(|UyIHoZ0X`hfoUzk@0cIp8v~7*bPxneEU0$P|2~FENt8_x z0*58+_oVC-G!D}muSjiF2?%ogD$X-nZhmUZ7V_T6|2#q=FOnAO7url#zAX&NsvC?` z?RMWrEBU%!D$^^QOIIe?94-x!A12d%G;dl;5T{QkPoDhO%{Rk2FaYx&1x%@??{m6bI|BSdVTD-$I!c@TFk|>$dwz= zARUg7h1!F=zpC;NjVikBTb_BOUk zaV)GGYP*io>LGVaR{Kk=DGi3<4@dhjMC%9X3&U$O)eRV= zVUA#cYAK_-i-?aOhA>yQEofos^kRabqZN^^9BX@xzO3Frzmd1`#v8-qBgc=Y9}a(T zcHTUqVEp)F-RXzt-N_sihI;b0d8ndjh2B?`0l{|v86N{l#}tq3F*dyWg9mxZiMJoF8BqPXme;xI1y~~3Ms1>p9Sb@irL}r zDfxx`%$#00ST< zCGF_%PshloYn~+t`HYx zvnJa|&Q@uzn`vLMLGbx^Fvn=6N*62j#PWq`@k`M0fIjd0Df089>+pq|=N~Cf#BZ3; zaYJ>u{&aVVh$(qwRJm-!vuuA=al5WkBxaV7duZ0Q8U1qnc``chJKr701CkHr66no5eU3rZ=ek%7{IkjgH8D_C8kvMHAJ4e`2h_Ed05H&Z4z!}K{s5F~Fs8qRVEqCz_h-B)O3 z$3)KbWB?0d;O}4b?3e((Sr|74Dw47g25hJt z#1D&+51P;EVCjQ}0{x`;4Eg^J;DpLI`}hj!p|%x+x~Q}i?z!{oi;z!sLqAF;qlzPVnBSmnegIOyV9`Tv4?{Mf1c1mj2!Zp^)2T!3 z-}#AUkhc3>uGKn{)gqVXy{~-bUG}&ahVu@_-%T^(KR$qqRngW+N3<=9!tIWvq)wK; zYXxj~f7E83i!f(nWKoRYcZ_6Ym{;_ol77)kp5@RwU2+5G3Yu zvc^Nb+;QMH?0;UHOCSVT>o@k`?gIFZ|0JgnLBO|p{JXp35joPG*2LG}0ClfHY~ zwr$1JIzppnqKd|4Zo@JwQBD4oN-^nKJ-legLu=Tht$%T(IGm8QrG=S|8*GCKyMB1?*&c?})5_f(1 zWzTflJXF}3H+tC*ecvZg-rsiN>usbYKSFPwAlJp?XvK-t*?f9wwwznaDY!j-AI%4t zmwofiSmmBO_rlYdjx(=b2HY40@i`T6A+EQ`^hQA_2;_ufugoV(dT>bY8ZlwvG5C`B zJO`x+zJ?&1kfz%x+CO3H3F)q`ICOPdB2$$q=x-NBC#K(y{(fomW^`vsN9boZdEMrJ z#nIY0`4hQsGx`$3W9N*;uA2Ht)Kd{@ggW`~$trGRaj-n#ET{48BppM_XW`=H*^_FF+z-8z>4%&kM-mDlYviK9Ic=tVxh4*f=cNUM5&QFzI{cWm=t|3u$$xKlg&pA#p*k_-WDUxiTe z(jZcwJo(WZZ(zOHdi52Mw+isv2eB3Y@&3>&mh}uBeS@kp6N3FOE+&qDo&Msh`|dj> zSL#PDc1PMTXh*HE7)GVgievYsgHe=qWedLEy(1pKBY89|5r--d%y@EJsAk`Y#>LUZ z(IojJ;F5;OI(|HL?AQm#k7wj-G}H+mpoIsql$ugufRn}W4_E-f9UM8vbzuQ_VVuY@@_X(qy5p@e-s*AV-LXD${av9M#oT1etdu4w$y=22PA+2m>ot2`?KnoW zq>bn|@ac}G-_|v&?4RC@8x{dJ+aJ&_H81Mqn zT5F^dG4MF>2MxIW*iD5;1l>WED1&amS|nrk_vd6o48T9k+USA0tg9> zJ^0|$#OP^pXa=!Zg;lJgG@E8iJJHWakAiEk6mR^C%!SLGi8bV9%>uqVM~eW$vniP! z>Um|ji4J^)Z~U6%0?+iWCjmW2#1VO3ys}6HtI3k^@-`KsFIBw zr4z<&6rG*YefIB>&+kAt)Q?V(KeEslixv@+-a?4Hn*8mzH{bm1v*hQWzm+yDLhoxW z#)!@6@@z;bor2Jqq)C!6_ycw_KvSh{M3c>1!$JFx9!(rNM801aS-w0{7i@^MwniFw zaCG@uQ?CAvc7`^b@->AJ^$Sut{9#nr$zawL=b)|~8f!u=Fg~>+PlGHsmxmBSPY}uVl`3=;0~X zh=w(Y(Ba%VawHcyX|#uG4W>#f+1U$|kzg(7i`*A@mGUA>P5O;q;=;ILuGE1_p~VqT zJ0Ol^M8BRn^V9K!)b0{QxKY=9q-m@e0LwJ*qH{*K~)K>RCLcVXi@F9;cq=Q5G-P&o6rF$Px0}yJNAt z6Nvy0vjVDgQ9TwN{l4&k4gj47zD#ChDLhBUyTg&+|MpcOhWSTd6|^>1Z5U?Ix%@e) zKzI}~)w+`HY$w1&-rbSAmJmGM;)$}(RTj;zRjQ5G3BUQ3KpT|WK!Yj5VW9;tX7}~| z1Tjq_T)L!7)jpLy&eS}MZ&t~kq=MZ40+;%hh8}f^W=eHgm!r%)sW!!sxojb~Zq5RM zOIf@qJB1X755JYs-3*9ZJq~tT3!qKL-oX{WVaR2{5kbcdj@80RLGa21K_37b+)Dya ze@%bm^e$v^wOG*bW6?}&%xe!=n@`i;R%+J)$i(zs3r=U!tNC zMbrC&uOIEf7qOmv><71#n^fDK=#Ip#k8J8Rq^v@=LKV9usV3mXDtO-05C>Di`DD7yu+$A`Y^WB;&p6}(Lt-f2 z!FC{M0SqI9J;oWU@DEW_6tqQ0G8^Txyz>n67D&)+-@XIUcK<)Z-UA@2EBgcXz5xc9 zftg|YFrDeW_d-W{vmr$VQLHHTUZTd5SQ8VCv5PHD)3fQ>#Pl^~Q#ZZ0O|sb}yZ`mQ zeCNKIL6Y6?`yM_Rh5_f^d;0mEb4WC_Eq({n9k7}Wg_lqB4qTvKjuX8xN{qhB+~`NY zF_BpY+f?GON-SZUf+d_QU!X^*N(Yi9lm289V8S5( zFzw-b!<1jIdDK+M2fypI1p-SEjPifLfxuD(3&i*UZsQ zUa3}Y7&P`T3w`}{;>RBe@|W0Gw?pXeaTafy!>ALeeDxygvpk;IEO9xCG&;XA$)^4? zG&J<@M?!J$p57_fm50J>&#gY2+_^J(hsZ+Jf<`xEg48C0yGr6-e?+x)dx0xZQe)Dz zs3w)G%FkBM0tUlm3bte!_?`y=|D5Vt;{x7FogVGurQiQ3+ZRp@b647;2TltD)1`d9 z|8Z~($6T3jS-5t;a!XaEg`>6DC-B3C64cPCX{4lalT{dK4h0pgs(CDdQG^DDcx#?{{?t#{N?kDc}f8VblP z$m=&c5ib()Mk4cRoJl^!BVGY4RP%`Mmx)SX>k^{0;7fyjcvY9w_OB%NQOgQZRUyh&#zUJ} z*O<(_tlVts9?@bVcIBs)#1GU()HLPFCWa9ltX~fw9@`FAunn%JTW@{3ZgZj4AqcT# zGMDxnaSACDaR%Mylm>kWs<<%ZW~Hqkp{BCGiQBmb{%?cmHMEmRya}Fw{=@ zvF%R0#LbRpDftmr;k%Kq;^-b~8{s2Obqp=Zi48@%^&KsfuVXE!n0amBvv}guaPN+I z&`(GY9f}SQf0)V%;Ck-|L2hWkKN}r2fpdax_^B1rp=t;?{%*7@4ahL2kQkr~Mtt|H z=_B+y^;U^h9jI1sLnq!+ioQiN2a8v=v<|A~*3^bkZ*iIRfGe)|i^)6hxkr>B<|n8^ zIK=dghW8mug7)@`P$;22tB=|%;@%0rE9p#h%V9H0Ko=1M@fBWPhzR+eFmwMY^;zoq(-^9F_ z<5J@B#}gOy>W(;-g|2KrbxpIfZq==ukd@;|SUC5S9{`IP(Z4}uojmygZ9c)8n&6yn zVyH**rA}-q;&cII?4aJ1&X*$G6=THj#M1n~mWWkYD)$F%GVl^1#d7{e-hto# z0x`QLknDvJ?@q;z9dKq)hX$rBT>~HcqX_dsY!o-)oK3vC4Gf$I;yhJjIG!v1-*Sl%<+z_gW_^kSkup_NiMm#QTZF zSRsFbmDJWUj30xm@#_x(b3&+j7l{%FcOE(Nx8dQk#CpHUvo?~`wV1EbZut38{>b6^3mc=)E2aA%d(~D&qK1c zja37(&eHNA8Xu^q>b9b@j!31QTuYLGfQUzlt{7E}KFi$bEjL>1iR}*>B94UD5>`7Z z)`mhWYY)U?mT9qM&%XHVB>55Oq9)KqiG(|m7>q$(2H1sN5W6|cs2$BkG%gG4SV+tR zfnA-?n=%Nm+81e=_(vNz!$u6*xTtQ<8nafth4XrGu^sq%(D~vQmoB~icKq|tUw!qL z-;hMx>@hkz$y?*p6OirPym>{1I+D0$sX_TlX{^u9m)`<-Nc-eKe0sDuzFC;(lSMTgED9#I?t`%uyB+tN`Tos9a|@ z6e4F#bK*4i<>Msz{&L;)?_PW{@x~j}Jmb2r$bSdE#b)`$w{*M6m%$=GmVh|*(!_zp zviO`i@n!IUdIsK|4gGTDNbtbE!-wAmcaV-JeF}0|0JaUrpH~LrHKQA8Is*0BzOfX9a=H`th&2%_tRsyn;zat9!6EVIAqv-Y9nnlny9O zC~L%3d)>O=sr0X`IQ{Me|~8s zSW^%VMYcBu_!8#`1LSRrl8(=p~CijOtp<3dSsZQ=843omP@MHrsS0r?^<4m)1u%=UFn5sRZ zA5^qdlHJh9eYO#5$|BBabT)0z=0c6}gJ1*V+2~fV0`W9@+*hU+bEEI9eor*24ai+k8YT6(=Tz`)mhgmXrL)~_ zbVRBuc6gz#uf<>O@m4Xq)Z}XP8fAAN7^V}ds8|7c&|JWX<9>D1KynHJsZ~FM4s zWt(jdotAZ-q*Ss9y`3ZE*rclZVpd*zAAf42RM5xl&LLQ#Y$lO|in6I+InwTgKH1M_ za^)OeF)Noz>0fwZZ-Rit&Zxb?kBKh=YMnAn{T7`b(b-x%s@Hbq^}iww>mV}EDKyH1 z(h_%dVVJGtcIKEoMa8uC<|;HHJf^U`SKT{=)oxU2&$kSp&nUI z6s8`c3WBfT#`tErU!PQK%T|U%OX|r7aBPpbH`rLLZ1m-b>|&|JRdkrfwUU29?^Dpj z+SbsJb>@t{&Wi_<=Zt9q+w>%(1~PgX?-ia-kHP>|gef1EMQftbsb4JeK=gyI!Ws+i z5y_7Mob+00H~p}8Tb+7mUo^0`L)Eo8{PmRwtEwWiBP$|si@gyFJfX1w9l}@yn~|nvrU11g>iOsNfCa$SRx>jTg%JseE|Fh%UpGW7w3% z3YG@n>_*>^rs%g9(9^fRapVZKdL=x#HGG8ZTX{XYpYL=G;9)!`f*hyV*^eFijv`aZ;G=mMUQtm-xYik*RMyv)nC5|if!kOF+ zSebA-H8J{)9tWXwz^>}dmG9sN0$qw_}GMtH}^*M7G>0BfCCFl9Pi(Z<9F+ZG+pzq~8zQj1z$Ny0^j+8M=f9yp+jX$~J=M|~C<*6rU<+aS+_jnn{- zIDkh;^~S*U3*c5|?D5g*A3AXh$$Y#bTv7qlNk10jhoh|&KTPL*=f`|*X#t1)G8K(h zqBBtI8h~zjXO|rf6NV_&M18gFL9~8fk;B^09*|0y#QVWuz|M;JuKWjLczOgeym{x& zxyRN(0_d~TX9k_8Pj`nxTMXu+NWM_C9Arrs#K2C04TWp0ucE!oAd7JgA6VgF-Ql+e z)_9sSM^j~rNNEB@z=l}x7-@C{!9#Oo+F-$liHjsaQ=yyp9w)>X_EHj<_x=5S==sT$ zsk>I98@sx8O`g1`i#)xOQ1Q&lwe^~@H+uHX7Tq>}u3u8T;joAo<8aWpD?hA6+Z9Ic z1H2+)Xdn_9_>7v#5Ry^CF>vniVHSqluJ9ZfoxRvy#`^%W|QD5?gGlF=7++IYs2 zDB$NViX$eCnx^A6u-AtfWoax4mM>-W+94weD^HnCBXT6Q1AO4g{}F4aUkm$$1ME;2 z2e3YP9lbS#CbD{UyZeb@3m&x_*qKk{fUI zT`V|t@!~L>ci;f<=2XDPoc)8G!S>}C1RfQGHh(c4(v<+|FcH`fqXS?;d{n@*5SymI zrj9NpWodc^eESX5+~pS|#KY7l-z`V|NI|Y$PTWLo5eU&*R$f-I+gepnVb^5mGn>Ty zGfb7OF77z0} zw~aS}@R1qXdxrKlSe$KBtEO#_J3>nu%Z)Z|uRnAum50p-?8GC0PmqnsRg700K`P+P zP?~aq?`X^-omK$OgBI^N7mIlj18iU`QjbABpf}Ks)Ew%?=szDs3ox$chxH7YHbT@y zsCHs{8Nt2s+xov_JP3^m*^3`DWS7ZP3_a-};xq0za%47jJS)Oju~ZPrLJPtLx7@O> z{k`^*BS!`%71V?M-v_ltcQ7h|?lPz#H)uCpQ3$iy=~_4pTA61&b^Xbr-scgm*Kj~0H1?$$*L8| zKDI9SNdd7$YvB-{eA}du+2U6>I=zN~?iuPk#D9x?kWoy!&@Z>%8UqcYtuGG9Dz(|d z8o9?=dzZgW>s6FFW2Nq>DPT+3wXXh6Qk_J5{sFJanOt3+|yGUF| zoql*3^4`zPK2N>0?6DYnCPrPPo?k{3;HWtIi_R<8s5IWng`QZY4@CQ+`^<8mPi3(S zZRF2jBdUqj&CP277pyXD1<3PXp?&r)CsLd}+c$nZ*eF03bD^f_c8E`;unMUjQMkf% zGR;9Fag{W}0vH~+g-=J9(*;l?@;t>$*t3S0jM8EmC59;jd=G3rqm|nyz4}e$i=FCyx{c}zThM`=+)}B-87BLp|9S*B@oLExpo$rMtFmE8HZX6>ksU2H6Q#wv%2)0}q2Ee?A zj?El7vRvqh_#2ekKrrM`dnNG_Q>`Hu@v0UN4@=ahxFm^ZJmWawM-b0#{x7|bF{Z!? z*fz%Z$PNghQUnIlVM!>|;J^vJWc)%bhhR%mu?I#XPB~(`BfH4n(3f{DU;ZT_BESA} z`Dt{T>WIMP=F8OS<-{)X=_d*T9)Xi^Ujv;&ObUkX(xvMkcz~>fC@nx0hKB`F;fWJH zZv+Rb&z<`;5|Kqw zHei7mG316%FA$iYUMegI@{k#ENZT`+L^$Kr3hMHr{H4e}L^8tfpPDiqMv|CTJFaT~a2nS_!I-DRrJ>dG$ zqzpU`zn9Vnv{^zc1Te@R4E-Q?zlWawOqcauh^!8NavSLJ3$HK6pHVY8Ycb(Aluex@ z?_2M`VW#j%_f0G*|KNSa1yRTe)n55|F$yK_wx(X#3_Q4!KmAJpcqsiqb}=De6# zRbIaQ*s=9D{W_4u3&q2plZd>0)fzz?!jLL*JN^fqFC4p~oZ)(Vz#>Nvdn$wjkOH zg=25LxA<4|A*KFxF=3z%pqbPG>ZQexp_wFEw$-NyUmr)a4ZDM~nVT8%7w#oX8W`p^ zosXS7DTOw6bB-NrOC*L8iDCf%mX?x|GF3=bA-PyoB@YGTK*N1Bj%v~<|EM)x`HnrL zOlS|J4XvlrbH(5gM>SCi!W&LgEC2;y0l}*t5_Vv*f5@P_(_p@pL~rBOI6KwNsM{<$qO)#Tqe3oXfSh zPvzE(%WLBadP@StAZZ`ZFui;DaMs$jYpLh$ChPk3cBgLTkt1(oiw)y=?qeJWdu?=% zvnEwj1GO=k7BUdc0|W&PVg%MrZYeLm>>r-r;fRRar_0YG%Do~ zM1X<#dd&ozW*T$u9L>}o<}vcm=-KB`9(nTaF!2}a?9(fW{nS}Bk(|BqT(#aTDife_ z?52DRvsOrMUHR?-wLqv08Q3n5!OYQlFU+~l5iVKK5qlYW5WVm+`h}F&Gn_+pbqi0Q zj@4VwoYB`Btm0}>6T7Rxz#b>k!s2o4<{rjss7dD0ni_W`OG4~7(-&b3!iWRFagK1V z{=c4}61}9q zYn^SDNVzsBH!Ffw-li74Ry)xO6XlZ11Fon_(rOI5N{nTdl88vlFBh1KN<{`yqKE=8 z`s~?M2l_J=b0AGJ87wVbl7LJW_%LyZ3l1~70q2D)+9I^{#}m!R6ww?5kzSgiu{NMZ z8c6&KI@=uEt_O zLehBxA+73Dns3A$raKeUS{-)Go7z4IxZuPM{L+Yw8A&@Gqy8ikq1f@u6!)`@mGku- z^VKKr=j5Khn|=9Ke#9%zGcld(L_SVXZ9V*N_E-PNEkIuKwhI6zz^+7F#2mdb9KLaO zm!oNaTO?o$Pm6%i(G`{!$h4D`#o@}xw6NESR+HX|3?1Od5;x!c21X2HAL${`1HGfx zmEjZIVVFK{I7;}Sv+k(?AQlj*8wZK8A#intup6+((P2Ow*BBMU!14m;Oz>CYwbm`V zg>|8xP|X6vj`~eI4jx>yX5aNF-^%jxJp3MkljAQooPTQH*s z%Y0<-33cKJ-*0aX)Gz%E3yAmTkN0-$3QP=72=#;sP8C-rZB!|m6ve)9xMyehdO}H> zYZ>YfPMlcO6Phq#0`SH%+^%B=&StZqzV;GhBlP{laq^5kkg|AF70!4Ycp8kMPUm}Z z^-#txNa0;*PX|_pO+VT`NXKeY{3F)mMfaQzBKhYoCR_ezKv z&2SBzJn3(RYvIY0Fs^HXXOhbuy?#M<#5Hc-Zjg((9_j(8U#)^uw?n6xAoSJ2-F|Tl z4NHEU*&#A3!wn5m>Hr@#Ee`Q1LZ%}X+rV)b1Sui9|1rD}f*7>8pjjXC1@xhhSbyiK zAB9GDLrJ7ic$BBpdf8UBkYLwm8~whBk+99=aw1IGgoEqZKkWD2)JJT{M@#< zX&i-V!vunXQ8XK*2?dhYMj9x~0Q?N!Ve82ouaM_cAJkNoI6LJt(XLMFb5@r4%hzB3 z4UPFN^5c)Ml_%_Gla3q=eR3YshnH_bg9pi97Cm`OeQUf__(G&hIgQ+N#CzaOP zTyA~L=C5b4P$Z`Up-{NHI~<~Zt4x&m5cL$pPs#}5+`0T9T+ZQPguf1yWhvB4FI>R3 zA`@ai&w`!Y0v%9NwM(f^9}vBRX?`Qn2QU@r6=TCWB_4oT=$=Bfj1Z4usKCGRN z1Hg+Jr#I-gxuWCpTg=55wruJBdCJzUxB1sy+~sS3=bd(67x^a9yCT|B#LVMQx$+|vwrSCo)2H)R{OQ!G z56+!CIv=kmcT5NL!NGl&Q}wrcs0qyAelgYy?yH`Up62A#Vd5kQ4p`E0JUB#}X)@+M zoq@+tZOr26F7z+*CBk~;AK{PBE?9tW^REibpC4G|9{@c?N>ykK^_o^8Yw)kRRZ+A0f(202^{^IYq$ID7S2=Q&p$_nQWuZ>=CX}({LWhXM z0$Gy^dIov2va}9z?-HW2oxIY6 zM5|Uy{)1TA=-Y4KdTYmm1(7!*01m2K_hAzMrI!zF@ zJeLE$G18yW1&-QKv%(w`2;OYmPE}H0)5Rx5)j6BywlkZfps@bF2uTA*(caw3I1qJXRPR-76yu$K7dQH#HXfRq}MCr@5iY77HddB}A1 z=mz|?>(r^)izZ9QZhg$Y*du7$@=3!yUg;{<+;RESrUC=xLw!H)snr5Dj2G;>46-5B zOK(hiCY2vf`VS>1pwUyN-iWYB*{!2!1am)~GR3zPuq`gXpq&eKD-%O^Hh?MgZl*eKAmm5a`b3FSZwaYhbMKwwHX>8!BqhH z7;MLCN7XA^)77bEaPFKZif8nbP^_gOa&sWu^~7u4lwE3TUtxu zCo{*7VK4Ywnga0C#8H$_-ivZS|NMRQ%Lkv4yuc^7BkOP&N|om=L-Tmf0!RJF9+?d( zo#X|GP!sKZ8!IUEQ3|QI(9dy;QA>e>>1WCdy7C0sk!&f7c80^9!O3U>b#8Kyf*N+x zK-~K|T#uYRd#JcoCfhQ%&09?P+r1}F{O#=7O{RrdU$t#CavX;SOy)7N(3De}{xw)Z^vXPHiI^HXh8DljKl{#L!k3`C6 z!M<4f96>pb6gJUe>Z{Mwp(7v|5RO1D5E6jbW}fo}z3^(RA>j3VnnF@Ufk_3n1oaQi z;=b@YK|}~*UsPTqOcWMRd{41pCV$~-jcEDGPoTlk(xpp@EN*p~$zQ?emHI8!ab`k9oQR z#;P^rf;nBjehV`wmnmDhXk)cf&I#s2r=5xUa#aOn+i2O+3;TSQAq6`FOVf6G&fCD4 zxv?J{D(!n8Oj5l9!=z0YLmtpJsLjeO*Q# zXP)=eDe`@^n%V@&@Uv&3LLr2kAhKa*^q)aq<9;j{XPMDkWf03uO*zX{3#M&OU_qRO zyShghE}BUv0LWpqe@Yvvpu0i2|L$q*?*&tnj*YZ7gE$UsDseKrvQ<(ZralYR$;U5= z-w+AKJk&=2SNA|?3mG~`O$eYk^%Qm2nodOUtmY}K3E-(5HVe&LtsVb_xdIbet?MEv~!zE^UvCSB?_0}A7mQ_LVFHoon2l6#%-D52$ zPCOo<9xpNQf_WlwxPtEkXv_uxWc=A+_G3<_ z#a>V(2Gw9N?hm|tGa)Tt?nlecw9i9E(v)RL^%*oIF`S z**|F#V|bYQq%>}?D!q-HG&`IQ1^6#GZ-M8qrtBPZTE~NPgJbr7&|`4ZM(im29@>=! zul@i_?BBf@=fZ#c8+HC{ z@Z2KwRu=VL&ielTz?3P7zn*$~!L(_?Ua|<_Tx~@4^IWAtDr7=G9>NDVA+VagZu49s z*FB|8R@Z9EVG#3tz9!$4DZVz++{Q3}bo%s#kTWQ1@|Kl(E34YQ-ga+kskh?931}SE z=l8Fa7v%~hObN@t&XX6KG&+vav7OD0Hb>v$veigScI?sW7gy4w> zux|jHO2xWU_Du#CMk@oFOj3>CV3)9-ODn@n2pp{?Sme)<+rsG4fq{cF56+xPl`L68 z^iXzmGj$SmkpoLEp{|YP+ns`eo2Zi&JAG2${&I$lqW(!5+88$T4RAd;JPehB1=O`@ z#r#;z6^(jMor=U_TV#G=$Q5!5yt05eD1bFFA!c_s^d@ai+dA03!}G5Gf6z2IuZp!G z*59d0&k^-US6;xzk{zv2u0xjW@^kkk2e27FR_( z;fhvNkO3$rx1*nqAOBnz_l6yzET=c2Oe(^ma#2Jzxz$}2Qa}fayB0)WdBt5PSDdfdR+`P4JW(6CJY}vx69Bq2JcDA)%2;cLOSFT z!-8&;Vtp($-pOPY^k`t*-PAko$gZ;3LvFJhi5s%hKJ0YJN1g?r#S2*o?74QqB!rnT zpK=hQZcI9tha34ri$g4|A=(I{38)$49%+)!!Qwmxw!kLdRg?-A?J1fE3j-%KMq=+Y zKo#<{z!pqAi!37oiGG5aW%X3EYE(iZXK}tiW^p&A?&Nh+UdeD1d2D;^X6t+r>b7tiox&C^pf5wa% z9~y=tHosUmsk*=1-671*T5?i1Y(0|4t6JtWL0`PkosZGPILEouP4DbWI>wwb0e*p{-I@Nw15j0-X3LFe7{=$1O*qjl@GDX(Y#F z^y+k6B8PG!7?At3P9F`)Es;d8VDTP5;!`uJCzlhy;tuHG7mp>?4Gh~S!^3^0_<|MV zYXMjjPaHS-lg>ik(VDqsitwb_ZM&TFOhryFwCuO5GSL_1E~t zi_i>l^*8g5AOH2W*B$|@5&Ml#LTnpPLc{&NQ`6?kAzO@*oY-@L_m0mG*aANA67eJW z3OBZt=%1%7al9dz2&-U4Ys>E_b_k`EqZ&yVuj=X?W?S22Tq$ z1N(KvMO?@^IQY=1Q(+LdKZPv4&UC%cG`jA-9dJEy$n)U2GAU`g=xSE)sw)Poxr&Z} z)uNQl%*nKJqI)PX4In9?-sO)!qtBda#6P=gZ{h(fI6#CaEa3i6#LaVur6;>Xo(+>^ z-P`>?Q+^ja*Ue;?`cWbI2wNU0vU9`2sItIP)GL`NG4thCHgO$S6GAXzFUqlU0)m8= zWi9F#j?d?q3iM{yb%kmlm3wR8@Bn(C9$zF0y%=7d+YoB+e)TV&@%E6pI~?diyKQb} ziI-Z?6F{AQ`vxD+y1|g0Q1hKnnOWWW)q`volkf8`WP0tJ-5lFmQ%;SHXMNGg(gX`! z*RU;ozkk*Xq`948`SA2<%hGeJC!T_U3)a(xAYt? zza8py&NscQ}5VGf3qB-Rn;`ZAg3VS?ZA2_*%Q0Mq3#s$bvWcCPmJemmE z$K_+z(z7Xcjky6!+jLVIyd_wgq_j}x8*IAb#{fg3T{%82J~TodcFX7u16oXNBi<)I zL>S;NKx31uk3e-nLIa~T4NxZmcm`}#0O8VGOanfk#tEMb{RI302%Ik*sEDa*iY-2u zD^zlUUt>`>OD%3_PgHU{w?iFjwwmLzW)+V@mxHg~0;$QHs6ixh+xR>CLq%b4n?SIy z!zwg*$glcNF3-BJs4zG1_VV2OgoVria^>YQg+)Sj0gFup31(hiURDkxE1RlmPzx(8 zDCr9crEv+lukYD1tEjd3+j*i2Gcm2yC974dBjysfoR?Hf8kMS`P3Ke=Hi#@8Duk%%x$-|3ySPfbCIh^pkdk~IeM&T3BKyg7!#-@O{eD!g>^9@u|beoF2#udgD`JHX4=#CLWfw zpjM2=lyVtSqxkTW#P8%T{6E-@C7gl7hXXeT&z}$8_{*KXJE0`m&2Nw!*k>cd;ZRqCOyVTwSXs;<`v{HbE#v~ zr+cP(`ufP*p+uY57V=>y26{s`k=98J`$wQ!$5zYQ)utk4z!kRmDmraU@Y-0i0#?-s=sZRe zVr{9(LsQ4hNKYQZaZR9KCbsi2>C?_#3f2T^9LxtetRbd<7R0jT4@xT#q=#VPD%PMF zpG6S=l^`Gf9_%uZ{mAD`Z6gGF>ESLQ%?MIl+i0yC%jY%Mm9&q|-LWfz&Ouq7dK$Gl z@%nOGzLw{dD~xDS-{X(piFTg5@4lt2)SLEoEkTyD>fsghxgn*(5`_4hm8UqHFSW@F z?#={AA9Yx}uO$=AyUlr9@p`R9V8aN7!X%Y93le zZPESY^VtJ|>EL=XA&+(mY@KQ8juY5_+%x7zT7vsQjiqjSe_(^sjZZwigu3xhfCFf=0#Z%z(^Ttr5G7z&t{Opv?5V#;H(v{JD} zRNzjk#|9+qb#tWo(_E(T-|x&no|kR&O~{>~7G>oP7n!x9jzI6`HCYvhf;rCE4U8;b z7UR)}@;FYD)gRB#gAp3+oNOkk?IW~yd(a+qu%m?vPLMBcG%IX&2RuVwfK-hRT{(N! zT@)zL$r8n9&QQlD@_jm^Q|_@loRNtuY7Kdn(HghKVw>btTR0-Z)g}?z9dWBG*UI4L z*>ZFIrq))Ib!vH{t$0?ghHDq7MfF1$F9L7$(($hj&?>1eo@%HlS~O*ll5+Y}dzxv=3~j>GNJx|!XbD{eFjL@D!Dj3SmYPV%jCpHfzve8 zE-F%4{i`pNB~1*g#_N6j+_{jeKOm5r6ucrB%EixD=6DrymdZ-)R~sxUtu^AV*Z8%i zmL7$pZjwNoCm50&f>Ik-Bs00Tw}^}Qk_6gv=FDRvv0ZWZ-3z6fNJJyOiEHJQIE%4< z;Ly3JGmO+sg{yIy-`C>m&GraSb@SbAg)Vj-hIe$$ozuDB4f_b1Q_N zLYY>mvwM@Sgu9%$!SDC^_MJH4_rhiK!S(sEV--m-n3g04%VA#%A@2VS*pJRM4&`b^ zRTVw+3}dq(cZYKR3ylvkUJ%;!4TR{;q^U>J)?P4k>>iUmOcg zXYIh&p?8*X(`@RFxmk13xPCGZEXX-?h#$!xTeKd_pw{WA^13YD)ZV#seKUM>=8$*J zAx>5s-L3g`O2N5vLF!pqt3~&;Ow9Q9zCmMrGxgD&do~|)7)-J#@iR-vh!7&ym51n# zXa7ERYSq1!!j2~Xv;qEj7X~>@m{2i1{3rk4-TH_?{*J$^>rw4eRUqo(3Q+%l7OR36 zLZO^9X8>Y&yfL8EO~o|D1uWa6;JbV2IadE~G&Oh3LhyFdL`2gNY#Yu2V3|5XMmP`- zY|e-0@vhKRl-`|83V^04khyq=;R}6d!-K0~CNNR3Z3=cI#6=<}!|yeky}7x=3UAg| zAB#Tt;1XQllRv0Th2Af|xNi%({aUJY9$~+hdK;#9Z8>@^*)Z>^d+Ce1JdZed_quC_ zIXk9I7>hn=o78!T(pR;OYYmQL7^%MIsXRepf}f8V)Hi;ejs6%m8x+vo`I|pm;kxg{ ziMl}(=7;Yj2ANYRId$kZN zGShPF-OOo9CK#O9YB?7@d~yxwlQc25i33eO=N&ENP`AN zVsYw~@S0ICP0`|2t%-qkV)z|=2jhJv1S{fC7no&XU_34!p>j> zCef#9osQ&*@o~XPi7*y1h+5fFFV`#$diY{XQM+)W)QFghd?S%f>?{;`QRJ~UR;~rN zA82Gr{Y6HuvnZrznfa526GaAnt|@O1TW%v4dY;@3eMt7)j4$HgLGrKN#6D@EIa@&; z6tfNal5eF2h8$rYb!(0&&!oz!&So0e3^XN+X~<_hQd|3~aje=UW>ZrZW+4$&BfUjh zyPrSw*5SQ;KtF8Q!YZdvW90q@!tT30uWrY13-fa;(=eJa<<4N(5RUJRT zzMfj-vqb@Y$EjHs49|ZszFjv>Tcf6ki8~ao4Q{-~Ea*Fi``i^kb$JRo1OEH}S_5sV z(Q*^a128A(<%$u5&Z1}?! zx~d5JnGW)&Lx(v+DsQ0jw(|#vcx%ywTY1Nc6Z|viyZBA4>#6nd#w98wsaYLM26nf^ zmYGW;R&A3gmXI#iNhL^47DMx!h8R-EYnsko&mqC0dn;zT{|Y*NIMH4=29_D z2+*a%!HAY$|Kq&U<_}n+uvjr|ho%{ev7j)w=j$ZnnWINT$BwO1aDx%~l?!O$6u)Jf z#b;`81nO9|3R&f#C^S{~;2Zv$Jf=mWQzkuk3>=>w3CeSdvP%oTKuf`I+WtvypR@46 z0pgcPY)RA;-BtiOa-S_CRU+>8Me*>S)@W%nn`w}nN-C4nf5jI~k4cCroP+iG0IQ2} z%yhqb2si%mz_9TKM*?fWi+}^e9wkkmAX;!l8#)XNHT?f_r(il&4*p%*jKtV|!UK(O zEIfl-A2-`e3#uesZ>?lp@VfnCYrZhwTq%vQzeIZWgkpK~43FYJ`Ol*4!Y*pTXM*-> zd9qC=>}*!`w6Si#h$eA04u6Rk##%D3hc@KNRpZGcqVvEoicQr@|~z@m;W z#ci@63m&KYn00_m9|UiF1w>lL;AYwM$ojN-keXNzL;%}?nK$z_1^1_yPPYeyjmS75 zkV5$bQ(q+#kjW$L0NaW2RbUa*I1kzwW^4{rO4Ws->Lyl`vxKqsA=zVF^YSCh+ePD7 z`$Wo)N@-Vb@gb2!9UC-+Cfk_UUdg#`^lACVJnifG*F{I7FPWn}XvYe6Sk)nHUHV zn;!Ohgd|5cjY-Q)a2r!QI%36v-+{9UIH-z*w9)IwhTo_^Nvq0~bD93-^#8VK!<~2f zhlf{Emr=WYs($!&wo$5NmK0Zc64YyGO<~>)MbTXhFPHFUyID&$j+*?iSO5jzLlNq^ zC!a+5n?h$)KE=@K)1me2(c$r%70Djx2GmibZR)i@_I&UyV(QdDM@L}l8%$BHF_fDoc>tB^-3B{W)C06yG#W*L-SxVEgt!clM;kfts4Y;z@IA z1BNEon{T=6tv+jOED#REpb^{y@;Sz7h#7JrOCL+m{!V4OX)%j44w#|AcF6t%&-+I< z0W&R+24 z-30$4n~3pl_biXzUNqhFWaJe405!{RkIGd4+1Z;6-!U$-RX;UeAC4*)7zO!H&Z3xA z9#56K8ZtWHRew=lUcRslE}$#CAJ0H7Hm>MPWv6C|M9FdT)V<0MVw<4V80@4Fs9-+z+_Zaj7{J+GLMx8{o zoiDe`G(`%Dr(ETkq}X`v(9mRbwsXabJ=+x3E^*Rk(NcGsz2-7tSQsd(!RMHK>LrAbx91m)Lcmk{HrN$8A^dJ-k*I}uw8fzY)fR9<(ZcyAqXNm0y; z4djVD-QBhZTZ`5f)Z`T~+bb%x`XYthXlqc)IO@qZrbXS^2`^~mvGN7Z{D4~QnU<(j zYo#8)t>MMcN!Cr&e7`+ZtVPoGD+A#pZ4uctg?f6#DP@PFE6a7%G*s5(@mM^bF1Pyy zfk)yh_Vact-Bb{b3zaS;EzrholXj3-;wZ)5E=vHB7l((n953LSVqhNkO;ruh8!GDV^jc2ZAEnZ{jpW>aX-~b zWcj;2>K1-&$X?%&b<+V9l&U>?UtyHPe|0fJ*SL_1S`a{!V06+f)Cr$gLf23Qb&9$P zC9L|Y4Mx}ey3JMYiFYqgJXe!2RvxP_s7)lK_5HX`l>xo_aDJf?I?hyrUphvgIV3FK ze*gC1&6No85X60Oc<2$x?{b6;$5o?Vwc9n9c8B)IrY)#rI^%X^0XB)r&Y0Io6HuTCCtd89#GXN{kn;I z>dq3GOfOQk#fpf1xw*YQz zC(v3lZ6u6};tP>E*Eq+X3<+D}zA3GY^XJ1_Mc5cAO0p#{%tjj!`Oafh{F!&&Njwb0 zlM>*d7~x*w^;=i_TS}|fm%FFmzOww8F0Xv-*%pqszU-NDRGQphTDm{E7aGc=71XsA zi3c9Qbe9b}3*+zb{EJNY&8z1J$9L7j#wQ0it)IK#0^ldriy?y$_5~Th zXjqM7<%Cw)YD|Of@r_xB4+k|8-&lE^DSds(x^*bGsgf8&5DczfA9EO|pq%xK^fjHq z9HJAQg!vE>HIE<~sJ9>kmcA8xbe~^ZdTE-)yzdapIWBpr)ax4G9u-w8bUKdS z`M9iit3RxYwS{Fw3HjbB6`!wemZ66R29C`*HURV*qp?iwa9+5>7uF9TSwLR@zn>Na zOlZ=@n~=(w!QNo|CVhJNd5A2RAQ{2#wK=$DC6-y{*2Zk`z2qvF)?b+52wq(TI7D8b zdD&mT{yO&k_kUSr?ECDgr(%~bOt8^aUkHd~sOO(>|G+?)s(SbwYZfi&_V)oN@p2kAX9<+FJoGuF;l4kH|zn99b|Ti55_2ZF*bq7j#05# z-FU;CVK4PAqwJslo4h4~y}{K~jCgYA8j3r1EE?`^i%HERYO3T#>6Y3z98vCN|-MMfCnzJMS%GH@MuS8x_W20Q_OCM!Es zJ=GPIMq?!tDid4!JR1+FY)uX=LRsWRrRkxEV**=As6)I5&Hfl%1jbrt(j^Lb-*TG6 zkubWCTEA+G`L=5)(xxaj8z-C4T`Q+jzDl%LUlwS4c_1DqZ$d2~CxTF)azBF&{WkSb zmx7Tv^n4PU*zlYmctb!n|Kn}q01~|8l=8;u1BPy&W!L1+TdhYXS5KCprxlZ`sCtd= zwhIxJO66^r>_Bb(lBQUqN%BBQ>(>RS#Y>mYoEn)vXZ~Pla9-c9#}bvNew>EBUp0Y> zR-kJ_wz8HdXC@Mb``(>6`R%=~m2HyFA?Qn;KHo~f*&JiwDPp)A8qUdreVhrt`%+bQ zsdgzK+5ovAG^RTZ)7u*jxKx!fKNu{|S=|psk@w0o==Hy8oL8uy;x=n>e1%x~$}8xl zibb(dsXS>3OkIhJuKbjV9CQP14CXKyqfeVY?9p9sGcU%Y#L@7q-nAip#l|W1wz6p(A_o7y71J*&%;)cj zLZ6R$j_aYz)Yj{CJ0^GCqCY%gyfb93t+GPzBq|5$l7EWU_nN$}{>BzpW$Sdi-#XM2 zMLC00sc1R6P80HSY z*2~z!>FveU_aG&KIPg0ji966P3%%Jm@}H74^k$!F4UZib`P}mh=BZWe4?gk=oyF#I zk9Mkt+J&Af-f9+;Yf0yXP} z#Rf4~kSAig;uB4Z4mEkd*m&wk-O|A(&Y0F1gk`@H{4a=GM^TyF2Z_vQA^ z-ka=%3?PKC!roi<9wG=LpyEQbDr#LQZN*VXZEIV%wXLnSb+p#6U+cYo&-=e51m!zS za+%)yywCVOo3~^xE~`L8icqlmy#>Hc_aKs51Kvr3T;9##LF|AJ)Q61(v%{TN-v5o#_zT=)1I&C*MCiN+^Z z$<{qxgAbuy3AB^JixkD*K3HlGIBzP(|Coa>Ci=`J)|tsq{#-oEUSb}MM|*og&lN#k z$7x3A_)b?6W1hyN6|NV0Ex@}@>uS2znS>uf<&i3NzyTn$#EId6T^O23QWG<{NVk^`dy;fO+}?g<|!vdD%+t8^4A9%?stKusXzz zvbkvbvkdCp)DngI%so-DMjo#c>_-Fdf>hRyoi2{Y5BIhotqRxOIkyz{I6vE0E~(tNZY+xZ}RKMkVw#iZd22q-2*anWIta z>1?|^F0$mjksy2I7th3rO0+6usV{wTEOkGsWZ~WLda{65e*qYw8hXDRw8bt4to;A( z4k9e^Lt=t%PuoER4qZBLMq5Oqi6J{6X$osF_RXy^>1P~nm3BSx%Qgl6wtEZDs${P96S+pnt8hN5Ck(oj_vk0#uG0k182 zcQ+4)TedB5Aokb{94bNkBi{P>_4h$9qA z3cx#yz8qqghATpKbv3wCtUY-?A{I-{)A{RB=tbZr9y_r6nnzZx3RHFD2Z=)_mlp&$ zaV~WtwTesq8-JX@%LG*0;vpQXM58WKYsDKgfwAtT!9eCTAN28&JZdgX4EK!NtRL*a z9S^({(*>I5I*m$OV#q1ex>iKYFV(6t#HB`~uSbDy9`KgTDj2fK-+VKXdPJ-_bR?3e zHFWt_AQvhU#d5+Kxmk6ocY1pnkyYKP9?IklM(TdC=*?s*6?GKLOz6|(OE1+d>%pa^ zXtgxr-T!Gx3071clZe@}{L5PqSyDrC2G0O5DnKsCR1A7U4avL$q?tO}5=<{Bqw(4# zVY(^_65l@*noRf-sbiuGI~ey-`I&ioxy&Up^PW7N^Pz&BkDk2^F|Rz$QRa*DpxHEo ziN1XwHEfpI@A*+kC6l{aWz-P<@gv#Pz4-0bt4}NYI8Kc%&~-Z1QZ^RIk>^tX!5=hD zPu^6mlL};{s* zzXVZUq#l$BXaZ_Pf%N0pfxZ*?3i$yLGC{XN}(UE7YMRcNon8)i~Na z0e(Br2!JLY-_4#b00rvPu+C4<+aUn}Kkc(XRUAwjp2T&;Pa&|07XS168xu-*A~qu| zYq37c^@scc59jyy3G1F5fXP?Cxj&{=OZ@HpJ*acPz<$^LumR~eZ`pKFh4!C_?^T4Y z!p8~@o!{PKXjpU0KBN){N=3%p6PQfa!EhN^c zTuovmKqHmb5>Cu(N=CEM*itG)Enfx2jsvNihlfiT)4#n~TFPL?<6|Q@R*E)zys^gD z=jNwIspF{+OZ&sK{MOi>8F`6tPggV{FeUdbpkIF$=-{V;?$v-pd|++rp_9845bBhG z866BA_YnO7sCEWM0T!ZTG4O#rgQMKYaTJlH3@A48ecB;ApGom7MxD7N) zDlT+@;#We-`dUXxg^D4t@|D^wg<_!tzpXNyJr7Y0k++ez8AV2nMgC5-ECQW$Nq%Z* z!GZ_p+U$k42qP1BHu(dFdi)`L^?=_53wKDIitOR(fLyr^bhn@Gv%4-n5lF)s zIzIJ7`2q)o41NZ&BQ-C@BQqv=<8^wJsR;vZBAQ>1I8Ec zD&uP$;gM3i>OY|HDr(UVgTKD=>0uak=JvK1q@F_6H}8nvc3W^^ZvT#GTN|i9IA<8@ zrtSk=tfOZ?kQvYEd5F-WL{~yWECBL<6MQf(eg>!*Ad&`kk0R*(OFIKZwS%&`4wNzg zkfND>M;@xnU}Bg?YBRkh2nO+wkKKf-c5g=rKSa$H^cW4|A!TX>V@B#T zwGDj}!;W}GxAcJ_Fk75Jc~X zR|#q{Z3iaAl{=9Z0QZP^nuHOE$TH5QzWpqpm&g<2GKKNp2O|QR)Y2f@f`Vu?legUM zRW7yE*k(B$j@gFa9Mm4@>piJE)md%}8^cLm&{T)f^5xW(WU|PgZ)(h0Gm3lL(6HH2 zTl0&#$z*4i)mbA4k7OkHZMi$z)BRaGm z{LCnwC7m{2bx$Bh8fFlV9DjrFk$o?u)|Ge}kU=7xJD$h*o|BQb9b|kXYKlGo{MoY) zKOB4Df!l7o=N?qpj21K4nGCVdHI(0})pHmbIn-H3wld-mc>VJ1pKEpK%Mx5xTs9={ zSnK`|V=#3Rwu6?Tg?0YsKyzUFba>b7N190RO33QqMRVnH+diA2W&S|K;%}Kzjr+_x z{BNi8XZ~Q=-5U&rzF0`GqYv!G{gB7tf%Q&Dw*rBF3jmAWz(5e-PGkIWdeD05 ze%uMG@rs=e&L1tT1jmWj-X4`pOuh!;W>m9BuDq49oO~F!KYhzB$&;z@%$d~H;`@&N zXiuBEY0DkEzU&i7b#qh0b!e4WpJ;l1UUBip>BsN~#l=Wl{PN2XF^4Ds!Ba0X4gmk< zjbmLB55CS8BQSV`!2q|XRVIC9>8HSGaBZMUzi-YBMA5)+Fj`}Lk8>uw2Lc-Sw?e-~ zEenT>U~F8Ibi;;9WnScXG~ZRS47Z8&`}c-K60WX{JBGw)F8Fih(;0zOuFWlov*mJh zyIooCE3>vh*6hT(*@2R~S{adjoXkXyiJ#zbPext?&d;bybYCY zmYB}M9>$-cSn4cE3hqLSWqdzx+LlGy;x&fp@2asq<->A>i78S z)u`21;LqcjVs@odV0XQMDR(r_$Kq6JJhgJ?V^ZU}+oO6V$5AKPh8l;5e|aGuzmT93 zxTfX71+1a+nsw^ZuF6)~hQyuy&o5whRBWq~-#Vik4J{ic&i|S!M_26&b0Et6AWGsZ zz@Z745pp-+XExyf^rQgN=W)#<5amxS8)3jnCjhWA(Sbzh$j9-%XOik=+>LpE*5$;g z>?|cSH5>KrRq5}%FDf?(-0cGDLEMM#O4UFD@d@$_-xf!=$MJhekKdzynM}6v#aX=k z3O>XJHud7lTC~pNX-i&R@N5)YP-!%R)ufx_bAARuxdgUej=;@e5%{4X%*C6v^ znejBmis9#Ce+RYqocd0gLcVdW3}%pJF-j;#X7*b})KE0K9tb#+ottD@a#A2&v z76B(_;BQe0ejEs1ARguN!mZYrrmm|>;*E-nv^VLNtVsJ1#FtBu^GP>L5Wh)e^7m;9rNV9!MP$ZMVQu*d)YTE*QxZC9_ zOsH*bXr78J`tH+D-}LYWvd{Z37NI|Sb4l!C_|&QJ#n{5*1-I~Rv*uANB9ZkyxVjXr zbvf%!zMi^>%ILd{fgQU5wg1G9`6jG3+!Q$;QT}=&K~R`LxU*FGypQGye>J{`u<8j4o6-GbeiJpQ#ar@z|{)u}thP6YoONZK2RQiLleO zg>q(6xeB>QuZT3rCHnr{!dzhiqEgks0G`xdILLxrDCCzFL~;t2G_0J7D~i!Fr@gM> z(juri{c|fGnH~$|Soo`Acho3V`FacTFVk_|V$%`{r%++w-;2Oe&72m68YVKMFAhEA z#OHwNlyF88UKmf127xb$=O6;01q=)F_PvEx;|L4{HN8_~Durm#^6WOzjn(uwRlVEzR&V^-`5)`7hQb z?sKl|v~jgd6V}QL-HV?neEYCZT~w!TK%Xz~!@elm6m-^|cn4xH4#dTu$8N)?&~zdb zX`k%waUaA5HM@26b|^v%Vl1F2PY>B)fH6c_ynbS_!m8HZm^J=5m`|y4yq$B?~JZE+b18A@@B}o*}_B1 zhj4v4S{FChMqYrCHmOHY4F9aymT9gf_xxVvCsPUbAZQlUb>h@W+fTG6Wn z9UZ5A6fqqioS5PQQuF_8qXYd0Hs=O0K~nxdwWyEq1NAf8(rW#drl@te$=E$=|Mg!l zqvv1zDm6>3I(}PJB9?;>wi$*0XY$kaUF_+T9Y7s(7RH@nL&y4F{6%qOMiC4AGIkU_7VC_3MmwSno>IFr>Y-{k4&sth zv^!8-T=p)xuQR9uKTZ03W8ZxUx~h`4nI`ZXeSm+a!&HGGus_$rX25Vif&w?vv?H+#1m+ zg{~U7r}8hkE^ZOm#ra0z^C?XbD_fP?F*t}WbIcMqOpCqMM^PDe-%JN!QFUA+K?}ux zzgvoH_&Rn`u7OAK=fBu<+#YouX?tw(JL_kVyxGR6xis;!wXv98(x{TE>jkfu_R73X zQG64sXjC_crKUNGy{!DWIyY#yct5&o5K9d|D!sSxj+(#twkL9<`~9?jPr}|Dva?PE zx1XLYab5mzat<%>XP`RB0Nv7=QP>=sWM=W|`sa9WjwoWUCRaM}YyiuTK=p%C|eLax?19EsSX_YKK(n zt{0~6m^&A(d~|OpzNQLLe7Jg5A`-r529hP#txH%}%!V#`ut1)w`d8{PmY>9gWhn^UE!FsV>&tNYNxgO z=pSOSVCC9~t$!srHBcbSMHg|q>z&cj&^)Nlp>4=Z5Z@v5%gK}$Vqu}P5(XQjUpGut zAg}$JXYzeCMTf3v*93k6JZa#=?<3*yNCG+kG-6ob`;4O_i`~ao7hdcbYS9T+-puE0 zd-@N5c>M8?KaPC;^^1Q0H*67V>=jyMWh=hD6&dEL62;0_CvIK2*LYuv+E`xQoRS2laS7zNGU-OIDNxieaO)8^0>$`^`Es{*Ct8{scVEp>Y;nRWj+j(9>_>cmi+XjgAR61rjs> zJz-1%9F>uGGWCbMcJDq!F;5))_CXX`snH)h5tqsN<`yyH?io37U|Ml0;fAIzAl1iE%UxB+Kv*{+L(j{r%88W^< zcty~-Ot;^Y*OE!_6499i^AQ0Aq?q=jA)4bS2b`=wE<|^b)wcKoGL1-0M~(&|r!wD2 z%^>T+QfFq&pgzcCc;c~;GwxoXR_bqp4m^uY%!2O1`FGrLTju+U!aSH$cLWs{7cZL) zMM-FD)Y^LQ^YQ-v;Ls2jqM9!@gsfKRuRAR+l?3GVqJF)AB~`|J&x}Q%3ddWb*}Ov3 z3Vi7U+1n3&Rq359#HvG%6LQ;t@y7#EbV}(4=ccUzlQ1n95isikwfI1llpW9)%9ikB(tC> zFK^v%pJe2v)K(ielq-`_H-GzAsM$&^>&E4!=q8WPcl`I6_AYU|>lcxQ30}N6g3pl! zyoOi9YCnv?2p=VSCEaJ!0ME$29ir{Y2EB3L3?}>k&@6(hHrW&h;SdeLJ37=cv1Ep% zW&jUUufL6RKaxf8ESd4-skqQ2^VW$spc3@KgN|rzZkb+u5#1WcA0uVzB$&N5sxG_R z(WXEdMKe5$xoirONqxM1%ZG{jSYwOTSHHLfDtBjm8L%Yy>i3Z-dGa5q`2EBtui3jc z!l*o^e@M!MQxws&l)J#oB$4om3DWfBXqenYgrd+(;ydsnCl-GI2D^b7yAc9o-2b2G zmtS7pl<&{;iIUC@Vwmt56mR%Bo-MW9dM>Or3jM9pov7@S{VyFk^3wk3k<`wSk%y9x zQ_cOd_C^Ei<^&=nJ~Bwbg}XlqS<+X>;7 zkK#>D-+qA->(m+;+sM&Ntkoi_`6GCw9)zuBtsi}a7kgH_4UP?7@#$0ezmW_*Hs2K+XH^%0Y*E- zE+%e<@Yxh8g3Ez_;1^0BkzIu#PWj*Mb6pCndm#0j|g+h z*o7ZlPMtv2yOc)A$S4%b(ITjSxk3UdD4E(1TNqr=lE`h2RJNR$uP>2h4(f{A*k&KT z7wI6V(TA%m(0Z>Wdi;}KXcFp%)w-p?Uops?Jx=HU2{N9rQ!uk(s$QF_)43DSQ%hrdkJ02q{yV~czqnco zW?rPz+p81ZQ5J`TDmO+?z5!{%rMRLJt##Nc8eSg4-^k*)-|Itn)`?*lXvrUEpPDm= z*e0@n>BmrDN&5cNJ8>&O`;+aYP$fF)5|3+gP>%Er9P(~F-yXaH5y+)ZrrW=dPVQpl zXJs43;(eml@ zS+|mOlFyq?97C|naE&FN~Qob z{twOzD@oHrOpZNq6FQ8-MgFRfjRKR}i5ZF*vT z`u<|D7v%!%OeZm%BD#`gVuRwi*F%6*dQS*M5PxU|AV4!|1>h*xJ^`IW3SP*VJQBnq z9r4t8=k6v&(;io(8>#X8o-$4EO>TT#p*waWkf&7JD+L=+0zC;Wjx9O$1_^HaRAULU zY%rtM8caJ_ih=&6=i6tA`wLyPodb1DMGYE%H(W6tV)U$}61Kw>KJ*P7plctYYjER^O z1Xq4|WV$$EbD~PFhgmJjXe+w~);BTzt2&h(+alQo?xCb=!8~Q@fb7ISslp)XCmFcytMF%w)OsTU=Awhr)>?BiRa#(A35B$D_X?M471gX#Z2JTjpIsn@1| za3Wq62u}GzI=4=nCkSoP`Bdr?!$7gLX|Xk`TiBLYvd(fy{uxt!@@P9 zX>IedpueAbDA1%0Ip)-tjrzRC>AtYDI5%Mk%a+b2VULYP-rCBShm%R%T*&wKt#=0X z1NC`gmmy@3clM0+W_oq#brb1*xyj_Z%J01$-NaS%42P$c&pbmXFcV zc|h{l1x08d60{ZVEnZ`K;Gnc4O=A%Ohz7K@f*D7@Luc^N3o7G0wHW_ND2YCmiC3GYVW3{0LFc=(YIAnT&$C{7_s<39RjG`h30HnzYyqiVIg-s`v4x$&*>?j}>xv!teM zaUeGLeB#WRM48(!H^;x5hKkdAEDl)rA?PtK0!=`=YDt&N80?Lh>^_+APoUWR+F0z# z1SX$JqSRo1vX~4gn;;Mc&P<#!vKbB9m!^^oZWTyIG<$l0IP_%_?M{?Oy6C{f=;~Co zMH4d=EWN6Xoz(C)p+dFFt5g>vo2`+?!t0vbS61-Dfqeck8h^hA7)LxQ7aGVBM<7Q$jUZBD zw+TsTyh_Ue`HBH-0U-=&hnb>~;WHl1T_%z$CVmKLAy7bGbc$kqdl#}T6Ps>19+Jof zogT!3(C9;?e-b}FFz^BT8`oi}$Vtj&Iryfb8gmveM~~Fof}74rit_Ag%OLe(IIM}q z7LL$KL$|YG_xpc-{&`;)&PAm#qW`D(PP3I8WI+|11g}7wN8~xOeDA&Y-|yEpN=3mK z9V2~_u4xU@ax*bMXD09yU>T5S;-^j;dva`Y0Qi^A-hm~gg=LET-!S)Mpj88mvLbu~ z){_h?qOGX_de$#|d)4o^hURC_)Qj+iP)W&EoI15?)ykFO6QSL^SFE@>)uPZIzBwq8 zh|Cp|b;yH8$RL~4yZ!y>nFFZ(K==Ushkjo5jo=l;jM-T=n@~qKFs-cqI0PI`tK@EmW%y9gcDI#-R|JhK_)rp}>E@KW%SE+Bo`M z47~FJe2&qy__38M9~;^ki|uO1mBk2#NQI7mj&Fx5s^z#7l9L9w*30hr@#CNbSdfc+ zlCh1^44T^r-n$=s5`x6(yPF1Fd2KuFxCzXd@pLe_?lKECnM)3 z#uEHZ@(Hae(#C)=yQw*;Evr<;SOSTlvcM><(V>Czn6@}Bjk6vw#s*)=cjdTp9eJB= zd~d#yjXMjRxgL%yhuV-E&vS9YIVn9u8Bg_oDz~A}<2V_so~~%!X5UN=1Nyy!lT^H1 z<6>E(%K zeZnPzQJzvXOMqDq-9NURXLI0GQPJg)IbB}>H$`iVHl6^VsA{geL%|z>`yPuAELb-B9;Sw$Sy4;eZD}KKXWh|SCD_hV`i@W&{ zD87~R=VKi((sO2IlbSE=Q1-ajxN>^8hG&%LC1;&1+nkjzXV110+GGK3t}+fY!XOV( zdMa=SAS%Gg*YG|}R)!{&DZ0+j1P%$(3EuHJ6{K4mR0x;~V5943MxYpo4$%nkCQ8$X zF~jEwmgg3075AEqbCn#EKFRV+#aX`Ed=qb}^CIHE{q|pwI(U}RD7ENn@d=S~)lNT` zD{uv&K!}abdcCP9+uD93j+;3i9wYoM4#MaprN&?yQ?Ii)c=23`eAHjdadQ_GJrElR z4@B@AvBwz0Hf^X6eY>(3N6OH!R^Hh7!2D>mAoYR^HHI`jDrvQp)|DSa9gT?A4pFC3CSD10OdV6d$KBmrLof#BRkc&6G5Fr1(P4YUOX9ZCWa|2b*rLCdRp+R?*x58cKLwlmpZEd)+4(-smDl2}p zATc!5yt-uioOfpI8OEp*e=-&bjD?$EYcAtw$gXY$7U$tk?wVUDGWz+5q|xM~eY(#G57|IbuB+_da;&)byl6 zGh*UMjcPtcxe&$R8gvo6e)rOW_QSE{5m%?Co8J^bL#G+LFk!wMWIB* zxJ;@!^8T#jBO}LW!TbH)sVlv`)T2v3zS-tQLn?3kpV4S8$D4yM3URJM;bQoaYu% z+f$;B4l20%Tu#^FE4QrK~)|td%uL71L;ZB zjzC9T>Lk&cE`bxiY>5}@y1X-n$^3%r{~A) zZ(I6I1LnESJ4NpDAkG8CpWEA8)Z-lUx1vaMaL$}yGrrpDN000%<2x2R=PjRsVLLJeI9X_Zg9ZzjX2)pEE061MiJSPOvfKd9iJWSy#l%?njzvg52h-^ zRE4xSfyS8%;K4?bG<_xsz0q2TFb6@OlZ-<137-iOp2?q}odO!!6P*PHFbx4FEI$*h zK6OCn(3m8jumyq4TD9~h%t1qV6y1(bpn*-U8l|H~H!Kdu*n4kb)NuH)Vk^&4`07GL zp>J%JC0(3_Rn;TH-b5eVR^F@3mHl6L=v`@F>V-6VPzUv=wwU&-IoJIECyE1=<2T zeXqn5R2>1nSVTHP78c>>G0!YmE`yk~gkkZ11L*lik#rRA5S zITbz09WS+wj-WG}%g@i#V>hTdCg9NLX$+M=o|&FIZNs!Jz?T5?!3QT22(e!%siol* zXnfMp4|D}TtX#LHPqDzGqQ!@*uxMe>H`=?VXJ^C_^qw$I^Y4<8i zCVBx!85vGzLB2GDm(R(Uu<$RW4Lpeg$)uWr+7+F_a%Zm3(i*Y`YK;ji$KWUrY}jyk zjoSmo)CFa~TDHt!{VH7Q@fZ}payV|Y>HYe0+Y^zkf+A#Y_j&b+#E5~E#|>Wp^GM#N$YO<_7p-h z1nTwMe8NJpFdZsci;fzJOWGoBp2pSnFrf@ zha7X~t~wqtw*H};S>GGo{!+*2OzQP$bj2X9EwpZM<(7YMX)oAnl zh4Rw-Vbj&)sO+Zp_M$FQI6`I&JPEeA1|oy4(}t&wg3M2>froQ~*%XS14FC$!CX`lw zU>AT8AmBnTnqsNxLTRE;3C|J^Hxfe)TL2gns)8tpH0Yzls1L9fC%U9Uw>M%!K`NJ% z+?Q*wF+{k9pW$yM8ssy(3@ux>Fgf_!d95r4#=C|L5l??K1guG{r;$ z8YTMu>3lh<0`_YR>Bu9sX%u#5>Y_=`5YJT@XE9)GM++l+0NJ)uS<1|U0!|L1Yz&4_ zYnO}p5u3)J;PmHt*kKl9HDV1gSgTUiQq92wL79--YF>hbe`w6wB;kDr9gnsMdMu1D z6ZaaGQofQ|B@SOJy*Sff<@Yx^d@OF8l$|dYYjaAia-}|GEH}aeP4Be@ngc8Qab+di z5;QdLc`F*d*cXE3ncVz^FjEZ5(Js>KGk-e1I+MoSU z2C@!!hUP((b;<1}K3lMgszS?HFP}=zalHn;GXLMQAh_91Y>WF|*O#YtOdsFAXJk z-MM)Fu+X~YXKkY}mF>T?EAkApbw9SumveqZ@HJ~%2y$8XF`Q7}-%QtIUz`05I&iWs z8<-KQ_z10NionXS@k(W|Ou)dj7Xmbz%#A>BZbFbJb1 z8_Qx6pb4Kb#v*wPvBv?O%^FoM;A{pSW%>LbkDE&sc^w^@%Tat*_8;HPG-qZSZppP} zxs*(9rlXfn$3{wdfz;2B1hhO+uWJob&sXXW9z|X@zJlL{QX>NcO3mI_`u^p`+BH|>85in!^G~;d1`8bPGaU0Uy}4g{)w>n{a=gfcdG+j8v&kJFkTEcdQ@ z=!?(){qH~iG4z4e`T@S{kYC0ZGUGJGMSn3Q4wn zndz~u&Q_N$*gdD~o~HJl-u&PDBJLP00b1-{gp2FZ0gt?D$sZ$;6*E7Ixg!w6A+rux zkUx2kaSzmKkoi9rx?VQjNdyaW(>8c~+9!w{oa{OQPAk03|7Wx(ao#3;6maH%To5Xy z&Zn+kkT3n4I5&~cWB+DZ>UVPBkBC^nHJ9?&APw63pk&=sAK=uJi2MHgSJ6+eevtC? z^q{8>UOfo^1rJiNj&#r$5(;42r7vew7p`hRTWspq*dx#eku#V|l@=A1daF?${%5ru zl@-Tgu|;8cz*(~)w?^jv_rf#MfkLw0O|(^*Kzc#Z)Nwp;)gW}B%E$$h-P?fWz{G-X zB8ZNB7vS|cpc4VAn*+$^l==Q z#;W^;54Yr?%ea&QF0qTrt;X9NUQbEf@3X{`Ejqc~eQTU+^#vwQXE2p!lQGU@iTD8479=toifov+H6 zN?k$Omo7cb)malN7yra@G;+&}FJ4X|MjU78_2@5g?2A=)i(3x4mFQLMqn;D%%4+y| zQnORZ%Xjj0DwzYeM5*LhN{QUJnagwmTaiTLuRdqi>4f+tuh|UodWBV4R$gHC!L$gI z$8_20)XH9WxenQkK6SJCM^)fogu%bKz)%4WDWWGkknUe-3(hpryMd?FLqhq1DM+e6 zL3XA@R%q?7F@@7%L0#6B!o{99V{QYYrctA@f`FQ&%o7 zWzJ(W_!eUoD=w3QPS2u7*<=5e$hPkbh-@admcH3MHJKl*z z7R)~fLxbVrD5f8&r#K<$$iSVJK+>I^$2wQNuy|tkqX~9DsyuSLTyI5HBTgq!C!ul@&%oa4KRcOeTg{%vQxMAtFMY+S?Zde9_j&Hj&_0**$NwVhsZ`}FJL{-ZV5&m2F>oOPyrtml;hfCfkK7QS}xp(Jgdd#L$8YU?^g_~8e}S0 zCm!jrs-(4Qd}&~SdJ(Tgd+4`IA+#<4WzJw&sQpE^`*`f>F3Ir04GpR}5AS@eW->@30(=Lm-r?A1$1nWuw=Ty- zY&r(BRxd()kn|W%ppp4xk4oAK1Ji>6odk8)JQkGYA77s|>7z_!dD5t5ywTUsBwlkG z=g~amM?|nDSOubLe%f5~nT*;HR)kN7Z^Jn!fW41yvSX}*KYWzMvWZPiMhQd9+R1kCYzz&zhEt$dsPJd|ehPJdDky_e?#$eW z`fg<~^bb9>43>E8u0XS`n$p@wmnRY%+VFxZN~~G6oup{lypZ<$t9YO4!u+iQR+C%V7zc$9eyv^_yyCp zPTK{voQO1POc1ak+&4Z-hqhvXrRk|_Fo*%-HT2jW+CYJ(F-;Y?aYqj%jYu1fbAXHl zh?8Iu1Rv8+vcP?uxSC0ylIB__`ao&-+vJGD3NW&&L@Zdj6TFzmHL2WoKZ`Bku<|K} zJChv|m<8DknaXU@s0!czP^sE;*e?{zJSD;n$iMQ5Czuy5;482gWnp7udwz*458_OS znXR?@DT$dA4P{V;X7g;PHOiA!YxoSjJ}3@|!c=bOiB?tPiCK57MWM1LVWGJ(bfmFy zn7?ySEHvx26`Vp#(U``VpKZ}uoW9{_?K_~`st#?k7%Lkdo*RiQ@g`sR5vZ);Ct-bt zxQWH)6t>QmWF%*4YVA3_5IVPckMB|l_}a43&ce23fE~i$y$d#=0=hql{i_?VYXaJl z0PeWeCs=z5l)iojYr5J+G#Q+j^l5;c5FHjVAnXm3(&5U+ok^km7DMf?d{aCe~!Q2-w^2O2{gcyIsO({MCcnSKgcs~wxxbuf|lw+RVP1= zMAq~b1>78~9l!4O3ZuUAEurnm8pq3E+$L!DG!|$F{cZ$$B%pec^zGBtr;ttq4gxyR zT$gq|NL-dlD|=`_1$74^OqvO=k>Z*rFn~NW1Mu@ArUBezFpo4cpw5k+lioZMn0How zVd3BY(^OTE70k=*ZbuA?UnX-l=q_6I46vRiBF3trl(>$0aV zm7!H~dtKFCb7HYAEwfgvcqwPOLcl$9^_cCrb{*_QfLb@w-}VN`Y6solM)nM}gXSBZ zws_haVjbZ8p{R+ zz#k{SPcl;U527pnzbT_xvX6YJ+a$J|Pxz!lp`{82#OM#_>V$FwN1ta&F+(LFGlMrSN2$~S1eGKYP0 zB44O-kDYIKRGgUguRvh`9EQwXr0kYe2R*T>=VG>E6ZAh;{C!i<780qK`qo7I-HIkX zliR>YY)=FhE2sOEE<@a=1-P!6_I=3iKDrZ_NDm=J5GV%krwZfbb2ZD*T&9>L- z5z+w$;6|Nc!ctwA6T6Whz%(mjdzCx8E}+>-y`hdZIc=FTH!M+zK!Wu%LlPgF5kJYh$}60`1p%Z z{sOdWWiU&gZDNYEr+rr^E(`dy|CUF+CA{Wfwc6qo<2zVlrb(5JKj`ZE1icqaooYDL z)8p;v*bNA?NL9B)=FQ=|@vzHS1kv?o*o+>XQT?VPp@E6Pj>_etJcmNl5!&8{%j?iC zowcR(F7W1EW%7F!x#fJ0AjZR6Wlpgv*zEDtM@>Sv%65Ok;tVT=#r(o<9^Q%So`2i# zzneS&S&Q_f#$JT>z{Wt++ypx%4oy2g?KHU)Xw##w1c-V47UZelfGx;V(Ex3W zp!l7{-3gqf{{ZMFh#L@sBM2drC^CT)WXdZ@%EZ{dOi=CcKfG>FGOertM9|u6{N6y1 zfn7jL)>Q{p)l#d^qpi~3!D+B)o5Xfc&{(0ryU(U>6x*zEZI!YRQ(L9dU3-Ih5>uCX z5fY;J@R*OQXo{J+M|&(ny?2PWzqtZnS(vH{?6R{=4t4ROO#BV@q+D{6mc%aY7(n~0xgTF|eR_F!vGw$D}bKLDe6v$I6 z7Z;xdH3T*EqNuIN;U2>0KO4woWrgyyaT{XHmgu)_4T$C9AsyO$Wkvm!D_2m-p+oq8 zd{6CYZAGj@=xwhTw?rbSCr|DkXpioQtXUJ;F?fP=sO$b{)b2F0s!FpVv^N`<)}s9$ zPszhSI|-A9B4lAE3WX03<9(qm9b+>X*?a)Ts4KSh}XClO9(!1>|$>D8Y=4*M> zKgS;{oWHZoC^fiBeVS4eBg^SDDL8HML)Eyu5zSH-9H z(25KwX#hY>Q=93f#~n`l0lA6srsb#nFI!+x$n+G1aglos?) zoOfdO?85_xXU(b%Biq1YPM&o{!@s>_0H1v`|H*Zx!s&x-fX%BR(7(@cD&u#ef*fy? zcyN0t#L7`xn!8rMF)J;S6;8vUf}aPH}{dG(;}0;Wcxp3v64r&cf;eB+iv@f z!C+P9oARAexlUT`iCNe#iB>Cl4{}i{>6T=)$Cs1O?onw2hVEhx@QA}e z>-%z)jnXVN8tsnFQ=hH=o;{~k~!~a3?6T}YE0q?Wm3gCaq zgr|Ha+TcL>S=_K372s_2a_XT?zCyml;T0_SpAM;PJ>|kCbRNcEFHY^KuSfsTGF1_E zOd+xsz3;k678x2 ztvEx+9-3b)v6nQJ-;HmFiiHCCbVYx4HQVQWq`yNLnN#oNSE*%e&`t?Depm;SFKVEw zwV$qaAzPpTv!?B;DeW%Np;XY`M1!Z_KzVrq(x*`>;qWOiHC@66p3?ZW!fnEj(w`a9 zo`YekjUV?p2!g! z(2=pS^`)U_#ym@>7tA|essE*vwQ%0=xdVIZ*De3^rzK15i7Y2l1Ad7&{n3$fgMZ zSJ}bR`oNx$`9dVZJD0BS0(%L))5(s}OuJV1LLA2uR-@oi6&GHw;%h?tuuP zpn)bkB06g3o~uc4uFoYhYl=n5D7q;EA+c2}6ieKZ5b? z3K&=I%@Iq`yJw@(RRg#Z#+$m`B_%(fp9`s`m@N*E)LFai;Mo$tC0h1|&lV2bd~f7i zY+_X1wSIkGWo}_}QX0s&(KX6s7RXcJD|ExuA+q^*-n6wuGi4DEjOJq6RiZf)6g&Cy z#MkMFDVffjjtB#nr++m4v)5^}G;E=ZUVwlE@=VAG$z;JVl4K422N+CCOy1}yuYI7Y z$sSP3&2D=@tx1T67VgQ@>qB~dE>iG0VUh5oMT-^*<4U>Bn)+r(fK$L%hxqdlV?eIj zwgZY_q=$-%E?n5V_YivX=6mibDmr=cW;Fk%5|=U5?QU;(XDA$cx#IBtYNtt7YjU@L zw&&r)HPZ53=XWE6N5gl7!|iTLS0=QmqZ{!Hb!fkg+w40DbI-PR;Ekwiixbao^Txg9 ztK!~HbjIl|2Q5$Hr}si_A?XR7=r`|ySi*AH0=XWde`I$Y9do7;A21nce;@?ZQQkn% zff!^nG7|^|BM-a`LZfR`qn{D}Oj9jQLzsDX12aJ25B$G~%S%4;A&Z#K(@ZKlg<_@#B=XJMiV75byN^E?WiHyi8?kop-kkn$Wryq7vE|E~qfyTSPhDNps#Ont zXz)dHgIeCEOpV^9Rpq4qx3!hJwEibBQ$4XB;rpxC)KcF@Z(n@m$kMY>w>z-89Z#=9 z^EJMv#(Nhyv!}xu>DOZ8Iub z+1uM`d$iN2EhvhR2bfssEDD1^b%D;O^1#AQqX*s5!zbyfn%+X8h*W%x$5lWDr1_Tw z>Q(t37B{R@iz&;R)yZUN>tO0TkE5V)jl^Kslq_#G&D!h7-=Hq)w6vo@sfqh-c=qs& z)bALF@ZYR+>b~eruyeZwL3KZ_Li?-ZvD&kHb;k$#?$Mp;^tY%UdH3D2zx?H* zs>a*CC$+LsZZ{=h^QN+O$nhH_cf(+Qc?FC&l~8xq*Gm>zQqwBinwpBsYr=cF%Wg>? zKc2j$qHjmADuIqv%8JZU@Bq%Zyou_KeaYnP(!9FmF8A_U`YaBpy?&I@4_43wkp$@7 z;(=|Vk25U@IGJcu=ye01170ODZ{yGc;#{PZABYhrPJ57%iElB_v${On9r%|T30o|sJ|xT3`di92l;m;>!KK`=%BAta zdG9KztY|paTUyo+7esblU55EU-Eezk8aaJqB3S4{+v96G@7k@=wc+1Ckx=w!l`d9+ zZW6B>HC-xE?_6NtyqojZJ5N1T^44$Azf-Rb#luNS<6`@ph;R3Jx~cnG%caNhy{jH7 ziLbiceQQp9Okr}@E!*}{7>4?UE* z#?4Ocr~A;E8Zz5>q(56ZSmOXf1ioK6$mQZbKN!nS0CGEwox!o-cM86?mGQVS*+~Ju z9X=Q=0mD7>;PAlD7%8xUOF4Yy7+)U0kae|0s%>sa%1ZjD8s|?clb|C-$4Uw^ED?%v z=vRg6<%v-BZn;uyml#AM;n0DADPCrm5lAt?U@eDAK%=TB8LX?gR`WAdW{Nh~J=vL@ zG|TF2PsVdy^Dgogm8JKsocg3E>)pIy$^429TPw|4buw9po|+Uh_-ZPmiq0~RvaO7+ zRo2mS(Y{PW>dcjFZfr*ykS)CaEcBB(1DGuv&@EhXG%R1ZY8GrfP`0sg8CH&rf&>i! z_wQ!W!pyk8+B7k(gK++J8ahOKnwrRI$<>Lvp_d)u{q{MjXi`?S-28ncK~JnE?(Gj~ z!hKhvtgxMylY}k(`s;uE<3}H*GySdGT+`|C=u~CAlDynQ(Tk9n{tWW%Ip9g|0Uf}F z88kPn!SFkUdj(1u-X@qu$UKAk|N7elMUA}>4rm9QZ2k=7M5bXE7NFffP16Udcj!+m z+{;}ZWCDt&=h&eT9!ZX%xksU@jXX+Rn&*+aYk5-cfqRE^4GqL^?){>rVY6$HZbE13 z(pAkzCT8jP{|Jxlr|`ggifb&MV0-{JWJ&|zJ;7EFYb{0y;A)LZK|WUw;%&ZlPeENN zm>eE#u3Y|s&XTY}Z(+^SN&3O16`IY<&8yc*KFK_id^|h!18+^-L#gVgyIw^ipY4-7 z(Ml9rOI&VhQg>(RDUKc8lsr_kN#Xd+c#vvX1=J2f+w zU-~{&^N#slS$+*Ukl}rhyJmw=Cfwna^y2ZJuy{fhl-L;MtW5M8^f@8GaYFp^7O8An-<{5^k#(YIwo9I^s`@j5> z)%oSU(4JpDG@Cd)^uxv3*p#>`AA_lXC03=-6P@K3g9>86B_AggegmGd(g> zFstbr+?xpl1S(n{L#Cc? z(j6s9n8*^+73*Ym+CtJN*Xnxo#%PL+y2+WzWT4I!tj&ICciG(Z2OG_Hahj{t%uH>w zwCG4py}8idph^0tFmjLY=w((fWb+KLKQPb-P8TCO*o7PwCM^4Si8q?4e(hB8^rsK_ zHbK%QBX<7t!6f~o)I(oZI+p&H>+xzX9FE}6i?S9F6k$h zDJIAMaE{)TOrq82($@tAnF4J|x`h8wZbJQ?!2ucy^plk}9s7Hzp<}P)?_?VK-CP3< zpa3+10OocKYbyl?t}s~wuH(&m31kSO3J5+x{IPF{{e(Qg^=FMg+IkRLzIBke4a+H6 zj7-LLO$%-Bp)#Ad+lcJ{8Tu|_z5e>!=<`2(kGPcV(;aN2|3=+Sn>9J8ExFi2X1k7b zq;uUhg@fK@>!E`hwKAPv37yLG{qddy-?bg~mIqoDbJGGhA>Xiq&<*`2>(fxqw7jX`_!L6r9#yS4hN=E(R8K#Ko*8-()oAMF zaa+~**nCbz3B#~l#Pv*DsX^IQo1^Q~db$;zNK`ep@9xQxEv?#a1vF6ob@ntZrzqcE zx4v_^gewzHwi$%IP0U@v_WT;yNDjn+)5omJYXY!=Sj@p`xh1m>!wD4FcftI*k%nZ< z9m;2*@&coPlfi&|P%#?N58Kt@F=-gbfnR^ic{25jrHb#H3<;jVRz#fT7V#}QjayS9 zNOn|=ua*n9b{g1)Y^A)_OV#nyA@Y!+ESpn{+Nh*#3& z_h$w~5~Z|AQYb)zR)?!BX&ySM25`!)iB2tl>N*fHM6(4faDq_WU=U;{>-L>L47>4edf5S z+KtmFd(3WXaiZNbBSu55pI)wy&{NTNGL)^l&`D*wTXj_`*kvr6*C4NfV{ixDojB;O z2_yO!r+D%vnQ?dZmfN|yBW>eBpc%$CP#0vwZkXwK9SIy}bgabB4-|(*3w-VfKcO(i5kW{CwqCPM5-%;!D1qxs zFI7SnwZttaE}NIiX8GScM(*EF9wSkuDx}KLjp$G|S=D%AkgA+a(SLNUQ>yf9tXsG4 zkQ(kwU!nw7Y0hdFm$+RY*Bvw|xC)Isv?ndDJP>AZBP{!!AX72G{&8lxANr;O_>HX+ z+?6r6j@uX|8s%|+5PQb-f_@lYIy3wVJ2cFkBCNh|QXN6y<{9-k$OC@?J3l{{#=`=; zf*6Pp1R--fR{5RJD|E)1$@-Izmx{<@8FD;eoRKQmX`WG33|KUX{;j`#Z|rbOls;}Y zt}sXSDSgC{onU(Oiz8dMpxmZS^s`%Hn~2NynPrM5_s}y@p(LlZJ0&iEJqpVe{_uhf z8Ltvj0kU#|DI!sEh_F2`v*;OIZKzE@7zHGAP5I9Q@4Ge@Q!UOJ) z3;K~>2OcyV>hf@vXAf{Y7Y}QA1-P(`M#*DfAgKzD&uT5?Ku=^?Dr2{?kmg^6Ms{WR zo3M#DQ)+Qo1)@IcE5mhT@C^KGq)f2&TA`{UmT9kb=K{v^Zny3Oq>?(dO*VI#ry5>TUdT)oZcO?o~>$GdylAWhn5P?@53J(Wt0uTg|#^jqO@vL*vv&C{K$;>p}hXOFL< zquE#2(P=ZgYf^RbxPZ)O}!d$SDQCWEijC{gb?fE`U4w~U^K>bV*M{0m;W0eDjDeuU_rw|a` zx*}ZVc?&$K4>Oq!*omKN0F)FjnleHB-0O%)v3~Xet6*TYPQR}9O^3O~NJE@!Y%G&ZurC};PliW&aFD)J;{<2^Vl)+R+%l1*dTXln(;DY_sPLXi zOrWhY`~FcSFT+c`4I z2=EDTQvjJ@dQ3;6a4YathH+xJGIG>G2KM1iR2-~c(eq(?dSuo0=#uEN@bxw5kF0_M zO(N11C)JPy`d5j?9snjXLM;262k4#X(sv{-TQxsrRlSXVNWbW<^y!kNsMydJP6mmt zO&eR*?dd&wbll!`^dIU|(S%B!P7#ObyXgOT>_U;nW)9e2ht$;o#XeqSSmW zK6BUsDJtj;D9!E4W8b9-JGdoBk*-IgiW0?FS*icRuf| zn8AdA63k1*h*)ERkuozQMyti%g@S*vlSdrl7c0qx(_8w=wZe}}1-xZnPUE#M<`%vG zL!MfuRu@Y-1;upt6Fkn6(0YrZ)nQiFC(Y5at=VVkeJS*E61SYiPvQ_YLvEdJhY1R+~VGC4N99~gD@fF`SiI7;up71D8WMQAFYekg0rkEh~?Bo*=k`V2brYgKh1p}!g{WcgCF z{jP-CRAW&Q`-FNylwDRzbL3V*jBl3{XSg%1%1L3~s%$vAebRS&lM^2w`qCMKsN+;D znW;P2na=dptU}`z{SvLVP5FZovqbEWm6eC|+{9Mby0Rt7WyKn(OtuH!e+hRkf5dfn zQlMNt<>S!i6hFBq0y+ivE5S3ua2{9!bFDFo4Kz6f(vx>e@~{!8PKM3>jm<1oL_w%n zatIf~w}vmQDRlU}x{wlx={|uC=4wiG;EK`rZ&fDK_C#53(DW=KEZUrR5RKh|gfPrk z^!D4I5*7b^kNA|nhPvA|LRYX?KUpugdFl(=`ugbC6+y6t>*I+k>g3eQ++Zx?cniMV zH5iKxM*3p0K3EW*E&eR@9>$nJo^J!0S*#_&QNIN&hoXo4ZQ(#c1ZJLre#o0Zd?SWq zNv8IQu^YE8Tmf1KXHefVX_5vV+VeMeq@7o5Qa#>OLas?`^>-;DlW2E<4Gr_Xz8)Mh`8%c{P-ksdVo|kw$pp)k3+76O8@em z9D4zuEe))aCe(3)w}yTR-8u0`Q@ETop)OAI1v7s1KyRfr!Y}gJg4k?^lXZ1#CT7O< z;|G?LG^01#K!fPvRH;!OJP6USoG9{Sh|w4bcbFaq4?$PJrht0*n#N$hHli|M(-qcP(k!>oeg7>FxbNU)JNRb$IqI@QhoX zUEf%ntniBxbPd{;4CXSYx^hEabcOEq|7^yL8E4=*1KUh4gUw$B8^VT47&mwgqs-U* ztuPOpL~Q)>-8-YV#pQu8I1(#{zuvynoNwQWOS@mTl_iV@bRw{0Y+1)1FWCz%t3L47 zS6rJ(KU^s@Ycp|3oiy~CUPNO|cA_TiOmy;F%^JGuA9`nu=TVnzL%iT4+qMzsCWkeR z&D&Zx+Y;{0O#@AlAY7b*i*!>)?k=b3X7H)K@yfd=zyQF88afrT!^q^}Hl8t2DQ@FA zhdu;tJVOJ}5+38-DZo(IGy8z&@D9rara>~bJNyMYk$+6?~3f8IUT&~p&F1aW=C3+lfJ06=%v-(JPKwUM?`sBY&rdB_3 zPi3DfK6Txf`;lM>C1G~P%R^(=)@W*099)9#`QR=3?=S>F_!j-Z00a&D$XHeVo_>nz zs%pln3QjQo)x*$Vt``tiJcly?@zofFN!wVwaq0}77(5AE8G&a5;SkW4o2(sH|Dee* zbe3Kcc2nv(|_hjJ|hnCGHey`|nGWSm?IE46`;v`#Fz%6*6uxyPF zO2#YWTlaKz?OER3rmE}Nvz1<45)jk%=E2T9d9uiDat(fS=A%T|!SJ z>1S}aba)z4TRr8$-9gk>*IJy?9jzlCY^v5x^>-fFOFt}WrlDD7+)akBPRLX>)oL=z za91kTg{LAsSzjNoqYsxg>%)dF)k_W^*AyMJ;k63k{yshI2j$Nm&jZ9vc3j_+@4z>_ zYCzgSFJS`)?%jxYi215GreS=7;iv}dH>|}m-agW)7GB1D{)P zJ)j72cz#rt2u13{we-)Btv1{XkJYvFz*DXf1)zAc5$&r^HQ)6_<`C|$2alDF%wy;; zsEfYty05M-SOaIv)SEm8bIp3OL@<%u4Wt2>Tf^3F*ww@G4DD?5+ME$y*cE19u9EpS+exyhgs6O1=3W{U>n|nnFLe7Y!byAF}#MXS}qov;5&NY^rY4_`T%8 zw@#e+#p!{*h~Z_9g19`}r2a`&cp`iZ zSkigcA;>-Wg_y<$&W>Ufe^4-B1K7ht>`e~eoDn4mH|mzIl|aQB?p(*QG6obo74Txv zpn&CzSyzT0MLb=mP*6}r+wBTcRxW(I*4`%77{-yo3Xgw|PBy19_o&&dBuRTlQrlvG znl_ry6_E&)O56^ZG@{e;|EAQRFO>xJ@u2))B{ub4@gGQe?+hrUqIm)6kFk{b+2NBjoA!haHwkxi935TL--VweE<<2;#&xod2g8^zc&SU%)t8HDO~5^08=f*yFI zUJ2}+K2Ip~5?JUX(eD8^6SXPEnS#BF5U=EjW$i5U^rhv?_$E_XrBX;~C_>mZ$y*#O z;5;v?FZK3(U!szG`9r@FM^pBssJf%#ktApAsVh;-OIcJuMISBk)tX)U zde0A&^X$(4GUB|c%Osx?f74NUrBSH2$JV~w=k2YETZ^{VJDWfs!%hf`H8jeGh?vM-a#w#0vTJ?2O$jfZ2sp=OKI+8#u`Gu`GH zn7D*zZ#)M0M;rKYV}W0@Zp{8M$Htt7JvE2fR&y9HnsMAYa!7*%9h!~fH~o#KDtaI; zgxM@$M_Qw}aM8f*5*!0&1CKmmSdWGgZj zvb-!2*URFu3__c*Tu%tyv^BJ$XIF=V7oH}U$I?%KmK3#P|H9i`$M{^)383OjlsI(#G2bYPK}>xVhBAuF5(fIj*rY95S~;%b;lJw4%ZrJEp)X_TAU(HfXee;> zC<@bl=pl+b%<_n3#5sMxUOG8m83@#s^wKgIHwuZrOilBFaa5+gY8M(uzwLS3xyt7F z;7@;===Hk2-j{r7fj(VBt`O$bA7Gx>qG(H){ykE| z^D&Qh_tP`AQyy6NORIm$-Ex`5*XBPLRTmHZxw=QzU0EJcx6!pG2i=CwAv*NhM=7zBYwm{$&*G{NNe(z^q53WeYy)y9p?r!*~1pL_uFy1f@Msf17?QrrBK7-!?+$*rr zphV%Le8(I3f?mdXjl(qyA`-O%9Yfrqmp)fgB_WHt#0i56^vKP3@d!SBQIbvSBhqMx z^ZFm5n$?TGvO>nuRZKXO{5wh|)_DeLX6SFaG7c;Fb|Im&c({613Be{=sIbC}z71s) z*Mwn@!{-&V;t01sViOlciV5mcOEw0Ly#|YhKK(`9rchfFmI$jHm{e1E9Lq zlWj@uoR&xormI8Wo?x*#XSO5ZcAYzJ6NsKDGKu`E=XGJ1$M>Yp;1($FwDKc4djmzU z+hMP>*=?20+1berjb;nnRUvR#@c#FJeZQGki{%g3X|w`cbi~KTy{zFN;G#0MU97Kr zMx{&!`Q{=41$g8m@(5?hply)W!Y5M5dA&jltQPL{gw_R&WZTfLx9k(J^0Q{WMR0}4~3>Riob0YT}8M9Y6vbYrP2 zq;{f-=4{a4Yn_uCoV9Xg@QyoX&o0-3dVXWN(mIzmTlBGtptLe;IE{W;QPHAC%kI9L z{y!KU9*?8xxM>FcdAGw>T3`Mhl|5ut)vAbb!L4=j3SW38?Ln{V7bRwvCZ`RZ3%1d} zL*i+ZymR-`vmtMjnoSp4Dnxz}VRFal*W4x$^`nk>b;G=YWNIAQRSd;g&7*fAar6eB_Ar$dPA` z9;N?LU%#}j?%CQ}s0tn0flR%8evM2MGMU_ZLsYzsNc4yboN1jiZZsQGrbWcrfvnsa z%DR`Hi+eor(S_=Trws5X?_x&W<+RzUKigLY4uh+kN0v=0oXH$B0B=VSAl8{(k z6%n&p(_r%Z^_+^3-Qo%8Eg{V1B*0(C91lCw{dEjvO`xST&Vk+Jd^5K*zX>3Q!1}nU zZy^*3Ld!1m`SZHTtCrWc1^s7-hEdZ2+S4^8Y_g)({~Zx zv`IHv7qQp#a31~6!D!{>4wT+4<&u1tEG?8JGrysyd%V&tuS|>1eHXvS7~oPzSZ+Wm ztAN*d(_6!`gWnm}4|&nSsDwFKc=Paee0HF{vBOId00lu!%{*0x43Qp3i)zhIs*d~g za(+CqV+U6$%E{4PYgqzzK^YleBV6osNLa#xb<`_J)6yO7s8gdRbo?uAhMtZtZXJl_ zF1M%e7V=8%xXgJUAyGP(_QkqbXl#%9-LTy@>E-f zTlZ|8FkMZo{;nh#nqD%)pyR#!?qlfey@W5 z67amrc^3Pfk)ek!yH=%A^?FxV#fdZXZJg$p8jD?1kG?j+oAx`P^p3C0D92TPXYTmE zKqXbR^I9O@DoFX!D&#YbHd&`JxL-%?^r5T~YjQUPZ zt*>bMLARi(>3RRRoSW%!=-%{dPdIYDlRCiTCwOK_QY5~I`pU7|ZgH(}!cyRV1;2V3 z@V)@>wSdGme7_*RxEYnWaS#aPCf^DKr$e#51`9lz5=HV|yId0-6%+;iM}uAJU3(gQ z+?vO7f~GXlTFEM4D`JW3DadB_(c96Z^hJ@)x!z?H5=1B+aSVEm8JKZ@Y{{%p-Nlad zZV@K~!@x80V%jL4=!gVlfM1}gf639i=+B(?5}^UR7x4c5kk7rJRRf&3F1T$YozCIy z;ixKz$6(2$H|D}eqEQaEsyE(r6y+7z?H3Hk#JV5yFPcR7{Xk4biCf7v)Y~xFTkdgr zpr~8TBZ~G@YvA3yXtRhAYv^uQyWBOM^haIR7VY|tzUIQ(D^!uAX~naicDq|r8V#YD zr5Vl7zw1I{KCY+#++wkbK@X@t2>>B#i<7?Fs2A{c`nkyG^^H@zR)Nw!y1FNlyxx`B z$K$1W72*uP_&I@H zmKDhlWt+0}M075TL?V8M?Y{`<49zkxm(`o**)Ras{3(C#@%HgakoPKe^!ty>SF zIVpJ9R)HAbA!EDHfihB3H0% zQ{bOnW|?4icocIN(QS0nsP!1$(;H)bprt z{+parQjjjNLE+_-=%0&zekW}2$qjw=!L#j|y#g-9Gf6VVqLZ2M{V%8K$6R1B1Z8DN z`tyyk#Kl>eE?rV8P@nuNlfiHxW4oUO|DqnO2XHW9q9dr&k;eQH{3eYzcjjjd2h4l> zZ~BT^GhltP#Z^3ph?AM7Z2K;QdU6`@gdjVS{-a@&=nHI!l6P#u3Yf$&;xpR z%AYZN8dRHi3PHpOcMR8F>|xb`O-}<$4Yckp zM`DCFu^1S(7Q*4caREpUpm6^9ZgU&K=0I}INIdb5)bH)%ck8sgBZ9%oh{t7@uV94@b@RV`kfPH{6=>S5x%>KIs%{Vum8%hSN@t?_jS=Gj|na?#nO z<4fYVrv`+QlWE5e#~?6gPao^9josUs-Y4XeT(ca?3m0#knm;oSaEPwJdPLxJL!Lj6 zOa~!@34d;u>svj;{NX5w_g@>jxSl*rJvj8cS*cjo=IPSy*ijSXH$9#c#e24I?~kH) zX~@|T?L*&UN$4L?e_sq#x6V!bGgB=!dGdz)ShW8z|)C{7GVIC-C-n)Pz8`bV^?e?9G(ZbdTv!XC~>L)Ddiah9F zB$!`b&ZmD^l&TJ8!y=e<_!H!ye?t6oCG6Uj!!v5BYhAg0LJlP`O41_n$(->L`h3Xh z@yRlLwdTU4jqUcjc?V8+KK}Tp-wFrn7$52ph!IJ+A9&6&aQ7K931f!yECL8P;8x?< z4FVT5PMx2rUim_f%sji6 z_=tWN_d_P>JLVuGiL679{vMq(HM$cs1cj*rog^hJnVWDon9}a4sh!*7ll)y9QYTNQ zo=GH<8#dsxFND2*jCCG#ynC2C*9-hqxX}#ZC7%ifnZ=f8L`!0dDZERZ_<<+X72ftv zgDzt32ZUKnZio%pHYTGcCq8;YP+*VPga(6xAPUf(@qt2HTcs&ir(^AvtlhIG(I1D_ zSO;Al33ocj)1N%2jRq(FLtG(Ep<+ct8$blLXp z(B=vIy-z|O*~lxfn0Uj3Viu)9ICU{>E8wzWwFW2~I8$g`ODvJnSsThW?r8OL zD=$};I6HUmUQQV3PvC$uB>fTTC7pvCE6$_;qwhqe^hd}vjwaFZ9o_Z8GddwVad==Ec@8)&J%G^UBV~}=yzLt1==@J_ zd>C|L?WX8QsiTJC+EN9*c(;3y%H+x{W3^lMx_UX4PgL_N(;Lu(^rNVOcxs4KT}OO5 zHCEh|dn0_=wF3O>`|cTNiCh_*J;W7I9D|rD;h)LQ`o}*ihlaooVBg^aD-3oRtutj{iV4<31pO#9<%>I0Y9n-y+|c0TRDq4RwBB*Y z67=lez4WERgNK$INW-kA%#2XV+T53Vqa)rKn>{<$`Q4;?y4@ZuDb;YrGI>A+slV$h z`)VSG8xwbnI7uuZeCe4)&9XmrB)X(I=B_;g{z)CgghA*K2Mh~Hg$#ETyixRNWo}$P zL&9`81D(ul5p#;1|EVSLoF|-XWz9*j38~X&4cn4sHOh#lLF;)`f2c&OP|0Dq*W+26LXeJW*D+2Xa;;ljw3T(PZT1>ybYPf2=r>ZgC{>a1e@ z>^S1<8x!{q(e$3$GR$R?u%(#vV&rFIRiuBjn5 zfo%MXzPIz(j6~Qzsd3$M`=;c~*;c$BYy%zvS=Gb&1_09}nVK`O0q~}=>xEsJ+ecR* zzqc$a&zKN{?PUQnhcjyovz7yiI9$E}qaV95XRWb^czv^4-S(Cr9n~m` z;7;Rw>?Oz>I-pZT3uI!4>t`)i<}4uB4@=NPp%3gro&bljz+vVrFo>c+9{e8%4!t87 z0uj~_d1J}C#O6t>%6w(TMe!c!xW`s2VkBb+J$DE zMC{EI#Xfu1uVg2DX>TmujxKN5K#!|)nj@eaMPiE{A%)EWm3{Ldggd@)8W8MT5=V-7 zq*h1rBAgnjX0%7y$4PkBp z=)lP64wwP%mt&s_Gy#@=_y_9;yp=3Kt|L)aXH@2KO%RTAXlaYTK4J^G>Z1#mcWHQg zck&z!;0oFK;)AmIjBaP`gtEB;x!ac!%|sD(H2RCu;u)o4R(6Qv@kx_q&ex3#AglhZ zJ14r=$Ji zJg^Veh&febq+qgfauL$opvFcWSp4>J?1>k`*ltXqy}3sbyqp2ktyV~@=iT2i3j9Nb zoTo`NqePl@(()`t*_3N4L;Tc9m6(mnOv|C=@!hLwU_=#__Mj>7EQ&Q|2AkU zbrNFCy@8I0$H^2ug?|LE`2NW3uC&fvq32HF7n zNjqZ+E-OJu_~m8(inLrMD%aXmK9|C+Oy!&&ZJeWth{>3vN{rsoi|u8l()<}e4|qZ! zV1=<^#_X7zD~a+aha+z}aOA)UUQ_tW>1<^#xC!cEo5b3-Q^t5Hg@GZwXKQ2NlH>^eG2Es z;CaEEao0D;Q^9P!)nxTW4_3)t_SD+$lJxdeVX-BI;p?6SHM`bQ`vpa59L@^FC#g=d zdX{>~f)i#`;^BuuCt@zeWw2`|@ODy6Z){NIa4Cmr50Qu!2BeQz{$bs22FfqCYw}1G z04&&pVN))>HNv^|f-wFbni6meQB$#0C=*~&O|S@-*r-=KEk7=eh3Q8Qu0ypu6!QsU z;9WH_qr6jZHgxM=p}(Czze2VDYL=?IZ;N{VS(smqwmh?xxChPO$yswh+KS$_k_|Qe zU#Y|;YGtLW7)DOQAS0vqJu2H>XIm(JA|8K`1?~ET5ff#3<}`9QbBa2}B4n=uXs;;M-{N5@X(fT~lNYR~HTR01{k=+uG8mMe(d&nrrBE2Oa!;VCj%?GVQ91wBxb;3`zFcPySB zGY7`cXi5dmk{aW|Syo#~sz|AtKhU>yQq)+peObzFFNc5`>zz1Y5|>$3fJxw)G@w_R zePqx^kR<(${r%?l`hc^CUjzCGe-r-Awgn3yxWWD)&XIRAv6vscL2L(@(}vs`9zG-@MpOcGWf(1a<2JzlU~dBC2I~qI zykBO{5McEzYeW)7Lp2^ss6@hCGW~0n;{|tj!r`eXPnOPF6!&u?M?z9<(}D%>T4FI{ z0IJcb+R|uw2q6u8|*1o?NRgX`-Q2b!`^mUdMbPnxb-`O5J*+Jc1 zTtxA$Vu~j_OKHm%?pO9+8oFL}q3`9b!N(CfZmdhb=BM3g3*Sbcms(I9JVxveuH3nM=!62d-o?H4ko7&%am$vFPUWd(WVG+Y4tukG2r&2UDrR zcitI1HWfeUSE3F2bg(1)tKZ}%d#DN+(b`s+5EUo+MPp;oBLc^ymm!t|&H`3bj2417 zJQ9fF_XWCa_;^N2;_#QSabwsw;0;534X=Zr1+mZDb67Ce|5)L%c8y&h;MphO1|Vpf zoPK~@`M!Md1@b>g)>qGN{!Wud_LhI|)bf*^e?STt-SMNRT&bX{5Y7L(UF{ z8^(O!gm$VQN6};0-p-^NQ;5rGUk87 zuMh!@K!S{-hX@A2*D!cB!VO0~lniRw)W%xnnk`Kcb|pk&+1T8x( zT&1X?9zxglJs?_rFHK+Welu5G`}4ZIp0ENIdq3bHVqjImxgv&lhutYiaDkfwZ{GS% z;TzS&@cjbjEFoIQ@d{2lVJT!@5XI9GnR3A>>k^`EZYWfApfMg&E#uRDhSIpb$ri;3 zyS}6F@p*#A;NryrU}_eyC@2vOQi-(hSmEiqIQQ{Ki+ct>Vt=MEn`|0__;KbZ54^WT zJMTMZk$ILnskM}}=pyj? z5i#`7@kwW@+)yXGbA45ule?GVdFvJ}vUQ^QFZjbR9a~mRY(e!~S%OEEjoOge5qM?N zfwpMQy1^-0{k*o!f*CfWC^7+AUI%UIm0J@{(dvM$YB4eM&i1I{1 zM7U~XHc3-5kX3dBH}9BSQTY5Kk+)@OE4qiCLLwvm%kk(ZGM{H2e=Z#3*%Uf~DT3(J zT6Ya}6KWPA~#^)7NoG<{(Gh2)Mj(BH!>Xu;pXJ5CX=WOqYT~w39N3X8=#Bj%>j738^ivu~<@v1R z2u_~Y(2P5kHyk(Tj_~@vW?I3~;skHFQb6h(ruE@pg!hDa%QdxC**V9VvQC?*9y`(c z+$RfXaZv06zjo6XLl4?ql!r8r^DkSSO|om?7W*1ktY{9P3-UFEe745#SSMcVswfn$ zmmfkL;lK>r#4aHl%^^~udQ-~Q8Q*&_Q0q%r((dF;9y?iLN{K}Clk^+@Anq2mbbNB% znam-8nfP#_1>&DhCv3<@qjQ>dWtg{60DFV`GkL%V#kzYC zc4zcj{6C}Xu)gDrUMj5cZYVtf@v1ELxHDq4R+TPVQSabV zC*vX?b;lj47-6DA-b5Tqk%*!cy%1f6rWyq@hjpgKCOJ`XdQ0IhS0A_UlN`U{uGb3Q z{1vtmLrbFw{iq@*y(GfR^tq6--^ocK%48(+>ZYuNhO0c5!} zXAWI13h9mJk`lXhf;b``TO1R$afB4#DM`soJLR#`v6`r+#Z+3PP-0uahWO*Hr}y zvzHQlGqge6Lnv8<$YRNPEwhQTADR27N)geEzP(hYD=U>1B33>9GD?m-KV1UMj{8q^ zHh9i=r4I=?nIgS7!xNuP2Tm?tY%+<=-WL?yCkK;`sqF`Ua zuUn@I$>eBtrYs_CJ1#VqM2d~Y?+NX)pe&ANOj#X1FkRBK*6p37s?csfqvWNusIgxb zF$cR1M*1F=vbLtuO#Gj?oSiEulMcPw2O4z8^vQ`855i{g)4*OzON8grDQ%)u@x%3X zD(yA7ZL(Enp5ibWCiv8jO+(+MDjgX$CHL!9K|!X4s`7=LWCh{BX&}2;Xy51wtV@CSe-iP zYb@o{Q_z{^6QhlL$7XkPi!(fnEL|cwo~^H>pQO;H%0cA{d=GH{i)Ub7Qxo755vXGw z?gW4e2~Oc~0s`{OV72j~g57}Sj7Co1R2lgE;guMT2UCF$eYn;H&lbW2dSVV@A>>a7 zg$}*37!~YN**%`9A|h7&Zl1m=XbA($r*z5;Q-TxN8W!lP#u4o;uF!bPXJh>e4>MVL-Mr|x5GMfl}pvq9adMuA5-$`w;4+5F!^t{P6?o%!C zd$_z9S1*q9#8=}(KVC9z-m=J&wG)4HESEd}e*fC(#8V%hZh7X;N9lvnFJVuLpuYEc z)>$}L9#pSafQMTLh<1LeDb|>HBk#OS>LyP?FW_cYINZZ|GHCwlKnmKhBlEtpZ zwf!XeRf;}w@Zk549Qppi#1Zt}{?vYX?UepB zzscS118Uz4bY$&7Ja?@#yQhRr@t}E=P<$dg^d|Z}tq%Wf8T zb8d+!N-kTrnJ~S4H1)5<@z)AN@tDsiMZdfJ@_730?|v7}_}9NaTu9GGM;4543LI$9 z><0^|Hc_JDQ<>@`QDK3`=-akm--Z-_S#W*j%Igb!P8q%W7@RK$)`I1B zEU*iGiM1re zg7KF2HGbz){BmPeukOTSrNWd*j4b!(HdJU;PnA|og7JoEXlK@MY)wKphw^IGnsp>u zNS&$UCsMON^9~%3ot)r*ed9(%0u=qy#@L3(1X-^G8YuEG5SY-b+xnx` zXIs<9ASUAKrKFgDF`cS)H5psNvDrLUyg&_szhGg^+hC4co4s3iggR_elPWqn9Wur( zS!1Y_)H|K(DswcX-KO^zKzHq&8}1j*t>fHv^%xbz5H%1_ffou(%_{|Fxsb}s<4VR+ zJ3Bn7i1{|$H)b;xQb6+ayTmA9paI?ZG)i3SzF@ANVNbf2wpbpX^oKtrKmM40cyh`Y zRT(SQOP5mq!tb{g+sCe2wQC=zxWsC3shw)C*Aoz;c|5VjS>`c@OZA@Way0+JZPCQ4 z2KqUaZQhWIuX_b4sWodTdj2wKAhdVQgsS+(@rb%xz$KxIlrK3=`OYA}%dLn6t*#_h zZ>qbqR^;{vV>WxDG8EP?*bH_h2|Dx&+#4;hSn%9}ItDcv(Tk%u-IzFo_X+h*j(LKHv949?a^WKCdQxC-UAD|Vph7>T?dUiT zr{XX~j^!hBaaYQ}*r2!X3@S`cHHa8JZx81kWr;9(@ZjI31HbX|rg8P#4^u~ge#f&( zQ^nHrRHJ1Q`VW2J#}g+e1_tQ$#quU?)M1aJ_G79_Wn9y&oM0;DY04r&swq(<5u>pS zlAtfcpkp2c9$OCl>{&3c9g-ntC>wOMIQld#bXDcgGs@*Q(G_$QFkob{192%IDv_umV7Zl8x zoCr9R6B{GGNJ)LE-ZCLr#H-KA}{tN_kj8=GDuUR z;uA?ErLVry(-Z0K9a;%t{0v_Ju?7YbgM$pVfcq`G;eC(Hr3Sw8jri*}0>PM2P^mZ` zXINd#p^qH=sA`o5M>tZTG#H8$YLbviN5C=Gzji~7pO?KSRUAl6p1gtxb=Usu_d}n3 zMt|Af*_rwKn(^#u$lhPnv-w}O-SjgnS3U}VQY%*?i450s?^)LpU-?YtFmO5`#v)6E z=QD|AhONnU@%Xyr*3{P2JU%N4!_1-k*d3{@_`&?m;7NF`m5}pu0plZqeFX&mFx1#7 za6UW?7j)QIC~oUbLSI>|E*WPMEWyYYz_FoUk-w^-Wa3|twA5F+CTRhRZX84>@or z^PBYd$jSUSl?F$73(XML4yO*0A>2k_+%|P2T@j0|PPuYr($CGEN){V>SFUXKM$yAy zt(OClFR^8%)a#uJy>fwW`19@VBicU5Qy(Ja^O1N8(A3T@?nz`%wNaY{+%(@T$?!|} zQNghh@Z}zPp{*^})j>>w1JW9jHO#ZlF!VQ< zCtI`|FUiH<<~QBpuc<_Qo6$Zy;jmqhiBWKhMK&Qp3ASqP%m)?|=$ zdD_eFT1!!!m*)y%wM&<3bE+&1$>~GWfvRcU!~bJ?Y)T`EIQ;&Yj!$y<<;n103GJsp z|2fS0?1nt;sZ|61%;ipMr=TcRVIWIHN2$89oEqjnfBWT@mSjuI(90QBBH|wFb_T~! zsw)h*nlnX6O;NK*91wBM1X}cfQ14w`>{eRsWI@|3fJJDcwsU5B>H{ z&s6v>mm#ng%JzGf>z%92=xJGNSya&uP0Owx>}lF`jyfV|r%cdZKz^CpJc%SDPFjLK zJurS>e{}qK`bV0kim<&- z3v0X4S{)meTV#90ybzCnKo|U)-UZyqu&bfmS*O~#t0vA#ov11C<(AQx(Y8y;%a@av zKnuMr3-~4rrs&G}z+Axd&RSWW^RfMzPan2kurGz`x64cBSCJ<=k|&Cbl3arqa;`|srnoZt4V75SOE{xucUrS~GnM94 zol6S>*_ksnnKJYwp258vafYrS**ouiN+|#QPQELfF02xo6SQ6<1GUzN_HOCSWKT{@ z?#&xk5*ya6s!EHrFsAnFz1axBFJ1&+I|?g` zu#(aGtZyzH+b81O`GpIYJV??T$oej6FgthdnNL66GEg`h1!wa+k3zizP4_JER+FBn zN|7s@y*L!(#*QV6O#$hQ?1tnjFi}(*{Yqp>YD<~+Hf99<$i1YY8e zv_BkfKE@icqgXnb1H_vE--H_gKIZVz{=LUyR6FHKPgvFLU9rMh!>PIi8MdZXtB3=I z1e;)~qJfYgXf#2O2>u~~xHfIn57_G~!j;OiB}yk!()TMA25+4%fG*Q-DkZ1_UtRRQ zRZ{aQkF2k-=d7L4_e6_2@d15s$ee zAy>Z|{fj~xg1Kl2I2vIs5_NV4yTEC}{VOr|t_g5AoPivvCC9Q0hBS}6;cEcFcleuf z%q$+RJ-m1Xi;;pT<2*BVpzsf|G&^@vXUOf_$uqQZyQ@FtOUrdls%6U;^Dw8J!vfzJdd0lx|6DE?0m*9NU{6B>hX48@B0jubxJVd(xq zNoij%th&ZEk|87Vss{Dyb?Hb&=5&JRXEk`KoNOrnlUiR+z zUf*J{lc%rgZS&vUp4(H&rMMPJS|mD}yZcDt325%_415Af?S%`mp22bN8JI^i!kysh zemucb)cbHNz?&0O1LJHv@mLd`;EDHB3JQsL`mU%3eAkp~PKPajiR+=y=gnfR{$`}I zZ}Yc>;kEXjq_18z%?vu4!iEz+3D3gXq-$38{`=EIp|EobnSr7T z#n#Fp^|2(A^xo`Q0n_HEmaeQnwIg%Ausq3uW^e70OyOm;jo#pUVd_){E)@7L4mR>` zRu|aFTLY7H#Ey;xEH3H@P7fLkaivYAw5#Jr zJ=qs|U(zS@IlPcZy=!G3ncF>uI>P5B0pL9)65K}VYwO%c{`kk_`g~ahw^kXeu93!N zsVY*JhPF2*-nk$Ah;C*k2v4!(VEUes&~XET>Gc20CCNY-ai&lP`~&KoRAQ$IXoz_U zoTxbTG7)zb3g;2`t+$dtEqt6br@~RUtFh|Z-MPsXHTTpDV)e_G>1PideCvku8Tx7J z8(!`vkFNV3mzk-Mj5EMQX1@>*l~0Hfp!>O4bt|z0_Mb{25$o2%2Y;!`33;qC`k>e=p{!x= z4AZ)e%>j1Jt!aKJyKGr~7=8Ve)bDBL*E=-NqNg(S;}`@4HAv!pQA1bN2hkDOOspfI zqBlm>6aXvGI<0&GQbYB&#&vjfSr8>r3?wwO?fq zUuNelyv!Y+mjs^*<4B)_oGKS^Rsu{H_;{SloF7_XK<8pl1L*L7hP}lMBD@$}Mts-h z;HY2%Lgr+A(=%*T{TMkWTcf7rPb(Y6!4}PTC|aEiCUxE6E4SpM{FxXD4wmlUdqURX0*5A5$2Xxap};EllDb z+&;Bw{nHTKb21fX8R)yi>CS98<6h)-wQl;mePKFuRm(l}OPS2K_-geU3J!vK8|PjayQ04ftg z>!|*73?`YcmaQm)Nz$)+laOjZxaQKDO%GAm3AwnClvVH#Q|?F1>DuE;^VIa0v-VDL zHZ0q|Xq!Zw2yK~GxMK-u#~quRl8axb_Hl%1o&{>rf)iA>s}?B+Hq^}XW%-&4p`xjJ zZEfwEdbv>qa~9BX8;C7l%;?C+fi?pWcQ=*lOG8*&fDglrMU-kMBzPu8g$Jq75<`LR zgt4dp;-&F$3>O`-?!;aY=ueCyiyI(Wx5pm2GxK$3=i$RoKou_O56J(isI0Sm@WmsA z_hbQSBAQaQCO2>CH}lq>;Rot3yDSqHcRz99z#~^)N&oZfuhGvFm-o?s^7)apFV=Yc z&851|P5VYsvOzCeo}9MfJ#|b0`t#VHsrj*2GyBBll*9+_`|(UFACsXi?=-}xz0=T; zXl$f6tpOWR0sg}C;4fH9xx6f7#@Z*%nY0LKFn%&7@lfK8qR$4UB^*@a)i-!95>ANW z`9Z#zl3dX?lz_eBz+;THfE5`R3Og47xJxiWXmSq;=*X5D#q!IV+#Kq7mgjDRN~#Hl zP8~@!pT0qn%J9})j!0tN#xjPi`;%hr&d@Rf~zCqZNM+UM_BjX z1~OL3_>UM_31=o4tqDv3h86^8oT5J@SU@I#X)$3t#k4AdsRd%=&L$=kr*g2e6z$0D z&FrNG(kf%hozwRQH*UrO-tk;zG`(;k&oA;;6SBe|32osV8i^Vqe!XWXY&VjOIQ`ye z30calEaQtW?tJ)Rdb8Z_0=TndV{`JmZpxH0sE}YhoN)fxp&{ycEiRlb0u}Wh9Ax?yds01OXGzV_(_D^fl9tp8VSpl z5{1cE8I4%IfOcky2eae=EHwc**F~=BHDrpKneiOf zT;Vx1K%ceW+uRHol9)yqGv;yDHr5D8(stkj$kKf;EM$fzfD|z59Dzwgcz3ZsoFNx} zhv_jMDFI*^8%9k^Ta9q{D*V9lA0TI{oLE7D?EP=srXqs!O0)_I%Bs_353kp57qeIu z1btkmv<;xxVDtbtG~BJ3H$|{jY&9)Qgxj<;rwFgGS;BRum_~p$bIMq%NOXWV#cBe? z5E$k=DAl?85s9Wn_g8owkI8Ejp_H;Myk?zyGMl;~Ss6^PSg{7}HH!nkhbfkDrC#s7 z#${Cua^+Ko<-@z2sh~Du2s_t{^?{+8S}myySk~DCk?A$Euu^ust=#Vw2ccl9*lpJu zr*akD(}kRYYfKU4ab-PyDY|9rwD#q9WNxacgl=_7O31%0(+J>Fxb}x@%iYk?JP7?X zpv!C*v%a`LG!`#X7aW43el9U3d=4rsj(HRQ0xt=>iyX&=lFvm3y`bblY~i)|uHdDh z=m5#xp@wyj-hZdqtp`bKPd1sqWHC6PX`5SH+B>Wv<`m>as@167Q==oNid;ZQooG4cD{X zhD?0LU9(bygQ;2fuI@^dSEXH9quuP~Sw&W9NE+#>yU7|!`nfzVYK}(fs~h8s>@Eq(Q>%w4n|-z|-iY-dwujF# zGAM&=N*3~GczfF<0I@9D0MK}th2ZlG7=u4#6=8KN2o)1+#+WM91U%NF@B(FF?z$v{ z46A_%TBwB!sb^DLwxpi@B2PIRgDEy@{$M6N?ska;Ra}Dc!AeBO8XQXRHY# z>&FhVm6xqjbAI{DjmWk9WBPz~me=PSu_9%1NuYYJm1tN<&7OVfrPP82^gs{vvu#<> zR=?$13Y!0NkTV1@v0Eu$tPaICcBT3^!stTU3)?y8-5IlIxJuQbEFk(1t%|Jl1{{l{ zWx1^$$2wfkz%#74-?0($XklhX6J)ML7)+d^!2|}u{O~`s7BF@W99H_aqQG8ASy^yW zN_*XP$){7xm#3bl<<<^;A0RNAOts?W>(l89>Qs_XrstvOt!L=M7hhz4@r%#*>E``X zpLD){!^72=u^MNkFlc#d)~s3d?Mlb=++4^c?q1c@5IfvVLB=%0g<*3Evd($N_%Va^4o z1A_;puK&xxVRbDdOJxB?F^~8?U~>*DY>TY!($pU(kE{~kKRr8AE!%vQCq4M?%c0QA zsn=7f*WagqFA$=cg>$O9q%&phjBVQN=w;__O7kht*qF!{75#n}nsMNzU8w_a96tQs z@u6uk<9XegR*%CN)hDZM!kEmcG&rmgh5sly=EsQU{xR~X#TXf=u7OGC&nRvJ!1(yMLWm={ z3)Hx%d?%PD+}Z`v7|w*5aO+=boQDbFLG3V!j-p2a-SW^Kiw#-49Gfu*YSTxP;f&Ir zR4>~YtF3r&u^?K%c5Q>twf`((x^o{be06XzF*LLf-VY95OS=j5cVDKvvc;*Pza~g} zZS7QhXC7HP@}AB*#InTU=#O4M(vsORB?~Q}8KWIE?=9JULl8|HBFUlm-yhlwd*GLR z8H+u*odg75%W85(cT1qQzoISg>%+?}@oeK$P-nn36gSxR(V~4a*&TLOyrd}pr-)eM z&=4ChPJx%Wjss@w`z0f6{2lmJ90#TZhD^cjASEyz*2%&%h~SosP@SG`OJvQaNHUU? zJJXug>+&Q!b0k-pw=Y}v#|Gytt!8GO*AVb0gSuE_uHmGdi`X)+ySb7Ap!ZL!bO*S` zaKIH*{#s(!Y)HMvmA{if%M-+}6A8UCQ7&3dzZ$O%IzseJbmsbj_WbsK3Wi1!kPA&n zDo#^nR!!O?OnEI{i{7I5TfI)(e1Z*;JO-vwbIiGap9|5M@Mg3bY#(@ho)bf9=%Vz zZ=|yqJy|z2w2N-3?d(e-UY!cu?|oal>Q+CN+YRQs!j3@@kLGH0DHRk~|9w+38BC{d zdtgfsJZQ1lQ609WHP%pnG}4){%Acxrxod!rIE*bn%j88ZkR@*fd)|fh5hE@aA{3Z_ z!;s;{SQYBV3=jf`gO@Had;|ic00pLG#04^CMH7U_PQbV`X)-(x2ui$Uo8a{%+u~4O zD1E`VF1+LQ*W*9_@jWm(beloVo2)NeLUG?;k#-fNf_mb$m%ahs~>MUnqeb@|7b zF%11-wdyJEm20Q#DzdjG`GMM{NV7=VG}%hkNJeBqNt(yH5|#C^xHFLxAylR|_^Q>i zq}v3J=&&HfPjXpn5qE^e9b3mL*RX{W5f8EG1EEg-)f%}kg&s`dUOoIoUs<Mgt>g zny-^2C&!|dqG?rpp@ynPgR$_SQzE9J37@{o|7=fBq8rlKnC2LFyp`#9zqlXd2R*BD zkl6nf5EirD0Gb^pgzMGVw|Ai|4p6}|)5>o23_3IOB*WQoc z5sMq*c!OZPp`l<8tGpH;v~%6Gen#8oaUX}7hjrMxw#*I{pOPx&#yp1bY)+AtS#jk(; z3-qTCKKTTd7oMDbYvSa|qY}T+A6EY|ROOFqS4soIge*ZjHDO^y5K^NKvnbBi3Wh}T zWSNwU0MQA6z|sCK-A&YuJvE1*;Q+ToiXu1x;YpuJl7iw_^R{aRco@o%GklAG@P6>Cz40A@QS!C z9eZbx9UaduO9zre=Mjhg_oFkC!Q84qe%)=;EcI&-j-md+%DHj+&mmPqpgpMeYhdxY zJE96IgY7FrE2i{9opS%)&c?OpGy4JNhUbtcWWrlAxk$}#+5-&@4IvD%4)mm$ovkxL z_hUbzSZTvj#|RrkBB1QVt@gO5$HA1IaBmV~nkN*HEHrUoe+Re@&;|kQh5MEM3w8n~ zx0#9V$oQx^AyTrxdA8nDCaLuCKictFk96?0{!HiF!_wET2LQT_R~=zluSdPZ<(J-u zE+-Dzg`P%BIs7+#u`(w&>)alTg#H?_d3{b-yl$(L{>{uQlaKi{P#RM6h z?Y1O8DQF2DIv8pZrH!rOcsW7xcqF?VGN?tL9?w2-C-`5uQi=PYn4AmvwBTc4|KWmz z|ATy0%)-N>5@12>;A84RH3sKOaZ!eK3OrV>l$#OA=mT7xR4j}KqN;Y!idC5ikG#HC z5{)ieG+kc>t<~%}+Ci^s{l`Bdd-ozQokqK%F=`quBNh?gLOxd^GLbZ&r<03B^a#3f zOK;bP6S*6pLA%1CNK1sLbJ?kSNP)f;c+0ci<^K4KFYvaEV366C!GjnVV=D|mZpqH% z#fuY8gOe^{@+gegh<$Md2T~q#SQgI6WXWxcMdKi#aTV+Th<`!+iYYh@-v=|2%=s}X zu>NL>nD8TrH7ZlEUKg$8PWldZBh5p|yGy%{(#&hs%$x(zCGWbL>`QO-^(BKLwLVw1 zWUR%&PVdZ!d|6mtXWwNO&Z&|BKAi=9n4qsmW6aZfuvY81oJ0dh;<9P$h@bnt#;LPK zHbs0$Ia$Twm95N15|;)#v{CDf1V>b1chz|3(#@W^(AIo>XIoR~mT8%t!g7jhfSDiR zjhW=pRgsErrLyoLTC^Y%i3EdlB9T`^rid&o-4<3QY%XwLp=LuuU($Whn7e`0vF*X`H?@fj%n-p21TwMSC~VTgAE^ClyeAPtr_!9W}~D%et#7ZdohP zG*pj{rlNuvZi!5y2>sfU&pso>F!x}CW(NReIvKlsx^~JsG85f2TRUf4{S0 z3n&4H=ZysFZl9L9Qc{*xdvY8ZduOJ(HYG-XdL=RV_De58egDN|ry*_bjQ!~OP`f*0 zYm1~C8URtWm`ivT=87ZG+fomC-FcANT|Q~;q$?(EEn52t^FoX&pJ44F3ki%+guTMB z+0ZG-WZ&>dB}6bXZiDl}8-8JF?vBZ`mo}zG6pmsQuY{G(H~4=MriP{&_f$#$?Sk;Wc^ zF3el6?P^IJ>ZK0hR&kirmI_W%l)72H+c;g`YP9>0T>*~d3r9biXPEucQRt)Z{|VV* zb*rm&zYh9>uG12qB_yggTxv$gjIh|p|Ep{Kes3T^x2u}v!dP}}ftlM*-I88fX`g4l zcS=Lq)Ox6N;W_WSSXuBUY>?$E;=DFMZ5jJtS2A}3bWu&Hw=*`Bq5Op`g-V5^EKGQ~ z5(FQ}fbZhW)fFiWxFaPS0qOxa5Mr;aL~~4ZRxwuu5rl*Ju8RSj1>NMgLl8=wBD zXOVN6ZDalVt1lC;8Fn)9R;I5k^?{@Xx28-X^wq4btgG zna9VW7gLc;m|7LR0%fV8Fk36DH1p>0s$?Fwm|i_6)M!_#Gks!P1w{oKVG^Ubhv*r= ze)WML7ltSd=Z1zRErwWZ*QC8T5@Y!Ce>rrRYaN(uI0Q3+3dTqS6MZ*3z-g2wf5r!Y zVCOcx5GqjsC<0PcDoEkrSO(AX3jy4J4h+Hf*}%L0LnP}^ka1VEy4f|lf(mjodq|-> zG3P@=mtFTIFB2JnwL4wDn0>EWDH$CNEDDZ}@?Dmg7Yeb_Ei!dfCoL-rl_8eQVx(h# zH3h;vze2{|w{IW%rVJoy>jQSuHO*LU9`h;mE{h5}L}UBVzZ&DsZhe-Xf$qF&r1`27 z@tgP+DITQXM8Y$1hi!q`B_(SHUVd3U)y+3meKDVT(0OEbKQ}|>6$cEl>MDiQ5R?iU zI3`mieFR-g|JZK~0*1YL3#!Uy2ZmyHd*JtL(5c?B-A z5@vtl4^`oWJ?iWXjIBt8*`vF88N+qM9Gx^UaJqx3UJe|qd#?C8eoiEjiEU!)@zn*B=*QyWT=7u)+wqev-(a?GgO5HW`VF;#*xZB{D`NTzF!{( znDy%x_eR$~LhT3436Uo&<(#1E*8V!2k7WP$w`?RIMr%ltyadc+WHx+yNFGv#HF4=4 zOE}>RVwtD_-xTN62cef?4$OTmp0pCI1l}u$>wp&+6c-=H$pys-Ak%+*PL3-S47dRl z!(n1LMn)Y!kd5cSYZ>hW#EnBPBaX43Wq8U=0`U)Nk(ErxX@DYezZG@{&Rw>7^Vo?% z;FhK9*TdiE42Hcm5vlZFO}Csle}1)2iwX-Vtu7VyhGf+l#k_^u#)|lXDBsXEe2>QV zR+j#9WkhADtX;im(eB;5yXB$^FTlZurZ}Nl&^6_aUYFbHbaii8rtvMmR8y^VOg`B| zKQL2asIEXeQ!N&fnflr`&>cG7li0@=LA))?R4Q*y{JYAO8O`SlFB!GO@_e4YV|X}- z_fA*9+)Z(why)!o6(;F$u4gM#+s53AtJg|^?Eyy}mkBX3fSMrl&>hH7uY|z^%7eK9 z2X^VeJ;Nj~DsJ3sircS1qr*kO*M3-am6#m>SDF9pdt1?h8o88H<#lrOsI2fH;_yUe zL0tq8oSP64BG6sSxdNpG@GE?XP*%%e3ma89-{)I)MURes&9aIakd1KYHI6{Id3lT8 zsdmQgVS8I}(c+YsJN1OXp6nxHv=$F#!?QmIeK@5>RqvYps{%D~CyxvUrD~4ECk2Uu}u#rA^1uQ$&^TRYx2+T&2IYh^UpYgi3G#nc<01fRzFO}Cz%TO3fQK$e$qCu zxVR96JKll9()Sraw%xITeP{@`{QG-pw-?DSQMAc1=26lKbu^-+c3)2Oq$X!aQld z%0Wcs0Ai;wWfTh*SGBU^*N_rhc3@>Wzbqptzx_{UH^n10;%)FiYKsQltuhn$!J;I) zJ($oM?R;KJD@+vL=C_hoiN}TRZ$XOTc*k9Jp=>aVrnV^C4K5<5h9v9p?E}rpqdn;x zfaSSXISksLNDHS$GPjLX#qA=Agu~$wfCoWe-XafkMJ|`sWLDXhdu(=3 zWw?SD6j}Y~9!X5$kR|1Xty$6;GITrU&kx2p@#|xirpd#@vp;KS2=`j_dfw=)S%ZUf z=N_ErbQw1`)jRZBcU(9--WqL=w}dW5gJTZS_~11HPNc;sWzy4jP!M#(5SR|$v}M1A$9s%J{MLW$TED{p|pL~ zG-^V%9Ht>gfOZ`dM3M>PywSLkTkP;vwToWcSq$o&` zh7wsp0|C()4OAlN#rB6E1OU8n7Z{2Tt1Y-+5HS24x3@UN!q$Ze6kz{o@jpOqO_z9I zIAZFuC+hUm;*+)dE|o1+ch@_f*&Rv`vFPfn&lYaK>$>aac+1MYJuzoUsZY!2%uSit zE7u7U-YHXvPJfd%?rQLr9Xj;tkt5`xL*x;f1~+KmpAQbHx^6+sd$lcYMfwb3Q-#ez z!(>Y^Q+?faI%SQ*5{?9oL3KckKFIoO{qQIIz7##{S_ld3Lzho!B2P}qZWFMRDpx`# z*q@CID1yeGA$vqQCmxT_gw$i=ME1_ww|%~lGq%gf(@988Es!0efDHIKILC&2-*!xT zbJEWzeFQ!z<6f5Df!YQJhESg=<}&ZbaR~s*27&b&|M+PtJH}Rl zpUnJDj`poB&mq1_@h2qVSI`fj8I>qp2`!7)^4MsYzFHzdpMByQ>#w|IbEMon6&XK0 zc$oLzj>x9Jik=#M3%R>m%X)VqtKR(VdX8|@V~WV~V8;^qVE3j|Q;{QH)n&bOE04G4 zD(;Opb5OA2sTbH~PrOvlFRUXQ#o0Jfdz`MxS73Kay?wj0$1a_c4Aw4^&8%OxQ%xdQ zK+|X%*}&m1y}XRQ?{GzsbNWt#eal@u_9xRue%db(mk&$S=mWL9W}%v>F&eA_xm_x< zIawU5O<4&aS{2nIpUh=2!B^~hlOtH&J}u(*>P>c28DX{A7fVHKK^d=b8`0=ig&GG) zi(4IZI4X=ry~S^_2^1E1$jiePb)|)`v?Z!s)j<*0=OCkOKHJVF+%6xL~{6cYok%X6|$5npI%%htcF@}&v!p#9fh4!m4F(k zfjrtI$R2{e1-AnJE=G>yE-qxnF7|)nV3TCk6z1j7QVReU4`;tVy{PK=QW{pj$F+ge z3F2#drIbu;6mGDZ`LO%$hUE|RsYf4(Wc=BByMv#prqnsHzxJDuUxxRtea`Sc{(YWe zY_brnvJT@c$e3S;Q8M^98*fojl>vi8s^CI5ti-|@Hv*XXFq?y3ip6o#6Z|K(0vD-^ zJ(UnC-<3nlghFmzxf69{>Am5X+f_}khOa79{43=Q68DdtHCbg6-pJXp5lvTBM%Afd%~evW z0TF#^E@yLyJP8ywx5Zs%%I>Hg4kf+qF|$3T3Wm}q3TAule)^~mQ2x|;E$D#B-#x-Q zz?us8A_GiOH_V3NywrrdDHTfN<2DY&$i`@52jES|;>=hs#?*${659nLfs!skHh}3Q zS=qwEELvl(v!nhT-Jd$us&6_ICd!j{M2t13qV#2#UV+vxacx@Qixu7$)$Ecti)gOdF40QXi|Sc6XVLfLzQX9$bEIns~uEse7iiMnBW>Tl*qi+&0@} zT{J^F5|}?64QcB2mrbMhx(SUx(OXwVsiRr1Fcb0mWudx#SGucsFjSk6GkS0sa^ss> zLktbHkn_Zy8{ebB1!ll1$nL;i8;0c}arzRB1t0<{SapS2S+r7>*P>ZDdS2?uT3PL* z@fZJ?Q%4@FS(rm%`t=$Hl6^S$;wGmkQ!-KFP5dUJ3dtv@&7deyRND!Z5Y7i1) z7(Xb&A`2TSG*errL;V^0s>|LL_W{21tlukZ-bfMD*`1n0Pu07gzEykuJ$CvuT2Vp& z7O8Wna>aeGvVTI{+q-L~VDT<)tS+gp(dl}U>P9KOs$-TRZC^Unxh_E2=jSsPOJio% ztRH87++;Q3GsJ+thfDOWf`04&JOi+i3VvlUw%|*_^a_*H!^{N~#{Yy7T;N|AeuK}T zrwFKh358Z$>s@FlS8%jF8CFJ~Y(vk^z9S~@c-M@U6bgyGW6r_t;jfTH1JH+8K5F@xy$!* z^D(o(r7GvTdBn77lPoZe;hBDz4?YC8vv@BU4-OJM6Xj-NG8-%*c;TQ_;85VwOUfC% zJIIbnSX5zo4z2XoBvEUwpgDg)+5A)wdSl6-<;lOk}cg0K|L1J~gZEQ^M*jPT`Zc=TKac5P9g=_oE-vt8z$0{~6-I`H=IwoHeB+ z-xqsG7}QpvFQfG^^)bDR-VTr#7#lOV1Q^={&Jd1{am-Dys-W+wFq%c9%8KYa5I4LP zTA%+A_G7-|Z=gRUHqoEteuJ!^eoFu1H@Qz)Q~J&>TYh`P2bvB=M%Aw7DBHxDsumGM zWRQ-33SB=kGIGN(JaDg2^LJmdjzP@<>mdzL^Lw;pCSI7|57^tm0{Z|g4RnMW6`TOS zvFb5HEK${H(L9P?A9=A^KKaGS4IO`#HUEo;Hhg<-Pv%YfS{9E!i=xE&;lnMG!NbZr zNb}Vx%N4B|O$$AGJe@{*`cpKPP9MMtY|f;n?_Pks#59=4!!}~Po2zIk7_E;(Xz2M? zmNQzRNJE5k)Z+@y}H5^dGs`08$&(xUFrAVCtmElCa9KFqy^k-yQ@@fNqN15RpUrWlj=eh z(MVezZfOGcU>_{}1)Yzzy+@8=|E}-5j{!e@218RVU{Mw8RmCf3?CyW)wMjTth<|mQ zB7p2I_T@^W-!_2vE-kH$1NQ;_Sda6cD7ezlUEKzLA*RW8?Rxhl&L`hcFDMt1sreBK)9 zVZ9m7%ZG_F9h?_W)0OOcT*X|~g#(XsdO7xKFWwiE+Ah{5E+`*Bp#&xaj)Th^GuBE} zXevctoqV!W+459+*PEt%BfP4oR-yCDd)r$&Elu5I{FS1^c6sgO>+%uk4BQy z*2P^(mBae?#&wecR08v5-FLr(-sXPhoYnu$m)}2W$s`~q(*U)R7!GFPo=tF#X+mO= z!CjsM(NRisVKhL|+Z$i+S9HJHy0!hSPHoMXS!9~`?a_H3@We3EURI+Mq4jiZ_wMwy z*FL{%7xD1u8IMst`|RR*=BcKPuHVWs4O!F1MVi99AWd@&5qX7-C+u@k;{(Oa??3R} zN37k@7g+Q;id8Wf!@xMw|E-a5ngfCW5TsJ2R8?eBs6rRhuDC&~43DvxFm@>V4zz!+ zFgc5^FqGFm+AHaKJbN(zM4OC!xAxzKBZPxiqAzA<33f$YGu;?Us56_l2*m#wbEhG4*E6ue5kJO*Aks0n@z|0xai!8+dv>h9vnO5M?;yMlj)eY_s!07ZXU-6ah68%awYuiT#i#nDRJBP><8FEcq?|&mxPDGay?C< z6#_w1IadPOkKS@d;A=F#26DMF5>UZ>W^j`^n;crL)J9`Ag zbA#9WodTIAzIUi2LB`aXDweQL9aq*%Y13+waHSS3Sa9p;NsKYdo-_zH5BHtoSVP5h z8s#qPfeW;UgeE3DQp923ViCF=+zj}wFLs?mb30icX(Cf;vUD zKdqr(`7`|sodTJ4B#*5^74)%mNvrE;x8Bp(7;Xjy#-7v$bFya` zUjzH-=@RPf1U+tJ7nug!K!%x$El|Kg#V_M%n}nTcWUP`TD{usk(Z-Nv}F?_o*3FU#Gk)kAa4L* z>&XgXvMME;ETD(-i=d9YIA1sFkNf5|#@KxEhQ!R7@5UsC?Fn~6WnO4aAcbTP0LrDV ztQ4@Tg`8uTiQ&HSE{I1Uwwwj`Erl)i49@2FJuyb%LF*7h92PVo;tPk_As|n2+X~k2 zuuMS3ys#*cP+i7x=ll#>q^i}TOQ=Gi^<<6Wo~zL7&R@e8^=HT*Jy`x7(pqP-gUN8f zBOuNd_7aEbTf*6aPD4k1&(*TCFVv$ch2Ik|P21fqoxR&6i&upjEc^K_!iXv-q{rS) zq^HEBiZ`Zh?X%|RublT_>;`YN)fXT`0jo!owrv@^ha^D;Yrgv_#H!OlpI{j*cAfxE zL@{Co!7hp*C_fdPX#AK6(vBDW5I8Nn+yP(-*lmK>fz5-)*rHj(XOEM&XJNTnEmtQ% zT~uKpe=a7D+*5}(H{F(#1|P3Q`^X+mI(7qpugf9&8$BXM`40+z+%f{ow(80;I4$MAgud`an~6AT-(q7aL(bXQx|s~tHXnnls;I-c&9+;e7U^p{=g`j2xzS~z4MUK@__YucCd7e(D= zUBB3%-tuuTN7(Vkh;r!VUTw#n9f}#xbkpAu^Z)MA`BV>1G3OT$PxtTXlFhz;YLBr& zO_BBD*6NJ5Lq-qG_obriXK0gMJ^73~8L-r@@uod%>U6r;d}B&CD`K$_^ujCw7cfp90sPoSKnEFPNATXe@kxRokbao$io*bxQZJnME;$6_%#UAHKc9^QD zk*sLzS|rU0+m_YV)~c(!-SJ2`=#7P4*iSE;)DHUoDELD{rkXyd#4pUD8J58bWVm{4 zS>aES3^AL-5ijSAFC-}u1?(~wA>7=AwF9=z5N*Ookh2_5TQcb%_#t4B=ZM7laUVnA`LFD-dr>uczcZ;JUYoYZWD;}ThCS2hQLWz; z3$V*bEuXyT18ENlHqUMUF^4Mn)nJGch zA@s(5?i+5Ft=m4wWW4=h`LV;`0Hd*TL_bcvOg{?fn;VHYdv_RNLy4knM=zj_@+)~U zIW(2%VS+@n|v`N+#)N-2kc}(+8bCNOURv#+0f$nPlrlDQrXQA&+tjVmUrc zHWj-cVeoBSgBH1Df?&XjF~h_l!vohnP5{XEK!;Vyj9?@MFcFMjvj?^S=`&F6UzMic z4cvXB^4JS8$Fp}<9e>D7-+-<_^e;^up=fYRlWf~UVhaojpsVQzGOy&s`De3_7ye0b z$DhPI{X1i-%DU^O6ix+lsD;NJwQ| zcuy>L33&;L{WS{kgmAyP2mBri`mS1GP6PsMU}?A}#%G4Qcqw1UxCA(ihDaX=cP2P? z9Kp30(UTa^N#J^D2m6jiYa3&aM9ef7(4~a5Qgtj={;yeI z{%*%?$+FA)jh9yW9CMW2Q^J;BCoNAMO{b3n4^)7jcmr|_I3C6`Bt1}zT>*7*fa;!T zU!cl&;l{yEUPKgO0w=H@cI-wliUgJj;+Mjp5-3pk7X;eYp~N4lusg59$t)E_D@A~8 zCR!QKiZa;f*6Pr7s!+Km+tws%eKs%eXxG8!%`#=VnhZ~??66ye<>dm_ z8nuI+vrFAgiGFvBf3+;Ddo%1fePCyhMO*_)j2yxQfnsjH0H(IZ$6?b3LNR)wuwyK8+RQbMY3Ldpta_wI-|B507Tj?)IzIx8A=MB4)~rHDle+o@u3@8u~b~@nPV2&UTByr$8wAN;7gK- z)6mf2&vdNgY~7g*TB6Sis?k7({zvN@9pXLj&CR{pE9!nV7diIy)A94?j~{>V!T5s@ z9z)VcA5A^<(5-0GNm?@>RV+9@KfZu?dCJb5R$aSuib9@J#oZ}YS{9e4)rAk@mY{8pSJm4SW12uB#NWGcij-^=ceM=uDg z+|nC(c8sxcl#ly&jHti>40hp25HPHSP)QC!E5VnsV+U6cx+z;&oI|UXc@65J=&M36 z){7=T7l0ON8@d@5fL{$(Oqvwa$NX-OJERP7W0g5Y zoX>aVqg4%(F=|0bf07|D>CKm|CTjKZiMX(XlB_OhO}d(RCTuQXoc*J)4A;6{F0HhPqV8 z0$RhrUzh^~jl||K-vXvj#gHAo!YDdBd_PbcgbWN}IsT#SII9^JhM^6{qzBCg+{e_D z@yg^=GMPJC1EJjp!MDq(bOBt(Bi{LMSr+fylQThc#WT0<@7&#upS z>Ce#Y!osDpOOu)*cwN#*e~3l{lLI+{{E`Ye|M4Cw97hAbsr6&L?NeLl^hVt+&TOb9 z^nyvro3m<3%e#l=pIv#N-eLM2$z-Zr6;VbjW=!?dA9xY*N$prl);`9U1b6|iUm{8d zB*BVyr`sr0IKu{|Q_Zv3g*KVh<+d96sxps8ZEp+V*ZOx(*3R5{HEoC!EtPR;ts-F}^P-qMr;2gQ1KwLgHT3BRAH1p;FzG;x z4FJC$13cJb{lf>cjW=90fo$U`fo0!6OGzP@B9OA8NMn}5(Kil2N~IsD{V?nqXU<&w zxa@mwS@Gq-d0Tf*UeJQTt-qdI#@TLHmD6f4%C&2y3(;9B?6upNqc7XR)wxYYX)c48%B19oOdr& z>^wu2wIx%YY?A)a+2Wk+|Efyau8i{r6~tRT+w&D|+f&lXF-cNr6o=%bEqth78Q~8a zy_6wUmF`Y?y(#xHS6mzs%VU~Z8LtaEi9!G~;JG^;3|F-uZFWLlptE`M0BoQd>u+dK zN&N1NGwWiza&F3>6}xWm7)AHF!MDRbO?#L)2iGGmfqwci@LMEmTE)z=;itHE{VV( ztPY1}heF{{XsIF z5VhZ1j~><^D!25u=_|bnQbE+6?N^>Y%@$s@Q?u=Akz&yz&5pGnpnpDO@FZ29Zk2h* zSsAx5%hKYGWQZLC+BdM+vC6t(**U}9K9KI~v`!hf?YjOWlCYTcICQY}Iw}sU3oj~{ z8o+~4k^-yUF7fo}NOGP+)9ej7n&ir)H5h0Jhvk}7#E_E(=d2c$|JvT*@@44uSRl3} zHugg5+awe|1(Q0!w>|^0WHINBr_(WJ-Ner6aW7l0n!u(G9F}0|MHju~q6<*`GKe|M z8A0uWc`H?*z!N7Sy=98yW-aCk-VXM7iXISfDf79BJeNT$ER<4?x>A42-o7H+>=PF=$BE9{?blrr=5s;m3)+Lz7MQX4gE}|$@Gv7z zs+m6Y&O1o_;fM5B@1#GZfBSDDK?9@yJB`YSkDFH43y{mcx~W2uP={O;(#9k~aiqF% zXrNlh7a9%97k&51R3Xc7P$%a)!awu9SRs>DR7mAM9eP}&311rw2B!w#@s0x?PCGn+ zKPxBo!>*~l;3pUBqQ(3>j_is^k>9VGlms8lHt-?%08GgTSeNMo1v;7{K-yvhV=?|f zOdXmW#|R+G)o2w}$Wmyta!2IF7E%6u_Q)v2xKR61_61U*oBfHn!&O}U((q8Oj(UZ}s~cr94U;@2~A+W?MeCuVqA zaJEvSf;l^I4gA4TY>{u6_pG8XGv11f&1lKFR5~VxW5s49aBF6!(J~9{ULHz+g9Zv} zORYN9W^$$FckIwmqgBGne{179;FI%sA0>W@@7z|dcwrgfuq){=E!B7|Fv%!gRrGs>aO)7BoCgGF};gngAvyIC;2D3%-Q|3G!lfd%SdDDUuYT z4s0+?638+D=i%A``r3*vr3ypPq@m`LpCv0(pM~d{|Ii?q{A(xDe1dc%zQ*&e;0$i3 zS&bU*_(;-ubDsVLHC&>+pV+#Ie$c*@-cP&{uJ7h{dUSuH7JDVaI{GlW^irFQ-iL_P z?ZW*n$*;d=4K=Q)t6v#DEROSn+?a&mq$NRVvSP#Zz(QTC%i#zLm6lnKP^DiV3OEA+ zNuuy2Kx6;isUs!oA1SO7f4O?|W((g-|LzNQ*zeNc9|Aku{@r_^%W$p;_afl_;c1xT zz8|LnVSvXN&&I)G&dtQD*ePaWkz&6p^CGanrN42;8rTjiHAuxXQHulfA#T*ZF^$$& zQwly>k|~(RuB#IqIEC(iZ?5pU%O0I~OuM*6IlEesqGD0{S7=Tk{B2{5)T*QP+Hg*3 zwGSJ$zL|}SwNp0)=#NnyUyL~4zVI*UAYo6#6yJUH-HONfA~2_$8YHA?Q}d8QFC!hs z^*{YDE_iI(>>%U~D72 z0@D*nQS9&lIu_p;-EaGkUXa(0owvww$UJCHE{uZ)Tr~2+5~vP}jHb zexu%tVmxWw$U)+@TKYXc?`M4e3bgUtSI|bh9EARySP1P+B)#_|r26P1`gb2CKO)|1 zSXnC-8hA49>c%0VN8yv%G?$B_;((MCJrfAj`4t8gC4^0SMzh{ZTY^4c&>MdL{jm2F zuh-k}g9ncH@lM&ZjNX<)tlvJt8h%f6m7FvPWFd|R0+h5EkOe0@;vA^iVA=q=m`wG> z?bq-n*t1G9UN9boVZ+=;(HM?*GDDBlr8%_3T<=CRbA?dn{W*Gd_OXWmkV?)=S`m9oft5pjau|I%t+5TJ{lzog%)Io4NrPPk_JAbRPF9#(G3 zm3)9lFns)R_LEPtAA|dQb)$6JHoYXO4tk^XA(%G)ncIWhA`vOi+G27Mj1;iQ3aL{c z5X(Ts9=s$AxkMvKu zM%Sc!UlzOdZKzwM!S@ zDa_HP+#7x3&ev;@mfN7Ks-T+_S#*02V6_V$n&LjcAI5QBcsY6f^{A6ecQp?;*;kv_ zEjg#J@6U@7dU@|&;`Pp}D6O)Lx~lWEgpx<9LuC@bG9@F$NmXH`mn)Y)K>r1$Uw!rO zIs>d_i`G~I`Q9Vph0rW_A_DUy%OZ=PkA35Ci`VSl?S#TVt`DDQ?PTI6TtDrebnQeB ze*!B_&`YIxW6(c1{DHh0wv8r;usGwrz2wDJWSBIQiW-fBLxYlegusBYpG9^o1qMt4 zrBsAuI28BZs020T;HCCZg_^c=8R4DV(IeL98^oC(`RNCujUFn975&1~ z<(wliywFk*NYiig833yiKVXJB)L$vKTLb z(ZtS3u?<gAIl}DDE|3GNmx{U98Gr9tKW_FG4VY%?t?=&LF8_#)UoR(llW=Rw#mGz3g5P#dMz2#*u+-~0^&$Qu`SWxW~jZf zP3Q}LMgXYN#MBLj7=ObE znrD^wrmgt5oCyp-~y_M6A?sl*WJ1YY87p*+Pdm$-PT%bYprr0{;&7v5(xVJ zJ^qOol3Wt+{d&Llc)nQ5u#!EVS_cNHdinYMM${Gx8DmcJcPHbV}H=Ph(s^M#h z8j^PCX&#~C+g#nNgHr<%wT=n^P~Zm&Y>u`zRQ z_bh!8zpbq1LgEg~-EY8N%z(O!8#)IEC&lQi0bl3*le*E31&AdfVhIRFbX?ppXF*6V zwT|P_BapnwL}Y*lRYGE;rb%WUob%Fbb#MnU~EwJ0j)&h8|^ZLWf#jO^9HF`K3=%x8Hu{l{YP` z^+TG-B=Js@GevmV`B}_`j&wDoE5ggv>Q!Yi+W|Ci-8kx;jxS z<;7}w8B|k6yx6|Q?3c^jj?Hb%eWq}9YP?FoO-uaBxY;N*)Px(DF9}FSiK0OZBTHR( zU20)`aYB@pnJg}iw%4qQI}(fI3sJYz)Np1qyqyJLJWUtd`)xAgnCWWU94p_t$o_zF zSYH#VC#;;(FvmF4jCBA3I^Z$5qkiyy)DW|Qa)U4(VlZ%9DqPKT1A+|a7G`;#S%zvO zx1uv#tqwGBEhS;zh&+pKD+0d6UB%A|Yb56vKKtx5_?sBt^+TBTYK0Bl7B=flLTz}8 zT+MUCy-d_6p~n)>uCT)^DLlC0hOeCvpTG|LiWq?;rDXV4(|Cdo-`0cI@HgH=XXN!-4xU@{fPG$PFFP~u+>A=^r#@#DrCUXxYn5Nq?oyrr~(`JX5xA?1-e zPV42eUFC=8+R{0#u0RrdnT^bepid}vxy>D#=P>&JD+R`976wHnB!Fg5&L|=pg_o8! zrj|cs#I3K;m~MEU?Ngu|)}3OYoejDb!JP#9RbD&1YA2i*hhBKV7GxVvqvmo_<7ZJ< zgLNOLMKS@;0nI^Uo*ZNWf}ay^3{lfud9{(bKTJhdpjPHLXj*mGOC{00Qco=HXtB&5Cs4T(ImoOM-#zyt0Ct(cFzWl<~4%>23TGw4h<%oub;0jWY4{{?vh(+ zCSVr3M6_+riPqX}%r{(_r0-Z`?SB^YJR-v)cR&{K*aj{;yI8pY2BTcU*MSkmvpX+A zpJOlE0gQJ#b%M31RQjQV8&n`ZH@F>cI4aDwf;Gh0&iKrb1A<$|bFFD0#dy*kCq&?) zd0dt>_@|(r08WEd+H*Y@i21jiC}T5@&#Hzm3j}MpKDU zs4Jf-4J^Je1Od(=ri1^lmjd-(qU7mTBi&FXBy)(VV73KBlOlB9_P}R(Qs;z+{ zv8Nm6&GO#dcIK)qa^t(@*XwY$oHfzH3bBY~snAWG%uk-noll9(5# z8G~7v;8KXksTB<&S#Qn5U+Z1k`ZQZ*Tp#(j7kn_xw)e^B-Bdil~p((-7JPf0#&CvZc6)|^mP21`Xlq8`( z=X-%mLP`N-%XCLII?QR?E!zJf~f&FaQn{ zy5R8PS$RffOBXmQWV@W-{m+>yPv;9IVe3ncd)Jt8&nNDQ{5$xMJwOKq+%BHZNU@y* zaOf%=pI{#6jf(&n6L1_WcZ_8M^F4?Gaq7(2sYTnqRwW}qGL0+YA#zc z=Mvr7Bct@x(MZ{VrZRV-_A33qXs)_s5$fVjnlvu^7GRZgGFL5e=+O_#-+%GcQ>8~a zJ-eirf4V7O*xZ-37Tj%(5z$@)0c^s_qyc5vue1~NRMsbMLQ z^#?0uC%$5iJ1CT(PE4T#$m(IO;o885i*)k{#hibHy$mwIZ4+_jPJ>DtC$C6ix=fX3j;seousXxD8Elc zn9u752J;$HSzPpatwWlLiHS?@-FoldK5O@mo7Vrg5D=VQc;Htxm*12xuWVH}9Yy`h zJv2(827) z`eY7N9)*(qtoOQ%2R&g@i$1(eaQP963FyU{Dp;CP zH5>8TYCpSAuWo*&L(u+8)18G`Tb9PM!OGTYQ}gT~{CQribK0qn%!-BvHc%wI1^ zrv~zZc$;~K%}lfcbwKZ)>`ca3Qh1)I8YqJcx;SXVGCE0KD@2;Cp|E8JaV38Hq&(mq z6HIe!naAm;m@~aa7g`pW8g}L^sF!RCwIzcROm)_`e77_s z1I^?@4b0=Hfz!);#MF9&vZC7A@H=7=GS}`BUjAWmu`AjZ*g7=mp5C{MV!&|wghlgw zRNcaF`DUKR&~>ay&~vQev9+24N3$HIGTS#w;mzIcl**2`=F z0*#P>U0l&lJbZHYygV-Nl==NffD9A=Yw0?y9&xUpHhLrkvmSu2Bs@Np0Fq*9t-_@7mvz6lk4#QH7eFE-AaMIX%1J^mYf zeZ-N22Vrop{trd2cAFAL#6pq9vhPGLaaQK7L+(7fhhmOF78f3zuDz10?%zq%J7FpJ zh7A;-itc2Vm<9H_BB35VF|+o=qw*?G|4a3P);}~e47!_{8h>lSg6zVD5PKI-ybAtQ zKYS*Djck~6eSp%1H!@<|fSD7kC^Zh4;TsiQTXB~vEDEkR|R5L;^1-%QeqLwS6_4 znfI%Z1YO?oO0%Hll~(4C?DVuZSF6^vWbNJQ^g@2cmc9!5xf?1S7+D+dvYw5%KRCdE zLb+m+sB(cn)4-X)q!qbqWO6H-umO7q@`O_#@V%B9x&LtO-OR{t393l_RNcSXz`W_Q;^Dg1;%a(M} z%$%N=OM=p|Uevni^J3DS!ENUt4+Y58+{&5CYW~X9I0e$s3fX=@uEIeva1D?O)Eg`I zkb?n>hy4qdAj)_GtR7?(fS)*cBIP6#0d4}Od^P-<9ZK3<9_QAVXy(67>Y1LMSHTRJ zSpBze)UAnIXh!BOxKR5xL|$+K^D6TMDpgl8PYaBf%7c7b6VDqz4ahTfP6@hhsjTWe zRJyQAdQ1M^d-DJz4pCxU7DzHfzGmx+6~xU~3HM#Qak_2e#!kBYsim2$w#-2_T{Zid z|H4+1OW)cK+|zbTto__`n=xh!o{(T%f>g} zc;napz`evjj$ePAt5WVkBO+PBhuSVCnLEW2`3G54O4Ue2fUf#MRFsx@ZMC)jzEI+f zs(H!)2u#06nW&Wv8kBNFNvsAGc6Yqs@Ajf)rBbX^`fng%W;B_MysMOflK)`l;K4{X zdl1*j>QDRyuos)bN*KqxzOW~khYSqvhJ-N zE5QRw6(<)*v+x8~Q58x7?SWX3B!J&fqxsoO8#O!DBcfXNC&I{_5ZC>Q`Bde-*eA8` z^j^&DYgylJyZi#Qv!X$<7A-qR&g-=Hoj-)XIp?Ce(u?NE>cnzXcl`SQ@HN+wf=C*uN1rK4gy;bGg`D(kGXTwY6sQN(i2ye#VDb{sEX>#(gvQ*&O`ypwzek-;SQ^CVtJ5*HaEMuv~j zKmd1w!vt_V;Ky0V1f*5TSyE2TLpN$VHE3HoMqFNQCU%z3Ch;OSw3GR6selI?{Z_s1 z@HN&IbmCRjCX3IYt>wfJhtOG-CauE<#YI!ZVyb(A$FBx9=9I5mP9#N<*`1u^ZOPiJ z4Th-7(E5x{9pdSv%kCl;I$>#ZA!+JHZcCw>REul}TMw~tS))&p{D#kymKhLfZqg<#kr|*h#1+FZ zVc(tbJ8JfU1%9G3f`517!LC$?zxt(vw4;~qO#kgU^wiOhQ+KD!6_b8=>C!J(G5H$$ z7|Bzgt;z^9p6H4?r#LS0)S?TQF8$}3XO1!2?~IN>omnXwiVwzzQnzyX)$b7+PW>e{ zJD%J2V0%2?{@}JZexNNrpO4v*=R4~tj%q%VvcEE|e)Q2#D6E7c2~Z>n{Qx+xBm?YhJf{q+{@CDhQV9afFUu=H9Z=r5SZn3ryc(Xd z1kDa|%#sKH%l;_mmLs(~en#RNafY+LE?|2FGb2VPBV(pKOa=)LxpP93S}Y?-eYLHn zxK*$&n$%Wt#GJ39ciiCLbs+q8eP5`s+(G=KAWs<-&Vi0rbN<=}VIW+X(cva&Z*_x(xCD>T2d_A-C*qluN{p&~D!r5INP#eSXCGK?)gFokjC>r2#&(dqW1!A>m=r!xJf zBQZ*QLqpfyYMpP6{n;F4D6HTha=K5R8;3end|NA9;aDa2-Y zuiU?2#$W`_W`dK&et9ym(v>5D@`V#3D+gx*K$SpmEwDBZTFl-E1NQGUO;Msn)i?T* zIpU&w1BraM!=LRT3w#YU`@G<|@A$36d;z2Gewbq_f8aEs{bJ(L=4>FlnUuH#RK018 zqUz1F8#9K5R|`xT<|<}?HoF+A1@sN)pAWAy5A$nxK%bMGwG5EII&}jeT+8%S=mE-ycy3l)QvGMqK-%My`qs=54N2QzNC6 zJYOo6ch;gsTuzmNO8rsvB4GVNBA2-llI?)}axHPRvoR9AIIna$az?p7dt}hmkZXg~=Ikdi%d|JtuKA2P5!}tg1W`9$ZF`}K#1v0;P4zs)u9>lzDYjKp5h1vI;A-0#oy}&@`sau(Nz1X{f zLtx36PzZ7*FwQtQ2{02}L@pg$pn;vX2;Jn8C+*(Xd>K<-B|@g##BN@~5^FcOrD)~x z{d)@^<&lp#GTP?L-VI%G(IVxr6pP)t)!lN+SXAc)wp3Rp%lzYig#xs%Y(xM0ZLvb@ zM!;*on}hd;n}N@Pf6ytuW?$fV?R5O5LIxCS*%cbI-#V=!JB>Gl^Eh`uoJzE`IYL=q zn~oBwn6X4Q9$28E#cISooX*vw_voSWiPVf((^O|sB7g+T%dgyK+Vfh>fAoNH$HA#} zL~|7)KBfDNE4tdP^;fkhZ3%i-b0BtiK?%+ne48nnu3az|Xx=d@_vo@8-Q>%=E}A!I zdpPGCSwBRo9d&Rx4$KMt5%Mdz-elN*&E-{6qHdPifWzFC zPTYyK7%%a)&{Vu>DMf7Y7~%aOk+ipVVhR zXHvJCO@@Jpu~)}3vJ>Fd;y<&iH;h`1pXQv09P_FuZjHFcG9uh;5t7&Yd35_APZA4oZxbZSu8<#CCLsv6?P zDobliu%V={I%5$cesoA~*IM=w-_UlerP>%&wkB;0u953A^wfecc_rXA2IzXTE&p_O z{hfPyQt5e#Iix$-_v?iUO=y}RKr-v1F$FEK*r}&-o37p19%@~-{j7qbE^T6dKd+cg z;qf@&`}~PdK=(`so8X#K{2AY4d+TyncpqGTE|)jqef+F(4!f;55P-b!fFwl~;IBFLYiu5O*~< zIEq)~WuAD!U8ggL-HJk?Rj2HdNBAmB4-M@L1I}?@xmbKW7?u~o%-x31)Ce~F5L-vD zAh}o0G+_-+6GE1A!Tn_~Vq6Zv%YdjK9v_8@;cfoBM2~p4M^g>N#ka>}&AlFDHqxx2 zq}qFFR@Dcn@+TDX;o-G^5c={b#2uY&;cL&8`IGq`^NM`F%eJz?XK7haD!tK%#^^Q7 zevrbsYiMTlVTd?Wkc<5_X9#qL9q3<#I!QN3AP5#7<6_-a5JEQV%Dzw$pBHz{um#bH*VR^}A}M&<8;q>i#8+a0NRTHS1LGml4dk@%1XvK3QKT&L&ORj2L7 zJ+JLnUGaP~ReyA!`pOp?Y>=jm&J)bES@>J#?hWppj;{7ymWoC$3lQ^)%UUJVa-EJo zbGoo@YNMOnTadYexp19~XDWq$Tpg(oEoyID7|F*Mw=Q6=4@MLPzR`T=&hye+H;Y`m zbCJ{@FUV0Roc#)rBN4>ic)p!|(X7?Os#1OOz^r3cZ3WZ!GdQS;G=7B?Y%Lyxsle5} z9g2p=vXo4MxHnVr&gX_=Ie`-K59JbVtxhMbZ`cZJN_t>)VikX-(|Q~)2M@knDaRl@j^q=Gv_>&OHU3N&uUw-M4@+)Q_ZW%@FHcx#5@;GYe=Eh z{+-+vU74ER6d!Pqo>gtkdUVtt=80f%j05?Fe~jBnGgLY!Nf>*>zwqHFyD4xKmN#Du zB%+ZaZ9V42>!R_}1MXG>4gQtm*O5q5x6`ji=N&(MLH-@)Y)*)|1{H`08yf=T#k9l| zDRi5b%}rd|EQq@bEw;ri6m##x`8=CkKakJg4qbQ^+w~CG0nCTyDOt#UfTwcGo>y$w zB#(-{k%@FFE&!z<#D_LTY`?&w;U*k*;N$&RHX50)%S-j;>mt$8>$bSAAk-n-ucOhX z4y!=rd*(*V4NoU}n5QgvKdfiYUx5sHWLsVyd>`39ChqPo_~X~+rT!?@Y&|2HTvRW$ z<*0i5d{WMw+r8Y@~sbACYZOPI5PbI z;VwX!0q$ZWXlUq2q};GdSPG69)_YJMnBcH?{Hfq3OV}_0HX*1eWTyu54d3 zUv4o|?Y3nRGC&%Oj58iX?-==31zG+9CG?8aqLgf&Bh=;0dFC}SFIVoX0eY=&VSO(v z)XobSB^9RQCxi!Ep3}7kt26!|ZMg8GxC)DF9qMU>nbtPY2BE z)A<lQ8#dv>A5ZmUuq_$TlK}}9$Zx5uaY7Fg!0L};*>H4B zM@zLo9hCBAS^7PHNRLHRb4?_!5BBP9XroH(8eG{g8G7R?DXKeYlmIncoO%LfnU6)f z;T7iT^H5d3%U~1oGS3^a)W6BNewki zLauCs)lEl242L%NjmQjPZNZg<-DN%9-ORmNuQ?Y>xQVK~(kRYK(89jHzDvT+fq?-w zru`VO_=ef|4^Qn(q7c?dtOIjO`?8#a`3X<*uuK8<6^IR4)x?q$gH2mGz(s6k=?}5A zEz<*;5;LlLK zNuEv4uZfexxtw;JCz9taB++PVJzpag()ZRShZ%}Ran;Xy#?yO4OSlyok;P!>0n>IP$Tlq6derdqEk+YmIZ z+$IgW%C~MMi4pqDGa1v?Zy(+Wub%_;47@X9KJd1Mt?5-1Qbpd`7k}!)Smj&^&?od3 zTpL_qC7@@63|7*CyFZF%lZlyHTBd~1(X0wf3$L|iNRb9P4n?BP!)hm5{LMxArU$o~ zw>?;7cB3o*W(&t5-6@nlnP=|s{mDtg&ItQ{(=Nw1%{A4H)h;Qe1(_6u+^K7Lit!`M4MMi zP}Pw-aZbS{HvhJ^j+YaNdrammFe%MfeK<7x2j=#*2Ua1&eDMsD`n(wy~oO(cq zPK22?cBg1yhWuZLVDHo!KmC;dJQ-!~>!))s5-$~D%3?jK@anSmxS&U#%^Gz&aRm6) zm>kPVHGy{rn&{?2LQ!uln2HjSMWZoyiNTaB72)7Av$4K$Dm^vd+w1hjUvvWIG0-Rr ze(kMbqa{$o4*bGb$70Kd+>;4(Drb{MSh)kW#zh#Dh25X~b-p2^djkFxh)*}XJjvstSK9>`=71(SS|pV^LJXb+oc2d=bGvDKI~oG@wqAXJE? z<{R=-2ZApUviUc432qQy(|a-7S+9%gx+P=gK*BHM)qR0MC{{ z>XDJ`jH?mBtAgwZ=%C_>ccGSxV=X6CBB#P6$RsYuC&b#61$>w!*Z80qNDaW8P6v%+ z6)^ZGEPW;hpuu=!sE)rxbJ5r|9i4 zj_K=$-4<~$xVUbn$x!=l`7`2*XLRNTGfZQn%p<;?i#&7Bc7B5r=)JO&c$avtx8P4* zKx_Q!ELX;ln^O6~v(HrM{aIzvETvskmbs|en+cArpDzB3!zBfrF!fh+a=97Vc!W%3 zgS|5@D@ExuJF-kC%*RxN|Nk`Sa?Wh9Lk{5Q2-_=%*>b{U@iDJJA2NwIxLKu`WEy0*)>Zp59_PItG*iC2oGH?fXXTg-G!VO8bS zH0`ijwO!r@TWn22>Chg3m{?Q3ESN#BGeOkWMTUcCWlS0gHAFUPQ;rTxLYKA075b=8 z9)~_();3|@pT_zv&0vrFf5sQ!v78d^Q~3lF!Ey!8u>i^7*4fC1b-^K4f-XXqSd~Kl z2f=J2HL5Mjb>#!apjj3o_B>35P1$ynFzX6yih6OEd&VWpwCk>0;x{hbw_0<>&0g5+ zxl+sQMN0uKjQN!k-Nd{*90(4DnH#2t{c{||y~VscvxAZbgSmF6Ly^wqS{&}ixWWV{ zl~Shid+D~MqGV{A)l@&)Y%jWJbkcpB=xNjF&As%hX$j9<4>2?3nd1c-;27yun61FI z4ii)oPnQcgKdaa#$^2=#M7D;b=7J9g=|mva`2DllMA#$a2HoI>JO##rN~^2Ym0nxt zr!?)4EY)X(`tq%Gcq(zxLmi%#`Gh|_LT99Ma8k!Ku6;|?b9SobsjS+;uaQ*2yb3_y z+xn)NE$_cC{KKm@BLll6x}y)q`(%8N2vFsHQSAp<3YRNd`zJwIF{tEo{6 zh-;pYh@*tsC(paB*`7>ZA!WX0KH+pSpM5};57Hl!2M@ka%-WRk9^{znNk=-ZA`TI= z4r%RY5Km^PaOj*Ij27gKUb#tV5DG1dTrRINR~eJWQqa0!n>cJ`PEgdDOd*p-Po2r+ zXL~Y3*`A_3W(nsa^$so2paFV5FkS@aAqR_R1l=B!6~zCMR)EL{RBkm+!b&0QRB3ty zal-7RRAT)DoP)o>GWKzX3dZKcfVgVPiHGx zIXB)L%Fx4ZW7gZGD$13sW_=>|de(^M84Ua%3S~jWlK~ir>9d*J2G20JoEPRrTVnA* zc)GSI)g$7@NWcYP4x7IE7Dayeq3|t%Vm}byWgGQl4Nhb798w4=_;M?=*B4UIHEOM8 z@7{2K*qzzf`EqM(G;5ig;=pE|K=49J>I`MucJbu_YbgKUm)-1+Zr7}?|K2<1&WJjfC24FmKt2zkJ$MKe6 zX1*S(FvoB7N)eB56rOFf3d&!6QB73WchBVa`u#E( z4Z26r-4khVO# zB8E5uBOF133RSYZCmpJvCHoiVDSN>x&}8e{QkK zqfQ%f#S?l%#2#q&m@}Q(qE2|^yz`i83N!keX1b_`+1A0;_M4uN@EAZBB{(m@np%`+ z9^(qS(EUsogz=O&b_pr9gwqkTk&2|#Hjh1#EqTrJH%k+`2e%}*xShY(>d!~wGxP<4;k!K^0H`_dHc!;28rGUJ&ZL17Xvz%nzcP=wr+h8p!4T~tV+6_I z8Wk$3FEP#cM=hx&o+;%0(XBa|AwtsDStLo>OU9tz6rBwcA2~|({r&Ic+NJ(HMVS;S z#|o37*9zjt}M4+=Vf(mT#0Dgos{J+={xbdXc00E^3s+bvoL=+y?7bIfDIp9bnh(vh< z5l&HAKk;0?{2StBW{1*tr$vKMjz!V@eLTpVs7Blms{XcQeKnFGs%FE3h~$8-UdyS; z71Yzx4%5I|u_T_G(G-YYo|igwxr}KV^Tw~fUT9eK)zy19H#IT4)C#*+u5}4rJijBF zFiVn>j3^-@Yq=?*D=CrC^0*?cX8HV0s58w1{}_A672Q7Ru0cPu{LMN?_%j#yK0r41 zb}O+ymfR_z%*Uf)7&rkVUx|@n_+VVEzn98{lGAlG4DwaqAdn{c5~^ICmaVkH;006Fi>npxyDyvQnovo400N*(~XZ zhU^w&q@R-cZR@;dWi}g8K9*XWVa`K*=0EYwqD2+C$2uJEv6r%aTWz>6_tec!sO?ji zi=$(FzS9S);Y{p1t~#*q5JBTK2YgqFzHUyHL?&c@R3ejv6|th)!aN*LC*pI|4HCom zx0)ppnV;CO?T#L5@Xk$|vk!MOU!wwW_XQNX0RAa4e{Z_wTqewv5eL5bkyyWQgUC!} z)R3pKHF_h{+tZHt?zG4i%_J>)S9{s)npOYddxcR?gSkKZhk+ct4g4Y7Z$gDpy!Mst zVSf>Pbe!X`gFK%?;hH4V_%`u5Cv4YA7yUHHB*|ed*~#v2+{8Z7wTGD-yc0agfxuG( zwr3yjr%JBqTw_)({^5X97xt@(i|?=VMykZ2nr@pT(Ur|QULjS9=mP%Su=$5nO6qnd z>O!z#wl~+C!$5gNE?0iO55DLnj`o(~;Y(>%M4PjE&8^lPDWdGvnkGIKwl6qW(f&K8 z)*qIWMlbrqXVh0FlOV&DNZl56nignDLSq8Lhz=>hW{RXwFv%HwrDZk-gFN>5#?ylfG3%jlJ=ozIkB zo!`(h_t(VZ;dD2@+pIW1UP)ek_0_LmNgg;r{3p|(vYT>7UO=OtyHI9Lr285?={-e7 z(vUVA^fqa>R;VeI7DWpY3a@+3N0!##nX7M1L$B&(%$spnMrlgy-Fwwlw|~5{@&4NnG)8ToS6$rTMfonCz>kq)PJRk7?#(@j!pDxl{7~=bV>5 zGi4&Q)A6M*o$dPDi7CuC9O9{NG8EfME6iaMa9AucElI|uz8VwFFH*Mod!#Nev;Ppy z{D`={8;X(mf)Z_Y&7_@_yAaZ+`|S(qLx-lb0WO~Z_${jkF(R=KI{Di{ekwRB;63ic@3qYvU*s;SN0&e_FLo*6|fuB#aPlIv+`e4t{&5j_0V}$APWwtSx znrEQJ${!Dy1Sw8UH5f4B!UNv4&M+MCC5Ka#mGQ4mne&U^C#tKfIZ^78y3IZ#hr|0X zS3Q50ala8U^9Pn#{AZfcHX6O1WA?vnP|gz2(jh(3!R#kq7-$V9enqRTj!Z;tsutBs zo$^ek$B>J5+!j7!MtZULvTj@yy1_2do%9z@vuw6aC{3-g5 z$p4hiWWGE5;H8%Wui*K&#{dU(4DyX0$QE~3`q@wM3-}LUBe2^r&M>FU2PTXfp!)Fh zY&Z4cOsPLJmz<+Y&JPE=tJMwQAwBsZCQGd#mCX&&6AsWTt^j zlh-Vskt@H8o`%j(bleuth8>i!NuJe4@*bcM-feyx%!pupT+ud5PRSDw1~0DMJyYgHpdMn$BpN>@hB2}#0yJ)Ry$<53By#d^HxB}^gCfPo*XjejrXvqGpzy7XKb=sx#4Vauni(qDDUT~CH+E(+sov3p^>l*jaGIj(P91GEZRzQoty?|MtM_T0 z7E=EC=hRB(CnKS#=I7LS>=%6bBH<%*G}rsIkTI#QW*o_KT(t1byd~luKn^08a-` zbgi(WiHp(f=5r-WSYWoe^1TRTi>7Py(9ltLOCT42mkQ7_0W#km%BH)t2Ikk1`iRdf zMkQ_0fRcssQMAhU&>HnQPehx))qIxv9wz^boTs66R+Uw0))TLF6nv5MG7`Nj)2W)9 znzmR3C7WjTXwb`i=Usd2qImtBmDB%H&+mLWGjk?G&eZ3vbL%%Y(W!H?+;}7s=Vrxz zX;fi^lr~F)$5|TSc&1XvsL%ihC)l{3mda!qYf{-Am zez-Z3Y$UFF%9o%>l_q5g!J-<~lgu0Fa%Ob_HgaFV{25sbXbZSn$vkB8wfI3i-xLdV z?NNKJiIROmB9S+bI&4uSK zSVw%CuO?FUAJItw5Wn6UcqCv!<-8rckLoxnx+U*E%|Wb{It^MMp<2> zwAJlsbgWsda66tTzu`A$rJ9R-S2Tjk*>L7g;W9d!ETNB?r+0-cMn6g~TQ>i>=bmGJ zTsjIm1J_}10zU=kl`t;S09c`)b9u%5oK7l8zkz2m4iv&-e!&P38;au|LDuKP)WXdN zV3=@50-nSjH}Aw?5`07t{rZS=C>~QriJkXLoMBaNKxK@%1Dd=_vc&(z@kbEX@(@~J zq1;@QdxQDJ^yk}Z4Cw14`ix`yUgMVMo8LMjMVxgT*II%v|7cgJl-jqQX#?>>TYWIS zl@xnj={B>oA)Qu4Y_>+H*OuxbB@rWYd|f)dE)!Gb>oV)c1fG;9r?xrM&bY}BlMObr z-VjX%EZcPIq%2y=MHWG)-VC)2+)s#6r^oQL;%Qm{D@JUL;cRS=8~YV75RO}cgK(Y| z2*tj$k}&?`K%MbcH%zAqEMNE=5b##vKW62<8zsZZ>H0i03!?*4KCdRGbG_y8>hg9J z{x!!JcH8=-MmgG5zFm9XPCb1&&HRM+zyxw*0T0?TwD8# zm&v}{*hn_dvy9!G4AiO7AamnjqSwj1E+L+6&bw1vNRcI&QORog=0MmV@5&3iGn|+$ z<}~@9N<{`s8GW}a)s@cY)7$H*6xnoUwB4k#N^;7O$sE^Y-F{ap7Kwy5kamG?n@5## z;znYOkMShrcq+A!ELiT!s6%OlP6Z?83Fn?eh|MaKqvZ{S+&ubp=`6_vlgga#&p&`GZ1{kugJSc8)qav z8J+!EdnQnbr6TcU^XzEO(H_f{uR?vyJ^s5&r8Ms5sfFHhoa37ga^`@nP!ehyc$yjG zS5Be>t5APdFm<_IQOr%O9R`{Jk_ZSM46%bct$b4r%mqsdSWd$V_Ty+OoE!iTPtDvE z&M3W)j$}yA~PK_E#f#N^cMI+6q&!J8_;t;DK?ZfGCFPc&wg-YnkHRw`N zLCSnqB+e}`98{kCAYN-EgOPPIIy{8czcLUlBhJa3=!WN?r$!TRAp3UMe*% zSiBQUh2k>|BorKYH@YL4N1UzDM6E`_8{EOdVy7LQcisaXRNMXM%g?{732>^;W&XE$ zGw6gj={xUa?zx;80LYDv%#BSCo%iFL#52taS9A*{cli@(!_@A`#;nBcPlV0g#c+9@ zsXcMmUC9npO5YYwbiR?OuWxT}ZA~m)dUK>R5oy(yLFNfv&jy2wh}J(BY%@W+3KWKyno*kPG*ShcSjSA zt>$pbN1F38{x{`n;-~U5BEU#+0N#`*eoL31fi}exD32jTAdQJ=G==ymlMZ-?fPw2$ z?V7o%RI`3gG-HoW%SbJ<@8it(hYrEM0S+2q?hM`0($aEn1H4u6@qPn(Ybu+ov%$0) z&g#s8jRTW1JM4)j79{GLR`^(FtQGzJKHHw8e)Xtp+JZ0#5fp)(ieg<{(M`;s zcc~y_$aSjcwFcKt&qV!uXp!HQ>((tgeuB7}nP!-)TzTJ!f9&@O?5J$pmO*b+-hN-^YEh}PK-{6WKWl_~>QMX#xs12^|Ap6aL%W?qx3|!;D z`x`KFDByFjmXx)5ARjpNV)+Dra?%A;Luhu&2g6O#kO>0D0bEdUwy>2=gSYi3Pr#JNBH>25TG zISj!+@yA$waKSRYNuM@KROUWuIxVvaY;(PC+u$6f$@Bnog87gPWOa_Wm?LO6=CWg@ zor8mCJs9hSJVyS+F}99@dq3)bQSoPe0`_R9dr@qsA@t*&%zUvBN*I9+-k~)UCRaf< zz+*!Dv-J&-9@}2sLqn(ZX0#32KB3=#|Jc^BA|TTXWnBH4>X;~|@~%Jj3Gqq`?FpZs5j$;} ztj?$;Et!n0#T@MLc}?M-5`bQBZ^@LHwJx7Dr?r5q(TKh)Wm?MboqvA36&fQQ3cCoF zQW+XlW?Lc-myF{G3hMclens0j4&WpW!79g-Nsf~_LN+kPDGnAXZPIjWWfXl}yns924+IZhF!oIZw2cEvFRpSfTyi801-;;eK75*;-?fXE_s&BKO>nc5AFqQO8cmT^?`v+|72(7iz9&@!vsdckU%3MVybiQQse>4Fst?kMT71!OTr zCL)pDkbG3HN;&%zCr(^Nl0mFndEl2n#JLB0o+w2H5AGS_iRUN|5b zkobX*_}$5}68;yWlZp8P*5AZScpy=fO21F0gUK;NL9YJkNIu68D~N;72Te&`Q^DfN zwr1<4iffq93xGfOU&Mvn76qHjKSRSz{AJ=`e0tYL_1^wj*Bf^Zy9yQ<=J{QusW%K6 z)p$6vHLY-4(=&ClRzo_S_8Gl{xvXa44!+k@-kH0c*z%{&?CAUH`RUiF1q<$5)Z{nk ze5p(@>j+!=)5+vOGTje+LqmS>FxVIgU`YBQ3O~(Xf`ejJF_9m{oPZ;;aSMZsMHsq= z6NhRq#>AjpScJnV4ujbzJfn-A*pd_ za|(}oYA$EfX`=RY4BAE|*1+1hA}nUUO)O0&nSori=xq!IqxJpu0>7S0@!m?M`o{N# z)I;2g_jqA{iDI!nPUC=zOYt+WnWF@PfR2Q-f{DS}qw*P?8d9bZxq?msih&M=aw!mx z)l6@vv!39vDF^*@Q*NeXeZL!wQVQ5H@hBA;tPeWgIubaSq-I!Rqm~=1g9dp3QXhdA`6i|s2-PjxqV19OSzd$UpB1qHR^$Z(OPPll)Uk>4aV|UF;_pZDmC9ee(O)Z@u4e)7nxXm$$W^ z>xnAl$j#gxZJQA!j&l%E(N`<#w z^_pH6HiUy=6Z4>Gcrc z#^1~!5%JixOeC^{){2rOrPB#ABGVZQuaG&cWXP?x#0))|`$t-M)}rUC3+j9xi?6NO z7M7GcN9(fvJ>7lWaKv@Rg|$AHX$@s`N*V_H>T(S;={1&(nmJ*csh0*GjYA#<@6A@g z3}p%MLuO5xKV=!aF=;Y2+1P(FIa!9o$_qAO z!cRk!}N%YWUKga&Zrx>Y62RjWm_te%1V z#o=eT&al0Z;E;&(<)1LkiW}zqM;0R^ zV&+j*h@az!c`i}-b}BA)KNVT$ zm-@E`KA1b7+q_r`HyQ66x@PDG>O>opbkfyX?hhnLc}hdn zRuurCF-pCX^%u4Fn>|K?cqx$z1{NnZR#Pft7|8aVr8HYoIpa{m|5rgwmUfPKLY)C& zZzxk#IfRji6FobWv^?l(RP-!zG_AWncdjn9+<8``G^y~lCrxBkmi%#f9PlwP4_885T%de6OM27O z^*NZFfjrN4hP6FuTkPEzEumYpM58e6eFmF?XhW zdBK}jp&V@1yU1E}g+=B5hD0ge)I>Z!QV3ByiXv+?t18&@xp`wZ2;G6~bh~v%p=o+A zbB^DblIYIczyHXQzoJ)PD(0A}bf&SR)SGB;$?rdtrk5S8M?Lp0%IuiFb~F2i{jfZ{ zR}$l~U~CDz3uzk8kfMs`F*$iWZc9KfP8cJM%yP=quntGX8v{gn@IKgOqb$2&rUl_2 z_s2N5J#17*O(sdo;LW<@TB=IM%ndcfVpH|GI??wBBXv=s!)EgNXiLCAjd}<%GX|(` zdUPAK#qBmH790MW*7^M@1((vTRdm(>L` z9S!ctMoOX8XYwMOA{e}2P9qd#uJd^F>qgs;%x%d%E8IP>wpAoVG zyykkeVWxWjqxy)cDXnS{K-^D(Z+a`x zKmuBH2_S}@u0ydkt{gm#Zx{Z9P!-=Y`{8td6P$o?m!0+7!MelO4#1$Dpczk|O2glP zKMDVUR9T5MRAv7XPQ{|b22w6ToP(jHS3&G}L~jWCsj8sK?ay4zcxpw2wET`lijK7` zTK$y#;|nhQQ2FqN(WaV?t?t1!uD8CC)_i<}%d4MGT!Lop)YueVN>i>Fqs)d0;^{;VG*{+x^0yh~$pt18i@Z8aqmCcBAKovK2%a58GQM{R~kTg0p) zEz!6mtFwpO=MRRn>)N6|L)7nA5otnLYx1j1PHo$$BEdXMXKQ$a7E2KZ)^u4N^Xpt5 zmtI&q^)}}&=&8cJVuPS{=fl>3^QP>WOk>axm>}v2;bD~xP;v^k+LJQ|*zWADm6fkBJv-#89Zdr9TpR-wE?F4Z1?U^(D!?B%12QAyXmrE`&DsGJR zgp1RyX{09_d4j5+l$XlQJO4kvz5_mv>RR7-R?B}j0S0V3rkiR^hfqWBgc5ovfd?dz2T35wdr97V33(}K{6BYgWy?<9e`WQR{L{JT zp8B2dG#dI9F`2Yk|7pHfWK6*3%Xq;Syxoa+YTr{}^TtLy@n|cT(|1~k_j-#V>U>7& zb+i4t0ZsSFUX7AwX|LMfR0{Dg!fx_GOCdF=n|^I0I5tAuRR#F0Cc@|&MsqmXZUxM6RyAx64!Ow z0h57kKb}jE!p(>2Tfy>IGBB*VC#`-pUxlv-nP(-;ommc_?vqvWawvPlJwadfCiBo4 zcOLVnQcKN#{&|v(uFT|AFsXW*n@&91zY#sKp1f_npC+enQ64&kzS;Lgcd_e-dkr7# z^_F+8YRON*=@}0 zZ;TvL2Pj~WjyUa=3~Q#_~gg3v?881Br!#P!16J9$OnV}V8n&^TvQDsbz= z4}UsfmLReeV1GhaMChhN{P=G%HWC^GFsBaH+?Ze+-|eJI;17Jjl2-0*utE;=_k)p< zJlAUt^gH)ol;4S-8yF~jR{-?(cLn|@-+lKT@pOAR9=$qei1`XV?sZM;c4)~+vE*5- zipq39abF%?#ovJz?OoHFx~dQGu*LBbuW4*Ve<+QQFXG?1?x(lhf_r&ssLLD`@(;Wf z23!5Dvv$nd2ezqV{ZBgblO@elhDLN|_zczr%svA}nT((%uSY;4pL_B6FPat)d{fsK@SmXOpCW{xSyIau68Kpk{H5;x6BXNs0uw06CS z?=(7(b1yGldUV4Eb}71H1MzBeCOCdbX9*RG_N=>5$U75}a@vvJlU151{)haUurH%B z_FsN^VgY9uI&1lLA*wLmq1Spc?N{W&-GA!`qMM@3ZY#Z364f62>2hDw!Alp_*A|y` zd%f*%_4f9@)opgG@Dxlva3ekm{U+Q)bwSow6#A#jaUG}{jh~Y8X|Xt{8GMc}U)A&r zX*LFI2%S`17KQM88m5EIyWmBzxTott!ihpx9-8u^TW+%`qUALX9}~m*tu;}-{gr@fv;I}iD~~fw2rzyW=dTZ zAB^-xc73ItI@0#S<+`u;^b}kBY;SCc+N?k^{QSsJ2mg^2sNp;+*IX**rWTjfP?2rP zgsB~z*5l<;?x91n2u&$tQpS){`b^roctbvJ=l}N02Oj)Yu2@bpEw#EfW6IK|T@@*t z1J;s{bHt4qPdJc{crv4VJTCLDK(MI+-(v~X+1>yi^(7F&4FS#pZ+zYc{HJGw$2~GF zV~Ef}ln}-UFQ@4!)O|o=!j^6Do-CFg)ZK(LfuE`f8UG|?PJ)XC`;X%pNWuXG<->3A za2ou2U_x})Li7)#NZ=?8MiRU3Qxy*3Z~~<0hUe?)$hP_b%LiAKxElMPZj@;1fz~gg zKxaqN1CKNq6}rR^`9%S_|7VJLLv(I;iv+!);)CcPimCr4&{wT7c~ttO4kj%4ca0=4 zW=FISN%S?WNLwcYKCgz7xt<|vkTuF|-!U?%_xa?hdZS^Ht+c)z&JC{|x<-?d>Ij0V z(}y$OM7U?6nPM}k_|QtdIL0b0QiEOJsQy0CTnkG+v?lA^ghH<^)*D1M#LrrvZL2c| z-`K6z%r}J>u)V7QN?oqFaAX zhS(mbH?hjce~cpdm3RwwSGyc7J*~0S!MxF9+ZA6&8-lugi<6Bsdqy8gbOiIsyxr?5 zkWWqBPh39r2S<~&Yu(T<8cQRL-bm>A7yCmV+7>4p(Y*ftG_(-vp|9{R#4XsCxB>Nq zdy)MD=kLFa-ysyND6(LoYb!zoZ51N3MP00~*Ob$NBWqu7cH72O0Zq#|V)-Y7K|}5u67fY7 zOD~ZvNCxgL@mmwRw9|`%^Z8qy;&~sLI5lVI*7FAvPKGfMZ;a5f(B&CT$d+sIg-W@c zE9~1aJ7%E|<ccP-V*BlkR)gw)SSxdgDJ@6b_=?AnXh<3h z7BbPEPS=@``uk@^ZBuDtaO!vHGoHMd_#^OI>}c5Ha!RKzBAS1j|J{wOE-Yu%&i&TB zGHOoUOuS9e@x&Noby+fEg->R8w6bi#>XGMc;k0*bwO$qbIK}_tZ3CTx=OxZ|91`%$BgJb7>ON2jc4NyHihz)!JHC>HecflX7Si^17T@OuW)vNJcK? zfPSBw@7$iv4tTc~GL{Ydl@T|;Aut&+#4Newh=TQRNjk~JHVGB;maYn^8lj%LkWYug zs=UF2&MwpWp}4}q*aNNc1l7Etk4Hp);nW2t7|WLoZF+A>UbBo7E$*$~R1#_tm`h|> zxX)csf2eGPUnzL8GbR5o@Ic^%DFSJOu-FYVlbK zXI3q8eoEzI%m|P)=u!Qp@u)5KXnVQ{33%{gsXZtwZ1daUhQ0m2>Tqbbw8Lk5j(aBe zXZ|}R<<7B6!cvdotndG0cC#lwDG36&^&Ap?_Y8XQIewi()2u0r2h=Y=!#zj5Q{W=W z4JobD#d2;R)3{)x)??#j9*?3q8`|2uzrLY@fs?59x9gmG(An zv@}`L6Xs0=@W{zh&(LP|=X5P_D!~ATSiq5E4o!@+$Gg1O&)Ndayt zXn_Z*-1h)Ei!lNWy0YnwVXCr$3J+FlJgW+Cg;oa$QP6iU1O=6u#|nzp4I%A}g#>rL zRE+99a&=9s*`D=N=0byxhYT=DEwC2lS~TlH8FGl@Qc6oK%hrfQMmqj^DV=!wYTFf$ zhWP)Ub)v!4CR3xy8{Ce_xel3MpEHDYzm#P|KaeD?!B<=k8NsjoD@yS^|L%WCiN6v_ z9+u}2T$j%fQ|apBK?gmO)fqzEoPs}bVNPfBa7siS&A1(q#5{6@L0i-(I$u z+R=^L^d*Jc2OSbN(-2fR&D}1iT}8EY7_W328epDlb$@^VPkqsWfruacQ2E5C;D@lS zz`SS8;D;)cLNjz$sBz4+79dA4NL38NJe{8us!BML6~bDQ750re2_d%>L@zL+VX(}% zna*%}sS}E6f4(6D80>Q%$kY8DMt?exaOEVmQgK4BLY+#&R>~ca?|}(FjELYrrZUlm zZTw&N^N(Jm=2mFz8LR#?QH=jjQubg_pLaOVXE5rDJ9310v%f2zytrh5dc>&pERG#^ zo<*j-$+@k1y~L<^Ds1J4Q>+mftHa#STDuCN$;%%khE-b8y^{dp~PAMylKv$uY_)6nT z4N(i=04iHat5M8!m|3~!Uq>55E6N#r?Vlf31RE%&!%G~0EiFsfxvas(_>u{W`CER%htVf9U#FH2KTWkW`IcZHEcqR@~6xzqclgaqeOlCZJSt=D`FICc(v^lN5)RATLZqS`Z zVU`kOBixV=<)N3e7;0n~pH{i4r>?13xVfi3cNlCsN0|i-i;QvNY?nx{N05& zR;NPoNa~4nhzh0o5z+cYu~XJ*F*jUX@7{9oAmBh&Zb75`-Dp%)15ryV&7?h<D>OfI%4cvD(za>NFw081(^gnms*b)mS%JsRtFJF4lWV)NCauSpIkiA%E zx~8~oTk)_#K61s#NX0i}4*FZ6PmAXn0#LD{p=J#z13_Xl0+bn|!m<<#)KWFoET%$1 zQ&ACs;o=c15MTxiPC!{;uYg}fw>|+o+y|V&++eJ%lks=;B!HY|82I;%zzUp7{_bJ) z%oXAr_}06K_sQ9GPmur2K{KHs|74U&+Zxeo)L5E&rPO&_S)08$`}e;S#S^p8ERiVJ zVqeT<>>IWzXj85+N5!{g`mt;W6hyJ_k&q}btsIs zHh6V8wX?n@w{&T4NoB9hv%;OiIHMNmTQ7tz>RGdP%{p*eydq5E{R_8ALOyr0pn;K& zAOL||g#p4$!2BbDa0N68>=JI?U@H$hl)-0W1{*MjIEEIoZ1^UQX+Wqr6~_VhCkZx@ zTmXb!W}u+F$*4)FVz16k<)s$ny+2T!h!_UF-fxxin^KL7rAzJB2J{r+2&b&Tid`!A zYZSzM(se9Fk%LH~wKPhm+)q8VycCrOWJJ$-6BFkfcg-1M&?aVZcoTmW)!9r>{YvcD zQYC=K<^q7-(Rm_Sm(5{m>|U`?ZAs_odS}s=r!=Hx>Thu$3!A}T*>$ibK(;IzNGtyX z2LDd@!=0Yss56=xZCUQnbDgigXxwWw^_uw=fyU-g-a~gT^3V<3{vm|*bSf3P%XGtakX&*+Bkh}fw2^Z z+fwA2UjgaDk5{z!(^TF!IH#Zj>YH2q(zw)PBX-c368FO=)at~h zh4qZp?@4$Int(e|$=e=<-ZjRNx~iUXi4dcl?k_8U;tVSY9#ad|MF{JJZ&ji@A#)Qj zQuwxFtQ2?!!3cutLgJyXKuqDM@Jx>$FBHc0$S>nik{z}HXW;+d>~Kdl?ZlDSyCa%; zOfXD$74p_6*RFkX{o}SrHx@4~PTt{uY~uZI_@5)+mtXRK`o!Q&ryQSp{9OFM_>YU1 z!sDZB-@3GTE%9|B=Z~$U^(GVJ3E9oFGfbb`Fp^I?hc;;_{{9`A9ZT6#)3RrO|NAE< zI`gd)=~j4YN!vW0yg$ggOKHF?r(2;p2d}MyulxuwJ(!Ew0U25hSTzQ!naKZ*UIrb5 zdnf1z1h;iq^{`E=z^6cN7|4Sov@ilvR*6`b}pB^9lv-rqktdmz#f% z%>`2giThTwBW&K@gQGCe77)JwL{V}P@5;BcBU?m0P8zK2ua{Ol$$r;?4b;<3^ z;FDy4HMnzYQk$37xw(Kmsg!#SEu!WPgM|V-$ke-U>gdi^$Zjh76io3 zPzD!7I(?nt7UMw<@F0TTK`Ev3eF3osW)OAzky+@tU-dJFZdOd$KR#1BkR-}*c@(@7JzPf-uHiOxr?*zzK8hw z@ttPw@g@y5yU6E@r6$ASAGfu1zjDFbmyeBKT!UUCeq3fqY8$HwxY#;XP~wzlx8U+@;;t*-pBxr)HE+8s<1ZS2+KeO#a@YFGQA=nkIwN zra5z(vdfp_{Gt{5;5a|VvoJ-d8Z3sGWgYn7X~|&wgzcOEqz0af>Sh2;1lLjh8AzXn z>n+57Sk5PL7s9>9rCYDKI%eR-!}91;fJ4D#Q2fJ|h}c!4oPPfOG#jVqTk{&#Kko53 ziKrwTA*_3i7cN_QM0eH#j*1(}#A1D>UV|)|VLq{MAC*&kWd?~&mE+3h&}`F%7ou~! zM0xo74e&u01UFwE-z*cP>ot&g%T;Y``n>D-$VxlfC3jXYB<; zC;+o?k;u-NS#76>VQOJlAx9TANts#dQSo=8!Oi`RnZr$*!mejR>9S<%OH4oE<#z6b z{Z(b?McoNHh$i#%}4t8$F5{XDinU)myQg3to);sH4J%_*GMcX`T)YuM$4{Ht0{-l3F4%mdh81%W6gM*mg z5@WD&zoFunG4==ZGGSiosuMZofzQZ-L49j0x3+q#;rl1`5oa8b@K%FHa5R{HSBRx= zdD>x5xah3fo%W?Pd6kmy2F|3!h+VkhPsF9Po+CCErp_*+6>3dc?nYfxpEv>~I_2iJ zyITnQALh?X{L`g-l#+u2mu-iLV9Ah^Dm#)kc8t;l9c)WF8rcRK2p?(ezWyn8*^N$% zR>y59<@u$kqmjyFj&}nvc{YD}D1x>|y`Rs!7&w+CfP;Dg>>R$c9+1Q^+}cT~3~Uzq zUZ*q+JBgDzCR|Opxq=S>H&TT+k~rYQuR;>bX4o2h-|-!DY4vk%G-ZM8FiG)Mjq8U{KQnhp(Pc*eAzR;aJ zU#uxEnRkzo%vfFgAJv`)ropQ`F00MfKuh$SP1?`)5}P*fQb!H>w9MNLoDkc4a)J2y zS*^pJPiQ@c{(sxOQ|Szvwo^t@&*i)8GGk}CyxvZ`$X))mzsO`u**wfd7qZdd*{k#( z6+4kX+L}tY@L$%EFq@!p7$wC$+65DD&^zMt=z;jZubHTuT;5)E=gN5hEym$I2{@c_ z0edZS-vqg=oDLXw*91b8$0>be zCwl5I>b{6q?c9l|!`wy0C5tv&B+5D?7m)|*V=8l&UYnMa4o|Qt6C2%O(>eIBoB0D7 zGNW^(ZoKg}^sDAfbF-#itX6a{hci6s zG}^BU+o=#hL9+{=0hkng5=`ogSW9qe7pu_nPcX?Q;>as`sxN2p#2b^0T=8Fpsr>~s zWahk*aG$3$d9NoZ*|1f6?#jZCkGbxAQ)YZ9YDc1c4N*hor~bk;CpsA7Dk@AH$~9m9 zu27hIqWDdb_>?O}=I=0>t!%et8OL=xRs>_N!AYIn^!z2m3yyI5zIVx7Fcbbpsd5`x zqpR+rTSTpA#n0-1EjV$h*%x&$WfnOnvp?vZ?T9TC^Ox-V*mKX8%K$e~KJg9IX-A;a zb&?Us4|N#$(`LR@=AIxHKV@SUyVEgR{DDm2J&oSCeFp-PW3+!3B5!Jq=cO`1j?7~Ygy@rzfPtYWP zGiu|XO?sNl1t$u=mz2ld(gt7B<3EgxJ$%QX5dED#kw`}ASnBHJ5-Mmfa><~7Wm2bi zrOc!y7z|QNl2@lQ$At?>Ml?x%X4^?3XedmZ%m8>11(G_F`%fF0@wkLep!NlUyp=;eU;RH;A81OX?5h__?;Et1uK3x8kX$3R@bP|5Jr zv#X6hAD2xvIpUqBi!RF^SdQia)9yUt)h>6i88~T~y;-d{hO!Qs$sNjdr@dzR@7F*X;eW}l9(X}aJNDS^EAXE`Jt1F zC7P@`+-|zVBg>e{uY3_>4kpE95)^w9)>t(5Y0YS;Jhi@zuGgc>zrP7xtN#npUF0eB zK7O6r-uiqA>VBQ*FW-*n4nIzO5TgRr!mNgLvQdpOknmBxtg2R4=g@GlPjJ}jEnGc5 zel>r%nPr=inE%^N9GA@Uzf@+yQROs8OSWtzUCBA{ToC3>_X6Iw8~7|nt8=M>jh}I8 zP76%1#{{1#+#cMvfUa95&cjg(mIt;i@QP6-(@7w{SMW5y(D0}_VK7H`e^izwmc|X1 zxHn}<93fY0Yo%*|-R&TKIoOK!=U zUGD5+yQV1h{$?}nh;Ume0M{vxQK&d{WaDL<96fn5h361tMO z`wQX&`G|z*Sm^RrgF1+5L?GWnZ41N?b|1VUE|mQ&SxQh7z&fm(I-W~+ zz7%RTmY_ovez-j~G7t#9A06&7cP@#AR`pujmq)H5CfF0R!eT>n8@jnO9I3hDcJxqe z4#nSf`|TH^ZOxr@&^vmkBQQ8>o4R8idpq$=U%NkbO%w3(W=HJXma_+&)gfPY(6zNY zC+ZnyGSs4`(n^*|uI%Q0Hu>(YZZS(Vplex{zoHI;@A|PZg?F8+dEA%H&!6qGQcX(} zhSYNZ0cAbh!b;unZHNc44!Q)%VfObi&_bX&JehAyTPi;m6gF1ahJzFkR=sHkrs=66 zd=pPj4pj%XND_e@aXUy1a>V#1B>bvrtPc93GnH3pQR0=joYFV8T7CTUQHESx;GnV~ zx`#{ZIV9616NB>psDppE;gWq{*KQj4oVb?1dqwt>!if91=g>y0+da9-5%lpd{t3nZ z{OA0i{*?bSzi{sn!_8|^Tg#n{vGJ}far@9>*i@2EDjn7qGClqosl+k6kPbL|d;Pm^`z6va?m;haHWCEK4fNy6N)rWodbD;1{)M;#t!=%O&~ z-O%c_`TGf(q0t|)q2p5%$mJZ|*%S;ncI6Dk)vsOrMDcJdSCZQ7=N8wk`<rl&Kdi-b;^P(cuaj#<-Mn+914xEMXOg7t!O zgKrV;H+0>Eqo^h`)nAA2zYp#{m=E;voc6rhjL73Mq?Sb~j=4c|Fa(r>1^LT#$M^)=rK`b1{FyRMQfOb8-GYN6r?}vCgnJ z>5YV$g=Mot7a26dYc0D6UT-$XPP% zI}{tF=hLlViIrM?}jgJ?ffBxp7dP&{?uMc=YI#gVy=S9_PQ7$(A>$ zfTb!MrT64CAupTLS$%CiOZTf{E-vbHOFFDvinq;==*@a-<1_Dd_vX9lu&i}ObEI*y zi}S?w(Tu;F-M#y_Hx-O(y*?ZsIOm+yXqrr0(zBtu3vafiM-jm^lKtSvN+;fd{td=? zkyRfyU$DZP{*~NuRs#9-w7z3|0ah7-=ZBZm@utu#tsWADN5a63@Z{-GfCT>sxd~XA ziq!yX^Rpzd5iGXj)u!TCGSajpVI>}T7pWu`8RaZ6Ry*r1>kA4!zn_2`Ol*|ZTIlFb z$p)88RYTOGsq5Y|iOV8AD(87G?C02Siper^%0PG|URgqH5G(5Vf0r9%Y@JQ|mDD3k z89c;$O|ZLUZ$@PgW?Iz_b=jNAAg$b~cE{QxL0fcXTm@yl@kBg%4UBZK4K}-$(V+Vo zom58u=OxKxG@0Z>k)@VLKWpM;QJ+vSI$guA- zf&$~d1I`(ZM$sK#s6xtwvSB4RdED=DG)rb;lJ{l zS{&~7==s11l5SZ$HkSV~JMmc&I8i4g%biy7?JWQOm%=008S+!jB57We(0O!SmQ2dW zEX_iu-g*V%tx>QGxK}*@J=ZY-o3r&_>FG)PfV~)42i%IOTyuQ6 z@vGkZe-wgo2NKsZgo-bGD2$Iy#~NO}mhsw&6!T#$2V^=HxL`>9qTkFY+kQPn7bGSW zf1^DagQi$`)iyqO*5qV%Keu)*x_Gy1Uwg*^$Ii}-U+(fHn$%e+S!nv?=`erx{{6_$ zw(9S$k??vn6y;`1haK9l;X%An%mpdvJ4CEp)Z;J@4lLcPHpaO@cP>|89-?4KrVI~? z?`z6+4b17}%5(bm8X|P=%05Xtl^r_*b-u0}=PtimqT^|_gZ~X3=Irje14y0AMJqF9 zST~=7IIP00)eLo??Jy&Qt?p@4GD6&S%Ia3aSn-+Vo{GH{%D&hvq#$Umh0d+8YXr^# zhZlh65%{Ppd4pge1ZzT&xLA)TyV-xnq~k_zWPufCWM`c?8fQwcw6i6v7zH1X(al`} zSCczNNlA-lj2o{t+fqKA*1w8nRmvypQfPtJG|!Wyl<6ADB?h-amyvqBwAWq#->Vf4 zYfU2N$}-}1zFudNU!yjtpB)`NGB(DIqASOUA9XbbsmpQ(pEuX#SUGRidD?riD_ZlxzJlmr%M9IY1W$O+RwTH(K z>}_$EH*8w;lG0slsC|+j&8AynZp zk{VA4oXjk$HKG}6jLKpGdX?`2JnEx#@ zzS_4#zkGijSfUxF_B+Xwt7G(%EIO2+92cBrBXtt~TcrxU_giY?h;duT8%W1LL*GjJ zUjXXpuh4&g$^ZT<`b*-!3ar<^lGa5M8S9+%aF0f5OElXig2Aar!;z+@NcjDb#_LO4 zlb*QBRvYOr`78-jz1bLSZ3-A9b#)e9Jebg@oTM)FJ_>lfR=?MNe+WNFcz}qZ=g|y3 zq~oCXa;U>`LKfT)wRv1M!S&D6@f;P{hM=urI6(WgC+p21UNFRKj$YT?f~O;HGFX0``2I3hul`PctGVs7jbA~7Qkrm z5XlRiYlKdcBkOEbVIV_=cjWA5UB1~pIW=Gj&1;Fa^m*cK>k2t@!dka5GjeTn6Z+dT zfj#?I&$hR$JFvBPczBq9(!%W>Ub~k699_e&ji6b%T&z%7EpT|%2)s;p3wGHHx!c*N zbac9EGF?XzlDDemm{oCVAg>u%@sl#G6SNj`5Ujj7G{eG$4+Wb7QiL2>_ymdyCox9M zsfhZ$7!wH3ac7|vRe!HnmC`0Z39#0JPK9Dmt6j)FDyiKOma&@L9fkDbyar0brkHI>)M(=8tc_F2v&KYqwS|Gs z0z;;~<|PB^_Dxz_XBx;4{lTOqqt1Jo`QAyRtxIQxZ4F*a*=Ost#(bGxwvEg(tle*G zO8s!^1!6ZhO9p+=9}ApVc!tamUa?ZmYJ+?lS9I3TJd29n;_{zRf~ZDrVyuqpijO-P zI;b(#&&=Bowwr}eO?W%qKdFJIV{Fbe@RDJS;-52(-o#=@-l+cDu@uupT<{2!YU}d^ zlYuNi1=Y{-|0tk)P@^_U?UZbGJ0<+sJH+O<`33KM{`U7Ucv>JY3{wd@GWD|6reriW zBRUYzwxy#vy$uABM8roeTr9E!LK#!K&9)?+ZnG?6)1K&Lv&!#$kpCOMjkuEk8xl|b zGEQk3sm%ebVnukw@teQoa_&a@hQZnPfk8df!6-|;Ng^0zuD;Bs*V|)~FwQ+Nm+3Kx z({Qd=g!p37ObJfs9;X9nuu{U!PgUv@AY}%gR|SDU;}etb;ouIw2FhMF#t^!}lk{#9 zg65y4iqY7FEf1xenui!Et+fV;s~!$3Xk)I$Y0EUjrdj0=iaU(yNGufmNpWg<;cZ(y z?n!tq2gbU4o9{cIyYRubV(aY}>d(Ki5g7Wm{yQQ12JQWj-;ORKKI_PbB74&+mz8Z) z+01T;wP>}|*^=@(hbPre^F=#M5!xH~9pA9wI7z#_c`LssT(ahTE%D>W6OG=SJsVE9 z-gHyz!>|#OmfW9x@x@JgeL87pY8ytgS6smxC(xCv zi1#`&zSw!JT%Rlyw6o_eK40#RWH>{AM`%Hg?~S?uE;YPo4>MZu^ljR3L~5Yf#jW~~ z_+pJsPpY*{uaBVz>SXzI01`RJ=*_(bJtY3y+^$`@l~JFiZ(OFcluFr^ZGGCHrmwFL zV&hh*|KD0Ymt3{WTWaWWosuRL{XuuDqD-+um<){pko9yYZ@NV{ z4Q0Z^gYT#5KnzO%I-pFwHIXV2hwqP5&7)C!dLEsZn~xuT>n-L-Kf2+DC!b^5#%zn;ZaP!UlqUEzrEWcqnv;3*gKPO)9ViGhSBlTsQ9gA~0&OHjO#Orq`oynAm{!yXL2j!Uvw!-T=fihmcJyixec zX#*)V-bhmTn&{Ra^~ZFb(wb&pI2q_Pc-N-8H*I>d&&x1{sonOxGNUZo&}%VM#^=07 zGX$*A;{ z`0(8q{5`mifDr@;IL&HpyCQOsEw~f>v9NOWl>G!k0v~vL2LA-U<=DDnF#o9v?uXHA z0Mga0)WRRLm}zt9&6Zrjg#yo|QbfQ-bb6g=zc*i=`g=Ld>E_?o%)j%E%SJ{P*WQ92 zKGbm4a|hI+c+?*FHUItQ9*(U@RJRC{?!D93QAQ@*BoS=CNDv7A|1@~H7rxR(tfv- z8S(!%EMFq#|C(u8v8*Yx4}f*Mo8bYr6!+|2fVv{yUyOO7D{T8%o=~$Df*a5^L3coS zVEn2U626Et+5iqfaYwH5jZ=3P@URtMBm4mVx)5;UI}VNr*8>Eg4Ja?%ZJIU@S))6RsIwF%TN9n&*(_B^pnotEeu zcKM@@f=_#(Zc1k!Z#C!V$<(%}LR&9tAmX(R#&|gI)|ysd^i+fPcGu?3(Wdz}I&bqh zBF);>sQY7M!bs}WM6eDeiTc{Qpw<z51;es&{R)TaMKoai%2Z<>rLV%-UY4<-ax9 z$L3l(M+|zcLt~c`7f);YK0 zu9LexzPQVvpy9uFhoXMBOs3P?8~h4Qz}}}cD}d7Qvrvw`^2du~=}9IOPr;6YV?){O zjcK3Oo*v@g_DEGSpV}Qo3OJIujfD;rV@T`fDO^RlA?ndl4l_-Ly#Xdlkyda53S5fU{NvS z+DhK$1~N=s$|T-4dVew%{%Kqp);4*fO`-I-GXt|)H7~b@OsS);0+PIMreX{oCB4Z& zm!6gB_;#cO{+s_^=`Fhbfi(X>!4pvS)s=|0BA-ET;a6h7>ig;|T46JEG9@6?VJ%kJ zs^>CgoUNC0=9I6HE=PR{7}^99i(eeX4>#lYhU5EKk5ARji!pO#=%#WRoFT+&m|x<4 z=m#mQyc!jW{FB8NEQ%1TRqQpsJZJVNvC}&lah(w(U>gOc1Z-L0wbH4?CW-}}2T*}v zXTIQa!RRDRci3i~IMz%LKQovq`w)4o5FZ=$Mt>0=Y_T+thC@p_?9IzUzd7`yA2F}J zcIeQ<4>R}Qf9N0H5T%MJ`5i+)AN&L@Ep(6i2JUrl@9|9?9AXB6EV4*NFXhy>1lRA` zG_hr$x|8ehtS&Zb<}6CF)QZlIjU1QQI>2|9a0F38H6cpoFz<_OF z&s@9)br}%48%ng9l+XjN1qN8`Ch>SHs3G7h{0#a!m@}7wchCN^+EpL*2a7H*|2C6J zP(#k5T>rP*?9SNSrDBSXmU?78R+D&@zkX+f?(DM*n>I~~(6{_LbakHpC9?0Ilhupn zv2PQ3$E~Vr}y80Uq3l&&&gnl^~%~hkyz1?iD=*OrEPg9irufUrz568j};xr@n_}# z(^68)Kbm?*710*hW>=)&w&y@$>sD8z5xvYmg>Lz&Dn$0k$~KeuXR!P4#{81dNF$=G zbL|ORkCXUuds`@ZK~ZB5XT{Q5iAnD2wsmHfH0xS(+OVhWDn);@b`83{Pff-7dvqxS z?EGB3qrXHS?kXKTSfaeGw(A+@g_{FG4cA~WUB>PlT@oFbsIyN7{J}+DteYKx*TWbx ze77t7g&4)s1{v$=k_A1)uIJRv#vg%NRbLrF&0yNki%C<&5H5hgQyAETvHKN%LwuKU zhaErlgD_~tfF%or;&u0gqJ{TbQmhM6&xB)zA-f-;%?1AL0{?ys@JUgw|U-+AdJ zwD$Ga?}vArUw^%As+D+-U+*4|qU%Roou zK;ztXntU>A9ZDoN-nr|chj!hWiN!7gKvW+|k_X~9TFQ1clZeO%l(g1mOe6*<3c@Oo zP5X%tsx=WibP6i7AU$6SX$)kS3;zlOo`cX;jX zw{y4Tjvs%0YH?8niIb>MLx^;dU}Gvkh-&z={b*Ya-=b2gYw}rQ6JM`FkDwZaR1TNN zK=41hBBE>FMSM|e4O5#lDs3RsY+saO%Fc;2<(OEd%JZ+@7{hU9bMyOMJ9quGMJG4p zz%46Dn&B=sWIU-_rTiO;D%cjGf=mWhP`=^=nim|UmGu3M< zxYbHoQ<%>MA}oA=D;HWQd*E9LYE=oi1cy-Zw2*uX&w{wAP6$^){uyAY72{RyKV=Gm z%gY z_RRVIQx@@5Ar*YH2ujStx2i$|$!1_`P7ol3v7p$%{R4G-IG!qnGyZCT1cwaY6C62y z4|q_)W8?PV$4n-Yge^|63!>p(M;+JT|0tBPl%P!SIA&0#t)_8H;O`y-+`i;H$ z@ZdAsSFuvvhbxF(#4LV8LFFXrl7>@iZxD&)ob~P}egKbmk5nGSk2_MK z#MzvN^kti5W=n55IZ;yiT)6-=PQo#ozc%jmuO^aQcYKIS=FS@|E>5PsHfYakJ&96w zr1+zBJNnodjF(oIlnJul5>hT;mNS#-Wr$inHF+HXudoico_H1V|8cO5ZkX?FpLO(~ zZQ^v$SIPguJ_1?*!o=wpY!-!=ir>kwkZgei!Zb4QoYR8&3=4*!ngV#plNkUwbk+!R zKUJM(#BfAV&t)of+BoCDt(H$hg%~wr;pF;RCoYY}n~Ad?wQ1u3LmBc}sJ0}lm5nFf z2f%3)?1w)1{qyJ`Ue3ec@@^uPZTK*zySAm%0et8GBgV1cCkoxt4p0B2M5%S@wHB#Z zE23*mB2|R!EGcD+F4CXB$fWrE0ErO2J=wUdVLWUrFALPZ_t8gNinUL_DDG|fprMZV zZ7dxKEDC82wm4@UjK|$TcI@{hZH}IKDwpoubc_k7qtSGjiKR`AvZReKC4v!)&)C@? zjOTnI+Mfx^=Q5l;mo+m$JE&EmjxUnTVwL=XD zq9A8DJ~&`Fy2@#ReS$;`9|ELqfEgk2Z-ER!2)qP7U$`DPiUXt_JgJfc)PWxme&A%? zyJGc4_dKMe;=$MgJD7Mb=d9xw04O~>2e9Awsk4oYWE@Tv`jM!PU}PP7 zIi)pU%m3NrvOupY3a&?HbJ*l6re3OuIIWycW$JdfJ4pw>5g5&%LGAoUcwmRU_11g0 zhGF8g6idUb=?&@LxNmffDAWR zH90&Pi^(K%(ux58?!@jUf2cH2yjf@0)^!K9W>=coQnD6v_NF#xy~4$`8%UUI@tSnn zOf+ffx^!85$?a6jfpz{3*R+f|$GV@-E$}B-M!aHqNf!48(i_K2#(F)TDX15AHsO7? znD@3)C*A z2=|Ci**E_T>ju9I|0gH!CP*4z;a_o$k@SQEMvDG^v%5Z_GMb4Ce&}vUNagPO9%q1Q zr^}Yw%pa$I?=X8LZSA#nbHce^S*y>SbOoh-?9QF}*=>Pzo#K9foc`>qciwr2?|!)Q z_;z5?#1ZBArNzCE>+T#Ko{N6TZ*QG5)Jc5YnTv0|%(tt5ixyR^N{=VfZj}ReFNC zxGUz+WmP_N+`(Ba(RP{!uxu9MpaS&uE4ZEt&UpxK6YeBWPjpn`Y(f6uKF%yL!JQBw z@pX_53OO9E7p3(7l7SH94o`zr`($(u*a`6YFfzip@h!qdejJ`xK1%6<4lh+S*P-Yu zbqY&uB4%uLyZL9>betNp6--j3cqA213W-I?yB|FASLmSWfYHfLjc zg~I90$g>jd8#xK-;cF~pCPo^POFihzh}kaQSicOy^iHehLWfLzRx}l+-Cq;?7M!cf z%Gr8TNbKzOmQtJN=cDv_1udmvXIs?)m6!ixU`^B=+Y;5^we{9px8B9yIyX6|zjuCe zaB$vsi;l@0YHmm{?9wZW$Vq2m2MVkR&L<9*?_QAH*{3;}ev@7rhJwV}Ff*PEMBBJI z7ves1`-yLbSRL0*XL8(P#wg&r6mA1n_)0tlSKzep3R^l%W?Pv96HF5pD)=Ex!-2m8 zwF+F4z$J?h{Y+$n%cymz{yD2YuCr->-x$hCO^A748Hi*xO*#`0`96;>Ha3*x4rrG> zGBq1UZ>HW}g;wx4uz&q4dT{D`BFyX1e@=~y#)(gpbZ}^m+U-h)taf|M7#SW_8ly=~ z($#Bp`BIUHSEkVmBv}DY;y@8%a8Nvu4!*}eP$}U9Fcz#Hgd&)N>6Knkozb&Z(V+kKr7U@)40-Nz zIm7P!fZfPH94$mv<#STY)DKL-c*;$*%=`Y>3V)=-bR4ZJp;d=<#G?4rp)&HGr>O6a zdOJgZL0NWQkO|HX^2ri$Yia5W!aMcF7SzbUlz;0j;*Q7=qlp;OUcV<&jyikunegZq zjXs>R(e|b=OnxNY<|a=(7K__NtP_Q9Hb^3lP}KJ{=D1+L=HX7%1T*)|`3WT~>m=nCi;dXJWxEO2k12t{Z z%dz9g*ym~`YkPc)tl^gs2L5~Fl z>j$Cw$+$vDfGU0pV?+$b>8GGi+RAyDJd8e`PB)GONdCQ8DMHV46m_bpJ1ojnT1<|0 zn!6WxpV(J4h2*|aXdwIMn{3%?^M?c3-!YM3I*R)C`FC~opX0lrvtZGg60vSgQ7%LE zz0y=-Sve3AU#+NrXrXETG4tLz3z81Y>I*cIoUGxe`h>@o{m&t1RDZ8kN}M-zzAmWC z^_Z76(y(kOnAn`v_+cZXSwGA_OYtQIK3HmvXT#BLkVbx3g{FGAhT)I z9-L_<^sXEw97L_aA%y{ISmp7Vvk)P`KU}ti-z4aOy7G&w(chWDUkbMirkj0VrZO>l z%vyx5iRfsMk~uW5G?$A|CvpA2)KDL{yXU%`sD~$SH%eHEhX2EAb(owj?eV}073F!` zwq-YLh$m~&&wmwA5Ovo5G02k@<|`oA-pH*oVRhaea%O!ki%VB9qL1qdpjECU>2u$c*? z3ai&vxN2bMKuh4>f)Nzd1#$xTbb1EF2+;_T<)wtp0RghA9gs@6NgT4EKbDz-8~L6y z8WT}ttIxte6>p3(Q(cd(xNdnzssHgUFB0=!zM20otsG4rzSiXHzi92+V=GrOYj0V_dtzIbp{J#MU-y#U zz-G^`?GK@V$&2C(KS6r_KZrZ~*6QHb7_U(H<^VSRk8cvo&1D@&h~P8M+RJiz_a)EIIO~Hn>AQ z_3yR1gN{&8?y?6xZC`w`3&qa;$xr53fhqR$LM#+s6<1n8do(61#V}3wyf+u}kE~ES z9Q<7>W7@BxOMbo9)EyT~#CK`jag%;ul&&>W3b)dd45*`am}<7iADjAVK9QGMEg8Hx z;OS0J+|)&ex+D2!{!hlJm~zl!f5#{tn6z8e)!2ty|`v2-LpNO(@gHcr3C2S~!Ja42v!akPfLS>=~OfB7`B zrV26)2H1qsJ^p?0>)>AEp8%T*DOjp0PLEq*_X3*rfZa@~>6g5Hh9dAjcwW|!v4LX3 zWn$+`6NL_GYsgv?TQ|o%XM3`teq@~{yCAM;=IlpC@UB?|=$J+yqEy28(ZePK% zD}l}F#GD)hq!76Fkp_(W?E?NRFbi*r!TUlhyT%tn?1GbJ@OxGFgpUuOU$xRId=;N5 zOsjxPtEOW3#3}&{UXY=O*Xinp*30Axy%#VRc>>mr|LsPNHmg#}+B5ki1w6w>#jF$P zz9%HL3URjHBAFcs=6Xv-rG~$-DIYWya`DFCICJ2<+?FlG6?_i82Ybj*@Ry?;|2qB2 zCvzWtlzIH|6$1lD=gc8q4MYt7pk=5jLSG4VB$m7ZyrWK^Gj`{lYmaDsK7Mnt9Je>Z z#@Wfmz3G)#6!-2e&gQjQV#(^;5-t2quf2BtYJf$|0;|*lct_Za|@DVb;S_ zD4*%cXCX>BDV5XeL?A|lMFDQ$w9&vSIJfp>vln~@&a&Wn!h0bF7UYR#%7|{iH;|?$ z{PuK5KBv<0Bk^oBKgTY#M{f0`;^;3^4-*6Y3!fs{r=OSgtzzfR31CKst=GCw&!n&?iNi7xkM2#Cfh1e@TpuvaE@p6h_qD@ zUc~~Ol;FwyLMRmoiY1r`P&pw$2BCt;;fH)|#fW2~Z{g14APe-?2tu1Npn}Q81m7MY zKaLbgu2f0xjAhMG6dx4jE+(8-pQZ!hbNPY>>nEyyxDe?Z&>--ZaU*H_dt8M}Cjb zFd8m7tn^Z1o*-SN3uJiRhOnm94zJFcmJ6;G@a{msMU(b1>KGFW@#+tWiO_LZdWg3vPw}#Zss8JoN>J{ z@$+)x{Yc2+pPrBybb*ATGuuC3SQ&^)njKjclrd)@ zSrhYkYrIjWk1k;NOUhE-(wsfIV3ye&&n#OSh6N@PrIP1h`y8>LJMEU)>E7+1>W!RG zi9o1c9GV_v8YALz0mQCTkk`AX;4=py`*O>V+1?+E4~z#_V0C4b8j6=6lt)D>aW@>+ zx>(awCt}4Q7o2L!N>ek_u{MUIlSlzoRabH3pK&F=+WkI<+ERv(4F{{xNa_Z4cO+?K?3!7q0ad+8} zL9@0Axjf-{QN`mu8%0ZUi{<=vQi-jp63J9xB| z=8DWJ8?eAmdjeg{JF*0}9`+_;TCF+43tGNK>C`~o3F1;B?1XV>e7zxPRfy$jb!|V_ ztPT2MhCoQGaR%Ny@KnFK^Y*=n=@YqR;GD{wVzynXvWtVzbzpW&y_3BfuR`s!!#rz` zNu<>H4Lud`Qk}u3)rtk&5-ZCtF{@?gN32>OM^a=L9ggt0KDpd1FiQ1hDyPq1=ii6* z7Hd)q*yV>nZ}FfqdkXXz%!XYLyTCSIH|eHHrzhQ0IOWS0Iq%$1<{9gJpgeUt@HyCC z%6Fu`h9N-U@K~^+E_@B2!@!2|xQNAS#j*&`hOuNy@lOHtg_|U?370?=9Nr4=0EZ~g z43|H{>;fHnl!T5?bjy1}AKJ5)?wTkxfS?;SjG&GAR8=@3MY^AI*d;-Cok5`r^VN2O ztN54@PetkqvB#z}B&F=Jw)s6qWv$HP(Q2}?Pu=yAAo&*h^UT5i-He(#MUN5+=jL}! zS=Pl4)NkYbj^HuOGPQ|QCZs2d1f?uaF|(+QJ~s6^8u@QNweM+NLsJ&c4iMZG%Q^{1PP8kJK zqx&Iwi`(grNN49zbgIOUq#~I7XiRvcWo*{PU)a*&LR>ZG zC2YEj!SS$Jm^Z3_cn)%bxYtJq7A?Qe3){2tnE6!^LMavap)5Qlj?}@!4rnm<5CFWg z|BsJDuDDl(c z*+!G!mQ==B{D;VYB+zWzIL~S#N)E3ElJd*ytrlRCu*k?X*Ndb`X|#1Ri|? zN5r2YhtEa zIx{~47ywoY1;;Gbw{%r7=S~HQJw!X{bPJp>fgKUDQ0OJG)Rk?~Yoxo^uH8oNM5Ep1 zxeV&|*HW%_8Ve#l0<;C3Xik( zZ&Yw<->4*hZ%W%N{Z7c*dDBW&%3D7Ll64-fLD}!svFx5T0S8o5{vbC5r+8gcBNmlM z@3-jR@$hRMuIk0E?E!L)SL9)b^)^)8WHiP&97ofi_gn4rIykjufjg#64Ryu~mI zG{zOHcskswACIbGjfA~-$f03V0}MwOQ|?8g&=Y6@bQruD>=;y# z!OLi%PujgW21SBnN6jH0v6yVSC=AQalesYY zt8fI_gU@d#8vDv!q2W$PZS6Yc=C-yyx*g5s4gueqZT3atA!{UU9s6B(_eR9kdn*}H zjp$Ceh%IgV_zsx6CGDv2ugH`;R?ZbF^|6$6@>o18Xg{!{t9SRMOE-t-@0?qs4J~HR zZ7laC;te&qXe`$-EaE!jOO=yz!fDyu&^2km%Jaxy>SY0rM70K$1OncC-3nt;3~a>R zpa<{{q>71}^()cr75?^rjbkRR0R2=5g!0kg1OS~;7N7(p-v8|Y(C)rZ8)3-?m8MvE zF2$5>KB13D`Q5>=-PfZ^3xo*WEVkA0Q@*gaanStaF{#Px5(3BB=v-;gPz%)dq4&rB zL@>zRC`CRWc>n#b#~$-P{`k!9?qgkDw9-_S0FKG8nY;-aSW^|^h`usl^RA8y&FZli zC6*=u`38#nZPDoR+LSk{sxwJaZPDUlwg~Us5RZMEAV%h$_EnPyUwrYpxeyL7o>T|e z7Got5^i*fTmfk+;;c+{P`|-xv4{TsQ-46)#vdToT9|Kx0|TDASn)Hh~6$5ZaK6W#DRK52!R+F-;F6Aq@&~!sK8Wr0U?qVZCTW zn>yI|(D#J>-%tDc$UmtSVGYZs;20z_3tJP=Np>nEcDCN9R%x~Bh{4BH6CXxxwqP7t zbg;)hQeohgHbo*qwn69T`|Mg*caXzylWRt9uvCP@Drc(cvu(jYJ}hK~BpyDwz-#dc zG%suILKpNLnUm^>Ng$QjGIx@<$n-`TO%>3{q-I5>I>59D)xGEf=|#WedJ5 zG2?w9R3VwfTCfP?p<;}?fN8AMN7~s(tW2lQ)$Owe>`lrXUrhE!5+>IGL`7i4_bH#J zD9E{9%rY&m2^LEp&rTIw{laZ6)8@v@Cw_z42wSl~Hj*tP>?=>O{3X;)||~ zjhXB|*hlh;IaZ8G<)B0<)_gUB!V0%l#9Kl)_<)}{9eH&1;HF+`FVZh&Db zVotp`Wo>N^@Y!h6F#(-pyi1!4gc%Z~d(f&&lh+IRXzUmB){qa1_2&(uMzR!rJN^hw zU6NRJGlS_Dl-)|sLDzpN&H33xNm07XtCy|RyL1XQ)5ECYL*1XMSM7G&i#f~}6at^6 zG1?gl7=t#h=&K@Tv6`FmIZ`VmDuG=BD{bVtZV_K+_wm0HacyA(x}-MQ6M(p2k-_a&z zWhr^e?PrI7X>iGN3=y&)Rd9W7$$XIxts|kCTBoy00-}=DFjz#n|G+3*H+}yvnL*p* z4vXnSa%VAvyMA$h|KQ*{^49Q%4QLx)oK0@r02lroRUdh#{kHG;FK?Z*h*-CL&e~s& z?e3qyvX}TMm2&wO1O;AJIHeWW1UxNKp2QN?2@MIQ-c1gER3>m++N&8!zR<#}cg7tF zN8&+)IXH6zFT?N5Pm^rar-aT^c~@XY@U z2Nz%rWHYHz4pb5oB5!((ti@4rlzIy|C2(5QoEK^?TsZ3p^l1`8?mzDi>gSSgJ|Qc$ z5I4Q-W}BvyFTV@jLq?x92*Wf4WZzRyp`9H{>IAk=NRads1JqUPD}IXS#MWL8l2rUNQN=aeTGZ9Vfo1jJRwI0z8A* zpOcS{YMEZY-^nzNj!rQ+Otu<7U#a+w)9JkTvBkUFHMQ%9|LD0kGs70~hQi52(jSh- ztE396V|9%05&Ou0m-P3D%Y3DObbuCfTq;rgrKqg9C7qd(@^~Ds!IBaOoJj@Diptk3 z8(?mFehN6wE3SnJCR-_6kCoDY8T*35f~TxlO&P^l{{t-{213O(vHie#4oZONg(*<* zvIz(<{-ha87#uXfaj+y(>P_&fAPpX3YDzfm?n445P7$n-T70&U5XKCXQzJf4Pjy1d zK-^npB3GqRUb$@Uf?}87={D0BE#25G^Q=;aGz*E0L*KL>^08~yY-m+lSII)kRixZ& z9VEvVzCJ7q5zX@yf!E%J>z2T4C+E(kvBCl-$D|BdLdHm^m+KIP+kCdRJpvd@uT|-$ z{`PceDmN7Eh9x~YSBN_`bUM(%9SqR!3@r>j@(6smGt|nOniS3m1hl7byVLBu(NEs~ zm)E=tq2+}GvG*?~dcSd(RbtZB-L-un2Uk?~0BwQaCsYn)5v*Xc^FAQ;FRnxp>&M#< z#{Y#j1e}<_9SLBZadHAn3f|-kuZJH=9Vn%d@k8U$X4qRJ(!3Itp|-9ov->y_4=-qp8j*Ys=b9?vq1#iBBqOgC?Yt0w>#`HI)O zpU>53ttDcIQD>JpWGY*kExUEv|ff80r%JV^#Vq7^!7CYi?*Jrv`@ z&Opq~*ohf0WGMmfLn4)$3yX^|_0gUMnNXW8*e%<2B(ray+?duOo?RFhxtZULf(WHtr?2uPS$4eYNBjVWv(<4dPqczeCw@WToPD&EzYfD8#5VV zTP}y|ugiaU0qj^K^!((Y4-e0hhkk3|25^riB#in+5l9@kU^p5HQ&b@s1L7CXFTsIB zh8uzV$hTx-FA8T9K$5|iq62P#G#CGndno0r(b$?~I}fDR5*k!SXV_JmgxJhEb1nB{ z4Tm%Dn_+hO3RL#YlLC`e#9$ReGI;D6130=m`L854*eZhRPZt%{cB4m?7wkKsA$8HJ zfg0bD)->uR?@p6nOM1jsXEHtSP4Y}f#i}q<%qA`{Y?3TbwEke`oz8?R}{}#UN-QnDc(Cp#JHp$x7|=tjtvQT)J(xLfE_JLgW;j zss-bbV)*@^23~7~nPql}_{6ExoP?$FvKq|uoB4m9E*xkS!w1ZQdiY!lcSC(CC4Tx6N^&Ifu*Xa%h(8xL*-E=qCT=IpmRr_IYg^ z{GwfLMb;6I=vJyvhz9dApP}7LYk>%?9q(^~_F$}!sF`YXIwZ0|VyIsk0w_>&f*BEb z5XwzpDIp}fE>H8IP%MVNpz?7_1@00%j(G4#h2v`&gG9?@$ghdXNpGh7s7DsOty*cB z?p6~-il9k@-ZeWPsn1lO-=f_9U=4Z8-`XNpZL`k4BQvVkc1s=77QuVq0NjOXV zlT-cVXitLYNg&3kAOI2@5+;obxxSIpYI6w4B3EDf^H1W_99c_GBHQS5E2^~4#~!oG zT4gS2ojM!uw!pcjz(+nqYnr5hC|(AvCZ1@CB@}*NoTd9OF@>-lpp+{b>w=UVQ*HZv zK=$~S3xbL7Jzp#jfe&V-$R&36Awe?GByrS8M)o8}3H4`@ngy(RIvx8@e_G!|UyaK8 zOB;5Q?|Kb>ftUTM#N$`Fh=e^I^(4p_yrHNdDMoiOo1{URdm)s>Y(%Sf&YT?CRiDKL ztJ&JzdlkVPQ>}g(j+|XwUaS-5xP0;$sOdI|V~g{5joU>{+xD~`kj`8Ex54#t&vuyd zf^}35>I3hf)lt0dp==pY01$6sL;=_va3aB@r_?Est3zC^?sPs|oJJD4YiF3YZ`Na`|hkZRVkRK7(WZojPm6hn(w=x$l zJW1Y~bv@zO3EA&O`#L);$6B*!I{D7eC3iZ`n?`(M$41*&8WEsRvOhcwSf^u>5-M}> zb`J2}e(bn#LnP=(tXV;|!lF;$S*%(p>{!U*!6_B00x()!wKuvO>eaLj|F>2lP2@B&HfL_?_1Ay%fIy*E z2?#XCD-ZH4j4vf&h$FLp!DiH|$hBzJjGpY|)$ueMByUc0UFw9QVsF&p3L~3f(f)<8 zaJX;vY+mx|-lX*SJ&cOX`SZ!AljQHzV!3`4HC+*3_CT_PQmr4z~mx`5aXs<3NBdJBA;oIwFHNm})?rBU6${AMN|y5wxfQKmyQrpWYh4!2 zA+OJhqy`qpC_!Hys*|{PF-sWqFLu?7)>3=v=&;-T8QR0NHn2BQh=*1~{s?nOfpame zEuaG_#JjNB1x<|CFc+%w;WIoJHJ|Rqmie++kBi`gQUN& z-Be?=XflMtywfRmZ?t@REGd{hj~@)KCtoz1xIwN8@+U#`I{d_w>+?1Z``CBW9Mr6= z6gBM))(IiJsfMa6#CIQp_U9{?k5X;LC1xx>m<%BLKt4ZE78|wyv;|;3!~j&`87MCV zy#XCX09&kJ1&0D|HfmxE*nk|vmCu|O*hm``$56u z%SHTvnxm*w7>sO_?}JQvcb#pS;PH6;oIqwEmYnqJ@$lqO7e>@l3lK}y*ZM; z@4j@b!-B45SIPstI)NxF4Jcq7e!iFPS=s@Jv6}$xm*@KxFmK=yc0del2SBj^0GHyM z&GQV9NB`q^uOLs@R>12aRH3IGNNt{h#2NCtEGMpCe(i$TClZy-WN|NWNSwK4mNbmV zC&o_fb4^crI?aJD>9HG=TQm+`nCE2w6Sk=cO{HipqLbed_pXHDSM%d>)Lp6!1?FxW zGMi00lV{i`b(Y`LEpU3spPFyCt{@Mgm5Y{yE3T_a!+g`@Y1FX2LulLN`d!yXL2kK$ z52dt7O+?RaLNvCZt_*T}vgPaOQ3rh+8O zyi%Ja<>hxL=FUybjDP>Ab6WhJU#@A&G%ikLeAaAKyK9%B+HMnOC5eR%xZf%V^9oLb z{96lFJwNjZHfmqBn1$-Z!P&Pf)MDXR&s#ful2>UB07m$3s25QcQ}AXS<)$j2PXF#D zM=Z_?!q|cRiM_Gs-gzgntBGq9Oi@*h%^Dr8jadD#sSb9m2lgClR(OI&^E*y%*vUn| z*o)ftKfgDzpEx*=EDqW-WgAc5P!?;5iOX3ZuE%uRy?x|uJ{VL;N0*m(`RXS(b^$0fP0=oFeMi6RvDx=n&yuX zq!Vri_Q6Z9xX;VZli8Z*&S;*QQNF}cj5iTmv33aSu_lc|0LEORk4E#^UaK$)-=RuX zfv-n{cv7hZH1=9=_B2z?j~KP?lsjp)wMh5uOKpG!Kmulzr7r%XSrg`28ANS_9#10J zI_zG@TA$V;PL5Z6d7ZTh!qH1c)+8&y7&qE<+RGbd_9c6X+| zeZasmNh%}uBzYa0I;%SwJ6x4PQ_0hr)vY~lc5tiq`K_G-=Ozc~y)Dh;xO~Ed(!%~Y z`O!zW+)~#|=cOgCMkRT!zveb(*5(wagav$bH?10Mi5|An`2ZtMGXDn(%Db8f)IcFl zVx)ZiaV!WB1ZzOR%Gd(ssfpzVSKy=L?%{;JL1Lyff+5Px_*1YHM7OqH81e|yNP#Rv8{j&uY+@E)%p+AW?@d?wx4hb|o;x_U)+LNFY z&iM4OrkTenSU)+*Kd1dAM$_n@cbch&DIaiBQRHDN;I>0F{x&E229pSA2-75Wkpz%8Kp^< zSr8YAdjwHI52QGo3=**c_#pyvbvO93a`2TfwrIp!F~1t1FzM8q*NOm`!C#mNHek$w zuF@vs5emu}U>ZgxQo@`{4RBp9pUcWXu}jBO=+{eTl4G->$8zkOL{yV>w73_pjLx5L zs^oC>VY&2Ti89QT5pJaWegj$%sp{{II71DMHICFM^8AIDB5M-~^mqynkRmU3llxF# z?|^T`JxT1wA;y$!0C!P9bCj@ zS%|b=N%l1)E#*d6jby_nn6|Y_!gTTiJUi=MhKcLv7+J)#(PEkba;lbOO~j=gaB`$d zK9eB77$`kv;xgjXrxTab=MSCR+ih&!a`T?=R9$poP372kQ^}KP>gQF;Iz7f0z z{mZh+CbF$_d}LkY6xi0d$$3Gha8=l54?`Q^4>gT8=WOc>kTkZ~s#9}AYyp?BJr5g) zFgBr9H+rZA=>dOaHc)JW=|Rbk>&!?LXa)BV_ly^pU?ni|GE4!W2Ivq7==_IY zeHly9#@`LF0XT-JliK|rmn0!6M%rRlk;q|P#2SF!XjdVLRhPH6*{P|d*-Gtg?l@h-+D=m_Hjh=Rh;^Dg7&<`&HR-Zgc3f2jC z_bq`cni0@G6PEW%_5xLkgAV{isW1f4^D2OfiHWIN_y8IQh)QLJC~B1y>KvfJANV(~ zy6E`>$V(qRpE;_|aFY5evo9fDzA?FYafcCQRj>l(qSACkqh_L_I6=_FZpS0h0PIKE zkUns)fb;N0Av;wn{rY<@2YFAWRvIgVfNM>jzOr^-(A^+M-l*H-ijz66FT^*OzG;&L zT>_%VCZ8uWe_6vLcc8^X^D-4XJ2I$?yeAXvFfZmQ<4@bv44EROW{VP1*oqQJyyS5O zhZdx);bg~1S@+^()SOya8}vlWRj@53f!?jC(hpdMyF%JfmnnCg8{B01d|xeZf9>A` z{(v9T8|RxZ04~5WNh^g5k`T?pUi85KvTONn6l%I{!N%w1zhL98AfS{QWo*r}X#Nd( zy5?(8&~@(Nw6{vGZBcC5k>0)?Et%7)-7-DRPVRD=jL)oy#$D+!swuB@>Kde^-=YeE z%lw%|5=|2FkwMLa8TIG_(q|jcMU^|9lN*eU?W?+~ExI19YHuUYIcooWf0f`8dH7m?V&;kZ)zL)N&48Tv zx;YD@x~9&qdQBa)8DK1#0^4{GtqS;43E4Uv2-) z&JVGTWw3eYHdy1EC?@==B=qZe7wOfets&STrjV5po(Jb4l`iB-55;Kt*H+&?XACa7SphU@KgJF!8Rne3u4g5wGx(aZP5BPlwya5qaQ= ztB6J~mCA+ z`;eH4RCIs{ndYS17K0^q^EPzW`Hyq}iM}$;gvy#>!zRs;*`}VQ|0Hrmbjn-oU#%0@ zZhnaTD1CRn&**W8yL3QzZG@sgzWZX587yW#%8zd*?-JqLAAeAO;nj4&cz|-I^P%pj z5N;a>Y;Zc0c7|M@5i30l!x@3{AV(4_)7Zm(p#RsA%8WbT8Nav>OJr&r>P-S%T>ZU?c~1H2{a@<8%fkcbY;P!#*lA+TL$B$ z%;JV^g1zPEcUMWeR*?UEn_d_l8vr*moqeFu)ZX1 z4#jnJq)Me0rQrT6so9E6aQ}Aj1frlfd$1-j|END;&ZkJRZ%Dx#uBkp7IH(*8(5FyTVXl2OD;_`Pvh%j8ao48`+xJwhh(CXU~p5kQy9JJ@AxW7Ur56 zZ%eHqo`FN~6tib2`xole+m|XgtmSL(WqnXd;1zRrAoj(x|ub9dqz z*b=Iz9OE4;Nc<4g(csG0b)XS(EFk~DcZ^y6GQ;PoK|K#<_9_K14upY0=)e{fkti2Z zinS^>=umq`HH%B|lvNg+BpIg49R4p^jsTunzN8x5M z>D*^z7LQs-C}-je{kl8U8=5xln$Vt}KzFdOFW4RE`KDO~OSt+MwKw^X)+g|u#zZtk z7gz=FN0-zz*xOnr?_|x)4sVWqm`MIAF?C-GLNY(JuVn|5;`%3gRtVZxczXBZX18L{ zqtAhj!F$hxlakP--w1yCB;i%65>o+mC@!tLI{rxMR*LRG@$tesBgJ6Y@sxr^fegeI znJP-VQflbCY!xnKwA9HP?Q8bNR}ksHzQ?m$D%n{`74MDz^5c(_e;pg-x!57Di~FL? z5fqhZOVD*?A)ZrdVIlW5vAugY-fnlntg6n9*PwImY?IF$0Y=-kaZ&S%BN-g2&HQ=x z*ta$ZKMO@L7sg&Emds8sTXv6ii<{@&V(r|-&xwcS8CC~$DW-VjWB1=584Trm#Fg35 zAn;j=icwAhOQVH;fc(UeI^Z*iLSaZz@}n*Y$W`8SL_xXWf)HvZ2o2Z~V03{&^LmCh zm+U?vE~`|K&-cU~74|^2eBYt?mMyyJYjCSWRi^Vkt|FZqz&%#SjlyI~iC!J^Ufa*VI={I@5; ze(J!YI6$X{fC4$FR;8*ZfaP(FZrmz}L4|;-Gy`;^zDJ>Z@RrOl3)dFUMh_1_o?$mT9wD2fRrMen^g zFgbA4(_0%3*El{psC0c_VOd3fzLI=98#eoDio6M{xRZRUdG;Hxhq|m4nkHXtbTsDg zP-CnahCK6q5L=Kyl-UW?hykL&U9aP@n1Xvl=>d$S0Av(~JYadY77!CwkkP`8<0vJf zOf3XsAXFXXL$ALTld8+_OdB(n0n3{8>FLw^ZA3Bo^J#n*EVDWI=W{&R<{LJW^I=XX z>y0uCWGc+1=(|Uep7~AF0^=gX%FZLofg1PIPd}FqL&kY%YENYJ+8pX2@6QG2`VNvr zUm{v+sr=W_ubpd5$|apmKNFnvJ*Z#l61tWcKY^k)l#M1$GJwo(f?dk89+)=e6PdgUKS8IX6t{V7qf6aXS9Y*}0q#Vz-RmTR2&0~$bT z=gLeD7!c@eoe&bp;(%%Z{dwg~Cty2JAh=$gh6xLA$r~o*sEQb~R(G9f*|Nxj1^O07 zu)a=Xoe@*H&9SiCE-%ld{-jqt!w{OvJUZnxy_I~OVd40=X68k$*ee59{b#*gi=~R0 zGK%NVkM#bnUD+N@Oi}fP(nd5%mszOrhh@KKIQTFo{R*GU;_suow-UyQ77?*v8{k@Hi+-beS zx5$qGt;HlJ#7?YV4GPFmnz zjFaG`#`POuq98W-cnlit&hK9fdBb+5LdILYTIh36WwvWIEb`BqR+&-SrBTg-aK>xp zMgM&5wJ*MieLvQ`YE{q)T?t|+p73k*<wE#NUzwBFLp^s8uQh}qwzJNkGI97Ks6BC#gG$chj&=Bw`re?mI117&?Kmg>H z{C`Hc6lVOP3PB{{44Mt~;tiVIB2cC(BO1jKu8SWQk+1R@4vyR6dcx_~+N6!*+e3&TT#>0hS3;_;(1{3g ztpbYmq%7>Pyee-1pLE8$#-#U-#uTCJF6&HtmKrJanEFzmdCr7 z9Y49Q3;o`>$uhba&?>Xpo|@tD3eKYuQzVAYx*Sjg_u=ppT!k^ z>OP%Bp!T~r``OD@e0^P=Xv=h}4&-sj7flA=zM47%+|LD^T?~mUP!jok2j~cZ5Y%HF zMgY%F)J7KSBY?P9v$BvnfD#PCumgmneo{Pely-Vmk&wO78|z@SSfzJu-#+83sav<+ z7FnxV`gcW2_4~2T_3Kk?wTHI|KNl6(Dbo)h?yga1!XIT$up@q)RdY7a`lc0^x1oc(?paK>1f^&+uwHU^QSmxMk`S zs<@z_NU<^%!%Q~Nn*T`=3}OM7fx5;h6<8FDv_uNTg#I8i^%1^H%k2Bo5wBxbn_+3E z%wUOG6+DSe8~b7}tPXuI0Yd^FkJb=YLnLX>NG)0Mx{b+^*>*ulVtqv;4~i_rl=t5I z)MvlnBUPfM4?ENfx7SH%TF5CuOH`~2a5gk#gopNVC%bd?!ox&S%l`PRS>RWF7md{u z9%~F}HGO$kYv>xF73RY2n;W=2Yu8K`7%sayM_nbi;W^ zoKrUdy30~qu$w7)UJY0tL=smR;0W)eJbEdi;0yRQgh; zEdnV)_fhc?5NV5*vP%CuSiDKXWdT;3?UExE@i5wR3beyhua#K%Fldtx4dY=N&FW4& z4630fO>4jOpeV>+SVANJ_^V%8C4AF4^lZG#qH8mf7o^#MUf(Gur0bt-NH#pRQMmU0 z8uARoiI~uSFr?D>z0bhZS@f%%II00I63fuueNDOSFs z{x4a3e65ydtwHN-fdsMvU<&RY1B{Nkw%-L z(A#^vpkayfg^g2q&P~qBY}Qd;e)gfggK&{_Z7ghwsco_5K%gUJmR-y^9a+qG1jJ>| z(|$thgK1L0h&Y@^nF4GE3fXv&LJHr2++8kT!}BOH4}r+ymtx+u=3CwKGy(|)2!vZ! zDa8j63cf?7^1;P|s7)BlIy|sh1^{J*)Nl#yhAkJGQz!o_Tz_8;dG4!jole^!m#j{W zh~0HE-Vn3bpke-i0&$JP8xVI_OZPpJc`IS7LpBH3&2Vcp>gs(?k64QY`@!2V$5*XV z8nmi7|1x_%so&iD5IjB`DvAS>uN7GM|6_#e`vWvDaQnQ%3+l0INaTIMRX7amQSK{S9TkvC;mgasp$6Y$I0i(tdHs8;j*IF*%r2+86pVhsZ;a!L484d-@f>M((d2o(S*0cF3{X@ zrGJwREs-~=-S!AHYt7v-xjK5dF^Oiu5PxY`Y$%4g4*r3ecf@MTPMg@d$@-~hjh$~e zU9xqE{6i;PLHvRIgSbHx*4IcM)OJNQ)k4gZxJTpy#0r}M+ZX}OU!KgLpW*W#G@FQS zAbU!&x@=TnN&&PYYl;G>VG7~yKV@7ar@aT*g5EibP^urG*O2PoFr_h5J zc9Kt`YWzT}^$DHZ5dl!rzpTG$#XSksN8XwEtTqy@6xVIMuN&8r1mF|#n^ybvv$5FSdHzz72B01lx%T?mb0 zKL$%K?#rO~s33Fr+wcd%IzJi%kwRf6b^=Q%EQq6I@X{c0=D{_A>TtL+xcb4+SyZ$t z*a)l8y>2HblnpLF^YKUj_{Tf%MBcMn-zRVEh*~pBV@|SSeRO1G-M7(zmHQ^!$Ov(D zr3CvnxDX?=Z%X^vKThvBUrzo7aTM0`J3Q@vL#%gp&s`l;_W3xU22$R1pu2mCcQN^8 zFO*;}TRpWtc(5srD>BmI4M_hQ|4`~eV4Z>IUExTky-;9;XZ8LRuviQ9ep?|X5}D|% zfPD^y_+tLXA)G@Shgz4}hpJ|w{55?0c?qKmGk{k>=ZmGlFQC=*QXCAT;0GT20eTIc zd*PjTlE3{eY@ZqEje9CZeU43=<4cyX+}z;z;JyV+^0ygwo{wi`5nOU{oS?;kDQW^_ z2f807J)y*!v7g;9k@~s*e#lD)*cxKAJOtXMJWL*22(yZok4$elyeo>+?ok!T%ZnhU zb%OB23FvN-A&1hF*dvAaB4o{394ELa6_J{z!Z)*A4Dezc%_FS&1)tscND(B;U#Pz z=~OTV$E0DSd_mY?zDjr;jsUvFN{VnEKMsTF_wK#Dh|pPmrA8?<%r6QpVY;m_6Qf_O zlhrl}wsCW_F_E=d$&xKlvvh#NadJH|&Z|Xsp`Rxg6TG4S>monsCAz;Fg0>WhzD+#* zC#K1wyOOBOz1O9HS$)LevTM`P%Kad2Alf3l3jM8RQ-2 zMMY6bl!s#!X{bHMv04sH^Oiscq#Dj(8EoFlckIFm6#8+odK#ZmT!=*#FbO5Q`PLM4 zg?}+2@=&l`GF~x3&F-YyA;88KkYXfa&4;+am!o(WpAdCKH_t@(vv~D7w0tZ;92l!1 zc8{%15W=xPp*coZSQ9n1n1&aG*REA?LL#!{O`|QxjLUfoid0&UN>)k>fOCe_dbvh9 znXS`#^lHZWGA4&viU2E;60OuyrXjd}D!E;(C*Pe5TOVi537BlG-ax|bzs4CMWd`%v zJ7Ot&dx!tjDL+@n^oUTHywPe1@+7H9lSk`FwKVzs^>VXXWkd+U3P zWtTCzs!nxB&LrW4iO;kSK=n}C1Y2_W`S|~!Nsk;!+!WuiA$}97)$~Zsu+}o}s#LT^ zwrq>9`U^MUb;@61ICwsRwG0jX-Vv!~)))j2I&Gyau z*P^8x2dWo5n#68ha(Ut!!OF=Gn;=H! zU2Y`Xt3Hc5!xHV8VH!z6xo{PwuLzsaaDESDvOBs5Q^y z93|C>md}b-;=HJkN_b0w661y{uVQW{IGbaeE$uysj9z7Y(V_`kAhg901l%W2aNO+7Si8vV{Y0?R6dcYoECf9l8HkcXUGB^tq_9ulZ*y&aPZ}c13h$&mXVNT=#q7 zsfEnX!H*VC*QbLd2 zn*e^Mb`=PkG)0ct9-F;~CqTcrSv4%JX zMe(qQ<7XTGJ+q6?bq}T<*Bnp_Y^&_#b0zJbFWi1N{3S=edBQbgh6ScU@Ll4(kMSh% zVhi{fxEdip{jz=n6Hzt+I1O}9o*;1dRO%#JlAnPMl_iuD3(hMxA$E_!KSb_Fvln(H)7RGLlaA{(8J;p&{T{RL8Sv3?fNWu+iV>Z~k}VON|?v@%$K^d%cG? z4JaF@>Rc~RU&Xs_7vgg2KnFIPm~o9|SJ@FXCh1b7lIn058K>`Qd3X zLZBqLbQ9Ukd`cU~qzgwv-0`l!Vv3rqe9{`%@qV#-)~wZ+ZYQ^&ChswXWA;j!saCpa z3;2DL?R0_P8|1tpw)*%EbmO}9{fD4_V(V7hQY2U$TDml}m|W>uVo`0bLuOfjPEjmm z_lM9Uqi0526^$b|uS1D`QN1e<c=M0@VJ=TJFVn|}l!-`UxT7)Q|u!4TOF z^~Y1#D*s?-VrK&OcJG{dQgEp6Y&zW>X7&ZiZ_?&IyUzh_J>ByLvup1v!)*;`)5}^` z3toD3q*>rtWqCP~xF&gc7I_V7Ais&-bkj^KCzXa6^;xK6Oo3P31zyl9nDM+Frca!L zOd*AzaWgdq_M-H{$op{;QWzbEK>{Tpy@x4@Q-|XXUle6QZ4^Tu5!)Mz@!+c;ca~t% zA*DGWv~Pt;SeH?e6#BSvAn^*30P%=QX=mxBWlD+6AtF3Ac9oF4uXto=Xv&nq!Nq86 zF)d0I-+YR}=EFumnOj%N;Bksei|F*RPikc05_ZbuQkfSuNCHfb&dfG@L=wYy+YleoVO`AKnhIV|e z#i30$7Smu|l}6@_@rGj6(TZ4eeAP88_s1Wa>si>$n|CDZF~|7|T_1X>e-+bctTcs_ z0p;-0QoqII;h9o0Z-Bh9n_PohhucE2o0{{rMY32NwP`vOI!%*qRmUicyT0v7_bRKv zvC{e(X)86v*N!#EDxx*f)-pGig%rd`?ty%R3OXz<$8`JQOgB=K6hQlqYZ}b%aE=9a zQ$kH6EZ~a#8W?FXisK#N?^45@0WA~+2LdbDDq{6LE<_*+7Ha{C;O8df3v#{GOUH8* zP=0r?Or?%$HKmIs%(e^iB$M}GHAgrmpT8L$-#CvvdJ@?Dm~Oge?3QG59>jf;Np$`g zdF^eRHuZ3YK9#mKSjMOcZ`-zUBY9&estjw|oF-SV`q+cv`|r1G9uZC(1%G+V5XVJwd&8vA;q(cTX}=ymVd0hJ3YR*bz9i7bsRaXBQp z=>djEKYe;^No0)*_gErbzP>-b z_}M~5^`0}^klC-3_#@Fia&@Z&620BSomGKjO+|SFtWnro(eI*b=&WMizW6U+X3d*U ze&->FJA>(9)tYp$2i@!mR^@enp5NQ4nP2&;Sv(1M1vQ820GtHHdpHaT(3dJn2Tg+c zw(t!+O$0&;KWbW~`Qr9yp4lgcAO+zJ;YV3Y;I!Zs`DfvX#KvMu_hAb`y#l^0oIO=( zhNIG`-x`pF_?)}e`#5%WyhLICS5hD1vL9X*VA^11lvpSwk4r0~wxryY6R%kxoi(c{ z)bOoN;dOCdwkfhF`SN81zye zv+iytXT+he?~-KQmdZL=<{k3sWy=r`W~+}xW@dO^Z8$De+jHKqbzlk0rwu2$8hKpe zwvy8nb>vYrZDCJ!^gvA#H_RqoMbTEM+v??=4F7ab9p5(MB=0a8^nPDsEY@HN*u(+5 zD?ausZmhfB=^e_RJ6ED)&{?IyHD<*$gD0IcSHhg9fy$r!WLIAwG!Npw;^%4SXgyG| zRRuMAlX0CA&g@YJggPRy8I&{tsi{q|mz$v=dt7Kf$Kn$SL)l^>o)ZRWgTZDV=zvQ^ zokOd@wA2x?bX(U~IQ-E=nU|CtSn(RCGs?){2+G@uo1(>p(P^-o_Rw^-b)l*XH4Uv? ziOPu=Ij}30O%i-(ywu1d|H4-lCt)JjDRyzHh=sn7ThF#@%J)V~rc`UEJX<5J(McDr zlJFHWO{!`R`6=^0kIC#N7|e%%eyGRMw)H2coVlKLZ;rh#6$lhhxULbHR_MPAA5N9U zuMeZrIivJtYk)2x*fWcLU5atOF3$4`Z-E&o!t6;6ds^yRu29 z1bDjy=rhsfVb1e!NrBi;y;OPj5yOOxK@-b zW}vkQX;QTtaCqh?Xj{x?enF8~c47d9&5Z z%auyxQ;+(|>1dygyvC$wG1NwFv)H}TEjkhUU2Jch8#)5rjlVD2Iuq*w+)MUzi0QUM z?OO!uKdJ!*{x?5?!i2f8KsLa7mn)Van_Q68d}&+0l=w;>qIjx_7hGnGsb;!2#~s1Y zzSQH^a7&Uye#)zxOgytv91g5yuYjq=ft_>RYxhLSKO$$AtYe!?V5A}WjX=+gml=4c zOIbt)G3e-wiexUtI6^;8Xiq2wL0SCwp?f1sSCd;FqurbdIdF21ijHh6wV z`SZ_{Q@#83yul5B5ADmA#g4{NQSPL?*mq3?+o19vK7g8{R*)s^VdSW&_K)2?NP`ti z2T(u=aT7X(@AC_YaacP7Qit16^@aPlQnHihl9E2IYqmbDo2p-z)&)Xa(+{~}23NQg zhIPTcRWyB$?uX$0ugPa%k1mr**H@=Ev$uFWB08gleABR(V2ypkbF$(_b&A1Eu!?Wx zD(JCd1NwVBak@g0yVgnX4QV?cZRXCF%^Q{oL2t;@jq~6ZKRzbMOcVB|1rQ*BCw~jjr;SbnN zte*=5M+(9~4SE2DgYVs9t>d|uMdG?8#_f?%Wc|U1jbVYC6XTPgiK_aDi*IM&c%x-; z>6A%7^s;MLj136wtR&MeL@u>A#j^8qyy7fk($Uwc;HfT6{3b_nRgL+f8w7(w_XZa~ zqOPi%-&+pb(r+wB3MRLl;SiPcgygSauJHaCCwwFZlTMEEoEzQ6`==Ls_rVf3j5R)l zoc(0zp23kZJpHHgKkxLidZ756hB2Eys)w~4aNt}503CH_g<};07_nNcaJs{~n65{& z%4rG&H$?B2$^%|cSU~=RSKmY|R?mf~_d*8wQg%74<2@%XRsNGjcGX3gHvYG4c}bLR z*eB+ti@6N+T{wPcl`?&;vm~3-bw5J8RQm!g$x5Y8qi-^q+j4Kd`P=(s zhFKOed?-`e_88)9A7Z&illUHS4J+=E=RkG_?^tUGUvAur!!$dH!$1_wS~#w8LqVM` zb-)GoE~1zhi!~(IqYzWYMjoKm6@~$;J(!4HFVBgjDBYE8A-{5j$n89Tn=@tX)US(c zBEEgWhrPjKc1eks6{48^x7_MR^z)qy+z(`wtA_kD7JCUu-;*En%t4cg+|E@O$4iv# zn~PZV44r{4O?he|x2+=gmh{QFQ)^P0-JSA^y`JKZpswX&hC5ryQFD32S_Ni!MY?s? zA1*b;cRf4f*5+vS{+s74T_VttH)od$hU;cWk3@=MC%{S=u8Y$aL4_wB>fGN2nePIb zcSEH+uJpw`|6k@)g5hSK?Ew-3e#cQ8DsporK`29MwKj6Mr6bxw)8?Ee6dz4IWZ(pf zdCY8>TTK3%7oS6L9vS<5US?bR&auCm75|U1uYi)PJpbm7ySvYh>yF*In_XiHlt4sD@G(Lna*d)_;<$%eM)e8ZV+W|-NW_jwKaU zV*hL8p5?U?zC}4uREzITF@b~_YswB*a36w*gu5QaAIRsAP%rO)r$}CF!O1AsAn(;8 z9d}(cM!85Xc0q#Vd1s(YTs+N^u=iS)+P#so8N@A0Z3F@c=xUz}DqT?{D z%}Q*Du}?t1Hk`j4=EPCW-%_ybP!Gr(De%6po$p%6374+?4;?Y;Od`FQKQ+*4RF6>V zf_728XqvNok=e9ns&C~Q(XU^@E?g*m{)LY(KPL81wEdiY zc}Z4Lw_OAn$UHOeN2!6GWSJyK`5bUAu~{oGl$4|oj_*BKm1ufkQ5ow(>HYVY-pMK3 zo=Tjl(q#_%Io&x+@5M4nIH20xsL<%OnfgKUOV@0(%rn>Ud3Z~T9UqQdUNVm{+yZn! zxI|CV&p@n66ee3>GHz^A8=VZ@Jc{u1sYHlS2vi{fi9xCX6eoB<5VgQtqb=M_N<%=c zd5$s`8V3cTA|d&{c+KYf<(P#*cNROnj)zpKR45%5V-cQQE*7#e`p6Z& zjm81@mfS1RGZIXl=#YS)W%ICah@Hm`dzQ09Lq)A;C&@D|O<-|ULl|@8CSEKl*(M&j z@n+oRiDDb4F1TrG)|=^Dx?(rkVUc2NwPxQv-dRkIbWTNkW^;^%?@3>ox0baJVkp)P zN$`8_fq6;84PgpBp%o&ApJ;|r{E&hXP@^;&b#RIw(3x56u{%eB4tY+Y=#ekt=A)=y zNnpB}D4J+pQsxLx`q{GG2OO0hmeT<&0_2|>` zW}POm#-o-N8FS-Xd6{|b$LdgNhCG5NWnTq&ev-mBz9V7>GOz0nS1mX8_GR zfX6{Sk)Fx-h=T|z9ct#)x%s?8{fTGMEKc?M;>iW;BmPKqTjsop9}|X@Nf|jJuIQkx z%7I>0g@c`m*m?H72iYg88u?Q(tXbT8^ljFM7>7xIrL&glbpXF$a#+(!5BasUh(oZg zl2hPXWZ0l&iKea1x;e)lctWR!e#{{-7*gaa>vt*!I{$5m7nAL2|@H=8e>`yB^i5Re4Pb*Bc7*k z04zH#_?iftBagU=NQ6`jdIFKI2_UqEj!{KWu&_DusgZmTPkkC1^C+&mUh85>RW_pf zOqzu_a%M?5HZ&onjD}Z-?@-Eyv5%FJKnt(hViA!0GT78mqAD96R~~8$S=&-YL~(g} zlGGcELN4`3LLD1t8-!aqqBw_#{VH@oMHtiVL%jIOZTi_KIg?$c?q8J4;|^@%h~E>G zXZdOT0O^wP!D+FSIym$Q={)(;lRYN1N8*{fi?^||J0mI=9ow7Y;s+8KW5!L4yPIRh8fBC?~xb9g);e-8=NA7r57J&aG`uh z0sh4}h^x@l9GV)1oBbczsJt&tc}U}z3S}ddc1ng*)7m5 zbhzZ7|NPG0ZjvP?TuSaG81X|%jNWhy{YW?T9%LcUbQky%I=El7d1nY}y$j&f$Usp3 zBE`ID(F-NBaLJA_z1+aBRCX;MCm3KbY;5eBCxXKYzMpCc^mXKmP~4Gc3aBi(;Op6S zZFMyU+00EEVTPyvgA0%Q^>~GEWgm8GAYGot<`6B-OH@0$t7nxZ(zb+l)WSaOXTGjNK4Yr#(w_{_0ZF zHqY&qv>ad?cA>_M-?G>I@IJy^Rxsj094Z?Qp- zJ_i^;2;;~Lyp^LgDRkH8W3@2~loA|7xI!H=Pc29)<7l3r4SOX}pST+Jh3Dxvszsq= z8r_i7)g+w;vzI*!2~(6<6T4uPL^UP2l>AXqR(Hdd%sVtLm!IP)Sij5miMNMlcx#0SL7MACat~Zg^oyU)w1(Ep)6wZ^Ksb~Oj=n>V?RCe$v}GJCG3NHZXfyB zA&W_iZ~;F<<1&tmv@A&VB1yZYT32?^$G}^yPwkbc0Xe&Du1*!ymo!g3NWPNIVjqwO z+J1oGeLJ^ya#op{&YZWF5#JL0GPX6v7=Id$2X|izb!mi&tA|^!gQyKnq)qde)x&fak@Gs-U*JhWxQ??ZjQ!R zhi_IZp=lA9T$bnRX)iM+OrcY+4{8+}_I;lkURoR{Qjc(fnMzs<#STdQnZg+W!N!8V^e_ilX;`Q?fRN&T{~6MN$< z;sAkR<4!X0tpTt<8Q_gO!M8ORa$VW~p6kkcvq&NkgJ4RCY|uU`_>)q|6lt!vvmGMe zP*K`C!89*!!DH+3J0OWE0Un4e;wqAaNEaip6Qmn^m7I@tX2_3En{08NoxCD8Ff$yz z;2@uqrt`7yh}5|%edeT>J&rJUJys!Q@Y!~|x@c8cMVP=bCEs}@OE;IEAs>b;VR*_q z$+Xp9C5Gd?(ZTyT@940I=uHXtLuWq0{S)ocKRti=)?ILoU z|HBWV)8m-fU`}!UBJw*~5jPDv>(h|jVTiDA@bs;f=CUW4^<}oc2g^h`t9EUNT4&Iv zn`dIQ+{o?-t|}3jt+c7R=gD;?7=wJv-rga%-01l#wTIxwj{s};?PfP0gV`=5_vqZZ z!oJ;n2W9T+$qNp}XaR`2S~Pcrsj9k!9VKVLY|uNHg)H~W5{jafjFVyWPKmWFf}s&%b{Yp%UHojVuHvAZ)e^Hl za#Yr+Q>6~6Bc_s;AxtpxB9?sm>FkqFzB4Z^s~KeM>17S~d>P-GV8nK(mDwz%&z=B% z)&V_KX!af<_}l;u^Ps~KL!zv6-sGua7znsEe$_jK^STJQJr`(k#Kx#3gS;m6iF^?Y zoGALO^Y&zh*~SQl|bh^_JayzydBf=X8YOH$o> zrYSAgNle6?Z zUMicUojG2Q5|gHE<2>@Sca_mi!LNut1S@eMj?wFHWw_U)0xZf&{}%4D6|!}m&_zET zFvp;q=~~waxCw9|P|(+)_(5kyQNIW{1BDYpZf>+=Bu|!~dX#yx8wQGx!QiOq(gHDHchEN**8d zAmj2sMgH-S{_fK+HU%R7HQBq0gb}AN0>Rg1ad``ErMz0@(pSkw-V@t}xX4Cl(!Q6N z*#y@lJSc>9S3KrJ6_x&AB&73?8y}zDz3ZhCa&A}hq+bG)fgjf_`gx}O$TH%_F4f%A zm8EW@+N|j^W1eIdEKKf{gBxlwwEbe7yu+yOS*kQKt*QFe_o-?X9&NRX)OnY~s;DJA zDf^rFzBnrmDPY#N=``PVDEm#h)|V`vz_1 z?YWV|Z3hQ~q0WWY%oMd!vZUt{zB|K+AB2!5uqDns3FScM4V713gZx<|oZl_7al1|u(+1caLP}ndRW@Ole_ofX9aiDtIzc_q%+Zr^KD#GbRT_%kq@iQG_I6-2TUnz zhiOL88Xeg5fWImtj&W@Y5`4A6~(Z{tLYnOA=#>gn(8^Q zQ3k4u3Z1(Gy5rDPEMg9%)c)ge)aES8FJ+;kdp_ODL{=FTRX$FpUZBtz>oFBEIdLJN z*;j9tcqZAAySfCb?;GdbF4Syn+~)iMd+%`>FVistJAaD@dR=t@>udYoXu#PP$uS;3 z3I6#nNj7!~d$oQXEG)Aqb1g&MG=8Q|TYkX98W%Ocbd$i6zPtI0wb^V;H}V;_^Y#T* zFS~v-wV_v>ob^%5aZY4s{QKF<=;Js20V;1Oznr}C3bng}UqDY7vl|SCrB+493g}nbH7PaWcX(H=?537WlaI4+o{X_3>}0kd0)9bwj(>uBbUnnj4wy?t zK5QXRbban*)Vo1KgJxrphYn|z(s8gwz&vPX!$Gk`-kui@-XV5(8wq`rr9WthO`fse z6m!7hc3Bbhtv2V zMjv;azGM-c5$*Iq7{%j250q0@<3BR&pxnTGUNx|J=adrvf_RMOg~Bx#qSpLzk^4fO zzDwfnw-crT-QrMDII<>kP$&^cT%nlkU$R^s_K(RoTGsTYJGMIaO@8&S4?g(IU#8!D z^SS5#HS(-g6_pkJ8*)VnhE}+Pt70T!BlN%1@iPsE%xOETK5OZIyj*N@o3&lbH34<1 zZq9RO2htO^k#AwvZPV}>w`CWlmt@wg$t+1P$~Lx?D=S*Rjt>*e*!~PgE8gql+zM?5 z=)OxJo;?S4KzFsX6y-DW5z95MF4)k5mc#(c#)dP%j3R>pE(Qdyg|%|ftdytLb=N*s z+Ee`aFC z`}da&mmC%DtXnZvvc2(>&s9U=Xx^$ z+)!W!In>(6q{Kl(QlFGUtTy&-Bou&UQ3&23kAdW$3hA$zNyJ=ESBE^;v{LHpEJ_&r z3^RhNaCk*ZvFn|N&c_ncs8!xI zOen(ttdybl1tH|z(b%`(F(BC@kZ^#6 zAwpdzT$J&lS{X(IgHhDe{JNHF(uJr836V**gtbq+x~jZNvgHSYSE?ib!je@<>t*XtwtT0IuiH0q*ro_gRqpnvC3o>WprkvAeP^j7`#&`ReAWZX8!JzbcYz@RKv%eRru0Y^9*f1h{rjaklM$ zgO^+u^wc-JU6-_)61=(xtz8}`BavS~JbG$1F3C00%fmZuzIE0}4(%>FXtyuI6)JndojN3A^< z%Y`9a+pt#W(dK+xrk=$$rfuO1p)CPJa6{yu-lZnJdzNpchnUn^SrM-;vo;KrR5psz zH6^WUBQ;gnuIg$?ozAHwkIt#ku1q%Ld*d9`dC7>=wgk8b5R$B?2dqkqTSGpnSHuL?zk*j0hqkUw3?UZhLLDZRJX5JDIT2u$Wc7~@%ZDWEYNa7d zwtWawjd;BQc?uO5cU}JE7w4C?DJ@2&V_;&dYTAZFGnL70E4x@uxxA@y{CLjFEjxQ= zEm_|+ckY6^$f6-}+fCmSyArI#!8pc#a@u^x3bVboA1JM}10fdKl9AjXUuA(XBBie#f zRxJz-6s3%lOv_=6(6=Fbk4X@A_+t|ClC-9pRu5QDcatahI^*jw@lK2!tPO9DhR z#`o5^irOaya=5HSO>WjkG>Ir%&}qKJVAP*dS?VncY$S;iEJ8QLbOcj~ zU5=IrHX(q**Vy{ql6q8Flf0PwXR0o;&O^*q2Or}PYpP{ zGg|w%Y6Ck0HNC@!UgJrOen)?2T~-Ph-qQD;WbYFo1MN z*_D?lKgU4j?eo)mD02@yT`(<(VlI$*HD~~$@B_B;BCmGr_%<~IP9;^SqHsZ(u%m!3 z0gmsrg*RZ=;5G{BEJSHabzqgIScK32mo(C~R%V}Oh#SY5r}`by=t%!Eu^Ot>z zv@qn>?oS;^zWm{bsokoW+^eb)m>bAH9l$2;f8{`GKPlr{C2_s}W3iD-@ZH zQ}!Q9cBw5|RW!YU6${;0s>>WGW>-`gdoNbYa^_cOlH8a#tF*K|@x)7Lcg~hAnz+=X ztPz>W3rpaUSOAYY!<-QFK&f1lMDF?Aj?-yt4gk^5sAOE-> ztJ?PdcJoAI#4yQpPT%B-bSBSp4U!nw#ALa+q9j|eOD{!z%gR*K6m~LlpiNu8+sCZT znxEMsbd)ISHp$eAx7I$@d2@zH9qD*<6)2Z?I64&5zdN`A`yQq%2~y5Sm2+ zO%G=?ktdL6Lx`x9qxdBMzm}iLVpH5qxmQkC)sp0yGuhp{gY6t{t=QvTJs=wQa4WWUPv%jm=r${0<*=a2S|lRx;B30*D^A_ww5=um&g5d-Z(hPqDKfU| z9Yv*NS;HBlD!o6B#eO$llYTNv8_#2Q#(H~W;}{$)#>N;G0c%uYbV<7np%Q!z%aIf- z8J&D$#|}@mi~Mw^$0toeGg${eW~t>AYYB%tpkaiK#Y;V%;6vcH1H7CKBpE)>LjMN*BNS6U;}5#K;%TTd&@ir_Xy-RNRA&`! zlW$e~XZyqD?Ypbo(aiDcw*`(^QF&a*C2z`MbWH^OszJ9A@Iro&B{DLsn43(q)8yNt zUvid82ruk|FMpZN`c+%4S4v>_n8sNn)@I{JnJAtO(e7@F$7XF;qzox?14+2jYU@Xw zOv55x(zmP8vS>D|qtw~|WVOKDrmDV0W!1c{3xs5Z$-`#6nf~iv)2FeOr%!X`wRXb5 zBOmZA6An1l^}&+e;UeFzGS=z^`W=9&4ZK%hhK<27OM5jlxz7GOQ)b* zC1?Fg(mG+2I8bx)wY%-Yb*m1)Zff>6*9+!k{n+_=-9-%Aio%5Huocw1U^j zo}i)cQRs*KEsQOXAoJ2G>1xGw-KGU$Tg1OUdDdI3%fzIYB)LZHy}yrq`OlYMeiXa# z*em3>>mFab_VIPuwPe^Li1Xw;Qet4n*;?T+lS?Ncxc(+F=RsZaq>tWMYG{3`RytV~ z*{|{%v(*d8a{wo|DzgN8o`jSFcJH=r+n(JH5AwAYDIwnC{~b#ZB*f6(3Y*iA-;QjOxIf+Ojo2#aJDzDK{U6l0W?V>ETxzmhlKX94NkuZpJ? zGEH%Dr!Ec4Iwp~uOh#-AJ9qgxRk~pf&(~ukbX~@QxRLO!&I}9n5l?YM^^z2`vmQ>x z9QF3!0OGgwSDJE#Ll-lXH(4|>qb9^-$EDka5PUP#0xZ=qAZW(Zj7^U=ip@!F{|tTfqqAqRXv|-4A#c0m4uK{I z*~^I0IXGT6x0OHZM#iV#;rnn#bbpr0T|Nakqz#a}$->UXngSN-|KKx<(ZK#vqow(> z5D*6un4%ojkpNaEifZg?;5`_xC=d{_Da4S#ixVYH&1rLkVU{|Kc{kgebr+&;!+_vF7f$@@q-5X_a)a zCSm9{4uvfV|GMOHH!bMfKJvj~N8cZ9CH~iUzU)~4`z$t`CI5N1T@y1H-CPC(j=xU? z#DHdE+8r=e_UGqxF?A-lfSF!*Oq)68Vl-xq<X;e90-eZy?#F=)c) zfM0=Xu9c7vj#1hQ&8PH(^~+BrjB?HOY=f+tYOG0&0bM8@Cp3#TCL0JT($RK~LXn$| zeiH<_z*3lB>Jx<|Zz@x@i{*i8M@rjj84PJ+&h`F13Mtl<$jN;!4PT?v@GNG$gLn?> z56Z%kA^v2$k^PR~%$aB=Lmm^^xG|~oC5=2G(=IV4cp~f>&oZApXeen|!bw$Z?$)P{ z1R1SiBh1^0(_YN;#o9T^jD`@}oxx@4E0;f@cSw>W>mliv=4mao)yb#HMM=y+zTw;C z5;t!7uKgVArqvkhBi2gr27q2A&>wIP`V2h~OVt-Vu}O&AFkl+w;g8ySw4DI?_~?mr z4Rvs8TM5)n(2Fr*h_sA?qBV3HLM_X{2MJJ5rC zGOS7R-I9L`HB6jg*}>$|lXL+#oP2b)%P)rxC-ubSy5yR$TH#OKT&Z!nqN^X7N)SJ* z(d14z*x^po_$RWGDXO+5O03wF%B_B)Dzx<8HIJ^Fv8ztuSs(ZgA5PGF@4^_)o87FA zqhJ%$5I@~Z)gM!!t@B(q8V~%G9+Z?JrGwNJ&`^;TNz=Q*>+2@Q`VJ&8&|sZP!QyQvPBWt4wcruC6{>|Z`ZOc5f_ zDZMF;`3sSr2`xCnom?JlpoiJmG;v#{E_24ssmSPCAF0)r>om2qwS1L5(=zSD$h^v2 zIZTrEmgZ{5+cLrR-CbL=cae9byg9B~_hQ*2s@Q>0ZgjXIgC@~4K4EPWaK(gQ{wCobdDw? z){WdGa!)WP41K0)etQhs;cqtyBC3)oQ*;HBrRu-L?vn-rJ;ENnj!XU$%TP~WR2FrB z6Az7>n1Hn(8rBHNTG2j8)y4(Q_Q)28St(Ccl%;rzYO$;F`*ia53LSAe#HlVZc0OJz zt%aG3lWLL0R8lc_ z12~Y9qU9UFugwtednhk9KWPCtXa6ftg?P?R$@yr=F9$Kc=Gr3m1sGq7+<6#%xB<$< z=H@1wZVY_->X#$$ zeevsGZ^TMBy?Uz_m*MUt`DaaKRFahTYxPmR>T98vgM+8Lo5xE+m){quh@xW9Sx^f(y%jgC2hOAT36`%Q zX@)tICciZp3Wen`$Sw~AJ;s2vDAb@Naif{g zn*SkGF$jiv2TPDd7U&Y5gaOU;DJMIZHncokCkrVQ0!`O^wa=8TSbp^Wt?~5ADwwm0 zRV~ZKQg`)XmfV^(xn$TxRCtP$6=n9axed}{JehNca97&4ZUMQkc0k_M{9|%B&g#4e zW2GLZ`*#vRPagP3e*`^%W>0d3t`d~3LTKnS@>u|iF31>PZ*g2qMFIN*949KpOzF$3 z-glfJ(3mGGw*Ms448(R7cy&~hb}Q3W3&ozA0B&v94f&nX;F{P8jX0!AMy0gb47%Ls zo}UUl`6Qt1UwGjKP0>W~F)`W<@@q_R+h(kO_~PdHFlkUaqZW-F%nuNey+NP^tF97Y zl5F)1Mk=vsimCjllU`Y6ZhNdkSXQLO52`}z>wQj_#Gft4hT7hHuZ_GpRix!Y%4;ejLIbWd`(WPK%!LkF3~yF=f_b|+bp18MM)qJSZDC(sVzXVyS3PCwwKOa}}| zDB2hJ@ux&X%;*@6Wk6^tl{y-)KxPjX4FX>RcShht=)-i%q>mKGWC4CsA&y#Hxa12-qJ{Pr10avO+(qe$Id#u~SnIRMXc$J`d>D{s zF@vdEBedSwk3AMl&*jgs>By(CQtBCyu%sM9X!%Y8gZ47}ejYag!+&2V!>8M{+t#s( z3Pa!B6_Rp~q0eB~)m4y9eo!?hdAf`wO zjOqr*7!t$3LcQvOtiC0wZ#B(L#Agld;%j0YD^D8d(ch9*mEZ4Q5?$_AM`y3VjYcN4}}nVmk6wEJ8hx#4#S(AS@EZ zZ1T6oS_aP42seRj(G$?^`y~`oYqVvDg4{%t;m+F?l^SKo3Ps#pT03nCdB49{mV`At zJiozE!SYmC;^9D1V~Nuh(|1O&YVxAjjfvaaW9_8w(zC>q?qxyQ?y^sbtuc0VS8^m* zQ)y%#YeaUP2leTP!LGLeT}LtTPxYTAd>91)Q&y=O8F5gl6i_=w zt)Hq<0*H<@b)In>MD8ipgk^$ZYT6jxG4Oy!5i679V)8ralE(HiX|lIz(yugsAp5eI zpL~7HlYjwfoypG<(9<+Nd#^yo$S@TA?})X2OJrRX>(1=845zV=vgvy&&Dpa)R)47I z&1JHrS992}aT!bMrf*TOYBQN3QigrDuXlJn(btFf3`i!nu>yM|jL5FyOVOPY0czm23KbG`$bo=H`0s6`SMA$q)CG);{u$7)c_yYMWHx()icpy`b&0*Idb)Z^ zYsGTI)Jnoh-ryZ?Kl{p5!<2>O%h*9@uTy-A=Ms3?zCl;0kylluR5=zUX*|pgleOe$ zig2KXUuLxm$-6ikE|6C^d(PPHGi`57CG5{XkF6`SbuY8m9c_^F1@OS*)BwSst|p(_zd?z^$JM~1ME>N zLT;Bs!sMr&m!n#isf^-nqMGJZTV$fq&Wmkm-!GL$!dhJNzM{00_V&|fvp=Q}JmZL_ zY!2}f?1n|+uBlIxWzS6SoB1PpjC?P#M2)g{L|S$Xh6;BGI0To29rzKul$i0L_RcnD zxJ+jJV4gVOP%f-e_%!Kp(@lfq4Y4?lHdyx9`)f??d(W@Mcr3FrClRh$B4m)OI<|J^ zqASEp;{T3qOE40<@k`TIFt&B}bVEZX+Gm6?QqEGofC)N&oHyimfad!X#}z6ndFdSW zqg(`V7)36$?*yqw2T1s6HW#6}qLNA>E*iD;=sUpmKp&`J;cWtMe^K1fubtzuMWc&D zhwNcRG$Q(!w7l{;SOOWtw!wO2CHaXXfxVt2PaQt|%HhN0?N(!$rDdtV6v4>4QnOtQ zN>PYe4iks5?~?fG22Ju72fM7q*z-_@WSTN`O77QZnrFS9&5|Gd?svrC$O7yRvOD;f znKNhBPMRW7k5krvMeK~TP7GZ+Tf1|*5PPM#6Ady6$BP+2oC*q3RG~velIn!4>B;obyV&b^+4s{DXa^jh;}{6S|rX6 zfa+|}wZZU@d$DJSK*TK;YE5C`8`9zm+G~j_xw5sicw%>w#&4=1?`|f%QJ<$uHDw0j z;75mXk-N0Bvo3(yMIm`q5fHy2@QP!aVsft57&57U%ahPz4E1t8Hx9@v2v@8^S9Qe2 z>Wv!Oew~sQc{Fl+eYea{GiFQYkGxWQe4zhCeetv(?uOkoFBxL{|I+d9baD|kys$Hv zIn`czn9s_vZPJWLd`oF*e-_)9yE`FE-d0ZjE_op?>3{xc=PLe&hHn9>i!uAal?hu| z^OmFdF?;1@$R{B?LqP0^q;#m@#s4qPqXhM8%7~ityw)y4A6*~-3YH=^RW7BXBG8Kz zBeJPm7)^z=3EcfAT-R$D@;M`+rP2M?kfJy&dRLmLqW$gCAEh2=89!5`B6r1q`_V_) zOY?W&yLYd@r6?4PC_2~eATL?{0ZD}Spj{VL**_M@8F9LjzeT`}L%Yb=>Eta{>cp)+ zR&}MJ>xpW`6e)f{9yDj`W}llr@+}11DY+4Qe&l7^yy+$L=a1Y=tsf>Pt0viFgIIBK zwNc+B{MU>g@wf$F$9KgU(Y;v=>)*q;X$b5yapkW-w??QVS5f#J$cvvlx(Pe4<*tQ4 zNNnNM$S|{`z;;vu8DtVMAaab<$jVT`c|p1zHrTYK1_uk)=n1z3BeALbUKCjyc4s#(73lIG^Gf1+&mLu&K>;-@(&?$<=0;r8sc!`k0OJ<>R-q)UP_Veni0IOC z^0igdr)Q>4#fAxaxVTHuYtZvfo=guLLlJd^xs;yyzY=a~C4bJG^xU~y$>U~GRHXj{ zUk$B$8s0j#AWr8pFj1~`kdt0@mo{;mpV<)8rQU`G++k_DL1q(4Vt>wNcVxG0$-+I2 z_=56`05BLo$-Vd@Ieq+Ud3J~7F+E$$$w2^iF0*i9=4R>Esc~s?^4F0=DR%l;2xI2% zVZ^qEknTm8j0jV^2mB};qNYW8KWcPHY)tNm5^V6y^ZN7}n~%+&pqZ)y;wf_R3(OuT z5X%FvU+46z(T5?bLZk;R0skLeKp~M&yKHYw8tMmwG3)I9qN?%claI-zB~lT_y8MXP zH{irAFg$DUgrke22bKJ&O+#=$lvFg*Fuvy1^r|O^6>Dx=K>XYyt%NmwsjRN^#`wI` ziW7IXyd@3_#1MA|i;UG9{eFe*%N{^7>^3b2+G7osHhqek_9w?Jn9sTt_Mdw zXMoIbZMGE^U93m}!T0}@$(^nm>BI)Sre(3y9%q4@9E!(_u# zn;zS;CBA7>e2addOgC{wtF8Vx?{7w_pp<4NKQI-wmpQ5$$+>z}T%(3s6u=E?1UtnH zloj|Ux$aiY@NzbhGYmf@acCW~@Fuy`C^u%7ltVVDOd!85 zi4P_Oav^p@I^9x?6Onji#jF|P^<9_ZyONwZ0K#x?ZeVO31YJ=I*kccXjz+lRXloX- zZvQ>=2Sh`>7e+(_v`H9+0r>|%Vho}+itzKy3v{Af2z8;or@+g32f{UmhuQ5CTZ=WO zZ_x~e6fs!xwcQz#c!VM08&{i+%C0>*-3_Atx!czu!~@zA3xQo&YW_vx_s(!zex$EE4)iQ zGM6fuTk%Z^m%M~w%B-5!Jn|TKHra^Xy}HRCKGB>x$p^p)qbMO1+?KJO!UX0r-*cD|~{v31z!f1brvUfW0Q%ASeqbfY=p_-THLQ1lc z7!$un}=fyJmo2?2jEEb&8*suF#gA{hJx(mUZf>NrqL316q8wV!fkpCnxk z$-f?RT+`$nNGZe1Chs;yC6PFXy>QPN8vERC(*9g$C*IYyqqB2YXQ|X{oYiV+ULV{( zO?9qeBB9i;*sl_$1aho?KRc;#xcv@{4WW z)cj7-?5;VQjK`%=-fPx&;Raf$g>sD2W9pVt$;L0 z((`>SsHp{32aTFI>Lw_n16|QQz_(o#(~ge%!dKxJbSfMjmYBX>H`(Wj*>4OTc7=7} zD4X-13@^v5{zOuzbQ_dGv4#g@;U8wnLtp=a)cyIETaI!~N##=P^hc_I?VYqwIz`~I zuDI*2ks+ai6=Ep)8^z2hQ-EDcC(qOya%Vj3nkGZ_8WZCejoMgRJ*RB`qaQCc zg(AZFgw)ewt-f_XR`L7fgb5=z6T~c{hZ89(H^u?&!ojaID>q850aib8EWEvxP8`Iq zEE~_94dr;mmk5vI5ajlCpc4_6dcG!__hbJf3q=uwq6H8TLvg}rDIfBBsAovZ9Q$6r zu$9-02(ALHpoB8jbrWF7PEoXM7Msfg#ri5`S6CZ%Eca|M729+X;cunEI&9toq1jX} z!cDUC@%h-?xFQ^E61MmZWQ)}ouz80JzM#Xl0LRI<&YwR|zGhK{l;&SSGCIms@)xlL zv?vQfRJwQ0QCA$bGrObO#@}T{Q{;2|6(Ll`#~YpeD zx6^BZAM2<{0AVAn{x8wj7+VhsXe8dC6R(n!%9Da*=Sf2eH%hLcq(2a~AnTw|QCbhp z-XblKw>>VztD{Z;vPW^ERpMy3CG}nUnMQ9cx-@u7D=pT?!otrbi8|~&Neg`ao-9(_ z#;dcM1$SXgRX<=4Sd&fi>a?jL%Q>BT3)9^^Ve%Q>`bjfW_ovaO&li#dJ)1`E_DSO^ zyOS=VrMTQ1A$CZKWY~suPao`bA@JjnxjF7*g&GZgkK{xpZe{6am0gjkT=;o38e;uk zYnFsqBGD0vR1jzL7;cweAJq6viNJT5OMdxZfdEVpX}!5F{MQp<0a|CyX(ZQGiMwaB zY_oSJ-m;9qT6s-Wdqq_c+;Mev77o%JW+66NOd@$5a2&4tw3Hkjz{g55X z`(gzZfC66XHmFeRzk`U5@;uu0f_?!sz@dfGV>;jv(NPN={SN>6o710U_Z~m~b6PP} z7nE)#M5}V!-?`cnf$^o^pTGPkk+9w=nZKlfS{(fwLgRidVbY)@{FFk<`S zSNghR%zKDuAfDv`MnBrC(*r(I4P^h(_VRuzK3Ib`i8w%YqNYsL6gJ9~(n6e?-@H27 z2Ij(Cab(2`#teRpWq7y>WWJzx8uSJeUcpMt!beo<5Yn|szqqKcjB4D1J4?SnqtRs^ z=Mvm!T{p$GXp`pQ+m)OWw&LBtuoyOtb$pSI#-LsPKY^nUaDKbAv!f=!LJFNziz22F zF8N+uk;4w{dhY1a=e9C;Vf{xLJFLcb4ad*7GueLLFf{s-GsO}`F@Ox5hh$BJtq~4G zh5=B$4@#11n^WSA-_^daI?{OCH0+LQs0mb(-3H+w%xbl@R)h^CQUErPJ!NMBL{i_y z5_!(5tlFdu7&ASSBJ0+zBVP)*eH%6eitWu)65LX`R69kal$gZ9t^xA%c&1b`t+6Ar zJIg8A51~c-5qj~~2-P=$<^fxwMvZos<|B&@`5N`LPAbZ#A)*7DBNm5eKJo&#rWii& zRZzojh*|&hf}DAR7syZ1drUAvsg=>r0W%GL1MMz(z~96#Gn@F&$S2P}Dsxsv&!@lm z+uu^}zyDS0L4T?BaK!4KeI8?ah2F_dpIkn|7h5_ZdDfyEfUZ^7V*gDH4wEAY@qO}$ z&Bw&C1+yH>;so-|$3M#8V42 zCWUb&E)A-Q%u4dpx<2VZ?f2oGSr#!&VDumicnofy>1sE{G|gUmd3B^U2|5PZ%HKk* zrW)cqgu3s5>@4z$^O^?X=pnBHo`{tTykB5?6h}Z;sF7yH(cX$tqY2#4M!S_@$J~Fs zNx5|}UPQSbpjyBcbU})ao&UvDb6~Fw?^q$8l#W!?&54^sfz`fe=>2R3?*+BObkO3KapF{jNQCevtNOJscnF}^$l&SR*josvqD#|K`Pg1qNw|7tX-kzS+ zyCbi4r#5^?0cB z^O3tb8WtbM(`?-LbgBdgMlHF#1PHq-T|JjUM7B=Ur?*GxEn!pl{ngM#xx7=U(qO-m zXc#%VT0*{QTNKv1R(Z_49YQ5%wNW4$;K|t9Ve;W;_kI(6Vb8Ov70IY$!c=)M4O0l( zK4s7>^Y1i&*MEkS+>c|-&V$U%g?zsJ8LFQ^4*9VB%o@V?fV==Kdo1WsL@l5XyhcJv zv0GluugSS9Ut2}488~EAoJ)*W6I|aAlJn*zpGvM+k$mdk@^oDB zku+DaSln5HYg_eGeAb9-N$Q{jyO5aOWNKJI)C^!H-$Uav?`$|TBmd4TaM`fNR(Y!YG{0y+OZt@O!AXNSA}xw{BL6Wlic*tC?=SAh`n*~&ki1>bYKQ@ zDK|n+AipybVdA2mDU?f@T<9k!^2E6o#Ui5R$Jsy>p(c$a65c^!01yUQoxEhij8;Kd zkx#*f!VIMZ$50NjBj`e)@HlBn?^a<$FFwP+EL0Ra&~n%o_Syl_@0p8V#Lr_ld_vr+ zeUhBZRb*T&8Q-MAf-$$#SSsT2-b|8DCyz?FD^R<|0fP;9s=g6vp%q9g8fFXV6#k_L zE5-Q9lGWV!^o!c(N?5fi^Cv^17EMpD#`}w*BN_6x>K-hVA=fPKicLDA;H#=_9EH%X zvm}Zg_WwuNdjLpPUj4)S+}WAg-RZsey3>2F+k0hqVOh%3I|5RqDWaetRf_a32-vU% zVvI(kzUpf%F_uKJVC*FtHE+UPzVqDKrKtaW7iMRt?!C`B=XcI?YCZWcgNhso2GLQ6 z@ry~(y7j^>S!l)@W=5dO7r8ct=;8J5hKUnlJ#oC|9qp5>ZZEOV9WKF@tUg) zD5|3hi1DzZFW&!*T`8t5+O!Z=fUOW^wqDbQWO(n@$*mkvho#--{(vrQy~(>*BJ$ge ze$KaoNFDmDPL1IF~@lJefZ=J`P)An8HjwpTVU8uyhHb_53^(tZ*-qDL=Cb+nyE+5E*^N=i*Yr>-I!fe+*=cOK=j!{shfSQje155NC1Pz}6X6x$)| z37EC$4S=jQP&>fFm?P22rL|#qTD!pOt-&_*1-TQ2NRs?NsHLo7<+Pd#@;7C>R=mLpEQ=WweWnZ95t@DPqMKZFiQc_g zrEVZ=CY%y&U5Kil?~+FDNfBcu60OZ`@2HbXp6;x zb@~P=sPxi1v!V5Gs7nTOiO{oO~ zEFD_V;&7@g^tlCd$TIZgP>Krnnd6nkJ1PniFa(E#kRmGs%%xtTjUmUyzL6zV7h)HG zU{{XNhSaUv=_XTP`iwQ&pj{MZzs7G!p_gO{vCWyBX|*#e+Y}wCSbk#4om<>ps!(LC zDyYqjm(tu}a<9w-Lmqun^fGw0gW>?G;;2i@m=UuGZM!p>B*({hB@0^=f(XNKGX%Cv zxx}d(&P-)5crYGH?W>lhH+vX~8r9QV*wgtpb#PtM#K1(hIxjZ6tI6jSI*C50Q@{U| zZ9=EM_d2hTqcrPu+*ZGJtDzURhhz1}byFcX?mF^B!^cv=xl*J26dg%=;1AewYEngot+~>~`aj6)*v;gBcJzD`pc983EVY_t);fiY$$J znOaNWV9eBrNd?x56+f_Z!>kY62}py14H`8{46@~Dptt~ki&-&N8~%pk4h>sWm}Ind z)5t&0Va!0mc^6;jRrVW0`Yz2}o!2*K?hZBI4^t+EU-ILPgevUu31ITOPU*Lc-sMz6 zi^YJ*T8=gni137~Jo;8z`SfY(!S*s5u@F6Z8%J%^i3lP}J_PQX;agBD=LeiJ@;Efe zi!h9oS}`fAmijD4&#m_SI8#ht6=cY+dbne64t6r&91e_KK#0pby`m0f4ha&F1+=cMg;Uw1imJL) zQzQ=W)WL1?a*xy<;GXBln$YU(7ypFIY05hlX2D++0bPwDt9PdNHzJ8jV-}Jt)~#!7 zY($+;d|UX(N9WF^o+!Lw6$hpIzks$3LJ8vvW;qchpyNXmugsgJd8dG4?Z?6+~}F#8H?RM)mI;^^VNh<%iv(g;#|kncgC!n{My_|WNy5@ z-8sLPJ8sJN@pU2SLKVLh-=L$}o&cUN2ON3=@O~s{$f=Z1=X&6DJgpW_-NdV48pf0% zHjPB1SDIP?^Wt|zVL7m`mulY*)&<4;RI)>niDkH$v?v$Rp*!Gh5Dyy#W3c+zW5LAk zON>R}0Fqqg{lR>H;(;yuOe3@!%QTBXWtB_H5aFYh+5#z;W!?Pw^Jpg}+_D6MF{lwG z*Q}AcxdtxtpEO=YidG@~^5Xp*%LsK)-mMzzQv@6f1AFuyww2}Od?<)kp-I#BLL6sW zXxiq1LM@al1k04{o0+TtP~f5YA8i)~rCvo^SgjYAN#q?BF@4l#3nSao@FsVxHpJpo zpxC69OX@`N1ZPg3SGh2^WfiJndyQPSKKm2QxUZ7Ops8_(Pt|(1PCQL$Ug_d_^r^b( z)$`}izh?@65p|GbVK@yVvN2d_++BLp1dK&V*bkoqy08F?LV!eB$T;btVnl08HkRxJ z8>R>%0NT(fpXyvhQHV)7kW9FRCKTy{7J&j?pb>R5L6sbocj1!yAaFYf!Bq+zxEcfi zUbn%RT9XtuG?(|%SK1R|g)(G(4wds1y%tzm7yYS!@VM;uCS`~vLN!qPQvOf2B*JVl zDb^4s|90{@QDeIh^_GWYbZ>wN?d7zx@0@lUbSmf=IVqY7pq#5FJ0QNkreGmYt=nDYQOUeIi^cRt&}qc@A#3vwcNR_ zVdHq~BD2Le`}%`I)-&kU!Vd(E+=x8nYtg5kYQEAMsUC zEUe+Jqf*ad2*%^7^&*ErnaRy(Cbq4UrniUbzL@sp8h%iVLWSlu&6nwZIXH#1{_w+7 zn>LN}&2yL5B}g;?J9My2tZ{{3b5uB;CB>k@ma;pXKGi)|uzt|I~OqgVj zpq{4|>R@2Cn1h&Feo_NTq;eM5Q7h``2(-7$G#q(FVJi>n#@Ut*ZAja$=+parQzqQb z76rBH5Q9NX%-j5yw9T8BW%K^ovlR)~#Vtxc;WY&DFbZLYhV+zdDxsYR=Y7^O+u$Sg|7ERN8nt7B8BgRThiyt`%49veToZ{8 z^CzfmN8B@J3Rp8R0T7#e*f)%XX@=-#VBP0E1JpNGZU^wWwX{$KziT@@xGL$=%5yuZX z89i!w{oaY<2_o}Jen_8eTCnjq)bZ^@a6Gan!fjjqFBnnw@Ym_#x zJa=Og-@V9pep&~+Z;&y1Qd#siimzXwa6}2cXogXADOCHd0j?hc1-#M#q|{LAZl#u~$63XiYW}yY%N6`b`4TwRf-ZpG`Lz?v!JqSMKE0V!1>Ot7~7QfcUq+^m4 zzl*3B8EtLQK1z*fRRXUuGebGMF*ZXtDIC<3v*{fb_5r$mWaP2%&5>0RbX1s>+U0S< zZkj%>wR6Z7j9^72&kKDnxF(jX!o))vvDe+r8mWfrzu#4CS@~Ak()m_#z5AKIq@nA9 zRp_W+rLS45OKnGLW$$z3m#fywtM2p?LrRgZzI?K6tIBv$277Ptc9(W*KN_$sPO7%npr?Cc z6Jxl}3hsKyvuIHwy{}rC-{gWR2%3lXh?~Un=Vi#J%T~=eJCXcs1RSAmIFzQ@Vg|R2 zKfCL@#4T}pbZZC^`4fy4vp<(KX~Mm>`-D}7aIay=cW?^&1moNxR|%f66dTHQDOPGl zuhonczQF1L!iex<7eR=!C$7nNxm@dDxm?Sho$^AB(2X$$_ICl6)#Sh5E?&z$Ak&n2G4!&p53AAnC1?-7xidH&G)>q z%4N03Oy-H9aoU8gQXL^vDn(GL{1iH>;%S(-FxZiD1|m{zOIV@U!CF~nkB!+IRCGMw zDxM(;iROsJHZ*cD78`u~?ZKUM@bOk8F~StD9FctM?c`{8)X|rMAw`CqTe)_vGUe9U zQ^Jn1g1(Xe7ug(PhPOvyz}gxuVmq6&mqnUEJ`}4 zJf;n@tni)HkHKQNxD6{tiV#Ea!e9=Y&cpFq3*KV_Cg~t{gfw>_EhX5!@3 zC~6}=d;fi6w97fiCu3JeM%`}r>z3ptR(;+qDVMU;1_gQFS7BRRqJOJAnw-Xn*jjVt z^fXP)PqDcRYUy2Zh=MwPa2jV8A}5C{nM0?`)F29F}&T&Ev%18r@mrD+v;{%;TX zI>GqGe%d@0lfiQYaHZxr@ODq9JT5=m(HHp`78($mS|Cc^i&hA=V#8Ph_qd}BwV9+a z1@C@_u!Tmw6HG+B0hK{w^3T~Bf4{ArTiLGceNA`2os=o%$TP(c!TRBZ7Bc zRc|NP>Lnqi;&Tqzp`cOM&{?zyq#6Dcgehe5G&GjZrPN&y)d?nwowNNSrd*&+RxKn? zg|Yf;Ub^W|WhU}DSn z(M?fi@XiE;T-Sw5H$hwViV_dhBH^4vDWC?9EEFT&^PmC|`y7YOIJ{Pvn~T<6^s1++ zrNQ6;RYer64ly*`71t9a0T>te;|DLw6-fX_rWSn2n}ySo@FEWfyfM!MG!KmOPMa4T zGsXms$1Ooy{a*+Dy}iMeHv|SWjqaeUzVN>sYo9Hm=(J3zvxhG7FFIL$(GlxnHJWAL5tW5|1$w_uOr8`NSm6pIZz~TPwsX-~`z)W)JHu8uYjDrf zp|5%-CT7o0=rs|O)N=Ixrbbum4E)5zY%tP#@3F>|GaBACLLT1{FU!}fdw*Le>`+Nu zOP%6?Hq|oYf!H(KBX;PrxjOz#BOT z7k5}7B)B93---!i!-hFFgrE_LAeElRvyD(k<=vV-yE)`o6x<`^l?FHteu!)A*yv1n zZt2z^9#^@(6Rjh^N1R8r=~$1eszcuSY_goKj!BQOg(JEh?&x^xNe_Bl-QAwh^zjYuIr}eMxOeg5MdZJ8 zb9gl~JHqQC%*duN*?$Lp&3M3+G~_H{8(j&yv6ITQ83#Kb=%E8^jMdl`)&(R0Zl@Tx z2ohi#7L5`NB9!_AkzfXKYaDQR;XxD_*{K&I0`Q^R0HVRZfVdih->!IU;VpeUL%TYx zY*+N=IRWJc+XjQrX!Z;L%(t}=U){K9QRMpTZ|cxm7qiQQ6`bp>>Sj_(l-|F_CyV<< zl2!I{M`Q8}Pj!xbtQB>hS)#HiD|(u_1OCHE_C5JV`plV| zwvL*9bK$|)U!NS>7GoZofN0ZhC6>>l{Dc1saf)uRvvKxf(I38{o8dPVhrs|R!}3~~R9O@(8D8B;Gy+#C zfjL~T&AeiNUt@j)Fu`{ey--%z^ix!!z}#v}%fvLqD|OStqDmTr6Wz`fXvGe#2YSJ< z%2>=Yx|_~o>69`D*J{=UEb{UXOJQjJlMN2Pu;SCWC-{=iOinv-0t)YG?5-BgLbu~O z#fUVMR=9ZJKxKP7ic12XR#t;kLq5v9Y0WETD`#pNy59@lpg8pbT3=Qem$n7j>kOJr#rAT(E!5P;pW0TfEvEg{vWpy%KQU z3goB4Od4p!QPiT>TH{4cJR~(1V0xfa(T7{SlTd*_uwLjuWNhPLQ^p35K}>+eOdv4O zZb%D&4ko_cM4C4RM+FKq`L+&ySlOZM_o;mLrH+lpfG`x|zt3~GqZy!H1m3nzVq5Ufiu?e%a%PUxr-bZ-uQ(aH|8`n>d z9XpoXA4=|NhRpOzdS_JG@nW1mQ4+XI3dNYoWy`QJKH{bx>iE6ygQ>yb)DtI`Hf09~ z`BVD83vP(vcqy%UFQoL1#!0>`z?nw?XYkB9FHAGQlf<*IiWq4YU?0FQ(1wByCIvTu z%CIf~HrODC8>}JJ@q;X2xB=*)!)>@ZF9;153##xRuOWO5NE%*R>I<(nVupi7og8bg z)~D=JPO-~Y!cl3P(Z@@x{xa zm1T_1KtD#}hwG%70~SVuN7?pZGdJXx&gmCJi9z%187m)Z2!$FRTDfJC)9LJUIGyHv zRuNU#R;8;f5nVp{aj3b<%n9SZLBnv1ldK3CjH>V<4!E^Am z0jpUUW%CO;j$wu&q3SbDb8BOPh0)!#QZ6?j2}HQx3fu$e?uhiQ7Orx8{?{xk2MNefmH$J_e>^_){hj2c z*48f7SgBbh*pT~X!d=YJmiQ&xJ|koO=toDwBnhe>^JTDIFt4{m1=jYGJtap){jh;#W}5 z45=I`gpCAOee7zep@;-DYT{VIL$7F;k1`npu9==KB^0Zl| zDRXNK+MqIFRB1zE<@JrGY{!%TBy5*To=x|wos**EgAOFn2uG(j3+;NbPQ^1wm1gOEdSgs0*2%<1nTqrnWPXMARkoZNV8~d@ zSY=`8{`5mM1ł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

      ^eB&ZMBf#O!D z^majVz1KTF(h{5R=?lm8IiqrBpf)(vED^^vYSbxjR$22xQe7&gYHQJcc~Bmegyo1Z z*QDd_cp8c_gfilvk?`b|mPVI>xVKrF+nH7yr5p=8P*y*KmK;5LLGi|k(s+*is@5A+ z6!qS>y(=d8Qi0a4aCxk=p)=Z$)5i3X(&R4h-yRb-#0Ar4p|1wpb4K0sIW%f3JPX`U{xIa_I^isNfh~fkh5-5ndyGvB2S1E+ zeHnmWNK=ed_gM6Z5P)}8e}<@R1cjABVL^M7_$k24P?nII^@^ryG_nj^aNSb2vSbr4 zU!GdAQe!ZfkizUqbIid+iyrk0r;#fvoRj3jLLpgGL-16S8};jxs;TYf=@a;O+=F`5 zhG@Z(nW2ESHXA_Ppvt7s7v#}A!kvY1PVNf#`y3=u+U&N+B$ZTkK~ zVSVu~(8`757TU7q&6)mSB00aY(B)3Vje{G6`e>mw4Bn74W<1nt@hp)Tc)@lGA1$J% zI)E4VWTF}kIK;Cy9M&HL5omba5nTPjRCj*SHMUKzGE6mir05NqS$a$5H4|Y4^86RDdk$%ntMZj;e?gkJKlhs7C;rrT zd7~(8xNIO`jj413!y&7dt5l_>im=%N6Ouk0_g`0d(r&p;oak8C=}koZ`e?5~E347h z_$%ARY9UiX$czd{A=)HwMM@~ZZlz=Ixer$9AH5})IFNa%$|8SKVN@P?R0eHy<(m_8ey`g4z zMy$AeYABvWS5J(@Gj@d~5)P$>60%Qh7iHOq|Gfxm@a{RQS3@8*-ZZ~l#?)ZB6lmG5{Ta-5wYl@ukH z;mz(C718%H{FK4b0sO&!3e@6%$$fh8w}!JHAQfChf_8t*i^Pm%^iW&=hL4|qI{(BI zpOSyAkD@W9;A&qWNnX~&ZMr?oIk7elp@I16fAAKrDsF)bug4A%V zl){DrXiB(>R82R6!KLL!$u^-PF8<(ybcdRqWC$1x{@^FiW%i`^W(Um9v?BQdTe)`C zs^rj6he9x?R7%1;^Go-lDSMvZo8B{ZhCCdYDV%0D^2zVitp;b8+cFoe%@GS)GfS72 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%0D${f9g$sswM}!NZikMS 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

      cf4*SFsw>q8#usPq{n^j5@4kC7J-+bl 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

      >cmJ6^}|U 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 c=yphj`8oOAi4KW&sw*d_ZF9{g8-pj$hw@ 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

      { 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